Search code examples
javaarrayslambdainterfacefunctional-interface

How is it possible that when I pass a lambda expression as a parameter it can access other variables in this scope?


public class ArraysDemo {

    public static void main(String[] args) {

          int[] a = {0, 2, 4, 6, 8};
          int[] b = {10, 12, 14, 16, 18};
          Arrays.setAll(a, i -> b[i]+1);
          System.out.println(Arrays.toString(a));
    }  
}

outputs: [11, 13, 15, 17, 19]

The source of the setAll() function used is as follows:

public static void setAll(int[] array, IntUnaryOperator generator) {
        Objects.requireNonNull(generator);
        for (int i = 0; i < array.length; i++)
            array[i] = generator.applyAsInt(i);
}

IntUnaryOperator is a functional interface and this is a part of its source:

public interface IntUnaryOperator {
    int applyAsInt(int operand);
    // rest methods are omitted
}

Correct me if I'm wrong but my understanding of lambda expressions in Java is that when I pass a lambda expression as a parameter to the setAll() method, an object of an anonymous class that implements the IntUnaryOperator interface is created and is called generator. And the lambda expression is essentially an implementation of the applyAsInt() method so I believe it would translate to something like:

int applyAsInt(int operand){
    return b[operand]+1;
}

It makes sense to me that it can access operand as it is passed as an argument in array[i] = generator.applyAsInt(i); However, I don't get how it can manipulate b - it is not passed as a parameter so how is it possible that it can be referenced? What am I missing?


Solution

  • Even without lambda expressions, e.g. using anonymous inner classes, you can capture values of the surrounding context, i.e.

    int[] a = {0, 2, 4, 6, 8};
    int[] b = {10, 12, 14, 16, 18};
    Arrays.setAll(a, new IntUnaryOperator() {
        public int applyAsInt(int i) {
            return b[i]+1;
        }
    });
    

    but there are some differences, though not relevant to your question. When using an anonymous inner class, the keywords this and super refer to the instance of the inner class, whereas in lambda expressions, they have the same meaning as in the surrounding context. Further, when the inner class accesses private members of the surrounding class, the compiler will insert helper method to perform the access, whereas lambda expressions can access the members of theur containing class naturally.

    To achieve this, the code of your lambda expression i -> b[i]+1 will get compiled to synthetic method of your class having the form:

    private static int lambda$main$0(int[] b, int i) {
        return b[i]+1;
    }
    

    and since it’s a method within your class, it has access to all members.

    So the class generated at runtime is equivalent to

    final class ArraysDemo$Lambda$1 implements IntUnaryOperator {
        final int[] b;
        ArraysDemo$Lambda$1(int[] b) {
            this.b = b;
        }
        public int applyAsInt(int i) {
            return ArraysDemo.lambda$main$0(b, i);
        }
    }
    

    the focus on the word equivalent, not suggesting that it has to be exactly like this (not to speak of the fact, that ordinary Java classes couldn’t access the private method ArraysDemo.lambda$main$0).


    But these are only technical details, the most important key point is that lambda expressions can access effectively final local variables as well as members of their containing class and the compiler and runtime environment will ensure that it works. You can simply consider them functions being evaluated in the context where they are defined.