Friday, July 2, 2021

Java Tutorial: Lambda Expression

Chapters

Lambda Expression

Lambda Expression was introduced in java8. It's a clear and compact way of defining a function of an abstract method in a functional interface. Functional interface is an interface with one abstract method only. Let's create an example demonstrating lambda expression.
General Form: (arg1,arg2) -> {};
Where:
(arg1,arg2) - argument-list
-> - arrow token
{}; - body


public class SampleClass{
  
  static void displayString(PrintString ps,
                            String s){
      if(s.startsWith("My"))
         ps.printString(s);
    
  }
  
  public static void main(String[]args){
    
    //Standard way of writing lambda.
    //
    //we can change the argument name in the argument-list
    //even the name doesn't match with the
    //parameter name of functional interface
    //and this code will still run fine.
    //
    //for convenience, we want the argument-list of
    //lambda to have the same name as the
    //parameter-list of the abstract method
    //in functional interface.
    SampleClass.displayString((String s) -> 
                             {System.out.println(s);},
                             "MyString1");'
                             
    //Lambda body({}) can be omitted if the body
    //only has a single method call statement.
    //Some statements require the body like the return
    //statement,assignment statement, etc.
    //e.g
    //() -> return true; //invalid
    //() -> {return true;}; //valid
    //() -> System.out.println(); //valid
    //
    //Types in argument-list can also
    //be omitted.
    //e.g. (String s) = (s)
    //Sometimes, java can't infer the type from the
    //parameter-list. Java will notify us about this
    //and if we receive a message then, we need
    //to explicitly write the type.
    //Note that if one element in the argument-list
    //has or doesn't have type then, all elements
    //in the argument-list must have or mustn't have
    //types
    //
    //We can omit the parentheses of argument-list
    //if there's only one argument in the argument-list
    //e.g (s) = s
    //
    //We can store lambda expression in a
    //functional interface variable.
    PrintString ps = s -> System.out.println(s);
    SampleClass.displayString(ps,"MyString2");
    
    //displayString() lambda with omitted body({})
    /*
        SampleClass.displayString((String s) -> 
                                  System.out.println(s),
                                  "MyString1");
    */
  }
}

@FunctionalInterface
interface PrintString{
  void printString(String s);
}
Now, let's try lambda expression with return type and no argument-list.
public class SampleClass{

  public static void main(String[]args){
    
    //leave the argument-list blank
    //since sayHello() abstract method in SayHello
    //interface doesn't have parameters
    SayHello sh = () -> System.out.println("Hello");
    sh.sayHello();
    
    //We can put multiple statements in lambda's
    //body
    CompareStringLength compStrLength = 
    (s1,s2) -> { 
      //if s1 is greater than s2
      if(s1.length() > s2.length())
         return 1;
      //if s2 is greater than s1
      else if(s1.length() < s2.length())
         return -1;
      
      //If both strings have equal length
      return 0;
    };
    
    switch(compStrLength.compareLength("String1","String2")){
    
      case 1:
      System.out.println("s1 is greater than s2");
      break;
      
      case -1:
      System.out.println("s2 is greater than s1");
      break;
      
      case 0:
      System.out.println("s1 and s2 have equal length");
      break;
    }
    
    //if the lambda body only has a return statement
    //we can omit the return keyword like in this
    //lambda expression
    StringEquality se =
    (s1,s2) -> s1.equals(s2);
    
    //the stament above is equivalent to this
    //statement
    //StringEquality se =
    //(s1,s2) -> {return s1.equals(s2);};
    
    boolean result = se.checkEquality("A","A");
    System.out.println(result);
  }
}

@FunctionalInterface
interface SayHello{
  //abstract method with no
  //parameters and return
  //type
  void sayHello();
}

@FunctionalInterface
interface CompareStringLength{
  
  //abstract method with parameters
  //and return type.
  int compareLength(String s1, String s2);
}

@FunctionalInterface
interface StringEquality{

  boolean checkEquality(String s1, String s2);
}
var Type Name in Lambda Argument-List

Starting from java11, We can use the var Type Name as type in lambda argument-list.
public class SampleClass{
  
  static void displayString(PrintString ps,
                            String s){
      if(s.startsWith("My"))
         ps.printString(s);
    
  }
  
  public static void main(String[]args){
    //use var keyword as type on argument-list
    PrintString ps1 = (var s) -> System.out.println("String:" + s);
    
    SampleClass.displayString(ps1,"MyString1");
  }
}

@FunctionalInterface
interface PrintString{
  void printString(String s);
}
Lambda Variable Capture

Lambda expression can capture variables outside of its scope. Lambda expression can capture local, instance and static variables.
public class SampleClass{
  //instance variable
  String str1 = "str1";
  //static variable
  static String str2 = "str2";
  
  void displayString(){
    //local variable
    //Note: local variables referenced
    //from a lambda expression must be
    //final or effectively final
    String str3 = "str3";
    
    //this lambda expression captures
    //str1,str2 and str3
    ConcatString cs = () ->
    {return str1 + str2 + str3;};
    
    System.out.println(cs.concatString());
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    sc.displayString();
  }
}

@FunctionalInterface
interface ConcatString{

  String concatString();
}
Be careful capturing global variables in multithreading. Even though global variables can be captured even they're not final or effectively final, the values they're holding may not change if they're updated in the lambda expression. Take a look at this example.

Note: This example may cause infinite loop, you should know how to close JVM by force before testing this example.
public class SampleClass{
  
  volatile static int count = 0;
  public static void main(String[] args){
    
    while(count < 3){
      System.out.println("To infinity and beyond...");
      new Thread(() -> {
        count++;
      });
    }
  }
}
Method Reference as Lambda Expression

We can substitute a method/constructor call as lambda expression. The parameters, parameter type and return type of the method definition of that method call should match the abstract method parameters, parameter type and return type in the functional interface.

First, we need to convert the method/constructor call as method reference. Method reference is similar to method call structure but without the parentheses and with additional "::" symbol.
Syntax: class-name::method-name;
public class SampleClass{

  public static void main(String[]args){
    //println is a member of out class in
    //System class. So, we refer to the System
    //then refer to out and refer to println().
    DisplayString ds = System.out::println;
    //The statement above is equivalent to this lambda
    //DisplayString ds = s -> System.out.println(s);
    
    ds.displayString("String");
  }
}

@FunctionalInterface
interface DisplayString{

  void displayString(String s);
}
In the example above, we see that the functionality of println() becomes the functionality of displayString() in DisplayString interface. We can also create method references from the available methods of a parameter of the abstract method in functional interface.
public class SampleClass{

  public static void main(String[]args){

    StringCaps sc = String::toUpperCase;
    
    String str = sc.capitalizeString("String");
    
    System.out.println(str);
  }
}

@FunctionalInterface
interface StringCaps{

  String capitalizeString(String s);
}

Result
STRING
In the example above, we create a method reference of String.toUpperCase(). This is possible because the parameter is capitalizeString() is a String type and toUpperCase() is available to any String instance.

Method Reference with Unequal Parameter Count

There are methods that can be referred to a functional interface even the parameter count of those method don't match the parameter of the abstract method in functional interface. The first parameter in the functional interface calls the referenced method the others are used as arguments.
public class SampleClass{

  public static void main(String[]args){
    
    //concat() in String class only has
    //one parameter and concat() in
    //ConcatString has two parameters.
    ConcatString cs = String::concat;
    
    //The statement above is equivalent to 
    //this lambda.
    //
    //As we can see, s1 calls concat() of String
    //class and s2 is the argument for concat() of
    //String.
    //ConcatString cs = (s1,s2) -> s1.concat(s2);
    
    System.out.println(cs.concat("A","B"));
  }
}

@FunctionalInterface
interface ConcatString{

  String concat(String s1,String s2);
}
Try changing the parameter of concat() in ConcatString and you will get an invalid method reference error.

Method Reference Return Type and Parameter Type

It's alright if the return type of the method in functional interface and the return type of the referenced method are not the same, as long as those return types are related. For parameters, the parameter type of the method in functional interface should be equal.
public class SampleClass{

  public static void main(String[]args){
    
    ConcatString cs = String::concat;
    
    System.out.println(cs.concat("A","B"));
  }
}

@FunctionalInterface
interface ConcatString{
  
  //the return type of concat() in String class
  //is String. CharSequence and String are
  //related, so concat() of String can be
  //referenced to this method
  CharSequence concat(String s1,String s2);
  
  //We can't reference concat() of String to this
  //method 'cause the first parameter is CharSequence and
  //CharSequence doesn't have concat(). So, s1 can't call
  //concat()
  //
  //Second, the second parameter is CharSequence and the
  //parameter of concat() of String class is String.
  //We can't downcast a parameter type. So, the second
  //parameter is invalid.
  //CharSequence concat(CharSequence s1,CharSequence s2);
}
We can also refer a functional interface with void return type to a method reference with a return type that is not void. However, we can't refer a functional interface with non-void return type to a method reference with void return type.
public class SampleClass{

  public static void main(String[] args){
    
    //invalid
    //interface1 i1 = SampleClass::meth;
    
    //valid
    interface2 i2 = SampleClass::meth2;
    i2.invoke(2);
  }
  
  static void meth(int num){
    System.out.println(num*num);
  }
  
  static int meth2(int num){
    System.out.println(num*num);
    return num*num;
  }
}

@FunctionalInterface
interface interface1{

  int invoke(int num);
}

@FunctionalInterface
interface interface2{

  void invoke(int num);
}
User-Defined Methods as Method Reference

We can also use our own method as method reference. Static and non-static methods can be referred.
public class SampleClass{
  private String name;
  
  SampleClass(String name){
    this.name = name;
  }
  
  static void displayString(CharSequence s){
    System.out.println(s);
  }
  
  String concatName(SampleClass sc){
    return name.concat(sc.toString());
  }
  
  @Override
  public String toString(){
    return name;
  }
  
  boolean checkEquality(String s1, String s2){
    return s1.equals(s2);
  }
  
  public static void main(String[]args){
    //using static method as method reference
    //also, notice the parameter of displayString()
    //and printString().
    //printString() has a parameter type of String
    //whereas displayString() has a parameter type
    //of CharSequence.
    //String can be upcasted to CharSequence.
    //That's why this method reference is valid.
    PrintString ps = SampleClass::displayString;
    ps.printString("String");
    
    SampleClass sc1 = new SampleClass("SampleClass1");
    
    //using non-static method as method reference
    //using class name
    CombineString cs = SampleClass::concatName;
    
    String str = cs.concat(sc1,new SampleClass("SampleClass2"));
    System.out.println(str);
    
    //using non-static method as method reference
    //using variable name
    CompareString compStr = sc1::checkEquality;
    System.out.println(compStr.compare("Str1","str2"));
    
    //invalid: when we refer a method to a functional interface
    //using class instance, we must match the parameter count
    //of the functional interface to the parameter count of
    //referenced method.
    //concatName has one parameter whereas concat() in
    //CombineString has two parameters. So, this
    //method reference is invalid
    //CombineString cs = sc1::concatName;
  }
}

@FunctionalInterface
interface PrintString{

  void printString(String s);
}

@FunctionalInterface
interface CombineString{

  String concat(SampleClass sc1, SampleClass sc2);
}

@FunctionalInterface
interface CompareString{

  boolean compare(String s1,String s2);
}
Constructor Reference

We can refer a constructor to a functional interface. We need to tweak our method reference syntax to be suitable for constructors. This is our new syntax: class-name::new;
public class SampleClass{

  public static void main(String[]args){
    //using constructor as a reference for
    //StringInstance
    StringInstance si = String::new;
    //The statement above is equivalent to
    //this lambda
    //StringInstance si = letters -> new String(letters);
    
    String s1 = si.createString(new char[]{'s','t'});
    String s2 = si.createString(new char[]{'r','i','n','g'});
    
    System.out.println(s1.concat(s2));
  }
}

@FunctionalInterface
interface StringInstance{

  String createString(char[] letters);
}
The parameters of the method in functional interface must match the parameters of constructor. Also, we can see that createString() method is similarly behaving like a constructor.
java.util.function Introduction

Note: You need to have an understanding about generics to understand generics syntax that we're going to discuss here. java.util.function package has lots of pre-built functional interfaces that we can use. As the title of this topic implies, I'll only introduce basic usage of some functional interfaces in this package.

Let's try the Predicate<T> interface.
import java.util.function.Predicate;
public class SampleClass{

  public static void main(String[]args){
    String str = "myString";
    
    //Predicate<T> is a functional
    //interface that is part of
    //java.util.function package
    Predicate<String> predicate =
    (t) -> t.toString().equals(str);
    
    //test() method in Predicate accepts "T"
    //type as argument and return a boolean value
    boolean result = predicate.test("MyString");
    System.out.println(result);
  }
}
Next, let's try the Consumer<T> interface.
import java.util.function.Consumer;
public class SampleClass{
  static String str = "string";
  
  public static void main(String[]args){
    
    Consumer<String> consumer = 
    t -> str = str.concat(t.toString());
    
    Consumer<CharSequence> after =
    t -> System.out.println(t.toString() + str);
    
    //andThen(Consumer<? super T> after)
    //returns a composed Consumer that
    //performs, in sequence, this operation
    //followed by the after operation.
    //This method composes the "before" and
    //"after" operations that we specify in
    //the Consumer<T> object
    //
    //"before" operation is the lambda expression
    //that we first passed to the consumer variable
    //"after" operation is the lambda expression
    //that we passed to after variable
    //
    //once the composition is done, the method
    //will return the Consumer<T> object
    //with the composed operation.
    consumer = consumer.andThen(after);
    
    //Instead of creating "after" variable,
    //we can rely on type inference and directly
    //put the lambda expression in andThen()
    /*
    consumer = consumer.andThen(
    t -> System.out.println(t.toString() + str));
    */
    
    //accept(T t) method accepts "T"
    //type as argument and returns
    //nothing
    consumer.accept("\"");
  }
}
Lambda expression is commonly used in collection. Thus, you will see some prebuilt functional interface being used in Collection API. For example, forEach() method has one parameter which is Consumer<? super T> action. forEach() is a member of Iterable interface, which is implemented by some classes in Collection API like ArrayList, etc.
import java.util.ArrayList;
import java.util.function.Consumer;
public class SampleClass{
  static String str = "";
  
  public static void main(String[]args){
  
    ArrayList<String> arrList = 
    new ArrayList<>();
    arrList.add("A");
    arrList.add("B");
    arrList.add("C");
    
    Consumer<String> consumer = 
    t -> str = str.concat(t.toString());
    
    consumer = consumer.andThen(
    t -> System.out.println(str));
    
    //forEach iterates through 
    //collection where "t" in accept(T t)
    //of Consumer interface is
    //the collection element
    arrList.forEach(consumer);
  }
}
Target Typing in Lambda Expression

Target type is a type in an expression that is expected by the compiler. Target typing is used in typecasting and type inference in generics. Target typing is also implemented in lambda expression.

The Java compiler determines the target type with two other language features: overload resolution and type argument inference. Let's start with type argument inference.
public class SampleClass{
  
  static void concatString(String s,ConcatString cs){
    System.out.println(s.concat(cs.concat("C","D")));
  }
  
  public static void main(String[]args){
    //In this statement the compiler expects that the
    //type in the second argument is ConcatString
    //So, the lambda expression we put in the second
    //argument is going to be ConcatString type
    //
    //The type that is expected by the method is the
    //target type
    SampleClass.concatString("AB",
                            (s1,s2) -> s1.concat(s2));
  }
}

@FunctionalInterface
interface ConcatString{

  String concat(String s1,String s2);
}
For more information about target typing using overload resolution and other information related to target typing, I recommend you to read the The Java™ Tutorials

Lambda Expression as an Alternative to Anonymous Class

One of the usage of lambda expression is to be an alternative to anonymous class. If we want to refer a single abstract method in an interface to a variable then better use lambda.
public class SampleClass{
  
  static void displayString(PrintString ps,
                            String s){
      if(s.startsWith("My"))
         ps.printString(s);
    
  }
  
  public static void main(String[]args){
    
    //anonymous class
    PrintString ps1 = new PrintString(){
    
      @Override
      public void printString(String s){
        System.out.println("Print String: " + s);
      }
    };
    
    //Lambda Expression
    PrintString ps2 = (s) -> System.out.println("String:" + s);
    
    SampleClass.displayString(ps1,"MyString1");
    SampleClass.displayString(ps2,"MyString2");
    
  }
}

@FunctionalInterface
interface PrintString{
  void printString(String s);
}
As we can see, lambda expression is more clear, concise and compact than anonymous class. However, Lambda expression is not a complete alternative to anonymous class.

If we want to refer multiple methods to a variable then, we can use anonymous class but not lambda expression. Lambda expression can be only used to define a single abstract method in an interface.
public class SampleClass{

  public static void main(String[]args){
  
   ClassA a1 = new ClassA(){
   
     @Override
     void meth1(){System.out.println("meth1");}
     
     @Override
     void meth2(){System.out.println("meth2");}
   };
   a1.meth1();
   a1.meth2();
   
  }
}

abstract class ClassA{

  abstract void meth1();
  abstract void meth2();
}
We can also replace anonymous class with lambda expression when implementing prebuilt interfaces with only one abstract method like Runnable, EventListener, etc.

Let's try using lambda expression to implement Runnable interface
public class SampleClass{

  public static void main(String[]args){
    
    //Anonymous Class
    /*
    Thread thread = new Thread(
      new Runnable(){
        
        @Override
        public void run(){
          System.out.println("run!");
        }
     });
     thread.start();
    */
    
    //lambda
    Thread thread = () -> System.out.println("run!");
    thread.start();
  }
}

No comments:

Post a Comment