Как создать список <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());
}

Дополнительная карта - это минимальная производительность, поскольку она просто копирует указатели на объекты, а не сами объекты.