Динамический возврат с типом вызывающего класса с использованием обобщений в Java - PullRequest
0 голосов
/ 23 сентября 2019

Рассмотрим следующие классы.

public class ClassA {

    public ArrayList<String> v1 = new ArrayList<>();

    public ClassA addOnce(String s1) {
        v1.add(s1);
        return this;
    }

}


public class ClassB extends ClassA {

    public ClassB addTwice(String s2) {
        v1.add(s2);
        v1.add(s2);
        return this;
    }

}

Теперь можно выполнить серию операций над ClassA, как показано ниже

new ClassA.addOnce("1").addOnce("2");

, но все жеНельзя достичь того же самого на ClassB, так как new ClassB().addOnce("1").addTwice("2"); недопустим, так как метод addOnce() возвращает тип суперкласса, и необходимо снова выполнить приведение к ClassB, чтобы использовать метод addTwice().Я пытался решить эту проблему с помощью обобщений в методе addOnce().

Новая реализация addOnce:

public<R extends ClassA> R addOnce(String s1) {
    v1.add(s1);
    return (R)this;
}

Но все же компилятор запрашивает приведение, поскольку он не может найти тип возвращаемого значениятолько на основе следующего вызова метода, поскольку метод может быть доступен для нескольких классов (addOnce() доступен как для ClassA, так и ClassB), а возвращаемый тип не будет сходиться.Есть ли способ вернуть this с типом вызывающего класса, обычно когда подкласс вызывает метод родительского класса?

Ответы [ 3 ]

3 голосов
/ 23 сентября 2019

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

Подход первый: generics

В частности, рекурсивные (которые относятся к самому универсальному классу):

public class ClassA<R extends ClassA<R>> { // note the recursive nature
    public ArrayList<String> v1 = new ArrayList<>();

    @SuppressWarnings("unchecked")
    protected R self() {
      return (R) this;
    }

    public R addOnce(String s1) {
        v1.add(s1);
        return self();
    }
}

Если вам не нравятся такие SuppressWarnings (и вам не следует! Они могут скрывать ошибки), вы можете обойти это, сделав abstract class AbstractClassA, с protected abstract R self() переопределением каждого подкласса.:

// AbstractClassA looks like ClassA above; it contains addOnce
class ClassA extends AbstractClassA<ClassA> {
    @Override
    public ClassA self() {
        return this;
    }
}

class ClassB extends AbstractClassA<ClassB> {
    ClassB addTwice(String s2) {
        ...
        return this;
    }
}

Это означает, что в большинстве случаев вы будете использовать подстановочный знак AbstractClassA<?>.Помимо этого уродства, это становится волосатым, если вы попытаетесь сериализовать эти экземпляры.AbstractClassA.class возвращает Class<AbstractClassA>, , а не a Class<AbstractClassA<?>>, что может заставить вас делать больше непроверенных приведений .

Кроме того, подклассы AbstractClassA не наследуйте этот <R> тип, поэтому, если вы хотите определить новые методы, которые сохраняют шаблон (например, в ClassB), вам придется повторять шаблон с новым универсальным параметром в каждом,Я предполагаю, что вы пожалеете об этом.

Подход второй: переопределить каждый метод

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

public class ClassA {

    public ArrayList<String> v1 = new ArrayList<>();

    public ClassA addOnce(String s1) {
        v1.add(s1);
        return this;
    }

}
public class ClassB extends ClassA {

    public ClassB addOnce(String s2) {
        super.addOnce(s2);
        return this; // same reference as in the super method, but now typed to ClassB
    }
    ...

}

В этом методе нет скрытых ошибок, как в первом подходе.Это работает так же, как вы думаете, что будет.Это просто боль, и это означает, что каждый раз, когда вы добавляете метод в ClassA, вы должны также добавить этот метод в ClassB (и ClassC и т. Д.), Если хотите, чтобы все продолжало работать.Если у вас нет контроля над ClassB / C / и т. Д. (Например, если вы публикуете API), то это может создать проблему и заставить ваш API чувствовать себя недоделанным.

Подход третий: Сначала цепочка из более специфичных подклассов

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

new ClassB().addTwice("2").addOnce("1");

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

Мое предложение: забудь об этом

Java просто плохо обрабатывает цепочки методов такого рода.Я бы посоветовал вам просто забыть все это и не пытаться бороться с языком в этом вопросе.Заставьте методы вернуть void и поместите каждый вызов в отдельную строку.Поработав с кодом, который попробовал первый и третий подходы, я обнаружил, что уродство вокруг него очень быстро стареет и, как правило, приносит больше боли, чем оно того стоит.(Я никогда не видел, чтобы второй подход работал, потому что это такая боль, что никто не хочет даже пробовать это. :))

1 голос
/ 23 сентября 2019

Этот вопрос напоминает мне метод overridding.

class A {

    protected List<String> list = new LinkedList<>();

    public A addOnce(String s) {
        list.add(s);
        return this;
    }
}

class B extends A {

    public B addOnce(String s) {
        return (B) super.addOnce(s);
    }

    public B addTwice(String s) {
        super.addOnce(s);
        super.addOnce(s);
        return this;
    }
}

Почему класс A должен даже знать, что класс B существует.Это B ответственность.В этом решении класс B делегирует функциональность однократного добавления к своему методу суперкласса, но приведение типов выполняется B после получения возвращенного значения.Использование дженериков действительно не требуется в текущем случае использования.

1 голос
/ 23 сентября 2019

Чтобы сделать вывод типа, вы должны объявить универсальный параметр в ClassA.ClassA должно выглядеть следующим образом:

public class ClassA<T extends ClassA<T>> {
  public ArrayList<String> v1 = new ArrayList<>();

  public T addOnce(String s1) {
    v1.add(s1);
    return (T) this;
  }
}

Подробнее о Тип Вывод

...