Created
October 23, 2013 20:31
-
-
Save nicholashagen/7126156 to your computer and use it in GitHub Desktop.
The following demonstrates various examples of the new Streaming API in Java 8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.devchat.lambdas.examples; | |
import java.io.BufferedReader; | |
import java.io.InputStreamReader; | |
import java.nio.charset.Charset; | |
import java.nio.file.FileSystems; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.Comparator; | |
import java.util.List; | |
import java.util.LongSummaryStatistics; | |
import java.util.Map; | |
import java.util.Optional; | |
import java.util.Random; | |
import java.util.Set; | |
import java.util.function.BinaryOperator; | |
import java.util.function.Function; | |
import java.util.function.Predicate; | |
import java.util.function.Supplier; | |
import java.util.function.ToDoubleFunction; | |
import java.util.function.ToLongFunction; | |
import java.util.jar.JarEntry; | |
import java.util.jar.JarFile; | |
import java.util.regex.Pattern; | |
import java.util.stream.Collector; | |
import java.util.stream.Collectors; | |
import java.util.stream.DoubleStream; | |
import java.util.stream.IntStream; | |
import java.util.stream.Stream; | |
import java.util.zip.ZipEntry; | |
import java.util.zip.ZipFile; | |
/** | |
* <p> | |
* The following provide various examples of the new Streaming API in Java 8. | |
* The Streaming API provides the ability to operate on a set of arbitrary data | |
* using the power of parallelism (although not enforced) and Lambda expressions. | |
* The Streaming API should not be confused with traditional I/O streams in Java, | |
* although I/O streams do support the Streaming API in some contexts. The | |
* streaming API essentially supports traversing, filtering, mapping, grouping, | |
* sorting, and a variety of other operations in a generic capability. For | |
* example, using the Streaming API, one could easily take an existing set of | |
* data, filter certain data out, map a given property, and collect the new | |
* data into a new data set. | |
* </p> | |
* | |
* <p> | |
* The Streaming API defines three core constructs: | |
* </p> | |
* | |
* <ol> | |
* <li> | |
* Streams are <em>born</em> at a particular source such as a collection, file | |
* system, etc. Streams may either be created as a parallel stream | |
* or a traditional stream. Resulting operations are not impacted in | |
* either scenario. | |
* </li> | |
* <li> | |
* Streams support a variety of <em>intermediate operations</em> that operate | |
* on the particular stream and create a new resultant stream. Intermediate | |
* operations are typically lazy and only invoked when a terminal operation | |
* occurs. Intermediate operations include concepts such as filtering data, | |
* mapping data to new data (ie: user to their address), sorting data, etc. | |
* </li> | |
* <li> | |
* Streams are complete when a <em>terminal operation</em> is invoked on a | |
* stream. Terminal operations essentially pull data from the stream, thus | |
* invoking any intermediate operations, and return a new set of data based | |
* on the terminal operation. The most common terminal operation is the | |
* collection operation. | |
* </li> | |
* </ol> | |
* | |
* <p> | |
* The Streaming API also introduces a variety of helper classes that provide | |
* an easier construct for lambda expressions in certain cases. | |
* </p> | |
* | |
* <ul> | |
* <li> | |
* {@link Collectors} provides a variety of static methods to creating | |
* {@link Collector} classes that may be passed to the | |
* {@link Stream#collect(java.util.stream.Collector)} operation. Examples | |
* include creating a new collection of data, grouping data, summarizing | |
* data, etc. | |
* </li> | |
* <li> | |
* {@link Comparator} provides several static methods for creating | |
* comparator instances by mapping data to other naturally comparable | |
* types (ie: user to first name as a string), providing a reverse order, | |
* and even stringing multiple comparators together (ie: first this, | |
* then that). | |
* </li> | |
* </ul> | |
* | |
* Note that the following examples use assignments to functional interfaces | |
* rather than inlining the lambda expression due to issues with the type system | |
* and Java 8 with Eclipse. It is hopeful that these issues will be addressed | |
* once the beta releases go final. The power in Lambdas and the Streaming API | |
* is the automatic type inference that goes with it. | |
*/ | |
public class StreamExamples { | |
private static final Random random = new Random(); | |
/** | |
* The following are various examples of using the Streaming API. This | |
* includes stateless operations, stateful operations, termination | |
* operations, and various complex examples. | |
*/ | |
public static void main(String[] args) { | |
final int numPlayers = 100; | |
List<Player> players = createPlayers(numPlayers); | |
///////// STATELESS EXAMPLES | |
filterExample(players); | |
mapExample1(players); | |
mapExample2(players); | |
flatMapExample(players); | |
peekExample1(players); | |
peekExample2(players); | |
///////// STATEFUL EXAMPLES | |
sortedExample(players); | |
distinctExample(players); | |
substreamExample(players); | |
///////// TERMINATION EXAMPLES | |
collectExamples(players); | |
matchExamples(players); | |
maxExample(players); | |
reductionExample(players); | |
///////// COMPLEX EXAMPLES | |
complexExample1(players); | |
complexExample2(players); | |
} | |
/** | |
* The following examples provides various ways to create a stream from a given source. | |
* This includes collection streams, file system streams, I/O streams, etc. A given | |
* stream can then use any of the intermediate or termination operations regardless | |
* of data structures. | |
*/ | |
@SuppressWarnings({ "unused" }) | |
public static void inputSources() throws Exception { | |
/********************************************************************** | |
* The following provide collection-based streams. Collection-based streams | |
* may either return a single-core stream or a parallel stream that utilizes | |
* all available cores. The stream operates and processes each element within | |
* the collection. | |
**********************************************************************/ | |
Collection<Player> collection = new ArrayList<Player>(); | |
Stream<Player> cstream1 = collection.stream(); | |
Stream<Player> cstream2 = collection.parallelStream(); | |
/********************************************************************** | |
* The following provide file system streams that operate on files or data | |
* within the files. Streams operate on the new JDK 7-based Path object | |
* rather than the traditional File objects. | |
**********************************************************************/ | |
// reference a given path in the file system | |
Path start = FileSystems.getDefault().getPath("/home/user"); | |
// create a stream that operates on all files and directories recursively | |
Stream<Path> fstream1 = Files.walk(start); | |
// create a stream that operates on all matching files and directories, | |
// recursively that match a given expression | |
Stream<Path> fstream2 = | |
Files.find(start, 10, (path, attrs) -> (path.endsWith(".txt"))); | |
// create a stream that operates on all files within the given directory | |
// excluding any sub-directories (ie: not recursive) | |
Stream<Path> fstream3 = Files.list(start); | |
// create a stream that operates on every line of a given file | |
Stream<String> fstream4 = | |
Files.lines(start, Charset.defaultCharset()); | |
/********************************************************************** | |
* The following provides I/O streams that operate on the lines of data within | |
* a given I/O stream. The stream reads and processes each line within a file. | |
**********************************************************************/ | |
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); | |
Stream<String> rstream = reader.lines(); | |
/********************************************************************** | |
* The following provides streams that operate on an array of data | |
* rather than a collection of data. Note that there are primitive | |
* based streams for primitive arrays. Although you can use autoboxing, | |
* it is at times more efficient to deal with primitives directly. | |
**********************************************************************/ | |
DoubleStream dstream = Arrays.stream(new double[] { 1.2, 1.3, 2.4, 3.14 }); | |
IntStream istream = Arrays.stream(new int[] { 1, 2, 3, 5, 8, 13 }); | |
Stream<String> sstream = Arrays.stream(new String[] { "a", "b", "c" }); | |
/********************************************************************** | |
* The following provides streams that operate on a set of strings | |
* representing each match of a given regular expression pattern. | |
**********************************************************************/ | |
Stream<String> pstream = | |
Pattern.compile("[a-z]").splitAsStream("test-for-me"); | |
/********************************************************************** | |
* The following provides streams that operate on each entry within a | |
* ZIP or JAR file including recursive traversal. Note the following | |
* examples also use the new try-with-resources feature in JDK 7. | |
**********************************************************************/ | |
// JAR and ZIP-based streams for processing entries | |
try (JarFile jarFile = new JarFile("myjarfile.jar")) { | |
Stream<JarEntry> jstream = jarFile.stream(); | |
}; | |
try (ZipFile zipFile = new ZipFile("myzipfile.zip")) { | |
Stream<? extends ZipEntry> zstream = zipFile.stream(); | |
}; | |
/********************************************************************** | |
* The following provides a stream of int primitives that represent | |
* each character within a given string. | |
**********************************************************************/ | |
IntStream chstream = "string".chars(); | |
} | |
/** | |
* The following example provides the pre-JDK 8 solution of iterating. | |
* Notice however that this solution is not parallel and concurrent and | |
* does not take advantage of multi-core systems. | |
*/ | |
public static List<Player> oldFilterExample(List<Player> players) { | |
List<Player> found = new ArrayList<Player>(players.size()); | |
for (Player player : players) { | |
if (player.getAvg() > .300) { | |
found.add(player); | |
} | |
} | |
return found; | |
} | |
/** | |
* The following example demonstrates the filter intermediate operation | |
* that filters a data set based on a boolean {@link Predicate predicate}. | |
* | |
* This example filters the given players to only those players who are | |
* batting over .300 as their batting average. This uses the | |
* <code>Stream.filter</code> method with a lambda expression. | |
* | |
* It then collects the results into a new list by leveraging the | |
* <code>Collectors.toCollection</code> method that takes a supplier | |
* that is responsible for creating the actual collection. The | |
* supplier is created based on the method reference to the constructor | |
* of the collection implementation (ie: <code>ArrayList::new</code>). | |
* | |
* Note that due to Eclipse, the <code>Supplier</code> must be explicitly | |
* defined for the type checker to work. In general, you would purely | |
* specify: <code>Collectors.toCollection(ArrayList::new)</code>. | |
*/ | |
public static void filterExample(List<Player> players) { | |
Supplier<List<Player>> supplier = ArrayList::new; | |
List<Player> playersOver300 = players.parallelStream(). | |
filter(player -> player.getAvg() > 0.300). | |
collect(Collectors.toCollection(supplier)); | |
System.out.println("PLAYERS OVER .300: " + playersOver300); | |
} | |
/** | |
* The following example gets the list of all roles from all players building | |
* into a set to avoid duplicates. This maps each player to their respective | |
* role and collects as a set. This uses the <code>Stream.map</code> operation | |
* with a given {@link Function function} that maps input type (ie: Player) | |
* to a new output type (ie: Role). | |
* | |
* Note that due to Eclipse, the <code>Function</code> and its types must | |
* be explicitly defined. Typically, you would purely invoke: | |
* <code>stream.map(player -> player.getUser().getRole())</code>. | |
*/ | |
public static void mapExample1(List<Player> players) { | |
Supplier<Set<Role>> supplier = HashSet::new; | |
Function<Player, Role> mapper = (player -> player.getUser().getRole()); | |
Set<Role> roles = players.parallelStream(). | |
map(mapper). | |
collect(Collectors.toCollection(supplier)); | |
System.out.println("ROLES: " + roles); | |
} | |
/** | |
* The following example is similar to the example above, but rather than a | |
* explicit lambda expression, it uses a method reference to map each player | |
* to their respective user. | |
*/ | |
public static void mapExample2(List<Player> players) { | |
Supplier<List<User>> supplier = ArrayList::new; | |
Function<Player, User> mapper = Player::getUser; | |
List<User> users = players.parallelStream(). | |
map(mapper). | |
collect(Collectors.toCollection(supplier)); | |
System.out.println("USERS: " + users); | |
} | |
/** | |
* The following example uses the <code>Stream.flatMap</code> rather than | |
* the <code>Stream.map</code> example. This example gets the list of all | |
* roles of all users as a single flat collection. Purely using | |
* <code>map(User::getRoles)</code> would return a <code>List<List<Role>></code> | |
* or a list of lists of roles rather than just a flat list of roles. By | |
* using flatMap, we reduce the list of lists into a single list. Note | |
* that as we use a list here, rather than a set, we include all duplicates | |
* to demonstrate the point. The <code>flatMap</code> operation takes a | |
* functional interface that maps the input type to a stream of the output | |
* type. The stream is then used to actually fetch any element from the | |
* data to reduce into the single collection. | |
*/ | |
public static void flatMapExample(List<Player> players) { | |
Supplier<List<Role>> supplier = ArrayList::new; | |
Function<Player, Stream<Role>> mapper = | |
(player -> player.getUser().getRoles().stream()); | |
List<Role> roles = players.parallelStream() | |
.flatMap(mapper) | |
.collect(Collectors.toCollection(supplier)); | |
System.out.println("ALL ROLES: " + roles); | |
} | |
/** | |
* The following example applies a peek operation to each user before applying | |
* an actual operation. Note that in this form, this actually does nothing as | |
* intermediate operations have no impact until a termination operation occurs. | |
* The next example demonstates the need for a terminal operation such as | |
* <code>forEach</code> to actually process the list. The second example | |
* also showcases how the parallel stream operates as the peek print and the | |
* for each print intertwine in their console output. Change the | |
* <code>parallelStream</code> to just <code>stream</code> and each peek and | |
* forEach operate one at a time per element in the collection. | |
*/ | |
public static void peekExample1(List<Player> players) { | |
players.parallelStream().peek(System.out::println); | |
} | |
public static void peekExample2(List<Player> players) { | |
players.parallelStream().peek(System.out::println) | |
.forEach(p -> System.out.println("FOR EACH: " + p)); | |
} | |
/** | |
* The following example demonstrates the stateful <code>sort</code> operation | |
* to sort the players by last name followed by first name in reverse order printing | |
* the resulting ordered list. The sort operation takes a standard | |
* <code>Comparable</code> instance that is built using the helper methods in | |
* {@link Comparable}. In this example, we first use a method reference to compare the | |
* last name, then follow it with a first name comparison in cases where the last names | |
* are the same. Finally, we reverse the order. The <code>comparing</code> method | |
* takes a function that maps a given data type to a natually comparable data type. | |
* | |
* Note the use of the <code>peek</code> operation and the fact that each element is | |
* printed, then the sort operation takes place, then the forEach print is done. This | |
* showcases the stateful effect of the sort operation having to fetch all data first | |
* before it processes and invokes the terminal operation. Normally, each element | |
* is processed, in parallel, one-by-one through each operation including the terminal | |
* operation. The sort operation causes the stream to essentially block and fetch all | |
* items from the initial stream before it can proceed to letting the terminal operations | |
* complete. | |
*/ | |
public static void sortedExample(List<Player> players) { | |
Function<Player, String> lmapper = Player::getLastName; | |
Function<Player, String> fmapper = Player::getFirstName; | |
Comparator<Player> comparator = | |
Comparator.comparing(lmapper) | |
.thenComparing(fmapper) | |
.reversed(); | |
Supplier<List<Player>> supplier = ArrayList<Player>::new; | |
players.parallelStream() | |
.peek(System.out::println) | |
.sorted(comparator) | |
.collect(Collectors.toCollection(supplier)) | |
.forEach(p -> System.out.println("SORTED: " + p)); | |
} | |
/** | |
* The following examples applies another type of stateful operation, the | |
* <code>distinct</code> operation. This operation fetches all data and only processes | |
* items that are distinct tossing out duplicates per the standard | |
* {@link Object#equals(Object)} method. This example essentially grabs all | |
* the unique roles of all users. It first uses <code>flatMap</code> to grab | |
* the list of all roles of all users in a single flattened list. It then grabs | |
* the distinct roles. | |
* | |
* Note that there are several overlapping capabilities between intermediate | |
* operations (ie: grouping, distinct, mapping, etc) and terminal collection | |
* operations that provide similar behaviors. The actual case of when to use | |
* one or the other depends on whether you need to apply additional rules or | |
* intermediate operations or whether the result of the operation will be | |
* the result of the data. | |
*/ | |
public static void distinctExample(List<Player> players) { | |
Supplier<List<Role>> supplier = ArrayList::new; | |
Function<Player, Stream<Role>> mapper = | |
(player -> player.getUser().getRoles().stream()); | |
List<Role> roles = players.parallelStream() | |
.flatMap(mapper) | |
.distinct() | |
.collect(Collectors.toCollection(supplier)); | |
System.out.println("DISTINCT ROLES: " + roles); | |
} | |
/** | |
* The following example provides the ability to count all elements in the stream | |
* as well as creating a substream of the top 50 results. Substreams are even more | |
* powerful when combined with other operations such as a sort. In other words, | |
* one could apply a sort operation followed by a substream operation to grab the | |
* top X number of players based on a particular comparable field. | |
*/ | |
public static void substreamExample(List<Player> players) { | |
System.out.println("COUNT: " + players.parallelStream().count()); | |
System.out.println("SUBCOUNT: " + players.parallelStream().substream(50).count()); | |
} | |
/** | |
* The following examples provide the various examples of using the terminal | |
* operation <code>Stream.collect</code> to collect data in a variety of formats. | |
*/ | |
public static void collectExamples(List<Player> players) { | |
/********************************************************************** | |
* The following examples returns a single double value that provides | |
* the average of a set of doubles. This particular example grabs | |
* the batting average of each user and then generates the average of | |
* those batting averages. Note that if we applied a filter operation | |
* the average would only be calculated on the filtered results. | |
*********************************************************************/ | |
ToDoubleFunction<Player> mapper1 = (player -> player.getAvg()); | |
System.out.println("AVERAGE AVG: " + | |
players.parallelStream() | |
.collect(Collectors.averagingDouble(mapper1))); | |
/********************************************************************** | |
* The following example returns a single long value that provides the | |
* total summation of all other values. This particular example grabs | |
* the number of hits of each player and returns the total number of | |
* hits of all players by summing them all together. | |
*********************************************************************/ | |
ToLongFunction<Player> mapper2 = Player::getHits; | |
System.out.println("TOTAL HITS: " + | |
players.parallelStream() | |
.collect(Collectors.summingLong(mapper2))); | |
/********************************************************************** | |
* The following example produces a summary of statistics based on a | |
* given set of data. This is similar to the above examples, but | |
* rather than providing just a single value (sum or avg), this | |
* creates a set of statistics (count, min, max, avg, etc) | |
*********************************************************************/ | |
ToLongFunction<Player> mapper3 = Player::getWalks; | |
LongSummaryStatistics stats = players.parallelStream() | |
.collect(Collectors.summarizingLong(mapper3)); | |
System.out.println("STATS: " + stats.toString()); | |
/********************************************************************** | |
* The following example groups a set of data based on a particular | |
* expression or field. In this example, we group the list of | |
* players into a map of role to list of players with the role. This | |
* works by specifying a function of the input type to the output type | |
* which is used as the key to the resulting map of lists. Also note | |
* the use of the method reference to the printEntry method to print | |
* each result generically. | |
*********************************************************************/ | |
System.out.println("PLAYERS BY ROLES"); | |
Function<Player, Role> mapper4 = (player -> player.getUser().getRole()); | |
players.parallelStream() | |
.collect(Collectors.groupingBy(mapper4)) | |
.entrySet().forEach(StreamExamples::printEntry); | |
/********************************************************************** | |
* The following example joins together a set of data into a string | |
* with a given delimiter. This particular creates a list of first | |
* names separated by comma. It first uses the map operation to map | |
* the player to their respective first name. It then uses natural | |
* sorting on the first names to sort the results. Finally, it joins | |
* the sorted first name results by comma. | |
*********************************************************************/ | |
Comparator<String> comparator5 = Comparator.naturalOrder(); | |
Function<Player, String> mapper5 = Player::getFirstName; | |
String names = players.parallelStream() | |
.map(mapper5) | |
.sorted(comparator5) | |
.collect(Collectors.joining(", ")); | |
System.out.println("FIRST NAMES: " + names); | |
/********************************************************************** | |
* The following example grabs the maximum value of a particular set | |
* of data based on a given comparable. In this example, we first | |
* map the players to their number of hits. We then collect the max | |
* number of hits by using natural sorting. We could have also used | |
* a <code>Comparable.comparingBy(Player::getHits)</code> to grab the | |
* player with the most hits rather than just the max hits as a long | |
* value. Note that the collector returns a <code>Optional</code> | |
* class object to represent the difference between valid value, | |
* null value, and no value available (ie: empty stream). | |
*********************************************************************/ | |
Function<Player, Long> mapper6 = Player::getHits; | |
Comparator<Long> comparator6 = Comparator.naturalOrder(); | |
System.out.println("MAX HITS: " + players.parallelStream() | |
.map(mapper6) | |
.collect(Collectors.maxBy(comparator6)) | |
.get()); | |
/********************************************************************** | |
* The following example provides a map grouping a set of data to a | |
* reduction of another set of data. In other words, this example | |
* maps each player to their respective role and then grabs the player | |
* with the highest average in that particular role. This is achieved | |
* by first grouping the data (ie: role to list of users with the role). | |
* As part of the grouping, we also pass a reducing operation to reduce | |
* the list of players at a particular role to a single value. In this | |
* case we reduce the list by using the maximum value based on a given | |
* comparable. | |
*********************************************************************/ | |
System.out.println("TOP AVG BY ROLE"); | |
Function<Player, Role> mapper7 = (player -> player.getUser().getRole()); | |
Comparator<Player> byAvg = Comparator.comparingDouble(Player::getAvg); | |
Map<Role, Optional<Player>> bestAvgByRole = | |
players.parallelStream() | |
.collect(Collectors.groupingBy( | |
mapper7, | |
Collectors.reducing(BinaryOperator.maxBy(byAvg)))); | |
bestAvgByRole.entrySet().forEach(StreamExamples::printEntry); | |
/********************************************************************** | |
* The following examples partition the data into a boolean map where | |
* all data matching the predicate are in the TRUE collection and all | |
* other users in the FALSE collection. This example returns a map | |
* of active users vs inactive users. | |
*********************************************************************/ | |
System.out.println("ACTIVE/INACTIVE PLAYERS"); | |
Predicate<Player> pred8 = (player -> player.getUser().isActive()); | |
players.parallelStream() | |
.collect(Collectors.partitioningBy(pred8)) | |
.entrySet().forEach(StreamExamples::printEntry); | |
/********************************************************************** | |
* The following examples collects data into a map by using two | |
* function mappers to produce the associated key and associated value. | |
* If a given value results in a duplicate key, it will be overwritten. | |
* This example also uses a supplier to generate the underlying map | |
* implementation, a tree map in this example to sort the keys as the | |
* last name. | |
*********************************************************************/ | |
Supplier<Map<String, Double>> supplier9 = TreeMap::new; | |
Function<Player, String> keyMapper9 = Player::getLastName; | |
Function<Player, Double> valMapper9 = Player::getAvg; | |
System.out.println("LAST NAME TO AVG: " + players.parallelStream() | |
.collect(Collectors.toMap(keyMapper9, valMapper9, null, supplier9))); | |
} | |
protected static <K, V> void printEntry(Map.Entry<K, V> entry) { | |
System.out.println(" " + entry.getKey() + ": " + entry.getValue()); | |
} | |
/** | |
* The following example is a termination operation that provides boolean | |
* values of whether data matches a given lambda expression. The | |
* <code>allMatch</code> example returns <code>true</code> if and only if | |
* all data in the stream returns <code>true</code> from the lambda. | |
* The <code>anyMatch</code> returns true if any data in the stream | |
* returns <code>true</code> from the lambda. The <code>noneMatch</code> | |
* example only returns <code>true</code> if all values in the stream | |
* return <code>false</code> in the lambda. | |
*/ | |
protected static void matchExamples(List<Player> players) { | |
System.out.println("ALL ACTIVE: " + players.parallelStream() | |
.allMatch(player -> player.getUser().isActive())); | |
System.out.println("ANY ACTIVE: " + players.parallelStream() | |
.anyMatch(player -> player.getUser().isActive())); | |
System.out.println("NONE ACTIVE: " + players.parallelStream() | |
.noneMatch(player -> player.getUser().isActive())); | |
} | |
/** | |
* The following example uses the <code>Stream.max</code> terminal | |
* operation to return the item from the data set that represents the | |
* max value per the given comparator instance. This particular | |
* example compares each player's average to grab the player with the | |
* highest average. | |
*/ | |
protected static void maxExample(List<Player> players) { | |
Comparator<Player> comparator = Comparator.comparingDouble(Player::getAvg); | |
System.out.println("MAX AVG: " + players.parallelStream() | |
.max(comparator)); | |
} | |
/** | |
* The following example uses the <code>Stream.reduce</code> terminal | |
* operation to reduce a set of data to a single value. This particular | |
* example uses a {@link BinaryOperator} to compare two values and return | |
* the user that should not be excluded. The stream is continually | |
* processed comparing values against each other until a sole value | |
* remains. Note that the return type is actually {@link Optional} to | |
* denote a valid value, null value, or value not available. | |
*/ | |
protected static void reductionExample(List<Player> players) { | |
BinaryOperator<Player> op = | |
(p1, p2) -> ( | |
p2.getUser().getRole().isAdmin() && | |
p2.getAvg() > p1.getAvg() ? p2 : p1 | |
); | |
System.out.println("TOP ADMIN AVG: " + players.parallelStream() | |
.reduce(op).get()); | |
} | |
/** | |
* The following example gets the user with the highest average per role, | |
* but only for active users beginning with an 'A' in their first name. | |
* This particular example builds the stream up in each step showing how | |
* the steps are constructed together. At the end, commented out, is a | |
* demonstration of putting all the pieces together as a single expression | |
* as well as an example of the iteration example done in earlier JDKs. | |
* Note the easier readability of the full example compared to all the | |
* logic in the legacy example. Also note the reduction in code. Finally, | |
* it should be noted that the legacy example is single threaded, whereas | |
* the full streams-based example is stateless and highly concurrent. | |
*/ | |
protected static void complexExample1(List<Player> players) { | |
// step 1: create the initial stream from the collection | |
Stream<Player> stream1 = players.parallelStream(); | |
// step 2: map the data from a player to the associated user | |
// to operate on a stream of users | |
Stream<User> stream2 = stream1.map(Player::getUser); | |
// step 3: first filter out the users that are not active and | |
// then filter out the users not starting with A | |
Stream<User> stream3 = stream2 | |
.filter(User::isActive) | |
.filter(user -> user.getFirstName().charAt(0) == 'A'); | |
// step 4: collect into groups of Map<Role, List<Player>> by mapping | |
// each user to their role and grouping by that role. Note | |
// that this example is commented out as we build upon this | |
// grouping example with an alternate form as shown in step 5. | |
Function<User, Role> mapper = User::getRole; | |
//Map<Role, List<User>> users1 = | |
// stream3.collect(Collectors.groupingBy(mapper)); | |
// step 5: utilize the premise above to group users per role but | |
// additionally apply a reduction to reduce each list to | |
// a single value by comparing each player's batting avg | |
// and reducing to the player with the max average. | |
ToDoubleFunction<User> mapper2 = (user -> user.getPlayer().getAvg()); | |
Comparator<User> comparator = Comparator.comparingDouble(mapper2); | |
Map<Role, Optional<User>> users2 = | |
stream3.collect(Collectors.groupingBy(mapper, Collectors.maxBy(comparator))); | |
// step 6: traverse and print each map entry (role to user with max avg) | |
System.out.println("ROLES"); | |
users2.entrySet() | |
.forEach(entry -> { | |
System.out.println(" " + entry.getKey() + ": " + entry.getValue().get().getPlayer()); | |
}); | |
/* FULL EXAMPLE | |
players.parallelStream() | |
.map(Player::getUser) | |
.filter(User::isActive) | |
.filter(user -> user.getFirstName().charAt(0) == 'A') | |
.collect(Collectors.groupingBy( | |
User::getRole, | |
Collectors.maxBy( | |
Comparator.comparingDouble(user -> user.getPlayer().getAvg()) | |
) | |
)) | |
.entrySet().forEach(entry -> { | |
System.out.println(" " + entry.getKey() + ": " + entry.getValue().get().getPlayer()); | |
}); | |
*/ | |
/* LEGACY EXAMPLE | |
Map<Role, List<User>> roleUsers = new HashMap<Role, List<User>>(); | |
for (Player player : players) { | |
User user = player.getUser(); | |
if (user.isActive() && user.getFirstName().charAt(0) == 'A') { | |
Role role = user.getRole(); | |
List<User> existing = roleUsers.get(role); | |
if (existing == null) { | |
existing = new ArrayList<User>(); | |
roleUsers.put(role, existing); | |
} | |
existing.add(user); | |
} | |
} | |
Map<Role, Double> roleAvgs = new HashMap<Role, Double>(); | |
for (Map.Entry<Role, List<User>> entry : roleUsers.entrySet()) { | |
Double avg = null; | |
List<User> users = entry.getValue(); | |
if (!users.isEmpty()) { | |
Iterator<User> it = users.iterator(); | |
avg = it.next().getPlayer().getAvg(); | |
while (it.hasNext()) { | |
Double tmp = it.next().getPlayer().getAvg(); | |
if (tmp > avg) { avg = tmp; } | |
} | |
} | |
roleAvgs.put(entry.getKey(), avg); | |
} | |
for (Map.Entry<Role, Double> entry : roleAvgs.entrySet()) { | |
System.out.println(" " + entry.getKey() + ": " + entry.getValue().getPlayer()); | |
} | |
*/ | |
} | |
/** | |
* The following example is very simlar to the above but it goes a step further and groups the | |
* users by first letter of their first name and then groups each of those groups by role and | |
* max average. In other words it produces a <code>Map[String, Map[Role, User]]</code>. The | |
* key difference is that we apply a <code>groupingBy</code> within the initial grouping. | |
*/ | |
protected static void complexExample2(List<Player> players) { | |
Function<Player, User> playerToUser = Player::getUser; | |
Predicate<User> isActive = User::isActive; | |
Function<User, String> userToFirstLetter = (u -> String.valueOf(u.getFirstName().charAt(0))); | |
Function<User, Role> userToRole = User::getRole; | |
ToDoubleFunction<User> userToAvg = (u -> u.getPlayer().getAvg()); | |
System.out.println("BY LETTER"); | |
players.parallelStream() | |
.map(playerToUser) | |
.filter(isActive) | |
.collect( | |
Collectors.groupingBy( | |
userToFirstLetter, | |
Collectors.groupingBy( | |
userToRole, | |
Collectors.maxBy( | |
Comparator.comparingDouble(userToAvg) | |
) | |
) | |
) | |
) | |
.entrySet().forEach(entry -> { | |
String firstLetter = entry.getKey(); | |
Map<Role, Optional<User>> usersPerRoles = entry.getValue(); | |
System.out.println(" " + firstLetter + ": BY ROLE"); | |
usersPerRoles.entrySet().forEach(entry2 -> { | |
Role role = entry2.getKey(); | |
User user = entry2.getValue().get(); | |
System.out.println(" " + role + ": " + user.getPlayer()); | |
}); | |
}); | |
} | |
protected static List<Player> createPlayers(int numPlayers) { | |
List<Player> players = new ArrayList<Player>(numPlayers); | |
for (int i = 0; i < numPlayers; i++) { | |
char first = (char) ('A' + random.nextInt(26)); | |
char last = (char) ('A' + random.nextInt(26)); | |
long hits = random.nextInt(500), | |
strikeouts = random.nextInt(200), | |
walks = random.nextInt(200); | |
players.add( | |
new Player( | |
new User( | |
getValue(first, 8), getValue(last, 8) | |
) | |
.active(random.nextBoolean()) | |
.role(new Role(Type.values()[random.nextInt(3)])) | |
.role(new Role(Type.values()[random.nextInt(3)])) | |
.role(new Role(Type.values()[random.nextInt(3)])) | |
) | |
.hits(hits) | |
.strikeouts(strikeouts) | |
.walks(walks) | |
.atBats(hits + strikeouts + walks) | |
); | |
} | |
return players; | |
} | |
protected static String getValue(char startCh, int length) { | |
StringBuilder value = new StringBuilder(length); | |
value.append(startCh); | |
for (int i = 1; i < length; i++) { | |
value.append((char) ('a' + random.nextInt(26))); | |
} | |
return value.toString(); | |
} | |
public static class Player { | |
private User user; | |
private long hits; | |
private long atBats; | |
private long walks; | |
private long strikeouts; | |
public Player(User user) { | |
this.user = user; | |
user.player(this); | |
} | |
public User getUser() { | |
return this.user; | |
} | |
public String getFirstName() { | |
return this.getUser().getFirstName(); | |
} | |
public String getLastName() { | |
return this.getUser().getLastName(); | |
} | |
public long getHits() { | |
return this.hits; | |
} | |
public Player hits(long hits) { | |
this.hits = hits; | |
return this; | |
} | |
public long getAtBats() { | |
return this.atBats; | |
} | |
public Player atBats(long atBats) { | |
this.atBats = atBats; | |
return this; | |
} | |
public long getWalks() { | |
return this.walks; | |
} | |
public Player walks(long walks) { | |
this.walks = walks; | |
return this; | |
} | |
public long getStrikeouts() { | |
return this.strikeouts; | |
} | |
public Player strikeouts(long strikeouts) { | |
this.strikeouts = strikeouts; | |
return this; | |
} | |
public double getAvg() { | |
return (double) this.hits / (double) this.atBats; | |
} | |
public String toString() { | |
return "Player(" + this.user + ", " + this.getAvg() + ")"; | |
} | |
} | |
public static class User { | |
private String firstName; | |
private String lastName; | |
private boolean active; | |
private Player player; | |
private List<Role> roles = new ArrayList<Role>(); | |
public User(String firstName, String lastName) { | |
this.firstName = firstName; | |
this.lastName = lastName; | |
} | |
public Player getPlayer() { | |
return this.player; | |
} | |
public User player(Player player) { | |
this.player = player; | |
return this; | |
} | |
public String getFirstName() { | |
return this.firstName; | |
} | |
public String getLastName() { | |
return this.lastName; | |
} | |
public Role getRole() { | |
return this.roles.get(0); | |
} | |
public List<Role> getRoles() { | |
return this.roles; | |
} | |
public User role(Role role) { | |
this.roles.add(role); | |
return this; | |
} | |
public boolean isActive() { | |
return this.active; | |
} | |
public User active(boolean active) { | |
this.active = active; | |
return this; | |
} | |
public String toString() { | |
return "User(" + this.firstName + ", " + this.lastName + ")"; | |
} | |
} | |
public static enum Type { GUEST, USER, ADMIN } | |
public static class Role { | |
private String name; | |
private Type type; | |
public Role(Type type) { | |
this.type = type; | |
this.name = type.name().toLowerCase(); | |
} | |
public String getName() { | |
return this.name; | |
} | |
public Type getType() { | |
return this.type; | |
} | |
public int hashCode() { | |
return this.type.hashCode(); | |
} | |
public boolean equals(Object other) { | |
if (other == null) { return false; } | |
else if (other == this) { return true; } | |
else if (!(other instanceof Role)) { return false; } | |
else { return this.type.equals(((Role) other).type); } | |
} | |
public String toString() { | |
return "Role(" + this.type.name() + ")"; | |
} | |
public boolean isAdmin() { | |
return this.type == Type.ADMIN; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment