Как создать список <T> из карт <K, V> и List <K> ключей?
Используя Java 8 lambdas, какой "лучший" способ эффективно создать новый List<T>
с учетом List<K>
возможных ключей и Map<K,V>
? Это сценарий, в котором вам предоставляется List
возможных Map
ключей и ожидается генерация List<T>
, где T
- это некоторый тип, который построен на основе некоторого аспекта V
, типов значений карты,
Я изучил несколько и не чувствую себя комфортно, утверждая, что один путь лучше другого (возможно, одно исключение - см. код). Я уточню "лучший" как комбинацию четкости кода и эффективности выполнения. Это то, что я придумал. Я уверен, что кто-то может сделать лучше, что является одним из аспектов этого вопроса. Мне не нравится аспект filter
большинства, поскольку это означает необходимость создания промежуточных структур и нескольких проходов над именами List
. Прямо сейчас, я выбираю пример 6 - простой цикл ol. (ПРИМЕЧАНИЕ. Некоторые критические мысли содержатся в комментариях к коду, особенно "нужно ссылаться извне..." Это означает, что внешний из лямбда.)
public class Java8Mapping {
private final Map<String,Wongo> nameToWongoMap = new HashMap<>();
public Java8Mapping(){
List<String> names = Arrays.asList("abbey","normal","hans","delbrook");
List<String> types = Arrays.asList("crazy","boring","shocking","dead");
for(int i=0; i<names.size(); i++){
nameToWongoMap.put(names.get(i),new Wongo(names.get(i),types.get(i)));
}
}
public static void main(String[] args) {
System.out.println("in main");
Java8Mapping j = new Java8Mapping();
List<String> testNames = Arrays.asList("abbey", "froderick","igor");
System.out.println(j.getBongosExample1(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", ")));
System.out.println(j.getBongosExample2(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", ")));
System.out.println(j.getBongosExample3(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", ")));
System.out.println(j.getBongosExample4(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", ")));
System.out.println(j.getBongosExample5(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", ")));
System.out.println(j.getBongosExample6(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", ")));
}
private static class Wongo{
String name;
String type;
public Wongo(String s, String t){name=s;type=t;}
@Override public String toString(){return "Wongo{name="+name+", type="+type+"}";}
}
private static class Bongo{
Wongo wongo;
public Bongo(Wongo w){wongo = w;}
@Override public String toString(){ return "Bongo{wongo="+wongo+"}";}
}
// 1: Create a list externally and add items inside 'forEach'.
// Needs to externally reference Map and List
public List<Bongo> getBongosExample1(List<String> names){
final List<Bongo> listOne = new ArrayList<>();
names.forEach(s -> {
Wongo w = nameToWongoMap.get(s);
if(w != null) {
listOne.add(new Bongo(nameToWongoMap.get(s)));
}
});
return listOne;
}
// 2: Use stream().map().collect()
// Needs to externally reference Map
public List<Bongo> getBongosExample2(List<String> names){
return names.stream()
.filter(s -> nameToWongoMap.get(s) != null)
.map(s -> new Bongo(nameToWongoMap.get(s)))
.collect(Collectors.toList());
}
// 3: Create custom Collector
// Needs to externally reference Map
public List<Bongo> getBongosExample3(List<String> names){
Function<List<Wongo>,List<Bongo>> finisher = list -> list.stream().map(Bongo::new).collect(Collectors.toList());
Collector<String,List<Wongo>,List<Bongo>> bongoCollector =
Collector.of(ArrayList::new,getAccumulator(),getCombiner(),finisher, Characteristics.UNORDERED);
return names.stream().collect(bongoCollector);
}
// example 3 helper code
private BiConsumer<List<Wongo>,String> getAccumulator(){
return (list,string) -> {
Wongo w = nameToWongoMap.get(string);
if(w != null){
list.add(w);
}
};
}
// example 3 helper code
private BinaryOperator<List<Wongo>> getCombiner(){
return (l1,l2) -> {
l1.addAll(l2);
return l1;
};
}
// 4: Use internal Bongo creation facility
public List<Bongo> getBongosExample4(List<String> names){
return names.stream().filter(s->nameToWongoMap.get(s) != null).map(s-> new Bongo(nameToWongoMap.get(s))).collect(Collectors.toList());
}
// 5: Stream the Map EntrySet. This avoids referring to anything outside of the stream,
// but bypasses the lookup benefit from Map.
public List<Bongo> getBongosExample5(List<String> names){
return nameToWongoMap.entrySet().stream().filter(e->names.contains(e.getKey())).map(e -> new Bongo(e.getValue())).collect(Collectors.toList());
}
// 6: Plain-ol-java loop
public List<Bongo> getBongosExample6(List<String> names){
List<Bongo> bongos = new ArrayList<>();
for(String s : names){
Wongo w = nameToWongoMap.get(s);
if(w != null){
bongos.add(new Bongo(w));
}
}
return bongos;
}
}
Ответы
Ответ 1
Если namesToWongoMap
- это переменная экземпляра, вы не можете избежать захвата лямбда.
Вы можете очистить поток, разделив несколько операций:
return names.stream()
.map(n -> namesToWongoMap.get(n))
.filter(w -> w != null)
.map(w -> new Bongo(w))
.collect(toList());
return names.stream()
.map(namesToWongoMap::get)
.filter(Objects::nonNull)
.map(Bongo::new)
.collect(toList());
Таким образом, вы не вызываете get
дважды.
Это очень похоже на цикл for
, за исключением, например, его теоретически можно распараллелить, если namesToWongoMap
не может быть мутирован одновременно.
Мне не нравится аспект filter
большинства, поскольку это означает необходимость создания промежуточных структур и нескольких проходов над именами List
.
Нет промежуточных структур, и есть только один проход над List
. В потоковом конвейере говорится: "для каждого элемента... выполните эту последовательность операций". Каждый элемент посещается один раз и применяется конвейер.
Вот некоторые соответствующие цитаты из описания java.util.stream
:
Поток не является структурой данных, в которой хранятся элементы; вместо этого он передает элементы из источника, такого как структура данных, массив, функция генератора или канал ввода-вывода, через конвейер вычислительных операций.
Процессные потоки лениво позволяют значительно повысить эффективность; в конвейере, таком как пример суммы карты-фильтра, фильтрация, сопоставление и суммирование могут быть объединены в один проход данных с минимальным промежуточным состоянием.
Ответ 2
Ответ Radiodef в значительной степени прибил его, я думаю. Решение, данное там:
return names.stream()
.map(namesToWongoMap::get)
.filter(Objects::nonNull)
.map(Bongo::new)
.collect(toList());
вероятно, о лучшем, что можно сделать в Java 8.
Я действительно хотел упомянуть о небольшой морщине в этом. Вызов Map.get
возвращает null
, если имя отсутствует на карте, и это впоследствии отфильтровывается. В этом нет ничего плохого, хотя он испекает семантику нулевого значения - нет - в структуре конвейера.
В каком-то смысле нам нужна операция конвейера картера, у которой есть выбор возврата нуля или одного элемента. Способ сделать это с потоками - с помощью flatMap
. Функция flatmapper может возвращать произвольное количество элементов в поток, но в этом случае мы хотим только нуль или один. Вот как это сделать:
return names.stream()
.flatMap(name -> {
Wongo w = nameToWongoMap.get(name);
return w == null ? Stream.empty() : Stream.of(w);
})
.map(Bongo::new)
.collect(toList());
Я признаю, что это довольно неуклюже, и поэтому я бы не рекомендовал это делать. Немного лучший, но несколько неясный подход:
return names.stream()
.flatMap(name -> Optional.ofNullable(nameToWongoMap.get(name))
.map(Stream::of).orElseGet(Stream::empty))
.map(Bongo::new)
.collect(toList());
но я все еще не уверен, что рекомендую это, поскольку оно стоит.
Использование flatMap
указывает на другой подход. Если у вас есть более сложная политика, касающаяся того, как бороться с непредвиденным случаем, вы можете реорганизовать это в вспомогательную функцию, которая возвращает поток, содержащий результат, или пустой поток, если нет результата.
Наконец, JDK 9, все еще находящийся в разработке на момент написания этой статьи, добавил Stream.ofNullable
, который полезен именно в таких ситуациях:
return names.stream()
.flatMap(name -> Stream.ofNullable(nameToWongoMap.get(name)))
.map(Bongo::new)
.collect(toList());
В стороне JDK 9 также добавил Optional.stream
, который создает поток "нуль-один" из Optional
. Это полезно в тех случаях, когда вы хотите вызвать функцию необязательного возврата из flatMap
. Подробнее см. этот ответ и этот ответ.
Ответ 3
Один подход, который я не видел, - retainAll
:
public List<Bongo> getBongos(List<String> names) {
Map<String, Wongo> copy = new HashMap<>(nameToWongoMap);
copy.keySet().retainAll(names);
return copy.values().stream().map(Bongo::new).collect(
Collectors.toList());
}
Дополнительная карта - это минимальная производительность, поскольку она просто копирует указатели на объекты, а не сами объекты.