Modify existing Key of HashMap In Java - java

I'm working with HashMap since few days, and facing below weird situation.
Case 1:Changed Key which is already existing in HashMap, and print HashMap
Case 2: Changed key which is already existing in HashMap and Put that key again into the HashMap. Print HashMap.
Please find below code as well as two different output of two case.
Could you please anyone let me know, whats going on in below code.
import java.util.HashMap;
import java.util.Set;
class Emp{
private String id ;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Emp(String id) {
super();
this.id = id;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Emp other = (Emp) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
#Override
public String toString() {
return "Emp [id=" + id + "]";
}
}
public class HashMapChanges {
public static void main(String[] args) {
// TODO Auto-generated method stub
Emp e1 = new Emp("1");
Emp e2 = new Emp("2");
Emp e3 = new Emp("3");
Emp e4 = new Emp("4");
HashMap<Emp, String> hm = new HashMap<Emp,String>();
hm.put(e1,"One");
hm.put(e2,"Two");
hm.put(e3,"Three");
hm.put(e4,"Four");
e1.setId("5");
/** Uncomment below line to get different output**/
//hm.put(e1,"Five-5");
Set<Emp> setEmpValue = hm.keySet();
for(Emp e : setEmpValue){
System.out.println("Key"+ e +" Value "+ hm.get(e));
}
}
}
Output of above code :
KeyEmp [id=2] Value Two
KeyEmp [id=5] Value null
KeyEmp [id=4] Value Four
KeyEmp [id=3] Value Three
Output After uncommenting line
KeyEmp [id=5] Value Five-5
KeyEmp [id=2] Value Two
KeyEmp [id=5] Value Five-5
KeyEmp [id=4] Value Four
KeyEmp [id=3] Value Three

Using mutable objects as keys in a Map is not permitted when the key used to determine its location in the Map is mutable.
From the Javadoc for java.util.Map<K,V>:
Note: great care must be exercised if mutable objects are used as map keys. 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.
You are violating the contract required of map keys because your Emp object is mutable, and the change modifies the attribute used to determine where in the map the key resides.
The result is undefined behavior.
I suspect you have misunderstood the Map concept, based on your code, but without understanding what you're actually trying to achieve we really cannot help further. I suggest you ask a new question explaining your actual goals.

You overwrite the hashCode() and equals() method ,
then ,the Map's key is the hashCode result.
then id=1 and id=5 are two different items.
You can comment the two methods and try again.

Related

Comparing An Entry In A Map With An Object

I have a Map in Java like so,
private HashMap<String, Object[][]> theMap;
Where the key is a String and the entry is going to be something along the line of,
theMap = new HashMap<>();
Object[][] theData = {
{Boolean.FALSE, "Text"}
};
theMap.put("Key1", theData);
Somewhere along the line I would like to check if an entry in the map is equivalent to another object. Currently I am doing it like this,
Object[][] tempData = {
{Boolean.FALSE, "Text"}
};
for(Object key: entries.keySet()) {
if(entries.get(key).equals(tempData)) {
entries.remove(key);
}
}
And it is not working.
I would prefer the comparison to be done with an object rather than with another map. I'm wondering what I'm doing wrong with this comparison here?
The reason you are not getting equality is that arrays inherit Object#equals() which is based on identity, not equality of contents. You could consider using java.util.Arrays.deepEquals(Object[], Object[]) to compare.
That is the answer to the immediate question. However, using a 2-dimensional array of Object to hold a boolean and a String is really bad code smell and indicates you need to encapsulate what you are putting in the array.
Identity vs Equivalence
Please make sure that you understand that by default the equals() method of Object checks on whether two object references are referring to the same object (identity), which is not what your code is checking.
Instead, your code is checking whether the two objects (the values you put on the map) are having the same value (equivalence).
Here are two articles about this topic:
What is the difference between identity and equality in OOP?
Overriding equals method in Java
In this particular problem of yours, I think the solution involves two steps:
Your tempData and theData does not seems to be an array
of elements of the same type (it does not appear to be a 2-dimensional
array either). Instead, it contains a Boolean value and then a
String value. In this case, I think you really should think
through what this thingy is and design a class for it (I am showing
an example below)
The class should override the equals() (and hashCode()) methods
so that you can use its equals() for equivalence checking.
Note also that your IDE (e.g. Eclipse) probably can generate a template for equals() and hashCode() for you.
Example: (here I assume your Boolean represents a condition, and your String represents a message)
class MyRecord {
private Boolean condition;
private String message;
public Boolean getCondition() {
return condition;
}
public void setCondition(Boolean condition) {
this.condition = condition;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((condition == null) ? 0 : condition.hashCode());
result = prime * result
+ ((message == null) ? 0 : message.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MyRecord other = (MyRecord) obj;
if (condition == null) {
if (other.condition != null)
return false;
} else if (!condition.equals(other.condition))
return false;
if (message == null) {
if (other.message != null)
return false;
} else if (!message.equals(other.message))
return false;
return true;
}
}

Correct way to implement Map<MyObject,ArrayList<MyObject>>

I was asked this in interview. using Google Guava or MultiMap is not an option.
I have a class
public class Alpha
{
String company;
int local;
String title;
}
I have many instances of this class (in order of millions). I need to process them and at the end find the unique ones and their duplicates.
e.g.
instance --> instance1, instance5, instance7 (instance1 has instance5 and instance7 as duplicates)
instance2 --> instance2 (no duplicates for instance 2)
My code works fine
declare datastructure
HashMap<Alpha,ArrayList<Alpha>> hashmap = new HashMap<Alpha,ArrayList<Alpha>>();
Add instances
for (Alpha x : arr)
{
ArrayList<Alpha> list = hashmap.get(x); ///<<<<---- doubt about this. comment#1
if (list == null)
{
list = new ArrayList<Alpha>();
hashmap.put(x, list);
}
list.add(x);
}
Print instances and their duplicates.
for (Alpha x : hashmap.keySet())
{
ArrayList<Alpha> list = hashmap.get(x); //<<< doubt about this. comment#2
System.out.println(x + "<---->");
for(Alpha y : list)
{
System.out.print(y);
}
System.out.println();
}
Question: My code works, but why? when I do hashmap.get(x); (comment#1 in code). it is possible that two different instances might have same hashcode. In that case, I will add 2 different objects to the same List.
When I retrieve, I should get a List which has 2 different instances. (comment#2) and when I iterate over the list, I should see at least one instance which is not duplicate of the key but still exists in the list. I don't. Why?. I tried returning constant value from my hashCode function, it works fine.
If you want to see my implementation of equals and hashCode,let me know.
Bonus question: Any way to optimize it?
Edit:
#Override
public boolean equals(Object obj) {
if (obj==null || obj.getClass()!=this.getClass())
return false;
if (obj==this)
return true;
Alpha guest = (Alpha)obj;
return guest.getLocal()==this.getLocal()
&& guest.getCompany() == this.getCompany()
&& guest.getTitle() == this.getTitle();
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (title==null?0:title.hashCode());
result = prime * result + local;
result = prime * result + (company==null?0:company.hashCode());
return result;
}
it is possible that two different instances might have same hashcode
Yes, but hashCode method is used to identify the index to store the element. Two or more keys could have the same hashCode but that's why they are also evaluated using equals.
From Map#containsKey javadoc:
Returns true if this map contains a mapping for the specified key. More formally, returns true if and only if this map contains a mapping for a key k such that (key==null ? k==null : key.equals(k)). (There can be at most one such mapping.)
Some enhancements to your current code:
Code oriented to interfaces. Use Map and instantiate it by HashMap. Similar to List and ArrayList.
Compare Strings and Objects in general using equals method. == compares references, equals compares the data stored in the Object depending the implementation of this method. So, change the code in Alpha#equals:
public boolean equals(Object obj) {
if (obj==null || obj.getClass()!=this.getClass())
return false;
if (obj==this)
return true;
Alpha guest = (Alpha)obj;
return guest.getLocal().equals(this.getLocal())
&& guest.getCompany().equals(this.getCompany())
&& guest.getTitle().equals(this.getTitle());
}
When navigating through all the elements of a map in pairs, use Map#entrySet instead, you can save the time used by Map#get (since it is supposed to be O(1) you won't save that much but it is better):
for (Map.Entry<Alpha, List<Alpha>> entry : hashmap.keySet()) {
List<Alpha> list = entry.getValuee();
System.out.println(entry.getKey() + "<---->");
for(Alpha y : list) {
System.out.print(y);
}
System.out.println();
}
Use equals along with hashCode to solve the collision state.
Steps:
First compare on the basis of title in hashCode()
If the title is same then look into equals() based on company name to resolve the collision state.
Sample code
class Alpha {
String company;
int local;
String title;
public Alpha(String company, int local, String title) {
this.company = company;
this.local = local;
this.title = title;
}
#Override
public int hashCode() {
return title.hashCode();
}
#Override
public boolean equals(Object obj) {
if (obj instanceof Alpha) {
return this.company.equals(((Alpha) obj).company);
}
return false;
}
}
...
Map<Alpha, ArrayList<Alpha>> hashmap = new HashMap<Alpha, ArrayList<Alpha>>();
hashmap.put(new Alpha("a", 1, "t1"), new ArrayList<Alpha>());
hashmap.put(new Alpha("b", 2, "t1"), new ArrayList<Alpha>());
hashmap.put(new Alpha("a", 3, "t1"), new ArrayList<Alpha>());
System.out.println("Size : "+hashmap.size());
Output
Size : 2

removeAll from Interface Set

I want to compare database dump to xml and *.sql. In debagge toRemove and toAdd only differ in dimension. toRemove has size 3, toAdd has size 4. But after running the code, removeAll, toRemove has size 3 and toAdd has size 4. What's wrong?
final DBHashSet fromdb = new DBHashSet(strURL, strUser, strPassword);
final DBHashSet fromxml = new DBHashSet(namefile);
Set<DBRecord> toRemove = new HashSet<DBRecord>(fromdb);
toRemove.removeAll(fromxml);
Set<DBRecord> toAdd = new HashSet<DBRecord>(fromxml);
toAdd.removeAll(fromdb);
Update:
public class DBRecord {
public String depcode;
public String depjob;
public String description;
public DBRecord(String newdepcode, String newdepjobe, String newdesc) {
this.depcode = newdepcode;
this.depjob = newdepjobe;
this.description = newdesc;
}
public String getKey() {
return depcode + depjob;
}
public boolean IsEqualsKey(DBRecord rec) {
return (this.getKey().equals(rec.getKey()));
}
public boolean equals(Object o) {
if (o == this)
return true;
if (o == null)
return false;
if (!(getClass() == o.getClass()))
return false;
else {
DBRecord rec = (DBRecord) o;
if ((rec.depcode.equals(this.depcode)) && (rec.depjob.equals(this.depjob)))
return true;
else
return false;
}
}
}
In order to properly use HashSet (and HashMap, for that matter), you must implement a hashCode() as per the following contract:
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 hash tables.
The code you've supplied for DBRecord does not overide it, hence the problem.
You'd probably want to override it in the following way, or something similar:
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + depcode.hashCode();
result = prime * result + depjob.hashCode());
return result;
}

Howto Store 2 combined unique key in a HashMap?

I want to store values that are binded to a name + number.
Like, (John,1) (RED) and (John,2) (BLUE) and (Elize,1) (GREEN)
So how can i store a 2 keys that are combineded unique?
Create a new type which represents the composite key (the name and the number here). You'll need to override hashCode() and equals(), and I'd strongly advise you to make the type immutable. For example:
public final class NameIntPair {
private final int intValue;
private final String name;
public NameIntPair(int intValue, String name) {
this.intValue = intValue;
this.name = name;
}
#Override
public int hashCode() {
int hash = 17;
hash = hash * 31 + intValue;
hash = hash * 31 + (name == null ? 0 : name.hashCode());
return hash;
}
#Override
public boolean equals(Object obj) {
if (!(obj instanceof NameIntPair)) {
return false;
}
if (this == obj) {
return true;
}
NameIntPair other = (NameIntPair) obj;
return other.intValue == intValue && Objects.equal(other.name, name);
}
}
I'm using Objects.equal from Guava for convenience to avoid explicit nullity checks here - if you're not using Guava, you'd either have to use an equivalent or handle nullity in the code. Alternatively, you may well want to prevent null names, validating this in the constructor.
I'll use a String concatenation if I'm sure about the uniqueness of the combination and if the keys object are easy to stringify. I might use a special character to join the keys (like "John#1" and "John#2").
If I'm not sure about that I'll use Guava's Table:
Typically, when you are trying to index on more than one key at a
time, you will wind up with something like Map(FirstName,
Map(LastName, Person)), which is ugly and awkward to use. Guava
provides a new collection type, Table, which supports this use case
for any "row" type and "column" type
So a Table is
A collection that associates an ordered pair of keys, called a row key
and a column key, with a single value.
Define your specific Key class like this :
public class Key {
final String name;
final int number;
public Key(String name, int number) {
this.name = name;
this.number = number;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result
+ ((name == null) ? 0 : name.hashCode());
result = prime * result + number;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Key other = (Key) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (number != other.number)
return false;
return true;
}
private Test getOuterType() {
return Test.this;
}
}
The important point is to ensure that you respect the contract of equals and hashCode to enable your collection (any standard collection using key) to work as intended.
Here I simply used the generated methods produced by Eclipse but there are also many dynamic utilities (for example in Guava) helping you on this topic.
There is also simple alternative approach with concatenated key suitable for this case:
public static String getKey(String name, int number) {
return name + number;
}
If names are not too long, overhead for string concatenation won't be greater than for creating composite key objects suggested in other answers.

TreeMap returning null for value that should exist for some object keys

I have an issue with a TreeMap that we have defined a custom key object for. The issue is that after putting a few objects into the map, and trying to retrieve with the same key used to put on the map, I get a null. I believe this is caused by the fact that we have 2 data points on the key. One value is always populated and one value is not always populated. So it seems like the issue lies with the use of compareTo and equals. Unfortunately the business requirement for how our keys determine equality needs to be implemented this way.
I think this is best illustrated with code.
public class Key implements Comparable<Key> {
private String sometimesPopulated;
private String alwaysPopulated;
public int compareTo(Key aKey){
if(this.equals(aKey)){
return 0;
}
if(StringUtils.isNotBlank(sometimesPopulated) && StringUtils.isNotBlank(aKey.getSometimesPopulated())){
return sometimesPopulated.compareTo(aKey.getSometimesPopulated());
}
if(StringUtils.isNotBlank(alwaysPopulated) && StringUtils.isNotBlank(aKey.getAlwaysPopulated())){
return alwaysPopulated.compareTo(aKey.getAlwaysPopulated());
}
return 1;
}
public boolean equals(Object aObject){
if (this == aObject) {
return true;
}
final Key aKey = (Key) aObject;
if(StringUtils.isNotBlank(sometimesPopulated) && StringUtils.isNotBlank(aKey.getSometimesPopulated())){
return sometimesPopulated.equals(aKey.getSometimesPopulated());
}
if(StringUtils.isNotBlank(alwaysPopulated) && StringUtils.isNotBlank(aKey.getAlwaysPopulated())){
return alwaysPopulated.equals(aKey.getAlwaysPopulated());
}
return false;
}
So the issue occurs when trying to get a value off the map after putting some items on it.
Map<Key, String> map = new TreeMap<Key, String>();
Key aKey = new Key(null, "Hello");
map.put(aKey, "world");
//Put some more things on the map...
//they may have a value for sometimesPopulated or not
String value = map.get(aKey); // this = null
So why is the value null after just putting it in? I think the algorithm used by the TreeMap is sorting the map in an inconsistent manner because of the way I'm using compareTo and equals. I am open to suggestions on how to improve this code. Thanks
Your comparator violates the transitivity requirement.
Consider three objects:
Object A: sometimesPopulated="X" and alwaysPopulated="3".
Object B: sometimesPopulated="Y" and alwaysPopulated="1".
Object C: sometimesPopulated is blank and alwaysPopulated="2".
Using your comparator, A<B and B<C. Transitivity requires that A<C. However, using your comparator, A>C.
Since the comparator doesn't fulfil its contract, TreeMap is unable to do its job correctly.
I think the problem is that you are returning 1 from your compareTo if either of the sometimesPopulated values is blank or either of the alwaysPopulated values is blank. Remember that compareTo can be thought of returning the value of a subtraction operation and your's is not transitive. (a - b) can == (b - a) even when a != b.
I would return -1 if the aKey sometimesPopulated is not blank and the local sometimesPopulated is blank. If they are the same then I would do the same with alwaysPopulated.
I think your logic should be something like:
public int compareTo(Key aKey){
if(this.equals(aKey)){
return 0;
}
if (StringUtils.isBlank(sometimesPopulated)) {
if (StringUtils.isNotBlank(aKey.getSometimesPopulated())) {
return -1;
}
} else if (StringUtils.isBlank(aKey.getSometimesPopulated())) {
return 1;
} else {
int result = sometimesPopulated.compareTo(aKey.getSometimesPopulated());
if (result != 0) {
return result;
}
}
// same logic with alwaysPopulated
return 0;
}
I believe the problem is that you are treating two keys with both blank fields as greater than each other which could confuse the structure.
class Main {
public static void main(String... args) {
Map<Key, String> map = new TreeMap<Key, String>();
Key aKey = new Key(null, "Hello");
map.put(aKey, "world");
//Put some more things on the map...
//they may have a value for sometimesPopulated or not
String value = map.get(aKey); // this = "world"
System.out.println(value);
}
}
class Key implements Comparable<Key> {
private final String sometimesPopulated;
private final String alwaysPopulated;
Key(String alwaysPopulated, String sometimesPopulated) {
this.alwaysPopulated = defaultIfBlank(alwaysPopulated, "");
this.sometimesPopulated = defaultIfBlank(sometimesPopulated, "");
}
static String defaultIfBlank(String s, String defaultString) {
return s == null || s.trim().isEmpty() ? defaultString : s;
}
#Override
public int compareTo(Key o) {
int cmp = sometimesPopulated.compareTo(o.sometimesPopulated);
if (cmp == 0)
cmp = alwaysPopulated.compareTo(o.alwaysPopulated);
return cmp;
}
}
I think your equals, hashCode and compareTo methods should only use the field that is always populated. It's the only way to ensure the same object will always be found in the map regardless of if its optional field is set or not.
Second option, you could write an utility method that tries to find the value in the map, and if no value is found, tries again with the same key but with (or without) the optional field set.

Categories

Resources