2017-02-14 24 views
4

Как можно идиоматический перечислить Stream<T>, которая отображает каждый T экземпляр уникального целым число с помощью Java-8 методов потока (например, для массива T[] values, создавая Map<T,Integer>, где Map.get(values[i]) == i вычисляется в true)?идиоматический перечисление потока объектов в Java 8

В настоящее время, я определяю анонимный класс, который приращение int поля для использования с Collectors.toMap(..) методом:

private static <T> Map<T, Integer> createIdMap(final Stream<T> values) { 
    return values.collect(Collectors.toMap(Function.identity(), new Function<T, Integer>() { 

     private int nextId = 0; 

     @Override 
     public Integer apply(final T t) { 
      return nextId++; 
     } 

    })); 
} 

Однако, есть не более лаконичный/элегантный способ сделать это с помощью Java- поток API? — бонусные баллы, если их можно безопасно распараллелить.

+1

Все значения в потоке 'values' уникальны? – Andremoniy

+0

В моем конкретном случае да, но было бы также интересно увидеть решение, которое может обрабатывать множественные вхождения объекта. – errantlinguist

+1

@SME_Dev определенно нет. – Andremoniy

ответ

5

Ваш подход потерпит неудачу, если есть дубликат элемента.

Кроме того, ваша задача требует изменчивого состояния, поэтому ее можно решить с помощью Mutable reduction. Когда мы заполняем карту, мы можем просто использовать размер карты для получения неиспользуемого id.

Более сложная часть - операция слияния. Следующая операция просто повторяет задания для правильной карты, которые будут обрабатывать потенциальные дубликаты.

private static <T> Map<T, Integer> createIdMap(Stream<T> values) { 
    return values.collect(HashMap::new, (m,t) -> m.putIfAbsent(t,m.size()), 
     (m1,m2) -> { 
      if(m1.isEmpty()) m1.putAll(m2); 
      else m2.keySet().forEach(t -> m1.putIfAbsent(t, m1.size())); 
     }); 
} 

Если опираться на уникальные элементы, или вставить явное distinct(), мы можем использовать

private static <T> Map<T, Integer> createIdMap(Stream<T> values) { 
    return values.distinct().collect(HashMap::new, (m,t) -> m.put(t,m.size()), 
     (m1,m2) -> { int leftSize=m1.size(); 
      if(leftSize==0) m1.putAll(m2); 
      else m2.forEach((t,id) -> m1.put(t, leftSize+id)); 
     }); 

} 
+0

Мне нравится трюк о размере карты. умная. но зачем нужна проверка 'if (leftSize == 0)'? это неконкурентный сборщик, поэтому поставщик будет вызван для того, чтобы в потоке было столько элементов, и тогда накопитель собирается помещать элемент в пустую карту перед комбайнером – Eugene

+1

@Eugene: функция слияния будет быть вызваны для частичных результатов. Они зависят от возможности расщепления источника потока (т. Е. Может ли он разделяться сбалансированным) и существуют ли операции изменения размера между ними ('filter' или' flatMap'). Таким образом, возможно, что функция объединителя вызывается с пустыми частичными результатами. Это все еще не делает тест для пустых карт * required * здесь, поскольку обычная операция слияния будет делать правильные вещи. Это только оптимизация, использование дешевого теста и упрощение операции в этом случае. – Holger

+1

Метод с тремя аргументами 'collect' не позволяет довести это до максимума. Если вы создадите пользовательский коллекционер через 'Collector.of', вы можете даже вернуть вторую карту, если первая пуста, опуская всю операцию' putAll'. Если вы интересуетесь более подробными сведениями о расщеплении работы и возможных пустых частичных результатах, вы можете рассмотреть [этот вопрос и ответы] (http://stackoverflow.com/q/34381805/2711488). – Holger

4

Я хотел бы сделать это таким образом:

private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) { 
    List<T> list = values.collect(Collectors.toList()); 
    return IntStream.range(0, list.size()).boxed() 
      .collect(Collectors.toMap(list::get, Function.identity())); 
} 

Ради или параллельности, он может быть изменен на

return IntStream.range(0, list.size()).parallel().boxed(). 
       (...) 
+1

Вы совершенно правы, я не заметил, что 'parallel()' может быть вызван напрямую, спасибо – Andremoniy

+3

Ваше решение прост и достаточно, если вы можете позволить себе промежуточное хранилище. Как сказано, вы можете использовать 'List list = values.distinct(). Collect (Collectors.toList());' если ожидаются дубликаты. Хотя, ну, вам не нужно; Иды будут уникальными в любом случае, и никто не сказал, что им не разрешено иметь пробелы ... – Holger

+0

Этот ответ более читабельен, чем [Хольгер] (http://stackoverflow.com/a/42229622/1391325), но это было бы приятно не использовать промежуточный список. Также грустно, что кажется, что ['Collectors.toList()'] (https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toList--) isn 't гарантированно вернет список случайного доступа, что означает, что сложность вашего решения может сильно различаться; Будет ли что-то в стандартной библиотеке Java, аналогичной Python ['enumerate (iterable)'] (https://docs.python.org/3/library/functions.html#enumerate), не будет здорово? – errantlinguist

0

Сравнивая для преобразования входного потока в списке первым в растворе, предоставленной Andremoniy. Я бы предпочел сделать это по-другому, потому что мы не знаем стоимость «toList()» и «list.get (i)», и нет необходимости создавать дополнительный список, который может быть небольшим или большим.

private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) { 
    final MutableInt idx = MutableInt.of(0); // Or: final AtomicInteger idx = new AtomicInteger(0);   
    return values.collect(Collectors.toMap(Function.identity(), e -> idx.getAndIncrement())); 
} 

Независимо от вопроса, я думаю, что это плохая конструкция для передачи потоков в качестве параметров в методе.