Web/_posts/2019-11-26-java_7.md

235 lines
12 KiB
Markdown
Raw Permalink Normal View History

2024-06-14 17:49:53 +02:00
---
layout: blog
title: "☕ Java Tutorial, Part 7: Overriding Methods"
description: In this Java tutorial for beginners, we cover overriding methods, calling superclass methods and toString().
tags: [Programming]
discuss: https://twitter.com/Ellpeck/status/1199339701640945664
archived: true
---
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:
```java
// 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` annotation[^1] 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:
```java
// 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.
```java
// 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:
```java
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:
```java
@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
```java
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:
```java
// 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:
```java
@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:
```java
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:
```java
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](https://gist.github.com/Ellpeck/8cf63c747313e070b7475d99e2bed5a1).
As always, happy coding!
[^1]: Annotations are another rather advanced topic that almost never comes up when using Java. They can be useful sometimes, but you'll hardly ever have a use for them.
[^2]: Which, in this case, would obviously be a bit of a nonsensical order to display this information in. But you get the idea.