读完 Java 名著《Effective Java》: 我整理了这 50 条技巧
《Effective Java》Java名著,必读。如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素质。
以下内容只记录了我自己整理的东西,还是建议读原文。为了聚焦知识点,一些说明故意忽略掉了。相当于是一篇摘要。转发+关注,然后添加VX(tkzl6666)获取一份《Effective Java》中文版第三版PDF资料
1、考虑用静态工厂方法替代构造函数
例子:
Integer.valueOf(“1”)、Boolean.valueOf(“true”)等。
优势:
可读性高(方法名)
性能(不一定创建对象)
灵活性高
下面针对三个优势进行一些解读。
可读性高
new Point(x,y)和Point.at(x,y)、Point.origin()。构造函数只能看出两个参数,不知其意,后者更易理解。
性能
在某些情况下,可以事先进行实例化一些对象,调用时直接调用即可,不需要进行改变。比如,Boolean。
灵活性高
可根据具体情况,返回子类。相当于更强大的工厂。直接从父类获取到子类。尤其适用于工具类(提供各种API)。例子:Collections。
2、多个构造函数时,考虑使用构造器
尤其在进行Android开发时,会碰到这种情况。通常是一个对象,具有多个成员变量可能需要初始化,常规方法,需要提供大量构造函数。例如:
有多种样式的警告框,为了调用方便,必须提供多个构造函数。否则用户在调用时,只能使用完整构造函数,容易犯错且无法进行阅读。极不灵活。如果采用另外一种方式,则可以解决,但会花费很多精力处理并发的情况:
调用时,通过调用各个参数的set方法进行设置。问题来了:
并发
无法进行参数校验。
例如,只创建了对象,设置了标题,却没有尺寸,相当于创建了一个没有尺寸的警告框。
在Android中,大量的控件都使用了构造器Builder。
于是,可以根据相应需求,进行相应设置,并在AlertDialog真正构造时,进行参数校验。就像这样:
上述例子,会成功抛出异常。
3、用私有化构造器或者枚举型强化Singleton。
Singleton指最多会被实例化一次的类。通常情况下,以前的做法是没有问题的。但是在某些高级情况,通过使用反射的相关知识访问private的构造函数,破坏Singleton。
另一种情况,在序列化的过程中,反序列化得到的对象已经不再是以前的对象(破坏了Singleton),这种情况下,可以通过单元素枚举型处理。
4、通过私有化构造器强化不可实例化的能力
有一些工具类,仅仅是提供一些能力,自己本身不具备任何属性,所以,不适合提供构造函数。然而,缺失构造函数编译器会自动添加上一个无参的构造器。所以,需要提供一个私有化的构造函数。为了防止在类内部误用,再加上一个保护措施和注释。
弊端是无法对该类进行继承(子类会调用super())。
5、避免创建不必要的对象
对象的重用
昂贵的对象,使用对象池
廉价的对象,慎用对象池。
现代JVM对廉价对象的创建和销毁非常快,此时不适于使用对象池。
6、消除过期的对象引用
以下三种情况可能会造成内存泄露:
自己管理的内存(数组长度减小后,pop出的对象容易导致内存泄漏)
缓存
监听和回调
自己管理的内存
对于自己管理的内存要小心,比如:
弹出的对象不再有效,但JVM不知道,所以会一直保持该对象,造成内存泄露。
解决:
缓存
缓存的对象容易被程序员遗忘,需要设置机制来维护缓存,例如不定期回收不再使用的缓存(使用定时器)。某些情况下,使用WeakHashMap可以达到缓存回收的功效。注,只有缓存依赖于外部环境,而不是依赖于值时,WeakHashMap才有效。
监听或回调
使用监听和回调要记住取消注册。确保回收的最好的实现是使用弱引用(weak reference),例如,只将他们保存成WeakHashMap的键。
7、避免显示调用GC
Java的GC有强大的回收机制,可以简单的记住:不要显示调用finalizer。可以这样理解:
jvm是针对具体的硬件设计的,然而程序却不是针对具体硬件设计的,所以,java代码无法很好的解决gc问题(因为他具有平台差异化)。另外,finalizer的性能开销也非常大,从这个角度上考虑也不应该使用它。
8、覆盖equals方法请遵守通用约定
自反性。 x.equals(x) == true
对称性。 当前仅当y.equals(x)==true时,x.equals(y)==true
传递性。 if(x.equals(y)&&y.equals(z)),y.equals(z)==true
一致性。
非空性。 x.equals(null)==false
9、覆盖equals方法时总要覆盖hashCode
为了保证基于散列的集合使用该类(HashMap、HashSet、HashTable),同时,也是Object.hashCode的通用约定,覆盖equals方法时,必须覆盖hashCode。
10、始终覆盖toString
Object的toString方法的通用约定是该对象的描述。注意覆盖时,如果有格式,请备注或者严格按照格式返回。
11、谨慎覆盖clone
12、考虑实现Comparable接口
13、使类和成员的可访问性最小化
目的是解耦。简单来讲,使用修饰符的优先级从大到小,private>protected>default(缺省)>public。如果在设计之初,设计为private修饰符后,在之后的编码过程如果不得不扩大其作用于,应该先检查是否设计的确如此。
子类覆盖超类,不允许访问级别低于超类的访问级别。(超类的protected,子类覆盖后不能改为default)。
成员变量决不允许是公有的。一旦设置为公有,则放弃了对他处理的能力。这种类并不是线程安全的。即使是final的,也不允许。除非希望通过public static final来暴露常量。成员变量总是需要使用setter和getter来维护。有一个例外:长度非零的数组。这是安全漏洞的一个根源。
改进:
另一种:
14、在公有类中使用访问方法而非公有成员变量(类似13)
15、使可变性最小化
16、复合优先于继承
继承有利于代码复用,但是尽可能不要进行跨包的继承。包内的继承是优秀的设计方式,一个包里的文件处在同一个程序员的控制之下。但是继承有其局限性:子类依赖于超类。超类一旦发生更改,将可能破坏子类。并且,如果超类是有缺陷的,子类也会得“遗传病”。
复合,即不扩展已有的类,而是在的类中新增一个现有类的。相当于现有类作为一个组件存在于新类中。如此,将只会用到需要用到的东西,而不表现现有类所有的方法和成员变量。新类也可以称为“包装类”,也就是设计模式中的Decorate模式。
17、要么就为继承而设计,并提供文档说明,要么就禁止继承
18、接口优于抽象类
19、接口只用于定义类型
20、类层次优先于标签类
21、用函数对象表示策略
函数参数可以传入类似listener的对象,目的是使用listener中的方法。如果使用匿名的参数,每一次调用会创建新的对象。可以将listener声明为成员变量,每次都复用同一个对象,并且可以使用静态域(static变量)。比如String类的CASE_INSENSITIVE_ORDER域。
22、优先考虑静态类成员
嵌套类的目的应该只是为了他的外围类提供服务,如果以后还可能用于其他环境中,则应该设计为顶层类。静态类相当于一个普通的外部类,只是恰好声明在了一个类内部。通常的用户是:Calculator.Operation.PLUS等。和普通类的区别只是,在PLUS前,有了2个前缀,来表明其含义。而非静态类必须存在于外部类对象中。不要手动在外部创建一个内部非静态类对象,创建的过程是:instance.New MemberClass()。这非常奇怪。
如果成员类不需要访问外围类,则需要添加static,使他成为静态成员类,否则每个实例都将包含一个额外指向外围对象的引用。将会影响垃圾回收机制。
23、应指定泛型的具体类型,而不是直接使用原生类型。
例如,应该指定List<E>,而不建议直接使用List。
24、消除非首检警告
在使用IDE进行编码时,强大的IDE都会在你编码过程中提示warning,需要尽可能的消除warning,至少,应该小心这些warning。慎用SuppresWarning,如果IDE提示你可以通过添加该注解解决掉warning,请不要那么做。如果实在要使用,请添加注释说明原因。
25、列表优先于数组
类比泛型,数组是有一定缺陷的。List<SuperClass>和List<SubClass>是没有关系的,而Sub[]是Super[]的子类。
从代码中可以看到,使用泛型,会提前发现错误。
26、优先考虑泛型
27、优先考虑泛型方法
28、利用有限制通配符来提升API的灵活性
PECS,producer-extends,consumer-super。
所有comparable和comparator都是消费者(Consumer)。
29、优先考虑类型安全的异构容器
30、用enum代替int常量
枚举型在java中非常强大,当需要一组固定常量时,使用enum比int好很多。比如代码可读性,安全性等。
31、enum用实例域代替序数
永远不要像第一种的方式,利用序数访问enum,需要在构造函数中使用参数来初始化。
32、用EnumSet代替位域
以上叫做位图法,但是有更好的方案来传递多组常量——EnumSet。
33、用EnumMap代替序数索引
任何时候都不要使用enum的ordinal()方法。
34、用接口模拟可伸缩的枚举
35、注解优先于命名模式
36、坚持使用Override注解
38、检查参数的有效性
公有方法检查参数,参数异常需要跑出Exception。私有方法利用断言assertion检查参数。
39、必要时进行保护性拷贝
假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性的设计程序。以下是一个不可变类的设计。
注意:保护性拷贝是在检查参数之前进行的,防止多线程的影响。不要使用clone方法进行保护性拷贝。
以上方法防御了传入参数的修改,但是对于get方法获取到的对象,仍然可以被修改,通过以下方法可以防止这种攻击。
40、谨慎设计方法签名
41、慎用重载
42、慎用可变参数
43、返回0长度的数组或者集合,而不是null
null一般用于表示没有被初始化或处理,如果方法返回了null,则需要在上层做更多的处理,以防止NPE。
44、为所有导出的API元素编写文档注释
正确的javadoc文档,需要每个被导出的类、接口、构造器、方法和域之前增加文档注释。注释应该是对实现透明的,只需要简洁的描述它和客户端之间的约定。并且,还应该附上该方法的副作用。
45、将局部变量的作用域最小化
46、for-each优先于for循环
for-each规避掉了for循环的index变量的引用,通常来说它是不必要的——会增加引入错误的风险,并且风险一旦发生,很难被发现。不过有三种情况下,无法使用for-each(注:在jdk1.8中已经很好的解决了这些问题)。
过滤
转换
48、如果需要精确的答案,请避免使用float和double
float和double是执行的二进制浮点运算,目的是在广泛数值范围上使用精确的快速近似计算而设计的。然而他们并没有提供完全精确的计算(实际应用中,经常会碰到出现x.99999等结果)。尤其是,在进行货币计算时,他们并不适用。比如:
得到的结果将是:0.610000000001。
为了解决这个问题,需要使用BigDecimal。然而这也有一些问题,相对于普通的运算,它显得更加麻烦,而且也更慢。通常来说后一个缺点可以忽略,但是前者可能会让人很不舒服。有一种做法是将需要处理的数值*10(或更多),使用int进行计算,不过需要你自己处理四舍五入等操作。
49、基本类型优先于装箱基本类型
基本类型只有值,装箱类具有与他们值不同的同一性。
基本类型只有功能完备的值,装箱类还具有非功能值:
null。
所以你可能会碰到NPE
基本类型省空间省时间
50、如果有更精确的类型,请避免使用字符串
字符串不适合代替其他值的类型。
例如:int,boolean等
不适合代替枚举类型(第30条)
51、当心字符串连接的性能
操作符“+”可以将多个字符串进行连接。但是在大规模使用“+”的情况下,连接n个字符串的开销是n的平房级时间。这是由于字符串的不可变性导致的。在这种情况下请使用StringBuilder进行连接。
52、通过接口引用对象
53、接口优先于反射机制
使用反射机制会带来以下的问题:
丧失了编译期类型检查
代码笨拙冗长
性能损失
反射基本上只适合用在编写组件时、代码分析器、RPC等场景下使用。在使用反射机制时,如果可能,尽可能只通过反射机制实例化对象,而访问方法时,使用已知的接口或者超类。
54、谨慎使用JNI
55、谨慎进行优化
很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他原因——甚至包括盲目的做傻事。
——William A. Wulf
不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
——Donald E. Knuth
在优化方面,我们应该遵守两条规则:
规则1:不要进行优化。
规则2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的优化方案前,请不要进行优化。
——M. A. Jackson
这些格言比java的出现还要早20年。他们讲述了一个关于优化的深刻事实:优化的弊大于利。
要努力编写好的程序,而不是快的程序。低耦合的重要性远远大于性能。当程序编写得足够低耦合后,通过工具发现了性能瓶颈的代码块,才可以保证对其的修改不影响任何外部环境。
56、遵守普遍的命名规则
57、只针对异常情况才使用异常
不要尝试通过异常机制来做正常代码应该做的事情,比如,检查数组下标。
jvm很少对异常进行优化,因为它只用于不正常的情况。并且,如果你将代码放入try-catch代码块,jvm就丧失了本来可以对它进行的优化。
58、对于可恢复的情况使用受检异常,对于编程错误的情况使用运行时异常
如果期望调用者适当的恢复,则需要使用受检异常,强迫调用者使用try-catch代码块,或者将他们抛出去
当调用发生前提违例——违反约定的情况时,使用运行时异常,这个时候程序已经无法再执行下去了。
例如调用数组的-1索引。
评论 (2 条评论)