The course is part of this learning path
This training course provides you with a deep dive into generics, type inference, Lambda expressions, and functional interface development.
Learning Objectives
What you'll learn:
- What generics are and when and why you might choose to implement them
- Type inference and the var keyword
- The basic concept of functional programming
- How to write basic Lambda expressions
- Functional interfaces and when to use them
- The key differences between anonymous classes and lambda expressions
Prerequisites
- A basic understanding of the Java programming language
- A basic understanding of software development
- A basic understanding of the software development life cycle
Intended Audience
- Software Engineers interested in advancing their Java skills
- Software Architects interested in using advanced features of Java to design and build both applications and frameworks
- Anyone interested in advanced Java application development and associated tooling
- Anyone interested in understanding the advanced areas and features of the Java SDK
Okay, welcome back. In this lecture, we'll explore the concepts of functional interfaces and lambda expressions. In particular, we'll review the following topics: understanding the basic concept of functional programming, understanding functional interfaces, writing basic lambda expressions and understanding the difference between anonymous classes and lambda expressions. At the conclusion of this lecture, you should be able to perform each of the items above. Take a moment to rate yourself on each of these items on a scale of one through to five. At the conclusion of this lecture, these objectives will be reviewed, you should rate yourself again to see how much benefit you received from this lecture. Functional versus object oriented programming. With functional programming, the developer is focused on creating functions that can be reused throughout the application. A function relies only on the input parameters it receives and avoids the use of state immutable data, calling the same function multiple times with the same parameters will therefore provide the same result every time. Furthermore, a function's sole purpose is to produce some kind of result. This result can be internal within the function, for example, printing something on a screen, but most often it will return a value. Within Object Oriented Programming, the functions, or again in Java speak methods, are used to change the internal state of an object. Calling the same method twice might actually provide a different result because the internal state of the object has changed by the earlier method invocation. Also, in most OO applications, methods on objects do not even provide a result. Invoking a setter method on an object only changes the internal state of the object. Functional Interfaces. Java 8 introduced the concept of functional interfaces. Also sometimes called Single Abstract Method interfaces. Functional interfaces contain at most one abstract method. May contain default methods and may also contain static methods. Instances of the interfaces can be created either using Lambda Expressions, using method references, using constructor references and by using anonymous inner classes. As we have seen in an earlier lecture, Lambda Expressions can be used instead of anonymous inner classes. However, where anonymous inner classes can also be used to implement interfaces that contain multiple methods, Lambda Expressions can only be used when the interface contains at most one abstract method. Keep in mind that methods without a method body have to be declared as abstract since methods in an interface never contain an implementation, they are implicitly abstract. Throughout the JDK you'll find a number of interfaces that only contain a single method, for example, runable, comparable and/or the action listener interface. These types of interfaces are often referred to as Single Abstract Method interfaces. In Java 8 these are called functional interfaces. A functional interface contains, at most, one abstract method. Starting with Java 8 it has also become possible to write default and static methods in an interface. Until Java 8, the only option you had to come up with an implementation of one of these Single Abstract Method interfaces was to implement a class that implements this interface. From now on you can also use Lambda Expressions to accomplish exactly the same task. Additionally, you can also use a reference to a method or a constructor to create an instance of the functional interface. Method and constructor references will be covered later. The functional interface annotation introduced in Java 8 is considered an informative annotation, allowing the compiler to throw errors when the interface is not compliant. The use of the annotation is optional. Any interface that defines a single abstract method is considered to be a functional interface, however it is recommended that all functional interfaces are annotated using this annotation. Since functional interfaces have only one method by definition, lambda expressions can therefore be used to provide the method implementation. We just need to provide method arguments and the business logic. A lambda expression is a powerful and concise shorthand notation for defining interface implementations and is often useful in places where a method invocation is only required once. A lambda expression is an anonymous method, that is a method that comes without a name. It may have parameters and it comes with a method body. The method definition for the lambda expression comes without a name, it has no access modifier, nor return type declaration. Lambda expressions can be seen as just methods without a name, think anonymous methods. Just like normal methods in the Java programming language, lambda expressions have parameters and a method body, they just don't have a name, again, just like anonymous classes, and their syntax is slightly different from the methods you have written previously. Every lambda expression consists of two parts. One, the parameters that are used within the expression, and two, the method body of the lambda expression. The parameters of the lambda expression are variables, local to the method body of the expression, similar to method parameters within a regular method. The method body defines the function that is to be performed when this expression is interpreted. The lambda expression parameters and method body are separated by the dash angle bracket notation. Now, before we drill further into the details of lambda expressions, let's step back and review how the Java language itself evolved over time and led to the introduction of lambda expressions. In doing so, you will begin to understand some of the motivations as to why lambda expressions now exist in the language and some of the problems they solve. For starters, a common requirement when developing with Java, or any other software language for that matter, is the need to implement one or more utility methods. A utility method is one that takes some sort of data as an input and performs some sort of processing on it. In Java they were often implemented as static methods on a container class. Taking this approach prevented the need of having to instantiate the class first before using the method, but more importantly, it implied through convention that the utility method would have no side effects. The static method implementation would take as one of its parameters a functional interface to abstract the required processing. In the example shown here, the functional interface defines an apply method, which takes in a string and applies some form of processing on it and returns a resulting string. Following on from the previous slide, different implementations can now be created for the same functional interface. In this slide an implementation of the previously defined functional interface is provided in which it simply uppercases the input string. In our first version of the display text method, we can now simply invoke our StringUtil static print method by passing in an instance of the ToUpperCaseFunction class, which itself provides an implementation of the previously defined functional interface. That which processes and converts the input string to uppercase. Our next iteration of the display text method involves using an inner class. An inner class provides us with an ability to move and embed the class definition containing the print utility method, directly within the class where the print utility method is actually used and invoked. In this type of design the embedded class is called an inner class. An inner class is only known to its surrounding class or method and, as such, has the following benefits: the inner class can access the private data of the outer class, the inner class can be hidden from other classes within the same package, the inner class increases encapsulation, and it can lead to more readable and more maintainable code. Taking our previous inner class design, used within the display text method, we can again refactor it by using an anonymous inner class. When we do this it becomes an inner class without a name and for which only a single object or instantiation of it is created. The syntax of an anonymous class expression is much like the invocation of a constructor, except that there is a class definition contained in a block of code. Taking this approach condenses the code, again making it simpler to read and understand. So now we are at the stage where we can begin to understand and appreciate the motivations as to why lambda expressions were introduced into the Java language syntax. When we now look a little closer at our anonymous inner class, defined within the top box, and compare it back to the static print utility method in the StringUtil class, in the bottom box, we can see that we have some redundancy in the language, in the fact that we are having to redeclare the interface. What the Java language designers decided to do was to remove this need and, instead, leverage the compiler to infer the type from the parameter type of the method that we are trying to invoke. Following on from the previous slide, the same language redundancy was also realized for the method name and type, in this case the apply method, which returns a string type. So again, the Java language designers decided that the requirement to have to redeclare the method name and return type could also be dropped and, instead, leverage the compiler at compile time to infer this information from the functional interface, which we know can only contain a single method. So by acknowledging each of the language redundancies within the previous slides and for which are now replaced by leveraging the compiler at compile time to infer the required details, we are able to reduce the previously defined anonymous inner class into the resulting lambda expression. Again the key point here is that the compiler is being used to provide type inference. As can be seen here, the resulting lambda expression is far more concise, reducing overall code, but as we'll see in the following slides, further coding optimizations can still be applied under certain conditions, to condense the lambda expression even further. As just mentioned, there are several lambda expression optimizations that can be applied. Some only when certain conditions exist, for starters the input parameter types can be inferred by the compiler, meaning that they can be dropped from the lambda expression. The next optimization allows you to drop the surrounding input parameter parentheses when the lambda expression has a single input parameter. Both of these optimizations are applied here in the lambda expression in the bottom box. Next we can condense any lambda expression to a single line, without the need for any curly brackets, if the lamba expression body contains a single statement. In this case, the return type becomes that of the return type of the body expression. The requirement to specify the return key read itself can also be dropped, simplifying it even further. Lambda expressions can define zero or more parameters. As you will soon see, the amount of parameters depends on the interface that lies underneath. Parentheses are used to enclose a comma-separated list of parameters. When no parameters are required by the lambda expression, again, empty parentheses are used. The types of parameters can be explicitly declared in the expression, but since the types can most often be inferred from the context of the expression, the definition of parameter types is optional. When the expression only accepts a single parameter and the type is not explicitly declared, again the use of parentheses, as previously mentioned, is also optional. As you have seen throughout this lecture, the body of the lambda expression may consist of zero or more lines, so it is possible to write a function that does nothing. When the body of the expression consists of two or more lines the method body must be enclosed by curly brackets. Note, the semicolon after the closing curly bracket, similar to the implementation of an anonymous class. However, when the expression consists of just a single statement, again the use of curly brackets is optional. When the body of a lambda expression consists of only a single line of code, the return type of this anonymous method is determined by the result of the expression that makes up the body. In the example here, the length method returns a value of type int and can therefore not be assigned to a variable of type function. Just like with regular methods, it is possible to have multiple return statements within the method block. Naturally all return values should be of the same type. When different types are used, the compiler will check the expected type by inspecting the type declared in the assignment, and indicate which return statement returns the wrong type. Functional interfaces can be declared and generalized even further through the use of using generics in their definition. The example shown here takes the original functional interface and abstracts it even further by redefining the apply method to take an input of type T and having it return a result of type U. With this new functional interface in place we are now able to declare two different utility methods, as shown here. The first utility method exists within the DoubleUtil class and is named round. Declaring the functional interface with a Double and Integer types, so that the apply method takes in a value of type Double, rounds it and returns a result of type Integer. The second utility method exists within the StringUtil class and is named print, declaring the functional interface with a string type for both T and U, such that the apply method takes in a string, processes it and returns another string. Mixing functional interfaces with generics has no impact on the compilers ability to infer the parameter and return types of the lambda expression, as seen here. This is still perfectly doable by the compiler at compile time. Starting with Java 8 the use of the final keyword has become optional. Variables that are not explicitly defined as final can now be used from within anonymous inner classes. However, the referenced variable must still effectively be final. The code snippet will not compile since the variable is rear site. Also, notice that instead of the bulky anonymous inner class we have used a lambda expression to define the event handler, since ActionListener contains only one abstract method, the interface is considered to be a functional interface and, as a result, a lambda expression can be used to define this handler. The java.util.function package contains a wide variety of functional interfaces. These interfaces contain definitions for the most commonly used operations to which lambda expressions can be applied. The consumer, functions, predicate and supplier interfaces are considered to be interfaces which define the basic shapes. Method references. As we have seen, a lambda expression defines both the parameters and logic that, until now, you would have implemented in the method of an interface. What if you have a method that has a signature that corresponds to the functional method? You could write a lambda expression that invokes that method, but you can also use a reference to this method directly. A method reference is defined by using a double colon followed by the method name without parentheses. In the next couple of slides we will talk about the part before the double colon, but for now, just remember that the method is not invoked at this point. A reference to the method is used. Invoking this method occurs when the entire expression is executed. The function interface accepts one argument and produces an object reference, so when you have a class that has a single argument constructor, you might want to reference this constructor. Implementing a functional interface can be accomplished by referencing a constructor of a class. As stated throughout this lesson, lambda expressions can be used wherever anonymous inner classes are used, as long as the interface to be implemented is a functional interface. However, there are a few minor differences between the two. When the this keyword is used from within the body of an anonymous inner class method, the this pointer references the instance of the anonymous inner class. When the this keyword is used from within the method body of a lambda expression, the this pointer references the instance of the enclosing class. Although not as important for you as a developer, you should also notice that when a class containing an anonymous inner class is compiled, it results in two separate class files, one for the enclosing class and one for the anonymous inner class. The class name of the inner class is generated by adding a dollar sign X where X is a number, to the class name of the enclosing class. When compiling a class containing a lambda expression the expression is compiled into a private method of the enclosing class. Okay, before we complete this lecture, pause this video and consider the following questions to test yourself on the content that we have just reviewed. Write down your answers for each question and then resume the video to compare answers. Okay, the answers to the above questions are one, false, you can define a lambda expression without defining parameters, however for this, you need to define a set of empty parentheses. Two, false, for example, the consumer interface performs the function within the method body of the expression. Three, an interface that contains, at most, one abstract method. Four, no, it is an informative interface, allowing the compiler to check if the interface complies to the specification of functional interfaces. All interfaces that contain only a Single Abstract Method are considered functional interfaces. Five, the use of the this keyword and the fact that lambda expressions can only be used for functional interfaces. Anonymous inner classes can also be used when the interface contains multiple abstract methods.
Jeremy is a Content Lead Architect and DevOps SME here at Cloud Academy where he specializes in developing DevOps technical training documentation.
He has a strong background in software engineering, and has been coding with various languages, frameworks, and systems for the past 25+ years. In recent times, Jeremy has been focused on DevOps, Cloud (AWS, Azure, GCP), Security, Kubernetes, and Machine Learning.
Jeremy holds professional certifications for AWS, Azure, GCP, Terraform, Kubernetes (CKA, CKAD, CKS).