Java8 Streams FlatMap 方法示例

我一直在检查即将到来的 Java update,即: Java 8 or JDK 8。是的,我没有耐心,有很多新的东西,但是,有些东西我不明白,一些简单的代码:

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

Javadocs 是

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

返回一个流,该流包含将此流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。每个被映射的流在其内容被放入该流之后都将关闭。(如果映射流为空,则使用空流。) 这是一个中间操作。

如果有人能够创建一些关于 flatMap的简单的实际例子,我将非常感激,您如何能够在以前的 Java 版本 Java[6,7]中编写它,以及您如何能够使用 Java 8编写相同的例程。

130720 次浏览

Made up example

Imagine that you want to create the following sequence: 1, 2, 2, 3, 3, 3, 4, 4, 4, 4 etc. (in other words: 1x1, 2x2, 3x3 etc.)

With flatMap it could look like:

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

where:

  • IntStream.rangeClosed(1, 4) creates a stream of int from 1 to 4, inclusive
  • IntStream.iterate(i, identity()).limit(i) creates a stream of length i of int i - so applied to i = 4 it creates a stream: 4, 4, 4, 4
  • flatMap "flattens" the stream and "concatenates" it to the original stream

With Java < 8 you would need two nested loops:

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

Real world example

Let's say I have a List<TimeSeries> where each TimeSeries is essentially a Map<LocalDate, Double>. I want to get a list of all dates for which at least one of the time series has a value. flatMap to the rescue:

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

Not only is it readable, but if you suddenly need to process 100k elements, simply adding parallel() will improve performance without you writing any concurrent code.

It doesn't make sense to flatMap a Stream that's already flat, like the Stream<Integer> you've shown in your question.

However, if you had a Stream<List<Integer>> then it would make sense and you could do this:

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

Which would print:

1
2
3
4
5

To do this pre-Java 8 you just need a 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);
}

Am I the only one who finds unwinding lists boring? ;-)

Let's try with objects. Real world example by the way.

Given: Object representing repetitive task. About important task fields: reminders are starting to ring at start and repeat every repeatPeriod repeatUnit(e.g. 5 HOURS) and there will be repeatCount reminders in total(including starting one).

Goal: achieve a list of task copies, one for each task reminder invocation.

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

Output:

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}

P.S.: I would appreciate if someone suggested a simpler solution, I'm not a pro after all.

UPDATE: @RBz asked for detailed explanation so here it is. Basically flatMap puts all elements from streams inside another stream into output stream. A lot of streams here :). So, for each Task in initial stream lambda expression x -> LongStream.iterate... creates a stream of long values that represent task start moments. This stream is limited to x.getRepeatCount() instances. It's values start from x.getStart().toEpochSecond(ZoneOffset.UTC) and each next value is calculated using lambda p -> (p + x.getRepeatPeriod()*x.getRepeatUnit().getDuration().getSeconds(). boxed() returns the stream with each long value as a Long wrapper instance. Then each Long in that stream is mapped to new Task instance that is not repetitive anymore and contains exact execution time. This sample contains only one Task in input list. But imagine that you have a thousand. You will have then a stream of 1000 streams of Task objects. And what flatMap does here is putting all Tasks from all streams onto the same output stream. That's all as I understand it. Thank you for your question!

Extract unique words sorted ASC from a list of phrases:

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

... and the output:

Unique words: [confounded, incumbent, jailer, perjury, skimming, sporadic]

Given this:

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


}

and this:

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


}

We can then do this:

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

This method takes one Function as an argument, this function accepts one parameter T as an input argument and return one stream of parameter R as a return value. When this function is applied on each element of this stream, it produces a stream of new values. All the elements of these new streams generated by each element are then copied to a new stream, which will be a return value of this method.

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

A very simple example: Split a list of full names to get a list of names, regardless of first or last

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


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

This prints out:

Barry
Allen
Bruce
Wayne
Clark
Kent