Java 后端

Java 语法与类型

编译执行与版本边界、基本类型与取值范围、包装类与自动装箱、字符串与常量池、流程控制、方法重载、数组、关键字与访问控制、枚举与 Record。

先弄清代码怎么跑起来、值怎么存进变量,再看类型系统保证了什么。语法层的绝大多数“怪事”——1 + 2 + "3" 为什么等于 "33"、两个 Integer 比较为什么不一定相等——都能用值、引用和类型提升这三件事解释清楚。

一、编译执行与版本边界

Java 编译分两步:javac 把 .java 源文件编译成 .class 字节码,java 再由虚拟机加载执行。字节码不绑定具体操作系统,这是“一次编译,到处运行”的来源。

运行形态 版本边界 适用场景
java SingleFile.java 只在单个源文件内按传统方式编译运行 快速验证一段逻辑
JShell 交互式片段执行,语句即时求值 试探 API、验证表达式
正式项目 javac --release 21 统一编译 交付代码

--release 21 不只是加一个编译参数:它同时锁定“语言特性 + 目标字节码版本 + 标准库 API”。只写 -source 21 而不写 --release,代码能编译却可能调用到目标运行时不存在的 API,编译期不报错、运行期才炸。

java3 行
java single-file.java          // 单文件直接运行
jshell                        // 交互式
javac --release 21 App.java   // 正式构建

提示:IDEA 里能跑不等于构建能过。IDE 可能用了更高版本的 JDK,正式构建往往锁在 --release 指定的版本上,差异就出现在这里。

二、基本类型与取值范围

8 种基本类型占住了最常用的数值区间。选型的直觉很简单:够用就好,不必随手用最大的。

类型 大小 取值范围 说明
byte 1 字节 −128 ~ 127 节省空间,常用于文件字节、计数
short 2 字节 −32,768 ~ 32,767 现今业务代码少用
int 4 字节 −2,147,483,648 ~ 2,147,483,647 整数默认类型
long 8 字节 ±9.22×10^18 时间戳、大数值用
float 4 字节 约 7 位有效数字 近似值,有精度损失
double 8 字节 约 15~16 位有效数字 小数默认类型
char 2 字节 0 ~ 65,535 无符号 Unicode 码点
boolean JVM 未规定 true / false 不参与数值运算

字面量后缀决定它落到哪个类型:整数默认 int,10L 才是 long;小数默认 double,1.5f 才是 float。

java5 行
int i = 10;        // int
long n = 10;       // 仍然 int,赋值时提升为 long
long big = 10L;    // 明确 long
float f = 1.5f;    // 明确 float
double d = 1.5;    // double

宽类型可以隐式放进窄类型,反方向必须显式强转,且可能溢出:

java5 行
long big = 300L;
int small = (int) big;   // 300

double avg = 34.9;
int truncated = (int) avg;  // 34,直接截断小数,不是四舍五入

警告:float/double 是二进制近似表示,0.1 + 0.2 不精确等于 0.3。金额、计量这类要求十进制精确的场景必须用 BigDecimal,不能用 double 硬扛。

三、变量、包装类与自动装箱

变量在声明时就有确定的存储形式:基本类型变量直接保存值,引用类型变量保存对对象的引用。8 种基本类型各有一个对应的包装类,把“值”包装成“对象”。

包装类存在的三个理由:集合与泛型只能装对象(List<int> 非法,要写 List<Integer>)、提供 parseInt/isDigit/isNaN 等工具方法、对象引用可以是 null 而基本类型变量不能。

自动装箱与拆箱让语法上不必手写转换,底层走的是 valueOf 与 intValue:

java7 行
List<Integer> numbers = new ArrayList<>();
numbers.add(10);           // 装箱:int → Integer
int first = numbers.get(0); // 拆箱:Integer → int

char letter = 'A';
int code = letter;              // 65,char 底层是码点
char next = (char) (letter + 1);// 'B'

Integer.valueOf 会复用 −128 ~ 127 的缓存对象,这带来一个必须记住的陷阱:

java8 行
Integer a = 127;
Integer b = Integer.valueOf(127);
Integer x = 128;
Integer y = Integer.valueOf(128);

a == b   // true,命中缓存
x == y   // false,缓存范围外是不同对象
x.equals(y) // true,内容相同

警告:比较数值是否相同一律用 equals(或先拆箱再比较),不要依赖 == 是否碰巧命中缓存。字符串转整数用 Integer.parseInt("42"),需要对象时才用 Integer.valueOf("42"),new Integer("42") 已废弃。

final 修饰变量后只能赋值一次;var 让编译器推断局部变量类型,声明处必须能从右值推出具体类型,不能用于字段、参数或无初始化的声明。

四、字符串、相等性与常量池

String 是不可变类——concat、toUpperCase、trim、replace 都返回新串,原串不变。这条性质换来三件事:字符串能安全地跨线程共享;能安全地作为 Map 键;相同字面量能共享同一个对象。

操作 场景 写法
少量固定拼接 常量、配置键、错误消息 String s = "a" + b
循环或频繁拼接 单线程累加 StringBuilder
多线程共享可变容器 少见 StringBuffer(方法带同步)
连接多值 分隔输出 String.join(",", "A", "B", "C")

+ 是唯一“运算符”级别的字符串操作,规则是从左向右求值,一旦有一侧是 String 就转为拼接:

java5 行
1 + 2            // 3
"1" + 2          // "12"
1 + 2 + "3"      // "33",先算出 3 再拼接
"value=" + 20 + 20 // "value=2020"
"value=" + (20 + 20) // "value=40"

不可变加上常量池机制,让相同内容的字面量共享对象:

java9 行
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = c.intern();

a == b          // true,字面量复用池中同一对象
a == c          // false,new 明确要求再创建一个
a.equals(c)     // true,内容相同
a == d          // true,intern 返回池中该内容的规范引用

intern() 准确说是“返回常量池中与该内容对应的规范引用”,而不是把 new 出来的对象搬进池。业务上比较内容一律用 equals,== 只回答“是不是同一个对象”。

常用方法按用途分组记忆,比逐个背 API 更快:

类别 方法
取长度与字符 length()、charAt(i)、isEmpty()、isBlank()
截取 substring(begin, end)、strip()、lines()
查找 indexOf、lastIndexOf、contains、startsWith、endsWith
变换 toUpperCase、toLowerCase、replace、concat
拆分合并 split、String.join、formatted

注意:索引从 0 开始,substring(begin, end) 的 end 不包含在内——"abcdef".substring(1, 3) 得到 "bc"。越界访问抛 StringIndexOutOfBoundsException 而非返回 null。

多行文本用文本块(Java 15)比手写 \n 和转义清晰,三个双引号后必须换行:

java5 行
String sql = """
        SELECT id, name
        FROM users
        WHERE active = true
        """;

五、流程控制

条件分支从 if 走向“取值更安全”的形态:

java7 行
int score = 85;
String grade = switch (score / 10) {
    case 10, 9 -> "A";
    case 8 -> "B";
    case 7 -> "C";
    default -> "D";
};

传统 switch 靠每个分支末尾的 break 防贯穿,漏写一个 break 就会意外执行下一个分支。switch 表达式(Java 14 正式定稿)没有贯穿风险,多个值用逗号并列,分支逻辑复杂时用代码块加 yield 返回结果。

循环三兄弟按用途选:

循环 适用 特点
for 次数已知 初始化、条件、步进写在一起
while 次数未知 条件在先,循环体在后
do-while 至少执行一次 循环体在前,条件在后
java3 行
int n = 5;
int factorial = 1;
for (int i = 1; i <= n; i++) factorial *= i; // 120

for 循环体里声明的 i 作用域仅限循环内,循环结束就不可见——刻意限制变量泄漏。

循环最常见的错误是差一(off-by-one):

java2 行
for (int i = 0; i <= 5; i++)  // 6 次
for (int i = 0; i < 5; i++)   // 5 次

六、方法与重载

方法把一段操作命名并收口,签名 = 返回类型不算在内的“方法名 + 形参列表”。重载发生在编译期:编译器按实参的类型和数量挑选最匹配的版本。

java3 行
static int sum(int a, int b)          { return a + b; }
static double sum(double a, double b) { return a + b; }
static long sum(long a, long b, long c) { return a + b + c; }

重载三不原则:不靠返回值重载(int f() 与 void f() 无法共存)、参数列表必须真的不同、选择歧义时要显式转型。

可变参数(varargs)让方法接受零个或多个同类型实参,方法内部就是一个数组:

java7 行
static int sum(int... numbers) {
    int total = 0;
    for (int number : numbers) total += number;
    return total;
}
sum();          // 0
sum(1, 2, 3);   // 6

注意:可变参数必须是签名中最后一个参数,一个方法只能有一个;传 null 时编译器会警告“可能传成了数组”,这时应加 (int[]) null 明确表达。

断言用来验证开发阶段的假设,默认不启用,需 -ea 打开:

java1 行
assert score >= 0 && score <= 100 : "score out of range: " + score;

七、数组

数组是定长的连续存储容器:创建后长度不能改,元素类型统一,访问快。长度是字段 length,不是方法——这和字符串的 length() 正好相反:

java3 行
int[] marks = {97, 98, 100};
marks.length        // 3,字段
"abc".length()      // 3,方法

数组不能直接打印成内容,要用工具方法:

操作 方法
打印 Arrays.toString(arr)
比较内容 Arrays.equals(a, b)
比较二维内容 Arrays.deepEquals(a, b) / deepToString
复制并调整长度 Arrays.copyOf(arr, newLength)
大量复制到已有数组 System.arraycopy(src, 0, dst, 0, len)
java5 行
int[] bigger = Arrays.copyOf(marks, 5);  // 多出的位置为 0
System.out.println(Arrays.toString(marks)); // [97, 98, 100]

int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]]

数组默认值:数值类型为 0(浮点 0.0)、boolean 为 false、对象引用为 null。

注意:Java 只有值传递。方法收到数组参数时复制的是引用,所以改元素会影响调用者,而给形参重新指向新数组不会。

java2 行
static void mutate(int[] values) { values[0] = 99; }        // 调用者能看到 99
static void replaceRef(int[] values) { values = new int[3]; } // 调用者不受影响

八、关键字与访问控制

final 三种用法含义不同:final 变量只能赋值一次;final 方法子类不能重写(但可重载);final 类不能被继承。

abstract 声明抽象方法(只有签名没有实现)并禁止实例化;抽象类可以带字段、构造器和具体方法,适合做模板。

访问控制从宽到严:

修饰符 可见范围
public 任意可访问该类型的代码
protected 同包代码,以及子类
不写(包访问) 仅同一个包,子包不算
private 仅声明它的类型内部

警告:包访问不等于“包及其子包”,这是最常被记错的一条。子类访问 protected 成员也受限制,只能在继承语境中访问,不能拿一个外部任意 Animal 引用去取 protected 字段。

this 指当前对象,this(...) 委托同类的另一个构造器,this.field 访问当前对象字段;super 指直接父类,super(...) 调父类构造器,super.method() 调父类实现。super(...) 必须是构造器第一条显式构造调用。

九、枚举与 Record

enum 是一种受限制的类型:只有列出的常量合法,编译器负责挡住拼错和魔法字符串。它还能带字段、构造器、getter 和业务方法。

java18 行
enum Direction {
    NORTH("up"), SOUTH("down"), EAST("right"), WEST("left");

    private final String movement;

    Direction(String movement) { this.movement = movement; }

    public String getMovement() { return movement; }

    public Direction opposite() {
        return switch (this) {
            case NORTH -> SOUTH;
            case SOUTH -> NORTH;
            case EAST -> WEST;
            case WEST -> EAST;
        };
    }
}

枚举上有 switch 时可以做穷举处理,新增常量编译器会提醒漏掉的分支。面向枚举的集合用 EnumSet(内部按位向量实现,运算快,但不能放 null),键是枚举的映射用 EnumMap。

警告:不要把 ordinal() 当作永久业务编号。枚举顺序一调整,所有序号都变,库里存下来的旧数据会全部失真。需要稳定编号就自己给常量加一个 value 字段。

record(Java 16 引入)用来表达不可变数据载体,编译器自动生成规范构造器、访问器(name() 而非 getName())、equals、hashCode 和 toString:

java7 行
record User(String name, String email) {
    User {  // 紧凑构造器,做校验
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name is required");
        }
    }
}

注意:record 不能继承普通类、不能有额外实例字段,适合 DTO 与值对象,不适合需要可变状态的领域对象。

十、易错点总表

现象 原因 处理
1 + 2 + "3" 得 "33" 加号从左向右、遇 String 即拼接 需要先算就加括号
0.1 + 0.2 != 0.3 二进制浮点近似 金额与精确十进制用 BigDecimal
Integer 用 == 比较结果诡异 装箱走 valueOf 且有 −128~127 缓存 比较数值用 equals 或先拆箱
substring 结果少一位 end 不含在结果内 记住左闭右开
数组 println 打出地址 没有覆写 toString Arrays.toString / deepToString
方法内改数组元素影响调用方 传的是引用值的副本 要隔离就先复制
long 变量写成 long n = 10; 仍是 int 字面量默认 int 大整数写 10L
子类访问包内 protected 失败 包访问不含子包 调整包划分或提供公开方法
枚举 ordinal() 存库后全错位 顺序可变 自己维护稳定 value
断言线上不生效 断言默认关闭 关键校验走显式判断或异常

把编译路径、取值范围、引用语义和访问边界想在前,语法层的错误大多不会发生;真正的复杂度总在集合、多态边界和并发上。

← 返回文章