语法与数据类型
一、标识符与关键字
1.1 标识符
标识符是给 Java 中的类、方法、变量、常量等命名的字符序列。
命名规则(必须遵守)
| 规则 | 说明 | 示例 |
|---|---|---|
| 字符范围 | 字母、数字、下划线 _、美元符号 $ | userName, _count, $price |
| 数字限制 | 不能以数字开头 | 1name ❌ → name1 ✅ |
| 关键字限制 | 不能是 Java 关键字 | class ❌ → clazz ✅ |
| 大小写敏感 | 区分大小写 | Name 和 name 是不同的标识符 |
命名规范(建议遵守)
| 类型 | 规范 | 示例 |
|---|---|---|
| 类名/接口名 | 大驼峰(UpperCamelCase) | UserService, HttpServletRequest |
| 方法名/变量名 | 小驼峰(lowerCamelCase) | getUserName(), totalCount |
| 常量名 | 全大写+下划线 | MAX_VALUE, DEFAULT_TIMEOUT |
| 包名 | 全小写 | com.example.service |
// ✅ 正确示例
public class UserService { // 类名:大驼峰
private static final int MAX_COUNT = 100; // 常量:全大写+下划线
private String userName; // 变量:小驼峰
public void getUserName() { // 方法:小驼峰
int totalCount = 0; // 变量:小驼峰
}
}
// ❌ 错误示例
public class userservice { // 类名应为大驼峰
private static final int maxCount = 100; // 常量应为全大写
private String UserName; // 变量应为小驼峰
}1.2 关键字
关键字是 Java 语言保留的、具有特殊含义的单词,全部由小写字母组成。
关键字分类
访问控制修饰符
private protected public default(非显式关键字)类、方法、变量修饰符
abstract class extends final implements
interface native new static strictfp
synchronized transient volatile程序控制语句
break case continue default do
else for if return switch
while异常处理
try catch finally throw throws包相关
package import基本数据类型
byte short int long float double
char boolean变量引用
this super void保留字(未使用但保留)
goto const常见面试题
Q1: Java 有多少个关键字?
A: Java 共有 50 个关键字,另外还有 2 个保留字(goto、const)和 3 个特殊直接量(true、false、null)。
Q2: goto 和 const 为什么是保留字?
A: 这两个词在 Java 中没有实际用途,但为了避免将来可能的使用冲突,被保留为关键字。goto 源自其他语言的跳转语句,Java 设计者认为它会导致代码混乱所以未采用;const 则用 final 替代。
Q3: true、false、null 是关键字吗?
A: 不是。它们是布尔直接量和空直接量,属于字面量而非关键字,但同样不能用作标识符。
// ❌ 编译错误:不能用作标识符
int true = 1; // true 是布尔直接量
int null = 0; // null 是空直接量
int goto = 2; // goto 是保留字二、数据类型分类
Java 是一门强类型语言,每个变量都必须声明一种类型。Java 的数据类型分为两大类:基本数据类型和引用数据类型。
2.1 基本数据类型(Primitive Types)
Java 共有 8 种基本数据类型,直接存储数据值,存储在栈内存中。
分类一览表
| 分类 | 类型 | 字节数 | 位数 | 默认值 | 取值范围 |
|---|---|---|---|---|---|
| 整型 | byte | 1 | 8 | 0 | -128 ~ 127 |
short | 2 | 16 | 0 | -32768 ~ 32767 | |
int | 4 | 32 | 0 | -2³¹ ~ 2³¹-1 | |
long | 8 | 64 | 0L | -2⁶³ ~ 2⁶³-1 | |
| 浮点型 | float | 4 | 32 | 0.0f | ±3.4E38(6-7位有效数字) |
double | 8 | 64 | 0.0d | ±1.7E308(15位有效数字) | |
| 字符型 | char | 2 | 16 | '\u0000' | 0 ~ 65535(Unicode字符) |
| 布尔型 | boolean | 1(JVM规范未明确定义) | - | false | true / false |
整型详解
// byte:适用于节省内存的大数组
byte b = 127; // 最大值
byte b2 = -128; // 最小值
// short:较少使用
short s = 32767;
// int:最常用的整型(默认)
int i = 2147483647; // 21亿+
// long:大整数,需加 L/l 后缀
long l = 9223372036854775807L; // 注意 L 后缀浮点型详解
// float:单精度,需加 F/f 后缀
float f = 3.14f; // 必须加 f 或 F
// double:双精度(默认),精度更高
double d = 3.141592653589793;
double d2 = 3.14; // 默认就是 double
double d3 = 3.14d; // 可选 d 后缀
// 科学计数法
double scientific = 1.23e5; // 1.23 × 10⁵ = 123000.0字符型详解
// char:16位 Unicode 字符
char c1 = 'A'; // 字符
char c2 = 65; // Unicode 编码值(也是 'A')
char c3 = '\u0041'; // Unicode 转义(也是 'A')
char c4 = '中'; // 中文字符
// 转义字符
char newline = '\n'; // 换行
char tab = '\t'; // 制表符
char backslash = '\\'; // 反斜杠
char quote = '\''; // 单引号布尔型详解
// boolean:只有 true 和 false
boolean flag = true;
boolean isOk = false;
// 注意:Java 中 boolean 不能与整数互转
// ❌ 错误写法
// int x = (int) flag; // 编译错误
// if (1) { } // 编译错误(C/C++ 可以)2.2 引用数据类型(Reference Types)
引用类型存储的是对象的内存地址,实际对象存储在堆内存中。
分类
| 类型 | 说明 | 示例 |
|---|---|---|
| 类(Class) | 自定义类、系统类 | String, User, System |
| 接口(Interface) | 接口类型 | List, Runnable, Comparable |
| 数组(Array) | 同类型元素的集合 | int[], String[], Object[][] |
| 枚举(Enum) | 枚举类型 | enum Color { RED, GREEN, BLUE } |
| 注解(Annotation) | 注解类型 | @Override, @Deprecated |
// 类类型
String name = "Java";
User user = new User();
// 接口类型
List<String> list = new ArrayList<>();
Runnable task = () -> System.out.println("Hello");
// 数组类型
int[] arr = new int[10];
String[] names = {"Tom", "Jerry"};
// 枚举类型
enum Color { RED, GREEN, BLUE }
Color c = Color.RED;
// 注解类型
@Override
public String toString() {
return "User";
}2.3 基本类型 vs 引用类型
| 对比项 | 基本类型 | 引用类型 |
|---|---|---|
| 存储内容 | 实际数据值 | 对象的内存地址 |
| 存储位置 | 栈(Stack) | 引用在栈,对象在堆(Heap) |
| 默认值 | 有默认值(0, false, '\u0000') | 默认值为 null |
| 内存大小 | 固定(1-8字节) | 不固定 |
| 赋值方式 | 值拷贝 | 地址拷贝(引用拷贝) |
| 比较方式 | == 比较值 | == 比较地址,.equals() 比较内容 |
| 垃圾回收 | 不涉及 | 由 GC 管理 |
// 基本类型:值拷贝
int a = 10;
int b = a; // b 得到 a 的值的副本
a = 20; // 修改 a 不影响 b
System.out.println(b); // 输出 10
// 引用类型:地址拷贝
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // arr2 指向同一个数组
arr1[0] = 100; // 修改 arr1 会影响 arr2
System.out.println(arr2[0]); // 输出 100
// 引用类型比较
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)2.4 常见面试题
Q1: 为什么 Java 保留了基本数据类型?
A:
- 性能:基本类型直接存储在栈中,访问速度快,无需对象创建和垃圾回收开销
- 内存效率:基本类型占用内存小,适合大量数值计算
- 简化编程:对于简单数值,无需创建对象
Q2: int 和 Integer 有什么区别?
A:
| 对比项 | int | Integer |
|---|---|---|
| 类型 | 基本类型 | 引用类型(包装类) |
| 默认值 | 0 | null |
| 存储 | 栈 | 堆 |
| 泛型 | 不支持 | 支持 |
| 比较 | == 比较值 | == 比较地址,需用 equals() |
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(缓存)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(超出缓存范围)
System.out.println(c.equals(d)); // trueQ3: float f = 3.4 对吗?
A: 不对。3.4 默认是 double 类型,需要强制转换或加后缀:
// ❌ 编译错误
float f = 3.4;
// ✅ 正确写法
float f1 = 3.4f; // 加 f 后缀
float f2 = (float) 3.4; // 强制转换Q4: char 能存储中文吗?
A: 能。Java 的 char 是 16 位 Unicode 字符,可以存储一个中文字符。
char c = '中'; // ✅ 正确
String s = "中国"; // 多个中文用 StringQ5: boolean 占用多少字节?
A: JVM 规范未明确定义。实际实现中:
- 在数组中:1 字节
- 在局部变量中:通常 1 字节(但不保证)
- 单个 boolean 变量可能使用 4 字节(int 大小)以对齐内存
三、自动拆箱/装箱
3.1 什么是装箱与拆箱?
Java 是面向对象语言,但基本数据类型不是对象。为了让基本类型也能像对象一样使用(如放入集合、支持泛型),Java 为每种基本类型提供了对应的包装类(Wrapper Class)。
| 基本类型 | 包装类 |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
// 手动装箱(JDK 5 之前)
Integer num = Integer.valueOf(10);
// 手动拆箱(JDK 5 之前)
int value = num.intValue();3.2 自动装箱(Autoboxing)
自动装箱:基本类型自动转换为对应的包装类对象。
// 自动装箱:int → Integer
Integer a = 10; // 编译器自动转换为 Integer.valueOf(10)
// 等价于
Integer b = Integer.valueOf(10);
// 常见场景
List<Integer> list = new ArrayList<>();
list.add(100); // 自动装箱:int → Integer
Map<String, Integer> map = new HashMap<>();
map.put("age", 25); // 自动装箱3.3 自动拆箱(Unboxing)
自动拆箱:包装类对象自动转换为对应的基本类型。
// 自动拆箱:Integer → int
Integer a = 10;
int b = a; // 编译器自动转换为 a.intValue()
// 等价于
int c = a.intValue();
// 常见场景
Integer num = 100;
int result = num + 50; // 自动拆箱后运算
List<Integer> list = new ArrayList<>();
list.add(100);
int value = list.get(0); // 自动拆箱:Integer → int3.4 装箱与拆箱的本质
自动装箱和拆箱是编译器语法糖,在编译时自动插入转换代码。
// 源代码
Integer a = 10;
int b = a;
// 编译后的字节码(反编译)
Integer a = Integer.valueOf(10);
int b = a.intValue();3.5 Integer 缓存机制(重要!)
Integer 内部维护了一个缓存池,缓存了 -128 到 127 范围内的 Integer 对象。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}缓存范围测试
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(缓存池同一对象)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(超出缓存,新对象)
System.out.println(c.equals(d)); // true(内容相同)
// 其他包装类也有缓存
Long l1 = 127L;
Long l2 = 127L;
System.out.println(l1 == l2); // true
Long l3 = 128L;
Long l4 = 128L;
System.out.println(l3 == l4); // false缓存范围总结
| 包装类 | 缓存范围 |
|---|---|
Byte | -128 ~ 127(全部) |
Short | -128 ~ 127 |
Integer | -128 ~ 127 |
Long | -128 ~ 127 |
Character | 0 ~ 127 |
Boolean | TRUE / FALSE(两个常量) |
Float | 无缓存 |
Double | 无缓存 |
3.6 常见陷阱
陷阱1:NPE 风险
Integer num = null;
int value = num; // 运行时抛出 NullPointerException!
// 原因:自动拆箱调用 num.intValue(),而 num 是 null
// 安全写法
if (num != null) {
int value = num;
}陷阱2:使用 == 比较包装类
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(缓存)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超出缓存)
// 正确做法:使用 equals() 比较值
System.out.println(c.equals(d)); // true陷阱3:算术运算中的拆箱
Integer a = null;
Integer b = 10;
int sum = a + b; // NPE!a 自动拆箱时调用 a.intValue()
// 安全写法
Integer sum = (a != null ? a : 0) + b;陷阱4:三元运算符类型不一致
Integer a = null;
Integer b = true ? a : 0; // NPE!
// 原因:三元运算符要求两边类型一致,0 会自动装箱
// 而 a 为 null,拆箱时 NPE
// 安全写法
Integer b = true ? a : Integer.valueOf(0);3.7 性能考量
// ❌ 低效:循环中频繁装箱
Long sum = 0L;
for (int i = 0; i < 10000; i++) {
sum += i; // 每次循环都创建新的 Long 对象
}
// ✅ 高效:使用基本类型
long sum = 0L;
for (int i = 0; i < 10000; i++) {
sum += i; // 无对象创建开销
}3.8 常见面试题
Q1: 下面代码输出什么?
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);A: 输出 true 和 false。原因是 Integer 缓存机制,-128~127 范围内返回同一对象。
Q2: new Integer(100) == Integer.valueOf(100) 结果是?
A: false。new Integer() 始终创建新对象,valueOf() 在缓存范围内返回缓存对象。
Integer a = new Integer(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // falseQ3: 如何修改 Integer 的缓存范围?
A: 可以通过 JVM 参数修改最大缓存值:
-Djava.lang.Integer.IntegerCache.high=256最小值 -128 不可修改。
Q4: 为什么要有包装类?
A:
- 泛型支持:Java 泛型不支持基本类型,
List<int>不合法,必须用List<Integer> - 集合存储:集合只能存储对象
- null 表示:包装类可以为 null,表示"缺失值"
- 工具方法:提供
parseInt()、toString()等实用方法
四、常量与变量
4.1 变量(Variable)
变量是程序运行过程中值可以改变的存储单元。Java 中变量必须先声明、后使用。
变量声明与初始化
// 声明变量
int age;
String name;
// 声明并初始化
int age = 25;
String name = "张三";
// 同时声明多个同类型变量
int a = 1, b = 2, c = 3;变量分类
| 分类 | 说明 | 生命周期 | 默认值 |
|---|---|---|---|
| 局部变量 | 方法/代码块内定义 | 方法调用时创建,执行结束销毁 | 无默认值,必须显式初始化 |
| 成员变量(实例变量) | 类中、方法外定义 | 对象创建时存在,对象销毁时消失 | 有默认值 |
| 静态变量(类变量) | 用 static 修饰 | 类加载时存在,类卸载时消失 | 有默认值 |
public class VariableDemo {
// 静态变量(类变量)
static int staticVar;
// 成员变量(实例变量)
int instanceVar;
public void method() {
// 局部变量:必须显式初始化
int localVar = 10;
System.out.println(localVar);
}
public static void main(String[] args) {
VariableDemo obj = new VariableDemo();
System.out.println(staticVar); // 0(默认值)
System.out.println(obj.instanceVar); // 0(默认值)
}
}成员变量默认值
| 数据类型 | 默认值 |
|---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000'(空字符) |
boolean | false |
| 引用类型 | null |
4.2 常量(Constant)
常量是值不可改变的量。Java 中使用 final 关键字定义常量。
常量的特点
- 只能赋值一次,赋值后不可修改
- 命名规范:全大写字母 + 下划线分隔
- 通常与
static一起使用,作为全局常量
public class Constants {
// 实例常量
final int MAX_AGE = 150;
// 静态常量(推荐)
public static final double PI = 3.14159;
public static final int MAX_VALUE = 100;
public static final String APP_NAME = "MyApp";
// 接口中的变量默认是 public static final
interface Config {
int TIMEOUT = 30; // 等价于 public static final int TIMEOUT = 30;
}
}final 关键字
final 可以修饰类、方法、变量,含义各不同:
| 修饰对象 | 含义 |
|---|---|
| 类 | 不能被继承(如 String) |
| 方法 | 不能被重写 |
| 变量 | 值不可变(常量) |
| 引用变量 | 引用不可变,但对象内容可变 |
// final 修饰基本类型:值不可变
final int NUM = 10;
// NUM = 20; // 编译错误
// final 修饰引用类型:引用不可变,对象内容可变
final int[] arr = {1, 2, 3};
arr[0] = 100; // ✅ 可以修改数组元素
// arr = new int[5]; // ❌ 不能重新赋值
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 可以添加元素
// list = new ArrayList<>(); // ❌ 不能重新赋值4.3 常量池(Constant Pool)
Java 为了节省内存和提高性能,对某些常量进行了缓存。
字符串常量池
// 字符串常量池
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true(指向常量池同一对象)
String s3 = new String("hello");
System.out.println(s1 == s3); // false(堆中新建对象)
System.out.println(s1 == s3.intern()); // true(intern() 返回常量池引用)包装类常量池
除 Float 和 Double 外,其他包装类都有缓存(见第三节)。
4.4 变量作用域
变量的作用域决定了它的可见性和生命周期。
public class ScopeDemo {
// 类级别:整个类可见
private int classVar = 1;
public void method(int param) { // 方法参数:方法内可见
int methodVar = 2; // 方法级别:方法内可见
if (true) {
int blockVar = 3; // 代码块级别:代码块内可见
System.out.println(classVar); // ✅ 可访问
System.out.println(param); // ✅ 可访问
System.out.println(methodVar); // ✅ 可访问
System.out.println(blockVar); // ✅ 可访问
}
// System.out.println(blockVar); // ❌ 编译错误:超出作用域
}
}作用域规则
- 就近原则:变量名冲突时,优先使用最近的定义
- 代码块作用域:
{}内定义的变量,外部无法访问 - for 循环变量:循环变量仅在循环内有效
public void test() {
int x = 10;
if (true) {
int x = 20; // ❌ 编译错误:变量名冲突
}
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// System.out.println(i); // ❌ 编译错误:i 超出作用域
}4.5 常见面试题
Q1: 局部变量为什么必须显式初始化?
A: 局部变量存储在栈中,不会自动初始化。如果不显式赋值,变量值是随机的(垃圾值),使用会导致不可预测的行为。Java 编译器强制要求初始化,是为了避免这类错误。
public void test() {
int a;
// System.out.println(a); // 编译错误:可能尚未初始化变量 a
a = 10; // 先赋值
System.out.println(a); // ✅ 正确
}Q2: final 修饰的变量能被修改吗?
A: 分情况:
- 基本类型:值不可变
- 引用类型:引用不可变,但对象内容可变
final StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // ✅ 可以修改内容
// sb = new StringBuilder(); // ❌ 不能修改引用Q3: 成员变量和局部变量的区别?
| 对比项 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 类中、方法外 | 方法/代码块内 |
| 默认值 | 有 | 无,必须初始化 |
| 生命周期 | 对象/类生命周期 | 方法调用期间 |
| 存储位置 | 堆(实例变量)/ 方法区(静态变量) | 栈 |
| 访问修饰符 | 可以使用 | 不能使用 |
Q4: 接口中的变量有什么特点?
A: 接口中的变量默认是 public static final,即全局常量。
interface MyInterface {
int VALUE = 100; // 等价于 public static final int VALUE = 100;
}
// 使用
System.out.println(MyInterface.VALUE);Q5: 静态变量和实例变量的区别?
A:
| 对比项 | 静态变量 | 实例变量 |
|---|---|---|
| 关键字 | static | 无 |
| 所属 | 类 | 对象 |
| 内存位置 | 方法区 | 堆 |
| 数量 | 只有一份 | 每个对象一份 |
| 访问方式 | 类名.变量名 | 对象.变量名 |
public class Demo {
static int staticVar = 0;
int instanceVar = 0;
public static void main(String[] args) {
Demo d1 = new Demo();
Demo d2 = new Demo();
d1.staticVar = 10;
d1.instanceVar = 20;
System.out.println(d2.staticVar); // 10(所有对象共享)
System.out.println(d2.instanceVar); // 0(每个对象独立)
}
}五、修饰符
Java 修饰符用于定义类、方法、变量的访问权限和特性。主要分为两类:访问修饰符和非访问修饰符。
5.1 访问修饰符
Java 提供四种访问级别,从严格到宽松依次为:private → default → protected → public。
访问权限一览表
| 修饰符 | 同一类内 | 同一包内 | 不同包子类 | 不同包非子类 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
default(默认) | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
package com.example;
public class AccessDemo {
private int privateVar = 1; // 只能在本类内访问
int defaultVar = 2; // 同一包内可访问
protected int protectedVar = 3; // 不同包子类也可访问
public int publicVar = 4; // 任意位置可访问
private void privateMethod() { }
void defaultMethod() { }
protected void protectedMethod() { }
public void publicMethod() { }
}
// 同一包内其他类
class SamePackage {
void test() {
AccessDemo demo = new AccessDemo();
// demo.privateVar; // ❌ 编译错误
demo.defaultVar; // ✅
demo.protectedVar; // ✅
demo.publicVar; // ✅
}
}不同包子类访问
package com.other;
import com.example.AccessDemo;
public class SubClass extends AccessDemo {
void test() {
// privateVar; // ❌ 编译错误
// defaultVar; // ❌ 编译错误(不同包)
protectedVar; // ✅ 子类可访问
publicVar; // ✅
}
}使用原则
| 成员类型 | 推荐访问级别 |
|---|---|
| 成员变量 | private(封装,通过 getter/setter 访问) |
| 构造方法 | public(便于创建对象) |
| 普通方法 | public(对外提供服务) |
| 工具方法 | private(内部使用) |
5.2 非访问修饰符
static 静态修饰符
static 修饰的成员属于类,不属于对象实例。
public class StaticDemo {
// 静态变量:所有对象共享
public static int count = 0;
// 实例变量:每个对象独立
public String name;
// 静态代码块:类加载时执行一次
static {
System.out.println("类加载完成");
}
// 静态方法:可直接通过类名调用
public static void staticMethod() {
System.out.println("静态方法");
// name; // ❌ 不能直接访问实例变量
// instanceMethod(); // ❌ 不能直接调用实例方法
}
// 实例方法
public void instanceMethod() {
System.out.println("实例方法");
count; // ✅ 可以访问静态变量
staticMethod(); // ✅ 可以调用静态方法
}
}
// 使用
StaticDemo.count = 10; // 通过类名访问静态变量
StaticDemo.staticMethod(); // 通过类名调用静态方法final 最终修饰符
final 表示"不可改变"。
// final 类:不能被继承
public final class FinalClass { }
// final 方法:不能被重写
public class Parent {
public final void finalMethod() { }
}
// final 变量:只能赋值一次(常量)
final int MAX = 100;
// final 引用:引用不可变,对象内容可变
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 可以修改内容
// list = new ArrayList<>(); // ❌ 不能修改引用abstract 抽象修饰符
abstract 用于定义抽象类和抽象方法。
// 抽象类:不能实例化
public abstract class Animal {
// 抽象方法:只有声明,没有实现
public abstract void makeSound();
// 普通方法:可以有实现
public void sleep() {
System.out.println("睡觉");
}
}
// 子类必须实现所有抽象方法
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
}synchronized 同步修饰符
synchronized 用于多线程同步,保证线程安全。
public class SyncDemo {
// 同步实例方法:锁的是当前对象(this)
public synchronized void instanceMethod() {
// 同一时间只有一个线程能执行
}
// 同步静态方法:锁的是类对象(Class 对象)
public static synchronized void staticMethod() {
// 同一时间只有一个线程能执行
}
// 同步代码块:锁指定对象
public void blockMethod() {
synchronized(this) { // 或其他对象
// 同步代码块
}
}
}volatile 易变修饰符
volatile 保证变量的可见性和有序性,但不保证原子性。
public class VolatileDemo {
// 可见性:一个线程修改后,其他线程立即可见
// 有序性:禁止指令重排序
private volatile boolean running = true;
public void stop() {
running = false; // 其他线程立即可见
}
public void doWork() {
while (running) {
// 工作逻辑
}
}
}transient 瞬态修饰符
transient 修饰的变量不会被序列化。
public class User implements Serializable {
private String name;
private transient String password; // 序列化时忽略
// 反序列化后 password 为 null
}5.3 修饰符组合使用
| 组合 | 说明 |
|---|---|
public static final | 全局常量 |
public static | 静态方法/变量 |
public abstract | 抽象方法 |
public synchronized | 同步方法 |
private static | 私有静态变量(单例模式常用) |
public class ModifierCombo {
// 全局常量
public static final int MAX_VALUE = 100;
// 私有静态变量(单例模式)
private static ModifierCombo instance;
// 获取单例
public static ModifierCombo getInstance() {
if (instance == null) {
instance = new ModifierCombo();
}
return instance;
}
// 同步方法
public synchronized void syncMethod() { }
}5.4 常见面试题
Q1: private 构造方法有什么用?
A: 主要用于单例模式和工具类,防止外部实例化。
// 单例模式
public class Singleton {
private static Singleton instance;
private Singleton() { } // 私有构造方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 工具类
public class StringUtils {
private StringUtils() { } // 私有构造方法,防止实例化
public static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
}Q2: static 能修饰构造方法吗?
A: 不能。构造方法用于创建对象,而 static 属于类级别,两者矛盾。
Q3: abstract 和 final 能一起用吗?
A: 不能。abstract 要求子类重写,final 禁止重写,两者矛盾。
Q4: protected 和 default 的区别?
A:
| 对比项 | protected | default |
|---|---|---|
| 同一包内 | ✅ 可访问 | ✅ 可访问 |
| 不同包子类 | ✅ 可访问 | ❌ 不可访问 |
| 不同包非子类 | ❌ 不可访问 | ❌ 不可访问 |
Q5: 为什么成员变量建议用 private?
A:
- 封装性:隐藏实现细节
- 数据保护:防止外部直接修改
- 灵活性:可以通过 getter/setter 添加验证逻辑
- 可维护性:修改内部实现不影响外部代码
public class Person {
private int age; // 私有变量
public int getAge() {
return age;
}
public void setAge(int age) {
if (age > 0 && age < 150) { // 添加验证逻辑
this.age = age;
}
}
}六、方法重载与重写
方法重载(Overloading)和方法重写(Overriding)是 Java 多态性的重要体现,两者名称相似但本质完全不同。
6.1 方法重载(Overloading)
重载是指在同一个类中,定义多个同名方法,但参数列表不同。
重载的规则
| 规则 | 说明 |
|---|---|
| 方法名相同 | 必须完全一致 |
| 参数列表不同 | 参数个数、类型、顺序至少有一项不同 |
| 与返回值无关 | 仅返回值不同不能构成重载 |
| 与访问修饰符无关 | 可以有不同的访问权限 |
public class Calculator {
// 重载:参数个数不同
public int add(int a, int b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
// 重载:参数类型不同
public double add(double a, double b) {
return a + b;
}
// 重载:参数顺序不同
public void print(int a, String b) {
System.out.println(a + \": \" + b);
}
public void print(String a, int b) {
System.out.println(a + \": \" + b);
}
}重载的调用匹配
编译器根据方法签名(方法名 + 参数列表)选择最匹配的方法:
public void test(int a) { System.out.println(\"int\"); }
public void test(long a) { System.out.println(\"long\"); }
public void test(Integer a) { System.out.println(\"Integer\"); }
public void test(int... a) { System.out.println(\"int...\"); }
// 调用匹配优先级:精确匹配 > 自动类型转换 > 自动装箱 > 可变参数
test(10); // 输出 \"int\"(精确匹配)
test(10L); // 输出 \"long\"(精确匹配 long)
test(Integer.valueOf(10)); // 输出 \"Integer\"(精确匹配)
test(10, 20); // 输出 \"int...\"(可变参数)构造方法重载
构造方法也可以重载,常用 this() 调用其他构造方法:
public class User {
private String name;
private int age;
private String email;
// 无参构造
public User() {
this(\"未知\", 0, null); // 调用全参构造
}
// 两参构造
public User(String name, int age) {
this(name, age, null); // 调用全参构造
}
// 全参构造
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}6.2 方法重写(Overriding)
重写是指子类对父类允许访问的方法进行重新编写(实现内容改变,签名不变)。
重写的规则
| 规则 | 说明 |
|---|---|
| 方法签名相同 | 方法名、参数列表必须完全一致 |
| 返回类型 | 必须相同或是父类返回类型的子类型(协变返回类型) |
| 访问权限 | 不能比父类更严格(可以更宽松) |
| 异常 | 不能抛出新的检查异常或更广的检查异常 |
final 方法 | 不能被重写 |
static 方法 | 不能被重写(可以被隐藏) |
private 方法 | 不能被重写(子类不可见) |
public class Animal {
public void makeSound() {
System.out.println(\"动物发出声音\");
}
protected Animal create() {
return new Animal();
}
public final void sleep() {
System.out.println(\"睡觉\");
}
public static void run() {
System.out.println(\"动物跑\");
}
}
public class Dog extends Animal {
// ✅ 重写:方法签名相同
@Override
public void makeSound() {
System.out.println(\"汪汪汪\");
}
// ✅ 重写:返回类型可以是父类返回类型的子类
@Override
protected Dog create() {
return new Dog();
}
// ❌ 编译错误:final 方法不能重写
// @Override
// public void sleep() { }
}6.3 重载 vs 重写
| 对比项 | 重载(Overloading) | 重写(Overriding) |
|---|---|---|
| 发生位置 | 同一个类中 | 父子类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回类型 | 无关(可不同) | 必须相同或为子类型 |
| 访问权限 | 无关(可不同) | 不能更严格 |
| 异常 | 无关(可不同) | 不能抛出更广的异常 |
| 多态类型 | 编译时多态(静态绑定) | 运行时多态(动态绑定) |
// 重载示例
public class OverloadDemo {
public void show(int a) { }
public void show(String a) { }
}
// 重写示例
class Parent {
public void show() { System.out.println(\"Parent\"); }
}
class Child extends Parent {
@Override
public void show() { System.out.println(\"Child\"); }
}
// 多态调用
Parent obj = new Child();
obj.show(); // 输出 \"Child\"(运行时多态)6.4 常见面试题
Q1: 下面代码输出什么?
public class Test {
public void print(Object obj) { System.out.println(\"Object\"); }
public void print(String str) { System.out.println(\"String\"); }
public static void main(String[] args) {
Test t = new Test();
Object obj = \"hello\";
t.print(obj);
}
}A: 输出 Object。重载是静态绑定,编译时根据声明类型决定调用哪个方法。
Q2: 为什么返回类型不能作为重载的区分条件?
A: 因为调用方法时可以忽略返回值,编译器无法区分。
Q3: static 方法能被重写吗?
A: 不能。static 方法属于类,可以被隐藏但不能重写。
Q4: private 方法能被重写吗?
A: 不能。private 方法对子类不可见,子类定义同名方法是新方法,不是重写。
rn
七、构造方法
构造方法(Constructor)是类的一种特殊方法,用于创建和初始化对象。
7.1 构造方法的特点
| 特点 | 说明 |
|---|---|
| 方法名 | 必须与类名完全相同 |
| 返回类型 | 没有返回类型(连 void 也没有) |
| 作用 | 创建对象并初始化成员变量 |
| 调用时机 | 使用 new 关键字创建对象时自动调用 |
| 默认构造 | 如果没有定义任何构造方法,编译器自动生成无参构造方法 |
public class Person {
private String name;
private int age;
// 构造方法:与类名相同,无返回类型
public Person() {
this.name = \"未知\";
this.age = 0;
}
// 带参构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// 使用构造方法创建对象
Person p1 = new Person(); // 调用无参构造
Person p2 = new Person(\"张三\", 25); // 调用带参构造7.2 默认构造方法
如果类中没有定义任何构造方法,编译器会自动生成一个无参构造方法(默认构造方法)。
public class Student {
private String name;
private int age;
// 没有定义任何构造方法
}
// 编译器自动生成:
// public Student() { }
Student s = new Student(); // 使用默认构造方法注意事项
public class Teacher {
private String name;
// 定义了带参构造方法
public Teacher(String name) {
this.name = name;
}
}
// ❌ 编译错误:没有无参构造方法
// Teacher t = new Teacher();
// ✅ 正确:使用定义的构造方法
Teacher t = new Teacher(\"李老师\");
// 如果需要无参构造,必须显式定义
public class Teacher {
private String name;
public Teacher() { } // 显式定义无参构造
public Teacher(String name) {
this.name = name;
}
}7.3 构造方法重载
构造方法可以重载,提供多种初始化方式。
public class Book {
private String title;
private String author;
private double price;
// 无参构造
public Book() {
this(\"未知\", \"未知\", 0.0);
}
// 两参构造
public Book(String title, String author) {
this(title, author, 0.0);
}
// 全参构造
public Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
}
// 使用不同的构造方法
Book b1 = new Book();
Book b2 = new Book(\"Java编程\", \"张三\");
Book b3 = new Book(\"Java编程\", \"张三\", 99.9);7.4 this() 构造调用
使用 this() 可以在构造方法中调用同一个类的其他构造方法,实现代码复用。
规则
this()必须放在构造方法的第一行- 一个构造方法只能调用一个
this() - 不能形成循环调用
public class User {
private String name;
private int age;
private String email;
// 无参构造 → 调用三参构造
public User() {
this(\"未知\", 0, null); // 必须是第一行
}
// 两参构造 → 调用三参构造
public User(String name, int age) {
this(name, age, null); // 必须是第一行
}
// 全参构造
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
// ❌ 错误示例:this() 不是第一行
public User() {
System.out.println(\"创建对象\");
this(\"未知\", 0, null); // 编译错误:必须第一行
}
// ❌ 错误示例:循环调用
public User() {
this(0); // 调用 User(int)
}
public User(int age) {
this(); // 调用 User() → 形成循环 → 编译错误
}7.5 父类构造调用(super())
子类构造方法默认调用父类的无参构造方法。
规则
- 子类构造方法第一行默认有
super()(调用父类无参构造) - 如果父类没有无参构造,子类必须显式调用父类的有参构造
super()和this()不能同时存在(都必须是第一行)
public class Animal {
private String name;
// 无参构造
public Animal() {
System.out.println(\"Animal 无参构造\");
}
// 带参构造
public Animal(String name) {
this.name = name;
System.out.println(\"Animal 带参构造\");
}
}
public class Dog extends Animal {
private String breed;
// 默认调用 super()
public Dog() {
super(); // 可省略,自动调用父类无参构造
System.out.println(\"Dog 无参构造\");
}
// 显式调用父类带参构造
public Dog(String name, String breed) {
super(name); // 必须是第一行,不能省略
this.breed = breed;
System.out.println(\"Dog 带参构造\");
}
}
Dog d1 = new Dog(); // 输出:Animal 无参构造 → Dog 无参构造
Dog d2 = new Dog(\"旺财\", \"柴犬\"); // 输出:Animal 带参构造 → Dog 带参构造父类无无参构造的情况
public class Parent {
private int value;
// 只有带参构造,没有无参构造
public Parent(int value) {
this.value = value;
}
}
public class Child extends Parent {
// ❌ 编译错误:父类没有无参构造
// public Child() { }
// ✅ 正确:显式调用父类构造
public Child() {
super(0); // 必须显式调用
}
public Child(int value) {
super(value); // 必须显式调用
}
}7.6 构造方法的访问权限
| 访问权限 | 使用场景 |
|---|---|
public | 任意位置可创建对象(最常用) |
protected | 同一包和子类可创建对象 |
default | 同一包内可创建对象 |
private | 本类内创建对象(单例模式、工厂模式) |
私有构造方法的应用
单例模式
public class Singleton {
private static Singleton instance;
// 私有构造方法:外部无法实例化
private Singleton() { }
// 提供全局访问点
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 使用
Singleton s = Singleton.getInstance();工具类
public class MathUtils {
// 私有构造方法:防止实例化
private MathUtils() {
throw new UnsupportedOperationException(\"工具类不能实例化\");
}
public static int add(int a, int b) {
return a + b;
}
}7.7 构造方法 vs 普通方法
| 对比项 | 构造方法 | 普通方法 |
|---|---|---|
| 方法名 | 必须与类名相同 | 任意合法标识符 |
| 返回类型 | 无返回类型 | 必须有返回类型(包括 void) |
| 调用方式 | new 关键字 | 对象.方法名 |
| 作用 | 创建对象并初始化 | 执行特定功能 |
| 继承 | 不能被子类继承 | 可以被子类继承/重写 |
| abstract | 不能是抽象的 | 可以是抽象的 |
| static/final | 不能修饰 | 可以修饰 |
7.8 构造代码块
构造代码块在创建对象时,优先于构造方法执行。
public class Demo {
private String name;
// 构造代码块:每次创建对象都执行
{
System.out.println(\"构造代码块执行\");
name = \"默认值\";
}
public Demo() {
System.out.println(\"无参构造执行\");
}
public Demo(String name) {
this.name = name;
System.out.println(\"带参构造执行\");
}
}
Demo d1 = new Demo(); // 输出:构造代码块执行 → 无参构造执行
Demo d2 = new Demo(\"测试\"); // 输出:构造代码块执行 → 带参构造执行7.9 初始化顺序
public class InitOrder extends Parent {
private static String staticVar = \"静态变量\";
private String instanceVar = \"实例变量\";
static { System.out.println(\"静态代码块\"); }
{ System.out.println(\"构造代码块\"); }
public InitOrder() {
System.out.println(\"构造方法\");
}
public static void main(String[] args) {
new InitOrder();
}
}
// 输出顺序:
// 1. 父类静态变量/静态代码块
// 2. 子类静态变量/静态代码块
// 3. 父类实例变量/构造代码块
// 4. 父类构造方法
// 5. 子类实例变量/构造代码块
// 6. 子类构造方法7.10 常见面试题
Q1: 构造方法可以被继承吗?
A: 不可以。构造方法属于类本身,不能被继承。子类可以通过 super() 调用父类的构造方法。
Q2: 构造方法可以被重写吗?
A: 不可以。构造方法不能被继承,自然不能被重写。
Q3: 为什么 this() 和 super() 必须在第一行?
A: 确保对象初始化的完整性。父类构造先执行,才能保证子类使用父类成员时已初始化。
Q4: 抽象类可以有构造方法吗?
A: 可以。抽象类不能实例化,但子类实例化时会调用抽象类的构造方法。
public abstract class AbstractClass {
protected String name;
public AbstractClass(String name) {
this.name = name;
}
}
public class ConcreteClass extends AbstractClass {
public ConcreteClass(String name) {
super(name); // 调用抽象类的构造方法
}
}Q5: 接口可以有构造方法吗?
A: 不可以。接口没有成员变量需要初始化,且接口不能实例化,因此没有构造方法。
rn
八、可变参数
可变参数(Varargs)是 JDK 5 引入的特性,允许方法接受任意数量的参数。
8.1 基本语法
// 语法:类型... 参数名
public void print(String... args) {
// args 本质是数组
for (String arg : args) {
System.out.println(arg);
}
}
// 调用方式
print(); // 不传参数
print(\"Hello\"); // 1个参数
print(\"A\", \"B\", \"C\"); // 多个参数
print(new String[]{\"X\", \"Y\"}); // 传数组8.2 可变参数的特点
| 特点 | 说明 |
|---|---|
| 本质 | 编译器将可变参数转换为数组 |
| 位置 | 必须是方法的最后一个参数 |
| 数量 | 一个方法最多只能有一个可变参数 |
| 调用 | 可以传0个、1个或多个参数 |
| 重载 | 可以与数组参数形成重载(但不推荐) |
// ✅ 正确:可变参数在最后
public void method(int a, String... args) { }
// ❌ 错误:可变参数不在最后
// public void method(String... args, int a) { }
// ❌ 错误:多个可变参数
// public void method(String... args, int... nums) { }8.3 可变参数的本质
可变参数是编译器语法糖,编译后自动转换为数组。
// 源代码
public void print(String... args) { }
// 编译后(反编译)
public void print(String[] args) { }
// 调用转换
print(\"A\", \"B\", \"C\");
// 编译后
print(new String[]{\"A\", \"B\", \"C\"});8.4 可变参数与数组参数
public class VarargsDemo {
// 可变参数
public void method1(int... nums) {
System.out.println(\"可变参数: \" + nums.length);
}
// 数组参数
public void method2(int[] nums) {
System.out.println(\"数组参数: \" + nums.length);
}
}
VarargsDemo demo = new VarargsDemo();
// 可变参数:可传0个或多个
demo.method1(); // ✅ 输出:可变参数: 0
demo.method1(1, 2, 3); // ✅ 输出:可变参数: 3
demo.method1(new int[]{1, 2, 3}); // ✅ 也可以传数组
// 数组参数:必须传数组
// demo.method2(); // ❌ 编译错误
demo.method2(new int[]{1, 2, 3}); // ✅ 必须传数组主要区别
| 对比项 | 可变参数 | 数组参数 |
|---|---|---|
| 调用方式 | 可传0~N个值 | 必须传数组 |
| 不传参 | ✅ 可以 | ❌ 编译错误 |
| 灵活性 | 更灵活 | 较严格 |
| 底层实现 | 转换为数组 | 本身就是数组 |
8.5 可变参数与重载
可变参数可以参与方法重载,但需要注意歧义问题。
public class OverloadVarargs {
// 方法1:可变参数
public void print(int... nums) {
System.out.println(\"int...\");
}
// 方法2:可变参数
public void print(String... strs) {
System.out.println(\"String...\");
}
}
OverloadVarargs ov = new OverloadVarargs();
ov.print(1, 2, 3); // ✅ 输出:int...
ov.print(\"A\", \"B\"); // ✅ 输出:String...
// ov.print(); // ❌ 编译错误:歧义,两个方法都匹配优先级规则
当可变参数与固定参数重载时,优先匹配固定参数:
public class PriorityDemo {
public void print(int a, int b) {
System.out.println(\"固定参数\");
}
public void print(int... nums) {
System.out.println(\"可变参数\");
}
}
PriorityDemo pd = new PriorityDemo();
pd.print(1, 2); // 输出:固定参数(优先匹配)
pd.print(1, 2, 3); // 输出:可变参数(固定参数不匹配)8.6 实际应用场景
1. 字符串格式化
// String.format()
String s = String.format(\"姓名: %s, 年龄: %d\", \"张三\", 25);
// System.out.printf()
System.out.printf(\"分数: %.2f%n\", 95.5);2. 集合初始化
// Arrays.asList()
List<String> list = Arrays.asList(\"A\", \"B\", \"C\");
// List.of() (JDK 9+)
List<String> list2 = List.of(\"X\", \"Y\", \"Z\");
// Set.of() (JDK 9+)
Set<Integer> set = Set.of(1, 2, 3);3. 自定义工具方法
public class MathUtils {
// 求任意数量整数的和
public static int sum(int... nums) {
int total = 0;
for (int num : nums) {
total += num;
}
return total;
}
// 求最大值
public static int max(int... nums) {
if (nums.length == 0) {
throw new IllegalArgumentException(\"参数不能为空\");
}
int max = nums[0];
for (int num : nums) {
if (num > max) max = num;
}
return max;
}
}
// 使用
int sum = MathUtils.sum(1, 2, 3, 4, 5); // 15
int max = MathUtils.max(3, 7, 2, 9, 1); // 98.7 注意事项
1. 空参数检查
public void print(String... args) {
// args 可能为空数组,但不是 null
if (args.length == 0) {
System.out.println(\"没有参数\");
return;
}
for (String arg : args) {
System.out.println(arg);
}
}
print(); // args = new String[0],长度为0的数组2. null 参数问题
public void print(String... args) {
System.out.println(args.length);
}
print(); // 输出:0(空数组)
print((String) null); // 输出:1(包含一个 null 元素)
print((String[]) null); // ❌ NullPointerException!3. 性能考量
// 每次调用都会创建新数组
for (int i = 0; i < 10000; i++) {
method(1, 2, 3); // 每次创建 int[3]
}
// 高性能场景:使用数组重载
public void method(int[] nums) { }
method(new int[]{1, 2, 3}); // 可复用数组8.8 常见面试题
Q1: 可变参数可以放在参数列表的开头吗?
A: 不可以。可变参数必须是最后一个参数。
// ❌ 编译错误
// public void method(String... args, int a) { }
// ✅ 正确
public void method(int a, String... args) { }Q2: 一个方法可以有多少个可变参数?
A: 最多一个。因为可变参数必须是最后一个,多个可变参数无法区分边界。
Q3: main 方法可以改成可变参数吗?
A: 可以。JVM 兼容可变参数形式的 main 方法。
// 标准写法
public static void main(String[] args) { }
// 可变参数写法(也合法)
public static void main(String... args) { }Q4: 可变参数和数组参数可以重载吗?
A: 不能。编译后两者签名相同,会导致编译错误。
// ❌ 编译错误:方法重复
// public void method(int... nums) { }
// public void method(int[] nums) { }Q5: 调用时不传参数,可变参数是 null 还是空数组?
A: 是空数组(长度为0),不是 null。
public void test(String... args) {
System.out.println(args == null); // false
System.out.println(args.length); // 0
}
test(); // args = new String[0]rn
九、递归
递归(Recursion)是指方法直接或间接调用自身的一种编程技巧。
9.1 递归的基本概念
// 递归的基本结构
public void recursive() {
// 1. 终止条件(基线条件)
if (终止条件) {
return;
}
// 2. 递归调用
recursive();
}递归要素
| 要素 | 说明 |
|---|---|
| 基线条件 | 递归终止条件,防止无限递归 |
| 递归条件 | 问题分解,向基线条件逼近 |
| 递归调用 | 方法调用自身 |
9.2 经典递归案例
1. 阶乘
public int factorial(int n) {
// 基线条件
if (n <= 1) {
return 1;
}
// 递归条件:n! = n * (n-1)!
return n * factorial(n - 1);
}
factorial(5); // 5 * 4 * 3 * 2 * 1 = 1202. 斐波那契数列
// 1, 1, 2, 3, 5, 8, 13, 21...
public int fibonacci(int n) {
// 基线条件
if (n <= 2) {
return 1;
}
// 递归条件:f(n) = f(n-1) + f(n-2)
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(6); // 83. 求和
// 计算 1 + 2 + 3 + ... + n
public int sum(int n) {
if (n <= 0) {
return 0;
}
return n + sum(n - 1);
}
sum(100); // 50504. 幂运算
// 计算 x^n
public double power(double x, int n) {
if (n == 0) {
return 1;
}
return x * power(x, n - 1);
}
power(2, 10); // 10245. 字符串反转
public String reverse(String str) {
if (str.length() <= 1) {
return str;
}
// 最后一个字符 + 反转前面的部分
return str.charAt(str.length() - 1) + reverse(str.substring(0, str.length() - 1));
}
reverse(\"hello\"); // \"olleh\"9.3 递归 vs 迭代
| 对比项 | 递归 | 迭代 |
|---|---|---|
| 代码可读性 | 更简洁、更符合数学定义 | 相对复杂 |
| 内存消耗 | 较高(栈帧累积) | 较低 |
| 性能 | 较低(方法调用开销) | 较高 |
| 栈溢出风险 | 有可能 | 无 |
| 适用场景 | 树/图遍历、分治算法 | 简单重复操作 |
// 递归实现阶乘
public int factorialRecursive(int n) {
if (n <= 1) return 1;
return n * factorialRecursive(n - 1);
}
// 迭代实现阶乘
public int factorialIterative(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}9.4 递归的内存模型
每次递归调用都会在栈上创建一个新的栈帧(Stack Frame)。
factorial(3) 的执行过程:
调用 factorial(3)
→ 调用 factorial(2)
→ 调用 factorial(1)
→ 返回 1
→ 返回 2 * 1 = 2
→ 返回 3 * 2 = 6
栈帧变化:
┌─────────────┐
│ factorial(3)│ ← 栈顶
├─────────────┤
│ factorial(2)│
├─────────────┤
│ factorial(1)│
└─────────────┘栈溢出
public void infinite() {
infinite(); // 无终止条件
}
// 调用后抛出:
// StackOverflowError9.5 尾递归优化
尾递归是指递归调用是方法的最后一步操作。某些编译器可以优化尾递归,避免栈溢出。
// 普通递归(非尾递归)
public int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归后还有乘法操作
}
// 尾递归
public int factorialTail(int n, int accumulator) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // 递归是最后操作
}
// 封装调用
public int factorial(int n) {
return factorialTail(n, 1);
}
factorial(5); // factorialTail(5, 1) → factorialTail(4, 5) → factorialTail(3, 20) → ...注意:Java 编译器不支持尾递归优化,但理解尾递归有助于编写更优雅的递归代码。
9.6 递归的优化
1. 记忆化(缓存结果)
// 未优化:大量重复计算
public int fib(int n) {
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2); // fib(n-1) 和 fib(n-2) 有重复
}
// 优化:使用数组缓存
private int[] memo = new int[1000];
public int fibMemo(int n) {
if (n <= 2) return 1;
if (memo[n] != 0) return memo[n]; // 已缓存,直接返回
memo[n] = fibMemo(n - 1) + fibMemo(n - 2);
return memo[n];
}
// fib(50):未优化几乎无法计算,优化后瞬间完成2. 转换为迭代
// 斐波那契迭代实现
public int fibIterative(int n) {
if (n <= 2) return 1;
int prev = 1, curr = 1;
for (int i = 3; i <= n; i++) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}9.7 递归的经典应用
1. 二分查找
public int binarySearch(int[] arr, int target, int left, int right) {
if (left > right) {
return -1; // 未找到
}
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, left, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, right);
}
}2. 二叉树遍历
class TreeNode {
int val;
TreeNode left, right;
}
// 前序遍历
public void preorder(TreeNode root) {
if (root == null) return;
System.out.println(root.val); // 访问根
preorder(root.left); // 遍历左子树
preorder(root.right); // 遍历右子树
}
// 中序遍历
public void inorder(TreeNode root) {
if (root == null) return;
inorder(root.left);
System.out.println(root.val);
inorder(root.right);
}
// 后序遍历
public void postorder(TreeNode root) {
if (root == null) return;
postorder(root.left);
postorder(root.right);
System.out.println(root.val);
}3. 汉诺塔
public void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
System.out.println(\"移动盘子 1 从 \" + from + \" 到 \" + to);
return;
}
hanoi(n - 1, from, aux, to); // n-1个盘子从from移到aux
System.out.println(\"移动盘子 \" + n + \" 从 \" + from + \" 到 \" + to);
hanoi(n - 1, aux, to, from); // n-1个盘子从aux移到to
}
hanoi(3, 'A', 'C', 'B');
// 移动盘子 1 从 A 到 C
// 移动盘子 2 从 A 到 B
// 移动盘子 1 从 C 到 B
// 移动盘子 3 从 A 到 C
// 移动盘子 1 从 B 到 A
// 移动盘子 2 从 B 到 C
// 移动盘子 1 从 A 到 C4. 快速排序
public void quickSort(int[] arr, int low, int high) {
if (low >= high) return;
int pivot = partition(arr, low, high);
quickSort(arr, low, pivot - 1); // 排序左半部分
quickSort(arr, pivot + 1, high); // 排序右半部分
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}9.8 常见面试题
Q1: 递归和迭代的区别?
A:
| 对比项 | 递归 | 迭代 |
|---|---|---|
| 实现方式 | 方法调用自身 | 循环结构 |
| 内存 | 占用栈空间 | 占用固定空间 |
| 性能 | 有调用开销 | 通常更快 |
| 可读性 | 更直观 | 可能更复杂 |
Q2: 什么情况下应该使用递归?
A:
- 问题可以分解为相同的小问题
- 数据结构本身是递归的(树、图)
- 代码可读性更重要时
- 递归深度可控
Q3: 如何避免栈溢出?
A:
- 确保有正确的终止条件
- 控制递归深度
- 使用尾递归(如果语言支持)
- 转换为迭代实现
Q4: 计算递归的时间复杂度?
A: 使用主定理或递归树分析。
// T(n) = T(n-1) + O(1) → O(n)
// T(n) = 2T(n-1) + O(1) → O(2^n)
// T(n) = 2T(n/2) + O(n) → O(n log n)Q5: 实现一个递归方法,判断字符串是否是回文?
A:
public boolean isPalindrome(String str) {
if (str.length() <= 1) {
return true;
}
if (str.charAt(0) != str.charAt(str.length() - 1)) {
return false;
}
return isPalindrome(str.substring(1, str.length() - 1));
}
isPalindrome(\"aba\"); // true
isPalindrome(\"hello\"); // false知识点概览
- 标识符&关键字
- 数据类型分类(基本/引用)
- 自动拆箱/装箱
- 常量与变量
- 修饰符
- 方法重载/重写
- 构造方法
- 可变参数
- 递归
syntax.mdx 内容已全部完成!