Java Idiom

2017/12/16 JavaSE

来自Effective Java第二版的Java编程习惯用法

创建和销毁对象

  • 何时以及如何创建对象
  • 何时以及如何避免对象
  • 如何确保它们能够适时销毁
  • 如何管理对象销毁之前必须进行的各种清理动作

静态工厂方法替代构造函数

静态工厂方法并不直接对应设计模式中的工厂方法模式

优势

  1. 名称
    • 名称可以清楚表达目的而不是使用不同参数的构造函数区分
      • BigInteger.probablePrime()// 返回可能是素数
  2. 避免创建不必要的重复对象
    • 重复使用不可变类或预先构造好的类,提升性能
      • Boolean.valueOf()
    • 实例受控,如不可变类和单例
  3. 可以返回子类对象
    • API可以返回对象,同时又不会使对象的类变成公有的,使API简洁
      • Collections中返回同步集合、不可修改集合
    • 可以随参数返回不同子类实例
  4. 创建参数化实例时更简洁
    • 泛型推断(Jdk1.7已支持)

缺点

  1. 类如果不含有公有的或者受保护的构造器,就不能被子类化
    • 如实现Collections Framework中类的子类
  2. API文档中没有明确的标注
    • 使用注释标注可以使用静态工厂,同时使用标准命名
      • valueOf–实际上是类型转换方法
      • of–valueOf的简明代替
      • getInstance–通过方法参数描述,返回共享实例
      • newInstance–返回不同实例
      • getT–和getInstance一样,T表示返回的对象类型
      • newT–和newInstance一样,T表示返回的对象类型

Tips:静态工厂通常更加合适

遇到多个构造器参数时要考虑使用构造器(建造者模式)

静态工厂和构造器有共同局限性:不能很好的扩展到大量的可选在参数
重叠构造器(多个参数不同的构造)虽然可行,但是参数多的时候客户端很难编写且不易阅读
使用JavaBeans模式(新建实例后通过set方法设值)(可读性好),这种方式使创建的实例不一致,不可控,可能导致线程问题

优势

  1. 可选参数可以通过setter方法来控制,而必选参数可使用builder的构造函数控制
  2. 建造者模式与反射相比,后者破坏了编译时期检查(如没有对应构造函数和构造函数异常处理等)
  3. 代码易于阅读和编写

缺点

  1. 增加了需要先创建Builder的性能消耗
  2. 代码会比重叠构造器冗长

注意事项:如果违反了约束条件,build方法应该抛出IllegalStateException,而在这之前,setter抛出IllegalArgumentException是个好做法
注意事项:如果参数较多,优先使用建造者模式是一个好选择,因为后期添加的builder和先前的构造器还有静态工厂很不协调

用私有构造器或者枚举强化Singleton属性(单例模式)

私有构造函数方式可以修改构造函数在被要求创建第二个实例的时候抛出异常抵御反射攻击
使用transient和readResolve方法保证反序列化后使用一个实例
返回实例的工厂函数很灵活,如每个线程返回一个实例

Tips:单元素的枚举已经成为实现单例的最好方法,可以防止序列化和反射攻击

通过私有构造强化不可实例化的能力

使用私有构造器防止编译器默认生产公有无参构造,并增加注释

注意事项:不能使用抽象防止实例化,因为子类可以实例化

避免创建不必要的对象

重用不可变对象,如不新建String的实例,使用工厂函数而不是构造,如Boolean.valueOf(“true”)代替new Boolean(“true”)
使用静态初始化一个变量而不是每次访问都初始化,如Calendar对象和日期对象
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱,如在for中使用Long sum += i运算

注意事项:对象池只适用于昂贵的创建代价的资源,如数据库连接池,否则垃圾回收机制性能很容易就会超过轻量级对象池的性能
注意事项:在提倡保护性拷贝的时候,重用对象的代价要远远大于创建对象付出的代价
注意事项:必要时如果没能实施保护性拷贝,将会导致程序错误和安全漏洞
注意事项:不必要的创建对象智慧影响程序的风格和性能

消除过期对象的引用

内存泄漏常见来源

  1. 只要类是自己管理内存,程序员就应该警惕内存泄漏问题
  2. 内存泄漏的另一个常见来源是缓存
    • 使用WeakHashMap(缓存外存在对某个的引用,该项才有意义)
    • 使用Timer、Schedule或者添加时清理,LinkedHashMap的removeEldestEntry也可以实现
    • 使用java.lang.ref
  3. 第三个常见来源是监听器和其他回调
    • 显示取消注册
    • 使用WeakHashMap保存它们的弱引用
// 原因1例子
public Object pop(){
    if(size==0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size]=null;// 解除引用
    return result;
}

注意事项:清空对象引用是一种例外,而不是一种规范行为

避免使用终结方法(finalizer)

使用显示的终止方法(如close方法),要求类的客户端在每个实例不适用的时候调用这个方法,显示的终结方法必须在一个私有成员中记录下是否有效,无效状态再次调用抛出IllegalStateException
Java中采用try-finally来配合完成非内存资源的回收

问题

  1. finalize方法不能保证被及时执行
  2. finalize方法不能保证被执行
  3. 不应该依赖终结方法来更新重要的持久状态
  4. 未捕获异常在终结过程中被抛出来,这种异常可能被忽略,终结过程也会终止
  5. 终结方法有一个非常严重的性能损失

finalizer用途

为客户端忘记调用显示终结方法增加一层保障,如FileInputStream的finalize方法
协助释放native方法的非关键资源

注意事项:终结方法需要使用try-finally调用父类的终结方法
注意事项:为了防止父类终结方法忘记被调用,采用匿名内部类(终结方法守卫者finalizer guardian)释放外围实例

public class Foo {
    private final Object finalizerGuardian = new Object() {
        @Override
        protected void finalize() throws Throwable {
            close();
        }
    };
    // 注意这里不是调用finalize方法,所以finalize方法是否被子类调用没有实际意义
    public void close() {
        //do finalize
    }
}

对象的通用方法

遵循通过约定(general contract)的方法:

  • equals
  • hashCode
  • toString
  • clone
  • finalize(见避免使用终结方法部分)
  • compareTo(非Object的方法,在本章介绍)

覆盖equals方法请遵循通用约定

何时覆盖equals

如果类具有自己特有的“逻辑相等”概念(不同于对象相等的概念),且超类还没有覆盖equals方法以实现期望行为时,这也使得映射或者集合表现出预期的行为

注意事项:值类在保证只存在一个对象时(如枚举),逻辑相同等同于对象等同

Object规范的equals约定

  • 自反性(reflexive)
    • 对于任何非null值的引用x,x.equals(x)必须返回true
    • contains方法要求遵循这个原则
  • 对称性(symmetric)
    • 对于任何非null值的引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)也返回true
  • 传递性(transitive)
    • 对于任何非null值的引用x、y和z,当y.equals(x)返回true时,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true
  • 一致性(consistent)
    • 对于任何非null值的引用x和y,多次比较操作在对象中的所用信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false
  • 非空性(Non-nullity)
    • 对于任何非null值的引用x,x.equals(null)必须返回false

注意事项:在非抽象类的子类中覆写或继承equals方法时,很容易破环对称性和传递性,可以使用组合代替继承并提供一个返回被组合对象方法的方式,避免这个问题,而抽象的父类不能被实例化,所以不存在这样的问题
注意事项:java.sql.Timestams对java.util.Data类进行了扩展,其equals方法违反了对称性,所以不能混用
注意事项:java.net.URL的equals方法依赖于对URL中主机IP地址的比较,获得IP需要访问网络,随着时间的推移,不确保会产生相同的结果,违反了一致性

高质量equals方法实现

  1. 使用==检查参数是否为这个对象的引用
    • 如果是,返回true。
    • 如果比较操作性能昂贵,这是一个值得做的性能优化
  2. 使用instanceof操作符检查参数是否为正确类型
    • 如果不是,返回false。
    • 如果类实现的接口改进了equals约定,允许实现类之间比较,如集合接口
    • instanceof操作符在第一个操作数为null时,都会返回false,所以不需要单独的null检查
  3. 把参数转化成正确的类型
  4. 对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配
    • 如果全部测试成功,返回true,否则返回false
    • 对于非float和double类型的基本类型,使用==比较
    • float类型使用Float.compare(),double使用Double.compare()
      • 由于存在Float.NaN,-0.0f及类似的double常量,特殊处理是有必要的,参看Float.equals文档
        • If f1 and f2 both represent Float.NaN, then the equals method returns true, even though Float.NaN==Float.NaN has the value false.
        • If f1 represents +0.0f while f2 represents -0.0f, or vice versa, the equal test has the value false, even though 0.0f==-0.0f has the value true.
    • 对于数组元素,使用Arrays.equals()
    • 对于允许null值的对象
      • (field==null?o.field==null:field.equals(o.field))
    • 对于通常不相等的对象引用的判断,递归调用equals
    • 对于通常是相等的对象引用的判断
      • (field==o.field||(field!=null)&&field.equals(o.field))
  5. 问自己三个问题,是否对称,传递且一致,并测试

注意事项:域的比较顺序可能会影响到equals方法的性能,应该先比较最有可能不一致的域或开销最低的域,最理想的情况是都满足的域先比较
注意事项:覆盖euqals时总要覆盖hashCode
注意事项:不要将equals声明中的Object对象换为其他类型,因为那就不是重写

覆盖equals使总要覆盖hashCode

Object规范的hashCode约定

  • 程序执行其间,equals用到的信息没有被修改,多次调用hashCode应该返回同一个整数,程序的多次执行过程中,返回的整数可以不一致
  • 两个对象equals方法相等,hashCode必须产生相同的结果
  • 两个对象equals方法不相等,hashCode可以产生相同的结果,程序员应该为不相等的对象产生不同的散列码
    • 产生不同的结果有可能提高散列表(hash table)的性能
    • 如果相同,对象会被映射到同一个散列桶中,是散列表退化为链表,使得本该线性时间运行的程序变成了以平方时间在运行

Tips:如果计算hashCode很麻烦,可以使用懒加载加缓存的形式存储hashCode
注意事项:不要试图从计算中去除一个对象的关键部分来提高性能
Tips:新版本的Jdk中提供了Objects.equals()和Objects.hash(id)帮助开发者实现equals和hashCode

始终要覆盖toString

Object规范的toString约定

  • 被返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”
  • 建议所有子类都应该覆盖这个方法

toString方法应该返回对象中包含的所有值得关注的信息

是否在文档中指定返回格式

  • 指定格式
    • 优点:可以作为一种标准的、明确的、是何人阅读的对象表示法,这种表示法也可以用于输入和输出,以及用在永久的适合人阅读的数据对象中
      • 指定格式最好提供一个相匹配的静态工厂或者构造器,以便在对象和它的字符串表示法之间来回转换,如绝大多数包装类型
    • 缺点:指定格式后一旦被广泛使用,就必须始终如一的坚持这种格式,否则会破坏相关代码和数据
    • 不指定格式
      • 优点:保留灵活性,以便以后改进格式

注意事项:无论是否指定格式,都应在文档中明确的标明意图
注意事项:无论是否指定格式,都应为toString中的信息提供一个编程式的访问路径,而不是从字符串中解析而引发潜在错误

谨慎的覆盖clone

Object规范的clone约定

  • 创建和返回对象的一个拷贝
  • x.clone()!=x
  • x.clone().getClass()==x.getClass()
  • x.clone().equals(x)

Cloneable接口的作用

如果一个类实现了Cloneable,对象的clone方法就返回该对象的逐域拷贝,或者抛出CloneNotSupportedException

clone方法实现

所有实现了Cloneable接口的类,都应该用一个公有的方法覆盖clone,此公有方法首先调用super.clone,然后修正任何需要修正的域

注意事项:clone架构与引用可变对象的final域的正常用法是不相兼容的,为了正常使用clone,需要删除final修饰符
注意事项:调用方法重新构造一个对象没有直接操作对象及其克隆对象的内部状态clone方法快

另一个实现对象拷贝的好方法是提供一个拷贝构造器或拷贝工厂

  • 拷贝构造器
    • public Yum(Yum yum)
  • 拷贝工厂
    • public static Yum newInstance(Yum yum)
  • 优点
    • 不依赖于语言之外的创建机制
    • 不要求遵守尚未制定好文档的规范
    • 不会与final域的正常使用冲突
    • 不会抛出不必要的受检异常
    • 不需要进行类型转换

注意事项:拷贝构造器和拷贝工厂也需要对对象进行深拷贝

考虑实现Comparable接口

compareTo方法的约定

将这个对象与指定的对象进行比较,当对象小于、等于、大于指定对象的时候,分别返回一个负数、零或正整数,如果由于指定对象的类型无法与该对象进行不交,则抛出ClassCastException

  • x.compareTo(y)==-y.compareTo(x)
    • 如果第一个对象小于第二个对象,则第二个对象一定大于第一个对象
    • 如果第一个对象等于第二个对象,则第二个对象一定等于第一个对象
    • 如果第一个对象大于第二个对象,则第二个对象一定小于第一个对象
  • 传递性,(x.compareTo(y)>0&&y.compareTo(z)>0),则x.compareTo(z)>0
    • 如果一个对象大于第二个对象,并且第二个对象大于第三个对象,则第一个对象一定大于第三个对象
  • x.compareTo(y)==0,则所有的z,x.compareTo(z)==y.compareTo(z)
    • 比较时被认为相等的对象,它们跟别的对象比较时一定会产生相同的结果
  • 强烈建议x.compareTo(y)==0,x.equals(y),但不强制,如果违反应该给出说明

注意事项:同equals方法,如果继承体系和compareTo一起使用会造成问题,可以使用组合避免问题,并提供一个方法返回被组合对象
注意事项:BigDecimal的compareTo和equals不一致(objects equal only if they are equal in value and scale),这使得hash结构和tree结构会产生不同的结果
注意事项:比较整数型基本类型的域,使用关系操作符<>,浮点域使用Double.compare和Float.compare
注意事项:compareTo方法应该从相对关键的域开始比较,如果产生非0结果,就返回
注意事项:比较时可以直接进行运算,返回运算结果而不是判断大小,这样可以直接得出大于还是小于,但是要考虑值溢出的问题

类和接口

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

封装

设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后模块之间只通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况
它可以有效的接触组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试、优化、使用、理解和修改

规则

  • 尽可能的使每个类或者成员不被外界访问

Tips:如果一个包级私有的顶层类(或接口)只是在某一个类的内部被用到,就应该考虑使它成为位移使用它的那个类的私有嵌套类
Tips:如果类或者接口能够被做成包级私有的,他就应该被做成包级私有。通过把类和接口做成包级私有,它实际上成了这个包的实现的一部分,而不是包导出的API的一部分
Tips:访问级别从包级私有变成保护级别时,会大大增强可访问性受保护成员是类的导出的API的一部分。受保护的成员应该尽量少用
注意事项:除了的公有静态final域,公有类都不应该包含公有域,并且要确保公有静态final域所引用的对象是不可变的

在公有类中使用访问方法而非公有域

如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性
公有类永远不应该暴漏可变的域,虽然还是有问题,但是让公有类暴漏不可变的域其危害比较小
但是,有时候会需要用包级私有的或者私有的嵌套类来暴漏域,无论这个类是可变的还是不可变的,作用范围被限制在包范围和外围嵌套类范围

使可变性更小化(不可变对象)

优点

  • 不可变类比可变类更加易于设计、实现和使用

缺点

  • 对于每个不同的值都需要一个单独的对象
    • 可以预测哪些多步骤操作,将它们作为基本类型提供。如BigInteger及其配套加速如摸指数运算的类
    • 如果无法预测,提供一个公有的可变配套类。如String和StringBuilder

Java平台中的不可变类

String、基本类型包装类、BigInteger、BigDecimal等

不可变类的五条原则

  1. 不提供任何会修改对象状态的方法
  2. 保证类不会被扩展(禁止子类化)
    • 可以使用final声明类
    • 使用包级或者私有的构造器加工厂函数实现,更灵活
      • 可以有多个实现类
      • 可以改进工厂方法增强性能
  3. 使所有的域都是final的(或者没有一个方法能够对对象的状态产生外部可见的改变)
  4. 使所有的域都成为私有的
    • 保留内部表示法,使用set方法获取
  5. 确保对于任何可变组件的互斥访问
    • 保证客户端无法获得可变对象引用
    • 不适用客户端引用初始化这些域
    • 在访问方法中返回该对象引用
    • 在构造器、访问方法和readObject方法中使用保护性拷贝

不可变对象只有一种状态,它被创建时的状态
不可变对象本质上是线程安全的,它们不要求同步

Tips:频繁用到的不可变常量使用静态常量或工厂函数获得
注意事项:不可变类不应该提供clone方法或者拷贝构造器
注意事项:不可变类如果实现Serializable,必须提供显示的readObject和readResolve

技巧

  • 坚决不要为每个get方法编写一个相应的set方法,除非有很好的理由要让类成为可变的类,否则就应该是不可变的
  • 如果类不能被做成是不可变的,仍然应该尽可能的限制他的可变性,除非有充足的理由使域变成非final的,否则就应该是final的
  • 构造器应该创建完全初始化的对象,并建立起所有的约束关系,并且不提供公有构造和工厂外的公有初始化方法,除非必须这么做。也不应该提供重新初始化方法,因为复杂度和性能不成正比

复合优先于继承(装饰模式)

继承的功能非常强大,但是也存在诸多问题,因为它违背了封装原则。
只有当子类和超类确实存在子类型关系时,使用继承才恰当。即便如如此,子类和超类处在不同的包中,并且超类并不是为了继承设计的,那么继承就将会导致脆弱性
为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候
包装类不仅比子类更健壮,功能也更强大

注意事项:复合不适用于回调框架,因为被包装类不知道包装类的存在,在传递自身对象用于回调时会忽略掉包装对象

JDK中的反例:

  • Stack和Vector
  • Properties和Hashtable

要么为继承而设计,并提供文档说明,要么就禁止继承

好的API文档应该描述一个给定的方法做了什么工作,而不是描述他是如何做到的
被继承的类的文档会超出上面描述的范围,必须描述清楚那些有可能未定义的实现细节

为继承而设计,并提供文档说明

  • 该类的文档必须精确的描述覆盖每个方法所带来的影响(该类必须有文档说明它可覆盖的方法的自用性)
  • 类必须通过某种形式提供适当的钩子,以便能够进入它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域
  • 对于为了继承而设计的类,唯一的测试方法就是编写子类,必须在发布类之前先编写子类对类进行测试,发布后修改很困难

注意事项:构造器绝不能调用可被覆盖的方法,如果被覆盖的方法依赖子类构造,将不会在调用前被正确的初始化;clone方法和readObject方法存在类似问题
注意事项:在为了继承而设计的时候,Cloneable和Serializable接口出现了特殊的困难,无论实现哪个,都不是个好主意
注意事项:如果Serializable接口,那么如果有readResolve和writeReplace方法,它们应该是受保护的而不是私有的

禁止继承

对于那些并非为了安全的进行子类化而设计和编写文档的类,要禁止子类化

  • 声明类为final
  • 构造器私有或者包级私有,增加一些公有静态工厂

用于继承的普通类

  • 消除类中可覆盖方法的自用特性
    • 建立一个辅助方法,封装父类方法的可覆盖方法实现,父类可覆盖方法的引用改为辅助方法引用

接口优于抽象类

接口优势

  • 现有的类可以很容易被更新,以实现新的接口
  • 接口是定义混合类型(mixin类型,除了它的基本类型)的理想选择
  • 接口允许我们构造非层次结构的类型框架

抽象类优势

  • 抽象类的演变比接口的演变要容易的多,可以在以后的发行版本中增加具体方法减少破坏子类

接口和抽象类结合

通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的有点结合起来

优势

骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不加强“抽象类被用作类型定义时”所特有的严格限制
对于大多数的实现来讲,扩展骨架实现类是个很显然的选择,但并不是必须的
实现了接口的类把接口方法调用转发到内部的骨架实现类上,称作模拟多重继承。具有多重继承的大多数有点,又避免了响应缺陷

接口只用于类型定义

实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的

常量接口是对接口的不良使用

如果客户端实现了这个接口,对用户来说并没有市集价值,但却绑定了发行承诺,子类也被常量污染命名空间

如JDK中java.io.ObjectStreamConstants

替代

使用枚举类型或者不可实例化的工具类导出常量

类层次优于标签类

标签类

标签类充斥着样板代码,包括枚举声明、标签域以及条件语句,破坏了可读性
标签类是类层次的一种简单的仿效

类层次

结构清楚,反应了本质上的层次关系,增强了灵活性
不受不相关标签域影响
便于扩展
杜绝了条件语句
提供了编译时期检查

用函数对象表示策略(策略模式)

函数对象

对象的执行执行其他对象(这些对象被显示传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被成为函数对象,如Comparator实例

注意事项:匿名函数对象会在每次执行创建一个实例,当它只被用到一次时,考虑将对象存储到一个私有的静态final域里并重用它,并取有意义名称

优先考虑静态成员类

嵌套类

嵌套类存在的目的应该只是为了它的外围类提供服务
如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类

分类

  • 静态成员
    • 作为公有辅助类,仅当与它的外部类一起使用时才有意义
    • 作为外围类代表对象的组件
  • 非静态成员
    • 定义一个Adapter,它允许外部类的实例被看作是另一个不相关的实例,如集合的Interator实现
  • 匿名类
    • 动态的创建函数对象
    • 创建过程对象,如Runable、Thread、TimmerTask实例等
    • 静态工厂方法的内部
  • 局部类

注意事项:如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用

原则

  1. 如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,就应该使用成员类
  2. 如果成员类的每个实例都需要外围类实例的引用,就要把成员类做成非静态的,否则做成静态的
  3. 如果类在方法内部且只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特性,就要把他做成匿名类,否则就做成局部类

泛型

请不要在新代码中使用原生态类型(能使用泛型的地方使用泛型)

如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势

消除非受检警告

要尽可能地消除每一个非受检警告
如果无法消除,同事可以证明引起警告的代码是类型安全的,才可以使用@SuppressWarnings(“unchecked”)禁止警告,并增加注释

注意事项:应该尽可能的限制SuppressWarnings的范围,即使需要提取一个变量,也是值得的

列表优于数组

数组与泛型不同点

  • 数组是协变的,泛型是不可变的,是严格的类型要求
  • 数组是具体化的,而泛型存在泛型擦除

注意事项:一般来说,数组和泛型不能很好的混合使用,如果遇到了相关的编译时警告或错误,使用列表代替数组

优先考虑泛型 优先考虑泛型方法

使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易
静态工具方法尤其适用于泛型化

Tips:在没有类型推倒或创建不可变但又适合许多不同类型对象时,使用泛型单例工厂模式(安全的强制转换成泛型类型),如Collections.emptySet
Tips:通过某个包含类型参数本身的表达式来限制类型参数是允许的,这就是递归类型限制<T extends Comparable<T>>

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

为了获得最大限度的灵活性,要在表示生产者和消费者的输入参数上使用通配符类型(PECS:prodcutor-extends,consumer-super)
如果输入参数既是生产者,又是消费者,需要严格的类型匹配

注意事项:通配符类型对用户来说应该是透明的,如果类的用户必须考虑通配符类型,类的API或许就会出错
Tips:所有的Comparator和Comparable都是消费者 Tips:如果类型参数只在声明中出现一次,就可以用通配符取代它,如果是无限制的类型参数,就用无限制的通配符取代,如果是有限制的类型参数,就用有限制的通配符取代

优先考虑类型安全的异构容器

类Class在JDK1.5中可以被泛型化。如,String.class属于Class<String>类型,Integer.class属于Class<Integer>类型
Class<String> stringClass = String.class;

类型安全的异构容器

集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数,你可以通过将类型参数放在键上而不是容器上来避开这一限制
对于这种类型安全的异构容器,可以用Class作为键,以这种方式使用的Class对象称作类型令牌
也可是使用定制的键类型,如Column<T>作为键

  • 类型安全
  • 异构(键是不同类型)

局限性

  • 客户端可以使用原始类型的Class对象,破环类型安全,但是编译会有未受检警告
    • 我们用下方的cast方法检验,避免这样的问题
    • java.util.Collections中的checkedSet、checkedList、checkedMap都使用了类似的技巧
  • 不能用于不可具体化的类型中,如无法区分List<String>List<Integer>
// 类型安全的异构容器

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();
    public <T> void putFavorite(Class<T> type, T instance) {
        // cast方法是Java的cast操作符的动态模拟,检验是否为Class对象所表示的类型的实例
        favorites.put(type, type.cast(instance));
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

Tips:类Class提供了一个安全的进行转换子类实例的方法asSubclass,避开转换的编译时警告

AnnotatedElement annotatedElement = MyClass.class;
Class<?> type = Class.forName("com.xpress.annotation.MyAnnotation");
Annotation annotation = annotatedElement.getAnnotation(type.asSubclass(Annotation.class));

枚举和注解

用枚举代替int常量

  • int枚举模式
    • int枚举是编译时常量,被编译到使用它们的客户端中,如果枚举常量关联的int发生了变化,客户端就必须重新编译
    • 无法翻译成可打印的字符串
    • 无法便利和获取枚举组大小
  • String枚举模式
    • 依赖字符串的比较,性能问题
    • 导致硬编码的字符串常量在客户端中出现
  • 枚举
    • 本质上是int值,是单例(单元素的枚举)的泛型化
    • 通过公有的静态final域为每个常量导出类的实例
    • 由于没有可访问的构造器,枚举类型是真正的final
    • 枚举类型是受控的,无法新建和扩展实例
    • 提供了编译时类型安全
    • 常量并没有被编译到客户端代码中,而是在int枚举模式中
    • toString转化成可打印的字符串
      • 返回声明名称
      • 可以覆盖toString修改
    • 允许添加域、方法和实现接口
    • 提供了Object方法的高级实现,实现了Comparable和Serializable接口,优化了序列化方式

枚举

每当需要一个常量时使用枚举

  • 为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
  • 枚举有一个values方法,按照声明顺序返回它的值数组
  • 在枚举中使用抽象方法将每个枚举与不同行为关联起来(特定于常量的方法实现)
  • 枚举自动产生valueOf方法,将常量的名字转变成常量本身
    • 如果覆盖了toString,要考虑编写一个一个fromString方法,将定制的字符串表示法变成相应的枚举
  • 计算移到一个私有的嵌套枚举中,将这个策略枚举的实例穿到枚举实例中,切换多个枚举共享的行为
// fromString方法
enum MyEnum {
    TYPE1("a"),
    TYPE2("b"),
    TYPE3("c");
    private static final Map<String, MyEnum> types = new HashMap<>();
    private final String type;
    static {
        types.putAll(
                Arrays.stream(values())
                        .collect(
                                Collectors.toMap(
                                            e -> e.type,
                                            e -> e
                                        )
                        )
        );
    }
    MyEnum(String type) {
        this.type = type;
    }
    @Override
    public String toString() {
        return type;
    }
    public static MyEnum fromString(String type) {
        return types.get(type);
    }
}
// 嵌套枚举
enum MyEnum {
    TYPE1(Operation.PLUS),
    TYPE2(Operation.PLUS),
    TYPE3(Operation.SUB);
    private final Operation operation;
    MyEnum(Operation operation) {
        this.operation = operation;
    }
    public String getOperator() {
        return operation.getOperator();
    }
    // 嵌套枚举
    enum Operation {
        PLUS {
            @Override
            String getOperator() {
                return "+";
            }
        },
        SUB {
            @Override
            String getOperator() {
                return "-";
            }
        };
        abstract String getOperator();
    }
}

注意事项:如果枚举具有普遍性,它就应该成为一个顶层类;如果是被用在一个特定的顶层类中,他就应该成为该顶层类的成员类
注意事项:枚举天生是不可变的,所以所有所有的域都应该为final的
注意事项:枚举会优先使用comparable而非int常量
注意事项:与int常量比,枚举装载和初始化时会有空间和时间的成本

用实例域代替序数

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

大多数程序员都不要使用这个而方法,他是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构

注意事项:永远不要根据枚举的序数导出与它关联的值,而是要将它保存到一个实例域中

用EnumSet代替位域

  • 位域允许利用位操作,有效地执行像union和交集这样的集合操作,但是位域有着int枚举常量的所有缺点,甚至更多
  • EnumSet集位域的简洁和性能优势于一身,还有枚举类型的所有优势
// 位域
private static final int STYLE_BOLD = 1<<0;// 00000001
private static final int STYLE_ITALIC = 1<<1;// 0000010
text.applyStyles(STYLE_BOLD|STYLE_ITALIC);//00000011

EnumSet<MyEnum> myEnums = EnumSet.of(MyEnum.TYPE1, MyEnum.TYPE2);

Tips:底层枚举类型有64个或更少的元素,整个EnumSet就用单个long表示,removeAll和retailAll方法多事利用位算法来实现的

用EnumMap代替序数索引

EnumMap更简短,更清楚,更安全,运行速度方面可以与使用序数的程序相媲美

  • 避免了数组的不安全转换
  • 输出会自动转化成可打印的字符串
  • 计算索引时不会出错
  • 内部使用数组,性能可观
 enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
        private final Phase src;
        private final Phase dist;
        Transition(Phase src, Phase dist) {
            this.src = src;
            this.dist = dist;
        }
        private static final Map<Phase, EnumMap<Phase, Transition>> map = new EnumMap<>(Phase.class);
        static {
            // init map
            for (Phase phase : Phase.values()) {
                map.put(phase, new EnumMap<>(Phase.class));
            }
            // init data
            for (Transition transition : Transition.values()) {
                map.get(transition.src).put(transition.dist, transition);
            }
        }
        public Transition getTransition(Phase src, Phase dist) {
            return map.get(src).get(dist);
        }
    }
}

Tips:使用EnumMap<...,EnumMap<...>>处理多维关联

用接口模拟可伸缩的枚举

虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。

注意事项:这种方式无法实现从一个枚举类型继承到另一个枚举类型。如果代码非常少,可以复制;否则使用辅助类或者静态辅助方法中避免代码重复

注解优先于命名模式

命名模式

使用命名规范标明程序元素需要通过某种工具或者框架进行特殊处理

缺点

  • 文字拼写错误会导致失败,且没有任何提示
  • 无法确保它们只用于相应的程序元素上
  • 没有提供将参数值与程序元素关联起来的好办法

注解

注解永远不会改变被注释代码的语义,但是使它可以通过工具进行特殊的处理

Tips:注解中数据参数的语法十分灵活,他是进行过优化的单元素数组

坚持使用Override注解

应该在你想要覆盖超类声明的每个方法声明中使用Override注解
IDE会产生警告在你没有注解的覆盖方法上,防止无意识的覆盖

用标记接口定义类型

标记接口与标记注解

  • 标记接口
    • 标记接口指定类型,允许在编译时捕捉注解要在运行时才能捕捉到的错误
    • 可以被更佳精确的进行锁定。标记接口拓展适用的特殊接口
  • 标记注解
    • 方便的在被注解的类型上增加更多的注解信息,而标记接口通常不可能轻易增加方法
    • 它们更符合注解相关框架,是更大注解机制的一部分

选择

  • 如果要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择
  • 如果想要标记程序元素而非类和接口,考虑未来可能要给标记添加更多的信息,或者标记要适用于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择

方法

检查参数的有效性

每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显示的检查来实施这些限制
一个例外情况是, 检查工作非常昂贵,或者根本是不切实际的,并且检查已经包含在计算过程中,如Comparable检查

Tips:计算过程中抛出的异常与文档描述不符,应该使用异常转译转换成正确的异常

必要时进行保护性拷贝

如果类具有从客户端得到或者返回可变组件,类就必须保护性的拷贝这些组件
如果拷贝的成本收到限制,并且类信任它的客户端不会不恰当的修改组件,就可以在文档中指明客户端的职责是不得修改收到影响的组件,以此来替代保护性拷贝

注意事项:保护性拷贝实在检查参数的有效性之前进行的,并且有效性检查是针对拷贝后的对象,这样做可以避免“危险阶段”(从检查参数到拷贝参数之间的时间段)(TOCTOU攻击)从另一个线程改变类的参数
注意事项:对于参数类型可以不被信任方子类化的参数,请不要使用clone方法进行保护性拷贝
Tips:访问方法可以使用clone方法拷贝,因为成员在类内部可控。但最好使用拷贝构造器或拷贝工厂
Tips:长度非0的数组总是可变的
Tips:使用Date.getTime()的long值用作时间表示法,因为Date对象是可变的

public final class Period {
    private final Date start;
    private final 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 + "after" + end);
        }
    }
    public Date getStart() {
        return new Date(start.getTime());
    }
    public Date getEnd() {
        return new Date(end.getTime());
    }
}

谨慎设计方法签名

  • 谨慎地选择方法名称
    • 首先目标应该是易于理解的,并且与同一个包中的其他名称风格一直
    • 选择与大众认可的名称相一致的名称
  • 不要过于追求提供便利的方法
    • 每个方法都应该尽其所能。方法太多会使类难以学习、使用、文档化、测试和维护
    • 只有当一项操作被经常用到的时候,才考虑为它提供快捷方式。如果不确定,还是不提供为好
  • 避免过长的参数列表
    • 目标是四个参数,或者更少
    • 缩短过长的参数列表
      • 分解成多个方法,每个方法只需要这些参数的一个子集
        • 通过方法的正交性,避免方法过多
      • 创建辅助类,用来保存参数的分组
      • 使用Builder模式
        • 在执行excute方法时,对参数的合法性进行检查

Tips:对于参数类型,优先使用接口而不是实现类
Tips:对于boolean参数,优先使用两个元素的枚举,它可以方便的扩展第三个值,并且可以得到枚举类型的许多好处

慎用重载

  • 重载方法的选择是静态的
  • 覆盖方法的选择是动态的

安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法

至少应该避免同一组参数只需要通过类型转转就可以呗传递给不同的重载方法
如果不能,就应该保证传递同样的参数时,所有的重载方法行为一致,可以让更具体化的重载方法把调用转发给更一般化的重载方法

如果方法使用可变参数,保守策略是根部不要重载它
这项限制不麻烦,因为你始终可以给方法起不同的名字,而不使用重载机制
对于构造方法,可以选择导出静态工厂,而不是构造器

注意事项:自动装箱和泛型成了Java语言的一部分之后,谨慎重载显得更加重要了,如List的remove(int)方法

慎用可变参数

可变参数方法接受0个或者多个指定类型的参数,将参数转变成数组后传递给方法

注意事项:可变参数方法每次调用都导致进行一次数组分配和初始化,如果大多数调用都使用少于三个参数的方法,那么就重载它,当参数大于三个时才使用可变参数
Tips:使用Arrays.toString()方式打印数组,避免可变参数带来的Arrays.asList()问题

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

返回类型为数组或集合的方法没理由返回null,应该返回一个零长度的数组或者集合

两点原因不用担心性能问题

  • 在这个级别担心性能问题是不明智的,除非分析标明这个方法正是造成性能问题的真正源头
  • 对于不返回任何元素的调用,每次返回同一个零长度的数组是有可能的,因为零长度的数组是不可变的,而不可变对象有可能被自由的共享

习惯用法

private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
private final List<Object> objectList = new ArrayList<>();
public Object[] getObjects() {
    return objectList.toArray(EMPTY_OBJECT_ARRAY);
}

零长度的数组常量被传递给toArray方法,以指明期望的返回类型, 正常情况下,toArray方法分配了返回的数组,但是如果集合是空的,它将使用零长度的输入数组
Collection.toArray(T[])的规范保证:如果输入数组大到足够容纳这个集合,他讲返回这个输入数组。因此,这种做法永远也不会分配零长度的数组

Tips:集合方法返回Collections.emptySet、emptyList、emptyMap
Tips:StringUtils.EMPTY、StringUtils.SPACE、StringUtils.INDEX_NOT_FOUND等可用作替代对应常量

为所有导出的API元素编写文档注释

为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法或域声明之前增加一个文档注释
如果文档是可序列化的,也应该对它的序列化形式编写文档
文档注释也应该描述类或者方法的线程安全性
方法的文档注释应该简洁地描述出它和客户端之间的约定,这个约定应该说明这个方法做了什么; 还应该列举方法的前置条件和后置条件; 方法还应该在文档中描述方法的副作用,即系统状态中可观察到的变化

  • {@literal}可以对文档中的html字符进行转义
  • {@code}除了转义外还会被用代码字体进行呈现
  • {@inheritDoc}从超类中继承文档注释中的部分内容

Tips:文档注释的第一句话,成了该注释所属元素的概要描述
Tips:为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数
Tips:为枚举类型编写文档时,要确保文档中说明常量
Tips:为注解类型编写文档时,要确保在文档中说明所有成员以及类型本身

通用程序设计

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

  • 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明
  • 几乎每个局部变量的声明都应该包含一个初始化表达式,如果不能,就推迟声明;try-catch块是一个特殊情况
  • 循环中提供了特殊机会来将变量的作用域最小化,循环变量的作用域正好在需要的范围内。如果在再循环终之后不在需要循环变量的内容,for循环就优先于while循环
  • 使方法小而集中,将方法分成多个,可以避免局部变量作用域混合在不同的操作范围内
// 遍历集合的首选方法
for (Element e : c) {
    doSomething(e);
}
// n的作用域被限制在最小范围
for (int i = 0, n = length(); i < n; i++) {
    doSomething(i);
}

foreach循环优先于传统的for循环

foreach循环在间接性和预防bug方面悠着传统的for循环无法比拟的优势,并且没有性能损失

三种情况不适合foreach循环

  • 过滤
    • 便利并删除集合元素,需要使用remove方法
  • 转换
    • 便利列表或者数组,取代元素,需要使用索引
  • 平行迭代
    • 并行地便利多个集合,要显示控制迭代器或者索引变量,使它们同步前移

Tips:如果你编写的类型表示一组元素,即使你不选择实现Collection,也要让它实现Iterable,这样允许用户利用foreach循环遍历你的类型

了解和使用类库

优势

  • 通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验
  • 不必浪费时间为那些工作不太相关的问题提供特别的解决方案
  • 它们的性能往往会随着时间的推移而不断提高,无需你做任何努力
  • 可以是自己的代码融入主流,这样的代码更容易读、更容易维护、更容易被大多数的开发人员重用

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

float和double没有提供完全精确的结果,尤其不适合与货币计算
解决这个问题的正确办法是使用BigDecimal、int或者long进行货币运算

  • BigDecimal运算结果精确,允许控制舍入。但是与基本类型比,很不方便,而且很慢
  • 如果性能关键,不介意记录十进制的小数点(使用分为单位),并且数值又不太大,可以使用int和long
    • 没有超过9位数字,可以使用int
    • 没有超过18位数字,可以使用long
    • 超过18位数字,必须使用BigDecimal

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

自动装箱和自动拆箱模糊了但是没有完全抹去基本类型和装箱基本类型之间的区别
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱

基本类型和装箱基本类型主要区别

  • 基本类型只有值,而装箱基本类型具有与它们的值不同的同一性(==比较)
  • 基本类型只有功能完备的值,而每个装箱基本类型除了它对应的基本类型的所有功能值之外,还有非功能值:null
  • 就比恩类型通常被装箱基本类型节省时间和空间

什么时候应该使用装箱基本类型

  • 作为集合中的元素、键和值,不能使用基本类型
  • 参数化类型,泛型不能指定基本类型
  • 反射的方法调用时必须使用装箱基本类型

自动装箱减少了使用装箱基本类型的繁琐性,但是没有减少它的风险,当可以选择的时候,基本类型要优先于装箱基本类型

  • 使用==比较装箱基本类型几乎总是错误的
  • 如果null对象被自动拆箱,就得到一个空指针异常
  • 装箱基本类型会导致高开销和不必要的对象创建,如在for循环中对装箱基本类型使用+=操作

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

  • 不适合代替其他的值类型
  • 不适合代替枚举类型
  • 不适合代替聚集类型
    • 分隔符出现在域中会造成混乱
    • 解析过程很慢很繁琐,也容易出错
    • 无法提供toString、equals、compareTo等功能
  • 字符串也不适合代替能力表
    • 使用ThreadLocal

当心字符串连接的性能

不要使用字符串链接操作符来合并多个字符串,除非性能无关紧要

注意事项:为连接N个字符串而重复的使用字符串连接操作符,需要N的平方级的时间,StringBuilder的做法是线性增加
Tips:使用StringBuilder的append方法
Tips:使用字符数组,或者每次只处理一个字符串而不是把它们组合起来

通过接口引用对象

如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明
这样有助于我们有了更好性能或者更多功能的实现时更改这个实现

最佳实践

  • 有适当接口:用接口引用对象就会使程序更加灵活
  • 没有适当接口:则使用类层次结构中提供了必要功能的最基础的类

注意事项:如果原来的实现提供了某种特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么很关键的一点是,新的实现也要提供同样的功能;这就需要在声明变量的地方给这些需求建立相应的文档声明

接口优先于反射机制

如果你编写的程序必须要与编译时未知的类一起工作,如果有可能,就应该仅仅使用反射机制来实例化对象,而访问对象是则使用编译时一直的某个接口或者超类

反射的代价

  • 丧失了编译时检查的好处,包括异常检查
  • 执行反射访问需要的代码非常笨拙和冗长
  • 性能损失

注意事项:通常,普通应用程序在运行时不应该以反射方式访问对象

谨慎的使用本地方法

本地方法的用途

  • 提供了“访问特定与平台的机制”的能力,如注册表和文件锁
  • 提供了访问遗留代码库的能力,从而可以访问遗留数据
  • 可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能
    • 不值得提倡,因为JVM实现变得越来越快了

使用本地方法的缺点

  • 本地语言不是安全的,有可能会造成内存损坏错误
  • 本地方法不可移植
  • 难以调试
  • 进入和退出本地代码时,有固定开销,所以只做少量工作可能降低性能
  • “胶合代码”写起来单调乏味,并且难以阅读

谨慎的进行优化

优化格言:

很多计算上的过时都被归咎于效率(没有必要的效率),而不是任何其他原因,甚至包括盲目地做傻事
不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源
在优化方面,我们应该遵循两条原则:1.不要进行优化 2.没有绝对清晰的优化方案之前,不要进行优化

优化的弊大于利,特别是不成熟的优化。在优化过程中,产生的软件可以既不快速,也不正确,而且还不容易修正

最佳实践

  • 不要费力去编写快速的代码,应该努力编写好的程序,速度自然会随之而来
  • 在设计系统的时候,尤其是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素
  • 如果性能不够好,在性能剖析工具的帮住下,找到问题的根源,然后设法优化系统中相关的部分
    • 再多低层的优化也无法弥补算法的选择不当

遵守普遍接受的命名惯例

  • 字面的
    • 包的名称应该是层次状的,用句号分割每个部分,每个部分都包括小写字母和数字(很少使用数字);除了域名转换的前缀外,包名称的其余部分应该包括一个或者多个描述该包的组成部分。鼓励使用有意义的缩写形式
    • 类和接口的名称,包括枚举和注解类型的名称,都应该包含一个或者多个单词,每个单次的首字母大写;应该尽量避免使用缩写,除非是一些首字母缩写和一些通用缩写;强烈推荐首字母大写的形式,更易读
    • 方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,只不过首字母应该小写
      • 常量域是一个例外,它的名称由一个或多个大写单词组成,中间用下划线隔开,这是唯一推荐使用下划线的情形
    • 局部变量名称的字面惯例与成员名称类似,只不过它也允许缩写,取决于上下文环境
    • 类型及参数名称通常由单个字母组成
      • T表示任意的类型
      • E表示集合的元素类型
      • K和V表示映射的键和值类型
      • X表示异常
      • 任何类型的序列尅是T、U、V或者T1、T2、T3
  • 语法的
    • 类通常用一个名词或者名词短语命名
    • 接口的命名同类命名,用一个-able、-ible结尾的形容词命名
    • 注解类型没有特殊安排,所有词类都很常用
    • 方法通常用动词或者动词短语来命名
      • JavaBean的方法使用get、set开头
      • 转换对象类型、返回不同类型的独立对象的方法使用toType
      • 返回视图不同于接受对象的类型的方法用asType
      • 返回一个被调用对象同值的基本类型的方法使用typeValue
      • 静态工厂方法常用名为valueOf、of、getInstance、getType、newType
    • 域名称语法没有很好的建立
      • boolean类型一般同访问方法名称,去掉is或者get
      • 其他类型的域常用名词或者名词短语命名
    • 局部变量语法管理类似于域的语法惯例,但是要求更弱一些

异常

只有在异常的情况才使用异常

异常应该只用于异常的情况下;它们永远不应该用于正常的控制流

  • 因为异常机制的设计初衷是用于不正常的情形,所以很少会有JVM师徒对它们进行优化,使得与显示的测试一样快速
  • 把代码放在try-catch块中反而组织了现代JVM实现本来可能要执行的某些特定优化
  • 对数组进行便利的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉

Tips:设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。

状态测试方法和可识别的返回值

  • 如果类有“状态相关”的方法,往往也应该有一个“状态测试”方法,指示是否可以调用这个“状态相关”方法;如hasNext方法
  • 如果“状态相关”的方法被调用时,处于不适当的状态中,它就返回一个可识别的值

最佳实践

  • 如果对象在缺少外部同步的情况下被并发访问,或者被外界改变状态,使用可被识别的返回值可能是很必要的,因为在“状态测试”与“状态相关”的方法调用的间隔中可能被改变内部状态
  • 如果“状态测试”方法必须重复“状态相关”方法的工作,从性能角度考虑,就应该使用可识别的返回值
  • 如果其他方面都是相同的,“状态测试”方法则略优于可悲识别的返回值,它提供了更好的可读性,更易于检测和改正(忘记测试的异常很容易被发现,而忘记检查返回值的bug很难检测)

对可恢复的情况使用受检异常,对变成错误使用运行时异常

Java三种可抛出结构

  • 受检异常
  • 运行时异常
  • 错误

异常选择原则

  • 对于可恢复的情况,使用受检异常,如果不可恢复
  • 对于程序错误,使用运行时异常

方法中声明要抛出的每个受检异常,都是对API用户的一种潜在指示:与异常相关的条件是调用这个而方法的一种可能的结果
运行时异常和错误在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构,如果没有捕获,会导致线程停止并出现适当的错误信息

Tips:错误往往被JVM保留用于表示组资源不足、约束失败、或者其他程序无法执行的条件,所以你实现的所有未受检异常的抛出结构都应该是RuntimeException的直接或者间接子类
Tips:因为受检异常往往指明了可恢复的条件,所以对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息

避免不必要的受检异常

受检异常强迫成语言处理异常的条件,大大增强了可靠性;过分使用受检异常会使API使用起来非常不方便

参考上面的《只有在异常的情况才使用异常》重构受检异常

优先使用标准的异常

好处

  • 使你的API更加易于学习和使用,因为它与程序员已经熟悉的习惯用法是一致的
  • 对于这些API的程序而言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常
  • 异常类越少,意味着内存印迹就越小,装载这些类的时间开销也越少
异常 使用场合
IllegalArgumentException 非null的参数不正确
IllegalStateException 对象状态不合适
NullPointerException 禁止null的参数被传递null
IndexOutOfBoundException 下标参数越界
ConcurrentModificationException 并发修改异常
UnsupportOperationException 不支持请求方法
ArithmeticException 算术异常
NumberFormatException 非法数字格式异常

抛出与抽象相对应的异常

如果可以检查调用保证调用成功从而避免它们抛出,可以避免异常转译被滥用
如果不能组织或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也适合才可以将异常从低层传播到高层

Tips:高层的实现捕获低层的异常,同事抛出可以按照高层抽象进行解释的异常,称作异常转译
Tips:大多数的标准异常都有支持链的构造器,通过构造器包装低层异常形成异常链,对于没有支持链的异常,使用Throwable的initCause方法设置原因

在细节消息中包含能捕获失败的信息

异常类型的toString方法应该尽可能多地返回有关失败原因的信息,这一点特别重要
为了确保异常的细节信息中包含足够能捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些信息;提供这样的访问方法对于受检异常比非受检异常更为重要,因为这些细节对于失败恢复很有帮助

注意事项:异常的细节消息不应该与“用户层次的错误信息”混为一谈,后者对于最终用于而言必须是可理解的

努力使失败保持原子性

失败原子性

失败的方法调用应该使对象保持在被调用之前的状态

实现

  • 使用不可变对象,对象被创建之后它就处于一致的状态中,以后不会再发生变化
  • 对于可变对象,可以在执行前检查参数的有效性,使得对象在被修改之前,就抛出适当的异常
    • 如果参数只有执行了部分计算后才能检查,可以调整计算处理过程的顺序,是得任何可能失败的计算部分在对象被修改之前发生
  • 编写一段恢复代码,这种办法主要用于永久性的数据结构
  • 在对象的一份临时拷贝上执行操作,操作完成后再用临时拷贝中的结果代替对象的内容,如果失败,原对象不变

注意事项:如果调用方法违反了失败原子性,应该在API文档中清楚地致命对象会处于什么样的状态

不要忽略异常

空的cache块会使异常达不到应有的目的,至少应该记录异常,如果可以忽略,cache块也应该要包含一些说明,解释为什么可以忽略

并发

同步访问共享的可变数据

Java语言规范保证读写一个变量是原子的,除非这个变量的类型为long或者double(long、double是64位的,其读写会分成两次32位的操作,并不是原子操作,但很多商用虚拟机都进行了优化)
但是它并不保证一个线程写入的值对于另外一个线程将是可见的,这归因于Java语言规范的内存模型,它规定了一个线程所做的变化合适以及如何变成对其他线程可见

未能同步共享可变数据可能会造成程序的活性失败安全性失败

活性失败

// 如果没有volatile,线程程序将无法停止,因为后台线程一直在运行
// 或者使用同步的get、set方法保持通信效果
private static volatile boolean stop;
public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        int i = 1;
        while (!stop) {
            ++i;
        }
    }).start();
    TimeUnit.SECONDS.sleep(1);
    stop = true;
}

Tips:虽然volaile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚写入的值

安全性失败

// volatile保证了不同线程间修改可见
private static volatile int serialNumber = 0;
public static int getNextSerialNumber() {
    return serialNumber++;// 但是++操作非原子,会导致序列重复
}
// 使用synchronized,并去除volatile
private static int serialNumber = 0;
public static synchronized int getNextSerialNumber() {
    return serialNumber++;
}
// 使用AomticLong,保证安全,防止int越界
private static final AtomicLong serialNumber = new AtomicLong();
public static long getNextSerialNumber() {
    return serialNumber.getAndIncrement();
}

避免问题

多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步,如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知

  • 不共享不可变的数据,将可变数据限制在单个线程中,并建立对应的文档说明,了解框架和类库也很重要,因为它们引入了你不知道的线程
  • 安全发布:让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,只同步共享对象的引用的动作,然后其他线程没有进一步的同步也可以读取对象,只要它没有再被修改,这种对象被称作事实上不可变的
    • 保存在静态域中,作为类初始化的一部分
    • 保存在volatile域、final域或者通过正常锁定访问的域中
    • 放到可并发访问的集合中

避免过度同步

过度同步可能会导致性能降低、死锁,甚至不确定的行为

  • 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制
    • 不要调用设计成要被覆盖的方法
    • 不要调用由客户端以函数对象的形式提供的方法

由于Java语言中的锁是可重入的,即调用线程已经有这个锁,该线程再次尝试获得锁会成功,尽管概念上不相关的另一项操作正在该锁保护的对象上正在进行
从本质上讲,这个锁没有尽到它的职责。可重入的锁简化了多线程的面向对象程序的构造,但是他们会将活性失败转化成安全性失败

public class MySubject...
    private String message;
    private final List<Observer> observers = new ArrayList<>();
    public void registerObserver(Observer observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }
    public void removeObserver(Observer observer) {
        synchronized (observers) {
            observers.remove(observer);
        }
    }
    public void notifyObservers() {
        synchronized (observers) {
            for (Observer observer : observers) {
                observer.update(this, message);
            }
        }
    }
main...
// 异常
MySubject subject = new MySubject();
MyObserver myObserver = new MyObserver(subject) {
    @Override
    public void update(Subject subject, Object args) {
        if (StringUtils.equals("remove", args.toString())) {
            // 由于锁的重入机制,synchronized不能保证observers遍历时不被修改
            subject.removeObserver(this);// 并发修改异常
        }
    }
};
subject.setMessage("remove");
subject.notifyObservers();

// 死锁 
MySubject subject = new MySubject();
MyObserver myObserver = new MyObserver(subject) {
    @Override
    public void update(Subject subject, Object args) {
        MyObserver current = this;
        if (StringUtils.equals("remove", args.toString())) {
            try {
                Executors.newSingleThreadExecutor().submit(
                        new Thread(() -> {
                            // 这里会死锁
                            subject.removeObserver(current);
                        })
                ).get();// get会等待submit结束后拿到结果,造成锁竞争
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
};
subject.setMessage("remove");

解决

通过将外来方法移除同步代码块解决上面的问题

public void notifyObservers() {
    List<Observer> snapshot;
    synchronized (observers) {
        snapshot = new ArrayList<>(observers);// 创建快照
    }
    for (Observer observer : snapshot) {
        observer.update(this, message);
    }
}

并发结合CopyOnWriteArrayList专门为此定制。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。(读不加锁,写时加锁)
如果大量使用,CopyOnWriteArrayList性能将大受影响,但是对于观察者列表来说却是很好的,因为他们几乎不改动,并且经常被遍历

// 修改方法内部提供了锁,便利不加锁
List<Observer> observers = new CopyOnWriteArrayList<>();
public void registerObserver(Observer observer) {
    observers.add(observer);
}
public void removeObserver(Observer observer) {
    observers.remove(observer);
}
public void notifyObservers() {
    for (Observer observer : observers) {
        observer.update(this, message);
    }
}

在同步区域之外被调用的外来方法称作“开放调用”,除了可以避免死锁,还可以极大的增加并发性,因为避免了外来线程调用的时间长而造成等待
你应该在同步区域内做尽可能少的工作,如果你必须要执行很耗时的工作,则应该设法把这个动作移动到同步区域的外面

过度同步

  • 实际成本并不是指获取锁锁花的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致内存视图而导致的延迟
  • 另一项潜在开销是它会限制JVM优化代码执行的能力

最佳实践

  • 如果一个可变类要并发使用,应该使这个类变成是线程安全的,通过内部同步,可以获得明显比从外部锁定整个对象更高的并发性
    • 内部同步的类提升并发性
      • 分拆锁
      • 分离锁
      • 非阻塞并发控制
  • 否则,就不要使用内部同步,让客户端在必要的时候从外部同步
    • 修改静态域的方法必须使用同步访问,用户无法在外部同步,因为不能保证所有客户端都进行外部同步
  • 当你不确定的时候,不要同步,并通过文档说明它不是线程安全的

executor和task优先于线程

Java1.5版本后包含了一个Executor Framework,是一个很灵活的基于接口的任务执行工具

注意事项:你不应该编写自己的工作队列,还不应该直接使用线程 注意事项:现在关键的抽象不再是Thread了,而是工作单元task,任务有两种,Runable和Callable

Timer和ScheduledThreadPoolExecutor

  • Timer只用一个线程来执行任务,长期运行时,会影响定时的准确性;Executor支持多个线程
  • Timer唯一的线程抛出未捕获的异常,Timer就会停止运行;Executor可以优雅地从抛出未受检异常任务中回复

并发工具优先于wait和notify

java.util.concurrent中高级工具分为三类:Executor Framework、并发集合以及同步器

Executor Framework

参看上条及线程部分

并发集合

  • CopyOnWriteCollection
  • ConcurrentCollection

并发集合为标准的集合接口提供了高性能的并发实现,这些实现在内部自己管理同步。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢。

Tips:连续两个方法调用的操作原子性无法通过锁定对象得到保证(因为无法保证所有写操作都锁定并发集合对象),参看《避免过度同步》的最佳实践部分采用内部同步或者外部同步原子操作
Tips:除非不得已,否则应该优先使用ConcurrentHashMap,而不是Collections.synchronizedMap或者Hashtable,因为前者对并发操作做了优化

有些集合接口通过阻塞操作进行了扩展,例如BlockingQueue,它们会一直等待到可以操作成功为止
大部分的ExecutorService都使用BlockingQueue

同步器

参考《线程》部分

  • 常用
    • CountDownLatch(计数等待)
    • Semaphore(限制访问线程数)
  • 不常用
    • CyclicBarrier(等待集合)
    • Exchanger(线程数据交换)

Tips:对于间歇式的定时,始终应该使用System.nanoTime,而不是System.currentTimeMills,前者更准确也更加精确,因为不受系统的实时时钟的调整所影响

wait和notify

始终应该使用wait循环模式来调用wait方法,在等待之前和等待之后测试;永远不要在循环之外调用wait方法

synchronized(obj){
    while(<your condition>){
        obj.wait();
    }
    // opteration
}
  • wait前的测试,可以运行时,就跳过等待,可以保证不会死锁
    • 条件是可运行,在等待之前,notify和notifyAll方法已经调用,不检查就等待的情况不保证该线程将会从等待中苏醒
  • wait后的测试,条件不成立继续等待,可以保证安全
    • 另一个线程获得了锁,并且得到锁的线程已经修改了条件,这时继续等待
    • 条件不成立,另外的线程恶意调用了notify或者通知线程过度大方调用了notifyAll方法
    • 伪唤醒

注意事项:一般情况下,优先使用notifyAll而不是notify,可以防止不相关线程的以外或恶意等待,真正等待的线程无限等待下下去。虽然从优化的角度看应该使用notify,但那需要保持程序的活性

线程安全性的文档化

一个类为了可被多个线程安全使用,必须在文档中清楚地说明它所支持的线程安全性级别。

  • 不可变的
    • 这个类的实例是不可变的。不需要外部的同步,如String,Long,BigInteger。
  • 无条件的线程安全
    • 这个类的实例是可变的,但是这个类有足够的内部同步。如Random,ConconcurrentHashMap。
  • 有条件的线程安全
    • 除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件安全相同。如Collections.synhronized包装返回的集合,它们的迭代器要求外部同步。
  • 非线程安全
    • 这个类的实例是可变的。为了并发使用它们,客户必须利用自己选择的外部同步包围每个方法调用。例子包括ArrayList
  • 线程对立的
    • 这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外围同步包围。很少见。

当一个类承诺了“使用一个公共可访问的锁对象”时就意味着允许客户端以原子的方式执行一个方法调用序列
但是这种灵活性是要付出代价的,客户端可以有意或无意超时持有锁,发起拒绝服务攻击

  • 有条件的线程安全类上,必须在文档中声明:在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁
  • 无条件的线程安全类上,应该使用final的私有锁对象,代替同步的方法,因为私有锁对象不能被这个类的客户端程序访问,所以它们不能妨碍对象的同步
    • 私有锁特别适合专门为继承设计的类,防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活地对并发控制采取更加复杂的办法

慎用延迟初始化

虽然延迟初始化主要是一种优化,但它也可以用来打破类和初始化中的有害循环
和其他优化一样,除非绝对必要,否则就不要这么做

利弊

  • 它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域开销

最佳实践

多个线程共享一个延迟初始化的域,需要同步,否则可能造成严重问题

  • 对于实例域,就使用双重检查模式
  • 对于静态域,就使用lazy initialization holder class模式
  • 对于可接受重复初始化的实例域,可以考虑使用单重检查模式

参看《设计模式》的单例部分

不要依赖于线程调度器

不要让应用程序的正确性依赖于线程调度器,否则,结果得到的应用程序既不健壮,也不具有可移植性
作为推论,不要使用Thread.yield或者线程优先级

注意事项:如果线程没有做有意义的工作,就不应该运行
注意事项:线程不应该处于忙等状态,除了容易收到调度器的变化影响状之外,忙等极大的增加了处理器的负担,降低了其他线程可以完成的有用工作量
注意事项:线程优先级是Java平台上最不可移至的特性
Tips:Thread.yield的唯一用途是在测试其间人为的增加并发性,但是在Java语言规范中,Thread.yield不做实质性的工作,只是将控制权返回给它的调用者;因此使用Thread.sleep(1)来代替Thread.yield来进行并发测试,不要使用Thread.sleep(0),他会立即返回

避免使用线程组

线程组并没有提供太多有用的功能,而且他们提供的许多功能还都有缺陷的。

Tips:如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor
Tips:Thread.setUncaughtExceptionHandler方法捕获未被捕捉异常的控制权

序列化

谨慎的实现Serializable接口

虽然一个类可被序列化的直接开销非常低,甚至可以忽略不计,但是为了序列化而付出长期开销往往是实实在在的

  • 最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性
    • 设计良好的序列化形式也许会给类的演变带来限制;但是设计不好的序列化形式则可能会使类根本无法演变。
    • 如果你通过任何方式改变了这些信息,比如,增加了一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化。因此,如果你没有声明一个显式的序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
  • 增加了出现Bug和安全漏洞的可能性
    • 因为反序列化机制中没有显式的构造器,所以你很容易忘记要确保:反序列化过程必须也要保证所有”由构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。依靠默认的反序列化机制,可以很容易地使对象的约束关系遭到破坏,以及遭受到非法访问。
  • 随着新的版本发行,相关的测试负担也增加了

Tips

  • 如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这个类来说,实现Serializable接口就非常有必要
  • 为了继承而设计的类应该很少实现Serializable,接口也应该很少会扩展它

如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。
特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。
因此,对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器。

//Nonserializable stateful class allowing serializable subclass
public abstract class AbstractFoo {
    private int x, y;//Our state
    // This enum and field are used to track initialization
    private enum State {
        NEW, INITIALIZING, INITIALIZED
    }
    private final AtomicReference<State> init = new AtomicReference<>(State.NEW);
    public AbstractFoo(int x, int y) {
        initialize(x, y);
    }
    // This constructor and the following method allow
    // subclass's readObject method to initialize our state.
    protected AbstractFoo() {}
    protected final void initialize(int x, int y) {
        if (init.compareAndSet(State.NEW, State.INITIALIZING))
            throw new IllegalStateException("Already initialized");
        this.x = x;
        this.y = y;
        //Do anything else the original constructor did
        init.set(State.INITIALIZED);
    }
    //These methods provide access to internal state so it can
    //be manually serialized by subclass's writeobject method.
    protected final int getX() {
        checkInit();
        return x;
    }
    protected final int getY() {
        checkInit();
        return y;
    }
    //Must call from all public and protected instance methods
    private void checkInit() {
        if (init.get() != State.INITIALIZED)
            throw new I1legalStateException("Uninitialized");
    }
    //...Remainder omitted
}
//Serializable subclass of nonserializable stateful class
public class Foo extends AbstractFoo implements Serializable {
    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        // Manually deserialize and initialize superclass state
        int x = s.readInt();
        int y = s.readInt();
        initialize(x, y);
    }
    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();
        //Manually serialize superclass state
        s.writeInt(getX());
        s.writeInt(getY());
    }
    //Constructor does not use the fancy mechanism
    public Foo(int x, int y) {
        super(x, y);
    }
    private static final long serialVersionUID = 1856835860954L;
}
  • 内部类不应该实现Serializable
    • 它们使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值。因此,内部类的默认序列化形式是定义不清楚的。然而,
  • 静态成员类却是可以实现Serializable接口。

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

一般来讲,只有当你自行设计的自定义序列化形式与默认的形式基本相同时,才能接受默认的序列化形式。
即便默认的序列化形式是合适的,通常还必须提供一个readObject 方法以保证约束关系和安全性

默认序列化形式缺点

  • 它使这个类的导出API永远的束缚在该类的内部表示法上,即使今后找到更好的的实现方式,也无法摆脱原有的实现方式
  • 它会消耗过多的空间
    • 进行不必要的计算和存储
  • 它会消耗过多的时间
    • 经历一个昂贵的图遍历的过程
  • 它会引起栈溢出
    • 取决于JVM的具体实现和内存参数

注意事项:对于散列而言,接受默认的序列化形式将会造成一个严重的Bug,因为约束关系会遭到严重破坏(如对象所处的桶不保证有同样的结果)

自定义序列化

在序列化过程中,虚拟机会试图调用对象类里的writeObject()和readObject(),进行用户自定义的序列化和反序列化,如果没有则调用ObjectOutputStream.defaultWriteObject()和ObjectInputStream.defaultReadObject()
同样,在ObjectOutputStream和ObjectInputStream中最重要的方法也是writeObject()和 readObject(),递归地写出/读入byte
所以用户可以通过writeObject()和 readObject()自定义序列化和反序列化逻辑。对一些敏感信息加密的逻辑也可以放在此

transient

transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的
当一个或多个域字段被标记为transient时,如果要进行反序列化,这些域字段都将被初始化为其类型默认值

对象引用域被置为null
数值基本域的默认值为0
boolean域的默认值为false

如果这些值不能被任何transient域所接受,你就必须提供一个readObject方法。它首先调用defaultReadObject,然后再把这些transient域恢复为可接受的值,另外一种方法是,延迟初始化这些值到第一次使用的时候
readObject、writeObject方法中总是调用defaultReadObject和、defaultWriteObject总是推荐的,它会影响序列化的形式,极大的增强灵活性,方便在以后的版本中方便的增加非transient域,保持向前或者向后兼容

注意事项:在决定将一个域做成非transient的之前,一定要确信它的值是该家对象逻辑状态的一部分,如果使用自定义序列化,大多数的域应该是transient的
注意事项:无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步
Tips:无论你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。这样可以避免序列版本UID成为潜在的不兼容根源,同时也会带来小小的性能好处,因为不需要去算序列版本UID。

保护性地编写readObject方法

  • 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别
  • 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后
    • 保护性拷贝在有效性检查之前进行,而且不使用不可信的clone方法拷贝
  • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口[JavaSE6,Serialization]
  • 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法
    • 被覆盖的方法将在子类的状态被反序列化之前运行,程序有可能会报错

注意事项:不要使用ObjectOutputStream的writeUnshared和readUnshared方法,因为会受到攻击

石蕊测试:增加一个公有的构造器,其参数对应于该对象中每个非transient的域,并且无论参数的值是什么,都是不进行检查就可以保存到对应的域中
如果这种做法不合适,就必须提供一个显示的readObject方法,并且它必须执行构造器所要求的保护性拷贝和有效性检查
另一种方法是,可以使用序列化代理模式

Tips:总而言之,每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例。

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

  • 你应该尽可能地使用枚举类型来实施实例控制的约束条件
  • 如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolver方法,并确保该类的所有实例域都为基本类型,或者是瞬时的

readResolve特性允许你用readObject创建的实例代替另一个实例

注意事项:任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例
注意事项:如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的,否则会受到反序列化攻击
Tips:你将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例。JVM对此提供了保障

readResolve的可访问性(accessibility)很重要

  • 如果把readResolve方法放在一个final类上,它就应该是私有的
  • 如果把readResolver方法放在一个非final的类上,就必须认真考虑它的可访问性
    • 如果它是私有的,就不适用于任何子类
    • 如果它是包级私有的,就只适用于同一个包中的子类
    • 如果它是受保护的或者公有的,就适用于所有没有覆盖它的子类
  • 如果readResolve方法是受保护的或者公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样有可能导致ClassCastException异常

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

序列化代理模式

每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。 这个嵌套类被称作序列化代理(serialization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。 这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。
按设计,序列代理的默认序列化形式是外围类最好的序列化形式。
外围类及其序列代理都必须声明实现Serializable接口。

// EnumSet使用了序列化代理
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable{
    /**
     * The class of all the elements of this set.
     */
    final Class<E> elementType;
    /**
     * All of the values comprising T.  (Cached for performance.)
     */
    final Enum[] universe;
    private static Enum[] ZERO_LENGTH_ENUM_ARRAY = new Enum[0];
    EnumSet(Class<E>elementType, Enum[] universe) {
        this.elementType = elementType;
        this.universe    = universe;
    }
    //N多方法代码已省略
    /**
     * This class is used to serialize all EnumSet instances, regardless of
     * implementation type.  It captures their "logical contents" and they
     * are reconstructed using public static factories.  This is necessary
     * to ensure that the existence of a particular implementation type is
     * an implementation detail.
     *
     * @serial include
     */
    private static class SerializationProxy <E extends Enum<E>>
        implements java.io.Serializable{
        /**
         * The element type of this enum set.
         */
        private final Class<E> elementType;
        /**
         * The elements contained in this enum set.
         */
        private final Enum[] elements;
        SerializationProxy(EnumSet<E> set) {
            elementType = set.elementType;
            elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
        }
        private Object readResolve() {
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum e : elements)
                result.add((E)e);
            return result;
        }
        private static final long serialVersionUID = 362491234563181265L;
    }

    Object writeReplace() {
        return new SerializationProxy<>(this);
    }
    // readObject method for the serialization proxy pattern
    // See Effective Java, Second Ed., Item 78.
    private void readObject(java.io.ObjectInputStream stream)
        throws java.io.InvalidObjectException {
        throw new java.io.InvalidObjectException("Proxy required");
    }
}

Tips:当writeReplace()方法被实现后,序列化机制会先调用writeReplace()方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中,这便是序列化代理需要的功能
Tips:代理中readResolve方法仅仅使用公有API完成了外围类实例的创建,可以省去约束检查,因为公有API中已经包含

序列化代理模式有两个局限性

  • 它不能与可以被客户端扩展的类兼容
  • 它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理

注意事项:序列化代理模式锁增强的功能和安全性的代价在于性能开销高于保护性拷贝的开销


Effective Java

Search

    Post Directory