小白學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=SearchResults