每日 Java 面试题分享【第 12 天】
欢迎来到每日 Java 面试题分享栏目!
订阅专栏,不错过每一天的练习
今日分享 3 道面试题目!
评论区复述一遍印象更深刻噢~
目录
- 问题一:Java 中 final、finally 和 finalize 各有什么区别?
- 问题二:为什么在 Java 中编写代码时会遇到乱码问题?
- 问题三:为什么 JDK 9 中将 String 的 char 数组改为 byte 数组?
问题一:Java 中 final、finally 和 finalize 各有什么区别?
final
、finally
和 finalize
是 Java 中三个容易混淆的概念,它们的作用和使用场景完全不同。以下是详细分析:
1. final
作用
final
是一个关键字,用于声明类、方法或变量,表示某些特性不可更改。
使用场景
-
修饰类:
-
表示类不能被继承。
-
示例:
public final class FinalClass { // 类体 } // 报错:无法继承 FinalClass class SubClass extends FinalClass {}
-
-
修饰方法:
-
表示方法不能被子类重写。
-
示例:
public class Parent { public final void display() { System.out.println("This is a final method."); } } class Child extends Parent { // 报错:不能重写 final 方法 // @Override // public void display() {} }
-
-
修饰变量:
-
表示变量值不能被重新赋值(常量)。
-
示例:
public class Example { public static final double PI = 3.14159; public void test() { final int x = 10; // 报错:不能修改 final 变量 // x = 20; } }
-
总结
final
用于 限制修改:类不能被继承、方法不能被重写、变量不能重新赋值。
2. finally
作用
finally
是一个用于 异常处理 的关键字,表示无论是否发生异常,finally
块中的代码都会执行。
使用场景
-
确保释放资源:
-
通常用于关闭文件、释放锁、关闭数据库连接等操作。
-
示例:
public class Example { public static void main(String[] args) { try { int result = 10 / 0; // 抛出异常 } catch (ArithmeticException e) { System.out.println("Caught exception: " + e.getMessage()); } finally { System.out.println("This is the finally block."); } } }
-
输出:
Caught exception: / by zero This is the finally block.
-
-
注意事项:
-
即使
try
块中有return
,finally
块仍然会执行。 -
示例:
public class Example { public static void main(String[] args) { System.out.println(test()); } public static int test() { try { return 10; } finally { System.out.println("Finally block executed"); } } }
-
输出:
Finally block executed 10
-
总结
finally
块是 异常处理机制 的一部分,用于执行清理代码。
3. finalize
作用
finalize
是 Object
类的一个方法,用于在对象被 垃圾回收(GC)之前执行清理操作。
使用场景
-
定义对象的清理逻辑:
-
可以在
finalize
方法中释放资源,如关闭文件或网络连接。 -
示例:
public class Example { @Override protected void finalize() throws Throwable { System.out.println("finalize method called"); } public static void main(String[] args) { Example obj = new Example(); obj = null; // 使对象变为垃圾 System.gc(); // 显式调用垃圾回收器 } }
-
-
注意事项:
- 不建议使用:
finalize
的执行时间不确定,依赖垃圾回收机制,可能导致性能问题。 - 已被弃用:从 Java 9 开始,
finalize
方法已被标记为 Deprecated,推荐使用try-with-resources
或显式清理资源代替。
- 不建议使用:
总结
finalize
用于对象被回收前执行清理操作,但已被弃用,不推荐使用。
区别总结表
特性 | final | finally | finalize |
---|---|---|---|
类型 | 关键字 | 关键字 | 方法 |
作用 | 限制类、方法、变量的修改或继承 | 异常处理中用于清理操作 | 对象回收前执行清理操作 |
使用场景 | 定义不可更改的类、方法或常量 | 异常处理中释放资源 | 垃圾回收前释放对象持有的资源 |
是否推荐 | 推荐 | 推荐 | 不推荐(已被废弃) |
扩展:考察重点
- 面试时通常结合实际场景考察:
final
:用于实现不可变类、确保线程安全等。finally
:确保资源释放,如何正确使用异常处理机制。finalize
:了解其作用及替代方案(如AutoCloseable
)。
最佳实践
- 用
final
修饰变量时,配合static
创建常量(例如:public static final
)。 - 资源释放首选
try-with-resources
,避免依赖finally
或finalize
。 - 了解
finalize
的替代方案(如PhantomReference
或显式清理)。
问题二:为什么在 Java 中编写代码时会遇到乱码问题?
在 Java 中编写代码时,遇到乱码问题通常是由于 字符编码 处理不当导致的。Java 对字符编码有一套标准机制,但如果开发、存储和传输的编码方式不一致,就可能产生乱码。以下是详细分析:
常见原因
1. 文件编码与环境编码不一致
- 问题描述:源代码文件保存时的编码与编译或运行时的编码不一致。
- 典型场景:
- 用 UTF-8 编写的文件,在运行时被解释为 GBK 或 ISO-8859-1 等其他编码。
- 比如:文件中有中文字符,保存为 UTF-8 编码,但 Java 默认按系统编码(如 Windows 常用的 GBK)读取文件,导致乱码。
2. 控制台显示编码问题
- 问题描述:Java 程序在控制台输出文本时,控制台的编码与程序的编码不同步。
- 典型场景:
- 在 Windows 上运行程序时,控制台默认编码是 GBK,而程序的输出编码是 UTF-8,结果中文显示乱码。
3. 数据存储或传输中的编码问题
- 问题描述:文本在文件、数据库、网络传输中,存储的编码方式与读取时的编码方式不匹配。
- 典型场景:
- 数据库中保存的文本为 UTF-8,但读取时用 GBK 解码。
- 通过网络传输的数据编码方式未统一,导致接收端解析失败。
4. 未显式指定编码
- 问题描述:在开发或运行时,没有明确指定字符编码,导致 Java 使用了平台默认编码(可能是 GBK、UTF-8、ISO-8859-1 等)。
- 典型场景:
-
读取文件时未指定编码:
BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
默认会使用平台编码,可能与文件实际编码不一致。
-
5. 数据流中的编码转换不当
- 问题描述:在字符流和字节流之间转换时,编码方式未匹配。
- 典型场景:
-
使用
String
的构造方法或getBytes()
方法时,未指定编码:String str = new String(byteArray); // 编码方式默认依赖系统
-
如何避免和解决乱码问题
1. 确定并统一编码
-
统一开发环境编码:
- 确保 IDE(如 IntelliJ IDEA、Eclipse)保存的文件编码一致。
- 常用设置:文件编码使用 UTF-8。
- 在 IntelliJ IDEA 中:
- 设置路径:
File -> Settings -> Editor -> File Encodings
。 - 推荐将项目、全局和默认编码均设置为 UTF-8。
- 设置路径:
-
统一运行时编码:
-
显式指定 JVM 的默认编码:
java -Dfile.encoding=UTF-8 YourClassName
-
在代码中指定读取或写入时的编码:
BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream("test.txt"), "UTF-8"));
-
2. 控制台编码匹配
- 解决控制台显示乱码:
-
在 Windows 系统下,手动将 CMD 控制台编码改为 UTF-8:
chcp 65001
-
或者直接在代码中设置:
System.setOut(new PrintStream(System.out, true, "UTF-8"));
-
3. 数据库编码
- 确保数据库编码正确:
-
数据库表和列的编码设置为 UTF-8(如 MySQL 中
CHARACTER SET utf8
)。 -
确保 JDBC URL 中设置字符编码,例如:
jdbc:mysql://localhost:3306/dbname?useUnicode=true&characterEncoding=UTF-8
-
4. 网络传输编码
- 统一客户端和服务端的编码:
-
在 HTTP 请求或响应时,显式指定内容的编码格式:
response.setCharacterEncoding("UTF-8");
-
5. 避免依赖系统默认编码
- 字符串与字节数组之间转换时指定编码:
-
错误示例:
String str = new String(byteArray);
-
正确示例:
String str = new String(byteArray, "UTF-8");
-
6. 使用工具检查编码
- 文件编码检查:
-
使用工具(如 Notepad++、VS Code)检查和修改文件编码。
-
Linux 中使用
file
命令检查:file -i test.txt
-
扩展:乱码的本质
乱码的根本原因是:
- 字节数据和字符数据之间的解码不匹配:
- 字节流中的数据按照错误的字符编码方式被解码。
- 平台编码差异:
- Java 默认使用的编码(如系统默认编码)和实际数据编码不同。
例如:
- 编码阶段:字符
"中"
在 UTF-8 中表示为0xE4B8AD
。 - 解码阶段:按 GBK 解码时,
0xE4B8AD
会被解释为乱码字符。
总结
为了避免 Java 中的乱码问题,应做到:
- 统一编码:开发环境、运行环境和数据存储都使用 UTF-8。
- 显式指定编码:文件读取、网络传输、数据库连接时,明确设置编码格式。
- 遵循最佳实践:避免依赖系统默认编码,尤其是在跨平台开发中。
通过以上方法,可以有效避免和解决 Java 开发中的乱码问题。
问题三:为什么 JDK 9 中将 String 的 char 数组改为 byte 数组?
在 JDK 9 中,String
类的底层实现从使用 char[]
数组改为使用 byte[]
数组,这是 Java 性能优化 的一部分。这一改变被称为 Compact Strings,是为了解决内存占用问题并提升性能。下面详细解答这一问题的背景、原因以及优缺点:
背景
-
JDK 8 及之前的实现:
String
类底层使用char[]
数组存储字符,每个字符占用 2 字节(基于 UTF-16 编码)。- 无论字符串是否只包含单字节字符(如 ASCII 字符),都统一分配 2 字节存储空间。
- 举例:
- 字符串
"abc"
,实际只需要 3 字节,但用char[]
存储需要占用 6 字节。
- 字符串
-
内存浪费问题:
- 大多数应用中的字符串包含大量 ASCII 字符(单字节字符)。
- 使用
char[]
数组存储所有字符串,导致约 50% 的内存被浪费。
Compact Strings 的实现
在 JDK 9 中,String
类的底层实现改为 byte[]
数组,并添加了一个 coder
字段,表示字符串的编码方式:
-
byte[]
数组存储实际数据:- 如果字符串只包含单字节字符(如 ASCII),使用单字节编码(
ISO-8859-1
)。 - 如果字符串包含多字节字符(如中文、阿拉伯文),使用双字节编码(
UTF-16
)。
- 如果字符串只包含单字节字符(如 ASCII),使用单字节编码(
-
coder
字段:- 一个额外的字节字段,标记当前字符串是单字节(
ISO-8859-1
)还是双字节(UTF-16
)。
- 一个额外的字节字段,标记当前字符串是单字节(
具体原因
1. 内存占用优化
- 对于大多数只包含单字节字符(如英文和数字)的字符串,使用
byte[]
存储只需占用原来一半的内存。 - 举例:
- JDK 8 中,字符串
"abc"
使用char[]
需要 6 字节。 - JDK 9 中,字符串
"abc"
使用byte[]
只需 3 字节。
- JDK 8 中,字符串
- 对于包含多字节字符的字符串,
byte[]
仍使用 UTF-16,但内存占用与之前相同,不会额外浪费。
2. 提升 CPU 缓存利用率
byte[]
占用的内存更少,意味着在运行时,更多的字符串数据可以装入 CPU 的缓存,从而减少缓存未命中的概率。- 这对性能要求高的应用(如大规模 web 应用)尤为重要。
3. 减少 GC 压力
- 内存占用减少,意味着堆中存储的字符串对象更小,从而降低垃圾回收器(GC)的压力,提升程序整体性能。
4. 保持向后兼容
- 尽管底层改用
byte[]
实现,但在 API 层面,String
的行为保持不变。 - 用户无需修改代码,仍然可以像以前一样使用字符串操作。
优缺点分析
优点
- 内存效率显著提升:
- 减少字符串对象的内存开销,特别是在大多数字符串为 ASCII 字符的场景下(如 web 服务、日志系统)。
- 性能优化:
- 更少的内存占用,提升 CPU 缓存利用率。
- 更低的 GC 开销,提升运行效率。
- 无缝向后兼容:
- 不需要修改现有代码,API 保持一致。
- 提升整体应用性能:
- 尤其对使用大量短字符串的应用效果显著。
缺点
- 稍微增加的复杂性:
- 为了支持两种编码(
ISO-8859-1
和UTF-16
),需要维护额外的coder
字段。 - 一些字符串操作(如
charAt
、substring
)需要根据编码动态判断字节或字符长度,稍微增加了运算复杂度。
- 为了支持两种编码(
- 特定场景下优化有限:
- 如果字符串包含大量多字节字符(如中文、阿拉伯文),内存占用优化效果不明显。
代码示例
以下是 String
在 JDK 8 和 JDK 9 中的底层实现对比(伪代码展示):
JDK 8 实现
public final class String {
private final char[] value; // 使用 char[] 存储字符串,每个字符占 2 字节
private final int hash; // 缓存 hashCode 值
// …
}
JDK 9 实现
public final class String {
private final byte[] value; // 使用 byte[] 存储字符串,节省内存
private final byte coder; // 标记编码类型(0: ISO-8859-1, 1: UTF-16)
private final int hash; // 缓存 hashCode 值
// …
}
扩展:实际应用场景
1. Web 应用
- 大量处理 HTTP 请求、响应头和 JSON 数据,这些数据大多是 ASCII 字符。
- 使用 Compact Strings 可以显著降低内存使用。
2. 数据库连接和日志
- 数据库查询返回的结果、系统日志文件等通常以英文为主,Compact Strings 带来巨大优化。
3. 内存敏感型应用
- 如移动设备上的 Java 应用,内存资源有限,使用 Compact Strings 是提升性能的关键。
总结
JDK 9 将 String
的底层实现从 char[]
改为 byte[]
的主要目的是为了 优化内存使用 和 提升性能,通过支持两种编码(单字节和双字节)显著减少了字符串对象的内存占用。这种改进在不改变 API 的情况下,提升了 Java 应用在内存和运行效率方面的表现,是一项重要的性能优化措施。
总结
今天的 3 道 Java 面试题,您是否掌握了呢?持续关注我们的每日分享,深入学习 Java 面试的各个细节,快速提升技术能力!如果有任何疑问,欢迎在评论区留言,我们会第一时间解答!
明天见!🎉