1.泛型的概念和作用
泛型是JDK在1.5版本引入的一个特性,在某种程度上,泛型的出现简化了我们的代码,在编译阶段保证代码的安全性。《Think in Java》中这样解释泛型:泛型实现了参数化类型的概念,使得类型可以作为参数适用于尽可能多的场景。我们平时写代码都是具体的类型,方法参数要么是基本数据类型要么自定义的类型,这样写有什么不好吗,不是的,要看使用场景。下面给出一个最经典的例子:
public class Client {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Integer(1));
list.add("2");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println(object);
}
}
}
List容器放入一个Integer类型的对象和一个字符串,这样编译器是允许的,但是我在获取容器内元素的时候,元素的类型被擦除了,取出来的都是Object类型(因为Object类型是所有类的父类,所以取出来的内容都是Object类型)。类型的向上转换导致丢失了具体类型信息,这对于我们实际工作非常不友好;但是我们可以通过手动向下类型转换进行类型还原,这种做法虽然可行,但对于我编码人员来说繁杂且不可靠。
如果不想手动向下类型转换,唯一的办法就是在放入的时候做一个限制,即一个容器只放入某一种类型的对象元素,这样我编译器就能确定放入的到底对不对。上面的示例改成下面这个样子:
List<Integer> intList = new ArrayList<Integer>();
intList.add(new Integer(2));
intList.add(new Integer(6));
//intList.add("3"); // 不符合类型要求 编译报错
for (int i = 0; i < intList.size(); i++ ) {
Integer integer = intList.get(i); // 直接获取Integer
System.out.println(integer); // 拆箱
}
这里通过泛型把intList容器限定为只存放Integer类型的对象,所以获取的时候不是Object类型,而是Integer类型。也可以通过下面的方法:
List<String> strList = new ArrayList<String>();
把strList容器限定为只存放String类型的对象,这样编译器就知道该容器不能存放其他类型的对象。
像List接口或者ArrayList类在定义时是通过尖括号来实现这样的目的,尖括号内具体放置的类型,在List接口或者ArrayList类定义时并不知晓,而是把放置的类型抽象化,用某种符号来表示,这种做法就是类型参数化,使ArrayList类适用更多的场景。
2.如何定义泛型
我们上面介绍了泛型的基本概念和使用场景,那么在代码层面是如何定义一个泛型的呢?
泛型是一个特性,它可以实现类型参数化,只要使用到类的地方都可以拥有这个特性;那么我们可以定义一个类,接口或者方法(因为方法参数可以使用自定义类型)拥有泛型的特性,拥有这种特性的类、接口或者方法我们称之为泛型类、泛型接口和泛型方法。
JDK1.5之后引入了很多泛型类和泛型接口,常见的就是容器接口和容器类:
// 泛型接口
public interface List<E> extends Collection<E>
public interface Set<E> extends Collection<E>
public interface Map<K,V>
// 泛型类
public class ArrayList extends AbstractList implements List
public class HashSet extends AbstractSet implements Set
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
// 泛型方法
void add(int index, E element); // List接口方法
<T> T[] toArray(T[] a); // Set接口方法
V put(K key, V value); // Map接口方法
声明接口或者类时,接口名和类名后尖括号内大写字母“E”代表这个接口或者类拥有泛型这个特性,它是参数化类型的一种表达方式;泛型类和泛型接口表明该类可以操作某一种类型的类,这个被操作的类的类型没有确定,而是用一个泛化的字母“E”来表示,当我们实际使用的时候就需要确定该类的类型;如泛型类ArrayList< E>的定义,及它的使用ArrayList< Integer>,ArrayList< String>,此时“E”是Integer类和String类的参数化表达。
另外,参数化类型“E”作用于泛型接口和泛型类的整个声明过程,即泛型类属性的声明,泛型类方法的声明等。所以可以知道,拥有泛型特性的类就会拥有泛型方法和泛型属性。同时注意,泛型类的使用并不会实际影响到泛型类的声明,什么意思呢,就是说定义的ArrayList< Integer>和ArrayList< String>并不影响到ArrayList< E>,可以理解为一种形参和实参。
注意:参数化类型一般用单个的大写字母来表示,JDK中用“E”,"K"或者"V"等来表示参数化类型,尽量避免小写,因为容易跟类中的属性或者变量混淆。
3.泛型通配符
这个时候你或许有个疑问,Java中不是所有的类型都继承自Object类吗,参数化类型为什么不直接用父类Object表示?
在回答这个问题之前,我们先看下面的示例:
List<Integer> intList = new ArrayList<Integer>(); // a
List<Object> objList = intList; // b
如果按照常规的Object类是Integer类的父类,所以objList引用可以指向intList引用指向的对象,所以会理所当然的认为没有问题。先别急着下结论,下面简单分析下:
假设没有问题,那么下面的操作也可以:
objList.add(new Object()); // c
Integer num = intList.get(0); // d
objList和intList指向同一块内存,两个引用可以同时操作这块内存,所以通过objList引用往内存中添加一个object对象,再通过intList引用获取该对象,此时获取的是Object类型的对象,而intList引用只能获取Integer类型的对象,这两者是相互矛盾的,所以上面的结论是错误的,b代码不会通过编译。即子类型和父类型的关系,在泛型特性里并不能保证,也就不能用Object类型替代类型参数化”E“了。
既然List< Object>不是所有泛型接口List的父类,那么什么是泛型接口List的父类呢?
大概是无法表达它的父类,Java中用”?“替代字母"E"来表示,如List<?>是List< E>的父类,Set<?>是Set< E>的父类等。"?"表明类型可以匹配任何类型,称为泛型通配符。所以上面的示例可以改为:
List<Integer> intList = new ArrayList<Integer>();
List<?> objList = intList;
objList引用是intList引用的父类。但问题you出现了,我们无法执行下面语句:
objList.add(new Object()); // 编译报错
按理来说,objList是父类引用,放什么都能接受,我们假设这是正确的,下面简单分析下;
泛型类ArrayList< E>的add方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
add方法参数是类型参数化"E",假如我放进去了没有问题,那么通过objList引用取出来呢,编译器无法知道取出来的元素类型是什么,因为objList的泛型是"?",所以在放入的过程就会编译报错。注意,可以放入null,因为null是所有类型共有的。
这个时候你可能觉得泛型父类List<?>没什么用,因为它并不能放任何东西,别急,往下看。
4.有限制的通配符
现在有一个画布可以画各种形状的图形,用代码表示就是:
形状接口以及各形状类:
public interface Shape {
void draw(Canvas c); // canvas为画布类
}
// 圆形
public class Circle implements Shape {
@Override
public void draw(Canvas c) {
System.out.println("circle draw");
}
}
// 正方形
public class Square implements Shape {
@Override
public void draw(Canvas c) {
System.out.println("Square draw");
}
}
画布类:
public class Canvas {
public void draw(Shape shape) { // 一个画布画单个图形
shape.draw(this);
}
public void drawAll(List<Shape> shapes) { // 一个画布画多个图形
for (Shape shape : shapes) {
shape.draw(this);
}
}
}
客户端:
public class Client {
public static void main(String[] args) {
Canvas canvas = new Canvas();
List<Shape> shapeList = new ArrayList<Shape>();
shapeList.add(new Square());
shapeList.add(new Circle());
canvas.drawAll(shapeList); // 编译通过
List<Circle> circleList = new ArrayList<Circle>();
circleList.add(new Circle());
canvas.drawAll(circleList); // 编译报错
canvas.draw(new Circle()); // 编译通过
}
}
是否发现了什么?没错,drawAll()方法只接受List< Shape>类型的引用,而不接受List< Circle>类型的引用,这与我们前面说的遥相呼应。Circle类型是Shape的子类或者实现类,但泛型类List< Shape>并不是泛型类List< Circle>的父类。
如果我们把drawAll(List< Shape> shapes)改成drawAll(List<?> shapes),很明显在循环便利的时候并不知道取出来的元素是什么类型,所以这种方法也不行。Java里面提供一种有限制的通配符,用下面这种方式表示:
List<? extends Shape>
它表示泛型类的参数类型(? extends Shape)是Shape类的子类,即Shape类以及任何子类和实现类,这才是我们想要表达的内容;这种写法之后,编译不再报错。
注意区分这里提到的三个泛型引用:List<?>, List<? extends Shape>,List< Shape>。
5.泛型方法
上面讲到参数化类型<? extends Shape>是< Shape>的父类,那下面的代码是否行得通呢?
List<? extends Shape> list = new ArrayList<Shape>();
list.add(new Circle());
很不幸,编译报错。原因很简单,<? extends Shape>是一个表示范围的未知类型,跟<?>是一样的,只不过范围缩小了;所以,< Shape>即可。
上面介绍了泛型类和泛型接口,下面看看泛型方法。定义一个泛型方法:
public <T> void addList(List<T> list, T t) {
list.add(t);
}
把类型为T的对象t放到一个集合list中,注意public和void之间有一个,少了它泛型方法则不成立。泛型方法可以自己实践,比较简单。
6.泛型使用的注意事项
A.泛型类在使用过程中类型不会被改变
List<Integer> intList = new ArrayList<Integer>();
List<String> strList = new ArrayList<String>();
System.out.println(intList.getClass() == strList.getClass()); // true
运行时获取引用intList和strList的Class对象,如果你了解反射,那么就能明白两者的Class对象是同一个,所以泛型作为类的一种特性,使用过程中不会影响类的类型。
PS::不了解反射的可以看这篇文章。
B.泛型类的instanceOf
if (intList instanceof List<Integer>) {} // 编译报错
if (intList instanceof List<?>) {} // 编译通过
第一点说明,运行时并不会区分参数化类型,即类型的信息在运行时被擦除了,所以第一个判断中检查intList引用是否是一个特定类型的泛型类是没有任何意义的。第二个判断,虽然编译能通过,但也没有实际意义。
7.总结
关于泛型的大部分知识已经学完了,主要有下面几个点:
泛型的概念和作用;
如何在Java代码层面定义体现泛型特性;
通过具体问题引出泛型的通配符和有限通配符;
泛型方法的定义和使用;
泛型的两个注意事项