1. Java常用类深度解析
Java常用类是每个开发者必须熟练掌握的基础知识,它们就像建筑的地基,支撑着整个Java开发体系。在实际开发中,Object类、包装类和String类的使用频率极高,也是面试官最喜欢考察的重点。
1.1 Object类:万物起源
Object类是Java中所有类的超父类,理解它的核心方法对于掌握Java面向对象编程至关重要。我在实际开发中发现,很多初级开发者对这些方法的理解往往停留在表面。
equals()方法是最容易出错的地方。记得有一次面试,我问候选人:"如果重写equals()但不重写hashCode()会有什么问题?"结果10个人中有7个答不上来。实际上,这会导致在使用HashSet、HashMap等集合时出现严重问题。根据Java规范,当两个对象equals()返回true时,它们的hashCode()必须相同。
java复制@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
toString()方法看似简单,但在日志记录和调试时非常有用。我建议始终重写这个方法,而不是使用默认实现。在团队协作中,统一的toString()格式能大大提高问题排查效率。
1.2 包装类:基本类型的华丽变身
包装类解决了基本类型不能参与面向对象操作的问题,但自动装箱拆箱背后隐藏着不少坑。最经典的例子就是Integer的缓存问题:
java复制Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false
这是因为Integer在-128到127之间做了缓存。在实际项目中,我曾遇到过因为不了解这个机制导致的bug:比较两个ID时使用了==而不是equals(),当ID超过127时就会出现问题。
实用建议:
- 包装类比较一定要用equals()方法
- 警惕自动拆箱时的NullPointerException
- 大量数学运算还是用基本类型效率更高
1.3 String类:不可变的艺术
String的不可变性是面试必问点。很多开发者知道String不可变,但说不清为什么这样设计。实际上,这种设计带来了诸多好处:
- 安全性:字符串常用于敏感信息,不可变防止被篡改
- 线程安全:天然线程安全
- 缓存哈希值:提升性能
- 字符串池优化:减少内存消耗
java复制String s1 = "hello"; // 字符串池
String s2 = new String("hello"); // 堆内存
System.out.println(s1 == s2); // false
性能优化技巧:
- 少量字符串拼接用+即可
- 循环内拼接一定要用StringBuilder
- 使用String.format()提高可读性
- 优先使用StringUtils等工具类处理字符串
2. 集合框架实战指南
Java集合框架是日常开发中使用最频繁的API之一。根据我的经验,90%的业务代码都会涉及集合操作。理解不同集合的特性和适用场景,能显著提升代码质量和性能。
2.1 Collection体系精讲
List接口是我们最常用的集合类型,其中ArrayList和LinkedList的选择往往让人纠结。通过一个实际案例来说明:我们有个需求要处理10万条数据,需要频繁按索引查询和中间插入。
java复制// ArrayList测试
List<Integer> arrayList = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
arrayList.add(0, i); // 头部插入
}
System.out.println("ArrayList耗时:" + (System.currentTimeMillis() - start));
// LinkedList测试
List<Integer> linkedList = new LinkedList<>();
start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
linkedList.add(0, i); // 头部插入
}
System.out.println("LinkedList耗时:" + (System.currentTimeMillis() - start));
测试结果会显示LinkedList明显快于ArrayList,因为前者不需要移动元素。但如果是随机访问:
java复制arrayList.get(50000); // 极快
linkedList.get(50000); // 需要遍历
选择原则:
- 查询多、增删少 → ArrayList
- 增删多、查询少 → LinkedList
- 线程安全 → CopyOnWriteArrayList
- 去重 → HashSet/LinkedHashSet
- 排序 → TreeSet
2.2 Map体系深度解析
HashMap是面试重点中的重点,我建议每个Java开发者都应该了解它的实现原理。JDK1.8后的HashMap引入了红黑树优化,当链表长度超过8时会转换为红黑树。
java复制Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
// 遍历方式1:entrySet(推荐)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 遍历方式2:Java8 forEach
map.forEach((k, v) -> System.out.println(k + ": " + v));
HashMap常见问题:
- 为什么容量是2的幂次?为了优化哈希计算:index = (n - 1) & hash
- 加载因子为什么默认0.75?空间和时间成本的折中
- 线程不安全的表现:可能形成环形链表
并发场景选择:
- ConcurrentHashMap:分段锁,性能好
- Hashtable:全表锁,已过时
- Collections.synchronizedMap:包装器,性能一般
2.3 集合工具类妙用
Collections和Arrays类提供了很多实用方法,可以大大简化代码:
java复制List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 排序
Collections.sort(list);
Collections.sort(list, Comparator.reverseOrder());
// 不可变集合
List<Integer> unmodifiableList = Collections.unmodifiableList(list);
// 数组转集合
String[] array = {"a", "b", "c"};
List<String> listFromArray = new ArrayList<>(Arrays.asList(array));
注意事项:
- Arrays.asList()返回的是固定大小列表,不能add/remove
- 集合转数组要使用toArray(T[] a)形式,避免类型转换问题
- 使用Collections.emptyList()而不是new ArrayList()表示空集合
3. IO流编程实践
IO流是Java中处理输入输出的核心API,虽然现在很多项目都用第三方库封装了,但理解底层原理仍然很重要。我曾经遇到过因为不理解缓冲流导致性能问题的案例。
3.1 流分类与选择
IO流的选择首先要明确三个维度:
- 方向:输入流 vs 输出流
- 单位:字节流 vs 字符流
- 功能:节点流 vs 过滤流
字节流与字符流的区别:
- 字节流(InputStream/OutputStream):处理所有类型文件
- 字符流(Reader/Writer):只能处理文本文件,内部有编码转换
java复制// 复制文件(字节流)
try (InputStream is = new FileInputStream("source.jpg");
OutputStream os = new FileOutputStream("target.jpg")) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
}
// 读取文本文件(字符流)
try (BufferedReader reader = new BufferedReader(new FileReader("text.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
3.2 高效IO编程技巧
缓冲流的重要性:不使用缓冲流时,每次读写都是直接操作磁盘,性能极差。我曾经测试过复制一个100MB的文件:
java复制// 无缓冲:约5000ms
// 有缓冲:约500ms
NIO新特性:Java NIO提供了更高效的IO处理方式,核心概念包括:
- Channel:双向通道
- Buffer:数据缓冲区
- Selector:多路复用器
java复制// NIO文件复制
try (FileChannel inChannel = new FileInputStream("source.txt").getChannel();
FileChannel outChannel = new FileOutputStream("target.txt").getChannel()) {
inChannel.transferTo(0, inChannel.size(), outChannel);
}
实用建议:
- 始终使用try-with-resources确保流关闭
- 大文件处理考虑使用NIO或内存映射文件
- 文本文件注意编码问题,推荐明确指定UTF-8
- 使用Files工具类简化常见操作
3.3 对象序列化陷阱
对象序列化看似简单,但隐藏着很多坑。我曾经遇到过序列化版本不一致导致的InvalidClassException。
java复制public class User implements Serializable {
private static final long serialVersionUID = 1L; // 必须显式声明
private String name;
private transient String password; // 不会被序列化
}
注意事项:
- 显式声明serialVersionUID,避免自动生成导致的不兼容
- transient修饰的字段不会被序列化
- 静态变量不会被序列化
- 序列化对象要保证其所有属性也是可序列化的
4. 多线程并发精要
多线程是Java面试的重点难点,也是实际项目中最容易出问题的地方。根据我的经验,90%的线程问题都出在共享资源的访问上。
4.1 线程创建方式对比
创建线程有三种方式,每种适用场景不同:
java复制// 1. 继承Thread类
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
// 2. 实现Runnable接口(推荐)
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running");
}
}
// 3. 实现Callable接口(带返回值)
class MyCallable implements Callable<String> {
public String call() throws Exception {
return "Callable result";
}
}
// 使用示例
new MyThread().start();
new Thread(new MyRunnable()).start();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get()); // 获取返回值
选择建议:
- 优先选择Runnable,因为Java不支持多继承
- 需要返回值时用Callable
- 使用线程池而不是直接创建线程
4.2 线程同步机制
synchronized是最基本的同步手段,但很多人不知道它的实现原理。实际上:
- 同步代码块使用monitorenter/monitorexit指令
- 同步方法使用ACC_SYNCHRONIZED标志
- 锁信息存储在对象头中
java复制// 同步代码块
public void doSomething() {
synchronized(this) {
// 临界区
}
}
// 同步方法
public synchronized void doSomething() {
// 临界区
}
Lock接口更灵活:
java复制Lock lock = new ReentrantLock();
try {
lock.lock();
// 临界区
} finally {
lock.unlock(); // 必须在finally中释放
}
死锁案例:
java复制// 线程1
synchronized(resourceA) {
synchronized(resourceB) {}
}
// 线程2
synchronized(resourceB) {
synchronized(resourceA) {}
}
避免死锁的方法:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 静态代码分析工具检测
4.3 线程池最佳实践
直接创建线程的代价很高,线程池是更好的选择。Java提供了Executors工具类,但不建议直接使用,因为:
- newFixedThreadPool和newSingleThreadExecutor使用无界队列,可能OOM
- newCachedThreadPool最大线程数是Integer.MAX_VALUE,可能创建过多线程
推荐手动创建线程池:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
线程池参数设置经验:
- CPU密集型:核心线程数 = CPU核数 + 1
- IO密集型:核心线程数 = CPU核数 * 2
- 混合型:拆分线程池,分别处理
常见问题排查:
- 线程泄漏:忘记关闭线程池
- 资源耗尽:任务队列过大
- 响应迟缓:线程数不足
- 任务丢失:拒绝策略不当
5. 反射与设计模式实战
反射和设计模式是Java进阶的必经之路,它们能让你的代码更加灵活和优雅。我在框架开发中经常使用这些技术。
5.1 反射高级应用
反射不仅能获取类信息,还能动态操作对象。Spring等框架大量使用反射实现依赖注入。
java复制Class<?> clazz = Class.forName("com.example.User");
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object user = constructor.newInstance("张三", 25);
Method method = clazz.getMethod("setName", String.class);
method.invoke(user, "李四");
Field field = clazz.getDeclaredField("age");
field.setAccessible(true); // 突破private限制
field.set(user, 30);
性能考虑:
- 反射调用比直接调用慢约50-100倍
- 可以缓存Class对象和Method对象提升性能
- 考虑使用MethodHandle替代反射
实际应用场景:
- 框架开发(如Spring IOC)
- 动态代理
- 注解处理器
- 序列化/反序列化工具
5.2 单例模式演进
单例模式看似简单,但要写出线程安全的实现并不容易。我从初级到高级的演进过程:
版本1:饿汉式
java复制class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
问题:类加载时就初始化,可能浪费资源
版本2:懒汉式(非线程安全)
java复制class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题:多线程环境下可能创建多个实例
版本3:同步方法
java复制public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
问题:每次获取实例都要同步,性能差
版本4:双重检查锁
java复制class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注意:必须使用volatile防止指令重排序
版本5:静态内部类(推荐)
java复制class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
优点:懒加载、线程安全、无同步开销
5.3 工厂模式实践
工厂模式解耦了对象的创建和使用,我在项目中最常用的是简单工厂和抽象工厂。
简单工厂示例:
java复制interface Product {}
class ProductA implements Product {}
class ProductB implements Product {}
class ProductFactory {
public static Product createProduct(String type) {
switch(type) {
case "A": return new ProductA();
case "B": return new ProductB();
default: throw new IllegalArgumentException();
}
}
}
抽象工厂示例:
java复制interface GUIFactory {
Button createButton();
Menu createMenu();
}
class WinFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
public Menu createMenu() { return new WinMenu(); }
}
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
public Menu createMenu() { return new MacMenu(); }
}
设计模式应用心得:
- 不要为了用模式而用模式
- 优先考虑简单性,必要时才引入模式
- 结合Spring等框架的特性使用模式
- 模式是手段不是目的,解决实际问题才是关键
6. Java核心知识面试精粹
经过多年的面试官经验,我总结了一些高频Java面试题及其考察点,帮助你在面试中游刃有余。
6.1 Java基础高频题
问题1:HashMap和Hashtable的区别?
- 线程安全:Hashtable是,HashMap不是
- null值:Hashtable不允许,HashMap允许
- 性能:HashMap通常更快
- 迭代器:Hashtable用Enumeration,HashMap用Iterator
- 继承关系:不同父类
问题2:ArrayList的扩容机制?
- 默认初始容量10
- 扩容时增加为原来的1.5倍
- 扩容使用Arrays.copyOf
- 预估容量可减少扩容次数
问题3:String为什么设计为不可变?
- 安全性:如网络连接、文件路径等
- 线程安全:无需同步
- 缓存哈希值:提升性能
- 字符串池优化:减少重复创建
6.2 并发编程必问题
问题1:volatile关键字的作用?
- 保证可见性:一个线程修改后对其他线程立即可见
- 禁止指令重排序
- 不保证原子性(如i++)
问题2:ThreadLocal原理和使用场景?
- 每个线程有自己的变量副本
- 内部使用ThreadLocalMap存储
- 典型场景:SimpleDateFormat、数据库连接
- 注意内存泄漏问题
问题3:CAS机制和ABA问题?
- CAS:Compare And Swap,无锁算法
- 实现:Unsafe类提供native方法
- ABA问题:值从A变B又变回A,CAS会误认为没变
- 解决方案:版本号(AtomicStampedReference)
6.3 JVM相关深入题
问题1:Java内存模型(JMM)是什么?
- 定义了线程如何与内存交互
- 主内存和工作内存的概念
- happens-before原则
- volatile、synchronized的内存语义
问题2:GC垃圾回收算法有哪些?
- 标记-清除:产生碎片
- 复制算法:空间浪费
- 标记-整理:适合老年代
- 分代收集:新生代(复制)、老年代(标记-整理)
问题3:类加载过程是怎样的?
- 加载:获取二进制字节流
- 验证:确保符合JVM规范
- 准备:分配内存,设置初始值
- 解析:符号引用转直接引用
- 初始化:执行clinit方法
6.4 项目经验类问题
问题1:你遇到过什么内存泄漏问题?如何解决的?
- 案例:静态集合持有对象、未关闭资源、监听器未注销
- 工具:MAT、VisualVM
- 解决方案:弱引用、及时释放资源
问题2:如何优化Java程序性能?
- 基准测试:JMH
- 瓶颈定位:Profiler工具
- 常见优化:减少对象创建、使用缓存、并发编程
- JVM调优:堆大小、GC策略
问题3:你读过哪些Java开源项目的源码?有什么收获?
- 推荐阅读:JDK核心类、Spring框架、Guava
- 学习重点:设计思想、实现细节、代码风格
- 收获举例:对集合的理解更深、学到设计模式应用
在准备面试时,我建议不仅要记住答案,更要理解背后的原理。最好的学习方式是通过实际项目验证这些知识点,遇到问题时深入分析,这样才能真正掌握Java核心技术。