Effect Java 阅读笔记(一)

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 filed
    public class Elvis{
    public static final Elvis INSTANCE = new Elvis();
    private Elvis(){....}
    public void leaveTheBuilding(){.....}
    }
  • Singleton实现2

    //Singleton with static factory
    public 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 approach
    public 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;
@Override
public 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 hole
    public static final Thing[] VALUES = {...};
    • 长度非0的数组总是可变的。
    • 如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容,这是安全漏洞的常见根源。

    解决方法:

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

    private static final Thing[] PRIVATE_VALUES = {...};
    //build a unmodified view for the static array
    public 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;
      
      @Override
      public boolean add(E e) {
          addCount++;
          return super.add(e);
      }
      
      @Override
      public 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 inheritance
    class 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 class
    class 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),使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承兼顾会导致脆弱性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值