Appearance
Java 语法
一、为什么学 Java 语法?
语法是编程语言的"法律",规定了代码怎么写、怎么执行。Java 语法的设计哲学是"简单、健壮、可移植"——一次编写,到处运行。理解语法背后的原理,比死记关键词重要得多。
二、基本数据类型
2.1 为什么有 8 种基本类型?
Java 是强类型语言,编译器需要知道每个变量占多少内存、能存什么值。8 种基本类型覆盖了 99% 的数据场景,且直接存储在栈上,访问极快。
| 类型 | 大小 | 范围 | 默认值 | 用途 |
|---|---|---|---|---|
| byte | 1 字节 | -128 ~ 127 | 0 | 二进制数据、IO 流 |
| short | 2 字节 | -32768 ~ 32767 | 0 | 节省内存的场景 |
| int | 4 字节 | -2^31 ~ 2^31-1 | 0 | 默认整数类型 |
| long | 8 字节 | -2^63 ~ 2^63-1 | 0L | 大数值、时间戳 |
| float | 4 字节 | ±3.4E-38 ~ ±3.4E+38 | 0.0f | 科学计算 |
| double | 8 字节 | ±1.8E-308 ~ ±1.8E+308 | 0.0d | 默认浮点类型 |
| char | 2 字节 | 0 ~ 65535(Unicode) | '\u0000' | 单个字符 |
| boolean | JVM 依赖 | true / false | false | 条件判断 |
2.2 包装类型与自动装箱
为什么需要包装类型? 基本类型不能为 null,不能用于泛型(如 List<int> 不合法)。包装类型解决了这两个问题。
java
Integer a = 100; // 自动装箱:int → Integer
int b = a; // 自动拆箱:Integer → int
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false!-128~127 外不会用缓存装箱缓存池: Integer 默认缓存 -128~127,Byte/Short/Long 缓存 -128~127,Character 缓存 0~127。Boolean 缓存 TRUE 和 FALSE。这就是为什么 Integer.valueOf(100) 两次返回同一个对象,而 Integer.valueOf(200) 返回不同的对象。
为什么用 == 比较包装类型容易出 Bug? == 比较的是引用地址,不是值。缓存范围外两个对象地址不同,== 返回 false。始终用 equals() 比较包装类型。
2.3 BigDecimal:精确计算
为什么不能用 float/double 做金额计算? 浮点数在二进制中无法精确表示十进制小数。0.1 在二进制中是无限循环小数,存储时被截断,导致精度丢失。
java
System.out.println(0.1 + 0.2); // 0.30000000000000004,不是 0.3原理: IEEE 754 标准用尾数×指数的方式存储浮点数,如同十进制无法精确表示 1/3(0.333...),二进制无法精确表示 1/10。
java
BigDecimal a = new BigDecimal("0.1"); // 用字符串构造,精确
BigDecimal b = new BigDecimal("0.2");
BigDecimal result = a.add(b); // 0.3,精确注意: 不要用 new BigDecimal(0.1),这样构造的仍然是近似值。必须用字符串构造。
三、String
3.1 为什么 String 不可变?
String 底层是 private final char[] value,final 修饰使得数组引用不可变,private 让外部无法直接修改。不可变带来的好处:
- 线程安全:多线程共享同一 String 无需同步
- Hash 缓存:hash 值计算一次后缓存,HashMap 的 key 用 String 性能极好
- 字符串常量池:相同字面量复用同一对象,节省内存
- 安全:类加载器、数据库连接 URL 等关键字符串不可被篡改
3.2 字符串常量池
原理: JVM 在堆中开辟一块区域存放字符串常量。编译期确定的字面量直接放入常量池,运行时通过 intern() 方法手动加入。
java
String s1 = "hello"; // 放入常量池
String s2 = "hello"; // 从常量池取,s1 == s2 为 true
String s3 = new String("hello"); // 堆上新对象,s1 != s3 为 true
String s4 = s3.intern(); // 把 s3 加入常量池,返回常量池引用
System.out.println(s1 == s4); // true3.3 StringBuilder vs StringBuffer
为什么需要 StringBuilder? String 不可变,每次 + 拼接都创建新对象。循环中拼接字符串会产生大量临时对象,严重影响性能。
java
// 反例:每次循环创建新 String 对象
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // O(n²) 时间复杂度
}
// 正例:用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString(); // O(n) 时间复杂度| StringBuilder | StringBuffer | |
|---|---|---|
| 线程安全 | 否 | 是(synchronized) |
| 性能 | 高 | 低(锁开销) |
| 适用场景 | 单线程(99% 情况) | 多线程 |
四、运算符
4.1 位运算的应用场景
位运算直接操作二进制位,比算术运算快一个数量级。常见场景:
java
// 1. 判断奇偶(比 % 2 快)
boolean isOdd = (n & 1) == 1;
// 2. 乘除 2 的幂次(比 * / 快)
int half = n >> 1; // 除以 2
int double = n << 1; // 乘以 2
// 3. 权限控制(位掩码)
int READ = 1; // 001
int WRITE = 2; // 010
int EXECUTE = 4; // 100
int permission = READ | WRITE; // 011
boolean canRead = (permission & READ) != 0; // true4.2 短路求值
&& 和 || 是短路运算符:如果左侧已经能确定结果,右侧不执行。这既是性能优化,也是常见的编程模式:
java
// 安全写法:先判空,再访问
if (obj != null && obj.getValue() > 0) { ... }
// 错误写法:先访问,后判空 → NPE
if (obj.getValue() > 0 && obj != null) { ... }五、流程控制
5.1 switch 的底层实现
原理: 编译器将 switch 转换为两种字节码指令:
- tableswitch:case 值连续(如 1,2,3,4),生成跳转表,O(1) 定位
- lookupswitch:case 值离散(如 1,10,100),生成二分查找表,O(log n) 定位
java
// case 值连续 → tableswitch,性能好
switch (day) {
case 1: ...; break;
case 2: ...; break;
case 3: ...; break;
}
// case 值离散 → lookupswitch,性能略差但仍可接受
switch (code) {
case 100: ...; break;
case 500: ...; break;
case 999: ...; break;
}5.2 for-each 的底层实现
原理: for-each 是语法糖,编译器将其转换为 Iterator 模式:
java
// 你写的
for (String s : list) {
System.out.println(s);
}
// 编译器生成的
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}不能用 for-each 的场景: 需要修改集合(删除元素)时,用 Iterator 的 remove() 方法,否则会抛 ConcurrentModificationException。
六、数组
6.1 数组的内存结构
原理: 数组在堆上分配连续内存,元素按索引顺序存储。由于是连续内存,通过索引访问的时间复杂度是 O(1)——首地址 + 索引 × 元素大小,直接定位。
java
int[] arr = new int[5]; // 堆上分配 5×4=20 字节连续内存
// arr[0] 在地址 0x1000
// arr[1] 在地址 0x1004
// arr[2] 在地址 0x1008
// ...6.2 Arrays 工具类
java
int[] arr = {3, 1, 4, 1, 5};
Arrays.sort(arr); // 快速排序(基本类型),TimSort(对象类型)
int idx = Arrays.binarySearch(arr, 4); // 二分查找,O(log n)
int[] copy = Arrays.copyOf(arr, 10); // 扩容,底层调用 System.arraycopy(native)
String str = Arrays.toString(arr); // [1, 1, 3, 4, 5]七、面试要点
Q:== 和 equals() 的区别?
== 比较基本类型值或引用地址;equals() 默认也是比较引用地址(Object 的实现),但 String/Integer 等重写了 equals,比较的是值。自定义类必须重写 equals 和 hashCode。
Q:String 为什么不可变?有什么好处?
底层 private final char[],不可变。好处:线程安全、Hash 缓存、常量池复用、安全。如果需要在循环中拼接字符串,用 StringBuilder。
Q:int 和 Integer 的区别?
int 是基本类型,占 4 字节,存在栈上,不能为 null;Integer 是包装类,存在堆上,可以为 null,支持泛型。自动装箱/拆箱由编译器生成 Integer.valueOf() / intValue() 实现。
Q:final、finally、finalize 的区别?
final 修饰类/方法/变量(不可继承/重写/修改);finally 是异常处理的收尾块(保证执行);finalize 是 Object 的方法,GC 前调用(已废弃,用 Cleaner 替代)。
