一、什么是泛型(Generics)
它可以让类、接口或方法在定义时不指定具体类型,而在使用时由调用者确定,从而实现类型安全和模板复用。
通俗比喻:动物园:
动物分类:猫科动物都有锋利的牙齿和利爪(共有属性),会利用利爪捕猎(共有行为);犬科动物也有自己的共性。
差异化:不同种类的猫科动物有不同的花纹、叫声和习性,不同犬科动物也各自不同。
如果建动物园时只是把所有动物随便放在一个大笼子里(用父类或子类管理),游客在参观时就会混乱,不知道每只动物具体是什么。
泛型的作用:
泛型就像给每类动物建造一个“指定的区域”,并贴上清晰的标签(类型参数)。
在这个模板区域里,共有属性和行为被统一管理,而每种动物的特有属性和行为也能被正确识别。
这样既保证了统一管理的逻辑(模板),又保证了类型安全(不同科类不会混在一起)。
简单来说,泛型就是在编程中实现了这种“分类模板”,让相似逻辑可复用,同时不会丢失具体类型信息。
当然泛型并不是取代继承的,而是为了互补。
二、泛型的分类与使用
1.泛型类
定义模板类,字段或者方法的类型不固定,由调用者指定。
class Box<T> {
private T content;
public void set(T item) { content = item; }
public T get() { return content; }
}
// 使用 TV(电视)是自定义的类,当然也可以使用 Integer Boolean 等
// 在java中而是因为java泛型 只接受引用类型(对象),所以需要使用包装类。
Box<TV> tvBox = new Box<>();
tvBox.set(new TV());
TV tv = tvBox.get(); // 类型安全,不需要强转
// Fridge(冰箱)是自定义的类
Box<Fridge> fridgeBox = new Box<>();
fridgeBox.set(new Fridge());
Fridge f = fridgeBox.get();
2.泛型方法
方法级别模板,方法内部的类型由调用者指定
public static <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
ITV tv = createInstance(TV.class); // 编译期就知道类型是 TV
public static <T> T createInstance(String className, Class<T> clazz) throws Exception {
return (T) Class.forName(className).getDeclaredConstructor().newInstance();
}
// 使用
ITV tv = createInstance("com.sh.service.impl.TVImpl",ITV.class)
第二种写法是不推荐的,因为通过Class.forName获取类时,编译器无法在编译时验证类型,导致需要在运行时进行强制转换。这会失去编译时类型安全检查,如果类路径错误或类型不匹配,会在运行时抛出ClassCastException。
3.泛型接口
接口中方法的参数或返回值类型由使用者指定
interface IRepository<T> {
void save(T entity);
T findById(String id);
}
IRepository<User> userRepo;
IRepository<Product> productRepo;
三、泛型边界
有时候需要泛型只允许特定类型或者子类,就需要泛型边界(约束)
泛型上界:可以接受Device 或其子类,上界指的是这个T最大的范围,可以操作读取,但是写入不安全,因为不知道具体是哪个类型,只能当他是Device
public <T extends Device> void handleDevice(List<T> device) {
device.get(0).powerOn();
//device.add(new device());// 编译错误。
}
泛型下界:可以接受 Device 或其父类,下界指的是这个T最小的范围,可以写入,但是不能读取,因为不知道具体的值类型,只能当 Object 处理
public void addDevice(List<? super Device> devices) {
devices.add(new TV()); // 安全
//device.get(0).powerOn(); // 编译错误
}
多重边界:多重边界是对泛型类型参数 同时施加多个约束,限制它必须满足 多个类型条件。
最多只能继承一个类(必须放在第一个位置)
可以实现多个接口(用 & 连接)
调用方法时可以使用类和接口的方法
public <T extends ClassA & InterfaceB & InterfaceC> void method(T t) {
//T必须满足必须是 classA或者classA子类,但是同时又得是interfaceB和interfaceC的接口。
// 三个条件都满足才可以。
}
四、<?>
<?> = 盒子里装 某种具体但未指定的类型 → 可以看,但不能随便放东西。
<? extends T> = 盒子里装 T 或子类 → 可以取出来当 T,但不能放。
<? super T> = 盒子里装 T 或父类 → 可以放 T,但取出来只能当 Object。
五、协变和逆变
在泛型中,类型参数 在继承关系下的行为叫做变性(Variance)。
协变(Covariant,协变):允许将 子类型泛型对象赋值给父类型泛型变量。
逆变(Contravariant,逆变):允许将 父类型泛型对象赋值给子类型泛型变量。
不变(Invariant):泛型类型不能自动转换,即使泛型参数有继承关系。
协变(Covariance)
含义:类型参数是只读的(Producer),只能读取,不能写入
Java 表示法:? extends T
List<Interger> ints = new ArraryList<>();
List<Object> nums = ints;// 不对,虽然 interger是Object的子类,但是泛型不支持继承兼容
// 通过一下方式可以实现
List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0); // 读取安全
// nums.add(10); // 写入不安全,因为具体子类可能不同
逆变(Contravariance)
含义:类型参数是只写的(Consumer),可以写入,但读取不确定类型
Java 表示法:? super T
List<? super Integer> list = new ArrayList<Number>();
list.add(10); // 写入安全
Object o = list.get(0); // 读取只能当 Object
| 类型 | 比喻 | 可读 | 可写 |
|---|---|---|---|
| 协变 | 盒子装的是某类动物的子类 → 可以观察(读取) | ✅可以读取为父类 | ❌写入不安全 |
| 逆变 | 盒子可以装某类动物的父类 → 可以放入具体子类 | ❌ 读取只能当 Object | ✅ 写入安全 |
六、类型擦拭
泛型擦拭是 Java 编译器在编译阶段做的一件事:
把泛型类型信息从字节码中移除(擦掉具体类型参数)。
编译器会在必要的地方插入 类型转换(cast) 和 边界检查。
目的是 保证向下兼容老版本的 Java(因为泛型是在 JDK 1.5 引入的)。
public static <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
ITV tv = createInstance(TV.class);
编译后伪代码
public static Object createInstance(Class clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
ITV tv = (ITV) createInstance((Class) TV.class);
T 被擦掉 → 编译器插入强制类型转换
如果类型不匹配,会在运行时抛 ClassCastException
Java泛型详解
867

被折叠的 条评论
为什么被折叠?



