Effective Java 英文版第2版阅读笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Fun_Woody/article/details/83718506

CHAPTER 2

Item 1: Consider static factory methods instead of constructors

用静态工厂方法(在该条目中,这个名词与设计模式中的工厂方法模式并没有直接的关联)来创建对象,比起构造方法有以下好处:

  1. 可以任意命名。这样,我们可以给它取一个合适的名字来清晰地表达该方法的含义,不像构造方法只能通过参数(类型、个数、类型顺序等)来区分重载。
  2. 每次调用时,不一定非要创建一个新对象。这在需要频繁地创建相等的对象,尤其是当创建的代价不菲时,可以带来很大的性能提升。
    通过静态工厂方法在多次调用时返回同一个对象,这使得类对于任何时候应该存在哪些对象这一点,有着很好的控制,这样的类也称为实例可控的(instance-controlled)。有这样一些原因需要写实例可控的类:实例控制使一个类可以是单例的,或者不可实例化的;另外它也可以让一个不可变类不存在两个相等的实例,即a.equals(b)当且仅当a == b,如果某个类有这样的性质,那么它的客户端就可以使用==操作符来代替 equals(Object) 方法,这将带来性能提升(枚举类型就有这个性质)。
  3. 可以返回方法返回类型的任意子类型的对象。如Collections工具类,提供了大量便利的方法返回各种集合实例,如不可变集合,同步集合,等等,这些实例的类都是非public的,这样使Collections框架API精减了很多,也使得调这些API的client可以只引用接口,而不是实现类。
    通过传不同的入参,同一个方法可以返回不同类的实例;不同的发布版本之间也可能会改变返回的类与对象,这可能是为了提升软件的可维护性或性能。
  4. 减少了创建泛型类的实例时的冗余。

静态工厂方法有如下缺点:

  1. 没有public或protected构造方法的类无法被继承。如Collections框架提供的便利实现类都无法被继承,但也有人说这是因祸得福,因为这使得程序员们会去使用组合取代继承。
  2. 它们不是很容易跟其它静态方法区分开来。所以我们可能要多看看类中的静态工厂或接口注释,以及遵守约定俗成的静态工厂方法的命名规范:
  • valueOf——返回一个跟参数的值相同的实例,一般是类型转换方法。
  • of——valueOf的精简版,EnumSet用得比较多。
  • getInstance——根据参数返回一个实例,不一定是同一个实例。在单例类中,它没有参数,返回的是唯一的实例。
  • newInstance——跟getInstance差不多,但保证返回新的实例。
  • getType——同getInstance,但是用在工厂方法在另一个类里的情况下,"Type"表示的就是方法返回对象的类型。
  • newType——同newInstance,情况同上。

总而言之,静态工厂方法跟构造方法各有优劣,但总体来看静态工厂方法更佳。

CHAPTER 3

Item 8: Obey the general contract when overriding equals

重写equals方法很容易出错,结果后果可能很严重哦。最简单的办法就是不要重写,这样每个实例都只与自己相等。如果以下任一条件满足,就可以不重写:

  • 该类的每一个实例本来就是独一无二的。如Thread类,它代表的是一种活跃的实体,而不是简单的数值。
  • 你并不关心这个类是否能提供“逻辑上相等”的测试。如Random类,判断两个Random实例是否能提供相同的随机序列,这个可以有,但是一般不需要。
  • 父类已经重写了equals,它的行为也正适合这个子类。比如,很多Set的实现类从AbstractSet继承了equals方法,List实现类继承了AbstractList的,Map实现类继续了AbstractMap的。
  • 这个类是private或者package-private的,并且你确定它的equals方法绝对不会被调用。有争议地,有人说这种情况下应该重写equals方法以防它被意外调用:
@Override
public boolean equals(Object o) {
    throw new AssertionError(); // Method is never called
}

所以,当一个类有“逻辑相等”的概念(而不仅仅是对象的身份),且它没有父类来提供合适的equals时,重写equals就是比较合适的。值类型常常是这种情况。重写equals不仅仅是让类满足我们对其逻辑相等的预期,更是为了让实例在作为map的key和set的元素时,表现出可预测的、正确的行为。
重写equals方法要满足以下约定:

  • 自反
  • 对称
  • 传递
  • 一致
  • (取个什么名呢)对于任何非空引用x,x.equals(null)必须返回false。

有一点需要注意,当你扩展一个可实例化的类,并且新增值成员,那么你是没办法遵循上述所有规则的。但是可以用组合代替继承来解决这个问题。
不要让equals方法依赖不可靠的资源。例如,java.net.URL的equals方法依赖于URL对象相关联的host的IP地址,这在实践中会导致问题,但因为兼容性的问题,这个问题没法修复。几乎没有例外地,equals方法应该对内存中的对象表现出确定的行为。
另外,重写equals(Object o)时没必要对o进行判空,因为o instanceof SomeType在o为null时会返回false。
综上,做到以下几点可以写出一个高质量的equals方法:

  • 用==来检查是否参数就是该对象的引用,如果是,返回true。这仅仅是性能上的优化,但考虑到做比较可能开销不低,这一点还是值得的。
  • 用instanceof来检查参数是否具有正确的类型,如果否,返回false。典型情况下,正确的类型就是这个equals方法的类; 有时候,正确的类型是该类所实现的某个接口,举例来说,集合接口如Set, List, Map, Map.Entry都具有这个性质。
  • 将参数转化为正确的类型
  • 对于该类中每一个“重要的”的成员,检查参数的该字段与这个对象的该字段是否匹配,如果所有的都能匹配,返回true,否则返回false。
    对于非float和double的原始类型成员,用==来比较;对于对象成员,递归地调用equals来比较;对于float成员,用Float.compare方法;对于double成员,用Double.compare方法。对于数组成员,将上述准则应用于每一个元素,如果所有元素都需要比较,可以使用Arrays.equals的某一版本(1.5引入)。
    考虑到对象成员可能是null,可使用下述表达式来比较,以避免NPE:
    (field == null ? o.field == null : field.equals(o.field))
    又因为field和o.field经常是identical的(应该是指同一个,而不是逻辑相等吧),下述表达式可能性能更好:
    (field == o.field || (field != null && field.equals(o.field)))
    equals方法的性能也可能会受成员比较的顺序影响。应该先比较那些更有可能不同的成员,或者是那些比较开销较小的成员。一定不能比较与对象逻辑状态无关的成员,如用来同步操作的Lock成员。不需要去比较冗余成员,即可以根据“重要成员”计算出来的成员,不过这样做也有可能提升性能,即当某个冗余成员表示了该类的一个总体情况,那么该成员如果不相等,可以节省比较具体数据的成本。如一个多边形类,如果你保存了它的面积,那么当两个多边形面积不等时,你就不再需要去比较它的边和角了。
  • 写完一个equals方法时,要考虑它是否满足对称性、传递性、一致性。不要仅仅思考,还要写UT来检查。

具体的符合上述规则的equals案例可以参见Item 9。
下面是最后几个注意事项:

  • 重写equals时,一定要重写hashCode。
  • 不要自作聪明,简单地测试成员是否相等就好。
  • equals的参数类型就用Object,不要用任何其它类型去替代。否则你写的equals方法并没有覆盖Object.equals,而是重写了它。虽然可以在正常的版本之外再附加一个这样的强类型的equals方法(只要两个方法返回结果相同),但是它带来的性能优化很少,却增加了复杂性(Item 55)。
    用@Override注解就可以避免上述情况。

猜你喜欢

转载自blog.csdn.net/Fun_Woody/article/details/83718506
今日推荐