12 KiB
layout | title | summary | tags | discuss | |
---|---|---|---|---|---|
blog | Java Tutorial, Part 7: Overriding Methods | In this Java tutorial for beginners, we cover overriding methods, calling superclass methods and toString(). |
|
https://twitter.com/Ellpeck/status/1199339701640945664 |
So it's been a hot minute since the last tutorial, and I apologize for that. However, it seems like there are some people that actually use these tutorials to properly learn Java, and so I didn't want to leave you all hanging.
Today's tutorial is going to cover method overrides, which are another awesome object orientation concept that will help you out greatly when programming.
Let's imagine that we want our different vehicle types (Car
and Truck
) from the last tutorial to be able to print out some information about themselves to the console. To do so, we could simply add a printInformation()
method to each of the classes. However, that would end up being problematic if we wanted to print out information about the entirety of our stock, which, as you might recall, is stored in our ArrayList<Vehicle> stock
, as we would have to create instanceof
checks for both Car
and Truck
to be able to access their printInformation()
methods.
An easy fix for that would be to create a basic printInformation()
method in our base class (Vehicle
) and then override that method in our subclasses, allowing us to change their behavior. Let's see what that would look like:
// Vehicle.java
public class Vehicle {
public int amountOfWheels;
public Vehicle(int amountOfWheels) {
this.amountOfWheels = amountOfWheels;
}
public void printInformation() {
// Does nothing for now
}
}
// Car.java
public class Car extends Vehicle {
public Car(int amountOfWheels) {
super(amountOfWheels);
}
@Override
public void printInformation() {
System.out.println("This car has " + this.amountOfWheels + " wheels");
}
}
As you can see, I've added a simple printInformation()
method to the Vehicle
class (which does absolutely nothing for now). However, I have then overridden that method in the Car
class. The way I've done that is by adding a method that has the exact same name, return type and accepted parameters as the base class's method. Technically, that would already be enough to override a method, however, to make it a little clearer to read, people usually like to add the @Override
annotation1 above the method.
So what does this mean, exactly? Basically, when calling the printInformation()
method on an instance of the Vehicle
class, nothing will happen, because the Vehicle
's method is empty. However, when calling the method on an instance of the Car
class, the print statement above will be executed, printing information about the car's wheel amount.
The important thing to understand is this: Which class's method is called isn't determined by the variable type, but by the type that the object itself has. Let's check out this example:
// This is in our Main class from last time
private static void getTrucks() {
for (int i = 0; i < stock.size(); i++) {
Vehicle vehicle = stock.get(i);
vehicle.printInformation();
}
}
Now, each vehicle that isn't a car (so each truck in ou example) will not print out any information, but each of our cars will print out the information we specified above, despite the fact that we're not doing any instanceof
checks or anything else. Cool.
Calling super
methods
Now, if we wanted to also add an override of the printInformation()
method to our Truck
class, we will come across a minor annoyance: To print out the truck's amount of wheels, we'd basically have to copy the print statement from our Car
class, which is a bit ugly.
Well, that's where super
calls come to the rescue! We've already briefly touched on super calls when talking about the super constructor for extending other classes, and super calls are very similar to that. Let's change our code up a bit, and then I'll explain what it all means.
// Vehicle.java
public class Vehicle {
public int amountOfWheels;
public Vehicle(int amountOfWheels) {
this.amountOfWheels = amountOfWheels;
}
public void printInformation() {
System.out.println("Vehicle info:");
System.out.println("Wheel amount: " + this.amountOfWheels);
}
}
// Car.java
public class Car extends Vehicle {
public Car(int amountOfWheels) {
super(amountOfWheels);
}
@Override
public void printInformation() {
super.printInformation();
}
}
As you can see, I've modified the Vehicle
's method to print out the wheel amount by default, and I've changed the Car
's method to call super.printInformation()
. What this call does is simply execute its parent class's printInformation()
method, so a car will also have its wheel amount printed out just the same as any other vehicle.
It should be noted that simply calling the super
method in an override and doing absolutely nothing else is the default behavior, meaning that, in the example above, we could leave out the printInformation()
override in our Car
class completely and still get the same effect.
Now let's add a printInformation()
override to our Truck
class as well, but this time, let's extend the behavior a bit:
public class Truck extends Vehicle {
public int storageArea;
public Truck(int storageArea) {
super(4);
this.storageArea = storageArea;
}
@Override
public void printInformation() {
super.printInformation();
System.out.println("Storage area: " + this.storageArea);
}
}
Now, calling a truck's printInformation()
method will first print out "Vehicle information:", then the wheel amount, and then the storage area. If we wanted this behavior to occur in a different order, we could simply swap the two lines as follows:
@Override
public void printInformation() {
System.out.println("Storage area: " + this.storageArea);
super.printInformation();
}
Now, the storage area will be printed out first, followed by "Vehicle information:" and the wheel amount.2
Object
methods
As I briefly mentioned in the last tutorial, all classes implicitly extend Java's Object
class without you having to specify that information. This is finally going to be useful now, as this class provides some useful methods that we can override in our implementations.
toString()
This method is probably the most versatile one of the bunch: It gets called whenever an object's information needs to be converted into a string in some circumstance. For example, if you simply write
Car car = new Car(4);
System.out.println(car);
then the car's toString()
method will be called inside of println
in order to convert the car's information into a string.
However, by default, the toString()
method simply prints out some not-so-useful information about the object's internal identifier. If we override this method, however, we can make it display some more useful information. Let's do so in our Vehicle
and Car
class as an example:
// Vehicle.java
public class Vehicle {
public int amountOfWheels;
public Vehicle(int amountOfWheels) {
this.amountOfWheels = amountOfWheels;
}
@Override
public String toString() {
return "Vehicle with " + this.amountOfWheels + " wheels";
}
}
// Truck.java
public class Truck extends Vehicle {
public int storageArea;
public Truck(int storageArea) {
super(4);
this.storageArea = storageArea;
}
@Override
public String toString() {
String vehicleInfo = super.toString();
return vehicleInfo + " and " + this.storageArea + " storage area";
}
}
As you can see, Truck
additionally calls the toString()
super method and then appends some more information to the string created in Vehicle
. Pretty nice.
equals()
Remember how I told you that you should always use equals()
to compare two strings instead of the double equals sign ==
?
Well, here's why: Java's String
class overrides the equals()
method from Object
and changes its behavior so that two strings are considered equal if their contents are identical.
As an example, let's first override the equals()
method in our Vehicle
class with the default behavior that it would have if you didn't override it at all:
@Override
public boolean equals(Object other) {
return this == other;
}
As you can see, the default behavior is simply the double equals sign ==
, which compares if two variable pointers point to the exact same object, as previously explained. That's not what we might want in our example, though.
Let's expand our Vehicle
class to also have a unique identifier: The license plate's text. Let's say that we want to identify each vehicle by its plate, and so we make it the key factor in determining whether two vehicles are the same or not:
public class Vehicle {
public int amountOfWheels;
public String licensePlate;
public Vehicle(int amountOfWheels, String licensePlate) {
this.amountOfWheels = amountOfWheels;
this.licensePlate = licensePlate;
}
@Override
public boolean equals(Object other) {
if (other instanceof Vehicle) {
Vehicle v = (Vehicle) other;
return v.licensePlate.equals(this.licensePlate);
}
return false;
}
}
As you can see, each Vehicle
now accepts a license plate in the constructor, and the equals()
method is overridden in a way that makes two vehicles be considered equal if their license plates match exactly.
Now, a great example of how this could be useful is with lists. The ArrayList
class contains the contains()
method, which determines if a certain element is already present in the list. Now, the cool thing is that this method uses the equals()
method on each element to determine whether or not an element is present. We can use this behavior to check if a vehicle with a certain license plate is already in our stock pretty easily:
import java.util.ArrayList;
public class Main {
private static ArrayList<Vehicle> stock = new ArrayList<>();
public static void main(String[] args) {
stock.add(new Car(4, "AC JS 1999"));
stock.add(new Truck(10, "AC NS 1998"));
Car carToCheck = new Car(4, "AC HI 1234");
if (stock.contains(carToCheck)) {
System.out.println("The queried car is already in our stock :)");
}
}
}
Obviously, in this specific example, the text won't be printed because there isn't a car with that license plate in the stock
list.
Conclusion
So yea, today you learned about the second key concept of object orientation in Java. To put this knowledge to the test, I'm going to give you an exercise all about extending classes and overriding methods that you can try to solve if you want.
Let's imagine you're trying to create a program that allows you to draw manage different shapes, namely rectangles, right angle triangles and circles. Obviously, these shapes all have different properties, like the rectangle's and triangle's side lengths and the circle's radius. All of the shapes you currently have are stored in a list. For each of the shapes, you want to be able to calculate its circumference as well as its area. To test this program, you add a couple of shapes to your list and calculate the average of all of their areas and circumferences.
Note that you can find some methods and constants you might need in Java's default Math
class, namely Math.PI
and Math.sqrt()
, the latter of which is used to calculate a square root. If you're stuck, you can check my solution.
As always, happy coding!