第1章 引言
虽然本书中的规则不会百分百的适用于任何时刻和任何场合,但他们体现了绝大多数啊 情况下的最佳程序设计实践。你不应该盲目遵从这些规则,但也应该只在偶尔情况下, 有了充分的理由之后才去打破这些规则。
第2章 创建和销毁对象
- 何时以及如何创建对象
- 何时以及如何避免创建对象
- 如何确保他们能够适时的销毁
- 如何管理对象销毁之前必须进行的各项清理动作
第1条:考虑用静态工厂方法代替构造器
静态工厂方法与构造器相比有三大优势:
- 它们有名称
- 这样在相同参数的情况下也能构造不同的实例出来
- 不必在每次调用它们的时候都创建一个新对象
- 这个应该是使用静态工厂方法的最主要的原因和情景
- 它们可以返回原返回类型的任何子类型的对象,可以要求客户端使用通过接口来引用被返回的对象
静态工厂方法的缺点:
- 类如果不含有公有的或者受保护的构造器,就不能被子类化
- 他们与其他的静态方法,实际上没有任何区别
- 这样的话就不像构造器那样容易被找到,一般使用如下的命名方式来区分别的静态方法
- valueOf
- of
- getInstance
- newInstance
- getType
- newType
- 这样的话就不像构造器那样容易被找到,一般使用如下的命名方式来区分别的静态方法
第2条:遇到多个构造器参数时要考虑用构建器
如果有多个可选参数,那么使用构建器模式是最好的处理方式。
如果有多个参数,而且有些参数必填,有些参数可选,在编写构造器的时候有如下几种方式:
- JavaBeans模式
- 使用默认的无参构造器,其余的参数通过set方法设置
- 使用重叠构造器模式
- 多个构造器每个构造器多一个参数
- Builder构建器模式
- 创建一个内部类Builder,作为构造器的参数
构建器模式范例代码:
第3条:用私有构造器或者枚举类型强化Singleton属性
如果确定要使用单利模式,那么使用单个元素的枚举类型是最好的处理方式,虽然这种方法还没有被广泛使用。
单例模式有如下几种方法:
- final修饰的公有域
- 最简单便捷的方法,需要考虑反射、序列化的问题
- 静态工厂方法
- 如果使用懒加载需要保证线程安全,同样需要考虑反射、序列化的问题
- 枚举类型
- 最优方法,代码简洁,由虚拟机提供序列化机制,绝对防止反射等方法导致的多次实例化。简洁。高效、线程安全,真的可以说是最佳单例写法。
范例代码:
Elvis01.java
Elvis02.java
Elvis03.java
第4条:通过私有构造器强化不可实例化的能力
- 对于只包含静态常亮和静态方法的类,比如工具类,是不需要提供实例化的功能的。 但因为所有的类都会提供一个默认的无参构造器,那么它就会有实例化的能力。 所以可以写一个私有的无参构造器来声明此类不能被实例化。
第5条:避免创建不必要的对象
- 如果对象是不可变对象,那么可以一直被重复使用
- 如果对象是可变对象,那么可以将某些不变的属性设置为static属性以供复用
- 在牵扯到数据计算时,使用基本数据类型而不是装箱基本类型,可以避免无意识的自动装箱,提高性能
- 于此相对的是 第39条 有关“保护性拷贝”的内容,必要时如果没有实施保护性拷贝,将会导致潜在的错误和安全漏洞; 而不必要的创建对象只会影响程序的风格和性能。
第6条:消除过期的对象引用
- 清空对象的引用是为了防止内存泄露
- “清空对象引用应该是一种例外,而不是一种行为规范”,也就是说,不是每个对象都需要显示得清空引用
- “只要类是自己管理内存,就应该警惕内存泄露问题”
- 内存泄露的另一个常见来源是缓存,使用WeakHashMap来解决
- 内存泄露的第三个常见来源是监听器和其他回调,解决方法是只保存他们的弱引用
内存泄露和解决方法范例代码: Stack.java
第7条:避免使用终结方法
- 反正不要使用
finalizer
就行了~
第3章 对于所有对象都通用的方法
主要的方法有:
- equals
- hashCode
- toString
- clone
- finalize
第8条:覆盖equals时请遵守通用约定
在不覆盖 equels
方法的情况下,类的每个实例都只与它自身相等。如果满足了以下任何一个条件, 这就是所期望的结果:
- 类的每个实例本质上都是唯一的
- 不需要关心类是否提供了”逻辑相等”的测试功能
- 超类已经覆盖了equals,从超类继承过来的行为对于子类也合适,
类是私有的或是包级私有的,可以确定他的euqals方法永远不会被调用(此时可以覆写一个抛出异常的equals方法) git 在覆写的时候要遵守它的通用约定:
- 自反性
- x.equals(x) => true
- 对称性
- x.equals(y) <==> y.equals(x)
- 传递性
- x.equals(y), y.equals(z) => x.equals(z)
- 一致性
- 多次调用返回的结果相同,不会因为调用次数不同出现不同的结果
实现高质量的equals方法:
- 使用”==”操作符检查”参数是否为这个对象的引用”,也就是比较是否是同一个实例对象
- 使用instanceof操作符检查”参数是否为正确的类型”,如果不是同一个类型,肯定为false
- 把参数转换为正确的类型
- 对于该类中的每个”关键(significant)”域,检查参数中的域是否域该对象中对应的域相匹配
- 基本数据类型使用 “==”
- 对象引用域,递归调用equals方法
- float域,使用 Float.compare 方法
- double域,使用 Double.compare 方法
覆写 equals 方法范例代码: User.java
第9条:覆盖equals时总要覆盖hashCode
在每个覆盖了 equals 方法的类中,也必须覆盖 hashcode 方法。
另外,不要试图从散列码计算中排除掉一个对象的关键部分来来提高性能。
对于hashcode的约定内容如下:
- 一个对象在没有修改的情况下,多次调用hashcode方法必须始终如一地返回同一个整数
- 如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任何一个对象的hashcode方法都产生同样的整数结果
- 如果两个对象根据equals方法比较是不想等的,那么hashcode不一定要产生不同的整数结果,也就是说可能相同也可能不同
求取hashcode的方法:
- 把某个非零的常数值,比如17,保存在一个名为 result 的int类型的变量中
- 对于对象中每个关键域 f ,完成以下步骤:
- 如果该域是boolean类型,计算(f?1:0)
- 如果该域是byte、char、short或者int类型,则计算(int)f
- 如果该域是long类型,则计算(int)(f^(f»>32))
- 如果该域是float类型,则计算Float.floatToIntBits(f)
- 如果该域是double类型,计算Double.doubleToLongBits(f),然后按照计算long的方法继续计算
- 如果该域是对象引用,则递归调用hashcode方法;如果这个域为null,则返回0
- 如果该域是一个数组,则要把每个元素当作单独的域进行处理,递归处理每个元素
- 按照下面的公示,将上面计算的散列码 c 合并到 result 中:
- result = 31 * result + c
- 返回 result
第10条:始终要覆盖toString
- 建议所有的子类都覆盖java.lang.Object提供的toString方法
- toString方法应该返回对象中包含的所有值得关注的信息
第11条:谨慎地覆盖clone
TODO:一般拷贝和深度拷贝的问题,要注意拷贝出来的对象是否引用了和原来的对象相同的域
第12条:考虑实现Comparable接口
第4章 类和接口
第13条:使类和成员的可访问性最小化
对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的 和 共有的。 优先将其设置为包级私有的,在以后的发行版本中,可以对它进行修改、替换,或者删除, 而无需担心会影响到现有的客户端程序。如果做成共有的,就有责任永远支持它,以保持它们的兼容性。
- 尽可能地使类或者成员不被外界访问
- 实例域绝不能使共有的
第14条:在公有类中使用访问方法而非公有域
- 公有类永远不应该暴露可变的域(也就是私有域,公有setter和getter方法)
- 但是如果此类是包级私有的,或者是私有的嵌套类,那么直接暴露它的数据域并没有本质的错误
第15条:使可变性最小化
不可变类是其实例不能被修改的类。 存在不可变类的理由:
- 不可变类比可变类更加易于设计、实现和使用。
- 它们不容易出错,且更加安全。
- 不可变对象本质上是线程安全的,它们不要求同步。
- 因为线程安全,不可变对象可以被自由地共享。所以,可以尽可能地共用同一个实例,提高性能。
为了使类成为不可变类,通常使用”函数化”(functional)方法,遵循下面五条规则:
- 1、不要提供任何会修改对象的方法
- 2、保证类不会被扩展
- 这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为
- 防止子类化的方法
- 使这个类成为final的
- 其他方法后面补充 TODO
- 3、使所有的域都是final的
- 通过系统的强制方式,可以清楚表明意图
- 4、使所有的域都成为私有的
- 这样可以防止客户端获得访问被域引用的可变对象的权限
- 提供公有的方法来访问域,但不能修改
- 5、确保对于任何可变组件的互斥访问
- 如果类具有只想可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用
- 永远不要使用客户端提供的对象来初始化这样的域,也不要从任何访问方法中返回该类的对象引用
- 在构造器、访问方法和readObject方法中,请使用保护性拷贝技术(见第39条)
对于有些类而言,要求它们是不可变是不切实际的。如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。 因此,除非有令人信服的理由要使域变成是非final的,否则要使每个域都是final的。
第16条:复合优先于继承
- 如果使用继承,当超类实现细节改变的时候,会影响到子类的正确性
- 只有当子类真正是超类当子类型(subtype)时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在”is-a”关系当时候,类B才应该扩展类A。
- 使用组合(composition)的方法代替继承,在新的类中增加一个私有域,它引用现有类的一个实例。
- 新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为”转发”。
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
对于专门为了继承而设计并且具有良好文档说明的类而言:
- 首先,该类的文档必须精确的描述覆盖每个方法所带来的影响。换句话说,该类必须有文档说明它可覆盖的方法的自用性。(参考第16条中的范例代码)
- 构造器绝不能调用可被覆盖的方法。无论是直接调用还是间接调用。
第18条:接口优于抽象类
- 因为Java中类只允许单继承,所以抽象类作为类型定义收到了极大的限制
- 接口一旦被公开发行,并被广泛实现,再想改变这个接口,几乎是不可能的
- 抽象类可以使用 接口 + 骨架实现 的方式进行等价实现
- 其实JDK8支持了 接口默认实现,接口和抽象类之间的差别就更小了
综上所述,只需要根据抽象类和接口在语义上的区别进行使用就行了。
第19条:接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型
(type)。因此,实现了接口,就表明 客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。”常亮接口”模式 就是对接口的不良使用。应该使用类中的静态常量的方法,而不是接口。如果常量有很多,想要省略常量名 之前的类名,可以使用”静态导入”的方式。
第20条:类层次优于标签类
本节内容只需要理解标签类
这个概念就行了,对于面向对象的语言Java来说, 正常的思维就应该是抽象出共有特征为接口和抽象类,而不是使用标签类这种形式。
第21条:用函数对象表示策略
就是使用函数对象来实现类似函数指针,代理,lambda表达式 的功能,基本内容参考策略模式的实现即可。
第22条:优先考虑静态成员类
嵌套类有如下四种:
- 静态成员类(static member class)
- 非静态成员类(non-static member class)
- 匿名类(anonymous class)
- 局部类(local class)
除了第一种之外,其他都被称为内部类。 嵌套类也可以用private,public等访问修饰符进行修饰,其用法和域的用法完全一致。 静态成员类是最简单的一种嵌套类,最好把它看做普通的类,只是碰巧被声明在另一个类的内部而已。 它可以访问外围类的所有成员,包括那些被声明为私有的成员。 非静态成员类的每个实例都隐含着与外围类的一个外围实例(inclosing instance)相关联。在非 静态成员类的实例方法的内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实 例的引用。 如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使他成为静态成员类,而不是非静态成员类。 否则,每个实例都将包含一个额外的指向外围对象的引用。保存这份引用需要消耗时间和空间,并且会导致外围实例在符合垃圾 回收时仍然得以保留。
第5章 泛型
第23条:请不要在新代码中使用原生态类型
概念介绍
- 泛型(generic) 声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口。 比如从Java 1.5版本起,List接口就只有单个类型参数E,表示列表的元素类型。泛型类和接口统称为泛型。
- 原生态类型(raw type) 每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。 例如,与List
相对应的原生态类型类型是List。如果使用原生态类型,就失去了泛型在安全性和表述性f方面的所有优势。 现在之所以还支持原生态类型,是为了提供兼容性。 - 无限制的通配符类型(unbounded wildcard type) 如果要使用泛型,但不确定或者不关心具体的类型参数,就可以使用一个问号代替。 类如,泛型Set
的无限制通配符类型Set<?>,这是最普通的参数化Set类型,可以持有任何集合。
List
- 原生态类型 List 逃避了泛型检查,参数化的类型 List
- 泛型的子类型化(subtyping):List
是原生态类型List的一个子类型,但不是参数化类型List - 使用List这样的原生态类型,就会失去类型安全性,而使用List
Set<?> 和 Set 的区别
- 原生态类型不安全,通配符类型是安全的
第24条:消除非受检警告
- 修改代码,解决非受检警告
- 使用注解标记非受检警告:SuppressWarnings
- 永远不要在整个类上使用此注解
- 使此注解的作用范围越小越好
第25条:列表优先于数组
第26条:优先考虑泛型
第27条:优先考虑泛型方法
第28条:利用有限制通配符来提升API的灵活性
第29条:优先考虑类型安全的异构容器
第6章 枚举和注解
第30条:用enum代替int常量
第31条:用实例域代替序数
第32条:用EnumSet代替位域
第33条:用EnumMap代替序数索引
第34条:用接口模拟可伸缩的枚举
第35条:注解优先于命名模式
第36条:坚持使用Override注解
第36条:使用标记接口定义类型
第7章 方法
第38条:检查参数的有效性
第39条:必要时进行保护性拷贝
第40条:谨慎设计方法签名
第41条:慎用重载
第42条:慎用可变参数
第43条:返回零长度的数组或者集合,而不是null
第44条:为所有导出的API元素编写文档注释
第8章 通用程序设计
第45条:将局部变量的作用域最小化
第46条:for-each循环优先于传统的for循环
第47条:了解和使用类库
第48条:如果需要精确的答案,请避免使用float和double
第49条:基本类型优先于装箱基本类型
第50条:如果其他类型更适合,则尽量避免使用字符串
第51条:当心字符串连接的性能
第52条:通过接口引用对象
第53条:接口优先于反射机制
第54条:谨慎地使用本地方法
第55条:谨慎地进行优化
第56条:遵守普遍接受的命名惯例
第9章 异常
第57条:只针对异常的情况才使用异常
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
第59条:避免不必要地使用受检的异常
第60条:优先使用标准的异常
第61条:抛出与抽象相对应的异常
第62条:每个方法抛出的异常都要有文档
第63条:在细节消息中包含能捕获失败的信息
第64条:努力使失败保持原子性
第65条:不要忽略异常
第10章 并发
第66条:同步访问共享的可变数据
第66条:同步访问共享的可变数据
第68条:executor和task优先于线程
第69条:并发工具优先于wait和notify
第70条:线程安全性的文档化
第71条:慎用延迟初始化
第72条:不要依赖于线程调度器
第73条:避免使用线程组
第11章 序列化
第74条:谨慎地实现Serializable接口
第75条:考虑使用自定义的序列化形式
第76条:保护性地编写readObject方法
第77条:对于实例控制,枚举类型优先于readResolve
第78条:考虑用序列化代理代替序列化实例
参考资料
文档信息
- 本文作者:Bob.Zhu
- 本文链接:https://adolphor.github.io/2016/09/20/effective-java-second-edition/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)