11. Inheritance
Your children are not your children.
They are the sons and daughters of Life’s longing for itself.
They come through you but not from you,
And though they are with you yet they belong not to you.
11.1. Problem: Boolean circuits
In Chapter 4, we talked extensively about Boolean
algebra and how it can be applied to if
statements in order to control
the flow of execution in your program. The commands that we give in
software must be executed by hardware in order to have an effect. It
shouldn’t be surprising that computer hardware is built out of digital
circuits that behave according to the same rules as Boolean logic. Each
component of these circuits is called a logic gate. There are logic
gates corresponding to all the Boolean operations you’re used to: AND,
OR, XOR, NOT, and others.
The output of an AND gate is the result of performing a logical AND on its inputs. That is, its output is true if and only if both of its inputs are true. The same correlation exists between each gate and the Boolean operator with the same name. At the level of circuitry, a 1 (or “on”) is often used to represent true, and a 0 (or “off”) is often used to represent false. Modern computer circuitry is built almost entirely out of such gates, performing addition, subtraction, and all other basic operations as complicated combinations of logic gates, where each digit of every number is a 1 or 0 determined by the circuit.
Because these circuits can become large and unwieldy, your problem is to write a Java program that will allow a user to specify the design of such a circuit and then see what its output is. The input to this program will be a text file redirected to standard input that gives the number of gates of the circuit, lists what each gate is, and then lists the connections between them.
The following is an example of input to make a circuit with six components.
6 true false AND XOR NOT OUTPUT 1 2 0 1 3 2 1 4 3 5 4
The first line specifies the total number of components. The next two
components give either true or false inputs, depending on their names.
The AND
and the XOR
correspond to gates of the same name which each
take two inputs. The NOT
corresponds to a NOT gate with a single
input. Finally, OUTPUT 1
is the single output of this circuit that
we’re interested in. After the list of gates is a list of how they’re
connected. The line 2 0 1
specifies that the gate at index 2, which
happens to be an AND gate, has an input from the gate at index 0 and an
input from the gate at index 1. In other words, the AND gate has one
true input and one false input. The final circuit produced would look
like the following.
We’re only interested in the value of any OUTPUT gates. Thus, the program that’s simulating this circuit would print the following.
OUTPUT 1: true
There are many different ways to implement a solution to this problem.
The total number of gates is given as the first input. Thus, you can
make an array of gates. When you go to connect them, the indexes given
in the input will map naturally onto the gates in the array. But what
type should the array be? You could create a Gate
class that could do
the work of any conceivable gate, but the implementation would be
awkward. All gates would have to have two inputs even if they don’t need
any. Adding different kinds of gates later (like a NAND, for example)
would mean rewriting the Gate
class.
Instead, a cleaner approach to the solution is to use inheritance. In object-oriented languages like Java, inheritance is a process that allows you to create a new, specialized class from a more general, preexisting class.
We recommend the following inheritance hierarchy, in which the arrows point from each child class to its parent class.
As you can see, every class inherits from the Gate
class. If your
array can be of type Gate
, it will be able to hold objects of any
child of Gate
. UnaryOperator
, BinaryOperator
, True
, and False
are children of the basic Gate
class. Then, Output
and Not
are
children of UnaryOperator
since each only has a single input.
Naturally, And
, Or
, and Xor
are children of BinaryOperator
,
since they all have two inputs.
If this jumble of classes seems bewildering, don’t be discouraged. Each is very short and easy to write. We’ll explain what the inheritance relationship means and how to use it in the next few sections.
11.2. Concepts: Refining classes
Here we give a brief overview of inheritance that will give us enough information to continue onward. We’ll cover some of the deeper areas of the subject in Chapter 17.
11.2.1. Basic inheritance
The process of creating an inherited class out of an existing class is called inheriting a class, deriving a class, or simply subclassing. The class that already exists is called a parent class, base class, or superclass and the new class is called a child class, derived class, or subclass.
When you create a child class from a parent class, the child class inherits all of its fields and methods. Thus, you can use a child class object anywhere you’d use the parent class object. This relationship explains the terms superclass and subclass: Since you can treat any subclass object as if it were a superclass object, the superclass type can be thought of as a superset including all subclass objects.
The names superclass and subclass can sound misleading because the subclass can usually do more than the superclass. From that perspective, the child class has a superset of the fields and methods of its parent class. To avoid confusion, we favor the terminology of parent and child classes.
11.2.2. Adding functionality
When creating a child class, a programmer will normally add
functionality above and beyond the original parent class. Otherwise,
there’s little point in creating a child class. For example, a simple
Fish
class might be able to do things like swim and feed. A child
Flounder
class has the additional ability to camouflage itself.
Another child class, the Shark
class, adds the ability to eat other
Fish
objects.
By adding a few methods, we can create a new class with special abilities without interfering with the basic functionality of the underlying class.
11.2.3. Code reuse
Of course, a programmer who wished to program a Shark
class could
simply copy and paste in all the code from the Fish
class and then
make the necessary additions. In many ways the evolution of modern
programming languages has been to reduce the need for copying and
pasting.
Old mistakes are propagated with copying and pasting. When discovered, they must be fixed in several different locations. New mistakes can also be introduced by cutting and pasting. Instead, we wish to guarantee that working code from a parent class continues to work in a child class. Ideally, code from parent classes will not need to be debugged a second time when a child class is created.
Even without the issue of errors introduced by copying and pasting, the total amount of code increases. By minimizing the amount of code, issues of performance and storage can be improved, but not always. Object-oriented languages have taken criticism for low speed and high memory use due to the additional complexities of objects and inheritance, but compiler optimization, good library design, and improved JVM performance have brought Java a long way in this area.
11.3. Syntax: Inheritance in Java
In this section we discuss the mechanism for creating a child class in
Java using the extends
keyword. Then, we discuss access restriction
and visibility, constructor issues, the Object
class, and overriding
methods.
11.3.1. The extends
keyword
In order to make a child class in Java, we use the extends
keyword.
Let’s give an example using the Fish
class defined below. This class
creates a basic fish that can swim, feed, and die. We can check its
color, location, and whether or not it’s alive. When it runs out of
energy, it dies.
import java.awt.*;
public class Fish {
protected Color color = Color.GRAY;
private double location = 0.0;
private double energy = 100.0;
private boolean alive = true;
public Color getColor() { return color; }
public double getLocation() { return location; }
public boolean isAlive() { return alive; }
public void swim() {
if(alive) {
location += 0.5;
energy -= 0.25;
}
if(energy <= 0.0)
die();
}
public void feed() { energy = 100.0; }
public void die() { alive = false; }
}
From here we can create a child class called BoringFish
that does
exactly what Fish
does. To do so, we use the extends
keyword after
the new class name, followed by the parent class name (in this case
Fish
), followed by the body of the class.
public class BoringFish extends Fish {
}
Just as we’re allowed to make an empty class, we’re allowed to make an
inherited class and add nothing, but doing so is pointless. Instead, we
can make a Flounder
class that can change its color.
import java.awt.*;
public class Flounder extends Fish {
public void setColor(Color newColor) { color = newColor; }
}
The Flounder
class can do everything a Fish
can: It can swim, feed,
and die. But we also add the ability to change color since
flounders are famous for their ability to mimic the ocean floor they
swim over. Note that the color
field in the Fish
class has the
protected
access modifier, not private
. We’ll come back to this
point.
Here’s a Shark
class that extends Fish
in another way, by adding
the capability of eating other Fish
.
public class Shark extends Fish {
public void eat(Fish fish) {
fish.die();
feed();
}
}
Here we have added an eat()
method that takes another Fish
object as
a parameter. First, the Fish
parameter is killed; then the eat()
method calls feed()
, restoring the energy of the Shark
object. Note
that the Shark
object is able to call the feed()
method even though
it isn’t defined inside of Shark
. Because it inherits from Fish
, it
has a version of feed()
.
Single inheritance only
Particularly if you’ve programmed in C++, you might be wondering if it’s
possible to have one class inherit from multiple classes in Java.
In multiple inheritance, a single class can have many different parents.
Since C++ supports multiple inheritance, it would allow you to have a
SharkAlligatorMan
class that inherits from the
Shark
, Alligator
, and Human
classes. If you go back to the sorting
problem from Chapter 10, multiple inheritance would
allow us to solve the problem with an Age
class and a Weight
class
from which Dog
, Cat
, Person
, and Cheese
all inherit.
However, the designers of Java decided not to allow multiple
inheritance, perhaps for this reason: Imagine a River
class with a
run()
method and a Politician
class with a run()
method. It seems
strange to create a class which is both a river and politician, but
there is no rule in C++ which makes doing so impossible. If you did have
a RiverPolitician
class which inherits from both, what would happen
when you call the run()
method? How would the RiverPolitician
class
know which of its parents' methods to pick? Surely, the way that a
politician runs for office is very different from the way a river runs
along its banks.
This problem is similar to the issue discussed in Section 10.3.1, where a class could implement more than one interface with the same default method; however, the problem is more severe in the case of multiple inheritance since it becomes unclear which fields inside of parent classes are being referred to, not just which methods.
If you find yourself in a situation where you want to use multiple inheritance in Java, try to reformulate your class hierarchy into one where your classes implement multiple interfaces. Recall that multiples interfaces can be implemented by a single class in Java, and like multiple inheritance, this practice allows a single class to be used in wildly different contexts.
Interfaces using extends
The extends
keyword is not limited to classes. It’s possible for an
interface to extend another interface. In fact, an interface can extend
any number of other interfaces. As when a class implements multiple
interfaces, each interface in an extends list is separated by commas.
When an interface extends other interfaces, it includes all the methods
(and constants) they define. If a class implements an interface that
extends other interfaces, it must contain versions of all the methods
specified by all the interfaces. Recall the Ageable
and Weighable
interfaces from Chapter 10, which specified the
getAge()
and getWeight()
methods, respectively. We could create an
interface that required both of these methods by extending Ageable
and
Weighable
.
public interface AgeableAndWeighable extends Ageable, Weighable {
}
We could add additional methods to the AgeableAndWeighable
interface,
but even empty it will enforce the contracts defined by both Ageable
and Weighable
. It’s usually not necessary to create an interface
that extends other interfaces, since a class could implement each of the
individual interfaces. Nevertheless, it can be used as a convenience to
save typing or to create a reference type with certain guaranteed
abilities.
Note that a class can never extend an interface. Likewise, an interface cannot extend a class or implement another interface.
11.3.2. Access restriction and visibility
The Shark
example above gives an example of inheritance in which the
child class only calls methods of the parent class and does not
interfere with the fields of the parent class. Generally, leaving parent
fields alone is a good thing because it protects the state of the parent
class from getting corrupted. However, it’s not always possible.
If we return to the earlier Flounder
example, we had to change the
color
field directly since there was no mutator to change it.
Perhaps the Fish
class was poorly designed because it didn’t have a
color
mutator. On the other hand, most fish cannot change their color,
so it might be good design to prevent outside code from changing the
color
field with such a mutator. There are no absolute rules for
making these kinds of decisions.
We introduced access modifiers in Section 9.3.4, but inheritance
gives them new meaning. Recall that the
access modifier for the color
field of Fish
was protected
. A field
or method with the protected
modifier can be accessed by all child
classes (as well as classes in the same package). If the modifier for
color
was private
, the Flounder
class would not be able to change
it directly.
In the Shark
class, it must use mutators to change the value of its
own energy
and the alive
field of the fish
object it eats since
they’re both marked private
. It’s generally preferable to use mutator
and accessor methods whenever possible, even within the same class, so
that fields are not inadvertently corrupted.
11.3.3. Constructors
When you create a child class, you can imagine that a copy of the parent class exists inside of the child. When you create an object from a child class, how do you properly initialize the fields inside the parent class?
As we discussed in Chapter 9, every class has a
constructor, even if it’s a default one created for you. Whenever the
constructor for a child class is invoked, the constructor for the parent
class is invoked as well. If the parent class is also the child of some
other class, that grandparent class will have its constructor invoked as
well. This chain of constructors will continue, reaching all the way
back to the ultimate ancestor, Object
.
When writing the constructor for a child class, the first line of it should be the call to the parent constructor. If you don’t explicitly call the parent constructor, its default (no parameter) constructor will be called. If the parent class does not have a default constructor, then leaving off an appropriate call to a parent constructor will result in a compiler error. Consider the following two classes.
public class Parent {
private String name;
public Parent(String name) { this.name = name; }
public String getName() { return name; }
}
public class Child extends Parent {
public Child(String name) {
super("Baby " + name);
}
}
As shown above, the super
keyword is used to call the constructor of a
parent class. The Child
constructor takes a name and prepends the
String
"Baby "
to it before passing it on to the Parent
constructor.
In a similar way, the this
keyword can be used to call another
constructor in the same class, provided that a constructor to the
parent class is eventually reached. For example, we could add the
following constructor to the Child
class.
public Child() {
this("Unknown");
}
This second constructor will be called whenever a new Child
object is
instantiated without any arguments. It will supply the String
"Unknown"
to the other constructor, which will add "Baby"
and pass
it on to the Parent
class.
11.3.4. Overriding methods and hiding fields
Sometimes a parent method doesn’t provide all the power you want in the child class. It’s possible to override a parent method in the child class. Then, when that method is called on child objects, the new method will be called. The new method has exactly the same name and parameters. The return type must either be exactly the same or a child class of the original return type.
We can return to the Fish
class example and make a new kind of fish
that never moves.
public class LazyFish extends Fish {
public void swim() {
System.out.println("I think I'll just sit here.");
}
}
Whenever someone calls the swim()
method on a LazyFish
object, it
will announce that it’s going to sit where it is. Its location isn’t
updated, and its energy doesn’t change.
On the other hand, we could create another child class that swims twice
as fast as the original Fish
.
public class FastFish extends Fish {
public void swim() {
super.swim();
super.swim();
}
}
Every time swim()
is called on objects of type FastFish
, those
objects will call the swim()
method from Fish
twice. Thus, this fish
will move twice as fast (and consume twice as much energy). Because the
location
and energy
fields are private
, we must use methods from
Fish
to affect them. Note the use of the keyword super
, allowing us
to specify that we want to call the swim()
method from Fish
and not
just call the same method from FastFish
again. Using the super
keyword, we can call methods from the parent. If the parent didn’t
override a method from an ancestor class, we can still use super
to
call a method from the most recent ancestor class that did implement the method.
However, Java does not allow us to skip over a parent method to call a
grandparent method if there’s an implementation in the parent class. In
other words, there’s no way to call something like a
super.super.swim()
method.
Just as methods are overridden, fields are hidden. It’s perfectly legal to declare a field with the same name as a field from a parent class, but the new field will then be used instead of the old one.
public class A {
protected int a;
public int getA() { return a; }
public void setA(int value) { a = value; }
}
public class B extends A {
protected int a;
public void setA(int value) { a = value; }
}
Class B
is a child of class A
and declares a field called a
,
hiding a field of the same name from A
. However, which a
is which
can cause some confusion. Consider the following fragment of code.
A objectA = new A();
B objectB = new B();
objectA.setA(5);
objectB.setA(10);
System.out.println("A = " + objectA.getA());
System.out.println("B = " + objectB.getA());
The output of this code is:
A = 5 B = 0
Calling the setA()
method on an A
object sets the a
field inside
of A
. Calling the overridden setA()
method on a B
object sets the
a
field inside of B
, but since the getA()
method hasn’t been
overridden, the a
field from the A
parent class part of B
is
returned. Since that a
field in B
hasn’t been given a value, it
still has the default value of 0
. Both a
fields exist inside of B
,
but the methods are poorly designed, leaving one field capable only of
being set and the other capable only of being retrieved.
11.3.5. The Object
class
You may not have realized it, but every class you’ve created in Java uses
inheritance. To provide uniformity, the designers of Java made every
class the child (or grandchild or great-grandchild…) of a class
called Object
. When you omit the extends
clause in a class
definition, you’re making that class a direct child of Object
.
As a consequence, all classes in Java are guaranteed to have the following methods.
Method | Purpose |
---|---|
|
Make a separate copy of an object. |
|
Determine if two objects are the same. |
|
Perform cleanup when an object is garbage collected. Similar to a destructor in C++. Rarely used. |
|
Find out what the class type of a given object is. |
|
Get the hash code for an object, useful for making hash tables of objects. |
|
Used for synchronization with threaded programs. More in Chapter 14. |
|
Same as previous. |
|
Get a |
|
Used with |
Java provides basic implementations for most of these, but if you want
them to work well for your object, you’ll have to override some of
them with appropriate methods. For example, the Object
version of
toString()
returns the virtual address of the object in JVM memory,
which is not very useful information.
Nevertheless, API classes usually have good equals()
and toString()
methods. Aside from making a few useful methods available, having a
common ancestor for all classes means that you can store any object in
an Object
reference. An array of type Object
can hold anything,
provided that you know how to retrieve it. We discuss the finer points
of inheritance and polymorphism in Chapter 17 and
how to build lists and other data structures using Object
references
in Chapter 18.
11.4. Examples: Problem solving with inheritance
Here are two extended examples showing how we can use inheritance to solve problems. First, we revisit the student roster example from Chapter 9 and then move onto an inheritance hierarchy of polygons.
The Student
class we created in Example 9.1 is useful
but works only for undergraduate students. With only a few additions, we
can make it suitable for graduate students as well. First, let’s take
another look at the Student
class.
public class Student {
public static final String[] YEARS = {"Freshman", "Sophomore", "Junior", "Senior"};
private String name;
private int year;
private double GPA;
public Student(String name, int year, double GPA) {
setName(name);
setYear(year);
setGPA(GPA);
}
public void setName(String name) { this.name = name; }
public void setYear(int year) { this.year = year; }
public void setGPA(double GPA) {
if(GPA >= 0 && GPA <= 4.0)
this.GPA = GPA;
else
System.out.println("Invalid GPA: " + GPA);
}
public String getName() { return name; };
public int getYear() { return year; };
public double getGPA() { return GPA; };
public String toString() {
return name + "\t" + YEARS[year] + "\t" + GPA;
}
}
We want to create a GraduateStudent
class that inherits from
Student
. We need to add a thesis topic for each graduate student.
Likewise, we need to update the toString()
method so that outputs the
appropriate data. We use 4
as the year value for graduate students.
Student
to add graduate student capabilities.public class GraduateStudent extends Student {
private String topic; (1)
public GraduateStudent(String name, double GPA, String topic) {
super(name, 4, GPA); (2)
setTopic(topic);
}
public void setTopic(String topic) { this.topic = topic; }
public String toString() { (3)
return getName() + "\tGraduate\t" + getGPA() + "\tTopic: " + topic;
}
}
1 | Because we’re inheriting most of the fields we need, we only need to
declare the topic field. |
2 | Then, in the GraduateStudent constructor,
we call the parent constructor with the name, year, and GPA and then set
topic to the input value. |
3 | Finally, we override the toString() method so that "Graduate" and
the thesis topic are output. Note that we must use the getName() and
getGPA() accessors since those fields are private in Student . |
Most code that uses Student
objects should be able to incorporate
GraduateStudent
objects easily. Code that creates Student
objects
from input will need slight modifications to handle the thesis topic.
Also, old code that only expects values of 0
, 1
, 2
, or 3
for
year may need to be modified so that it doesn’t break.
Let’s examine a class hierarchy used to create several different
polygons. Our base class needs to be general. It can represent any kind
of closed polygon, using an array of Point
objects. The Point
library class
is a way to package up x
and y
values of type int
. Each coordinate
in the array gives the next vertex of the polygon.
import java.awt.*; (1)
public class Polygon {
protected Point[] points; (2)
public Polygon(Point[] points) { (3)
this.points = points;
}
1 | The import statement allows us to use the Point class as well as the
Graphics class. |
2 | Our array of type Point is declared protected so
that the child classes we want to create can access it directly. |
3 | The constructor takes an array of type Point and stores it. |
public double getPerimeter() { (1)
double perimeter = 0.0;
for(int i = 0; i < points.length - 1; i++)
perimeter += points[i].distance(points[i + 1]);
perimeter += points[0].distance(points[points.length - 1]);
return perimeter;
}
public void draw(Graphics g) { (2)
for(int i = 0; i < points.length - 1; i++)
g.drawLine(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
g.drawLine(points[0].x, points[0].y,
points[points.length - 1].x, points[points.length - 1].y);
}
}
1 | The getPerimeter() method can determine the length
of the perimeter by adding the lengths of the segments connecting the
vertices. Although it’s also possible to determine the area enclosed by a list of
vertices, the algorithm is complex. |
2 | The draw() method draws the
polygon by drawing each line segment that connects adjacent vertices. We
discuss the Graphics class in Chapter 15. If you compile and run this code, please note that in
Java graphics, like many computer graphics environments, the upper left
hand corner of the screen or window is considered (0,0), and
y values increase going downward, not upward. |
The number of things that can be done with this very general Polygon
class are limited, but with this basic parent class defined, we can
design a Triangle
class as a child of it.
import java.awt.*; (1)
public class Triangle extends Polygon {
public Triangle(int x1, int y1, int x2, int y2, int x3, int y3) { (2)
super(toPointArray(x1, y1, x2, y2, x3, y3));
}
protected static Point[] toPointArray(int x1, int y1, int x2, int y2, int x3, int y3) {
Point[] array = {new Point(x1, y1), new Point(x2, y2), new Point(x3, y3)}; (3)
return array;
}
1 | Again, the import statement is for the Point class. |
2 | One reasonable
constructor for a triangle takes in six values, giving the
x and y coordinates of the three vertices of
the triangle. Of course, the Polygon class requires an array of type
Point , but the super constructor must be the first line of the
Triangle constructor. |
3 | To solve this problem, we create a static
method to package the values into an array. We could have done the same
thing in the argument list of the super constructor, but it would have
looked messier. The toPointArray() is protected because there’s no
reason to let external code have access to it. |
public String getType() {
double a = points[0].distance(points[1]);
double b = points[1].distance(points[2]);
double c = points[2].distance(points[0]);
if(a == b && b == c)
return "Equilateral";
if(a == b || b == c || a == c)
return "Isosceles";
return "Scalene";
}
}
Finally, the getType()
method allows us to do something specific with
triangles. We can use the distance()
method from the Point
class to
find the length of each of the three sides. By comparing these lengths,
we can determine whether the triangle represented is equilateral,
isosceles, or scalene. Of course, computing the perimeter and drawing
the triangle are already taken care of by the Polygon
class.
We can easily make a Rectangle
class along the same lines.
import java.awt.*;
public class Rectangle extends Polygon {
public Rectangle(int x, int y, int length, int width) { (1)
super(toArray(x, y, length, width));
}
protected static Point[] toArray(int x, int y, int length, int width) { (2)
Point[] array = {new Point(x, y), new Point(x + length, y),
new Point(x + length, y + width), new Point(x, y + width)};
return array;
}
public int getArea() { (3)
int length = points[1].x - points[0].x;
int width = points[2].y - points[1].y;
return length * width;
}
}
1 | The constructor is similar to the Triangle constructor except that the
upper left corner of the rectangle is specified, along with the length
and the width. |
2 | From these values, the appropriate array of Point
values is generated. |
3 | The rectangle-specific code that we add is the
getArea() method, which determines the length and width of the
rectangle by examining the points array and then calculates area. |
Using inheritance as form of specialization, we can go one step further
and make a Square
class.
public class Square extends Rectangle {
public Square(int x, int y, int size) {
super(x, y, size, size);
}
}
This very short class uses everything available in Rectangle
but
simplifies the constructor slightly so that the user doesn’t have to
enter both length and width.
11.5. Solution: Boolean circuits
Here we present our solution to the Boolean Circuits problem. First, we
define a parent class for all circuit components, called Gate
.
public class Gate {
private String name;
public Gate(String name) { this.name = name; }
public String getName() { return name; }
public String toString() {
return getName() + ": " + getValue();
}
public boolean getValue() { return false; }
}
The Gate
class doesn’t do anything except set up ways to store a name
and to get a value. It
doesn’t really matter what getValue()
gives back for Gate
, but we
can say that it’s false
.
In principle, it shouldn’t be possible to create an object of type Gate
,
UnaryOperator
, or BinaryOperator
. Classes that are designed only to
be parent classes and never to be instantiated are called abstract classes and
are discussed in Section 17.3.1.
From Gate
, we can define the most basic circuit
components: gates whose value is either always true or always false.
public class True extends Gate {
public True() { super("true"); }
public boolean getValue() { return true; }
}
public class False extends Gate {
public False() { super("false"); }
public boolean getValue() { return false; }
}
To conform with the constructor for Gate
, these new classes must pass
a String
giving their name to the super
constructor. The values
returned by the getValue()
method are clear. Next, we want to create a
class that can be used as a parent for all unary operators.
public class UnaryOperator extends Gate {
private Gate input;
public UnaryOperator(String name) { super(name); }
public void setInput(Gate input) { this.input = input; }
public Gate getInput() { return input; }
}
The important addition in the UnaryOperator
class is the input
field. Any unary operator must have a single input gate that it operates
on. This class provides a mutator and accessor for input
, as well as
an appropriate constructor. From UnaryOperator
, we can derive two
specific operators.
public class Output extends UnaryOperator {
public Output(int i) { super("OUTPUT " + i); }
public boolean getValue() { return getInput().getValue(); }
}
The Output
class takes in an int
value and uses it to make a
numbered name. Its getValue()
method simply returns the value of its
input. The Output
class doesn’t do anything except serve as a marker
for circuit output.
public class Not extends UnaryOperator {
public Not() { super("NOT"); }
public boolean getValue() { return !getInput().getValue(); }
}
The Not
class uses "NOT"
as the name supplied to
the super
constructor and returns the logical NOT of the value of its
input.
Just as we did for unary operators, we also need a parent class for binary operators.
public class BinaryOperator extends Gate {
private Gate operand1;
private Gate operand2;
public BinaryOperator(String name) { super(name); }
public Gate getOperand1() { return operand1; }
public Gate getOperand2() { return operand2; }
public void setOperand1(Gate operand) { operand1 = operand; }
public void setOperand2(Gate operand) { operand2 = operand; }
}
A BinaryOperator
has two Gate
fields, operand1
and operand2
,
representing the inputs to the operator. The BinaryOperator
class has
an appropriate constructor and then accessors and mutators for the
operands. With BinaryOperator
as a parent, only a few lines of code
are necessary to define any logical binary operator.
public class And extends BinaryOperator {
public And() { super("AND"); }
public boolean getValue() {
return getOperand1().getValue() && getOperand1().getValue();
}
}
public class Or extends BinaryOperator {
public Or() { super("OR"); }
public boolean getValue() {
return getOperand1().getValue() || getOperand1().getValue();
}
}
public class Xor extends BinaryOperator {
public Xor() { super("XOR"); }
public boolean getValue() {
return getOperand1().getValue() ^ getOperand1().getValue();
}
}
In each case, a constructor passes the name of the gate to the super
constructor. Then, each getValue()
method gets the values from the two
operands and combines them with AND, OR, or XOR, respectively. This
design allows the programmer to focus only on the important element of
each class. Adding new classes for NAND, NOR, or any other possible
logical binary operator would be quick.
The client code that uses these classes to simulate a circuit follows.
import java.util.*;
public class BooleanCircuit {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int count = in.nextInt();
Gate[] gates = new Gate[count];
String name;
int value;
First we have the import
needed for Scanner
. In main()
, we read in the
total number of gates, create an array of type Gate
of that length,
and declare a few useful temporary variables.
// Create gates
for(int i = 0; i < count; i++) {
name = in.next().toUpperCase();
if(name.equals("true"))
gates[i] = new True();
else if(name.equals("false"))
gates[i] = new False();
else if(name.equals("AND"))
gates[i] = new And();
else if(name.equals("OR"))
gates[i] = new Or();
else if(name.equals("XOR"))
gates[i] = new Xor();
else if(name.equals("NOT"))
gates[i] = new Not();
else if(name.equals("OUTPUT")) {
value = in.nextInt();
gates[i] = new Output(value);
}
}
Then, we parse the input, creating an appropriate gate based on the name read in. In the case of an OUTPUT gate, we must also read in a number so that we can identify which OUTPUT gate is which later.
//connect gates
while(in.hasNextInt()) {
value = in.nextInt();
name = gates[value].getName();
if(name.equals("AND") || name.equals("OR") || name.equals("XOR")) {
BinaryOperator operator = (BinaryOperator)gates[value];
operator.setOperand1(gates[in.nextInt()]);
operator.setOperand2(gates[in.nextInt()]);
}
else if(name.equals("NOT") || name.startsWith("OUTPUT")) {
UnaryOperator operator = (UnaryOperator)gates[value];
operator.setInput(gates[in.nextInt()]);
}
}
As long as there’s input remaining, we read in an index. Based on the name of the gate at that index in the array, we either read in two more indexes (for binary operators) or just a single additional index (for unary operators). In either case, we set the input or inputs of the operator to the gate or gates at those indexes.
// Compute output
for(int i = 0; i < count; i++)
if(gates[i].getName().startsWith("OUTPUT"))
System.out.println(gates[i]);
}
}
Finally, the simulation of the circuit is surprisingly simple. We look
through array until we find a gate whose name starts with "OUTPUT"
.
Then, we print out its value. In order to determine its value, it will
ask its input what its value is, which in turn will ask for the values
from its input. The toString()
in the Gate
class will assure us that
the final output is nicely formatted. This system accommodates any
number of output gates connected arbitrarily, as long as the circuit has
no loops inside of it, such as an AND gate whose output is also one of
its inputs.
11.6. Concurrency: Inheritance
Like interfaces, inheritance in Java is not closely related to concurrency. However, two ways in which inheritance interacts with concurrency deserve attention.
The first is the Thread
class. Each thread of execution in Java
(except the main thread) is managed with a Thread
object or an object
whose type inherits from Thread
. Creating such types is done by
extending Thread
, just as you would extend any other class. Further
information about extending Thread
for concurrency is given in
Section 13.4. Extending the Thread
class to make
your own customized threads of execution is an alternative to
implementing the Runnable
interface mentioned in
Section 10.6 and is discussed in greater detail in
Section 13.4.5.
The second interaction between inheritance and concurrency is again very
similar to the problem with interfaces and concurrency: There’s no way
to specify that a method is thread-safe. Recall that it’s not allowed
to use the synchronized
keyword on a method in an interface
declaration. Likewise, there’s no restriction on overriding a
synchronized method with a non-synchronized method or vice versa.
The rules for overriding methods in Java guarantee that an object of a child class is usable anywhere that an object of the parent class is usable. Thus, you cannot override a public method with a private one, reducing the visibility of a method. We discuss a similar restriction with exceptions in Section 17.3.4.
If it has these restrictions, why doesn’t Java prevent a synchronized method from being overridden by a non-synchronized method? In the first place, a non-synchronized method can be used anywhere a synchronized one could (unlike a private method, which is not accessible everywhere a public one is). In the second, the designers of Java put thread safety in the category of implementation details left up to the programmer. Some classes need specific methods to be synchronized and others (even child classes) do not. However, if you override a class with a synchronized method, it’s safest to mark your method synchronized as well.
11.7. Exercises
Conceptual Problems
-
Give three advantages of using inheritance instead of copying and pasting code from a parent class. Are there any disadvantages to using inheritance?
-
Consider classes
Radish
andCarrot
which both extend classVegetable
and implement interfaceCrunchable
. Which of the following sets of assignments are legal and why?-
Radish radish = new Radish();
-
Radish radish = new Vegetable();
-
Vegetable vegetable = new Radish();
-
Crunchable crunchy = new Radish();
-
Radish radish = new Carrot();
-
-
In the context of inheritance, the keyword
super
can be used for two different purposes. What are they? -
Consider the following class definitions.
public class A { private String value; public A(String s) { value = "A" + s + "A"; } public String toString() { return value; } } public class B extends A { public B(String s) { super("B" + s + "B"); } } public class C extends B { public C(String s) { super("C" + s + "C"); } }
What’s output by the following code fragment?
C c = new C("ABC"); System.out.println(c);
-
Beginning Java programmers often confuse package-private access (no explicit specifier) with
public
access. How is this confusion possible when default access is more constrained than bothpublic
andprotected
access? (Hint: The file system plays a role.) -
What are the similarities and differences between overloading a method and overriding a method?
-
What’s field hiding? How can software bugs arise from this Java feature?
-
Give reasons why the designers of Java decided not to allow multiple inheritance. Would you have made the same decision? Why or why not?
-
Draw a class hierarchy establishing a sensible relationship between the
Human
,Soldier
,Sailor
,Marine
,General
, andAdmiral
classes. For this class hierarchy, refer to the U.S. military structure in which the U.S. Marine Corps is a part of the U.S. Navy.
Programming Practice
-
Create an
InternationalStudent
class that extendsStudent
. It should includeString
fields for country of origin and visa status. It should include mutator and accessor methods for these two new fields. -
Add
Pentagon
andHexagon
classes that extend thePolygon
class. The constructor for each class should take an x, y and radius value, each ofint
type. Both classes should be implemented to create regular polygons, that is, polygons in which all five or six sides have the same length. The x and y values should give the center of the polygon, and each of the five or six points should be the radius distance away from that center.Because the internal structure of
Polygon
keeps all vertices asPoint
values, the x and y coordinates of the points must beint
values. This requirement will force you to round these x and y coordinates after using trigonometry to determine their locations. As a result, the final pentagons and hexagons stored and displayed will be slightly irregular. -
The inheritance design of our solution to the Boolean circuits problem given in Section 11.5 makes adding new gates easy. Add classes that implement a NAND gate and a NOR gate. Then, rewrite the
main()
method ofBooleanCircuit
to accommodate these two extra classes. -
Re-implement the object hierarchy in the solution from Section 10.5 to the sort it out problem. This time, let the
Cat
,Dog
, andPerson
classes extend theCreature
class defined below.public class Creature implements Ageable, Weighable { protected int age; protected double weight; public Creature(int age, double weight) { this.age = age; this.weight = weight; } public int getAge() { return age; } public double getWeight() { return weight; } }
Refactor your code so that the
Cat
,Dog
, andPerson
classes are as short as possible. How many lines of code do you save? -
Design a celestial body simulator. You’ll need to create a class containing fields for the x, y, and z locations, x, y, and z velocities, radii, and masses of each object. For each time step of length t, you must do the following.
-
Compute the sum of forces exerted on each body by every other body. The equation for gravitational force on body b exerted by body a is given by the following equation.
As mentioned in Example 8.1, the gravitational constant G = 6.673 × 10-11 N·m2·kg-2. Also, |rab| is the distance between the centers of objects a and b. Finally, the unit vector between the centers of the two objects is given by the equation below.
-
Compute the x, y, and z components of the acceleration vector a for each object using the equation F = ma once the sum of forces has been calculated.
-
Update the x, y, and z components of the velocity vector v for each object using the equation vnew = vold + at.
-
Experiments
-
Inheritance is a powerful technique, but it comes with some overhead costs. Create a class called
A
with the following implementation.public class A { protected int a; }
Then, create 25 more classes named
B
throughZ
. ClassB
should extendA
and add aprotected
int
field calledb
. Continue in this manner, with each new class extending the previous one and adding anint
field named the lowercase version of the class name. Thus, if you create an object of typeZ
, it will contain, through inheritance, 26int
fields nameda
throughz
. But for a singleZ
object to be created, it must call 27 (Z
back throughA
plusObject
) constructors. You may wish to use the file I/O material in Chapter 20 to write a program to create all these classes so that you do not have to do so by hand.Finally, create a new class called
All
which contains 26protected
fields ofint
type nameda
throughz
. Now, the purpose of creating all these classes is to compare the time needed to instantiate an object of typeZ
with one of typeAll
, though they both only contain 26int
fields nameda
throughz
.Create an array of 100,000 elements of type
Z
and then populate it with 100,000Z
objects. Time this process. Create an array of 100,000 elements of typeAll
and then populate that array with 100,000All
objects. You may wish to use theSystem.nanoTime()
method described in Chapter 13 to accurately time these processes. Is there a significant difference in the times you found?