Effective Java部分读书笔记
2.创建和销毁对象
1.使用静态工厂方法代替构造器
一般使用构造器(构造函数)创建对象实例,还可以使用静态工厂方法来创建对象实例。
优点
使用静态工厂方法代替构造器创建对象实例有以下优点:
1)静态构造方法的名称可以是更加有意义的,具有更好的可读性,而构造器的名称必须与类名称保持一致。
2)静态工厂方法不必在每次调用时都创建一个新对象,这对于那些需要重复创建相同对象的场景下尤其有用。
3)静态工厂方法可以返回原返回类型的任意子类型对象
4)静态工厂方法在创建参数化实例的时候,使程序更加简洁。
缺点
1)使用静态工厂方法创建实例的类,一般要求构造器是私有的,也就是说这个类不能被继承,不能被子类实例化。
2)静态工厂方法与其他静态方法并没有实质的区别,不能显而易见的判断某静态方法是不是工厂方法,所以,人们就做了一个约定,静态工厂方法有一些惯用名称:例如valueOf、of、getInstance、newInstance、getType、newType。
参考:1 2
2.遇到多个构造器参数时要考虑用构建器
如果类的构造器(或者静态工厂方法)有多个参数,并且这些参数中有些是可选参数时,就要考虑使用Builder模式。
3.通过私有构造器强化不可实例化的能力
有些工具类不希望被实例化,因为实例化对它来说没有什么意思,例如java.lang.Math,java.util.Arrays。有人通过将类定义为抽象类来避免类的实例化,不过,这样容易引起误解,让用户误以为这个类是专门为了继承而设计的。我们可以通过一个简单的方法来避免类被实例化,就是显式地将类的构造器声明为私有的(private),这样就保证避免类的实例化(前提是类中的其他成员不调用这个私有的构造器)。
4.避免创建不必要的对象
也就是说最好是能够重用已有的对象,而不是在每次需要的时候就创建一个相同功能的对象。
1)如果对象是不可变的(immutable),那它始终可以被重用。基本类型的包装类、String、BigInteger和BigDecimal都是不可变类。以字符串为例:
String str=new String("Java");
这条语句执行时,每次都会创建一个新的String实例,不过这是不必要的,因为每次创建的实例是相同的,都是字符串"Java",而“Java”本身就是字符串实例。如果这种方法用在循环或者被频繁调用的方法中,就会创建成千上万个相同功能的实例。改进后的版本如下:
String str="Java"
这种方法并没有创建新实例,而是使用已创建好的“Java”实例。并且,如果在代码的其他位置,使用了相同的字符串字面常量,仍然不会创建新对象,而是仍然复用这个“Java”实例,对于其他的不可变类也是如此。
2)除了重用不可变类之外,还可以重用那些创建了之后就不会修改的可变对象。参考
6.清除过期的对象引用(内存泄漏)
一个对象,如果在程序之后的执行过程中不会再使用,正常情况下,其所在的内存区域应该被回收,以便于存储新的对象。不过,由于一些原因未被回收,还是占据着内存,导致可用内存量下降,就像内存变少了一样,称这种现象为内存泄漏。Java虽然具有垃圾回收的功能,不过也会发生内存泄漏。
1)如果是类自身管理内存,可能会发生内存泄漏。以数组实现的栈为例,栈独自管理数组所占的内存空间,代码实现如下:
1 public class Stack { 2 private Object[] elements; 3 private int size = 0; 4 private static final int DEFAULT_INITIAL_CAPACITY = 100; 5 public Stack() { 6 elements = new Object[DEFAULT_INITIAL_CAPACITY]; 7 } 8 public void push(Object e){ 9 ensureCapacity(); 10 elements[size++] = e; 11 } 12 public Object pop(){ 13 if(size == 0){ 14 throw new EmptyStackException(); 15 } 16 return elements[--size]; 17 } 18 private void ensureCapacity(){ 19 if(elements.length == size){ 20 elements = Arrays.copyOf(elements, 2 * size + 1); 21 } 22 }
出栈时,pop()方法只是简单的返回原栈顶元素的前一个元素,没有做其他任何的处理。假如,栈先增长到80,然后再收缩至20,之后的增长和收缩只是在20以内,那么数组中[21,80]存储的对之前对象的引用一直保持,尽管这些对象在程序中不在使用,垃圾收集器不会收集这些区域,这就造成了内存泄漏。解决办法很简单,在pop()方法里加一句代码(第5行)即可:
1 public Object pop(){ 2 if(size == 0){ 3 throw new EmptyStackException(); 4 } 5 elementsp[size] = null; 6 return elements[--size]; 7 }
2)把对象引用放到缓存中,也经常会引起内存泄漏。
3)监视器和其他回调也会引起内存泄漏
解决方法
可以通过仔细检查代码或者借助于Heap剖析工具(heap Profiler)发现内存泄漏问题。
7.避免使用终结方法
finalize()终结方法通常是不可预测的,应该避免使用。
3.对于所有对象都通用的方法
Object是Java中所有类的父类,它的equals、hashCode、toString、clone、finalize原则上应该被所有子类覆盖,这些方法都有明确的通用约定,任何子类覆盖这些方法的时候要遵循这些约定,否则就不能与其他依赖这些约定的类一起正常运作。
8.覆盖equals时请遵守通用约定
不要覆盖equlas的情况
1)类的实例本质上都是唯一的,即每次创建的对象实例都是唯一的
2)不关心类是否提供了判断“逻辑相等”的方法,也即是说类的使用过程中不会出现判断两个类实例是否相等的情况
3)父类已经实现了equals,子类利用这个equals可以判断自身实例是否“逻辑相等”。
覆盖equals方法时的约定
1)自反性:对象等于其自身。对于任意非空的对象x,有x.equals(x)=true。
2)对称性:对于任意非空x,y,如果x.equals(y)=true,那么y.equals(x)也等于true。
3)传递性:对于任意非空x,y,z,如果x.equals(y)=true,y.equals(z)=true,那么也有x.equals(z)=true。
4)一致性:如果两个对象相等,只要两个对象都未被修改,那么它们任意时刻都应该是相等的。
5)非空性:所有的对象都不等于null。
实现高质量equals
1)使用“==”检查参数“是否为这个对象的引用”,这只是一种性能优化。
2)使用操作符"instanceof"检查参数是否为正确的类型。
3)把参数向下转化(Object—>T)为正确的类型。
4)最重要的,检查参数中的各个域是否与对象中的各个区相匹配。如果域是除了double和float的基本类型,使用"=="进行比较。如果是对象引用域,递归调用equals。对于float类型,使用Float.compare方法,对于double,使用Double.compare,对float和Double进行特殊处理,是因为存在着Float.NaN、-0.0f以及类似的double常量。如果域是数组,将以上原则应用到每个元素上。
最后,覆盖equlas()是总要同时覆盖hashCode。不要讲equals中的Object类型替换为其他类型,因为这样只是重载了,而非覆盖。可以使用@Override关键字显示指出覆盖,供编译器检查,防止误把覆盖写成重载。
9.覆盖equals时总要覆盖hashCode
hashCode约定
1)在程序的执行期间,如果equals的比较操作所用到的信息没有被修改,也就是说对象还是那个对象,那么多次调用hashCode应该始终如一的返回同一个整数。不过,程序多次执行时,可以返回不同的整数。
2)通过equals比较相等的两个对象,调用hashCode应该返回相同的整数(哈希码)。
3)通过equals比较不相等的两个对象,调用hashCode不一定要返回不相等的整数。不过,为了减少哈希冲突,不相等的对象调用其实应该返回不同的整数。
不覆盖hashCode会怎么样
如果覆盖了equals的实现,未覆盖hashCode的话,会影响这个类与基于哈希的集合一起正常运作,例如HashMap,HashSet以及Hashtable。因为没有覆盖的话,会调用Object的hashCode实现,即使子类调用已重写的equals,得到两个相等的对象,不过通过Object的hashCode计算得出的哈希码却并不相同(本该相同),因此不能与基于哈希的集合一起运作。
10.始终要覆盖toString
Object提供的toString的输出包含类的名称,一个“@”已经标识对象的无符号十六进制散列码。例如,“Phone@163b91”。当对象被传递给println、printf字符串连接符"+"时,自动调用对象的toString()方法。应该重写toString,使其返回对本类有意义的信息。
4.类和接口
15.使可变性最小化
实例对象不能被修改的类成为不可变类。不可变类的对象实例包含的所有信息必须在创建时提供,并在对象的生命周期内保持不变。
如何实现不可变类
1)将类的所有字段声明为private类型,将字段定义为final类型,使字段在第一次初始化之后就不能再被修改。将类声明为final类型,表示不能被继承,防止子类无意识修改对象状态。
2)不提供任何修改对象字段的方法,也就是不提供任何改变对象状态的方法。
优点
1)不可变类易于设计、实现和使用,不容易出错且更加安全,不变的东西更容易控制。
2)不可变对象是线程安全的,因为即使是多个线程并发访问这类对象,它们也不会遭到破坏,因为并未提供更改对象状态的方法。
缺点
对于每个不同的值,不可变类都要提供一个单独的对象。这对于大型的对象来说,低价很高。例如上百万位的BigInteger。
7.方法
38.检查参数的有效性
对于包含参数的方法,在使用之前应该检查其参数的有效性。如果参数是索引、长度等要保证是非负数,如果是对象的引用,要保证是非空的。但是,并不是任何时候都要检查参数的有效性,如果参数的有效性检查是昂贵的,甚至是不切实际的,并且在方法的计算过程中已经隐含了对参数的有效性检查,此时不必检查参数的有效性。如果参数无效,可以采取抛出异常的方式,如下:
public static double getGravity(double mass) { // 参数有效性检查 if (mass <= 0) { throw new IllegalArgumentException("无效质量:" + mass); } return mass * 9.82; }
41.慎用重载
8.通用程序设计
48.如果需要精确的答案,避免使用float和double。
Java中的float和double在进行算术运算时,不保证得到完全精确的结果。例如
System.out.println(1-0.9)
的结果可能为0.100000001。
可以使用BigDecimal解决需要精确答案的问题。
52.通过接口引用对象
如果可以的话,优先使用接口引用对象,而不是使用类。
什么时候使用
如果类是实现了某个接口,也就是说类是有接口的话,优先考虑使用接口作为类型。不过,要意识到使用接口类型的对象,并不能访问类中新增的方法(相对接口来说)。
有什么好处
使用接口作为类型可以增加程序的灵活性,很容易实现接口实现类间的替换。声明Vector的两种方法:
//方法1 Vector<Subscriber> subscribers = new Vector<Subscriber>(); //方法2 List<Subscriber> subscribers = new Vector<Subscriber>();
如果需要更改接口的实现,将Vector改为ArrayList(Vector和ArrayList都是List接口的实现),如果程序中使用方法2的话,只需要将右侧的Vector改为ArrayList即可。
为什么要改变实现
新的实现可能会提供更好的性能。