参考JavaGuide及其他开源资料,自用留存无商业目的
Java编译原理
1. Java语言的编译方式是什么,与其他语言有什么区别
Java是 编译解释型 语言。
编译语言有三种方式:
1)编译型语言,例如C。是使用编译器将源码一次性翻译为机器码,再执行。例如C语言代码将.c源码编译为机器码并保存在可执行文件中(exe或.o)。这种方式开发效率低,但执行效率高。
2)解释型语言,例如python。是在运行时将源码一句句进行解释执行。这种方式开发效率高,但执行效率低。
3)编译解释型语言。先将源码翻译为字节码文件,再对字节码文件进行解释,包含了编译和解释两个阶段,因此叫编译解释型语言。
2. 字节码文件是什么,有什么好处
字节码文件即.class文件,Java编译的方式是先翻译为.class文件,然后.class在JVM虚拟机中运行。
使用字节码文件的好处:
1)与编译型语言相比实现了平台无关性:编译型语言(例如C)在不同平台需要重新编译,例如C语言代码在Windows系统下要编译为exe文件,而换了Linux系统要重新编译为.o文件才能执行。
2)与解释型语言相比效率较高:解释型语言在运行时需要一句句进行解释,效率较低。而Java先将源码翻译为字节码文件,再对字节码文件进行解释,效率较高。
3. JVM、JRE、JDK分别是什么
JVM:Java虚拟机,用于运行字节码文件。JVM的主要作用就是将字节码文件翻译为不同系统的机器码然后执行,不同系统需要安装不同版本的JVM,JVM是Java实现平台无关性的关键。
JRE:JVM+Java类库,是Java应用程序的运行环境。
JDK:JRE+Java开发工具,在JRE的基础上增加了Java开发调试所需的工具,是程序员进行Java编程所需要的环境。
Java9以后,取消了单独的JRE,而是使用jlink工具生成Java程序所需要的最小运行环境。
4. Java即时编译
即时编译(Just in Time Compilation),或称为JIT。JIT会将热点代码编译为机器码存储在codeCache中,当下次执行遇到这段代码,就从codeCache中直接读取机器码运行,来提高性能。
引入JIT后的JVM流程:
1)将源码编译为字节码文件,这一过程包括词法分析、语法分析、语义分析等
2)判断是否是热点代码(判断方式是设置一个阈值,如果方法或代码块在一段时间内调用次数超过阈值,就认为它是热点代码)
3)如果是热点代码,在codeCache中读取机器码执行。
4)如果不是热点代码,直接解释执行。
5)解释执行后如果代码调用次数超过阈值,触发编译,并将编译后的机器码存入codeCache中
除了缓存热点代码的机器码以后,JIT还会进行一系列优化:
1)中间表达形式
2)方法内联:在遇到方法调用时,将目标方法的方法体纳入编译范围,并取代原有的方法调用的手段。
3)逃逸分析:逃逸分析指分析对象在哪些地方被调用,从而判断对象是否逃逸出线程或方法。JIT在内联方法的基础上进行逃逸分析,进而使用锁消除、栈上分配、标量替换等方法进行优化。
锁消除:如果JIT分析锁并没有逃逸出当前线程,那么加锁解锁就没有意义,就会直接删除该锁。
栈上分配与标量替换:如果JIT经过逃逸分析发现对象没有逃逸出方法,就可以不分配在堆中,而是分配在栈上。JIT使用的栈上分配方法实际上是通过标量替换实现的,即对于一个对象的创建,直接用它其中包括的多个变量去替换这个对象。
4)循环展开:通过展开循环减少循环次数。
5. Java静态编译
静态编译(Ahead of Time Compilation),或称为AOT、提前编译,即在程序执行前将Java代码编译为机器码,避免了JIT预热的开销,减少了内存占用。
但AOT不支持Java的一些动态特性,例如反射、动态代理等。
Java语法
1. 什么是标识符和关键字
标识符是程序员给变量和方法起的名字,关键字是Java已经赋予其含义的单词,不能作为标识符。
2. 访问控制修饰符有哪些,分别什么范围
public:所有类都能访问
protected:当前类、同一包、子类能访问
默认(default):当前类、同一包能访问
private:只有当前类能访问
3. 成员变量和局部变量的区别
成员变量是在类中定义的,局部变量是在方法或代码块中定义中。
成员变量可以被访问修饰符或static、final修饰,局部变量不能。
成员变量会自动赋对应类型的默认值,局部变量不会自动赋值,必须显式赋值。
4. static、final
static修饰的成员变量是静态变量,它是属于类的,而不是属于某个对象的,被类的所有实例共享。
如果成员变量被static final修饰,那么它是一个常量,不能被改变,必须显式赋值。
5. 静态方法为什么不能调用非静态成员
静态方法是属于类的,在类加载的时候分配内存,此时非静态成员还不存在,所以不能调用。
非静态成员是在实例化以后才存在。
6. 异常处理
异常Exception和错误Error的区别:
Throwable有两个子类,一个是Exception,一个是Error。Exception是程序可以捕获并处理的,Error不能捕获并处理,遇到了程序就会停止。
受检查异常和不受检查异常的区别:
受检查异常(Checked Exception):必须处理,不处理不能通过编译,例如IOException、除零异常等;不受检查异常(Unchecked Exception):可以不处理,不处理也能通过编译,例如空指针异常NullPointException。
try-catch-finally语法:
try {
// 可能抛出异常的代码
throw new RuntimeException("抛出异常");
} catch(Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally");
}
当程序中有多种类型异常时,使用多个catch字段
throw、throws:
throw用于抛出异常,throws用于声明方法可能抛出异常
public static void method throws IOException {
throw IOException("hi");
}
7. 重写和重载有什么区别
重载是不同的方法有相同的方法名和不同的参数列表,当调用方法时,会根据参数列表匹配对应的方法。
重写是子类继承父类后,编写与父类方法具有相同方法名、参数列表的方法,当调用方法时,会根据所属的类自动调用对应重写后的方法。
重写用@Override注解修饰
例如重写priorityQueue的排序方法
Queue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
@Override
public int compare(int[] a, int[] b){
return a[0] != b[0] ? b[0] - a[0] : b[1] - a[1];
}
});
8. 引用拷贝、浅拷贝、深拷贝
引用拷贝:新建一个引用,指向同一个对象
浅拷贝:在堆上新建一个对象,但对于对象内部的引用类型的变量,直接指向原来的
深拷贝:在堆上新建一个对象,对象内部的引用类型变量也新建
9. Java注解
注解(Annotation)是一种特殊的注释,可以修饰类、方法、字段。
注解有两种解析方式:
编译时解析,例如@Override,在编译时扫描到这个注解就会处理。
运行时解析,例如@Value、@Component,在运行时通过反射动态解析。
10. Java值传递
Java方法调用采用的是值传递,也就是实参传递给方法的形参时,是复制一个副本传递过去,而不是直接将引用传递过去。
当参数是引用类型时,复制的是引用的地址,仍然是值传递。
例如
public void test() {
int a = 1;
System.out.println(a); // 1
change(a);
System.out.println(a); // 仍然是1
}
public void change(int a) {
a++; // 对形参的改变不影响实参
}
public void test() {
int[] a = {1, 2, 3, 4, 5};
System.out.println(a[0]); // 1
change(a);
System.out.println(a[0]); // 变为0,传递的是复制的地址
}
public void change(int[] a) {
a[0] = 0;
}
public class Item {
int a;
int b;
}
public void test() {
Item item = new Item(1, 2);
change(item);
System.out.println(item.a); // 2
}
public void change(Item item) {
item.a++;
}
public void test() {
String str1 = "aaa", str2 = "bbb";
swap(str1, str2);
System.out.println(str1); // 仍然是aaa
System.out.println(str2); // 仍然是bbb
}
public void swap(String str1, String str2) {
String temp = str1;
str1 = str2;
str2 = temp;
System.out.println(str1); // bbb 形参str1指向bbb的地址,形参str2指向aaa的地址,但实参没有发生改变
System.out.println(str2); // aaa
}
10. Java SPI
SPI(Service Provider Interface),即给服务提供者的接口。
API(Application Programming Interface)。
API是服务提供者编写好代码后,向外提供调用的接口。而SPI是服务调用方提供统一的接口后,服务提供方可根据接口进行不同的实现。
11. 泛型
泛型即类型参数,指用T、E等占位符作为类型,不指定具体的类型,而是在编译时进行类型检查。
泛型类:
public class Test<T> {
private T name;
public Test(T name) {
this.name = name;
}
public T getName() {
return name;
}
}
// 使用时传入类型
Test<String> test = new Test();
泛型接口:
public interface Test<T> {
public T getName();
}
泛型方法:
public T printList(T[] list) {
for(T item : list) {
System.out.println(item);
}
return list[0];
}
Integer[] intList = {1, 2, 3, 4, 5};
String[] strList = {"aa", "vv", "cc"};
printList(intList); // 可以传入不同类型的数组
printList(strList);
泛型的类型擦除:
Java的泛型是一种“伪泛型”,在编译时,会动态地擦除泛型,并修改为Object或指定的其他类型。
既然会擦除,为什么还要使用泛型?
泛型能够提高代码灵活性,如果没有泛型,只能用Object类型代替,需要手动进行强制类型转换,比较麻烦而且容易出错。泛型在编译期就进行了类型检查和类型擦除,也就是只要能通过编译,就确保了类型安全。而使用Object进行强制类型转换,哪怕通过编译,在运行期间也可能报错。
桥方法:
继承泛型类后,如果没有重写类的泛型方法,编译器会自动创建对应的桥方法来保证多态。
12. 通配符
通配符是<?>,表示不确定的类型。分为三种:无界通配符<?>可以传入任何类型,下边界通配符<? extends FatherClass>只能传入FatherClass及其子类,上边界通配符<? super SonClass>只能传入SonClass及其父类。
通配符表示不确定传入的会是什么类型,所以List<?>不能向其中添加元素,只能将其作为Object对象进行处理。
与泛型相同,通配符也会在编译时被类型擦除,编译后都变成了Object或指定类型。
Java数据类型
1. 八种基本数据类型和对应的包装数据类型分别是什么
基本数据类型:boolean、byte、short、int、long、float、double、char
包装数据类型:Boolean、Byte、Short、Integer、Long、Float、Double、Character
2. 基本数据类型和包装数据类型有什么区别
存储位置:基本数据类型定义的局部变量存储在栈中,非静态成员变量存储在堆中,静态成员变量存储在元空间中;包装数据类型实际上是对象实例,几乎都存储在堆中。(这里几乎的含义:Hotspot虚拟机引入JIT优化后,会对对象开启逃逸分析,如果对象没有逃逸到方法外部,就分配在栈上,而不会在堆中分配内存)
默认值:基本数据类型有默认值,包装数据类型默认值是null
比较方式:基本数据类型可以用==比较,包装数据类型用==比较的是内存地址,要用equals函数比较,才是比较的实际值。
3. 包装数据类型的缓存机制
Byte、Short、Integer、Long默认创建[-128, 127]的缓存,Character默认创建[0, 127]的缓存。当创建新对象的值在这个范围内时,直接返回缓存池中的对象,而不会创建新对象,以此来提升性能。
Integer i1 = 2;
Integer i2 = 2;
Integer i3 = new Integer(2); // 显式创建新对象,不用缓存
System.out.println(i1==i2); // 输出true
System.out.println(i2==i3); // 输出false
4. 什么是自动拆装箱,什么时候会发生自动拆装箱,底层是怎么实现的,为什么自动拆箱可能带来NPE风险
自动拆箱指包装数据类型转换为基本数据类型,自动装箱指的是基本数据类型转换为包装数据类型。
Integer i1 = 10; // 发生自动装箱
int i2 = i1; // 发生自动拆箱
自动拆箱的原理是intValue()、charValue()等,自动装箱的原理是valueOf(),上面两行代码就等价于
Integer i1 = Integer.valueOf(10);
int i2 = i1.intValue();
NPE也就是NullPointException(空指针异常),在自动拆箱时调用对应的intValue()、charValue()方法,然而,如果包装数据是null,调用这些方法就会导致NPE。如下
public void test() {
int value1 = getValue(1);
int value2 = getValue(-1); // 空指针异常
}
Integer getValue(int key) {
if(key > 0) return key + 1;
else return null;
}
5. String类型:String、StringBuilder、StringBuffer有什么区别
String是不可变的,StringBuilder和StringBuffer是可变的,其中StringBuffer对方法加了同步锁,所以是线程安全的,StringBuilder是线程不安全的。
因此,操作少量数据使用String,单线程操作大量数据使用StringBuilder,多线程操作大量数据使用StringBuffer。
String的不可变:
String内部是使用private fianal byte[] value来保存字符串的,因此是不可变的。Java重载了String类的“+”和“+=”运算符,当使用“+”来操作String时,实际上是先将其转为StringBuilder,调用其append方法完成加操作后,使用toString方法再转回String。
但当多次使用“+”时,编译器会多次创建StringBuilder,会增加内存消耗,因此如果要在循环中大量操作字符串,最好还是直接使用StringBuilder。
6. 什么是字符串常量池
字符串常量池是JVM为了减少内存消耗而专门开辟的一块区域,当字符串第一次创建时,会保存在常量池中,当下一次创建相同的字符串时,会直接返回常量池中的字符串,而不会重新创建,如下:
String a1 = "aaa";
String a2 = "aaa";
System.out.println(a1 == a2); // 输出true
String a3 = "aaa"; // 直接返回字符串常量池中的对象
String a4 = new String("aaa"); // 创建一个新的String对象,并用字符串常量池中的"aaa"对象初始化它
问:当显式new一个String对象时,例如String a1 = new String(“abc”);会创建几个对象
1)如果字符串常量池中没有"abc",创建两个。具体来说,先创建一个"abc"字符串,保存在字符串常量池中,再在堆中创建一个新的String对象,并使用"abc"常量进行初始化;
2)如果字符串常量池中已经存在"abc"常量,只在堆中创建一个String对象。
7. 如何改变一个String的值
问如何改变一个String的值,如果回答直接赋值是不对的,因为String内部的char数组/byte数组是final修饰的,不可变的。如果直接赋值的话,其实是创建了一个新的String对象,然后将引用指向了这个对象,如果打印前后的hashcode,是不一样的。
那么如何不创建新对象,原地改变这个String的值呢?
正确的回答是用反射机制。
String str = "abc";
System.
Field field = String.class.getDeclaredField("value"); // 获取String内部的value字段(也就是char数组/byte数组)
field.setAccessible(true); // 绕过访问修饰符
field.set(str, new char[]{'d', 'e', 'f'}); // 将str对象的field字段的值修改为def
System.out.println(str.hashCode()); // 前后两个哈希值是一样的,说明是同一个对象
8. 常量折叠
对于编译期常量的运算,javac编译器会进行常量折叠的优化,也就是将常量的简单操作计算出来,直接存储结果。八种基本数据类型常量和string常量都可以进行常量折叠。而对于变量和运行时常量,由于编译期间无法确定其值,所以不会进行常量折叠优化。例如:
String str3 = "abc" + "def"; // 是编译期常量,常量折叠后,在字符串常量池中创建"abcdef"对象,而不会创建"abc"和"def"对象
String str4 = "abcdef"; // 常量池中的"abcdef"对象
System.out.println(str3 == str4); // true
String str1 = "abc";
String str2 = "def";
String str5 = str1 + str2; // 变量,这里做的操作是字符串对象拼接,底层原理是先将String转为StringBuilder,使用其append方法进行拼接,最后再将其转回String,所以它返回的不是字符串常量池中的对象
final String str6 = str1 + str2; // 是运行时常量,虽然由final修饰,但右边表达式是变量,编译期无法确定值,也就无法进行常量折叠优化
System.out.println(str3 == str5); // false
System.out.println(str3 == str6); // false
9. String的intern方法
String的intern方法是一个native方法,用于返回String类型在字符串常量池中的引用。
如果字符串常量池中有相同内容的对象,就返回其引用。如果没有,就在字符串常量池中创建,再返回其引用。
String s1 = new String("hiii"); // 在字符串常量池中创建"hiii"对象,然后创建一个新的String对象,并用常量初始化它
String s2 = s1.intern(); // 字符串常量池中的对象
System.out.println(s1 == s2); // false
System.out.println(s2 == "hiii"); // true
String s1 = new String("hi") + new String("hy"); // 创建hi常量、hy常量,创建两个新String对象分别用两个常量初始化,然后用String重载后的+运算符完成字符串拼接
String s2 = s1.intern(); // 在字符串常量池中创建hihy对象
System.out.println(s1 == "hihy"); // false
System.out.println(s2 == "hihy"); // true
Java面向对象
1. 面向对象和面向过程的区别
面向过程是将解决问题的过程拆分为一个个方法的执行,面向对象是先抽取出对象,然后用对象调用方法的方式去解决问题。
2. 面向对象的三大特征
封装:将一个对象的属性隐藏在内部,不允许直接访问,而是通过暴露的方法接口访问。
继承:子类可以使用extends关键字继承父类,拥有父类全部的方法和属性(但无法访问private的方法和属性)
多态:即一个对象拥有多种状态,当子类继承父类,且重写父类的方法后,当调用该方法时,会自动根据具体的类型调用对应的方法
3. 抽象类和接口
使用abstract修饰,只有方法名,没有方法体,子类继承后重写抽象方法
abstract public class Animal {
// 抽象方法
abstract void eat();
// 非抽象方法
void sleep(){
System.out.println("dog sleeping");
}
}
public class Dog {
@Override
void eat(){
System.out.println("dog eatting");
}
}
接口中的方法都是只有方法名,没有方法体,使用implements关键字实现接口后,使用@Override注解重写
接口中的变量必须是public static final的,而且必须有初始值
以前接口中的方法必须是抽象方法,但现在允许接口中有方法的默认实现,用default关键字修饰,当实现接口的类需要重写时,只需用@Override注解重写即可
public interface Animal {
public static final name = "animal";
public void eat();
default public void bark(){
System.out.println("barking");
}
}
public class Dog implements Animal {
@Override
public void eat(){
System.out.println("dog eatting");
}
@Override
public void bark(){
System.out.println("dog barking: wangwangwang");
}
}
抽象类和接口有什么区别:
- 目的不同:抽象类主要是为了实现代码复用,接口主要是为了规定必须有的能力(即实现这个接口,就必须实现对应的方法)
- 要求不同:Java是单继承的,只能继承一个类,但可以实现多个接口
Java反射机制
反射机制含义及优缺点
Java反射机制是指在运行时动态获取类信息并操作类或对象。
反射的优缺点:
优点:使方法调用更灵活
缺点:
- 反射调用比普通调用性能低
反射之所以性能相对较差,主要是因为它在运行时进行了大量的查找和验证操作。比如,通过名字查找方法、字段等,这都增加了额外的开销。
优化反射性能的一些常见方法有:
1)缓存反射操作的结果,避免重复查找。
2)尽量减少反射的使用,只在必要的时候使用。
3)对于频繁使用的反射操作,可以考虑使用动态代理来提高性能。
- 可能造成安全风险。例如会破坏封装性,反射机制可以绕过访问控制符,访问private的字段和方法;恶意代码可能利用反射来执行不被允许的操作。
防范方法:
1)设计严格的权限控制,只允许受信任的代码使用反射;
2)对输入的反射相关参数进行检查和过滤。
反射的使用步骤
- 获取类的Class对象
Class<?> clazz = MyClass.class;
Class<?> clazz = object.getClass();
Class<?> clazz = Class.forName("com.example.MyClass");
- 获取类的构造方法、字段、方法
// 获取构造方法
Constructor<?> constructor = clazz.getDeclareConstructor();
// 获取字段
Field field = clazz.getDeclaredField("fieldName");
// 获取方法
Method method = clazz.getMethod("methodName");
- 操作构造方法、字段、方法
// 创建一个该类的新对象
Object object = constructor.newInstance("arg");
// 访问字段
field.setAccessible(true); // 绕过访问控制权限检查
field.set(object, "value"); // 设置object对象field字段的值为"value"
// 调用方法
method.invoke(object, 123);
Spring框架中应用反射的例子
依赖注入
对于@Autowired注解标记的变量,扫描其类路径,找到有@Component注解标记的类,使用反射机制创建对应实例,并通过反射注入到变量中。
注解解析
使用getAnnotations()获取注解,并使用.value、.number等解析注解的内容
// 获取类的注解
Class<?> clazz = MyClass.class; // 获取类对象
Annotation[] annotations = clazz.getAnnotations(); // 获取注解
MyAnnotation myAnnotation = clazz.getAnnotation(MyAnnotation.class); // 获取指定注解
// 获取方法注解
Method method = clazz.getMethod("myMethod"); // 获取指定方法
Annotation[] methodAnnotations = method.getAnnotations(); // 获取该方法的注解
MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class);
// 获取字段注解
Field field = clazz.getField("myField");
Annotation[] fieldAnnotations = field.getAnnotations();
MyAnnotation fieldAnnotation = field.getAnnotation(MyAnnotation.class);
// 解析注解的内容
String value = myAnnotation.value();
int number = myAnnotation.number();
动态代理:
使用反射类Method调用方法,实现方法的代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserServiceInvocationHandler implements InvocationHandler {
private final Object target; // 目标对象
public UserServiceInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 增强逻辑:在方法调用前执行
System.out.println("Before method: " + method.getName());
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 增强逻辑:在方法调用后执行
System.out.println("After method: " + method.getName());
return result;
}
}
Java代理模式
代理模式就是用代理对象代替真实对象来进行访问,在不修改真实对象的前提下,为对象提供某些扩展和增强功能。
代理模式有两种,一种是静态代理,一种是动态代理。
1. 静态代理
静态代理是为每一个目标类手动编写代理类,并在编译时将其翻译为字节码文件。
动态代理是编写代理类,并在运行时传入目标类,动态为目标类生成代理类。
与静态代理相比,动态代理的优点是更加灵活,不必为每个目标类都单独生成代理类,避免了代码冗余。
动态代理有两种实现方式:
2. JDK动态代理
JDK动态代理是基于接口实现的,要求目标类必须实现某个接口。如下:
// 目标类接口
public interface TargetInterface {
public void method();
}
// 目标类具体实现
public class TargetImpl implements TargetInterface {
public void method() {
System.out.println("方法实现");
}
}
// 代理类
public JDKProxyHandler implements InvocationHandler {
private Object target; // 目标对象
public JDKProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法执行前的代理代码
Object result = method.invoke(target, args);
// 方法执行后的代理代码
return result;
}
}
public class Test {
public static void main(String[] args) {
// 目标类,必须由接口实现
TargetInterface target = new TargetImpl();
// 代理类,由Proxy类的newProxyInstance方法实现,并类型转换为目标类接口
TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new JdkProxyHandler(target)
);
// 调用代理增强后的method方法
proxy.method();
}
}
3. CGLIB动态代理
CGLIB动态代理不要求实现接口,而是生成目标类的子类来实现的。
public class Target {
public void method() {
System.out.println("目标类方法");
}
}
// 自定义方法拦截器(通过实现MethodInteceptor接口)
public class CGLIBProxyInterceptor implements MethodInteceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 方法执行前的代理代码
Object result = proxy.invokeSuper(obj, args);
// 方法执行后的代理代码
return result;
}
}
public class Test {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperClass(Target.class);
enhancer.setCallback(new CGLIBProxyInterceptor());
Target proxy = (Target) enhancer.create();
proxy.method();
}
}
JDK动态代理和CGLIB动态代理的区别:
JDK动态代理要求目标类必须实现接口,CGLIB动态代理不要求,而是通过生成目标类的子类实现的。因此,CGLIB不适用于final类型的类。另外JDK动态代理的性能要比CGLIB动态代理高。在Spring AOP中,实现了接口的类使用JDK动态代理,没有实现接口的使用CGLIB动态代理。
16万+

被折叠的 条评论
为什么被折叠?



