effective java 学习心得
目的
记录一下最主要学习心得,不然凭我这种辣鸡记忆力分分钟就忘记白看了...
用静态工厂方法代替构造器的最主要好处
1.不必每次都创建新的对象
Boolean.valueOf
Long.valueOf
2.直接返回接口的子类型,对于外界来说并不需要关心实现细节,主要知道这个接口就行
Collections.unmodifiableList
......
为什么避免使用终结方法
1.终结方法不会被及时执行
2.不同jvm上实现不同
3.可能根本不会执行
4.在其中抛出的异常会被忽略
5.性能差
何时使用:
1.作为释放资源的备用方法
FileInputStream:
1 protected void finalize() throws IOException { 2 if ((fd != null) && (fd != FileDescriptor.in)) { 3 4 /* 5 * Finalizer should not release the FileDescriptor if another 6 * stream is still using it. If the user directly invokes 7 * close() then the FileDescriptor is also released. 8 */ 9 runningFinalize.set(Boolean.TRUE); 10 try { 11 close(); 12 } finally { 13 runningFinalize.set(Boolean.FALSE); 14 } 15 } 16 }
2.调用native方法释放native Object
3.调用父类的终结方法,因为子类覆盖父类,父类的方法可能不会被调用
equals方法
1.无法在扩展可实例化类的同时,既增加新的值组件,同时有保留equals约定.
所以如果子类equals传入了父类实例,却不是子类的实例,应该返回false.
2.equals不应该依赖于不可靠资源,equals方法应该对驻留在内存中的对象执行确定性的计算.
其他:
float比较可以用Float.compare
double同理
hashcode
1.覆盖equals,也应该覆盖hashcode.
2.相等的对象应该有相等的hashcode,有相等的hashcode的对象不一定是equals的
3.eclipse可以自动生成hashcode,一般不用自己写
clone
1.最好不要扩展Cloneable接口,而是用其他方法克隆,比如公司用BeanCopier
其他:clone虽然返回Object类型对象,但是数组clone的时候能够自动转型不需要额外操作...下面代码是没有编译错误的
1 String[] s = new String[]{"1"}; 2 String[] s2 = s.clone();
Comparable接口
1.有序集合contains可能会调用compareTo方法,而非有序集合contains则可能会调用equals来判断. (例子??)
使可变性最小化
不可变类的条件:
1.不同修改对象状态的方法(成员域没有set方法)
2.类不会被扩展(final修饰)
3.所有域都是final的
4.所有域都是私有的
5.外部可不获得不可变对象内部可变对象的引用,或者没有指向可变对象的域
但是感觉有些条件是多余的,比如String是不可变类,但是他就有个hash域,不是final的,反正只要这个类的域(状态)不可能被修改就可以了.
接口优于抽象类
1.实现了接口的类可以把对于接口方法的调用,转发到一个内部私有类的实力上,这个私有内部类扩展了骨架实现类.
比如List接口有个ListIterator<E> listIterator(); 方法, ArrayList中内私有内部类private class Itr implements Iterator<E>就实现了这个方法.
同时,这是私有内部类的一种用法,用来实现接口,Itr 依赖于外部的ArrayList,不能独立存在,但是又可以有自己额外的一些方法的实现.
接口只用于定义类型
1.常量接口模式是对接口的不良使用,实现常量接口会导致把实现细节泄露到该类的导出API中
用函数对象表示策略
1.因为策略接口被用作所有具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成共有的.相反,宿主类还可以导出共有的静态域,其类型为策略接口,具体的策略类可以是宿主类的私有嵌套类.
策略模式使用的时候一般是: 策略接口名称 引用名 = 调用具体策略的获得方法.
所以外界使用的时候是使用接口做为类型的而不是具体的策略类名.这样不需要关心策略的具体实现细节,也是多态的表现.
比如 private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable
String类的CaseInsensitiveComparator是私有的静态嵌套类,外界不可能获得这个类的引用,但是可以从String的共有的域public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
来获得Comparator接口CaseInsensitive的实现.
同时,这是静态内部类的一种用法,也是用来实现接口,与Itr依赖于外部的ArrayList不同的是这个Comparator不依赖于外部的String,所以是static的.并且很多时候是作为类的静态变量导出,而不是实例的成员域
所以说实现接口的时候如果是用内部类来实现的话,如果实现接口需要用到外部类的实例的域的话,那应该使用private class,如果依赖于外部的类的话可以使用private static class.另外就是非静态内部类的是依赖于外部类的实例的,所以如果外部类有static类型的域那是不可能指向非static的内部类的..
优先考虑静态成员类
1.静态成员类的一种常见用法是作为共有的辅助类,仅当与它的外部类一起使用时才有意义.
比如前面String的CaseInsensitiveComparator.
再比如把枚举类定义在类内部一样(例子??).
优先考虑静态成员类
1.静态类和非静态类的例子:
HashMap中的static class Entry<K,V> implements Map.Entry<K,V>, Entry不需要用到HashMap中的域
HashMap中的private final class KeySet extends AbstractSet<K>, KeySet需要用到HashMap中的域,比如size
请不要在新代码中使用原生态类型
消除非受检警告
1.数组具有协变的特性,而泛型没有,泛型是具体的.
String extends Object, 所以String[] 是Object[]的子类型, 而List<String>却不是List<Object>的子类型
创建泛型,参数化类型,类型参数的数组(List<E>[], List<String>[], E[])是非法的(创建实例new是非法的,引用名字是可以的), 例外是使用无限制通配符?
2. 类型参数T使用通配符?时,只能放null元素
3.因为类型擦除,所以instanceof和类文字(XXX.class)中必须使用原生类型
优先考虑泛型方法
1.泛型方法被调用的时候大多数时候是不需要制定类型参数的,这是由于类型推断的结果,但是构造方法是个例外,必须指明类型参数的具体值.
例如 Map<String, String> map = new HashMap<String, String>();因为是构造方法,所有必须指明类型参数为String.
2.通过包含类型参数的表达式类限制类型参数是允许的,这就是递归类型限制
例如:public static <T extends Comparable<T>> T max(List<T> list){....}
利用有限制通配符来提升API灵活性(泛型最有用的一节)
1.因为泛型不是协变的,所以如果方法参数是List<T> list,而你传入的list的类型参数不是T而是T的子类型的话就会报错.
解决办法是在方法的参数中的类型参数中使用通配符,比如ArrayList的addAll方法:
1 public boolean addAll(Collection<? extends E> c) { 2 .............. 3 }
这里使用Collection<? extends E>而不是Collection<E>就是这个原因,使用了这个通配符以后泛型就似乎有了像数组一样协变的特性.
同理? super XXX也是一样的道理,popAll如果允许传入一个Collection,并将pop掉的所有元素都放入那个Collection的话那参数就应该写成
1 public void popAll(Collection<? super E> c){....}
书上总结出的规律就是PECS(producer-extends consumer-super),刚才addAll是生产,popAll是消费
当方法的参数是带参数类型的时候都可以考虑使用通配符
2.有一个小技巧,通配符?的集合比如List<?>是不能插入null以外的元素的,但是可以将这个集合传入另外一个带参数List<T>的方法,用那个方法去add元素
书上的例子:
1 public static void swap(List<?> list, int i, int j){ 2 swapHelper(list, i, j); 3 } 4 5 public static <E> void swapHelper(List<E> list, int i, int j){ 6 list.set(i, list.set(j, list.get(i))); 7 }
3.当有限制的通配符遇上递归类型限制的话会比较麻烦,但是PECS还是有用的
1 package test; 2 3 import java.util.Arrays; 4 import java.util.List; 5 6 public class GenericTest2 { 7 public static <T extends Comparable<? super T>> T max(List<? extends T> list) { 8 return null;//具体的实现省略..... 9 } 10 11 public static void main(String[] args) { 12 max(Arrays.asList(new MyNumber()));//不使用?extends和super的最一般情况 13 max(Arrays.asList(new MyInteger()));//如果不是Comparable<? super T>而是Comparable<T>的话就会报错,因为MyInteger extends的是Comparable<MyNumber>而不是Comparable<MyInteger> 14 GenericTest2.<MyInteger> max(Arrays.asList(new MyPositiveInteger()));//如果不是List<? extends T>而是List<T>的话就会报错,因为T在这里是MyInteger 15 } 16 } 17 18 class MyNumber implements Comparable<MyNumber> { 19 @Override 20 public int compareTo(MyNumber o) { 21 return 0;//具体实现省略.... 22 } 23 24 } 25 26 class MyInteger extends MyNumber { 27 28 } 29 30 class MyPositiveInteger extends MyInteger { 31 32 }
具体解释请看代码注释
用enum代替int常量
1.枚举和int或者String常量相比最大的优势就是它是个类,可以有方法和自己的属性,可以提供一些行为.
2.枚举是不可变的,所以成员域应该为final
3.如果枚举有通用性,那就该成为顶层类(public),否则可以写在另外1个类中作为成员类
4.如果每个枚举对象都有自己独特的行为的话,就是说枚举类中的方法在每个枚举对象中都是不同实现的话可以考虑把这个方法写成abstract的(枚举类中可以定义abstract的方法),然后每种枚举对象都实现这个方法.
比如:
1 enum Operation { 2 PLUS { 3 @Override 4 double apply(double a, double b) { 5 return a + b; 6 } 7 }, 8 MINUS { 9 @Override 10 double apply(double a, double b) { 11 return a - b; 12 } 13 }, 14 TIMES{ 15 @Override 16 double apply(double a, double b) { 17 return a * b; 18 } 19 }; 20 21 abstract double apply(double a, double b); 22 }
5.如果多个枚举对象共享同一种行为,另外多个枚举对象共享另外一种行为,可以考虑使用枚举策略,在其内部再定义一个枚举类.意思就是说枚举类中的abstract相当于是接口方法,枚举对象覆盖这个方法相当于是接口的实现.
比如:
1 enum PayrollDay { 2 MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY( 3 PayType.WEEKDAY), SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); 4 5 private final PayType payType; 6 7 PayrollDay(PayType p) { 8 payType = p; 9 } 10 11 double pay(double hours, double payRate) { 12 return payType.pay(hours, payRate); 13 } 14 15 private enum PayType { 16 WEEKDAY { 17 @Override 18 double overtimePay(double hours, double payRate) { 19 return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2; 20 } 21 }, 22 WEEKEND { 23 @Override 24 double overtimePay(double hours, double payRate) { 25 return hours * payRate / 2; 26 } 27 }; 28 private static final int HOURS_PER_SHIFT = 8; 29 30 abstract double overtimePay(double hours, double payRate); 31 32 double pay(double hoursWorked, double payRate) { 33 double basePay = hoursWorked * payRate; 34 return basePay + overtimePay(hoursWorked, payRate); 35 } 36 } 37 }
PayType相当于就是一个策略接口,它的2个实现相当于是具体的策略.如果不用PayType这个内部枚举类的话可能就要写switch case了....
6.枚举的构造方法在static区块之前被调用,这个很神奇,但是也可以理解,因为枚举类的实例相当于是final且唯一的,会被优先创建....
1 enum A { 2 3 A1("a1"), A2("a2"); 4 static EnumTest2 test2; 5 String s; 6 public String toString() { 7 return s; 8 } 9 10 private A(String s){ 11 this.s = s; 12 System.out.println(s);//在static方法之前被调用 13 } 14 15 static{//枚举创建对象在static之前 16 System.out.println("static"); 17 } 18 }
先输出a1,a2再输出static
用实例域代替序数
1.获取枚举实例的定义顺序的时候可以使用ordinal方法,但是最好还是构造枚举对象的时候传一个数字作为参数更好,而不是依赖于实例定义的顺序.
用EnumSet代替位域
1.方法传递多个配置项的时候最好不用多个int的|操作,而是使用enumset传递多个enum作为参数.
int bold = 1 << 0;
int italic= 1 << 1;
传递 bold | italic 不如传递2个枚举
用EnumMap代替序数索引
1.如果有类型依赖于多个枚举常量,那它也可以写成新的枚举,实例构造方法传递含有之前的枚举常量作为成员域,而不是写个数组简单的包含多个之前的枚举(可以使用EnumMap,key强制为枚举类型).
因为数组的话就依赖于数组的下标,维护起来比较麻烦.
注解优于命名模式
1.方法名称这样的命名模式比如使用注解来的方便,而且注解可以添加额外信息.
比如junit testXX方法比如使用@Test注解.不会影响本来的方法命名.
用标记接口定义类型
1.接口在编译时就能被检查,而注解可以在运行时获取更多的信息.
2.接口定义好以后很难再去修改,而注解可以不断增加新的信息
3.接口和类只能被类扩展,而注解可以用于任何元素
检查参数的有效性
1.public方法可以使用@throws说明抛出的异常
2.非public方法使用assert检查参数,因为这些方法只能是你自己调用,外部使用者不可能会调用,我自己写的方法我能保证调用参数是正确的,所以可以使用assert尽快发现错误.
3.错误应该尽快发现,尽早检查.
必要时进行保护性拷贝
1.可以先copy对象再检查对象的有效性,因为你在检查对象的有效性时,别的线程可能会改变这个对象.
2.当对象进入你的数据结构以后可能发生变化的话,可能也需要进行拷贝,因为传给你的对象的调用者在之后可能会修改这个对象
慎用重载
1.方法调用的时候选择哪个重载方法是在编译期根据引用的类型来判断的,而不是引用指向的对象的类型来判断的.(在编译期就能确定调用哪个重载方法)
2.对于重载方法的选择是静态的在编译期根据引用类型来确定的,对于覆盖方法的选择是动态的在运行期根据对象类型来确定的.
3.为了避免混乱,尽量不要到处躲个具有相同参数数目的重载方法
慎用可变参数
1.当可变参数列表与基本类型数组结合起来的时候会有一些问题.
1 public class ParamterTest1 { 2 public static void main(String[] args) { 3 Arrays.asList(new int[] { 1, 2, 3, 4 }); 4 Arrays.asList(new Integer[] { 1, 2, 3, 4 }); 5 System.out.println(Arrays.asList(new int[] { 1, 2, 3, 4 })); 6 System.out.println(Arrays.asList(new Integer[] { 1, 2, 3, 4 })); 7 } 8 } 9 10 输出 11 [[I@6dc57a92] 12 [1, 2, 3, 4]
问题在于new Integer[]{....}传入方法的时候被当做一个Integer的数组,泛型T是Integer
而new int{}{...}传入方法的时候被当作一个只有 一个元素为int[] 的数组,泛型T是int[]
原因是因为泛型T只能是引用类型,不能是基本类型,所以T只能是int[]而不能是int.
返回零长度的数组或者集合而不是null
1.可以使用Collections下的类来帮助实现.
将局部变量的作用域最小化
1.局部变量应该在使用到它的地方声明并直接初始化,而不是全部在方法第一行直接全部定义好.
2.for比while好的一个地方在于while里用到的便利有时候需要定义在while外侧.导致作用域变大,而for循环里的变量的作用域只限于for,所以CVfor循环的代码一般不会出错,而while代码CV起来不注意的话可能会有问题.
for-each循环优先于传统的for循环
1.for-each可能效率会比一般的for高一点,主要还是简单,不容易出错,但是要在便利过程中修改集合的话,还是需要使用迭代器.
第48条:如果需要精确的答案,请避免使用float和double
1.精确计算小数可以使用bigdecimal..但是操作比较麻烦速度也比较慢..如果速度要求较高的话可以把小数扩大一定倍数以后用int和long来计算..但是也很麻烦.
第52条:通过接口引用对象
1.如果可以应该使用接口引用对象而不是类.因为这样可以使程序更加灵活. 如果接口引用周围的代码需要依赖具体的实现类的细节,那么在接口引用的地方用注释说明.因为这个时候代码其实与接口的具体实现是相关的.
2.当使用接口引用对象的时候更换具体的实现是非常简单的..更换具体的实现类的原因可能是因为新的实现可以提供更好的效率.
第57条:只针对异常的情况才使用异常
1.不应该用异常控制正常程序的流程.
原本的公司就有这样的问题,通过throw不同的异常来返回上层方法.然后后面需求改了,有些时候这些异常即使throw了也要继续下去.然后代码就控制不了了,因为发生异常以后就会返回上层方法调用,或者进入catch代码块跳过异常后续的代码.
2.如果要有状态相关的方法.可以才用状态测试方法和可识别的返回值两种方法代替异常
我一般在方法的参数里传入dto然后dto里有个success标注这次方法执行的状态.供外层方法检测.
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
1.如果异常是可以恢复的,那就是用受检异常,如果异常是不可以恢复的,那就是用runtimeexception和它的子类.
2.写类继承throwable是没必要的,可以直接继承exception
第60条:优先使用标准的异常
常用异常:
IllegalArgumentsException 非NULL参数值不正确
IllegalStateException 对于方法调用,对象状态不适合
NullPointerException 在禁止传入null的情况下传入了null值
IndexOutOfBoundsException 下表参数值越界
ConcurrentModificationException 在禁止并发修改的情况下检测到对象被修改
UnSupportOperationException 对象不支持用户请求的方法
第61条:抛出与抽象相对应的异常
1.高层的方法在必要的时候可以try catch底层方法抛出的异常并且重新throw一个高层的异常,因为底层的异常太底层了有时候很难明白到底是啥错误,为啥会有这个错误
2.如果底层的异常是有帮助的,那可以利用异常链,throw一个高层异常,同时cause里传入底层的异常.
第64条:努力使失败保持原子性
1.因为抛出异常的时候需要输出出错的信息,所以就要尽量保持对象的状态是原始的状态而不是修改后的状态.
保持对象状态不变有一些方法:
不可变的对象
在操作之前检查有效性
调整操作顺序让可能会抛出异常的操作先于改变对象的操作
编写一段恢复对象的代码
操作对象之前备份
2.不是所有的时候都一定要这么做,因为有时候代价会很大,而且有时候对象就是会被修改.
第74条:谨慎地实现Serializable接口
1.实验得出:当子类实现Serializable父类没实现,而父类有没有无参构造方法的时候readObject方法就会报错
2.1的同时如果父类有无参构造方法,那子类可以反序列化,但是父类的属性都没有值,都自己去写readObject(ObjectInputStream in)和writeObject(ObjectOutputStream out)方法
3.当序列化反序列化出现错误的时候(不知道啥错误)不能正确初始化成员域的时候会使用readObjectNoData()代替readObject(ObjectInputStream in)方法(虽然我觉得1万年都用不到)
4.writeReplace方法先于writeObject方法调用,readResolve后于readObject方法调用.所以可以分别替换写入和读取的对象可能会用于单例.
第75条:考虑使用自定义的序列化形式
1.很多情况需要自己写readObject和writeObject去覆盖默认的序列化实现,因为对象中还有其他对象的引用,会遍历到其他对象,直到遍历所有对象为止.可能会消耗过多资源..如果对象是个值类.那序列化可能没啥问题,不然可能就需要自己去覆盖默认的序列化行为.
2.即使是private的属性,因为序列化的原因以后也会像public的api一样,不可以随意修改.
3.实验得出:反序列化的时候并没有调用构造方法.但是会调用父类的构造方法.
第76条:保护性地编写readObject方法
1.如果序列化的类的属性里有private的其他类,那反序列化的时候需要对这个类进行保护性拷贝(代价就是这个属性类不可能被声明为final了,因为要拷贝,二次赋值),以防止被修改(可以修改字节流,额外增加引用指向之前private的类,然后用这个引用修改private类达到目的).另外在拷贝完成之后进行字段的校验.
第77条:对于实例控制,枚举类型优先于readResolve
1.如果单例类的属性是非transient的那么修改字节流可以让它被反序列化的时候被替换,原理就是编写一个其他类,readResolve返回一个新的值,然后修改字节流,属性引用指向这个新写的类.当单例类被反序列化的时候会先反序列化它的属性类,它的属性类指向新写的修改类,修改类的readResolve就会被调用.
2.解决这个问题可以将属性加上修饰词transient或者将类设计成枚举类(但是绝大部分情况下这个类的成员域可能在编译的时候是不知道的.这样就不能设计成枚举类).
第78条:考虑用序列化代理代替序列化实例
1.反序列化是用到了语言之外的特性的.我们可以使用java语言自带的功能来完成反序列化.原理就是写一个private的内部类.外部类序列化的时候writeObject其实是写入了内部类的对象.然后内部类的readResolve方法可以返回外部类的实例.这样相当于是已知一个内部类对象,copy一个外部类对象.
即使伪造字节流也可以免受影响,因为内部类对象到外部类对象的copy是我们自己手动用代码完成的.