Java编程思想第三版摘录
===========
Chap1 对象简介
1.抽象的过程
Alan Kay总结了Smalltalk的五项基本特征。这些特征代表了纯的面向对象的编程方法:
(1).万物皆对象。将对象想成一种特殊的变量;它存储数据,而且还可以让你“提要求”,命令它进行某些操作。从理论上讲,你可以把所有待解决的问题中的概念性组件(狗,建筑,服务等)都标识成程序里的对象。
(2).程序就是一组相互之间传递消息的对象。你只要向那个对象“发一个消息”,就能向它提出要求。更确切的说,你可以这样认为,消息是调用专属某个对象的方法的请求。
(3).每个对象都利用别的对象来组建它自己的记忆。换言之,你通过将已有的对象打成一个包,来创建新的对象。由此,你可以将程序的复杂性,隐藏在对象的简单性之下。
(4).对象都有类型。任何对象都是某个类的实例(instance of a class)。用以区分类的最突出的特点就是“你能传给它什么消息?”
(5).所有属于同一类型的对象能接受相同的消息。这种互换性(substitutability)是OOP最强大的功能之一。
Booch还给对象下了个更为简洁的定义:
对象有状态,行为和标识。
这就是说,对象可以有内部数据(状态),有方法(因而产生了行为),以及每个对象都能同其它对象区分开来--具体而言,每个对象在内存里都有唯一的地址。
这句话或许有点太过了。因为对象还能存在于另一台及其上以及不同的内存空间中,此外还能保存在硬盘上。在这种情况下,对象的身份就不能用内存地址,而必须要用别的方法来确定。
2.可凭借多态性相互替换的对象
非OOP的编译器的做法称为前绑定(early binding)。编译器会产生那个名字的函数的调用,而连接器负责将这个调用解析成须执行的代码的绝对地址。在OOP中,不到运行的时候,程序没法确定代码的地址,所以向泛型对象发送一个消息的时候,就要用到一些特别的手段。
OOP语言用了后绑定(late binding)的概念。当你向某个对象送了一个消息后,不到运行时,系统不能确定到底该调用哪段代码。编译器只保证这个方法存在,并且检查参数和返回值的类型(不这么做的语言属于弱类型weakly typed),但是它并不知道具体执行的是哪段代码。
在有些语言中,你必须明确申明,某个方法要用到后绑定的灵活性(C++用virtual关键字)。在这些语言中,方法不是默认地动态绑定的。而动态绑定是Java的缺省行为,因此无需添加什么额外的关键词就能获得多态性。
将派生类当作它的基类来用的过程称为上传(upcast),反之称为下传(downcast)。下传所需的运行时检查会引起程序运行效率的降低,也加重了编程的负担。解决方案就是参数化类型(parameterized type)机制,即泛型。
3.Collection和迭代器
ArrayList和LinkedList,都是简单的线性序列,具有相同的接口和外部行为。对于ArrayList,随机访问是一种时间恒定的操作。然而对于LinkedList,随机访问和选取元素的代价会很大。另一方面,如果要在序列中插入元素,LinkedList的效率会比ArrayList的高出许多。
=============
Chap2 万物皆对象
1.数据存在哪里
数据可以存储在以下六个地方:
(1).寄存器(registers)。这是反应最快的存储,因为它处在CPU里。但寄存器数量有限,由编译器分配,你不能直接控制。
(2).栈(stack)。位于常规内存区里,CPU可以通过栈指针对它进行直接访问。栈指针下移就创建新的存储空间,上移就释放内存空间。这是仅次于寄存器的最快、最有效率的分配内存的方法。由于Java编译器必须生成能控制栈指针上下移的代码,所以程序编译的时候,那些将被存储在栈中的数据的大小和生命周期必须是已知的。Java把对象的reference存放在栈里。
(3).堆(heap)。这是一段多用途的内存池,所有Java对象都保存在这里。在堆中分配空间时,编译器无需知道该分配多少空间,或数据会在堆里待多长时间。但是其速度比分配栈的慢些。
(4).静态存储(static storage)。这里“静态”的意思是“在固定的位置”(尽管还是在RAM里面)。静态存储里面的数据在整个程序运行期间都能访问到。可以用static关键词指明对象的某个元素是静态的,但是Java对象本身是决不会放到静态存储中去的。
(5).固定存储(constant storage)。常量值通常直接放在程序里。有时常量还能为自己设置界限,这样在嵌入式系统中,就能选择是不是把它们放到ROM里面去。
(6).非内存的存储(Non-RAM storage)。如果数据完全独立于程序,那么即使程序不运行,它也应该还在。对象被转化成某种能保存在其它介质上的东西,要用的时候,又能在内存里重建。Java提供了轻量级persistence的支持。
特例:primitive类型
primitive(原始)类型的变量直接保存值,并且存储在栈中。
高精度的数值
Java还包括两个能进行高精度算术运算的类:BigInteger和BigDecimal。
作用域
int x = 12;
{
int x = 100;//illegal
}
2.创建新的数据类型:类
只有在“变量被用作类的成员”时,Java才能确保它获得默认值。本地变量,没有这种保障。
不管在哪种情况下,Java在传递对象的时候,实际上是在传递reference。
============
Chap3 控制程序流程
1.运算符
逗号运算符
Java里面,唯一一个把逗号当运算符用的地方是for循环。
String的+运算符
加号(+)用在String上的时候,如果表达式中有String,那么Java编译器会把其他的操作数都转换成String。
Java没有sizeof
C和C++的sizeof()用于获取数据要占用多少字节的内存,需要sizeof的主要原因是为了移植。相同的数据类型在不同的机器上占用的内存长度可能会不一样。
Java没有移植的问题,因此不需要sizeof,所有数据类型在所有的机器上都是相同的。
运算符的总结
在进行数学运算或混和赋值的时候,char,byte,short,都会先进行提升,运算结果也是int。如果要把结果赋给原先那个变量,就必须明确地进行类型转换。
除了boolean之外,所有的primitive类型都能被转换成其它的primitive类型。
2.执行控制
Java不允许把数字当作boolean用,尽管C和C++允许这么做(非零值表示true,零表示false)。
Java里,唯一能放标签的地方,就是在循环语句的外面。而且必须直接放--在循环语句和标签之间不能有任何东西。而这么做的唯一理由就是,你会嵌套多层循环或选择。因为通常情况下break和continue关键词只会中断当前循环,而用了标签后,就会退到label所在的地方。
label1:
outer-iteration{
inner-iteration{
break;//中断内循环,退到外循环
continue;//中断本次内循环,重新移到内循环开始处,执行下次内循环
continue label1;//中断本次外循环,移到外循环开始处,重新执行下次外循环
break label1;//退出外循环,执行循环以后的语句
}
}
如果退出循环或选择的同时,还要退出方法,可以直接使用return。
continue,break以及label的规则:
(1).普通的continue会退到内部循环的最开始,然后继续执行内部循环。
(2).带标签的continue会跳转到标签,并且重新进入直接跟在标签后面的循环。
(3).break会从循环的“底部溜出去”。
(4).带标签的break会从由这个标签标识的循环的“底部溜出去”。
在Java里能使用标签的唯一理由就是,在嵌套循环的同时要用break和continue退出多层循环。
3.switch
switch会根据整数表达式的值(可以是char)决定应该运行哪些代码。
找到匹配的值后,就会执行相应的case语句,不会再进行比较。通常case语句应该以break结束。否则会直接执行下一个case语句,而不会再次进行匹配。如果没有匹配的case,则执行default语句。
计算细节
将float或double转换成整数的时候,它总是将后面的小数截去。
Math.random()会生成一个double,值域是[0,1)。
============
Chap4 初始化与清理
1.用构造函数确保初始化
构造函数的名字必须与类的名字大小写完全相同。构造函数本身没有返回值,虽然new表达式会返回新创建对象的reference。
2.默认的构造函数
如果你写了一个没有构造函数的类,编译器会自动创建一个默认的构造函数(不带参数)。但是,只要你定义了构造函数,不管带不带参数,编译器就不会再自动合成默认构造函数了。
3.this关键字与构造函数
在类的构造函数中,可以用this(arg)调用另一个构造函数,但是不能调用两个构造函数(即this()在构造函数中只能出现0或1次)。此外,必须在程序代码的最前面调用构造函数。在非构造函数的方法里,不能用this()调用同一个类的构造函数。
4.static的含义
类的static方法只能访问其他的static方法和static数据成员,不能在static方法调用非static方法,但是反过来是可以的。
但是,如果可以传一个对象的reference给static方法,就可以通过这个reference调用非static方法和非static数据成员。但要达到这个目的,通常应该使用非static的方法。
5.清理:finalize和垃圾回收
java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。
(1).对象不一定会被回收。
(2).垃圾回收不是拆构函数。
(3).垃圾回收只与内存有关。
(4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。
6.成员的初始化
方法中的局部变量,使用前必须进行初始化,否则编译器会给出错误消息。
类的成员数据会被自动初始化,对于primitive变量,会被清零(char型也为0);对于对象reference,会被赋值为null。
7.指定初始化
对类成员数据进行初始化时,可以调用方法获取初始值,该方法如果有参数,参数不能是类的其他尚未初始化的数据成员。可以这样:
class Test{
int i=f();
int j=g(i);
}
不能这样:
class Test{
int j=g(i);//i尚未初始化
int i=f();
}
8.初始化的顺序
对类而言,初始化的顺序是由变量在类里定义的顺序决定的,变量的初始化会优先于任何方法,包括构造函数。
创建对象的过程(小结,以Dog类为例)
(1).第一次创建Dog类的对象(构造函数实际上是static方法),或者第一次访问Dog类的static的方法或字段的时候,Java解释器会搜寻classpath,找到Dog.class.
(2).装载了Dog.class之后,(创建了Class对象之后),会对所有的static数据进行初始化。这样第一个装载Class对象的时候,会先进行static成员的初始化。
(3).用new Dog()创建新对象的时候,Dog对象的构建进程会先在堆(heap)里为对象分配足够的内存。
(4).这块内存先被清零,自动把Dog对象的primitive类型的成员赋缺省值(对于数字是零,或是相应的boolean和char),将reference设成null。
(5).执行定义成员数据时所作的初始化。
(6).执行构造函数。可能牵涉到继承关系的很多活动。
9.非静态的实例初始化
实例初始化(instance initialization)语句,除了没有static关键字,其他与静态初始化没有两样。这种语法为匿名内部类(anonymous inner class)的初始化提供了必不可少的支持。
10.数组的初始化
Java可以将一个数组赋给另外一个,实际上是复制reference.
可以用花括号括起来的对象列表初始化对象数组。有两种形式:
Integer[] a = {
new Integer(1),
new Integer(2),
};
Integer[] b = new Integer[]{
new Integer(1),
new Integer(2),
};
列表的最后一个逗号是可选的,这个特性能使长列表的维护工作变得简单一些。
===============
Chap5 隐藏实现
1.package:类库的单元
Java的源代码文件通常被称为编译单元(compilation unit),必须是一个以.java结尾的文件,其中必须有一个与文件名相同的public类(大小写也必须相同,但不包括.java扩展名)。每个编译单元只能有一个public类。
编译.java文件的时候,里面的每个类都会产生输出,文件名是类名,扩展名是.class。
创建独一无二的package名字
package语句必须是文件里的第一个非注释行。
如果使用JAR文件打包package,必须把文件名放到CLASSPATH里面。
冲突
如果两个import语句所引入的类库都包含一个同名的类,只要不写会引起冲突的代码(不使用这个同名的类),就一切OK,否则编译器会报错。
不论哪种对象,只要放进了String的表达式,就会被强制转化成该对象的String表示形式。如:
System.out.print(""+100);//Force it to be a String
2.Java的访问控制符
Java的一个访问控制符只管它所定义的这一项,而C++的访问控制符会一直管下去,直到出现另一个。
private:除非是用成员所在类的方法,否则一律不得访问。
package访问权限(package access,有时也称为“friendly”),是默认的访问权限,没有关键词。同属一个package的类都能访问这个成员,但是对于不属于这个package的类来说,这个成员就是private的。
protected:继承的访问权限。protected也赋予成员package权限--同一个package里的其它类也可以访问protected元素。除此之外,继承类(不管是否在同一个package)也可以访问protected成员。
public:访问接口的权限,任何类都能访问。
3.类的访问权限
类只有两种访问控制权,public和package。(inner class 可以是private或protected)
每个编译单元(文件)只能有一个public类,也可以没有public类(不常见)。public类的名字必须和编译单元文件名大小写完全相同。如果没有public类,就可以随意给编译单元文件起名。
对于package权限的类,通常应该将方法也设成package权限。
如果不写类的访问控制符,默认是package权限的。package中的任何一个类都能创建这个类的对象,但是package以外的类就不行了。但是,如果这个类有一个public 的static成员,那么即使客户程序员不能创建这个类的对象,他们也还可以访问这个static成员。
==============
Chap6 复用类
1.合成所使用的语法
对象的reference作为类的成员时,会被自动初始化为null。此外可以在三个时间对类的reference成员进行初始化:
(1)在定义对象reference的时候。这就意味着,在类的构造函数调用前,已经初始化完毕了。
private String a=new String("hello"), b="world";
(2)在类的构造函数。
(3)在即将使用对象reference之前。这被称为lazy或delayed initialization,如果创建对象的代价很大,或不是每次都需要创建对象,这种做法就能降低程序开销。
2.基类的初始化
当你创建一个派生类对象的时候,这个对象里面还有一个基类的子对象(subobject)。
对于默认构造函数,即无参构造函数,Java会让派生类的构造函数自动地调用基类的构造函数。基类会在派生类的构造函数调用它之前进行初始化。
对于带参数的构造函数,必须用super关键字以及合适的参数明确地调用基类构造函数,super()语句必须是派生类构造函数的第一条语句。
3.确保进行妥善的清理
不要依赖垃圾回收器去做任何与内存回收无关的事情。如果要进行清理,一定要自己写清理方法,别去用finalize()。
清理的顺序:先按照创建对象的相反顺序进行类的清理,然后调用基类的清理方法。
4.名字的遮盖(重载方法的覆盖)
如果Java的基类里有一个被重载了好几次的方法,那么在派生类里重新定义那个方法,是不会把基类里定义的任何一个给覆盖掉的。(在C++里,就会把基类方法全都隐藏起来)
5.用合成还是继承
合成用于新类要使用旧类的功能,而不是其接口的场合。继承则是要对已有的类做一番改造,以获得一个特殊版本,即将一个较为抽象的类改造成能适用于某些特定需求的类。
合成要表达的是“有(has-a)”关系,继承要表达的是一种“是(is-a)”关系。
判断该用合成还是继承时,可以问一下是不是会把新类上传给基类。如果必须上传,那么继承就是必须的。
6.final关键词
6.1 final的数据
常量能用于两种情况:(1)编译时的常量(compile-time constant),这样就再也不能改了;(2)运行时初始化的值,这个值你以后不想改了。
如果是编译时常量,编译器会把常量放到表达式中,可以降低运行时的开销。Java中这种常量必须是primitive的,要用final表示,这种常量的赋值必须在定义的时候进行。
当final修饰对象的reference时,表示reference是常量,初始化的时候,一旦将reference指向了某个对象,那么它就再也不能指向别的对象了。但是这个对象本身是可以修改的。
final int v1 = 10;//compile-time constants
final int v2 = rand.nextInt(100);//
final Value v3 = new Value();//v3的数据成员是可变的
final int[] arr={1,2,3};//arr的元素是可变的,如a[i]++;
空白的final数据(Blank finals),是指声明了final成员,却没有在声明时赋值。编译器会强制在构造函数中初始化final数据。通过使用带参数的构造函数,根据参数对空白的final数据进行初始化,可以在保持final数据不变性的同时,提供一定的灵活性。
可以把方法的参数声明为final的。
6.2 final方法
使用final方法有两个目的。第一,可以禁止派生类修改方法。第二,效率。对于final方法,编译器会把调用转换成“内联(inline)”,即用方法本身的拷贝来代替方法的调用。但是,如果方法很大,程序会很快膨胀,于是内联也不会带来什么性能的改善。
只有是基类接口里的东西才能被覆写。如果基类的方法是private的,那它就不属于基类的接口。即使在派生类里创建了一个同名的方法,它同基类中可能同名的private方法没有任何联系。
6.3 final类
把类定义成final的,可以禁止继承这个类。final类的数据可以是final的,也可以不是。final类的方法都隐含地变成final了。
7.初始化与类的装载
在传统的编程语言中,程序启动的时候都是一次装载所有的东西,然后进行初始化,接下来再开始执行。这些语言必须仔细控制初始化的顺序。
Java采用了一种新的装载模式。编译之后每个类都保存在它自己的文件里。不到需要的时候,这个文件是不会装载的。即“类的代码会在它们第一次使用的时候装载”,第一次访问static成员或创建对象的时候。
继承情况下的初始化=====
首次使用类的时候,装载器(loader)就会寻找类的.class文件,转载过程中,会依次追溯装载基类(不管是否创建基类对象,这个过程都会发生)。下一步,会执行“根基类(root base class)”的static初始化,然后是下一个派生类的static初始化,以此类推。
所有类都装载结束,就可以创建对象了。首先对象里所有成员数据会被初始化为缺省值,这个过程是一瞬间完成的,对象的内存会被设置成二进制0。然后开始构造基类,基类的构造过程及顺序与派生类相同,先对基类的变量按字面顺序进行初始化,再调用基类的构造函数(调用是自动发生的,但你可以用super关键字指定要调用基类的哪个构造函数)。之后会对派生类的变量按定义的顺序进行初始化,最后执行派生类构造函数其余代码。
=============
Chap7 多态性
1.方法调用的绑定
将方法的调用连到方法本身被称为“绑定(binding)”。当绑定发生在程序运行之前时(由编译器或连接器负责),被称作“前绑定(early binding)”。
“后绑定(late binding)”指在程序运行时,根据对象类型决定该绑定哪个方法。后绑定也被称为“动态绑定(dynamic binding)”或“运行时绑定(run-time binding)”。
除了static和final方法(private方法隐含有final的意思),Java的所有方法都采用后绑定。
2.错误:“覆写”private的方法
public class Parent{
private void f(){
//...
}
public static void main(String[] args){
Parent pa = new Child();
pa.f();//执行的是Parent.f()
}
}
class Child extends Parent{
public void f(){
//...
}
}
应该避免用基类的private的方法名去命名派生类中方法。
只有非private的方法才能被覆写。
3.继承与清理
清理的顺序应该与初始化的顺序相反。对数据成员而言,清理顺序就应该与声明的顺序相反。
在派生类的清理方法(比如dispose())的最后,应该调用基类的清理方法。
4.多态方法在构造函数中的行为
如果在构造函数里调用了动态绑定的方法,那么它会调用那个覆写后的版本。
abstract class Glyph{
abstract void 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){
radius = r;
System.out.println("RoundGlyph.RoundGlyph(),radius = " + radius);
}
void draw(){
System.out.println("RoundGlyph.draw(),radius = " + radius);
}
}
public class PolyConstructors{
public static void main(String[] args){
new RoundGlyph(5);
}
}
//输出========
//Glyph() before draw()
//RoundGlyph.draw(), radius = 0
//Glyph() after draw()
//RoundGlyph.RoundGlyph(), radius = 5
上面程序中,Glyph的构造函数调用了draw(),而这个调用最后落到了RoundGlyph.draw()。而此时radius的值还没有被初始化为1,还是0。
真正的对象初始化过程:
(1)类加载结束,并且static初始化结束后,在进行其它工作之前,分配给这个对象的内存会被初始化为二进制的0。
(2)构造基类,调用基类的构造函数。这时会调用被覆写的draw()方法(在调用RoundGlyph的构造函数之前),受第一步的影响,radius的值还是0。
(3)数据成员按照声明的顺序进行初始化。
(4)调用派生类的构造函数的正文。
5.用继承来进行设计
一般准则:使用继承来表示不同的行为,使用成员数据(合成)来表示不同的状态。
6.总结
人们通常会把多态性同Java的那些非面向对象的特性相混淆,比如方法的重载,它常常会被当作面向对象的特性介绍给大家。千万别上当:不是后绑定的,就不是多态性。
===========
Chap8 接口与内部类
1.接口(interface)
interface也可以包含数据成员,但是它天生就是public,static和final的。
interface默认是package权限的,只能用于同一个package。也可以加上public(只有保存在同名文件里的interface才可以加)。
可以把interface里的方法声明成public的,但是即便不写public关键词,这些方法也自动是public的。当implements一个interface的时候,必须把这个interface的方法定义成public的。
2.Java的“多重继承”
由于interface不带任何“实现”--也就是说interface和内存无关--因此不会有谁去阻挠interface之间的结合。
到底是用interface,还是用abstract类?只要基类的设计里面可以不包括方法和成员变量的定义,就应该优先使用interface。只有在不得不定义方法或成员变量的情况下,才把它改成abstract类。
3.合并接口时的名字冲突
在要合并的接口里面放上同名的方法,通常会破坏程序的可读性,应该避免。
4.用继承扩展interface
可以用继承往interface里添加新的方法,也可以把多个interface合并成一个新的interface。使用extends关键词,多个“基接口(base interface)”之间用逗号分割。
interface Vampire extends IMonster, IUndead{}
5.常量的分组
由于interface的数据成员自动就是public,static和final的,因此interface是一种非常方便的创建一组常量值的工具。这点同C和C++的enum很相似,但没有enum那样的类型安全。
6.接口的嵌套
接口既可以嵌套在类里,也可以嵌套在接口里面。
实现接口的时候,不一定要实现嵌套在里面的接口。同样,private接口只能在定义它的类里实现。
7.内部类(inner class)
内部类与合成是截然不同的。
除非是在“宿主类(outer class)”的非static方法里面,否则无论在哪里创建内部类的对象,都必须用OuterClassname.InnerClassName的形式来表示这个对象的类型。
8.内部类与上传
内部类可以被定义成private或protected,非内部类只可能是public或package权限的。
9.在方法和作用域里的内部类
在方法的某个作用域里定义的内部类,比如if语句里,并不意味着这个类的创建是有条件的--它会同别的东西一起编译。但是,这个类的访问范围仅限于定义它的那个作用域。除此之外,它同普通的类没有区别。
10.匿名内部类
public class Parcel6{
public Contents cont(){
return new Contents(){
private int i = 11;
public value(){return i;}
};//Semicolon required
}
}
上例中return new Contents(){...}语句表达的是:创建一个继承Contents的匿名类的对象。new语句所返回的reference对自动上传到Contents。这个return语句是如下代码的简化形式:
class MyContents implements Contents{
private int i = 11;
public int value(){return i;}
}
return new MyContents();
这个匿名内部类是通过默认构造函数来创建Contents的,如果基类需要的是一个带参数的构造函数,可以直接将参数传给基类的构造函数,return new Contents(x){...}。
如果在定义匿名内部类的时候,要用到外面的对象,编译器会要求把这个参数的reference声明成final的。
public class Parcel8{
//Argument must be final to use inside anonymous inner class
public Destination dest(final String ds){
return new Destination(){
private String lbl = ds;
public String readLabel(){return lbl;}
};
}
}
不能在匿名内部类里创建构造函数(因为它根本就没有名字),但是可以通过“实例初始化(instance initialization)”(与static初始化对应),进行一些类似构造函数的操作。实际上实例初始化过程就是匿名内部类的构造函数。但它的功能是有限的,由于不能重载实例初始化,因此只能有一个构造函数。
public Base getBase(int i){
return new Base(i){
{
System.out.println("Inside instance initializer.");
}
public void f(){
System.out.println("In anonymous f().");
}
};
}
11.与宿主类的关系
内部类能访问宿主类的所有成员。内部类对象里存在一个隐蔽的指向宿主类对象的reference,由编译器处理。
12.嵌套类(静态内部类)
如果不需要这种“内部类对象和宿主类对象之间的”联系,可以把内部类定义成static的,通常被称作“嵌套类(nested class)”。嵌套类的意思是:
(1)无需宿主类的对象就能创建嵌套类的对象。
(2)不能在嵌套类的对象里面访问非static的宿主类对象。
此外,嵌套类同普通的内部类还有一点不同。普通的内部类的成员数据和方法只能到类的外围这一层,因此普通的内部类里不能有static数据,static数据成员或嵌套类。但是,这些东西在嵌套类里都可以有。
嵌套类可以是interface的一部分。
每个类可以带一个供测试的main()方法,但编译后,会产生额外的代码。可以考虑在嵌套类里创建供测试的main()。编译后会产生单独的名称如Test$NestedTester.class的文件,发布的时候可以删除这个文件。
13.引用宿主类的对象
在内部类里通过宿主类名字后面加句点再加this来表示宿主类对象的reference。比如类Sequence.Selctor里,可以用Sequence.this来获取它所保存的宿主类Sequence对象的reference。
要在其他地方创建内部类的对象,就必须在new表达式里面给出宿主类对象的reference。如:
Parcel11 p = new Parcel11();
//Must use instance of outer class to create an instance of the inner class.
Parcel11.Contents c = p.new Contents();
14.在多层嵌套的类里向外访问
内部类的嵌套层次不是问题--它可以透明地访问它的各级宿主类的成员。
class MNA{
private void f(){}
class A{
private void g(){}
public class B{
void h(){
g();
f();
}
}
}
}
public class MultiNestingAccess{
public static void main(String[] args){
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
".new"语句指明了正确的作用域,因此无需在调用构造函数的语句里再限定类的名字了。
15.继承内部类
class WithInner(){
class inner{}
}
public class InheritInner extends WithInner.Inner{
//!InheritIner(){}//Won't compile
InheritIner(WithInner wi){
wi.super();
}
public static void main(String[] args){
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
InheritInner继承的只是内部类,默认的构造函数不能通过编译。必须传递宿主类对象的reference。此外,必须在构造函数里面使用这种语法:enclosingClassReference.super();这样才能提供那个必须的reference,才能编译通过。
16.内部类可以被覆写吗
像覆写宿主类的方法那样去“覆写”内部类,是不会有实际效果的。但是,可以在继承类的内部类明确继承基类的内部类。
public class BigEgg2 extends Egg2{
public class Yolk extends Egg2.Yolk{
}
}
17.本地内部类(Local inner classes)
本地内部类是在代码段,通常是方法的正文部分创建的。本地内部类不能有访问控制符,因为它并不属于宿主类,但它可访问当前代码段的final变量,以及宿主类的所有成员。
由于本地内部类的名字在代码段(通常是一个方法)外面是没法访问的,因此选择本地内部类来代替匿名内部类的一个理由就是,需要一个有名字的构造函数,并要重载这个构造函数,因为匿名内部类只能进行实例初始化。另一个理由是,需要创建多个那种类的对象。
18.内部类的标识符
宿主类名字,加上"$",再加上内部类的名字。如果是匿名内部类,编译器会直接用数字来表示。
如:OuterClass$InnerClass.class,OuterClass$2.class。
19.为什么要有内部类?
每个内部类都可以独立的继承某个“实现(implementation)”。内部类能在事实上继承多个实体类或abstract类。可以把它当作彻底解决多重继承问题的办法,接口部分地解决了这个问题。
========
Chap9 用异常来处理错误
1.重抛异常
如果直接重抛当前的异常,则printStackTrace()所打印出来的那些保存在异常对象里的信息,还会指向异常发生的地方,不会被指到重抛异常的地点。如果要装载新的栈轨迹信息,你可以调用fillInStackTrace()。
try{
f();
} catch(Exception e){
e.printStackTrace();
//throw e;//17
throw e.fillInStackTrace();//18
}
也可以抛出一个与捕捉到的异常不同的异常。这么做的效果同使用fillInstackTrace()的差不多--异常最初在哪里发生的信息被扔了,现在里面保存的时抛出新异常的地点。
2.异常链
异常链可以在捕捉到一个异常并且抛出另一个异常的时候,仍然保存前一个异常的信息。
在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造函数,它们是Error,Exception和RuntimeException。如果要链接其它异常,就不能用构造函数,而只能用initCause()方法了。
3.错误:丢失的异常
当finally语句出现异常时,会取代前面try..catch语句抛出的异常。这种“第一个异常尚未得到处理,就产生了第二个异常”的现象,属于想当严重的错误。
4.加在异常上面的限制
覆写方法的时候,只能抛出这个方法在基类中的版本所声明的异常,或其子异常,也可以不抛出异常。
即使继承类同时要实现interface(且该interface中方法抛出了不同于基类方法异常),也不能扩大派生类方法的异常说明接口。
这种异常方面的限制对构造函数不起作用。派生类的构造函数只会根据自己需要抛出异常。然而,派生类的构造函数会调用基类构造函数,所以必须在派生类的构造函数的异常说明中声明,基类构造函数所抛出的异常。注意,派生类的构造函数不能捕获任何由基类构造函数抛出的异常。
在继承和覆写的过程中,方法的异常说明的接口变小了。
异常说明本身并不属于方法的特征。
5.其它方法
异常处理的一个重要准则就是“如果你不知道该如何处理这个异常,你就别去捕捉它”。
“所有模型都是错的,但有些是能用的。”
6.观点
好的编程语言能帮程序员写出好程序。但是无论哪种语言挡不住你去写坏程序。
7.将checked exception转换成unchecked exception
调用一个方法,然后发现不知道怎样处理这个异常,但是又不能把它私吞。可以直接把checked exception包进RuntimeException里面,如下:
void throwRuntimeException(){
try{
//to do sth
} catch(IOException e){
throw new RuntimeException(e);
}
}
在异常处理程序中捕获这个包装后的异常,进行处理,还可以用getCause()获取包装前的原始异常。如下:
try{
throwRuntimeException();
} catch(RuntimeException re){
try{
throw re.getCause();
} catch(IOException e){
//deal with IOException
}
}
=========
Chap10 检测类型
运行时类型识别(run-time type identification,缩写为RTTI)
1.Class对象
类的常规对象是由Class对象创建的。
程序里的每个类都要有一个Class对象。这个对象存储在同名的.class文件里。程序运行时,需要创建一个这个类的对象时,JVM会检查是否装载了那个Class对象。如果没有,JVM就会去找到那个.class文件,然后装载。一旦那种类型的Class对象被装进了内存,所有那个类的对象就会由它来创建了。
Class对象同其它对象一样,也可以用reference来操控(装载器会处理)。
静态方法Class.forName(String)会返回Class对象的reference,该方法可能会抛出异常。
2.Class常数
Java还提供了另一种获取Class对象的reference的方法,即class常数:ClassName.class。这种方法更安全,因为它是在编译时做检查的,它不会像Class.forName()那样抛出异常。此外,由于没有方法调用,它的执行效率也更高一些。
Class常数可用于普通类,接口,数组和primitive类型。此外,每种primitive的wrapper类还有一个名为TYPE的数据成员,能返回“与这种wrapper类相关联的primitive”的Class对象的reference。如下:
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE
3.转换之前先作检查
Java中RTTI的形式有:
(1).经典的强制类型转换:如“(Circle)Shape”。
(2).代表对象类型的Class对象。
(3).instanceof关键词。
if(o instanceof Dog)
((Dog)o).bark();
instanceof的限制很严,只能拿它跟类名,而不是Class对象作比较。
4.动态的instanceof
ClassObejct.isInstance()方法还提供了一种动态调用instanceof的方法。
Dog.class.isInstance(o);
isInstance()能完全替代instanceof。
ObjectName.getClass()也可获取Class对象的reference。
5.Reflection:运行时的类信息
除了Class类,还有一个类库,java.lang.reflect也支持reflection。这个类库里有Field,Method和Constructor类(它们都实现了Member接口)。
使用reflection与未知类打交道的时候,JVM会和普通的RTTI一样,先检查这个对象是否属于那个具体类型,然后仍然必须先装载Class对象,也就是,不管从本地还是从网络,JVM必须找到那个.class文件。
RTTI与reflection的真正区别在于,RTTI时在编译时让编译器打开并检查.class文件。而对于reflection,编译时得不到.class文件,它是在运行时打开并检查那个文件。
========
Chap11 对象的集合
1.数组
数组与其它容器的区别体现在三个方面:效率,类型识别,以及可以持有primitives。
数组是Java提供的,能随机存储和访问reference序列的诸多方法中,最高效的一种。速度的代价是,当一个数组创建后,容量就固定了。
创建数组的时候,同时指明了数组元素的类型。而泛型容器类如List,Set和Map等,所持有的对象均被上传为Object。
2.数组是第一流的对象
数组的标识符实际上是一个“创建在堆(heap)里的实实在在的对象的”reference。这个对象持有其它对象的reference,或直接持有primitive类型的值。
3.Arrays类
java.util.Arrays类,包括了一组可用于数组的static方法。其中asList()方法,可把数组转成一个List。
Arrays.fill()方法,把一个值或对象的reference拷贝到数组的各个位置,或指定的范围。
4.复制一个数组
相比for循环,System.arrayCopy()能以更快的速度拷贝数组。如果是对象数组,拷贝的是数组中对象的reference,对象本身不会被拷贝。这被称为浅拷贝(Shallow copy)。
5.数组的比较
Arrays提供了equals()方法。数组是否相等是基于其内容的。
数组要想完全相等,它们必须有相同数量的元素,且数组的每个元素必须与另一个数组的对应位置上的元素相等。
元素的相等性,用equals()判断。对于primitive,会使用其wrapper类的equals()。
6.数组元素的比较
实现比较功能的一个方法是实现java.lang.Comparable接口。这个接口只有一个compareTo()方法。
Arrays.sort()会把传给它的数组的元素转换成Comparable。如果数组元素没有实现Comparable接口,就会引发一个ClassCastException。
实现比较功能的另一个方法使用策略模式(strategy design pattern),即实现Comparator接口。
Arrays.sort()可接受一个数组和一个Comparator,根据Comparator的compare()方法对数组元素排序。
Java标准类库所用的排序算法已经作了优化--对于primitive,它用的是快速排序(Quicksort),对于对象,它用的是稳定合并排序(stable merge sort)。
7.查询有序数组
一旦对数组进行了排序,就能用Arrays.binarySearch()进行快速查询了。但切忌对一个尚未排序的数组使用binarySearch()。
如果Arrays.binarySearch()查找到了,就返回一个大于或等于0的值。否则返回负值。这个负值的意思是,如果手动维护这个数组,这个值应该插在哪个位置。这个值是:
-(插入点)-1
“插入点”就是,在所有比要找的值更大的值中,最小的那个值的下标。如果数组中所有值都比要查找的值小,它就是a.size()。
如果数组里有重复元素,binarySearch()不能保证返回哪一个,但也不报错。
如果排序的时候用到了Comparator,那么调用binarySearch()的时候,也必须使用同一个Comparator。
8.数组部分的总结
如果要持有一组对象,首选,同时效率最高的,应该是数组。如果是要持有一组primitive,也只能用数组。
9.容器简介
Java2的容器类要解决“怎样持有对象”,它把这个问题分成两类:
(1).Collection:通常是一组有一定规律的独立元素。List必须按特定的顺序持有这些元素,而Set不能保存重复的元素。
(2).Map:一组以“键-值”(key-value)形式出现的pair。Map可以返回键(Key)的Set,值的Collection,或者pair的Set。
10.填充容器
Collection也有一个辅助类Collections,它包含了一些静态的使用工具方法,其中有fill()。fill()只是把同一个对象的reference负值到整个容器,而且只能为List,不能为Set和Map工作。并且这个fill()只能替换容器中的值,而不是往List加新元素。如:
List list = new ArrayList();
for(int i = 0; i<10; i++)
list.add("");
Collections.fill(list, "Hello");
11.容器的缺点:不知道对象的类型
Java的容器只持有Object。容器对“能往里面加什么类型的对象”没有限制。在使用容器中的对象之前,还必须进行类型转换
12.迭代器
迭代器(iterator),又是一个设计模式。iterator能让程序员在不知道或不关心他所处理的是什么样的底层序列结构的情况下,在一个对象序列中前后移动,并选取其中的对象。iterator是“轻量级”的对象,即创建代价很小的对象。
不经意的递归(Unintended recursion)
public class A{
public String toString(){
return "A address:" + this +"/n";//
}
public static void main(String[] args){
System.out.println(new A());
}
}
上面的程序会出现无穷无尽的异常。
"A address:" + this ,编译器会试着将this转换成String,要用大toString(),于是就变成递归调用了。
如果想打印对象的地址,应该调用Object的toString()方法。而不要用this,应该写super.toString()。
13.List的功能
ArrayList,一个用数组实现的List。能进行快速的随机访问,但是往列表中插入和删除元素比较慢。
LinkedList,对顺序访问进行了优化。在List中插入和删除元素代价也不高。但是随机访问的速度相对较慢。可以把它当成栈(Stack),队列(queue)或双向队列(deque)来用。
14.Set的功能
加入Set的每个元素必须是唯一的。要想加进Set,Object必须定义equals(),才能标明对象的唯一性。
HashSet,为优化查询速度而设计的Set。要放进HashSet的Object还要定义hashCode()。
TreeSet,一个有序的Set,能从中提取一个有序序列。用了红黑树(red-black tree)数据结构。
LinkedHashSet,使用链表的Set,既有HashSet的查询速度,又能保存元素的插入顺序。用Iterator遍历Set的时候,它是按插入顺序进行访问的。
Set要有一个判断以什么顺序来存储元素的标准,也就是说必须实现Comparable接口,并且定义compareTo()方法。
15.SortedSet
SortedSet(只有TreeSet这一个实现可用)中的元素一定是有序的。SortedSet的意思是“根据对象的比较顺序”,而不是“插入顺序”进行排序。
16.Map的功能
如果知道get()是怎么工作的,就会发觉在ArrayList里面找对象是相当慢的。而这正是HashMap的强项。HashMap利用对象的hashCode()来进行快速查找。
Map的keySet()方法返回一个由Map的键组成的Set。values()返回的是由Map的值所组成的Collection。由于这些Collection的后台都是map,因此对这些Collection的任何修改都会反映到Map上。
17.SortedMap
SortedMap(只有TreeMap这一个实现)的键肯定是有序的。
18.LinkedHashMap
为提高速度,LinkedHashMap对所有东西都作了hash,而且遍历的时候,还会按插入顺序返回pair。此外,还可通过构造函数进行配置,让它使用基于访问的LRU(least-recently-used)算法,这样没被访问过的元素(通常也是要删除的候选对象)就会出现在队列的最前面。
19.散列算法与Hash数
要想用自己的类作HashMap的键,必须覆写equals()和hashCode()。HashMap用equals()来判断查询用的键是否与表里其它键相等。
Object的hashCode(),在缺省情况下就是返回对象的内存地址。
一个合适的equals()必须做到以下五点:
(1).反身性:对任何x,x.equals(x)必须是true。
(2).对称性:对任何x和y,如果y.equals(x)是true的,那么x.equals(y)也必须是true。
(3).传递性:对任何x,y和z,如果x.equals(y)是true,且y.equals(z)也是true,那么x.equals(z)也必须是true。
(4).一致性:对任何x和y,如果对象里面用来判断相等性的信息没有修改过,那么无论调用多少次x.equals(y),它都必须一致地返回true或false。
(5).对于任何非空的x,x.equals(null)必须返回false。
默认的Object.equals()只是简单地比较两个对象的地址,所以一个Dog("A")会不等于另一个Dog("A")。
下面是覆写equals()和hashCode()的例子。
public class Dog{
public int id;
public Dog(int x){ id = x; }
public int hashCode(){ return id; }
public boolean equals(Object o){
return (o instanceof Dog) && (id == ((Dog)o).id)
}
}
equals()在利用instanceof检查参数是不是Dog类型的同时,还检查了对象是不是null,如果是null,instanceof会返回false。
20.理解hashCode()
数组是最快的数据结构,所以很容易想到用数组存储Map的键的信息(而不是键本身)。Map要能存储任意数量的pair,而键的数量又被数组的固定大小限制了,所以不能用数组存储键本身。
要解决定长数组的问题,就得允许多个键生成同一个hash数,也就是会有冲突,每个键对象都会对应数组的某个位置。
查找过程从计算hash数开始,算完后用这个数在数组里定位。如果散列函数能确保不产生冲突(如果对象数量是固定的,这是可能的),那么它就被称为“完全散列函数”,这是特例。通常,冲突是由“外部链(external chaining)”处理的:数组并不直接指向对象,而是指向一个对象的列表。然后再用equals()在这个列表中一个个找。如果散列函数定义得好,每个hash数只对应很少的对象,这样,与搜索整个序列相比,能很快跳到这个子序列,比较少量对象,会快许多。
hash表的“槽位”常被称为bucket。
21.影响HashMap性能的因素
Capacity:hash表里bucket的数量。
Initial capacity:创建hash表时,bucket的数量。
Size:当前hash表的记录的数量。
Load factor:size/capacity。一个负载较轻的表会有较少的冲突,因此插入和查找的速度会比较快,但在用迭代器遍历的时候会比较慢。
HashMap和HashSet都提供了能指定load factor的构造函数,当load factor达到这个阀值的时候,容器会自动将capacity(bucket的数量)增加大约一倍,然后将现有的对象分配到新的bucket里面(这就是所谓的rehash)。缺省情况下HashMap会使用0.75的load factor。
22.选择实现
HashTable,Vector和Stack属于老版本遗留下来的类,应该避免使用。
如何挑选List
数组的随机访问和顺序访问比任何容器都快。ArrayList的随机访问比LinkedList快,奇怪的时LinkedList的顺序访问居然比ArrayList略快。LinkedList的插入和删除,特别时删除,比ArrayList快很多。Vector各方面速度都比ArrayList慢,应避免使用。
如何挑选Set
HashSet各项性能都比TreeSet好,只有在需要有序的Set时,才应该用TreeSet。
LinkedHashSet的插入比HashSet稍慢一些,因为要承担维护链表和hash容器的双重代价,但是它的遍历速度比较快。
如何挑选Map
首选HashMap,只有在需要有序map时,才选TreeMap。LinkedHashMap比Hashmap稍慢一些。
23.把Collection和Map设成不可修改的
Collections.unmodifiableCollection()方法,会把传给它的容器变成只读版返回。这个方法有四种变形,unmodifiableCollection(),unmodifiableList(),unmodifiableSet(),unmodifiableMap()。
24.Collection和Map的同步
Collections里有一个自动对容器做同步的方法,它的语法与“unmodifiable”方法有些相似。synchronizedCollection(),synchronizedList(),synchronizedSet(),synchronizedMap()。
25.Fail fast
Java容器类库继承了fail-fast(及早报告错误)机制,它能防止多个进程同时修改容器的内容。当它发现有其它进程在修改容器,就会立即返回一个ConcurrentModificationException。
26.可以不支持的操作
可以用Arrays.asList()把数组改造成List,但它只是部分的实现了Collection和List接口。它支持的都是那些不改变数组容量的操作,不支持add(),addAll(),clear(),retainAll(),remove(),removeAll()等。调用不支持的方法会引发一个UnsupportedOperationException异常。
要想创建普通容器,可以把Arrays.asList()的结果做为构造函数参数传给List或Set,这样就能使用它的完整接口了。
=============
Chap12 Java I/O系统
1.File类
File类有一个极具欺骗性的名字,可以用来表示某个文件的名字,也可以用来表示目录里一组文件的名字。
File类的功能不仅限于显示文件或目录。它能创建新的目录,甚至是目录路径。此外还能检查文件的属性,判断File对象表示的是文件还是目录,以及删除文件等。
2.输入与输入
流(Stream)是一种能生成或接受数据的,代表数据的源和目标的对象。流把I/O设备内部的具体操作给隐藏起来了。
Java的I/O类库分成输入和输出两大部分。
3.添加属性与适用的接口
使用“分层对象(layered objects)”,为单个对象动态地,透明地添加功能的做法,被称为Decorator Pattern。Decorator模式要求所有包覆在原始对象之外的对象,都必须具有与之完全相同的接口。无论对象是否被decorate过,传给它的消息总是相同的。
为InputStream和OutputStream定义decorator类接口的类,分别是FilterInputStream和FilterOutputStream,它们都继承自I/O类库的基类InputStream和OutputStream,这是decorator模式的关键(惟有这样,decorator类的接口才能与它要服务的对象的完全相同)。
对于I/O类库来说,比较明智的做法是,普遍都做缓冲,把不缓冲当特例。
Reader和Writer类系
InputStream和OutputStream的某些功能已经淘汰,但仍然提供了很多有价值的,面向byte的I/O功能。而Java 1.1引进的Reader和Writer则提供了Unicode兼容的,面向字符的I/O功能。Java 1.1还提供了两个适配器(adapter)类,InputStreamReader和OutputStreamWriter负载将InputStream和OutputStream转化成Reader和Writer。
Reader和Writer要解决的,最主要是国际化。原先的I/O类库只支持8位的字节流,因此不可能很好地处理16位的Unicode字符流。此外新类库的性能也比旧的好。
4.数据源和目的
几乎所有的Java I/O流都有与之对应的,专门用来处理Unicode的Reader和Writer。但有时,面向byte的InputStream和OutputStream才是正确的选择,特别是java.util.zip,它的类都是面向byte的。
明智的做法是,先用Reader和Writer,等到必须要用面向byte的类库时,你自然会知道,因为程序编译不过去了。
5.常见的I/O流的使用方法
(1).对输入文件做缓冲
BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java"));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "/n";//readLine()会把换行符剥掉,所以在这里加上。
in.close();
//读取标准输入
BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in));
System.out.print("Enter a line:");
System.out.println(stdin.readLine());
(2).读取内存
StringReader in2 = new StringReader(s2);
int c;
while((c = in2.read())!=-1)//read()会把读出来的byte当做int
System.out.print((char)c);
(3).读取格式化内存
try{
DataInputStream in3 = new DataInputStream(new ByteArrayInputStream(s2.getBytes()));
while(true)
System.out.print((char)in3.readByte());//无法根据readByte()返回值判断是否结束
} catch(EOFException e){
System.err.println("End of stream");
}
//使用available()来判断还有多少字符
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("TestEOF.java")));
while(in.available() != 0)
System.out.print((char)in.readByte());
(4).读取文件
try{
BfferedReader in4 = new BufferedReader(new StringReader(s2));
PrintWriter out1 = new PrintWriter(new BufferedWriter(new FileWriter(IODemo.out)));
int lineCount = 1;
while((s = in4.readLine())!= null)
out1.println(lineCount++ +": "+ s);
out1.close();
} catch(EOFException e){
System.err.println(End of stream);
}
使用PrintWriter去排版,就能得出能够读得懂的,普通的文本文件。
6.标准I/O
标准I/O是Unix的概念,意思是,一个程序只使用一个信息流。所有输入都是从“标准输入”进来的,输出都从“标准输出”出去,错误消息都送到“标准错误”里。
Java遵循标准I/O的模型,提供了Syetem.in,System.out,以及System.err。
将System.out转换成PrintWriter
System.out是PrintStream,也就是说它是OutputStream。不过可通过PrintWriter的构造函数把它改造成PrintWriter。
PrintWriter out = new PrintWriter(System.out, true);
out.println("Hello, world");
为了启动自动清空缓冲区的功能,一定要使用双参数版的构造函数,并把第二个参数设成true。这点非常重要,否则就有可能会看不到输出。
标准I/O的重定向
Java的System类提供了几个能重定向标准输入,标准输出和标准错误的静态方法:
setIn(InputStream),setOut(PrintStream),setErr(PrintStream)。
I/O重定向处理的不是character流,而是byte流,因此不能用Reader和Writer,要用InputStream和OutputStream。
7.New I/O
Java 1.4的java.nio.*引入了一个新的I/O类库,其目的就是提高速度。实际上,旧的I/O类库已经用nio重写。
性能的提高源于它用了更贴近操作系统的结构:channel和buffer。
java.nio.ByteBuffer是唯一一个能直接同channel打交道的buffer。它是一个相当底层的类,存储和提取数据的时候,可以选择是以byte形式还是以primitive形式,但它不能存储对象。这是为了有效地映射到绝大多数操作系统上。
新I/O修改了旧I/O的三个类,即FileInputStream,FileOutputStream,以及RandomAccessFile,以获取FileChannel。
// Write a file:
FileChannel fc = new FileOutputStream("data.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text ".getBytes()));
fc.close();
// Add to the end of the file:
fc = new RandomAccessFile("data.txt", "rw").getChannel();
fc.position(fc.size()); // Move to the end
fc.write(ByteBuffer.wrap("Some more".getBytes()));
fc.close();
// Read the file:
fc = new FileInputStream("data.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(4096);
fc.read(buff);
buff.flip();
while(buff.hasRemaining())
System.out.print((char)buff.get());
用wrap( )方法把一个已经拿到手的byte数组"包"到ByteBuffer。如果是用这种方法,新创建的ByteBuffer是不会去拷贝底层的(byte)数组的,相反它直接用那个byte数组来当自己的存储空间。所以我们说ByteBuffer的"后台"是数组。
从buffer中取数据前,要调用buffer的flip()。往buffer中装数据前,要调用buffer的clear()。
FileChannel
in = new FileInputStream(args[0]).getChannel(),
out = new FileOutputStream(args[1]).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
while(in.read(buffer) != -1) {
buffer.flip(); // Prepare for writing
out.write(buffer);
buffer.clear(); // Prepare for reading
}
View Buffers
View Buffer能让你从特殊的视角,来观察其底层的ByteBuffer。对view的任何操作都会作用到ByteBuffer上。同一个ByteBuffer,能读出不同的数据。ByteBuffer以1字节区分数据,CharBuffer是2字节,IntBuffer,FloatBuffer是4字节,LongBuffer和DoubleBuffer是8字节。
ByteBuffer bb = ByteBuffer.wrap(new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' });
bb.rewind();
System.out.println("Byte Buffer");
while(bb.hasRemaining())
System.out.println(bb.position()+ " -> " + bb.get());
CharBuffer cb = ((ByteBuffer)bb.rewind()).asCharBuffer();
System.out.println("Char Buffer");
while(cb.hasRemaining())
System.out.println(cb.position()+ " -> " + cb.get());
FloatBuffer fb = ((ByteBuffer)bb.rewind()).asFloatBuffer();
System.out.println("Float Buffer");
while(fb.hasRemaining())
System.out.println(fb.position()+ " -> " + fb.get());
IntBuffer ib = ((ByteBuffer)bb.rewind()).asIntBuffer();
System.out.println("Int Buffer");
while(ib.hasRemaining())
System.out.println(ib.position()+ " -> " + ib.get());
Buffer的细节
如果使用相对定位的get()和put()方法,buffer的position会跟着变化。也可以用下标参数调用绝对定位的get()和put()方法,这时它不会改动buffer的position。
mark()方法会记录当前position,reset()会把position设置到mark的位置。rewind()把position设置到buffer的开头,mark被擦掉了。flip()把limit设为position,把position设为零。当你将数据写入buffer,准备读取的时候,必须先调用这个方法。
内存映射文件
memory-mapped file能让你创建和修改那些大到无法读入内存的文件(最大2GB)。
int length = 0x8FFFFFF; // 128 Mb
MappedByteBuffer out = new RandomAccessFile("test.dat","rw").getChannel().map(FileChannel.MapMode.READ_WRITE,0,length);
for(int i = 0; i < length; i++)
out.put((byte)'x');
for(int i = length/2;i
System.out.print((char)out.get(i));
MappedByteBuffer是ByteBuffer的派生类。例程创建了一个128MB的文件,文件的访问好像只是一瞬间的事,这是因为,真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。Java是调用操作系统的"文件映射机制(file-mapping facility)"来提升性能的。只有RandomAccessFile才能写映射文件。
文件锁
Java的文件锁是直接映射操作系统的锁机制的,因此其它进程也能看到文件锁。
FileOutputStream fos= new FileOutputStream("file.txt");
FileLock fl = fos.getChannel().tryLock();
if(fl != null) {
System.out.println("Locked File");
Thread.sleep(100);
fl.release();
System.out.println("Released Lock");
}
fos.close();
tryLock( ) 是非阻塞的。它会试着去获取这个锁,但是如果得不到(其它进程已经以独占方式得到这个锁了),那它就直接返回。而lock( )是阻塞的。如果得不到锁,它会在一直处于阻塞状态,除非它得到了锁,或者你打断了调用它(即lock( )方法)的线程,或者关闭了它要lock( )的channel,否则它是不会返回的。最后用FileLock.release( )释放锁。
还可以像这样锁住文件的某一部分,
tryLock(long position, long size, boolean shared)
或者
lock(long position, long size, boolean shared)
这个方法能锁住文件的某个区域(size - position)。其中第三个参数表示锁能不能共享。
对于带参数的lock( )和tryLock( )方法,如果你锁住了position到position+size这段范围,而文件的长度又增加了,那么position+size后面是不加锁的。而无参数的lock方法则会锁定整个文件,不管它变不变长。
8.压缩
Java I/O类库还收录了一些能读写压缩格式流的类,它们是InputStream和OutputStream的派生类。这是因为压缩算法是针对byte而不是字符的。
GZIP的接口比较简单,因此如果你只有一个流要压缩的话,用它会比较合适。
BufferedReader in = new BufferedReader(new FileReader(args[0]));
BufferedOutputStream out = new BufferedOutputStream(
new GZIPOutputStream(new FileOutputStream("test.gz")));
System.out.println("Writing file");
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
out.close();
System.out.println("Reading file");
BufferedReader in2 = new BufferedReader(
new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));
String s;
while((s = in2.readLine()) != null)
System.out.println(s);
只要用GZIPOutputStream 或ZipOutputStream把输出流包起来,再用GZIPInputStream 或ZipInputStream把输入流包起来就行了。
用Zip存储多个文件
FileOutputStream f = new FileOutputStream("test.zip");
CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
ZipOutputStream zos = new ZipOutputStream(csum);
BufferedOutputStream out = new BufferedOutputStream(zos);
zos.setComment("A test of Java Zipping");
// No corresponding getComment(), though.
for(int i = 0; i < args.length; i++) {
System.out.println("Writing file " + args[i]);
BufferedReader in = new BufferedReader(new FileReader(args[i]));
zos.putNextEntry(new ZipEntry(args[i]));
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
}
out.close();
// Checksum valid only after the file has been closed!
System.out.println("Checksum: " + csum.getChecksum().getValue());
// Now extract the files:
System.out.println("Reading file");
FileInputStream fi = new FileInputStream("test.zip");
CheckedInputStream csumi = new CheckedInputStream(fi, new Adler32());
ZipInputStream in2 = new ZipInputStream(csumi);
BufferedInputStream bis = new BufferedInputStream(in2);
ZipEntry ze;
while((ze = in2.getNextEntry()) != null) {
System.out.println("Reading file " + ze);
int x;
while((x = bis.read()) != -1)
System.out.write(x);
}
System.out.println("Checksum: " + csumi.getChecksum().getValue());
bis.close();
// Alternative way to open and read zip files:
ZipFile zf = new ZipFile("test.zip");
Enumeration e = zf.entries();
while(e.hasMoreElements()) {
ZipEntry ze2 = (ZipEntry)e.nextElement();
System.out.println("File: " + ze2);
// ... and extract the data as before
}
虽然标准的Zip格式是支持口令的,但是Java的Zip类库却不支持。
Java ARchives (JARs)
一个JAR只有一个文件,包含两个文件,一个是Zip文件,另一个是描述Zip文件所包含的文件的"manifest(清单)"。
如果JAR是用0(零)选项创建的,不会进行压缩,那么它就能被列入CLASSPATH了。
不能往已经做好的JAR里添加新文件或修改文件。不能在往JAR里移文件的同时把原来的文件给删了。不过JAR格式是跨平台的,无论JAR是在哪个平台上创建的,jar程序都能将它读出来(zip格式有时就会有问题了)。
9.对象的序列化
Java的"对象序列化"能让你将一个实现了Serializable接口的对象转换成一组byte,需要的时候,根据byte数据重新构建那个对象。这一点甚至在跨网络的环境下也是如此,序列化机制能自动补偿操作系统方面的差异。
对象序列化不仅能保存对象的副本,而且还会跟着对象里面的reference,把它所引用的对象也保存起来,然后再继续跟踪那些对象的reference,以此类推。这种情形常被称为"单个对象所联结的'对象网'"。这个机制所涵盖的范围不仅包括对象的成员数据,而且还包含数组里面的reference。
Worm w = new Worm(6, 'a');
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject("Worm storage/n");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
把对象从序列化状态中恢复出来的必要条件是,一定要让JVM找到.class文件。
控制序列化
可以让对象去实现Externalizable而不是Serializable接口,并以此来控制序列化的过程。
对于Externalizable对象,readExternal( )要在默认的构造行为会发生之后(包括在定义数据成员时进行的初始化)才启动。
不但要在writeExternal( )的时候把重要的数据保存起来(默认情况下,Externalizable对象不会保存任何成员对象),还得在readExternal( )的时候把它们恢复出来。为了能正确地存取其父类的组件,你还得调用其父类的writeExternal( )和readExternal( )。
transient关键词
要想禁止敏感信息的序列化,除了可以实现Externalizable外。还可以使用transient关键词修饰Serializable对象中不想序列化的成员。
默认情况下,Externalizable对象不保存任何字段,因此transient只能用于Serializable对象。
Externalizable的替代方案
如果你不喜欢Externalizable,还可以选择Serializable接口,然后再加入(注意,我没说"覆写"或"实现")序列化和恢复的时候会自动调用的writeObject( )和readObject( )方法。也就是说,如果你写了这两个方法,Java就会避开默认的序列化机制而去调用这两个方法了。
两个方法的特征签名如下,(它们都是private的,怪异):
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
如果你决定用默认的序列化机制来存储非transient的数据,你就得在writeObject( )里面调用defaultWriteObject( ),不带参数,而且得第一个做。恢复的时候,也要在readObject( )的开头部分调用defaultReadObject( )。
如果你要序列化static数据,就必须亲自动手。
Preferences
JDK 1.4所引入的Preferences API能自动存储和恢复信息。但是,它只能存取很少几种数据——primitive和String,而且每个String的长度都不能超过8K。
Preferences是一组存储在"由节点所组成的层次体系(a hierarchy of nodes)"里的键值集(很像Map)。Preferences API是借用操作系统的资源来实现功能的。对于Windows,它就放在注册表里。
//也可以用systemNodeForPackage( )
//"user"指的是单个用户的preference,而"system"指整个系统的共用配置
//一般用XXX.class做节点的标识符
Preferences prefs = Preferences.userNodeForPackage(PreferencesDemo.class);
prefs.put("Location", "Oz");
prefs.putInt("Companions", 4);
prefs.putBoolean("Are there witches?", true);
10.正则表达式
正则表达式是JDK 1.4的新功能。由java.util.regex的Pattern和Matcher类实现的。
Pattern p = Pattern.compile(("//w+");
Matcher m = p.matcher(args[0]);
while(m.find()) {
System.out.println("Match /"" + m.group() +
"/" at positions " +
m.start() + "-" + (m.end() - 1));
}
只要字符串里有这个模式,find( )就能把它给找出来,但是matches( )成功的前提是正则表达式与字符串完全匹配,而lookingAt( )成功的前提是,字符串的开始部分与正则表达式相匹配。
split()
所谓分割是指将以正则表达式为界,将字符串分割成String数组。
String[] split(CharSequence charseq)
String[] split(CharSequence charseq, int limit)//限定分割的次数
String input = "This!!unusual use!!of exclamation!!points";
System.out.println(Arrays.asList(Pattern.compile("!!").split(input)));
===========
Chap13 并发编程
1.基本线程
要想创建线程,最简单的办法就是继承java.lang.Thread。run( )是Thread最重要的方法,什么时候run( )返回了,线程也就中止了。
Thread的start( )方法会先对线程做一些初始化,再调用run( )。
整个步骤应该是:调用构造函数创建一个Thread对象,并且在构造函数里面调用start( )来配置这个线程,然后让线程的执行机制去调用run( )。如果你不调用start( ),那么线程永远也不会启动。
有时我们创建了Thread,但是却没去拿它的reference。如果是普通对象,这一点就足以让它成为垃圾,但Thread不会。Thread都会为它自己"注册",所以实际上reference还保留在某个地方。除非run( )退出,线程中止,否则垃圾回收器不能动它。
线程的调度机制是非决定性,即多个线程的执行顺序是不确定的。
yielding
如果知道run()已经告一段落了,你就可以用yield( )形式给线程调度机制作一个暗示。Java的线程调度机制是抢占式的(preemptive),只要它认为有必要,它会随时中断当前线程(运行到yield之前),并且切换到其它线程。总之,yield( )只会在很少的情况下起作用。
Sleeping
sleep( )一定要放在try域里,这是因为有可能会出现时间没到sleep( )就被中断的情况。如果有人拿到了线程的reference,并且调用了它的interrupt( ),这种事就发生了。(interrupt( )也会影响处于wait( )或join( )状态的线程,所以这两个方法也要放在try域里。)如果你准备用interrupt( )唤醒线程,那最好是用wait( )而不是sleep( ),因为这两者的catch语句是不一样的。
优先级
线程往控制台打印的时候是不会被中断的,否则控制台的显示就乱了。
守护线程
所谓"守护线程(daemon thread)"是指,只要程序还在运行,它就应该在后台提供某种公共服务的线程,但是守护线程不属于程序的核心部分。因此,当所有非守护线程都运行结束的时候,程序也结束了。
要想创建守护线程,必须在它启动之前就setDaemon(true)。守护线程所创建的线程也自动是守护线程。
连接线程
线程还能调用另一个线程的join( ),等那个线程结束之后再继续运行。如果线程调用了另一个线程t的t.join( ),那么在线程t结束之前(判断标准是,t.isAlive( )等于false),主叫线程会被挂起。
另一种方式:Runable
类可能已经继承了别的类,这时就需要实现Runable接口了。
如果要在这个实现了Runable的类里做Thread对象才有的操作,必须用Thread.currentThread()获取其reference。
除非迫不得已只能用Runnable,否则选Thread。
2.共享有限的资源
多线程环境的最本质的问题:永远也不会知道线程会在什么时候启动。
我们不能从线程内部往外面抛异常,因为这只会中止线程而不是程序。
资源访问的冲突
Semaphore是一种用于线程间通信的标志对象。如果semaphore的值是零,则线程可以获得它所监视的资源,如果不是零,那么线程就必须等待。如果申请到了资源,线程会先对semaphore作递增,再使用这个资源。递增和递减是原子操作(atomic operation,也就是说不会被打断的操作),由此semaphore就防止两个线程同时使用同一项资源。
解决共享资源的冲突
一个特定的对象中的所有的synchronized方法都会共享一个锁,而这个锁能防止两个或两个以上线程同时读写一块共用内存。当你调用synchronized方法时,这个对象就被锁住了。在方法返回并且解锁之前,谁也不能调用同一个对象的其它synchronized方法。
一定要记住:所有访问共享资源的方法都必须是synchronized的,否则程序肯定会出错。
一个线程能多次获得对象的锁。比如,一个synchronized方法调用了另一个synchronized方法,而后者又调用了另一synchronized方法。线程每获一次对象的锁,计数器就加一。当然,只有第一次获得对象锁的线程才能多次获得锁。线程每退出一个synchronized方法,计数器就减一。等减到零了,对象也就解锁了。
此外每个类还有一个锁(它属于类的Class对象),这样当类的synchronized static方法读取static数据的时候,就不会相互干扰了。
原子操作
通常所说的原子操作包括对非long和double型的primitive进行赋值,以及返回这两者之外的primitive。不过如果你在long或double前面加了volatile,那么它就肯定是原子操作了。最安全的原子操作只有读取和对primitive赋值这两种。
如果你要用synchronized修饰类的一个方法,索性把所有的方法全都synchronize了。要判断,哪个方法该不该synchronize,通常是很难的,而且也没什么把握。
并发编程的最高法则:绝对不能想当然。
关键段
有时你只需要防止多个线程同时访问方法中的某一部分,而不是整个方法。这种需要隔离的代码就被称为关键段(critical section)。创建关键段需要用到synchronized关键词,指明执行下列代码需获得哪个对象的锁。
synchronized(syncObject) {
// This code can be accessed by only one thread at a time
}
关键段又被称为"同步块(synchronized block)"。相比同步整个方法,同步一段代码能显著增加其它线程获得这个对象的机会。
3.线程的状态
线程的状态可归纳为以下四种:
(1).new: 线程对象已经创建完毕,但尚未启动(start),因此还不能运行。
(2).Runnable: 处在这种状态下的线程,只要分时机制分配给它CPU周期,它就能运行。
(3).Dead: 要想中止线程,正常的做法是退出run( )。
(4).Blocked: 就线程本身而言,它是可以运行的,但是有什么别的原因在阻止它运行。线程调度机制会直接跳过blocked的线程,根本不给它分配CPU的时间。除非它重新进入runnable状态,否则什么都干不了。
如果线程被阻塞了,那肯定是出了什么问题。问题可能有以下几种:
(1).你用sleep(milliseconds)方法叫线程休眠。在此期间,线程是不能运行的。
(2).你用wait( )方法把线程挂了起来。除非收到notify( )或notifyAll( )消息,否则线程无法重新进入runnable状态。
(3).线程在等I/O结束。
(4).线程要调用另一个对象的synchronized方法,但是还没有得到对象的锁。
4.线程间的协作
wait与notify
线程sleep( )的时候并不释放对象的锁,但是wait( )的时候却会释放对象的锁。也就是说在线程wait( )期间,别的线程可以调用它的synchronized方法。 此外,sleep( )属于Thread。wait( ), notify( ), 和notifyAll( )是根Object的方法。
只能在synchronized方法里或synchronized段里调用wait( ),notify( )或notifyAll( )。
wait( )能让你在等待条件改变的同时让线程休眠,当其他线程调用了对象的notify( )或notifyAll( )的时候,线程自会醒来,然后检查条件是不是改变了。
安全的做法就是套用下面这个wait( )定式:
while(conditionIsNotMet)
wait( );
用管道进行线程间的I/O操作
在很多情况下,线程也可以利用I/O来进行通信。对Java I/O类库而言,就是PipedWriter(可以让线程往管道里写数据)和PipedReader(让另一个线程从这个管道里读数据)。
5.死锁
Dijkstra发现的经典的死锁场景:哲学家吃饭问题。
只有在下述四个条件同时满足的情况下,死锁才会发生:
(1).互斥:也许线程会用到很多资源,但其中至少要有一项是不能共享的(同一时刻只能被一个线程访问)。
(2).至少要有一个进程会在占用一项资源的同时还在等另一项正被其它进程所占用的资源。也就是说,要想让死锁发生,哲学家必须攥着一根筷子等另一根。
(3).(调度系统或其他进程)不能从进程里抢资源。所有进程都必须正常的释放资源。我们的哲学家都彬彬有礼,不会从他的邻座手里抢筷子。
(4).需要有等待的环。一个进程在等一个已经被另一进程抢占了的资源,而那个进程又在等另一个被第三个进程抢占了的资源,以此类推,直到有个进程正在等被第一个进程抢占了的资源,这样就形成了瘫痪性的阻塞了。这里,由于每个哲学家都是先左后右的拿筷子,所以有可能会造成等待的环。在例程中,我们修改了最后一位哲学家的构造函数,让他先右后左地拿筷子,从而破解了死锁。
Java语言没有提供任何能预防死锁的机制。
6.停止线程的正确的方法
为了降低死锁的发生几率,Java 2放弃了Thread类stop( ),suspend( )和resume( )方法。
应该设置一个旗标(flag)来告诉线程什么时候该停止。
7.打断受阻的线程
有时线程受阻之后就不能再做轮询了,比如在等输入,这时你就不能像前面那样去查询旗标了。碰到这种情况,你可以用Thread.interrupt( )方法打断受阻的线程。最后要把受阻线程的 reference设成null。
8.总结
诺贝尔经济学奖得主Joseph Stiglitz有一条人生哲学,就是所谓的承诺升级理论:
"延续错误的代价是别人付的,但是承认错误的代价是由你付的。"
多线程的主要缺点包括:
(1).等待共享资源的时候,运行速度会慢下来。
(2).线程管理需要额外的CPU开销。
(3).如果设计得不不合理,程序会变得异常复杂。
(4).会引发一些不正常的状态,像饥饿(starving),竞争(racing),死锁(deadlock),活锁(livelock)。
(5).不同平台上会有一些不一致。
通常你可以在run( )的主循环里插上yield( ),然后让线程调度机制帮你加快程序的运行。
==============
Chap14 创建Windows与Applet程序
设计中一条基本原则:让简单的事情变得容易,让困难的事情变得可行。
软件工业界的“三次修订”规则:产品在修订三次后才会成熟。
1.控制布局
在Java中,组件放置在窗体上的方式可能与其他GUI系统都不相同。首先,它完全基于代码,没有用来控制组件布局的“资源”。第二,组件的位置不是通过绝对坐标控制,二十由“布局管理器”(layout manager)根据组件加入的顺序决定其位置。使用不同的布局管理器,组件的大小、形状和位置将大不相同。此外,布局管理器还可以适应applet或视窗的大小,调整组件的布局。
JApplet,JFrame,JWindow和JDialog都可以通过getContentPane()得到一个容器(Container),用来包含和显示组件。容器有setLayout()方法,用来设置布局管理器。
2.Swing事件模型
在Swing的事件模型中,组件可以触发一个事件。每种事件的类型由单独的类表示。当事件被触发时,它将被一个或多个监听器接收,监听器负责处理事件。
所谓事件监听器,就是一个“实现了某种类型的监听器接口的”类的对象。程序员要做的就是,先创建一个监听器对象,然后把它注册给触发事件的组件。注册动作是通过该组件的addXXXListener()方法完成的。
所有Swing组件都具有addXXXListener()和removeXXXListener()方法。
3.Swing组件一览
工具提示ToolTip
任何JComponent子类对象都可以调用setToolTipText(String)。
Swing组件上的HTML
任何能接受文本的组件都可以接受HTML文本,且能根据HTML格式化文本。例如,
JButton b = new JButton("Hello
Press me");
必须以""标记开始,但不会强制添加结束标记。
对于JApplet,在除init()之外的地方添加新组件后,必须调用容器的validate()来强制对组件进行重新布局,才能显示新添加的组件。
4.选择外观(Look & Feel)
“可插拔外观”(Pluggable Look & Feel)使你的程序能够模仿不同操作系统的外观。
设置外观的代码要在创建任何可视组件之前调用。Swing的跨平台的金属外观是默认外观。
try{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch(Exception e){
}
catch子句中什么也不用做,因为缺省情况下,如果设置外观失败,UIManager将设置成跨平台的外观。
动态绑定事件
不能保证事件监听器被调用的顺序与它们被添加的顺序相同。
5.Swing与并发
始终存在着一个Swing事件调度线程,它用来依次对Swing的所有事件进行调度。
管理并发
当你在类的main()方法中,或在一个独立线程中,准备修改任何Swing组件属性的时候,要注意,Swing的事件调度线程可能会与你竞争同一资源。
要解决这个问题,必须确保在任何情况下,只能在事件调度线程里修改Swing组件的属性。Swing提供了两种机制:SwingUtilities.invokeLater(Runnable)和SwingUtilities.invokeAndWait(Runnable)。它们都接受runnable对象作参数,并且在Swing的事件处理线程中,只有当事件队列中的任何未处理的事件都被处理完毕之后,它们才会调用runnable对象的run()方法。
SwingUtilities.invokeLater(new Runnable(){
public void run(){
txt.setText("ready");
}
});
invokeLater()是异步方法,会立即返回。invokeAndWait()是同步方法,会一直阻塞,直到事件处理完毕才会放回。
6.JavaBean与同步
当你创建Bean的时候,你必须要假设它可能会在多线程环境下运行。也就是说:
(1).尽可能让Beand中的所有公共方法都是synchronized。这将导致synchronized的运行时开销。
(2).当一个多路事件触发了一组对该事件感兴趣的监听器时,必须假定,在遍历列表进行通知的同时,监听器可能会被添加或移除。
public void notifyListeners(){
ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null);
ArrayList lv = null;
//Make a shallow copy of the List in case someone adds a listener while we're
//calling listeners
synchronized(this){
lv = (ArrayList)actinListeners.clone();
}
for(int i = 0; i < lv.size(); i++){
((ActionListener)lv.get(i)).actionPerformed(a);
}
}
==============
Chap15 发现问题
1.单元测试
//Discover the name of the class this object was created within:
className = new Throwable().getStackTrace()[1].getClassName();
JUnit
JUnit在输出消息中使用"."表示每个测试的开始。
JUnit为每个测试创建一个测试对象(继承自TestCase),以确保在测试运行之间没有不利的影响。所有的测试对象都是同时被创建的,而不是正好在测试方法执行之前才创建。
setUp是在每个测试方法运行之前被调用的。
2.利用断言提高可靠性
断言语法
assert boolean-expression;
assert boolean-expression: information-expression;
在JDK 1.4中,缺省情况下断言是关闭的。为了防止编译时的错误,必须带下面的标志进行编译:
-source 1.4
如:javac -source 1.4 Assert1.java
运行程序也必须加上标志-ea,全拼是-enableassertions。这样才会执行所有的断言语句。
我们也可以基于类名或包名来决定打开或关闭断言。
还有另一种动态控制断言的方法:通过ClassLoader对象的方法setDefaultAssertionStatus(),它为所有随后载入的类设置断言的状态。
public static void main(String[] args){
ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
//other statements
}
这样可以在运行时,不必使用-ea标志,但是仍然必须使用-source 1.4标志编译。
为DBC使用断言
DBC(Design by Contract)是由Bertrand Meyer(Eiffel编程语言的创建者)所阐明的一个概念,它通过确保对象遵循特定的、不能被编译时的类型检查所验证的规则,来帮助建立健壮的程序。
3.剖析和优化
“我们应该忽略较小的效率,在97%的时间里我们都应该说:不成熟的优化是万恶之源。”--Donald Knuth
最优化指南
避免为性能而牺牲代码的可读性。
不能孤立地考虑性能。要权衡所需付出的努力与能得到的利益之间的关系。
性能是大型工程要关心的问题,但通常不是小型工程要考虑的。
使程序可运转比钻研程序的性能有更高的优先权。仅当性能被确定是一个关键因素的时候,在初始设计开发过程期间才应该予以考虑。
不要假设瓶颈在什么地方,而应该运行剖析器来获得数据。
在任何可能的情况下,尽量通过将对象设置为null,从而显式地将其销毁。有时这可能是对垃圾回收器的一种很有帮助的提示。
程序大小的问题。仅当程序是大型的、运行时间长而且速度也是一个问题时,性能优化才有价值。
static final变量可以通过JVM进行优化以提高程序的速度。
做可以运转的最简单的事物。(极限编程)
================
附录A:对象的传递与返回
确切地说,Java有指针。Java中(除了基本类型)每个对象的标识符就是一个指针。但是它们受到了限制,有编译器和运行期系统监视着它们。Java有指针,但没有指针的相关算法。可以将它们看作“安全的指针”。
“别名效应”是指,多个引用指向同一个对象。将引用作为方法的参数传递时,它会自动被别名化。
制作局部拷贝
Java中所有的参数传递,执行的都是引用传递。当你传递对象时,真正传递的只是一个引用,指向存活于方法外的“对象”。对此引用做的任何修改,都是在修改方法外的对象。此外:
(1).别名效应在参数传递时自动发生。
(2).方法内没有局部对象,只有局部引用。
(3).引用有作用域,对象则没有。
(4).在Java中,不需要为对象的生命周期操心。
(5).没有提供语言级别的支持(例如“常量”)以阻止对象被修改,或者消除别名效应的负面影响。不能简单地使用final关键字来修饰参数,它只能阻止你将当前引用指向其他对象。
克隆对象
如果确实要在方法调用中修改参数,但又不希望修改外部参数,那么就应该在方法内部制作一份参数的副本,以保护原参数。
Object类提供了protected方法clone(),要使用它,必须在子类中以public方式重载此方法。例如,ArrayList就重载了clone()。ArrayList的clone()方法,并不自动克隆容器中包含的每个对象,只是将原ArrayList中的对象别名化,即只复制了ArrayList中对象的引用。这称为浅拷贝(shallow copy)。
使类具有克隆能力
虽然在所有类的基类Object中定义了克隆方法,但也不是每个类都自动具有克隆能力。
克隆对象时有两个关键问题:
(1).调用super.clone()
(2).将子类的clone()方法声明为public
基类的clone()方法,能“逐位复制(bitwise copy)”对象。
实现Cloneable接口
interface Cloneable{}
这样的空接口称为“标记接口(tagging interface)”。
Cloneable接口的存在有两个理由。第一,如果某个引用上传为基类后,就不知道它是否能克隆。此时,可以用instanceof检查该引用是否指向一个可克隆的对象。
if(myref instanceof Cloneable)//...
第二,与克隆能力的设计有关,考虑到也许你不愿意所有类型的对象都是可克隆的。所以Object.clone()会检查当前类是否实现了Cloneable接口,如果没有,就抛出CloneNotSupportedException异常。所以,作为实现克隆能力的一部分,通常必须实现Cloneable接口。
==与!=
Java比较对象相等的等价测试并未深入对象的内部。==和!=只是简单地比较引用。如果引用代表的内存地址相同,则它们指向同一个对象,因此视为相等。所以,该操作符测试的是:不同的引用是否是同一个对象的别名。
Object.clone()的效果
克隆过程的第一步通常都是调用super.clone()。它制作出完全相同的副本,为克隆操作建立了基础。在此基础上,你可以执行对完成克隆必要的其他操作。
这里的其他操作是指,对对象中的每个引用,都明确地调用clone()。否则,那些引用会被别名化,仍指向原本的对象。
只要没有向子类中添加需要克隆的引用,那么无论clone()定义于继承层次中多深的位置,只需要调用Object.clone()一次,就能完成所有必要的复制。
对ArrayList深层拷贝而言,以下操作是必须的:克隆ArrayList之后,必须遍历ArrayList中的每个对象,逐 一克 隆。对HashMap做深层拷贝,也必须做类似的操作。
向继承体系的更下层增加克隆能力
可以向任意层次的子类添加克隆能力,从那层以下的子类,也就都具备了克隆能力。
克隆小结
如果希望一个类可以被克隆:
(1).实现Cloneable接口。
(2).重载clone(),声明为public。
(3).在clone()中调用Super.clone()。
(4).在clone()中捕获异常。
只读类
在只读类中所有数据都是private的,并且没有定义会修改对象内部状态的方法。只读类的对象可以有很多别名,也不会造成伤害。例如,Java标准类库中所有基本类型的包装类。
恒常性(immutability)的缺点
当你需要一个被修改过的此类的对象的时候,必须承受创建新对象的开销,也会更频繁地引发垃圾回收。对于有些类(如String),其代价让人不得不禁止这么做。
解决之道是创建一个可被修改的伴随类(companion class)。
=============
附录B:Java编程指南
设计
1.优雅设计终将得到回报。精心设计程序的时候生产率不会很高,但欲速则不达。
2.先能运行,再求快速。
3.分而治之。
4.尽量让所有东西自动化。(如测试和构建,先写测试,再编写类)
5.尽可能使类原子化。
建议重新设计类的线索有:
(1).复杂的switch语句,请考虑使用多态。
(2).有许多方法,处理类型极为不同的操作:请考虑划分成不同的类。
(3).有许多成员变量,表示类型极为不同的属性:请考虑划分成不同的类。
(4).参考《Refactoring:Improving the Design of Existing Code》,Martin Fowler著,(Addison-Wesley 1999)。
6.将变动的和不变的因素分离。
7.在判断应该使用继承还是组合的时候,考虑是否需要上传为基类。
实现
1.编写通用性的类时,请遵守标准形式。包括定义equals()、hashCode()、toString()、clone()(实现Cloneable接口,或者选择其它对象复制策略),并实现Comparable和Serialiable接口。
2.在构造器中只做必要的动作:将对象设定为正确的状态。避免在构造器内调用其它方法(final方法除外),因为这些方法可能会被其他人重载,这就可能在构造期间得到意外的结果。
3.优先选择接口而不是抽象类。只有在必须放进方法定义或成员变量时,才把它改为抽象类。接口只和客户希望的动作有关,而类则倾向于实现细节。