Is this valid Java?
Asked Answered
E

10

60

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.

Embellishment answered 24/6, 2010 at 12:45 Comment(5)
possible duplicate of stackoverflow.com/questions/3105177Fabrication
@Fabrication he already referenced that in the question.Operose
Nasal demons? I mean, you've got all the answers people have given so far, and they're very inconclusive.Devise
The code compiles and runs fine and nobody explained why.Embellishment
Actually it works and is runnable and outputs: strings numbersTribunate
P
30

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

Provincial answered 24/6, 2010 at 13:11 Comment(13)
Nice, lets try to read that now. I dont read bytecode everyday.Embellishment
+1 for the bytecode investigation. I personally think it's a little perverse that the compiler uses the List generics argument type to determine which method is being called in source code, then compiles that into bytecode that's differentiated based on return type, but there you go.Fungistat
So does the bytecode knows which method to call thanks to "#7" or to the "//Method f:(Ljava/util/List;)Ljava/lang/String;"Embellishment
@Tim: Yeah, it's one of those places where the bytecode semantics diverge from the language semantics. I believe there are other examples, but I can't remember them off the top of my head.Provincial
@simon: The #7 is irrelevant. In the bytecode, you can think of the #7 as a pointer to the "//Method f:(Ljava/util/List;)Ljava/lang/String;" description.Provincial
@simon: Have fun. :) Granted, it's not something that Java programmers typically have to deal with.Provincial
@simon: If you're curious about the actual layout of a class file, you can refer to java.sun.com/docs/books/jvms/second_edition/html/….Provincial
Still, it's not the generated bytecode that answers the question "Is this valid java?" The bytecode can only answer "does the author of the compiler think it's valid java?" Ultimately, we have to rely on the spec to answer this question, then look at the compiler to see whether it's compliant or not.Scotfree
@Don: I agree, the generated bytecode doesn't declare it valid Java source code or not. Perhaps I wasn't clear. I believe it's not valid Java source code. However, some compiler authors ignore this and allow the source code to compile because it's not a problem at the bytecode level (which some programmers may be trying to target).Provincial
How exactly is this the correct answer to this question? The point being made about other languages generating byte code is irrelevant to whether the code is valid Java. The Java compiler validates Java source, which is the actual question. Not whether other JVM based languages can differentiate the methods due to how the byte code is generated.Spinelli
@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.Trabeated
@Robin: As per my response to Don's comment, I believe my answer indicates that it is invalid Java source code. The answer goes on to illustrate why compiler authors may ignore that fact. :)Provincial
@ILMTitan: Thanks! :) I find the Java Language Specification a bit overwhelming at times.Provincial
B
13

--- 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.

Burlburlap answered 24/6, 2010 at 12:58 Comment(4)
Agreed with last part - attempt to do something like List<String> s = f(Arrays.asList("asdf")); List<Integer> i = f(Arrays.asList(123));Substandard
I think you only can take the erasure of one of the two signatures to see if these two are override-equivalent. See my answer.Bricky
Yea we all hate corner cases, we are just trying to describe them. But I dont exclude using that corner case and ask someone to put void at all these "f" methods just to see its face at all the error he will get. I hope saying "f" method wont trigger a vulgarity event in stackoverflow engine.Embellishment
"Erasure will turn both the List<String> and List<Integer> into an unadorned List." => No, it won't!Gounod
G
12

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.

Gallo answered 25/6, 2010 at 11:1 Comment(1)
Thanks for the missing piece in the puzzle.Embellishment
B
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.

Bricky answered 24/6, 2010 at 13:27 Comment(1)
You seem to have missed the bullet just above the part you first quoted, the one that Don Branson pointed out.Ernaline
S
5

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.

Scotfree answered 24/6, 2010 at 13:31 Comment(0)
E
1

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

}
Embellishment answered 24/6, 2010 at 13:10 Comment(1)
I tried compiling that class and I received the same result. After looking at the bytecode generated by the compiler, it appears that (at the bytecode level), it's doing the thing demonstrated in my answer.Provincial
D
1

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

Dorrie answered 24/6, 2010 at 13:29 Comment(0)
B
1

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?

Benevolent answered 24/6, 2010 at 14:16 Comment(0)
T
0

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

Tribunate answered 24/6, 2010 at 13:27 Comment(1)
As far as I know only eclipse 3.6 has a problem with that.Embellishment
C
0

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
Chapbook answered 25/6, 2010 at 13:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.