關(guān)于Java的private,你可能不知道的事

最近學(xué)習(xí)JVM,學(xué)習(xí)過程中,對(duì)某些基礎(chǔ)的知識(shí)又有了新的認(rèn)識(shí),故得此博文。

private

盡管private看起來已經(jīng)那么熟悉,實(shí)際上還是有很多地方?jīng)]有遇到過也沒有思考過,這里先引用Thinking in Java中對(duì)它的描述:

The private keyword means that no one can access that member except the class that contains that member, inside methods of that class. Other classes in the same package cannot access private members, so it’s as if you’re even insulating the class against yourself.

關(guān)鍵有兩點(diǎn):

  • 只能在包含該成員的類中去訪問它,例如在包含該成員的類的方法
  • 其余類中一律不能訪問,不管以何種方式

按理來講,這兩條算是說的清清楚楚,但是由于對(duì)某些概念迷迷糊糊,導(dǎo)致在實(shí)際過程中,還是可能犯糊涂。

示例1

設(shè)有一下代碼:

// Person.java
public class Person {
  private int pri;
}
// Man.java
public class Man extends Person {
  public static void main(String[] args) {
    Person person = new Person();
    // 以下代碼會(huì)出現(xiàn)語法錯(cuò)誤
    person.pri = 5;
  }
}

這個(gè)例子可能大部分人都能看出錯(cuò)誤的原因了,大部分人也幾乎不會(huì)犯這樣的錯(cuò)誤,不過這里引出了一些需要注意的東西,所以還是提一下。錯(cuò)誤的原因,是對(duì)The private keyword means that no one can access that member except the class that contains that member有誤解,認(rèn)為,person是擁有pri的,應(yīng)當(dāng)是可以訪問的。當(dāng)然若仔細(xì)看前面提到的那兩條,就會(huì)發(fā)現(xiàn),實(shí)際上這個(gè)錯(cuò)誤非常明顯。首先,第一點(diǎn)后面還有一句inside methods of that class,這句話表明,你應(yīng)該在包含該變量的類的方法中去訪問,此處是在Man中的方法,而不是Person的方法,故有誤。第二條更加直接,不能再其它的類訪問,故這兩條均說明了上面那樣訪問是有問題的。出現(xiàn)這樣的錯(cuò)誤,應(yīng)該是對(duì)含有該變量的類和含有該變量的對(duì)象產(chǎn)生了混淆,前面說的是:private修飾的變量,應(yīng)該在包含該變量的類里面去訪問;而對(duì)象,僅僅是類的實(shí)例(實(shí)際上,在運(yùn)行時(shí),對(duì)象中的數(shù)據(jù)并不包含方法數(shù)據(jù),方法的數(shù)據(jù)都是保存在類中,也就是方法區(qū)中),不存在”包含該變量的對(duì)象里面去訪問“的說法。

這里有幾點(diǎn)需要提一下,首先是,擁有該變量的的類(或者更準(zhǔn)確的,聲明該變量的類),這里,擁有該pri的類就是Person,然后是,訪問代碼所在的類,例如前面的person.pri所在的類,就是Man;最后是訪問該變量的對(duì)象所對(duì)應(yīng)的類,還是以person.pri為例,訪問pri的變量的對(duì)象person所對(duì)應(yīng)的類就是Person。

實(shí)際上,想要訪問private修飾的變量,必須要前面所提到的三個(gè)類都是相同的

示例2

// Person.java
public class Person {
  private int pri;
  public static void main(String[] args) {
    Person person = new Person();
    
    person.pri = 5; // 如前面所介紹的,此處訪問是沒有問題的
    Man man = new Man();
    
    man.pri = 10;  // 根據(jù)前面介紹的,man所對(duì)應(yīng)的類是Man而不是Person,因此是不能這樣訪問的
    ((Person) man).pri = 2; // ?
  } 
}
// Man.java
public class Man extends Person {
}

代碼的?所標(biāo)記的地方,其實(shí)是可以這樣訪問的,由于強(qiáng)制類型轉(zhuǎn)化,將man對(duì)應(yīng)的類變成了Person,所以符合前面的規(guī)則。

動(dòng)態(tài)類型和靜態(tài)類型

對(duì)JVM有一定了解的人(或者熟悉多態(tài)的人)都知道所謂的對(duì)象的動(dòng)態(tài)類型和靜態(tài)類型。靜態(tài)類型,就是指編譯時(shí)期確定的類型,動(dòng)態(tài)類型就是指在運(yùn)行時(shí)期,對(duì)象實(shí)際的類型。最典型的就是多態(tài):

// Person.java
public class Person {
  public void fun() {
    System.out.println("fun in Person");
  }
}

// Man.java
public class Man extends Person {
  public void fun() {
    System.out.println("fun in Man");
  }
  public static void main(String[] args) {
    Person man = new Man();
  }
}

以上代碼是很常見的,尤其對(duì)于面向接口編程的時(shí)候,尤為廣泛。man在編譯時(shí)候能確定其類型為Person(嚴(yán)格來講,應(yīng)該是引用類型,該引用指向Person類,此處這么講只是為了方便理解),而實(shí)際上在運(yùn)行時(shí)期,它是一個(gè)Man類型(同樣,嚴(yán)格來講是個(gè)引用類型,這里這么說是為了方便理解)。我們還可以通過強(qiáng)制類型轉(zhuǎn)換來轉(zhuǎn)換靜態(tài)類型:man = (Man)man。動(dòng)態(tài)類型并不會(huì)因?yàn)閺?qiáng)轉(zhuǎn)而改變,例如,我們將Man的main函數(shù)改為下面的代碼:

public static void main(String[] args) {
  Man man = new Man();
  Person person = (Person) man;
  man.fun();
  person.fun();
}

運(yùn)行代碼你會(huì)發(fā)現(xiàn)輸出為:

fun in Man
fun in Man

這說明,man的動(dòng)態(tài)類型沒有改變,更嚴(yán)格的來講,man和person均是一個(gè)引用類型(類符號(hào)引用),man應(yīng)當(dāng)指向一個(gè)Man類實(shí)例(就是前面講的,man的靜態(tài)類型為Man),person應(yīng)當(dāng)指向一個(gè)Person類的實(shí)例(就是指person的靜態(tài)類型為Person),而實(shí)際上,man和person是指向了同一個(gè)實(shí)例,即Man的實(shí)例。因此,強(qiáng)制類型轉(zhuǎn)換,只是改變了變量應(yīng)當(dāng)指向某種類型的實(shí)例,但不能改變變量所指向類實(shí)例的真實(shí)類型(用前面的話來講,強(qiáng)制類型轉(zhuǎn)換,只是改變了靜態(tài)類型,而無法改變動(dòng)態(tài)類型)。

再看示例2

// Person.java
public class Person {
  private int pri;
  public static void main(String[] args) {
    Person person = new Person();
    
    person.pri = 5; // 如前面所介紹的,此處訪問是沒有問題的
    Man man = new Man();
    
    man.pri = 10;  // 根據(jù)前面介紹的,man所對(duì)應(yīng)的類是Man而不是Person,因此是不能這樣訪問的
    ((Person) man).pri = 2; // ?
  } 
}
// Man.java
public class Man extends Person {
}

我們已經(jīng)知道(Pewrson)man表明man應(yīng)當(dāng)指向一個(gè)Person類的實(shí)例。然而實(shí)際上,man還是指向了Man的實(shí)例,那么問題來了,既然還是Man的實(shí)例,為什么能夠訪問父類Person的private 變量呢?private變量不是不會(huì)被繼承嗎?雖然前面已經(jīng)通過規(guī)則說明這樣訪問是沒有問題的,但是,這里引出了一些問題:創(chuàng)建子類對(duì)象,調(diào)用父類的默認(rèn)構(gòu)造器究竟做了什么?父類的private變量真的不能繼承嗎?

首先回答第一個(gè)問題,調(diào)用父類的默認(rèn)構(gòu)造器究竟做了什么?

很明顯,并沒有創(chuàng)建對(duì)象,創(chuàng)建普通對(duì)象(特指非數(shù)組對(duì)象)需要虛擬機(jī)的new指令(注意和Java的new關(guān)鍵字區(qū)分開),創(chuàng)建子類對(duì)象時(shí),僅僅會(huì)調(diào)用父類的默認(rèn)構(gòu)造器。

那么,什么是父類的默認(rèn)構(gòu)造器呢?

默認(rèn)構(gòu)造器就是無參構(gòu)造器,若一個(gè)類沒有提供任何構(gòu)造器,則編譯器會(huì)生成一個(gè)無參構(gòu)造器,若提供了一個(gè)構(gòu)造器,則編譯器便不會(huì)生成無參構(gòu)造器,除非自己寫了一個(gè)無參構(gòu)造器,否則該類就沒有默認(rèn)構(gòu)造器,對(duì)于沒有默認(rèn)構(gòu)造器的類,其子類必須顯示調(diào)用父類構(gòu)造器;對(duì)有默認(rèn)構(gòu)造器的類,子類的構(gòu)造器可以不顯示調(diào)用父類的構(gòu)造器,但是編譯器實(shí)際上會(huì)在構(gòu)造器前面加上super();以保證調(diào)用了父類的構(gòu)造器。

因此,嚴(yán)格來講,創(chuàng)建子類對(duì)象,其實(shí)不一定是調(diào)用父類的默認(rèn)構(gòu)造器(畢竟父類可能沒有默認(rèn)構(gòu)造器),但一定調(diào)用了父類的某個(gè)構(gòu)造器,而父類的構(gòu)造器是用來進(jìn)行初始化父類的變量(非靜態(tài)變量)的,但是調(diào)用父類構(gòu)造器,并沒有創(chuàng)建對(duì)象,那么那些非靜態(tài)變量放在哪里呢?

只可能是子類實(shí)例里面了,那么,父類的構(gòu)造器若對(duì)private變量進(jìn)行初始化,表明這個(gè)private修飾的變量也應(yīng)該存在!那么實(shí)際上,在子類實(shí)例里面,還是有父類的private修飾的變量的。所以,實(shí)際上,private變量也被“繼承”了。但是基于前面所講的規(guī)則,是無法在子類的方法里面訪問父類的private成員變量的,于是問題2也答案也同時(shí)明確了。看起來,private成員變量也是被“繼承”了,只不過這沒什么用,不能讀不能寫,放在那里還占內(nèi)存。不過至少有一個(gè)好處,那就是Java虛擬機(jī)實(shí)現(xiàn)上變簡(jiǎn)單了,不用考慮父類成員變量是否是private的,一股腦的全部放過來就是了,只需要在訪問時(shí),進(jìn)行權(quán)限檢查就行了。而權(quán)限檢查是很常見的,各種調(diào)用方法和訪問變量的時(shí)候都需要進(jìn)行檢查,因此,并沒有為把父類的private變量“繼承”之后進(jìn)行單獨(dú)的檢查,所以實(shí)現(xiàn)上確實(shí)要比不“繼承”來的方便,更加關(guān)鍵的,對(duì)于例子2,我們通過((Person) man).pri就成功訪問了父類的private變量,若沒有把這個(gè)變量繼承,反而會(huì)造成其他的問題。

小結(jié)

  • 一個(gè)類若沒有顯示提供構(gòu)造器,則編譯器會(huì)提供一個(gè)無參的構(gòu)造器
  • 一個(gè)類若提供了構(gòu)造器,則編譯器不會(huì)提供構(gòu)造器
  • 無參構(gòu)造器就是默認(rèn)構(gòu)造器,一個(gè)類有構(gòu)造器,要么該類沒有顯示提供任何構(gòu)造器,要么顯示提供了無參構(gòu)造器
  • 父類的成員變量(包括private修飾的)會(huì)在子類的實(shí)例中。
  • 子類的成員變量若和父類的成員變量重名也沒有什么關(guān)系,雖然都放在子類實(shí)例中,但是放在了不同的地方

對(duì)于最后一點(diǎn)可以看看如下代碼:

public class Parent {
  int a;
}
public class Child extends Parent {
  int a;
  
  public void fun() {
    a = 10;
    super.a = 20;
    System.out.println("a in child: " + a);
    System.out.println("a in parent: " + super.a);
  }
  public static void main(String[] args) {
    Child child = new Child();
    child.fun();
  }
}

運(yùn)行代碼便知。同樣的,父類方法和子類方法重名時(shí)也和這種情況類似。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評(píng)論 18 399
  • 20- 枚舉,枚舉原始值,枚舉相關(guān)值,switch提取枚舉關(guān)聯(lián)值 Swift枚舉: Swift中的枚舉比OC中的枚...
    iOS_恒仔閱讀 2,313評(píng)論 1 6
  • 一:java概述:1,JDK:Java Development Kit,java的開發(fā)和運(yùn)行環(huán)境,java的開發(fā)工...
    ZaneInTheSun閱讀 2,687評(píng)論 0 11
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,923評(píng)論 18 139
  • 總得來說 MVP 模式可分為五大模塊: 我們先以實(shí)現(xiàn)一個(gè)登錄界面的實(shí)例,來大致看一下MVP模式的幾大模塊 bean...
    好爛的筆頭閱讀 302評(píng)論 0 0