Effecvtive Java Note
- 代码应该被重用,而不是被拷贝
- 同大多数学科一样,学习编程的艺术首先要学会基本的规则,然后才能知道什么时候可以打破这些规则
创建和销毁对象
1.考虑用静态工厂方法代替构造器。
优势:有名称、不必再每次调用他们的时候都创建一个对象、可以返回原类型的任何子类型的对象、代码变得更简洁
//抽象产品角色 public interface Car { public void drive(); } //具体产品角色 public class Benz implements Car { public void drive() { System.out.println("Driving Benz "); } } public class Bmw implements Car { public void drive() { System.out.println("Driving Bmw "); } } //工厂类角色 public class Driver { //工厂方法.注意 返回类型为抽象产品角色 public static Car driverCar(String s) throws Exception { //判断逻辑,返回具体的产品角色给Client if (s.equalsIgnoreCase("Benz")) return new Benz(); else if (s.equalsIgnoreCase("Bmw")) return new Bmw(); else throw new Exception(); } } //欢迎暴发户出场...... public class Magnate { public static void main(String[] args) { try { //告诉司机我今天坐奔驰 Car car = Driver.driverCar("benz"); // /下命令:开车 car.drive(); } } }
2.遇到对个构造器参数时要考虑用构造器
只要一个泛型机就能满足所有的builder,无论它们在构建那种类型的对象
public interface Builder<T> { public T build(); }
3.用私有构造器或者枚举类型强化singleton(实例化一次)属性
这种方法更简洁
public enum Elvis{ INSTANCE; public void something(){} }
4.通过私有构造器强化不可实例化的能力
5.避免创建不必要的对象
String s=new String("string"); //DON'T DO THIS!
String s="string";
这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且,他可以保证。对于所有在同一虚拟机中运行的代码,只要他们包含相同的字符串字面常亮,该对象会被重用。
Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; }
只因为打错了一个字符,变量sum被声明Long而不是long,意味着构造了2的31次方多余的Long实例。所以要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
通过维护自己的对象池来避免创建对象并不是一种很好的做法,除非池中的对象是非常重量级的。
public class Person { private Date brithdate; private static final Date BOOM_START; private static final Date BOOM_END; static { Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT")); gmt.set(1946,Calendar.JANUARY,1,0,0,0); BOOM_START=gmt.getTime(); gmt.set(1965,Calendar.JANUARY,1,0,0,0); BOOM_END=gmt.getTime(); } public boolean isBaby(){ return brithdate.compareTo(BOOM_START)>=0 &&brithdate.compareTo(BOOM_END)<0); } }
只创建了一次实例
6.消除过期的对象引用
消除过期引用最好的方法是让包含该引用的变量结束其生命周期(x=null)或者保存他们的弱引用(例如weakHashMap)
7.避免使用终结方法
Java语言规范不仅不保证终方法(finalize)会被及时执行,而且根本就不保证它们会被执行,当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这完全是可能的。例如,依赖终结方法来释放共享资源(比如数据库)上的永久锁,很容易让整个系统垮掉。其他原因:速速慢、不打印警告。
应该怎么做呢?提供一个显示的终止方法,例如:流的close、timer的cancel等
对于所有对象都通用的方法
8.覆盖equals时请遵守通用约定
①使用==操作符检查“参数是否为这个对象的引用“
②使用instance of操作符检查”参数是否为正确的类型“
③把参数转换成正确的类型
9.覆盖equals时总要覆盖hashcode
10.始终要覆盖tostring方法(简洁、易于阅读)
11.谨慎的覆盖clone
@Override protected Person clone() throws CloneNotSupportedException { return (Person) super.clone(); } }
返回的是person,这样有助于覆盖方法提供更多返回对象的信息,并且在客户端中不必转换。
clone架构与引用可变对象的final域(成员)的正常用法是不兼容的(不能克隆)
12.考虑实现comparable借口
如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序,那你就应该考虑实现这个接口。
类和接口
13.使类和成员的可访问性最小化
要区别设计良好的模块和设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。它可以有效的解除组成系统的各个模块之间的耦合关系,使得这些模块可以独立开发、测试、优化
14.在共有类中使用访问方法而非公有域(get/set方法)
15.使可变性最小化
Java平台包含许多不可变得类,其中有string、基本数据的包装类等,不可变得类比可变的类更易于设计,实现和使用,它们不容易出错,且更加安全。为了是类成为不可变,有以下原则:
①不要提供任何改变对象状态(属性)的方法
②保证类不会被扩张
③使所有域都是final
④使所有的域都成为私有的
public class Complex { private final double re; private final double im; public Complex(double re, double im) { this.re = re; this.im = im; } public Complex add(Complex complex){ return new Complex(re+complex.re,im+complex.im); } }
创建并返回新的complex实例,而不是修改这个实例。
不可变的对象本质上是线程安全的,它们不要求同步。
16.复合优先于继承
继承打破了封装性。子类依赖于超类中特定功能的实现细节,超类的实现有可能会随着版本的不同而有所变化,如果真的发生了变化,子类可能会招到破坏,即使它的代码完全没有改变。
不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例,这种设计叫做复合。它不依赖于现有类的实现细节,即使现有类添加了新的方法,也不会影响新的类。这个实现分类俩个部分:类本身和可重用的转发类,它包含了所有转发方法,没有其他方法。
转发类
public class ForwardingSet implements Set { private final Set set; public ForwardingSet(Set set){ this.set=set; } @Override public int size() { return set.size(); } @Override public boolean isEmpty() { return set.isEmpty(); } @Override public boolean contains(Object o) { return set.contains(0); } @NonNull @Override public Iterator iterator() { return set.iterator(); } @NonNull @Override public Object[] toArray() { return set.toArray(); } @Override public boolean add(Object o) { return set.add(o); } @Override public boolean remove(Object o) { return set.remove(o); } @Override public boolean addAll(@NonNull Collection c) { return set.addAll(c); } @Override public void clear() { set.clear(); } @Override public boolean removeAll(@NonNull Collection c) { return set.removeAll(c); } @Override public boolean retainAll(@NonNull Collection c) { return set.retainAll(c); } @Override public boolean containsAll(@NonNull Collection c) { return set.containsAll(c); } @NonNull @Override public Object[] toArray(@NonNull Object[] a) { return set.toArray(a); } }
统计add了几次
public class InstrumentedSet extends ForwardingSet { private int addCount=0; public InstrumentedSet(Set set) { super(set); } @Override public boolean add(Object o) { addCount++; return super.add(o); } @Override public boolean addAll(@NonNull Collection c) { addCount+=c.size(); return super.addAll(c); } public int getAddCount(){ return addCount; } }
InstrumentedSet类实现了set接口,并且拥有单个构造器,他的参数也是set类型,从本质上讲,这个类把set转换成另一个set,同时增加了计数功能,并且set怎么变化都不会影响子类的逻辑。他把set包装起来了,所以可以叫包装类。正也是Decorator(委托)模式。
17.要么为继承而设计,并提供文档说明,要么就禁止继承。
好的api文档应该描述一个给定的方法做了什么工作,而不是描述它是如何工作的。
构造器决不能调用可覆盖的方法,无论是直接调用还是间接调用,如果违反了这个原则,很可能导致程序失败。超类的构造器在子类的构造器之前,所以,子类中覆盖方法将会在子类的构造器之前就先调用。
public class Super { public Super(){ overrideMe(); } public void overrideMe() { } } public class Sub extends Super { private final Date date; Sub (){ date=new Date(); } @Override public void overrideMe() { System.out.print(date); } public static void main(String[]args){ Sub sub = new Sub(); sub.overrideMe(); } }
会空指针异常
18.接口优于抽象类
因为Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。
19.接口只用于定义类型
有一种接口叫常量接口,这是对接口的不良使用,实现常量接口会导致这样的实现细节泄露到该类的导出API中。如果这将来的版本中,这个类被修改了,它不再需要这些常量了,它依然必须实现这个接口。可以用枚举类型或者工具类。
简而言之,接口应该只被用来定义类型,它们不应该被用来到处常量。
19.类层次优先于标签类
标签类
public class Figure { enum Shape {RECTACNGLE, CIRCLE} final Shape shape; double length; double width; double radius; Figure(double radius){ shape=Shape.CIRCLE; this.radius=radius; } Figure(double length,double width){ shape=Shape.RECTACNGLE; this.length=length; this.width=width; } double area(){ switch (shape){ case RECTACNGLE: return length*width; case CIRCLE: return Math.PI*(radius*radius); default: throw new AssertionError(); } } }
这种类多个实现乱七八糟挤在单个类中,破坏了可读性,内存占用也增加了,因为实例承担着属于其他风格的相关的域。标签类容易出粗,并且效率低下。可以用类层次实现。
abstract class Figure{ abstract double area(); } class Circle extends Figure{ final double radius; Circle(double radius){ this.radius=radius; } @Override double area() { return Math.PI*(radius*radius); } }
这个类层次的每个类型的实现都配有自己的类,这些类没有受到不相关的数据域的拖累,也杜绝了由于遗漏Switch而导致运行失败的可能性。
简而言之,标签类很有有适用的时候,当你遇到一个标签域的现有类时,就要考虑将它重构到一个层次结构中去。
21.用函数对象表示策略
public interface Comparator <T>{ public int compare(T t1,T t2); } Arrays.sort(stringArray,new Comparator<String>(){ return s1.length()-s2.length(); }); public class Host { private static class StrlenCmp implements java.util.Comparator<String>,Serializable{ @Override public int compare(String o1, String o2) { return o1.length()-o2.length(); } } public static final java.util.Comparator<String> STRING_LEN_COMPARATOR=new StrlenCmp(); }
函数指针的主要用途就是实现策略模式,为了在Java中实现这种模式,要声明一个借口表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体的策略之被使用了一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略类时设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
22.优先考虑静态成员类
嵌套类是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为它的外部类提供服务。如果嵌套类可能用于其他的某个环境,那它应该是顶层类。嵌套类用四种:静态成员类、非静态成员类、匿名类和局部类。
非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围类实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类必须是静态内部类。
非静态内部类的每个实例都包含一个额外的指向外围对象的引用,保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留。
匿名类有许多限制,它们在被声明之外是无法实例化的,不能执行instanof测试,或者做任何需要命名类的其他事情。匿名类必须保持简洁,大约10行或者更少,否则会影响性能。
泛型
在没有泛型之前,从集合读取到的每一个对象都必须进行转换,如果有人不小心插入了类型错误的对象,在运行的转换处理就会出错。有了泛型之后,可以告诉编译器每个集合中接受哪些对象类型。编译器自动为你的插入进行转化,并在编译时告知是否插入了类型错误的对象,这样更安全,也更清楚。
23.请不要在新代码中使用原生生态类型
使用原生生态类型会在运行时导致异常,因此不要在新代码中使用,原生态类型只是为了与引用泛型之前的遗留代码进行兼容和相互提供,set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合,set<?>是一个通配符,表示只能包含某种未知对象类型的一个集合,set则是个原生态类型,它脱离了泛型系统,前俩种是安全的,最后一种是不安全的。
24.消除非受检警告
非受检警告很重要,不要忽略它们。每一种警告都表示可能在运行时抛出异常。要尽最大的努力消除这些警告,如果无法消除,可是可以证明引起警告的代码类型安全的,就可以在尽可能小的范围中,用@suppressWarnings注解禁止该警告,要用注释把禁止该警告的原因记录下来。
25.列表优先于数组
数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样,一般来说,数组和泛型不能很好的混合使用。如果你发现自己将他们混合起来使用,并且得到了编译时错误或者警告,你的第一反应应该是用列表代替数组。
26.优先考虑泛型
使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保他们不需要这种转换就可以使用。这通常意味着这把类做成泛型的。
27.优先考虑泛型方法
更加安全、不用转换就可以使用。
28.利用有限制通配符来提升api的灵活性
? entends E:接收E类型或者E的子类型对象,一般储存对象用。
? super E:接收E类型或者E的父类,一般取出对象的时候用。
29.优先考虑类型安全的异构容器
集合API的泛型限制了每个容器只能有固定数目的类型参数,你可以通过将类型参数(泛型Class(T))放在键上而不是容器上来避开这一限制。
枚举和注解
30.用enum代替int常量
enum Operation { PLUS,MINUS,TIMES,DIVIDE; Double apply(double x,double y){ switch (this){ case PLUS: return x+y; case MINUS:return x-y; case TIMES:return x*y; case DIVIDE:return x/y; } throw new AssertionError("Unknown op" + this); } }
这段代码可行,但不太好看,如果没有throw语句,他就不能进行编译。如果你添加了新的枚举常量,却忘记给switch添加相应的条件,就会运行失败。
enum Operation { PLUS { @Override Double apply(double x, double y) { return x+y; } },MINUS { @Override Double apply(double x, double y) { return x-y; } },TIMES { @Override Double apply(double x, double y) { return x*y; } },DIVIDE { @Override Double apply(double x, double y) { return x/y; } }; abstract Double apply(double x,double y); }
如果给operation添加新常量,你就不可能忘记提供apply方法,因为枚举类型中的抽象方法必须被它所有的常量中的具体方法所覆盖。
特定于常量的方法实现可以与特定于常量的数据结合起来。下面的operation覆盖了tostring来返回通常与该操作相关联的符号
public enum Operation { PLUS("+") { @Override Double apply(double x, double y) { return x+y; } },MINUS("-") { @Override Double apply(double x, double y) { return x-y; } },TIMES ("*"){ @Override Double apply(double x, double y) { return x*y; } },DIVIDE ("/"){ @Override Double apply(double x, double y) { return x/y; } }; private String symbol; Operation(String symbol) { this.symbol=symbol; } @Override public String toString() { return symbol; } abstract Double apply(double x, double y); } double x=3; double y=4; for (Operation operation : Operation.values()) { System.out.printf("%f %s %f = %f%n",x,operation,y,operation.apply(x,y)); }
与int常量相比,枚举要易读的多,也更安全,功能更强大。但是枚举在装载和初始化时会有空间和时间的成本。
31.用实例域代替序数
所有的枚举都有一个ordinal方法,他返回每个枚举常量中数字位置。但是常量进行重新排序,这个方法就会遭到破坏
public enum Ensemble { SOLO(1),DUET(2),TRIO(3); Ensemble(int i) { } }
32.用enumset代替位域
33.用enummap代替序数索引
34.用接口模拟可伸缩的枚举,参考30的例子
interface Operation{ double apply(double x,double y); }
35.注解优先于命名模式
在Java1.5前,一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理,例如,JUnit测试框架原本要求它的用户一定要用test作为测试方法名称的开头,这种方法文字拼写错误会导致失败,且没有任何提示,还会给人以测试正确的假象。用@Test
36.坚持使用Override注解
37.用标记接口定义类型
方法
38.检查参数的有效性
39.必要时进行保护性拷贝
40.谨慎设计方法签名
41.慎用重载
42.慎用可变参数
43.返回零长度的数组或集合,而不是null
44.为所有导出的api元素编写文档注释
通用程序设计
45.将局部变量的作用域最小化
要使局部变量的作用域最小化,最用力的方法就是在第一次使用的它的地方申明,如果变量在使用之前进行声明,这只会造成混乱,等用到该变量的时候,可能已经记不起该变量的类型或者初始值了
46.for-each循环优先于传统的for循环
完全隐藏迭代器或者索引变量,避免了混乱和出错的可能
47.了解和使用类库
不用重新发明轮子,如果你要做的事情看起来十分常见,有可能类库已经有某个类完成了这样的工作
48.如果需要精确的答案,请避免使用float和double
它们并没有提供完全精确的结果,尤其不适用于货比计算,因为要让一个float或double精确的表示0.1(或者10的任何其他负数次方值)是不可能的,请使用BigDecimal
49.基本类型优先于装箱类型
当可以选择的时候,基本类型要优先于装箱类型,基本类型更加简单,也更加快速。如果必须使用装箱类型,要特别小心!自动装箱减少了使用装箱类型的繁琐性,但是并没有减少他的风险。当程序用==操作附比较俩个装箱类型时,他做了个统一性比较,有可能抛出空指针异常。当程序装箱了基本类型值时,会导致高开销和不必要的对象创建。
50.如果其他类型更适合,则尽量避免使用字符串
51.当心字符串连接的性能(StringBuilder)
52.通过接口引用对象
53.接口优先于反射机智
丧失了编译时类型检查的好处:调用了不存在或不可访问的方法
执行反射访问所需要的代码非常的笨拙繁琐
性能损失
可以以反射的方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例
54.谨慎的使用本地方法
在使用本地方法之前务必三思,极少数情况下会需要使用本地方法来提高性能。要全面进行测试,本地代码中的一个bug就有可能破坏整个应用程序