《Effective Java》读书笔记

创建和销毁对象

静态工厂模式

  • 构造器里未传參的成员不会被初始化。

    int类型是0,布尔类型是false,String类型是null。List<>也是null。

重叠构造器

  • 进阶1:javabean模式。使用set方法来初始化成员,缺点是构造过程中javabean可能处于不一致状态(能够理解成该模式下成员的设置的分步进行的,可能某处使用到该类的某个成员时其还未被初始化),而且该模式阻止了把类变成不可能的可能,须要考虑线程安全。
  • 进阶2: Builder模式:类里定义一个静态类builder(事实上就是javabean),对builder初始化完毕后使用build()返回该类,Buidler模式的状态不一致是builder,而不是类本身,而且类自身的成员也可设置成final。

修饰符

  • 长度非零的数组总是可变的,即使是final类型:
public static final int[] VALUES={...} //错误
//正确1:添加一个公有的不可变列表
private static final int[] VALUES=...
public static final List< intergeR > VALUES=
{Collections.unmodifiableList(Arrays.adList(PRIVATE_VALUES));
//正确2:返回私有数组的拷贝
private static final int[] VALUES=...
public static final int[] values() {
    return VALUES.clone();
}
  • 假设是公有类。直接暴露数据会有非常大的隐患,由于当你将来想改变其内部表示法时已经不可能了。由于共同拥有类的client代码已经遍布各处了。
public class Point{ //错误
    public int x;
    public int y;
}

public class Point{ //正确
    private int x;
    private int y;

    public int getX() { return x;}
}

类和接口

使可变性最小化

  • 线程安全最easy的做法:仅仅提供訪问方法。不提供设值方法,对对象的加减乘除都又一次返回一个新的对象。

    对象不会变化,也就不要求同步。

  • 能够把开销昂贵的计算结果缓存起来。比如String的hashcode方法。第一次计算后会将结果保存在成员hashCode里。

复合优先继承

  • 子类脆弱:比如一个类继承HashSet,假设子类里重写了addAll和add方法来计数,就会导致错误。由于HashSet的addAll是基于add方法实现的。不能保证父类不随着版本号而变化,因此extends 子类继承父类是非常脆弱的。
  • 仅仅有当子类真正是超类的子类型。即A和B。两者确实存在B is A的关系时。类B才应该扩展A。假设答案是否定的,通常情况下B应该包括A的一个私有实例,而且暴露一个较小的。简单的API:A本质上不是B的一部分。仅仅是它的实现细节而已。

装饰者模式(Decorator模式)

结合上面说到的,HashSet是implement Set类的,在HashSet里重写了Set接口定义的add,addAll等方法。

因此新的子类继承Hashset重写add、addAll就不可避免会将HashSet里的实现继承下来。

使用装饰者模式:ForwardingSet implements Set,该类有成员private final Set s s,构造器里就是传入一个Set ,该类不详细实现Set的不论什么方法,比如:

public boolean add(E e) {
    return s.add(e);
}

InstrumentedSet extends ForwardingSet。构造器super父类就可以。在这个类里加入一些功能,比如:

@Override
public boolean add(E e){
    count++;
    return super.add(e);
}

这样的模式下,InstrumentedSet 仅仅是一个包装类,仅仅是对其成员Set进行修饰,为它添加计数特性。

包装类并不实现详细功能,构造器里传入的就是实现详细功能的Set,能够是HaseSet或者自己实现的Set。

另可參考阅读:
Android源代码学习之装饰模式应用

继承后构造方法的调用

  1. 假设子类未定义构造方法。则调用父类的无參数的构造方法。
  2. 假设子类定义了构造方法。不论是无參数还是带參数,在创建子类的对象的时候,首先运行父类无參数的构造方法,然后运行自己的构造方法。
  3. 假设子类调用父类带參数的构造方法,能够通过super(參数)调用所须要的父类的构造方法,切该语句做为子类构造方法中的第一条语句。
  4. 假设某个构造方法调用类中的其它的构造方法,则能够用this(參数),切该语句放在构造方法的第一条。
    说白了:原则就是,先调用父亲的。

    (没有就默认调,有了就按有的调,反正仅仅要有一个就能够了)

public class Son extends Father {
    public Son() {
    //        super(); //没加默认调用父类无參构造方法
        super("from son");
        Log.e("zyz", "son-constructor");
    }

    public Son(String str) {
//        super(); //没加默认调用父类无參构造方法
        Log.e("zyz", str + " son-constructor-with-params");
    }

    @Override
    public void print() {
        Log.e("zyz", "son-print");
    }
}

public class Son extends Father {
    public Son() {
//        super(); //没加默认调用父类无參构造方法
        super("from son");
        Log.e("zyz", "son-constructor");
    }

    public Son(String str) {
//        super(); //没加默认调用父类无參构造方法
        Log.e("zyz", str + " son-constructor-with-params");
    }

    @Override
    public void print() {
        Log.e("zyz", "son-print");
    }
}

接口优于抽象类

抽象类能够写实例方法,通过派生继承,实现代码复用(子类可直接调用父类方法)。但由于重用方法添加了耦合度。接口的方法一定须要重写,最大程度实现了解耦。

类层次优于标签类

标签类:
比如使用枚举或常量定义了圆和矩形,成员里有半径、长、宽。在公共方法 计算面积里,使用switch来推断是那种形状,再分别计算。相似的把多个实现乱七八糟地挤在单个类中,破坏可读性,又添加了内存占用。由于实例承担着属于其它类型的域。

应该使用类层次来优化:
定义一个抽象类,包括抽象方法:将共同拥有的方法(计算面积),假设有公有的成员还能够将其放在抽象类中。

之后不同的类圆和矩形继承公共抽象类,另外加入自己的參数。并重写自己的计算面积的方法。

优先考虑静态成员

假设成员类不要求訪问外围实例,就要定义成静态内部类。

非静态内部类始终要保持外围对象的引用,不仅消耗内存,还将导致外围实例无法被垃圾回收。
比如Map实现的内部都有Entry对象,每一个Entry都与Map关联,可是entry的方法(getKey/getValue)等并不须要訪问Map,因此私有的静态成员类是最佳的选择。


- 假设一个嵌套类须要在单个方法之外可见。或者它太长了不适合放在方法内部。就使用成员类。
- 假设成员类的每一个实例都须要一个指向外围实例的应用。就使用非静态成员类。否则就使用静态成员类。
- 假设嵌套类属于一个方法的内部。且你仅仅须要在一个地方创建实例。而且已经有了一个预置的类型能够说明这个类的特征,就使用匿名类。否则就使用局部类。

泛型

列表优先于数组

二者的不同点:

数组是协变的(covariant)

假设B是A的子类,那么B[]就是A[]的子类型。

//编译时不报错,运行时报错ArrayStoreException
Object[] test = new Long[1];
test[0] = "test";

而两个不同的类型A、B,List既不是List的子类也不是超类。

List<Object> test2 = new ArrayList<Long>(); //编译时报错
test2.add("123");

数组是详细化的(reified)

数组在运行时才知道并检查他们的元素类型约束。

泛型则是通过擦除(erasure)来实现的。泛型仅仅在编译时强化类型信息,在运行时擦除元素类型信息。擦除就是使泛型能够与没有使用泛型的代码随意互用。

利用有限制通配符提升API的灵活性

PECE producer-extends。consumer-siper
假设參数化类型表示生产者T,就使用

//src产生E实例供使用,是生产者
public void pushAll(Iterable<? extands E> src) {
    for (E e : src) push(e);
}
//dst消费E实例,是消费者
public void popAll(Collection<E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}

不要用通配符类型作为返回參数

枚举和注解

用enum取代int常量

(android不推荐使用enum)

  • 枚举本质上是int值
  • 枚举同意加入随意的方法和域
public enum Test {
    APPLE("test1", 2),
    pen("test2", 1);

    private final String name;

    private final int num;

    Test(String name, int num) {
        this.name = name;
        this.num = num;
    }

    public void print() {
        Log.e("zyz", APPLE.name + APPLE.num);
    }
}
//遍历枚举
Test[] values = Test.values();

用实例域取代序数

  • 全部枚举都有一个ordinal方法,返回每一个枚举常量在类型中的数字位置。

    避免使用ordinal方法,除非是编写EnumSet和EnumMap这样的基于枚举的通用数据结构。使用实例域(相似成员变量)来保存与枚举相关的值。

注解

  • 注解类型声明
@Retention(RetentionPolicy.RUNTIME) //运行时保留
@Target(ElementType.METHOD) //仅仅在方法声明中才是合适的
public @interface MyTest {

}

坚持使用Override注解

覆盖equals时的參数是Object类型的。否则则变成了重载。

但假设使用@Override注解后写错了编译器就会报错。

用标记接口定义类型

  • 标记接口是没有包括方法声明的接口,仅仅是指名了某个类实现了具有某种属性的接口(比如Serializable接口)
  • 标记接口胜过标记注解的两点:
    1. 接口定义的类型是由被标记类的实例实现的,注解则未定义这样的类型。

      这个类型同意你在编译时捕捉到错误。而不像注解须要在运行时才干捕捉到

    2. 接口能够被更加精确地锁定。假设一个标记仅仅适用于特殊接口的实现,假设定义成标记接口就能够用它将唯一的接口扩展成它适用的接口。
  • 注解胜过接口的两点:
    1. 注解能够不断演变。而接口通常不可能在实现后再给它加入方法。

    2. 注解是注解机制的一部分。

      注解能够作为支持注解作为编程元素之中的一个的框架中具有一致性。

  • 接口和注解使用场景:
    1. 假设标记是应用到不论什么程序元素而不是类或接口。就必须使用注解,由于仅仅有类和接口能够用来实现或扩展接口。

    2. 假设标记仅仅给类和接口。若要编写多个仅仅接受有这样的标记的方法则优先使用接口。这样能够在编译时进行类型检查。
    3. 假设要永远限制这个标记仅仅用于特殊接口的元素,最好将标记定义成该接口的一个子接口。
    4. 假设2,3都是否定的,则应该使用注解。

方法

检查參数的有效性

  • assert 对于有些參数,方法本身没实用到,却被保存起来供以后使用,能够使用断言检验这类參数的有效性。

    假设断言失败,则会抛AssertionError。

必要时进行保护性拷贝

  • 假设类的成员是可变的,为了保护内部信息变化。对于构造器的每一个可变can’shu參数进行保护性拷贝是必要的。使用被封对象作为实例的组件。而不使用原始的对象。但注意,保护性拷贝是在检查參数的有效性之前进行的,而且有效性检查是针对拷贝之后的对象而不是原始对象。
  • 慎用clone

    假设对于非final的成员。不能保证clone方法一定返回同样的类的对象。它有可能返回专门出于恶意目的而设计的不可信子类的实例,比如这样的子类能够在每一个实例被创建时把指向该实例的引用记录到一个私有的静态列表中,而且同意攻击者訪问这个列表。这将使得攻击者能够自由地控制全部的实例。

    为了阻止这样的攻击,对于參数类型能够被不可信任方子类话的參数。请不要使用clone方法进行保护性拷贝。

  • 另外须要改动訪问方法,返回可变内部域的保护性拷贝:

    public Data end() {
        return new Data(end.getTime());
    }
  • 仅仅要可能,都应该使用不可变的对象作为对象内部的组件,这样就不必再为保护型拷贝担心。

慎用重载

  • 类型还是父类。尽管调用父类方法指向子类引用。

  • 安全而保守的策略是:永远不要导出两个具有同样參数数目的重载方法。假设方法使用可变參数,保守的策略是根本不要重载它。

慎用可变參数

  • 假设client调用这种方法时并没有传递參数进去,它就会在运行时而不是编译时失败。

    //带两个參数。避免没有传參导致的问题
    static init min(int firstArg, int... remainingArgs) {
        int min = firstArg;
        for(int arg : remainingArgs) {
            ...
        }
    }
  • 在重视性能的情况下。使用可变參数要特别小型,可变參数方法的每次调用都会导致进行一次数组分配和初始化。

    能够使用多个重载方法,每一个重载方法带有0至3个普通參数,当參数数目超过3个时,就使用可变參数方法。

返回零长度的数组或集合,而不是null

通用程序设计

for each循环优于传统的for循环

  • 假设你在编写的类型是一组元素。实现Iterable能够同意用户利用for-each循环遍历你的类型。
  • 三种常见的无法使用for-each的情况:
    1. 过滤——须要遍历集合并删除选定元素
    2. 转换——须要遍历集合并取代它的部分或全部元素值
    3. 平行迭代——须要并行地遍历多个集合,就须要显式地控制迭代器或者索引变量以便全部迭代器或索引变量都能够得到同步前移

了解和使用类库

  • 伪随机数生成器

    //错误
    Math.abs(new Random().nextInt());
    //正确
    Random.nextInt(int)
  • 了解和使用标准类库提供的便利工具,而不用浪费时间为那些与工作不太相关的问题提供特别的解决方式。

    标准类库太庞大了。以至于不可能去学习全部文档。可是每一个程序猿都应该熟悉java.lang,java.util,某种程度上还有java.io种的内容。

    有两种工具值得特别一提。

    • Collections Framework 用来表示和操作集合
    • java.util.concurrentbao’zhong包中添加了一组并发使用工具

    总而言之。不要又一次发明轮子,假设你要做的事情看起来是十分常见的,有可能类库中已经有某个类完毕了这样的工作。

假设须要精确的答案,请避免使用float和double

float和double类型尤其不适合用于货币计算,由于要让一个float或double精确地表示0.1(或者10的ren’he’qi’ta不论什么其它负数次方值)是不可能的。

改进

使用BigDecimal取代double:

BigDecimal bigDecimal = new BigDecimal(0.1); 

BigDecimal同意你全然控制舍入。每当一个操作设计舍入的时候,它同意你从8种舍入模式中选择其一。

可是缺点是与基本运算类型比,不仅不方便,而且非常慢。

假设性能非常关键,而且又不介意自己记录是金子小数点,而且涉及的数值又不太大,就能够使用int或long(比如0.1改变单位计作10)。假设数值范围没超过9位十进制数字,就能够使用int。

假设不超过18位数值,就能够使用long。

假设数值超过18位数字,就必须使用BigDecimal。

基本类型优于装箱基本类型

当程序装箱了基本类型值时,会导致高开销和不必要的对象创建。

当心字符串连接的性能

连接操作符不适合运用在大规模的场景中,为连接n个字符串而反复地使用字符串连接操作符。须要n的平方级的时间。

这是由于字符串不可变。当两个字符串被连接在一起时,它们的内容都要被拷贝。

使用StringBuilder:

StringBuilder test = new StringBuilder("test");
test.append("test2")

通过接口引用对象

假设有合适的接口类型存在。那么对于參数、返回值、变量和域来说,就都应该使用接口类型进行声明。

仅仅有当你利用构造器创建某个对象的时候,才真正须要引用这个对象的类。

List<String> list = new ArrayList<>();

这样会使程序更灵活,当你决定更换实现时,仅仅须要改变构造器中类的名称:

List<String> list = new Vector<>();

全部的代码都能够继续工作,代码并不知道原来的实现类型,所以对于这样的变化并不在意。

接口优先于反射机制

反射机制同意一个类使用还有一个类,即使当前者被编译的时候后者还根本不存在,然而这样的能力也是要付出代价的:

  • 丧失了编译时类型检查的优点(包括异常检查)
  • 运行反射訪问所须要的代码非常笨拙和冗长
  • 性能损失

异常

仅仅针对异常的情况才使用异常

//Dont't do this
try {
    int i = 0;
    while (true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundsException e) {

}

不要优先使用基于异常的模式:

  • 异常机制的设计初衷是用于不正常的情形,所以非常少会有JVM实现试图对它们进行优化。
  • 代码块放在try-catch块中反而阻止了现代JVM实现本来可能要运行的某些特定优化。

  • 对数组进行比那里的标准模式并不会导致冗余的检查。有些现代的JVM实现会将它们优化掉。
  • 基于异常的循环模式不仅模糊了代码的意图,还减少了性能。而且它还不能保证正常工作,假设出现不想关的bug。这个模式会悄悄地失效。

努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这样的属性的方法被称为具有失败原子性(failure atomic)。有几种途径能够实现这样的效果:

  1. 在运行操作前检查參数的有效性。这能够使在对象状态被改动前先抛出适当的异常。

  2. 调整计算处理过程的顺序,使得不论什么可能会失败的计算部分都在对象状态被修之前发生。
  3. 编写一段恢复代码,由它来拦截操作过程发生的失败。以及使对象回滚到操作開始之前的状态,这样的办法主要用于永久性的数据结构。

  4. 在对象的一份暂时拷贝上运行操作。操作完毕之后再用暂时拷贝中的结果取代对象的内容。

不要忽略异常

忽略一个异常非常easy,仅仅需将方法调用通过try语句包围起来,并包括一个空的catch块。空的catch块会使异常达不到应有的目的。至少,catch块也应该包括一条说明。解释为什么能够忽略这个异常。

并发

正确地使用同步能够保证没有不论什么方法会看到对象处于不一致的状态中。它还能够保证刚进入同步方法或者同步代码块的每一个线程,都看到由同一个锁保护的之前全部的改动效果。换句话说,读取一个非long或double类型的变量,能够保证返回的值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地改动这个变量也是如此。

不要使用 Thread.stop方法。要阻止一个线程妨碍还有一个线程。建议做法是让第一个线程轮训一个boolean域,这个域一開始为false,可是能够通过第二个线程设置为true,以表示第一个线程将终止自己。由于boolean域的读写操作都是原子的,程序猿在訪问这个域的时候不再使用同步。

实际上。假设读和写操作没有都被同步,同步就不会起作用。

假设变量修饰符是volatile,则读取变量时不须要锁,尽管volatile修饰符不运行相互排斥訪问,但它能够保证不论什么一个线程在读取该域的时候都将看到近期刚刚被写入的值。

使用volatile的时候务必要小心。

//错误
private static volatile  int number = 0;
//须要使用synchronization
public static int getNumber() {
    return number++;
}

尽管number是原子的。可是增量操作符不是原子的,它首先读取值。然后写回一个新值。

假设第二个线程在第一个线程读取旧值和返回新值期间读取这个域就会出错。

避免过度同步

在一个被同步的区域内部。不要调用设计成要被覆盖的方法,或者是由client以函数对象的形式提供的方法。这样的方法是外来的,这个类不知道方法会做什么事情。也无法控制它。从同步区域中调用它非常可能会导致异常、死锁或者数据损坏。

通常。你应该在同步区域内做尽可能少的工作。

假设你必须要运行某个非常耗时的动作,应该设法把这个动作移到同步区域的外面。

executor 和 task 优先于线程

Java1.5添加了java.util.concurrent,这个包中包括了一个Executor Framework:

ExecutorService executorService = Executors.newSingleThreadExecutor();
//运行提交一个runnable方法
executorService.execute(runnable);
//告诉executor怎样优雅地终止
executor.shutdonw();

你能够利用executor service完毕很多其它的事情。

比如,能够等待一个任务集合中的不论什么任务或全部任务完毕(invokeAny或invokeAll)。你能够等待executor service优雅地完毕终止(awaitTermination)。能够在任务完毕时逐个地获取这些任务的结果(ExecutorCompletionService)等。

并发工具优于wait和notify

自从java1.5发型版本号開始,java就提供了更高级的并发工具,他们能够完毕曾经必须在wait和notify上手写代码来完毕的各项工作。其分成三类:

  • Executor Framework
  • 并发集合(Concurrent Collectionin)
  • 同步器(Synchronizer)
    并发集合为标准的集合接口(如List、Queue、Mpa)提供了高性能的并发实现。为了提供高并发性。这些实如今内部自己管理同步。因此。并发集合中不可能排除并发活动,将它锁定没有什么作用,仅仅会是程序的速度变慢。

同步器(Synchronizer)是一些使线程能够等待还有一个线程的对象。同意他们协调动作。最经常使用的同步器是CountDownLatch和Semaphore。

倒计数锁存器(CountDown Latch)是一次性的障碍,同意一个或者多个线程等待一个或者多个其它线程来做某些事情。CountDownLatch是唯一构造器带有一个int类型的參数,这个int參数是指同意全部在等待的线程被处理之前。必须在锁存器上调用countDown方法的次数。

比如:一个方法带有一个运行该动作的executor。一个并发级别(表示要并发运行该动作的次数),以及表示该动作的runnable。全部的工作线程自身都准备好,要在time线程启动时钟之前运行该动作(为了实现准确的定时)。当最后一个工作线程准备好运行该动作时,timer线程就“发起头炮”,同事同意工作线程运行该动作,一旦最后一个工作线程运行完该动作。timer线程就马上停止计时。直接在wait和notify上实现这个逻辑至少来说会非常混乱,而在CountDownLatch之上实现则相当简单:

public long getTime(Executor executor, int councurrency, final Runnable action) throws InterruptedException {
    final CountDownLatch ready = new CountDownLatch(councurrency);
    final CountDownLatch start = new CountDownLatch(1);
    final CountDownLatch done = new CountDownLatch(councurrency);
    for (int i = 0; i < councurrency; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                ready.countDown();
                try {
                    start.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();
                }
            }
        });
    }
    ready.await();
    long startNano = System.nanoTime();
    start.countDown();
    done.await();
    return System.nanoTime() - startNano;
    }

用ready来告诉timer线程他们已经准备好了。然后工作线程会在start上等待。当最后一个工作线程调用ready.countDown时,timer线程记录下起始时间,并调用start.countDown。同意全部的工作线程继续进行。然后timer线程在done上等待。直到最后一个工作线程运行完该动作,并调用donw.countDown。一旦调用这个,timer线程就会苏醒过来,并记录下结束时间。

wait方法的标准模式:

synchronized(obj) {
    while() {
        obj.wait(); //release lock, and reacquires on wakeup
    }
}

始终应该使用wait循环模式来调用wait方法。永远不要在循环之外调用wait方法。循环会在等待之前和之后測试条件。

线程安全性的文档化

线程安全性的几种级别。(这份列表并没有涵盖全部的可能。而仅仅是些常见的情形:

  • 不可变的(immutable):这个类的实例是不变的。所以不须要外部的同步。比如String、Long、BigInteger。
  • 无条件的线程安全(unconditionnally thread-safe):这个类的实例是可变的。可是这个类有着足够的内部同步。所以它的实例能够被并发使用。无需不论什么外部同步。 比如:Random和ConcurrentHashMa
  • 有条件的线程安全(conditionally thread-safe):除了有些方法为进行安全的并发而使用须要外部同步
  • 非线程安全(not thread-safe):这个类的实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每一个方法调用(或者调用序列)。这样的样例包括通用的集合实现,比如ArrayList和HashMap。

  • 线程对立的(thread-hostile):这个类不能安全地被多个线程并发使用,即使全部的方法调用都被外部同步包围。线程对立的根源通常在于。没有同步地改动静态数据。Java平台类库中。线程对立的类或者方法非常少。System.runFinalizersOnExit方法是线程对立的。但已经被废除了。
//私有锁对象
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}

私有锁对象模式仅仅能用在无条件的线程安全类上。有条件的线程安全类不能使用这样的模式,由于它们必须在文档中说明:在运行某些方法调用序列时。它们的client程序必须获得哪把锁。

私有锁对象模式特别适用于那些专门为继承而设计的类。假设这样的类使用它的实例作为锁对象。之类可能非常easy在无意中妨碍基类的操作,反之亦然,出于不同的目的而使用同样的锁,子类和基类非常可能会“互相绊住对方的脚”。

有条件的线程安全类必须在文档中指明哪些方法调用序列须要外部同步,以及在运行这些序列的时候须要获得哪把锁。假设你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来取代同步的方法以防止client程序和子类的不同步干扰。

慎用延迟初始化

假设处于性能的考虑须要对静态域使用延迟初始化:

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

static FieldHolder getField() {

假设处于性能的考虑须要对实例域使用延迟初始化:

private volatile FieldType field;

FieldTpye getField() {
    FieldType result = field;
    if(result == null) { //First check(no locking)
        synchronized (this) {
            result = field;
            if(result == null) //Second check(with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

假设须要延迟初始化一个能够接受反复初始化的实例域:

private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if(result == null) {
        field = result = computeFiedlValue();
    }
    return  result;
}

不要依赖于线程调度器

线程不应该一直处于忙-等状态,即反复地检查一个共享对象,以等待某些事情的发生。

不要让应用程序的正确性依赖于线程调度器。否则结果得到的应用程序将既不健壮。也不具有可移植性。不要依赖Thread.yield或者线程优先级。线程优先级能够用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本能不能工作的程序。

序列化

慎重地实现Serializable接口

实现Serializable接口而付出的巨大代价是,一旦一个类被公布,就大大减少了“改变这个类的实现”的灵活性。
假设一个类实现了Serializable接口,它的字节流编码(序列化形式)就变成了它的导出的API的一部分。一旦这个类被广泛使用,往往必须永远支持这样的序列化形式。

第二个代价是,它添加了出现bug和安全漏洞的可能性。

你可能会忘记确保:反序列化过程必须也要保证全部“由真正的构造器建立起来的约束关系”,而且不同意攻击者訪问正在构造过程中的对象的内部信息。

第三个代价是,随着类发行新的版本号,相关的測试负担也添加了。可序列化的类被修订后,你必须既要确保“序列化-反序列化”过程成功。也要确保结果产生的对象真正是原始对象的复制品。

内部类不应该实现Serializable。

假设一个类为了继承而设计。要更加小心。对于这样的类而言,在“同意子类实现Serializable接口”或者“禁止子类实现serialzable”两者间的一个折衷方案是:提供一个可訪问的无參构造器,这样的方案同意(但不要求)子类实现Serializable接口。

posted @ 2018-04-08 11:39  llguanli  阅读(169)  评论(0编辑  收藏  举报