一、基本概念
1.1 JVM / JDK / JRE
JVM 是 Java 虚拟机,JDK 是 Java 开发工具,JRE 是 Java 运行时环境。
1.2 什么是字节码
狭义:在 Java 中可以被 JVM 理解的代码就叫做字节码,即被编译后生成的 .class 文件。
1.3 为什么说 Java 是 “编译与解释并存”
Java 语言既具有编译语言的特性,也具有解释语言的特性。Java 程序需要先经过编译,生成 .class 文件,然后由 Java 解释器来解释执行。
1.4 静态方法为什么不能调用非静态成员
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。非静态成员是属于实例对象的,只有在对象实例化之后才会存在,需要类的实例对象才可以访问。在类的非静态成员不存在时,静态成员就已经存在了,所以不能调用。
1.5 == 和 equals() 区别
- == 对于基本数据类型来说比较的是值,对于引用数据类型来说比较的是对象的地址值。
- equals() 对于没有覆盖 equals() 方法时等价于 ==,对于已经覆盖了 equals() 方法时比较的是对象的属性是否相等。
1.6 包装类使用
- 装箱:基本数据类型转换为对应的包装类型;
- 拆箱:包装类型转换为对应的基本数据类型;
装箱和拆箱都是自动进行的,如果频繁的进行装箱拆箱操作,也会严重影响系统性能(自测相差10~12倍左右)。
1.7 成员变量和局部变量
成员变量可以不用初始化,局部变量必须进行初始化。
- 成员变量属于实例对象的一部分,而实例对象都是在堆内存中分配的,在 JVM 分配存储空间的时候,就会把对象的成员变量初始化为零值。
- 局部变量是位于方法中,每一个方法都是存放在栈帧中,调用方法前必须确定方法栈帧需要分配内存空间,所以局部变量在创建时就必须进行初始化以确定分配内存大小。
1.8 值传递与引用传递
- 值传递指的是在方法调用时,传递的参数是实参值的拷贝。
- 引用传递指的是在方法调用时,传递的参数是实参的引用,也可以理解为实参的内存空间地址。
- 从本质上来说,在Java语言中的引用传递实际上都是值传递,传递的是地址的值。
1.9 main() 方法为什么必须是静态的
Java 平台调用 main() 方法时不会创建这个类的实例,因此这个方法必须声明为 static,如果没有声明为static 时,程序能正常编译但运行时会抛 NoSuchMethodError 异常。
1.10 泛型的本质
Java 在 JDK1.5 中引入泛型这一新特性,泛型的本质是参数化类型,即可以把数据类型指定为一个参数,这个参数类型可以用在类、接口和方法的创建中。在 Java 中泛型只存在于编译阶段,而不存在于运行阶段,也就是通常所说的类型擦除。
1.11 Error 和 Exception 区别是什么
Error 和 Exception 都是 Throwable 的子类,在Java中只有Throwable类型的实例才可以被抛出或者捕获,它是异常处理机制的基本类型。
- Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。
- Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于不可用状态。
- Exception又分为可检查(checked)异常和不可检查(unchecked)异常。
二、String 对象
2.1 什么叫不可变类
- 是指当一个对象被创建出来以后,它的值就不能被修改了,也就是说当对象被创建出来后,在其整个生命周期中,它的成员变量就不能被修改了。
- 在 Java 类库中,所有基本类型的包装类和 String 都是不可变类。不可变类具有使用简单、线程安全、节省内存等优点。
2.2 String 的特性
String 是标准的不可变类,对它的任何改动,其实就是创建了一个新对象,再把引用指向该对象。String 对象赋值之后就会在常量池中缓存,如果下次创建会判定常量池是否已经有缓存对象,如果有则直接返回该引用给创建者。
2.3 new String(“test”) 创建了几个对象
创建了一个或两个对象,如果在字符串常量池中已经存在"test",则会在堆中创建对象并指向常量池中的"test",如果常量池中不存在"test",则会在常量池中会创建一个”test“,并且在堆中创建一个对象指向常量池的对象。
2.4 String 拼接字符串效率低原因
String 拼接字符串效率低是指在循环中调用字符串拼接,这是因为字符串在拼接时底层会创建一个StringBuilder 对象,然后调用 append 方法进行拼接,如果在循环中调用字符串拼接,则每次循环都会创建一个新的对象,造成效率低下。
2.5 String、StringBuffer、StringBuilder 的区别
- 线程安全方面:String 和 StringBuilder 都是线程不安全的,StringBuffer 是线程安全的,因为 StringBuffer 被 synchronized 修饰。
- 执行效率方面:StringBuilder > StringBuffer > String,因为 StringBuffer 是被 synchronized 进行修饰的,创建的 String 对象都是不可变的。
- 应用场景方面:如果要操作少量的数据用 String,单线程操作字符串缓冲区 StringBuilder,多线程操作字符串缓冲区 StringBuffer。
2.6 String 的 intern() 方法
使用引号声明的字符串都会在字符串常量池中生成,而 new 出来的 String 对象是放在堆区域。intern() 方法用于查找常量池中是否存在该字符串,如果已经存在则直接返回当前字符串,如果常量池中不存在则先在常量池中创建对应的字符串,然后返回此字符串对应的引用。
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab");
System.out.println(str5.equals(str3));//true
System.out.println(str5 == str3);//false
System.out.println(str5.intern() == str3);//true
System.out.println(str5.intern() == str4);//false
2.7 String 的 intern() 方法底层实现
Java 使用 JNI 调用 C++ 实现的 StringTable 的 intern 方法,intern 方法跟 Java 中的 HashMap 的实现类似,只是不能自动扩容,默认大小是1009。
2.8 字符串常量池
字符串常量池是存储在 Java 堆内存中的字符串池,是为防止每次新建字符串时的时间和空间消耗的一种解决方案。在创建字符串时 JVM 会首先检查字符串常量池,如果字符串已经存在池中,就返回池中的实例引用,如果字符串不在池中,就会实例化一个字符串放到池中并把当前引用指向该字符串。
三、反射基础
3.1 反射机制
反射是指能够获取到处于运行状态的类或对象的所有属性和方法,它主要实现了以下功能:
- 获取类的访问能修饰符、方法、属性以及父类信息。
- 在运行时根据类的名字创建对象。在运行时调用任意一个对象的方法。
- 在运行时判断一个对象属于哪个类。
- 生成动态代理。
3.2 获取Class对象
- 通过 className.class 来获取,不执行静态块和动态构造块。
- 通过 Class.forName() 来获取,只执行静态块、而不执行动态构造块。
- 通过 Object.getClass() 来获取,因为需要创建对象,所以会执行静态块和动态构造块。
3.3 动态代理
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的(动态代理的实现依赖于反射)。
3.4 基于 JDK 动态代理
在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心,通过 Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现 InvocationHandler 接口的类的 invoke()方法,可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
(proxy, method, args) -> method.invoke(target, args) // 代理对象对应的自定义 InvocationHandler
);
}
}
3.5 基于 CGLIB 动态代理
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(MethodInterceptor.intercept());
// 创建代理类
return enhancer.create();
}
}
四、IO 模型
五、未归类问题
5.1 switch 语句问题
JDk1.5 以前 switch() 语句只支持 byte、short、char、int类型(或其包装类)的常量表达式,从 JDk1.5 开始支持枚举类型,从 JDk1.7 开始支持 String 类型。
JVM 并没有增加新的指令来处理 String 类型,而是通过调用 switch (string.hashCode) 的方式,将string转换为int从而进行判断。
5.2 try-catch 为什么比较耗费性能
JVM 在构造异常实例的时候需要生成该异常的栈轨迹,这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息,这就会导致使用异常捕获会比较耗费性能。
5.3 finally 为什么总能被执行
因为编译器在编译 Java 代码时,会复制 finally 代码块的内容,然后分别放在 try-catch 代码块所有的正常执行路径及异常执行路径的出口中,这样 finally 才会不管发生什么情况都会执行。
5.4 finalize() 方法的作用
finalize() 是在 Object 类中定义的一个方法,所有的类都继承了它,子类可以覆盖 finalize() 方法来整理系统资源或者执行其他清理工作,Java 允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
5.5 finalize()方法的调用时机
fnalize() 方法是由垃圾收集器在确定这个对象没有被引用时自动调用的,但 JVM 不保证此方法总被调用,当对象不可达时,会判断该对象是否覆盖了 finalize 方法,若未覆盖则直接将其回收,若已覆盖且未执行过该方法时,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的 finalize 方法,执行finalize方法完毕后,GC会再次判断该对象是否可达,若对象不可达进行回收,若对象可达则对象"复活"。