on java 8: 第十一章 读书笔记

第十一章 内部类

一个定义在另个一个类中的类,叫做内部类。

原文

内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外部类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样(而且 Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求)。

我的理解

控制内部的类的可见性和代码隐藏机制其实是一种意思。内部类其实就是开发者想将代码隐藏起来,不让使用者轻易改变,从而增加程序的健壮性。

创建内部类

public class Parcel1 {
    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; }
    }
    // Using inner classes looks just like
    // using any other class, within Parcel1:
    public void ship(String dest) {
        Contents c = new Contents();
        Destination d = new Destination(dest);
        System.out.println(d.readLabel());
    }

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

输出:

Tasmania

Parcel1类中有两个类,ContentsDestination然后在ship方法中分别创健了两个类的对象c和d,d在调用readLabel方法输出传入的Tasmania字符串,这很好理解就是一个简单创建内部类再实例化使用的过程。但是上面创建的对象的引用并没有被返回,只是在内部使用。下面这个例子就通过方法返回对象的引用。

public class Parcel2 {
    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) {
        Parcel2 p = new Parcel2();
        p.ship("Tasmania");
        Parcel2 q = new Parcel2();
        // Defining references to inner classes:
        Parcel2.Contents c = q.contents();
        Parcel2.Destination d = q.to("Borneo");
    }
}

输出:

Tasmania

**to()contents()**方法都返回了内部类实例化后的对象。

如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 main() 方法中那样,具体地指明这个对象的类型:OuterClassName.InnerClassName。(译者注:在外部类的静态方法中也可以直接指明类型 InnerClassName,在其他类中需要指明 OuterClassName.InnerClassName。)

实例化某个内部类的对象的时候需要通过 “外部类.内部类”的方式获得内部类的类型,当然静态方法除外。

链接外部类

内部类可以访问外部对象的所有成员,而不需要任务特殊条件。此外内部类还可以访问外部类所有元素的访问权

interface Selector {
    boolean end();
    Object current();
    void next();
}
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 = 0;
        @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();
        }
    }
}

直接看上面的例子,可以看出这时候一个迭代器的例子。add方法可以序列增加新的元素。获取到Selector接口之后,还可以通过end方法判断是否是最后一个;current方法获取当前索引的元素;next方法判断是否还有下一个元素。

这里SequenceSelectorprivate类,所以很简单控制使用者对他的访问。然你会发现内部类都调用了items这个数组,这是外部类才有的元素,内部类可以轻易访问。所以当某个外部类的对象创建了一个内部类的对象的时候,此内部类对象就会秘密地捕获一个指向那个外部类对象的引用。在访问外部类成员的时候就是通过这个引用来选择外部类的成员的。但你=是内部类不能是static的,我想原因是static是和对象无关的。他在一开始就被加载进内存了,而此时可能对象还没被创建。

使用.this和.new

public class DotThis {
    void f() { System.out.println("DotThis.f()"); }

    public class Inner {
        public DotThis outer() {
            return DotThis.this;
            // A plain "this" would be Inner's "this"
        }
    }

    public Inner inner() { return new Inner(); }

    public static void main(String[] args) {
        DotThis dt = new DotThis();
        DotThis.Inner dti = dt.inner();
        dti.outer().f();
    }
}

输出

DOThis.f()

直接看main方法,先是创建DoThis对象,然后在创健内部类Inner的对象。最后内部类对象调用outer方法。outer方法使用了**.this返回了外部类对象的引用。最后才能调用f**方法。emmm…这就是绕了个圈。。。

public class DotNew {
    public class Inner {}
    public static void main(String[] args) {
        DotNew dn = new DotNew();
        DotNew.Inner dni = dn.new Inner();
    }
}

.new 就是通过外部类对象创建内部类对象。

内部类与向上转型

// innerclasses/Destination.java
public interface Destination {
    String readLabel();
}
// innerclasses/Contents.java
public interface Contents {
    int value();
}
// innerclasses/TestParcel.java
class Parcel4 {
    private class PContents implements Contents {
        private int i = 11;
        @Override
        public int value() { return i; }
    }
    protected final class PDestination implements Destination {
        private String label;
        private PDestination(String whereTo) {
            label = whereTo;
        }
        @Override
        public String readLabel() { return label; }
    }
    public Destination destination(String s) {
        return new PDestination(s);
    }
    public Contents contents() {
        return new PContents();
    }
}
public class TestParcel {
    public static void main(String[] args) {
        Parcel4 p = new Parcel4();
        Contents c = p.contents();
        Destination d = p.destination("Tasmania");
        // Illegal -- can't access private class:
        //- Parcel4.PContents pc = p.new PContents();
    }
}

Parcel4 中,内部类 PContentsprivate,所以除了 Parcel4,没有人能访问它。普通(非内部)类的访问权限不能被设为 private 或者 protected;他们只能设置为 publicpackage 访问权限。

内部类修饰为private进一步隐藏代码。其实想一想类和普通变量的区别,到了底层是不是可以认为都是指向也某个地址的变量,那段地址保存了某些信息。既然如此就可以设置为private

PDestinationprotected,所以只有 Parcel4 及其子类、还有与 Parcel4 同一个包中的类(因为 protected 也给予了包访问权)能访问 PDestination,其他类都不能访问 PDestination,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 private 内部类(或 protected 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 TestParcel 类中看到的那样。

也是在说因为修饰的原因你无法直接去新建内部类对象,只能是使用对外提供的方法获取其对象。

private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。

这句话我读了很久才有了些感悟。加黑的部分意思可能是,所有你需要的类你都在内部实现了,那么你这个类对外就是没有依赖的。依赖越少就代表出意外的可能越低。后面给编译器生成高效代码的机会意思就是既然不能增加原本不属于接口的方法。那么就可以认为这里是“确定”的,就意味着更少的变化。是不是这部分就可以直接替换为了底层的汇编,有点类似final的感觉。不知道这样理解对不对。。。。。

内部类方法和作用域

到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法重写了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。

感觉被打击到了,我反而觉得内部类很烦,不知道啥时候改用。

这么做有两个理由:

  1. 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
  2. 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

这个很好理解,定义内部类之后可以通过返回其引用这样可以隐藏代码。第二点也是隐藏代码,也可以减少依赖。

// innerclasses/Parcel5.java
// Nesting a class within a method
public class Parcel5 {
    public Destination destination(String s) {
        final class PDestination implements Destination {
            private String label;

            private PDestination(String whereTo) {
                label = whereTo;
            }

            @Override
            public String readLabel() { return label; }
        }
        return new PDestination(s);
    }

    public static void main(String[] args) {
        Parcel5 p = new Parcel5();
        Destination d = p.destination("Tasmania");
    }
}
public class Parcel6 {
    private void internalTracking(boolean b) {
        if(b) {
            class TrackingSlip {
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip() { return id; }
            }
            TrackingSlip ts = new TrackingSlip("slip");
            String s = ts.getSlip();
        }
        // Can't use it here! Out of scope:
        //- TrackingSlip ts = new TrackingSlip("x");
    }
    public void track() { internalTracking(true); }
    public static void main(String[] args) {
        Parcel6 p = new Parcel6();
        p.track();
    }
}

第一个例子实在方法内定义内部类,也被叫作局部内部类。

第二个例子在if里面定义,也就说跳出做个范围就不可以访问。

匿名内部类

public class Parcel7 {
    public Contents contents() {
        return new Contents() { // Insert class definition
            private int i = 11;

            @Override
            public int value() { return i; }
        }; // Semicolon required
    }

    public static void main(String[] args) {
        Parcel7 p = new Parcel7();
        Contents c = p.contents();
    }
}

创建类和定义的过程结合在一起了,所以这个类木有名字。我一开很奇怪Contents不就是名字吗?为什么说没什么名字,刚刚想到。一般的定义一个类都是如下这样。

public class Parcel7{}

你想想和上面一样这样我们在实例化的时候,是不是就可以通过构造传参。但是匿名类就不是,如下:

  return new Contents() { // Insert class definition
            private int i = 11;

            @Override
            public int value() { return i; }
        }; /

实际上**{ … }里面真正的定义类的部分,前年的Contents()他不是。那他是什么呢,为什么不写成return {}这样的形式呢。有js那味了,我又迷糊了。期望有人解答一下。但是我认为他应该就是类名吧,就像Parcel7{}Parcel17一样,而后的{}**也是类定义部分。

public class Parcel7b {
    class MyContents implements Contents {
        private int i = 11;
        @Override
        public int value() { return i; }
    }

    public Contents contents() {
        return new MyContents();
    }

    public static void main(String[] args) {
        Parcel7b p = new Parcel7b();
        Contents c = p.contents();
    }
}

上面这个例子就是把返回内部类的对象应用单独放到一个方法里面,本质差不多。

public class Parcel8 {
    public Wrapping wrapping(int x) {
        // Base constructor call:
        return new Wrapping(x) { // [1]
            @Override
            public int value() {
                return super.value() * 47;
            }
        }; // [2]
    }
    public static void main(String[] args) {
        Parcel8 p = new Parcel8();
        Wrapping w = p.wrapping(10);
    }
}

匿名内部类没有名字,自然就无法有构造函数,只有一个默认构造函数。但是如果基类也有参数怎么办?

public class Parcel8 {
    public Wrapping wrapping(int x) {
        // Base constructor call:
        return new Wrapping(x) { // [1]
            @Override
            public int value() {
                return super.value() * 47;
            }
        }; // [2]
    }
    public static void main(String[] args) {
        Parcel8 p = new Parcel8();
        Wrapping w = p.wrapping(10);
    }
}
  • [1] 将合适的参数传递给基类的构造器。
  • [2] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。
// innerclasses/Parcel9.java
public class Parcel9 {
    // Argument must be final or "effectively final"
    // to use within the anonymous inner class:
    public Destination destination(final String dest) {
        return new Destination() {
            private String label = dest;
            @Override
            public String readLabel() { return label; }
        };
    }
    public static void main(String[] args) {
        Parcel9 p = new Parcel9();
        Destination d = p.destination("Tasmania");
    }
}

将参数dest通过方法的形参的方式传过去 ,在内部类就可以使用过这个参数。但是参数必须是final这和闭包有关,java的闭包并不完美,所以必须是final。就算你不加,java编译器会自动加上final

如果是想做类似构造器的行为,可以这样做:

// innerclasses/AnonymousConstructor.java
// Creating a constructor for an anonymous inner class
abstract class Base {
    Base(int i) {
        System.out.println("Base constructor, i = " + i);
    }
    public abstract void f();
}
public class AnonymousConstructor {
    public static Base getBase(int i) {
        return new Base(i) {
            { System.out.println(
                    "Inside instance initializer"); }
            @Override
            public void f() {
                System.out.println("In anonymous f()");
            }
        };
    }
    public static void main(String[] args) {
        Base base = getBase(47);
        base.f();
    }
}

看如上代码直接用**{…}**把初始化代码放在里面就可以。其实这就是匿名类的构造器,只不过你无法重载。

输出:

Base constructor, i = 47
Inside instance initializer
In anonymous f()

嵌套类

嵌套类就是将内部类声明为static,通常称为嵌套类。

因为static关键字的原因很显然创建嵌套类对象的时候,不需要外部类的对象。不能从嵌套类的对象中访问非静态的外部对象(static的特性)

普通内部类和嵌套类有一个区别。普通内部类的字段和方法,只能放在类的外部层次,所以不能有static数据和static字段,但是嵌套类可以。

public class Parcel11 {
    private static class ParcelContents implements Contents {
        private int i = 11;
        @Override
        public int value() { return i; }
    }
    protected static final class ParcelDestination
            implements Destination {
        private String label;
        private ParcelDestination(String whereTo) {
            label = whereTo;
        }
        @Override
        public String readLabel() { return label; }
        // Nested classes can contain other static elements:
        public static void f() {}
        static int x = 10;
        static class AnotherLevel {
            public static void f() {}
            static int x = 10;
        }
    }
    public static Destination destination(String s) {
        return new ParcelDestination(s);
    }
    public static Contents contents() {
        return new ParcelContents();
    }
    public static void main(String[] args) {
        Contents c = contents();
        Destination d = destination("Tasmania");
    }
}

main 中,直接调用static修饰的方法返回ContentsDestination的引用。

一般内部类都有一个this引用可以链接到外部类对象,但是static没有,这就类似一个static方法一样和对象不相关。

接口内部的类

接口内的任何类自动都是publicstatic的。

其实很好理解,接口不能实例化,static和实例化的对象又无关。另外接口就是用来被实现的所以天然public。

// innerclasses/ClassInInterface.java
// {java ClassInInterface$Test}
public interface ClassInInterface {
    void howdy();
    class Test implements ClassInInterface {
        @Override
        public void howdy() {
            System.out.println("Howdy!");
        }
        public static void main(String[] args) {
            new Test().howdy();
        }
    }
}

输出:

Howdy!

如上代码你甚至可以在接口的内部类里面实现另一个接口。

从多层嵌套类中访问外部类的成员

class MNA {
    private void f() {}
    class A {
        private void g() {}
        public class B {
            void h() {
                g();
                f();
            }
        }
    }
}
public class MultiNestingAccess {
    public static void main(String[] args) {
        MNA mna = new MNA();
        MNA.A mnaa = mna.new A();
        MNA.A.B mnaab = mnaa.new B();
        mnaab.h();
    }
}

就和递归一样层层往下。

为什么需要内部类

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

再读这句话,才明白其实这有点类似多重继承。无论外部类继承或实现了什么类。内部类都是独立的,它不必去实现哪些类。这就像是外部类有了继承或者实现类的特性,同时内部类继承了别的类,这样外部类就有了两种类的特性。

interface A {}
interface B {}
class X implements A, B {}
class Y implements A {
    B makeB() {
        // Anonymous inner class:
        return new B() {};
    }
}
public class MultiInterfaces {
    static void takesA(A a) {}
    static void takesB(B b) {}
    public static void main(String[] args) {
        X x = new X();
        Y y = new Y();
        takesA(x);
        takesA(y);
        takesB(x);
        takesB(y.makeB());
    }
}

如果是使用过接口的话,就java8来说内部类其实没那么必要,因为接口本来就允许多实现。另外default关键字从某种程度上也允许了多重构继承。

但如果抽象类和类就不一样了,因为只允许继承一个类。

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

如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
  2. 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。 稍后就会展示一个这样的例子。
  3. 创建内部类对象的时刻并不依赖于外部类对象的创建
  4. 内部类并没有令人迷惑的"is-a”关系,它就是一个独立的实体。

第三点:

我一开始不太理解对于非嵌套类必须有外部类对象才能创建对象啊,为什么作者这样说呢?

The point of creation of the inner-class objects not tied to the creation of the outer-class object.
个人认为这句话应该理解为创建一个外部类的时候不一定要创建这个内部类,关键点应该是“时刻”的概念。

作者:小酱油
链接:https://www.zhihu.com/question/20969764/answer/20409150
来源:知乎

找到知乎上一个回答,我觉得这样理解比较对。

第四点:

内部类相对于外部类是独立,他可以有自己的实现和继承。

举个例子,如果 Sequence.java 不使用内部类,就必须声明"Sequence 是一个 Selector",对于某个特定的 Sequence 只能有一个 Selector,然而使用内部类很容易就能拥有另一个方法 reverseSelector(),用它来生成一个反方向遍历序列的 Selector,只有内部类才有这种灵活性。

意思就是有多个内部类实现同一个接口,这样实现的功能也就不一样。

闭包与回调

interface Incrementable {
    void increment();
}
// Very simple to just implement the interface:
class Callee1 implements Incrementable {
    private int i = 0;
    @Override
    public void increment() {
        i++;
        System.out.println(i);
    }
}
class MyIncrement {
    public void increment() {
        System.out.println("Other operation");
    }
    static void f(MyIncrement mi) { mi.increment(); }
}
// If your class must implement increment() in
// some other way, you must use an inner class:
class Callee2 extends MyIncrement {
    private int i = 0;
    @Override
    public void increment() {
        super.increment();
        i++;
        System.out.println(i);
    }
    private class Closure implements Incrementable {
        @Override
        public void increment() {
            // Specify outer-class method, otherwise
            // you'll get an infinite recursion:
            Callee2.this.increment();
        }
    }
    Incrementable getCallbackReference() {
        return new Closure();
    }
}
class Caller {
    private Incrementable callbackReference;
    Caller(Incrementable cbh) {
        callbackReference = cbh;
    }
    void go() { callbackReference.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.getCallbackReference());
        caller1.go();
        caller1.go();
        caller2.go();
        caller2.go();
    }
}

输出:

Other operation
1
1
2
Other operation
2
Other operation
3

这个例子进一步展示了外部类实现一个接口与内部类实现此接口之间的区别。就代码而言,Callee1 是更简单的解决方式。Callee2 继承自 MyIncrement,后者已经有了一个不同的 increment() 方法,并且与 Incrementable 接口期望的 increment() 方法完全不相关。所以如果 Callee2 继承了 MyIncrement,就不能为了 Incrementable 的用途而重写 increment() 方法,于是只能使用内部类独立地实现 Incrementable,还要注意,当创建了一个内部类时,并没有在外部类的接口中添加东西,也没有修改外部类的接口。

说下我的理解,当Callee2继承了MyIncrement之后就要实现increment()方法,但是这个行为肯定是MyIncrement所想的行为,你要想拥有Incrementable 的行为就可以使用内部类,其实就是多重继承的感觉。

注意,在 Callee2 中除了 getCallbackReference() 以外,其他成员都是 private 的。要想建立与外部世界的任何连接,接口 Incrementable 都是必需的。在这里可以看到,interface 是如何允许接口与接口的实现完全独立的。 内部类 Closure 实现了 Incrementable,以提供一个返回 Callee2 的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 Incrementable 的引用,都只能调用 increment(),除此之外没有其他功能(不像指针那样,允许你做很多事情)。

Caller 的构造器需要一个 Incrementable 的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Caller 对象可以使用此引用回调 Callee 类。

回调的价值在于它的灵活性-可以在运行时动态地决定需要调用什么方法。例如,在图形界面实现 GUI 功能的时候,到处都用到回调。

至于回调其实就是利用内部类有一个指向外部类的this,这样就可以在内部类保存外部类的属性,这就是闭包的定义。然后把引用传来传去就可以调用方法。

内部类与控制框架

应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并重写某些方法。你在重写的方法中写的代码定制了该应用程序框架提供的通用解决方案,来解决你的具体问题。这是设计模式中模板方法的一个例子,模板方法包含算法的基本结构,而且会调用一个或多个可重写的方法来完成该算法的运算。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可重写的方法就是变化的事物。

感觉就像是spring里面那一堆需要配置的类都是各种继承重载,一个是一个原理。在被继承的类里面已经定义好了调用顺序,你只需要在自定义的部分重写一下就行。

控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。

要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪ready()”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。下面是一个控制框架,它不包含具体的控制信息。那些信息是通过继承(当算法的 action() 部分被实现时)来提供的。

这里是描述了所有控制事件的接口。之所以用抽象类代替了真正的接口,是因为默认行为都是根据时间来执行控制的。也因此包含了一些具体实现:

import java.time.*; // Java 8 time classes
public abstract class Event {
    private Instant eventTime;
    protected final Duration delayTime;
    public Event(long millisecondDelay) {
        delayTime = Duration.ofMillis(millisecondDelay);
        start();
    }
    public void start() { // Allows restarting
        eventTime = Instant.now().plus(delayTime);
    }
    public boolean ready() {
        return Instant.now().isAfter(eventTime);
    }
    public abstract void action();
}

run() 方法循环遍历 eventList,寻找就绪的(ready())、要运行的 Event 对象。对找到的每一个就绪的(ready())事件,使用对象的 toString() 打印其信息,调用其 action() 方法,然后从列表中移除此 Event

注意,在目前的设计中你并不知道 Event 到底做了什么。这正是此设计的关键所在—"使变化的事物与不变的事物相互分离”。用我的话说,“变化向量”就是各种不同的 Event 对象所具有的不同行为,而你通过创建不同的 Event 子类来表现不同的行为。

这正是内部类要做的事情,内部类允许:

  1. 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的 action()
  2. 内部类能够很容易地访问外部类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。

考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重新启动系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类 Event 的多种派生版本。对于温室系统的每一种行为,都继承创建一个新的 Event 内部类,并在要实现的 action() 中编写控制代码。

作为典型的应用程序框架,GreenhouseControls 类继承自 Controller

public class GreenhouseControls extends Controller {
    private boolean light = false;
    public class LightOn extends Event {
        public LightOn(long delayTime) {
            super(delayTime); 
        }
        @Override
        public void action() {
            // Put hardware control code here to
            // physically turn on the light.
            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() {
            // Put hardware control code here to
            // physically turn off the light.
            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() {
            // Put hardware control code here.
            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() {
            // Put hardware control code here.
            water = false;
        }
        @Override
        public String toString() {
            return "Greenhouse water is off";
        }
    }
    private String thermostat = "Day";
    public class ThermostatNight extends Event {
        public ThermostatNight(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here.
            thermostat = "Night";
        }
        @Override
        public String toString() {
            return "Thermostat on night setting";
        }
    }
    public class ThermostatDay extends Event {
        public ThermostatDay(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            // Put hardware control code here.
            thermostat = "Day";
        }
        @Override
        public String toString() {
            return "Thermostat on day setting";
        }
    }
    // An example of an action() that inserts a
    // new one of itself into the event list:
    public class Bell extends Event {
        public Bell(long delayTime) {
            super(delayTime);
        }
        @Override
        public void action() {
            addEvent(new Bell(delayTime.toMillis()));
        }
        @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(); // Rerun each event
                addEvent(e);
            }
            start(); // Rerun this Event
            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";
        }
    }
}

注意,lightwaterthermostat 都属于外部类 GreenhouseControls,而这些内部类能够自由地访问那些字段,无需限定条件或特殊许可。而且,action() 方法通常都涉及对某种硬件的控制。

大多数 Event 类看起来都很相似,但是 BellRestart 则比较特别。Bell 控制响铃,然后在事件列表中增加一个 Bell 对象,于是过一会儿它可以再次响铃。读者可能注意到了内部类是多么像多重继承:BellRestartEvent 的所有方法,并且似乎也拥有外部类 GreenhouseContrlos 的所有方法。

一个由 Event 对象组成的数组被递交给 Restart,该数组要加到控制器上。由于 Restart() 也是一个 Event 对象,所以同样可以将 Restart 对象添加到 Restart.action() 中,以使系统能够有规律地重新启动自己。

下面的类通过创建一个 GreenhouseControls 对象,并添加各种不同的 Event 对象来配置该系统,这是 命令 设计模式的一个例子—eventList 中的每个对象都被封装成对象的请求:

public class GreenhouseController {
    public static void main(String[] args) {
        GreenhouseControls gc = new GreenhouseControls();
        // Instead of using code, you could parse
        // configuration information from a text file:
        gc.addEvent(gc.new Bell(900));
        Event[] eventList = {
                gc.new ThermostatNight(0),
                gc.new LightOn(200),
                gc.new LightOff(400),
                gc.new WaterOn(600),
                gc.new WaterOff(800),
                gc.new ThermostatDay(1400)
        };
        gc.addEvent(gc.new Restart(2000, eventList));
        gc.addEvent(
                new GreenhouseControls.Terminate(5000));
        gc.run();
    }
}

上面这个例子在书里面很长,我觉得理解起来其实不难。一个温室里面的各种功能都是一个类都是一个事件,他们都继承了Event但是所实现的行为不同,每个action都是自定义的功能。而且内部的这些类可以轻易访问外部类的属性,这样也减少了依赖。上面这个类将每个Event添加进一个序列配置该系统。最后当时间发生的时候可以一个个调用,有点类似命令模式的感觉。

输出:

Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Greenhouse water is off
Bing!
Thermostat on day setting
Bing!
Restarting system
Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Bing!
Greenhouse water is off
Thermostat on day setting
Bing!
Restarting system
Thermostat on night setting
Light is on
Light is off
Bing!
Greenhouse water is on
Greenhouse water is off
Terminating

继承内部类

因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:

// innerclasses/InheritInner.java
// Inheriting an inner class
class WithInner {
    class Inner {}
}
public class InheritInner extends WithInner.Inner {
    //- InheritInner() {} // Won't compile
    InheritInner(WithInner wi) {
        wi.super();
    }
    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner ii = new InheritInner(wi);
    }
}

因为内部类有只想外部类的引用,这个引用也必须被初始化,所以要传入一个外部类对象的引用,先初始化外部类。

内部类可以重写吗?

如果创建了一个内部类,然后继承其外部类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被重写吗?这看起来似乎是个很有用的思想,但是“重写”内部类就好像它是外部类的一个方法,其实并不起什么作用:

// innerclasses/BigEgg.java
// An inner class cannot be overridden like a method
class Egg {
    private Yolk y;
    protected class Yolk {
        public Yolk() {
            System.out.println("Egg.Yolk()");
        }
    }
    Egg() {
        System.out.println("New Egg()");
        y = new Yolk();
    }
}
public class BigEgg extends Egg {
    public class Yolk {
        public Yolk() {
            System.out.println("BigEgg.Yolk()");
        }
    }
    public static void main(String[] args) {
        new BigEgg();
    }
}

输出:

New Egg()
Egg.Yolk()

把内部类当做方法来重载,实际什么效果都木有。这两个内部类都是独立的个体,都在各自的命名空间内,当然可以明确的继承某个内部类。

// innerclasses/BigEgg2.java
// Proper inheritance of an inner class
class Egg2 {
    protected class Yolk {
        public Yolk() {
            System.out.println("Egg2.Yolk()");
        }
        public void f() {
            System.out.println("Egg2.Yolk.f()");
        }
    }
    private Yolk y = new Yolk();
    Egg2() { System.out.println("New Egg2()"); }
    public void insertYolk(Yolk yy) { y = yy; }
    public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
    public class Yolk extends Egg2.Yolk {
        public Yolk() {
            System.out.println("BigEgg2.Yolk()");
        }
        @Override
        public void f() {
            System.out.println("BigEgg2.Yolk.f()");
        }
    }
    public BigEgg2() { insertYolk(new Yolk()); }
    public static void main(String[] args) {
        Egg2 e2 = new BigEgg2();
        e2.g();
    }
}

输出

Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()

局部内部类

// innerclasses/LocalInnerClass.java
// Holds a sequence of Objects
interface Counter {
    int next();
}
public class LocalInnerClass {
    private int count = 0;
    Counter getCounter(final String name) {
        // A local inner class:
        class LocalCounter implements Counter {
            LocalCounter() {
                // Local inner class can have a constructor
                System.out.println("LocalCounter()");
            }
            @Override
            public int next() {
                System.out.print(name); // Access local final
                return count++;
            }
        }
        return new LocalCounter();
    }
    // Repeat, but with an anonymous inner class:
    Counter getCounter2(final String name) {
        return new Counter() {
            // Anonymous inner class cannot have a named
            // constructor, only an instance initializer:
            {
                System.out.println("Counter()");
            }
            @Override
            public int next() {
                System.out.print(name); // Access local final
                return count++;
            }
        };
    }
    public static void main(String[] args) {
        LocalInnerClass lic = new LocalInnerClass();
        Counter
                c1 = lic.getCounter("Local inner "),
                c2 = lic.getCounter2("Anonymous inner ");
        for(int i = 0; i < 5; i++)
            System.out.println(c1.next());
        for(int i = 0; i < 5; i++)
            System.out.println(c2.next());
    }
}

输出

LocalCounter()
Counter()
Local inner 0
Local inner 1
Local inner 2
Local inner 3
Local inner 4
Anonymous inner 5
Anonymous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9

局部内部类不能有访问说明符,因为他是方法的一部分不是外部类的一部分。但是他可以访问当前代码块的常量,以及此外部类的所有成员。

Counter 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化。

所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。

内部类标识符

由于编译后每个类都会产生一个 .class 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 Class 对象)。

你可能猜到了,内部类也必须生成一个 .class 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外部类的名字,加上 “$” ,再加上内部类的名字。例如,LocalInnerClass.java 生成的 .class 文件包括:

Counter.class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class

如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与 “$” 的后面。

虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况[^1]。因为这是 Java 的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。)

很好理解!!!

小结

比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如 C++ 就没有这些。将两者结合起来,同样能够解决 C++ 中的用多重继承所能解决的问题。然而,多重继承在 C++ 中被证明是相当难以使用的,相比较而言,Java 的接口和内部类就容易理解多了。

虽然这些特性本身是相当直观的,但是就像多态机制一样,这些特性的使用应该是设计阶段考虑的问题。随着时间的推移,读者将能够更好地识别什么情况下应该使用接口,什么情况使用内部类,或者两者同时使用。但此时,读者至少应该已经完全理解了它们的语法和语义。

当读者见到这些语言特性的实际应用时,就能最终理解它们了。

个人觉得内部类好复杂,目前来说知道他的定义和语法。以及一些基本的作用但是落实到实际运用估计还差得远。

随笔

编程还是多写,当你突然那天觉得自己的设计不行的时候,很多时候设计模式和上文这些东西就会浮现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值