Java Lambdas

This shall be a brief overview over Java Lambdas. I will try to make a comparison to anonymous functions concepts how they are being used in other languages. As a comparison language, I choose Scala, since it is statically typed (just like Java is). However, Scala provides type inference, which makes it feel more like a dynamically typed language at times although it is not. For this little comparison though, I won’t make use of type inference, because I want to focus on the conceptual differences.

A first example of Java Lambdas

Let’s start off with a simple example, first in Scala:

object LambdaScala {
  def main(args: Array[String]) = {
    val f: (Int, Int) => Int = (a, b) => {
      a + b
    }
 
    val sum: Int = f(11, 22)
    println("sum: " + sum) // 33
  }
}

So what would the equivalent code look like in Java using Lambdas? Here it is:

import java.util.function.BiFunction;

class LambdaJava {
  public static void main(String args[]) {
    BiFunction<Integer, Integer, Integer> f = (a, b) -> {
      return a + b;
    };

    Integer sum = f.apply(11, 22);
    System.out.println("sum: " + sum); // 33
  }
}

At first sight there is not much difference here (apart from the fact that Java is somewhat more verbose, just like you would expect) compared to the Scala code.

At a closer look though, two differences attract attention:

  • In the Java code there is an interface involved (java.util.function.BiFunction), while Scala features a native type for anonymous functions.
  • In Java, in order to invoke the anonymous function, you actually call a method of the interface (apply), while in Scala you can call it just like any other function.

Interfaces instead of native function types

Well yeah, in order to assign a lambda to a variable, that variable of course needs a type, as we are dealing with statically typed languages here. The fact that Java is using interfaces here instead of native types makes handling an anonymous function somewhat more clumsy than it would otherwise have to be. You have to import that Interface while in Scala you can just type away the applicable type declaration.

Well in the package java.util.function you will find a whole bunch of predefined Interfaces that might be sufficient for you.

If you encounter a case in which you don’t find an interface that applies to your use case, you will have to define your own:

class Lambda3Java {
  @FunctionalInterface
  interface MyFnType {
    Double apply(Integer i, String s, Double d);
  }

  public static void main(String args[]) {
    MyFnType f = (i, s, d) -> {
      return i.doubleValue() + Double.parseDouble(s) + d;
    };

    Double sum = f.apply(11, "22.2", 33.3);
    System.out.println("sum: " + sum); // 66.5
  }
}

Ah well… not really something that you’d call elegant, huh? In fact that means you either write lambdas that match one of the interfaces in java.util.function or you define your own interface. That is not exactly what someone who is used to functional programming would expect. However, it saved the Java developers a whole bunch of problems, since they certainly didn’t have to change the type system much (if at all) to introduce this kind of lambdas.

Passing around Lambdas

Now what does that all mean when we want to pass around an anonymous function. A common use case eventually, because that is what makes functional programming powerful, e.g. separating business logic from algorithms, having functions apply other functions to data in a certain manner, without having to know that that other function is doing.

So, while in Scala you would do something like:

object LambdaPScala {
  def make_adder(): (Int, Int) => Int = {
    (a, b) => {
      a + b
    }
  }

  def main(args: Array[String]) = {
    val adder: (Int, Int) => Int = make_adder()
    println("sum: " + adder(11, 22)) // 33
  }
}

the equivalent Java code looks like this:

import java.util.function.BiFunction;

class LambdaPJava {
  private static BiFunction<Integer, Integer, Integer> make_adder() {
    return (a, b) -> {
      return a + b;
    };
  }

  public static void main(String args[]) {
    BiFunction<Integer, Integer, Integer> adder = make_adder();
    System.out.println("sum: " + adder.apply(11, 22)); // 33
  }
}

Nothing that suprises us now, as we’ve recognized the interface & “apply” differences before.

Lambdas as Closures

So, one more thing: What about closures?

We can easily build a closure in Scala that acts as an accumulator, i.e. calling it with an argument will add that argument to the accumulator’s value while that accumulator maintains state:

object LambdaCScala {
  def make_acc(): (Int) => Int = {
    var sum: Int = 0
    (a) => {
      sum = sum + a
      sum
    }
  }

  def main(args: Array[String]) = {
    val acc: (Int) => Int = make_acc()
    println("sum: " + acc(11)) // 11
    println("sum: " + acc(22)) // 33
    println("sum: " + acc(33)) // 66
  }
}

Java Lambdas however turns out to not allow modifying state of free variables (i.e. the variables the anonymous function closes over, sum in this case). This means the following, equivalent Java code does not compile:

// this won't compile!
 
import java.util.function.Function;

class LambdaCJava {
  private static Function<Integer, Integer> make_acc() {
    Integer sum = 0;
    return (a) -> {
      sum = sum + a;
      return sum;
    };
  }

  public static void main(String args[]) {
    Function<Integer, Integer> acc = make_acc();
    System.out.println("sum: " + acc.apply(11));
    System.out.println("sum: " + acc.apply(22));
    System.out.println("sum: " + acc.apply(33));
  }
}

The compiler will complain about that sum is not declared final, due to trying to access it from within the lambda. Declaring it final though means that you won’t be able to assign a new value to it during a call to the lambda, rendering implementation of our accumulator in this way impossible.

From within Java lambdas you can read free variables though.

Maneuvering around the Java Closure “final” restriction

You can of course avoid this restriction by not assigning a new value to the free variable directly, but arranging it to be a container for the value you want to represent and then call methods on that object to mutate your state within that object. We could for example make sum an AtomicInteger and then call get() and set() on it instead of assigning it a new value directly (thanks to Dave for pointing this out):

import java.util.function.Function;
import java.util.concurrent.atomic.AtomicInteger;

class LambdaC2Java {
  private static Function<Integer, Integer> make_acc() {
    AtomicInteger sum = new AtomicInteger(0);
    return (a) -> {
      sum.set(sum.get() + a);
      return sum.get();
    };
  }

  public static void main(String args[]) {
    Function<Integer, Integer> acc = make_acc();
    System.out.println("sum: " + acc.apply(11)); // 11
    System.out.println("sum: " + acc.apply(22)); // 33
    System.out.println("sum: " + acc.apply(33)); // 66
  }
}

However, this feels like cheating a bit and is not as concise as it could be.

Conclusion

Lambdas in Java are definitely an improvement to the language. However, they still fall short on several expectations that you’d usually have when talking about anonymous functions, lambdas and functional programming:

  • You’d expect them to have a native type (instead of taking an indirect route via interfaces)
  • You’d expect that you can call them using the normal call syntax of your language (i.e. fn(1, 2) instead of fn.apply(1, 2) )
  • You’d expect to get full featured lexical closures – including mutation of free variables, as this is an important aspect for why closures are powerful.

So Java Lambdas feel a bit restrained, especially when you’ve experience with anonymous functions in other languages that really treat lambdas as very native features of their own. The Scala code here is even more verbose than it would usually be, as I didn’t make use of Scala’s type inference. If you start throwing dynamically typed languages into the mix, the discrepancy between Lambdas as they are now implemented in Java and these languages will even grow.

Leave a Reply

Your email address will not be published. Required fields are marked *

*