1. 程式人生 > >Java基礎8——深入理解java的內部類

Java基礎8——深入理解java的內部類

深入理解內部類

內部類基礎

  在Java中,可以將一個類定義在另一個類裡面或者一個方法裡面,這樣的類稱為內部類。廣泛意義上的內部類一般來說包括這四種:成員內部類、區域性內部類、匿名內部類和靜態內部類。

問題:為什麼要使用內部類?
  在《Think in java》中有這樣一句話:使用內部類最吸引人的原因是:每個內部類都能獨立地繼承一個(介面的)實現,所以無論外圍類是否已經繼承了某個(介面的)實現,對於內部類都沒有影響。
  可以利用內部類提供的、可以繼承多個具體的或者抽象的類的能力來解決這些程式設計問題。可以這樣說,介面只是解決了部分問題,而內部類使得多重繼承的解決方案變得更加完整

1. 成員內部類

  成員內部類 是最普通的內部類,其定義位於另一個類的內部且在方法外,作為 類及類的成員而存在。

  • 作為類,可宣告為abstract的,即可以被其他的內部類所繼承。
  • 作為類的成員,其可宣告為final、static(靜態內部類)和abstract的,且與外部類不同的是,內部類可以使用四種訪問修飾符進行修飾
class Circle {
    double radius = 0;
     
    public Circle(double radius) {
        this.radius = radius;
    }
     
    class
Draw { //內部類 public void drawSahpe() { System.out.println("drawshape"); } } }

  這樣看起來,類Draw像是類Circle的一個成員,Circle稱為外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括private成員和靜態成員)。

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(
double radius) { this.radius = radius; } class Draw { //內部類 public void drawSahpe() { System.out.println(radius); //外部類的private成員 System.out.println(count); //外部類的靜態成員 } } }

  注意的是,當成員內部類擁有和外部類同名的成員變數或者方法時,會發生隱藏現象,即預設情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要以下面的形式進行訪問:

外部類.this.成員變數
外部類.this.成員方法

  雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先建立一個成員內部類的物件,再通過指向這個物件的引用來訪問

class Circle {
    private double radius = 0;
 
    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必須先建立成員內部類的物件,再進行訪問
    }
     
    private Draw getDrawInstance() {
        return new Draw();
    }
     
    class Draw {     //內部類
        public void drawSahpe() {
            System.out.println(radius);  //外部類的private成員
        }
    }
}

  成員內部類是依附外部類而存在的,也就是說,如果要建立成員內部類的物件,前提是必須存在一個外部類的物件。建立成員內部類物件的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        //第一種方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必須通過Outter物件來建立
         
        //第二種方式:
        Outter.Inner inner1 = outter.getInnerInstance();
        
        //若對於靜態內部類建立物件:
        Outter.Inner inner2 = new Outter.Inner1();//不需要建立Outter的物件,直接建立內部類的物件
    }
}
 
class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    class Inner {
        public Inner() {
             
        }
    }
    static class Inner1(){
    	public Inner() {
    	      
        }
    }
}

2. 區域性內部類

  區域性內部類 是定義在一個方法內或一個作用域(如if條件判斷程式碼塊)中的類,其和成員內部類的區別在於區域性內部類的訪問僅限於該方法內或該作用域中。
注意點:

  • 區域性內部類可以訪問當前程式碼塊的常量以及其外部類的所有成員。
  • 區域性內部類非外部類的成員,故外部類無法訪問該內部類
  • 區域性內部類可以看做一個區域性變數,不能有public、protected、private 和static修飾。
class People{
    public People() {
         
    }
}
 
class Man{
    public Man(){
         
    }
     
    public People getWoman(){
        class Woman extends People{   //區域性內部類
            int age =0;
        }
        return new Woman();
    }
}

3. 匿名內部類

基本結構:

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

  匿名內部類 指沒有名字的內部類,故其只能使用一次,通常用來簡化程式碼編寫,如Android中為控制元件新增監聽事件。

scan_bt.setOnClickListener(new OnClickListener() {
             
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                 
            }
        });
         
history_bt.setOnClickListener(new OnClickListener() {
             
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                 
            }
        });

  使用匿名內部類能夠在實現父類或者介面中的方法情況下同時產生一個相應的物件,但是前提是這個父類或者介面必須先存在才能這樣使用。當然像下面這種寫法也是可以的,跟上面使用匿名內部類達到效果相同。

private void setListener()
{
    scan_bt.setOnClickListener(new Listener1());        
    history_bt.setOnClickListener(new Listener2());
}
 
class Listener1 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}
 
class Listener2 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}

注意點:

  • 匿名內部類必須繼承一個父類或實現一個介面,進而對繼承方法進行實現或重寫(匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的介面的所有抽象方法
  • 匿名內部類是唯一一種沒有構造器的類,其在編譯時由系統自動起名為「外部類名序號.class 」如「Outter1.class」。
  • 匿名內部類一定是在new的後面,僅用於建立該匿名內部類的一個例項。
  • 匿名內部類為區域性內部類,所以區域性內部類的所有限制同樣對匿名內部類生效。

最常用的情況就是在多執行緒的實現上,因為要實現多執行緒必須繼承 Thread類或是繼承Runnable介面。

public class 匿名內部類 {

}
interface D{  //介面D
    void run ();
}
abstract class E{ //抽象類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() {

                }
            };
        }

}
--------------------- 
作者:How 2 Play Life 
來源:CSDN 
原文:https://blog.csdn.net/a724888/article/details/80087616 
版權宣告:本文為博主原創文章,轉載請附上博文連結!

為什麼區域性內部類和匿名內部類只能訪問區域性final變數?

public class Test1 {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int b) {
        final int a = 10;//若去掉a,b前面任意一個final編譯都出現錯誤
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
} // 這段程式碼會被編譯成兩個class檔案:Test.class和Test1.class。

在這裡插入圖片描述
分析原因:
  當test方法執行完畢之後,變數a的生命週期就結束了,而此時Thread物件的生命週期很可能還沒有結束,那麼在Thread的run方法中繼續訪問變數a就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?Java採用了 拷貝 的手段來解決這個問題。將這段程式碼的位元組碼反編譯可以得到下面的內容:
在這裡插入圖片描述
  這條指令表示將運算元10壓棧,表示使用的是一個本地區域性變數。這個過程是在編譯期間由編譯器預設進行,如果這個變數的值在編譯期間可以確定,則編譯器預設會在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。這樣一來,匿名內部類使用的變數是另一個區域性變數,只不過值和方法中區域性變數的值相等,因此和方法中的區域性變數完全獨立開

public class Test2 {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

  這裡是將變數test方法中的形參a以引數的形式傳進來對匿名內部類中的拷貝(變數a的拷貝)進行賦值初始化。
以區域性內部類為例:

public class OuterClass {
    public void display(final String name,String age){
        class InnerClass{
            void display(){
                System.out.println(name);
            }
        }
    }
}
--------------------- 
作者:How 2 Play Life 
來源:CSDN 
原文:https://blog.csdn.net/a724888/article/details/80087616 
版權宣告:本文為博主原創文章,轉載請附上博文連結!

  從上面程式碼中看好像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 );
    }
}
--------------------- 
作者:How 2 Play Life 
來源:CSDN 
原文:https://blog.csdn.net/a724888/article/details/80087616 
版權宣告:本文為博主原創文章,轉載請附上博文連結!

  所以從上面程式碼來看,內部類並不是直接呼叫方法傳遞的引數,而是利用自身的構造器對傳入的引數進行備份,自己內部方法呼叫的實際上時自己的屬性而不是外部方法傳遞進來的引數。

  也就說如果區域性變數的值在編譯期間就可以確定,則直接在匿名內部裡面建立一個拷貝(如Test1)。如果區域性變數的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。(如Test2)

  簡單理解就是,拷貝引用,為了避免引用值發生改變,例如被外部類的方法修改等,而導致內部類得到的值不一致,於是用final來讓該引用不可改變。

4. 靜態內部類

  靜態內部類也是定義在另一個類裡面的類,只不過在類的前面多了一個關鍵字static。靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非static成員變數或者方法,這點很好理解,因為在沒有外部類的物件的情況下,可以建立靜態內部類的物件,如果允許訪問外部類的非static成員就會產生矛盾,因為外部類的非static成員必須依附於具體的物件。

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();//不需要建立Outter的物件,直接建立內部類的物件
    }
}
 
class Outter {
    public Outter() {
         
    }
     
    static class Inner {
        public Inner() {
             
        }
    }
}

內部類初始化

  我們一般都是利用構造器來完成某個例項的初始化工作的,但是==匿名內部類是沒有構造器的!那怎麼來初始化匿名內部類呢?使用構造程式碼塊!==利用構造程式碼塊能夠達到為匿名內部類建立一個構造器的效果。

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_;
            }
        };
    }
--------------------- 
作者:How 2 Play Life 
來源:CSDN 
原文:https://blog.csdn.net/a724888/article/details/80087616 
版權宣告:本文為博主原創文章,轉載請附上博文連結!