公司动态

Java面试核心40问:从原理到实战,告别死记硬背

📅 2026/8/8 13:17:12
Java面试核心40问:从原理到实战,告别死记硬背
1. 面试准备与心态调整为什么“背题”是下下策又到了招聘季最近帮团队面试了不少候选人也和一些朋友交流了面试心得。我发现一个挺普遍的现象很多朋友尤其是工作1-3年的开发者在面对Java基础面试时第一反应就是去网上找一份“最新最全”的面试题合集然后开始死记硬背。结果呢面试官稍微换个角度问或者结合实际场景深入追问一下就露馅了。面试官问“HashMap的负载因子为什么默认是0.75”你背下了“是时间和空间成本的一个折中”但如果追问“这个0.75是怎么算出来的在什么场景下你会考虑调整它”很多人就卡壳了。这其实陷入了一个误区把面试当成了知识点的背诵考试。但真实的面试尤其是技术面考察的是你理解、运用和串联知识的能力而不仅仅是记忆。面试官抛出“ArrayList和LinkedList的区别”这种经典问题他期待的绝不仅仅是“一个基于数组查询快增删慢一个基于链表增删快查询慢”这样的标准答案。他更想听到的是你在实际项目中因为什么需求选择了ArrayList又在什么情况下发现LinkedList更合适在扩容时遇到过性能问题吗你是怎么发现并解决的所以我整理这40个问题目的不是给你一份“标准答案”去背而是希望通过这些问题作为线索帮你重新梳理和深化对Java核心机制的理解。每个问题我都会拆解其背后的原理、设计意图、常见应用场景以及容易踩的坑。我的建议是以点带面深度挖掘。每看到一个题目先尝试自己回答然后对照着去翻源码、写Demo验证、思考它的变体和边界情况。这样准备下来你脑子里形成的是一张相互关联的知识网而不是一堆孤立的碎片。当面试官问起时你就能从容地从一个点展开展示出你的知识深度和思考过程这才是能打动面试官的关键。2. 面向对象核心三要素封装、继承、多态的本质与实战陷阱面向对象是Java的基石但这三个概念很多人只停留在字面理解一到实际编码和面试深挖就容易出问题。2.1 封装不仅仅是private封装常被简单理解为“用private隐藏数据提供public的getter/setter”。这没错但这只是数据封装是封装最浅的一层。封装的本质是隐藏对象的属性和实现细节仅对外公开接口控制对属性的读写和修改。更深层次的理解是“行为封装”和“变化封装”。举个例子一个Order类有一个calculateTotalPrice()方法。这个方法内部可能涉及商品单价、折扣规则、税费计算、运费等一系列复杂逻辑。好的封装会把所有这些计算细节都封装在这个方法内部对外只暴露一个简单的getTotal()接口。调用者完全不用关心价格是怎么算出来的。未来如果折扣规则从满减改为百分比或者增加了会员积分抵扣你只需要修改calculateTotalPrice的内部实现所有调用方的代码都无需改动。这就是封装带来的最大好处隔离变化降低耦合。一个常见的面试陷阱是“Getter/Setter破坏了封装吗” 这要看你怎么用。如果你只是机械地为每个字段生成getter/setter那确实和public字段没啥区别数据可以被随意修改封装形同虚设。正确的做法是按需提供。不是所有字段都需要setter对于集合类字段返回的应该是不可修改的视图或副本防止外部直接修改内部集合例如return Collections.unmodifiableList(this.itemList);。2.2 继承慎用“is-a”关系“继承”听起来很美代码复用嘛。但滥用继承是系统设计僵化的一个重要原因。Java只支持单继承这本身就是一种限制提醒你要谨慎。判断是否使用继承光看“是不是”is-a关系不够。更要看“有没有破坏父类的契约”。经典的“正方形继承长方形”问题就是反例。从数学上说正方形是长方形但长方形有setWidth和setHeight两个独立方法而正方形设置宽高的行为会同时改变另一边这违反了长方形类设定的行为契约里氏替换原则。在实战中我更喜欢用组合Composition替代继承Inheritance尤其是当两者关系不是特别严格的时候。比如Car类需要Engine的功能但Car不是Engine。用class Car { private Engine engine; }比class Car extends Engine要灵活得多。未来你想换一种引擎或者让Car拥有多个引擎混动组合都能轻松应对而继承就会很别扭。面试时如果被问到继承一定要能说出它的缺点破坏了封装子类依赖父类实现细节、耦合度高父类改动可能影响所有子类、以及“脆弱的基类”问题。同时要能自然地带出“组合优于继承”的原则和实际应用场景。2.3 多态运行时绑定的威力与“重写”铁律多态是面向对象最精髓的部分它允许同一个行为具有多种表现形式。其技术基础是方法重写Override和向上转型Upcasting。这里有个必须死记的规则实例方法调用基于运行时类型实际对象类型而静态方法、字段包括静态字段和实例字段的访问基于编译时类型引用变量类型。我举个例子你就明白了class Animal { public String name \Animal\; public void shout() { System.out.println(\Animal shout\); } public static void staticMethod() { System.out.println(\Animal static\); } } class Dog extends Animal { public String name \Dog\; // 隐藏父类字段 Override public void shout() { System.out.println(\Dog shout\); } public static void staticMethod() { System.out.println(\Dog static\); } } public class Test { public static void main(String[] args) { Animal animal new Dog(); // 向上转型 System.out.println(animal.name); // 输出Animal (看编译时类型) animal.shout(); // 输出Dog shout (看运行时类型) animal.staticMethod(); // 输出Animal static (看编译时类型) } }理解了这个就能明白为什么多态如此强大。它让程序架构变得极其灵活。比如我们有一个Payment接口有pay()方法。AlipayPayment和WechatPayment分别实现它。在业务代码中我们只需要持有Payment引用调用pay()方法。具体是支付宝付还是微信付由运行时传入的实际对象决定。增加一个新的支付方式比如BankCardPayment业务代码完全不用修改只需要新增一个实现类并配置进去即可。这是开闭原则对扩展开放对修改关闭的完美体现。面试常问“重写Override和重载Overload的区别”。记住核心重写是子类对父类相同签名方法的重新实现遵循“两同两小一大”规则方法名、参数列表相同返回值类型和抛出异常小于等于父类访问权限大于等于父类发生在运行时重载是同一个类里方法名相同但参数列表不同发生在编译时与返回值、异常、访问权限无关。3. 集合框架深度剖析从数据结构到并发安全Java集合是使用频率最高的API之一但也是面试中细节最多、最容易挖坑的地方。不能只停留在会用得知道它们肚子里的“货”。3.1 ArrayList vs LinkedList选型背后的数据结构博弈这几乎是必问题。但如果你只回答“ArrayList基于数组随机访问快增删慢LinkedList基于双向链表增删快随机访问慢”那只能算及格。面试官想听的是更深层的分析和你的实战经验。ArrayList的底层是一个Object[] elementData。它的“快”与“慢”都和数组的特性紧密相关。随机访问快O(1)因为数组在内存中是连续存储的通过下标i计算内存地址首地址 i * 元素大小就能直接定位这是硬件级别的优化。尾部添加快摊销O(1)直接放在数组末尾就行。但这里有个关键点——“摊销”。因为数组容量不足时需要扩容grow()方法通常会扩容为原来的1.5倍JDK版本有细微差别并将旧数组数据拷贝到新数组这个操作是O(n)的。但由于不是每次添加都扩容所以平均下来摊销时间复杂度仍是O(1)。中间插入/删除慢O(n)因为需要将插入点之后的所有元素向后移动或向前移动一位。数据量越大性能损耗越明显。LinkedList的底层是一个双向链表每个节点Node包含前驱、后继引用和实际数据。头部/尾部插入删除快O(1)只需要修改几个节点的引用。随机访问慢O(n)因为它需要从头部或尾部开始遍历链表直到找到第i个节点。中间插入理论上定位到节点是O(n)修改引用是O(1)。所以整体还是O(n)。那么实战中到底怎么选绝大多数情况用ArrayList。因为现代CPU缓存对连续内存访问数组非常友好遍历效率极高。即使需要中间插入删除只要数据量不是特别大比如几千条以内ArrayList的整体性能通常优于LinkedList因为链表节点在内存中是不连续的缓存命中率低。只有当你需要频繁在列表头部进行插入删除操作比如实现一个队列或栈并且非常确定随机访问的需求很少时才考虑LinkedList。java.util.Deque接口的实现就常用LinkedList。一个重要的内存考量ArrayList预分配的空间capacity可能大于实际元素数量size有空间浪费。LinkedList每个元素都要包装成Node对象包含前后引用和数据对象开销更大。对于海量小对象ArrayList的内存利用率可能更高。3.2 HashMap从哈希碰撞到红黑树的进化HashMap是面试的重灾区必须吃透。它的核心是一个“数组链表/红黑树”的结构。1. 工作原理Put过程当你调用map.put(key, value)时计算key的哈希值hash (key null) ? 0 : (h key.hashCode()) ^ (h 16)。这里的高16位异或低16位是为了让哈希值的高位特征也能参与到后续的数组下标计算中减少哈希冲突。根据哈希值和数组长度计算下标i (n - 1) hash。这里n是数组长度永远是2的幂次方所以(n-1)的二进制是111...与操作相当于取模运算hash % n但效率更高。如果数组该位置为空直接放入新节点。如果不为空哈希碰撞则遍历该位置上的链表或树如果找到相同keyhash相等且key相等或equals则覆盖旧值。如果没找到则将新节点插入链表尾部JDK1.7是头插法1.8后改为尾插法避免在多线程下扩容时产生环形链表。插入后如果链表长度超过TREEIFY_THRESHOLD默认8并且数组长度达到MIN_TREEIFY_CAPACITY默认64则将链表转换为红黑树以提升极端情况下的查询效率从O(n)提升到O(log n)。最后检查数组元素总数是否超过阈值threshold capacity * loadFactor超过则进行扩容。2. 为什么负载因子默认是0.75这是一个时间和空间的权衡。负载因子越小如0.5哈希冲突的概率越低查询越快但空间浪费越严重数组空位多。负载因子越大如1.0空间利用率高但哈希冲突加剧链表变长查询变慢。0.75是统计学和大量实验得出的一个在冲突概率和空间利用率之间较好的平衡点。当元素数量达到数组长度的3/4时扩容能保证在哈希表变得过于拥挤之前就进行重整。3. 扩容机制Resize扩容会新建一个两倍大小的数组然后重新计算所有元素在新数组中的位置Rehash。这个过程是耗时的O(n)。JDK1.8优化了rehash的计算由于新容量是旧容量的两倍元素的新位置要么在原下标i要么在i oldCap。这得益于容量是2的幂次方扩容后(n-1)的二进制比原来多了一位1新位置取决于元素哈希值新增的那一位是0还是1。这避免了重新计算哈希值提升了性能。4. 线程安全问题HashMap不是线程安全的。并发环境下多个线程同时执行put操作可能导致数据覆盖、链表成环JDK1.7、或扩容时数据错乱。解决方案是Collections.synchronizedMap(new HashMap())给整个Map加锁性能较差。ConcurrentHashMap推荐方案。它采用分段锁JDK1.7或CASsynchronizedJDK1.8实现更细粒度的并发控制性能高得多。3.3 ConcurrentHashMap的并发之道既然提到了就深入说一下。JDK1.8的ConcurrentHashMap摒弃了分段锁设计非常精妙。数据结构和HashMap类似也是数组链表/红黑树。初始化与插入采用CASCompare-And-Swap乐观锁来初始化数组和创建链表头节点。如果CAS失败说明有其他线程在竞争则重试或转为加锁。锁的粒度锁的不是整个表而是每个数组桶bucket的头节点synchronized锁。这意味着只要多个线程操作的key散列到不同的桶上就可以完全并行。扩容协助当一个线程触发扩容时其他线程在put时如果发现正在扩容会主动帮助迁移数据helpTransfer加快扩容过程。size计算采用分段的计数方式避免全局锁通过baseCount和CounterCell数组来累加最后求和得到近似值强一致性场景下不适用。4. 异常处理与IO/NIO稳健性与性能的基石4.1 异常体系Error、Exception与自定义异常Java的异常体系是Throwable的两个子类Error和Exception。Error系统级错误应用程序通常无法处理也无需捕获。如OutOfMemoryError、StackOverflowError。面试常问OOM的可能原因内存泄漏、一次性加载过多数据如大文件、创建过多线程等。Exception程序运行时异常需要关注。又分为受检异常Checked Exception继承自Exception但不继承RuntimeException。编译器强制要求处理try-catch或throws。如IOException、SQLException。代表一种“可预期的异常情况”。非受检异常RuntimeException继承自RuntimeException。编译器不强制处理。如NullPointerException、IndexOutOfBoundsException、IllegalArgumentException。通常代表编程错误。关于异常处理的几个最佳实践不要捕获Throwable或Exception然后什么都不做空的catch块。这等于隐藏了错误让问题在后期更难排查。至少应该打印日志。优先捕获更具体的异常而不是宽泛的Exception。异常只用于处理异常情况不要用异常来控制正常的业务流程比如用throw来替代return性能开销大。自定义异常当标准异常无法清晰表达业务错误时使用。通常继承RuntimeException非受检或某个具体的受检异常。要提供有意义的错误信息和错误码。例如class InsufficientBalanceException extends RuntimeException { private String accountId; private BigDecimal required; ... }4.2 IO与NIO从阻塞到多路复用这是理解高性能网络编程的基础。传统BIOBlocking IO模型一个连接一个线程。服务器为每个客户端连接创建一个线程线程在read()、write()、accept()等操作上阻塞等待。缺点线程是昂贵的资源内存、上下文切换开销。当连接数成千上万时系统无法承受。这就是经典的“C10K问题”。NIONew IO / Non-blocking IO核心组件Channel双向通道可以非阻塞读写。FileChannel、SocketChannel、ServerSocketChannel。Buffer数据缓冲区。读写都必须经过Buffer。Selector多路复用器。一个线程可以管理多个Channel。Selector会轮询注册在其上的Channel当某个Channel有事件连接就绪、读就绪、写就绪发生时Selector就会返回这些Channel的集合线程再进行处理。工作流程创建Selector。创建ServerSocketChannel设置为非阻塞绑定端口注册到Selector关注OP_ACCEPT事件。循环调用selector.select()阻塞直到有事件发生。获取发生事件的SelectionKey集合遍历处理。如果是OP_ACCEPT接受连接将新的SocketChannel设置为非阻塞注册到Selector关注OP_READ事件。如果是OP_READ从Channel读取数据到Buffer进行业务处理。如果需要写数据可以关注OP_WRITE事件在可写时写入。优点用一个或少量线程就能处理大量连接极大地提升了系统的可伸缩性。AIOAsynchronous IO基于事件和回调真正意义上的异步IO。应用发起IO操作后立即返回操作系统完成IO后会通知应用。但目前在Linux上实现不够成熟Netty等主流框架也还是基于NIO或Epoll构建。面试常问NIO的Selector在Linux上底层是什么答案是epoll。epoll是Linux内核提供的高效I/O事件通知机制相比早期的select和poll它没有文件描述符数量的限制并且采用事件驱动的方式避免了线性扫描性能更高。Selector.open()在Linux下会创建EPollSelectorImpl。5. 多线程与并发编程核心概念与避坑指南并发是Java面试中最硬核的部分也是最能区分程序员水平的地方。5.1 线程状态与生命周期务必清晰掌握Java线程的6种状态Thread.StateNEW新建尚未调用start()。RUNNABLE可运行。包含了操作系统线程状态中的就绪Ready和运行Running。正在运行或等待CPU时间片。BLOCKED阻塞。线程在等待一个监视器锁monitor lock比如等待进入synchronized同步块/方法。只有针对synchronized关键字才会进入此状态。WAITING无限期等待。需要被其他线程显式唤醒。调用Object.wait()、Thread.join()、LockSupport.park()会进入此状态。TIMED_WAITING限期等待。无需唤醒超时后自动返回。调用Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()等会进入此状态。TERMINATED终止。线程执行完毕。一个关键区别BLOCKED和WAITING/TIMED_WAITING。BLOCKED是等待获取一个锁这个锁当前被别的线程持有。而WAITING是已经持有锁的线程主动释放锁并等待某个条件如wait()需要别人notify()它。5.2 synchronized与LockReentrantLock的深度对比synchronized是Java内置的关键字ReentrantLock是java.util.concurrent.locks包下的一个类。特性synchronizedReentrantLock实现层面JVM层面原生语法支持JDK层面Java代码实现锁的获取隐式获取和释放进入同步块自动获取退出正常或异常自动释放显式调用lock()和unlock()必须在finally块中释放锁灵活性较差。锁的获取和释放是固化的灵活。可尝试非阻塞获取(tryLock())、可中断获取(lockInterruptibly())、超时获取(tryLock(time))公平性非公平锁默认两者都可选构造方法传入true为公平锁条件队列一个锁关联一个隐式的等待/通知队列wait/notify一个锁可以关联多个Condition对象实现更精细的线程间通信性能早期版本性能较差经过大量优化偏向锁、轻量级锁、自旋锁、锁消除、锁粗化等后在大部分场景下与Lock相当甚至更好在高度竞争的场景下可能提供更好的吞吐量如何选择优先使用synchronized语法简洁不易出错自动释放锁JVM持续优化。能满足90%以上的同步需求。考虑使用ReentrantLock当且仅当你需要它的高级特性比如可中断的锁等待避免死锁、尝试非阻塞获取锁、公平锁、或者需要绑定多个条件Condition来实现复杂的线程协作如生产者-消费者模型。5.3 volatile关键字可见性与有序性volatile是轻量级的同步机制。它保证了两件事可见性当一个线程修改了volatile变量的值新值会立即被刷新到主内存。同时其他线程中该变量的缓存行会失效迫使它们必须去主内存读取最新值。禁止指令重排序通过插入内存屏障Memory Barrier来防止编译器和处理器对指令进行重排序优化。但它不保证原子性经典的例子是count这个操作是“读取-修改-写入”三个步骤volatile无法保证这三个步骤作为一个整体不被其他线程打断。解决原子性问题需要用synchronized或AtomicInteger这样的原子类。典型的使用场景状态标志volatile boolean shutdownRequested;一个线程设置它为true其他线程看到后停止工作。单例模式的双重检查锁定DCL在JDK1.5之后volatile可以解决DCL失效问题因为它能防止new Singleton()这行代码内部的指令重排序分配内存、初始化、赋值引用避免其他线程拿到一个未完全初始化的对象。public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // volatile防止此处重排序 } } } return instance; } }5.4 线程池核心参数与工作流程直接使用Thread类创建线程的代价很高。线程池是管理和复用线程的最佳实践。核心参数ThreadPoolExecutorcorePoolSize核心线程数。即使线程空闲也会保留在线程池中除非设置了allowCoreThreadTimeOut。maximumPoolSize最大线程数。线程池允许创建的最大线程数。workQueue工作队列。用于存放等待执行的任务。常用的有LinkedBlockingQueue无界队列除非指定容量。如果任务提交速度持续大于处理速度队列会无限增长可能导致OOM。ArrayBlockingQueue有界队列。SynchronousQueue不存储元素的队列。每个插入操作必须等待另一个线程的移除操作。用于实现直接传递。keepAliveTime非核心线程的空闲存活时间。超过这个时间多余的非核心线程会被回收。threadFactory线程工厂。用于创建新线程可以设置线程名、优先级、守护线程等。handler拒绝策略。当线程池和队列都满了如何处理新提交的任务。AbortPolicy默认抛出RejectedExecutionException。CallerRunsPolicy由调用者线程提交任务的线程自己执行该任务。DiscardPolicy直接丢弃任务不抛异常。DiscardOldestPolicy丢弃队列中最老的任务然后尝试提交新任务。工作流程提交一个任务时如果当前运行的线程数 corePoolSize则创建新线程来执行任务即使其他核心线程空闲。如果 corePoolSize则尝试将任务放入工作队列。如果队列已满且当前线程数 maximumPoolSize则创建新的非核心线程来执行任务。如果队列已满且当前线程数 maximumPoolSize则触发拒绝策略。避坑指南不要使用Executors的快捷工厂方法如newFixedThreadPool,newCachedThreadPool。因为它们使用的队列可能是无界的LinkedBlockingQueue或最大线程数是无限的Integer.MAX_VALUE在任务量突增时容易导致OOM或创建海量线程。手动创建ThreadPoolExecutor根据业务特点CPU密集型、IO密集型设置合理的参数并使用有界队列。给线程池设置有意义的名称通过ThreadFactory方便监控和问题排查。记得关闭线程池shutdown()或shutdownNow()。6. JVM内存模型与垃圾回收理解程序运行的底层环境6.1 运行时数据区堆、栈、方法区JVM内存分为以下几个主要区域程序计数器线程私有。指向当前线程正在执行的字节码指令地址。分支、循环、跳转、异常处理都依赖它。Java虚拟机栈线程私有。生命周期与线程相同。每个方法执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。我们常说的“栈内存”主要指这里的局部变量表部分。局部变量表存放基本数据类型和对象引用。StackOverflowError就是栈深度超过虚拟机允许的最大深度时抛出的。本地方法栈为Native方法服务。Java堆线程共享。几乎所有对象实例和数组都在这里分配内存。是垃圾回收器管理的主要区域因此也叫“GC堆”。堆可以细分为新生代Eden, Survivor0, Survivor1和老年代。方法区线程共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。JDK1.8之前叫“永久代”PermGen1.8之后改为“元空间”Metaspace使用本地内存不再受JVM堆大小限制避免了PermGen的OOM问题。运行时常量池方法区的一部分。存放编译期生成的各种字面量和符号引用。6.2 垃圾回收算法与收集器如何判断对象已死引用计数法简单但无法解决循环引用问题。Java未采用。可达性分析算法主流。以一系列“GC Roots”对象作为起点向下搜索走过的路径称为“引用链”。如果一个对象到GC Roots没有任何引用链相连则判定为可回收。GC Roots包括虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象等。垃圾回收算法标记-清除先标记所有需要回收的对象然后统一清除。问题效率不高产生内存碎片。复制将内存分为两块每次只用一块。垃圾回收时将存活对象复制到另一块然后清空当前块。效率高无碎片但浪费一半空间。新生代的Eden和Survivor区采用的就是这种算法的优化版。标记-整理标记过程同“标记-清除”但后续不是直接清除而是让所有存活对象向一端移动然后直接清理掉边界以外的内存。老年代通常采用这种算法。分代收集理论根据对象存活周期的不同将堆划分为新生代和老年代。新生代对象朝生夕死回收频繁。采用复制算法。分为Eden区和两个Survivor区S0, S1。新对象在Eden分配。Minor GC时将Eden和S0中存活的对象复制到S1然后清空Eden和S0。年龄增加到一定阈值默认15的对象会晋升到老年代。老年代对象存活时间长。采用标记-清除或标记-整理算法。当老年代空间不足时会触发Major GC/Full GC速度比Minor GC慢得多。常见的垃圾收集器Serial/Serial Old单线程简单高效适用于客户端或小内存应用。ParNewSerial的多线程并行版本用于新生代。Parallel Scavenge/OldJDK8默认组合。关注吞吐量用户代码运行时间/(用户代码运行时间GC时间)。CMS以获取最短回收停顿时间为目标。过程复杂初始标记STW-并发标记-重新标记STW-并发清除。会产生“浮动垃圾”且内存碎片问题严重。G1JDK9后默认。将堆划分为多个大小相等的Region可预测停顿时间整体上看是“标记-整理”局部两个Region之间是“复制”。适合大内存、多核服务器。ZGC/Shenandoah新一代低延迟收集器停顿时间可达亚毫秒级。6.3 类加载机制双亲委派模型及其破坏类加载过程加载Loading- 链接Linking验证、准备、解析- 初始化Initialization- 使用 - 卸载。双亲委派模型类加载器之间的层次关系。启动类加载器Bootstrap ClassLoader加载JAVA_HOME/lib下的核心类库。扩展类加载器Extension ClassLoader加载JAVA_HOME/lib/ext下的类。应用程序类加载器Application ClassLoader加载用户类路径ClassPath上的类。自定义类加载器用户自定义。工作过程当一个类加载器收到加载请求时它首先不会自己去加载而是把这个请求委派给父类加载器去完成。只有当父加载器反馈无法完成在自己的搜索范围内没找到该类时子加载器才会尝试自己去加载。好处避免类的重复加载保证Java核心API的类型安全。比如java.lang.Object无论哪个加载器加载最终都会委派给启动类加载器从而保证在JVM中只有一份。安全防止用户自定义一个恶意的java.lang.String类来破坏核心库。破坏双亲委派模型的场景SPIService Provider Interface机制如JDBC。java.sql.DriverManager在启动类加载器中但它需要加载由各个厂商实现在ClassPath下的java.sql.Driver接口实现类。这需要线程上下文类加载器来反向委派。热部署、热替换如OSGi、Tomcat。每个Web应用或模块可能需要自己的类加载器来加载独立的类版本这就需要自定义类加载器并打破双亲委派优先自己加载加载不了再委派。7. 其他高频核心问题精讲7.1 String、StringBuilder、StringBufferString不可变字符序列。用final char[] value存储JDK9后改为byte[]。任何修改操作concat,substring等都会生成新的String对象。频繁拼接字符串性能极差。StringBuffer可变字符序列。线程安全所有方法都用synchronized修饰。性能有损耗。StringBuilder可变字符序列。线程不安全。性能最高。选择单线程操作大量字符串拼接用StringBuilder多线程环境用StringBuffer定义常量或不需要修改的字符串用String。字符串常量池JVM为了提升性能和减少内存开销专门为String开辟的一块内存区域。String s1 \abc\;这样的字面量会先在常量池中查找有则返回引用无则创建并放入池中。String s2 new String(\abc\);会在堆中创建一个新对象。s1.intern()方法可以将堆中的字符串对象尝试放入常量池。7.2 和 equals 的区别对于基本数据类型比较的是值是否相等对于引用数据类型比较的是内存地址是否相等即是否是同一个对象。equals定义在Object类中默认实现就是。但很多类如String,Integer重写了equals方法使其比较的是对象的逻辑内容是否相等。重写equals必须同时重写hashCode这是Object类的通用约定。如果两个对象equals相等那么它们的hashCode必须相等。反之hashCode相等equals不一定相等。这主要是为了支持基于哈希的集合类如HashMap,HashSet的正常工作。7.3 final, finally, finalizefinal修饰类类不可被继承。修饰方法方法不可被重写。修饰变量变量一旦初始化就不能再修改对于基本类型是值不能变对于引用类型是引用不能变但对象内部状态可以变。finally异常处理的一部分用于定义必须执行的代码块。通常用于释放资源如关闭流、数据库连接。finally块中的代码几乎总是会执行除非在try或catch中调用了System.exit()或者线程被终止。finalizeObject类的一个方法。垃圾回收器在回收对象之前会调用该方法。但不推荐使用因为调用时机不确定性能差且不能保证一定会被调用。资源释放应该用try-with-resources或显式地在finally块中完成。7.4 反射与动态代理反射在运行时动态获取类的信息类名、方法、字段、注解等并操作对象。核心APIClass,Method,Field,Constructor。虽然强大但性能较差需要做安全检查、方法调用无法被JIT深度优化且破坏了封装性。常用于框架如Spring的IoC、IDE、测试工具等。动态代理在运行时动态创建代理类和对象。Java原生支持基于接口的代理java.lang.reflect.ProxySpring AOP的JDK动态代理就是基于此。还有基于类的代理如CGLIB。动态代理是AOP面向切面编程实现的基础。7.5 序列化与反序列化将对象的状态信息转换为可以存储或传输的形式字节流的过程叫序列化反之叫反序列化。实现Serializable接口即可。注意serialVersionUID最好显式声明一个。如果不声明JVM会根据类结构自动生成一个。一旦类结构发生变化如增删字段自动生成的UID会变导致反序列化失败。显式声明可以保持兼容性。敏感字段不要序列化可以用transient关键字修饰。自定义序列化过程可以重写writeObject和readObject方法。替代方案考虑更高效的序列化框架如Protobuf、Kryo、Hessian等。准备Java基础面试关键在于把每个知识点都“嚼碎了”理解其设计初衷、实现原理和适用场景并能在脑海中形成知识网络。当你被问到HashMap时能自然联想到ConcurrentHashMap的优化谈到synchronized时能对比出Lock的优劣说到GC能分析出不同收集器对应用性能的影响。这样无论面试官从哪个角度提问你都能从容应对展现出扎实的基本功和清晰的逻辑思维。这份清单里的40个问题是一个很好的起点但真正的功夫在平时的积累和思考。多写代码多读源码哪怕是JDK里的一小部分多思考“为什么这样设计”你的技术深度自然会提升。