Home > Net >  Java generics: getting rid unchecked cast warning (case described inside)
Java generics: getting rid unchecked cast warning (case described inside)

Time:04-29

Please forgive the bad naming, the problem described below is a very crude simplification and alteration of real code

I have the following types defined:

interface Bundle {
  // omitted
}
interface Content<T extends Bundle> {
  void useBundle(T bundle);
  Class<? extends T> getSupportedBundleClass();
  // omitted
}
class Service<T extends Bundle> {
  private final T bundle;
  
  public Collection<Content<? super T>> someMethod(Collection<Object> allContents) {
    // omitted
  }  
}

The purpose of someMethod in Service is to filter such instances of Content whose supported bundle class is either the same as the bundle's class or higher up the hierarchy.

For example:

Given the hierarchy of Bundle...

interface Bundle {}
interface A extends Bundle {}
class B implements A {}

... and this set-up, ...

Content<Bundle> contentBundle = // omitted
Content<A> contentA = // omitted
Content<B> contentB = // omitted

Service<Bundle> serviceBundle = // omitted
Service<A> serviceA = // omitted
Service<B> serviceB = // omitted

List<Object> allContents = List.of(contentBundle, contentA, contentB);

... the following is true:

serviceBundle.someMethod(allContents) => [contentBundle]
serviceA.someMethod(allContents) => [contentBundle, contentA]
serviceB.someMethod(allContents) => [contentBundle, contentA, contentB]

Here is how someMethod is implemented (with the helper method to do the casting and resolve the wildcard):

public Collection<Content<? super T>> someMethod(Collection<Object> allContents) {
  return allContents.stream()
    .map(obj -> obj instanceof Content ? (Content<? extends Bundle>) obj : null)   // cast #1
    .filter(Objects:nonNull)
    .map(this::castOrNull)
    .filter(Objects::nonNull)
    .collect(Collectors.toList());
}

private <U extends Bundle> Content<? super T> castOrNull(Content<U> content) {
  return content.getSupportedBundleClass().isInstance(bundle) ? (Content<? super T>) content : null;   // cast #2
}  

My reasoning in the castOrNull method is that if the bundle in the Service class is an instance of the supported bundle class of the provided Content instance, then the provided Content's type parameter is a supertype ofT (bundle's type).

someMethod works correctly, as expected (described in the example above). However, I get the unchecked cast warning Content<U> to Content<? super T> on the line with the comment **// cast #2** (the line with **// cast #1** is fine). Is there any way to eliminate the warning (apart from, of course, suppressing it)?

CodePudding user response:

This unchecked cast is unavoidable. You are casting something that doesn't involve a type variable to something that does. To check the validity of this cast, the JVM would need to know what type the type variable T holds. It unfortunately doesn't. No matter which intermediate types you cast the Object to, you are going to introduce the type variable T at some point, and that is when the runtime cannot check the validity of the cast, and the cast would be marked as an unchecked cast - the runtime won't do anything about this cast, and it might be possible that this is actually invalid, and everything will blow up later down the line.

Of course, you can introduce your own checks, such as

content.getSupportedBundleClass().isInstance(bundle)

But note that this is very different from the check that the JVM would have done, for checking the validity for (Content<? super T>) content, had the type of T been available at runtime. In particular, your check depends on implementers of Content implementing getSupportedBundleClass correctly, returning the enclosing class, and not some subclass of the enclosing class:

class ContentBundle implements Content<Bundle> {
    @Override
    public void useBundle(Bundle bundle) {
        
    }

    @Override
    public Class<? extends Bundle> getSupportedBundleClass() {
        return B.class;
    }
}

This wouldn't be a problem if getSupportedBundleClass returned Class<T> instead.

I don't know how the field bundle gets initialised so I don't know whether this could happen or not, but your check also depends on Service.bundle always being an instance of T, and not a subclass of T. If serviceBundle.bundle actually stores an instance of B, then serviceBundle.someMethod(allContents) would include all 3 contents.

I'd suggest that you use a Class<T> to store the type information:

private final Class<T> bundleType;
public Service(Class<T> bundleType) {
    this.bundleType = bundleType;
}

and use isAssignableFrom rather than isInstance.

If you think your own checks suffices (for example, if you are sure that everyone would implement getSupportedBundleClass in the expected way), then you are doing the job of checking the cast, and you should not worry about the JVM not being able to check it. It is fine to suppress it.

In most situations, the solution to the JVM not being to check a cast is to just check it yourself :)

  • Related