一、定义泛型接口、类
JDK 1.5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
下面是 JDK 1.5 改写后 List 接口、Iterator 接口、Map 的代码片段:
// 定义接口时制定了一个类型形参,该形参名为 E
public interface List<E>
{
// 在该接口里,E 可作为类型使用
// 下面方法可以使用 E 作为参数类型
void add(E x);
Iterator<E> iterator();
}
// 定义接口时制定了一个类型形参,该形参名为 E
public interface Iterator<E>
{
// 在该接口里 E 完全可以作为类型使用
E next();
bollean hasNext();
}
// 定义接口时制定了两个类型形参,该形参名为 K、V
public interface Map<K, V>
{
// 在该接口里 K, V 完全可以作为类型使用
Set<K> keySet();
V put(K key, V value);
}
我们可以为任何类增加泛型的声明(并不是只有集合类才可以使用泛型声明,虽然泛型是集合类的重要使用场所)。例:
public class Apple<T>
{
// 使用 T 类型形参定义属性
private T info;
public Apple(){}
// 下面方法中使用 T 类型参数来定义方法
public Apple(T info)
{
this.info = info;
}
public void setInfo(T info)
{
this.info = info;
}
public T getInfo()
{
return this.info;
}
}
实例化带有泛型的 Apple 类:
二、从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或者从该父类来派生子类,但是,当使用这些接口、父类时不能再包含类型形参。下面的代码是错误的:
// 定义类 A 继承 Apple 类,Apple 类不能跟类型参数
public class A extends Apple<T>{}
如果想从 Apple 类派生一个子类,可以改为如下方法:
// 使用 Apple 类时,为 T 形参传入 String 类型
public class A extends Apple<String>
当然也可以不为接口、类传入的类型参数传入实际类型,所以下面的代码也是正确的:
public class A extends Apple
如果从 Apple<String> 类派生子类,则在 Apple 类中所使用 T 类型形参的地方都将被替换成 String类型,即它的子类将会集成到 String getInfo() 和 void setInfo(String info) 两个方法,如果子类需要重新写父类的方法,必须注意到这一点。例如:
public class A1 extends Apple<String>
{
// 正确重写了父类的方法,返回值与父类 Apple<String> 的返回值完全相同
public String getInfo()
{
return "子类" + super.getInfo();
}
/*
// 下面方法是错误的,重写父类方法时返回值类型不一致
public Object getInfo()
{
return "子类";
}
*/
}
三、并不存在泛型类
我们可以把 ArrayList<String> 类当做 ArrayList 的子类,而事实上系统并没有为 ArrayList<String> 生成新的 class 文件,而且也不会把 ArrayList<String> 当成新类来处理。
例如:下面程序的打印结果是true
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型参数。
下面程序演示了这种错误:
public class R<T>
{
// 下面程序代码错误,不能在静态属性声明中使用类型参数
static T info;
T age;
public void foo(T msg){}
// 下面代码错误,不能在静态方法声明中使用类型形参
public static void bar(T msg){}
}
由于系统中并不会真正生成泛型类,所以 instanceof 运算符后不能使用泛型类,例如下面的代码是错误的:
Collection cs = new ArrayList<String>();
// 下面代码编译时引发错误:instanceof 运算符后不能使用泛型类
if(cs instanceof List<String>){...}
四、类型通配符
如果 SubClass 是 SuperClass 的子类型(子类或者子接口),而 G 是具有泛型声明的类或者接口,那么 G<SubClass> 是 G<SuperClass> 的子类型并不成立。例如:List<String> 并不是 List<Object> 的子类。
与数组进行对比:
// 下面程序编译正常、运行正常
Number[] nums = new Integer[7];
nums[0] = 9;
System.out.println(nums[0]);
// 面程序编译正常、运行时发生 java.lang.ArrayStoreException 异常
Integer[] ints = new Integer[5];
Number[] nums2 = ints;
nums2[0] = 0.4;
System.out.println(nums2[0]);
// 下面程序发生编译异常,Type mismatch: cannot convert from List<Integer> to List<Number>
List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList;
数组和泛型有所不同,如果 SubClass 是 SuperClass 的子类型(子类或者子接口),那么 SubClass[] 依然是 SuperClass[] 的子类;但是 G<SubClass> 不是 G<SuperClass> 的子类。
如何适用类型通配符:
为了表示各种泛型 List 的父类,我们需要使用类型通配符,类型通配符是一个问号 (?),将一个问号作为类型实参传给 List 集合,写作:List<?> (意思是未知类型元素的 List)。这个问号 (?) 被称作通配符,它的元素类型可以匹配任何类型。例如:
public void test(List<?> c)
{
...
}
现在我们可以使用任何类型的 List 来调用它,程序依然可以访问集合 c 中的元素,其类型是 Object。
这种写法适用于任何支持泛型声明的接口和类,例如:Set<?>、Collection<?>、Map<?, ?>等。
但是这种带通配符的 List 仅表示它是各种泛型 List 的父类,并不能把元素加入到其中,例如下面的代码将引发编译错误:
List<?> c = new ArrayList<String>();
// 下面程序引发编译错误
c.add(new Object());
因为我们不知道上面程序中 c 集合中的元素类型,所以不能向其中添加对象。唯一的例外是 null,它是所有引用类型的实例。例如:下面程序是正确的:
c.add(null);
五、设置类型通配符的上限
当直接使用 List<?> 这种形式时,即表明这个 List 集合是任何泛型 List 的父类。但还有一种特殊的情况,我们不想这个 List<?> 是任何泛型 List 的父类,只想表示它是某一类泛型 List 的父类。
被限制的泛型通配符如下表示:
List<? extends SuperClass>
六、设定类型形参的上限
Java 泛型不仅允许在使用通配符形参时设定类型上限,也可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型必须是上限类型,或是该上限类型的子类。例如:
import java.util.*;
public class Apple<T extends Number>
{
T col;
public static void main(String[] args)
{
Apple<Integer> ai = new Apple<Integer>();
Apple<Double> ad = new Apple<Double>();
//下面代码将引起编译异常
//因为String类型传给T形参,但String不是Number的子类型。
Apple<String> as = new Apple<String>();
}
}
有时候程序需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限)表明该类型形参必须是其父类的子类(包括是父类本身也行),并且实现多个上限接口。例如:
// 表明 T 类型必须是 Number 类或其子类,并必须实现 java.io.Serializable 接口
public class Apple<T extends Number & java.io.Serializable>
{
...
}
七、泛型方法
1、定义泛型方法
泛型方法的用法格式是:
修饰符 <T, S> 返回值类型 方法名(形参列表)
{
// 方法体……
}
示例:
static void fromArrayToCollection(Object[] a, Collection<Object> c)
{
for(Object o : a)
{
c.add(o);
}
}
上面的方法中形参 c 的数据类型是 Collection<Object>,因为 Collection<Object> 不是 Collection<String> 类的父类,所以这个方法的功能非常有限,它只能将 Object 数组的元素复制到 Object (Object 的子类不行) Collection 集合,及下面的代码会引发编译异常:
String[] str = {"a", "b"};
List<String> strList = new ArrayList<String>();
// Collection<String> 对象不能当成 Collection<Object> 调用,下面的代码出现编译异常
fromArrayGToCollection(str, strList);
上面方法的参数类型不可以使用 Collection<String>,那是用通配符 Collection<?> 也是不可行的,因为不能把对象放进一个未知类型的集合当中去。
使用泛型方法解决这个问题:
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<Object>();
//下面代码中T代表Object类型
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
//下面代码中T代表String类型
fromArrayToCollection(sa, cs);
//下面代码中T代表Object类型
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
//下面代码中T代表Number类型
fromArrayToCollection(ia, cn);
//下面代码中T代表Number类型
fromArrayToCollection(fa, cn);
//下面代码中T代表Number类型
fromArrayToCollection(na, cn);
//下面代码中T代表String类型
fromArrayToCollection(na, co);
//下面代码中T代表String类型,但na是一个Number数组,
//因为Number既不是String类型,也不是它的子类,所以出现编译错误
fromArrayToCollection(na, cs);
}
上面程序定义了一个泛型方法,该泛型方法中定义了一个 T 类型形参,这个 T 类型形参就可以在该房内当成普通类型来使用。与在接口、类中定义的类型形参不同的是,方法声明中定义的类型形参只能在方法里使用,而接口、类声明中定义的类型形参则可以住在整个接口、类中使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无需显式传入实际类型参数,如上面程序中,当程序调用 fromArrayToCollection 时,无须在调用该方法前传入 String、Object 等类型,编译器可以根据实参推断出类型形参的值。
但是不要是编译器迷惑,例如下面的程序:
public class Test
{
// 声明一个泛型方法,该泛型方法中带一个 T 类型参数
static <T> void test(Collection<T> a, Collection<T> c)
{
// 方法体
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<Object>();
List<String> as = new ArrayList<String>();
// 下面代码将产生编译错误
test(as, ao);
}
}
上面程序中,编译器无法正确识别 T 所代表的实际类型。可以将该方法修改为下面的形式:
public class Test
{
// 声明一个泛型方法,该泛型方法中带一个 T 类型参数
static <T> void test(Collection<? extends T> a, Collection<T> c)
{
// 方法体
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<Object>();
List<String> as = new ArrayList<String>();
// 下面代码编译正常
test(as, ao);
}
}
上面代码中将方法的第一个形参类型修改为 Collection<? extends T>,这种采用类型通配符的表示方法,只要 test 方法的前一个 Collection 集合元素类型是后一个 Collection 集合元素类型的子类即可。
2、泛型方法和类型通配符的区别
JDK 中对于 Collection 接口中两个方法的定义:
public interface Collection<E>
{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}
上面两个方法都采用了类型通配符的形式,如果采用泛型方法形式来代替它们,如下所示:
public interface Collection<E>
{
boolean <T> containsAll(Collection<T> c);
boolean <T extends E> addAll(Collection<T> c);
}
上面方法使用了 <T extends E> 泛型形式,这是定义类型形参时设定上限。
3、设定通配符的下限
Java 允许设置通配符下限:<? super Type>,这个通配符表示它必须是 Type 本身,或者是 Type 的父类。示例:
public class MyUtils
{
// 下面 dest 集合元素类型必须与 src 集合元素类型相同,或是其父类
public static <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<Number>();
List<Integer> ln = new ArrayList<Integer>();
li.add(5);
// 此处可准确的知道最后一个被复制的元素是 Integer 类型(与 src 集合元素的类型相同)
Integer last = copy(ln, li);
System.out.println(ln);
}
}