Classes in Java exist in a hierarchy. A class in Java can be
declared as a subclass of another class using the
extends
keyword. A
subclass inherits variables and methods from its
superclass and can use them as if they were declared
within the subclass itself:
class
Animal
{
float
weight
;
...
void
eat
()
{
...
}
...
}
class
Mammal
extends
Animal
{
// inherits weight
int
heartRate
;
...
// inherits eat()
void
breathe
()
{
...
}
}
In this example, an object of type Mammal
has both the instance variable weight
and the method eat()
. They are inherited from Animal
.
A class can extend only one other class. To use the proper terminology, Java allows single inheritance of class implementation. Later in this chapter, we’ll talk about interfaces, which take the place of multiple inheritance as it’s primarily used in other languages.
A subclass can be further subclassed. Normally, subclassing specializes or refines a class by adding variables and methods (you cannot remove or hide variables or methods by subclassing). For example:
class
Cat
extends
Mammal
{
// inherits weight and heartRate
boolean
longHair
;
...
// inherits eat() and breathe()
void
purr
()
{
...
}
}
The Cat
class is a type of
Mammal
that is ultimately a type of
Animal
. Cat
objects inherit all the characteristics of
Mammal
objects and, in turn, Animal
objects. Cat
also provides additional behavior in the
form of the purr()
method and the
longHair
variable. We can denote the
class relationship in a diagram, as shown in Figure 6-1.
A subclass inherits all members of its superclass not designated as
private
. As we’ll discuss
shortly, other levels of visibility affect which inherited members of the
class can be seen from outside of the class and its subclasses, but at a
minimum, a subclass always has the same set of visible members as its
parent. For this reason, the type of a subclass can be considered a
subtype of its parent, and instances of the subtype
can be used anywhere instances of the supertype are allowed. Consider the
following example:
Cat
simon
=
new
Cat
();
Animal
creature
=
simon
;
The Cat
instance simon
in this example can be assigned to the
Animal
type variable creature
because Cat
is a subtype of Animal
. Similarly, any method accepting an
Animal
object would accept an instance
of a Cat
or any Mammal
type as well. This is an important aspect
of polymorphism in an object-oriented language such as Java. We’ll see how
it can be used to refine a class’s behavior, as well as add new
capabilities to it.
In Chapter 5, we saw that a local variable of the same name as an instance variable shadows (hides) the instance variable. Similarly, an instance variable in a subclass can shadow an instance variable of the same name in its parent class, as shown in Figure 6-2. We’re going to cover the details of this variable hiding now for completeness and in preparation for more advanced topics, but in practice you should almost never do this. It is much better in practice to structure your code to clearly differentiate variables using different names or naming conventions.
In Figure 6-2, the variable
weight
is declared in three places:
as a local variable in the method foodConsumption()
of the class Mammal
, as an instance variable of the class
Mammal
, and as an instance variable
of the class Animal
. The actual
variable selected when you reference it in the code would depend on the
scope in which we are working and how you qualify the reference to
it.
In the previous example, all variables were of the same type. A
slightly more plausible use of shadowed variables would involve changing
their types. We could, for example, shadow an int
variable with a double
variable in a subclass that needs
decimal values instead of integer values. We can do this without
changing the existing code because, as its name suggests, when we shadow
variables, we don’t replace them but instead mask them. Both variables
still exist; methods of the superclass see the original variable, and
methods of the subclass see the new version. The determination of what
variables the various methods see occurs at compile time.
Here’s a simple example:
class
IntegerCalculator
{
int
sum
;
...
}
class
DecimalCalculator
extends
IntegerCalculator
{
double
sum
;
...
}
In this example, we shadow the instance variable sum
to change its type from int
to double
.[15] Methods defined in the class IntegerCalculator
see the integer variable
sum
, while methods defined in
DecimalCalculator
see the
floating-point variable sum
. However,
both variables actually exist for a given instance of DecimalCalculator
, and they can have
independent values. In fact, any methods that DecimalCalculator
inherits from IntegerCalculator
actually see the integer
variable sum
.
Because both variables exist in DecimalCalculator
, we need a way to reference
the variable inherited from IntegerCalculator
. We do that using the
super
keyword as a
qualifier on the reference:
int
s
=
super
.
sum
;
Inside of DecimalCalculator
,
the super
keyword used in this manner
selects the sum
variable defined in
the superclass. We’ll explain the use of super
more fully in a bit.
Another important point about shadowed variables has to do with
how they work when we refer to an object by way of a less derived type
(a parent type). For example, we can refer to a DecimalCalculator
object as an IntegerCalculator
by using it via a variable
of type IntegerCalculator
. If we do
so and then access the variable sum
,
we get the integer variable, not the decimal one:
DecimalCalculator
dc
=
new
DecimalCalculator
();
IntegerCalculator
ic
=
dc
;
int
s
=
ic
.
sum
;
// accesses IntegerCalculator sum
The same would be true if we accessed the object using an explicit
cast to the IntegerCalculator
type or
when passing an instance into a method that accepts that parent
type.
To reiterate, the usefulness of shadowed variables is limited. It’s much better to abstract the use of variables like this in other ways than to use tricky scoping rules. However, it’s important to understand the concepts here before we talk about doing the same thing with methods. We’ll see a different and more dynamic type of behavior when methods shadow other methods, or to use the correct terminology, override other methods.
In Chapter 5, we saw that we could declare overloaded methods (i.e., methods with the same name but a different number or type of arguments) within a class. Overloaded method selection works in the way we described on all methods available to a class, including inherited ones. This means that a subclass can define additional overloaded methods that add to the overloaded methods provided by a superclass.
A subclass can do more than that; it can define a method that has exactly the same method signature (name and argument types) as a method in its superclass. In that case, the method in the subclass overrides the method in the superclass and effectively replaces its implementation, as shown in Figure 6-3. Overriding methods to change the behavior of objects is called subtype polymorphism. It’s the usage most people think of when they talk about the power of object-oriented languages.
In Figure 6-3, Mammal
overrides the reproduce()
method of Animal
, perhaps to specialize the method for
the behavior of mammals giving birth to live young.[16] The Cat
object’s
sleeping behavior is also overridden to be different from that of a
general Animal
, perhaps to
accommodate cat naps. The Cat
class
also adds the more unique behaviors of purring and hunting mice.
From what you’ve seen so far, overridden methods probably look like they shadow methods in superclasses, just as variables do. But overridden methods are actually more powerful than that. When there are multiple implementations of a method in the inheritance hierarchy of an object, the one in the “most derived” class (the furthest down the hierarchy) always overrides the others, even if we refer to the object through a reference of one of the superclass types.[17]
For example, if we have a Cat
instance assigned to a variable of the more general type Animal
, and we call its sleep()
method, we still get the sleep()
method implemented in the Cat
class, not the one in Animal
:
Cat
simon
=
new
Cat
();
Animal
creature
=
simon
;
...
creature
.
sleep
();
// accesses Cat sleep();
In other words, for purposes of behavior (invoking methods), a
Cat
acts like a Cat
, regardless of whether you refer to it as
such. In other respects, the variable creature
here may behave like an Animal
reference. As we explained earlier,
access to a shadowed variable through an Animal
reference would find an implementation
in the Animal
class, not the Cat
class. However, because methods are
located dynamically, searching subclasses first,
the appropriate method in the Cat
class is invoked, even though we are treating it more generally as an
Animal
object. This means that the
behavior of objects is dynamic. We can deal with
specialized objects as if they were more general types and still take
advantage of their specialized implementations of behavior.
A common programming error in Java is to accidentally
overload a method when trying to override it. Any difference in the
number or type of arguments (the method
signature) produces two overloaded methods
instead of a single, overridden method. The new annotations syntax in
Java 5.0 provides a way to get the compiler to help with this problem.
An annotation, as we’ll describe in Chapter 7, allows us to add special markers or
metadata to source code that can be read by the compiler or runtime
tools. One of the standard annotations that Java defines is called
@Override
and it tells the compiler
that the method it marks is intended to override a method in the
superclass. The compiler then warns if the method doesn’t match. For
example, we could specify that the sleep()
method of our Cat
class overrides one in a superclass like
so:
class
Cat
extends
Mammal
{
...
@Override
void
sleep
()
{
...
}
}
In a previous section, we mentioned that overloaded methods are selected by the compiler at compile time. Overridden methods, on the other hand, are selected dynamically at runtime. Even if we create an instance of a subclass our code has never seen before (perhaps a new class loaded over the network), any overriding methods that it contains are located and used at runtime, replacing those that existed when we last compiled our code.
In contrast, if we created a new class that implements an additional, more specific, overloaded method, and replace the compiled class in our classpath with it, our code would continue to use the implementation it discovered originally. This situation would persist until we recompiled our code along with the new class. Another effect of this is that casting (i.e., explicitly telling the compiler to treat an object as one of its assignable types) affects the selection of overloaded methods at compile time but not overridden methods.
In practice what we’ve just described is not something you need to worry about often, but it’s important in understanding what the virtual machine does and does not do at runtime.
Static methods don’t belong to any object instance; they are accessed directly through a class name, so they are not dynamically selected at runtime like instance methods. That is why static methods are called “static”; they are always bound at compile time.
A static method in a superclass can be shadowed by another static method in a subclass, as long as the original method was not declared final. However, both methods are always accessible directly via their respective class names. You can’t “override” a static method with an instance method. In other words, you can’t have a static method and instance method with the same signature in the same class hierarchy.
In languages like C++, the default is for methods to act
like shadowed variables, so you have to declare explicitly the methods
you want to be dynamic (or, as C++ terms them,
virtual). In Java, instance methods are, by
default, dynamic. But you can use the final
modifier to
declare that an instance method can’t be overridden in a subclass, and
it won’t be subject to dynamic binding.
We have seen final
used with
variables to effectively make them constants. When applied to a
method, final
means that its
implementation is constant—no overriding allowed. final
can also be applied to an entire
class, which means the class can’t be subclassed.
In the old days, dynamic method binding came with a significant
performance penalty, and some people are still inclined to use the
final
modifier to guard against
this. Modern Java runtime systems eliminate the need for this kind of
tweaking. A profiling runtime can determine which methods are not
being overridden and “optimistically” inline them, treating them as if
they were final until it becomes necessary to do otherwise. As a rule,
you should use the final
keyword
when it is correct for your program’s structure, not for performance
considerations.
In some older versions of Java, the javac compiler can be run with a -O switch, which tells it to perform certain optimizations, like inlining, statically. Most of these optimizations are now done at runtime by smarter VMs, so switches like this are generally not necessary.
Another kind of optimization allows you to include debugging
code in your Java source without incurring a size or performance
penalty. Although Java doesn’t have a preprocessor to explicitly
control what source is included, you can get some of the same effects
by making a block of code conditional on a constant (i.e., static
and final
) variable. The Java compiler is smart
enough to remove this code when it determines that it won’t be called.
For example:
static
final
boolean
DEBUG
=
false
;
...
final
void
debug
(
String
message
)
{
if
(
DEBUG
)
{
System
.
err
.
println
(
message
);
// do other stuff
...
}
}
In this case, the compiler can recognize that the condition on
the DEBUG
variable is always
false
, and the body of the debug()
method will be optimized away. With
a modern compiler, the method call might even be optimized away
entirely.
Note that this kind of debugging code is useful for purposes such as logging. In contrast to assertions, which we covered in Chapter 4, which are supposed to be yes/no tests that guarantee the correctness of your program logic, these conditional blocks of code might do expensive formatting or other output processing that is useful during develoment but you don’t wish to have around in the final product.
By now you should have a good, intuitive feel for how methods are selected from the pool of potentially overloaded and overridden method names of a class. If, however, you are dying for more detail, we’ll provide it now.
In a previous section, we offered an inductive rule for overloaded method resolution. It said that a method is considered more specific than another if its arguments are assignable to the arguments of the second method. We can now expand this rule to include the resolution of overridden methods by adding the following condition: to be more specific than another method, the type of the class containing the method must also be assignable to the type of the class holding the second method.
What does that mean? Well, the only classes whose types are assignable are classes in the same inheritance hierarchy, meaning that we’re talking about the set of all methods of the same name in a class or any of its parent or child classes. Because subclass types are assignable to superclass types, but not vice versa, the resolution is pushed in the way that we expect down the chain toward the subclasses. This effectively adds a second dimension to the search, in which resolution is pushed down the inheritance tree toward more refined classes and, simultaneously, toward the most specific overloaded method within a given class.
An overriding method may change the behavior of an
object, but in some ways, it must still fulfill the contract of the
original method with the user. Specifically, an overriding method must
adhere to the throws
clause of the
original method. The new method cannot throw new types of checked
exceptions. It can only declare that it throws exception types
assignable to those thrown by the method in the parent class; that is,
it may declare that it throws the same types of exceptions or subtypes
of those declared by the original method. If the new method does not
throw any of the checked exceptions of the original, it does not have
to declare them and callers of the method via the subclass do not have
to guard against them. (In this way, you can override a method to
“handle” exceptions for the user.)
So the new method may declare exactly the same checked
exceptions as the original, or it has the option to refine those types
by declaring that it throws more specific subtypes than the overridden
method. This is not the same as just saying that the method can simply
throw subtypes of its declared exceptions; any method can do that. The
new method can actually redefine the throws
clause of the method to be more
specific. This technique is called covariant typing of the throws
clause, which means that the
exception types against which the user must guard change to become
more refined with the subtype.
Let’s quickly review what the throws
clause really means. If a method
declares that it can throw an IOException
, it is really saying that it can
throw exceptions of type IOException
or its subtypes. For example,
FileNotFoundException
is a type of
IOException
. A method declaring
that it can throw IOException
could
actually throw FileNotFoundException
or any other subtype
of IOException
at runtime:
public
void
readFile
()
throws
IOException
{
...
if
(
error
)
throw
new
FileNotFoundException
(
filename
);
}
When we call this method, the compiler will ensure that we allow
for the possibility of any kind of IOException
, using either a try/catch
block or by throwing the exception
from our own method.
When we override a method in a subclass, we get an opportunity
to rewrite the throws
clause of the
method a bit. The new method must still be backward-compatible with
the original, so any checked exceptions it throws must be assignable
to those thrown by the overridden method. But we can be more specific
if we want, refining the type of exception to go along with the new
method’s behavior. For example:
class
MeatInedibleException
extends
InedibleException
{
...
}
class
Animal
{
void
eat
(
Food
f
)
throws
InedibleException
{
...
}
}
class
Herbivore
extends
Animal
{
void
eat
(
Food
f
)
throws
MeatInedibleException
{
if
(
f
instanceof
Meat
)
throw
new
MeatInedibleException
();
...
}
}
In this code, Animal
specifies that it can throw an InedibleException
from its eat()
method. Herbivore
is a subclass of Animal
, so its eat()
method must also be able to throw an
InedibleException
. However,
Herbivore
’s eat()
method actually declares that it
throws a more specific exception: MeatInedibleException
. It can do this
because MeatInedibleException
is a
subtype of InedibleException
. If we
are working with an Herbivore
type
directly, the compiler will allow us to catch just the MeatInedibleException
and not require us to
guard against the more general InedibleException
:
Herbivore
creature
=
...
try
{
creature
.
eat
(
food
);
}
catch
(
MeatInedibleException
)
{
// creature can't eat this food because it's meat
}
On the other hand, if we don’t care why the food is inedible,
we’re free to guard for the more general InedibleException
alone and treat it as any
other Animal
.
To sum up, an overriding method can refine not only the behavior of the parent method, but also the type of checked exceptions it throws. Next, we’ll talk about overridden methods that change their return type in exactly the same way.
For a method to qualify as an overridden method in a
subclass, it must have exactly the same number and types of arguments.
It must have the same “inputs,” as it were. As we saw in the previous
section, overriding methods may refine their “output” to some extent.
Namely, they can narrow their throws
clause by declaring that they throw
subtypes of the original method’s exception types. What about the main
“output” of a method? Its return value? Can we change the return type
of a method by overriding it? The answer is that Java gives us
covariant return types on methods just as it does for exception
types.
What this means is that when you override a method, you may
change the return type to a subtype of the original method’s return
type. For example, if our Animal
class has a factory method called create()
that produces an instance of
Animal
, our Mammal
class could refine the return type to
Mammal
:
class
Animal
{
Animal
create
()
{
...
}
}
class
Mammal
extends
Animal
{
Mammal
create
()
{
...
}
}
As we’ll see later, this coding technique is very helpful because it eliminates some runtime casting of objects.
The special references this
and super
allow you to refer to the members of the
current object instance or to members of the superclass, respectively.
We have seen this
used elsewhere to
pass a reference to the current object and to refer to shadowed instance
variables. The reference super
does the same for
the parents of a class. You can use it to refer to members of a
superclass that have been shadowed or overridden. Being able to invoke
the original method of the superclass allows us to use it as part of our
new method, delegating to its behavior before or after we perform
additional work:
class
Animal
{
void
eat
(
Food
f
)
throws
InedibleException
{
// consume food
}
}
class
Herbivore
extends
Animal
{
void
eat
(
Food
f
)
throws
MeatInedibleException
{
// check if edible
...
try
{
super
.
eat
(
f
);
}
catch
(
InedibleException
e
)
{
...
}
}
}
In this example, our Herbivore
class overrides the Animal eat()
method to first do some checking on the food
object. After doing its job, it uses
super.eat()
to call the (otherwise
overridden and inaccessible) implementation of eat()
in its superclass.
super
prompts a search for the
method or variable to begin in the scope of the immediate superclass
rather than the current class. The inherited method or variable found
may reside in the immediate superclass or one further up the tree. The
usage of the super
reference when
applied to overridden methods of a superclass is special; it tells the
method resolution system to stop the dynamic method search at the
superclass instead of at the most derived class (as it otherwise does).
Without super
, there would be no way
to access overridden methods.
A cast explicitly tells the compiler
to change the apparent type of an object reference. The main use for
casts is when an object is temporarily assigned to a more general type.
For example, if a String
were
assigned to a variable of type Object
, to use it as a String
again, we’d have to perform a cast to
get it back. The compiler recognizes only the declared types of
variables and doesn’t know that we actually placed a String
into it. In Java, casts are checked
both at compile time and at runtime to make sure they are legal. At
compile time the Java compiler will stop you from trying to perform a
cast that cannot possibly work (such as turning a Date
directly into a String
). And at runtime, Java will check that
casts that are plausible (such as our Object
to String
) are actually correct for the real
objects involved.
Attempting to cast an object to an incompatible type at runtime
results in a Class
Cast
Exception
. Only casts between objects in
the same inheritance hierarchy (and, as we’ll see later, to appropriate
interfaces) are legal in Java and pass the scrutiny of the compiler and
the runtime system. Casts in Java affect only the treatment of
references; they never change the form of the actual object. This is an
important rule to keep in mind. You never change the object pointed to
by a reference by casting it; you change only the compiler’s (or runtime
system’s) notion of it.
A cast can be used to narrow or downcast the type of a reference—to
make it more specific. Often, we’ll do this when we have to retrieve an
object from a more general type of collection or when it has been
previously used as a less derived type. (The prototypical example is
using an object in a collection, as we’ll see in Chapter 11.) Continuing with our Cat
example:
Animal
creature
;
Cat
simon
;
// ...
creature
=
simon
;
// OK
// simon = creature; // Compile-time error, incompatible type
simon
=
(
Cat
)
creature
;
// OK
We can’t reassign the reference in creature
to the variable simon
even though we know it holds an instance
of a Cat
(Simon). We have to perform
the indicated cast to narrow the reference. Note that an implicit cast
was performed when we went the other way to widen
the reference simon
to type Animal
during the first assignment. In this
case, an explicit cast would have been legal but superfluous.
What all this means is that you can’t lie or guess about what an
object is. If you have a Cat
object,
you can use it as an Animal
or even
Object
because all Java classes are a
subclass of Object
. But if you have
an Object
you think is a Cat
, you have to perform a cast to get it back
to an Animal
or a Cat
. If you aren’t sure whether the Object
is a Cat
or a Dog
at runtime, you can check it with
instanceof
before you
perform the cast. If you do not check and you get the cast wrong, the
runtime system throws a Class
Cast
Exception
.
if
(
creature
instanceof
Cat
)
{
Cat
cat
=
(
Cat
)
creature
;
cat
.
meow
();
}
As we mentioned earlier, casting can affect the selection of
compile-time items such as variables and overloaded methods, but not the
selection of overridden methods. Figure 6-4 shows the difference. As shown in
the top half of the diagram, casting the reference simon
to type Animal
(widening it) affects the selection of
the shadowed variable weight
within
it. However, as the lower half of the diagram indicates, the cast
doesn’t affect the selection of the overridden method sleep()
.
Casting in Java is something that programmers strive to avoid. This is not only because it indicates a weakness in the static typing of the code, but because casts can also simply be tedious to use and make code less readable. Unfortunately, a great deal of code written in Java in the past has had no choice but to rely on casting so that it can work with any type of object the user requires. Java 5.0 introduced a major new language feature, generics, partly to address this issue. Generics allow Java code to be “typed” for a particular kind of object by the user, eliminating the need to cast in many situations. We’ll cover generics in detail in Chapter 8 and see how they reduce the need for casts in most Java code.
When we talked earlier about constructors, we discussed
how the special statement this()
invokes an overloaded constructor upon entry to another constructor.
Similarly, the statement super()
explicitly invokes the constructor of a superclass. Of course, we also
talked about how Java makes a chain of constructor calls that includes
the superclass’s constructor, so why use super()
explicitly? When Java makes an
implicit call to the superclass constructor, it calls the default
constructor. If we want to invoke a superclass constructor that takes
arguments, we have to do so explicitly using super()
.
If we are going to call a superclass constructor with super()
, it must be the first statement of our
constructor, just as this()
must be
the first call we make in an overloaded constructor. Here’s a simple
example:
class
Person
{
Person
(
String
name
)
{
// setup based on name
...
}
...
}
class
Doctor
extends
Person
{
Doctor
(
String
name
,
String
specialty
)
{
super
(
name
);
// setup based on specialty
...
}
...
}
In this example, we use super()
to take advantage of the implementation of the superclass constructor
and avoid duplicating the code to set up the object based on its name.
In fact, because the class Person
doesn’t define a default (no arguments) constructor, we have no choice
but to call super()
explicitly.
Otherwise, the compiler would complain that it couldn’t find an
appropriate default constructor to call. In other words, if you subclass
a class whose constructors all take arguments, you have to invoke one of
the superclass’s constructors explicitly from at least one of your
subclass’s constructors.
Instance variables of the class are initialized upon return from
the superclass constructor, whether that’s due to an explicit call to
super()
or an implicit call to the
default superclass constructor.
We can now tell the full story of how constructors are chained together and when instance variable initialization occurs. The rule has three parts and is applied repeatedly for each successive constructor that is invoked:
If the first statement of a constructor is an ordinary statement—that is, not a call to
this()
orsuper()
—Java inserts an implicit call tosuper()
to invoke the default constructor of the superclass. Upon returning from that call, Java initializes the instance variables of the current class and proceeds to execute the statements of the current constructor.If the first statement of a constructor is a call to a superclass constructor via
super()
, Java invokes the selected superclass constructor. Upon its return, Java initializes the current class’s instance variables and proceeds with the statements of the current constructor.If the first statement of a constructor is a call to an overloaded constructor via
this()
, Java invokes the selected constructor, and upon its return, simply proceeds with the statements of the current constructor. The call to the superclass’s constructor has happened within the overloaded constructor, either explicitly or implicitly, so the initialization of instance variables has already occurred.
A method in Java can be declared with the abstract
modifier to
indicate that it’s just a prototype. An abstract method has no body;
it’s simply a signature declaration followed by a semicolon. You can’t
directly use a class that contains an abstract method; you must instead
create a subclass that implements the abstract method’s body:
abstract
void
vaporMethod
(
String
name
);
In Java, a class that contains one or more abstract methods must
be explicitly declared as an abstract class, also using the abstract
modifier:
abstract
classVaporClass
{
...
abstract
void
vaporMethod
(
String
name
);
...
}
An abstract class can contain other nonabstract methods and
ordinary variable declarations, but it can’t be instantiated. To be
used, it must be subclassed and its abstract methods must be
“overridden” with methods that implement a body. Not all abstract
methods have to be implemented in a single subclass, but a subclass that
doesn’t override all its superclass’s abstract methods with actual,
concrete implementations must also be declared abstract
.
class
MyVaporImplementation
extends
VaporClass
{
void
vaporMethod
(
String
name
)
{
...
}
}
Abstract classes provide a framework for classes that is to be
“filled in” by the implementer. The java.io.InputStream
class, for example, has a
single abstract method called read()
.
Various subclasses of InputStream
implement read()
in their own ways to
read from their own sources. The rest of the InputStream
class, however, provides extended
functionality built on the simple read()
method. A subclass of InputStream
inherits these nonabstract methods
to provide functionality based on the simple read()
method that the subclass
implements.
[15] Note that a better way to design our calculators would be to
have an abstract Calculator
class
with two subclasses: IntegerCalculator
and DecimalCalculator
.
[16] The Platypus
is a highly
unusual egg-laying Mammal
. We
could override the reproduce()
behavior again for it in its own subclass of Mammal
.
[17] An overridden method in Java acts like a virtual
method in
C++.
Get Learning Java, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.