Objects.hash returning different hashCodes for equal objects

Visionary Software Solutions :

Given the following class:

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);
    }  
}

And the following tests:

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));
    }
}

hashCodeIsImplementedCorrectly() is failing in a way I don't expect: two objects that satisfy the equals contract are returning different hashcodes. This seems in direct violation of the JavaDoc:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

I'm just using Objects.hash in the recommended, IDE auto-generated manner...

This method is useful for implementing Object.hashCode() on objects containing multiple fields. For example, if an object that has three fields, x, y, and z, one could write:

@Override public int hashCode() {
    return Objects.hash(x, y, z);
}

Am I missing something obvious? I haven't had this problem before, and written lots of unit tests for equals()/hashCode().

I shudder to think, but in case it's relevant...

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)
Mureinik :

As you noted, if objects A and B are equal (in the sense that A.equals(B) returns true, they should have the same hash code. By extension, if you implement your equals method by checking the equality of a series of fields, using Objects.hash should provide a proper hashcode.

But this isn't what you're doing here - you're using Arrays.equals to compare two arrays - as you should. Arrays with the same contents are not equal, and thus may (and probably will) have different hash codes. Instead, you can use Arrays#hashCode to get value's hash code:

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

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=421720&siteId=1