泛型允许开发者编写类型参数化的代码,我觉得它最直接的作用: 能够将运行时异常转为编译时异常
引入泛型之前,集合类通常都是Object
类型,从集合中获取元素时,必须手动将 Object
显式转换为具体类型。
// 没有使用泛型的情况 List list = new ArrayList(); list.add("Hello"); list.add(123); // 添加了一个 Integer String str = (String) list.get(0); // 这是安全的 String numStr = (String) list.get(1); // 运行时会抛出 ClassCastException
引入泛型之后,编译器可以在编译阶段检查类型,比如,向 List
添加非 String
类型对象,编译器会直接报错。
// 使用泛型的情况 ListstringList = new ArrayList<>(); stringList.add("Hello"); // 下面这行代码会导致编译错误,因为只能添加 String 类型的对象 // stringList.add(123); String str = stringList.get(0); // 不需要显式类型转换,而且是安全的
其次泛型允许编写类型无关的通用逻辑,比如可以使用无界泛型T,编写一个通用的泛型 Box类,即可支持任意类型的属性
class Box{ private T value; public Box(T value) { this.value = value; } public T get() { return value; } }
泛型擦除是 Java 泛型的实现机制。编译器在编译阶段移除所有泛型类型信息,替换为原始类型或上界,以确保与 Java旧版本的字节码兼容。
这样一来在运行时无法获取泛型的实际类型。
//无界泛型:被擦除为 Object //有界泛型: 被擦除为 Number
既然擦除了类型,为什么在运行期通过反射可以获得类型?
泛型信息会存在字节码元数据中,所以在运行期通过反射可以获得类型
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic{ private T key; public T getKey(){ return key; } } Generic genericInteger = new Generic (123456);
泛型接口
public interface Generator{ public T method(); } //实现泛型接口,不指定类型 class GeneratorImpl implements Generator { @Override public T method() { return null; } } //实现泛型接口,指定类型 class GeneratorImpl implements Generator { @Override public String method() { return "hello"; } }
extend T>
表示类型的上界, ? 这个类型要么是T,要么是T的子类
通常用于读取操作,确保可以读取为 T或T的子类的对象。
// 定义一个泛型方法,接受任何继承自Number的类型 publicvoid processNumber(T number) { double value = number.doubleValue(); //因为 number可以是任何Number类型或其子类型,所以可以使用Number的方法 // 其他操作... }
super T>
表示类型的下界,? 这个类型要么是T,要么是 T 的父类型,直至 Object
通常用于写入操作,确保可以安全地向泛型集合中插入 T 类型的对象
// 定义一个泛型方法,接受任何类型的List,并向其中添加元素 publicvoid addElements(List super T> list, T element) { //表明列表的参数类型至少是T的父类 list.add(element); // 其他操作... }
我们在使用上下界通配符
的时候,需要遵循 pecs 原则,即上界生产,下界消费。
PECS原则
Producer Extends : 从一个容器中获取数据时,应该使用 ? extends T
,确保读取的是T或其子类型
Consumer Super : 向一个容器中写入数据时,应该使用 ? super T
,确保写入的是T或其子类型