编程建议
Effective java第三版笔记
创建和销毁对象
用静态工厂方法代替构造器
使用时机
当类需要返回一个特殊的对象的时候
例如需要Integer
返回一个质数, 光靠构造器名称很难表达返回质数的意图, 但是静态工厂方法可以取方法名, 表明返回质数的意图
当类需要复用之前创建的对象的时候
调用构造器如果不报错, 必定会创建一个新的对象. 如果想返回一个之前创建的对象, 只能使用静态方法, 这也是实现单例模式的前提
当工具类返回对象的时候
工具类例如Driver.getConnect()
, Collections.list()`, 这些都是静态方法
注意事项
- 使用静态方法时, 尽量不要使静态方法依赖底层资源, 应该通过依赖注入的方法处理
遇到多个构造器参数时要考虑使用构造器
使用时机
构造器参数多余4个的时候
问题的本质在于参数过多, 可以把多个参数打包放入一个类中, 一次传递. 这个对于非构造方法也是通用的.
注意事项
- 如果一个类不需要保证有效性, 直接使用set方法设置值也是可以的. 例如用于参数传递的类(Builder), 它们的效性就是无关紧要的, 用的就是set方法来设置值. 对于需要保证有效性的类, 建议使用构造器模式
用私有构造器或枚举类型强化Singleton属性
使用时机
创建单例类的时候
使用单例模式时, 必须私有化构造器, 这是显然的, 不需要解释
注意事项
- 优先使用静态域来构建单例模式, 它比枚举更具有通用性
通过私有构造器强化不可实例化的能力
使用时机
创建工具类的时候
工具类不应该有实例, 所以一定要私有化构造器, 防止被别人误用创建出实例
注意事项
抽象类虽然不能被实例化, 但是它的子类可以被实例化, 因此使用抽象类来创建工具类不合适. 抽象类就是用来解决复用的
优先考虑依赖注入来引用资源
使用时机
当类new一个对象,且这个对象必须通过依赖注入才能进行单元测试的时候
当new一个对象的时候,考虑这个对象是否需要进行mock,如果不需要就new,如果必须要进行mock,则通过依赖注入的方式来使用
注意事项
- 依赖注入还可以注入依赖对象的工厂方法类,这样被注入的类这一自己创建对象
避免创建不必要的对象
使用时机
当new一个对象时,看这个类时候提供了相同效果的静态方法,或者是字面量
new一定会创建一个新对象,对于某些不可变对象来说,非常浪费,这时使用静态方法,类的实现者可以实现对象的复用
注意事项
- 当使用包装类进行计算时,也会创建很多不必要的对象,所以优先使用基本类型
消除过期的对象引用
使用时机
自己实现集合的时候
自己实现集合的时候,从集合中删除元素后,自己没有将元素的引用设置为null,存在过期引用
注意事项
- 这说明java也是会存在内存泄漏的
- weakhashmap可以用来实现一个不用自己手动清理过期引用的缓存
- 注册回调函数,但客户端不取消注册,也会导致服务端内存泄漏
避免使用终结方法和清除方法
使用时机
任何时候都不要实现总结方法和清除方法,它们的调用时机不是有程序员编程控制的,而且它们都不一定会被JVM调用。因此不要期待用终结方法和清除方法来清理资源,应该使用Java的final机制
终结方法和清除方法存在的另外一个问题是,对于任何对象,哪怕在构造器中抛出异常的对象,在被JVM回收时,都会调用终结方法。这样就可以构造出一种攻击手段,在终结方法种调用构造失败的对象的方法,调用这种对象的方法是非常危险的
注意事项
- 清除方法石java9中的特性
- 在任何时候都不要使用终结方法和清除方法,哪怕是把它们作为资源回收的保底机制也不要使用
try-with-resources优先于try-finally
使用时机
任何时候try-with-resources都优先于try-finally
因为try和finnally都有可能抛出异常,如果两个地方都抛出了异常,finnally的异常会覆盖try里面的异常,而try里面的异常通常才是根本原因
注意事项
- 如果要使用try-with-resources机制,必须要实现autocloseable接口,Java类库里需要关闭资源的类应该都已经实现了这个接口,可以直接使用这个机制
- try-with-resources优先抛出try里面的异常,但close里面的异常也是会被记录的,而try-finnaly只会有finnally里面的异常
对所有对象都通用的方法
覆盖equals时请遵守通用约定
使用时机
任何时候
自己实现的类需要有逻辑相等的要求,但这时父类的equals并不满足要求,这时候就要自己手动实现equals方法。注意这时就不要使用继承了,使用组合来代替继承,自己来返回一个视图,equeals用instanceof来实现
注意事项
- equals一定不要和非本类或父类之外的其他类进行比较,这样肯定会违反equals的对称约束
- 覆盖父类(非Object)的equeals(instanceof)会违反对称性,覆盖父类(非Object)的equeals(getClass),子类很容易违反里氏替换原则
- 不要重写非Object父类的equeals方法,如果要实现逻辑相等的功能,使用复合+视图的方式
覆盖equeals时总要覆盖hashcode
使用时机
任何时候
重写equeals方法时。保证hashcode用的域和equeals里面的域一致,少了增大了hash碰撞的可能性,降低效率;多了会让该对象在hashmap和hashset中运行出现问题
如果这个类为不可变类,且是为了在hash类集合里作为键,可以选择在创建或第一调用hashcode方法时,将hashcode缓存下来。
注意事项
- hashcode相当于equeals的提前过滤
始终要覆盖toString
使用时机
非静态类和枚举类
toString,会在“+”,print当中被自动调用,可以让类使用起来更舒适,静态类不需要,枚举类则是枚举类型自己实现了一个toString方法
注意事项
- toString中打印一些需要对外展示的东西就行了
- 对于值类,如果可以考虑将toString的格式固定,同时提供一个静态方法将String转回对象。如果格式是固定的,请在方法说明中,指明这是一个固定格式,如果不是固定的,也请指明这个格式不是固定的
- 对toString打印的值,提供可访问的方法,不然别人也只能假设toString的格式,通过toString来获得值
谨慎地覆盖clone
使用时机
任何时候都不要考虑覆盖clone
java中clone,是一种特殊的机制,和反序列化一样可以绕过构造器。如果需要实现clone的效果,建议使用静态工厂方法。因为clone是依赖了语言外的创建对象机制,同时clone无法设置final域。
注意实现
- 尽量不要把接口当作标记接口来使用,但是如果确实要用于编译期识别类,那也没有办法
- 不要在构造方法中,调用可被重写的方法,因为这时有可能调用到子类的方法,而这时子类的状态还处于不一致状态
- 覆写方法的访问可以大于父类
考虑实现Comparable接口
使用时机
一个值类,需要有比较功能的时候
通过实现comparable接口,可以让这个类就可以利用Java类库中于此接口相关的算法和集合进行操作
注意事项
- 违反hashcode会使其无法正常使用依赖hashcode的类,而违反comparable约定,会无法正常使用依赖comparable接口的类
- 一个值类继承父类后,如果覆写了父类的compareTo和equeals,则无法遵守compareTo和equals的约定,所以应该利用组合的方式来实现comparable
- 在compareTo返回0应该保持与equeals方法等价,这样可以避免算法之间的差异,例如hashSet和TreeSet,它们都可以去重,如果compareTo=0时不与equeals等价,它们的去重后的接口会不一样
- 实现compareTo或compare时,针对基本类型和包装类型,不用使用-,<, >等操作符,利用包装类的比较方法,这样可以避免溢出和浮点数误差带来的错误结果
- java8提供快速创建比较器的方法comparingInt,comparingLong等等,但是这样创建的比较器,性能会慢10%左右,建议自己不要使用,比较器自己实现有不难,使用高级特性还会导致别人阅读起来更麻烦
接口和类
使类和成员的可访问性最小化
使用时机
任何时候
访问权限最小化,可以让类的使用者可以了解更少的知识,就能使用这个类。同时,让类的开发者也有足够的灵活性去修改这个类
如何保证一个类是可控的,让所有实例和静态域都变成私有的,除了静态常量,只有静态常量可以设置为public
注意事项
- 访问性可以分为两种,对外不暴露的和对外暴露的。私有的和包级私有的是不对外暴露的,保护的和公共的是对外暴露的
在公共类中使用访问方法而不是访问域
使用时机
任何时候
只有这样可以保证这个类的状态,始终是在开发者的可控范围之类的
使变化性最小化
使用时机
任何时候
使类的变化性最小化,最好是不可变对象,一个类能反生变化越小,使用起来越简单
不可变对象有很多优点,1. 不可变对象是线程安全的;2. 不可变对象可以被自由的共享;3. 不可变对象状态比较简单,使用使用时不必在意它现在的状态
注意事项
- 不可变对象应该考虑提供静态方法,来尽可能复用现有的实例
- 唯一的缺点,任何一个小改动,都会创建一个新对象,从而带来性能上的问题,这时可以考虑提供一个可变的配套类,就像String和StringBuilder一样
- 不给变类要是final的,或者构造器私有的,否则别人可以用一个恶意的可变子类来充当不可变父类,使用者依然以为是不可变类,但实际上这个类是可以改变的
复合优于继承
使用时机
任何时候
子类和父类的关系太紧密,父类失去了添加新方法的能力和修改已实现方法内部细节的能力,而复合的方式,添加方法和修改方法细节就没有问题
注意事项
- 继承就是为了解决复用问题,但是继承不如复合灵活
- 覆写方法时,光靠父类方法的调用约定是无法写出正确的子类方法的,子类必须知道父类如何实现改方法时,才有可能写对,但这就破坏了父类的封装性
- 因为能覆写方法,父类调用的方法就不再受父类开发者控制了,打破了类的封装
- 因为构造器不能被继承,如果用继承的方式来扩展类,子类有可能需要自己来实现构造器,使用组合就没有这个问题
要么设计继承并提供文档说明,要么禁止继承
使用时机
自己设计可被继承类的时候
文档需要写出覆盖每个方法带来的影响,如果有影响,需要用@implSpec说明。其实也只有使用模版方法设计模式的时候,才有可能需要设计可以被继承的类,其他的时候,都可以用组合来解决。
注意事项
- 构造器不要调用可以被覆盖的方法
- 为了继承而设计的类,因为公开了实现说明,这个类的修改就很受限制
- 一个类一定要为自己的功能实现一个接口,使用已有的或自己设计的都行,不然组合实现的类没办法实现多态特性
接口优于抽象类
使用时机
任何时候
抽象类除了可以具有实例属性,来实现某些实例方法外,相较接口没有任何优势,其实没必要编写抽象类和继承,除了使用模版方法的时候
注意事项
- 如果一个接口方法跟其他的接口方法有强关联,可以考虑用默认方法,但是要加上@implSpec注解
- 接口和抽象类完全就是两个东西,拿来比较是没有任何意义的
- 模版方法中父类被称为骨架类,这个父类通常是抽象的,用来实现代码复用。对于一个重要的接口,可以考虑实现它的骨架类,并且实现该接口的一个基本实现,这个基本实现可以继承这个骨架类
为后代接口设计类(不要轻易使用接口的默认方法,来给接口添加新的方法)
使用时机
设计类的时候
尽量不要使用给接口增加方法,这样会导致某些实现使用这些方法出现问题。就算要为接口添加新方法,要慎重考虑对实现的影响
接口只用于定义类型(不要定义常量接口)
使用时机
在创建的接口的时候
这时应该用工具类来实现常量,如果常量是可枚举的,可以使用枚举类型
注意事项
- 常量接口是指只有常量,没有方法的接口
- 这条建议不是说接口中不要定义常量,而是不要只定义常量,如果这个常量更接口中的方法有关,是可以定义的
类层次优于标签类
使用时机
想让一个类拥有两个类的功能的时候
应该使用继承,或者接口+组合的方式来代替标签类
注意事项
- 标签类这种东西是为了给不支持多态的编程语言的一种解决方法,在java中不因该使用这种东西
静态成员类优于非静态成员类
使用时机
编写嵌套类的时候
静态成员类优于非静态成员类的主要原因是非静态成员类会降低性能
注意事项
- java有四种嵌套类,静态成员类,非静态成员类,匿名类和局部类,后面三种属于内部类,可以有外部类实例的引用
- 什么时候用嵌套类,当一个类必须和另外一个类同时使用时,这个类可以定义为嵌套类。嵌套类对于编程语言来说,并不是必须的,作为一种可有可无的特性吧
限制源文件为单个顶级类
使用时机
创建类的时候
保证.java文件中只有一个顶级类
注意事项
- java允许一个源文件中定义多个顶级类,这样会让源文件和类定义的关系变得混乱,所以根本没有必要
泛型
请不要使用原生类型
使用时机
任何时候
泛型可以让程序更健壮和清楚,避免放入错误的类型和取出时的强制转型
注意事项
- 原生类型就是不带类型参数的类型,例如List
的原生类型就是List - >不允许除null之外的任何东西放入
消除非受检的警告
使用时机
出现非受检异常的时候
非受检警告指明了运行时可能出现的问题,如果确定运行时不会出现问题,应该使用@SuppressWarnings(“unchecked”)来消除警告,同时通过注释指明它为什么不会出现问题
注意事项
- getClass()是方法,是运行时才能获得信息,编译器是无法得到,只会把它当作是Object
- 应该在尽可能小的范围内使用@SuppressWarnings(“unchecked”)
列表优于数组
使用时机
想使用数组的时候
泛型列表比数组更安全
注意事项
- 数组在运行时知道它的具体类型,Object[] objects = new Long[10]; 你无法将一个String放入到objects中,会报ArrayStoreException,这个有点像多态。泛型只在编译期知道类型,在运行时,是不知道类型的,变成了Object,类型的具体信息被擦除了
- 在泛型类里,泛型参数全部都是Object,字节码是这样的,只有在被取出时,才进行了强转
优先考虑泛型
使用时机
编写可以适用于类型无关的类的时候
泛型可以让使用者避免强制类型转换,同时也避免放入错误的类型
注意事项
- 并非任何时候列表都能取代数组,数组和链表是计算机中最基本的元素,是数据结构的基础。例如某些List就是数组实现的。需要极高性能的时候,使用数组也是可以的
优先考虑泛型方法
使用时机
编写与类型无关的方法的时候
泛型方法避免了调用者的类型转换
注意事项
- 因为Java中泛型是用擦除来完成的,可以给不同的参数类型使用单个对象
利用有限制通配符来提升API的灵活性
使用时机
编写泛型类的方法和静态泛型方法时
为了获得更大程度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符
注意事项
- 要分清方法的参数时生产实例(从参数中创建实例)还是消费实例(把实例放入到参数中)
- 如果类型参数只在方法生命中出现一次,就可以用通配符取代它,通配符看起来更简单
谨慎并用泛型和可变参数
使用时机
编写带有可变参数的泛型方法的时候
泛型和可变参数配合并不好,这是Java设计如此
注意事项
- 如果确保没有问题可以使用@SafeVarargs()注解
- 可以考虑用列表来实现可变参数
- 要特别注意在泛型方法中调用可变参数泛型方法,会生成Object数组
优先考虑类型安全的异构容器
使用时机
一个容器需要同时存储任意种类型时
这时泛型可以起到一个检验参数类型的作用,保证类型安全
注意事项
- 泛型最常用于集合,充当类型参数,另外一种用法是将泛型作为键传入容器,用来保证容器的类型安全,保证不会放入别的类型
枚举和注解
用enum代替int常量
使用时机
想定义一组有关系且数量有限的常量时
int类型的常量不具备类型安全,因为它们都是int类型,可能用混。常量是编译时放入代码的,不重新编译代码是无法改变的,枚举是类,就不会有这个问题
注意事项
- 枚举相较int常量,枚举可以直接打印名称,同时可以一次性取出相同类型的全部枚举
- 枚举相较String常量,唯一的优势是可以列举,所以如果不能会枚举出来或需要进行枚举,还是可以用String常量
- 枚举的ValueOf可以枚举名称找到枚举
- 枚举有三种用法,枚举本身,枚举属性和枚举方法,分别是名称的枚举,属性的枚举和方法的枚举
使用实例字段代替序数
使用时机
需要列举一组固定的数字时
不要使用枚举本身的序号,自己新建一个实例字段。好处是这样可以设置任何自己想要的值,以及允许值重复
注意事项
- 枚举的序号可以说是内部细节,就不该给程序员使用
使用EnumSet代替位域
使用时机
任何使用位域的地方
位域的唯一目的就是为了集合操作,java中已经有集合接口,也就没有必要使用位域了
注意事项
- 位域就是Set的一种特殊实现,为了更高效的实现集合运算,是一种低级实现,使用起来比较麻烦
用EnumMap代替序数索引
使用时机
想使用枚举的ordinal值作为数组索引的时候
EnumMap可读性更好,也更灵活
注意事项
序数很不灵活,不要使用
用接口模拟可扩展的枚举
使用时机
没有使用的地方
没必要实现可扩展的枚举
注意事项
- 本质上就是枚举类型实现接口
注解优先于命名模式
使用时机
想给某个类,方法,字段,参数加上其他信息
注解就是为了这个而存在的
注意事项
- 命名模式就是将数据或标识写在类名,方法命中,字段名中或参数名中,这样非常不灵活
- 注解类型声明中使用的注解被称为元注解
- Java8增加了另外一种形式的多值注解,叫做重复注解,但是可读性不太好,了解就行
坚持使用Override注解
使用时机
子类想覆盖超类方法的时候
这样可以防止方法名或者方法参数写错导致没能覆盖父类方法的情况
注意事项
- 如果没有标注Override,子类方法还覆盖了父类的方法,IDE还会给出提示,防止子类意外覆盖父类的方法
用标记接口定义类型
使用时机
想要标记一个类时,更具体的是正在编写ElementType.Type类型的标记注解的时候
如果有方法使用标记类型作为参数,使用标记接口更合适,如果没有这样的方式,才使用标记注解
标记接口相较标记注解,标记接口能在编译期就能识别被标记的类,而注解必须结合反射在运行时才能发现
注意事项
- 标记接口是不包含方法声明的接口,用于标记一个类是否允许被执行某种操作,例如Clonable接口和Serializable接口就是标记接口,用来标记一个类允许被复制和序列化
- 标记接口其实是接口的一种特殊用法,接口通常是来说明某个类具有某种功能,而不是标记一个允许被用来干什么,标记接口很少有使用机会
- 接口使用定义类型的,不要定义常量接口。如果要定义类型,优先使用接口而不是抽象类,抽象类只能单继承,很不灵活
Lambda和Stream
Lambda优先于匿名类
使用时机
实现单个方法的匿名类的时候
因为Lambda相比匿名类更简洁
注意事项
- 在Java8之前,通常用带有单个方法的接口作为函数类型,它们的实例称作函数对象
- Lambda没有名称和文档,如果一个Lambda表达式超过了3行,还是使用匿名类,匿名类的可读性比Lambda好
方法引用优于Lambda表达式
使用时机
想使用Lambda表达式时
如果这时有对应的方法引用,使用方法引用更好。因为方法引用比Lambda更简洁
注意事项
- Lambda表达式的可读性比方法引用更好,毕竟多了参数名称。这时可以将Lambda改造成一个新方法中,然后再通过方法名引用
坚持使用标准的函数接口
使用时机
自己想声明函数接口的时候
标准的函数接口可以减少学习成本,是类库直接支持的,如果自己编写,别人还要看来你的实现。如果一个函数接口会为了某一个目的被广泛使用,这时可以考虑自己声明函数接口,函数接口名称可以提供一些描述信息,函数接口上还可以写文档
注意事项
- 模版方法本质上就是传递一段代码,使用继承或者传递代码实例都行,只不过传递实例更好
- 不要用带包装类型的基础函数接口来代替基本函数接口。因为基本类型优于装箱基本类型,装箱基本类型的拆箱和装箱会带来严重的性能问题
- 当自己编写函数接口时,记得使用@FunctionalInterface注解,这样可以让编译器帮忙检查自己的函数接口写的对不对
- 不要使用重载,重载会使代码的可读性变差
谨慎使用Stream
使用时机
编写某些特别很难理解的流操作时
过分使用流会导致代码非常难理解,这时可以把流操作放进一个函数中,用非流的方式来实现。总结来说就是只使用简单的流操作
注意事项
- 流非常依赖函数对象,但函数对象比起代码块有很多限制,代码块可以读取和修改范围内的任意局部变量,而函数对象则只能读取final变量,并且不能修改任何局部变量;代码块可以从外围方法return,break和continue到外围循环,或者抛出异常,函数对象则办不到
优先选择Stream中无副作用的函数
使用时机
使用Steam进行编程的时候
Steam里面的函数最好是纯函数
注意事项
- 纯函数是指结果只取决于输入函数:它不依赖任何可变的状态,也不更新任何状态
- forEach操作应该只用于报告Stream计算的结果,而不是执行计算
- 方法引用也可以进行转型,可以转型成该方法实现的函数接口
Stream要优先用Collection作为返回类型
使用时机
需要流返回元素序列时
因为Collection接口是Iterable的子接口,并且有Stream方法,可以同时提供迭代和流。如果集合元素很少,可以放入集合中,可以考虑返回具体实现类,例如ArrayList,HashSet。如果不能返回一个自己实现的特殊集合
如果返回元素不能作为Collection返回,例如不好实现Contains和Size方法,这时可以返回Iterable,最好也实现一个Stream
注意事项
- 如果不使用流,返回序列的方法通常返回的是集合接口Collection;如果序列不需要进行Collection操作,可以返回Iterable;如果返回类型时基本数据类型,或则对性能要求很高,可以返回数组
- 实现了Iterable接口才能使用foreach循环
谨慎使用Stream并行
使用时机
任何时候都不要使用Stream中的并行
Stream中的并行流编写非常复杂,很难编写对
注意事项
- 如果不得不编写并行流来提高性能,元素个数乘以每个元素需要执行的代码行数大于10万,这时并行流才可能带来性能上的提升