For the following snippet, I see that the latest insertion replaces the old, i.e. the data in Line 2 replaces the one in Line 1.
Map<String, String> someMap = new HashMap<>();
someMap.put("A", "101A9901"); // Line 1
someMap.put("B", "102Z4902");
someMap.put("A", "103C5389"); // Line 2
However, while working with a custom Key class, the old one is retained the new one is never added, i.e. line 1 is not replaced with line 2
Order order1 = new Order(101L, 201L, new BigDecimal(284.50), "Preparing");
Order order2 = new Order(102L, 204L, new BigDecimal(780.00), "Dispatched");
Order order3 = new Order(101L, 201L, new BigDecimal(284.50), "Cancelled");
Order order4 = new Order(104L, 207L, new BigDecimal(550.00), "Cancelled");
Order order5 = new Order(105L, 203L, new BigDecimal(320.50), "Confirmed");
Order order6 = new Order(106L, 207L, new BigDecimal(470.00), "Delivered");
Map<Order, String> orderMapAddtionalStatus = new HashMap<>();
orderMapAddtionalStatus.put(order1, "OK"); // line 1
orderMapAddtionalStatus.put(order2, "OK");
orderMapAddtionalStatus.put(order3, "OK"); // line 2
orderMapAddtionalStatus.put(order4, "OK");
orderMapAddtionalStatus.put(order5, "OK");
orderMapAddtionalStatus.put(order6, "OK");
I have also overridden the equals and hashCode method. Following are the overridden methods -
#Override
public int hashCode() {
int result = getOrderId().hashCode();
result = 31 * result + getCustomerId().hashCode();
return result;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false;
Order order = (Order) o;
if (!getOrderId().equals(order.getOrderId())) return false;
return getCustomerId().equals(order.getCustomerId());
}
Can some please tell me what I might be missing?
If you have a look at HashMap.putVal(...), the method that is used interally, you'll see the following:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
This means the key is added to a new node if it wasn't already present.
However, if there already is a key this portion is executed after the node has been found:
Node<K,V> e;
... //code to find the existing node
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
As you can see, the key is not replaced and that's consistent with the JavaDoc on put(...):
If the map previously contained a mapping for the key, the oldvalue is replaced.
This states the value is replaced, not the key. From the perspective of the map this is consistent since equals() and hashCode() state the keys are the same so either of both (the existing and the new key) can be used.
Related
I am facing a situation similar to described below in my project, of which I am unable to implement the code.
I have a POJO Class
public class TranObject {
public String loadId;
public String vDate;
public String dDate;
public String pDate;
public TranObject(String loadId, String vDate, String dDate, String pDate) {
super();
this.loadId = loadId;
this.vDate = vDate;
this.dDate = dDate;
this.pDate = pDate;
}
//Getter and Setters
//toString()
}
Now I have another processor class where I want to implement some comparison between tranload objects that I am receiving through a data service call and collect them into another collection.
The implementation logic is given in the comments below. Please read the below comments
import java.util.Arrays;
import java.util.List;
public class DemoClass {
public static void main(String[] args) {
List<TranObject> listObj = Arrays.asList(
new TranObject("LOAD1", "20180102", "20180202", null),
new TranObject("LOAD2", "20180402", "20180403", null),
new TranObject("LOAD3", "20180102", "20180202", "20190302"),
new TranObject("LOAD4", "20180402", "20180403", null),
new TranObject("LOAD5", "20200202", "20200203", null)
);
/*
IF (obj1, obj3 vdate and dDate are equal)
IF(pDate == null for obj1 or obj3)
THEN obj1 and obj3 are equal/duplicates, and we collect them.
ELSE IF(pDate != null for obj1 and obj3)
IF(pDate is same for obj1 and obj3)
THEN obj1 and obj3 are duplicates, and we collect them.
ELSE
THEN obj1 and obj3 are unique.
*/
}
}
My End result should be a collection like List containing duplicate Tran objects for further update.
I searched internet in order to how to solve it using Lambda API.
-> Tried using groupingBy first with vDate and then dDate, but then I could not compare them for pDate equality.
Can anyone help me solve this issue. A little help will be very helpful for me. I am stuck here
UPDATE:
After some reading I am trying to implement the same by over-riding equals method in POJO class as shown below:
#Override
public boolean equals(Object obj) {
boolean isEqual=false;
if(obj!=null) {
TranObject tran = (TranObject) obj;
isEqual=(this.vDate.equals(tran.getvDate()) && this.dDate.equals(tran.getdDate()));
if(isEqual && this.pDate != null && tran.getpDate()!= null) {
isEqual = (this.pDate.equals(tran.getpDate()));
}
}
return isEqual;
}
Still it's not working as expected... Can anyone please help me why??
The closest to your requirement would be grouping in a nested manner and then filtering the inner Map for varied conditions while being interested only in values eventually.
Stream<Map<String, List<TranObject>>> groupedNestedStream = listObj.stream()
.collect(Collectors.groupingBy(a -> Arrays.asList(a.vDate, a.dDate)
, Collectors.groupingBy(t -> t.pDate == null ? "default" : t.pDate)))
.values().stream();
from these groupings further the conditions for the values (from map) to be eligible are
they all have same pDate in this case the innerMap would have just one entry with the common pDate (m.size() == 1)
one of the values after grouping has exactly one pDate as null (meaning m.containsKey("default") && m.get("default").size() == 1)
List<TranObject> tranObjects = groupedNestedStream
.filter(m -> m.size() == 1 || (m.containsKey("default") && m.get("default").size() == 1))
.flatMap(m -> m.values().stream().flatMap(List::stream))
.collect(Collectors.toList());
Note, the use of "default" string constant to avoid failures(or poor practice) in collecting a Map with null keys or values.
Sounds like TranObject needs an equals and hashCode method.
#Override
public boolean equals(Object obj) {
//check instanceof and self comparison
TranObject other = (TranObject) obj;
if(this.vDate.equals(other.vDate) && this.dDate.equals(other.dDate)) {
//if pDate is not given then consider them duplicate
if(this.pDate == null || other.pDate == null)
return true;
//if pDate are the same then they are duplicate, otherwise they are unique
return this.pDate.equals(other.pDate);
}
return false;
}
//auto generated by Eclipse
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((dDate == null) ? 0 : dDate.hashCode());
result = prime * result + ((pDate == null) ? 0 : pDate.hashCode());
result = prime * result + ((vDate == null) ? 0 : vDate.hashCode());
return result;
}
Now that you have an equals method to determine if two TranObjects are considered equal (based on the rules you specified), just collect the elements that occur in the list more than once:
private static List<TranObject> collectDuplicates(List<TranObject> list) {
List<TranObject> result = new ArrayList<TranObject>();
for(TranObject element : list) {
if(Collections.frequency(list, element) > 1)
result.add(element);
}
return result;
}
This will return all elements that have a duplicate.
Note: collectDuplicates does not return a unique list of the elements that are duplicated. Instead, it returns a list of each duplicated element (as required by OP's question).
Investigating a special case where some objects didn't equal as they should and came to this simple test case that simplifies my issue.
When running this with JUnit in Eclipse with jdk8u152 the last assertEquals fails, can anyone explain why?
It's something with Set/HashSet because if I change as,bs to be ArrayList's instead the final assertEquals goes through.
#Test
public void test()
{
String list = "list";
String object = "object";
String value = "value";
Map<String, Object> a = new HashMap<>();
Map<String, Object> b = new HashMap<>();
assertEquals(a, b);
Set<Object> as = new HashSet<>();
Set<Object> bs = new HashSet<>();
a.put(list, as);
b.put(list, bs);
assertEquals(a, b);
Map<String, Object> ao = new HashMap<>();
as.add(ao);
Map<String, Object> bo = new HashMap<>();
bs.add(bo);
assertEquals(a, b);
ao.put(object, value);
bo.put(object, value);
assertEquals(a, b);
}
You're mutating the elements of the sets. That leads to unspecified behaviour.
From the JavaDoc:
Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
You are adding ao and bo HashMaps to the HashSets as and bs.
Later you mutate ao and bo by putting a new entry in each of them.
This means that the hashCode that was used to place ao in as is no longer the current hashCode of ao, and the hashCode that was used to place bo in bs is no longer the current hashCode of bo.
As a result, AbstractSet's equals cannot locate the element of one Set in the other Set, so it concludes that as is not equal to bs. As a result a is not equal to b.
Here's the implementation of AbstractSet's equals. You can see that it uses containsAll, which in turns calls contains(), which relies on the hashCode of the searched element. Since that hashCode has changed after the element was added to the Set, contains() doesn't find the element.
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
If you mutate an element of a HashSet in a way that affects the result of equals or hashCode, you must remove the element from the HashSet prior to the update and add it again after the update.
Adding the following remove and add calls will cause a to be equal to b in the end:
....
assertEquals(a, b);
bs.remove (bo); // added
as.remove (ao); // added
ao.put(object, value);
bo.put(object, value);
as.add (ao); // added
bs.add (bo); // added
assertEquals(a, b);
That is because of the hascode implementation of HashMap which is basically x-or of key and value. If key or value is null then hascode will be zero. Hence all empty hashmaps will have hashcode as zero.
/*hashcode of HashMap*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
/*hashcode of object*/
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
Upon adding key value pairs the hashcode value changes.
According to Java HashMap documentation, put method replaces the previously contained value (if any): https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html#put-K-V-
Associates the specified value with the specified key in this map. If
the map previously contained a mapping for the key, the old value is
replaced.
The documentation however does not say what happens to the (existing) key when a new value is stored. Does the existing key get replaced or not? Or is the result undefined?
Consider the following example:
public class HashMapTest
{
private static class Key {
private String value;
private Boolean b;
private Key(String value, Boolean b) {
this.value = value;
this.b = b;
}
#Override
public int hashCode()
{
return value.hashCode();
}
#Override
public boolean equals(Object obj)
{
if (obj instanceof Key)
{
return value.equals(((Key)obj).value);
}
return false;
}
#Override
public String toString()
{
return "(" + value.toString() + "-" + b + ")";
}
}
public static void main(String[] arg) {
Key key1 = new Key("foo", true);
Key key2 = new Key("foo", false);
HashMap<Key, Object> map = new HashMap<Key, Object>();
map.put(key1, 1L);
System.out.println("Print content of original map:");
for (Entry<Key, Object> entry : map.entrySet()) {
System.out.println("> " + entry.getKey() + " -> " + entry.getValue());
}
map.put(key2, 2L);
System.out.println();
System.out.println("Print content of updated map:");
for (Entry<Key, Object> entry : map.entrySet()) {
System.out.println("> " + entry.getKey() + " -> " + entry.getValue());
}
}
}
When I execute the following code using Oracle jdk1.8.0_121, the following output is produced:
Print content of original map:
> (foo-true) -> 1
Print content of updated map:
> (foo-true) -> 2
Evidence says that (at least on my PC) the existing key does not get replaced.
Is this the expected/defined behaviour (where is it defined?) or is it just one among all the possible outcomes? Can I count on this behaviour to be consistent across all Java platforms/versions?
Edit: this question is not a duplicate of What happens when a duplicate key is put into a HashMap?. I am asking about the key (i.e. when you use multiple key instances that refer to the same logical key), not about the values.
From looking at the source, it doesn't get replaced, I'm not sure if it's guaranteed by the contract.
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
It finds the existing mapping and replaces the value, nothing is done with the new key, they should be the same and immutable, so even if a different implementation can replace the key it shouldn't matter.
You can't count on this behavior but you should write your code in a way that it won't matter.
When a new pair is added, the map uses hasCode,equals to check if the key already present in the map. If the key already exists the old value is replaced with a new one. The key itself remains unmodified.
Map<Integer,String> map = new HashMap<>();
map.put(1,"two");
System.out.println(map); // {1=two}
map.put(1,"one");
System.out.println(map); // {1=one}
map.put(2,"two");
System.out.println(map); // {1=one, 2=two}
There is an issue with your equals and hashCode contract. ke1 and key2 are identical according to your implementation:
#Override
public boolean equals(Object obj)
{
if (obj instanceof Key)
{
return value.equals(((Key)obj).value);
}
return false;
}
you need to compare Boolean b as well
Key other = (Key) obj;
return value.equals(other.value) && b.equals(other.b);
The same rule apples to hasCode
#Override
public int hashCode()
{
return value.hashCode();
}
return value.hashCode() + b.hashCode();
with these changes key1 and key2 are different
System.out.println(key1.equals(key2));
and the output for your map will be
> (foo-true) -> 1
> (foo-false) -> 2
It is not replaced - neither it should. If you know how a HashMap works and what hashCode and equals is (or more precisely how they are used) - the decision of not touching the Key is obvious.
When you put the other Key/Entry in the map for the second time, that key is first look-up in the map - according to hashCode/equals, so according to the map IFF keys have the same hashCode and are equal according to equals they are the same. If so, why replace it? Especially since if it would have been replaced, that might rigger additional operations or at least additional code to not trigger anything else if keys are equal.
Apparently the current HashSet implementation relies on this HashMap behaviour in order to be compliant to the HashSet documentation.
With that i mean that when you add a new element in an HashSet the documentation says that if you try to add an element in an HasSet that already contains the element, the HashSet is not changed and so the element is not substituted,
In the openjdk8 implementation the HashSet uses an HashMap keys to hold the values and in the HashSet.add method it calls the HashMap.put method to add the value, thus relying on the fact that the put method will not substitute the object
Although this still not a direct specification in the documentation and it's subject to variations in the JRE implementation, it probably provides a stronger
assurance that this will probably not change in the future
I have a class that has a Map<String, Object> field (the keys are Strings, the values are Objects that have correctly implemented the "equals" method for comparison).
I would like to override equals for this class in a way that only returns true if the Maps have equal mappings between keys and values.
Here is my attempt:
// Assumes that the Object values in maps have correctly implemented the equals method.
private boolean mapsEqual(Map<String, Object> attributes)
{
if (this.attributes_.keySet().size() != attributes.keySet().size() ||
this.attributes_.values().size() != attributes.values().size())
return false;
for (String key : attributes.keySet()) {
if (!this.attributes_.keySet().contains(key))
return false;
if (!this.attributes_.get(key).equals(attributes.get(key)))
return false;
}
return true;
}
However, this implementation fails when the same key is added more than once or when a key is removed from the map (the size tests fail for the values, as they count the duplicates and do not resize when values are removed.)
It seems that my situation should be common enough to find information that is relevant to my case, but I could not find any. Is there any legacy code or widely accepted solution to this situation? Any help or working solution is appreciated.
I am going to put this as an answer even though I am not 100% sure it solves your problem (but it's simply not gonna fit in a comment).
First off, to repeat my comments: The Map interface forbides that a map has duplicate keys or multiple values per key. Any proper implementation (e.g. java.util.HashMap) will therefore not allow this. Typically they will just replace the value if this happens.
Furthermore, the specification for equals, to me, seems to be doing what you want. Again, a proper implementation must live up to that specification.
So, what's the point here: If you are writing your own class that is implementing Map, then it simply cannot allow duplicate keys (methods like get wouldn't make sense anymore). If you are using a built-in implementation such as HashMap, it replaces the values anyway.
Now you are saying that you're experiencing size issues with keySet() and values(). I think you should add example code that will cause this behavior. The following works just fine for me:
Map<String, String> map = new HashMap<String, String>();
map.put("Foo", "Bar");
System.out.println(map.keySet().size()); // 1
System.out.println(map.values().size()); // 1
map.put("Foo", "Baz"); // the HashMap will merely replace the old value
System.out.println(map.keySet().size()); // still 1
System.out.println(map.values().size()); // still 1
Removing a key will, of course, change the size. I don't see how you consider this a problem based on your explanations so far.
As for equals, you may just want to look at the implementation for HashMap, which can be found here:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
Consider the following example:
Map<String, String> map1 = new HashMap<String, String>();
map1.put("Foo", "Bar");
Map<String, String> map2 = new HashMap<String, String>();
map2.put("Foo", "Bar");
System.out.println(map1.equals(map2)); // true
Firstly, you complain about your maps having duplicate keys... not possible (unless you're using a badly broken implementation).
This should do it:
public boolean equals(Object o) {
if (!(o instanceof MyClass))
return false;
MyClass that = (MyClass)o;
if (map.size() != that.map.size())
return false;
for (Map.Entry<String, Object> entry : map) {
Object a = entry.getValue();
Object b = that.map.get(entry.getKey());
if ((a == null ^ b == null) || (a == null && !a.equals(b)))
return false;
}
return true;
}
Is there a way to recursively traverse a HashMap so that value1 of key1 is actually the new key2 which returns value2 that again will be the next key3 and so on ... till it returns null? The logic is as follows:
hm.get(key)
hm.get(hm.get(key))
hm.get(hm.get(hm.get(key)))
......
I'm assuming this may be done through some recursion procedure? Please correct me if I were wrong. Thanks!
Is this the one you wanted procedure? it will return the ultimate value by traversing the hashmap:
Public Object traverseMap(Object key)
while(hm.get(key) != null){
key = hm.get(key);
}
return key;
}
If the hashmap would be set up this way (i.e. it contains a value which is also the key for another value) it would be possible. You could do that in a recursive method but a loop would be sufficient:
Object key = someInitialKey;
Object value = null;
do {
value = hm.get( key );
key = value;
} while( value != null );
Well, anyway, that's the (tail!) recursive version you asked for:
public class Qsdf {
public static Object traverseMap(Map m, Object key) {
return traverseMap(m, key, new HashSet());
}
public static Object traverseMap(Map m, Object key, Set traversed) {
if (key == null) { // first key has to be null
throw new NullPointerException();
}
traversed.add(key);
Object value = m.get(key);
if (traversed.contains(value)) { // added after Stephen C's comment on other answer
// cycle found, either throw exception, return null, or return key
return key;
}
return value != null ?
traverseMap(m, value, traversed) :
key; // I guess you want to return the last value that isn't also a key
}
public static void main(String[] args) {
final HashMap<Integer, Integer> m = new HashMap<Integer, Integer>();
m.put(0, 1);
m.put(1, 2);
m.put(3, 4);
m.put(2, 3);
final Object o = traverseMap(m, 0);
System.out.println(o);
}
}