issues
search
funnycoding
/
blog
A Github Issue Blog
22
stars
0
forks
source link
《On Java8》 11.内部类
#6
Open
funnycoding
opened
4 years ago
funnycoding
commented
4 years ago
第十一章 内部类
一个定义在另一个类中的类,叫作内部类。
本章重点:**内部类与外类的通信**,怎样使用内部类写出更加优雅和清晰的代码,以及对内部类语法更加详尽的探索,这些特性是为为了语言的完备性而设计,也许你现在并不会用到。 **Java8 的Lambda 表达式和方法引用减少了编写内部类的需求【13,14章会详细讲,到时候看看】。** 检视阅读:【本章总共17个小部分,从标题上可以看出是对内部类各种语法的详细说明】 最初,内部类可能看起来有些奇怪,而且要花些时间才能在设计中轻松地使用她它们。对内部类的需求并非总是很明显的,但是在描述完内部类的基本语法与语义之后,"为什么使用内部类” 就明确使用内部类的实际好处。 ### 创建内部类 顾名思义,在类中继续定义一个类就是内部类。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212174358.png) **第一个例子,非常的简单直白,使用内部类与使用其他的独立类没有区别** 更**典型**的情况是**,外部类将有一个方法,该方法返回一个指向内部类的引用**。 就像下面这样:
外部类中存在返回内部类对象的方法。 如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 main() 方法中那样,具体地指明这个对象的类型 **OuterClassName.InnerClassName**(在外部类的静态方法中也可以直接指明 **InnerClassName**,在其他类中需要指明 外部类名称.内部类名称) ### 链接外部类 上面的例子只体现出代码的隐藏,可以将逻辑相同的类放在一起,让代码有更好的组织模式。 这些很有用,但还不是最引人注目的,当生成一个内部类的对象时,此对象与制造它的外围类(enclosing object) 之间就有了一种联系,**所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类所有元素的访问权** 【内部类隐式持有外部类引用】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212213808.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212213830.png) **Sequence** 类知识一个固定大小的Object 数组,以类的形式包装了起来。 可以使用 add() 在序列末尾增加新的 Object (只要还有空间),要获取 Sequence 中的每一个对象,可以使用 Selector 接口。这是**迭代器** 设计模式的一个例子,在本书稍后的部分将更多地学习它。 **Selector** 允许你检查序列是否到末尾了。 **end**() ,访问当前对象 **current**() ,以及移到序列中的下一个对象 **next**() 。因为 Selector 是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且其他方法能以此接口为参数,来生成更加通用的代码。 这里 **SequenceSelector** 是提供 Selector 功能的 私有内部类。 可以看到在 main() 中创建一个 Sequence,并向其中添加了一个 String对象,然后通过调用 selector() 获取一个 **Selector**,并用他在 Sequence 中移动和选择每一个元素。内部类可以访问外围类的方法和字段,这带来了很大的方便。 **所以内部类自动拥有对其外围类所有成员的访问权。** **当某个外围类的对象创建了一个内部类对象时,内部类必定会秘密捕获一个指向外围类对象的引用。** 当访问外围类成员时,使用的就是指向外围类的引用来访问。 **【这些代码是由编译器生成的。】** 但你现在可以看到:**内部类的对象只能在于其外围类的对象相关联的情况下才能被创建**(就像现在这样非 static 的内部类) **构建内部类对象时,需要一个指向指向其外围类的引用**。如果编译器访问不到这个引用就会报错。 ### 使用.this 和 .new 如果需要生成外部类对象的引用,可以使用外部类的名字后紧跟圆点和 **this**,这样产生的引用自动的具有正确类型,这一点在编译器就被知晓并受到检查。**因此没有任何运行时开销。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200214160030.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212222852.png) 又是你可能想要告知某些其他对象,去创建某个内部类的对象。要实现此目的,你必须在 **new** 表达式中提供对其他外部类对象的引用,这是需要使用 .**new** 语法,像下面这样。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200214160147.png) 这里创建内部类对象使用的就是 【外围类实例的名称.new 内部类名】 这个语法。 要想直接创建内部类对象,**不能直接使用外部类名 DotNew**,而是必须使用外部类的对象来创建该内部类对象。这也解决了内部类名字作用域的问题,你不必也不能这样声明【dotNew.new DotNew.Inner】 **下面这个例子直接将演示 .new 应用于 Parcel 的例子:** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212224632.png) 这里创建内部类的对象没必要使用 外部类.内部类 的形式啊,直接 内部类名就可以了。 在拥有外部类对象之前是不可能创建内部类对象的。**因为内部类对象会隐式持有外部类引用。**,但是如果你创建的是**嵌套类(静态内部类)**,那么它就不需要对外部类对象的引用。 静态内部类不持有外部类引用,【所以某些情况下也不会出现可能内存泄漏的问题。】 【到这里都是介绍基本的内部类的语法,包括怎么根据外部类的对象创建内部类的实例,怎样在内部类中访问外部类的元素,之后的小节开始设计到多态等元素】 --- ### 内部类与向上转型 **将内部类向上转型为其基类,尤其是转型为接口的时候,内部类就有了用武之地。**(从实现了某个接口的对象得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。 **这是因为此内部类—某个接口的实现 能够完全不可见,并且不可用。**所得到的只是指向基类或接口的引用。所以能够很方便的隐藏实现细节。 > 创建一个之前示例的接口: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212230002.png) 现在 **Contents** 和 **Destination** 表示客户端程序员可用的接口。 **当取得了一个指向基类或接口的引用。甚至可能无法找出它确切的类型。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200214160929.png) 在 Parcel4 中,**内部类 PContents 是私有的,所以除了 Parcel4 没有人能访问它**。 **普通非内部类的访问权限不能被设定为 private 或 protected**; 它们只能被设置为 public 公开权限或者 package 包访问权限。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213010511.png) **PDestination** 是 **protected**,所有只有 **Parcel4** 以及其子类 还有与 Parcel4 同一个包中的类(因为 protected 包含了包访问权限)能访问 **PDestination**。 其他类都不能访问 PDestination**,这意味着,如果客户端程序员也想了解或访问这些成员,那是要受到限制的**。实际上,甚至不能向下转型为 **private** 内部类(或 protected 内部类,除非继承自它的子类),因为不能访问其名字,就像 TestParcel 类中看到的这样。 **private** 内部类给类的设计者提供了一种途径,**通过这种方式可以完全阻止任何依赖于类型的编码** ,并且**完全隐藏了实现的细节**。此外,从客户端程序员的角度来看,由于不能访问任何新增加的,原本不属于公共接口的方法,所以扩展接口是没有价值的。 这也给 Java 编译器提供了生成高效代码的机会。**【这句话不知道从哪里进行证实,只记结论的话,没啥用】** **【说实话,这段我感觉还是有点抽象,需要在理解理解,然后自己扩展一下例程。 update : 过一天后来看,没啥难的。。。就是基本的内部类实现接口然后外部类有一个方法返回内部类的引用而已。。很基础】** ### 内部类方法和作用域 目前为止看到的只是内部类的典型用途。 如果所读,写的代码包含了内部类,那么它们都是平凡的内部类,简单并且容易理解。 **然而内部类的语法覆盖了大量其他更难以理解的技术。** 例如可以在一个方法里面或者任意的作用域内定义内部类。【要开始骚操作了】 在后面的例子中,先前的代码将被修改,用来实现: 1. 一个定义在方法中的类 2. 一个定义在作用域内的类,此作用域在方法的内部。 3. 一个实现了接口的匿名类。 4. 一个匿名类,它扩展了没有默认构造器的类。 5. 一个匿名类,它执行字段初始化 6. 一个匿名类,它通过实例初始化实现构造(匿名内部类不可能有构造器)。 **第一个例子展示方法内的局部内部类:** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213024507.png) **PDestination**类是方法 destination() 的一部分,而不是 Parcel5外部类的一部分。所以在 destination() 之外无法访问 **PDestination**类。 注意出现在 return 语句中的向上转型,返回的是 接口类型 **Destination** 的引用,它是 PDestination 的基类。 **当然在 destination() 中定义了内部类 PDestination,并不意味着 destination() 方法执行完毕 该 PDestination 就不可用了。而是方法结束就无法访问了** 【那么问题来了,这种局部内部类什么时候被垃圾回收器回收呢?】 你可以在同一个子目录下的任意类中创建一个类名为 PDestination 的类,不会存在命名冲突。 【那么问题又来了,具体的应用场景呢?】 我自己想到了一个—— Android 中的回调操作。 > 下面的例子展示如何在任意的作用域内嵌入一个内部类: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213024453.png) **TrackingSlip** 类被嵌入在 **if** 语句的作用域内,这**并不是**说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 TrackingSlip 的作用域之外,它是不可用的,除此之外,它与普通类一样。 【意思就是作用域限定,但是**编译期和别的类一起进行了编译,而不是条件满足才编译**】 ### 匿名内部类 > 下面的例子看起来有点奇怪: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213135939.png) **contents()** 方法将返回值的生成语表示这个返回值的类的定义 结合在一起。另外这个类是匿名的,它没有名字。 **这种语法指的是:创建一个继承自 Contents 的匿名类对象,通过 new 表达式返回的引用被自动向上转型为 Contents 的引用。** 上述匿名内部类的语法是下述形式的简化形式: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213141443.png) 在这个匿名内部类中,使用了默认构造器来生成 Contents。 【因为生成这个类对象的时候没有传入参数,所以调用的是父类的默认无参构造器】 > 下面的代码展示的是,如果你的基类需要一个有参数的构造器该怎么办: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213141924.png) 1. 将核实的参数传递给基类构造器【这里就是一个 int 类型的数字】 2. **在匿名内部类末尾的分号,并不是用来标记此内部类结束的**,**它表达的是表达式的结束**。只不过这个表达式正好包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的,没有特殊含义。 > 尽管 Wrapping 是一个普通类,但是在这里还是被当做公共的接口来使用: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213141940.png) 为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。 > 在匿名类中定义字段时,还能够对其执行初始化操作:【意思字段可以有一个修改的阶段】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213142536.png) **如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的**,也就是说,**该参数在初始化后不会改变**,所以可以被当成 final 的。 如果只是单纯给字段赋值,那么这个例子就够了,但是如果想做一些类似构造器的行为,该怎么做呢?因为**匿名内部类不存在构造器**【因为匿名】。 但**通过实例初始化,就能达到为匿名内部类创建一个构造器的效果**: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213144057.png) 在此例中,不要求变量一定是 **final**的,**因为被传递给匿名类的基类构造器,它不会在匿名类内部被直接使用。** 下例是带实例初始化的 parcel 形式。 注意 destination() 的参数必须是 **final**的,因为它们是在匿名类内部使用(即使不加 final,Java 8的编译器也会为我们自动加上 final,以保证数据的一致性。) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213145259.png) 在实例初始化操作的内部,可以看到一段代码,**它们不能作为字段初始化动作的一部分来执行**(就是 if 语句)。 **所以对于匿名类而言,实例初始化的实际效果就是构造器。** > 当然它受到了限制—你不能重载实例初始化方法,所以仅有一个这样的构造器。 > 匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如如果实现接口,也只能实现一个接口。 > 【因为实例化对象的时候就指明了要实现的接口,你只能 new 一种接口的实例 】 ### 嵌套类(静态内部类) > 如果不需要内部类对象与外围类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。 **【静态内部类被称为嵌套类】** 想要理解 static 应用于内部类时的含义,就必须记住,**普通内部类对象隐式保存了一个引用,指向创建它的外围类对象。**然而当内部类是静态类时,该引用不存在。 嵌套类意味着: 1. ##### 要创建嵌套类的对象,不需要外围类对象。【因为都是静态的,与实例对象无关】 2. ##### 不能从嵌套类对象访问非静态外部类的对象。【因为隐式引用不存在了】 > 嵌套类与普通内部类还有一个区别: > > **普通内部类的字段与方法,只能放在类的外部层次上**,所以**普通的内部类不能有 静态数据和静态字段**,**也不能包含嵌套类**。 > > 但是静态内部类可以包含静态数据和静态字段。 【静态和静态一起玩呗,因为它们有单独的存储空间】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213151219.png)![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213151231.png) 在 main() 方法中,没有任何 Parcel11 的对象是必须的; > 使用选取 static 成员的普通语法来调用方法,这些方法返回对 Contents 和 Destination 的引用。 就像你在本章前面看到的一样,在一个普通的 (非 static)内部类中,通过一个特殊的 this 引用可以链接到其外围类对象。 嵌套类就没有这个特殊的 this引用,这使得它类似于一个 static 方法。 **【反正静态内部类不持有外部类对象这一句话就概括完了。】** #### 接口内部的类 **嵌套类可以作为接口的一部分,你放到接口中的任何类都自动地是 public 和 static 的**。因为类是 static的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。 > 你甚至可以在内部类中实现其外围类接口,像下面这样: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213152052.png) > 如果你想要创建某些公共代码,使得它们可与被某个接口的所有不同实现所共用,那么使用该接口内部的嵌套类会显得很方便【应用场景来了】。 作者曾经在书中建议过,在每个类中都写一个 main() 方法,用来测试这个类。 这样做有一个缺点,那就是必须带着这些已经编译过的额外代码,如果这对你来说是个麻烦,那就可以使用嵌套类来放置测试代码。 **【换句话来说,嵌套类不需要编译?】** ![image-20200213152433512](/Users/xuyanxin/Desktop/typora-pics/image-20200213152433512.png) 这生成了一个独立类 TestBed$Tester 你可以使用这个类测试,但是不必在发布的产品中包含它。可以在打包产品前删除 **TestBed$Tester.class**。 **【但是说实话这个用途国内互联网不会有...知道就好了】** ![image-20200213152515398](/Users/xuyanxin/Desktop/typora-pics/image-20200213152515398.png) #### 从多层嵌套类中访问外部类的成员 一个内部类被嵌套多少层并不重要——**它能透明地访问所有它所迁入的外围类的所有成员**,如下所示: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213153045.png) 开页看到在 MNA.A.B 中,调用 g() 和 f() 不需要任何条件,即使该函数被定义为 私有。 > 这个例子展示了如何从不同的类里创建多层嵌套的内部类对象的语法 **".new"**语法能产生正确的作用域,所以不必在调用构造器时限定类名。 ### 为什么需要内部类 至此,已经看到了许多描述内部类的语法和语义。 但是这并不能回答“为什么需要内部类”这个问题。 那么 Java设计者们为什么如此费心地增加这项基本语言特性呢? 一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。**所以可以认为内部类提供了某种进入其外围类的窗口。** 内部类必须要回答的一个问题是:**如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?** 答案是:如果这能满足需求,就应该这样做。 那么内部类实现一个接口与外围类实现这个接口有什么区别呢? 答案是:**后者不是总能享受到接口带来的方便,有时需要用到接口的实现**。所以使用内部类最吸引人的原因是: **每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。** 【想实现哪个接口就搞个内部类,就可以使用该接口的功能了,安卓里回调大量使用了匿名内部类。】 如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程的问题就很难解决。**从这个角度看,内部类使得多重继承的解决方案变得完整**。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类) 为了看到更多的细节,考虑这样一种情形:**必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两种选择:使用单一类,或者使用内部类。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213155539.png) 当然,这里假设的是在两种方式下的代码结构都确实有逻辑意义。 然而遇到问题的时候,通常问题本身就能给出某些指引,告诉你是应该使用单一类还是使用内部类。 但是如果没有任何其他限制,从实现的观点来看,前面的例子没有任何区别,它们都能正常运作。 如果拥有的是抽象类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承:【情况来了】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213160143.png) 如果不需要解决“**多重继承**的问题,那么自然可以用别的方式编码,而不需要使用内部类。 但是如果使用内部类,还可以获得其他一些特性: 1. **内部类可以有多个实例,每个实例有自己的状态信息**,并且**与其外围类对象的信息互相独立**。 2. 在单个外围类中,**可以让多个内部类以不同的方式实现同一个接口**,或继承同一个类。【我感觉这个是比较重要的,可以对同个接口实现不同的功能 3. 创建内部类对象的时刻并不依赖于外围类对象的创建 4. 内部类没有令人迷惑的 "is-a"关系,它就是一个独立的实体。【这个不太明白,外围类不继承也没有is-a关系啊】 举个例子,如果 Sequence.java 不使用内部类,就必须声明 Sequence 是一个 Selector,对于某个特定的 Sequence 只能有一个 Selector,然而使用内部类很容易就能拥有另一个方法 reverseSelector(), 用它来生成一个反方向遍历序列的 Selector,只有内部类才有这种灵活度。 【也就是可以产生任意接口的实现类,根据情况自己实现接口】 ### 闭包与回调 闭包(**closure**)**是一个可调用的对象**,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,**可以看出内部类是面向对象的闭包**,因为它不仅包含了外围类的对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类的引用,在此作用域内,内部类有权操作所有的成员,包括 **private** 成员。 【说实话,对 Java 中的闭包的定义还真不理解, js中的闭包在我理解就是局部变量的含义,这段话说闭包是可调用的对象,不知道是我断句有问题还是理解不对,无法理解这个对象是哪个类的。】 **【好像这意思,内部类 == 面向对象概念下的闭包,因为内部类持有外部引用且能访问外部字段】** 在 Java 8 之前,**生成闭包行为的唯一方式就是内部类**。**在 Java8 之后,我们可以使用 Lambda 表达式来生成闭包行为**。所以非常有必要理解这种形式。 【**这个闭包,指的是一种行为,是动词**。而不像是js中的,是一种形容词(也可能我js理解的都是错的。)】在**函数式编程**这一章会重点讲 **Lambda产生的闭包**。同时应该**优先使用 Lambda 表达式用于内部类闭包**。但是 **Java 8之前的代码还存在很多使用内部类来标识闭包的方式**,所以非常有必要理解这种形式。 Java 最引人争议的问题之一就是,**人们认为 Java 应该包含某种类似指针的机制,以允许回调**。 通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。 稍后会看到回调是非常有用的概念,如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针,然而 Java 语言中没有包含指针。 **通过内部类提供闭包**功能是优良的解决方案,它比**指针更灵活,更安全**,见下例: 【应该要到了匿名内部类实现回调的地方了】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213183838.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213184118.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213184323.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213184405.png) 这个类比较长,所以多分了几个图。这个例子还是挺有意思的**下面是书上的分析**: 这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言, **Callee1 是更简单的实现方式**。【确实,直接搞一个类实现接口,完事】 Callee2 继承自 **MyIncrement**,后者已经**有了一个不同的 increment() 方法**,并且**与 Incrementable 接口期望的 increment() 方法完全不相关。** 所以如果 **Callee2**继承了 MyIncrement,就不能为了 Incrementable 的用途而覆盖 increment() 方法,于是只能使用**内部类独立地实现 Incrementable**。【这里的不覆盖其父类方法的 increment 指的是,**使用 super.increment() 保证父类的行为,然后在后面实现自己的逻辑**】 **还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。** 注意,在 Callee2 中除了 getCallbackReference() 以外,其他成员都是 private的。**想要建立与外部世界的任何连接,接口 Incrementable 都是必需的。**在这里可以看到**,interface 是如何允许 接口与接口的实现完全独立的。** 内部类 Closure 实现了 Incrementable,以提供一个返回 Callee2 的“钩子”,而且是一个安全的钩子。无论谁获得此 Incrementable 的引用,都只能调用 increment()。除此之外没有其他任何功能(因为元素都为 private) **Caller** 的构造器需要一个 Incrementable 的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Caller 对象可以使用此引用回调 Callee 类。 > 回调的价值在于它的灵活性—可以在运行时动态地决定需要调用什么方法。例如 图形界面 GUI 功能,到处都需要使用回调。 ### 内部类与控制框架 在将要介绍的 控制框架(control framework)中,可以看到更多的使用内部类的具体例子。 应用程序框架(**application framework**)就是被设计用以解决某类特定问题的一个类或一组类。 【 要运用某个应该程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。 这是模板方法的一个例子,**模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。** 设计模式总是将变化的事物与保持不变的事物分开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。 控制框架是一类特殊的应用程序框架,**它用来解决响应事件的需求。** 主要用来响应事件的系统被称作 **事件驱动**系统,应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。 】 **【上面这段是对应用程序框架和模板方法设计模式的解释,我觉得挺精髓的。】** 要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架:它的工作就是在事件 “就绪” 的时候执行事件。虽然就绪可以指任何事,但在本例中是指基于时间出发的事件。 接下来的问题就是:**对于要控制什么,控制框架并不包含任何信息**,这些信息是在实现算法 actionO() 部分的时候,通过继承来提供的。 首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以**使用抽象类代替实际的接口**。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213194506.png) 当希望运行 **Event** 并随后调用 start() 时,那么构造器就会捕获从对象创建的时刻开始的时间,此时间是这样得来的:start() 获取当前时间,然后增加一个延迟,生成触发事件的时间。 **start() 是一个独立的方法,而没有包含在构造器内**,因为**这样就可以在事件运行以后重新启动构造器**,也就是能够重复使用 Event 对象。 例如你想要重复一个事件,只需要简单地在 action() 中调用 start() 方法。 ready() 告诉你何时可运行 action() 方法,当然,可以在派生类中覆盖 ready() 方法,使得 Event 能够基于时间以外的因素而触发。 下面的文件包含了一个用来管理并触发事件的实际控制框架。**Event**对象被保存在 List
容器中。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213195224.png) run() 方法遍历 eventList,寻找就绪的,还要运行的 Event 对象。 对找到的每一个就绪的事件(ready()),使用对象的 toString() 打印其信息,调用其 action() 方法,然后从列表中移除此 Event。 注意,在目前的设计中你并不知道 Event 到底做了什么。 这正是设计的关键所在【使变化的事物与不变的事物互相分离】。用我的话说**,“变化向量“** 就是各种不同的 Event 对象所具有的不同行为,而**你通过创建不同的 Event 子类来表现不同的行为。** 【这里完全由你来实现,你需要什么功能,就在action中进行实现就行了】 > 这正是内部类要做的事情,内部类允许: > > 1. 控制框架的完整实现是由单个类创建的,从而使得实现的细节被封装了起来。内部类用来标识解决问题所必须的各种不同的 action()。 > 2. 内部类能够很容易的访问外围类成员,所以可以避免这种实现变得笨拙【意思就是外围类可以放心将成员设置为 private,保证了封装性】。 > 考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光,水,温度调节器的开关,以及响铃和重新启动系统。每个行为都是完全不同的。 **控制框架的设计使得分离这些不同的代码变得非常容易。**使用内部类,可以在单一类里面产生对同一个基类 Event 的多种派生版本。 【也就是多种行为都可以集中在一个类里去实现,使用多个内部类就行了】 对于温室系统的每一种行为,都创建一个新的继承 Event 的内部类,并在 action() 中实现其对应的业务代码。 作为典型的应用程序框架 **GreehouseControls** 类继承自 **Controller** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213201255.png)![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213201311.png)![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213201324.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213201401.png) **注意, light ,water 和 thermostat 都属于外围类 GreenhouseControls 的字段**,而这些内部类能够自由访问这些字段,无需限定条件或特殊许可。而且 action() 通常都涉及对某种硬件的控制。 大多数 Event 类看起来很相似,但是 **Bell** 和 Restart 比较特别, Bell 控制响铃,然后在事件列表中增加一个 Bell 对象,于是过一会它可以再次响铃。 读者可能注意到了内部类多么像多重继承:Bell 和 Restart 有 Event 的所有方法,并且似乎也拥有外围类 GreenhouseControls 的所有方法。 一个由 Event 对象组成的数组被递交给 Restart,该数组要加到控制器上。由于 restart() 也是一个 Event 对象,所以同样可以将 restart 对象添加到 Restart.action() 中,以使系统能够有规律地重新启动自己。 **【这个类挺有意思的,仔细看看】** 下面的类通过创建一个 GreenhouseControls 对象,并添加各种不同的 Event 对象来配置该系统,这是**命令设计模式**的一个例子— **eventList 中的每一个对象都被封装成对象的请求**: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213203846.png) 这个类的作用就是初始化系统,所以它添加了所有相应的事件,。 Restart 事件反复运行,而且它每次都会将 eventList 加载到 GreenhouseControls 对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序(测试程序的时候使用的) 更灵活的方式是避免对事件进行硬编码。 这个例子应该使读者了解内部类的价值了,特别是在控制框架中使用内部类的时候。 **【事件驱动的框架,使用多个内部类实现不同的逻辑】** ### 继承内部类 因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有些复杂。 > 问题在于,那个指向外部类对象的隐式引用必须被初始化。 > > **而在内部类的派生类中不再存在可连接的默认对象**。 > > 要解决这个问题,必须使用**特殊的语法**来明确说清楚它们之间的关联。 > ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213214618.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213214642.png) 可以看到 InheritInner 只能继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,**而且不能只是传递一个指向外围类对象的引用**。 此外,必须在构造器内使用如下语法: ```java enclosingClassReference.super(); ``` 这样才提供了必要的引用,然后程序才能编译通过。 > 【问题】: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213214947.png) **【之前说 这个类只能继承内部类,但是继承外围类也是OK的,所以这段话存疑。】** ### 内部类可以被覆盖么? 如果创建了一个内部类,然后继承其外围类并重新定义此内部类会发生什么呢? 也就是说,内部类会被覆盖吗? 这似乎是个很有用的思想,但是”覆盖”内部类就好像它是外围类的一个方法,其实并不起什么作用: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213215658.png) 默认无参构造器是编译器自动生成的。这里调用的是基类的默认构造器。可以看到这里创建 Yolk对象调用的仍然是基类中的 Yolk的构造器,并没有使用子类覆盖的 Yolk类的构造器。 这个例子说明:**当继承了某个外围类时,内部类并没有发生什么神奇的变化。这两个类是完全独立的两个实体,各自在自己的命名空间内。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213220219.png) 【看字节码更直观一些,这两个内部类确实毫无关联】 > 当然,明确地继承某个内部类也是可以的: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213221818.png)![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213221854.png) 现在 BigEgg2.Yolk 通过 extends Egg2.Yolk 明确继承了此内部类,并且覆盖了其中的方法。 insertYolk() 方法允许 BiggEgg2 将它自己的 Yolk 对象向上转型为 Egg2 中的引用 y。 所以当 g() 调用 y.f() 的时候,覆盖后的新版 f() 被执行。第二次调用 Egg2.yolk 的时候,结果是 BigEgg2.Yolk 的构造器调用了其基类的构造器。 可以看到在调用 g() 的时候 f()被调用了。 ### 局部内部类 前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体里面创建。 局部内部类不能有访问权限修饰符,说明它不是外围类的一部分;但是它可以访问当前代码块内的常量,以及此外围类的所有成员。 **下面的例子对局部内部类与匿名内部类进行了比较:** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213224330.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213224346.png) Counter 返回的是序列中的下好一个值。**我们分别使用匿名内部类和局部内部类实现了整个功能,它们有相同的行为和你能力。** 既然局部内部类名字在方法外是不可见的,**为什么我们仍然使用局部内部类而不是匿名内部类呢?** 唯一的理由是,**我们需要一个已命名的构造器**,或者需要重载构造器,而 **匿名内部类只能用于实例初始化**。 **【使用局部内部类是因为其可以有构造器,匿名内部类没有,只能在实例初始化的时候从方法传参数进去】** ### 内部类标识符 由于编译后每个类都会产生一个**.class** 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 **Class** 对象)。 你可能猜到了,内部类也必须生成一个**.class** 文件以包含它们的 **Class** 对象信息。这些类文件的命名有严格的规则:外围类的名字,加上“**$**",再加上内部类的名字。例如,**LocalInnerClass.java** 生成的 **.class** 文件包括: ``` Counter.class LocalInnerClass$1.class LocalInnerClass$LocalCounter.class LocalInnerClass.class ``` **如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符**。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与“**$**”的后面。 虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。 因为这是 java 的标准命名方式,**所以产生的文件自动都是平台无关的** ### 本章小结 比起面向对象编程中的其他概念来,接口和内部类更深奥复杂。比如 C++就没有这些。 将两者结合起来,同样能够解决 C++ 中用多重继承所解决的问题。然而 多重继承在 C++ 中被证明是相当难以使用的,相比较而言,Java 的接口和内部类就容易理解多了。 虽然这些特性本身是相当直观的,但是就像多态机 制一样,**这些特性的使用应该是设计阶段考虑的问题。**随着时间的推移,读者能够更好地识别什么情况下应该使用接口,什么情况使用内部类,或者两者同时使用。 但此时,读者至少应该完全理解了它们的语法和语义。 【没错,我理解了,但是还需要经常回顾一下】
第十一章 内部类
本章重点:**内部类与外类的通信**,怎样使用内部类写出更加优雅和清晰的代码,以及对内部类语法更加详尽的探索,这些特性是为为了语言的完备性而设计,也许你现在并不会用到。 **Java8 的Lambda 表达式和方法引用减少了编写内部类的需求【13,14章会详细讲,到时候看看】。** 检视阅读:【本章总共17个小部分,从标题上可以看出是对内部类各种语法的详细说明】 最初,内部类可能看起来有些奇怪,而且要花些时间才能在设计中轻松地使用她它们。对内部类的需求并非总是很明显的,但是在描述完内部类的基本语法与语义之后,"为什么使用内部类” 就明确使用内部类的实际好处。 ### 创建内部类 顾名思义,在类中继续定义一个类就是内部类。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212174358.png) **第一个例子,非常的简单直白,使用内部类与使用其他的独立类没有区别** 更**典型**的情况是**,外部类将有一个方法,该方法返回一个指向内部类的引用**。 就像下面这样: 外部类中存在返回内部类对象的方法。 如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 main() 方法中那样,具体地指明这个对象的类型 **OuterClassName.InnerClassName**(在外部类的静态方法中也可以直接指明 **InnerClassName**,在其他类中需要指明 外部类名称.内部类名称) ### 链接外部类 上面的例子只体现出代码的隐藏,可以将逻辑相同的类放在一起,让代码有更好的组织模式。 这些很有用,但还不是最引人注目的,当生成一个内部类的对象时,此对象与制造它的外围类(enclosing object) 之间就有了一种联系,**所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类所有元素的访问权** 【内部类隐式持有外部类引用】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212213808.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212213830.png) **Sequence** 类知识一个固定大小的Object 数组,以类的形式包装了起来。 可以使用 add() 在序列末尾增加新的 Object (只要还有空间),要获取 Sequence 中的每一个对象,可以使用 Selector 接口。这是**迭代器** 设计模式的一个例子,在本书稍后的部分将更多地学习它。 **Selector** 允许你检查序列是否到末尾了。 **end**() ,访问当前对象 **current**() ,以及移到序列中的下一个对象 **next**() 。因为 Selector 是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且其他方法能以此接口为参数,来生成更加通用的代码。 这里 **SequenceSelector** 是提供 Selector 功能的 私有内部类。 可以看到在 main() 中创建一个 Sequence,并向其中添加了一个 String对象,然后通过调用 selector() 获取一个 **Selector**,并用他在 Sequence 中移动和选择每一个元素。内部类可以访问外围类的方法和字段,这带来了很大的方便。 **所以内部类自动拥有对其外围类所有成员的访问权。** **当某个外围类的对象创建了一个内部类对象时,内部类必定会秘密捕获一个指向外围类对象的引用。** 当访问外围类成员时,使用的就是指向外围类的引用来访问。 **【这些代码是由编译器生成的。】** 但你现在可以看到:**内部类的对象只能在于其外围类的对象相关联的情况下才能被创建**(就像现在这样非 static 的内部类) **构建内部类对象时,需要一个指向指向其外围类的引用**。如果编译器访问不到这个引用就会报错。 ### 使用.this 和 .new 如果需要生成外部类对象的引用,可以使用外部类的名字后紧跟圆点和 **this**,这样产生的引用自动的具有正确类型,这一点在编译器就被知晓并受到检查。**因此没有任何运行时开销。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200214160030.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212222852.png) 又是你可能想要告知某些其他对象,去创建某个内部类的对象。要实现此目的,你必须在 **new** 表达式中提供对其他外部类对象的引用,这是需要使用 .**new** 语法,像下面这样。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200214160147.png) 这里创建内部类对象使用的就是 【外围类实例的名称.new 内部类名】 这个语法。 要想直接创建内部类对象,**不能直接使用外部类名 DotNew**,而是必须使用外部类的对象来创建该内部类对象。这也解决了内部类名字作用域的问题,你不必也不能这样声明【dotNew.new DotNew.Inner】 **下面这个例子直接将演示 .new 应用于 Parcel 的例子:** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212224632.png) 这里创建内部类的对象没必要使用 外部类.内部类 的形式啊,直接 内部类名就可以了。 在拥有外部类对象之前是不可能创建内部类对象的。**因为内部类对象会隐式持有外部类引用。**,但是如果你创建的是**嵌套类(静态内部类)**,那么它就不需要对外部类对象的引用。 静态内部类不持有外部类引用,【所以某些情况下也不会出现可能内存泄漏的问题。】 【到这里都是介绍基本的内部类的语法,包括怎么根据外部类的对象创建内部类的实例,怎样在内部类中访问外部类的元素,之后的小节开始设计到多态等元素】 --- ### 内部类与向上转型 **将内部类向上转型为其基类,尤其是转型为接口的时候,内部类就有了用武之地。**(从实现了某个接口的对象得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。 **这是因为此内部类—某个接口的实现 能够完全不可见,并且不可用。**所得到的只是指向基类或接口的引用。所以能够很方便的隐藏实现细节。 > 创建一个之前示例的接口: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200212230002.png) 现在 **Contents** 和 **Destination** 表示客户端程序员可用的接口。 **当取得了一个指向基类或接口的引用。甚至可能无法找出它确切的类型。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200214160929.png) 在 Parcel4 中,**内部类 PContents 是私有的,所以除了 Parcel4 没有人能访问它**。 **普通非内部类的访问权限不能被设定为 private 或 protected**; 它们只能被设置为 public 公开权限或者 package 包访问权限。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213010511.png) **PDestination** 是 **protected**,所有只有 **Parcel4** 以及其子类 还有与 Parcel4 同一个包中的类(因为 protected 包含了包访问权限)能访问 **PDestination**。 其他类都不能访问 PDestination**,这意味着,如果客户端程序员也想了解或访问这些成员,那是要受到限制的**。实际上,甚至不能向下转型为 **private** 内部类(或 protected 内部类,除非继承自它的子类),因为不能访问其名字,就像 TestParcel 类中看到的这样。 **private** 内部类给类的设计者提供了一种途径,**通过这种方式可以完全阻止任何依赖于类型的编码** ,并且**完全隐藏了实现的细节**。此外,从客户端程序员的角度来看,由于不能访问任何新增加的,原本不属于公共接口的方法,所以扩展接口是没有价值的。 这也给 Java 编译器提供了生成高效代码的机会。**【这句话不知道从哪里进行证实,只记结论的话,没啥用】** **【说实话,这段我感觉还是有点抽象,需要在理解理解,然后自己扩展一下例程。 update : 过一天后来看,没啥难的。。。就是基本的内部类实现接口然后外部类有一个方法返回内部类的引用而已。。很基础】** ### 内部类方法和作用域 目前为止看到的只是内部类的典型用途。 如果所读,写的代码包含了内部类,那么它们都是平凡的内部类,简单并且容易理解。 **然而内部类的语法覆盖了大量其他更难以理解的技术。** 例如可以在一个方法里面或者任意的作用域内定义内部类。【要开始骚操作了】 在后面的例子中,先前的代码将被修改,用来实现: 1. 一个定义在方法中的类 2. 一个定义在作用域内的类,此作用域在方法的内部。 3. 一个实现了接口的匿名类。 4. 一个匿名类,它扩展了没有默认构造器的类。 5. 一个匿名类,它执行字段初始化 6. 一个匿名类,它通过实例初始化实现构造(匿名内部类不可能有构造器)。 **第一个例子展示方法内的局部内部类:** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213024507.png) **PDestination**类是方法 destination() 的一部分,而不是 Parcel5外部类的一部分。所以在 destination() 之外无法访问 **PDestination**类。 注意出现在 return 语句中的向上转型,返回的是 接口类型 **Destination** 的引用,它是 PDestination 的基类。 **当然在 destination() 中定义了内部类 PDestination,并不意味着 destination() 方法执行完毕 该 PDestination 就不可用了。而是方法结束就无法访问了** 【那么问题来了,这种局部内部类什么时候被垃圾回收器回收呢?】 你可以在同一个子目录下的任意类中创建一个类名为 PDestination 的类,不会存在命名冲突。 【那么问题又来了,具体的应用场景呢?】 我自己想到了一个—— Android 中的回调操作。 > 下面的例子展示如何在任意的作用域内嵌入一个内部类: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213024453.png) **TrackingSlip** 类被嵌入在 **if** 语句的作用域内,这**并不是**说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 TrackingSlip 的作用域之外,它是不可用的,除此之外,它与普通类一样。 【意思就是作用域限定,但是**编译期和别的类一起进行了编译,而不是条件满足才编译**】 ### 匿名内部类 > 下面的例子看起来有点奇怪: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213135939.png) **contents()** 方法将返回值的生成语表示这个返回值的类的定义 结合在一起。另外这个类是匿名的,它没有名字。 **这种语法指的是:创建一个继承自 Contents 的匿名类对象,通过 new 表达式返回的引用被自动向上转型为 Contents 的引用。** 上述匿名内部类的语法是下述形式的简化形式: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213141443.png) 在这个匿名内部类中,使用了默认构造器来生成 Contents。 【因为生成这个类对象的时候没有传入参数,所以调用的是父类的默认无参构造器】 > 下面的代码展示的是,如果你的基类需要一个有参数的构造器该怎么办: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213141924.png) 1. 将核实的参数传递给基类构造器【这里就是一个 int 类型的数字】 2. **在匿名内部类末尾的分号,并不是用来标记此内部类结束的**,**它表达的是表达式的结束**。只不过这个表达式正好包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的,没有特殊含义。 > 尽管 Wrapping 是一个普通类,但是在这里还是被当做公共的接口来使用: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213141940.png) 为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。 > 在匿名类中定义字段时,还能够对其执行初始化操作:【意思字段可以有一个修改的阶段】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213142536.png) **如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的**,也就是说,**该参数在初始化后不会改变**,所以可以被当成 final 的。 如果只是单纯给字段赋值,那么这个例子就够了,但是如果想做一些类似构造器的行为,该怎么做呢?因为**匿名内部类不存在构造器**【因为匿名】。 但**通过实例初始化,就能达到为匿名内部类创建一个构造器的效果**: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213144057.png) 在此例中,不要求变量一定是 **final**的,**因为被传递给匿名类的基类构造器,它不会在匿名类内部被直接使用。** 下例是带实例初始化的 parcel 形式。 注意 destination() 的参数必须是 **final**的,因为它们是在匿名类内部使用(即使不加 final,Java 8的编译器也会为我们自动加上 final,以保证数据的一致性。) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213145259.png) 在实例初始化操作的内部,可以看到一段代码,**它们不能作为字段初始化动作的一部分来执行**(就是 if 语句)。 **所以对于匿名类而言,实例初始化的实际效果就是构造器。** > 当然它受到了限制—你不能重载实例初始化方法,所以仅有一个这样的构造器。 > 匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如如果实现接口,也只能实现一个接口。 > 【因为实例化对象的时候就指明了要实现的接口,你只能 new 一种接口的实例 】 ### 嵌套类(静态内部类) > 如果不需要内部类对象与外围类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。 **【静态内部类被称为嵌套类】** 想要理解 static 应用于内部类时的含义,就必须记住,**普通内部类对象隐式保存了一个引用,指向创建它的外围类对象。**然而当内部类是静态类时,该引用不存在。 嵌套类意味着: 1. ##### 要创建嵌套类的对象,不需要外围类对象。【因为都是静态的,与实例对象无关】 2. ##### 不能从嵌套类对象访问非静态外部类的对象。【因为隐式引用不存在了】 > 嵌套类与普通内部类还有一个区别: > > **普通内部类的字段与方法,只能放在类的外部层次上**,所以**普通的内部类不能有 静态数据和静态字段**,**也不能包含嵌套类**。 > > 但是静态内部类可以包含静态数据和静态字段。 【静态和静态一起玩呗,因为它们有单独的存储空间】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213151219.png)![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213151231.png) 在 main() 方法中,没有任何 Parcel11 的对象是必须的; > 使用选取 static 成员的普通语法来调用方法,这些方法返回对 Contents 和 Destination 的引用。 就像你在本章前面看到的一样,在一个普通的 (非 static)内部类中,通过一个特殊的 this 引用可以链接到其外围类对象。 嵌套类就没有这个特殊的 this引用,这使得它类似于一个 static 方法。 **【反正静态内部类不持有外部类对象这一句话就概括完了。】** #### 接口内部的类 **嵌套类可以作为接口的一部分,你放到接口中的任何类都自动地是 public 和 static 的**。因为类是 static的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。 > 你甚至可以在内部类中实现其外围类接口,像下面这样: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213152052.png) > 如果你想要创建某些公共代码,使得它们可与被某个接口的所有不同实现所共用,那么使用该接口内部的嵌套类会显得很方便【应用场景来了】。 作者曾经在书中建议过,在每个类中都写一个 main() 方法,用来测试这个类。 这样做有一个缺点,那就是必须带着这些已经编译过的额外代码,如果这对你来说是个麻烦,那就可以使用嵌套类来放置测试代码。 **【换句话来说,嵌套类不需要编译?】** ![image-20200213152433512](/Users/xuyanxin/Desktop/typora-pics/image-20200213152433512.png) 这生成了一个独立类 TestBed$Tester 你可以使用这个类测试,但是不必在发布的产品中包含它。可以在打包产品前删除 **TestBed$Tester.class**。 **【但是说实话这个用途国内互联网不会有...知道就好了】** ![image-20200213152515398](/Users/xuyanxin/Desktop/typora-pics/image-20200213152515398.png) #### 从多层嵌套类中访问外部类的成员 一个内部类被嵌套多少层并不重要——**它能透明地访问所有它所迁入的外围类的所有成员**,如下所示: ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213153045.png) 开页看到在 MNA.A.B 中,调用 g() 和 f() 不需要任何条件,即使该函数被定义为 私有。 > 这个例子展示了如何从不同的类里创建多层嵌套的内部类对象的语法 **".new"**语法能产生正确的作用域,所以不必在调用构造器时限定类名。 ### 为什么需要内部类 至此,已经看到了许多描述内部类的语法和语义。 但是这并不能回答“为什么需要内部类”这个问题。 那么 Java设计者们为什么如此费心地增加这项基本语言特性呢? 一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。**所以可以认为内部类提供了某种进入其外围类的窗口。** 内部类必须要回答的一个问题是:**如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?** 答案是:如果这能满足需求,就应该这样做。 那么内部类实现一个接口与外围类实现这个接口有什么区别呢? 答案是:**后者不是总能享受到接口带来的方便,有时需要用到接口的实现**。所以使用内部类最吸引人的原因是: **每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。** 【想实现哪个接口就搞个内部类,就可以使用该接口的功能了,安卓里回调大量使用了匿名内部类。】 如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程的问题就很难解决。**从这个角度看,内部类使得多重继承的解决方案变得完整**。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类) 为了看到更多的细节,考虑这样一种情形:**必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两种选择:使用单一类,或者使用内部类。** ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213155539.png) 当然,这里假设的是在两种方式下的代码结构都确实有逻辑意义。 然而遇到问题的时候,通常问题本身就能给出某些指引,告诉你是应该使用单一类还是使用内部类。 但是如果没有任何其他限制,从实现的观点来看,前面的例子没有任何区别,它们都能正常运作。 如果拥有的是抽象类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承:【情况来了】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213160143.png) 如果不需要解决“**多重继承**的问题,那么自然可以用别的方式编码,而不需要使用内部类。 但是如果使用内部类,还可以获得其他一些特性: 1. **内部类可以有多个实例,每个实例有自己的状态信息**,并且**与其外围类对象的信息互相独立**。 2. 在单个外围类中,**可以让多个内部类以不同的方式实现同一个接口**,或继承同一个类。【我感觉这个是比较重要的,可以对同个接口实现不同的功能 3. 创建内部类对象的时刻并不依赖于外围类对象的创建 4. 内部类没有令人迷惑的 "is-a"关系,它就是一个独立的实体。【这个不太明白,外围类不继承也没有is-a关系啊】 举个例子,如果 Sequence.java 不使用内部类,就必须声明 Sequence 是一个 Selector,对于某个特定的 Sequence 只能有一个 Selector,然而使用内部类很容易就能拥有另一个方法 reverseSelector(), 用它来生成一个反方向遍历序列的 Selector,只有内部类才有这种灵活度。 【也就是可以产生任意接口的实现类,根据情况自己实现接口】 ### 闭包与回调 闭包(**closure**)**是一个可调用的对象**,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,**可以看出内部类是面向对象的闭包**,因为它不仅包含了外围类的对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类的引用,在此作用域内,内部类有权操作所有的成员,包括 **private** 成员。 【说实话,对 Java 中的闭包的定义还真不理解, js中的闭包在我理解就是局部变量的含义,这段话说闭包是可调用的对象,不知道是我断句有问题还是理解不对,无法理解这个对象是哪个类的。】 **【好像这意思,内部类 == 面向对象概念下的闭包,因为内部类持有外部引用且能访问外部字段】** 在 Java 8 之前,**生成闭包行为的唯一方式就是内部类**。**在 Java8 之后,我们可以使用 Lambda 表达式来生成闭包行为**。所以非常有必要理解这种形式。 【**这个闭包,指的是一种行为,是动词**。而不像是js中的,是一种形容词(也可能我js理解的都是错的。)】在**函数式编程**这一章会重点讲 **Lambda产生的闭包**。同时应该**优先使用 Lambda 表达式用于内部类闭包**。但是 **Java 8之前的代码还存在很多使用内部类来标识闭包的方式**,所以非常有必要理解这种形式。 Java 最引人争议的问题之一就是,**人们认为 Java 应该包含某种类似指针的机制,以允许回调**。 通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。 稍后会看到回调是非常有用的概念,如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针,然而 Java 语言中没有包含指针。 **通过内部类提供闭包**功能是优良的解决方案,它比**指针更灵活,更安全**,见下例: 【应该要到了匿名内部类实现回调的地方了】 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213183838.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213184118.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213184323.png) ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213184405.png) 这个类比较长,所以多分了几个图。这个例子还是挺有意思的**下面是书上的分析**: 这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言, **Callee1 是更简单的实现方式**。【确实,直接搞一个类实现接口,完事】 Callee2 继承自 **MyIncrement**,后者已经**有了一个不同的 increment() 方法**,并且**与 Incrementable 接口期望的 increment() 方法完全不相关。** 所以如果 **Callee2**继承了 MyIncrement,就不能为了 Incrementable 的用途而覆盖 increment() 方法,于是只能使用**内部类独立地实现 Incrementable**。【这里的不覆盖其父类方法的 increment 指的是,**使用 super.increment() 保证父类的行为,然后在后面实现自己的逻辑**】 **还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。** 注意,在 Callee2 中除了 getCallbackReference() 以外,其他成员都是 private的。**想要建立与外部世界的任何连接,接口 Incrementable 都是必需的。**在这里可以看到**,interface 是如何允许 接口与接口的实现完全独立的。** 内部类 Closure 实现了 Incrementable,以提供一个返回 Callee2 的“钩子”,而且是一个安全的钩子。无论谁获得此 Incrementable 的引用,都只能调用 increment()。除此之外没有其他任何功能(因为元素都为 private) **Caller** 的构造器需要一个 Incrementable 的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Caller 对象可以使用此引用回调 Callee 类。 > 回调的价值在于它的灵活性—可以在运行时动态地决定需要调用什么方法。例如 图形界面 GUI 功能,到处都需要使用回调。 ### 内部类与控制框架 在将要介绍的 控制框架(control framework)中,可以看到更多的使用内部类的具体例子。 应用程序框架(**application framework**)就是被设计用以解决某类特定问题的一个类或一组类。 【 要运用某个应该程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。 这是模板方法的一个例子,**模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。** 设计模式总是将变化的事物与保持不变的事物分开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。 控制框架是一类特殊的应用程序框架,**它用来解决响应事件的需求。** 主要用来响应事件的系统被称作 **事件驱动**系统,应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。 】 **【上面这段是对应用程序框架和模板方法设计模式的解释,我觉得挺精髓的。】** 要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架:它的工作就是在事件 “就绪” 的时候执行事件。虽然就绪可以指任何事,但在本例中是指基于时间出发的事件。 接下来的问题就是:**对于要控制什么,控制框架并不包含任何信息**,这些信息是在实现算法 actionO() 部分的时候,通过继承来提供的。 首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以**使用抽象类代替实际的接口**。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200213194506.png) 当希望运行 **Event** 并随后调用 start() 时,那么构造器就会捕获从对象创建的时刻开始的时间,此时间是这样得来的:start() 获取当前时间,然后增加一个延迟,生成触发事件的时间。 **start() 是一个独立的方法,而没有包含在构造器内**,因为**这样就可以在事件运行以后重新启动构造器**,也就是能够重复使用 Event 对象。 例如你想要重复一个事件,只需要简单地在 action() 中调用 start() 方法。 ready() 告诉你何时可运行 action() 方法,当然,可以在派生类中覆盖 ready() 方法,使得 Event 能够基于时间以外的因素而触发。 下面的文件包含了一个用来管理并触发事件的实际控制框架。**Event**对象被保存在 List