我已經閱讀了一些關于Java 中的Covariance、Contravariance和Invariance的文章,但我對它們感到困惑。
我正在使用 Java 11,并且我有一個類層次結構A => B => C(意味著它C是Band的子型別A,并且B是 的子型別A)和一個類Container:
class Container<T> {
public final T t;
public Container(T t) {
this.t = t;
}
}
例如,如果我定義一個函式:
public Container<B> method(Container<B> param){
...
}
這是我的困惑,為什么第三行編譯?
method(new Container<>(new A())); // ERROR
method(new Container<>(new B())); // OK
method(new Container<>(new C())); // OK Why ?, I make a correction, this compiles OK
如果在 Java 中泛型是不變的。
當我定義這樣的東西時:
Container<B> conta = new Container<>(new A()); // ERROR, Its OK!
Container<B> contb = new Container<>(new B()); // OK, Its OK!
Container<B> contc = new Container<>(new C()); // Ok, why ? It's not valid, because they are invariant
uj5u.com熱心網友回復:
協方差是在出現超型別時傳遞或指定子型別的能力。如果您的 C 類擴展了 B,則 C 是 B 的子類。C 和 B 之間的這種關系也稱為is-a關系,其中 C 的實體也是 B 的實體。因此,當您的變數contc期望 B 實體并且您'正在傳遞new C(),因為new C()是 C 的實體和 C 的實體is (also)-anB 的實體,那么編譯器允許以下寫法:
Container<B> contc = new Container<>(new C());
相反,當你寫作時
Container<B> conta = new Container<>(new A());
您收到錯誤,因為 A 是 B 的超型別,沒有is-a從 A 到 B 的關系,而是從 B 到 A。這是因為 B 的每個實體也是 A 的實體,但不是 A 的每個實體是 B 的一個實體(舉一個愚蠢的例子,每個拇指都是手指,但不是每個手指都是拇指)。A是B的推廣;因此它不能出現在預期 B 實體的位置。
這里有一篇很好的文章擴展了java中協方差的概念。
https://www.baeldung.com/java-covariant-return-type
uj5u.com熱心網友回復:
該問題的示例并未證明泛型的不變性。
一個可以證明這一點的例子是:
ArrayList<Object> ao = new ArrayList<String>(); // does not compile
(您可能錯誤地期望上面的代碼能夠編譯,因為String它是 . 的子類Object。)
這個問題向我們展示了構造物件的不同方法Container<B>——其中一些可以編譯,而另一些則不能,因為 , 和 的繼承A層次B結構C。
該菱形運算子<>意味著創建的容器B在??每種情況下都是型別。
如果你看下面的例子:
Container<B> contc = new Container<>(new C()); // compiles
并通過用 填充菱形重新撰寫它C,您將看到以下內容無法編譯:
Container<B> contc = new Container<C>(new C()); // does not compile
這將給您與我的ArrayList示例相同的“不兼容型別”編譯錯誤。
uj5u.com熱心網友回復:
Java 7 引入的好處之一是所謂的菱形運算子 <>。
它已經存在了很長時間,以至于很容易忘記每次在實體化泛型類時使用 diamond 時,編譯器都應該從背景關系中推斷出泛型型別。
如果我們定義一個變數來保存對Person物件串列的參考,如下所示:
List<Person> people = new ArrayList<>(); // effectively - ArrayList<Person>()
編譯器將從左側變數的型別推斷ArrayList實體的型別。people
在Java 語言規范中,運算式new ArrayList<>()被描述為類實體創建運算式,并且因為它沒有指定泛型型別引數并且在背景關系中使用,所以它應該被歸類為poly 運算式。規范中的參考:
如果類實體創建運算式使用菱形形式作為類的型別引數,并且它出現在賦值背景關系或呼叫背景關系中(第 5.2 節、第 5.3 節),則它是一個多邊形運算式(第 15.2節)。
即當diamond <>與泛型類實體化一起使用時,實際型別將取決于它出現的背景關系。
下面的三個陳述句代表了所謂的賦值背景關系的情況。并且所有三個實體Container都將被推斷為 type B。
Container<B> conta = new Container<>(new A()); // 1 - ERROR because `B t = new A()` is incorrect
Container<B> contb = new Container<>(new B()); // 2 - fine because `B t = new B()` is correct
Container<B> contc = new Container<>(new C()); // 3 - fine because `B t = new C()` is also correct
由于容器的所有實體都是型別B,并且承包商期望的引數型別也將是B. 即可以提供一個實體B或其任何子型別。因此,在1我們遇到編譯錯誤的情況下,同時2and 3(B和 的子型別B)將正確編譯。
而且它不違反不變的行為。這樣想:我們可以存盤, , 等的List<Number>實體Integer,這不會導致任何問題,因為它們都可以表示它們的超型別。但是編譯器不允許將此串列分配給任何非型別串列,因為否則無法確保此分配是安全的。這就是不變性的含義——我們只能分配給一個型別的變數(但我們可以自由地在其中存盤任何子型別,這是安全的)。ByteDoubleNumberList<Number>List<Number>List<Number>Number
例如,讓我們考慮Container類中有一個 setter 方法:
public class Container<T> {
public T t;
public Container(T t) {
this.t = t;
}
public void setT(T t) {
this.t = t;
}
}
現在讓我們使用它:
Container<B> contb = new Container<>(null); // to avoid any confusion initialy `t` will be assigned to `null`
contb.setT(new A()); // compilation error - because expected type is `B` or it's subtype
contb.setT(new B()); // fine
contb.setT(new C()); // fine because C is a subtype of B
當我們使用 diamond 處理類實體創建運算式<>時,該運算式作為引數傳遞給方法,型別將從呼叫背景關系推斷為上述規范中的參考狀態。
因為method()期望Container<B>,所以上面的所有實體都將被推斷為 type B。
method(new Container<>(new A())); // Error
method(new Container<>(new B())); // OK - because `B t = new B()` is correct
method(new Container<>(new C())); // OK - because `B t = new C()` is also correct
筆記
值得一提的是,在 Java 8 之前(即 Java 7,因為我們使用的是 diamond)new Container<>(new C()),編譯器會將運算式解釋為獨立的運算式(即背景關系將被忽略),創建Container<C>. 這意味著您最初的猜測有些正確:對于Java 7,以下陳述句將無法編譯。
Container<B> contc = new Container<>(new C()); // Container<B> = Container<C> - is an illegal assignment
但是 Java 8 引入了一個稱為目標型別和多運算式(即出現在背景關系中的運算式)的特性,以確保型別推斷機制始終考慮背景關系。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/477886.html
