Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Understanding Classes in Java (Part 4)

DZone's Guide to

Understanding Classes in Java (Part 4)

Our journey to an intuitive understanding of Java classes ends with polymorphism, dependency injection, and a way to frame projects in terms of state and behavior.

· Java Zone ·
Free Resource

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

In the previous three articles in this series, we have accumulated an intuitive understanding of both classes and objects, as well as how both reside in the realm of Java. In this article, we will push towards understanding more advanced concepts that transition our simple programs to practical programs. Foremost among these is polymorphism and its direct descendant dependency injection.

Polymorphism

At the core of Java is the ability to differentiate between the interface of an object and its implementation. For example, as we saw in the previous article, when we instantiate an object, we use the notation...

Vehicle myVehicle = new Vehicle("Ford", "F150", 2017, 113);


...which tells us the myValue is of type Vehicle (denoted by the preceding Vehicle type) and has an initial manufacturerName of Ford, modelName of F150, productionYear of 2017, and wheelCircumference of 113. The left side of the assignment can be viewed as the interface portion, and the right can be viewed as the implementation portion. In essence, the type on the left of the assignment denotes how the object will be treated and the type on the right of the assignment denotes what the object actually is. Since the interface type and implementation type are both Vehicle, it seems redundant to have to specify the type of the object twice, but when we have a different interface and implementation type, this separation provides us a very powerful mechanism.

For example, suppose we have the following interface and class definitions:

public interface Animal {
    public String getNoise();
}

public class Dog implements Animal {

    @Override
    public String getNoise() {
        return "Woof";
    }

    public void rollOver() {
        // Perform a roll-over action 
    }
}

public class Cat implements Animal {

    @Override
    public String getNoise() {
        return "Meow";
    }
}


If we want to create a Dog, we can simply follow the same process as before:

Dog dog = new Dog();


This is a pretty uneventful instantiation, but what if we instead used the Animal interface as the interface type and Dog as the implementation type?

Animal animal = new Dog();


This creates an interesting situation: We know that the implementation type of our animal object is Dog, but it is instead treated as an Animal since we have declared its interface type as such. This means that although its implementation type is Dog, we cannot call the rollOver() method since the Animal interface does not have this method. In fact, doing so results in the following compilation error:

The method rollOver() is undefined for the type Animal


This seems odd since we know that the type of our animal object is Dog, but the only reason that we know so is that the right side of the assignment says so. If we create a class that has a method that returns an Animal, such as the following...

public class AnimalCreator {

    public Animal makeAnimal() {
        // Intentionally left blank for now 
    }
}


...and we make the assignment using our new class, the implementation type is no longer so obvious:

AnimalCreator creator = new AnimalCreator();
Animal animal = creator.makeAnimal();


We know that animal is an Animal, but what will be returned if we call animal.makeNoise()? If makeAnimal() returns a Dog, then animal.makeNoise() will return "Woof"; if a Cat is returned, then animal.makeNoise() will return "Meow". The importance of this dichotomy of possibilities cannot be overstated: Although the type of our animal object is Animal, the result of calling one of its methods depends on the implementation type. For example, the following snippet would result in Woof being printed to the output:

public class AnimalCreator {

    public Animal makeAnimal() {
        return new Dog();
    }
}

AnimalCreator creator = new AnimalCreator();
Animal animal = creator.makeAnimal();
System.out.println(animal.makeNoise()); // Output: "Woof"


If we instead change our AnimalCreator class to create Cats instead of Dogs, we will receive Meow as our output:

public class AnimalCreator {

    public Animal makeAnimal() {
        return new Cat();
    }
}

AnimalCreator creator = new AnimalCreator();
Animal animal = creator.makeAnimal();
System.out.println(animal.makeNoise()); // Output: "Woof"


Amazingly, the instantiation of our animal object and the execution of animal.makeNoise() is identical in both cases, but the output changes solely based on the implementation type of our animal object. This is the power of polymorphism. By definition, polymorphism is the condition of concurrently maintaining multiple forms. In the case of Java, or object-oriented programming in general, we can devise a more appropriate definition:

Polymorphism is the ability for an object to define its behavior in terms of its interface, but have its runtime behavior vary depending on its implementation type

This definition may seem complex, but we have already seen it in action. In the example above, we defined our animal object in terms of the Aminal interface, but its execution (runtime) behavior of animal.makeNoise() varied depending on the implementation type: When the implementation type was Dog, the result was Woof, but when it was Cat, the result was Meow.

Polymorphism does more than putting a smirk on both the novice and expert programmer's face, but it allows us to perform some amazing techniques. For example, we can use double-dispatching to perform some complex operations without needing to know implementation type of an object:

public interface Announcer {
    public String announce(Birthday birthday);
    public String announce(NewYears newYears);
}

public class EnglishAnnouncer implements Announcer {

    @Override
    public String announce(Birthday birthday) {
        return "Happy Birthday!";
    }

    @Override
    public String announce(NewYears newYears) {
        return "Happy New Years!";
    }
}

public class ItalianAnnouncer implements Announcer {

    @Override
    public String announce(Birthday birthday) {
        return "Buon Compleanno!";
    }

    @Override
    public String announce(NewYears newYears) {
        return "Felice Anno Nuovo!";
    }
}

public interface Event {
    public void announce(Announcer announcer);
}

public class Birthday implements Event {

    @Override
    public void announce(Announcer announcer) {
        announcer.announce(this);
    }
}

public class NewYears implements Event {

    @Override
    public void announce(Announcer announcer) {
        announcer.announce(this);
    }
}

public class Composer {

    public Event getEvent() {
        // Return some event
    }

    public Announcer getAnnouncer() {
        // Return some announcer
    }
}

Composer composer = new Composer();
Announcer announcer = composer.getAnnouncer();
Event event = composer.getEvent();
event.announce(announcer); // What will the result be?


Although this example seems complicated, it is simple in polymorphic terms. We create an Event interface that accepts an Announcer. Each of the concrete Event concrete classes simply pass themselves to the supplied Announcer. This is an important step, because the type of this is the type of the concrete class, which decides which of the announce methods is called on theAnnouncer concrete class. For example, by passing this, the Birthday class ensures that theannounce(Birthday birthday) method is called. We then define two different Announcer concrete classes.

At the end of this process, the actual result depends on both the implementation type of theannouncer object and the implementation type of the event object being announced (hence the double in double-dispatch). For example, if our Composer class returned a Birthday object from its getEvent() method and an EnglishAnnouncer object from its getAnnouncer() method, the result would be Happy Birthday!. On the other hand, if our Composer class returned a NewYears event and an ItalianAnnouncer announcer, the result would be Felice Anno Nuovo!.

There is a corollary to our polymorphism definition that we can now define:

When deciding on the interface type of an object, prefer abstract classes over concrete classes, and interfaces over abstract classes

In general, the interface type of an object should be an interface. If this is not feasible, an abstract class should be used. If neither of these constraints is feasible, then a concrete class should be used. This does not mean that every object should be defined in terms of an interface, or abstract class, but rather, when practical, they should. It will take experience and a good pragmatic sense to know when to use an interface, abstract class, or concrete class as the interface type for an object.

For example, will we want to create a String, we can simply create the String object without creating an interface and hiding the String behind that interface. This is a balancing act that requires good judgment. Many times, junior programmers will go from never using interfaces as the interface type of an object to always using interfaces. Experienced developers have, over time, gained a good balance between always and never and use interfaces and abstract classes at the appropriate time. That said, the use of interfaces allows us to incorporate some very useful techniques, including the one described in the following section: Dependency injection.

Dependency Injection

By incorporating polymorphism in our applications, we can facilitate the inclusion of a very powerful technique. In a basic class, we internalize the dependencies by instantiating objects within our constructor. For example, in our Vehicle class, we directly instantiated the Engine and Transmission objects:

public Vehicle(String manufacturerName, String modelName, int productionYear, int wheelCircumference) {
    this.manufacturerName = manufacturerName;
    this.modelName = modelName;
    this.productionYear = productionYear;
    this.engine = new Engine();
    this.transmission = new Transmission();
    this.wheelCircumference = wheelCircumference;
}


This causes some protracted issues. Foremost of these is the inability of other classes to parameterize the Engine or Transmission that is used in our Vehicle. For example, if there were multiple types of Engines that could be placed into our Vehicle, we are forced to use the type that the Vehicle class designated as best. Instead, if we were to extract the Engine into an interface and allow the client that instantiates the Vehicle object to supply an engine of its choice, we end up with a Vehicle that is much more parameterizable and much less coupled to outside classes. Instead of having to know the type of Engine to create, we allow the client to supply an engine of its choice and work with it just like any other.

public interface Engine {
    public int getRpms();
    public void increaseRpms(int amount);
    public void decreaseRpms(int amount);
}

public class Subaru2Point0IEngine implements Engine {

    private SubaruEngineControlUnit ecu; // Assume this type exists
    private int currentRpms;

    public Subaru2Point0IEngine() {
        this.currentRpms = 0;
    }

    @Override
    public int getRpms() {
        return this.currentRpms;
    }

    @Override
    public void increaseRpms(int amount) {
        if (this.ecu.isSafeToIncreaseRpms(this.currentRpms, amount)) {
            this.currentRpms += amount;
        }
    }

    @Override
    public void decreaseRpms(int amount) {
        if (this.ecu.isSafeToDecreaseRpms(this.currentRpms, amount)) {
            this.currentRpms -= amount;
        }
    }
}

public class Vehicle {

    Vehicle(Engine engine) {
        this.engine = engine;
        this.transmission = new Transmission();
    }
}

// Create our new vehicle
Engine subaruEngine = new Subaru2Point0IEngine();
Vehicle crosstrek = new Vehicle(engine);


Note that for the sake of simplicity, we will ignore all other parameters apart from the Engine parameter. Without knowing the implementation type of the Engine supplied to us, our Vehicle class can operate as normal, increasing and decreasing the RPMs as needed, completely unaware that the Engine is a Subaru2Point0IEngine. This technique runs contrary to the common practice of internalizing dependencies and is therefore called inversion of control. Instead of our class controlling what the implementation type of the Engine object is, we allow some external entity to control that decision-making process.

This inversion of control is also useful when testing classes. For example, if we wanted to test the effect of accelerating our Vehicle, we could create a mock object that captures its current RPMs as follows:

public class MockEngine implements Engine {

    private int recordedRpms;

    public MockEngine(int initialRpms) {
        this.recordedRpms = initialRpms;
    }

    @Override
    public void increaseRpms(int amount) {
        this.recordedRpms += amount;
    }

    @Override
    public void decreaseRpms(int amount) {
        this.recordedRpms -= amount;
    }

    public int getRecordedRpms() {
        return this.recordedRpms;
    }
}

// Create our mock engine and the vehicle under test
MockEngine mockEngine = new MockEngine(0);
Vehicle vehicleUnderTest = new Vehicle(mockEngine);

// Accelerate and check the results
vehicleUnderTest.accelerate(10);
mockEngine.getRecordedRpms(); // Check the value


Had we simply internalized the Engine dependency within our Vehicle class, the Vehicle class, we could not have injected our mock Engine object. Using this injection example, we can create a concise definition of dependency injection:

Dependency injection is the process of supplying an object with its dependencies rather than having the object manage its own dependencies

The astute reader will recognize that we have created a regression problem: Who creates the dependencies we need and injects them? For example, who creates the object that created the Engine object we needed for our Vehicle object? In general, when declaring our dependencies, we will end up with an acyclic dependency graph, where an incoming edge denotes that the node is depended on by another node, and an outgoing edge denotes a dependency on another node. For example, the dependency graph for our Vehicle class will be the following:

Depedency graph

To resolve these dependencies, we must have some specifications or instructions on which implementation types to use. For example, when our Vehicle class depends on the Engine interface, we must provide a concrete Engine object that can be used to resolve this dependency. In essence, we are answering the following question: When I need an Engine object, whichEngine object do I use?

In order to answer this question, we must provide some dependency specification that includes sufficient information to resolve a dependency with a concrete object. For example, when some entity requests an Engine object, we respond with "use this full constructed Subaru2Point0IEngine object." If in the process of creating our Subaru2Point0IEngine object we require other objects to be injected, then we include in the specification those dependencies. With enough information in our dependency specification, we can resolve our concrete Engine object and the dependencies it requires, and provide this object to the requesting entity.

In order to complete this resolution, create a resolution context, or a resolution container, that has a sufficient dependency specification information to return a fully constructed object. In practice, this container is represented by some class with an associated specification (whether a file specification or a codified specification written in Java) with methods that allow an entity to request an object of some type. For example, the use case for this container can be illustrated in the following snippet:

ResolutionContainer container = new ResolutionContainer("some-dependency-specification-file.xml");
Vehicle vehicle = container.getObjectOfType(Vehicle.class);


Our example dependency specification file could resemble the following (for simplicity, we will ignore the constructor parameters besides the Engine parameter; these dependencies must be resolved in a true dependency injection system, but they are ignored for the sake of brevity in this example):

<dependencies>
    <dependency forInterface="Engine" useClass="Subaru2Point0IEngine" />
</dependencies>


Although this dependency specification is simple enough already, we can make it even simpler. Given some class name in Java, we can use reflection to find the interfaces that this class implements. For example, given the name Subaru2Point0IEngine, Java has the capability to allow a resolution container to find that the the Subaru2Point0IEngine implements the Engine interface. Thus, we do not need to specify which interface we want the Subaru2Point0IEngine to provide an implementation for. Instead, we simply tell the resolution container that we have a Subaru2Point0IEngine that can be used for resolving dependencies.

When the container is requested to build a Vehicle object, it traverses through the dependency graph for that object and then sees that it needs an implementation object for an Engine object. Once this discovery is made, it looks through the list of dependency specifications and creates an object of the type specified. In essence, the dependency specification simply tells the resolution container, "Try any of these objects to resolve the dependencies that you have."

An issue arises when we have two valid class entries in the dependency specification that can be used to resolve the same dependency. For example, suppose that we create another concrete Engine class called Porsche6CylinderBoxerEngine and include it in the dependency specification. During resolution, the container will see two possible concrete classes that can be used to resolve the same dependency. This ambiguity should result in an error, as two options should not be provided for resolving the same dependency. A definitive selection must be made so that only one possibility is ever present to resolve a single dependency. (If a class implements multiple interfaces and should only be used for resolving dependencies of a subset of its interfaces, then some notation can be introduced into the dependency specification to restrict its resolution scope to the specified interfaces rather than all the interfaces it implements.)

This leads to a corollary question: What if we want a dependency to be resolved to one class at some time and another class at another time? For this, we would need to create different containers and supply separate dependency specifications to each (hence why a resolution container is sometimes referred to as a resolution context). For example:

ResolutionContainer daytimeContext = new ResolutionContainer("daytime-dep-spec-file.xml");
ResolutionContainer nighttimeContext = new ResolutionContainer("nighttime-dep-spec-file.xml");

// Call the correct context dependending on the time of day


While this article is not about any specific framework, we would be remiss to not at least mention the frameworks in Java that support dependency injection: Spring and Java Enterprise Edition (JavaEE). While outside the scope of this article, most intermediate object-oriented programming will include one of these two dependency injection frameworks. Therefore, learning either or both is essential to becoming a mature Java programmer.

Conclusion

With decades of revision and building, classes are a complex topic to discuss, with different experts creating different, sometimes contradictory, rules. Through all of the politics, we find two simple concepts: State and behavior. This state and behavior are then transformed into objects that exist in memory are runtime and can perform useful work through a computer.

Apart from the conceptual understanding of classes, Java specifically includes many techniques and tools that facilitate mature object-oriented development, including polymorphism and dependency management. Combining all of this knowledge, it is important not to just become technical experts, but pragmatic practitioners. Although it is intellectually stimulating to know classes on a formal level, at the end of the day, our goal is to develop software that completes a task. In order to meet this need, we need a simple understanding; an intuitive understanding. For that, we need a gut feeling.

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

Topics:
java ,polymorphism ,dependency injection ,state ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}