參考 & 推薦
-
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確實是很危險的, 因為編譯器只會給警告, 而不會阻止我們做一些潛在危險的事情
}