29 августа 2014 в 11:29

Возвращаем дочерний класс из родительского. Факультатив tutorial

С какой целью это может понадобиться? Например, если хочется использовать "текучий интерфейс". Правда, если часть методов разместить в родительском классе, то для них не получится сделать полноценный «текучий интерфейс» — эти методы требуется приводить (кастовать) к дочернему.

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

Итак, имеется исходное состояние (коммит):

1) интерфейс IChildParam, который знает о всех требуемых методах
2) родительский класс BaseClass
1) класс-наследник ChildClass
public interface IChildParam {
    int getBaseParam();

    int getChildParam();

    <T> T setBaseParam(int i);

    <T> T setChildParam(int i);
}
public abstract class BaseClass implements IChildParam {
    private int baseParam;

    public int getBaseParam() {
        return baseParam;
    }

    public BaseClass setBaseParam(int i) {
        this.baseParam = i;
        return this;
    }
}
public class ChildClass extends BaseClass  {
    private int childParam;

    public int getChildParam() {
        return childParam;
    }

    public ChildClass setChildParam(int i) {
        this.childParam = i;
        return this;
    }
}


А так же, напишем небольшой тестовый класс, в котором опишем три возможных варианта вызова методов setBaseParam и setChildParam:
public class ClassTest {

    public static final int BASE_PARAM  = 1;
    public static final int CHILD_PARAM = 2;

    @DataProvider(name = "data")
    public static Object[][] data() {

        final ChildClass item0 = (ChildClass) new ChildClass().setBaseParam(BASE_PARAM);
        item0.setChildParam(CHILD_PARAM);

        final ChildClass item1 = (ChildClass) new ChildClass().setChildParam(CHILD_PARAM).setBaseParam(BASE_PARAM);

        final ChildClass item2 = ((ChildClass) new ChildClass().setBaseParam(BASE_PARAM)).setChildParam(CHILD_PARAM);

        return new Object[][]{{item0}, {item1}, {item2}};
    }

    @Test(dataProvider = "data")
    public void testChildClass(final IChildParam item) throws Exception {
        Assert.assertEquals(item.getBaseParam(), BASE_PARAM);
        Assert.assertEquals(item.getChildParam(), CHILD_PARAM);
    }
}


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

Что же, давайте исправлять это «недоразумение».

Для начала, изменим объявление интерфейса — вынесем Generic-тип с уровня методов на уровень интерфейса:
public interface IChildParam<T> {
    int getBaseParam();

    int getChildParam();

    T setBaseParam(int i);

    T setChildParam(int i);
}


Далее внесём изменения в объявление класса BaseClass, добавив ему Generic-тип «самого себя же»:
public abstract class BaseClass<T extends BaseClass> implements IChildParam<T> {
    private int baseParam;

    public int getBaseParam() {
        return baseParam;
    }

    public T setBaseParam(int i) {
        this.baseParam = i;
        return (T) this;
    }
}


И заключительный шаг изменений в основном коде — изменим объявление в дочернем классе:
public class ChildClass extends BaseClass<ChildClass>  {
    private int childParam;

    public int getChildParam() {
        return childParam;
    }

    public ChildClass setChildParam(int i) {
        this.childParam = i;
        return this;
    }
}


Теперь ничто не мешает нам переписать код в тесте с полноценным использованием «текучего интерфейса»:
было:
final ChildClass item0 = (ChildClass) new ChildClass().setBaseParam(BASE_PARAM);
item0.setChildParam(CHILD_PARAM);

final ChildClass item1 = (ChildClass) new ChildClass().setChildParam(CHILD_PARAM).setBaseParam(BASE_PARAM);

final ChildClass item2 = ((ChildClass) new ChildClass().setBaseParam(BASE_PARAM)).setChildParam(CHILD_PARAM);
стало:
final ChildClass item0 = new ChildClass().setBaseParam(BASE_PARAM);
item0.setChildParam(CHILD_PARAM);

final ChildClass item1 = new ChildClass().setChildParam(CHILD_PARAM).setBaseParam(BASE_PARAM);

final ChildClass item2 = new ChildClass().setBaseParam(BASE_PARAM).setChildParam(CHILD_PARAM);


Коммит с результатом

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

UPD.0: Добавил пример для комментария
UPD.1: Добавил пример для комментария
Виктор Аленьков @Borz
карма
22,7
рейтинг 7,3
Java Senior Developer
Самое читаемое Разработка

Комментарии (21)

  • 0
    спасибо за статью, всё понятно
    но ведь точно тоже самое есть на вики?
    • 0
      1) убираем ручное приведение типов
      2) выносим проверку приведения типа с уровня Runtime на этап компиляции
    • 0
      эм. из-за того, что вы изменили комментарий, мой стал неактуален.
      Отвечу на ваш изменённый.
      А где на вики? по той ссылке, что приведена в начале поста, описан только сам «текучий интерфейс», но там нет примера применения с использованием наследования классов
      • 0
        применение там есть
        Некоторые API в Java реализуют такой интерфейс, например Java Persistence API:
        
        public Collection<Student> findByNameAgeGender(String name, int age, Gender gender) {
            return em.createNamedQuery("Student.findByNameAgeGender")
                     .setParameter("name", name)
                     .setParameter("age", age)
                     .setParameter("gender", gender)
                     .setFirstResult(1)
                     .setMaxResults(30)
                     .setHint("hintName", "hintValue")
                     .getResultList();
        }
        
        • 0
          Если посмотреть на исходный код JPA, то там несколько иная реализация и иные цели.

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

          Как думаете, стоит расширить код и показать такой пример?
  • 0
    del
  • +2
    Забавно, решения практически одинаковые :)
    habrahabr.ru/post/234945/
    • +4
      рекурсия пост-комментарий?
  • 0
    Недавно видел лекцию на подобную тему. Может кому пригодится: Рекурсивное расширение типа
  • 0
    По теме Java Wildcards действительно очень мало информации на русском языке. Спасибо, что внесли свой вклад.
    Жду в следующий раз рассказ про <T super class>
  • 0
    я обычно решал такую задачу через типизацию this:
    Base<T extends Base<T>> { protected T getThis() { return this; } public T setBaseParam(int base) { this.base = base; return getThis(); } } Child extends Base<Child> { public T setChildParam(int child) { ... return getThis(); } }

    как-то так — и интерфейс не нужен и доп касты, как вот здесь
    ((ChildClass) new ChildClass().setBaseParam(BASE_PARAM)).setChildParam(CHILD_PARAM);

    PS простите за код — что-то не выходит у меня с разметкой
    • 0
      быстрое гугление дало вот такую более подробную статью weblogs.java.net/node/642849
      • 0
        мне кажется там несколько более усложнённый код для той же цели
    • 0
      Согласен, интерфейс тут не обязателен — изначально, когда писал пост, было 2 дочерних класса и думал показать пример с ними, но позже решил упростить и оставил только один дочерний класс, а вот интерфейс остался :)
      в коммите добавил в пакет ru.itbasis.demo.java.self.child.woi пример без интерфейса.
  • 0
    и еще, вот это:
    BaseClass<T extends BaseClass>
    

    неверно.

    У Вас типизированный класс BaseClass используется в unchecked версии (примерно то же самое, как написать List x = (List) new ArrayList()). Это может привести к трудноуловимым проблемам с типизацией позднее. См мой вопрос stackoverflow.com/questions/3067891/what-would-be-different-in-java-if-enum-declaration-didnt-have-the-recursive-pa
    • 0
      Да, спасибо за замечание.
      Кстати, IDEA тоже не очень корректно отрабатывает в таких ситуациях: youtrack.jetbrains.com/issue/IDEA-126457
      • 0
        Все проблемы устраняются путем BaseClass<T extends BaseClass<T>>
  • 0
    «Слегка нарушим правило», чтобы потом нарушить принцип разделения команд и запросов? :)
    • 0
      тут либо CQRS либо Fluent interface
      • 0
        Хотя я тут несколько неправ. Fluent interface может сосуществовать с CQRS требуемым ограничением (по определению) что такие set-методы должны возвращать тот же объект без изменений либо быть конечным и возвращать void

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