Lambda Expressions
Introduction to Lambda Expressions
In this part of the course, we deal with the manipulation of objects for the purpose of data analysis. As an example, we use the domain of students, courses and course results obtained by the students. To start out, we first define a class that can help us model course data.
public class Course {
private final long courseNumber;
private final int courseYear;
private final String courseName;
private final String teacher;
private final double ects;
public Course(long nr, int year, String name, String teacher, double ects) {
super();
this.courseNumber = nr;
this.courseYear = year;
this.courseName = name;
this.teacher = teacher;
this.ects = ects;
}
public long getCourseNumber() {
return courseNumber;
}
public int getCourseYear() {
return courseYear;
}
public String getCourseName() {
return courseName;
}
public double getEcts() {
return ects;
}
public String getTeacher() {
return teacher;
}
// And default overridden versions of hashCode(), equals() and toString()
}
As you can see, four types of data are stored in a Course
object: a number for the course, the year in which the course starts, the name of the course, the teacher of the course and the number of ects of this course. There are getter methods for these properties, and we also assume that we used our IDE to generate overridden versions of hashCode()
, equals()
and toString()
.
As a result, the code
Course c = new Course(22012, 2019, "Programming", "Paul Bouman", 4);
System.out.println(c);
prints the following:
Comparators Revisited
When we have a list of course objects, there could be different ways in which to sort them. For example, we could sort the courses based in an oldest course first order.
Alternatively, we could sort the courses based on the number of ects earned upon completion. For the different ways in which we can sort courses, we can define separate implementations of the Comparator
interface, which requires us to implement the following method:
public interface Comparator<T> {
public int compare(T left, T right);
}
that should return a negative int
value if left
< right
, return a positive int
value if left
> right
and zero if and only if left
= right
.
If we intend to sort a List<Course>
by the alphabetical order of the names of the teachers of the courses, we could implement the Comparator
as a static class within the Course
-class, and define a sortCourses
method as follows:
public class Course {
// other contents omitted
public static class TeacherComparator implements Comparator<Course> {
@Override
public int compare(Course o1, Course o2) {
return o1.getTeacher().compareTo(o2.getTeacher());
}
}
public static void sortCourses(List<Course> courses) {
Collections.sort(courses, new TeacherComparator());
}
}
By defining a static class
within another class, we avoid the need to put the TeacherComparator
in a separate file. But still, there is quite a bit of code necessary to express that we want to order courses based on the teacher. The essence of what we want to express is the following short expression: o1.getTeacher().compareTo(o2.getTeacher())
.
However, to be able to use this expression to sort Course
objects, we need to define both a class header for the TeacherComparator
class, and the method header of the compare
method. Programmers sometimes refer to this kind of code as boilerplate code: we need to write it to let the compiler check that all our types are safe, but does not express what we want to do.
In this case, it is possible to use a feature of Java called an anonymous class. This is a class that can be defined within a method. Unfortunately, it still looks rather cumbersome:
public static void sortCourses(List<Course> courses) {
Comparator<Course> comp;
comp = new Comparator<Course>() {
@Override
public int compare(Course o1, Course o2) {
return o1.getTeacher().compareTo(o2.getTeacher());
}
};
Collections.sort(courses, comp);
}
In this course, we advise against using anonymous classes. When they are used, the code is often hard to read and understand. For example, it is rather confusing that it results in a method definition within a method definition. The number of nested curly braces also blows up fast, making it complicated to grasp the structure of the program.
Prior to Java 8, the anonymous class was the shortest way to transform our teacher comparison expression into a Comparator
object. Fortunately, Java 8 introduced a feature called lambda expressions, that provides a syntax that is more concise and easier to read than the anonymous class.
Lambda expressions can only be used to create an object that implements an interface with a single unimplemented method. An interface with only a single unimplemented method is called a functional interface. This term is based on the concept of a mathematical function.
The Comparator
interface contains only a single unimplemented method:
compare
. For our example, we can turn our expression
o1.getTeacher().compareTo(o2.getTeacher())
into a Comparator
object
as follows:
Comparator<Course> comp = (Course o1, Course o2) -> {
return o1.getTeacher().compareTo(o2.getTeacher());
};
A lambda expression can be recognized by the arrow, written as ->
. Before this arrow, parameter variables are defined that will be available inside the body of the lambda expression. After the arrow, we have a block or expression, that contains regular code.
It is required that the types and the number of the parameter variables match those of the single method of the functional interface.
For a Comparator<Course>
, the compare
method expects two Course
objects as input, which matches the arguments of our lambda expression. The type of the code, after the arrow, must match the return type of the functional interface method.
Since the return type of the compare
method is int
, we are required to return an int
in our case. Fortunately, the compareTo
method produces an int
, and so the compiler is happy that our lambda expression matches the compare
method.
Since the parameter and return types of the lambda expressions are required to match the parameter and return types of the functional interface, the compiler often does not need the explicit input types of the lambda expression. It can use type inference (i.e. automatic detection of types used) to determine what the types of the lambda expression’s inputs will be. As a result, we can shorten our example:
Comparator<Course> comp = (o1,o2) -> {
return o1.getTeacher().compareTo(o2.getTeacher());
};
There is another useful trick we can use here. Notice that the only code within the lambda is a single return statement. If only a single expression is returned, we are allowed to omit the curly braces and return statement and only write the expression. This provides an extremely concise syntax to create our Comparator
:
Comparator<Course> comp = (o1,o2) -> o1.getTeacher().compareTo(o2.getTeacher());
While this is great for a Comparator
that compares a single attribute of our Course
object, it is not sufficient for a more complex Comparator
object. Take, for example, the order in which we first compare the teacher. If the teachers are equal, we compare the course year. If the course years are also equal, we finally compare the names of the courses in alphabetical order. In that case, we should still use a more verbose lambda expression with explicit return statements.
Comparator<Course> comp = (o1, o2) -> {
if (!o1.getTeacher().equals(o2.getTeacher())) {
return o1.getTeacher().compareTo(o2.getTeacher());
}
if (o1.getCourseYear() != o2.getCourseYear()) {
return o1.getCourseYear() - o2.getCourseYear();
}
return o1.getCourseName().compareTo(o2.getCourseName());
};
If we can only use lambda expressions, this is the best we can. But Java introduced more features that make it possible to define a complex comparator in a much more compact way than the above code. But before we can work on that, we need to study two additional topics: functional interfaces and method references. We do so in the following two sections.
Test your knowledge
In this quiz, you can test your knowledge on the subjects covered in this chapter.
Which of the following pieces of code are a valid lambda expression?
// Piece 1
Comparator<String> strCmp = (String s1, String s2) -> s1.length() - s2.length();
// Piece 2
Comparator<List<Object>> listCmp;
listCmp = (List<Object> lst1, List<Object> lst2) -> lst2.size() - lst1.size();
// Piece 3
Comparator<Course> courseCmp = course1.getCourseName().compareTo(course2.getCourseName());
// Piece 4
Comparator<Integer> modSevenCmp = (Integer i1, Integer i2) -> {
int tmp1 = i1 % 7;
int tmp2 = i2 % 7;
return tmp1-tmp2;
}
What is the difference between the form
Comparator<Course> c = (o1,o2) -> o1.getTeacher().compareTo(o2.getTeacher());
and the following form?
Comparator<Course> c = (Course o1, Course o2) -> o1.getTeacher().compareTo(o2.getTeacher());
Why can the types be omitted from the first form?
Look at the Course
class at the start of this chapter. Write a separate lambda expression
that defines a Comparator<Course>
for each of the following orderings.
Do not use of static methods of the Comparator
class such as Comparator.comparing
yet.
- Order the courses in alphabetic order of their course names
- Order the courses by descending number of ects
- Order the courses by ascending number of ects
- Order the courses by ascending course year
- Order the courses by descending course year