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

18

У меня есть два похожих, но разных типа, блоков кода в 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;
}

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

задан klepsydhra 03.01.2018 в 14:05
источник

7 ответов

37

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

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);
}
    
ответ дан Ward 03.01.2018 в 14:27
источник
5

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

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

Например:

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 случаев.

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

    
ответ дан Andy Turner 03.01.2018 в 14:18
источник
5

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

    
ответ дан Aashish Bharadwaj 03.01.2018 в 17:07
источник
3

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

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

В вашем случае отличием двух сокращенных кодов является доступ к методам 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;
}

Еще одна проблема заключается в том, что тип переменной 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 ... и вызывать ваши IDE автоматический рефакторинг метод extract :

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.

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

    
ответ дан Timothy Truckle 03.01.2018 в 14:32
источник
2

Совершенно другой подход из моего другого ответа (и других ответов): не используйте 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 везде действительно многословно)

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

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

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

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

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

    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, поскольку исходные методы возвращают тип в штучной упаковке; Я не знаю, действительно ли это действительно желательно, или просто артефакт того, как был написан цикл. Однако полезно вообще не создавать эти объекты, если они вам действительно не нужны.

    
ответ дан Andy Turner 04.01.2018 в 08:57
источник
1

Не идеальное решение, но оно по-прежнему обеспечивает необходимое удаление дублированного кода и имеет дополнительное преимущество, не требующее 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();
}

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

    
ответ дан OldCurmudgeon 03.01.2018 в 14:57
источник
0

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

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");
    
ответ дан zhh 03.01.2018 в 14:38
источник