ok

原则:只能将一个类的实例赋值给它本身或者它的子类,而不能将一个父类的实例赋值给一个子类的引用

1
2
3
B extends A;
B b = new A(); //错误
A a = new B(); //合法,但是通过 a 只能访问 A 类中定义的方法和属性,除非 B 类重写了这些方法

关键字

基本类型

  • boolean 1
  • byte 1
  • short 2
  • char 2
  • int 4
  • float 4
  • long 4
  • double

    包装类型

  • Boolean
  • Byte
  • Short
  • Character
  • Integer
  • Long
  • Float(没有实现缓存机制)
  • Double(没有实现缓存机制)
  • BigDecimal(浮点精确运算的场景,传统浮点类型计算时,会出现位数不够的时候,计算机会给这个浮点表示进行截断),计算机x86一般用小端存储,高(位)存高(地址),低存低
    • 低地址:指的是内存中较小的地址值。在大多数系统中,低地址对应于内存中的起始位置,也就是地址为0的位置。
    • 高地址:指的是内存中较大的地址值。它是相对于低地址而言的,表示内存的结束位置。
  • BigInteger(存储超过64 位 long 整型的数字)BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

https://javaguide.cn/java/basis/bigdecimal.html

装箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer num1 = 40; //发生装箱,相当于 Integer.valueOf(40);
Integer num2 = new Integer(40);
return num1 == num2; | return false;
解释:num1 直接使用的是缓存中的对象: num2 直接创建了新对象
// 自动装箱函数
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
// 自动拆箱
如果把第一句换成 int num1 =40;
num1 == num2;| return true; //会对num2发生自动拆箱,会实现两个int类型的比较,返回true
// 如果要用num2.equals比较:
num2.equals(Integer.valueOf(num1));
  • 所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
  • == 操作符会比较两个对象的引用是否相等,而不是它们的值。因为 num1 和 num2 都是通过自动装箱得到的,它们实际上是不同的对象,即使它们包装的值相同
  • 自动拆箱与装箱的例子:
    • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
    • int n = i 等价于 int n = i.intValue();
  • 如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作

    访问控制

  • private
  • protected
  • public

    类,方法,变量修饰符

  • class
  • new
  • abstract
  • extends
  • static:
  • final
  • implements
  • interface
  • synchronized
  • enum
  • native
  • volatile
  1. static:static 修饰的变量和方法可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。

  2. 静态方法只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

  3. 【为什么不能调用非静态成员】:在类加载的时候就会分配内存,非静态成员需要实例化后才能有效访问

    错误处理

  • try
  • catch
  • throw
  • throws
  • finally
    • 当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

重写 override 和 重载 overload

  • override是子类重写父类方法,参数需要一样
  • overload是在一个类中重载的某个方法
  • 构造方法可以背重载,不可以重写

接口和抽象类有什么共同点和区别?

共同点:都不能被实例化。都可以包含抽象方法。都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。一个类只能继承一个类,但是可以实现多个接口。接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值

深拷贝与浅拷贝/引用拷贝

  • 深拷贝:完整复制某个对象,包括这个对象中的内部对象,都是不同的对象
  • 浅拷贝:在堆上创建一个新对象,这个新对象与原对象使用的是同一个内部对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    public Person clone() {
    try {
    Person person = (Person) super.clone();
    person.setAddress(person.getAddress().clone());
    //加入这一行,内部的address对象也进行拷贝,本质上是堆上的另外一个对象。
    return person;
    } catch (CloneNotSupportedException e) {
    throw new AssertionError();
    }
    }

Object:基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public final native Class<?> getClass()
//native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。获取哈希码(int 整数),也称为散列码,也可以比较两个对象是否相等。两个对象的hashCode 值相等并不代表两个对象就相等:可能发生冲突
public native int hashCode()
//用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
public boolean equals(Object obj)
//native 方法,用于创建并返回当前对象的一份拷贝。
protected native Object clone() throws CloneNotSupportedException
//返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public String toString()
//native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notify()
//native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll()
//native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
public final native void wait(long timeout) throws InterruptedException
//多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
public final void wait(long timeout, int nanos) throws InterruptedException
//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
public final void wait() throws InterruptedException
//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }

String StringBuffer StringBuilder

  • String类不可变:创建时,采用final的字符数组表示字符串了,final char [],(java9以后用的byte[])
  • StringBuilderStringBuffer都继承自 AbstractStringBuilder 类,也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

字符串常量池

是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

1
2
3
4
5
6
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

【面试题】String s1 = new String("abc");这句话创建了几个字符串对象?

  • 一个或两个:
  • 一个的情况,abc在字符串常量池里,仅需要abc的引用创建s1
  • 两个的情况,abc不存在常量池里,要先创建abc,再将其引用创建s1

【面试题】手动将某字符串加入字符串常量池用什么方法

  • String.intern()
  • 将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
    • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 在堆中创建字符串对象”Java“
      // 将字符串对象”Java“的引用保存在字符串常量池中
      String s1 = "Java";
      // 直接返回字符串常量池中字符串对象”Java“对应的引用
      String s2 = s1.intern();
      // 会在堆中在单独创建一个字符串对象
      String s3 = new String("Java");
      // 直接返回字符串常量池中字符串对象”Java“对应的引用
      String s4 = s3.intern();
      // s1 和 s2 指向的是堆中的同一个对象
      System.out.println(s1 == s2); // true
      // s3 和 s4 指向的是堆中不同的对象
      System.out.println(s3 == s4); // false
      // s1 和 s4 指向的是堆中的同一个对象
      System.out.println(s1 == s4); //true

【面试题】字符串加号操作 str + str

1
2
3
4
5
6
7
8
String str1 = "str";//创建了一个字符串常量 "str",它会存储在常量池中
String str2 = "ing";
String str3 = "str" + "ing";//这里的 "str" 和 "ing" 都是字符串字面量,它们会在编译时就被合并成一个新的字符串常量 "string",然后存储在常量池中。
String str4 = str1 + str2;//这里使用了变量 str1 和 str2 进行字符串拼接,这是在运行时进行的。因此,新的字符串对象 "string" 会在堆内存中创建,而不是常量池
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

Exception 和 Error(两者继承Thorwable)

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • Error:Error 属于程序无法处理的错误 ,jvm一般会选择线程终止。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Checkable Exception 受检查异常,必须用catch或者throw捕获

除了RuntimeException及其子类以外(下列),其他的Exception类及其子类都属于受检查异常

  • NullPointerException(空指针错误)
  • IllegalArgumentException(参数错误比如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)
  • ArithmeticException(算术错误)
  • SecurityException (安全错误比如权限不够)
  • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class A {
// 使用throws关键字声明可能抛出异常
public void doSomething() throws MyException {
// 在方法内部发现异常情况
if (/* some condition */) {
// 抛出自定义异常
throw new MyException("Something went wrong");
}
}
public void doSomething(){
try{

}catch(MyException e){
e.printStackTrace(); //在控制台上打印 Throwable 对象封装的异常信息
e.getMessage();// 返回异常发生时的简要描述
e.toString();// 返回异常发生时的详细信息
e.getLocalizedMessage();//返回异常对象的本地化信息。
}finally{

}
}
}

泛型 Generics //对比CPP的template

相同点:

  • 参数化类型:两者都允许你定义可以接受不特定类型的数据结构或算法,从而提高代码的复用性和灵活性。
  • 类型安全:Java 的泛型和 C++ 的模板都在编译时进行类型检查,确保类型的一致性。
  • 支持容器类:在两者中,可以创建可以容纳任何类型的容器类(例如,List、Set、Map 等)。

不同点:

  • 实现方式
    • Java 泛型是通过擦除(type erasure)来实现的。在编译时,泛型类型信息会被擦除,编译器会将泛型代码转化成非泛型的代码。这意味着在运行时,不会保留关于泛型类型的信息。这也是为什么在 Java 中不能直接创建泛型数组的原因。
    • C++ 模板是通过编译器在编译时进行代码生成,每次使用模板时,都会根据模板参数生成对应的代码。这使得 C++ 模板可以实现更为复杂和灵活的类型推断。
  • 语法
    Java 泛型使用来表示泛型类型,可以在类、接口、方法等级别使用泛型。
    C++ 模板使用或者来声明模板参数,可以在类、函数等级别使用模板。
  • 泛型的通配符
    Java 的泛型可以使用通配符(wildcards)来表示不确定的类型。例如:List<?>表示一个不确定类型的 List。
    C++ 模板可以通过模板特化来处理特定的类型。
  • 模板元编程
    C++ 的模板系统支持模板元编程,这意味着可以在编译时进行计算和逻辑操作,从而实现更为复杂的类型处理。
  • 依赖
    Java 的泛型不依赖于运行时类型信息(RTTI)。
    C++ 的模板依赖于编译时类型信息(CTTI)。

反射机制

  • 代理机制

使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

  • 动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
  • 静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
  • JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
1
2
3
4
5
6
7
Class alunbarClass = TargetObject.class; //知道目的类名为TargetObject,直接获取
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");//通过类的全路径获取
TargetObject instance = new TargetObject();//通过实例获取
Class alunbarClass2 = instance.getClass();

//通过类加载器进行全路径的loadclass
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
  • 静态代理
    可以在代理类中增加方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface SmsService {
    String send(String message);
    }
    public class SmsServiceImpl implements SmsService {
    public String send(String message) {
    // ...
    }
    }
    public class SmsProxy implements SmsService {
    private final SmsService smsService;
    public SmsProxy(SmsService smsService) {
    this.smsService = smsService;
    }
    }
    SmsService smsService = new SmsServiceImpl();
    SmsProxy smsProxy = new SmsProxy(smsService);
  • 动态代理:JDK 动态代理、CGLIB 动态代理

    • InvocationHandler 接口和 Proxy 类
    • 还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。
      1
      2
      3
      4
      5
      6
      public interface InvocationHandler {
      //当你使用代理对象调用方法的时候实际会调用到这个方法
      public Object invoke(Object proxy, Method method, Object[] args){
      throws Throwable;
      }
      }

IO流

数据从外部存储和内存之间进出的过程就是IO。

字节流

如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。

字符流

字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时

序列化 Protobuf,Hessian,Kyro

  • transient 阻止实例中那些用此关键字修饰的的变量序列化;
  • 当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
  • transient 只能修饰变量,不能修饰类和方法。
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

sun.misc.Unsafe类:并发工具类的组件,一个直接操作内存空间的类。

主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。

  • Unsafe中提供的方法需要依赖native方法,Java 代码中只是声明方法头,具体的实现则交给本地代码

堆外内存

使用方法

1
2
3
4
5
6
7
8
9
10
//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);

这种方式分配堆外内存,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作,最终在finally块中进行内存释放。

【面试题】为什么要用堆外内存

  • 对GC停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
  • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
1
2
3
4
5
6
7
8
9
10
11
// 单例模式的应用:
// 利用反射获得 Unsafe 类中已经实例化完成的单例对象 theUnsafe.
Unsafe unsafe = Unsafe.reflectGetUnsafe();
try{
long maddr = unsafe.allocateMemory(1024);
}catch(OutOfMemoryError e){
e.printStackTrace();
throw e;
}finally{
unsafe.freeMomory(maddr);
}

DirectByteBuffer

  • 实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,在NIO中使用广泛。
  • 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现

内存屏障 Memory Barrier:组织指令重排序

阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况

  • 屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能
    1
    2
    3
    4
    5
    6
    //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
    public native void loadFence();
    //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
    public native void storeFence();
    //内存屏障,禁止load、store操作重排序
    public native void fullFence();

运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。