Хотя эти операции выглядят похожими в некоторых аспектах, они принципиально отличаются.В отличие от операции Linq GroupBy
, Java groupingBy
представляет собой Collector
, предназначенную для работы с терминальной операцией collect
Stream Stream, которая сама по себе не является промежуточной операцией и, следовательно,не может использоваться для реализации операции отложенного потока вообще.
Сборщик groupingBy
использует другой нисходящий поток Collector
для групп, поэтому вместо потоковой передачи по элементам группы для выполнения другой операции выбудет указывать коллектор, выполняющий эту операцию на месте, в лучшем случае.Хотя эти коллекторы не поддерживают короткое замыкание, они устраняют необходимость собирать группы в List
s, просто для их потоковой передачи.Просто подумайте, например, groupingBy(f1, summingInt(f2))
.Случай сбора групп в List
считается достаточно распространенным, чтобы подразумевать, что toList()
подразумевается, когда вы не указываете сборщик, но это не учитывалось в случае отображения элементов перед сбором в список..
Если вы столкнетесь с этим случаем достаточно часто, было бы легко определить свой собственный сборщик
public static <T,K,V> Collector<T,?,Map<K,List<V>>> groupingBy(
Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {
return Collectors.groupingBy(key, Collectors.mapping(value, Collectors.toList()));
}
и использовать его как
Map<Integer,List<String>> result = map.entrySet().stream()
.collect(groupingBy(Map.Entry::getValue, Map.Entry::getKey));
и, так как выне обязаны использовать ссылки на методы и хотят быть ближе к оригиналу Linq:
Map<Integer,List<String>> result = map.entrySet().stream()
.collect(groupingBy(kvp -> kvp.getValue(), kvp -> kvp.getKey()));
, но, как уже отмечалось, если вы собираетесь потом пролистывать эту карту и беспокоиться о нелицеприятности этогоВ любом случае вы, возможно, захотите использовать коллектор, отличный от toList()
.
Хотя этот подход обеспечивает некоторую гибкость в отношении результирующих значений, Map
и его ключи являются неизбежной частью этой операции, так кактолько Map
обеспечивает логику хранения, его операция поиска также отвечает за формирование групп, что также определяетсемантическая.Например, когда вы используете вариант с поставщиком карт с () -> new TreeMap<>(customComparator)
, вы можете получить совершенно другие группы, как и по умолчанию HashMap
(например, String.CASE_INSENSITIVE_ORDER
).С другой стороны, когда вы предоставляете EnumMap
, вы можете не получить другую семантику, но совершенно другие характеристики производительности.
В отличие от этого, операция GroupBy
из описанного вами Linq выглядит как промежуточная операция, котораяне имеет кулона в Stream API вообще.Как вы сами себе предположили, высоки шансы, что он все же совершит полный обход после опроса первого элемента, полностью заполняя структуру данных за кулисами.Даже если реализация пробует некоторую лень, результаты ограничены.Вы можете дешево получить первый элемент первой группы, но если вас интересует только этот элемент, вам вообще не понадобится группировка.Второй элемент первой группы может уже быть последним из исходного потока, требующего полного обхода и хранения.
Таким образом, предложение такой операции подразумевало бы некоторую сложность с небольшим преимуществом по сравнению со сбором с нетерпением.Также трудно представить реализацию с параллельной поддержкой (предлагая преимущества по сравнению с операцией collect
).Фактическое неудобство связано не с этим дизайнерским решением, а с тем фактом, что полученный Map
не является Collection
(учтите, что реализация Iterable
в одиночку не подразумевает наличие stream()
метод ) и решение разделить операции сбора и потоковые операции .Эти два аспекта приводят к требованию использовать entrySet().stream()
для потоковой передачи по карте, но это выходит за рамки этого вопроса.И, как сказано выше, если вам это нужно, сначала проверьте, не может ли другой нижестоящий коллектор для коллектора groupingBy
обеспечить желаемый результат в первую очередь.
Для полноты, вот решение, котороепытается реализовать отложенную группировку:
public interface Group<K,V> {
K key();
Stream<V> values();
}
public static <T,K,V> Stream<Group<K,V>> group(Stream<T> s,
Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {
return StreamSupport.stream(new Spliterator<Group<K,V>>() {
final Spliterator<T> sp = s.spliterator();
final Map<K,GroupImpl<T,K,V>> map = new HashMap<>();
ArrayDeque<Group<K,V>> pendingGroup = new ArrayDeque<>();
Consumer<T> c;
{
c = t -> map.compute(key.apply(t), (k,g) -> {
V v = value.apply(t);
if(g == null) pendingGroup.addLast(g = new GroupImpl<>(k, v, sp, c));
else g.add(v);
return g;
});
}
public boolean tryAdvance(Consumer<? super Group<K,V>> action) {
do {} while(sp.tryAdvance(c) && pendingGroup.isEmpty());
Group<K,V> g = pendingGroup.pollFirst();
if(g == null) return false;
action.accept(g);
return true;
}
public Spliterator<Group<K,V>> trySplit() {
return null; // that surely doesn't work in parallel
}
public long estimateSize() {
return sp.estimateSize();
}
public int characteristics() {
return ORDERED|NONNULL;
}
}, false);
}
static class GroupImpl<T,K,V> implements Group<K,V> {
private final K key;
private final V first;
private final Spliterator<T> source;
private final Consumer<T> sourceConsumer;
private List<V> values;
GroupImpl(K k, V firstValue, Spliterator<T> s, Consumer<T> c) {
key = k;
first = firstValue;
source = s;
sourceConsumer = c;
}
public K key() {
return key;
}
public Stream<V> values() {
return StreamSupport.stream(
new Spliterators.AbstractSpliterator<V>(1, Spliterator.ORDERED) {
int pos;
public boolean tryAdvance(Consumer<? super V> action) {
if(pos == 0) {
pos++;
action.accept(first);
return true;
}
do {} while((values==null || values.size()<pos)
&&source.tryAdvance(sourceConsumer));
if(values==null || values.size()<pos) return false;
action.accept(values.get(pos++ -1));
return true;
}
}, false);
}
void add(V value) {
if(values == null) values = new ArrayList<>();
values.add(value);
}
}
Вы можете проверить это на следующем примере:
group(
Stream.of("foo", "bar", "baz", "hello", "world", "a", "b", "c")
.peek(s -> System.out.println("source traversal: "+s)),
String::length,
String::toUpperCase)
.filter(h -> h.values().anyMatch(s -> s.startsWith("B")))
.findFirst()
.ifPresent(g -> System.out.println("group with key "+g.key()));
, который напечатает:
source traversal: foo
source traversal: bar
group with key 3
, показывая, чтолень работает как можно дальше.Но
- Каждая операция,требуется знать, что все группы / ключи требуют полного обхода источника, поскольку самый последний элемент может вводить новую группу
- Каждая операция, которая требует обработки всех элементов хотя бы одной группы, требует полного обхода,поскольку самый последний элемент источника может принадлежать к этой группе
- Предыдущий пункт относится даже к операциям с коротким замыканием, если они не могут остановиться рано.Например, в приведенном выше примере поиск соответствия во второй группе подразумевает неудачный полный обход первой группы, следовательно, полный обход источника
Приведенный выше пример можно переписать в
Stream.of("foo", "bar", "baz", "hello", "world", "a", "b", "c")
.peek(s -> System.out.println("source traversal: "+s))
.filter(s -> s.toUpperCase().startsWith("H"))
.map(String::length)
.findFirst()
.ifPresent(key -> System.out.println("group with key "+key));
, который предлагает еще большую лень (например, если совпадение не в первой группе).
Конечно, пример был надуман, но у меня есть сильное чувство, что почти любая операция, которая несетПотенциал отложенной обработки, т.е. не требует всех групп и не требует всех элементов хотя бы одной группы, может быть переписан в операцию, которая вообще не нуждается в группировке.