资讯专栏INFORMATION COLUMN

Java进阶2 —— 使用Object的通用方法

jzman / 3267人阅读

摘要:判断另外一个对象是否与当前对象相等返回当前对象的哈希值返回一个表示当前对象的字符串唤醒一个等待当前对象的锁监视器的线程。

原文链接:http://www.javacodegeeks.com/2015/09/using-methods-common-to-all-objects.html

本文是Java进阶课程的第二篇。

本课程的目标是帮你更有效的使用Java。其中讨论了一些高级主题,包括对象的创建、并发、序列化、反射以及其他高级特性。本课程将为你的精通Java的旅程提供帮助。

内容提纲

引言

equals和hashCode方法

toString方法

clone方法

equals方法与"=="操作符

有用的帮助类

源码下载

下章概要

1. 引言

从前面一篇对象的创建与销毁中,我们知道Java是一种面向对象编程语言(尽管不是纯粹的面向对象)。Java类层次结构的顶层是Object类,所有的其他类都隐式的继承于它。因此,所有的类也都从Object中继承了方法,其中最重要的几个方法如下表:

方法 描述
protected Object clone() 创建并返回当前对象的一份拷贝
protected void finalize() 当垃圾回收器判断出该对象不再被引用时,就会调用finalize()方法。在对象的创建与销毁中有对finalizers的介绍。
boolean equals(Object obj) 判断另外一个对象是否与当前对象相等
int hasCode() 返回当前对象的哈希值
String toString() 返回一个表示当前对象的字符串
void notify() 唤醒一个等待当前对象的锁监视器的线程。我们将会在第9篇文章并发最佳实践中详细介绍此方法
void notifyAll() 唤醒所有等待当前对象的锁监视器的线程。我们将会在第9篇文章并发最佳实践中详细介绍此方法
void wait()
void wait(long timeout)
void wait(long timeout, int nanos)
使当前线程进入等待状态直到其他线程调用了当前对象的notify()notifyAll()方法。我们将会在第9篇文章并发最佳实践中详细介绍此方法

表1

在本篇文章中我们将重点介绍equalshashCodetoStringclone方法。通过本章节的学习,需要对这几个方法的用法及重要的使用限制了然于胸。

2. equlas和hashCode方法

默认情况下,Java 中任何两个对象引用(或类实例引用)只有指向相同的内存地址时才认为是相等的(引用相等)。但是Java允许通过重载Objectequals()方法给类自定义判等规则。听起来这是个很强大的概念,然而在适当的equals()方法实现需要满足以下几个规则限制:

自反性:对象x必须与其自身相等,equals(x)返回true

对称性:如果equals(y)true,则y.equals(x)也要返回true

传递性:如果equals(y)true,并且y.equals(z)也为true,则x.equals(z)也要为true

一致性:多次调用equals()方法应该返回相同值,除非对用于判等的任何一个属性进行了修改

与null判等equals(null)总是要返回false

不幸的是Java编译器并不会在编译时对以上规则进行检查。然而,不遵守上述规则时可能会引入非常怪异并难以解决的问题。通用的建议是:如果需要重写equals()方法,请至少思考两次重写的必要性。遵循以上规则,我们为Person类重写一个简单的equals()实现。

package com.javacodegeeks.advanced.objects;

public class Person {
    private final String firstName;
    private final String lastName;
    private final String email;
    
    public Person( final String firstName, final String lastName, final String email ) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
    
    public String getEmail() {
        return email;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public String getLastName() {
        return lastName;
    }

    // Step 0: Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public boolean equals( Object obj ) {
        // Step 1: Check if the "obj" is null
        if ( obj == null ) {
            return false;
        }
        
        // Step 2: Check if the "obj" is pointing to the this instance
        if ( this == obj ) {
            return true;
        }
        
        // Step 3: Check classes equality. Note of caution here: please do not use the 
        // "instanceof" operator unless class is declared as final. It may cause 
        // an issues within class hierarchies.
        if ( getClass() != obj.getClass() ) {
            return false;
        }
        
        // Step 4: Check individual fields equality
        final Person other = (Person) obj;
        if ( email == null ) {
            if ( other.email != null ) {
                return false;
            } 
        } else if( !email.equals( other.email ) ) {
            return false;
        }
        
        if ( firstName == null ) {
            if ( other.firstName != null ) {
                return false;
            } 
        } else if ( !firstName.equals( other.firstName ) ) {
            return false;
        }
            
        if ( lastName == null ) {
            if ( other.lastName != null ) {
                return false;
            }
        } else if ( !lastName.equals( other.lastName ) ) {
            return false;
        }
        
        return true;
    }        
}

在此部分介绍hashCode()方法并不是偶然的,至少要记住下面这条规则:任何时候重载equals()方法时,需要一并重载hashCode()方法。如果两个对象通过equals()方法判等时返回true,则每个对象的hashCode()方法需要返回相同的整数值(反过来并没有限制:如果两个对象通过equals()方法返回false,则hashCode()方法可以返回相同或不同的整数值)。下面看一下Person类的hashCode()方法:

// Please add the @Override annotation, it will ensure that your
// intention is to change the default implementation.
@Override
public int hashCode() {
    final int prime = 31;
        
    int result = 1;
    result = prime * result + ( ( email == null ) ? 0 : email.hashCode() );
    result = prime * result + ( ( firstName == null ) ? 0 : firstName.hashCode() );
    result = prime * result + ( ( lastName == null ) ? 0 : lastName.hashCode() );
        
    return result;
}      

为了避免得到不可预期的结果,尽可能在实现equals()hashCode()方法时使用final字段,从而保证方法的结果不会受到字段变化的影响(尽管真实场景中未必发生)。

最后,要确保在实现equals()hashCode()方法是使用相同的字段,以确保在不可预期的字段调整时保证这两个方法行为的一致性。

3. toString方法

toString()是最让人感兴趣的方法,并且被重载的频率也更高。此方法的目的是提供对象(类实例)的字符串表现。如果对toString()方法重载恰当,能极大的简化debug难度和分析解决问题的过程。

默认情况下,toString()的结果仅仅返回以@符分隔的全类名与对象哈希值串,然而这个结果在大多场景下并没什么用途。如下:

com.javacodegeeks.advanced.objects.Person@6104e2ee

我们来通过重写PersontoString()方法以使其输出更有用,下面是其中一种实例:

// Please add the @Override annotation, it will ensure that your
// intention is to change the default implementation.
@Override
public String toString() {
    return String.format( "%s[email=%s, first name=%s, last name=%s]", 
        getClass().getSimpleName(), email, firstName, lastName );
}

现在我们在toString()方法中包含了Person的所有字段,然后执行下面的代码片段:

final Person person = new Person( "John", "Smith", "john.smith@domain.com" );
System.out.println( person.toString() );

控制台中将输出以下结果:

Person[email=john.smith@domain.com, first name=John, last name=Smith]

遗憾的是在Java标准库中对toString()方法实现的支持有限,不过还是有几个有用的方法:Objects.toString(), Arrays.toString() / Arrays.deepToString()。下面看一下Office类以及其toString()的实现。

package com.javacodegeeks.advanced.objects;

import java.util.Arrays;

public class Office {
    private Person[] persons;

    public Office( Person ... persons ) {
         this.persons = Arrays.copyOf( persons, persons.length );
    }
    
    @Override
    public String toString() {
        return String.format( "%s{persons=%s}", 
            getClass().getSimpleName(), Arrays.toString( persons ) );
    }
    
    public Person[] getPersons() {
        return persons;
    }
}

相应的控制台输出如下(同时也有Person实例的字符串值):

Office{persons=[Person[email=john.smith@domain.com, first name=John, last name=Smith]]}

Java社区实例了大量有用的类库以简化toString()的实现。其中广泛使用的有Google Guava的Objects.toStringHelper和Apache Commons Lang的ToStringBuilder

4. clone方法

如果举出Java中最声名狼藉的方法,当属clone()无疑。clone()方法的目的很简单——返回对象实例的拷贝,然而有一堆理由可证明其使用并不像听起来那么轻而易举。

首先,实现自定义的clone()方法时需要遵守Java文档)中列出的一系列约定。其次,在Object类中clone()方法被声明为protected,所以为了提高方法的可见性,在重载时需要声明为public并把返回值类型调整为重载类自身类型。再次,重载类需要实现Cloneable接口(尽管该接口作为一种声明,并未提供任何方法定义),否则将会抛出CloneNotSupportedException异常。最后,在实现clone()方法时要先调用super.clone()然后再执行其他需要的动作。下面看一下Person类中的实现:

public class Person implements Cloneable {
    // Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public Person clone() throws CloneNotSupportedException {
        return ( Person )super.clone();
    }
}

上面的实现看起来简单直接,然而却隐藏着错误。当类实例的clone动作被执行时,未调用任何构造方法,后果将导致预料外的数据泄露。下面再看下Office类中的定义:

package com.javacodegeeks.advanced.objects;

import java.util.Arrays;

public class Office implements Cloneable {
    private Person[] persons;

    public Office( Person ... persons ) {
         this.persons = Arrays.copyOf( persons, persons.length );
    }

    @Override
    public Office clone() throws CloneNotSupportedException {
        return ( Office )super.clone();
    }
    
    public Person[] getPersons() {
        return persons;
    }
}

在这个实现中,Office实例克隆出来的所有对象都将共享相同的person数组,然而这并不是我们预期的行为。为了让clone()实现正确的行为,我们还要做一些额外的工作:

@Override
public Office clone() throws CloneNotSupportedException {
    final Office clone = ( Office )super.clone();
    clone.persons = persons.clone();
    return clone;
}

看起来是正确了,但如果对persons字段声明为final就将破坏这种正确性,因此final字段不能被重新赋值,从而导致数据再次被共享。

总之,当需要类实例的拷贝时,尽可能避免使用clone() / Cloneable,相反可以选择其他更简单的替代方案(例如:C++程序员熟悉的复制构造方法,或者工厂方法——在对象的创建与销毁中讨论过的一种有用的构造模式)。

5. equals方法与"=="操作符

在Java中,==操作符与equals()方法有种奇怪的关系,却会引入大量的问题与困惑。大多数情况下(除比较基本数据类型),==操作符执行的是引用相等:只要两个引用指向同一个对象时为true,否则返回false。下面举例说明二者的区别:

final String str1 = new String( "bbb" );
System.out.println( "Using == operator: " + ( str1 == "bbb" ) );
System.out.println( "Using equals() method: " + str1.equals( "bbb" ) );

从我们人类的视角来看,str1 == "bbb" 和 str1.equals("bbb")并无区别:str1仅仅是"bbb"的一个引用,所以结果应该是相同的;但对于Java来说却不尽然:

Using == operator: false
Using equals() method: true

尽管两个字符串看起来完全一样,但事实上却是两个不同的String实例。作为建议,在处理对象引用时要使用equals()Objects.equals()进行判等,除非你真的是要判断两个引用是否指向同一个实例。

6. 有用的帮助类

从Java 7发布以来,一批有用的帮助类加入到了标准Java库中,Objects便是其中之一。具体来说,以下三个方法可以简化你的equals()hashCode()方法实现。

方法 描述
static boolean equals(Object a, Object b) 当参数中的两个对象相等时返回true,否则返回false
static int hash(Object...values) 为参数列表生成哈希值
static int hashCode(Object o) 为非null参数生成哈希值,如果参数为null返回0

如果使用上面的方法来重写Personequals()hashCode()实现,代码量将会大大缩减,同时代码的可读性也将大大增强。

@Override
public boolean equals( Object obj ) {
    if ( obj == null ) {
        return false;
    }
        
    if ( this == obj ) {
        return true;
    }
        
    if ( getClass() != obj.getClass() ) {
        return false;
    }
        
    final PersonObjects other = (PersonObjects) obj;
    if( !Objects.equals( email, other.email ) ) {
        return false;
    } else if( !Objects.equals( firstName, other.firstName ) ) {
        return false;            
    } else if( !Objects.equals( lastName, other.lastName ) ) {
        return false;            
    }
        
    return true;
}
        
@Override
public int hashCode() {
    return Objects.hash( email, firstName, lastName );
}      

7. 源码下载

可以从这里下载本文中的源码:advanced-java-part-2

8. 下章概要

在本章中,我们学习了作为Java面向对象基础的Object类,以及自定义的类如何通过自己的判等规则重载Object的相关方法。下一章中,我们将会把视线暂时从代码实现上收起,转向去讨论如何设计合适的类和接口。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65466.html

相关文章

  • PHP 进阶之路 - 后端多元化之快速切入 Java 开发

    摘要:以实现自己熟悉的东西为导向比如我们做后端开发,首先是常用的循环迭代条件判断增删改成。它是由实现的,不保证元素的顺序,也就是说所说元素插入的顺序与输出的顺序不一致。 下面是我直播的文字版,直播地址:https://segmentfault.com/l/15...代码:https://github.com/zhoumengka...整个项目我们我又细分了6个版本来演进,希望更加便于大家对比...

    Cristic 评论0 收藏0
  • PHP 进阶之路 - 后端多元化之快速切入 Java 开发

    摘要:以实现自己熟悉的东西为导向比如我们做后端开发,首先是常用的循环迭代条件判断增删改成。它是由实现的,不保证元素的顺序,也就是说所说元素插入的顺序与输出的顺序不一致。 下面是我直播的文字版,直播地址:https://segmentfault.com/l/15...代码:https://github.com/zhoumengka...整个项目我们我又细分了6个版本来演进,希望更加便于大家对比...

    xi4oh4o 评论0 收藏0
  • Java进阶1 —— 对象创建与销毁

    摘要:构造方法是在对象实例初始化过程中具有举足轻重的地位,并且提供了多种方式来定义构造方法。在中创建对象的开销是相当低的,并且速度很快。对象终结器前面我们讲述的都是构造方法和对象初始化相关的主题,但还未提及他们的反面对象销毁。 原文链接:http://www.javacodegeeks.com/2015/09/how-to-create-and-destroy-objects.html 本文...

    nemo 评论0 收藏0
  • Java进阶3 —— 类和接口设计原则

    摘要:首当其冲的便是接口中的每个声明必须是即便不指定也是,并且不能设置为非,详细规则可参考可见性部分介绍。函数式接口有着不同的场景,并被认为是对编程语言的一种强大的扩展。抽象类与中的接口有些类似,与中支持默认方法的接口更为相像。 原文链接:http://www.javacodegeeks.com/2015/09/how-to-design-classes-and-interfaces.htm...

    lauren_liuling 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<