Java 然后比较通配符签名



为什么声明看起来像这样:

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>一样扩展吗?

为什么? 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); // 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. 如您所见,strict()在没有显式声明的情况下工作正常,因为T被推断为Integer以匹配局部变量的泛型类型。

  2. 然后,当我们尝试将Supplier<Integer>传递为Supplier<Number>时,它会中断,因为IntegerNumber兼容。

  3. 然后它适用于fluent(),因为? extends NumberInteger兼容的。

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

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

带有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); // compile error
error: incompatible types: Function<Object,B> cannot be converted to Function<? super Object,A>

TL;DR:

Comparator.thenComparing(Function< ? super T, ? extends U > keyExtractor)(您的问题专门询问的方法)可能会以这种方式声明为一种惯用/内部编码约定,出于整个 API 一致性的原因,JDK 开发团队必须遵循该约定。


冗长版

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

该部分对Function必须返回的特定类型施加约束。听起来你已经把那部分写下来了。

然而,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 = …;

/* 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;

…
}

">难道U不能参数化到keyExtractor返回的任何内容,并且仍然Comparable<? super U>一样扩展?...">

我已经通过实验确定Function< ? super T, ? extends U > keyExtractor确实可以重构为限制性更强Function< ? super T, U > keyExtractor,并且仍然可以完美地编译和运行。例如,注释/取消注释我的实验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-上限有界通配符对于涵盖更多用例是必要的

似乎您的问题是关于类型参数的一般问题,因此对于我的回答,为了简单起见,我将在我的答案中将您提供的类型参数与它们所属的类型分开。

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

这并非在所有情况下都有效。类型参数U没有? extends U具有的多功能性和附加类型安全性。通配符是唯一的类型参数,其中参数化类型(使用通配符类型参数)的实例化不像类型参数是具体类型或类型参数那样受类型参数的限制;通配符基本上是占位符,比类型参数和具体类型(用作类型参数时)更通用。java 教程中关于通配符的第一句话是:

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

为了说明这一点,请看一下这个

class A <T> {}

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

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

因此,这应该说明通配符类型参数如何不像具体类型那样限制实例化。但是类型参数呢?使用类型参数的问题最好地体现在方法中。为了说明,请检查此类:

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
}

请注意引用ca是具体类型。现在这在另一个答案中得到了解决,但在另一个答案中没有解决的是类型参数的概念与编译时错误的关系(为什么一个类型参数会导致编译时错误而另一个不会导致),这种关系是为什么有问题的声明是用声明的语法声明的原因。这种关系是通配符提供的附加类型安全性和通用性,而不是某些类型约定。现在为了说明这一点,我们必须给A一个类型参数的成员,所以:

class A<T> { T something; }

在 parameterMethod() 中使用类型参数的危险在于,类型参数可以以强制转换的形式引用,这样就可以访问something成员。

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

这反过来又使堆污染成为可能。通过参数方法的实现,test() 方法中C<Number> c = new C();语句可能会导致堆污染。因此,当具有类型参数的方法从声明类的类型参数中传递任何对象而不进行强制转换时,编译器会发出编译时错误;同样,如果类型参数的成员实例化到任何对象,而无需从类型参数的声明类中进行强制转换,则该成员将发出编译时错误。这里要强调的真正重要的事情是没有强制转换,因为您仍然可以将对象传递给具有类型参数参数的方法,但必须将其转换为该类型参数(或者在本例中,强制转换为包含类型参数的类型)。在我的示例中

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

如果a被强制转换为A<U>,则c.parameterMethod(a)将起作用,因此如果该行看起来像这样c.parameterMethod((A<U>) a);则不会发生编译时错误,但是如果您尝试在调用parameterMethod()后将int变量设置为等于a.something,则会收到运行时 castclassexection 错误(再次, 编译器需要强制转换,因为U可以表示任何内容)。整个场景如下所示:

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
}

因此,由于类型参数可以以强制转换的形式引用,因此将对象从声明类的类型参数中传递到具有类型参数参数或包含类型参数的方法都是非法的。通配符不能以强制转换的形式引用,因此wildMethod(A<? extends U> a)中的a无法访问 A 的 T 成员;由于这种额外的类型安全性,由于使用通配符避免了堆污染的可能性,因此 Java 编译器确实允许在C<Number> c = new C()中的引用 C 调用时将一个没有强制转换的具体类型传递给 wildMethod ;同样,这就是为什么可以将通配符的参数化类型实例化为没有强制转换的具体类型的原因。当我说类型参数的多功能性时,我说的是它们在参数化类型的角色中允许哪些实例化;当我说额外的类型安全时,我说的是无法以规避堆污染的强制转换形式引用通配符。

我不知道为什么有人会强制转换类型参数。但我确实知道开发人员至少会喜欢通配符与类型参数的多功能性。我可能写得令人困惑,或者可能误解了你的问题,在我看来,你的问题似乎是关于一般的类型参数,而不是这个特定的声明。此外,如果声明Function<? super T, ? extends U> keyExtractor中的 keyExtractor 以永远无法访问属于第二类参数Function的成员的方式使用,那么通配符是理想的,因为它们无论如何都无法访问这些成员;那么,为什么开发人员不希望通配符提供此处提到的多功能性呢?这只是一个好处。

最新更新