Updated September 1, 2024
Table of contents
-
Inheritance
The
Object
ClassAbstract Methods and Classes
Interfaces
Inheritance vs. Composition
Exercises
Inheritance
Recall from our discussion on creating a GUI that we chose to use the phrase extends JFrame
(for Swing) or extends Application
(for JavaFX). This meant the class containing our program could obtain the ability to display a window and place text fields and buttons within the viewable area. The discussion then briefly described the benefits of extending the class in that our program was now part of that class, an extension of that class.
Of course, we did not add anything new to the existing JFrame
or Application
class, or what we call the superclass (also known as the base class). Our program became the subclass (or derived class) by way of inheritance.
The subclass inherits all of the properties of the superclass. As a result, we can create instance variables of both the superclass and the subclass types in the same object, which also reduces the programming complexity since any change to the superclass design is automatically inherited by the subclass the next time we compile our program.
The details of inheritance are relatively simple. The concept of single inheritance is a subclass that inherits a single superclass’s properties. The concept of multiple inheritance is a subclass that inherits the properties of more than one superclass. The single and multiple inheritance principles are created to describe how a subclass can come into being, but Java only supports single inheritance. In other words, we can only extend one class at a time. With inheritance, there are some strict rules which are:
- The
private
members of the superclass cannot be accessed by the subclass. - The
public
member of the superclass are directly accessible to the subclass. - The subclass can contain additional data or method members.
- All data members of the superclass are also data members of the subclass (because of inheritance).
- The subclass can overload the public methods of the superclass.
- The subclass can redefine or override public methods of the superclass.
We will use the code in Example 1, Example 2 to demonstrate our itemized list of limitations and capabilities.
public class Car {
private String color;
private int doors;
private boolean hatchback;
protected String make;
public Car (String make, int doors, boolean hatchback) {
this.make = make;
this.doors = doors;
this.hatchback = hatchback;
}
public int getDoors() {
return doors;
}
public String getColor() {
return color;
}
public void setColor(String c) {
color = c;
}
public boolean getHatchback() {
return hatchback;
}
public String getMake() {
return make;
}
@Override
public String toString() {
String hb=" with hatchback.";
if (!hatchback)
hb = ".";
return make + " " + doors + " door" + hb;
}
}
Example 1: Source code for the class Car.
The code for Car
in Example 1 has three private
instance variables color
and doors
and hatchback
. There is one constructor, one mutator method and four accessor methods. Included is a toString()
method for details of the instance variable values. This class is very basic and conforms to all of the user-defined class components we have seen previously.
Remember that toString()
is overriding the inherited one from Object
. We saw this with user-defined classes in Chapter 8.
We are also introducing the use of protected
here. If it were marked private
, the subclass would not have direct access to it. By marking it protected
, it remains private to the world but accessible to subclasses.
public class Honda extends Car {
public enum pkg_type { LX, EX, SE, EX_L, SPORT, TOURING };
public enum eng_type { InLine4Cylinder, V6};
public enum model_type { Accord, Civic, Fit, CR_V, Odyssey };
private pkg_type pkg;
private eng_type engine;
private model_type model;
private int year;
public Honda (String color, int doors, boolean hatchback, model_type model) {
// Car wants make, doors and hatchback - in that order!
super("Honda", doors, hatchback);
this.model = model;
//this.color = color; // NO NO NO!
setColor(color);
}
public model_type getModel() {
return model;
}
public void setEngine(eng_type engine) {
this.engine = engine;
}
public eng_type getEngine() {
return engine;
}
public void setPkg(pkg_type p){
pkg = p;
}
@Override
public String toString() {
String hb=" with hatchback.";
if (!getHatchback())
hb = ".";
return make + " " + model + " " + pkg + " " + getEngine() + " " + getDoors() + " door" + hb;
}
}
Example 2: Source code for Honda
which extends Car
.
In Example 2, Honda
is defined as a class that extends Car
. By doing so we are indicating that Honda
is to inherit all of the properties of Car
. In addition to this inheritance, we are defining four additional private
instance variables pkg
, engine
, model
and year
.
Honda
has three enum types, one constructor, two accessors and two mutator methods. The method toString()
has, again, been overridden for the subclass.
Our subclass constructor invokes the superclass constructor by calling super()
with an appropriate number of arguments. Whenever we call the superclass constructors they must be the first line of the subclass constructor body and must use the super
reserved word.
We can also use the super
reserved word to access methods within the superclass. This is especially necessary when our subclass has methods that override ones of the superclass. These forms are shown in the setString()
and toString()
methods.
It is important to note that we are calling the mutator methods of the superclass from within our subclass and not attempting to modify the instance variables of the superclass. This direct manipulation would not be possible since the superclass instance variables are private
and therefore cannot be accessed from the subclass.
If, however, we did want a subclass to have direct access to an instance variable of the superclass while still disallowing direct access outside the class, we could use a different modifier called protected
. A protected
instance variable of the superclass can be directly accessed from a subclass and still require all other access outside the class to be done using the accessor and mutator methods. So if we wanted the color
instance variable of Car
to be accessed from Honda
, we could simply modify its declaration as follows:
protected String color;
No matter how many generations of class are derived from a superclass, all of the public
and protected
members of the superclass will continue to be accessible to all the subsequent generations. This is also true for any intermediate generation forward in the inheritance hierarchy.
We must be sure that all subclasses make appropriate calls to the super()
constructors and that the superclass initializes those instance variables correctly.
public class CarExerciser {
public static void main (String[] args) {
Car somecar = new Car("Acura", 4, true), othercar;
Honda hondacar = new Honda("Silver", 4, true, Honda.model_type.Fit);
hondacar.setPkg(Honda.pkg_type.SPORT);
hondacar.setEngine(Honda.eng_type.InLine4Cylinder);
System.out.println(somecar);
System.out.println(hondacar);
// same types
othercar = somecar;
System.out.println(othercar);
// polymorphism (Car references Honda)
othercar = hondacar;
// Late-binding
System.out.println(othercar);
}
}
Example 3: CarExerciser
program to test Car
and Honda
objects.
Now, let’s look at our exerciser program in Example 3. We have declared reference variable somecar
of class type Car
and reference variable hondacar
of class type Honda
.
First, let us look at the very commonplace appearance of somecar
and hondacar
. They are populated with objects from their respective constructors and then made part of a call to println()
. This implies a call to the toString()
methods for each object. Recall that Honda
redefined (override) toString()
to include the additional instance variables.
We then call some subclass mutator methods to alter the values of the instance variables for hondacar
. Now for something exciting. othercar
is assigned the value of somecar
, then we print othercar
which calls the toString()
method of Car
which is exactly as we would expect to happen.
However, we then assign hondacar
to othercar
. This is not an error. On the contrary, any superclass reference variable may reference an object of any of its subclasses. What is really interesting is when we print othercar
the second time, the toString()
method of Honda
is the method that is invoked!
This requires a bit of explaining. First let us look at the output of the CarExerciser.java
program shown here:
Acura 4 door with hatchback. Honda Fit SPORT InLine4Cylinder 4 door with hatchback. Acura 4 door with hatchback. Honda Fit SPORT InLine4Cylinder 4 door with hatchback.
The Java runtime environment knows that othercar
is pointing to an object of Honda
and not to an object of Car
. As a result we call the toString()
method of the referred object, not the method of the reference variable class type. This particular phenomenon is called late binding (also dynamic binding, runtime binding and virtual method invocation).
Essentially, the method to call is determined at runtime, not compile time. Every subclass is allowed to have its own toString()
method, and the meaning of the call to toString()
is based on the object referenced and not the reference variable. The ability for us to assign multiple meanings to a method is called polymorphism. Polymorphism is implemented by using late binding.
The reference variable othercar
can point to any object of class type Car
or the class Honda
. Because othercar
can have many forms, we say that othercar
is a polymorphic reference variable.
Look closely at the conditional tests that use another reserved word called instanceof
. This reserved word is an operator, and we can use this operator at runtime to determine what a reference variable refers to. This way, we can be confident that actions are on the appropriate object.
If polymorphism is something you wish to stop, you can prohibit the overriding of a method by declaring the method of the superclass as final
. You can also stop the ability to create a subclass of a given class by declaring the class as final
. There are some exceptions to late binding as well; Java does not use late binding on static
, final
, or private
methods.
The Object
Class
Recall that every class has a default toString()
method even if you do not provide one. Also recall that the default value of a class printed with the toString()
method provided is the class name and a hash value (not an address).
So where did this default toString()
method come from? It comes from the standard class Object
. This class is defined in the package java.lang
, which is the package that every Java program imports even when you have no import statements. In addition, when you define a class that has no extends keyword, your class automatically becomes a subclass of the class Object
. One way or another, your class will be a subclass of the ultimate superclass, Object
. Table 1 shows some members of the class Object
.
Useful members of the class Object |
public Object() Object constructor. |
protected Object clone() Returns a object that is a copy of this object. |
public boolean equals(Object obj) Returns true if obj and this object refer to the same memory space. |
protected void finalize() This method is called when an object goes out of scope. |
public int hashCode() Returns the integer representation of the object. Especially useful for hash tables supported by java.util.Hashtable . |
public String toString() Returns a string representation of the object. |
Table 1: Members of the Object
class.
Since every class is a subclass of Object
, we already know that all of the methods of Object
are available to our classes when we define them. We now have a greater understanding of object hierarchies and can better understand some of the class lineages we see in the Java documentation.
Abstract Methods and Classes
A method that has only a heading and no body is known as an abstract method. The heading ends in semicolon and includes the reserved word abstract
. An abstract class is also declared with the reserved word abstract
. Below are some rules about abstract classes:
- An abstract class may contain abstract methods.
- An abstract class may contain nonabstract methods, constructors, finalizers and instance variables.
- Any class containing abstract methods must be declared abstract.
- Abstract class objects cannot be instantiated, although you can have an abstract class type reference variable.
- You may instantiate an object of a subclass of an abstract class, provided the subclass defines all of the abstract methods of the superclass.
The primary purpose of abstract classes is to force the definition of subclasses as direct descendants of the superclass and to force these subclasses to support the abstract methods the superclass declares.
Why would you want to do this? Consider the Car
class from earlier. Car
could stand on its own as a class. Honda
was another class that was derived from Car
, but there is nothing tightly coupling the two as a cohesive unit other than ancestry.
What if we devised a more purposeful Car
class that allowed for subclasses to be defined but forced them all to have some things in common. This is where an abstract class can come in handy. The negative side of declaring new subclasses of Car
is that there is nothing to stop each developer of a subclass from naming its accessor and mutator methods differently.
Imposing the use of abstract methods makes all subclasses consistent. Consider the following:
Interfaces
Recall that the ActionListener
(Swing) and the EventHandler
(JavaFX) class is a special form of class called an interface
. Interfaces are completely abstract classes. We will discover later that there are other interesting interfaces defined within Java. Reember that Java only supports single inheritance. Therefore we can only create classes that are derivatives of one superclass.
This is precisely the reason we have interfaces. Java will allow us to define a class using more than one interface
. Our GUI programs implemented an event handling mechanism using what is known as inner classes, anonymous classes, and even Lamba expressions.
Interfaces are attached to your program by using the implements
reserved word. See Example 5b of Chapter 7a and Example 10a of Chapter 7b for examples of main classes implement the interface instead of using inner or anonymous classes.
Essentially an interface
is a class that declares only abstract methods. They may also contain named constants.
Remember that any class which implements
an interface
must provide definitions for all of the methods of the interface, or you will not be able to instantiate an object of that class.
Another useful aspect of interfaces is the ability to declare a reference variable of an interface type and then reference any object type that implements that interface. Consider the code in Example 4.
import java.util.List;
import java.util.Vector;
import java.util.ArrayList;
import java.util.Arrays;
public class Lists {
public static void main(String[] args) {
List<String> l;
l = new Vector<String>( Arrays.asList("red", "bed", "led"));
System.out.println(l);
l = new ArrayList<String>( Arrays.asList("abc", "def", "ghi"));
System.out.println(l);
}
}
Example 4: Using an interface to reference different objects.
The reason the reference variable l
can point to so many different object types is because all of those classes implement List<E>
. The benefit of this is we can declare reference variables and method parameters of type List<E>
and not worry about which type is actually assigned because we will able to perform List
specific actions.
Inheritance Versus Composition
We are trying to tell the difference between an is-a relationship and a has-a relationship. With inheritance, we are saying that the subclass is-a superclass, or using our examples from earlier, Honda
is-a Car
since it is a derivative class.
What comes about later is we define classes that have other classes as members within the class. For example, in Car
, there is a String
instance variable called make
. So we can say that Car
has-a String
.
Just as every person has a name. The simplest way to represent this is a class called Person
with an instance variable called name
. For example:
public class Person
{
String name;
// rest of class definition
}
This way, we show composition or the relationship between the two classes. String
is a class defined inside of Person
.