Java:Language - Streams and Lambdas - Exercises

From Juneday education
Jump to: navigation, search

Exercises on Java 8 Lambdas and Java 8 Streams.

Exercises

All interfaces with only one abstract non-static method are called Functional interfaces. Such interfaces can be implemented on-the-fly by creating a lambda expression.

On useful such interface is java.util.function.Predicate<T>. It represents a logical predicate (a claim about the world, which is either true or false).

There are many methods that accept a Predicate as an argument. Typically filtering methods accept a predicate.

Here are some methods on creating and using predicates (some of which are Predicates). There are also some exercises on the streams API (which works in combination with lambdas).

A stream can be obtained from e.g. a collection. The stream will contain the objects in the collection, which can be filtered. No unlike how you can use a stream of text in Bash and pipe that stream to various filters. There are some exercises below, also on such streams.

Both streams and lambdas where introduced in Java 8.

Some useful imports for these exercises:

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Filtering a list of strings

Create a small program (a class with a main method) which converts the args to a List<String>.

You can use the following idiom:

    // create a List from the String[] args:
    List<String> arguments = Arrays.asList(args);

Note: The resulting List is fixed-size and backed by the array - changes in the List will change also the array in the corresponding way.

Now, you shall sort the list using a custom java.util.Comparator. Comparator is a functional interface, since the only public abstract method is int compare(T t1, T t2).

Task a)

Create a lambda representing the compare method in a Comparator for Strings, which ignores case in both argument strings. Use this lambda as argument to Collections.sort() (or arguments.sort()).

Hint: The lambda should take two arguments, and return the first as only lower case letters, compareTo the second as only lower case letters.

Expand using link to the right to see one possible solution (there are infinitely many solutions to this).

    arguments.sort((s1, s2) -> s1.toLowerCase().compareTo(s2.toLowerCase()));
    // Or, using Collections.sort:
    Collections.sort(arguments, (s1, s2) -> s1.toLowerCase().compareTo(s2.toLowerCase()));

Task b)

Create another lambda for comparing the strings. This time you should pretend the strings in args are group names of music bands. The lambda should ignore a leading "The ", so that The Doors is considered less than Europe (the leading "The " is not used in the compareTo call inside the lambda).

Hint: It is easier to test your code, if you scramble your list between the different sortings: Collections.shuffle(arguments);

Hint: When giving e.g. The Doors as argument to your program, surround it with double-quotes: "The Doors"

Expand using link to the right to see one possible solution (there are infinitely many solutions to this).

    arguments.sort( (s1, s2) ->
                      (s1.startsWith("The ") ? s1.substring(4) : s1)
                      .compareTo(
                      (s2.startsWith("The ") ? s2.substring(4) : s2)
                                )
                  );

Task c)

Use a predicate, to filter your list.

Since the list you get from Arrays.asList() is fixed-size, you need to create a copy of the list as a new ArrayList for this exercise:

    List<String>copy = new ArrayList<>(arguments);

The only non-static abstract method in Predicate<T> is boolean test(T t). So a lambda for a predicate takes one argument of type T and returns a boolean value.

Now, create a java.util.function.Predicate<String> which returns true if the string (the only argument) starts with "The ".

Use this predicate to keep only the arguments that start with "The " and print the result to standard out.

Hint: You can use removeIf(Predicate<? super E> filter) on the copy list. That method takes a predicate as argument and removes the elements for whom the predicate returns true

But, wait! The predicate returns true for strings that start with "The "! We shouldn't remove those! We should keep those! Well, there is no retainIf(Predicate) method, so what to do?

You can negate a predicate, using the method default Predicate<T> negate(). If your predicate returns true for strings that start with "The ", then applying negate() on your predicate will do the opposite!

So, call copy.removeIf(yourPredicate.negate()), in order to keep the strings starting with "The ".

Start by declaring your Predicate<String>.

    Predicate<String> p = .....your lambda here;
    copy.removeIf(p.negate());

Expand using link to the right to see one possible solution (there are infinitely many solutions to this).

    List<String>copy = new ArrayList<>(arguments);
    System.out.println("copy: ");
    System.out.println(copy);

    Predicate<String> p = s -> s.startsWith("The ");
    copy.removeIf(p.negate());
    System.out.println("\ncopy with only strings staring with 'The ':");
    System.out.println(copy);

Task d)

Now, it's time to use a Stream<String> to do some filtering.

Start by restoring your copy:

    copy = new ArrayList(arguments);

To get a Stream<String> from copy, you only have to call the method stream() on the list (or any collection).

Hints:

  • stream() creates a Stream<E> from a Collection<E>
  • filter(Predicate<E>) filters a stream
  • collect(Collector<E>) gathers elements of a stream to a collection
  • Collectors.toList() is a collector for creating a List of the stream
  • All of the above method calls can be chained together.
    • Each chained method call, corresponds to a Bash/Unix pipe
    • e.g. cat file.txt | grep 'The ' corresponds to collection.stream().filter(pred).collect(someCollector)

Expand using link to the right to see one possible solution (there are infinitely many solutions to this).

    copy = new ArrayList<>(arguments);
    System.out.println("\ncopy, restored:");
    System.out.println(copy);

    List<String> result = copy
      .stream()
      .filter(p)
      .collect(Collectors.toList());
    System.out.println("\nCopy filtered using stream():");
    System.out.println(result);

Complete source of an example solution

This is not the only solution, it is merely one solution out of many possible solutions:

Expand using link to the right to see one possible solution (there are infinitely many solutions to this).

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.function.Predicate;

public class FilterArgs {
  public static void main(String[] args) {
    List<String> arguments = Arrays.asList(args);
    //Collections.sort(arguments);
    System.out.println("Un-sorted:");
    System.out.println(arguments);

    arguments.sort((s1, s2) -> s1.toLowerCase().compareTo(s2.toLowerCase()));
    System.out.println("\nCase-insensitive sort:");
    System.out.println(arguments);

    Collections.shuffle(arguments);
    System.out.println("\nShuffled:");
    System.out.println(arguments);

    arguments.sort( (s1, s2) ->
                       (s1.startsWith("The ") ? s1.substring(4) : s1)
                   .compareTo(
                       (s2.startsWith("The ") ? s2.substring(4) : s2)
                              ) );
    System.out.println("\nRecord-store sort:");
    System.out.println(arguments);
                   
    Collections.shuffle(arguments);
    System.out.println("\nShuffled:");
    System.out.println(arguments);

    List<String>copy = new ArrayList<>(arguments);
    System.out.println("copy: ");
    System.out.println(copy);

    Predicate<String> p = s -> s.startsWith("The ");
    copy.removeIf(p.negate());
    System.out.println("\ncopy with only strings staring with 'The ':");
    System.out.println(copy);

    copy = new ArrayList<>(arguments);
    System.out.println("\ncopy, restored:");
    System.out.println(copy);

    List<String> result = copy
      .stream()
      .filter(p)
      .collect(Collectors.toList());
    System.out.println("\nCopy filtered using stream():");
    System.out.println(result);
  }
}

Example runs:

$ javac -Xlint:unchecked FilterArgs.java && java FilterArgs "The Eagles"
 "The Who" "Europe" "The Doors" "ABBA" "Carola Häggkvist" "The The" "The Beatles" "Bronsky Beat" "Frank Zappa" "The Mothers"
Un-sorted:
[The Eagles, The Who, Europe, The Doors, ABBA, Carola Häggkvist, The The, The Beatles, Bronsky Beat, Frank Zappa, The Mothers]

Case-insensitive sort:
[ABBA, Bronsky Beat, Carola Häggkvist, Europe, Frank Zappa, The Beatles, The Doors, The Eagles, The Mothers, The The, The Who]

Shuffled:
[Carola Häggkvist, The Eagles, The Beatles, The The, ABBA, The Doors, Frank Zappa, Europe, The Mothers, Bronsky Beat, The Who]

Record-store sort:
[ABBA, The Beatles, Bronsky Beat, Carola Häggkvist, The Doors, The Eagles, Europe, Frank Zappa, The Mothers, The The, The Who]

Shuffled:
[Europe, Bronsky Beat, ABBA, The Doors, The The, Carola Häggkvist, Frank Zappa, The Eagles, The Mothers, The Who, The Beatles]
copy: 
[Europe, Bronsky Beat, ABBA, The Doors, The The, Carola Häggkvist, Frank Zappa, The Eagles, The Mothers, The Who, The Beatles]

copy with only strings staring with 'The ':
[The Doors, The The, The Eagles, The Mothers, The Who, The Beatles]

copy, restored:
[Europe, Bronsky Beat, ABBA, The Doors, The The, Carola Häggkvist, Frank Zappa, The Eagles, The Mothers, The Who, The Beatles]

Copy filtered using stream():
[The Doors, The The, The Eagles, The Mothers, The Who, The Beatles]

Books and chapters

Let's say we want to handle Books in an application, where a Book has a title and a list of chapters. A Chapter has a name and a number of pages. A Book also has a number of pages, as a function of the number of pages in all its chapters.

Domain classes:

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public final class Book {

  private final List<Chapter> chapters;
  private final String title;

  public Book(String title) {
    this(title, new ArrayList<>());
  }

  public Book(String title, List<Chapter> chapters) {
    this.title = title;
    this.chapters = chapters;
  }
  
  public String title() {
    return title;
  }

  public List<Chapter> chapters() {
    return Collections.unmodifiableList(chapters);
  }

  public int pages() {
    // You implement the pages method
    // as a the sum of all chapters' pages
    return 0;
  }

  public String toString() {
    return new StringBuilder(title)
      .append(" (")
      .append(pages())
      .append(" pages)\n")
      .append("Chapters: ")
      .append(chapters)
      .toString();
  }
}

public class Chapter {

  private int pages;

  private String name;

  public Chapter(String name, int pages) {
    this.name = name;
    this.pages = pages;
  }

  public int pages() {
    return pages;
  }

  public String name() {
    return name;
  }

  public String toString() {
    return new StringBuilder(name)
      .append(" (")
      .append(pages)
      .append(" pages)")
      .toString();
  }
}

Your tasks are to finish the pages() method of Book using streams, and also to write some utility methods working on books and chapters.

You can use these methods to create some test lists with books:

  static List<Book> getBooks() {
    return new ArrayList<>(Arrays.asList(new Book("Java"),
                                       new Book("C"),
                                       new Book("Databases"),
                                       new Book("Web")));
  }

  static List<Book> getBooksWithChapters() {
    List<Book> books =
      new ArrayList<>(Arrays
                      .asList(new Book("Java",
                                       Arrays.asList(new Chapter("Variables",
                                                                 30),
                                                     new Chapter("Methods",
                                                                 48),
                                                     new Chapter("Classes",
                                                                 44))),
                              new Book("Databases",
                                       Arrays.asList(new Chapter("SELECT",
                                                                 40),
                                                     new Chapter("DELETE",
                                                                 38),
                                                     new Chapter("JOIN",
                                                                 24)))));
    return books;
  }

Task a)

Finish the pages() method of Book.

Hints:

  • Create a stream from the chapters list
  • map every element to int
    • you may use a method reference here
  • sum it up

Links:

Expand using link to the right to see a suggested solution.

Suggested solution:

  public int pages() {
    return chapters
      .stream()
      .mapToInt(Chapter::pages)
      .sum();
  }

Task b)

Write a utility method, static Set<String> titlesUnion(List<Book> b1, List<Book> b2) which takes two lists of books and returns a Set<String> with the unique titles from both lists.

The method should return a set with the titles from both lists without any duplicates (that's what a set is).

Hints:

  • You can create streams from both lists
  • You can concat the streams into one stream
  • You can map all elements to a string
    • You can use a method reference here
  • You can collect each element (now a String) to a set

Links:

Expand using link to the right to see a suggested solution.

Suggested solution:

  static Set<String> titlesUnion(List<Book> b1, List<Book> b2) {
    return Stream.concat(b1.stream(), b2.stream())
      .map(Book::title)
      .collect(Collectors.toSet());
  }

Task c)

Write a utility method static Book findByTitle(List<Book> books, String title) which takes a list of books and a title string, and returns the corresponding book from the list, if it exists, or a new Book with the title if it doesn't exist.

Hints:

  • Use a helper method which returns an Optional<Book>
  • You can filter the stream from the list with a Predicate<String>
  • the findFirst() method of Stream returns an Optional of the element type

Links:


Expand using link to the right to see a suggested solution.

Suggested solution:

  static Book findByTitle(List<Book> books, String title) {
    return findByTitle_(books, title).orElse(new Book(title));
  }
  
  static Optional<Book> findByTitle_(List<Book> books, String title) {
    return books
      .stream()
      .filter(b -> b.title().equals(title))
      .findFirst();
  }

Alternative solution without the helper method:

  static Book findByTitle2(List<Book> books, String title) {
    return books
      .stream()
      .filter(b -> b.title().equals(title))
      .findFirst()
      .orElse(new Book(title));
  }

Links

Source code

Where to go next

Previous page was the theory/topic page (with videos and examples) for Streams and Lambdas. Next page is not yet decided.

« PreviousBook TOCNext »