您好,登錄后才能下訂單哦!
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看
https://github.com/h3pl/Java-Tutorial
喜歡的話麻煩點下Star哈
文章首發(fā)于我的個人博客:
www.how2playlife.comww.how2playlife.com
內(nèi)部類是指在一個外部類的內(nèi)部再定義一個類。內(nèi)部類作為外部類的一個成員,并且依附于外部類而存在的。內(nèi)部類可為靜態(tài),可用protected和private修飾(而外部類只能使用public和缺省的包訪問權(quán)限)。內(nèi)部類主要有以下幾類:成員內(nèi)部類、局部內(nèi)部類、靜態(tài)內(nèi)部類、匿名內(nèi)部類
(1)內(nèi)部類仍然是一個獨立的類,在編譯之后內(nèi)部類會被編譯成獨立的.class文件,但是前面冠以外部類的類名和$符號 。
(2)內(nèi)部類不能用普通的方式訪問。
(3)內(nèi)部類聲明成靜態(tài)的,就不能隨便的訪問外部類的成員變量了,此時內(nèi)部類只能訪問外部類的靜態(tài)成員變量 。
(4)外部類不能直接訪問內(nèi)部類的的成員,但可以通過內(nèi)部類對象來訪問
內(nèi)部類是外部類的一個成員,因此內(nèi)部類可以自由地訪問外部類的成員變量,無論是否是private的。
因為當某個外圍類的對象創(chuàng)建內(nèi)部類的對象時,此內(nèi)部類會捕獲一個隱式引用,它引用了實例化該內(nèi)部對象的外圍類對象。通過這個指針,可以訪問外圍類對象的全部狀態(tài)。
通過反編譯內(nèi)部類的字節(jié)碼,分析之后主要是通過以下幾步做到的:
1 編譯器自動為內(nèi)部類添加一個成員變量, 這個成員變量的類型和外部類的類型相同, 這個成員變量就是指向外部類對象的引用;
2 編譯器自動為內(nèi)部類的構(gòu)造方法添加一個參數(shù), 參數(shù)的類型是外部類的類型, 在構(gòu)造方法內(nèi)部使用這個參數(shù)為1中添加的成員變量賦值;
3 在調(diào)用內(nèi)部類的構(gòu)造函數(shù)初始化內(nèi)部類對象時, 會默認傳入外部類的引用。
靜態(tài)內(nèi)部類的作用:
1 只是為了降低包的深度,方便類的使用,靜態(tài)內(nèi)部類適用于包含類當中,但又不依賴與外在的類。
2 由于Java規(guī)定靜態(tài)內(nèi)部類不能用使用外在類的非靜態(tài)屬性和方法,所以只是為了方便管理類結(jié)構(gòu)而定義。于是我們在創(chuàng)建靜態(tài)內(nèi)部類的時候,不需要外部類對象的引用。
非靜態(tài)內(nèi)部類的作用:
1 內(nèi)部類繼承自某個類或?qū)崿F(xiàn)某個接口,內(nèi)部類的代碼操作創(chuàng)建其他外圍類的對象。所以你可以認為內(nèi)部類提供了某種進入其外圍類的窗口。
2 使用內(nèi)部類最吸引人的原因是:每個內(nèi)部類都能獨立地繼承自一個(接口的)實現(xiàn),所以無論外圍類是否已經(jīng)繼承了某個(接口的)實現(xiàn),對于內(nèi)部類都沒有影響
3 如果沒有內(nèi)部類提供的可以繼承多個具體的或抽象的類的能力,一些設(shè)計與編程問題就很難解決。
從這個角度看,內(nèi)部類使得多重繼承的解決方案變得完整。接口解決了部分問題,而內(nèi)部類有效地實現(xiàn)了”多重繼承”。
問得好,區(qū)別如下:
(1)靜態(tài)內(nèi)部類不持有外部類的引用
在普通內(nèi)部類中,我們可以直接訪問外部類的屬性、方法,即使是private類型也可以訪問,這是因為內(nèi)部類持有一個外部類的引用,可以自由訪問。而靜態(tài)內(nèi)部類,則只可以訪問外部類的靜態(tài)方法和靜態(tài)屬性(如果是private權(quán)限也能訪問,這是由其代碼位置所決定的),其他則不能訪問。(2)靜態(tài)內(nèi)部類不依賴外部類
普通內(nèi)部類與外部類之間是相互依賴的關(guān)系,內(nèi)部類實例不能脫離外部類實例,也就是說它們會同生同死,一起聲明,一起被垃圾回收器回收。而靜態(tài)內(nèi)部類是可以獨立存在的,即使外部類消亡了,靜態(tài)內(nèi)部類還是可以存在的。(3)普通內(nèi)部類不能聲明static的方法和變量
普通內(nèi)部類不能聲明static的方法和變量,注意這里說的是變量,常量(也就是final static修飾的屬性)還是可以的,而靜態(tài)內(nèi)部類形似外部類,沒有任何限制。
1 成員內(nèi)部類 之所以叫做成員 就是說他是類實例的一部分 而不是類的一部分
2 結(jié)構(gòu)上來說 他和你聲明的成員變量是一樣的地位 一個特殊的成員變量 而靜態(tài)的變量是類的一部分和實例無關(guān)
3 你若聲明一個成員內(nèi)部類 讓他成為主類的實例一部分 然后又想在內(nèi)部類聲明和實例無關(guān)的靜態(tài)的東西 你讓JVM情何以堪啊
4 若想在內(nèi)部類內(nèi)聲明靜態(tài)字段 就必須將其內(nèi)部類本身聲明為靜態(tài)
非靜態(tài)內(nèi)部類有一個很大的優(yōu)點:可以自由使用外部類的所有變量和方法
下面的例子大概地介紹了
1 非靜態(tài)內(nèi)部類和靜態(tài)內(nèi)部類的區(qū)別。
2 不同訪問權(quán)限的內(nèi)部類的使用。
3 外部類和它的內(nèi)部類之間的關(guān)系
//本節(jié)討論內(nèi)部類以及不同訪問權(quán)限的控制
//內(nèi)部類只有在使用時才會被加載。
//外部類B
public class B{
int i = 1;
int j = 1;
static int s = 1;
static int ss = 1;
A a;
AA aa;
AAA aaa;
//內(nèi)部類A
public class A {
// static void go () {
//
// }
// static {
//
// }
// static int b = 1;//非靜態(tài)內(nèi)部類不能有靜態(tài)成員變量和靜態(tài)代碼塊和靜態(tài)方法,
// 因為內(nèi)部類在外部類加載時并不會被加載和初始化。
//所以不會進行靜態(tài)代碼的調(diào)用
int i = 2;//外部類無法讀取內(nèi)部類的成員,而內(nèi)部類可以直接訪問外部類成員
public void test() {
System.out.println(j);
j = 2;
System.out.println(j);
System.out.println(s);//可以訪問類的靜態(tài)成員變量
}
public void test2() {
AA aa = new AA();
AAA aaa = new AAA();
}
}
//靜態(tài)內(nèi)部類S,可以被外部訪問
public static class S {
int i = 1;//訪問不到非靜態(tài)變量。
static int s = 0;//可以有靜態(tài)變量
public static void main(String[] args) {
System.out.println(s);
}
@Test
public void test () {
// System.out.println(j);//報錯,靜態(tài)內(nèi)部類不能讀取外部類的非靜態(tài)變量
System.out.println(s);
System.out.println(ss);
s = 2;
ss = 2;
System.out.println(s);
System.out.println(ss);
}
}
//內(nèi)部類AA,其實這里加protected相當于default
//因為外部類要調(diào)用內(nèi)部類只能通過B。并且無法直接繼承AA,所以必須在同包
//的類中才能調(diào)用到(這里不考慮靜態(tài)內(nèi)部類),那么就和default一樣了。
protected class AA{
int i = 2;//內(nèi)部類之間不共享變量
public void test (){
A a = new A();
AAA aaa = new AAA();
//內(nèi)部類之間可以互相訪問。
}
}
//包外部依然無法訪問,因為包沒有繼承關(guān)系,所以找不到這個類
protected static class SS{
int i = 2;//內(nèi)部類之間不共享變量
public void test (){
//內(nèi)部類之間可以互相訪問。
}
}
//私有內(nèi)部類A,對外不可見,但對內(nèi)部類和父類可見
private class AAA {
int i = 2;//內(nèi)部類之間不共享變量
public void test() {
A a = new A();
AA aa = new AA();
//內(nèi)部類之間可以互相訪問。
}
}
@Test
public void test(){
A a = new A();
a.test();
//內(nèi)部類可以修改外部類的成員變量
//打印出 1 2
B b = new B();
}
}
//另一個外部類
class C {
@Test
public void test() {
//首先,其他類內(nèi)部類只能通過外部類來獲取其實例。
B.S s = new B.S();
//靜態(tài)內(nèi)部類可以直接通過B類直接獲取,不需要B的實例,和靜態(tài)成員變量類似。
//B.A a = new B.A();
//當A不是靜態(tài)類時這行代碼會報錯。
//需要使用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為私有內(nèi)部類,外部類不可見
//當A使用private修飾時,使用B的實例也無法獲取A的實例,這一點和私有變量是一樣的。
//所有普通的內(nèi)部類與類中的一個變量是類似的。靜態(tài)內(nèi)部類則與靜態(tài)成員類似。
}
}
可能剛才的例子中沒辦法直觀地看到內(nèi)部類是如何加載的,接下來用例子展示一下內(nèi)部類加載的過程。
1 內(nèi)部類是延時加載的,也就是說只會在第一次使用時加載。不使用就不加載,所以可以很好的實現(xiàn)單例模式。
2 不論是靜態(tài)內(nèi)部類還是非靜態(tài)內(nèi)部類都是在第一次使用時才會被加載。
3 對于非靜態(tài)內(nèi)部類是不能出現(xiàn)靜態(tài)模塊(包含靜態(tài)塊,靜態(tài)屬性,靜態(tài)方法等)
4 非靜態(tài)類的使用需要依賴于外部類的對象,詳見上述對象innerClass 的初始化。
簡單來說,類的加載都是發(fā)生在類要被用到的時候。內(nèi)部類也是一樣
1 普通內(nèi)部類在第一次用到時加載,并且每次實例化時都會執(zhí)行內(nèi)部成員變量的初始化,以及代碼塊和構(gòu)造方法。
2 靜態(tài)內(nèi)部類也是在第一次用到時被加載。但是當它加載完以后就會將靜態(tài)成員變量初始化,運行靜態(tài)代碼塊,并且只執(zhí)行一次。當然,非靜態(tài)成員和代碼塊每次實例化時也會執(zhí)行。
總結(jié)一下Java類代碼加載的順序,萬變不離其宗。
規(guī)律一、初始化構(gòu)造時,先父后子;只有在父類所有都構(gòu)造完后子類才被初始化
規(guī)律二、類加載先是靜態(tài)、后非靜態(tài)、最后是構(gòu)造函數(shù)。
靜態(tài)構(gòu)造塊、靜態(tài)類屬性按出現(xiàn)在類定義里面的先后順序初始化,同理非靜態(tài)的也是一樣的,只是靜態(tài)的只在加載字節(jié)碼時執(zhí)行一次,不管你new多少次,非靜態(tài)會在new多少次就執(zhí)行多少次
規(guī)律三、java中的類只有在被用到的時候才會被加載
規(guī)律四、java類只有在類字節(jié)碼被加載后才可以被構(gòu)造成對象實例
在方法中定義的內(nèi)部類稱為局部內(nèi)部類。與局部變量類似,局部內(nèi)部類不能有訪問說明符,因為它不是外圍類的一部分,但是它可以訪問當前代碼塊內(nèi)的常量,和此外圍類所有的成員。
需要注意的是:
局部內(nèi)部類只能在定義該內(nèi)部類的方法內(nèi)實例化,不可以在此方法外對其實例化。
public class 局部內(nèi)部類 {
class A {//局部內(nèi)部類就是寫在方法里的類,只在方法執(zhí)行時加載,一次性使用。
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報錯,方法里不能定義靜態(tài)內(nèi)部類。
//因為只有在方法調(diào)用時才能進行類加載和初始化。
}
}
}
簡單地說:匿名內(nèi)部類就是沒有名字的內(nèi)部類,并且,匿名內(nèi)部類是局部內(nèi)部類的一種特殊形式。什么情況下需要使用匿名內(nèi)部類?如果滿足下面的一些條件,使用匿名內(nèi)部類是比較合適的:
只用到類的一個實例。
類在定義后馬上用到。
類非常?。⊿UN推薦是在4行代碼以下)
給類命名并不會導致你的代碼更容易被理解。
在使用匿名內(nèi)部類時,要記住以下幾個原則:
1 匿名內(nèi)部類不能有構(gòu)造方法。
2 匿名內(nèi)部類不能定義任何靜態(tài)成員、方法和類。
3 匿名內(nèi)部類不能是public,protected,private,static。
4 只能創(chuàng)建匿名內(nèi)部類的一個實例。
5 一個匿名內(nèi)部類一定是在new的后面,用其隱含實現(xiàn)一個接口或?qū)崿F(xiàn)一個類。
6 因匿名內(nèi)部類為局部內(nèi)部類,所以局部內(nèi)部類的所有限制都對其生效。
一個匿名內(nèi)部類的例子:
public class 匿名內(nèi)部類 {
}
interface D{
void run ();
}
abstract class E{
E (){
}
abstract void work();
}
class A {
@Test
public void test (int k) {
//利用接口寫出一個實現(xiàn)該接口的類的實例。
//有且僅有一個實例,這個類無法重用。
new Runnable() {
@Override
public void run() {
// k = 1;報錯,當外部方法中的局部變量在內(nèi)部類使用中必須改為final類型。
//因為方外部法中即使改變了這個變量也不會反映到內(nèi)部類中。
//所以對于內(nèi)部類來講這只是一個常量。
System.out.println(100);
System.out.println(k);
}
};
new D(){
//實現(xiàn)接口的匿名類
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
參考文件: http://android.blog.51cto.com/268543/384844
我們給匿名內(nèi)部類傳遞參數(shù)的時候,若該形參在內(nèi)部類中需要被使用,那么該形參必須要為final。也就是說:當所在的方法的形參需要被內(nèi)部類里面使用時,該形參必須為final。
為什么必須要為final呢?
首先我們知道在內(nèi)部類編譯成功后,它會產(chǎn)生一個class文件,該class文件與外部類并不是同一class文件,僅僅只保留對外部類的引用。當外部類傳入的參數(shù)需要被內(nèi)部類調(diào)用時,從java程序的角度來看是直接被調(diào)用:
public class OuterClass { public void display(final String name,String age){ class InnerClass{ void display(){ System.out.println(name); } } } }
從上面代碼中看好像name參數(shù)應該是被內(nèi)部類直接調(diào)用?其實不然,在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 );
}
}
所以從上面代碼來看,內(nèi)部類并不是直接調(diào)用方法傳遞的參數(shù),而是利用自身的構(gòu)造器對傳入的參數(shù)進行備份,自己內(nèi)部方法調(diào)用的實際上時自己的屬性而不是外部方法傳遞進來的參數(shù)。
直到這里還沒有解釋為什么是final
在內(nèi)部類中的屬性和外部方法的參數(shù)兩者從外表上看是同一個東西,但實際上卻不是,所以他們兩者是可以任意變化的,也就是說在內(nèi)部類中我對屬性的改變并不會影響到外部的形參,而然這從程序員的角度來看這是不可行的。
畢竟站在程序的角度來看這兩個根本就是同一個,如果內(nèi)部類該變了,而外部方法的形參卻沒有改變這是難以理解和不可接受的,所以為了保持參數(shù)的一致性,就規(guī)定使用final來避免形參的不改變。
簡單理解就是,拷貝引用,為了避免引用值發(fā)生改變,例如被外部類的方法修改等,而導致內(nèi)部類得到的值不一致,于是用final來讓該引用不可改變。
故如果定義了一個匿名內(nèi)部類,并且希望它使用一個其外部定義的參數(shù),那么編譯器會要求該參數(shù)引用是final的。
我們一般都是利用構(gòu)造器來完成某個實例的初始化工作的,但是匿名內(nèi)部類是沒有構(gòu)造器的!那怎么來初始化匿名內(nèi)部類呢?使用構(gòu)造代碼塊!利用構(gòu)造代碼塊能夠達到為匿名內(nèi)部類創(chuàng)建一個構(gòu)造器的效果。
public class OutClass {
public InnerClass getInnerClass(final int age,final String name){
return new InnerClass() {
int age_ ;
String name_;
//構(gòu)造代碼塊完成初始化工作
{
if(0 < age && age < 200){
age_ = age;
name_ = name;
}
}
public String getName() {
return name_;
}
public int getAge() {
return age_;
}
};
}
如果你創(chuàng)建了一個內(nèi)部類,然后繼承其外圍類并重新定義此內(nèi)部類時,會發(fā)生什么呢?也就是說,內(nèi)部類可以被重載嗎?這看起來似乎是個很有用的點子,但是“重載”內(nèi)部類就好像它是外圍類的一個方法,其實并不起什么作用:
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();
}
}
復制代碼
輸出結(jié)果為:
New Egg()
Egg.Yolk()
缺省的構(gòu)造器是編譯器自動生成的,這里是調(diào)用基類的缺省構(gòu)造器。你可能認為既然創(chuàng)建了BigEgg 的對象,那么所使用的應該是被“重載”過的Yolk,但你可以從輸出中看到實際情況并不是這樣的。
這個例子說明,當你繼承了某個外圍類的時候,內(nèi)部類并沒有發(fā)生什么特別神奇的變化。這兩個內(nèi)部類是完全獨立的兩個實體,各自在自己的命名空間內(nèi)。
因為內(nèi)部類的構(gòu)造器要用到其外圍類對象的引用,所以在你繼承一個內(nèi)部類的時候,事情變得有點復雜。問題在于,那個“秘密的”外圍類對象的引用必須被初始化,而在被繼承的類中并不存在要聯(lián)接的缺省對象。要解決這個問題,需使用專門的語法來明確說清它們之間的關(guān)聯(lián):
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);
}
}
復制代碼
輸出結(jié)果為:
this is a constructor in WithInner.Inner
this is a constructor in InheritInner
可以看到,InheritInner 只繼承自內(nèi)部類,而不是外圍類。但是當要生成一個構(gòu)造器時,缺省的構(gòu)造器并不算好,而且你不能只是傳遞一個指向外圍類對象的引用。此外,你必須在構(gòu)造器內(nèi)使用如下語法:
enclosingClassReference.super();
這樣才提供了必要的引用,然后程序才能編譯通過。
有關(guān)匿名內(nèi)部類實現(xiàn)回調(diào),事件驅(qū)動,委托等機制的文章將在下一節(jié)講述。
內(nèi)部類為什么能夠訪問外部類的成員?
定義內(nèi)部類如下:
使用javap命令進行反編譯。
編譯后得到Main.class Main$Inner.class兩個文件,反編譯Main$Inner.class文件如下:
可以看到,內(nèi)部類其實擁有外部類的一個引用,在構(gòu)造函數(shù)中將外部類的引用傳遞進來。
匿名內(nèi)部類為什么只能訪問局部的final變量?
其實可以這樣想,當方法執(zhí)行完畢后,局部變量的生命周期就結(jié)束了,而局部內(nèi)部類對象的生命周期可能還沒有結(jié)束,那么在局部內(nèi)部類中訪問局部變量就不可能了,所以將局部變量改為final,改變其生命周期。
編寫代碼如下:
這段代碼編譯為Main.class Main$1.class兩個文件,反編譯Main$1.class文件如下:
可以看到,java將編譯時已經(jīng)確定的值直接復制,進行替換,將無法確定的值放到了內(nèi)部類的常量池中,并在構(gòu)造函數(shù)中將其從常量池取出到字段中。
可以看出,java將局部變量m直接進行復制,所以其并不是原來的值,若在內(nèi)部類中將m更改,局部變量的m值不會變,就會出現(xiàn)數(shù)據(jù)不一致,所以java就將其限制為final,使其不能進行更改,這樣數(shù)據(jù)不一致的問題就解決了。
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/dengchengchao/p/9713979.html
如果大家想要實時關(guān)注我更新的文章以及分享的干貨的話,可以關(guān)注我的公眾號【Java技術(shù)江湖】一位阿里 Java 工程師的技術(shù)小站,作者黃小斜,專注 Java 相關(guān)技術(shù):SSM、SpringBoot、MySQL、分布式、中間件、集群、Linux、網(wǎng)絡(luò)、多線程,偶爾講點Docker、ELK,同時也分享技術(shù)干貨和學習經(jīng)驗,致力于Java全棧開發(fā)!
Java工程師必備學習資源: 一些Java工程師常用學習資源,關(guān)注公眾號后,后臺回復關(guān)鍵字 “Java” 即可免費無套路獲取。
作者是 985 碩士,螞蟻金服 JAVA 工程師,專注于 JAVA 后端技術(shù)棧:SpringBoot、MySQL、分布式、中間件、微服務,同時也懂點投資理財,偶爾講點算法和計算機理論基礎(chǔ),堅持學習和寫作,相信終身學習的力量!
程序員3T技術(shù)學習資源: 一些程序員學習技術(shù)的資源大禮包,關(guān)注公眾號后,后臺回復關(guān)鍵字 “資料” 即可免費無套路獲取。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。