The Object class

The Object class

All you need to know about the Object class

Written by: saeed1907057

Sun, 17 Nov 2024

next image

Here, we will be discussing about the Object class with its commonly used methods.

Introduction:

  1. This is available in java.lang package.
  2. This is the direct or indirect superclass of all the classes (built-in and user defined) in Java.
  3. A variable of Object class can hold object of any classes (since superclass). Example:
    Object greet = "Hello";
    Object bye = new StringBuilder("Bye");
    

Methods of the Object class:

There are 9 methods available in Object class. We can categorize them based on customization.

  1. Final methods:

    • The methods are getClass(), notify(), notifyAll(), wait().
    • These are defined as finalmethod, so we can’t override them.
  2. Non-final methods:

    • The methods are toString(), equals() ,hashCode(), clone(), finalize()
    • We can customize by overriding them.
    • These methods have a default implementation, and we can customize.

Let us explain those methods one by one based on commonly used. We will be using these two classes as the base for some of our methods.

// located in package z_medium
public class Medium {

    static class Point{
        final int x;
        final int y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    static class Circle {
        final Point center;
        final float radius;

        public Circle(Point center, float radius) {
            this.center = center;
            this.radius = radius;
        }

    }

    public static void main(String[] args) {
        
    }

}
  1. toString()

    • It is mainly used for debugging purpose.
    • Whenever String representation is needed of an object, the toString() method is called automatically.
    • System.out.println(circle); is actually interpreted as System.out.println(circle.toString());
    • "Result: "+circle is interpreted as "Result: "+circle.toString()
    • Even the debugger of IDE like (IntellIJ) will use this implementation when you hover over the variable.
    • The default implementation prints the class name (with package) followed by @ followed by hash-code of the object in hexadecimal.
      public String toString() {
          return getClass().getName() + "@" + Integer.toHexString(hashCode());
      }
      
    • If we print an object of the Circle class, it will be printing like this:
      System.out.println();
      final Point point = new Point(1, 2);
      Circle circle = new Circle( point, 5f );
      System.out.println(circle); // z_medium.Medium$Circle@4dd8dc3
      
    • Let’s provide a custom implementation.
      public class Medium {
          static class Point{
              final int x;
              final int y;
      
              public Point(int x, int y) {
                  this.x = x;
                  this.y = y;
              }
      
              @Override
              public String toString() {
                  return "("+x+", "+y+")";
              }
          }
      
          static class Circle {
              final Point center;
              final float radius;
      
              public Circle(Point center, float radius) {
                  this.center = center;
                  this.radius = radius;
              }
      
              @Override
              public String toString() {
                  return "center: " + center + ", radius: " + radius;
              }
          }
      
          public static void main(String[] args) {
              final Point point = new Point(1, 2);
              Circle circle = new Circle( point, 5f );
              System.out.println(circle); // center: (1, 2), radius: 5.0
          }
      
      }
      
    • See, we had to provide implementation for the nested class Point as well, since we are using it in the toString() method of the Circle class.
  2. equals()

    • This is very useful for comparing two objects directly or in Comparator for sorting.
    • This is also used by Map when we use our custom class as the key to the Map.
    • The default implementation just checks if both refers to same object or not.
      public boolean equals(Object obj) {
          return (this == obj);
      }
      
    • But the default equals() can be very bad since it will return false even if the two objects have exactly the same values.
      Point pointOne = new Point(1, 2);
      Point pointTwo = new Point(1,2);
      System.out.println( pointOne.equals(pointTwo) ); // false
      
    • Let's provide our own implementation.
      public class Medium {
      
          static class Point{
              final int x;
              final int y;
      
              public Point(int x, int y) {
                  this.x = x;
                  this.y = y;
              }
      
              @Override
              public boolean equals(Object obj) {
                  if(this == obj) { // same object -- (1)
                      return true;
                  }
      
                  if( !(obj instanceof Point other)) {// different classes --- (2)
                      return false;
                  }
      
                  return x == other.x && y == other.y;//comparing the fields -- (3)
              }
      
          }
      
          public static void main(String[] args) {
              final Point pointOne = new Point(1, 2);
              final Point pointTwo = new Point(1,2);
              final Point pointThree = new Point(2,3);
              final String john = "John";
      
              System.out.println( pointOne.equals(pointOne) ); // true (1)
              System.out.println( pointOne.equals(john) ); // false (2)
              System.out.println( pointOne.equals(pointTwo) ); // true (3)
              System.out.println( pointTwo.equals(pointThree) ); // false (3)
      
          }
      
      }
      
    • Keep in mind that, we still have to use equals(Object) for comparing. We can’t use ==.
  3. hashCode()

    • This is another important method used with equals to be used in Map.
    • Suppose we want to use the Point class as the key in a map.
    • Let’s do it first without any custom implementation and find out the problem.
      public static void main(String[] args) {
          final Point pointOne = new Point(1, 2);
          final Point pointTwo = new Point(1,2);
          final Point pointThree = new Point(2,3);
      
          List<Point> pointList = List.of(pointOne, pointTwo, pointThree);
      
          final Map<Point, Integer> pointCountMap = new HashMap<>();
          for(Point point: pointList){
              pointCountMap.put(point, pointCountMap.getOrDefault(point, 0) + 1);
          }
      
          for(Map.Entry<Point, Integer> entry: pointCountMap.entrySet()){
              System.out.println(entry.getKey() + ": " + entry.getValue());
          }
          /*
          Output is:
          (2, 3): 1
          (1, 2): 1
          (1, 2): 1
          */
      }
      
    • We had the toString() and equals() method in the Point class. But still the count is not what we expected. This is because Map uses the hashCode of the object internally when we use a custom class as key.
    • By default, the hashCode() uses the memory location. So, for different objects having same value, their hash-code is different. As a result they are treated differently.
    • To solve this what we need is, to return same value from the hashCode() function if the properties are same.
    • Let’s define our own implementation:
      static class Point{
          // other codes
      
          @Override
          public int hashCode() {
              // return super.hashCode(); // default implementation
              return 11 * x + 31 * y;
              // return Objects.hash(x, y); // or use this
      
              // return 1; // will work. find yourself or let me know in comment
          }
      }
      
    • Output of the previous program will be as expected like this:
      (2, 3): 1
      (1, 2): 2
      

    If two objects are equal, then their hash-code have to be the same. But even the hash-code of two objects is same, they may be different. Do you know why?

  4. getClass()

    • It is used to find the runtime class of a variable.
    • Let’s see an example:
      String greet = "Hello";
      System.out.println( greet.getClass() ); // class java.lang.String
      
      Object objStr = greet; // ------ (2)
      System.out.println( objStr.getClass() ); // class java.lang.String
      
      Object obj = new Object();
      System.out.println( obj.getClass() ); // class java.lang.Object
      
    • At (2), we are storing a String object in a variable of type Object, but getClass() still prints the class java.lang.String since it is the runtime type.
  5. clone()

    • A useful method for copying an object.
    • The default clone() method can do it for us.
    • Only thing we need it, we have to handle the nested object cloning manually to make sure an actual deep copy.
    • Let’s see an example:
      public class Medium {
      
          static class Point implements Cloneable{
              int x, y;
      
              // constructor and toString
      
              @Override
              public Object clone() throws CloneNotSupportedException {
                  return super.clone();
              }
          }
      
          static class Circle implements Cloneable{
              Point center;
              float radius;
      
              // constructor and toString
      
              @Override
              public Object clone() throws CloneNotSupportedException {
                  Circle circle = (Circle) super.clone();
                  circle.center = (Point) center.clone(); // ------------------ (a)
                  return circle;
              }
          }
      
          public static void main(String[] args) {
      
              Circle circle = new Circle( new Point(1, 2), 3.5f );
              System.out.println(circle); // center: (1, 2), radius: 3.5
      
              try {
                  Circle cloned = (Circle) circle.clone();
                  System.out.println(cloned); // center: (1, 2), radius: 3.5
      
                  circle.center.x = 10;
                  // System.out.println(cloned.center.x); // 10 --> without (a)
                  System.out.println(cloned.center.x); // 1
      
              } catch (CloneNotSupportedException e) {
                  e.printStackTrace();
              }
      
          }
      
      }
      
    • Keep in mind that, the class must have to implement the Cloneable interface.
    • For the nested child, we have to do it like line (a) or by creating new object of the nested class manually if nested class is immutable or we don’t have the access to implement clone() method.
  6. finalize()

    • I am writing about it just because it’s still a part of the Object class.
    • This method is called by JVM when garbage collector destroys the object.
    • We can do some resource cleanup here.
    • But this is deprecated and even is not guaranteed that it will be called every time when object is being destroyed.
    • We better NOT rely on it for any operation.
  7. wait(), notify() and notifyAll()

    • These are for thread synchronization, but rarely used as we have awesome concurrency packages like java.util.concurrent for multithreading.
    • Just for the sake of understanding, we will create a program with a common class Artist which will generate and draw a point in a consistent way.
    • We will do the generate and draw part in two different threads with consistent behaviour between them.
    • Let me show you the code:
      public class Medium {
      
          static class Point {
              int x, y;
              public Point(int x, int y) { this.x = x; this.y = y; }
      
              @Override
              public String toString() { return "("+x+", "+y+")"; }
          }
      
          static class Artist {
              private final Point point = new Point(0,0);
              private boolean isCurrentPointDrawn = true;
      
              public synchronized void productNextPoint(int value) {
                  while (!isCurrentPointDrawn) {
                      try {
                          wait(); // Wait if current point isn't drawn yet
                      } catch (InterruptedException e) {
                          Thread.currentThread().interrupt();
                      }
                  }
      
                  try{ Thread.sleep(2000); }catch (InterruptedException e){}
                  point.x += value;
                  point.y += value;
                  System.out.println("Produced: " + point);
                  isCurrentPointDrawn = false;
                  notify(); // Notify that point is available to draw
              }
      
      
              public synchronized void drawCurrentPoint() {
                  while (isCurrentPointDrawn) {
                      try {
                          wait(); // Wait if no point is available to draw
                      } catch (InterruptedException e) {
                          Thread.currentThread().interrupt();
                      }
                  }
                  System.out.println("Drawn: " + point);
                  isCurrentPointDrawn = true;
                  notify(); // Notify the waiting producer that point has been drawn
              }
          }
      
          public static void main(String[] args) {
              final Artist resource = new Artist();
              final int noOfPoints = 3;
      
              Thread producerThread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (int i = 0; i < noOfPoints; i++) {
                          resource.productNextPoint(i);
                      }
                  }
              });
              Thread consumerThread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (int i = 0; i < noOfPoints; i++) {
                          resource.drawCurrentPoint();
                      }
                  }
              });
      
              producerThread.start();
              consumerThread.start();
          }
      
      }
      
    • Output of the program is:
      Produced: (0, 0)
      Drawn: (0, 0)
      Produced: (1, 1)
      Drawn: (1, 1)
      Produced: (3, 3)
      Drawn: (3, 3)
      
    • This program guarantees that a point is produced before it is drawn and the order remains consistent.
    • Don’t be confused at the while loop. We can use if also, but the problem with theif is that, for any exception in wait(), order may be wrong, which can product unexpected result.
    • Also, you may think the program will work without the wait() statement. Yes, it should. But it will cause very high CPU usages and inefficiency in program.
    • notifyAll() is same as notify()but it notifies all the threads that are waiting.

That's the end!!!

User12024-11-02

This is a comment 1.

User12024-11-02

This is a comment 2.