Zero-Day Java Guide 4 – Advanced OOP

Interfaces

There are cases in software development when we need to agree upon a contract that dictates how software components interact with each other. This allows many people to write independent code while still maintaining the core functionality of the software component. For example, all automobiles have the same basic functionality: turning and signaling, for example. Trucks and motorcycles, along with cars, can perform these functionalities as well. In Java, we call these contracts interfaces. In this section, we’re going to be looking at the syntax and use of interfaces.

Introduction to Interfaces

In order to understand how to use interfaces, let’s take a look at an interface:

public interface Operable {
    int LEFT = 1;
    int RIGHT = 2;

    void changeDirection(int direction);
    void signal(int direction);
}

Interfaces get their own separate Java file. They start with an access modifier, the interface keyword, and the name of the interface. As we can see, we can declare only constants or method signatures in an interface. Any variables we put in an interface is automatically a public static final constant, and any methods will be public and abstract. We can make methods static by adding the static modifier to a method. Note that we don’t need to provide an implementation for these method signatures. This is from the concept of the contract: any implementing class can choose to implement it however it wants!

We can make a class agree to the contract by making it implement the interface. When a class does this, the contract is enforced, and the class must implement the methods defined by the method signatures in the interface.

public class Car implements Operable {
    ...
    @Override
    public void changeDirection(int direction) {
        ...
    }

    @Override
    public void signal(int direction) {
        ...
    }

}

In the above code, we can make our Car class implement the Operable interface by adding implements Operable to our class definition. We also have to add method bodies to all of the methods signatures in the interface. We also see there’s something else above the declaration: @Override. This is called an override annotation and it tells the compiler that we’re implementing the interface’s method with this current class’s implementation. We’ll see more of this annotation when we get to inheritance.

Interfaces as Types

Moving on with the metaphor of contracts, we can actually use an interface as a reference type, even though we can’t create a new instance of a interface. The reason we can’t directly instantiate a new interface like Operable op = new Operable()  is because we need some class to implement the contract of the Operable interface. The Car class is one example since it provides method bodies. Suppose we have another class: public class Truck implements Operable {…} . We can use interfaces as a type, but we have to initialize them to a class that implements that interface.

Operable machine1 = new Car();
Operable machine2 = new Truck();

machine1.signal(Operable.RIGHT);
machine1.changeDirection(Operable.RIGHT);

machine2.signal(Operable.LEFT);
machine2.changeDirection(Operable.LEFT);

In the above code, we’re declaring two Operable variables and setting them to be different reference types, one of which is a Car and the other a Truck. Java enforces the rule that if a variable has the type of an interface, it must be set to an instance of an class that implements that particular interface. In our case, the objects are new instances of the Car and Truck class, both of which implement Operable. The only methods we can call on those objects are those that are declared in the Operable interface. This is because we don’t care about how Car or Truck implement its method, just that they are!

In this section, we covered interfaces and how we can relate them to contracts. In an interface, we can define method signatures and constants and have a class implement that interface. When it does, then the class is responsible for providing method bodies for the method signatures in the interface. We can also use an interface as a reference type as long as we set it to an object whose class implements that interface.

Inheritance

I’ve mentioned before that we can model real-world things as objects. Suppose we were modeling a zoo. We would have classes for many different animals and instances for each individual in the zoo. But we know more than just the animals themselves: we know the relationships between them. For example, we might have a zoo with reptiles and mammals. Within reptiles, we have snakes and lizards; within mammals, we have lions, tigers, and bears. All reptiles share properties between them and so do mammals. In our code, we don’t want to have to rewrite the same code many times for each mammal or reptile we decide to put in our zoo. This is where inheritance can help us. In this section, we’re going to learn more about inheritance as well as a few OOP principles that are strongly related to inheritance.

How to Use Inheritance

Let’s continue with the zoo analogy. We have mammals that share the the properties of “live birth”, “warm-blooded”, and “has fur”. In fact, big cats like tigers and lions share even more properties. This is an example of the natural hierarchy we have in the real world, and we can simulate that using inheritance. With it, we can create hierarchies that make software easier to follow and prevent us from rewriting the same code over and over again. Let’s create a class called Animal and assume it has the basic properties of all animals that belong to a zoo:

public class Animal {
    private int zooId;
    private String name;

    public Animal() {
        this.zooId = 0;
        this.name = "Unnamed";
    }

    public Animal(int zooId, String name) {
        this.zooId = zooId;
        this.name = name;
    }

    public int getZooId() {
        return zooId;
    }

    public void setZooId(int zooId) {
        this.zooId = zooId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " is speaking!");
    }
}

In the above code, we’re giving each animal an id and a name, as well as getters and setters for them. We also declare another method called speak, but we’re revisit that later. Now we can create separate Mammal and Reptile class that are subclasses of the Animal class: public class Mammal extends Animal { … }  and public class Reptile extends Animal { … }

We can make a class a subclass by saying extends after the class declaration (as opposed to implements for interfaces) and the name of the class. Regarding interfaces, a class can only extend a single class, but can implement multiple interfaces. When a class extends another class, it inherits all of the states and behaviors of its superclass. For example, if I create an instance of a Mammal, I can call someMammal.setName(“Steve”)  since setName is a public method on Animal. This also means that each subclass of Animal also retains the property of a zoo ID and a name, but, due to encapsulation, those fields are only accessible through the field’s getter and setters. If we wanted to access them directly, we could make the fields protected instead of private. As we can see, using inheritance prevents us from copying-and-pasting code from the Animal class to the Mammal and Reptile classes.

Typecasting

In Java, there might be cases where we want to treat a superclass as a specific subclass or an interface as a specific implementing class or even a double as a int. We call this conversion typecasting. When we typecast something, we’re essentially telling the compiler that we’re confident we know what we’re doing and to treat the type as another valid type. Let’s see how we can do this with primitive types:

double pi = 3.1415
int x = (int) pi;

In the above code, the value of x is going to be 3 since we’re treating pi as an int and ints can’t have decimal parts. The syntax for typecasting is to put a set of parentheses with the type that we want to convert the variable to in front of the variable we want to convert. Eclipse and any other IDE will only allow this if the types are somewhat related, else the IDE will throw an error immediately. For example, if we were going to typecast pi to a Car, Java would obviously have an issue with that! Let’s see what typecasting for reference types looks like.

Animal anAnimal = new Mammal();
Mammal steve = (Mammal) anAnimal;
Reptile kevin = (Reptile) anAnimal;

In the above code, we’re instantiating an animal and setting to a new Mammal object. We can do this because Animal is a superclass of Mammal. This is similar to why we can use an interface as a reference type by setting it to a new instance of class that implements that interface. In the second line, we typecast the animal to be a Mammal, and we’re correct and the code will work perfectly time. On the other hand, the third line will cause a runtime error because we know for a fact that anAnimal is not a Reptile. We’re illustrating here that typecasting of reference types can be dangerous unless we’re sure the type of the incoming variable is related to the type of the outgoing, typecasted variable.

Overriding and Overloading Methods

We’re going to return to methods to talk about the difference between overriding and overloading. We’ve already seen an example of overriding when we learned about implementing interfaces. We can override any of the superclass’s methods (that don’t have the final modifier) by using the exact same method signature and putting the override annotation before the method signature:

public class Mammal {
    ...
    @Override
    public void speak() {
        ...
    }
    ...
}

In the above code, the Mammal class is overriding the Animal class’s speak method. When we instantiate a new Mammal object and call the speak method, we’ll be executing the code in the Mammal class, not the Animal class.

On the other hand, method overloading is when we have a method with the same access modifier, return type, and method name, but different parameters types or a different parameter order. For example, we can declare another method speak that takes a String input parameter, for example. This is an example of method overloading, which is different than method overriding.

public class Mammal {
    ...
    public void speak(String text) {
        ...
    }
    ...
}

In the above code, we overloaded the speak method so now users can call the speak method with no parameters or pass in a String for the parameterized speak. It’s important to note that the parameter types have to be in a different order or there need to be a different number of them for overloaded to occur, else Java will throw an error saying we have duplicate methods. The parameter names don’t really matter: it’s the types and the order in which they appear as method parameters. To reiterate, method overriding occurs when a subclass defines it’s own implementation of a superclass’s method. Method overloading occurs when we have two methods with the same signature with the exception of the parameter types or the order in which they appear.

Accessing the superclass

In cases where we have a subclass, there may be times where we need to access the superclass’s methods or constructors. We already learned how to access the methods: plain old method calling. Intrinsically, we are calling the superclass’s methods by explicitly using the super keyword: super.speak() ; for example.

However, suppose we want to use the superclass’s constructors in a subclass. Instead of calling the setters for name and zooId, we just want our Mammal class to call the Animal class’s parameterized constructor since it already sets those. We’ve already seen how we can do this in previous sections, but let’s take a more in-depth look.

public class Mammal {
    ...
    public Mammal(int zooId, String name) {
        super(zooId, name);
        ...
    }
    ...
}

In the above code, we have a call to the superclass’s constructor by using the super keyword and then parentheses with the parameters, almost like a method call whose name is super. This statement will jump to the superclass’s constructor and give the id and name to the Mammal, as well as any other initialization that the Animal constructor might do. If we just wanted to call the default constructor, then we would just have the super keyword and empty parentheses. In most cases, it’s a good idea to design our software so in the constructor of subclasses, we’re calling the superclass constructor to do any generic setup, as opposed to duplicating code in each subclass.

Polymorphism

The last OOP concept we’ll talk about is that of polymorphism. In order to understand the concept, let’s suppose we have a few subclasses of Mammal: Dog, Cat, and Duck. Now let’s look at how we can use polymorphism.

Mammal fido = new Dog();
Mammal kitty = new Cat();
Mammal amy = new Duck();

fido.speak();
kitty.speak();
amy.speak();

In the above code, we declare three Animal variables and set them to be new instances of different subclasses. Again, this is completely valid since Mammal is a superclass. Note that we can only call methods on it that are defined in Mammal since that’s the type of the variable. Next we have calls to the speak method. Now Java knows that fido is a Dog, kitty is a Cat, and amy is a Duck, so when we call the speak method, Java will call that respective subclass’s overridden speak method, if it overrode it. In the case of fido, kitty, and amy, Java will call Dog, Cat, and Duck’s speak methods respectively. If they don’t exist, then Java will call the superclass’s speak method.

This language feature of Java can be incredibly helpful. In a more practical example, suppose we have a superclass called SortingAlgorithm and we have several subclasses called MergeSort, InsertionSort, and HeapSort. We can just declare a SortingAlgorithm object and assign it to a particular sorting algorithm depending on our data set. (Sorting algorithms can vary in efficiency depending on the data set.) Then, when we call something like mySortingAlgorithm.sort(…) , and Java will call the sort method of the subclass of the particular variable.

In this section, we learned about how we can create relations between classes using inheritance. We learned how to use inheritance in Java code through the extends keyword. In addition, we went even further to learn how we could tell the compiler to treat a particular variable as another class type. Revisiting methods, we differentiated method overriding, which is when a subclass implements a superclass’s method, and overloading, which is when two methods have almost the same signatures, with the exception of different parameter types. We also learned how we can access the superclass’s methods and constructors with the super keyword. Polymorphism is the last of the OOP concepts we covered, where Java will know what implementing/subclass method to call.

 

Over the course of this book, we’ve come a long way from learning variables and variable types! We quickly moved on to operators and decision and looping control flow statements, like if-then statements and while loops. Moving from concrete to more abstract, we transitioned into object-oriented programming. Along with objects, we covered member variables, access modifiers, and methods. Interfaces and inheritance were covered next as well as typecasting and polymorphism.

Towards the beginning of this book, we came in with barely any knowledge on Java but we’ve learned so much as we progressed on our journey. However, this isn’t the end of our journey. There are still many more concepts, structures, and topics in Java that are really interesting and useful. Consider this book as just the springboard for your next Java project, further learning, or your next journey.

“For the wise man looks into space and he knows there is no limited dimension.” – Lao Tzu