1. 引言
1.1 什么是泛型
泛型(Generics)是Java 5引入的一个重要特性,它允许类、接口和方法操作未知类型的对象。通过使用泛型,我们可以编写更加通用、类型安全的代码,同时保持代码的简洁性和可读性。
泛型本质上是一种"代码模板",它使用类型参数来表示类型,这些类型参数可以在实际使用时被实际的类型替换。这样,一份代码可以适用于多种不同的数据类型,而不需要为每种数据类型编写单独的实现。
简单来说,泛型就是允许我们在定义类、接口和方法时使用类型参数(type parameters),这些类型参数稍后会被用来指定具体的类型(实际类型参数)。
// 没有泛型的情况下,我们需要处理Object类型
List listWithoutGenerics = new ArrayList();
listWithoutGenerics.add("Hello");
listWithoutGenerics.add(123); // 可以添加任何类型的对象
// 使用时需要强制类型转换,且容易出错
String s = (String) listWithoutGenerics.get(0); // 正确
String s2 = (String) listWithoutGenerics.get(1); // 运行时错误:ClassCastException
// 使用泛型
List<String> listWithGenerics = new ArrayList<>();
listWithGenerics.add("Hello");
// listWithGenerics.add(123); // 编译错误,只能添加String类型
String s3 = listWithGenerics.get(0); // 不需要强制类型转换
1.2 为什么需要泛型
在Java 5之前,Java集合框架(如List、Set、Map等)只能存储Object类型的对象。这带来了两个主要问题:
- 类型安全问题:可以将任何类型的对象添加到集合中,容易引入类型不匹配的错误。
- 类型转换的繁琐:从集合中获取元素时需要进行显式的类型转换,既麻烦又容易出错。
下面通过一个简单的例子来说明:
// Java 5之前的代码
List names = new ArrayList();
names.add("张三");
names.add("李四");
names.add(100); // 可以添加任何类型,编译器不会检查
// 获取元素时需要类型转换
String name = (String) names.get(0);
// 如果忘记元素的实际类型,可能引发运行时错误
String anotherName = (String) names.get(2); // 运行时抛出ClassCastException
上面的代码在运行时会抛出ClassCastException
,因为我们试图将一个Integer对象转换为String类型。而且,这种错误只能在运行时才能被发现,在编译时无法检测。
泛型的引入解决了这些问题:
// 使用泛型的代码
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
// names.add(100); // 编译错误,类型安全
// 不需要类型转换
String name = names.get(0);
String anotherName = names.get(1);
使用泛型后,编译器会确保只有String类型的对象才能被添加到names列表中,这样就避免了类型不匹配的运行时错误。同时,从列表中获取元素时不再需要显式的类型转换,代码更加简洁。
1.3 泛型的优势
泛型的引入带来了诸多优势:
- 类型安全:编译器可以在编译时检查类型约束,防止类型错误。
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
// numbers.add("三"); // 编译错误:不兼容的类型
- 消除类型转换:从泛型集合中获取元素时不需要进行显式的类型转换。
List<Integer> numbers = new ArrayList<>();
numbers.add(100);
Integer number = numbers.get(0); // 不需要类型转换
- 代码重用:通过使用类型参数,同一段代码可以操作不同类型的对象。
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 可以用于各种类型
Box<Integer> intBox = new Box<>();
Box<String> stringBox = new Box<>();
Box<Double> doubleBox = new Box<>();
- 提高代码可读性:通过在编译时指定类型,使代码更易于理解和维护。
// 没有泛型时,需要注释或文档说明类型
Map customerOrders = new HashMap();
// 使用泛型,类型信息一目了然
Map<Customer, List<Order>> customerOrders = new HashMap<>();
- 支持泛型算法:可以编写适用于不同类型的通用算法,而无需为每种类型重新实现。
public static <T extends Comparable<T>> T findMax(List<T> list) {
if (list.isEmpty()) {
return null;
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// 可用于不同类型
List<Integer> numbers = Arrays.asList(1, 5, 3, 9, 7);
Integer maxNumber = findMax(numbers); // 返回9
List<String> words = Arrays.asList("apple", "orange", "banana");
String maxWord = findMax(words); // 返回"orange"(按字典序)
总之,泛型使Java代码更加类型安全、简洁和灵活,同时提高了代码的可读性和可维护性。这些优势使泛型成为Java语言的一个重要特性,广泛应用于Java库和应用程序开发中。
2. 泛型基础
2.1 泛型类
泛型类是指在类声明中使用一个或多个类型参数的类。这些类型参数在类使用时可以被替换成具体的类型。泛型类的定义格式如下:
public class ClassName<T> {
// T是类型参数,可以在类的定义中使用
private T field;
public void setField(T field) {
this.field = field;
}
public T getField() {
return field;
}
}
泛型类的典型例子是Java的集合类,如ArrayList<E>
、HashMap<K, V>
等,其中E
、K
、V
都是类型参数。
下面是一个简单的泛型类示例:
// 定义一个泛型类Box,可以存储任何类型的单个对象
public class Box<T> {
private T content;
public Box() {
}
public Box(T content) {
this.content = content;
}
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public boolean hasContent() {
return content != null;
}
}
// 使用泛型类
public class BoxDemo {
public static void main(String[] args) {
// 创建一个存储Integer的Box
Box<Integer> intBox = new Box<>();
intBox.setContent(100);
Integer intValue = intBox.getContent(); // 不需要类型转换
System.out.println("整数值:" + intValue);
// 创建一个存储String的Box
Box<String> stringBox = new Box<>("Hello Generics");
String strValue = stringBox.getContent();
System.out.println("字符串值:" + strValue);
// 创建一个存储自定义类型的Box
Box<Person> personBox = new Box<>();
personBox.setContent(new Person("张三", 30));
Person person = personBox.getContent();
System.out.println("姓名:" + person.getName() + ",年龄:" + person.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name; }
public int getAge() {
return age; }
}
可以看到,使用相同的Box类,我们可以处理不同类型的数据,而不需要为每种类型创建单独的类。这体现了泛型的代码重用能力。
多个类型参数
泛型类可以有多个类型参数,各个类型参数之间用逗号分隔:
// 定义一个具有两个类型参数的键值对类
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key; }
public V getValue() {
return value; }
public void setKey(K key) {
this.key = key; }
public void setValue(V value) {
this.value = value; }
@Override
public String toString() {
return "(" + key + ", " + value + ")";
}
}
// 使用多类型参数的泛型类
public class PairDemo {
public static void main(String[] args) {
// 创建一个String-Integer对
Pair<String, Integer> score = new Pair<>("张三", 95);
System.out.println(score.getKey() + "的分数是:" + score.getValue());
// 创建一个String-Double对
Pair<String, Double> price = new Pair<>("苹果", 5.99);
System.out.println(price.getKey() + "的价格是:$" + price.getValue());
}
}
2.2 泛型方法
泛型方法是在方法声明中使用类型参数的方法。泛型方法可以定义在普通类中,也可以定义在泛型类中。泛型方法的类型参数独立于所在类的类型参数。
泛型方法的定义格式如下:
public <T> returnType methodName(T param) {
// 方法体
}
泛型方法的类型参数列表(如上面的<T>
)位于方法返回类型之前。
下面是一些泛型方法的例子:
public class GenericMethodExample {
// 泛型方法,打印任何类型的数组
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// 返回两个值中较大的一个(需要参数类型实现Comparable接口)
public static <T extends Comparable<T>> T findMax(T first, T second) {
int result = first.compareTo(second);
return result >= 0 ? first : second;
}
// 在列表中查找特定元素,返回其索引,不存在则返回-1
public static <T> int findElement(List<T> list, T element) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(element)) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
// 使用printArray方法
Integer[] intArray = {
1, 2, 3, 4, 5};
String[] strArray = {
"Hello", "World", "Generics"};
System.out.println("整数数组:");
printArray(intArray);
System.out.println("字符串数组:");
printArray(strArray);
// 使用findMax方法
System.out.println("较大的数:" + findMax(10, 20));
System.out.println("字典序较大的字符串:" + findMax("apple", "orange"));
// 使用findElement方法
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
System.out.println("'orange'的索引:" + findElement(fruits, "orange"));
System.out.println("'watermelon'的索引:" + findElement(fruits, "watermelon"));
}
}
当调用泛型方法时,通常不需要显式指定类型参数,因为Java编译器可以通过类型推断确定类型参数。但在某些情况下,可能需要显式指定类型参数:
List<String> list = new ArrayList<>();
// 显式指定类型参数
GenericMethodExample.<String>findElement(list, "apple");
2.3 泛型接口
泛型接口是在接口声明中使用类型参数的接口。实现泛型接口的类需要指定接口的类型参数。
泛型接口的定义格式如下:
public interface InterfaceName<T> {
// 接口方法
void method(T t);
T getResult();
}
下面是一个泛型接口的例子:
// 定义一个泛型接口
public interface Processor<T> {
T process(T input);
boolean isValid(T input);
}
// 实现泛型接口,指定类型参数为String
public class StringProcessor implements Processor<String> {
@Override
public String process(String input) {
return input.toUpperCase();
}
@Override
public boolean isValid(String input) {
return input != null && !input.isEmpty();
}
}
// 实现泛型接口,指定类型参数为Integer
public class NumberProcessor implements Processor<Integer> {
@Override
public Integer process(Integer input) {
return input * 2;
}
@Override
public boolean isValid(Integer input) {
return input != null && input >= 0;
}
}
// 演示泛型接口的使用
public class ProcessorDemo {
public static void main(String[] args) {
Processor<String> stringProc = new StringProcessor();
String input = "hello";
if (stringProc.isValid(input)) {
System.out.println("处理结果:" + stringProc.process(input));
}
Processor<Integer> numberProc = new NumberProcessor();
Integer num = 10;
if (numberProc.isValid(num)) {
System.out.println("处理结果:" + numberProc.process(num));
}
}
}
也可以定义一个泛型类来实现泛型接口,这样可以在创建类实例时指定接口的类型参数:
// 泛型类实现泛型接口
public class GenericProcessor<T> implements Processor<T> {
private Function<T, T> processFunction;
private Predicate<T> validationFunction;
public GenericProcessor(Function<T, T> processFunction, Predicate<T> validationFunction) {
this.processFunction = processFunction;
this.validationFunction = validationFunction;
}
@Override
public T process(T input) {
return processFunction.apply(input);
}
@Override
public boolean isValid(T input) {
return validationFunction.test(input);
}
}
// 使用泛型类实现的泛型接口
public class FlexibleProcessorDemo {
public static void main(String[] args) {
// 创建一个处理String的处理器
Processor<String> stringProc = new GenericProcessor<>(
s -> s.toUpperCase(),
s -> s != null && !s.isEmpty()
);
// 创建一个处理Integer的处理器
Processor<Integer> intProc = new GenericProcessor<>(
i -> i * i,
i -> i != null && i >= 0
);
System.out.println("字符串处理:" + stringProc.process("hello"));
System.out.println("数字处理:" + intProc.process(5));
}
}
2.4 类型参数命名约定
在Java中,泛型类型参数的命名有一些常见的约定。这些约定不是强制性的,但遵循这些约定可以使代码更易读和理解:
- 单个大写字母:通常使用单个大写字母来表示类型参数,这是最常见的约定。
常见的类型参数名称有:
T
- Type(类型),最常用的类型参数名E
- Element(元素),常用于集合类,如List<E>
K
- Key(键),常用于映射中的键V
- Value(值),常用于映射中的值N
- Number(数字),表示数字类型S
,U
,V
等 - 用于表示多个类型参数时的第2, 3, 4个类型参数
- 描述性名称:在某些情况下,尤其是当类型参数有特定含义时,可以使用更有描述性的名称。
public class CustomMap<KeyType, ValueType> {
// 使用有描述性的名称
}
public class DataProcessor<InputType, OutputType> {
// 使用有描述性的名称
}
- 类型参数的使用约定:
// 定义泛型接口
public interface Repository<T> {
T findById(long id);
List<T> findAll();
void save(T entity);
}
// 定义泛型类
public class Box<T> {
private T content;
public T getContent() {
return content;
}
}
// 定义泛型方法
public <T> T firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
遵循这些命名约定可以使代码更加一致和易于理解,尤其是当其他开发人员阅读你的代码时。
3. 类型擦除
3.1 什么是类型擦除
类型擦除是Java泛型实现的关键机制。简单来说,Java的泛型是在编译时由编译器实现的,在运行时,所有的泛型信息都会被"擦除",这就是类型擦除(Type Erasure)。
类型擦除的基本原则是:
- 将所有的泛型类型参数替换为它们的边界或者
Object
(如果没有指定边界)。 - 必要时插入类型转换以保证类型安全。
- 生成桥接方法(bridge methods)以保持多态性。
下面通过一个简单的例子说明类型擦除:
// 原始泛型代码
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 经过类型擦除后的等效代码
public class Box {
private Object content;
public void setContent(Object content) {
this.content = content;
}
public Object getContent() {
return content;
}
}
如上所示,编译器会将泛型类型参数T
替换为Object
(因为T
没有指定边界),并在必要的地方添加类型转换。这样,Box<String>
和Box<Integer>
在经过类型擦除后都会变成相同的类。
如果类型参数有边界,则会被替换为边界类型:
// 带有边界的泛型代码
public class Box<T extends Number> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 经过类型擦除后的等效代码
public class Box {
private Number content;
public void setContent(Number content) {
this.content = content;
}
public Number getContent() {
return content;
}
}
3.2 类型擦除的影响
类型擦除虽然简化了Java泛型的实现,但也带来了一些影响和限制:
1. 无法获取泛型类型参数的实际类型
由于类型擦除,在运行时无法获知泛型类型参数的实际类型:
public class TypeErasureExample {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();
// 两者的类是相同的
System.out.println(stringBox.getClass() == intBox.getClass()); // 输出:true
// 无法获取类型参数的实际类型
System.out.println(stringBox.getClass().getName()); // 输出:Box
}
}
2. 无法创建泛型类型的数组
由于类型擦除,无法直接创建泛型类型的数组:
// 这行代码会导致编译错误
Box<Integer>[] boxArray = new Box<Integer>[10]; // 编译错误:不能创建泛型数组
// 可以通过通配符来创建
Box<?>[] wildcardBoxArray = new Box<?>[10]; // 这是允许的
3. 无法使用instanceof
检查泛型类型
无法使用instanceof
运算符检查对象是否为特定泛型类型的实例:
Box<Integer> intBox = new Box<>();
// 这行代码会导致编译错误
if (intBox instanceof Box<Integer>) {
// 编译错误:不能使用参数化类型的instanceof
// ...
}
// 只能检查原始类型
if (intBox instanceof Box) {
// 这是允许的
// ...
}
4. 泛型类的静态上下文中不能使用类型参数
public class StaticContext<T> {
// 编译错误:无法在静态上下文中使用类型参数T
private static T staticField;
// 编译错误:无法在静态方法中使用类型参数T
public static T getStaticValue() {
return null;
}
// 编译错误:无法在静态块中使用类型参数T
static {
T temp = null;
}
// 这是允许的:泛型方法可以是静态的,因为它有自己的类型参数
public static <E> E getStaticValue(E value) {
return value;
}
}
5. 异常类不能是泛型的
// 编译错误:泛型类不能扩展Throwable
public class GenericException<T> extends Exception {
// 编译错误
private T data;
public GenericException(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
3.3 桥接方法
桥接方法(Bridge Methods)是Java编译器在类型擦除过程中生成的特殊方法,用于保持泛型类型的多态性。
考虑以下情况:
public class Node<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
public class