Java基础

参考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");
    }
} 

抽象类和接口有什么区别:

  1. 目的不同:抽象类主要是为了实现代码复用,接口主要是为了规定必须有的能力(即实现这个接口,就必须实现对应的方法)
  2. 要求不同:Java是单继承的,只能继承一个类,但可以实现多个接口

Java反射机制

反射机制含义及优缺点

Java反射机制是指在运行时动态获取类信息并操作类或对象。

反射的优缺点:

优点:使方法调用更灵活

缺点:

  1. 反射调用比普通调用性能低

反射之所以性能相对较差,主要是因为它在运行时进行了大量的查找和验证操作。比如,通过名字查找方法、字段等,这都增加了额外的开销。

优化反射性能的一些常见方法有:

1)缓存反射操作的结果,避免重复查找。

2)尽量减少反射的使用,只在必要的时候使用。

3)对于频繁使用的反射操作,可以考虑使用动态代理来提高性能。

  1. 可能造成安全风险。例如会破坏封装性,反射机制可以绕过访问控制符,访问private的字段和方法;恶意代码可能利用反射来执行不被允许的操作。

防范方法:

1)设计严格的权限控制,只允许受信任的代码使用反射;

2)对输入的反射相关参数进行检查和过滤。

反射的使用步骤

  1. 获取类的Class对象
Class<?> clazz = MyClass.class;
Class<?> clazz = object.getClass();
Class<?> clazz = Class.forName("com.example.MyClass");
  1. 获取类的构造方法、字段、方法
// 获取构造方法
Constructor<?> constructor = clazz.getDeclareConstructor();
// 获取字段
Field field = clazz.getDeclaredField("fieldName");
// 获取方法
Method method = clazz.getMethod("methodName");
  1. 操作构造方法、字段、方法
// 创建一个该类的新对象
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动态代理。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值