第2章 创建与销毁对象
第1条:考虑用静态方法代替构造器
获取实例的方法:公有构造器;公有的静态工厂方法
静态工厂方法优势:
- 有名称;
- 不必在每次调用它们时候第一创建一个新对象;
- 可以返回原返回类型的任何子类型的对象。
- 在创建参数化类型实例的时候,它们使代码更加简洁
静态工厂方法缺点:
- 类如果不含公有的或者受保护的构造器,就不能被子类化
- 与其他的静态方法实际上没有任何区别
第2条:遇到多个构造器参数时考虑用构建器
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择
NutritionFacts cocaCola=new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
第3条:用私有构造器或者枚举类型强化Singleton属性
Singleton属性一般采取两种方式:
1.静态final instance
2.私有final instance。提供静态getInstance方法返回,我们一般采取这种方式,因为它可以保证在API不便的情况下选定是否为Singleton或修改为其他形式。
3.只编写一个包含单个元素的枚举类型
第4条:通过私有构造器强化不可实例化的能力
对只有静态方法和域的类,主要用于把基本类型的值或数据类型上的相关方法组织起来(Math,Arrays),可以通过Collections的方法把实现特定接口的对象上的惊天方法组织起来,可以利用这种类把final类上的方法组织起来,以取代扩展该类的做法。
此工具类是不希望被实例化的,实例化对他么有任何意义。然后我们如果不提供构造器,jvm会自己提供,那还是会被实例化,那么我们只要在类中提供一个私有的构造器就可以了,并添加注释说明。
这样带来的问题是不能子类化,因为子类要求要求super父类的构造函数。
第5条:避免创建不必要的对象
->若一个方法频繁调用且每次生成相同的内部实例,可以作为static,如Map的keyset。
->维护自己的对象池来避免重复创建对象不是很好的做法,除非对象是非常重量级的,Object pool也增加了内存占用。
第6条:消除过期的对象引用
存在过期引用会导致无法GC,但清空对象引用应该是一种例外,而不是一种规范行为。
一下三种情况要考虑会发生内存泄露:
1.类自己申请内存管理
2.缓存,易忘记管理,如WeakHashMap可以自动处理没有被外部引用的缓存项。一般利用后台线程定时清理,也可以类似LinkedHashMap使用removeEldestEntry在添加条目时清理。对于复杂的缓存,必须直接使用java.lang.ref
3.监听器和其他回调,回调此时可以做成弱引用。
第7条:避免使用终结方法
JVM采用跟搜索算法进行GC对象选择,JVM发现需要回收的对象(当然直观的来看就是没有引用的对象了)会做一次标记并进行一次筛选,筛选的条件就是此对象是否要执行finalize方法(对象未覆盖或finalize已被JVM执行过一次都被视为不需要执行),需要执行的放在F-Queue中,并稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行,但执行仅指虚拟机会触发这个方法,但不承诺会等待此方法运行结束,原因很简单,不可能因为等待这一个进程而使得其他要回收的资源一直等待,这样会导致GCC崩溃。当然一般也没有人会去这样做,毕竟教材没有这样教的,这里effective java中提到了两个用途很重要去掌握,回收资源一般作为一种显示的stop方法。finalize方法的作用:
1. 当用户没有调用stop,这时候在finilize中回收资源总比不回收强,相当于补充一个安全网。(Android中最典型的是MediaPlayer,如果不回收资源将会带来很大的问题,可以查看Android确实是这样做的,它调用的方法如下:
protected void finalize() { native_finalize(); }
具体含义不是这里应该继续讨论的问题,但是需要关注的是如果去做,这里MediaPlayer做的不是realse一样的内容,实际需要考虑finalize执行的问题,它不能耗时太长且不保证执行)
2. 普通对象通过本地方法委托给一个本地对象(本地对等体),因为本地对等体不是普通对象,gC不知道它,当本地对等体不拥有关键资源时finilize是最合适的工具。
第三章 对于所有对象都通用的方法
第8条:覆盖equals请遵守通用约定
覆盖equals的诀窍
- 使用==操作符检查“参数是否为这个对象的引用”。
- 使用instanceof操作符检查“参数是否为正确类型”。
- 把参数转换成正确的类型。
- 对于该类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。
- 当你编写完一个equals方法后,应该问自己三个问题:它是否是对称的、传递的、一致的?
同时还有几个告诫:
- 覆盖equals方法总要覆盖hashCode;
- 不要企图让equals方法过于智能;
- 不要将equals声明中的Object对象替换为其他的类型(最好加上@Override来确保覆盖)。
第9条:覆盖equals时总要覆盖hashCode
Object规范:
1.在应用程序执行期间,只要对象的equals方法的比较操作用到的信息没有被修改,那么对这一个对象调用多次,hashCode方法都必须始终如一的返回同一个整数。
2.如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。--->没有覆盖hashCode会违反这条
3.如果两个对象啊根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。--->hash码相等但对象不同,hash碰撞,必须对对象再次hash计算生成新的hashCode。
一个好的散列函数倾向于为不相等的对象产生不相等的散列码
第10条:始终要覆盖toString
toString方法应该返回对象中包含的所有值得关注的信息。
无论是否决定指定格式,都应该在文档中明确地表明你的意图。
无论是否指定格式,都为toString返回值中包含的所有信息,提供一种编程式的访问途径。避免需要信息的程序员不得不自己解析字符串。
第11条:谨慎地覆盖clone
好的方法是提供一个拷贝构造器或拷贝工厂。
第12条:考虑实现Comparable接口
实现Comparable接口可以利用其有序性特点,提高集合使用/搜索/排序的性能
Contact
sgn(x.compareTo(y)) == - sgn(y.compareTo(x)),当类型不对时,应该抛出ClassCastException,抛出异常的行为应该是一致的
transitive: x.compareTo(y) > 0 && y.compareTo(z) > 0 ==> x.compareTo(z) > 0
x.compareTo(y) == 0 ==> sgn(x.compareTo(z)) == sgn(y.compareTo(z))
建议,但非必须:与equals保持一致,即 x.compareTo(y) == 0 ==> x.equals(y),如果不一致,需要在文档中明确指出
TreeSet, TreeMap等使用的就是有序保存,而HashSet, HashMap则是通过equals + hashCode保存
当要为一个实现了Comparable接口的类增加成员变量时,不要通过继承来实现,而是使用组合,并提供原有对象的访问方法,以保持对Contract的遵循
实现细节
优先比较重要的域
谨慎使用返回差值的方式,有可能会溢出
第四章: 类和接口
第13条:使类和成员的可访问性最小化
私有的:只有在声明该成员的顶层类内部才可访问这个成员。private
包级私有的 :声明该成员的包内部的任何类都可以访问这个成员。 default
受保护的:声明该成员的类的子类可以访问这个成员。protected
公有的:任何地方都可以访问。public
如果方法覆盖了超类中的一个方法,子类中的访问级别不许低于超类中的访问级别。
除了公有静态final域的例外情况(常量)外,公有类都不应该包含公有域,并且要确保公有静态final域所引用的对象都是不可变的。
第14条:在公有类中使用访问方法而非公有域
公有类永远都不应该暴露可变的域
第15条:使可变性最小化
不可变类:其实例不能被修改
不可变类的规则:
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展
- 使所有的域都是final的
- 使所有的域都成为私有的
- 确保对于任何可变组件的互斥访问
不可变对象本质上是线程安全的,它们不要求同步
不可变类可以提供一些静态工厂,它们把频繁被请求的实例缓存起来,从而当现有实例可以符合请求的时候,就不必创建新的实例。
不可变对象可以被自由地共享导致的结果是,永远不需要进行保护性拷贝。
不可变类唯一的缺点是,对于每个不同的值都需要一个单独的对象。
坚决不要为每个get配套一个set
尽量标记为final
第16条:复合优先于继承
如果超类在后续的发行版本中获得了一个新的方法,并且不幸的是,你给子类提供了一个签名相同但返回类型不同的方法,那么这样的子类将无法通过编译。如果给子类提供的方法带有与新的超类方法完全相同的签名和返回类型,实际上就覆盖了超类的方法。
装饰者模式
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。
有两种方法禁止子类化:1.把类声明为final的。2.把构造器变成私有的,或者包级私有的,并增加一些公有的静态工厂方法来代替构造器。
第18条:接口优于抽象类
抽象类允许包含某些方法的实现,但是接口则不允许。
java只允许单继承,抽象类受到了极大限制。
现有的类可以很容易被更新,以实现新的接口。
接口是定义minin(混合类型)的理想选择。如Comparable
接口允许我们构造非层次结构的类型框架。
当演变容易性比灵活性和功能更为重要时,应该使用抽象类来定义类型。
第19条:接口只用于定义类型
常量接口模式是对接口的不良使用。
如果要导出常量有几种方案
- 如果常量与某个现有的类或者接口紧密联系,就把常量添加到类或接口中
- 如果最好被看做枚举类型的成员,就应该用枚举类型。
- 使用不可实例化的工具类。(构造方法为private)利用静态导入机制避免用类名修饰变量名。
第20条 类层次优于标签类
标签类过去冗长,容易出错,并且效率低下。
用继承
第21条:用函数对象表示策略
java没有函数指针,但是用对象引用可以实现策略模式。
comparator
第22条:优先考虑静态成员类
嵌套类有四种:
- 静态成员类。可以访问外围类的所有成员。
- 非静态成员类。每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例的方法。
- 匿名类
- 局部类
第五章 泛型
第23条:请不要在新代码中使用原生态类型
第24条:消除非受检警告
非受检警告很重要,每一条警告都表示可能在运行时抛出ClassCastException异常。
如果无法消除,就在尽可能小的范围内用注解禁止该警告。
每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是不安全的。
第25条:列表优先于数组
数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。数组提供了运行时的类型安全,但是没有编译时的类型安全。
第26条:优先考虑泛型
elements=new E[DEFUALT_INITIAL_CAPACITY];
会报错。不能创建不可具体化的类型的数组,如E。
解决方案一:elements=(E[])new Object[DEFUALT_INITIAL_CAPACITY]; 并禁止警告
解决方案二:将elements域类型从E[]改为Object[]。 E result=(E)elements[--size];
第27条:优化考虑泛型方法
泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。
第28条:利用有限制通配符来提升API的灵活性
PECS:producer-extends,consumer-super.
如果参数化类型表示一个T生产者,就使用<? extends T>
如果是一个T消费者,就使用<? super T>
所有的comparable和comparator都是消费者。
第29条:优先考虑类型安全的异构容器
泛型的一般用法,限制你每个容器都只能有固定数目的类型参数。
你可以通过将类型参数放在键上而不是容器上来避开这一限制。
第6章 枚举和注解
第30条:用enum代替int常量
int常量是十分脆弱的,编译时常量,如果与枚举常量相关联的int发生了变化,客户端就必须重新编译。
将int枚举常量翻译成可打印的字符串,并没有便利的方法。
枚举类型背后的基本思想:通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。枚举类型是实例受控的。是单例的泛型化。枚举提供了编译时的类型安全。
在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法。(特定于常量的方法实现)
加班工资的计算:
每当添加一个枚举常量时,就强制选择一种加班报酬策略。
将加班工资计算移到一个私有的嵌套枚举中,将这个策略枚举的实例传到枚举的构造器中。之后枚举将加班工资计算委托给策略枚举。
第31条:用实例域代替序数
永远不要根据枚举的序数导出与它关联的值,而是要讲它保存在一个实例域中。
第32条:用EnumSet代替位域
EnumSet.of(Style.BOLD,Style.ITALIC)
第33条:用EnumMap代替序数索引
最好不要用序数来索引数组,而要使用EnumMap。如果是多维的,用EnumMap<...,EnumMap<...>>
第34条:用接口模拟可伸缩的枚举
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。
第35条:注解优先于命名模式
命名模式的缺点:
- 文字拼写错误会导致失败,而且没有任何提示。
- 无法确保它们只用于相应的程序元素上。
- 没有提供将参数值与程序元素关联起来的好方法。
第36条:坚持使用Override注解
在你想要覆盖超类声明的每个方法声明中使用Override注解。
第37条:用标记接口定义类型
标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口。
标记接口定义的类型是由被标记类的实例实现的。
标记接口可以被更加精确地进行锁定。
如果想要定义类型,一定要使用接口。
什么时候用标记注解?
- 如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解。
第7章 方法
第38条 检查参数的有效性
对于公有的方法,用Javadoc的@throws标签在文档中说明违反参数值限制时会抛出的异常。
非公有的方法通常使用断言来检查参数。
断言如果失败,会抛出AssertionError;如果断言没有起到作用,本质上也不会有成本开销。
有些参数被保存起来供以后使用,如构造器。检查构造器参数的有效性十分重要。
第39条:必要时进行保护性拷贝
保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。
对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝。
只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必再为保护性拷贝操心。
有经验的程序员通常用Date.getTime()返回的long基本类型作为内部的时间表示法,而不是使用Date对象引用。之所以这样做,是因为Date是可变的。
如果类所包含的方法或者构造器的调用需要移交对象的控制权,这个类就无法让自身抵御恶意的客户端。
第40条:谨慎设计方法签名
API设计技巧:
- 谨慎地选择方法的名称
- 不要过于追求便利的方法
- 避免过长的参数列表:
- 把方法分解成多个方法
- 创建辅助类,用来保存参数的分组
- 从对象构建到对象调用都采用Builder模式。
- 对于参数类型,要优先使用接口而不是类。比如用Map
- 对于boolean参数,优先使用两个元素的枚举类型
第41条:慎用重载
当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法被执行;最为具体的那个覆盖版本会得到执行。
而重载,对象的运行时类型不会影响重载,选择工作是在编译时进行的,完全基于参数的编译时类型。
安全策略:永远不要导出两个具有相同参数数目的重载方法。
第42条:慎用可变参数
第43条:返回零长度的数组或者集合,而不是null
对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要这种曲折的处理方式。这样做很容易出错。因为编写客户端程序的程序员可能会忘记写这种专门的代码来处理null返回值。
集合值的方法也可以做成在每当需要返回空集合时,都返回一个不可变的空集合。Collections.emptySet、emptyList和emptyMap方法正是你所需要的。
return Collections.emptyList();
第44条:为所有导出的API元素编写文档注释
第8章 通用程序设计
第45条:将局部变量的作用域最小化
小心while循环的剪切-粘贴错误
for循环的优势
第46条:for-each循环优先于传统的for循环
for-each循环 简洁性,预防bug
不能使用for-each循环:
- 过滤。如果需要遍历集合并删除选定元素,必须使用显示迭代器
- 转换。
- 平行迭代
第47条:了解和使用类库
不要重新发明轮子
第48条:如果需要精确的答案,请避免使用float和double
可以使用BigDecimal,允许完全控制舍入,性能差。
也可以用int和long,自己记录小数点。没超过9位用int,没超过18位,用long
第49条:基本类型优先于装箱基本类型
基本类型优于装箱基本类型。
当程序用==操作符比较两个装箱基本类型时,它做了个同一性比较,这几乎肯定不是你所希望的。当程序进行涉及装箱和拆箱基本类型的混合类型计算时,它会进行拆箱,当程序进行拆箱时,会跑出NullPointer异常。程序装箱基本类型值时,会导致高开销和不必要的对象创建。
用装箱的场景:
- 作为集合中的元素、键和值。
- 参数化类型中,必须使用装箱基本类型
- 进行反射的方法调用时
第50条:如果其他类型更合适,则尽量避免使用字符串
字符串不适合代替其他的值类型。
字符串不适合代替枚举类型。
字符串不适合代替聚集类型
字符串不适合代替能力表。
第51条:当心字符串连接的性能
为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间。
使用StringBuilder代替。
第52条:通过接口引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,都应该用接口类型进行声明。
如果没有合适的接口类型,完全可以用类而不是接口来引用对象:
- 值类。通常是final的,很少有对应的接口。
- 对象属于一个框架,而框架的基本类型是类
- 类实现了接口,但是它提供了接口中不存在的额外方法。
第53条:接口优先于反射机制。
反射的缺点:
- 丧失了编译时类型检查的好处
- 执行反射访问所需要的代码笨拙冗长
- 性能损失
第54条:谨慎的使用本地方法
本地代码的缺点:
- 不安全
- 平台相关
- 降低性能
第55条:谨慎地使用优化
不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。
第56条:遵守普遍接受的命名惯例
第9章:异常
第57条:只针对异常的情况才使用异常
不要把异常用于普通的控制流
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
运行时异常(RuntimeException)也称作未检测的异常(unchecked exception),这表示这种异常不需要编译器来检测。RuntimeException是所有可以在运行时抛出的异常的父类。一个方法除要捕获异常外,如果它执行的时候可能会抛出RuntimeException的子类,那么它就不需要用throw语句来声明抛出的异常。
例如:NullPointerException,ArrayIndexOutOfBoundsException,等等
受检查异常(checked exception)都是编译器在编译时进行校验的,通过throws语句或者try{}cathch{} 语句块来处理检测异常。编译器会分析哪些异常会在执行一个方法或者构造函数的时候抛出。
第59条:避免不必要地使用受检的异常
第60条:优先使用标准的异常
第61条:抛出与抽象相对应的异常
更高层的实现应该捕获低层的异常,同时跑出可以按照高层抽象进行解释的异常。这种做法称为异常转译
第62条:每个方法抛出的异常都要有文档
第63条:在细节消息中包含能捕获失败的信息
第64条:努力使失败保持原子性
第65条:不要忽略异常
第10章 并发
第66条:同步访问共享的可变数据
如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,还可以保证进入同步方法或者同步代码块的每个进程,都看到由同一个锁保护的之前的所有的修改效果。
不要使用Thread.stop。要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询一个boolean域。
第67条:避免过度同步
为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般地讲,要限制同步区域内部的工作量。
第68条:executor和task优先于线程
不直接使用线程,利用Executor Framework
第69条:并发工具优先于wait和notify
java.util.concurrent中更高级的工具分为三类:Executor Framework、并发集合以及同步器。
并发集合为标准的集合接口提供了高性能的并发实现。例如ConcurrentMap、BlockingQueue
同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。如CountDown Latch(倒计数锁存器)是一次性的障碍,允许一个或者多个线程等待一个或者多个线程来做某些事情。
对于间歇性的定时,始终应该优先使用System.nanoTime().
如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait。
循环会在等待之前和之后测试条件:
- 在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。
- 在等待之后测试条件,当条件不成立的话继续等待,这对于确保安全性是必要的。
可使一个线程苏醒的理由:
- 另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。
- 条件并不成立,但是另一个线程可能意外地或恶意地调用了notify。
- 通知线程在唤醒等待线程时可能会过度大方。
- 在没有通知的情况下,等待线程也可能会苏醒过来。
一般情况下,应该优先使用notifyAll,而不是notify.
第70条:线程安全性的文档化
一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
不可变的(immutable)这个类的实例不可变。不需要外部同步。如String,Long,BigInteger
无条件的线程安全——类的实例可变,但是这个类有足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。包括Random,ConcurrentMap
有条件的线程安全——有些方法为进行安全的并发使用而需要外部同步
非线程安全——这个类的实例可变,为了并发使用它们,客户必须利用自己选择的外部同步包围每个方法调用。这个例子包括通用的集合实现,例如ArrayList和HashMap
线程对立的——不能安全地被多个线程并发使用,即使所有方法调用都被外部同步包围。线程对立的根源通常在于,没有同步地修改静态数据。
第71条:慎用延迟初始化
大多数的域应该正常初始化,而不是延迟初始化。
如果为了性能目标,或者是为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。
对于实例域,使用双重检查模式。
对于静态域,使用lazy initialization holder class idiom
对于可以接受重复初始化的实力域,使用单重检查模式。
第72条:不要依赖于线程调度器
不要让应用程序的正确性依赖于线程调度器。否则,结果得到的应用程序将既不健壮,也不具有可移植性。作为推论,不要依赖Thread.yield或者线程优先级。
第73条:避免使用线程组
应该用线程池executor
第11章 序列化
第74条:谨慎地实现Serializable接口
最大代价是:一旦一个类被发布,就大大降低了改变这个类的实现的灵活性
实现Serializable第二个代价:增加了出现bug和安全漏洞的可能性。反序列化机制是一个隐藏构造器。
第三个代价:随着类发行新的版本,相关的测试负担也增加了。
实现Serializable接口是个严肃的承诺,必须认真对待。如果一个类是为了继承设计的,则更加需要加倍小心。对于这样的类而言,在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折中方案是,提供一个可访问的无参构造器,这种方案允许但不要求子类实现Serializable接口。
第75条:考虑使用自定义的序列化形式
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式的缺点:
- 它使这个类的导出API永远地束缚在该类的内部表示法上。
- 它会消耗过多的空间。
- 它会消耗过多的时间。
- 它会引起栈溢出
transient修饰符表明这个实例域从一个类的默认序列化形式中省略掉。
第76条:保护性地编写readObject方法
第77条:对于实例控制,枚举类型优于readResolve
第78条:考虑用序列化代理代替序列化实例