文章目录
一、基本概念
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。
在没有泛型的时候,一旦把一个元素放进容器中,容器就会忘记该对象的类型,把所有的元素都当成 Object 类型处理。当程序从容器中取出对象时,就需要进行强制类型转换,这种强制类型转换会使代码臃肿,降低代码可读性;而且容易导致 ClassCastException 异常。示例如下:
public class ListErr{
public static void main(String[] args){
//创建一个纸箱保存字符串的 List 容器
List list = new ArrayList();
list.add("一个字符串");
//把 Integer 对象放进容器并不会引发错误
list.add(21);
//试图把 Integer 转换为 String, 引发 ClassCastException 异常
list.forEach(str -> System.out.printIn(((String)str).length()));
}
}
Java 引入泛型很大程度上是为了让容器能够记住其保存的元素的数据类型。增加了泛型支持后的容器可以记住容器中元素的类型,可以在编译时检查容器中元素的类型,如果试图向容器中添加不满足类型要求的元素,编译器会抛出错误。将上述示例修改为泛型支持的:
public class ListRig{
public static void main(String[] args){
//创建一个纸箱保存字符串的 List 容器
List list = new ArrayList();
list.add("一个字符串");
//把 Integer 对象放进容器,引发编译错误
list.add(21);
list.forEach(str -> System.out.printIn(((String)str).length()));
}
}
二、泛型的使用
泛型有三种常用的使用方式:泛型接口,泛型类和泛型方法。
1、泛型接口
定义泛型接口的示例如下:
//定义该接口时指定了一个类型形参,该形参名为 E
public interface Iterator<E>{
//在该接口里 E 完全可以作为类型使用
E next();
boolean hasNext();
...
}
//定义该接口时指定了两个类型形参,其形参名为 K、V
public interface Map<K, V>{
//在该接口里 K、V 完全可以作为类型使用
Set<K> KeySet();
V put(K key, V value);
...
}
2、泛型类
自定义一个泛型类 Apple 的示例如下:
//定义 Apple 类时使用了泛型声明
public class Apple<T>{
//使用 T 类型形参定义实例变量
private T info;
//使用 T 类型形参来定义构造器
public Apple(T info){
this.info = info;
};
public void setInfo(T info){
this.info = info;
}
public T getInfo(){
return this.info;
}
public static void main(String[] args){
//由于传给 T 形参的是 String,所以构造器参数只能是 String
Apple<String> a1 = new Apple<>("");
System.out.printIn(a1.getInfo());
//由于传给 T 形参的是 Double,所以构造器参数只能是 Double
Apple<Double> a2 = new Apple<>(2.56);
System.out.printIn(a2.getInfo());
}
}
说明:如上所示,当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。
2.1 从泛型类派生子类
当创建了带泛型声明的接口、类之后,在实现这些接口或者派生子类时不能再包含类型形参。错误示例如下:
public calss A extends Apple< T>{}
在使用类、接口和方法时,应当为类型参数传入实际的类型,示例如下:
//使用 Apple 类时为 T 形参传入 String 类型
public calss A extends Apple<String>{
}
调用方法时必须为类型参数传入实际的类型,使用类和接口时可以不用,此时编译器会发出泛型警告:使用了未经检查或不安全的操作。另外,系统会把 Apple< T > 类里的 T 形参当成 Object 类型进行处理。示例如下:
//使用 Apple 类时,不为 T 形参传入实际的类型参数
public calss A extends Apple{
}
附:泛型类并不存在
不管为泛型的类型传入哪一种类型实参,因此对于 Java 来说,ArrayList、ArrayList< String> 和 ArrayList< Integer> 都会被 Java 当成同一个类来处理,它们在运行时时同样的 class。因此在静态方法、静态初始化块和静态变量的声明和初始化中不允许使用类型形参。由于系统不会生成泛型类,所以 instanceof 运算符之后不能使用泛型类。
3、泛型方法
泛型方法就是在声明方法时定义一个或多个类型形参。泛型方法的语法格式如下:
修饰符 <T, S> 返回值类型 方法名(形参列表){
//方法体...
}
泛型方法的使用示例如下:
public class Test{
//声明一个泛型方法,该泛型方法中带有一个 T 类型形参
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for(T o : a){
c.add(o);
}
}
public static void main(String[] args){
Integer[] 1a = new Integer[100];
Collection<Number> cn = new ArrayList<>();
//此时 T 代表 Number 类型
fromArrayToCollection(ia, cn);
}
}
三、类型通配符
当使用一个泛型类时,都需要为这个泛型类传入一个类型实参。当需要定义一个方法,方法里有一个容器形参,容器形参的元素类型是不确定的,此时则可以使用类型通配符。类型通配符是一个问号(?),讲一个问号作为类型实参传给容器,表示元素类型未知的容器。示例如下:
public void test(List<?> c){
for(int i - 0; i < c.size(); i++){
System.out.printIn(c.get(i));
}
}
此时调用 test() 方法的时候,传入的参数类型可以为 List< String>、List< Integer> 或 List< Double> 等。
带通配符的容器表示它是各种泛型容器的父类,但是并不能把元素加入其中,因为程序无法确定容器中的元素的类型,示例如下:
List<?> c = new ArrayList<String>();
//添加时引发编译时错误
c.add(new Object());
3.1 设定类型通配符的上限
当直接使用 List<?> 时,表明这个 List 容器可以是任何泛型 List 的父类。但是有时程序不希望这个 List<?> 是所有泛型 List 的父类,只希望它代表某一类泛型 List 的父类,此时可以设定类型通配符的上限。示例如下:
public class Test{
public void test(List<? extends Number> numbers){
for(Number num : numbers){
System.out.printIn(num);
}
}
}
此时调用 test() 方法的时候,传入的参数类型可以为List< Integer> 或 List< Double> 等,只要 List 后尖括号里的类型是 Number 的子类型(也可以是 Number 本身)即可。
3.2 设定类型通配符的下限
与设定类型通配符的上限的方法类似,程序也可以设定设定类型通配符的下限。示例如下:
public class Test{
public void test(List<? super Integer> objects){
for(Object obj : objects){
System.out.printIn(obj);
}
}
}
此时调用 test() 方法的时候,传入的参数类型可以为List< Integer> 或 List< Number> 等,只要 List 后尖括号里的类型是 Integer的父类型(也可以是 Integer 本身)即可。
3.3 附:设定类型形参的上限或下限
Java 泛型允许在使用类型通配符时设定上限,也可以在定义类型形参时设定上限。示例如下:
public class Apple<T extends Number>{
...
}
使用 Apple 类时为 T 形参传入的实际类型参数只能是 Number 或 Number 类的子类。
设定下限的方式也相似。
四、类型通配符和泛型方法的区别
大多数时候都可以使用泛型方法来代替类型通配符。示例如下:
对于 Collection 中使用类型通配符实现的两个方法:
public interface Collection<E>{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
...
}
将其改为泛型方法的形式:
public interface Collection<E>{
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
...
}
泛型方法和通配符也可以同时使用。示例如下:
对于 Collections.copy() 方法:
public class Collections{
public static <T> void copy(List<T> dest, List<? extends T> src){
...}
...
}
也可以将其方法改为泛型方法,不使用类型通配符:
public class Collections{
public static <T, S extends T> void copy(List<T> dest, List<S> src){
...}
...
}
五、泛型的擦除和转换
擦除:当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都会被扔掉。
转换:对泛型而言,可以直接把一个 List 对象赋给一个 List< String> 对象,编译器仅提示未经检查的转换。
泛型的擦除和转换实例如下:
public class Test1{
public static void main(String[] args){
List<Integer> li = new ArrayList<>;
li.add(2);
li.add(3);
//泛型擦除,丢失尖括号里的类型信息
List list = li;
//引起“未经检查的转换”警告,但编译和运行时完全正常
List<String> ls = list;
//但是在访问时会出错,引发运行时异常
System.out.printIn(ls.get(0));
}
}
六、泛型与数组
Java 的数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符,但可以声明元素类型包含类型变量或类型形参的数组。因此可以这样使用类型通配符创建数组:
List<?>[] ls = new ArrayList<?>[10];
也可以这样子:
//会提示“未经检查的转换”的警告
List<String>[] ls = new ArrayList[10];
但是不能这样子:
List<String>[] ls = new ArrayList<String>[10];
可以使用反证法来证明不能这样子使用。即假设 Java 支持创建 ArrayList< String>[10] 这样的数组对象:
//实际上不允许的代码
List<String>[] lsa = new ArrayList<String>[10];
//将 lsa 向上转型为 Object 类型的变量
Object[] oa = (Object[])lsa;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
//将 List<Integer> 对象作为 oa 对象的第二个元素,不会引发任何警告
oa[1] = li;
//同样不会引发警告,但是会引起 ClassCastException 异常
String s = lsa[1].get(0);
此段代码违反了 Java 的设计原则之一:如果一段代码在没有提示“未经检查的转换”的警告时,则程序在运行时不会引起 ClassCastException 异常。因此不该方式合法。