第十章:内部类

内部类

  • 所谓内部类就是将一个类定义在另一个类的定义内部。还记得组合的定义是:成员是另外一个类的对象。有点类似,那却不同。那么问题来了,为什么要使用有内部类?使用内部类有什么好处?如何使用内部类?听我娓娓道来。

创建内部类

  • 我们来看一个简单的例子:
public class Outer {
    String name;
    void method() {
        Inner in = new Inner();//在Outer内部可以直接使用
        //这个表明了在外围类中,内部类的用法就和其他类一模一样。甚至看不出区别。
    }
    class Inner {

    }
}
class other {
    void method() {
        Outer o = new Outer();
        Outer.Inner i = o.new Inner();
        //要想使用Outer.Inner 必须有default以上访问权限修饰符修饰,且必须先创建外围类。
        //这里还用到了.new语法,后续会进行介绍
    }
}
  • 看起来除了Inner 类定义的位置是在Outer内部以外好像并没有其他区别。不过这里可以用任意的访问权限修饰符进行修饰,作用和修饰方法成员类似,后续会有例子。还可以用staticfinal去修饰。final不必多说,而static修饰会在后面进行讨论,此处先讨论非static的内部类。

链接到外部类

  • 我们知道组合是对象中包含其他对象的引用,那内部类对象会有怎样的特性呢。上面的例子我们在其他类中创建了一个Outer.Inner对象。这个很好理解,Inner是default的,所以在同包下可以访问,它又是附属于Outer的,所以必须带上命名空间,否则就无法和普通类区别了。我们使用.new语法创建了一个Inner对象。那为什么不能直接这样去创建对象呢?
Outer.Inner i = new Outer.Inner();//错误
  • 其实上述写法只要在Inner是static的情况下,就可以通过编译。一个很重要的思想,如果一个成员或者方法不是static的,那就必须创建对象后,才能去使用它。这句话对内部类也同样适用,我们可以认为非static内部类的对象是外围类的一个“可选择成员”。因为存在这种先后关系,内部类对象暗暗地连接到创建它的外部类对象上。有两个问题需要考虑:
    1. 怎么通过外围类对象去访问内部类对象?
    2. 怎么通过内部类对象去访问外围类对象?
  • 首先,通过外围类对象如何创建内部类对象我们已经知道了,我们可以通过外围类对象去创建任意个数内部类对象。因此我们好像无法直接通过外围类对象去获得某个内部类对象的引用(数量未知)。但反过来就不一样了,由于所有的内部类对象都是由一个固定的内部类对象创建的,因此我们就可以通过.this语法去获得外围类对象的引用:
public class Main {
    public static void main(String args[]) {
        Outer o = new Outer();
        Outer.Inner i = o.new Inner();
        i.run();//inner run
        i.outerRun();//Outer run
        i.outerRun2();//Outer run2
    }
}
class Outer {
    void run() {System.out.println("Outer run");}
    //即使是private的 内部类也可以直接调用
    private void run2() {System.out.println("Outer run2");}
    class Inner {
        //可以通过Outer.this去获得创建该内部类的对象的引用
        void outerRun() {Outer.this.run();}
        void run() {System.out.println("inner run");}
        //省略了Outer.this
        void outerRun2() {run2();}
    }
}
  • 这里使用了Outer.this这样的语法,还记得普通方法里面的this吗,通常我们可以省略this。只要没有歧义,内部类也支持这种省略。我们可以说内部类具有外围类的一切信息。

内部类与向上转型

  • 问题又来了,private的内部类有什么用?此外,为什么要使用内部类,我直接用两个类进行组合不好吗?
  • 这两个问题先放着,继续讨论内部类。我们知道对象可以向上转型,那么内部类对象也是可以这样的。我们可以让内部类实现一些接口,或者继承一个父类,这样的话,内部类的意义就体现出来了。
//接下来的例子都是针对这个例子进行修改。
public class Main {
    public static void main(String args[]) {
        Outer o = new Outer();
        Animal a = o;
        a.run();//animal run
        RunImpl r = o.getRun();
        r.run();//inner run
        EatImpl e = o.getEat();
        e.eat();//inner eat
        Sleeper s = o.getSleep();//从外部看,好像是o"继承"了Sleeper
        s.sleep();//inner sleep
    }
}
abstract class Animal {
    abstract void run();
}
abstract class Sleeper {
    abstract void sleep();
}
class Outer extends Animal {
    private class Inner extends Sleeper implements RunImpl, EatImpl {
        public void run() {
            System.out.println("inner run");
        }
        public void eat() {
            System.out.println("inner eat");
        }
        void sleep() {
            System.out.println("inner sleep");
        }
    }
    RunImpl getRun() {
        return new Inner();
    }
    EatImpl getEat() {
        return new Inner();
    }
    Sleeper getSleep() {
        return new Inner();
    }
    void run() {
        System.out.println("animal run");
    }
}
interface RunImpl {
    void run();
}
interface EatImpl {
    void eat();
}
  • 对外而言,Inner类是完全不可见的,但是我们却可以通过接口去间接使用它。由于Inner类具有Outer 类的所有信息,我们完全可以认为Outer 类实现了RunImpl和EatImpl接口。不仅如此,我们还可以认为Outer 类继承了Sleeper类,这样解决了多重继承的问题。有了这个方法以后我们写代码就可以更加方便了。这体现了一点内部类的意义。

在方法和作用域内的内部类

  • 上面一些使用内部类的方法都是比较简单的,我们还可以在方法或者任意的作用域内定义内部类。这样做是有原因的:
    1. 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
    2. 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。
  • 在后面的例子中,先前的代码将被修改,以用来实现:
    1. 一个定义在方法中的类。
    2. 一个定义在作用域内的类,此作用域在方法的内部。
    3. 一个实现了接口的匿名类。
    4. 一个匿名类,它扩展了有非默认构造器的类。
    5. 一个匿名类,它执行字段初始化。
    6. 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。

定义在方法中的类

  • 我们在刚才的例子中Outer类中加入以下方法:
RunImpl getRun2() {
    final int i = 5;
    //方法内部是没有访问权限修饰符的,这样被称为局部内部类
    class temp implements RunImpl {
        private int j = i;//要想引用方法内的对象(或基本类型),该类型必须是final的
        public void run() {
            System.out.println("temp run");
        }
    }
    return new temp();
}

    RunImpl r2 = o.getRun2();
    r2.run();//temp run
  • 这里有一个疑问,那就是:定义在方法内的类(其实下面的例子都有这个特性),想要引用方法作用域内的局部变量,则该变量必须是常量。这里说一下我的看法。按照我们的逻辑,在定义内部类时传入的形参值应该是在执行该类前的最后一次值。比如下面这个例子:
//这是个编译不通过的例子
RunImpl getRun2() {
    int a = 1;
    class temp implements RunImpl {
        public void run() {
            System.out.println(a);//我们会理所当然地认为输出1
            a++;
        }
    }
    a++;//按照我们的理解应该为3?
    return new temp();
}
  • 但是还记着之前讲过的生命周期吗?在getRun2()内,a是一个局部变量,它会随着方法的结束而销毁。但是内部类却不会被销毁(不然你怎么调用它的run()呢?)。我们可以说内部类的生命周期是长于方法中的局部变量的。那我们在内部类中引用一个已经被销毁的局部变量算哪门子事?其实在这里编译器悄悄在内部类的域中增加了一个成员,然后初始化它为局部变量的值。这样的话,我们就可以在内部类中去使用它,且不用担心它被销毁的问题了。但这种隐藏操作,开发人员是看不见的。在内部类中的变量和方法中的变量本质上是不同的,只是他们的值或者指向的对象是相同的。那么如果该变量不是final的,也就意味着它可以在方法内被修改。但是在方法的作用域内修改又不会影响内部类中那个隐藏的成员。这个特性很容易造成错误。所以Java干脆就禁止这种修改,规定其必须为final。大家有兴趣可以看看这边博文

定义在作用域内的类

  • 下面的例子展示了如何在任意的作用域内嵌入一个类:
RunImpl getRun3(boolean flag) {
    if (flag) {
        class temp implements RunImpl {
            public void run() {
                System.out.println("temp run");
            }
        }
        return new temp();
    }
    //return new temp();//error
    return null;
}
  • 和上一个例子相同,temp类出了if的作用域后就不能再使用了。不过有个地方需要注意,这里的语义不是说如果flagtrue就定义temp类。无论flag为什么,temp类始终会被编译将其定义在if作用域内意味着它只能在该作用域内被使用。

匿名类(剩余的内部类样例)

  • 如果一个内部类只在方法内被使用(事实上由于作用域的限制,其他地方也根本不可能使用它),那我们就可以写的更加简洁。看一个例子:
RunImpl getRun4() {
    return new RunImpl() {
        public void run() {
            System.out.println("runImpl run");
        }
    };
}
Animal getRun5() {
    return new Animal() {
        private String test = "1";//也可以定义变量
        public void run() {
            System.out.println("animal run" + test);
        }
    };
}
  • 这种语法指的是,创建一个继承自Animal(或者是实现了RunImpl接口)的匿名类的对象。通过new表达式返回的引用被自动向上转型为对AnimalRunImpl)的引用。
  • 上述两个例子都使用了默认的构造器,同样也可以使用父类的其他构造器来生成匿名类:
//扩展Animal类
abstract class Animal {
    String type;
    abstract void run();
    public Animal() {}
    public Animal(String type) {this.type = type;}
}
Animal getRun6() {
    return new Animal("miaoch") {
        public void run() {
            System.out.println(super.type + " run");//此处用super
            Outer.this.run();//不要混淆了内部类与外围类和子类与父类的引用关系。
        }
    };
}
  • 至于为什么只能借用父类的构造器,而不是自己重新定义构造器的原因是:它是匿名的,完全没办法编写构造器(不过也有一些类似的方法,下面会讨论)。
  • 如果是实现接口的话,就不能借用父类的(因为没有父类)非默认构造器了。但是我又的确需要传入某个值,那该怎么办呢?我们可以通过传入final局部变量,再使用类中的字段初始化的第二种方法。
RunImpl getRun4() {
    final String s = "2";
    final int i = 3;
    return new RunImpl() {
        //注意不要定义成同名s,至少按照我目前的知识无法获得局部变量s
        //private String s2 = s;//当逻辑不是很复杂的时候,直接初始化也可以。
        //当逻辑复杂的时候就可以用下面的方法模仿一个构造器
        private String s2;
        private int i2;//当逻辑复杂的时候就可以用下面的方法模仿一个构造器
        {
            s2 = s;
            i2 = i > 2 ? 2 : i;
            run();
        } //缺点是不能重载构造器
        public void run() {
            System.out.println(s2);
            System.out.println(i2);
        }
    };
}

嵌套类

  • 如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static,这通常称为嵌套类嵌套类意味着:
    1. 要创建嵌套类的对象,并不需要其外围类的对象。
    2. 不能从嵌套类的对象中访问非静态的外围类对象。
  • 我们来看一个嵌套类的例子:
class Outer extends Animal {
    //...省略大部分代码
    private static int a = 1;
    private int b = 2;
    private static class temp2 {
        static int c = 3;
        void method() {
            System.out.println(a);//相当于Outer.a
            System.out.println(b);//error
        }
    }
}
  • 普通的内部类是不能有static成员或方法的,这是因为普通的内部类对象需要依附于外围类对象。如果没有外围类对象,那static成员或方法也是无法直接访问的,这没有体现static存在的意义。但是嵌套类就没有这个限制,我们可以在嵌套类中定义static成员或方法
  • 再来说一下此处请求b报错的原因。其实原因很简单,b是一个非static的字段,而嵌套类对象的存在不需要外围类对象为前提(但是b的存在需要外围类对象为前提),所以嵌套类不能访问外围类的非static成员或方法
  • 当有多层嵌套类的时候,我们可以通过类名.嵌套类名.嵌套类名去编写代码。
class Outer extends Animal {
    //...省略大部分代码
    class A {
        class B {
        }
    }
    static class C {
        static class D {
        }
    }
    static class E {
        class F {
        }
    }
    //下面这种方式不被允许
    //class G {
        //static class H {
        //}
    //}
}
//main方法中测试代码:
Outer o = new Outer();
//----------------- 非static写法
Outer.A o_a = o.new A();
Outer.A.B o_a_b = o_a.new B();
//----------------- static写法
Outer.C o_c = new Outer.C();
Outer.C.D o_c_d = new Outer.C.D();
//----------------- 外围类static 内部类非static写法
Outer.E o_e = new Outer.E();
Outer.E.F o_e_f = o_e.new F();

接口中的嵌套类

  • 之前我们知道接口中的方法都是public的,接口中的成员都是staticfinalpublic的。如果在接口中写入一个内部类,无论有没有static关键字,都会被认为是嵌套类(也是public的,但不是final的)。嵌套类就不过多说明了。下面是一个用来测试的例子:
//通常我们会编写main方法去做测试,但是这会造成主类中增加了不必要的方法。
//我们可以用嵌套类的形式来放置测试代码,不需要的时候就可以将嵌套类的class文件去除
//main{intface$Temp}
public class intface {
    void run() {
        System.out.println("run");
    }
    static class Temp {
        public static void main(String args[]) {
            new intface().run();
        }
    }
}
//我们可以通过java intface$Temp 命令来进行测试,而部署项目的时候即可将该类文件移除。

一个关于内部类的综合例子

import java.util.ArrayList;
import java.util.List;

/**
 * 这是一个抽象事件类,利用ready()判断是否可以开始执行事件程序action()
 */
abstract class Event {
    private long eventTime;
    protected final long delayTime;
    public Event(long delayTime) {
        this.delayTime = delayTime;
        start();
    }
    public void start() {
        eventTime = System.currentTimeMillis() + delayTime;
    }
    public boolean ready() {
        return System.currentTimeMillis() >= eventTime;
    }
    public abstract void action();
    public abstract String toString();
}

/**
 * 这是一个控制器类,用于控制多个事件的执行
 */
class Controller {
    private List<Event> eventList = new ArrayList<Event>();
    public void addEvent(Event e) {
        eventList.add(e);
    }
    public void run() {
        //通过while将eventList中所有事件都执行一遍。我觉得此处开线程比较好。
        //因为这个方法的执行时间并不是很确定。不过只是一个测试,不用那么较真
        while (eventList.size() > 0) {
            //由于不能边遍历,边remove(且action中也可能包含addEvent操作),所以我们先复制一个副本,这种方式属于浅拷贝
            List<Event> copyList = new ArrayList<Event>(eventList);
            for (Event e : copyList) {
                if (e.ready()) {
                    System.out.println(e);
                    e.action();
                    eventList.remove(e);
                }
            }
        }
    }
}
/**
 * 这是一个扩展的控制器
 */
class GreenHouseControls extends Controller {
    private boolean light;//默认为false
    //这是一个开灯的内部类
    public class LightOn extends Event {
        public LightOn(long delayTime) {
            super(delayTime);
        }
        public void action() {
            light = true;
        }
        public String toString() {
            return "Light is on";
        }
    }
    //这是一个关灯的内部类
    public class LightOff extends Event {
        public LightOff(long delayTime) {
            super(delayTime);
        }
        public void action() {
            light = false;
        }
        public String toString() {
            return "Light is off";
        }
    }
    //这是一个响铃的内部类
    public class Bell extends Event {
        public Bell(long delayTime) {
            super(delayTime);
        }
        public void action() {
            addEvent(new Bell(delayTime));//意味着持续响铃
            //也可以写成下面这样:
            //start();
            //addEvent(this);
        }
        public String toString() {
            return "Bing!";
        }
    }
    //这是一个重复执行某些动作的内部类
    public class Restart extends Event {
        private Event[] eventlist;
        public Restart(long delayTime, Event[] eventlist) {
            super(delayTime);
            this.eventlist = eventlist;
        }
        public void action() {
            //这个事件是将指定的事件集加入eventList
            for (Event e: eventlist) {
                e.start();
                addEvent(e);
            }
            //然后再将自己的事件集加入eventList,以便重复执行
            //其实此处也可以写成 一句 addEvent(new Restart(delayTime, eventlist))
            start();
            addEvent(this);
        }
        public String toString() {
            return "Restart system";
        }
    }
    //程序控制类
    public static class Terminate extends Event {
        public Terminate(long delayTime) {
            super(delayTime);
        }
        public void action() {
            System.exit(0);
        }
        public String toString() {
            return "Terminating";
        }
    }
}
//这个是主类,一个测试类
public class GreenHouseController {
    public static void main(String args[]) {
        GreenHouseControls gc = new GreenHouseControls();
        gc.addEvent(gc.new Bell(1000L));
        Event[] eventList = new Event[] {
            gc.new LightOn(200L),
            gc.new LightOff(400L)
        };
        gc.addEvent(gc.new Restart(2000L, eventList));
        gc.addEvent(new GreenHouseControls.Terminate(10*1000L));
        gc.run();
    }
}

内部类的继承

  • 先讲一下内部类继承内部类。我们都知道内部类(非嵌套类)都需要外围类对象作为依附。因为创建子内部类对象需要先创建父内部类对象父内部类对象存在的条件是外围类对象必须存在。而子内部类存在的条件也是外围类对象必须存在,所以我们在创建子内部类对象时得先创建外围类对象,这样就没有什么问题。
  • 但是当一个非内部类要去继承一个内部类时(或者说只要他们不是同属于一个外围类),问题就出现了。我要想创建该非内部子类的对象,我就必须先创建外围类:
public class Test extends A1.A2 {
    public Test() {

    }
}
class A1 {
    class A2 {

    }
}
  • 这样必然是无法通过编译的。问题就出在必须先创建A1对象。那么我该如何创建A1对象呢?或者说我该如何让某个A1对象成为父类A2对象的外围类呢?
public class Test extends A1.A2 {
    public Test(A1 a1) {
        a1.super(1);
        //a1.super();
    }
    public static void main(String args[]) {
        A1 a = null;
        A1 b = new A1();
        new Test(b).run();//通过b.super()语法将b指定为父类的外围类对象
        //new Test(a);// thorw NPE
    }
}
class A1 {
    private int i = 123;
    class A2 {
        A2 (int i) {
            System.out.println("A2构造器: " + i);
        }
        A2 () {
        }
        void run() {
            System.out.println("通过特殊构造器建立了外围类关联: " + A1.this.i);
        }
    }
}
---------------
A2构造器: 1
通过特殊构造器建立了外围类关联: 123

内部类可以被覆盖吗

  • 自然是不行的,它与变量和方法是不一样的。假设你意图在子类中重新定义某个内部类,那他们也不过是存在于两个命名空间里面的名字刚好相同的类而已。有兴趣的同学可以做做例子,我这里就省略了。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值