Thinking in Java 4th chap10笔记-内部类
1.内部类
可以将一个类的定义放在另一个类的定义的内部,这就是内部类。
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制内部的类的可见性 。然而必须了解,内部类与组合是完全不同的概念,这一点很重要。
在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是你将会了解到,内部类远不止如此。它 了解外围类 ,并与之通信,而且你用内部类写出的代码更加优雅和简洁,尽管不总是这样。
最初,内部类可能看起来有一点奇怪,而且要花些时间才能在设计中轻松使用他们。对内部类的需求并非总是非常明显的,但是在描述完内部类的基本语法和语义后,使用内部类的益处就明确显现了。
2.1.创建内部类
1.创建内部类的方式就如同你想的一样,把类的定义置于外围类的里面。
2.普通的情况下,是在外围类的某个方法中直接使用内部类,不过 更为典型的情况是外部类将有一个方法,该方法返回一个指向内部类的引用。
3.如果想从外部类的非静态方法之外的任意位置创建某个类的内部对象,那么就必须具体的指明这个对象的类型, OuterClassName.InnerClassName ,如在main方法中的那样。
注 :个人认为从例子来看,在static main方法中创建内部类对象引用的是 Parcell2.Destination d = p2.to("Beijing");而在外部类的普通方法中可以直接用,如:
//返回内部类引用的一个方法
public Destination to(String s)
{
return new Destination(s);
}
3. 链接到外部类-
到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,不过但还不是最引用注目的,它还有其他的用途。当 生成一个内部类的对象时,此对象与制造它的外围对象-enclosing object之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外部类的所有元素的访问权。
注:这与C++嵌套类的设计非常不同,在C++中只是单纯的名字隐藏机制,与外围对象没有联系,也没有隐含的访问权。
1.内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向外围类对象的引用。然后,在你访问外部类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节。但你现在可以看到,内部类的对象只能在于其外围类的对象相关联的情况下才会被创建(就像你应该看到的,在内部类是非static类时)。构建内部类对象时,需要一个指向其外围对象的引用,如果编译访问不到这个引用就会报错。不过绝大多数时候,这都无需程序员关心。
4.使用 .this 与 .new
1.如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this,这样产生的引用自动具有正确的类型。这一点在编译期就被知晓被受到检查,因为没有任何运行时开销。
2.有时你想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其外部类对象的引用。这就需要.new语法。
注 :要想直接创建内部类的对象,你不能按照你想要的方式,去引用外部类的名字DotNew,而是必须使用外部类的对象来创建该内部类的对象。即:
DotNew dn = new DotNew();
//注意这种语法
DotNew.Inner inner = dn.new Inner();
这也解决了 内部类名字作用域 的问题,因此你不能声明(实际上你不能声明)dn.new DotNew.Innter();在拥有外部类对象之前,是不可能创建内部类对象的。这是因为内部类对象会暗暗的连接到创建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部对象的类引用。
注 :个人认为因为内部类对象时连接到它的外部类对象的,所以用dn.new;如果用dn.new DotNew.Innter(),这样的语法形式其实和外部类对象没有关系了。和正常直接创建对象区别不大。正如上面说的,如果你创建的是静态内部类,那么应该就不需要这样了。Java这样的设计其实就是为了区分正常创建对象的方式和内部类创建对象的方式。dn.new Inner()这样的语法,直接说明了Inner是dn的内部类,毋庸置疑。
5.内部类与向上转型:
1.当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就又了用武之地(从实现了某个接口的对象,得到对此接口的引用与向上循转型为这个对象的基类,实际上效果是一样的),这是因为此内部类,某个接口的实现-能够完全不可见,并且不可用(注:因为可以设置内部类的访问修饰符,如private,protected),所得到的只是指向基类或者接口的引用,所以能很方便的 隐藏实现细节。
注 :将实现了外部接口的内部类修为是private或者protected的话,客户端则根本无法了解或者访问这些成员。注:通常客户端的package和api的package肯定不是同一个,所以也无法访问protected,除非是继承它的子类。因为不能访问这些内部类的名字,所以甚至不能向下转型为private内部类。于是,private内部类给类的设计者提供了一种途径,通过这种方式完全可以阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的,原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给Java编译器提供了生成更高效的代码的机会。
注:个人认为上面的最后一句话的意思说的就是私有的内部类,扩展了外部接口,不过却没有价值。因为不能被客户端程序员访问。所以Java编译器可以利用这一点,生成高效的代码。
6.在 方法和作用域内的内部类
1.可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:
1.如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
2.你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可以用的。
2.注意方法中的局部内部类是属于方法的一部分,而不是类的一部分。所以在方法之外不能访问。当然在方法中定义了内部类,并不意味着一旦该方法执行完毕,该内部类就不可用了。
注:因为在方法中,所以不会出现命名冲突的问题。你可以在同一个子目录的任意类中对某个内部类使用类标识符PDestination.
3.在任意的作用域内嵌入一个内部类,该内部类被嵌入到一个if的作用域内,并不是说该类的创建条件是有条件的,它其实与别的类一起编译过了。然而在定义其出现的作用域之外,它是不可用的。除此之外,它与普通类一样。
7. 匿名内部类:
1.匿名类中不可能有命名构造器,因为它根本没名字。
2.public Contents contents()
{
//匿名内部类,匿名,没有名字,创建一个继承自Contents的匿名类的对象.通过new表达式返回的引用被自动向上转型为对Contents的引用
return new Contents()
{
private int i = 3;
@Override
public int value()
{
return i;
}
};
}
3.在2的匿名内部类中,使用了默认的构造器来生成Contents,如果你的基类需要一个有参数的构造器,怎么办?
public Wrapping wrapping(int x)
{
//注意看这里的用法,即使Wrapping是一个普通类,但其还是被其导出类当做”公共接口“来使用
return new Wrapping(x)
{
//更要注意,这里用到了Override关键字,说明返回的这个匿名内部类是Wrapping的一个导出类
@Override
public int value()
{
return super.value() * 3;
}
};// 注 :这个分号只是标记表达式的结束,只不过这个表达式正巧包含了内部类而已。并不是用来标识此内部类的结束的。因此,这地方与别的地方使用的分号是一致的。
}
4.在匿名类中定义字段时,还能够对其执行初始化操作。如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。如果你忘了,会得到一个编译时错误消息。
5.除了只是简单的字段赋值外,如果想做一些类似构造器的行为,怎么做?因为匿名内部类不可能有命名构造器(因为 它根本没名字 ),但通过实例初始化,就能够做到为匿名内部类创建一个构造器的效果。
注:
1.在匿名内部类使用的参数一定是final的。
2.如果说一个匿名内部类定义的返回的是一个接口,那么注意:return new Interface()...,这里一定是没有参数的。因为我大胆的推断,接口的最原始基类也是Object, 而Object中没有定义含参数的构造函数。
3. 注 :匿名内部类中实例初始化如果有if相应的判断,则不会被执行。因为他们不能作为字段初始化的一部分来执行。所以对于匿名类来说,实例初始化的实际效果就是构造器。不过其有限制,你不能重载实例初始化方法。所以你仅有一个这样的构造器。
4.匿名内部类与正规的继承相比有些限制,因为匿名内部类即可以扩展类,也可以实现接口。但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
9. 再访工厂方法
1.将工厂作为一个实现的匿名内部类/静态内部类。这样实现的构造函数就可以为private.实现隐藏。同样也没有必要创建作为工厂的具名类。
2.请记住第九章给出的建议:优先使用类而不是接口。如果你的设计需要某个接口,你必须了解它。 否则,不到迫不得已,不要将其放到你的设计中。
10.嵌套类:
1.如果不需要内部类对象与其外围类对象之前有联系,那么可以将内部类声明为static.这通常称为嵌套类。想要理解static应用于内部类的含义,就必须记住。 普通的内部类对象隐式的保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时候,就不是这样了。嵌套类意味着:
1.要创建嵌套类的对象,并不需要其外围类的对象。
2.不能从嵌套类的对象中访问非静态的外围类对象。
嵌套类与普通的内部类还有一个区别,普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段。也不能包含嵌套类。但是嵌套类可以包含所有这些东西。
注:
1.个人认为因为普通内部类是和外围对象挂钩的,即必须要先创建外围类对象。而static和static字段都是属于类的。即无论有没有外围类的对象,都应该存在或者访问。
2.嵌套类中没有可链接到外围类的this对象,所以这使得它类似一个static方法。
11. 接口内部的类
正常情况下,不能在接口内部放置任何代码。但嵌套类可以作为接口的一部分.你放到接口中的任何类都自动是public和static的。因为类是static的,只是将嵌套类置于接口的命名空间内,这并违反接口的规则.你甚至可以在内部类中实现其外围接口。
注:
1.如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。
2.之前作者在建议,在每个类中写一个main方法,用来测试这个类。不过这样做有一个缺点,那就是必须带着那些已编译过的额外代码(注:个人认为这句话的意思是main被编译了,发布产品后也需要有这个东西,但其实这个东西在产品发布后就没有了,所以说是额外的)。如果这对你是一个麻烦的话,那么就可以使用嵌套类来放置测试代码。如TestBed$Tester,要执行这个程序,只需要执行TestBed$Tester即可(UNIX/LINUX中必须转义$)。可以使用这个类做测试,但是不必在发布的产品中包含它。在将产品打包前可以简单的删除TestBed$Tester.class.
12.从多层嵌套类中访问外围类的成员:
1.一个内部类被嵌套多少层不重要,它能透明的访问它所有嵌入的外围类的所有成员。
注:.new语法能产生正确的作用域,所以不必在调用构造器时,限定类名
13. 为什么需要内部类?
一般来说,内部类继承自某个类或实现接口。内部类的代码操作创建它的外围类对象。所以可以认为内部类提供了某种进入其外围类的窗口。内部类必须要回答的一个问题是,如果只是需要一个接口的引用,为什么不通过外围类实现那个接口呢?答案是:如果这能满足需求,就应该这样做。那么内部类实现一个接口和外围了实现这个接口有什么区别呢?答案是后者不是总能享用到接口带来的方便。有时需要用到接口的实现。所以使用内部类最吸引人的原因是:
每个内部类都能独立的继承自一个(接口)的实现,所以无论外围类是否已经继承了某个(接口)的实现,对于内部类都没有影响。
如果没有内部类提供的,可以继承多个具体的或者抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变的完整。接口解决了部分问题,而内部类有效地实现了"多重继承"。也就是说,内部类允许继承多个非接口类型,(即类或者抽象类).
注:为了看到更多的细节,我们考虑一种情形,即必须在一个类中以某种方式实现两个接口.由于接口的灵活性,你有两种选择,使用单一类或者内部类:通常遇到问题的时候,问题本身就能给出某些指引,告诉你是应该使用单一类还是使用内部类。但是如果没有其他任何限制,从实现的观点来看,二者没什么不同。
不过如果拥有的是抽象的类或者是具体的类,而不是接口,那么就只能使用内部类才能实现多重继承。
14.如果不需要解决“多重继承”的问题,那么自然就可以用别的方式编码,而不需要使用内部类。但是如果使用内部类,还可以获得一些其他特性:1.内部类可以有多个实例,每个实例都有自己的状态信息.并且与其外围类对象的信息相互独立。
2.在单个内部类中,可以让多个内部类以不同的方式实现同一个接口或者继承自同一个类。
3.创建内部类的对象的时候并不依赖与外围类对象的创建。
4.内部类并没有令人迷惑的is-a关系,它就是一个独立的实体。
15. 闭包与回调:
1.闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包。因为它不仅包含外围类对象(创建内部类的作用域),还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有成员,包括private成员。
2.Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回调(callback).通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。稍后将会看到这是一个非常有用的概念。如果回调是通过指针实现的,那么只能寄望于程序员不会误用该指针。然而,Java语言更小心仔细,所以没有在语言中包括指针。
通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活,更安全。
3.
//传说中的闭包,内部类实现Incrementable
//内部类Closure实现了Incrementable,以提供一个返回Callee2的"钩子"(hook),而且是一个安全的钩子。
private class Closure implements Incrementable
{
@Override
public void increment()
{
//调用外围类的increment方法
Callee2.this.increment();
}
}
//得到回调的引用,提供了一个对外的接口,这里要用接口
//无论谁获得此Incrementable引用,都只能调用increment.除此之外,没有其他功能。不像指针那样,允许你做很多事情
Incrementable getCallBackReference()
{
return new Closure();
}
//Caller的构造器需要一个Incrementable引用作为参数(虽然可以在任何时刻捕获回调引用),然后在以后的某个时刻,Caller对象可以使用此引用回调Callee类
Caller(Incrementable i)
{
callbackReference = i;
}
注 :上面的例子因为Caller2继承了MyIncrement,而Caller需要Incrementable对象。当然Caller2也可以实现接口Incrementable.不过因为MyIncrement和Incrementable都有increment方法。不能为了Incrementable的用途覆盖increment方法,所以选择用内部类独立的实现Incrementable接口。这样Caller就可以根据一个Incrementable参数回调Callee.
回调的价值在于它的灵活性。可以在运行时动态的决定需要调用什么方法。在实现GUI功能的时候,都出都用到了回调。
16. 内部类与控制框架
1.应用程序框架就是被设计用来解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。这是设计模式模板方法的一个例子。模板方法包含算法的基本结构,并且会调用一个或多个可覆盖方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分开,在这个模式中,模板方式是保持不变的事物,而可覆盖的方法就是变化的事物。
控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称为事件驱动系统。应用程序设计中最常见的问题之一是图形用户接口。它几乎是完全是事件驱动的系统。Java Swing库就是一个控制框架,它优雅的解决了GUI的问题,并使用了大量的内部类。
考虑这样一个控制框架:
它的工作就是在事件就绪的时候执行事件。虽然就绪可以指任何事,不过本例中是指基于时间触发的事件。对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的action()部分时,通过继承来提供的。
2.//用来管理并触发事件的实际控制框架
//这就是一个框架,你不知道Event到底做了什么。这正是关键是所在。使变化的事物与不变的事物相互分离。而变化的则是各种不同的Event对象所
//具有的不同行为,而你通过创建不同的Event子类来表现不同的行为。
class Controller
{
//事件列表
private List<Event> eventList = new ArrayList<Event>();
//加入事件
public void addEvent(Event e)
{
eventList.add(e);
}
public void run()
{
//注意这里要copy一个新的eventList,因为下面会有删除操作;否则会造成并发修改
for(Event e : new ArrayList<Event>(eventList))
{
//需要就绪要运行的Event对象
if(e.ready())
{
System.out.println(e);
e.action();
eventList.remove(e);
}
}
}
}
这就是内部类要做的事情,内部类允许:
1.控制框架的完整实现是由单个的类创建的,从而使实现的细节被封装了起来。内部类用来表示解决问题所必须的各种不同的action.
2.内部类能够很容易的访问外围类的任意成员,所有可以避免这种实现变的笨拙。如果没有这种能力,代码会变得非常讨厌。以至于你肯定会选择别的办法。
17. 内部类的继承:
1.因为内部类的构造器必须连接到指向其外围类对象的引用.所以在继承内部类的时候,事情变的有些复杂。问题在于,那个指向外围类对象的秘密的引用必须被初始化。而在导出类中不再存在可连接的默认对象。要解决这个问题,必须要用特殊的语法来明确说清他们之前的关联。
2.public class InheriInner extends WithInner.Inner
{
//继承WithInnte的内部类
public InheriInner(WithInner wi)
{
//必须要使用这样的语法,这样特殊的语法.
//不能只传递一个指向外围类的引用。此外,还必须在构造器中使用.super语法
//注:个人认为1.需要调用InheriInner基类的构造函数,正常情况下应该是super
//2不过由于其基类是和一个外围类引用挂钩的。所以采用wi.super()的语法。和wi.new语法恰恰相反。.
wi.super();
}
}
18.内部类可以被覆盖吗?
1.当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然可以明确的继承某个内部类也是可以的。
19.局部内部类
1.可以在代码里创建内部类,典型的方式是在一个方法体里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分。但是它可以访问当前代码块内的常量以及外围类的所有成员。
2.使用局部内部类和匿名内部类实现了同样的功能,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要以命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。
所以使用局部内部类而不使用匿名内部类的另一个理由是,需要不止一个该内部类对象。
20. 内部类标识符:
1.由于每一个类都会产生一个.class文件,其中包含了如何创建该类型对象的全部信息。(此信息产生一个"meta-class",叫做CLass对象).内部类也必须生成一个.class文件以包含它们的Class对象信息。这些类文件的命名有严格的规则,外围类的名字,加上$,再加上内部类的名字。如果内部类是匿名的,编译器则会简单的产生一个数字作为其标识符。如果内部类是嵌套在别的内部类中,只需直接将它们的名字加在其外围类标识符与$的后面。
注 :对于Unix shell来说,$是一个元字符。所在在列出.class文件的时候,有时会有问题。
虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。见注。因为这是Java的标准命名方式。所以产生的文件自动都是平台无关的。注意,为了保证你的内部类能其作用。Java编译器会尽可能转换他们。
部分源码:



















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































