2. 创建和销毁对象
2.1 考虑用静态方法代替构造器
what
// 静态方法
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
// demo1
Boolean a1 = new Boolean(true);
Boolean a2 = new Boolean(true);
Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
if (a1 == a2){
System.out.println("==========1"); //NO
}
if (b1 == b2){
System.out.println("==========2"); //OK
}
// demo2
Map> m = new HashMap>();
Map> m = HashMap.newInstance();
public static HashMap newInstance(){
return new HashMap();
}
why
静态方法相比构造器优点:
- 有名称
- 不必每次调用时创建一个新的对象(此时可以用==代替equals,从而提升性能,demo1)
- 可以返回原返回类型的任何子类型对象,从而隐藏实现类使api更简洁(服务提供者框架)
- 创建参数化类型实例时,利用类型推导(type inference)代码更简洁
缺点:
- 类如果没有公有的或受保护的构造器,就无法被子类化
2.2 使用构建器代替多个参数的构造器
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).build();
}
}
2.3 使用私有构造器或枚举强化Singleton属性
what
Singleton指仅被实例化一次的类,常依赖代表那些本质上唯一的组件。
好的单例模式要做到:
- 防止反射,序列化,高并发导致的单例失败;
- 能便捷的切换成多例模式
HOW
自己动手实现牛逼的单例模式
2.4 通过私有构造器强化不可实例化的能力
防止缺省的构造器导致类仍可以实例化,可以主动实现个无参构造器,并在其中返回异常,这样做的缺点是导致该类无法子类化
2.5 避免创建不必要的对象
- 重用不可变对象
String s = "a"
代替String s = new String("a")
- 重用已知不会被修改的可变对象,如date
class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0
&& birthDate.compareTo(BOOM_END) < 0;
}
}
- 优先使用基本类型,而不是装箱类型
- 除非是重量级的大对象,如数据库连接池,不然没必要去刻意维护个对象池来实现重用,因为小对象的创建回收是廉价的,相比维护对象池而言(代码乱,占用内存)
2.6 清除过期的对象引用
为什么清除:内存泄露,占用内存,导致机器性能越来越慢,甚至内存溢出
清空对象引用应该是一种例外,而不是规范行为,应当最小化变量的作用域。
哪些情况会导致过期引用
自己管理内存的类
-
缓存:一旦把对象引用放到缓存里,就容易被遗忘。
只要缓存之外存在对某个键的引用,该键才有意义,考虑使用WeakHashMap实现该缓存
监听器和其他回调:如注册回调却没有显式取消回调。确保回调可以垃圾被垃圾回收的最佳方法是只保存弱引用(weak reference),例如保存成WeakHashMap的键
2.7 避免使用finalizer()
3. 对所有对象都通用的方法
4. 类和接口
4.1 使类和成员的可访问性最小化
信息隐藏:模块之间只通过他们的api通信,一个模块不需要知道其他模块的实现细节
实例域不能是公有的,否则即放弃了对存储在这个域中的值进行限制的能力,也因此包含公有可变域的类不是线程安全的
长度非零的数组总是可变的,不要返回它或者将其声明为公有的静态final数组域。处理办法是声明成私有的,然后返回不可变数组,或者数据的拷贝(clone())
4.2 在公有类中使用访问方法而不是公有域
如果公有类暴露了数据域,那今后想改也晚了,应该调用它的代码已经遍布客户端
4.3 使可变性最小化
what
不可变类是其实例不能被修改的类,实例中所有信息都在创建时提供,如String,基本类型的包装类,BigInteger,BigDecimal.
不可变对象只有一个状态,即被创建时的状态
不可变对象是线程安全的,不需要同步,故可被自由的共享,进而永远不需要保护性拷贝
不可变对象的缺点是每个不同的值都需要一个单独的对象
How
为使类成为不可变,遵循5条原则:
4.4 复合优于继承
继承的缺点:
- 打破了封装性,在子类的实现依赖父类的某个特性时会是个问题,其功能可能随父类发行版本的变化而失效,如HashSet中addAll()是调用add()实现的,所以统计add()元素个数时,只能重写add()方法
- 把超类api中的缺陷传递到子类
导致问题的另一个原因,在超类在后续的发行方法中会新增加方法:
- 子类有方法与新加的方法签名相同但返回类型不同,则编译报错
- 子类有方法与新加的方法签名且返回类型相同,其实现可能不会遵循该新方法的约定
包装类(复合,把现有的类变成新类的组件;新类方法中调用超类中对应方法并返回其结果,称为转发方法)的缺点:
- 不适用在回调框架(callback framework),可能会导致SELF问题,因为被包装起来的对象并不知道它外面的包装对象
只有A IS B 时,才使用继承
4.5 接口优于抽象类
区别:
- 抽象类允许包含某些方法的实现而接口不允许;
- 为实现抽象类定义的类型,必须成为抽象类的子类
抽象类的弊端:
- 破坏类层次,某个类一旦实习抽象类,其所有后代都要扩展这个新的抽象类,无论这个后代是否合适
- 不能Mixin,接口可以实现多个,但类只能继承一个
- 无法构建非层次结构的类型框架
抽象类优点:
- 可以增加已实现的方法
骨架实现(skeletal implementtation):使用抽象类实现接口,亦被称作abstractInterface,可以使程序员很容易提供自己的接口实现,对于重要的接口,最好提供对应的骨架实现类。
通过对你导出的每个重要接口都提供一个抽象的骨架实现类(skeletal implementation)类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作(可以只实现需要的方法)。
// 应用骨架类实现自己的List
public class IntArrays {
static List intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList() {
public Integer get(int i) {
return a[i]; // Autoboxing (Item 5)
}
@Override
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // Auto-unboxing
return oldVal; // Autoboxing
}
public int size() {
return a.length;
}
};
}
public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++)
a[i] = i;
List list = intArrayAsList(a);
Collections.shuffle(list);
System.out.println(list);
}
}
4.6 优先考虑静态成员类
嵌套类:
- 定义在另一个类内部的类,目的在于为其外围类服务,如果其可能用在其他类,应该是顶层类。
- 分类:静态成员类、内部类(非静态成员类,匿名类,局部类)
[图片上传失败...(image-c90921-1553657651829)]
5. 泛型
5.1 不要在新代码中使用原生态类型
每个泛型都会定义一个原生态类型(Raw Type),即不带任何实际类型参数的泛型名称。例如,与List
List
原因
- 无法在编译时就发现类型错误
- 可读性差
java之所以支持原生态类型是未来兼容。
参考:java 泛型之不要使用原生态类型
5.2 消除非受检警告
如果不是确定没关系,不要置之不理,其可能有潜在的CLassCastException风险
5.3 列表优于数组
数组是协变的,如果sub是super的子类型,那sub[]就是super[]的子类型,总是在运行时才发现类型错误;
Object[] objectArr = new Long[1];
objectArr[0] = "11";// 运行时报错
List
数组是具体化(reified)的,它会在运行时才检查元素类型约束;而泛型通过擦除(crasure)实现,只在编译时强化类型信息,并在运行时丢弃类型信息(JVM中并没有泛型)。
总之,数组是协变且可具体化的,泛型是不可变且可擦除的。因此数组提供了运行时的类型安全,但没有编译时的类型安全,泛型则相反。所以如果你将泛型和数组混用,并在编译时得到警告或错误,用列表代替数组。
6. 枚举和注解
6.1 用enum代替int常量
枚举:通过公有的静态final域为每个枚举常量导出实例的类,是真正的final。
优点:
- 可读性更好
- 实例受控,是单例的泛型化,不会被扩展或创建新的实例
- 编译时的类型安全
- 有独立的命名空间,所以不同枚举类型可以有同名常量
- 可添加方法和域,并实现接口
缺点:装载和初始化枚举时会占用空间和时间成本
应用:
当想给常量绑定对应的行为时:
- 使用抽象方法,(防止新加的常量没有重写对应的行为)枚举中的抽象方法必须被它所有常量中的具体方法所覆盖;
- 当多个常量共享一个行为时,考虑策略枚举(私有的嵌套枚举类)
- 抽象方法可定义在接口中,再由枚举类实现之,提高扩展性
// 常量绑定抽象方法
public enum Operation {
PLUS("+") {
double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
double apply(double x, double y) {
return x - y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
abstract double apply(double x, double y);
// Test program to perform all operations on given operands
public static void main(String[] args) {
double x = Double.parseDouble("1");
double y = Double.parseDouble("1");
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
// 枚举绑定行为
public enum Planet {
MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
public static void main(String[] args) {
double earthWeight = Double.parseDouble("1000");
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
// 策略枚举
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
* payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
public static void main(String[] args) {
System.out.println(PayrollDay.FRIDAY.pay(2,0.3));;
System.out.println(PayrollDay.SUNDAY.pay(2,0.3));;
}
}
7. 方法
7.1 检查参数有效性
7.2 必要时进行保护性拷贝
// 不安全
public Date start() {
return start;
}
public Date end() {
return end;
}
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
// 攻击1
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies i
// 攻击2
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);
// 安全
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start +" after "+ end);
}
注意:
- 保护性拷贝是在参数有效性检查之前进行,因为有效性是针对检查之后的对象
- 对参数类型可以被不可信任方子类化的参数,不要使用clone()进行保护性拷贝。因为对于非final对象如Date,clone()方法无法保证返回java.util.Date对象,它可能返回的是恶意的子类实例
- 只要有可能,使用不可变对象作为对象组件,这样就不需要保护性拷贝了
- 保护性拷贝可能会有性能损失,如果确定客户端可以信任,则在文档中指明客户端不可修改受到影响的组件
7.3 关于方法签名的设计
避免参数超过四个的三种方法:
- 拆成多个子方法
- 创建辅助类,保存参数的分组
- 使用Builder模式
对于参数类型,优先使用接口而不是类,如用Map而不是HashMap;
8. 通用程序设计
8.1 需要精确的答案,不要使用float和double
System.out.println(1.03 - .42);//0.6100000000000001
System.out.println();
System.out.println(1.00 - 9 * .10);//0.09999999999999998
System.out.println();
使用BigDecimal,int或long进行货币运算
需要十进制小数点--BigDecimal
不超过9位十进制数字--int
不超过18位十进制数字--long
9. 异常
9.1 只针对异常情况使用异常
JVM在处理异常情况时可能性能会更低
9.2 对可恢复的情况使用受检异常,对编译错误使用运行时异常
9.3 尽量使用标准的异常
9.4 抛出与所在类层次对应的异常
异常转义:高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常
9.5 在异常的构造器中保存异常的细节信息
10. 并发
10.1 同步访问共享的可变数据
正确的使用同步,可以保证没有任何方法会看到对象处于不一致的状态。
将可变数据限制在单个线程中,不然每个读或者写操作必须执行同步
参考:Java并发编程:volatile关键字解析
10.2 避免过度同步
在一个被同步的区域内部,不要调用那些外来的方法,如果被设计成要被覆盖的方法,或者客户端以函数对象形式提供的方法,因为你无法控制它们。比如我在遍历一个List,在同步的代码块内是没问题,但如果调用了一个外部方法,导致其回调删除了List中的一个元素,就会报错。
在一个被同步的区域内部,尽量少做事情,把耗时的操作移到外面去
10.3 executor和task优于线程
现在关键的抽象不再是Thread,而是工作单元,称作任务(task),有Runnable及其近亲Callable两种。执行任务的通用机制是executor service。
executor framework 也有个替代java.util.Timer的机制,即ScheduledThreadPoolExecutor .Timer是单线程的,如果其唯一线程抛出未被捕获的异常,timer就会停止运行。而executor 支持多线程,并可从抛出为受检异常的任务中恢复。
10.4 并发工具优于wait和notify
没理由在新代码中使用,如果不得不使用wait和notify:
- 始终应该使用wait循环模式来调用wait方法(即要在while循环内部调用,调用wait前后测试条件的成立与否)
- 一般用notifyAll而不是notify,它可以唤醒所有需要被唤醒的线程,非目标的线程会在检查等待条件后继续等待。除非等待状态的所有线程都在等待同一个条件,而每次只唤醒其中一个。
10.5 线程安全的文档化
当一个类的实例或静态方法被并发调用时,这个类的行为如何,是该类与客户端建立的约定的重要组成成分。
线程安全性是有多个级别的,一个类必须在文档中清楚说明所支持的安全级别。
- 无条件线程安全类:必须把锁对象私有化,防止被客户端访问
- 有条件线程安全类:文档中必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪些锁
10.6 慎用延迟初始化
延迟初始化降低了初始化类或者创建实例的开销,但增加了访问被延迟初始化域的开销。
要明确是否延迟,唯一的办法是测量类在用和不用延迟时的性能差别。
如果多个线程会共享同一个延迟初始化的域,那必须做好同步:
- 对静态域的初始化:
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() {
return FieldHolder.field;
}
- 对只初始化一次的实例域的初始化:
// 双重检查
private volatile FieldType field4;
FieldType getField4() {
FieldType result = field4;
if (result == null) { // First check (no locking)
synchronized (this) {
result = field4;
if (result == null) // Second check (with locking)
field4 = result = computeFieldValue();
}
}
return result;
}
- 对重复初始化的实例域的初始化:
// 单检查模式,如果放宽到每个线程可以也初始化一次实例,并且实例是不可变对象,就删去volatile
private volatile FieldType field5;
private FieldType getField5() {
FieldType result = field5;
if (result == null)
field5 = result = computeFieldValue();
return result;
}
10.7 不要依赖于线程调度器
线程调度器:当有多个线程可以运行时,它决定哪些线程将会运行,以及运行多长时间。不同的操作系统采用的策略可能大相径庭,依赖于线程调度器的程序,很可能是不可移植的。
进而不要依赖Thread.yield或者线程优先级,这些措施仅仅是对调度器做些暗示。可以使用线程优先级提高已能正常工作的程序的质量,但不能用来修正一个原本不能工作的程序。
要编写健壮的、响应良好的、可移植的多线程程序,最好的办法是确保可运行线程的平均数量不明显多于处理器数量。注意,可运行线程的平均数量不等于线程总数量,因为等待的线程并不是可运行的。
10.8 避免使用线程组
11. 序列化
序列化:把一个对象编码成字节流
反序列化:从字节流编码中重新构建对象
一旦对象序列化后,就可以从一台虚拟机传递到另一台虚拟机上,或者存储到磁盘,供后续反序列化使用。
参考:
Effective Java--序列化--你以为只要实现Serializable接口就行了吗
对Java Serializable(序列化)的理解和总结
11.1 谨慎实现Serializable接口
实现Serializable接口的缺点
1. 类被发布后,改变类的灵活性变小
如果一个类实现了Serializable接口,它的字节流编码也变成了它导出API的一部分,它的子类都等价于实现了序列化,以后如果想要改变这个类的内部表示法,可能导致序列化形式不兼容。
如果被序列化的类没有显示的指定serialVersionUID标识(序列版本UID),系统会自动根据这个类来调用一个复杂的运算过程生成该标识。此标识是根据类名称、接口名称、所有公有和受保护的成员名称生成的一个64位的Hash字段,若我们改变了这些信息,如增加一个方法,自动产生的序列版本UID就会发生变化,等价于客户端用这个类的旧版本序列化一个类,而用新版本进行反序列化,从而导致程序失败,类兼容性遭到破坏。
2. 更容易引发Bug和安全漏洞
一般对象是由构造器创建的,而序列化也是一种对象创建机制,反序列化也可以构造对象。由于反序列化机制中没有显式的构造器,开发者一般很容易忽略它的存在。
构造器创建对象有它的约束条件:不允许攻击者访问正在构造过程中的对象内部信息,而用默认的反序列化机制构造对象过程中,很容易遭到非法访问,使构造出来的对象,并不是原始对象,引发程序Bug和其他安全问题。
3. 随着类发行新版本,相关测试负担加重
当一个可序列化的类被修改后,需要检查“在新版中序列化一个实例,在旧版本中反序列化”及“在旧版本中序列化一个实例,在新版本反序列化”是否正常,当发布版本增多时,这种测试量幂级增加。如果开发者早期进行了良好的序列化设计,就可能不需要这些测试。
4. 开销大
序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其他对象也进行序列化。如果一个对象包含的成员变量是容器类等并深层引用时(对象是链表形式),此时序列化开销会很大,这时必须要采用其他一些手段处理。
无参的构造器
若父类没有实现Serializable,而子类需要序列化,需要父类有一个无参的构造器,子类要负责序列化(反序列化)父类的域,子类要先序列化自身,再序列化父类的域。
至于为什么父类要有无参构造器,因为父类没有实现Serializable接口时,虚拟机不会序列化父对象,而一个Java对象的构造必须先有父对象,才有子对象,反序列也是构造对象的一种方法,所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
11.2 使用自定义的序列化形式
一个理想的序列化形式,应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法相互独立。
若一个对象的物理表示法等同于它的逻辑内容,则可能适合使用默认的序列化形式。一般而言,只有自定义的和默认的形式基本相同,才考虑使用默认的。如一个表示人名的Name类,从逻辑角度一个名字由姓和名组成,而Name中亦只有firstName和lastName两个字段,故是可以采用默认形式的。
若一个对象的物理表示法与逻辑数据内容有实质性区别时,如下面的类:
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
}
该类逻辑上是一个字符串序列,但物理意义是双向链表形式,使用默认序列化有以下4个缺点:
a) 该类导出API被束缚在该类的内部表示法上,链表类也变成了公有API的一部分,若将来内部表示法发生变化,仍需要接受链表形式的输入,并产生链式形式的输出。
b) 消耗过多空间:像上面的例子,序列化既表示了链表中的每个项,也表示了所有链表关系,而这是不必要的。这样使序列化过于庞大,把它写到磁盘中或网络上发送都很慢;
c) 消耗过多时间:序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个图遍历过程。
d) 引起栈溢出:默认的序列化过程要对对象图执行一遍递归遍历,这样的操作可能会引起栈溢出。
对于StringList类,可以用treansient修饰head和size变量控制其序列化,自定义writeObject,readObject进行序列化。
具体改进如下:
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
//此类不再实现Serializable接口
private static class Entry {
String data;
Entry next;
Entry previous;
}
private final void add(String s) {
size++;
Entry entry = new Entry();
entry.data = s;
head.next = entry;
}
/**
* 自定义序列化
* @param s
* @throws IOException
*/
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject();
s.writeInt(size);
for (Entry e = head; e != null; e = e.next) {
s.writeObject(e.data);
}
}
/**
* 自定义反序列化
* @param s
* @throws IOException
*/
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
s.defaultReadObject();
size = s.readInt();
for (Entry e = head; e != null; e = e.next) {
add((String) s.readObject());
}
}
}
(2) 如果对象状态需要同步,则对象序列化也需要同步
如果选择使用了默认序列化形式,就要使用下列的writeObject方法
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
}
如果把同步放在writeObject中,就必须确保它遵守与其他动作相同的锁排列(lock-ordering)约束条件,否则由遭遇资源排列(resource-ordering)死锁的危险
用来两个private方法来实现自身序列化,这两个函数为什么会被调用到?
writeObject: 用来处理对象的序列化,如果声明该方法,它会被ObjectOutputStream调用,而不是默认的序列化进程;
readObject: 和writeObject相对应,用来处理对象的反序列化。
ObjectOutputStream使用反射getPrivateMethod来寻找默认序列化的类是否声明了这两个方法,所以这两个方法必须声明为private提供ObjectOutputStream使用。虚拟机会先试图调用对象里的writeObject, readObject方法,进行用户自定义序列化和反序列化,若没有这样的方法,就会使用默认的ObjectOutputSteam的defaultWriteObject及ObjectInputStream里的defaultReadObject方法。
关键字transient
(1) transient关键字作用是阻止变量的序列化,在变量声明前加上此关键字,在被反序列化时,transient的变量值被设为初始值,如int型是0, 对象型是null;
(2) transient关键字只能修饰变量,而不能修饰方法和类;
(3) 静态变量不管是否被transient修饰,均不能被序列化;
(4)defaultWriteObject被调用时,未被标记transient的实例域都会被序列化,所以可以加transient的都加上
11.3 保护性地编写readObject方法
readObject实际上相当于另一个构造器(不严格地说,用字节流作唯一参数的构造器),也需要检查参数有效性,必要时作保护性拷贝。
如下面的类:
public final class Period implements Serializable {
private Date start;
private Date end;
public Period(Date start, Date end) {
this.start = new Date(start.getTime());//保护性拷贝
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start bigger end");
}
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
start = new Date(start.getTime());//保护性拷贝
end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start bigger end");
}
}
}
readObject方法不可以调用可以被覆盖的方法,因为被覆盖的方法将在子类的状态被反序列化之前先运行,这样程序很可能会crash.
11.4 对于实例控制,枚举类型优先于readObsolve
采用readObsolve方法实现单例序列化
对于下面的单例
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getINSTANCE() {
return INSTANCE;
}
}
通过序列化工具,可以将一个类的单例的实例对象写到磁盘再读回来,从而有效获得一个实例。如果想要单例实现Serializable,任何readObject方法,不管显示还是默认的,它会返回一个新建的实例,这个新建实例不同于该类初始化时创建的实例。从而导致单例获取失败。但序列化工具可以让开发人员通过readResolve来替换readObject中创建的实例,即使构造方法是私有的。在反序列化时,新建对象上的readResolve方法会被调用,返回的对象将会取代readObject中新建的对象。
具体方法是在类中添加如下方法就可以保证类的Singleton属性:
//该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个Elvis实例
private Object readResolve() {
return INSTANCE;
}
由于Elvis实例的序列化形式不需要包含任何实际的数据,因此该类的所有的类成员(field)、带有对象引用类型的实例域都应该被transient修饰。
采用枚举实现单例序列化
采用readResolve的一些缺点:
- readResolve的可访问性需要控制好,否则很容易出问题。如果readResolve方法是受保护或是公有的,且子类没有覆盖它,序列化的子类实例进行反序列化时,就会产生一个超类实例,这时可能导致ClassCastException异常。
- readResolve需要类的所有实例域都用transient来修饰,除非它们都是基本数据类型,否则可能被攻击。
而将一个可序列化的实例受控类用枚举实现,可以保证除了声明的常量外,不会有别的实例。
所以如果一个单例需要序列化,最好用枚举来实现:
public enum Elvis implements Serializable {
INSTANCE;
private String[] favriteSongs = {"test", "abc"};//如果不是枚举,需要将该变量用transient修饰
}
11.5 考虑用序列化代理代替序列化实例
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 precede start
* @throws IllegalArgumentException
* if start is after end
* @throws NullPointerException
* if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(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 SerializationProxy implements Serializable {
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 serialization proxy pattern - page 312
private Object writeReplace() {
return new SerializationProxy(this);
}
// readObject method for the serialization proxy pattern - Page 313
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}
public class MutablePeriod {
// A period instance
public final Period period;
// period's start field, to which we shouldn't have access
public final Date start;
// period's end field, to which we shouldn't have access
public final Date end;
public MutablePeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
// Serialize a valid Period instance
out.writeObject(new Period(new Date(), new Date()));
/*
* Append rogue "previous object refs" for internal Date fields in
* Period. For details, see "Java Object Serialization
* Specification," Section 6.4.
*/
byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
bos.write(ref); // The start field
ref[4] = 4; // Ref # 4
bos.write(ref); // The end field
// Deserialize Period and "stolen" Date references
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();
Period p = mp.period;
Date pEnd = mp.end;
// Let's turn back the clock
pEnd.setYear(78);
System.out.println(p);
// Bring back the 60s!
pEnd.setYear(69);
System.out.println(p);
}
}