Is this valid Java?

2019-01-10 09:13发布

问题:

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