一、关于泛型
1.1 简介
Java泛型自J2SE 5.0引入,彻底改变了开发者处理数据类型的方式,将类型安全与代码复用推向新高度。
Java 泛型(Generics)是 Java 5 引入的一个特性,允许在类、接口和方法中使用类型参数,使得代码更加通用、灵活、可重用,并且能提供更强的类型安全性。
在 Java 中,泛型允许在定义类、接口或方法时使用类型参数,具体的类型在使用时再指定。泛型使得同一类或方法能够处理不同类型的数据,而无需编写多个版本的代码。
例如,使用泛型的集合类 List 可以存储不同类型的元素,T 代表类型参数。
1.2 发展
Java 泛型(Generics)的发展历程可以追溯到 Java 语言的早期版本。下面是 Java 泛型的主要发展过程:
1. Java 语言初期(1.0 - 1.4)
在 Java 的早期版本中,Java 并没有提供泛型机制。开发者只能使用原始类型(如 Object)来实现代码的通用性,这导致了以下两个主要问题:
- 类型安全问题: 由于 Java 不支持泛型,开发者必须使用 Object 来表示任何类型,这导致了大量的类型转换错误,增加了出错的机会。
- 代码冗余: 开发者不得不编写多个版本的代码来处理不同类型的数据,这使得代码冗长且不易维护。
例如,早期集合类如 Vector 和 ArrayList 只能存储 Object 类型的元素,需要在取出元素时进行强制类型转换:- Vector vector = new Vector();
- vector.add("Hello");
- String str = (String) vector.get(0); // 需要类型转换
复制代码 2. Java 5(2004年发布) - 引入泛型
Java 5(J2SE 5.0,也叫做 "Tiger")是引入泛型的关键版本。泛型成为 Java 的一项重要特性,它改变了 Java 语言的编程方式,使得类型安全性和代码可重用性得到了极大的提升。主要特性如下:
- 引入泛型语法: 使用 表示类型参数,例如 List 表示一个只能存储 String 类型的集合。
- 类型擦除机制: 泛型在编译时会被擦除(type erasure),即类型参数在编译后的字节码中不再保留具体类型信息。
- 通配符(Wildcard)与边界: 支持通配符(如 ?)和上界(extends)与下界(super)限定符,用于进一步限制泛型的类型范围。
- // 引入泛型的例子
- List<String> list = new ArrayList<>();
- list.add("Hello");
- String str = list.get(0); // 无需强制类型转换
复制代码 3. Java 6 和 Java 7(2006年和2011年发布)
Java 6 和 Java 7 主要对泛型进行了一些优化和增强,但没有引入重大变化。
- Java 6(Java 5 后的修复与优化): 在 Java 6 中,泛型得到了进一步的优化,减少了类型擦除带来的性能损失。
- Java 7(引入钻石操作符 ): Java 7 引入了钻石操作符(),它使得代码更加简洁,开发者不再需要显式地指定泛型类型,在编译时由编译器自动推断。
- // Java 7 引入的钻石操作符
- List<String> list = new ArrayList<>(); // 不再需要显式指定泛型类型
复制代码 4. Java 8(2014年发布)
Java 8 在泛型方面没有引入新特性,但它引入了与泛型紧密相关的其他功能,特别是 Lambda 表达式 和 Stream API。这两个特性使得开发者能够更简洁地处理集合和泛型数据,尤其是在函数式编程风格的代码中。
- Stream API:通过泛型支持,Stream API 可以对集合进行更复杂的操作,而无需显式地指定具体类型。
- List<String> list = Arrays.asList("A", "B", "C");
- list.stream().filter(s -> s.startsWith("A")).forEach(System.out::println);
复制代码 5. Java 9 及以后(2017年发布,后续版本)
Java 9 引入了模块化(Project Jigsaw),但并未对泛型进行重大更新。此后,Java 10、Java 11 等版本也未对泛型进行大的改动。
然而,Java 10 引入了 局部变量类型推断(var ) ,可以让编译器自动推断局部变量的类型,包括泛型类型。这使得在某些场景下,泛型的使用更加灵活和简洁。- // Java 10 引入的局部变量类型推断
- var list = new ArrayList<String>(); // 编译器推断类型
复制代码 6. 泛型的局限性与挑战
尽管泛型在 Java 中带来了很多好处,但也存在一些局限性和挑战:
- 类型擦除: 泛型信息在编译时会被擦除,导致 Java 在运行时无法获取泛型的具体类型。这限制了某些操作,如无法通过反射获得泛型类型。
- List<String> list = new ArrayList<>();
- if (list instanceof List<String>) { // 编译时错误,因为泛型信息被擦除
- // 代码无法运行
- }
复制代码 - 无法创建泛型数组: 由于类型擦除的机制,不能直接创建带有泛型类型的数组。例如,new T[] 是不允许的。
- // 编译错误
- T[] array = new T[10]; // 无法创建泛型数组
复制代码
1.3 功能特点
Java 泛型(Generics)是一种允许编写可以与多种数据类型一起工作的类、接口和方法的特性。它是在 Java 5 中引入的,旨在提高代码的类型安全性、可读性和重用性。以下是 Java 泛型的一些主要特点:
- 类型安全:
- 泛型提供编译时的类型检查,这意味着如果尝试将不兼容类型的对象插入到泛型集合中,编译器会报错,而不是在运行时抛出 ClassCastException 。
- 这种特性使得开发者能够在开发阶段捕获错误,而不需要等到程序运行时才发现问题。
- 消除强制类型转换:
- 使用泛型可以减少或完全避免显式的类型转换。例如,在没有使用泛型的情况下,从集合中获取元素通常需要进行强制类型转换;而在使用了泛型之后,这些转换是自动完成的,并且是隐式的 。
- 向后兼容:
- 泛型被设计为向后兼容的,因此现有的非泛型代码可以在新的支持泛型的环境中继续工作。这种兼容性是通过类型擦除实现的,即在编译期移除所有泛型类型信息 。
- 通配符:
- 泛型支持通配符 ? 来表示未知类型。这允许编写更加灵活的方法签名,比如接受任意类型的参数列表而不必指定具体的类型 。
- 有界类型参数:
- 泛型允许定义有界的类型参数,这意味着你可以限制传入的类型必须是某个特定类型或其子类。例如,你可以定义一个泛型方法只接受实现了 Comparable 接口的类型作为参数 。
- 类型擦除:
- 在Java中,泛型信息在编译时被移除,这个过程被称为类型擦除。这意味着泛型类型参数在运行时并不存在,所有的泛型类型都会被替换为其上限类型(通常是 Object),除非指定了下限 。
- 泛型不能用于基本数据类型:
- 泛型只能用于引用类型,如 Integer, String, 自定义类等,而不能直接用于基本数据类型如 int, char, boolean 等。不过,由于自动装箱/拆箱机制的存在,可以直接使用包装类来间接地处理基本数据类型 。
- 泛型类和泛型方法:
- 泛型不仅可以应用于类,也可以应用于单独的方法。这意味着你可以在不改变整个类的前提下,使单个方法具有泛型能力 。
- 性能提升:
- 使用泛型减少了不必要的装箱和拆箱操作,特别是在处理集合时,这可以带来一定的性能增益 。
- 复用性和灵活性:
- 泛型提高了代码的复用性,因为相同的逻辑可以适用于多种不同的数据类型,同时保持了类型的安全性 。
二、泛型语法
- 泛型类:指定类的类型参数。
- 泛型方法:指定方法的类型参数。
- 通配符(Wildcard) :用于表示不确定的类型。
2.1 泛型类
使用泛型定义类或接口时,需要在类名后面加上尖括号 < >,并在其中指定类型参数。
类型参数通常使用大写字母表示(如:T, E, K, V等)。常用T表示,是Type的缩写。
示例
- // 定义一个泛型类
- public class Box<T> {
- private T value;
- public T getValue() {
- return value;
- }
- public void setValue(T value) {
- this.value = value;
- }
- }
复制代码 使用泛型类时,指定实际类型:- Box<Integer> intBox = new Box<>();
- intBox.setValue(100);
- System.out.println(intBox.getValue()); // 输出100
- Box<String> strBox = new Box<>();
- strBox.setValue("Hello");
- System.out.println(strBox.getValue()); // 输出Hello
复制代码
2.2 泛型接口
接口定义类型参数,实现类需指定具体类型。
示例
- public interface List<T> {
- void add(T element);
- T get(int index);
- }
- // 实现类
- public class StringList implements List<String> {
- private ArrayList<String> elements = new ArrayList<>();
- @Override
- public void add(String element) {
- elements.add(element);
- }
- @Override
- public String get(int index) {
- return elements.get(index);
- }
- }
复制代码
2.3 泛型方法
泛型方法是指在方法中使用类型参数。它允许在方法中灵活地指定类型。
示例
泛型方法是指在方法中使用类型参数。它允许在方法中灵活地指定类型。- public <T> void printArray(T[] array) {
- for (T element : array) {
- System.out.println(element);
- }
- }
复制代码 调用时可以传入不同类型的数组:- Integer[] intArray = {1, 2, 3};
- String[] strArray = {"A", "B", "C"};
- printArray(intArray); // 输出 1 2 3
- printArray(strArray); // 输出 A B C
复制代码 - package org.example.Generics;
- public class MethodTest {
- public static void main(String[] args) {
- Integer[] intArray = {1, 2, 3};
- String[] strArray = {"A", "B", "C"};
- MethodTest test = new MethodTest();
- test.printArray(intArray);
- test.printArray(strArray);
- }
- public <T> void printArray(T[] array) {
- for (T element : array) {
- System.out.println(element);
- }
- }
- }
复制代码
2.4 通配符(Wildcard)
泛型的通配符 ? 用于增强灵活性,常见于方法参数或返回值。
- ? extends T:表示类型是 T 或 T 的子类型。
- ? super T:表示类型是 T 或 T 的父类型。
- ?:表示未知类型。
1. 无界通配符
匹配任意类型,但只能读取数据,无法写入。- public void printList(List<?> list) {
- for (Object elem : list) {
- System.out.println(elem);
- }
- }
复制代码
2. 上界通配符 |