7

I know the Java compiler generates different classes for lambda functions depending on the context and closure they have. When I receive the lambda as a parameter (using the Consumer<> class), may I know the lifetime of the parameter?

For example, I have the following Observable class, that keeps a weak reference to its observes.

class Observable {
    private final List<WeakReference<Consumer<Object>>> observables = new ArrayList<>();
    private Object obj;

    public Observable(Object obj){
        this.obj = obj;
    }

    public void observe(Consumer<Object> cons){
        this.observables.add(new WeakReference<>(cons));
    }

    public void set(Object obj){
        this.obj = obj;
        // notify observes
        for(WeakReference<Consumer<Object>> cons : this.observables){
            if(cons.get() != null)
                cons.get().accept(this.obj);
            // clearing the non-existing observes from the list is ommited for simplicity
        }
    }
}

Now I use it as follows.

public class Main {
    public static void main(String[] args) {
        Object c = new Object();
        Observable obs = new Observable(c);

        new ContainingClass(obs);
        obs.set(c);
        System.gc();
        obs.set(c);
    }
}

The code just creates the object and its observer and creates ContainingClass (definition follows) that observes. Then the object is set once, garbage collector explicitly called (so the created ContainingClass is deleted) and set the object a second time.

Now, as long as the lambda is instance-specific (reference either this or its instances method) it's called only once (because it is destroyed by the GC).

public class ContainingClass {
    public ContainingClass(Observable obs){
        obs.observe(this::myMethod);
    }

    private void myMethod(Object obj) {
        System.out.println("Hello here");
    }
}
public class ContainingClass {
    private Object obj;

    public ContainingClass(Observable obs){
        obs.observe(obj -> {
            this.obj = obj;
            System.out.println("Hello here");
        });
    }
}

But as the lambda becomes static, it is called twice, even after GC.

public class ContainingClass {
    public ContainingClass(Observable obs){
        obs.observe((obj) -> System.out.println("Hello here"));
    }
}

The reference to this lambda is never destroyed and therefore add as an observer every time ContainingClass instance is created. As a result, it will be stuck in observers until the program ends.

Is there a way to detect this and at least show a warning, that the lambda will be never removed?

One thing I figured out is that lambda with instance lifetime has arg$1 property, so I can ask about the number of properties.

public void observe(Consumer<Object> cons){
    if(cons.getClass().getDeclaredFields().length == 0)
        System.out.println("It is static lifetime lambda");
    this.observables.add(new WeakReference<>(cons));
}

Is it a universal approach? May there be a situation when this doesn't work?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Patrik Valkovič
  • 706
  • 7
  • 26
  • *"Is it a universal approach?"* You shouldn't make any assumptions about the internal workings. – Olivier Jun 29 '21 at 14:40
  • 3
    To solve your issue, I would add a mechanism to unregister an observer. When you're done with an observer, unregister it. You won't even need weak references anymore. – Olivier Jun 29 '21 at 14:47
  • @Olivier I am aware of that option. In fact, I may return kind of handler that can be used to unregister callback. I may even store weak reference to this callback inside the `Observable` and as soon as its destroyed I may automatically unregister the callback itself. However, this require handling of the handler at the called side and I was looking for a more elegant solution. I am aware I can't depend on the private properties of the callback, that is why I asked the question. – Patrik Valkovič Jun 29 '21 at 16:47
  • 1
    You should not depend on gc by weakreference. you can not know if the consumer is used by other class. – shalk Jul 03 '21 at 03:25
  • 4
    "Garbage collector is explicitly called, so the object is deleted" not quite, `System#gc` provides a _hint_ to the JVM that a garbage collection pass may be performed, but it's just as welcome to do absolutely nothing at all. In terms of reference types that could be useful here, check out `PhantomReference`, the spookiest of all the reference types. – Rogue Jul 03 '21 at 21:12

2 Answers2

1

Your question is similar to this question, so the answers there apply here too.

A static nested class has a flag that can be checked:

 Modifier.isStatic(clazz.getModifiers())   // returns true if a class is static, false if not
kutschkem
  • 7,826
  • 3
  • 21
  • 56
  • _Your question is similar to this question, so the answers there apply here too._ Doesn't that make this question a duplicate? – Abra Jul 06 '21 at 07:15
  • @Abra The question is slightly different but contains all the necessary information. I can't bring myself to actually cast the vote, feel free to check and see if it's similar enough to be a duplicate. – kutschkem Jul 06 '21 at 07:20
1

I think a good solution would be the one hinted by @Olivier: you can return an object with a remove method that removes your Consumer from your list when called, like the following example:

@FunctionalInterface
public interface Registration {
    void remove();
} 
class Observable {
    private final List<Consumer<Object>> observables = new ArrayList<>();
    private Object obj;

    public Observable(Object obj) {
        this.obj = obj;
    }

    public Registration observe(Consumer<Object> cons) {
        this.observables.add(cons);
        return () -> this.observables.remove(cons);
    }

    public void set(Object obj) {
       [...]
    }
}

The alternative would be to check if the class the lambda belongs to is static or not, as suggested by @kutschkem, but I don't like resorting to introspection if there is a good alternative.

As already stated by @shalk, relying on WeakReference to handle GC can lead to unwanted behaviours, because there is no way to ensure that your Consumer isn't referenced (maybe by mistake) somewhere else.

gscaparrotti
  • 663
  • 5
  • 21