2

Below are two sets of codes for creating multiple runnable objects via Java 8 lambdas. However, I am seeing a difference in the behavior of object creation.

Code 1 - create multiple objects, as hashcode is different

PasswordUtils passwordUtils = new PasswordUtils();
for(int i = 0 ; i < 100 ; i++) {
    Runnable r = () -> passwordUtils.print();            //difference       
    System.out.println(r.hashCode());
}

Output -

1129670968
1023714065
.. //varies in each iteration

Code 2 - create single object, as hashcode is same for all object.

for(int i = 0 ; i < 100 ; i++) {
    Runnable r = () -> new PasswordUtils().print();      //difference       
    System.out.println(r.hashCode());
}

Output-

1254526270
1254526270
... // same on all iterations

The difference is when the PasswordUtils object is created. What is the reason for this difference?

Edit: Just to complete the code, one can use

class PasswordUtils {
    void print() {
    }
}
Naman
  • 27,789
  • 26
  • 218
  • 353
  • 3
    @bharatbhushan See these related questions - [Does a lambda expression create an object on the heap every time it's executed?](https://stackoverflow.com/questions/27524445/does-a-lambda-expression-create-an-object-on-the-heap-every-time-its-executed) and [Is method reference caching a good idea in Java 8?](https://stackoverflow.com/questions/23983832/is-method-reference-caching-a-good-idea-in-java-8) – Naman Jul 03 '20 at 13:47
  • 2
    From the comment on the first linked question, "For stateless lambdas (those that do not capture anything from their lexical context), only one instance will ever be created (lazily), and cached at the capture site." In your second case, your code does not depend on anything except for what is inside of the lambda. So there is 1 instance. – matt Jul 03 '20 at 13:55

2 Answers2

3

This code that reproduces your findings:

public class Main {
    public static void main(String[] args) throws IOException {
        Object passwordUtils = new Object();
        for(int i = 0 ; i < 10 ; i++) {
            Runnable r = () -> passwordUtils.toString();
            System.out.println(r.hashCode());
        }

        System.out.println("-------");

        for(int i = 0 ; i < 10; i++) {
            Runnable r = () -> new Object().toString();
            System.out.println(r.hashCode());
        }
    }
}

compiles to:

METHOD: main([Ljava/lang/String;)V
--------------------------------------
L0:
{
    NEW
    DUP
    INVOKESPECIAL   java/lang/Object/<init>()V
    ASTORE_1

    //int I = 0
    ICONST_0
    ISTORE_2
}

L2:
{
    ILOAD_2
    BIPUSH  10
    IF_ICMPGE  L3
}

L4:
{
    ALOAD_1
    INVOKEDYNAMIC   java/lang/invoke/LambdaMetafactory/metafactory (MethodHandles$Lookup, String, MethodType, MethodType, MethodHandle, MethodType)CallSite; : run(Object)Runnable;
    ASTORE_3
}

L5:
{
    GETSTATIC   java/lang/System/out Ljava/io/PrintStream;
    ALOAD_3
    INVOKEVIRTUAL   java/lang/Object/hashCode()I
    INVOKEVIRTUAL   java/io/PrintStream/println(I)V
}

L6:
{
    IINC
    GOTO  L2
}

L3:
{
    GETSTATIC   java/lang/System/out Ljava/io/PrintStream;
    LDC  "-------"
    INVOKEVIRTUAL   java/io/PrintStream/println(Ljava/lang/String;)V
}

L7:
{
    ICONST_0
    ISTORE_2
}

L8:
{
    ILOAD_2
    BIPUSH  10
    IF_ICMPGE  L9
}

L10:
{
    INVOKEDYNAMIC   java/lang/invoke/LambdaMetafactory/metafactory (MethodHandles$Lookup, String, MethodType, MethodType, MethodHandle, MethodType)CallSite; : run()Runnable;
    ASTORE_3
}

L11:
{
    GETSTATIC   java/lang/System/out Ljava/io/PrintStream;
    ALOAD_3
    INVOKEVIRTUAL   java/lang/Object/hashCode()I
    INVOKEVIRTUAL   java/io/PrintStream/println(I)V
}

L12:
{
    IINC
    GOTO  L8
}

L9:
{
    RETURN
}



METHOD: lambda$main$1()V
--------------------------------------
L0:
{
    NEW
    DUP
    INVOKESPECIAL   java/lang/Object/<init>()V
    INVOKEVIRTUAL   java/lang/Object/toString()Ljava/lang/String;
    POP
    RETURN
}

METHOD: lambda$main$0(Ljava/lang/Object;)V
--------------------------------------

L0:
{
    ALOAD_0
    INVOKEVIRTUAL   java/lang/Object/toString()Ljava/lang/String;
    POP
    RETURN
}

Notice that the first InvokeDynamic instruction has an ALOAD_1 and the other doesn't.. that ALOAD_1 is the captured object passwordUtils variable..

The InvokeDynamic instructions above translates to:

//Calls Runnable.run with a captured variable `passwordUtils` -> #1
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(
                lookup,
                "run",
                MethodType.methodType(Runnable.class, Object.class),
                MethodType.methodType(void.class),
                lookup.findStatic(Main.class, "Main$lambda$1", MethodType.methodType(void.class, Object.class)),
                MethodType.methodType(void.class)
        );

        MethodHandle factory = site.getTarget();
        Runnable r = (Runnable)factory.invoke(new Object());
        //r.run();
        System.out.println(r.hashCode());

//Calls Runnable.run() -> #2
MethodHandles.Lookup lookup = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(
                lookup,
                "run",
                MethodType.methodType(Runnable.class),
                MethodType.methodType(void.class),
                lookup.findStatic(Main.class, "Main$lambda$2", MethodType.methodType(void.class)), //doesn't capture
                MethodType.methodType(void.class)
        );

        MethodHandle factory = site.getTarget();
        Runnable r = (Runnable)factory.invoke();
        //r.run();
        System.out.println(r.hashCode());

If you encapsulated each #1 and #2 in for loops and run them, every single time it'd recreate the CallSite object and therefore would result in different hashcodes..

However, if you were to optimize it just like the JVM would, you'd notice that one callsite is cached and the other isn't..

IE:

for (int i = 0; i < 10; ++i)
{
    Runnable r = (Runnable)factory.invoke();
    System.out.println(r.hashCode());
}

In the case where it captures the variable, a NEW Runnable is returned every single time (it can modify the method handle if it can't optimize the callsite), and in the case where the body of the function stays the same (IE: no capturing), the same Runnable is returned every time (because it calls InvokeExact internally).

/**
     * Invokes the method handle, allowing any caller type descriptor,
     * and optionally performing conversions on arguments and return values.
     * <p>
     * If the call site's symbolic type descriptor exactly matches this method handle's {@link #type type},
     * the call proceeds as if by {@link #invokeExact invokeExact}.
     * <p>
     * Otherwise, the call proceeds as if this method handle were first
     * adjusted by calling {@link #asType asType} to adjust this method handle
     * to the required type, and then the call proceeds as if by
     * {@link #invokeExact invokeExact} on the adjusted method handle.
     * <p>
     * There is no guarantee that the {@code asType} call is actually made.
     * If the JVM can predict the results of making the call, it may perform
     * adaptations directly on the caller's arguments,
     * and call the target method handle according to its own exact type.
     * <p>
     * The resolved type descriptor at the call site of {@code invoke} must
     * be a valid argument to the receivers {@code asType} method.
     * In particular, the caller must specify the same argument arity
     * as the callee's type,
     * if the callee is not a {@linkplain #asVarargsCollector variable arity collector}.
     * <p>
     * When this method is observed via the Core Reflection API,
     * it will appear as a single native method, taking an object array and returning an object.
     * If this native method is invoked directly via
     * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
     * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
     * it will throw an {@code UnsupportedOperationException}.
     * @param args the signature-polymorphic parameter list, statically represented using varargs
     * @return the signature-polymorphic result, statically represented using {@code Object}
     * @throws WrongMethodTypeException if the target's type cannot be adjusted to the caller's symbolic type descriptor
     * @throws ClassCastException if the target's type can be adjusted to the caller, but a reference cast fails
     * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
     */
    public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;
Brandon
  • 22,723
  • 11
  • 93
  • 186
2

Based on the comment from Does a lambda expression create an object on the heap every time it's executed?

... For stateless lambdas (those that do not capture anything from their lexical context), only one instance will ever be created (lazily), and cached at the capture site. (This is how the implementation works; the spec was carefully written to allow, but not require, this approach.)

In code one a PasswordUtil is created and then used in the lambda, so it does capture something from the lexical context. In code 2, the PasswordUtil is created inside of the lambda, and the lambda never captures anything from it's context at creation, so only one instance is created. Hence the same hashCode is output.

The answer to the linked question provides more information about where this criteria can be found in the JLS. I've added an answer here to show why this specific case is example of when a new instance is not being created.

matt
  • 10,892
  • 3
  • 22
  • 34
  • 5
    It’s worth pointing out that printing the hashcodes is not a reliable test for the object identities. It happened to be in line with the actual behavior here, but for a better test, the code snippets of [this answer](https://stackoverflow.com/a/23991339/2711488) keep the objects from different iterations and compare via `==`. And always keep in mind that this is an implementation specific behavior. A different runtime could even reuse objects of capturing lambda expressions or always create new objects even for non-capturing expressions. All within the specification… – Holger Jul 03 '20 at 15:25