Java基础——内部类

可以将一个类的定义放在另一个类的定义内部,这就是内部类。

使用内部类的好处:

  • 允许你把一些逻辑相关的类组织在一起,并且可以控制内部类的对外访问权限
  • 内部类是一种代码隐藏机制,同时内部类还了解外部类并且能够与外部类进行通信

1. 创建内部类

创建内部类的方式就是将内部类的定义放在外部类的定义中:

public class Parcel {
    class Contents {
        private int i = 11;
        public int value() {
            return i;
        }
    }
    class Destination {
        private String label;
        Destination(String whereTo){
            label = whereTo;
        }
        String readLabel() {
            return label;
        }
    }
    public Destination to(String s) {
        return new Destination(s);
    }
    public Contents contents() {
        return new Contents();
    }
    public void ship(String dest) {
        Contents c = contents();
        Destination d = to(dest);
        System.out.println(d.readLabel());
    }

    public static void main(String[] args) {
        Parcel p = new Parcel();
        p.ship("Tasmania");
        Parcel q = new Parcel();

        // 内部类对象的引用格式
        Parcel.Contents c = q.contents();
        Parcel.Destination d = q.to("XTU");
        // 由于是在外部类的main方法中,下面的这两句与上面等价
        // Contents c = q.contents();
        // Destination d = q.to("XTU");
    }
}

/* output
Tasmania
*///

上述类中有一个典型的情况,外部类有一个方法,该方法将返回一个指向内部类的引用(to方法和contents方法)。那为什么需要这样的方法呢?

内部类的访问权限一般情况下都是包访问权限或者私有的类方法权限,这个时候在包的外部我们就不能够通过内部类的构造方法来得到内部类的对象。为了得到内部类的对象,我们需要在外部类中提供接口,接口返回指向内部类的引用。

注意:在外部类中,我们可以通过InnerClassName指明内部类对象的类型。但是在其它的类中,如果想要指明某个内部类对象的类型,必须声明为OuterClassName.InnerClassName

2. 链接到外部类

当我们生成一个内部类的对象时,该对象会和制造它的外围对象之间建立一种联系,借助这个联系,它可以轻松访问到其外围对象的所有成员。除此之外,内部类还拥有其外围类的所有元素的访问权。

public class Sequence {
    private Object[] items;
    private int next = 0;

    public Sequence(int size) {
        items = new Object[size];
    }
    public void add(Object x) {
        if(next < items.length) {
            items[next++] = x;
        }
    }
    private class SequenceSelector implements Selector {
        private int i;
        @Override
        public boolean end() {
            return i == items.length;
        }

        @Override
        public Object current() {
            return items[i];
        }

        @Override
        public void next() {
            if (i < items.length) {
                i++;
            }
        }
    }
    
    public Selector selector() {
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for (int i = 0; i < 10; i++) {
            sequence.add(Integer.toString(i));
        }
        Selector selector = sequence.selector();
        while (!selector.end()) {
            System.out.print(selector.current() + " ");
            selector.next();
        }
    }
}
/* output
0 1 2 3 4 5 6 7 8 9 
*///

上述的例子很好的体现了内部类拥有其外围类的所有元素访问权的特性。内部类SequenceSelector是私有的,为了得到它的对象,我们只能通过调用外部类selector接口。得到的这个对象会和制造它的外部类对象绑定在一起,这个时候通过它可以遍历外部类对象的items数组,这是**“迭代器”设计模式**的一个例子。

那么内部类是如何自动拥有其外部类的所有成员访问权呢?

当某个外部类的对象创建一个内部类对象时,此内部类对象必定会秘密捕捉一个指向那个外部类对象的引用。然后当你在访问外部类元素时,就是那个引用来选择外部类的元素。但幸运的是编译器会帮我们处理掉所有的细节。因此我们可以看到:内部类的对象只有在与其外部类对象关联时才能被创建(内部类是非static时),用时内部类对象中需要有一个指向其外部类对象的引用,如果编译器访问不到这个引用就会报错,不过这些都不需要程序员去操心。

3. .this和.new

如果想要在内部类的定义中获得当前外部类对象的引用,可以使用外部类名字.this的形式获得。这样产生的引用会自动具有正确的类型,同时该点是在编译期间被知晓和检查,因此没有任何运行时开销。

如果你想要调用构造方法创建某个内部类(非static类)的对象,首先你需要有一个外部类对象,然后外部类对象.new 内部类名字()的形式创建内部类对象,此时内部类对象会暗暗地连接到创建它的外部类对象身上。

4. 匿名内部类

  • 一般写法:new 接口名字或抽象类名(构造方法有参数传参数){ 匿名内部类的具体实现};new的这个匿名内部类的对象引用会被自动向上转型为接口或抽象类的引用。

  • A getA(Object s){
        return new A(){
          // 如果这个s不是用在A的构造器中,而是用在类中其它地方。那么编译器认为这个参数s是final的,
          // 只能读不能写,不能将s指向其它的Object,读者可以尝试用对象引用和方法间传参数的知识推测
          // 为什么要这样设计
          // s = new String("123");    编译器报错
        };
    }
    
  • 匿名内部类中是不能有构造器存在的,原因也很简单,没得名字哪来的显示构造器

匿名内部类与工厂模式的美妙联动

interface Service {
    void method1();
    void method2();
}
interface ServiceFactory {
    Service getService();
}

class Implementation1 implements Service{
    private Implementation1(){}
    @Override
    public void method1() {
        System.out.println("Implementation1 method1");
    }

    @Override
    public void method2() {
        System.out.println("Implementation1 method2");
    }
    
    public static ServiceFactory factory = new ServiceFactory() {
        @Override
        public Service getService() {
            return new Implementation1();
        }
    };
}
class Implementation2 implements Service{
    private Implementation2(){}
    @Override
    public void method1() {
        System.out.println("Implementation2 method1");
    }

    @Override
    public void method2() {
        System.out.println("Implementation2 method2");
    }
    public static ServiceFactory factory = new ServiceFactory() {
        @Override
        public Service getService() {
            return new Implementation2();
        }
    };
}
public class Factories {
    public static void serviceConsumer(ServiceFactory factory){
        Service service = factory.getService();
        service.method1();
        service.method2();
    }
    public static void main(String[] args) {
        Factories.serviceConsumer(Implementation1.factory);
        Factories.serviceConsumer(Implementation2.factory);
    }
}
/* output
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
*///

上例中,Implementation1类和Implementation2类的构造方法是私有的。此时,为了得到它们的对象,可以也只能利用factory的getService方法得到。同时,对于某一种具体的服务,在实际上应该只有一座工厂去单独创建这一样的服务。因此它被创建为具体服务中的一个static域。

优先使用类而不是接口。如果你的设计中需要某个接口,你必须了解它。否则,不到迫不得已,不能将其放到你的设计中。

5. 嵌套类

如果不需要内部类对象与其外部类对象之间有联系,可以将内部类声明为static。这种情况的内部类通常称为嵌套类

普通的内部类对象隐式地保存了一个指向创建它的外部类对象的引用。然而,当内部类为static时,情况就不一样了。

  • 不需要依靠外部类的对象创建嵌套类的对象
  • 不能从嵌套类的对象中访问非静态的外围类元素

注意:普通的内部类不能拥有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有的东西。

接口内部的类

嵌套类可以作为接口的一部分,因为放在接口中的任何类都自动是public和static的。同时放在接口中的类可以被接口的所有不同实现共用,这就意味着类中可以写你想要被所有实现包括的代码。

6. 为什么需要内部类

每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口)的实现,对于内部类都没有影响。

利用内部类,可以间接地解决“多重继承”自多个类的问题。

class D{}
abstract class E{}
class Z extends D{
    E makeE(){
        return new E(){};
    }
}
public class MultiImplementation{
    static void takesD(D d){}
    static void takesE(E e){}
    public static void main(String[] args){
        Z z = new Z();
        takesD(z);
        takesE(z.makeE());
    }
}

如果使用内部类,还可以获得其它一些特性:

  • 内部类可以有多个实例,每个实例都有自己的状态信息,并且与外部类对象的信息相互独立
  • 在当个外部类中,可以让多个内部类以不同的方式实现同一个接口或继承自同一个类
  • 内部类并没有令人疑惑的“is-a”关系,它就是一个独立的个体。

6.1 闭包与回调

闭包是指一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包。

通过回调,对象能够携带一些信息,这些信息运行它在稍后的某个时刻调用初始的对象。在Java中简单来讲就是在某个类设置一个成员变量为回调对象,然后这个类的对象在某个特定时刻调用这个回调对象的某个方法。

interface Incrementable{
    void increment();
}
class Callee1 implements Incrementable{
    private int i = 0;
    @Override
    public void increment() {
        System.out.println(++i);
    }
}
class MyIncrement {
    public void increment() {
        System.out.println("Other operation");
    }
    public static void f(MyIncrement mi){
        mi.increment();
    }
}
class Callee2 extends MyIncrement{
    private int i = 0;

    @Override
    public void increment() {
        super.increment();
        System.out.println(++i);
    }
    private class Closure implements Incrementable{
        @Override
        public void increment() {
            Callee2.this.increment();
        }
    }
    Incrementable getCallbackRefrence() {
        return new Closure();
    }
}
class Caller {
    private Incrementable callbackRef;

    public Caller(Incrementable callbackRef) {
        this.callbackRef = callbackRef;
    }
    void go() {
        callbackRef.increment();
    }
}
public class Callbacks {
    public static void main(String[] args) {
        Callee1 c1 = new Callee1();
        Callee2 c2 = new Callee2();
        MyIncrement.f(c2);
        Caller caller1 = new Caller(c1);
        Caller caller2 = new Caller(c2.getCallbackRefrence());
        caller1.go();
        caller1.go();
        caller2.go();
        caller2.go();
    }
}
/* output
Other operation
1
1
2
Other operation
2
Other operation
3
*///

在上面这个例子可以进一步看到外部类实现一个接口和内部类实现一个接口的区别,同时也可以看到回调的使用。名称为callbackRef的引用是Caller类的一个回调对象,当你调用Caller对象的go方法时,它会回调callbackRef引用的increment方法。

6.2 内部类和控制框架

应用程序框架就是被设计用以解决某特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖其中的某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案。

设计模式总是将变化的事物与保持不变的事物分离开,在模版方法中,模版方法是保持不变的事物,而可覆盖的方法就是变化的事物。

控制框架就是一类特殊的应用程序框架,它用来解决响应事件的需求(典型的如Swing)。它的工作就是在事件“就绪”的时候执行事件,但是控制框架并不提供具体的执行内容,执行内容通过继承提供。

Event类:

public abstract class Event {
    private long eventTime;
    protected final long delayTime;

    public Event(long delayTime) {
        this.delayTime = delayTime;
        start();
    }
    public void start(){
        eventTime = System.nanoTime() + delayTime;
    }
    public boolean ready(){
        return System.nanoTime() >= eventTime;
    }
    public abstract void action();
}

Event是一个抽象类,它可以由任意具体的事件继承。同时,在创建一个事件的具体对象时,会调用start方法,意味着启动计时器,ready方法判断是否已经到了可以响应事件的时间,action方法是响应事件的具体逻辑,在ready方法返回true时才能执行。

管理并触发事件的实际控制框架:Controler类

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

public class Controler {
    private List<Event> eventList = new ArrayList<>();
    public void addEvent(Event c){
        eventList.add(c);
    }
    public void run() {
        while (eventList.size() > 0) {
            // 容器中不能够边遍历边移除
            for(Event e : new ArrayList<>(eventList)) {
                if(e.ready()){
                    System.out.println(e);
                    e.action();
                    eventList.remove(e);
                }
            }
        }
    }
}

可以通过addEvent方法向该框架添加事件,同时run方法循环遍历事件列表,寻找就绪的可以运行的Event对象,调用这个对象的toString()方法将这个对象打印出来并且调用它的action()方法,最后将它从事件列表中移除。

在目前的设计中,我们还不知道Event到底做了什么。而这也这是设计的关键,“使变化的事物与不变的事物相互分离”,“变化向量”就是各种不同的Event对象所具有的不同行为,而这也是创建不同的Event子类所做的事情。

  • 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装起来。内部类用来解决问题所必须的各种不同的action()。
  • 内部类能够很容易地访问外部类的任意成员,所以可以很便捷地写出对应的代码
public class GreenhouseControls extends Controler{
    private boolean light = false;
    public class LightOn extends Event {
        public LightOn(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            light = true;
        }

        @Override
        public String toString() {
            return "Light is on";
        }
    }
    public class LightOff extends Event {
        public LightOff(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            light = false;
        }

        @Override
        public String toString() {
            return "Light is off";
        }
    }
    private boolean water = false;
    public class WaterOn extends Event {
        public WaterOn(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            water = true;
        }

        @Override
        public String toString() {
            return "GreenHouse Water is on";
        }
    }
    public class WaterOff extends Event {
        public WaterOff(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            water = false;
        }

        @Override
        public String toString() {
            return "GreenHouse Water is off";
        }
    }
    public class Bell extends Event{
        public Bell(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            addEvent(new Bell(delayTime));
        }

        @Override
        public String toString() {
            return "Bing!";
        }
    }
    public class Restart extends Event{
        private Event[] eventList;
        public Restart(long delaytime, Event[] eventList) {
            super(delaytime);
            this.eventList = eventList;
            for(Event e : eventList) {
                addEvent(e);
            }
        }

        @Override
        public void action() {
            for(Event e : eventList) {
                e.start();
                addEvent(e);
            }
            start();
            addEvent(this);
        }

        @Override
        public String toString() {
            return "Restarting system";
        }
    }
    public static class Terminate extends Event {
        public Terminate(long delayTime) {
            super(delayTime);
        }

        @Override
        public void action() {
            System.exit(0);
        }

        @Override
        public String toString() {
            return "Terminating";
        }
    }
}

// 命令设计模式的一个实例
public class GreenhouseController {
    public static void main(String[] args) {
        GreenhouseControls gc = new GreenhouseControls();
        gc.addEvent(gc.new Bell(900));
        Event[] eventList = {
                gc.new LightOn(200),
                gc.new LightOff(200),
                gc.new WaterOn(200),
                gc.new WaterOff(200)
        };
        gc.addEvent(gc.new Restart(2000, eventList));
        // 停不下来
        gc.run();
    }
}

从上面的例子可以看出内部类的价值所在,特别是在控制框架中使用内部类的时候,非常优雅。

使用局部内部类而不使用匿名内部类的原因:

  • 我们需要一个已命名的构造器或者是需要重载的构造器
  • 我们需要不止一个该内部类的对象

如果有时间的话,强烈建议有一定Java基础的读者去啃Java编程思想这本书,虽然字多书小,但这本书绝对是充实Java基础知识以及进阶的不二之选。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZW钟文

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值