什么是內(nèi)部類
如果一個類定義在另一個類的內(nèi)部意推,那么這個類就是內(nèi)部類∩后埃可以通過public菊值,protected,private修飾符來控制內(nèi)部類的可見性育灸。同時腻窒,內(nèi)部類也可以被聲明為abstract類型,供其他類繼承和擴展磅崭。
內(nèi)部類是一個編譯時的概念儿子,一旦編譯成功,內(nèi)部類和外部類就是兩個完全不同的類砸喻。例如柔逼,對于一個外部類Outer,在其中定義一個內(nèi)部類Inner恩够,編譯完成后會生成Outer.class
和Outer$Inner.class
兩個class文件卒落。所以內(nèi)部類的成員變量和方法名可以和外部類相同。
內(nèi)部類用法
按照內(nèi)部類的用法蜂桶,可以將內(nèi)部類分為成員內(nèi)部類儡毕,局部內(nèi)部類,匿名內(nèi)部類以及靜態(tài)內(nèi)部類。
成員內(nèi)部類
成員內(nèi)部類就是在外圍類的內(nèi)部直接定義一個類腰湾±资眩可以通過public,protected费坊,private修飾符來控制其可見性倒槐。在創(chuàng)建內(nèi)部類的時候,可以直接創(chuàng)建內(nèi)部類附井,也可以讓內(nèi)部類繼承某個基類或者接口讨越。如下所示。
1. 直接創(chuàng)建內(nèi)部類
public class Parcel1 {
private class Contents {
private String content = "goods";
public String contents() {
return content;
}
}
public class Destination {
private String dest;
Destination(String dest) {
this.dest = dest;
}
public String dest() {
return dest;
}
}
public void ship1(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
public static void ship2(String dest) {
Parcel1 p = new Parcel1();
Contents c = p.new Contents();
Destination d = p.new Destination("dest2");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship1("dest1");
Parcel1.ship2("dest2");
}
}
在上面例子中永毅,Contents
和Destination
都定義在Parcel1
類的內(nèi)部把跨,它們都是public的,所以通過其他類也可以訪問到沼死。通過ship1
方法和ship2
方法着逐,我們可以看到創(chuàng)建內(nèi)部類對象的兩種方式。
-
在外部類的非靜態(tài)方法創(chuàng)建內(nèi)部類對象
這種方式和使用普通類沒有什么區(qū)別意蛀∷时穑可以直接通過
new InnerClass()
的方式來創(chuàng)建內(nèi)部類對象。如ship1
方法所示县钥。 -
在外部類的靜態(tài)方法或者其他類中創(chuàng)建內(nèi)部類對象
在外部類的靜態(tài)方法或者其他類創(chuàng)建內(nèi)部類對象時秀姐,我們必須首先創(chuàng)建外部類對象
outterObj
,然后通過outterObj.new InnerClass()
的方式來創(chuàng)建內(nèi)部類對象魁蒜。如ship2
方法所示囊扳。
2.通過實現(xiàn)接口/繼承基類創(chuàng)建內(nèi)部類
除了直接創(chuàng)建內(nèi)部類,我們還可以讓內(nèi)部類實現(xiàn)某個接口或者繼承某個基類兜看。這樣蔽介,我們在使用內(nèi)部類對象的時候眯杏,可以將其向上轉(zhuǎn)型為對其基類或者接口的引用党窜。這樣能夠方便地隱藏內(nèi)部類實現(xiàn)細節(jié)并屏蔽類型差異且警。如下所示。
public interface IContents {
String contents();
}
public interface IDestination {
String dest();
}
public class Parcel2 {
private class Contents implements IContents{
private String content = "goods";
@Override
public String contents() {
return content;
}
}
private class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
public Destination destination(String dest) {
return new Destination(dest);
}
public Contents contents() {
return new Contents();
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
IContents c = p.contents();
IDestination d = p.destination("dest1");
System.out.println("contents: " + c.contents() + ", destination: " + d.dest());
}
}
這里弧轧,內(nèi)部類Destination
和Contents
分別實現(xiàn)了接口IDestination
和IContents
雪侥,我們在訪問內(nèi)部類對象的時候,可以通過IDestination
和IContents
的引用來訪問內(nèi)部類對象精绎,而不再需要關心內(nèi)部類對象的具體類型速缨。因此,我們可以把Destination
和Contents
設置為private類型代乃。設置為private類型后旬牲,在其他類中無法直接創(chuàng)建內(nèi)部類對象仿粹,所以需要在外部類中新增destination
和content
方法來獲取內(nèi)部類對象。
訪問外對象成員
內(nèi)部類可以無縫地訪問外部類對象的所有成員原茅,因為當創(chuàng)建一個內(nèi)部類對象的時候吭历,內(nèi)部類對象會隱式地持有一個指向外部類對象的引用,通過這個引用來訪問外部類的所以成員變量及方法擂橘。當然晌区,也可以訪問外部類對象本身。關于這點通贞,我們可以直接通過反編譯內(nèi)部類的class文件來驗證朗若。我們首先使用javac
命令編譯前面的Parcel1
,編譯之后昌罩,我們看到目錄下生成了三個class文件捡偏。
Parcel1$1.class
Parcel1$Contents.class
Parcel1$Destination.class
這也驗證了內(nèi)部類是一個編譯時的概念,在編譯完成后峡迷,內(nèi)部類和外部類就是兩個完全獨立的類了。通過IntelliJ IDEA我們可以查看反編譯后的class文件你虹,我們查看下Parcel1$Contents.class
文件绘搞,代碼如下所示。
class Parcel1$Contents {
private String content;
private Parcel1$Contents(Parcel1 var1) {
this.this$0 = var1;
this.content = "goods";
}
public String contents() {
return this.content;
}
}
可以看到傅物,編譯器為我們做了很多工作夯辖,它會自動生成一個內(nèi)部類的構(gòu)造方法,這個方法傳入的是外部類對象的一個引用董饰,在內(nèi)部類中蒿褂,然后在內(nèi)部類中還會一定一個成員變量this$0
來存儲外部類對象的引用。所以卒暂,我們在創(chuàng)建一個內(nèi)部類對象之前啄栓,必須首先要創(chuàng)建外部類對象。
持有外部類對象的引用是內(nèi)部類最有用的一個特性也祠,通過這種方式昙楚,我們可以把內(nèi)部類作為訪問外部類的一個窗口。所有內(nèi)部類(靜態(tài)內(nèi)部類除外)在創(chuàng)建的時候都會持有外部類對象的應用诈嘿,所以在創(chuàng)建內(nèi)部類對象的時候堪旧,我們必須先創(chuàng)建一個外部類對象。而不能直接通過 new InnerClass
的方式來創(chuàng)建內(nèi)部類對象奖亚。例如淳梦,下面通過內(nèi)部類來實現(xiàn)一個迭代器。
public interface Selector {
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) {
items = new Object[size];
}
public void add(Object x) {
if (next<items.length) {
items[next++] = x;
}
}
private class SequenceSelector implements Selector {
private int i=0;
@Override
public boolean end() {
return i==items.length;
}
@Override
public Object current() {
return items[i];
}
@Override
public void next() {
if (i < items.length) {
i++;
}
}
}
public Selector selector() {
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(20);
for (int i=0;i<20;i++) {
sequence.add(i);
}
Selector selector = sequence.selector();
while (!selector.end()) {
System.out.println(selector.current());
selector.next();
}
}
}
如果想要在內(nèi)部類中訪問外部類對象本身昔字,可以通過OutterClass.this
來訪問爆袍。
局部內(nèi)部類
除了可以定義成員內(nèi)部類(也就是在外部類中直接定義內(nèi)部類)外,我們還可以在方法或者局部作用域內(nèi)定義內(nèi)部類。
1.在方法內(nèi)部定義內(nèi)部類
public class Parcel3 {
public IDestination destination(String dest) {
class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
return new Destination(dest);
}
public IContents contents() {
class Contents implements IContents{
private String content = "goods";
@Override
public String contents() {
return content;
}
}
return new Contents();
}
public static void main(String[] args) {
Parcel3 p = new Parcel3();
IContents c = p.contents();
IDestination d = p.destination("dest1");
System.out.println("contents: " + c.contents() + ", destination: " + d.dest());
}
}
這里的Destination
和Contents
類分別定義在了方法destination
和 content
內(nèi)部螃宙,所以他們只能在所定義的方法內(nèi)部訪問蛮瞄。方法之外的其他地方無法訪問該內(nèi)部類。
2.在代碼塊定義內(nèi)部類
除了可以在方法中定義內(nèi)部類外谆扎,我們也可以直接在代碼塊中定義內(nèi)部類挂捅,如下所示。
public class Parcel4 {
public IDestination destination(String dest) {
class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
return new Destination(dest);
}
public IContents contents(boolean hasContent) {
if (hasContent) {
class Contents implements IContents {
private String content = "goods";
@Override
public String contents() {
return content;
}
}
return new Contents();
}
return null;
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
IContents c = p.contents(true);
IDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
可以看到Contents
的定義放在了if
語句內(nèi)部堂湖,這就意味著闲先,在if
語句外,我們無法使用該內(nèi)部類无蜂。
雖然Contents
定義在了if
代碼塊內(nèi)部伺糠,但是這并不代表該類的創(chuàng)建是有條件的,因為內(nèi)部類是在編譯器生成的斥季,編譯完成后训桶,該類和外部類沒有區(qū)別,只是作用域不同罷了酣倾。
局部內(nèi)部類除了作用域不同外舵揭,具有和成員內(nèi)部類一樣的特性,局部內(nèi)部類也可以實現(xiàn)某個接口或者繼承某個基類躁锡,也可以直接訪問外部類對象中的所有成員和方法午绳。但是局部內(nèi)部類不能有訪問說明符,因為他只能在方法或者代碼塊內(nèi)部訪問映之。
訪問本地變量
當局部內(nèi)部類引用本地變量時拦焚,本地變量必須是final類型或者實際上是final類型「苁洌看下面的例子赎败。
public class Parcel7 {
public IDestination destination(String localDest) {
class Destination implements IDestination{
@Override
public String dest() {
return localDest;
}
}
return new Destination();
}
public IContents contents(boolean hasContent) {
if (hasContent) {
class Contents implements IContents {
private String content = "goods";
@Override
public String contents() {
return content;
}
}
return new Contents();
}
return null;
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
IContents c = p.contents(true);
IDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
在上面的例子中,在Destination
是一個局部內(nèi)部類蠢甲,并且在Destination
的dest
方法中直接引用了外部類destination
方法中傳入的localDest
參數(shù)螟够。這Java8之前,這段代碼在編譯時會直接報錯峡钓,報錯提示如下所示(用Java7編譯)妓笙。
java: 從內(nèi)部類中訪問本地變量localDest; 需要被聲明為最終類型
也就是說localDest
必須聲明為final類型,為什么要聲明為final類型呢能岩?原因是本地變量的生命周期在方法執(zhí)行完成的時候就結(jié)束了寞宫,而內(nèi)部類的生命周期大多數(shù)啊情況下會比本地變量的生命周期要長。如果本地變量被回收拉鹃,那么在內(nèi)部類執(zhí)行到訪問本地變量的代碼時就會導致空指針問題辈赋。所以鲫忍,為了解決這個問題,在創(chuàng)建內(nèi)部類的時候钥屈,會在內(nèi)部類中以成員變量的形式對本地變量做一個備份悟民,這個是編譯器自動幫我們完成的。關于這點篷就,我們可以通過反編譯Parcel7
類來找到答案射亏。
我們通過javac
命令編譯Parcel7
類,編譯完成后會產(chǎn)生如下三個.class文件竭业。
Parcel7$1Contents.class
Parcel7$1Destination.class
Parcel7.class
然后使用javap
命令對Parcel7$1Destination.class
文件進行反編譯智润。
javap -private Parcel7$1Destination.class
反編譯結(jié)果如下所示。
class Parcel7$1Destination implements IDestination {
final java.lang.String val$localDest;
final Parcel7 this$0;
Parcel7$1Destination();
public java.lang.String dest();
}
可以看到未辆,在內(nèi)部類中多了一個val$localDest
的成員變量窟绷,這個成員變量就是內(nèi)部類Destination
中用來對Parcel7.destination
方法中傳入的localDest
做備份的。同時也可以看到val$localDest
是final類型的咐柜。這意味著兼蜈,如果我們想在內(nèi)部類內(nèi)對localDest
做修改,也是不允許的拙友。那為什么本地變量要定義成final類型呢饭尝,先來看看final類型的特點。如果一個變量被聲明為final類型献宫,它有如下特點。
- 當final修飾基本數(shù)據(jù)類型的變量時实撒,這個變量在初始化時就會被賦值姊途,并且初始化完成之后其值就不能再被改變了。
- 當final修飾引用類型的變量時知态,這個變量在初始化時就會被賦值捷兰,并且初始化完成之后其值就不能再被改變,但是該變量指向?qū)ο蟮亩褍?nèi)存的值是可以改變的负敏。
如果當內(nèi)部類引用本地變量的時候贡茅,不把本地變量聲明為final類型,在內(nèi)部類或者外部類的方法中對本地變量做修改后其做,很容易造成理解上的混淆和代碼行為的錯亂顶考,所以為了避免混淆和錯亂的問題,在內(nèi)部類引用本地變量的時候妖泄,需要把本地變量聲明為final類型驹沿。這樣內(nèi)部類和方法中的本地變量值就可以始終保持一致。
在Java8之后蹈胡,不再需要強制將本地變量聲明為final類型渊季,所以上面的代碼在Java8下編譯的時候朋蔫,不會報錯也可以正常執(zhí)行。但是却汉,如果我們想嘗試改變本地變量的值驯妄,編譯器還是會報錯。例如合砂,我們將destination
做如下修改青扔。
public IDestination destination(String localDest) {
class Destination implements IDestination{
@Override
public String dest() {
return localDest;
}
}
localDest = "dest2";
System.out.println(localDest);
return new Destination();
}
我們在方法內(nèi)重新對localDest
變量進行賦值,然后編譯既穆,編譯器報錯如下所示赎懦。
java: 從內(nèi)部類引用的本地變量必須是最終變量或?qū)嶋H上的最終變量
所以,Java8雖然不再需要將本地變量強制聲明為final類型幻工,但是實際上還是要求本地變量時final類型励两。關于這點,我們可以查看Parcel7.class
文件囊颅。
public class Parcel7 {
public Parcel7() {
}
public IDestination destination(final String var1) {
class Destination implements IDestination {
Destination() {
}
public String dest() {
return var1;
}
}
return new Destination();
}
……
}
可以看到当悔,如果在內(nèi)部類引用了本地變量,編譯器會自動將該本地變量轉(zhuǎn)變?yōu)閒inal類型踢代。
匿名內(nèi)部類
匿名內(nèi)部類本質(zhì)也屬于一種局部內(nèi)部類盲憎,它主要是用于某個類只需要創(chuàng)建一個對象,而不需要再使用的場景胳挎。饼疙。如下所示。
public class Parcel5 {
public IDestination destination(String dest) {
class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
return new Destination(dest);
}
public IContents contents() {
return new IContents() {
private String content = "goods";
@Override
public String contents() {
return content;
}
};
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
IContents c = p.contents();
IDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
這里在創(chuàng)建Contents對象的時候慕爬,就使用到了匿名內(nèi)部類窑眯。之所以叫匿名內(nèi)部類,就是這個類沒有名字医窿,而是直接通過實現(xiàn)某個接口或者基類來創(chuàng)建一個對象磅甩,匿名內(nèi)部類的語法格式如下所示。
new SuperType(construction parameters){
//inner class methods and data
}
其中SuperType可以是像IContents
這樣的接口姥卢,也可以是一個類卷要。在Java中定義回調(diào)函數(shù)的時候,經(jīng)常會用到匿名內(nèi)部類独榴。例如僧叉,在創(chuàng)建一個子線程的時候,我們通常會給Thread的構(gòu)造方法傳遞一個實現(xiàn)了Runnable
接口的匿名內(nèi)部類對象棺榔。
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Thread");
}
}).start();
上面的contents
方法通過實現(xiàn)IContents
接口創(chuàng)建了匿名內(nèi)部類對象彪标,默認使用了匿名內(nèi)部類的無參構(gòu)造器。在創(chuàng)建匿名內(nèi)部類對象的時候掷豺,也可以使用有參數(shù)的構(gòu)造器上捞烟,不過因為匿名內(nèi)部類是沒有名字的薄声,所以肯定不能在匿名內(nèi)部類里邊定義帶參數(shù)的構(gòu)造器。所以题画,如果想在創(chuàng)建匿名內(nèi)部類的時候默辨,使用帶參數(shù)的構(gòu)造器,必須要匿名內(nèi)部類的基類含有帶參數(shù)的構(gòu)造器苍息。如下所示缩幸。
public class BaseDestination {
private String dest = "";
public BaseDestination (String dest) {
this.dest = dest;
}
public String dest() {
return dest;
}
}
public class Parcel6 {
public BaseDestination destination(String dest) {
return new BaseDestination(dest) {
@Override
public String dest() {
return super.dest();
}
};
}
public IContents contents() {
return new IContents() {
private String content = "goods";
@Override
public String contents() {
return content;
}
};
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
IContents c = p.contents();
BaseDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
上面的例子中,我們通過繼承BaseDestination
來創(chuàng)建了一個匿名內(nèi)部類對象竞思,在創(chuàng)建的時候表谊,我們使用了匿名內(nèi)部類基類的帶參構(gòu)造器。
由于匿名內(nèi)部類本質(zhì)上也是一種局部內(nèi)部類盖喷,它在訪問本地變量時也需要將本地變量聲明為final類型或者本地變量實際上符合final類型的特點爆办。
靜態(tài)內(nèi)部類
前面學習的成員內(nèi)部類,局部內(nèi)部類和匿名內(nèi)部類對象在創(chuàng)建的時候都會隱式地持有外部類對象的引用课梳,如果不想內(nèi)部類和外部類對象之間有聯(lián)系距辆,可以將內(nèi)部類聲明為static類型,這就是所謂的靜態(tài)內(nèi)部類暮刃。靜態(tài)內(nèi)部類對象不會持有外部類對象的引用跨算,所以在創(chuàng)建靜態(tài)內(nèi)部類對象的時候,并不需要事先創(chuàng)建外部類對象椭懊。由于沒有持有外部類對象的引用诸蚕,所以,在靜態(tài)內(nèi)部類中只能訪問外部類的靜態(tài)成員變量和靜態(tài)方法氧猬,而無法訪問非靜態(tài)成員變量和非靜態(tài)方法背犯。
靜態(tài)內(nèi)部類由于切斷了和外部類的聯(lián)系,所以它和外部類更加獨立狂窑。例如,我們在設計模式中經(jīng)常用到的構(gòu)建者模式桑腮,就是靜態(tài)內(nèi)部類的一種典型應用泉哈。如下所示。
public class Student {
private String name = "";
private int age;
private String address = "";
private Student(Builder builder) {
name = builder.name;
age = builder.age;
address = builder.address;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getAddress() {
return address;
}
public static Builder newBuilder(){
return new Builder();
}
public static class Builder {
private String name = "";
private int age;
private String address = "";
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setAge(int age) {
this.age = age;
return this;
}
public Builder setAddress(String address) {
this.address = address;
return this;
}
public Student build() {
return new Student(this);
}
}
}
public class BuilderTest {
public static void main(String[] args) {
Student.Builder builder = Student.newBuilder();
Student student = builder.setName("xxx")
.setAge(18)
.setAddress("China")
.build();
System.out.println("name: " + student.getName() + ", age: " + student.getAge() + ", address: " +student.getAddress() );
}
}
為什么需要內(nèi)部類
那么Java為什么要使用內(nèi)部類呢破讨?通過內(nèi)部類的特點和用法丛晦,我們可以總結(jié)出以下原因。
- 內(nèi)部類定義在一個類的內(nèi)部提陶,通過內(nèi)部類我們可以訪問外部類的變量和方法烫沙,對外部類對象進行操作,作為訪問外部類的一個窗口隙笆。
- 內(nèi)部類可以對其他類隱藏可見性和實現(xiàn)細節(jié)锌蓄,保證內(nèi)部類的擴展性和隔離性升筏。
- 內(nèi)部類是實現(xiàn)多繼承的一種方式,每個內(nèi)部類都能獨立的繼承一個類或者實現(xiàn)若干個接口瘸爽,而不受外圍類和其他內(nèi)部類的限制您访,所以是實現(xiàn)多繼承的一種方案。
- 內(nèi)部類有一些有用的特性剪决,例如灵汪,當想要定義一個回調(diào)方法的時候,使用匿名內(nèi)部類能提供很大的便捷性柑潦。