关于Java中通用返回类型的混淆。

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

Confusion about generic return type in Java

问题

"Generic has always been a topic of fear for me.
Recently I came across a code snippet while learning the retrofit in Android

public static S createService(Class serviceClass) {
return retrofit.create(serviceClass);
}

Can anybody, in depth, explain me meaning of this 'S' part to me ?
Especially why the '' is added to the left of S ?"

英文:

Generic has always been a topic of fear for me.
Recently I came across a code snippet while learning the retrofit in Android

public static <S> S createService(Class<S> serviceClass) {
        return retrofit.create(serviceClass);
    }

Can anybody, in depth, explain me meaning of this <S>S part to me ?
Especially why the <S> is added to the left of S ?

答案1

得分: 1

The <S> 告诉编译器,在编译对该方法的调用时,将会有一些类型需要编译器推断,这些类型是该方法将要操作的类型。

S 是方法的返回值,与通常一样。它告诉编译器将返回的类型。

Class<S> 定义了参数的类型。例如,如果类型恰好是 String,则参数的类型将是 Class<String>(也可以写为 String.class,这是您可能更习惯看到的形式)。

例如,如果您有一个名为 CustomerService 的类,并且要像这样调用该方法:createService(CustomerService.class),因为 CustomerService.class 的类型是 Class<CustomerService>,参数是 Class<S>,编译器将推断 SCustomerService,因此期望该方法返回这个类型。

英文:

The <S> tells the compiler that when it is compiling calls to the method, there will be some type that the compiler will have to infer, which is the type that the method is going to operate on.

The S is the return value of the method, as usual. It tells the compiler what type will be returned.

Class<S> defines the type of the parameter. For instance, if the type happens to be String, the parameter would be of type Class<String> (which can also be written as String.class, a form you may be more accustomed to seeing it in.)

For example, if you had a class, CustomerService, and you were to call the method like this: createService(CustomerService.class), since the type of CustomerService.class is Class<CustomerService> and the parameter is Class<S>, the compiler would infer that S is CustomerService and thus would expect the method to return that.

答案2

得分: 0

<S> 是该方法的类型参数列表(在这种情况下只有一个)。类型参数列表的存在使其成为泛型方法。在方法的头部和体部,符号 S 代表了与给定方法调用相关联的类型参数,无论是根据推断还是明确指定的。

泛型方法的返回类型出现在类型参数列表和方法名称之间。在示例中,返回类型为 S,即类型参数,因此方法的给定调用的返回类型是该调用的类型参数。通常,它将从方法参数中推断出来(这正是提供 Class<S> 作为方法参数的目的)。

没有类型参数列表(<S>)的情况下,该方法将是一个普通方法,而不是泛型方法。在这种情况下,S 需要是编译器可以解析的特定类型的名称。参数需要是表示类型 S 的特定 Class 对象,而方法将(需要)返回类型为 S 的实例。

主要区别在于,对于泛型方法,所有这些属性都由调用的上下文在每次调用时确定,而对于普通方法,它们在编译时一次性确定,适用于所有调用。当然,细节非常重要。泛型方法需要一种方式来满足在运行时返回指定类型的实例的要求。类型为 Class<S> 的参数可以有所帮助,而更一般地说,只有依赖于其参数或返回 null 的情况下,泛型方法才能以其类型参数的方式返回实例。

此外,方法的泛型性与类/接口的泛型性是独立的。

  • 非泛型类可以拥有泛型方法。实际上,大多数泛型方法都处于这种情况下。
  • 泛型类可以拥有普通方法,这是常规情况。方法使用 其类的 类型参数并不使该方法成为泛型方法。
  • 泛型类也可以拥有泛型方法。您可以始终识别此类泛型方法,因为它们具有自己的类型参数列表。
英文:

The <S> is the list of type parameters for the method (only one in this case). The presence of a type-parameter list makes this a generic method. Within the method header and body, the symbol S represents the type argument associated with a given invocation of that method, whether inferred or specified explicitly.

A generic method's return type appears between the type-parameter list and the method name. In the example, this is given as S, the type parameter, so the return type of a given invocation of the method is that invocation's type argument. Typically, it would be inferred from the method argument (and that's exactly the purpose of providing a Class<S> as a method parameter).

Without the type-parameter list (<S>), the method would be an ordinary one, not a generic one. In that case, S would need to be the name of a specific type that the compiler could resolve. The argument would need to be the particular Class object representing type S, and the method would (need to) return an instance of type S.

The main difference is that with a generic method, all those properties are determined on a per-invocation basis by the context of the invocation, whereas with the ordinary method, they are determined once, at compile time, for all invocations. Of course, the devil is in the details. The generic method needs a way to deliver on the requirement of returning an instance of a type specified at runtime. The argument of type Class<S> can help, and more generally, a generic method that returns an instance of its type argument can do so only by relying on its arguments or by returning null.

Also, method genericity is orthogonal to class / interface genericity.

  • Classes that are not generic can have generic methods. In fact, this is the situation for most generic methods.
  • Generic classes can have ordinary methods, and that's the norm. That a method uses its class' type parameters does not make that method generic.
  • And generic classes can also have generic methods. You can recognize such generic methods, as always, because they bear their own type-parameter lists.

答案3

得分: 0

以下是您要翻译的内容:

Generics are just variables, except these variables are references to types, whereas 'normal' variables reference values.

Given:

Number n;
n = Integer.valueOf(5);
System.out.println(n);

A few things are happening:

  1. Number n; declares a new variable. It says to the system: I decree there is some variable. Variables aren't values (they hold a value, a nuanced difference - Integer.valueOf(5), that is a value). This variable furthermore has a limitation assigned to it: It must only ever be holding a value that has the property that it was created either as new Number(), or as new Something(), where Something has Number somewhere in its type hierarchy. That last bit is very important (after all, Number is abstract; new Number() is not possible at all).

  2. In stark contrast, the second and third line do not declare anything. They merely use what has already been declared.

Type variables work the same way.

Given:

public static <S> S createService(Class<S> serviceClass) {
     return retrofit.create(serviceClass);
}

The <S> part declares a new type variable. It is the typevar equivalent of writing Object s;. It declares a variable without assigning any value to it. If you had tried to write:

public static S createService(Class<S> serviceClass) { ... }

The compiler would complain exactly the same way it complains when you write:

void foo() {
 n = 5; // n? What is n? I have no idea what is going on!
}

You have to declare before you can use them. Just <S> says there is no limit - it's short for <S extends Object>, and every reference type extends Object, hence, S can be anything (except primitives - generics and primitives don't currently (JDK21 and older) work together, maybe in some future java version that will change though).

You could have just as easily written:

public <S extends Number> createService(Class<S> serviceClass) { .. }

Which is just fine; that restricts S to refer only to either Number, or Integer, or Double, or any other type that has Number in its list of supertypes (interface or class, does not matter).

Now, things get a little different. The one that sets this variable is the caller, not you. The caller decides what S is. You (the one writing the method) merely specify the type restriction. In that sense, it's exactly like a method parameter, where you (the method author) decide what type it has to be, but the caller picks the value for it.

And that is not exactly the right 'view' - at runtime you can't ask what S is (the 'values' of typevars are erased by the compiler; in the class file, it just isn't there at all).

Given that generics are erased, the only real point to generics is to link things. Imagine I want a method that simply prints the int value of any number, and then just returns it, i.e. that I can write:

Integer i = printAsInt(Integer.valueOf(5));

Note how this is different from:

Integer i = Integer.valueOf(5);
printAsInt(i);

Because that second snippet is two lines - to 'compress it down to one', we need the printNumber method to not just print that number, it also needs to return the parameter verbatim. We can trivially write this, of course:

public Number printAsInt(Number n) {
  System.out.println(n.intValue());
  return n;
}

But this actually doesn't work! If we try to now write:

Integer i = printAsInt(Integer.valueOf(5));

javac will complain: The printAsInt method guarantees that it returns a Number. Yes, we can see it returns its input, but you are free to change that method's contents later on and assuming you stick to the contract that is not considered 'backwards incompatible' (and 'return any Number' is part of the contract, not necessarily 'return the input').

So, we want a way in the type system to link things: To say, the type of the parameter is.. dunno. But whatever it is, the return type? It's the same type.

And hence, this is the right way to write printAsInt:

public <N extends Number> N printAsInt(N number) {
  System.out.println(number.intValue());
  return number;
}

Yes, that means if you use a declared type var 0 or 1 times total, it's a hack or a useless type var. Needs to show up in at least 2 places. (printAsInt has it in 2 places: Return type, and type of its parameter).

Your snippet:

public static <S> S createService(Class<S> serviceClass) {
     return retrofit.create(serviceClass);
}

So now we know how to read this thing:

Caller, you can call this method by picking some type (any type is fine - S has no bounds), as well as picking a value for the only parameter I have. We linked some things: Whatever you picked for S? You need to provide a value whose type is Class<S> - in practice, if you pick String, the only thing you can pass is String.class. I will return - the type you picked.

Where to declare typevars:

You can legally declare a type var in one of two places:

On a type declaration - looks like public class Foo<T extends Number> {} - here T declares a new type var, and it exists for all non-static methods and fields (and even non-static inner classes) defined anywhere inside that type.

On a method or constructor - looks like public <T extends Number> void foo() - exists only for that method.

Your snippet is an example of the second bullet. Something like Java's own java.util.List is an example of the first - it is declared as public interface List<E> {} and that E shows up everywhere - add(E elem), E get(int idx), Iterator<E> iterator(), and many more.

Care - variance!

Java itself is covariant. That means a type is a perfectly fine stand-in for any of its supertypes. If you have a method void foo(Number n) {}, you are free to call it with foo(i) where i is declared as Integer i; - because Integer is a perfectly fine stand-in for Number as Number is a supertype.

Generics are not like that. Because math/universe. For example, given a List<Number>, that has a method .add(Number n), and we just said that this means I can invoke .add(someInteger). And thus also .add(someDouble). But, if I can write:

List<Integer> ints = new ArrayList<Integer>();
List<Number> numbers = ints; // same list!
numbers

<details>
<summary>英文:</summary>

generics are just variables, except, these variables are references to __types__ whereas &#39;normal&#39; variables reference __values__.

Given:

Number n;
n = Integer.valueOf(5);
System.out.println(n);


A few things are happening:

1. `Number n;` _declares a new variable_. It says to the system: I decree there is some variable. Variables aren&#39;t values (they __hold__ a value, a nuanced difference - `Integer.valueOf(5)`, that _is_ a value). This variable furthermore has a limitation assigned to it: It must only ever be holding a value that has the property that it was created either as `new Number()`, or as `new Something()`, where `Something` has `Number` somewhere in its type hierarchy. That last bit is very important (after all, `Number` is abstract; `new Number()` is not possible at all).

2. In stark contrast, the second and third line do not _declare_ anything. They merely _use_ what has already been declared.

__Type variables work the same way__.

Given:

public static <S> S createService(Class<S> serviceClass) {
return retrofit.create(serviceClass);
}


The `&lt;S&gt;` part __declares__ a new type variable. it is the typevar equivalent of writing `Object s;`. It declares a variable without assigning any value to it. If you had tried to write:

public static S createService(Class<S> serviceClass) { ... }


The compiler would complain exactly the same way it complains when you write:

void foo() {
n = 5; // n? What is n? I have no idea what is going on!
}


You have to declare before you can use them. Just `&lt;S&gt;` says there is no limit - it&#39;s short for `&lt;S extends Object&gt;`, and every ref type extends Object, hence, S can be anything (except primitives - generics and primitives don&#39;t currently (JDK21 and older) work together, maybe in some future java version that will change though).

You could have just as easily written:

public <S extends Number> createService(Class<S> serviceClass) { .. }


Which is just fine; that restricts `S` to refer only to either `Number`, or `Integer`, or `Double`, or any other type that has `Number` in its list of supertypes (interface _or_ class, does not matter).

Now, things get a little different. The one that _sets_ this variable is the caller, _not_ you. The caller decides what S is. You (the one writing the method) merely specifies the type restriction. In that sense it&#39;s exactly like a method parameter, where you (the method author) decide what type it has to be, but the caller picks the value for it.

And that is not exactly the right &#39;view&#39; - at runtime you can&#39;t ask what S is (the &#39;values&#39; of typevars are erased by the compiler; in the class file it just isn&#39;t there, at all).

Given that generics are erased, the only real point to generics is __to link things__. Imagine I want a method that simply prints the int value of any number, and then just returns it, i.e. that I can write:

Integer i = printAsInt(Integer.valueOf(5));


Note how this is different from:

Integer i = Integer.valueOf(5);
printAsInt(i);


Because that second snippet is two lines - to &#39;compress it down to one&#39;, we need the `printNumber` method to not just print that number, it also needs to _return the parameter verbatim_. We can trivially write this, of course:

public Number printAsInt(Number n) {
System.out.println(n.intValue());
return n;
}


But this actually doesn&#39;t work! If we try to now write:

Integer i = printAsInt(Integer.valueOf(5));


`javac` will complain: The `printAsInt` method guarantees that it returns a `Number`. Yes, __we__ can see it returns its input, but you are free to change that method&#39;s contents later on and assuming you stick to the contract that is not considered &#39;backwards incompatible&#39; (and &#39;return any Number&#39; is part of the contract, not necessarily &#39;return the input&#39;).

So, we want a way in the type system to link things: To say, the type of the parameter is.. dunno. But whatever it is, the return type? It&#39;s the same type.

And hence, this is the right way to write `printAsInt`:

public <N extends Number> N printAsInt(N number) {
System.out.println(number.intValue());
return number;
}


Yes, that means if you use a declared type var 0 or 1 times total, it&#39;s a hack or a useless typevar. Needs to show up in at least 2 places. (printAsInt has it in 2 places: Return type, and type of its parameter).

## Your snippet

public static <S> S createService(Class<S> serviceClass) {
return retrofit.create(serviceClass);
}


So now we know how to read this thing:

_Caller, you can call this method by picking some type (any type is fine - S has no bounds), as well as picking a value for the only parameter I have. We linked some things: Whatever you picked for S? You need to provide a value whose type is `Class&lt;S&gt;` - in practice, if you pick `String`, the only thing you can pass is `String.class`. I will return - the type you picked_.

## Where to declare typevars

You can legally declare a typevar in one of two places:

* On a type declaration - looks like `public class Foo&lt;T extends Number&gt; {}` - here T declares a new type var, and it exists for all non-static methods and fields (and even non-static inner classes) defined anywhere inside that type.

* On a method or constructor - looks like `public &lt;T extends Number&gt; void foo()` - exists only for that method.

Your snippet is an example of the second bullet. Something like java&#39;s own `java.util.List` is an example of the first - it is declared as `public interface List&lt;E&gt; {}` and that `E` shows up everywhere - `add(E elem)`, `E get(int idx)`, `Iterator&lt;E&gt; iterator()`, and many more.

## Care - variance!

Java itself is covariant. That means a type is a perfectly fine standin for any of its supertypes. If you have a method `void foo(Number n) {}` you are free to call it with `foo(i)` where `i` is declared as `Integer i;` - because Integer is a perfectly fine standin for Number as Number is a supertype.

__Generics are not like that__. Because math/universe. For example, given a `List&lt;Number&gt;`, that has a method `.add(Number n)`, and we just said that this means I can invoke `.add(someInteger)`. And thus also `.add(someDouble)`. But, if I can write:

List<Integer> ints = new ArrayList<>();
List<Number> numbers = ints; // same list!
numbers.add(Double.valueOf(5.5));
Integer i = ints.get(0);


then.. that code makes no sense, at all. There is only one list here (count the `new` - there is just the one `new`), just, 2 vars that both refer to the same list. I add a `Double`... to... a list.. of... ints. Uhoh. That&#39;s not right. That last line is non-sensical!

That&#39;s why the above _does not compile_. Because unlike &#39;normal&#39; java, the stuff within the `&lt;&gt;` is __invariant__. If you need `Number`, only `Number` will do, not any subtype of Number.

This is incredibly restrictive - imagine you want to write a method that takes in a list of numbers and all you do is print them out. If we write:

public void printAllAsInt(List<Number> list) {
for (Number n : list) System.out.println(n.intValue());
}


then we indeed cannot write:

List<Integer> list = new ArrayList<Integer>();
printAllAsInt(list);


Because generics are invariant, and thus a `List&lt;Integer&gt;` is not an acceptable type to pass to a method that takes a `List&lt;Number&gt;` for the same reason you can&#39;t pass an `JLabel` to a method that takes a `File` object - completely different, unrelated types.

But that&#39;s annoying - our code would work just fine for any number (all numbers have an `intValue()` method, even doubles). You can write that:

void printAllAsInt(List<? extends Number> list) {
...
}


Now you _can_ pass a `List&lt;Integer&gt;` to it. `? extends` means: &quot;I want covariance&quot;, and `? super` means: &quot;I want contravariance&quot;. The compiler protects you: You __cannot__ invoke `.add` on a `List&lt;? extends Number&gt;`. At all.

`List&lt;? extends Number&gt;` does __not__ mean &quot;A list that can contain numbers - any kinds of numbers&quot;. After all, `List&lt;Number&gt;` already means precisely that. No, `List&lt;? extends Number&gt;` means: A list that contains... well, I don&#39;t actually know. It&#39;s certainly restricted to.. something. I do know that whatever the restriction is, it&#39;s either `Number`, or `Integer`, or `Double`, or any other type that extends Number. Thus, calling `.get(idx)` on this list guarantees at the very least something assignable to a variable of type `Number` falls out. But, `.add()` is just straight up uncallable, given that I have no idea - could be a `List&lt;Double&gt;`, could be a `List&lt;Integer&gt;` - no value is _both_ a `Double` __and__ an `Integer` at the same time, so NOTHING can be passed to `add`. Well, except, literally, `null`, which is all types, but that&#39;s mostly useless.


</details>



# 答案4
**得分**: 0

代码部分不要翻译。以下是翻译好的部分:

- The syntax is newer than the originating _Java_ syntax.
  - 语法比起最初的 _Java_ 语法更加新。

- _Java_ ran for several issues before implementing the _[Generics](https://docs.oracle.com/javase/tutorial/java/generics/index.html)_ interface, which was introduced in _[Java SE 5.0](https://en.wikipedia.org/wiki/Java_version_history)_.
  - 在实施引入 _[Java SE 5.0](https://en.wikipedia.org/wiki/Java_version_history)_ 中的 _[泛型](https://docs.oracle.com/javase/tutorial/java/generics/index.html)_ 接口之前,_Java_ 经历了许多问题。

- In addition to this, _Java_ was founded on the same syntax as _[C](https://en.wikipedia.org/wiki/C_(programming_language))_, with the aim to offer an easy transition from _C_ to _Java_.
  - 此外,_Java_ 的语法与 _[C](https://en.wikipedia.org/wiki/C_(programming_language))_ 相同,旨在为从 _C_ 迁移到 _Java_ 提供简单的过渡。

- Hence, the only way to correctly define generic-type parameters would be to create a new syntax.
  - 因此,正确定义泛型类型参数的唯一方法是创建新的语法。

- You can review _generic_ method syntax, at the following link.
  - 您可以在以下链接中查看 _通用_ 方法的语法。

- Essentially, it is stating that, within the method there will be a type _S_ object.
  - 本质上,它表明在方法内部将有一个类型为 _S_ 的对象。

- So the compiler doesn't throw an error.
  - 因此,编译器不会抛出错误。

<details>
<summary>英文:</summary>

The syntax is newer than the originating _Java_ syntax.

_Java_ ran for several issues before implementing the _[Generics](https://docs.oracle.com/javase/tutorial/java/generics/index.html)_ interface, which was introduced in _[Java SE 5.0](https://en.wikipedia.org/wiki/Java_version_history)_.

In addition to this, _Java_ was founded on the same syntax as _[C](https://en.wikipedia.org/wiki/C_(programming_language))_, with the aim to offer an easy transition from _C_ to _Java_.

Hence, the only way to correctly define generic-type parameters would be to create a new syntax.

You can review _generic_ method syntax, at the following link.  
_[Generic Methods (The Java&amp;trade; Tutorials &amp;gt; Learning the Java Language &amp;gt; Generics (Updated))](https://docs.oracle.com/javase/tutorial/java/generics/methods.html)_.

Essentially, it is stating that, within the method there will be a type _S_ object.  
So the compiler doesn&#39;t throw an error.

</details>



huangapple
  • 本文由 发表于 2023年6月9日 03:24:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76435103.html
匿名

发表评论

匿名网友

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

确定