7

The JavaDoc for the LambdaMetaFactory of Java 1.8 specifies that lambda capture "may involve allocation of a new function object, or may return an existing function object", but it doesn't specify when and under what circumstances it might choose one way or the other.

Looking at the actual implementation of the LambdaMetaFactory, on the other hand, it is clear that it happens if and only if the lambda expression captures no parameters.

What I'm wondering is, is this behavior actually specified somewhere (outside of the JavaDoc) and can be relied upon? It would be nice to know whether I can rely on a lambda expression's identity being constant or not.

9
  • 1
    I'd hope not. That behavior should be able to change. If you depend on a lambda being constant, then store it. Commented Apr 1, 2014 at 3:59
  • 1
    Well, to be honest, it's more important for me to be able to rely on a capture always returning a different object if it does take parameters, even if the captured parameters happen to be the same as one returned previously. The current specification leaves open the possibility for the metafactory to intern objects on captured parameters, however. Commented Apr 1, 2014 at 4:01
  • I'm not sure I follow why that would be relevant? Commented Apr 1, 2014 at 4:02
  • I often tend to use object factories of various kinds as keys of a map to the objects they have created. Commented Apr 1, 2014 at 4:03
  • ...Why would you do that? Factories don't usually have any kind of useful identity. Commented Apr 1, 2014 at 4:31

2 Answers 2

8

There is essentially no contract that covers the identity of objects that result from evaluating a lambda expression. This is covered in the JLS section 15.27.4, Run-time Evaluation of Lambda Expressions. This section explicitly leaves unspecified the exact behavior of creation vs reuse of lambda objects. The rationale from that section explains this well:

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).

You can, of course, experiment with the implementation, call equals() or use == on lambda objects, put them into IdentityHashMaps, etc., but since these exact behaviors are unspecified, your program may change its behavior (i.e., break) when run on different versions of the JDK or on different implementations of Java SE.

I read the exchange in the comments below the question but I don't really have anything more to offer. Perhaps if you explain what you're trying to do, we could come up with some suggestions for alternatives to using lambdas as keys in a map.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for locating the relevant part of the JLS. Don't worry about finding a solution; I'm already using anonymous classes, and that works. It would just have been nice to shorten them down to lambda expressions.
@StuartMarks: I'm interested in clarifications relevant for one concrete situation: Say that I create a listener with a lambda, save it in a field, and add it as a listener to some component. Can I use the reference in that field to remove the listener? (This assumes that listeners identity are used to removed them.)
@StuartMarks: A more abstract way to state (I think) the same problem: Given that the value of a is created with a lambda, does the following two properties always hold: 1) a == a 2) If a == b, then calling the method a or b will execute exactly equivalent code.
Property 1 implies that some listener will be removed when I try to remove one. Property 2 implies that even if the listener that is removed is created at a different lambda expression, then it will be equivalent to the one I wanted to remove.
@Lii An object reference resulting from a lambda expression has the usual reference identity semantics. References are == to themselves. If you copy a reference around and later compare them, they will be ==, e.g., after Runnable a = () -> { }; Runnable b = a; then a == b. However if you write the "same" lambda, the references might be unequal, e.g., Runnable a = () -> { }; Runnable b = () -> { }; then a and b might or might not be ==. There is no sense of equality for lambdas along the lines of "equivalent code." There is only reference equality.
2

You should separate behavior from identity. Lambdas can be used to implement behavior while a single ordinary class can be used to implement identity by creating instances out of it. The following simplified example derived from your code should illustrate it:

import java.util.function.Function;

public class MeshBuf
{
  {
    // use case 1:
    LayerID<Col> idCol = new LayerID<>(mbuf -> mbuf.new Col());
    // use case 2:
    Attribute attrib=…;
    LayerID<Vec1Layer> idVec1 = new LayerID<>(mbuf->new Vec1Layer(attrib));
    // the expression new LayerID<>(…) is guaranteed to create new instances…
    LayerID<Vec1Layer> idVec2 = new LayerID<>(mbuf->new Vec1Layer(attrib));
    // therefore idVec1 != idVec2 even if referring to the same expression
  }

  // single class is enough for maintaining the identity
  public static final class LayerID<L> {
    private final Function<MeshBuf, L> cons;

    public LayerID(Function<MeshBuf, L> f) {
      cons = f;
    }

    public L cons(MeshBuf buf) {
      return cons.apply(buf);
    }
  }

  // the other classes are taken from your code, unchanged

  public class Col extends Layer<Color> {
    public VertexBuf.ColorArray build(Collection<Color> in) {
        FloatBuffer data = Utils.wfbuf(in.size() * 4);
        for(Color c : in) {
      data.put(c.getRed() / 255.0f);  data.put(c.getGreen() / 255.0f);
      data.put(c.getBlue() / 255.0f); data.put(c.getAlpha() / 255.0f);
        }
        return(new VertexBuf.ColorArray(data));
    }
  }

  public abstract class AttribLayer<T> extends Layer<T> {
    public final Attribute attrib;

    public AttribLayer(Attribute attrib) {
        this.attrib = attrib;
    }
  }

  public class Vec1Layer extends AttribLayer<Float> {
    public Vec1Layer(Attribute attrib) {super(attrib);}

    public VertexBuf.Vec1Array build(Collection<Float> in) {
        FloatBuffer data = Utils.wfbuf(in.size());
        for(Float d : in)
      data.put(d);
        return(new VertexBuf.Vec1Array(data, attrib));
    }
  }
}

1 Comment

Admittedly, that's a good point. I've noticed that Java 8 commonly inverts a lot of my previous assumptions, and I haven't yet completely gotten around to it. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.