根据 JavaGuide 整理了一个自己看的版本。
目录
[Java SE vs Java EE](#Java SE vs Java EE)
[JVM vs JDK vs JRE](#JVM vs JDK vs JRE)
[为什么Java 是编译与解释共存的语言?](#为什么Java 是编译与解释共存的语言?)
[AOT 有什么优点?为什么不全部使用 AOT 呢?](#AOT 有什么优点?为什么不全部使用 AOT 呢?)
[Oracle JDK vs OpenJDK](#Oracle JDK vs OpenJDK)
[Java 和 C++ 的区别?](#Java 和 C++ 的区别?)
[continue、break 和 return 的区别?](#continue、break 和 return 的区别?)
[超过 long 整型的数据应该如何表示?BigInteger](#超过 long 整型的数据应该如何表示?BigInteger)
[成员变量 vs 局部变量](#成员变量 vs 局部变量)
[static 静态变量有什么作用?](#static 静态变量有什么作用?)
[面向对象 vs 面向过程](#面向对象 vs 面向过程)
[创建一个对象用什么运算符? 对象实例与对象引用有何不同?](#创建一个对象用什么运算符? 对象实例与对象引用有何不同?)
[对象相等 vs 引用相等](#对象相等 vs 引用相等)
[构造方法特点?是否可被 override?](#构造方法特点?是否可被 override?)
[深拷贝 vs 浅拷贝?引用拷贝?](#深拷贝 vs 浅拷贝?引用拷贝?)
[hashCode() 有什么用?](#hashCode() 有什么用?)
[为什么要有 hashCode?](#为什么要有 hashCode?)
[String 不可变原因](#String 不可变原因)
[Java 字符串拼接:"+" vs StringBuilder](#Java 字符串拼接:“+” vs StringBuilder)
[Java 字符串常量池(String Pool)](#Java 字符串常量池(String Pool))
[String s1 = new String("abc"); 会创建几个字符串对象?](#String s1 = new String("abc"); 会创建几个字符串对象?)
[String 类型的变量和常量做"+"运算时发生了什么?](#String 类型的变量和常量做“+”运算时发生了什么?)
[Exception 和 Error 有什么区别?](#Exception 和 Error 有什么区别?)
[ClassNotFoundException 和 NoClassDefFoundError 的区别](#ClassNotFoundException 和 NoClassDefFoundError 的区别)
[Checked Exception 和 Unchecked Exception 有什么区别?](#Checked Exception 和 Unchecked Exception 有什么区别?)
[你更倾向于使用 Checked Exception 还是 Unchecked Exception?](#你更倾向于使用 Checked Exception 还是 Unchecked Exception?)
[Throwable 类常用方法有哪些?](#Throwable 类常用方法有哪些?)
[throw 和 throws 的区别](#throw 和 throws 的区别)
[try-catch-finally 如何使用?](#try-catch-finally 如何使用?)
[finally 一定会执行吗?](#finally 一定会执行吗?)
[如何使用 try-with-resources 代替 try-catch-finally?](#如何使用 try-with-resources 代替 try-catch-finally?)
[😺Java IO 流](#😺Java IO 流)
[Java IO 四大抽象基类是什么?](#Java IO 四大抽象基类是什么?)
[BIO、NIO 和 AIO 的区别?](#BIO、NIO 和 AIO 的区别?)
😺Java的特点是什么?
- 简单易学 ;
语法相对简单,上手容易,去除了 C/C++ 中复杂的指针和手动内存管理。 - 面向对象 (封装,继承,多态);
面向对象:Java 是用"对象"来描述现实世界的。
封装:把属性和方法放在一起,并保护数据,外部不能随便改。
继承:子类可以继承父类的属性和方法,提高代码复用,Dog 自动拥有 animal 的 eat 方法。
多态:同一个方法,不同对象表现不同。 - 平台无关性 (Java 虚拟机实现平台无关性);
Java 的跨平台性是通过 JVM 实现的,JVM 屏蔽了底层操作系统差异,Java 代码不会直接运行在 Windows 或 Linux 上,而是先编译成.class字节码文件,然后由不同平台对应的 JVM 负责解释和执行(Write Once, Run Anywhere一次编写,到处运行)
流程:.java -> javac编译 -> .class -> JVM运行。 - 可靠性和安全性
垃圾回收机制(GC):对象不再使用时自动释放内存,减少内存泄漏和野指针问题。
异常处理机制 try-catch:用于捕获运行时异常,提高程序稳定性。 - 支持多线程 ;
Java 天生支持多线程,这对于高并发项目非常重要。
例如可以通过 Thread(最基础的线程创建方式)、Thread Pool 线程池(提前准备好一批线程,反复复用,避免创建过多线程。降低创建开销、控制最大线程数、提高性能)、加锁synchronized(简单锁)、Lock(更灵活的高级锁)等机制实现并发处理。 - 支持网络编程并且很方便 ;
Java 提供丰富的网络编程 API,例如 Socket(建立一条通信通道)、HTTP(发送 HTTP 请求调用服务),便于开发分布式系统和 Web 项目。 - 编译与解释并存 ;
Java 先通过编译器编译为字节码,再由 JVM 解释执行,同时 Just In Time (JIT) 即时编译器会将热点代码编译为机器码提升性能。 - 生态完整 。
Java 拥有强大的企业级生态,是当前最大的优势之一。Spring Boot(项目骨架 / 地基:启动服务器、接收 HTTP 请求、返回 JSON、依赖注入、配置管理YAML文件)、Apache Maven(项目依赖管理工具 pom.xml)、MyBatis(数据库框架,负责 Java 和 MySQL 交互)、Redis 客户端生态(实现缓存、分布式锁、消息队列、登录状态管理等功能)、Spring Cloud(微服务框架,负责服务之间通信)、Apache Kafka(消息队列MQ,用于削峰、异步处理、解耦系统)企业级开发几乎全覆盖。 - 比如典型 Java 生态:SpringBoot + MyBatis + Redis + MySQL。
联想到的问题:
- Java 中进程和线程的区别
简单说:进程是资源分配单位,线程是 CPU 调度单位,一个进程里可以有多个线程。
详细说:进程是操作系统进行资源分配的基本单位,每个进程拥有独立的内存空间和系统资源;线程是 CPU 调度和执行的基本单位,是进程中的一条执行路径,线程共享进程的内存空间和资源,但拥有独立的程序计数器和栈空间。
- MyBatis 和 MyBatis Plus 的区别
MyBatis 就是数据库框架,需要自己写 SQL,灵活,SQL 可控。
比如查用户:
java@Select("select * from user where id = #{id}") User selectById(Long id);MyBatis-Plus 是基于 MyBatis 的增强版,核心特点就是封装常用了CRUD,少写 SQL。
比如查主键:
javauserMapper.selectById(1);删除:
javauserMapper.deleteById(1);条件查询:MyBatis-Plus 里的一个条件构造器 LambdaQueryWrapper,专门用来拼接 SQL 查询条件where的工具。常用写法:
eq 等值查询:
java// select * from user where id = 1 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getId, 1);like 模糊查询:
java// where name like '%Tom%' wrapper.like(User::getName, "Tom");gt / lt 范围查询:
java// where age > 18 wrapper.gt(User::getAge, 18);实际的应用例子:
java// select * from user where phone = ? User user = userService.getOne( new LambdaQueryWrapper<User>() .eq(User::getPhone, phone) );LambdaQueryWrapper 和 QueryWrapper 的区别:
QueryWrapper用字符串字段名。
LambdaQueryWrapper用类型安全字段引用,避免了字符串字段名写错的问题,具有更好的类型安全性和重构友好性。
java// QueryWrapper 字符串字段名 wrapper.eq("name", "Tom"); // LambdaQueryWrapper 字段引用 wrapper.eq(User::getName, "Tom");
Java SE vs Java EE
Java SE(Standard Edition)是 Java 标准版,包含 JVM 和核心类库,是 Java 开发的基础,主要用于桌面应用和基础服务端程序开发。
Java EE(Enterprise Edition)是建立在 Java SE 基础上的企业版,提供了面向企业级开发的标准和规范,例如 Servlet、JDBC 等,主要用于开发大型 Web 应用和分布式服务端系统。
简单来说,SE 偏基础,EE 偏企业级开发。
JVM vs JDK vs JRE
JDK = 开发工具包
JRE = 运行环境
JVM = 真正运行 Java 代码的虚拟机
java
JDK
├── JRE
│ ├── JVM
│ └── 核心类库(String/List/HashMap/IO...)
└── 开发工具(javac/javap/javadoc...)
JDK > JRE > JVM
JVM(Java 虚拟机 Java Virtual Machine)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。JVM 并不是只有一种!JVM 是一套规范,只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。

JRE(Java 运行环境 Java Runtime Environment)是给"运行程序的人"准备的,JRE = JVM + 基础类库。
JDK(Java 开发工具包 Java Development Kit)是一个功能齐全的 Java 开发工具包,供开发者使用,用于创建和编译 Java 程序,JDK = JRE + 开发工具。开发工具包括:编译器 javac(把 .java 编译成 .class)、javap(反编译工具,可以查看字节码)、javadoc文档生成工具(用于生成 API 文档)、jdb(调试器)等。 JDK 不仅包含 JRE,还包括用于开发和调试 Java 程序的工具。
什么是字节码?采用字节码的好处是什么?
字节码就是 JVM 可以理解的代码(扩展名为 .class),是 Java 源代码经过编译器 javac 编译后生成的中间代码,供 JVM 执行。它不面向任何特定的处理器,只面向虚拟机,因此Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

为什么不直接生成机器码? C / C++:源代码 -> 机器码,直接编译给 CPU 执行。而 Java 多了一层字节码中间层,为什么?核心就是跨平台,因为机器码和操作系统、CPU 强相关,所以 Java 设计成先编译成平台无关的字节码,再交给不同平台的 JVM 去转换。
字节码最大的好处:
1)跨平台性强 (最核心),字节码不依赖具体操作系统和 CPU,它只面向 JVM。
2)兼顾性能和可移植性 。
3)支持运行时优化,JVM 可以通过 JIT 即时编译器将 Hot Spot 热点代码编译为机器码,提高程序运行效率。
JIT 是什么? Just In Time Compiler 即时编译器。
Java 运行流程: java 源码 → javac编译器 → .class 字节码 → 先解释执行(边读边生成 CPU 指令并执行)→ 热点代码被 JIT 编译 → 直接机器码执行。
**.class→机器码:**这一步 JVM 先加载字节码文件,通过解释器逐行解释执行,这种方式的执行速度比较慢。后面引进了 JIT 编译器,其在运行时编译,会将热点代码字节码对应的机器码保存下来,下次可以直接使用,机器码的运行效率肯定是高于 Java 解释器的。
**为什么 Java 越跑越快?**核心原因就是 JIT 热点优化。根据二八定律(20%代码消耗80%性能),消耗大部分系统资源的只有那一小部分的代码(热点代码Hot Spot,平时最常用的 JVM HotSpot 名字就是这么来的),而这也就是 JIT 所需要编译的部分。比如某个方法被调用很多次,第一次执行时解释执行,如果执行了很多次,JVM 发现这是热点代码,就交给 JIT 编译成机器码,下次直接执行机器码,速度就非常快。

JDK、JRE、JVM、JIT 这四者的关系如下图所示。

为什么Java 是编译与解释共存的语言?
因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。
首先,Java 源代码(.java)需要通过 javac 编译器编译成字节码文件(.class),这体现了编译型语言的特点。
其次,程序运行时,JVM 会通过解释器逐条解释执行字节码,这体现了解释型语言的特点。
此外,对于频繁执行的热点代码,JVM 还会通过 JIT 即时编译器将字节码编译为本地机器码,以提高执行效率。
我们可以将高级编程语言按照程序的执行方式分为两种:
- 编译型:编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型:解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快(因为写完就能跑,修改后立即生效),执行速度比较慢。常见的解释性语言有 Python、JavaScript 等等。
为了改善解释语言的效率而发展出的即时编译技术 JIT,已经缩小了这两种语言间的差距。
AOT 有什么优点?为什么不全部使用 AOT 呢?
AOT(提前编译 Ahead of Time Compilation),和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 可以提高 Java 程序的启动速度,能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生(专门为云环境设计的软件开发方式)场景,对微服务架构的支持也比较友好。
**既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?**因为 Java 大量依赖动态特性,例如反射、动态代理等等,而 Spring 等主流框架底层广泛使用这些特性。如果完全使用 AOT,这些动态能力会受到限制。
Oracle JDK vs OpenJDK
OpenJDK 是 Java 的开源实现,也是官方参考实现;Oracle JDK 是 Oracle 基于 OpenJDK 发布的商业发行版。
两者主要区别包括:
-
是否开源:OpenJDK 完全开源,Oracle JDK 并非完全开源。
-
授权协议:OpenJDK 免费商用,Oracle JDK 需要关注商业授权。
-
功能差异:早期 Oracle JDK 包含部分专有工具,但 Java 11 之后功能基本一致。
-
发行支持:Oracle JDK 提供官方商业支持,OpenJDK 更多依赖社区和第三方发行版。
在实际开发中,通常优先选择 OpenJDK 或基于它的发行版。
Java 和 C++ 的区别?
Java 和 C++ 都是面向对象语言,都支持封装、继承和多态,但两者有以下主要区别:
-
内存管理不同:Java 使用垃圾回收机制GC自动管理内存;C++ 需要手动申请和释放内存。
-
指针支持不同:C++ 支持显式指针操作;Java 不提供直接内存指针,安全性更高。
-
继承机制不同:Java 类只支持单继承,但接口支持多实现;C++ 支持多继承。
-
重载能力不同:C++ 支持方法重载和运算符重载;Java 仅支持方法重载。
-
跨平台能力不同:Java 通过 JVM 实现跨平台;C++ 通常需要针对不同平台重新编译。
总体来说,Java 更注重开发效率和安全性,C++ 更注重性能和底层控制能力。
移位运算符
Java 中有三种移位运算符:
| 运算符 | 名称 | 移动方向 | 符号位处理 | 类比数学运算 |
|---|---|---|---|---|
| << | 左移 | 左 | 高位丢弃,低位补0 | x × 2^n(不溢出时) |
| >> | 带符号右移 | 右 | 高位补符号位(正数补0,负数补1) | x ÷ 2^n(向下取整) |
| >>> | 无符号右移 | 右 | 高位补0,忽略符号位 | 无符号除以 2^n |
- int 和 long 才能直接移位,byte、short、char 会先提升到 int 再操作。
- 移位常用于高性能乘除、哈希、位标志等
联想到的问题:HashMap 的 hash 方法
HashMap 是 Java 中常用的键值对集合,它的底层结构是 数组 + 链表(或红黑树):
- 数组:存放哈希桶(bucket)
- 链表/红黑树:解决哈希冲突(同一个桶内的元素)
关键点:数组长度固定为 2 的幂,所以哈希值需要做一些处理才能均匀分布,hash方法是HashMap内部优化哈希值的工具,目的是均匀分布、降低冲突、提升查询效率。
为什么 HashMap 需要 h ^ (h >>> 16)?因为数组长度是 2 的幂,哈希值直接取模只看低位,容易冲突;异或高位可以把高位信息混合进低位,使分布更均匀。
continue、break 和 return 的区别?
continue:指跳出当前的这一次循环,继续下一次循环。
break:指跳出整个循环体,继续执行循环下面的语句。
return 用于跳出所在方法,结束该方法的运行。
- return; 直接使用 return 结束方法执行,用于没有返回值函数的方法。
- return value; 返回一个特定值,用于有返回值函数的方法。
😺基本数据类型
| 类型 | 大小 | 默认值 | 对应包装类 | |
|---|---|---|---|---|
| 整数型 | byte | 8 | 0 | Byte |
| 整数型 | short | 16 | 0 | Short |
| 整数型 | int | 32 | 0 | Integer |
| 整数型 | long | 64 | 0L | Long |
| 浮点型 | float | 32 | 0.0f | Float |
| 浮点型 | double | 64 | 0.0d | Double |
| 字符类型 | char | 16 | '\u0000' | Character |
| 布尔型 | boolean | 1(逻辑位) | false | Boolean |
- long 必须加 L 后缀,否则整数字面量默认是 int,可能溢出。
- float 必须加 f 或 F 后缀,否则将无法通过编译。
- char a = 'h'char :单引号,String a = "hello" :双引号。
- 每个基本类型都有对应包装类,可用于泛型、集合或工具方法,会自动装箱 / 拆箱。
基本类型和包装类型的区别?
包装类的出现,主要是为了解决 基本类型在 Java 中的一些局限性 ,它们本质上是 把基本类型封装成对象,从而能享受对象特性、支持泛型、可为 null,并提供丰富的工具方法,适合面向对象开发和框架使用。
| 区别点 | 基本类型(Primitive) | 包装类型(Wrapper) |
|---|---|---|
| 用途 | 常用于局部变量、常量 | 可用于对象属性、方法参数、集合、泛型 |
| 存储方式 | 局部变量 → JVM 栈 成员变量(非 static) → 堆 | 所有对象 → 堆(JIT 逃逸分析优化可能栈上分配) |
| 占用空间 | 小,紧凑 | 大,包含对象头 + 数据 + 对齐填充 |
| 默认值 | 成员变量有默认值,如 int: 0, boolean: false | 成员变量默认值为 null |
| 比较方式 | == 比较值 | == 比较对象地址,比较值需用 equals() |
| 泛型支持 | 不支持 | 支持(如 List<Integer>) |
| 装箱/拆箱 | 无 | 自动装箱/拆箱 (int ↔ Integer) |
| 特点 | 存储简单,空间小 速度快 不能用于泛型 | 是对象,可以存放在集合类中(支持泛型) 可以调用方法(如 Integer.parseInt()) 默认值为 null(成员变量) |
性能敏感场景尽量用基本类型,基本类型内存占用少、访问快、没有自动拆箱/装箱的额外开销。
包装类型的缓存机制?
Java 为了提升性能,引入了 包装类缓存机制 。常用的小数值会在内存中 提前创建好对象, 下次再用同样的数值时 直接复用对象,而不是每次都 new。
优点是减少对象创建,提高性能,节省内存。
Integer / Byte / Short / Long / Character / Boolean 会缓存,浮点类型不会缓存。
整数类型缓存区间 [-128,127],Character 只缓存 ASCII 范围字符,Boolean 只有两个对象 TRUE 和 FALSE,没有范围概念。
自动装箱与拆箱?原理是什么?
装箱(Boxing):将基本类型用它们对应的引用类型包装起来;
拆箱(Unboxing):将包装类型转换为基本数据类型;

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
java
Integer i = 10; // 自动装箱,等价于 Integer.valueOf(10)
int n = i; // 自动拆箱,等价于 i.intValue()
- 自动拆装箱可以使代码更简洁,但频繁使用可能影响性能,尤其在循环中对大量数据进行计算时。
- 建议在性能敏感场景下使用基本类型,避免不必要的装箱操作。
浮点数运算时的精度丢失风险
**计算机底层使用二进制存储数据,而很多十进制小数无法被二进制精确表示。**例如十进制的0.2,转换为二进制后是一个无限循环小数,只能截断保存,后面的位数直接舍弃,因此实际保存的是一个近似值,而不是精确值。
在需要高精度计算(如金额计算)时,通常使用 BigDecimal 来避免精度丢失问题。
如何解决浮点数运算的精度丢失问题?
使用 BigDecimal 进行高精度运算,避免 float 和 double 的精度误差(float / double 的问题是底层使用二进制浮点数表示,而 BigDecimal 本质上是用高精度的十进制方式进行计算)。
BigDecimal 比较值是否相等推荐使用 compareTo(),因为 equals() 不仅比较值,还会比较小数位数(scale),例如 0.2 和 0.20 使用 equals() 比较结果为 false。
java
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(c);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.2 */
System.out.println(y); /* 0.20 */
// 比较内容,不是比较值
System.out.println(Objects.equals(x, y)); /* false */
// 比较值相等用相等compareTo,相等返回0
System.out.println(0 == x.compareTo(y)); /* true */
联想到的问题:
- ==
- 对于基本类型,比较的是值是否相等
- 对于引用类型,比较的是对象地址是否相同
- equals()
- 用于比较对象的逻辑内容是否相等
- 默认实现比较地址
- 很多类(如String、自定义类)都重写了该方法
- 重写 equals() 时通常也要重写 hashCode()
- compareTo()
- 用于比较对象大小或顺序关系
- 返回负数表示小于,0 表示等于,正数表示大于
- 常用于排序和数值比较
==equals()compareTo()本质 运算符 方法 (定义在 Object) 接口方法 (Comparable<T>) 返回类型 boolean boolean int 核心语义 是否"同一个" 是否"逻辑相等" 谁大谁小 / 顺序关系 基本类型 比较值 × × 引用类型 比较地址 比较内容(通常) 比较大小 / 字典序 是否可重写 不可 可重写 通过实现接口定义 常见返回 true/false true/false -1/0/1 常见类 所有类型 String、包装类、自定义类 String、包装类、BigDecimal 是否用于排序 不适合 不适合 核心用途 是否受对象地址影响 是 通常否(重写后) 否 举例 String s1 = new String("abc"); String s2 = new String("abc"); s1 == s2 false s1.equals(s2) true s1.compareTo(s2) 0
**超过 long 整型的数据应该如何表示?**BigInteger
在 Java 中,long 是最大的基本整数类型,占 64 位,其取值范围为:-2^63 ~ 2^63 - 1
如果超过这个范围,就会发生整数溢出(overflow)。
如果需要表示超过 long 范围的整数,应该使用 BigInteger。其底层通过 int 数组(int[]) 存储数据,因此理论上只要内存足够,就可以表示无限大的整数。不过,相较于 long 这种基本类型,BigInteger 的运算效率较低,因为其运算需要进行数组级别的处理。
😺变量
成员变量 vs 局部变量
java
public class Person {
private String name; // 成员变量
public void test() {
int age = 18; // 局部变量
}
}
成员变量属于对象(或类),局部变量属于方法。
|------------|-------------------------------------------|-------------------------------------------|
| | 成员变量 | 局部变量 |
| 属于 | 对象(或类) | 方法 |
| 定义在 | 类中、方法外、代码块外,它属于类结构的一部分。 | 方法内部、构造方法内部、代码块内部、方法参数。 |
| 修饰符区别 | public、private、protected、static、final | final 局部变量本身只能在方法内部访问,不需要访问权限修饰。 |
| 内存中的区别 | 跟对象走,对象存储在堆内存(Heap) | 局部变量一般存栈内存(Stack) 方法调用时入栈,方法结束时出栈。(快) |
| 生命周期 | 对象创建开始,到对象销毁结束 | 方法调用开始,到方法结束 |
| 默认值区别 | Java 自动给默认值,保证对象状态的安全性和可预测性,避免出现未初始化的垃圾值。 | 没有默认值,因为局部变量必须先赋值后使用。 |
static 静态变量有什么作用?
静态变量属于类本身,不属于某一个具体对象。
java
public class User {
public static int count = 0;
}
这里的 count 不是某个对象独有的,而是整个 User 类共享一份。user1、user2、user3 这三个对象共用同一个 count。
主要作用:
-
类的所有实例共享
无论创建多少个对象,所有对象共享同一份静态变量。
修改后所有对象访问到的值都会同步变化。 -
节省内存
静态变量只会在内存中分配一次。
相比普通成员变量每个对象都保存一份,能够减少内存占用。 -
表示类级别属性
适用于描述整个类共有的信息,例如计数器、常量、配置项等。 -
方便通过类名直接访问
通常使用
类名.变量名的方式访问,例如User.count。 -
常与 final 一起使用
public static final 常用于定义全局常量,例如:
javapublic static final double PI = 3.1415926;
字符常量char和字符串常量String的区别?
- 形式不同。字符常量使用单引号表示,例如:'A'。字符串常量使用双引号表示,例如:"Hello"。
- 表示内容不同。字符常量只能表示一个字符。字符串常量可以表示 0 个或多个字符。
- 数据类型不同。字符常量对应 char 类型,属于基本数据类型。字符串常量对应 String 类型,属于引用数据类型。
- 存储内容不同。char 本质上存储的是字符对应的 Unicode 编码值('A' → 65、'a' → 97),因此可以参与表达式运算。String 存储的是字符串对象的引用(地址)。
- 内存占用不同。char 在 Java 中固定占 2 个字节。String 的内存占用与字符数量和编码方式有关。
😺方法
为什么静态方法不能调用非静态成员?
静态方法属于类,非静态成员属于对象;类存在时对象可能还没创建,所以静态方法不能直接访问非静态成员。
静态方法和实例方法有何不同?
- 所属关系不同:静态方法属于类,实例方法属于对象。
- 调用方式不同 :静态方法:
类名.方法名(),不需要创建对象。实例方法:对象.方法名(),必须通过对象调用。 - 访问权限和限制不同 :实例方法可以访问类的 所有成员 (静态成员和实例成员)。静态方法只能访问 静态成员,不能直接访问实例成员,因为静态方法在类加载时就存在,还没有对象。
- 应用场景:静态方法通常用于工具方法、计数器或全局访问,不依赖对象状态。实例方法用于操作对象状态或依赖对象属性。
重载和重写有什么区别?
- 重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
- 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法。
方法的重写要遵循"两同两小一大":
"两同"即方法名相同、形参列表相同;
"两小"指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
"一大"指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
| 特性 | 重载 (Overloading) | 重写 (Overriding) |
|---|---|---|
| 发生范围 | 同一个类或父子类中 | 父类与子类之间(必须继承关系) |
| 参数列表 | 必须不同 (类型、个数或顺序至少一项不同) | 必须相同 |
| 返回类型 | 与返回类型无关,可任意修改 | 基本类型/void 不可修改,引用类型可为子类 |
| 访问权限 | 可任意修改 | 子类不可低于父类权限 (public > protected > default > private) |
| 异常 | 可任意修改 | 子类异常范围 ≤ 父类 |
| 绑定时期 | 编译期(静态绑定) | 运行期(动态绑定) |
| 构造方法 | 可以重载 | 不能重写 |
| static 方法 | 可以重载 | 不能重写(可以重新声明) |
private/final/static 方法不能被子类真正重写,static 和 private 允许同名隐藏,final 不允许任何形式改写。(private:子类看不到父类方法,只能定义同名新方法。final:不允许子类改写。static:不能被真正意义上重写,静态方法属于类,而不是对象,重写是针对对象的多态行为,但是子类可以声明一个同名 static 方法)
什么是可变长参数?
从 Java 5 开始,允许方法在调用时传入 不定长度的参数,提高方法的灵活性。
java
public static void method(String... args) {
for(String s : args) {
System.out.println(s);
}
}
// 调用时可以传入:
method(); // 0 个参数
method("a"); // 1 个参数
method("a", "b", "c"); // 多个参数
可变参数必须 位于方法参数的最后一个,前面可以有固定参数。
java
public static void method(String fixed, int... numbers) {
// fixed 是固定参数
// numbers 是可变长参数
}
如果一个方法有重载版本,编译器 优先匹配固定参数 的方法,只有在固定参数不匹配时才使用可变参数方法。
编译器底层原理:Java 编译器会将可变参数 转换成数组。
😺面向对象基础
面向对象 vs 面向过程
面向过程(POP)和面向对象(OOP)是两种不同的编程思想。
- 核心思想不同
面向过程关注步骤,也就是"怎么做",把问题拆成一个个步骤,通过函数按顺序执行来解决问题。
面向对象关注对象职责,也就是"谁来做",先抽象出对象,再由对象通过调用方法完成具体功能。
- 代码组织方式不同
面向过程以 函数(C) / 方法(Java) 为核心组织代码。
面向对象以 类和对象 为核心组织代码。
- 适用场景不同
面向过程适合逻辑简单、流程明确的小型程序。
面向对象适合复杂系统和大型项目开发。
- 优缺点
面向对象具有 封装性、继承性和多态性,代码更易维护、复用和扩展。
面向过程实现简单直接,但随着业务复杂度提升,维护成本较高。
联想到的问题:
POP 和 OOP 和编程语言有关吗?
有关系,但本质上 POP(面向过程)和 OOP(面向对象)首先是一种编程思想或编程范式,并不专属于某一种编程语言。只是不同编程语言对不同范式的支持程度不同,比如 C 语言更偏向面向过程,Java 天然支持面向对象,Python、C++ 同时支持多种编程范式
创建一个对象用什么运算符? 对象实例与对象引用有何不同?
在 Java 中,通常使用 new 运算符 来创建对象。
java
Student s = new Student();
- new Student() :表示创建一个对象实例 ,对象实例存储在堆内存中。
- Student s :表示一个 对象引用 ,引用变量通常存储在栈内存 中,用于保存对象的地址。
一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
对象实例 :是实际创建出来的对象、存储对象的属性和方法数据、位于堆内存中。
**对象引用:**本质上是一个变量、用于保存对象在堆中的地址、通过引用访问对象。
对象相等 vs 引用相等
对象的相等(equals )一般比较的是内存中存放的内容是否相等。引用相等(==)一般比较的是他们指向的内存地址是否相等。
java
String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
// 使用 == 比较字符串的引用相等
System.out.println(str1 == str2);
System.out.println(str1 == str3);
// 使用 equals 方法比较字符串的相等
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));
// false
// true
// true
// true
String str1 = "hello"; 字符串常量会优先放到字符串常量池(String Pool)。
String str2 = new String("hello"); 只要用了 new 一定在堆 里重新创建新对象。
String str3 = "hello"; 这里没有 new 所以 JVM 会去字符串常量池里找,发现已经有 "hello" 了,直接复用。
== 运算符比较的是字符串的引用是否相等。equals 方法比较的是字符串的内容,即使这些字符串的对象引用不同,只要它们的内容相等,就认为它们是相等的。
联想到的问题:
1. 为什么重写 equals 必须重写 hashCode?
对于 String、包装类以及业务对象,一般使用 equals() 判断逻辑相等。
equals():比较对象内容,前提是重写。
那为什么重写 equals 必须重写 hashCode?
因为必须保证:两个相等的对象,hashCode 一定相等。 即必须满足 a.equals(b) == true,那么必须 a.hashCode() == b.hashCode()。反过来不一定,因为可能哈希冲突。
2. 为什么 HashMap 为什么先比较 hashCode 再比较 equals?
因为 hashCode 用于快速定位 key 在数组中的存储位置(桶位置),通过哈希值可以快速缩小查找范围,提高查询效率。
equals 用于最终确认,不同对象可能产生相同的 hashCode,即哈希冲突,因此在定位到桶之后,需要使用 equals() 进一步判断是否为同一个 key()。
3. equals() 方法定义在哪里?
equals() 方法最初定义在 Object 类中,因此所有 Java 类默认都继承该方法。Object 类中 equals() 的默认实现本质上是比较两个对象的引用地址是否相同"=="。
那为什么 String 能比较内容?
像 String、Integer 等常用类都重写了 equals() 方法,使其比较对象内容而不是地址。
如果一个类没有声明构造方法,该程序能正确执行吗?
构造方法(Constructor)是一种特殊的方法,用于初始化对象 。如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法,Java 编译器会自动添加一个默认的无参构造方法(也叫默认构造器)。我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号,因为要调用无参的构造方法。
构造方法特点?是否可被 override?
特点:
-
名称与类名相同:构造方法名称必须与类名一致。
-
没有返回值:构造方法不能声明返回类型,包括 void。
-
自动执行:对象创建时自动调用,无需显式调用。
-
不能被重写(Override):子类不能重写父类的构造方法。
-
可以被重载(Overload):同一类中可以定义多个构造方法,只要参数列表不同。
注意事项:
-
构造方法虽然不能被 override,但子类可以通过 super() 调用父类构造方法实现父类属性初始化。
-
构造方法重载可以提供多种初始化方式,增强对象创建的灵活性。
示例:
java
class Parent {
Parent() {
System.out.println("Parent constructor");
}
}
class Child extends Parent {
Child() {
super(); // 调用父类构造方法
System.out.println("Child constructor");
}
}
public class Test {
public static void main(String[] args) {
Child c = new Child();
}
}
// Parent constructor
// Child constructor
面向对象三大特征
封装(Encapsulation)
- 将对象的属性隐藏在内部,不允许外部直接访问,通过方法操作属性。
- 属性通常使用 private,提供 public 的 getter/setter 方法。
- 提高数据安全性和可维护性。
继承(Inheritance)
- 子类可以继承父类的属性和方法,扩展自己的属性和方法。
- 子类可以重写父类方法,实现不同的行为。
- 提高代码复用性和开发效率。
多态(Polymorphism)
- 父类引用指向子类对象,调用方法时根据实际对象类型决定执行哪个类的方法。
- 优点:提高程序的灵活性和可扩展性。
- 限制:只能调用父类存在的方法,子类独有的方法无法直接调用。
示例:
java
class Animal {
public void sound() { System.out.println("Animal makes sound"); }
}
class Dog extends Animal {
@Override
public void sound() { System.out.println("Dog barks"); }
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); // 父类引用指向子类对象
a.sound(); // 输出 "Dog barks"
}
}
接口和抽象类的共同点和区别
共同点:
- 都不能直接实例化,必须通过子类实现或继承后创建对象。
- 都可以包含抽象方法,要求子类必须实现。
区别:
设计目的不同
- 接口主要用于定义行为规范,强调"能做什么"。
- 抽象类主要用于代码复用和父子类继承关系,强调"是什么"。
继承方式不同
- 类只能继承一个抽象类(单继承)。
- 类可以实现多个接口(多实现)。
成员变量不同
- 接口中的变量默认是 public static final 常量。
- 抽象类中的成员变量可以使用任意访问修饰符。
使用场景:
- 行为规范使用接口。
- 公共属性和代码复用使用抽象类。
深拷贝 vs 浅拷贝?引用拷贝?
引用拷贝:同一个对象 ,两个引用变量指向它。
浅拷贝:新对象,但内部引用对象共享 。
深拷贝:新对象,内部引用对象也全部复制。

Object.clone() 默认是浅拷贝:默认执行按字段复制,对于引用类型字段只复制地址。
深拷贝实现:
-
clone递归复制(最常见):
javaperson.setAddress( person.getAddress().clone() ); -
通过先序列化再反序列化,天然深拷贝。
😺Object
| 方法名 | 作用 | 是否可重写 |
|---|---|---|
| getClass() | 获取运行时类对象 | ❌ 不可 |
| hashCode() | 返回对象哈希值 | ✅ 可 |
| equals(Object obj) | 比较对象是否相等 | ✅ 可 |
| toString() | 返回对象字符串表示 | ✅ 可 |
| clone() | 对象拷贝,默认浅拷贝 | ✅ 可 |
| wait() | 当前线程等待并释放锁 | ❌ 不可 |
| notify() | 唤醒一个等待线程 | ❌ 不可 |
hashCode() 有什么用?
hashCode() 的作用是返回对象的哈希码(一个 int 整数),主要用于确定对象在哈希表中的存储位置。在 Java 中,hashCode() 定义在 Object 类中,因此所有类都默认拥有该方法。
它的主要应用场景包括:HashMap、HashSet、Hashtable。
这些哈希结构在存储和查找元素时,会先调用 hashCode() 计算哈希值,再确定对象应该存储在哪个桶(bucket)中,从而实现快速查找,平均时间复杂度通常为 O(1)。
重要规则:
- 如果两个对象通过 equals() 判断相等,那么它们的 hashCode() 必须相同。
- 如果两个对象 hashCode() 相同,不代表它们一定相等,可能只是发生了哈希冲突。
因此,在实际开发中重写 equals() 时必须同时重写 hashCode(),否则在哈希容器中可能导致查找异常或去重失败。
为什么要有 hashCode?
hashCode() 的主要作用是提高哈希容器中元素查找和去重的效率。
以 HashSet 为例,在插入元素时先调用对象的 hashCode() 方法计算哈希值,根据哈希值确定对象应该存储到哪个桶(bucket)
- 如果桶为空,直接插入。
- 如果桶中已有元素,再调用 equals() 进行精确比较。
这样做的好处是先通过 hashCode 缩小查找范围,再通过 equals 精确判断,从而大幅减少 equals() 的调用次数,提高查找效率。
- 如果只使用 equals(),每次都需要遍历整个集合,效率较低。
- 如果只使用 hashCode(),又可能因为哈希冲突导致误判。
😺String
| 类型 | 可变性 | 存储结构 | 继承关系 | 使用场景 |
|---|---|---|---|---|
| String | 不可变 | 字符数组 char[],使用 final 修饰 | 继承自 Object | 单线程、少量字符串操作 |
| StringBuffer | 可变 | 继承AbstractStringBuilder,内部用char[]保存字符串,可变 | 线程安全(同步锁) | 单线程、大量字符串操作 |
| StringBuilder | 可变 | 继承AbstractStringBuilder,内部用char[]保存字符串,可变 | 非线程安全 | 多线程、大量字符串操作 |
java
String str = "Hello";
str += " World"; // 生成新的 String 对象
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 原对象修改
StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World"); // 原对象修改,线程安全
String 不可变原因
- 内部数组私有且不可访问,并且没有提供修改方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
- 所有修改操作都会返回新对象,保证原对象不变。
Java 字符串拼接:"+" vs StringBuilder
"+" 的机制:编译器将 "a" + "b" 转换为 new StringBuilder().append(a).append(b).toString()。循环中使用 "+" 的问题是每次循环都会创建新的 StringBuilder 对象和 String 对象。临时对象过多,性能低下。
推荐使用 StringBuilder:循环内只创建一个 StringBuilder 对象,通过 append() 修改原对象,最后 toString() 得到结果,性能最佳。
使用建议:
- 单次拼接:使用 +。
- 循环或大量拼接:使用 StringBuilder(单线程)或 StringBuffer(多线程)。
java
StringBuilder sb = new StringBuilder();
for (String str : arr) {
sb.append(str);
}
String result = sb.toString();
Java 字符串常量池(String Pool)
定义:JVM 为提升性能和节省内存,为 String 类专门开辟的一块内存区域。
作用:避免重复创建字符串对象。
特点:
- 只针对字符串字面量。
- JVM 会先在常量池中查询是否存在相同字符串。
- 已存在 → 复用引用;不存在 → 创建并加入常量池。
与 new String() 的区别:
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
- 字面量 → 常量池
- new → 堆内新对象,不复用常量池
String s1 = new String("abc"); 会创建几个字符串对象?
答:1 或 2 个对象。
- 常量池中不存在 "abc" → 创建 2 个对象(常量池 + 堆)。
- 常量池中已存在 "abc" → 创建 1 个对象(堆)。
原理分析:
- new 总是在堆中创建新对象。
- 字面量 "abc" 是否在常量池存在,决定常量池对象是否创建。
- 使用常量池里的源对象初始化新对象,保证堆对象内容与源一致。
示例代码:
java
String s1 = new String("abc"); // 堆中总会创建新对象
String s2 = "abc"; // 常量池对象(已存在则复用)
String#intern()
String.intern() 是一个 native(本地方法),用于处理字符串常量池 中的字符串对象引用 。调用 intern() 可以将这些字符串统一映射到常量池中的唯一引用,从而减少堆里的大量重复对象、节省内存。
- 常量池中已有相同内容的字符串对象:intern() 返回常量池中已有对象的引用。
- 常量池中没有相同内容的字符串对象:intern() 将当前字符串对象的引用添加到常量池,并返回该引用。
java
// s1 指向字符串常量池中的 "Java"
String s1 = "Java";
// s2 指向常量池中的同一个对象
String s2 = s1.intern();
// 堆中创建新的 "Java" 对象
String s3 = new String("Java");
// s4 指向常量池中的对象(和 s1 相同)
String s4 = s3.intern();
// 输出验证
System.out.println(s1 == s2); // true
System.out.println(s3 == s4); // false
System.out.println(s1 == s4); // true
String 类型的变量和常量做"+"运算时发生了什么?
-
常量 + 常量 → 编译期优化,直接放进字符串常量池,这个过程叫:常量折叠。
javaString a = "ab" + "cd"; // 编译器直接优化成:String a = "abcd"; String b = "abcd"; System.out.println(a == b); // true -
变量 + 变量 → 运行期拼接,不能在编译期确定,JVM 运行时处理。
javaString s1 = "ab"; String s2 = "cd"; String s3 = s1 + s2; // 底层实际上是:new StringBuilder().append(s1).append(s2).toString(); String s4 = "abcd"; System.out.println(s3 == s4); // false // s3 在堆中是新对象,s4 在常量池 -
final 修饰变量,如果变量被 final 修饰,并且值在编译期可确定,编译器会将其当作常量处理,同样进行常量折叠。
javafinal String str1 = "str"; final String str2 = "ing"; String c = "string"; String d = str1 + str2; System.out.println(c == d); // true
😺异常

Exception 和 Error 有什么区别?
Java 所有异常和错误的根节点都是 java.lang.Throwable,Throwable 类有两个重要的子类:
java
Throwable
├── Exception
└── Error
- Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,编译期间必须处理,否则无法通过编译) 和 Unchecked Exception (不受检查异常,也叫运行时异常,编译器不强制处理)。
- Error:属于程序无法处理的错误 ,不建议通过catch捕获,因为即使捕获后程序通常也无法继续正常运行。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
ClassNotFoundException 和 NoClassDefFoundError 的区别
ClassNotFoundException 是Exception,发生在使用反射等动态加载时找不到类,当 JVM 根据类名主动加载类时,如果找不到对应类,就会抛出该异常,是可预期的,可以 try-catch 捕获处理。
NoClassDefFoundError 是Error,是编译时存在的类,在运行时链接不到了(比如 jar 包缺失),它通常属于运行环境问题,导致 JVM 无法继续。
联想到的问题:
- 什么叫"加载类"?
把 .class 文件读进 JVM 内存,变成可以运行的类。
- 什么叫动态加载?
程序运行的时候,才决定加载哪个类,比如类名是字符串,程序无法在编译时就知道,程序运行时才知道要加载哪个。
- 什么是反射?
反射是指程序在运行时获取类的信息,并动态操作类,比如:获取类名、获取属性、获取方法、创建对象、调用方法。
正常开发是:对象 → 调方法。
而反射是:类信息 → 创建对象 → 调方法。
有点像"反过来操作类",所以叫反射。
- 为什么很多框架都用反射?
比如 Spring Boot
java@Service public class UserService {}Spring 怎么知道你的类?怎么帮你创建对象?反射 + 动态加载。
Eg: IOC容器 -- @Service
1)获取 Class 对象。Spring 启动时扫描类路径(class文件),找到@Service。
2)通过反射动态创建对象。Class.forName(...)、newInstance() 创建 Bean。
3)放进 IoC 容器。
Eg: 依赖注入 DI -- @Service
1)获取 Class 对象。Spring 启动时扫描 @Autowired。
2)通过反射找到这个字段 Field。
3)再执行 field.set(...) 完成注入。
Eg: AOP(事务、日志)--@Transactional
1)Spring 启动时扫描 @Transactional。
2)通过动态代理 + 反射生成代理对象。
3)在方法调用前后插入:开启事务、提交事务、回滚事务。
Eg: Spring MVC 请求分发
1)接受请求,Spring 通过反射找到对应 Controller 方法。
2)然后执行 method.invoke() 调用你的接口方法。
Checked Exception 和 Unchecked Exception 有什么区别?
Java 中异常分为 受检异常(Checked Exception) 和 非受检异常(Unchecked Exception)。
- Checked Exception(受检异常):如果代码中可能出现此类异常,必须使用 try-catch 捕获,或者使用 throws 向上抛出,否则编译无法通过。除了 RuntimeException 及其子类之外都属于受检异常。
常见的受检异常有:IOException 输入输出异常、SQLException 数据库异常、ClassNotFoundException 类未找到异常、FileNotFoundException 文件未找到异常...
- Unchecked Exception(非受检异常):即使不使用 try-catch 或 throws,代码依然可以正常编译。它们都继承自 RuntimeException。
常见的有:NullPointerException 空指针异常、ArrayIndexOutOfBoundsException 数组越界、NumberFormatException 字符串转数字失败、ClassCastException 类型转换错误、ArithmeticException 算术异常、IllegalArgumentException非法参数异常、IllegalStateException非法状态异常...
- 核心区别
- Checked Exception:编译阶段就必须处理,否则代码直接报错,连运行机会都没有。
- Unchecked Exception:编译器不强制处理,编译可以通过,运行时异常。
- 本质上 RuntimeException 及其子类都属于非受检异常,其余为受检异常。
你更倾向于使用 Checked Exception 还是 Unchecked Exception?
默认优先使用 Unchecked Exception(RuntimeException) ,只有在确实需要调用方显式处理时才使用 Checked Exception。
我们可以把 Unchecked Exception(比如 NullPointerException)看作是代码 Bug,对待 Bug 最好的方式是让它暴露出来然后去修复代码,而不是用 try-catch 去掩盖它。
只在一种情况下使用 Checked Exception:当这个异常是业务逻辑的一部分,并且调用方必须处理它时。比如说,一个余额不足异常。这不是 bug,而是一个正常的业务分支,我需要用 Checked Exception 来强制调用者去处理这种情况,比如提示用户去充值。再比如:库存不足、用户余额不足、权限审批失败、文件不存在。这样就能在保证关键业务逻辑完整性的同时,让代码尽可能保持简洁。
Throwable 类常用方法有哪些?
java.lang.Throwable 是 Java 异常体系的根类,常用方法如下:
- getMessage():返回异常发生时的详细错误信息。
- toString():返回异常类名和异常信息的简要描述。
- printStackTrace():打印完整异常堆栈信息,包括异常类型、错误信息和代码调用链,便于问题定位。
throw 和 throws 的区别
throw 和 throws 都与异常处理有关,但作用不同。
1. throw 主动抛出一个异常对象
java
throw new RuntimeException("异常信息");
方法内部,后面跟对象,通常用于方法内部主动制造异常,一次只能抛一个对象。
2. throws 在方法声明处声明可能抛出的异常类型
java
public void test() throws IOException, SQLException
方法声明处,后面跟异常类名,表示该方法可能抛出此异常,交由调用者处理,可以声明多个。
try-catch-finally 如何使用?
- try块:用于包裹可能发生异常的代码。如果 try 中发生异常,程序会立即跳转到匹配的 catch 块。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch块:用于捕获并处理 try 中抛出的异常,可以有多个 catch 块用于处理不同类型的异常。
- finally 块:用于执行收尾操作,一般用于释放资源。比如:关闭文件流、关闭数据库连接、释放锁。无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
**不要在 finally 中使用 return!**因为它会覆盖 try 或 catch 中原本的返回值,甚至吞掉异常信息。
finally 一定会执行吗?
**通常会执行,但不是绝对一定。**因为 finally 不是绝对物理规则,依赖JVM 正常运行。
以下情况 finally 不执行:
- System.exit() 这里直接终止整个 JVM 进程。
- 线程被强制终止。
- JVM 崩溃 / 机器断电 / CPU 关闭。
- 死循环 / 卡死。
如何使用 try-with-resources 代替 try-catch-finally?
什么是 try-with-resources?try-with-resources 是 Java 7 引入的一种资源管理机制,用于自动关闭资源,推荐优先替代传统的 try-catch-finally。把资源声明写在 try() 中,JVM 自动帮你关闭,不需要 finally 。
1. 基本写法
java
try(Scanner scanner = new Scanner(new File("test.txt"))){
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch(
FileNotFoundException e){
e.printStackTrace();
}
资源对象声明在 try() 中,执行结束后 JVM 会自动调用 close() 方法。
2. 多资源写法
java
try(
BufferedInputStream bin = new BufferedInputStream(...);
BufferedOutputStream bout = new BufferedOutputStream(...)
){
...
} catch(IOException e){
e.printStackTrace();
}
多个资源使用分号分隔,关闭顺序遵循:后创建先关闭。
相比 try-finally,优点如下:代码更简洁、自动关闭资源、不容易遗漏 close、异常信息更完整。
因此在实际开发中,优先推荐使用 try-with-resources。
平时异常你怎么用?有哪些规范?
1. 不要将异常定义为静态变量
异常对象内部保存了堆栈信息,如果定义为静态变量并重复使用,会导致异常栈信息混乱,不利于问题定位。
应该每次抛出时重新创建:
java
throw new IllegalArgumentException("参数错误");
2. 异常信息要有意义
异常信息应尽可能明确,方便排查问题。
例如:
java
throw new IllegalStateException("库存不足");
优于:
java
throw new RuntimeException("error");
3. 尽量抛出更具体的异常
应优先使用语义更明确的异常类型。
例如数字格式错误时,应使用:
java
NumberFormatException
而不是直接使用其父类:
IllegalArgumentException
4. 避免重复记录日志
如果异常已经在统一异常处理层记录过日志,业务层不应重复打印相同日志,否则会导致日志膨胀并干扰问题定位。
5. 不要吞异常
避免写空的 catch 块:
catch (Exception e) {}
至少应记录日志或继续抛出异常。
6. 尽量避免直接 catch Exception
应尽量捕获具体异常类型,提升代码可维护性和问题定位效率。
😺什么是泛型?有什么作用?
泛型 = 类型参数,也就是把数据类型当作参数传进去。
在定义类、接口、方法时不写死类型,用占位符(T Type、E Element、K Key、V Value)表示,使用时再指定具体类型,eg:HashMap<K,V>。
作用:编译期类型检查 (ArrayList<String> 只能放String)、避免强制转换 (泛型可以自动推断List<String> 里面是 String 类型)、提高代码安全与可读性(避免非法类型传入)。
泛型 3 种使用方式:泛型类、泛型接口、泛型方法。
项目中哪里用到泛型?
1)通用返回结果类 Result<T>
java
public class Result<T> {
private boolean success;
private String msg;
private T data; // 泛型
}
T 可以是任何类型:User、Shop、Blog、List、Integer 等。
好处:
- 一套返回体,适配所有接口
- 前端收到什么数据一目了然
- 不用每个模块写一个 Result
2)MyBatis-Plus Service 泛型 MyBatis-Plus Service 泛型
java
public class UserServiceImpl
extends ServiceImpl<UserMapper, User>
implements IUserService
ServiceImpl 是 MyBatis-Plus 提供的通用 Service 实现类 ,内部已经封装好了所有增删改查逻辑, 并通过泛型注入对应的 Mapper 和实体类,我们的业务类继承它之后,就可以直接使用 CRUD 方法,不用重复实现。
这里两个泛型:UserMapper 表示 Mapper 类型、User 表示实体类类型。
3)Redis 缓存工具类 RedisTemplate
Redis 缓存工具类是对 RedisTemplate 的一层封装 ,使用泛型方法 实现任意类型数据的存取get/set ,简化缓存操作、统一规范、减少重复代码,在黑马点评中用于店铺、用户、优惠券、订单等信息的缓存。
4)各种通用集合 List<T>、Map<K,V>
java
List<Shop> shopList = new ArrayList<>();
Map<String, User> userMap = new HashMap<>();
- 存放店铺、用户、评论、优惠券、订单等
- 保证集合里只能存一种类型,不乱、不报错
😺什么是反射?
反射 = 程序在运行时动态地查看和操作类的信息。
可以把它理解成 Java 在运行时"看自己",Reflection(反观自身)。
反射到底能干什么?
1)获取类信息
可以拿到:类名、方法、属性、构造器、注解、父类。
2)动态创建对象
等价于 new User(),但这是运行时动态创建。
java
Class<?> clazz = User.class;
Object obj = clazz.newInstance();
// 等价于 new User(),但这是运行时动态创建。
3)动态调用方法 invoke()
java
Method method = clazz.getMethod("getName");
Object result = method.invoke(obj);
// 相当于 obj.getName()
4)动态修改字段
java
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(obj, "Tom");
// 相当于 obj.name = "Tom"; 甚至private都能改,破坏封装性。
反射核心类:
- Class 类的字节码对象,eg:User.class。
- Constructor 构造器方法,用于new对象。
- Method 方法,用于 invoke()。
- Field 属性,用于 set()、get()。
**优点:**动态性强、框架开发基础、提高代码通用性。
**缺点:**性能开销较大、可能破坏封装性、错误在运行时暴露。
结合项目讲:
- Spring IoC 容器创建 Bean
- @Autowired 依赖注入
- @Transactional AOP 动态代理:@Transactional 本质上是 Spring 基于 AOP 生成代理对象,代理对象会在方法执行前后自动完成事务的开启、提交和回滚。
- MyBatis 查询结果映射实体类:能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。
😺代理
如何实现动态代理?
动态代理是指在不修改目标类源代码 的前提下,在运行时对方法进行功能增强。
- 打日志
- 权限检查
- 开启事务
- 性能统计
Java 中主流的动态代理实现方式有两种:
- JDK 动态代理:JDK 官方提供,要求目标类必须实现接口 。(默认)
其核心是通过 java.lang.reflect.Proxy 的 newProxyInstance() 方法,在运行时动态生成一个实现目标接口的代理对象。所有方法调用都会被转发到 java.lang.reflect.InvocationHandler 的 invoke() 方法中,在这里可以实现前置和后置增强逻辑。 - CGLIB 动态代理:CGLIB 是第三方字节码增强库,不要求目标类实现接口。(没有接口时)
它通过在运行时动态生成目标类的子类,并重写其中的方法,实现代理增强。所有方法调用会进入 MethodInterceptor 的 intercept() 方法。
核心区别在于:JDK 基于接口,CGLIB 基于继承
| JDK动态代理 | CGLIB动态代理 | |
|---|---|---|
| 是否需要接口 | 必须 | 不需要 |
| 实现方式 | 接口实现 | 继承子类 |
| 核心接口 | InvocationHandler | MethodInterceptor |
| 代理 final 方法 | 不行 | 不行 |
| Spring 默认 | 有接口优先 | 无接口使用 |
介绍一下动态代理在框架中的实际应用场景
动态代理最典型的实际应用场景就是 AOP。
AOP(Aspect-Oriented Programming,面向切面编程)主要用于将与业务逻辑无关但多个模块都会使用的公共逻辑进行统一管理,例如:事务管理、日志记录、权限控制、性能监控、异常处理。
-
有接口:如果目标对象实现了接口,Spring 默认使用 JDK 动态代理 创建代理对象。
-
无接口:如果目标对象没有实现接口,Spring 会使用 CGLIB 动态代理,通过生成目标类子类的方式实现代理。
-
实际应用:例如
@Transactional注解底层就是通过动态代理实现的。
在目标方法执行前开启事务,执行成功后提交事务,发生异常时进行事务回滚。
😺注解
注解就是给程序"打标签",本质是一个特殊接口,所有注解都默认继承:java.lang.annotation.Annotation。所以,注解本质是一个继承 Annotation 接口的特殊接口。
用于给类、方法、属性或参数添加元信息,这些信息可以在编译期或运行期被程序读取并处理。
注解解析方式
(1)编译期解析
编译器在编译阶段直接扫描和处理注解。
例如:@Override
编译器会检查该方法是否真正重写了父类方法。
(2)运行期反射解析
框架通常在运行时通过反射读取注解信息。
例如 Spring Framework 中:@Component、@Autowired、@Value、@Transactional
这些注解都是通过反射解析并生效的。
😺Java IO 流
输入流:数据从外部进入程序内存,文件、键盘、网络、数据库 -> 内存。
输出流:数据从程序内存输出到外部设备,内存 -> 文件、浏览器、网络、数据库。
Java IO 四大抽象基类是什么?
- 字节输入流 java.io.InputStream (读取字节 01010101)
- 字节输出流 java.io.OutputStream
- 字符输入流 java.io.Reader(读取字符a中文)
- 字符输出流 java.io.Writer
为什么分字节流和字符流?
虽然计算机底层最小存储单位是字节,但 Java 仍然区分字节流和字符流,主要有两个原因:
-
编码问题:字符流可以自动完成字节到字符的编码转换,避免乱码问题,尤其处理中文文本时更方便。
-
使用方便:处理文本文件时,字符流更符合开发习惯,如:txt、json、xml。
-
使用场景:字节流用于图片、视频、文件上传、网络传输。字符流用于文本文件、配置文件、日志输出。
BIO、NIO 和 AIO 的区别?
| BIO | NIO | AIO | |
|---|---|---|---|
| 全称 | Blocking I/O | Non-blocking I/O | Asynchronous I/O |
| I/O 类型 | 同步阻塞 | 同步非阻塞 / 多路复用 | 异步非阻塞 |
| 线程模型 | 每个连接一个线程 | 一个线程管理多个连接 | 事件回调机制,线程可做其他事 |
| 阻塞情况 | 阻塞 | 非阻塞(select/轮询) | 不阻塞 |
| 并发能力 | 低 | 高 | 极高 |
| 编程复杂度 | 简单 | 中等 | 高 |
| 工作模式 | 应用程序发起 read 调用后,线程 阻塞,直到数据从内核空间拷贝到用户空间 | 应用线程发起 I/O 调用时,不阻塞,可以管理 一个线程处理多个连接,内核告诉应用哪些连接可读/可写 | 应用线程发起 I/O 操作后立即返回,后台线程完成 I/O,操作系统触发回调通知应用 |
| 应用场景 | 小型服务、低并发 | 高并发服务器、Netty | 理论上适合高并发、大文件传输 |
😺什么是语法糖?
语法糖是编程语言为了提升开发效率和代码可读性而提供的简化语法。
写起来更简单,但不增加新的语言能力,本质逻辑没变。
比如:增强 for、lambda、泛型、自动拆装箱、可变参数等等。
真正支持语法糖的是 Java 编译器,不是 JVM,因为 JVM 只认识字节码 ,所以编译器会在编译阶段进行 解糖(desugar),将高级语法转换成 JVM 可识别的基础语法。
Java代码 -> 编译器解糖 -> 字节码 -> JVM执行