《Effective Java》笔记

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条原则:


不可变类的5条原则.jpg

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。原生态类型就像从类型声明中删除了所有泛型信息一样。实际上,原生态类型List与java平台没有泛型之前的接口类型List完全一样。

List是个参数化类型,表示可以包含任何对象类型的一个集合;List是一个通配符类型,表示只能包含某种未知对象类型的一个集合;List是原生态类型,它脱离泛型系统。前两种是安全的,可以在编译时就抛出错误,最后一种不可以,它是不安全的

原因

  • 无法在编译时就发现类型错误
  • 可读性差

java之所以支持原生态类型是未来兼容。

参考:java 泛型之不要使用原生态类型

5.2 消除非受检警告

如果不是确定没关系,不要置之不理,其可能有潜在的CLassCastException风险

5.3 列表优于数组

数组是协变的,如果sub是super的子类型,那sub[]就是super[]的子类型,总是在运行时才发现类型错误;

Object[] objectArr = new Long[1];
objectArr[0] = "11";// 运行时报错

List objectList = new List();
//无法通过编译,因为List不是协变的,List不会成为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 尽量使用标准的异常

常用的异常.jpg

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 线程安全的文档化

当一个类的实例或静态方法被并发调用时,这个类的行为如何,是该类与客户端建立的约定的重要组成成分。

线程安全性是有多个级别的,一个类必须在文档中清楚说明所支持的安全级别。


线程安全级别.jpg
  • 无条件线程安全类:必须把锁对象私有化,防止被客户端访问
  • 有条件线程安全类:文档中必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪些锁

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的一些缺点:

  1. readResolve的可访问性需要控制好,否则很容易出问题。如果readResolve方法是受保护或是公有的,且子类没有覆盖它,序列化的子类实例进行反序列化时,就会产生一个超类实例,这时可能导致ClassCastException异常。
  2. 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);
    }
}

你可能感兴趣的:(《Effective Java》笔记)