Переопределить недействительные методы, чтобы вернуть свой аргумент, чтобы облегчить беглость: нарушение изменений?

"Дизайн API похож на секс: сделайте одну ошибку и поддержите ее на всю оставшуюся жизнь" (Джош Блох в твиттере)суб >

В библиотеке Java много ошибок дизайна. Stack extends Vector (обсуждение), и мы не можем исправить это, не вызывая поломки. Мы можем попытаться осудить Integer.getInteger (обсуждение), но он, вероятно, останется навсегда.

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

Эффективное Java 2nd Edition, пункт 18: Предпочитает интерфейсы для абстрактных классов: существующие классы могут быть легко модифицированы для реализации нового интерфейса ".

Примеры: String implements CharSequence, Vector implements List и т.д.

Эффективное Java 2nd Edition, пункт 42: разумно использовать varargs: вы можете модифицировать существующий метод, который принимает массив как его последний параметр, чтобы вместо этого принимать varags, не влияя на существующих клиентов.

Известный пример (in) Arrays.asList, который вызвал путаницу (обсуждение), но не поломка.

Этот вопрос касается другого вида переоснащения:

Можете ли вы модифицировать метод void, чтобы вернуть что-то, не нарушая существующий код?

Моя первая догадка указывает на да, потому что:

  • Тип возврата не влияет на то, какой метод выбран во время компиляции
    • Смотрите: JLS 15.12.2.11 - Тип возврата не рассматривается
    • Таким образом, изменение типа возвращаемого значения не изменяет, какой метод выбран компилятором
    • Дооснащение из void, чтобы вернуть что-то, является законным (но не наоборот).
  • Даже когда вы используете отражение, такие вещи, как Class.getMethod, не различают по типу возврата

Однако я хотел бы услышать более тщательный анализ других, более опытных в разработке Java/API.


Приложение: Мотивация

Как указано в названии, одна мотивация заключается в облегчении программирования стиля свободного интерфейса.

Рассмотрим этот простой фрагмент, который печатает перетасованный список имен:

    List<String> names = Arrays.asList("Eenie", "Meenie", "Miny", "Moe");
    Collections.shuffle(names);
    System.out.println(names);
    // prints e.g. [Miny, Moe, Meenie, Eenie]

Had Collections.shuffle(List) объявлен для возврата списка ввода, мы могли бы написать:

    System.out.println(
        Collections.shuffle(Arrays.asList("Eenie", "Meenie", "Miny", "Moe"))
    );

В Collections есть другие методы, которые были бы гораздо приятнее использовать, если бы они возвращали список ввода вместо void, например. reverse(List), sort(List) и т.д. На самом деле, если Collections.sort и Arrays.sort return void особенно прискорбно, потому что он лишает нас возможности писать выразительный код, например:

// DOES NOT COMPILE!!!
//     unless Arrays.sort is retrofitted to return the input array

static boolean isAnagram(String s1, String s2) {
    return Arrays.equals(
        Arrays.sort(s1.toCharArray()),
        Arrays.sort(s2.toCharArray())
    );
}

Этот тип возвращаемого типа void, предотвращающий свободное владение, не ограничивается этими методами полезности. java.util.BitSet также могли быть записаны для возврата this (ala StringBuffer и StringBuilder), чтобы облегчить свободное владение.

// we can write this:
    StringBuilder sb = new StringBuilder();
    sb.append("this");
    sb.append("that");
    sb.insert(4, " & ");
    System.out.println(sb); // this & that

// but we also have the option to write this:
    System.out.println(
        new StringBuilder()
            .append("this")
            .append("that")
            .insert(4, " & ")
    ); // this & that

// we can write this:
    BitSet bs1 = new BitSet();
    bs1.set(1);
    bs1.set(3);
    BitSet bs2 = new BitSet();
    bs2.flip(5, 8);
    bs1.or(bs2);
    System.out.println(bs1); // {1, 3, 5, 6, 7}

// but we can't write like this!
//  System.out.println(
//      new BitSet().set(1).set(3).or(
//          new BitSet().flip(5, 8)
//      )
//  );

К сожалению, в отличие от StringBuilder/StringBuffer, ALL мутаторов BitSet возвращает void.

Связанные темы

Ответы

Ответ 1

К сожалению, да, изменение метода void для возврата чего-либо является нарушением изменений. Это изменение не повлияет на совместимость исходного кода (т.е. Тот же исходный код Java будет компилироваться так же, как и раньше, без какого-либо заметного эффекта), но он нарушает двоичную совместимость (т.е. байт-коды, которые ранее были скомпилированы против старого API, больше не будут выполняться).

Вот соответствующие выдержки из Спецификации Java Language Specification 3rd Edition:

13.2 Какая двоичная совместимость есть и не является

Совместимость двоичных файлов не совпадает с совместимостью с исходным кодом.


13.4 Эволюция классов

В этом разделе описываются эффекты изменений в объявлении класса и его элементов и конструкторов в ранее существовавших двоичных файлах.

13.4.15 Тип результата метода

Изменение типа результата метода, замена типа результата на void или замена void на тип результата имеет комбинированный эффект:

  • удаление старого метода и
  • добавление нового метода с новым типом результата или новым результатом void.

13.4.12 Объявления по методу и конструктору

Удаление метода или конструктора из класса может нарушить совместимость с любым ранее существовавшим двоичным файлом, который ссылался на этот метод или конструктор; a NoSuchMethodError может быть брошен, когда такая ссылка из существующего двоичного файла связана. Такая ошибка будет возникать только в том случае, если в суперклассе объявлен не метод с подходящей сигнатурой и типом возврата.

То есть, в то время как возвращаемый тип метода игнорируется компилятором Java во время компиляции во время процесса разрешения метода, эта информация значительна во время выполнения на уровне байт-кода JVM.


В дескрипторах метода байт-кода

Подпись метода не включает тип возврата, но его дескриптор байт-кода делает.

8.4.2 Подпись метода

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


15.12 Выражения вызова метода

15.12.2 Время компиляции Шаг 2: Определение подписи метода

Дескриптор (тип подписи и возврата) наиболее конкретного метода используется во время выполнения для отправки метода.

15.12.2.12 Пример: Разрешение во время компиляции

Наиболее применимый метод выбирается во время компиляции; его дескриптор определяет, какой метод фактически выполняется во время выполнения.

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

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

Небольшая проверка байт-кода поможет прояснить это. Когда javap -c запускается в фрагменте перетасования имени, мы видим следующие инструкции:

invokestatic java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
             \______________/ \____/ \___________________/\______________/
                type name     method      parameters        return type

invokestatic java/util/Collections.shuffle:(Ljava/util/List;)V
             \___________________/ \_____/ \________________/|
                   type name        method     parameters    return type

Связанные вопросы


О неразрывной модернизации

Теперь давайте рассмотрим, почему переоснащение нового interface или vararg, как объяснено в Effective Java 2nd Edition, не нарушает двоичную совместимость.

13.4.4 Суперклассы и суперинтерфейсы

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

Дооснащение новой interface не приводит к тому, что тип теряет какой-либо член, следовательно, это не нарушает двоичную совместимость. Аналогичным образом, из-за того, что varargs реализуется с использованием массивов, такой вид переоснащения также не нарушает двоичную совместимость.

8.4.1 Формальные параметры

Если последний формальный параметр является параметром переменной arty типа T, считается, что он определяет формальный параметр типа T[].

Связанные вопросы


Нет ли способа сделать это?

На самом деле, да, есть способ модифицировать возвращаемое значение ранее void. Мы не можем иметь два метода с одинаковыми точными сигнатурами на уровне исходного кода Java, но мы можем иметь это на уровне JVM при условии, что у них разные дескрипторы (из-за наличия разных типов возвращаемых данных).

Таким образом, мы можем предоставить двоичный код, например. java.util.BitSet, который имеет методы одновременно с типами возвратов void и не void. Нам нужно только опубликовать версию non void в качестве нового API. Фактически, это единственное, что мы можем опубликовать в API, поскольку наличие двух методов с одной и той же сигнатурой является незаконным в Java.

Это решение является ужасным взломом, требующим специальной (и spec-defying) обработки для компиляции BitSet.java в BitSet.class, и, возможно, это не стоит того, чтобы это сделать.

Ответ 2

Если вы не можете модифицировать, вы все равно можете перенести свой класс в новый, который использует те же методы, но с правильной отдачей (MyClassFluent). Или вы можете добавлять новые методы, но с разными именами, вместо Arrays.sort() мы могли бы иметь Arrays.getSorted().

Я думаю, что решение заключается не в том, чтобы заставить вещи, а только иметь дело с ними.

РЕДАКТИРОВАТЬ: Я знаю, что я не ответил на вопрос "доочистить пустоты", но ваш ответ уже действительно ясен.

Ответ 3

Если вам нужно иметь дело только с совместимостью с исходниками, то просто продолжайте. Переход от void к типу возврата не будет нарушен.

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

Это для игры с ним или потому, что вы обнаружили, что это действительно здорово?