Related
I have a HashMap like this:
private HashMap<Integer, HashMap<String, Material>> logs = new HashMap<>();
Then I have multiple Materials stored as enum (for example. Material.OAK_LOG).
Is there any easy way to check if HashMap logs contains HashMap with specific Material?
I came up with this, which works, but I want to know if there is any other way to do this without looping through the entire HashMap
private boolean hasLog(Material mat){
boolean contains = false;
for (Map.Entry<Integer, HashMap<String, Material>> entry : this.logs.entrySet()) {
if(entry.getValue().containsValue(mat)){
contains = true;
break;
}
}
return contains;
}
No, you have to loop through the maps, doing sequential search.
You can simplify the logic a little by using values() instead of entrySet(), and simply return directly, but that's just minor refactoring:
private boolean hasLog(Material mat) {
for (HashMap<String, Material> submap : this.logs.values())
if (submap.containsValue(mat))
returns true;
return false;
}
You can write the same logic using Java 8+ Streams, but it is the same nested loop sequential search, so runtime complexity remains O(nm).
private boolean hasLog(Material mat) {
return this.logs.values().stream()
.anyMatch(submap -> submap.containsValue(mat));
}
If your Material objects are immutable and unique from an equals perspective, you could use them as a key in a cross reference map. But if Material will change, your maps could get corrupted depending on how equals is set up.
Map<Material, String> crossRef = new HashMap<>();
Whenever you add a new Map with a material to logs, do the following:
int outerKey; = ... // some integer to get the inner map
String innerKey = .. // some string to get the actual Material
Map<String, Material> innerMap = logs.get(outerKey);
Material mat = new Material(...);
innerMap.put(innerKey, mat);
crossRef.put(mat, outerKey+"_"+innerKey);
Then later
if (crossRef.contains(mat)) {
// it exists somewhere.
String mapId = crossRef.get(mat);
key[] parts = mapId.split("_");
int outerKey = Integer.valueOf(parts[0]);
String innerKey = parts[1];
Map<String, Material> map = logs.get(outerKey);
Material mat = map.get(innerKey);
}
One other downside is that your speeding up lookup time at the cost of more storage.
And to re-emphasize if two different Material objects compare equally they will be considered duplicates and thus cannot be used as keys to access both types of material.
Instead of using a concatenated String as the cross-ref key you could use a simple class or record that holds those as their specific type.
This was a drawn out answer to a simple question but it may provide some alternative ideas as to how to address your problem.
If a HashMap's key is a String[] array:
HashMap<String[], String> pathMap;
Can you access the map by using a newly created String[] array, or does it have to be the same String[] object?
pathMap = new HashMap<>(new String[]{"korey", "docs"}, "/home/korey/docs");
String path = pathMap.get(new String[]{"korey", "docs"});
It will have to be the same object. A HashMap compares keys using equals() and two arrays in Java are equal only if they are the same object.
If you want value equality, then write your own container class that wraps a String[] and provides the appropriate semantics for equals() and hashCode(). In this case, it would be best to make the container immutable, as changing the hash code for an object plays havoc with the hash-based container classes.
EDIT
As others have pointed out, List<String> has the semantics you seem to want for a container object. So you could do something like this:
HashMap<List<String>, String> pathMap;
pathMap.put(
// unmodifiable so key cannot change hash code
Collections.unmodifiableList(Arrays.asList("korey", "docs")),
"/home/korey/docs"
);
// later:
String dir = pathMap.get(Arrays.asList("korey", "docs"));
No, but you can use List<String> which will work as you expect!
Arrays in Java use Object's hashCode() and don't override it (the same thing with equals() and toString()). So no, you cannot shouldn't use arrays as a hashmap key.
You cannot use a plain Java Array as a key in a HashMap. (Well you can, but it won't work as expected.)
But you could write a wrapper class that has a reference to the Array and that also overrides hashCode() and equals().
In most cases, where the Strings inside your array are not pathological and do not include commas followed by a space, you can use Arrays.toString() as a unique key. i.e. your Map would be a Map<String, T>. And the get/put for an array myKeys[] would be
T t = myMap.get(Arrays.toString(myKeys));
myMap.put(Arrays.toString(myKeys), myT);
Obviously you could put in some wrapper code if desired.
A nice side effect is that your key is now immutable. Of course, of you change your array myKeys and then try a get(), you won't find it.
Hashing of Strings is highly optimized. So my guess is that this solution, though it feels a bit slow and kludgy, will be both faster and more memory efficient (less object allocations) than #Ted Hopp solution using an immutable List. Just think about whether Arrays.toString() is unique for your keys. If not, or if there is any doubt, (e.g. the String[] comes from user input) use the List.
Like said you need a wrapper class around your array which overrides equality and hashCode.
e.g.
/**
* We can use this instance as HashKey,
* the same anagram string will refer the same value in the map.
*/
class Anagram implements CharSequence {
private final char[] anagram;
public Anagram(String anagram) {
this.anagram = anagram.toCharArray();
Arrays.sort(this.anagram);
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Anagram that = (Anagram) o;
return Arrays.equals(this.anagram, that.anagram);
}
#Override
public int hashCode() {
return Arrays.hashCode(this.anagram);
}
#Override
public int length() {
return anagram.length;
}
#Override
public char charAt(int index) {
return anagram[index];
}
#Override
public CharSequence subSequence(int start, int end) {
return new String(anagram).subSequence(start, end);
}
#Override
public String toString() {
return Arrays.toString(anagram);
}
}
Otherwise declare your map as IdentityHashMap, then the user knows we need to use the same instance for your CRUD.
Ted Hopp is right it will have to be same object.
For information see this example:
public static void main(String[] args) {
HashMap<String[], String> pathMap;
pathMap = new HashMap<String[], String>();
String[] data = new String[]{"korey", "docs"};
pathMap.put(data, "/home/korey/docs");
String path = pathMap.get(data);
System.out.println(path);
}
When you run the above code, it will print "docs".
Since Java 9, you can use Arrays::compare method as a comparator for TreeMap that compares the contents of arrays.
Map<String[], String> map = new TreeMap<>(Arrays::compare);
String[] key1 = {"one", "two"};
String[] key2 = {"one", "two"};
String[] key3 = {"one", "two"};
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.size()); // 1
System.out.println(map.get(key1)); // value2
System.out.println(map.get(key2)); // value2
System.out.println(map.get(key3)); // value2
See also: How to make a Set of arrays in Java?
A running example using the Arrays utility and the hash code it provides:
String[] key1 = { "korey", "docs" };
String value1 = "/home/korey/docs";
HashMap<Integer, String> map = new HashMap<Integer, String>();
map.put(Arrays.hashCode(key1), value1);
System.out.println(map);
{-1122550406=/home/korey/docs}
This approach is useful if your focus is in storing only. Retrieving using the readable (original) key is simple:
String retrievedValue = map.get(Arrays.hashCode(key1));
System.out.println(retrievedValue);
/home/korey/docs
I had an interview today, and I got the following Java code:
public class Question_6 {
public static void main(String[] args){
Map<Integer,String> map1 = new HashMap<Integer,String>();
map1.put(new Integer(1),"001a");
map1.put(new Integer(1),"001b");
map1.put(new Integer(2),"002");
System.out.println(map1.size());
Map<MyInt,String> map2 = new HashMap<MyInt,String>();
map2.put(new MyInt(1),"001a");
map2.put(new MyInt(1),"001b");
map2.put(new MyInt(2),"002");
System.out.println(map2.size());
}
}
public class MyInt {
int i;
public MyInt(int i) {
this.i = i;
}
}
The questions were:
What will be printed to the console?
Suggest a solution to the problem.
I know now that the answer to the first question is :
2
3
But I don't know why? What is the problem with MyInt?
Your problem is that equals() and hashcode() is not implemented on MyInt.
You are expected to have 2 as a result in both cases.
HashMap, as the name implies, groups the keys into buckets based on the keys' hashcode(). But the default hashcode does not match for two instances of MyInt with the same value.
To determine equality, you have to override equals() as well.
One solution:
public class MyInt {
[...]
#Override
public int hashCode() {
return value;
}
#Override
public boolean equals(Object obj) {
if (obj instanceof MyInt) {
return i == ((MyInt)obj).i;
}
return false;
}
}
You need to override the equals() and hashCode() method in your MyInt class , so that HashMap can comprehend new MyInt(1).equals(new MyInt(1)) is true.
The Integer class overrides the equals() method to do value based comparison. Hashmaps cannot contain two keys that are "equal", so the 2nd insertion into map1 will overwrite the first entry. As well, the hashcode() method is overridden.
However, MyInt does not override the equals() or hashcode() method so equality is memory location based. Therefore, map2 sees three distinct keys and makes three distinct entries.
Map<MyInt,String> map2 = new HashMap<MyInt,String>();
MyInt one = new MyInt(1);
MyInt two = new MyInt(2);
map2.put(one,"001a");
map2.put(one,"001b");
map2.put(two,"002");
System.out.println(map2.size());
Produces an output of 2 in this case because one.equals(one) is true in this case.
map1.put(new Integer(1),"001a");
map1.put(new Integer(1),"001b");//same location in map
map1.put(new Integer(2),"002");
in this part you use the Integer class, Integer class don't allow setting the same location, but your Integer class allow.
Change code like this, and you see the problem
public class Question_6 {
public static void main(String[] args){
Map<Integer,String> map1 = new HashMap<Integer,String>();
map1.put(new Integer(1),"001a");
map1.put(new Integer(2),"001b");
map1.put(new Integer(3),"002");
System.out.println(map1.size());
Map<MyInt,String> map2 = new HashMap<MyInt,String>();
map2.put(new MyInt(1),"001a");
map2.put(new MyInt(2),"001b");
map2.put(new MyInt(3),"002");
System.out.println(map2.size());
}
this code will print ;
3
3
So, your Integer class(myInt) is true but missing
The Integer class overrides the equals() method to do value-based comparison.No need to manually include equals() or hashcode() methods. My solution is as follows
import java.util.HashMap;
import java.util.Map;
public class HashMapEqualsHashcode {
public static void main(String[] args) {
MyInt obj = new MyInt(50);
Map<Integer, String> map1 = new HashMap<Integer, String>();
map1.put(new Integer(1),"001a");
map1.put(new Integer(1),"001b");
map1.put(new Integer(2),"002");
System.out.println("map1 size "+map1.size());
Map<MyInt,String> map2 = new HashMap<MyInt,String>();
map2.put(new MyInt(1),"001a");
map2.put(new MyInt(1),"001b");
map2.put(new MyInt(2),"002");
System.out.println("map2 size "+map2.size());
}
}
class MyInt {
int i;
public MyInt(int i) {
this.i = i;
}
}
Screenshot for the solution
https://i.stack.imgur.com/yVkgL.png
You must override the hashCode() and equals methods. For all cases where equals returns true for two objects, hashCode returns the same value. The hash code is a code that must be equal if two objects are equal
Why??
if you inspect in the source code of HashMap.put method. you can see that this method check both hashcode and equality before inserting. So if you don't override these methods it will use the superclass' (object's) methods which will return different values for different objects. So ALthough for the same key two values will be inserted in separate place of Hashmap. So you need to override those two and make sure that for two equal objects you should return same hashcode.
Code
So your MyInt should be something like
public class MyInt {
int i;
public MyInt(int i) {
this.i = i;
}
public int hashCode() {
return i;
}
public boolean equals(Object obj) {
if (obj instanceof MyInt && i == ((MyInt)obj).i) {
return true;
} else
return false;
}
}
The Integer class overrides the equals() method to do value-based comparison. Hashmaps cannot contain two keys that are "equal", so the 2nd insertion into map1 will overwrite the first entry. As well, the hashcode() method is overridden.
However, Myint does not override the equals() or hashcode() method so equality is memory location-based. Therefore, map2 sees three distinct keys and makes three distinct entries.
I am attempting to sort a hashmap on type <Integer,Double> using a TreeMap and a SortedMap I want to sort on the absolute values of the Doubles but I also want to retain the sign value (hence not storing as an unsigned Double).
Below is the code I am using, however I am not getting the values I expect, presumably due to the use of hashcode() can anybody point out how to fix this?
Map<Integer,Double> termWeights = new HashMap<Integer,Double>();
SortedMap sortedData = new TreeMap(new ValueComparer(termWeights));
System.out.println(termWeights);
sortedData.putAll(termWeights);
System.out.println(sortedData);
class ValueComparer implements Comparator {
private Map _data = null;
public ValueComparer(Map data) {
super();
_data = data;
}
public int compare(Object o1, Object o2) {
Double e1 = Math.abs((Double) _data.get(o1));
Double e2 = Math.abs((Double) _data.get(o2));
int compare = e2.compareTo(e1);
if (compare == 0) {
Integer a = o1.hashCode();
Integer b = o2.hashCode();
return b.compareTo(a);
}
return compare;
}
}
Thanks
Can you give an example of expected and actual results?
Sorted map: {17=1.644955871228835, 0=-1.029545248153297, 10=-5.291765636407169E-4, 9=-3.331976978545177E-4, 1=-2.7105555587851366E-4, 2=-2.7105555587851366E-4, 7=-2.0897436261984377E-4, 8=-1.305197184270594E-5, 3=0.0, 4=0.0, 5=0.0, 6=0.0, 11=0.0, 12=0.0, 13=0.0, 14=0.0, 15=0.0, 16=0.0, 18=0.0, 19=0.0, 20=0.0, 21=0.0, 22=0.0}
So what is the problem?
That looks correctly sorted from biggest to smallest.
But I would avoid using hashCode in the tie-break secondary comparator, because you need it to never return the same value for different inputs. In this case, it works, because you are calling it on an Integer, where hashCode just returns the same int. But if you used Long or String keys in your map, it would have collisions. Compare the two keys directly instead.
And finally, you must not change the weights after starting to use the comparator. That will lead to an inconsistent TreeMap.
I would like to store a group of objects in a hashmap , where the key shall be a composite of two string values. is there a way to achieve this?
i can simply concatenate the two strings , but im sure there is a better way to do this.
You could have a custom object containing the two strings:
class StringKey {
private String str1;
private String str2;
}
Problem is, you need to determine the equality test and the hash code for two such objects.
Equality could be the match on both strings and the hashcode could be the hashcode of the concatenated members (this is debatable):
class StringKey {
private String str1;
private String str2;
#Override
public boolean equals(Object obj) {
if(obj != null && obj instanceof StringKey) {
StringKey s = (StringKey)obj;
return str1.equals(s.str1) && str2.equals(s.str2);
}
return false;
}
#Override
public int hashCode() {
return (str1 + str2).hashCode();
}
}
You don't need to reinvent the wheel. Simply use the Guava's HashBasedTable<R,C,V> implementation of Table<R,C,V> interface, for your need. Here is an example
Table<String, String, Integer> table = HashBasedTable.create();
table.put("key-1", "lock-1", 50);
table.put("lock-1", "key-1", 100);
System.out.println(table.get("key-1", "lock-1")); //prints 50
System.out.println(table.get("lock-1", "key-1")); //prints 100
table.put("key-1", "lock-1", 150); //replaces 50 with 150
public int hashCode() {
return (str1 + str2).hashCode();
}
This seems to be a terrible way to generate the hashCode: Creating a new string instance every time the hash code is computed is terrible! (Even generating the string instance once and caching the result is poor practice.)
There are a lot of suggestions here:
How do I calculate a good hash code for a list of strings?
public int hashCode() {
final int prime = 31;
int result = 1;
for ( String s : strings ) {
result = result * prime + s.hashCode();
}
return result;
}
For a pair of strings, that becomes:
return string1.hashCode() * 31 + string2.hashCode();
That is a very basic implementation. Lots of advice through the link to suggest better tuned strategies.
Why not create a (say) Pair object, which contains the two strings as members, and then use this as the key ?
e.g.
public class Pair {
private final String str1;
private final String str2;
// this object should be immutable to reliably perform subsequent lookups
}
Don't forget about equals() and hashCode(). See this blog entry for more on HashMaps and keys, including a background on the immutability requirements. If your key isn't immutable, then you can change its components and a subsequent lookup will fail to locate it (this is why immutable objects such as String are good candidates for a key)
You're right that concatenation isn't ideal. For some circumstances it'll work, but it's often an unreliable and fragile solution (e.g. is AB/C a different key from A/BC ?).
I have a similar case. All I do is concatenate the two strings separated by a tilde ( ~ ).
So when the client calls the service function to get the object from the map, it looks like this:
MyObject getMyObject(String key1, String key2) {
String cacheKey = key1 + "~" + key2;
return map.get(cachekey);
}
It is simple, but it works.
I see that many people use nested maps. That is, to map Key1 -> Key2 -> Value (I use the computer science/ aka haskell curring notation for (Key1 x Key2) -> Value mapping which has two arguments and produces a value), you first supply the first key -- this returns you a (partial) map Key2 -> Value, which you unfold in the next step.
For instance,
Map<File, Map<Integer, String>> table = new HashMap(); // maps (File, Int) -> Distance
add(k1, k2, value) {
table2 = table1.get(k1);
if (table2 == null) table2 = table1.add(k1, new HashMap())
table2.add(k2, value)
}
get(k1, k2) {
table2 = table1.get(k1);
return table2.get(k2)
}
I am not sure that it is better or not than the plain composite key construction. You may comment on that.
Reading about the spaguetti/cactus stack I came up with a variant which may serve for this purpose, including the possibility of mapping your keys in any order so that map.lookup("a","b") and map.lookup("b","a") returns the same element. It also works with any number of keys not just two.
I use it as a stack for experimenting with dataflow programming but here is a quick and dirty version which works as a multi key map (it should be improved: Sets instead of arrays should be used to avoid looking up duplicated ocurrences of a key)
public class MultiKeyMap <K,E> {
class Mapping {
E element;
int numKeys;
public Mapping(E element,int numKeys){
this.element = element;
this.numKeys = numKeys;
}
}
class KeySlot{
Mapping parent;
public KeySlot(Mapping mapping) {
parent = mapping;
}
}
class KeySlotList extends LinkedList<KeySlot>{}
class MultiMap extends HashMap<K,KeySlotList>{}
class MappingTrackMap extends HashMap<Mapping,Integer>{}
MultiMap map = new MultiMap();
public void put(E element, K ...keys){
Mapping mapping = new Mapping(element,keys.length);
for(int i=0;i<keys.length;i++){
KeySlot k = new KeySlot(mapping);
KeySlotList l = map.get(keys[i]);
if(l==null){
l = new KeySlotList();
map.put(keys[i], l);
}
l.add(k);
}
}
public E lookup(K ...keys){
MappingTrackMap tmp = new MappingTrackMap();
for(K key:keys){
KeySlotList l = map.get(key);
if(l==null)return null;
for(KeySlot keySlot:l){
Mapping parent = keySlot.parent;
Integer count = tmp.get(parent);
if(parent.numKeys!=keys.length)continue;
if(count == null){
count = parent.numKeys-1;
}else{
count--;
}
if(count == 0){
return parent.element;
}else{
tmp.put(parent, count);
}
}
}
return null;
}
public static void main(String[] args) {
MultiKeyMap<String,String> m = new MultiKeyMap<String,String>();
m.put("brazil", "yellow", "green");
m.put("canada", "red", "white");
m.put("USA", "red" ,"white" ,"blue");
m.put("argentina", "white","blue");
System.out.println(m.lookup("red","white")); // canada
System.out.println(m.lookup("white","red")); // canada
System.out.println(m.lookup("white","red","blue")); // USA
}
}
public static String fakeMapKey(final String... arrayKey) {
String[] keys = arrayKey;
if (keys == null || keys.length == 0)
return null;
if (keys.length == 1)
return keys[0];
String key = "";
for (int i = 0; i < keys.length; i++)
key += "{" + i + "}" + (i == keys.length - 1 ? "" : "{" + keys.length + "}");
keys = Arrays.copyOf(keys, keys.length + 1);
keys[keys.length - 1] = FAKE_KEY_SEPARATOR;
return MessageFormat.format(key, (Object[]) keys);}
public static string FAKE_KEY_SEPARATOR = "~";
INPUT:
fakeMapKey("keyPart1","keyPart2","keyPart3");
OUTPUT: keyPart1~keyPart2~keyPart3
I’d like to mention two options that I don’t think were covered in the other answers. Whether they are good for your purpose you will have to decide yourself.
Map<String, Map<String, YourObject>>
You may use a map of maps, using string 1 as key in the outer map and string 2 as key in each inner map.
I do not think it’s a very nice solution syntax-wise, but it’s simple and I have seen it used in some places. It’s also supposed to be efficient in time and memory, while this shouldn’t be the main reason in 99 % of cases. What I don’t like about it is that we’ve lost the explicit information about the type of the key: it’s only inferred from the code that the effective key is two strings, it’s not clear to read.
Map<YourObject, YourObject>
This is for a special case. I have had this situation more than once, so it’s not more special than that. If your objects contain the two strings used as key and it makes sense to define object equality based on the two, then define equals and hashCode in accordance and use the object as both key and value.
One would have wished to use a Set rather than a Map in this case, but a Java HashSet doesn’t provide any method to retrieve an object form a set based on an equal object. So we do need the map.
One liability is that you need to create a new object in order to do lookup. This goes for the solutions in many of the other answers too.
Link
Jerónimo López: Composite key in HashMaps on the efficiency of the map of maps.