Effective java 读书笔记( 二 )

来源:互联网 发布:豫广网络投诉电话 编辑:程序博客网 时间:2024/05/18 23:15

9.覆盖equals时总要覆盖hashCode

1.在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致

2.如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。

3.如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。

10.始终要覆盖toString

1.toString约定指出,“建议所有的子类都覆盖这个方法”

2.在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息

11.谨慎地覆盖clone

1.Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆(Clone),遗憾的是,它并没有成功地达到这个目的。其主要缺陷在于,它缺少一个clone方法,Object的clone方法是受保护的。如果不借助于反射,就不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法。

2.即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的clone方法。尽管存在这样那样的缺陷,这项设施仍然被广泛地使用着,因此值得我们进一步地了解

3.既然Cloneable并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportException异常。这是借口的一种极端非典型的用法,也不值得效仿。

4.通常情况下,实现接口是为了表明类可以为它的客户做些什么,然而,对于Cloneable接口,它改变了超类中受保护的方法的行为

5.如果实现Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议

6.永远不要让客户去做任何类库能够替客户完成的事情

7.clone架构与引用可变对象的final域的正常用法是不相兼容的,除非在原始对象和克隆对象之间可以安全地共享此可变对象。为了使类成为可克隆的,可能有必要从某些域中去掉final修饰符

8.Object ’s  clone method is not synchronized, so even if it is otherwise satisfactory, you may have to write a synchronized clone method that invokes super.clone()。

9.简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。

10.一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归地调用clone来完成,但这通常并不是最佳方法。如果该类只包含基本类型的域,或者指向不可变对象的引用,那么多半的情况是没有域需要修正。

11.这条规则也有例外,譬如,代表序列号或者其他唯一ID值的域,或者代表对象创建时间的域,不管这些域是基本类型还是不可变的,也都需要被修正。

12.考虑实现Comparable接口

1.无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面向对象的抽象优势。

2.针对equals的权宜之计同样也适用于compareTo方法。如果你想为一个实现了Compareable接口的类增加值组件,请不要扩展这个类,而是别写一个不相关的类,其中包含第一个类的一个实例。然后提供一个“视图(view)”方法返回这个实例。这样既可以让你自由地在第二个类上实现compareTo方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例

3.编写compareTo方法与编写equals方法非常相似,但也存在几处重大的区别。因为Comparable接口是参数化的,而且comparable方法是静态的类型,因此不必进行类型检查,也不必对它的参数进行类型转换。

4.如果一个域并没有实现Comparable接口,或者你需要使用一个非标准的排序关系,就可以使用一个显式的Comparator来代替。或者编写自己的Comparator,或者使用已有的Comparator。

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

1.要区别设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。

2.尽可能地使每个类或者成员不被外界访问

3.如果覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别,这样可以保证任何可使用超类的实例的地方也都可以使用子类的实例。

4.实例域决不能是公有的,因为包含公有可变域的类并不是线程安全的。

5.With the excep-tion of public static final fields, public classes should have no public fields.Ensure that objects referenced by public static final fields are immutable.

6.除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域索引用的对象都是不可变的

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

1.如果类是package-private类型,或者是private的嵌套类,直接暴露它的数据域并没有本质的错误

2.Java平台类库中有几个类违反了“公有类不应该直接暴露数据域”的告诫。显著的例子包括java.awt包中的Point和Dimension类。它们是不值得仿效的例子,相反,这些类应该被当作反面的 警告示例

3.public classes should never expose mutable fields. It is less harmful, though still questionable, for public classes to expose immutable fields.

4.It is, however, sometimes desirable for pac kage-private or private nested classes to expose fields, whether mutable or immutable.

15.使可变性最小化

1.不可变对象本质上是线程安全的,它们不要求同步,因此它们可以被自由地共享

2.“不可变对象可以被自由地共享”导致的结果是,永远也不需要进行保护性拷贝。实际上,你根本无需做任何拷贝,因为这些拷贝始终等于原始的对象。因此,你不需要,也不应该为不可变的类提供clone方法或者拷贝构造器。

3.不仅可以共享不可变对象,甚至也可以共享它们的内部信息。

4.为了使类成为不可变,要遵循如下规则:

(1)不要提供任何会修改对象状态的方法(mutatetor)

(2)保证类不会被扩展,为了防止子类化,一般做法是使这个类成为final的,但是也有其他方法

(3)使所有的域都是final的

(4)使所有的域都成为私有的

(5)确保对于任何可变组件的互斥访问,如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法(accessor)中返回该对象的引用。

5.在设计新的类时,选择用静态工厂代替公有的构造器可以让你以后有添加缓存的灵活性,而不必影响客户端

6.让不可变的类编程final的另一种办法是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂(static factory)来代替共有的构造器

7.当BigInteger和BigDecimal刚被编写出来的时候,对于“不可变的类必须为final的”还没有得到广泛地理解,所以它们的所有方法都有可能会被覆盖。遗憾的是,为了保持向后兼容,这个问题一直无法得以修正。如果你在编写一个类,它的安全性依赖于(来自不可信客户端的)BigInteger或者BigDecimal参数的不可变形,就必须进行检查,以确定这个参数是否为“真正的”BigInteger或者BigDecimal,而不是不可信任子类的实例。如果是后者的话,就必须在假设他可能是可变的前提下对它进行保护性拷贝。示例代码:

public static BigInteger safeInstance(BigInteger val) {if (val.getClass() != BigInteger.class)return new BigInteger(val.toByteArray());return val;}

8.本条目开头处关于不可变类的诸多规则指出,没有方法会修改对象,并且它的所有域都必须是final的。实际上,这些规则比真正的要求更强硬了一点,为了提高性能可以有所放松。事实上,这些规则比真正的要求更强硬了一点,为了提高性能可以有所放松。事实上应该是这样:没有一个方法能够对对象的状态产生外部可见(external visible)的改变。然而,许多不可变的类拥有一个或者多个非final的域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。如果将来再次请求同样的计算,就直接返回这些缓存的值,从而节约了重新计算所需要的开销。这种技巧可以很好地工作,因为对象是不可变的,它的不可变性保证了这些计算如果被再次执行,就会产生同样的结果。

9.坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类成为可变的类,否则就应该是不可变的。

10.不可变的类有许多优点,唯一缺点是在特定的情况下存在潜在的性能问题

11.在Java平台类库中,有几个类如java.util.Date和java.awt.Point,他们本应该是不可变的,但实际上却不是

12.你也应该认真考虑把一些较大的值对象做成不可变的,例如String和BigInteger,只有当你确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类

13.对于有些类而言,其不可变性是不切实际的。如果类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。降低对象的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。因此,除非有令人信服的理由要使域变成非final的,否则要使每个域是final的

16复合优先于继承

注:这里的“继承”指的是“实现继承”(implementation inheritance),即一个类扩展另一个类,这里讨论的问题并不适用于接口继承(interface inheritance),即不适用与一个类实现一个接口的时候,或者一个接口扩展另一个接口的时候

1.与方法调用不同,继承打破了封装性

2.如果在适合使用复合的地方使用继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更为严重的是,由于暴露了内部的细节,客户端就有可能直接访问这些内部细节。这样至少会导致语义上的混淆。

3.再决定使用继承而不是复合之前,还应该问自己最后一组问题。对于你正试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把那些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷

4.继承的功能虽然强大,但是存在诸多问题,它也违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性(fragility)。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

5.使用继承导致子类脆弱的一个相关的原因是,它们的超类在后续的发行版本中可以获得新的方法。

6.如果在扩展一个类的时候,仅仅是增加新的方法,而不是覆盖现有的方法,你可能会认为这是安全的。虽然这种扩展方式比较安全一些,但是也并非完全没有风险。

7.如果超类在后续的发行版本中获得了一个新的方法,并且不幸的是,你给子类提供了一个签名相同但返回类型不同的方法,那么这样的子类将无法通过编译。如果给子类提供的方法带有与新的超类方法完全相同的签名和返回类型,实际上就覆盖了超类中的方法。此外,你的方法是否能够遵守新的超类方法的约定,这也是很值得怀疑的,因为当你在编写子类方法的时候,这个约定根本没有面试。

8.要避免上面问题的方法,可以在新的类中增加一个私有域,他引用现有类的一个实例。这种设计被称作“复合”(composition),因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为“转发”(forwarding),新类中的方法被称为“转发方法”(forwarding method)。这样得到的类将会非常稳固,它不依赖与现有类的实现细节。即使现有类添加了新的方法,也不会影响到新的类。为了进行更具体的说明,请看下面的实例:

// Reusable forwarding classpublic class InstrumentedSet<E> extends ForwardingSet<E> {    private int addCount = 0;    public InstrumentedSet(Set<E> 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;    }}// Wrapper class - uses composition in place of inheritancepublic class ForwardingSet<E> implements Set<E> {    private final Set<E> s;    public ForwardingSet(Set<E> 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 Iterator<E> 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(); }}

17.要么为继承而设计并提供文档,要么禁用继承

1.对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。有两种办法可以禁止子类化。比较容易的办法是把这个类声明为final的。另一种办法是把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来代替构造器。

2.对于为了继承而设计的类,该类的文档必须精确地描述覆盖每个方法所带来的影响。话句话说,该类必须有文档说明它可以覆盖的方法的自用性(self-use)

3.对于每个公有的或者受保护的方法或者构造器,它的文档必须知名该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的

4.更一般地,类必须在文档中说明,在哪些情况下它会调用可覆盖的方法

5.按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“This implementation ”这样的句子不应该被认为是在表明该行为可能会随着版本的变迁而改变。它意味着这段描述关注该方法的内部工作情况

6.为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了是程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的(protected)方法,也可以是受保护的域,后者比较少见

Design for inheritance involves more than just documenting patterns of self-use. To allow programmers to write efficient subclasses without undue pain, a class may have to provide hooks into its internal working in the form of judiciously chosen protected methods or, in rare instance, protected fields.

7.为了对于继承而设计的类,唯一的测试方法就是编写子类,必须在发布类之前先编写子类对类进行测试

8.构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用

9.如果一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。

10.如果一个为了继承而设计的类中实现了Serializable,并且该类有一个readResolve或者writeReplace方法,就必须使readResolve或者writeReplace成为受保护的方法,而不是私有的方法

18.接口优于抽象类

1.现有的类可以很容易被更新,以实现新的接口。一般来说,无法更新现有的类来扩展新的抽象类。如果你希望让两个类扩展同一个抽象类,就必须把抽象类放到类型层次(type hierarchy)的高处,以便这两个类的一个祖先成为它的子类。遗憾的是,这样做会间接地伤害到类层次,迫使这个公共祖先的所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适。

2.接口是定义mixin(混合类型)的理想选择

3.接口允许我们构造非层次接口的类型框架

4.Skeletal implementations are useful since they reduce the effort required to implement the interface.

5,接口一旦被公开发行,并且已被广泛实现,要想改变这个接口几乎是不可能的,你必须在初次设计的时候就保证接口是正确的,如果接口包含微小的瑕疵,它将会一直影响你以接口的用户。如果接口有严重的缺陷,它可以导致API彻底失败

6.在发行新接口的时候,最好的做法是,在接口被“冻结”之前,尽可能让更多的程序员用尽可能多的方式来实现这个新接口。这样有助于在依然可以改正缺陷的时候就发现它们

7.接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更加重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。

8.如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。

9.应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来进行全面的测试

10.骨架实现类示例代码:

// Skeletal Implementationpublic abstract class AbstractMapEntry<K,V>implements Map.Entry<K,V> {// Primitive operationspublic abstract K getKey();public abstract V getValue();// Entries in modifiable maps must override this methodpublic V setValue(V value) {throw new UnsupportedOperationException();}// Implements the general contract of Map.Entry.equals@Override public boolean equals(Object o) {if (o == this)return true;if (! (o instanceof Map.Entry))return false;Map.Entry<?,?> arg = (Map.Entry) o;return equals(getKey(),   arg.getKey()) &&equals(getValue(), arg.getValue());}private static boolean equals(Object o1, Object o2) {return o1 == null ? o2 == null : o1.equals(o2);}// Implements the general contract of Map.Entry.hashCode@Override public int hashCode() {return hashCode(getKey()) ^ hashCode(getValue());}private static int hashCode(Object obj) {return obj == null ? 0 : obj.hashCode();}}

19.接口只用于定义类型

20.类层次优于标签类

1.标签类很少有适用的时候。当你想要编写一个包含显示标签类的时候,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替
2.标签类示例:
package com.example.activitys;//Tagged class - vastly inferior to a class hierarchy!class Figure {enum Shape {RECTANGLE, CIRCLE};// Tag field - the shape of this figurefinal Shape shape;// These fields are used only if shape is RECTANGLEdouble length;double width;// This field is used only if shape is CIRCLEdouble radius;// Constructor for circleFigure(double radius) {shape = Shape.CIRCLE;this.radius = radius;}// Constructor for rectangleFigure(double length, double width) {shape = Shape.RECTANGLE;this.length = length;this.width = width;}double area() {switch (shape) {case RECTANGLE:return length * width;case CIRCLE:return Math.PI * (radius * radius);default:throw new AssertionError();}}}
3.类层次代码示例:
//Class hierarchy replacement for a tagged classabstract class Figure {abstract double area();}class Circle extends Figure {final double radius;Circle(double radius) {this.radius = radius;}double area() {return Math.PI * (radius * radius);}}class Rectangle extends Figure {final double length;final double width;Rectangle(double length, double width) {this.length = length;this.width = width;}double area() {return length * width;}}
4.当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去

21.用函数对象表示策略

1.函数指针的主要作用就是实现策略模式,为了在Java中实现这种模式,要声明一个接口表示该策略,并且为每个具体策略声明一个实现了该接口的类
2.当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类
3.当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口
package com.example.activitys;import java.io.Serializable;import java.util.Comparator;//Exporting a concrete strategyclass Host {private static class StrLenCmp implements Comparator<String>, Serializable {public int compare(String s1, String s2) {return s1.length() - s2.length();}}// Returned comparator is serializablepublic static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();// Bulk of class omitted}

22.优先考虑静态成员类

1.嵌套类(nested class)存在的目的应该只是为它的外围类(enclosing class)提供服务
2.如果嵌套类将来可能会用于其他的某个环境,它就应该是顶层类(top-level class)
3.嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class),除了第一种之外,其他三种都被称为内部类(inner calss)
4.静态成员类是最简单的一种嵌套类,最好把它看作是普通的类,只是碰巧被生命在另一个类的内部而已
5.静态成员类可以访问外围类的所有成员,包括那些声明为私有的成员
6.静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则
7.如果静态成员类被声明为私有的,它就只能在外围类的内部才可以被访问
8.如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类,在没有外围实例的情况下,要想创建非静态成员类的实例时不可能的
9.如果只想访问成员类,而不需要访问外围实例,就需要始终把该成员类声明为静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。而保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留
10.匿名类的三种常见用法:动态地创建函数对象(function object),创建过程对象(process object)以及在静态工厂方法的内部
11.局部类是四种嵌套类中用的最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复地使用。与匿名类一样,只有当局部类是在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须非常简短,以便不会影响到可读性
12.假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则就做成局部类




END



原创粉丝点击