Unit 9 - Inheritance
Unit 9 Lesson
- Setup
- What is Inheritance, and why is it useful?
- SuperClasses and Subclasses with Cars
- Writing Constructors for Subclasses
- Hack 1
- Overriding Methods
- Hack 2
- Super Keyword
- Creating References Using Inheritance Hierarchies
- Polymorphism
- Hack 3
- 9.7 Object Superclass
Setup
One of the main goals of learning inheritance is to teach you how to structure your code. So, we're going to leave a lot of the hacks up to your creativity, and you can make anything you want as long as it follows the structure of inheritance.
Open up a new notebook in your personal fastpages (you can do this in pairs or groups). The hacks in this lesson will be performed from scratch in this notebook.
What is Inheritance, and why is it useful?
Imagine you wanted to create a racing game. You would need a bunch of different cars-bugattis, lamborghinis, rolls royce. Each of these cars would have different features; they would have different fuel types, tires, and engines. A car like an Aston Martin would have a spoiler inside the car for aerodynamics. Lambos have cools doors for when you pull up to a race. But all of these cars would also have a lot in common; all cars have gear shifts, gas, breaks, and steering.
We could model each car with its own class, with attributes and methods specific to each car. But we would find that we're repeating a lot of the same code over and over - it doesn't really make sense to redefine the methods for gas and breaks for every single car, when it will do the same thing.
This is a great use case for inheritance. We can define a base "Car Class" that has the methods and attributes common to every car - steering methods, gas and break methods, and attributes like speed and miles per gallon.
Each car will "extend" from this base class. This means that it "inherits" the methods and attributes in the base Car Class (this is why it's called Inheritance). But each of the new car classes, for example a Bugatti Class, will have its own special methods and attributes.
public class Car {
protected String brandName;
protected double range;
protected double doorNumber;
protected double maxSpeed;
// Constructor for the attributes present in the superclass
public Car(String brandName, double range, double doorNumber, double maxSpeed) {
this.brandName = brandName;
this.range = range;
this.doorNumber = doorNumber;
this.maxSpeed = maxSpeed;
}
public void gas () {
System.out.println("Go!");
}
public void brake () {
System.out.println("Stop!");
}
public void gearShift () {
System.out.println("Use the stick");
}
public void steer () {
System.out.println("turning left...");
}
public void horn () {
System.out.print("honking... ");
}
}
public class TeslaModelS extends Car {
// Additional attribute not present in the superclass
protected String hornSound;
// Constructor for Subclass
public TeslaModelS(String brandName, double range, double doorNumber, double maxSpeed, String hornSound) {
// We use the Superclass constructor for the shared attributes through the keyword "super"
super(brandName, range, doorNumber, maxSpeed);
// hornSound is not in the Superclass, so we add it separately in the constructor
this.hornSound = hornSound;
}
// We use override to change the functionality in the subclass of an existing method in the superclass
@Override
public void gearShift () {
System.out.println("Use the gear selector next to the wheel");
}
public void steer () {
System.out.println("turning right...");
}
// Here, we don't fully change the functionality of the existing horn method in the superclass
// Instead, we take all of the functionality of the superclass method, and then add on to it
public void horn () {
super.horn();
System.out.print(hornSound);
}
public static void main(String[] args) {
// 5 argument constructor
TeslaModelS modelS = new TeslaModelS("Tesla", 396, 4, 200, "eugh");
// We can still use the methods from the child class, even though we didn't mention them in the subclass!
modelS.gas();
// Using the overridden method
modelS.gearShift();
modelS.steer();
// Using the method we added on to
modelS.horn();
}
}
TeslaModelS.main(null);
In the previous example, the TeslaModelS class was a subclass. Recall that a constructor is what initializes the values of the attributes of a class when a new object of the class is created. How do we write constructors for subclasses?
Well, if the attributes of the subclass are exactly the same as the attributes of the superclass, then we can just use the constructor of the superclass. Take a look at the superclass, the Car class. We have defined a constructor within that class already.
To use the constructor of the superclass in our subclass, we need to use some specific syntax. Namely, we need to make use of the super keyword. This allows us to use constructors that we define in the superclass.
Look again at the constructor in the TeslaModelS class. We have passed in 4 attributes to the super constructor, meaning that we are making use of the 4 argument constructor in the superclass.
But our subclass also has an additional attribute: hornSound. We don't include this in the superclass since this isn't customizable for most cars. This attribute needs to be included in the constructor for our subclass, but it doesn't make sense to make a completely new constructor for all 5 attributes when 4 of the attributes are shared with the superclass.
So, what we do is call the superclass constructor with 4 attributes, but then simply add an additional assignment for the 5th, unique, attribute.
In your own notebook, make any class with 2 attributes and 0 methods. Create a 2 argument constructor for that class. This will be your superclass. Now, create a subclass that extends from the superclass you made. Create 1 additional attribute in your subclass that was not present in the superclass.
Then, create a constructor for the subclass that uses the superclass constructor with the super keyword, and then adds an additional assignment for the third attribute.
Overriding allows a subclass or child class to provide a specific implementation of a method that has already been provided by a super-classes or parent classes. When a method in a subclass has the same name, same parameters or signature, and same return type (or sub-type) as a method in its super-class, then the method in the subclass is said to override the method in the super-class.
// the existing method in the superclass
public void gearShift () {
System.out.println("Use the stick");
}
public void steer () {
System.out.println("turning left...");
}
// We use override to change the functionality in the subclass of an existing method in the superclass
@Override
public void gearShift () {
System.out.println("Use the gear selector next to the wheel");
}
public void steer () {
System.out.println("turning right...");
}
Add a method to the superclass you created before. This method should be very general; it should only have functionality that you know for sure will be needed in almost every single subclass. In your subclass, override this method. Remember that overriding the method will give your subclass the specific functionality it needs from that method.
The two main uses of the super keyword are to use constructors in the superclass and methods in the superclass in a child class.
In this example child class TeslaModelS, the super keyword can be found in the constructor and the method horn. When it says, super(brandName, range, doorNumber, maxSpeed) in the constructor, the super keyword is used to utilize the super class constructor inside the child class. Next, in the horn() method, super.horn() is called. This line utilizes the super keyword to call the method horn() from the parent class Car.
Type Diagram
It is similar to looking at it like a family tree. A is the superclass, or the head of the family. The descendants are the subclasses.
So
public class A public class B extends A public class C extends B
A Reference refers to an object of the class, or the object of an inherited class. So an Inheritance Hierarchy can create references
public class ToyotaCamry extends Car {
public ToyotaCamry (String brandName, double range, double doorNumber, double maxSpeed) {
super(brandName, range, doorNumber, maxSpeed);
}
@Override
public void gearShift () {
System.out.println("Manual shift!");
}
public void turbo (int a) {
System.out.println("Engaging turbo " + a);
}
public void turbo (int a, int b) {
System.out.println("Engaging turbo " + a + " and nitro " + b);
}
public static void main(String[] args) {
// 4 superclass argument constructor
ToyotaCamry camry = new ToyotaCamry("Toyota", 348, 4, 145);
// Using the overridden method
camry.gearShift();
// Using the overloaded method
camry.turbo(1);
camry.turbo(1, 1);
}
}
ToyotaCamry.main(null);
Polymorphism means "many forms". It means that we do one thing in many ways through inheritance.
For example, the gearShift method defined in the superclass simply says to use the stick. But in manual cars and a Tesla, you need to do different things. So, we show the method to shift gears being used in two different ways in two different classes. We can have two different implementations through overriding methods.
Look at the different outputs for the method in the ToyotaCamry and TeslaModelS class.
This method of polymorphism is called Runtime Polymorphism. It is also called Dynamic Binding or Dynamic Method Dispatch. When you hear any of these things, think of method overriding.
Imagine if you want to do a single method in a single class, but in different ways. For example, in the ToyotaCamry class, we need to either engage a turbo by itself, or a turbo with a nitro. So, we have two methods called turbo. When the method is called, the actual functionality is resolved at compile-time.
When we call camry.turbo(1), then the single argument turbo method is called. When we call camry.turbo(1, 1), the two argument turbo method is called.
Having two methods with the same name but different arguments and functionalities is called method overloading.
This is another form of polymorphism called Compile Time Polymorphism or Static Polymorphism. When you see these terms, think of method overloading.
Create another subclass from your original superclass. Now, implement method overloading in this subclass. Remember that this means having two methods with the same name, but with different arguments. The method you are using for method overloading doesn't have to exist in the superclass. This will implement Static Polymorphism.
Next, override the method in your superclass in your new subclass. Make sure it has different functionality than your method in the other subclass. This will implement Runtime Polymorphism.
Important Things to Know
- Object class is the superclass of all other classes in Java
- Object is part of the java.lang package (know for imports)
- Important Object class methods include: boolean equals(Object x) & String toString()
- Object class' subclasses override the equals and toString methods
toString Method
- Prints out the attributes of an object
- Converts string object into a string
equals Method
- Compares two strings
- Returns a boolean value of true if equal, otherwise returns false
// Demonstration of toString method
// Utilized toString methods in all FRQs so far, here's an example from FRQ1
public String dayOfWeekToString() {
return ("{ \"month\": " + this.month + ", " + "\"day\": " + this.day + ", " + "\"year\": " + this.year + ", "
+ "\"dayOfWeek\": " + this.dayOfWeek + " }");
}
public String toString() {
return dayOfWeekToString();
}
// Demonstration of equals method
// Outputs boolean value of true or false
// If one object equals another
public class Student
{
private String name;
public Student(String name)
{
this.name = name;
}
public static void main(String[] args)
{
Student student1 = new Student("Bob");
Student student2 = new Student("Jeff");
Student student3 = student1;
Student student4 = new Student("A");
Student student5 = student4;
System.out.println(student1.equals(student2));
System.out.println(student2.equals(student3));
System.out.println(student1.equals(student3));
System.out.println(student3.equals(student4));
System.out.println(student3.equals(student4));
System.out.println(student5.equals(student4));
}
}
Student.main(null);