Это действительная Java?

Это действительная 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));
    }

}
  • Затмение 3.5 говорит да
  • Затмение 3.6 говорит нет
  • Интелли 9 говорит да
  • Sun Javac 1.6.0_20 говорит да
  • GCJ 4.4.3 говорит да
  • GWT компилятор говорит да
  • Толпа на мой предыдущий вопрос Stackru говорит нет

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

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

10 ответов

Решение

Это зависит от того, как вы хотите вызвать эти методы. Если вы хотите вызвать эти методы из другого исходного кода 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
строки
чисел

Обновить

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

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
Скомпилировано из "RealyCompilesAndRunsFine.java"
Класс RealyCompilesAndRunsFine extends java.lang.Object{
RealyCompilesAndRunsFine();
  Код:
   0:   aload_0
   1:   invokespecial   #1; // Метод java/lang/Object."":()V
   4: возврат

public static java.lang.String f(java.util.List);
  Код:
   0:   aload_0
   1:   iconst_0
   2:   invokeinterface #2,  2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
   7: чеккаст №3; // класс java/lang/String
   10: возвращение

public static java.lang.Integer f(java.util.List);
  Код:
   0:   aload_0
   1:   iconst_0
   2:   invokeinterface #2,  2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
   7: чеккаст № 4; // класс java/lang/Integer
   10: возвращение

public static void main(java.lang.String[]);
  Код:
   0:   iconst_1
   1: заново № 3; // класс java/lang/String
   4: дуп
   5:   iconst_0
   6:   ldC#5; // Строка asdf
   8: Аастор
   9:   invokestatiC#6; // Метод java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
   12:  invokestatiC#7; // Метод f:(Ljava/util/List;)Ljava/lang/String;
   15:  astore_1
   16:  iconst_1
   17: заново № 4; // класс java/lang/Integer
   20: дуп
   21:  iconst_0
   22:  bipush  123
   24:  invokestatiC#8; // Метод java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   27: Аастор
   28:  invokestatiC#6; // Метод java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
   31:  invokestatiC#9; // Метод f:(Ljava/util/List;)Ljava/lang/Integer;
   34:  astore_2
   35: гестатик № 10; // Поле java/lang/System.out:Ljava/io/PrintStream;
   38:  aload_1
   39:  invokevirtual   #11; // Метод java/io/PrintStream.println:(Ljava/lang/String;)V
   42: гестатик № 10; // Поле java/lang/System.out:Ljava/io/PrintStream;
   45:  aload_2
   46:  invokevirtual   #12; // Метод java/io/PrintStream.println:(Ljava/lang/Object;)V
   49: возвращение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

И вот почему: это особенность.

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

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

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

Это действительно ваз по спецификации.

Подпись метода m1 это подпись метода m2 если либо

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

  • подпись m1 так же, как стирание подписи m2,

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

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

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

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

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

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

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

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

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

...
    public static String f (List list) {
        System.out.println ("струна");
        вернуть ноль;
    }

    public static Integer f (List list) {
        System.out.println ("число");
        вернуть ноль;
    }
...

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

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

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

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

Также работает (с 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);
    }

}

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

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

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

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

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

Другие вопросы по тегам