一名合格的Java程序员,当然要经常翻翻JDK的源代码。经常看JDK的API或者源代码,我们才能更加了解JDK,才能更加熟悉底层。
一、引出泛型
然而,在看源代码的过程中,我们经常会看到类似于如下这样的代码:
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}
上面代码出自HashMap中的一段。代码中的泛型,虽然不影响我们看懂这段代码,但是属于半个处女座的我,还是有着强烈的强迫症,就有那么一股冲动,怂恿着我把它搞清楚。
我们可以观察一下上面的代码,“? extends K”,很明显,这应该是某种继承关系的体现,但是什么时候会用到这种泛型里的继承关系呢?除了这种泛型关系,你应该还见过一些其他的表现形式,那么好,如果你想搞清楚这些,就好好看这篇文章吧,我相信:搞清楚这些,一定会对你做研发时,封装代码,有很大的帮助。
二、泛型的定义
泛型,大家在学习,工作中,一般都会多多少少的接触过一些。所以,关于定义,我们长话短说,帮大家复习一下即可。
泛型是在JDK1.5增加的,主要用来标记Java集合中元素的数据类型。怎么讲呢?
在JDK1.5之前,一旦把一个对象丢进Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序冲集合中取出对象时,就需要进行强制类型转换,这种做法,不仅代码看起来很臃肿,而且容易引发类型转换异常ClassCastException。
而通常情况下使用集合,一个集合,只存放同一类型的东西。即一个定义了的String类型的List,不能盛放Integer类型的数据。
而泛型,可以标记或规定集合中元素的类型,并且在编译时,检查其类型。
三、泛型的应用
1、定义泛型接口、类
所谓泛型:就是允许在定义类、接口时,指定类型形参,这个类型形参将在声明变量、创建对象时确定(即传入实际的类型参数,也可以成为类型实参)。
搞个例子看看,List、ListIterator、Map接口,都是在定义时指定类型,而类型的确定是在创建变量的时候,如List<String> list= new ArrayList<String>(),创建list变量时,确定类型实参为String类型:
//定义接口时指定了一个类型形参,该形参名为E
public interface ListA<E> extends Collection<E> {
//在该接口里,E可作为类型使用
boolean add(E e); //参数类型
ListIterator<E> listIterator();
}
//定义接口时,指定你了一个类型形参,该形参名为E
public interface ListIterator<E> extends Iterator<E>{
//在该接口里E完全可以作为类型使用
E next();
E previous();
void set(E e);
}
//定义该接口时,指定了两个类型形参,其形参名为K、V
public interface Map<K,V>{
//在该接口里K,V完全可以作为类型使用
Set<K> keySet();
V put(K key, V value);
void putAll(Map<? extends K, ? extends V> m);
}
这就是泛型的实质:允许在定义接口、类时,指定类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用其他普通类型的地方,都可以使用这个类型形参。
上面方法声明返回值类型:ListIterator<E>、Set<K>,这表明Set<K>形式 是一种特殊的数据类型,是一种与Set不同的数据类型——我们可以认为Set<K>是Set类型的子类。
例如,我们使用List类型时,为E形参传入String类型实参,则产生了一个新的类型:List<String>类型,我们可以把List<String>想象成E被全部替换成String的特殊List子接口。
我们可以把List<String>想象成E被全部替换成String的特殊List子接口:
//List<String>等同于如下接口
public interface ListString extends List {
//原来的E形参全部变成String类型实参
boolean add(String e); //参数类型
ListIterator<String> listIterator();
}
通过上面这种方式,解决了一个问题:虽然程序只定义了一个List<E>接口,但实际使用时,可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。如List<String>,List<Integer>,List<Long>,List<Boolean>等等。
当然,List<String>绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘没有,内存中也没有。
PS:包含泛型声明的类型可以在定义变量、创建对象时,传入一个类型实参,从而可以动态生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
搞个实例:
//定义Apple类时使用了泛型声明
public class Apple<E> {
//使用E类型形参定义属性
private E info;
public Apple() {}
//下面方法中使用E类型形参来定义方法
public Apple(E info) {
this.info = info;
}
public E getInfo() {
return info;
}
public void setInfo(E info) {
this.info = info;
}
public static void main(String[] args) {
//因为传给T形参的是String实际类型,所以构造器的参数只能是String
Apple<String> a1 = new Apple<String>("苹果");
System.out.println(a1.getInfo());
//因为传给T形参的是Double实际类型,所以构造器的参数只能是Double或double
Apple<Double> a2 = new Apple<Double>(5.67);
System.out.println(a2.getInfo());
}
}
上面程序定义了一个带泛型声明的Apple<T>类,而在Main函数中,实际使用Apple<T>类时会为T形参传入实际类型,这样就可以生成如Apple<String>、Apple<Double>……形式的多个逻辑子类(物理上并不存在),这时创建对应的逻辑形参。
当创建带泛型声明的自定义类,为该类定义构造器时,构造器还是原来的类名,不要增加泛型声明。例如为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>,但调用该构造器时,却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。
2、从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从父类派生子类,但是:当使用这些接口、父类时不能再包含类型形参。
例如下面代码是错误的:
//定义类A继承Apple类, Apple类后面不能跟类型形参
public class A extends Apple<E> {}
注意:方法中的形参,只有当定义方法时,才可以使用数据形参,当调用方法时,必须为这些数据形参传入实际的数据;与此类似的是:类、接口中的类型形参,只有在定义类、接口时,才可以使用类型形参,当使用类、接口时,应为类型形参传入实际的类型。
所以,总结起来就是:方法、类、接口 中的类型形参,只有在定义时,才可以使用类型形参;在使用时,应为类型形参传入实际的类型。
所以,上面的代码可以改为如下(当然删除后面的String也是可以的):
//使用Apple类时,为T形参传入String类型
public class A extends Apple<String> {}
或
public class A extends Apple {}
如果从Apple<String>类派生子类,则在Apple类中所有使用E类型形参的地方都将被替换成String类型,即它的子类将会继承到String getInfo() 和 void setInfo(String info)两个方法,如果子类需要重写父类的方法,必须注意这一点。
搞个例子,解释一下:
public class subApple extends Apple<String> {
//正确重写了父类的方法,返回值与父类Apple<String>的返回值完全相同
public String getInfo(){
return "子类:"+super.getInfo();
}
// 下面方法是错误的,重写父类方法时,返回值类型不一致
// public Object getInfo(){
// return "子类";
// }
}
如果使用Apple类时没有传入实际的参数类型,系统会将Apple<E>类里的E形参当成Object类型处理。
上面例子是带泛型声明的父类派生子类,创建带泛型声明的接口的实现类于此几乎完全一样。
3、不存在泛型类
ArrayList<String>类,是一种特殊的ArrayList类,这个ArrayList<String>对象只能添加String对象作为集合元素。
但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。
下面看个例子:
import java.util.ArrayList;
import java.util.List;
public class CommonTest {
public static void main(String[] args) {
List<String> lst1 = new ArrayList<String>();
List<Integer> lst2 = new ArrayList<Integer>();
Boolean flag = false;
flag = lst1.getClass() ==lst2.getClass();
//true
System.out.println(flag);
}
}
上面程序的输出结果是true。因为不管泛型类型的实际类型参数是什么,它们在运行时总有同样的类(class)。
实际上,泛型对其所有可能的类型参数,都具有同样的行为,从而可以把相同的类当成许多不同的类来处理。
与此完全一致的是:类的静态变量和方法也在所有实例间共享,所以在静态方法、静态初始化 或者 变量的声明 和初始化 中,不允许使用类型形参。
下面程序演示了这种错误:
public class Test<E> {
E age;
//下面代码错误,不能再静态属性声明中使用类型形参
// static E info;
public void foo(E msg){}
//下面代码错误,不能在静态方法声明中使用类型形参
// public static void bar(E msg){}
}
由于系统不并不会正真生成泛型类,所以instanceof运算后,不能使用泛型类,如下代码是错误的:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Test2 {
public static void main(String[] args) {
Collection<String> cs = new ArrayList<String>();
//下面代码编译时引发错误:instanceof运算符后不能使用泛型类
// if (cs instanceof List<String>) {}
}
}
本文初步讲解了泛型基础内容,下一篇继续深入探讨。