Inheritance in Java, Part 2: Object and its methods

how-to
Jun 04, 202423 mins
JavaProgramming LanguagesSoftware Development

The more familiar you are with Object and its methods, the more you can do with your Java programs.

Russian dolls
Credit: sethislav/Shutterstock

The first half of this tutorial introduced the basics of inheritance in Java. You learned 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 turn our focus to the mothership of the Java class inheritance hierarchy, java.lang.Object.

What you’ll learn in this Java tutorial

Studying Object and its methods will give you a more functional understanding of Java inheritance and how it works in your programs:

  • All about Object: Java’s superclass
  • How to extend Object: An example
  • What Java’s getClass() method does
  • What Java’s clone() method does
  • What Java’s equals() method does
  • What Java’s finalize() method does
  • What Java’s hashCode() method does
  • What Java’s toString() method does
  • What Java’s wait() and notify() methods do
download
Download the source code for example applications in this tutorial. Created by Jeff Friesen.

All about 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 you can use them to perform special tasks in the context of your Java classes. First, let’s consider the basic rules and mechanisms for Object inheritance.

How to extend 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

What Java’s getClass() does

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 also the entry point to the Java Reflection API, which a Java application uses to learn about its own structure.

What Java’s clone() method does

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. The code in Listing 3 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()

We didn’t need to override clone() in the previous example 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, 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 is a refactored version of 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 implement Cloneable. It’s not necessary because only Object‘s clone() method requires that a class implement this interface, and this clone() method isn’t being called.
  • The overriding clone() method doesn’t throw CloneNotSupportedException. This exception is thrown only from Object‘s clone() 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‘s clone() method isn’t called (there’s no super.clone() call) because shallow copying isn’t required for the Address class—there’s only a single field to copy.

To clone the Address object, it suffices to create a new Address object and initialize it to a duplicate of the object referenced from the city field. The new Address object is then returned.

Compile Listing 6 (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: Denver

Cloning arrays

Array types have access to the clone() method, which lets you shallowly clone an array. When used in an array context, you don’t have to cast clone()‘s return value to the array type. Listing 7 demonstrates array cloning.

Listing 7. Shallowly cloning a pair of arrays

class City
{
   private String name;

   City(String name)
   {
      this.name = name;
   }

   String getName()
   {
      return name;
   }

   void setName(String name)
   {
      this.name = name;
   }
}

class CloneDemo
{
   public static void main(String[] args)
   {
      double[] temps = { 98.6, 32.0, 100.0, 212.0, 53.5 };
      for (int i = 0; i < temps.length; i++)
         System.out.print(temps[i] + " ");
      System.out.println();
      double[] temps2 = temps.clone();
      for (int i = 0; i < temps2.length; i++)
         System.out.print(temps2[i] + " ");
      System.out.println();

      System.out.println();

      City[] cities = { new City("Denver"), new City("Chicago") };
      for (int i = 0; i < cities.length; i++)
         System.out.print(cities[i].getName() + " ");
      System.out.println();
      City[] cities2 = cities.clone();
      for (int i = 0; i < cities2.length; i++)
         System.out.print(cities2[i].getName() + " ");
      System.out.println();

      cities[0].setName("Dallas");
      for (int i = 0; i < cities2.length; i++)
         System.out.print(cities2[i].getName() + " ");
      System.out.println();
   }
}

Listing 7 declares a City class that stores the name and (eventually) other details about a city, such as its population. The CloneDemo class provides a main() method to demonstrate array cloning.

main() first declares an array of double-precision floating-point values that denote temperatures. After outputting this array’s values, it clones the array—note the absence of a cast operator. Next, it outputs the clone’s identical temperature values.

Continuing, main() creates an array of City objects, outputs the city names, clones this array, and outputs the cloned array’s city names. As a proof that shallow cloning was used, note that main() changes the name of the first City object in the original array and then outputs all of the city names in the second array. The second array reflects the changed name.

Compile Listing 7 (javac CloneDemo.java) and run this application (java CloneDemo). You should observe the following output:

98.6 32.0 100.0 212.0 53.5
98.6 32.0 100.0 212.0 53.5

Denver Chicago
Denver Chicago
Dallas Chicago

What Java’s equals() method does

The equals() method lets you compare the contents of two objects to see if they are equal. This form of equality is known as content equality.

Although the == operator compares two primitive values for content equality, it doesn’t work the way you might expect (for performance reasons) when used in an object-comparison context. In this context, == compares two object references to determine whether or not they refer to the same object. This form of equality is known as reference equality.

Object‘s implementation of the equals() method compares the reference of the object on which equals() is called with the reference passed as an argument to the method. In other words, the default implementation of equals() performs a reference equality check. If the two references are the same, equals() returns true; otherwise, it returns false.

You must override equals() to perform content equality. The rules for overriding this method are stated in Oracle’s official documentation for the Object class. They’re worth repeating below:

  • Be reflexive: For any non-null reference value x, x.equals(x) should return true.
  • Be symmetric: For any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • Be transitive: For any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • Be consistent: For any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.

Additionally, for any non-null reference value x, x.equals(null) should return false.

Content equality

The small application in Listing 8 demonstrates how to properly override equals() to perform content equality.

Listing 8. Comparing objects for content equality

class Employee
{
   private String name;
   private int age;

   Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Employee))
         return false;

      Employee e = (Employee) o;
      return e.getName().equals(name) && e.getAge() == age;
   }

   String getName()
   {
      return name;
   }

   int getAge()
   {
      return age;
   }
}

class EqualityDemo
{
   public static void main(String[] args)
   {
      Employee e1 = new Employee("John Doe", 29);
      Employee e2 = new Employee("Jane Doe", 33);
      Employee e3 = new Employee("John Doe", 29);
      Employee e4 = new Employee("John Doe", 27 + 2);
      // Demonstrate reflexivity.
      System.out.println("Demonstrating reflexivity...");
      System.out.println();
      System.out.println("e1.equals(e1): " + e1.equals(e1));
      System.out.println();
      // Demonstrate symmetry.
      System.out.println("Demonstrating symmetry...");
      System.out.println();
      System.out.println("e1.equals(e2): " + e1.equals(e2));
      System.out.println("e2.equals(e1): " + e2.equals(e1));
      System.out.println("e1.equals(e3): " + e1.equals(e3));
      System.out.println("e3.equals(e1): " + e3.equals(e1));
      System.out.println("e2.equals(e3): " + e2.equals(e3));
      System.out.println("e3.equals(e2): " + e3.equals(e2));
      System.out.println();
      // Demonstrate transitivity.
      System.out.println("Demonstrating transitivity...");
      System.out.println();
      System.out.println("e1.equals(e3): " + e1.equals(e3));
      System.out.println("e3.equals(e4): " + e3.equals(e4));
      System.out.println("e1.equals(e4): " + e1.equals(e4));
      System.out.println();
      // Demonstrate consistency.
      System.out.println("Demonstrating consistency...");
      System.out.println();
      for (int i = 0; i < 5; i++)
      {
         System.out.println("e1.equals(e2): " + e1.equals(e2));
         System.out.println("e1.equals(e3): " + e1.equals(e3));
      }
      System.out.println();
      // Demonstrate the null check.
      System.out.println("Demonstrating null check...");
      System.out.println();
      System.out.println("e1.equals(null): " + e1.equals(null));
   }
}

Listing 8 declares an Employee class that describes employees as combinations of names and ages. This class also overrides equals() to properly compare two Employee objects.

The equals() method first verifies that an Employee object has been passed. If not, it returns false. This check relies on the instanceof operator, which also evaluates to false when null is passed as an argument. Note that doing this satisfies the final rule above: “for any non-null reference value x, x.equals(null) should return false.”

Continuing, the object argument is cast to Employee. You don’t have to worry about a possible ClassCastException because the previous instanceof test guarantees that the argument has Employee type. Following the cast, the two name fields are compared, which relies on String‘s equals() method, and the two age fields are compared.

Compile Listing 8 (javac EqualityDemo.java) and run the application (java EqualityDemo). You should observe the following output:


Demonstrating reflexivity...

e1.equals(e1): true

Demonstrating symmetry...

e1.equals(e2): false
e2.equals(e1): false
e1.equals(e3): true
e3.equals(e1): true
e2.equals(e3): false
e3.equals(e2): false

Demonstrating transitivity...

e1.equals(e3): true
e3.equals(e4): true
e1.equals(e4): true

Demonstrating consistency...

e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true

Demonstrating null check...

e1.equals(null): false

A problem with equals()

Listing 8’s equals() method is problematic when Employee is subclassed, perhaps by a SalesRep class, which adds a String-based region field and overrides equals() to also take this field into account. Assume that you’ve created Employee and SalesRep objects with the same names and ages. However, you’ve also added a specific region to SalesRep.

Suppose you invoke equals() on the Employee object and pass the SalesRep object to this method. Because SalesRep is a kind of Employee, the instanceof check passes and the rest of equals(), which compares the name and age fields, executes. Because each object has identical name and age values, equals() returns true. This return value is correct when you consider that an Employee object is being compared with the Employee part of a SalesRep object. However, it’s incorrect when you consider an Employee object being compared with a SalesRep object in its entirety.

Now suppose that you invoke equals() on the SalesRep object and pass the Employee object to this method. Because Employee is not a kind of SalesRep (otherwise, you could access an Employee object’s nonexistent region field and crash the JVM), instanceof causes equals() to return false. Because equals() returns true in one context and false in another, symmetry is violated.

Joshua Bloch points out in Item 7 of the first edition of his Effective Java Programming Language Guide book that there’s no way to extend an instantiable class (such as Employee) and add an aspect (think field, such as region) while preserving the contract of the equals() method. Although it’s possible to solve the symmetry violation, this solution comes at the price of violating transitivity. Bloch points out that the workaround to this mess is to favor composition over inheritance: Instead of having SalesRep extend Employee, SalesRep would introduce a private Employee field. (See my Java tip on when to use composition versus inheritance to learn more about using composition in Java programs.)

Calling equals() on an array

You can call equals() on an array object reference, as shown in Listing 9, but you shouldn’t. Because equals() performs reference equality in an array context, and because equals() cannot be overridden in this context, this capability isn’t useful.

Listing 9. An attempt to compare arrays

class EqualityDemo
{
   public static void main(String[] args)
   {
      int x[] = { 1, 2, 3 };
      int y[] = { 1, 2, 3 };

      System.out.println("x.equals(x): " + x.equals(x));
      System.out.println("x.equals(y): " + x.equals(y));
   }
}

Listing 9’s main() method declares a pair of arrays with identical types and contents. It then attempts to compare the first array with itself and the first array with the second array. However, because of reference equality, only the array object references are being compared; the contents are not compared. Therefore, x.equals(x) returns true (because of reflexivity—an object is equal to itself), but x.equals(y) returns false.

Compile Listing 9 (javac EqualityDemo.java) and run the application (java EqualityDemo). You should observe the following output:

x.equals(x): true
x.equals(y): false

What Java’s finalize() method does

Suppose you’ve created a Truck object and have assigned its reference to Truck variable t. Now suppose that this reference is the only reference to that object (that is, you haven’t assigned t‘s reference to another variable). By assigning null to t, as in t = null;, you remove the only reference to the recently created t object, and make the object available for garbage collection.

The finalize() method lets an object whose class overrides this method (which is known as a finalizer) perform cleanup tasks when called by the garbage collector. This cleanup activity is known as finalization.

The default finalize() method does nothing; it returns when called. You must provide code that performs some kind of cleanup task. Here’s the pattern for finalize():

class someClass
{
   // ...

   @Override
   protected void finalize() throws Throwable
   {
      try
      {
         // Finalize the subclass state.
         // ...
      }
      finally
      {
         super.finalize();
      }
   }
}

Because finalize() can execute arbitrary code, it’s capable of throwing an exception. Because all exception classes ultimately derive from Throwable (in the java.lang package), Object declares finalize() with a throws Throwable clause appended to its method header.

Finalizing superclasses

It is also possible for a superclass to have a finalize() method that must be called. In these cases, we can use a tryfinally construct within finalize() to execute the finalization code. The finally block ensures that the superclass’s finalize() method will be called, regardless of whether finalize() throws an exception.

When finalize() throws an exception, the exception is ignored. Finalization of the object terminates, which can leave the object in a corrupt state. If another thread (i.e., path of execution) tries to use this object, the resulting behavior will be nondeterministic.

The finalize() method is never called more than once by the JVM for any given object. If you choose to resurrect an object by making it reachable to application code (such as assigning its reference to a static field), finalize() will not be called a second time when the object becomes unreachable (i.e., eligible for garbage collection).

What Java’s hashCode() method does

The hashCode() method returns a hash code (the value returned from a hash—scrambling—function) for the object on which this method is called. This method is used by hash-based Java collection classes, such as the java.util package’s HashMap, HashSet, and Hashtable classes to ensure that objects are properly stored and retrieved.

You typically override hashCode() when also overriding equals() in your classes, to ensure that objects instantiated from these classes work properly with all hash-based collections. This is a good habit to get into, even when your objects won’t be stored in hash-based collections.

The JDK documentation for Object‘s hashCode() method presents a general contract that must be followed by an overriding hashCode() method:

  • Whenever hashCode() is invoked on the same object more than once during an execution of a Java application, hashCode() must consistently return the same integer, provided no information used in equals() comparisons on the object is modified. However, this integer doesn’t need to remain consistent from one execution of an application to another.
  • When two objects are equal according to the overriding equals() method, calling hashCode() on each of the two objects must produce the same integer result.
  • When two objects are unequal according to the overriding equals() method, the integers returned from calling hashCode() on these objects can be identical. However, having hashCode() return distinct values for unequal objects may improve hashtable performance.

What Java’s toString() method does

The toString() method returns a string representation of the object on which this method is called. The returned string is useful for debugging purposes. Consider Listing 10.

Listing 10. Returning a default string representation

class Employee
{
   private String name;
   private int age;

   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }
}

Listing 10 presents an Employee class that doesn’t override toString(). When this method isn’t overridden, the string representation is returned in the format classname@hashcode, where hashcode is shown in hexadecimal notation.

Suppose you were to execute the following code fragment, which instantiates Employee, calls the object’s toString() method, and outputs the returned string:

Employee e = new Employee("Jane Doe", 21);
System.out.println(e.toString());

You might observe the following output:

Employee@1c7b0f4d

Listing 11 augments this class with an overriding toString() method.

Listing 11. Returning a non-default string representation

class Employee
{
   private String name;
   private int age;
   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }
   @Override
   public String toString()
   {
      return name + ": " + age;
   }
}

Listing 11’s overriding toString() method returns a string consisting of a colon-separated name and age. Executing the previous code fragment would result in the following output:

Jane Doe: 21

What Java’s wait() and notify() methods do

Object‘s three wait() methods and its notify() and notifyAll() methods are used by threads to coordinate their actions. For example, one thread might produce an item that another thread consumes. The producing thread should not produce an item before the previously produced item is consumed; instead, it should wait until it’s notified that the item was consumed. Similarly, the consuming thread should not attempt to consume a non-existent item; instead, it should wait until it’s notified that the item is produced. The wait() and notify() methods support this coordination.

Conclusion

The Object class is an important part of classes and other reference types. Every class inherits Object‘s methods and can override some of them. Being familiar with Object and its methods is foundational for understanding the Java class hierarchy. It should also help you make more sense of Java source code, or at least the code won’t look quite so strange.