读书笔记,如有问题,请多多指教。
目录
第九条 try-with-resources优先于try-finally
第四十七条 Stream要优先用Collection作为返回类型
第六十条 如果需要精确的答案,请避免使用float和double
第七十条 对可恢复的情况使用受检异常,对编程错误使用运行时异常
第八十条 executor,task,stream优先于线程
第八十九条 对于实例控制,枚举类型优先于readResolve
第一章 引言
本文用来记录Effective Java学习过程中的各个知识点,受此时能力所限,有些点可能讲的不太清楚,或者理解有错误的地方,请多多指教!
第二章 创建和销毁对象
第一条 用静态工厂方法代替构造器
可参考文章:https://blog.csdn.net/u014129886/article/details/89670049
总而言之,静态工厂方法和公有构造器各有各的用处,我们需要理解他们各自的长处,静态工厂经常更加合适。
此处的静态工厂方法与设计模式中的工厂模式不同,这里是一种构造方法,它是一个返回类实例的静态方法。
静态工厂方法有五大优点:
1.相比于构造器,它们有名称,因为构造器是没有名字的。
2.不必再每次调用的时候都创建一个新的对象,比如单例模式饿汉式里的getInstance方法。
3.返回对象可以是返回类型的任何子类对象,这样变得更加灵活。
4.所返回的对象的类可以随着每次的调用而发生变化,这取决于方法的参数值。
5.方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。理解为返回类型可以为一个接口,它的子类都可以返回,但是此时就算没有创建也是可以的,例如服务提供者框架。
静态工厂方法的两大缺点:
1.如果静态工厂方法所属的类中不含有公有或者受保护的构造器,就不能被继承了,但是这样,我们却可以使用复合的方式,也就是装饰者模式。
2.在API文档中它们没有像构造器那样标识出来,所以程序员很难发现它们,所以必须建立一些惯用的约定名称。
from 类型转换方法,它只有单个参数,返回该类型的一个实例,并把它们合并起来。
of 聚合方法,带有多个参数,返回该类型的一个实例,把他们合并起来。
valueOf 比from和of更繁琐的一种替代方法。
instance或者getInstance 返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值。
create或者newInstance 像instance或者getInstance一样,但create或者newInstance能够确保每次调用都返回一个新的实例
getType 像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
newType 像getInstance一样,但是在工厂方法处于不同的类中的时候使用。
type getType和newType的精简版。
第二条 遇到多个构造器参数时要考虑使用构建器
总而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是一种不错的选择,与重迭构造器相比,易于阅读,与javaBeans相比更加安全。
public class NutritionFacts {
//构建外部属性
private final int requiredParameter1;
private final int requiredParameter2;
private final int optionalParameter1;
private final int optionalParameter2;
private final int optionalParameter3;
public static class Builder{
//声明必要参数
private final int requiredParameter1;
private final int requiredParameter2;
//声明可选参数
private int optionalParameter1 = 0;
private int optionalParameter2 = 0;
private int optionalParameter3 = 0;
//可选参数初始化
public Builder(int requiredParameter1,int requiredParameter2){
this.requiredParameter1 = requiredParameter1;
this.requiredParameter2 = requiredParameter2;
}
public Builder optional1(int val){
optionalParameter1 = val;
return this;
}
public Builder optional2(int val){
optionalParameter2 = val;
return this;
}
public Builder optional3(int val){
optionalParameter3 = val;
return this;
}
//调用build方法创建外部对象
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder){
requiredParameter1 = builder.requiredParameter1;
requiredParameter2 = builder.requiredParameter2;
optionalParameter1 = builder.optionalParameter1;
optionalParameter2 = builder.optionalParameter2;
optionalParameter3 = builder.optionalParameter3;
}
}
public class Main {
public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts.Builder(11,11)
.optional1(11)
.optional2(11)
.optional3(11)
.build();
}
}
第三条 用私有构造器或者枚举类型强化Singleton属性
第四条 通过私有构造器强化不可实例化的能力
总而言之,第三条和第四条说的是将构造器私有化,可以强化singleton属性,使其不能被实例化。
第五条 优先考虑依赖注入来引用资源
总而言之,不要用singleton和静态工具类来实现依赖一个或多个底层资源的类,该资源的行为会影响到该类的行为,也不要直接用这个类来创建这些资源。推荐使用依赖注入的方式,将资源传给构造器,通过它们来创建类,大大提升了类的灵活性和资源的重用性。
第六条 避免创建不必要的对象
总而言之,这一条是显而易见的,当然不要创建没有必要的对象,关键在于在什么场景下,我们会不经意的创建出没有必要的对象。
下面有三个例子:
1.String s = new String("abc");里面的“abc”本身就是一个实例,然后通过new又创建一个实例,如果它在循环里,将会产生大量不必要的对象,正确的做法应该是String s = "abc";
2.String.matches方法,虽然这个方法最容易看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用。问题在于,它的内部为正则表达式创建了一个pattern实例,却只用了一次,这个对象成本很高,不易这样做。应该显示的将正则表达式编译成一个pattern实例。
3.自动装箱使得基本类型和包装基本类型之间变得模糊起来,但是并没有完全消除,要优先使用基本类型而不是包装基本类型,当心无意识的自动装箱。
第七条 消除过期的对象引用
总而言之,对象引用很容易引发内存泄漏问题,比如threadlocal类等等。
下面给出几点注意(目前还没怎么接触过,接触到的目前只有threadlocal)
1.只要类是自己管理内存,程序员就应该警惕内存泄漏的问题。
2.内存泄漏的一个常见来源是缓存。
3.内存泄漏的另一个常见来源是监听器和其它回调。
第八条 避免使用终结方法和消除方法
总而言之,尽量不要使用终结方法或者清除方法。
终结方法finalizer通常是不可预测的,也是危险的,一般情况下是不必要的。
清楚方法cleaner没有终结方法那么危险,但是仍然是不可预测的,十分缓慢的,一般情况下是不必要的。
第九条 try-with-resources优先于try-finally
总而言之,在处理必须关闭的资源时,始终要优先考虑try-with-resources,而不是try-finally,这样得到的代码更加简洁,清晰,产生的异常也更有价值,它会自动关闭资源,并且会禁止close的异常,便于调试。
格式为:
//需要关闭的资源放到try的括号内
try(InputStream in = new FileInputStream("path")){
//其它处理处理操作
in.read();
}catch (IOException e){
e.printStackTrace();
}
第三章 对于所有对象都通用的方法
第十条 覆盖equals时请遵守通用约定
总而言之,不要轻易覆盖equals方法。
第十一条 覆盖equals时总要覆盖hashCode
总而言之,覆盖equals方法一定要覆盖hashcode方法。
第十二条 始终要覆盖toString
总而言之,要在你编写的每一个可实例化的类中覆盖Object的toString方法,除非已经在父类中做了,这样会便于调试。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。
无论是否决定指定格式,都应该在文档中表明你的意图。
第十三条 谨慎地覆盖clone
有待于研究
第十四条 考虑实现Comparable接口
总而言之,每当实现一个对排序有需求的类时,都应该让这个类实现Comparable接口,以便其实例可以轻松地被分类,搜索,以及用在基于比较的集合中。每当在CompareTo方法的实现中比较域值的时候,应避免使用><操作符,而应该使用compare方法,或在Comparator接口中使用比较器的构造方法。
有待于研究
第四章 类和接口
第十五条 使类和成员的可访问性最小化
总而言之,尽可能让每个类或者成员不被外界访问,使用尽可能小的访问级别。除了公有静态final域的特殊情形之外(此时充当常量),公有类不应该包含公有域,并且要确保公有静态final域引用的对象都是不可变的。
第十六条 要在公有类而非公有域中使用访问方法
总而言之,此条的意思可以理解为,将变量私有化,不对外暴露,并提供get和set方法。
第十七条 使可变性最小化
总而言之,不可变的类更加容易设计和使用,就算一个类不能设计成不可变的,也要使他的可变性最小化。
不可变对象的优点:
1.不可变对象简单。
2.不可变对象本质上是线程安全的,它们不要求同步。
3.不可变对象可以被自由的共享,甚至可以共享它们的内部信息。
4.不可变对象为其它对象提供了大量的构建。
5.不可变对象提供了失败的原子性。
不可变对象的唯一缺点:
对于每一个不同的值都要有一个单独的对象。(字符串)
我们应该怎么尽量做到不可变:
1.不提供set方法。
2.保证类不被继承和扩展。
3.声明的域都是final的。
4.声明的域都是私有的。
第十八条 复合优先于继承
总而言之,无法确定父类的自用性,随着发行版本的改变,十分危险,所以可以采用复合和转发,可以理解为装饰者模式。
第十九条 要么设计继承并提供文档说明,要么禁止继承
总而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明它的自用模式,并且一旦建立了文档,在这个类的整个生命周期中都要遵守。唯一测试是否有用的方法就是编写子类,必须在发布类之前编写子类对类进行测试。
第二十条 接口优先于抽象类
总而言之,接口避免了继承结构上的臃肿,并且,我们也可以使用骨架实现类作为实现类和接口的中间类,可以提供一些公共方法的实现,避免重复代码。
public abstract class middleClass implements Inteface{}
第二十一条 为后代设计接口
总而言之,尽管缺省方法已经成为了java平台的一部分,但谨慎设计接口仍然是至关重要的。缺省方法可以在现有接口上添加方法,有很大的风险,接口的现有实现不会出现编译错误或者警告,但是运行时却失败了。
interface HelloDefaultMethod {
void sayHello(String helloString);
default String helloString(String helloString) {
return "来自helloString缺省方法的问候语:" + helloString;
}
}
第二十二条 接口只用于定义类型
总而言之,接口只用于定义类型,而不应该导出常量,某些常量可以用枚举实现。
第二十三条 类层次优先于标签类
总而言之,多种类型标签集中于一个类之中,不如重构为抽象类,类层次继承的模式。
第二十四条 静态成员类优先于非静态成员类
有待于研究
第二十五条 限制源文件为单个顶级类
有待于研究
第五章 泛型
第二十六条 请不要使用原生态类型
总而言之,推荐使用泛型。
第二十七条 消除非受检的警告
总而言之,非受检警告很重要,不要忽略它们。每一条警告都可能在运行时抛出classCastException异常。应该最大可能的消除这些警告,如果无法消除,同时证明引起警告的代码是类型安全的,就可以在尽量小的范围内使用@SuppressWarnings(“unchecked”),并把原因记录下来。
第二十八条 列表优于数组
总而言之,数组和泛型不能很好的混合使用,第一反应应该是用列表代替数组。
泛型只在编译时强化它们的类型信息,并在运行时擦出它们的元素类型信息,属于java的语法糖。
第二十九条 优先考虑泛型
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型更加安全,更加容易,对于某些类型的新用户来说变得更加轻松,又不会破坏现有的客户端。
第三十条 优先考虑泛型方法
总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数的方法更加安全,更加容易。
第三十一条 利用有限制通配符来提升API的灵活性
有待于研究。
第三十二条 谨慎并用泛型和可变参数
有待于研究。
第三十三条 优先考虑类型安全的异构容器
有待于研究。
第六章 枚举和注解
第三十四条 用enum代替int常量
总而言之,与int常量相比,枚举类型的优势不言而喻,而且可读性嗯好,更加安全。
第三十五条 用实例域代替序数
总而言之,用 SOLO(1),DUET(2)代替SOLO,DUET,其中的1和2就代表实例域,用这个代替ordinal()方法,这个方法返回枚举类型在枚举中的序数,从0开始,如果顺序改变,维护十分困难。
第三十六条 用EnumSet代替位域
https://blog.csdn.net/moakun/article/details/80617845
有待于研究
第三十七条 用EnumMap代替序数索引
有待于研究
第三十八条 用接口模拟可扩展的枚举
有待于研究
第三十九条 注解优先于命名模式
总而言之,除了平台框架程序员外,大多数程序员都不必定义注解类型,但是所有的程序员都应该使用java平台所提供的注解类型。
命名模式是很久以前的一种规范,使用命名模式表明某些程序元素需要通过某种工具或者框架进行特殊处理,比如java4中的单元测试等。
第四十条 坚持使用Override注解
总而言之,如果你想要覆盖父类中的方法,那么就要用Override注解,编译器可以替你防止大量的错误,但有一个例外,在具体的类中,不必标注你确信覆盖了抽象类声明的方法,但做了也没有坏处。
第四十一条 用标记接口定义类型
总而言之,标记接口和标记注解各有各的好处。
标记接口:它是不包含方法声明的接口,比如Serializable接口,只用来表明是可序列化的。
标记注解:就是注解机制中的注解们。
标记接口的优点:
1.允许在编译期捕捉到的错误,而这个错误在使用标记注解要在运行时才可以捕捉到。
2.可以更精确的进行锁定。
标记接口的优点:
1.它是注解机制的一部分。
如果标记的是接口或者类,多考虑一下标记接口。
如果是方法什么的,可以考虑一下标记注解。
第七章 Lambda和Stream
第四十二条 Lambda优先于匿名类
总而言之,从java8开始,lambda就成了小函数对象的最佳方式,尽量就不要用匿名内部类了。
值得注意的两条:
1.删除所有lambda参数的类型,除非它们的存在能够使程序更加清晰。
2.lambda没有名称,如果一个计算本身不是自描述的或者超出了几行,就不要把它放在一个lambda中。
第四十三条 方法引用优先于Lambda
总而言之,方法引用和lambda哪个简洁清晰就用哪一个。
方法引用:
GoshThisClass::action
lambda:
() -> action()
第四十四条 坚持使用标准的函数接口
总而言之,java8已经提供了大量标准的函数接口,只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门构建一个新的函数接口。
什么是函数式接口?
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}
基本接口对对象引用类型进行操作。Operator接口表示结果和参数类型相同的函数。Predicate接口表示一个接收一个参数并返回布尔值的函数。Function接口表示其参数和返回类型不同的函数。Supplier接口表示不带参数并返回(或“提供”)值的函数。最后,Consumer表示一个函数,它接受一个参数并且什么都不返回,本质上消费它的参数(essentially consuming its argument)。6个基本功能接口总结如下:
Interface | Function Signature | Example |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger::add |
Predicate | boolean test(T t) | Collection::isEmpty |
Function<T,R> | R apply(T t) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
第四十五条 谨慎使用Stream
总而言之,滥用Stream会使得程序代码更难以读懂和维护,应该进行合理的使用。
Stream的应用场景:
1.避免利用Sream来处理char值。
2.统一转换元素的序列。
3.过滤元素的序列。
4.利用单个操作合并元素顺序。
5.将元素的序列存放到一个集合当中。
6.搜索满足条件的元素序列。
第四十六条 优先选择Stream中无副作用的函数
有待于研究,有待于熟练使用Stream
第四十七条 Stream要优先用Collection作为返回类型
总而言之,优先使用集合作为返回类型。
第四十八条 谨慎使用Stream并行
有待于研究
第八章 方法
第四十九条 检查参数的有效性
总而言之,在编写方法或者构造器的时候,应该考虑它的参数有哪些限制,应该把这些限制写到文档当中,并在开头出,通过显示的检查来实施这些限制。不光要进行参数校验,还要进行身份验证,防止某些攻击的产生。
第五十条 必要时进行保护性拷贝
总而言之,如果一个类包含有从客户端得到的或者返回客户端的可变组件,这个类就应该进行保护性拷贝,保护性拷贝应该在参数校验之前进行。如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组建,就可以在文档中指明客户端的职责不得修改受影响的组件,以此代替保护性拷贝。
例子(没有进行保护性拷贝):
public final class Period{
private final Date start;
private final Date end;
public Period(Date start,Date end){
if(start.compareTo(end)>0){
throw new IllegalArgumentException("");
}
this.start=start;
this.end=end;
}
}
造成问题的代码:
Date start = new Date();
Date end = new Date();
Period p = new Period(start,end);
//没有进行保护性拷贝,上面创建的对象将会改变
end.setYear(78);
保护性拷贝的代码:
public Period(Date start,Date end){
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(start.compareTo(end)>0){
throw new IllegalArgumentException("");
}
}
第五十一条 谨慎设计方法签名
这里面给出了几条建议:
1.谨慎的选择方法的名称。
2.不要过于追求提供便利的方法,方法太多会使得类难以学习。
3.避免过长的参数列表。(1)可以将一个方法分解为多个方法。(2)创建辅助类来保存参数分组。(3)从对象构建到方法调用都采用builder模式。
4.对于参数类型,要优先使用接口而不是类。
5.对于boolean参数,要优先使用两个元素的枚举类型。
第五十二条 慎用重载
总而言之,能够重载方法并不意味着就应该重载方法。一般情况下具有相同参数数目的方法来说,应该避免重载,可以起不同的名称。在某些情况下,特别使构造器,遵循这一条几乎是不可能的,同样的,我们可以使用静态工厂方法,比如第一条。
原因:十分容易导致未知的错误。
第五十三条 慎用可变参数
总而言之,可变参数机制首先会创建一个数组,数组的大小为在调用未知所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,会有一些效率问题。所以我们可以这样做,当参数超过若干个的时候,是很少见的,先把少量参数的方法做了,当很多个参数出现的时候采用可变参数,也就是小概率会出现需要创建数组的情况。
public void test(){}
public void test(int a){}
public void test(int a,int b){}
public void test(int a,int b,int...rest){}
第五十四条 返回零长度的数组或者集合,而不是null
总而言之,永远不要返回null,而是要返回零长度的数组或者集合,这样很少会出错,其它调用的地方也很少出现NPE空指针异常。
第五十五条 谨慎返回optinal
总而言之,先记住有这样一个optinal类,用起来需要注意的地方很多。
java8之后一共有三种方法编写一个在特定环境下无法返回值的方法:
1.抛出异常,但抛出异常的代价很高。
2.返回null,容易出现NPE。
3.返回Optinal<T>,它代表一个不可变的容器,可以存在非空引用或者什么内容都没有,具体使用还有待研究。
第五十六条 为所有导出的API元素编写文档注释
为了正确的编写API文档,必须在每个被到处的类,接口,构造器,方法和域声明之前增加一个文档注释。
方法的文档注释应该简洁的描述出它和客户端之间的约定。
当为枚举类型编写文档时,要确保在文档中说明常量。
类或者静态方法是否线程安全,应该在文档中对它的线程安全级别进行说明。
第九章 通用编程
第五十七条 将局部变量的作用域最小化
总而言之,将局部变量的作用域最小化,可以增强代码的可读性,并降低出错的可能性。
下面给出几种方法注意:
1.最有力的方法就是在第一次使用它的地方进行声明,使用之前声明,这只会造成混乱。
2.几乎每一个局部变量的声明都应该有一个初始化的表达式,try-catch除外。
3.如果循环终止之后不在需要循环变量的内容,for优于while。
4.方法小而集中。
第五十八条 for-each循环优先于传统的for循环
总而言之,高级for循环比普通for循环更好。
高级for循环可以用来遍历数组或者集合,下面是它不能用的几种场景:
1.遍历集合并删除选定的元素。
2.遍历集合并替换里面的元素。
3.并行的遍历多个集合。
第五十九条 了解和使用类库
总而言之,不要重复造轮子。如果你要做的事情看起来十分常见,有可能类库中已经有某个类完成了这样的工作,可以直接利用现成的。
现在选择随机数生成器时,大多使用ThreadLocalRandom,他可以产生高质量的随机数,并且速度很快。
第六十条 如果需要精确的答案,请避免使用float和double
总而言之,对于任何需要精确答案的计算任务,比如货币计算,请不要使用float和double。如果数值范围没有超过9位十进制数字,就可以用int,如果不超过18位数字,就使用long,如果超过18位,就使用bigdecimal。
第六十一条 基本类型优先于装箱基本类型
总而言之,基本类型优先于装箱基本类型,更加简单快速。当作为集合的元素,建或者值的时候,就必须用装箱基本类型了。
值得注意的点:
1.当一项操作中混合使用基本类型和装箱基本类型的时候,装箱基本类型会自动拆箱,拆箱的过程可能发生空指针异常。
2.对装箱基本类型运用==操作符几乎总是错误的。
基本类型和装箱基本类型的主要区别:
1.基本类型只有值,装箱基本类型不光具有值,还有对象的同一性。
2.装箱基本类型还可能位null。
3.基本类型往往更快。
第六十二条 如果其它类型更适合,则尽量避免使用字符串
总而言之,如果可以使用更加合适的数据类型,就应该避免使用字符串来表示对象,如果使用不当,会比其它类型更加笨拙,速度更慢,更容易出错。
第六十三条 了解字符串连接的性能
总而言之,不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要,否则应该使用StringBuilder的append方法。
其实字符串连接操作符的内部就是创建StringBuilder对象来实现的,没遇到一个+号,就创建一个对象,效率非常低,还不如自己创建一个,不断的去往里添加。
第六十四条 通过接口引用对象
总而言之,给定的对象是否具有适当的接口应该时很显然的,如果是,用接口引用对象会使得程序更加灵活,如果没有合适的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象把。
如果有合适的接口类型存在,那么对于参数,返回值,变量和域来说,就都应该使用接口类型进行声明。
第六十五条 接口优先于反射机制
总而言之,如果编写的程序必须要与编译时未知的类一起工作的时候,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
核心反射机制提供了通过程序访问任意类的能力,可以获得构造器,方法以及域。
反射的缺点:
1.损失了编译时类型检查的优势。
2.代码很长。
3.性能损失。
第六十六条 谨慎使用本地方法
总而言之,在使用本地方法之前务必三思,用本地方法来提高性能的做法是不值得提倡的,同时也有一些严重的缺陷。
第六十七条 谨慎地进行优化
总而言之,在你还没有绝对清晰的优化方案之前,请不要优化,97%的情况下,不成熟的优化才是一切问题的来源,优化前后要对性能进行分析。
第六十八条 遵守普遍接受的命名惯例
总而言之,要科学的命名,具体如何命名本文不再赘述。
第十章 异常
第六十九条 只针对异常的情况才使用异常
总而言之,异常应该只用于异常的情况下,而不是用他们作为控制流。
第七十条 对可恢复的情况使用受检异常,对编程错误使用运行时异常
总而言之,如果期望调用者能够适当地恢复,应该使用受检异常。如果属于不可恢复,继续执行有害无益,那么就抛出运行时异常或者错误。实现的所有的未受检的抛出结构都应该是RuntimeException的子类。
第七十一条 避免不必要的使用受检异常
总而言之,在谨慎使用的前提下,受检异常可以提升程序的可读性,如果过度使用,会使得API使用起来十分痛苦。如果属于可以恢复的,并且想要调用者处理异常的条件,首先应该返回一个optional值,当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
第七十二条 优先使用标准的异常
标准的异常更加通用,大多数人都可以看懂
第七十三条 抛出与抽象对应的异常
有待于研究。
第七十四条 每个方法抛出的所有异常都要建立文档
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。这个文档在文档注释中应当采用@throws标签的形式,要在方法的throws子句中为每个受检的异常单独声明,但是不要声明未受检的异常。如果没有位可以抛出的异常建立文档,其他人就很难或者根本不可能有效的使用你的类或者接口。
第七十五条 在细节消息中包含失败捕获信息
总而言之,为了捕获失败,异常的细节信息应该包含对该异常有贡献的所有参数和域的值,但是千万不要在细节消息中包含密码等类似的保密信息。
第七十六条 努力使失败保持原子性
总而言之,作为方法规范的一部分, 它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则,API文档就应该清楚地指明对象将会处于什么状态。
失败的原子性:失败的方法调用应该使对象保持在被调用之前的状态。
保持失败原子性的途径:
1.设计一个不可变的对象。
2.执行操作之前检查参数的有效性,这使得对象的状态被修改之前,先抛出适当的异常。
3.调整计算过程的顺序,使得任何可能会失败的计算部分在对象状态修改之前发生。
4.在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。
第七十七条 不要忽略异常
总而言之,不要忽略异常,空的catch使异常达不到应有的目的。如果选择忽略异常,catch中应该包含一条注释,说明为什么可以这么做。
第十一章 并发
第七十八条 同步访问共享的可变数据
总而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。
值得注意的点:
1.除非读和写操作都被同步,否则无法保证同步能起作用,有时候,会在某些机器上只看到了写或者读操作看起来也可以正常工作,但在这种情况下,表象具有很大的欺骗性。
2.千万不要使用Thread.stop方法,本质上是不安全的方法。
第七十九条 避免过度同步
总而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,更通俗的讲,要将同步区域内部的工作量限制到最少。
第八十条 executor,task,stream优先于线程
总而言之,善用现有的包和工具类,很多时候性能比自己写得线程性能更好。
第八十一条 并发工具优先于wait和notify
总而言之,直接使用wait方法和notify方法就像用并发汇编语言进行编程一样,而java.util.concurrent则提供了更高级的语言。没有理由在新代码中使用wait方法和notify方法,即使有,也是极少的。
第八十二条 线程安全性的文档化
总而言之,每个类都应该说明线程安全注解,清楚的说明她所属于的安全级别,synchronized修饰符与文档毫无关系。有条件的线程安全类必须在文档中指明,哪个方法调用序列需要外部同步,以及在执行这些序列的时候需要获取哪一把锁。
安全级别:
1.不可变,不需要外部同步,如String。
2.无条件的线程安全,有足够的内部同步,如AtomicLong。
3.有条件的线程安全,与无条件类似,除了有些方法需要进行外部同步外。
4.非线程安全,为了并发的使用它们,客户端必须利用自己选择的外部同步包围每个方法调用。
5.线程对立,无论怎么做都是不安全的。
第八十三条 慎用延迟初始化
总而言之,大多数的域应该正常地进行初始化,而不是延迟初始化,很多时候并不能得到性能的提升。如果必须要延迟初始化,就应该选择合适的方式,对于实例域,采用双重检查模式,如果可以接受重复初始化,可以考虑单重检查模式,对于静态域,则用lazy initialization holder class idiom模式。
//lazy initialization holder class idiom
private static class FieldHolder{
static final FieldType field = computeFieldValue();
}
private static FieldType getField(){
return FieldHolder.field;
}
//双重检查的方式
private volatile FieldType field;
private FieldType getField(){
FieldType result = field;
if(result == null){
synchronized(this){
if(field==null){
field = result = computeFieldValue();
}
}
}
return result;
}
//单重检查模式
private volatile FieldType field;
private FieldType getField(){
FieldType result = field;
if(result==null){
field = result = computeFieldValue();
}
return result;
}
第八十四条 不要依赖线程调度器
总而言之,不要让程序的正确性依赖于线程调度器,否则,得到的应用程序将变得不健壮,也不具有可移植性。同样,不要依赖Thread.yield或者线程的优先级,线程的优先级是java平台最不可移植的特征了。
第十二章 序列化
第八十五条 其他方法优先于java序列化
总而言之,序列化是很危险的,应该予以避免,如果重新设计一个系统,一定要用跨平台的结构化数据表示法代替,如JSON。不要反序列化不被信任的数据,如果必须这么做,就要使用对象的反序列化过滤,但这也不能阻止所有的攻击。
第八十六条 谨慎地实现Serializable接口
总而言之,千万不要认为实现Serializable接口会很容易,必须认真对待,如果一个类允许继承,要更加的小心。
实现Serializable接口的代价与注意:
1.最大代价,一旦这个类被发布,就大大降低了改变这个类实现的活性,设计不好的序列化形式可能会使得类根本无法演变。
2.每个可序列化的类都有一个唯一的标识号与它相关联,如果没有显示的声明UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
3.第二个代价,它增加了出现Bug和漏洞的可见性。
4.第三个代价,随着发行新的版本,相关的测试负担也会增加。
5.为了继承而设计的类,应该尽可能少的去实现Serializable接口,用户的接口也应该尽可能少继承serializable接口。
6.内部类不应该实现Serializable接口。
第八十七条 考虑使用自定义的序列化形式
总而言之,当你决定要将一个类做成序列化的时候,请仔细思考应该采用什么样的序列化形式,只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式,否则,就应该设计一个自定义的序列化形式,合理的描述对象的状态。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式,否则,则要自定义。
不管是哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显示的UID,并且,不要修改序列版本UID,否则将会破坏类现有的已被序列化实例的兼容性。
当对象的物理表示发与它的逻辑内容区别很大的时候,使用默认序列化形式会有以下四个缺点:
1.它使得这个类的导出的API永远束缚在该类的内部表示法上。
2.它会消耗过多的时间。
3.它会消耗更多的空间。
4.它会引起栈溢出,里面递归太多。
第八十八条 保护性地编写readObject方法
有待于研究。
第八十九条 对于实例控制,枚举类型优先于readResolve
有待于研究。
第九十条 考虑用序列化代替序列化实例
有待于研究。