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)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.emptydig 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();
}
}-
You can put this method into any class and it has not to be a static method. This is just for convention.
-
<R>is a generic type.digmethod has a parameterized return type so that developers can use it without casting. -
No need for extra null checking because
instanceofwould returnfalsefor `null`s. -
We need to assume that the
value(the next object in the map) is of typeMap<String, ?>. This is a safe assumption for us because if thekeyof the new map is not a string or the value of it is not aMapwe won’t have an error in next loop and simply returnOptional.empty(). -
If the
valueis not aMapand we have just used the lastkeythen it means that we have got the final value. -
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();-
Clone the and import the project into your IDE
-
Run
DigMethodShowCaseclass
Or execute in command line
-
Build the project with
mvn clean packagecommand-
Run this command in project’s root (where the
pom.xmlresides)
-
-
Execute it via:
java -jar target/ruby-dig-method-for-java-map-0.0.1-SNAPSHOT-jar-with-dependencies.jar
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.rbIt will parse sample.json into a map (Ruby folks call it hash) and use dig on it.