Introduction
Suppose we have a JSON file like below and we want to convert to into a map (a hash in Ruby terminology.)
{
"first": {
"second": {
"third": 1881
}
}
}
Ruby has a dig() method which provides a null-safe way to go deep in a nested map and Groovy has a safe navigation syntax to do the same.
Below 2 examples would return the value 1881
map.dig('first', 'second', 'third') # returns 1881
map.dig('first', 'X', 'third') # returns ""
map.get('first')?.get('second')?.get('third') // returns 1881
map.get('first')?.get('X')?.get('third') // returns "null"
Unfortunately, Java doesn’t provide similar syntax. We would write something like below in Java:
// it's just too verbose, let's skip it...
var em = Collections.emptyMap();
map.getOrDefault("first", em).getOrDefault("second", em).getOrDefault("third", em)
Ruby’s dig
method in Java
We will implement an equivalent of Ruby’s dig method in Java which will be used as follows:
dig(map, "first", "second", "third")) // retuns Optional[1881]
dig(map, "first", "X", "third")) // returns Optional.empty
dig
method in Javaclass MapUtils { (1)
public static <R> Optional<R> dig(Map<String, ?> map, String... keys) { (2)
if (map == null) {
throw new IllegalArgumentException("'map' cannot be null!");
}
if (keys == null || keys.length == 0) {
throw new IllegalArgumentException("You should provide at least one key!");
}
Map<String, ?> currentMap = map;
for (int i = 0; i < keys.length; ++i) {
final String key = keys[i];
final Object value = currentMap.get(key);
if (value instanceof Map) { (3)
currentMap = (Map<String, ?>) value; (4)
}
else {
if (i == keys.length - 1) { (5)
final R retVal = (R) value; (6)
return Optional.ofNullable(retVal);
}
return Optional.empty();
}
}
return Optional.empty();
}
}
1 | You can put this method into any class and it has not to be a static method. This is just for convention. |
2 | <R> is a generic type. dig method has a parameterized return type so that developers can use it without casting. |
3 | No need for extra null checking because instanceof would return false for `null`s. |
4 | We need to assume that the value (the next object in the map) is of type Map<String, ?> . This is a safe assumption for us because if the key of the new map is not a string or the value of it is not a Map we won’t have an error in next loop and simply return Optional.empty() . |
5 | If the value is not a Map and we have just used the last key then it means that we have got the final value. |
6 | We need to cast the return value. Here, the type safety is gone and successful execution is dependent on the developer’s correct assumption of the result type. |
Remember the sample.json
the last value is an integer 1881
not a string "1881"
. Hence:
Optional<String> result = dig(map, "first", "second", "third");
String year = result.get();
// Exception: java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
// Correct usage should be:
Optional<Integer> result = dig(map, "first", "second", "third");
Integer year = result.get();
Trying the null safe navigation for Java HashMap
-
Clone the and import the project into your IDE
-
Run
DigMethodShowCase
class
Or execute in command line
-
Build the project with
mvn clean package
command-
Run this command in project’s root (where the
pom.xml
resides)
-
-
Execute it via:
java -jar target/ruby-dig-method-for-java-map-0.0.1-SNAPSHOT-jar-with-dependencies.jar
Seeing the original behavior
There is also a Ruby file to demonstrate the original dig
method in src/main/resources/sample.rb
If you have Ruby installed you can run the file via
$ cd src/main/resources
$ ruby sample.rb
It will parse sample.json
into a map (Ruby folks call it hash) and use dig
on it.
Comments