[JAVA] Erstellen Sie eine einfach zu erweiternde Stream-API-Alternative

Einführung

Die Stream-API ist eine API, die Datenspalten verarbeitet, die aus Java SE 8 hinzugefügt wurden. Es ist möglich, die komplizierte Verarbeitung, die bisher für Sammlungen durchgeführt wurde, in leicht verständlichen Code zu schreiben. Es gibt jedoch einige Punkte, über die Sie sich Sorgen machen müssen, wenn Sie es tatsächlich verwenden.

Ich möchte eine neue API erstellen, um diese zu verbessern. Die Richtlinie lautet wie folgt.

Der gesamte Code, den ich in diesem Artikel geschrieben habe, finden Sie unter Versuchen Sie, eine einfach zu erweiternde Stream-API-Alternative zu erstellen.

Eine alternative Schnittstelle zu Stream

Wenn Sie eine Alternative zur Stream-API erstellen möchten, müssen Sie das Äquivalent der Stream-Schnittstelle definieren. Sie können eine neue Schnittstelle definieren, aber hier verwenden wir die "Iterable" -Schnittstelle. Die "Collection" -Schnittstelle implementiert die "Iterable" -Schnittstelle, sodass Sie die "Iterable" direkt ohne eine Operation wie "list.stream ()" abrufen können.

Zwischenverarbeitung

map

Lassen Sie uns zuerst map implementieren. Empfängt "Iterable " und gibt als Ergebnis "Iterable " zurück. List <U> implementiert Iterable <U>. Wenn Sie also der Meinung sind, dass Sie List <U> zurückgeben können, können Sie dies tun.

public static <T, U> Iterable<U> map(Function<T, U> mapper, Iterable<T> source) {
    List<U> result = new ArrayList<>();
    for (T element : source)
        result.add(mapper.apply(element));
    return result;
}

Dies erzeugt jedoch 1 Million "ArrayList", wenn 1 Million Elemente in "Quelle" vorhanden sind. Bei der Zwischenverarbeitung ist es erforderlich, das Ergebnis der Anwendung von "Mapper" nacheinander zurückzugeben, wenn das Element benötigt wird. Dazu müssen Sie "Iterator " implementieren, der "Mapper" nacheinander anwendet. Sobald Sie "Iterator " implementiert haben, ist es einfach, "Iterable " zu implementieren, das es zurückgibt. Dies liegt daran, dass "Iterable " eine funktionale Schnittstelle ist, in der nur "Iterator iterator ()" definiert ist.

public static <T, U> Iterable<U> map(Function<T, U> mapper, Iterable<T> source) {
    return () -> new Iterator<U>() {

        final Iterator<T> iterator = source.iterator();

        @Override
        public boolean hasNext() {
            return iterator.hasNext();
        }

        @Override
        public U next() {
            return mapper.apply(iterator.next());
        }

    };
}

Definieren Sie vor dem Testen den Beendigungsprozess "toList". Empfängt "Iterable " und gibt "List " zurück. Es ist einfach, weil Sie die erweiterte for-Anweisung verwenden können.

public static <T> List<T> toList(Iterable<T> source) {
    List<T> result = new ArrayList<>();
    for (T element : source)
        result.add(element);
    return result;
}

Der Test sieht so aus. Ich werde zum Vergleich auch Code mit der Stream-API schreiben.

@Test
void testMap() {
    List<String> actual = toList(
        map(String::toUpperCase,
            List.of("a", "b", "c")));
    List<String> expected = List.of("A", "B", "C");
    assertEquals(actual, expected);

    List<String> stream = List.of("a", "b", "c").stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());
    assertEquals(stream, expected);
}

In der Stream-API wird die Verarbeitung in einer Kette von Instanzmethoden ausgeführt, sodass es sich anfühlt, als würde die Verarbeitung sequentiell von oben nach unten erfolgen. Da jedoch statische Methoden verwendet werden, wird die Reihenfolge der Beschreibung umgekehrt. Es mag sich etwas seltsam anfühlen.

filter

Als nächstes implementieren wir filter. Da wir es wie im Fall von "map" sequentiell verarbeiten müssen, definieren wir eine anonyme innere Klasse, die "Iterator " implementiert. map ist einfach, weil es eine Eins-zu-Eins-Entsprechung zwischen Eingabe und Ausgabe gibt, aber filter ist nicht so, also ist es ein wenig nervig.

public static <T> Iterable<T> filter(Predicate<T> selector, Iterable<T> source) {
    return () -> new Iterator<T>() {

        final Iterator<T> iterator = source.iterator();
        boolean hasNext = advance();
        T next;

        boolean advance() {
            while (iterator.hasNext())
                if (selector.test(next = iterator.next()))
                    return true;
            return false;
        }

        @Override
        public boolean hasNext() {
            return hasNext;
        }

        @Override
        public T next() {
            T result = next;
            hasNext = advance();
            return result;
        }
    };
}

Es ist notwendig, nach vorne zu schauen, um zu sehen, ob es etwas gibt, das die Bedingungen von "Selektor" erfüllt, wenn "Filter" aufgerufen wird. Wenn es etwas zu befriedigen gibt, speichern Sie es in der Instanzvariablen "next" und geben Sie es zurück, wenn "next ()" aufgerufen wird. Suchen Sie gleichzeitig das Element, das den folgenden "Selektor" erfüllt. Ich werde es testen. Nehmen Sie nur die gerade Zahl aus der Folge von ganzen Zahlen und multiplizieren Sie sie mit 10, um die Zahlenspalte zu finden.

@Test
void testFilter() {
    List<Integer> actual = toList(
        map(i -> i * 10,
            filter(i -> i % 2 == 0,
                List.of(0, 1, 2, 3, 4, 5))));
    List<Integer> expected = List.of(0, 20, 40);
    assertEquals(expected, actual);

    List<Integer> stream = List.of(0, 1, 2, 3, 4, 5).stream()
        .filter(i -> i % 2 == 0)
        .map(i -> i * 10)
        .collect(Collectors.toList());
    assertEquals(stream, actual);
}

Im Fall von Stream kann der Stream nach Abschluss des Beendigungsvorgangs nicht mehr verwendet werden. Im Fall von Iterable können Sie das Zwischenergebnis speichern und später wiederverwenden.

@Test
void testSaveFilter() {
    Iterable<Integer> saved;
    List<Integer> actual = toList(
        map(i -> i * 10,
            saved = filter(i -> i % 2 == 0,
                List.of(0, 1, 2, 3, 4, 5))));
    List<Integer> expected = List.of(0, 20, 40);
    assertEquals(expected, actual);

    assertEquals(List.of(0, 2, 4), toList(saved));
}

Beendigung

Die Beendigung ist einfach, da Sie das Ergebnis einfach mit der erweiterten for-Anweisung wie der obigen toList () speichern.

toMap

toMap () erstellt einfach eine Map mit Function, die den Schlüssel und den Wert aus dem Element extrahiert.

public static <T, K, V> Map<K, V> toMap(Function<T, K> keyExtractor,
    Function<T, V> valueExtractor, Iterable<T> source) {
    Map<K, V> result = new LinkedHashMap<>();
    for (T element : source)
        result.put(keyExtractor.apply(element), valueExtractor.apply(element));
    return result;
}

groupingBy

Als nächstes implementieren wir ein einfaches groupingBy. Nehmen Sie einfach den Schlüssel aus dem Element und machen Sie daraus eine Karte. Elemente mit doppelten Schlüsseln werden in die Liste gepackt.

public static <T, K> Map<K, List<T>> groupingBy(Function<T, K> keyExtractor,
    Iterable<T> source) {
    Map<K, List<T>> result = new LinkedHashMap<>();
    for (T e : source)
        result.computeIfAbsent(keyExtractor.apply(e), k -> new ArrayList<>()).add(e);
    return result;
}

Ich werde es testen. Dies ist ein Beispiel für die Gruppierung nach der Länge einer Zeichenfolge.

@Test
public void testGroupingBy() {
    Map<Integer, List<String>> actual = groupingBy(String::length,
        List.of("one", "two", "three", "four", "five"));
    Map<Integer, List<String>> expected = Map.of(
        3, List.of("one", "two"),
        5, List.of("three"),
        4, List.of("four", "five"));
    assertEquals(expected, actual);

    Map<Integer, List<String>> stream =
        List.of("one", "two", "three", "four", "five").stream()
        .collect(Collectors.groupingBy(String::length));
    assertEquals(stream, actual);
}

Als nächstes folgt groupingBy, das nach Schlüssel gruppiert und dann doppelte Elemente aggregiert.

static <T, K, V> Map<K, V> groupingBy(Function<T, K> keyExtractor,
    Function<Iterable<T>, V> valueAggregator, Iterable<T> source) {
    return toMap(Entry::getKey, e -> valueAggregator.apply(e.getValue()),
        groupingBy(keyExtractor, source).entrySet());
}

Aggregieren Sie Elemente mit demselben Schlüssel mit "value Aggregator". Definieren wir einen weiteren Beendigungsprozess zum Testen.

public static <T> long count(Iterable<T> source) {
    long count = 0;
    for (@SuppressWarnings("unused")
    T e : source)
        ++count;
    return count;
}

Die folgenden Gruppen gruppieren nach Zeichenfolgenlänge und zählen die Anzahl der Zeichenfolgen mit derselben Zeichenfolgenlänge.

@Test
public void testGroupingByCount() {
    Map<Integer, Long> actual = groupingBy(String::length, s -> count(s),
        List.of("one", "two", "three", "four", "five"));
    Map<Integer, Long> expected = Map.of(3, 2L, 5, 1L, 4, 2L);
    assertEquals(expected, actual);

    Map<Integer, Long> stream = List.of("one", "two", "three", "four", "five").stream()
        .collect(Collectors.groupingBy(String::length, Collectors.counting()));
    assertEquals(stream, actual);
}

Dinge, die mit der Stream-API schwer zu implementieren sind

Lassen Sie uns abschließend etwas implementieren, das mit der Stream-API schwer zu implementieren ist. Beides sind Zwischenprozesse. Alle Zwischenoperationen in der Stream-API sind Instanzmethoden und können nicht einfach erweitert werden.

zip

zip ist ein Prozess, bei dem jedes Element vom Anfang zweier Datenzeichenfolgen zu einer Datenzeichenfolge abgeglichen wird. Wenn die beiden Eingabespalten unterschiedlich lang sind, wird die kürzere angepasst und die längere ignoriert.

static <T, U, V> Iterable<V> zip(BiFunction<T, U, V> zipper, Iterable<T> source1,
    Iterable<U> source2) {
    return () -> new Iterator<V>() {

        final Iterator<T> iterator1 = source1.iterator();
        final Iterator<U> iterator2 = source2.iterator();

        @Override
        public boolean hasNext() {
            return iterator1.hasNext() && iterator2.hasNext();
        }

        @Override
        public V next() {
            return zipper.apply(iterator1.next(), iterator2.next());
        }

    };
}

Dies ist ein Test zum Anordnen der Reihenfolge von Ganzzahlen und Zeichenfolgen.

@Test
void testZip() {
    List<String> actual = toList(
        zip((x, y) -> x + "-" + y,
            List.of(0, 1, 2),
            List.of("zero", "one", "two")));
    List<String> expected = List.of("0-zero", "1-one", "2-two");
    assertEquals(expected, actual);
}

cumulative

Dies ist ein Zwischenprozess, bei dem Elemente akkumuliert werden. Ähnlich wie "redu ()", aber "redu ()" gibt einen einzelnen Wert zur Beendigung zurück, während "kumulativ" eine Spalte zurückgibt.

public static <T, U> Iterable<U> cumulative(U unit, BiFunction<U, T, U> function,
    Iterable<T> source) {
    return () -> new Iterator<U>() {

        Iterator<T> iterator = source.iterator();
        U accumlator = unit;

        @Override
        public boolean hasNext() {
            return iterator.hasNext();
        }

        @Override
        public U next() {
            return accumlator = function.apply(accumlator, iterator.next());
        }

    };
}

Dies ist ein Test, um die Teilsumme von Anfang an zu finden.

@Test
public void testCumalative() {
    List<Integer> actual = toList(
        cumulative(0, (x, y) -> x + y,
            List.of(0, 1, 2, 3, 4, 5)));
    List<Integer> expected = List.of(0, 1, 3, 6, 10, 15);
    assertEquals(expected, actual);
}

flatMap

flatMap () ist auch in der Stream-API enthalten, aber es ist schwer zu verstehen, wie es implementiert wird. Deshalb werde ich es hier einfügen.

public static <T, U> Iterable<U> flatMap(Function<T, Iterable<U>> flatter, Iterable<T> source) {
    return () -> new Iterator<U>() {

        final Iterator<T> parent = source.iterator();
        Iterator<U> child = null;
        boolean hasNext = advance();
        U next;

        boolean advance() {
            while (true) {
                if (child == null) {
                    if (!parent.hasNext())
                        return false;
                    child = flatter.apply(parent.next()).iterator();
                }
                if (child.hasNext()) {
                    next = child.next();
                    return true;
                }
                child = null;
            }
        }

        @Override
        public boolean hasNext() {
            return hasNext;
        }

        @Override
        public U next() {
            U result = next;
            hasNext = advance();
            return result;
        }

    };
}

Unten ist ein Test. Blasen Sie jedes Element in zwei Reihen auf.

@Test
public void testFlatMap() {
    List<Integer> actual = toList(
        flatMap(i -> List.of(i, i),
            List.of(0, 1, 2, 3)));
    List<Integer> expected = List.of(0, 0, 1, 1, 2, 2, 3, 3);
    assertEquals(expected, actual);

    List<Integer> stream = List.of(0, 1, 2, 3).stream()
        .flatMap(i -> Stream.of(i, i))
        .collect(Collectors.toList());
    assertEquals(stream, actual);
}

Schließlich

Ich habe nicht alle APIs implementiert, die die Stream-API unterstützen, aber ich fand, dass es überraschend einfach war, ein gleichwertiges Produkt zu erstellen.