问题 1:请解释Java中ArrayList
和LinkedList
的区别?
回答思路:
- 数据结构:明确底层实现(数组 vs 双向链表)。
- 性能对比:从查询、插入/删除、内存占用三方面分析。
- 适用场景:根据性能特点给出使用建议。
- 补充细节:扩容机制、线程安全性等。
示例回答:
ArrayList基于动态数组实现,支持快速随机访问(时间复杂度O(1)),但在中间插入或删除元素时,需要移动后续元素,性能较差(平均O(n))。
LinkedList基于双向链表实现,插入和删除操作只需修改指针(O(1)),但随机访问需要遍历链表(O(n))。
内存方面,LinkedList每个节点需要额外存储前后指针,占用更多空间。
适用场景:
- 频繁查询用ArrayList;
- 频繁增删用LinkedList。
此外,ArrayList默认初始容量为10,扩容时增长50%;LinkedList无需扩容。两者均非线程安全。
问题 2:什么是线程安全?如何保证HashMap
的线程安全?
回答思路:
- 定义线程安全:多线程下操作共享数据的结果符合预期。
- HashMap的问题:解释多线程下扩容可能导致的死循环或数据丢失。
- 解决方案:列举至少两种方法(ConcurrentHashMap、Collections工具类)。
- 对比方案:说明不同方案的优缺点。
示例回答:
线程安全指多线程环境下,对共享资源的操作不会出现数据不一致问题。
HashMap在多线程同时put时可能触发扩容,导致链表成环(JDK7)或数据覆盖(JDK8)。
保证线程安全的方法:
- 使用ConcurrentHashMap:JDK8前采用分段锁,JDK8后使用CAS+synchronized锁单个桶,性能优于Hashtable。
- 用Collections.synchronizedMap()包装:内部通过synchronized代码块保证安全,但高并发时性能较差。
推荐优先使用ConcurrentHashMap,它兼顾了线程安全和性能。
问题 3:synchronized
和ReentrantLock
有什么区别?
回答思路:
- 基本区别:实现层面(JVM vs API)、功能特性。
- 功能对比:公平锁、中断等待、条件变量。
- 使用建议:场景选择(简单同步用synchronized,复杂需求用Lock)。
示例回答:
synchronized是JVM级别的关键字,ReentrantLock是JDK提供的API类。主要区别:
- 锁的获取:
- synchronized自动获取/释放锁;
- ReentrantLock需手动调用lock()/unlock(),更灵活。
- 公平性:
- synchronized非公平;
- ReentrantLock可设置为公平锁(减少线程饥饿)。
- 功能扩展:
- ReentrantLock支持
tryLock()
(尝试获取锁)、lockInterruptibly()
(可中断等待)和多个条件变量(Condition)。
推荐在简单同步场景使用synchronized;需要高级功能(如超时、公平性)时使用ReentrantLock。
问题 4:JVM内存模型包含哪些区域?各有什么作用?
回答思路:
- 分区结构:堆、方法区、栈、本地方法栈、程序计数器。
- 核心作用:每部分的职责(对象存储、线程私有数据等)。
- 补充说明:JDK8后方法区的变化(元空间替代永久代)。
示例回答:
JVM内存主要分为:
- 堆(Heap) :存放对象实例,是垃圾回收的主要区域。
- 方法区(Method Area) :存储类信息、常量、静态变量(JDK8后由元空间Metaspace实现)。
- 虚拟机栈(VM Stack) :线程私有,保存方法调用时的栈帧(局部变量、操作数栈等)。
- 本地方法栈(Native Stack) :为Native方法服务。
- 程序计数器(PC Register) :记录当前线程执行的位置。
其中,堆和方法区是线程共享的;栈、本地方法栈和程序计数器是线程私有的。
问题 5:什么是泛型擦除?如何理解其局限性?
回答思路:
- 定义泛型擦除:编译后泛型类型信息被擦除。
- 局限性举例:无法用泛型类型做实例化、类型判断等。
- 绕过方法:反射或传递Class对象。
示例回答:
泛型擦除指Java在编译期间将泛型类型转换为原始类型(如
List<String>
变为List
),并在字节码中移除类型信息。
局限性:
- 无法通过
new T()
实例化泛型对象(运行时无类型信息);- 不能使用
instanceof
判断泛型类型(如list instanceof ArrayList<String>
会编译错误)。
解决方法:
- 通过反射创建对象(需传递Class<T>参数);
- 使用显式类型检查(如强制转换)。
泛型擦除的设计是为了兼容旧版本JDK,但限制了运行时操作泛型的能力。
练习建议:
- 理解问题本质:先明确问题考察的知识点(如集合、线程、JVM等)。
- 结构化回答:用“总-分-总”结构,先概括核心点,再分点详细说明。
- 结合实际代码:适当举例(如HashMap的put流程)会让回答更生动。
- 控制时间:每个回答尽量在2分钟内完成,避免冗长。
问题 6:Java中的深拷贝和浅拷贝有什么区别?如何实现深拷贝?
回答思路:
- 定义区别:
- 浅拷贝:复制对象的基本字段,引用字段仍指向原对象(修改副本会影响原对象)。
- 深拷贝:完全复制对象及其引用对象(副本与原对象完全独立)。
- 实现方法:
- 手动逐层复制(重写
clone()
方法)。 - 序列化与反序列化(如用
ObjectOutputStream
)。 - 第三方库(如Apache Commons Lang的
SerializationUtils
)。
- 手动逐层复制(重写
示例回答:
浅拷贝只复制对象的直接属性,若属性是引用类型(如数组、对象),副本和原对象会共享这些引用。深拷贝会递归复制所有引用对象,使副本完全独立。
实现深拷贝的两种方式:
- 重写
clone()
方法,对每个引用类型字段手动调用其clone()
方法(需所有引用对象支持深拷贝)。- 将对象序列化为字节流再反序列化(需要实现
Serializable
接口)。例如:ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(original); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); DeepCopy copy = (DeepCopy) ois.readObject();
问题 7:String
、StringBuilder
和StringBuffer
的区别?
回答思路:
- 不可变性:
String
不可变,每次修改会生成新对象。 - 可变性:
StringBuilder
和StringBuffer
内部用字符数组实现,可动态修改。 - 线程安全:
StringBuffer
用synchronized
保证线程安全,StringBuilder
非线程安全但性能更高。
示例回答:
String
是不可变类,对字符串的拼接、替换等操作会频繁创建新对象,适合少量操作的场景。
StringBuilder
和StringBuffer
都是可变类,适合频繁修改字符串的场景。
关键区别:
StringBuffer
是线程安全的(方法用synchronized
修饰),但性能较低;StringBuilder
非线程安全,性能更高(单线程下优先使用)。
例如,在循环中拼接字符串应使用StringBuilder
:StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100; i++) { sb.append(i); // 避免生成大量String对象 }
问题 8:什么是Java的反射机制?举例说明其应用场景和缺点。
回答思路:
- 定义:在运行时动态获取类信息(属性、方法、构造器等)并操作对象。
- 应用场景:框架设计(如Spring的依赖注入)、动态代理、IDE的代码提示。
- 缺点:性能开销大、破坏封装性、代码可读性差。
示例回答:
反射机制允许程序在运行时通过
Class
对象获取类的结构并动态调用方法或修改字段值。
典型应用:
- Spring框架通过反射创建Bean实例并注入依赖。
- JUnit通过反射查找带有
@Test
注解的方法并执行。
缺点:- 反射调用比直接调用慢(需检查访问权限、解析类信息);
- 绕过访问修饰符(如能调用私有方法),破坏封装性;
- 代码可读性和维护性降低(如
Method.invoke()
的参数是字符串)。
问题 9:Java中的equals()
和==
有什么区别?如何正确重写equals()
方法?
回答思路:
- 区别:
==
比较对象内存地址(基本类型比较值)。equals()
默认行为同==
,但可重写为逻辑相等(如String比较内容)。
- 重写规则:满足自反性、对称性、传递性、一致性,并重写
hashCode()
。
示例回答:
==
用于判断两个对象是否是同一个内存地址(或基本类型的值是否相等)。
equals()
默认比较地址,但可被重写为逻辑相等。例如,String重写了equals()
,只要字符序列相同就返回true。
重写步骤:
- 检查是否为同一对象(
if (this == obj) return true;
)。- 检查类型是否匹配(
if (!(obj instanceof MyClass)) return false;
)。- 强制类型转换后逐个比较关键字段。
- 必须同时重写
hashCode()
,确保相等的对象哈希码相同。
问题 10:解释Java的类加载机制(双亲委派模型)及其作用。
回答思路:
- 加载流程:自底向上委派,自顶向下加载。
- 类加载器层级:Bootstrap → Extension → Application → Custom。
- 作用:避免重复加载、保证核心类安全(如防止自定义
java.lang.String
)。
示例回答:
双亲委派模型规定类加载器的加载流程:
- 收到类加载请求时,先委派给父加载器处理。
- 父加载器无法完成时(如父加载器范围内找不到类),子加载器才尝试加载。
类加载器层级:
- Bootstrap:加载JRE核心库(如rt.jar)。
- Extension:加载
jre/lib/ext
目录的类。- Application:加载用户类路径(classpath)的类。
作用:- 避免重复加载(父加载器已加载的类,子加载器不会重复加载)。
- 防止核心API被篡改(如自定义的
java.lang.String
不会被加载)。
问题 11:什么是Java的内存泄漏?举例说明常见场景。
回答思路:
- 定义:对象不再被使用,但被意外保留引用导致无法被GC回收。
- 场景:静态集合、未关闭的资源(如数据库连接)、监听器未注销。
示例回答:
内存泄漏指程序在运行中,不再需要的对象因被错误引用而无法被垃圾回收,最终导致内存耗尽。
常见场景:
- 静态集合长期持有对象:
public class Leak { static List<Object> list = new ArrayList<>(); void add(Object obj) { list.add(obj); // 即使obj不再使用,list仍持有其引用 } }
- 未关闭资源:如打开文件流或数据库连接后忘记调用
close()
。- 监听器未注销:注册事件监听后,对象销毁前未移除监听器。