Related
I have made various methods for someone to add a key which then includes various values from another created object.
I need to then allow a user to search using a method name which will then return all the people that match their search.
public Set findPerson(String aTrade)
{
Set<String> suitablePeople = new HashSet<>();
for (String test : this.contractors.keySet())
{
System.out.println(contractors.get(test));
if (contractors.containsValue(aTrade))
{
suitablePeople.add(test);
}
}
return suitablePeople;
}
I know this code is wrong but I'm just not sure how I can simply go through and find a value and return all the people that have this value within a range of values. For instance, their age, job, location.
Some assumptions, because your question is rather unclear:
contractors is a Map<String, ContractorData> field. Possibly ContractorData is some collection type (such as MyList<Contractor>), or named differently. The String represents a username.
aTrade is a string, and you want to search for it within the various ContractorData objects stored in your map. Then you want to return a collection of all username strings that are mapped to a ContractorData object that contains a trade that matches aTrade.
Whatever ContractorData might be, it has method containsValue(String) which returns true if the contractordata is considered a match. (If that was pseudocode and it's actually a List<String>, just .contains() would do the job. If it's something else you're going to have to elaborate in your question.
Then, there is no fast search available; maps allow you to do quick searches on their key (and not any particular property of their key, and not on their value or any particular property of their value). Thus, any search inherently implies you go through all the key/value mappings and check for each, individually, if it matches or not. If this is not an acceptable performance cost, you'd have to make another map, one that maps this property to something. This may have to be a multimap, and is considerably more complicated.
The performance cost is not important
Okay, then, just.. loop, but note that the .entrySet() gives you both key (which you'll need in case it's a match) and value (which you need to check if it matches), so that's considerably simpler.
var out = new ArrayList<String>();
for (var e : contracts.entrySet()) {
if (e.getValue().containsValue(aTrade)) out.add(e.getKey());
}
return out;
or if you prefer stream syntax:
return contracts.entrySet().stream()
.filter(e -> e.getValue().containsValue(aTrade))
.map(Map.Entry::getKey)
.toList();
The performance cost is important
Then it gets complicated. You'd need a single object that 'wraps' around at least two maps, and you need this because you want these maps to never go 'out of sync'. You need one map for each thing you want to have a find method for.
Thus, if you want a getTradesForUser(String username) as well as a findAllUsersWithTrade(String aTrade), you need two maps; one that maps users to trades, one that maps trades to users. In addition, you need the concept of a multimap: A map that maps one key to (potentially) more than one value.
You can use guava's MultiMaps (guava is a third party library with some useful stuff, such as multimaps), or, you roll your own, which is trivial:
given:
class ContractData {
private List<String> trades;
public boolean containsValue(String trade) {
return trades.contains(trade);
}
public List<String> getTrades() {
return trades;
}
}
then:
class TradesStore {
Map<String, ContractData> usersToTrades = new HashMap<>();
Map<String, List<String>> tradesToUsers = new HashMap<>();
public void put(String username, ContractData contract) {
usersToTrades.put(username, contract);
for (String trade : contract.getTrades()) {
tradesToUsers.computeIfAbsent(username, k -> new ArrayList<>()).add(username);
}
}
public Collection<String> getUsersForTrade(String trade) {
return tradesToUsers.getOrDefault(trade, List.of());
}
}
The getOrDefault method lets you specify a default value in case the trade isn't in the map. Thus, if you ask for 'get me all users which have trade [SOME_VALUE_NOBODY_IS_TRADING]', this returns an empty list (List.of() gives you an empty list), which is the right answer (null would be wrong - there IS an answer, and it is: Nobody. null is means: Unknown / irrelevant, and is therefore incorrect here).
The computeIfAbsent method just gets you the value associated with a key, but, if there is no such key/value mapping yet, you also give it the code required to make it. Here, we pass a function (k -> new ArrayList<>()) which says: just.. make a new arraylist first if I ask for a key that isn't yet in there, put it in the map, and then return that (k is the key, which we don't need to make the initial value).
Thus, computeIfAbsent and getOrDefault in combination make the concept of a multimap easy to write.
Assuming that your Map's values are instances of Contractor and the Contractor class has a Set<String> of trades (implied by the contains method call) and a getTrades() method that returns the list you could do it like this. Not certain what value the Map key would play in this.
get the values from the map and stream them.
filter only those Contractors that have the appropriate trade.
aggregate to a set of able contractors.
Set<Contractor> suitablePeople =
contractors.values()
.stream()
.filter(c->c.getTrades().contains(aTrade))
.collect(Collectors.toSet());
Note that a possible improvement would be to have a map like the following.
Map<String, Set<Contractors>> // where the key is the desired trade.
Then you could just get the Contractors with a single lookup up for each desired trade.
Set<Contractors> plumbers = mapByTrade.get("plumbers"); // all done.
Here is how you would set it up. The Contractor class is at the end. It takes a name and a variable array of trades.
Set<Contractor> contractors = Set.of(
new Contractor("Acme", "plumbing", "electrical", "masonry", "roofing", "carpet"),
new Contractor("Joe's plumbing", "plumbing"),
new Contractor("Smith", "HVAC", "electrical"),
new Contractor("Ace", "electrical"));
Then, iterate over the list of contractors to create the map. Then those are grouped by trade, and each contractor that matches is put in the associated set for that trade.
Map<String,Set<Contractor>> mapByTrade = new HashMap<>();
for (Contractor c : contractors) {
for (String trade : c.getTrades()) {
mapByTrade.computeIfAbsent(trade, v->new HashSet<>()).add(c);
}
}
And here it is in action.
Set<Contractor> plumbers = mapByTrade.get("plumbing");
System.out.println(plumbers);
System.out.println(mapByTrade.get("electrical"));
System.out.println(mapByTrade.get("HVAC"));
prints
[Acme, Joe's plumbing]
[Ace, Acme, Smith]
[Smith]
And here is the Contractor class.
class Contractor {
private Set<String> trades;
private String name;
#Override
public int hashCode() {
return name.hashCode();
}
#Override
public boolean equals(Object ob) {
if (ob == name) {
return true;
}
if (ob == null) {
return false;
}
if (ob instanceof Contractor) {
return ((Contractor)ob).name.equals(this.name);
}
return false;
}
public Contractor(String name, String...trades) {
this.name = name;
this.trades = new HashSet<>(Arrays.asList(trades));
}
public Set<String> getTrades() {
return trades;
}
#Override
public String toString() {
return name;
}
}
Is it bad practice to use mutable objects as Hashmap keys? What happens when you try to retrieve a value from a Hashmap using a key that has been modified enough to change its hashcode?
For example, given
class Key
{
int a; //mutable field
int b; //mutable field
public int hashcode()
return foo(a, b);
// setters setA and setB omitted for brevity
}
with code
HashMap<Key, Value> map = new HashMap<Key, Value>();
Key key1 = new Key(0, 0);
map.put(key1, value1); // value1 is an instance of Value
key1.setA(5);
key1.setB(10);
What happens if we now call map.get(key1)? Is this safe or advisable? Or is the behavior dependent on the language?
It has been noted by many well respected developers such as Brian Goetz and Josh Bloch that :
If an object’s hashCode() value can change based on its state, then we
must be careful when using such objects as keys in hash-based
collections to ensure that we don’t allow their state to change when
they are being used as hash keys. All hash-based collections assume
that an object’s hash value does not change while it is in use as a
key in the collection. If a key’s hash code were to change while it
was in a collection, some unpredictable and confusing consequences
could follow. This is usually not a problem in practice — it is not
common practice to use a mutable object like a List as a key in a
HashMap.
This is not safe or advisable. The value mapped to by key1 can never be retrieved. When doing a retrieval, most hash maps will do something like
Object get(Object key) {
int hash = key.hashCode();
//simplified, ignores hash collisions,
Entry entry = getEntry(hash);
if(entry != null && entry.getKey().equals(key)) {
return entry.getValue();
}
return null;
}
In this example, key1.hashcode() now points to the wrong bucket of the hash table, and you will not be able to retrieve value1 with key1.
If you had done something like,
Key key1 = new Key(0, 0);
map.put(key1, value1);
key1.setA(5);
Key key2 = new Key(0, 0);
map.get(key2);
This will also not retrieve value1, as key1 and key2 are no longer equal, so this check
if(entry != null && entry.getKey().equals(key))
will fail.
Hash maps use hash code and equality comparisons to identify a certain key-value pair with a given key. If the has map keeps the key as a reference to the mutable object, it would work in the cases where the same instance is used to retrieve the value. Consider however, the following case:
T keyOne = ...;
T keyTwo = ...;
// At this point keyOne and keyTwo are different instances and
// keyOne.equals(keyTwo) is true.
HashMap myMap = new HashMap();
myMap.push(keyOne, "Hello");
String s1 = (String) myMap.get(keyOne); // s1 is "Hello"
String s2 = (String) myMap.get(keyTwo); // s2 is "Hello"
// because keyOne equals keyTwo
mutate(keyOne);
s1 = myMap.get(keyOne); // returns "Hello"
s2 = myMap.get(keyTwo); // not found
The above is true if the key is stored as a reference. In Java usually this is the case. In .NET for instance, if the key is a value type (always passed by value), the result will be different:
T keyOne = ...;
T keyTwo = ...;
// At this point keyOne and keyTwo are different instances
// and keyOne.equals(keyTwo) is true.
Dictionary myMap = new Dictionary();
myMap.Add(keyOne, "Hello");
String s1 = (String) myMap[keyOne]; // s1 is "Hello"
String s2 = (String) myMap[keyTwo]; // s2 is "Hello"
// because keyOne equals keyTwo
mutate(keyOne);
s1 = myMap[keyOne]; // not found
s2 = myMap[keyTwo]; // returns "Hello"
Other technologies might have other different behaviors. However, almost all of them would come to a situation where the result of using mutable keys is not deterministic, which is very very bad situation in an application - a hard to debug and even harder to understand.
If key’s hash code changes after the key-value pair (Entry) is stored in HashMap, the map will not be able to retrieve the Entry.
Key’s hashcode can change if the key object is mutable. Mutable keys in HahsMap can result in data loss.
This will not work. You are changing the key value, so you are basically throwing it away. Its like creating a real life key and lock, and then changing the key and trying to put it back in the lock.
As others explained, it is dangerous.
A way to avoid that is to have a const field giving explicitly the hash in your mutable objects (so you would hash on their "identity", not their "state"). You might even initialize that hash field more or less randomly.
Another trick would be to use the address, e.g. (intptr_t) reinterpret_cast<void*>(this) as a basis for hash.
In all cases, you have to give up hashing the changing state of the object.
There are two very different issues that can arise with a mutable key depending on your expectation of behavior.
First Problem: (probably most trivial--but hell it gave me problems that I didn't think about!)
You are attempting to place key-value pairs into a map by updating and modifying the same key object. You might do something like Map<Integer, String> and simply say:
int key = 0;
loop {
map.put(key++, newString);
}
I'm reusing the "object" key to create a map. This works fine in Java because of autoboxing where each new value of key gets autoboxed to a new Integer object. What would not work is if I created my own (mutable) Integer object:
MyInteger {
int value;
plusOne(){
value++;
}
}
Then tried the same approach:
MyInteger key = new MyInteger(0);
loop{
map.put(key.plusOne(), newString)
}
My expectation is that, for instance, I map 0 -> "a" and 1 -> "b". In the first example, if I change int key = 0, the map will (correctly) give me "a". For simplicity let's assume MyInteger just always returns the same hashCode() (if you can somehow manage to create unique hashCode values for all possible states of an object, this will not be an issue, and you deserve an award). In this case, I call 0 -> "a", so now the map holds my key and maps it to "a", I then modify key = 1 and try to put 1 -> "b". We have a problem! The hashCode() is the same, and the only key in the HashMap is my MyInteger key object which has just been modified to be equal to 1, so It overwrites that key's value so that now, instead of a map with 0 -> "a" and 1 -> "b", I have 1 -> "b" only! Even worse, if I change back to key = 0, the hashCode points to 1 -> "b", but since the HashMap's only key is my key object, it satisfied the equality check and returns "b", not "a" as expected.
If, like me, you fall prey to this type of issue, it's incredibly difficult to diagnose. Why? Because if you have a decent hashCode() function it will generate (mostly) unique values. The hash value will largely take care of the inequality problem when structuring the map but if you have enough values, eventually you'll get a collision on the hash value and then you get unexpected and largely inexplicable results. The resultant behavior is that it works for small runs but fails for larger ones.
Advice:
To find this type of issue, modify the hashCode() method, even trivially (i.e. = 0--obviously when doing this, keep in mind that the hash values should be the same for two equal objects*), and see if you get the same results--because you should and if you don't, there's likely a semantic error with your implementation that's using a hash table.
*There should be no danger (if there is--you have a semantic problem) in always returning 0 from a hashCode() (although it would defeat the purpose of a Hash Table). But that's sort of the point: the hashCode is a "quick and easy" equality measure that's not exact. So two very different objects could have the same hashCode() yet not be equal. On the other hand, two equal objects must always have the same hashCode() value.
p.s. In Java, from my understanding, if you do such a terrible thing (as have many hashCode() collisions), it will start using a red-black-tree as opposed to ArrayList. So when you expect O(1) lookup, you'll get O(log(n))--which is better than the ArrayList which would give O(n).
Second Problem:
This is the one that most others seem to be focusing on, so I'll try to be brief. In this use case, I try to map a key-value pair and then I do some work on the key and then want to come back and get my value.
Expectation: key -> value is mapped, I then modify key and try to get(key). I expect that will give me value.
It seems kind of obvious to me that this wouldn't work but I'm not above having tried to use things like Collections as a key before (and quite quickly realizing it doesn't work). It doesn't work because it's quite likely that the hash value of key has changed so you won't even be looking in the correct bucket.
This is why it's very inadvisable to use collections as keys. I would assume, if you were doing this, you're trying to establish a many-to-one relationship. So I have a class (as in teaching) and I want two groups to do two different projects. What I want is that given a group, what is their project? Simple, I divide the class in two, and I have group1 -> project1 and group2 -> project2. But wait! A new student arrives so I place them in group1. The problem is that group1 has now been modified and likely its hash value has changed, therefore trying to do get(group1) is likely to fail because it will look in a wrong or non-existent bucket of the HashMap.
The obvious solution to the above is to chain things--instead of using the groups as keys, give them labels (that don't change) that point to the group and therefore the project: g1 -> group1 and g1 -> project1, etc.
p.s.
Please make sure to define a hashCode() and equals(...) method for any object you expect to use as a key (eclipse and, I'm assuming, most IDE's can do this for you).
Code Example:
Here is a class which exhibits the two different "problem" behaviors. In this case, I attempt to map 0 -> "a", 1 -> "b", and 2 -> "c" (in each case). In the first problem, I do that by modifying the same object, in the second problem, I use unique objects, and in the second problem "fixed" I clone those unique objects. After that I take one of the "unique" keys (k0) and modify it to attempt to access the map. I expect this will give me a, b, c and null when the key is 3.
However, what happens is the following:
map.get(0) map1: 0 -> null, map2: 0 -> a, map3: 0 -> a
map.get(1) map1: 1 -> null, map2: 1 -> b, map3: 1 -> b
map.get(2) map1: 2 -> c, map2: 2 -> a, map3: 2 -> c
map.get(3) map1: 3 -> null, map2: 3 -> null, map3: 3 -> null
The first map ("first problem") fails because it only holds a single key, which was last updated and placed to equal 2, hence why it correctly returns "c" when k0 = 2 but returns null for the other two (the single key doesn't equal 0 or 1). The second map fails twice: the most obvious is that it returns "b" when I asked for k0 (because it's been modified--that's the "second problem" which seems kind of obvious when you do something like this). It fails a second time when it returns "a" after modifying k0 = 2 (which I would expect to be "c"). This is more due to the "first problem": there's a hash code collision and the tiebreaker is an equality check--but the map holds k0, which it (apparently for me--could theoretically be different for someone else) checked first and thus returned the first value, "a" even though had it kept checking, "c" would have also been a match. Finally, the 3rd map works perfectly because I'm enforcing that the map holds unique keys no matter what else I do (by cloning the object during insertion).
I want to make clear that I agree, cloning is not a solution! I simply added that as an example of why a map needs unique keys and how enforcing unique keys "fixes" the issue.
public class HashMapProblems {
private int value = 0;
public HashMapProblems() {
this(0);
}
public HashMapProblems(final int value) {
super();
this.value = value;
}
public void setValue(final int i) {
this.value = i;
}
#Override
public int hashCode() {
return value % 2;
}
#Override
public boolean equals(final Object o) {
return o instanceof HashMapProblems
&& value == ((HashMapProblems) o).value;
}
#Override
public Object clone() {
return new HashMapProblems(value);
}
public void reset() {
this.value = 0;
}
public static void main(String[] args) {
final HashMapProblems k0 = new HashMapProblems(0);
final HashMapProblems k1 = new HashMapProblems(1);
final HashMapProblems k2 = new HashMapProblems(2);
final HashMapProblems k = new HashMapProblems();
final HashMap<HashMapProblems, String> map1 = firstProblem(k);
final HashMap<HashMapProblems, String> map2 = secondProblem(k0, k1, k2);
final HashMap<HashMapProblems, String> map3 = secondProblemFixed(k0, k1, k2);
for (int i = 0; i < 4; ++i) {
k0.setValue(i);
System.out.printf(
"map.get(%d) map1: %d -> %s, map2: %d -> %s, map3: %d -> %s",
i, i, map1.get(k0), i, map2.get(k0), i, map3.get(k0));
System.out.println();
}
}
private static HashMap<HashMapProblems, String> firstProblem(
final HashMapProblems start) {
start.reset();
final HashMap<HashMapProblems, String> map = new HashMap<>();
map.put(start, "a");
start.setValue(1);
map.put(start, "b");
start.setValue(2);
map.put(start, "c");
return map;
}
private static HashMap<HashMapProblems, String> secondProblem(
final HashMapProblems... keys) {
final HashMap<HashMapProblems, String> map = new HashMap<>();
IntStream.range(0, keys.length).forEach(
index -> map.put(keys[index], "" + (char) ('a' + index)));
return map;
}
private static HashMap<HashMapProblems, String> secondProblemFixed(
final HashMapProblems... keys) {
final HashMap<HashMapProblems, String> map = new HashMap<>();
IntStream.range(0, keys.length)
.forEach(index -> map.put((HashMapProblems) keys[index].clone(),
"" + (char) ('a' + index)));
return map;
}
}
Some Notes:
In the above it should be noted that map1 only holds two values because of the way I set up the hashCode() function to split odds and evens. k = 0 and k = 2 therefore have the same hashCode of 0. So when I modify k = 2 and attempt to k -> "c" the mapping k -> "a" gets overwritten--k -> "b" is still there because it exists in a different bucket.
Also there are a lot of different ways to examine the maps in the above code and I would encourage people that are curious to do things like print out the values of the map and then the key to value mappings (you may be surprised by the results you get). Do things like play with changing the different "unique" keys (i.e. k0, k1, and k2), try changing the single key k. You could also see how even the secondProblemFixed isn't actually fixed because you could also gain access to the keys (for instance via Map::keySet) and modify them.
I won't repeat what others have said. Yes, it's inadvisable. But in my opinion, it's not overly obvious where the documentation states this.
You can find it on the JavaDoc for the Map interface:
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
Behaviour of a Map is not specified if value of an object is changed in a manner that affects equals comparision while object(Mutable) is a key. Even for Set also using mutable object as key is not a good idea.
Lets see a example here :
public class MapKeyShouldntBeMutable {
/**
* #param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Map<Employee,Integer> map=new HashMap<Employee,Integer>();
Employee e=new Employee();
Employee e1=new Employee();
Employee e2=new Employee();
Employee e3=new Employee();
Employee e4=new Employee();
e.setName("one");
e1.setName("one");
e2.setName("three");
e3.setName("four");
e4.setName("five");
map.put(e, 24);
map.put(e1, 25);
map.put(e2, 26);
map.put(e3, 27);
map.put(e4, 28);
e2.setName("one");
System.out.println(" is e equals e1 "+e.equals(e1));
System.out.println(map);
for(Employee s:map.keySet())
{
System.out.println("key : "+s.getName()+":value : "+map.get(s));
}
}
}
class Employee{
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public boolean equals(Object o){
Employee e=(Employee)o;
if(this.name.equalsIgnoreCase(e.getName()))
{
return true;
}
return false;
}
public int hashCode() {
int sum=0;
if(this.name!=null)
{
for(int i=0;i<this.name.toCharArray().length;i++)
{
sum=sum+(int)this.name.toCharArray()[i];
}
/*System.out.println("name :"+this.name+" code : "+sum);*/
}
return sum;
}
}
Here we are trying to add mutable object "Employee" to a map. It will work good if all keys added are distinct.Here I have overridden equals and hashcode for employee class.
See first I have added "e" and then "e1". For both of them equals() will be true and hashcode will be same. So map sees as if the same key is getting added so it should replace the old value with e1's value. Then we have added e2,e3,e4 we are fine as of now.
But when we are changing the value of an already added key i.e "e2" as one ,it becomes a key similar to one added earlier. Now the map will behave wired. Ideally e2 should replace the existing same key i.e e1.But now map takes this as well. And you will get this in o/p :
is e equals e1 true
{Employee#1aa=28, Employee#1bc=27, Employee#142=25, Employee#142=26}
key : five:value : 28
key : four:value : 27
key : one:value : 25
key : one:value : 25
See here both keys having one showing same value also. So its unexpected.Now run the same programme again by changing e2.setName("diffnt"); which is e2.setName("one"); here ...Now the o/p will be this :
is e equals e1 true
{Employee#1aa=28, Employee#1bc=27, Employee#142=25, Employee#27b=26}
key : five:value : 28
key : four:value : 27
key : one:value : 25
key : diffnt:value : null
So by adding changing the mutable key in a map is not encouraged.
To make the answer compact:
The root cause is that HashMap calculates an internal hash of the user's key object hashcode only once and stores it inside for own needs.
All other operations for data navigation inside the map are doing by this pre-calculated internal hash.
So if you change the hashcode of the key object (mutate) it will be still stored nicely inside the map with the changed key object's hashcode (you could even observe it via HashMap.keySet() and see the altered hashcode).
But HashMap internal hash will not be recalculated of course and it will be the old stored one and the map won't be able to locate your data by the provided mutated key object new hashcode. (e.g. by HashMap.get() or HashMap.containsKey()).
Your key-value pairs will be still inside the map but to get it back you will need that old hash code value that was given when you put your data into the map.
Notice that you also will be unable to get data back by the mutated key object taken right from the HashMap.keySet().
I'm new to java stream API.
I have 2 lists, and if both their internal object ID matches wants to put some attributes to MAP.
Below is the implementation.
List<LookupMstEntity> examTypeDetails; //This list contains values init.
List<MarksMstEntity> marksDetailList; //This list contains values init.
//FYI above entities have lombok setter, getter, equals & hashcode.
Map<Long, Integer> marksDetailMap = new HashMap<>();
//need below implementation to changed using java 8.
for (LookupMstEntity examType : examTypeDetails) {
for (MarksMstEntity marks : marksDetailList) {
if (examType.getLookupId() == marks.getExamTypeId())
marksDetailMap.put(examType.getLookupId(), marks.getMarks());
}
}
Creating a set of lookupIds Set<Long> ids helps you to throw away duplicate values and to get rid of unnecessary checks.
Then you can filter marksDetailList accordingly with examTypeId values:
filter(m -> ids.contains(m.getExamTypeId()))
HashSet contains() method has constant time complexity O(1).
Try this:
Set<Long> ids = examTypeDetails.stream().map(LookupMstEntity::getLookupId)
.collect(Collectors.toCollection(HashSet::new));
Map<Long, Integer> marksDetailMap = marksDetailList.stream().filter(m -> ids.contains(m.getExamTypeId()))
.collect(Collectors.toMap(MarksMstEntity::getExamTypeId, MarksMstEntity::getMarks));
As long as you are looking for these with equal ID, it doesn't matter which ID you use then. I suggest you to start streaming the marksDetailList first since you need its getMarks(). The filtering method searches if there is a match in IDs. If so, collect the required key-values to the map.
Map<Long, Integer> marksDetailMap = marksDetailList.stream() // List<MarksMstEntity>
.filter(mark -> examTypeDetails.stream() // filtered those where ...
.map(LookupMstEntity::getLookupId) // ... the lookupId
.anyMatch(id -> id == mark.getExamTypeId())) // ... is present in the list
.collect(Collectors.toMap( // collected to Map ...
MarksMstEntity::getExamTypeId, // ... with ID as a key
MarksMstEntity::getMarks)); // ... and marks as a value
The .map(..).anyMatch(..) can be shrink into one:
.anyMatch(exam -> exam.getLookupId() == mark.getExamTypeId())
As stated in the comments, I'd rather go for the for-each iteration as you have already used for sake of brevity.
An observation:
First, your resultant map indicates that there can only be one match for ID types (otherwise you would have duplicate keys and the value would need to be a List or some other way of merging duplicate keys, not an Integer. So when you find the first one and insert it in the map, break out of the inner loop.
for (LookupMstEntity examType : examTypeDetails) {
for (MarksMstEntity marks : marksDetailList) {
if (examType.getLookupId() == marks.getExamTypeId()) {
marksDetailMap.put(examType.getLookupId(),
marks.getMarks());
// no need to keep on searching for this ID
break;
}
}
}
Also if your two classes were related by a parent class or a shared interface that had access to to the id, and the two classes were considered equal based on that id, then you could do something similar to this.
for (LookupMstEntity examType : examTypeDetails) {
int index = marksDetailList.indexOf(examType);
if (index > 0) {
marksDetailMap.put(examType.getLookupId(),
marksDetaiList.get(index).getMarks());
}
}
Of course the burden of locating the index is still there but it is now under the hood and you are relieved of that responsibility.
You can do it with O(N) time complexity using HashMap, first convert two lists into Map<Integer, LookupMstEntity> and Map<Integer, MarksMstEntity> with id as key
Map<Integer, LookupMstEntity> examTypes = examTypeDetails.stream()
.collect(Collectors.toMap(LookupMstEntity::getLookupId,
Function.identity()) //make sure you don't have any duplicate LookupMstEntity objects with same id
Map<Integer, MarksMstEntity> marks = marksDetailList.stream()
.collect(Collectors.toMap(MarksMstEntity::getExamTypeId,
Function.identity()) // make sure there are no duplicates
And then stream the examTypes map and then collect into map if MarksMstEntity exists with same id in marks map
Map<Integer, Integer> result = examTypes.entrySet()
.stream()
.map(entry->new AbstractMap.SimpleEntry<Integer, MarksMstEntity>(entry.getKey(), marks.get(entry.getKey())))
.filter(entry->entry.getValue()!=null)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
My sample request
{
"requestModel":{
"CUSTID": "100"
},
"returnParameters":[
{
"name":"NETWORK/NETID",
"datatype":"String",
"order":"asc",
"sequence":1
},
{
"name":"INFODATA/NAME",
"datatype":"String",
"order":"asc",
"sequence":1
},
{
"name":"SOURCE/SYSTEM",
"datatype":"int",
"order":"asc",
"sequence":2
},
]
}
Sample Response
Below is my dynamically generated Map format of json response[Response parameters will be different each time based on the request params],
"responseModel":{
"documents": [
{
"NETWORK":[
{"NETID":"1234"},
{"ACT":"300"}
],
"SOURCE": {
"SYSTEM":"50"
},
"INFODATA":{
"NAME":"PHIL"
}
},
{
"NETWORK":[
{"NETID":"1234"},
{"ACT":"300"}
],
"SOURCE": {
"SYSTEM":"100"
},
"INFODATA":{
"NAME":"PHIL"
}
}
]
}
Problem Statement
I need to do multi level sorting based on the "returnParameters" in the request which is dynamic...
"order" indicates ascending (or) descending and sequence indicates the the priority for ordering like (group by in sql query)
Code
Map<String,Object> documentList = new HashMap<String,Object>();
JSONObject jsonObject= new JSONObject(response.getContent());
response.getContent() -> is nothing but it contains the above json response in Map format.
Now I converting the map to list of json object
JSONArray jsonArray= (JSONArray)jsonObject.get("documents");
ArrayList<JSONObject> list = new ArrayList<>();
for(int i=0;i<jsonArray.length();i++){
list.add((JSONObject) jsonArray.get(i));
}
Collections.sort(list, new ResponseSorter());
public class ResponseSorter implements Comparator<JSONObject> {
#Override
public int compare(JSONObject o1,JSONObject o2){
String s1= (String)((JSONObject) o1.get("NETWORK")).get("NETID");
String s2= (String)((JSONObject) o2.get("NETWORK")).get("NETID");
int i1=Integer.parseInt(s1);
int i2=Integer.parseInt(s2);
return i1-i2;
}
}
I'm stuck here to proceed further. Created one for Integer comparator, .Should I create for each dataType? also
I need to dynamically construct the composite comparator by parsing the "retunrParameters" , below sample is hard coded, how to create dynamically??
(String)((JSONObject) o1.get("NETWORK")).get("NETID"); -> this should be dynamically framed , since "returnParameters" are also dynamic in nature.[NETWORK & NETID may not be come in another request],so my comparator should be capable enough to frame the keys in runtime
Would anyone able to assist me to create composite comparator in runtime for sorting?
NOTE:- Java Pojo cannot be created as the response is dynamic nature
In your case a simple comparator that's provided with the sort parameters might be easier to understand than a bunch of nested comparators.
Basically you'd do something like this:
class ReturnParameterComparator implements Comparator<JSONObject> {
private List<ReturnParameter> params; //set via constructor
public int compare( JSONObject left, JSONObject right) {
int result = 0;
for( ReturnParameter p : params ) {
//how exactly you get those values depends on the actual structure of your data and parameters
String leftValueStr = left.get( p );
String rightValueStr = right.get( p );
switch( p.datatype ) {
case "String":
result = String.compare( leftValueStr, rightValueStr );
break;
case "int":
//convert and then compare - I'll leave the rest for you
}
//invert the result if the order is descending
if( "desc".equals(p.order ) {
result += -1;
}
//the values are not equal so return the order, otherwise continue with the next parameter
if( result != 0 ) {
return result;
}
}
//at this point all values are to be considered equal, otherwise we'd have returned already (from the loop body)
return 0;
}
}
Note that this is just a stub to get you started. You'll need to add quite a few things:
how to correctly use the parameters to extract the values from the json objects
how to convert the data based on the type
how to handle nulls, missing or incompatible data (e.g. if a value should be sorted as "int" but it can't be parsed)
Adding all those would be way too much for the scope of this question and depends on your data and requirements anyway.
EDITED after additional questions in comments and additional info in description
You have a couple of steps you need to do here to get to the solution:
You want to have the sorting be dynamic based on the value of the property sequence in the request. So you need to parse the names of those returnParameters and put them in order. Below I map them to a List where each String[] has the name and order (asc/desc). The list will be ordered using the value of sequence:
List<String[]> sortParams = params.stream() // params is a List<JSONObject>
.filter(json -> json.containsKey("sequence")) // filter those that have "sequence" attribute
.sorted( sequence ) // sorting using Comparator called sequence
.map(jsonObj -> new String[]{jsonObj.get("name").toString(), jsonObj.get("order").toString()} )
.collect(Collectors.toList());
Before this you'll map the objects in the returnParameters array in the request to a List first.Then the stream is processed by 1. filtering the JSONObjects to only keep those that have prop sequence, 2. sorting the JSONObjects using comparator below. 3. from each JSONObject get "name" & "order" and put them in a String[], 4. generate a list with those Arrays. This list will be ordered in the order of attributes with priority 1 first, then priority 2, etc, so it will be ordered in the same way you want the JSONObjects ordered in the end.
Comparator<JSONObject> sequence = Comparator.comparingInt(
jsonObj -> Integer.valueOf( jsonObj.get("sequence").toString() )
);
So for your example, sortParams would look like: List( String[]{"NETWORK/NETID", "asc"}, String[]{""INFODATA/NAME", "asc"}, String[]{"SOURCE/SYSTEM", "asc"} )
Then you need to write a method that takes two params: a JSONObject and a String (the path to the property) and returns the value of that property. Originally I advised you to use JSONAware interface and then figure out the sub-class, but let's forget about that for now.
I am not going to write this method for you. Just keep in mind that .get(key) method of JSON.Simple always yields an Object. Write a method with this signature:
public String findSortValue(JSONObject doc, String path){
// split the path
// find the parent
// cast it (parent was returned as an Object of type Object)
// find the child
return value;
}
Write a generic individual comparator (that compares values of just one sort attribute at a time) and figures out if it's an Int, Date or regular String. I would write this as a regular method so it'll be easier to combine everything later on. Since you had so many questions about this I've made an example:
int individualComparator(String s1, String s2){
int compResult = 0;
try{
int numeric1 = Integer.parseInt(s1);
int numeric2 = Integer.parseInt(s2);
compResult = numeric1 - numeric2; // if this point was reached both values could be parsed
} catch (NumberFormatException nfe){
// if the catch block is reached they weren't numeric
try{
DateTime date1 = DateTime.parse(s1);
DateTime date2 = DateTime.parse(s2);
compResult = date1.compareTo(date2); // compareTo method of joda.time, the library I'm using
} catch (IllegalArgumentException iae){
//if this catch block is reached they weren't dates either
compResult = s1.compareTo(s2);
}
}
return compResult;
};
Write an overall Comparator that combines everything
Comparator<JSONObject> overAllComparator = (jsonObj1, jsonObj2) -> {
List<String[]> sortValuesList = sortParams.stream()
.map(path -> new String[]{ findValueByName(jsonObj1, path), findValueByName(jsonObj2, path) } )
.collect(Collectors.toList());
//assuming we always have 3 attributes to sort on
int comp1 = individualComparator(sortValuesList.get(0)[0], sortValuesList.get(0)[1]);
int comp2 = individualComparator(sortValuesList.get(1)[0], sortValuesList.get(1)[1]);
int comp3 = individualComparator(sortValuesList.get(2)[0], sortValuesList.get(2)[1]);
int result = 0;
if (comp1 != 0){
result = comp1;
} else if (comp2 != 0){
result = comp2;
} else{
result = comp3;
}
return result;
};
This Comparator is written lambda-style, for more info https://www.mkyong.com/java8/java-8-lambda-comparator-example/ .
First it takes the ordered list of sortParams we made in step 1 and for each returns an array where position 0 has the value for jsonObj1, and position 1 has the value for jsonObj2 and collects it in sortValuesList. Then for each attribute to sort on, it get the result of the individualComparatormethod. Then it goes down the line and returns as result of the overall comparison the first one that doesn't result in 0 (when a comparator results in 0 both values are equal).
The only thing that's missing now is the asc/desc value from the request. You can add that by chainingint comp1 = individualComparator(sortValuesList.get(0)[0], sortValuesList.get(0)[1]); with a simple method that takes an int & a String and multiplies the int by -1 if the String equals "desc". (Remember that in sortParams we added the value for order on position 1 of the array).
Because the first list we made, sortParams was ordered based on the priority indicated in the request, and we always did evertything in the order of this list, the result is a multi-sort in this order. It is generic & will be determined dynamically by the contents of returnParams in the request. You can apply it to your list of JSONObjects by using Collections.sort()
My suggestion: learn about:
Comparator.comparing which allows you to build your comparator by specifying the key extractor
Comparator.thanComparing which allows you to chain multiple comparators. The comparators later in the chain are called only if predecessors say the objects are equal
A tutorial if you need one: https://www.baeldung.com/java-8-comparator-comparing
If I pass the same key multiple times to HashMap’s put method, what happens to the original value? And what if even the value repeats? I didn’t find any documentation on this.
Case 1: Overwritten values for a key
Map mymap = new HashMap();
mymap.put("1","one");
mymap.put("1","not one");
mymap.put("1","surely not one");
System.out.println(mymap.get("1"));
We get surely not one.
Case 2: Duplicate value
Map mymap = new HashMap();
mymap.put("1","one");
mymap.put("1","not one");
mymap.put("1","surely not one");
// The following line was added:
mymap.put("1","one");
System.out.println(mymap.get("1"));
We get one.
But what happens to the other values? I was teaching basics to a student and I was asked this. Is the Map like a bucket where the last value is referenced (but in memory)?
By definition, the put command replaces the previous value associated with the given key in the map (conceptually like an array indexing operation for primitive types).
The map simply drops its reference to the value. If nothing else holds a reference to the object, that object becomes eligible for garbage collection. Additionally, Java returns any previous value associated with the given key (or null if none present), so you can determine what was there and maintain a reference if necessary.
More information here: HashMap Doc
You may find your answer in the javadoc of Map#put(K, V) (which actually returns something):
public V put(K key,
V value)
Associates the specified value with the specified key in this map
(optional operation). If the map
previously contained a mapping for
this key, the old value is replaced by
the specified value. (A map m is said
to contain a mapping for a key k if
and only if m.containsKey(k) would
return true.)
Parameters:
key - key with which the specified value is to be associated.
value - value to be associated with the specified key.
Returns:
previous value associated with specified key, or null if there was no
mapping for key. (A null return can also indicate that the map previously associated null with the specified key, if the implementation supports null values.)
So if you don't assign the returned value when calling mymap.put("1", "a string"), it just becomes unreferenced and thus eligible for garbage collection.
it's Key/Value feature and you could not to have duplicate key for several values because when you want to get the actual value which one of values is belong to entered keyin your example when you want to get value of "1" which one is it ?!that's reasons to have unique key for every value but you could to have a trick by java standard lib :
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class DuplicateMap<K, V> {
private Map<K, ArrayList<V>> m = new HashMap<>();
public void put(K k, V v) {
if (m.containsKey(k)) {
m.get(k).add(v);
} else {
ArrayList<V> arr = new ArrayList<>();
arr.add(v);
m.put(k, arr);
}
}
public ArrayList<V> get(K k) {
return m.get(k);
}
public V get(K k, int index) {
return m.get(k).size()-1 < index ? null : m.get(k).get(index);
}
}
and you could to use it in this way:
public static void main(String[] args) {
DuplicateMap<String,String> dm=new DuplicateMap<>();
dm.put("1", "one");
dm.put("1", "not one");
dm.put("1", "surely not one");
System.out.println(dm.get("1"));
System.out.println(dm.get("1",1));
System.out.println(dm.get("1", 5));
}
and result of prints are :
[one, not one, surely not one]
not one
null
It replaces the existing value in the map for the respective key. And if no key exists with the same name then it creates a key with the value provided.
eg:
Map mymap = new HashMap();
mymap.put("1","one");
mymap.put("1","two");
OUTPUT
key = "1", value = "two"
So, the previous value gets overwritten.
The prior value for the key is dropped and replaced with the new one.
If you'd like to keep all the values a key is given, you might consider implementing something like this:
import org.apache.commons.collections.MultiHashMap;
import java.util.Set;
import java.util.Map;
import java.util.Iterator;
import java.util.List;
public class MultiMapExample {
public static void main(String[] args) {
MultiHashMap mp=new MultiHashMap();
mp.put("a", 10);
mp.put("a", 11);
mp.put("a", 12);
mp.put("b", 13);
mp.put("c", 14);
mp.put("e", 15);
List list = null;
Set set = mp.entrySet();
Iterator i = set.iterator();
while(i.hasNext()) {
Map.Entry me = (Map.Entry)i.next();
list=(List)mp.get(me.getKey());
for(int j=0;j<list.size();j++)
{
System.out.println(me.getKey()+": value :"+list.get(j));
}
}
}
}
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.
To your question whether the map was like a bucket: no.
It's like a list with name=value pairs whereas name doesn't need to be a String (it can, though).
To get an element, you pass your key to the get()-method which gives you the assigned object in return.
And a Hashmap means that if you're trying to retrieve your object using the get-method, it won't compare the real object to the one you provided, because it would need to iterate through its list and compare() the key you provided with the current element.
This would be inefficient. Instead, no matter what your object consists of, it calculates a so called hashcode from both objects and compares those. It's easier to compare two ints instead of two entire (possibly deeply complex) objects. You can imagine the hashcode like a summary having a predefined length (int), therefore it's not unique and has collisions. You find the rules for the hashcode in the documentation to which I've inserted the link.
If you want to know more about this, you might wanna take a look at articles on javapractices.com and technofundo.com
regards
Maps from JDK are not meant for storing data under duplicated keys.
At best new value will override the previous ones.
Worse scenario is exception (e.g when you try to collect it as a stream):
No duplicates:
Stream.of("one").collect(Collectors.toMap(x -> x, x -> x))
Ok. You will get: $2 ==> {one=one}
Duplicated stream:
Stream.of("one", "not one", "surely not one").collect(Collectors.toMap(x -> 1, x -> x))
Exception java.lang.IllegalStateException: Duplicate key 1 (attempted merging values one and not one)
| at Collectors.duplicateKeyException (Collectors.java:133)
| at Collectors.lambda$uniqKeysMapAccumulator$1 (Collectors.java:180)
| at ReduceOps$3ReducingSink.accept (ReduceOps.java:169)
| at Spliterators$ArraySpliterator.forEachRemaining (Spliterators.java:948)
| at AbstractPipeline.copyInto (AbstractPipeline.java:484)
| at AbstractPipeline.wrapAndCopyInto (AbstractPipeline.java:474)
| at ReduceOps$ReduceOp.evaluateSequential (ReduceOps.java:913)
| at AbstractPipeline.evaluate (AbstractPipeline.java:234)
| at ReferencePipeline.collect (ReferencePipeline.java:578)
| at (#4:1)
To deal with duplicated keys - use other package, e.g:
https://google.github.io/guava/releases/19.0/api/docs/com/google/common/collect/Multimap.html
There is a lot of other implementations dealing with duplicated keys.
Those are needed for web (e.g. duplicated cookie keys, Http headers can have same fields, ...)
Good luck! :)
I always used:
HashMap<String, ArrayList<String>> hashy = new HashMap<String, ArrayList<String>>();
if I wanted to apply multiple things to one identifying key.
public void MultiHash(){
HashMap<String, ArrayList<String>> hashy = new HashMap<String, ArrayList<String>>();
String key = "Your key";
ArrayList<String> yourarraylist = hashy.get(key);
for(String valuessaved2key : yourarraylist){
System.out.println(valuessaved2key);
}
}
you could always do something like this and create yourself a maze!
public void LOOK_AT_ALL_THESE_HASHMAPS(){
HashMap<String, HashMap<String, HashMap<String, HashMap<String, String>>>> theultimatehashmap = new HashMap <String, HashMap<String, HashMap<String, HashMap<String, String>>>>();
String ballsdeep_into_the_hashmap = theultimatehashmap.get("firststring").get("secondstring").get("thirdstring").get("forthstring");
}
BTW, if you want some semantics such as only put if this key is not exist. you can use concurrentHashMap with putIfAbsent() function.
Check this out:
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html#put(K,%20V)
concurrentHashMap is thread safe with high performance since it uses "lock striping" mechanism to improve the throughput.
Yes, this means all the 1 keys with value are overwriten with the last added value and here you add "surely not one" so it will display only "surely not one".
Even if you are trying to display with a loop, it will also only display one key and value which have same key.
HashMap<Emp, Emp> empHashMap = new HashMap<Emp, Emp>();
empHashMap.put(new Emp(1), new Emp(1));
empHashMap.put(new Emp(1), new Emp(1));
empHashMap.put(new Emp(1), new Emp());
empHashMap.put(new Emp(1), new Emp());
System.out.println(empHashMap.size());
}
}
class Emp{
public Emp(){
}
public Emp(int id){
this.id = id;
}
public int id;
#Override
public boolean equals(Object obj) {
return this.id == ((Emp)obj).id;
}
#Override
public int hashCode() {
return id;
}
}
OUTPUT : is 1
Means hash map wont allow duplicates, if you have properly overridden equals and hashCode() methods.
HashSet also uses HashMap internally, see the source doc
public class HashSet{
public HashSet() {
map = new HashMap<>();
}
}