@
为什么要使用泛型
Java 集合有个缺点一一把一个 对象"丢进"集合里之后,集合就会"忘记"这个对象的数据类型 ,当再次取出该对象时 , 该对象的编译类型就变成 了 Object 类型(其运行时类型没变) 。
Java 集合之所以被设计成这样,是因为集合 的 设计者不知道我们会用集合来保存什么类型的对象 ,所以他们把集合设计成能保存任何类型 的对象,只要求具有很好的通用性 。 但这样做带来如下两个问题 :
集合对元素类型没有任何限制,这样可能引发一些问题 。 例如,想创建一个 只能保存 Dog 对象的集合,但程序也可以轻易地将 Cat 对象"丢"进去,所以可能引发异常 。
由于把对象"丢进"集合时 , 集合丢失了对象的状态信息,集合只知道它盛装的是 Object,因此取出集合元素后通常还需要进行强制类型转换 。 这种强制类型转换既增加 了编程的复杂度 ,也可能引发ClassCastException异常。
使用泛型
从 Java 5 以后, Java 引入了"参数化类型 Cpara meterized type )" 的概念 ,允许程序在创建集合时指定集合元素的类型 ,如List , 这表 明 该 List 只能保存字符串类型的对象 。 Java 的参数化类型被称为泛型 (Generic) 。
GenericList.java
public class GenericList
{
public static void main(String[] args)
{
// 创建一个 只想保存字符串的List集合
List<String> strList = new ArrayList<String>(); // ①
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
// 下面代码 将引起编译错误
// strList.add(5); // ②
strList.forEach(str -> System .out.println(str.length())); // ③
}
}
深入泛型
泛型,就是允许在定义类、接口、方法 时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用 方法 时动态地指定(即传入实际的类型参数,也可称为类型实参 ) 。
定义泛型接口、类
下面是 Java 5 改写后 List 接口、 Itera tor 接口、 Map 的代码 片段 。
// 定义接口时指定了 一个 泛型形参,该形参名为 E
public interface List<E>{
// 在该接口里. E 可作为类型使用
// 下面方法 可以使用 E 作为参数类型
void add (E x);
Itera tor<E> itera tor(); //①
// 定义接口时指定了一个 泛型形参 ,该形参名为 E
}
public interface Itera tor<E>{
//在该接口里 E 完全可以作为类型使用
E next() ;
boolean hasNext() ;
// 定义该接口时指定了两个泛型形参,其形参名为 K 、 v
}
public interface Map<K,V>{
// 在该接口里 K 、 V 完全可以作为类型使用
Set<K> keySet() //②
V put(K key,V value)
}
尖括号中的内容 一一就是泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参 。
可以为任何类、接口增加 泛型声明(并不是只有集合类才可以使用泛型声明 ,虽然集合类是泛型的重要使用场所) 。 下面自定义 一个 Apple 类,这个 Apple 类就可以包含一个 泛型声明 。
Apple.java
// 定义Apple类时使用了泛型声明
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;
}
public static void main(String[] args)
{
// 由于传给T形参的是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System .out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
System .out.println(a2.getInfo());
}
}
当使用一个 泛型类时 (包括 声明变量和创建对象两种情况) , 都应该为这个泛型类传入一个 类型实参。如果没有传入类型实际参数 , 编译器就会提出泛型警告 。 假设现在需要定义一个 方法 , 该方法 里有一个 集合形参,集合形参的元素类型是不确定的, 那应该怎样定义呢?
使用类型通配 符
为了表示各种泛型 List 的父类 ,可以使用类型通配 符,类型通配 符是一个 问号 ( ?) ,将一个 问号作为类型实参传给 List 集合 , 写作: List<?> (意思是元素类型未知的 List ) 。 这个问号(?)被称为通配 符,它的元素类型可以匹配任何类型 。
public void test(List<?> c){
for (int i = 0 ; i < c . size() ; i++ ){
System . out .println (c.get(i) );
}
}
在使用任何类型的 List 来调用 它,程序依然可以访问集合 c 中的元素,其类型是 Object,这永远是安全的,因为不管 List 的真实类型是什么,它包含的都是Object 。
但这种带通配 符的 List 仅表示它是各种泛型 List 的父类 ,并不能把元素加入到其中 。 例如,如下代码 将会引起编译错误 :
List<?> c = new ArrayLi st<String> ();
//下丽程序引起编译错误
c . add(new Object()) ;
因为程序无法确定 c 集合中元素的类型,所以不能向其中添加 对象 。 根据前面的 List接口定义的代码 可以发现 : add()方法 有类型参数 E 作为集合的元素类型,所以传给 add 的参数必须是 E 类的对象或者其子类的对象 。 但因为在该例中不知道 E 是什么类型,所以程序无法将任何对象"丢进"该集合 。 唯一的例外是 nulL ——它是所有引用类型的实例 。
另 一方面 , 程序可以调用 get()方法 来返回 List<?>集合指定索引处的元素,其返回值是一个 未知类型,但可以肯定的是,它总是一个 Object 。 因此,把 get()的返回值赋值给一个 Object 类型的变量,或者放在任何希望是 Object 类型的地方都可以 。
设定类型通配 符的上限
当直接使用 List这种形式时,即表明这个 List 集合可以是任何泛型 List 的父类 。 但还有一种特殊的情形,程序不希望这个 List是任何泛型 List 的父类 ,只希望它代表某一类泛型 List 的父类 。
一个 简单的绘图程序,下面先定义三个形状类 :
Shape.java
// 定义一个 抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
Circle.java
// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法 ,以打印字符串来模拟画图方法 实现
public void draw(Canvas c)
{
System .out.println("在画布" + c + "上画一个 圆");
}
}
Rectangle.java
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法 ,以打印字符串来模拟画图方法 实现
public void draw(Canvas c)
{
System .out.println("把一个 矩形画在画布" + c + "上");
}
}
上面定义了 三个形状类,其中 Shape 是一个 抽象父类 , 该抽象父类 有两个子类 : Circle 和 Rectangle 。接下来定义一个 Canvas 类 , 该画布类可以画数量 不等的形状 (Shape 子类的对象) 。
Canvas.java
public class Canvas
{
// // 同时在画布上绘制多个形状
// public void drawAll(List<Shape> shapes)
// {
// for (Shape s : shapes)
// {
// s.draw(this);
// }
// }
// 同时在画布上绘制多个形状,使用被限制的泛型通配 符
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
public static void main(String[] args)
{
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,// 所以下面代码 引发编译错误
c.drawAll(circleList);
}
}
程序中使用了被限制的泛型通配 符。
被限制的泛型通配 符表示如下 :
//它表示泛型形参必须是 Shape 子类的 List
List<? extends Shape>
List<? extends Shape>是受限制通配 符的例子,此处的问号 (?) 代表一个 未知的类型,就像前面看到的通配 符一样 。 但是此处的这个未知类型一定是 Shape 的子类型(也可以是 Shape 本身),因此可以把 Shape 称为这个通配 符的上限 (upper bound) 。
类似地,由于程序无法确定这个受限制的通配 符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中 。 例如,下面代码 就是错误 的:
public void addRectangle(List<? extends Shape> shapes){
//下面代码 引起编译错误
shapes .add(O,new Rectangle());
}
简而言之,这种指定通配 符上限的集合,只能从集合中取元素(取出的元素总是上限的类型) ,不能向集合中添加 元素(因为编译器没法确定集合元素实际是哪种子类型) 。
设定类型通配 符的下限
除可以指定通配 符的上限之外, Java 也允许指定通配 符的下限,通配 符的下限用<? super 类型>的方式来指定,通配 符下限的作用与通配 符上限的作用恰好相反 。
指定通配 符的下限就是为了支持 类型型变 。 比如 Foo 是 Bar 的子类,当程序需要一个 A<? super Bar>变量时,程序可以将 A<Foo> 、 A<Object>赋值给 A<? super Bar>类型的变量,这种型变方式被称为逆变。
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类 型,但具体是哪种父类 型则不确定。
因此,这种逆变的泛型集合能向其中添加 元素(因为实际赋值的集合元素总是逆变声明的父类 ) ,从集合中取元素时只能被当成 Object 类型处理(编译器无法确定取出的到底是哪个父类 的对象)。
假设实现一个 工具方法 :实现将 src 集合中的元素复制到 dest 集合的功能 ,因为 dest 集合可以保存 src 集合中的所有元素,所以 dest 集合元素的类型应该是 src 集合元素类型的父类 。
对于上面的 copy() 方法 ,可以这样理解两个集合参数之间的 依赖关系;不管 src 集合元素的类型是什么,只要 dest 集合元素的类型与前者相同或者是前者的父类 即可,此时通配 符的下限就有了用武之地 。
下面程序采用通配 符下限的方式来实现该 copy() 方法 :
MyUtils.java
public class MyUtils
{
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T cop y(List<? super T> dest,List<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);
// 此处可准确的知道最后一个 被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = cop y(ln,li); // ①
System .out.println(ln);
}
}
使用这种语句,就可以保证程序的①处调用 后推断出最后一个 被复制的元素类型是 Integer,而不是笼统的 Number 类型 。
实际上, Java 集合框架中的 TreeSet有一个 构造器也用到了这种设定通配 符下限的语法,如下所示 :
//下面的 E 是定义 TreeSet 类时的泛型形参
TreeSet(Compara tor<? super E> c)
通过这种带下限的通配 符的语法 ,可以在创建 TreeSet 对象时灵活地选择合适的 Compara tor 。 假定需要创建一个 TreeSet<String>集合,并传入一个 可以比较 String 大小 的 Compara tor , 这个 Compara tor既可以是 Compara tor,也可以是 Compara tor一一只要尖括号里传入的类型是 String 的父类 型(或它本身) 即可。
TreeSetTest.java
public class TreeSetTest
{
public static void main(String[] args)
{
// Compara tor的实际类型是TreeSet的元素类型的父类 ,满足要求
TreeSet<String> ts1 = new TreeSet<>(
new Compara tor<Object>()
{
public int compare(Object fst,Object snd)
{
return hashCode() > snd.hashCode() ? 1
: hashCode() < snd.hashCode() ? -1 : 0;
}
});
ts1.add("hello");
ts1.add("wa");
// Compara tor的实际类型是TreeSet元素的类型,满足要求
TreeSet<String> ts2 = new TreeSet<>(
new Compara tor<String>()
{
public int compare(String firs t,String second)
{
return firs t.length() > second.length() ? -1
: firs t.length() < second.length() ? 1 : 0;
}
});
ts2.add("hello");
ts2.add("wa");
System .out.println(ts1);
System .out.println(ts2);
}
}
设定泛型形参的上限
J ava 泛型不仅允许在使用通配 符形参 时设定上限,而且可以在定义泛型形参 时设定 上限 ,用于表示传给该泛型形参的实际类型要么是该上限类型 ,要么是该上限类型的子类。
下面程序示范了这种用法 :
Apple.java
public class Apple<T extends Number>
{
T col;
public static void main(String[] args)
{
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面代码 将引起编译异常,下面代码 试图把String类型传给T形参
// 但String不是Number的子类型,所以引发编译错误
// Apple<String> as = new Apple<>(); // ①
}
}
上面程序定义了 一个 Apple 泛型类 , 该 Apple 类的泛型形参的上限是 Number 类,这表明使用 Apple类时为 T 形参传入的实际类型参数只能是 Number 或 Number 类的子类 。 上面程序在①处将引起编译错误 : 类型 T 的上限是 Number 类型,而此处传入的实际类型是 String 类型 ,既不是 Number 类型,也不
是 Number 类型的子类型,所以将会导致编译错误 。
在一种更极端的情况下,程序需要为泛型形参设定多个上限 (至多有一个 父类 上限,可以有多个接口上限),表明该泛型形参必须是其父类 的子类(是父类 本身也行),并且实现多个上限接口。
如下代码 所示 :
// 表明 T 类型必须是 Number 类或其子类,并必须实现 java.io.Seria1izab1e 接口
pub1ic c1ass Apple<T extends Number & java. i o . Serializab1e>{
……
}
假设需要实现这样一个 方法 一一该方法 负责将一个 Object 数组的所有元素添加 到一个 Collection 集合中 。
考虑采用如下代码 来实现该方法 :
static void fromArrayToCollection(Object[) a,Collection<Object> c){
for (Object 0 : a){
c . add (o);
}
}
上面定义的方法 没有任何问题,关键在于方法 中的 c 形参,它的数据类型是 Collection<Object>。 Collection不是 Collection的子类型一一所以这个方法 的功能 非常有限,它只能将 Object[]数组的元素复制到元素为 Object ( Object 的子类不行)的 Collection 集合中。
下面代码 将引起编译错误 :
String[] strArr = {"a","b " };
List<String> strList = new ArrayList<>() ;
/ / Collection<String>对象不能当成 Collection<Object>使用,下面代码 出现编译错误
fromArrayToCollection(strArr,strList);
为了解决 这个问题,可以使用 Java 5 提供的泛型方法 (Generic Method)。所谓泛型方法 ,就是在声明方法 时定义→个或多个泛型形参。
泛型方法 的语法格式如下:
修饰符 <T,S> 返回值类型方法 名(形参列表){
//方法 体 .. .
}
采用支持 泛型的方法 ,就可以将上面的fromArrayToCollection 方法 改为如下形式:
static <T> void fromArrayToCollection (T[] a,Collection<T> c){
for (T 0 : a){
c.add(o) ;
}
}
下面程序示范了完整的用法 :
Genericme thodTest.java
public class Genericme thodTest
{
// 声明一个 泛型方法 ,该泛型方法 中带一个 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);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
// 下面代码 中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<>();
// 下面代码 中T代表Number类型
fromArrayToCollection(ia,cn);
// 下面代码 中T代表Number类型
fromArrayToCollection(fa,cn);
// 下面代码 中T代表Number类型
fromArrayToCollection(na,cn);
// 下面代码 中T代表Object类型
fromArrayToCollection(na,co);
// 下面代码 中T代表String类型,但na是一个 Number数组,
// 因为Number既不是String类型,
// 也不是它的子类,所以出现编译错误
// fromArrayToCollection(na,cs);
}
}
泛型方法 和类型通配 符的区别
大多数时候都可以使用泛型方法 来代替类型通配 符 。
例如,对于 Java 的 Collection 接口中两个方法 定义 :
public interface Collection<E>{
boolean containsAll (Coll ection<?> c);
boolean addAll(Collection<? extends E> c) ;
……
}
上面集合中两个方法 的形参都采用了类型通配 符的形式,也可以采用 泛型方法 的形式, 如下所示 :
public interface Collection<E>{
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Col工ection<T> c) ;
……
}
上面方法 使用了 <T extends E>泛型形式 , 这时定义泛型形参时设定上限(其中 E 是 Collection 接口里定义的泛型,在该接口里 E 可当成普通类型使用) 。
上面两个方法 中泛型形参 T 只使用了 一次,泛型形参 T 产生的唯一效果 是可以在不同的调用 点传入不同的实际类型 。对于这种情况,应该使用通配 符 : 通配 符就是被设计用来支持 灵活的子类化的 。
泛型方法 允许泛型形参被用来表示方法 的一个 或多个参数之间的 类型依赖关系,或者方法 返回值与参数之间的 类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法 。
Java 7 的"菱形"语法与泛型构造器
正如泛型方法 允许在方法 签名中声明泛型形参一样, Java 也允许在构造器签名中声明泛型形参 ,这样就产生了所谓的泛型构造器 。
一旦定义了泛型构造器,接下来在调用 构造器时,就不仅可以让 Java 根据数据参数的类型来"推断"泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型 。
如下程序所示:
GenericConstructor.java
class Foo
{
public <T> Foo(T t)
{
System .out.println(t);
}
}
public class GenericConstructor
{
public static void main(String[] args)
{
// 泛型构造器中的T类型为String。
new Foo("疯狂Java讲义");
// 泛型构造器中的T类型为Integer。
new Foo(200);
// 显式指定泛型构造器中的T类型为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new <String> Foo("疯狂Android讲义");
// 显式指定泛型构造器中的T类型为String,
// 但传给Foo构造器的实参是Double对象,下面代码 出错
new <String> Foo(12.3);
}
}
Java 7 新增 的"菱形"语法 ,它允许调用 构造器时在构造器后使用 一对尖括号来代表泛型信息 。 但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用"菱形"语法 。
如下程序所示 :
GenericDiamondTest.java
class MyClass<E>
{
public <T> MyClass(T t)
{
System .out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest
{
public static void main(String[] args)
{
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码 是错的。
// MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}
擦除和转换
在严格的泛型代码 里,带泛型声明的类总应该带着类型参数 。 但为了与老的 Java 代码 保持一致,也允许在使用带泛型声明的类时不指定实际的类型 。 如果没有为这个泛型类指定实际的类型, 此时被称作 raw type (原始类型) , 默 认是声明该泛型形参时指定的第一个 上限类型。
当把一个 具有泛型信息的对象赋给另 一个 没有泛型信息的变量时,所有在尖括号之间的 类型信息都将被扔掉 。 比如一个 List类型被转换为 List,则该List 对集合元素的类型检查变成了泛型参数的上限(即 Object ) 。
下面程序示范了这种擦除:
ErasureTest.java
class Apple<T extends Number>
{
T size;
public Apple()
{
}
public Apple(T size)
{
this.size = size;
}
public void setSize(T size)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
}
public class ErasureTest
{
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6); // ①
// a的getSize方法 返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Apple变量,丢失尖括号里的类型信息
Apple b = a; // ②
// b只知道size的类型是Number
Number size1 = b.getSize();
// 下面代码 引起编译错误
// Integer size2 = b.getSize(); // ③
}
}
从逻辑上来看, List<String>是 List 的子类,如果直接把一个 List 对象赋给一个 List<String>对象应该引起编译错误 ,但实际上不会。对泛型而言,可以直接把一个 List 对象赋给一个 List对象 ,编译器仅仅提示 "未经检查的转换"。
看下面程序 :
ErasureTest2.java
public class ErasureTest2
{
public static void main(String[] args)
{
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
// 下面代码 引起“未经检查的转换”的警告,编译、运行时完全正常
List<String> ls = list; // ①
// 但只要访问ls里的元素,如下面代码 将引起运行时异常。
System .out.println(ls.get(0));
}
}
参考:
【1】:《疯狂Java讲义》
【2】:《Java核心技术 卷一》
【3】:廖雪峰的官方网站:泛型
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。