26、Java基础教程之新特性·泛型

  • 1️⃣ 概念
  • 2️⃣ 优势
  • 3️⃣ 使用
    • 3.1 泛型类
  • 3.2 泛型接口
  • 3.3 泛型方法
  • 4️⃣ 通配符(Wildcards)
    • 4.1 无界通配符(Unbounded Wildcard)
  • 4.2 上限通配符(Upper Bounded Wildcard)
  • 4.3 下限通配符(Lower Bounded Wildcard)
  • 5️⃣ 类型擦除(Type Erasure)
  • 6️⃣ 泛型对协变和逆变的支持
    • * 协变(covariant)和逆变(contravariant)是什么?
  • 7️⃣ 应用场景
  • * 总结
  • * 本文源码下载地址

*

1️⃣ 概念

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

举个例子,假设现在要开发一个地理信息系统 (Geographic Information System, GIS),肯定需要一个可以描述坐标的类 (Point),同时在这个类里面要求保存有以下3种类型的坐标。
*保存数字: x=10、y=20;
*保存小数: x=10.2、y=20.3;
*保存字符串: x= 东经20度、 y= 北纬15度。

这个Point 类设计的关键就在于 xy 这两个变量的数据类型的选择上,必须有一种数据类型可以保存这三类数据,那么首先想到的一定是Object 类型,因为此时会存在如下转换关系。
*int 数据类型: int 自动装箱为 IntegerInteger 向上转型为 Object
*double数据类型:double 自动装箱为 Double, Double 向上转型为 Object
*String 数据类型:直接向上转型为 Object

//	范例 1: 定义Point 类,使用Object作为属性类型
class Point{
   
     		//定义坐标
	private Object x;		//可以保存任意数据
	private Object y;		//可以保存任意数据
	
	public void setX(Object x){
   
     
		this.x = x;
	}
	public void setY(Object y){
   
     
		this.y = y;
	}
	public Object getX(){
   
     
		return x;
	}
	
	public Object getY(){
   
     
		return y;
	}
}

此程序Point 类中的两个属性全部定义为 Object类类型,这样就可以接收任意的数据类型了。为了更好地说明问题,下面分别设置不同的数据类型,以测试程序。

//	范例 2: 在Point 类里面保存整型数据、浮点型数据、字符串数据
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		//第一步:根据需要设置数据,假设此时的作用是传递坐标
		Point point1 = new Point();	//实例化Point类数据
		point1.setX(10);		//设置坐标数据
		point1.setY(20);		//设置坐标数据

		//第二步:根据设置好的坐标取出数据进行操作
		int x =(Integer) point1.getX();                         //取出坐标数据
		int y =(Integer) point1.getY();                         //取出坐标数据
		System.out.println("x坐标:"+x+", y坐标:"+y);

		Point point2 = new Point();
		point2.setX(10.2):
		point2.setY(20.3);
		
		double x =(Double) point2.getX();                           //取出坐标数据
		double y =(Double) point2.getY();                           //取出坐标数据
		System.out.println("x坐标:"+x+", y坐标:"+y);

		Point point3 = new Point();
		point3.setX("东经100度");
		point3.setY("北纬20度")
		
		String x =(String) point3.getX();                            //取出坐标数据
		String y =(String) point3.getY();                            //取出坐标数据
		System.out.println("x坐标:"+x+", y坐标:"+y);
	}
}

程序执行结果:

x坐标:10, y坐标:20
x坐标:10.2, y坐标:20.3
x坐标:东经100度, y坐标:北纬20度

上边范例程序都是利用基本数据类型自动装箱与自动拆箱的特性实现数据传递。在这里一定要记住,调用 setter 方法设置坐标时,所有的数据类型都发生了向上转型,而在取得数据时都发生了强制性的向下转型。

本程序对于设计的基本要求已经成功地实现了,而整个设计的关键就在于 Object 类的使用,由于 Object 类型可以描述所有的数据类型,所以这时会带来一个严重的后果: 一旦设置的内容出现错误, 在程序编译时是无法检查出来的。

// 范例 3: 错误的程序
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		//第一步:根据需要设置数据,假设此时的作用是传递坐标
		Point p = new Point();
		p.setX("东经100度");
		p.setY(10);
		//第二步:根据设置好的坐标取出数据进行操作
		String x = (String) p.getX();                              //取出坐标数据
		String y = (String) p.getY();                              //取出坐标数据
		System.out.println("x坐标:"+x+", y坐标:"+y);
	}
}

程序执行结果:

Exception in thread"main"java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at com.xiaoshan.demo.TestDemo.main(TestDemo.java:26)

本程序原本打算设置的坐标数据类型是字符串数据,但是在设置数据时出现了错误,将 Y 坐标数据设置为了一个数字(“p.setY(10)”") 而不是字符串。由于int 型可以通过自动装箱使用 Object 接收,所以这样的问题在程序编译时不会有任何语法错误。

而在程序执行过程中当需要将 Y 坐标数据取出时, 一定会按照预定的模式将 Y 坐标以 String 的形式进行强制向下转型,这时就会发生 “ClassCastException” (这样就带来了安全隐患)。可是整个异常并不是在程序编译时出现的,而是在运行中出现的,这样就会为开发带来很大的困扰,实际上这就证明利用 Object 接收参数会存在安全隐患,面对这样的问题如果可以在编译时就能够排查出来才是最合理的。

在JDK 1.5 以前对于以上错误只能在程序中利用一系列的判断进行检测,而从 JDK 1.5之后增加了 泛型技术,此技术的核心意义在于:类属性或方法的参数在定义数据类型时,可以直接使用一个标记进行占位,在具体使用时才设置其对应的实际数据类型,这样当设置的数据类型出现错误后,就可以在程序编译时检测出来。

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

//	范例 4: 使用泛型修改Point 类
package com.xiaoshan.demo;

//此时设置的T在Point类定义上只表示一个标记,在使用时需要为其设置具体的类型
class Point<T>{
   
     
	private T x;
	private T y;
	
	public void setX(T x){
   
     
		this.x =x;
	}
	public void setY(T y){
   
     
		this.y =y;
	}
	public T getX(){
   
     
		return x;
	}
	public T getY(){
   
     
		return y;
	}
}

本程序在 Point 类声明时采用了泛型( class Point<T>)支持,同时在属性声明中所采用的也是泛型标记 T, 那么这就表示在定义 Point 类对象时需要明确地设置 xy 属性的数据类型。

//	范例 5: 使用Point类将泛型类型设置为String
public class TestDemo {
   
     
	public static void main(String[] args){
   
     
		//第一步:根据需要设置数据,假设此时的作用是传递坐标
		Point<String> p = new Point<String>();
		p.setX("东经100度");
		p.setY("北纬20度");
		//第二步:根据设置好的坐标取出数据进行操作
		String x = p.getX();         //取出坐标数据,不再需要强制转换
		String y = p.getY();        //取出坐标数据,不再需要强制转换
		System.out.println("x坐标:"+x+", y坐标:"+y);
	}
}

程序执行结果:

x坐标:东经100度,y坐标:北纬20度

本程序在定义 Point类对象时使用了 String 作为泛型标记,这就表示在 Point类中的 xy 属性、 setter 参数类型以及 getter返回值类型都是 String, 从而就避免了数据设置错误(如果设置为非 String 类型,会造成语法错误) 以及强制向下转型操作,这样的操作才属于安全的操作。

使用泛型后,所有类中属性的类型都是动态设置的,而所有使用泛型标记的方法参数类型也都发生了改变,这样就相当于避免了向下转型的问题,从而解决了类对象转换的安全隐患。但是需要特别说明的是,如果要想使用泛型,那么它能够采用的类型只能够是类,即不能是基本类型,只能是引用类型。

2️⃣ 优势

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

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

3️⃣ 使用

3.1 泛型类

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

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

//	范例 6: 定义一个泛型类
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 来声明成员变量和方法的类型,并在实例化时指定具体的类型。

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

//	范例 7: 泛型类的使用
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用于获取容器中的元素:

//	范例 8: 定义了一个泛型接口
public interface Container<T> {
   
     
    void addItem(T[] item);  // 添加一组元素到容器中
    T[] getItem();           // 获取容器中的元素
}

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

//	范例 9: 一个实现了泛型接口的具体类
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) +
                '}';
    }
}

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

//	范例 10: 泛型接口的使用
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)。

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

//	范例 11: 定义泛型方法
public class ArrayUtils {
   
     
    public static <T> void printArray(T[] array) {
   
     
        for (T element : array) {
   
     
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

使用泛型方法:

//	范例 12: 使用泛型方法
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)

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

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

//	范例 13: 使用无界通配符
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 或其子类。

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

//	范例 14: 使用上限通配符
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的子类。

//	范例 15: 使用下限通配符 ——定义两个类
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中下界通配符的使用:

//	范例 16: 使用下限通配符
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 兼容,并且可以在运行时提高性能。

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

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

//	范例 17: 类型擦除
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️⃣ 泛型对协变和逆变的支持

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

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

* 协变(covariant)和逆变(contravariant)是什么?

协变和逆变是类型系统中的概念,用于描述类型之间的替换关系。

协变指的是在一种类型替换另一种类型时,被替换的类型(通常称为子类型或窄类型)可以是原始类型(通常称为父类型或宽类型)的子类型。换句话说,当你需要一个特定类型的对象时,你可以使用它的子类型作为替代,且不会引发任何错误。

逆变则是相反的概念-指的是当一个类型替换另一个类型时,被替换的类型可以是原始类型的父类型。换句话说,在需要特定类型的地方,可以使用其父类型的实例而不会导致错误。

总结起来,***协变描述了窄类型(子类型)替换宽类型(父类型),而逆变则描述了宽类型(父类型)替换窄类型(子类型)***。在类型系统中,这些概念有助于确保类型安全和灵活性,允许我们在使用类型的地方传递更具体或更一般化的类型对象。

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

//	范例 18:  泛型对协变和逆变的支持
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 里的代码全部无法通过编译,如下图:

*

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

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

7️⃣ 应用场景

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

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

* 总结

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

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

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

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

* 本文源码下载地址

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

*


[* ]nbsp_nbsp 3