五十一 当心字符串连接的性能
1、为连接n个字符串而重复使用字符串连接符+,需要n的平方级的时间。这是由于字符串不可变而导致的结果。当两个字符串被连接在一起时,它们的内容都要被拷贝。
2、如果项目数量巨大,为了获取可以接受的新能,请使用StringBuilder替代String
StringBuilder类(非同步),已经替代了同步的StringBuffer类。
3、不要使用字符串连接符来合并多个字符串,除非新能无关紧要。替代方法①使用StringBuilder的append方法②使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。
五十二 通过接口引用对象
1、应该使用接口而不是类作为参数类型。
2、应该优先使用接口而不是类来引用对象。如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就应该使用接口进行声明。接口使得程序更加灵活。
3、如果养成了用接口作为类型的习惯,你的程序将会更加灵活。当你决定更换实现时,所要做的就只是改变构造器中的类的名称。
4、无法使用接口的三中情况:①如果没有合适的接口存在,完全可以使用类而不是接口来引用对象(值类型)。②对象属于一个框架,而框架的基本类型是类不是接口(java.util.TimerTask)。③类实现了接口,但是它提供了接口中不存在的额外方法——例如LinkedHashMap。
五十三 接口优先于反射机制
1、反射机制(java.lang.reflect)提供了“通过程序来访问关于已装载的类的信息”的能力。
2、反射机制的缺点:①丧失了编译时类型检查的好处,包括异常检查。如果程序企图用反射方式调用不存在后置不可访问的方法,在运行时它会失败。②执行反射访问所需要的代码非常笨拙和冗长。③性能损失。反射方法比普通方法调用慢了许多。(具体情况,具体说明。作者2-50倍)
3、反射机制最初是为了基于组件的应用创建工具而设计的。反射功能只是在设计时被用到。通常,普通应用程序在运行时不应该以反射凡是访问对象。
4、在RPC(远程过程调用)系统中使用反射机制非常合适,这样可以不需要存根编译器。
5、如果只是非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。
五十四 谨慎地使用本地方法
1、Java Native Interface(JNI)允许java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计语言来编写的特殊方法。本地方法在本地语言中可以执行任意任务,并返回到java中。
2、随着JVM的发展,使用本地方法来提高性能的做法不值得提倡。(待商榷),因为本地语言是不安全的,同时是不可移植的。同时本地方法的编写和调试困难。
五十五 谨慎地进行优化
1、很多计算上的过失都被归咎于效率,而不是任何其他原因,甚至包括盲目的做事。不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是问题的根源。
2、优化的弊大于利,特别是不成熟的优化。
3、不要因为性能而牺牲合理的结构,要努力编写好的程序而不是快的程序。好的程序是指高内聚低耦合的程序。
4、总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。
五十六 遵守普遍接受的命名惯例
1、两大命名惯例:字面的和语法的。《TheJava Language Specification》
2、包名称应该以组织的Internet域名开头,其余组成部分由不超过8个字符的英文缩写存在。
3、类和接口名称,首字母大写,如HttpClient。
4、方法名首字母小写。
五十七 只针对异常的情况才使用异常
1、异常机制设计的初衷是用于不正常的情形,所以很少会有JVM实现试图对它们进行优化,使得与显示的测试一样快速。把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些优化。
2、实际上,现代的JVM实现上,基于异常的模式比基于标注模式要慢得多。
3、异常应该只能用于异常的情况下;它们永远不应该用于正常的控制流。
4、良好设计的API不应该强迫它的客户端为了正常的控制流而使用异常。
五十八 对可恢复的情况使用受检异常,对编程错误使用运行时异常
1、三种异常:受检时异常(编译时异常),运行时异常,错误。
2、运行时异常表明编程错误。
3、所有的运行时异常都是RuntimeException的子类。
4、受检时异常能够调用适当的方法进行恢复操作。
五十九 避免不必要地使用受检时异常
1、受检的异常是java程序设计语言的一项很好的特性。
六十 优先使用标准异常
1、专家程序员与缺乏经验的程序员一个最主要的区别在于,专家最求并且通常也能够实现高度的代码重用。代码重用是值得提倡的,这是一条通用的规则,异常也不例外。
2、重用异常有很多好处。①使得API更加易于学习和使用。②可读性更高。③内存印迹越小,装载这些类的时间开销也越少。
六十一 抛出与抽象相对应的异常(针对编译时异常)
1、更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法成为异常转译。
2、尽管异常转译与不加选择地从底层传递异常的做法相比有所改进,但是它不能被滥用。
3、底层处理不了异常,就要把这个异常抛给高层,这种方式可以使用异常转译,也可以不使用。如果高层能够处理的了就不适用异常转译,使高层捕捉到这个异常并加以处理,如果是高层也处理不了的异常可进行异常转译工作,将其转译为运行时异常。
六十二 每个方法抛出的异常都要有文档
1、始终要单独地声明受检的异常,并且利用javadoc的@throws标记,准确地记录下抛出每个异常的条件。要对异常本身进行描述,而不要描述异常的父类。
2、总而言之,腰围每个方法所能抛出的每个异常建立文档。
六十三 在细节消息中包含能捕获失败的信息
1、为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。
2、捕获信息对于从失败中恢复是非常有用的。
六十四 努力使失败保持原子性
1、一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。
六十五 不可忽略异常
1、要忽略一个异常非常容易,只要将方法调用通过try语句包围起来,并包含在一个空catch块中。
2、当异常发生时,如果使用了这种忽略异常的方式,程序会在catch异常之后继续执行,并且因为之前发生了异常后面的程序再次发生异常,如果之前的异常处理不了,或者没有打印出异常信息将会对后面异常的检查和调试带来困难。
六十六 同步访问共享的可变数据
1、并发是从多核处理器中获得更好的性能的一个条件。
2、synchronized关键字可以保证在同一时刻,只有一个线程可以访问某一个方法,或者某一个代码块。Java语言规范保证读或者写一个变量是原子的,除非这个变量的类型为long或者double。如boolean就是原子的。即可以保证返回的值是某个线程保存在改变量中的。????
3、Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。
而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
4、++操作不是原子的。首先读取它的值,然后写回一个新值,相当于原来的值再加上1。如果第二个线程在第一个线程读取久值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败。
5、如果读写操作没有同步,其他同步就起不了作用。
6、可以将可变数据限制在单个线程中,以避免同步操作。
7、原子变量(AtomicLong,AtomicInteger, AtomicReference)。类似i++这样的"读-改-写"复合操作(在一个操作序列中,后一个操作依赖前一次操作的结果),在多线程并发处理的时候会出现问题,因为可能一个线程修改了变量,而另一个线程没有察觉到这样变化,当使用原子变量之后,则将一系列的复合操作合并为一个原子操作,从而避免这种问题,i++=>i.incrementAndGet()。
原子变量只能保证对一个变量的操作是原子的,如果有多个原子变量之间存在依赖的复合操作,也不可能是安全的,另外一种情况是要将更多的复合操作作为一个原子操作,则需要使用synchronized将要作为原子操作的语句包围起来。因为涉及到可变的共享变量(类实例成员变量)才会涉及到同步,否则不必使用synchronized。
8、当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。
六十七 避免过度同步
1、过度同步可能会导致性能降低、死锁,甚至不确定的行为。
2、为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。换句话说,在一个被同步的区域内部,不要调用设计成被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。
3、通常,应该在同步区域内做尽可能少的工作。
4、为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般地讲,要尽量限制同步区域内部的工作量。
六十八 executor和task优先于线程
1、如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool通常是一个不错的选择,因此其不需要配置,并且一般情况下可以正确的完成工作。但是对于大负载的服务器来说哦,缓存的线程池就不是很好的选择。在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以至它所有的CPU都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会市情况变得更糟。因此在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类。
2、应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。
3、现在关键的抽象不再是Thread了,它以前可是既充当工作单元,优势执行机制。现在工作单元和执行机制是分开的。
4、现在的工作单元:任务。任务分为Runnable和Callable(有返回值)。
5、执行任务的通用机制是executor service
6、Executor Framework也有一个可以替代java.util.Timer的东西,即ScheduledThreadPoolExecutor。
六十九 并发工具优先于wait和notify
1、JDK1.5之后java平台提供了更高级的并发工具,它们可以完成以前必须在wait和notify上首先代码来完成的各项工作。
2、java.util.concurrent中更高级的工具分成三类:①ExecutorFramework②并发集合(3、4、5)③同步器(6、7、8)
3、并发集合为标准的集合接口提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序变慢。有些并发集合接口已通过依赖状态的修改操作进行了扩展,它将几个基本操作合并到了单个原子操作中。
4、除非不得已,否则应该优先使用ConcurrentHashMap(速度快),而不是使用Collections.synchronizedMap或者Hashtable。只要用并发Map替换老式的同步Map,就可以极大地提升应用程序的性能。更一般的,应该优先使用并发集合,而不是使用外部同步集合。
5、大多数ExecutorService实现都使用BlockingQueue。
6、同步器是一些使线程能够等待一个线程的对象,允许他们协调动作。最常见的同步器是CountDownLatch和Semaphore。不常用的是CyclicBarrier和Exchanger。
7、CountDownLatch是一次性障碍,允许一个或者多个线程等待一个或者多个其他线程来做事情,CountDownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待线程被处理之前,必须在锁存器上调用countDown方法的次数(即所有未完成任务的数量)。(工作机制:同时开启多个线程,使用CountDownLatch对这些线程进行计数操作,直到所有的线程都结束为止)
8、对于间歇式的定时,使用应该优先使用System.nanoTime而不是使用System.currentTimeMills。System.nanoTime更加准确也更加精准,它不受系统的实时时钟的调整所影响。
9、使用wait方法的标准模式
Synchronized(obj){
While(<conditiondoes not hold>)
Obj.wait();//Relesses lock, and reacquires on wakeup
//Performaction appropriate to condition
}
始终应该使用wait循环模式来调用wait方法;永远不要再循环之外调用wait方法。循环会在等待之前和之后测试条件。
10、notify唤醒的是单个正在执行的线程。notifyAll唤醒的是所有正在执行的线程。应该优先使用notifyAll,它总会产生正确的结果。因为它可以保证你将唤醒所有需要唤醒的线程。你可能也会唤醒其他的一些线程,但是这不会影响程序的正确性。这些线程唤醒之后,会检查它们正在执行的条件,如果发现不满足,就会继续等待。
七十 线程安全性的文档化
1、在正常操作中,javadoc并没有在它的输出中包含synchronized修饰符,这是有理由的。因为在一个方法声明中出现synchronized修饰符,这个是实现细节,并不会导出的API的一部分。它并不一定表明这个方法是线程安全的。
2、
3、私有锁对象
private final Object lock = new Object();//lock域被声明为final的。这样防止不小心改变它的内容。
public void foo(){
synchronized(lock){
}
}
如果是无条件的线程安全类,就应该考虑使用私有化对象来代替同步的方法。
七十一 慎用延迟初始化
1、延迟初始化是延迟到需要域的值时才将它初始化的行为。如果不需要这个值,这个域永远不会被初始化。这种方法既适用于静态域,也适用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例化中的有害循环。
2、像大多数优化一样,对于延迟初始化,最好建议“除非绝对必要,否则不要这么做”。延迟初始化就像一把双刃剑。根据延迟初始化的域最终需要初始化的比例,初始化这些域要多少开销,以及每个域多久被访问一次,延迟初始化实际上降低了性能。
3、在大多数情况下,正常的初始化要优先于延迟初始化。
4、如果出于性能的考虑而需要实例域使用延迟初始化,就使用双重检查模式(单例模式代码四),第一次检查时没有锁定,看看这个值是否初始化了;第二次检查时有锁定,只要当第二次检查时表明这个域没有初始化,才会调用初始化操作。
5、简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。
七十二 不要依赖于线程调度器
1、当有多个线程可以运行时,由线程调度器决定哪些线程将会运行,以及运行多长时间。任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。
2、要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。
3、可运行的线程的数量并不等于线程的总数量,前者可能更多。在等待的线程并不是可运行的。
4、保持可运行线程数量尽量可能少的主要方法是,让每个线程做些有意义的工作,然后等待更有意义的工作。如果线程没有在做有意义的工作(比如线程一直处于忙-等状态),就不应该运行。这意味着适当地规定线程池的大小,并且使任务保持适当地小,彼此独立。
5、线程的优先级是java平台上最不可移植的特性了,类似的还有Thread.yield来修饰的程序,也不可移植。
6、Thread.yield根本不做实质性的工作,只是将控制权返回给它的调用者。应该调用Thread.sleep(1)代替Thread.yield来进行并发测试,千万不要使用Thread.sleep(0),它会立即返回。
7、不要让应用程序的正确性依赖于线程调度器,否则,结果得到的程序既不健壮,也不具有可移植性。不要依赖于Thread.yield或者线程优先级。
七十三 避免使用线程组
1、因为线程组已经过时了,所以实际上根本没有必要修正。
七十四 谨慎地实现Serializable接口
1、对象序列化:它提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。“将一个对象编码成一个字节流”,称作将该对象序列化;相反的处理过程反序列化。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时用。
2、虽然一个类可被序列化的直接开销非常低,甚至可以忽略不计,但那时为了序列化而付出的长期开销是实实在在的。
3、实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。一旦一个类实现了Serializable接口,它的字节流编码就变成了导出的API的一部分(在以后发行的版本当中会继续使用这些配置)。如果使用默认的序列化形式,并且在以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。自定义序列化形式。设计良好的序列化形式也许会给类的演变带来限制。
4、序列化会使得累的演变受到限制,这种限制的一个例子与流的唯一标识符又换,通常被称为序列版本UID。每个可序列化的类都有一个唯一标识号与其相关联。如果没有一个名为serialVersionUID的私有静态final的long类型变量,系统就会自动根据这个类的名称,所实现的接口,以及这个类所涉及到的所有信息影响。产生这个信息是非常复杂的运算过程。
5、增加了出现BUG和安全漏洞的可能性。反序列化机制都是一种“隐藏的构造器”。
6、随着发行新的版本,相关的测试负担也增加了。因为要测试新的版本与旧版本的序列化能否兼容。
7、为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少的继承Serializable接口。
8、内部类不应该实现Serializable,因为其指向了外围类实例。而静态成员类可以实现Serializable接口。
9、对于transient和static修饰的域,默认的序列化机制是不进行操作的。
七十五 考虑使用自定义的序列化形式
1、默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它描述了所有这些对象被连接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理数据表示法应该是各自独立的。
2、如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。例如:
public class Name implements Serializable{
private final StringlastName;
private final StringfirstName;
private final StringmiddleName;
}
3、即使确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以确保约束关系和安全性。
4、当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有一下4个缺点:
5、自定义序列化类型例子
package com.serializable;
// StringList with areasonable custom serialized form
import java.io.*;
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// Nolonger Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appendsthe specified string to the list
public final void add(String s) {
// Implementation omitted
}
/**
* Serialize this {@code StringList}instance.
*
* @serialData The size of the list (thenumber of strings
* it contains) is emitted ({@codeint}),followed by all of
* its elements (each a {@code String}), inthe proper
* sequence.
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e =head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
private static final long serialVersionUID = 93248094385L;
//Remainder omitted
}
6、java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。
Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。
7、在决定将一个域做成非transient的之前,一定要确信它的值将是该对象逻辑状态的一部分。正确的的使用一种自定义的序列化形式,大多数实例域,或者所有的实例域都应该标记为transient,就像上面例子中的StringList那样。
8、什么是writeObject 和readObject?可定制的序列化过程
9、如果使用transient标记了多个域,那么在反序列话的时候这些域将被初始化为它们的默认值:引用变为null;基本数据类型变为0;boolean类型变为false;可以在readObject方法当中首先调用defaultReadObject,然后把这些transient域恢复为可接受的值,另一种方法就是这些域可以推迟到第一次使用的时候才真正被初始化。
10、如果再读取整个对象状态的任何其他方法上强制任何同步,则必须在对象序列化上强制这种同步。
11、不管选择了哪种同步序列化形式,都要为自己编写的每个可序列化的类声明一个现实序列版本UID(serial version UID)。这样可以避免序列化版本UID成为潜在的不兼容根源。如果没有提供显示的序列化版本UID,就需要在运行时通过一个高开销的计算过程产生一个UID。
12、如果一个类生成一个新的版本,这个类与现有的类不兼容,那么只要修改UID声明的值即可。前一版本的实例经序列化之后,再做反序列化时就会引发
InvalidClassException异常。
七十六 保护性地编写readObject方法
1、readObject方法实际上相当于另一个公有的构造器。如同其他构造器一样,它要求检查参数的有效性,在必要的时候进行保护性拷贝,如果没有做到这两点,就有可能受到攻击。
2、readObject是一个“用字节流作为唯一参数”的构造器。如果这个字节流是正常产生的,产生正常的结果,但是如果这个字节流是人工故意修改过的,而readObject方法没有对齐进行有效性验证的话,就会出现错误。
3、有效性验证可以再调用defaultReadObject()方法之后,接着对参数进行验证,如果不真确,则抛出异常。
4、当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的。
5、有效性检查和保护性拷贝,序列化代理。不要使用writeUnshared和readUnshared方法,它们通常比保护性拷贝更快,但是它们不提供必要的安全性保护。
七十七 对于实例控制(单例模式),枚举类型优先于readResolve(单例模式)
1、任何一个readObject方法,不管是显式还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。因此在这里如果继续使用单例模式,就会产生多于一个实例的结果,而造成单例模式失败。
2、readResolve特性允许你用readObject创建的实例代替另一个实例。该方法返回的对象引用将被返回,取代新建的对象,而新建对象的引用不需要再保留,因此立即成为垃圾回收的对象。(没看懂)
3、如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。
4、readResolve方法被用于所有可序列化的实例受控的类。
5、尽量用枚举类型来实施实例控制的约束条件。如果做不到又需要一个既可以序列化有是实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或是transient的。
枚举类型:
public enum Elvis {
INSTANCE;
private String[]favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
readResolve方法类型:
public class Elvis implements Serializable {
public static final Elvis INSTANCE =new Elvis();
private Elvis() { }
private String[]favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve()throwsObjectStreamException {
returnINSTANCE;
}
}
七十八 考虑用序列化代理代替序列化实例
1、序列化代理:首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套的类本称作为序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要任何一致性检查或者保护性拷贝,从设计的角度来看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类机器序列化代理都必须声明实现Serializable接口。
2、当你发现你必须要自己写readObject和writeObject时,要考虑使用序列化代理。使用实例:
import java.util.*;
import java.io.*;
public final class Period implements Serializable {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precedestart
* @throws IllegalArgumentException ifstart is after end
* @throws NullPointerException ifstart or end is null
*/
public Period(Date start, Date end) {
this.start =newDate(start.getTime());
this.end =newDate(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
}
public Date start () {return new Date(start.getTime()); }
public Date end () {return new Date(end.getTime()); }
public String toString() {return start + " - " + end; }
//Serialization proxy for Period class - page 312
private static class SerializationProxyimplementsSerializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID =
234098243823485285L; // Any number will do (Item 75)
// readResolve method for Period.SerializationProxy - Page 313
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
}
//writeReplace method for the serializationproxy pattern - page 312
private Object writeReplace() {
return new SerializationProxy(this);
}
//readObject method for the serializationproxy pattern - Page 313
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}