Java Objects.hash为相同的对象返回不同的哈希代码

Java Objects.hash为相同的对象返回不同的哈希代码,java,Java,鉴于以下类别: package software.visionary.identifr; import software.visionary.identifr.api.Authenticatable; import software.visionary.identifr.api.Credentials; import javax.crypto.*; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEParamet

鉴于以下类别:

package software.visionary.identifr;

import software.visionary.identifr.api.Authenticatable;
import software.visionary.identifr.api.Credentials;

import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Objects;

public final class PasswordCredentials implements Credentials {
    private final Authenticatable owner;
    private final byte[] value;
    private final SecretKey key;

    public PasswordCredentials(final Authenticatable human, final String password) {
        if (Objects.requireNonNull(password).trim().isEmpty()) {
            throw new IllegalArgumentException("Invalid password");
        }
        this.owner = Objects.requireNonNull(human);
        this.key = asSecretKey(password);
        this.value = this.key.getEncoded();
    }

    private SecretKey asSecretKey(final String password) {
        try {
            final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
            final SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndTripleDES");
            return secretKeyFactory.generateSecret(pbeKeySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final PasswordCredentials that = (PasswordCredentials) o;
        return owner.equals(that.owner) &&
                Arrays.equals(value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(owner, value);
    }  
}
以及以下测试:

package software.visionary.identifr;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import software.visionary.Randomizr;
import software.visionary.identifr.api.Authenticatable;
import software.visionary.identifr.api.Credentials;

import java.util.UUID;

final class PasswordCredentialsTest {
    @Test
    void rejectsNullOwner() {
        final Authenticatable owner = null;
        final String password = Randomizr.INSTANCE.createRandomPassword();
        Assertions.assertThrows(NullPointerException.class, () -> new PasswordCredentials(owner, password));
    }

    @Test
    void rejectsNullPassword() {
        final Authenticatable owner = new Authenticatable() {
            @Override
            public Credentials getCredentials() {
                return null;
            }

            @Override
            public UUID getID() {
                return null;
            }
        };
        final String password = null;
        Assertions.assertThrows(NullPointerException.class, () -> new PasswordCredentials(owner, password));
    }

    @Test
    void rejectsEmptyPassword() {
        final Authenticatable owner = new Authenticatable() {
            @Override
            public Credentials getCredentials() {
                return null;
            }

            @Override
            public UUID getID() {
                return null;
            }
        };
        final String password = "";
        Assertions.assertThrows(IllegalArgumentException.class, () -> new PasswordCredentials(owner, password));
    }

    @Test
    void rejectsWhitespacePassword() {
        final Authenticatable owner = new Authenticatable() {
            @Override
            public Credentials getCredentials() {
                return null;
            }

            @Override
            public UUID getID() {
                return null;
            }
        };
        final String password = "\t\t\n\n\n";
        Assertions.assertThrows(IllegalArgumentException.class, () -> new PasswordCredentials(owner, password));
    }

    @Test
    void hashCodeIsImplementedCorrectly() {
        final Authenticatable owner = Fixtures.randomAuthenticatable();
        final String password = Randomizr.INSTANCE.createRandomPassword();
        final PasswordCredentials creds = new PasswordCredentials(owner, password);
        final int firstHash = creds.hashCode();
        final int secondHash = creds.hashCode();
        Assertions.assertEquals(firstHash, secondHash);
        final PasswordCredentials same = new PasswordCredentials(owner, password);
        Assertions.assertEquals(creds.hashCode(), same.hashCode());
        final PasswordCredentials different = new PasswordCredentials(owner, Randomizr.INSTANCE.createRandomPassword());
        Assertions.assertNotEquals(firstHash, different.hashCode());
    }

    @Test
    void equalsIsImplementedCorrectly() {
        final Authenticatable owner = Fixtures.randomAuthenticatable();
        final String password = Randomizr.INSTANCE.createRandomPassword();
        final PasswordCredentials creds = new PasswordCredentials(owner, password);
        Assertions.assertTrue(creds.equals(creds));
        final PasswordCredentials same = new PasswordCredentials(owner, password);
        Assertions.assertTrue(creds.equals(same));
        Assertions.assertTrue(same.equals(creds));
        final PasswordCredentials different = new PasswordCredentials(owner, Randomizr.INSTANCE.createRandomPassword());
        Assertions.assertFalse(creds.equals(different));
        Assertions.assertFalse(different.equals(creds));
    }
}
hashCodeIsImplementedRightly()
以一种我无法预料的方式失败:满足
等于
契约的两个对象返回不同的hashcodes。

如果根据equals(Object)方法两个对象相等,那么对两个对象中的每一个调用hashCode方法必须产生相同的整数结果

我只是在用

此方法对于在包含多个字段的对象上实现Object.hashCode()非常有用。例如,如果对象具有三个字段x、y和z,则可以写入:

@Override public int hashCode() {
    return Objects.hash(x, y, z);
}
我错过了什么明显的东西吗?我以前没有遇到过这个问题,并且为equals()/hashCode()编写了很多单元测试

我不寒而栗地想,但如果这是相关的

java --version
openjdk 11.0.5 2019-10-15
OpenJDK Runtime Environment (build 11.0.5+10-post-Ubuntu-0ubuntu1.119.04)
OpenJDK 64-Bit Server VM (build 11.0.5+10-post-Ubuntu-0ubuntu1.119.04, mixed mode, sharing)

仔细观察生成的
equals
方法,就会发现:这是因为
value
是一个
字节[]
。将数组用作字段时,
对象.hash
需要使用
数组.hashCode(值)

这是正确的:

@Override
    public int hashCode() {
        return Objects.hash(owner, Arrays.hashCode(value));
    }

如您所述,如果对象
A
B
相等(在
A.equals(B)的意义上)
返回
true
,它们应该具有相同的哈希代码。进一步说,如果您通过使用
对象检查一系列字段的相等性来实现
equals
方法,则哈希
应该提供正确的哈希代码

但这不是您在这里要做的-您正在使用
数组。equals
来比较两个数组-这是您应该做的。具有相同内容的数组不相等,因此可能(也可能会)具有不同的哈希代码。相反,您可以使用来获取
值的哈希代码:

@Override
public int hashCode() {
    return Objects.hash(owner, Arrays.hashCode(value));
    // Here -------------------^
}