Search code examples
javasonarlintpecs

Java possibly requiring unnecessary extends keyword in PECS construction


I have a code example where Java forces me to use the extends keyword on a class that is final in a PECS construction. I do not understand why exactly. The code can be found below or at https://onecompiler.com/java/3yvf4g97f. The compiler fails to compile method named process. If I remove method process then SonarLint will trigger a violation of rule https://rules.sonarsource.com/java/RSPEC-4968 for method named processPecs. Which seems reasonable, or is this a false positive? Is there a better approach for "processing arbitrary data"? Do I need to suppress SonarLint's warning here and file a bug report for SonarLint?

import java.util.List;

class PecsTest {
  static final class DataContainer<D> {
  
      final D data;
      
      public DataContainer(D data) {
          this.data = data;
      }    
  
      D getData() {
          return data;
      }
  }
  
  static class Processor<D> {
  
      @SuppressWarnings("unchecked")
      List<DataContainer<D>> processPecs(List<? extends DataContainer<? super D>> list) {
        return (List<DataContainer<D>>) list;
      }
      
      @SuppressWarnings("unchecked")
      List<DataContainer<D>> process(List<DataContainer<? super D>> list) {
        return (List<DataContainer<D>>) list;
      }
  }
  
  static class Data {
  }
  
  static class ExtendedData extends Data {
  }
  
  public static void main(String[] args) {
    new Processor<Data>().processPecs(List.of(new DataContainer<>(new ExtendedData())));
    new Processor<ExtendedData>().processPecs(List.of(new DataContainer<>(new Data())));
  }
}

Solution

  • A DataContainer<ExtendedData> is not assignable to DataContainer<Data>. But since DataContainer is a producer, it makes sense to use ? extends Data here (the PE in PECS), as both types can produce a value of type Data:

    DataContainer<ExtendedData> a = new DataContainer<>(new ExtendedData());
    DataContainer<Data> b = new DataContainer<>(new Data());
    
    DataContainer<? extends Data> a2 = a, b2 = b;
    
    Data d1 = a2.getData(), d2 = b2.getData();
    

    In other words, both, DataContainer<ExtendedData> and DataContainer<Data>, are subtypes of DataContainer<? extends Data>. The fact that the class DataContainer is final is irrelevant.

    So, the Sonar warning is wrong here. Suggesting to avoid final classes as upper bound only makes sense if the final class is not generic.


    The same reasoning about DataContainer<ExtendedData> not being assignable to DataContainer<Data> despite ExtendedData is a subclass of Data applies to a List of generic types.

    DataContainer<ExtendedData> is a subtype of DataContainer<? extends Data> but a List<DataContainer<ExtendedData> is not assignable to List<DataContainer<? extends Data>>.

    You have to resort to ? extends when the list acts as a producer, i.e. allows you to get DataContainer<? extends Data> elements from it.

    List<DataContainer<ExtendedData>> list1
        = Collections.singletonList(new DataContainer<>(new ExtendedData()));
    List<DataContainer<Data>> list2
        = Collections.singletonList(new DataContainer<>(new Data()));
    
    // List<DataContainer<? extends Data>> list3 = list1; // does not compile
    // List<DataContainer<? extends Data>> list4 = list2; // neither does this
    
    // both lists can act as producer of DataContainer<? extends Data>
    List<? extends DataContainer<? extends Data>> list3 = list1; 
    Data d3 = list3.get(0).getData();
    List<? extends DataContainer<? extends Data>> list4 = list2;
    Data d4 = list4.get(0).getData();
    

    Note that since DataContainer<Data> and DataContainer<ExtendedData> are distinct types, neither being a subtype of the other, there is no way to define a common consumer type for List<DataContainer<Data>> and List<DataContainer<ExtendedData>> that allows to add elements of both types.


    Keep in mind that with invocation type inference, results may seem to contradict these rules at the first glance. For example, when you write

    List<DataContainer<Data>> list
        = Collections.singletonList(new DataContainer<>(new ExtendedData()));
    

    you get no error. Not because you can add a DataContainer<ExtendedData> to a List<DataContainer<Data> but because in this case, the compiler inferred DataContainer<Data> as the type to construct. And passing an instance of ExtendedData to the constructor of DataContainer<Data> is valid.