Java泛型通配符详解

Java基础 专栏收录该内容
20 篇文章 0 订阅

数组的协变

在介绍泛型的通配符之前,我先简单介绍一下数组的协变,我们知道子类对象可以通过向上转型赋值给父类对象,那么在数组中满足这种转型条件吗?答案是满足。子类数组可以转型为父类数组,父类数组对象可以添加其任意子类的对象。
测试代码

		Number[] numbers  = new Integer[10];
		numbers[0]=1; //编译通过,运行时通过
		numbers[1]=1.1; //编译通过,运行时报错

分析:这就体现了数组协变的思想,Integer是Number的子类,故满足数组的赋值条件,numbers对象自然会也可以添加其子类的Integer对象或者是Double对象,在编译期,这些语句都是合法的,但是一旦运行这段代码,就会在添加1.1这个数据的地方报错,因为第一条语句已经指定了numbers数组中数据的具体类型是Integer类型,在运行期间是不允许添加Double类型数据的。总之,以上体现了数组协变的思想,即子类数组对象可以转型为父类数组对象;编译期只考察引用关系是否符合,只要父类数组对象添加的参数是其子类对象,就可以通过编译;运行时考察父类数组中的实际数据类型,添加的数据不符合条件就会报错。
补充:在Java中数组是协变的,而集合运用泛型,是不支持协变的,集合的等号两边数据类型必须一致。

泛型 < T >

泛型在Java的集合中应用十分广泛,一个集合对象往往可以存储不同类别的数据,比如ArrayList,它可以存储String,Integer,对象的引用等各种类型的数据,源码中关于类的对象都用T来表示,T只是一个标识符,不代表任何具体的类。也就是说在代码运行前我们是不知道这个集合会用来存储什么类型数据的,只要在new集合对象的时候指定存储的数据类型就可以了。这就是常见的泛型的使用方法,但是这使得参数被指定为了唯一的一个类的对象,失去了在参数传递时的灵活性,这违背了设计模式中避免后期修改代码的原则,如果设计一种泛型的表示,可以接受一定范围的类,就使得方法的使用更加灵活了。

泛型通配符 < ? >

概念:当把T换为“?”,原本的T会在程序运行中被指定为具体的内容,然而“?”表示数组内数据类型的不确定,可能是Object的任何子类,这样一来,如果使用add()方法是不安全的,因为add的类的对象不能确保可以向上转型为数组内的类的对象;如果使用get()方法也是不安全的,因为数组中的类可能是Object的任何子类,除了转型为Object取出没有别的选择,一旦转型为Object取出,会失去子类的独有属性。总之,被这个参数指定的集合既不能使用get()方法也不能使用add()方法,因为在集合的数据类型不确定的时候,是不会通过编译的,编译器会在代码的编写过程中产生错误提示。
测试代码

	public static void main(String[] args){
		ArrayList<?> animals =new ArrayList<>();
		animals.add(new Animal(12));   //编译不通过,不能添加不确定类型
		animals.add(new Dog(13));    //编译不通过,不能添加不确定类型
		animals.add(new BlackDog(14)); //编译不通过,不能添加不确定类型
		animals.add(null);      //编译通过,任意集合都可以存null
		Dog dog = animals.get(0); //编译不通过,对象类型不确定
		Animal animal = animals.get(0);//编译不通过,对象类型不确定
		Object a = animals.get(0);//编译通过,可以转为Object对象
	}	
}
class Animal implements Comparable<Animal>{    
	public int age;	
	public Animal(int age) {
		this.age=age;
	}		
	public int compareTo(Animal o) {
		// TODO Auto-generated method stub
		return this.age-o.age;
	}   
}
class Dog extends Animal{
	public Dog(int age) {
		super(age);
		// TODO Auto-generated constructor stub
	}
}
class BlackDog extends Dog{
	public BlackDog(int age) {
		super(age);
		// TODO Auto-generated constructor stub
	}	 
}

分析:以上代码写了一个BlackDog继承Dog继承Animal的继承体系,并加了一个age属性。然后指定一个ArrayList的数据类型<? >,在数组内数据类型不确定的情况下,既不能add(),也不能get(),编译不通过(存null和取出转Object是特例)。

泛型通配符 < ? extends T >

概念:它申明了集合内的数据类型是T的子类或者T本身。
测试代码

	public static void main(String[] args){
		ArrayList<? extends Animal> animals =new ArrayList<>();
		animals.add(new Animal(12));   //编译不通过,不能添加不确定类型
		animals.add(new Dog(13));    //编译不通过,不能添加不确定类型
		animals.add(new BlackDog(14)); //编译不通过,不能添加不确定类型
		animals.add(null);      //编译通过,任意集合都可以存null
		Dog dog = animals.get(0); //编译不通过,不能确保得到dog类对象
		Animal animal = animals.get(0);//编译通过,自动转型父类
		for(Animal a : animals){
			System.out.println(a.age);  //编译通过,转型父类输出
		}
	}	
}
class Animal implements Comparable<Animal>{    
	public int age;	
	public Animal(int age) {
		this.age=age;
	}	
	public int compareTo(Animal o) {
		// TODO Auto-generated method stub
		return this.age-o.age;
	}   
}
class Dog extends Animal{
	public Dog(int age) {
		super(age);
		// TODO Auto-generated constructor stub
	}
}
class BlackDog extends Dog{
	public BlackDog(int age) {
		super(age);
		// TODO Auto-generated constructor stub
	}	 
}

分析:依然是之前的三个有继承关系的类,然后指定一个ArrayList的数据类型<? extends Animal>,这个通配符表示数据类型为Animal及其子类,表示一个范围,具体是什么类,是不确定的。理论上似乎可以添加这三个类的对象给数组,但是编译没有通过。其实原因是因为传入的类表示的是一个范围是不确定的,Java编译器是不会允许编译通过的,Java不允许集合接受一个类型不确定的对象。但为什么get()方法就可以编译通过了呢?因为我们get()方法获取到对象之后自动转型为Animal对象(是范围的上限),然后进行操作,就不可能出错,因为转型之后的运行是确定的,这是可以编译通过的。

泛型通配符 < ? super T >

概念:这申明了集合内的数据类型是T的超类或者T本身,即可能是T和Object之间的任意满足继承关系的类(通配符仅仅是一种申明,作为写入数据和取出数据时的判断)。
测试代码

		ArrayList<? super Dog> dogs =new ArrayList<>();
		dogs.add(new Animal(12));   //编译不通过,不能确保安全转型
		dogs.add(new Dog(13));    //编译通过
		dogs.add(new BlackDog(14)); //编译通过
		dogs.add(null);      //编译通过,任意集合都可以存null
		BlackDog blackDog=dogs.get(0);//编译不通过,不确定具体是什么类,不能取出为指定类
		Dog dog = dogs.get(0); //编译不通过,不确定具体是什么类,不能取出为指定类
		Animal animal = dogs.get(0);//编译不通过,不确定具体是什么类,不能取出为指定类
		Object a = dogs.get(0); //编译通过,转型为Object类对象,转型安全
		for(Object o : dogs){
			System.out.println(o);  //编译通过,转型Object对象输出
		}

分析:依然采用上面用到过的有继承关系的三个类,运行代码后,我们可以发现,使用该通配符申明ArrayList之后,只能add T和其子类,不能get得到除了Object以外的任何一个类对象。原因其实很简单,Java编译器需要确保对象转型的安全,因为集合中的元素可能是T的任何超类或者其本身,所以addT的子类或者T本身都可以确保能够安全地转型到T类的对象或者T的超类。get时,因为不确定要转型为什么对象,除了转型为Object对象可以确保安全之外,没有其他选择。因此,使用super后,只能add不能get。

泛型中的PECS原则

1、只需要从集合中读取数据,不需要向集合中写数据,就可以使用<? extends T>,这是一种生产者模型。
2、只需要向集合中写数据,不需要从集合中读取数据,就可以使用<? super T>,这是一种消费者模型。
3、既要从集合中读取数据,又要向集合中写数据,就不需要使用任何通配符。

<T extends Comparable>和<T extends Comparable<? super T>>比较分析

在学习了以上通配符之后,我们再介绍一下另一种稍微复杂一点的用法。
第一种参数约束的分析:<T extends Comparable>中使用到了extends,这说明参数类型规定为必须是实现了Comparable接口的类,并且Comparable的泛型参数类型被指定为了T。Comparable是一个用于实现对象之间比较的接口,可以被实现并改写,和前面讲述的必须是某类的子类不同,这里是指实现了接口,成为了参数T的某种限制条件。Comparable接口是支持泛型的接口,所以上式可以理解为某个类T必须先实现Comparable接口,然后Comparable接口的参数类型是T,可以对T类对象比较,然后这个T的对象就可以作为符合<T extends Comparable>条件的参数传入了。
第二种参数约束相对第一种的优点:<T extends Comparable<? super T>>和前者的区别就是把T换为了<? super T>,接下来我们简单分析一下这样做的好处。假设存在一个BlackDog继承Dog继承Animal的关系,Animal实现了Comparable接口,Comparable的参数被指定为Animal,然后假设Dog类的集合想使用一个排序方法,因为这个排序方法对参数的限定是<T extends Comparable>,当Dog类没有实现Comparable接口时,默认使用的是父类的Comparable,这样参数的类型就是<Dog extends Comparable>,因为只有满足<Dog extends Comparable>才能调用,所以不能编译通过。虽然Dog类重新实现Comparable接口后可以调用排序方法,但是这违背了设计模式中尽量避免修改代码的原则,如果使用<T extends Comparable<? super T>>,就可以接收类似于<Dog extends Comparable>的数据,其中<? super T>表示Comparable中的参数可以是T类的父类。这样一来,子类可以不用繁琐地写实现接口的代码,并且都可以调用统一的排序方法了。
测试代码

public class Test {	
	public static  void main(String[] args){
	    ArrayList<Animal> animals = new ArrayList<>();
	    animals.add(new Animal(20));
	    animals.add(new Animal(14));
	    animals.add(new Animal(5));	    
	    ArrayList<Dog> dogs = new  ArrayList<>();
	    dogs.add(new Dog(4));
	    dogs.add(new Dog(7));
	    dogs.add(new Dog(6));		 
	    Test.sortList(animals);
	    for(Animal a : animals)
	    {
	    	System.out.println(a.age);
	    }
	    Test.sortList1(dogs);
	    for(Dog d : dogs)
	    {
	    	System.out.println(d.age);
	    }
	}	
	/**
	 * 排序方法1每个子类都必须自己实现Comparable接口
	 * @param list
	 */
	public static <T extends Comparable<T>> void sortList(List<T> list){
		Collections.sort(list);
	}	
	/**
	 * 排序方法2:接口继承更加灵活,不用实现Comparable接口
	 * @param list
	 */
	public static <T extends Comparable<? super T>> void sortList1(List<T> list){
		Collections.sort(list);
	}		
}
 class Animal implements Comparable<Animal>{    
	public int age;	
	public Animal(int age) {
		this.age=age;
	}		
	public int compareTo(Animal o) {
		// TODO Auto-generated method stub
		return this.age-o.age;
	}
}
 class Dog extends Animal{
	public Dog(int age) {
		super(age);
		// TODO Auto-generated constructor stub
	}
} 
 class BlackDog extends Dog{
	public BlackDog(int age) {
		super(age);
		// TODO Auto-generated constructor stub
	}	 
 }

结果:以上代码实现了对动物年龄的排序,传入的集合可以是Animal类的集合,也可以是它子类的集合。其中这段代码比较有意思:public static <T extends Comparable<? super T>> void sortList1(List list) 。这个方法的接收List类的对象,其参数用泛型T表示,在接收到一个list后,T就被指定为具体类了,然后在<T extends Comparable<? super T>>中进行判断,符合条件就能通过编译并且可以使用这个方法。

  • 2
    点赞
  • 0
    评论
  • 2
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

mayifan_blog

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值