11. Lambda Expressions Lesson

Lambda Expressions

Lambda Expressions

Lesson Objectives

Motivate and introduce lambda Expressions.

Part 1: Another Generic max method?
Towards Generic Interfaces
Example 1: Drawable
The Drawable interface

!includesub chap10-lesson.common.puml!STYLE
!includesub chap10-lesson.common.puml!DRAWABLE
!includesub chap10-lesson.common.puml!DRAWABLE_CLASSES

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Person : "implements"

Remember: Drawable was an interface that captured common functionality. All implementing classes have a draw method and can be drawn.

This interface is NOT generic.

Activity: Using the Drawable interface

!includesub chap10-lesson.common.puml!STYLE
!includesub chap10-lesson.common.puml!DRAWABLE
!includesub chap10-lesson.common.puml!DRAWABLE_CLASSES

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Person : "implements"

public static void test(Drawable d) {
   d.______;
} // test

With your group, write which method(s) can be called using variable d. Then, write down where the code for the method(s) comes from.

Solution

Since any object that d refers to is an object of some class and all classes have java.lang.Object at the top of their hierarchy, we can call draw and all instance methods of the Object class using d.

However, since an interface is used as the datatype for d, it is probably the programmer's intention to call the method required by that interface inside the method.

Example 2: StringComparator
The StringComparator interface

!includesub chap10-lesson.common.puml!STYLE
!includesub chap10-lesson.common.puml!STRING_COMPARATOR
!includesub chap10-lesson.common.puml!STRING_COMPARATOR_CLASSES

StringComparator <|..down.. LengthComparator : "implements"
StringComparator <|..down.. VowelCountComparator : "implements"

Similarly, StringComparator captures the common functionality of comparing strings. All implementing classes contain a method called compare that allows two strings to be compared.

This interface is NOT generic.

public static void test(StringComparator sc) {
    sc.______;
} // test

With your group, write which method(s) can be called using variable sc. Then, write down where the code for the method(s) comes from.

Implement the StringComparator interface

!includesub chap10-lesson.common.puml!STYLE
!includesub chap10-lesson.common.puml!STRING_COMPARATOR
!includesub chap10-lesson.common.puml!STRING_COMPARATOR_CLASSES

StringComparator <|..down.. LengthComparator : "implements"
StringComparator <|..down.. VowelCountComparator : "implements"

Listing 102 The compare method in LengthComparator.
 1public class LengthComparator implements StringComparator {
 2
 3   /**
 4    * Compares its two arguments by length. Returns a
 5    * negative integer if the first argument is shorter
 6    * than the second, zero if they are equal length, or
 7    * a positive integer if the first argument is longer
 8    * than the second.
 9    *
10    * @param o1 the first argument to compare.
11    * @param o2 the second argument to compare.
12    * @return the result of the length comparison.
13    */
14   @Override
15   public int compare(String o1, String o2) {
16       //
17       // your code here
18       //
19   } // compare
20
21} // LengthComparator

Write the code for the compare method in LengthComparator on your exit ticket (and in your own notes). Be sure to include the method signature. Do not include a Javadoc comment, unless you need it for your notes.

Sample Solution
public class LengthComparator implements StringComparator {

   @Override
   public int compare(String o1, String o2) {
       int length1 = o1.length();
       int length2 = o2.length();
       int result = length1 - length2;
       // negative: when length1 is shorter
       //    equal: when length1 is equal to length2
       // positive: when length2 is shorter
       return result;
   } // compare

} // LengthComparator
Example: Using our StringComparator implementation

With your groups, fill in the blanks below:

Listing 103 in class Utility
 1/**
 2 * Returns the "max" string in the array. In this we
 3 * define "max" as longest.
 4 *
 5 * @param items the array of strings to compare.
 6 * @param sc a reference to an object containing
 7 *     the compare method.
 8 * @return the "max" string.
 9 */
10public static String max(String[] items, LengthComparator sc) {
11    String max = ________;
12    for (int i = 1; i < items.length; i++) {
13        String item = ________;
14        int result = ________;
15
16        if (result > 0) {
17            max = item;
18        } // if
19    } // for
20    return max;
21} // max
Solution
 1/**
 2 * Returns the "max" string in the array. In this we
 3 * define "max" as longest.
 4 *
 5 * @param items the array of strings to compare.
 6 * @param sc a reference to an object containing
 7 *     the compare method.
 8 * @return the "max" string.
 9 */
10public static String max(String[] items, LengthComparator sc) {
11    String max = items[0];
12    for (int i = 1; i < items.length; i++) {
13        String item = items[i];
14        int result = sc.compare(item, max);
15        // negative: when item is considered shorter
16        //    equal: when item is considered equal to max
17        // positive: when item is considered greater
18        if (result > 0) {
19            max = item;
20        } // if
21    } // for
22    return max;
23} // max

Now, fill in the blanks below to call the max method on the provided string array:

1String[] strs = new String[] { "aa", "abc", "abcd", "a" };
2
3String max = ________; // call max here
4System.out.println(max); // should be abcd
Solution
String[] strs = new String[] { "aa", "abc", "abcd", "a" };
LengthComparator lc = new LengthComparator();

String max = Utility.max(strs, lc); // call max here
System.out.println(max); // should be abcd
Improving the max method
Activity: Multiple max methods

!includesub chap10-lesson.common.puml!STYLE
!includesub chap10-lesson.common.puml!STRING_COMPARATOR
!includesub chap10-lesson.common.puml!STRING_COMPARATOR_CLASSES

StringComparator <|..down.. LengthComparator : "implements"
StringComparator <|..down.. VowelCountComparator : "implements"

public static String max(String[] items, LengthComparator sc) {
      String max = items[0];
      for (int i = 1; i < items.length; i++) {
          String item = items[i];
          int result = sc.compare(item, max);
          if (result > 0) {
              max = item;
          } // if
      } // for
      return max;
} // max
public static String max(String[] items, VowelCountComparator sc) {
      String max = items[0];
      for (int i = 1; i < items.length; i++) {
          String item = items[i];
          int result = sc.compare(item, max);
          if (result > 0) {
              max = item;
          } // if
      } // for
      return max;
} // max

Can we write max once and in a way that allows us to plug in different StringComparator implementations, regardless of whether that implementation is a LengthComparator, VowelCountComparator, or something else?

With your group, write the method signature for a version of the max method that works with LengthComparator, VowelCountComparator, and other kinds of StringComparator implementations. You only need to write down the signature.

Sample Solution: One max method
public static String max(String[] items, StringComparator sc) { ...
Discussion: Using our max method
Listing 104 Assume this is in class Driver
public static String max(String[] items, StringComparator sc) { ...

How would we call our new max method in the blanks below?

String[] items = ...; // Assume the array is properly instantiated and holds multiple strings

String longest = _____________; // Call max here

String mostVowels = ____________; // Call max here
Sample Solution
String[] items = ...;

StringComparator byLength = new LengthComparator();
String longest = Driver.max(items, byLength);

StringComparator byVowelCount = new VowelCountComparator();
String mostVowels = Driver.max(items, byVowelCount);

It's "plug and play" since we used an interface!

Discussion: One more update to max (1)

What is a limitation of the current max method? How could we improve it?

public static String max(String[] items, StringComparator comp) {
Hint

Can we write one method that works for all reference types?

public static String max(String[] items, StringComparator comp) { ...
public static Song max(Song[] items, SongComparator comp) { ...
public static Person max(Person[] items, PersonComparator comp) { ...
public static Dog max(Dog[] items, DogComparator comp) { ...
Example 3: Comparator<T>
Activity: A better max method
public static String max(String[] items, StringComparator comp) { ...
public static Song max(Song[] items, SongComparator comp) { ...
public static Person max(Person[] items, PersonComparator comp) { ...
public static Dog max(Dog[] items, DogComparator comp) { ...

With your group, write the method signature for a version of the max method that works for any reference type. You only need to write down the signature.

Sample Solution: A better max method
public static <T> T max(T[] items, Comparator<T> comp) { ...

This assumes that all the objects referred to by comp are adjusted so that they implement Comparator<T>, an interface that you were exposed to when you did the reading for your latest project.

Instead of separate interfaces, each class can implement Comparator<T>, since the signature for each of their compare method is the same. To bring everything together, we make the following adjustments:

  • implements StringComparator to implements Comparator<String>

  • implements SongComparator to implements Comparator<Song>

  • implements PersonComparator to implements Comparator<Person>

  • implements DogComparator to implements Comparator<Dog>

Activity: Write the generic max method
public static <T> T max(T[] items, Comparator<T> comp) {
   T maxItem = ________;                       // Line 1
   for (int i = 1; i < items.length; i++) {
       T item = ________;                      // Line 2
       int result = _______;                   // Line 3
       if (result > 0) {
           maxItem = item;
       } // if
   } // for
   return maxItem;
} // max

Write the code for the max method on your exit ticket (and in your own notes). Be sure to include the method signature. Do not include a Javadoc comment, unless you need it for your notes.

Sample Solution: The generic max method
public static <T> T max(T[] items, Comparator<T> comp) {
    T maxItem = items[0];
    for (int i = 1; i < items.length; i++) {
        T item = items[i];
        int result = comp.compare(item, maxItem);
        if (result > 0) {
            maxItem = item;
        } // if
    } // for
    return maxItem;
} // max

Another version for linked lists:

public static <T> T max(Node<T> head, Comparator<T> comp) {
    T maxItem = head.getItem();
    while (head != null) {
        T item = head.getItem();
        int result = comp.compare(item, maxItem);
        if (result > 0) {
            maxItem = item;
        } // if
        head = head.getNext();
    } // while
    return maxItem;
} // max
Part 2: Functional Interfaces
Examples: Implementing the Comparator<T> interface

Here, we show how we can implement Comparator<T> using a regular class and how we transition from a regular class to an anonymous class to a lambda expression.

Example: Person by age() (ascending)
public class PersonAgeComparator implements Comparator<Person> {

    @Override
    public int compare(Person o1, Person o2) {
        return o1.age() - o2.age();
    } // compare

} // PersonAgeComparator
public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byAge = new PersonAgeComparator();

    Person oldest = Driver.<Person>max(persons, byAge);

    ...

} // main
public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byAge = new Comparator<Person>() {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.age() - o2.age();
        } // compare
    };

    Person oldest = Driver.<Person>max(persons, byAge);

    ...

} // main

Warning

Although the code above will compile, use of anonymous class syntax is highly discouraged, because it includes almost the same amount of boilerplate as the regular class version.

public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byAge = public int compare(Person o1, Person o2) {
        return o1.age() - o2.age();
    };

    Person oldest = Driver.<Person>max(persons, byAge);

    ...

} // main

Warning

The code above will NOT compile, but writing it like that makes it clear that we are still overriding the compare method in whatever object byAge ends up referring to…

public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byAge = (Person o1, Person o2) -> {
        return o1.age() - o2.age();
    };

    Person oldest = Driver.<Person>max(persons, byAge);

    ...

} // main

Important

A lambda expression can only be assigned to a variable with a functional interface as its type. Why?

Warning

There are shorter ways to write the lambda expression shown above, but the version shown above looks more like a method, and everyone in the room has experience writing methods. Don't try to move too fast. Once you are comfortable writing lambda expresisons like the one above, we will start using the shorter syntax.

Example: Person by name() (ascending)
public class PersonNameComparator implements Comparator<Person> {

    @Override
    public int compare(Person o1, Person o2) {
        return o1.name().compareTo(o2.name());
    } // compare

} // PersonNameComparator
public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byName = new PersonNameComparator();

    Person last = Driver.<Person>max(persons, byName);

    ...

} // main
public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byName = new Comparator<Person>() {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.name().compareTo(o2.name());
        } // compare
    };

    Person last = Driver.<Person>max(persons, byName);

    ...

} // main

Warning

Although the code above will compile, use of anonymous class syntax is highly discouraged, because it includes almost the same amount of boilerplate as the regular class version.

public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byName = public int compare(Person o1, Person o2) {
        return o1.name().compareTo(o2.name());
    };

    Person last = Driver.<Person>max(persons, byName);

    ...

} // main

Warning

The code above will NOT compile, but writing it like that makes it clear that we are still overriding the compare method in whatever object byName ends up referring to…

public static void main(String[] args) {

    Person[] persons = ...;

    Comparator<Person> byName = (Person o1, Person o2) -> {
        return o1.name().compareTo(o2.name());
    };

    Person last = Driver.<Person>max(persons, byName);

    ...

} // main

Important

A lambda expression can only be assigned to a variable with a functional interface as its type. Why?

Warning

There are shorter ways to write the lambda expression shown above, but the version shown above looks more like a method, and everyone in the room has experience writing methods. Don't try to move too fast. Once you are comfortable writing lambda expresisons like the one above, we will start using the shorter syntax.

Example: Movie by various (ascending; lambda)
public static void main(String[] args) {

    Movie[] movies = ...;

    Comparator<Movie> byYear = (Movie o1, Movie o2) -> {
        return o1.year().compareTo(o2.year());
    };

    Comparator<Movie> byRating = (Movie o1, Movie o2) -> {
        return o1.rating() - o2.rating();
    };

    Comparator<Movie> byRuntime = (Movie o1, Movie o2) -> {
        return o1.runtime().compareTo(o2.runtime());
    };

    Comparator<Movie> byCastSize = (Movie o1, Movie o2) -> {
        return o1.cast().size() - o2.cast().size();
    };

    Comparator<Movie> byTaglineLength = (Movie o1, Movie o2) -> {
        return o1.tagline().length() - o2.tagline().length();
    };

    ...

} // main
Discussion: How this relates to P3

The two versions of the max method we have developed over the past two chapters demonstrate the two ways you are expected to compare items for the Urgency Queue Project (P3). Take a look at the UML diagram for P3 and discuss how using a Comparator and a bounded type parameter relates. Make sure you focus on the differences between the child classes and understand how they work.

Activity: Exploring the Predicate<T> interface

To get started: search for "Java 17 Predicate" to pull up the Java API page for java.util.function.Predicate<T>

Predicate<String> longerThan10 = (String t) -> {
   return t.length() > 10;
};

Answer the following questions about the code above on your exit ticket (and in your notes):

  1. Which abstract method declared in Predicate is being implemented by the lambda expression above? You may need to check the Java API to get the exact name.

  2. What method can we call using longerThan10 as the calling object?

  3. Using longerThan10, write code that prints whether or not "Hello, World" contains more than 10 characters.

Sample Solution: Exploring the Predicate<T> interface
Predicate<String> longerThan10 = (String t) -> {
   return t.length() > 10;
};
System.out.println(longerThan10.test("hello"));        // ?
System.out.println(longerThan10.test("hello, world")); // ?
Discussion: The <T>printlnMatches method
/**
 * Prints the elements of the array that pass the test specified by the given
 * predicate. Each element will be printed on its own
 * line.
 *
 * @param <T> the type of the array elements
 * @param items the specified array
 * @param condition the specified predicate
 */
public static <T> void printlnMatches(T[] items, Predicate<T> condition) {
   throw new UnsupportedOperationException("not yet implemented");
} // printlnMatches

Suppose we want to implement the printlnMatches method above.

  • Is the method printlnMatches a generic method? How can you tell?

  • What method can we call using condition as the calling object?

Solution
  • Yes! It has a type parameter, T

  • condition is a reference to an object of a class that implements Predicate<T>. Therefore, it is guaranteed to have a test method.

Activity: Writing the <T>printlnMatches method
/**
 * Prints the elements of the array that pass the test specified by the given
 * predicate. Each element will be printed on its own
 * line.
 *
 * @param <T> the type of the array elements
 * @param items the specified array
 * @param condition the specified predicate
 */
public static <T> void printlnMatches(T[] items, Predicate<T> condition) {
   throw new UnsupportedOperationException("not yet implemented");
} // printlnMatches

Write the code for the printlnMatches method on your exit ticket (and in your own notes). Be sure to include the method signature. Do not include a Javadoc comment, unless you need it for your notes.

Sample Solution: Writing the <T>printlnMatches method
public static <T> void printlnMatches(T[] items, Predicate<T> condition) {
    for (int i = 0; i < items.length; i++) {
        T item = items[i];
        if (condition.test(item)) {
            System.out.println(item);
        } // if
    } // for
} // printlnMatches
Activity: Calling the <T>printlnMatches method

Finish the code below so that printlnMatches prints all strings containing the letter a:

String[] myStrings = new String[] {
   "CSCI", "1302", "is", "an", "awesome", "course!"
};

Predicate<String> containsA = _____________;         // Blank 1

Driver.<String>printlnMatches(_________, _________); // Blanks 2 and 3
Sample Solution: Calling the <T>printlnMatches method
String[] myStrings = new String[] {
   "CSCI", "1302", "is", "an", "awesome", "course!"
};

Predicate<String> containsA = (String t) -> {
    return t.contains("a");
};

Driver.<String>printlnMatches(myStrings, containsA);
Part 3: Hands-on Activity
Download starter code

Download the starter code for this chapter and places it into a subdirectory called cs1302-gen-methods:

sh -c "$(curl -fsSL https://cs1302book.com/_bundle/cs1302-gen-methods.sh)"
- downloading cs1302-gen-methods bundle...
- verifying integrity of downloaded files using sha256sum...
- extracting downloaded archive...
- removing intermediate files...
subdirectory cs1302-gen-methods successfully created
Activity: Fill in the blank: <T>print

Finish the code below so that the print method functions according to the specification:

/**
 * Prints all of the provided list. Each element will be printed
 * on its own line.
 *
 * @param <T> the type of the list elements
 * @param list the specified list
 */
public static <T> void print(List<T> list) {
    for (_____ elem: list) {              // Blank 1
        System.out.println(_________);    // Blank 2
    } // for
} // print
Sample Solution: <T>print
1public static <T> void print(List<T> list) {
2    for (T elem: list) {              // Blank 1
3        System.out.println(elem);     // Blank 2
4    } // for
5} // print
Activity: Fill in the blanks: <T>printlnMatches

Finish the code below so that print functions according to the specifications:

 1/**
 2 * Prints the elements of the list that pass the test specified by the given
 3 * predicate. Each element will be printed on its own line.
 4 *
 5 * @param <T> the type of the list elements
 6 * @param list the specified list
 7 * @param condition the specified predicate
 8 */
 9 public static <T> void printlnMatches(List<T> list, Predicate<T> condition) {
10     for (_____ elem: list) {               // Blank 1
11         if (_______________) {             // Blank 2
12             System.out.println(______);    // Blank 3
13         } // if
14     } // for
15 } // printlnMatches
Sample Solution: <T>printlnMatches
 1/**
 2 * Prints the elements of the list that pass the test specified by the given
 3 * predicate. Each element will be printed on its own line.
 4 *
 5 * @param <T> the type of the list elements
 6 * @param list the specified list
 7 * @param condition the specified predicate
 8 */
 9 public static <T> void printlnMatches(List<T> list, Predicate<T> condition) {
10     for (T elem: list) {
11         if (condition.test(elem)) {
12             System.out.println(elem);
13         } // if
14     } // for
15 } // printMatches
Calling <T>printlnMatches

Use the method printlnMatches to print the names of all movies made after 1999.

Note

You may need to review the Movie class given as part of the Jar file for Project 3.

public static <T> void printlnMatches(List<T> list, Predicate<T> condition) {
    for (T elem: list) {
        if (condition.test(elem)) {
            System.out.println(elem);
        } // if
    } // for
} // printlnMatches
Movie[] movies = ...;

// setup a predicate (an object containing a test method)

// call printlnMatches
Movie[] movies = ...;

Predicate<Movie> after1999 = ____;

Driver.<Movie>printlnMatches(____, ____);
Movie[] movies = ...;

Predicate<Movie> after1999 = (Movie t) -> {
    Year year1999 = Year.of(1999);
    return t.year().after(year1999);
};

Driver.<Movie>printlnMatches(____, ____);
Movie[] movies = ...;

Predicate<Movie> after1999 = (Movie t) -> {
    Year year1999 = Year.of(1999);
    return t.year().after(year1999);
};

Driver.<Movie>printlnMatches(movies, after1999);
Code Update

Update the code in ModelTests to print all movies made after 1999.

Run the code. Looks a bit messy. Let's update the query.

Discussion: Updated Query - Part 1

What if we only want to print the name of all of our movies?

We already have a print method. Can we adjust it to print only the name out of each movie?

public static <T> void print(List<T> list) {
    for (T elem: list) {
        System.out.println(elem + "\n");
    } // for
} // print
Discussion: Updated Query

What if we want to print just the name of the movies made after 1999 (or the name of their director, for example) instead of calling the toString method to print all of the information for the movies?

Need a new method with another functional interface. We will call it printlnMappedMatches.

Discussion: The <T>printlnMappedMatches method

To get started: search for "Java 17 Function" to pull up the Java API page for java.util.function.Function<T, R>

/**
 * Prints the elements of the array that pas the test
 * specified by the given predicate using a string
 * mapper. Each string-mapped element will be printed on
 * its own line.
 *
 * @param <T> the type of the list elements
 * @param list the specified list
 * @param condition the specified predicate
 * @param mapper the specified string mapper
 */
public static <T> void printlnMappedMatches(
    List<T> list, Predicate<T> condition, Function<T, String> mapper
) {
    throw new UnsupportedOperationException("not yet implemented");
} // printlnMappedMatches

Suppose we want to implement the printlnMappedMatches method above.

  • Is the method printlnMappedMatches a generic method? How can you tell?

  • What do you think is the purpose of mapper?

  • What method can we call using mapper as the calling object?

Solution
  • Yes! It has a generic type parameter T.

  • It changes the data type of an element. In this case, it changes the type from T to String. That will be perfect for converting a Movie to its name (String).

  • The single, abstract method in Function is apply.

Activity: Writing the <T>printlnMappedMatches method
/**
 * Prints the elements of the array that pass the test
 * specified by the given predicate using a string
 * mapper. Each string-mapped element will be printed on
 * its own line.
 *
 * @param <T> the type of the list elements
 * @param list the specified list
 * @param condition the specified predicate
 * @param mapper the specified string mapper
 */
public static <T> void printlnMappedMatches(
    List<T> list, Predicate<T> condition, Function<T, String> mapper
) {
    throw new UnsupportedOperationException("not yet implemented");
} // printlnMappedMatches

Write the code for the printlnMappedMatches method on your exit ticket (and in your own notes). Be sure to include the method signature. Do not include a Javadoc comment, unless you need it for your notes.

Sample Solution: Writing the <T>printlnMappedMatches method
public static <T> void printlnMappedMatches(
    List<T> list, Predicate<T> condition, Function<T, String> mapper
) {
    for (int i = 0; i < list.size(); i++) {
        T item = list.get(i);
        if (condition.test(item)) {
            String mappedItem = mapper.apply(item);
            System.out.println(mappedItem);
        } // if
    } // for
} // printlnMappedMatches
Activity: Calling the <T>printlnMappedMatches method
public static <T> void printlnMappedMatches(
    List<T> list, Predicate<T> condition, Function<T, String> mapper
) { ...

Use the method printlnMappedMatches to print the names of all movies made after 1999.

List<Movie> movies = ...;

// setup a predicate

// setup a mapper function

// call printlnMappedMatches
List<Movie> movies = ...;

Predicate<Movie> after1999 = ____;

Function<Movie, String> asTitle = ____;

Driver.<Movie>printlnMappedMatches(____, ____, ____);
List<Movie> movies = ...;

Predicate<Movie> after1999 = (Movie t) -> {
    Year year1999 = Year.of(1999);
    return t.year().after(year1999);
};

Function<Movie, String> asTitle = ____;

Driver.<Movie>printlnMappedMatches(____, ____, ____);
List<Movie> movies = ...;

Predicate<Movie> after1999 = (Movie t) -> {
    Year year1999 = Year.of(1999);
    return t.year().after(year1999);
};

Function<Movie, String> asTitle = (Movie t) -> {
    return t.title();
};

Driver.<Movie>printlnMappedMatches(____, ____, ____);
List<Movie> movies = ...;

Predicate<Movie> after1999 = (Movie t) -> {
    Year year1999 = Year.of(1999);
    return t.year().after(year1999);
};

Function<Movie, String> asTitle = (Movie t) -> {
    return t.title();
};

Driver.<Movie>printlnMappedMatches(movies, after1999, asTitle);