Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

May I query lambda function lifetime at runtime?

Tags:

java

lambda

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?

like image 803
Patrik Valkovič Avatar asked Jun 23 '21 14:06

Patrik Valkovič


2 Answers

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.

like image 126
gscaparrotti Avatar answered Oct 08 '22 21:10

gscaparrotti


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
like image 27
kutschkem Avatar answered Oct 08 '22 21:10

kutschkem