笔记

创建和销毁对象

静态工厂模式

优点:有名称、不必再每次调用时都创建一个新的对象、可以返回原返回类型的任何子类型的对象、根据静态工厂方法的参数值对象的类可以随着每次调用而变化、方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。几乎所有这些实现都通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。

构造器里未传参的成员不会被初始化。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();
}

如果是公有类,直接暴露数据会有很大的隐患,因为当你将来想改变其内部表示法时已经不可能了,因为共有类的客户端代码已经遍布各处了。

public class Point{ //错误
    public int x;
    public int y;
}

public class Point{ //正确
    private int x;
    private int y;
    
    public int getX() { return x;}
}

类和接口

使类和成员的可访问性最小化

线程安全最容易的做法:只提供访问方法,不提供设值方法,对对象的加减乘除都重新返回一个新的对象。对象不会变化,也就不要求同步。

可以把开销昂贵的计算结果缓存起来,例如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<E> implements Set<E>,该类有成员private final Set<E> s s,构造器里就是传入一个Set<E> ,该类不具体实现Set的任何方法,例如:

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

InstrumentedSet<E> extends ForwardingSet<E>,构造器super父类即可,在这个类里添加一些功能,例如:

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

这种模式下,InstrumentedSet 只是一个包装类,只是对其成员Set<Set>进行修饰,为它增加计数特性。包装类并不实现具体功能,构造器里传入的就是实现具体功能的Set,可以是HaseSet或者自己实现的Set。

继承后构造方法的调用

  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,因此私有的静态成员类是最佳的选择。

  1. 如果一个嵌套类需要在单个方法之外可见,或者它太长了不适合放在方法内部,就使用成员类。
  2. 如果成员类的每个实例都需要一个指向外围实例的应用,就使用非静态成员类。否则就使用静态成员类。
  3. 如果嵌套类属于一个方法的内部,且你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就使用匿名类。否则就使用局部类。

泛型

列表优先于数组

数组时协变的、具体化的;泛型是不可变且可以被干掉的。数组提供了运行时的类型安全,但是没有编译时的类型安全。当混合使用数组和泛型时,编译时得到了错误和警告,应该立马用列表代替数组。

  1. 数组是协变的(covariant):
//编译时不报错,运行时报错ArrayStoreException
Object[] test = new Long[1];
test[0] = "test";

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

List<Object> test2 = new ArrayList<Long>(); //编译时报错
test2.add("123");
  1. 数组是具体化的(reified):
    数组在运行时才知道并检查他们的元素类型约束。泛型则是通过擦除(erasure)来实现的。泛型只在编译时强化类型信息,在运行时擦除元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意互用。

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

PECE producer-extends,consumer-siper
如果参数化类型表示生产者T,就使用<? extends T>,如果表示消费者T,就使用<? super 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常量

枚举本质上是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都是否定的,则应该使用注解。

Lambda和Stream

Lambda优先于匿名类

  • 尽量删除所有Lambda参数的类型,除非能让程序变得更加清晰。
  • Lambda没有名称和文档,若果一个计算本身不是自描述的,或者超出了几行,就不要把它们放在一个Lambda里面。
  • 尽量不要序列化一个Lambda(或者匿名类实例)。
  • 不要给函数对象使用匿名类,除非必须创建费函数式接口的类型实例。

方法引用优先于Lambda

只要方法引用更加简洁、清晰,就用方法引用;若果方法引用并不简洁,就坚持使用Lambda。

方法

检查参数的有效性

  • assert 对于有些参数,方法本身没有用到,却被保存起来供以后使用,可以使用断言检验这类参数的有效性。如果断言失败,则会抛AssertionError。

必要时进行保护性拷贝

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

  • 慎用clone。如果对于非final的成员,不能保证clone方法一定返回同样的类的对象,它有可能返回专门出于恶意目的而设计的不可信子类的实例,例如这样的子类可以在每个实例被创建时把指向该实例的引用记录到一个私有的静态列表中,并且允许攻击者访问这个列表,这将使得攻击者可以自由地控制所有的实例。为了阻止这种攻击,对于参数类型可以被不可信任方子类话的参数,请不要使用clone方法进行保护性拷贝。

  • 另外需要修改访问方法,返回可变内部域的保护性拷贝:

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

慎用重载

  • 类型还是父类,虽然调用父类方法指向子类引用。

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

慎用可变参数

  • 如果客户端调用这个方法时并没有传递参数进去,它就会在运行时而不是编译时失败。
//带两个参数,避免没有传参导致的问题
static init min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for(int arg : remainingArgs) {
        ...
    }
}
  • 在重视性能的情况下,使用可变参数要特别小型,可变参数方法的每次调用都会导致进行一次数组分配和初始化。可以使用多个重载方法,每个重载方法带有0至3个普通参数,当参数数目超过3个时,就使用可变参数方法。

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

通用编程

将局部变量的作用域最小化

  • 要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明。
  • 几乎每一个局部变量的声明都应该包含一个初始化表达式。
  • 使方法小而集中(每个操作用一个方法来完成)。

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

  • 有三种常见情况不能使用for-each循环:
    1.结构过滤--如果需要遍历集合,并删除特定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法;
    2.转换--若需要遍历列表或者数组,并取代它的部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值;
    3.平行迭代--若需要并行地遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量可以同步前进。

  • for-each循环在间接性、灵活性以及出错预防性方面占有绝对的优势,并且没有性能惩罚问题。

了解和使用类库

  • 选择随机数生成器时,大多使用ThreadLocalRandom。
  • 使用标准类库不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。
  • 标准类库性能会不断提高。
  • 随着时间推移,标准类库会增加新的功能。

如果需要精确的答案,请避免使用float和double

  • float和double不适合用于货币计算。
  • 数值范围没有超过9位十进制数字,可以用int;如果不超过18位数字,就可以使用long;如果数值可能超过18位数字,必须使用BigDecimal。

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

  • 对于装箱基本类型不能使用 == 操作符。(同一性比较)
  • 在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。(NULL拆箱会抛出空指针异常)
  • 在进行反射的方法调用时,必须使用装箱基本类型。

如果其他类型更合适,尽量避免使用字符串

字符串不适合代替其他的值类型、枚举类型、聚合类型、能力表。

了解字符串连接的性能

  • 为n个字符串而重复的使用字符串连接操作符(+),需要n的平方的时间。(字符串不可变,它们的内容都要被拷贝)
  • 不要使用字符串连接操作符来合并多个字符串,除非不考虑性能,应该使用StringBuilder的append方法。

通过接口引用对象

  • 如果有合适的接口类型存在,对于参数、返回值、变量和域来说,应使用接口类型进行声明。
  • 程序会更加灵活,可以更换实现。
  • 如果没有合适的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象。

接口优先于反射机制

  • 核心反射机制,java.lang.reflect包,提供了“通过程序来访问任意类”的能力。给定一个Class对象,可以获得Constructor、Method、Field实例,他们分别代表了该Class实例所代表的的类的构造器、方法和域。
  • 反射缺点:损失了编译时类型检查的优势、执行反射访问需要的代码冗余、性能有损失。
  • 尽量用反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。

谨慎使用本地方法

使用本地方法来提高性能的做法不值得提倡。

谨慎地进行优化

  • 不要为了性能牺牲合理的结构。
  • 避免做出限制性能的决策。
  • 考虑API设计决策的性能后果,不能为了获取好的性能对API进行包装。
  • 每次做优化前后,要对性能进行测量。

遵守普遍接受的命名规则

  • 包和模块的名称应该是层次状的。
  • 包名称的其余部分应该包括一个或多个描述该包的组成部分。
  • 类、接口、枚举、注解类型的名称应该包括一个或多个单词,每个单词的首字母大写。
  • 方法的名称第一个字母小写。

异常

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

//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. 在对象的一份临时拷贝上执行操作,操作完成之后再用临时拷贝中的结果代替对象的内容。

不要忽略异常

忽略一个异常非常容易,只需将方法调用通过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是原子的,但是增量操作符不是原子的,它首先读取值,然后写回一个新值。如果第二个线程在第一个线程读取旧值和返回新值期间读取这个域就会出错。

避免过度同步

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

通常,你应该在同步区域内做尽可能少的工作。如果你必须要执行某个很耗时的动作,应该设法把这个动作移到同步区域的外面。

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) {
        ...
    }
}

私有锁对象模式只能用在无条件的线程安全类上。有条件的线程安全类不能使用这种模式,因为它们必须在文档中说明:在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。

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

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

慎用延迟初始化

如果处于性能的考虑需要对静态域使用延迟初始化:

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或者线程优先级。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本能不能工作的程序。

序列化

其他方法优于Java序列化

谨慎地实现Serializable接口

  • 最大的代价,一旦一个类被发布,就大大降低了“改变这个类实现”的灵活性。
  • 第二个代价,增加了出现Bug和安全漏洞的可能性。
  • 第三个代价,随着类发行新的版本,相关的测试的负担也会增加。

如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或持久化,对于这个类来说,实现Serializable接口就非常有必要。
为了继承而设计的类应该尽可能的少去实现Serializable接口,用户的接口也应该尽可能少继承Serializable接口。
内部类不应该实现Serializable接口(静态成员类可以)。

考虑使用自定义的序列化形式

  • 尽量不要使用默认的序列化形式。
  • 如果一个对象的物理表示方法等同于他的逻辑内容,可能就适用于使用磨人的序列化形式。
  • 若使用默认的序列化形式,必须提供一个readObject方法保证约束关系和安全性。
  • 当一个对象的物理表示方法与他的逻辑数据内容有实质性的区别时,使用默认的序列化形式会有以下四个缺点:它使这个类导出的API永远束缚在该类的内部表述方法上、它会消耗过多的空间、消耗过多的时间、它会引起栈溢出。
  • 如果使用自定义的序列化形式,大多数实例域都应该被标记为transient。
  • 如果在读取整个对象状态的任何状态的其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。
  • 无论选择哪一种序列化形式,都要为可序列化的类声明一个显式的系列版本UID。
  • 不要修改序列版本UID,否则将会破坏类现有的已被序列化实例的兼容性。

保护性的编写readObject方法

当一个对象被反序列化的时候,对于客户端不该拥有的对象引用,如果哪一个域包含了这样的对象引用,就必须要做保护性拷贝。

对于实例的控制,枚举类型优于readResolve

  • 如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则必须声明为transient。
  • 如果做不到使用枚举类型来实施实例控制的约束条件,同时又需要一个既可以序列化又是受实例控制的类,就必须提供一个readResolver方法,并确保该类所有的实力域都为基本类型,或者是瞬时的。

考虑用序列化代理代替序列化实例

  • 序列化代理模式有两个局限性:它不能与可以被客户端拓展的类相兼容、不能与对象图中包含循环的某些类相兼容。
  • 当有需求在一个不能被客户端拓展的类上编写readObject或者writeObject方法时,就应该考虑使用序列化代理模式。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容