Exemplo de método Java 8 Streams FlatMap

86

Tenho vindo a verificar a próxima Java update, a saber: Java 8 or JDK 8. Sim, estou impaciente, há muitas coisas novas, mas há algo que não entendo, um código simples:

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

os javadocs são

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

Retorna um fluxo que consiste nos resultados da substituição de cada elemento deste fluxo com o conteúdo de um fluxo mapeado produzido aplicando a função de mapeamento fornecida a cada elemento. Cada fluxo mapeado é fechado após seu conteúdo ter sido colocado neste fluxo. (Se um fluxo mapeado for nulo, um fluxo vazio será usado, em vez disso.) Esta é uma operação intermediária.

Eu apreciaria se alguém criasse alguns exemplos simples da vida real sobre flatMapcomo você poderia codificá-lo em versões anteriores do Java Java[6,7]e como você pode codificar as mesmas rotinas usando Java 8.

chiperortiz
fonte
2
Existem cerca de um milhão de exemplos de uso de flatMap (pelo menos para Scala, e eles são basicamente os mesmos :)) na internet, você já tentou pesquisar? Aqui está um para começar: brunton-spall.co.uk/post/2011/12/02/…
Peter Svensson
3
Não entendo Scala Nunca trabalhei com
Scala
O que quero dizer é que flatMap é um conceito geral que agora existe em Java e também em Scala.
Peter Svensson
ok vou ler mais sobre isso, cara.
chiperortiz
10
flatMap em Java é a mesma ideia, mas parece bem diferente com streams. Não aponte as pessoas para o Scala!
orbfish

Respostas:

158

Não faz sentido para flatMapum stream que já está plano, como o Stream<Integer>que você mostrou em sua pergunta.

No entanto, se você tivesse um Stream<List<Integer>>, faria sentido e você poderia fazer o seguinte:

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

Que imprimiria:

1
2
3
4
5

Para fazer isso antes do Java 8, você só precisa de loops:

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
fonte
113

Exemplo inventado

Imagine que você deseja criar a seguinte sequência: 1, 2, 2, 3, 3, 3, 4, 4, 4, 4 etc. (em outras palavras: 1x1, 2x2, 3x3 etc.)

Com flatMapele pode ser parecido com:

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

Onde:

  • IntStream.rangeClosed(1, 4)cria um fluxo de int1 a 4, inclusive
  • IntStream.iterate(i, identity()).limit(i)cria um fluxo de comprimento i de inti - então aplicado a i = 4ele cria um fluxo:4, 4, 4, 4
  • flatMap "nivela" o fluxo e "concatena-o" com o fluxo original

Com Java <8, você precisaria de dois loops aninhados:

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

Exemplo do mundo real

Digamos que eu tenha um List<TimeSeries>onde cada TimeSeriesé essencialmente um Map<LocalDate, Double>. Desejo obter uma lista de todas as datas para as quais pelo menos uma das séries temporais tem um valor. flatMappara o resgate:

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

Não apenas é legível, mas se você de repente precisar processar 100k elementos, simplesmente adicionar parallel()irá melhorar o desempenho sem você escrever nenhum código simultâneo.

assilias
fonte
14
Ambos os exemplos são muito melhores do que na resposta aceita.
Sebastian Graf
compilador reclama sobre identidade () como indefinida
Nirmal
2
@ user3320018 você precisa fazer a importação estática Function.identity.
Assylias
@assylias Eu tentei importar java.util.function.Function mas não funcionou, eu sou novo no java 8 e isso pode ou não ser específico do java 8, mas você pode me dizer exatamente como remover esse erro.
Nirmal
4
import static java.util.function.Function.identity;
Assylias
18

Extraia palavras exclusivas classificadas ASC de uma lista de frases:

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

... e a saída:

Unique words: [confounded, incumbent, jailer, perjury, skimming, sporadic]
Igor Baiborodine
fonte
11

Sou o único que acha aborrecido descomprimir listas? ;-)

Vamos tentar com objetos. A propósito, exemplo do mundo real.

Dado: Objeto que representa tarefa repetitiva. Sobre campos de tarefas importantes: os lembretes começam a tocar starte se repetem a cada repeatPeriod repeatUnit(por exemplo, 5 HORAS) e haverá repeatCountlembretes no total (incluindo o início).

Objetivo: obter uma lista de cópias de tarefas, uma para cada chamada de lembrete de tarefa.

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

Resultado:

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: Eu agradeceria se alguém sugerisse uma solução mais simples, afinal não sou um profissional.

ATUALIZAÇÃO: @RBz pediu uma explicação detalhada, então aqui está. Basicamente, flatMap coloca todos os elementos de streams dentro de outro stream no stream de saída. Muitos streams aqui :). Portanto, para cada Tarefa no fluxo inicial, a expressão lambda x -> LongStream.iterate...cria um fluxo de valores longos que representam os momentos de início da tarefa. Este fluxo é limitado a x.getRepeatCount()instâncias. Seus valores começam em x.getStart().toEpochSecond(ZoneOffset.UTC)e cada próximo valor é calculado usando lambda p -> (p + x.getRepeatPeriod()*x.getRepeatUnit().getDuration().getSeconds(). boxed()retorna o stream com cada valor longo como uma instância de wrapper Long. Em seguida, cada Long nesse fluxo é mapeado para uma nova instância de Task que não é mais repetitiva e contém o tempo de execução exato. Este exemplo contém apenas uma tarefa na lista de entrada. Mas imagine que você tem mil. Você terá então um fluxo de 1000 fluxos de objetos Task. E o queflatMapO que faz aqui é colocar todas as tarefas de todos os fluxos no mesmo fluxo de saída. Isso é tudo que eu entendo. Obrigado por sua pergunta!

Aleksandr Kravets
fonte
8
Am I the only one who finds unwinding lists boring?+1
Whitfin
3
Acho muito difícil entender este exemplo. :(
RBz
As operações do @RBz Stream às vezes não são fáceis de entender, especialmente se houver mais de uma operação envolvida. É uma questão de prática. O melhor que você pode fazer é pesquisar no Google cada palavra pouco clara da amostra e tentar usá-la você mesmo. Na verdade, o exemplo de estilo imperativo usual teria sido muito mais fácil de entender (e às vezes mais rápido). Então, pense se você realmente precisa usar streams.
Aleksandr Kravets
Obrigado pela resposta cara. No entanto, estou bem com os conceitos de streams. O que estou tendo problemas aqui é um exemplo específico. Eu não fui muito bom com a API Time, mas mesmo lendo isso não está me ajudando a entender o que está acontecendo aqui. Posso estar sendo ingênuo, mas será ótimo ter um pouco mais de explicação para sua resposta. Isso realmente me ajudaria a entender seu exemplo. Eu sei, estou apenas bloqueado por curiosidade! :)
RBz
Exemplo incrível ... um pouco difícil de entender no começo, mas uma vez que eu executei no meu IDE ... alternativa tão poderosa !! Muito obrigado !
Cristiano
2

Este método recebe uma Função como argumento, esta função aceita um parâmetro T como um argumento de entrada e retorna um fluxo do parâmetro R como um valor de retorno. Quando esta função é aplicada em cada elemento deste fluxo, ela produz um fluxo de novos valores. Todos os elementos desses novos fluxos gerados por cada elemento são então copiados para um novo fluxo, que será um valor de retorno desse método.

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

lalitbhagtani
fonte
2

Um exemplo muito simples: divida uma lista de nomes completos para obter uma lista de nomes, independentemente do primeiro ou último

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

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

Isso imprime:

Barry
Allen
Bruce
Wayne
Clark
Kent
Somaiah Kumbera
fonte
1

Dado isso:

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

    }

e isto:

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

}

Podemos então fazer isso:

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
fonte