Skip to content

Instantly share code, notes, and snippets.

@nicholashagen
Created October 23, 2013 20:31
Show Gist options
  • Save nicholashagen/7126156 to your computer and use it in GitHub Desktop.
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
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