跳至主要內容
Java 基础面试一

Java 基础面试一

钝悟...大约 32 分钟JavaJavaCore面试JavaJavaCore面试

Java 基础面试一

Java 常识

【简单】Java 语言有什么优势?

  • 跨平台:【一次编写,到处执行(Write Once, Run Anywhere)】——JVM 执行字节码。
  • 自动垃圾回收:垃圾回收(GC)减少内存泄漏风险。
  • 强大生态:Spring、Hadoop、Android 等广泛支持。
  • 面向对象:支持封装、继承、多态,代码结构清晰易维护。
  • 高性能:JIT 编译优化,多线程支持高并发。
  • 健壮安全:强类型检查、异常处理、JVM 安全机制。

【简单】Oracle JDK 和 Open JDK 有什么区别?

OpenJDKOracle JDK
是否开源完全开源闭源
是否免费完全免费JDK8u221 之后存在限制
更新频率一般每 3 个月发布一个版本;不提供 LTS 服务一般每 6 个月发布一个版本;大概每三年推出一个 LTS 版本
功能性Java 11 之后,OracleJDK 和 OpenJDK 的功能基本一致
协议GPL v2BCL/OTN

【简单】Java SE 和 Java EE 有什么区别?

Java 技术既是一种编程语言,又是一种平台。Java 编程语言是一种具有特定语法和风格的高级面向对象语言。Java 平台是 Java 编程语言应用程序运行的特定环境。

  • Java SE(Java Platform, Standard Edition) - Java 平台标准版。Java SE 的 API 提供了 Java 编程语言的核心功能。它定义了从 Java 编程语言的基本类型和对象到用于网络、安全、数据库访问、图形用户界面 (GUI) 开发和 XML 解析的高级类的所有内容。除了核心 API 之外,Java SE 平台还包括虚拟机、开发工具、部署技术以及 Java 技术应用程序中常用的其他类库和工具包。
  • Java EE(Java Platform, Enterprise Edition) - Java 平台企业版。Java EE 构建在 Java SE 基础之上。 Java EE 定义了企业级应用程序开发和部署的标准和规范,如:Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS。

【简单】JDK、JRE、JVM 之间有什么关系?

JDK、JRE、JVM 的定义和简介:

  • JVM - Java Virtual Machine 的缩写,即 Java 虚拟机。JVM 是运行 Java 字节码的虚拟机。JVM 不理解 Java 源代码,这就是为什么要将 *.java 文件编译为 JVM 可理解的 *.class 文件(字节码)。Java 有一句著名的口号:“Write Once, Run Anywhere(一次编写,随处运行)”,JVM 正是其核心所在。实际上,JVM 针对不同的系统(Windows、Linux、MacOS)有不同的实现,目的在于用相同的字节码执行同样的结果。
  • JRE - Java Runtime Environment 的缩写,即 Java 运行时环境。它是运行已编译 Java 程序所需的一切的软件包,主要包括 JVM、Java 类库(Class Library)、Java 命令和其他基础结构。但是,它不能用于创建新程序。
  • JDK - Java Development Kit 的缩写,即 Java SDK。它不仅包含 JRE 的所有功能,还包含编译器 (javac) 和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

总结来说,JDK、JRE、JVM 三者的关系是:JDK > JRE > JVM

JDK = JRE + 开发/调试工具
JRE = JVM + Java 类库 + Java 运行库
JVM = 类加载系统 + 运行时内存区域 + 执行引擎

【中等】什么是字节码?采用字节码的好处是什么?

Java 字节码(Java Bytecode)是 Java 源代码编译后生成的中间代码,它是 Java 虚拟机(JVM)执行的指令集。JVM 通过解释器或即时编译(JIT)将字节码转换为机器码执行。字节码是 Java 实现【一次编写,到处执行(Write Once, Run Anywhere)】的核心技术之一。

Java 字节码要点

  • 基本概念
    • 平台无关的中间代码,存储在 .class 文件中。
    • 包含类结构、字段、方法及对应的字节码指令。
  • 指令集:包含加载(aload/iload)、存储(astore)、运算(iadd)、控制流(if_icmpgt)等操作。
  • 执行方式
    • 解释执行:JVM 逐条解释字节码。
    • JIT 编译:热点代码动态编译为机器码优化性能。
  • 动态能力
    • 反射:运行时动态解析/修改字节码(如生成代理类)。
    • 字节码增强:框架(Spring AOP 等)通过 ASM、Javassist 等工具修改字节码,实现 AOP 等功能。

【中等】Java 是编译型语言还是解释型语言?

结论:Java 既是编译型语言,也是解释型语言

什么是编译型语言?什么是解释型语言?

  • 编译型语言open in new window - 程序在执行之前需要一个专门的编译过程,把程序编译成为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。一般情况下,编译型语言的执行速度比较快,开发效率比较低。常见的编译型语言有 C、C++、Go 等。
  • 解释型语言open in new window - 程序不需要编译,只是在程序运行时通过 解释器open in new window ,将代码一句一句解释为机器代码后再执行。一般情况下,解释型语言的执行速度比较慢,开发效率比较高。常见的解释型语言有 JavaScript、Python、Ruby 等。

为什么说 Java 既是编译型语言,也是解释型语言?

Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因此,我们说 Java 是编译和解释并存的。

  • 编译:源码 → 字节码(.java.class)。
  • 解释/JIT:字节码 → 机器码(解释执行 + 热点代码编译优化)。

Java 的源代码,首先,通过 Javac 编译成为字节码(bytecode),即 *.java 文件转为 *.class 文件;然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码来执行。正是由于 JVM 这套机制,使得 Java 可以【一次编写,到处执行(Write Once, Run Anywhere)】。

为了改善解释语言的效率而发展出的 即时编译open in new window 技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成 字节码open in new window 。到执行期时,再将字节码直译,之后执行。Javaopen in new windowLLVMopen in new window 是这种技术的代表产物。常见的 JVM(如 Hotspot JVM),都提供了 JIT(Just-In-Time)编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。

【中等】Java 如何调用外部可执行程序或系统命令?

Java 提供了两种调用外部可执行程序或系统命令的方式:

  • ProcessBuilder
  • Runtime.exec()

【困难】AOT 有什么优点?为什么不全部使用 AOT 呢?

什么是 AOT?

JDK 9 引入静态编译模式 AOT(Ahead of Time Compilation) 。AOT 模式下,程序运行前直接编译为机器码(类似 C/C++/Rust)。

AOT 和 JIT 有什么区别?

AOT vs. JIT

维度AOTJIT
启动速度⭐⭐⭐(极快)⭐(依赖预热)
内存占用⭐⭐⭐(低)⭐⭐(较高)
峰值性能⭐⭐(静态优化)⭐⭐⭐(动态优化)
动态支持❌(受限)✅(完整支持)
适合场景云原生/微服务高吞吐/动态框架

提到 AOT 就不得不提 GraalVMopen in new window 了!GraalVM 是一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。 GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。感兴趣的同学,可以去看看 GraalVM 的官方文档open in new window。如果觉得官方文档看着比较难理解的话,也可以找一些文章来看看,比如:

既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?

AOT 的局限性在于不支持动态特性

  • 不支持反射、动态代理、运行时类加载、JNI 等
  • 影响框架兼容性(如 Spring、CGLIB 依赖 ASM 技术生成动态字节码)

AOT 的适用场景

  • 适合:启动敏感的微服务、云原生应用
  • 不适合:需动态特性的复杂框架或高频优化的长运行任务

Java 基础语法

【简单】Java 有几种注释形式?

注释用于在源代码中解释代码的作用,可以增强程序的可读性,可维护性。 空白行,或者注释的内容,都会被 Java 编译器忽略掉。

Java 注释主要有三种类型:

  • 单行注释
  • 多行注释
  • 文档注释(JavaDoc)
public class HelloWorld {
    /**
     * 文档注释
     */
    public static void main(String[] args) {
        // 单行注释
        /*
        多行注释
        */
        System.out.println("Hello World");
    }

}

【简单】Java 有哪些标识符命名规则?

Java 所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。

标识符基本规则

  • 组成元素:类名、变量名、方法名等统称为标识符
  • 允许字符:可包含字母、数字、$_
  • 首字符要求:不能以数字开头
  • 禁止关键字:如 classpublic 等保留字不可作为标识符
  • 大小写敏感ageAge 被视为不同标识符

命名规范

在 Java 中,标识符通常遵循 驼峰命名法open in new window

类型命名法示例
类/接口名大驼峰(Upper CamelCase)StudentInfoUserService
方法/变量名小驼峰(Lower CamelCase)getUserName()studentAge
常量名全大写蛇形(SNAKE_CASE)MAX_SIZEDEFAULT_TIMEOUT

注意事项

  • 避免使用 $:虽然合法,但通常用于编译器生成代码
  • 无长度限制:但应保持简洁且语义明确(如用 count 而非 c
  • Unicode 支持:可使用中文等字符(但不推荐)

【简单】Java 中有哪些关键字?

下面列出了 Java 保留字,这些保留字不能用于常量、变量、和任何标识符的名称。

分类关键字
访问级别修饰符private、protected、public、default
类,方法和变量修饰符abstract、class、extends、final、implements、interface、native、new、static、strictfp、synchronized、transient、volatile、enum
程序控制语句break、continue、return、do、while、if、else、for、instanceof、switch、case
错误处理assert、try、catch、throw、throws、finally
包相关import、package
数据类型boolean、byte、char、short、int、long、float、double、enum
变量引用super、this、void
其他保留字goto、const

注意

Java 的 null 不是关键字,类似于 truefalse,它是一个字面常量,不允许作为标识符使用。

官方文档https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.htmlopen in new window

【中等】如果移位操作位数超限会怎样?

移位位数处理机制

Java 对移位位数超限的处理采用隐式取模运算

  • int 类型(32 位):实际移位位数 = 指定位数 % 32
    • 例如:x << 42 → 实际左移 42 % 32 = 10
  • long 类型(64 位):实际移位位数 = 指定位数 % 64
    • 例如:x << 100 → 实际左移 100 % 64 = 36

位操作统一规则

操作符示例等效操作说明
<<x << 35x << 3 (35%32=3)左移,低位补 0
>>x >> 35x >> 3 (35%32=3)右移,高位补符号位(算术右移)
>>>x >>> 35x >>> 3 (35%32=3)无符号右移,高位补 0

底层原理

  • 硬件优化:CPU 执行移位指令时,实际只使用指定位数的低 5 位(int)或低 6 位(long),与 Java 的取模规则一致。
  • 安全设计:避免无效的大位数移位(如 x << 1000)导致不可预测行为。

示例

int i = -1; // 二进制全 1(32 个 1)
System.out.println(i << 10);  // 左移 10 位,输出 -1024
System.out.println(i << 42);  // 等效左移 10 位(42%32=10),同样输出 -1024

long l = -1L;
System.out.println(l << 70);  // 等效左移 6 位(70%64=6),输出 -64

特殊情况

  • 移位 0 位:任何 x << 32x >> 64 等效不移位(因 32%32=064%64=0)。
  • 负数移位:移位位数可为负数,但会通过取模转为正数(如 x << -6x << 26,因 -6 % 32 = 26)。

为什么这样设计?

  • 兼容性:与 C/C++的移位行为一致。
  • 性能:直接映射到 CPU 指令,无需额外检查。
  • 确定性:保证结果可预测,避免未定义行为。

Java 数据类型

【简单】Java 有哪些值类型?

Java 中的数据类型有两类:

  • 值类型(又叫内置数据类型,基本数据类型)
  • 引用类型(除值类型以外,都是引用类型,包括 String、数组等)

Java 语言提供了 8 种基本类型,大致分为 4 类:布尔型、字符型、整数型、浮点型。

基本数据类型分类大小默认值取值范围包装类说明
boolean布尔型-falsefalse, trueBooleanboolean 的大小,是由具体的 JVM 实现来决定的
char字符型16 bit'u0000'[0, 2^16 - 1]Character存储 Unicode 码,用单引号赋值
byte整数型8 bit0[-2^7, 2^7 - 1]Byte
short整数型16 bit0[-2^15, 2^15 - 1]Short
int整数型32 bit0[-2^31, 2^31 - 1]Integer
long整数型64 bit0L[-2^63, 2^63 - 1]Long赋值时一般在数字后加上 lL
float浮点型32 bit0.0f[2^-149, 2^128 - 1]Float赋值时必须在数字后加上 fF
double浮点型64 bit0.0d[2^-1074, 2^1024 - 1]Double赋值时一般在数字后加 dD

【简单】什么是装箱、拆箱?

什么是装箱、拆箱?

Java 中为每一种基本数据类型提供了相应的包装类,如下:

Byte <-> byte
Short <-> short
Integer <-> int
Long <-> long
Float <-> float
Double <-> double
Character <-> char
Boolean <-> boolean

引入包装类的目的就是:提供一种机制,使得基本数据类型可以与引用类型互相转换

基本数据类型与包装类的转换被称为装箱和拆箱。

  • 装箱(boxing)是将值类型转换为引用类型。例如:intInteger
    • 装箱过程是通过调用包装类的 valueOf 方法实现的
  • 拆箱(unboxing)是将引用类型转换为值类型。例如:Integerint
    • 拆箱过程是通过调用包装类的 xxxValue 方法实现的。(xxx 代表对应的基本数据类型)。

什么是自动装箱与拆箱?

Integer a = 10;  //装箱
int b = a;   //拆箱

上面这两行代码对应的字节码为:

   L1

    LINENUMBER 8 L1

    ALOAD 0

    BIPUSH 10

    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;

   L2

    LINENUMBER 9 L2

    ALOAD 0

    ALOAD 0

    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;

    INVOKEVIRTUAL java/lang/Integer.intValue ()I

    PUTFIELD AutoBoxTest.n : I

    RETURN

通过字节码代码,不难发现,装箱其实就是调用了 包装类的 valueOf() 方法;而拆箱其实就是调用了 xxxValue() 方法。再次印证前文的内容:

  • 装箱过程是通过调用包装类的 valueOf 方法实现的
  • 拆箱过程是通过调用包装类的 xxxValue 方法实现的

因此,

  • Integer a = 10 等价于 Integer a = Integer.valueOf(10)
  • int b = a 等价于 int b = a.intValue();

【中等】包装类型的缓存机制了解么?

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}

下面我们来看一个问题:下面的代码的输出结果是 true 还是 false 呢?

Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);

Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。

因此,答案是 false 。你答对了吗?

值得一提的是,包装类通过缓存一定范围的常用数值,避免重复创建对象,以减少内存使用的思想,正是采用了享元模式(设计模式之一)。

记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较

【简单】比较包装类型为什么不能用 ==?

Java 值类型的包装类大部分都使用了缓存机制来提升性能:

  • ByteShortIntegerLong 这 4 种包装类,默认都创建了数值在 [-128,127] 范围之间的相应类型缓存数据;
  • Character 创建了数值在 [0,127] 范围之间的缓存数据;
  • Boolean 直接返回 True or False

试图装箱的数值,如果超出缓存范围,则会创建新的对象。

Long.valueOf 方法为例:

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

【中等】为什么浮点数运算的时候会有精度丢失的风险?

浮点数运算精度丢失代码演示:

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a); // 0.100000024
System.out.println(b); // 0.099999905
System.out.println(a == b); // false

为什么会出现这个问题呢?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

就比如说十进制下的 0.2 就没办法精确转换成二进制小数:

// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

【简单】如何解决浮点数运算的精度丢失问题?

BigDecimal 直接使用字符串初始化(如 new BigDecimal("0.1"))可完全避免二进制浮点误差。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)可以通过 BigDecimal 来处理。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */

【简单】超过 long 整型的数据应该如何表示?

基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。

在 Java 中,64 位 long 整型是最大的整数类型。

long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

Java 变量

【简单】静态变量、成员变量、局部变量的区别?

静态变量、成员变量、局部变量的主要区别

特性静态变量(static)成员变量(非static)局部变量
所属类(所有实例共享)对象(每个实例独立)方法/代码块内
生命周期类加载时创建,程序结束时销毁对象创建时存在,垃圾回收时销毁方法调用时创建,执行完销毁
存储位置方法区(JDK8+在元空间/堆)堆(对象内部)栈(方法栈帧)
默认值有(如int默认为0)有(同静态变量)(必须手动初始化)
访问方式类名.变量名对象.变量名对象.变量名只能在声明的方法/块内使用

一句话总结

  • 静态变量:全局唯一,类共享。
  • 成员变量:对象私有,每个实例独立。
  • 局部变量:临时使用,方法内有效。

【简单】为什么成员变量有默认值?

成员变量有默认值的核心原因是:防止随机值风险

  • 内存安全:未初始化的变量会指向内存中的随机值,可能导致程序行为异常或崩溃。
  • 稳定运行:自动赋默认值(如 int0booleanfalse)确保程序逻辑可预测。

编译器设计的权衡

  • 成员变量自动赋默认值是内存安全与灵活性的平衡
    • 运行时可能通过反射、构造器等动态赋值,编译器无法完全静态检测。
    • 为避免误报错误,统一自动赋默认值。
  • 局部变量严格编译检查确保代码可靠性
    • 作用域限于方法内,编译器可严格检查是否赋值。
    • 强制手动初始化以规避潜在风险。

【简单】字符型常量和字符串常量的区别?

场景字符常量字符串常量
表示形式单引号括起的单个字符'A'双引号括起的字符序列"ABC"
数据类型char(基本类型)String(引用类型)
内存占用2 字节(Unicode 字符,如 '中''\n'对象开销+字符数据(可变长度)
转义字符支持('\t''\\'同样支持("\t""\\"
空值表示不可为空(至少 1 字符)可为空(""
运算行为按 Unicode 值运算重载+为拼接

Java 方法

【简单】Java 方法有哪些类型?

Java 方法的类型可以从不同维度分类。

类型关键字调用方式特点示例
实例方法对象名.方法名()依赖对象实例,可访问实例成员list.add("item")
静态方法static类名.方法名()不依赖实例,只能访问静态成员Math.abs(-1)
构造方法new 类名()用于对象初始化,无返回值类型new String("hello")

代码示例

// 实例方法 vs 静态方法
class Calculator {
    // 实例方法
    public int add(int a, int b) { return a + b; }

    // 静态方法
    public static int staticAdd(int a, int b) { return a + b; }
}

// 抽象方法
abstract class Shape {
    abstract void draw(); // 必须由子类实现
}

// 默认方法
interface Logger {
    default void log(String msg) { System.out.println(msg); }
}

// 泛型方法
class Box {
    public <T> T wrap(T item) { return item; }
}

如何选择方法类型?

  • 需要操作对象状态 → 实例方法(如user.getName()
  • 工具类操作 → 静态方法(如Collections.sort()
  • 强制子类实现 → 抽象方法(如Animal.eat()
  • 接口功能扩展 → 默认方法(Java 8+)
  • 线程安全控制synchronized方法

【简单】静态方法和实例方法有何不同?

静态方法和实例方法主要区别

维度静态方法 (Static Method)实例方法 (Instance Method)
归属属于类属于对象实例
关键字使用 static 修饰static 修饰
调用方式类名.方法名()对象名.方法名()
内存分配类加载时分配,永久代(JDK8 前)/元空间(JDK8+)对象实例化时分配,堆内存
生命周期与类相同(从类加载到 JVM 退出)与对象相同(从对象创建到被 GC 回收)

访问权限对比

维度静态方法实例方法
访问静态成员✅ 可直接访问✅ 可直接访问
访问实例成员❌ 不能直接访问(需先创建对象)✅ 可直接访问
this/super❌ 不可使用✅ 可使用

代码示例

class Calculator {
    // 静态方法
    public static int add(int a, int b) {
        return a + b;  // 不依赖对象状态
    }

    // 实例方法
    private int base;
    public void setBase(int base) {
        this.base = base;  // 依赖对象状态
    }
    public int calculate(int x) {
        return base + x;  // 访问实例变量
    }
}

// 调用示例
public class Main {
    public static void main(String[] args) {
        // 静态方法调用
        int sum = Calculator.add(3, 5);  // 无需创建对象

        // 实例方法调用
        Calculator calc = new Calculator();
        calc.setBase(10);
        int result = calc.calculate(5);  // 需要对象实例
    }
}

【简单】重载和重写有什么区别?

Java 重载(Overload)与重写(Override)的核心区别

特性重载(Overload)重写(Override)
定义同一类中方法名相同但参数不同子类重新实现父类的方法
目的处理不同类型/数量的参数修改或扩展父类方法的行为
多态类型编译时多态(静态绑定)运行时多态(动态绑定)
作用范围同一类中(或父子类间)子类与父类之间
方法签名必须不同参数(类型/数量/顺序)必须完全相同(方法名+参数)
返回值可自由修改基本类型/void:必须相同;引用类型:可协变(子类更具体)
异常可自由声明子类异常 ≤ 父类异常范围
访问权限可自由修改子类权限 ≥ 父类(不能更严格)
限制方法不能重写 private/final/static 方法
class Calculator {
    // 参数类型不同
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }

    // 参数数量不同
    int add(int a, int b, int c) { return a + b + c; }
}

关键区别总结

  • 绑定时机
    • 重载:编译时根据参数决定调用的方法(Calculator.add(int) vs Calculator.add(double)
    • 重写:运行时根据对象实际类型决定方法(Animal.sound() 实际调用 Cat.sound()
  • 设计目的
    • 重载:横向扩展(同一功能的不同参数版本)
    • 重写:纵向覆盖(子类定制父类行为)
  • 验证阶段
    • 重载:编译器检查参数差异
    • 重写:编译器检查方法签名 + JVM 运行时验证

【简单】什么是可变长参数?

从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。

public static void method1(String... args) {
   //......
}

另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。

public static void method2(String arg1, String... args) {
   //......
}

遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?

答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。

我们通过下面这个例子来证明一下。

public class VariableLengthArgument {

    public static void printVariable(String... args) {
        for (String s : args) {
            System.out.println(s);
        }
    }

    public static void printVariable(String arg1, String arg2) {
        System.out.println(arg1 + arg2);
    }

    public static void main(String[] args) {
        printVariable("a", "b");
        printVariable("a", "b", "c", "d");
    }
}

输出:

ab
a
b
c
d

另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 class文件就可以看出来了。

public class VariableLengthArgument {

    public static void printVariable(String... args) {
        String[] var1 = args;
        int var2 = args.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            String s = var1[var3];
            System.out.println(s);
        }

    }
    // ......
}

Java 异常

【简单】Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  • Exception - 程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又分为检查(checked)异常和非检查(unchecked)异常,检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。
  • Error - Error 属于程序无法处理的错误。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

【简单】Checked Exception 和 Unchecked Exception 有什么区别?

差异对比

特性Checked ExceptionUnchecked Exception
编译检查必须显式处理(catch/throws),否则编译失败不强制处理,编译可通过
继承体系继承自 Exception(非 RuntimeException 分支)继承自 RuntimeException
设计目的处理可预见的、可恢复的异常情况(如文件不存在)处理程序逻辑错误(如空指针)
// 必须处理IOException(受检异常)
try {
    Files.readAllBytes(Paths.get("file.txt"));
} catch (IOException e) {  // 或声明 throws IOException
    System.err.println("文件读取失败: " + e.getMessage());
}

常见异常类型

Checked ExceptionUnchecked Exception
IOExceptionNullPointerException
SQLExceptionIllegalArgumentException
ClassNotFoundExceptionArrayIndexOutOfBoundsException
InterruptedExceptionClassCastException

选择原则

  • 用 Checked Exception

    • 调用方必须处理该异常(如文件不存在、网络断开)
    • 异常是业务逻辑的合法流程(如用户输入校验)
  • 用 Unchecked Exception

    • 表示程序错误(如参数为null、数组越界)
    • 调用方无法合理恢复(如内存溢出)

【简单】Throwable 类常用方法有哪些?

  • String getMessage(): 返回异常发生时的简要描述
  • String toString(): 返回异常发生时的详细信息
  • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

【简单】try-catch-finally 如何使用?

  • try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块:用于处理 try 捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

代码示例:

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
} finally {
    System.out.println("Finally");
}

输出:

Try to do something
Catch Exception -> RuntimeException
Finally

注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

jvm 官方文档open in new window 中有明确提到:

If the try clause executes a return, the compiled code does the following:

  1. Saves the return value (if any) in a local variable.
  2. Executes a jsr to the code for the finally clause.
  3. Upon return from the finally clause, returns the value saved in the local variable.

代码示例:

public static void main(String[] args) {
    System.out.println(f(2));
}

public static int f(int value) {
    try {
        return value * value;
    } finally {
        if (value == 2) {
            return 0;
        }
    }
}

输出:

0

【简单】finally 中的代码一定会执行吗?

不一定的!在某些情况下,finally 中的代码不会被执行。

就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
    // 终止当前正在运行的 Java 虚拟机
    System.exit(1);
} finally {
    System.out.println("Finally");
}

输出:

Try to do something
Catch Exception -> RuntimeException

另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:

  1. 程序所在的线程死亡。
  2. 关闭 CPU。

【简单】如何使用 try-with-resources 代替try-catch-finally

  1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
  2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

《Effective Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

Java 中类似于InputStreamOutputStreamScannerPrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

//读取文本文件的内容
Scanner scanner = null;
try {
    scanner = new Scanner(new File("D://read.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。

通过使用分号分隔,可以在try-with-resources块中声明多个资源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
}
catch (IOException e) {
    e.printStackTrace();
}

【简单】NoClassDefFoundError 和 ClassNotFoundException 有什么区别

NoClassDefFoundError是一个 Error,而 ClassNotFoundException 是一个 Exception。

ClassNotFoundException 产生的原因:

  • 使用 Class.forNameClassLoader.loadClassClassLOader.findSystemClass 方法动态加载类,如果这个类没有被找到,那么就会在运行时抛出 ClassNotFoundException 异常;
  • 当一个类已经被某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。

NoClassDefFoundError 产生的原因:当 JVM 或 ClassLoader 试图加载类,却找不到类的定义时(编译时存在,运行时找不到),抛出异常。

【简单】异常使用有哪些需要注意的地方?

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
  • 抛出的异常信息一定要有意义。
  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException
  • 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
  • ……

【中等】Java 中 final、finally 和 finalize 有什么区别?

特性finalfinallyfinalize
类型关键字代码块方法
作用域变量/方法/类异常处理块Object类方法
作用声明不可变性即使有异常也必然执行,确保资源释放对象回收前的清理(已废弃)
特点可修饰变量(常量)、方法(不可重写)、类(不可继承)try-catch搭配,必然执行(除非JVM退出)不推荐用,执行时机不可控
使用场景定义常量/限制继承资源清理历史遗留的清理逻辑

一句话总结final不变性finally必执行finalize过时的清理机制

(注:现代Java开发用try-with-resources替代finalize

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7