Java thenComparing wildcard signature

前端 未结 3 1755
广开言路
广开言路 2021-02-05 18:48

Why does the declaration look like this:

default > Comparator thenComparing(
            Function

        
相关标签:
3条回答
  • 2021-02-05 19:15

    It seems like your question is regarding type arguments in general so for my answer I will be separating the type arguments you provided from the types they belong to, in my answer, for simplicity.

    First we should note that a parameterized type of wildcard is unable to access its members that are of the respective type parameter. This is why, in your specific case the ? extends U can be substituted for U and still work fine.

    This won't work in every case. The type argument U does not have the versatility and additional type safety that ? extends U has. Wildcards are a unique type argument in which instantiations of the parameterized types (with wildcard type arguments) are not as restricted by the type argument as they would be if the type argument was a concrete type or type parameter; wildcards are basically place holders that are more general than type parameters and concrete types (when used as type arguments). The first sentence in the java tutorial on wild cards reads:

    In generic code, the question mark (?), called the wildcard, represents an unknown type.

    To illustrate this point take a look at this

    class A <T> {}
    

    now let's make two declarations of this class, one with a concrete type and the other with a wild card and then we'll instantiate them

    A <Number> aConcrete = new A <Integer>(); // Compile time error
    A <? extends Number> aWild = new A<Integer>() // Works fine
    

    So that should illustrate how a wildcard type argument does not restrict the instantiation as much as a concrete type. But what about a type parameter? The problem with using type parameters is best manifested in a method. To illustrate examine this class:

    class C <U> {
        void parameterMethod(A<U> a) {}
        void wildMethod(A<? extends U> a) {}
        void test() {
            C <Number> c = new C();
            A<Integer> a = new A();
            c.parameterMethod(a); // Compile time error
            c.wildMethod(a); // Works fine
        }
    

    Notice how the references c and a are concrete types. Now this was addressed in another answer, but what wasn't addressed in the other answer is how the concept of type arguments relate to the compile time error(why one type argument causes a compile time error and the other doesn't) and this relation is the reason why the declaration in question is declared with the syntax it's declared with. And that relation is the additional type safety and versatility wildcards provide over type parameters and NOT some typing convention. Now to illustrate this point we will have to give A a member of type parameter, so:

    class A<T> { T something; }
    

    The danger of using a type parameter in the parameterMethod() is that the type parameter can be referred to in the form of a cast, which enables access to the something member.

    class C<U> {
        parameterMethod(A<U> a) { a.something = (U) "Hi"; }
    }
    

    Which in turn enables the possibility of heap pollution. With this implementation of the parameterMethod the statement C<Number> c = new C(); in the test() method could cause heap pollution. For this reason, the compiler issues a compile time error when methods with arguments of type parameter are passed any object without a cast from within the type parameters declaring class; equally a member of type parameter will issue a compile time error if it is instantiated to any Object without a cast from within the type parameter's declaring class. The really important thing here to stress is without a cast because you can still pass objects to a method with an argument of type parameter but it must be cast to that type parameter (or in this case, cast to the type containing the type parameter). In my example

        void test() {
            C <Number> c = new C();
            A<Integer> a = new A();
            c.parameterMethod(a); // Compile time error
            c.wildMethod(a); // Works fine
        }
    

    the c.parameterMethod(a) would work if a were cast to A<U>, so if the line looked like this c.parameterMethod((A<U>) a); no compile time error would occur, but you would get a run time castclassexection error if you tried to set an int variable equal to a.something after the parameterMethod() is called (and again, the compiler requires the cast because U could represent anything). This whole scenario would look like this:

        void test() {
            C <Number> c = new C();
            A<Integer> a = new A();
            c.parameterMethod((A<U>) a); // No compile time error cuz of cast
            int x = a.something; // doesn't issue compile time error and will cause run-time ClassCastException error
        }
    

    So because a type parameter can be referenced in the form of a cast, it is illegal to pass an object from within the type parameters declaring class to a method with an argument of a type parameter or containing a type parameter. A wildcard cannot be referenced in the form of a cast, so the a in wildMethod(A<? extends U> a) could not access the T member of A; because of this additional type safety, because this possibility of heap pollution is avoided with a wildcard, the java compiler does permit a concrete type being passed to the wildMethod without a cast when invoked by the reference c in C<Number> c = new C(); equally, this is why a parameterized type of wildcard can be instantiated to a concrete type without a cast. When I say versatility of type arguments, I'm talking about what instantiations they permit in their role of a parameterized type; and when I say additional type safety I'm talking about about the inability to reference wildcards in the form of a cast which circumvents heapPollution.

    I don't know why someone would cast a type parameter. But I do know a developer would at least enjoy the versatility of wildcards vs a type parameter. I may have written this confusingly, or perhaps misunderstood your question, your question seems to me to be about type arguments in general instead of this specific declaration. Also if keyExtractor from the declaration Function<? super T, ? extends U> keyExtractor is being used in a way that the members belonging to Function of the second type parameter are never accessed, then again, wildcards are ideal because they can't possibly access those members anyway; so why wouldn't a developer want the versatility mentioned here that wildcards provide? It's only a benefit.

    0 讨论(0)
  • 2021-02-05 19:17

    TL;DR:

    Comparator.thenComparing(Function< ? super T, ? extends U > keyExtractor) (the method your question specifically asks about) might be declared that way as an idiomatic/house coding convention thing that the JDK development team is mandated to follow for reasons of consistency throughout the API.


    The long-winded version

    …But I don't get this part: Function<? super T, ? extends U>

    That part is placing a constraint on the specific type that the Function must return. It sounds like you got that part down already though.

    The U the Function returns is not just any old U, however. It must have the specific properties (a.k.a „bounds“) declared in the method's parameter section: <U extends Comparable<? super U>>.

    …Why not just have: Function<? super T, U>

    To put it as simply as I can (because I only think of it simply; versus formally): The reason is because U is not the same type as ? extends U.

    Changing Comparable< ? super U > to List< ? super U > and Comparator< T > to Set< T > might make your quandary easier to reason about…

    default < U extends List< ? super U > > Set< T > thenComparing(
        Function< ? super T, ? extends U > keyExtractor ) {
            
        T input = …;
            
        /* Intuitively, you'd think this would be compliant; it's not! */
        /* List< ? extends U > wtf = keyExtractor.apply( input ); */
          
        /* This doesn't comply to „U extends List< ? super U >“ either */
        /* ArrayList< ? super U > key = keyExtractor.apply( input ); */
            
        /* This is compliant because key is a „List extends List< ? super U >“
         * like the method declaration requires of U 
         */
        List< ? super U > key = keyExtractor.apply( input );
            
        /* This is compliant because List< E > is a subtype of Collection< E > */
        Collection< ? super U > superKey = key;
            
        …
    }
    

    Can't the U just parameterize to whatever the keyExtractor returns, and still extend Comparable<? super U> all the same?…

    I have established experimentally that Function< ? super T, ? extends U > keyExtractor could indeed be refactored to the the more restrictive Function< ? super T, U > keyExtractor and still compile and run perfectly fine. For example, comment/uncomment the /*? extends*/ on line 27 of my experimental UnboundedComparator to observe that all of these calls succeed either way…

    …
    Function< Object, A > aExtractor = ( obj )-> new B( );
    Function< Object, B > bExtractor = ( obj )-> new B( ) ;
    Function< Object, C > cExtractor = ( obj )-> new C( ) ;
            
    UnboundedComparator.< Object, A >comparing( aExtractor ).thenComparing( bExtractor );
    UnboundedComparator.< Object, A >comparing( bExtractor ).thenComparing( aExtractor );
    UnboundedComparator.< Object, A >comparing( bExtractor ).thenComparing( bExtractor );
    UnboundedComparator.< Object, B >comparing( bExtractor ).thenComparing( bExtractor );
    UnboundedComparator.< Object, B >comparing( bExtractor ).thenComparing( aExtractor );
    UnboundedComparator.< Object, B >comparing( bExtractor ).thenComparing( cExtractor );
    …
    

    Technically, you could do the equivalent debounding in the real code. From the simple experimentation I've done — on thenComparing() specifically, since that's what your question asks about — I could not find any practical reason to prefer ? extends U over U.

    But, of course, I have not exhaustively tested every use case for the method with and without the bounded ? .

    I would be surprised if the developers of the JDK haven't exhaustively tested it though.

    My experimentation — limited, I admit — convinced me that Comparator.thenComparing(Function< ? super T, ? extends U > keyExtractor) might be declared that way for no other reason than as an idiomatic/house coding convention thing that the JDK development team follows.

    Looking at the code base of the JDK it's not unreasonable to presume that somebody somewhere has decreed: «Wherever there's a Function< T, R > the T must have a lower bound (a consumer/you input something) and the R must have an upper bound (a producer/you get something returned to you)».

    For obvious reasons though, U is not the same as ? extends U. So the former should not be expected to be substitutable for the latter.

    Applying Occam's razor: It's simpler to expect that the exhaustive testing the implementers of the JDK have done has established that the U -upper bounded wildcard is necessary to cover a wider number of use cases.

    0 讨论(0)
  • 2021-02-05 19:21

    Why is it ? extends U and not U?

    Because of code conventions. Check out @deduper's answer for a great explanation.

    Is there any actual difference?

    When writing your code normally, your compiler will infer the correct T for things like Supplier<T> and Function<?, T>, so there is no practical reason to write Supplier<? extends T> or Function<?, ? extends T> when developing an API.

    But what happens if we specify the type manually?

    void test() {
        Supplier<Integer> supplier = () -> 0;
    
        this.strict(supplier); // OK (1)
        this.fluent(supplier); // OK
    
        this.<Number>strict(supplier); // compile error (2)
        this.<Number>fluent(supplier); // OK (3)
    }
    
    <T> void strict(Supplier<T>) {}
    <T> void fluent(Supplier<? extends T>) {}
    
    1. As you can see, strict() works okay without explicit declaration because T is being inferred as Integer to match local variable's generic type.

    2. Then it breaks when we try to pass Supplier<Integer> as Supplier<Number> because Integer and Number are not compatible.

    3. And then it works with fluent() because ? extends Number and Integer are compatible.

    In practice that can happen only if you have multiple generic types, need to explicitly specify one of them and get the other one incorrectly (Supplier one), for example:

    void test() {
        Supplier<Integer> supplier = () -> 0;
        // If one wants to specify T, then they are forced to specify U as well:
        System.out.println(this.<List<?>, Number> supplier);
        // And if U happens to be incorrent, then the code won't compile.
    }
    
    <T, U> T method(Supplier<U> supplier);
    

    Example with Comparator (original answer)

    Consider the following Comparator.comparing method signature:

    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, U> keyExtractor
    )
    

    Also here is some test classes hierarchy:

    class A implements Comparable<A> {
        public int compareTo(A object) { return 0; }
    }
    
    class B extends A { }
    

    Now let's try this:

    Function<Object, B> keyExtractor = null;
    Comparator.<Object, A>comparing(keyExtractor); // compile error
    
    error: incompatible types: Function<Object,B> cannot be converted to Function<? super Object,A>
    
    0 讨论(0)
提交回复
热议问题