Java 编程思想笔记:Learn 7

10.7 嵌套类

如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 static。这通常称为嵌套类。要想理解 static 应用于内部类的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是 static 时,就不是这样:

  • 要创建嵌套类的对象,并不需要其外围类的对象
  • 不能从嵌套类的对象中访问非静态的外围类对象
  • 普通内部的字段和方法,只能放在累的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类可以包含这些东西。
interface Contents{
    int value();
}

interface Destination{
    String readLabel();
}

public class Parcel11 {
    private static class ParcelContents implements Contents{
        private int i = 11;
        public int value(){
            return i;
        }
    }

    protected static class ParcelDestination implements Destination{
        private String label;

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

        public String readLabel(){
            return label;
        }

        public static void f(){}

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


class parcel11b {
    class p{
        private  Integer a = 1;
    }
}

10.7.1 接口内部的类

正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 public 和 static。因为类是 static的,只是将嵌套类置于接口的命名空间内,这并不违反接口规则。你甚至可以在内部类中实现其外部接口:

public interface ClassInterface{
    void howdy();
    class Test implements ClassInterface{
        public void howdy(){
            System.out.println("Howdy");
      }

      public static void main(String[] args){
            new Test().howdy();
      }
  }
}

如果你想创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。

我曾在本书中建议过,在每个中都写一个 main 方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已经编译过的额外代码。如果觉得这是个麻烦,可以使用嵌套类来放置测试代码

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

  public static clss Tester{
      public static void main(String[] args){
        TestBed t = new TestBed();
        t.f();
    }
  }
}

这会生成一个独立的类,TestBed$Tester,要运行这个类执行 java TestBed$Tester 即可。

在 Uinx / linux 中需要加 \ 转义 $。反编译查看文件命令,javap TestBed$Tester

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

一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员,如下所示:

public class MNA {
    private void f() {}

    class A{
        private void g(){}

        public class B{
            void h(){
                g();
                f();
            }
        }
    }
}

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

可以看到在 MNA.A.A.B 中,调用方法 g() 和 f() 不需要任何条件(即使他们被定义成 private)。这个例子同时展示了如何从不同的类里面创建多层嵌套的内部类对象的基本语法。“.new” 语法能产生正确的作用域,所以不必在调用构造器时限定类名。

10.8 为什么需要内部类

为什么需要内部类?
一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。

如果只是对一个接口的引用,为什么不通过外围类实现那个接口呢?
如果这能满足需求,那么就应该通过外围类实现接口。

内部类实现一个接口与外围类实现这个接口有什么区别呢?
外围类不总能享受到接口带来的便利,有时需要用到接口的实现。

所以,内部类的最佳使用理由是:

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

如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部使得多重继承的解决方案变得完整。接口解决了部分问题,内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型。

有这样一种情况,必须在一个类中以某种方式实现两个接口。

// 使用外部类实现接口
interface AA {}

interface B {}

class X implements AA, B{}

class Y implements AA{
    B makeB(){
        return new B() {};
    }
}

public class MultiIntefaces {
    static void taskA(AA a){}

    static void taskB(B b){}

    public static void main(String[] args){
        X x = new X();
        Y y = new Y();
        taskA(x);
        taskA(y);
        taskB(x);
        taskB(y.makeB());
    }
}

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

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
  2. 在单个外围类中,可以让多个内部类以不同的方式创建同一个接口,或继承同一个类。
  3. 创建内部类对象的时刻并不依赖于外围类对象的创建
  4. 内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体。举例,如果 Sequence.java 不使用内部类,就必须声明 "Sequence 是一个 Selector", 对于某个特点的 Sequence 只能有一个 Selector。然而使用内部类很容易就能拥有另外一个方法 reverseSelector(), 用它来生成一个反方向遍历序列的 Selector。只有内部类才有这种灵活性。

10.8.1 闭包与回调

闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员`。

Java 最引人争议的问题之一就是,人们认为 Java 应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。稍后会看到这个是一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用指针。然而,Java没有在语言中包括指针。

通过内部类提供的闭包功能是回调的优良解决方案,比指针更安全、灵活。

interface Incrementable{
    void increment();
}

class Callee1 implements Incrementable{
    private int i = 0;
    public void increment(){
        i++;
        System.out.println(i);
    }
}

class MyIncrement{
    public void increment(){
        System.out.println("Other operation");
    }

    // MyIncrement 及其子类都可以调用该方法
    // 并且 increment 被它的子类重写了
    static void f(MyIncrement myIncrement){
        myIncrement.increment();
    }
}

class Callee2 extends MyIncrement{
    private int i = 0;

    public void increment(){
        super.increment();
        i++;
        System.out.println(i);
    }

    private class Closure implements Incrementable{
        public void increment(){
            Callee2.this.increment();
        }
    }

    Incrementable getCallbackReference(){
        return new Closure();
    }
}

class Caller{
    private Incrementable incrementable;

    Caller(Incrementable incrementable){
        this.incrementable = incrementable;
    }

    void go(){
        incrementable.increment();
    }
}

public class Callbacks {
    public static void main(String[] args){
        Callee1 c1 = new Callee1();
        Callee2 c2 = new Callee2();
        // 在这点击代码跳转的不对,c2 跳转的是父类的 increment,其实 c2 把 increment 重写了
        MyIncrement.f(c2); // "Other operation"
        Caller caller1 = new Caller(c1);
        Caller caller2 = new Caller(c2.getCallbackReference());
        caller1.go(); // 1
        caller1.go(); // 2
        caller2.go(); // "Other operation"
                      // 1
        caller2.go();
                      // "Other operation"
                      // 2
    }
}

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

注意,在 Callee2 中除了 getCallbackReference() 以外,其他成员都是 private。想要建立与外部世界的任何连接, interface Incrementable 都是必须的。在这里看到,interface 允许接口与接口的实现完全独立。

内部类 Closure 实现了 Incrementable, 以便提供一个返回 Callee2 的钩子,而且是一个安全的钩子。无论谁获得了此 Incrementable 的引用,都只能调用 increment(), 除此之外没有其他的功能。

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

回调的价值在于它的灵活性,可以在运行时动态地决定需要调用什么方法。

10.8.2 内部类与控制框架

在将要介绍的控制框架(control framework) 中,可以看到更多使用内部类的具体例子。

应用程序框架(application framework) 就是被设计用来解决某类特定问题的一个类或者一组类。要运用某个应用程序框架,通常是需要继承一个或多个类,并覆盖某个方法。在覆盖后的方法中,编写代码定制应用程序提供的通用解决方案,以解决你特定的问题(这是设计模式中模板方法的一个例子)。

模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是把将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。

控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要是用来响应事件的系统被称为事件驱动系统。应用程序设计中常见的问题之一就是图形用户接口(GUI),它几乎全是事件驱动的系统。在第 22 章中会看到,Java Swing 库就是一个控制框架,它解决了GUI的问题,并使用了大量的内部类。

考虑这样一个控制框架,它的工作就是在事件 “就绪” 的时候执行事件。虽然 “就绪”可以指任何事,但是在本例中是指基于时间触发的事件。接下来的问题是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的 action 部分时,通过继承来提供。

首先,接口描述了要控制的事件。因为默认的行为是基于时间去执行控制,所以使用抽象类代替实际的接口。下面的例子包含了某些实现:

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() 时,那么构造器就会捕获( 从对象创建的时刻开始的) 时间,此时间是这样得来的: start() 获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。start() 是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用 Event 对象。例如,如果想重复使用 Event 对象。例如,如果想要重复一个事件,只需要简单地在 action() 中调用 start() 方法。

ready() 告诉你何时可以运行 action() 方法。当然,可以在导出类中覆盖 ready() 方法,使得 Event 能够基于时间以外的其他因素而触发。

下面的文件包含了一个用来管理并触发事件的实际控制器。 Event 对象保存在 List<Event> 类型的容器对象中。
List<Event> 类型的容器:

  • 可以使用 add() 方法将一个 Object 添加到 List 的尾部,
  • size() 方法用来得到 List 中元素的个数
  • foreach 语法迭代
  • remove 用来删除指定的 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();
}


class Controller{

    private List<Event> events = new ArrayList<>();

    public void addEvent(Event c){
        events.add(c);
    }

    public void run(){
        while (events.size() > 0){
            for(Event e : new ArrayList<>(events)){
                if(e.ready()){
                    System.out.println(e);
                    e.action();
                    events.remove(e);
                }
            }
        }
    }
}

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

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

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

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

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


10.9 内部类的继承

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

class WithInner{
  class Inner{}
}


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容

  • 第一章 对象导论 对象具有状态、行为和标识。这意味着每一个对象都可以拥有内部数据和方法,并且每一个对象都可以唯一地...
    niaoge2016阅读 821评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,622评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 听说,你特喜欢《疯狂动物城》,那这十个镜头你有没有注意呢? 镜头一: 朱迪被牛局长派去当交警,开罚单。朱迪为了证明...
    西小东阅读 9,718评论 3 2
  • 雅礼中学 1602班 周若木 他耷拉着脑袋,竭力想写出一些优美的句子。他呆在桌前,幻想着能拿奖的壮观场面。他兴奋而...
    聽濤阁阅读 1,910评论 0 9