Przykład metody Java 8 Streams FlatMap

85

Sprawdzałem nadchodzące Java update, a mianowicie:Java 8 or JDK 8 . Tak, nie mogę się doczekać, jest dużo nowych rzeczy, ale jest coś, czego nie rozumiem, prosty kod:

final Stream<Integer>stream = Stream.of(1,2,3,4,5,6,7,8,9,10);
stream.flatMap();

javadocs są

public <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

Zwraca strumień składający się z wyników zastąpienia każdego elementu tego strumienia zawartością zmapowanego strumienia utworzonego przez zastosowanie dostarczonej funkcji mapowania do każdego elementu. Każdy zmapowany strumień jest zamykany po umieszczeniu jego zawartości w tym strumieniu. (Jeśli zmapowany strumień ma wartość null, zamiast tego używany jest pusty strumień). Jest to operacja pośrednia.

Byłbym wdzięczny, gdyby ktoś stworzył proste przykłady z życia wzięte flatMap, jak można to zakodować w poprzednich wersjach Java Java[6,7]i jak można zakodować te same procedury przy użyciu Java 8.

chiperortiz
źródło
2
Istnieje około miliona przykładów użycia flatMap (przynajmniej dla Scali, a są one w zasadzie takie same :)) w Internecie, czy próbowałeś wyszukiwać? Oto jeden, od którego można zacząć: brunton-spall.co.uk/post/2011/12/02/…
Peter Svensson
3
nie rozumiem Scali nigdy nie pracowałem z scala
chiperortiz
Chodzi mi o to, że flatMap to ogólna koncepcja, która obecnie istnieje zarówno w Javie, jak i Scali.
Peter Svensson
ok, przeczytam o tym więcej dzięki stary.
chiperortiz
10
flatMap w Javie to ten sam pomysł, ale wygląda zupełnie inaczej w przypadku strumieni. Nie kieruj ludzi do Scali!
orbfish

Odpowiedzi:

158

Nie ma to sensu w flatMapprzypadku strumienia, który jest już płaski, jak ten Stream<Integer>, który pokazałeś w swoim pytaniu.

Jednak gdybyś miał Stream<List<Integer>>to miałoby to sens i możesz to zrobić:

Stream<List<Integer>> integerListStream = Stream.of(
    Arrays.asList(1, 2), 
    Arrays.asList(3, 4), 
    Arrays.asList(5)
);

Stream<Integer> integerStream = integerListStream .flatMap(Collection::stream);
integerStream.forEach(System.out::println);

Który wydrukowałby:

1
2
3
4
5

Aby to zrobić przed Java 8, potrzebujesz tylko pętli:

List<List<Integer>> integerLists = Arrays.asList(
    Arrays.asList(1, 2), 
    Arrays.asList(3, 4), 
    Arrays.asList(5)
)

List<Integer> flattened = new ArrayList<>();

for (List<Integer> integerList : integerLists) {
    flattened.addAll(integerList);
}

for (Integer i : flattened) {
    System.out.println(i);
}
Nick Holt
źródło
113

Zmyślony przykład

Wyobraź sobie, że chcesz stworzyć następującą sekwencję: 1, 2, 2, 3, 3, 3, 4, 4, 4, 4 itd. (Innymi słowy: 1x1, 2x2, 3x3 itd.)

Dzięki flatMaptemu mogłoby wyglądać:

IntStream sequence = IntStream.rangeClosed(1, 4)
                          .flatMap(i -> IntStream.iterate(i, identity()).limit(i));
sequence.forEach(System.out::println);

gdzie:

  • IntStream.rangeClosed(1, 4)tworzy strumień intod 1 do 4 włącznie
  • IntStream.iterate(i, identity()).limit(i)tworzy strumień o długości i z inti - zastosowany do i = 4niego tworzy strumień:4, 4, 4, 4
  • flatMap „spłaszcza” strumień i „łączy” go z oryginalnym strumieniem

W Javie <8 potrzebujesz dwóch zagnieżdżonych pętli:

List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 4; i++) {
    for (int j = 0; j < i; j++) {
        list.add(i);
    }
}

Przykład z prawdziwego świata

Powiedzmy, że mam List<TimeSeries>gdzie każdy TimeSeriesjest zasadniczo a Map<LocalDate, Double>. Chcę uzyskać listę wszystkich dat, dla których co najmniej jeden szereg czasowy ma wartość. flatMapna pomoc:

list.stream().parallel()
    .flatMap(ts -> ts.dates().stream()) // for each TS, stream dates and flatmap
    .distinct()                         // remove duplicates
    .sorted()                           // sort ascending
    .collect(toList());

Jest nie tylko czytelny, ale jeśli nagle potrzebujesz przetworzyć 100k elementów, samo dodanie parallel()poprawi wydajność bez pisania współbieżnego kodu.

asylias
źródło
14
Oba przykłady są dużo lepsze niż w przyjętej odpowiedzi.
Sebastian Graf
kompilator narzeka na tożsamość () jako niezdefiniowaną
Nirmal
2
@ user3320018 musisz dokonać importu statycznego Function.identity.
assylias
@assylias Próbowałem zaimportować java.util.function.Function, ale nie zadziałało, jestem nowy w Javie 8 i może to być specyficzne dla Java 8, ale czy możesz mi powiedzieć dokładnie, jak usunąć ten błąd.
Nirmal
4
import static java.util.function.Function.identity;
assylias
18

Wyodrębnij unikalne słowa posortowane ASC z listy fraz:

List<String> phrases = Arrays.asList(
        "sporadic perjury",
        "confounded skimming",
        "incumbent jailer",
        "confounded jailer");

List<String> uniqueWords = phrases
        .stream()
        .flatMap(phrase -> Stream.of(phrase.split("\\s+")))
        .distinct()
        .sorted()
        .collect(Collectors.toList());
System.out.println("Unique words: " + uniqueWords);

... i wyjście:

Unique words: [confounded, incumbent, jailer, perjury, skimming, sporadic]
Igor Baiborodine
źródło
11

Czy tylko ja uważam, że listy rozwijania są nudne? ;-)

Spróbujmy z przedmiotami. Nawiasem mówiąc, przykład z prawdziwego świata.

Dane: Obiekt reprezentujący powtarzalne zadanie. O ważnych polach zadań: przypomnienia zaczynają dzwonić starti powtarzać się co repeatPeriod repeatUnit(np. 5 GODZIN) i będąrepeatCount przypomnienia w sumie (w tym jedno początkowe).

Cel: uzyskać listę kopii zadań, po jednej na każde wywołanie przypomnienia o zadaniu.

List<Task> tasks =
            Arrays.asList(
                    new Task(
                            false,//completed sign
                            "My important task",//task name (text)
                            LocalDateTime.now().plus(2, ChronoUnit.DAYS),//first reminder(start)
                            true,//is task repetitive?
                            1,//reminder interval
                            ChronoUnit.DAYS,//interval unit
                            5//total number of reminders
                    )
            );

tasks.stream().flatMap(
        x -> LongStream.iterate(
                x.getStart().toEpochSecond(ZoneOffset.UTC),
                p -> (p + x.getRepeatPeriod()*x.getRepeatUnit().getDuration().getSeconds())
        ).limit(x.getRepeatCount()).boxed()
        .map( y -> new Task(x,LocalDateTime.ofEpochSecond(y,0,ZoneOffset.UTC)))
).forEach(System.out::println);

Wynik:

Task{completed=false, text='My important task', start=2014-10-01T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-02T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-03T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-04T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-05T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}

PS: Byłbym wdzięczny, gdyby ktoś zaproponował prostsze rozwiązanie, w końcu nie jestem profesjonalistą.

UPDATE: @RBz poprosił o szczegółowe wyjaśnienie, więc oto jest. Zasadniczo flatMap umieszcza wszystkie elementy ze strumieni wewnątrz innego strumienia w strumieniu wyjściowym. Dużo tu streamów :). Tak więc dla każdego zadania w początkowym wyrażeniu lambda strumienia x -> LongStream.iterate...tworzy strumień długich wartości, które reprezentują momenty rozpoczęcia zadania. Ten strumień jest ograniczony do x.getRepeatCount()instancji. Jego wartości zaczynają się od, x.getStart().toEpochSecond(ZoneOffset.UTC)a każda następna wartość jest obliczana za pomocą lambda p -> (p + x.getRepeatPeriod()*x.getRepeatUnit().getDuration().getSeconds(). boxed()zwraca strumień z każdą długą wartością jako instancję opakowania Long. Następnie każdy Long w tym strumieniu jest mapowany na nową instancję Task, która nie jest już powtarzalna i zawiera dokładny czas wykonania. Ten przykład zawiera tylko jedno zadanie na liście wejściowej. Ale wyobraź sobie, że masz tysiąc. Będziesz mieć wtedy strumień 1000 strumieni obiektów Task. I coflatMaprobi to umieszczenie wszystkich zadań ze wszystkich strumieni w tym samym strumieniu wyjściowym. To wszystko, tak jak ja to rozumiem. Dziękuję za Twoje pytanie!

Aleksandr Kravets
źródło
8
Am I the only one who finds unwinding lists boring?+1
whitfin
3
Naprawdę trudno mi zrozumieć ten przykład. :(
RBz
Operacje @RBz Stream czasami nie są łatwe do zrozumienia, zwłaszcza jeśli zaangażowanych jest więcej niż jedna operacja. Jest to jednak kwestia praktyki. Najlepsze, co możesz zrobić, to wygooglować każde niejasne słowo z próbki i spróbować użyć go samodzielnie. W rzeczywistości zwykła próbka stylu imperatywnego byłaby znacznie łatwiejsza do zrozumienia (a czasami szybsza). Pomyśl więc, czy naprawdę potrzebujesz strumieni.
Aleksandr Kravets
Dzięki za odpowiedź. Jednak jestem całkiem w porządku z koncepcjami strumieni. To, co mam w tym przypadku, dotyczy konkretnego przykładu. Nie byłem zbyt dobry z Time API, ale nawet przeczytanie go nie pomaga mi zrozumieć, co się tutaj dzieje. Być może jestem naiwny, ale byłoby wspaniale mieć trochę więcej wyjaśnień dla twojej odpowiedzi. Naprawdę pomogłoby mi to w zrozumieniu twojego przykładu. Wiem, jestem po prostu zamknięty z ciekawości! :)
RBz
Niesamowity przykład ... na początku trochę trudny do zrozumienia, ale kiedy uruchomię go w moim IDE ... tak potężna alternatywa !! wielkie dzięki !
Cristiano,
2

Ta metoda przyjmuje jedną funkcję jako argument, ta funkcja przyjmuje jeden parametr T jako argument wejściowy i zwraca jeden strumień parametru R jako wartość zwracaną. Gdy ta funkcja jest stosowana do każdego elementu tego strumienia, tworzy strumień nowych wartości. Wszystkie elementy tych nowych strumieni generowanych przez każdy element są następnie kopiowane do nowego strumienia, który będzie zwracaną wartością tej metody.

http://codedestine.com/java-8-stream-flatmap-method/

lalitbhagtani
źródło
2

Bardzo prosty przykład: podziel listę imion i nazwisk, aby uzyskać listę nazwisk, niezależnie od imienia i nazwiska

 List<String> fullNames = Arrays.asList("Barry Allen", "Bruce Wayne", "Clark Kent");

 fullNames.stream()
            .flatMap(fullName -> Pattern.compile(" ").splitAsStream(fullName))
            .forEach(System.out::println);

To drukuje:

Barry
Allen
Bruce
Wayne
Clark
Kent
Somaiah Kumbera
źródło
1

Biorąc to pod uwagę:

  public class SalesTerritory
    {
        private String territoryName;
        private Set<String> geographicExtents;

        public SalesTerritory( String territoryName, Set<String> zipCodes )
        {
            this.territoryName = territoryName;
            this.geographicExtents = zipCodes;
        }

        public String getTerritoryName()
        {
            return territoryName;
        }

        public void setTerritoryName( String territoryName )
        {
            this.territoryName = territoryName;
        }

        public Set<String> getGeographicExtents()
        {
            return geographicExtents != null ? Collections.unmodifiableSet( geographicExtents ) : Collections.emptySet();
        }

        public void setGeographicExtents( Set<String> geographicExtents )
        {
            this.geographicExtents = new HashSet<>( geographicExtents );
        }

        @Override
        public int hashCode()
        {
            int hash = 7;
            hash = 53 * hash + Objects.hashCode( this.territoryName );
            return hash;
        }

        @Override
        public boolean equals( Object obj )
        {
            if ( this == obj ) {
                return true;
            }
            if ( obj == null ) {
                return false;
            }
            if ( getClass() != obj.getClass() ) {
                return false;
            }
            final SalesTerritory other = (SalesTerritory) obj;
            if ( !Objects.equals( this.territoryName, other.territoryName ) ) {
                return false;
            }
            return true;
        }

        @Override
        public String toString()
        {
            return "SalesTerritory{" + "territoryName=" + territoryName + ", geographicExtents=" + geographicExtents + '}';
        }

    }

i to:

public class SalesTerritories
{
    private static final Set<SalesTerritory> territories
        = new HashSet<>(
            Arrays.asList(
                new SalesTerritory[]{
                    new SalesTerritory( "North-East, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Maine", "New Hampshire", "Vermont",
                                                                                    "Rhode Island", "Massachusetts", "Connecticut",
                                                                                    "New York", "New Jersey", "Delaware", "Maryland",
                                                                                    "Eastern Pennsylvania", "District of Columbia" } ) ) ),
                    new SalesTerritory( "Appalachia, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "West-Virgina", "Kentucky",
                                                                                    "Western Pennsylvania" } ) ) ),
                    new SalesTerritory( "South-East, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Virginia", "North Carolina", "South Carolina",
                                                                                    "Georgia", "Florida", "Alabama", "Tennessee",
                                                                                    "Mississippi", "Arkansas", "Louisiana" } ) ) ),
                    new SalesTerritory( "Mid-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Ohio", "Michigan", "Wisconsin", "Minnesota",
                                                                                    "Iowa", "Missouri", "Illinois", "Indiana" } ) ) ),
                    new SalesTerritory( "Great Plains, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Oklahoma", "Kansas", "Nebraska",
                                                                                    "South Dakota", "North Dakota",
                                                                                    "Eastern Montana",
                                                                                    "Wyoming", "Colorada" } ) ) ),
                    new SalesTerritory( "Rocky Mountain, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Western Montana", "Idaho", "Utah", "Nevada" } ) ) ),
                    new SalesTerritory( "South-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Arizona", "New Mexico", "Texas" } ) ) ),
                    new SalesTerritory( "Pacific North-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Washington", "Oregon", "Alaska" } ) ) ),
                    new SalesTerritory( "Pacific South-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "California", "Hawaii" } ) ) )
                }
            )
        );

    public static Set<SalesTerritory> getAllTerritories()
    {
        return Collections.unmodifiableSet( territories );
    }

    private SalesTerritories()
    {
    }

}

Następnie możemy to zrobić:

System.out.println();
System.out
    .println( "We can use 'flatMap' in combination with the 'AbstractMap.SimpleEntry' class to flatten a hierarchical data-structure to a set of Key/Value pairs..." );
SalesTerritories.getAllTerritories()
    .stream()
    .flatMap( t -> t.getGeographicExtents()
        .stream()
        .map( ge -> new SimpleEntry<>( t.getTerritoryName(), ge ) )
    )
    .map( e -> String.format( "%-30s : %s",
                              e.getKey(),
                              e.getValue() ) )
    .forEach( System.out::println );
G Butler
źródło