Effective Java笔记

  这几天把Joshua Bloch的effective Java扫了一遍,记得前几年也曾想看过此书,不过看了几页就发现迫于自己的java基础和英语基础双双不过关,只能放弃,在经过了几年的修炼之后,在英文字典的帮助下,勉强可以理解一些内容了。既然看了就要留下点脚印,因此我把我觉得应该留点深刻印象的部分记录了下来,这其中也包含了我个人的一些理解。

1.       使用静态工厂方法替代构造方法。好处有两点:第一,静态工厂方法是有名字的;第二,静态工厂方法可以不必要在每次调用时创建一个新的对象(对象池)。

2.       迫使单例类使用私有构造函数。单例类的两种方式,一种使用私有静态实例延迟加载,另一种是用公有静态最终实例。

3.       迫使不可实例化的类使用私有构造函数(比如那些仅仅包含静态功能函数的类)。

4.       避免使用析构函数,析构函数不应该赋予它功能性的用途,它的用途可以是作为回收资源的最后一层防护网。

5.       关于hashcode,两个equal的对象必须拥有相同的hashcode,两个unequal的对象不一定要有不同的hashcode,但是这会降低哈希表的效率。

在第一点2equal对象如果有不同的hashcode,哈希表会根据hashcode把对象放入不同的bucket中,如果第一次放入了一个对象作为key,但是使用equalhashcode不同的对象去查那个key,会导致哈希表到不同的bucket去寻找最终找不到。

6.       最小化类和成员的可见度。尽量隐藏内部数据。例如使用静态最终数组是不正确的,因为其内容总是可以改变。

7.       尽量使用不可变的对象,JAVA中的BIGDECIMALBIGINTEGER之类的其实都是imuttable对象。他们简单,且线程安全,不过要对每一个值实例一个对象造成空间浪费。因此如果要有多步的频繁修改,而最终只取最后一步值的情况,可以考虑使用这些类的可变伴随类,例如STRINGSTRINGBUFFERBIGINTEGERBITSET。构造函数最好完全执行初始化任务而不是将部分放入其他方法中去(降低多个状态导致的出错危险性)。

8.       尽量使用聚合代替继承,继承会破坏封装,父类中内容变化导致不可预测的危险(假如不是出自同一个程序员且没有OK的文档)。继承需要有良好的设计和文档,不然就必须禁止。在子类中自己增加的方法,如果后期版本的父类出现了同名不同返回类型的函数,会导致子类无法编译。

9.       尽量使用接口而不是抽象类。JAVA只允许单继承,但能继承多个接口。接口可以构造非继承性的关系,比如一个人可以同时是歌手和作曲人。考虑使用骨架抽象类(实现了接口中某些原生部分留了一些继承类应实现的部分)。在接口中新增方法会导致原来的子类无法编译。接口和抽象类是灵活性和进化特性的一种平衡。

10.   接口应该被用来定义类型,而不是输出常量。如果一个类继承了输出常量的接口,会造成API的一种泄漏(常量是一种实现的细节而不是抽象)。

11.   内部类有静态,非静态,匿名,本地四种。(1)静态内部类和静态变量一样,静态内部类一般作为辅助用途,它不依赖外部类的实例。(2非静态内部类一般被用来作为适配器使用,例如继承MAP接口的子类有私有非静态内部类继承了Collection接口来表现令外一个特性(通过一个函数导出这个Collection)。如果不需要访问外部实例,建议使用静态内部类。如果不需要访问外部实例且只为外部类服务,应使用私有静态内部类。因为它依赖于实例,所以不能有静态属性。(3)匿名内部类是即时定义和实例化的,他们只能复写接口或父类中方法而不能拥有新方法。匿名内部类一般用来实现功能性对象,例如实现Comparator接口;第二一般可以用来实现进程对象,比如threadRunableTimetask;第三可以用于静态工厂方法;第四可以用来分隔拥有不同操作的几个相同类型的属性。(4)本地内部类和其他三种内部类差不多,它是定义在代码块里面的内部类,它不能是静态的,也不能有静态成员。

12.   类型安全的枚举模式,在1.5中可以使用新加入到enum类型,它可以加入静态或非静态的属性和方法,可以加入构造函数(只能是私有,因为枚举中的每个值只能在内部调用构造函数),可以继承接口。enum和类差不多,而且使用类可以完全模拟enum的实现,只要使用私有构造方法,且在类中定义几个静态最终自己类型的常量来代替枚举类型中的变量。如果仅仅使用接口中定义的静态常量来代替,则会导致非类型安全的枚举,例如整数1可能代表春天,或者苹果,但是如果定义了类型安全的枚举,那么简单的传入1是无效的,必须先定义它的类型是季节还是水果,并且赋值为1。通过把构造函数变为protected可以加入可扩展特性。

13.   抽象类不能直接被实例化,但是可以通过匿名内部类的方式变相实例化。

14.   对于不可变对象中的复杂属性,使用防御性拷贝技术,defensive copy

15.   访问者模式是将算法和具体对象结构所分离的一种方式,在具体对象中可以加入一个accept方法来接受一个访问者接口对象,然后通过回调来执行访问模块。在Visitor的具体实现中封装了访问的回调函数,来进行具体的访问操作。(accept/visit

16.   重载是在编译是决定的,不能通过重载来识别运行时动态识别的多态信息。重载是静态识别的,而重写是动态识别的。一个安全的措施是,避免使用相同数目参数的重载。如果要用相同个数的参数,考虑使用不同的命名的函数取代重载。如果一定要使用相同个数参数重载,则参数不应该能够相互转化成另一个(即一个非另一个的父类)。如果一定一定要破坏上述规则,那么你重载的函数的功能应该是一样的。

17.   在需要返回数组的场合,如果是空数组,不要简单返回一个null,可以考虑返回一个长度为0的相关类型的常量数组,以保持类型的一致。

18.   最小化局部变量的作用范围。C语言需要你在开头声明所有变量,但JAVA不必要,且有必要在第一次使用变量的时候声明他,以最大程度减少错误的可能性。另外,对for的使用应比while频繁,因为for里面可以包含自定义的临时变量,而while会暴露这样的变量。

19.   了解并使用库函数。MATH.ABS(INTEGER.MIN_VALUE)会返回一个复数,最小值-2147483648的绝对值2147483648是超出整型的范围的,因此他的绝对值还是复数。如果在一个产生随机值的程序里面出现了这个情况,可能导致无法再现的程序崩溃。因此使用库函数Random.nextInt(int)而不是自己写一个Math.abs(new Random().nextInt)%n。使用库函数会使程序更快,更可靠,更可读。JAVA提供很多库函数,比如集合接口下的一些功能类(Collection, Set, Map, List, SortedList, SortedMap),通过Collections.synchronizedXXX(xx)可以把它们封装成线程安全的容器。其他例如Java.util.regex是一个perl样式的正则表达式库,java.util.prefs用来管理持久性存储,java.nio非阻塞型I/O,还有java.util.LinkedHashMap, java.util.LinkedHashSet, java.util.IdentityHashMap(这个类和普通的哈希迈普的不同是他比较键的方式是==,也就是引用相同,而普通哈希迈普是equal)这些新的集合类。

20.   不要使用浮点数进行货币计算,1.03-0.42会得出0.6100000000000001而不是0.61.使用BigDecimal或者int或者long进行货币计算。浮点数用于科学计算。虽然使用BIGDECIMAL耗资源,不方便,但是避免计算错误。总而言之,当需要一个精确的计算结果时,避免float或者double

21.   关于ThreadLocal,这是java.lang中提供的一个变量,他的作用是在每个线程中维护一个属于此线程变量,他提供了getset方法。原理是:每个线程有一个变量叫做threadlocalmap用来保存属于自己的一组变量,map是需要键来取值的,因此,threadloca首先从每个线程中取得map,然后将自己作为key来进行取值和存值,它并不是用来保证对象的对方访问的。并且他的map里面的entry是一个weakreference,是可以自动释放的,速度快且方便。

22.   使用反射会导致:错过了编译时类型检查(很多运行时使用反射导致的错误如果正常使用是不会编译通过的),编写了一些晦涩难懂又长又臭的代码和,性能的损失(可能会几十倍的慢于正常的方法调用)。反射应仅仅用于设计时,作为一种工具来使用,比如自动生成代码等情况,或者代码分析工具。在正常运行的一个应用程序中不应该出现反射。如果你必须一定要在运行时使用一些编译时未知的类,那么记住仅仅使用反射来实例化他,而是用他的接口或者超类来使用他。

23.   Native代码主要用于访问(1)一些平台相关的特性,(2)使用遗产代码和(3)编写一些性能很重要的代码块。使用了native代码后,java的跨平台特性将被减弱,且可能发生本地代码导致的内存破坏等问题。现在的java平台速度越来越快,且提供越来越多的特性,应该尽量避免使用native方法。

24.   不要为了实现性能更好的程序而挣扎,力求写更符合规则,更好的代码,性能会随之而来。

25.   命名规则应遵循印刷规则或者语法规则。

26.   异常的使用应该仅仅限于需要检查异常的地方,而不应该被用于一般正常的语句逻辑。

27.   JAVA的异常有三种,检查型异常(CHECKED EXCEPTION),运行时异常(RUN TIME…),错误(ERROR)。第一种应用于处理程序中出现的异常可恢复的状况,第二种用于处理一些程序错误,例如数组越界、空指针(你不期待恢复这些状况),第三种不应该由我们来使用,它是一些系统级别的错误,例如资源不足等情况。

28.   异常所表达的内容应该和它的抽象层次所表达的内容相一致,底层次的异常到了上层,如果不能代表上层次的内容,则应被上层捕捉以后封装为上层抽象相应的内容再抛出。在JAVA1.4及以后的版本中,VMThrowable加入了管理异常链的支持。

29.   同步关键字synchronizedVM规范定义了除了longdouble以外的其他原始类型的操作都是原子的。但是原子性不代表线程安全,原子性保证一个线程在访问一个原子变量是不会得到一个随机值,而不保证一个线程写的值对另一个线程可见。因此,同步关键字不仅用于保证互斥访问,也用于保证可靠通信(volatile关键字同样可以保证后者)。另外,一个对象在其构造完成之前就会把自己的引用发布出来,如果不正确的同步,会导致其他线程读到其非正确的状态(未初始化的状态)。如果要兼顾线程安全和延迟加载,有一种策略是使用静态内部类中的静态变量来引用那个实例,根据VM规范,变量在所属类第一次被使用时被初始化,而静态内部类在内部只是定义,未被使用,因此其中的静态变量不会被初始化(可以用于单例模式)。

30.   关于过度的同步。不要在同步语句块中包含交给client去实现的函数,因为这有很大的不确定性(导致死锁或性能的大幅度下降)。在同步语句块中,应做尽可能少的工作。当设计自己的类时,考虑是否有必要加入同步,可以在外部增加包装器类的方式增加同步功能,就像Collections做的那样。

31.   永远记住一定要在一个while中使用wait。比如一个生产者队列为空时,很多消费者都在等待,而当队列中被加入一个产品并使用了notifyall指令,那么可能导致消费者数量仍然大于产品的情况,因此再次判断是否需要wait是很必要的。并且使用notifyallnotify要有性能的优势。

32.   不要使用busy-wait(比如轮询一个状态)。使用wait-notify(比如回调)。

33.   不要使用threadgroup

你可能感兴趣的:(Effective Java)