本文件旨在提供一系列Java基础核心面试题,重点考察候选人对Java语言底层原理和核心API的掌握程度。
==
vs equals()
: 请解释 ==
和 equals()
方法的根本区别。特别是对于包装类型(如 Integer
),请解释以下代码的输出,并说明原因。
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
答案:
==
:
int
, char
),==
比较的是它们的值。Object
, String
, Integer
),==
比较的是对象的内存地址,即判断两个引用是否指向同一个对象实例。equals()
:
Object
类的一个方法,其默认实现与 ==
相同,也是比较内存地址。String
, Integer
, Double
等)重写了 equals()
方法,用于比较对象的内容是否相等。代码输出及解释:
System.out.println(a == b);
-> true
Integer
类型使用了缓存机制。对于 -128
到 127
之间的整数,通过 Integer.valueOf(int)
创建的 Integer
对象会被缓存。因此,a
和 b
都指向了缓存池中同一个 Integer
对象。System.out.println(c == d);
-> false
200
超出了 Integer
的缓存范围 (-128
to 127
)。因此,c
和 d
是通过 new Integer(200)
创建的两个不同的对象,它们的内存地址不同。System.out.println(s1 == s2);
-> false
s1
和 s2
是通过 new String("hello")
创建的两个不同的 String
对象,它们位于堆内存中,地址不同。System.out.println(s1.equals(s2));
-> true
String
类重写了 equals()
方法,用于比较字符串的字符序列内容。因为 s1
和 s2
的内容都是 “hello”,所以结果为 true
。String
, StringBuilder
, StringBuffer
: 请比较这三者的异同点,并说明它们各自的适用场景。
答案:
特性 | String |
StringBuilder |
StringBuffer |
---|---|---|---|
可变性 | 不可变 (Immutable) | 可变 (Mutable) | 可变 (Mutable) |
线程安全 | 线程安全 | 非线程安全 | 线程安全 |
性能 | 低(每次修改都创建新对象) | 高 | 中(因同步开销) |
内部实现 | final char[] (Java 8) / final byte[] (Java 9+) |
char[] / byte[] |
char[] / byte[] (方法加 synchronized ) |
详细说明:
String
:
String
の修改操作(如拼接、替换)都会导致创建一个新的 String
对象。HashMap
の键。StringBuilder
:
append()
, insert()
等方法来修改内容,操作在原对象上进行,效率高。StringBuffer
:
StringBuilder
类似,也是可变字符串。但其所有公开方法(如 append
, insert
)都由 synchronized
关键字修饰,保证了线程安全。StringBuilder
。选择建议:
String
,除非需要频繁修改。StringBuilder
。StringBuffer
。// 适用场景示例
public class StringUsageExample {
// StringBuilder: 单线程循环拼接
public String buildQuery(String[] fields) {
StringBuilder sb = new StringBuilder("SELECT ");
for (int i = 0; i < fields.length; i++) {
sb.append(fields[i]);
if (i < fields.length - 1) {
sb.append(", ");
}
}
sb.append(" FROM users;");
return sb.toString();
}
}
重写 (Override
) 与重载 (Overload
): 请解释它们之间的区别。
答案:
重载 (Overloading):
重写 (Overriding):
public
> protected
> default
> private
)。示例代码:
// 重载示例
class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
// 重写示例
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override // 注解 @Override 强制编译器检查是否满足重写规则
public void makeSound() {
System.out.println("Dog barks");
}
}
抽象类 vs 接口: 请比较抽象类 (Abstract Class) 和接口 (Interface) 的区别,尤其是在 Java 8 之后。
答案:
抽象类和接口是 Java 中实现抽象的两种核心方式,它们既有相似之处,也有本质区别。
特性 | 抽象类 (Abstract Class) | 接口 (Interface) |
---|---|---|
继承关系 | 单继承 (一个类只能 extends 一个抽象类) |
多实现 (一个类可以 implements 多个接口) |
成员变量 | 可以包含各种类型的成员变量(实例变量、静态变量) | 只能包含 public static final 类型的常量 (隐式) |
构造方法 | 有构造方法 (用于子类初始化) | 没有构造方法 |
方法类型 | 可以包含抽象方法和具体方法 | Java 8 前只能有抽象方法 |
Java 8+ | 无变化 | 增加了 default 方法和 static 方法 |
设计理念 | “is-a” 关系,体现一种本质上的归属,强调代码复用 | “has-a” 或 “can-do” 关系,体现一种能力的契约,强调行为规范 |
Java 8 之后的主要变化:
Java 8 为接口引入了 default
方法和 static
方法,这使得接口和抽象类的界限变得有些模糊,但它们的根本设计理念没有改变。
default
方法: 允许在接口中提供一个方法的默认实现。实现该接口的类可以不重写此方法,直接使用默认实现,也可以根据需要重写它。这解决了在接口中添加新方法时,所有实现类都必须修改的“接口易碎”问题。static
方法: 允许在接口中定义静态方法,这些方法只能通过接口名直接调用,不能被实现类继承或重写。通常用作工具方法。如何选择?
优先使用接口:
使用抽象类:
public
的方法)时。final
, finally
, finalize
的区别:
答案:
这三个关键字在 Java 中用途完全不同,但因拼写相似而常常被放在一起比较。
final
:
Override
)。finally
:
try-catch
异常处理语句块中。finally
块中的代码总会被执行(除非在 try
或 catch
中调用了 System.exit()
或 JVM 崩溃)。主要用于确保资源的释放,如关闭文件流、数据库连接、网络连接等。try-with-resources
语句是其最佳实践。finalize
:
Object
类的一个方法 (protected void finalize() throws Throwable
)。finalize
。它的执行时机不确定,可能导致性能问题,并且不是一个可靠的资源释放方式。自 Java 9 起,该方法已被废弃 (deprecated)。现代 Java 中,应使用 try-with-resources
语句或 java.lang.ref.Cleaner
来进行资源管理。Java 异常体系: 请描述 Java 的异常体系结构,以及 Checked Exception 和 Unchecked Exception 的区别。
答案:
Java 的异常体系结构基于 Throwable
类,所有异常和错误都继承自它。
体系结构:
Throwable
: 所有错误或异常的超类。
Error
(错误): 表示程序无法处理的严重问题,通常是 JVM 层面或底层资源耗尽等问题。应用程序不应该(也通常无法)捕获或处理 Error
。
OutOfMemoryError
, StackOverflowError
。Exception
(异常): 表示程序本身可以处理的异常情况。这是我们日常编程中主要关注和处理的部分。它又分为两类:
Exception
类及其子类中,除了 RuntimeException
及其子类之外的所有异常。try-catch
块捕获,或者在方法签名上使用 throws
关键字声明抛出。IOException
, SQLException
, ClassNotFoundException
。RuntimeException
类及其所有子类。NullPointerException
, IllegalArgumentException
, ArrayIndexOutOfBoundsException
。核心区别总结:
特性 | Checked Exception | Unchecked Exception |
---|---|---|
强制处理 | 是 (编译器强制) | 否 |
继承关系 | 继承自 Exception (非 RuntimeException ) |
继承自 RuntimeException |
产生原因 | 通常是外部因素,可恢复 | 通常是程序逻辑错误 |
处理方式 | try-catch 或 throws |
应该修复代码逻辑,而非捕获 |
Java 的四种引用类型: 请解释 Java 中的四种引用类型(强、软、弱、虚)及其应用场景。
答案:
Java 提供了四种不同强度的引用类型,让开发者能更灵活地控制对象的生命周期和与垃圾收集器 (GC) 的交互。
引用类型 | 特点 | 回收时机 | 应用场景 |
---|---|---|---|
强引用 (Strong) | 默认的引用类型 (Object obj = new Object() ) |
只要强引用存在,GC 永远不会回收 | 普通的对象引用 |
软引用 (Soft) | 内存不足时才会被回收 | 当 JVM 即将发生 OutOfMemoryError 之前 |
实现内存敏感的高速缓存 |
弱引用 (Weak) | 只能存活到下一次 GC 发生之前 | 只要发生 GC,无论内存是否充足,都会被回收 | ThreadLocal 、WeakHashMap 、防止内存泄漏 |
虚引用 (Phantom) | 任何时候都可能被回收,必须和 ReferenceQueue 联合使用 |
与弱引用类似,但主要用于跟踪对象被回收的状态 | 管理堆外内存 (DirectByteBuffer) |
详细解释:
强引用 (Strong Reference):
Object obj = new Object();
。只要对象有强引用指向,垃圾收集器就绝不会回收它。如果内存不足,JVM 宁愿抛出 OutOfMemoryError
,也不会回收具有强引用的对象。软引用 (Soft Reference):
SoftReference
弱引用 (Weak Reference):
WeakReference
WeakHashMap
: key
是弱引用,当 key
没有其他强引用时,这个 entry
就会被 GC 清理。ThreadLocal
: ThreadLocalMap
的 key
是对 ThreadLocal
对象的弱引用,有助于防止内存泄漏。虚引用 (Phantom Reference):
ReferenceQueue
联合使用。PhantomReference
NIO
中的 DirectByteBuffer
使用虚引用来管理堆外内存的释放。当 DirectByteBuffer
对象被回收时,其对应的虚引用会进入 ReferenceQueue
,一个专门的线程会处理队列中的引用,并调用 freeMemory
方法释放堆外内存。多态的实现原理: 请解释 Java 中多态的实现原理。
答案:
Java 的多态性(特别是运行时多态)是其面向对象三大特性(封装、继承、多态)之一,其实现核心依赖于动态绑定 (Dynamic Binding),也称为后期绑定 (Late Binding)。
实现原理可以概括为以下几点:
方法表 (Method Table):
vtable
)。这个方法表存放了该类所有方法的直接引用(即实际内存地址)。invokevirtual
指令:
Animal animal = new Dog(); animal.makeSound();
),编译器生成的字节码是 invokevirtual
指令。Animal.makeSound
)。运行时解析:
invokevirtual
指令被执行时,JVM 会执行以下步骤:
Dog
)。Dog
类的方法表)。makeSound
),获取其直接引用(内存地址)。总结:
多态的实现,就是将“调用哪个方法”的决定,从编译期推迟到了运行期。编译器只检查方法是否存在于父类引用类型中(语法检查),而 JVM 在运行时根据对象的真实身份(new
出来的对象类型)来动态选择要执行的具体方法版本。这个过程就是动态绑定。
equals()
与 hashCode()
的契约: 为什么重写 equals()
方法时必须重写 hashCode()
方法?
答案:
这是 Java 中一条非常重要的规则,主要为了保证在使用哈希集合(如 HashMap
, HashSet
, Hashtable
)时能够正确工作。Object
类的通用约定规定了 hashCode()
和 equals()
之间必须满足的契约:
等价对象等价哈希码: 如果两个对象通过 equals(Object)
方法比较是相等的,那么调用这两个对象中任意一个的 hashCode()
方法都必须产生相同的整数结果。
if (a.equals(b)) { assert a.hashCode() == b.hashCode(); }
非等价对象不要求不等价哈希码: 如果两个对象通过 equals(Object)
方法比较是不相等的,那么它们的 hashCode()
方法不被要求必须产生不同的结果。但是,为不相等的对象产生不同的哈希码有助于提高哈希表的性能。
哈希码一致性: 在一个 Java 应用的执行期间,如果一个对象用于 equals
比较的信息没有被修改,那么对该对象多次调用 hashCode()
方法必须始终返回相同的值。
为什么必须遵守这个契约?
hashCode()
来快速定位对象所在的哈希桶 (bucket)。equals()
而不重写 hashCode()
,就会导致两个通过 equals()
判断为相等的对象,由于继承了 Object
类的 hashCode()
方法(该方法通常返回对象的内存地址),而拥有不同的哈希码。HashSet
时,它们会被分配到不同的哈希桶中,导致 HashSet
认为它们是两个不同的对象,从而破坏了 Set
的唯一性。key
去 HashMap
中查找时,由于哈希码不同,你可能永远也找不到另一个“相等”的对象作为 key
存入的 value
。示例:
class Person {
String name;
public Person(String name) {
this.name = name; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return name.equals(person.name);
}
// 如果不重写 hashCode(),就会出现问题
// @Override
// public int hashCode() {
// return name.hashCode();
// }
}
Set<Person> set = new HashSet<>();
set.add(new Person("Alice"));
System.out.println(set.contains(new Person("Alice"))); // 如果没重写 hashCode(),这里会是 false
泛型通配符 ? extends T
与 ? super T
: 请解释这两种泛型通配符的区别和使用场景 (PECS 原则)。
答案:
? extends T
(上界通配符) 和 ? super T
(下界通配符) 是 Java 泛型中用于增加方法灵活性的高级特性。它们的区别可以通过 PECS (Producer Extends, Consumer Super) 原则来理解。
PECS 原则:
T
类型对象的泛型集合(即你只会从中读取 T
),那么使用 ? extends T
。T
类型对象的泛型集合(即你只会向其中写入 T
),那么使用 ? super T
。? extends T
(上界通配符)
List extends Number>
表示这个列表可以持有 Number
或其任何子类型(如 Integer
, Double
)的对象。Number
。null
)。因为编译器无法确定列表的确切类型。例如,你不能往一个 List
中添加一个 Double
对象。// 计算列表中所有数字的总和
public static double sum(Collection<? extends Number> numbers) {
double sum = 0.0;
for (Number n : numbers) {
// 安全地读取 Number
sum += n.doubleValue();
}
// numbers.add(1); // 编译错误!
return sum;
}
// 调用:
// sum(new ArrayList());
// sum(new ArrayList());
? super T
(下界通配符)
List super Integer>
表示这个列表可以持有 Integer
或其任何父类型(如 Number
, Object
)的对象。Integer
或其子类型的对象,因为它们都可以向上转型为列表声明的任何父类型。Object
,无法确定其具体类型(除非进行强制类型转换)。// 将多个整数添加到集合中
public static void addIntegers(List<? super Integer> list) {
list.add(1); // 安全地写入 Integer
list.add(2);
// Object obj = list.get(0); // 读取时只能确定是 Object
}
// 调用:
// addIntegers(new ArrayList());
// addIntegers(new ArrayList());
// addIntegers(new ArrayList
String s = new String("xyz");
创建了几个对象?
答案:
这句代码可能创建一个或两个对象,具体取决于字符串常量池(String Constant Pool)中是否已经存在 “xyz”。
一个对象: 如果字符串常量池中已经存在 “xyz” 的引用。
new String("xyz")
只会在堆内存中创建一个新的 String
对象,这个对象的内容是 “xyz” 的一个副本。两个对象: 如果字符串常量池中不存在 “xyz” 的引用。
new String()
会在堆内存中再创建一个 String
对象,这个对象的内容同样是 “xyz”。