EFFECTIVE-JAVA(第二版)内容概要

本书一共七十八小节,每个小节讲述了一条规则。 这些规则实践通常会使那些最优秀和最富有经验的程序员从中受益。 所有的内容分为11章,每章都涉及软件设计的一个广泛方面。 本书不需要按顺序阅读:每个小节都或多或少地独立存在。 这些小节有很多交叉引用,因此您可以通过本书轻松学习自己的课程。

目录

第二章 创建和销毁对象
第1条 考虑用静态工厂方法代替构造器
第2条 遇到多个构造器参数时要考虑用构建器(build模式)
第3条 用私有构造器或者枚举类型强化Singleton属性
第4条 通过私有构造器强化不可实例化的能力
第5条 避免创建不必要的对象
第6条 消除过期的对象引用
第7条 避免使用终结方法

第三章 对于所有对象都通用的方法
第8条 覆盖equals时请遵守通用约定
第9条 覆盖equals时总要覆盖hashCode
第10条 始终要覆盖toString
第11条 谨慎地覆盖clone
第12条 考虑实现Comparable接口

第四章 类和接口
第13条 使类和成员的可访问性最小化
第14条 在公有类中使用访问方法而非公有域
第15条 使可变性最小化
第16条 复合优先于继承
第17条 要么为继承而设计,并提供文档说明,要么就禁止继承
第18条 接口优于抽象类
第19条 接口只用于定义类型
第20条 类层次优于标签类
第21条 用函数对象表示策略(函数编程)
第22条 优先考虑静态成员类(内部类尽量使用static修饰)

第五章 泛型
第23条 请不要在新代码中使用原生态类型
第24条 消除非受检警告
第25条 列表优先于数组
第26条 优先考虑泛型
第27条 优先考虑泛型方法
第28条 利用有限制通配符来提升API的灵活性
第29条 优先考虑类型安全的异构容器

第六章 枚举和注解
第30条 用enum代替int常量
第31条 用实例域代替序数
第32条 用EnumSet代替位域
第33条 用EnumMap代替序数索引
第34条 用接口模拟可伸缩的枚举
第35条 注解优先于命名模式
第36条 坚持使用Override注解
第37条 用标记接口定义类型

第七章 方法
第38条 检查参数的有效性
第39条 必要时进行保护性拷贝
第40条 谨慎设计方法签名
第41条 慎用重载
第42条 慎用可变参数
第43条 返回零长度的数组或者集合,而不是null
第44条 为所有导出的API元素编写文档注释

第八章 通用程序设计
第45条 将局部变量的作用域最小化
第46条 for-each循环优先于传统的for循环
第47条 了解和使用类库(使用标准的工具提高开发效率并更容易维护)
第48条 如果需要精确的答案,请避免使用float和double
第49条 基本类型优先于装箱基本类型
第50条 如果其他类型更适合,则尽量避免使用字符串
第51条 当心字符串连接的性能
第52条 通过接口引用对象
第53条 接口优先于反射机制
第54条 谨慎地使用本地方法
第55条 谨慎地进行优化
第56条 遵守普遍接受的命名惯例

第九章 异常
第57条 只针对异常的情况才使用异常
第58条 对可恢复的情况使用受检异常,对编程错误使用运行时异常
第59条 避免不必要地使用受检的异常
第60条 优先使用标准的异常
第61条 抛出与抽象相对应的异常
第62条 每个方法抛出的异常都要有文档
第63条 在细节消息中包含能捕获失败的信息
第64条 努力使失败保持原子性
第65条 不要忽略异常

第十章 并发
第66条 同步访问共享的可变数据
第67条 避免过度同步
第68条 executor和task优先于线程
第69条 并发工具优先于wait和notify
第70条 线程安全性的文档化
第71条 慎用延迟初始化
第72条 不要依赖于线程调度器
第73条 避免使用线程组

第十一章 序列化
第74条 谨慎地实现Serializable接口
第75条 考虑使用自定义的序列化形式
第76条 保护性地编写readObject方法
第77条 对于实例控制,枚举类型优先于readResolve
第78条 考虑用序列化代理代替序列化实例

第二章 创建和销毁对象

第1条 考虑用静态工厂方法代替构造器

可读性更强
它们有名称,扩展代码的可读性,更能清晰的表达程序的意图
避免重复创建
不必在每次调用它们的时候都创建一个新的对象,例如Boolean.valueOf(boolean)方法
返回子对象
可以返回原返回类型的任何子类型对象
代码更加简洁
不需要重复指明类型参数

不能被继承
无公共构造方法,将不能被继承
无实例化标志
与普通静态方法没有区别,没有明确的标识一个静态方法用于实例化类

第2条 遇到多个构造器参数时要考虑用构建器(build模式)

如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是不错的选择。
与重叠构造器相比,builder模式的客户端更易与阅读和编写
与JavaBeans相比,更加的安全

第3条 用私有构造器或者枚举类型强化Singleton属性
实现单例模式通常有3种方法:静态成员,静态工厂方法,单元素枚举类型

静态成员
静态工厂方法
1、前两种不能保证全局只有一个对象,例如可以通过反射机制,设置AccessibleObject.setAccessible(true),改变构造器的访问属性,调用构造器生成新的实例。
2、为了保证反序列化的时候,实例还是Singleton,必须声明所有的实例域都是transient的
单元素枚举类型
通过枚举实现Singleton更加简洁,同时枚举类型无偿地提供了序列化机制,可以防止反序列化的时候多次实例化一个对象

第4条 通过私有构造器强化不可实例化的能力
在创建工具类的时候,大部分是无需实例化的,实例化对它们没有意义。在这种情况下,创建的类,要确保它是不可以实例化的。
要避免这个问题,使用的方式是,定义一个私有的构造器,添加 throw new AssertionError(),阻止实例化。

第5条 避免创建不必要的对象
装箱类型尽量避免重新创建对象,例如反面例子:String a = new String(“Hello World”);

第6条 消除过期的对象引用
一般而言,只要是自己管理内存,就应该警惕内存泄漏问题。
假如你开辟了一段内存空间,并一直持有这段空间的引用,就有责任管理它,
因为GC无法自动完成对你承诺管理的内存的回收,除非你告诉它(显式地清空引用)。

第7条 避免使用终结方法
所谓的终结方法其实是指finalize()。终结方法finalize通常是不可预测的,也是很危险的
finalize()的执行过程:

第三章 对于所有对象都通用的方法

第8条 覆盖equals时请遵守通用约定

正确覆盖equals方法,需要遵守它的通用约定:

自反性
x.equals(x) 必须返回true
对称性
x.equals(y) 与 y.equals(x) 返回结果应该相同
传递性
x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)应该返回true
一致性
只要比较的实例对象的关键属性值没有改变,无论调用多少次equals,结果都应该相同
x.equals(null)=false
对于非null的x实例,x.equals(null) 永远返回false

第9条 覆盖equals时总要覆盖hashCode

如果两个对象利用equals方法比较是相等的,那么这两个对象必须能返回同样的hashCode

第10条 始终要覆盖toString

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

第11条 谨慎地覆盖clone

实际上,clone方法就是另一个构造器,必须确保它不会伤害到原始对象,并确保正确的创建被克隆对象中的约束条件。
由于覆盖clone方法很复杂,最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能

第12条 考虑实现Comparable接口

什么时候应该考虑是想Comparable接口
你写的类是一个值类(前面的文章介绍过)。
类中有很明显的内在排序关系,如字母排序、按数值顺序或是时间等

第四章 类和接口

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

可以有效的解除系统中各个模块的耦合度、实现每个模块的独立开发、使得系统更加的可维护,更加的健壮
第一条规则:尽可能使每个类和成员不被外界访问。尽可能使用最小的访问级别。

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

反面示例

public class Point {  
    public double x;  
    public double y;  
}

总之,公有类永远都不应该暴露可变的域

第15条 使可变性最小化

不要提供任何会修改对象状态的方法(mutator)
保证类不会被扩展
使所有的域都是final的
使所有的域都成为私有的
确保对于任何可变组件的互斥访问

第16条 复合优先于继承

在新的类中添加一个私有域,这种设计被称作”复合(composition)”。
这得到的类稳固,它不依赖于现有类的实现细节。即使现有的类添加了新的方法,也不影响新的类

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

构造器决不能调用可被覆盖的方法
对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化

第18条 接口优于抽象类

接口的优点

现有的类可以很容易的被更新,以实现新的接口

接口是定义mixin(混合类型)的理想选择。

接口允许我们构造非层次结构的类型框架。
接口可以多继承
接口可以使得类的增强变得安全。

第19条 接口只用于定义类型

常量接口是对接口的一种不良使用
总之,接口应该只被用来定义类型,它们不应该用来导出常量。

第20条 类层次优于标签类

标签类有着许多缺点,标签类过于冗长、容易出错,并且效率低下,标签类正是类层次的一种简单效仿

第21条 用函数对象表示策略(函数编程)

使用函数编程代替策略模式

第22条 优先考虑静态成员类(内部类尽量使用static修饰)

非静态成员类的每个实例都隐含着与外围类的一个外围实例

第五章 泛型

第23条 请不要在新代码中使用原生态类型

指定泛型时尽量缩小泛型的范围

第24条 消除非受检警告

(洁癖的我一直做的很好,eclipse没有任何黄色警告)

第25条 列表优先于数组

数组是协变的
如果Sub是Super的子类型,那么数组类型Sub[]就是Super[]的子类型。泛型确是不可变的,List不是List的子类型
数组是具体化的
数组在运行时才知道并检查它们的元素类型约束
数组是协变且可以具体化的
泛型是不可变的且可以被擦除的

第26条 优先考虑泛型

使用泛型比使用需要在客户端代码中进行转化的类型来的更安全

第27条 优先考虑泛型方法

泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来的更加安全,也更加容易

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

public class Stack<E> {
       public void pushAll(Iterable<E> src) {
              for (E e : src)
                     push(e);
       }
}

如果类的E为Number,方法里的E传的又是Integer,此时会报错,原因在于Iterable并不是Iterable的子类型,泛型是不协变的
解决方法:使用通配符 Iterable<? extends E>即可解决这个问题

public class Stack<E> {
       public void pushAll(Iterable<? extends E> src) {
              for (E e : src)
                     push(e);
       }
}

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

一般情况下,泛型最通常应用于集合,如set和Map,以及单元素的容器。
这些容器都被充当被参数化了的容器,意味着每个容器只能有一个或者多个固定数目的类型参数(Set、List只有一个,Map两个)
有时候我们可能需要更多的灵活性,比如数据库行可以有任意多的列,每个列的类型可能不一样,希望能以类型安全的方式访问所有列。

下面就实现了这样的效果

private Map<Class<?>,Object>  map = new HashMap<Class<?>, Object>();
private Map<List<?>,Object>  map2 = new HashMap<List<?>, Object>();

public <E> void add(Class<E> type,E instance) {
    if(type!=null) {
        map.put(type, instance);
    }
}

public <E> void add(List<E> type,E instance) {
    if(type!=null) {
        map2.put(type, instance);
    }
}

public static void main(String[] args) {
       Favorite f = new Favorite();
       f.add(String.class, "string");
       f.add(Integer.class, Integer.valueOf(123));
       f.add(new ArrayList<String>(), "String");
       f.add(new ArrayList<Integer>(), 123);
}

第六章 枚举和注解

第30条 用enum代替int常量

探索enum的一些新用法:
定义枚举可以覆盖抽象方法
特定于常量的类主体:枚举类型中声明一个抽象方法特定于常量的方法实现:用具体的方法覆盖每个常量的抽象方法
尝试使用switch

第31条 用实例域代替序数

定义枚举实例域写死序数,而不是用指针序数

第32条 用EnumSet代替位域

如果多个枚举常量同时共享相同的行为,则考虑策略枚举(嵌套枚举)

第33条 用EnumMap代替序数索引

大多数程序员都不需要这个方法。除非你在编写的是这种数据结构,否则最好避免使用original方法

第34条 用接口模拟可伸缩的枚举

在java中一个枚举是无法直接去扩展另一个枚举的,但通过接口我们可以变相的实现这一点

public interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) {return x + y;}
    },
    MINUS("-") {
        public double apply(double x, double y) {return x - y;}
    },
    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
    public String toString() {
        return symbol;
    }
}

第35条 注解优先于命名模式

一般使用命名模式表明哪些程序元素需要通过某种工具或框架进行特殊处理 . 但是它有严重的缺点 – 以 Junit 为例
文字拼写错误会导致失败,但是没有任何提示
无法确保它们只用于相应的程序元素上(Junit针对的是方法名,不是类名)
它们没有提供将参数值与程序元素关联的好方法
注解类型可以很好的解决命名模式的缺点

第36条 坚持使用Override注解

Override 注解来覆盖超类声明 , 编译器可以替你防止大量错误

第37条 用标记接口定义类型

标记接口:指明一个类实现了具有某种属性的接口,例如Serializable接口。
标记注解:特殊类型的注解,不包含成员类型,标记注解的唯一目的就是标记声明。例如:@Override
如果想要定义类型,一定要使用接口,标记接口可以更加精确的进行锁定

第七章 方法

第38条 检查参数的有效性

参数检查的必要性
比如对象引用不能为null,比如必须是正数,你应该在文档中(或者注释中)清楚地指出所有这些限制
共有方法参数检查
对于共有方法,要用javadoc的@throw标签在文档中说明违反参数值限制会抛出异常
非共有方法参数检查
非公有方法一般使用断言来检查参数的有效性
保留的方法参数检查
对于有些参数,方法本身没有用到,保存起来供以后使用.检测这些参数的有效性尤为重要

第39条 必要时进行保护性拷贝

对于构造器的每个可变参数进行保护性拷贝

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(start + " after " + end);
            }
            this.start = start;
            this.end = end;
      }

      public Date start() {
            return start;
      }

      public Date end() {
            return end;
      }
}

这个类看上去没有什么问题,时间是不可改变的。然而Date类本身是可变的。

Date start = new Date(); 
Date end = new Date(); 
Period period = new Period(start, end); 
end.setYear(78); 
System.out.println(period.end());

为了保护Period实例的内部信息避免受到修改,导致问题,对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的:

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(this.start + " after " + this.end); 
    } 
}

对于参数类型可以被不可信任类子类化的参数,请不要使用clone方法进行保护性拷贝
当X类提供了可变内部成员的访问能力时,该访问返回的应该是可变内部域的保护性拷贝。

第40条 谨慎设计方法签名

谨慎选择方法名称,遵循风格一致大众认可的名称
勿过于追求提供便利的方法:每个方法都应该尽其所能.方法太多会使得类难以学习
避免过长的参数列表。(少于等于4个,参数太多容易出错)
a.分解多个方法
b.创建辅助类来保存参数分组,辅助类一般为静态成员类。
c.从对象构建到方法调用都采用Builder模式
对于参数类型,优先使用接口而不是类(例如:在编写方法使用HashMap类作为参数,还是使用Map接口作为参数呢?)
对于boolean类型,要优先使用两个元素的枚举类型

第41条 慎用重载

应该避免胡乱的使用重载机制

第42条 慎用可变参数

在重视性能的情况下,使用可变参数机制要小心,因为可变参数方法的每次调用都会导致进行一次数组分配和初始化。
有一种折中的解决方案,先声明出所有参数数目小于等于3的方法,当参数数目超过3个时,使用可变参数方法

public void foo() {}
public void foo() {int a1}
public void foo() {int a1, int a2}
public void foo() {int a1, int a2, int a3}
public void foo() {int a1, int a2, int a3, int... rest}

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

编写客户端程序的程序员可能会忘记写这种专门的代码来处理null返回值

有时候会有人认为:null返回值比零长度数组更好,因为它避免了分配数组所需要的开销。
这个级别担心性能是不明智的,除非分析表明这个方法就是造成性能的根本原因;
其次对于不返回任何元素的调用,每次都返回同一个零长度数组是可能的,因为零长度数组是不可变的,而不可变对象有可能被自由共享

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

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

基础知识:
/*this is a description/注释若干行,并写入 javadoc 文档
文档注释三部分:
第一部分是简述。
第二部分是详细说明部分。该部分对属性或者方法进行详细的说明,在格式上没有什么特殊的要求,可以包含若干个点号。
第三部分是特殊说明部分。这部分包括版本说明、参数说明、返回值说明等。

第八章 通用程序设计

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

java允许你在任何可以出现语句的地方声明变量
要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明
几乎每个局部变量的声明都应该包含一个初始化表达式(变量必须初始化)
将局部变量作用域最小化的方法是使方法小而集中

第46条 for-each循环优先于传统的for循环

for-each循环通过完全隐藏迭代器或者索引变量,避免混乱和出错的可能,适用于集合和数组和任何实现Iterable接口的对象
无法使用for-each循环的情况:
过滤,需要遍历集合并删除选定的元素,需要显式的迭代器,以便调用它的remove方法
转换,需要遍历列表或者数组,并取代它部分或者全部元素值,需要列表迭代器或者数组索引,以便设定元素的值
平行迭代,并行地遍历多个集合,需要显式地控制迭代器或者索引变量,以便所有迭代器和索引变量都可以得到同步前移

第47条 了解和使用类库(使用标准的工具提高开发效率并更容易维护)

1、通过使用标准类库,可以充分利用这些编写标准类库专家的知识,以及在你之前的其他人的使用经验。
2、不必将你的时间浪费在与你工作基本无关的问题上。与大多数程序员一样,你应该将你的时间放在你的应用研发上,而不是底层细节上。
3、无需你的努力去维护,标准类库的性能会随着时间的推迟而改善。因为类库被许多人使用,也因为类库以工作标准基准被使用,所以类库的供应商有着强烈的动机让类库运行得更快。
4、可以使你的代码融于主流中,这种类码更易读,更易维护,更易被大多数开发者重用。

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

1、float和double类型尤其不适合用于货币计算,因为存在精度丢失,float最多表示8位数,dubbo最多表示16位数
2、可以使用bigDecimal、int或者long进行货币计算(BigDecimal不方便而且慢,如果数值范围没有超过9位十进制数字,就可以使用int;如果不超过18位数字,就可以使用long;如果可能超过18位,就必须使用BigDecimal)

第49条 基本类型优先于装箱基本类型

基本类型和装箱基本类型之间的三个主要区别:
基本类型只有值,而装箱基本类型具有与它们的值不同的同一性(两个装箱基本类型可以具有相同的值和不同的同一性)
基本类型只有功能完备的值,而每个装箱基本类型还有个非功能值:null
基本类型通常比装箱基本类型更节省空间和时间。

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

字符串不适合代替其他的值类型
字符串不适合代替枚举类型
字符串不适合代替聚合类型()
字符串不适合代替能力表(不重复的唯一KEY)
总之:如果有更合适的数据类型,避免使用字符串来表示对象。

第51条 当心字符串连接的性能

字符串连接操作符(+),把多个字符串合并成一个字符串的便利方式.但不适合运用在大规模场景
不要使用字符串连接操作符进行拼接多个字符串,除非性能无关紧要,
要使用StringBuilder的append()方法;或者使用字符串组,或者每次只处理一个字符串,而不是将它们组合起来.

第52条 通过接口引用对象

应该优先使用接口而不是类来引用对象,例如Vector的情况。

List<Subscriber> subscribers = new Vector<Subscriber>();
Vector<Subscriber> subscribers = new Vector<Subscriber>();

如上,如果使用接口作为类型,程序将会更加灵活,当决定更换实现时,只需改变构造器中的类的名称

第53条 接口优先于反射机制

核心反射机制java.lang.reflect提供了“通过程序来访问关于已装载的类的信息”的能力,
给定一个Class实例,可以获得Constructor、Method、Field实例,这些对象提供“通过程序来访问类的成员名称、域类型、方法签名等信息”的能力。
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,存在的代价:
失去编译时类型检查的好处,包括异常检查。
执行反射访问所需的代码冗长。
性能上的损失。
反射功能只是在设计时被用到。通常,普通应用程序在运行时不应该以反射方式访问对象。

对于有些程序,必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类,
就可以以反射的方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。

第54条 谨慎地使用本地方法

本地方法,是指本地程序设计语言(c,或者c++)来编写的特殊方法.
本地方法制约跨平台使用的能力.可能对于不同系统的不同版本存在制约问题

第55条 谨慎地进行优化

很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他的原因,——甚至包括盲目地做傻事。
                      ——William A.Wulf[Wulf72]
不要去计校效率上的一些小小的得失,在97%的情况下。不成熟的优化才是一切问题的根源。
                      ——Donald E.Knuth[Knuth74]
在优化方面,我们应该遵守两条规则:
  规则1:不要进行优化。
  规则2(仅针对专加):还是不要进行优化一一也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化.
                      ——M.A.Jackson[Jackson75]
在每次试图做优化之前和之后,要对性能进行测量:
在Java乎台上对优化的结果进行测量,比在其他的传统平台上更有必要。
因为Java程序设计语言没有很强的性能模型 ,各种基本操作的相对开销也没有明确定义。
努力避免那些限制性能的设计决策

第56条 遵守普遍接受的命名惯例

包的名称:层次状的,句号分隔。包括小写字母和数字(数字很少使用)。域名倒述+模块名。追求简短而有意义的缩写命名方式。只取首字母缩写形似也是可以接受的
类和接口的名称:按驼峰命名方式命名,首字母大写
方法和域的名称:驼峰命名,首字母小写。(常量域:全部大写,多个单词用下划线隔开)
类型参数:
a、执行动作方法常用动词或者动作短语命名
b、对于返回boolean类型的参数,往往以is开头
c、转换类型的方法、返回类型独立对象的方法,通常被称为toType,例如toString和toArray
d、返回视图的方法通常被称为asType。例如asList
e、返回一个与被调对象同值的基本类型的方法,通常被称为typeValue,例如intValue。
f、静态工厂的常用名称为valueOf、of、getInstance、newInstance、getType和NewType
T表示任意类型的,E表示集合元素类型,K和V表示映射的键和值类型,X表示异常

第九章 异常

第57条 只针对异常的情况才使用异常

企图利用java的错误判断机制来提高性能是错误的:
异常机制设计的初衷是用来处理不正常的情况,所以JVM很少对它们进行优化.
代码放在try..catch中反而阻止了jvm本身要执行的某些特定优化
对数组进行遍历的标准模式并不会导致冗余的检查
总而言之,异常是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使他们这么做的API。

第58条 对可恢复的情况使用受检异常,对编程错误使用运行时异常

所有的异常都是从Throwable 的类派生出来的。throwable下面有三种结构:受检异常、运行时异常和错误。
受检异常
如果期望调用者在遇到异常后可以进行恢复的操作,那么使用受检异常,也就是 xxx extends Exception的异常
运行时异常
如果不希望调用者进行恢复,想直接中断程序的运行,则抛出运行时异常,也就是 xxx extends RuntimeException的异常。
错误
Error(错误)表示系统级的错误和程序不必处理的异常,是java运行环境中的内部错误或者硬件问题,比如,内存资源不足、操作系统出错。
在大多数情况下,当遇到这样的错误时,建议让该程序中断。这样的异常超出了程序可控制的范围。

第59条 避免不必要地使用受检的异常

使用异常可以增加可靠性,但是过分使用异常就会导致问题不断

第60条 优先使用标准的异常

专家程序员和普通程序员主要区别在于专家程序员追求并且通常能够实现高度的代码复用.代码复用的通用的规则,异常亦是.
复用异常可以增加代码的可读性 ,减少内存印记,减少类装载的开销。
所有的错误都可以归结为非法参数和非法状态.
异常
使用场合
IllegalArgumentException
参数值不正确
IllegalStateException
对于方法调用而言,对象状态不合适
NullPointerException
在禁止使用null的情况下参数值为null
IndexOutOfBoundsException
下标参数值越界
ConcurrentModificationException
在禁止并发修改的情况下,检测到对象的并发修改
UnsupportedOperationException
对象不支持用户请求的方法

第61条 抛出与抽象相对应的异常

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常
使用异常链来让高层抽象包含低层的异常,便于以后排除问题。

第62条 每个方法抛出的异常都要有文档

异常转译:更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译
可以在给低层传递参数之前,检查更高层方法的参数的有效性,从而避免低层方法抛出异常。(高层检查参数,避免低层异常)
如果无法避免低层异常,可以让更高层来悄悄地绕开这些异常,从而将高层方法的调用者与低层的问题隔离开来。使用适当地记录机制来将异常记录下来。(记录低层异常,然后跳过这些异常)
除非保证低层方法可以保证他所抛出的所有异常正好对高层也合适,才可以从低层传播到高层

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

对于程序开发者或者运维人员,异常类型的toString方法应该尽可能返回有关联失败原因的信息,即异常的细节消息应该捕获住失败,便于以后分析.
异常细节包含对该异常有用的参数和属性的值
新构建的异常类中包含导致此异常原因的属性,抛出(检查or运行)异常时对各个属性赋值足够有价值的数据
异常给客户的信息和给开发维护人员的信息不同,给客户信息要求可理解的,给开发人员的信息内容高于可理解

第64条 努力使失败保持原子性

当对象抛出异常后,通常期望这个对象仍然保持在一种定义良好的可用状态,即使异常发生在执行某个操作的过程中间.这对于检查时异常尤为重要.因为调用者期望能从异常中进行恢复.
如何实现失败原子性呢?
对象不可变:第一种最简单的方法设计不可变对象
检查参数并抛出适当异常:对于可变对象上执行操作的方法,获得失败原子性最常见的方法是,在执行操作之前检查参数有效性.使得对象状态被改变之前,先抛出适当异常.
对象持久化异常回滚:编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态.这种方法主要用于永久性数据结构(存在磁盘中).
对象拷贝:在对象的一份临时拷贝上执行操作,当操作完成在用临时性拷贝的结果代替对象的内容.

第65条 不要忽略异常

空的catch块会使异常达不到应有的目的.忽视异常如同把火警信号关掉一般
最起码catch块中包含一条说明,解释为什么可以忽略此异常
是否有情景可以忽略异常呢?
关闭FileInputStream时,可以忽略.因为没有改变文件状态,因此不必进行任何恢复操作;已经从文件中读取所需信息,因此不需要终止进行的操作.

第十章 并发

第66条 同步访问共享的可变数据

关键字synchronized可以保证同一时刻,只有一个线程可以执行某个方法。
保证线程安全方法:
不要跨线程访问共享变量
使共享变量是final类型的(常量)
将共享可变数据的操作加上同步
当多线程访问可变数据时,每个读或者写的线程都必须执行同步
可变数据同步方法:
synchronized
volatile 共享原子数据,非原子操作也要同步
ReentrantLock锁
Atomic类
并发集合CopyOnWriteList、ConcurrentHashMap、BlockingQueue
concurrent框架
executor框架

第67条 避免过度同步

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

为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,要尽量限制同步区域内部的工作量

第68条 executor和task优先于线程

如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池。你可以用固定或者可变的数目的线程来创建一个线程池。
在大负载的产品服务中,最好使用Executors.newFixedThreadPool,它为你提供一个包含固定线程数目的线程池.要想最大限度的控制它,就直接使用ThreadPoolExecutor类。

第69条 并发工具优先于wait和notify

在java.util.conturrent包中更高级的并发工具分成三类:Executor Framework,并发集合(Concurrent Collection)以及同步器(Synchronizer).
ConcurrentMap除了提供了卓越的并发性外,速度也是非常快的。除非不得已,我们应当优先使用ConcurrentHashMap而不是Collections.synchronizedMap或者Hashtable。
线程不安全的HashMap
效率低下的hashtable 的容器:HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态,使用synchronized来保证线程安全,如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap:分段锁技术(HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术)

wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。wait方法被用来使线程等待某个条件。它必须在同步区域内被调用。
notify():唤醒线程池中一个线程(任意),没有顺序。
notifyAll():唤醒线程池中的所有线程。

第70条 线程安全性的文档化

线程安全性的几种级别:
不可变的:这个类的实例是不变的,所以不需要外部同步。例如 String、Biginter、Long
无条件的线程安全:这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。例如 Random和ConcurrentHashMap
有条件的线程安全:除了有些方法为进行安全的并发而需要使用外部同步之外(这是它的条件),这种线程安全和无条件的线程安全级别完全相同。例如:Collenctions.synchronized包装返回的集合,它们的迭代器要求外部同步
非线程安全:这个实例是可变的。为了并发的使用他们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。例如:ArrayList和HashMap
线程对立的(thread-hostie):这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。

第71条 慎用延迟初始化

延迟初始化:延迟到需要域的值时才将它初始化的这种行为
延迟初始化就像一把双刃剑,它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。
根据延迟初始化的域最终需要初始化的比例、初始化这些 域要多少开销,以及每个域多久被访问一次,延迟初始化(就像其他的许多优化一样)实际上降低了性能

延迟初始化有它的好处。如果域只在类的实例部分被访问,井且初始化这个域的开销很高,可能就值得进行延迟初始化。要确定这一点,唯一的办法就是测量类在用和不用延迟初始化时的性能差别。
多线程初始化要慎重:当有多个线程时,延迟初始化是需要技巧的。如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的Bug。
静态域考虑使用延迟加载模式:如果出于性能的考虑而需要对静态域使用延迟初始化,就使用 lazy initialization holder class模式。这种模式保证了类要到被用到的时候才会被初始化

private static class fieldHolder(){
    static final FieldType field = computeFieldValve();
}
static FiledType getField() {
    return FieldHolder.field;
}

第72条 不要依赖于线程调度器

简单理解:决定哪些线程将会运行,不能依赖于操作系统的策略做到公正。这样的程序很可能是不能移植的。
当有多个线程可以运行时,由线程调度器决定哪些线程将会运行,以及运行多长时间。任何一个合理的操作系统在做出这样的决定时,都会努力做到公正,
但是所有操作系统的竞争策略却大相径庭。因此,编写良好的程序不应该依赖于这种策略的细节。
任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。线程优先级是Java平台上最不可移植的特征。

第73条 避免使用线程组

线程组并没有提供太多有用的功能,而且他们提供的很多线程组还都是有缺陷的。
我们最好把线程组看成是一个不成功的试验。你可以忽略掉他们,就当他们根本不存在一样。
如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。

第十一章 序列化

第74条 谨慎地实现Serializable接口

代价一:失去灵活性
如果一个类实现了Serializable接口。它的字节流编码(或者说序列化形式,serialized form)就变成了它的导出的API的一部分

代价二:安全漏洞
增加了bug和安全漏洞的可能性。通常情况下,对象是利用构造器来构建的。序列化机制是一种语言之外的对象创建机制。
无论你是接受了默认的行为,还是覆盖了默认的行为,反序列化机制都是一个“隐藏的构造器”,具备与其他构造器相同的特点。

值类或集合类可考虑实现Serializable
实现Serializable接口 提供了一些实在的益处: 如果一个类将要加人到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这个类来说,实现Serializable接口就非常有必要。
比如Date和BigInteger这样的值类,还有大多数的集合类。代表活动实体的类,比如线程池,一般不应该实现serializable

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

如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受。

第76条 保护性地编写readObject方法

每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要
假设这个字节流一定代表着一个真正被序列化过的实例。
对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。
对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。

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

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

第78条 考虑用序列化代理代替序列化实例

实现java.io.Serializable接口, 会增加出错和出现安全问题的可能性, 因为它开放了实例的另一种来源 —- 反序列化. 有一种方法可以减少风险, 那就是序列化代理模式.

public class Person implements Serializable {
      private static final long serialVersionUID = 1L;
      private String name;
      private String hobby;
      private Integer age;

      public Person() {
      }

      public Person(String name, String hobby, Integer age) {
            this.name = name;
            this.hobby = hobby;
            this.age = age;
      }

      public String getName() {
            return name;
      }

      public void setName(String name) {
            this.name = name;
      }

      public String getHobby() {
            return hobby;
      }

      public void setHobby(String hobby) {
            this.hobby = hobby;
      }

      public Integer getAge() {
            return age;
      }

      public void setAge(Integer age) {
            this.age = age;
      }

      private Object writeReplace() {
            System.out.println("Person.writeReplace()");
            return new PersonProxy(this);
      }

      private static class PersonProxy implements Serializable {
            private static final long serialVersionUID = 1L;

            private final String name;
            private final String hobby;
            private final int age;

            public PersonProxy(Person original) {
                  System.out.println("PersonProxy(Person original)");
                  this.name = original.getName();
                  this.hobby = original.getHobby();
                  this.age = original.getAge();
            }

            private Object readResolve() {
                  System.out.println("PersonProxy.readResolve()");
                  Person person = new Person(name, hobby, age);
                  System.out.println("resolveObject: " + person);
                  return person;
            }
      }
}

public static void main(String[] args) {
      try {
        Person person = new Person("张三", "足球" ,25);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serial.ser"));
        out.writeObject(person);
        out.flush();
        out.close();

        Thread.sleep(1000);

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serial.ser"));
        Person deserPerson = (Person) in.readObject();
        System.out.println("main: " + person);
        in.close();

        if(person == deserPerson) {
            System.out.println("序列化前后是同一个对象");
        } else {
            //反序列化会创建对象, 但是不会执行类的构造方法, 而是使用输入流构造对象
            System.out.println("序列化前后不是同一个对象, 哈哈哈");
        }
      } catch (Exception e) {
            e.printStackTrace();
      }
}

1、序列化对象时 会调用writeReplace()生成一个PersonProxy对象,然后对此对象进行序列化
2、反序列化时,会调用PersonProxy的readResolve()方法生成一个Person对象
3、Person类的序列化工作完全交给PersonProxy类


 上一篇
手写线程同步锁 ,CAS无锁算法 手写线程同步锁 ,CAS无锁算法
类似于ReentrantLock(重入锁) import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.
2019-11-14
下一篇 
代码整洁之道 代码整洁之道
读了代码整洁之道,觉得这本书写的很好,所以就将里面自己觉得很经典的内容记录下来,作为自己以后写代码的标准和准则。同时也为那些曾经困惑过的人一点参考吧! 一、在正式开始之前,我们先思考几个几个问题:1.需求与代码哪个重要?答:并不是所有的产品
2019-10-10
  目录