《Java编程思想》读书笔记一
很早之前就买了《Java编程思想》这本书,初学时看这本书看的云里雾里的,实在费劲,就放在一边垫桌底了。感觉这本书是适合C/C++程序员转行到Java学习的一本书,并不适合零基础的初学者去看这本书,毕竟当初花了一百多买了这本书,现在还是把它倒腾出来看一下吧,当作是巩固Java基础知识,本文会把自己感兴趣的知识点记录一下,相关实例代码:https://gitee.com/reminis_com/thinking-in-java
第一章:对象导论
这一章主要是帮助我们了解面向对象程序设计的全貌,更多是介绍的背景性和补充性的材料。其实萌新应该跳过这一章,因为这章并不会去讲语法相关的知识,当然可以在看完这本书后续章节后,再来回看这一章,这样有助于我们了解到对象的重要性,以及怎样使用对象进行程序设计。
Alan Kay曾经总结了第一个成功的面向对象语言、同时也是Java所基于的语言之一的Smalltalk的五个基本特性,这些特性表现了一种纯粹的面向对象的程序设计方式:
- 万物皆为对象。理论上讲,你可以抽取待求解问题的任何概念化构件(狗、建筑物、服务等),将其表示为程序中的对象。
- 程序是对象的集合,它们通过发送消息来告知彼此所要做的。要想请求一个对象,就必须对该对象发送一条消息。更具体的说,可以把消息想象为对某个特定对象的方法的调用请求。
- 每个对象都有自己的由其它对象所构成的存储。换句话说,可以通过创建包含现有对象的方式来创建新类型的对象。
- 每个对象都拥有其类型。按照通用的说法,“每个对象都是某个类(class)的一个实例(instance)”,每个类最重要的区别与其他类的特性就是“可以发送什么样的消息给它”。
- 某一特定类型的所有对象都可以接受同样的消息。
第二章:一切都都是对象
用引用操纵对象
每种编程语言都有自己操作内存中元素的方式。有时候,程序员必须注意将要处理的数据是什么类型,你是直接操纵元素,还是用某种特殊语法的间接表示(例如C/C++里得指针)来操作对象?
所有这一切在Java里都得到了简化。一切都被视为对象,因此可采用单一固定的语法。尽管一切都看作对象,但操纵的标识符实际上是对象的一个”引用”(reference)。可以将这情形想像成用遥控器(引用)来操纵电视机(对象)。只要握住这个遥控器,就能保持与电视机的连接。当有人想改变频道或者减小音量时,实际操控的是遥控器(引用),再由遥控器来调控电视机(对象)。如果想在房间里四处走走,同时仍能调控电视机,那么只需携带遥控器(引用)而不是电视机(对象)。 此外,即使没有电视机,遥控器亦可独立存在。也就是说,你拥有一个引用,并不一定需要有一个对象与它关联。
存储到什么地方
程序运行时,对象是怎么进行放置安排的呢?特别是内存是怎样分配的呢?对这些方面的了解会对你有很大的帮助。有五个不同的地方可以存储数据∶
1)寄存器。这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象(另一方面,C和C++允许您向编译器建议寄存器的分配方式)。
2)堆栈。位于通用RAM(随机访问存储器)中,但通过堆栈指针可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存;若向上移动、则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些Java 数据存储于堆栈中–特别是对象引用,但是Java对象并不存储于其中。
3)堆。一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于堆栈的好处是∶编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当需要一个对象时,只需用new写一行简单的代码,当执行这行代码时、会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代价∶用堆进行存储分配和清理可能比用堆栈进行存储分配需要更多的时间(如果确实可以在Java中像在C++中一样在栈中创建对象)。
4)常量存储。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分隔离开,所以在这种情况下,可以选择将其存放在ROM(只读存储器)中。
5)非RAM存储。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本的例子是流对象和持久化对象。在流对象中,对象转化成字节流,通常被发送给另一台机器。在”持久化对象”中,对象被存放于磁盘上,因此,即使程序终止,它们仍可以保持自己的状态。这种存储方式的技巧在于∶把对象转化成可以存放在其它媒介上的事物,在需要时,可恢复成常规的、基于RAM的对象。java提供了对轻量级持久化的支持,而诸如JDBC和Hibernate这样的机制提供了更加复杂的对在数据库中存储和读取对象信息的支持。
第三章:操作符
本章的内容比较基础,主要讲了赋值、算数操作符、关系操作符、逻辑操作符、按位操作符、移位操作符、三元操作符等基础知识。本章只是记录下递增和递减的相关知识。
自动递增和递减
递增和递减操作符不仅改变了变量,并且以变量的值作为生成的结果。这两个操作符各有两种使用方式,通常称为前缀式和后缀式,对于前缀递增和前缀递减(假设a是一个int值,如++a或–a),会先执行运算,再生成值,而对于后缀递增和后缀递减(如a++或a–),会先生成值,在执行运算,下面是一个例子:
public class AutoInc {
public static void main(String[] args) {
int i = 1;
System.out.println("i: " + i); // 1
System.out.println("++i: " + ++i); // 执行完运算后才得到值,故输出2
System.out.println("i++: " + i++); // 运算执行之前就得到值,故输出2
System.out.println("i: " + i); // 3
System.out.println("--i: " + --i); // 执行完运算后才得到值,故输出2
System.out.println("i--: " + i--); // 运算执行之前就得到值,故输出2
System.out.println("i: " + i); // 1
}
}
总结:对于前缀形式,我们在执行完运算后才得到值。但对于后缀形式,则是在运算执行之前就得到值。
第四章:控制执行流程
本章介绍了大多数编程语言都具有的基本特性:运算、操作符优先级、类型以及选择和循环等。例如布尔表达式、循环如while、do-While、for、分支判断如if-else以及选择语句switch-case-break等。由于本章的内容都是非常基础的语法知识,这里不再赘述。
第五章:初始化和清理
在Java中,通过提供构造器,类得设计者可以确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。对于不再使用的内存资源,Java提供了垃圾回收器机制,垃圾回收器会自动地将其释放。
- 为什么不能以返回值区分重载方法?
比如下面两个 方法,虽然他们有同样的方法名称和形参列表,但却很容易区分它们:
public void f(int i);
public int f(int i) { return i; }
只要编译器可以根据语境明确判断出语义,比如在 int x = f(1)
中,那么的确可以据此区分重载方法。不过,有时我们并不关心方法的返回值,我们想要的是方法调用的其它效果(这通常被称为“为了副作用而调用”),这时你可能会调用方法而忽略其返回值,如这样调用方法:f(1)
,此使Java如何才能判断你调用的哪一个f(int i)
方法呢?因此,根据方法的返回值来区分重载是行不通的。
- 静态数据的初始化
无论你创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且没有对他进行初始化,那么它就会获得基本类型的标准初始值,如果它是一个对象引用,那么它的默认初始值就是null。
静态数据初始化示例如下:
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
new Cupboard();
System.out.println("Creating new Cupboard in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f1(int marker) {
System.out.println("f1(" + marker + ")");
}
}
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard");
bowl4.f1(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
/* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard
f1(2)
Creating new Cupboard in main
Bowl(3)
Cupboard
f1(2)
f2(1)
f3(1)
*/
总结一下对象的创建过程,假设有个名为Dog的类:
- 即使没有显示地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类得静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
- 然后载入Dog.class,有关静态初始化的所有动作都会执行,因此,静态初始化只在Class对象首次被加载的时候进行一次。
- 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
- 这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值,而引用则被设置成了null
- 执行所有出现于字段定义处的初始化动作
- 执行构造器
3.finalize()
的用途何在?
无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存,这将对finalize()
的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间,但Java中一切皆为对象,那这种特殊情况是怎么回事呢?
看来之所以要有finalize()
方法,是由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法,这种情况主要发生在“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式,本地方法目前只支持C和C++,但它们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非Java代码中,也许会调用C的malloc()
函数系列来分配存储空间,而且除非调用了free()
函数,否则存储空间将永远得不到释放,从而造成内存泄漏,当然,free()
是C和C++中的函数,所以需要在finalize()
中用本地方法调用它。
记住,无论是“垃圾回收”还是“终结”,都不保证一定会发生,如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
如下例,示范了finalize()可能的使用方式:
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// proper cleanup
novel.checkIn();
// Drop the reference, forget to clean up
new Book(true);
// 强制进行终结动作,并调用finalize()
System.gc();
}
}
class Book {
boolean checkOut = false;
Book(boolean checkOut) {
this.checkOut = checkOut;
}
void checkIn() {
checkOut = false;
}
@Override
protected void finalize() {
if (checkOut) {
System.out.println("Error: checked out");
// 你应该总是假设基类的finalize()也要做某些重要的事情,因此要用super来调用它
// super.finalize();
}
}
}
本例的总结条件是:所有的Book对象在被当作垃圾回收前都应该被签入(check in),但在main()方法中,由于程序员的错误,有一本书未被签入,要是没有finalize()来验证终结条件,将很难发现这种缺陷。
第六章:访问权限控制
本章讨论了类是如何被构建成类库的:首先,介绍了一组类是如何被打包到一个类库中的;其次,类是如何控制对其成员访问的。在Java中,关键字package、包的命名模式和关键字import,可以使你对名称进行完全的控制,因此名称冲突的问题是很容易避免的。
控制对成员的访问权限有两个原因:第一是为了使用户不要碰触那些他们不该碰触的部分,这些部分对于类内部的操作是必要的,但是它并不属于客户端程序员所需接口的一部分。因此将方法和域指定为private,对客户端程序员而言是一种服务。二是为了让类库设计者可以更改类的内部工作方式,而不必担心这样会对客户端程序员产生重大的影响。
第七章:复用类
在本章介绍了两种代码重用机制,分别是组合和继承。在新的类中产生现有类的对象,由于新的类是由现有类的对象组成,所以这种方法称为组合。该方法只是复用了现有程序代码的功能。第二种方式则是按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式并在其中添加新的代码,这种方式称为继承。
在使用继承时,由于导出类具有基类接口,因此它可以向上转型至基类,这对多态来说至关重要。
final关键字
可能使用到final的三种情况:属性,方法和类。
- final属性:对于基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变。一但引用被初始化指向一个对象,就无法再把它改为指向另外一对象,然而,对象其自身却是可以被修改的。
- final方法:把方法锁定,以防任何继承类修改它的含义。(类中所有的private方法都是隐式地指定为是final的,由于无法取用private方法,所以也就无法在导出类中覆盖它。当然你可以对private方法添加final修饰,但这并不能给该方法增加任何额外的意义)
- final类:当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这么做 。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。(由于final类禁止继承,所以final类中的所有方法都隐式指定为是final的,因为无法覆盖他们。在final类中可以给方法添加final修饰词,但这并不会增添任何意义。)
第八章:多态
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。多态的作用则是消除类型之间的耦合关系,由于继承允许将对象视为他自己本身的类型或其基类型来加以处理,因此它允许将许多种类型(从同一基类导出的)视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。
方法调用绑定
将一个方法调用 同 一个方法主体关联起来被称作绑定。若在程序执行前进行绑定,就叫做前期绑定(面向过程语言的默认绑定方式)。若在程序运行时根据对象的类型进行绑定就叫做后期绑定(也叫动态绑定和运行时绑定)。
Java中除了static方法和final方法(private方法属于final方法)之外,其他的所有方法都是后期绑定。由于Java中所有方法都是通过动态绑定来实现多态,我们就可以编写只与基类打交道的程序代码,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。
构造器和多态
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用,这样做是有意义的,因为构造器具有一项特殊任务:检查对象是都被正确构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有的构造器都得到调用,否咋就不能可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。
让我们来看看下面这个例子,他展示了组合、继承以及多态在构建顺序上的作用:
public class Sandwich extends PortableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
Sandwich() {
System.out.println("sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
}
class Meal {
Meal() {
System.out.println("Meal()");
}
}
class Bread {
Bread() {
System.out.println("Bread()");
}
}
class Cheese {
Cheese() {
System.out.println("Cheese()");
}
}
class Lettuce {
Lettuce() {
System.out.println("Lettuce()");
}
}
class Lunch extends Meal {
Lunch() {
System.out.println("Lunch()");
}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
/* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
sandwich()
*/
复杂对象调用构造器要遵照如下顺序:
- 调用基类的构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类。
- 按声明顺序调用成员的初始化方法
- 调用导出类的构造器主体
构造器内部的多态方法的行为:构造器调用的层次结构带来了一个有趣的两难问题,如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况呢?一个动态绑定的方法调用会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操作的成员变量可能还未进行初始化——这肯定会招致灾难,如下例:
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
class Glyph{
void draw() {
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
this.radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
@Override
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}
/* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/
由该示例可以看出,上面说的初始化顺序并不完整,初始化实际过程的第一步应该是:在其它任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
构造器的编写准则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。在构造器内唯一能够安全调用的那些方法就是基类中的final方法(也适用于private方法)。
第九章:接口
接口也可以包含域,但是这些域隐式地是static和final的(因此接口就成为了一种很便捷的用来创建常量组的工具)。你可以选择在接口中显示地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定位为是public的;否则,它们将只能得到默认的包访问权限,这样在方法被继承的过程中,其可访问权限就降低了,这是Java编译器所不允许的。
如果要从一个非接口的类继承,那么只能从一个类去继承。其余的基本元素都必须是都必须是接口。需要将所有的接口名都置于implements关键字之后,用逗号将它们一一隔开。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。下面这个例子展示了一个具体类组合数个接口之后产生了一个新类。
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
public void fight() {}
}
/**
* 当通过这种方式将一个具体类和多个接口组合在一起时,这个具体类必须放在前面,
* 后面跟着的才是接口(否则编译器会报错)
*/
class Hero extends ActionCharacter
implements CanFight, CanFly, CanSwim {
@Override
public void swim() { }
@Override
public void fly() { }
}
public class Adventure {
public static void t(CanFight x) { x.fight(); }
public static void f(CanFly x) { x.fly(); }
public static void s(CanSwim x) { x.swim(); }
public static void a(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero h = new Hero();
t(h);
f(h);
s(h);
a(h);
}
}
该例也展示了使用接口的两个核心原因:
- 为了能够向上转型为多个基类型(以及由此而带来的灵活性)
- 防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口
我们也可以通过继承来扩展接口;通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。如下:
interface Monster {
void menace();
}
interface DangerousMonster extends Monster {
void destroy();
}
interface Lethal {
void kill();
}
class DragonZilla implements DangerousMonster {
@Override
public void menace() {}
@Override
public void destroy() {}
}
/**
* 改语法仅适用于接口继承
*/
interface Vampire extends DangerousMonster, Lethal {
void drinkBlood();
}
class VeryBadVampire implements Vampire {
@Override
public void menace() {}
@Override
public void destroy() {}
@Override
public void kill() {}
@Override
public void drinkBlood() {}
}
public class HorrorShow {
static void u(Monster b) { b.menace(); }
static void v(DangerousMonster d) {
d.menace();
d.destroy();
}
static void w (Lethal l) {
l.kill();
}
public static void main(String[] args) {
DangerousMonster barny = new DragonZilla();
u(barny);
v(barny);
Vampire vlad = new VeryBadVampire();
u(vlad);
v(vlad);
w(vlad);
}
}
由于接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用构造器不同,我们在工厂对象上调用的时创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,我们的代码将完全与接口的实现分离,这就使得我我们可以透明地将某个实现替换成另一个实现,下面的实例展示了工厂方法的结构:
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
Implementation1() { }
@Override
public void method1() {
System.out.println("Implementation1 method1");
}
@Override
public void method2() {
System.out.println("Implementation1 method2");
}
}
class Implementation1Factory implements ServiceFactory {
@Override
public Service getService() {
return new Implementation1();
}
}
class Implementation2 implements Service {
Implementation2() { }
@Override
public void method1() {
System.out.println("Implementation2 method1");
}
@Override
public void method2() {
System.out.println("Implementation2 method2");
}
}
class Implementation2Factory implements ServiceFactory {
@Override
public Service getService() {
return new Implementation2();
}
}
public class Factories {
public static void serviceConsumer(ServiceFactory factory) {
Service s = factory.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(new Implementation1Factory());
serviceConsumer(new Implementation2Factory());
}
}
为什么我们想要添加这种额外级别的间接性呢?一个常见的原因就是想要创建框架。
第十章:内部类
可以将一个类得定义放在另一个类得定义内部,这就是内部类。
链接到外部类
在最初,内部类看起来就像是一种代码隐藏机制;其实它还有其他用途。当生成一个内部类的对象时,此对象与制造它的外围对象之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。如下:
interface Selector {
// 检查元素是否到末尾
boolean end();
// 访问当前对象
Object current();
// 移动到序列中的下一个对象
void next();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) {
this.items = new Object[size];
}
public void add(Object o) {
if (next < items.length) {
items[next++] = o;
}
}
// 内部类可以访问外围类的方法和字段
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();
}
}
}
使用.this 和 .new
-
如果你需要生成对外部对象的引用,可以使用外部类的名字后面紧跟原点和this。这样产生的引用会自动地具有正确的类型,这一点在编译器就会被知晓并受到检查,因此没有任何运行时开销,如下:
public class DoThis { void f() { System.out.println("DoThis.f()"); } public class Inner { public DoThis outer() { // 使用.this语法,生成外部类对象的引用 return DoThis.this; } } public Inner inner(){ return new Inner(); } public static void main(String[] args) { DoThis dt = new DoThis(); Inner inner = dt.inner(); inner.outer().f(); } }
-
有时你可能想要告知某些其他对象,去创建某个内部类的对象,你必须在new表达式中提供对外部类对象的引用,这时需要使用.new语法,如下:
public class DotNew { public class Inner {} public static void main(String[] args) { DotNew dotNew = new DotNew(); // 使用.new 语法生成内部类的对象 Inner inner = dotNew.new Inner(); } }
-
在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建到它的外部类对象上。但是,如果你创建的时嵌套类(静态内部类),那么他就不需要对外部类对象的引用。如下:
public class Parcel3 {
// 静态内部类
static class Contents {
private int i = 11;
public int value() {
return i;
}
}
public static void main(String[] args) {
Parcel3.Contents contents = new Parcel3.Contents();
System.out.println(contents.value());
}
}
在方法和作用域内的内部类
可以在一个方法里面或者在任意的作用域内定义内部类,这么做有两个理由:
- 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用
- 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公用的。
下面的这些例子,先前的代码将被修改,以用来实现:
- 一个定义在方法中的类
- 一个定义在作用域内的类,此作用域在方法的内部
- 一个实现了接口的匿名类
- 一个匿名类,它扩展了非默认构造器的类
- 一个匿名类,它执行字段初始化
- 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)
先创建两个接口:
public interface Contents {
int value();
}
public interface Destination {
String readLabel();
}
示例1:展示了在方法的作用域内(为不是在其它类的作用域内),创建一个完整的类,这被称作局部内部类。
public class Parcel6 {
public Destination destination(String s) {
// 内部类PDestination是destination()方法的一部分,而不是Parcel6的一部分
// 所以,在destination()方法之外,不能访问PDestination
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) {
Parcel6 parcel6 = new Parcel6();
Destination d = parcel6.destination("Tasmania");
}
}
示例2:下面的示例展示了如何在任意的作用域内嵌入一个内部类
public class Parcel7 {
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();
System.out.println(s);
}
// 不能在这里使用,因为已经超出作用域
// TrackingSlip ts = new TrackingSlip("slip");
}
public void track() {internalTracking(true);}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
p.track();
}
}
匿名内部类
示例3:匿名内部类
public class Parcel8 {
/**
* contents()方法将返回值的生成与表示这个返回值的类的定义放在一起,这个类是匿名的,它没有名字
*/
public Contents contents() {
// 在这个匿名内部类中,使用了默认的构造器来生成Contents()
return new Contents() {
private int i = 11;
@Override
public int value() {
return i;
}
}; // 这个分号是必须的
}
public static void main(String[] args) {
Parcel8 parcel8 = new Parcel8();
Contents c = parcel8.contents();
System.out.println(c.value());
}
}
示例4:一个匿名类,它扩展了有非默认构造器的类
public class Parcel9 {
public Wrapping wrapping(int x) {
// 只需要简单的传递合适的参数给基类的构造器即可,这里是将x传进ew Wrapping(x)
return new Wrapping(x) {
public int value() {
return super.value() * 47;
}
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Wrapping w = p.wrapping(10);
System.out.println(w.value());
}
}
/**
* 尽管Wrapping只是一个具有具体实现的普通类,但它还是可以被其导出类当作公共“接口”来使用
*/
public class Wrapping {
private int i;
public Wrapping(int x) {
i = x;
}
public int value() {
return i;
}
}
示例5:一个匿名类,它执行字段初始化
public class Parcel10 {
// 如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求
// 其参数是final的,如果你忘记写了,这个参数也是默认为final的
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) {
Parcel10 p = new Parcel10();
Destination d = p.destination("Tasmania");
}
}
示例6:如果知识简单地给一个字段赋值,那么示例四中的方法就很好了。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,如下:
abstract class Base {
public 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();
}
}
再访工厂方法
匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。使用匿名内部类重写工厂方法:
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");
}
// jdk1.8之后,可以使用lambda表达式来简写: () -> new Implementation1();
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");
}
// jdk1.8之后,可以使用lambda表达式来简写: () -> new Implementation2();
public static ServiceFactory factory = new ServiceFactory() {
@Override
public Service getService() {
return new Implementation2();
}
};
}
public class Factories {
public static void serviceConsumer(ServiceFactory factory) {
Service s = factory.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
serviceConsumer(Implementation2.factory);
}
}
为什么需要内部类?
- 内部类提供了某种进入其外围类的窗口
- 每个内部类对能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口得)实现,对于内部类都没影响。
- 接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类)
示例如下:
class D {}
abstract class E {}
class Z extends D {
E makeE() {
return new E() {};
}
}
public class MultiImplementation {
static void taskD(D d) {};
static void taskE(E e) {};
public static void main(String[] args) {
Z z = new Z();
taskD(z);
taskE(z.makeE());
}
}
闭包与回调:闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
回调:通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。在C/C++中回调通过指针实现,由于Java中没有包括指针,但我们可以通过内部类提供闭包的功能来实现,如下例:
interface Incrementable {
void increment();
}
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();
}
}
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() {
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();
}
}
/** outpput:
* Other operation
* 1
* 1
* 2
* Other operation
* 2
* Other operation
* 3
*/
限于篇幅,本文先对前10章进行记录,《Java编程思想》这本书在讲解封装、继承、多态、接口和内部类时,写了很多有助于我们理解的示例代码,其中也用到了很多设计模式,目前已经提及到的设计模式有:单例模式、策略模式、适配器模式、代理模式,命令模式、模板方法模式以及工厂方法等示例代码。