Effictive Java 不完全学习笔记

Effictive Java 不完全学习笔记

这里只是基于自己的理解做的一些记录, 一些对自己来说已经是一种常识, 将不再列出. 

由于作者是sun jdk collection framework的主要贡献者, 因此书中很多的effective rule, 大多数是对collection framework实现的一个总结(很多例子都是拿Collection中的东东在说事儿), 站在JDK API的高度来思考的, 这些考虑是非常严谨的, 而我们平时的开发可能不会注意到这些细节, 或者可能没有作者所说的场景, 因此可以根据需要选择性的理解. 

第5条 消除过期的对象引用 
这里讲到了一个因为过期引用导致的内存泄漏的问题, 而平时我们也很难注意到这个问题,  这个例子是一个Stack的实现, 在Stack内部用一个数组来缓存堆栈中的所有元素, 在其pop方法里面这样写: 
Java代码   收藏代码
  1. public Object pop(){  
  2.     if (size == 0){  
  3.         throw new EmptyStackException();  
  4.     }  
  5.     return elements[--size];  
  6. }  

当弹出一个元素的时候, elements其实还存在对这个元素的引用. 正确的做法应该是 
Java代码   收藏代码
  1. Object result = elements[--size];  
  2. element[size]=null;  
  3. return result;  

但是对于java来说, 每次对不再使用的对象手工强制设置为null这不是一种好的处理方式, 之所以要这么做的前提是:需要对象自己来管理内存, 即对于不再使用的对象, 需要明确的告诉垃圾收集器不再使用, 否则就会造成内存泄漏. 

第8条 改写equals时总是要改写hashCode 
hashCode对HashMap这种需要用到HashCode的容器具有非常重要的意义. 
这里作者举了个电话号码的例子, 如果一个电话号码包括区号, 号码和分机号, 对象为PhoneNumer("0571", "88155188", "1234"), 但是未实现hashCode()方法;在hashMap中用PhoneNumber作为key, 用户名作为value这样保存, hashMap.put(new PhoneNumer("0571", "88155188", "1234"),"jenny"), 如果用PhoneNumer("0571", "88155188", "1234")到hashMap中取得的却是null 

计算hashCode, 17是一个不错的初始值, 37是一个不错的乘数因子, 计算hashcode, 一般采用这种写法: 
Java代码   收藏代码
  1. result = 17;  
  2. result = result * 37 + property1 hashcode;  
  3. result = result * 37 + property2 hashcode;  
  4. result = result * 37 + property3 hashcode;  


第12条 使类和成员的可访问能力最小化 
如果需要声明一个共有的静态final数组时, 应该将其声明为私有,  然后提供一个非可变的 final List, 比如下面的做法: 
Java代码   收藏代码
  1. private static final type[] PRIVATE_VALUES = {...};  
  2. public static final List VALUES = Collections.unmodiableList(Arrays.asList(PRIVATE_VALUES));  

还有一个做法是通过一个公共的方法, 然后返回数组的clone, 即保护性拷贝 

第14条 复合优于继承 
这是一个老生常谈的话题, 继承的优点是有利于重用, 但是却带来安全的问题. 
在一个包内的继承是可以接受的, 因为包内的继承几乎可以被认为是在同一个程序员的控制范围之内, 还有只有那些专门为继承而设计的基类, 这样的继承基本上也是安全的, 而对于普通的具体类使用继承的一个问题就是破坏了原有类的封装性, 从而给程序埋下了bug的种子. 
这里作者举了继承HashSet实现计数功能的例子, 计数针对add和addAll方法来进行, 但是addAll内部是调用add方法来添加元素的, 而子类的计数功能必须依赖父类这个并没有承诺的实现细节,  如果父类addAll方法的实现细节发生改变, 将导致子类的计数功能失败. 为了保证子类的计数功能不依赖父类的实现, 通常不得已的做法是重写父类的相关代码. 
盲目的继承会存在各种问题, 而优于继承的复合却能克服这些问题, 作者针对上面的问题用复合来举例子, 让用于计数的HashSet实现Set接口, 内部持有一个真正的HashSet实例, 然后将所有的方法转发到HashSet实例上, 这样整个计数Set完全不用依赖HashSet内部的实现细节, 除了带来健壮性外, 还带来了灵活性 
只有两个类A和B之间存在is-a关系的时候(B确实是一个A), 才应该采用继承. 

第15条 
好的API文档应该描述一个方法做了什么工作, 而并非描述它是如何做到的. 
构造函数最好不要调用(或依赖)可被改写的方法, 这个主要是处于初始化的考虑, 因为父类的构造函数会在子类构造函数初始化之前被调用, 而被改写的方法又依赖于子类是否初始化, 这样导致初始化失败. 

第16条 接口优于抽象类 
抽象类主要处理具有层次关系的架构(在这种场景下, 抽象类可能优于接口了), 而接口则可以不受这个应用场景的限制, 他可以为类mixin新的功能. 

第17条 接口只能用于定义类型 
这个主要是作者针对在接口中定义常量的做法的控诉, 我觉得没那么严重, 只要没有让需要使用常量的类继承常量接口即可, 这样常量接口跟在类中定义常量没有区别. 

第18条 优先考虑静态成员类 
如果你声明的成员类不要求访问外围实例, 那么请记住把static修饰符放到成员类的声明中. 
非静态成员类的每一个实例都隐含的与外围类的一个实例紧密的关联在一起, 这样非静态成员类的实例才能调用外围类实例的方法. 
作者举了几个Collection中的例子.一个是List中的Iterator, Map中的keySet, valueSet, 因为他们自己都不缓存数据, 需要访问外部类实例中数据, 因此必须是非静态的内部类.而对于HashMap中的每一对键值(key-value)的包装类Entry来说, 虽然一个Entry需要与一个具体的Map关联, 但是它的getValue, setValue等方法并不需要访问外部类HashMap, 而是在创建每一个Entry的时候就已经从Map中拿到key, value并塞到Entry中去了. 
对于匿名类, 主要用来封装那些非常简短的逻辑, 如果代码超过20行, 则采用匿名类就不是很理想了. 

第28条 为导出的API编写文档 
为API编写的JavaDoc文档应该简洁的描述它与客户之间的约定, 应该说明它做了什么, 而不是描述它如何做的, 如果是方法的话, 最好描述它的前置条件(调用这个方法必须满足的条件)和后置条件(成功调用之后, 哪些条件必须满足), 一般情况下, @throws中隐含的非检查异常就是前置条件 ,因为每一个非检查异常就意味着一个不满足的条件, 有时候也在@param中来说明前置条件. 
此外还应该描述方法的副作用, 也就是对其他相关内容造成的影响. 
在写方法的JavaDoc的时候, 一般@param和@return后面应该接一个名词, 而@throws 应该以"如果"开头来表明异常将在什么情况下抛出来. 
对于方法, 类, 属性的描述有一些不成为的规律: 
对于方法和构造函数的概要描述, 应该是一个动词短语, 比如 
ArrayList(int)描述为:用指定的初始容量构造一个空的列表 
Collection.size() 返回集合中元素的数目 
对于类, 接口和属性, 概要描述应该是一个名词短语,比如 
TimerTask:可以被一次调度的一项任务. 

第29条 将局部变量的作用域最小化 
这里说明了for循环比while要好的例子. 
因为while需要在循环之外定义变量, 而for循环则被包含在了循环内部, 这样就可以避免那些偷懒拷贝代码带来隐患 

第30条 了解和使用库 
主要是为了说明避免重复发明轮子. 
每个程序员应该熟悉java.lang, java.util甚至java.io类库 
另外apache的一些库也是我们每个人应该熟悉的:) 

第31条 如果要求精确计算, 请避免使用float和double 
就是说用float, double来进行涉及到小数的计算是不靠谱的, 应该转换成int和long或者使用BigDecimal. 

第39条 只针对不正常的条件才使用异常 
异常永远不能用于正常的控制流 

第40条 对于可恢复的条件使用被检查的异常, 对于程序错误使用运行时异常 
其实我们的程序中, 抛出的异常绝大部分是不可恢复的.因此应该避免使用被检查的异常 

第42条 尽量使用标准的异常 
IllegalStateException, 这个异常应该属于常用的异常, 如果在一个对象被正确初始化之前被调用, 那么应该抛出这个异常. 
一般来说, 所有错误方法的调用可以归结为非法的参数和非法的状态, 但是我们一般不能笼统的抛出IllegalArgumentException, 而应该使用更明确说明异常原因的异常, 比如NullPointerException, IndexOutOfBoundsException等. 

第48条 对共享可变数据的同步访问 
这里举了个很好的使用同步的例子:使用双检查, 将同步缩小在最小范围 
延迟初始化, 一般会这样写: 
Java代码   收藏代码
  1. private static Foo foo = null;  
  2. public synchronized static Foo getFoo(){  
  3.     if (foo == null){  
  4.         foo = new Foo();  
  5.     }  
  6.     return foo;  
  7. }  

但是可以将同步进一步缩小, 这里采用双检查模式, 在foo初始化之后, 不用再被同步(但是可能存在部分初始化的问题): 
Java代码   收藏代码
  1. public static Foo getFoo(){  
  2.     if (foo == null){  
  3.         synchronized(Foo.class){  
  4.             if (foo == null){  
  5.                 foo = new Foo();  
  6.             }  
  7.         }  
  8.     }  
  9.     return foo;  
  10. }  

但是更精妙的做法是完全不用同步: 
Java代码   收藏代码
  1. private static class FooHolder{ static final Foo foo = new Foo();}  
  2. public static Foo getFoo(){return FooHolder.foo;}  

他主要利用了java语言中的"只有当一个类被用到的时候才被初始化" 

你可能感兴趣的:(Effictive Java 不完全学习笔记)