Это действительная Java? - PullRequest
59 голосов
/ 24 июня 2010

Это действительная 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 говорит да
  • Затмение 3.6 говорит: нет
  • Интеллидж 9 говорит да
  • Sun Javac 1.6.0_20 говорит да
  • GCJ 4.4.3 говорит да
  • GWT-компилятор говорит да
  • Толпа на мой предыдущий вопрос Stackoverflow говорит нет

Мое понимание теории Java говорит: нет !

Было бы интересно узнать, что JLS говорит об этом.

Ответы [ 10 ]

29 голосов
/ 24 июня 2010

Это зависит от того, как вы хотите вызывать эти методы. Если вы хотите вызвать эти методы из другого исходного кода Java , то он считается недействительным по причинам, показанным в ответ Эдвина . Это ограничение языка Java.

Однако не все классы должны быть сгенерированы из исходного кода Java (рассмотрите все языки, которые используют JVM в качестве среды выполнения: JRuby, Jython и т. Д.). На уровне байт-кода JVM может устранить неоднозначность двух методов, поскольку инструкции байт-кода указывают возвращаемый тип , который они ожидают. Например, вот класс, написанный на Jasmin , который может вызывать любой из этих методов:

.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

Я компилирую его в файл класса, используя следующую команду:

java -jar jasmin.jar CallAmbiguousMethod.j

И позвоните, используя:

java CallAmbiguousMethod

Вот, вывод:

> java CallAmbiguousMethod
strings
numbers

Обновление

Саймон опубликовал пример программы , которая вызывает эти методы:

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

}

Вот сгенерированный байт-код Java:

>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

Оказывается, компилятор Sun генерирует байт-код, необходимый для устранения неоднозначности методов (см. Инструкции 12 и 31 в последнем методе).

Обновление № 2

Спецификация языка Java предполагает, что на самом деле это может быть действительный исходный код Java. На странице 449 (§15.12 Выражения вызова метода) мы видим это:

Вполне возможно, что ни один метод не является наиболее специфичным, потому что есть два или больше методов, которые максимально специфичны. В этом случае:

  • Если все максимально специфические методы имеют сигнатуры, эквивалентные переопределению (§8.4.2), затем:
    • Если точно один из максимально специфических методов не объявлен абстрактным, это самый специфический метод.
    • В противном случае, если все максимально конкретные методы объявлены абстрактными, и сигнатуры всех максимально специфических методов имеют одинаковые erasure (§4.6), , тогда наиболее конкретный метод выбирается произвольно среди подмножество максимально специфических методов, которые имеют наиболее специфические тип возврата . Тем не менее, наиболее конкретный метод считается бросить проверенное исключение, если и только если это исключение или его удаление объявлено в предложения throws каждого из максимально специфических методов.
  • В противном случае мы говорим, что вызов метода неоднозначен, и время компиляции возникает ошибка.

Если я не ошибаюсь, это поведение должно применяться только к методам, объявленным как абстрактные ...

Обновление № 3

Благодаря комментарию ILMTitan:

@ Адам Пейнтер: Ваш жирный текст делает не имеет значения, потому что это только случай когда два метода эквивалентный переопределению, который показал Дэн был не тот случай. Таким образом определяющим фактором должно быть, если JLS принимает во внимание общие типы, когда определение наиболее конкретного метода. - ILMTitan

13 голосов
/ 24 июня 2010

--- Отредактировано в ответ на комментарии ниже ---

Хорошо, значит, это допустимая Java, но она не должна быть.Ключевым моментом является то, что на самом деле он полагается не на тип возвращаемого значения, а на стертый параметр Generics.

Это не будет работать для нестатического метода и явно запрещено для нестатического метода.Попытка сделать это в классе потерпит неудачу из-за дополнительных проблем, во-первых, типичный класс не final , как класс Class .

Это несоответствие в другом довольно последовательном языке.Я выйду на конечность и скажу, что это должно быть незаконным, даже если это технически разрешено.Это на самом деле ничего не добавляет к читабельности языка, и мало что добавляет к способности решать значимые проблемы.Единственная значимая проблема, которую он, похоже, решает, это то, достаточно ли вы знакомы с языком, чтобы знать, когда его основные принципы кажутся нарушенными внутренними несоответствиями языка в разрешении стирания типов, обобщенных типов и полученных сигнатур методов..

Определенно код, которого следует избегать, поскольку тривиально решить ту же проблему любым другим, более значимым образом, тривиально, и единственное преимущество заключается в том, чтобы узнать, знает ли рецензент / расширитель пыльный грязный угол языкаspec.

--- Исходное сообщение следует ---

Хотя компиляторы могли бы разрешить это, ответ все еще нет.

Erasure превратит оба списка и список в неукрашенный список.Это означает, что оба ваших метода "f" будут иметь одну и ту же сигнатуру, но разные типы возврата.Возвращаемый тип не может использоваться для дифференциации методов, потому что это не удастся, когда вы вернетесь в общий супертип;как:

Object o = f(Arrays.asList("asdf"));

Вы пытались захватить возвращенные значения в переменные?Возможно, компилятор оптимизировал вещи таким образом, что он не наступает на правильный код ошибки.

11 голосов
/ 25 июня 2010

Один вопрос, на который не был дан ответ: почему он вызывает только ошибку компиляции в Eclipse 3.6?

И вот почему: это функция .

В javac 7 рассматриваются два метода дубликаты (или ошибка конфликта имен) независимо от их типа возврата.

Это поведение теперь более последовательное с Javac 1,5, который сообщил имя Столкновение ошибок на методах и игнорируется их типы возврата. Только в 1.6 был изменение, включающее возвращаемые типы при обнаружении дублирующих методов.

Мы решили внести это изменение на всех уровнях соответствия (1,5, 1,6, 1.7) в версии 3.6, поэтому пользователи не будут удивлены изменением, если они скомпилируйте их код, используя javac 7.

5 голосов
/ 24 июня 2010

Что ж, если я правильно понимаю пункт три пункта в первом списке из раздела 8.4.2 спецификации, он говорит, что ваши методы f () имеют одинаковую сигнатуру:

http://java.sun.com/docs/books/jls/third_edition/html/classes.html#38649

Это спецификация, которая действительно отвечает на этот вопрос, а не наблюдаемое поведение компилятора X или IDE X. Все, что мы можем сказать, глядя на инструменты, это то, как автор инструмента интерпретировал спецификацию.

Если мы применим пулю три, мы получим:

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

и сигнатуры совпадают, поэтому существует коллизия, и код не должен компилироваться.

5 голосов
/ 24 июня 2010

Действителен в соответствии со спецификацией .

Подпись метода m1 является подписью метода m2, если либо

  • m2 имеет ту же подпись, что и m1, или

  • подпись m1 совпадает с удалением подписиm2.

Таким образом, это не подписи друг друга, поскольку стирание List<String> не равно List<Integer> и наоборот.

Две сигнатуры метода m1 и m2 эквивалентны переопределению, если либо m1 является подписью m2, либо m2 является подписью m1.

Так что этидва не переопределены (обратите внимание на , если ).И правило перегрузки:

Если два метода класса (оба они объявлены в одном и том же классе или оба наследуются одним классом или один объявлен и один унаследован) имеют одинаковое имя, ноподписи, которые не эквивалентны переопределению, тогда говорят, что имя метода перегружено.

Следовательно, эти два метода перегружены, и все должно работать.

1 голос
/ 24 июня 2010

Вывод типа Java (что происходит, когда вы вызываете статические, универсальные методы, такие как Array.asList), сложен и не очень хорошо определен в JLS.Эта статья 2008 года содержит очень интересное описание некоторых проблем и способов их устранения:

Разбит вывод типа Java: как мы можем это исправить?

1 голос
/ 24 июня 2010

Из того, что я могу сказать, файл .class может содержать оба метода, поскольку дескриптор метода содержит параметры, а также тип возвращаемого значения.Если бы возвращаемый тип был бы таким же, то дескрипторы были бы такими же, а методы были бы неразличимы после стирания типа (следовательно, это также не работает с void).http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#7035

Теперь для вызова метода с invoke_virtual требуется дескриптор метода, так что вы можете фактически сказать, какой из методов вы хотите вызвать, поэтому кажется, что все эти компиляторы, которые все еще имеют общийинформацию, просто поместите дескриптор для метода, который соответствует универсальному типу параметра, чтобы затем в байт-коде было жестко прописано, какой метод вызывать (в отличие от их дескрипторов или, точнее, от типа возврата в этих дескрипторах), даже еслипараметр теперь является списком, без общей информации.

Хотя я нахожу эту практику несколько сомнительной, я должен сказать, что я считаю, что это здорово, что вы можете сделать это, и думаю, что генерики должны были быть разработаны дляуметь работать в первую очередь так (да, я знаю, что это создаст проблемы с обратной совместимостью).

1 голос
/ 24 июня 2010

Также работает (с Sun Java 1.6.0_16 на этот раз)

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

}
0 голосов
/ 25 июня 2010

Похоже, что компилятор выбирает наиболее специфический метод на основе обобщений.

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

}

Вывод:

strings
numbers
0 голосов
/ 24 июня 2010

Eclipse может выдавать из этого байт-код:

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

Выход:

4

15129

...