Java重新出发--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 oi = 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.classOuterClass$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.这就是静态内部类,也称为嵌套类。普通的内部类对象隐式的保存了一个指向它外部类引用的变量,所以可以无条件的使用外部类的变量,但是内部类用static修饰时,就不会有这个变量了。这也意味着:要创建嵌套类的对象,并不需要其外围类的对象。
静态内部类中不能访问非静态的外部类变量,但是可以访问外部类的静态变量。
除此之外,由于普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有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;//访问不到i非静态
            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是在条件语句中,但是它的创建是无条件的,和其它类一样进行编译。仅仅只是作用域不同而已。通过这样的方式,就解决了上面提到的第二个问题:不希望这个类是公用的。

6.匿名内部类

匿名内部类使用的地方有很多,看一个例子:

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;
            }
        };

这不是一个接口吗?没错这就是匿名内部类。事实上,这段代码和下面的写法是等价的:

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 、和OuterInner.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内部类,也会通过隐含的包可见构造器成功的获得私有内部类的构造权限。
再者,Outer0,那么通过这个引用就可以方便的得到外部类对象中可见成员。
但是Outer类中的private成员是如何访问到的呢?这就要看看下面Outer.class文件中的秘密了。

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

现在可以解释第二个特点了:为什么内部类可以引用外部类的私有域?
原因的关键就在编译器在外围类中添加了静态方法access0。 它将返回值作为参数传递给他的对象域data。 这样内部类Inner中的打印语句:System.out.println(data); 实际上运行的时候调用的是:System.out.println(this0.access$0(Outer));
总结一下编译器对类中内部类做的手脚吧:
(1) 在内部类中偷偷摸摸的创建了包可见构造器,从而使外部类获得了创建权限。
(2) 在外部类中偷偷摸摸的创建了访问私有变量的静态方法,从而 使 内部类获得了访问权限。这样,类中定义的内部类无论私有,公有,
静态都可以被包围它的外部类所访问。

内部类我还是一知半解,以后还是要回过头来结合实例详细了解。

你可能感兴趣的:(Java重新出发--Java学习笔记(八)--内部类相关)