参考 & 推荐
-
Effective Java(2nd Edition)
December, 2017 马上就要出版第三版了, 这本书真的非常经典, 强烈推荐! - Time To Really Learn Generics: A Java 8 Perspective
- 张拭心 - 深入理解 Java 泛型
推荐阅读:
如何理解List<? extends Number>
根据定义, List<? extends Number> list
指的是list引用可以指向声明为List<继承于Number>的实例(不一定要是直接父类, 祖先有Number即可).
比如说以下都是合法的,
List<? extends Number> listOfNumbers = new ArrayList<Number>();
List<? extends Number> listOfIntegers = new ArrayList<Integer>();
List<? extends Number> listOfDoubles = new ArrayList<Double>();
但是我们却不能向上面任何一个容器加入数据.
List<? extends Number> listOfIntegers = new ArrayList<Integer>();
listOfIntegers.add(100); // 错误, 不允许添加
用呆杰的话来理解就是,
现在给了你一个List<? extends Number>
的引用listOfNumber
, 他可能是任何继承于Number的List<>. 如果允许往其中加入数据的话很显然是不安全的, 比如说调用list.add(1.4)
, 但是list
实际上指向的是List<Integer>
类型, 这样很显然是不允许的.
那List<? extends Number>
用处是什么? 一个常见的用例就是作为函数参数类型, 因为虽然我们不能对List<? extends Number>
的引用进行写操作, 但是我们可以读内容. 因为能传进来的List的泛型类型都是继承于Number类的, 所以总是能将其元素安全地转换为Number类. 比如下面这个例子:
private static double sumList(List<? extends Number> list) {
return list.stream()
.mapToDouble(Number::doubleValue) // returns DoubleStream
.sum();
}
public static void main(String[] args) {
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
List<BigDecimal> bigDecimals = Arrays.asList(
new BigDecimal("1.0"),
new BigDecimal("2.0"),
new BigDecimal("3.0"),
new BigDecimal("4.0"),
new BigDecimal("5.0")
);
System.out.printf("ints sum is %s%n", sumList(ints));
System.out.printf("doubles sum is %s%n", sumList(doubles));
System.out.printf("bigdecimals sum is %s%n", sumList(bigDecimals));
}
可能我们会有疑问, 为什么不直接把参数类型定义为List<Number>
呢?为什么非得加上? extends
看起来如此复杂的声明? 答案是, List<Number>
和 List<Integer>
其实没有任何关系, 并没有List<Integer>
是List<Number>
的子类的意思. 其实, 因为泛型擦除的原因, 这两个类最终都是List
. 从下面的例子可以看出List<People>
和List<Man>
并没有什么关系. 所以Java中的泛型是不协变
的, 即A是B的父类, 但是List<A>和List<B>并没有关系.
更多协变内容: Treant - Java中的逆变与协变
下面代码表明了Java的泛型不是协变的.
class People{
}
class Man extends People{
}
class Boy extends Man{
}
public void test(){
List<People> peopleList = new ArrayList<People>();
List<Man> manList = new ArrayList<Man>();
peopleList = (List<People>) manList; // 错误, 不能转换类型
}
原文中给出的例子:
List<String> strings = new ArrayList<>();
String s = "abc";
Object o = s; // allowed
// strings.add(o); // not allowed
// List<Object> moreObjects = strings; // also not allowed, but pretend it was
// moreObjects.add(new Date());
// String s = moreObjects.get(0); // uh oh
// 感觉按照下面的解释, 这里应该是 String s = strings.get(0);
Since String is a subclass of Object, you can assign a String reference to an Object reference. You can’t however, add an Object reference to a List<String>, which feels strange. The problem is that List<String> is NOT a subclass of List<Object>. When declaring a type, the only instances you can add to it are of the declared type. That’s it. No sub- or superclass instances allowed. We say that the parameterized type is invariant.
The commented out section shows why List<String> is not a subclass of List<Object>. Say you could assign a list of strings to a reference to a list of objects. Then, using the list of objects reference, you could add something that wasn’t a string to the list, which would cause a cast exception when you tried to retrieve it using the original reference to the list of strings. The compiler wouldn’t know any better.
这段话主要说明了List<TypeA>
和List<TypeB>
是没有什么关系的. 如果List<String>
能转型为List<Object>
那么我们就可以往List<String>
里面加入其他类型的对象, 这显然是不正确的, 所以泛型类并不是协变的.
如何理解 <? super>
List<? super Number> list
表明list
引用可以指向元素类型为Number
或者Number
的超类的List
, 比如说List<Number>
和List<Object>
.
实例:
public void numsUpTo(Integer num, List<? super Integer> output) {
IntStream.rangeClosed(1, num)
.forEach(output::add);
}
ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number> numberList = new ArrayList<>();
ArrayList<Object> objectList = new ArrayList<>();
numsUpTo(5, integerList);
numsUpTo(5, numberList);
numsUpTo(5, objectList);
因为<? super Integer>
所以, 往容器加Integer
是绝对安全的, 因为实际的List
要么是Integer
要么是Integer
的父类, 所以Integer
引用一定能转型为Integer
或者Integer
的父类引用.
实例2(Collections
类的max方法):
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) {
if (comp==null)
return (T)max((Collection) coll);
Iterator<? extends T> i = coll.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (comp.compare(next, candidate) > 0)
candidate = next;
}
return candidate;
}
注意Comparator<? super T> comp
部分, 在Comparator
的泛型参数中使用了super
, 表明可以使用T
的父类的比较方法.
注意对比以下几个方法声明:
public static <T> T max(Collection<? extends T> collection, Comparator<T> comparator){
return null;
}
public static <T> T max2(Collection<T> collection, Comparator<T> comparator){
return null;
}
然后有Father和Son类:
static class Father{
}
static class Son extends Father{
}
测试:
public static void main(String[] args) {
List<Son> sons = new ArrayList<Son>();
Collections.max(sons, new Comparator<Father>(){
@Override
public int compare(Father o1, Father o2) {
return 0;
}
});
max(sons, new Comparator<Father>(){
@Override
public int compare(Father o1, Father o2) {
return 0;
}
});
// 不能这样调用max2
// max2(sons, new Comparator<Father>(){
// @Override
// public int compare(Father o1, Father o2) {
// return 0;
// }
// });
}
其中max2的调用是错误的, 因为max2的参数表明该容器存放的类型必须实现了跟自己比较的Comparator
.
但是为什么max(Collection<? extends T> collection, Comparator<T> comparator)
的Comparator
的参数没用super
但是例子中调用却是合法的呢? 这是因为对于调用max(sons, new Comparator<Father>(){...})
, Java推断了类型参数<T>
为Father
, 而Collection<? extends Father>
表明是可以传入存放Son
类型的容器的.
如何合理使用通配符
PECS - Producer - Extends, Consumer - Super, 这个词来来源Effective Java一书.
- Producer
这里生产者的意思是, 你要从某个参数中获取某个类型的数据, 那么声明这个参数类型为<? extends T>. 比如说List<? extends Number> list, 表明list是一个生产者, 你可以从list中取出Number
对象. - Consumer
这里消费者的意思是, 这个参数将消费(使用)到某个类型的数据, 那么应该将参数声明为<? super T>. 比如说Collection<? super E> coll
, 表明coll
可以消费E
类型的数据. - 即要消费又要生产
那么就不使用通配符.
java 官方文档也有关于使用通配符的建议.
下面是一些例子, 多数来自Effective Java (2nd Edition).
实例
static <E> E reduce(List<E> list, Function<E> f, E iniVal); // #1
list仅仅用于produce类型为E的数据, 所以符合producer的角色, 所以应该将其声明为List<? extends E>
. 而Function<E> f既要消费E又会产生E, 所以直接使用具体类型. 修改后的声明如下:
static <E> E reduce(List<? extends E> list, Function<E> f, E iniVal); // #2
那么上面两者有什么区别呢? 对于#1
, 当Function<E> f
是Function<Number>
的时候, 对于List<Integer> listOfIntegers
来说, 是传不进去的, 只能是List<Number>
. 但是#2
, 因为list
为List<? extends E>
, 所以此处可以传入listOfIntegers
.
- 类型推导
public static <E> Set<E> union(Set<E> s1, Set<E> s2);
s1和s2都是producer, 所以修改为以下声明
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);
注意返回值仍然是Set<E>, 而不是Set<? extedns E>. 如果改成后者, 那么用户代码也必须使用通配符, 这是一个不好的决定.
类型推导的规则十分复杂, 在[JLS, 15.12.2.7-8]中有整整16页描述. 虽然大多数情况下, 用户无需指定类型参数, 但是对于有些情况, 则必须由用户指定边界的类型到底是什么.
Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = union(integers, doubles);
感觉上Java应该推断E为Number, 但是在不指定具体类型参数的时候, 却会报错.
注意: 在我个人实验这段代码的时候, 编译器已经正确推导出了类型.(Java 1.8), 因为Effective Java (2nd Edition)
出版于2008年, 所以应该是老版本编译器的问题.
显式指定类型参数:
注意以下几点:
- class后面的类型参数是无法被静态方法使用的, 静态方法必须自己重新定义类型参数
这个和class的类型参数不能用于静态方法一样, 因为静态属性和方法都是整个类共有的, 如果有其他地方传入了两种不同的类型, 那么静态属性或者方法不可能同时拥有两种类型, 所以这是不被允许的. 可见我的另一篇文章Java 泛型使用限制. - 显式指定类型参数的静态泛型方法的调用格式:
ClassName.<Type...>methodName();
<>
后和方法名之间不用再加.
了
public class TypeInference {
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2){
Set<E> result = new HashSet<>();
result.addAll(s1);
result.addAll(s2);
return result;
}
public static void main(String[] args) {
Set<Integer> integerSet = new HashSet<>();
Set<Double> doubleSet = new HashSet<>();
Set<Number> numberSet = union(integerSet, doubleSet);
// 显式指定
numberSet = TypeInference.<Number>union(integerSet, doubleSet);
}
}
- max方法的声明
public static <T extends Comparable<T>> T max(List<T> list); // #1
修改过后
public static <T extends Comparable<? super T>> T max(List<? extends T> list); // #2
那么#2
的优点在哪呢? 首先对于Comparable<? super T>
表明, 可以用其父类的比较函数来比较子类. 对于list
参数, 是对PECS的应用, 但是我个人认为list
在这个语境下即使被定义为List<T> list
也能有同样的效果.
- List<?> 和 List<E>
public static <E> void swap(List<E> list, int i, int j); // #1
public static void swap(List<?> list, int i, int j); // #2
对于#1
, list能够get
也能add
, 对于#2
只能取出Object
, 而且只有null
能作为add
的参数.
基于#2
的交换代码:
public static void swap(List<?> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}
我们会发现, 编译器不会通过这段代码, 看起来很违背直觉. 从同一个列表拿出的元素竟然不能放回去. 这是因为<?>
, 编译知道list
中的元素是某个具体类型, 但是因为是?
, 所以并不知道具体是哪个类型. 所以从list
的get
方法中拿出的数据, 只能是Object
引用, 这样才安全. 对于set()
方法, 除了null
, 编译器不会允许我们放入任何其他东西, 因为编译器无法判断我们要添加的东西到底是不是?
的那个类型, 所以就会阻止我们这么做.
而使用#1
的声明, 这一操作就可以执行了, 因为编译器知道list
中的元素可以安全的转换为E
, 而add
方法由于现在有了E
, 也知道, 可以安全的放入E
对象, 所以swap
就可以正常工作.
书上提到<?>
会比<E>
看起来是更好的API
声明, 像swap
函数, 对外的声明仍然是#2
形式, 然后内部实现采用#1
的私有函数来做. 这里也涉及到一个概念叫capture
, 有些编译错误中会有capture
, 实际是指的是编译器为不确定的?
类型定义了一个名字而已. 详细可见capture.
<?> 与 Raw Type
stackoverflow上有一篇讨论raw type的提问, 里面讲到了Raw Type和<?>的区别.
what-is-a-raw-type-and-why-shouldnt-we-use-it
使用了<?>的话, compiler会进行类型检查, 所以不能够通过一个List<?>的引用, 往List实例中添加任何元素(null
除外, 因为null
可以赋值给任何引用对象), 因为根本无法确定List<?>到底指向了什么类型的List, 所以无法保证类型安全, 所以不能通过这种引用添加元素.
static void appendNewObject(List<?> list) {
list.add(new Object()); // compilation error!
}
但是, 如果参数是List这种Raw Type, 那么添加任何元素都是可以的:
List list = new ArrayList<Integer>();
list.add(0);
list.add("what");
上面这段代码是可以运行的, 但是compiler会给出警告.
在引入泛型以后, 使用Raw Type是不被推荐的, 使用Raw Type只是为了兼容性问题!
例外情况, 因为Java泛型擦除的关系, List<String>.class是错误的, 因为Java泛型没有生成新的class, 所以当需要引用List这个class的时候, 必须使用List.class
, 同理使用instanceof操作符的时候, 也只能用o instanceof Set
而不能够o instanceof Set<String>
.
List, List<?>, List<Object> 区别
public static void testFunction(List<Integer> integerList){
// do nothing
}
public static void main(String[] args) {
List rawList = new ArrayList();
List<?> wildcardList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
// 编译器不会阻止我们添加任何对象, 但是会给出一个警告
rawList.add("we can add anything to rawList");
// 编译器直接拒绝这个操作
// wildcardList.add("we cannot add anything, because the compiler don't know what the exact type for ? is");
// 可以添加任何对象
objectList.add("we can add anything too, because everything is derived from Object");
// 同样是warning, 但是允许传入, 这很不安全, 但是raw type就是可以这样做
// 所以raw type可作为任何List<AnyType>的参数
testFunction(rawList);
// 编译器不允许
// testFunction(wildcardList);
// 也是不允许的
// testFunction(objectList);
// 所以使用RawType确实是很危险的, 因为编译器只会给警告, 而不会阻止我们做一些潜在危险的事情
}