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 -------------------^
}