必须了解的Object知识

作为Java中所有类的根类,Object提供了很多基础的方法,我们经常会覆写它的方法,但很多时候因为不了解这些方法内在的含义以及与其他方法之间的关系而错误的覆写。下面介绍一下各个方法,已经如何合理地覆写它们。

Hashcode

hashcode方法为对象定义了计算哈希值的方式。在Java中有这么几条硬性的规定:

  1. 在程序运行期间,如果对象用于equals方法中比较的变量没有改变,那么它的哈希值应该不变。
  2. 如果两个对象的是相等的(调用equals返回为true),那么这两个对象的哈希值应该相同。
  3. 如果两个对象不相等,它们的哈希值可能相等,也可能不等。但大量不同对象的哈希值相等时,会影响该类对象在散列表中的表现。

目的

覆写hashCode方法最大的目的在于使对象能够在用哈希值辅助存储的数据结构中能够正常工作,如HashMap、HashSet和HashTable。

在Java原生的hashCode实现方式下会给对象的每个实例一个不同的哈希值,这样上面的第二点要求是没法满足的,举个具体的例子来说,我们定义一个Man类来表示一个人,有一个唯一的id(类似身份证号)和姓名两个参数。用一个HashMap来表示他的所有的朋友。

public class Man {
private String name;
private int id; public Man(String name,int id) {
this.name=name;
this.id=id;
} public static void main(String args[]){
Map<Man, Man> friends = new HashMap<Man, Man>();
friends.put(new Man("Mike", 1211), new Man("Andy", 1119));
System.out.println(friends.get(new Man("Mike", 1211)));
}
}

执行上面的程序我们可以看到输出的是null。因为Mike虽然是同一个人(有着相同的id),但在上面的语句中却通过new来生成了两个不同的对象,根据Java原生的hashCode方法,会导致存储和查找的位置不同,最终查找失败。

实现方式

那么我们该如何覆写hashCode呢,最简单的方式是直接返回一个常数,这样上面的三条规则就都能满足了。但这样的话所有人包括Mike和Andy的哈希值都会相同,这样他们都会存储到一个散列桶中,散列表直接退化为一个链表或数组,效率大幅下降。典型的做法是把在equals中要用到的域都通过计算集成到哈希值中。一些专家做了一些优化,并形成了一套可行性很高的方案:

  1. 把一个非零常数存于一个int型的result值中
  2. 把对象中的所有域依次计算哈希值h,并通过公式result = 31 * result + h集成到result中
  3. 返回result作为hash值

下面给出例子中的对象的散列实现方式:

@Override
public int hashCode() {
int result = 1;
result = 31 * result + id;
result = 31 * result + (name == null ? 0 : name.hashCode());
return result;
}

注意点

  • 冗余的域(可以通过其他的域计算出来的域)可以不用来计算hashCode
  • 没有在equals中用到的域不要用来计算hashCode
  • result初始值不为0可以保证即使起始的一些域的hashCode都为0时,result仍在不断变化
  • 31可以换成其他的质数,不过31通过位移运算在乘法计算中有一些性能优化
  • 对一些计算成本高的哈希值可以采用延迟计算、缓存等优化方案

Equals

equals方法用来判断两个对象在逻辑上是不是等同的,例如,如果一个人是通过id唯一确定的,那么不管new几个id相同的对象,逻辑上他们都是同一个人。Java中对equals方法的要求是:

  1. 自反性:对自己调用equals必须返回true
  2. 对称性:对于非null的x和y,当且仅当x.equals(y)返回true时,y.equals(x)返回true
  3. 传递性:对于非null的x,y,z,有如下关系x.equals(y),y.equals(z) -> x.equals(z)
  4. 一致性:只要equals比较要用到的域的值不变,equals方法前后返回的值就应该相同
  5. 对于非null的x,x.equals(null)返回false

目的

原Object对equals的实现方式是通过引用是否相同,虽然符合上面的所有的性质。但此时new Man("Mike", 21).equals(new Man("Mike", 21))返回为false,显然不符合我们的要求。

实现方法

为了实现以上的约束,并尽量提高程序的效率,我们可以对equals进行如下的构造:

  1. 判断传入的是否是同一个对象,即判断obj==this,这只是一个性能上的优化
  2. 判断传入的是否是该类型的对象,这可以快速排除对象不同、null等情况
  3. 对该对象的要比较的域依次进行比较

下面是例子中的equals方法的实现,把只要id相等的对象都认为是同一个人:

@Override
public boolean equals(Object obj) {
if(obj==this){
return true;
}
if(!(obj instanceof Man)){
return false;
}
Man other = (Man)obj;
if(this.id==other.id){
return true;
}
else{
return false;
}
}

注意点

  • equals方法传入的是Object参数,很多人会粗心地自己重载一个类型对应的equals方法
  • 对象的域也可能是另一个对象,一般判断该域相等时也应该调用它的equals方法
  • 注意equals和hashCode的关联
  • equals方法一般用来进行简单的值类的比较,复杂的逻辑的比较应自定义方法

Clone

clone方法用来给出对象本身的一个副本。

目的

很多时候我们需要复制一个对象,如当作副本进行缓存,当然可以获取相应的参数通过构造函数来实现对象的复制,但这会使得调用方的方法变得繁杂。Object提供了clone方法来帮助对象创建副本。

实现方式

想要你的类能够调用clone方法,该类必须要实现Cloneable接口,这是一个空的接口,仅仅来声明实现了这个接口的类是可以clone的。如果不实现该接口,会抛出CloneNotSupportedException异常。需要注意的是,Object中的clone方法是protected的,且返回的值是Object类型。如果我们要覆写clone方法,应把该方法改为public,且返回类自身的对象。

@Override
public Man clone() throws CloneNotSupportedException {
return (Man) super.clone();
}

在例子中只是调用了super方法并进行了一下转型,这是因为Man中只有基本类型的域。但如果类中有可变的引用域时,简单的调用super.clone()就不行了。下面为Man添加一个妻子的属性,妻子是一个独立的对象。具体代码如下:

public class Woman implements Cloneable {
private String name; public Woman(String name) {
this.name = name;
} @Override
public Woman clone() throws CloneNotSupportedException {
return (Woman) super.clone();
} @Override
public String toString() {
return String.format("[name:%s]", this.name);
} public void rename(String newName) {
this.name = newName;
}
} public class Man implements Cloneable{
private String name;
private int id;
private Woman wife; public Man(String name, int id, Woman wife) {
this.name = name;
this.id = id;
this.wife = wife;
} @Override
public Man clone() throws CloneNotSupportedException {
return (Man) super.clone();
} @Override
public String toString() {
return String.format("[name:%s, id:%d]", this.name,this.id);
} public static void main(String args[]) {
Man mike = new Man("Mike", 1211,new Woman("Anny"));
try {
Man mike2 = (Man) mike.clone();
mike.wife.rename("Lily");
System.out.println(mike2.wife);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} }
}

这时的输出是[name:Lily],可以看出Mike的妻子改了个名字,结果保存的副本也改变了。那要怎么做才能使副本的值不变呢?我们需要对可变的引用也进行复制。

@Override
public Man clone() throws CloneNotSupportedException {
Man man = (Man) super.clone();
man.wife = (Woman) this.wife.clone();
return man;
}

注意点

  • 继承的子类调用父类的clone方法时要确保父类的clone方法是正常工作的
  • 对可变引用进行clone时要保证引用被正确clone,因为引用自身也可能有可变的引用域

toString

toString方法用来将对象的信息转化成一个字符串。

目的

Object中对toString实现的方式是类名+@+十六进制的对象编号,例如drfish.Man@24da72,显然这种形式的输出阅读起来不方便,让人无法了解对象的真正情况。Java建议所有的类都覆写toString方法,返回对象的关键信息,方便查看和诊断。

实现方式

toString的实现比较简单,只要自己构造一个包含想要展示的信息的字符串并返回。

@Override
public String toString() {
return String.format("[name:%s, id:%d]", this.name,this.id);
}

输出结果如下:[name:Mike, id:1211]

注意点

  • 字符串连接性能的考虑
  • 在文档注释中对你的格式进行说明
  • 不要随意更改格式

总结

这篇文章主要介绍了Java中的基础类Object中的一些可覆写的方法,以及在覆写它们时需要注意的事项。

必须了解的Object知识的更多相关文章

  1. AJAX--XMLHttpRequest Object 知识整理

    1.创建XMLHttpRequest对象 variable = new XMLHttpRequest() variable = new ActiveXObject('Microsoft.XMLHTTP ...

  2. JavaScript对象 Object类型基础

    前言 JavaScript 对象是整个语言学习的一个难点.本文主要带大家入门学习Object知识 对象定义 javascript的基本数据类型包括undefined.null.boolean.stri ...

  3. [知识图谱]利用py2neo从Neo4j数据库获取数据

    # -*- coding: utf-8 -*- from py2neo import Graph import json import re class Neo4jToJson(object): &q ...

  4. 第二十九节:Java基础知识-类,多态,Object,数组和字符串

    前言 Java基础知识-类,多态,Object,数组和字符串,回顾,继承,类的多态性,多态,向上转型和向下转型,Object,数组,多维数组,字符串,字符串比较. 回顾 类的定义格式: [类的修饰符] ...

  5. Object&nbsp;c&nbsp;基础知识

    文件类型说明:.h 头文件,用于定义类.实例变量及类中的方法等定义信息(interface). .m 源文件,定义方法体,可实现objce-c和c方法(implementation). .mm c++ ...

  6. Httpd服务入门知识-Httpd服务常见配置案例之DSO( Dynamic Shared Object)加载动态模块配置

    Httpd服务入门知识-Httpd服务常见配置案例之DSO( Dynamic Shared Object)加载动态模块配置 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.加载动 ...

  7. javaSE之Object及hashcode等相关知识

    object: package javaBasic; public class TestObject { public static void main(String[] args) { // TOD ...

  8. Object c 基础知识

    文件类型说明:.h 头文件,用于定义类.实例变量及类中的方法等定义信息(interface)..m 源文件,定义方法体,可实现objce-c和c方法(implementation)..mm c++源文 ...

  9. Java基础知识强化27:Object类之toString()方法

    1. Object类的toString()方法: public  String  toString():返回该对象的字符串表示 2. 案例演示: (1)Student类: package cn.itc ...

随机推荐

  1. git 的简单实用

    一. 安装 Git(git_for_windows.xp510.com.rar) 二. 使用 a) 进入到 git bash(命令行工具) b) 初始化user.name,user.email $ g ...

  2. Swagger如何测试Date类型参数

    问题 Swagger测试时,参数直接输入日期格式化后的类型,会报参数日期转换错误 :ConversionFailedException 解决 网上说在参数上添加注解 @DateTimeFormat(p ...

  3. Lua语法基础(一)

    1. 注释 -- 单行注释 --[[ 多行注释 --]] 2. 运行方式     (1)交互式运行         命令行下 lua进入交互模式     (2)命令行运行         lua + ...

  4. Oracle 查看链接数、创建索引等的DDL语句

    select count(*),machine from v$session group by machine 今天打算将一个数据库的索引在另一个测试库上重新创建一遍,研究了一下. set pages ...

  5. FreeNAS插件打造ownCloud私有云网盘

    ownCloud 是一个自由开源的个人云存储解决方案,可以自由获取无需付费,但用户需要自行架设服务器,好在FreeNAS可以通过插件轻松的构建ownCloud服务器. ownCloud 分为服务器端和 ...

  6. ASP.NET概念

    ASP.NET :是一个开发框架,用于通过 HTML.CSS.JavaScript 以及服务器脚本来构建网页和网站. ASP.NET两种开发语言:VB C#

  7. Memcached操作

    标准协议和字段 Memcached的标准协议字段包含以下部分: 键,key,任意字符,最大250字节,不能有空格和换行 标志位,32比特,不能为0 超时时间,单位是秒,0代表永不超时,最长30天,30 ...

  8. Linux设备树(五 根节点)

    五 根节点 一个最简单的设备树必须包含根节点,cpus节点,memory节点.根节点的名字及全路径都是“/”,至少需要包含model和compatible两个属性.model属性我们在属性那节已经说过 ...

  9. Qt: 非阻塞时间延迟;

    1.使用时间耗损循环: #include <QTime> ... QTime delayTime = QTime::currentTime().addMSecs(1000); while( ...

  10. Linux性能工具图册-便于查阅

    该图表示了,Linux系统哪种问题用哪种工具