CISC 3115
Modern Programming Techniques
Lecture 7
Class Inheritance

Sample Classes — UnboundedCounter & UpperBoundedCounter

A Sample Application Program

App.java App.out
public class App {
	public static void main(String [] args) {
		System.out.println("Playing with UnboundedCounter");

		UnboundedCounter uc = new UnboundedCounter();
		System.out.println("Initially: " + uc);

		for (int i = 1; i <= 20; i++)
			uc.up();
		System.out.println("After 20 up's: " + uc);

		for (int i = 1; i <= 3; i++)
			uc.down();
		System.out.println("After 3 down's: " + uc);

		System.out.println();	//---------

		System.out.println("Playing with UpperBoundedCounter");

		UpperBoundedCounter ubc = new UpperBoundedCounter(10);
		System.out.println("Initially: " + ubc);

		for (int i = 1; i <= 20; i++)
			ubc.up();
		System.out.println("After 20 up's: " + ubc);

		for (int i = 1; i <= 3; i++) 
			ubc.down();
		System.out.println("After 3 down's: " + ubc);
	}
}
			
Playing with UnboundedCounter
Initially: A unboundedCounter with value 0
After 20 up's: A unboundedCounter with value 20
After 3 down's: A unboundedCounter with value 17

Playing with UpperBoundedCounter
Initially: A upper-bounded counter with value 0 and limit 10
After 20 up's: A upper-bounded counter with value 10 and limit 10
After 3 down's: A upper-bounded counter with value 7 and limit 10
			

Implementation 1 — Unrelated Types

UnboundedCounter.java UpperBoundedCounter.java
public class UnboundedCounter {
	UnboundedCounter() {val = 0;}

	void up() {val++;}	
	void down() {val--;}	

	int getVal() {return val;}

	public String toString() {
		return "A unboundedCounter with value " + val;
	}

	private int val;
}
			
public class UpperBoundedCounter {
	UpperBoundedCounter(int limit) {
		val = 0;
		this.limit = limit;
	} 

	void up() {if (val < limit) val++;}	
	void down() {val--;}	

	int getVal() {return val;}
	int getLimit() {return limit;}

	public String toString() {
		return "A upper-bounded counter with value " + val + 
				" and limit " + limit;
	}

	private int val;
	private int limit;
}
			

Implementation 2 — Composition

UnboundedCounter.java UpperBoundedCounter.java
public class UnboundedCounter {
	UnboundedCounter() {val = 0;}

	void up() {val++;}	
	void down() {val--;}	

	int getVal() {return val;}

	public String toString() {
		return "A unboundedCounter with value " + val;
	}

	private int val;
}
			
public class UpperBoundedCounter {
	UpperBoundedCounter(int limit) {
		unboundedCounter = new UnboundedCounter();
		this.limit = limit; 
	}	

	// New methods
	void up() {if (unboundedCounter.getVal() < limit) unboundedCounter.up();}	
	int getLimit() {return limit;}
	public String toString() {
		return "A upper-bounded counter with value " + getVal() + 
				" and limit " + limit;
	}

	//Delegation methods
	void down() {unboundedCounter.down();}
	int getVal() {return unboundedCounter.getVal();}

	
	private UnboundedCounter unboundedCounter;	
	private int limit;
}
			

Implementation 3 — Inheritance

The immediate question becomes how B accesses the state/behavior it inherits from C.

Method 1. Removing the private of the parent's instance variable (val)

UnboundedCounter.java UpperBoundedCounter.java
public class UnboundedCounter {
	UnboundedCounter() {val = 0;}

	void up() {val++;}	
	void down() {val--;}	

	int getVal() {return val;}

	public String toString() {
		return "A unboundedCounter with value " + val;
	}

	/*private*/ int val;
}
			
public class UpperBoundedCounter extends UnboundedCounter {
	UpperBoundedCounter(int limit) {this.limit = limit;} 

	// Overridden methods
	void up() {if (val < limit) val++;}
	public String toString() {
		return "A upper-bounded counter with value " + val + 
				" and limit " + limit;
	}

	// new method
	int getLimit() {return limit;}

	private int limit;
}
			

Method 2. Using protected

UnboundedCounter.java UpperBoundedCounter.java
public class UnboundedCounter {
	UnboundedCounter() {val = 0;}

	void up() {val++;}	
	void down() {val--;}	

	int getVal() {return val;}

	public String toString() {
		return "A unboundedCounter with value " + val;
	}

	protected int val;
}
	
			
public class UpperBoundedCounter extends UnboundedCounter {
	UpperBoundedCounter(int limit) {this.limit = limit;} 

	// Overridden methods
	void up() {if (val < limit) val++;}
	public String toString() {
		return "A upper-bounded counter with value " + val + 
				" and limit " + limit;
	}

	// new method
	int getLimit() {return limit;}

	private int limit;
}
			

Method 3. Using the parent's methods

UnboundedCounter.java UpperBoundedCounter.java
public class UnboundedCounter {
	UnboundedCounter() {val = 0;}

	void up() {val++;}	
	void down() {val--;}	

	int getVal() {return val;}

	public String toString() {
		return "A unboundedCounter with value " + val;
	}

	private int val = 0;
}
	
			
public class UpperBoundedCounter extends UnboundedCounter {
	UpperBoundedCounter(int limit) {this.limit = limit;} 

	// Overridden methods
	void up() {if (getVal() < limit) super.up();}	
	public String toString() {
		return "A upper-bounded counter with value " + 
				getVal() + " and limit " + limit;
		}

	int getLimit() {return limit;}

	private int limit;
}
			

More Terminology

The Substitution Principle — A Consequence of the is-a Nature of Inheritance

Polymorphism

App2.java App2.out
public class App2 {
	public static void main(String [] args) {
		UnboundedCounter [] counters = {new UnboundedCounter(), new UpperBoundedCounter(10)};

		for (int i = 0; i <  counters.length; i++) {
			System.out.println("Playing with counters[" + i + "]:");
			doIt(counters[i]);
		}
	}

	static void doIt(UnboundedCounter unboundedCounter) {
		System.out.println("\tInitially: " + unboundedCounter);

		for (int i = 1; i <= 20; i++)
			unboundedCounter.up();
		System.out.println("\tAfter 20 up's: " + unboundedCounter);

		for (int i = 1; i <= 3; i++)
			unboundedCounter.down();
		System.out.println("\tAfter 3 down's: " + unboundedCounter);
	}
}
			
Playing with counters[0]:
	Initially: A unboundedCounter with value 0
	After 20 up's: A unboundedCounter with value 20
	After 3 down's: A unboundedCounter with value 17
Playing with counters[1]:
	Initially: An upper-bounded counter with value 0 and limit 10
	After 20 up's: An upper-bounded counter with value 10 and limit 10
	After 3 down's: An upper-bounded counter with value 7 and limit 10
			

What's going on here?

Static vs Dynamic Types

Given the following declarations:
UnboundedCounter uc;
UpperBoundedCounter ubc;
it is clear that the type of the variable uc is the class UnboundedCounter (and similarly for the type of ubc. If we now add code to create objects to be referenced by the variables:
UnboundedCounter uc = new UnboundedCounter();
UpperBoundedCounter ubc = new UpperBoundedCounter(12);
while we can tell the types of the objects reference by uc (it's a UnboundedCounter object) and y ubc (an UpperBoundedCounter object), it's important to realize that the objects are not actually created until we execute the program (and actually call the new operator).

The Rule of Polymorphism

As a result of the above, at runtime, there are two types associated with a reference variable: The 'rule of polymorphism' states that when we call an instance method of a class on a receiver, we look for the method in the class of the object rather than the class of the variable, i.e., we use the dynamic type rather than the static type to determine which class' methods we use.

Saying it Another Way

A Consequence of the Above Consequence

Up- and Down-Casting

Note that all of the above is similar to what we did with interface implementation and inheritance in the previous lecture; the only difference here is the 'root' of the hierarchy (UnboundedCounter in our example) is a class rather than an interface. The is-arelationship between subclass and superclass, and the consequential Substitution Principle, tells us that an object of a subclass can always appear in any context that an object of the superclass can appear in.

A Summary of Interfaces vs Classes wrt Casting

Remember upcasting is always legal and does not require an explicit cast; downcasting requires an explicit cast and may fail with a ClassCastException

Runtime vs Compile-time Polymorphism

Initializing the Superclass

Consider the following class:

class Name {
	Name(String first, String last) {
		this.first  first;
		this.last = last;
	}

	…

	private String first, last;
}

Note that one must supply first and last names when creating a Name object.

Name name = new Name("Gerald", "Weiss");

Now, consider a subclass FormalName that inherits from Name and adds a salutation:

class FormalName extends Name {

	…

	private String salutation;
}

FormalName is-a Name as well, and thus when one creates a FormalName you are also (implicitly) creating a Name object and thus need to supply a first/last name as well as the salutation, e.g.:

FormalName formalName = new FormalName("Mr.", "Gerald", "Weiss");

this gives us the constructor:

FormalName(String salutation, String first, String last) {
	this.salutation = salutation;
	…
}

The question becomes, how do we initialize the instance variables of the Name class? FormalName's constructor cannot/should-not do it:

The issue is resolved through yet another use of super:
FormalName(String salutation, String first, String last) {
	super(first, last);
	this.salutation = salutation;
}
in the context of constructors, super is used by a subclass to pass constructor arguments to the superclass

What if We Had Used Composition?

The situation is different if FormalName had been defined using composition instead of inheritance:
class FormalName {
	FormalName(String salutation, String first, String last) {
		name = new Name(first, last);
		this.salutation = salutation;
	}

	…

	private Name name;
	private String salutation;
}

Abstract Classes

Revisiting our Collection interface:
interface Collection {
	boolean add(int value);
	boolean remove(int value);
	int size();
	boolean isEmpty();
}
As we've discussed numerous times, isEmpty can leverage off size:
boolean isEmpty() {return size() == 0;}
i.e., we really don't need for every implementing class to write its own copy of isEmpty. However, interfaces do not permit method bodies, and we WANT our Shape interface to include an isEmpty method. We can get what we want with an abstract class
abstract class AbstractCollection implements Collection {
	//public abstract boolean add(int value);		// unnecessary
	/public abstract boolean remove(int value);
	//public abstract int size();
	public boolean isEmpty() {return size() == 0;}
}

Finally, … Something to Think About

What sort of hierarchy could we construct if we wanted: UpCounter, DownCounter, UnboundedCounter, UppperBoundedUpCounter, UppperBoundedCounter, LowerBoundedDownCounter, LowerBoundedCounter, BoundedCounter?

Summary

Code Relevant to This Lecture