程序员

深入理解 Java Object

2018-12-12  本文已影响0人  风骚无俩

Java中的Object对象为所有对象的直接或间接父对象,里面定义的几个方法容易被忽略却非常重要。以下来自Effective Java 对Object中几个关键方法的应用说明.

public class PhoneNumber implements Cloneable, Comparable<PhoneNumber> {
    private final short linNum, prefix, areaCode;

    public PhoneNumber(int number, int prefix, int areaCode) {
        this.linNum = rangeCheck(number, 999, "linNUm");
        this.prefix = rangeCheck(prefix, 999, "prefix");
        this.areaCode = rangeCheck(areaCode, 999, "areaCode");
    }

    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max) {
            throw new IllegalArgumentException(arg + ":" + val);
        }
        return (short) val;
    }
  }

equals(Object o)

Object中equals方法的实现仅仅是比较了两个对象的地址,对于某些类来说正是所需用的、毋需复写的

什么时候需要对类的equals方法复写?

当一个类表示一个值,如String、Integer;它的不同实例需要逻辑上判断是否相同,而不仅仅是地址是否相同,此时需要复写来自定义相等的条件。由于Map的键和Set的元素都是唯一的,如何判断元素相同是使用此类集合的基础。

equals方法的复写需要满足以下通用约定

如无必要不要复写equals 方法,如果复写了此方法一定要记得复写hashCode方法,因为两个对象相等,它们的hashCode也要相等,下面是equals方法的常用步鄹

@Override
    public boolean equals(Object o) {
        //判断引用是否相等
        if (o == this) {
            return true;
        }
        //判断参数类型是否正确 如果o为null也会返回false

        //这里判断的是class类型,也有可能是接口类型,这样就允许实现这个接口的类之间进行比较

        //AbstractSet,AbstractList,AbstracMap的equals方法这一步都是比较的接口

        if (!(o instanceof PhoneNumber)) {
            return false;
        }
        //类型转换
        // AbstractSet的类型转换  Collection<?> c = (Collection<?>) o;

        PhoneNumber pNum = (PhoneNumber) o;

       // 判断重要字段的相等,如果使用的是接口,调用接口的方法获取字段

       // 对于基本类型 如果不是float或double 直接使用==比较

       // float使用Float.compare(float, float), 原因参考testFloat方法

        //double使用Double.compare(double, double) 同上

        //Float.equals和Double的equals都设计autobox,影响性能

        //引用类型继续调用其equals方法

       // 上述方法也同样适用于数组元素,如果要比较整个数值,使用Arrays.equals对应的方法

        //对象的某些字段能为Null,为了避免NPE,使用Objects.equals(Object, Object)

        return this.linNum == pNum.linNum &&
                this.areaCode == pNum.areaCode &&
                this.prefix == pNum.prefix;
    }

当两个对象存在父子关系,并且子类添加新的值字段,在equals方法中使用instanceOf判断类型时容易破坏对称性或传递性,如Timestamp;使用getClass判断类型又违法里氏替换原则,所以避免使用继承,尝试使用组合;但如果父类是抽象的,不能实例化,则不会出现上述问题。

//使用组合方式替代继承Point
class ColorPoint {
        private final Point point;
        private final Color color;

        public ColorPoint(int x, int y, Color color) {
            point = new Point(x, y);
            this.color = Objects.requireNonNull(color);
        }

        public Point asPoint() {
            return point;
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            //不需要对o判空
            if (!(o instanceof ColorPoint))
                return false;
            ColorPoint cp = (ColorPoint) o;
            return cp.point.equals(point) && cp.color.equals(color);
        }
    }

为什么不使用==比较浮点值,因为有两个例外使比较不一致


private void testFloat() {
        Float f1 = Float.NaN;
        Float f2 = Float.NaN;
        System.out.println(f1.floatValue() == f2.floatValue());//false
        System.out.println(f2.equals(f1));//true

        f1 = 0.0f;
        f2 = -0.0f;
        System.out.println(f1.floatValue() == f2.floatValue());//true
        System.out.println(f2.equals(f1));//false
    }

hashCode()

上文说到如果复写equals方法一定要复写hashCode方法。下面说说hash值的计算

@Override
    public int hashCode() {
        int result = 0;
        result = Short.hashCode(linNum);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(areaCode);
        return result;
    }

那些不可变对象如果hash值计算量大,需要使用缓存防止重复计算影响性能,这里线程不安全

private int hashCode = 0;
    public int hashCode() {
        int result = hashCode;
        if (result == 0) {
            result = Short.hashCode(linNum);
            result = 31 * result + Short.hashCode(prefix);
            result = 31 * result + Short.hashCode(areaCode);
        }
        return result;
    }

toString()

尽量复写toString方法,虽然不及equals和hashCode方法必要,但良好的类描述将能提供充分和友好的信息,AbstractCollection的toString为其子类统一提供集合信息的描述

如果要指定返回值的格式 可做如下说明 这样用户知道如何对其解析 但缺点是如果变更将导致以前的解析方式失败


/**

  * 返回格式化的电话号码"XXX-YYY-ZZZZ"
  * 每个大写字母表示一个数字
  * XXX表示区号,YYY表示前缀,ZZZZ是号码
  * 位数不够的用0填充,如最后一个是123将表示为0123
  */

  @Override
    public String toString() {
        return String.format(Locale.CHINA, "%03d-%03d-%04d", areaCode, prefix, linNum);
    }

clone()

如果一个class 实现了Cloneable接口 那么它应该 提供一个public clone方法

  @Override
    public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            //实现Cloneable接口就不会跑出此异常
            throw new AssertionError();
        }
    }

在实际中要实现对象拷贝,除了数组,并不建议使用clone方法,而建议采用静态工厂或构造器方式提供复制操作

相比clone的优点:

比如某些集合类,以接口为参数的复制构造函数,还能实现转换复制

//将其他集合复制成TreeSet
public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }
//复制转化成TreeMap
public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }

Comparable

compareTo是个很重要的方法,虽然不是Object中的,因为和其他几个方法一样广泛应用,所以放在这里解释,实现Comparabe接口,复写compareTo方法后一个对象就有了可比较性。

@Override
    public int compareTo(PhoneNumber phoneNumber) {
        int result = Short.compare(areaCode, phoneNumber.areaCode);
        if (result == 0) {
            result = Short.compare(prefix, phoneNumber.prefix);
            if (result == 0) {
                result = Short.compare(linNum, phoneNumber.linNum);
            }
        }
        return result;
    }

注意:不要使用< >来比较大小,对浮点有例外,也不要使用减号,会有溢出

建议如上使用基本数据类型包装类的静态比较方法compare

    private void testOverFlow() {
        int p1 = Integer.MAX_VALUE;
        int p2 = -1;
        System.out.printf("p1比p2大:%s", (p1 - p2) > 0);//false
        p1 = Integer.MIN_VALUE;
        p2 = 1;
        System.out.printf("p1比p2大:%s", (p1 - p2) > 0);//true
    }

也可以使用Comparator接口里面的方法,在Java8中,可以如下生成按某种顺序比较的复合比较器。内部实现是从最后一个比较方法进入向前调用的

// 一般用static final 修饰,对象只创建一次
private static final Comparator<PhoneNumber> COMPARATOR =
            comparingInt((ToIntFunction<PhoneNumber>) phoneNumber -> phoneNumber.areaCode)
                    .thenComparingInt(pn -> pn.prefix)
                    .thenComparingInt(pn -> pn.linNum);

@Override
    public int compareTo(PhoneNumber phoneNumber) {
        return  COMPARATOR.compare(this, phoneNumber);
    }

在Effect Java中花了很长篇幅详细介绍了这几个方法,说明其重要性.实际开发中,编辑器、第三方库都能自动生成,但理解原理还是很重要的。

上一篇 下一篇

猜你喜欢

热点阅读