小白學Java:內部類
目錄
- 小白學Java:內部類
- 內部類的分類
- 成員內部類
- 區域性內部類
- 靜態內部類
- 匿名內部類
- 內部類的繼承
- 內部類有啥用
- 內部類的分類
小白學Java:內部類
內部類是封裝的一種形式,是定義在類或介面中的類。
內部類的分類
成員內部類
即定義的內部類作為外部類的一個普通成員(非static),就像下面這樣:
public class Outer { class Inner{ private String id = "夏天"; public String getId() { return id; } } public Inner returnInner(){ return new Inner(); } public void show(){ Inner in = new Inner(); System.out.println(in.id); } }
我們通過以上一個簡單的示例,可以得出以下幾點:
- Inner類就是內部類,它的定義在Outer類的內部。
- Outer類中的returnInner方法返回一個Inner型別的物件。
- Outer類中的show方法通過我們熟悉的方式建立了Inner示例並訪問了其私有屬性。
可以看到,我們像使用正常類一樣使用內部類,但實際上,內部類有許多奧妙,值得我們去學習。至於內部類的用處,我們暫且不談,先學習它的語法也不遲。我們在另外一個類中再試著建立一下這個Inner物件吧:
class OuterTest{ public static void main(String[] args) { //!false:Inner in = new Inner(); Outer o = new Outer(); o.show(); Outer.Inner in = o.returnInner(); //!false: can't access --System.out.println(in.id); System.out.println(in.getId()); } }
哦呦,有意思了,我們在另一個類OuterTest
中再次測試我們之前定義的內部類,結果出現了非常明顯的變化,我們陷入了沉思:
- 我們不能夠像之前一樣,用
Inner in = new Inner();
建立內部類例項。 - 沒關係,我們可以通過Outer物件的
returnInner
方法,來建立一個例項,成功! - 需要注意的是:我們如果需要一個內部類型別的變數指向這個例項,我們需要明確指明型別為:
Outer.Inner
,即外部類名.內部類名
。 - 好啦,得到的內部類物件,我們試著直接去訪問它的私有屬性!失敗!
- 那就老老實實地通過
getId
方法訪問吧,成功!
說到這,我們大概就能猜測到:內部類的存在可以很好地隱藏一部分具有聯絡程式碼,實現了那句話:我想讓你看到的東西你隨便看,不想讓你看的東西你想看,門都沒有。
連結到外部類
其實我們之前在分析ArrayList
原始碼的時候,曾經接觸過內部類。我們在學習迭代器設計模式的時候,也曾領略過內部類帶了的奧妙之處。下面我通過《Java程式設計思想》上:通過一個內部類實現迭代器模式的簡單案例做相應的分析與學習:
首先呢,定義一個“選擇器”介面:
interface Selector {
boolean end();//判斷是否到達終點
void next();//移到下一個元素
Object current();//訪問當前元素
}
然後,定義一個序列類Sequence:
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 void next() {
if (i < items.length) {
i++;
}
}
@Override
public Object current() {
return items[i];
}
}
//向上轉型為介面,隱藏實現的細節
public Selector selector() {
return new SequenceSelector();
}
}
- 內部類
SequenceSelector
以private修飾,實現了Selector
介面,提供了方法的具體實現。 - 內部類訪問外部類的私有成員
items
,可以得出結論:內部類自動擁有對其外部類所有成員的訪問權。
當內部類是非static時,當外部類物件建立了一個內部類物件時,內部類物件會產生一個指向外部類的物件的引用,所以非static內部類可以看到外部類的一切。
- 外部類
Sequence
的selector
方法返回了一個內部類例項,意思就是用介面型別接收實現類的例項,實現向上轉型,既隱藏了實現細節,又利於擴充套件。
我們看一下具體的測試方法:
public static void main(String[] args) {
Sequence sq = new Sequence(10);
for (int i = 0; i < 10; i++) {
sq.add(Integer.toString(i));
}
//產生我們設計的選擇器
Selector sl = sq.selector();
while (!sl.end()) {
System.out.print(sl.current() + " ");
sl.next();
}
}
- 隱藏實現細節:使用Sequence序列儲存物件時,不需要關心內部迭代的具體實現,用就完事了,這正是內部類配合迭代器設計模式體現的高度隱藏。
- 利於擴充套件:我們如果要設計一個反向迭代,可以在Sequence內部再定義一個內部類,並提供Selector介面的實現細節,及其利於擴充套件,妙啊。
.new和.this
我們稍微修改一下最初的Outer:
public class Outer {
String id = "喬巴";
class Inner{
private String id = "夏天";
public String getId() {
return id;
}
public String getOuterId(){
return Outer.this.id;
}
public Outer returnOuter(){
return Outer.this;
}
}
public static void main(String[] args) {
Outer o = new Outer();
System.out.println(o.new Inner().getId());//夏天
System.out.println(o.new Inner().getOuterId());//喬巴
}
}
- 在內部類Inner體內添加了returnOuter的引用,
return Outer.this;
,即外部類名.this
。 - 我們可以發現,內部類內外具有同名的屬性,我們在內部類中,不加任何修飾的情況下預設呼叫內部類裡的屬性,我們可以通過引用的形式訪問外部類的id屬性,即
Outer.this.id
。
我們來測試一波:
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
System.out.println(oi.getId());//夏天
Outer o = oi.returnOuter();
System.out.println(o.id);//喬巴
}
- 外部類產生內部類物件的方法已經被我們刪除了,這時我們如果想要通過外部類物件建立一個內部類物件:
Outer.Inner oi = new Outer().new Inner();
,即在外部類物件後面用.new 內部類構造器
。
我們對內部類指向外部類物件的引用進行更加深入的理解與體會,我們會發現,上面的程式碼在編譯之後,會產生兩個位元組碼檔案:
Outer$Inner.class
和Outer.class
。我們對Outer$Inner.class
進行反編譯:
確實,內部類在建立的過程中,依靠外部類物件,而且會產生一個指向外部類物件的引用。
區域性內部類
方法作用域內部類
即在方法作用域內建立一個完整的類。
public class Outer {
public TestOuter test(final String s){
class Inner implements TestOuter{
@Override
public void testM() {
//!false: s+="g";
System.out.println(s);
}
}
return new Inner();
}
public static void main(String[] args) {
Outer o = new Outer();
o.test("天喬巴夏").testM();//天喬巴夏
}
}
interface TestOuter{
void testM();
}
需要注意兩點:
- 此時Inner類是test方法的一部分,Outer不能在該方法之外訪問Inner。
- 方法傳入的引數s和方法內本身的區域性變數都需要以final修飾,不能被改變!!!
JDK1.8之後可以不用final顯式修飾傳入引數和區域性變數,但其本身還是相當於final修飾的,不可改變。我們去掉final,進行反編譯:
任意作用域內的內部類
可以將內部類定義在任意的作用域內:
public class Outer {
public void test(final String s,final int value){
final int a = value;
if(value>2){
class Inner{
public void testM() {
//!false: s+="g";
//!false: a+=1;
System.out.println(s+", "+a);
}
}
Inner in = new Inner();
in.testM();
}
//!false:Inner i = new Inner();
}
public static void main(String[] args) {
Outer o = new Outer();
o.test("天喬巴夏",3);
}
}
同樣需要注意的是:
- 內部類定義在if條件程式碼塊中,並不意味著建立該內部類有相應的條件。內部類一開始就會被建立,if條件只決定能不能用裡頭的東西。
- 如上所示,if作用域之外,編譯器就不認識內部類了,因為它藏起來了。
靜態內部類
即用static
修飾的成員內部類,歸屬於類,即它不存在指向外部類的引用。
public class Outer {
static int a = 5;
int b = 6;
static class Inner{
static int value;
public void show(){
//!false System.out.println(b);
System.out.println(a);
}
}
}
class OuterTest {
public static void main(String[] args) {
Outer.Inner oi = new Outer.Inner();
oi.show();
}
}
需要注意的是:
- 靜態內部類也可以定義非靜態的成員屬性和方法。
- 靜態內部類物件的建立不依靠外部類的物件,可以直接通過:
new Outer.Inner()
建立內部類物件。 - 靜態內部類中可以包含靜態屬性和方法,而除了靜態內部類之外,即我們上面所說的所有的內部類內部都不能有(但是可以有靜態常量
static final
修飾)。 - 靜態內部類不能訪問非靜態的外部類成員。
- 最後,我們反編譯驗證一下:
匿名內部類
這個型別的內部類,看著名字就怪怪的,我們先看看一段違反我們認知的程式碼:
public class Outer {
public InterfaceInner inner(){
//建立一個實現InterfaceInner介面的是實現類物件
return new InterfaceInner() {
@Override
public void show() {
System.out.println("Outer.show");
}
};
}
public static void main(String[] args) {
Outer o = new Outer();
o.inner().show();
}
}
interface InterfaceInner{
void show();
}
真的非常奇怪,乍一看,InterfaceInner
是個介面,而Outer類的inner方法怎麼出現了new InterfaceInner()
的字眼呢?介面不是不能建立例項物件的麼?
確實,這就是匿名內部類的一個使用,其實inner方法返回的是實現了介面方法的實現類物件,我們可以看到分號結尾,代表一個完整的表示式,只不過表示式包含著介面實現,有點長罷了。所以上面匿名內部類的語法其實就是下面這種形式的簡化形式:
public class Outer {
class Inner implements InterfaceInner{
@Override
public void show(){
System.out.println("Outer.show");
}
}
public InterfaceInner inner(){
return new Inner();
}
public static void main(String[] args) {
Outer o = new Outer();
o.inner().show();
}
}
interface InterfaceInner{
void show();
}
不僅僅是介面,普通的類也可以被當作“介面”來使用:
public class Outer {
public OuterTest outerTest(int value) {
//引數傳給匿名類的基類構造器
return new OuterTest(value) {
@Override
public int getValue() {
return super.getValue() * 10;
}
};
}
public static void main(String[] args) {
Outer o = new Outer();
System.out.println(o.outerTest(10).getValue());//100
}
}
class OuterTest {
public int value;
OuterTest(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
需要注意的是:
- 匿名類既可以擴充套件類,也可以實現介面,當然抽象類就不再贅述了,普通類都可以,抽象類就更可以了。但不能同時做這兩件事,且每次最多實現一個介面。
- 匿名內部類沒有名字,所以自身沒有構造器。
- 針對類而言,上述匿名內部類的語法就表明:建立一個繼承OuterTest類的子類例項。所以可以在匿名內部類定義中呼叫父類方法與父類構造器。
- 傳入的引數傳遞給構造器,沒有在類中直接使用,可以不用在引數前加final。
內部類的繼承
內部類可以被繼承,但是和我們普通的類繼承有些出處。具體來看一下:
public class Outer {
class Inner{
private int value = 100;
Inner(){
}
Inner(int value){
this.value = value;
}
public void f(){
System.out.println("Inner.f "+value);
}
}
}
class TestOuter extends Outer.Inner{
TestOuter(Outer o){
o.super();
}
TestOuter(Outer o,int value){
o.super(value);
}
public static void main(String[] args) {
Outer o = new Outer();
TestOuter tt = new TestOuter(o);
TestOuter t = new TestOuter(o,10);
tt.f();
t.f();
}
}
我們可以發現的是:
- 一個類繼承內部類的形式:
class A extends Outer.Inner{}
。 - 內部類的構造器必須連結到指向外部類物件的引用上,
o.super();
,即都需要傳入外部類物件作為引數。
內部類有啥用
可以看到的一點就是,內部類內部的實現細節可以被很好地進行封裝。而且Java中存在介面的多實現,雖然一定程度上彌補了Java“不支援多繼承”的特點,但內部類的存在使其更加優秀,可以看看下面這個例子:
//假設A、B是兩個介面
class First implements A{
B makeB(){
return new B() {
};
}
}
這是一個通過匿名內部類實現介面功能的簡單的例子。對於介面而言,我們完全可以通過下面這樣進行,因為Java中一個類可以實現多個介面:
class First implements A,B{
}
但是除了介面之外,像普通的類,像抽象類,都可以定義獨立的內部類去單獨繼承並實現,使用內部類使“多重繼承”更加完善。
由於後面的許多內容還沒有涉及到,學習到,所以總結的比較淺顯,並沒有做特別深入,特別真實的場景模擬,之後有時間會再做系統性的總結。如果有敘述錯誤的地方,還望評論區批評指標,共同進步。
參考:
《Java 程式設計思想》
https://stackoverflow.com/questions/70324/java-inner-class-and-static-nested-class?r=SearchRes