深入了解 Java Object

作者 : 开心源码 本文共6477个字,预计阅读时间需要17分钟 发布时间: 2022-05-12 共116人阅读

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方法的实现仅仅是比较了两个对象的地址,对于某些类来说正是所需用的、毋需复写的

  • Thread,因为每个线程对象天生就是独一无二的,重点表达是实体而不是值,不需要比较

  • java.util.regex.Pattern,正则表达式的类型也没有比较实例能否相同的必要

  • 父类复写了equals方法,并且是子类所需要的,如AbstractSet,AbstractList,AbstractMap,其子类毋需复写。

  • private或者package private修饰的类,其方法不会被调用

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

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

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

  • 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true,就是自己和自己比较必需相等。

  • 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true,就是x若等于y,那么y也应该等于x。

  • 传递递性:对于任何非空引用值 x、y 和 z,假如 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。

  • 一致性:对于任何非空引用值 x 和 y,屡次调用 x.equals(y) 始终返回 true 或者始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。

  • 非空性:对于任何非空引用值 x,x.equals(null) 都应返回 false。

如无必要不要复写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判断类型又违法里氏替换准则,所以避免使用继承,尝试使用组合;但假如父类是笼统的,不能实例化,则不会出现上述问题。

//使用组合方式替代继承Pointclass 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;            }            if (!(o instanceof ColorPoint))                return false;            ColorPoint cp = (ColorPoint) o;            return cp.point.equals(point) && cp.color.equals(color);        }    }

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

  • 假如 f1 和 f2 都表示 Float.NaN,那么即便 Float.NaN == Float.NaN 的值为 false,equals 方法也将返回 true。

  • 假如 f1 表示 +0.0f,而 f2 表示 -0.0f,那么即便 0.0f==-0.0f 的值为 true,equal 测试也将返回 false。

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值的计算

  • 确保与equals中使用的字段一致

  • 假如字段是基本类型,使用包装类计算hash值如Float.hashCode(f)

  • 假如字段是引用类型,并且在equals方法中递归调用去equals方法,那么这里也递归调用其hashCode方法

  • 假如字段是数组类型,对其中重要元素的hash计算上述方法同样使用,假如要计算整个数组的hash值,使用Arrays.hashCode(array)

  • 质素31的选取是个传统,能尽量让不同对象拥有不同hash值,即分布均匀,

  • Objects.hash(linNum,prefix,areaCode)方法简便,但涉及可变数组的创立和拆装箱操作,性能敏感

  • 此方法返回的值不应该有详细规范,如String的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方法

  • 这是一个毋需构造器就能创立对象的方法

  • 注意:这种方式复制对象容易出错而且复杂,难以维护 仅仅在对基本类型数组的复制是可取的

  • 这个方法是个浅拷贝,也就是字段到字段的复制,假如都是基本类型,那将是一步到位的,

  • 但假如还有引用类型,它们指向的对象不会被拷贝,而仅仅拷贝了引用,这就会导致拷贝后的对象和被拷贝的对象不是相互独立的,这些引用指向了相同的对象,也就是任何一方的修改都在另一方得到表现

  • 假如要深度拷贝,可以每个引用类型都需要实现cloneable接口和clone方法,

或者者使用序列化的方式将对象写到磁盘中,再通过反序列化实现克隆对象,如Apache Commons3工具类,transient修饰的字段不会被序列化。

  • 我们这个类的字段都是基础类型,clone方法比较简单,因为字段都是final,这个一个immutable(不可更改的)类,提供拷贝方法就是多余的,这里仅做演示

  • 肯定要先实现Cloneable接口,虽然里面什么都没有“

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

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

相比clone的优点:

  • 不依赖容易出错的对象创立机制;

  • 不会与final字段的正确使用冲突

  • 不会抛出 checked exceptions;

  • 不要求类型转换

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

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

Comparable

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

  • 假如此方法返回0那么equals应该返回true,假如不是肯定要说明不一致性

  • HashSet依赖equals比较元素能否重复,TreeSet依赖compareTo给元素排序

  • BigDecimal这两个方法就是不一致的,BigDecimal(1.0)and BigDecimal(1.00)equals返回false,因而加入HashSet是不相同的元素
    但compareTo返回0,也就是大小相等,加入TreeSet就只有一个元素

@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中,可以如下生成按某种顺序比较的复合比较器。内部实现是从最后一个比较方法进入向前调用的

  • 优点:在lambda表达式的帮助下逻辑清晰,表达简便

  • 缺点:效率比传统的低,每层比较都创立新对象

// 一般用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中花了很长篇幅详细详情了这几个方法,说明其重要性.实际开发中,编辑器、第三方库都能自动生成,但了解原理还是很重要的。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 深入了解 Java Object

发表回复