Подтвердить что ты не робот

Можем ли мы объединить два метода, которые в значительной степени отличаются от типа?

У меня есть два похожих, но разных типа, блоков кода в Java:

private Integer readInteger() {
    Integer value = null;
    while (value == null) {
        if (scanner.hasNextInt()) {
            value = scanner.nextInt();
        } else {
            scanner.next();
        }
    }

    return value;
}

private Double readDouble() {
    Double value = null;
    while (value == null) {
        if (scanner.hasNextDouble()) {
            value = scanner.nextDouble();
        } else {
            scanner.next();
        }
    }

    return value;
}

Можно ли сделать только один метод, который будет работать для них обоих?

4b9b3361

Ответ 1

Я бы сказал, используйте общий метод в сочетании с функциональными интерфейсами, представленными на Java 8.

Метод read теперь становится функцией более высокого порядка.

private <T> T read(Predicate<Scanner> hasVal, Function<Scanner, T> nextVal) {
    T value = null;
    while (value == null) {
        if (hasVal.test(scanner)) {
            value = nextVal.apply(scanner);
        } else {
            scanner.next();
        }
    }

    return value;
}

Вызов кода:

read(Scanner::hasNextInt, Scanner::nextInt);
read(Scanner::hasNextDouble, Scanner::nextDouble);
read(Scanner::hasNextFloat, Scanner::nextFloat);
// ...

Таким образом, метод readInteger() можно адаптировать следующим образом:

private Integer readInteger() {
    return read(Scanner::hasNextInt, Scanner::nextInt);
}

Ответ 2

У вас может быть что-то с тремя способами:

  • Тот, который говорит, что есть значение правильного типа
  • Другой, который получает значение правильного типа.
  • Другой, который отбрасывает любой токен, который у вас есть.

Например:

interface Frobnitz<T> {
  boolean has();
  T get();
  void discard();
}

Вы можете передать это в свой метод:

private <T> T read(Frobnitz<? extends T> frob) {
    T value = null;
    while (value == null) {
        if (frob.has()) {
            value = frob.get();
        } else {
            frob.discard();
        }
    }

    return value;
}

А затем просто реализуйте Frobnitz для своих случаев Double и Integer.

Честно говоря, я не уверен, что это вас очень сильно, особенно если у вас только два случая; Я был бы склонен просто всасывать небольшое количество дублирования.

Ответ 3

Многие люди ответили, что вы можете использовать generics, но вы также можете просто удалить метод readInteger и использовать только readDouble, поскольку целые числа могут быть преобразованы в двойные, без потери данных.

Ответ 4

Это о дублировании кода.

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

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

@FunctionalInterface
interface ScannerNext{
   boolean hasNext(Scanner scanner);
}

@FunctionalInterface
interface ScannerValue{
   Number getNext(Scanner scanner);
}

Затем замените фактический вызов методов в сканере с помощью функционального интерфейса:

private Integer readInteger() {
    ScannerNext scannerNext = (sc)->sc.hasNextInt();
    ScannerValue scannerValue = (sc)-> sc.nextInt();
    Integer value = null;
    while (value == null) {
        if (scannerNext.hasNext(scanner)) {
            value = scannerValue.getNext(scanner);
        } else {
            scanner.next();
        }
    }
    return value;
}

Еще одна проблема заключается в том, что тип переменной значения отличается. Поэтому мы заменяем его своим общим супертипом:

private Integer readInteger() {
    ScannerNext scannerNext = (sc)->sc.hasNextInt();
    ScannerValue scannerValue = (sc)-> sc.nextInt();
    Number value = null;
    while (value == null) {
        if (scannerNext.hasNext(scanner)) {
            value = scannerValue.getNext(scanner);
        } else {
            scanner.next();
        }
    }
    return (Integer)value;
}

Теперь у вас есть места с большим равным участком. Вы можете выбрать один из этих разделов, начинающийся с Number value = null;, заканчивающийся на } до return ..., и вызывать автоматическое рефакторинг IDEs с помощью :

private Number readNumber(ScannerNext scannerNext,  ScannerValue scannerValue) {
    Number value = null;
    while (value == null) {
        if (scannerNext.hasNext(scanner)) {
            value = scannerValue.getNext(scanner);
        } else {
            scanner.next();
        }
    }
    return value;
}

private Integer readInteger() {
    return (Integer) readNumber( (sc)->sc.hasNextInt(), (sc)-> sc.nextInt());
}
private Double readDouble() {
    return (Double) readNumber( (sc)->sc.hasNextDouble(), (sc)-> sc.nextDouble());
}

Комментарии противоречат использованию пользовательских интерфейсов для предопределенных интерфейсов из JVM.

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

Ответ 5

Совершенно другой подход из моего другого ответа (и других ответов): не используйте generics, а вместо этого просто пишите методы более кратко, поэтому вы действительно не замечаете дублирование.

TL; DR: перепишите методы как

while (!scanner.hasNextX()) scanner.next();
return scanner.nextX();

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

Подписи метода Java не учитывают тип возвращаемого значения, поэтому невозможно, чтобы метод next() возвращал Integer в одном контексте, а Double - в другой (не доставляя общий супертип).

Таким образом, вы должны иметь что-то на сайтах вызовов, чтобы различать эти случаи:

  • Вы можете рассмотреть нечто вроде Integer.class или Double.class. Это имеет то преимущество, что вы можете использовать generics, чтобы знать, что возвращаемое значение соответствует этому типу. Но вызывающие могут передать что-то еще: как бы вы справились с Long.class или String.class? Либо вам нужно все обрабатывать, либо вы не выполняете во время выполнения (не очень хороший вариант). Даже при более жесткой привязке (например, Class<? extends Number>) вам все равно нужно обрабатывать больше, чем Integer и Double.

    (Не говоря уже о том, что написание Integer.class и Double.class везде действительно многословно)

  • Возможно, вы захотите сделать что-то вроде @Ward answer (что мне нравится, BTW: если вы собираетесь делать это с помощью дженериков, сделайте это например, и передать функциональные объекты, которые могут иметь дело с типом интереса, а также предоставить информацию типа для указания типа возврата.

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

При принятии любого из этих подходов вы можете добавить вспомогательные методы, которые передают соответствующие параметры методу "общего" read. Но это похоже на шаг назад: вместо уменьшения количества методов до 1, оно увеличилось до 3.

Кроме того, теперь вам нужно как-то различать эти вспомогательные методы на сайтах вызовов, чтобы иметь возможность вызвать соответствующий:

  • У вас могут быть перегрузки с параметром типа значения, а не с типом класса, например

    Double read(Double d)
    Integer read(Integer d)
    

    а затем вызывается как Double d = read(0.0); Integer i = read(0);. Но любому, кто читает этот код, будет интересно узнать, что такое магическое число в коде - есть ли какое-либо значение для 0?

  • Или проще, просто вызовите две перегрузки что-то другое:

    Double readDouble()
    Integer readInteger()
    

    Это приятно и легко: хотя он немного более подробный, чем read(0.0), он читается; и это более кратким, чем read(Double.class).


Итак, это вернуло нас к сигнатурам метода в OP-коде. Но это, надеюсь, оправдывает, почему вы все еще хотите сохранить эти два метода. Теперь для обращения к содержимому методов:

Поскольку Scanner.nextX() не возвращает нулевые значения, метод можно переписать как:

while (!scanner.hasNextX()) scanner.next();
return scanner.nextX();

Итак, это действительно легко дублировать это для двух случаев:

private Integer readInteger() {
  while (!scanner.hasNextInt()) scanner.next();
  return scanner.nextInt();
}

private Double readDouble() {
  while (!scanner.hasNextDouble()) scanner.next();
  return scanner.nextDouble();
}

Если вы хотите, вы можете вытащить метод dropUntil(Predicate<Scanner>), чтобы избежать дублирования цикла, но я не уверен, что это действительно так сильно вас спасает.

Единственная (близкая) дублированная строка менее обременительна в коде, чем все эти дженерики и функциональные параметры. Это просто старый код, который оказывается более кратким (и, вероятно, более эффективным), чем "новые" способы его написания.

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

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

Ответ 6

Не идеальное решение, но оно по-прежнему обеспечивает необходимое удаление дублированного кода и имеет дополнительное преимущество, не требующее Java-8.

// This could be done better.
static final Scanner scanner = new Scanner(System.in);

enum Read{
    Int {
        @Override
        boolean hasNext() {
            return scanner.hasNextInt();
        }

        @Override
        <T> T next() {
            return (T)Integer.valueOf(scanner.nextInt());
        }

    },
    Dbl{
        @Override
        boolean hasNext() {
            return scanner.hasNextDouble();
        }

        @Override
        <T> T next() {
            return (T)Double.valueOf(scanner.nextDouble());
        }

    };

    abstract boolean hasNext();
    abstract <T> T next();

    // All share this method.
    public <T> T read() {
        T v = null;
        while (v == null) {
            if ( hasNext() ) {
                v = next();
            } else {
                scanner.next();
            }
        }
        return v;
    }
}

public void test(String[] args) {
    Integer i = Read.Int.read();
    Double d = Read.Dbl.read();
}

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

Ответ 7

Отражение - альтернатива, если вы не заботитесь о производительности.

private <T> T read(String type) throws Exception {
    Method readNext = Scanner.class.getMethod("next" + type);
    Method hasNext = Scanner.class.getMethod("hasNext" + type);
    T value = null;
    while (value == null) {
        if ((Boolean) hasNext.invoke(scanner)) {
            value = (T) readNext.invoke(scanner);
        } else {
            scanner.next();
        }
    }
    return value;
}

Затем вы вызываете

Integer i = read("Int");