Inheritance
INHERITANCE
In Lab 3, you modified several methods in the SList
class so that a "tail" reference could keep track of the end of the list, thereby speeding up the insertEnd()
method.
We could have accomplished the same result without modifying SList
--by creating a new class that inherits all the properties of SList
, and then changing only the methods that need to change. Let's create a new class called TailList
that inherits the fields and methods of the original SList class.
1 | public class TailList extends SList { |
This code declares a TailList
class that behaves just like the SList
class, but has an additional field "tail" not present in the SList
class. TailList
is said to be a subclass of SList
, and SList
is the superclass of TailList
. A TailList
has three fields: head, size, and tail.
A subclass can modify or augment a superclass in at least three ways:
- It can declare new fields.
- It can declare new methods.
- It can override old methods with new implementations.
We've already seen an example of the first. Let's try out the third. The advantage of TailList
is that it can perform the insertEnd()
method much more quickly than a tail-less SList
can. So, let's write a new insertEnd()
for TailList
, which will override SList's
old, slow insertEnd()
method.
1 | public void insertEnd(Object obj) { |
The isEmpty()
, length()
, nth()
, and toString()
methods of SList do not need any changes on account of the tail reference. These methods are inherited from SList
, and there's no need to rewrite them.
Inheritance and Constructors
What happens when we construct a TailList
? Java executes a TailList
constructor, as you would expect, but first it executes the code in the SList()
constructor. The TailList
constructor should initialize fields unique to TailList
. It can also modify the work done by SList()
if appropriate.
1 | public TailList() { |
The zero-parameter SList()
constructor is always called by default, regardless of the parameters passed to the TailList
constructor. To change this default behavior, the TailList
constructor can explicitly call any constructor for its superclass by using the super
keyword.
1 | public TailList(int x) { |
The call to super()
must be the first statement in the constructor. If a constructor has no explicit call to super
, and its (nearest) superclass has no zero-parameter constructor, a compile-time error occurs. There is no way to tell Java not to call a superclass constructor. You have only the power to choose which of the superclass constructors is called.
Invoking Overridden Methods
Sometimes you want to override a method, yet still be able to call the method implemented in the superclass. The following example shows how to do this. Below, we want to reuse the code in SList.insertFront
, but we also need to adjust the tail reference.
1 | public void insertFront(Object obj) { |
Unlike superclass constructor invocations, ordinary superclass method invocations need not be the first statement in a method.
The protected
Keyword
I lied when I said that we don't need to modify SList
. One change is necessary. The head
and size
fields in SList
must be declared protected
, not private
.
1 | public class SList { |
protected
is a level of protection somewhere between public
and private
. A protected
field is visible to the declaring class and all its subclasses, but not to other classes. private
fields aren't even visible to the subclasses.
If head
and size
are declared private, the method TailList.insertFront
can't access them and won't compile. If they're declared protected
, insertFront
can access them because TailList
is a subclass of SList.
When you write an ADT, if you think somebody might someday want to write a subclass of it, declare its vulnerable fields protected
, unless you have a reason for not wanting subclasses to see them. Helper methods often should be declared "protected" as well.
Class Hierarchies
Subclasses can have subclasses. Subclassing is transitive: if Proletariat
is a subclass of Worker
, and Student
is a subclass of Proletariat
, then Student
is a subclass of Worker
. Furthermore, every class is a subclass of the Object
class (including Java's built-in classes like String and BufferedReader.) Object
is at the top of every class hierarchy.
That's why the item
field in each listnode is of type Object
: it can reference any object of any class. (It can't reference a primitive type, though.)
Dynamic Method Lookup
Here's where inheritance gets interesting. Any TailList
can masquerade as an SList
. An object of class TailList
can be assigned to a variable of type SList
--but the reverse is not true. Every TailList
is an SList
, but not every SList
is a TailList
. It merits repeating:
!!! Every TailList
IS an SList
. !!! For example:
1 | SList s = new TailList(); // Groovy. |
Memorize the following two definitions.
Static type: The type of a variable.
Dynamic type: The class of the object the variable references.
In the code above, the static type of s
is SList
, and the dynamic type of s
is TailList
. Henceforth, I will often just say "type" for static type and "class" for dynamic type. (Dynamic type can be subclass of static type.)
When we invoke an overridden method, Java calls the method for the object's dynamic type, regardless of the variable's static type.
1 | SList s = new TailList(); |
This is called dynamic method lookup, because Java automatically looks up the right method for a given object at run-time. Why is it interesting? (Static method check when compile)
WHY DYNAMIC METHOD LOOKUP MATTERS (Worth reading and rereading)
Suppose you have a method (in any class) that sorts an SList
using only SList
method calls (but doesn't construct any SLists
). Your method now sorts TailLists too, with no changes. Suppose you've written a class--let's call it RunLengthEncoding
--that uses SLists
extensively. By changing the constructors so that they create TailLists
instead of SLists
, your class immediately realizes the performance improvement that TailLists
provide--without changing anything else in the RunLengthEncoding
class.
Subtleties of Inheritance
-
Suppose we write a new method in the
TailList
class calledeatTail()
. We can't calleatTail
on anSList
. We can't even calleatTail
on a variable of typeSList
that references aTailList
.1
2
3
4TailList t = new TailList();
t.eatTail(); // Groovy.
SList s = new TailList(); // Groovy--every TailList is an SList.
s.eatTail(); // COMPILE-TIME ERROR.Why? Because not every object of class SList has an "eatTail()" method, so Java can't use dynamic method lookup on the variable
s
.But if we define
eatTail()
inSList
instead, the statements above compile and run without errors, even if noeatTail()
method is defined in classTailList
. (TailList
inheritseatTail()
fromSList
.) -
I pointed out earlier that you can't assign an
SList
object to aTailList
variable. The rules are more complicated when you assign one variable to another.1
2
3
4
5
6
7SList s;
TailList t = new TailList();
s = t; // Groovy.
t = s; // COMPILE-TIME ERROR.
t = (TailList) s; // Groovy.
s = new SList();
t = (TailList) s; // RUN-TIME ERROR: ClassCastException.Why does the compiler reject
t = s
, but acceptt = (TailList) s
? It refusest = s
because not everySList
is aTailList
, and it wants you to confirm that you're not making a thoughtless mistake. The cast in the latter statement is your way of reassuring the compiler that you've designed the program to make sure that theSList
s
will always be aTailList
.If you're wrong, Java will find out when you run the program, and will crash with a
ClassCastException
error message. The error occurs only at run-time because Java cannot tell in advance what class of object s will reference.Recall that
SLists
store items of typeObject
. When they're recovered, they usually have to be cast back to a more specific type before they can be used. Suppose we have a list ofIntegers
. Recall thatnth()
returns type Object.1
2int x = t.nth(1).intValue(); // COMPILE-TIME ERROR.
int y = ( (Integer) t.nth(1) ).intValue(); // Groovy.Some methods are defined on every Object, though.
1
String z = t.nth(1).toString(); // Groovy.
-
Java has an
instanceof
operator that tells you whether an object is of a specific class. WARNING: Theo
ininstanceof
is not capitalized.1
2
3if (s instanceof TailList) {
t = (TailList) s;
}This
instanceof
operation will return false ifs
is null or doesn't reference aTailList
. It returns true ifs
references aTailList
object--even if it's a subclass ofTailList
. -
More Dynamic Method Selection, Overloading vs. Overriding.
1 | public interface Animal { |
1 | Animal a = new Dog(); |
flatter is overloaded
, not overridden
!
Animal does not have bark()
method!
The Method Selection Algorithm
Consider the function call foo.bar(x1)
, where foo
has static type TPrime
, and x1
has static type T1
.
At compile time, the compiler verifies that TPrime
has a method that can handle T1
. It then records the signature of this method.
Note: If there are multiple methods that can handle T1, the compiler records the “most specific” one. For example, if T1=Dog
, and TPrime
has bar(Dog)
and bar(Animal)
, it will record bar(Dog)
.
At runtime, if foo’s
dynamic type overrides the recorded signature, use the overridden method. Otherwise, use TPrime’s
version of the method.
for each
LOOPS
Java has a for each
loop for iterating through the elements of an array.
1 | int[] array = {7, 12, 3, 8, 4, 9}; |
Note that i is not iterating from 0 to 5; it's taking on the value of each array element in turn. You can iterate over arrays of any type this way.
1 | String concat = ""; |
For some reason, the type declaration must be in the for
statement. The compiler barfs if you try
1 | int i; |
Default Methods In Java
Before Java 8, interfaces could have only abstract methods. The implementation of these methods has to be provided in a separate class. So, if a new method is to be added in an interface then its implementation code has to be provided in the class implementing the same interface. To overcome this issue, Java 8 has introduced the concept of default methods
which allow the interfaces to have methods with implementation without affecting the classes that implement the interface.
Use the default
keyword to specify a method that subclasses should inherit from an interface.
1 | // A simple program to Test Interface default |
Interface vs. Implementation Inheritance
Interface Inheritance (a.k.a. what):
- Allows you to generalize code in a powerful, simple way.
Implementation Inheritance (a.k.a. how):
- Allows code-reuse: Subclasses can rely on superclasses or interfaces.
- Example:
print()
implemented in List61B.java. - Gives another dimension of control to subclass designers: Can decide whether or not to override default implementations.
- Example:
Important: In both cases, we specify “is-a” relationships, not “has-a”.
- Good: Dog implements Animal, SLList implements List61B.
- Bad: Cat implements Claw, Set implements SLList.
The Dangers of Implementation Inheritance
Particular Dangers of Implementation Inheritance
- Makes it harder to keep track of where something was actually implemented (though a good IDE makes this better).
- Rules for resolving conflicts can be arcane. Won’t cover in 61B.
- Example: What if two interfaces both give conflicting default methods?
- Encourages overly complex code (especially with novices).
- Common mistake: Has-a vs. Is-a!
- Breaks encapsulation!
- What is encapsulation? See next week.
Implementation Inheritance: Extends
Because of extends, RotatingSLList
inherits all members of SLList
:
- All instance and
static
variables. (Now thestatic
variables are shared) - All methods.
- All nested classes.
Constructors are not inherited.
Suppose we want to build an SLList
that:
- Remembers all Items that have been destroyed by
removeLast
. - Has an additional method
printLostItems()
, which prints all deleted items.
1 | public static void main(String[] args) { |
Note: Java syntax disallows super.super
.
Constructors
are not inherited. However, the rules of Java say that all constructors must start with a call to one of the super
class’s constructors.
- Idea: If every
VengefulSLList
is-anSLList
, everyVengefulSLList
must be set up like anSLList
.- If you didn’t call
SLList
constructor, sentinel would be null. Very bad.
- If you didn’t call
- You can explicitly call the constructor with the keyword
super
(no dot). - If you don’t explicitly call the constructor, Java will automatically do it for you.
1 | public VengefulSLList() { |
1 | public VengefulSLList() { |
These constructors are exactly equivalent.
If you want to use a super constructor other than the no-argument constructor, can give parameters to super.
1 | public VengefulSLList(Item x) { |
1 | public VengefulSLList(Item x) { |
Not equivalent! Code below makes implicit call to super()
, not super(x)
.
As it happens, every type in Java is a descendant of the Object
class.
Important Note: extends should only be used for is-a (hypernymic) relationships!
Common mistake is to use it for “has-a” relationships. (a.k.a. meronymic).
- Possible to subclass
SLList
to build a Set, but conceptually weird, e.g.get(i)
doesn’t make sense, because sets are not ordered.