Class modifiers

Just as with fields and methods, classes can be modified to change their access or behavior.

final: If you declare a class final, it cannot be extended. String, for example, is final.

abstract: Abstract classes exist ONLY to be superclasses. You cannot create an object of that type directly.

Let's consider the case of a Student. Students from different schools have many things in common with each other, and many others that are unique to their school. It would make sense to have a parent class Student that holds common data points (e.g. name, school name, GPA, etc.) and child classes to hold school-specific information (e.g. alpha code, obstacle-course scores, etc.)

Our first abstract class

public abstract class Student {
    protected String school;
    protected String name;
    protected int id;
    protected float gpa;

    public Student(String n, String s) {
        name = n;
        school = s;
        setID();
    }

    protected abstract void setID();    

    public String toString() {
        // Object.getClass() returns the name of the object's class
        return String.format("%8d %10s %15s   (%s)", id, name, school, getClass());
    }    
}

Some items to note:

Our first final classes

public final class Mid extends Student {
    private static int nextAlpha = 6;

    public Mid(String n) {
        super(n, "USNA");
    }

    protected void setID() {
        id = 170000 + nextAlpha;
        nextAlpha+=6;
    }

    protected String schoolCheer() {
        return "Go Navy";
    }

    protected void driveYP() {
        System.out.println("Full speed ahead");
    }

    public static void main(String[] args) {
        Mid[] marray = new Mid[5];
        marray[0] = new Mid("Alice");
        marray[1] = new Mid("Bob");
        marray[2] = new Mid("Charlie");
        marray[3] = new Mid("Dave");
        marray[4] = new Mid("Eve");
        for (int i=0; i<marray.length; i++) {
            System.out.println(marray[i]);
        }
    }
}
public final class Cadet extends Student {
    private static int nextCadetID = 1;

    public Cadet(String n) {
        super(n, "West Point");
    }

    protected void setID() {
        id = 20170000 + nextCadetID;
        nextCadetID+=1;
    }

    protected String schoolCheer() {
        return "Go Army";
    }

    protected void driveTank() {
        System.out.println("What does this lever do?");
    }

    public static void main(String[] args) {
        Cadet[] carray = new Cadet[5];
        carray[0] = new Cadet("Albert");
        carray[1] = new Cadet("Billy");
        carray[2] = new Cadet("Carol");
        carray[3] = new Cadet("Don");
        carray[4] = new Cadet("Eddie");
        for (int i=0; i<carray.length; i++) {
            System.out.println(carray[i]);
        }
    }
}

Some items to note:

What can Polymorphism do for me?

So all of this inheritance and abstract classes stuff seems vaguely helpful, but why all the excitement? Is this really worth a whole six weeks? Yes. Polymorphism is part of the reason why.

Polymorphism says that if you declare a pointer to a particular type, you can make it point to an object of that type or of any sub-type of that type. Thus I can say:

  Student s = new Mid();

without error. The ramifications of this are substantial.

Most of the time you won't do just that, but instead you will create a group of objects, each of a different type with the same super-type:

public class TestStudents {
    public static void main(String[] args) {
        Student[] sarray = new Student[10];
        sarray[0] = new Mid("Alice");
        sarray[1] = new Mid("Bob");
        sarray[2] = new Mid("Charlie");
        sarray[3] = new Mid("Dave");
        sarray[4] = new Mid("Eve");
        sarray[5] = new Cadet("Albert");
        sarray[6] = new Cadet("Billy");
        sarray[7] = new Cadet("Carol");
        sarray[8] = new Cadet("Don");
        sarray[9] = new Cadet("Eddie");

        System.out.println("List of items from Student[]");
        System.out.println("----------------------------");
        for (int i=0; i<sarray.length; i++) {
            System.out.println(sarray[i]);
        }
    }
}

Output:

List of items from Student[]
----------------------------
  170006      Alice            USNA   (class Mid)
  170012        Bob            USNA   (class Mid)
  170018    Charlie            USNA   (class Mid)
  170024       Dave            USNA   (class Mid)
  170030        Eve            USNA   (class Mid)
20170001     Albert      West Point   (class Cadet)
20170002      Billy      West Point   (class Cadet)
20170003      Carol      West Point   (class Cadet)
20170004        Don      West Point   (class Cadet)
20170005      Eddie      West Point   (class Cadet)

Some items to note:

Now all the different object that can do Student things are in one place.

More importantly, though, given an arbitrary element of sarray, we are guaranteed that it has certain members. Each of these, because they have Student as a superclass, has a name, school, id, gpa, and schoolCheer(). So, we can do:

    for (int i=0; i<sarray.length; i++)
        System.out.println(sarray[i].schoolCheer());
    }

The final important observation here is, not only do things just work, without having lots of if/then bits to them, but it factors the code amongst the classes. Without polymorphism, the loop might look like this:

    for (int i=0; i<sarray.length; i++) {
        if(sarray[i].school.equals("USNA")) {
            System.out.println("Go Navy");
        }
        else if(sarray[i].school.equals("West Point")) {
            System.out.println("Go Army");
        }
    }

We can dispense with all of that code, scattering it among each of the individual classes. The code doesn't live in a single place, it lives with each class. The main focus of the code is no longer on finding the correct method to call, but on the various classes/objects.

What can Polymorphism not do for me?

Polymorphism does not let you easily reach methods that exist in the subclass but not the superclass. See if you can understand why:

Call unknown methods in a subclass:

GOOD:

Student s = new Mid("Alice");
System.out.println(s.name);

BAD:

Student s = new Mid("Alice");
System.out.println(s.driveYP());

Two workarounds:

Student s = new Mid("Alice");
((Mid)s).driveYP() ;

Mid m = new Mid("Alice");
m.driveYP();