對(duì)于泛型的原理和基礎(chǔ),可以參考筆者的上一篇文章
java泛型,你想知道的一切
一個(gè)問(wèn)題代碼
觀察以下代碼 :
public static void main(String[] args) {
// 編譯報(bào)錯(cuò)
// required ArrayList<Integer>, found ArrayList<Number>
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<Number> list2 = list1;
// 可以正常通過(guò)編譯,正常使用
Integer[] arr1 = new Integer[]{1, 2};
Number[] arr2 = arr1;
}
上述代碼中,在調(diào)用print
函數(shù)時(shí),產(chǎn)生了編譯錯(cuò)誤 required ArrayList<Integer>, found ArrayList<Number>
,說(shuō)需要的是ArrayList<Integer>
類型,找到的卻是ArrayList<Number>
類型, 然后我們知道,Number
類是Integer
的父類,理論上向上轉(zhuǎn)型,是沒(méi)有問(wèn)題的!
而使用java數(shù)組類型,就可以向上轉(zhuǎn)型.這是為什么呢????
原因就在于, Java中泛型是不變的,而數(shù)組是協(xié)變的.
下面我們來(lái)看定義 :
不變,協(xié)變,逆變的定義
逆變與協(xié)變用來(lái)描述類型轉(zhuǎn)換(type transformation)后的繼承關(guān)系,其定義:如果A泽腮、B表示類型,f(?)表示類型轉(zhuǎn)換膛薛,≤表示繼承關(guān)系(比如米辐,A≤B表示A是由B派生出來(lái)的子類);
- f(?)是逆變(contravariant)的,當(dāng)A≤B時(shí)有f(B)≤f(A)成立催首;**
- f(?)是協(xié)變(covariant)的蚁趁,當(dāng)A≤B時(shí)有f(A)≤f(B)成立裙盾;**
- f(?)是不變(invariant)的,當(dāng)A≤B時(shí)上述兩個(gè)式子均不成立他嫡,即f(A)與f(B)相互之間沒(méi)有繼承關(guān)系**
由此,可以對(duì)上訴代碼進(jìn)行解釋.
數(shù)組是協(xié)變的,導(dǎo)致數(shù)組能夠繼承子元素的類型關(guān)系 : Number[] arr = new Integer[2];
-> OK
泛型是不變的,即使它的類型參數(shù)存在繼承關(guān)系,但是整個(gè)泛型之間沒(méi)有繼承關(guān)系 : ArrayList<Number> list = new ArrayList<Integer>();
-> Error
通配符
在java泛型中,引入了 ?(通配符)符號(hào)來(lái)支持協(xié)變和逆變.
通配符表示一種未知類型,并且對(duì)這種未知類型存在約束關(guān)系.
? extends T
(上邊界通配符upper bounded wildcard) 對(duì)應(yīng)協(xié)變關(guān)系,表示 ?
是繼承自 T
的任意子類型.也表示一種約束關(guān)系,只能提供數(shù)據(jù),不能接收數(shù)據(jù).
?
的默認(rèn)實(shí)現(xiàn)是 ? extends Object
, 表示 ?
是繼承自Object
的任意類型.
? super T
(下邊界通配符lower bounded wildcard) 對(duì)應(yīng)逆變關(guān)系,表示 ?
是 T
的任意父類型.也表示一種約束關(guān)系,只能接收數(shù)據(jù),不能提供你數(shù)據(jù).
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>();
// 協(xié)變, 可以正常轉(zhuǎn)化, 表示list2是繼承 Number的類型
ArrayList<? extends Number> list2 = list1;
// 無(wú)法正常添加
// ? extends Number 被限制為 是繼承 Number的任意類型,
// 可能是 Integer,也可能是Float,也可能是其他繼承自Number的類,
// 所以無(wú)法將一個(gè)確定的類型添加進(jìn)這個(gè)列表,除了 null之外
list2.add(new Integer(1));
// 可以添加
list2.add(null);
// 逆變
ArrayList<Number> list3 = new ArrayList<>();
ArrayList<? super Number> list4 = list3;
list4.add(new Integer(1));
}
? 與 T 的差別
-
?
表示一個(gè)未知類型,T
是表示一個(gè)確定的類型. 因此,無(wú)法使用?
像T
聲明變量和使用變量.如
// OK
static <T> void test1(List<T> list) {
T t = list.get(0);
t.toString();
}
// Error
static void test2(List<?> list){
? t = list.get(0);
t.toString();
}```java
-
?
主要針對(duì) 泛型類的限制, 無(wú)法像T
類型參數(shù)一樣單獨(dú)存在.如
// OK
static <T> void test1(T t) {
}
// Error
static void test2(? t){
}
-
?
表示? extends Object
, 因此它是屬于 in類型(下面會(huì)說(shuō)明),無(wú)法接收數(shù)據(jù), 而T
可以.
// OK
static <T> void test1(List<T> list, T t) {
list.add(t);
}
// Error
static void test2(List<?> list, Object t) {
list.add(t);
}
-
?
主要表示使用泛型,T
表示聲明泛型
泛型類無(wú)法使用?
來(lái)聲明,泛型表達(dá)式無(wú)法使用T
// Error
public class Holder<?> {
...
// OK
public class Holder<T> {
...
public static void main(String[] args) {
// OK
Holder<?> holder;
// Error
Holder<T> holder;
}
- 永遠(yuǎn)不要在方法返回中使用
?
,在方法中不會(huì)報(bào)錯(cuò),但是方法的接收者將無(wú)法正常使用返回值.因?yàn)樗祷亓艘粋€(gè)不確定的類型.
通配符的使用準(zhǔn)則
學(xué)習(xí)使用泛型編程時(shí)番官,更令人困惑的一個(gè)方面是確定何時(shí)使用上限有界通配符以及何時(shí)使用下限有界通配符.
官方文檔中提供了一些準(zhǔn)則.
"in"類型:
“in”類型變量向代碼提供數(shù)據(jù)。 如copy(src钢属,dest)
src參數(shù)提供要復(fù)制的數(shù)據(jù)徘熔,因此它是“in”類型變量的參數(shù)。
"out"類型:
“out”類型變量保存接收數(shù)據(jù)以供其他地方使用.如復(fù)制示例中淆党,copy(src酷师,dest)
,dest參數(shù)接收數(shù)據(jù)染乌,因此它是“out”參數(shù)山孔。
"in","out" 準(zhǔn)則
- "in" 類型使用 上邊界通配符
? extends
. - "out" 類型使用 下邊界通配符
? super
. - 如果即需要 提供數(shù)據(jù)(in), 又需要接收數(shù)據(jù)(out), 就不要使用通配符.
下面看java源碼中 Collections
類中的copy
方法來(lái)驗(yàn)證該原則.
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
// dest 接收數(shù)據(jù), src 提供數(shù)據(jù)
dest.set(i, src.get(i));
}
...
}
PECS(producer-extends,consumer-super)
這個(gè)是 Effective Java
中提出的一種概念.
如果類型變量是 生產(chǎn)者,則用 extends
,如果類型變量 是消費(fèi)者,則使用 super
. 這種方式也成為 Get and Put Principle
.
即get
屬于生產(chǎn)者,put
屬于消費(fèi)者. 這樣的概念比較難懂.
繼續(xù)使用上述 copy
方法的例子.
// dest 消費(fèi)了數(shù)據(jù)(set),則使用 super
// src 生產(chǎn)了數(shù)據(jù)(get), 則使用 extends
dest.set(i, src.get(i));
動(dòng)手編寫(xiě)通配符函數(shù)
接下來(lái)我們通過(guò)通配符的知識(shí),來(lái)模擬幾個(gè)在Python語(yǔ)言中很常用的函數(shù).
map()
函數(shù)
在python中,map
函數(shù)會(huì)根據(jù)提供的函數(shù)對(duì)指定序列做映射.
strArr = ["1", "2"]
intArr = map(lambda x: int(x) * 10, strArr)
print(strArr,list(intArr))
# ['1', '2'] [10, 20]
接下來(lái),我們使用java泛型知識(shí)來(lái),實(shí)現(xiàn)類似的功能, 方法接收一個(gè)類型的列表,可以將其轉(zhuǎn)化為另一種類型的列表.
public class Main {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
strList.add("1");
strList.add("2");
// jdk8 使用lambda表達(dá)式
List<Integer> intList = map(strList, s -> Integer.parseInt(s) * 10);
// strList["1","2"]
// intList[10,20]
}
/**
* 定義一個(gè)接口,它接收一個(gè)類型,返回另一個(gè)類型.
*
* @param <T> 一個(gè)類型的方法參數(shù)
* @param <R> 一個(gè)類型的返回
*/
interface Func_TR<T, R> {
// 接收一個(gè)類型,返回另一個(gè)類型.
R apply(T t);
}
/**
* 定義mapping函數(shù)
*
* @param src 提供數(shù)據(jù),因此這里使用(get) 上邊界通配符
* @param mapper mapping 函數(shù)的具體實(shí)現(xiàn)
* @param <? extends R> 提供數(shù)據(jù),這里是作為apply的返回值, 因此使用 上邊界通配符
* @param <? super T>接收數(shù)據(jù),這里作為 apply的傳入?yún)?shù)
* @return 返回值不要使用 通配符來(lái)定義
*/
public static <R, T> List<R> map(List<? extends T> src, Func_TR<? super T, ? extends R> mapper) {
if (src == null)
throw new IllegalArgumentException("List must not be not null");
if (mapper == null)
throw new IllegalArgumentException("map func must be not null");
// coll 既需要接收數(shù)據(jù)(add),又需要提供數(shù)據(jù)(return),所以不使用通配符
List<R> coll = new ArrayList<>();
for (T t : src) {
coll.add(mapper.apply(t));
}
return coll;
}
filter()
函數(shù)
Python中,filter() 函數(shù)用于過(guò)濾序列,過(guò)濾掉不符合條件的元素荷憋,返回由符合條件元素組成的新列表台颠。
intArr = [1, 2, 3, 4, 5]
newArr = filter(lambda x: x >= 3, intArr)
print(list(newArr))
# [1, 2, 3, 4, 5] [3, 4, 5]
接下來(lái),我們使用java泛型知識(shí)來(lái),實(shí)現(xiàn)類似的功能,方法接收一個(gè)列表,和過(guò)濾方法,返回過(guò)濾后的列表.
public class Main {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
intList.add(4);
intList.add(5);
List<Integer> filterList = filter(intList, i -> i >= 3);
// filterList[3,4,5]
}
/**
* 定義一個(gè)接口,它接收一個(gè)類型,返回布爾值
*
* @param <T> 一個(gè)類型的方法參數(shù)
*/
interface Func_Tb<T> {
boolean apply(T t);
}
/**
* filter 函數(shù)的實(shí)現(xiàn)
*
* @param src 傳入的列表只提供數(shù)據(jù),這里只調(diào)用了迭代操作, 因此使用 上邊界通配符
* @param func func需要接收一個(gè)數(shù)據(jù), 因此使用 下邊界通配符
* @return 返回值不要使用 通配符來(lái)定義,返回過(guò)濾后的列表
*/
public static <T> List<T> filter(List<? extends T> src, Func_Tb<? super T> func) {
if (src == null)
throw new IllegalArgumentException("List must not be not null");
if (func == null)
throw new IllegalArgumentException("filter func must be not null");
// coll 既需要接收數(shù)據(jù)(add),又需要提供數(shù)據(jù)(return),所以不使用通配符
List<T> coll = new ArrayList<>();
for (T t : src) {
if (func.apply(t))
coll.add(t);
}
return coll;
}
}
reduce()
函數(shù)
Python中,reduce() 函數(shù)會(huì)對(duì)參數(shù)序列中元素進(jìn)行累積。
函數(shù)將一個(gè)數(shù)據(jù)集合(鏈表台谊,元組等)中的所有數(shù)據(jù)進(jìn)行下列操作:用傳給 reduce 中的函數(shù) function(有兩個(gè)參數(shù))先對(duì)集合中的第 1蓉媳、2 個(gè)元素進(jìn)行操作譬挚,得到的結(jié)果再與第三個(gè)數(shù)據(jù)用 function 函數(shù)運(yùn)算,最后得到一個(gè)結(jié)果酪呻。
from functools import reduce
result = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
print(result)
# 15
同樣的, 我們利用java泛型知識(shí),來(lái)實(shí)現(xiàn)類似的功能
public class Main {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
intList.add(4);
intList.add(5);
int result = reduce(intList, (t1, t2) -> t1 + t2);
// result = 15
}
/**
* 定義一個(gè)接口,接收兩個(gè)同一個(gè)類型的參數(shù),返回值也屬于同一類型
*
* @param <T> 作為方法參數(shù),和返回值
*/
interface Func_TTT<T> {
T apply(T t1, T t2);
}
/**
* reduce函數(shù)的實(shí)現(xiàn)
*
* @param src 傳入的列表只提供數(shù)據(jù),這里只調(diào)用了迭代操作, 因此使用 上邊界通配符
* @param func T 作為 apply()函數(shù)的參數(shù)和返回值,即接收也提供數(shù)據(jù), 因此不能使用通配符
* @return 返回值不要使用 通配符來(lái)定義, 返回參數(shù)相互迭代的值
*/
public static <T> T reduce(List<? extends T> src, Func_TTT<T> func) {
if (src == null || src.size() == 0)
throw new IllegalArgumentException("List must not be not null or empty");
if (func == null)
throw new IllegalArgumentException("reduce func must be not null");
int size = src.size();
T result = src.get(0);
if (size == 1) return result;
// 將前兩項(xiàng)的值做apply操作后的返回值,再與下一個(gè)元素進(jìn)行操作
for (int i = 1; i < size; i++) {
T ele = src.get(i);
result = func.apply(result, ele);
}
return result;
}
}
通過(guò)這三個(gè)例子, 相信大家對(duì)java泛型以及通配符的使用,有了比較直觀的了解.