为什么 Java 中的泛型类型参数不能有下限?

发布于 2024-10-16 02:41:41 字数 1840 浏览 2 评论 0原文

我认为您不能将 Java 泛型类型参数绑定到下限(即使用 super 关键字)。我正在阅读 Angelika Langer Generics 常见问题解答关于该主题的内容 。他们说这基本上可以归结为无用的下限(“没有任何意义”)。

我不相信。我可以想象它们的用途是帮助您更灵活地处理生成类型化结果的库方法的调用者。想象一个方法,它创建了用户指定大小的数组列表并用空字符串填充它。一个简单的声明是

public static ArrayList<String> createArrayListFullOfEmptyStrings(int i);

但这对您的客户造成了不必要的限制。为什么他们不能像这样调用你的方法:

//should compile
List<Object> l1 = createArrayListFullOfEmptyStrings(5); 
List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
List<String> l3 = createArrayListFullOfEmptyStrings(5);

//shouldn't compile
List<Integer> l4 = createArrayListFullOfEmptyStrings(5);

此时我很想尝试以下定义:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {
  List<T> list = new ArrayList<T>(size);
  for(int i = 0; i < size; i++) {
     list.add("");
  }
  return list;
}

但它不会编译; super 关键字在此上下文中是非法的。

我上面的例子是一个坏例子吗(忽略我下面所说的)?为什么下限在这里没有用?如果它有用,那么 Java 中不允许它的真正原因是什么?

PS

我知道更好的组织可能是这样的:

public static void populateListWithEmptyStrings(List<? super String> list, int size);

List<CharSequence> list = new ArrayList<CharSequence>();
populateListWithEmptyStrings(list, 5);

我们可以为此目的吗?问题假装由于需求,我们需要在一个方法调用中执行这两个操作?

编辑

@Tom G(有道理)询问 List 相对于 List 有什么好处。其一,没有人说返回的列表是不可变的,所以这是一个优点:

List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
l2.add(new StringBuilder("foo").append("bar"));

I gather that you cannot bind a Java generics type parameter to a lower bound (i.e. using the super keyword). I was reading what the Angelika Langer Generics FAQ had to say on the subject. They say it basically comes down to a lower bound being useless ("not making any sense").

I'm not convinced. I can imagine a use for them to help you be more flexible to callers of a library method that produces a typed result. Imagine a method that created an array list of a user-specified size and filled it with the empty string. A simple declaration would be

public static ArrayList<String> createArrayListFullOfEmptyStrings(int i);

But that's unnecessarily restrictive to your clients. Why can't they invoke your method like this:

//should compile
List<Object> l1 = createArrayListFullOfEmptyStrings(5); 
List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
List<String> l3 = createArrayListFullOfEmptyStrings(5);

//shouldn't compile
List<Integer> l4 = createArrayListFullOfEmptyStrings(5);

At this point I would be tempted to try the following definition:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {
  List<T> list = new ArrayList<T>(size);
  for(int i = 0; i < size; i++) {
     list.add("");
  }
  return list;
}

But it will not compile; the super keyword is illegal in this context.

Is my example above a bad example (ignoring what I say below)? Why isn't a lower bound useful here? And if it would be useful, what's the real reason that it is not permitted in Java?

P.S.

I know that a better organization might be something like this:

public static void populateListWithEmptyStrings(List<? super String> list, int size);

List<CharSequence> list = new ArrayList<CharSequence>();
populateListWithEmptyStrings(list, 5);

Can we for the purpose of this question pretend that due to a requirement, we need to do both operations in one method call?

Edit

@Tom G (justifiably) asks what benefit having a List<CharSequence> would have over a List<String>. For one, nobody said the returned list is immutable, so here's one advantage:

List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
l2.add(new StringBuilder("foo").append("bar"));

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

相思碎 2024-10-23 02:41:41

基本上,它不够有用。

我认为您的示例指出了下限的唯一优点,常见问题解答将其称为受限实例化

底线是:“超级”束缚会买给你的全部是限制只有 Number 的超类型可以用作类型参数。 ....

但正如其他帖子所指出的,即使这个功能的用处也可能是有限的。

由于多态性和专业化的本质,上限比常见问题解答中描述的下限有用得多(访问非静态成员类型擦除)。我怀疑下限带来的复杂性不值得其有限的价值。


OP:我想补充一点,我认为你确实表明它很有用,但还不够有用。提出无可辩驳的杀手级用例,我就会支持 JSR。 :-)

Basically, it's not useful enough.

I think your example points out the only advantage of a lower bound, a feature the FAQ calls Restricted Instantiation:

The bottom line is: all that a " super " bound would buy you is the restriction that only supertypes of Number can be used as type arguments. ....

But as the other posts point out, the usefulness of even this feature can be limited.

Due to the nature of polymorphism and specialization, upper bounds are far more useful than lower bounds as described by the FAQ (Access To Non-Static Members and Type Erasure). I suspect the complexity introduced by lower bounds aren't worth its limited value.


OP: I want to add I think you did show it is useful, just not useful enough. Come up with the irrefutable killer use cases and I'll back the JSR. :-)

静若繁花 2024-10-23 02:41:41

例如,规范确实讨论了类型参数的下限

4.10.2

类型变量是其下界的直接超类型。

2010年5月1日

一个新的类型变量......其下界

看起来,如果类型变量是通配符捕获的合成变量,则该类型变量只有一个(非空)下界。如果语言允许所有类型参数的下限怎么办?也许它不会造成很多麻烦,并且排除它只是为了使泛型更简单(嗯...)更新据说对下界类型参数的理论研究尚未彻底进行。

更新:一篇声称下界可以的论文:Daniel Smith 的“Java 类型推断已损坏:我们可以修复它吗”

撤回:以下参数是错误的。 OP的例子是合法的。

你的具体例子不是很有说服力。首先它不是类型安全的。返回的列表确实是一个 List,将其视为其他类型是不安全的。假设您的代码可以编译:

    List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

那么我们可以向其中添加非 String,这是错误的。

    CharSequence chars = new StringBuilder();
    l2.add(chars); 

List 不是这样,而是有点像 CharSequence 列表。您的需求可以通过使用通配符来解决:

public static  List<String> createArrayListFullOfEmptyStrings(int size)  

// a list of some specific subtype of CharSequence 
List<? extends CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

// legal. can retrieve elements as CharSequence
CharSequence chars = l2.get(0);

// illegal, won't compile. cannot insert elements as CharSequence
l2.add(new StringBuilder());

the spec does talk about lower bounds of type parameters, for example

4.10.2

a type variable is a direct supertype of its lower bound.

5.1.10

a fresh type variable ... whose lower bound

It appears that a type variable only has a (non-null) lower bound if it's a synthetic one as result of wildcard capture. What if the language allow lower bounds on all type parameters? Probably it doesn't cause a lot of trouble, and it's excluded only to keep generics simpler (well ...) Update it is said that theoretical investigation of lower bounded type parameters is not thoroughly conducted.

Update: a paper claiming lower bounds are ok: "Java Type Infererence Is Broken: Can We Fix It" by Daniel Smith

RETRACT: the following argument is wrong. OP's example is legitimate.

Your particular example is not very convincing. First it's not type safe. The returned list is indeed a List<String>, it's unsafe to view it as another type. Suppose your code compiles:

    List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

then we can add non-String to it, which is wrong

    CharSequence chars = new StringBuilder();
    l2.add(chars); 

Well a List<String> is not, but somewhat like a list of CharSequence. Your need can be solved by using wildcard:

public static  List<String> createArrayListFullOfEmptyStrings(int size)  

// a list of some specific subtype of CharSequence 
List<? extends CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

// legal. can retrieve elements as CharSequence
CharSequence chars = l2.get(0);

// illegal, won't compile. cannot insert elements as CharSequence
l2.add(new StringBuilder());
山色无中 2024-10-23 02:41:41

这不仅仅是一个答案,这是另一个(可能是杀手?)用例。
我有一个 ModelDecorator 助手。我希望它具有以下公共 API

class ModelDecorator<T>{
    public static <T> ModelDecorator<T> create(Class<T> clazz);
    public <SUPER> T from(SUPER fromInstance);
}

因此,给定类 A、B 扩展 A,它可以像这样使用:

A a = new A();
B b = ModelDecorator.create(B.class).from(a);

但我希望在 T 和 SUPER 上有界限,所以我确保只有子类可以使用 API 实例化。此时,我可以这样做:

C c = new C();
B b = ModelDecorator.create(B.class).from(c);

其中B不继承自C。

显然,如果我可以这样做:

    public <SUPER super T> T from(SUPER fromInstance);

那将解决我的问题。

More than an answer, this is another (possibly killer?) use case.
I have a ModelDecorator helper. I want it to have the following public API

class ModelDecorator<T>{
    public static <T> ModelDecorator<T> create(Class<T> clazz);
    public <SUPER> T from(SUPER fromInstance);
}

So, given classes A, B extends A, it can be used like this:

A a = new A();
B b = ModelDecorator.create(B.class).from(a);

But I want to have bounds on T and SUPER, so I make sure that only subclases can be instantiated using the API. At this moment, I can do:

C c = new C();
B b = ModelDecorator.create(B.class).from(c);

Where B DOES not inherit from C.

Obviously, if I could do:

    public <SUPER super T> T from(SUPER fromInstance);

That would solve my problem.

我还不会笑 2024-10-23 02:41:41

此时输入列表会给您带来什么好处?当您迭代返回的集合时,您仍然应该能够执行以下操作:

for(String s : returnedList) {
CharSequence cs = s;
//do something with your CharSequence
}

What advantage does typing the List give you at that point? When you iterate over the returned collection, you should still be able to do the following:

for(String s : returnedList) {
CharSequence cs = s;
//do something with your CharSequence
}
暮倦 2024-10-23 02:41:41

编辑:我带来了好消息。有一种方法可以得到你想要的大部分东西。

public static <R extends List<? super String>> R createListFullOfEmptyString(IntFunction<R> creator, int size)
{
  R list = creator.apply(size);
  for (int i = 0; i < size; i++)
  {
    list.add("");
  }
  return list;
}

// compiles
List<Object> l1 = createListFullOfEmptyString(ArrayList::new, 5);
List<CharSequence> l2 = createListFullOfEmptyString(ArrayList::new, 5);
List<String> l3 = createListFullOfEmptyString(ArrayList::new, 5);
// doesn't compile
List<Integer> l4 = createListFullOfEmptyString(ArrayList::new, 5);

缺点是客户端确实需要提供 R 的实例来进行变异,或者提供某种构造 R 的方法。没有其他方法可以安全地构造它。

我将保留下面的原始答案以供参考。


总之:

没有充分的理由,只是还没有完成。

并且在此之前,不可能为执行以下所有操作的方法编写具有正确方差的精确类型:

A) 接受或创建参数化数据结构

B) 将计算的(未传入的)值写入该数据结构

C) 返回该数据结构

写入/接受值正是应用逆变的情况,这意味着类型参数数据结构上的下限必须由写入数据结构的值的类型决定。当前在 Java 中表达这一点的唯一方法是在数据结构上使用下限通配符,例如 List。


如果我们正在设计一个像 OP 这样的 API,它可能自然地(但不合法)表达为:

// T is the type of the value(s) being computed and written to the data structure

// Method creates the data structure
<S super T> Container<S> create()

// Method writes to the data structure
<S super T> Container<S> write(Container<S> container)

那么我们可用的选项是:

A) 使用下限通配符,并强制调用者强制转换输出:

// This one is actually useless - there is no type the caller can cast to that is both read- and write-safe.
Container<? super T> create()

// Caller must cast result to the same type they passed in.
Container<? super T> write(Container<? super T> container)

B)过度限制数据结构上的类型参数以匹配正在写入的值的类型,并强制调用者强制转换输入和输出:

// Caller must accept as-is; cannot write values of type S (S super T) into the result.
Container<T> create()

// Caller must cast Container<S> (S super T) to Container<T> before calling, then cast the result back to Container<S>.
Container<T> write(Container<T> container)

C) 使用新的类型参数并在内部执行我们自己的不安全转换:

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> create()

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> write(Container<S> container)

选择你的毒药。

Edit: I bring good news. There is a way to get most of what you want.

public static <R extends List<? super String>> R createListFullOfEmptyString(IntFunction<R> creator, int size)
{
  R list = creator.apply(size);
  for (int i = 0; i < size; i++)
  {
    list.add("");
  }
  return list;
}

// compiles
List<Object> l1 = createListFullOfEmptyString(ArrayList::new, 5);
List<CharSequence> l2 = createListFullOfEmptyString(ArrayList::new, 5);
List<String> l3 = createListFullOfEmptyString(ArrayList::new, 5);
// doesn't compile
List<Integer> l4 = createListFullOfEmptyString(ArrayList::new, 5);

The downside is clients do need to provide either an instance of R to mutate, or some means to construct an R. There is no other way to safely construct it.

I'll retain my original answer below for informational purposes.


In summary:

There is not a good reason, it just has not been done.

And until such time as it is, it will be impossible to write exact types with correct variance for methods that do all of:

A) Accept or create parametrized data structure

B) Write computed (not-passed-in) value(s) to that data structure

C) Return that data structure

Writing/accepting values is exactly the case where contravariance applies, which means the type parameter on the data structure must be lower-bounded by the type of the value being written to the data structure. The only way to express that in Java currently is using a lower-bounded wildcard on the data structure, e.g. List<? super T>.


If we are designing an API such as the OP's, which might naturally (but not legally) be expressed as:

// T is the type of the value(s) being computed and written to the data structure

// Method creates the data structure
<S super T> Container<S> create()

// Method writes to the data structure
<S super T> Container<S> write(Container<S> container)

Then the options available to us are:

A) Use a lower-bounded wildcard, and force callers to cast the output:

// This one is actually useless - there is no type the caller can cast to that is both read- and write-safe.
Container<? super T> create()

// Caller must cast result to the same type they passed in.
Container<? super T> write(Container<? super T> container)

B) Overly restrict the type parameter on the data structure to match the type of the value being written, and force callers to cast the input and output:

// Caller must accept as-is; cannot write values of type S (S super T) into the result.
Container<T> create()

// Caller must cast Container<S> (S super T) to Container<T> before calling, then cast the result back to Container<S>.
Container<T> write(Container<T> container)

C) Use a new type parameter and do our own unsafe casting internally:

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> create()

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> write(Container<S> container)

Pick your poison.

夢归不見 2024-10-23 02:41:41

这会让你几乎到达目标。

static public void main(String...args){
    List<Object> l1 = createArrayListFullOf("", 5);
    List<CharSequence> l2 = createArrayListFullOf("", 5);
    List<String> l3 = createArrayListFullOf("", 5);

    // vvv compile time error vvv
    List<Integer> l4 = createArrayListFullOf("", 5);
}


public static <T, E extends T> List<T> createArrayListFullOf(E value, int size) {
    List<T> list = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        list.add(value);
    }
    return list;
}

我认为这可能更接近您正在寻找的东西。基本上,通过使 E 扩展 T,T 成为 E 的超类。问题是您需要一些 T 类型的引用 - 即需要绑定的 E。在这种情况下,添加了一个绑定到 E 的值填充参数。

This gets you almost there.

static public void main(String...args){
    List<Object> l1 = createArrayListFullOf("", 5);
    List<CharSequence> l2 = createArrayListFullOf("", 5);
    List<String> l3 = createArrayListFullOf("", 5);

    // vvv compile time error vvv
    List<Integer> l4 = createArrayListFullOf("", 5);
}


public static <T, E extends T> List<T> createArrayListFullOf(E value, int size) {
    List<T> list = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        list.add(value);
    }
    return list;
}

I think this maybe closer to what you are looking for. Basically T becomes a super of E by making E extends T. The problem is you need some reference for type for T - namely E that needs to be bound. I this case a value-to-fill parameter is added that binds to E.

樱娆 2024-10-23 02:41:41

嗯,好吧 - 让我们一起解决这个问题。您定义一个方法:

public static列表createArrayListFullOfEmptyStrings(int size) {

这是什么意思?这意味着如果我调用你的方法,那么我会得到一个 String 的某个超类的列表。也许它返回一个字符串列表。也许它返回一个对象列表。我不知道。

凉爽的。

列表<对象> l1 = createArrayListFullOfEmptyStrings(5);

根据您的说法,应该可以编译。但那是不对的!我可以将一个整数放入对象列表中 - l1.add(3) 。但如果你返回一个字符串列表,那么这样做应该是非法的。

列表<字符串> l3 = createArrayListFullOfEmptyStrings(5);

根据您的说法,应该可以编译。但那是不对的! l3.get(1) 应该始终返回一个 String ...但该方法可能返回一个 Object 列表,这意味着 l3.get(1) 可能是一个 Integer。

唯一有效的是

List l5 = createArrayListFullOfEmptyStrings(5);

我所知道的是我可以安全地调用 l4.put("foo"),并且我可以安全地获取 Object o = l4.get (2)

Hmm, ok - let's work with this. You define a method:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {

What does that mean? It means that if I call your method, then I get back a list of some superclass of String. Maybe it returns a list of String. Maybe it returns a list of Object. I don't know.

Cool.

List<Object> l1 = createArrayListFullOfEmptyStrings(5);

According to you, that should compile. But that's not right! I can put an Integer into a list of Object - l1.add(3) . But if you are returning a list of String, then doing that should be illegal.

List<String> l3 = createArrayListFullOfEmptyStrings(5);

According to you, that should compile. But that's not right! l3.get(1) should always return a String ... but that method might have returned a list of Object, meaning that l3.get(1) could conceivably be an Integer.

The only thing that works is

List<? super String> l5 = createArrayListFullOfEmptyStrings(5);

All I know is that I can safely call l4.put("foo"), and I can safely get Object o = l4.get(2) .

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文