[英]Why do this() and super() have to be the first statement in a constructor?
Java 要求如果在構造函數中調用this()
或super()
,它必須是第一條語句。 為什么?
例如:
public class MyClass {
public MyClass(int x) {}
}
public class MySubClass extends MyClass {
public MySubClass(int a, int b) {
int c = a + b;
super(c); // COMPILE ERROR
}
}
Sun 編譯器說, call to super must be first statement in constructor
。 Eclipse 編譯器說, Constructor call must be the first statement in a constructor
。
但是,您可以通過稍微重新安排代碼來解決此問題:
public class MySubClass extends MyClass {
public MySubClass(int a, int b) {
super(a + b); // OK
}
}
這是另一個例子:
public class MyClass {
public MyClass(List list) {}
}
public class MySubClassA extends MyClass {
public MySubClassA(Object item) {
// Create a list that contains the item, and pass the list to super
List list = new ArrayList();
list.add(item);
super(list); // COMPILE ERROR
}
}
public class MySubClassB extends MyClass {
public MySubClassB(Object item) {
// Create a list that contains the item, and pass the list to super
super(Arrays.asList(new Object[] { item })); // OK
}
}
因此,它不會阻止您在調用super()
之前執行邏輯。 它只是阻止您執行無法放入單個表達式的邏輯。
調用this()
也有類似的規則。 編譯器說, call to this must be first statement in constructor
。
為什么編譯器有這些限制? 你能給出一個代碼示例,如果編譯器沒有這個限制,會發生一些不好的事情嗎?
父類的構造函數需要在子類的構造函數之前調用。 這將確保如果您在構造函數中調用父類的任何方法,則父類已經正確設置。
您正在嘗試做的事情,將 args 傳遞給 super 構造函數是完全合法的,您只需要在執行時內聯構造這些 args,或者將它們傳遞給您的構造函數,然后將它們傳遞給super
:
public MySubClassB extends MyClass {
public MySubClassB(Object[] myArray) {
super(myArray);
}
}
如果編譯器沒有強制執行此操作,您可以這樣做:
public MySubClassB extends MyClass {
public MySubClassB(Object[] myArray) {
someMethodOnSuper(); //ERROR super not yet constructed
super(myArray);
}
}
在父類具有默認構造函數的情況下,編譯器會自動為您插入對 super 的調用。 由於 Java 中的每個類都繼承自Object
,因此必須以某種方式調用對象構造函數,並且必須首先執行它。 編譯器自動插入 super() 允許這樣做。 強制 super 首先出現,強制構造函數主體以正確的順序執行,即: Object -> Parent -> Child -> ChildOfChild -> SoOnSoForth
我通過鏈接構造函數和靜態方法找到了解決這個問題的方法。 我想做的看起來像這樣:
public class Foo extends Baz {
private final Bar myBar;
public Foo(String arg1, String arg2) {
// ...
// ... Some other stuff needed to construct a 'Bar'...
// ...
final Bar b = new Bar(arg1, arg2);
super(b.baz()):
myBar = b;
}
}
所以基本上基於構造函數參數構造一個對象,將對象存儲在一個成員中,並將對該對象的方法的結果傳遞給 super 的構造函數。 使成員成為最終成員也相當重要,因為類的性質是它是不可變的。 請注意,實際上,構造 Bar 實際上需要一些中間對象,因此在我的實際用例中它不能簡化為單行。
我最終使它像這樣工作:
public class Foo extends Baz {
private final Bar myBar;
private static Bar makeBar(String arg1, String arg2) {
// My more complicated setup routine to actually make 'Bar' goes here...
return new Bar(arg1, arg2);
}
public Foo(String arg1, String arg2) {
this(makeBar(arg1, arg2));
}
private Foo(Bar bar) {
super(bar.baz());
myBar = bar;
}
}
合法的代碼,它完成了在調用超級構造函數之前執行多條語句的任務。
因為 JLS 是這么說的。 是否可以以兼容的方式更改 JLS 以允許它? 是的。
但是,這會使語言規范復雜化,而這已經足夠復雜了。 這不是一件非常有用的事情,並且有一些方法可以解決它(使用靜態方法或 lambda 表達式this(fn())
的結果調用另一個構造函數 - 該方法在另一個構造函數之前調用,因此也超級構造函數)。 所以做改變的功率重量比是不利的。
請注意,僅此規則不會阻止在超類完成構造之前使用字段。
考慮這些非法的例子。
super(this.x = 5);
super(this.fn());
super(fn());
super(x);
super(this instanceof SubClass);
// this.getClass() would be /really/ useful sometimes.
這個例子是合法的,但是是“錯誤的”。
class MyBase {
MyBase() {
fn();
}
abstract void fn();
}
class MyDerived extends MyBase {
void fn() {
// ???
}
}
在上面的示例中,如果MyDerived.fn
需要來自MyDerived
構造函數的參數,則需要使用ThreadLocal
來處理它們。 ;(
順便說一下,從 Java 1.4 開始,包含外部this
的合成字段是在調用內部類超級構造函數之前分配的。 這會在針對早期版本編譯的代碼中導致特殊的NullPointerException
事件。
另請注意,在存在不安全發布的情況下,除非采取預防措施,否則可以通過其他線程重新排序查看構造。
2018 年 3 月編輯:在消息記錄中:構造和驗證Oracle 建議刪除此限制(但與 C# 不同,在構造函數鏈接之前, this
肯定是未分配的(DU))。
從歷史上看,this() 或 super() 必須在構造函數中排在第一位。 這種限制從未流行過,並且被認為是任意的。 有許多微妙的原因,包括對 invokespecial 的驗證,導致了這種限制。 多年來,我們已經在 VM 級別解決了這些問題,以至於考慮解除這個限制變得切實可行,不僅是為了記錄,而是為了所有構造函數。
僅僅因為這是繼承哲學。 根據 Java 語言規范,構造函數的主體是這樣定義的:
ConstructorBody: { ExplicitConstructorInvocation opt BlockStatements opt }
構造函數主體的第一條語句可以是
如果構造函數體不是以顯式構造函數調用開始,並且被聲明的構造函數不是原始類 Object 的一部分,則構造函數體隱式以超類構造函數調用“super();”開始,調用它的直接超類不帶參數。 依此類推.. 將有一個完整的構造函數鏈一直調用到 Object 的構造函數; “Java 平台中的所有類都是對象的后代”。 這個東西被稱為“構造函數鏈接”。
為什么會這樣?
而Java之所以以這種方式定義ConstructorBody,是因為它們需要維護對象的層次結構。 記住繼承的定義; 它正在擴展一個類。 話雖如此,你不能擴展不存在的東西。 需要先創建基類(超類),然后才能派生它(子類)。 這就是為什么他們稱它們為 Parent 和 Child 類; 沒有父母就不能生孩子。
在技術層面上,子類從其父類繼承所有成員(字段、方法、嵌套類)。 而且由於構造函數不是成員(它們不屬於對象。它們負責創建對象),因此它們不會被子類繼承,但可以調用它們。 而且由於在創建對象時只執行了一個構造函數。 那么我們如何在創建子類對象的時候保證超類的創建呢? 因此,“構造函數鏈接”的概念; 所以我們有能力從當前構造函數中調用其他構造函數(即super)。 Java 要求此調用是子類構造函數中的第一行,以維護和保證層次結構。 他們假設如果您不首先顯式創建父對象(就像您忘記了它一樣),他們會為您隱式創建。
此檢查在編譯期間完成。 但是我不確定運行時會發生什么,我們會得到什么樣的運行時錯誤,如果 Java 在我們顯式嘗試從其中間的子類的構造函數中執行基本構造函數時沒有拋出編譯錯誤身體而不是從第一行開始......
我相當確定(那些熟悉 Java 規范的人)這是為了防止您(a)被允許使用部分構造的對象,以及(b)強制父類的構造函數在“新鮮“ 目的。
“壞”事情的一些例子是:
class Thing
{
final int x;
Thing(int x) { this.x = x; }
}
class Bad1 extends Thing
{
final int z;
Bad1(int x, int y)
{
this.z = this.x + this.y; // WHOOPS! x hasn't been set yet
super(x);
}
}
class Bad2 extends Thing
{
final int y;
Bad2(int x, int y)
{
this.x = 33;
this.y = y;
super(x); // WHOOPS! x is supposed to be final
}
}
您問為什么,而其他答案,imo,並沒有真正說明為什么可以調用您的超級構造函數,但前提是它是第一行。 原因是您並沒有真正調用構造函數。 在 C++ 中,等價的語法是
MySubClass: MyClass {
public:
MySubClass(int a, int b): MyClass(a+b)
{
}
};
當您像這樣在左大括號之前看到初始化器子句時,您就知道它很特別。 它在任何其他構造函數運行之前運行,實際上在任何成員變量被初始化之前運行。 Java 並沒有什么不同。 有一種方法可以讓一些代碼(其他構造函數)在構造函數真正開始之前運行,在子類的任何成員被初始化之前。 這種方式就是將“呼叫”(例如super
)放在第一行。 (在某種程度上, super
或this
有點在第一個大括號之前,即使您在之后鍵入它,因為它將在您到達所有內容完全構造之前執行。)大括號之后的任何其他代碼(如int c = a + b;
)使編譯器說“哦,好的,沒有其他構造函數,我們可以初始化所有內容。” 所以它運行並初始化你的超類和你的成員等等,然后在左大括號之后開始執行代碼。
如果在幾行之后,它遇到一些代碼說“哦,是的,當你構造這個對象時,這是我希望你傳遞給基類的構造函數的參數”,那就太晚了,它不會有任何意義。 所以你得到一個編譯器錯誤。
因此,它不會阻止您在調用 super 之前執行邏輯。 它只是阻止您執行無法放入單個表達式的邏輯。
實際上,您可以使用多個表達式執行邏輯,您只需將代碼包裝在靜態函數中並在 super 語句中調用它。
使用您的示例:
public class MySubClassC extends MyClass {
public MySubClassC(Object item) {
// Create a list that contains the item, and pass the list to super
super(createList(item)); // OK
}
private static List createList(item) {
List list = new ArrayList();
list.add(item);
return list;
}
}
我完全同意,限制太強了。 使用靜態輔助方法(如 Tom Hawtin - tackline 建議的那樣)或將所有“pre-super() 計算”推入參數中的單個表達式並不總是可行的,例如:
class Sup {
public Sup(final int x_) {
//cheap constructor
}
public Sup(final Sup sup_) {
//expensive copy constructor
}
}
class Sub extends Sup {
private int x;
public Sub(final Sub aSub) {
/* for aSub with aSub.x == 0,
* the expensive copy constructor is unnecessary:
*/
/* if (aSub.x == 0) {
* super(0);
* } else {
* super(aSub);
* }
* above gives error since if-construct before super() is not allowed.
*/
/* super((aSub.x == 0) ? 0 : aSub);
* above gives error since the ?-operator's type is Object
*/
super(aSub); // much slower :(
// further initialization of aSub
}
}
正如 Carson Myers 所建議的,使用“對象尚未構造”異常會有所幫助,但在每個對象構造期間檢查此異常會減慢執行速度。 我更喜歡 Java 編譯器,它可以做出更好的區分(而不是間接禁止 if 語句但允許參數中的 ? 運算符),即使這會使語言規范復雜化。
我的猜測是,他們這樣做是為了讓編寫處理 Java 代碼的工具的人的生活更輕松,在某種程度上也讓閱讀 Java 代碼的人更輕松。
如果您允許super()
或this()
調用移動,則需要檢查更多變體。 例如,如果您將super()
或this()
調用移動到條件if()
中,它可能必須足夠聰明才能將隱式super()
插入到else
中。 如果你調用super()
兩次,或者同時使用super()
和this()
,它可能需要知道如何報告錯誤。 它可能需要在調用super()
或this()
之前禁止接收器上的方法調用,並確定何時變得復雜。
讓每個人都做這些額外的工作似乎是成本大於收益。
我找到了一個解決方法。
這不會編譯:
public class MySubClass extends MyClass {
public MySubClass(int a, int b) {
int c = a + b;
super(c); // COMPILE ERROR
doSomething(c);
doSomething2(a);
doSomething3(b);
}
}
這有效:
public class MySubClass extends MyClass {
public MySubClass(int a, int b) {
this(a + b);
doSomething2(a);
doSomething3(b);
}
private MySubClass(int c) {
super(c);
doSomething(c);
}
}
你能給出一個代碼示例,如果編譯器沒有這個限制,會發生一些不好的事情嗎?
class Good {
int essential1;
int essential2;
Good(int n) {
if (n > 100)
throw new IllegalArgumentException("n is too large!");
essential1 = 1 / n;
essential2 = n + 2;
}
}
class Bad extends Good {
Bad(int n) {
try {
super(n);
} catch (Exception e) {
// Exception is ignored
}
}
public static void main(String[] args) {
Bad b = new Bad(0);
// b = new Bad(101);
System.out.println(b.essential1 + b.essential2);
}
}
構造期間的異常幾乎總是表明正在構造的對象無法正確初始化,現在處於不良狀態,無法使用,並且必須進行垃圾回收。 但是,子類的構造函數有能力忽略其超類之一中發生的異常並返回部分初始化的對象。 在上面的例子中,如果給new Bad()
的參數是 0 或大於 100,那么essential1
和essential2
都沒有被正確初始化。
你可能會說忽略異常總是一個壞主意。 好的,這是另一個例子:
class Bad extends Good {
Bad(int n) {
for (int i = 0; i < n; i++)
super(i);
}
}
有趣,不是嗎? 在這個例子中我們創建了多少個對象? 一? 二? 或者也許什么都沒有……
允許在構造函數的中間調用super()
或this()
將打開一個由令人發指的構造函數組成的潘多拉魔盒。
另一方面,我理解在調用super()
或this()
之前經常需要包含一些靜態部分。 這可能是任何不依賴this
引用的代碼(實際上,它已經存在於構造函數的開頭,但在super()
或this()
返回之前不能有序使用)並且需要進行此類調用。 此外,就像在任何方法中一樣,在調用super()
或this()
之前創建的一些局部變量可能會在它之后需要。
在這種情況下,您有以下機會:
super()
和 pre this()
代碼。 這可以通過對super()
或this()
可能出現在構造函數中的位置施加限制來完成。 實際上,即使是今天的編譯器也能夠區分好的和壞的(或潛在的壞)情況,其程度足以安全地允許在構造函數的開頭添加靜態代碼。 事實上,假設super()
和this()
返回this
引用,反過來,你的構造函數有return this;
在最后。 以及編譯器拒絕代碼
public int get() {
int x;
for (int i = 0; i < 10; i++)
x = i;
return x;
}
public int get(int y) {
int x;
if (y > 0)
x = y;
return x;
}
public int get(boolean b) {
int x;
try {
x = 1;
} catch (Exception e) {
}
return x;
}
如果出現錯誤“變量 x 可能尚未初始化”,它可以對this
變量執行此操作,就像對任何其他局部變量一樣對其進行檢查。 唯一的區別是this
不能通過super()
或this()
調用以外的任何方式分配(並且,像往常一樣,如果在構造函數中沒有這樣的調用,編譯器會在開頭隱式插入super()
)和可能不會被分配兩次。 如果有任何疑問(例如在第一個get()
中,實際上總是分配x
),編譯器可能會返回錯誤。 這比簡單地在除了super()
或this()
之前的注釋之外的任何構造函數上返回錯誤要好。
您可以在調用其構造函數之前使用匿名初始化程序塊來初始化子項中的字段。 這個例子將演示:
public class Test {
public static void main(String[] args) {
new Child();
}
}
class Parent {
public Parent() {
System.out.println("In parent");
}
}
class Child extends Parent {
{
System.out.println("In initializer");
}
public Child() {
super();
System.out.println("In child");
}
}
這將輸出:
在父母
在初始化器中
在兒童
構造函數按派生順序完成執行是有道理的。 因為超類不知道任何子類,所以它需要執行的任何初始化都與子類執行的任何初始化是分開的,並且可能是其先決條件。 因此,它必須首先完成其執行。
一個簡單的演示:
class A {
A() {
System.out.println("Inside A's constructor.");
}
}
class B extends A {
B() {
System.out.println("Inside B's constructor.");
}
}
class C extends B {
C() {
System.out.println("Inside C's constructor.");
}
}
class CallingCons {
public static void main(String args[]) {
C c = new C();
}
}
該程序的輸出是:
Inside A's constructor
Inside B's constructor
Inside C's constructor
我知道我參加聚會有點晚了,但我已經使用過幾次這個技巧(而且我知道這有點不尋常):
我用一種方法創建了一個通用接口InfoRunnable<T>
:
public T run(Object... args);
如果我需要在將它傳遞給構造函數之前做一些事情,我就這樣做:
super(new InfoRunnable<ThingToPass>() {
public ThingToPass run(Object... args) {
/* do your things here */
}
}.run(/* args here */));
實際上, super()
是構造函數的第一條語句,因為要確保在構造子類之前它的超類是完全形成的。 即使您的第一條語句中沒有super()
,編譯器也會為您添加它!
那是因為您的構造函數依賴於其他構造函數。 要使您的構造函數正常工作,其他構造函數必須正常工作,這是依賴的。 這就是為什么有必要首先檢查在構造函數中由 this() 或 super() 調用的依賴構造函數。 如果 this() 或 super() 調用的其他構造函數有問題,那么點執行其他語句,因為如果調用的構造函數失敗,所有語句都會失敗。
Java為什么這樣做的問題已經得到解答,但是由於我偶然發現了這個問題,希望找到一個更好的替代單線的方法,因此我將分享我的解決方法:
public class SomethingComplicated extends SomethingComplicatedParent {
private interface Lambda<T> {
public T run();
}
public SomethingComplicated(Settings settings) {
super(((Lambda<Settings>) () -> {
// My modification code,
settings.setting1 = settings.setting2;
return settings;
}).run());
}
}
調用靜態函數應該執行得更好,但如果我堅持將代碼“放在”構造函數中,或者如果我必須更改多個參數並發現定義許多靜態方法不利於可讀性,我會使用它。
其他答案已經解決了問題的“為什么”。 我將提供一個解決此限制的技巧:
基本思想是用嵌入的語句劫持super
語句。 這可以通過將您的語句偽裝成表達式來完成。
考慮我們想在調用super()
Statement9()
) 執行Statement1()
) :
public class Child extends Parent {
public Child(T1 _1, T2 _2, T3 _3) {
Statement_1();
Statement_2();
Statement_3(); // and etc...
Statement_9();
super(_1, _2, _3); // compiler rejects because this is not the first line
}
}
編譯器當然會拒絕我們的代碼。 因此,我們可以這樣做:
// This compiles fine:
public class Child extends Parent {
public Child(T1 _1, T2 _2, T3 _3) {
super(F(_1), _2, _3);
}
public static T1 F(T1 _1) {
Statement_1();
Statement_2();
Statement_3(); // and etc...
Statement_9();
return _1;
}
}
唯一的限制是父類必須有一個構造函數,它至少接受一個參數,以便我們可以將語句作為表達式潛入。
這是一個更詳細的示例:
public class Child extends Parent {
public Child(int i, String s, T1 t1) {
i = i * 10 - 123;
if (s.length() > i) {
s = "This is substr s: " + s.substring(0, 5);
} else {
s = "Asdfg";
}
t1.Set(i);
T2 t2 = t1.Get();
t2.F();
Object obj = Static_Class.A_Static_Method(i, s, t1);
super(obj, i, "some argument", s, t1, t2); // compiler rejects because this is not the first line
}
}
改造成:
// This compiles fine:
public class Child extends Parent {
public Child(int i, String s, T1 t1) {
super(Arg1(i, s, t1), Arg2(i), "some argument", Arg4(i, s), t1, Arg6(i, t1));
}
private static Object Arg1(int i, String s, T1 t1) {
i = Arg2(i);
s = Arg4(s);
return Static_Class.A_Static_Method(i, s, t1);
}
private static int Arg2(int i) {
i = i * 10 - 123;
return i;
}
private static String Arg4(int i, String s) {
i = Arg2(i);
if (s.length() > i) {
s = "This is sub s: " + s.substring(0, 5);
} else {
s = "Asdfg";
}
return s;
}
private static T2 Arg6(int i, T1 t1) {
i = Arg2(i);
t1.Set(i);
T2 t2 = t1.Get();
t2.F();
return t2;
}
}
事實上,編譯器可以為我們自動化這個過程。 他們只是選擇不這樣做。
在構造子對象之前,必須創建父對象。 如您所知,當您編寫這樣的課程時:
public MyClass {
public MyClass(String someArg) {
System.out.println(someArg);
}
}
它轉向下一個(擴展和超級只是隱藏):
public MyClass extends Object{
public MyClass(String someArg) {
super();
System.out.println(someArg);
}
}
首先我們創建一個Object
,然后將此對象擴展到MyClass
。 我們不能在Object
之前創建MyClass
。 簡單的規則是必須在子構造函數之前調用父構造函數。 但是我們知道類可以有多個構造函數。 Java 允許我們選擇一個將被調用的構造函數(它將是super()
或super(yourArgs...)
)。 因此,當您編寫super(yourArgs...)
時,您重新定義了構造函數,該構造函數將被調用以創建父對象。 您不能在super()
之前執行其他方法,因為該對象尚不存在(但在super()
之后將創建一個對象,您將能夠做任何您想做的事情)。
那么為什么我們不能在任何方法之后執行this()
呢? 如您所知this()
是當前類的構造函數。 我們也可以在我們的類中有不同數量的構造函數,並像this()
或this(yourArgs...)
一樣調用它們。 正如我所說,每個構造函數都有隱藏方法super()
。 當我們編寫自定義super(yourArgs...)
時,我們使用super(yourArgs...)
yourArgs...) 刪除super()
)。 此外,當我們定義this()
或this(yourArgs...)
時,我們還會在當前構造函數中刪除我們的super()
,因為如果super()
與this()
在同一方法中,它將創建多個父對象。 這就是為什么對this()
方法施加相同規則的原因。 它只是將父對象的創建重新傳輸到另一個子構造函數,並且該構造函數調用super()
構造函數來創建父對象。 所以,代碼實際上是這樣的:
public MyClass extends Object{
public MyClass(int a) {
super();
System.out.println(a);
}
public MyClass(int a, int b) {
this(a);
System.out.println(b);
}
}
正如其他人所說,您可以執行如下代碼:
this(a+b);
你也可以像這樣執行代碼:
public MyClass(int a, SomeObject someObject) {
this(someObject.add(a+5));
}
但是你不能執行這樣的代碼,因為你的方法還不存在:
public MyClass extends Object{
public MyClass(int a) {
}
public MyClass(int a, int b) {
this(add(a, b));
}
public int add(int a, int b){
return a+b;
}
}
此外,您必須在this()
方法鏈中擁有super()
構造函數。 您不能像這樣創建對象:
public MyClass{
public MyClass(int a) {
this(a, 5);
}
public MyClass(int a, int b) {
this(a);
}
}
class C
{
int y,z;
C()
{
y=10;
}
C(int x)
{
C();
z=x+y;
System.out.println(z);
}
}
class A
{
public static void main(String a[])
{
new C(10);
}
}
看例子如果我們調用構造函數C(int x)
那么 z 的值取決於 y 如果我們不在第一行調用C()
那么這將是 z 的問題。 z 將無法獲得正確的值。
在子類構造函數中添加 super() 的主要目的是編譯器的主要工作是將所有類與 Object 類直接或間接連接,這就是編譯器檢查我們是否提供了 super (參數化)然后編譯器不承擔任何責任。 以便所有實例成員都從 Object 初始化到子類。
這是官方重播:從歷史上看,this() 或 super() 必須在構造函數中排在第一位。 這個
限制從未流行,並被認為是任意的。 有一個
一些微妙的原因,包括invokespecial的驗證,
這促成了這種限制。 多年來,我們解決了
這些在 VM 級別,到了實際可行的程度
考慮取消這個限制,不僅是為了記錄,而是為了所有人
構造函數。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.