Java类型系统
Java 语言基础的四大块是基本类型、包装类、String 和泛型,理解它们的底层行为能解释一半的日常踩坑。基本类型是值语义,包装类引入自动装箱的陷阱;String 不可变 + 常量池让 == 比较产生迷惑;泛型在编译期擦除,运行期拿不到类型参数——这四条规则构成 Java 类型系统的骨架。
基本类型
基本类型存值、对象存引用,这是 Java 类型系统的根本分界。八种基本类型按字节大小分四档:
| 类型 | 字节 | 默认值 | 范围 |
|---|---|---|---|
byte | 1 | 0 | -128 ~ 127 |
short | 2 | 0 | -32768 ~ 32767 |
int | 4 | 0 | ±21 亿 |
long | 8 | 0L | 极大 |
float | 4 | 0.0f | 约 ±3.4E38 |
double | 8 | 0.0 | 约 ±1.8E308 |
char | 2 | '\u0000' | 0 ~ 65535 |
boolean | 1(JVM 视实现) | false | true/false |
整数运算溢出
int 运算结果超出 21 亿会静默溢出不报错:
int max = Integer.MAX_VALUE;
System.out.println(max + 1); // -2147483648,环绕成最小值包装类与自动装箱
每个基本类型有对应包装类(Integer/Long/...),用于泛型和集合(不能存基本类型)。Java 5 起自动装箱(int → Integer)和拆箱(反向),但底层是 valueOf/intValue 调用,有性能与相等性陷阱:
Integer a = 127; // 装箱,实际是 Integer.valueOf(127)
Integer b = 127;
a == b // true:-128~127 有缓存,同一对象
Integer c = 128;
Integer d = 128;
c == d // false:超出缓存范围,两个新对象判断规则
包装类比较用 equals(),不用 ==(-128~127 缓存内的巧合不可依赖)。自动装箱在循环中会创建大量包装对象,性能敏感处避免。
Integer x = null;
int y = x; // 拆箱空引用 → NullPointerExceptionString
String 不可变 + 字符串常量池,是 Java 最容易被 == 坑到的地方:
String s1 = "hello"; // 常量池中的对象
String s2 = "hello"; // 直接复用
s1 == s2 // true:同一常量池对象
String s3 = new String("hello"); // 强制新建堆对象
s1 == s3 // false:不同对象
s1.equals(s3) // true:内容相同String 不可变的原因(常量池共享、安全性、线程安全、哈希缓存)和 StringBuilder/StringBuffer 的选型见 Java集合 的 StringBuilder 章节。频繁拼接字符串在循环中用 StringBuilder,避免 + 每次创建新对象。
泛型与类型擦除
Java 泛型在编译期做类型检查,编译后擦除为原始类型(Object 或上界),运行期拿不到类型参数。这是与 C++ 模板的本质区别,也带来两个经典现象:
// 1. 运行期无法用 instanceof 检查泛型类型
List<String> list = new ArrayList<>();
// list instanceof List<String> // ❌ 编译错误
// 2. 泛型数组创建受限
// String[] arr = new String[10]; // 普通数组 OK
// List<String>[] arr = new List<String>[10]; // ❌ 不允许
// 3. 桥方法现象
class Parent<T> { T get() { return null; } }
class Child extends Parent<String> {
@Override String get() { return "str"; } // 编译器生成桥方法 Object get()
}泛型与重载冲突
擦除导致"参数化类型不同但擦除后相同"的方法签名冲突:
// ❌ 编译错误:List<String> 和 List<Integer> 擦除后都是 List
void f(List<String> a) { }
void f(List<Integer> b) { }通配符
| 写法 | 含义 | 场景 |
|---|---|---|
List<?> | 任意类型 | 只读遍历 |
List<? extends T> | T 或 T 的子类 | 生产者(只读) |
List<? super T> | T 或 T 的父类 | 消费者(只写) |
PECS 原则(Producer-Extends, Consumer-Super):只读时用 extends 上界,只写时用 super 下界,保证类型安全:
void read(List<? extends Number> list) { Number n = list.get(0); } // 读取
void write(List<? super Integer> list) { list.add(1); } // 写入枚举
枚举是类型安全的常量集合,JLS 保证枚举常量单例——除了定义的常量外没有其他实例,序列化、克隆、反射都无法创建新实例,因此比较用 == 即可,比 equals 更快且编译期能检测类型兼容性:
Color.BLACK == Chiral.LEFT; // 编译失败!类型不兼容(正确行为)
Color.BLACK.equals(Chiral.LEFT); // 编译通过(不应该)BigDecimal
浮点数无法用二进制精确表示(2.0f - 1.9f = 0.100000024),涉及金额计算必须用 BigDecimal。
创建方式
禁止 new BigDecimal(0.1),推荐 new BigDecimal("0.1") 或 BigDecimal.valueOf(0.1)——构造器传 double 会先转二进制再转十进制,精度已丢失。
a.add(b); // 加法
a.subtract(b); // 减法
a.multiply(b); // 乘法
a.divide(b, 2, RoundingMode.HALF_UP); // 除法(必须指定精度和舍入模式)变量与作用域
Java 变量按声明位置分三类作用域:局部变量(方法内)、实例变量(对象内,有默认值)、静态变量(类内,所有实例共享)。局部变量没有默认值,使用前必须初始化,否则编译错误——这是 Java 比 C 安全的地方。
语言基础与 Java内部类 的四种类型、反射机制 的运行时动态能力、Java8新特性 的现代语法共同构成 Java 语言核心。