Skip to content

Java 语法

一、为什么学 Java 语法?

语法是编程语言的"法律",规定了代码怎么写、怎么执行。Java 语法的设计哲学是"简单、健壮、可移植"——一次编写,到处运行。理解语法背后的原理,比死记关键词重要得多。

二、基本数据类型

2.1 为什么有 8 种基本类型?

Java 是强类型语言,编译器需要知道每个变量占多少内存、能存什么值。8 种基本类型覆盖了 99% 的数据场景,且直接存储在栈上,访问极快。

类型大小范围默认值用途
byte1 字节-128 ~ 1270二进制数据、IO 流
short2 字节-32768 ~ 327670节省内存的场景
int4 字节-2^31 ~ 2^31-10默认整数类型
long8 字节-2^63 ~ 2^63-10L大数值、时间戳
float4 字节±3.4E-38 ~ ±3.4E+380.0f科学计算
double8 字节±1.8E-308 ~ ±1.8E+3080.0d默认浮点类型
char2 字节0 ~ 65535(Unicode)'\u0000'单个字符
booleanJVM 依赖true / falsefalse条件判断

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 缓存 TRUEFALSE。这就是为什么 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 让外部无法直接修改。不可变带来的好处:

  1. 线程安全:多线程共享同一 String 无需同步
  2. Hash 缓存:hash 值计算一次后缓存,HashMap 的 key 用 String 性能极好
  3. 字符串常量池:相同字面量复用同一对象,节省内存
  4. 安全:类加载器、数据库连接 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);     // true

3.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) 时间复杂度
StringBuilderStringBuffer
线程安全是(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;  // true

4.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:intInteger 的区别?

int 是基本类型,占 4 字节,存在栈上,不能为 null;Integer 是包装类,存在堆上,可以为 null,支持泛型。自动装箱/拆箱由编译器生成 Integer.valueOf() / intValue() 实现。

Q:finalfinallyfinalize 的区别?

final 修饰类/方法/变量(不可继承/重写/修改);finally 是异常处理的收尾块(保证执行);finalize 是 Object 的方法,GC 前调用(已废弃,用 Cleaner 替代)。