【问题标题】:Are hashCodes unique for Strings?hashCodes 对于字符串是唯一的吗?
【发布时间】:2014-06-03 03:19:04
【问题描述】:

最近遇到一段代码,其中Map<Integer, String>被使用,其中Integer(key)是某个字符串的hashCode,而String的值与之对应。

这是正确的做法吗?因为现在,在调用get 时,不会为String 调用equals。 (get 也是在 String 对象上使用 hashCode() 方法完成的。

或者,hashCode(s) 对于唯一字符串是唯一的?

我检查了equals od String 类。有为此写的逻辑。我很困惑。

【问题讨论】:

  • 这是没用的,字符串的哈希值不是唯一的
  • 这样做当然是错误的; equals/hashCode合约规定两个相等的对象必须有相同的hashcode,但不保证两个不相等的对象一定有不同的hashcode。
  • 所以,我是对的。 :)
  • 快速问题@Batty - 有多少不同的String 值?以及hashCodeString 可以取多少个不同的值?

标签: java hashcode


【解决方案1】:

HashMap确实使用equals() 来比较键。它只使用hashCode()查找key所在的bucket,与equals()相比,大大减少了key的数量。

显然,hashCode() 无法产生唯一值,因为 int 被限制为 2^32 个不同的值,并且可能的 String 值有无穷多个。

总之,hashCode() 的结果不太适合 Map 的键。

【讨论】:

  • 一切都是有限的:字符串是字符数组,Java 中数组的最大长度刚好在 Integer.MAX_VALUE 以下,字符是无符号的 2 字节整数。
  • 这个答案并没有真正解决 OP 的问题。代码可能应该使用 Map。一般来说,它不应该对键字符串进行自己的散列。当然,除非该代码有充分的理由。
  • 生成任何字符串的哈希码都是基于特定的哈希函数。由于这些数字大小的限制,根据鸽子洞原理,在散列中肯定会有一定的碰撞。它不能完全消除。
  • @OliCharlesworth: touche.
【解决方案2】:

不,它们不是唯一的,例如“FB”和“Ea”都有 hashCode = 2236

【讨论】:

  • 是的,这是另一个问题,这并不是说您只为任何类型的奇怪字符串创建冲突,hashCode() 定义不明确,甚至可能在实现之间有所不同。不过,我不确定一个示例是否值得单独回答。
【解决方案3】:

这是不正确的,因为hashCode() 会发生冲突。由于hashCode() 的定义不明确,它可能对任何一对字符串都有冲突。所以你不能使用它。如果您需要一个指向字符串的唯一指针,那么您可以使用加密哈希作为密钥:

以下代码显示了如何做到这一点:

/**
 * Immutable class that represents a unique key for a string. This unique key
 * can be used as a key in a hash map, without the likelihood of a collision:
 * generating the same key for a different String. Note that you should first
 * check if keeping a key to a reference of a string is feasible, in that case a
 * {@link Set} may suffice.
 * <P>
 * This class utilizes SHA-512 to generate the keys and uses
 * {@link StandardCharsets#UTF_8} for the encoding of the strings. If a smaller
 * output size than 512 bits (64 bytes) is required then the leftmost bytes of
 * the SHA-512 hash are used. Smaller keys are therefore contained in with
 * larger keys over the same String value.
 * <P>
 * Note that it is not impossible to create collisions for key sizes up to 8-20
 * bytes.
 * 
 * @author owlstead
 */
public final class UniqueKey implements Serializable {

    public static final int MIN_DIGEST_SIZE_BYTES = 8;
    public static final int MAX_DIGEST_SIZE_BYTES = 64;

    /**
     * Creates a unique key for a string with the maximum size of 64 bytes.
     * 
     * @param input
     *            the input, not null
     * @return the generated instance
     */
    public static UniqueKey createUniqueKey(final CharSequence input) {
        return doCreateUniqueKey(input, MAX_DIGEST_SIZE_BYTES);
    }

    /**
     * Creates a unique key for a string with a size of 8 to 64 bytes.
     * 
     * @param input
     *            the input, not null
     * @param outputSizeBytes
     *            the output size
     * @return the generated instance
     */
    public static UniqueKey createUniqueKey(final CharSequence input,
            final int outputSizeBytes) {
        return doCreateUniqueKey(input, outputSizeBytes);
    }

    @Override
    public boolean equals(final Object obj) {
        if (!(obj instanceof UniqueKey)) {
            return false;
        }
        final UniqueKey that = (UniqueKey) obj;
        return ByteBuffer.wrap(this.key).equals(ByteBuffer.wrap(that.key));
    }

    @Override
    public int hashCode() {
        return ByteBuffer.wrap(this.key).hashCode();
    }

    /**
     * Outputs an - in itself - unique String representation of this key.
     * 
     * @return the string <CODE>"{key: [HEX ENCODED KEY]}"</CODE>
     */
    @Override
    public String toString() {
        // non-optimal but readable conversion to hexadecimal
        final StringBuilder sb = new StringBuilder(this.key.length * 2);
        sb.append("{Key: ");
        for (int i = 0; i < this.key.length; i++) {
            sb.append(String.format("%02X", this.key[i]));
        }
        sb.append("}");
        return sb.toString();
    }

    /**
     * Makes it possible to retrieve the underlying key data (e.g. to use a
     * different encoding).
     * 
     * @return the data in a read only ByteBuffer
     */
    public ByteBuffer asReadOnlyByteBuffer() {
        return ByteBuffer.wrap(this.key).asReadOnlyBuffer();
    }

    private static final long serialVersionUID = 1L;

    private static final int BUFFER_SIZE = 512;

    // byte array instead of ByteBuffer to support serialization
    private final byte[] key;

    private static UniqueKey doCreateUniqueKey(final CharSequence input,
            final int outputSizeBytes) {

        // --- setup digest

        final MessageDigest digestAlgorithm;
        try {
            // note: relatively fast on 64 bit systems (faster than SHA-256!)
            digestAlgorithm = MessageDigest.getInstance("SHA-512");
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalStateException(
                    "SHA-256 should always be avialable in a Java RE");
        }

        // --- validate input parameters

        if (outputSizeBytes < MIN_DIGEST_SIZE_BYTES
                || outputSizeBytes > MAX_DIGEST_SIZE_BYTES) {
            throw new IllegalArgumentException(
                    "Unique key size either too small or too big");
        }

        // --- setup loop

        final CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
        final CharBuffer buffer = CharBuffer.wrap(input);
        final ByteBuffer encodedBuffer = ByteBuffer.allocate(BUFFER_SIZE);
        CoderResult coderResult;

        // --- loop over all characters
        // (instead of encoding everything to byte[] at once - peak memory!)

        while (buffer.hasRemaining()) {
            coderResult = encoder.encode(buffer, encodedBuffer, false);
            if (coderResult.isError()) {
                throw new IllegalArgumentException(
                        "Invalid code point in input string");
            }
            encodedBuffer.flip();
            digestAlgorithm.update(encodedBuffer);
            encodedBuffer.clear();
        }

        coderResult = encoder.encode(buffer, encodedBuffer, true);
        if (coderResult.isError()) {
            throw new IllegalArgumentException(
                    "Invalid code point in input string");
        }
        encodedBuffer.flip();
        digestAlgorithm.update(encodedBuffer);
        // no need to clear encodedBuffer if generated locally

        // --- resize result if required

        final byte[] digest = digestAlgorithm.digest();
        final byte[] result;
        if (outputSizeBytes == digest.length) {
            result = digest;
        } else {
            result = Arrays.copyOf(digest, outputSizeBytes);
        }

        // --- and return the final, possibly resized, result

        return new UniqueKey(result);
    }

    private UniqueKey(final byte[] key) {
        this.key = key;
    }
}

【讨论】:

    【解决方案4】:

    hashCode 是为了提高集合的性能。例如,使用包含唯一项目的 Set。添加新项目时,将使用 hashCode 来检测该项目是否已存在于 Set 中。

    如果发生冲突,我们将进行(通常)较慢的 equals 比较。

    因此,hashCode 应该返回一个不同的值对性能很重要。更重要的是,在给定相同的内部状态的情况下,对象的 hashCode 在调用之间是一致的。

    因此,在地图中使用 hashCode 作为键是不明智的!它们不是唯一的。

    【讨论】:

      【解决方案5】:

      通过对键字符串进行自己的哈希处理,该代码可能会冒两个不同的键字符串生成相同整数映射键的风险,并且代码在某些情况下会失败。

      一般来说,代码应该使用Map&lt;String,String&gt;

      但是,作者这样做可能是出于良好(即故意)的原因,而这不是错误。不看代码我们无法判断。

      【讨论】:

      • 这将是一种糟糕的代码气味,但我的眼睛会流泪。如果它上面没有非常具体的评论,它永远不会通过我会做的任何审查。
      猜你喜欢
      • 2012-09-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-31
      • 1970-01-01
      • 1970-01-01
      • 2023-04-03
      • 2014-06-27
      相关资源
      最近更新 更多