I'm not sure what the author means when he writes that a singleton static factory method can guarantee that no two equal instances exist. Well actually I do kind of understand that but I'm confused by the following text when he demonstrates the equals method vs the literal comparison operator: "a.equals(b) if and only if a==b."
I understand that the equals() method actually compares the contents of an object while the literal == compares to see if they are the same object in memory. This is confusing because he goes on to say that the client can use the == instead of the .equals(object) method. How so? Why would the client use the == comparator if they're guaranteed to only one object?
Could someone write me a short coded example to explain this more concretely?
The authors text is below:
The ability of static factory methods to return the same object from
repeated invocations allows classes to maintain strict control over
what instances exist at any time. Classes that do this are said to be
instance-controlled. There are several reasons to write
instance-controlled classes. Instance control allows a class to
guarantee that it is a singleton (Item 3) or noninstantiable (Item 4).
Also, it allows an immutable class (Item 15) to make the guarantee
that no two equal instances exist: a.equals(b) if and only if a==b. If
a class makes this guarantee, then its clients can use the == operator
instead of the equals(Object) method, which may result in improved
performance. Enum types (Item 30) provide this guarantee.
In the particular snippet you quote at the top he's talking about enforcing one instance for each possible set of values in instances of an immutable class:
Also, it allows an immutable class (Item 15) to make the guarantee
that no two equal instances exist: a.equals(b) if and only if a==b
That is, you might want your static factory to guarantee that if a and b represent the same values, then they are the same instance in memory (i.e. duplicates cannot exist). When this is true, then == works the same as equals(Object), which means that you are free to use == where you think it might help with performance.
As Jon says in the comments, static factories are not restricted to singletons.
I think you've almost got it. The static method makes the following promise, "if you request a new object that would compare .equals() to an existing object, I'll return the existing object instead". Given that guarantee, you know that a.equals(b) means that a == b, and you know that a == b means that a.equals(b). As a result, if you want to see if a and b are equal, you can use the == operator instead of the .equals method. That's useful because == is very fast and, depending on the object types, .equals could be slow.
Here's a concrete example. Suppose we have a person class. A person is defined by their first and last name (pretend that there are no two people in the world with the same name). My class might look like this (didn't try to compile, so no guarantee of correctness):
class Person {
private final String fname;
private final String lname;
// Private constructor - must use the static method
private Person(String first, String last) {fname = first; lname = last;}
// Note that this is slow - the time it takes is proportional to the length of the
// two names
public boolean equals(Object o) {
// Should check types here, etc.
Person other = (Person) o;
if (!person.fname.equals(other.fname)) {return false;}
if (!person.lname.equals(other.lname)) {return false;}
return true;
}
// Registry of all existing people
private static Map<String, Person> registry = new TreeMap<String, Person>();
public static getPerson(String fname, String lname) {
String fullName = fname + "-" + lname;
// If we already have this person, return that object, don't construct a new one.
// This ensures that p1.equals(p2) means that p1 == p2
if (registry.containsKey(fullName)) {return registry.get(fullName);}
Person p = new Person(fname, lname);
registry.put(fullName, p);
return p;
}
}
And then you can use it like this:
public boolean isSamePerson(Person p1, Person p2) {
// Guaranteed to have the same result as "return p1.equals(p2)" but will be faster
return p1 == p2;
}
If you can guarantee (perhaps with a Flyweight pattern) that equal objects will have the same referent, then callers may use == (and get a performance benefit); as an example consider an enum type... you can use == to determine if any two enum instances are the same.
Related
If hashCode() calculation uses immutable fields and equals() uses all the fields would it be a problem when the class is used as a hash key? E.g.
import java.util.Objects;
public class Car {
protected final long vin;
protected String state;
protected String plateNumber;
public Car( long v, String s, String p ) {
vin = v; state = s; plateNumber = p;
}
public void move( String s, String p ) {
state = s; plateNumber = p;
}
public int hashCode() {
return (int)( vin % Integer.MAX_VALUE );
}
public boolean equals( Object other ) {
if (this == other) return true;
else if (!(other instanceof Car)) return false;
Car otherCar = (Car) other;
return vin == otherCar.vin
&& Objects.equals( state, otherCar.state )
&& Objects.equals( plateNumber, otherCar.plateNumber );
}
}
And move() is called on a car object after it is inserted into a hashset, possible via a reference kept elsewhere.
I am not after performance issues here. Only correctness.
I have read java hashCode contact, few answers on SO including this by venerable Jon Skeet and this from big blue. I feel that the last link gives the best explanation and imply that above code is correct.
Edit
Conclusion:
This class satisfy constraints placed on ‘equals()’ and ‘hashCode()’ in java. However it violates restrictions additional requirements placed on ‘equals()’ when used as keys in collections, hashed or not.
The additional requirement is that ‘equals()’ need to be consistent as long as the object is a key.
See the counter example by Louis Wasserman and the reference provided by Douglas below.
Few clarifications:
A) This class satisfy java object level constraints:
( carA == carB ) implies ( carA.hashCode() == carB.hashCode() )
( carA.hashCode() != carB.hashCode() ) implies ( carA != carB )
equals() need to be reflexive, symmetric, transitive.
hashCode() need to be consistent. i.e. Cannot change for an object during its lifetime.
equals() need to be consistent as long as neither object is modified.
Note that the reverse of ‘1.’ and ‘2.’ are not necessary. And the class above satisfies all the conditions.
Also java docs mention "equals() … implements the most discriminating possible equivalence relation on objects", but not sure if that is compulsory.
B) As for performance, the increment in collision avoidance probability decrease with each successive member variable we combine. Usually few well chosen member variables is sufficient.
It's correct if you never, ever call move after the Car is in the map. Otherwise it's wrong. Both hashCode and equals have to stay consistent after a key is in the map.
When considering only the hashCode and equals contracts, you are correct that this implementation satisfies their requirements. hashCode using a strict subset of the fields that equals uses is sufficient to guarantee that a.equals(b) implies a.hashCode() == b.hashCode() as required.
However, things change when you bring in Map. From the Map javadoc, "The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map."
After you call move on a Car that is a key in a Map, the behavior of that Map is now unspecified. In many cases it will in practice still work the way you want it to, but bizarre things could happen in ways that are hard to predict. While it would technically be valid for the Map to spontaneously empty itself or switch all lookups to use a random number generator, a more likely scenario might go like this:
Car car1 = ...
Car car2 = ... // a copy of car1
Map<Car, String> map1 = ...
map1.put(car1, "value");
assert map1.get(car2).equals("value"); // true
car1.move(...);
assert map1.get(car2).equals("value"); // NullPointerException on the equals call, car2 is no longer found
Notice that neither car2 nor the Map were changed themselves in any way, but the mapping of car2 changed (or rather, disappeared) anyway. This behavior is not officially specified, but I would guess most Map implementations do behave this way.
You may mutate your key candidates as much as you want, before or after (not during) they are used as keys.
In practice, it is very hard to enforce this rule. If you mutate objects you do not have a control if somebody uses them as keys or not.
Immutability for keys is just easier, removes source of subtle, hard-to-find bugs and just work better for key.
In your case I see no correctness issues. But why you ever bother not to include all fields in hashcode?
Short answer: it should be OK, but prepare for bizarre behavior.
Longer answer: when you change fields that participate in equals() on a key, the value keyed by that key will no longer be found.
Still longer answer: this looks as X/Y problem: you're asking about X, but you really need X to accomplish Y. Maybe you should ask about Y?
The car in your case is uniquely identified by vin. A car equals to itself. But, a car can be registered in different states. Maybe the answer is to have a Registration object (or a few of them) attached to the car? And then you can separate car.equals() from registration.equals().
Hash works by putting items into "buckets". Each bucket is calculated by the hashcode. After finding the bucket then the search continues comparing each item one by one using equals.
For example:
During insertion: an object whose id is 100 is placed in bucket 5 (the hashcode calculated 5).
During retrieval: you ask the hashmap to find the item 100. If the hash calculates 7 now then the algorithm will search for your object in bucket 7 but your object will never be found as it is dwelling in bucket 5.
In summary: the hash code and the actual key work together. The former is used to know in which bucket the item should be. The latter is used by the equals comparison seeking the actual item to return.
When your hashCode() implementation uses only limited number of fields (vs equals) you're reducing performance of almost any algorithm/data structure that uses hashing: HashMap, HashSet etc. You're increasing collision probability - it's the situation when two different objects (equals return false) have the same hash value.
The short answer is: No.
Long answer:
Fully immutability is not neccessary. BUT:
Equals must only depend on immutable values. Hashcode must depend on immutable values either a constant or a subset of the values used in equals or all values used in equals. Values that are not mentioned within equals mustn't be part of hashcode.
If you mutate values equals and hashcode rely on it is likely that you do not find your objects again in a hash based datastructure. Look at this:
public class Test {
private static class TestObject {
private String s;
public TestObject(String s) {
super();
this.s = s;
}
public void setS(String s) {
this.s = s;
}
#Override
public boolean equals(Object obj) {
boolean equals = false;
if (obj instanceof TestObject) {
TestObject that = (TestObject) obj;
equals = this.s.equals(that.s);
}
return equals;
}
#Override
public int hashCode() {
return this.s.hashCode();
}
}
public static void main(String[] args) {
TestObject a1 = new TestObject("A");
TestObject a2 = new TestObject("A");
System.out.println(a1.equals(a2)); // true
HashMap<TestObject, Object> hashSet = new HashMap<>(); // hash based datastructure
hashSet.put(a1, new Object());
System.out.println(hashSet.containsKey(a1)); // true
a1.setS("A*");
System.out.println(hashSet.containsKey(a1)); // false !!! // Object is not found as the hashcode used initially before mutation was used to determine the hash bucket
a2.setS("A*");
System.out.println(hashSet.containsKey(a2)); // false !!! Because a1 is in wrong hash bucket ...
System.out.println(a1.equals(a2)); // ... even if the objects are equals
}
}
Let's say i have this class:
public class Person {
private Integer idFromDatabase;
private String name;
//Getters and setters
}
The field idFromDatabase is the attribute that should be verified in equals and used to create the hashCode. But sometimes, i am working with a list of People in memory, and have not yet stored the objects on the database, so the idFromDatabase is null for all objects, which would cause hashCode to return the same value for every object.
I solved this issue by adding the following to equals and hashCode metods:
if(idFromDatabase == null) return super.equals(o);
and
if(idFromDatabase == null) return super.hashCode();
It worked, but is it safe? Can i do it for every class that relies on a database field for equality check?
if(idFromDatabase == null) return super.equals(o); is incorrect as super's equals (if implemented correctly) does a getClass() check, which will of course be different, thus super.equals will always be false.
As already noted by #Jeroen Vannevel, if you are likely to end up with having 2 or more objects not stored in database holding the exact same information, then this technique will not help you in identifying this.
#Solver is also quite true in that a subclass is meant to have different behavior than its superclass, so you shouldn't return that they're equal.
However, in your particular example, you are just extending the Object class, so your assumption that it is safe is true (if we exclude the possibility of having 2 not-yet-persisted same Persons in memory).
Object provides the most basic equals method:
For any non-null reference values x and y, this method returns true
if and only if x and y refer to the same object
(x == y has the value true).
The hashCode method of Object:
As much as is reasonably practical, [...] does return distinct integers for distinct objects
These definitions make it clear that if you're only extending Object, then this technique is safe.
From your description I'm inferring that when comparing two People objects:
If both have an ID, they are equal if they have the same ID, even if they have different names
Otherwise, they are only equal if they are the same instance.
If that's correct, then:
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (this.idFromDatabase == null)
return false;
if (! (obj instanceof People))
return false;
People that = (People)obj;
if (that.idFromDatabase == null)
return false;
return this.idFromDatabase.equals(that.idFromDatabase);
}
#Override
public int hashCode() {
// Use super.hashCode to distribute objects without an idFromDatabase
return (this.idFromDatabase != null ? this.idFromDatabase.hashCode() : super.hashCode());
}
There are a few problems with your reasoning.
equals and hashcode are not subtype-friendly so it doesn't make sense to start thinking about super calls,
super is Object anyway so it's equals and hashcode are useless in this context.
What if you have two Person objects referring to the same person, but only one is stored in the database. Are they the same or different?
One universal solution is to make two classes
Person which stores a 'local' person. Doesn't contain idFromDatabase,
StoredPerson which contains idFromDatabase and a Person (or all fields of Person, but this is harder to maintain)
This way, at least equals and hashcode are well-defined and well-behaved at all times.
Implementation and usage
If you use any kind of Set/Map to store people, you now have two of them. When you save new Persons to database, you remove them from the 'local' Set/Map, wrap them in StoredPerson, and put them in the 'database' Set/Map.
If you want a searchable list of all people, make one with all Persons from both datasets into one. When you find a Person you're interested in and want to retrieve the idFromDatabase, if any, then you'd do good to prepare a map from Person to StoredPerson beforehand.
Thus you need at least,
Set<Person> localPeople = new HashSet<>();
Map<Person, StoredPerson> storedPeople = new HashMap<>();
and something like this:
void savePerson(Person person) {
synchronized (lockToPreserveInvariants) {
int id = db.insert(person);
StoredPerson sp = new StoredPerson(id, person);
localPeople.remove(person);
storedPeople.put(person, sp);
}
}
It is stated in Object's .equals(Object) javadoc:
It is symmetric: for any non-null reference values x and y,
x.equals(y) should return true if and only if y.equals(x) returns
true.
Almost everywhere in example code I see overridden .equals(Object) method which uses instanceof as one of the first tests, for example here: What issues / pitfalls must be considered when overriding equals and hashCode?
public class Person {
private String name;
private int age;
public boolean equals(Object obj) {
if (obj == null)
return false;
if (obj == this)
return true;
if (!(obj instanceof Person))
return false;
...
}
}
Now with class SpecialPerson extends Person having in equals:
if (!(obj instanceof SpecialPerson))
return false;
we con not guarantee that .equals() is symmetric.
It has been discussed for example here: any-reason-to-prefer-getclass-over-instanceof-when-generating-equals
Person a = new Person(), b = new SpecialPerson();
a.equals(b); //sometimes true, since b instanceof Person
b.equals(a); //always false
Maybe I should add in the beginning of SpecialPerson's equals direct call to super?
public boolean equals(Object obj) {
if( !obj instanceof SpecialPerson )
return super.equals(obj);
...
/* more equality tests here */
}
A lot of the examples use instanceof for two reasons: a) it folds the null check and type check into one or b) the example is for Hibernate or some other code-rewriting framework.
The "correct" (as per the JavaDoc) solution is to use this.getClass() == obj.getClass(). This works for Java because classes are singletons and the VM guarantees this. If you're paranoid, you can use this.getClass().equals(obj.getClass()) but the two are really equivalent.
This works most of the time. But sometimes, Java frameworks need to do "clever" things with the byte code. This usually means they create a subtype automatically. Since the subtype should be considered equal to the original type, equals() must be implemented in the "wrong" way but this doesn't matter since at runtime, the subtypes will all follow certain patterns. For example, they will do additional stuff before a setter is being called. This has no effect on the "equalness".
As you noticed, things start to get ugly when you have both cases: You really extend the base types and you mix that with automatic subtype generation. If you do that, you must make sure that you never use non-leaf types.
You are missing something here. I will try to highlight this:
Suppose you have Person person = new Person() and Person personSpecial = new SpecialPerson() then I am sure you would not like these two objects to be equal. So, its really working as required, the equal must return false.
Moreover, symmetry specifies that the equals() method in both the classes must obey it at the same time. If one equals return true and other return false, then I would say the flaw is in the equals overriding.
Your attempt at solving the problem is not correct. Suppose you have 2 subclasss SpecialPerson and BizarrePerson. With this implementation, BizarrePerson instances could be equal to SpecialPerson instances. You generally don't want that.
don't use instanceof. use this.getClass() == obj.getClass() instead. then you are checking for this exact class.
when working with equalsyou should always use the hashCode and override that too!
the hashCode method for Person could look like this:
#Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
and use it like this in your equals method:
if (this.hashCode() != obj.hashCode())
{
return false;
}
A type should not consider itself equal to an object of any other type--even a subtype--unless both objects derive from a common class whose contract specifies how descendants of different types should check for equality.
For example, an abstract class StringyThing could encapsulate strings, and provide methods to do things like convert to a string or extract substrings, but not impose any requirements on the backing format. One possible subtype of StringyThing, for example, might contain an array of StringyThing and encapsulate the value of the concatenation of all those strings. Two instances of StringyThing would be defined as equal if conversion to strings would yield identical results, and comparison between two otherwise-indistinguishable StringyThing instances whose types knew nothing about each other may have to fall back on that, but StringyThing-derived types could include code to optimize various cases. For example, if one StringyThing represents "M repetitions of character ch" and another represents "N repetitions of the string St", and the latter type knows about the first, it could check whether St contains nothing but M/N repetitions of the character ch. Such a check would indicate whether or not the strings are equal, without having to "expand out" either one of them.
I am quite new to the concepts and pretty naive user so please excuse me for the following question,but
I am trying to understand the basic concepts of collection in java
I have made the following class
package com.vish;
public class HashSetDemo {
private int age;
public HashSetDemo(int age) {
this.age = age;
}
}
Now here I am having set collection framework described in my following class
package com.vish;
import java.util.HashSet;
public class HashSetDemo1 {
public static void main(String args[]) {
HashSetDemo hsd = new HashSetDemo(23);
HashSetDemo hsd1 = new HashSetDemo(24);
HashSet<HashSetDemo> hashset = new HashSet<HashSetDemo>();
hashset.add(hsd);
hashset.add(hsd1);
System.out.println(hashset.size());
System.out.println(hashset.contains(hsd));
System.out.println(hashset.contains(new HashSetDemo(23)));
}
}
Now the outut of this is following
2
true
false
Why is the last one false,when it has the same object reference
Thanks
Why is the last one false,when it has the same object reference
It doesn't. You've created a new object which happens to have the same value for age.
It's like asking a builder to build you two houses with 5 bedrooms. Yes, they look the same - but they're different houses, with different addresses.
Now HashSet actually doesn't for equal references - it checks for equal objects - where equality is determined via the hashCode and equals methods. By default, this checks for reference identity, but it doesn't have to. So if you override equals and hashCode to determine equality just your age value, then it would consider your new object equal to the old one.
public final class HashSetDemo {
private final int age;
public HashSetDemo(int age) {
this.age = age;
}
#Override public int hashCode() {
return age;
}
#Override public boolean equals(Object other) {
if (!(other instanceof HashSetDemo)) {
return false;
}
HashSetDemo otherDemo = (HashSetDemo) other;
return age == otherDemo.age;
}
}
Because you have not implemented equals() in your HashSetDemo class. If you don't do that, then java can't figure out how to tell if two objects are equal. It does have a default implementation though, and that default implementation is to ask, "Are these two objects the same reference?"
Since you are explicitly creating a new HashSetDemo, Java uses the default equals() and says, "no, these are not the same instance of HashSetDemo"
Because you have not provided a custom equals and hashCode method for your class. Your class uses the implementations provided by Object.
If you overrode equals to be return this.age == ((HashSetDemo)other).age and overrode hashCode to return a hash value derived from age, then your last call to hashset.contains would return True.
Its not the same reference. In second case, you are creating new instance of HashSetDemo, and that has different address in memory.
Your HashSet is using default comparator for searching, and that compares instances of objects, not their content.
Why is the last one false,when it has the same object reference
Even though the objects are exactly the same, they aren't the same objects. It's like putting one brand new bicycle in a garage, getting another brand new bicycle exactly the same as the other, and asking the garage if it contains the second bike. Sure, the bikes may be equal, but they're not the same.
Good question, by the way.
You need to override equals() and hashCode() in HashSetDemo. This tells your program how to determine whether 2 separate instances are equal. If you don't, your program will fall back to the default implementation, which only checks the object reference. In your third line, the object is equivalent, but Java doesn't know that - it only knows that its a different reference.
In the last case you have created new Object new HashSetDemo(23) It will store in different location in the java heap memory. It is different compare to the other two objects hsd and hsd1.
Have the following class:
public class Member {
private int x;
private long y;
private double d;
public Member(int x, long y, double d) {
this.x = x;
this.y = y;
this.d = d;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = (int) (prime * result + y);
result = (int) (prime * result + Double.doubleToLongBits(d));
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Member) {
Member other = (Member) obj;
return other.x == x && other.y == y
&& Double.compare(d, other.d) == 0;
}
return false;
}
public static void main(String[] args) {
Set<Member> test = new HashSet<Member>();
Member b = new Member(1, 2, 3);
test.add(b);
System.out.println(b.hashCode());
b.x = 0;
System.out.println(b.hashCode());
Member first = test.iterator().next();
System.out.println(test.contains(first));
System.out.println(b.equals(first));
System.out.println(test.add(first));
}
}
It produces the following results:
30814
29853
false
true
true
Because the hashCode depends of the state of the object it can no longer by retrieved properly, so the check for containment fails. The HashSet in no longer working properly. A solution would be to make Member immutable, but is that the only solution? Should all classes added to HashSets be immutable? Is there any other way to handle the situation?
Regards.
Objects in hashsets should either be immutable, or you need to exercise discipline in not changing them after they've been used in a hashset (or hashmap).
In practice I've rarely found this to be a problem - I rarely find myself needing to use complex objects as keys or set elements, and when I do it's usually not a problem just not to mutate them. Of course if you've exposed the references to other code by this time, it can become harder.
Yes. While maintaining your class mutable, you can compute the hashCode and the equals methods based on immutable values of the class ( perhaps a generated id ) to adhere to the hashCode contract defined in Object class:
Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
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.
It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables.
Depending on your situation this may be easier or not.
class Member {
private static long id = 0;
private long id = Member.id++;
// other members here...
public int hashCode() { return this.id; }
public boolean equals( Object o ) {
if( this == o ) { return true; }
if( o instanceOf Member ) { return this.id == ((Member)o).id; }
return false;
}
...
}
If you need a thread safe attribute, you may consider use: AtomicLong instead, but again, it depends on how are you going to use your object.
As already mentioned, one can accept the following three solutions:
Use immutable objects; even when your class is mutable, you may use immutable identities on your hashcode implementation and equals checking, eg an ID-like value.
Similarly to the above, implement add/remove to get a clone of the inserted object, not the actual reference. HashSet does not offer a get function (eg to allow you alter the object later on); thus, you are safe there won't exist duplicates.
Exercise discipline in not changing them after they've been used, as #Jon Skeet suggests
But, if for some reason you really need to modify objects after being inserted to a HashSet, you need to find a way of "informing" your Collection with the new changes. To achieve this functionality:
You can use the Observer design pattern, and extend HashSet to implement the Observer interface. Your Member objects must be Observable and update the HashSet on any setter or other method that affects hashcode and/or equals.
Note 1: Extending 3, using 4: we may accept alterations, but those that do not create an already existing object (eg I updated a user's ID, by assigning a new ID, not setting it to an existing one). Otherwise, you have to consider the scenario where an object is transformed in such a way that is now equal to another object already existing in the Set. If you accept this limitation, 4th suggestion will work fine, else you must be proactive and define a policy for such cases.
Note 2: You have to provide both previous and current states of the altered object on your update implementation, because you have to initially remove the older element (eg use getClone() before setting new values), then add the object with the new state. The following snippet is just an example implementation, it needs changes based on your policy of adding a duplicate.
#Override
public void update(Observable newItem, Object oldItem) {
remove(oldItem);
if (add(newItem))
newItem.addObserver(this);
}
I've used similar techniques on projects, where I require multiple indices on a class, so I can look up with O(1) for Sets of objects that share a common identity; imagine it as a MultiKeymap of HashSets (this is really useful, as you can then intersect/union indices and work similarly to SQL-like searching). In such cases I annotate methods (usually setters) that must fireChange-update each of the indices when a significant change occurs, so indices are always updated with the latest states.
Jon Skeet has listed all alternatives. As for why the keys in a Map or Set must not change:
The contract of a Set implies that at any time, there are no two objects o1 and o2 such that
o1 != o2 && set.contains(o1) && set.contains(o2) && o1.equals(o2)
Why that is required is especially clear for a Map. From the contract of Map.get():
More formally, if this map contains a mapping from a key
k to a value v such that (key==null ? k==null : key.equals(k)), then this method returns v, otherwise it returns null. (There can be at most one such mapping.)
Now, if you modify a key inserted into a map, you might make it equal to some other key already inserted. Moreover, the map can not know that you have done so. So what should the map do if you then do map.get(key), where key is equal to several keys in the map? There is no intuitive way to define what that would mean - chiefly because our intuition for these datatypes is the mathematical ideal of sets and mappings, which don't have to deal with changing keys, since their keys are mathematical objects and hence immutable.
Theoretically (and more often than not practically too) your class either:
has a natural immutable identity that can be inferred from a subset of its fields, in which case you can use those fields to generate the hashCode from.
has no natural identity, in which case using a Set to store them is unnecessary, you could just as well use a List.
Never change 'hashable field" after putting in hash based container.
As if you (Member) registered your phone number (Member.x) in yellow page(hash based container), but you changed your number, then no one can find you in the yellow page any more.