Effect Java 阅读笔记(一)

来源:互联网 发布:代理商域名转到阿里云 编辑:程序博客网 时间:2024/06/03 17:06

Chapter2 创建和销毁对象

1. 考虑用静态工厂方法代替构造器

  1. 一个静态工厂的小例子

    //以下方法得到的对象是事先构造好的不可变对象,反复利用public static Boolean valueOf(boolean b){  return b? Boolean.TRUE : Boolean.FALSE;}
  2. 使用静态工厂的优势

    • 有名称,见文知意(当一个类需要多个带有相同签名的构造器时,就可用静态工厂方法代替构造器,并且慎重的选择它们的名字以便于区分
    • 不必在每次调用的时候都创建新的对象(例如Singleton、上面例子
    • 可以返回原类型的任何子类型的对象

      Effj : API可以返回对象,同时又不会使对象的类型变成公有的,以这种方式隐藏实现类会使API变得十分简洁

    • 在创建参数化类型实例的时候,静态工厂方法使代码变得简洁

      //有了静态工厂方法,编译器可以替你找到类型参数(类型推导 type inference)public static <K, V> HashMap<K, V> newInstance(){  return new HashMap<K, V>();}
  3. 静态工厂方法的缺点
    • 类如果不含有公有的或者受保护的构造器,就不能被子类化(也有好处,鼓励程序员使用复合,而不是继承)
    • 它们与其他静态方法没有什么区别,在API文档中,没有明确标识

2. 遇到多个构造器参数时要考虑使用构建器(Builder)

简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是不错的选择

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

Singleton指仅仅被实例化一次的类,Singleton通常备用来代表那些本质上唯一的组件

  • Singleton实现1

    //Singleton with public final filedpublic class Elvis{public static final Elvis INSTANCE = new Elvis();private Elvis(){....}public void leaveTheBuilding(){.....}}
  • Singleton实现2

    //Singleton with static factorypublic class Elvis{private static final Elvis INSTANCE = new Elvis();private Elivis(){....}public static getInstance(){ return INSTANCE; }public void leaveTheBuilding(){...}}
  • Singleton实现3

    //Enum singleton - the preferred approachpublic Enum Elvis{INSTANCE;public void leaveTheBuilding(){...}}

    单元素的枚举类型已经成为实现Singleton的最佳方法。(它更加简洁,无偿提供了序列化机制,绝对防止多次实例化)

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

显示指定构造器是私有的(则该类不可被实例化,也同时不可被子类化)

ps : 企图通过将类做成抽象类来强制该类不可被实例化是行不通的,该类可被继承,继而实例化

5. 避免创建不必要的对象

  • 静态工厂方法优先于构造函数,避免不必要的对象创建
  • 重用不可变对象
  • 延迟初始化实现更复杂,但性能并没有很大的提升
  • 优先使用基本数据类型而不是装箱类型,当心无意识的自动装箱
  • 当对象的创建相当重量级时,才应该通过维护自己的对象池来避免创建对象

本条提及 “当你应该重用对象的时候,请不要创建对象”,对应的在39条说 “当你应该创建对象的时候,请不要重用现有对象”

6. 消除过期对象的引用

7. 避免使用终结方法(finalizer)

  • 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的

Chapter3 对于所有对象都通用的方法

8. 覆盖equals时请遵守通用约定

  • 几点规则
    • 自反性
    • 对称性
    • 传递性
    • 一致性
  • 实现高质量equals的诀窍
    • 使用==操作符检查 “参数是否为这个引用”
    • 使用instanceof操作符检查 “参数是否为正确的类型”
    • 把参数转换为正确的类型(上一步的instanceof可以保证这一点)
    • 对于该类中的每个关键域(significant)检查参数中的域是否与该对象中相应的域匹配
    • 当你编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的
  • 几点告诫
    • 覆盖equals时总要覆盖hashCode
    • 不要企图让equals方法过于智能
    • 不要将equals申明中的Object换成其它对象

9. 覆盖equals时总要覆盖hashCode

public final class PhoneNumber {    private final int areaCode;    private final int prefix;    private final int lineNumber;    public PhoneNumber(int areaCode, int prefix, int lineNumber) {        rangeCheck(areaCode, 999, "area code");        rangeCheck(prefix, 999, "prefix");        rangeCheck(lineNumber, 9999, "lineNumber");        this.areaCode = areaCode;        this.prefix = prefix;        this.lineNumber = lineNumber;    }    private void rangeCheck(int arg, int max, String name) {        if (arg < 0 || arg > max)            throw new IllegalArgumentException(name + ":" + arg);    }    @Override    public boolean equals(Object o) {        if (o == this)            return true;        if (!(o instanceof PhoneNumber))            return false;        PhoneNumber pn = (PhoneNumber) o;        return pn.lineNumber == lineNumber                && pn.prefix == prefix                && pn.areaCode == areaCode;    }    @Override    public int hashCode() {        return 42;    }    public static void main(String[] args) {        //如果没有覆盖hashCode,两个相等的实例具有不同的散列码        //要保证equals比较相等的两个实例返回相同的hashCode,散列集合才有效        Map<PhoneNumber, String> m = new HashMap<>();        m.put(new PhoneNumber(707, 867, 5309), "jeny");        String phoneNumber = m.get(new PhoneNumber(707, 867, 5309));    }}
  • 复写hashCode方法的一种实现

    • 把某个非0的常数值,比如说17,保存在一个名为result的int型变量中
    • 对于对象中的每个关键域f(指equals方法中涉及的每个域),完成以下步骤
    • 为该域计算int类型的散列码c
      • 如果该域是boolean型,则计算 (f ? 1 : 0) ;
      • 如果该域是byte、char、short、或者int、类型,则计算 (int)f ;
      • 如果该域是long类型,则计算 (int)(f ^ *(f >> 32));
      • 如果该域是float类型,则计算 Float.floatToIntBits(f) ;
      • 如果该域是double类型,则计算 Double.douebleToLongBits(f) ,然后按上述第三小步计算散列值
      • 如果该域是一个对象的引用, 并且该类的equals方法通过递归调用equals的方式来比较这个域,这同样为这个递归调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用hashCode。 如果这个域的值为null,则返回0(或者其他某个常数,通常为0) ;
      • 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列值。如果数组域中的每个元素都很重要,可以利用Arrays.hashCode方法
    • 按照下面的公式,把上述步骤计算得到的散列码c合并到result中

    result = 32 * result + c

  • 返回result
  • 完成了hashCode方法验证以后,问问自己“相等的实例是否都具有相等的散列码”
private volatile int hashCode;@Overridepublic int hashCode(){int result = hashCode;if(result == 0){  result = 17;  result += 31 * result + areaCode;  result += 31 * result + prefix;  result += 31 * result + lineNumber;  hashCode = result;}return result;}

不要试图从散列码计算中排除掉一个对象的关键部分来提高性能

10. 始终要覆盖toString方法

返回的字符串应该是简洁的,但信息丰富,并且易于阅读

11. 谨慎的覆盖clone方法

12. 考虑实现Compareable接口


Chapter4 类和接口

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

  • 尽可能的让每个类或者成员不被外界访问

    • 如果类或者接口能做成包级私有的,那么它就应该被做成包级私有的
    • 如果一个包级私有的顶层类(或者接口)只在某一个类的内部使用到,考虑做成私有嵌套类
    • 降低不必要共有类的可访问性,比降低包级私有的顶层类更重要的多
    • 接口中所有的方法都隐含着公有访问级别
    • 子类中复写的方法,其访问权限不能低于超类对应方法的访问权限
  • 实例域决不能是公有的

  • 类具有公有的静态final数组域,或者返回这种与的访问方法,这几乎总是错的

    //Potential security holepublic static final Thing[] VALUES = {...};
    • 长度非0的数组总是可变的。
    • 如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容,这是安全漏洞的常见根源。

    解决方法:

    • 可以使数组变成私有的,并增加一个公有的不可变列表

    private static final Thing[] PRIVATE_VALUES = {...};//build a unmodified view for the static arraypublic static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  • 可以使数组变成私有的,并添加一个公有的方法,它返回私有数组的一个备份

    private static final Thing[] PRIVATE_VALUES = {...};public static final Thing[] values(){  return PRIVATE_VALUES.clone();}

14. 在公有类中使用访问方法而非公有域

  • 公有类不应该直接暴露数据域(数据域私有,提供getter、setter)
  • 如果类是包级私有的,或者是私有的嵌套类,则直接暴露它的数据域没有本质的错误
    总之,公有类永远都不应该暴露可变的域

15. 使可变性最小化

不可变类:实例不能被修改的类,每个实例包含的信息都必须在创建该实例时就提供,并且在整个生命周期(lifetime)内固定不变。(String, 基本类型的包装类,BigInteger,BigDecimal)=>不可变类比可变类更易于设计、实现和使用。他们 不容易出错且更加安全

  • 不可变类遵循的5条规则
    • 不要提供任何会修改对象状态的方法
    • 保证类不会被扩展/继承(一般做法是使类成为final,也可让构造器私有化,并添加公有的静态工厂来替代公有的构造器
    • 使所有的域都是final的
    • 使所有的域都成为私有的(可防止客户端获得域引用的可变对象的权限,同时防止客户端直接修改这些对象
    • 确保对于任何可变组件的互斥访问(如果类具有指向可变对象的域,则必须确保客户端无法获得该对象的引用;在构造器、访问方法和readObject方法中都应该采用保护性拷贝技术(defensive copy)
  • 不可变类本质上是线程安全的,它不要求同步,可被自由的共享
  • 不可变类唯一的缺点就是对于每个不同的值都要有一个单独的对象
  • 坚决不不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类成为可变的类,否则就应该是不可变的(唯一的缺点就是在特定的情况下存在潜在的性能缺陷)
  • 如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性

16. 复合优先于继承

  • 使用继承存在的问题

    • 假设我们要实现统计HashSet历史添加元素的个数,我们可能会有如下实现

      class InstrumentedHashSet<E> extends HashSet<E>{private int addCount = 0;@Overridepublic boolean add(E e) {    addCount++;    return super.add(e);}@Overridepublic boolean addAll(Collection<? extends E> c) {    addCount += c.size();    return super.addAll(c);}public int getAddCount() {    return addCount;}}
    • 但当我们执行如下操作测试的时候,发现输出的是6(这是因为HashSet的addAll方法是基于add方法实现的)

      InstrumentedHashSet<String> s = new InstrumentedHashSet<>();s.addAll(Arrays.asList("asd", "dsaf", "asdf"));System.out.println(s.getAddCount());
    • 对于上述问题,我们只要去掉对addAll方法的复写即可,但是同时它功能的正确性依赖于超类中addAll方法是基于add方法实现这一事实(这依赖于父类的实现细节,一旦父类的addAll方法更改实现方式,这个类可能变得不可用)

    • 导致子类脆弱的原因

      • 如果子类中复写了超类中的某个方法,并且这个实现依赖于超类中的某个实现细节, 如果 下一个发行版中该超类的对应实现改变了 ,这个子类就有可能出错

      • 如果子类不复写超类中的方法,只是扩展了一些方法。则 如果超类在后续的发行版本中获得了一个新的方法,并且该方法的签名与你在子类中扩展的方法相同,只是返回值类型不同 。那么此时子类将无法通过编译

  • 采用复合/转发的方法来代替InstrumentedHashSet类。(包含类本身和可重用的转发类 forwarding classs, 包含了所有转发方法,没有其他方法)

    //Wrapper class - uses composition in place of inheritanceclass InstrumentedHashSet<E> extends ForwardingSet<E>{    private int addCount = 0;    public InstrumentedHashSet(Set<E> s) {        super(s);    }    @Override    public boolean add(E o) {        addCount++;        return super.add(o);    }    @Override    public boolean addAll(Collection<? extends E> c) {        addCount += c.size();        return super.addAll(c);    }    public int getAddCount() {        return addCount;    }}//Reusable forwarding classclass ForwardingSet<E> implements Set<E>{    private final Set<E> s;    public ForwardingSet(Set<E> s) {        this.s = s;    }    @Override    public int size() {        return s.size();    }    @Override    public boolean isEmpty() {        return s.isEmpty();    }    @Override    public boolean contains(Object o) {        return s.contains(o);    }    @Override    public Iterator<E> iterator() {        return s.iterator();    }    @Override    public Object[] toArray() {        return s.toArray();    }    @Override    public <T> T[] toArray(T[] a) {        return s.toArray(a);    }    @Override    public boolean add(E e) {        return s.add(e);    }    @Override    public boolean remove(Object o) {        return s.remove(o);    }    @Override    public boolean containsAll(Collection<?> c) {        return s.containsAll(c);    }    @Override    public boolean addAll(Collection<? extends E> c) {        return s.addAll(c);    }    @Override    public boolean retainAll(Collection<?> c) {        return s.retainAll(c);    }    @Override    public boolean removeAll(Collection<?> c) {        return s.removeAll(c);    }    @Override    public void clear() {        s.clear();    }}
    • 上述InstrumentedHashSet类是一个包装类,可以用来包装任何Set实现;

    • InstrumentedHashSet对一个集合进行了修饰,为它增加了计数特性

    • 包装类几乎没有什么缺点,需要注意的是,包装类不适合用在回调框架(Callback framework)当中

    简而言之,继承功能非常强大,但也存在诸多问题,因为它违背了封装原则。只有当子类和超类之前确实存在继承关系(is-a),使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承兼顾会导致脆弱性。

原创粉丝点击