5

I am getting the following (confusing) warning when using annotation-based null analysis where an array is involved:

Null type safety (type annotations): The expression of type 'int[]' needs unchecked conversion to conform to 'int @Nullable[]'

This occurs when I pass an unannotated int[] to an int @Nullable[] parameter.

This is surprising. Normally it is only a problem if you take a nullable value and try to pass it to a nonnull parameter, but I am doing the opposite - taking a known non-null (though not annotated) array and passing it to a method (Arrays.equals) which does accept nulls.

Also it does not seem to be a problem with non-array objects. Normally a variable or return of (unannotated, non-array) type T can be assigned to a @Nullable T.

So my question is why does this change when T is an array type?


My class is a hashable/equality-comparable proxy for a native class that uses an int[] (taken from a C++ function) as a unique identifier. My package uses annotation-based null analysis and the 3rd-party package (with the native class) does not.

Here's a cut-down version that shows the problem:

TagKey.java:

package example;

import java.util.Arrays;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

import other.Tag;

import com.google.common.collect.Interner;
import com.google.common.collect.Interners;

public class TagKey {

    private TagKey(Tag tag) {
        this.tag = tag;
    }

    public static TagKey forTag(Tag tag) {
        TagKey candidateKey = new TagKey(tag);
        return INTERNER.intern(candidateKey);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(this.tag.getUniqueIdentifier());
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        } else if (obj == null) {
            return false;
        } else if (getClass() != obj.getClass()) {
            return false;
        } else {
            return ((TagKey) obj).hasMatchingIdentifier(this.tag.getUniqueIdentifier()); // Warning appears here
        }
    }

    public boolean hasMatchingIdentifier(int @Nullable[] id) {
        return Arrays.equals(this.tag.getUniqueIdentifier(), id);
    }

    @SuppressWarnings("null") // Guava cannot use type parameter annotations due to backward compatibility with Java 6
    private static final Interner<TagKey> INTERNER = Interners.newWeakInterner();

    private final Tag tag;

}

package-info.java:

/**
 * @author finnw
 *
 */
@org.eclipse.jdt.annotation.NonNullByDefault
package example;

Tag.java: (partial)

package other;

public class Tag {
    public native int[] getUniqueIdentifier(); // Currently returns a fresh array on each call, but may change in the near future to reuse a single array
}

The workaround I am currently using:

    public boolean hasMatchingIdentifier(@Nullable Object id) {
        return id instanceof int[] &&
               Arrays.equals(this.tag.getUniqueIdentifier(), (int[])id);
    }

I would prefer to avoid these approaches:

  • Adding @SuppressWarnings("null") to TagKey or its equals method (The production version is somewhat more complex and has a high risk of embarrassing NPEs.)

Notes:

  • My JDT version is 3.10.0.v20140606-1215
  • Earlier I made the mistake of declaring @Nullable int[] id. Surprisingly there was no warning message for this, even though it implies that the primitive int elements may be null which is clearly wrong.

2 Answers 2

1
+50

Add this method:

@SuppressWarnings("null")
public static int @Nullable[] getUniqueIdentifier(Tag tag) {
    return tag.getUniqueIdentifier();
}

then:

return ((TagKey) obj).hasMatchingIdentifier(getUniqueIdentifier(this.tag));

This is why I'm ignoring the "unchecked conversion from non-annotated" warning until nullity profiles are supported, you get stuck either suppressing null warnings everywhere (which defeats the point) or making annotated wrapper methods for every library.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, but I don't really want to do this as I am not sure it is an improvement over hasMatchingIdentifier(@Nullable Object). Do you know why it behaves differently for arrays vs. non-array objects?
+1 anyway as I think you are right that ABNA is not mature enough yet for use on production code. But I wonder who will volunteer to create nullity profiles for the whole of the JRE.
Nullity profies, aka external annotations, will be supported in Eclipse Mars (4.5). Nobody uses the entire API of the JRE - adding just a few external annotations will typically go a long way towards a fully null-checked project.
0

Any array being checked by Arrays.equals can handle nulls. You were right before to say @Nullable int[] id because @Nullable doesn't define that the items in the array are nullable, it defines that the parameter - the object reference - can be null. Arrays.equals(null, null) returns true.

private int[] a = null; 

is perfectly reasonable code. So what you're checking for is that the array references match, which is true if all elements match each other, or if both references are null. If a variable is declared as int[], its internal members cannot, by definition, be null. That is, the code above is legal, this is not:

a[0] = null;

It'd be different of course if it was defined as Integer[], but it's not.

1 Comment

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.