不推荐此书中文版,部分翻译可能使我们产生误导。
备注:第1~8章笔记是以前写的,时间不够不做重新整理&排版了。第9章之后内容与张龙的Java基础视频笔记类似,不作重复整理。
第1章 对象入门
为什么OOP会在软件开发领域造成如此震撼的影响?
对管理人员,它实现了更快和更廉价的开发与维护过程。
对分析与设计人员,建模处理变得更简单,能生成清晰、易于维护的设计方案。
对程序员,对象模型显得如此高雅和浅显。
缺点:思考对象的时候,需要采用形象思维,而不是程序化的思维。思维的转变需要花费较大精力。
1.1抽象的进步
一种较有争议的说法:解决问题的复杂程度直接取决于抽象的种类及质量。
汇编语言是对机器码的抽象,命令式(如C、BASIC等)是对汇编语言的抽像。
尽管抽象在进化,但其本质仍要求我们在设计方案时考虑计算机的结构。
这就造成了解决问题时,我们必须同时掌握2种不同的思维方式:
一、将问题拆分成独立元素,由这元素组织成解决方案。
二、将组成解决方案的元素转换成机器模型,考虑它们在机器内的形式。
将2种思维联系需要付出较大精力。
在OOP中,构成解决方案的元素表示为Object,Object在机器内的形式由OOP处理,因此有效屏蔽了第二种思维。
Alan Kay总结了Smalltalk的五大基本特征(它是Java的基础语言):
一、所有东西都是对象。
二、程序由一堆对象组成,对象间通过消息传递告诉对方自己能干什么。
三、每个对象都有自己的存储空间,可容纳其他对象。
四、每个对象都有一种类型。
五、同一类所有对象都能接收相同的消息。
1.2对象的接口
“类型(Type)”的概念:所有对象,尽管各有特色,都属于某一系列对象的一部分,这些对象具有通用的特征和行为。
在Java中使用Class(类)关键字标识“类型”,“类型”决定了接口,而“类”是那个接口的一种特殊实现方式。
建立完类后,需要考虑的最大问题是:如何完成方案到代码间的转换过程?
譬如:我们已经了解通过开关可以控制电灯的明亮与否,“电灯”与"开关"则是构成方案的元素。
转换成代码Light lt = new Light(); lt.on();
其中,类=Light、方法1=on、方法2=off、方法3=brighten、方法4=dim。所谓的接口则是那些方法。
lt是我们为Light创建的“句柄(Handle)”,通过“句柄.方法”的格式使用类中包含的方法。
1.3实现方案的隐藏
这一领域从业人员大致可分两种:类创建者(卖方)、客户程序员(买方)。
卖方希望为买方提供实现(应用程序或是类库)而不让其掌握技术细节,非正规的买方往往不会遵守。
OOP使用public、private、protected等关键词限定了接口的访问权限。
1.4方案的重复使用
许多人认为代码或设计方案的重复使用时OOP提供的最伟大的一种杠杆。
但设计出一个可复用的方案却要求较多的经验及洞察力,只有极少数专家才能做到。
为了重复使用一个类,最简单的方法是在已有类的基础上置入新的类,新类由任意数量和类型的其他对象构成。
譬如:汽车是一个类、变速箱也是一个类,但汽车却能包含变速箱。
新类的成员通常设为private,这样可以不干扰客户代码的前提下,从容地修改那些成员(包括运行期更改)。
注意:这并非继承,且继承并不具备这样的灵活性。
1.5继承:重新使用接口
注意:Class关键字用于标识一种数据类型。
OOP通过各种数据类型(即对象)间的消息传递实现软件的功能。
假如在费尽心思作出一种数据类型后,不得不为另一个相似却有变化的问题重新作出一种数据类型就太令人灰心了。
继承解决了这一问题,在通过继承一个已有类后创建的新类,不仅复制了已有类,还允许改动已有成员或添加新的成员。
这意味着两者类型相同。
缺点:基类的句柄无法调用新类的新增方法。
1.5.1改善基础类:方法重写
1.5.2等价与类似关系:通常认为基础类和衍生类之间存在一种等价(类型相同)关系,即圆就是一种几何形状。
1.6多形对象的互换使用
继承通常以一系列类收场
这样做的好处是,我们只需要编写单一代码,令其忽略特定细节,只与基础类打交道,因为以上4者是同一类型。
譬如:void doStuff(Shape s){
s.erase();
//...
s.draw();}
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
这个函数可与任何Shape类型通信,完全独立于它要描绘的特定类型,这是非常有用的编程技巧。
这种把衍生类型当作它的基本类型处理的过程,称为“上溯造型(Upcasting)”。
1.6.1动态绑定
从机器层面理解,在doStuff函数编译时,它并不知道运行时实际传入的Shape类型位于哪块内存区域。
从概念上理解,对象接受一条消息时,并不知道对方的具体类型是什么,但采取的行为同样是正确的,称“多形性(Polymorphism)”。
对于OOP来说,用以实现多形性的方法叫做“动态绑定”。
1.6.2抽象的基础类和接口
设计时,有时只希望基类为衍生类提供一个接口,而不能创建它的对象,可以使用abstract关键字。同时,abstract关键字也能用于描述尚未实现的方法,以便继承它的子类实现。
1.7对象的创建和存在时间
OOP涉及的重要问题:抽象、继承、多形性、对象的创建与回收。
对象的创建有2种选择:
位于堆栈:需要在运行前了解对象的准确的数量、存在时间、以及类型,局限性大。
位于内存堆:避免了创建于堆栈带来的局限性,可在运行时管理数量、存在时间、类型,灵活性大。
显然,大部分程序语言选择了后者,C++为了执行效率,允许程序员对其管理,而Java则提供了垃圾回收器进行管理。
1.7.1集合与继续器
怎样才能知道那些对象要求多少空间呢?除非进入运行期。
在OOP中,提供了一种称为“集合”的对象,允许自动扩充自己,无论是C++还是Java都提供了集合的标准类库。
如果要对集合中的一系列元素进行操作,就要使用“继续器(Iterator)”了。或者使用Java最初提供的Enumeration。
1.7.2单根结构
在C++中有一个突显的问题:所有类最终是否都应该从单一基类继承?
C++为了保证灵活性并未这么做,由此在进行OOP时,以便获得OO的便利,需要添加新类库,还要使用一些不兼容的接口。
Java则采用了替代方案,使用单根结构,不仅简化了参数的传递,还可实现一个垃圾回收器。
当然,单根结构在控制上的成功,势必在其效率上有所降低,带来一些程序设计上的限制。
1.7.3集合库与方便使用集合
由于单根结构的所有对象都是Object类型,所以当创建了一个可以容纳Object的集合时,可以装入所有其他类型。
但从集合中取出类型的时候,原有类型会丢失,我们只能得到Object类型的对象,这时就要使用Downcasting。
那么是否存在一种“智能”集合?答案是肯定的,可以采用“参数化类型”。
1.7.4清除时的困境:由谁负责清除?
每个对象都要求资源(内存)才能“生存”,如果不再需要使用一个对象,就必须将其清除,以便释放资源。
垃圾收集器“知道”一个对象在什么时候不再使用,然后会自动释放那个对象占据的内存空间。
当然,这也需要付出一定的代价,就是运行期的开销。
这也意味着Java在运行期存在着一种不连贯的因素,但可以通过“实时程序”避免。
1.8违例控制:解决错误
从最古老的程序设计语言开始,错误控制一直都是设计者们需要解决的一个大问题,但很难设计出一套完美的方案。
违例控制器通常运行于独立的路径,当程序违例(Exception)时,会将捕获并抛出,我们可以在某个地方设置处置方案,这样程序就不必中止。
在Java中,违例控制模块是一开始就封装好的,所以必须使用它!
1.9多线程
计算机编程中,一个基本的概念就是同时对多个任务加以控制。
比如我们按下一个按钮,用户希望马上得到反馈,而不是等计算机执行完当前任务后才采取行动。
多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器,程序在逻辑意义上被分割为数个线程。
值得注意的是,当多线程同时访问一个共享资源(比如打印机)时,就会出现混乱,因此线程具备锁定资源的能力。
1.10永久性
Java1.1提供了对“有限永久性”的支持,允许我们将对象简单地保存到磁盘上,以后任何时间内取回。
1.11Java和因特网
为什么有些人认为Java是一个里程碑?因为它处理解决传统问题外,还能解决World Wide Web上的编程问题。
1.11.1什么是Web?
这是一种大范围人群共享资源的方式,一般将数据保存在DB中,根据其他人或机器的请求将信息传递给对方。
集中保存信息的称为Server,发出请求的称为Client。
这里,性能问题尤为重要:可能有数百万个Client同时发出请求,所以任何微小的延误都不容忽视。
为尽可能缓解潜伏的问题,程序员需要谨慎地分散任务的处理负担,一般可以考虑让Client处理部分任务。
但有时亦可分派给其他Server所在地的其他机器,那些机器亦叫做“中间件”(它也用于改进系统的维护)。
1.11.2客户端编程
Web最初采用BS方案提供交互式内容,Server通过CGI处理Client的请求,并回传静态页面。
执行流程:Client发出请求,Server启动cgi-bin目录下的CGI程序处理请求,生成静态页面回传。
可CGI启动很慢,并且其响应时间取决于需要传送多少数据,这就造成一些动态图片等无法连贯显示。
解决这一问题的方法是使用插件,这造成了编写插件的脚本语言(比如JS)爆炸性增长,它能产生更有互动力的GUI。
脚本语言是客户端编程的开始,解决了80%的问题,剩下的20%(比如多线程、DB访问、连网程序及分布式计算)由Java解决。
ActiveX是一种类似Java-Applet的组件,IE固化支持它,经过一家独立的专业协会的努力,已经具备了跨平台性。
安全问题:ActiveX通过“数字签名”解决。Java通过“沙箱”解决,解释器内嵌于浏览器,控制Applet访问本地磁盘。
1.11.3服务器端编程:Servlet编程
1.11.4一个独立的领域,应用程序:CS结构
1.12分析和设计
1.12.1不要迷失
1.12.2阶段0:拟出一个计划
1.12.3阶段1:要制作什么?
1.12.4阶段2:如何构建?
1.12.5阶段3:开始创建
1.12.6阶段4:校订
1.12.7计划的回报
1.13Java还是C++?
Java会替代C++吗?这个逻辑存在很大疑问,C++有一些特性是Java没有的,且存在众多爱好者(会对其维护)。
Java强大之处反映在C++稍有不同的领域,且拥有最“健壮”的类型检查及错误控制系统。
但不可否认,C++的执行效率上超越这Java。
========================= ========================= 20120828=============== =========================第2章 一切都是对象
尽管以C++为基础,但Java是一种更纯粹的OOP语言。
C++、Java都属于杂合语言,之所以说C++是杂合语言,是因为它支持与C语言的向后兼容能力,是C的超集。
只有将思维转入OO的世界,才能体会到Java的易学易用。
2.1用句柄操纵对象
每种编程语言都有自己的数据处理方法,有时,程序员必须刻意留意准备处理的是什么类型,或处理一些间接表示的对象。
所有这些在Java中得到了简化,采用一种统一的语法,用“句柄(Handle)”操作,可将它想象成电视机与遥控器。
譬如:创建一个String的句柄
String s;//s只是创建的句柄,并不是对象,如果在运行期向其发送消息就会获得一个错误
s = "asdf";//s进行了初始化,连接了一个对象
2.2所有对象都必须创建
创建句柄时,我们希望它同一个新对象连接,通常用new关键字。
因此上面例子等同于:String s = new String("asdf");//new的意思是把我变成String类型的新对象
2.2.1保存到什么地方
1、寄存器:位于CPU,是最快的保存区域,但数量十分有限,所以是根据需要由编译器分配的。
2、堆栈:位于RAM,速度仅次于寄存器,可以通过“堆栈指针”进行操作:上移(释放)下移(创建)。
创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。
这是由于它必须生成相应的代码,以便向上和向下移动指针。句柄保存于此。
3、堆(Heap):或内存池,位于RAM,Java对象保存于此。
和堆栈不同,编译器不必知道要从堆里分配多少存储空间、数据要在堆里停留多长的时间。
但分配存储空间时要花掉更长时间。
4、静态存储:位于RAM,保存使用Static关键字修饰过的静态元素句柄,其对象仍保存于Heap。
5、常数存储:永远都不会改变数值,通常直接置于程序代码内部。
6、非RAM存储:数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。
2.2.2特殊情况:主要类型
有一系列特殊类,可想象为主要(Primitive)类型,会被我们频繁用到。
对于这些类型,Java、C、C++采用了相同方法,直接在堆栈中本该创建句柄的位置创建“自动”变量,以便更高效地存取。
它们是:
boolean(1)
char(16)
byte(8)
short(16)
int(32)
long(64)
float(32)
double(64)
Void
Primitive本身位于Stack,但它们拥有自己的“封装器(Wrapper)”类,用于在Heap中的表示自己。
譬如:Character c = new Character('c'); char c = 'c'; 是不同的
BigInteger、BigDecimal用于高精度计算,他们是Warpper却没有对应的Primitive,因此不能使用运算符,只能用方法调用。
2.2.3Java的数组
为避免C++中数组(只是些内存块)引发的安全问题,Java会自动进行范围检查、运行期对索引的校验,但会造成内存开销。
创建数组句柄时,会自动初始化一个特殊值:null。一旦运行期Java看到句柄指向null就会报告问题,避免了安全问题。
2.3绝对不要清除对象
变量的Lifetime一直是程序员需要考虑的重要问题
2.3.1作用域
Java、C++、C中作用域(Scope)用花括号的位置决定。Scope决定了变量的“可见性”以及“存在时间”。
{ int x = 12; { int x = 96; /* illegal */ } }
注意:编译器会认为变量x 已被定义,在C 和C++能将一个变量“隐藏”在一个更大的作用域里,但在Java 里是不允许的。
2.3.2对象的作用域
一旦超出Scope,句柄就没办法访问它所指向的对象,但对象仍然存在,这在C++中尤为突出(内存溢出问题)。
譬如:{ String s = new String("a string"); } /* 作用域的终点 */
在Java中,垃圾回收器会检查new的对象是否被引用,若否就自动释放由那些闲置对象占据的内存。
2.4新建数据类型:类
一切东西都是对象,那么用什么决定一个类型的外观与行为呢?是关键字“class”。
譬如:class ATypeName {/*类主体置于这里}
然后就可用new创建这个类型的一个新对象:ATypeName a = new ATypeName();
2.4.1字段和方法
定义一个类时,可在自己的类里设置两种类型的元素:数据成员(字段)、成员函数(方法)。
数据成员是一种对象(通过它的句柄与其通信),可以为任何类型(Primitive或Class)。
譬如:class DataOnly { int i; float f; boolean b; }
在创建了该类实例后DataOnly d = new DataOnly();可以通过“对象句柄.成员”的方式调用d.i = 47; d.f = 1.1f; d.b = false;
Primitive的默认值(Java会自动初始化,C++则不会):
Boolean false
Char '\u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
2.5方法、自变量和返回值
Java中,使用“方法(Method)”一词(即通常意义上的Function)指代“完成某事的途径”。
基本表达形式:
返回类型 方法名( /* 自变量列表*/ ) {/* 方法主体 */}
前面讲到过对象间以消息传递的方式通信,在实例化一个对象后,就可以通过“对象名.方法名(自变量1…)”向对象发送消息。譬如:
Shape s = new Shape();
int x = s.getNum();
2.5.1自变量列表
自变量列表规定了我们传送给方法的是什么信息,如同Java内其他任何东西——采用的都是对象的形式。
因此,我们必须在自变量列表中给定要传递的对象类型,以及每个对象的名字。
如果其他地方处理对象的方式一样,实际传递的是“句柄”,但Primitive除外。
譬如:
int storage(String s) { return s.length() * 2; } //return关键字意味着“离开方法,返回其后附带的值”
2.6构建Java程序
2.6.1名字的可见性
为防止命名冲突,C++用额外关键字引入“命名空间”的概念,Java也是如此,且设计者鼓励反转使用自己的Internet域名。
2.6.2使用其他组件
要使用预先定义好的类,就要通过关键字import导入,譬如import java.util.Vector,如果想使用util下全部类,可使用通配符*。
2.6.3static关键字
在两种特殊情况下:
一、只想用一个存储区域来保存一个特定的数据。(无论创建多少个对象或没有对象,数据只有一个)
二、我们需要一个特殊的方法,即使没有对象关联也能被调用。
这里可以使用static关键字,有些OOL使用了“类数据”和“类方法”这两个术语。
这意味着它们只是为作为一个整体的类而存在的,并不是为那个类的任何特定对象。
2.7我们的第一个Java程序
import java.util.*;
public class Property{
public static void main(String[] args){
System.out.println(new Date()); //这里创建Date对象的唯一目的就是在控制台输出,因此无须为它创建句柄
Properties p = System.getProperties(); //getProperties方法将获得系统属性,并返回一个Properties对象
p.list(System.out); //list方法需要接收一个PrintStream对象,将p对象的内容以列表形式传入该对象
System.out.println("---Memory Usage:");
Runtime rt = Runtime.getRuntime();
System.out.println("Total Memory=");
+ rt.totalMemory() //连接字符串可用“+”,且会自动将数值转换成字符串类型
+ " Free Memory = "
+ rt.freeMemory()); //这种“自动类型转换”亦划入“运算符过载”处理的范畴。
try {
Thread.currentThread().sleep(5 * 1000);
} catch(InterruptedException e) {}
}
} //通过例子,运用上述学到的知识,尝试理解对象间的消息传递机制
2.8注释和嵌入文档
Java 里有两种类型的注释。第一种是传统的、C 语言风格的注释,是从C++继承而来的。/*多行注释*/
第二种类型的注释也起源于C++。//这是一条单行注释,无论哪一种注释,编译时都会被忽略
2.8.1注释文档
Java最体贴的一项设计就是文档与代码的“链接”。我们不必在每次更改代码后重复更新文档。
这需要使用一种特殊的注释语法,和提取这些注释的工具javadoc,它输出的是一个HTML文件。
它采用了部分来自Java 编译器的技术,查找我们置入程序的特殊注释标记。
2.8.2具体语法
javadoc命令都只能出现于“/** @Doc tags */”注释中,注释可加于类、方法、变量之上。
注意:javadoc只能为public和protected成员处理注释文档。
2.8.3嵌入HTML
我们可以通过HTML标签格式化文档,譬如/** System.out.println(new Date()); */
2.8.4@see:引用其他类
"@see 完整类名#方法名"允许我们引用其他类里的文档,它会在文档里加入一个超链接的“See Also”(参见)条目。
2.8.5类文档标记
“@version 版本信息”、“@author 作者信息”
2.8.6变量文档标记
变量文档只能包括嵌入的HTML 以及@see 引用。
2.8.7方法文档标记
除HTML 和@see 引用之外,追加“@param 参数名 说明”、“@return 说明”、“@exception 完整类名 说明”、“@deprecated”。
2.8.8文档示例
//: Property.java 作者自己的方法,指出这是包含了源文件名字的一个注释行
import java.util.*;
/**
* The first Thinking in Java example program. Lists system information on
* current machine.
*
* @author Bruce Eckel
* @author http://www.BruceEckel.com
* @version 1.0
*/
public class Property
{
/**
* Sole entry point to class & application
*
* @param args
* array of string arguments
* @return No return value
* @exception exceptions
* No exceptions thrown
*/
public static void main(String[] args)
{
System.out.println(new Date());
Properties p = System.getProperties();
p.list(System.out);
}
} // /:~ 作者自己的方法,代表注释结束
2.9编码样式
一个非正式的Java编程标准是大写一个类名的首字母。若类名由几个单词构成,每个嵌入单词的首字母都采用大写形式。
对于其他几乎所有内容:方法、字段以及对象句柄名称,可接受的样式与类样式差不多,只是标识符的第一个字母采用小写。
2.10总结
到这,对语言的总体情况及一些基本思想应该有了一定程度的认识,以上都是采用单线形式的方式叙述,但OO又该如何设计?
2.11练习
写一个程序,打印出从命令行获取的三个自变量。Scanner类
==================================================20120830========================================
第3章 控制程序流程
“就象任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程中作出判断与选择。”
3.1使用Java运算符
加号(+)、减号和负号(-)、乘号(*)、除号(/)以及等号(=)的用法与其他所有编程语言都是类似 的。几乎所有运算符都只能操作“主类型”(Primitives)。
唯一的例外是“=”、“==”和“!=”,它们能操作所有对象(也是对象易令人混淆的一个地方)。除此以外,String 类支持“+”和“+=”。
3.1.1优先级
最简单的规则是乘除优于加减,其他的优先级规则,由于经常忘记,只需要用括号明确规定计算顺序即可。
3.1.2赋值
赋值是用等号运算符(=)进行的,它的意思是“取得右边的值,把它复制到左边”。
右边的值可以是任何常数、变量或者表达式,只要能产生一个值就行。
左边的值必须是一个明确的、已命名的变量。也就是说,它必须有一个物理性的空间来保存右边的值。
Primitive的赋值非常直接,因为它容纳了实际的值。譬如:a=3; b=a; //a和b将各有一个独立的值3
对象的赋值却有变化,它意味着将句柄从一个地方赋值到另一个地方。譬如:A a=new A(); B b=a;//b实际容纳了a句柄的对象
//: Assignment.java
// Assignment with objects is a bit tricky
package c03;
class Number { int i; }
public class Assignment {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
n1.i = 9;
n2.i = 47;
System.out.println("1: n1.i: " + n1.i + ", n2.i: " + n2.i);
n1 = n2; //由于n1句柄容纳了n2句柄的对象,n1句柄原本的对象将被“垃圾回收器”清除
System.out.println("2: n1.i: " + n1.i + ", n2.i: " + n2.i);
n1.i = 27;
System.out.println("3: n1.i: " + n1.i + ", n2.i: " + n2.i);
}
} ///:~
方法调用中的别名处理:
//: PassObject.java
// Passing objects to methods can be a bit tricky
class Letter { char c; }
public class PassObject {
static void f(Letter y) {
y.c = 'z';
}
public static void main(String[] args) {
Letter x = new Letter();
x.c = 'a';
System.out.println("1: x.c: " + x.c);
f(x); //该方法表面上似乎在Scope内制作自己的自变量Letter y的一个副本,实际传递的是一个句柄
System.out.println("2: x.c: " + x.c);
}
} ///:~
3.1.3算数运算符
包括加号(+)、减号(-)、除号(/)、乘号(*)以及模数(%,从整数除法中获得余数)。
整数除法会直接砍掉小数,而不是进位。支持简写x+=4意为4加到x,并将结果赋给x。
//: MathOps.java
// Demonstrates the mathematical operators
import java.util.*;
public class MathOps {
// Create a shorthand to save typing:
static void prt(String s) {
System.out.println(s);
}
// shorthand to print a string and an int:
static void pInt(String s, int i) {
prt(s + " = " + i);
}
// shorthand to print a string and a float:
static void pFlt(String s, float f) {
prt(s + " = " + f);
}
public static void main(String[] args) {
// Create a random number generator,
// seeds with current time by default:
Random rand = new Random();
int i, j, k;
// '%' limits maximum value to 99:
j = rand.nextInt() % 100;
k = rand.nextInt() % 100;
pInt("j",j); pInt("k",k);
i = j + k; pInt("j + k", i);
i = j - k; pInt("j - k", i);
i = k / j; pInt("k / j", i);
i = k * j; pInt("k * j", i);
i = k % j; pInt("k % j", i);
j %= k; pInt("j %= k", j);
// Floating-point number tests:
float u,v,w; // applies to doubles, too
v = rand.nextFloat();
w = rand.nextFloat();
pFlt("v", v); pFlt("w", w);
u = v + w; pFlt("v + w", u);
u = v - w; pFlt("v - w", u);
u = v * w; pFlt("v * w", u);
u = v / w; pFlt("v / w", u);
// the following also works for
// char, byte, short, int, long,
// and double:
u += v; pFlt("u += v", u);
u -= v; pFlt("u -= v", u);
u *= v; pFlt("u *= v", u);
u /= v; pFlt("u /= v", u);
}
} ///:~
3.1.4自动递增和递减
递减运算符是“--”,意为“减少一个单位”;递增运算符是“++”,意为“增加一个单位”。
3.1.5关系运算符
关系运算符评价的是运算对象值之间的关系,生成的是一个Boolean结果。
包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。
//: Equivalence.java
public class Equivalence {
class Value { int i; }
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2); //尽管对象的内容相同,句柄却是不同的,而==和!=比较的正好就是对象句柄
System.out.println(n1 != n2); //因此,第一个输出false,第二个输出true
System.out.println(n1.equals(n2)); //Integer的equals方法被重写过,能对对象的内容进行比较
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2)); //而Value是我们新建的类,equals方法并未重写,默认对句柄进行比较
}
} ///:~
3.1.6逻辑运算符
逻辑运算符AND(&&)、OR(||)以及NOT(!)能生成一个boolean——以自变量的逻辑关系为基础。
在进行AND运算时,譬如:test1(0))&&test2(2)&&test3(2),如果test2产生了false,则不会执行test3,这一现象称“短路”。
3.1.7按位运算符
按位运算符允许我们操作一个整数主数据类型中的二进制位。
AND(&)都是则是、OR(|)一个是则是、XOR(^)只能一个是则是、NOT(~)取反
对于布尔值,按位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。
3.1.8移位运算符
移位运算符面向的运算对象也是二进制的“位”。左移位(<<)、“有符号”右移位(>>)、“无符号”右移位(>>>)。
若对byte 或short 值进行右移位运算,得到的可能不是正确的结果。
它们会自动转换成int 类型,并进行右移位。但“零扩展”不会发生,所以在那些情况下会得到-1 的结果。
//: URShift.java
// Test of unsigned right shift
public class URShift {
public static void main(String[] args) {
int i = -1; i >>>= 10;
System.out.println(i);
long l = -1; l >>>= 10;
System.out.println(l);
short s = -1; s >>>= 10;
System.out.println(s);
byte b = -1; b >>>= 10;
System.out.println(b);
}
} ///:~
3.1.9三元if-else运算符
形式:布尔表达式 ? 值0:值1
若“布尔表达式”的结果为true,就计算“值0”,但若“布尔表达式”的结果为false,计算的就是“值1”。
3.1.10逗号运算符
在C 和C++里,逗号不仅作为函数自变量列表的分隔符使用,也作为进行后续计算的一个运算符使用。
在Java 里需要用到逗号的唯一场所就是for 循环。
3.1.11字串运算符+
这个运算符在Java 里有一项特殊用途:连接不同的字串。
3.1.12运算符常规操作规则
3.1.13造型运算符
“造型”(Cast)的作用是“与一个模型匹配”。在适当的时候,Java 会将一种数据类型自动转换成另一种。
譬如:int i = 200; long l = (long)i;
“缩小转换(Narrowing Conversion)”可能会丢失信息。此时,编译器会强迫我们进行造型。
”而对于“放大转换”(Widening conversion),则不必进行明确造型,因为新类型肯定能容纳原来类型的信息。
字面值(Literal):十六进制用一个前置的0x或0X指示,八进制用一个前置0指示,指数用e表示。
3.1.14Java没有“sizeof”
C和C++中,数据类型的大小会因为计算机不同而不同,需要sizeof确认,Java则不同,数据类型大小在所有机器均相同。
3.1.15复习计算顺序
“Ulcer Addicts Really Like C A lot”
3.1.16运算符总结
3.2执行控制
Java 使用了C 的全部控制语句,所以假期您以前用C 或C++编程,其中大多数都应是非常熟悉的。
3.2.1真和假
“==、!=”表达式返回true 或false。
3.2.2if-else
这或许是控制流程最基本的形式,else是可选的。
譬如:
static int test2(int testval) {
if(testval > target)
return -1; //return关键字有两种用途:指定一个方法返回什么值,并立即返回该值
if(testval < target)
return +1;
return 0; // match这里不必加上else,因为方法在遇到return后便不再继续
}
3.2.3反复
while、do-while、for控制循环,有时将其划分为“反复语句”。
3.2.4do-while
while 和do-while 唯一的区别就是do-while 肯定会至少执行一次。
基本形式:do{ 语句 }while(布尔表达式){ 语句 }
3.2.5for
基本形式:for(初始表达式; 布尔表达式; 步进){ 语句 }
逗号运算符:唯一用到的就是for循环,譬如:for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) { 语句 }
3.2.6中断和继续
在任何循环语句的主体部分,亦可用break(强制退出循环)和continue(停止当前反复,开始新反复)控制循环的流程。
譬如:for(int i = 0; i < 100; i++) {
if(i == 74) break; // Out of for loop
if(i % 9 != 0) continue; // Next iteration
System.out.println(i);
}
臭名昭著的“goto”(源码级跳转)若总是从一个地方跳到另一个地方,还有什么办法能识别代码的流程呢?因此Java没有goto
goto与continue、break使用了相同的机制:label(标签)。譬如:continue label1; break label2;
如果没有break outer 语句,就没有办法在一个内部循环里找到出外部循环的路径。
这是由于break 本身只能中断最内层的循环(对于continue 同样如此)。
注意:Java 里唯一需要用到标签的地方就是拥有嵌套循环,而且想中断或继续多个嵌套级别的时候。
3.2.7开关
“开关”(Switch)有时也被划分为一种“选择语句”,从一系列代码选出一段执行。
基本形式:switch(整数选择因子) { case 整数值1 : 语句; break; case 整数值2 : 语句; break; default:语句; }
注意:每个case均以一个break结尾,若省略break,会继续执行后面的case语句的代码,直到遇到一个break为止。
char c = (char)(Math.random() * 26 + 'a');试着思考这个代码的执行过程。
3.3总结
本章讲述了大多数程序设计语言都具有的基本特性:计算、运算符优先级、类型转换以及选择和循环等。
3.4练习
==================================================20120831========================================
第4章 初始化和清除
“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。”
“初始化”和“清除”是这些安全问题的其中两个。C++为我们引入了“构建器”的概念。Java也沿用了这个概念。
4.1用构建器自动初始化
"构建器"会在对象被创建时自动调用,以担保每个对象都会得到正确的初始化。为了让编译器知道要调用哪个方法进行初始化,构建器的名字与类名相同。
和其他任何方法一样,构建器也能使用自变量,以便我们为一个对象的初始化设定相应的参数。
4.2方法过载
任何程序设计中,一项重要的特性就是名字的运用,它能使程序更易人们理解和修改。
人类的大多数语言都具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。
我们用相同的词表达多种不同的含义——即词的“过载”。方法过载:class Tree{ Tree(){ } Tree(int i){ } void info(){ } void info(String s){ } }
使用方法过载的原因是:同一种概念用两个名字表达,显然有些多余。
4.2.1区分过载方法
有一个简单的规则:每个过载的方法都必须采取独一无二的自变量类型列表(顺序颠倒形成2个方法是被允许的)。
4.2.2主类型的过载
若我们的数据类型“小于”方法中使用的自变量,就会对那种数据类型进行“转型”处理。
char 获得的效果稍有些不同,这是由于假期它没有发现一个准确的char 匹配,就会转型为int。
若我们的自变量“大于”过载方法期望的自变量,就必须用括号中的类型名将其转为适当的类型。
4.2.3返回值过载
Java没有返回值过载,因为无从判断我们是否忽略、弄错返回值。
4.2.4默认构造器
若创建一个没有构建器的类,则编译程序会帮我们自动创建一个默认构建器。
4.2.5this关键字
this 关键字(注意只能在方法内部使用)可为已调用了其方法的那个对象生成相应的句柄。
假若您希望将句柄返回给当前对象,那么它经常在return 语句中使用。
//: Leaf.java
// Simple use of the "this" keyword
public class Leaf {
private int i = 0;
Leaf increment() { i++; return this; } //通过this 关键字返回当前对象的句柄
void print() { System.out.println("i = " + i); }
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print(); //所以可以方便地对同一个对象执行多项操作
}
} ///:~
若想在构建器中调用另一个过载构建器,可用this,但必须是第一件做的事,且不能在一个构建器内调用两次。
我们不可从一个static 方法内部发出对非static 方法的调用,但反过来可以。
事实上,那正是static方法最基本的意义。它就好象我们创建一个全局函数的等价物(在C 语言中)。
4.3清除:收尾和垃圾收集
Java可用GC回收再也用不到的对象占据的内存,但并非用new分配的内存(特殊情况),GC是不会回收的。
为解决这个问题,Java提供一种名为finalize()的方法,它的工作原理在理想状态下是这样的:
GC准备好释放对象所占的内存,首先调用finalize(),然后在下一次垃圾收集过程中,才真正回收对象的内存。
注意:Java的GC并不等于C++的Destructor(破坏器)。
重点一、垃圾收集并不等于“破坏”。重点二、对象也可能不会当作垃圾被收掉!
4.3.1finalize()用途何在
重点三、垃圾收集只跟内存有关!
GC将finalize的用途限制到特殊情况,即我们用C写的本地接口生成的对象是不会被GC回收的,这时就需要重写finalize。
4.3.2必须执行清除
finalize()最有用处的地方之一是观察垃圾收集的过程,它可能在任何时间运行。
在创建了一定数量的对象后,GC便开始回收(finalize()开始被调用),在GC开始运行后的某些时候,程序会停止创建对象。
注意:GC并不会根据对象的创建顺序进行回收!
4.4成员初始化
若将Primitive设置成类成员可以被任何一个方法初始化,因此在使用数据前强迫程序员给其一个初始值显得不合理。然而局部成员的情况恰巧与类成员相反,因此要求程序员给其一个初始值。
4.4.1规定初始化
自己为变量赋予初始值有一个最直接的做法,就是在类内部定义变量的同时也为其赋值。
注意:编译器对初始化的顺序有直接关系,它只能“向后引用”。
4.4.2构建器初始化
用构建器执行初始化,可获得更大灵活性,但请注意自动初始化会先于构建器初始化。
譬如:class Counter { int i; Counter() { i = 7; } } // i会先自动初始化为0,再由构建器赋值为7
1)类型为XXX的对象首次创建时,Java解释器必须找到XXX.class。2)XXX.class的所有static初始化模块都会运行。static初始化在Class对象首次载入时仅发生一次。
3)XXX的构建进程会在Heap中为一个XXX对象分配足够多的存储空间。
4)初始化顺序:static成员=》类成员=》构建器
对于static成员与非static成员,Java1.1开始都提供一种初始化方法。
eg1: static{ | eg2: {
c1 = new C(); | c2 = new C();
} | }
4.5数组初始化
初始化数组2种方法,使用int请参照Primitive原则,使用Integer请参照Class原则。
4.5.1多维数组
4.6总结
与C++一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。
这种形式的错误很难发现,由于构建器的使用避免了这种错误。
在C++中,struction&destruction工作同样重要,Java中提供了Garbage会自动为所有对象释放内存,但付出相应的开销也是必须的。
==================================================20120905========================================
第5章 隐藏实施过程
“进行面向对象的设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔开。”
限制访问的目的在于,类创建者在修改库时不至于影响客户程序员对库的使用情况。
5.1包:库单元
我们用import 关键字导入一个完整的库时,就会获得“包”(Package)。譬如:import java.util.*;
为了在Internet上得到完整控制,避免重名的冲突,这里使用了一种特殊机制“Name Space”以便管理。
每个源码文件,通常称“编辑单元”,都有.java后缀,内部只能有一个public类(与编辑单元名字相同),其它类就能对外隐藏。
一个有效的Java程序就是一些列.class文件,它们可被封装和压缩到一个JAR文件里,由解释器负责寻找、装载和解释。
package关键字用于指定编辑单元位于哪个包中。
5.1.1创建独一无二的包名
按照约定,package名的第一部分是Internet域名,因此命名肯定不会重复。
解释器首先来到CLASSPATH中设置的"...\LIB"目录下,搜索将域名反转后生以“\”代替“.”形成的目录(通常是JAR),是否存在import关键字导入的库。如果存在同名.class文件,则自动编译日期最新的。譬如:CLASSPATH=.;D:\JAVA \LIB;C:\DOC\JavaT,import com.bruceeckel.util.*;则会到C:\DOC\JavaT\com\bruceeckel\util搜索。
5.1.2自定义库工具
无论什么时候只要做出了一个有用的新工具,就可将其加入自己的utils 目录。
5.1.3利用导入改变行为
在两个包下创建类名、方法名相同的库,前者用于测试,后者用于正式,当测试完成发行时,只需改变import。
5.1.4包的停用
用于调试的一种方法,在package前加上注释。
5.2Java访问指示符
5.2.1友好的
没有修饰符的情况,限于同包访问,通常称为“Friendly”或Default。
public可从所有类访问,protected可从自身和它的子类访问,private只能自身访问。
5.2.2public
5.2.3private
5.2.4protected
5.3接口与实现
隐藏实施细节出于两种原因:
1、我们可在结构里构建自己的内部机制,不用担心客户程序员将其当作接口的一部分,从而自由地使用或者“滥用”。
2、将接口同实施细节分离开,若用户只访问public的接口,我们就可大胆的修改非public的所有东西。
5.4类访问
类访问只能使用public或default进行限制限制。若想使其他类无法访问本类,可使用private对构造器进行限制。
5.5总结
本章讲述了如何构建类,从而制作出理想的库。首先,将一组类封装到一个库,其次,如何对类中的成员进行访问控制。
5.6练习
==================================================20120906========================================
第6章 类再生
“Java 引人注目的一项特性是代码的重复使用或者再生。
但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。”
在像C那样的程序化语言里,代码的重用早已可行,但效果不是特别显著。
这一章将介绍两中方法:“合成”与“继承(Inheritance)”
6.1合成的语法
合成只需要在新类里简单地置入对象的句柄即可。
对于一个非基本类型的对象,若编译器本来希望一个String,但却获得某个这样的对象,就会自动调用toString()。
句柄初始化可在3种情况下进行:1、定义时。2、构建器中。3、实际使用前(可减少不必要开销)。
6.2继承的语法
继承使用extends关键字,这样子类可自动获得基类的所有数据成员以及方法。
要调用基类中的方法,可使用super.方法名()。
extends 关键字提醒我们准备将新方法加入基础类的接口里,对其进行“扩展”。
6.2.1初始化基础类
即使多重继承,初始化对struction的调用也会从最底层的基类开始,然后逐层转向子类。
若基类中只有带参struction,则必须在子类struction中使用super(var)进行主动调用,否则会抛出exception。
6.3合成与继承的结合
许多时候都要求将合成与继承两种技术结合起来使用。
6.3.1确保正确的清除
一旦希望清除什么东西,必须写一个特别的方法,明确、专门地做这件事。
同时,还要让用户知道,必须将这样的清除代码置入finally从句中。而try跟随的则是一个“警戒区”。
不能指望自己能确切知道何时会开始垃圾收集,除内存回收外,若想清除什么,请制作自己的清除方法。
6.3.2名字的隐藏
即使在衍生类中过载基类中的方法也是可行的。
6.4到底选择合成还是继承
根据2种方法的性质逆推理解:
如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。
//: Car.java
// Composition with public objects
class Engine {
public void start() {}
public void rev() {}
public void stop() {}
}
class Wheel {
public void inflate(int psi) {}
}
class Window {
public void rollup() {}
public void rolldown() {}
}
class Door {
public Window window = new Window();
public void open() {}
public void close() {}
}
public class Car {
public Engine engine = new Engine();
public Wheel[] wheel = new Wheel[4];
public Door left = new Door(), right = new Door(); // 2-door
Car() {
for(int i = 0; i < 4; i++)
wheel[i] = new Wheel();
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
} ///:~
如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。
6.5protected
protected能更好的为继承服务,因为实际应用中,常想把某些东西藏起来,但同时允许衍生类访问。
6.6积累开发
继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。
类的隔离效果非常好,甚至不需要方法的源代码,只要导入一个包。(这对合成或继承都是成立的)
注意:程序开发是一个不断递增或积累的过程,就像人们学习知识一样。
6.7上溯造型
继承是对新类和基础类之间的关系的一种表达。衍生类是基类的一种类型。
6.7.1何谓“上溯造型”?
类继承图的画法是根位于最顶部,再向下扩展,由于造型的方向是从基类到衍生类,因此成upcasting。
为判断自己应该使用合成还是继承,最简单的就是考虑是否需要从新类上溯造型回基类,避免继承滥用。
6.8final关键字
final关键字用于声明不可改变的东西,这是在考虑到设计或效率后开始使用的。
6.8.1final数据
final修饰primitive时,会使那个值无法改变。修饰句柄时,会使句柄仅能指向那个对象。
final primitive的赋值过程必须在定义时或struction中确定,但其值在以在运行期才确定(例如使用Random对其赋值)。
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
g.spin();
}
6.8.2final方法
目的在于:1、为方法“上锁”,防止继承类改变它。2、执行效率提升,对那个方法的所有调用都置入“嵌入”调用里。
由于不能访问一个private方法,所以它绝对不会被覆盖,自然成为了final。
6.8.3final类
将类定义成final 后,结果只是禁止进行继承——没有更多的限制。
6.8.4final的注意事项
1、我们很难预测一个类以后会以什么样的形式再生或重复利用。
2、如果考虑到代码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。
6.9初始化和类装载
在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程序。
C++在static数据初始化时,可能会因为需要的另一个static数据未初始化而造成问题。
Java则避免了这一问题,运行时,除非那个类的一个对象构造完毕,否则那段代码不会载入。
6.9.1继承初始化
基类static变量=》衍生类static变量=》primitive设成default、句柄设成null=》基类struction=》衍生类struction
6.10总结
典型情况下,使用合成实现现有类的“再生”或“重用”,将其作为新类型基础实施过程的一部分使用。
但如果要实现接口“再生”,就应该使用继承,它能使衍生类upcasting为基类。
尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用合成技术。
类不应继承太多功能,否则很难实现它,也不应太小,否则不能使用或添加新功能。
6.11练习
==================================================20120907========================================
第7章 多形性
“对于面向对象的程序设计语言,多形性(Polymorphism)是第三种最基本的特征(前两种是数据抽象和继承)。”
“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来。
通过合并各种特征与行为,封装技术可创建出新的数据类型。
通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。
7.1上溯造型
从衍生类向基类upcasting可能“缩小”那个接口,但不能把它变得比基类的完整接口更小。
7.1.1为什么要上溯造型
如果一系列Super Class都具有某个相同的方法与参数,不同只在于方法内的行为, 那么就没必要在调用它们的Class里重复编写能够接受这一些列Super Class的重载方法, 而只需要用一个能接受它们Base Class的方法即可,从而为了减少工作量。
7.2深入理解
那么,当upcasting时,接口是如何得知它该调用衍生类的方法还是基类的方法呢?这需要探讨一个主题“绑定(Building)”。
7.2.1方法调用的绑定
若在程序运行以前执行绑定(由编译器或链接程序等),称“早期绑定”。譬如C编译器只有“早期绑定”。
若在运行期间进行,以对象的类型为基础,称“后期绑定”或“动态绑定”或“运行期绑定”。
将一个方法声明为final,意味着不能改变它,会关闭“运行期绑定”。
7.2.2产生正确的行为
有一个经典的例子,Shape是一个基类,由它衍生出很多基类,Circle、Square、Triangle都是Shape类型。
向一个接收Shape对象的方法中传入它的任一衍生类,都能很好调用衍生类的draw方法,因为动态绑定已经介入。
为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对draw()的所有调用都是通过动态绑定进行的。
7.2.3扩展性
即使是衍生类的衍生类,通过Polymorphism机制也能很适应upcasting。
7.3覆盖与过载
“过载”是指同一样东西在不同的地方具有多种含义。
而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。
衍生类支持对基类的覆盖,也支持对基类的过载。
7.4抽象类和方法
显然,在Shape的例子中,基类的draw方法存在目的只是为了在不同的衍生类中表达不同含义,因此本身大可不用实现。
它为我们建立了一种基本形式,使我们能在所有衍生类里定义一些“通用”的东西。因此,可以把Shape称“抽象基础类”。
换言之,Shape的作用仅仅是表达接口,而不是一些具体的实施细节,可用abstract关键字修饰。
抽象类禁止实例化,且其中的抽象方法,都必须在衍生类中实现。如果衍生类中仍存在抽象方法,则衍生类必须为抽象。
7.5接口
interface关键字使抽象的概念更深入了一层,可以把它想象成完全的抽象类,其中的所有方法都不允许实现。
接口也包含了基本数据类型的数据成员,但它们都默认为static 和final。
7.5.1Java的“多重继承”
在C++中,将多个类合并到一起的行动称作“多重继承”,其操作较为不便,因为每个类都可能有一套自己的实施细节。
Java中也可以采取这一行为,但只有一个类拥有实施细节,所以不会带来C++那样的问题。
一个衍生类能implements多个interface和abstract,但仅能extends一个Base Class。(Base Class必须排于最前)
接口,使衍生类能upcasting到多个基类,又可防止客户程序员制作这个类的一个对象。
7.5.2通过继承扩展接口
接口可以继承接口,interface ExITFC extends BaseITFC,实现了扩展接口的新类也属于基础接口类型。
7.5.3常数分组
由于置入一个接口的所有字段都自动具有static和final属性(也是public的),所以接口是对常数值进行分组的一个好工具。
接口具有与C 或C++的enum 非常相似的效果,注意:常数命名全部采用大写字母。
7.5.4初始化接口中的字段
接口中字段是static 的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。
当然,字段并不是接口的一部分,而是保存于那个接口的static 存储区域中。
7.6内部类
内部类可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”,但它与合成有根本区别。
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 void ship(String dest) {
Contents c = cont();
Destination d = to(dest);
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p.ship("Tanzania");
Parcel2 q = new Parcel2();
// Defining handles to inner classes:
Parcel2.Destination d = q.to("Borneo"); // 若想获得内部类句柄,需要用外部类.内部类形式
}
}
7.6.1内部类和上溯造型
在upcasting中,将衍生类作为内部类使用,通过外部类方法返回衍生类对象,将获得完全隐藏(甚至衍生类类型)的效果。
7.6.2方法和作用域中的内部类
使用内部类的2方面原因:
1、我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
2、要解决一个复杂的问题,并希望创建一个不愿意公开的类,用来辅助自己的程序方案。
eg1方法内的内部类
public class Parcel4 {
public Destination dest(String s) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) { label = whereTo; }
public String readLabel() { return label; }
}
return new PDestination(); // 该方法返回的仍是一个有效的Destination类型
}
} // Destination d = Parcel4.dest("instance");返回的是一个PDestination对象,然后upcasting
eg2作用域中的内部类
if(b) { // 作用域中的内部类,离开了scope将不能使用
class TrackingSlip {
private String id;
TrackingSlip(String s) { id = s; }
String getSlip() { return id; }
}
/*eg3匿名内部类*/public Contents cont(){
public Contents cont(){ class MyContents extends Contents{
return new Contents(){ //... }; 同 //... }
} return new MyContents(); }
若想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。
7.6.3连接到外部类
内部类拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。
7.6.4static内部类
若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static,也包括内部类设为static。
static内部类可以成为interface的一部分,由于类是“静态”的,所以不会违反接口的规则。
7.6.5引用外部类对象
Parcel11.Contents c = p.new Contents();
7.6.6从内部类继承
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Won't compile
InheritInner(WithInner wi) { wi.super(); } // 继承内部类需要的特殊语法,让构造器获得外部类句柄
}
7.6.7内部类可以覆盖吗?
从外部类继承的时候,没有任何额外的内部类继续下去。(即继承不会使内部类会被覆盖)
但可以让内部类“明确”的通过构造器或方法继承。
public void insertYolk(Yolk yy) { y = yy; } // 基类中的方法
public BigEgg2() { insertYolk(new Yolk()); } // 衍生类中用继承后的内部类对象赋给基类句柄
7.6.8内部类标识符
内部类也必须生成相应的.class 文件,先是封装类的名字,再跟随一个$,再跟随内部类的名字
譬如:WithInner$Inner.class,如果内部类是匿名的,将由简单的数字代替。
7.6.9为什么要用内部类:控制框架
一个“应用程序框架”是指一个或一系列类,他们专门设计用来解决特定类型的问题。控制框架属于应用程序框架的一种特殊类型,叫做“由事件驱动的系统”(比如GUI),它可以通过内部类完美解决。
abstract public class Event { // The common methods for any control event
private long evtTime;
public Event(long eventTime) { evtTime = eventTime; }
public boolean ready() { return System.currentTimeMillis() >= evtTime; }
abstract public void action();
abstract public String description();
} ///:~
class EventSet {
private Event[] events = new Event[100];
private int index = 0;
private int next = 0;
public void add(Event e) {
if(index >= events.length)
return; // (In real life, throw exception)
events[index++] = e;
}
public Event getNext() {
boolean looped = false;
int start = next;
do {
next = (next + 1) % events.length;
// See if it has looped to the beginning:
if(start == next) looped = true;
// If it loops past start, the list is empty:
if((next == (start + 1) % events.length) && looped)
return null;
} while(events[next] == null);
return events[next];
}
public void removeCurrent() { events[next] = null; }
}
public class Controller {
private EventSet es = new EventSet();
public void addEvent(Event c) { es.add(c); }
public void run() {
Event e;
while((e = es.getNext()) != null) {
if(e.ready()) {
e.action();
System.out.println(e.description());
es.removeCurrent();
}
}
}
} ///:~
7.7 构建器和多形性
7.7.1 构建器的调用顺序
//: Sandwich.java
// Order of constructor calls
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()");
}
}
class Sandwich extends PortableLunch {
Bread b = new Bread();
Cheese c = new Cheese();
Lettuce l = new Lettuce();
Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~
对于一个复杂的对象,构建器的调用遵照下面的顺序:
(1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构建器的主体。
7.7.2 继承和finalize()
覆盖衍生类的finalize()时,务必记住调用finalize()的基础类版本。否则,基础类的初始化根本不会发生。
//: Frog.java
// Testing finalize with inheritance
class DoBaseFinalization {
public static boolean flag = false;
}
class Characteristic {
String s;
Characteristic(String c) {
s = c;
System.out.println("Creating Characteristic " + s);
}
protected void finalize() {
System.out.println("finalizing Characteristic " + s);
}
}
class LivingCreature {
Characteristic p = new Characteristic("is alive");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void finalize() {
System.out.println("LivingCreature finalize");
// Call base-class version LAST!
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Animal extends LivingCreature {
Characteristic p = new Characteristic("has heart");
Animal() {
System.out.println("Animal()");
}
protected void finalize() {
System.out.println("Animal finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Amphibian extends Animal {
Characteristic p = new Characteristic("can live in water");
Amphibian() {
System.out.println("Amphibian()");
}
protected void finalize() {
System.out.println("Amphibian finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
public class Frog extends Amphibian {
Frog() {
System.out.println("Frog()");
}
protected void finalize() {
System.out.println("Frog finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
public static void main(String[] args) {
if(args.length != 0 && args[0].equals("finalize"))
DoBaseFinalization.flag = true;
else
System.out.println("not finalizing bases");
new Frog(); // Instantly becomes garbage
System.out.println("bye!");
// Must do this to guarantee that all
// finalizers will be called:
System.runFinalizersOnExit(true);//请记住垃圾收集(特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动,System.runFinalizersOnExit(true)添加了额外的开销,以保证收尾工作的正常进行。
}
} ///:~
============================备注:以上笔记未经过整理&排版===========================
第8章 对象的容纳(20130114)
若非程序正式运行,我们根本不知道实际需要多少个对象,甚至不知道它们的准确类型,所以需要一种动态长度的“集合”来容纳运行过程中产生的不确定数量的对象。
8.1 数组
对象的容纳是本章的重点,而数组只是容纳对象的一种方式。
数组实际代表一个简单的线性序列。
Java中,对数组和集合都要进行范围检查,因此会损失一定的性能。
8.1.1 数组和第一类对象
基本数据类型数组和对象数组几乎一样,区别只在于它们所能容纳的类型。
8.1.2 数组的返回
C&C++只能返回一个数组的指针,我们不知道它会存在多久,这很容易造成内存“漏洞”,Java虽然类似,但GC会为我们自动回收。
8.2 集合
Java 提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)。
8.2.1 缺点:类型未知
使用Java集合的缺点是将对象置入集合时会丢失类型,不过Java会对错误的类型转换抛出异常。
但有时错误并不显露出来,比如编译器期待一个String对象,而得到一个Other对象,那么它会自动调用所有Java类都具备的toString()。
8.3 枚举型(反复器)
最初使用Vector,后来考了执行效率的原因,改变成一个List,我们该如何遍历不同集合中的对象呢?答案是Iterator。
但Java的Enumeration有一些限制(只能单方向移动),用集合的elements()返回一个Enumeration,调用nextElement()返回元素并指向下一个对象,hasMoreElements()检查是否有更多对象。
Enumeration无疑是Iterator的一种简单实现,除了单向移动不能做任何事。(虽然简单,但这种特定模式却是一个颇有价值的编程概念)
8.4 集合的类型
8.4.1 Vector
允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。
8.4.2 BitSet
8.4.3 Stack
有时被称为后入先出LIFO。
8.4.4 Hashtable
类似Dictionary,以Key-Value形式保存对象。
8.4.5 再论枚举器
Enumeration可以穿越一个序列的操作与那个序列的基础结构分隔开。
8.7.5 决定实施方案
从早些时候的那幅示意图可以看出,实际上只有三个集合组件:Map,List 和Set。而且每个接口只有两种或三种实施方案。若需使用由一个特定的接口提供的功能,如何才能决定到底采取哪一种方案呢?为理解这个问题,必须认识到每种不同的实施方案都有自己的特点、优点和缺点。比如在那张示意图中,可以看到Hashtable,Vector 和Stack 的“特点”是它们都属于“传统”类,所以不会干扰原有的代码。但在另一方面,应尽量避免为新的(Java 1.2)代码使用它们。其他集合间的差异通常都可归纳为它们具体是由什么“后推”的。换言之,取决于物理意义上用于实施目标接口的数据结构是什么。例如,ArrayList,LinkedList 以及Vector(大致等价于ArrayList)都实现了List 接口,所以无论选用哪一个,我们的程序都会得到类似的结果。然而,ArrayList(以及Vector)是由一个数组后推得到的;而LinkedList 是根据常规的双重链接列表方式实现的,因为每个单独的对象都包含了数据以及指向列表内前后元素的句柄。正是由于这个原因,假如想在一个列表中部进行大量插入和删除操作,那么LinkedList 无疑是最恰当的选择(LinkedList 还有一些额外的功能,建立于AbstractSequentialList 中)。若非如此,就情愿选择ArrayList,它的速度可能要快一些。作为另一个例子,Set 既可作为一个ArraySet 实现,亦可作为HashSet 实现。ArraySet 是由一个ArrayList后推得到的,设计成只支持少量元素,特别适合要求创建和删除大量Set 对象的场合使用。然而,一旦需要在自己的Set 中容纳大量元素,ArraySet 的性能就会大打折扣。写一个需要Set 的程序时,应默认选择HashSet。而且只有在某些特殊情况下(对性能的提升有迫切的需求),才应切换到ArraySet。
决定使用何种List?性能测试后发现,在ArrayList 中进行随机访问(即get())以及循环反复是最划得来的;但对于LinkedList 却是一个不小的开销。但另一方面,在列表中部进行插入和删除操作对于LinkedList 来说却比ArrayList 划算得多。我们最好的做法也许是先选择一个ArrayList 作为自己的默认起点。以后若发现由于大量的插入和删除造成了性能的降低,再考虑换成LinkedList 不迟。
决定使用何种Set?性能测试后发现,最后对ArraySet 的测试只有500 个元素,而不是1000 个,因为它太慢了。进行add()以及contains()操作时,HashSet 显然要比ArraySet 出色得多,而且性能明显与元素的多寡关系不大。一般编写程序的时候,几乎永远用不着使用ArraySet。
决定使用何种Map?性能测试后发现,即使大小为10,ArrayMap 的性能也要比HashMap 差——除反复循环时以外。而在使用Map 时,反复的作用通常并不重要(get()通常是我们时间花得最多的地方)。TreeMap 提供了出色的put()以及反复时间,但get()的性能并不佳。但是,我们为什么仍然需要使用TreeMap 呢?这样一来,我们可以不把它作为Map 使用,而作为创建顺序列表的一种途径。树的本质在于它总是顺序排列的,不必特别进行排序(它的排序方式马上就要讲到)。一旦填充了一个TreeMap,就可以调用keySet()来获得键的一个Set“景象”。然后用toArray()253产生包含了那些键的一个数组。随后,可用static 方法Array.binarySearch()快速查找排好序的数组中的内容。当然,也许只有在HashMap 的行为不可接受的时候,才需要采用这种做法。因为HashMap 的设计宗旨就是进行快速的检索操作。最后,当我们使用Map 时,首要的选择应该是HashMap。
第9章 违例控制
第10章 Java IO系统