1. Reading

  • Horstmann, Ed 11, chapter 6, sections 6.1-6.3 Interfaces, Lambda Expressions, and Inner Classes

Before we delve into lambda expressions and what we can use them for, we have to revise what an Interface is. Because lambda expressions are a way of concisely writing what is sometimes called an anonymous method, we need to understand what it does and what it is good for. We also need lambda expressions in modern testing frameworks, like JUnit 5 and AssertJ.

2. Parameterized tests

You will often see that test methods look a lot like each other. As an example: In the fraction exercise, in most test methods you have two inputs and on or two results, then an operation is done followed by some assertion, often of the same kind. This quickly leads to the habit of copy and waste programming. Many errors are introduced this way: You copy the original, tweak the copy a bit and you are done. Then you forget one required tweak, because they are easy to miss, but you do not notice it until too late.

Avoid copy and waste at almost all times. It is not a good programming style. If you see it in your code, refactor it to remove the copies and in the end have less of it. Worse: When the original has a flaw, you are just multiplying the number of flaws in your code. This observation applies to test code just as well.
CODE THAT IS NOT THERE CAN’T BE WRONG.

2.1. Parameterized test, Junit 5 style

Below you see an example on how you can condense the toString() tests of fraction from 5 very similar test methods into 5 strings containing the test data and 1 (say one) test method.

Complete fraction test class with parameterized test, using an in-code csv source
package fraction;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.provider.CsvSource;

public class FractionCSVSourceTest {

    @ParameterizedTest
  @CsvSource(
             {
               //"message,      expected,     n,   d", (1)
               "two thirds,      '(2/3)',     2,   3", (2)
               "whole number,    '2',         2,   1",
               "one third,       '(1/3)',    -3,  -9",
               "minus two fifths, '(-2/5)',  12, -30",
               "-one + two fifths, '-1(2/5)', 35, -25"
            } )
    void fractionOps( ArgumentsAccessor args ) {
        String message = args.getString( 0 ); (3)
        String expected = args.getString( 1 );
        int n = args.getInteger( 2 ); (4)
        int d = args.getInteger( 3 );
        Fraction f = new Fraction( n, d );
        assertThat( f.toString() )
                .as( message )
                .isEqualTo( expected );
    }
}
1 Adding a comment is always a good idea. You may also want your values aligned for improved readability.
2 Parameters are separated by commas, which maybe in the test values. You can demarcate Strings with single quotes inside the csv record string. If you need another separator instead of comma, you can specify that too, see CsvSource API .
3 Getting the parameters from the csv records or lines is a bit reminiscent to using a Scanner object.
4 Same here, pick up integer values. Same goes for other numeric types.

For more details see Junit 5 parameterized tests .

2.2. Lookup lambda in a map.

Lambdas are special. The have no name and not a very useful toString, which you cannot even overwrite. But you can do the reverse: translate a string into a lambda, by using a map. You can then use simple names (strings), that can be put in a csv record. You could, if you wanted, even use the lambda expression i String form as a key.

   Map.of("(a,b)->a+b", (a,b)->a+b)

Note that this does not really help readability and might be a bit brittle is you are not consistent in your usage of white space.

Map of string to lambda
    final Map<String, BiFunction<Fraction, Fraction, Fraction>> ops = (1)
      Map.of(
              "times", ( f1, f2 ) -> f1.times( f2 ),
              "plus", ( f1, f2 ) -> f1.plus( f2 ),
              "flip", ( f1, f2 ) -> f1.flip(), (2)
              "minus", ( f1, f2 ) -> f1.minus( f2 ),
              "divideBy", ( f1, f2 ) -> f1.divideBy( f2 )
      );
1 Not that we use a BiFunction<T,U,R>, with T, U and R all of the same type: Fraction. This is legal.
2 f2 is not used in the right hand side of the arrow. This is legal too.
Using lambda names in test data
    @CsvSource(
             {
                "'one half times one third is 1 sixth', 'times', '(1/6)',1,2,1,3", (1)
                "'one thirds plus two thirds is 1'    , 'plus',      '1',1,3,2,3",
                "'flip',                                'flip',      '3',1,3,1,3", (2)
                "'one half minus two thirds is',       'minus', '(-1/6)',1,2,2,3"
            } )
1 The operation name is the second value in the csv record, here times. Note that you can quote strings, but that is not required.
2 In the flip operation, the second fraction is ignored, so any legal value is allowed. Here we use the same values for both fractions.
Test method using the test data
void fractionOps( ArgumentsAccessor args ) {
        // read test values
        String message = args.getString( 0 );
        String opName = args.getString( 1 );
        String expected = args.getString( 2 );
        int a = args.getInteger( 3 ); (1)
        int b = args.getInteger( 4 );
        int c = args.getInteger( 5 );
        int d = args.getInteger( 6 );
        Fraction f1 = frac( a, b ); (2)
        Fraction f2 = frac( c, d );
        System.out.println( "message = " + message );

        BiFunction<Fraction, Fraction, Fraction> op = ops.get( opName ); (3)
        Fraction result = op.apply( f1, f2 ); (4)
        assertThat( result.toString() )
                .as( message )
                .isEqualTo( expected );
    }
1 The fraction parameters a,b,c, and d are captured from the csvrecord. This makes the test method a tad longer, but also more understandable.
2 The fraction instances are created from a,b,c, and d.
3 The operation (op) is looked up in the map
4 Apply the operation, or rather the function and capture the result.

You can apply the same trick of looking up with enums too, even easier, because the enum itself can translate from String to value, as long as the spelling is exact.

Study the examples above, they might give you inspiration with the exercises coming up and will score you points during the exam.

3. Slides:

4. Exercises

Think test driven! So write tests FIRST, and then code. Testing is ensuring that the code is usable. To make code testable is also a design exercise: you need to make decisions about the design of your program when you write your tests. If you cannot test it, you cannot use it.

Exercise 1: Comparing students

Comparator as lambda

If you study the Java doc of the Comparator interface , you will see there is only one abstract method.[1] This makes this interface a candidate for a functional interface, and indeed it is.

A functional interface is an interface that has only one abstract method in it. Some of them you already know. For example, the interfaces Runnable or ActionListener are functional interfaces.

In this exercise, you will write a series of `Comparator, each handling a different comparable aspect.

The business side of the project has only one class, a Student class. The student class is given, so you can focus on the lambda expressions you are about to write.

A student instance has 5 fields, the rest is most boring, only getters, toString and a helper method that is not relevant for this exercise.

Student fields.
enum Gender {
        F, M, U
    }

    private final long sid;
    private final String firstName;
    private final String lastName;
    private final Gender gender;
    private final LocalDate dob;

    // rest is boring
}

Use the following template to create your comparators, written as lambda expressions:

comparator template
Comparator<Student> firstNameComparator = (s1,s2) -> ....

The tests in the test class are documented and you can read the actual tasks there. There is NO work to be done on the Student class. Do all your experiments on the test side of the project.

Exercise 2: In the pub (with an enum)

In the pub Enum

For this exercise, you are again expanding on the exercise from last week. Amend the pub simulation by turning the volume of a drink into an enum. Work test driven! for the parts that you add.

This is also a refactoring exercise, so you start out by copying the source and test classes for the week 1 in the pub exercise.

You’ll find the project template in your repository. This time, the project is mostly empty packages. Copy over your code from last week to continue here. Remember to write the tests to test the enum class.
After that the project should have the same package structure as the original. Keep you solution for week 1 intact and only modify the code in this weeks project.

  1. Beer can be either of size SMALL (0.2L), LARGE (1.0L) or PINT (0.57L)

  2. The Enum should contain exactly 3 values (small, large, pint), not more not less.

Exercise 3: Enum Calculator
  • Enum Calculator

In this exercise you build a calculator that has its operations expressed in an enum called Operator.+ The enum values in Operation and ADD, SUBTRACT, MULTIPLY, DIVIDE, and POWER with the usual meaning.
Each enum value has a symbol of type String "+" for ADD, etc and "**" for the Power operation.+

Test driven develop the Operator enum. The main class is given.

The functional requirements are:

  • The Operator class has a constructor that takes 2 parameters:

    1. a symbol string and

    2. a IntBinaryOperator functional interface, which defines the shape of a lambda expression.

  • You should be able to lookup the Operator by its symbol.

  • The Operator has a compute function int compute(int a, int b), which does the actual work.

The main class, Calculator reads the input as lines and splits the input by white space, to make parsing easier. So you will have to give the input values and operator separated by white space. See below.

The non-functional requirements regarding the tests are that you use a JUnit 5 @ParameterizedTest.

A csv source could look like this. Abbreviated.
   @ParameterizedTest
   @CsvSource( {
        "add,+," + ( 2 + 3 ) + ", 2, 3 ",
        "subtract,-," + ( 2 - 3 ) + ", 2, 3 ",
        "multiply,*," + ( 2 * 3 ) + ", 2, 3 ",
        "divide,/," + ( 2 / 3 ) + ", 2, 3 ",
        "power,**," + ( 2 * 2 * 2 ) + ", 2, 3 ",
   } )
    public void testOperator( ArgumentsAccessor args ) {
      // test code left out
    }

The app that uses the Operator class is shown below.

Calculator at work
class Calculator {
    public static void main( String[] args ) {
        Scanner scanner = new Scanner( System.in );
        System.out.print( "calc: " );
        String line = scanner.nextLine();
        String[] tokens = line.split( "\\s+" );
        while ( tokens.length >= 3 ) {
            System.out.println( line + " = " + compute( tokens ) );
            System.out.print( "calc: " );
            line = scanner.nextLine();
            tokens = line.split( "\\s+" );
        }
    }

    /**
     * Compute takes three tokens, a, operatorSymbol, and  b as in "5", "+"," "6".
     * @param tokens to be evaluated.
     */
    static int compute( String... tokens ) {
        if ( tokens.length < 3 ) {
            return 0;
        }
        int a = Integer.parseInt( tokens[ 0 ] );
        int b = Integer.parseInt( tokens[ 2 ] );
        String symbol = tokens[ 1 ];

        return Operator.get( symbol ).compute( a, b ); (1)
    }
}
1 lookup operation and apply it to the values.
sample output
calc: 3 ** 8
3 ** 8 = 6561
calc: 4 + 12
4 + 12 = 16

4.1. Test run reports Week 2


1. equals(Object other) does not count, it is already defined in Object