
Java 基础面试二
Java 基础面试二
Java 面向对象
【简单】对象实体与对象引用有何不同?
(1)对象是用来描述客观事物的一个抽象。一个对象由一组属性和对这组属性进行操作的一组服务组成。
(2)类是具有相同属性和方法的一组对象的集合,它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和方法两个主要部分。
(3)对象实体与对象引用的不同之处在于:
new
创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
【简单】接口和抽象类有什么区别?
(1)接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。
接口的主要特性有:
- 接口不能实例化。
- 接口不能包含任何非常量成员,任何字段都隐式的被
public static final
修饰。 - 接口中没有非静态方法,也就是说要么是抽象方法,要么是静态方法。
- 从 Java8 开始,接口增加了
default
方法特性,可以定义方法的默认实现;Java 9 以后,甚至可以定义私有的default
方法。
(2)抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。
(3)接口和抽象类有什么相同点和不同点?
Java 中的类可以实现多个接口。
(4)与 C++ 等语言不一样,Java 类不支持多继承。这意味着,Java 不能通过继承多个抽象类来重用逻辑。那么,如何来实现重用呢?Java 的解决方案是:接口支持多继承,准确的说,接口支持扩展多个接口,而接口也支持实现多个接口。
【中等】什么是 Java 内部类?内部类有什么作用?
什么是内部类?
内部类 (Inner Class) 是定义在另一个类内部的类。Java 中有四种类型的内部类:
- 成员内部类:作为外部类的成员存在
- 局部内部类:定义在方法或作用域内的类
- 匿名内部类:没有名字的局部内部类
- 静态嵌套类:用 static 修饰的嵌套类
内部类有什么作用?
- 逻辑分组:当某个类只对另一个类有用时,可以将其嵌入使用它的类中,保持代码在一起
- 增强封装性:内部类可以访问外部类的私有成员,同时自身也可以对外部完全隐藏
- 实现多重继承:通过内部类可以间接实现多重继承的效果
- 回调机制:常用于事件处理和监听器实现
- 代码简洁:特别是匿名内部类可以减少代码量
内部类有哪些特点?
- 内部类可以访问外部类的所有成员(包括 private)
- 外部类需要通过实例化内部类来访问其成员
- 内部类编译后会生成独立的。class 文件(格式:
OuterClass$InnerClass.class
) - 非静态内部类不能有静态成员(静态内部类可以)
- 内部类可以继承其他类或实现接口
【简单】为什么 Java 不支持多重继承?
Java 不支持多重继承的核心原因是为了避免【菱形继承问题(Diamond Problem)】。
什么是菱形继承问题?
菱形继承存在歧义性:
- 如果类 C 继承自类 A 和类 B,而 A 和 B 都有同名方法
method()
- 调用
C.method()
时无法确定应该调用 A 还是 B 的版本
由于菱形继承歧义性而引发的复杂性增加问题:
- 多重继承会显著增加编译器和 JVM 的实现复杂度
- 方法调用、构造函数调用顺序变得难以确定
Java 如何解决多重继承?
在 Java 中,类可以实现多个接口。接口提供多重继承的行为规范,但不包含具体实现。
JDK8 之后,接口支持默认方法(default),是不是又出现了菱形继承问题?
为了规避这个问题,Java 强制规定,如果多个接口存在相同的默认方法,子类必须重写这个方法。否则,编译器会报错。
【中等】深拷贝和浅拷贝有什么区别?
深拷贝和浅拷贝有什么区别?
关键点 | 浅拷贝 | 深拷贝 |
---|---|---|
复制对象 | 只复制对象本身(基本类型值拷贝) | 递归复制对象及其引用的所有子对象 |
引用类型字段 | 新旧对象共享同一引用(修改相互影响) | 创建全新引用对象(修改完全隔离) |
内存开销 | 小(仅复制一层) | 大(递归复制所有关联对象) |
实现方式 | 默认Object.clone() | 需手动实现递归克隆/序列化/工具类 |
适用场景 | 对象无可变引用字段 | 对象含可变引用字段且需完全独立 |
本质区别:浅拷贝是"复制钥匙",深拷贝是"复制钥匙+保险箱"。
注意事项:
- 深拷贝需处理循环引用问题
- 推荐使用
SerializationUtils.clone()
或 JSON 序列化实现深拷贝 - 不可变对象(如 String)的浅拷贝是安全的
深拷贝和浅拷贝实现方式有什么区别?
实现方式对比
方法 | 浅拷贝 | 深拷贝 | 说明 |
---|---|---|---|
Object.clone() | ✓ | ✗ | 默认浅拷贝 |
手动递归克隆 | ✗ | ✓ | 需所有引用类型实现Cloneable |
序列化反序列化 | ✗ | ✓ | 通过ObjectOutputStream 实现 |
工具类(Apache Commons) | ✗ | ✓ | SerializationUtils.clone() |
class Person implements Cloneable {
String name;
Address address; // 引用类型字段
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 默认浅拷贝
}
}
// 测试
Person p1 = new Person("Alice", new Address("北京"));
Person p2 = (Person)p1.clone();
p2.address.city = "上海"; // p1.address.city 也会变成"上海"
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person)super.clone();
cloned.address = (Address)address.clone(); // 手动复制引用对象
return cloned;
}
// Address 类也需实现 Cloneable
class Address implements Cloneable {
String city;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
【简单】面向对象和面向过程有什么区别?
面向对象和面向过程的主要区别:
维度 | 面向对象(OOP) | 面向过程(POP) |
---|---|---|
核心思想 | 以对象为中心 | 以步骤为中心 |
代码组织 | 按现实实体抽象为类 | 按功能流程拆分为函数 |
数据管理 | 数据与行为封装在对象中 | 数据与函数独立 |
扩展方式 | 通过继承/多态扩展(开闭原则) | 需修改函数逻辑 |
典型特性 | 封装、继承、多态三大特性 | 无三大特性 |
典型语言 | Java, Python, C++ | C, Pascal |
【中等】面向对象三大特征和五大原则是什么?
面向对象三大特征是什么?
面向对象三大特征:
封装(Encapsulation) :隐藏内部细节,暴露安全接口。
- 用
private
保护数据,通过getter/setter
控制访问 - 示例:
BankAccount
类隐藏余额,提供deposit()
/withdraw()
方法
- 用
继承(Inheritance) :子类复用父类属性和方法。
- 通过
extends
实现(如Dog extends Animal
) - 注意:Java 是单继承(一个子类只能有一个父类)
- 通过
多态(Polymorphism) :同一行为的不同实现方式。
- 编译时多态:方法重载(
Overload
) - 运行时多态:方法重写(
Override
)+ 父类引用指向子类对象(如Animal a = new Dog(); a.sound();
)
- 编译时多态:方法重载(
一言以概之:封装保证安全性,继承提高复用性,多态增强扩展性。
面向对象的五大原则是什么?
面向对象的五大原则是 SOLID 原则:
- 单一职责原则 (SRP):一个类只负责一个功能,避免职责过多导致代码臃肿。
- 开闭原则 (OCP):对扩展开放,对修改关闭。通过抽象和继承扩展功能,而非直接修改原有代码。
- 里氏替换原则 (LSP):子类必须能替换父类,确保继承关系不会破坏程序逻辑。
- 接口隔离原则 (ISP):接口应当小而专,避免臃肿接口强制实现不必要的方法。
- 依赖倒置原则 (DIP):依赖抽象而非具体,高层模块不直接依赖低层模块,而是通过接口或抽象类交互。
一言以概之:SOLID 原则让代码更灵活、可维护、易扩展。
设计模式
典型问题
(1)你知道哪些设计模式?
(2)你知道哪些设计模式在 Java 源码中的应用案例?
(3)你知道哪些设计模式在主流框架中的应用案例?
知识点
(1)23 种经典设计模式分类如下:
- 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
- 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
- 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。
(2)设计模式在 Java 源码中应用的经典案例:
InputStream 是一个抽象类,标准类库中提供了 FileInputStream、ByteArrayInputStream 等各种不同的子类,分别从不同角度对 InputStream 进行了功能扩展,这是典型的装饰器模式应用案例。
(3)设计模式在主流框架中应用的经典案例:
如 Spring 等如何在 API 设计中使用设计模式。你至少要有个大体的印象,如:
- BeanFactory 和 ApplicationContext 应用了工厂模式。
- 在 Bean 的创建中,Spring 也为不同 scope 定义的对象,提供了单例和原型等模式实现。
- Spring Aop 使用了代理模式、装饰器模式、适配器模式等。
- 各种事件监听器,是观察者模式的典型应用。
- 类似 JdbcTemplate 等则是应用了模板模式。
Object
【简单】Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
方法签名 | 作用 | 默认行为 |
---|---|---|
String toString() | 返回对象的字符串表示 | 类名@十六进制哈希码 (如 Person@1b6d3586 ) |
boolean equals(Object obj) | 比较两个对象是否逻辑相等 | 比较内存地址(== ) |
int hashCode() | 返回对象的哈希码 | 基于内存地址生成 |
Class<?> getClass() | 返回对象的运行时类(Class 对象) | 由 JVM 提供 |
protected Object clone() | 创建并返回对象的副本 | 浅拷贝(需实现 Cloneable 接口) |
protected void finalize() | 已废弃,对象被 GC 回收前调用 | 空实现(不推荐使用) |
void notify() | 唤醒一个等待该对象监视器的线程 | 依赖 JVM 实现 |
void notifyAll() | 唤醒所有等待该对象监视器的线程 | 依赖 JVM 实现 |
void wait() | 让当前线程等待,直到被唤醒 | 必须在同步代码块中调用 |
void wait(long timeout) | 让线程等待,最多 timeout 毫秒 | 超时后自动唤醒 |
void wait(long timeout, int nanos) | 更精确的等待(纳秒级) | 实际精度依赖系统 |
【简单】== 和 equals() 有什么区别?
对比项 | == | equals() |
---|---|---|
基本类型比较 | 比较值 | 不能比较 |
引用类型比较 | 比较内存地址 | 默认比较内存地址(同 == ),但可重写为逻辑比较(如内容是否相同) |
是否可重写 | 否(运算符,行为固定) | 是(可自定义比较逻辑) |
用途 | 快速判断基本类型值相等或引用是否指向同一对象 | 判断对象逻辑是否相等(如内容、属性等) |
【简单】为什么重写 equals() 时必须重写 hashCode() 方法?
因为 Java 规定:两个对象若equals()
相等,它们的hashCode()
必须相同。
如果违背,则哈希集合(如 HashMap
、HashSet
)无法正确去重或查找。
HashMap
/HashSet
先通过hashCode()
快速定位数据,再用equals()
精确匹配。- 若
hashCode()
不一致,即使equals()
为true
,集合会误判为不同对象。
如何正确重写 `hashCode()`?
equals()
:比较所有关键字段(如name
、age
)。hashCode()
:用Objects.hash(字段1, 字段2)
生成(确保与equals()
字段一致)。
【简单】finalize 有什么用?
一言以概之,finalize
可用于对象销毁前的清理,但不可靠且性能差,现代 Java 开发应避免使用,改用 AutoCloseable
或 Cleaner
。
Java 9+ 已弃用 finalize
,推荐使用:
try-with-resources
(实现AutoCloseable
接口)Cleaner
或PhantomReference
(更可控的清理机制)。
finalize
的作用(Java) :
- 对象被垃圾回收前的清理:在对象被 GC 回收前,
finalize()
会被调用,可用于释放非内存资源(如文件句柄、数据库连接等)。 - 最后的补救机会:如果对象未被正确关闭,
finalize
提供最后一次资源释放的机会。
finalize
的问题 :
- 不保证执行:JVM 不保证
finalize
一定会执行(如程序突然终止时)。即使对象可达性失效,GC 可能延迟回收,导致finalize
延迟调用。 - 性能开销:覆写
finalize
的对象会被 JVM 放入特殊队列,垃圾回收变慢。可能引发内存泄漏(如果finalize
阻塞或执行过久)。 - 安全问题:在
finalize
中抛出异常会导致清理中断,且异常被忽略。可能被恶意代码利用(如通过重写finalize
复活对象,干扰 GC)。
String
【简单】String、StringBuffer、StringBuilder 的区别?
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
线程安全 | ✅(因不可变) | ✅(同步方法) | ❌(非线程安全) |
性能 | ⚠️ 最差(频繁创建新对象) | ⚠️ 中等(同步开销) | ✅ 最高(无同步开销) |
适用场景 | 常量、少量拼接 | 多线程字符串操作 | 单线程字符串操作(推荐) |
概括
- 用
String
存储常量,用StringBuilder
高效拼接(单线程),用StringBuffer
保证线程安全(多线程)。 - 优先选
StringBuilder
(90%场景适用)。
【简单】String 为什么是不可变的?
String
的不可变性是 Java 为安全、性能、线程安全做的核心设计。
String 不可变的核心原因:
final
修饰的char[]
数组:Java 中String
内部用private final char[]
(JDK 9+ 改为byte[]
)存储数据,数组引用和内容均不可修改。- 无修改内部状态的方法:所有看似“修改”的方法(如
concat()
、substring()
)都返回新String
对象,原对象不变。
设计安全优化
- 线程安全:不可变天然线程安全,无需同步。
- 缓存哈希值:
String
的hashCode()
计算结果可缓存(因内容不变),提升性能(如HashMap
的键)。 - 字符串常量池复用:如
String s = "abc"
会复用常量池中的相同字符串,减少内存开销。
为什么这样设计:
- 安全:防止恶意修改(如网络请求参数、数据库连接字符串被篡改)。
- 性能:哈希缓存、常量池复用提升效率。
- 简单:避免多线程同步问题。
示例验证不可变性:
String s1 = "Hello";
String s2 = s1.concat(" World");
System.out.println(s1); // 输出 "Hello"(原字符串未变)
System.out.println(s2); // 输出 "Hello World"(新对象)
【简单】字符串拼接用“+” 还是 StringBuilder?
循环/动态拼接 → StringBuilder
;简单常量拼接 → "+";多线程 → StringBuffer
(极少用)。StringBuilder
是默认推荐选择!
优先用 StringBuilder
(大多数场景)
- 适用情况:循环、动态拼接、大量字符串操作。
- 原因:
- 高性能:直接修改缓冲区,避免
+
频繁创建新对象。 - 低内存开销:减少临时对象和 GC 压力。
- 高性能:直接修改缓冲区,避免
简单拼接可用 "+"(编译期优化)
- 适用情况:少量固定字符串拼接(如
"a" + "b"
)。 - 原因:
- 代码简洁:可读性更好。
- 编译器优化:JVM 自动合并为常量(如
"ab"
),无性能损失。 - 通过“+”的字符串拼接方式,实际上是通过
StringBuilder
调用append()
方法实现的。 - 在循环内使用“+”,会导致创建过多的
StringBuilder
对象。JDK9 中,优化了这个问题,字符串相加 “+” 改为了用动态方法makeConcatWithConstants()
来实现,而不是大量的StringBuilder
了。
多线程拼接用 StringBuffer
(极少需要)
- 适用情况:多线程环境且需线程安全(通常局部变量仍可用
StringBuilder
)。
【简单】String#equals() 和 Object#equals() 有何区别?
对比项 | Object#equals() | String#equals() |
---|---|---|
默认行为 | 比较内存地址(== ) | 比较字符串内容(逐字符对比) |
重写目的 | 需子类自行重写以实现逻辑相等 | 已优化为内容比较,满足字符串业务需求 |
性能影响 | 无额外开销 | 需遍历字符数组,但优先检查地址和长度 |
使用场景 | 通用对象比较(默认不满足内容相等) | 字符串内容对比(如 "abc".equals("abc") ) |
【简单】字符串常量池有什么用?
字符串常量池是JVM 的特殊内存区域,用于存储字符串字面量(如 "abc"
),确保相同内容的字符串只存一份。
字符串常量池通过复用相同字符串,节省内存并提升性能,直接赋值("abc"
)优先使用池,new String()
强制创建新对象。
字符串常量池的作用有:
节省内存:相同字符串复用,避免重复创建(如 String s1 = "hello"
和 String s2 = "hello"
指向同一对象)。
提升性能:
- 快速比较:直接通过
==
判断地址是否相同(比equals()
更快)。 - 哈希优化:如
HashMap
的键可复用缓存的hashCode
。
实现规则
- 直接赋值(
String s = "abc"
)→ 优先从常量池引用。 new String("abc")
→ 强制在堆中创建新对象(不推荐,除非需隔离实例)。intern()
方法 → 将堆中的字符串对象添加到常量池(若池中不存在)。
注意事项
- 避免滥用
new String()
:无特殊需求时,直接用字面量赋值。 intern()
慎用:可能增加常量池内存压力,需权衡性能。
String s = new String("abc")
创建了几个字符串对象?
【简单】new String("abc")
可能创建1~2个对象(取决于常量池是否已存在"abc"),但堆中的新对象必定创建。
- 常量池已存在"abc":1个对象(仅堆中的
new String
) - 常量池不存在"abc":2个对象(常量池的"abc" + 堆中的
new String
)
【简单】String#intern 方法有什么用?
String#intern 方法的作用有:
- 强制字符串入池:将堆中的
String
对象添加到字符串常量池(若池中不存在) - 返回池中引用:保证相同内容的字符串始终返回同一内存地址
注意
- JDK7+ 优化:常量池从方法区移至堆内存,减少内存溢出风险。
- 慎用场景:
- 避免对动态生成的短生命周期字符串使用(可能导致池膨胀)
- 优先用于高频使用的静态字符串(如配置键值)
【简单】String 类型的变量和常量做“+”运算时会发生什么?
常量相加编译期优化,变量相加隐式转 StringBuilder
,循环拼接必须显式使用 StringBuilder
避免性能损耗。
常量折叠(编译期优化)
- 纯常量运算(如
"a"+"b"
)→ 直接合并为"ab"
,仅存于常量池 - final 变量 视为常量,同样触发优化
变量拼接(运行时行为)
- 含变量的运算(如
str + "b"
)→ 隐式转换为StringBuilder
操作// 实际执行逻辑 new StringBuilder().append(str).append("b").toString()
- 每次运算 生成临时
StringBuilder
和最终String
对象
性能关键差异
场景 | 内存/性能表现 | 优化建议 |
---|---|---|
常量+常量 | 零运行时开销 | 无需处理 |
单次变量+常量 | 1次 StringBuilder 创建 | 可接受 |
循环内拼接 | 多次创建 StringBuilder (性能陷阱) | 必须显式用 StringBuilder |
最佳实践
简单拼接:直接使用
+
(可读性优先)循环/批量拼接:
// ✅ 正确写法 StringBuilder sb = new StringBuilder(); for (String str : list) sb.append(str); String result = sb.toString(); // ❌ 错误写法(低效) String s = ""; for (String str : list) s += str; // 每次循环隐式新建 StringBuilder