Why is this an issue?

A return type containing wildcards cannot be narrowed down in any context. This indicates that the developer’s intention was likely something else.

The core problem lies in type variance. Expressions at an input position, such as arguments passed to a method, can have a more specific type than the type expected by the method, which is called covariance. Expressions at an output position, such as a variable that receives the return result from a method, can have a more general type than the method’s return type, which is called contravariance. This can be traced back to the Liskov substitution principle.

In Java, type parameters of a generic type are invariant by default due to their potential occurrence in both input and output positions at the same time. A classic example of this is the methods T get() (output position) and add(T element) (input position) in interface java.util.List. We could construct cases with invalid typing in List if T were not invariant.

Wildcards can be employed to achieve covariance or contravariance in situations where the type parameter appears in one position only:

However, covariance is ineffective for the return type of a method since it is not an input position. Making it contravariant also has no effect since it is the receiver of the return value which must be contravariant (use-site variance in Java). Consequently, a return type containing wildcards is generally a mistake.

How to fix it

The solution to this problem depends on the original intention of the developer. Given the examples:

List<? extends Animal> getAnimals() { ... } // Noncompliant, wildcard with no use
List<? super Plant> getLifeforms() { ... }  // Noncompliant, wildcard with no use

You can remove the wildcards to make the types invariant:

List<Animal> getAnimals() { ... }           // Compliant, using invariant type instead
List<Plant> getLifeforms() { ... }          // Compliant, using invariant type instead

Or replace them with a super- or subtypes (still invariant):

List<Dog> getAnimals() { ... }              // Compliant, using subtype instead
List<Lifeform> getLifeforms() { ... }       // Compliant, using supertype instead

Resources

Documentation

Articles & blog posts