Java excplicit cast of nested maps - java

Why does this cast work?
import java.util.HashMap;
import java.util.Map;
public class TestMap {
public static void main(String[] args) {
Map<String, Map<String, Map<String, Map<String,Integer>>>> resultMap = new HashMap<>();
Map<String, Object> aMap = new HashMap<String, Object>();
Map<String, Integer> hiddenMap = new HashMap<String, Integer>();
hiddenMap.put("fortytwo", 42);
aMap.put("key", hiddenMap);
resultMap = (Map<String, Map<String, Map<String, Map<String, Integer>>>>) aMap.get("key");
System.out.println(resultMap);
}
}
also this:
Map<String, Map<String, Map<String, Map<String,Map<String,Integer>>>>> resultMap = new HashMap<>();
...
resultMap = (Map<String, Map<String, Map<String, Map<String,Map<String,Integer>>>>>) aMap.get("key");
and so on...
How does this happen that the hidden map which is Map<String, Integer> gets successfully cast to Map<String, Map<String, Map<String, Map<String,Integer>>>> resultMap?
Always prints:
{fortytwo=42}
Also this works (Map instead of Map):
public static void main(String[] args) {
Map<String, Map<String, Map<String, Map<String,Map<String,Integer>>>>> resultMap = new HashMap<>();
Map<String, Map> aMap = new HashMap<String, Map>();
Map<String, Integer> hiddenMap = new HashMap<String, Integer>();
hiddenMap.put("fortytwo", 42);
aMap.put("key", hiddenMap);
resultMap = (Map<String, Map<String, Map<String, Map<String,Map<String,Integer>>>>>) aMap.get("key");
System.out.println(resultMap);
}
EDIT: So as #shizhz says, it is because of Type Erasure of course! So the code above is equivalent to:
Map resultMap = new HashMap();
Map aMap = new HashMap();
Map hiddenMap = new HashMap();
hiddenMap.put("fortytwo", 42);
aMap.put("key", hiddenMap);
resultMap = (Map) aMap.get("key");
Which also works

Because java generics is used at compile time to provide tighter type checks, the type parameter is erased by compiler according Type Erasure rules:
Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
Insert type casts if necessary to preserve type safety.
Generate bridge methods to preserve polymorphism in extended generic types.
In code Map<String, Map> aMap = new HashMap<String, Map>();, the value in aMap is a raw type Map, which means the compiler has no idea what's the type it contains, when you try to cast a raw type of Map to any generics type of Map like Map<String, Integer>, the best compiler can do is giving you a warning. The generic type is erased at compile time and type cast will be generated when you get value from a generic map, so you can only get a runtime ClassCastException exception if the type mismatchs.
Let's have a look at the following example:
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "world");
map.put(new Integer(1), 1);
map.put(new Object(), Lists.newArrayList("hello"));
Map<String, Integer> m = (Map<String, Integer>) map;
System.out.println(m);
Integer i = m.get("hello");// ClassCastException happens at here at runtime
}
I'm trying to convert a Map containing all kinds of keys and values to Map<String, Integer> but there's no compile error, after type erasure, the above code is actually equivalent to:
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "world");
map.put(new Integer(1), 1);
map.put(new Object(), Lists.newArrayList("hello"));
Map m = (Map) map;
System.out.println(m);
Integer i = (Integer)m.get("hello");
}
Now you can easily tell why the last line caused ClassCastException.

Since you've declared aMap as Map<String, Object>, the compiler cannot tell if the values won't indeed be of type Map<String, Map<String, Map<String,Integer>>>. It will just give you an "Unchecked cast" warning to let you think about the consequences.
The cast works unless you're actually trying to do something with the values:
resultMap.get("fortytwo").isEmpty();
will result in
Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.util.Map
If you had declared aMap as Map<String, Map<String, Map<String, Map<String, Map<String, Integer>>>>> you wouldn't be able to put hiddenMap in it in the first place.

Related

Map of Map iteration

Im storing 2 map with different structure in single map like below,
Map<String, List<String>> colMap = new HashMap<String, List<String>>();
Map<String, String> appMap = new HashMap<String, String>();
// colMap assigning some values
// appMap assigning some values
Map<String, Map> mainMap = new HashMap<String, Map>();
mainMap.put("appMap", appMap);
mainMap.put("colMap", colMap);
I want to get map one by one and iterate the map.
If I try get map like below, getting error,
.......
Map colMap = map.get("colMap");
for(Entry<String, List<String>> entry : colMap.entrySet())
Error: Type mismatch: cannot convert from element type Object to Map.Entry<String,List<String>>
Why not just create a simple container POJO class (or record in Java 16+) for the two maps instead of mainMap and keep the relevant type-safety which to do it Java-way?
public class MapPojo {
private final Map<String, List<String>> colMap;
private final Map<String, String> appMap;
public MapPojo(Map<String, List<String>> colMap, Map<String, String> appMap) {
this.colMap = colMap;
this.appMap = appMap;
}
// getters, etc.
}
MapPojo mainMap = new MapPojo(colMap, appMap);
Error you are getting because when you are doing map.get operation your reference is Just Map without any Generics which will treated as Object class's reference. You should use generics like below and it will work -
Map<String, List<String>> colMap = map.get("colMap");
for(Entry<String, List<String>> entry : colMap.entrySet())

How to declare generic Map of Map

How to declare such a Map where instead of Object I have specific type:
Map<Class, Map<String, ClassInstance>> map;
Such that could be used as:
Map<String, new Type()) valueMap = new HashMap();
map.put(Type.class, valueMap);
The problem is I can't figure out how to declare generic type of both 'Class' and 'ClassInstance'.
Map<Class<?>, Map<String, Object>> map;
You cannot statically enforce that the Object is of the given type. That's for your code to enforce.
Map<String, Object> valueMap = new HashMap<>();
valueMap.put("Foo", new Type());
map.put(Type.class, valueMap);

Casting Object (which is of type Map<String, String>) to Map<String, Integer>

I have an Object which is of type Map<String, String> which has few entries. I expected to get a ClassCastException while casting this object to Map<String, Integer>. But the cast was successful. Why is it that this did not throw any exception?
Map<String, String> map1 = new HashMap<>();
map1.put("key1", "value1");
map1.put("key2", "value2");
Object o = map1;
Map<String, Integer> map2 = (Map<String, Integer>) o;
Edit: Casting from o not map1.
Generic-checking is made at compile time,while casting checking is done at the time of running the program.So You had got casting exception at run time.
You parse it as Integer.parseInt(String) and put the value into map2.
Are you sure that's right?
Your example fails to compile:
Error:(21, 60) java: incompatible types: java.util.Map<java.lang.String,java.lang.String> cannot be converted to java.util.Map<java.lang.String,java.lang.Integer>
However, changing from map1 to o, does compile:
//...
Object o = map1;
Map<String, Integer> map2 = (Map<String, Integer>) o;
Perhaps you're looking for something like this?
Map<String, String> map1 = new HashMap<>();
map1.put("key1", "1");
map1.put("key2", "2");
Map<String, Integer> map2 = new HashMap<>();
map1.forEach((key,value) -> map2.put(key, Integer.parseInt(value)));

HashMap / TreeSet combination inconsistency

This works ok:
Map aMap;
aMap = new HashMap<String, TreeSet<String>>();
This does not compile:
Map<String, Set<String>> aMap;
aMap = new HashMap<String, TreeSet<String>>();
Error message:
Compilation failed (26/05/2014 11:45:43) Error: line 2 - incompatible types -
found java.util.HashMap<java.lang.String,java.util.TreeSet<java.lang.String>>
but expected java.util.Map<java.lang.String,java.util.Set<java.lang.String>>
Why?
The first one works because you use a raw type (without generic) so you can put any type of map in there.
The second one doesn't work because a XXX<Set> is not a XXX<TreeSet>.
So you need to choose between:
Map<String, Set<String>> aMap = new HashMap<String, Set<String>>();
//or
Map<String, TreeSet<String>> aMap = new HashMap<String, TreeSet<String>>();
And in both case you will be able to write:
aMap.put("abc", new TreeSet<>());
The main difference is when you get an item from the map, with the former construct you won't have access to the TreeSet specific methods.
Finally, with Java 7+ you can omit the generic information on the right hand side and the compiler will determine it automatically for you:
Map<String, Set<String>> aMap = new HashMap<>();
Map<String, TreeSet<String>> aMap = new HashMap<>();
Use this instead:
Map<String, ? extends Set<String>> aMap;
aMap = new HashMap<String, TreeSet<String>>();
Because the Set's generic must not be the same than TreeSet's generic.
+1 to Peter's answer, TreeSet implements SortedSet which extends Set.
Map<String, ? extends Set<String>> aMap;
aMap = new HashMap<String, TreeSet<String>>();
will work fine.

Easily convert Map<String, Object> to Map<String, String>

An API I am using has a method that returns a Map<String, Object>, but I know the Object's are String's in this case, so I want it as a Map<String, String>.
But for some reason I can't just cast it, Java says Map<String, Object> cannot be casted to Map<String, String>, for some reason.
I used:
Map<String, Object> tempMap = someApiMethodReturningAMap();
Map<String, String> map = new HashMap<String, String>();
for (String i : tempMap.keySet()) {
map.put(i, String.valueOf(tempMap.get(i)));
}
as a workaround, but is there an easier way?
Well you can't safely cast it to a Map<String, String> because even though you know you've only got strings as the values, the compiler doesn't. That's like expecting:
Object x = "foo";
String y = x;
to work - it doesn't; you need to explicitly cast.
Likewise you can explicitly cast here, too, if you go via Object:
Map<String, Object> x = ...;
Map<String, String> y = (Map<String, String>) (Object) x;
Now you'll get a warning saying that it's an unchecked cast, because unlike the earlier "object to string" cast, there's no execution-time check that it's really valid. Type erasure means that a map doesn't really know its key/value types. So you end up with checking only being done when elements are fetched:
import java.util.*;
class Test {
public static void main(String[] args) {
Map<String, Object> x = new HashMap<>();
x.put("foo", "bar");
x.put("number", 0);
Map<String, String> y = (Map<String, String>) (Object) x;
// This is fine
System.out.println(y.get("foo"));
// This goes bang! It's trying to cast an Integer to a String
System.out.println(y.get("number"));
}
}
So if you really want to avoid creating a new map, this "cast via Object" will work - but it's far from ideal.
Your approach is safer, although you can make it slightly more efficient by avoiding the lookup:
public static Map<String, String> copyToStringValueMap(
Map<String, Object> input) {
Map<String, String> ret = new HashMap<>();
for (Map.Entry<String, Object> entry : input.entrySet()) {
ret.put(entry.getKey(), (String) entry.getValue());
}
return ret;
}
A Java 8 solution:
private Map<String, String> stringifyValues(Map<String, Object> variables) {
return variables.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> (String) e.getValue()));
}
Good solutions here, but I want to add another one that taking into consideration handling null values:
Map<String,Object> map = new HashMap<>();
Map<String,String> stringifiedMap = map.entrySet().stream()
.filter(m -> m.getKey() != null && m.getValue() !=null)
.collect(Collectors.toMap(Map.Entry::getKey, e -> (String)e.getValue()));

Categories

Resources