Java基础20道经典面试题(二)
-
-
- [💡 面向对象与设计模式](#💡 面向对象与设计模式)
- [💡 Java核心机制与特性](#💡 Java核心机制与特性)
- [💡 集合与容器进阶](#💡 集合与容器进阶)
- [💡 多线程与并发基础](#💡 多线程与并发基础)
- [💡 综合与编码陷阱](#💡 综合与编码陷阱)
- [💎 高效准备建议](#💎 高效准备建议)
-
20道Java基础高频面试题,继续聚焦于那些既能考察深度、又极具迷惑性 的考点。
这些题目按专题汇总如下:
| 专题 | 题目 |
|---|---|
| 面向对象与设计 | 1-4:单例模式、值传递与引用传递、Object类方法、深/浅拷贝 |
| 核心机制与特性 | 5-12:JVM内存模型、String常量池、类加载、Lambda、try-with-resources、switch、自动拆装箱 |
| 集合与容器 | 13-16:HashMap、LinkedHashMap、集合排序、fail-fast |
| 并发编程基础 | 17-19:synchronized与Lock、volatile、创建线程方式 |
| 综合与编码 | 20:==与equals陷阱 |
💡 面向对象与设计模式
这部分考察你对Java设计思想和经典模式的掌握。
-
手写一个线程安全的单例模式(Singleton),并说明几种常见写法的优缺点。
- 核心分析 :这是必考题。要掌握饿汉式 (简单但可能提前加载)、懒汉式 (需要双重检查锁+
volatile防止指令重排)、静态内部类式 (推荐,利用类加载机制保证线程安全且延迟加载),以及枚举式 (最简洁,并能防止反射攻击)。要能解释清楚volatile在双重检查锁中的作用。
- 核心分析 :这是必考题。要掌握饿汉式 (简单但可能提前加载)、懒汉式 (需要双重检查锁+
-
Java是"值传递"还是"引用传递"?
- 核心分析 :Java只有值传递 。对于基本类型,传递的是值的副本。对于引用类型,传递的是对象引用的副本 (可以理解为地址的副本)。这意味着在方法内修改引用指向的对象(如调用
setter)会影响原对象,但让引用指向一个新对象则不影响原引用。
- 核心分析 :Java只有值传递 。对于基本类型,传递的是值的副本。对于引用类型,传递的是对象引用的副本 (可以理解为地址的副本)。这意味着在方法内修改引用指向的对象(如调用
-
Object类中有哪些常用方法?请至少列出5个并简要说明。- 核心分析 :必须烂熟于心:
getClass()(获取运行时类)、hashCode()(返回哈希码)、equals(Object obj)(判断对象相等)、clone()(实现克隆)、toString()(返回字符串表示)、notify()/notifyAll()/wait()(线程通信)、finalize()(已废弃,用于垃圾回收前的清理)。
- 核心分析 :必须烂熟于心:
-
什么是深拷贝(Deep Copy)和浅拷贝(Shallow Copy)?如何实现深拷贝?
- 核心分析 :浅拷贝 :只复制对象本身和其基本类型字段,引用类型字段与原对象共享同一对象。深拷贝 :对象及其所有引用关联的对象都完全复制一份。实现方式:① 实现
Cloneable接口并重写clone()方法,对引用类型递归调用clone();② 通过序列化与反序列化(常用且更可靠);③ 使用一些工具库进行复制。
- 核心分析 :浅拷贝 :只复制对象本身和其基本类型字段,引用类型字段与原对象共享同一对象。深拷贝 :对象及其所有引用关联的对象都完全复制一份。实现方式:① 实现
💡 Java核心机制与特性
这部分深入Java运行机制和现代语法特性。
-
描述一下JVM内存模型(JMM),并解释
volatile关键字在其中的作用。- 核心分析 :JMM定义了主内存 (所有线程共享)和工作内存 (每个线程私有)的抽象关系。
volatile的两个核心作用:① 保证可见性 :任何线程对volatile变量的写操作会立即刷新到主内存,并使其他线程工作内存中的缓存失效,强制重新读取。② 禁止指令重排序 :防止编译器或处理器进行可能破坏程序顺序的优化。但它不保证原子性。
- 核心分析 :JMM定义了主内存 (所有线程共享)和工作内存 (每个线程私有)的抽象关系。
-
String s = new String("abc");创建了几个对象?- 核心分析 :可能创建1个或2个 对象。① 如果字符串常量池中已存在
"abc",则只在堆 中创建1个新的String对象。② 如果常量池中不存在,则先在常量池 中创建"abc"对象,再在堆 中创建新的String对象。要理解String.intern()方法的作用(将堆中字符串的引用放入常量池)。
- 核心分析 :可能创建1个或2个 对象。① 如果字符串常量池中已存在
-
什么是双亲委派模型?它有什么好处?如何破坏它?
- 核心分析 :类加载器在加载类时,先委托给父加载器尝试加载,只有父加载器无法完成时才自己加载。好处 :① 避免核心类被篡改(如自定义
java.lang.String不会被加载)。② 避免类重复加载。破坏方式:① 自定义类加载器重写loadClass()方法;② 使用线程上下文类加载器(TCCL),如JDBC驱动加载。
- 核心分析 :类加载器在加载类时,先委托给父加载器尝试加载,只有父加载器无法完成时才自己加载。好处 :① 避免核心类被篡改(如自定义
-
Java 8中的Lambda表达式是什么?其本质是什么?
- 核心分析 :Lambda表达式是函数式编程 的语法糖,用于简洁地表示一个函数式接口 (只有一个抽象方法的接口)的实例。其本质是编译器在编译时生成一个实现了目标函数式接口的匿名内部类 。例如,
() -> System.out.println("hi")会被编译成一个实现了Runnable接口的类的实例。
- 核心分析 :Lambda表达式是函数式编程 的语法糖,用于简洁地表示一个函数式接口 (只有一个抽象方法的接口)的实例。其本质是编译器在编译时生成一个实现了目标函数式接口的匿名内部类 。例如,
-
try-with-resources语句的原理是什么?被自动关闭的资源需要满足什么条件?- 核心分析 :这是Java 7引入的语法糖,用于自动关闭资源(如
InputStream)。其原理是编译器在编译时自动生成finally块,并在其中调用资源的close()方法。资源必须实现AutoCloseable接口。相比于传统的try-catch-finally,它代码更简洁,且能更好地处理多个资源的关闭和异常屏蔽。
- 核心分析 :这是Java 7引入的语法糖,用于自动关闭资源(如
-
switch语句支持哪些数据类型?在Java 7和Java 8+中有何变化?- 核心分析 :
switch支持byte、short、char、int及其包装类,以及枚举 。Java 7 开始支持**String**(实际是通过hashCode()和equals()实现)。Java 12+ 引入了预览特性switch表达式,可以使用箭头语法和返回值,最终在Java 14中成为标准特性。
- 核心分析 :
-
什么是自动装箱(Autoboxing)和拆箱(Unboxing)?在什么场景下可能引发
NullPointerException?- 核心分析 :装箱 :基本类型自动转为包装类(
int -> Integer)。拆箱 :包装类自动转为基本类型(Integer -> int)。NPE风险 :当包装类对象为null时,对其进行拆箱操作会抛出NullPointerException。例如:Integer num = null; int i = num; // 这里会抛出NPE
- 核心分析 :装箱 :基本类型自动转为包装类(
-
什么是序列化(Serialization)?如何自定义序列化过程(如使用
transient或重写writeObject/readObject)?- 核心分析 :序列化是将对象状态转换为字节流的过程,用于网络传输或持久化。①
transient关键字修饰的变量不会被默认序列化。② 通过重写private void writeObject(ObjectOutputStream oos)和readObject(ObjectInputStream ois)方法,可以完全控制序列化和反序列化的细节。
- 核心分析 :序列化是将对象状态转换为字节流的过程,用于网络传输或持久化。①
💡 集合与容器进阶
这部分考察你对集合内部机制和高级用法的理解。
-
HashMap的负载因子(Load Factor)默认值是多少?为什么是0.75?- 核心分析 :默认负载因子是0.75 。这是一个在时间和空间成本上的折衷。值越高(如0.9),空间利用率高,但哈希冲突概率增加,查找成本上升。值越低(如0.5),冲突减少,但空间浪费严重,且会频繁触发扩容。0.75是统计学上的一个较优平衡点。
-
LinkedHashMap和HashMap有什么区别?它是如何实现有序的?- 核心分析 :
LinkedHashMap继承自HashMap。区别在于它维护了一个贯穿所有条目的双向链表 (在Entry中增加了before和after指针)。这使其可以保持两种顺序:① 插入顺序 (默认)。② 访问顺序 (构造函数传accessOrder=true,常用于实现LRU缓存)。访问顺序下,每次get或put一个已存在的键,都会将该条目移到链表末尾。
- 核心分析 :
-
Comparable和Comparator接口有什么区别?- 核心分析 :
Comparable(内部比较器) :定义在类的内部,通过compareTo(T o)方法实现,规定了对象的自然排序 。Comparator(外部比较器) :独立于比较的类,通过compare(T o1, T o2)方法实现,用于定义多种定制排序 策略,更灵活。Collections.sort()和Arrays.sort()都可以接受Comparator参数。
- 核心分析 :
-
什么是快速失败(fail-fast)和安全失败(fail-safe)?请举例说明。
- 核心分析 :快速失败 :在使用迭代器遍历集合时,如果集合结构被修改(非迭代器自身的
remove方法),会立即抛出ConcurrentModificationException。HashMap、ArrayList的迭代器是典型代表。原理是迭代器内部维护了一个modCount(修改次数),与集合的modCount对比。安全失败 :基于原集合的拷贝进行遍历,因此对原集合的修改不会影响迭代。ConcurrentHashMap、CopyOnWriteArrayList的迭代器属于此类。
- 核心分析 :快速失败 :在使用迭代器遍历集合时,如果集合结构被修改(非迭代器自身的
💡 多线程与并发基础
这部分是面试难点,但理解基础概念至关重要。
-
synchronized关键字和java.util.concurrent.locks.Lock接口有什么区别?- 核心分析 :主要区别:① 使用方式 :
synchronized是关键字,自动释放锁;Lock是接口,必须手动lock()和unlock()。② 功能丰富性 :Lock更强大,支持尝试非阻塞获取锁 (tryLock())、可中断锁 (lockInterruptibly())、公平锁 等。③ 性能 :在竞争不激烈时synchronized优化后(锁升级)性能接近;高竞争时Lock(如ReentrantLock)性能通常更好。
- 核心分析 :主要区别:① 使用方式 :
-
volatile关键字能否保证原子性?为什么?i++操作是原子的吗?- 核心分析 :
volatile不能保证原子性 。它只能保证单个 读/写操作的原子性和可见性,但像i++(包含读-改-写 三个操作)这种复合操作,在多线程下仍然可能被中断导致写覆盖。i++不是原子操作。要保证原子性,需要使用synchronized或java.util.concurrent.atomic包下的原子类(如AtomicInteger)。
- 核心分析 :
-
创建线程有哪几种方式?为什么更推荐使用线程池?
- 核心分析 :① 继承
Thread类;② 实现Runnable接口(更推荐,因为Java单继承);③ 实现Callable接口(可带返回值);④ 使用线程池 (如ExecutorService)。推荐线程池的原因 :降低资源消耗 (重用已创建的线程)、提高响应速度 (无需等待线程创建)、便于线程管理(可控制最大并发数、统一分配、监控等)。
- 核心分析 :① 继承
💡 综合与编码陷阱
最后这道题考察你的实际编码经验和细致程度。
- 下面这段代码的输出是什么?为什么?
Integer a = 100, b = 100; System.out.println(a == b); // true
Integer c = 200, d = 200; System.out.println(c == d); // false- 核心分析 :这涉及到
Integer的缓存机制 。Integer类内部缓存了**-128到127**之间的Integer对象。当通过自动装箱或Integer.valueOf()在这个范围内取值时,直接返回缓存的对象,所以a和b指向同一个对象,==为true。超出此范围,则会new新的Integer对象,所以c和d指向不同对象,==为false。
- 核心分析 :这涉及到
💎 高效准备建议
你已经积累了40个高质量的基础问题。接下来,我建议你:
- 实践驱动 :尝试手写代码实现单例、LRU缓存、生产者-消费者模型等,这能巩固理论。
- 关联思考 :学习时多问"为什么"。例如,学完
ConcurrentHashMap,可以对比HashMap、HashTable和Collections.synchronizedMap,思考各自适用场景。 - 模拟面试:请朋友或自己录音,尝试在限定时间内清晰、有条理地回答这些问题,锻炼表达和临场反应。
如果你对集合、JVM或并发中的某个特定知识点(比如想深入了解所有Map的实现类对比,或G1垃圾回收器的细节)有进一步的学习需求,我可以为你提供更聚焦的资料和问题清单。