========
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<length/2+6;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("<html><b><font size=+2>Hello<br>Press me");
必须以"<html>"标记开始,但不会强制添加结束标记。
对于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.优先选择接口而不是抽象类。只有在必须放进方法定义或成员变量时,才把它改为抽象类。接口只和客户希望的动作有关,而类则倾向于实现细节。