文章目录
- 泛型的传参
- 若函数里的参数使用基类接受所有的派生类,怎么做?
- 类型通配符的上限
- 类型通配符的下限
泛型的传参
注意
若类 Base 是类 Derived 的基类(父类),那么数组类型 Base[] 是 Derived[] 的基类(父类)。但是集合类型 List<Base> 不是 List<Derived> 的基类(父类)。
若函数里的参数使用基类接受所有的派生类,怎么做?
例如:函数里的参数要接受所有的List类,包括 List<Integer>
, List<String>
等。
public class Test01 {
public static void main(String[] args) {
ArrayList<String> strList = new ArrayList<String>();
strList.add("good");
strList.add("man");
strList.add("helo");
testGenericParameter(strList);
}
// 形参类型 可以使用 List (会有警告),List<?> (推荐,没有警告), 不可以使用 List<Object>
private static void testGenericParameter(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
例如:Shape 是基类,Circle、Rectangle 是派生类
只想要接受所有Shape派生类的入参
// ? extends Shape 表示 所有从Shape派生出的类
void testGenericParameter(List<? extends Shape> list) {
}
? extends Shape
表示 所有 Shape 的派生类? super Shape
表示所有 Shape 的基类(父类)
类型通配符的上限
List<? extends Shape> 是受限制通配符的例子,此处的问号(? )代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound)。
类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的
与使用普通通配符相似的是,shapes.add()的第二个参数类型是 ? extends Shape,它表示Shape未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。
简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。
对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。比如Foo是Bar的子类,这样A<Foo>
就相当于A<? extends Bar>
的子类,可以将A<Foo>
赋值给A<? extends Bar>
类型的变量,这种型变方式被称为协变。
对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。 口诀是:协变只出不进!
提示:没有指定通配符上限的泛型类,相当于通配符上限是Object 。例如 List<?> 表示 类型上限是Object
类型通配符的下限
除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super 类型> 的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。
指定通配符的下限就是为了支持类型型变。比如Foo是Bar的子类,当程序需要一个A<? super Foo>
变量时,程序可以将A<Bar> 、A<Object>
赋值给A<? super Foo>
类型的变量,这种型变方式被称为逆变。
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。
对于逆变的泛型而言,它只能调用泛型类型作为参数的方法;而不能调用泛型类型作为返回值类型的方法。 口诀是:逆变只进不出!
设自己实现一个工具方法:实现将src集合中的元素复制到dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。
对于上面的copy()方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或者是前者的父类即可,此时通配符的下限就有了用武之地。
下面程序采用通配符下限的方式来实现该copy()方法。
public class TestGenericType {
public static void main(String[] args) {
ArrayList<Number> list1 = new ArrayList<Number>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(1);
list2.add(2);
list2.add(3);
Integer last = copy(list1, list2);//①
System.out.println(list1);
}
// 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;
}
}
使用这种语句,就可以保证程序的①处调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。
实际上,Java集合框架中的TreeSet< E> 有一个构造器也用到了这种设定通配符下限的语法,如下所示。
public TreeSet(Comparator<? super E> comparator) {
//...
}
正如前一章所介绍的,TreeSet会对集合中的元素按自然顺序或定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数comparator就是进行定制排序的Comparator对象。
Comparator接口也是一个带泛型声明的接口
public interface Comparator<T> {
int compare(T o1, T o2);
}
通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。
假定需要创建一个TreeSet<String>
集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator<String>
,也可以是Comparator<Object>
只要尖括号里传入的类型是String的父类型(或它本身)即可。
private static void test02() {
// 既可以使用 new Comparator<Object> 因为 Object 是 String 的父类,也可以使用 Comparator<String> 作为构造器的参数
TreeSet<String> set1 = new TreeSet<String>(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return o1.hashCode() > o2.hashCode() ? 1 :
o1.hashCode() < o2.hashCode() ? -1 : 0;
}
});
set1.add("hello");
set1.add("wa");
TreeSet<String> set2 = new TreeSet<String>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() > o2.length() ? -1 :
o1.length() < o2.length() ? 1 : 0;
}
});
set2.add("hello");
set2.add("wa");
System.out.println(set1);
System.out.println(set2);
}
通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的活性。当然,不仅TreeSet有这种用法,TreeMap也有类似的用法,具体的请查阅Java的API文档。