Understanding Inheritance
Inheritance is the process by which a subclass automatically includes any public or protected members of the class, including primitives, objects or methods defined in the parent class.
We refer to any class that inherits from another class a subclass or child class, as it’s considered a descendant of that class. Alternatively, we refer to the class that the child inherits from as the superclass or parent class, as it’s considered an ancestor of the class. Inheritance is transitive.
public class BigCat {
public double size;
}
public class Jaguar extends BigCat {
public Jaguar() {
size = 10.2;
}
public void printDetails() {
sout(size);
}
}
size
is accesible because it’s marked as public. Jaguar
can read or write it as if it were its own member.
Single vs Multiple Inheritance
Java supports single inheritance, by which a class may inherit from only one direct parent class. It also supports multiple levels of inheritance, by which one class may extend another class, which in turn extends another class. You can have any number of levels of inheritance.
By design, Java doesn’t support multiple inheritance because it can lead to complex, often difficult-to-maintain data models. It allows though one exception - a class may implement multiple interfaces.
It is possible to prevent a class from being extended by marking it with final
modifier. If you try to inherit from such a class, then the class will fail to compile.
Inheriting Object
All classes inherit from a single class: java.lang.Object
. Object
is the only class in Java that doesn’t have a parent class. You don’t need to extend Obect
manually. The compiler automatically does the job for you, if you didn’t specify any extends to another class.
Primitives don’t inherit from Object
, since they’re not classes.
Creating Classes
Extending a class
public abstract class ElephantSeal extends Seal {
}
Let’s check out an example
public class Animal {
private int age;
protected String name;
public int getAge() {
return age;
}
public void setAge(int newAge) {
age = newAge;
}
}
public class Lion extends Animal {
public void setProperties(int age, String n) {
setAge(age);
name = n;
}
public void roar() {
sout(name + ", age" + getAge() + ", says: Roar!");
}
}
Lion
may access to the getters, but be careful when accessing age
directly, as it’s private
and it wouldn’t compile.
public void roar() {
sout("Lions age: " + age); // DOES NOT COMPILE
}
The name
variable can be accessed directly, as it’s marked as protected
.
Applying Class Access Modifiers
You can also apply access modifiers to class definitions. In Java, a top-level class is one that’s not defined inside another class. They can only have public
or package-private access
.
An inner class is a class defined inside of another class and is the opposite of a top-level class. Inner classes can also have protected
and private
access.
A Java file can have many top-level classes but at most one (or none) public top-level class. There’s also no requirement for the public class to be the first class in the file. One benefit of using the package-private access is that you can define many classes within the same Java file.
class Rodent {}
public class Groundhog extends Rodent {}
Accessing this reference
What do you think the following program prints?
public class Flamingo {
private String color;
public void setColor(String color) {
color = color;
}
public static void main(String... unused) {
Flamingo f = new Flamingo();
f.setColor("PINK");
sout(f.color);
}
}
It prints null
. When it sees color = color
, it thinks you’re assigning the method parameter value to itself.
The fix is to use the this
keyword. It refers to the current instance of the class and can be used to access any member of the class, including inherited members.
It cannot be used when there’s no implicit instance of the class, such as in a static
method or static initializer block.
public void setColor(String color) {
this.color = color;
}
Watch out for examples that aren’t common but you might see on the exam.
public void setColor(String color) {
color = this.color; // BACKWARDS. NOT GOOD.
}
If you see this example, be careful with data types and its default values.
Calling the super Reference
A variable or method can be defined in both a parent class and a child class. We reference the version in the parent or the current class with this
and super
. The super
reference is similar to this
reference, except that it excludes any members found in the current class. The member must instead be accessible via inheritance.
class Mammal {
String type = "mammal";
}
public class Bat extends Mammal {
String type = "bat";
public String getType() {
return super.type + ":" + this.type;
}
public static void main(String... zoo) {
sout(new Bat().getType()); // mammal:bat
}
}
Declaring Constructors
A constructor is a special method that matches the name of the class and has no return type. It’s called when a new instance of the class is created.
Creating a Constructor
public class Bunny {
public bunny() { } // DOES NOT COMPILE
public void Bunny() { } // VALID
}
The first one doesn’t match the class name, because it’s case sensitive, and it expects it to be a method, but it hasn’t a return type so it doesn’t compile.
The second one is perfectly valid but is a method, not a constructor as it has a return type.
(!) Like method parameters, constructor params can be any valid class, arrays… but may not include var
. (!)
class Bonobo {
public Bonobo(var food) { // DOES NOT COMPILE
}
}
A class may have multiple constructors, so long as each constructor has a unique signature. This is refered to as constructor overloading.
public class Turtle {
private String name;
public Turtle() {
name = "John Doe";
}
public Turtle(int age) {}
public Turtle(long age) {}
public Turtle(String age, String... favoriteFoods) {}
}
When calling the constructor with new
keyword:
- Java allocates memory for the new object
- It looks for a constructor with a matching signature
- Calls it
Default constructor
Every class in Java has a constructor whether you code one or not. If you don’t include any constructors in the class, Java will create one for you without any parameters. This is called the default constructor and is added anytime a class is declared without any constructors.
public class Rabbit {
public static void main(String... args) {
Rabbit rabbit = new Rabbit(); // calls default constructor
}
}
This happens during compile time.
public class Rabbit1 { } // DEFAULT CONSTRUCTOR
public class Rabbit2 {
public Rabbit2() {} // NORMAL CONSTRUCTOR. IT'S NOT DEFAULT!
}
Having only private constructors in a class tells the compiler not to provide a default no-argument constructor. It also prevents other classes from instantiating the class.
This is useful when a class has only static methods or the developer wants to have full control of all calls to create new instances of the class.
(!) Remember, static
methods in the class, including main()
, may access private
members and constructors
. (!)
Calling Overloaded Constructors with this()
public class Hamster {
private String color;
private int weight;
public Hamster(int weight) { // First constructor
this.weight = weight;
color = "brown";
}
public Hamster(int weight, String color) { // Second constructor
this.weight = weight;
this.color = color;
}
}
For this example there’s a bit of duplication. To remove it, how can we have a constructor call another constructor?
public Hamster(int weight) {
Hamster(weight, "brown"); // DOES NOT COMPILE
}
Constructors can be called only by writing new
before the name of the constructor. They’re not like normal methdos that you can just call.
public Hamster(int weight) {
new Hamster(weight, "brown"); // Compiles, but is incorrect
}
This would create a new object with default weight
and color
. It then constructs a different object with the desired weight
and color
.
public Hamster(int weight) {
this(weight, "brown"); // correct solution
}
Instead, Java provides a solution this()
. When it’s used with parentheses, it calls another constructor on the same instance of the class.
It has one rule though, if you choose to call it this()
must be the first statement in the constructor. The side effect of this is that there can be only one call to this()
in any constructor.
public Hamster(int weight) {
sout("constructor"); // DOES NOT COMPILE
this(weight, "brown");
}
There’s another rule, a constructor cannot call itself (or other constructors) in a loop.
public class Hamster {
public Hamster(int food) {
this(5); // DOES NOT COMPILE
}
}
public class Hamster {
public Hamster() {
this(5); // DOES NOT COMPILE
}
public Hamster(int food) {
this(); // DOES NOT COMPILE
}
}
this vs this()
They’re very different. Be sure to know which is which.
this
refers to an instance of the class.
this()
refers to a constructor call within the class.
Calling Parent Constructors with super()
The first statement of every constructor is either a call to another constructor within the class, using this()
, or a call to a constructor in the direct parent class, using super()
.
public class Animal {
private int age;
public Animal(int age) {
super(); // refers to java.lang.Object
this.age = age;
}
}
public class Zebra extends Animal {
public Zebra(int age) {
super(age); // refers to animal
}
public Zebra() {
this(4); // refers to Zebra with int arg
}
}
Like calling this()
, calling super()
can only be used as the first statement of the constructor. The following example won’t compile.
public class Zoo {
public Zoo() {
sout("Zoo created");
super(); // DOES NOT COMPILE
}
}
public class Zoo {
public Zoo() {
super();
sout("Zoo created");
super(); // DOES NOT COMPILE
}
}
If the parent class has more than one constructor, the child class may use any valid parent constructor in its definition.
super vs super()
super
is used to reference members of the parent class, while super()
calls a parent constructor.
Understanding Compiler Enhancements
The Java compiler automatically inserts a call to the no-argument constructor super()
if you do not explicitly call this()
or super()
.
Are Classes with only private Constructors Considered final?
A final
class cannot be extended. If you have a class that’s not marked as final
but only contains private constructors, you can extend the class, but only an inner class defined in the class itself can extend it.
An inner class is the only one that would have access to a private
constructor and be able to all super()
.
Missing a Default No-Argument Constructor
What happens if the parent class doesn’t have a no-argument constructor? Remember it’s only inserted by the compiler only if there’s no constructor defined in the class.
public class Mammal {
public Mammal(int age) {}
}
public class Elephant extends Mammal { // DOES NOT COMPILE
}
Since Elephant
does not define any constructor, the Java compiler will attempt to insert a default no-argument constructor. It will also auto-insert a call to super()
, but Mammal
has at least one constructor, so the compiler does not insert a default no-args constructor. Therefore, it doesn’t compiles.
// the previous example is the same as:
public class Elephant extends Mammal {
public Elephant() {
super(); // DOES NOT COMPILE
}
}
This is fixed by creating explicit constructor with an explicit call to the existing parent’s constructor.
public class Elephant extends Mammal {
public Elephant() {
super(10);
}
}
(!) If an exam question has code regarding inheritance, check that the code compiles before answering a question about if. Be wary of any exam question in which a class defines a constructor that takes arguments and doesn’t define a no-argument constructor. (!)
super() always refers to the most direct parent
A class may have multiple ancestors via inheritance. For constructors, super()
always refers to the most direct parent.
Constructors and final Fields
final static
variables must be assigned a value exactly once. This may happen in the line of declaration and in a static
initializer.
public class MouseHouse {
private final int volume;
private final String name = "The Mouse House";
{
volume = 10;
}
}
They may also be initialized inside a constructor. By the time the constructor completes, all final instance variables must be assigned a value.
public class MouseHouse {
private final int volume;
private final String type;
public MouseHouse() {
this.volume = 10;
type = "happy";
}
}
Unlike local final
variables, which are not required to have a value unless they’re actually used, final
instance variables must be assigned a value. (!) Default values are not used for these variables. (!)
If they’re not assigned a value in the line where they’re declared or in an instance initializer, then they must be assigned a value in the constructor declaration. Failure to do so will result in a compiler error.
(!) On the exam, be wary of any instance variables marked final
. Make sure they’re assigned a value in the line where they’re declared, in an instance initializer, or in a constructor. They should be assigned a value only once, and failure to assign a value is considered a compiler error in the constructor. (!)
Order of Initialization
Class Initialization
First you need to initialize the class, which involves invoking all static
members in the class hierarchy, starting with the highest superclass and working downward. This is referred as loading the class.
The most important rule is that it happens at most once for each class.
Initialize Class X
- If there’s a superclass Y of X, the initialize class Y first.
- Process all
static
variable declarations in the order they appear in the class.
- Process all
static
initializers in the order they appear in the class.
public class Animal {
static { sout("A"); }
}
public class Hippo extends Animal {
static { sout("B"); }
public static void main(String... grass) {
sout("C");
new Hippo();
new Hippo();
new Hippo();
}
}
This will print ABC
, since main()
is inside Hippo
class, the class will be initialized first, starting with its superclass.
A class must be initialized before it’s referenced or used. Also the class containing the program entry point is loaded before the main()
method is executed.
Instance Initialization
An instance is initialized anytime the new
keyword is used. Instance initialization is not the same as class initialization, because a class or superclass may have many constructors declared but only a handful used as part of instance initialization.
- First, start at the lowest level constructor where the
new
keyword is used.
The first line of every constructor is a call this()
or super()
and if omitted, the compiler will automatically insert a call to the parent no-argument constructor super()
.
- Then, progress upward and note the order of constructors.
- Finally, initialize each class starting with the superclass, processing each instance initializer and constructor in the reverse order in which it was called.
Initialize Instance of X
Example 1
- If there’s a superclass Y of X, then initialize the instance of Y first.
- Process all instance variable declarations in the order they appear in the class.
- Process all instance initializers in the order they appear in the class.
- Initialize the constructor including any overloaded constructors referenced with
this()
.
public class ZooTickets {
private String name = "BestZoo";
{ sout(name + "-"); }
private static int COUNT = 0;
static { sout(COUNT + "-"); }
static { COUNT += 10; sout(COUNT+"-"); }
public ZooTickets() {
sout("z-");
}
public static void main(String... patrons) {
new ZooTickets();
}
}
This outputs 0-10-BestZoo-z-
.
- First we have to initialize the class.
Since there’s no superclass declared (which means it’s Object
), we can start with the static
components of ZooTickets
.
- Next, we initialize the instance, so we start with the instance components.
- Finally, we run the constructor.
Example 2
class Primate {
public Primate() {
sout("Primate-");
}
}
class Ape extends Primate {
public Ape(int fur) {
sout("Ape1-");
}
public Ape() {
sout("Ape2-");
}
}
public class Chimpanzee extends Ape {
public Chimpanzee() {
super(2);
sout("Chimpanzee-");
}
public static void main(String... args) {
new Chimpanzee();
}
}
This outputs Primate-Ape1-Chimpanzee-
.
Example 3
public class Cuttlefish {
private String name = "swimmy";
{ sout(name) }
private static int COUNT = 0;
static { sout(COUNT); }
{ COUNT++; sout(COUNT); }
public Cuttlefish() {
sout("Constructor");
}
public static void main(String... args) {
sout("Ready");
new Cuttlefish();
}
}
This outputs the following (wtf?)
0
Ready
swimmy
1
Constructor
- There’s no superclass declared, so we can skip any steps that relate to inheritance.
- We first process
static
variables, and static
initializers.
- Then the
main()
method can run.
- Then goes instance initializers.
- Finally, the constructor.
Example 4
(This example is a bit too harsh, take a look on page 378 of the book to review it again)
class GiraffeFamily {
static { sout("A"); }
{ sout("B"); }
public GiraffeFamily(String name) {
this(1);
sout("C");
}
public GiraffeFamily() {
sout("D");
}
public GiraffeFamily(int stripes) {
sout("E");
}
}
public class Okapi extends GiraffeFamily {
static { sout("F"); }
public Okapi(int stripes) {
super("sugar");
sout("G");
}
{ sout("H"); }
public static void main(String[] grass) {
new Okapi(1);
soutln();
new Okapi(2);
}
}
This outputs
- Start with initializing
Okapi
class. Since it has a superclass, initialize it first printing A
.
- Next initialize
Okapi
, printing F
.
- After the classes are initialized, execute
main()
. The first line creates a new Okapi
object, triggering the instance initialization process. Per the first rule, the superclass instance is initialized first, and within it the instance initializer is called and B
printed.
- Initialize the constructors, which calls the overloaded constructor. This prints
EC
.
- Initialization of the
Okapi
instance itself. This prints HG
.
- Line break
- Initialization of a new
Okapi
object. The order is the same as the previous one, sans the class initialization, so BECHG
is printed again.
Reviewing Constructor Rules
- The first statement of every constructor is a call to an overloaded construcctor via
this()
, or a direct parent constructor via super()
.
- If the first statement of a constructor is not either
this()
or super()
, then the compiler will insert a no-argument super()
as the first statement.
- Calling
this()
and super()
after the first statement of a constructor results in a compiler error.
- If the parent class doesn’t have a no-argument constructor, then every constructor in the child class must start with an explicit
this()
or super()
- If the parent class doesn’t have a no-argument constructor and the child doesn’t define any constructor, then the child class will not compile.
- If a class only defines private constructors, then it cannot be extended by a top-level class.
- All final instance variables must be assigned a value exactly once by the end of the constructor. Any final instance variables not assigned a value will be reported as a compiler error on the line the constructor is declared.
Make sure you understand these rules. The exam will often provide code that breaks one or many of these rules and therefore, doesn’t compile.
Inheriting Members
Calling Inherited Members
Java classes may use any public
or protected
member of the parent class, including methods, primitives or object references. If the parent class and child class are part of the same package, then it may also use any package-private members defined in the parent class. Finally, a child class may never access a private member of the parent class.
Inheriting Methods
Inheriting a class also sets the stage for collisions between methods defined in both the parent and the subclass.
Overriding a Method
If there’s a method defined in both the parent and child classes, with the same signature and you want to define a new version of the method and have it behave differently for that subclass, the solution is to override the method in the child class.
Overriding a method occurs when a subclass declares a new implementation for an inherited method with the same signature (method’s name and parameters) and compatible return type.
When you override a method, you may reference the parent version using super
. In this manner this
and super
allow you to select between the current and parent versions.
public class Canine {
public double getAverageWeight() {
return 50;
}
}
public class Worf extends Canine {
public double getAverageWeight() {
return super.getAverageWeight() + 20;
}
public static void main(String... args) {
sout(new Canine().getAverageWeight()); // 50.0
sout(new Worlf().getAverageWeight()); // 70.0
}
}
If we didn’t use super.
in the previous example, the method would call it itself recursively in a closed loop.
To override a method, the compiler performs the following checks
- The method in the child class, must have the same signature as the parents’
- The method in the child class, must be at least as accessible as the method in the parent class.
- The method in the child class may not declare a checked exception that’s new or broader than the class of any exception declared in the parent.
- If the method returns a value, it must be the same or a subtype of the method in the parent class, known as covariant return types.
Overloading vs Overriding
An overloaded method will use a different list of method parameters. This allows overloaded methods a great deal more freedom in syntax than an overriden method would have.
First Rule - modify return type
public class Bird {
public void fly() {
sout("Bird is flying");
}
public void eat(int food) {
sout("Bird is eating + food + " units of food);
}
}
public class Eagle extends Bird {
public int fly(int height) {
sout("Bird is flying at " + height + " meters");
return height;
}
public int eat(int food) { // DOES NOT COMPILE
sout("Bird is eating " + food + " units of food");
return food;
}
}
The fly()
method is overloaded in Eagle
. Because it’s being overloaded and not overridden, the return type can be changed from void
to int
.
The eat()
method is overridden in Eagle
, since the signature is the same as in its parent class. The new return type must be compatible with the return type from the parent.
(!) Any time you see a method on the exam with the same name as a method in the parent class, determine whether the method is being overloaded or overriden first; doing so will help you with questions about whether the code will compile. (!)
Second Rule - access modifier
public class Camel {
public int getNumberOfHumps() {
return 1;
}
}
public class BactrianCamel extends Camel {
private int getNumberOfHumps() { // DOES NOT COMPILE
return 2;
}
}
This fails because it tries to override the method, but fails because the access modifier private
is more restrictive than the one defined in the parent version.
Third Rule - Checked exceptions
Overriding a method cannot declare new checked exceptions or checked exceptions broader than the inherited method. It may declare though, a checked exception more restricted than the inherited version.
public class Reptile {
protected void sleep() throws IOException {}
protected void exit() throws FileNotFoundException {}
}
public class GalapagosTortoise extends Reptile {
public void sleep() throws FileNotFouncException {} // COMPILES
public void exit() throws IOException {} // DOES NOT COMPILE
}
Fourth Rule - Covariant return type
This is the most complicated, as it requires knowing the relationships between the return types. The overriding method must use a return type that is covariant with the return type of the inherited method.
public class Rhino {
protected CharSequence getName() {
return "rhino";
}
protected String getColor() {
return "grey, black, or white";
}
}
class JavanRhino extends Rhino {
public String getName() {
return "javan rhino";
}
public CharSequence getColor() { // DOES NOT COMPILE
return "grey";
}
}
You should know that String
implemenents CharSequence
interface, making it a subtype. All String
values are CharSequence
values, but not viceversa.
(!) How to check if they’re covariant?: Given an inherited return type A and an overriding return type B, can you assign an instance of B to a reference variable for A without a cast? If so, then they’re covariant. (!)
Overriding a Generic Method
Review of Overloading a Generic Method
You cannot overload methods by changing the generyc type, due to type erasure.
public class LongTailAnimal {
protected void chew(List<Object>) input {}
protected void chew(List<Double>) input {} // DOES NOT COMPILE
}
For the same reason, you also can’t overload a generic method in a parent class.
public class LongTailAnimal {
protected void chew(List<Object> input) {}
}
public class Anteater extends LongTailAnimal {
protected void chew(List<Double> input) {} // DOES NOT COMPILE
}
Both of them fail to compile because of type erasure. When compiled, the generic type is dropped.
Generic Method Parameters
You can override though a method with generic params, but you must match the signature exactly.
public class LongTailAnimal {
protected void chew(List<String> input) {}
}
public class Anteater extends LongTailAnimal {
protected void chew(List<String> input) {}
}
The generic class or interface may change, but then it’s considered overloading, not overriding as the method signature is not the same.
public class LongTailAnimal {
protected void chew(List<String> input) {}
}
public class Anteater extends LongTailAnimal {
protected void chew(ArrayList<String> input) {} // COMPILES
}
Generics and Wilcards
Java includes support for generic wildcards using the question mark ?
character.
void sing1(List<?> v) {} // unbounded wildcard
void sing2(List<? super String> v) {} // lower bounded wildcard
void sing3(List<? extends String> v) {} // upper bounded wildcard
Generic Return Types
When you’re working with overriden methods that return generics, the return values must be covariant. In terms of generics, this means that the return type of the class or interface declared in the overriding method must be a subtype of the class defined in the parent class. The generic param type must match its parent’s type exactly.
public class Mammal {
public List<CharSequence> play() {}
public CharSequence sleep() {}
}
public class Monkey extends Mammal {
public ArrayList<CharSequence> play() {}
}
public class Goat extends Mammal {
public List<String> play() {} // DOES NOT COMPILE
public String sleep() {}
}
Monkey
compiles because ArrayList
is a subtype of List
.
The play()
method in the Goat
class doesn’t compile, because the generic type param must match. Even though String
is a subtype of CharSequence
, it doesn’t exactly match the generic type defined in Mammal
.
Redeclaring private Methods
In Java, you can’t override private
methods since they’re not inherited. Just because a child class doesn’t have access to the parent method doesn’t mean the child class can’t define its own version of the method. It just means, strictly speaking, that the new method is not an overriden version of the parent class’ method.
public class Camel {
private String getNumberOfHumps() {
return "Undefined";
}
}
public class DromedaryCamel extends Camel {
private int getNumberOfHumps() {
return 1;
}
}
The methods are unrelated. If the paren’t method were public
or protected
, the method in the child class wouldn’t compile.
Hiding Static Methods
A hidden method occurs when a child class defines a static method with the same name and signature as an inherited static method defined in a parent class.
It’s not exactly the same as method overriding. The same previous four rules apply here. In addition, a new rule is added:
The method defined in the child class must be marked as static if it’s marked as static in a parent class. If one is marked static and the other is not, the class will not compile.
public class Bear {
public static void eat() {
sout("Bear is eating");
}
}
public class Panda extends Bear {
public static void eat() {
sout("Panda is chewing");
}
}
This compiles and runs. The eat()
method in Panda
class hides the eat()
method in Bear
. Because they’re both marked static, this is not considered an overridden method.
public class Bear {
public static void sneeze() {
sout("Bear is sneezing");
}
public void hibernate() {
sout("Bear is hibernating");
}
public static void laugh() {
sout("Bear is laughing");
}
}
public class Panda extends Bear {
public void sneeze() { // DOES NOT COMPILE
}
public static void hibernate() { // DOES NOT COMPILE
}
protected static void laugh() { // DOES NOT COMPILE
}
}
Creating final Methods
final methods cannot be replaced. By marking a method as final, you forbid a child class from replacing this method. This rule applies both, when you try to override a method and when you hide a method.
public class Bird {
public final boolean hasFeathers() {
return true;
}
public final static void flyAway() {}
}
public class Penguin extends Bird {
public final boolean hasFeathers() { // DOES NOT COMPILE
return false;
}
public final static void flyAway() {} // DOES NOT COMPILE
}
Hiding Variables
Java doesn’t allow variables to be overriden. They can be hidden, though.
A hidden variable occurs when a child class defines a variable with the same name as an inherited variable defined in the parent class.
class Carnivore {
protected boolean hasFur = false;
}
public class Meerkat extends Carnivore {
protected boolean hasFur = true;
public static void main(String... args) {
Meerkat m = new Meerkat() ;
Carnivore c = m;
sout(m.hasFur()); // true
sout(c.hasFur()); // false
}
}
Understanding Polymorphism
Polymorphism is the property of an object to take on many different forms. A Java object may be accessed using a reference with the same type as the object, a reference that’s a superclass of the object, or a reference that defines an interface the object implements, either directly or through a superclass.
Interface Primer
- An interface can define abstract methods.
- A class can implement any number of interfaces.
- A class implements an interface by overriding the inherited abstract methods.
- An object that implements an interface can be assigned to a reference for that interface.
public class Primate {
public boolean hasHair() {
return true;
}
}
public interface HasTail {
public abstract boolean isTailStriped();
}
public class Lemur extends Primate implements HasTail {
public int age = 10;
public boolean isTailStriped() {
return false;
}
public static void main(String[] args) {
Lemur lemur = new Lemur();
sout(lemur.age);
HasTail hasTail = lemur;
sout(hasTail.isTailStriped());
Primate primate = lemur;
sout(primate.hasHair());
}
}
This compiles and outputs
There’s only one object (lemur
) created and referenced. Polymorphism enables an instance of Lemur
to be reassigned or passed to a method using one of its supertypes, such as Primate
or HasTail
.
Once the object has been assigned to a new reference type, only the methods and variables available to the reference type are callable on the object without an explicit cast.
HasTail hasTail = lemur;
sout(hasTail.age); // DOES NOT COMPILE
Primate primate = lemur;
sout(primate.isTailStriped()); // DOES NOT COMPILE
In this example, hasTail
has direct access only to methods defined in HasTail
interface.
Object vs Reference
In Java, all objects are accessed by reference. You should consider the object as the entity that exists in memory, allocated by the Java runtime environment. Regardless of the type of the reference you have for the object in memory, the object itself doesn’t change.
Since all objects inherit from Object
, they can all be reassigned to it.
Lemur lemur = new Lemur();
Object lemurAsObject = lemur;
The object itself doesn’t change and still exists as a Lemur
object in memory. What has changed though, is our ability to access methods within the Lemur
class with the lemurAsObject
reference. Without an explicit cast back to Lemur
we no longer have access to the Lemur
properties of the object.
- The type of the object determines which properties exist within the object in memory.
- The type of the reference to the object determines which methods and variables are accessible to the Java program.
Casting Objects
Once we change the reference type, we loose access to more specific members defined in the subclass that still exist within the object. We can reclaim those references by casting the object back to the specific subclass it came from.
Primate primate = new Lemur(); // implicit cast
Lemur lemur2 = primate; // DOES NOT COMPILE
Lemur lemur3 = (Lemur) primate;
sout(lemur3.age);
If the underlying object is not compatible with the type, then a ClassCastException
will be thrown at runtime.
- Casting a reference from subtype to a supertype doesn’t require an explicit cast.
- Casting a reference from a supertype to a subtype requires an explicit cast
- The compiler disallows cast to an unrelated class.
- At runtime, an invalid cast of a reference to an unrelated type results in
ClassCastException
(!) The exam may trick you with a cast that the compiler doesn’t allow. For example (!)
public class Bird {}
public class Fish {
public static void main(String[] args) {
Fish fish = new Fish();
Bird bird = (Bird) fish; // DOES NOT COMPILE
}
}
Casting is not without limitations.
public class Rodent {}
public class Capybara extends Rodent {
public static void main(String[] args) {
Rodent rodent = new Rodent();
Capybara capybara = (Capybara) rodent; // ClassCastException
}
}
Keep in mind the Rodent
object created does not inherit the Capybara
class in any way.
(!) When reviewing a question that involves casting and polymorphism, be sure to remember what the instance of the object actually is. Then, focus on whether the compiler will allow the object to be referenced with or without explicit casts. (!)
The instanceof Operator
It can be used to check whether an object belongs to a particular class or interface and to prevent ClassCastExceptions
at runtime.
if(rodent instanceof Capybara) {
Capybara capybara = (Capybara) rodent;
}
Just as the compiler doesn’t allow casting an object to unrelated types, it also doesn’t allow instanceof
to be used with unrelated types.
Fish fish = new Fish();
if(fish instanceof Bird) { // DOES NOT COMPILE
}
Polymorphism and Method Overriding
Polymorphism states that when you override a method, you replace all calls to it, even those defind in the parent class.
class Penguin {
public int getHeight() { return 3; }
public void printInfo() {
sout(this.getHeight());
}
}
public class EmperorPenguin extends Penguin {
public int getHeight() { return 8; }
public static void main(String[] fish) {
new EmperorPenguin().printInfo(); // prints 8
}
}
The getHeight()
method is overridden in the subclass, meaning all calls to it are replaced at runtime. Despite printInfo()
being defined in the Penguin
class, calling getHeight()
on the object calls the method associated with the precise object in memory, not the current reference type where it’s called.
Remember, you can choose to limit polymorphic behaviour by marking methods final
, which prevents them from being overriden by a subclass.
class Penguin {
public int getHeight() { return 3; }
public void printInfo() {
sout(this.getHeight());
}
}
public class EmperorPenguin extends Penguin {
public int getHeight() { return 8; }
public void printInfo() {
sout(super.getHeight());
}
public static void main(String[] fish) {
new EmperorPenguin().printInfo(); // prints 3!
}
}
Overriding vs Hiding Members
Overriding replaces the method everywhere it’s called. static method and variable hiding does not. Hiding members is not a form of polymorphism since the methods and variables maintain their individual properties.
Hiding members is very sensitive to the reference type and location where the member is being used.
class Penguin {
public static int getHeight() { return 3; }
public void printInfo() {
sout(this.getHeight());
}
}
public class CrestedPenguin extends Penguin {
public static int getHeight() { return 8; }
public static void main(String[] fish) {
new CrestedPenguin().printInfo(); // prints 3
}
}
The printInfo()
method is hidden, not overriden. Calling getHeight()
in CrestedPenguin
returns a different value than calling it in Penguin
, even if the underlying object is the same.
This constrasts with overriding a method, where it returns the same value for an object regardless of which class it’s called in.
(!) It’s permitted to use an instance reference to access a static variable or method. It’s discouraged though, but it works. (!)
class Marsupial {
protected int age = 2;
public static boolean isBiped() {
return false;
}
}
public class Kangaroo extends Marsupial {
protected int age = 6;
public static boolean isBiped() {
return true;
}
public static void main(String[] args) {
Kangaroo k = new Kangaroo();
Marsupial m = k;
sout(k.isBiped());
sout(m.isBiped());
sout(k.age);
sout(m.age);
}
}
This outputs
In this example, only one object, of type Kangaroo
is created and stored in memory. Since static methods can only be hidden and not overriden, Java uses the reference type to determine which version of isBiped()
should be called, resulting in k.isBiped()
printing true
and m.isBiped()
printing false
.
Likewise, the age
variable is hidden, not overriden, so the reference type is used to determine which value to output.
Summary
This chapter took the basic class structures we’ve presented throughout the book and expanded them by introducing the notion of inheritance. Java classes follow a multilevel single-inheritance pattern in which every class has exactly one direct parent class, with all classes eventually inheriting from java.lang.Object.
Inheriting a class gives you access to all of the public and protected members of the
class. It also gives you access to package-private members of the class if the classes are in the same package. All instance methods, constructors, and instance initializers have access to two special reference variables: this and super. Both this and super provide access to all inherited members, with only this providing access to all members in the current class declaration.
Constructors are special methods that use the class name and do not have a return type. They are used to instantiate new objects. Declaring constructors requires following a number of important rules. If no constructor is provided, the compiler will automatically insert a default no-argument constructor in the class. The first line of every constructor is a call to an overloaded constructor, this(), or a parent constructor, super(); otherwise, the compiler will insert a call to super() as the first line of the constructor. In some cases, such as if the parent class does not define a no-argument constructor, this can lead to compilation errors. Pay close attention on the exam to any class that defines a constructor with arguments and doesn’t define a no-argument constructor.
Classes are initialized in a predetermined order: superclass initialization; static variables and static initializers in the order that they appear; instance variables and instance
initializers in the order they appear; and finally, the constructor. All final instance variables must be assigned a value exactly once. If by the time a constructor finishes, a final instance variable is not assigned a value, then the constructor will not compile.
We reviewed overloaded, overridden, hidden, and redeclared methods and showed how they differ, especially in terms of polymorphism. A method is overloaded if it has the same name but a different signature as another accessible method. A method is overridden if it has the same signature as an inherited method, with access modifiers, exceptions, and a return type that are compatible. A static method is hidden if it has the same signature as an inherited static method. Finally, a method is redeclared if it has the same name and possibly the same signature as an uninherited method.
We also introduced the notion of hiding variables, although we strongly discourage this in practice as it often leads to confusing, difficult-to-maintain code.
Finally, this chapter introduced the concept of polymorphism, central to the Java language, and showed how objects can be accessed in a variety of forms. Make sure you understand when casts are needed for accessing objects, and be able to spot the difference between compile-time and runtime cast problems.
Exam Essentials
Be able to write code that extends other classes. A Java class that extends another class inherits all of its public and protected methods and variables. If the class is in the same package, it also inherits all package-private members of the class. Classes that are marked final cannot be extended. Finally, all classes in Java extend java.lang.Object either directly or from a superclass.
Be able to distinguish and make use of this, this(), super, and super(). To access a current or inherited member of a class, the this reference can be used. To access an inherited member, the super reference can be used. The super reference is often used to reduce ambiguity, such as when a class reuses the name of an inherited method or variable. The calls to this() and super() are used to access constructors in the same class and parent class, respectively.
Evaluate code involving constructors. The first line of every constructor is a call to another constructor within the class using this() or a call to a constructor of the parent class using the super() call. The compiler will insert a call to super() if no constructor call is declared. If the parent class doesn’t contain a no-argument constructor, an explicit call to the parent constructor must be provided. Be able to recognize when the default constructor isprovided. Remember that the order of initialization is to initialize all classes in the class hierarchy, starting with the superclass. Then, the instances are initialized, again starting with the superclass. All final variables must be assigned a value exactly once by the time the constructor is finished.
Understand the rules for method overriding. Java allows methods to be overridden, or replaced, by a subclass if certain rules are followed: a method must have the same signature, be at least as accessible as the parent method, must not declare any new or broader exceptions, and must use covariant return types. The generic parameter types must exactly match in any of the generic method arguments or a generic return type. Methods marked final may not be overridden or hidden.
Understand the rules for hiding methods and variables. When a static method is
overridden in a subclass, it is referred to as method hiding. Likewise, variable hiding is
when an inherited variable name is reused in a subclass. In both situations, the original method or variable still exists and is accessible depending on where it is accessed and the reference type used. For method hiding, the use of static in the method declaration must be the same between the parent and child class. Finally, variable and method hiding should generally be avoided since it leads to confusing and difficult-to-follow code.
Recognize the difference between method overriding and method overloading. Both method overloading and overriding involve creating a new method with the same name as an existing method. When the method signature is the same, it is referred to as method overriding and must follow a specific set of override rules to compile. When the method signature is different, with the method taking different inputs, it is referred to as method overloading, and none of the override rules are required. Method overriding is important to polymorphism because it replaces all calls to the method, even those made in a superclass.
Understand polymorphism. An object may take on a variety of forms, referred to as polymorphism. The object is viewed as existing in memory in one concrete form but is
accessible in many forms through reference variables. Changing the reference type of an object may grant access to new members, but the members always exist in memory.
Recognize valid reference casting. An instance can be automatically cast to a superclass or interface reference without an explicit cast. Alternatively, an explicit cast is required if the reference is being narrowed to a subclass of the object. The Java compiler doesn’t permit casting to unrelated class types. Be able to discern between compiler-time casting errors and those that will not occur until runtime and that throw a ClassCastException.