1️⃣ 概念

Java 编程语言在 JDK 5.0 版本中引入了泛型(Generics)的概念,以增加源代码的类型安全性和可读性。泛型允许类、接口和方法在定义时使用一个或多个类型参数,使得它们可以在编译时具有更强的类型检查,并且能够避免类型转换错误。

泛型的核心思想是参数化类型(Parameterized Type)。通过在类、接口或方法的定义中使用类型参数,我们可以创建一种“模板”,这个模板可以用于不同的数据类型,并在编译时进行类型检查。这个类型参数可以在实例化的时候指定具体的类型,从而实现类型安全。

2️⃣ 优势

使用泛型的主要优势是提供了类型安全和可读性方面的好处:

  • 类型安全:使用泛型可以在编译时捕获类型错误。通过在编译时对传递给泛型容器或方法的元素类型进行检查,可以避免在运行时发生意外的 ClassCastException
  • 可读性:通过明确指定类型参数,可以使代码更易读和理解。方法或类命名更清晰,并且减少了需要对类型进行注释或文档说明的情况;
  • 代码复用:泛型可以使用相同的类型安全体系来操作多种不同类型的数据,从而提高代码的可重用性;
  • 简化开发:使用泛型可以减少冗余的类型转换代码,提供更简洁和优雅的编程方式;
  • 错误检测:泛型使得编译器能够在编译时对代码进行更严格的类型检查,减少错误的产生,并会提供更精确的错误提示。

3️⃣ 使用

3.1 泛型类

在Java中,我们可以创建泛型类和泛型接口。泛型类和泛型接口的定义方式与普通类和接口相似,只是在名称后面加上一对尖括号(<>)并在其中指定类型参数。

下面是一个简单的示例,展示了如何定义一个泛型类:

public class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return "Box{" +
                "value=" + value +
                '}';
    }
}

在这个示例中,Box 是一个泛型类,类型参数 T 被放置在类名后的尖括号中。在类内部,我们可以使用类型参数 T 来声明成员变量和方法的类型,并在实例化时指定具体的类型。

以下是使用该泛型类的使用示例:

public class GenericClassDemo {

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.setValue(10);
        System.out.println(integerBox);

        Box<String> stringBox = new Box<>();
        stringBox.setValue("hello");
        System.out.println(stringBox);
    }
}

通过在实例化 Box 类时指定了具体的类型参数,我们可以创建不同类型的 Box 实例,并将不同类型的值存储在其中。

输出结果:

Box{value=10}
Box{value=hello}

3.2 泛型接口

下面是一个简单的示例,定义了一个泛型接口Container<T>,其中T表示泛型类型参数。接口中有两个方法:addItem用于添加一个元素到容器中,getItem用于获取容器中的元素:

public interface Container<T> {
    void addItem(T[] item);  // 添加一组元素到容器中
    T[] getItem();           // 获取容器中的元素
}

以下是一个实现了该泛型接口的具体类Box<T>,该类通过实现接口的方式来定义接口中的方法:

import java.util.Arrays;

// 实现泛型接口
public class Box<T> implements Container<T> {
    private T[] item;

    public void addItem(T[] item) {
        this.item = item;
    }

    public T[] getItem() {
        return item;
    }

    @Override
    public String toString() {
        return "Box{" +
                "item=" + Arrays.toString(item) +
                '}';
    }
}

以下是使用该泛型接口的使用示例:

public class GenericsInterfaceDemo {

    public static void main(String[] args) {
        // 使用泛型接口
        Container<String> container1 = new Box<>();
        container1.addItem(new String[]{"Hello","world"});
        System.out.println(container1);

        Container<Integer> container2 = new Box<>();
        container2.addItem(new Integer[]{42,36});
        System.out.println(container2);
    }
}

main函数中,我创建了两个泛型容器对象container1container2,分别使用了泛型实参StringInteger。然后,调用addItem方法将一组元素添加到容器中,最后,将容器对象的元素进行打印输出。

运行结果:

Box{item=[Hello, world]}
Box{item=[42, 36]}

3.3 泛型方法

除了在类和接口级别上定义泛型之外,Java 还允许在单独的方法中使用泛型。这被称为泛型方法(Generic Method)。

下面是一个简单的示例,展示了如何定义泛型方法:

public class ArrayUtils {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

使用泛型方法:

public class GenericMethodDemo {
    public static void main(String[] args) {
        // 使用泛型方法
        Integer[] intArray = {1, 2, 3, 4, 5};
        ArrayUtils.printArray(intArray);

        String[] strArray = {"Hello", "World"};
        ArrayUtils.printArray(strArray);
    }
}

在这个示例中,printArray 是一个泛型方法。类型参数 T 被放置在方法的返回类型之前的尖括号中,它表示任意类型。通过在调用泛型方法时传递相应的实际参数类型,编译器会对数组进行相应的类型检查,并执行相应的方法体。

运行结果:

1 2 3 4 5 
Hello World 

4️⃣ 通配符(Wildcards)

在泛型中,可以使用通配符来表示一组类型。通配符允许我们在泛型代码中处理不同类型的参数。

Java 提供了三种通配符:? 无界通配符(Unbounded Wildcard)、? extends T 上限通配符(Upper Bounded Wildcard)和 ? super T 下限通配符(Lower Bounded Wildcard)。

  • 无界通配符(?):用于表示未知类型。例如,List<?> 表示一个具有未知元素类型的列表;
  • 上限通配符(? extends T):用于限制传入的类型必须是某个类或其子类。例如,List<? extends Number> 表示一个元素类型是 Number 或其子类的列表;
  • 下限通配符(? super T):用于限制传入的类型必须是某个类或其父类。例如,List<? super Integer> 表示一个元素类型是 Integer 或其父类的列表。

4.1 无界通配符(Unbounded Wildcard)

无界通配符(?) 允许我们对未知类进行操作,并在类型安全的前提下进行编码。

以下是一个示例,展示了如何使用无界通配符:

import java.util.ArrayList;
import java.util.List;

//一个使用了泛型的无界通配符的演示程序
public class UnboundedWildcardDemo {

    static class Animal {
        public void makeSound() {
            System.out.println("Animal is making a sound");
        }
    }

    static class Dog extends Animal {
        public void makeSound() {
            System.out.println("Dog is barking");
        }
    }

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Animal());
        animals.add(new Dog());

        // 定义一个使用无界通配符的List
        List<?> wildcardList = animals;

        // 使用无界通配符迭代列表并调用方法
        for (Object animal : wildcardList) {
            // 由于通配符类型未知,只能调用Object上定义的方法
            System.out.println(animal.toString());
        }
    }

}

在这个例子中,我定义了一个Animal类和一个继承自AnimalDog类。然后创建了一个ArrayList来存储Animal对象和Dog对象。

接下来,使用无界通配符(?)定义了另一个列表wildcardList,将其指向之前创建的animals列表。这意味着可以将任何类型的列表赋值给wildcardList,而不仅仅是List<Animal>

在循环中,通过迭代wildcardList中的元素来调用它们的toString()方法。由于无界通配符的类型未知,只能将迭代出的元素视为Object。在本例中,输出结果将是每个对象的默认toString()实现。

运行结果:

com.xiaoshan.unboundedwildcard.UnboundedWildcardDemo$Animal@1b6d3586
com.xiaoshan.unboundedwildcard.UnboundedWildcardDemo$Dog@4554617c

4.2 上限通配符(Upper Bounded Wildcard)

上限通配符(? extends T):用于限制传入的类型必须是某个类T 或其子类。

以下是一个示例,展示了如何使用上限通配符:

import java.util.Arrays;
import java.util.List;

public class UpperBoundedWildcardDemo {
    public static double sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number number : numbers) {
            total += number.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3);
        double result = sum(integers);
        System.out.println(result);

        List<Double> doubles = Arrays.asList(1.5, 2.2, 3.82);
        result = sum(doubles);
        System.out.println(result);
    }
}

在这个示例中,sum 方法使用上限通配符(? extends Number),它允许方法接受继承自 Number 的子类的列表作为参数,例如 IntegerDouble,使得支持多种数字类型的运算。在方法内部,通过迭代遍历列表中的每个元素,将其转换为 double 类型并累加到变量 total 上。最后,返回 total 的值。
通过扩展泛型方法的灵活性,可以将不同类型的列表传递给 sum 方法,并对其进行求和操作。

运行结果:

6.0
7.52

4.3 下限通配符(Lower Bounded Wildcard)

下限通配符(? super T):用于限制传入的类型必须是某个类或其父类。

以下是一个示例,展示了如何使用下限通配符。
首先定义两个类FruitAppleFruit是一个父类,AppleFruit的子类。

public class Fruit {

    private String info = "一个水果";

    @Override
    public String toString() {
        return "Fruit{" +
                "info='" + info + '\'' +
                '}';
    }

}

class Apple extends Fruit {

    private String info = "一个苹果";

    @Override
    public String toString() {
        return "Apple{" +
                "info='" + info + '\'' +
                '}';
    }
}

然后以下程序展示了Java中下界通配符的使用:

import java.util.Arrays;
import java.util.List;

public class LowerBoundedWildcardDemo {
    public static void printf(List<? super Apple> list) {
        System.out.println(list);
    }

    public static void main(String[] args) {
        List<Apple> appleList = Arrays.asList(new Apple(), new Apple());
        printf(appleList);

        List<Fruit> fruitList = Arrays.asList(new Fruit(), new Fruit());
        printf(fruitList);
    }
}

静态方法printf的参数是一个类型为 List<? super Apple> 的列表,这个列表可以接受任何类型为Apple及其父类的列表。 然后只简单地打印了传递给方法的列表。

main方法中,首先创建了一个List<Apple>对象appleList并将其初始化为包含两个Apple对象的数组的列表。然后,调用printf方法并将appleList作为参数传递给它。由于参数类型为List<Apple>,而printf方法接受的参数类型为List<? super Apple>,所以这个调用是合法的。

接下来,创建了一个List<Fruit>对象fruitList并将其初始化为包含两个Fruit对象的数组的列表。然后,再次调用printf方法并将fruitList作为参数传递给它。由于FruitApple的父类,所以这个调用仍然是合法的。

运行结果:

[Apple{info='一个苹果'}, Apple{info='一个苹果'}]
[Fruit{info='一个水果'}, Fruit{info='一个水果'}]

5️⃣ 类型擦除(Type Erasure)

Java 的泛型实现使用了类型擦除机制。这意味着泛型的类型信息只存在于代码编译阶段,在运行时会被擦除掉。类型擦除是为了实现与之前版本的 Java 兼容,并且可以在运行时提高性能。

由于类型擦除,泛型类型参数在运行时会被擦除为它们的原始类型或限定类型。例如,一个泛型类在运行时会变成它的原始形式。这就是为什么无法在运行时获得泛型类型参数的具体类型。

以下是一个示例,展示了类型擦除的效果:

public class TypeErasureDemo<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        // 使用类型擦除
        TypeErasureDemo<String> example = new TypeErasureDemo<>();
        example.setValue("Hello");
        // 返回类型为 String
        String value = example.getValue();
        System.out.println(value);
        // 返回类型为 TypeErasureDemo,而不是 TypeErasureDemo<String>
        Class<? extends TypeErasureDemo> clazz = example.getClass();
        System.out.println(clazz);
    }
}

在这个示例中,GenericExample 是一个泛型类。虽然我在实例化 GenericExample 时指定了具体的类型参数(String),但是在运行时通过调用 example.getClass() 来获取其类对象时,,返回的类型是 GenericExample,而不是 GenericExample<String>。这说明在运行时,泛型类型参数会被擦除为其原始类型。

运行结果:

Hello
class com.xiaoshan.typeerasure.TypeErasureDemo

尽管在运行时无法获得泛型类型参数的具体类型信息,但是可以通过反射来获取泛型类或方法的基本信息。通过反射,可以获取泛型类的属性、方法和接口等,并进一步操作它们。

6️⃣ 泛型对协变和逆变的支持

需要注意的是,泛型在存在子类型关系时有一些特殊的规则:

  • 泛型不支持协变:即使 SubSuper 的子类,Container<Sub> 也不是 Container<Super> 的子类型;
  • 泛型不支持逆变:即使 SuperSub 的子类,Container<Super> 也不是 Container<Sub> 的父类型。
  • 泛型可以使用上限通配符(? extends T)模拟协变,使用下限通配符(? super T)模拟逆变。

以下是一个示例,展示了这些规则的应用:

import java.util.ArrayList;
import java.util.List;

public class InvarianceDemo{
    static class Super {
        @Override
        public String toString() {
            return "Super{}";
        }
    }
    static class Sub extends Super {
        @Override
        public String toString() {
            return "Sub{}";
        }
    }

    public static void test1() {
        // 1、不支持协变
        // 编译不通过:Required type: List <Super>. Provided: ArrayList <Sub>
        List<Super> superList1 = new ArrayList<Sub>();

        // 编译不通过:Required type: List <Super>. Provided: List <Sub>
        List<Sub> subList1 = new ArrayList<>();
        List<Super> superList = subList1;
        

        // 2、不支持逆变
        // 编译不通过:Required type: List <Sub>. Provided: ArrayList <Super>
        List<Sub> subList2 = new ArrayList<Super>();

        // 编译不通过:Required type: List <Sub>. Provided: List <Super>
        List<Super> superList2 = new ArrayList<>();
        List<Sub> subList = superList2;
    }


    public static void test2() {
    	// 1、实现协变
        // 创建一个泛型为 Super 的列表 list,其类型为 List<? extends Super>
        List<? extends Super> list = new ArrayList<Super>();

        // 创建一个子类为 Sub 的列表 subList 并向其中添加两个新的 Sub 对象
        List<Sub> subList = new ArrayList<>();
        subList.add(new Sub());
        subList.add(new Sub());
        // 将 subList 赋值给 list 变量,因为 subList 是一个子类列表,可以赋值给泛型为 Super 的列表
        list = subList;
        System.out.println(list);

        // 2、实现逆变
        // 创建一个泛型为 Sub 的超类列表 list2,其类型为 List<? super Sub>
        List<? super Sub> list2 = new ArrayList<Sub>();
        
        //  创建一个超类为 Super 的列表 superList 并向其中添加两个新的 Super 对象
        List<Super> superList = new ArrayList<>();
        superList.add(new Super());
        superList.add(new Super());
        //  将 superList 赋值给 list2 变量,因为 superList 是一个超类列表,可以赋值给泛型为 Sub 的列表
        list2 = superList;
        System.out.println(list2);
    }
}

在这个示例中,test1 展示了泛型类型的不变性。虽然 SuperSub 的父类,但是 List<Super> 并不是 List<Sub> 的父类,反之亦然。所以test1 里的代码全部无法通过编译,如下图:
【Java高级语法】(三)泛型:关于泛型最全面的讲解来了~-LMLPHP


test2 方法则展示了通过使用上限通配符(? extends T)来模拟实现协变,使用下限通配符(? super T)来模拟实现逆变。
运行结果:

[Sub{}, Sub{}]
[Super{}, Super{}]

7️⃣ 应用场景

泛型在实际开发中有很多应用场景,以下是一些常见的用法:

  • 容器类:泛型使得容器类(如列表、集合、映射)可以存储不同类型的数据,并提供类型安全的访问和操作;
  • 泛型算法:通过使用泛型方法,可以编写适用于多种类型的通用算法,减少了代码的重复,增加了代码的可重用性;
  • 数据结构:泛型也被广泛用于定义数据结构,如栈、队列、二叉树等。通过使用泛型,可以定义通用的数据结构,以便处理不同类型的数据;
  • 接口的泛型化:在设计接口时,可以使用泛型将其参数或返回类型与具体实现解耦,从而提高代码的灵活性和可扩展性;
  • 异常处理:Java 标准库中的异常类也使用了泛型,这样可以更好地捕获和处理特定类型的异常。

🌾 总结

泛型是 Java 语言提供的一种强大的功能,它允许在编译时对类型进行检查,并提高代码的类型安全性和可读性。通过在类、接口或方法的定义中使用类型参数,可以创建通用的数据结构和算法,并简化代码的开发过程。

尽管泛型的实现采用了类型擦除机制,导致在运行时无法获得泛型类型参数的具体类型信息,但是仍然可以通过通配符和反射等机制来操作泛型对象和获取相关的元数据。

在实际开发中,泛型被广泛应用于容器类、算法、数据结构、接口的设计以及异常处理等领域,它大大提高了代码的可重用性、可扩展性和可维护性。

掌握泛型的使用方法以及了解其原理和限制,将使开发人员能够更好地利用 Java 编程语言的强大功能,并编写更优雅、健壮且类型安全的代码。

📑 本文源码下载地址

Java语言 泛型讲解案例代码(泛型类、泛型接口、泛型方法、无界及上下限通配符、泛型对协变和逆变的支持、类型擦除 …)

【Java高级语法】(三)泛型:关于泛型最全面的讲解来了~-LMLPHP


【Java高级语法】(三)泛型:关于泛型最全面的讲解来了~-LMLPHP

06-15 23:47