該項(xiàng)目源碼地址:https://github.com/ggb2312/JavaNotes/tree/master/java-basic
1. 簡介
可以將一個(gè)類的定義放在另一個(gè)類的定義內(nèi)部,這就是內(nèi)部類。
一般格式為:
public class Zoo{
...
class Panda{
}
}
內(nèi)部類是一種非常有用的特性,它允許你把一些邏輯相關(guān)的類組織在一起,并控制位于內(nèi)部的類的可見性。內(nèi)部類與組合是完全不同的概念。
內(nèi)部類提供一種代碼隱藏機(jī)制:“將類置于其他類的內(nèi)部”,同時(shí)內(nèi)部類也了解外部類,并能與之通信。
2. 內(nèi)部類實(shí)例
內(nèi)部類有以下四種形式
- 成員內(nèi)部類
- 局部內(nèi)部類
- 靜態(tài)內(nèi)部類
- 匿名內(nèi)部類
成員內(nèi)部類和靜態(tài)內(nèi)部類可以擁有private訪問權(quán)限、protected訪問權(quán)限、public訪問權(quán)限及包訪問權(quán)限。比如上面的例子,如果成員內(nèi)部類Inner用private修飾,則只能在外部類的內(nèi)部訪問,如果用public修飾,則任何地方都能訪問;如果用protected修飾,則只能在同一個(gè)包下或者繼承外部類的情況下訪問;如果是默認(rèn)訪問權(quán)限,則只能在同一個(gè)包下訪問。這一點(diǎn)和外部類有一點(diǎn)不一樣,外部類只能被public和包訪問兩種權(quán)限修飾。我個(gè)人是這么理解的,由于成員內(nèi)部類看起來像是外部類的一個(gè)成員,所以可以像類的成員一樣擁有多種權(quán)限修飾。
下面我們通過幾個(gè)實(shí)例來看看內(nèi)部類具體長什么樣。
2.1 成員內(nèi)部類
成員內(nèi)部類和成員變量一樣,屬于類的全局成員。
成員內(nèi)部類可以擁有private訪問權(quán)限、protected訪問權(quán)限、public訪問權(quán)限及包訪問權(quán)限。
一般格式:
public class OuterClass { //外部類
int id; // 成員變量
class InnerClass { //成員內(nèi)部類
}
}
實(shí)例:
一個(gè)類作為另一個(gè)類的成員(不是作為成員變量,作為成員變量的話就成組合模式了),同時(shí)成員內(nèi)部類可以無條件的使用外部類的一切靜態(tài)變量、成員變量、靜態(tài)方法、成員方法,用于內(nèi)部類和外部類通信。
public class OuterClass {
private String name;
public OuterClass(String name) {
this.name = name;
}
// 成員內(nèi)部類,類比對(duì)象的成員變量
private class InnerClass {
int innerPrice;
public InnerClass(int innerPrice) {
System.out.println("成員內(nèi)部類~類比對(duì)象的成員變量");
this.innerPrice = innerPrice;
}
public void print() {
helloInnerClass();
System.out.println("出售:" + name + " 單價(jià):" + innerPrice);
}
}
public void helloInnerClass() {
System.out.println("我是外部類的helloInnerClass方法,內(nèi)部類你可以調(diào)用我");
}
public static void main(String[] args) {
OuterClass sample = new OuterClass("香蕉");
InnerClass inner = sample.new InnerClass(20);
inner.print();
}
}
運(yùn)行結(jié)果:
成員內(nèi)部類~類比對(duì)象的成員變量
我是外部類,內(nèi)部類你可以調(diào)用我
出售:香蕉 單價(jià):20
2.2 局部內(nèi)部類
局部內(nèi)部類和局部變量一樣,都是在方法內(nèi)定義的,其有效范圍只在方法內(nèi)有效。
一般格式:
public class OuterClass { //外部類
public void print(){ // print方法
class InnerClass { //局部內(nèi)部類
}
}
}
實(shí)例:
局部內(nèi)部類可以無條件的使用外部類的一切靜態(tài)變量、成員變量、靜態(tài)方法、成員方法,用于內(nèi)部類和外部類通信。
public class OuterClass {
private String name;
public OuterClass(String name) {
this.name = name;
}
public void helloInnerClass() {
System.out.println("我是外部類的helloInnerClass方法,內(nèi)部類你可以調(diào)用我");
}
public void print(int price) {
// 局部內(nèi)部類,類比方法內(nèi)的局部變量
class InnerClass {
int innerPrice;
public InnerClass(int innerPrice) {
System.out.println("局部內(nèi)部類~類比方法內(nèi)的局部變量");
this.innerPrice = innerPrice;
}
public void sell() {
helloInnerClass();
System.out.println("出售:" + name + " 單價(jià):" + innerPrice);
}
}
InnerClass apple = new InnerClass(price);
apple.sell();
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass("蘋果");
outerClass.print(10);
}
}
運(yùn)行結(jié)果:
局部內(nèi)部類~類比方法內(nèi)的局部變量
我是外部類的helloInnerClass方法,內(nèi)部類你可以調(diào)用我
出售:蘋果 單價(jià):10
2.3 靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類和靜態(tài)變量類似,它都是使用static關(guān)鍵字修飾。
靜態(tài)內(nèi)部類可以擁有private訪問權(quán)限、protected訪問權(quán)限、public訪問權(quán)限及包訪問權(quán)限。
一般格式:
public class OuterClass { //外部類
class InnerClass { //靜態(tài)內(nèi)部類
}
}
實(shí)例:
靜態(tài)內(nèi)部類只能使用外部類的一切成員變量、成員方法,用于內(nèi)部類和外部類通信。
public class OuterClass {
private static String name="靜態(tài)內(nèi)部類";
// 靜態(tài)內(nèi)部類,類比類的靜態(tài)變量
private static class InnerClass {
public void print() {
System.out.println(name+"~類比類的的靜態(tài)變量");
}
}
public static void main(String[] args) {
OuterClass.InnerClass sample = new OuterClass.InnerClass();
sample.print();
}
}
運(yùn)行結(jié)果:
靜態(tài)內(nèi)部類~類比類的的靜態(tài)變量
2.4 匿名內(nèi)部類
匿名內(nèi)部類就是沒有名字的內(nèi)部類,其名稱由Java編譯器給出,一般是形如:“外部類名稱+$+匿名類順序”,沒有名稱也就是其他地方就不能引用。其必須要實(shí)現(xiàn)一個(gè)接口或者繼承一個(gè)父類,主要是用來簡化代碼,常常用于Swing程序設(shè)計(jì)中的事件監(jiān)聽處理。
一般格式:
public class OuterClass { //外部類
public void print(){ // print方法
new InnerClass(){
...
};
}
}
實(shí)例:
匿名內(nèi)部類只能使用外部類的一切成員變量、成員方法,用于內(nèi)部類和外部類通信。
定義一個(gè)接口
public interface InnerClass {
// 接口方法默認(rèn)public
void print();
}
定義一個(gè)外部類
public class OuterClass {
public static void print(InnerClass innerClass) {
innerClass.print();
}
public static void main(String[] args) {
OuterClass.print(new InnerClass() {
@Override
public void print() {
System.out.println("匿名內(nèi)部類~由于沒有引用,每次新創(chuàng)建的,在Minor GC時(shí)被清除");
}
});
}
}
運(yùn)行結(jié)果:
匿名內(nèi)部類~由于沒有引用,每次新創(chuàng)建的,在Minor GC時(shí)被清除
匿名內(nèi)部類的名稱:外部類名稱+$+匿名類順序

使用JDK8提供的lambda表示替換匿名內(nèi)部類
public class OuterClass {
public static void print(InnerClass innerClass) {
innerClass.print();
}
public static void main(String[] args) {
OuterClass.print(() -> System.out.println("匿名內(nèi)部類~由于沒有引用,每次新創(chuàng)建的,在Minor GC時(shí)被清除"));
}
}
匿名內(nèi)部類在Swing中的實(shí)例:
import javax.swing.*;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class test extends JFrame{
JPasswordField passwordField;
JTextField textField;
test(){
super();
setTitle("QQ");
setBounds(100, 100, 380, 280);
getContentPane().setLayout(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
textField = new JTextField("密碼");
textField.setBounds(100, 155, 120, 21);
getContentPane().add(textField);
// new MouseAdapter()使用匿名內(nèi)部類
textField.addMouseListener(new MouseAdapter(){
@Override
public void mouseClicked(MouseEvent e) {
getContentPane().remove(textField);
passwordField = new JPasswordField();
passwordField.setBounds(100, 155, 120, 21);
getContentPane().add(passwordField);
}
});
}
public static void main(String[] args) {
new test().setVisible(true);;
}
}
在給textField添加鼠標(biāo)監(jiān)聽事件的時(shí)候,使用了new MouseAdapter(){}匿名內(nèi)部類作為方法的參數(shù)。
3. 深入理解內(nèi)部類
1. 為什么成員內(nèi)部類可以無條件訪問外部類的成員和類屬性?
成員內(nèi)部類可以無條件訪問外部類的成員變量、static變量、成員函數(shù)和static函數(shù)。
public class Outter {
private static int b = 2;
private int a = 1;
protected class Inner {
public Inner() {
System.out.println(a);//成員變量
System.out.println(b);//static變量
print();//成員函數(shù)
staticPrint();//static函數(shù)
}
}
private void print() {
}
private static void staticPrint() {
}
}
我們通過反編譯字節(jié)碼文件看看究竟是如何實(shí)現(xiàn)的。在編譯時(shí),會(huì)將內(nèi)部類單獨(dú)編譯成一個(gè)字節(jié)碼文件。

Outter.class是外部類的字節(jié)碼文件,Outter$Inner.class才是成員內(nèi)部類的字節(jié)碼文件。
反編譯Outter$Inner.class文件
javap -v Outter$Inner.class
得到一個(gè)關(guān)鍵信息
final cn.lastwhisper.javabasic.InnerClass.member.Outter this$0;
這是一個(gè)指向外部類對(duì)象的指針。也就是說編譯器會(huì)默認(rèn)為成員內(nèi)部類添加了一個(gè)指向外部類對(duì)象的引用,那么這個(gè)引用是如何賦初值的呢?下面接著看內(nèi)部類的構(gòu)造器:
public cn.lastwhisper.javabasic.InnerClass.member.Outter$Inner(cn.lastwhisper.javabasic.InnerClass.member.Outter);
從這里可以看出,雖然我們?cè)诙x的內(nèi)部類的構(gòu)造器是無參構(gòu)造器,編譯器還是會(huì)默認(rèn)添加一個(gè)參數(shù),該參數(shù)的類型為指向外部類對(duì)象的一個(gè)引用,所以成員內(nèi)部類中的Outter this&0 指針便指向了外部類對(duì)象,因此可以在成員內(nèi)部類中隨意訪問外部類的成員。從這里也間接說明了成員內(nèi)部類是依賴于外部類的,如果沒有創(chuàng)建外部類的對(duì)象,則無法對(duì)Outter this&0引用進(jìn)行初始化賦值,也就無法創(chuàng)建成員內(nèi)部類的對(duì)象了。
2. 為什么靜態(tài)內(nèi)部類只能訪問外部類的成員屬性?
靜態(tài)內(nèi)部類只能訪問外部類的static變量和static函數(shù)。
public class Outter {
private static int b = 2;
private int a = 1;
protected static class Inner {
public Inner() {
//System.out.println(a);//成員變量 會(huì)報(bào)錯(cuò)“Non-static field 'a' cannot be referenced from a static context”
System.out.println(b);//static變量
//print();//成員函數(shù) 會(huì)報(bào)錯(cuò)“Non-static field 'a' cannot be referenced from a static context”
staticPrint();//static函數(shù)
}
}
private void print() {
}
private static void staticPrint() {
}
}
如果靜態(tài)內(nèi)部類使用外部類的成員變量,就會(huì)報(bào)錯(cuò)“Non-static field 'a' cannot be referenced from a static context”,從字面意思很好理解,非static字段不能被static上下文所引用。靜態(tài)內(nèi)部類使用外部類的成員函數(shù)情況也類似。
類比著上一個(gè)問題,通過反編譯字節(jié)碼文件,發(fā)現(xiàn)編譯器在編譯時(shí)并沒有添加外部類的引用,所以靜態(tài)內(nèi)部類也無法使用外部類的成員變量和成員函數(shù)。
public cn.lastwhisper.javabasic.InnerClass.Static.Outter$Inner();
3. 為什么匿名內(nèi)部類只能訪問final修飾的局部變量?
在JDK8以前匿名內(nèi)部類只能訪問final修飾的局部變量,在JDK8以后匿名內(nèi)部類可以訪問非final修飾的局部變量
想必這個(gè)問題也曾經(jīng)困擾過很多人,在討論這個(gè)問題之前,先看下面這段代碼:
public class FinalTest {
public void test(final int b) {
final int a = 100;
new Thread() {
public void run() {
System.out.println(a);
System.out.println(b);
}
}.start();
}
}
這段代碼會(huì)被編譯成兩個(gè)class文件:FinalTest$1.class和FinalTest.class

默認(rèn)情況下,編譯器會(huì)為匿名內(nèi)部類起名為“外部類名稱+$+匿名類順序”
即test方法里面的匿名內(nèi)部類為:FinalTest$1.class
上段代碼中,如果把變量a和b前面的任一個(gè)final去掉,這段代碼都編譯不過。我們先考慮這樣一個(gè)問題:
當(dāng)test方法執(zhí)行完畢之后,局部變量a的生命周期就結(jié)束了,而此時(shí)Thread對(duì)象的生命周期很可能還沒有結(jié)束,那么在Thread的run方法中繼續(xù)訪問test方法的局部變量a就變成不可能了,但是又要實(shí)現(xiàn)這樣的效果,怎么辦呢?Java采用了 復(fù)制 的手段來解決這個(gè)問題。將這段代碼的字節(jié)碼反編譯可以得到下面的內(nèi)容:

得到的信息很多,我們分成兩個(gè)部分。
第一部分run方法

我們看到在run方法中有一條指令:
bipush 100
這條指令表示將操作數(shù)100壓棧,表示使用的是一個(gè)本地局部變量。這個(gè)過程是在編譯期間由編譯器默認(rèn)進(jìn)行,如果這個(gè)變量的值在編譯期間可以確定,則編譯器默認(rèn)會(huì)在匿名內(nèi)部類的常量池中添加一個(gè)內(nèi)容相等的字面量或者直接將相應(yīng)的字節(jié)碼嵌入到執(zhí)行字節(jié)碼中。
這樣一來,匿名內(nèi)部類方法中引用的變量其實(shí)并不是外部類方法中的局部變量,而是引用編譯器在匿名內(nèi)部類的常量池中添加的一個(gè)內(nèi)容相等的字面量。即匿名內(nèi)部類run方法中使用的a并不是test方法中的a,而是FinalTest$1常量池中的a=100。
第二部分匿名內(nèi)部類的構(gòu)造函數(shù)

我們看到匿名內(nèi)部類FinalTest$1的構(gòu)造器含有兩個(gè)參數(shù),一個(gè)是指向外部類對(duì)象的引用,一個(gè)是int型變量,很顯然,這里是將變量test方法中的形參b以參數(shù)的形式傳進(jìn)來對(duì)匿名內(nèi)部類中的拷貝(變量b的復(fù)制)進(jìn)行賦值初始化。
也就說如果局部變量的值在編譯期間就可以確定,則直接在匿名內(nèi)部里面創(chuàng)建一個(gè)拷貝。如果局部變量的值無法在編譯期間確定,則通過構(gòu)造器傳參的方式來對(duì)拷貝進(jìn)行初始化賦值。
這樣一來就解決了前面所說的生命周期不一致的問題。但是新的問題又來了,既然在run方法中訪問的變量a和test方法中的變量a不是同一個(gè)變量,當(dāng)在run方法中改變變量a的值的話,會(huì)出現(xiàn)什么情況?
對(duì),會(huì)造成數(shù)據(jù)不一致性,這樣就達(dá)不到原本的意圖和要求。為了解決這個(gè)問題,java編譯器就限定必須將變量a限制為final變量,不允許對(duì)變量a進(jìn)行更改(對(duì)于引用類型的變量,是不允許指向新的對(duì)象),這樣數(shù)據(jù)不一致性的問題就得以解決了。
至此我們可以回答“為什么匿名內(nèi)部類只能訪問final修飾的局部變量?”了。
1. Java為了避免數(shù)據(jù)不一致性的問題,做出了匿名內(nèi)部類只可以訪問final的局部變量的限制。
2. 補(bǔ)充:Java為了局部變量與匿名內(nèi)部類生命周期不一致的問題,將匿名內(nèi)部類使用到的外部類方法局部變量復(fù)制到自己的常量池中一份,操作時(shí)只使用自己常量池中的數(shù)據(jù)。
4. 為什么需要內(nèi)部類
至此我們已經(jīng)看到了許多描述內(nèi)部類的語法和語義,但是這并不能回答“為什么需要內(nèi)部類” 這個(gè)問題。
一般說來,內(nèi)部類繼承自某個(gè)類或?qū)崿F(xiàn)某個(gè)接口,內(nèi)部類的代碼操作創(chuàng)建它的外圍類的對(duì)象。所以可以認(rèn)為內(nèi)部類提供了某種進(jìn)入其外圍類的窗口。
內(nèi)部類必須要回答的一個(gè)問題是:如果只是需要一個(gè)對(duì)接口的引用,為什么不通過外圍類實(shí)現(xiàn)那個(gè)接口呢?答案是:“如果這能滿足需求,那么就應(yīng)該這樣做?!?那么內(nèi)部類實(shí)現(xiàn)一個(gè)接口與外圍類實(shí)現(xiàn)這個(gè)接口有什么區(qū)別呢?答案是:后者不是總能享用到接口帶來的方便, 有時(shí)需要用到接口的實(shí)現(xiàn)。所以,使用內(nèi)部類最吸引人的原因是 :
每個(gè)內(nèi)部類都能獨(dú)立地繼承自一個(gè)(接口的)實(shí)現(xiàn),所以無論外圍類是否已經(jīng)繼承了某個(gè)(接口的)實(shí)現(xiàn),對(duì)于內(nèi)部類都沒有影響。
內(nèi)部類使得多重繼承的解決方案變得完整。接口解決了部分問題,而內(nèi)部類有效地實(shí)現(xiàn)了 “多重繼承” 。 也就是說,內(nèi)部類允許繼承多個(gè)非接口類型(類或抽象類)。
實(shí)例:
使用一個(gè)類繼承兩個(gè)抽象類,模擬“多繼承問題”
abstract class MyClass1 { }
abstract class MyClass2 { }
/**
* 成員內(nèi)部類實(shí)例
* @author lastwhisper
*/
public class OuterClass extends MyClass1 {
private String name;
public OuterClass(String name) {
this.name = name;
}
// 成員內(nèi)部類,類比對(duì)象的成員變量
class InnerClass extends MyClass2 {
int innerPrice;
public InnerClass(int innerPrice) {
System.out.println("成員內(nèi)部類~類比對(duì)象的成員變量");
this.innerPrice = innerPrice;
}
public void print() {
helloInnerClass();
System.out.println("出售:" + name + " 單價(jià):" + innerPrice);
}
}
public void helloInnerClass() {
System.out.println("我是外部類的helloInnerClass方法,內(nèi)部類你可以調(diào)用我");
}
public static void main(String[] args) {
OuterClass sample = new OuterClass("香蕉");
InnerClass inner = sample.new InnerClass(20);
inner.print();
}
}
如果不需要解決“多重繼承” 的問題,那么自然可以用別的方式編碼,而不需要使用內(nèi)部類。但如果使用內(nèi)部類,還可以獲得其他一些特性:
- 內(nèi)部類可以有多個(gè)實(shí)例,每個(gè)實(shí)例都有自己的狀態(tài)信息,并且與其外圍類對(duì)象的信息相互獨(dú)立。
- 在單個(gè)外圍類中,可以讓多個(gè)內(nèi)部類以不同的方式實(shí)現(xiàn)同一個(gè)接口,或繼承同一個(gè)類。
- 創(chuàng)建內(nèi)部類對(duì)象的時(shí)刻并不依賴于外圍類對(duì)象的創(chuàng)建。
- 內(nèi)部類并沒有令人迷惑的“is-a”關(guān)系; 它就是一個(gè)獨(dú)立的實(shí)體 。
5. 總結(jié)
訪問權(quán)限:
- 成員內(nèi)部類和靜態(tài)內(nèi)部類可以擁有private訪問權(quán)限、protected訪問權(quán)限、public訪問權(quán)限及包訪問權(quán)限。
- 如果成員內(nèi)部類Inner用private修飾,則只能在外部類的內(nèi)部訪問,如果用public修飾,則任何地方都能訪問;如果用protected修飾,則只能在同一個(gè)包下或者繼承外部類的情況下訪問;如果是默認(rèn)訪問權(quán)限,則只能在同一個(gè)包下訪問。
- 這一點(diǎn)和外部類有一點(diǎn)不一樣,外部類只能被public和包訪問兩種權(quán)限修飾。我個(gè)人是這么理解的,由于成員內(nèi)部類看起來像是外部類的一個(gè)成員,所以可以像類的成員一樣擁有多種權(quán)限修飾。
資源使用:
- 成員內(nèi)部類可以無條件的使用外部類的一切靜態(tài)變量、成員變量、靜態(tài)方法、成員方法,用于內(nèi)部類和外部類通信。
- 局部內(nèi)部類可以無條件的使用外部類的一切靜態(tài)變量、成員變量、靜態(tài)方法、成員方法,用于內(nèi)部類和外部類通信。
- 靜態(tài)內(nèi)部類只能使用外部類的一切成員變量、成員方法,用于內(nèi)部類和外部類通信。
- 匿名內(nèi)部類只能使用外部類的一切成員變量、成員方法,用于內(nèi)部類和外部類通信。
使用內(nèi)部類的好處:
- 可以解決“多繼承問題”
- 內(nèi)部類可以有多個(gè)實(shí)例,每個(gè)實(shí)例都有自己的狀態(tài)信息,并且與其外圍類對(duì)象的信息相互獨(dú)立。
- 在單個(gè)外圍類中,可以讓多個(gè)內(nèi)部類以不同的方式實(shí)現(xiàn)同一個(gè)接口,或繼承同一個(gè)類。
- 創(chuàng)建內(nèi)部類對(duì)象的時(shí)刻并不依賴于外圍類對(duì)象的創(chuàng)建。
- 內(nèi)部類并沒有令人迷惑的“is-a”關(guān)系; 它就是一個(gè)獨(dú)立的實(shí)體 。
參考
《Java編程思想》
https://www.cnblogs.com/dolphin0520/p/3811445.html
https://www.cnblogs.com/cuipengfei/p/3150542.html#3901831