簡體   English   中英

Java繼承中的“this”關鍵字如何工作?

[英]How does the “this” keyword in Java inheritance work?

在下面的代碼片段中,結果確實令人困惑。

public class TestInheritance {
    public static void main(String[] args) {
        new Son();
        /*
        Father father = new Son();
        System.out.println(father); //[1]I know the result is "I'm Son" here
        */
    }
}

class Father {
    public String x = "Father";

    @Override
    public String toString() {
       return "I'm Father";
    }

    public Father() {
        System.out.println(this);//[2]It is called in Father constructor
        System.out.println(this.x);
    }
}

class Son extends Father {
    public String x = "Son";

    @Override
    public String toString() {
        return "I'm Son";
    }
}

結果是

I'm Son
Father

為什么“this”指向父構造函數中的Son,但“this.x”指向Father中的“x”字段。 “this”關鍵字如何運作?

我知道多態的概念,但[1]和[2]之間不會有什么不同嗎? 新Son()被觸發時,內存中發生了什么?

默認情況下,所有成員函數在Java中都是多態的。 這意味着當你調用this.toString()時,Java使用動態綁定來解析調用,調用子版本。 當您訪問成員x時,您訪問當前作用域(父親)的成員,因為成員不是多態的。

這里有兩件事情,讓我們來看看它們:

首先,您要創建兩個不同的字段。 看一下(非常孤立的)字節碼塊,你會看到:

class Father {
  public java.lang.String x;

  // Method descriptor #17 ()V
  // Stack: 2, Locals: 1
  public Father();
        ...
    10  getstatic java.lang.System.out : java.io.PrintStream [23]
    13  aload_0 [this]
    14  invokevirtual java.io.PrintStream.println(java.lang.Object) : void [29]
    17  getstatic java.lang.System.out : java.io.PrintStream [23]
    20  aload_0 [this]
    21  getfield Father.x : java.lang.String [21]
    24  invokevirtual java.io.PrintStream.println(java.lang.String) : void [35]
    27  return
}

class Son extends Father {

  // Field descriptor #6 Ljava/lang/String;
  public java.lang.String x;
}

重要的是第13,20和21行; 其他代表System.out.println(); 本身,或隱含的return; aload_0加載this參考, getfield從對象檢索一個字段值,在此情況下,從this 你在這里看到的是字段名稱是合格的: Father.x Son一行中,您可以看到有一個單獨的字段。 Son.x從未使用過; 只有Father.x是。

現在,如果我們刪除Son.x並添加此構造函數,該怎么Son.x

public Son() {
    x = "Son";
}

首先看一下字節碼:

class Son extends Father {
  // Field descriptor #6 Ljava/lang/String;
  public java.lang.String x;

  // Method descriptor #8 ()V
  // Stack: 2, Locals: 1
  Son();
     0  aload_0 [this]
     1  invokespecial Father() [10]
     4  aload_0 [this]
     5  ldc <String "Son"> [12]
     7  putfield Son.x : java.lang.String [13]
    10  return
}

第4,5和7行看起來很好:加載了this"Son" ,並且使用putfield設置了字段。 為什么選擇Son.x 因為JVM可以找到繼承的字段。 但重要的是要注意,即使該字段被引用為Son.x ,JVM找到的字段實際上Father.x

那么它能提供正確的輸出嗎? 很不幸的是,不行:

I'm Son
Father

原因是陳述的順序。 字節碼中的第0行和第1行是隱式的super(); 調用,所以語句的順序是這樣的:

System.out.println(this);
System.out.println(this.x);
x = "Son";

當然它會打印"Father" 要擺脫這種情況,可以做一些事情。

可能最干凈的是: 不要在構造函數中打印! 只要構造函數沒有完成,對象就不會完全初始化。 您正在假設,由於println是構造函數中的最后一個語句,因此您的對象已完成。 正如您所經歷的那樣,當您擁有子類時,情況並非如此,因為超類構造函數將始終在您的子類有機會初始化對象之前完成。

有些人認為這是構造者本身概念的缺陷; 有些語言在這個意義上甚至不使用構造函數。 您可以使用init()方法 在普通方法中,您具有多態性的優點,因此可以在Father引用上調用init() ,並調用Son.init() ; new Father()總是創建一個Father對象。 (當然,在Java中,你仍然需要在某個時候調用正確的構造函數)。

但我認為你需要的是這樣的:

class Father {
    public String x;

    public Father() {
        init();
        System.out.println(this);//[2]It is called in Father constructor
        System.out.println(this.x);
    }

    protected void init() {
        x = "Father";
    }

    @Override
    public String toString() {
        return "I'm Father";
    }
}

class Son extends Father {
    @Override
    protected void init() {
        //you could do super.init(); here in cases where it's possibly not redundant
        x = "Son";
    }

    @Override
    public String toString() {
        return "I'm Son";
    }
}

我沒有它的名字,但試試看。 它會打印出來

I'm Son
Son

那么這里發生了什么? 你最頂層的構造函數( Father )構造函數調用一個init()方法,該方法在子類中被重寫。 由於所有構造函數調用super(); 首先,它們有效地執行超類到子類。 因此,如果最頂層的構造函數的第一個調用是init(); 然后所有的init都在任何構造函數代碼之前發生。 如果init方法完全初始化對象,則所有構造函數都可以使用初始化對象。 由於init()是多態的,它甚至可以在存在子類時初始化對象,這與構造函數不同。

請注意, init()受到保護:子類將能夠調用並覆蓋它,但其他包中的類將無法調用它。 這比public略有改善,也應考慮用於x

如其他所述,您不能覆蓋字段,您只能隱藏它們。 JLS 8.3。 現場聲明

如果類聲明了具有特定名稱的字段,那么該字段的聲明將被稱為隱藏超類中具有相同名稱的字段的任何和所有可訪問聲明,以及該類的超接口。

在這方面,隱藏字段不同於隱藏方法(第8.4.8.3節 ),因為在字段隱藏中靜態和非靜態字段之間沒有區別,而在方法隱藏中區分靜態和非靜態方法。

如果是靜態的,則可以使用限定名稱(第6.5.6.2節 )訪問隱藏字段,或者使用包含關鍵字super(第15.11.2節 )或轉換為超類類型的字段訪問表達式來訪問隱藏字段。

在這方面,隱藏字段類似於隱藏方法。

類繼承自其直接超類和直接超接口超類和超接口的所有非私有字段,這些字段既可以訪問類中的代碼,也不會被類中的聲明隱藏。

你可以使用super關鍵字從Son的范圍訪問Father的隱藏字段,但是相反的情況是不可能的,因為Father類不知道它的子類。

雖然可以覆蓋方法,但可以隱藏屬性。

在您的情況下,屬性x是隱藏的:在您的Son類中,除非使用super關鍵字,否則無法訪問Fatherx值。 Father班不知道Sonx屬性。

在對立面中, toString()方法被覆蓋:將始終被調用的實現是實例化的類之一(除非它不覆蓋它),即在你的情況下Son ,無論變量的類型是什么( ObjectFather 。 ..)。

這是一種專門用於訪問私有成員的行為。 所以this.x查看為Father聲明的變量X,但是當你將它作為參數傳遞給父中的方法中的System.out.println時 - 它會根據參數的類型查看要調用的方法 -在你的情況下兒子。

那么你如何調用超類方法呢? 使用super.toString()

從父親來看,它無法訪問Son的x變量。

多態方法調用僅適用於實例方法。 您總是可以使用更通用的引用變量類型(超類或接口)引用對象,但在運行時,基於實際對象(而不是引用類型)動態選擇的唯一事物是實例方法NOT STATIC METHODS 。 不是變量 僅根據實際對象的類型動態調用重寫的實例方法。

因此變量x沒有多態行為,因為它不會在運行時動態選擇。

解釋你的代碼:

System.out.println(this);

Object類型是Son因此將調用toString()方法的重寫的Son版本。

System.out.println(this.x);

對象類型不在這里, this.xFather類中,因此將打印x變量的Father版本。

請參閱: java中的多態性

這通常被稱為陰影 注意你的類聲明:

class Father {
    public String x = "Father";

class Son extends Father {
    public String x = "Son";

當您創建Son的實例時,這將創建名為x 2個不同變量。 一個x屬於Father超類,第二個x屬於Son子類。 根據輸出,我們可以看到,在Father范圍內, this訪問Fatherx實例變量。 所以行為與“ this指向什么”無關; 它是運行時搜索實例變量的結果。 它只是上升的類層次結構來搜索變量。 一個類只能引用自身及其父類的變量; 它無法直接從其子類訪問變量,因為它對其子項沒有任何了解。

要獲得所需的多態行為,您應該只在Father聲明x

class Father {
    public String x;

    public Father() {
        this.x = "Father"
    }

class Son extends Father {
    public Son() {
        this.x = "Son"
    }

本文討論了您正在經歷的行為: http//www.xyzws.com/Javafaq/what-is-variable-hiding-and-shadowing/15

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM