「Java基本功」一文讀懂Java內部類的用法和原理
內部類初探
一、什麽是內部類?
內部類是指在一個外部類的內部再定義一個類。內部類作為外部類的一個成員,並且依附於外部類而存在的。內部類可為靜態,可用protected和private修飾(而外部類只能使用public和缺省的包訪問權限)。內部類主要有以下幾類:成員內部類、局部內部類、靜態內部類、匿名內部類
二、內部類的共性
(1)內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class文件,但是前面冠以外部類的類名和$符號 。
(2)內部類不能用普通的方式訪問。
(3)內部類聲明成靜態的,就不能隨便的訪問外部類的成員變量了,此時內部類只能訪問外部類的靜態成員變量 。
(4)外部類不能直接訪問內部類的的成員,但可以通過內部類對象來訪問
內部類是外部類的一個成員,因此內部類可以自由地訪問外部類的成員變量,無論是否是private的。
因為當某個外圍類的對象創建內部類的對象時,此內部類會捕獲一個隱式引用,它引用了實例化該內部對象的外圍類對象。通過這個指針,可以訪問外圍類對象的全部狀態。
通過反編譯內部類的字節碼,分析之後主要是通過以下幾步做到的:
1 編譯器自動為內部類添加一個成員變量, 這個成員變量的類型和外部類的類型相同, 這個成員變量就是指向外部類對象的引用;
2 編譯器自動為內部類的構造方法添加一個參數, 參數的類型是外部類的類型, 在構造方法內部使用這個參數為1中添加的成員變量賦值;
3 在調用內部類的構造函數初始化內部類對象時, 會默認傳入外部類的引用。
二、使用內部類的好處:
靜態內部類的作用:
1 只是為了降低包的深度,方便類的使用,靜態內部類適用於包含類當中,但又不依賴與外在的類。
2 由於Java規定靜態內部類不能用使用外在類的非靜態屬性和方法,所以只是為了方便管理類結構而定義。於是我們在創建靜態內部類的時候,不需要外部類對象的引用。
非靜態內部類的作用:
1 內部類繼承自某個類或實現某個接口,內部類的代碼操作創建其他外圍類的對象。所以你可以認為內部類提供了某種進入其外圍類的窗口。
2 使用內部類最吸引人的原因是:每個內部類都能獨立地繼承自一個(接口的)實現,所以無論外圍類是否已經繼承了某個(接口的)實現,對於內部類都沒有影響
3 如果沒有內部類提供的可以繼承多個具體的或抽象的類的能力,一些設計與編程問題就很難解決。 從這個角度看,內部類使得多重繼承的解決方案變得完整。接口解決了部分問題,而內部類有效地實現了"多重繼承"。
三、 那靜態內部類與普通內部類有什麽區別呢?問得好,區別如下:
(1)靜態內部類不持有外部類的引用 在普通內部類中,我們可以直接訪問外部類的屬性、方法,即使是private類型也可以訪問,這是因為內部類持有一個外部類的引用,可以自由訪問。而靜態內部類,則只可以訪問外部類的靜態方法和靜態屬性(如果是private權限也能訪問,這是由其代碼位置所決定的),其他則不能訪問。
(2)靜態內部類不依賴外部類 普通內部類與外部類之間是相互依賴的關系,內部類實例不能脫離外部類實例,也就是說它們會同生同死,一起聲明,一起被垃圾回收器回收。而靜態內部類是可以獨立存在的,即使外部類消亡了,靜態內部類還是可以存在的。
(3)普通內部類不能聲明static的方法和變量 普通內部類不能聲明static的方法和變量,註意這裏說的是變量,常量(也就是final static修飾的屬性)還是可以的,而靜態內部類形似外部類,沒有任何限制。
==為什麽普通內部類不能有靜態變量呢?==
1 成員內部類 之所以叫做成員 就是說他是類實例的一部分 而不是類的一部分
2 結構上來說 他和你聲明的成員變量是一樣的地位 一個特殊的成員變量 而靜態的變量是類的一部分和實例無關
3 你若聲明一個成員內部類 讓他成為主類的實例一部分 然後又想在內部類聲明和實例無關的靜態的東西 你讓JVM情何以堪啊
4 若想在內部類內聲明靜態字段 就必須將其內部類本身聲明為靜態
非靜態內部類有一個很大的優點:可以自由使用外部類的所有變量和方法
下面的例子大概地介紹了
1 非靜態內部類和靜態內部類的區別。
2 不同訪問權限的內部類的使用。
3 外部類和它的內部類之間的關系
-
//本節討論內部類以及不同訪問權限的控制
-
//內部類只有在使用時才會被加載。
-
//外部類B
-
public class B{
-
int i = 1;
-
int j = 1;
-
static int s = 1;
-
static int ss = 1;
-
A a;
-
AA aa;
-
AAA aaa;
-
//內部類A
-
-
public class A {
-
// static void go () {
-
//
-
// }
-
// static {
-
//
-
// }
-
// static int b = 1;//非靜態內部類不能有靜態成員變量和靜態代碼塊和靜態方法,
-
// 因為內部類在外部類加載時並不會被加載和初始化。
-
//所以不會進行靜態代碼的調用
-
int i = 2;//外部類無法讀取內部類的成員,而內部類可以直接訪問外部類成員
-
-
public void test() {
-
System.out.println(j);
-
j = 2;
-
System.out.println(j);
-
System.out.println(s);//可以訪問類的靜態成員變量
-
}
-
public void test2() {
-
AA aa = new AA();
-
AAA aaa = new AAA();
-
}
-
-
}
-
//靜態內部類S,可以被外部訪問
-
public static class S {
-
int i = 1;//訪問不到非靜態變量。
-
static int s = 0;//可以有靜態變量
-
-
public static void main(String[] args) {
-
System.out.println(s);
-
}
-
@Test
-
public void test () {
-
// System.out.println(j);//報錯,靜態內部類不能讀取外部類的非靜態變量
-
System.out.println(s);
-
System.out.println(ss);
-
s = 2;
-
ss = 2;
-
System.out.println(s);
-
System.out.println(ss);
-
}
-
}
-
-
//內部類AA,其實這裏加protected相當於default
-
//因為外部類要調用內部類只能通過B。並且無法直接繼承AA,所以必須在同包
-
//的類中才能調用到(這裏不考慮靜態內部類),那麽就和default一樣了。
-
protected class AA{
-
int i = 2;//內部類之間不共享變量
-
public void test (){
-
A a = new A();
-
AAA aaa = new AAA();
-
//內部類之間可以互相訪問。
-
}
-
}
-
//包外部依然無法訪問,因為包沒有繼承關系,所以找不到這個類
-
protected static class SS{
-
int i = 2;//內部類之間不共享變量
-
public void test (){
-
-
//內部類之間可以互相訪問。
-
}
-
}
-
//私有內部類A,對外不可見,但對內部類和父類可見
-
private class AAA {
-
int i = 2;//內部類之間不共享變量
-
-
public void test() {
-
A a = new A();
-
AA aa = new AA();
-
//內部類之間可以互相訪問。
-
}
-
}
-
@Test
-
public void test(){
-
A a = new A();
-
a.test();
-
//內部類可以修改外部類的成員變量
-
//打印出 1 2
-
B b = new B();
-
-
}
-
}
-
-
-
//另一個外部類
-
class C {
-
@Test
-
public void test() {
-
//首先,其他類內部類只能通過外部類來獲取其實例。
-
B.S s = new B.S();
-
//靜態內部類可以直接通過B類直接獲取,不需要B的實例,和靜態成員變量類似。
-
//B.A a = new B.A();
-
//當A不是靜態類時這行代碼會報錯。
-
//需要使用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為私有內部類,外部類不可見
-
//當A使用private修飾時,使用B的實例也無法獲取A的實例,這一點和私有變量是一樣的。
-
//所有普通的內部類與類中的一個變量是類似的。靜態內部類則與靜態成員類似。
-
}
-
}
內部類的加載
可能剛才的例子中沒辦法直觀地看到內部類是如何加載的,接下來用例子展示一下內部類加載的過程。
1 內部類是延時加載的,也就是說只會在第一次使用時加載。不使用就不加載,所以可以很好的實現單例模式。
2 不論是靜態內部類還是非靜態內部類都是在第一次使用時才會被加載。
3 對於非靜態內部類是不能出現靜態模塊(包含靜態塊,靜態屬性,靜態方法等)
4 非靜態類的使用需要依賴於外部類的對象,詳見上述對象innerClass 的初始化。
簡單來說,類的加載都是發生在類要被用到的時候。內部類也是一樣
1 普通內部類在第一次用到時加載,並且每次實例化時都會執行內部成員變量的初始化,以及代碼塊和構造方法。
2 靜態內部類也是在第一次用到時被加載。但是當它加載完以後就會將靜態成員變量初始化,運行靜態代碼塊,並且只執行一次。當然,非靜態成員和代碼塊每次實例化時也會執行。
總結一下Java類代碼加載的順序,萬變不離其宗。
規律一、初始化構造時,先父後子;只有在父類所有都構造完後子類才被初始化
規律二、類加載先是靜態、後非靜態、最後是構造函數。
靜態構造塊、靜態類屬性按出現在類定義裏面的先後順序初始化,同理非靜態的也是一樣的,只是靜態的只在加載字節碼時執行一次,不管你new多少次,非靜態會在new多少次就執行多少次
規律三、java中的類只有在被用到的時候才會被加載
規律四、java類只有在類字節碼被加載後才可以被構造成對象實例
成員內部類
在方法中定義的內部類稱為局部內部類。與局部變量類似,局部內部類不能有訪問說明符,因為它不是外圍類的一部分,但是它可以訪問當前代碼塊內的常量,和此外圍類所有的成員。
需要註意的是: 局部內部類只能在定義該內部類的方法內實例化,不可以在此方法外對其實例化。
-
public class 局部內部類 {
-
class A {//局部內部類就是寫在方法裏的類,只在方法執行時加載,一次性使用。
-
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報錯,方法裏不能定義靜態內部類。
-
//因為只有在方法調用時才能進行類加載和初始化。
-
-
}
-
}
-
}
匿名內部類
簡單地說:匿名內部類就是沒有名字的內部類,並且,匿名內部類是局部內部類的一種特殊形式。什麽情況下需要使用匿名內部類?如果滿足下面的一些條件,使用匿名內部類是比較合適的: 只用到類的一個實例。 類在定義後馬上用到。 類非常小(SUN推薦是在4行代碼以下) 給類命名並不會導致你的代碼更容易被理解。 在使用匿名內部類時,要記住以下幾個原則:
1 匿名內部類不能有構造方法。
2 匿名內部類不能定義任何靜態成員、方法和類。
3 匿名內部類不能是public,protected,private,static。
4 只能創建匿名內部類的一個實例。
5 一個匿名內部類一定是在new的後面,用其隱含實現一個接口或實現一個類。
6 因匿名內部類為局部內部類,所以局部內部類的所有限制都對其生效。
一個匿名內部類的例子:
-
public class 匿名內部類 {
-
-
}
-
interface D{
-
void run ();
-
}
-
abstract class E{
-
E (){
-
-
}
-
abstract void work();
-
}
-
class A {
-
-
@Test
-
public void test (int k) {
-
//利用接口寫出一個實現該接口的類的實例。
-
//有且僅有一個實例,這個類無法重用。
-
new Runnable() {
-
@Override
-
public void run() {
-
// k = 1;報錯,當外部方法中的局部變量在內部類使用中必須改為final類型。
-
//因為方外部法中即使改變了這個變量也不會反映到內部類中。
-
//所以對於內部類來講這只是一個常量。
-
System.out.println(100);
-
System.out.println(k);
-
}
-
};
-
new D(){
-
//實現接口的匿名類
-
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
使用的形參為何要為final
參考文件:http://android.blog.51cto.com/268543/384844
我們給匿名內部類傳遞參數的時候,若該形參在內部類中需要被使用,那麽該形參必須要為final。也就是說:當所在的方法的形參需要被內部類裏面使用時,該形參必須為final。
為什麽必須要為final呢?
首先我們知道在內部類編譯成功後,它會產生一個class文件,該class文件與外部類並不是同一class文件,僅僅只保留對外部類的引用。當外部類傳入的參數需要被內部類調用時,從java程序的角度來看是直接被調用:
public class OuterClass {
public void display(final String name,String age){
class InnerClass{
void display(){
System.out.println(name);
}
}
}
}
從上面代碼中看好像name參數應該是被內部類直接調用?其實不然,在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 );
-
}
-
}
所以從上面代碼來看,內部類並不是直接調用方法傳遞的參數,而是利用自身的構造器對傳入的參數進行備份,自己內部方法調用的實際上時自己的屬性而不是外部方法傳遞進來的參數。
直到這裏還沒有解釋為什麽是final
在內部類中的屬性和外部方法的參數兩者從外表上看是同一個東西,但實際上卻不是,所以他們兩者是可以任意變化的,也就是說在內部類中我對屬性的改變並不會影響到外部的形參,而然這從程序員的角度來看這是不可行的。
畢竟站在程序的角度來看這兩個根本就是同一個,如果內部類該變了,而外部方法的形參卻沒有改變這是難以理解和不可接受的,所以為了保持參數的一致性,就規定使用final來避免形參的不改變。
簡單理解就是,拷貝引用,為了避免引用值發生改變,例如被外部類的方法修改等,而導致內部類得到的值不一致,於是用final來讓該引用不可改變。
故如果定義了一個匿名內部類,並且希望它使用一個其外部定義的參數,那麽編譯器會要求該參數引用是final的。
內部類初始化
我們一般都是利用構造器來完成某個實例的初始化工作的,但是匿名內部類是沒有構造器的!那怎麽來初始化匿名內部類呢?使用構造代碼塊!利用構造代碼塊能夠達到為匿名內部類創建一個構造器的效果。
-
public class OutClass {
-
public InnerClass getInnerClass(final int age,final String name){
-
return new InnerClass() {
-
int age_ ;
-
String name_;
-
//構造代碼塊完成初始化工作
-
{
-
if(0 < age && age < 200){
-
age_ = age;
-
name_ = name;
-
}
-
}
-
public String getName() {
-
return name_;
-
}
-
-
public int getAge() {
-
return age_;
-
}
-
};
-
}
內部類的重載
如果你創建了一個內部類,然後繼承其外圍類並重新定義此內部類時,會發生什麽呢?也就是說,內部類可以被重載嗎?這看起來似乎是個很有用的點子,但是“重載”內部類就好像它是外圍類的一個方法,其實並不起什麽作用:
-
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();
-
}
-
}
-
復制代碼
-
輸出結果為:
-
New Egg()
-
Egg.Yolk()
缺省的構造器是編譯器自動生成的,這裏是調用基類的缺省構造器。你可能認為既然創建了BigEgg 的對象,那麽所使用的應該是被“重載”過的Yolk,但你可以從輸出中看到實際情況並不是這樣的。 這個例子說明,當你繼承了某個外圍類的時候,內部類並沒有發生什麽特別神奇的變化。這兩個內部類是完全獨立的兩個實體,各自在自己的命名空間內。
內部類的繼承
因為內部類的構造器要用到其外圍類對象的引用,所以在你繼承一個內部類的時候,事情變得有點復雜。問題在於,那個“秘密的”外圍類對象的引用必須被初始化,而在被繼承的類中並不存在要聯接的缺省對象。要解決這個問題,需使用專門的語法來明確說清它們之間的關聯:
-
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);
-
}
-
}
輸出結果為: this is a constructor in WithInner.Inner this is a constructor in InheritInner
可以看到,InheritInner 只繼承自內部類,而不是外圍類。但是當要生成一個構造器時,缺省的構造器並不算好,而且你不能只是傳遞一個指向外圍類對象的引用。此外,你必須在構造器內使用如下語法: enclosingClassReference.super(); 這樣才提供了必要的引用,然後程序才能編譯通過。
有關匿名內部類實現回調,事件驅動,委托等機制的文章將在下一節講述。
「Java基本功」一文讀懂Java內部類的用法和原理