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
.
It's an Android Studios bug as none of Android Studios suggestions work here.
No warning:
Apply suggestion and get the warning:
It also suggests to insert .filter(Objects::nonNull)
step when it is already there.
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);
}
}