Question générale: Quelle est la bonne façon d’inverser un flux? En supposant que nous ne sachions pas de quel type d’élément se compose ce flux, quel est le moyen générique d’inverser un flux?
Question spécifique:
IntStream
fournit une méthode de plage pour générer des entiers dans une plage spécifique IntStream.range(-range, 0)
, maintenant que je souhaite l'inverser, le passage d'une plage de 0 à une plage négative ne fonctionne pas. Je ne peux pas non plus utiliser Integer::compare
.
List<Integer> list = Arrays.asList(1,2,3,4);
list.stream().sorted(Integer::compare).forEach(System.out::println);
avec IntStream
je vais avoir cette erreur du compilateur
Erreur: (191, 0) ajc: la méthode
sorted()
dans le typeIntStream
n'est pas applicable pour les arguments (Integer::compare
)
qu'est-ce que j'oublie ici?
Pour la question spécifique de générer une variable IntStream
inverse, essayez l'une des solutions suivantes:
static IntStream revRange(int from, int to) {
return IntStream.range(from, to)
.map(i -> to - i + from - 1);
}
Cela évite la boxe et le tri.
Pour la question générale de savoir comment inverser un flux de n'importe quel type, je ne sais pas s'il existe un moyen "approprié". Il y a plusieurs façons auxquelles je peux penser. Les deux finissent par stocker les éléments de flux. Je ne connais pas de moyen d'inverser un flux sans stocker les éléments.
Cette première méthode stocke les éléments dans un tableau et les lit dans un flux dans l'ordre inverse. Notez que, comme nous ne connaissons pas le type d'exécution des éléments de flux, nous ne pouvons pas taper le tableau correctement, ce qui nécessite une distribution non contrôlée.
@SuppressWarnings("unchecked")
static <T> Stream<T> reverse(Stream<T> input) {
Object[] temp = input.toArray();
return (Stream<T>) IntStream.range(0, temp.length)
.mapToObj(i -> temp[temp.length - i - 1]);
}
Une autre technique utilise des collecteurs pour accumuler les éléments dans une liste inversée. Cela fait beaucoup d'insertions à l'avant des objets ArrayList
, donc il y a beaucoup de copies en cours.
Stream<T> input = ... ;
List<T> output =
input.collect(ArrayList::new,
(list, e) -> list.add(0, e),
(list1, list2) -> list1.addAll(0, list2));
Il est probablement possible d'écrire un collecteur d'inversion beaucoup plus efficace en utilisant une sorte de structure de données personnalisée.
MISE À JOUR 2016-01-29
Étant donné que cette question a récemment retenu l'attention, je pense que je devrais mettre à jour ma réponse pour résoudre le problème de l'insertion au début de ArrayList
. Ce sera terriblement inefficace avec un grand nombre d’éléments nécessitant une copie O (N ^ 2).
Il est préférable d'utiliser une variable ArrayDeque
, qui prend en charge efficacement l'insertion à l'avant. Une petite ride est que nous ne pouvons pas utiliser la forme à trois arguments de Stream.collect()
; il faut que le contenu du deuxième argument soit fusionné dans le premier, et il n'y a pas d'opération en bloc "add-all-at-front" sur Deque
. Au lieu de cela, nous utilisons addAll()
pour ajouter le contenu du premier argument à la fin du second, puis nous retournons le second. Cela nécessite l'utilisation de la méthode d'usine Collector.of()
.
Le code complet est le suivant:
Deque<String> output =
input.collect(Collector.of(
ArrayDeque::new,
(deq, t) -> deq.addFirst(t),
(d1, d2) -> { d2.addAll(d1); return d2; }));
Le résultat est une Deque
au lieu d'une List
, mais cela ne devrait pas poser trop de problème, car il peut facilement être itéré ou diffusé dans l'ordre inversé.
Solution élégante
List<Integer> list = Arrays.asList(1,2,3,4);
list.stream()
.boxed() // Converts Intstream to Stream<Integer>
.sorted(Collections.reverseOrder()) // Method on Stream<Integer>
.forEach(System.out::println);
Beaucoup des solutions ici trient ou inversent la IntStream
, mais cela nécessite inutilement un stockage intermédiaire. La solution de Stuart Marks est la voie à suivre:
static IntStream revRange(int from, int to) {
return IntStream.range(from, to).map(i -> to - i + from - 1);
}
Il gère également correctement les débordements en passant ce test:
@Test
public void testRevRange() {
assertArrayEquals(revRange(0, 5).toArray(), new int[]{4, 3, 2, 1, 0});
assertArrayEquals(revRange(-5, 0).toArray(), new int[]{-1, -2, -3, -4, -5});
assertArrayEquals(revRange(1, 4).toArray(), new int[]{3, 2, 1});
assertArrayEquals(revRange(0, 0).toArray(), new int[0]);
assertArrayEquals(revRange(0, -1).toArray(), new int[0]);
assertArrayEquals(revRange(MIN_VALUE, MIN_VALUE).toArray(), new int[0]);
assertArrayEquals(revRange(MAX_VALUE, MAX_VALUE).toArray(), new int[0]);
assertArrayEquals(revRange(MIN_VALUE, MIN_VALUE + 1).toArray(), new int[]{MIN_VALUE});
assertArrayEquals(revRange(MAX_VALUE - 1, MAX_VALUE).toArray(), new int[]{MAX_VALUE - 1});
}
Question générale:
Stream ne stocke aucun élément.
Ainsi, itérer des éléments dans l'ordre inverse n'est pas possible sans stocker les éléments dans une collection intermédiaire.
Stream.of("1", "2", "20", "3")
.collect(Collectors.toCollection(ArrayDeque::new)) // or LinkedList
.descendingIterator()
.forEachRemaining(System.out::println);
Mise à jour: LinkedList modifié en ArrayDeque (mieux) voir ici pour plus de détails
Impressions:
3
20
2
1
Par ailleurs, l'utilisation de la méthode sort
n'est pas correcte car elle trie, elle n'inverse PAS (les flux peuvent avoir des éléments non ordonnés)
Question spécifique:
J'ai trouvé cela simple, facile et intuitif (commentaire @Holger copié)
IntStream.iterate(to - 1, i -> i - 1).limit(to - from)
sans bibliothèque externe ...
import Java.util.List;
import Java.util.Collections;
import Java.util.stream.Collector;
public class MyCollectors {
public static <T> Collector<T, ?, List<T>> toListReversed() {
return Collectors.collectingAndThen(Collectors.toList(), l -> {
Collections.reverse(l);
return l;
});
}
}
Si implémenté Comparable<T>
(ex. Integer
, String
, Date
), vous pouvez le faire en utilisant Comparator.reverseOrder()
.
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.stream()
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
Vous pouvez définir votre propre collecteur qui collecte les éléments dans l'ordre inverse:
public static <T> Collector<T, List<T>, List<T>> inReverse() {
return Collector.of(
ArrayList::new,
(l, t) -> l.add(t),
(l, r) -> {l.addAll(r); return l;},
Lists::<T>reverse);
}
Et utilisez-le comme:
stream.collect(inReverse()).forEach(t -> ...)
J'utilise une ArrayList dans l'ordre avant pour insérer efficacement collecter les éléments (à la fin de la liste) et Guava Lists.reverse pour donner efficacement une vue inversée de la liste sans en faire une autre copie.
Voici quelques cas de test pour le collecteur personnalisé:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import Java.util.ArrayList;
import Java.util.List;
import Java.util.function.BiConsumer;
import Java.util.function.BinaryOperator;
import Java.util.function.Function;
import Java.util.function.Supplier;
import Java.util.stream.Collector;
import org.hamcrest.Matchers;
import org.junit.Test;
import com.google.common.collect.Lists;
public class TestReverseCollector {
private final Object t1 = new Object();
private final Object t2 = new Object();
private final Object t3 = new Object();
private final Object t4 = new Object();
private final Collector<Object, List<Object>, List<Object>> inReverse = inReverse();
private final Supplier<List<Object>> supplier = inReverse.supplier();
private final BiConsumer<List<Object>, Object> accumulator = inReverse.accumulator();
private final Function<List<Object>, List<Object>> finisher = inReverse.finisher();
private final BinaryOperator<List<Object>> combiner = inReverse.combiner();
@Test public void associative() {
final List<Object> a1 = supplier.get();
accumulator.accept(a1, t1);
accumulator.accept(a1, t2);
final List<Object> r1 = finisher.apply(a1);
final List<Object> a2 = supplier.get();
accumulator.accept(a2, t1);
final List<Object> a3 = supplier.get();
accumulator.accept(a3, t2);
final List<Object> r2 = finisher.apply(combiner.apply(a2, a3));
assertThat(r1, Matchers.equalTo(r2));
}
@Test public void identity() {
final List<Object> a1 = supplier.get();
accumulator.accept(a1, t1);
accumulator.accept(a1, t2);
final List<Object> r1 = finisher.apply(a1);
final List<Object> a2 = supplier.get();
accumulator.accept(a2, t1);
accumulator.accept(a2, t2);
final List<Object> r2 = finisher.apply(combiner.apply(a2, supplier.get()));
assertThat(r1, equalTo(r2));
}
@Test public void reversing() throws Exception {
final List<Object> a2 = supplier.get();
accumulator.accept(a2, t1);
accumulator.accept(a2, t2);
final List<Object> a3 = supplier.get();
accumulator.accept(a3, t3);
accumulator.accept(a3, t4);
final List<Object> r2 = finisher.apply(combiner.apply(a2, a3));
assertThat(r2, contains(t4, t3, t2, t1));
}
public static <T> Collector<T, List<T>, List<T>> inReverse() {
return Collector.of(
ArrayList::new,
(l, t) -> l.add(t),
(l, r) -> {l.addAll(r); return l;},
Lists::<T>reverse);
}
}
cyclops-react StreamUtils utilise une méthode de flux inversé ( javadoc ).
StreamUtils.reverse(Stream.of("1", "2", "20", "3"))
.forEach(System.out::println);
Cela fonctionne en collectant dans une ArrayList et en utilisant ensuite la classe ListIterator qui peut itérer dans les deux sens, pour parcourir en arrière la liste.
Si vous avez déjà une liste, ce sera plus efficace
StreamUtils.reversedStream(Arrays.asList("1", "2", "20", "3"))
.forEach(System.out::println);
Voici la solution que j'ai trouvée:
private static final Comparator<Integer> BY_ASCENDING_ORDER = Integer::compare;
private static final Comparator<Integer> BY_DESCENDING_ORDER = BY_ASCENDING_ORDER.reversed();
puis en utilisant ces comparateurs:
IntStream.range(-range, 0).boxed().sorted(BY_DESCENDING_ORDER).forEach(// etc...
Je suggérerais d'utiliser jOOλ , c'est une excellente bibliothèque qui ajoute de nombreuses fonctionnalités utiles aux flux et lambdas Java 8.
Vous pouvez alors faire ce qui suit:
List<Integer> list = Arrays.asList(1,2,3,4);
Seq.seq(list).reverse().forEach(System.out::println)
Aussi simple que cela. C'est une bibliothèque assez légère, qu'il convient d'ajouter à tout projet Java 8.
Manière la plus simple (collecte simple - prend en charge les flux parallèles):
public static <T> Stream<T> reverse(Stream<T> stream) {
return stream
.collect(Collector.of(
() -> new ArrayDeque<T>(),
ArrayDeque::addFirst,
(q1, q2) -> { q2.addAll(q1); return q2; })
)
.stream();
}
Méthode avancée (prend en charge les flux parallèles de manière continue):
public static <T> Stream<T> reverse(Stream<T> stream) {
Objects.requireNonNull(stream, "stream");
class ReverseSpliterator implements Spliterator<T> {
private Spliterator<T> spliterator;
private final Deque<T> deque = new ArrayDeque<>();
private ReverseSpliterator(Spliterator<T> spliterator) {
this.spliterator = spliterator;
}
@Override
@SuppressWarnings({"StatementWithEmptyBody"})
public boolean tryAdvance(Consumer<? super T> action) {
while(spliterator.tryAdvance(deque::addFirst));
if(!deque.isEmpty()) {
action.accept(deque.remove());
return true;
}
return false;
}
@Override
public Spliterator<T> trySplit() {
// After traveling started the spliterator don't contain elements!
Spliterator<T> prev = spliterator.trySplit();
if(prev == null) {
return null;
}
Spliterator<T> me = spliterator;
spliterator = prev;
return new ReverseSpliterator(me);
}
@Override
public long estimateSize() {
return spliterator.estimateSize();
}
@Override
public int characteristics() {
return spliterator.characteristics();
}
@Override
public Comparator<? super T> getComparator() {
Comparator<? super T> comparator = spliterator.getComparator();
return (comparator != null) ? comparator.reversed() : null;
}
@Override
public void forEachRemaining(Consumer<? super T> action) {
// Ensure that tryAdvance is called at least once
if(!deque.isEmpty() || tryAdvance(action)) {
deque.forEach(action);
}
}
}
return StreamSupport.stream(new ReverseSpliterator(stream.spliterator()), stream.isParallel());
}
Notez que vous pouvez rapidement l'étendre à d'autres types de flux (IntStream, ...).
Essais:
// Use parallel if you wish only
revert(Stream.of("One", "Two", "Three", "Four", "Five", "Six").parallel())
.forEachOrdered(System.out::println);
Résultats:
Six
Five
Four
Three
Two
One
Notes supplémentaires: Le simplest way
n'est pas très utile lorsqu'il est utilisé avec d'autres opérations de flux (la jointure de collecte rompt le parallélisme). Le advance way
n'a pas ce problème, et il conserve également les caractéristiques initiales du flux, par exemple SORTED
. C'est donc le moyen d'utiliser d'autres opérations de flux après l'inverse.
Que diriez-vous de cette méthode d'utilité?
public static <T> Stream<T> getReverseStream(List<T> list) {
final ListIterator<T> listIt = list.listIterator(list.size());
final Iterator<T> reverseIterator = new Iterator<T>() {
@Override
public boolean hasNext() {
return listIt.hasPrevious();
}
@Override
public T next() {
return listIt.previous();
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
reverseIterator,
Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}
Semble travailler avec tous les cas sans duplication.
On pourrait écrire un collecteur qui collecte les éléments dans l'ordre inverse:
public static <T> Collector<T, ?, Stream<T>> reversed() {
return Collectors.collectingAndThen(Collectors.toList(), list -> {
Collections.reverse(list);
return list.stream();
});
}
Et utilisez-le comme ceci:
Stream.of(1, 2, 3, 4, 5).collect(reversed()).forEach(System.out::println);
Réponse originale (contient un bogue - il ne fonctionne pas correctement pour les flux parallèles):
Une méthode inverse de flux à usage général pourrait ressembler à:
public static <T> Stream<T> reverse(Stream<T> stream) {
LinkedList<T> stack = new LinkedList<>();
stream.forEach(stack::Push);
return stack.stream();
}
Répondre à la question spécifique de l'inversion avec IntStream, ci-dessous a fonctionné pour moi:
IntStream.range(0, 10)
.map(x -> x * -1)
.sorted()
.map(Math::abs)
.forEach(System.out::println);
Ce n'est pas purement Java8, mais si vous utilisez la méthode Lists.reverse () de goyave conjointement, vous pouvez facilement y parvenir:
List<Integer> list = Arrays.asList(1,2,3,4);
Lists.reverse(list).stream().forEach(System.out::println);
la solution la plus simple utilise List::listIterator
et Stream::generate
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
ListIterator<Integer> listIterator = list.listIterator(list.size());
Stream.generate(listIterator::previous)
.limit(list.size())
.forEach(System.out::println);
Pour référence, je cherchais le même problème, je voulais joindre la valeur de chaîne des éléments de flux dans l'ordre inverse.
itemList = {dernier, milieu, premier} => premier, milieu, dernier
J'ai commencé à utiliser une collection intermédiaire avec collectingAndThen
de comonad ou le collectionneur ArrayDeque
de Stuart Marks , même si je n'étais pas satisfait de la collection intermédiaire et de le diffuser à nouveau.
itemList.stream()
.map(TheObject::toString)
.collect(Collectors.collectingAndThen(Collectors.toList(),
strings -> {
Collections.reverse(strings);
return strings;
}))
.stream()
.collect(Collector.joining());
J'ai donc itéré sur la réponse de Stuart Marks qui utilisait la fabrique Collector.of
, qui a l'intéressant finisher lambda.
itemList.stream()
.collect(Collector.of(StringBuilder::new,
(sb, o) -> sb.insert(0, o),
(r1, r2) -> { r1.insert(0, r2); return r1; },
StringBuilder::toString));
Étant donné que dans ce cas, le flux n'est pas parallèle, le combineur n'est pas très pertinent. J'utilise quand même insert
pour des raisons de cohérence du code, mais cela n'a pas d'importance, car cela dépend du constructeur de chaîne construit en premier.
J'ai regardé le StringJoiner, cependant il n'a pas de méthode insert
.
Chaîne d'inversion ou un tableau
(Stream.of("abcdefghijklm 1234567".split("")).collect(Collectors.collectingAndThen(Collectors.toList(),list -> {Collections.reverse(list);return list;}))).stream().forEach(System.out::println);
split peut être modifié en fonction du délimiteur ou de l'espace
C'est comme ça que je le fais.
Je n'aime pas l'idée de créer une nouvelle collection et de l'itérer à l'envers.
L'idée de la carte IntStream # est assez soignée, mais je préfère la méthode IntStream # iterate, car je pense que l'idée d'un compte à rebours à zéro est mieux exprimée avec la méthode iterate et plus facile à comprendre en termes de navigation du tableau de haut en bas.
import static Java.lang.Math.max;
private static final double EXACT_MATCH = 0d;
public static IntStream reverseStream(final int[] array) {
return countdownFrom(array.length - 1).map(index -> array[index]);
}
public static DoubleStream reverseStream(final double[] array) {
return countdownFrom(array.length - 1).mapToDouble(index -> array[index]);
}
public static <T> Stream<T> reverseStream(final T[] array) {
return countdownFrom(array.length - 1).mapToObj(index -> array[index]);
}
public static IntStream countdownFrom(final int top) {
return IntStream.iterate(top, t -> t - 1).limit(max(0, (long) top + 1));
}
Voici quelques tests pour prouver que cela fonctionne:
import static Java.lang.Integer.MAX_VALUE;
import static org.junit.Assert.*;
@Test
public void testReverseStream_emptyArrayCreatesEmptyStream() {
Assert.assertEquals(0, reverseStream(new double[0]).count());
}
@Test
public void testReverseStream_singleElementCreatesSingleElementStream() {
Assert.assertEquals(1, reverseStream(new double[1]).count());
final double[] singleElementArray = new double[] { 123.4 };
assertArrayEquals(singleElementArray, reverseStream(singleElementArray).toArray(), EXACT_MATCH);
}
@Test
public void testReverseStream_multipleElementsAreStreamedInReversedOrder() {
final double[] arr = new double[] { 1d, 2d, 3d };
final double[] revArr = new double[] { 3d, 2d, 1d };
Assert.assertEquals(arr.length, reverseStream(arr).count());
Assert.assertArrayEquals(revArr, reverseStream(arr).toArray(), EXACT_MATCH);
}
@Test
public void testCountdownFrom_returnsAllElementsFromTopToZeroInReverseOrder() {
assertArrayEquals(new int[] { 4, 3, 2, 1, 0 }, countdownFrom(4).toArray());
}
@Test
public void testCountdownFrom_countingDownStartingWithZeroOutputsTheNumberZero() {
assertArrayEquals(new int[] { 0 }, countdownFrom(0).toArray());
}
@Test
public void testCountdownFrom_doesNotChokeOnIntegerMaxValue() {
assertEquals(true, countdownFrom(MAX_VALUE).anyMatch(x -> x == MAX_VALUE));
}
@Test
public void testCountdownFrom_givesZeroLengthCountForNegativeValues() {
assertArrayEquals(new int[0], countdownFrom(-1).toArray());
assertArrayEquals(new int[0], countdownFrom(-4).toArray());
}
Dans tout cela, je ne vois pas la réponse à laquelle je voudrais aller en premier.
Ce n'est pas exactement une réponse directe à la question, mais c'est une solution potentielle au problème.
Construisez simplement la liste à l’arrière. Si vous le pouvez, utilisez une liste LinkedList au lieu d'une liste ArrayList. Lorsque vous ajoutez des éléments, utilisez "Push" au lieu de "add". La liste sera construite dans l'ordre inverse et sera ensuite diffusée correctement sans aucune manipulation.
Cela ne convient pas aux cas où vous utilisez des tableaux primitifs ou des listes qui sont déjà utilisés de diverses manières mais qui fonctionnent bien dans un nombre surprenant de cas.
ArrayDeque
sont plus rapides dans la pile que Stack ou LinkedList. "Push ()" insère des éléments à l'avant de la Deque
protected <T> Stream<T> reverse(Stream<T> stream) {
ArrayDeque<T> stack = new ArrayDeque<>();
stream.forEach(stack::Push);
return stack.stream();
}