第一阶段:对象构造与销毁
1. 构造器链与初始化顺序
问题: 以下代码输出什么?按顺序写出初始化步骤。
java
class A {
static { System.out.print("S1 "); }
{ System.out.print("I1 "); }
A() { System Lake.print("C1 "); }
}
class B extends A {
static { System.out.print("S2 "); }
{ System.out.print("I2 "); }
B() {
super();
System.out.print("C2 ");
}
}
public class Main {
public static void main(String[] args) {
new B();
System.out.println();
new B();
}
}
答案:
输出为:
S1 S2 I1 C1 I2 C2
I1 C1 I2 C2
初始化步骤:
- 第一次执行
new B():- 类加载阶段(只执行一次):
- 加载类
A,执行静态块:输出S1。 - 加载类
B,执行静态块:输出S2。
- 加载类
- 对象实例化阶段:
- 创建
B对象时,先调用父类A的构造器链:- 执行
A的实例初始化块:输出I1。 - 执行
A的构造器:输出C1。
- 执行
- 然后执行
B的实例初始化块:输出I2。 - 最后执行
B的构造器:输出C2。
- 创建
- 类加载阶段(只执行一次):
- 第二次执行
new B():- 类加载已完成(静态块不再执行)。
- 对象实例化:顺序同上,输出
I1 C1 I2 C2。
2. 重载构造器的陷阱
问题: 以下代码是否有编译错误?为什么?
java
public class Test {
private int value;
public Test(int value) {
this.value = value;
}
public Test() {
System.out.println("Default");
this(); // 这行有什么问题?
}
}
答案:
有编译错误。
原因:this() 用于调用本类的其他构造器,必须在构造器的第一行执行。这里 this() 位于 System.out.println("Default") 之后,不是第一行,因此编译器会报错。正确做法是:如果无参构造器要调用有参构造器,应将 this() 移到第一行。
3. final成员初始化时机
问题: 以下两种初始化方式有何本质区别?
java
// 方式一
class Example1 {
private final int x = 10;
}
// 方式二
class Example2 {
private final int x;
public Example2() {
x = 10;
}
}
答案:
两种方式都确保 final 变量 x 被初始化,但本质区别在于初始化时机:
- 方式一:
x在声明时直接初始化(在字段定义处)。这发生在对象创建时,实例初始化块执行前。 - 方式二:
x在构造器中初始化。这允许在构造器内赋值,更灵活,例如可以根据参数动态赋值。
关键点:final变量必须在构造器结束前被赋值一次,且不可修改。
4. 对象销毁与finalize()
问题: finalize() 方法是否适合用于资源释放(如关闭文件)?为什么?GC何时调用它?
答案:
- 是否适合: 不适合。
- 原因:
finalize()的执行时机不确定,由垃圾回收器(GC)在回收对象前调用,但 GC 运行时间不可预测。这可能导致资源(如文件句柄)未及时释放,引发内存泄漏或资源耗尽。资源释放应使用显式方法(如close())或try-with-resources机制。 - GC调用时机: GC 在对象被判定为"不可达"(即无引用)后、回收内存前调用
finalize()。但调用时间不保证,甚至可能不调用(如 JVM 关闭时)。
5. 实例初始化块 vs 构造器
问题: 如果一个类同时有多个实例初始化块和构造器,执行顺序是什么?
java
class Demo {
{ System.out.print("A"); }
Demo() { System.out.print("C"); }
{ System.out.print("B"); }
}
答案:
执行顺序:
- 实例初始化块按代码顺序执行(在构造器之前)。
- 构造器在实例初始化块后执行。
对于Demo类:
- 创建对象时,先执行所有实例初始化块:输出
A,然后B。 - 再执行构造器:输出
C。
因此,输出为A B C。
第二阶段:继承与多态深度拷问
6. 方法绑定之谜
问题: 以下代码输出什么?解释静态绑定与动态绑定的区别。
java
class Parent {
void show() { System.out.print("Parent "); }
static void print() { System.out.print("P-Static "); }
}
class Child extends Parent {
void show() { System.out.print("Child "); }
static void print() { System.out.print("C-Static "); }
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
p.show();
p.print();
Child c = new Child();
c.show();
c.print();
}
}
答案:
输出:
Child P-Static Child C-Static
解释:
- 静态绑定 vs 动态绑定:
- 静态绑定: 在编译时确定方法调用,基于引用类型(如变量声明类型)。适用于
static方法、final方法和private方法。这里print()是静态方法,所以p.print()调用Parent.print()(输出P-Static),因为p是Parent类型。 - 动态绑定: 在运行时确定方法调用,基于实际对象类型。适用于非静态方法(实例方法)。这里
show()是非静态方法,所以p.show()调用Child.show()(输出Child),因为实际对象是Child。
- 静态绑定: 在编译时确定方法调用,基于引用类型(如变量声明类型)。适用于
- 详细输出:
p.show():动态绑定,对象是Child,输出Child。p.print():静态绑定,引用是Parent,输出P-Static。c.show():动态绑定,对象是Child,输出Child。c.print():静态绑定,引用是Child,输出C-Static。
7. 访问权限的继承规则
问题: 子类能否继承父类的 private 方法?能否重写父类的 private 方法?以下代码是否编译通过?
java
class Base {
private void method() {}
}
class Derived extends Base {
public void method() {} // 这是重写吗?
}
答案:
- 能否继承: 子类不能继承父类的
private方法,因为private方法只在父类内部可见。 - 能否重写: 不能重写父类的
private方法,因为子类无法访问或覆盖它。 - 代码是否编译通过: 编译通过。
Derived.method()是一个新方法,不是重写(因为父类方法private)。 - 是否重写: 这不是重写,而是子类定义的新方法。重写要求方法签名相同,且父类方法可访问(非
private)。
8. super关键字的限制
问题: 在静态方法中能否使用 super 关键字?为什么?以下代码错在哪里?
java
class Animal {
String name = "Animal";
}
class Dog extends Animal {
String name = "Dog";
static void print() {
System.out.println(super.name); // 这里
}
}
答案:
- 能否使用
super: 在静态方法中不能使用super。 - 原因:
super指代父类对象实例,用于访问父类成员或方法,但静态方法不依赖于对象实例(属于类级别),因此无法使用super(它需要当前对象上下文)。 - 代码错误:
print()是静态方法,super.name试图访问父类实例字段,但无对象实例。正确做法:改为实例方法,或直接使用父类名(如Animal.name,但字段非静态)。
9. 构造器中的多态陷阱
问题: 以下代码输出什么?解释原因(涉及对象初始化状态)。
java
class Animal {
Animal() {
print();
}
void print() {
System.out.print("Animal ");
}
}
class Dog extends Animal {
private int age = 5;
Dog() {
super();
print();
}
void print() {
System.out.print(age + " ");
}
}
public class Main {
public static void main(String[] args) {
new Dog();
}
}
答案:
输出:
0 5
解释原因:
- 对象初始化顺序:
new Dog()调用Dog()构造器。Dog()中super()调用父类Animal()构造器。- 在
Animal()中,print()被调用。此时,由于多态(动态绑定),实际调用Dog.print()(因为对象是Dog)。 - 但此时
Dog的age尚未初始化(在super()后初始化),默认值为0,所以输出0。 Animal()完成后,执行Dog的字段初始化:age = 5。- 然后执行
Dog()中的print():输出5。
关键:父类构造器调用被重写方法时,子类状态可能未完全初始化,导致意外行为。
10. 接口默认方法冲突
问题: 如果一个类实现两个接口,且两个接口有相同的默认方法,类该如何处理?以下代码如何修改才能编译?
java
interface A {
default void show() { System.out.println("A"); }
}
interface B {
default void show() { System.out.println("B"); }
}
class C implements A, B { } // 编译错误
答案:
- 处理方式: 类必须覆盖冲突的默认方法,否则编译错误。
- 修改代码: 在
C中实现show()方法,例如:
java
class C implements A, B {
@Override
public void show() {
System.out.println("C"); // 自定义实现
// 或调用特定接口的默认方法:A.super.show();
}
}
第三阶段:高级特性与设计
11. 内部类的内存泄漏风险
问题: 非静态内部类隐式持有外部类的引用,这在什么场景下可能导致内存泄漏?如何避免?
答案:
- 风险场景: 当非静态内部类实例被长期持有(如作为监听器或缓存),而外部类实例不再需要时,内部类隐式引用会阻止外部类被垃圾回收,导致内存泄漏。常见于事件监听、线程或静态集合持有内部类实例。
- 如何避免:
- 使用静态内部类(
static修饰),它不持有外部类引用。 - 如果必须用非静态内部类,确保在外部类不再需要时,断开内部类与外部类的关联(如置为
null)。 - 避免将非静态内部类实例暴露给长生命周期对象。
- 使用静态内部类(
12. 匿名内部类的限制
问题: 匿名内部类能否有构造器?能否实现多个接口?以下代码是否合法?
java
Runnable r = new Runnable() {
private int x;
{ x = 10; } // 实例初始化块
public void run() { System.out.println(x); }
};
答案:
- 能否有构造器: 匿名内部类不能有显式构造器(不能定义构造方法),但可以用实例初始化块模拟初始化。
- 能否实现多个接口: 不能。匿名内部类只能实现一个接口或继承一个类(单继承)。
- 代码是否合法: 合法。这里使用实例初始化块初始化
x,实现了Runnable接口。
13. 单例模式的多线程安全实现
问题: 写出三种线程安全的单例实现方式,并分析优缺点。
答案:
-
饿汉式(Eager Initialization):
javapublic class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }- 优点: 线程安全(静态变量类加载时初始化),简单高效。
- 缺点: 可能浪费资源(如果实例未被使用)。
-
双重检查锁(Double-Checked Locking):
javapublic 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确保可见性。
-
静态内部类(Static Inner Class):
javapublic class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }- 优点: 懒加载,线程安全(类加载机制保证),无需同步。
- 缺点: 无法传参初始化。
14. 不可变类的设计
问题: 设计一个不可变类 ImmutablePoint(包含 x、y 坐标),需要满足哪些条件?
答案:
设计条件:
- 类声明为
final,防止子类修改。 - 所有字段为
private final,确保不可修改。 - 无 setter 方法。
- 通过构造器初始化所有字段。
- 如果字段是引用类型(如对象),需确保其不可变或深拷贝(避免外部修改)。
示例代码:
java
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
15. 对象克隆的深拷贝与浅拷贝
问题: 实现 Cloneable 接口时,Object.clone() 默认是浅拷贝。如何实现深拷贝?以下代码有什么问题?
java
class Data implements Cloneable {
int[] arr = {1, 2, 3};
@Override
public Data clone() throws CloneNotSupportedException {
return (Data) super.clone(); // 浅拷贝
}
}
答案:
-
实现深拷贝: 覆盖
clone()方法,手动复制引用对象。例如:java@Override public Data clone() throws CloneNotSupportedException { Data cloned = (Data) super.clone(); // 浅拷贝基础 cloned.arr = arr.clone(); // 深拷贝:复制数组 return cloned; } -
代码问题: 默认
super.clone()是浅拷贝,arr数组引用被共享(原对象和克隆对象共享同一数组)。修改数组会影响双方。需手动复制数组实现深拷贝。
附加题
16. 枚举类的本质
问题: enum 在 Java 中是什么?以下代码编译后相当于什么?
java
enum Color { RED, GREEN, BLUE }
答案:
-
本质:
enum是一种特殊类,继承自java.lang.Enum,每个枚举常量是类的实例(public static final)。 -
编译后等价: 相当于以下类:
javafinal class Color extends Enum<Color> { public static final Color RED = new Color("RED"); public static final Color GREEN = new Color("GREEN"); public static final Color BLUE = new Color("BLUE"); private Color(String name) { super(name); } // 其他方法(如 values(), valueOf())由编译器生成 }
17. 静态内部类的加载时机
问题: 静态内部类在何时加载?它与外部类的类加载器是否相同?
答案:
- 加载时机: 静态内部类在第一次使用时加载(如首次访问其静态成员或创建实例),不同于外部类加载(外部类加载时不加载内部类)。
- 类加载器: 相同。静态内部类和外部类使用同一个类加载器加载。
18. 方法重写的不可变性要求
问题: 如果父类方法返回 List<String>,子类重写时能否返回 ArrayList<String>?为什么?(协变返回类型)
答案:
- 能否返回: 能。
- 原因: Java 5+ 支持协变返回类型(covariant return type)。子类重写方法可以返回父类方法返回类型的子类型。
ArrayList<String>是List<String>的子类,因此符合规则。这增强了灵活性而不破坏类型安全。