Сглаживание коллекции

Скажем, у меня есть Map<? extends Object, List<String>>

Я могу легко получить значения карты и перебрать ее, чтобы создать один List<String>.

   for (List<String> list : someMap.values()) {
        someList.addAll(list);
    }

Есть ли способ сгладить его одним выстрелом?

  List<String> someList = SomeMap.values().flatten();

Ответ 1

Если вы используете Java 8, вы можете сделать что-то вроде этого:

someMap.values().forEach(someList::addAll);

Ответ 2

Использование Java 8 и если вы предпочитаете не создавать экземпляр List самостоятельно, как в предлагаемом (и принятом) решении

someMap.values().forEach(someList::addAll);

Вы можете сделать все это, потоковое с помощью этого оператора:

List<String> someList = map.values().stream().flatMap(c -> c.stream()).collect(Collectors.toList());

Кстати, должно быть интересно знать, что на Java 8 принятая версия кажется действительно самой быстрой. Он имеет примерно то же время, что и

for (List<String> item : someMap.values()) ...

и это путь быстрее, чем чистое потоковое решение. Вот мой маленький тестовый код. Я явно не называю его эталоном, чтобы избежать итогового обсуждения контрольных ошибок.;) Я делаю каждый тест дважды, чтобы, надеюсь, получить полную скомпилированную версию.

    Map<String, List<String>> map = new HashMap<>();
    long millis;

    map.put("test", Arrays.asList("1", "2", "3", "4"));
    map.put("test2", Arrays.asList("10", "20", "30", "40"));
    map.put("test3", Arrays.asList("100", "200", "300", "400"));

    int maxcounter = 1000000;

    System.out.println("1 stream flatmap");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> someList = map.values().stream().flatMap(c -> c.stream()).collect(Collectors.toList());
    }
    System.out.println(System.currentTimeMillis() - millis);

    System.out.println("1 parallel stream flatmap");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> someList = map.values().parallelStream().flatMap(c -> c.stream()).collect(Collectors.toList());
    }
    System.out.println(System.currentTimeMillis() - millis);

    System.out.println("1 foreach");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> mylist = new ArrayList<String>();
        map.values().forEach(mylist::addAll);
    }
    System.out.println(System.currentTimeMillis() - millis);        

    System.out.println("1 for");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> mylist = new ArrayList<String>();
        for (List<String> item : map.values()) {
            mylist.addAll(item);
        }
    }
    System.out.println(System.currentTimeMillis() - millis);


    System.out.println("2 stream flatmap");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> someList = map.values().stream().flatMap(c -> c.stream()).collect(Collectors.toList());
    }
    System.out.println(System.currentTimeMillis() - millis);

    System.out.println("2 parallel stream flatmap");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> someList = map.values().parallelStream().flatMap(c -> c.stream()).collect(Collectors.toList());
    }
    System.out.println(System.currentTimeMillis() - millis);

    System.out.println("2 foreach");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> mylist = new ArrayList<String>();
        map.values().forEach(mylist::addAll);
    }
    System.out.println(System.currentTimeMillis() - millis);        

    System.out.println("2 for");
    millis = System.currentTimeMillis();
    for (int i = 0; i < maxcounter; i++) {
        List<String> mylist = new ArrayList<String>();
        for (List<String> item : map.values()) {
            mylist.addAll(item);
        }
    }
    System.out.println(System.currentTimeMillis() - millis);

И вот результаты:

1 stream flatmap
468
1 parallel stream flatmap
1529
1 foreach
140
1 for
172
2 stream flatmap
296
2 parallel stream flatmap
1482
2 foreach
156
2 for
141

Редактировать 2016-05-24 (через два года):

Выполнение того же теста с использованием реальной версии Java 8 (U92) на той же машине:

1 stream flatmap
313
1 parallel stream flatmap
3257
1 foreach
109
1 for
141
2 stream flatmap
219
2 parallel stream flatmap
3830
2 foreach
125
2 for
140

Кажется, что есть ускорение для последовательной обработки потоков и еще больших накладных расходов для параллельных потоков.

Ответ 3

При поиске "java 8 flatten" это единственное упоминание. И дело не в выравнивании потока. Так что для большого блага я просто оставил его здесь.

.flatMap(Collection::stream)

Я также удивлен, что никто не дал одновременный ответ java 8 на исходный вопрос, который

.collect(ArrayList::new, ArrayList::addAll, ArrayList::addAll);

Ответ 4

Если вы используете Eclipse Collections, вы можете использовать Iterate.flatten().

MutableMap<String, MutableList<String>> map = Maps.mutable.empty();
map.put("Even", Lists.mutable.with("0", "2", "4"));
map.put("Odd", Lists.mutable.with("1", "3", "5"));
MutableList<String> flattened = Iterate.flatten(map, Lists.mutable.empty());
Assert.assertEquals(
    Lists.immutable.with("0", "1", "2", "3", "4", "5"),
    flattened.toSortedList());

flatten() является частным случаем более общего RichIterable.flatCollect().

MutableList<String> flattened = 
    map.flatCollect(x -> x, Lists.mutable.empty());

Примечание. Я являюсь коммиттером для коллекций Eclipse.

Ответ 5

Предлагаемый коллегой:

listOfLists.stream().flatMap(e -> e.stream()).collect(Lists.toList())

Мне нравится это лучше, чем forEach().

Ответ 6

Нет, более короткий метод. Вы должны использовать цикл.

Обновление Apr 2014: Наконец-то появилась Java 8. В новой версии вы можете использовать метод Iterable.forEach для перемещения по коллекции без использования явного цикла.

Обновление Nov 2017:. Найден этот вопрос случайно при поиске современного решения. Закончено с reduce:

someMap.values().stream().reduce(new ArrayList(), (accum, list) -> {
    accum.addAll(list);
    return accum;
}):

Это позволяет избежать зависимости от изменяемого внешнего состояния forEach(someList::addAll) служебных данных flatMap(List::stream).

Ответ 7

Если вы просто хотите перебирать значения, вы можете избежать всех этих методов addAll.

Все, что вам нужно сделать, это написать класс, который инкапсулирует вашу карту и реализует Iterator:

public class ListMap<K,V> implements Iterator<V>
{
  private final Map<K,List<V>> _map;
  private Iterator<Map.Entry<K,List<V>>> _it1 = null;
  private Iterator<V> _it2 = null;

  public ListMap(Map<K,List<V>> map)
  {
    _map = map;
    _it1 = map.entrySet().iterator(); 
    nextList();
  }

  public boolean hasNext()
  {
    return _it2!=null && _it2.hasNext();
  }

  public V next()
  {
    if(_it2!=null && _it2.hasNext())
    {
      return _it2.next();
    }
    else
    {
      throw new NoSuchElementException();
    }
    nextList();
  } 

  public void remove()
  {
    throw new NotImplementedException();
  }

  private void nextList()
  {
    while(_it1.hasNext() && !_it2.hasNext())
    {
      _it2 = _it1.next().value();
    }
  }
}

Ответ 8

Хорошим решением для подкатегории Карты карт является сохранение, по возможности, данных в Guava Table.

https://github.com/google/guava/wiki/NewCollectionTypesExplained#table

Итак, например, Map<String,Map<String,String>> заменяется на Table<String,String,String>, который уже flattend. Фактически, документы говорят, что реализация HashBasedTable, Table Hash, по существу, поддерживается HashMap<R, HashMap<C, V>>