java 泛型“T”,“?”,“? extends E”,“? super E”

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u013513053/article/details/100744662

最近查看java源码,看到很多泛型的应用。原先对泛型也是一知半解,使用的不是很多。今天研究了一下泛型的使用。

泛型的定义就不过多去说了,我就针对<T>,<?>,<? extends E>,<? super E>这几个泛型的使用进行说明。

<T>

这是最基础的泛型应用,用作类的定义上面,表示接收什么类型的定义。最常见的就是List,Map这种。我们看一下代码

public interface List<E> extends Collection<E> {
//···省略····
}

这个代码是List接口的定义。List是一个接口,我们使用的时候会去创建List的子类,比如ArrayList。这里的<E> 表示指定一个类型。这个类型在创建对象的时候就被定义。

List<Student> list = new ArrayList<Student>();

就像这样,我们指定List的内容是Student对象,如果传入其他的类型,则将会报错。这样的好处在于我们创建对象的时候,就可以指定它的类型,避免传入的类型出错。比如说一个学生数组里,你放入一个桌子对象,这肯定是不行的。
使用泛型除了可以编译的时候提示类型错误,还有一个好处就是不需要强制类型转换。比如说我们获取到List里面的一个元素,如果我们指定了它的泛型,那么取出来的就是指定的泛型类型。

List<Student> list = new ArrayList<Student>();
list.add(new Student());
Student student =  list.get(0);

这里我们其实是对方法的参数和返回值应用了定义的泛型。我们通过代码可以看到,List类添加了一个泛型E,然后在具体的方法中,我们没有指定具体的参数类型,返回值类型,而是使用的E代替,这样我们就可以保证,创建对象时指定的类型就是我们所需要使用的类型。

public interface List<E> extends Collection<E> {
	//···省略····
 	public boolean add(E e) {
		//省略具体代码
	}

	 public E get(int index) {
        //省略具体代码
    }
	//···省略····
}

这里的字母也并没有什么强制性要求,可以使用单个字母,比如T,E,K,V这种,也可以使用Type这种的单词。我们一般是习惯使用简写的字母,也可以写一些完整的单词来指定一些意思。如果是需要多个泛型,也不许要写多个尖括号<>,直接在一个尖括号内写多个,中间用逗号分隔就可以,如:

public interface Map<K,V> {
}

泛型如果没有指定类型的话,默认是Object。而且需要注意的是,这里必须是对象类型,不能是基础类型,比如说int,double,boolean等等,如果想要使用的话,请使用这些基本类型的封装类型Integer,Double,Boolean等。

 List list = new ArrayList();//这里没有指定类型,默认是Object
 list.add(new Student());//添加是没有问题,因为所有的对象都是Object子类
 Student student =  (Student) list.get(0);//获取的时候需要强制类型转换

<? extends E>

这个泛型的时候不是在定义类的时候使用的,而是在定义对象的时候使用的。

List<? extends Person> list = new ArrayList<Person>();

这里定义的时候用这个语法,但是new的时候就必须指定具体的类型。 <? extends E> 是 Upper Bound(上限) 的通配符,用来限制元素的类型的上限。意思就是,在这个list中,元素只能是Person以及它的子类。Person的父类不可以添加。
在java源码中,我们也能发现这样的身影

 public ArrayList(Collection<? extends E> c) 

这是一个ArrayList构造方法,传入的参数是Collection<? extends E> c,这里就是指定E这个泛型的子类及本身,而E这个泛型是在new List的时候传入的。这里extends可以是具体的类,也可以是一个泛型。

  • 添加
    那我们可以向里面添加什么类型?我们看一下代码
    在这里插入图片描述
    当我们定义完成后,添加一个Object对象竟然报错,添加一个子类也是报错。这是为啥?
    <? extends Student>只是告诉编译器集合中元素的类型上限,却不知道这个类型是啥类型。比如说,Teacher类和Student类都是Person的子类,我们new一个Teacher的ArrayList,但我们定义的是extends Person。那么实际上是Teacher,但是定义描述却是父类Person或者子类,那么添加就会报错。
    那么如何 添加呢?我们先创建一个ArrayList,并添加元素。然后再把整个赋值给list。
        ArrayList<Student> arrayList = new ArrayList<Student>();
        arrayList.add(new Student());
        List<? extends Person> list = arrayList;

这么做有什么好处呢?
的确,我们创建List的时候不会这个样子,而且添加麻烦。但是换个角度讲,这种方式读取起来就比较方便。

  • 读取
    因为我们定义的是Person以及子类,那么取出来的一定是Person对象。无论list指向什么,编译器都可以确定获取的元素是Person类型,所有读取集合中的元素是允许的。不管添加的是什么元素,用父类接收都是可以的。
 Person person = list.get(0);

我们常见的并不是在我们的逻辑使用中,而是在方法的参数中。因为方法的传参一定是赋值的一个过程,正好满足我们的赋值操作。其次,参数中定义了之后,我们就有明确的上限,就是明确的父类,这样子可以很方便方法内进行类型的判定和操作。上面提到了在ArrayList构造方法的入参中,就是这种类型,在方法内能够很方便的对他处理。

<?>

在说“?”之前先说了一下“<? extends E>”。因为单独一个“?”表示的意思也是很简单,就是
“<? extends Object>”的简写而已。

<? super E>

这个与之前的extends有些相似,但是描述的意思是完全相反的。<? extends E>表示上限,而<? super E>表示的是下限。意思就是说,里面的内容起码是E或者是E的父类。比如

List<? supper Student> list;
list = new ArrayList<Student>();
list = new ArrayList<Person>();

这样赋值都是合理的,Person是Student的父类。如果赋值的是子类或者其他的类,则会编译不通过。

  • 添加
    由于 限定了下限,也就是说,我们明确的知道当前最低的类型,我们添加的时候添加对象也有一个标准,添加下限的类或者子类是肯定没有问题的。但是添加父类也是不被允许的,因为不能确定当前使用的是哪个。举个例子,当你往list中添加一个Person类型对象时,但此时apples指向ArrayList<Student>,显然类型就不兼容了,Person不是Student的子类

  • 读取
    编译器允许从list中获取元素的,但是无法确定的获取的元素具体是什么类型,只能确定一定是Object类型的子类,因此我们想获得存储进去的对应类型的元素就只能进行强制类型转换了

Student s= (Student )list.get(0);//获取的元素为Object类型  

问题来了,JDK1.5引入泛型的目的是为了避免强制类型转换的繁琐操作,那么使用泛型<? super E>干嘛呢?这里就得谈到泛型PECS法则了

PECS法则

PECS法则:生产者(Producer)使用extends,消费者(Consumer)使用super
1、生产者
如果你需要一个提供E类型元素的集合,使用泛型通配符<? extends E>。它好比一个生产者,可以提供数据。
2、消费者
如果你需要一个只能装入E类型元素的集合,使用泛型通配符<? super E>。它好比一个消费者,可以消费你提供的数据。
3、既是生产者也是消费者
既要存储又要读取,那就别使用泛型通配符。

总结

为什么要引入泛型通配符?一句话:为了保证类型安全。
effective java中提出

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

猜你喜欢

转载自blog.csdn.net/u013513053/article/details/100744662