При создании конструктора с суперклассом родитель не может вернуть экземпляр дочернего класса [дубликат]


На этот вопрос уже есть ответ здесь:

Если я использую шаблон builder для настройки новых объектов, я могу иметь два класса, такие как Game и HockeyGame (показано ниже). Когда я хочу создать новый HockeyGame, я получаю его конструктор и начинаю вызывать методы для настройки объекта по мере необходимости.

Проблема, с которой я сталкиваюсь, показана в основной функции. Как только я вызываю один метод из суперкласса, он возвращается как intance Game.Builder, и я больше не могу вызывать какой-либо метод из дочернего класса.

Как лучше всего справиться с этим?

Главная.java

class Main {

    public static void main(String[] args){

        HockeyGame hg = new HockeyGame.Builder()
                .setScore(5)
                .setTimeLimit(3600)
//--------------------------------------------------------------------
                .setIceTemperature(-5) // Error! Cannot call setIceTempurature() on
                                       // an instance of Game.Builder
//--------------------------------------------------------------------
                .build();


    }
}

Игра.java

public class Game{

    int score;
    int timeLimit;

    public Game(int score, int timeLimit) {
        this.score = score;
        this.timeLimit = timeLimit;
    }

    public static class Builder {

        int score;
        int timeLimit;

        public Builder setScore(int score) {
            this.score = score;
            return this;
        }

        public Builder setTimeLimit(int timeLimit) {
            this.timeLimit = timeLimit;
            return this;
        }

        public Game build() {
            return new Game(score, timeLimit);
        }
    }
}

Хоккейная игра.java

public class HockeyGame extends Game {

    float iceTemperature;

    public HockeyGame(int score, int timeLimit, float iceTemperature) {
        super(score, timeLimit);
        this.iceTemperature = iceTemperature;
    }

    public static class Builder extends Game.Builder {

        float iceTemperature;

        public HockeyGame.Buidler setIceTemperature(float iceTemperature) {
            this.iceTemperature = iceTemperature;
            return this;
        }

        public HockeyGame build(){
            return new HockeyGame(score, timeLimit, iceTemperature);
        }
    }
}

Спасибо.

2 3

2 ответа:

Вам нужно использовать трюк getThis(), который широко распространен в очень беглом коде API.

Сначала вам нужно сделать свой Game.Builder родовым в себе :

public static class Builder<B extends Builder<B>>

Затем вы добавляете метод getThis():

public B getThis() {
    return (B) this;
}

Теперь вы меняете сеттеры, чтобы вернуть B и return getThis() вместо this:

public B setTimeLimit(int timeLimit) {
    //...
    return getThis();
}

Ваш класс расширения также должен быть универсальным сам по себе:

public static class Builder<B extends Builder<B>> extends Game.Builder<B>

Теперь вы можете использовать код, и он будет "помнить" предполагаемый тип:

HockeyGame hockeyGame = new HockeyGame.Builder<>().setScore(10)
        .setTimeLimit(20)
        .setIceTemperature(-1)
        .build();

Это окончательный код выглядит примерно так:

public class Game {

    private final int score;
    private final int timeLimit;

    private Game(final Builder<?> builder) {
        this.score = builder.score;
        this.timeLimit = builder.timeLimit;
    }

    public static class Builder<B extends Builder<B>> {

        private int score;
        private int timeLimit;

        public B setScore(int score) {
            this.score = score;
            return getThis();
        }

        public B setTimeLimit(int timeLimit) {
            this.timeLimit = timeLimit;
            return getThis();
        }

        protected B getThis() {
            return (B) this;
        }

        public Game build() {
            return new Game(this);
        }
    }
}

public class HockeyGame extends Game {

    private final float iceTemperature;

    private HockeyGame(final Builder<?> builder) {
        super(builder);
        this.iceTemperature = builder.iceTemperature;
    }

    public static class Builder<B extends Builder<B>> extends Game.Builder<B> {

        private float iceTemperature;

        public B setIceTemperature(float iceTemperature) {
            this.iceTemperature = iceTemperature;
            return getThis();
        }

        @Override
        public HockeyGame build() {
            return new HockeyGame(this);
        }
    }
}

N. B: я сделал поля private final, а также основные конструкторы типа - это заставляет людей использовать Builder. Кроме того, конструктор может взять Builder<?> и скопировать переменную оттуда - это немного приводит код в порядок.


Фактический взлом, как вы, возможно, заметили, здесь:

public B getThis() {
    return (B) this;
}

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

public static class FootballGame extends Game {

    private FootballGame(final Builder<?> builder) {
        super(builder);
    }

    public static class Builder<B extends HockeyGame.Builder<B>> extends Game.Builder<B> {

        float iceTemperature;

        @Override
        public FootballGame build() {
            return new FootballGame(this);
        }
    }
}

Это это взорвется во время выполнения с ClassCastException. Но метод setter вернет HockeyGame.Builder, а не FootballGame.Builder, поэтому проблема должна быть очевидной.

Попробуйте что-нибудь вроде этого

Вы явно возвращаете его к объекту HockeyGame.Builder и работаете с его собственным методом(методами).

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

HockeyGame hg = ((HockeyGame.Builder)(new HockeyGame.Builder().setScore(5)
                                                              .setTimeLimit(3600)))
                                                              .setIceTemperature(-5)
                                                              .build();

Также, setIceTemparature должен возвращать объект HockeyGame.Builder, чтобы иметь возможность build на нем.

public Builder setIceTemperature(float iceTemperature) {
    this.iceTemperature = iceTemperature;
    return this;
}