Effective Java读书笔记

Table of Contents generated with DocToc

2、创建和销毁对象

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

使用静态工厂方法的好处:

  1. 具有适当名称,易阅读
  2. 将构建好的实例缓存起来,重复利用,不用每次调用都会创建一个新对象,从而提高性能
  3. 可以返回原返回类型的任何子类型的对象,提高灵活性
    sample java.util.EnumSet 为例子
      1 如果他的元素=64 return RegularEnumSet
      2 否则 return JumboEnumSet
      3 class RegularEnumSetE extends EnumE extends EnumSetE 都是他的子类
      4 对客户端不可见 根据参数返回适配类型  更加灵活
      @param elementType
      @param E
      @return
     
    public static E extends EnumE EnumSetE noneOf(ClassE elementType) {
        Enum[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType +  not an enum);

        if (universe.length = 64)
            return new RegularEnumSet(elementType, universe);
        else
            return new JumboEnumSet(elementType, universe);
    }
  1. 在创建参数化类型实例的时候,更加简洁
MapString,ListString mStaticMap = newInstance()

使用静态工厂方法的坏处:

  1. 类如果不含公有的或者受保护的构造器,就不能被子类化
  2. 与其他的静态方法实际上没有任何区别

2、遇到多个构造器参数时考虑用构造器

  1. 重叠构造器模式:包含所有可选参数的构造器,许多本不想设置的参数,还要传值,难写难阅读
  2. JavaBeans模式:调用一个无参构造器创建对象,然后调用setter方法来设置每个必要参数,无法校验每个参数的有效性
  3. Builder模式:让客户端利用所有必要的参数得到一个builder对象,然后客户端调用无参的build方法来生成不可变的对象,这个builder是它构建的类的静态成员类,灵活易阅读
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
         Required parameters
        private final int servingSize;
        private final int servings;

         Optional parameters - initialized to default values
        private int calories      = 0;
        private int fat           = 0;
        private int carbohydrate  = 0;
        private int sodium        = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val)
            { calories = val;      return this; }
        public Builder fat(int val)
            { fat = val;           return this; }
        public Builder carbohydrate(int val)
            { carbohydrate = val;  return this; }
        public Builder sodium(int val)
            { sodium = val;        return this; }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
            calories(100).sodium(35).carbohydrate(27).build();
    }
}

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

1.枚举,极简

public enum Elvis {
    INSTANCE;
}
  1. 静态内部类
public class Singleton {

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton (){}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

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

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

如果对象是不可变的,它就始终可以被重用,反面例子

String s = new String(stringette);

推荐静态工厂方法Boolean.valueOf(String)
改造后的Person类只初始化时创建Calendar实例一次

import java.util.;

class Person {
    private final Date birthDate;

    public Person(Date birthDate) {
         Defensive copy - see Item 39
        this.birthDate = new Date(birthDate.getTime());
    }

    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calendar gmtCal =
            Calendar.getInstance(TimeZone.getTimeZone(GMT));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) = 0 &&
               birthDate.compareTo(BOOM_END)     0;
    }
}

6、消除过期的对象引用

消除过期引用除了减少内存占用,还有好处就是如果被错误地解除引用,程序会抛空指针异常,而不是悄悄地错误地运行下去。

一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空

  1. 内存泄露的一个常见来源是缓存,可以用WeakHashMap代替缓存,当缓存中的项过期之后,它们就会自动被删除
  2. 内存泄露的另一个常见来源是监听器和其他回调,注册类回调但没取消注册,确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用,只将它们保存成WeakHashMap中的键

7、避免使用终结方法

终结方法的缺点在于

  1. 不能保证会被及时地执行
  2. 性能耗损,用终结方法创建和销毁对象慢李大约430倍

显式的终结方法与try-finally结构结合起来使用,以确保及时终止

3、对于所有对象都通用的方法

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

  • ==是物理相等
  • equals是逻辑相等

因为每个类的实例对象本质上都是唯一的 ,利用物理相等(==)是指一个实例只能相等于它自己。

利用逻辑相等是(equals)指 一个实例是否和另一个实例的某些关键域相等,从而来判断这两实例是否相等。

什么时候需要覆盖Object类的equals方法?

  1. 当一个类需要自己的特有的“逻辑相等”时,例如我们希望两个实例的某些关键域相等时,我们就认为它们两者是逻辑上相等的,调用equals方法就返回true。
  2. 如果不覆盖equals方法,继承Object类的equals方法,实际上进行的物理相等的判断,即一个实例只能和它自身相等。

什么时候不需要覆盖Object类的equals方法?

  1. 希望这个类的实例只能和自身相等,不覆盖equals方法,只继承Object类的equals方法。
  2. 这个类的超类已经覆盖了Object类的equals方法,并且这个类继承超类的equals方法,对于它自身来说也是合适的,这时不覆盖equals方法。

正确覆盖equals方法,需要遵守它的通用约定:

  1. 自反性: x.equals(x) 必须返回true 。实例自身必然逻辑相等。
  2. 对称性: x.equals(y) 与 y.equals(x) 返回结果应该相同,同为true或者同为false。
  3. 传递性: x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)应该返回true。
  4. 一致性: 只要比较的实例对象的关键属性值没有改变 ,那么无论调用多少次equals方法返回的结果都应该相同,一致。
  5. 对于非null的x实例,x.equals(null) 永远返回false。

高质量equals方法:

  1. 使用==操作符检查“参数是否为这个对象的引用”。
  2. 使用instanceof操作符检查“参数是否为正确的类型”。
  3. 把参数转换成正确的类型。
  4. 对于该类的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。

9、覆盖equals时总要覆盖hashCode

在每个覆盖了equals方法的类中,也必须覆盖hashCode方法,
因为相等的对象必须具有相等的散列码

如果和基于散列的集合(HashMap、HashSet、HashTable)一起工作时,特别是将该对象作为key值的时候,一定要覆盖hashCode,否则会出现错误

10、始终要覆盖toString

无论是否指定格式,都要为toString返回值中包含的所有信息,提供一种编程式的访问途径。这样不仅可以提高程序的性能而且还会为回避掉在解析格式化字符串的过程出可能会出现错误的问题

11、谨慎地覆盖clone

在java中我们想得到一个和之前用过的对象一模一样的新对象,简单的使用=符号复制是不行的,我们需要用到clone方法,这就是clone方法的用处。因为clone方法是protected类型的,所以不能在外部直接使用,下面一段代码简单说明一下clone方法的使用:

public class CloneObject implements Cloneable {
  public String field01;
  public Object clone() {
    try {
      return super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
      throw new AssertionError();
    }
  }
}

首相我们需要是想Cloneable接口,然后再重载一个public类型的clone方法,然后在里面返回super.clone()就行了。

使用拷贝构造器或者拷贝工厂方法来代替覆盖Clone方法

拷贝构造器例子:

public class MyObject {
  public String field01;

  public MyObject() {
  }

  public MyObject(MyObject object) {
    this.field01 = object.field01;
  }
}

拷贝静态工厂:

public class MyObject {
  public String field01;

  public MyObject() {
  }

  public static MyObject newInstance(MyObject object) {
    MyObject myObject = new MyObject();
    myObject.field01 = object.field01;
    return myObject;
  }
}

12、考虑实现Comparable接口

1、如果实现了Comparable接口,可以将它在数组或是集合中进行排序。Comparable这个接口是用来实现对象排序的。

2、什么时候应该考虑是想Comparable接口

  1. 你写的类是一个值类。
  2. 类中有很明显的内在排序关系,如字母排序、按数值顺序或是时间等。
  3. 前面两者是并且关系。

3、如何很好的实现Comparable接口

  1. 满足对称性。即对象A.comparaTo(B) 大于0的话,则B.comparaTo(A)必须小于0;
  2. 满足传递性。即对象A.comparaTo(B) 大于0,对象B.comparaTo(Z)大于0,则对象A.comparaTo(Z)一定要大于0;
  3. 建议comparaTo方法和equals()方法保持一致。
    即对象A.comparaTo(B)等于0,则建议A.equals(B)等于true。
  4. 对于实现了Comparable接口的类,尽量不要继承它,而是采取复合的方式。

4、类和接口

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

尽可能地使用每个类或成员不被外界访问,可以有效的解除系统中各个模块的耦合度、实现每个模块的独立开发、使得系统更加的可维护,更加的健壮。

如何最小化一个了类中的成员的可访问性?

  1. 首先设计出该类需要暴露出来的api,然后将剩下的成员的设计成private类型。然后再其他类需要访问某些private类型的成员时,再删掉private,使其变成包级私有。
  2. 对于protected类型的成员,作用域是整个系统,所以,能用包访问类型的成员的话就尽量不要使用保护行的成员。
  3. 不能为了测试而将包中的类或者成员变为public类型的,最多只能设置成包级私有类型。
  4. 实例域绝对不能是public类型的。

14、在公有类中使用访问方法而不是公有域

有时候定义某些类,没什么作用,仅仅是集中一些实例域的。如:

class Point {
    public float x;
    public float y;
}

由于类设计成这样,可以直接通过对象访问,简洁了很多。但是这不符合面向对象的思想

所以最好我们为实例域提供访问方法,如:

class Point {
    private float x;
    private float y;

    public float getX() {return x;}

    public float getY() {return y;}

    public void setX(float x) {this.x = x;}
    public void setY(float y) {this.y = y;}
}

15、使可变性最小化

为了使类成为不可变,要遵循下面五条规则:

  1. 不要提供任何会修改对象状态的方法。(setter方法)
  2. 保证类不会被扩展。用final修饰类或者是私有化构造器提供公有静态工厂方法。
  3. 使所有的域都是final的。
  4. 使所有的域都成为私有的。
  5. 确保对于任何可变组件的互斥访问。

如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用。在构造器、访问方法和readObject中请使用保护性拷贝。

16、复合优先于继承

继承(inheritance)是实现代码重用的有力手段,但并非总是最好的选择。继承打破了封装性,因为子类依赖于超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而有所变化,导致子类遭到破坏。

用复合来扩展一个类需要实现两部分:新的类和可重用的转发类。转发类用于将所有方法调用转发给私有域。这样得到的类非常稳固,不依赖于现有类的实现细节。

import java.util.;

public class InstrumentedSetE extends ForwardingSetE {
    private int addCount = 0;

    public InstrumentedSet(SetE s) {
        super(s);
    }

    @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;
    }

    public static void main(String[] args) {
        InstrumentedSetString s =
            new InstrumentedSetString(new HashSetString());
        s.addAll(Arrays.asList(Snap, Crackle, Pop));    
        System.out.println(s.getAddCount());
    }
}
import java.util.;

public class ForwardingSetE implements SetE {
    private final SetE s;
    public ForwardingSet(SetE s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public IteratorE iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection extends E c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public T T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

无论是add方法还是addAll方法都转发给了私有域s来处理,这些方法对于s来说总是一致的,不会受InstrumentedSet的影响。另一个好处是此时的包装类InstrumentedSet可以用来包装任何Set实现,有了更广泛的适用性。例如

SetDate s = new InstrumentedSetDate(new TreeSetDate(cmp));
SetE s2 = new InstrumentedSetE(new HashSetE(capacity));

只有当子类和超类之间确实存在父子关系时,才可以考虑使用继承。否则都应该用复合,包装类不仅比子类更加健壮,而且功能也更加强大。

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

  1. 可继承类必须有文档说明它可覆盖(overridable)的方法的自用性(self-use)。
  2. 好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。
  3. 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。

18、接口优于抽象类

使用接口的原因:

  1. 现有的类可以很容易被更新,以实现新的接口。
  2. 接口是定义mixin(混合类型)的理想选择。
  3. 接口允许我们构造非层次结构的类型框架。

注意:类可以实现多个接口,而且接口和接口之间也是可以继承的

如果使用抽象类来定义类型,那么除了使用继承的手段来增加功能,没有其他的选择,通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来,接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。

19、接口只用于定义类型

当类实现接口时,接口就充当可以引用这个类的实例的类型,不应该被用来导出常量

20、类层次优于标签类

21、用函数对象表示策略

函数指针,就是将函数存储起来并传递,主要用途就是实现策略模式。为了在Java中实现种子模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体的策略只被使用一次时,通常使用匿名内部类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员,并通过共有的静态final域被导出,其类型为该策略接口。

例子:比较器函数
在 c 语言中,qsort 函数要求使用一个指向比较器函数的指针作为参数,不同的比较器函数可以得到不同的排序结果,这就是一种策略

Arrays.sort(StringArray,new ComparatorString(){
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

22、优先考虑静态成员类

嵌套类是定义在另一个类的内部的类,目的为外围类提供服务,有四种

  1. 静态成员类
  2. 非静态成员类
  3. 匿名类
  4. 局部类

静态成员类,可看成普通的类,只不过碰巧被声明在一个类的内部,可以访问外围类的所有成员,包括那些声明私有的成员。
静态成员类和非静态成员类的区别是非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联

如果一个嵌套类需要在单个方法之外仍然可见,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向外围实例的引用,就做成非静态的,否则做成静态的。如果这个嵌套类属于一个方法的内部,只需要在一个地方创建实例,并且有预置的类型可以说明这个类的特征,就做成匿名类,否则,做成局部类。

5、泛型

23、请不要在新代码中使用原生生态类型

原生生态类型是不带任何实际类型参数的泛型名称,比如List < E> 对应的原生生态类型List。

如果使用原生态类型,就失去了泛型在安全性和表述性方面的所有优势。

List和List区别

后者提供了编译期检查,明确的表明集合中可以放任何类型的元素

举例:对于方法 f(List param)和方法f(List),前者可以接受List和List类型的元素,后者则不行;因为后者可以接受任何类型的元素,即是Object类的子类,而List只能接受String类型的元素,List只能接受Integer类型的元素

因此,List类型丢了了安全性,而List保证了安全性

在这里插入图片描述

24、消除非受检警告

要尽可能的消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的,这意味着不会在运行时出现ClassCastException异常。

如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下)才可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。

25、列表优先于数组

数组与泛型的区别是

  1. 数组是协变类型的,协变的意思就是Number是Integer的父类,那么Number[] data = new Integer[]; 是成立的。泛型是不可变类型,就是说无法List< Number> list = new List< Integer>();

  2. 数组是具体化的,数组在运行时才知道并检查它们的元素类型约束。泛型是不可具体化的,运行期间虚拟机获得的信息不能够比编译期间更多(擦除导致的问题)。

数组与泛型无法结合使用
数组与泛型的结合:new List[]、new E[] 都是错误的。
因为数组是在运行期检验的,但是运行期间泛型会被擦除,从而导致Java虚拟机无法判断类型的安全性。同时由于数组是运行期检验,所以具有协变的特性。这也导致了泛型无法与数组结合。

26、优先考虑泛型

使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,更容易

import java.util.Arrays;

public class Stack<E> {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    // Appropriate suppression of unchecked warning
    public E pop() {
        if (size==0)
            throw new EmptyStackException();

        // push requires elements to be of type E, so cast is correct
        @SuppressWarnings("unchecked") E result = 
            (E) elements[--size];

        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    // Little program to exercise our generic Stack
    public static void main(String[] args) {
        Stack<String> stack = new Stack<String>();
        for (String arg : args)
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

27、优先考虑泛型方法

考虑如下的方法,它的作用是返回两个集合的联合:

// Users raw types - unaccepable!
public static Set union(Set s1, Set s2) {
	Set result = new HashSet(s1);
	result.addAll(s2);
	return result;
}
    编译这段代码会产生警告,为了修正这些警告(在新代码中不应该直接使用原始类型,当前是为了举例子)要将方法声名修改为声明一个类型参数,表示这三个元素类型(两个参数及一个返回值),并在方法中使用类型参数。声名类型参数的类型参数列表,处在方法的修饰符及其返回类型之间,修改后的代码如下:
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
	Set<E> result = new HashSet<E>(s1);
	result.addAll(s2);
	return result;
}

泛型方法的一个显著特征是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须要指定类型参数的,在为泛型方法的类型会存在一个类型推导的过程。编译器通过检查方法参数的类型来计算类型的值。在调用 泛型构造器的时候,要明确传递类型参数的值有点麻烦。

28、利用有限制通配符来提升API的灵活性

限定通配符包括两种:

  1. 表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类
  2. 表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类

上界<? extends T>不能往里存,只能往外取

下界<? super T>可以往里存,也可以往外取
通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。

29、优先考虑类型安全的异构容器

异构容器是指用key自身的class 类型作为key。因为Class 是参数化的类型,它可以确保我们使Context方法是类型安全的,而无需诉诸于一个未经检查的强制转换为T。这种形式的一个Class 对象称之为类型令牌(type token)

public class Context {
 
  private final Map<Class<?>, Object> values = new HashMap<>();
 
  public <T> void put( Class<T> key, T value ) {
    values.put( key, value );
  }
 
  public <T> T get( Class<T> key ) {
    return key.cast( values.get( key ) );
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值