
本系列文章将整理到我在GitHub上的《Java面試指南》倉庫,更多精彩内容請到我的倉庫裡檢視
https:// github.com/h2pl/Java-Tu torial
喜歡的話麻煩點下Star哈
文章首發于我的個人部落格:
www.how2playlife.comww.how2playlife.com
内部類初探
什麼是内部類?
内部類是指在一個外部類的内部再定義一個類。内部類作為外部類的一個成員,并且依附于外部類而存在的。内部類可為靜态,可用protected和private修飾(而外部類隻能使用public和預設的包通路權限)。内部類主要有以下幾類:成員内部類、局部内部類、靜态内部類、匿名内部類
内部類的共性
(1)内部類仍然是一個獨立的類,在編譯之後内部類會被編譯成獨立的.class檔案,但是前面冠以外部類的類名和$符号 。
(2)内部類不能用普通的方式通路。
(3)内部類聲明成靜态的,就不能随便的通路外部類的成員變量了,此時内部類隻能通路外部類的靜态成員變量 。
(4)外部類不能直接通路内部類的的成員,但可以通過内部類對象來通路
内部類是外部類的一個成員,是以内部類可以自由地通路外部類的成員變量,無論是否是private的。
因為當某個外圍類的對象建立内部類的對象時,此内部類會捕獲一個隐式引用,它引用了執行個體化該内部對象的外圍類對象。通過這個指針,可以通路外圍類對象的全部狀态。
通過反編譯内部類的位元組碼,分析之後主要是通過以下幾步做到的:
1 編譯器自動為内部類添加一個成員變量, 這個成員變量的類型和外部類的類型相同, 這個成員變量就是指向外部類對象的引用;
2 編譯器自動為内部類的構造方法添加一個參數, 參數的類型是外部類的類型, 在構造方法内部使用這個參數為1中添加的成員變量指派;
3 在調用内部類的構造函數初始化内部類對象時, 會預設傳入外部類的引用。
使用内部類的好處:
靜态内部類的作用:
1 隻是為了降低包的深度,友善類的使用,靜态内部類适用于包含類當中,但又不依賴與外在的類。
2 由于Java規定靜态内部類不能用使用外在類的非靜态屬性和方法,是以隻是為了友善管理類結構而定義。于是我們在建立靜态内部類的時候,不需要外部類對象的引用。
非靜态内部類的作用:
1 内部類繼承自某個類或實作某個接口,内部類的代碼操作建立其他外圍類的對象。是以你可以認為内部類提供了某種進入其外圍類的視窗。
2 使用内部類最吸引人的原因是:每個内部類都能獨立地繼承自一個(接口的)實作,是以無論外圍類是否已經繼承了某個(接口的)實作,對于内部類都沒有影響
3 如果沒有内部類提供的可以繼承多個具體的或抽象的類的能力,一些設計與程式設計問題就很難解決。 從這個角度看,内部類使得多重繼承的解決方案變得完整。接口解決了部分問題,而内部類有效地實作了"多重繼承"。
那靜态内部類與普通内部類有什麼差別呢?
問得好,差別如下:
(1)靜态内部類不持有外部類的引用 在普通内部類中,我們可以直接通路外部類的屬性、方法,即使是private類型也可以通路,這是因為内部類持有一個外部類的引用,可以自由通路。而靜态内部類,則隻可以通路外部類的靜态方法和靜态屬性(如果是private權限也能通路,這是由其代碼位置所決定的),其他則不能通路。
(2)靜态内部類不依賴外部類 普通内部類與外部類之間是互相依賴的關系,内部類執行個體不能脫離外部類執行個體,也就是說它們會同生同死,一起聲明,一起被垃圾回收器回收。而靜态内部類是可以獨立存在的,即使外部類消亡了,靜态内部類還是可以存在的。
(3)普通内部類不能聲明static的方法和變量 普通内部類不能聲明static的方法和變量,注意這裡說的是變量,常量(也就是final static修飾的屬性)還是可以的,而靜态内部類形似外部類,沒有任何限制。
為什麼普通内部類不能有靜态變量呢?
1 成員内部類 之是以叫做成員 就是說他是類執行個體的一部分 而不是類的一部分
2 結構上來說 他和你聲明的成員變量是一樣的地位 一個特殊的成員變量 而靜态的變量是類的一部分和執行個體無關
3 你若聲明一個成員内部類 讓他成為主類的執行個體一部分 然後又想在内部類聲明和執行個體無關的靜态的東西 你讓JVM情何以堪啊
4 若想在内部類内聲明靜态字段 就必須将其内部類本身聲明為靜态
非靜态内部類有一個很大的優點:可以自由使用外部類的所有變量和方法
下面的例子大概地介紹了
1 非靜态内部類和靜态内部類的差別。
2 不同通路權限的内部類的使用。
3 外部類和它的内部類之間的關系
//本節讨論内部類以及不同通路權限的控制
//内部類隻有在使用時才會被加載。
//外部類B
public class B{
int i = 1;
int j = 1;
static int s = 1;
static int ss = 1;
A a;
AA aa;
AAA aaa;
//内部類A
public class A {
// static void go () {
//
// }
// static {
//
// }
// static int b = 1;//非靜态内部類不能有靜态成員變量和靜态代碼塊和靜态方法,
// 因為内部類在外部類加載時并不會被加載和初始化。
//是以不會進行靜态代碼的調用
int i = 2;//外部類無法讀取内部類的成員,而内部類可以直接通路外部類成員
public void test() {
System.out.println(j);
j = 2;
System.out.println(j);
System.out.println(s);//可以通路類的靜态成員變量
}
public void test2() {
AA aa = new AA();
AAA aaa = new AAA();
}
}
//靜态内部類S,可以被外部通路
public static class S {
int i = 1;//通路不到非靜态變量。
static int s = 0;//可以有靜态變量
public static void main(String[] args) {
System.out.println(s);
}
@Test
public void test () {
// System.out.println(j);//報錯,靜态内部類不能讀取外部類的非靜态變量
System.out.println(s);
System.out.println(ss);
s = 2;
ss = 2;
System.out.println(s);
System.out.println(ss);
}
}
//内部類AA,其實這裡加protected相當于default
//因為外部類要調用内部類隻能通過B。并且無法直接繼承AA,是以必須在同包
//的類中才能調用到(這裡不考慮靜态内部類),那麼就和default一樣了。
protected class AA{
int i = 2;//内部類之間不共享變量
public void test (){
A a = new A();
AAA aaa = new AAA();
//内部類之間可以互相通路。
}
}
//包外部依然無法通路,因為包沒有繼承關系,是以找不到這個類
protected static class SS{
int i = 2;//内部類之間不共享變量
public void test (){
//内部類之間可以互相通路。
}
}
//私有内部類A,對外不可見,但對内部類和父類可見
private class AAA {
int i = 2;//内部類之間不共享變量
public void test() {
A a = new A();
AA aa = new AA();
//内部類之間可以互相通路。
}
}
@Test
public void test(){
A a = new A();
a.test();
//内部類可以修改外部類的成員變量
//列印出 1 2
B b = new B();
}
}
//另一個外部類
class C {
@Test
public void test() {
//首先,其他類内部類隻能通過外部類來擷取其執行個體。
B.S s = new B.S();
//靜态内部類可以直接通過B類直接擷取,不需要B的執行個體,和靜态成員變量類似。
//B.A a = new B.A();
//當A不是靜态類時這行代碼會報錯。
//需要使用B的執行個體來擷取A的執行個體
B b = new B();
B.A a = b.new A();
B.AA aa = b.new AA();//B和C同包,是以可以通路到AA
// B.AAA aaa = b.new AAA();AAA為私有内部類,外部類不可見
//當A使用private修飾時,使用B的執行個體也無法擷取A的執行個體,這一點和私有變量是一樣的。
//所有普通的内部類與類中的一個變量是類似的。靜态内部類則與靜态成員類似。
}
}
内部類的加載
可能剛才的例子中沒辦法直覺地看到内部類是如何加載的,接下來用例子展示一下内部類加載的過程。
1 内部類是延時加載的,也就是說隻會在第一次使用時加載。不使用就不加載,是以可以很好的實作單例模式。
2 不論是靜态内部類還是非靜态内部類都是在第一次使用時才會被加載。
3 對于非靜态内部類是不能出現靜态子產品(包含靜态塊,靜态屬性,靜态方法等)
4 非靜态類的使用需要依賴于外部類的對象,詳見上述對象innerClass 的初始化。
簡單來說,類的加載都是發生在類要被用到的時候。内部類也是一樣
1 普通内部類在第一次用到時加載,并且每次執行個體化時都會執行内部成員變量的初始化,以及代碼塊和構造方法。
2 靜态内部類也是在第一次用到時被加載。但是當它加載完以後就會将靜态成員變量初始化,運作靜态代碼塊,并且隻執行一次。當然,非靜态成員和代碼塊每次執行個體化時也會執行。
總結一下Java類代碼加載的順序,萬變不離其宗。
規律一、初始化構造時,先父後子;隻有在父類所有都構造完後子類才被初始化
規律二、類加載先是靜态、後非靜态、最後是構造函數。
靜态構造塊、靜态類屬性按出現在類定義裡面的先後順序初始化,同理非靜态的也是一樣的,隻是靜态的隻在加載位元組碼時執行一次,不管你new多少次,非靜态會在new多少次就執行多少次
規律三、java中的類隻有在被用到的時候才會被加載
規律四、java類隻有在類位元組碼被加載後才可以被構造成對象執行個體
成員内部類
在方法中定義的内部類稱為局部内部類。與局部變量類似,局部内部類不能有通路說明符,因為它不是外圍類的一部分,但是它可以通路目前代碼塊内的常量,和此外圍類所有的成員。
需要注意的是: 局部内部類隻能在定義該内部類的方法内執行個體化,不可以在此方法外對其執行個體化。
public class 局部内部類 {
class A {//局部内部類就是寫在方法裡的類,隻在方法執行時加載,一次性使用。
public void test() {
class B {
public void test () {
class C {
}
}
}
}
}
@Test
public void test () {
int i = 1;
final int j = 2;
class A {
@Test
public void test () {
System.out.println(i);
System.out.println(j);
}
}
A a = new A();
System.out.println(a);
}
static class B {
public static void test () {
//static class A報錯,方法裡不能定義靜态内部類。
//因為隻有在方法調用時才能進行類加載和初始化。
}
}
}
匿名内部類
簡單地說:匿名内部類就是沒有名字的内部類,并且,匿名内部類是局部内部類的一種特殊形式。什麼情況下需要使用匿名内部類?如果滿足下面的一些條件,使用匿名内部類是比較合适的: 隻用到類的一個執行個體。 類在定義後馬上用到。 類非常小(SUN推薦是在4行代碼以下) 給類命名并不會導緻你的代碼更容易被了解。 在使用匿名内部類時,要記住以下幾個原則:
1 匿名内部類不能有構造方法。
2 匿名内部類不能定義任何靜态成員、方法和類。
3 匿名内部類不能是public,protected,private,static。
4 隻能建立匿名内部類的一個執行個體。
5 一個匿名内部類一定是在new的後面,用其隐含實作一個接口或實作一個類。
6 因匿名内部類為局部内部類,是以局部内部類的所有限制都對其生效。
一個匿名内部類的例子:
public class 匿名内部類 {
}
interface D{
void run ();
}
abstract class E{
E (){
}
abstract void work();
}
class A {
@Test
public void test (int k) {
//利用接口寫出一個實作該接口的類的執行個體。
//有且僅有一個執行個體,這個類無法重用。
new Runnable() {
@Override
public void run() {
// k = 1;報錯,當外部方法中的局部變量在内部類使用中必須改為final類型。
//因為方外部法中即使改變了這個變量也不會反映到内部類中。
//是以對于内部類來講這隻是一個常量。
System.out.println(100);
System.out.println(k);
}
};
new D(){
//實作接口的匿名類
int i =1;
@Override
public void run() {
System.out.println("run");
System.out.println(i);
System.out.println(k);
}
}.run();
new E(){
//繼承抽象類的匿名類
int i = 1;
void run (int j) {
j = 1;
}
@Override
void work() {
}
};
}
}
匿名内部類裡的final
使用的形參為何要為final
參考檔案:http://android.blog.51cto.com/268543/384844
我們給匿名内部類傳遞參數的時候,若該形參在内部類中需要被使用,那麼該形參必須要為final。也就是說:當所在的方法的形參需要被内部類裡面使用時,該形參必須為final。
為什麼必須要為final呢?
首先我們知道在内部類編譯成功後,它會産生一個class檔案,該class檔案與外部類并不是同一class檔案,僅僅隻保留對外部類的引用。當外部類傳入的參數需要被内部類調用時,從java程式的角度來看是直接被調用:
public class OuterClass {
public void display(final String name,String age){
class InnerClass{
void display(){
System.out.println(name);
}
}
}
}
從上面代碼中看好像name參數應該是被内部類直接調用?其實不然,在java編譯之後實際的操作如下:
public class OuterClass$InnerClass {
public InnerClass(String name,String age){
this.InnerClass$name = name;
this.InnerClass$age = age;
}
public void display(){
System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );
}
}
是以從上面代碼來看,内部類并不是直接調用方法傳遞的參數,而是利用自身的構造器對傳入的參數進行備份,自己内部方法調用的實際上時自己的屬性而不是外部方法傳遞進來的參數。
直到這裡還沒有解釋為什麼是final
在内部類中的屬性和外部方法的參數兩者從外表上看是同一個東西,但實際上卻不是,是以他們兩者是可以任意變化的,也就是說在内部類中我對屬性的改變并不會影響到外部的形參,而然這從程式員的角度來看這是不可行的。
畢竟站在程式的角度來看這兩個根本就是同一個,如果内部類該變了,而外部方法的形參卻沒有改變這是難以了解和不可接受的,是以為了保持參數的一緻性,就規定使用final來避免形參的不改變。
簡單了解就是,拷貝引用,為了避免引用值發生改變,例如被外部類的方法修改等,而導緻内部類得到的值不一緻,于是用final來讓該引用不可改變。
故如果定義了一個匿名内部類,并且希望它使用一個其外部定義的參數,那麼編譯器會要求該參數引用是final的。
内部類初始化
我們一般都是利用構造器來完成某個執行個體的初始化工作的,但是匿名内部類是沒有構造器的!那怎麼來初始化匿名内部類呢?使用構造代碼塊!利用構造代碼塊能夠達到為匿名内部類建立一個構造器的效果。
public class OutClass {
public InnerClass getInnerClass(final int age,final String name){
return new InnerClass() {
int age_ ;
String name_;
//構造代碼塊完成初始化工作
{
if(0 < age && age < 200){
age_ = age;
name_ = name;
}
}
public String getName() {
return name_;
}
public int getAge() {
return age_;
}
};
}
内部類的重載
如果你建立了一個内部類,然後繼承其外圍類并重新定義此内部類時,會發生什麼呢?也就是說,内部類可以被重載嗎?這看起來似乎是個很有用的點子,但是“重載”内部類就好像它是外圍類的一個方法,其實并不起什麼作用:
class Egg {
private Yolk y;
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
public Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args) {
new BigEgg();
}
}
複制代碼
輸出結果為:
New Egg()
Egg.Yolk()
預設的構造器是編譯器自動生成的,這裡是調用基類的預設構造器。你可能認為既然建立了BigEgg 的對象,那麼所使用的應該是被“重載”過的Yolk,但你可以從輸出中看到實際情況并不是這樣的。 這個例子說明,當你繼承了某個外圍類的時候,内部類并沒有發生什麼特别神奇的變化。這兩個内部類是完全獨立的兩個實體,各自在自己的命名空間内。
内部類的繼承
因為内部類的構造器要用到其外圍類對象的引用,是以在你繼承一個内部類的時候,事情變得有點複雜。問題在于,那個“秘密的”外圍類對象的引用必須被初始化,而在被繼承的類中并不存在要聯接的預設對象。要解決這個問題,需使用專門的文法來明确說清它們之間的關聯:
class WithInner {
class Inner {
Inner(){
System.out.println("this is a constructor in WithInner.Inner");
};
}
}
public class InheritInner extends WithInner.Inner {
// ! InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
System.out.println("this is a constructor in InheritInner");
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
複制代碼 輸出結果為: this is a constructor in WithInner.Inner this is a constructor in InheritInner
可以看到,InheritInner 隻繼承自内部類,而不是外圍類。但是當要生成一個構造器時,預設的構造器并不算好,而且你不能隻是傳遞一個指向外圍類對象的引用。此外,你必須在構造器内使用如下文法: enclosingClassReference.super(); 這樣才提供了必要的引用,然後程式才能編譯通過。
有關匿名内部類實作回調,事件驅動,委托等機制的文章将在下一節講述。
Java内部類的實作原理
内部類為什麼能夠通路外部類的成員?
定義内部類如下:
使用javap指令進行反編譯。
編譯後得到Main.class Main$Inner.class兩個檔案,反編譯Main$Inner.class檔案如下:
可以看到,内部類其實擁有外部類的一個引用,在構造函數中将外部類的引用傳遞進來。
匿名内部類為什麼隻能通路局部的final變量?
其實可以這樣想,當方法執行完畢後,局部變量的生命周期就結束了,而局部内部類對象的生命周期可能還沒有結束,那麼在局部内部類中通路局部變量就不可能了,是以将局部變量改為final,改變其生命周期。
編寫代碼如下:
這段代碼編譯為Main.class Main$1.class兩個檔案,反編譯Main$1.class檔案如下:
可以看到,java将編譯時已經确定的值直接複制,進行替換,将無法确定的值放到了内部類的常量池中,并在構造函數中将其從常量池取出到字段中。
可以看出,java将局部變量m直接進行複制,是以其并不是原來的值,若在内部類中将m更改,局部變量的m值不會變,就會出現資料不一緻,是以java就将其限制為final,使其不能進行更改,這樣資料不一緻的問題就解決了。
參考文章
https://www. cnblogs.com/hujingnb/p/ 10181621.html https:// blog.csdn.net/codingtu/ article/details/79336026 https://www. cnblogs.com/woshimrf/p/ java-inner-class.html https://www. cnblogs.com/dengchengch ao/p/9713979.html
微信公衆号
Java技術江湖
如果大家想要實時關注我更新的文章以及分享的幹貨的話,可以關注我的公衆号【Java技術江湖】一位阿裡 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分布式、中間件、叢集、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術幹貨和學習經驗,緻力于Java全棧開發!
Java工程師必備學習資源:一些Java工程師常用學習資源,關注公衆号後,背景回複關鍵字
“Java”即可免費無套路擷取。
個人公衆号:黃小斜
作者是 985 碩士,螞蟻金服 JAVA 工程師,專注于 JAVA 後端技術棧:SpringBoot、MySQL、分布式、中間件、微服務,同時也懂點投資理财,偶爾講點算法和計算機理論基礎,堅持學習和寫作,相信終身學習的力量!
程式員3T技術學習資源:一些程式員學習技術的資源大禮包,關注公衆号後,背景回複關鍵字
“資料”即可免費無套路擷取。