1. Java Stream-API

The Stream API enables step-by-step processing of data. After a source emits data, different steps follow that filter and transform data and reduce it to a result.

Streams — although the term is ambiguous and could be confused with input/output streams — are an important innovation of Java 8 and take advantage of other innovations in the Java SE library such as predefined functional interfaces or Optional. Together with lambda expressions and method references, this results in compact code and a whole new way of declaratively configuring processing steps.

The first task in this assignment block makes use of the heroes we already met in the chapter about the class library. All the major terminal and intermediate operations are used for this collection of heroes. Different tasks follow, the solution of which show the elegance of the Stream API.

Prerequisites

  • be able to build a stream

  • be able to use terminal and intermediate operations

  • be able to handle primitive streams

  • be able to use lambda expressions practically

Data types used in this chapter:

1.1. Regular streams with their terminal and intermediate operations

For each stream, there are two mandatory steps and any number of optional steps in between:

  1. construction of the stream from a data source

  2. optional processing steps, called intermediate operations.

  3. final operation, called terminal operation.

1.1.1. Hero Epic: Meet Stream API ⭐

In chapter "The Java Class Library" the class Heroes with heroes was introduced. This assignment is based on it.

Stream construction:

  • For the following task items, always build a new Stream with the heroes, and then apply the terminal and intermediate operations according to the following pattern:

    Heroes.ALL.stream().intermediate1(...).intermediate2(...).terminal()

Terminal operations:

  1. Output all information about heroes in CSV format on the screen.

  2. Ask if all heroes were introduced after 1900.

  3. Ask if any female hero was introduced after 1950 (inclusive).

  4. Which hero appears first?

  5. Which hero is closest in year of publication to 1960? Only one terminal operation should be used on the stream.

  6. A StringBuilder is to be created that contains all years comma separated. The result is to be created with a single terminal Stream method, no intermediate operation in between. The order of the years in the string does not matter.

  7. Split the male and female heroes into two groups. The result shall be of type Map<Sex, List<Hero>>.

  8. Form two partitions with heroes introduced before and after 1970. The result shall be of type Map<Boolean, List<Hero>>.

Intermediate (non-terminal) operations:

  1. How many female heroes are there in total?

  2. Sort all heroes by release date, then output all heroes.

  3. Go through the following steps:

    a) Create a comma separated string with the names of all female heroes.
    b) In the hero there is no setter, because the hero is immutable. But with the constructor we can build new heroes. Convert the heroes to a list of anonymous heroes, where the plain name in parentheses is removed along with the parentheses themselves.
    c) Create an int[] with all years where heroes were introduced — without duplicate entries.

  4. Go through UNIVERSES and not ALL to output the names of all heroes.

1.1.2. Quiz: Double output ⭐

If the following three lines are in the main(…​) method and the program starts, what will the output be?

Stream<Integer> numbers = Stream.of( 1, 2, 3, 4, 5 );
numbers.peek( System.out::println );
numbers.forEach( System.out::println );

1.1.3. Get the beloved captain from a list ⭐

At the end of the year, the ship’s crew votes on which candidate for captain should pave their way to rich plunder in the future. The winner is the person with the most nominations.

Task:

  • Given is an array of strings with names. Which name was mentioned how often? The names are not case sensitive.

  • Many people just call Captain CiaoCiao simply CiaoCiao, this should be equivalent to Captain CiaoCiao.

Example:

{"Anne", "Captain CiaoCiao", "Balico", "Charles", "Anne", "CiaoCiao", "CiaoCiao", "Drake", "Anne", "Balico", "CiaoCiao" }{charles=1, anne=3, drake=1, ciaociao=4, balico=2}

1.1.4. Frame pictures ⭐

Captain CiaoCiao has been chosen as the best captain, so the joy is great. He would like to have his picture framed.

Given is a multiline string, such as

     ______
_.-':::::::`.
\::::::::::::`.-._
 \:::''   `::::`-.`.
  \         `:::::`.\
   \          `-::::`:
    \______       `:::`.
    .|_.-'__`._     `:::\
   ,'`|:::|  )/`.     \:::
  /. -.`--'  : /.\     ::|
  `-,-'  _,'/| \|\\    |:|
   ,'`::.    |/>`;'\   |:|
   (_\ \:.:.:`((_));`. ;:|
   \.:\ ::_:_:_`-','  `-:|
    `:\\|     SSt:
       )`__...---'

This is to be placed in a picture frame:

+------------------------------+
|                              |
|       ______                 |
|  _.-':::::::`.               |
|  \::::::::::::`.-._          |
|   \:::''   `::::`-.`.        |
|    \         `:::::`.\       |
|     \          `-::::`:      |
|      \______       `:::`.    |
|      .|_.-'__`._     `:::\   |
|     ,'`|:::|  )/`.     \:::  |
|    /. -.`--'  : /.\     ::|  |
|    `-,-'  _,'/| \|\\    |:|  |
|     ,'`::.    |/>`;'\   |:|  |
|     (_\ \:.:.:`((_));`. ;:|  |
|     \.:\ ::_:_:_`-','  `-:|  |
|      `:\\|     SSt:          |
|         )`__...---'          |
|                              |
+------------------------------+

Task:

  • Write a frame(String) method that frames a multi-line string. Use the String methods lines() and repeat(…​).

    • The horizontal lines consist of -.

    • The vertical lines consist of |.

    • In the corners there are plus signs +.

    • The spacing to the right and left of the frame is 2 spaces.

    • The inner space at the top and bottom is a blank line.

    • Line breaks are \n, but they should be relatively easy to change in the program.

*Java 8 backport

lines() and repeat(…​) were introduced in Java 11; if you want to realize the task with Java 8, you can solve the splitting into lines for example with split("\n") and the repeating of characters with a loop.

1.1.5. Look and say ⭐⭐

Captain CiaoCiao is daydreaming and writing on a piece of paper:

1

He sees the 1 and says to himself, "Oh, 1 times the 1!" He writes that down:

1 1

Now he sees two ones and can read it out like this:

2 1

"Arrr! That’s a two and a one!" He writes it down:

1 2 1 1

He reads the numbers again and says:

1 1 1 2 2 1

Now the one even appears three times:

3 1 2 2 1 1

Captain CiaoCiao finds that the numbers get big quickly, though. He is curious to see if after a few passes only 1, 2, and 3 occur as digits.

Task:

  • Create an infinite stream of look-and-say numbers using a Stream.iterate(…​).

  • Limit the stream to 20 elements

  • Output the numbers to the console; they can also be output compactly as a string like 111221.

The task can be solved with a clever regular expression with a back reference. However, this solution variant is sophisticated, and those who want to take this route can find more details at https://regular-expressions.mobi/backref.html.

What is asked here is the look-and-say sequence, which https://oeis.org/A005150 explains in more detail with many references.

1.1.6. Remove duplicate islands of rare earth metals ⭐⭐⭐

The business with rare earth metals is particularly attractive to Bonny Brain. Their crew compiles a list of islands are home to certain earth metals. The result goes into a text file that looks like this:

Balancar
Erbium
Benecia
Yttrium
Luria
Thulium
Kelva
Neodymium
Mudd
Europium
Tamaal
Erbium
Varala
Gadolinium
Luria
Thulium

One line contains the island, the next line contains the rare earth metals. However, it can happen that different crew members enter the same pairs in the text file. In the example it is the pair Luria and Thulium.

Task:

  • Write a program that deletes all duplicate line pairs from the text.

  • The program must be flexible enough that the input can come from a String, File, InputStream or Path.

  • The lines are always separated only with a \n. Also the last line ends with a \n.

For the solution the types Pattern, Scanner and MatchResult as well as the Scanner method findAll(..) and further Stream methods are helpful.

1.1.7. Where are the sails? ⭐⭐

Bonny Brain needs a new high performance sail for the ship. The clerks from materials management prepare a list of coordinates of suitable cloth manufacturer:

Point.Double[] targets = { // Latitude, Longitude
   new Point.Double( 44.7226698,  1.6716612 ),
   new Point.Double( 50.4677807, -1.5833018 ),
   new Point.Double( 44.7226698,  1.6716612 )
};

Task:

  • In the list some coordinates occur twice, these can be ignored.

  • At the end there should be a Map<Point.Double, Integer> with the coordinate and the distance in kilometers to the current location of Bonny Brain (40.2390577, 3.7138939).

An example output might look like this:

{Point2D.Double[50.4677807, -1.5833018]=1209, Point2D.Double[44.7226698, 1.6716612]=525}

The distance in kilometers is calculated using the Haversine formula like this:

private static int distance( double lat1, double lng1,
                             double lat2, double lng2 ) {
  double earthRadius = 6371; // km
  double dLat = Math.toRadians( lat2 - lat1 );
  double dLng = Math.toRadians( lng2 - lng1 );
  double a = Math.sin( dLat / 2 ) * Math.sin( dLat / 2 ) +
      Math.cos( Math.toRadians( lat1 ) ) * Math.cos( Math.toRadians( lat2 ) ) *
          Math.sin( dLng / 2 ) * Math.sin( dLng / 2 );
  double d = 2 * Math.atan2( Math.sqrt( a ), Math.sqrt( 1 - a ) );
  return (int) (earthRadius * d);
}

Captain CiaoCiao needs to increase his fleet, so he asks the crew what armored cars are recommended. He gets an array of model names of the following type:

{source,java}

String[] cars = {
  "Gurkha RPV", "Mercedes-Benz G 63 AMG", "BMW 750", "Toyota Land Cruiser",
  "Mercedes-Benz G 63 AMG", "Volkswagen T5", "BMW 750", "Gurkha RPV", "Dartz Prombron",
  "Marauder", "Gurkha RPV" };

Task:

  • Write a program that processes an array of model names and produces a Map<String, Long> at the end that associates the model names with the number of occurrences. This part of the task can be solved well with the Stream API.

  • There should be no models named only once; only models named twice or more should appear in the data structure. For this part of the task, we can better do without the Stream API and use another variant.

An example output could look like this:

{Mercedes-Benz G 63 AMG=2, BMW 750=2, Gurkha RPV=3}

Modify the query so that all models are in a map, but the names are associated with false if there are fewer than two mentions. An output might look like this:

{Marauder=false, Dartz Prombron=false, Mercedes-Benz G 63 AMG=true, Toyota Land Cruiser=false, Volkswagen T5=false, BMW 750=true, Gurkha RPV=true}

1.2. Primitive streams

In addition to streams for objects, the Java standard library provides three special streams for primitive data types: IntStream, LongStream and DoubleStream. Many methods are similar, important differences are ranges and special reductions, for example to sum or average.

1.2.1. Detect NaN in an array ⭐

Java supports three special values for the floating-point type double: Double.NaN, Double.NEGATIVE_INFINITY, and Double.POSITIVE_INFINITY; corresponding constants exist for float in Float. For mathematical operations it must be checked whether the result is valid and not a NaN. By the arithmetic operations like addition, subtraction, multiplication, division NaN cannot be achieved, unless an operand is NaN, but various methods from the class Math return NaN as result in case of invalid input. For example, in the methods log(double a) or sqrt(double a), if the argument a is genuinely less than zero, the result is NaN.

Task:

  • Write a method containsNan(double[]) that returns true if the array contains a NaN, otherwise false.

  • A single expression should suffice in the body of the method.

Example:

double[] numbers1 = { Math.sqrt( 2 ), Math.sqrt( 4 ) };
System.out.println( containsNan( numbers1 ) );           // false

double[] numbers2 = { Math.sqrt( 2 ), Math.sqrt( -4 ) };
System.out.println( containsNan( numbers2 ) );           // true

1.2.2. Generate decades ⭐

A decade is a period of ten years, regardless of when it begins and ends. Typically, decades are grouped by their common tens digit. The 1990s begin on January 1, 1990, and end on December 31, 1999, and this interpretation is called the 0-to-9-decade. There is also the 1-to-0 decade, in which the counting of decades begins with a 1 in the ones place. According to this interpretation, the 1990s start from January 1, 1991 and end on December 31, 2000.

Task:

  • Write a method int[] decades(int start, int end) that returns all decades from a start year to an end year as an array.

  • The 0-to-9 decade is to be used.

Examples:

  • Arrays.toString( decades( 1890, 1920 ) )[1890, 1900, 1910, 1920]

  • Arrays.toString( decades( 0, 10 ) )[0, 10]

  • Arrays.toString( decades( 10, 10 ) )[10]

  • Arrays.toString( decades( 10, -10 ) )[]

1.2.3. Generate array with constant content via stream ⭐

Task:

  • Write a method fillNewArray(int size, int value).

Example:

  • Arrays.toString( fillNewArray( 3, -1 ) )[-1, -1, -1]

1.2.4. Draw pyramids ⭐

Task:

  • Create the following output from a clever combination of range(…​), mapToObj(…​) and forEach(…​):

        /\
       /\/\
      /\/\/\
     /\/\/\/\
    /\/\/\/\/\

    The pyramid is five lines high.

  • Try to solve the task in just one statement, from building the pyramid to console output.

1.2.5. Get the letter frequency of a string ⭐

One requirement for compression is to represent frequently occurring strings as short as possible. If, for example, 0 0 0 1 1 occurs in a file, then the following is stored later: four times a 0, then three times a 1. A compression algorithm tries to express the information about the numbers 0 and 1 in very few bits. It is helpful if it is known how often a symbol or a sequence occurs altogether, in order to be able to estimate whether a compression of this sequence is worthwhile at all. A loop could be run over the input beforehand and count frequencies.

Task:

  • The input is a string. Using clever stream concatenation, generate a new string containing each letter of the source string followed by the frequency of that letter in the given string.

  • The pairs of letter and frequency should be separated by a slash in the result string.

  • Performance does not play a central role.

Examples:

  • "eclectic""e2/c3/l1/e2/c3/t1/i1/c3"

  • "cccc"c4/c4/c4

  • """"

1.2.6. From 1 to 0, from 10 to 9 ⭐⭐

Bonny Brain wants to buy a new boat and sends Elaine to the marina to evaluate boats. Elaine writes her ratings from 1 to 10 in a row on a piece of paper, something like this:

102341024

The Bonny Brain gets the sequence of numbers, but is not happy with the order and the numbers. First, the numbers should be separated by a comma, and second, they should start at 0, not 1.

Task:

  • Write a method String decrementNumbers(Reader) that reads a string of digits from an input source and converts it to a comma-separated string; all numbers should be decremented by 1. Anything that is not a digit should not be included in the result.

Examples:

  • 102341024"9, 1, 2, 3, 9, 1, 3"

  • -1"0"

  • abc123xyz456"0, 1, 2, 3, 4, 5"

1.2.7. Merge two int arrays ⭐⭐

Task:

  • A method is needed that joins two int arrays.

  • There should be two overloaded methods:

    • static int[] join(int[] numbers1, int[] numbers2) and

    • static int[] join(int[] numbers1, int[] numbers2, long maxSize)

      The optional third parameter can be used to reduce the maximum number of elements in the result.

Examples:

int[] numbers1 = { 7, 12 };
int[] numbers2 = { 51, 56, 0, 2 };
int[] result1 = join( numbers1, numbers2 );
int[] result2 = join( numbers1, numbers2, 3 );
System.out.println( Arrays.toString( result1 ) );   // [7, 12, 51, 56, 0, 2]
System.out.println( Arrays.toString( result2 ) );   // [7, 12, 51]

1.2.8. Determine winning combinations ⭐⭐

Bonny Brain plans the next party and prepares a ring toss game. The first thing she does is set up different objects, such as these two:

▨ ▧

Then she puts two rings in the players' hands and let them throw. If the ring goes over an object, that counts as a win. How many ways are there to win, and what are the possibilities? Taking the two objects ▨ ▧, it could be that a player "hits" ▨ or ▧, or both — ▨ and ▧ — no hit is no win.

Task:

  • Given a string of arbitrary characters from the Basic Multilingual Plane (BMP), i.e. U+0000 to U+D7FF and U+E000 to U+FFFF.

  • Create a list of all the ways a player can win.

Example:

  • ▨▧[▧, ▨, ▨▧], but not [▧, ▨, ▨▧, ▧▨].

  • ▣▢▲[▣▢▲, ▲, ▢, ▣, ▢▲, ▣▲, ▣▢]

  • MOON[OO, MN, MO, MOON, MOO, MON, M, N, OON, ON, O]

1.3. Statistics

The IntStream, LongStream and DoubleStream streams have terminating methods such as average(), count(), max(), min() and sum(). However, if not only one of these statistical information is interesting, but several, various information can be collected in an IntSummaryStatistics, LongSummaryStatistics or DoubleSummaryStatistics.

1.3.1. The fastest and slowest paddlers ⭐

Bonny Brain hosts the annual "Venomous Paddle Open" paddling competition on party island X Æ A-12. At the end, the best, worst, and average times should be given out. The results of the paddlers are represented by the following data type:

record Result( String name, double time ) { }

Task:

  • Create a Stream of Result objects. Pre-assign some Result objects with selected values for testing.

  • Output a small statistic of times at the end.

Example:

From the following stream …​

Stream<Result> stream = Stream.of(
      new Result( "Bareil Antos", 124.123 ), new Result( "Kimara Cretak", 434.22 ),
      new Result( "Keyla Detmer", 321.34 ), new Result( "Amanda Grayson", 143.99 ),
      new Result( "Mora Pol", 122.22 ), new Result( "Gen Rhys", 377.23 ) );

... the output may look like this:

count:   6
min:     122,22
max:     434,22
average: 253,85

*Java 8 backport

Records have been added to Java 16. If you are using older versions, please use a regular class:

class Result {
  String name; double time;
  Result( String name, double time ) { this.name = name; this.time = time; }
}

1.3.2. Calculate median ⭐⭐

The XXXSummaryStatistics types return the arithmetic mean with getAverage(). The arithmetic mean is calculated by dividing the sum of the given values by the number of values. There are a number of other mean values, such as the geometric mean or the harmonic mean.

Means are often used in statistics, but they have the problem of being more prone to outliers. Statistics often works with the median. The median is the central value, that is, the value that is "in the middle" of the sorted list. Numbers that are too small or too large are at the edge and are outliers and are not included in the median.

If the number of values is odd, then there is a natural middle.

  • Example 1: In the list 9, 11, 11, 11, 12, the median is 11. If the number of values is even, the median can be defined from the arithmetic mean of the two middle numbers.

  • Example 2: In the list 10, 10, 12, 12, the median is the arithmetic mean of the values 10 and 12, i.e. 11.

Task:

  • Given is a double[] with measured values. Write a method double median(double... values) that calculates the median of the array values with even and odd number.

  • Use a DoubleStream for the solution and consider whether limit(…​) and skip(…​) help.

1.3.3. Calculate temperature statistics and draw charts ⭐⭐⭐

Bonny Brain is good with numbers, however she likes charts much better. It’s much easier to capture data graphically than it is in text form.

She is given a chart with temperature data and would like to see at a glance when it is warmest and a vacation with the family is well possible.

Task:

  • We are looking for a program that can process and display temperatures. More precisely:

    • Generate a list of random numbers that in the best case follow the temperature curve of the year, say in the form of a sine curve from 0 to π.

    • Generate random temperature values for several years, and store the years with the values in an associative memory. Use the Year data type as a key for a Map sorted by years. Bonus: The number of days really corresponds to the number of days in the year, so 365 or 366.

    • Write an ASCII table with the temperatures of all years to the console.

    • Output the highest and lowest annual temperature of a year.

    • Output the highest, lowest and average temperature for a month of a year.

    • Generate a file that aggregates and visualizes the twelve average temperatures of a month from one year. Take the following HTML document as a basis and fill the data array accordingly.

<!DOCTYPE html><html>
<head><meta charset="UTF-8"></head>
<body>
<canvas></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.3.2/dist/chart.min.js"></script>
<script>
  const cfg = {
    type: "bar",
    data: {
     labels:"Jan. Feb. Mar. Apr. May June July Aug. Sept. Oct. Nov. Dec.".split(" "),
     datasets: [{
      label: "Average temperature",
      data: [11, 17, 21, 25, 27, 29, 29, 27, 25.6, 21.6, 17.5, 12.5],
     }]
    }
  };
  window.onload = () => new Chart(document.querySelector("canvas").getContext("2d"), cfg);
</script>
</body></html>

1.4. Suggested solutions

1.4.1. Hero Epic: Meet Stream API

Terminal Operation forEach(…​)

As an abbreviation for Heroes.ALL an helper variable is introduced in the following:

List<Hero> heroes = Heroes.ALL;
com/tutego/exercise/stream/LambdaHeroes.java
Consumer<Hero> csvPrinter =
    hero -> System.out.printf( "%s,%s,%s%n",
                               hero.name, hero.sex, hero.yearFirstAppearance );
heroes.stream().forEach( csvPrinter );

To run over all elements of a stream, the method forEach(…​) can be used:

void forEach(Consumer<? super T> action).

This method expects a consumer of type Consumer. The method forEach(Consumer) calls the method apply(…​) on the passed Consumer for each element and in this way transmits the element in the stream to the Consumer. For the Consumer we declare a mapping that implements console output, pulling the three components from the Hero. For just traversing all elements, we don`t need to fetch a Stream, an Iterable also provides forEach(…​).

Terminal operation allMatch(…​).
com/tutego/exercise/stream/LambdaHeroes.java
Predicate<Hero> isAppearanceAfter1900 = hero->hero.yearFirstAppearance>=1900;
System.out.println( heroes.stream().allMatch( isAppearanceAfter1900 ) );

A Stream provides three methods that find out whether all or certain elements in the stream have a property or not: allMatch(…​), anyMatch(…​) and noneMatch(…​). All methods are passed a Predicate to test the property. If we want to know whether all heroes were introduced after 1900, we use the allMatch(…​) method. The allMatch(…​) method walks over all elements of the stream and calls the Predicate method test(…​). If this test always returns true, then all elements in the stream meet the correct criteria, and the overall response is true. If one of the tests returns false, then the final result is already false.

Terminal operation anyMatch(…​)
com/tutego/exercise/stream/LambdaHeroes.java
Predicate<Hero> isFemale = hero -> hero.sex == Sex.FEMALE;
Predicate<Hero> isAppearanceAfter1950 =
    hero -> hero.yearFirstAppearance >= 1950;
System.out.println(
    heroes.stream().anyMatch( isFemale.and( isAppearanceAfter1950 ) )
);

The interface Predicate does have some default and static methods, including and(Predicate), or(Predicate), and negate(). If two criteria are supposed to be true at the same time, two predicates can be concatenated with the and(…​) method and thus bound together to form a larger predicate. If we want to test whether some hero is female and was introduced after 1950, we can first build two single predicates and then link them. This approach is reasonable because, first, the predicates become smaller and easier to test, and second, they are easy to reuse.

Terminal operation min(…​)
com/tutego/exercise/stream/LambdaHeroes.java
Comparator<Hero> firstAppearanceComparator =
    Comparator.comparingInt( h -> h.yearFirstAppearance );
System.out.println( heroes.stream().min( firstAppearanceComparator ) );

A Stream provides min(…​) and max(…​) methods and can determine the largest and smallest element based on an ordering criterion:

Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)

The return of both methods is an Optional, because it may be that the Stream is empty. The min(…​) and max(…​) methods expect a Comparator. If the hero/heroine is asked who was introduced first, the criteria is yearFirstAppearance. Comparator.comparingInt(…​) helps to quickly build a Comparator, and the smaller the year, the smaller the objects. The min(…​) method returns the answer according to the earliest hero.

Terminal operation reduce(…​)
com/tutego/exercise/stream/LambdaHeroes.java
System.out.println( heroes.stream().reduce( ( hero1, hero2 ) -> {
  int diff1 = Math.abs( 1960 - hero1.yearFirstAppearance );
  int diff2 = Math.abs( 1960 - hero2.yearFirstAppearance );
  return (diff1 < diff2) ? hero1 : hero2;
} ) );

For the question of which hero appeared first around 1960, we rely on the reduce(…​) method, which favors exactly those heroes that are closer to the year 1960. reduce(…​) is declared as follows:

Optional<T> reduce(BinaryOperator<T> accumulator).

So a BinaryOperator must be passed. That means, two elements come in as arguments, one element comes out. The chosen implementation first calculates the distance to the year 1960 from the first and second hero and then returns the hero that is closer to 1960. In principle, if both are equidistant from 1960, the algorithm can choose one of the two heroes.

Terminal operation collect(Supplier, BiConsumer, BiConsumer)
com/tutego/exercise/stream/LambdaHeroes.java
StringBuilder collectedYears =
    heroes.stream().collect(
        StringBuilder::new,
        ( sb, hero ) -> sb.append( sb.isEmpty() ? "" : "," )
                          .append( hero.yearFirstAppearance ),
        ( sb1, sb2 ) -> sb1.append( sb2.isEmpty() ? "" : "," + sb2 ) );
System.out.println( collectedYears );

If we want to end up with a StringBuilder with all values, we will not resort to the reduce(…​) method, but to the collect(…​) method. While reduce(…​) always reduces two values to one, collect(…​) looks at all elements and transfers them into another representation. The method is overloaded, but the variant of interest to us is the following:

R collect(Supplier<R> supplier,
          BiConsumer<R, ? super T> accumulator,
          BiConsumer<R, R> combiner).

The first argument is a producer of the result. Since in our case the result is a StringBuilder, the constructor reference StringBuilder::new produces this Supplier. The second parameter is a BiConsumer and puts the year of the hero into the StringBuilder. A special handling adds the comma between elements if necessary; the separator comes between elements exactly when the StringBuilder contains elements and is not empty. The last BiConsumer of collect(…​) combines several StringBuilders, which may have been created by concurrent processing, into one StringBuilder. Although this is not necessary in our case, we want to implement this functionality as well.

Terminal operation collect(Collector) with groupingBy(…​).
com/tutego/exercise/stream/LambdaHeroes.java
Map<Sex, List<Hero>> sexListMap =
    heroes.stream().collect( Collectors.groupingBy( hero -> hero.sex ) );
System.out.println( sexListMap );

The second collect(…​) method is parameterized as follows:

<R, A> R collect(Collector<? super T, A, R> collector).

A Collector is passed. The class Collectors declares a large number of static methods for predefined Collector implementations. These include, for example, toList(), toSet(). A Collector is handy that can be used to group stream elements: Collectors.groupingBy(…​):

<T, K> Collector<T, ?, Map<K, List<T>>
groupingBy(Function<? super T, ? extends K> classifier)

The generic type information is a bit obscure, but the method is actually simple: the job of the Function is to extract the keys for the resulting Map. All elements from the stream with the same key are associated as a list with the key in the Map.

Hence, if a Map of genders is desired, the function is hero -> hero.sex, and a Map<Sex, List<Hero>> is created, such that lists of heroes that are either male or female have lists attached under each of the two genders.

Terminal operation collect(Collector) with partitioningBy().
com/tutego/exercise/stream/LambdaHeroes.java
Predicate<Hero> isAppearanceAfter1970 = hero->hero.yearFirstAppearance>=1970;
Map<Boolean, List<Hero>> beforeAndAfter1970Partition =
    heroes.stream()
          .collect( Collectors.partitioningBy( isAppearanceAfter1970 ) );
System.out.println( beforeAndAfter1970Partition );

The result of groupingBy(…​) is always a Map with any number of keys. If the resulting set knows only two different parts, the method partitioningBy(…​) can be used alternatively:

Collector<T, ?, Map<Boolean, List<T>> partitioningBy(Predicate<? super T> predicate).

We pass a Predicate for a test, and those elements that pass this test go into the Map as keys under Boolean.TRUE, the others under Boolean.FALSE.

This answers the question which heroes were introduced before and after 1970.

Intermediate operation filter(..)
com/tutego/exercise/stream/LambdaHeroes.java
System.out.println( Heroes.ALL.stream()
                          .filter( hero -> hero.sex == Sex.FEMALE )
                          .count() );

Perhaps the most important intermediate Stream method is filter(…​):

Stream<T> filter(Predicate<? super T> predicate);

All elements that satisfy the predicate are preserved in the stream. If the question is about female heroes, we write a predicate that extracts the gender of the hero and tests for Sex.FEMALE. A new Stream is created, but after filtering, the stream may contain fewer elements. The count() method returns the number of elements.

Intermediate operation sorted(…​)
com/tutego/exercise/stream/LambdaHeroes.java
Heroes.ALL.stream()
      .sorted( Comparator.comparingInt( hero -> hero.yearFirstAppearance ) )
      .forEach( System.out::println );

Some methods of the Stream class are stateful. The operations cannot wait as long as possible with the evaluation (they are not lazy), but all elements of a stream must be read in, so that operations like sorting or removing duplicate elements can be realized. If the hero stream is to be sorted by the time the heroes were introduced, we will first form a Comparator again with Comparator.comparingInt(…​) and pass it to the sorted(…​) method:

Stream<T> sorted(Comparator<? super T> comparator).

With forEach(…​) we consume the sorted stream and output all heroes on the screen.

Intermediate operation map(…​) with Collectors.joining(…​)
com/tutego/exercise/stream/LambdaHeroes.java
String femaleNames = Heroes.ALL.stream()
                           .filter( hero -> hero.sex == Sex.FEMALE )
                           .map( hero -> hero.name )
                           .collect( Collectors.joining( ", " ) );
System.out.println( femaleNames );

Besides filter(…​), the map(…​) method might be the second most important of the stream interface:

<R> Stream<R> map(Function<? super T, ? extends R> mapper).

The map(…​) method applies the Function to each element from the stream, and a new stream is created, possibly of a new type. Thus, we can also use the map(…​) method to extract all hero names. After the desired filter operation, the name is extracted and then bound together via a special Collector to form a large String where the names are comma separated.

Intermediate operation map(…​) with Collectors.toList()
com/tutego/exercise/stream/LambdaHeroes.java
Function<Hero, Hero> nameAnonymizer = hero ->
    new Hero( hero.name.replaceAll( "\\s*\\(.*\\)$", "" ),
              hero.sex, hero.yearFirstAppearance );
System.out.println( Heroes.ALL.stream().map( nameAnonymizer )
                              .collect( Collectors.toList() ) );

In the task, we want to remove the entries in round brackets. Again, we do this via the map(…​) method and via a special function that replaces the heroes. The function gets a hero with a plain name and returns a new hero with everything in round brackets removed. Processing chains of this kind could in principle modify objects and return the modified object, but it is cleaner to create new objects with the desired changes. Since Hero objects are immutable, we also need to build new Hero objects. We take the name and use replaceAll(…​) to replace everything in round brackets — and any sequence of spaces before it — with an empty string, thus deleting that part. The new name is passed to the constructor, the gender and the year remain. The newly created Stream<Hero> is transferred to a list by Collectors.toList().

Instead of Function<Hero, Hero>, UnaryOperator<Hero> can also be used.

Intermediate operation mapToInt(…​)
com/tutego/exercise/stream/LambdaHeroes.java
int[] years = Heroes.ALL.stream()
    .mapToInt( hero -> hero.yearFirstAppearance )
    .distinct()
    .toArray();
System.out.println( Arrays.toString( years ) );

In addition to the map(…​) method, there are methods that return special primitive streams:

  • IntStream mapToInt(ToIntFunction<? super T> mapper)

  • LongStream mapToLong(ToLongFunction<? super T> mapper)

  • DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

These three special streams provide the toArray() method, which results in a primitive array at the end, rather than an array of objects. This is a good way to collect all the years in an array. Duplicate elements are removed by distinct().

Intermediate operation flatMap(…​)
com/tutego/exercise/stream/LambdaHeroes.java
Heroes.UNIVERSES.stream()
      .flatMap( Heroes.Universe::heroes )
      .map( hero -> hero.name )
      .forEach( System.out::println );

The passed function of the method map(…​) leads to a direct mapping. This does not create more elements, the elements are only exchanged. The situation is different with the flatMap(…​) method:

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

While map(…​) is passed a Function<? super T, ? extends R>, flatMap(…​) is passed a Function<? super T, ? extends Stream<? extends R>>; that is, for flatMap(…​) the function must return a Stream for each element. flatMap(…​) runs these substreams and puts the elements in the result stream. In other words, flatMap(…​) returns a Stream<…​> with the "sub-elements", while map(…​) would instead return a Stream<Stream<…​>>. flatMap(…​) provides a way for the resulting stream to grow and become larger.

Heroes.UNIVERSES is a List<Universe>. If we query a stream, the result is a Stream<Universe>. A Universe has the methods String name() and Stream<Hero> heroes(). If we are interested in all Hero objects of the two universes, flatMap( Heroes.Universe::heroes ) helps, because this returns a Stream<Hero>. For comparison, map(Heroes.Universe::heroes) returns a Stream<Stream<Hero>>, we can’t do anything with that.

Since only the names are relevant in the task, the additional map(…​) results in a stream<string> and forEach(…​) outputs the names.

1.4.2. Quiz: Double output

If the program is executed, the exception occurs:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

The intermediate operations of the stream objects return new stream objects on which we must cascade to call the other methods. If the intention is that both streams start over via peek(…​) and forEach(…​), the stream must also be rebuilt. Otherwise it is not possible that forEach(…​) starts from the beginning if the stream numbers was already started by peek(…​).

1.4.3. Get the beloved captain from a list

com/tutego/exercise/stream/NameOccurrences.java
String[] names = {
    "Anne", "Captain CiaoCiao", "Balico", "Charles", "Anne", "CiaoCiao",
    "CiaoCiao", "Drake", "Anne", "Balico", "CiaoCiao" };
Map<String, Long> nameOccurrences =
    Arrays.stream( names )
          .map( s -> "CiaoCiao".equalsIgnoreCase(s) ? "Captain CiaoCiao" : s )
          .collect( Collectors.groupingBy( String::toLowerCase,
                                           Collectors.counting() ) );
System.out.println( nameOccurrences );

If we run the program, the output is:

{captain ciaociao=4, charles=1, anne=3, drake=1, balico=2}

The starting point is the array names with the names. Arrays.stream(…​) gives us a Stream of strings, an alternative is Stream.of(…​). Since our captain can appear in different notations, we normalize the notations, and with the map(…​) method, Captain CiaoCiao always appears in the Stream instead of CiaoCiao. Other strings are not transformed.

The actual aggregation and counting is done by a special Collector:

groupingBy( Function<? super T, ? extends K> classifier,
            Collector<? super T, A, D> downstream )

If we pass this groupingBy(…​)-Collector to collect(…​) we get a result of type Map<K, D> at the end.

All groupingBy(…​)-collectors expect a classifier as first argument. This determines the keys of the resulting associative memory. If we did not specify a second Collector for the downstream, a list would be associated with the key, but we do not need that — we are only interested in the number of elements associated with a key. A Collector to reduce to a long instead of a list is provided by Collectors.counting(). Internally, the counting(…​) method is implemented as follows:

OpenJDK implementation of counting(…​)
public static <T> Collector<T, ?, Long> counting() {
  return summingLong(e -> 1L);
}

counting() returns a Collector<T, ?, Long>, that is, a collector that takes elements of type T and reduces them to a Long. From the code, we can see that this is also just a shortcut, and we might as well have written:

Collectors.groupingBy( String::toLowerCase, Collectors.summingLong( e -> 1L ) )

A function is passed to summingLong(ToLongFunction) that is called for each element in the stream. Due to the constant mapping e -> 1L, summingLong(…​) only adds 1, and this is actually a waste of processing power.

groupingBy(Function classifier, Collector downstream) is also just a shortcut for groupingBy(classifier, HashMap::new, downstream).

1.4.4. Frame pictures

com/tutego/exercise/stream/FramePicture.java
private static String frame( String string ) {
  if ( string == null || string.trim().isEmpty() )
    throw new IllegalArgumentException("String to frame can't be null or empty");

  final String NEW_LINE = "\n";
  int max = string.lines().mapToInt( String::length ).max().getAsInt();
  String topBottomBorder = '+' + "-".repeat( max + 4 ) + '+' + NEW_LINE;
  String emptyRow = "|  " + " ".repeat( max ) + "  |" + NEW_LINE;

  return
      string.lines()
            .map( s -> "|  " + s + " ".repeat( max - s.length() ) + "  |" )
            .collect( Collectors.joining(NEW_LINE,
                                         topBottomBorder + emptyRow,
                                         NEW_LINE + emptyRow + topBottomBorder) );
}

In order to draw the frame around the ASCII art, different sub-problems have to be solved. It starts with the question, how long is the longest line, because that determines the width of the frame. The actual answer is provided by a single stream expression. If we map each string to the string length, and determine the maximum from the stream of integers, we get our answer.

We do two things with this length: It helps generate the top and bottom horizontal frames, and it also pads shorter lines with spaces so that all lines are the same length later. The frame starts with a plus sign, and is followed by minus signs, two more for each page than the longest string is long. There will be two more per side, because that is the desired inner distance from the right and left margin. To generate the minus signs we use repeat(int) from String. The upper and lower border is not the only String that can be generated beforehand, the same is true for the free line that has only the stroke on the left and right and spaces in the middle. This free line we put later below the top line and above the bottom line.

After preparing the variables, the actual placing of the image in the frame is just a Stream expression. Again, we fetch a Stream of lines with the lines() method and use the map(…​) method to transform each String from the ASCII image:

  • Before the string from the image, the frame symbol and the spacing are set.

  • Spaces are placed after the string, so that the string always has the same width.

  • Again some spacing and the frame symbol follow on the right side.

Finally, we collect all the lines and use a Collector for this, which we supply with three pieces of information:

  1. How are the lines separated? With a line break.

  2. What is the prefix of the whole string? It is the upper border followed by a blank line.

  3. What is the suffix at the end of the frame? Again, the blank line and the bottom line for the frame.

*Java 8 backport

The solution makes several uses of the String method repeat(…​) introduced in Java 16. Users of Java 8 can build the repetitions for example by their own method.

1.4.5. Look and say

The beauty of the solution is that, at its core, it consists of just a single statement. It is fascinating to see what possibilities the Stream API offers. Behind the solution, however, sits the Pattern class with a very special regular expression.

com/tutego/exercise/stream/LookAndSay.java
Pattern sameSymbolsPattern = Pattern.compile( "(.)\\1*" );
Function<MatchResult, String> lengthAndSymbol =
    match -> match.group().length() + match.group( 1 );

Stream.iterate( "1", s -> sameSymbolsPattern.matcher( s )
                                            .replaceAll( lengthAndSymbol ) )
      .limit( 20 )
      .forEach( System.out::println );

Let’s take the string 111221. Probably at first you think of a counting loop, which sets the index always one position further and looks if the symbol has changed. But we can use regular expressions here. This sounds strange at first sight, because what should be recognized here? The change from 1 to 2 to 1? No! A regular expression can help us to catch the symbol that repeats. We would write normal repetitions with the following regular expression:

.*

However, we would then have matched a sequence of arbitrary characters. But we need to express that the same character repeats multiple times. The character itself can be arbitrary. To achieve this, we can resort to a special feature of the regex engine implementation: the back-reference.

(.)\\1*

\1 is the back-reference that refers to the first group, that is, to (.). The dot matches one symbol in any case, and the back-reference matches any other number of identical symbols. We have to use * here and not +, because it may be that the symbol occurs only once, that is, the back-reference is not necessary.

The practical thing about sequences is that we only have to consider sequences of the same symbols. In this symbol sequence are the two pieces of information we need: How many times did which symbol occur? If we find all places with the regular expression, we can replace the found places with the length of the string and the symbol. This is exactly what the proposed solution does.

In the first step, the intermediate variable sameSymbolsPattern stores the Pattern. This is always good if a program needs the same pattern several times. In the second step we declare a variable lengthAndSymbol to map the MatchResult to a String, with the result that the length of the symbol sequence is the first part of the string and the repeating symbol is the second character of the string. Interestingly, only the lengths 1, 2, 3 occur, so it always remains with two characters that are concatenated. In principle, both variables are not necessary, but they make the following stream a bit shorter and more readable.

The static method Stream.iterator(…​) needs a start value as first argument. This is "1". It is followed by an UnaryOperator, a Function with the same type for input as for output. The operator is called with the last value of iterator(…​), so at startup it is "1", and can work its way up from there. In total, we limit the stream to 20 elements and output all elements to the screen in a terminal operation. At each step, the string grows by about 30%, so it’s good to limit the number of iterations.

1.4.6. Remove duplicate islands of rare earth metals

The task has two special features, so we can’t just use

Arrays.stream( s.split( "\n" ) )
      .distinct()
      .collect( Collectors.joining("\n") )
  • The input can not only originate from a String, but can also be given via a File, an InputStream or a Path.

  • The input is not single-line, but always consists of two lines.

However, both requirements do not change the approach of building a Stream<String> and having duplicate entries removed via distinct(). The central question is only: How do we get the different sources into one Stream, and how are two lines realized as one string in the stream?

To convert a string, with whatever separators, into a Stream<String>, the very flexible class Scanner can be used. Scanner objects can be initialized with different input sources, and these include the String, File, InputStream and Path types required in the task.

com/tutego/exercise/stream/RemoveAllEqualPairs.java
String lines =
    "Balancar\nErbium\n" +
    "Benecia\nYttrium\n" +
    "Luria\nThulium\n" +  // <-
    "Kelva\nNeodym\n" +
    "Mudd\nEuropium\n" +
    "Tamaal\nErbium\n" +
    "Varala\nGadolinium\n" +
    "Luria\nThulium\n";   // <-

// "(?m)(^.*$\n?){2}
Pattern pattern = Pattern.compile( "(^.*$\n)" +  // A line
                                   "{2}",        // two lines
                                   Pattern.MULTILINE );
String s = new Scanner( lines )
              .findAll( pattern )
              .map( MatchResult::group )
              .distinct()
              .collect( Collectors.joining() );
System.out.println( s );

The Scanner is a tokenizer and can be used in different ways:

  • the reading of tokens which are separated by white space

  • reading of lines which are terminated by an end-of-line character

  • the reading of primitives

  • reading of tokens using any regular expression which determines the separators

  • reading tokens which match a regular expression

These nextXXX() methods do not help, because they do not result in a Stream. The Scanner class has three methods that return a Stream:

  • Stream<MatchResult> findAll(String patString)

  • Stream<MatchResult> findAll(Pattern pattern)

  • Stream<String> tokens()

The findAll(…​) method is helpful in the task, because it returns results from a Match. We just need to use the regular expression to determine what exactly we want to catch, and that is exactly two lines. The regular expression "(^.*$\n){2}" consists of two central components:

  1. ^ stands for the beginning of a line, $ for the end of a line. A line is followed by a newline.

  2. This construction of a line and a newline occurs twice in succession, expressing {2}.

So that the Boundary Matchers ^ and $ stand for a local line and not for the entire input, a flag Pattern.MULTILINE must be set. This variant is chosen by the proposed solution, but the flag can also be inserted directly into the regular expression, then one would write: "(?m)(^.*$\n?){2}".

If this regex expression comes into the findAll(…​) method, a Stream<MatchResult> is created. From the MatchResult objects in the stream, only the complete match returned by group() is relevant. Each group is a two-line string. distinct() removes all duplicate two-part strings, and finally a collector concatenates all these strings in the stream back into one big String.

Java 8 backport

The overloaded Scanner methods findAll(…​) as well as tokens() return a Stream, but can only be found in Java 9. In Java 8 the solution is more complex, that for example a Stream is built with a generator, which fetches the pairs from the Scanner. The two-line strings can then be processed in a stream.

1.4.7. Where are the sails?

In each case, a Stream must be built from Point.Double objects, and at the end Collectors.toMap(…​) creates the Map. There are two ways to reach the goal, because the peculiarity is that the double elements become a problem when they are transferred as keys into a Map, because in a Map the keys must appear only once.

The proposed solution presents two different variants. Variant 1:

com/tutego/exercise/stream/DistanceToNextStation.java
Function<Point.Double, Integer> distanceToCaptain =
    coordinate -> distance( coordinate.x, coordinate.y, 40.2390577, 3.7138939 );

Map<Point.Double, Integer> map =
    Arrays.stream( targets )
          .distinct()
          .collect( Collectors.toMap( Function.identity(), distanceToCaptain ) );

We can easily have duplicate elements removed with the distinct() method. Then collect(…​) can use a Collectors.toMap(…​) to transform the elements into a Map. Reminder:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper).

The first Function determines the keys, which in our case are the Point.Double objects in the Stream. Function.identity() is t -> t, so the elements in the stream are also immediately the keys. The second function calculates the distance to the captain, where our Function<Point.Double, Integer> internally falls back to distance(…​). Thus toMap(…​) establishes the association between the point and the distance.

If the distinct() method is missing, there are keys twice, and an exception of the type: IllegalStateException: Duplicate key follows.

But it works also without distinct(). Variant 2:

com/tutego/exercise/stream/DistanceToNextStation.java
map = Arrays.stream( targets )
            .collect( Collectors.toMap( Function.identity(),
                                        distanceToCaptain,
                                        (d,__) -> d ) );

The second approach uses a different toMap(…​) method:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction)

The third argument is a BinaryOperator and the parameter name already reveals what it is about: mergeFunction is only called if keys occur multiple times and reduces the values to one result. Since in our case the key-value pairs are always identical, we can simply drop a pair of coordinate and distance. This is handled by (d,__) -> d ); the BinaryOperator<Integer> is passed two distances; the second value is ignored, and only the first distance is ever returned, since they are always the same.

The Stream API is a blessing for Java developers, but one should resist the temptation to write everything obsessively in a stream expression. This task is one of them. It is not very economical to invest 30 minutes in an unreadable stream expression when two other expressions also solve it in two minutes. Hence the following suggested solution:

com/tutego/exercise/stream/PopularCar.java
Map<String, Long> map1 =
    Arrays.stream( cars )
          .collect( Collectors.groupingBy( Function.identity(),
                                           Collectors.counting() ) );

map1.entrySet().removeIf( stringLongEntry -> stringLongEntry.getValue() < 2 );

The solution makes use of the groupingBy(…​) method, which provides a function as classifier, and a Collector for the associated elements. As a reminder:

Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                      Collector<? super T, A, D> downstream.

Our Map<String, Long> is formed via Collectors.groupingBy(Function.identity(), Collectors.counting()). The element in the stream forms the key, and the associated value is the number of occurrences of that value in the stream.

The Map method entrySet() returns a Set<Entry<String, Long>>, and the set does not contain a copy, but is a live view of the data. It is possible to call removeIf(Predicate<Entry<String, Long>>), and this method iterates over the whole set and deletes exactly those elements that satisfy the passed predicate. The predicate says to delete the entries that occur less than twice.

An alternative solution uses a separate Collector. It also no longer creates a Map<String, Long>, but a Map<String, Boolean>.

com/tutego/exercise/stream/PopularCar.java
Collector<Object, long[], Boolean> collector = Collector.of(
    // Supplier<A> supplier
    () -> new long[1],
    // BiConsumer<A,T> accumulator
    (array, string) -> array[0]++,
    // BinaryOperator<A> combiner
    (array1, array2 ) -> { array1[0] += array2[0]; return array1; },
    // Function<A,R> finisher
    array -> array[0] > 1 );

Map<String, Boolean> map2 =
    Arrays.stream( cars ).collect( Collectors.groupingBy(Function.identity(),
                                                         collector ) );

The second proposed solution also uses groupingBy(Function, Collector), but here the program does not use a predefined Collector, but writes its own for the return Map<String, Boolean>. Collector objects can be built using the following static factory method:

Collector<T, A, R> of(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Function<A, R> finisher,
                      Characteristics... characteristics)

For the desired return type Map<String, Boolean> we are looking for a Collector that returns Boolean, so the parameterization must look like this: Collector<Object, XXX, Boolean>. The first type can remain Object because we do not access the strings. The type XXX does not occur in the stream and is an internal container, we use a long array to store the count; so the correct declaration is: Collector<Object, long[], Boolean>.

In the of(…​) method, we pass the four necessary arguments; Characteristics is a vararg and irrelevant in our case.

  1. The Supplier returns a long array that is one entry large. The Collector remembers the number. The Collector has a state by which it can later decide whether String has occurred more than twice.

  2. The BiConsumer gets the long array and the String, but only increments and does not access the String. Therefore the type argument could also be Object and did not have to be String.

  3. The BinaryOperator merges two long arrays. The operation is performed only on parallel streams.

  4. The Function at the Collector maps the result to a boolean. Whenever the counter in the array is genuinely greater than 1, the result is true and thus forms the value associated with the string.

1.4.9. Detect NaN in an array

com/tutego/exercise/stream/ArrayContainsNan.java
public static boolean containsNan( double[] numbers ) {
  return DoubleStream.of( numbers ).anyMatch( Double::isNaN );
}

The terminal method anyMatch(Predicate), a regular stream and a primitive streams have, solves this task easily. The method reference Double::isNaN is an abbreviation for value -> Double.isNaN(value).

1.4.10. Generate decades

com/tutego/exercise/stream/DecadesArray.java
public static int[] decades( int start, int end ) {
  return IntStream.rangeClosed( start / 10, end / 10 )
                  .map( x -> x * 10 )
                  .toArray();
}

The primitive streams IntStream and LongStream include two static rangeXXX(…​) methods for creating a stream from integers:

IntStream
  • IntStream range(int startInclusive, int endExclusive).

  • IntStream rangeClosed(int startInclusive, int endInclusive)

LongStream
  • LongStream range(long startInclusive, long endExclusive).

  • LongStream rangeClosed(long startInclusive, long endInclusive)

The start and end value can be determined, also whether the end value belongs to the stream or not, but the step size is always 1. Since in the task the end value belongs to the result rangeClosed(…​) is a good choice.

To solve the problem, we need to increase the step size from 1 to 10. This can be done by

  1. dividing the start and end value for rangeClosed(…​) by 10, which results in a stream in steps of one, and

  2. multiplying the elements by 10 in the next step via map(…​).

Since the target is an array and not an IntStream, toArray() returns the desired int[].

1.4.11. Generate array with constant content via stream

com/tutego/exercise/stream/GenerateAndFillArray.java
public static int[] fillNewArray( int size, int value ) {
  if ( size < 0 )
    throw new IllegalArgumentException( "size can not be negative" );

  return IntStream.range( 0, size ).map( __ -> value ).toArray();
}

In the first step, the method checks the validity of the parameters as usual. The size must not be negative, otherwise an exception follows. The assignment of value does not have to be checked, because the variable can be assigned with any value.

IntStream.range(…​) generates the IntStream with size many elements. The fact that the stream generates the numbers from 0 to size is irrelevant in our case, we transfer all values to a fixed value. The identifier __ expresses that the lambda parameter is unused. This creates a stream with only constant values. toArray(…​) converts the stream into an array.

1.4.12. Draw pyramids

com/tutego/exercise/stream/StreamPyramid.java
IntStream.rangeClosed( 1, 5 )
         .mapToObj( i -> " ".repeat( 5 - i ) + "/\\".repeat( i ) )
         .forEach( System.out::println );

The generated output has the peculiarity that there are always two characters /\ next to each other. In the first line it is one pair, in the second line it is two pairs and so on. So we have to create an IntStream that goes from 1 to the desired height. The spaces that have to be set in front are also dependent on this counter. For a generated stream from 1 to 5, the number of spaces are just 5 - i.

Java 8 Backport

The String method repeat(…​) is very useful here, but it only exists since Java 16. Users of Java 8 can implement the repetitions, for example, by a separate method that uses a loop.

1.4.13. Get the letter frequency of a string

Classes implementing the CharSequence interface have two useful methods: chars() and codePoints(). They return an IntStream, where in the first method the stream consists of char characters expanded to an int and in the second method they are equal to int. The difference primarily affects compound code points, for our case we’ll stick to the simple chars() method.

com/tutego/exercise/stream/LetterOccurrences.java
String input  = "eclectic";
String output =
    input.chars()
         .mapToObj( c ->   (char) c  + ""
                         + input.chars().filter(d -> d == c).count() )
         .collect( Collectors.joining( "/" ) );
System.out.println( output ); // e2/c3/l1/e2/c3/t1/i1/c3

The IntStream provides us with a stream containing each character. We now need to associate these characters with their respective frequencies in the input string. To count frequencies, we can repeatedly build an IntStream and use the filter method and count() to find out the number of characters in the stream. If we concatenate the character and this counter, in the next step we have a Stream<String> in which each character has been mapped to this pair. Finally, these pairs must be put together with a slash. This can be done by a reduction with Collectors.joining(…​).

1.4.14. From 1 to 0, from 10 to 9

com/tutego/exercise/stream/DecrementNumbers.java
private static String decrementNumbers( Reader input ) {
  return new Scanner( input )
      .findAll( "10|[1-9]" )          // Stream<MatchResult>
      .map( MatchResult::group )      // Stream<String>
      .mapToInt( Integer::parseInt )  // IntStream
      .map( Math::decrementExact )    // IntStream
      .mapToObj( Integer::toString )  // Stream<String>
      .collect( Collectors.joining( ", " ) );
}

With a smartly chosen Stream, the task can be solved in a single expression. To get from start to finish, let’s first look at the steps involved:

  1. Split string into single numbers

  2. Reduce numbers

  3. Convert numbers concatenated into a string

The first step is to recognize the numbers. Finding partial strings is the task of regular expressions. Regular expressions can be processed with the class Pattern, but this class does not help us with data streams. The second class that allows us to find strings that match regular expressions is Scanner. We can apply a Scanner directly to a Reader. The Scanner returns a Stream<MatchResult> of all matches with the findAll(…​) method. The regular expression must recognize all possible occurring numbers, i.e. 10|9|8|…​|1. The or-connection can be abbreviated as 10|[1-9], but not as [1-9]|10.

The matches are returned as MatchResult, and we have to fall back from that to the main group with group(); that gives us a String. For summing, we use mapToInt(…​) to convert the String to an integer first. This is done in Integer.parseInt(…​). It is perfect to use a method reference, because Integer.parseInt(…​) matches the signature of Function. Parse errors cannot exist, because the regular expression only matches numbers.

The integer itself must be decremented by 1. We could write this as a lambda expression, but there is a suitable method that can also be accessed via a method reference: Math.decrementExact(…​); it does throw an ArithmeticException if we exceed the value range, but that does not occur in our case.

After decrementing the number, everything must be converted to a large string. This is done in two parts: In the first step, each integer is converted into a string by itself, then these strings are assembled into one long string using a special collector.

1.4.15. Merge two int arrays

com/tutego/exercise/stream/JoinIntArrays.java
public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

public static int[] join( int[] numbers1, int[] numbers2, long maxSize ) {
  if ( maxSize > MAX_ARRAY_LENGTH )
    throw new IllegalArgumentException( "Requested array size exceeds VM limit" );

  return IntStream.concat( IntStream.of( numbers1 ), IntStream.of( numbers2 ) )
                  .limit( maxSize )
                  .toArray();
}

public static int[] join( int[] numbers1, int[] numbers2 ) {
  return join( numbers1, numbers2, (long) numbers1.length + numbers2.length );
}

To make our join(…​) method a bit more flexible there are two implementations. join(int[] numbers1, int[] numbers2, int maxSize) has an additional parameter maxSize which limits the number of resulting elements of the array. There is no separate method in Java for merging two arrays, but primitive streams come to the rescue. At the heart of this is the method:

IntStream concat(IntStream a, IntStream b)

We have to use IntStream.of(…​) to make an IntStream out of the two arrays, put them into concat(…​) and we get a primitive stream composed of two arrays. toArray() creates the desired array at the end.

However, we have to be careful about one thing: At the current state, arrays cannot grow larger than Integer.MAX_VALUE - 8, which is why there is a constant in the proposed solution. [1].

In the join(…​) method, the first control statement checks that the passed maxSize is not larger than the resulting array may be. Even if later numbers1.length + numbers2.length > MAX_ARRAY_LENGTH, limit(maxSize) of the Stream object keeps the resulting array within bounds.

The simpler join(…​) method with two parameters then determines the total length and delegates to the three-parameter method. Array lengths are of type int, and we extend that to long so that the sum is a long, we don’t get an overflow, and we can subsequently see that the sum is not over MAX_ARRAY_LENGTH.

1.4.16. Determine winning combinations

The algorithmic approach is the following: A method is declared that takes a string. From this string, the first character is deleted and then the method is called recursively with the newly created string. Then the second character is deleted and the method is again called recursively, and so on. The recursive method ends when the string has no more characters and is empty.

The method just described is in the solution removeLetter(…​):

com/tutego/exercise/stream/RemovingLetters.java
private static void removeLetter( String word, Set<String> words ) {
  if ( word.isEmpty() )
    return;
  words.add( word );
  IntStream.range( 0, word.length() )
           .mapToObj( i -> new StringBuilder(word).deleteCharAt(i).toString() )
           .forEach( substring -> removeLetter( substring, words ) );
}

public static Set<String> removeLetter( String word ) {
  Set<String> words = new HashSet<>();
  removeLetter( word, words );
  return words;
}

The resulting strings go into a set, and duplicate resulting strings fly out. Since the caller expects a set and should not pass an empty container into the method, there is a second public method removeLetter(String) that builds a HashSet, passes it along with the word to the private method removeLetter(String, Set<String>) and returns the built set at the end.

1.4.17. The fastest and slowest paddlers

The goal of the task is to create a DoubleSummaryStatistics object. This statistics object provides information about the number of elements considered, the minimum, maximum and average. There are two ways to implement a statistics object.

com/tutego/exercise/stream/PaddleCompetition.java
DoubleSummaryStatistics statistics =
    stream.mapToDouble( Result::time ).summaryStatistics();

System.out.printf( "count:   %d%n",   statistics.getCount() );
System.out.printf( "min:     %.2f%n", statistics.getMin() );
System.out.printf( "max:     %.2f%n", statistics.getMax() );
System.out.printf( "average: %.2f%n", statistics.getAverage() );

The first variant is to create a DoubleStream with the times from the Stream<Result> and then call summaryStatistics() on the DoubleStream.

The second possibility is to use directly a corresponding Collector:

com/tutego/exercise/stream/PaddleCompetition.java
DoubleSummaryStatistics statistics =
    stream.collect( Collectors.summarizingDouble( Result::time ) );

An extraction function can be supplied to Collectors.summarizingDouble(…​) that extracts the time directly from the Stream<Result>; this saves the program an intermediate step.

Java 8 Backport

For records, the record components are accessed via methods, for example time(). The method in turn can be used in a method reference. If you write the solution with a class, you will write the lambda expression result -> result.time instead of result::time.

1.4.18. Calculate median

com/tutego/exercise/stream/DoubleStreamMedian.java
public static double median( double... values ) {
  if ( values.length < 1 )
    throw new IllegalArgumentException( "array contains no elements" );

  int skip  = (values.length - 1) / 2;
  int limit = 2 - values.length % 2;
  return Arrays.stream( values ).sorted().skip( skip )
               .limit( limit ).average().getAsDouble();
}

As usual, we check the input, and if the array consists of no elements, there is an exception. Also there is automatically an exception if values is equal to null.

When calculating the median, we have to navigate to the middle. Then we have to consider one element or two elements in the middle. If a DoubleStream is built and then sorted, skip(…​) allows to skip a certain number of elements to get to the middle. limit(…​) in the next step reduces the number of remaining elements in the stream to either one element (array had an odd number of elements) or two elements (array had an even number of elements). Finally, the chain averages the value, with nothing much to calculate for one value, but with two elements, the average() method gives us the arithmetic average. Since the method wants a floating point number as the result, getAsDouble() returns that number, and that is valid since the stream has exactly one element. An alternative API design could have returned OptionDouble, thus accounting for the special case where the double array contains no elements.

The most exciting part is the calculation of the shift and the limit. For this, the program introduces two variables skip and limit, which are derived from the length of the input. Two examples:

Table 1. examples for the assignment of limit and skip.
listskiplimit

9, 11, 11, 11, 12

2

1

10, 10, 12, 12

1

2

The variables are initialized as follows:

int skip  = (values.length - 1) / 2;
int limit = 2 - values.length % 2;

Both variables depend on the length of the array. The center is the array length divided by 2, which fits quite well for arrays with an odd number of elements, but a pure division by 2 leads to a problem for arrays with an even number of elements. Because in that case we have to consider the two elements to the left of the center and to the right of the center. (These elements even have a name and are called upper and lower median). Therefore, if the length is decreased by 1 before dividing by 2, we end up with an even number one element before the center. To make it clear:

Table 2. examples for the calculation of skip.
list(values.length - 1) / 2(values.length) / 2

9, 11, 11, 11, 12

(5 - 1) / 2 = 2

5 / 2 = 2

10, 10, 12, 12

(4 - 1) / 2 = 1

4 / 2 = 2

The limit must be 2 for an even number of elements in an array and 1 for an odd number of elements. values.length % 2 returns 0 for an even number and 1 for an odd number. Consequently, the expression 2 - values.length % 2 returns 2 - 0 = 2 for an even number and 2 - 1 for an odd number, i.e. 1.

1.4.19. Calculate temperature statistics and draw charts

The task breaks down into several methods. A method randomTemperaturesForYear(Year) generates a random temperature for each day in the year. Another method createRandomTemperatureMap() calls randomTemperaturesForYear(…​) five times. The output in form of a small table on the command line is realized by another method printTemperatureTable(…​). Finally, the method writeTemperatureHtmlFile(…​) writes the average values to a file.

Let’s start with randomTemperaturesForYear(Year).

com/tutego/exercise/stream/TemperatureYearChart.java
private static int[] randomTemperaturesForYear( Year year ) {
  int daysInYear = year.length();
  return IntStream.range( 0, daysInYear )
      .mapToDouble( value -> sin( value * PI / daysInYear ) ) // 0..1
      .map( value -> value * 20 ) // 0..20
      .map( value -> value + 10 ) // 10..30
      .mapToInt( value -> (int) (value + 3 * (random() - 0.5)) )
      .toArray();
}

private static SortedMap<Year, int[]> createRandomTemperatureMap() {
  UnaryOperator<Year> previousYear = year -> year.minusYears( 1 );
  return Stream
      .iterate( Year.now(), previousYear )
      .limit( 5 )
      .collect( toSortedMap( identity(),
                             TemperatureYearChart::randomTemperaturesForYear ) );
}

private static Collector<Year, ?, TreeMap<Year, int[]>>
toSortedMap( Function<Year, Year> keyMapper,
             Function<Year, int[]> valueMapper ) {
  return Collectors.toMap( keyMapper,
                           valueMapper,
                           (y1, y2) -> {throw new RuntimeException("Duplicates");},
                           TreeMap::new );
}

The special datatype Year is useful because it returns the number of days in the year via the length() method, because not every year is 365 days long. With this number of days in the year, we can form an IntStream and then generate a random temperature for each day in the year. The average temperature distribution shows up in a curve: at the beginning and end of the year the temperature is low and in the middle of the year it is high. This is something we can express well using a sine function. Therefore, we transfer the integer with a function into a sine value, where the beginning of the year at the first day corresponds to the sine of 0 and the end of the year at the last day results in the sine of π (Pi) thus again 0. In between is the sine hill. The sine values between 0 and 1 are small, the maximum of sin(π) is 1, so the values are multiplied by 20 in the next step and then taken plus 10 in the next step. The values are not random now, so the last mapping brings in some randomness so that the sine values fluctuate up and down a bit.

createRandomTemperatureMap() uses the randomTemperaturesForYear(…​) method to build a Stream<Year> with five elements, starting at the current year and then moving forward one year at a time. The end result is a SortedMap<Year, int[]> of the temperature values associated with each year. However, the method toMap(Function keyMapper, Function valueMapper) with two arguments should not be used, because there is no prediction of the Map — internally the OpenJDK uses a HashMap. In our case, a Map sorted by years is useful. Therefore toMap(Function keyMapper, Function valueMapper, BinaryOperator mergeFunction, Supplier mapFactory) is used, so that explicitly the data comes into a sorted TreeMap.

This can go into the output:

com/tutego/exercise/stream/TemperatureYearChart.java
private static void printTemperatureTable( SortedMap<Year,
                                           int[]> yearToTemperatures ) {
  yearToTemperatures.forEach( (year, temperatures) -> {
    String temperatureCells =
        Arrays.stream( temperatures )
              .mapToObj( temperature -> String.format( "%2d", temperature ) )
              .collect( Collectors.joining( " | ", "| ", " | " ) );
    System.out.println( "| " + year + " " + temperatureCells );
  } );
}

printTemperatureTable(…​) takes care of printing the table for all the years passed in. The pass is an associative store that associates the years with the temperatures. The forEach(…​) method goes over the data structure sorted by years and creates a string temperatureCells. To build the string, the first step is to turn the int array into an IntStream. Each temperature value is mapped to a string, with values less than 10 getting a preceding space so that the output is always two digits. The collector combines all the individual strings, and the experience is temperatureCells. This string for the temperature values is then printed, with the preceding years, on the screen.

Let’s move on to the statistics:

com/tutego/exercise/stream/TemperatureYearChart.java
IntSummaryStatistics yearStatistics =
    Arrays.stream( yearToTemperatures.get( Year.now() ) ).summaryStatistics();
System.out.printf( "max: %d, min: %d%n",
                   yearStatistics.getMax(), yearStatistics.getMin() );

We have already used a few times the Arrays.stream(…​) method, which is an alternative to IntStream.of(…​) and creates an IntStream from an int array. The three primitive streams have a special method summaryStatistics() compared to the regular Stream objects, which provide a statistics object with information about minimum, maximum and average value. We can easily grab this information and output it.

A separate method getStatistics(…​) retrieves these IntSummaryStatistics for one month of a year from the Map:

com/tutego/exercise/stream/TemperatureYearChart.java
private static IntSummaryStatistics getStatistics( YearMonth yearMonth,
                                                   int... temperatures ) {
  int start = yearMonth.atDay( 1 ).getDayOfYear();
  int end   = yearMonth.atEndOfMonth().getDayOfYear();
  return Arrays.stream( temperatures, start - 1, end ).summaryStatistics();
}

In the passed int array temperatures all temperature values of the year are stored. If we want to calculate the statistics of a concrete month, we have to build a sub-array at the appropriate place. This can be solved well with Arrays.stream(…​), because the start and end index in this array can be given. The question with the approximately 365 days in the year is, when does e.g. March or December begin? The answer is provided by the YearMonth object. For the start value we request with atDay(1) a new YearMonth object for the beginning of the month and get with getDayOfYear() the day in the year. We do the same for the last day of the month, using the atEndOfMonth() method to set the YearMonth object to the end of the month. We pass the start and end values to Arrays.stream(…​), where the month starts at 1 and we need to move the start value one position to the left. We do not move the end one position to the left, because the value is exclusive and not inclusive.

The method can be called as follows:

com/tutego/exercise/stream/TemperatureYearChart.java
IntSummaryStatistics monthStatistics =
    getStatistics( YearMonth.of( 2020, SEPTEMBER ),
                   yearToTemperatures.get( Year.now() ) );
System.out.printf( "max: %d, min: %d, average: %.2f%n", monthStatistics.getMax(),
                   monthStatistics.getMin(), monthStatistics.getAverage() );

Finally, the method for writing to a file:

com/tutego/exercise/stream/TemperatureYearChart.java
  private static void writeTemperatureHtmlFile(
      Year year, Map<Year, int[]> yearToTemperatures, Path path )
        throws IOException {
    String template = """
<!DOCTYPE html><html>
 <head><meta charset="UTF-8"></head>
 <body>
 <canvas></canvas>
 <script src="https://cdn.jsdelivr.net/npm/chart.js@3.3.2/dist/chart.min.js">
 </script>
 <script>
   const cfg = {
     type: "bar",
     data: {
      labels:"Jan. Feb. Mar. Apr. May June July Aug. Sept. Oct. Nov. Dec.".split(" "),
      datasets: [{
       label: "Average temperature",
       data: [%s],
      }]
     }
   };
   window.onload=()=>new Chart(document.querySelector("canvas").getContext("2d"),cfg);
 </script>
 </body></html>""";

    String formattedTemperatures =
        IntStream.rangeClosed( JANUARY.getValue(), DECEMBER.getValue() )
          .mapToObj( Month::of )
          .map( month -> year.atMonth( month ) )
          .map( yearMonth->getStatistics(yearMonth,yearToTemperatures.get(year)) )
          .map( IntSummaryStatistics::getAverage )
          .map( avgTemperature -> String.format( ENGLISH, "%.1f", avgTemperature ) )
          .collect( Collectors.joining( "," ) );
    String html = String.format( template, formattedTemperatures );
    Files.writeString( path, html );
  }

The declaration of the HTML document takes up the largest area in the method. In one place it says data: [%s], and this %s is a typical placeholder in the format string, so we can use the String.format(…​) method later to insert the dynamically calculated values. The method proceeds as follows:

  • rangeClosed(…​) returns an IntStream with numbers from 1 to 12.

  • mapToObj(…​) transfers these numbers to Month objects.

  • The year is passed to the writeTemperatureHtmlFile(…​) method. The year.atMonth(month) expression associates the year with the month from the iteration and returns a new YearMonth object for mapping. The lambda expression month -> year.atMonth(month) could be abbreviated to year::atMonth, but the lambda expression written out should be more readable.

  • The resulting Stream<YearMonth> can be accessed using its own getStatistics(…​) method on the IntSummaryStatistics; the result is of type Stream<IntSummaryStatistics>.

  • From this IntSummaryStatistics we get the average value with the method reference, a Stream<Double> is created.

  • Finally, we format this average value to create a string for one month.

  • This is done for all months, and finally the strings are collected and comma separated.

Finally, the string formattedTemperatures contains the result, which now has to be inserted into the HTML document. After creating the complete HTML document, write(…​) creates the file.

writeTemperatureHtmlFile(…​) can be used as follows:

com/tutego/exercise/stream/TemperatureYearChart.java
try {
  Path tempFile = Files.createTempFile( "temperatures", ".html" );
  writeTemperatureHtmlFile( Year.now(), yearToTemperatures, tempFile );
  Desktop.getDesktop().browse( tempFile.toUri() );
}
catch ( IOException e ) { e.printStackTrace(); }

Java 8 backport

For the multi-line string, Java 15’s text blocks are very useful; Java 8 users must concatenate many lines here and be careful to mask out each " in the string with a \".

The Files method writeString(…​) is new in Java 11. In Java 8, only collections of lines can be written, so for a string html something like this: Files.write( path, Collections.singleton( html ) ).


1. The OpenJDK declares a constant in the internal package jdk.internal.util: public class ArraysSupport { public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; …​ }. Since the jdk.internal.util module is taboo for us, there is a copy of the constant in the proposed solution