Java泛型详解

文章目录

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类型的对象。这带来了两个主要问题:

  1. 类型安全问题:可以将任何类型的对象添加到集合中,容易引入类型不匹配的错误。
  2. 类型转换的繁琐:从集合中获取元素时需要进行显式的类型转换,既麻烦又容易出错。

下面通过一个简单的例子来说明:

// 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 泛型的优势

泛型的引入带来了诸多优势:

  1. 类型安全:编译器可以在编译时检查类型约束,防止类型错误。
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
// numbers.add("三"); // 编译错误:不兼容的类型
  1. 消除类型转换:从泛型集合中获取元素时不需要进行显式的类型转换。
List<Integer> numbers = new ArrayList<>();
numbers.add(100);
Integer number = numbers.get(0); // 不需要类型转换
  1. 代码重用:通过使用类型参数,同一段代码可以操作不同类型的对象。
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<>();
  1. 提高代码可读性:通过在编译时指定类型,使代码更易于理解和维护。
// 没有泛型时,需要注释或文档说明类型
Map customerOrders = new HashMap();
// 使用泛型,类型信息一目了然
Map<Customer, List<Order>> customerOrders = new HashMap<>();
  1. 支持泛型算法:可以编写适用于不同类型的通用算法,而无需为每种类型重新实现。
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>等,其中EKV都是类型参数。

下面是一个简单的泛型类示例:

// 定义一个泛型类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中,泛型类型参数的命名有一些常见的约定。这些约定不是强制性的,但遵循这些约定可以使代码更易读和理解:

  1. 单个大写字母:通常使用单个大写字母来表示类型参数,这是最常见的约定。

常见的类型参数名称有:

  • T - Type(类型),最常用的类型参数名
  • E - Element(元素),常用于集合类,如List<E>
  • K - Key(键),常用于映射中的键
  • V - Value(值),常用于映射中的值
  • N - Number(数字),表示数字类型
  • S, U, V 等 - 用于表示多个类型参数时的第2, 3, 4个类型参数
  1. 描述性名称:在某些情况下,尤其是当类型参数有特定含义时,可以使用更有描述性的名称。
public class CustomMap<KeyType, ValueType> {
   
    // 使用有描述性的名称
}

public class DataProcessor<InputType, OutputType> {
   
    // 使用有描述性的名称
}
  1. 类型参数的使用约定
// 定义泛型接口
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)。

类型擦除的基本原则是:

  1. 将所有的泛型类型参数替换为它们的边界或者Object(如果没有指定边界)。
  2. 必要时插入类型转换以保证类型安全。
  3. 生成桥接方法(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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值