Java中的`thenComparing`通配符签名

huangapple go评论75阅读模式
英文:

Java thenComparing wildcard signature

问题

为什么声明看起来是这样的:

default <U extends Comparable<? super U>> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor)

我理解其中大部分内容。U 可以是任何类型,只要它可以与其超类进行比较,从而也可以与自身进行比较,这是有道理的。

但是我不太明白这部分:Function<? super T, ? extends U>

为什么不直接使用:Function<? super T, U>

难道 U 不可以参数化为 keyExtractor 返回的任何类型,并且仍然满足 Comparable<? super U> 吗?

英文:

Why does the declaration look like this:

default &lt;U extends Comparable&lt;? super U&gt;&gt; Comparator&lt;T&gt; thenComparing(
            Function&lt;? super T, ? extends U&gt; keyExtractor)

I understand most of it. It makes sense that U can be anything as long as it's comparable to a superclass of itself, and thus also comparable to itself.

But I don't get this part: Function&lt;? super T, ? extends U&gt;

Why not just have: Function&lt;? super T, U&gt;

Can't the U just parameterize to whatever the keyExtractor returns, and still extend Comparable&lt;? super U&gt; all the same?

答案1

得分: 16

为什么是 ? extends U 而不是 U

这是由于代码约定。查看@deduper的回答中有一个很好的解释。

是否有任何实际区别?

在正常编写代码时,编译器会为诸如 Supplier<T>Function<?, T> 这样的情况推断正确的 T,因此在开发 API 时,没有实际上编写 Supplier<? extends T> 或者 Function<?, ? extends T> 的实际理由。

但是如果我们手动指定类型会发生什么呢?

void test() {
    Supplier<Integer> supplier = () -> 0;

    this.strict(supplier); // 正常 (1)
    this.fluent(supplier); // 正常

    this.<Number>strict(supplier); // 编译错误 (2)
    this.<Number>fluent(supplier); // 正常 (3)
}

<T> void strict(Supplier<T>) {}
<T> void fluent(Supplier<? extends T>) {}
  1. 如您所见,strict() 在没有显式声明的情况下可以正常工作,因为 T 被推断为 Integer 以匹配局部变量的泛型类型。

  2. 当我们试图将 Supplier<Integer> 作为 Supplier<Number> 传递时,会出现问题,因为 IntegerNumber 不兼容

  3. 在使用 fluent() 时,它可以正常工作,因为 ? extends NumberInteger 兼容

在实践中,只有在您有多个泛型类型,并且需要显式指定其中一个类型并错误地获得另一个类型(Supplier 类型)时,才会发生这种情况,例如:

void test() {
    Supplier<Integer> supplier = () -> 0;
    // 如果想要指定 T,那么就必须同时指定 U:
    System.out.println(this.<List<?>, Number> supplier);
    // 如果 U 是不正确的,那么代码就无法编译。
}

<T, U> T method(Supplier<U> supplier);

使用 Comparator 的示例(原始回答)

考虑以下 Comparator.comparing 方法签名:

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

这里还有一些测试类的层次结构:

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

class B extends A { }

现在让我们尝试一下:

Function<Object, B> keyExtractor = null;
Comparator.<Object, A>comparing(keyExtractor); // 编译错误
错误不兼容的类型无法将 Function<Object,B> 转换为 Function<? super Object,A>
英文:

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&lt;T&gt; and Function&lt;?, T&gt;, so there is no practical reason to write Supplier&lt;? extends T&gt; or Function&lt;?, ? extends T&gt; when developing an API.

But what happens if we specify the type manually?

void test() {
    Supplier&lt;Integer&gt; supplier = () -&gt; 0;

    this.strict(supplier); // OK (1)
    this.fluent(supplier); // OK

    this.&lt;Number&gt;strict(supplier); // compile error (2)
    this.&lt;Number&gt;fluent(supplier); // OK (3)
}

&lt;T&gt; void strict(Supplier&lt;T&gt;) {}
&lt;T&gt; void fluent(Supplier&lt;? extends T&gt;) {}
  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&lt;Integer&gt; as Supplier&lt;Number&gt; 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&lt;Integer&gt; supplier = () -&gt; 0;
    // If one wants to specify T, then they are forced to specify U as well:
    System.out.println(this.&lt;List&lt;?&gt;, Number&gt; supplier);
    // And if U happens to be incorrent, then the code won&#39;t compile.
}

&lt;T, U&gt; T method(Supplier&lt;U&gt; supplier);

Example with Comparator (original answer)

Consider the following Comparator.comparing method signature:

public static &lt;T, U extends Comparable&lt;? super U&gt;&gt; Comparator&lt;T&gt; comparing(
    Function&lt;? super T, U&gt; keyExtractor
)

Also here is some test classes hierarchy:

class A implements Comparable&lt;A&gt; {
	public int compareTo(A object) { return 0; }
}

class B extends A { }

Now let's try this:

Function&lt;Object, B&gt; keyExtractor = null;
Comparator.&lt;Object, A&gt;comparing(keyExtractor); // compile error
error: incompatible types: Function&lt;Object,B&gt; cannot be converted to Function&lt;? super Object,A&gt;

答案2

得分: 8

TL;DR:

Comparator.thenComparing(Function<? super T, ? extends U> keyExtractor) 这个方法可能是以这种方式声明的,作为一种惯用的/内部编码约定,JDK开发团队被要求遵循这种一致性的原因,确保API的一致性。


较详细的版本

"...但是我不明白这部分:Function<? super T, ? extends U>..."

这部分对*Function返回的特定类型施加了约束*。听起来你已经理解了这部分。

然而,U 并不是普通的 U。它必须拥有在方法参数部分声明的特定属性(也称为“边界”):<U extends Comparable<? super U>>

"...为什么不只是这样:Function<? super T, U>..."

简单地说(因为我只是简单地考虑,而不是正式地考虑):原因是 U 不是 ? extends U 类型

Comparable<? super U> 更改为 List<? super U>,将 Comparator<T> 更改为 Set<T>,可能会更容易理解你的困惑...

default <U extends List<? super U>> Set<T> thenComparing(
    Function<? super T, ? extends U> keyExtractor ) {
        
    T input = ;
        
    /* 直观上,你可能认为这是符合的;但实际上不是! */
    /* List<? extends U> wtf = keyExtractor.apply( input ); */
      
    /* 这也不符合 “U extends List<? super U>” */
    /* ArrayList<? super U> key = keyExtractor.apply( input ); */
        
    /* 这是符合的,因为 key 是一个“List extends List<? super U>”,就像方法声明对 U 所要求的那样 */
    List<? super U> key = keyExtractor.apply( input );
        
    /* 这是符合的,因为 List<E> 是 Collection<E> 的子类型 */
    Collection<? super U> superKey = key;
        
    
}

"...U 不仅可以参数化为 keyExtractor 返回的任何类型,而且仍然可以扩展 Comparable<? super U> 吗?..."

根据我进行的实验(链接)Function<? super T, ? extends U> 实际上可以被改写成 更严格Function<? super T, U>,并且仍然可以正常编译和运行。例如,在我的实验性UnboundedComparator中,对第 27 行的 /*? extends*/ 进行注释/取消注释,观察所有这些调用都可以正常运行...


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);

从技术上讲,你可以实际代码中进行等效的解限定。根据我做的简单实验——特别是关于thenComparing(),因为这是你的问题中特别询问的部分——我没有找到更喜欢 ? extends U 而不是 U 的实际原因。

当然,我没有对该方法的每种使用情况都进行详尽测试,有可能在不带有界的 ? 的情况下进行测试。我会感到惊讶,如果 JDK的开发人员 没有对此进行详尽测试的话。

我的实验(我承认,有限)使我相信 Comparator.thenComparing(Function<? super T, ? extends U> keyExtractor) 之所以以这种方式声明,可能只是因为JDK开发团队遵循了一种惯用的/内部编码约定。

JDK的代码库来看,假设某人曾经这样规定过:在任何地方 有一个 Function<T, R>T 必须 有一个下界(一个消费者/你提供了一些输入),而 R 必须 有一个上界(一个生产者/你得到了一些返回的东西)*。

出于显而易见的原因,U 并不等同于 ? extends U。因此,前者不能被期望可以替代后者。

奥卡姆剃刀原理可以预期 JDK 实现者进行的详尽测试已经确定 U - 上界通配符对于涵盖更多的用例是必要的

英文:

TL;DR:

Comparator.thenComparing(Function&lt; ? super T, ? extends U &gt; 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&lt;? super T, ? extends U&gt;

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: &lt;U extends Comparable&lt;? super U&gt;&gt;.

> „…Why not just have: Function&lt;? super T, U&gt;

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&lt; ? super U &gt; to List&lt; ? super U &gt; and Comparator&lt; T &gt; to Set&lt; T &gt; might make your quandary easier to reason about…

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

> „Can't the U just parameterize to whatever the keyExtractor returns, and still extend Comparable&lt;? super U&gt; all the same?…

I have established experimentally that Function&lt; ? super T, ? extends U &gt; keyExtractor could indeed be refactored to the the more restrictive Function&lt; ? super T, U &gt; 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&lt; Object, A &gt; aExtractor = ( obj )-&gt; new B( );
Function&lt; Object, B &gt; bExtractor = ( obj )-&gt; new B( ) ;
Function&lt; Object, C &gt; cExtractor = ( obj )-&gt; new C( ) ;
        
UnboundedComparator.&lt; Object, A &gt;comparing( aExtractor ).thenComparing( bExtractor );
UnboundedComparator.&lt; Object, A &gt;comparing( bExtractor ).thenComparing( aExtractor );
UnboundedComparator.&lt; Object, A &gt;comparing( bExtractor ).thenComparing( bExtractor );
UnboundedComparator.&lt; Object, B &gt;comparing( bExtractor ).thenComparing( bExtractor );
UnboundedComparator.&lt; Object, B &gt;comparing( bExtractor ).thenComparing( aExtractor );
UnboundedComparator.&lt; Object, B &gt;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 experimentationlimited, I admit — convinced me that Comparator.thenComparing(Function&lt; ? super T, ? extends U &gt; 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&lt; T, R &gt; 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.

答案3

得分: 1

似乎你的问题涉及通用类型参数,所以为了简单起见,我的回答将从你提供的类型参数中分离出它们所属的类型。

首先我们应该注意到,带有通配符的参数化类型无法访问其属于相应类型参数的成员。这就是为什么在你的特定情况下,? extends U 可以被替换为 U 并且仍然能正常工作。

然而,并非每种情况都适用。类型参数 U 并没有 ? extends U 那样的多样性和额外的类型安全性。通配符是一种独特的类型参数,实例化带有通配符类型参数的参数化类型(带通配符类型参数的实例化)不受类型参数限制,就像如果类型参数是具体类型或类型参数一样受限制;通配符基本上是比类型参数和具体类型(作为类型参数使用时)更一般的占位符。Java 泛型教程中的通配符部分 第一句话如下:

> 在通用代码中,问号(?)称为通配符,表示未知类型。

为了说明这一点,请看下面的示例:

class A&lt;T&gt; {}

现在让我们做两个声明,一个使用具体类型,另一个使用通配符,然后我们将实例化它们:

A&lt;Number&gt; aConcrete = new A&lt;Integer&gt;(); // 编译时错误
A&lt;? extends Number&gt; aWild = new A&lt;Integer&gt;(); // 可以编译

这样就说明了通配符类型参数并不像具体类型那样限制实例化。但是类型参数呢?使用类型参数的问题最好体现在方法中。请看下面的示例:

class C&lt;U&gt; {
    void parameterMethod(A&lt;U&gt; a) {}
    void wildMethod(A&lt;? extends U&gt; a) {}
    void test() {
        C&lt;Number&gt; c = new C&lt;Number&gt;();
        A&lt;Integer&gt; a = new A&lt;Integer&gt;();
        c.parameterMethod(a); // 编译时错误
        c.wildMethod(a); // 可以编译
    }

注意,引用 ca 是具体类型。尽管这个问题在另一个回答中有所讨论,但另一个回答没有涉及到类型参数引发的编译时错误与为什么另一种类型参数不会引发错误之间的关系,而这种关系正是相关声明以其特定语法声明的原因。这种关系是通配符相对于类型参数提供的额外类型安全性和多样性,而不是某种类型约定。为了说明这一点,我们需要给 A 添加一个类型参数的成员,如下:

class A&lt;T&gt; { T something; }

parameterMethod() 中使用类型参数的危险在于类型参数可以被引用为一种类型转换,从而可以访问 something 成员。

class C&lt;U&gt; {
    parameterMethod(A&lt;U&gt; a) { a.something = (U) &quot;Hi&quot;; }
}

这反过来可能导致堆污染的可能性。使用参数化方法的这种实现方式,test() 方法中的语句 C&lt;Number&gt; c = new C&lt;Number&gt;(); 可能会导致堆污染。因此,当类型参数的方法参数传递任何对象而没有从类型参数所在的类中进行转换时,编译器会发出编译时错误;同样,如果类型参数的成员被实例化为任何对象而没有从类型参数的声明类中进行转换,也会发生编译时错误。在这里需要强调的是“没有转换”,因为你仍然可以将对象传递给具有类型参数参数的方法,但必须将其转换为该类型参数(或在这种情况下,转换为包含类型参数的类型)。在我的示例中:

    void test() {
        C&lt;Number&gt; c = new C&lt;Number&gt;();
        A&lt;Integer&gt; a = new A&lt;Integer&gt;();
        c.parameterMethod(a); // 编译时错误
        c.wildMethod(a); // 可以编译
    }

如果将 a 转换为 A&lt;U&gt;,则 c.parameterMethod(a) 将可以工作,因此如果该行像这样 c.parameterMethod((A&lt;U&gt;) a);,则不会发生编译时错误,但是在调用 parameterMethod() 后,如果尝试将一个 int 变量设置为 a.something,则会导致运行时的 ClassCastException 错误(再次强调,编译器要求转换,因为 U 可以表示任何类型)。整个情景如下:

    void test() {
        C&lt;Number&gt; c = new C&lt;Number&gt;();
        A&lt;Integer&gt; a = new A&lt;Integer&gt;();
        c.parameterMethod((A&lt;U&gt;) a); // 没有编译时错误,因为有转换
        int x = a.something; // 不会引发编译时错误,但会导致运行时 ClassCastException 错误
    }

因为类型参数可以被引用为类型转换的形式,所以在类型参数的声明类内部,从该类传递对象给具有类型参数参数的方法或包含类型参数的方法是不合法的。通配符不能被引用为类型转换的形式,因此 wildMethod(A&lt;? extends U&gt; a) 中的 a 无法访问 A 的 T 成员;由于具有通配符可以避免这种堆污染的可能性,因此当通过引用 `

英文:

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 &lt;T&gt; {}

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 &lt;Number&gt; aConcrete = new A &lt;Integer&gt;(); // Compile time error
A &lt;? extends Number&gt; aWild = new A&lt;Integer&gt;() // 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 &lt;U&gt; {
    void parameterMethod(A&lt;U&gt; a) {}
    void wildMethod(A&lt;? extends U&gt; a) {}
    void test() {
        C &lt;Number&gt; c = new C();
        A&lt;Integer&gt; 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&lt;T&gt; { 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&lt;U&gt; {
    parameterMethod(A&lt;U&gt; a) { a.something = (U) &quot;Hi&quot;; }
}

Which in turn enables the possibility of heap pollution. With this implementation of the parameterMethod the statement C&lt;Number&gt; 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 &lt;Number&gt; c = new C();
        A&lt;Integer&gt; 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&lt;U&gt;, so if the line looked like this c.parameterMethod((A&lt;U&gt;) 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 &lt;Number&gt; c = new C();
        A&lt;Integer&gt; a = new A();
        c.parameterMethod((A&lt;U&gt;) a); // No compile time error cuz of cast
        int x = a.something; // doesn&#39;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&lt;? extends U&gt; 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&lt;Number&gt; 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&lt;? super T, ? extends U&gt; 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.

huangapple
  • 本文由 发表于 2020年8月27日 07:56:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/63607274.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定