Skip to content

Commit

Permalink
Reannotate ClassToInstanceMap and TypeToInstanceMap to use `Class<@…
Browse files Browse the repository at this point in the history
…nonnull ...>`.

(prompted by the mention of `Class` types in cl/519736884)

This lets us express that they can contain null values.

See discussion in jspecify/jspecify#86 (comment)

RELNOTES=n/a
PiperOrigin-RevId: 526184065
  • Loading branch information
cpovirk authored and Google Java Core Libraries committed Apr 22, 2023
1 parent edabd32 commit 2b98d3c
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.google.errorprone.annotations.DoNotMock;
import java.util.Map;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* A map, each entry of which maps a Java <a href="http://tinyurl.com/2cmwkz">raw type</a> to an
Expand All @@ -30,43 +32,29 @@
* <p>Like any other {@code Map<Class, Object>}, this map may contain entries for primitive types,
* and a primitive type and its corresponding wrapper type may map to different values.
*
* <p>This class's support for {@code null} requires some explanation: From release 31.0 onward,
* Guava specifies the nullness of its types through annotations. In the case of {@code
* ClassToInstanceMap}, it specifies that both the key and value types are restricted to
* non-nullable types. This specification is reasonable for <i>keys</i>, which must be non-null
* classes. This is in contrast to the specification for <i>values</i>: Null values <i>are</i>
* supported by the implementation {@link MutableClassToInstanceMap}, even though that
* implementation and this interface specify otherwise. Thus, if you use a nullness checker, you can
* safely suppress any warnings it produces when you write null values into a {@code
* MutableClassToInstanceMap}. Just be sure to be prepared for null values when reading from it,
* since nullness checkers will assume that values are non-null then, too.
*
* <p>See the Guava User Guide article on <a href=
* "https://github.com/google/guava/wiki/NewCollectionTypesExplained#classtoinstancemap">{@code
* ClassToInstanceMap}</a>.
*
* <p>To map a generic type to an instance of that type, use {@link
* com.google.common.reflect.TypeToInstanceMap} instead.
*
* @param <B> the common supertype that all entries must share; often this is simply {@link Object}
* @author Kevin Bourrillion
* @param <B> the common supertype that all values will share. When in doubt, just use {@link
* Object}, or use {@code @Nullable Object} to allow null values.
* @since 2.0
*/
@DoNotMock("Use ImmutableClassToInstanceMap or MutableClassToInstanceMap")
@GwtCompatible
@ElementTypesAreNonnullByDefault
// If we ever support non-null projections (https://github.com/jspecify/jspecify/issues/86),
// we might annotate this as...
// ClassToInstanceMap<B extends @Nullable Object> extends Map<Class<? extends @Nonnull B>, B>
// ...and change its methods similarly (<T extends @Nonnull B> or Class<@Nonnull T>).
public interface ClassToInstanceMap<B> extends Map<Class<? extends B>, B> {
public interface ClassToInstanceMap<B extends @Nullable Object>
extends Map<Class<? extends @NonNull B>, B> {
/**
* Returns the value the specified class is mapped to, or {@code null} if no entry for this class
* is present. This will only return a value that was bound to this specific class, not a value
* that may have been bound to a subtype.
*/
@CheckForNull
<T extends B> T getInstance(Class<T> type);
<T extends @NonNull B> T getInstance(Class<T> type);

/**
* Maps the specified class to the specified value. Does <i>not</i> associate this value with any
Expand All @@ -77,5 +65,5 @@ public interface ClassToInstanceMap<B> extends Map<Class<? extends B>, B> {
*/
@CanIgnoreReturnValue
@CheckForNull
<T extends B> T putInstance(Class<T> type, T value);
<T extends B> T putInstance(Class<@NonNull T> type, @ParametricNullness T value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.io.Serializable;
import java.util.Map;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.NonNull;

/**
* A {@link ClassToInstanceMap} whose contents will never change, with many other important
Expand All @@ -37,7 +38,9 @@
@Immutable(containerOf = "B")
@GwtIncompatible
@ElementTypesAreNonnullByDefault
public final class ImmutableClassToInstanceMap<B> extends ForwardingMap<Class<? extends B>, B>
// TODO(b/278589132): Remove the redundant "@NonNull" on B once it's no longer required by J2KT.
public final class ImmutableClassToInstanceMap<B>
extends ForwardingMap<Class<? extends @NonNull B>, B>
implements ClassToInstanceMap<B>, Serializable {

private static final ImmutableClassToInstanceMap<Object> EMPTY =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
Expand All @@ -41,76 +42,82 @@
* "https://github.com/google/guava/wiki/NewCollectionTypesExplained#classtoinstancemap">{@code
* ClassToInstanceMap}</a>.
*
* <p>This implementation <i>does</i> support null values, despite how it is annotated; see
* discussion at {@link ClassToInstanceMap}.
*
* @author Kevin Bourrillion
* @since 2.0
*/
@J2ktIncompatible
@GwtIncompatible
@SuppressWarnings("serial") // using writeReplace instead of standard serialization
@ElementTypesAreNonnullByDefault
public final class MutableClassToInstanceMap<B> extends ForwardingMap<Class<? extends B>, B>
public final class MutableClassToInstanceMap<B extends @Nullable Object>
extends ForwardingMap<Class<? extends @NonNull B>, B>
implements ClassToInstanceMap<B>, Serializable {

/**
* Returns a new {@code MutableClassToInstanceMap} instance backed by a {@link HashMap} using the
* default initial capacity and load factor.
*/
public static <B> MutableClassToInstanceMap<B> create() {
return new MutableClassToInstanceMap<B>(new HashMap<Class<? extends B>, B>());
public static <B extends @Nullable Object> MutableClassToInstanceMap<B> create() {
return new MutableClassToInstanceMap<B>(new HashMap<Class<? extends @NonNull B>, B>());
}

/**
* Returns a new {@code MutableClassToInstanceMap} instance backed by a given empty {@code
* backingMap}. The caller surrenders control of the backing map, and thus should not allow any
* direct references to it to remain accessible.
*/
public static <B> MutableClassToInstanceMap<B> create(Map<Class<? extends B>, B> backingMap) {
public static <B extends @Nullable Object> MutableClassToInstanceMap<B> create(
Map<Class<? extends @NonNull B>, B> backingMap) {
return new MutableClassToInstanceMap<B>(backingMap);
}

private final Map<Class<? extends B>, B> delegate;
private final Map<Class<? extends @NonNull B>, B> delegate;

private MutableClassToInstanceMap(Map<Class<? extends B>, B> delegate) {
private MutableClassToInstanceMap(Map<Class<? extends @NonNull B>, B> delegate) {
this.delegate = checkNotNull(delegate);
}

@Override
protected Map<Class<? extends B>, B> delegate() {
protected Map<Class<? extends @NonNull B>, B> delegate() {
return delegate;
}

static <B> Entry<Class<? extends B>, B> checkedEntry(final Entry<Class<? extends B>, B> entry) {
return new ForwardingMapEntry<Class<? extends B>, B>() {
/**
* Wraps the {@code setValue} implementation of an {@code Entry} to enforce the class constraint.
*/
private static <B extends @Nullable Object> Entry<Class<? extends @NonNull B>, B> checkedEntry(
final Entry<Class<? extends @NonNull B>, B> entry) {
return new ForwardingMapEntry<Class<? extends @NonNull B>, B>() {
@Override
protected Entry<Class<? extends B>, B> delegate() {
protected Entry<Class<? extends @NonNull B>, B> delegate() {
return entry;
}

@Override
public B setValue(B value) {
@ParametricNullness
public B setValue(@ParametricNullness B value) {
return super.setValue(cast(getKey(), value));
}
};
}

@Override
public Set<Entry<Class<? extends B>, B>> entrySet() {
return new ForwardingSet<Entry<Class<? extends B>, B>>() {
public Set<Entry<Class<? extends @NonNull B>, B>> entrySet() {
return new ForwardingSet<Entry<Class<? extends @NonNull B>, B>>() {

@Override
protected Set<Entry<Class<? extends B>, B>> delegate() {
protected Set<Entry<Class<? extends @NonNull B>, B>> delegate() {
return MutableClassToInstanceMap.this.delegate().entrySet();
}

@Override
public Iterator<Entry<Class<? extends B>, B>> iterator() {
return new TransformedIterator<Entry<Class<? extends B>, B>, Entry<Class<? extends B>, B>>(
public Iterator<Entry<Class<? extends @NonNull B>, B>> iterator() {
return new TransformedIterator<
Entry<Class<? extends @NonNull B>, B>, Entry<Class<? extends @NonNull B>, B>>(
delegate().iterator()) {
@Override
Entry<Class<? extends B>, B> transform(Entry<Class<? extends B>, B> from) {
Entry<Class<? extends @NonNull B>, B> transform(
Entry<Class<? extends @NonNull B>, B> from) {
return checkedEntry(from);
}
};
Expand Down Expand Up @@ -140,14 +147,14 @@ public Object[] toArray() {
@Override
@CanIgnoreReturnValue
@CheckForNull
public B put(Class<? extends B> key, B value) {
public B put(Class<? extends @NonNull B> key, @ParametricNullness B value) {
return super.put(key, cast(key, value));
}

@Override
public void putAll(Map<? extends Class<? extends B>, ? extends B> map) {
Map<Class<? extends B>, B> copy = new LinkedHashMap<>(map);
for (Entry<? extends Class<? extends B>, B> entry : copy.entrySet()) {
public void putAll(Map<? extends Class<? extends @NonNull B>, ? extends B> map) {
Map<Class<? extends @NonNull B>, B> copy = new LinkedHashMap<>(map);
for (Entry<? extends Class<? extends @NonNull B>, B> entry : copy.entrySet()) {
cast(entry.getKey(), entry.getValue());
}
super.putAll(copy);
Expand All @@ -156,19 +163,19 @@ public void putAll(Map<? extends Class<? extends B>, ? extends B> map) {
@CanIgnoreReturnValue
@Override
@CheckForNull
public <T extends B> T putInstance(Class<T> type, T value) {
public <T extends B> T putInstance(Class<@NonNull T> type, @ParametricNullness T value) {
return cast(type, put(type, value));
}

@Override
@CheckForNull
public <T extends B> T getInstance(Class<T> type) {
public <T extends @NonNull B> T getInstance(Class<T> type) {
return cast(type, get(type));
}

@CanIgnoreReturnValue
@CheckForNull
private static <B, T extends B> T cast(Class<T> type, @CheckForNull B value) {
private static <B, T extends B> T cast(Class<@NonNull T> type, @CheckForNull B value) {
return Primitives.wrap(type).cast(value);
}

Expand All @@ -181,10 +188,10 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException
}

/** Serialized form of the map, to avoid serializing the constraint. */
private static final class SerializedForm<B> implements Serializable {
private final Map<Class<? extends B>, B> backingMap;
private static final class SerializedForm<B extends @Nullable Object> implements Serializable {
private final Map<Class<? extends @NonNull B>, B> backingMap;

SerializedForm(Map<Class<? extends B>, B> backingMap) {
SerializedForm(Map<Class<? extends @NonNull B>, B> backingMap) {
this.backingMap = backingMap;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,47 +27,45 @@
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* A mutable type-to-instance map. See also {@link ImmutableTypeToInstanceMap}.
*
* <p>This implementation <i>does</i> support null values, despite how it is annotated; see
* discussion at {@link TypeToInstanceMap}.
*
* @author Ben Yu
* @since 13.0
*/
@ElementTypesAreNonnullByDefault
public final class MutableTypeToInstanceMap<B> extends ForwardingMap<TypeToken<? extends B>, B>
implements TypeToInstanceMap<B> {
public final class MutableTypeToInstanceMap<B extends @Nullable Object>
extends ForwardingMap<TypeToken<? extends @NonNull B>, B> implements TypeToInstanceMap<B> {

private final Map<TypeToken<? extends B>, B> backingMap = Maps.newHashMap();
private final Map<TypeToken<? extends @NonNull B>, B> backingMap = Maps.newHashMap();

@Override
@CheckForNull
public <T extends B> T getInstance(Class<T> type) {
public <T extends @NonNull B> T getInstance(Class<T> type) {
return trustedGet(TypeToken.of(type));
}

@Override
@CheckForNull
public <T extends B> T getInstance(TypeToken<T> type) {
public <T extends @NonNull B> T getInstance(TypeToken<T> type) {
return trustedGet(type.rejectTypeVariables());
}

@Override
@CanIgnoreReturnValue
@CheckForNull
public <T extends B> T putInstance(Class<T> type, T value) {
public <T extends B> T putInstance(Class<@NonNull T> type, @ParametricNullness T value) {
return trustedPut(TypeToken.of(type), value);
}

@Override
@CanIgnoreReturnValue
@CheckForNull
public <T extends B> T putInstance(TypeToken<T> type, T value) {
return trustedPut(type.rejectTypeVariables(), value);
public <T extends B> T putInstance(TypeToken<@NonNull T> type, @ParametricNullness T value) {
return this.<T>trustedPut(type.rejectTypeVariables(), value);
}

/**
Expand All @@ -81,7 +79,7 @@ public <T extends B> T putInstance(TypeToken<T> type, T value) {
@Override
@DoNotCall("Always throws UnsupportedOperationException")
@CheckForNull
public B put(TypeToken<? extends B> key, B value) {
public B put(TypeToken<? extends @NonNull B> key, @ParametricNullness B value) {
throw new UnsupportedOperationException("Please use putInstance() instead.");
}

Expand All @@ -94,37 +92,39 @@ public B put(TypeToken<? extends B> key, B value) {
@Deprecated
@Override
@DoNotCall("Always throws UnsupportedOperationException")
public void putAll(Map<? extends TypeToken<? extends B>, ? extends B> map) {
public void putAll(Map<? extends TypeToken<? extends @NonNull B>, ? extends B> map) {
throw new UnsupportedOperationException("Please use putInstance() instead.");
}

@Override
public Set<Entry<TypeToken<? extends B>, B>> entrySet() {
public Set<Entry<TypeToken<? extends @NonNull B>, B>> entrySet() {
return UnmodifiableEntry.transformEntries(super.entrySet());
}

@Override
protected Map<TypeToken<? extends B>, B> delegate() {
protected Map<TypeToken<? extends @NonNull B>, B> delegate() {
return backingMap;
}

@SuppressWarnings("unchecked") // value could not get in if not a T
@CheckForNull
private <T extends B> T trustedPut(TypeToken<T> type, T value) {
private <T extends B> T trustedPut(TypeToken<@NonNull T> type, @ParametricNullness T value) {
return (T) backingMap.put(type, value);
}

@SuppressWarnings("unchecked") // value could not get in if not a T
@CheckForNull
private <T extends B> T trustedGet(TypeToken<T> type) {
private <T extends @NonNull B> T trustedGet(TypeToken<T> type) {
return (T) backingMap.get(type);
}

private static final class UnmodifiableEntry<K, V> extends ForwardingMapEntry<K, V> {
private static final class UnmodifiableEntry<K, V extends @Nullable Object>
extends ForwardingMapEntry<K, V> {

private final Entry<K, V> delegate;

static <K, V> Set<Entry<K, V>> transformEntries(Set<Entry<K, V>> entries) {
static <K, V extends @Nullable Object> Set<Entry<K, V>> transformEntries(
Set<Entry<K, V>> entries) {
return new ForwardingSet<Map.Entry<K, V>>() {
@Override
protected Set<Entry<K, V>> delegate() {
Expand Down Expand Up @@ -157,7 +157,8 @@ public Object[] toArray() {
};
}

private static <K, V> Iterator<Entry<K, V>> transformEntries(Iterator<Entry<K, V>> entries) {
private static <K, V extends @Nullable Object> Iterator<Entry<K, V>> transformEntries(
Iterator<Entry<K, V>> entries) {
return Iterators.transform(entries, UnmodifiableEntry::new);
}

Expand All @@ -171,7 +172,8 @@ protected Entry<K, V> delegate() {
}

@Override
public V setValue(V value) {
@ParametricNullness
public V setValue(@ParametricNullness V value) {
throw new UnsupportedOperationException();
}
}
Expand Down
Loading

0 comments on commit 2b98d3c

Please sign in to comment.