3

I'm writing a utility method to extract an array of wrapped unparcelable objects:

public interface UnparcelableHolder<U> {
    @Nullable U getUnparcelable();
}

public final class FragmentUtil {
    @Nullable
    public static <U> List<U> getUnparcelableHolderListArgument(
            @Nonnull Fragment fragment,
            @Nonnull Class<UnparcelableHolder<U>> unparcelableHolderClass,
            @Nonnull String key
    ) {
        @Nullable final Bundle arguments = fragment.getArguments();
        if (arguments == null) {
            return null;
        } else {
            @Nullable final Parcelable[] parcelableArray = arguments.getParcelableArray(key);
            if (parcelableArray == null) {
                return null;
            } else {
                return Arrays
                        .stream(parcelableArray)
                        .filter(unparcelableHolderClass::isInstance)
                        .map(unparcelableHolderClass::cast)
                        .filter(Objects::nonNull)
                        .map(UnparcelableHolder::getUnparcelable)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toList());
            }
            if (unparcelableHolderClass.isInstance(parcelable)) {
                @Nonnull final UnparcelableHolder<U> unparcelableHolder =
                        Objects.requireNonNull(unparcelableHolderClass.cast(parcelable));
                return unparcelableHolder.getUnparcelable();
            } else {
                return null;
            }
        }
    }
}

Android Studio is warning me that my .map(UnparcelableHolder::getUnparcelable) call might cause a NullPointerException. This shouldn't be possible because of my preceding filter(Objects::nonNull) call. How do I tell Android Studio's inspector that my code is clean?

This is an MCVE is available on github built with Android Studio 3.4 beta 2:

build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.github.hborders.streamsnonnulljsr305"
        minSdkVersion 28
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.google.code.findbugs:jsr305:3.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

MainActivity.java:

package com.github.hborders.streamsnonnulljsr305;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class MainActivity extends AppCompatActivity {

    class Foo {
        @Nonnull
        private final String string;

        Foo(@Nonnull String string) {
            this.string = string;
        }

        @Nonnull
        String getString() {
            return string;
        }
    }

    class Bar {
        @Nullable
        private final Foo foo;

        Bar(@Nullable Foo foo) {
            this.foo = foo;
        }

        @Nullable
        Foo getFoo() {
            return foo;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final Bar bar1 = new Bar(new Foo("foo"));
        final Bar bar2 = new Bar(null);
        final Bar[] bars = new Bar[]{
                null,
                bar1,
                bar2,
        };
        final List<String> strings = Arrays
                .stream(bars)
                .map(Bar::getFoo)
                .filter(Objects::nonNull)
                .map(Foo::getString)
                .collect(Collectors.toList());
        System.out.println("strings: " + strings);

    }
}

The same problem occurs on the .map(Foo::getString) call. Ironically, Android studio doesn't complain about my .map(Bar::getFoo) call despite that definitely throwing a NullPointerException.

Heath Borders
  • 30,998
  • 16
  • 147
  • 256
  • What is the signature of `getUnparcelable()`? – CommonsWare Jan 25 '19 at 12:04
  • I declared it in the above snippet, but I just noticed I got the nullability wrong when I transcribed it. I edited the post. This is the signature: `public interface UnparcelableHolder { @Nullable U getUnparcelable(); }` – Heath Borders Jan 25 '19 at 16:50
  • I'm not certain whether it is complaining about `getUnparcelable()` being called on a `null` object, or that `getUnparcelable()` might return a `null` object. Try temporarily changing the signature to `@NotNull U getUnparcelable();` and see if the Lint warning changes. If it does, then it is complaining about the return value from `getUnparcelable()`. If it does not, then you're probably just running into a Lint limitation, in that it has no way to know that `filter()` in your case happens to return non-`null` objects. – CommonsWare Jan 25 '19 at 17:05
  • I think it's complaining about `getUnparcelable()` being called on a `null` because the `.map(unparcelableHolderClass::cast)` can also return `null`, and it doesn't complain about that. – Heath Borders Jan 25 '19 at 21:10

1 Answers1

2

It's an Android Studios bug as none of Android Studios suggestions work here.

No warning:

No Warning

Apply suggestion and get the warning:

Warning

It also suggests to insert .filter(Objects::nonNull) step when it is already there.

Insert step

So it's a definite AS bug.

This is a true MCVE for this issue:

import android.support.annotation.Nullable; // or any nullable you care to use

import java.util.Arrays;
import java.util.Objects;

public class MCVE {

    class Foo {
    }

    class Bar {
        @Nullable
        private final Foo foo;

        Bar(@Nullable Foo foo) {
            this.foo = foo;
        }

        @Nullable
        Foo getFoo() {
            return foo;
        }
    }

    public void mcve() {
        final Bar[] bars = new Bar[]{
                new Bar(new Foo()),
        };
        Arrays.stream(bars)
                .map(Bar::getFoo)
                .filter(Objects::nonNull)
                .map(Foo::toString);
    }
}
weston
  • 54,145
  • 21
  • 145
  • 203