可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Is this valid Java?
import java.util.Arrays;
import java.util.List;
class TestWillThatCompile {
public static String f(List<String> list) {
System.out.println("strings");
return null;
}
public static Integer f(List<Integer> list) {
System.out.println("numbers");
return null;
}
public static void main(String[] args) {
f(Arrays.asList("asdf"));
f(Arrays.asList(123));
}
}
- Eclipse 3.5 says yes
- Eclipse 3.6 says no
- Intellij 9 says yes
- Sun javac 1.6.0_20 says yes
- GCJ 4.4.3 says yes
- GWT compiler says yes
- Crowd at my previous Stackoverflow question says no
My java theory understanding says no!
It would be interesting to know what the JLS is saying about it.
回答1:
It depends upon how you wish to call these methods. If you wish to call these methods from other Java source code, then it is considered invalid for reasons illustrated in Edwin's answer. This is a limitation of the Java Language.
However, not all classes need to be generated from Java source code (consider all the languages that use the JVM as their runtime: JRuby, Jython, etc...). At the bytecode level, the JVM can disambiguate the two methods because the bytecode instructions specify the return type they are expecting. For example, here is a class written in Jasmin that can call either of these methods:
.class public CallAmbiguousMethod
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
.limit stack 3
.limit locals 1
; Call the method that returns String
aconst_null
invokestatic TestWillThatCompile/f(Ljava/util/List;)Ljava/lang/String;
; Call the method that returns Integer
aconst_null
invokestatic TestWillThatCompile/f(Ljava/util/List;)Ljava/lang/Integer;
return
.end method
I compile it to a class file using the following command:
java -jar jasmin.jar CallAmbiguousMethod.j
And call it using:
java CallAmbiguousMethod
Behold, the output is:
> java CallAmbiguousMethod
strings
numbers
Update
Simon posted an example program that calls these methods:
import java.util.Arrays;
import java.util.List;
class RealyCompilesAndRunsFine {
public static String f(List<String> list) {
return list.get(0);
}
public static Integer f(List<Integer> list) {
return list.get(0);
}
public static void main(String[] args) {
final String string = f(Arrays.asList("asdf"));
final Integer integer = f(Arrays.asList(123));
System.out.println(string);
System.out.println(integer);
}
}
Here is the Java bytecode generated:
>javap -c RealyCompilesAndRunsFine
Compiled from "RealyCompilesAndRunsFine.java"
class RealyCompilesAndRunsFine extends java.lang.Object{
RealyCompilesAndRunsFine();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."":()V
4: return
public static java.lang.String f(java.util.List);
Code:
0: aload_0
1: iconst_0
2: invokeinterface #2, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
7: checkcast #3; //class java/lang/String
10: areturn
public static java.lang.Integer f(java.util.List);
Code:
0: aload_0
1: iconst_0
2: invokeinterface #2, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
7: checkcast #4; //class java/lang/Integer
10: areturn
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: anewarray #3; //class java/lang/String
4: dup
5: iconst_0
6: ldc #5; //String asdf
8: aastore
9: invokestatic #6; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
12: invokestatic #7; //Method f:(Ljava/util/List;)Ljava/lang/String;
15: astore_1
16: iconst_1
17: anewarray #4; //class java/lang/Integer
20: dup
21: iconst_0
22: bipush 123
24: invokestatic #8; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
27: aastore
28: invokestatic #6; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
31: invokestatic #9; //Method f:(Ljava/util/List;)Ljava/lang/Integer;
34: astore_2
35: getstatic #10; //Field java/lang/System.out:Ljava/io/PrintStream;
38: aload_1
39: invokevirtual #11; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: getstatic #10; //Field java/lang/System.out:Ljava/io/PrintStream;
45: aload_2
46: invokevirtual #12; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
49: return
It turns out the Sun compiler is generating the bytecode necessary to disambiguate the methods (see instructions 12 and 31 in the last method).
Update #2
The Java Language Specification suggests that this may, in fact, be valid Java source code. On page 449 (§15.12 Method Invocation Expressions) we see this:
It is possible that no method is the most specific, because there are two or
more methods that are maximally specific. In this case:
- If all the maximally specific methods have override-equivalent (§8.4.2) signatures,
then:
- If exactly one of the maximally specific methods is not declared abstract,
it is the most specific method.
- Otherwise, if all the maximally specific methods are declared abstract,
and the signatures of all of the maximally specific methods have the same
erasure (§4.6), then the most specific method is chosen arbitrarily among
the subset of the maximally specific methods that have the most specific
return type. However, the most specific method is considered to throw a
checked exception if and only if that exception or its erasure is declared in
the throws clauses of each of the maximally specific methods.
- Otherwise, we say that the method invocation is ambiguous, and a compiletime
error occurs.
Unless I am mistaken, this behavior should only apply to methods declared as abstract, though...
Update #3
Thanks to ILMTitan's comment:
@Adam Paynter: Your bolded text does
not matter, because it is only a case
when two methods are
override-equivalent, which Dan showed
was not the case. Thus, the
determining factor must be if the JLS
takes generic types into account when
determining most specific method. –
ILMTitan
回答2:
--- Edited in response to comments below ---
Ok, so it is valid Java, but it shouldn't be. The key is that it's not really relying on the return type, but on the erased Generics parameter.
This wouldn't work on a non-static method, and is explicitly forbidden on a non-static method. Attempting to do so in a class would fail due to the extra issues, the first being that a typical class isn't final as the class Class is.
It's an inconsistency in an otherwise rather consistent language. TI'll go out on a limb and say that it should be illegal, even if technically allowed. It doesn't really add anything to the readability of the language, and it adds scant little to the ability to solve meaningful problems. The only meaningful problem it seems to solve is whether you are familiar enough with the language to know when it's core tenets seem to be violated by the language's internal inconsistencies in resolving type erasure, generics, and the resulting method signatures.
Definitely code to be avoided, as it is trivial to solve the same problem in any number of more meaningful ways, and the only benefit is to see if the reviewer/extender knows a dusty dirty corner of the language spec.
--- Original Post follows ---
While the compilers might have allowed it, the answer is still no.
Erasure will turn both the List<String> and List<Integer> into an unadorned List. That means that both of your "f" methods will have the same signature but different return types. Return type can't be used to differentiate methods, because doing so will fail when you return into a common super-type; like:
Object o = f(Arrays.asList("asdf"));
Have you tried capturing the returned values into variables? Perhaps the compiler has optimized out things in such a way that it's not stepping on the right error code.
回答3:
One questioned that hasn't been answered is: why does it only trigger a compile error in Eclipse 3.6?
Here's why: it's a feature.
In javac 7, two methods are considered
duplicates (or a name clash error)
regardless of their return types.
This behaviour is now more consistent
with javac 1.5, which reported name
clash errors on methods and ignored
their return types. Only in 1.6 was a
change made that included return types
when detecting duplicate methods.
We have decided to make this change
across all compliance levels (1.5,
1.6,
1.7) in the 3.6 release so users will not be surprised by the change if they
compile their code using javac 7.
回答4:
Well, if I understand bullet point three correctly in the first list from section 8.4.2 of the spec, it says your f() methods have the same signature:
http://java.sun.com/docs/books/jls/third_edition/html/classes.html#38649
It's the spec that really answers this question, and not the observed behavior of compiler X or IDE X. All we can say by looking at the tools is how the author of the tool interpreted the spec.
If we apply bullet three, we get:
...
public static String f(List<String> list) {
System.out.println("strings");
return null;
}
public static Integer f(List<String> list) {
System.out.println("numbers");
return null;
}
...
and the signatures match, so there's a collision and the code shouldn't compile.
回答5:
It's valid vased on the specification.
The signature of a method m1
is a
subsignature of the signature of a
method m2
if either
m2
has the same signature as m1
, or
the signature of m1
is the same as the erasure of the signature of
m2
.
So these are not subsignatures of eachother because the erasure of List<String>
is not List<Integer>
and vice versa.
Two method signatures m1
and m2
are
override-equivalent iff either m1
is a
subsignature of m2
or m2
is a
subsignature of m1
.
So these two are not override-equivalent (note the iff). And the rule for overloading is:
If two methods of a class (whether
both declared in the same class, or
both inherited by a class, or one
declared and one inherited) have the
same name but signatures that are not
override-equivalent, then the method
name is said to be overloaded.
Therefore, these two methods are overloaded and everything should work.
回答6:
Also working (with sun java 1.6.0_16 this time) is
import java.util.Arrays;
import java.util.List;
class RealyCompilesAndRunsFine {
public static String f(List<String> list) {
return list.get(0);
}
public static Integer f(List<Integer> list) {
return list.get(0);
}
public static void main(String[] args) {
final String string = f(Arrays.asList("asdf"));
final Integer integer = f(Arrays.asList(123));
System.out.println(string);
System.out.println(integer);
}
}
回答7:
From what I can tell the .class file can hold both methods since the method descriptor holds the parameters, as well as the return type. If the return type would be the same, then the descriptors would be the same and the methods would be indistinguishable after the type erasure (hence it doesn't work with void either). http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#7035
Now, calling the method with invoke_virtual requires the method descriptor, so you can in fact say which one of the methods you want to call, so it seems that all those compilers, which still have the generic information, simply put the descriptor for the method that matches the generic type of the parameter, so then it's hardcoded in the bytecode which method to call (as distinguished by their descriptors, or more exactly by the return type in those descriptors), even if the parameter is now a List, without generic information.
While I find this practice a little questionable, I must say I find it kind of cool that you can do this, and think that generics should have been designed to be able to work like this in the first place (yes, I know that would create problems with backwards compatibility).
回答8:
Java type inference (what's going on when you call static, generic methods like Array.asList) is complicated and not well-specified in the JLS. This paper from 2008 has a very interesting description of some of the issues and how it might be fixed:
Java Type Inference is Broken: How Can We Fix it?
回答9:
Eclipse can produce byte code out of this:
public class Bla {
private static BigDecimal abc(List<BigDecimal> l) {
return l.iterator().next().multiply(new BigDecimal(123));
}
private static String abc(List<String> l) {
return l.iterator().next().length() + "";
}
public static void main(String[] args) {
System.out.println(abc(Arrays.asList("asdf")));
System.out.println(abc(Arrays.<BigDecimal>asList(new BigDecimal(123))));
}
}
Output:
4
15129
回答10:
Looks like the compiler chooses the most specific method based on the generics.
import java.util.Arrays;
import java.util.List;
class TestWillThatCompile {
public static Object f(List<?> list) {
System.out.println("strings");
return null;
}
public static Integer f(List<Integer> list) {
System.out.println("numbers");
return null;
}
public static void main(String[] args) {
f(Arrays.asList("asdf"));
f(Arrays.asList(123));
}
}
Output:
strings
numbers