Java泛型
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态的指定(即传入实际的类型参数,也可称为类型实参)。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
1、定义泛型接口、类
public class Apple<T> { private T info; public Apple() { } public Apple(T info) { this.info = info; } public void setInfo() { 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 Apple<Double> a2 = new Apple<>(5.67); System.out.println(a2.getInfo()); } }
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>!调用该构造器时却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。
2、并不存在泛型类
//分别创建List<String>对象和List<Integer>对象 List<String> l1 = new ArrayList<>(); List<Integer> l2 = new ArrayList<>(); //调用getClass()方法来比较l1和l2的类是否相等 System.out.println(l1.getClass() == l2.getClass());
output:
true
不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。
不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。例如,下面代码是错误的:
Collection<String> cs = new ArrayList<>(); //下面代码编译时引起错误:instanceof运算符后面不能不能使用泛型类 if (cs instanceof ArrayList<String>) { }
eclipse提示:
Cannot perform instanceof check against parameterized type ArrayList<String>. Use the form ArrayList<?> instead since further generic type information will be erased at runtime 无法对参数化类型ArrayList <String>执行instanceof检查。 使用ArrayList <?>形式,因为进一步的通用类型信息将在运行时被擦除
3、类型通配符
如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型!
Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型;但G<Foo>不是G<Bar>的子类型。
在一种极端的情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。如:
//表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口 public class Apple<T extends Number & java.io.Serializable> { ... }
与类同时继承父类、实现接口类似的是,为类型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位。
4、泛型方法
import java.util.ArrayList; import java.util.Collection; public class GenericMethodTest { 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类型形参 static <T> void fromArrayToCollection(T[] a, Collection<T> c) { for (T o : a) { c.add(o); } } }
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符--因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参---也就是泛型方法。
如果有需要,可以同时使用泛型方法和通配符,如Java的Collection.copy()方法。
public class Collections { public static <T> void copy(List<T> dest, List<? extends T> src) { ... } }
copy方法中的dest和src存在明显的依赖关系,从源List中复制出来的元素,必须可以“丢进”目标List中,所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为,该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,无须使用泛型方法。
也可以将上面的方法签名改为使用泛型方法,不使用类型通配符,如:
class Collections { public static <T, S extends T> void copy(List<T> dest, List<S> src) {...} }
这个方法签名可以代替前面的方法签名。上面的类型形参S,仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于它,那类型形参S就没有存在的必要,即可以使用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明类型形参)更加清晰和准确,因此Java设计该方法时采用了通配符,而不是泛型方法。
类型通配符与泛型方法( 在方法签名中显示声明类型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明。
Java允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”类型形参的类型,而且程序员也可以显示地为构造器中的类型形参指定实际的类型。如:
public class GenericConstructor { public static void main(String[] args) { // 泛型构造器中的T参数为String new Foo("Java learning"); // 泛型构造器中的T参数为Integer new Foo(200); // 显式指定泛型构造器中的T参数为String // 传给Foo构造器的实参也是String对象,完全正确 new <String>Foo("Go Programming"); } } class Foo { public <T> Foo(T t) { System.out.println(t); } }
5、设定通配符下限
若一个工具方法:实现将src集合里的元素复制到dest集合里的功能,因为dest集合可以保存src集合里的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。为了表示两个参数之间的类型依赖,考虑同时使用通配符、泛型参数来实现该方法。代码如下:
public static <T> void copy()(Collection<T> dest, Collection<? extends T> src) { for(T ele : src) { dest.add(ele); } }
假设该方法需要一个返回值,返回最后一个被复制的元素,如:
public static <T> T copy()(Collection<T> dest, Collection<? extends T> src) { T last = null; for(T ele : src) { last = ele; dest.add(ele); } }
表面上看起来,上面的方法实现了这个功能,实际上有一个问题:当遍历src集合的元素时,src元素的类型是不确定的(只可以肯定它是T的子类),程序只能用T来笼统的表示各种src集合的元素类型。如:
List<Number> ln = new ArrayList<>(); List<Integer> li = new ArrayList<>(); //下面代码引起编译错误 Integer last = copy(ln, li);
ln的类型是List<Number>,可得到T的实际类型是Number,而不是Integer,即copy()方法的返回值也是Number类型,而不是Integer类型,但实际上最后一个复制元素的元素类型一定是Integer。也就是说,程序在复制集合元素的过程中,丢失了src集合元素的类型。
对于上面的copy方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或是前者的父类即可。为了表达这种约束关系,Java允许设定通配符的下限:<? super Type>,这个通配符表示它必须是Type本身,或是Type的父类。如:
import java.util.ArrayList; import java.util.Collection; import java.util.List; public class MyUtils { public static void main(String[] args) { List<Number> ln = new ArrayList<>(); List<Integer> li = new ArrayList<>(); li.add(5); Integer last = copy(ln, li); System.out.println(last); } //下面dest集合元素的类型必须与src集合元素的类型相同,或是其父类 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; } }
Java集合框架中的TreeSet<E>有一个构造器用到了设定通配符下限的语法:
TreeSet(Comparator<? super E> c)
Compartor接口是一个带泛型声明的接口:
public interface Comparator<T> { int compare(T fst, T snd); }
import java.util.Comparator; import java.util.TreeSet; public class TreeSetTest { public static void main(String[] args) { //Comparator的实际类型是TreeSet的元素类型的父类,满足要求 TreeSet<String> ts1 = new TreeSet<>( new Comparator<Object>() { 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>() { public int compare(String first, String second) { return first.length() > second.length() ? -1 : first.length() < second.length() ? 1 : 0; } }); ts2.add("hello"); ts2.add("wa"); System.out.println(ts1); System.out.println(ts2); } }
6、泛型与数组
Java泛型有一个很重要的设计原则--如果一段代码在编译时没有提出“【unchecked】未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型形参的数组。也就是说,只能声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的数组对象。
List<String>[] lsa = new ArrayList<String>[10]; //这是不允许的 //Java允许创建无上限的通配符泛型数组,因此下面这行代码是正确的 List<?>[] lsa = new ArrayList<?>[10]; Object[] oa = (Object[])lsa; List<Integer> li2 = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; Object target = lsa[1].get(0); if(target instanceof String) { //下面代码安全了 String s = (String )target; }