Last week we started looking at class design in Java, and launched our quest to create the Forneymon game.

We finished designing the Burnymon class, which (after implementing our “best practices” of class design) looked like the following.

Example

Recall the Burnymon class shown below (sans comments for brevity). Name and define the purpose of each section marked with a // [?].

package forneymon;
 
public class Burnymon implements MinForneymon {
 
    // [?] #1
    private int health;
    private String name;
 
    // [?] #2
    Burnymon (String n) {
        this.health = 15;
        this.name = n;
    }
 
    // [?] #3
    public int takeDamage (int dmg, String type) {
        this.health -= dmg;
        return this.health;
    }
 
    public String toString () {
        return this.name + " " + this.name;
    }
 
    // [?] #4
    public int getHealth ()  { return this.health; }
    public String getName () { return this.name; }
 
}

Now, recall that we wanted to create two different Forneymon: the Burnymon and the Dampymon. The difference was that:

  • Burnymon start with 15 health, but Dampymon start with 25.
  • Dampymon take 5 extra damage whenever they take “burny” damage.

So, with these differences in mind, suppose we decide to just copy-pasta our Burnymon class and make changes as necessary:

package forneymon;
 
public class Dampymon implements MinForneymon {
 
    private int health;
    private String name;
 
    Dampymon (String n) {
        // [!] Difference in starting health:
        this.health = 25;
        this.name = n;
    }
 
    public int takeDamage (int dmg, String type) {
        // [!] Difference in damage taken from "burny" dmg
        if (type.equals("burny")) {
            dmg += 5;
        }
        this.health -= dmg;
        return this.health;
    }
 
    public String toString () {
        return this.name + " " + this.name;
    }
 
    public int getHealth ()  { return this.health; }
    public String getName () { return this.name; }
 
}

Question

What are some (of the many) problems of simply copying and pasting classes with shared properties and behavior?

So then, what do we do if we have classes with shared properties and behavior but don’t want to define them in each individual class?

It would be nice if we could have some means of talking about the “general” properties and behavior while being able to perform “specification” for exceptions to the general.

Let’s look at how to do that next!


Inheritance

To review: our goal is to specify the “general” properties and behaviors of some classes in one place, and then specify exceptions to those generalities elsewhere.

For this, we turn to inheritance.

Conceptual Overview

Definition

In programming, inheritance describes the mechanisms by which one class can obtain the properties and behaviors of another.

Definition

In Java, we define superclasses that contain “general” or “default” properties and methods and subclasses that inherit those generalities while specifying any exceptions to them.

Definition

A subclass is said to inherit all* of the properties and methods defined in the extended superclass, thus gaining their definitions. (*though perhaps without access to all inherited properties and methods if those are restricted as private)

Example

For example, if we’re designing classes to represent household Pets, then we can pet() any Pet (the general), whereas Dogs (which are Pets) will bark when they speak() and Cats (also Pets) will meow.

In other words, if we designed a Pet superclass, it would contain the general properties and methods (like pet()) whereas the Dog and Cat classes would individually specify their class-specifics like speak().

Definition

When we draw inheritance relationships between classes, we typically draw arrows from subclasses into superclasses.

Take a Pet, Dog, and Cat example, which might look like the following:

Syntactic Overview

The syntax for implementing inheritance in Java is relatively simple.

Toolkit

A subclass inherits methods and fields from a superclass if the subclass uses the extends keyword in its definition.

This syntax looks like the following:

public class SubClass extends SuperClass {
    // ...
}

Definition

The effect of the above is that SubClass gains all of the public fields and methods defined in SuperClass.

Definition

If a field or method has the same name or signature in both SubClass and SuperClass (called an overloaded method), then a SubClass object will use the SubClass’ definition, and a SuperClass object will use the SuperClass’ definition.

Example

Implement, in simple Java classes, the Inheritance diagram with the Pet, Dog, and Fish example from above.

public class Pet {
 
    public void speak () {
        System.out.println("*silence*");
    }
 
}
public class Dog extends Pet {
 
    // Note the @Override annotation used to flag that a method
    // overrides another in the superclass
    @Override
    public void speak () {
        System.out.println("Woof!");
    }
 
}

Toolkit

@Override is an annototation best-practice that both improves code transparency (indicates that the intent is to override a method in a superclass) and additional error check that alerts the compiler to look for a superclass method with the EXACT signature that you’re overriding — mistakes can be avoided if you accidentally provide an override with a different signature.

public class Fish extends Pet {
 
    // ...nothing here!
 
}
public static void main () {
    Pet p = new Pet();
    Dog d = new Dog();
    Fish f = new Fish();
 
    // What will get printed below?
    p.speak();
    d.speak();
    f.speak();
}

This example demonstrates how wecan define default behaviors in a superclass and then override those when necessary in a subclass (as we’ll see in a moment).

Debug

Warning: complex chain of logic below: there’s one idiosyncracy with subclass construction that takes a little bit of thinking:

  1. Superclasses maintain their own fields, and may still have expectations for the initial values of these fields.

  2. We give fields initial values by using constructors.

  3. Classes may define multiple constructors, including: default constructors (which expect no parameters), and parameterized constructors (which do).

    // Default constructor:
    public ClassName () {
        // ...
    }
    // Parameterized constructor:
    public ClassName (type1 param1, ...) {
        // ...
    }
  4. In the case that a superclass defines a default constructor, it will be called in a subclass’ constructor by default.

  5. If, however, a superclass has no default constructor, but does specify a parameterized constructor, then we have to explicitly call that constructor with arguments in our subclass’ constructor.

(that’s a bit of a language-design issue that I don’t expect you to care about, but just explains the following:)

Debug

When a superclass has no default constructor, but defines at least one parameterized constructor, then a superclass constructor must be called inside of each subclass’ constructor.

Toolkit

The super keyword provides a reference to a superclass from within the subclass.

Toolkit

To call a superclass’ constructor from within a subclass’, we simply use the syntax: super(arg1, arg2, ...);

Toolkit

The super keyword may also be used to access the superclass’ methods explicitly by the syntax super.superClassMethod(...);

This last point is useful if we want to use behaviors of methods in a superclass that share a name with a method in the subclass (and so we can use the super keyword to distinguish which version of the method we’re invoking).

Example

Let’s apply all of the above to our running Forneymon example:

  1. All Forneymon have a name, health, can take damage, and say their name twice when “printed out” (the toString() behavior).
  2. Burnymon start with 15 health, Dampymon start with 25.
  3. Dampymon take 5 bonus damage from “burny” damage.

Question

Which properties and behaviors above constitute the “general” Forneymon traits and which the exceptions?

Remark

Rule of Thumb: try to capture the general properties / methods in a superclass, and then have subclasses define any exceptions to those.

So, let’s try creating a Forneymon superclass, and then have Burnymon and Dampymon inherit from it!

Since we want our Forneymon to be the superclass, we define the “default” behavior as follows:

package forneymon;
 
public class Forneymon {
 
    private int health;
    private String name;
 
    Forneymon (int h, String n) {
        this.health = h;
        this.name = n;
    }
 
    public int takeDamage (int dmg, String type) {
        this.health -= dmg;
        return this.health;
    }
 
    public String toString () {
        return this.name + " " + this.name;
    }
 
    public int getHealth ()  { return this.health; }
    public String getName () { return this.name; }
 
}

Note: we took all of the properties and methods common to all Forneymon and put them in the Forneymon class… now let’s change the Burnymon class to be a subclass and exploit the awesome power of inheritance!

package forneymon;
 
// [!] Burnymon is now a subclass of Forneymon
public class Burnymon extends Forneymon implements MinForneymon {
 
    Burnymon (String n) {
        // [!] Forneymon has a constructor that
        // takes health and name, so call it here
        super(15, n);
    }
 
}

Remark

Note: when both extending a class and implementing an interface, we list the extension first in the class declaration.

That’s literally the entire class after inheritance came to save the day!

Notice our previous BattleTest.java still works:

package forneymon;
 
public class BattleTest {
 
    public static void main(String[] args) {
        Burnymon emberlizard = new Burnymon("Dave");
        emberlizard.takeDamage(5, "dampy");
        System.out.println(emberlizard.getHealth());
    }
 
}

Some things to note:

  • Our Burnymon object above still has a health and name property, even if it cannot access them directly (they were just given in the Forneymon definition, but are private to the Forneymon class’ methods).
  • The Burnymon object also has access to all of the non-private methods of its superclass (including takeDamage and getHealth).

Amazing; now, let’s sort out the Dampymon class.

The only thing we need to do differently is provide our own definition for the takeDamage method (which is special for Dampymon). So, we have:

package forneymon;
 
public class Dampymon extends Forneymon implements MinForneymon {
 
    Dampymon (String n) {
        super(25, n);
    }
 
    @Override
    public int takeDamage (int dmg, String type) {
        if (type.equals("burny")) {
            dmg += 5;
        }
        return super.takeDamage(dmg, "burny");
    }
 
}

Note the @Override annotation in the above:

Quickly verifying our functionality (not to be used in the place of proper unit testing, except for this example):

package forneymon;
 
public class BattleTest {
 
    public static void main(String[] args) {
        Dampymon sudsturtle = new Dampymon("Sudsy");
        sudsturtle.takeDamage(5, "burny");
        // [!] Should print 15 since our Dampymon start with
        // 25 health and take 5 bonus damage from burny dmg
        System.out.println(sudsturtle.getHealth());
    }
 
}

Inheritance Miscellany

Just a couple of last tidbits:

Remark

Java does not support multiple-inheritance (a class extending more than 1 other class).

Definition

However, a class can implement multiple interfaces.

Definition

Interfaces themselves can employ inheritance (such that an interface extends another interface, so you can interface while you interface dog).


Abstract Classes

We have one final issue to talk about…

Consider our black-hat hacker back to ruin everyone’s good time playing Forneymon:

package forneymon;
 
public class BattleTest {
 
    public static void main(String[] args) {
        Forneymon missingNu = new Forneymon(9001, "HAHAHA");
        missingNu.takeDamage(5, "burny");
        System.out.println(missingNu.getHealth());
    }
 
}

Dang it! Our nemesis has found a way to side-step our fool-proof construction system and made an all-powerful Forneymon with health over 9000!

This begs an important philosophical and programmatic question: should we be able to make instances of categories?

For example: does it make sense to make a new Pet object (of indeterminate species, etc.) or a new Cat object? a new Dog object?

Sometimes, we need to indicate that our classes are not meant to be instantiable.

Toolkit

To indicate that objects cannot be constructed from a certain class, we tag that class as abstract.

Definition

Abstract classes are useful for inheritance, constructing libraries of functions, and a variety of other reasons, but cannot have any instances created.

So, to stop our hacker from creating a Forneymon object (rather than a specific Burnymon or Dampymon, which happen to inherit from Forneymon), we make it abstract:

// [!] Note the abstract tag
abstract public class Forneymon {
    ...
}

The previous BattleTest.java program will no longer compile (as expected), and we’ve saved the day once again.


The Object Superclass

If you’ve been following along with the tutorial, you may have been surprised that, after implementing the MinForneymon.java interface, we only needed to implement takeDamage before the compiler was happy, rather than both takeDamage and toString… what gives?

It turns out that, in Java, certain methods are so essential to a class’ behavior that default implementations are provided for free through an interesting mechanic:

Definition

In Java, ALL classes, custom or otherwise, inherit from the Object superclass, which provides default implementations of essential methods.

You can take a look at the Object class’ interface here:

Assignment

Although all fairly important methods, the following methods are the most important for us to be aware of:

  • String toString() returns a String representation of the object, which by default, is usually some nonsense looking numbers alongside the classname.
  • boolean equals(Object other) indicates whether or not this object is “equal to” another.
  • int hashCode() returns an integer hash code value for the object (we’ll talk about this later in the course at depth). Think of this as a semi-unique (i.e., as unique as possible) numerical representation for the object that is sensitive to its fields’ values.

Remark

Note: the above is still consistent with the idea that Java supports only single-superclass inheritance, since if A is a subclass of B, Object is still a superclass of B, meaning its methods will still be inherited by A.

The above is also why we’re able to compare two objects with the equals method even if we didn’t define it ourselves.

Equivalence Tests

Example

Consider the following example comparing various Forneymon to other objects using the default equals method, and try to predict what the outcome of each will be.

public class BattleTest {
 
    public static void main(String[] args) {
        Burnymon b1 = new Burnymon("Burny"),
                 b2 = new Burnymon("Burny"),
                 b3 = b1;
        Dampymon d1 = new Dampymon("Dampy");
 
        System.out.println(b1.equals(b1));
        System.out.println(b1.equals(b2));
        System.out.println(d1.equals(b1));
        System.out.println(b1 == b2);
        System.out.println(b1 == b3);
        System.out.println(b1.equals("lul here's a string"));
    }
 
}

Remark

There are likely some surprises in the above, and we’ll need a future lecture to fully understand them! For now, we’ll intuit some things…

Question

Which, of the above printed equality statements, is the most surprising, and why might we want to change its behavior?

So what’s the deal with all this equals and == business, and why do we have separate operations?

Toolkit

In Java, the == operator and equals method are separate and satisfy two different tests of equality:

  • Identity Equivalence: a == b returns true only if a and b are the exact same object in memory OR if a and b are two primitives with the same value. a.equals(b) is the same as a == b if the Object class’ equals method is NOT overridden
  • Field / Property Equivalence: If it is overridden, it provides the opportunity for class designers to specify how two objects are equivalent based on their properties / fields.

Example

To use an example from Biology, let’s say we have 2 sheep: Sheep A, and Sheep B which is a perfect clone of Sheep A. In this setting:

  • A == B => false because A and B are not the same Sheep
  • A.equals(B) => true if we defined equals to mean that A and B share the same genetic makeup.

So, let’s take a look at how we can specify this behavior of equals for our own custom classes as well!

Overriding Object Methods

Toolkit

One of the most common Object class methods to override is the equals method such that designers can specify precisely how two objects are considered equivalent based on their properties or fields.

Example

For Forneymon, suppose we wish two consider two to be equal if:

  • They are the same Forneymon subclass (e.g., both Burnymon) AND
  • They have the same name.

Let’s stub our overridden equals method in Forneymon.java:

...
@Override
public boolean equals (Object other) {
    // ...
}
...

A note on the above:

Debug

public boolean equals (Object other), the method signature, must precisely match the Object superclass’, including the parameter for some other Object to compare to (since we’ll allow comparisons to anything else, like Strings in the example above).

Toolkit

Note: One nice property of inheritance is that specifying parameters of a superclass type allows for arguments to be passed of any subclass type — since Object is a superclass of everyone, a parameter of type Object is the universal recipient!

Now, to complete the method, let’s include our criteria that the objects only be considered equal if they have the same Forneymon type (i.e., class) and name.

We’ll start by checking that they’re the same class using yet another tool from the Object superclass!

Toolkit

The getClass() method returns the runtime class of the calling object (which would, e.g., be Burnymon for objects b1 and b2 above).

...
@Override
public boolean equals (Object other) {
    // Ensure that this Forneymon and whatever we pass in the "other"
    // parameter are the same class / type -- if they aren't, return false
    if (this.getClass() != other.getClass()) {
        return false;
    }
    ...
}
...

The above will allow us to immediately return false if we, e.g., compare a Burnymon to a Dampymon, or any other type like Strings.

Great! Now, let’s implement the check to see if they have the same name… but it’s not quite as simple as the following:

...
@Override
public boolean equals (Object other) {
    // Ensure that this Forneymon and whatever we pass in the "other"
    // parameter are the same class / type -- if they aren't, return false
    if (this.getClass() != other.getClass()) {
        return false;
    }
    // [X] Syntax error here -- why?
    return this.name.equals(other.name);
}
...

Question

Why is our compiler upset with the new return statement we just added?

In other words, we could pass in a String or any other object as an argument into our equals method, and plainly, we’d like it if those returned false!

This fact reveals a hidden tool of both inheritance and interfaces:

Toolkit

A parameter whose type is declared as a will accept arguments of subclass types as well (thus, a parameter of type Object will take any object).

To get around this quirk, the solution rests on several assumptions at this point in our code:

  1. We know that this is a Forneymon (since we’re overriding the equals method in the Forneymon class), so it must have a name field.
  2. other must be a Forneymon as well since, by the time we’ve reached the second return statement, we must not have ended the function call with the first (wherein the classes are unequal), but we just don’t know which.
  3. Thus, if we could tell the compiler to trust that other is a Forneymon, it’ll be happy!

Question

What is a tool we saw in an earlier lecture to turn one type into another?

Toolkit

Downcasting is the process of interpretting a superclass reference/object as a subclass reference/object, and has the syntax: ((SubclassToCastTo) objectBeingCasted)

So, to interpret the Object other (with Object as the superclass) as a Forneymon (the subclass), we say: ((Forneymon) other):

...
@Override
public boolean equals (Object other) {
    // Ensure that this Forneymon and whatever we pass in the "other"
    // parameter are the same class / type -- if they aren't, return false
    if (this.getClass() != other.getClass()) {
        return false;
    }
    // [!] With the cast, everyone's happy!
    return this.name.equals(((Forneymon) other).name);
}
...

Now… there’s just oooone last obnoxious detail that we’ll have to hand-wave for now: remember that hashCode method the Object class defines?

Debug

Warning: whenever you override equals you must also override hashCode, or your objects won’t play well with certain data structures we’ll examine later.

Why? Well, that’s a discussion for a later lecture… for now, there’s an easy work-around.

Toolkit

To easily provide a hash code, simply use the Objects.hash(x, y, z,...) method, where x, y, z are the values of any object fields you used to test equivalence.

You’ll also need to import java.util.Objects; to use the above.

For us, that looks like the following, and again, don’t worry about it too much for now, we’ll learn why this is the case later:

...
@Override
public int hashCode () {
    return Objects.hash(this.name);
}
...

Example

Try re-running our equivalence test above to see if it checks out!

Nice! Now… that was a lot of stuff… how about some practice?