Often there is the need to transform results for a query like:
select category, count(*)
from table
group by category
to a map in which keys are categories and values are count of records belonging to the same category.
Many persistence frameworks return the results of such a query as List<Object[]>
, where object arrays contain two elements (category and the count for each returned result set row).
I am trying to find the most readable way to convert this list to the corresponding map.
Of course, traditional approach would involve creating the map and putting the entries manually:
Map<String, Integer> map = new HashMap<>();
list.stream().forEach(e -> map.put((String) e[0], (Integer) e[1]));
The first one-liner that came to my mind was to utilize the out of the box available Collectors.toMap
collector:
Map<String, Integer> map = list.stream().collect(toMap(e -> (String) e[0], e -> (Integer) e[1]));
However, I find this e -> (T) e[i]
syntax a bit less readable than traditional approach. To overcome this, I could create a util method which I can reuse in all such situations:
public static <K, V> Collector<Object[], ?, Map<K, V>> toMap() {
return Collectors.toMap(e -> (K) e[0], e -> (V) e[1]);
}
Then I've got a perfect one-liner:
Map<String, Integer> map = list.stream().collect(Utils.toMap());
There is even no need to cast key and value because of type inference. However, this is a bit more difficult to grasp for other readers of the code (Collector<Object[], ?, Map<K, V>>
in the util method signature, etc).
I am wondering, is there anything else in the java 8 toolbox that could help this to be achieved in a more readable/elegant way?
With Java 8, you can convert a List to Map in one line using the stream() and Collectors. toMap() utility methods. The Collectors. toMap() method collects a stream as a Map and uses its arguments to decide what key/value to use.
In Java 8, we have the ability to convert an object to another type using a map() method of Stream object with a lambda expression. The map() method is an intermediate operation in a stream object, so we need a terminal method to complete the stream.
I think your current 'one-liner' is fine as is. But if you don't particularly like the magic indices built into the command then you could encapsulate in an enum:
enum Column {
CATEGORY(0),
COUNT(1);
private final int index;
Column(int index) {
this.index = index;
}
public int getIntValue(Object[] row) {
return (int)row[index]);
}
public String getStringValue(Object[] row) {
return (String)row[index];
}
}
Then you're extraction code gets a bit clearer:
list.stream().collect(Collectors.toMap(CATEGORY::getStringValue, COUNT::getIntValue));
Ideally you'd add a type field to the column and check the correct method is called.
While outside the scope of your question, ideally you would create a class representing the rows which encapsulates the query. Something like the following (skipped the getters for clarity):
class CategoryCount {
private static final String QUERY = "
select category, count(*)
from table
group by category";
private final String category;
private final int count;
public static Stream<CategoryCount> getAllCategoryCounts() {
list<Object[]> results = runQuery(QUERY);
return Arrays.stream(results).map(CategoryCount::new);
}
private CategoryCount(Object[] row) {
category = (String)row[0];
count = (int)row[1];
}
}
That puts the dependency between the query and the decoding of the rows into the same class and hides all the unnecessary details from the user.
Then creating your map becomes:
Map<String,Integer> categoryCountMap = CategoryCount.getAllCategoryCounts()
.collect(Collectors.toMap(CategoryCount::getCategory, CategoryCount::getCount));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With