evant / gradle-retrolambda

A gradle plugin for getting java lambda support in java 6, 7 and android
Apache License 2.0
5.3k stars 449 forks source link

NoClassDefFoundError within anonymous class when proguarded #239

Closed rpattabi closed 7 years ago

rpattabi commented 7 years ago

Problematic code

    private final List<Listener> listeners = new ArrayList<>();

    @Override
    public void addListener(Listener listener) {
        if (listener == null || listeners.contains(listener))
            throw new IllegalStateException();

        listeners.add(listener);
    }

    @Override
    public void removeListener(Listener listener) {
        if (listener == null || !listeners.contains(listener))
            throw new IllegalStateException();

        listeners.remove(listener);
    }

    public interface Listener {
        void started(Exercise exercise);
    }

    private final Listener notifier = new Listener() {
        @Override
        public void started(Exercise exercise) {
            listeners.forEach(listener -> listener.started(exercise)); // <-- throws NoClassDefFoundError (on proguard)
        }
    };

This works

Instead of listeners.forEach(listener -> listener.started(exercise));, if we iterate like below it works even with proguard:

            for (Listener listener : listeners) {
                listener.started(exercise);
            }

Config

classpath 'me.tatarka:gradle-retrolambda:3.5.0' // in project build.gradle
apply plugin: 'me.tatarka.retrolambda' // last entry in app build.gradle

// https://github.com/orfjackal/retrolambda/issues/25
retrolambda {
      jvmArgs '-noverify'
}

retrolambdaConfig 'net.orfjackal.retrolambda:retrolambda:2.5.1' // within app dependencies. Even without this got the same error.
rmtmckenzie commented 7 years ago

I ran into a similar issue with no class def found - I've opened an issue in retrolambda itself rather than the gradle plugin: https://github.com/orfjackal/retrolambda/issues/126

No resolution yet, I've ended up switching to use Jack and proper java 8 (although it doesn't support default methods etc), but I'm running into the same problem with a different lambda there now.

From what I've been reading about the issue when it happens with Jack, it might be something to do with multidex - see https://code.google.com/p/android/issues/detail?id=213483

technoir42 commented 7 years ago

@rpattabi I think you might be getting NoClassDefFoundError because Iterable.forEach takes an instance of Consumer interface which is only available on API 24+. That seems like pretty normal behaviour because Retrolambda does not backport any Java 8 APIs.

rpattabi commented 7 years ago

@zergtmn In the code I gave above, do you mean Listener is consumer interface and iterable<Listener>.forEach is not supported by retrolambda?

If you meant I am using an interface only available on API 24+, that's not the case since Listener is a user defined interface as shown in the code.

rmtmckenzie commented 7 years ago

Are you building for nougat only? Otherwise I'd be very surprised if calling Iterable.forEach works considering it's a default method on an interface - and even the new Jack compiler that does support lambdas doesn't support those on marshmallow or earlier. Retrolambda does it for your own classes but not with library methods. So no, foreach is probably not supported by retrolambda.

I'm actually surprised that android studio doesn't warn you about calling forEach...

What he means is that forEach takes an argument (java.util.function.Consumer<? super T> action), and that for anything less than Nougat the class won't exist; hence the NoClassDefFoundError for your anonymous class as it's trying to implement an interface that doesn't exist.

I've raised an issue about a similar issue on the retrolambda project (https://github.com/orfjackal/retrolambda/issues/126), except not for foreach.

If you really want to use foreach you could copy the implementation of Consumer to your own interface class and implement a helper, something like

public interface Consumer<T> {
    void accept(T t);
}

public class ArrayUtils {
    public static <T> void forEach(Iterable<T> iterable, Consumer<? super T> action) {
        for (T t : iterable) {
            action.accept(t);
        }
    }
}
rpattabi commented 7 years ago

Are you building for nougat only?

No. I have min sdk: 16 and target sdk: 24.

forEach takes an argument (java.util.function.Consumer<? super T> action), and that for anything less than Nougat the class won't exist

I see what you mean. This code compiles which is surprising to me. I guess it is due to latest build tools I am using.

Thanks for the work around. I will close this issue since retrolambda or gradle-retrolambda doesn't have anything to do here.