- COMP.CS.140
- 5. Inheritance
- 5.3 Introduction to inheritance in Java
Introduction to inheritance in Java¶
Inheritance is a mechanism that allows us to define a class as an extension or customized version of another class. An inheriting class is called a subclass and an inherited class a superclass. Objects of a subclass can be seen as consisting of (sub)objects of the inherited superclasses plus some possible additional or substituting (“overriding”) members defined by the subclass.
In order to keep things simple, Java allows only single inheritance: a class may directly inherit
at most one class. A class can indirectly inherit multiple classes, but the inheritance hierarchy
(like a “family tree”) will be linear, e.g. “class Teacher
inherits class Employee
that
inherits class Person
. Here class Teacher
would inherit Employee
directly and
Person
indirectly via Employee
. Teacher
objects would have the properties of both
Employee
and Person
.
A Java class may inherit another class by adding the keyword extends
and the name of the
inherited superclass after the class name. If you already know C++ inheritance, it is worth noting
that Java does not use inheritance accessibility modifiers: Java inheritance works in similar
fashion as public inheritance in C++. A subclass can refer to all non-private members of its
superclasses. A subclass has no access to their private members and thus can manipulate those
only within the possibilities offered by non-private functions of the superclasses.
Let us consider an example where the previous three classes would have roughly the following forms:
The following figure shows how their objects would be represented in Java.
Note how a Teacher
object first contains the non-static member variables of Person
, then of
Employee
and only finally of Teacher
itself. In similar manner an Employee
object
contains first the non-static member variables of Person
and only then of Employee
. This
corresponds to how each class extends its superclass. Some of the member variables are shown in
light color. This depicts the fact that they are private members and thus the subclass cannot
access them directly even though they are contained inside the same object. The figure omits e.g.
the technical detail that each Java class object also has a reference to the so-called virtual
table of its class. Virtual tables contain information about member functions.
The next figure lists the constructors and public member functions of Person
, Employee
and
Teacher
. The figure illustrates the fact that constructors are not inherited, but non-private
member functions are. Here inheriting a function does not mean that the inheriting function would
hold its own separate copy of the function; it simply means that non-private functions of
superclasses can be called via subclass objects.
It is important no note that subclass constructors must ensure that also the superclasses are
initialized properly. To this end a subclass constructor must pass any required parameters to
a superclass constructor. The preceding example already hinted at this in how the subclass
constructors received more parameters than needed to initialize their “own part”: they also
received parameters required in order to initialize superclasses. Such parameters can be passed
to a superclass constructor via the keyword super
, and this must be done as the first step of
a constructor.
The example code below shows complete Java implementations for Person
, Employee
and
Teacher
. The classes should be placed into separate files.
public class Person {
private String name;
private String personId;
public Person(String name, String personId) {
this.name = name;
this.personId = personId;
}
public String getName() {
return name;
}
public String getPersonId() {
return personId;
}
}
public class Employee extends Person { // Inherit Person: add "extends Person" to the end.
private String title;
private float salary;
public Employee(String name, String personId, String title, float salary) {
super(name, personId); // First initialize the superclass Person.
this.title = title; // Then perform own initializaton steps.
this.salary = salary;
}
public String getTitle() {
return title;
}
public float getSalary() {
return salary;
}
}
import java.util.List; // Teacher has a List member.
public class Teacher extends Employee { // Inherit Employee: add "extends Employee" to the end.
private List<String> classes;
public Teacher(String name, String personId, String title, float salary,
List<String> classes) {
super(name, personId, title, salary); // First initialize the superclass Employee.
this.classes = classes;
}
public List<String> getClasses() {
return classes;
}
}
A subclass can define a similar (same name and compatible parameter and return types) member function as its superclass. This will result in overriding (ie. replacing) the inherited function.
Sub- and superclasses are “compatible” in a bottom-up direction in the inheritance hierarchy. A subclass object can be used as if it was an object of its superclass. It is e.g. legal to refer to a subclass object with a superclass type variable. This reflects how a subclass in principle inherits the properties of its superclasses. Inheritance is often called an “is a” relationship: subclass objects are (also) objects of their superclasses. As far as an object oriented programming language is concerned, a subclass object can be used in all contexts where a superclass object can. In practice this does require that a subclass has not defined some overriding features that lead to unexpected results with respect to a superclass. This should never happen: it is very bad style to break the behaviour expected from a superclass.
Java has a special superclass Object
that is inherited by all reference types (including
arrays). All objects therefore have e.g. the following public member functions defined by
Object
:
toString()
: return aString
representation of the object.The default implementation inherited from
Object
only prints out vague information about the identity of the object.When you print an object
x
e.g. asSystem.out.println(x)
, the objectx
is automatically converted into a string by callingx.toString
.
equals(Object b)
: returnstrue
orfalse
depending on whether this object is similar tob
.The default implementation inherited from
Object
only compares object identity and not “real” similarity.
hashCode()
: returns a hash code for this object (as anint
).The default implementation inherited from
Object
typically bases the hash code on the identity (e.g. memory address) and not “the value” of the object.
Since the default implementations inherited from Object
usually rely only on the identity and
not the value represented by an object, most other classes should define their own overriding
versions of these functions.
Below is an example class ObjectList
that inherits ArrayList<Object>
and whose only
self-defined function is toString
. Many classes override especially this function due to how it
simplifies printing objects. The example also notes that Java’s list containers have a common
supertype List
. E.g. ArrayList<Object>
, ObjectList
and LinkedList<Object>
objects
can therefore be handled ina uniform manner using List<Object>
references. It should be noted
that a member reference must be legal with respect to the type of the corresponding reference
variable. Therefore e.g. the example code would not be able to call the ArrayList
member
function trimToSize
via a List<Object>
reference: List
does not have such a member
function.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ObjectList extends ArrayList<Object> {
// An overriding implementation of toString. This also shows an example of Java's
// @Override annotation. Java allows (and it is recommended although not mandatory)
// to mark an overriding function by placing the annotation "@Override" before it. This
// directs the Java compiler to verify that the function in fact overrides a similar
// superclass function (this helps to avoid typos and such).
@Override
public String toString() {
StringBuilder sb = new StringBuilder(this.size() + " values:\n");
for(int i = 0; i < this.size(); i++) {
sb.append(" ").append(Integer.toString(i)).append(": ")
.append(this.get(i).toString());
}
return sb.toString();
}
public static void main(String[] args) {
// Create an ObjectList and add values "one", 2, 3.0 ja "four" into it.
// This in effect uses the add function ineherited from ArrayList.
ObjectList myList = new ObjectList();
myList.add("one");
myList.add(2);
myList.add(3.0);
myList.add("four");
// Create and print also an ArrayList<Object> and a LinkedList<Object> objecdt that
// are initialized with contents of myList. This is doable because Java containers
// can often be initialized by another container. Also ObjectList is a Java container
// because it is a subclass of the Java container class ArrayList.
ArrayList<Object> arrList = new ArrayList<>(myList);
LinkedList<Object> linkedList = new LinkedList<>(myList);
System.out.format("myList:%n%s%n%n", myList);
System.out.format("arrList:%n%s%n%n", arrList);
System.out.format("linkedList:%n%s%n%n", linkedList);
// An ObjectList object can be handled with a superclass reference type ArraList<ObjectA>.
ArrayList<Object> myList2 = myList;
// All Java list containers can also be handled using a supertype reference type List. Below
// each of the preceding lists are printed with the function printList that takes an object of
// type List<Object> as a parameter. Also ObjectList is acceptable as such due to how
// ObjectList inherits being a List via the superclass ArrayList.
printList("myList", myList);
printList("arrList", arrList);
printList("linkedList", linkedList);
}
// Prints out the items of a list of type List<Object>.
// The output contains the list name given by the parameter "title".
private static void printList(String title, List<Object> list) {
System.out.format("printList(%s):%n", title);
for(int i = 0; i < list.size(); i++) {
System.out.format(" %s[%d]: %s", title, i, list.get(i));
}
System.out.format("%n%n");
}
}
Executing the example outputs:
myList:
4 values:
0: one 1: 2 2: 3.0 3: four
arrList:
[one, 2, 3.0, four]
linkedList:
[one, 2, 3.0, four]
printList(myList):
myList[0]: one myList[1]: 2 myList[2]: 3.0 myList[3]: four
printList(arrList):
arrList[0]: one arrList[1]: 2 arrList[2]: 3.0 arrList[3]: four
printList(linkedList):
linkedList[0]: one linkedList[1]: 2 linkedList[2]: 3.0 linkedList[3]: four
Note how the function printList
could call list.size()
and list.get(i)
. The type
List<Object>
has such public functions and therefore also its subclasses, such as
ArrayList<Object>
and ObjectList
, must have similar puvlic functions. Our ObjectList
did not implement them itself, but it inherited them from the superclass ArrayList<Object>
.
A seconf, and very important, note about the function calls list.size()
and list.get(i)
is
that they are directed during runtime to the member functions of the list
object’s actual
class: e.g. although list
is a List<Object>
reference, the call list.get(i)
was
directed to the function of LinkedList<Object>
insted of the type List<Object>
when
list
referred to the object linkedList
. This behaviour illustrates the fact that all Java
member functions are inherently “virtual”, that is, a member function reference via an object is
resolved during runtime so that it corresponds to the actual type of the object. The “non-virtual”
alternative, which is e.g. the default behaviour in C++, would be to resolve member function calls
already during code compilation based on the reference type (in which case e.g the calls
list.size()
and list.get(i)
would have been directed to member functions size
and
get
of the type List<Object>
).
It is often said that one of the main motivations for using inheritance is code reuse: one does not
need to implement everything from scratch if useful existing functionality can be inherited from
superclasses. But the most important reason to use inheritance could be said to be the possibility
to handle various object types in a uniform manner, via a common supertype. E.g. the preceding
function printList
can handle all types of objects whose supertype is List<Object>
.
Inheritance made it possible to do this with one function instead of having to e.g. implement three
separate functions printList(ArrayList<ObjectList>)
, printList(ArrayList<Object>)
and
printList(LinkedList<Object>)
.
If you read the text carefully, you might have noticed that we never referred to List
as a
class: it was always referred to in a more general manner as a “type” List
. This is because
List
is not a class: it is an interface. We will discuss interfaces later.
Programming demo (duration 1:11:49)