Effective Java 笔记

我的感悟

       使用正确的态度学习这本书,有的原则会让java程序过于复杂

创建和销毁对象

1. 使用静态工厂代替构造函数

原因   

1.      静态工厂具有名字

                 i.          对于两个构造函数,如果参数类型和个数相同,则只能使用不同的顺序进行区分,而使用工厂函数可以为这两个构造函数指明不同的名称

                ii.          如果有多个构造函数,可考虑用静态工厂方法替换

2.      不要求创建新对象

                 i.          当系统需要该类的N或1个实例,如果使用构造函数,则每次调用都会创建一个实例,可通过单例或多例模式重用已有实例

3.      返回原类型的子类型对象

                 i.          对于一个继承体系,在基类中声明静态工厂方法,根据type返回不同的子类实例,且该子类为package 或 private,可以向客户端隐藏子类。

命名规范    

       getInstance     

        valueOf : 返回的实例与原有实例具有相同的值,如 Float 和float  , XXXConfig 和 XXX

2. 使用私有构造函数强化不可实例化能力

    私有构造函数使该类无法实例化,无法被继承,用于Utils 或者单例类

    如果该类支持serializable,参考最后一部分

3. 避免创建重复对象

    如果对象不变,或创建后不可变,可以对该对象进行共享,推荐使用 Flyweight 模式

4. 消除过期的对象引用  

     尽量减少变量的作用范围

    如果变量属于Stack,如数组中的元素不可用时,应该置为 null

5. 避免使用终结函数

    finalize 方法回收内存,但调用时间不确定,不建议使用

    使用场景: 调用native method

对于所有对象通用的方法

1.    改写equals

通用约定

    对称性:对于x和y ,如果x.equalsTo(y),则 y.equalsTo(x) , 如果x 和y属于不同的类,就要注意了

    传递性:x y  z , 使用委托代替继承

    非空性:对于任意非空 x.equals(null) 返回false,不抛出异常。 函数中,不需要对null做特殊的检查,null instanceof MyType  返回false

    当你编写完equals 要考虑 对称性、传递性

 其他告诫:

        1. 不要让equals 过于智能

        2. 不要让equals 依赖不可靠资源

        3. 不要让equals 参数为非Object

7. 改写equals 要改写hashcode

   Object中规范要求,如果equals 返回true,则 两个对象的hashCode 产生相同的整数值,如果equals使用逻辑判断,而hashCode未改写(使用引用地址),该对象作为HashMap或HashSet 的key,equals相同的两个对象作为不同的key

编写方法:

        1.  对每一个关键域(equals用到,而且非冗余)计算散列值 c

            boolean :   f?0:1;   

            byte  char  short  int  :  (int ) f

            long :(int) (f ^ (f >>>32))

            float :  Float.floatToIntBits(f)

            double: Double.doubleToLongBits(f).floatToIntBits

            对象引用,调用该对象的hashcode

        2.  公式  17*37+c

        3. 判断 hashCode 和equals 方法是否一致

8. 总是改写toString     

    输出有用信息,方便调试

9. 谨慎改写clone

建议: 

使用静态工厂替代  Interface newInstance(class a);    好处如下:

 1. 不与final属性发生冲突: final属性只能在构造函数中初始化,调用clone会有问题

2. 不要求捕获异常: 未实现Clonable 接口时

注意:

        1. 为继承而设计的类不要实现clonable接口,clone中不要调用非final的方法,这种方法可能被子类重载,从而在未初始化前使用某些数据;

        2. 其他接口不要扩展clonable接口

clone 实现:

       公有的clone方法,该方法先调用 super.clone() ,然后对引用进行修正,引用类型,如Date,或者Object的数组,则需要进行深拷贝,否则clone后的对象和源对象持有同一份引用。

10. 考虑实现comparable

    强烈建议满足 (x.compareTo(y)==0)==(x.equals(y)),否则如TreeSet中 equals不同而compareTo相同的对象只有一个

实现

 直接进行类型转换,当类型不一致时抛出ClassCastException,为null时抛出 NullPointerException 正好符合规定

类和接口

11. 使类和成员的可访问性最小

    顶层类只有 default和public,尽量为default

    成员(函数,属性,内嵌类)可以有 private default  protected public四种,先都声明为private,在需要的时候修改

    非零长度的数组总是可变的,所以具有公有的静态final 数组域总是错误的,应该替换为一个方法,返回一个拷贝

12. 支持不可变性

    1.  不提供修改方法

    2.  该类不可继承 final

    3.  所有域都是 private final

    4.  如果类指向可变对象的引用,保证客户无法获得该引用

    5.  如果类不能做成不变的,尽量限制其可变性,比如构造函数完成可变性创建

    好处是:不要求线程同步

13. 专门为继承设计的类给出文档说明

    1.  protected方法 描述改写带来的影响

    2.  public 调用那些可改写的函数    

    3. 构造函数、clone searilizable不能调用可改写的方法

16.  接口优于抽象类

特点

    抽象类包含实现,接口允许多继承

好处:

        1. 防止构造层次结构的类型框架。 对于一个类体系,有基本的功能和扩展,对于这些基本功能应该通过继承实现,而可选的功能应该通过接口和实现类来引入

17. 接口只用于定义类型

    常量不应该使用接口实现,如果该常量类枚举,应该使用 【类型安全枚举类】 来实现,否则使用不可被实例化的工具类 来定义这些常量

    策略模式可以和简单工厂模式相结合,将具体的策略类实现为工厂的private static member class,对外部隐藏

18. 优先考虑静态成员类

    嵌套类只定义在类内部,主要为父类服务,分为 static member class, nonstatic memeber class, annonymous class, local class.

    成员类类似于普通的成员,可以访问父类的成员,包括声明为private

    选择:如果成员类的每个函数不需要访问外部类的非静态信息,则使用 static member class, 否则为 nonstatic member class,如果该类只使用一次,且定义了接口或基类使用 annonymous class,local class只在方法内部定义的类,很少使用

    static member class 为public,通常作为父类的一个子部分,如常量信息的存放;为private 通常作为父类的一个组件,比如Map和Map.Entity,而且该类不允许被外部访问,该类不能访问父类的非静态成员。

    non static member class 多作为适配器,可以访问父类的非静态成员。

    当选择时,考虑是否需要访问父类的非静态成员,不需要就是用static member class

C语言结构的替代

19. 用类代替struct

20. 用类层次代替联合

21. 用类来代替enum

定义一个类来代替枚举类型,该类私有构造函数,

为每一个枚举常量定义 final static的对象,如public static final Suit CLUBS=new Suit("club");

调用时 Suit.CLUBS

好处:

       1. 常量名在类下不会冲突;

       2. 不会引入不正确的常量;

       3 .可以包括枚举相关的方法,如toString等;

       4. 类可以实现接口,如Comparable;

       5. 常量是通过构造函数定义,可以是计算后的结果。

22. 用类和接口来代替函数指针

       C中的函数指针通常是实现策略模式,在java中,接口定义通用策略,具体类实现具体的策略。

       如果该策略只适用一次,可使用匿名类实现。如果该策略有多个实现,可以将策略和简单工厂模式相结合,实现为工厂的private static member class,对外部隐藏

方法

23. 检查参数的有效性

    被外部调用的方法,使用异常标示参数非法,如IllegalArgumentException  NullPointerException  IndexOutOfBoundsException.

    非公有的 使用assert 处理

    有些参数,方法本身不使用,存储起来以后使用,检测尤为重要,如构造函数

    当参数检查非常昂贵,或者不切实际时,就不要参数检查了

24. 需要时使用保护性拷贝

    担心类被破坏时,需要考虑这种方法,一般是给别人使用的组件库会用到,如 Date getDate()  { return date; }  则使用者可以 对getDate() 进行修改,而这些修改可能是 getDate() 函数定义者不希望的,可以使用保护性拷贝,如  return new Date(date.getTime()) 返回一个新对象。

有三个地方:

1.     构造函数,在参数检查前进行

2.      2.  get方法  

3.     3. set方法

25. 谨慎设计方法原型

    选择好名字

    不要追求过于方便的方法:只有当一个操作过于频繁时,才提供快捷方法

    避免过长的函数列表

    参数类型优先选择接口

26. 谨慎地使用重载

    重载方法的调用是在编译期决定的,如 fun(Set s)  , fun(Collection c) 。 实参为 Collection[] tests=new Collection[] {new HashSet() };  如果你以为会调用fun(Set s) 那就错了,因为是在编译期决定,而参数类型是Collection,所以会调用 fun(Collection c).

    这种使用方式容易混淆,所以应该避免参数为同一体系的重载用法,或者更严格点,避免两个相同参数数目的重载方法

27. 返回零长度的数组而不是null

函数 Cheese[] getCheeses() 返回 new Cheese[0] 而不是null,好处有二,1. 符合逻辑需要 ; 2.  不需要对null额外处理

如果函数中用到多次 new Cheese[0],可以将其声明为常量  final static Cheese[] NULL_CHEESE_ARRAY=new Cheese[0] 然后在函数中使用。

28. 为公有api编写注释

    描述与client的约定:  

       @params  @return :名词短语

        @throws   异常名称 if 名词短语

       前提条件和后置条件  副作用      

       还可以使用html标签,支持任意的,常用标签: <p>  <code>(将代码分段)    

    降低文档出错的可能性,使用html有效性检查器,如weblint 

通用程序设计    

29. 将局部变量的作用域最小化

    局部变量在第一次使用时声明

    循环终止后循环变量的内容不再被需要的时候,则for循环优于while循环

    当循环判断中涉及到一个方法调用,且可以保证每次迭代中这个方法调用返回相同的结果,则使用  for(int i=0, n=list.size; i<n;i++) 将调用放在第一个;中

30.了解和使用库

    正常来说,被多人使用的库比自己编写的要好

每一个程序员应该熟悉 java.lang   java.util  java.io的内容,如 util.concurrent 并发工具,简化多线程; java.util.regex;  java.util.prefs 用于永久存储“环境变量‘的工具, java.nio 高性能的I/O工具

实例:

            产生 【0,n】的随机整数 java.util.Random.nextInt(int)

            忽略字符大小写的排序: Collection.sort(v, String.CASE_INSENSITIVE_ORDER);

            打印数组中元素  System.out.println(Arrays.asList(a ));

            获取两个hashTable 包括同样entry的所有key: Map tmp=new HashMap(h1);  tmp.entrySet().retainAll(h2.entrySet());  Set keys=tmp.keySet();

31. 如果要求精确,避免使用doublefloat

       使用 BigDecimal   int 或long 替代,

       如果要处理十进制小数点,使用BigDecimal,还可以完全控制四舍五入;

       如果性能非常关键,且不介意自己处理十进制小数点,而且涉及到的数值不太大,使用int或long;       如果数值没有超过9位十进制数,则使用int,如果不超过18位使用long,

       超过18位使用BigDecimal

32. 如果其他类型更合适,尽量避免使用字符串

    如果存在一恰当的类型请使用该类型,否则请编写一个适合的类型

33. 了解字符串的链接性能

    + 要求 平方级的时间; StringBuffer.append 线性增加

34.  通过接口引用对象

35. interface 优于 reflection 

    reflection 用于: 用到的类在编译时刻不能用

36. 谨慎地使用 native method

37. 谨慎进行性能优化

编写好的程序而不是快的程序。

在设计系统时,特别是在设计API、线路层协议和永久数据格式的时候,请考虑性能。

    使用测试工具发现性能问题后,检查 选择的算法

38. 遵守命名规范

    常用的已经在 命名规范 中描述,下面说些特殊的

    1. 转换对象类型的方法  toType  ,如 toArray

    2. 返回对象的一种表现形式  asType ,如 asXML

    3. 返回对象同值的primitive 类型  typeValue  如 intValue

    4. 静态工厂: valueOf (返回的实例与原有实例具有相同的值) 和 getInstance()

    5. boolean 类型的变量和函数很类似,不过是省略了is,如 initialized和 isInitialized

异常

39. 只针对异常情况使用异常

    正常的控制流不应该使用异常,如 循环的结束。

    对于状态相关的类,即某个方法的运行依赖于不可预知的条件,如数据库连接成功,才能进行查询操作,有两种方法:

        1. 增加一状态判断方法,如  判断数据库是否连接的 isConnected() 方法

        2. 返回特定值,如数据库未连接,方法返回null

选择方法:

       如果类可能被并发访问或者可被外界改变状态, 则使用 可识别的返回值 方法;

       如果状态判断和运行方法重复,则从性能角度使用 可识别的返回值 ;

       否则使用 增加状态判断 方法,因为有更好的可读性。

40. checked ExceptionUnchecked Exception

     CE:必须被捕获的异常,用于  如果正确使用API并不能阻止异常的发生,并且client需要对异常进行处理(恢复或转义抛出)

     UCE: 运行时异常,代表程序错误,不满足CE条件就使用UCE

    Error:被JVM保留,不要实现其子类

    异常也是对象,可以定义方法提供额外信息,特别是对于CE,可以存放异常原因。

    CE和UCE也不是界限分明的

41. 避免不必要的 CE 

42. 尽量使用标准异常

    如果标准异常能满足你的需要,请使用,以下是用的最频繁的

1.     IllegalArgumentException :用于参数不合法时,可以更具体,比如参数为null,NullPointerException;参数表示数组下标时,IndexOutOfBoundsException。

2.     IllegalStateException: 方法使用不按套路出牌,如未正确初始化前,企图使用这个对象。

3.     ConcurrentModificationException: 同步对象被并发修改

43. 抛出的异常要适应相应的层

    异常转义:高层捕获底层异常,并向上抛出一个更上层能看懂的异常,这种方法不应该被乱用

 处理底层异常的方法是 

        对参数进行显示地检查,尽量避免异常的抛出;

        如果不能避免异常抛出,则在本层处理异常,并logger 该异常

        如果上层需要该异常信息,通过转义向上层抛出

44. 每个异常都要有文档

    @throws   异常 if 名词短语 | 条件

    对于CE和UCE都要有文档

    如果该类多个方法抛出同一异常,可以在类上说明

45. 在细节消息中包含失败信息

    通常在 Exception的构造函数中包含,如果有额外信息,可以扩展该异常,加入函数

46. 努力使失败保持原子性

    原子性:方法调用抛出异常后,恢复成未调用前状态,有几种途径

        1. 该对象是 不变的

        2. 可变对象检查参数的有效性

        3. 编写失败的恢复代码,不常用

        4. 在对象的临时拷贝上执行操作,不常用

47. 不要忽略异常

    如果忽略了异常,至少要加上一条注释:解释为什么忽略

线程      

    这部分用的不太多,就先列出标题,以后用到再扩展

48. 对共享可变数据的访问

49. 避免过多的同步

50. 永远不要在循环的外面调用wait

51. 不要依赖于线程调度器

52. 线程安全性的文档化

53. 避免使用县城组

序列化

    如果类只是自己用,则使用默认的Serializable或者自定义的,是可以接受的,如果是开发组件库,那可能就要好好掂量掂量了。

    这部分因为用的少,理解的不够深刻,写的也比较乱

54. 谨慎使用Serializable

    一旦类发布,则改变这个类的实现 的灵活性将大大降低,该类的private 和protected 的实例变成了导出API的一部分; 

    如果修改了Serializable,低版本的客户端可能不兼容; 

    增加了bug的可能性,因为deserialization是隐藏的构造函数,如果不做处理,可能违反真正构造函数建立的约束关系; 

    类新版本的发行,测试负担增加

    为了继承而设计的类很少实现Serializable,但可以提供无参的构造函数,方便子类实现Serializable    

    内部类很少实现Serializable

    简而言之,不要相信Serializable很容易

55. 考虑自定义的序列化形式

    当设计一个类时,最重要是设计API

    如果没有认真考虑默认序列化的形式是否合适,则不要使用:灵活性、性能和正确性

    理想的序列化形式应该只包含逻辑数据

    如果一个对象的物理表示等同于它的逻辑内容,则默认的序列化形式可能是合适的,即使默认序列化合适,仍然需要提供一个readObject方法以保证约束关系。

    如果一个属性不是transient,一定确保其实对象逻辑状态的一部分,这些属性序列化时会初始化为默认值

    使用@serial 表明序列化的内容, transient表示不序列化

    显示申明 UID

    当物理和逻辑不一致时,默认序列化不合适的四点:

        1. 公开的API 被内部表示约束

        2. 消耗过多的空间和时间

        3. 引起栈溢出:如果对象包括太多子对象

   

56. 保护性编写 readObject方法

    readObject 类似于构造函数,需要检查实参的有效性,也需要进行保护性拷贝,指导原则如下:

        1.  private 对象 属性,如 非可变类的可变属性

        2. 有约束条件的类,如 Period中的 start 和end

        3. 无论是直接还是间接,不要调用可被改写的方法 

57. 必要时提供一个readResolve方法

    在类A中定义readResolve,正如该名字所暗示的,反序列化时对要生成的对象进行拦截,其return可以替代该对象,用在:

        1. 实例受控的类,如单例模式: 在readResolve中丢弃掉得到的对象,调用构造函数获得

        2. 代替 保护性的readObject:如果在构造函数中实现了保护性,在readSolve中根据得到对象的属性构造新的对象,从而实现保护性 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值