目录​​​​​​​

前言

一、泛型的基本概念与语法

1.1 泛型类的定义

1.2 泛型类的使用

1.3 类型推导

二、泛型的实现原理:类型擦除

2.1 什么是类型擦除?

2.2 类型擦除带来的限制

1. 不能创建泛型数组

2. 通过反射创建泛型数组

2.3 为什么选择类型擦除?

三、泛型的上界(类型边界)

3.1 单一上界

3.2 多重上界

3.3 上界通配符

四、泛型方法

4.1 泛型方法的定义

4.2 泛型方法的使用

4.3 泛型方法与泛型类的区别

五、通配符及其应用

5.1 无界通配符

5.2 上界通配符

5.3 下界通配符

5.4 PECS原则

六、泛型在实际开发中的应用

6.1 集合框架

6.2 设计模式中的泛型

工厂模式

策略模式

6.3 工具类开发

七、泛型的限制与注意事项

7.1 不能使用基本类型

7.2 不能创建泛型实例

7.3 静态上下文中不能使用类型参数

7.4 不能使用instanceof检查泛型类型

总结

泛型的优点


前言

在Java编程中,我们经常需要编写可以处理多种数据类型的代码。在泛型出现之前,我们通常使用Object类型来实现"通用"编程,但这种方式存在明显的缺陷:类型不安全需要显式强制类型转换

想象一下,你正在开发一个容器类,需要存储各种类型的数据。在没有泛型的情况下,你可能会这样写:

public class Container {
    private Object item;
    
    public void setItem(Object item) {
        this.item = item;
    }
    
    public Object getItem() {
        return item;
    }
}

// 使用时
Container container = new Container();
container.setItem("Hello");  // 存储字符串
String str = (String) container.getItem();  // 需要强制转换

这种方法的问题在于:

  1. 编译时无法检测类型错误

  2. 运行时强制转换可能导致ClassCastException

  3. 代码可读性和安全性较差

Java泛型(Generics)在JDK 1.5中引入,解决了这些问题。泛型的核心思想是:参数化类型,即在定义类、接口或方法时,使用一个或多个类型形参,在使用时指定具体的类型实参。


一、泛型的基本概念与语法

1.1 泛型类的定义

泛型类在类名后面使用尖括号<>声明类型参数:

public class Box<T> {
    private T content;
    
    public Box(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
    
    public void setContent(T content) {
        this.content = content;
    }
}

这里,T是一个类型形参(Type Parameter),它只是一个占位符,表示某种类型。按照约定,类型形参通常使用单个大写字母表示:

  • T - Type(类型)

  • E - Element(元素)

  • K - Key(键)

  • V - Value(值)

  • N - Number(数字)

1.2 泛型类的使用

使用泛型类时,需要指定具体的类型实参:

// 指定类型实参为String
Box<String> stringBox = new Box<>("Hello");
String content = stringBox.getContent();  // 无需强制转换

// 指定类型实参为Integer
Box<Integer> integerBox = new Box<>(42);
int value = integerBox.getContent();  // 自动拆箱

1.3 类型推导

Java编译器可以根据上下文推导类型实参,因此可以省略构造器中的类型实参:

// 完整写法
Box<String> box1 = new Box<String>("Hello");

// 简化写法(类型推导)
Box<String> box2 = new Box<>("Hello");  // 编译器推导出String

二、泛型的实现原理:类型擦除

2.1 什么是类型擦除?

Java泛型是通过类型擦除(Type Erasure)实现的。这意味着在编译期间,所有的泛型类型信息都会被擦除,替换为它们的原始类型(Raw Type)。

// 编译前
public class Box<T> {
    private T content;
    // ...
}

// 编译后(字节码层面)
public class Box {
    private Object content;
    // ...
}

可以通过查看字节码验证这一点:

javap -c Box.class

2.2 类型擦除带来的限制

由于类型擦除,泛型在使用上有一些限制:

1. 不能创建泛型数组
// 错误:不能创建泛型数组
T[] array = new T[10];  // 编译错误

// 正确的方式:使用Object数组并强制转换
T[] array = (T[]) new Object[10];

但是,强制转换的方式并不安全,因为运行时类型信息已被擦除:

public class GenericArray<T> {
    private T[] array;
    
    public GenericArray(int size) {
        // 编译时通过,但运行时可能出现ClassCastException
        array = (T[]) new Object[size];
    }
    
    public T[] getArray() {
        return array;  // 这里可能有问题
    }
}

// 使用时
GenericArray<Integer> intArray = new GenericArray<>(10);
Integer[] array = intArray.getArray();  // 运行时ClassCastException
2. 通过反射创建泛型数组

如果需要创建确切的泛型类型数组,可以使用反射:

public class SafeGenericArray<T> {
    private T[] array;
    
    @SuppressWarnings("unchecked")
    public SafeGenericArray(Class<T> clazz, int size) {
        // 使用反射创建确切类型的数组
        array = (T[]) Array.newInstance(clazz, size);
    }
    
    public T[] getArray() {
        return array;
    }
}

// 使用时
SafeGenericArray<Integer> intArray = new SafeGenericArray<>(Integer.class, 10);
Integer[] array = intArray.getArray();  // 安全

2.3 为什么选择类型擦除?

Java选择类型擦除的主要目的是向后兼容。泛型是在JDK 1.5中引入的,而Java已经有了大量的非泛型代码。通过类型擦除,可以确保泛型代码与非泛型代码的二进制兼容性。

三、泛型的上界(类型边界)

有时我们需要限制泛型类型参数的范围,这就是类型边界(Type Bounds)。

3.1 单一上界

public class NumberBox<T extends Number> {
    private T number;
    
    public NumberBox(T number) {
        this.number = number;
    }
    
    public double doubleValue() {
        return number.doubleValue();  // 可以调用Number的方法
    }
}

// 可以这样用
NumberBox<Integer> intBox = new NumberBox<>(10);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("Hello");  // 编译错误

3.2 多重上界

类型参数可以有多个上界,但类必须放在第一位,接口放在后面:

public class MultiBound<T extends Number & Comparable<T> & Serializable> {
    private T value;
    // ...
}

3.3 上界通配符

上界通配符使用? extends Type的语法,表示可以接受Type或其子类:

public void processNumbers(List<? extends Number> numbers) {
    for (Number n : numbers) {
        System.out.println(n.doubleValue());
    }
}

// 可以接受各种Number子类的List
processNumbers(new ArrayList<Integer>());
processNumbers(new ArrayList<Double>());

重要特性:上界通配符的集合是只读的(确切地说,不能添加除null以外的元素):

List<? extends Number> numbers = new ArrayList<Integer>();
// numbers.add(new Integer(10));  // 编译错误
// numbers.add(new Double(3.14)); // 编译错误
numbers.add(null);  // 只有null可以添加

这是因为编译器无法确定?的具体类型,所以为了类型安全,不允许添加元素。

四、泛型方法

除了泛型类,我们还可以定义泛型方法。泛型方法可以在普通类、泛型类或接口中定义。

4.1 泛型方法的定义

public class ArrayUtils {
    // 静态泛型方法
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // 非静态泛型方法
    public <E> E getFirstElement(List<E> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }
}

4.2 泛型方法的使用

// 使用类型推导
Integer[] numbers = {1, 2, 3, 4, 5};
ArrayUtils.swap(numbers, 0, 4);  // 交换第一个和最后一个元素

// 显式指定类型参数
ArrayUtils.<Integer>swap(numbers, 1, 3);

// 使用泛型方法
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String first = new ArrayUtils().getFirstElement(names);

4.3 泛型方法与泛型类的区别

  1. 作用域不同:泛型类的类型参数作用于整个类,而泛型方法的类型参数只作用于该方法

  2. 类型参数可以不同:泛型类中的泛型方法可以使用自己的类型参数,与类的类型参数不同

public class Pair<K, V> {
    private K key;
    private V value;
    
    // 泛型方法,使用自己的类型参数T
    public <T> void printType(T item) {
        System.out.println(item.getClass().getName());
    }
}

五、通配符及其应用

通配符?是泛型中一个强大的特性,它提供了更灵活的类型匹配。

5.1 无界通配符

无界通配符?表示可以匹配任何类型:

public void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

// 可以接受任何类型的List
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());
printList(new ArrayList<Object>());

5.2 上界通配符

前面已经介绍过,上界通配符? extends Type用于生产者(Producer),即提供数据的场景。

5.3 下界通配符

下界通配符使用? super Type语法,表示可以接受Type或其父类:

public void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);  // 可以添加Integer及其子类
    }
}

// 可以传入各种父类List
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());
addNumbers(new ArrayList<Object>());

重要特性:下界通配符的集合是可写的,但读取时只能作为Object类型:

List<? super Integer> list = new ArrayList<Number>();
list.add(10);  // 可以添加Integer
list.add(20);  // 可以添加Integer
// Integer num = list.get(0);  // 编译错误,不能直接读取为Integer
Object obj = list.get(0);  // 只能读取为Object

5.4 PECS原则

PECS(Producer Extends, Consumer Super)原则是使用通配符的重要指导原则:

  • 生产者(Producer):如果你需要从一个泛型集合中读取数据(生产数据),使用       ? extends T

  • 消费者(Consumer):如果你需要向一个泛型集合中写入数据(消费数据),使用     ? super T

  • 既是生产者又是消费者:如果既要读又要写,不要使用通配符

示例:

// 生产者示例:从源集合复制到目标集合
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
    for (T item : src) {
        dest.add(item);
    }
}

// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Number> numList = new ArrayList<>();
copy(intList, numList);  // 正确:Integer是Number的子类

六、泛型在实际开发中的应用

6.1 集合框架

Java集合框架是泛型最典型的应用:

// 类型安全的集合
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123);  // 编译错误,类型安全

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
int aliceScore = scores.get("Alice");  // 无需强制转换

6.2 设计模式中的泛型

工厂模式
public interface Factory<T> {
    T create();
}

public class StringFactory implements Factory<String> {
    @Override
    public String create() {
        return new String();
    }
}

public class IntegerFactory implements Factory<Integer> {
    @Override
    public Integer create() {
        return new Integer(0);
    }
}
策略模式
public interface Comparator<T> {
    int compare(T o1, T o2);
}

public class StringLengthComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

6.3 工具类开发

public class CollectionUtils {
    // 安全的空集合
    public static <T> List<T> emptyList() {
        return Collections.emptyList();
    }
    
    // 转换数组为列表
    public static <T> List<T> asList(T... items) {
        return Arrays.asList(items);
    }
    
    // 查找最大值
    public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
        if (coll == null || coll.isEmpty()) {
            return null;
        }
        Iterator<T> i = coll.iterator();
        T candidate = i.next();
        while (i.hasNext()) {
            T next = i.next();
            if (next.compareTo(candidate) > 0) {
                candidate = next;
            }
        }
        return candidate;
    }
}

七、泛型的限制与注意事项

7.1 不能使用基本类型

泛型类型参数必须是引用类型,不能是基本类型:

// 错误
List<int> intList;  // 编译错误

// 正确
List<Integer> integerList;  // 使用包装类

7.2 不能创建泛型实例

由于类型擦除,不能在运行时创建泛型类型的实例:

public class Example<T> {
    public void doSomething() {
        // T obj = new T();  // 编译错误
        // T[] array = new T[10];  // 编译错误
    }
}

7.3 静态上下文中不能使用类型参数

静态成员不能使用类的类型参数:

public class Box<T> {
    // private static T staticField;  // 编译错误
    // public static T staticMethod() { return null; }  // 编译错误
    
    // 但静态泛型方法可以使用自己的类型参数
    public static <E> E staticGenericMethod(E param) {
        return param;
    }
}

7.4 不能使用instanceof检查泛型类型

由于类型擦除,不能在运行时检查泛型类型:

public <T> void checkType(Object obj) {
    // if (obj instanceof T) {  // 编译错误
    //     // ...
    // }
    
    // 但可以检查原始类型
    if (obj instanceof Box) {
        Box<?> box = (Box<?>) obj;
        // ...
    }
}

总结

泛型的优点

  1. 类型安全:编译时检查类型,减少运行时错误

  2. 代码复用:编写一次,适用于多种类型

  3. 消除强制转换:使代码更简洁、清晰

  4. 更好的API设计:提供更灵活、更安全的API

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐