Java內部類新解,你沒有見過的船新版本

2020-12-06 探險家之指路明燈

基礎

Java支持類中嵌套類,稱之為nested class。嵌套的層數沒有限制,但實際中一般最多用兩層。根據內部類是否有static修飾,分為 static nested classnon-static nested class。non-static nested class又被稱為 inner class。inner class裡面又有兩個特殊一點的類: local classanonymous class。特殊之處主要在於語法使用上,實質功能是差不多的。 官方 是這樣解釋的:

Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.用圖表示分類及關係如下:

上面是按照官方的說法來分的,實際中很多人習慣把所有的嵌套類都稱為inner class(內部類),這只是個稱謂,不影響溝通就好。後文用nested class來統稱static nested class和non-static nested class。各個類的主要特性和限制已經在圖中說明了(適用於JDK 8及以後)。

那為什麼需要內部類呢?內部類是在Java 1.1中引入的,當時很多人質疑該設計增加了Java的複雜性,但實用性不強,當然這種問題仁者見仁智者見智。官方的解釋是這樣的:

Why Use Nested Classes?

Compellingreasons for using nested classes include the following:

It is a way of logically grouping classes that are only used in one place : If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such "helper classes" makes their package more streamlined.**It increases encapsulation : Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.It can lead to more readable and maintainable code : Nesting small classes within top-level classes places the code closer to where it is used.總的來說,嵌套類最大的目的是改善代碼的組織,並不是必不可少的功能。嵌套類能實現的功能,通過正常的類都可以實現,只不過可能要多寫點代碼,且不是很優雅而已。

為什麼inner class可以訪問外層類的非靜態成員

要說明的是: 嵌套類是對於編譯器而言的,對虛擬機來說沒有什麼嵌套類,只有正常的類 。也就是說嵌套類經過編譯器編譯之後要轉化成一個個正常的類,比如A類裡面嵌套了B類。那經過編譯器之後會變成兩個獨立的類: A 和 A$B 。這樣問題就很簡單了,B類要訪問A類的非靜態成員,要滿足兩個條件:

要有A類的實例。且要有訪問成員的權限或者方式。而編譯器在編譯期間就幫我們幹了這兩件事,下面驗證一下。

定義一個嵌套類:

public class OuterClass { /** * 定義一個公有成員變量 */ public Object publicVariable = "public member variable"; private Object privateVariable = "private member variable"; /** * 定義兩個私有成員變量 */ private Object privateVariable2 = "private member variable2"; /** * 定義一個私有成員方法 * @param parameter */ private void privateMethod(String parameter) { System.out.println(parameter); } /** * 調用inner class */ public void show() { InnerClass innerClass = new InnerClass(); innerClass.print(); } /** * inner class */ class InnerClass { void print() { // inner class裡面直接調用了外部類的私有成員變量和成員方法 System.out.println(privateVariable); privateMethod("invoke outer class private method."); // 調用外層類的公有變量 System.out.println(publicVariable); } } public static void main(String[] args) { new OuterClass().show(); }}上面代碼定義了外層類是 OuterClass ,內部類是 InnerClass 。外層類定義了1個公有變量、2個私有變量和1個私有方法。然後在InnerClass裡面直接使用了OuterClass的所有成員。程序運行結果如下:

private member variableinvoke outer class private method.public member variable反編譯一下上面的兩個類:

# javap -p OuterClass.class Compiled from "OuterClass.java"public class OuterClass { public java.lang.Object publicVariable; private java.lang.Object privateVariable; private java.lang.Object privateVariable2; public OuterClass(); private void privateMethod(java.lang.String); public void show(); public static void main(java.lang.String[]); # 注意這兩個靜態方法 static java.lang.Object access$000(OuterClass); static void access$100(OuterClass, java.lang.String);}# javap -p OuterClass\$InnerClass.classCompiled from "OuterClass.java"class OuterClass$InnerClass { # 注意這個final的成員變量和下面的構造函數 final OuterClass this$0; OuterClass$InnerClass(OuterClass); void print();}結論就是:

編譯器修改了內部類:增加了一個final的外層類實例作為內部類的成員變量;修改了內部類的構造函數,將外部類實例通過內部類的構造函數傳遞給內部類。這樣內部類就有了外部類的實例,上面的第1個條件就達成了。編譯器在外部類中增加了幾個非private的靜態方法。對於內部類訪問外部類的每一個私有成員,都會有這麼一個方法。這樣內部類就可以通過這些靜態方法去訪問外部類的私有成員了。非私有的成員直接通過1中的外層類實例即可訪問,所以就無需生成這些靜態方法了。再進一步驗證一下上面的結論1,當執行 InnerClass innerClass = new InnerClass(); 語句創建一個內部類實例之後,可以觀測到下面的結果:

可以看到,內部類實例( OuterClass$InnerClass@470 )自動引用了外層類實例( OuterClass@464 )。所以, inner class之所以能訪問外層類的成員是因為它在實例化的時候就已經和一個外層類的實例關聯了 ,實際是通過這個外層類實例去訪問外層類的成員。對於私有成員,還生成了一些輔助的靜態方法。這也說明,要實例化inner class,必須先實例化它的外層類。

另外有個限制就是inner class裡面不能定義靜態變量和靜態方法,一個例外是可以定義基礎類型和String類型的靜態常量。比如:

static final String s = 「s」; // OKstatic final int i = 5; // OKstatic final Integer ii = 5; // 錯誤local class和anonymous class都屬於特殊的inner class,所以上面講述的所有東西對他們也適用。

為什麼static nested class不能訪問外層類的非靜態成員

原因很簡單,static nested class除了被定義到某個類裡面以外,幾乎和普通的類沒有什麼區別。 它不會和外層類的某個實例關聯,比如我們在上面的OuterClass裡面再定義一個 StaticNestedClass :

static class StaticNestedClass { private int a; void foo() {}}反編譯以後:

# javap -p OuterClass\$StaticNestedClass.classCompiled from "OuterClass.java"class OuterClass$StaticNestedClass { private int a; OuterClass$StaticNestedClass(); void foo();}除了類名被改寫了以外,和原來定義的類沒有任何區別。所以如果沒有被定義為private的話,static nested class完全可以獨立於外層類使用。

所有nested class都可以訪問外層類的靜態成員

上面討論的都是nested class能不能訪問外層類的非靜態成員,那如果是靜態成員呢?結論就是所有nested class都可以訪問的靜態成員,不管是不是私有。原理的話和inner class訪問外層類非static成員是一樣的,如果是private的,編譯器會在外層中生成一個輔助訪問的static方法,如果是非私有的,那通過類就可以直接訪問。

## 如果nested class是private的?

我們知道正常的類是不能使用private和protected的(只能是public或者不加訪問修飾符),但nested class卻可以,因為nested class其實就是外層類的一個特殊成員,就像成員變量、成員方法一樣。比如,如果我們不想讓外部的其它類看到nested class的類,就可以將它設置成private的,但對於外層類是沒有影響的,照樣可以操作這個類。這個怎麼做到的呢?

我們將上面的StaticNestedClass改為private的:

private static class StaticNestedClass { void foo() { System.out.println(a); }}反編譯看下:

# javap -p OuterClass\$StaticNestedClass.classCompiled from "OuterClass.java"class OuterClass$StaticNestedClass { private OuterClass$StaticNestedClass(); void foo(); OuterClass$StaticNestedClass(OuterClass$1);}可以看到,如果nested class被設置成private,它原來的構造函數就會被設置為private的,同時編譯器又新增了一個外部可見的構造函數 OuterClass$StaticNestedClass(OuterClass$1) ,這個構造函數的一個入參就是外部類的實例。這樣,外部類實例化nested class的時候會先調用這個構造函數,這個構造函數內部又調用了原始的private的構造函數。inner class也是這樣的。

## 總結

嵌套類的實質就是外層類的成員,就像成員變量、成員方法一樣,初衷是提高代碼結構的緊湊和可維護性。 使用嵌套類的幾乎唯一的場景就是這個內部類僅供外層類(或者包含它的代碼塊)使用,其它場景都應該使用正常的一級類 。按這個思路使用即可,不要濫用,更不要搞騷操作。

Reference

Core Java Volume IOrace The Java Tutorials原文連結:https://niyanchun.com/java-nested-class.html

如果覺得本文對你有幫助,可以轉發關注支持一下

相關焦點

  • JAVA歷史版本
    java發展時間線 JAVA發展 1.1996年1月23日 JDK 1.0 Java虛擬機Sun Classic VM,Applet,AWT 2.1997年2月19日 JDK 1.1 JAR文件格式,JDBC,JavaBeans,RMI不 跨語言,內部類,反射 3.1998年12月4日 JDK 1.2
  • ECharts-Java 類庫 2.2.6 版本發布
    支持所有的Style類,如AreaStyle,ChordStyle,ItemStyle,LineStyle,LinkStyle等等。支持多種Data數據類型,一個通用的Data數據,以及PieData,MapData,ScatterData,KData等針對性的數據結構。
  • Java8 lambda表達式語法
    但是有一點這裡強調一下(Windows系統):目前我們工作的版本一般是java 6或者java 7,所以很多人安裝java8基本都是學習為主。這樣就在自己的機器上會存在多版本的JDK。而且大家一般是希望在命令行中執行java命令是基於老版本的jdk。但是在安裝完jdk8並且沒有設置path的情況下,你如果在命令行中輸入:java -version,屏幕上會顯示是jdk 8。
  • Java反射初探 ——「當類也學會照鏡子」
    例如,假設我們的java文件涉及三個類:a類,b類和c類,那麼編譯的時候就會對應生成a類的「類」對象,a類的「類」對象,a類的「類」對象,分別用於保存和a,b,c類對應的信息 我們的思維是這樣的: 一個對象必然有一個與之對應的類,因為只有類才能實例化對象啊 那麼,「類對象」的「
  • Visual Studio Code 10 月 Java 擴展更新
    Create non existing package當你的包名與文件夾名不匹配時,你可以選擇在代碼中更改包名,或者在文件系統中移動文件夾(即使目標文件夾還不存在)。新特性通過 VS Code 首選項中的 java.actionsOnPaste.OrganeImports 首選項啟用。如果為 true(默認值),則在將 Java 代碼粘貼到空文件中時觸發「Organize imports」。
  • 面試頻率最高的簡單問題——Java類的三大基本特徵
    學習過Java的程式設計師都知道,java類有三大特徵——封裝、繼承和多態。下面的文章給大家詳細的介紹一下java的這三大特性。封裝封裝是將描述某類事物的數據與處理這些數據的函數封裝在一起,形成一個有機整體,稱為類。類所具有的的封裝性可使程序模塊具有良好的獨立性與可維護性。
  • Java基礎教程:java反射機制教程
    現在出現一個問題,如果這個user類不是我們自己定義的,我們從外部看不到裡面有什麼東西,而且我們又想去知道內部長什麼樣,比如說有幾個欄位、方法、構造方法、共有還是私有的等等,這時候該怎麼辦呢?這時候java語言在設計的時候為我們提供了一個機制,就是反射機制,他能夠很方便的去解決我們的問題。
  • Java之Random類的簡單介紹
    各位小夥伴這次小編要介紹的是Random類,它是用來形成隨機數字的,使用Random有三個步驟,與之前講的Scanner類差不多。第一步,導包:import java.util.Random第二步,創建:Random a=new Random();小括號是可以留空的第三步,使用:如果要獲取一個隨機數int數字(範圍是int所有範圍,有正負兩種):int num=a.nextInt();為了方便大家的理解,小編就先粘幾行代碼,是一個比較簡單的猜數字小遊戲,代碼如下:
  • Java之成員內部類詳解
    前言在上文中,講到了靜態內部類,本文主要談一下成員內部類、局部內部類和匿名內部類。成員內部類和靜態內部類非常相似,都是定義在一個類中的成員位置,與靜態內部類唯一的區別是,成員內部類沒有static修飾。
  • Java8 lambda表達式
    在該例子中,我們創建了一個對象實現了ActionListener接口,該接口只有一個方法actionPerformed(),當用戶點擊了按鈕之後,這個方法會被調用,該匿名內部類提供了該方法的實現。匿名內部類是為了讓java程式設計師傳遞行為和傳遞數據一樣容易,不幸的是,他們並不容易,為了調用處理邏輯的代碼仍然有四行模板代碼,重複的模板代碼並不是唯一的問題,這種代碼也難以閱讀,我們並不想傳遞一個對象,而僅僅只需要傳遞某種行為,在java8中我們可以寫得更簡潔不同於傳遞一個實現某個接口的對象,我們傳遞了一段沒有命名函數的代碼
  • 我的世界:高達19格、自然生成的仙人掌,你沒有見過的MC新特性
    而對於MC玩家來說,別人的世界永遠都要比自己的世界更有趣,因為他們時常可以發現一些奇奇怪怪、未曾見過的新特性。而一起分享這些新特性,是MC玩家樂此不疲、最喜歡的事情。當然對於這種小特性MC玩家自然是見多不怪了,地圖種子-1505596464,適用版本為基巖版1.14.0,坐標在圖中右上角。注意版本一定要準確,迷戀在最新的測試版中並沒有發現這棵樹。
  • 萬字梳理,帶你拿下 Java 面試題!
    因為都是字符串啊,字符串比較的不都是堆空間嗎,猛然一看發現好像永遠也不會走,但是你忘記了 String.intern() 方法,它表示的概念在不同的 JDK 版本有不同的區分。在 JDK1.7 及以後調用 intern 方法是判斷運行時常量池中是否有指定的字符串,如果沒有的話,就把字符串添加到常量池中,並返回常量池中的對象。
  • Java反射機制深入詳解
    一.概念反射就是把Java的各種成分映射成相應的Java類。Class類的構造方法是private,由JVM創建。反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取他所有的成員變量和方法並且顯示出來。
  • Java之File類的構造方法
    Java之File類的簡單介紹,File類的靜態成員變量,這次小編要介紹的是File類的構造方法(f1);//重寫了Object類的toString方法,列印的是一個路徑:c:\Users\java\code\a.textFile f2=new File("c:\\Users\\java\\code");System.out.println(f2);
  • JAVA反序列化—FastJson抗爭的一生
    並沒有調用is開頭的方法自己從源碼中分析或者嘗試在類中添加isXx方法都是不會被調用的,這裡只是為了指出其他文章中的一個錯誤。這個與調用的set方法綁定的屬性,再之後並沒有發現對於調用過程有什麼影響。所以只要目標類中有滿足條件的set方法,然後得到的方法變量名存在於序列化字符串中,這個set方法就可以被調用。
  • 淺談Java中的幾種隨機數
    最明顯的,也是直觀的方式,在Java中生成隨機數隻要簡單的調用:java.lang.Math.random() 在所有其他語言中,生成隨機數就像是使用Math工具類,如abs, pow, floor, sqrt和其他數學函數。大多數人通過書籍、教程和課程來了解這個類。一個簡單的例子:從0.0到1.0之間可以生成一個雙精度浮點數。
  • Java類隔離加載實現原理是什麼?
    只要Java 代碼寫得足夠多就一定會出現這種情況:系統新引入了一個中間件的 jar 包,編譯的時候一切正常,一運行就報錯:java.lang.NoSuchMethodError,然後就哼哧哼哧的開始找解決方法,最後在幾百個依賴包裡面找的眼睛都快瞎了才找到衝突的 jar,把問題解決之後就開始吐槽中間件為啥搞那麼多不同版本的 jar,寫代碼五分鐘,排包排了一整天。
  • QQ 誕生 20 周年,第一個版本你見過嗎?
    ,如果沒有 20 年前發布的QQ,那麼就沒有如今強大的騰訊,相信大家都知道,QQ 的前身是 OICQ 。但你見過它最初的版本嗎?第一個 QQ 版本僅幾百KB,一張圖片大小20 年前的今夜,騰訊公司幾個創始人親自開發了第一個QQ版本並發布,據了解,當時國內還沒有綜合業務數字網(
  • EffectiveJava-3-類和接口
    保證類不會被擴展:用final修飾或將所有構造方法私有化並提供公有的靜態工廠方法3. 使所有域都是final的(實際上只要沒有方法能夠對域產生外部可見的改變即可,如延遲初始化,懶漢單例等,都不能讓該域是final的)4. 使所有域都是私有的5.
  • 我的世界一個遊戲兩個版本 你不知道的七個特性
    ,如果你直接用五個木板便能合成,那麼你的遊戲就是java版本,如果需要額外加一把木鏟,說明你的遊戲版本是基巖版。,不過也存在著特例,java版中的骨粉無法對花起作用,而基巖版本中,當玩家對著花使用骨粉時,會在其附近生長更多的花朵來,憑藉這個特性,分清兩個版本就不再困難了。