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判断类型又违法里氏替换原则,所以避免使用继承,尝试使用组合;但如果父类是抽象的,不能实例化,则不会出现上述问题。
 //使用组合方式替代继承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;
} 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;

  • 不要求类型转换

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

 将其他集合复制成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方法后一个对象就有了可比较性。

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

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

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

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

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

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

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

  • 缺点:效率比传统的低,每层比较都创建新对象在Effect Java中花了很长篇幅详细介绍了这几个方法,说明其重要性.实际开发中,编辑器、第三方库都能自动生成,但理解原理还是很重要的。

深入理解 Java Object的更多相关文章

  1. 理解JAVA - 面向对象(object) - 属性,方法

    理解JAVA - 面向对象(object) - 属性,方法 多态的体现:    向上造型,父类接收子类对象:向上造型:    从父类角度看不到子类独有的方法:面向对象,人类认知世界的方式:生活中每天都 ...

  2. 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

    作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language- ...

  3. java笔记--理解java类加载器以及ClassLoader类

    类加载器概述: java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制 ...

  4. 【转】深入理解 Java 垃圾回收机制

    深入理解 Java 垃圾回收机制   一.垃圾回收机制的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再 ...

  5. java提高篇(四)-----理解java的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  6. 深入理解java垃圾回收机制

    深入理解java垃圾回收机制---- 一.垃圾回收机制的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再 ...

  7. (6) 深入理解Java Class文件格式(五)

    前情回顾 本专栏的前几篇博文, 对class文件中的常量池进行了详细的解释. 前文讲解了常量池中的7种数据项, 它们分别是: CONSTANT_Utf8_info CONSTANT_NameAndTy ...

  8. (4) 深入理解Java Class文件格式(三)

    转载:http://blog.csdn.net/zhangjg_blog/article/details/21557357 首先, 让我们回顾一下关于class文件格式的之前两篇博客的主要内容. 在  ...

  9. (3) 深入理解Java Class文件格式(二)

    好文转载:http://blog.csdn.net/zhangjg_blog/article/details/21487287 在上一篇文章 深入理解Java Class文件格式(一) 中, 介绍了c ...

随机推荐

  1. matplotlib可视化之如何给图形添加数据标签?

    当我们获取完数据之后,一般来说数据可视化呈现的最基础图形就是:柱状图.水平条形图.折线图等等,在python的matplotlib库中分别可用bar.barh.plot函数来构建它们,再使用xtick ...

  2. 背水一战 Windows 10 (46) - 控件(ScrollViewer 基础): ScrollViewer, ScrollBar, ScrollContentPresenter

    [源码下载] 背水一战 Windows 10 (46) - 控件(ScrollViewer 基础): ScrollViewer, ScrollBar, ScrollContentPresenter 作 ...

  3. Keras学习笔记——Hello Keras

    最近几年,随着AlphaGo的崛起,深度学习开始出现在各个领域,比如无人车.图像识别.物体检测.推荐系统.语音识别.聊天问答等等.因此具备深度学习的知识并能应用实践,已经成为很多开发者包括博主本人的下 ...

  4. Linux巩固记录(2) java项目的编译和执行

    由于要近期使用hadoop等进行相关任务执行,操作linux时候就多了 以前只在linux上配置J2EE项目执行环境,无非配置下jdk,部署tomcat,再通过docker或者jenkins自动部署上 ...

  5. 【UML】:时序图

    时序图表达了类之间调用关系,以及调用时序关系. Actor: 调用者实例化类的对象,执行者. Lifeline: 生命线,竖的虚线.上方方框是类名表示存在的时间,从上至下表示时间流逝.Lifeline ...

  6. 利用django-simple-captcha生成验证码

    参考文档 http://django-simple-captcha.readthedocs.io/en/latest/ django支持1.7+ 1.安装 pip install django-sim ...

  7. QQ gtk,bkn算法

    public long GetGTK(string sKey) { ; , len = sKey.Length; i < len; ++i) { hash += (hash << ) ...

  8. odoo开发笔记 -- 异常信息处理汇总

    1 Traceback (most recent call last): File , in _handle_exception return super(JsonRequest, self)._ha ...

  9. vmworkstation安装unbuntu server 网络配置:NAT模式

    之前安装虚拟机测试环境的时候,习惯了使用桥接模式或者仅主机模式:今天偶然发现,其实NAT 模式的网络配置还是挺方便的. 在新建虚拟机的时候,选择网络模式为NAT,虚拟机创建完成之后,在vmworkst ...

  10. Redhat/CentOS 制作本地yum源

    一.制作本地yum源的场景有: (1) 操作系统ISO文件是通过光驱读取的 (2) 操作系统ISO文件是通过USB设备挂载的 (3) 操作系统ISO文件是被上传到本地文件夹的形式 二. 这3种配置方式 ...