[JAVA] Créez une alternative à l'API Stream facile à étendre

introduction

L'API Stream est une API qui traite les colonnes de données ajoutées à partir de Java SE 8. Il est possible d'écrire dans un code facile à comprendre le traitement compliqué qui a été effectué jusqu'à présent pour les collections. Cependant, il y a certains points à s'inquiéter lors de son utilisation.

Je voudrais créer une nouvelle API pour les améliorer. La politique est la suivante.

Tout le code que j'ai écrit dans cet article se trouve sur Essayez de créer une alternative à l'API Stream facile à étendre.

Une interface alternative à Stream

Quand il s'agit de créer une alternative à l'API Stream, vous devez définir l'équivalent de l'interface Stream. Vous pouvez définir une nouvelle interface, mais ici nous utilisons l'interface ʻIterable. L'interface Collection implémente l'interface ʻIterable, donc vous pouvez récupérer directement ʻIterable sans une opération comme list.stream ()`.

Traitement intermédiaire

map

Tout d'abord, implémentons map. Reçoit ʻIterable et renvoie ʻIterable <U> comme résultat. List <U> implémente ʻIterable , donc si vous pensez que vous pouvez retourner List `, vous pouvez le faire.

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;
}

Cependant, cela créera 1 million de ʻArrayList s'il y a 1 million d'éléments dans source. Dans le traitement intermédiaire, il est nécessaire de renvoyer séquentiellement le résultat de l'application de «mapper» lorsque l'élément est nécessaire. Pour ce faire, vous devez implémenter ʻIterator <U> , qui applique mappaer de manière séquentielle. Si vous implémentez ʻIterator , il est facile d'implémenter ʻIterable <U> qui le renvoie. C'est parce que ʻIterable est une interface fonctionnelle dans laquelle seul ʻIterator <U> iterator () est défini.

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());
        }

    };
}

Avant de tester, définissez le processus de terminaison toList. ʻReceive Iterable et renvoie List `. C'est facile car vous pouvez utiliser l'instruction for étendue.

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

Le test ressemble à ceci. J'écrirai également du code en utilisant l'API Stream à des fins de comparaison.

@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);
}

Dans l'API Stream, le traitement est effectué dans une chaîne de méthodes d'instance, donc cela ressemble à un traitement séquentiel de haut en bas, mais comme des méthodes statiques sont utilisées, l'ordre de description est inversé. Cela peut sembler un peu étrange.

filter

Ensuite, implémentons filter. Comme elle doit être traitée séquentiellement comme dans le cas de map, nous définissons une classe interne anonyme qui implémente ʻIterator <T>. map est simple car il y a une correspondance biunivoque entre l'entrée et la sortie, mais filter ne l'est pas, donc c'est un peu ennuyeux.

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;
        }
    };
}

Il est nécessaire de regarder en avant pour voir s'il y a quelque chose qui remplit les conditions de selector lorsque filter est appelé. S'il y a quelque chose à satisfaire, enregistrez-le dans la variable d'instance next et renvoyez-le quand next () est appelé. En même temps, trouvez l'élément qui satisfait le "sélecteur" suivant. Je vais le tester. Trouvez la colonne numérique de la colonne entière extraite de la colonne entière et multipliée par 10.

@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);
}

Dans le cas de Stream, une fois le processus de terminaison effectué, le Stream ne peut pas être réutilisé. Dans le cas de Iterable, vous pouvez enregistrer le résultat intermédiaire et le réutiliser ultérieurement.

@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));
}

Résiliation

La résiliation est facile car vous enregistrez simplement le résultat en utilisant l'instruction for étendue, comme toList () ci-dessus.

toMap

toMap () crée simplement une carte en utilisant Function qui extrait la clé et la valeur de l'élément.

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

Ensuite, implémentons un simple groupingBy. Prenez simplement la clé de l'élément et faites-en une carte. Les éléments avec des clés en double sont regroupés dans la liste.

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;
}

Je vais le tester. Voici un exemple de regroupement selon la longueur d'une chaîne de caractères.

@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);
}

Vient ensuite «groupingBy», qui regroupe par clé puis agrège les éléments en double.

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());
}

Agréger les éléments avec la même clé avec «value Aggregator». Définissons un autre processus de terminaison pour les tests.

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

Les groupes suivants sont classés par longueur de chaîne et comptent le nombre de chaînes de même longueur de chaîne.

@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);
}

Éléments difficiles à mettre en œuvre avec l'API Stream

Enfin, implémentons quelque chose de difficile à implémenter avec l'API Stream. Les deux sont des processus intermédiaires. Toutes les opérations intermédiaires dans l'API Stream sont des méthodes d'instance et ne peuvent pas être facilement étendues.

zip

zip est un processus pour faire correspondre chaque élément du début de deux chaînes de données en une seule chaîne de données. Si les deux colonnes d'entrée ont des longueurs différentes, la plus courte est mise en correspondance et la plus longue est ignorée.

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());
        }

    };
}

Il s'agit d'un test pour organiser la séquence d'entiers et de chaînes.

@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

Il s'agit d'un processus intermédiaire qui accumule des éléments. Similaire à «reduction ()», mais «reduction ()» renvoie une seule valeur pour la terminaison, tandis que «cumulative» renvoie une colonne.

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());
        }

    };
}

Ceci est un test pour trouver la somme partielle depuis le début.

@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 () est également dans l'API Stream, mais il est difficile de comprendre comment l'implémenter, alors je vais le mettre ici.

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;
        }

    };
}

Voici un test. Gonflez chaque élément en deux rangées.

@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);
}

finalement

Je n'ai pas implémenté toutes les API qui prennent en charge l'API Stream, mais j'ai trouvé qu'il était étonnamment facile de créer un produit équivalent.