第二章:创建和销毁对象
第一条:用静态工厂方法代替构造器
使用静态工厂方法的好处:
- 静态工厂方法有名称,当我们使用构造器时,即使使用参数不同的多个构造器,其名称一致,易混乱,而静态工厂方法可以是我们自定不同的名称。
- 不必每次调用时都新建一个对象,提高了性能,类似享元模式中将重复使用的部分单独放在一起。
- 返回原返回类型的任何子类型,在选择返回类时更有灵活性。
- 返回类型可以随着参数便会而变化,返回类型为原类型的子类型。
- 方法所返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在,即返回的类可以是父类,具体实现由子类承担。
//四大组成之一:服务接口
public interface LoginService {//这是一个登录服务
public void login();
}
//四大组成之二:服务提供者接口
public interface Provider {//登录服务的提供者。通俗点说就是:通过这个newLoginService()可以获得一个服务。
public LoginService newLoginService();
}
/**
* 这是一个服务管理器,里面包含了四大组成中的三和四
* 解释:通过注册将 服务提供者 加入map,然后通过一个静态工厂方法 getService(String name) 返回不同的服务。
*/
public class ServiceManager {
private static final Map<String, Provider> providers = new HashMap<String, Provider>();//map,保存了注册的服务
private ServiceManager() {
}
//四大组成之三:提供者注册API (其实很简单,就是注册一下服务提供者)
public static void registerProvider(String name, Provider provider) {
providers.put(name, provider);
}
//四大组成之四:服务访问API (客户端只需要传递一个name参数,系统会去匹配服务提供者,然后提供服务) (静态工厂方法)
public static LoginService getService(String name) {
Provider provider = providers.get(name);
if (provider == null) {
throw new IllegalArgumentException("No provider registered with name=" + name);
}
return provider.newLoginService();
}
}
在组成四中,可以通过实现具体的服务进行提供服务,但在此前,该服务的类可以不存在
第二条:遇到多个构造器参数时要考虑使用构建器
在集合框架中常常能看到一个类拥有几个构造器,提供了可选参数,这是重叠构造器模式,但是随着参数的增加,其构造难度和阅读难度也在增加。第二种办法,JavaBeans模式,先构造无参构造,在通过setter设值方法设置参数,问题在于,当我可以通过设值方法改变参数,会带来安全问题,第三种方法建造者模式,通过内部类,将参数的设值与对象的创建,放到内部类中。
第三条:用私有构造器或者枚举类型强化Singleton属性
Singleton指仅被实例化一次的类,实现中的两种方法,保持构造器私有,第一种提供字段返回,如:
public static final Object xxx = new Object();
第二种通过静态工厂方法返回,在序列化过程中加上readResolve方法,保证单例。 最后一种方法是使用单元素的枚举类型。
第四条:通过私有构造器强化不可实例化的能力
部分工具类,通过类方法提供服务,其实例化是无必要的,但是编译器会自动提供无参构造。当提供一个私有构造器时,它便不能被实例化(其实还可以通过反射来实例)。副作用:这样的类无法被子类化,因为子类会调用超类的构造器。
第五条:优先考虑依赖注入来引用资源
当某个类依赖多种资源时,不要使用单例模式或静态工具类,这样会将资源固定,无法改变。应有的做法是,使用构造器传入资源,这是依赖注入,但是依赖注入会出现问题,如果由大量嵌套的依赖这就会使项目十分混乱。
第六条:避免创建不必要的对象
我们创建对象时首先考虑静态工厂方法而不是构造器。有条件使用不可变对象,就使用不可变对象,提高重用减少资源的浪费。优先考虑基本数据类型而不是装箱基本类型,防止出现大量自动装箱拆箱的情况
第七条:消除过期的对象的引用
java的内存回收机制使用的可达性算法,当一个对象不被其他对象引用时才进行标记,所以当一个过期的对象引用存在于在使用的对象中,那么它将不被回收,当大量过期对象未被回收可能会出现OOM的去情况,即分配的内存被对象占满。
第八条:避免使用终结方法和清除方法
- 终结方法和清除方法的缺点在于不能保证会被及时执行
- 终结方法和清除方法有严重的性能缺失,运行速度慢于AutoCloseable
- 终结方法有严重的安全问题
第九条:try-with-resources优先于try-finally
try-finally首先是当我们使用多个资源时,代码混乱的问题,第二个问题是try和finally块中的代码都会抛出异常,而且后面的异常会抹除第一个异常。而try-with-resources中则不仅使代码更加简洁而且能够保留第一个异常,可以通过getSuppressed访问到被禁止的异常。
public class FileTest implements AutoCloseable{
public static void main(String[] args) {
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(new File("test.txt")))){
System.out.println(inputStream.markSupported());
}catch (Exception e){
System.out.println(e.getSuppressed());
}
}
@Override public void close() throws Exception {
System.out.println("文件被关闭");
}
}
第三章:对于所有对象都通用的方法
第十条:覆盖equals时请遵守的通用约定
1.每一个类实例对象都是唯一的。 2.如果对象没有比较是否相等的必要,就不用重写equals方法,如java.util.regex.Pattern就没必要。 3.父类重写的equals方法已经合适使用了,就不需要子类再重写。如AbstractMap。 4.如果类是private或者package-private的访问权限,则没有必要暴露equals方法,也就不用重写。 所以value classes(也就是需要比较类对象内容的类)适合重写equals方法,重写equals方法应该满足以下特性: 1.自反性,即x.equals(x)==true。 2.对称性,即如果x.equals(y)==y.equals(x)。 3.传递性,即如果x.equals(y)==y.equals(z)==true,则x.equals(z)==true。 4.一致性,即如果x.equals(y)==true,x,y值不改变的话,x.equals(y)==true一直成立。 5.如果x是不为null的引用对象,则x.equals(null)==false。
第十一条:覆盖equals时总要覆盖hashCode
如果两个对象进行equals是相等地,那么hashcode也应该相等,当两者equals不相等,则hashcode不一定不相等(hash碰撞),在hashmap和hashset中,将两个equals相等的对象放入,那么应该出现覆盖的现象,但是当两者hashcode不等,那么两者会同时存在于hashmap或hashset中,违反了hashset不重复的特性。
第十二条:始终要覆盖toString方法
打印对象是,如果对象没有覆盖toString方法,那么打印出来的会是一个编码,而覆盖toString方法,会将toString打印出来。
第十三条:谨慎地覆盖clone
在Object中的clone方法是受保护的,当类实现了Cloneable接口
protected native Object clone() throws CloneNotSupportedException;
clone的约定的内容:
x.clone() != x;
x.clone().getClass() == x.getClass();
//非必须
x.clone().equals(x);
返回的对象不应该依赖于被克隆的对象 调用super.clone实现浅拷贝,浅拷贝中,克隆的对象的域等同于被克隆对象的域,如果域为不可变的那么不需要处理,但是为可变的,在改变其中克隆与被克隆的对象之一的可变数据时,两者都会发生改变。想要实现深拷贝要重写clone方法,将被克隆对象中的数据逐一拷贝进克隆对象。
- 不可变对象永远都不应该提供clone方法(没有必要提供)
- 正确创建克隆对象的约束条件,以防止破坏被克隆对象
- clone禁止给final域赋新值
- 共有的clone对象应该省略throws声明
- 为继承设计类不应该实现cloneable接口
- 所有实现了cloneable的类都应该覆盖clone方法
- 对象克隆的更好方法其实是提供copy 构造方法或者 copy工厂方法
第十四条:考虑实现Comparable接口
Comparable用来给对象排序使用,实现该接口意味着类实例对象拥有一个天然的排序方式。当对象拥有排序的意义时,就可以考虑实现Comparable接口。这样对象可用在基于排序的集合中,如TreeSet,TreeMap。此外,在compareTo实现中,不要使用<,>来做比较,而应该继续调用比较对象(基础对象就用包装类的compare方法)的compare或compareTo方法。
第四章:类和接口
第十五条:使类和成员的可访问性最小化
- 尽可能地使类和成员不被外界访问
- 公有类的实例域决不能是公有的,包含公有可变域的类通常并不是线程安全的
- 公有的静态final指向了可变对象的引用会出错,其引用本身不可改变,但是引用对象是可变的,解决办法有将公有变为私有,通过方法返回转换后的列表或克隆对象。
第十六条:要在公有类而非公有域中使用访问方法
- 类可以在其包之外进行访问,就提供访问对象,暴露方法比暴露对象更有灵活性,因为暴露对象,在客户端改变困难。
第十七条:使可变性最小化
1.不提供外界方法修改对象的状态如:设值方法。 2.确保类不可以被继承,用final修饰,防止子类化。 3.所有的变量用final修饰。 4.所有的变量用private修饰。 5.确保不提供可变对象的访问方式。 不可变对象时线程安全的,可以被自由地共享,不可变对象无偿地提供了失败的原子性,不可变对象的缺点是,针对不同的值都需要一个不可变对象
第十八条:复合优先于继承
对于跨包边界的继承是危险的,继承破坏了封装性,子类的实现是依赖于超累的,而超类会随着版本改变而变化(除非不改变超类或谨慎改变超类),例如HashSet的实现是依托于Hashmap,如果HashMap发生改变,那么HashSet也要随着改变,如果我们要使用一个类中的方法,可以通过复合:即引入该类的实例。
第十九条:要么设计继承并提供文档说明,要么禁止继承
- 设计用于继承的类必须有文档用于说明它可覆盖的方法的自用性。
- 好的API文档应当描述一个给定方法做了什么工作,而非如何做。
- 构造方法决不能调用可被覆盖的方法,因为超类的构造方法会比子类更先调用,如果方法被覆盖掉,子类的方法将会在子类构造器运行前被调用。
- 无论是clone还是readObject,都不可以调用可覆盖方法,因为两者在行为上类似构造器
第二十条:接口优先于抽象类
一个类可以实现多个接口,但只能继承一个类,接口允许非层次结构的框架类型,对于接口中的一些公共方法可以使用骨架实现类,如AbstractSet,这样公共的方法可以在子类中不用重复实现。
第二十一条:为后代设计接口
在java8中,增加了缺省方法构造,目的是允许给现有的接口添加方法,但是给现有的接口添加方法是充满风险的。
第二十二条:接口只用于定义类型
- 接口常量是对接口的不良使用,常量应当被添加到类中,使用不可实例化的工具类来导出常量,当大量导出常量,可以使用静态导入,避免类名修饰常量名。
第二十三条:类层次优于标签类
- 标签类将多个类的实现糅杂在一起,如将rectangle和circle的实现放在figure类中,标签类容易冗长,容易出错
- 标签类是对类层次的一种效仿,类层次将共同的方法剥离出来作为抽象类的方法。
第二十四条:静态成员类优于非静态成员类
非静态成员类的每个实例都隐含地与外围类地一个外围实例相关联,在没有外围实例地情况下无法创建非静态成员类的实例,且每个实例都包含一个指向外围对象的引用,在可达性算法中可能会引发内存泄漏。
第二十五条:限制源文件为单个顶级类
把顶级类分别放入独立的源文件,如果要把多个顶级类放入一个源文件中,考虑使用静态成员类。如果一个文件名中包含多个顶级类,但是java文件名仅为其中一个顶级类的名称,可能会出现不同文件中出现同名类情况。
第五章:泛型
第二十六条:请不要使用原生类型
什么是原生类型如list,set即不带任何实际类型参数的泛型名称,其主要是为了与泛型出现之前的代码相兼容,如果使用原生类型就失掉了泛型在安全性和描述性方面的所有优势,原生类型逃避掉了泛型检查,安全替代原生类型的做法是使用无限制的通配符类型。
第二十七条:效处非受检的警告
尽可能地消除每一个非受检警告,如果无法效处警告,且确认引起警告的代码是安全的,可以使用@SuppressWarnings("unchecked")来禁止警告,始终在尽可能小的范围内来使用@SuppressWarnings("unchecked")
第二十八条:列表由于数组
当类型发生矛盾时(规定类型与输入的类型不同,如Long数组中放入String),数组在运行时报错,而列表则可以在编译时发现错误。数组是具体化的,而泛型是在编译时强化类型信息,在运行时擦除。
第二十九条:优先考虑泛型
当参数的类型不止一种时,如我们希望的到一个stack,但是栈中的数据可以进行设定,我们可以使用Object,然后进行类型转换,另外一种方法时使用泛型。但是创建泛型数组是非法的,这时,我们先使用Object数组,然后转换为泛型数组类型。
第三十条:优先考虑泛型方法
是指用泛型类型修饰方法的形式参数和返回值,同样可以避免方法中的类型强转。
第三十一条:利用有限通配符来提升API的灵活性
泛型之间如List<Object>与List<String>不同于Obejct与String有子类关系,这是有限制的通配符能够解决值类型的问题。 1.上边界通配符,形式:<? extends E>,表示是E或者E的子类型。 2.下边界通配符,形式:<? super E>,表示是E或者E的父类型。
第三十二条:谨慎并用泛型和可变参数
可变参数:参数的数量不定,当调用一个可变参数方法时,会创建一个数组来保存可变参数。 在以下条件下泛型可变参数是安全的:
- 他没有在可变参数中保存任何值。
- 他没有对不被信任的代码开放该数组。
第三十三条:有限考虑类型安全的异构容器
在集合中使用泛型时,每个容器只能有固定数目的参数类型,如Set<E>只有一个类型参数,map有两个类型参数,但是我们希望容器中能够存储多种类型,这是使用异构容器,将键进行参数化,而不将容器进行参数化,如:Map<Class<?>,Object>。
枚举和注解
第三十四条:用enum代替int常量
int常量的表达int值,这两两个名称不同的int常量但是int值相等,那么两者便是相等的,而且int值一旦发生改变会破坏客户端,还有String常量使用因编码到客户端代码,一旦出现书写错误也会报错。解决办法是使用枚举类型。
- 枚举类型保证了编译时的类型安全
- 包含同名常量的多个枚举类型可以在一个系统中和平共处 枚举类是继承Enum类实现的,枚举对象是public static final定义的(枚举对象可以是枚举类的子类实现)。同时枚举类构造方法是私有的,外界没有办法创建枚举实例,Enum类序列化相关方法会抛出异常,也就无法通过序列化创建出新的枚举对象。所以枚举对象是天然的不可变单例对象
第三十五条:用实例域代替序数
序数指枚举的实例在枚举中的数字位置,使用ordinal()得到,但是如果枚举中的实例位置改变,那么将会波坏客户端,结合觉办法是使用实例域。
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6),SEPTET(7), OCTET(8),DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
第三十六条:用EnumSet代替位域
位域:例如在读锁和写锁中,用一个32位的数,前十六位和后十六位分别存储读锁和写锁的重入数,位域是将二进制的数每一位赋予意义。但是使用位域时,我们要考虑位域的长度来指定数据类型,而且翻译位域是一件比较困难的工作,要将十进制数转化为二进制。而EnumSet中可以从单个枚举类型中提取多个值。
第三十七条:用EnumMap代替序数索引
当访问一个按照枚举的序数进行索引的数组时,使用正确的int必须由我们来保证。而EnumMap则避免了这一现象,有底层保证索引的正确。
第三十八条:用接口模拟可扩展的枚举
因为枚举类默认继承Enum类,可实现多个接口来扩展枚举对象的方法,枚举类型时不可扩展的,但是接口类型是可扩展的。
第三十九条:注解优先于命名模式
命名模式:如在java4发行前,JUnit测试框架原本要求用户一定要用test作为测试方法名称的开头。
命名模式的缺点:
1.文字拼写错误导致失败,测试方法没有执行,也没有报错
2.无法确保它们只用于相应的程序元素上,如希望一个类的所有方法被测试,把类命名为test开头,但JUnit不支持类级的测试,只在test开头的方法中生效
3.没有提供将参数值与程序元素关联起来的好方法。
第四十条:坚持使用Override注解
当我们没有加上@override注解时,如果出现覆盖出错,如:参数错误,变成重载。在覆盖方法时,编译不会出错,但是调用该方法时会发现调用的是没有被覆盖的方法。
第四十一条:用标记接口定义类型
标记接口:是不包含方法声明的接口,它只是标记了一个类实现了某种具有属性的接口,如Serializable表明该类可被序列化。 标记接口的存在允许在编译时就能捕捉到在使用标记注解的情况下在运行时才能捕捉到的错误。
第七章:Lambda和Stream
第四十二条:Lambda优先于匿名类
Lambda相比于匿名类更加简洁,其利用类型推导,省下了一部分代码,对于lambda而言一行代码最理想,最多不能超过三行,如果超过了可读性会下降,在lambda中无法获取到自身的引用,其this是指向外围实例。
第四十三条:方法引用优先于Lambda
方法引用比Lambda更加简洁,但是部分情况下,如:类名很长的情况下lambda更加简洁。
第四十四条:坚持使用标准的函数接口
只要标准的函数接口能够满足需求就应该优先考虑标准的函数接口,而不是再专门构建一个新的函数接口,千万不要用带包装类型的基础函数接口来代替基本函数接口。
第四十五条:谨慎使用Stream
Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但仅仅因为可以,并不意味着应该这样做。如果使用得当,流可以使程序更短更清晰;如果使用不当,它们会使程序难以阅读和维护。 流可以做的是:
- 统一转换元素序列
- 过滤元素序列
- 使用单个操作组合元素序列(例如求和、连接或计算最小值)
- 将元素序列累积到一个集合中,可能通过一些公共属性将它们分组
- 在元素序列中搜索满足某些条件的元素
第四十六条:优先选择Stream中无副作用的函数
纯函数的结果取决于其输入:它不依赖于任何可变状态,也不更新任何状态。为了实现这一点,你传递给流操作的任何函数对象(中间或终端)都应该没有副作用。 这样在流处理的过程中,每阶段的处理结果只依赖于它的前一阶段的输入结果。 同时不要使用forEach做流展示的运算。在做流的collect操作时,可通过Collectors实现一系列的集合操作,toList,toMap,groupingBy等。
第四十七条:Stream要优先用Collection作为返回类型
stream无法扩展iterable接口,所以stream无法使用for-each遍历stream,解决办法是使用一个适配器的到stream的iterator返回的iterable。collection接口是iterable的一个子类型,可以通过stream方法转化为流,对于公共的,返回序列的方法,collection或其适当的子类型通常是最佳的返回类型。 如果返回的元素较少可以返回一个标准的集合。 但是一旦元素的数量较多,就使用一个指定集合的幂集 如果无法返回集合,就返回stream或iterable
第四十八条:谨慎使用Stream并行
尽量不要并行stream pipeline,除非有足够的理由相信它能保证计算的正确性,并且能够加快程序的运行速度,使用并行时一定要对使用前后的速度和性能进行测试。 如果源来自Stream.iterate方法,或者使用中间操作limit方法,并行化管道也不太可能提高其性能,并行性带来的性能收益在ArrayList、HashMap、HashSet和ConcurrentHashMap实例、数组、int类型范围和long类型的范围的流上最好。这些数据结构的共同之处在于,它们都可以精确而廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很容易。
第八章:方法
第四十九条:检查参数的有效性
大部分方法和构造器都对参数值有限制,比如不能为负数,或者不能超过一定的范围,如果我们有检查有效性在这一关,那么再执行时会清晰地知道该错误,如果不检查可能会出现,在该方法内返回错误的数据导致其他方法失败,失败的源头很难找到,在编写方法的文档是要清晰地使用@throws标签表明错误,还有对于null我们要进行检测,使用Object.requireNonNull检查。
第五十条:必要时进行保护性拷贝
假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性的设计程序保护性的设计程序。例如:当参数是可变时,可以通过输入参数后,对参数进行变换,这就会对方法内部进行破坏,因为构造函数检测完有效性后,会认为该参数有效,但是将参数改变为无效后,再调用方法会出现问题,所以要对输入地参数进行保护性拷贝,然后再进行有效性检查,注,不要调用参数对象本身地clone进行拷贝,而是使用构造器,因为恶意的客户端很可能改变clone方法的返回。同时返回方法也不要将对象内的值的引用暴露出去,而是暴露拷贝对象。
第五十一条:谨慎设计方法签名
1.选取合适的方法名,易懂且具有包,类的共识一致性。 2.尽量使方法具有灵活通用性。 3.避免数量过多的方法参数,尽量不超过4个。可考虑拆成多个方法,创建帮助类,使用Builder模式等解决。 4.方法参数最好采用接口类型,而不是具体实现类类型。 5.使用两个元素的枚举类型作为对象,而不是boolean参数。前者方便扩展。
第五十二条:慎用重载
重载方法的选择在编译时,而覆盖方法的选择再运行。保守而安全的策略是永远不要导出两个具有相同参数数目的重载方法,每一对重载方法,至少有一个对应的参数在两个重载方法中具有两个根本不同的类型。
第五十三条:慎用可变参数
使用可变参数时小心出现无参的情况出现,而我们在方法中又需要至少一个参数。 在注重性能的情况下,可变参数会导致一次数组的分配和初始化。
第五十四条:返回零长度的数组或者集合,而不是null
如果要返回null,那么客户端在调用方法后要对返回值进行判断是否为null再进行处理,这样处理比较繁琐,且易出错。
第五十五条:谨慎返回optional
在特定环境下无法返回任何值的方法时,要么抛出异常,要么返回null,抛出异常的开销很高,而返回null则要进行处理。 在java8中,有第三种方法可以编写不能返回值的方法就是使用Optional<T> 1.不要再返回Optional的方法中返回null。 2.Optional<T>中T 不应该为容器类型:比如collections,maps,streams,arrays和optionals,不应该在包着一层Optional。 3.Optional<T>中T 不应该为包装类型,如Long等。 4.几乎也不把Optional<T> 用在集合和数组的key,value或者element上。 5.Optional<T>也不适合当成员变量。 6.严格考虑性能的方法,还是返回null或者抛异常吧。
第五十六条:为所有导出的API元素编写文档注释
添加文档注释规范: 一、为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。 二、方法的文档注释应该简洁的描述出它和客户端之间的约定。这个约定应该说明这个方法做了什么,而不是说明它是如何完成这项工作的。文档注释应该列举如下内容: 1.前提条件(precondition) 前提条件是指为了使客户能够调用这个方法,而必须满足的条件; 2.后置条件(postcondition) 所谓后置条件是指在调用成功完成之后,哪些条件必须要满足; 3.副作用(side effect) 副作用是指系统状态中可以观察到的变化,它不是为了获得后置条件而明确要求的变化; 4.类或者方法的线程安全性(thread safety当一个类的实例或者静态方法被并发使用时,这个类行为的并发情况。
第九章:通用编程
第五十七条:将局部变量的作用域最小化
- 要是局部变量的作用域最小化,最有力的方法是在第一次要使用它的地方声明
- for循环优先于while循环
- 使方法小而集中
第五十八条:for-each循环优先于传统的for循环
传统for循环,如果没有设置好循环次数,可能出现outofbound的问题。 有三种常见的情况无法使用for-each循环: 过滤——如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用他的remove方法。 转换——如果需要遍历列表或者数组,并取代他部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。 平行迭代——如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
第五十九条:了解和使用类库
第六十条:如果需要精确的答案,请避免使用float和double
float和double不能精确地表示0.1(或10的任何其他负数次方值),精确计算使用 BigDecimal 、 int 和long。
第六十一条:基本类型优先于装箱基本类型
装箱基本类型在装箱与拆箱过程中会使用大量的资源,同时如果不小心使用到装箱基本类型和基本类型转换会涉及到装箱与拆箱。对装箱基本类型使用==操作符几乎总是false即使两者的值等。
第六十二条:如果其他类型更适合,则尽量避免使用字符串
- 字符串不适合替代枚举类型
- 字符串不适合替代聚合类型,应当编写一个类来描述数据集
- 字符串不适合代替能力表,当字符串被用于对功能进行授权访问,但是字符串并不能保证唯一。
第六十三条:了解字符串连接的性能
为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间,这是因为字符串不可变,每次的连接要进行拷贝,使用StringBuilder代替String。
第六十四条:通过接口引用对象
使用接口而不是类作为参数类型,使用接口作为类型,程序会更加灵活,如果没有合适的接口,就用类层次结构中提供了必要功能的最小具体类来引用对象
第六十五条:接口优先于反射机制
反射允许一个类使用另一个类,即使当前类被编译时,后者还不存在。然而这种能力需要付出代价: 1.丧失了编译时类型检查的好处,包括异常检查。如果程序企图通过反射机制调用不存在的或者不可访问的方法,编译时没有错误,但是运行时它将会失败。 2.执行反射访问所需要的代码非常笨拙和冗长。阅读困难,编写乏味。 3.性能损失。 当程序必须用到的类在编译时不可用,但是在编译意识存在适当的接口或超类,可以通过反射创建实例,然后通过接口或超类,以正常方式访问这些实例。
第六十六条:谨慎地使用本地方法
使用本地方法来提高性能地做法不值得提倡,本地方法不是安全的,使用本地方法地程序也不再时可自由移植地。
第六十七条:谨慎地进行优化
优化的弊大于利,要努力编写好的程序而不是块的程序,在每次试图做优化之前和之后,要对性能进行测量。
第六十八条:遵守普遍接受的命名规则
包和模块的名称应该是层次的,每个部分包括小写字母,极少数情况下有数字。 类和接口的名称都应该包括一个或者多个单词,每个单词的首字母大写。 方法和域的名称与类和接口的名称一样,但是首字母小写。 常量域名称应该包含一个或多个大写的单词,中间用下划线隔开 Package/module:全小写(com.junit.jupiter) Class/Interface:驼峰命名(Stream,HashMap) Method/Filed:首字母小写,驼峰命名(remove.getCrc) Constant Filed:全大写,单词用下划线隔开(MIN_VALUE) Local Variable:首字母小写,驼峰命名(i,houseName) Type Parameter:大写单字母(T,E)
第十章:异常
第六十九条:只针对异常的情况才使用异常
一,因为异常机制的设计初衷使用于不正常的情形,所以很少会有JVM实现试图对它们进行优化,使得与显式的测试一样快速。 二,把代码放在try-cathch块中反而阻止了现代JVM实现本来可能要执行的某些特定优化。 三,对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉。 异常是为了异常情况下使用而设计的,不要用于普通控制流。
第七十条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。用运行时异常表明编程错误。
第七十一条:避免不必要地使用受检的异常
受检异常强迫程序员处理异常地条件,抛出受检异常地方法不能直接在Stream中使用,消除受检异常地方法是,返回所要地结果类型的一个optional。
第七十二条:优先使用标准的异常
标准的异常与程序员已经熟悉的习惯用法一致,可读性更好。
第七十三条:抛出与抽象相对应的异常
使用异常转义
try {
... //调用其他低级方法
}catch (LowerLevelException e){
throw new HigherLevelException(...);
}
第七十四条:每个方法抛出的异常都要有文档
使用javadoc的@throws标签,准确地记录下抛出每个异常地条件。 使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。 如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是为每个方法单独建立文档。
第七十五条:在细节消息中包含能捕获失败的信息
为了分析异常,我们捕获的异常细节应该是能分析出异常原因的数据。比如,IndexOutOfBoundsException异常的细节消息应该包含下界、上界以及没有落在界内的下标值,这三个值任何一个出错,程序就可能会错,尤其是三个参数都有问题。
第七十六条:努力使失败保持原子性
失败地方法调用应该使对象保持在被调用之前地状态。
第七十七条:不要忽略异常
调用catch后要对异常进行处理,不然要说明为何不处理。
第十一章:并发
第七十八条:同步访问共享的可变数据
同步可以阻止一个线程看到对象处于不一致地状态,还可以保证进入同步方法或者同步代码块地每个线程,都能看到由同一个锁保护的之前的所有修改效果。 同步可以通过synchronized,volatile修饰变量,当其修改时通知其他线程,还有atomic包下的类可以通过CAS实现同步。
第七十九条:避免过度同步
在同步区域类尽可能做少的工作,为了避免死锁和数据破坏,不要从同步区域内部调用外来方法。
第八十条:executor和task和stream优先于线程
使用线程池能够节约资源,提高效率。
第八十一条:并发工具优先于wait和notify
第八十二条:线程安全性的文档化
- 不可变的 — 这个类的实例看起来是常量。不需要外部同步。示例包括 String、Long 和 BigInteger。
- 无条件线程安全 — 该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。
- 有条件的线程安全 — 与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括 Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。
- 非线程安全 — 该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。
- 线程对立 — 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。generateSerialNumber 方法在没有内部同步的情况下是线程对立的。 在文档中记录一个有条件的线程安全类需要小心。你必须指出哪些调用序列需要外部同步,以及执行这些序列必须获得哪些锁(在极少数情况下是锁)。
第八十三条:慎用延迟初始化
延迟初始化用于域只在类的实例部分被访问,但是初始化的开销很高,可能就值得延迟初始化。 在多个线程是,延迟初始化需要一定的技巧,在大多数情况下,正常的初始化优于延迟初始化。 使用延迟初始化,考虑双重检验模式
第八十四条:不要依赖于线程调度器
任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。 不要企图通过调用Thread.yield开修正程序,线程优先级是java平台上最不可移植的特征。
第十二章:序列化
第八十五条:其他方法优先于java序列化
序列化的根本问题在于,其攻击面过于庞大,无法进行防护,在反序列化过程中是一个明显存在风险的行为,每当我们反序列化一个不受新人的字节流都要试着攻击他。
第八十六条:谨慎地实现Serializable接口
- 一旦一个类实现了Serializable接口被发布,就大大降低了改变这个类的实现的灵活性。
- 增加了出现bug和安全漏洞的可能性。
- 随着类发行新的版本,相关的测试负担也会加重。
- 内部类不应该实现Serializable接口
第八十七条:考虑使用自定义的序列化形式
当一个对象的逻辑内容和物理表示法等同,可以考虑默认的序列化形式。 但是有区别时会有缺点:
- 它使这个类导出的API永远地束缚在该类的内部表示法上 。
- 它会消耗过多的空间 。
- 它会消耗过多的时间。
- 它会引起栈溢出 。
第八十八条:保护性地编写readObject方法
readObject方法相当于一个公有的构造器,构造器必须进行参数有效性检查与保护性拷贝。
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
- 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
- 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。
第八十九条:对于实例控制,枚举类型优先于readResolve
任何一个readObject不管显式或默认的都会返回一个新建的实例,但是readResolve可以允许该方法返回的对象取代readObject新建的对象。如果使用readResolve进行实例控制就要将对象引用类型的所有实例域设置为transient。这是由于当对象包含一个非瞬时的对象引用域,这个域的内容可以在对象的readResolve方法运行前被反序列化。将可序列化的实例受控的类编写成枚举,java就可以绝对保证除了所声明的常量之外,不会有其他实例
第九十条:考虑用序列化代理序列化实例
为可序列化的类设计一个私有静态嵌套类来表示外围类的实例的状态,然后用writeplace方法返回该嵌套类,这样就能使序列化时产生一个序列化代理实例,代替外围类实例。