如何获取覆盖hashCode()方法的对象的唯一标识符?

281

当一个Java类没有重写hashCode()方法时,打印这个类的实例会得到一个不错而且独特的数字。

Object类的Javadoc对hashCode()进行了说明:

尽量合理地说,由Object类定义的hashCode方法返回的是不同对象的不同整数。

但是当该类重写hashCode()方法后,如何获取它的唯一编号?


43
主要是为了“调试”原因;) 能够说:“啊,同一个对象!” - ivan_ivanovich_ivanoff
7
为此,System.identityHashcode() 可能会有一定的用处。但是,我不会依靠它来实现代码功能。如果您想唯一地识别对象,可以使用AspectJ,并在每个创建的对象中编织一个唯一的ID。虽然需要更多工作。 - Brian Agnew
9
请记住,hashCode不保证唯一。即使实现使用内存地址作为默认的hashCode。为什么它不是唯一的?因为对象被垃圾收集并重用内存。 - Igor Krivokon
8
如果你想确定两个对象是否相同,请使用“==”而不是“hashCode()”。即使在原始实现中,“hashCode()”也无法保证唯一性。 - Mnementh
13
没有一个答案回答了真正的问题,因为它们陷入了讨论 hashCode(),这在这里是次要的。如果我在 Eclipse 中查看引用变量,它会显示一个独特的不可变的“id=xxx”。我们如何通过编程方式获得该值,而不必使用自己的 id 生成器?我想访问那个值以进行调试(记录日志),以识别对象的不同实例。有人知道如何获得那个值吗? - Chris Westin
显示剩余6条评论
11个回答

422

System.identityHashCode(yourObject)会返回yourObject的'原始'哈希码作为整数。并不能保证唯一性。 Sun JVM实现将给出与此对象的原始内存地址相关的值,但这是一个实现细节,您不应依赖它。

编辑:根据Tom下面关于内存地址和移动对象的评论修改了答案。


让我猜一下:当您在同一JVM中拥有超过2 ** 32个对象时,它不是唯一的? ;) 您能指导我一些描述非唯一性的地方吗?谢谢! - ivan_ivanovich_ivanoff
11
无论有多少个对象或内存大小都无所谓。不需要hashCode()或identityHashCode()来产生唯一的数字。 - Alan Moore
13
Brian说:实际上并不是内存位置,当首次计算时,你会得到一个重新散列过的地址版本。在现代虚拟机中,对象将会在内存中移动。 - Tom Hawtin - tackline
3
如果一个对象被创建在内存地址0x2000,然后被虚拟机移动,接着另一个对象被创建在0x2000的位置,它们的System.identityHashCode()会相同吗? - lmat - Reinstate Monica
19
在实际的JVM实现中,并不能保证唯一性...。要保证唯一性,需要要么不对堆进行重定位/压缩,要么需要一种昂贵而庞大的数据结构来管理活动对象的哈希码值。 - Stephen C
Object.hashCode() 如何保证对于同一实例在整个执行生命周期内保持不变,如果该 API 返回的是 '原始' hashCode() 的返回值? - juanmf

32

Object的javadoc指定:

通常这是通过将对象的内部地址转换为整数来实现的,但是JavaTM编程语言不要求使用该实现技术。

如果一个类重写了hashCode,则意味着它想生成一个特定的id,该id将(有望)具有正确的行为。

您可以使用System.identityHashCode 来获取任何类的该id。


12

hashCode()方法不是为对象提供唯一标识符的。它将对象的状态(即成员字段的值)转换为单个整数。这个值通常被一些基于哈希的数据结构,如地图和集合,用于高效地存储和检索对象。

如果您需要对象的标识符,我建议您添加自己的方法,而不是覆盖hashCode。为此,您可以创建一个类似下面的基本接口(或抽象类)。

public interface IdentifiedObject<I> {
    I getId();
}

使用示例:

public class User implements IdentifiedObject<Integer> {
    private Integer studentId;

    public User(Integer studentId) {
        this.studentId = studentId;
    }

    @Override
    public Integer getId() {
        return studentId;
    }
}

8
// looking for that last hex?
org.joda.DateTime@57110da6

如果您在查看对象的.toString()时想了解Java类型的hashcode,则其基础代码如下:

Integer.toHexString(hashCode())

7
也许这个快速的、简单粗暴的解决方案会奏效?
public class A {
    static int UNIQUE_ID = 0;
    int uid = ++UNIQUE_ID;

    public int hashCode() {
        return uid;
    }
}

这也可以给出一个类被初始化的实例数。

6
假设您可以访问该类的源代码。 - pablisco
2
它并不总是有效。该类可以是final的。我认为System.identityHashCode是更好的解决方案。 - pablisco
4
为了保证线程安全,可以像这个答案中所示使用 AtomicLong - Evgeni Sergeev
这不是线程安全的,即如果两个并发运行的线程创建了两个不同的对象,则可能会为这两个对象分配相同的uid。 - Volksman
Object类的Javadoc关于hashCode()方法说:“尽可能地,由Object类定义的hashCode方法确实会为不同的对象返回不同的整数。”再次强调,正如一开始所写的那样,这只是一个快速而简单的解决方案,可能有所帮助。如果你的程序很复杂(例如多线程或使用不同的类加载器),请不要使用此方法。对于简单的程序并需要快速进行调试,这个方法可能会有所帮助(也可能没有)。 - John Pang
显示剩余3条评论

5

我想到了这个解决方案,它适用于我有多个线程创建的可序列化对象的情况:

public abstract class ObjBase implements Serializable
    private static final long serialVersionUID = 1L;
    private static final AtomicLong atomicRefId = new AtomicLong();

    // transient field is not serialized
    private transient long refId;

    // default constructor will be called on base class even during deserialization
    public ObjBase() {
       refId = atomicRefId.incrementAndGet()
    }

    public long getRefId() {
        return refId;
    }
}

4
如果这是一个可以修改的类,您可以声明一个类变量static java.util.concurrent.atomic.AtomicInteger nextInstanceId。(您需要以明显的方式为其赋初始值。)然后声明一个实例变量int instanceId = nextInstanceId.getAndIncrement()

2

我曾经遇到过同样的问题,但之前看到的回答都不能保证唯一标识。

我也需要打印对象ID以用于调试。我知道必须有某种方法可以做到这一点,因为在Eclipse调试器中,每个对象都指定了唯一的标识。

我基于一个事实提出了一个解决方案:对于对象,“==”运算符仅在两个对象确实是同一实例时返回true。

import java.util.HashMap;
import java.util.Map;

/**
 *  Utility for assigning a unique ID to objects and fetching objects given
 *  a specified ID
 */
public class ObjectIDBank {

    /**Singleton instance*/
    private static ObjectIDBank instance;

    /**Counting value to ensure unique incrementing IDs*/
    private long nextId = 1;

    /** Map from ObjectEntry to the objects corresponding ID*/
    private Map<ObjectEntry, Long> ids = new HashMap<ObjectEntry, Long>();

    /** Map from assigned IDs to their corresponding objects */
    private Map<Long, Object> objects = new HashMap<Long, Object>();

    /**Private constructor to ensure it is only instantiated by the singleton pattern*/
    private ObjectIDBank(){}

    /**Fetches the singleton instance of ObjectIDBank */
    public static ObjectIDBank instance() {
        if(instance == null)
            instance = new ObjectIDBank();

        return instance;
    }

    /** Fetches a unique ID for the specified object. If this method is called multiple
     * times with the same object, it is guaranteed to return the same value. It is also guaranteed
     * to never return the same value for different object instances (until we run out of IDs that can
     * be represented by a long of course)
     * @param obj The object instance for which we want to fetch an ID
     * @return Non zero unique ID or 0 if obj == null
     */
    public long getId(Object obj) {

        if(obj == null)
            return 0;

        ObjectEntry objEntry = new ObjectEntry(obj);

        if(!ids.containsKey(objEntry)) {
            ids.put(objEntry, nextId);
            objects.put(nextId++, obj);
        }

        return ids.get(objEntry);
    }

    /**
     * Fetches the object that has been assigned the specified ID, or null if no object is
     * assigned the given id
     * @param id Id of the object
     * @return The corresponding object or null
     */
    public Object getObject(long id) {
        return objects.get(id);
    }


    /**
     * Wrapper around an Object used as the key for the ids map. The wrapper is needed to
     * ensure that the equals method only returns true if the two objects are the same instance
     * and to ensure that the hash code is always the same for the same instance.
     */
    private class ObjectEntry {
        private Object obj;

        /** Instantiates an ObjectEntry wrapper around the specified object*/
        public ObjectEntry(Object obj) {
            this.obj = obj;
        }


        /** Returns true if and only if the objects contained in this wrapper and the other
         * wrapper are the exact same object (same instance, not just equivalent)*/
        @Override
        public boolean equals(Object other) {
            return obj == ((ObjectEntry)other).obj;
        }


        /**
         * Returns the contained object's identityHashCode. Note that identityHashCode values
         * are not guaranteed to be unique from object to object, but the hash code is guaranteed to
         * not change over time for a given instance of an Object.
         */
        @Override
        public int hashCode() {
            return System.identityHashCode(obj);
        }
    }
}

我相信这应该确保程序的整个生命周期中唯一的ID。但需要注意的是,您可能不想在生产应用程序中使用此功能,因为它会保留对生成ID对象的所有引用。这意味着您为其创建ID的任何对象都永远不会被垃圾回收。
由于我将其用于调试目的,所以我并不太关心内存是否被释放。
如果需要释放内存,您可以修改此功能以允许清除对象或删除单个对象。

1

由于Object.hashCode()System.identityHashCode()不能提供保证唯一的ID,因此我认为正确的答案是生成UUID或GUID:

java.util.UUID.randomUUID()

这个答案是线程安全的,并且可以在不同的虚拟机中使用。

例如,Identifiable类可以扩展如下以为任何类提供唯一的ID:

public abstract class Identifiable {
    public final UUID id = UUID.randomUUID();
}

...

public class Example extends Identifiable {}

...

public static void main(String[] args) {

    Example example1 = new Example();
    Example example2 = new Example();

    example1.id.toString(); // e.g. 8308798d-7cec-427d-b7f8-7be762f3b5c7
    example1.id.equals(example1.id); // true
    example1.id.equals(example2.id); // false
}

0

从不同的角度来补充其他答案。

如果您想要重用“上面”的哈希码并使用类的不可变状态派生新的哈希码,则调用super将起作用。虽然这可能/可能不会级联到Object(即某些祖先可能不会调用super),但它将允许您通过重用来派生哈希码。

@Override
public int hashCode() {
    int ancestorHash = super.hashCode();
    // now derive new hash from ancestorHash plus immutable instance vars (id fields)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接