Java provides a standard class library consisting of thousands of classes and other reference types. Despite the disparity in their capabilities, these types form one massive inheritance hierarchy by directly or indirectly extending the Object
class. This is also true for any classes and other reference types that you create.
The first half of this tutorial on Java inheritance showed you the basics of inheritance, specifically how to use Java's extends
and super
keywords to derive a child class from a parent class, invoke parent class constructors and methods, override methods, and more. Now, we'll turn our focus to the mothership of the Java class inheritance hierarchy, java.lang.Object
.
Studying Object
and its methods will help you gain a more functional understanding of inheritance and how it works in your Java programs. Being familiar with those methods will help you make more sense of Java programs, generally.
Object: Java's superclass
Object
is the root class, or ultimate superclass, of all other Java classes. Stored in the java.lang
package, Object
declares the following methods, which all other classes inherit:
protected Object clone()
boolean equals(Object obj)
protected void finalize()
Class<?> getClass()
int hashCode()
void notify()
void notifyAll()
String toString()
void wait()
void wait(long timeout)
void wait(long timeout, int nanos)
A Java class inherits these methods and can override any method that's not declared final
. For example, the non-final
toString()
method can be overridden, whereas the final
wait()
methods cannot.
We'll look at each of these methods and how they enable you to perform special tasks in the context of your Java classes. First, let's consider the basic rules and mechanisms for Object
inheritance.
Extending Object: An example
A class can explicitly extend Object
, as demonstrated in Listing 1.
Listing 1. Explicitly extending Object
public class Employee extends Object
{
private String name;
public Employee(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public static void main(String[] args)
{
Employee emp = new Employee("John Doe");
System.out.println(emp.getName());
}
}
Because you can extend at most one other class (recall from Part 1 that Java doesn't support class-based multiple inheritance), you're not forced to explicitly extend Object
; otherwise, you couldn't extend any other class. Therefore, you would extend Object
implicitly, as demonstrated in Listing 2.
Listing 2. Implicitly extending Object
public class Employee
{
private String name;
public Employee(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public static void main(String[] args)
{
Employee emp = new Employee("John Doe");
System.out.println(emp.getName());
}
}
Compile Listing 1 or Listing 2 as follows:
javac Employee.java
Run the resulting application:
java Employee
You should observe the following output:
John Doe
Find out about a class: getClass()
The getClass()
method returns the runtime class of any object on which it is called. The runtime class is represented by a Class
object, which is found in the java.lang
package. Class
is the entry point into the Java Reflection API, which you'll learn about when we get into more advanced topics in Java programming. For now, know that a Java application uses Class
and the rest of the Java Reflection API to learn about its own structure.
Duplicating objects: clone()
The clone()
method creates and returns a copy of the object on which it's called. Because clone()
's return type is Object
, the object reference that clone()
returns must be cast to the object's actual type before assigning that reference to a variable of the object's type. Listing 3 presents an application that demonstrates cloning.
Listing 3. Cloning an object
class CloneDemo implements Cloneable
{
int x;
public static void main(String[] args) throws CloneNotSupportedException
{
CloneDemo cd = new CloneDemo();
cd.x = 5;
System.out.println("cd.x = " + cd.x);
CloneDemo cd2 = (CloneDemo) cd.clone();
System.out.println("cd2.x = " + cd2.x);
}
}
Listing 3's CloneDemo
class implements the Cloneable
interface, which is found in the java.lang
package. Cloneable
is implemented by the class (via the implements
keyword) to prevent Object
's clone()
method from throwing an instance of the CloneNotSupportedException
class (also found in java.lang
).
CloneDemo
declares a single int
-based instance field named x
and a main()
method that exercises this class. main()
is declared with a throws
clause that passes CloneNotSupportedException
up the method-call stack.
main()
first instantiates CloneDemo
and initializes the resulting instance's copy of x
to 5
. It then outputs the instance's x
value and calls clone()
on this instance, casting the returned object to CloneDemo
before storing its reference. Finally, it outputs the clone's x
field value.
Compile Listing 3 (javac CloneDemo.java
) and run the application (java CloneDemo
). You should observe the following output:
cd.x = 5
cd2.x = 5
Overriding clone()
The previous example didn't need to override clone()
because the code that calls clone()
is located in the class being cloned (CloneDemo
). If the call to clone()
were located in a different class, however, then you would need to override clone()
. Because clone()
is declared protected
, you would receive a "clone has protected access in Object" message if you didn't override it before compiling the class. Listing 4 presents a refactored Listing 3 that demonstrates overriding clone()
.
Listing 4. Cloning an object from another class
class Data implements Cloneable
{
int x;
@Override
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}
class CloneDemo
{
public static void main(String[] args) throws CloneNotSupportedException
{
Data data = new Data();
data.x = 5;
System.out.println("data.x = " + data.x);
Data data2 = (Data) data.clone();
System.out.println("data2.x = " + data2.x);
}
}
Listing 4 declares a Data
class whose instances are to be cloned. Data
implements the Cloneable
interface to prevent a CloneNotSupportedException
from being thrown when the clone()
method is called. It then declares int
-based instance field x
, and overrides the clone()
method. The clone()
method executes super.clone()
to call its superclass's (that is, Object
's) clone()
method. The overriding clone()
method identifies CloneNotSupportedException
in its throws
clause.
Listing 4 also declares a CloneDemo
class that: instantiates Data
, initializes its instance field, outputs the value of the instance field, clones the Data
object, and outputs its instance field value.
Compile Listing 4 (javac CloneDemo.java
) and run the application (java CloneDemo
). You should observe the following output:
data.x = 5
data2.x = 5
Shallow cloning
Shallow cloning (also known as shallow copying) refers to duplicating an object's fields without duplicating any objects that are referenced from that object's reference fields (if there are any reference fields). Listings 3 and 4 actually demonstrated shallow cloning. Each of the cd
-, cd2
-, data
-, and data2
-referenced fields identifies an object that has its own copy of the int
-based x
field.
Shallow cloning works well when all fields are of the primitive type and (in many cases) when any reference fields refer to immutable (unchangeable) objects. However, if any referenced objects are mutable, changes made to any one of these objects can be seen by the original object and its clone(s). Listing 5 demonstrates.
Listing 5. The problem with shallow cloning in a reference field context
class Employee implements Cloneable
{
private String name;
private int age;
private Address address;
Employee(String name, int age, Address address)
{
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
Address getAddress()
{
return address;
}
String getName()
{
return name;
}
int getAge()
{
return age;
}
}
class Address
{
private String city;
Address(String city)
{
this.city = city;
}
String getCity()
{
return city;
}
void setCity(String city)
{
this.city = city;
}
}
class CloneDemo
{
public static void main(String[] args) throws CloneNotSupportedException
{
Employee e = new Employee("John Doe", 49, new Address("Denver"));
System.out.println(e.getName() + ": " + e.getAge() + ": " +
e.getAddress().getCity());
Employee e2 = (Employee) e.clone();
System.out.println(e2.getName() + ": " + e2.getAge() + ": " +
e2.getAddress().getCity());
e.getAddress().setCity("Chicago");
System.out.println(e.getName() + ": " + e.getAge() + ": " +
e.getAddress().getCity());
System.out.println(e2.getName() + ": " + e2.getAge() + ": " +
e2.getAddress().getCity());
}
}
Listing 5 presents Employee
, Address
, and CloneDemo
classes. Employee
declares name
, age
, and address
fields; and is cloneable. Address
declares an address consisting of a city and its instances are mutable. CloneDemo
drives the application.
CloneDemo
's main()
method creates an Employee
object and clones this object. It then changes the city's name in the original Employee
object's address
field. Because both Employee
objects reference the same Address
object, the changed city is seen by both objects.
Compile Listing 5 (javac CloneDemo.java
) and run this application (java CloneDemo
). You should observe the following output:
John Doe: 49: Denver
John Doe: 49: Denver
John Doe: 49: Chicago
John Doe: 49: Chicago
Deep cloning
Deep cloning (also known as deep copying) refers to duplicating an object's fields such that any referenced objects are duplicated. Furthermore, the referenced objects of referenced objects are duplicated, and so forth. Listing 6 refactors Listing 5 to demonstrate deep cloning.
Listing 6. Deep cloning the address field
class Employee implements Cloneable
{
private String name;
private int age;
private Address address;
Employee(String name, int age, Address address)
{
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException
{
Employee e = (Employee) super.clone();
e.address = (Address) address.clone();
return e;
}
Address getAddress()
{
return address;
}
String getName()
{
return name;
}
int getAge()
{
return age;
}
}
class Address
{
private String city;
Address(String city)
{
this.city = city;
}
@Override
public Object clone()
{
return new Address(new String(city));
}
String getCity()
{
return city;
}
void setCity(String city)
{
this.city = city;
}
}
class CloneDemo
{
public static void main(String[] args) throws CloneNotSupportedException
{
Employee e = new Employee("John Doe", 49, new Address("Denver"));
System.out.println(e.getName() + ": " + e.getAge() + ": " +
e.getAddress().getCity());
Employee e2 = (Employee) e.clone();
System.out.println(e2.getName() + ": " + e2.getAge() + ": " +
e2.getAddress().getCity());
e.getAddress().setCity("Chicago");
System.out.println(e.getName() + ": " + e.getAge() + ": " +
e.getAddress().getCity());
System.out.println(e2.getName() + ": " + e2.getAge() + ": " +
e2.getAddress().getCity());
}
}
Listing 6 shows that Employee
's clone()
method first calls super.clone()
, which shallowly copies the name
, age
, and address
fields. It then calls clone()
on the address
field to make a duplicate of the referenced Address
object. Address
overrides the clone()
method and reveals a few differences from previous classes that override this method:
Address
doesn't implementCloneable
. It's not necessary because onlyObject
'sclone()
method requires that a class implement this interface, and thisclone()
method isn't being called.- The overriding
clone()
method doesn't throwCloneNotSupportedException
. This exception is thrown only fromObject
'sclone()
method, which isn't called. Therefore, the exception doesn't have to be handled or passed up the method-call stack via a throws clause. Object
'sclone()
method isn't called (there's nosuper.clone()
call) because shallow copying isn't required for theAddress
class -- there's only a single field to copy.