《effective java》阅读理解

290 阅读13分钟

java书籍如core Java经常分两册,上册为基础fundmental,下册为advanced。上册讲编程的基本概念,下册谈被升级的概念(如泛型对于Object,try是某种不判断的if,并发是循环的横向扩展,集合是某种高级的数组等等)。这类书给人的感觉是概念的任意组合,正交化,什么都可以做。
而本书所说的就是希望你用高级的概念的使用去避免基本概念使用的一些弊端,告诉你怎么做更好,什么样的做法可以避免。如书中有推荐使用工厂方法何时替代构造方法,集合何时优于数组,包装类与基本类的比较,ThreadLocal的由来等等。

书籍地址:github.com/singgel/Stu…

 

  代码应该被重用,而不是被拷贝。

  

一,考虑用静态工厂方法代替构造器

  public static Boolean valueOf(boolean b) {

  return b ? Boolean.TRUE : Boolean.FALSE;

  }

  静态工厂方法能够为重复的调用返回相同的对象。

  静态工厂惯用名称:valueOf, of, getInstance, newInstance, getType, newType.

二, 遇到多个构造器参数时要考虑用builder模式

三,用私有构造器或者枚举类型强化Singleton属性

  1,public class Elvis {

  public static final Elvis INSTANCE = new Elvis();

  private Elvis() { .... }

  public void leaveTheBuilding() {...}

  }

  2, public class Elvis {

  private static final Elvis INSTANCE = new Elvis();

  private Elvis() { ... }

  public static Elvis getInstance() {

  return INSTANCE;

  }

  public void leaveTheBuilding() { ... }

  }

  3, 单元素的枚举类型已经成为实现Singleton最佳方法

  public enum Elvis {

  INSTANCE;

  public void leaveTheBuilding() { ... }

  }

四,通过私有构造器强化不可实例化的能力

  只包含静态方法和静态域的工具类(utility class)实例对它没任何意义,只要让这个类构造器私有就可以保证不会被实例化。

  public class UtilityClass {

  private UtilityClass() {

  throw new AssertionError();

  }

  .....

  }

  副作用:这个类不能被继承。所有的构造器都必须显示或隐式地调用超类的构造器。

五,避免创建不必要的对象

  构造器在每次被调用时都会创建一个新的对象。可以使用静态工厂方法代替构造器,以避免创建不必要的对象。例:Boolean.valueOf(String)。

  静态初始化,只需初始化一次。

  延迟初始化。

六,消除过期的对象引用

  只要是类是自己管理内存,程序员就应该警惕内存泄漏。

七,比用使用终结方法

  终结方法(finalizer)通常是不可以预测的,也是很危险的。终结方法的缺点在于不能保证会被及时地执行。

  在C++中,析构器是回收一个对象所占资源的常规方法。Java中,一般用try-finally完成类似的工作。

 

八,覆盖equals时请遵守通用约定

  ==比较的是引用,equals比较的是值!

九,覆盖equals时总要覆盖hashcode

  @Override public boolean equals(Object O) { ... }

  @Override public int hashCode() { ... }

  相等的(equals)的对象必须具有相等的hash code。不相等的两个对象,hash code有可能相等。

  Eclispe提供了代码生成工具(Source->Generates hashCode() and equals()...)

十,始终要覆盖toString

  @Override public String toString() { ... }

十一,谨慎地覆盖clone()方法

  public interface Cloneable {

  }

  如果一个类实现了Cloneable,Object的clone方法就会返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException.

  clone()就是另一个构造器;你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。(深度拷贝,deep copy)

  无需调用构造器就可以创建对象。

  协变返回类型(covariant return type),override方法的返回类型可以是被覆盖方法返回类型的子类型。

  所有实现Cloneable接口的类都应该用一个公有方法覆盖clone。此方法首先调用super.clone(),然后clone自身域。

  @Override public Stack clone() {

  try {

  Stack result = (Stack)super.clone();

  result.elements = elements.clone();

  return result;

  } catch (CloneNotSupportedException e) {

  throw new AssertionError();

  }

  }

  另一个实现对象拷贝的办法是提供一个拷贝构造器或拷贝工厂:

  public Yum(Yum yum); copy ctor

  public static Yum newInstance(Yum yum); copy factory

十二,考虑实现Comparable接口

  类实现了Comparable接口,就表明它的实例具有内在的排序关系。

  public interface Comparable<T> {

  int compareTo(T t);

  }

  当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或正整数。

 

十三, 使类和成员的可访问性最小化

  private, package-private, protected, public

十四,在公有类中使用访问方法而非公有域

十五,使可变性最小化

十六,组合优先于继承

  实现继承(implementation inheritance), 接口继承(interface inheritance)

  组合(composition), 转发方法(forwarding method)

  包装类(wrapper class)-->Decorator模式

十七,要么为继承而设计并提供文档说明,要么就禁止继承

十八,接口优于抽象类

  抽象类允许包含某些方法的实现;接口不允许。

  为实现抽象类定义的类型,类必须成为抽象类的一个子类,Java只允许单继承;可以implement多个接口

十九,接口只用于定义类型

  静态导入(static import):JUnit,ass ertEquals()

二十,类层次优于标签类

二十一,用函数对象表示策略

  class StringLengthComparator {

  public int compare(String s1, String s2) {

  return s1.length() - s2.length();

  }

  }

  StringLengthComparator没有域是无状态的,因此非常适合成为Singleton。

  class StringLengthComparator {

  private StringLengthComparator() { }

  public static final StringLengthComparator INSTANCE = new StringLengthComparator();

  public int compare(String s1, String s2) {

  return s1.length() - s2.length();

  }

  }

  public interface Comparator<T> {

  public int compare(T t1, T t2);

  }

  Arrays.sort(stringArray, new Comparator<String>() {

  public int compare(String s1, String s2) {

  return s1.length() - s2.length();

  }

  };

  可以将函数对象存储到一个私有的静态final域中,并重用。

  class Host {

  private static class StringLengthComparator implements Comparator<String>, Serializable {

  public int compare(String s1, String s2) {

  return s1.length() - s2.length();

  }

  }

  public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StringLengthComparator();

  .....

  }

  函数指针的主要用途就是实现策略(Strategy)模式。Java中声明一个接口表示策略,并且为每个具体策略声明一个实现了该接口的类。例:Arrays.sort(array, comparator);

二十二,优先考虑静态成员类

  嵌套类有四种:静态成员类,非静态成员类,匿名类,局部类。

  非静态成员类的每个实例都隐含着与一个外围实例相关联

  匿名类一种常见用法是动态地创建函数对象;

  另一种用法是创建过程对象,比如Runnable,Thread或者TimerTask实例;

  第三种常见的用法是在静态工厂方法的内部。

 

二十三,不要在新代码中使用原生态类型

二十四,消除非受检警告

  @SuppressWarnings("unchecked")

二十五,List优先于数组

二十六,优先考虑泛型

二十七,优先考虑泛型方法

二十八,利用有限制通配符来提升API的灵活性

二十九,优先考虑类型安全的异构容器

 

三十,用enum代替int常量

三十一,用实例域代替序数

  public enum Ensemble {

  BOLO(1), DUET(2), TRIO(3), QUARTET(4);

  private final int numberOfMusicians;

  Ensemble(int size) { this.numberOfMusicians = size; }

  public int numberOfMusicians() { return numberOfMusicians; }

  }

三十二,用EnumSet代替位域

三十三,用EnumMap代替序数索引

三十四,用接口模拟可伸缩的枚举

三十五,注解优先于命名模式

三十六,坚持用Override注解

三十七,用标记接口定义类型

  标记接口(marker interface)是没有包含方法声明的接口。例:Serializable

  

三十八,检查参数有效性

  在方法体得开头处检查参数。

三十九,必要时进行保护性拷贝

四十,谨慎设计方法签名

  谨慎选择方法名;避免过长参数列表;

  参数类型优先使用接口而非具体类;

  对于boolean参数,优先使用两个元素的枚举类型:

  public enum TemperatureScale { FAHRENHEIT, CELSIUS }

四十一,慎用重载

  overload方法的选择是静态的,overridden方法的选择是动态。

四十二,慎用可变参数

四十三,返回零长度的数组或者集合而不是null

  Collections.emptySet(), Collections.emptyList(), Collections,emptyMap(0

四十四,为所有导出的API元素编写文档注释

 

四十五,将局部变量的作用域最小化

四十六,for-each循环优先于传统的for循环

  for (Iterator i = c.iterator(); i.hasNext(); ) {

  doSomething((Element)i.next());

  // i.remove();

  }

  public interface Iterable<E> {

  Iterator<E> iterator();

  }

  三种情况下无法使用for-each循环:过滤,转换,平行迭代。

四十七,了解和使用类库

  java.lang.*, java.util.*, Collections Framework

四十八,如果需要精确的答案,请避免使用float和double

  BigDecimal

四十九,基本类型优先于装箱类型

  永远不要用==做相等测试!Object.equals(object);

五十,如果其他类型更适合,则避免使用字符串

五十一,需要连接字符串时,用StringBuilder代替String

五十二,优先使用接口而非类来引用对象

五十三,接口优先于反射机制

五十四,谨慎地使用本地方法

五十五,谨慎地进行优化

  必须在设计时考虑性能问题;使用性能剖析工具探测性能瓶颈

五十六,遵守普遍接受的命名惯例

 

五十七,只针对异常的情况才使用异常

五十八,对可恢复的情况使用受检情况,对编程错误使用运行时异常

五十九,避免不必要地使用受检的异常

六十,优先使用标准的异常

六十一,抛出与抽象相对应的异常

六十二,每个方法抛出的异常都要有文档

六十三,在细节消息中包含能捕获失败的信息

六十四,努力使失败保持原子性

六十五,不要忽略异常

 

六十六,同步访问共享的可变数据

六十七,避免过度同步

六十八,executor和task优先于线程

六十九,并发工具优先于wait和notify

七十,线程安全性的文档化

七十一,延迟延迟初始化

七十二,不要依赖于线程调度器

七十三,避免使用线程组

 

七十四,谨慎地实现Serializable接口

七十五,考虑使用自定义的序列化形式

七十六,保护性地编写readObject方法

七十七,对于实例控制,枚举类型优先于readResolve

七十八,考虑用序列化代理代替序列化实例

深入思考

  忘记从哪里看见过一句话,大意是:只有了解全部的真相,才能获得全部的自由——编程领域更是如此。就好比我读《Effective Java》,如果只是简单阅读第12条考虑实现Comparable接口一节,想必几分钟就草草略过了。但是我获得了什么?是能够和别人吹嘘说:我把《Effective Java》看了3遍,《Java编程思想》看了5遍?即使看10遍又有什么意义呢?如果我不花费时间,问自己几个问题,亲自去研究一下Comparable接口,那么我不会知道JDK底层的实现细节,我并没有了解全部的真相,每次使用时都会有个疑问:咦,Arrays.sort是怎么通过对象的Comparable接口,实现不同的排序的?如果不去解决自己的疑惑,那么问题只会越来越多、越来越严重。

  套用M·斯科特·派克《少有人走的路——心智成熟的旅程》中的一句话(大意):逃避问题所带来的痛苦,甚至比问题本身所带来的痛苦,更为严重;问题永远不会自动消失,直到我们去直面问题、解决问题。我可能是有些过于类比了,但是如果关于编程的疑问,我们不去自己解决并亲自弄明白,那么我们就是在逃避问题。

  深入思考具有许多好处(自己浅显的总结,欢迎大家留言讨论):

  1. 不会每次遇到问题,就想一次:这个功能是怎么实现的?这次没时间,还是以后再研究吧!

  2. 大脑的容量是有限的,这些问题只有在真正解决之后,才不会占用大脑的容量。《Head First》系列在引言部分,提出的第一条建议就是:慢一点,你理解的越多,需要记住的就越少。

  3. 在我们完全理解一个问题后,下次再遇到同样的问题,只会更加深对于这个问题的思考,而不是那个老问题(这个功能是怎么实现的)。

  4. 增加自信,获得自由。

  深入思考并不会占用太多时间,我们离大部分真相都只有一步的距离。而且重要的是思考的深度,而且这样思考能够带来很大的满足感。至少我在读《Effective Java》时,就是这个感觉——一点也不觉得累,通过不断问自己问题,获得了很多很多知识,并且一直处于兴奋状态。

  优秀的框架(习惯),是指在无意识中也不会犯错

  在第45条将局部变量的作用域最小化一节中(中文第二版P182),作者为了说明为什么将作用域最小化,for循环比while循环好时,列举出了一个代码片段,包含一个“剪切——粘贴”错误:本来是要初始化一个新的循环变量i2,却使用了旧的循环变量i。代码仍然能够通过编译,运行的时候也不会抛出异常,但是实现的功能却是错误的。如果使用了for-each形式,就会从根本上避免这种无意识的错误。

  作者是Java API的设计者,考虑得不仅仅是如何才能够使Java API实现效率更高,还包括如何让客户端更具灵活性、如何从架构的设计上,减少客户端犯错误的机会。这完完全全也适合每一个开发者。进一步思考,在现实中也是一样啊!我曾经看过一篇文章,讨论如何通过优秀的习惯,减少无意识中犯错误的机会。

  最重要的是思想,而不是实现细节

  细节很重要,但是书籍、框架乃至Java的设计思想,才是最重要的(所以Bruce Eckel的《Thinking in Java》才会翻译成《Java编程思想》?)。在阅读源码时,不仅仅是语义上的理解,考虑一下为什么要这么实现这个功能?这样实现有什么好处?它的适用场景有哪些?是否还能够改进?哪些思想我能够借鉴到平时的工作和生活中?

  

抓住思想的好处:

  1. 提纲挈领,能够忽略不重要的细节,将注意力集中在重要的地方(所谓的二八法则)。

  2. 阅读速度的提升。可以快速略过不重要的细节,我的阅读效率大大提升。

  3. 能够看清全局,对整体有清晰的把握。

  4. 可以改良自己的思考方式,使得我们像一个科学家一样思考、像一个Java专家一样思考。