Roughly a month ago, I summarised Brian Goetz’ peek under the hood of lambda expressions in Java 8. Currently I’m researching for a post about default methods and to my mild surprise came back to how Java handles lambda expressions. The intersection of these two features can have a subtle but surprising effect, which I want to discuss.

Overview

To make this more interesting I’ll start the post with an example, which will culminate in my personal WTF?! moment. The full example can be found in a dedicated GitHub project.

We will then see the explanation for this somewhat unexpected behaviour and finally draw some conclusions to prevent bugs.

Example

Here goes the example… It’s not as trivial or abstract as it could be because I wanted it to show the relevance of this scenario. But it is still an example in the sense that it only alludes to code which might actually do something useful.

A Functional Interface

Assume we need a specialisation of the interface Future for a scenario where the result already exists during construction.

We decide to implement this by creating an interface ImmediateFuture which implements all functionality except get() with default methods. This results in a functional interface.

You can see the source here.

A Factory

Next, we implement a FutureFactory . It might create all kinds of Futures but it definitely creates our new subtype. It does so like this:

/**
 * Creates a new future with the default result.
 */
public static Future<Integer> createWithDefaultResult() {
	ImmediateFuture<Integer> immediateFuture = () -> 0;
	return immediateFuture;
}

/**
 * Creates a new future with the specified result.
 */
public static Future<Integer> createWithResult(Integer result) {
	ImmediateFuture<Integer> immediateFuture = () -> result;
	return immediateFuture;
}

Creating The Futures

Finally we use the factory to create some futures and gather them in a set:

public static void main(String[] args) {
	Set<Future<?>> futures = new HashSet<>();

	futures.add(FutureFactory.createWithDefaultResult());
	futures.add(FutureFactory.createWithDefaultResult());
	futures.add(FutureFactory.createWithResult(42));
	futures.add(FutureFactory.createWithResult(63));

	System.out.println(futures.size());
}

WTF?!

Run the program. The console will say…

4?

Nope. 3.

WTF?!

Evaluation of Lambda Expressions

So what’s going on here? Well, with some background knowledge about the evaluation of lambda expressions it’s actually not that surprising. If you’re not too familiar with how Java does this, now is a good time to catch up. One way to do so is to watch Brian Goetz’ talk “Lambdas in Java: A peek under the hood” or read my summary of it.

instances-non-capturing-lambdas

Instances of Lambda Expressions

The key point to understanding this behavior is the fact that the JRE makes no promise about how it turns a lambda expression into an instance of the respective interface. Let’s look at what the Java Language Specification has to say about the matter:

15.27.4. Run-time Evaluation of Lambda Expressions

[…]

Either a new instance of a class with the properties below is allocated and initialized, or an existing instance of a class with the properties below is referenced.

[… properties of the class – nothing surprising here …]

These rules are meant to offer flexibility to implementations of the Java programming language, in that:

  • A new object need not be allocated on every evaluation.
  • Objects produced by different lambda expressions need not belong to different classes (if the bodies are identical, for example).
  • Every object produced by evaluation need not belong to the same class (captured local variables might be inlined, for example).
  • If an “existing instance” is available, it need not have been created at a previous lambda evaluation (it might have been allocated during the enclosing class’s initialization, for example).

[…]

JLS, Java SE 8 Edition, §15.27.4

Amongst other optimizations, this clearly enables the JRE to return the same instance for repeated evaluations of a lambda expression.

Instances of Non-Capturing Lambda Expressions

Note that in the example above the expression does not capture any variables. It can hence never change from evaluation to evaluation. And since lambdas are not designed to have state, different evaluations can also not “drift apart” during their lifetime. So in general, there is no good reason to create several instances of non-capturing lambdas as they would all be exactly the same over their whole lifetime. This enables the optimization to always return the same instance.

(Contrast this with a lambda expression which captures some variables. A straight forward evaluation of such an expression is to create a class which has the captured variables as fields. Each single evaluation must then create a new instance which stores the captured variables in its fields. These instances are obviously not generally equal.)

So that’s exactly what happens in the code above. () -> 0 is a non-capturing lambda expression so each evaluation returns the same instance. Hence the same is true for each call to createWithDefaultResult().

Remember, though, that this might only be true for the JRE version currently installed on my machine (Oracle 1.8.0_25-b18 for Win 64). Yours can differ and so can the next gal’s and so on.

Lessons Learned

So we saw why this happens. And while it makes sense, I’d still say that this behavior is not obvious and will hence not be expected by every developer. This is the breeding ground for bugs so let’s try to analyze the situation and learn something from it.

Subtyping with Default Methods

Arguably the root cause of the unexpected behavior was the decision of how to refine Future. We did this by extending it with another interface and implementing parts of its functionality with default methods. With just one remaining unimplemented method ImmediateFuture became a functional interface which enables lambda expressions.

Alternatively ImmediateFuture could have been an abstract class. This would have prevented the factory from accidentally returning the same instance because it could not have used lambda expressions.

The discussion of abstract classes vs. default methods is not easily resolved so I’m not trying to do it here. But I’ll soon publish a post about default methods and I plan to come back to this. Suffice it to say that the case presented here should be considered when making the decision.

Lambdas in Factories

Because of the unpredictability of a lambda’s reference equality, a factory method should carefully consider using them to create instances. Unless the method’s contract clearly allows for different calls to return the same instance, they should be avoided altogether.

I recommend to include capturing lambdas in this ban. It is not at all clear (to me), under which circumstances the same instance could or will be reused in future JRE versions. One possible scenario would be that the JIT discovers that a tight loop creates suppliers which always (or at least often) return the same instance. By the logic used for non-capturing lambdas, reusing the same supplier instance would be a valid optimization.

Anonymous Classes vs Lambda Expressions

Note the different semantics of an anonymous class and a lambda expression. The former guarantees the creation of new instances while the latter does not. To continue the example, the following implementation of createWithDefaultResult()would lead to the futures– set having a size of four:

public static Future<Integer> createWithDefaultResult() {
	ImmediateFuture<Integer> immediateFuture = new ImmediateFuture<Integer>() {
		@Override
		public Integer get() throws InterruptedException, ExecutionException {
			return 0;
		}
	};
	return immediateFuture;
}

This is especially unsettling because many IDEs allow the automatic conversion from anonymous interface implementations to lambda expressions and vice versa. With the subtle differences between the two this seemingly purely syntactic conversion can introduce subtle behaviour changes. (Something I was not initially aware of.)

In case you end up in a situation where this becomes relevant and chose to use an anonymous class, make sure to visibly document your decision! Unfortunately there seems to be no way to keep Eclipse from converting it anyway (e.g. if conversion is enabled as a save action), which also removes any comment inside the anonymous class.

The ultimate alternative seems to be a (static) nested class. No IDE I know would dare to transform it into a lambda expression so it’s the safest way. Still, it needs to be documented to prevent the next Java-8-fanboy (like yours truly) to come along and screw up your careful consideration.

Functional Interface Identity

Be careful when you rely on the identity of functional interfaces. Always consider the possibility that wherever you’re getting those instances might repeatedly hand you the same one.

But this is of course pretty vague and of little concrete consequence. First, all other interfaces can be reduces to a functional one. This is actually the reason why I picked Future – I wanted to have an example which does not immediately screamCRAZY LAMBDA SHIT GOING ON! Second, this can make you paranoid pretty quickly.

So don’t overthink it – just keep it in mind.

Guaranteed Behaviour

Last but not least (and this is always true but deserves being repeated here):

Do not rely on undocumented behavior!

The JLS does not guarantee that each lambda evaluation returns a new instance (as the code above demonstrates). But it neither guarantees the observed behavior, i.e. that non-capturing lambdas are always represented by the same instance. Hence don’t write code which depends on either.

I have to admit, though, that this is a tough one. Seriously, who looks at the JLS of some feature before using it? I surely don’t.

Reflection

We have seen that Java does not make any guarantees about the identity of evaluated lambda expressions. While this is a valid optimisation, it can have surprising effects. To prevent this from introducing subtle bugs, we derived guidelines:

  • Be careful when partly implementing an interface with default methods.
  • Do not use lambda expressions in factory methods.
  • Use anonymous or, better yet, inner classes when identity matters.
  • Be careful when relying on the identity of functional interfaces.
  • Finally, do not rely on undocumented behaviour!

 

 

 

 

Instances of Non-Capturing Lambdas

About The Author
- Nicolai is a thirty year old boy, as the narrator would put it, who has found his passion in software development. He constantly reads, thinks, and writes about it, and codes for a living as well as for fun. Nicolai is the editor of SitePoint's Java channel, writes The Java 9 Module System with Manning, blogs about software development on codefx.org, and is a long-tail contributor to several open source projects. You can hire him for all kinds of things.

5 Comments

  • Stuart Marks
    Reply

    I think the main issue is that you’re storing Future instances into a Set, and a Set requires a notion of equality to ensure that the contained elements are unique. To me this is suspect right away. What’s the definition of equality for a Future? Since a Future represents (the future result from) an asynchronous task, its notion of equality seems ill-defined to me. As it happens, a lambda has no override of equals(), so the Set ends up using identity for equality, giving rise to the difference between capturing and non-capturing lambdas. But this is a detail that’s secondary to the fundamental semantic issue.

    Note also that this problem can still occur with anonymous or abstract class implementations of ImmediateFuture. Suppose there is a cached instance of an ImmediateFuture that returns zero. A call to createWithResult(0) might decide to return this cached instance instead of creating a fresh instance, giving rise to similarly strange behavior if several instances are stored into a Set.

    • Nicolai Parlog
      Reply

      As it says in several places throughout the article, this is an example. And a contrived one at that. (I even explain why I chose it but since this discussion has come up before I agree that it has not been a good one.) So saying that the main issue is storing a Future in a Set is missing the point.

      The main issue is that lambda expressions have weaker identity guarantees than their alternatives (anonymous or inner classes) and that this is not immediately obvious.

      And that is totally fine. They are meant to express behavior not state, so equality makes little sense for them. At the same time performance can be improved if no guarantees are made which I guess is the reason why they weren’t.

      But this does not change the fact that it is a non-obvious property, which can lead to misunderstandings and errors. And unlike the caching mechanisms you allude to, author or readers of such “factory-lambda”-code may assume that they are creating multiple instances while they are in fact not. All this article is doing is discussing this so that its readers won’t fall into that trap should it ever present itself.

    • Nicolai Parlog
      Reply

      > Why are you expecting different instances of lambdas to start with?

      Because this would be the most natural behavior for Java. You want to create something? You get a new instance. Also note, that he fact that the Future is created by a lambda is hidden from the APIs consumer.

      > What did you want to achieve with that?

      Nothing, I just played around. But I am sure that at some point some developer will want to achieve something and it might fail for non-obvious reasons.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>