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).
许多持久性框架将这样的查询结果返回到List []>
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:
我想到的第一行是利用现成的收集器。toMap收集器:
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:
然而,我发现这个e -> (T) e[I]的语法比传统的方法可读性差一些。为了克服这一点,我可以创建一个util方法,我可以在所有这些情况下重用:
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?
我想知道,在java 8工具箱中还有什么可以帮助我们以一种更可读/更优雅的方式实现这一点吗?
2 个解决方案
#1
14
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中封装:
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):
在您的问题的范围之外,理想情况下,您将创建一个类来表示封装查询的行。类似以下的东西(为了清晰起见,跳过了getter):
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));
#2
2
Instead of hiding the class cast, I would make couple of functions to help with readability:
我不会隐藏类cast,而是做几个函数来帮助提高可读性:
Map<String, Integer> map = results.stream()
.collect(toMap(
columnToObject(0, String.class),
columnToObject(1, Integer.class)
));
Full example:
完整的例子:
package com.bluecatcode.learning.so;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static java.lang.String.format;
import static java.util.stream.Collectors.toMap;
public class Q35689206 {
public static void main(String[] args) {
List<Object[]> results = ImmutableList.of(
new Object[]{"test", 1}
);
Map<String, Integer> map = results.stream()
.collect(toMap(
columnToObject(0, String.class),
columnToObject(1, Integer.class)
));
System.out.println("map = " + map);
}
private static <T> Function<Object[], T> columnToObject(int index, Class<T> type) {
return e -> asInstanceOf(type, e[index]);
}
private static <T> T asInstanceOf(Class<T> type, Object object) throws ClassCastException {
if (type.isAssignableFrom(type)) {
return type.cast(object);
}
throw new ClassCastException(format("Cannot cast object of type '%s' to '%s'",
object.getClass().getCanonicalName(), type.getCanonicalName()));
}
}
#1
14
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中封装:
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):
在您的问题的范围之外,理想情况下,您将创建一个类来表示封装查询的行。类似以下的东西(为了清晰起见,跳过了getter):
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));
#2
2
Instead of hiding the class cast, I would make couple of functions to help with readability:
我不会隐藏类cast,而是做几个函数来帮助提高可读性:
Map<String, Integer> map = results.stream()
.collect(toMap(
columnToObject(0, String.class),
columnToObject(1, Integer.class)
));
Full example:
完整的例子:
package com.bluecatcode.learning.so;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static java.lang.String.format;
import static java.util.stream.Collectors.toMap;
public class Q35689206 {
public static void main(String[] args) {
List<Object[]> results = ImmutableList.of(
new Object[]{"test", 1}
);
Map<String, Integer> map = results.stream()
.collect(toMap(
columnToObject(0, String.class),
columnToObject(1, Integer.class)
));
System.out.println("map = " + map);
}
private static <T> Function<Object[], T> columnToObject(int index, Class<T> type) {
return e -> asInstanceOf(type, e[index]);
}
private static <T> T asInstanceOf(Class<T> type, Object object) throws ClassCastException {
if (type.isAssignableFrom(type)) {
return type.cast(object);
}
throw new ClassCastException(format("Cannot cast object of type '%s' to '%s'",
object.getClass().getCanonicalName(), type.getCanonicalName()));
}
}