In my previous tutorial, I described the basics of Object Orientated Programming (OOP) in Java. In this tutorial, I’ll go through some other Java OOP concepts: inheritance, class variables/methods, abstraction and polymorphism.

Inheritance

Inheritance is the concept that some objects are specialized versions of other objects. For example, a graduate student is a type of student. We can thus say that a graduate student extends a student, or a graduate student is-a student. There are other types of students too, like high school students or undergraduate students.

For this tutorial, I will start by implementing a general Student class.

When implementing a class that is intended to be extended, you must first think of the attributes that all types of that class have. In our case, all students have a name (string), age (int) and identifier (string).

This can be thus implemented by:

public class Student {
	private String name;
	private int age;
	private String identifier;

	public Student(String name, int age, String identifier) {
		this.name = name;
		this.age = age;
		this.identifier = identifier;
	}

	public String getName() {
		return name;
	}

	// for simplicity, lets say that age doesn't change.
	public int getAge() {
		return age;
	}

	public String getIdentifier() {
		return identifier;
	}
}

The this Keyword

Notice that I use the keyword this. In Java by default when accessing variables not in the local scope, the interpreter assumes that you refer to the global variable. However this is sometimes inconvenient; this is obvious in constructors where you might want to initialize instance variables with the same name as the passed variables. One workaround is to rename the variables in the header. Another way to achieve this is using the this keyword, which explicitly tells the interpreter that you are referring to the instance variable.

Now, lets implement a subclass. I chose to implement a GraduateStudent. A graduate student definitely has an age, name and identifier (already described in the Student class.) They also have other properties. Some of these properties would be a degree (string) and a graduationYear (int). To indicate that a class should extend another class, we can use the extends keyword in the class declaration header.

Thus this class could be implemented as follows:

public class GraduateStudent extends Student {
	private String degree;
	private int graduationYear; // this would be the graduation year of their bachelors degree.

	public GraduateStudent(String name, int age, String identifier, String degree, int graduationYear) {
		super(name, age, identifier);
		this.degree = degree;
		this.graduationYear = graduationYear;
	}

	public String getDegree() {
		return degree;
	}

	public int getGraduationYear() {
		return graduationYear;
	}
}

Notice that the constructor calls super with some information. This is because we cannot access the private instance variables of the Student class within the GraduateStudent class. We must instead call the constructor of the Student class to initialize those variables. super refers to the parent class and can only be called in the first line of a constructor.

Static Methods/Variables

Have you wondered what the static keyword means in the public static void main(String[] args) header? Well in Java, methods and variables don’t always have to be associated with an instance of a class or a local scope, rather it can be associated with the class itself. This is especially useful when you want to be able to call a method without an instance of the class. Practically, static methods are used as conversions (e.g. Integer.parseInt) between data types and static variables (when combined with the final keyword) are used as constants (e.g. Math.PI).

For example, we can implement a fromStudent(Student stu, String degree, int graudationYear) method for our GraduateStudent class like this:

public static GraduateStudent fromStudent(Student stu, String degree, int graduationYear) {
	return new GraduateStudent(stu.getName(), stu.getAge(), stu.getIdentifier(), degree, graduationYear);
}

Static methods and variables can be referenced from every instance of the class and the class type itself. This means that that both of these code snippets are valid:

Student s = new Student("Qiming Weng", 24, "323449141G");
// way 1
GraduateStudent gs = GraduateStudent.fromStudent(s, "Bachelors of Computer Science", 2017);
// way 2
gs.fromStudent(s, "Bachelors of Computer Science", 2017);

Object References

In Java, all objects are references to a specific place in memory. When you declare a variable and initiate the object, the variable doesn’t store the object itself. Rather, it stores the memory location of that object. This means if you assign another variable to the original variable, only the memory location will be copied over. Although this feature is useful for both performance and convenience reasons, it also has some unintended consequences when attempting to duplicate an object.

⚠️ How NOT to Use References

When getting started, many programmers mistakingly attempt to do something like this:

Object a = new Object();
Object b = a; // b is now a clone of a right?
doSomethingTo(b);

This is wrong because the programmer assumes that assigning a to b means that b now contains a copy of a. This is wrong, you should never do this. The only way to do this is to copy over each of the object’s instance variables (and recursively do so if some of the instance variables are also objects).

Polymorphism

Polymorphism in Java comes in many forms, here are some of the common ones:

Method overloading: a method can have the same name, but have different passed parameters. The compiler determines which method that should be called.

For example, we could have this class:

class ExampleMath {
	int multiply(int a, int b) {
		return a * b;
	}
	int multiply(int a, int b, int c) {
		return a * b * c;
	}
}

and also run this code without error:

ExampleMath m = new ExampleMath(); // java provides a default empty constructor
// the compiler knows you refer to the first method, since you only provide 2 parameters
multiply(5, 4);
// the compiler knows you refer to the second method, you provide 3 parameters
multiply(4, 2, 7);

Overriding: a method in a subclass with the same method signature as one in the parent class will override the parent class’ method.

class Bird {
	public void say() {
		System.out.println("Tweet!");
	}
}

class SongBird extends Bird {
	// has the same signature as say() in Bird, so it is called instead
	public void say() {
		System.out.println("Sing!");
	}
}

// somewhere else in your code:

Bird b = new Bird();
b.say(); // outputs "Tweet!"

SongBird s = new SongBird();
s.say(); // outputs "Sing!"

Upcasting

Upcasting is a form of polymorphism, but it is different than other types because it is determined at runtime. Upcasting means that you are casting a object that is a subclass of another class to the parent class. For example, continuing the above example, you could run:

Bird b = new SongBird();

Java automatically casts the SongBird to Bird. This doesn’t remove the instance variables or methods of SongBird, they are simply inaccessible. To access them, you must manually cast back to SongBird (e.g. SongBird x = (SongBird) b).

At compile time, the compiler doesn’t know which say() you are referring to. It is left to the JVM to determine which one should be called.

Why Upcast?

Upcasting is very useful in Java for a variety of purposes. To see one reason, lets go back to our Student and GraduateStudent classes. Lets also say there’s another already implemented UndergraduateStudent class.

You want to write a method that takes a student and does something with their student identifier. All students have an identifier, but because Java is statically typed (i.e. you must declare the type of a variable when writing code), without upcasting, you would have to manually create a method for each type of student. With upcasting and the flexibility it provides, this task is infinitely easier. Just have your method accept a Student and then run the getIdentifier() method.

Abstraction

Abstraction is like a class for a class: it defines a template that other classes should follow. Abstraction is mostly used for inter-compatibility purposes. It allows client classes to use polymorphism, which heavily reduces duplicate code.

There are two main forms of abstraction in Java: Interfaces and Abstract Classes.

Interfaces

Interfaces in Java only allow you to declare method headers and constant values. Any class that implements (the keyword that indicates that a class is under the interface) this interface must implement at least the class headers given. As an example, we can implement an Animal interface that all specific animals should implement.

interface Animal {
	// notice the semicolon after each method header
	public void say(); // the noise that the animal makes
	public void move(int dx, int dy); // move the animal
	public void die(); // the animal was killed
}

Now lets implement a Bird that implements Animal.

class Bird implements Animal {
	private int x;
	private int y;
	private boolean alive;

	public void say() {
		System.out.println("Tweet!");
	}

	public void move(int dx, int dy) {
		x += dx;
		y += dy;
	}

	public void die() {
		alive = false;
	}
}

Although using an interface is suitable for this job, there is a better way.

Abstract Classes

Abstract classes allow you to define both normal methods, instance variables and abstract methods. Abstract methods are just like interface methods: they only define the method header. Abstract classes are useful when all subclasses will have some method implementations the same and other different implementations.

For example, all animals can move and die in the same way, but they talk differently.

Lets modify the Animal interface to be an abstract class:

abstract class Animal {
	private int x;
	private int y;
	private boolean alive;

	public abstract void say();

	public void move(int dx, int dy) {
		x += dx;
		y += dy;
	}

	public void die() {
		alive = false;
	}
}

class Bird extends Animal {
	public void say() {
		System.out.println("Tweet!");
	}
}

Next Step

Now you’ve learned more about OOP, try writing a tester class for all of the concepts that you have learned.

If you are up to a challenge: write a fully fledged traffic simulation Java application using the concepts you’ve learned. The complexity as well as what you will implement is up to you. I would be more than glad to look over your code, simply email me.