1. 程式人生 > >java內部類(6.7)

java內部類(6.7)

參考《瘋狂java講義》
定義在其它類內部的類就被稱為內部類,包含內部類的類就叫作外部類。內部類主要有如下作用:

  1. 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中其它類訪問該類。
  2. 內部類成員可以直接訪問外部類的是有資料,因為內部類也是外部類的成員,同一個類的成員之間可以互相訪問。但外部類不能訪問內部類的實現細節。例如內部類的成員變數。
  3. 匿名內部類適合用於建立那些僅需要使用一次的類(匿名內部類不是類成員)。
    內部類與普通類的語法區別:
  4. 內部類比外部類可以多使用3個修飾符:private,protected,static——外部類不能使用這3個修飾符
    。因為外部類的上一級程式單元是包,所以它只有兩個作用域;同一個包內和任意位置。因此只需要兩種訪問控制權限;包訪問許可權(預設)和public訪問許可權。
  5. 非靜態內部類不能擁有靜態成員。

1. 非靜態內部類

public class Cow
{
   private double weight;
   // 外部類的兩個過載構造器   
   public Cow(){}
   public Cow(double weight)
   {
      this.weight = weight;
   }
   //定義一個非靜態內部類   
   private class CowLeg
   {
      // /非靜態內部類的兩個例項變數
      private double length;
      private String color;
      // 非靜態內部類的兩個過載構造器
      public CowLeg(){}
      public CowLeg(double length , String color)
      {
         this.length = length;
         this.color = color;
      }
      // 下面省略length和color的setter,getter方法       
      // 非靜態內部類的例項方法
      public void info()
      {
         System.out.println("當前牛顏色是"
            + color + ", 搞" + length);
         //直接訪問外部類的private修飾的成員變數
         System.out.println("本牛腿所在奶牛重" + weight); 
      }
   }
   public void test()
   {
      CowLeg cl = new CowLeg(1.12 , "黑白相間");
      cl.info();
   }
   public static void main(String[] args)
   {
      Cow cow = new Cow(378.9);
      cow.test();
   }
}

編譯上面程式,看到在檔案所在路徑生成了兩個class檔案,一個Crow.class,一個是Cow$ CrowLeg.class前者是外部類的class檔案,後者是內部類的class檔案。即成員內部類(包括靜態的和非靜態的)的class檔名總是這種形式:OuterClass$InnerClass.class
在非靜態內部類裡能直接訪問外部類的私有例項成員成員,這是因為在非靜態內部類的物件裡,儲存了一個它所寄生的外部類物件的引用(當呼叫非靜態內部類的例項方法時,必須有一個非靜態內部類的物件,非靜態內部類例項必須寄生在外部類例項裡)
在非靜態內部類的方法裡訪問變數時,搜尋變數名的順序是1 方法內,2 非靜態內部類裡 3 外部類裡。因此當需要訪問被覆蓋的變數時,可用this.和外部類名.this.作為限定來區分。
如果外部類要訪問非靜態內部類對的成員,只能通過建立內部類物件來訪問。
非靜態內部類物件必須寄生在外部類物件裡,而靜態內部類物件則不一定要有非靜態內部類寄生。簡單的說,如果存才一個非靜態內部類物件,則一定存才外部類物件。但外部類物件存在時,不一定存在非靜態內部類物件。因此外部類物件訪問非靜態內部類成員時,可能非靜態內部類物件根本不存在,而非靜態內部類訪問外部類成員時,外部類物件一定存在。


根據靜態成員不能訪問非靜態成員的規則,外部類的靜態方法,靜態程式碼塊不能訪問非靜態內部類,包括不能使用非靜態內部類定義變數,建立例項等。總之,不允許在外部類的靜態成員中直接使用非靜態內部類。
非靜態內部類也不能有靜態成員。

2. 靜態內部類
當用static修飾內部類時,這個類就是靜態內部類是與類相關的。因外部類的上一級程式單元是包,所以外部類不能用static修飾(即只有上一級程式單元是類才能用static修飾符
靜態內部類可以包含靜態成員,也可包含非靜態成員。根據靜態成員不能訪問非靜態成員的規則,靜態內部類只能訪問外部類的靜態成員,不能訪問外部類的非靜態成員。即使是靜態內部類的例項方法也不能訪問外部類的例項成員。
注意:1 靜態內部類的例項方法之所以不能訪問外部類的例項成員是因為:靜態內部類是類相關的,而不是外部類例項相關的。也就是說,靜態內部類不是寄生在外部類的物件中,而是寄生在外部類本身。當靜態內部類的物件存在時,並不一定存在外部類物件,靜態內部類物件只持有外部類的類引用,並沒有持有外部類物件的引用。
2 外部類依然不能直接訪問靜態內部類成員,但可以通過靜態內部類的類名來呼叫,或者通過靜態內部類的物件來呼叫。
3 Java允許接口裡定義內部類,接口裡定義內部類預設使用public static修飾,也就是說,接口裡的內部類只能是靜態內部類。
4 Java裡還允許在接口裡定義內部介面,該內部介面預設使用public static修飾。當然,在接口裡定義內部介面意義不大,因為介面的作用是定義一個公共規範(爆露出來供大家使用),如果把這個介面定義為內部介面,那又有什麼意義呢?

3. 使用內部類
下面分三種情況來討論內部類的用法:

(1) 在外部類內部使用內部類
在外部類內部使用內部類與平常使用類沒有太大區別。唯一存在的區別就是;不要在外部類靜態成員使用非靜態內部類(實際上也沒有什麼區別)。
(2)在外部類以外使用內部類
如果希望在外部類以外使用內部類(包括靜態與非靜態的),則不要用private去修飾它,private修飾的內部類只能在外部類的內部使用它。對於其他的訪問控制權限的修飾符能在對應的訪問許可權內使用。

  • 省略訪問控制符——只能被與外部類在同一個包中的其他類訪問。
  • protected——可被與外部類處於同一包中的其他類和外部類的子類所訪問
  • public——可在任何地方訪問
//在內部類以外的地方定義內部類變數(包括靜態與非靜態)
OuterClass.InnerClass varName;
//在外部類以外的地方建立非靜態內部類例項
//使用外部類例項和new呼叫非靜態內部類的構造器來建立非靜態內部類的例項
OuterInstance.new InnerConstructor();
class Out
{
   // 定義一個內部類,不使用訪問控制符   
   // 即只有同一個包中其他類才能訪問內部類
   class In
   {
      public In(String msg)
      {
         System.out.println(msg);
      }
   }
}
public class CreateInnerInstance
{
   public static void main(String[] args)
   {
      Out.In in = new Out().new In("測試資訊");
      /*
      上面程式碼可改為如下三行      
      使用OutterClass.InnerClass的形式定義內部類變數
      Out.In in;
      建立外部類例項,非靜態內部類例項將寄生在該例項中
      Out out = new Out();
     通過外部類例項和new來呼叫內部類例項構造器建立非靜態內部類例項
      in = out.new In("測試資訊");
      */
   }
}
//下面定義一個子類繼承了Out類的非靜態內部類In類
public class SubClass extends Out.In
{
   //顯式定義SubClass的構造器   
   public SubClass(Out out)
   {
      //通過傳入Out物件顯式呼叫In的構造器
      out.super("hello");
   }
}

注意:非靜態內部類的子類不一定是內部類,它可以是一個外部類,但非靜態內部類的子類例項一樣需要保留一個引用,該引用指向其父類所在外部類的物件。也就是說,如果有一個內部子類的物件存在,則一定存在與之對應的外部類物件。

(3) 在外部類以外使用靜態內部類
在外部類以外的地方建立靜態內部類的例項語法如下:

new OuterClass.InnerCostructor();
class StaticOut
{
   // 定義一個靜態內部類,不使用訪問控制符
   static class StaticIn
   {
      public StaticIn()
      {
         System.out.println("靜態內部類的構造器");
      }
   }
}
public class CreateStaticInnerInstance
{
   public static void main(String[] args)
   {
      StaticOut.StaticIn in = new StaticOut.StaticIn();
      /*
      上面程式碼可分為如下兩行程式碼      
      使用OutterClass.InnerClass的形式定義靜態內部類變數
      StaticOut.StaticIn in;
      通過new來呼叫內部類的構造器來建立內部類例項
      in = new StaticOut.StaticIn();
      */
   }
}

從上面程式碼來看,不管是靜態內部類還是非靜態內部類,宣告變數的語法都是一樣的。只是建立例項時不同。靜態內部類只需使用外部類就能呼叫構造器,非靜態內部類必須使用外部類變數才能呼叫構造器。
當定義一個靜態內部類時,其外部類就好像一個包空間
相比之下,使用靜態內部類要簡單得多,只要把外部類當成靜態內部類的包空間即可。因此,當程式需要使用內部類時,應先考慮使用靜態內部類

疑問:能不能為外部類定義一個子類,子類裡定義一個外部類重寫父類的內部類?
不能,內部類的類名不再試簡單的由內部類的類名組成,它實際上還把外部類的類名作為一個名稱空間,作為內部類名的限制。因此子類中的內部類和父類中的內部類不可能完全同名,也就不可能重寫。

4. 區域性內部類
如果把一個類定義在方法體內,則該類就是區域性內部類,區域性類只在方法體中有效。由於區域性內部類在方法體外不能使用,所以區域性內部不能使用訪問控制符和static修飾。
注意:對於區域性變數而言,不管是區域性變數還是區域性內部類,他們的上一級程式單元都是方法,而不是類,使用static修飾他們沒有任何意義。因此所有的區域性成員都不能使用static修飾,不僅如此,因為區域性成員的作用域是所在方法,其他所在程式單元永遠也不能訪問另一個方法中的區域性成員,所以所有的成員變數不能使用訪問許可權控制符

public class LocalInnerClass
{
   public static void main(String[] args)
   {
      // 定義區域性內部類
      class InnerBase
      {
         int a;
      }
      // 定義區域性內部類的子類
      class InnerSub extends InnerBase
      {
         int b;
      }
      // 建立區域性內部類的物件
      InnerSub is = new InnerSub();
      is.a = 5;
      is.b = 8;
      System.out.println("InnerSub物件的a和b例項變數是:"
         + is.a + "," + is.b);
   }
}

編譯上面程式產生了三個位元組碼檔案:LocalInnerClass.class , LocalInnerClass$1InnerBase.class , LocalInnerClass$1InnerSub.class注意到區域性類的檔案比成員內部類檔案多了一個數字,這是因為,同一個類裡不可能有兩個同名的成員內部類,而區域性內部類則可能存在同名的情況(在不同方法中),因此Java為區域性內部類的class檔案多加了個數字。在實際開發中很少使用區域性內部類,因為區域性內部類的作用域只能在方法中,作用域太小了。

5. java 8 改進的匿名內部類
匿名內部類適合建立那種只需要一次使用的類。匿名內部類的語法有點奇怪,建立匿名內部類時會立即建立一個該類的物件。這個類定義會立即消失,匿名內部類不能重複使用。
匿名內部類的定義語法如下:

new 實現介面() | 父類構造器(實參列表)
{
	//匿名內部類的類體部分
}

從上面定義語法可以看出,匿名內部類必須實現一個介面或者繼承一個父類,但最多隻能實現一個介面或繼承一個父類。
關於匿名內部類還有如下兩個規則:

  • 匿名內部類不能是抽象類,因為系統在建立匿名內部類時會立即建立該類的一個例項,因此不能將匿名內部類定義為抽象類。
  • 匿名內部類不能定義構造器,由於匿名內部類沒有類名,所以無法定義構造器,但匿名內部類可以定義初始化塊,可以通過例項初始化塊來完成構造器需要完成的事情。
interface Product
{
   public double getPrice();
   public String getName();
}
public class AnonymousTest
{
   public void test(Product p)
   {
      System.out.println("購買了一個" + p.getName()
         + "花掉了" + p.getPrice());
   }
   public static void main(String[] args)
   {
      AnonymousTest ta = new AnonymousTest();
      // 呼叫test()方法,需要傳入一個Product引數
      // 此處傳入其匿名實現類的例項
      ta.test(new Product()
      {
         public double getPrice()
         {
            return 567.8;
         }
         public String getName()
         {
            return "AGP顯示卡";
         }
      });
   }
}

正如上面程式看到的,定義匿名內部類無需class關鍵字,而是在定義匿名類時直接生成匿名內部類的物件。由於匿名內部類不能是抽象類,所以匿名內部類必須實現它的抽象父類或接口裡包含的全部抽象方法。
當通過實現介面來建立匿名內部類時,匿名內部類也不能顯式建立構造器,因此匿名內部類只有一個隱式的無參構造器,故new介面名後的括號裡不能傳入引數值。
但如果通過繼承父類來建立匿名內部類時,匿名內部類將擁有和父類相似的構造器,此處的相似指的是相同的形參列表。

abstract class Device
{
   private String name;
   public abstract double getPrice();
   public Device(){}
   public Device(String name)
   {
      this.name = name;
   }
   // 此處省略name的setter和getter方法
}
  public class AnonymousInner
{
   public void test(Device d)
   {
      System.out.println("購買了一個" + d.getName()
         + ",花掉了" + d.getPrice());
   }
   public static void main(String[] args)
   {
      AnonymousInner ai = new AnonymousInner();
      // 呼叫有參構造器建立Device匿名內部類物件
      ai.test(new Device("電子示波器")
      {
         public double getPrice()
         {
            return 67.8;
         }
      });
      // 呼叫無參構造器建立Device匿名內部類物件
      Device d = new Device()
      {
         // 初始化塊        
         {
            System.out.println("匿名內部類的初始化塊...");
         }
         // 實現抽象方法         
         public double getPrice()
         {
            return 56.2;
         }
         // 重寫父類的例項方法
         public String getName()
         {
            return "鍵盤";
         }
      };
      ai.test(d);
   }
}
interface A
{
   void test();
}
public class ATest
{
   public static void main(String[] args)
   {
      int age = 8;
      A a = new A()
      {
         public void test()
         {
            // 在Java 8以前,下面語句將提示錯誤:age必須使用final修飾            
            // 在Java 8開始,匿名內部類,區域性內部類允許訪問非final的區域性成員
            System.out.println(age);
         }
      };
      a.test();
   }
}

在Java 8之前要求被區域性內部類,匿名內部類訪問的區域性變數必須是final修飾的,在java 8 之後更加智慧,如果區域性變數被區域性內部類,匿名內部類訪問,就相當於該區域性變數自動使用了final修飾。