1.JDK与JRE区别?
JDK提供了java的开发环境和运行环境,而JRE提供了java的运行环境。具体来说就是JDK包含了JRE,还包含了编译java源码的编译器javac,如果你要运行java程序就安装JRE就行了,如果你要编写java程序就需要安装jdk
2.== 和 equals 的区别是什么?
==在基本类型和引用类型中的效果是不同的,在基本类型中比较的是值是否相同,而在引用类型中比较的是引用是否相同。equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等、把它从引用比较变成了值比较,所以一般情况下 equals 比较的是值是否相等。
3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
不对,两个对象的 hashCode()相同,equals()不一定 true
4.为什么需要重写equals与hashCode方法?
重写 equals 和 hashCode 方法是因为它们是成对出现的,用于判断对象内容是否相等,它们需要协同工作,尤其是在使用哈希集合(如 HashSet 或 HashMap)时。如果重写了 equals 但没有重写 hashCode,就会导致同一个相等对象可能拥有不同的哈希码值,从而在哈希集合中被当作不同的元素存储,造成程序行为异常和效率低下。
- equals方法:用于判断两个对象的内容是否相等,而不是仅仅比较它们的内存地址。默认情况下,
Object类中的 equals 方法是比较对象的引用是否相同(即内存地址是否一样)。重写equals方法可以让开发者根据业务需求来定义对象的相等性,比如两个Person对象的id相同就认为它们相等。 - hashCode方法:用于计算对象的哈希值,这个值可以用来快速定位对象在哈希表(如
HashMap的桶)中的位置。 - 成对重写的原则:根据 Java 的约定,如果两个对象通过
equals方法比较是相等的(返回true),那么它们必须返回相同的hashCode值。 - 违反原则的后果:
- equals 为
true,hashCode 不同:当你在HashMap 中使用一个对象作为键,如果它与一个已存在的键通过equals判定相等,但它们的hashCode不同,HashMap会认为它们是两个不同的键,可能会创建新的条目,导致数据不一致或重复。 - 影响集合性能:
HashSet等集合会先根据hashCode找到可能的存储位置,然后再用equals进行确认。如果hashCode不一致,即使equals相等,也会导致对象没有被正确地添加到集合中,或者查询效率低下。
- equals 为
因此,在重写 equals 方法时,必须同时重写 hashCode 方法,以确保对象的正确性和一致性。
5.final, finally, finalize的区别?
final 是 Java 语言中的一个关键字,使用 final 修饰的对象不允许修改或替换其原始值或定义。
final 可以用来修饰:类、方法、变量和参数,其中可以用来修饰“参数”这一项,容易被人遗忘,这是 final 的 4 种用法。
final 用法说明
- 当 final 修饰
类时,此类不允许被继承,表示此类设计的很完美,不需要被修改和扩展。 - 当 final 修饰
方法时,此方法不允许任何从此类继承的类来重写此方法,表示此方法提供的功能已经满足当前要求,不需要进行扩展。 - 当 final 修饰
变量时,表示该变量一旦被初始化便不可以被修改。 - 当 final 修饰
参数时,表示此参数在整个方法内不允许被修改。
finally 则是 Java 中保证重点代码一定要被执行的一种机制。
我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证释放锁等动作
finalize 是 Object 类中的一个基础方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收,但在 JDK 9 中已经被标记为弃用的方法(deprecated)。
在实际开发中不推荐使用 finalize 方法,它虽然被创造出来,但无法保证 finalize 方法一定会被执行,所以不要依赖它释放任何资源,因为它的执行极不“稳定”。在 JDK 9 中将它废弃,也很好的证明了此观点。
6.String是基础数据类型吗?
String不属于基本数据类型,基本数据类型有8种,分别是byte short int long float double boolean char,String属于引用数据类型
7.java 中操作字符串都有哪些类?它们之间有什么区别?
有String、StringBuffer、StringBuilder
String和StringBuffer、StringBuilder的区别在于String声明的是不可变的字符序列,每次操作都会生成新的String对象,然后将指针指向新的String对象,而StringBuffer、StringBuilder可以在原有的对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用String
StringBuffer和StringBuilder的最大区别在于,StringBuffer是线程安全的,StringBuilder是线程不安全的,但StringBuilder的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder,多线程环境下使用StringBuffer
8. String str="i"与 String str=new String(“i”)一样吗?
不一样,内存分配方式不一样,Stirng str="i"的方式,java虚拟机会将其分配到字符串常量池中,而String str = new String("i")则会分配到堆内存中。类似于String str=new String(“i”)会创建两个对象:一个是在堆上创建的新的 String 对象,另一个是在字符串常量池中创建的字符串 "i"
9.如何将字符串反转?
使用StringBuilder 或者 StringBuffer的reverse() 方法
10.抽象类一定需要抽象方法吗?
不一定,抽象类不一定需要抽象方法
11.抽象类与普通类的区别?
普通类不能包含抽象方法,抽象类可以包含抽象方法。抽象类不能直接实例化new Xxx(),普通类可以直接实例化new Xxx()
12.抽象类与接口的区别?
修饰符不同,一个是abstract,一个是interface。抽象类中可以有不是抽象的方法,接口当中必须全是抽象方法(jdk1.8之前成立),jdk1.8还可以定义静态方法和默认方法。接口中变量全部默认使用public static final修饰的常量,抽象类中不限制 。抽象类只能够被单继承,接口可以有多实现。抽象类可以有构造函数;接口不能有。接口中的方法默认使用 public 修饰,抽象类中的方法可以是任意访问修饰符。
13.Java中IO分类?
按功能来分: 输入流、输出流 按类型来分: 字节流和字符流
字节流和字符流的区别是: 字节流按8位传输以字节为单位输入输出数据, 字符流按16位传输以字符为单位输入输出数据
14.BIO,NIO,AIO的区别?
BIO(Blocking I/O):采用阻塞式 I/O 模型,线程在执行 I/O 操作时被阻塞,无法处理其他任务,适用于连接数较少且稳定的场景。
NIO(New I/O 或 Non-blocking I/O):使用非阻塞 I/O 模型,线程在等待 I/O 时可执行其他任务,通过 Selector 监控多个 Channel 上的事件,提高性能和可伸缩性,适用于高并发场景。
AIO(Asynchronous I/O):采用异步 I/O 模型,线程发起 I/O 请求后立即返回,当 I/O 操作完成时通过回调函数通知线程,进一步提高了并发处理能力,适用于高吞吐量场景。
15.Files的常用方法都有哪些?
Files.exists(): 检测文件路径是否存在
Files.createFile(): 创建文件
Files.createDirectory(): 创建文件夹
Files.delete() : 删除一个文件或目录
Files.read(): 读取文件
Files.write(): 写入文件
15.Map与Collection?
Collection 和 Map 是 Java 集合框架的两个根接口,主要区别在于存储方式:Collection 存储一组对象(单列集合),而 Map 存储键值对(双列集合)。Collection 的子接口包括 List(有序、可重复)和 Set(无序、不可重复)等,而 Map 的键(key)是唯一的。
Collection
- 作用: 存储一组对象。
- 特性:
- 单列集合: 存储的是单个元素。
- 根接口: 是
List、Set、Queue等集合接口的父接口。 - 子接口:
List: 存储的元素有序且允许重复。Set: 存储的元素不重复。
Map
- 作用: 存储键值对(key-value)的映射关系。
- 特性:
- 双列集合: 存储的是键值对。
- 根接口: 与 Collection 接口同级,不继承自 Collection。
- 键(key)的唯一性: 每个键在 Map 中是唯一的。
- 值(value)的重复性: 允许重复的值。
16.ArrayList与LinkedList的区别?
ArrayList: ArrayList是Java集合框架中的一个类,它实现了List接口,底层基于数组实现。ArrayList的特点是支持动态数组,可以自动扩容,适合顺序访问和随机访问。
LinkedList: LinkedList也是Java集合框架中的一个类,同样实现了List接口,但底层基于链表实现。LinkedList的特点是支持高效的插入和删除操作,但随机访问的性能相对较差。
区别与优缺点对比 存储结构:ArrayList使用数组作为底层数据结构,数据在内存中是连续存储的,因此支持随机访问非常快速。LinkedList则使用链表作为底层数据结构,每个元素都包含指向前后元素的指针,插入和删除操作非常高效。
插入与删除操作:在ArrayList中,如果插入或删除元素,可能会导致数组元素的移动,从而影响性能。而LinkedList在插入和删除操作上具有明显优势,因为只需修改指针的指向,不需要移动大量元素。
随机访问性能:由于ArrayList的数组连续存储特性,它在随机访问上具有很好的性能。通过索引即可直接访问元素。而LinkedList需要从头或尾开始遍历链表,随机访问性能较差。
内存占用:由于LinkedList每个元素都需要存储前后指针,相对于ArrayList会占用更多的内存空间。如果需要存储大量数据,考虑内存占用也是一个重要因素。
迭代性能:在迭代(遍历)操作上,ArrayList由于连续存储的特性,性能通常较好。而LinkedList在迭代操作上由于需要通过指针跳转,性能相对较差。
如何选择?
- 使用ArrayList的场景: 需要频繁进行随机访问,例如根据索引获取元素。 数据集合相对固定,不需要频繁的插入和删除操作。 内存占用相对较少,不会造成严重的资源浪费。
- 使用LinkedList的场景: 需要频繁进行插入和删除操作,尤其是在中间位置。 不关心随机访问性能,而更关注插入和删除的效率。 可能需要更少的内存占用,尤其是在元素数量较少的情况下。
17.ArrayList与Vector的区别?
ArrayList和Vector的主要区别在于线程安全性和性能:Vector是线程安全的(同步的Vector的线程安全是通过在每个方法上都使用synchronized关键字来实现的),因此在多线程环境下使用;而ArrayList不是线程安全的(非同步的),性能更好,适合单线程环境。此外,在容量不足时,Vector默认将容量翻倍,而ArrayList默认增加容量的50%。
18.ArrayList如何转成成线程安全的List?
可以通过 Collections.synchronizedList()方法将 ArrayList 转换成线程安全的集合,或者使用线程不安全的 CopyOnWriteArrayList,Collections.synchronizedList() 方法会返回一个线程安全的 List,该列表中的所有方法都会进行同步,以保证在多线程环境下操作的安全性。
19.HashMap与HashTable的区别?
- HashMap不是线程安全的,HashTable是线程安全。
- HashMap允许空(null)的键和值(key),HashTable则不允许。
- HashMap性能优于Hashtable(Hashtable的线程安全是通过在每个方法上都使用
synchronized关键字来实现的)
20.synchronized锁的升级过程?
- 无锁 -> 偏向锁: 当一个线程首次执行同步代码块时,如果对象未上锁,
synchronized会将其偏向于该线程,此时升级为偏向锁。 - 偏向锁 -> 轻量级锁: 当有其他线程尝试竞争已经偏向某个线程的锁时,偏向锁会升级为轻量级锁。
- 轻量级锁 -> 重量级锁: 当轻量级锁发生竞争时,会通过 CAS 操作和自旋(自旋锁)来尝试获取锁。如果自旋一定次数后仍未获取到锁,则升级为重量级锁。
21.重载和重写的区别?
重载是发生在同一个类中,方法名相同,参数的类型、个数不同。重写发生在子类中,方法的名称、类型、返回值全部相同
22.Java 为什么要有包装类? 基本数据类型和对应包装类的区别?
因为当向ArrayList、HashMap中放东西时,int、double这种基本类型是放不进去的,因为容器都是装object的,这就需要有包装类了。基本数据类型不使用new关键字,而包装类需要使用new关键字在堆中分配存储空间;基本类型是将值存储在栈中,包装类型是将值放在堆中;
23.双亲委派机制可以打破吗?
可以打破。第一种是自定义类加载器去继承ClassLoader类重写loadClass()方法,第二种是线程上下文类加载器破坏双亲委派模型。
24.双亲委派机制是什么?
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。 Java中提供的这四种类型的加载器,是有各自的职责的:
- Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件 一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被ClassLoader和Extention ClassLoader加载的。
首先,通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。 另外,通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。
双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的
25.throws与throw的区别?
throw是抛出一个异常,throws用来声明一个方法可能抛出的异常
26.Java中的Exception与Error是什么?
在 Java 中,Exception 和 Error 都继承自 Throwable 类。Exception 是程序在运行中可能遇到的可预料的意外情况,可以被捕获和处理。Error 指的是系统级或严重问题,通常是程序无法处理的,比如 JVM 内存溢出,需要恢复的严重错误。- Exception分类:
- 编译时异常(Checked Exception) : 在源代码中必须显式处理,否则程序无法编译通过。例如,文件未找到 (FileNotFoundException)。
- 运行时异常(Unchecked Exception) : 继承自 RuntimeException。它们在编译时不会强制检查,可以不捕获或抛出。通常是由于程序逻辑错误引起,如 NullPointerException、ArrayIndexOutOfBoundsException 等。常见的RuntimeException NullPointerException (空指针异常) NumberFormatException (数字格式化异常) ArrayIndexOutOfBoundsException (数组越界异常) StringIndexOutOfBoundsException (字符串越界异常) ClassCastException (类型转换异常) ArithmeticException (算术异常) IllegalArgumentException (非法参数异常)
27.JDK动态代理实现方法与实现步骤是怎样的?
实现方法:
- 在java的动态代理机制中,有两个重要的类或接口,一个是
InvocationHandler(Interface)、另一个则是Proxy(Class),这一个接口和类是实现我们动态代理所必须用到的
实现步骤:
- step 1:拿到被代理对象的引用,并且通过反射获取到它的所有的接口。
- step 2:通过JDK Proxy类重新生成一个新的类,同时新的类要实现被代理类所实现的所有的接口。
- step 3:动态生成 Java 代码,把新加的业务逻辑方法由一定的逻辑代码去调用。
- step 4:编译新生成的 Java 代码.class。
- step 5:将新生成的Class文件重新加载到 JVM 中运行。
28.JDK动态代理与CGLIB代理的区别?
JDK动态代理是通过重写被代理对象实现的接口中的方法来实现,而CGLIB是通过继承被代理对象来实现,和JDK动态代理需要实现指定接口一样,CGLIB也要求代理对象必须要实现MethodInterceptor接口,并重写其唯一的方法intercept。
CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。(利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理)
注意:因为CGLIB是通过继承目标类来重写其方法来实现的,故而如果是final和private方法则无法被重写,也就是无法被代理。
29.Java中创建对象的几种方式?
- new关键字
- Class.newInstance
- Constructor.newInstance
- Clone方法
- 反序列化
30.深度拷贝与浅度拷贝区别,如何实现深度拷贝?
区别:
浅度拷贝只复制对象的引用,而深度拷贝会为对象及其所有子对象在内存中创建独立的副本
实现方法:
- 实现
Cloneable接口并重写clone()方法 - 使用序列化和反序列化,这种方法通过将对象转换为字节流,然后从字节流中恢复为新的对象来创建深拷贝副本。
31.Java中的封装,继承,多态如何理解?
- 封装 (Encapsulation)
-
概念:将数据(属性)和操作数据的方法(函数)捆绑在一起,形成一个独立的单元——类。
-
作用:
- 信息隐藏:隐藏对象的内部状态和实现细节,外部只能通过公共接口来访问和修改数据。
- 提高安全性:防止外部代码不小心或恶意地修改对象内部数据,提高代码的健壮性。
- 提高可维护性:当内部实现发生变化时,只要公共接口不变,就不会影响到使用该类的其他代码。
- 继承 (Inheritance)
-
概念:子类(派生类)可以继承父类(基类)的属性和方法,从而获得父类的特性。
-
作用:
- 代码复用:避免重复编写相同的代码,子类可以直接复用父类的代码。
- 建立层次结构:形成类之间的“is-a”关系(例如,“狗”is-a“动物”),使代码结构更清晰。
-
Java 特性:Java是单继承语言,一个类只能直接继承一个父类。
- 多态 (Polymorphism)
-
概念:多态意味着同一个方法调用,在不同的对象上会有不同的行为表现。
-
实现方式:
- 父类引用指向子类对象:父类引用变量可以指向其子类的对象。
- 方法重写 (Overriding) :当子类和父类中定义了相同的方法签名时,子类可以重写父类的方法。
-
作用:
- 灵活性和通用性:允许编写更通用、更灵活的代码。例如,一个处理“动物”的方法,可以通过多态来处理“狗”、“猫”等不同类型的动物对象,而无需知道具体是哪种动物。
- 多种形态:一个接口或父类可以有多种不同的实现。
-
编译时多态与运行时多态:
- 编译时多态:通常指方法重载 (Overloading),编译器在编译时根据方法参数的类型和数量来确定调用哪个方法。
- 运行时多态:通常指方法重写 (Overriding),根据对象的实际类型在运行时决定调用哪个方法。
32.说说HashMap的底层实现
HashMap的底层是一个数组,存储着Node(或TreeNode)对象,当链表过长时会转为红黑树。之所以容量总是2的幂次方,是因为这允许在计算元素位置时使用高效的位运算(hash & (length - 1)),而非缓慢的取模运算。同时,当容量为2的幂次方时,扩容时只需将容量翻倍,并根据新容量计算元素的新位置,过程更简单高效,并能确保哈希值能更均匀地分布,从而减少哈希冲突。
底层结构
- 数组:
HashMap的底层是一个数组,每个元素都是一个链表或红黑树的起始节点。 - Node:
Node是链表的节点,包含键、值、哈希值以及指向下一个节点的引用。 - TreeNode:当链表中的节点数量达到一定阈值(在JDK 8中是8个)时,该链表会转换为一颗红黑树,以优化查找性能。
为什么容量为2的幂次方
-
效率更高的哈希计算:使用位运算能显著提高查找和插入的效率。
- 当容量为2的幂次方时,可以用
(capacity - 1) & hash来代替取模运算%来计算索引。 - 例如,如果
capacity是16,那么(capacity - 1)是15(二进制为1111)。此时,hash & 15的结果就是取模运算的等价结果,且效率远高于取模运算。
- 当容量为2的幂次方时,可以用
-
更均匀的哈希分布:长度为2的幂次方能帮助更均匀地分布哈希值,减少哈希冲突,避免过长的链表。
- 这使得
HashMap在多数情况下都能保持较快的查找速度。
- 这使得
-
更高效的扩容:当
HashMap需要扩容时,新容量通常是原容量的两倍。- 扩容时,原有的链表数据不需要重新计算每一个元素的索引,只需将原链表按照新容量的计算方式分成两部分(奇偶索引),然后直接复制到新数组的对应位置即可,这比重新计算所有元素的索引要高效得多。
33.ConcurrentHashMap是如何实现线程安全的?
-
分段锁(Java 1.7及之前) :
ConcurrentHashMap内部维护一个或多个Segment数组。- 每个
Segment包含一个独立的哈希表,并由一个独立的锁来保护。 - 当线程需要访问一个段时,只需要获取该段的锁,而不需要锁定整个哈希表。
- 这允许不同线程同时操作不同的段,显著提高了并发度。
-
更精细的锁和CAS(Java 1.8及之后) :
- 放弃分段锁: Java 8 版本不再使用分段锁,而是采用了一种更精细的锁机制。
- 节点锁: 在写入时,每个哈希桶(通常是链表或红黑树的头节点)会有一个锁。
- CAS: 对于某些更新操作,会使用 CAS(Compare and Swap)原子操作来尝试更新数据,避免了获取全局锁的开销。
volatile: 关键字段使用volatile关键字来保证线程之间的可见性,尤其是在读操作时,这允许无锁读取。
-
内部数据结构:
ConcurrentHashMap使用一个结合了 数组、链表和红黑树 的数据结构来处理哈希冲突。- 当链表长度超过一定阈值时,会将其转换为红黑树,以提高查找、插入和删除的效率。
34.强引用、软引用、弱引用、虚引用的区别
1. 强引用(Strong Reference)
- 定义:最常见的引用类型,如
Object obj = new Object(),obj 就是强引用。 - 特点:只要强引用存在,GC 绝不会回收被引用的对象,即使内存不足时 JVM 会抛出 OOM 异常也不会回收。
- 使用场景:日常开发中绝大多数对象都是强引用。
2. 软引用(SoftReference)
- 定义:通过
SoftReference<T>类实现,如SoftReference<Object> softRef = new SoftReference<>(new Object())。 - 特点:内存充足时不会被回收;内存不足(即将 OOM)时,GC 会主动回收软引用关联的对象。
- 使用场景:缓存场景(如图片缓存),内存足够时保留缓存,内存不足时释放缓存避免 OOM。
3. 弱引用(WeakReference)
- 定义:通过
WeakReference<T>类实现,如WeakReference<Object> weakRef = new WeakReference<>(new Object())。 - 特点:无论内存是否充足,只要发生 GC,弱引用关联的对象就会被回收。
- 典型应用:
ThreadLocalMap中的 key 使用弱引用。当ThreadLocal对象没有强引用时,GC 会回收 key,避免因 ThreadLocal 未显式 remove 导致的内存泄漏(若 key 是强引用,即使 ThreadLocal 外部引用消失,ThreadLocalMap 仍持有强引用,导致 Entry 无法回收)。
4. 虚引用(PhantomReference)
- 定义:通过
PhantomReference<T>类实现,必须配合ReferenceQueue使用,如PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue)。 - 特点:最弱的引用,无法通过虚引用获取对象(
get()方法始终返回 null);唯一作用是在对象被 GC 回收时,将引用加入关联的ReferenceQueue,用于接收对象回收通知。 - 使用场景:管理堆外内存(如
DirectByteBuffer),通过虚引用感知对象回收,进而释放对应的堆外内存资源。
35.如何打破双亲委派机制
Java 的“双亲委派机制”想打破,最常见就是这两种:
- 自定义 ClassLoader,重写
loadClass,改成 child-first(子加载器优先) - 使用线程上下文类加载器 TCCL,让父层代码反过来用子加载器找实现类
先说本质:
ClassLoader 的默认 loadClass 顺序是:
- 先
findLoadedClass - 再委派给父加载器
- 父加载器找不到,自己再
findClass
这就是双亲委派的核心。官方文档也明确写了:默认实现就是这个顺序,并且更鼓励子类重写 findClass,而不是 loadClass。也就是说,双亲委派更多是默认实现策略,不是绝对不可改的铁律。
最直接的打破方式:重写 loadClass
public class ChildFirstClassLoader extends ClassLoader {
private final String basePackage;
public ChildFirstClassLoader(ClassLoader parent, String basePackage) {
super(parent);
this.basePackage = basePackage;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 已加载直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 只对指定包走 child-first,避免把 JDK 类也拦了
if (name.startsWith(basePackage)) {
try {
c = findClass(name); // 先自己找
} catch (ClassNotFoundException e) {
c = getParent().loadClass(name); // 自己找不到再交给父
}
} else {
// 其他类仍然父优先
c = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassBytes(name); // 你自己实现:从文件/JAR/网络读取字节码
if (bytes == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassBytes(String name) {
return null;
}
}
另一种常见“打破”:线程上下文类加载器 TCCL
双亲委派下,父加载器本来看不到子加载器加载的类。但有些场景必须“高层框架接口在父里,具体实现却在业务应用里”。这时 Java 提供了 Thread Context ClassLoader。
线程上下文类加载器允许线程创建者给线程指定一个加载器;官方权限文档也写到,系统代码和扩展代码会用它去查找那些系统类加载器里可能不存在的资源。ServiceLoader.load(service, loader) 也明确支持从指定加载器开始查找 provider。
典型理解
不是你自己改 loadClass,而是:
- 父层框架代码执行时
- 临时拿当前线程的
contextClassLoader - 再去加载子应用里的实现类
这其实是**“父用子”**,逻辑上就绕开了纯粹的双亲委派方向。
常见场景
- SPI
- JDBC Driver 加载
- JNDI
- 各种容器/中间件/插件机制
不能乱打破的边界
有几条红线要记住:
1. 不要去碰 java.*
官方 SecureClassLoader 文档明确写了:如果类名以 "java." 开头,defineClass 会抛 SecurityException。
2. 公共 API 不要重复加载
例如接口 com.demo.api.UserService 如果父、子各加载一份,子实现即便 implements UserService,父那边也可能认不出来。
3. 热加载时别残留强引用
类加载器不能回收,老版本类就卸不掉,最后容易内存泄漏。