美文网首页javajava爱好者重新认识java
重新认识java(九) ---- 内部类

重新认识java(九) ---- 内部类

作者: Sharember | 来源:发表于2018-01-17 12:02 被阅读169次

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

    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,其他类不能访问。

    这意味着,如果客户端程序员想要了解或者访问这些成员是要受到限制的。除此之外,private内部类也不可以被向下转型,因为不能访问他的名字。

    所以,private内部类给类的设计者提供了一种途径,通过这样的方式可以完全阻止任何依赖于类的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也个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

    相关文章

      网友评论

        本文标题:重新认识java(九) ---- 内部类

        本文链接:https://www.haomeiwen.com/subject/fwnyoxtx.html