• 又一次认识java(九) ---- 内部类


    注意注意!!

    前排提示!!。本篇文章过长,最好收藏下来慢慢看。假设你之前对内部类不是非常熟悉,一次性看完,大概你会懵逼。

    1. 内部类概述

    一个类的定义放在还有一个类的内部,这个类就叫做内部类。内部类是一种非常实用的特性。由于它同意你把一些逻辑相关的类组织在一起。

    内部类大体上能够分为四种:

    成员内部类,静态内部类,局部内部类,匿名内部类

    我们先来具体的看一下这四种内部类。

    2. 成员内部类

    成员的内部类,就是最基础的内部类。没有那些花里胡哨的修饰:

    //外部类
    public class Outer {
        private String a = "a";
        public int i = 1;
        //内部类
        class Inner{
            private String b = "b";
            public String c = "c";
    
            public int getInt(){
                return i; // 内部类能够訪问外部类变量
            }
    
            private String getString(){
                return a + b + c; // 内部类能够訪问外部类的private变量
            }
        }
    
        public String getParam(){
            Inner inner = new Inner();
            inner.b = "bb"; // 外部类能够訪问内部类的private变量
            inner.c = "cc";
            return inner.getInt() + inner.getString();
        }
    }
    //測试类
    class Test {
        public static void main(String[] args) {
            Outer outer = new Outer();
            System.out.println(outer.getParam()); // 输出:1abbcc
    
            Outer.Inner oi = outer.new Inner();
            oi.c = "ccc";
            //oi.b = "bbb";  编译失败
            System.out.println(oi.getInt()); // 输出:1
            //System.out.println(oi.getString()); 编译失败
        }
    }

    从这段代码中,我们总结一下普通内部类的要点:

    • 内部类能够訪问外部类变量。包括私有变量

    • 在外部类中使用内部类的方法须要new一个内部类的对象。



    • 在外部类中能够訪问到内部类的不论什么变量,包括私有变量。

    • 在其它类中创建内部类对象须要使用这种形式:
      OuterClassName.InnerClassName name = new OuterClassName().new InnerClassName()。

    • 在其它类中定义的内部类对象不能訪问内部类中的私有变量。

    当然。除了以上知识点以外,在内部类中,能够通过【.this】訪问到外部类对象。

    public class Outer {
        private int num ;
        public Outer(){}
    
        public Outer(int num){
            this.num = num;
        }
    
        private class Inner{
            public Outer getTest2(){
                return Outer.this; // Outer.this指的是外部类的对象
            }
    
            public Outer newTest2(){
                return new Outer();
            }
        }
    
        public static void main(String [] args){
            Outer test = new Outer(5);
            Outer.Inner inner = test.new Inner();
            Outer o1 = inner.getTest2();
            Outer o2 = inner.newTest2();
            System.out.println(o1.num); // 5
            System.out.println(o2.num); // 0
        }
    }

    注意通过.this得到的对象,和通过new出来的对象的差别。

    使用.this后,得到时创建该内部类时使用的外围类对象的引用,new则是创建了一个新的引用。

    到这里了我们须要明白一点,内部类是个编译时的概念,一旦编译成功后,它就与外围类属于两个全然不同的类(当然他们之间还是有联系的)。对于一个名为OuterClass的外围类和一个名为InnerClass的内部类,在编译成功后。会出现这样两个class文件:OuterClass.class和OuterClass$InnerClass.class。

    3. 内部类与向上转型

    到眼下为止,你可能认为内部类只是如此,没什么新鲜的地方。毕竟假设仅仅是为了隐藏一个类,java本身已经有了非常好的隐藏机制——仅仅给某个类包訪问权限。而用不着把类创建为内部类。

    可是。当一个内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。

    这是由于这种内部类(某个接口的实现类)对于其它人来说全然不可见。而且不可用。所得到的仅仅是指向基类或者接口的引用,所以能非常方便的隐藏实现细节。

    我们来看一段代码:

    //定义两个接口
    public interface Run {
        void run();
    }
    public interface Eat {
        void eat();
    }
    //外部类
    public class Person {
        //这里是private
        private class PEat implements Eat {
            @Override
            public void eat() {
                System.out.println("eat with mouse");
            }
        }
    
        //这里是protected
        protected class PRun implements Run{
            @Override
            public void run() {
                System.out.println("run with leg");
            }
        }
    
        public Eat howToEat(){
            return new PEat(); //向上转型
        }
    
        public Run houToRun(){
            return new PRun(); //向上转型
        }
    }
    //測试类
    class TestPerson{
        public static void main(String[] args) {
            Person p = new Person();
            Eat e = p.howToEat();
            Run r = p.houToRun();
    
            e.eat();
            r.run();
    
            Person.PRun ppr = p.new PRun();
            //Person.PEat ppe = p.new PEat(); 编译失败,由于PEat是private的
        }
    }
    

    从这段代码能够看出。PEat是private。所以除了Person(它的外部类),没有人能訪问到它。PRun是protected,所以仅仅有Person及其子类、还有与Person同一个包中的类能訪问PRun。其它类不能訪问。

    这意味着。假设client程序猿想要了解或者訪问这些成员是要受到限制的。除此之外。private内部类也不能够被向下转型,由于不能訪问他的名字。

    所以。private内部类给类的设计者提供了一种途径。通过这种方式能够全然阻止不论什么依赖于类的编码。而且全然隐藏了实现的细节。

    此外。从client程序猿的角度看。由于不能訪问不论什么新添加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也个java编译器提供了更高效代码的机会。

    所以说,一般成员内部类,都会定义为private的。

    普通的类(非内部类),不能声明为private或protected。它们之恩给你被赋予public或者包訪问权限。

    4. 静态内部类(嵌套类)

    假设不须要内部类对象与其外围类对象之间又联系。那么能够将内部类声明为static。

    这就是静态内部类,也被称为嵌套类。

    普通的内部类对象隐式的保存了一个指向它的外部类引用的变量。所以它能够无条件的使用外部类的变量。可是当内部类yogastatic修饰的时候。就不会有这个变量了。这意味着:

    • 要创建嵌套类的对象,并不须要其外围类的对象。
    • 静态内部类中不能訪问非静态的外部类变量,可是尅訪问外部类的静态变量。

    除此之外,由于普通内部类的字段与方法。仅仅能放在类的外部层次上,所以普通的内部类不能有static方法和static变量。也不能在普通内部类中再包括静态内部类。可是静态内部类能够包括全部这些东西:

    public class Outer {
        private int i = 1;
        public static String str = "str";
    
        static class StaClass implements inter{
            private String s = "s";
            static int j = 2;
    
            static int getInt(){
                //return i + j;
                return j;
            }
    
            private String getString(){
                return str + s;
            }
    
            @Override
            public void inter() {
                System.out.println("inter");
            }
    
            static class InStaClass{
                int x = 4;
                static int y = 5;
                 static int getInt(){
                    //return x; // x是非静态变量 不能够在静态方法中使用
                    return y;
                }
            }
        }
    
        public inter getInter(){
            return new StaClass();
        }
    }
    
    class Test{
        public static void main(String[] args) {
            int a = Outer.StaClass.getInt();
    
            //Outer.StaClass.getString(); // getString()为非静态方法,不能这样调用
    
            int b = Outer.StaClass.InStaClass.getInt();
    
            System.out.println(a + "----" + b); // 输出 2----5
    
            //new Outer().new StaClass(); 编译失败 StaClass是静态的
    
            new Outer().getInter().inter(); // 输出 inter
    
    
        }
    }

    通过这段代码。我们总结一下静态内部类的要点:

    • 在静态内部类中能够存在静态成员

    • 静态内部类仅仅能訪问外围类的静态成员变量和方法,不能訪问外围类的非静态成员变量和方法

    • 静态内部类中的静态方法能够通过【外部类.内部类.方法名】直接调用

    • 静态内部类在其它类中不能new出来。

      (new Outer().new StaClass()这样是不行的)

    • 可是在外部类中,能够new一个静态内部类的对象。

    • 静态内部类中不能使用【.this】

    5. 局部内部类

    在一个方法里或者随意作用域里定义的内部类叫做局部内部类。

    为什么要这么做呢?

    如前所看到的。你实现了某类型的接口,于是你能够创建并返回对其的引用,你须要这种引用。


    你要解决一个复杂的问题。想创建一个类来辅助你的解决方式,可是又不希望这个类是公共可用的。

    听起来有点费解,往下看你就明白了。

    5.1 一个定义在方法中的类

    public class Person {
        public Eat howToEat(){
            // 定义在方法中的类
            class EatWithMouth implements Eat{
                @Override
                public void eat() {
                    System.out.println("eat with mouth");
                }
            }
            // 向上转型
            return new EatWithMouth();
        }
    
        public static void main(String[] args) {
            Eat e = new Person().howToEat();
            e.eat(); // eat with mouth
        }
    }

    EatWithMouth是方法howToEat中的类而不是Person中的类。你甚至能够在同一个子文件夹下的随意一个类中给随意一个内部类起EatWithMouth这个名字,而不会有命名冲突。所以。在howToEat方法外的不论什么地方都不能訪问到EatWithMouth类。

    当然,这并不意味着一旦howToEat方法执行完成,EatWithMouth类就不能用了。

    5.2 在随意作用域嵌入一个内部类

    能够在随意作用域中嵌入内部类:

    public class EveryBlock {
        private String test(boolean b){
            if (b){
                class A{
                    private String a = "a";
                    String getString(){
                        return a;
                    }
                }
                A a = new A();
                String s = a.getString();
                return s;
            }
            //A a = new A();  编译失败 超出作用域
            return null;
        }
    
        public static void main(String[] args) {
            EveryBlock eb = new EveryBlock();
            System.out.println(eb.test(true)); // a
        }
    }

    尽管类A在if语句中,可是这并不表明类A的创建时有条件的。它事实上是和别的类一起编译的。可是它在它定义的作用域之外的不可用的,除此之外与普通内部类一样。

    通过这种方式。就攻克了上面提到的第二个问题:不希望这个类是公用的。

    6. 匿名内部类

    匿名内部类应该是使用的最多的了,尤其是在swing中。

    先看一个样例:

    public class OuterClass {
        public InnerClass getInnerClass(final int num,String str2){
            return new InnerClass(){
                int number = num + 3;
                public int getNumber(){
                    return number;
                }
            };//注意:分号不能省
        }
    
        public static void main(String[] args) {
            OuterClass out = new OuterClass();
            InnerClass inner = out.getInnerClass(2, "chengfan");
            System.out.println(inner.getNumber());
        }
    }
    
    interface InnerClass {
        int getNumber();
    }
    

    这段代码里有一段非常奇怪的东西:

            return new InnerClass(){
                int number = num + 3;
                public int getNumber(){
                    return number;
                }
            };

    没错,就是它。InnerClass不是一个借口么,怎么还能new呢?聪明的你一定知道,这就是匿名内部类。事实上。这段代码和以下的写法是等价的:

    public class OuterCla {
    
        class InnerClassImpl implements InnerClass{
            int number ;
            public InnerClassImpl(int num){
                number = num + 3;
            }
            public int getNumber(){
                return number;
            }
        }
        public InnerClass getInnerClass(final int num){
            return new InnerClassImpl(2);
        }
    
        public static void main(String[] args) {
            OuterCla out = new OuterCla();
            InnerClass inner = out.getInnerClass(2);
            System.out.println(inner.getNumber());
        }
    }

    这段代码你应该懂了。

    将两段代码一比較。你大概也清楚了,上面那样写,意思是创建了一个实现了InnerClass的匿名类的对象。

    匿名类能够创建接口、抽象类、与普通类的对象。

    创建接口和抽象类时,必须实现接口中全部方法。 创建匿名类时。能够是无參的,也能够有參数的。可是假设这个參数要在匿名类中使用,參数必须是final的。假设不使用。能够不被final修饰(代码中有体现)。

    6.1 为什么必须是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的。

    6.2匿名内部类小结

    • 匿名内部类是没有訪问修饰符的。



      匿名内部类中不能存在不论什么的静态成员变量和静态方法。

    • new 匿名内部类,这个类首先是要存在的。

      假设我们将那个InnerClass接口凝视掉,就会出现编译出错。



    • 当所在方法的形參须要被匿名内部类使用,那么这个形參就必须为final。

    • 匿名内部类创建一个接口的引用时是没有构造方法的。可是能够通过构造代码块来模拟构造器,像以下这样:
    public A getA(){  
        return new A(){  
            int num = 0;  
            String str;  
            {  
                str = "这是构造代码块!";  
                System.out.println("str 已经被初始化!");  
            }  
        };  
    } 
    • 可是当匿名内部类创建一个抽象类或者实体类的引用时。假设有必要,是能够定义构造函数的:
    public class Outer { 
        public static void main(String[] args) { 
            Outer outer = new Outer(); 
            Inner inner = outer.getInner("Inner", "gz"); 
            System.out.println(inner.getName()); 
        } 
    
        public Inner getInner(final String name, String city) { 
            return new Inner(name, city) { 
                private String nameStr = name; 
    
                public String getName() { 
                    return nameStr; 
                } 
            }; 
        } 
    } 
    
    abstract class Inner { 
        Inner(String name, String city) { 
            System.out.println(city); 
        } 
    
        abstract String getName(); 
    } 
    //注意这里的形參city,由于它没有被匿名内部类直接使用,而是被抽象类Inner的构造函数所使用。所以不必然义为final。

    • 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的全部抽象方法。

    事实上,创建匿名内部类要写的模板代码太多了,java8中的lambda表达式能够替代大部分的匿名类。优雅简洁代码少,所以建议大家学习java8,当然,匿名内部类的知识还是要掌握的。

    7.内部类的继承

    内部类的继承。是指内部类被继承,普通类 extents 内部类。

    而这时候代码上要有点特别处理,具体看以下样例:

    public class InheritInner extends WithInner.Inner { 
    
        // InheritInner() 是不能通过编译的,一定要加上形參 
        InheritInner(WithInner wi) { 
            wi.super(); 
        } 
    
        public static void main(String[] args) { 
            WithInner wi = new WithInner(); 
            InheritInner obj = new InheritInner(wi); 
        } 
    } 
    
    class WithInner { 
        class Inner { 
    
        } 
    } 

    能够看到子类的构造函数里面要使用父类的外部类对象.super();而这个对象须要从外面创建并传给形參。

    8. 多重继承

    内部类是除了接口外实现多重继承的又一有利工具。

    利用接口实现多重继承我们都知道,就是一次性实现非常多接口。那么。怎样利用内部类实现多重继承呢?

    看代码:

    //父亲
    public class Father {
        public int strong(){
            return 9;
        }
    }
    //母亲
    public class Mother {
        public int kind(){
            return 8;
        }
    }
    //儿子
    public class Son {
    
        /**
         * 内部类继承Father类
         */
        class Father_1 extends Father{
            public int strong(){
                return super.strong() + 1;
            }
        }
    
        class Mother_1 extends  Mother{
            public int kind(){
                return super.kind() - 2;
            }
        }
    
        public int getStrong(){
            return new Father_1().strong();
        }
    
        public int getKind(){
            return new Mother_1().kind();
        }
    }
    
    public class Test1 {
    
        public static void main(String[] args) {
            Son son = new Son();
            System.out.println("Son 的Strong:" + son.getStrong());
            System.out.println("Son 的kind:" + son.getKind());
        }
    
    }
    
    //输出
    //Son 的Strong:10
    //Son 的kind:6

    儿子继承了父亲。变得比父亲更加强壮。同一时候也继承了母亲。仅仅只是温和指数下降了。这里定义了两个内部类,他们分别继承父亲Father类、母亲类Mother类,且都能够非常自然地获取各自父类的行为,这是内部类一个重要的特性:内部类能够继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。

    9. 内部类的原理简析

    上面说过这样两点:

    (1) 在外部类的作用范围内能够随意创建内部类对象。即使内部类是私有的(私有内部类)。即内部类对包围它的外部类可见。

    (2) 在内部类中能够訪问其外部类的全部域,即使是私有域。即外部类对内部类可见。

    问题来了:上面两个特点究竟怎样办到的呢?内部类的”内部”究竟发生了什么?

    事实上,内部类是Java编译器一手操办的。

    虚拟机并不知道内部类与常规类有什么不同。 编译器是怎样瞒住虚拟机的呢?

    我们用javac命令编译一下以下的代码:

    class Outer{   
           //外部类私有数据域   
           private int data=0;   
           //内部类   
           class Inner{   
               void print(){   
                     //内部类訪问外部私有数据域   
                     System.out.println(data);   
               }    
           }   
    }  

    能够看到这种结果:

    这里写图片描写叙述

    对内部类进行编译后发现有两个class文件:Outer.class 、和Outer$Inner.class 。这说明内部类Inner仍然被编译成一个独立的类(Outer$Inner.class)。而不是Outer类的某一个域。 虚拟机执行的时候。也是把Inner作为一种常规类来处理的。

    但问题又来了,即然是两个常规类,为什么他们之间能够互相訪问私有域那(最開始提到的两个内部类特点)?这就要问问编译器究竟把这两个类编译成什么东西了。

    我们利用reflect反射机制来探查了一下内部类编译后的情况:

    //反编译后的Outer$Inner
    class Outer$Inner{   
            Outer$Inner(Outer,Outer$Inner);  //包可见构造器   
            private Outer$Inner(Outer);   //私有构造器将设置this$0域   
            final Outer this$0;   //外部类实例域this$0  
    } 

    好了,如今我们能够解释上面的第一个内部类特点了: 为什么外部类能够创建内部类的对象?而且内部类能够方便的引用到外部类对象?

    首先编译器将外、内部类编译后放在同一个包中。在内部类中附加一个包可见构造器。这样。 虚拟机执行Outer类中Inner in=new Inner(); 实际上调用的是包可见构造: new Outer$Inner(this,null)。因此即使是private内部类。也会通过隐含的包可见构造器成功的获得私有内部类的构造权限。

    再者。Outer$Inner类中有一个指向外部类Outer的引用this$0,那么通过这个引用就能够方便的得到外部类对象中可见成员。可是Outer类中的private成员是怎样訪问到的呢?这就要看看以下Outer.class文件里的秘密了。

    class Outer{   
        static int access$0(Outer);  //静态方法,返回值是外部类私有域 data 的值。   
    }  

    如今能够解释第二个特点了:为什么内部类能够引用外部类的私有域?

    原因的关键就在编译器在外围类中加入了静态方法access$0。 它将返回值作为參数传递给他的对象域data。

    这样内部类Inner中的打印语句:System.out.println(data); 实际上执行的时候调用的是:System.out.println(this$0.access$0(Outer));

    总结一下编译器对类中内部类做的手脚吧:

    • (1) 在内部类中偷偷摸摸的创建了包可见构造器,从而使外部类获得了创建权限。

    • (2) 在外部类中偷偷摸摸的创建了訪问私有变量的静态方法,从而 使 内部类获得了訪问权限。这样,类中定义的内部类不管私有。公有,静态都能够被包围它的外部类所訪问。

    反射的知识,仅仅会会讲。IDEA自带将.class文件反射的功能。可是它的功能强大到能够把Outer类完整的还原。。

    10. 总结

    本篇文章过长。总结完以后也是身心俱疲,所以就不多啰嗦了。文章有什么错误或者不足。请及时与我联系。


    转载请注明出处:
    本文地址:http://blog.csdn.net/qq_31655965/article/details/54988623
    原创自csdn:http://blog.csdn.net/qq_31655965
    博主:clever_fan

    看完了,假设对你有帮助,随手点个赞呗~~~

    參考资料
    《java编程思想》
    《java核心技术》
    http://android.blog.51cto.com/268543/384844/
    http://www.iteye.com/topic/442435
    http://www.cnblogs.com/chenssy/p/3389027.html
    http://www.cnblogs.com/jiangao/archive/2012/02/23/2364119.html

  • 相关阅读:
    设计模式desine pattern梳理
    system desing 系统设计(二): 数据库sharding和Consistent Hashing算法原理
    system desing 系统设计(一): 数据库sql和NoSql的选择
    system desing 系统设计(三): 分布式文件系统distributed file system设计原理
    system desing 系统设计(四):网站API和短网址short url的生成
    HTML 基础知识总结
    Linux 基础知识总结
    CSS 基础知识总结
    Codeforces Round #821 (Div. 2)
    AtCoder Beginner Contest 271
  • 原文地址:https://www.cnblogs.com/yjbjingcha/p/7388651.html
Copyright © 2020-2023  润新知