Сглаживание коллекции
Скажи у меня есть Map<? extends Object, List<String>>
Я могу достаточно легко получить значения карты и перебрать ее, чтобы получить List<String>
,
for (List<String> list : someMap.values()) {
someList.addAll(list);
}
Есть ли способ сгладить его за один выстрел?
List<String> someList = SomeMap.values().flatten();
8 ответов
Если вы используете Java 8, вы можете сделать что-то вроде этого:
someMap.values().forEach(someList::addAll);
Использование 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
Кажется, что есть ускорение для последовательной обработки потоков и еще большие издержки для параллельных потоков.
Изменить 2018-10-18 (два года спустя):
Используя сейчас версию Java 10 (10.0.2) на той же машине:
1 stream flatmap
393
1 parallel stream flatmap
3683
1 foreach
157
1 for
175
2 stream flatmap
243
2 parallel stream flatmap
5945
2 foreach
128
2 for
187
Накладные расходы для параллельной потоковой передачи, кажется, больше.
При поиске "java 8 flatten" это единственное упоминание. И дело не в сглаживающем потоке. Так что для хорошего блага я просто оставляю это здесь
.flatMap(Collection::stream)
Я также удивлен, что никто не дал одновременный ответ Java 8 на оригинальный вопрос, который
.collect(ArrayList::new, ArrayList::addAll, ArrayList::addAll);
Предложено коллегой:
listOfLists.stream().flatMap(e -> e.stream()).collect(Lists.toList())
Мне нравится это лучше, чем forEach().
Если вы используете 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 Collections.
Нет, нет более короткого метода. Вы должны использовать цикл.
Обновление апрель 2014: Java 8 наконец вышла. В новой версии вы можете использовать Iterable.forEach
метод обхода коллекции без использования явного цикла.
Обновление ноябрь 2017: случайно нашел этот вопрос, когда искал современное решение. Закончилось идти с reduce
:
someMap.values().stream().reduce(new ArrayList(), (accum, list) -> {
accum.addAll(list);
return accum;
}):
Это позволяет избежать зависимости от изменяемого внешнего состояния forEach(someList::addAll)
накладные расходы flatMap(List::stream)
,
Если вы просто хотите перебрать значения, вы можете избежать всех этих методов addAll.
Все, что вам нужно сделать, это написать класс, который инкапсулирует вашу карту, и который реализует итератор:
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();
}
}
}
Свести на функцию:
private <A, T> List<T> flatten(List<A> list, Function<A, List<T>> flattenFn) {
return list
.stream()
.map(flattenFn)
.flatMap(Collection::stream)
.collect(Collectors.toUnmodifiableList());
}
Хорошим решением для подслоя Карты является хранение, если возможно, данных в Guava. Table
,
https://github.com/google/guava/wiki/NewCollectionTypesExplained
Так, например, Map<String,Map<String,String>>
заменяется Table<String,String,String>
который уже плоский. На самом деле, документы говорят, что HashBasedTable
, Table
Реализация хеша, по сути, поддерживается HashMap<R, HashMap<C, V>>