Search code examples
javahashcodecloning

Why does changing a property of a reference variable in an object changes that object's memory address?


We have the Person class, with 3 variables, "firstName", "lastName" (both String) and "Address" address. Address class has 2 properties, "cityName" and "countryName" (both String).

Having instantiated a new Person object and given values to each property, when one changed the value "cityName" of the "address" property of the Person object, this change did not only change the hashcode of the address property, but it did for the person object as well.

Shouldn't the only change be the hashcode of the "cityName" property?

Here is the code:

@Setter
@AllArgsConstructor
@EqualsAndHashCode
public class Person implements Cloneable
{
    private String firstName;
    private String lastName;
    private Address address;

    public Person(Person personToBeCloned) {
        Address clonedAddress = personToBeCloned.getAddress();

        this.firstName = personToBeCloned.getFirstName();
        this.lastName = personToBeCloned.getLastName();
        this.address = new Address(clonedAddress.getStreetName(), clonedAddress.getCityName());
    }
    
    @Override
    public Person clone()  {
        try {
            return (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return "Person [firstName=" + firstName + ", lastName=" + lastName + ", address=" + address + "] " +
                this.getClass().getSimpleName() + "@" + this.hashCode();
    }
}
@Getter
@Setter
@AllArgsConstructor
@EqualsAndHashCode
public class Address implements Cloneable
{
    private String streetName;
    private String cityName;

    @Override
    public Address clone() {
        try
        {
            return (Address) super.clone();
        } catch (CloneNotSupportedException cloneException)
        {
            throw new RuntimeException(cloneException);
        }
    }

    @Override
    public String toString() {
        return "Address [streetName=" + streetName + ", cityName=" + cityName + "] "
            + this.getClass().getSimpleName() + "@" + this.hashCode();
    }
}

Here is the test being run and in the console, one can see that Person and Address change their in-memory address.

    @Test
    public void shallowCopy()
    {
        Person shallowCopyOfAlex = alex.clone();

        System.out.println("Details before change:");
        System.out.println(alex);
        System.out.println(shallowCopyOfAlex);

        assertEquals(alex, shallowCopyOfAlex);

        alex.getAddress().setCityName("Unknown City");

        System.out.println("\nDetails after change:");
        System.out.println(alex);
        System.out.println(shallowCopyOfAlex);

        assertEquals(alex.getAddress(), shallowCopyOfAlex.getAddress());
    }
Details before change:
Person [firstName=Alex, lastName=Jones, address=Address [streetName=Main Street, cityName=Main City] Address@-1825014247] Person@932142263
Person [firstName=Alex, lastName=Jones, address=Address [streetName=Main Street, cityName=Main City] Address@-1825014247] Person@932142263

Details after change:
Person [firstName=Alex, lastName=Jones, address=Address [streetName=Main Street, cityName=Unknown City] Address@-437944728] Person@-1975755514
Person [firstName=Alex, lastName=Jones, address=Address [streetName=Main Street, cityName=Unknown City] Address@-437944728] Person@-1975755514

Solution

  • Simple: no, it's shouldn't, because the hashCode function (create via lombok) consider all your fields, when a field value change, hashCode change accordingly.

    If you de-lombok your pojos, you'll see that hashCode method simply consider the hashCodes of all your fields; so, for any field that changes, doesn't matter at what level, hashCode change too

    package org.example;
    
    import lombok.Getter;
    import lombok.Setter;
    
    public class HashCodeExplanation {
        @Getter
        @Setter
        public static class Person {
            private String firstName;
            private String lastName;
            private Address address;
    
            public boolean equals(final Object o) {
                ...
            }
    
            public int hashCode() {
                final int PRIME = 59;
                int result = 1;
                final Object $firstName = this.getFirstName();
                result = result * PRIME + ($firstName == null ? 43 : $firstName.hashCode());
                final Object $lastName = this.getLastName();
                result = result * PRIME + ($lastName == null ? 43 : $lastName.hashCode());
                final Object $address = this.getAddress();
                result = result * PRIME + ($address == null ? 43 : $address.hashCode());
                return result;
            }
        }
    
        @Getter
        @Setter
        public static class Address {
            private String cityName;
            private String countryName;
    
            public boolean equals(final Object o) {
                ...
            }
    
            public int hashCode() {
                final int PRIME = 59;
                int result = 1;
                final Object $cityName = this.getCityName();
                result = result * PRIME + ($cityName == null ? 43 : $cityName.hashCode());
                final Object $countryName = this.getCountryName();
                result = result * PRIME + ($countryName == null ? 43 : $countryName.hashCode());
                return result;
            }
        }
    }