参考资料
[1]. Java泛型深入理解,
http://blog.csdn.net/sunxianghuang/article/details/51982979
[2]. 疯狂Java讲义第三版,李刚
[3]. Java总结篇系列:Java泛型,
https://www.cnblogs.com/lwbqqyumidi/p/3837629.html
泛型入门
编译时不检查类型的异常
下面是编译时不检查类型所导致的异常。
List strList = new ArrayList();
strList.add("a");
strList.add("aa");
// 添加了一个Integer类型的元素
// 这将导致java.lang.ClassCastException异常
strList.add(3);
strList.forEach(str -> System.out.println(((String)str).length()));
使用泛型
使用泛型在编译阶段就可以发现错误
List<String> strList = new ArrayList<String>();
strList.add("a");
strList.add("aa");
// 此处会报错,因为类型与泛型规定的不同
strList.add(3);
// 此处无需进行强制类型转换,因为strList可以“记住”它的所有集合元素都是String类型。
strList.forEach(str -> System.out.println((str).length()));
Java 7泛型的“菱形”语法
在Java 7以前,如果使用带泛型的接口、类定义变量的使用方法
List<String> strList = new ArrayList<String>();
Map<String, Integer>scores = new HashMap<String, Integer>();
在Java 7以后的省略用法,因为可以从带泛型的接口、类定义变量推断类型,所以后面可以直接使用<>
List<String> strList = new ArrayList<>();
Map<String, Integer>scores = new HashMap<>();
使用示范
List<String> books = new ArrayList<>();
books.add("a");
books.add("aa");
books.add("aaa");
books.forEach(str-> System.out.println(str.length()));
Map<String, List<String>> schoolsInfo = new HashMap<>();
List<String> schools = new ArrayList<>();
schools.add("a");
schools.add("aa");
schools.add("aaa");
schoolsInfo.put("a",schools);
schoolsInfo.forEach((k,v) -> System.out.println(k + "--" +v));
深入泛型
定义泛型类
下面是Java 5改写后List接口的代码片段
// 定义接口时指定了一个类型形参,该形参名为E
// 只要是继承自Collection类即可
public interface List<E> extends Collection<E> {
boolean add(E e);
Iterator<E> iterator();
...
}
下面是Java 5改写后Iterator接口
// 定义接口时指定了一个类型形参,该形参名为E
public interface Iterator<E> {
// 在该接口里E完全可以作为类型使用
E next();
boolean hasNext();
...
}
下面是Java 5改写后Map接口
// 定义该接口时指定了两个类型形参,其形参名为K、V
public interface Map<K,V> {
// 在该接口里K、V完全可以作为类型使用
Set<K> keySet();
V put(K key, V value);
...
}
例如使用List类型时,如果为E形参传入String类型实参,则产生了一个新的类型:List<String>
类型,可以把List<String>
想象成E被全部替换成String的特殊List子接口。
// List<String>等同于如下接口
public interface ListString extends List
{
// 原来的E形参全部变成String类型实参
void add(String x);
Iterator<String> iterator();
...
}
下面是一个示范的Apple类
// 定义Apple类时使用了泛型声明
public class Apple<T>
{
// 使用T类型形参定义实例变量
private T info;
public Apple(){}
// 下面方法中使用T类型形参来定义构造器
public Apple(T info)
{
this.info= info;
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
public static void main(String[] args)
{
// 由于传给T形参的是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("apple");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double
Apple<Double> a2 = new Apple<>(3.1415);
System.out.println(a2.getInfo());
}
}
从泛型类派生子类
为上面的Apple类派生派生子类
错误用法示例:
// 定义类A继承Apple类,Apple类不能跟类型形参
public class A extends Apple<T>{}
可以不限制类型
public class A extends Apple{}
或者限制为某个类型
public class A extends Apple<String>{}
重写父类方法时,会继承父类的类型
public class A1 extends Apple<String>{
// 正确重写了父类的方法,返回值
// 与Apple<String>的返回值完全相同
public String getInfo()
{
return "子类" + super.getInfo();
}
// 下面方法是错误的,重写父类方法时,返回值类型不一样
// 父类返回的值是String类型,而它返回的是Object类型
public Object getInfo()
{
return "子类";
}
}
并不存在泛型类
它们运行的类是一个类,不会为List生成新的class文件
List<String> a1 = new ArrayList<>();
List<Integer> a2 = new ArrayList<>();
// 输出为true
System.out.println(a1.getClass() == a2.getClass());
不管为泛型的类型形参传入哪一种类型形参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型形参,因为系统不会真正生成泛型类。
public class R<T>
{
// 下面代码错误,不能在静态变量声明中使用类型形参
static T info;
T age;
public void foo(T msg){}
// 下面代码错误,不能在静态方法中使用类型形参
public static void bar(T msg){}
}
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类,下面是错误示范:
Collection<String> cs = new ArrayList<>();
// 并不存在ArrayList<String>类
if (cs instanceof ArrayList<String>)
{
// ...
}
类型通配符
当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出警告。
假设集合形参的元素类型不确定,当List c是一个有泛型声明的接口,此处使用List接口时没有传入实际类型参数,这将会引起警告。
考虑下面的代码:
public void test(List c)
{
for (int i=0; i < c.size(); i++)
{
System.out.println(c.get(i));
}
}
因为List c集合里的元素类型是不确定的,将上面方法改为如下形式:
public void test(List<Object> c)
{
for (int i=0; i < c.size(); i++)
{
System.out.println(c.get(i));
}
}
上面的泛型信息是固定的Object,所以当我们使用其他泛型信息进行调用会发生编译错误
// 这表明List<String>对象不能当成<Object对象使用>,也就是说List<String>类并不是List<Object>类的子类
List<String> strlist = new ArrayList<>();
test(strlist);
看一下数组在这方面存在的问题
// 定义一个Integer数组
Integer[] ia= new Integer[5];
// 可以把一个Integer[]数组赋给Number变量
Number[] na = ia;
// 下面代码编译正常,但运行时会引发ArrayStoreException异常
// 因为0.5并不是Integer
na[0] = 0.5;
可以发现数组可以通过编译,但在运行时会出现ArrayStoreException异常。
Java在此方面进行了改进,不允许跨对象赋值。
List<Integer> ilist = new ArrayList<>();
// 此处会报错
// 在泛型出现以后,Java不再允许把List<Integer>对象赋值给List<Number>变量
List<Number> nlist = ilist;
使用类型通配符
使用通配符(?),它的元素类型可以匹配任何类型,即传入的参数可以为任意类型:
public void test(List<?> c)
{
for (int i=0; i < c.size(); i++)
{
System.out.println(c.get(i));
}
}
这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中,下面的代码会引起错误:
List<?> c = new ArrayList<String>();
// 下面的程序会引起错误
c.add(new Object);
设定类型通配符的上限
当直接使用List<?>
这种形式时,即表明这个List集合可以是任何泛型List的父类,但还有一种特殊的情形,程序不希望这个List<?>
是任何泛型List的父类,只希望它代表某一类泛型List的父类。
现在编写一个抽象的形状(Shape)类,然后再编写两个继承了它的实现子类圆(Circle)类和长方形(Rectangle)类,最后用一个画类
Shape抽象类,抽象了draw方法
public abstract class Shape {
public abstract void draw(Canvas c);
}
Circle实现类,实现了draw类
public class Circle extends Shape{
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}
Rectangle实现类,实现了draw类
public class Rectangle extends Shape{
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}
画画类Canvas
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
}
调用
// 定义一个装Circle类的List
List<Circle> circleList = new ArrayList<>();
Canvas c = new Canvas();
// 不能把List<Circle>当成List<Shape>使用,所以会引起编译错误
c.drawAll(circleList);
现在修改Canvas类
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<?> shapes)
{
for (Shape s : shapes)
{
// 因为使用了通配符来表示所有的类型,所以需要强制转换
Shape s = (Shape)obj;
s.draw(this);
}
}
}
如果使用被限制的泛型通配符,代码如下:
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
// 因为使用了限制,所以不需要强制转换
s.draw(this);
}
}
}
将Canvas改为如上形式,就可以把List<Circle>对象当成List<? extends Shape>使用。即List<? extends Shape>可以表示List<Circle>、List<Rectangle>的父类--只要List后尖括号里的类型是Shape的子类型即可。
设定类型形参上限
public class Apple<T extends Number>
{
T col;
public static void main(String[] args)
{
Apple<Integer> a1 = new Apple<>();
Apple<Double> a2 = new Apple<>();
// 下面代码将引发编译错误,下面代码试图把String类型传给T形参
// 但String不是Number的子类型,所以引起编译错误
Apple<String> as = new Apple<>();
}
}
泛型方法
定义泛型方法
现在需要将一个Object数组的所有元素添加到一个Collection集合中
public class GenericMethodTest {
// 这个方法只能放入Object类型,其他不可以
static void fromArrayToCollection(Object[] a, Collection<Object> c)
{
for (Object o : a)
{
c.add(o);
}
}
public static void main(String[] args)
{
String[] strArr = {"a", "b"};
List<String> strList = new ArrayList<>();
// Collection<String>对象不能当成Collection<Object>使用,下面会出现编译错误
fromArrayToCollection(strArr, strList);
}
}
使用泛型方法后,编译器根据实参推断类型形参的值。
因为是使用第二个参数来add添加集合的,所以第二个参数是什么类型,那么第一个类型就是什么类型,前提是第二个参数可以兼容第一个参数,比如Number和Integer。
public class GenericMethodTest {
// 使用<T>,声明一个泛型方法,该泛型方法中带一个T类型形参
static <T> void fromArrayToCollection(T[] a, Collection<T> c)
{
for (T o : a)
{
//
c.add(o);
}
}
public static void main(String[] args)
{
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
// 下面代码中T代表Object类型
fromArrayToCollection(oa, co);
}
}
错误的使用案例
public class ErrorTest {
static <T> void test(Collection<T> from, Collection<T> to)
{
for (T ele : from)
{
to.add(ele);
}
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
// 下面代码将产生错误
// 该方法中的两个形参from、to的类型都是Collection<T>
// 这要求调用该方法时的两个集合实参中的泛型类型相同
// 否则编译器无法准确推断出泛型方法中类型形参的类型。
test(as, ao);
}
}
改进代码
public class ErrorTest {
static <T> void test(Collection<? extends T> from, Collection<T> to)
{
for (T ele : from)
{
to.add(ele);
}
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
// 下面代码可以正常运行
// 只要前一个Collection集合里的元素类型是后一个
// Collection集合里的元素类型的子类即可。
test(as, ao);
}
}
泛型方法和类型通配符的区别
大多数时候都可以使用泛型方法来代替类型通配符,例如对于Java的Collection接口中的两个方法定义:
public interface Collection<E> extends Iterable<E>
{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
...
}
上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示:
public interface Collection<E> extends Iterable<E>
{
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
...
}
Java的Collections的copy方法,使用了类型通配符
public class Collections{
public static <T> void copy(List<? super T> dest, List<? extends T> src)
{
...
}
}
改为使用泛型方法
public class Collections{
public static <T, S extends T> void copy(List<T> dest, List<S> src)
{
...
}
}
类型通配符与泛型方法(在方法签名中显式声明类型形参)还有一个显著的区别,类型通配符即可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参只能在对应方法中显式声明。
Java 7的“菱形”语法与泛型构造器
public class Foo {
public <T> Foo(T t)
{
System.out.println(t);
}
public static void main(String[] args)
{
// 泛型构造器中的T参数为String
new Foo("a" );
// 泛型构造器中的T参数为Integer
new Foo(200);
// 显示指定泛型构造器中的T参数为String
// 传给Foo构造器的实参也是String对象,完全正确
new <String> Foo("a");
//new <String> Foo(12.3);
}
}
Java 7新增的“菱形”语法允许调用构造器时在构造器后使用一对尖括号来代表泛型信息,但如果程序显式的指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。如下所示:
public class MyClass <E>
{
public <T> MyClass(T t)
{
System.out.println("t参数的值为:" + t);
}
public static void main(String[] args)
{
// MyClass类声明中的E形参是String类型
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<String>(5);
// 显示指定泛型构造器中声明的T形参是Integer类型
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// MyClass类声明中的E形参是String类型
// 如果显示指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用“菱形”语法,下面的代码是错误的
MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}
设定通配符的下限
简单来讲就是可以设定传入的参数是某个参数的子类
现在实现一个工具:将src集合里的元素复制到dest集合里,代码如下:
public static <T> void copy(Collection<T> dest, Collection<? extends T> src)
{
for (T ele : src)
{
dest.add(ele);
}
}
假设上面的方法需要一个返回值,返回最后一个被复制的元素,代码如下:
// 返回的其实是Number类,但是实际返回的却是Integer,最后一个元素
public static <T> T copy(Collection<T> dest, Collection<? extends T> src)
{
T last = null;
for (T ele : src)
{
last = ele;
dest.add(ele);
}
return last;
}
调用上面的代码:
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
// 下面代码会引起编译错误
// 因为最后返回的是Number类型
Integer last = copy(ln, li);
为了解决这个问题,可以使用表示类型下限的表达式super,使用这种语句,就可以保证程序最后返回的值的类型是第一个参数的子类,而不是笼统的Number类型,代码如下:
public class MyUtils {
public static <T> T copy(Collection<? super T> dest, Collection<T> src)
{
T last = null;
for (T ele : src)
{
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args)
{
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
li.add(6);
Integer last = copy(ln, li);
System.out.println(last);
}
}
TreeSet类的实现
TreeSet类也是使用了设置通配符下限的方式
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的灵活性。
public class TreeSetTest {
public static void main(String[] args)
{
TreeSet<String> ts1 = new TreeSet<>(
new Comparator<Object>()
{
@Override
public int compare(Object fst, Object snd) {
return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 :0;
}
}
);
ts1.add("Hello");
ts1.add("wa");
TreeSet<String> ts2 = new TreeSet<>(
new Comparator<String>()
{
@Override
public int compare(String fst, String snd) {
return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 :0;
}
}
);
ts1.add("Hello");
ts1.add("wa");
}
}
泛型方法与方法重载
下面的两个方法虽然写法不一样,但在表达上面是一样的,所以不会通过编译。
public static <T> void copy(Collection<T> dest, Collection<? extends T> src)
{
...
}
public static <T> T copy(Collection<? super T> dest, Collection<T> src)
{
...
}
Java 8改进的类型推断
public class MyUtil <E>{
public static <Z> MyUtil <Z> nil()
{
return null;
}
public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail)
{
return null;
}
E head()
{
return null;
}
public static void main(String[] args)
{
// 可以通过方法赋值的目标参数(MyUtil<String> ls)来推断类型参数为String
MyUtil<String> ls = MyUtil.nil();
// 无需使用下面语句在调用nil()方法时指定类型参数的类型,<String>
MyUtil<String> mu = MyUtil.<String>nil();
// 可调用cons()方法所需的参数类型来推断类型参数为Integer
MyUtil.cons(42, MyUtil.nil());
// 无须使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil.cons(42, MyUtil.<Integer>nil());
// 无法推断,必须显示指定类型参数
// String s1 = MyUtil.nil().head();
// 改为下面的形式
String s2 = MyUtil.<String>nil().head();
}
}
擦除与转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数,如果没有为这个泛型类型指定实际的类型参数,则该类型参数被称作raw type(原始类型),默认是声明该类型参数时指定的第一个上限类型。
下面程序定义了一个带泛型声明的Apple类,其类型形参的上限是Number,这个类型形参用来定义Apple类的size变量
class Apple<T extends Number>
{
T size;
public Apple(){}
public Apple(T size)
{
this.size = size;
}
public void setSize(T info)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6);
System.out.println(a.getSize());
// 把a对象赋给Apple变量,将丢失尖括号里的类型信息
Apple b = a;
// b只知道size的类型是Number
// 因为类型是默认声明该类型参数时指定的第一个上限类型Number
Number size1 = b.getSize();
// 下面代码引起编译错误
//Integer size2 = b.getSize();
}
}
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
// 丢失泛型信息,这是典型的擦除
List list = li;
// 下面代码引起“未经检查的转换”警告,编译、运行时完全正常
List<String> ls = list;
// 但只要访问ls里的元素,就会引起运行时异常
// ls实际上引用的是List<Integer>集合
// 所以,当试图把该集合里的元素当成String类型的对象取出来的时候
// 将引发异常
System.out.println(ls.get(0));
下面的代码与上面的类似
List li = new ArrayList<>();
li.add(6);
li.add(9);
// 当试图通过强制类型转换把它转换成一个String,将引发运行时异常
// 因为实际引用的是List<Integer>
System.out.println((String)li.get(0));
泛型与数组
数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符,但可以声明元素类型包含类型变量或类型形参的数组。
// 错误用法
List<String>[] las1 = new ArrayList<String>[10];
// 正确用法,采用无上限的类型通配符
List<?>[] las2 = new ArrayList<?>[10];
下面的用法会引起“未经检查的转换”的警告,即编译器并不能保证这段代码是类型安全的。
List<String>[] las3 = new ArrayList[10];
Java允许创建无上限的通配符泛型数组,例如new ArrayList
List<?>[] lsa = new ArrayList<?>[10];
// 在这里同步两个变量
Object[] oa = lsa;
// 创建一个List集合
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(4));
// 将List集合放到Object[] oa里面
oa[1] = li;
// oa在这里是Object类型
System.out.println(oa[1].toString().getClass());
// 在这里是Integer类型
// 貌似泛型自动将同步进去的变量转换为Integer类型了
System.out.println(lsa[1].get(0).getClass());
// 所以转换将会引发编译异常
// java.lang.Integer cannot be cast to java.lang.String
// String s = (String)lsa[1].get(0);
// 为了保证转换成功,在此使用instanceof
Object target = lsa[1].get(0);
if (target instanceof String)
{
System.out.println("可以转换");
String sa = (String) target;
}
创建元素类型是类型变量的数组对象也将导致编译错误,如下
<T> T[] makeArray(Collection<T> coll)
{
// 下面代码导致编译错误
return new T[coll.size()]
}
由于类型变量只在编译时存在,在运行时不存在,而编译器无法确定实际类型是什么,因此编译器报错。