Learn Java

Java State

Sometimes you need to create a class whose behavior depends on the current state of its object. If there are few states and no complex transitions between them, an if-else statement or a switch statement will be enough. But as the number of states grows and their logic becomes more complex, the code of the class may start looking more like spaghetti and hard to extend. The State design pattern might help you avoid such mishaps.

The problem

Imagine that you are writing a program simulating the interaction between an observer and a wild animal. Suppose that the animal can be in one of the two states; calm and angry, and react to the observer depending on the state it is currently in. We must keep in mind that later we may want to simulate the animal's behavior in greater detail and add more states, such as frightened, hungry, tired, etc.

First, we'll determine the animal's behavior in different states and how it transitions from one state to another. Let's presume that when the animal is calm, it rests in the grass. And when it is angry, it chases the observer. For the sake of simplicity, let's determine that the current state of the animal changes after a while. We will represent it using the following class:

class Animal {
    private String state = "calm";

    public void observe() {
        if (state.equals("calm")) {
            System.out.println(this + " is lying in the grass.");
        } else {
            System.out.println(this + " is chasing you. Run away!");
        }
    }

    public void onTimePassed() {
        if (state.equals("calm")) {
            state = "angry";
        } else {
            state = "calm";
        }
    }

    @Override
    public String toString() {
        return "Animal";
    }
}

Now let's test our Animal implementation:

class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        for (int i = 0; i < 4; i++) {
            animal.observe();
            animal.onTimePassed();
        }
    }
}

We will get this output:

Animal is lying in the grass.
Animal is chasing you. Run away!
Animal is lying in the grass.
Animal is chasing you. Run away!

Not bad! But we are going to have some issues if we try to extend this class with new behavior, like adding more states and/or announcements of state changes. If the behavior gets more complex, the class will grow in size. If the behavior gets more diverse, the class will have too many responsibilities. This is where the State design pattern comes to our rescue.

The idea of a pattern

The State design pattern helps us avoid the above problem. The idea is to introduce an abstraction that will represent different states of a class. The abstraction will describe the common interface for classes encapsulating the logic of each state. This will allow an instance of the class to vary its behavior depending on its state. For clients, it will look like the class of the object changes during runtime. The image below shows a UML diagram of the State pattern.

State design pattern

Let's go through all elements of this diagram. Context defines an interface for interaction with clients and stores a reference to a ConcreteState object that represents the current state of ContextState defines an interface for encapsulating the behavior associated with a concrete state of ContextConcreteState classes implement the State interface and determine the behavior associated with a certain concrete state of Context. The transitions between the states may be defined either in Context or in ConcreteState classes. The latter option is more flexible but requires ConcreteState to have a reference to Context in order to set its current state. It also requires any ConcreteState class to be aware of at least one other ConcreteState.

Now let's apply the State pattern for our Animal class and extend it a bit by adding announcements when the Animal object enters a new state.

Pattern implementation

First, we define a common interface for different states. This interface will correspond to State in the diagram we looked at in the previous section:

interface State {

    void observe(); // used to observe the current behavior of Animal

    void onStateEntry(); // invoked when the state changes

    void onTimePassed(); // triggers the change of the state
}

The Animal class will correspond to Context in the diagram. We will add a State state field to it for storing the current state and the corresponding setter. We will delegate the invocations of the observe and onTimePassed methods to the corresponding methods of its current state object.

class Animal {
    private State state;

    public void observe() {
        state.observe();
    }

    public void onTimePassed() {
        state.onTimePassed();
    }

    public void setState(State state) {
        if (this.state == null) {
            this.state = state; // no announcement on initial assignment
        } else {
            this.state = state;
            this.state.onStateEntry();
        }
    }

    @Override
    public String toString() {
        return "Animal";
    }
}

After that, we create implementations of the State interface for the two states. Each of them will correspond to a ConcreteState in the diagram. In this example, we choose to let the state objects define when and where their transition to another state happens.

Here is the implementation of the calm state:

class CalmState implements State {
    private final Animal animal;

    CalmState(Animal animal) {
        this.animal = animal;
    }

    @Override
    public void observe() {
        System.out.println(animal + " is lying in the grass.");
    }

    @Override
    public void onStateEntry() {
        System.out.println(animal + " calms down and stops.");
    }

    @Override
    public void onTimePassed() {
        animal.setState(new AngryState(animal));
    }
}

And here is a code example of the angry state:

class AngryState implements State {
    private final Animal animal;

    AngryState(Animal animal) {
        this.animal = animal;
    }

    @Override
    public void observe() {
        System.out.println(animal + " is chasing you. Run away!");
    }

    @Override
    public void onStateEntry() {
        System.out.println(animal + " has spotted you.");
    }

    @Override
    public void onTimePassed() {
        animal.setState(new CalmState(animal));
    }
}

Now we are finally ready to test our new implementation!

Showcase

Let's test the behavior of the Animal class the same way we did before:

class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.setState(new CalmState(animal));

        for (int i = 0; i < 4; i++) {
            animal.observe();
            animal.onTimePassed();
        }
    }
}

Now let's run this code and see the output (this time we will have announcements displayed when the animal's state changes):

Animal is lying in the grass.
Animal has spotted you.
Animal is chasing you. Run away!
Animal calms down and stops.
Animal is lying in the grass.
Animal has spotted you.
Animal is chasing you. Run away!
Animal calms down and stops.

We can extend the animal's behavior by adding other implementations of State and defining their transitions.

Conclusion

The State pattern is useful when the behavior of your class depends on its current state and changes during runtime, or when the methods of your class have a lot of if-else statements. However, this pattern requires you to create extra interfaces and implementations and may lead to excessive code complexity if your class has few states which change rarely. If used in appropriate cases, this pattern allows for easy extension of the class by adding more states represented by new classes that encapsulate the necessary behavior.

Create a free account to access the full topic

“It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.”
Andrei Maftei
Hyperskill Graduate