范型
包装类
包装类的定义
在Java中,包装类(Wrapper Classes) 是连接基本类型与对象世界的桥梁,解决了Java"一切皆对象"理念与基本类型之间的矛盾。
通俗的来说:
包装类存在的根本原因:Java的8个基本类型不是对象,但Java的API(集合、泛型、反射、锁、工具方法)只认对象。包装类就是给基本类型"穿上对象的外衣",让它们能进入对象的世界。
在Java中一切皆为对象,要使基本类型定义的变量变成对象方便以对象的身份进行操作。
相关例子请继续阅读下文。
包装类的作用
场景1:我想把数字放进列表里
👀问题 List 只能存对象不能存 int
java
// ❌ 编译报错:List不接受基本类型
List<int> numbers = new ArrayList<>();
// ✅ 必须用包装类Integer
List<Integer> numbers = new ArrayList<>();
numbers.add(100); // 100自动变成Integer对象,才能存进去
没有包装类的后果:Java的集合框架对基本类型完全不可用。
场景2:我想让方法返回"没有结果"
👀问题:基本类型必须有值,不能表示"空"
java
// 从数据库查用户年龄,用户可能没填
public int getAge(int userId) {
// 如果没查到,返回什么?返回0?那和"0岁"混淆了!
return 0; // ❌ 歧义:是0岁还是没填?
}
// ✅ 用包装类返回null,明确表示"无值"
public Integer getAge(int userId) {
if (用户不存在) return null; // 清晰表示"没有数据"
return 25; // 自动装箱为Integer
}
没有包装类的后果:无法区分"值为0"和"没有值",数据库映射会出错
场景3:我想用工具类处理数字
👀问题:基本类型没有方法,不能调用功能
java
int num = 100;
// ❌ 编译报错:int不是对象,没有方法
num.toString();
num.compareTo(200);
// ✅ 包装类有丰富工具方法
Integer obj = 100;
String str = obj.toString(); // "100"
int max = Integer.MAX_VALUE; // 获取int最大值
String hex = Integer.toHexString(100); // "64"
没有包装类的后果:数字转换、进制转换、比较等操作无法简洁完成。
场景4:泛型方法要求对象类型
👀问题:泛型 不能接受 int
java
// 泛型方法:打印任意类型
public <T> void print(T value) {
System.out.println(value);
}
print("hello"); // ✅ String是对象
print(100); // ✅ 自动装箱为Integer,Integer是对象
// 如果没有包装类,100无法传入,泛型对基本类型完全失效
没有包装类的后果:泛型编程、反射、集合等高级特性基本类型无法使用。
场景5:我想在同步代码块里用数字作为锁
👀问题: synchronized 必须锁对象
java
int count = 0;
synchronized (count) { // ❌ 编译报错:不能锁基本类型
count++;
}
// ✅ 包装类是对象,可以锁
Integer count = 0;
synchronized (count) { // 合法
count++; // 拆箱+自增+装箱
}
没有包装类的后果:基本类型无法参与任何需要对象的同步机制。
装箱与拆箱
定义

生活比喻:快递打包
java
//伪代码:
基本类型 = 散装水果(int苹果)
包装类 = 快递盒(Integer盒子)
【装箱】:把散装水果装进快递盒
int 100 → new Integer(100) [穿上对象外衣]
//可以变成快递盒,使用快递盒的性质,对应对象的方法与使用
【拆箱】:从快递盒取出水果
Integer 100 → int 100 [取出数值]
代码演示:
java
//1. 手动装箱拆箱(JDK 5之前)
// 手动装箱
int num = 100;
Integer obj = new Integer(num); // 基本类型 → 包装类
// 手动拆箱
int back = obj.intValue(); // 包装类 → 基本类型
//2. 自动装箱拆箱(现代Java)
// 自动装箱(编译器帮你装)
Integer obj = 100;
// 实际执行:Integer.valueOf(100)
// 自动拆箱(编译器帮你拆)
int num = obj;
// 实际执行:obj.intValue()
为什么要装箱拆箱
java
//原因1:集合只存对象
List<int> list = new ArrayList<>(); // ❌ 报错!不能存基本类型
List<Integer> list = new ArrayList<>(); // ✅ 只能存包装类
list.add(100); // 自动装箱:100 → Integer
int n = list.get(0); // 自动拆箱:Integer → int
//原因2:需要 null 表示"无值"
int age; // 默认0,无法表示"没填"
Integer age = null; // ✅ 可以表示"未知"
//原因3:调用对象方法
int num = 100;
// num.toString(); // ❌ 基本类型没有方法
Integer obj = num; // 装箱
obj.toString(); // ✅ "100" 包装类有方法
总结
装箱 = 给基本类型穿上对象外衣(进集合、调方法、存null),拆箱 = 从对象里扒出数值(做计算、比较大小)。小整数有缓存,大整数新建;循环别用包装类,空值要防NPE。
范型
范型的定义
一句话定义:
泛型 = 类型的"占位符",让代码可以处理"任意类型",同时保持类型安全。
为什么叫"泛型"?
"泛" = 广泛、通用
"型" = 类型
泛型 = 广泛的类型 = 可以代表"任意类型"的"通用类型参数"
结合生活比喻范型
生活比喻:快递柜
没有泛型(像旧式储物柜)
旧式储物柜:只能存"物品",取出时需要自己辨认
- 你存了一个"手机",取出一个"物品"
- 你以为是手机,结果拿出来是双袜子
- 运行时才发现错误(ClassCastException)
有泛型(像智能快递柜)
智能快递柜:存的时候指定类型,取的时候自动识别
- 你存"手机"时,柜子贴上"手机柜"标签
- 取的时候,柜子确保拿出来的一定是手机
- 放错类型(比如放袜子)时,存的时候就报错,不会等到取的时候
代码实例
场景:存取一个值
没有泛型(JDK 1.5之前):
java
// 只能存Object,什么都往里塞
List list = new ArrayList();
list.add("hello"); // 存字符串
list.add(100); // 存整数(混乱!)
list.add(new Date()); // 存日期
// 取出来全是Object,必须强转
String str = (String) list.get(0); // ✅ 成功
String num = (String) list.get(1); // ❌ 运行时报错!ClassCastException
有泛型(现代Java):
java
// 指定只能存String
List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 成功
list.add(100); // ❌ 编译时报错!类型不匹配
// 取出来就是String,无需强转
String str = list.get(0); // ✅ 安全,编译器保证类型正确
以上代码实例对应上文生活例子
没有泛型时的三大痛点
痛点1:类型不安全(运行时崩溃)
java
// 旧式代码:List可以存任意对象
List list = new ArrayList();
list.add("hello");
list.add(100); // 编译通过!但类型混乱
list.add(new Date());
// 取出来必须强转,极易出错
String s = (String) list.get(1); // ❌ 运行时报错!100不是String
// 异常:ClassCastException
//后果:程序上线后突然崩溃,无法提前发现问题。
解决方案1:编译时类型检查(提前发现错误)
java
List<String> list = new ArrayList<>(); // 指定:只能存String
list.add("hello"); // ✅ 编译通过
list.add(100); // ❌ 编译报错!类型不匹配
// 错误在写代码时就被发现,不会带到运行时
//价值:程序更稳定,上线后不会因类型错误崩溃。
痛点2:代码重复(复制粘贴)
java
// 需要存字符串,写一套StringList
class StringList {
void add(String s) { ... }
String get(int index) { ... }
}
// 需要存整数,再写一套IntegerList(代码几乎一样!)
class IntegerList {
void add(Integer i) { ... }
Integer get(int index) { ... }
}
// 需要存学生,再写一套StudentList...
// 无穷无尽的复制粘贴!
//后果:100种类型就要写100个类,维护噩梦。
解决方案2:一套代码通用所有类型(消灭复制粘贴)
java
// 写一个泛型类,T可以代表任意类型
class Box<T> {
private T content;
void set(T value) { content = value; }
T get() { return content; }
}
// 使用时指定具体类型
Box<String> stringBox = new Box<>(); // T变成String
Box<Integer> intBox = new Box<>(); // T变成Integer
Box<Student> studentBox = new Box<>(); // T变成Student
// 无需重复写三个类!
//价值:代码复用率100%,维护只需改一处。
痛点3:强制类型转换(代码丑陋)
java
// 每次取出都要强转,繁琐且危险
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 必须写(String),不能省略
// 如果忘了强转?编译报错或运行异常
//后果:代码冗长,可读性差,出错率高。
解决方案3:自动类型推断(无需强转)
java
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // ✅ 直接是String,无需强转!
// 编译器自动知道里面是String
//价值:代码简洁,可读性强,出错率为0。
擦除机制
什么是擦除
定义:Java泛型在编译后会被"擦除"成原始类型,运行时完全不知道泛型的存在。
java
源代码:List<String> → 编译后:List(变成原始类型)
源代码:T extends Number → 编译后:Number(变成边界类型)
源代码:<T> → 编译后:Object(无边界时变成Object)
为什么需要擦除机制
- 历史兼容性(核心原因)
java
java
// JDK 1.5之前(2004年)没有泛型,大家这样写:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 手动强转
// JDK 1.5引入泛型后,必须兼容旧代码:
List<String> newList = oldList; // 必须能赋值,否则所有旧代码报废
解决方案:擦除成相同类型,保证新旧代码二进制兼容。
擦除的具体规则

在上述场景4中范型只能接受包装类integer,是因为Java中存在一种擦除机制