Sitemap

Java — Class Hierarchy

Basics of Java —Part V

Java Class Hierarchy — an Example!

Consider the following sample of a class hierarchy in Java.

Press enter or click to view image in full size

Here,

Manager and SalesPerson are subclasses of the Employee class. Employee is a subclass of the Person class. Person is a superclass of Customer and Employee class. Employee is thus a superclass (to Manager and SalesPerson) and a subclass(of Person) in the hierarchy.

Generalization v/s Specialization(Inheritance v/s Subtyping) — a revisit!

Generalization (Inheritance)

Generalization is the act of capturing similarities between classes and defining the similarities in a new generalized class; the classes then become subclasses of the generalized class. Inheritance allows subclasses to inherit attributes and methods from their superclass, promoting code reuse and abstraction.

// Superclass (General)
class Animal {
void eat() {
System.out.println("Animal is eating.");
}
}

// Subclass (Specialized)
class Dog extends Animal {
void bark() {
System.out.println("Dog is barking.");
}
}

// Subclass (Specialized)
class Cat extends Animal {
void meow() {
System.out.println("Cat is meowing.");
}
}

public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited from Animal
dog.bark();

Cat cat = new Cat();
cat.eat(); // Inherited from Animal
cat.meow();
}
}

In this example, Animal is a general superclass, and Dog and Cat are specialized subclasses. Both Dog and Cat inherit the eat() method from the Animal superclass. Additionally, Dog has a specific behavior (bark()), and Cat has a specific behavior (meow()), making them specialized.

Specialization (Subtyping)

Specialization is the act of capturing differences among objects in a class and creating new distinct subclasses with the differences. In this way, we are specializing information about objects of the superclass into subclasses. Subtypes can add new attributes or methods, override existing methods, or define additional behaviors.

// Supertype (General)
interface Shape {
double area();
}

// Subtype (Specialized)
class Rectangle implements Shape {
private double length;
private double width;

Rectangle(double length, double width) {
this.length = length;
this.width = width;
}

@Override
public double area() {
return length * width;
}
}

// Subtype (Specialized)
class Circle implements Shape {
private double radius;

Circle(double radius) {
this.radius = radius;
}

@Override
public double area() {
return Math.PI * radius * radius;
}
}

public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(5, 3);
System.out.println("Rectangle area: " + rectangle.area());

Circle circle = new Circle(2.5);
System.out.println("Circle area: " + circle.area());
}
}

In this example, Shape is a general supertype representing shapes. Rectangle and Circle are specialized subtypes implementing the Shape interface. Each subtype provides its own implementation of the area() method, reflecting the specific behavior of rectangles and circles.

The diagram below shows the difference:

Press enter or click to view image in full size

Java Class Hierarchy

Classes in a class hierarchy diagram are organized in such a way that generalized classes are placed toward the top of the hierarchy. As we traverse higher into a class hierarchy, the classes become more general in definition and more objects can be classified into them. As we traverse lower into the class hierarchy, the subclasses become more specialized in definition and fewer objects can be classified into them.

In Java, the class hierarchy is structured in a hierarchical manner, with all classes ultimately descending from the Object class. Thus, the Object class is most generalized class in Java. Here's an overview of the Java class hierarchy:

Object Class

  • At the root of the class hierarchy is the Object class, which is the superclass of all classes in Java.
  • The Object class provides several methods that are common to all objects, such as toString(), equals(), and hashCode().
  • All classes in Java either directly or indirectly inherit from the Object class.

Predefined Classes

  • Java provides several predefined classes, such as String, Integer, Double, Boolean, etc., which are part of the core Java API.
  • These classes provide functionality for common data types and utility operations.

User-Defined Classes

  • Classes defined by the user are organized in a hierarchical manner based on inheritance relationships.
  • Each class can have one direct superclass, and it inherits members (fields and methods) from its superclass.

Inheritance

  • Inheritance allows a class to inherit fields and methods from another class, known as its superclass.
  • A subclass can extend its superclass by adding new fields and methods or by overriding existing methods.
  • Java supports single inheritance of classes, meaning that each class can have at most one direct superclass. Java does not support Multiple Inheritance.

Interfaces

  • Interfaces define a contract for classes to implement.
  • Classes can implement multiple interfaces, allowing them to inherit behavior from multiple sources.
  • Interfaces provide a way to achieve abstraction and define common behavior without the need for multiple inheritance of classes.

Abstract Classes v/s Concrete Classes — an example!

Abstract Class — Shape

  • An abstract class Shape serves as a blueprint for various geometric shapes. It defines common attributes and methods that are shared by all shapes.
  • The Shape class contains an abstract method area() that represents the area of the shape. Each subclass must provide its own implementation of the area() method.
  • Additionally, the Shape class may contain concrete methods or attributes that are common to all shapes.
abstract class Shape {
abstract double area();
}

Concrete Class — Rectangle

  • A concrete class Rectangle represents a specific geometric shape, namely a rectangle. It provides a concrete implementation of the area() method defined in the Shape class.
  • The Rectangle class contains attributes length and width to represent the dimensions of the rectangle. It also provides a constructor to initialize these attributes.
class Rectangle extends Shape {
private double length;
private double width;

Rectangle(double length, double width) {
this.length = length;
this.width = width;
}

@Override
double area() {
return length * width;
}
}

In this example:

  • Shape is an abstract class that defines common behavior for all shapes. It cannot be instantiated directly, and its area() method must be implemented by subclasses.
  • Rectangle is a concrete class that represents a specific shape, providing a concrete implementation of the area() method. It can be instantiated directly to create rectangle objects with specific dimensions.

This example demonstrates the use of abstract and concrete classes in defining a class hierarchy for geometric shapes. The abstract class defines general behavior, while concrete classes provide specific implementations for individual shapes.

Get Binayak Basu’s stories in your inbox

Join Medium for free to get updates from this writer.

Overall, the Java class hierarchy is organized to promote code reuse, modularity, and extensibility through inheritance and interfaces. By leveraging inheritance relationships and implementing interfaces, developers can create modular, flexible, and maintainable Java code.

Object Class — usage of equals()

Consider the following example:

public class Main {
public static void main(String[] args) {
// Create an array of Object instances
Object[] objects = new Object[3];
objects[0] = new Object();
objects[1] = objects[0]; // Referencing the same object as objects[0]
objects[2] = new Object();

// Compare the objects using equals() method
System.out.println("Is objects[0] equal to objects[1]? " + objects[0].equals(objects[1])); // true
System.out.println("Is objects[0] equal to objects[2]? " + objects[0].equals(objects[2])); // false
}
}
  • We initialize objects[1] to refer to the same Object instance as objects[0].
  • Therefore, the comparison objects[0].equals(objects[1]) returns true because both elements of the array refer to the same Object instance.
  • The comparison objects[0].equals(objects[2]) still returns false because objects[2] refers to a different Object instance.

Say, we want to override the equals function with our own for a defined class. Look at the example below:

public class Student {
private int id;
private String name;

public Student(int id, String name) {
this.id = id;
this.name = name;
}

// Override equals() method to compare two objects based on their content
@Override
public boolean equals(Object obj) {
// Check if the other object is also a Student object
if (this == obj)
return true;
if (obj == null || !(obj instanceof Student))
return false;

// Cast the other object to Student and compare their attributes
Student student = (Student) obj;
return id == student.id &&
name.equals(student.name);
}

public static void main(String[] args) {
Student student1 = new Student(1, "John");
Student student2 = new Student(2, "Alice");
Student student3 = new Student(1, "John"); // Another student with the same ID and name as student1

// equals() method
System.out.println("Is student1 equal to student2? " + student1.equals(student2)); // false
System.out.println("Is student1 equal to student3? " + student1.equals(student3)); // true
}
}

The code portion which should draw your attention is:

Student student = (Student) obj;
return id == student.id &&
name.equals(student.name);

Why do we need this?

  1. Student student = (Student) obj;: This line casts the Object parameter obj to a Student object. This is done because the equals() method signature requires an Object parameter, but we want to compare Student objects specifically. This line assumes that the obj parameter is indeed an instance of the Student class. If it's not, a ClassCastException will be thrown at runtime.
  2. id == student.id: This line compares the id attribute of the current Student object (this) with the id attribute of the Student object (student) obtained from the obj parameter. It uses the == operator to check if the two id values are equal.
  3. name.equals(student.name): This line compares the name attribute of the current Student object (this) with the name attribute of the Student object (student) obtained from the obj parameter. It uses the equals() method of the String class to compare the name values. This is done because strings are objects, and comparing strings with == compares their references (memory addresses), not their actual content. Using equals() ensures that the content of the strings is compared.

Overriding toString() of Object Class

Overriding the toString() method is a powerful way to provide a meaningful string representation of your object. Let's implement it in a hypothetical Person class:

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Override toString() for a custom string representation
@Override
public String toString() {
return "Name: " + name + ", Age: " + age;
}
public static void main(String[] args) {
Person person = new Person("John", 25);
System.out.println(person); // Automatically calls toString() method
}
}

In this example, the toString() method is overridden to return a string containing the person's name and age. When you print the Person object using System.out.println(person), it automatically calls the overridden toString() method to provide a meaningful representation of the object.

Modifiers in Java — detailed discussion!

Let’s explore different combinations of modifiers in Java and the situations where they make sense:

Public vs. Private

  • public and private are access modifiers used to control access to classes, variables, and methods.
  • public allows unrestricted access to the class, variable, or method from any other class.
  • private restricts access to the class, variable, or method only within the same class.

Example

public class MyClass {
private int myPrivateVariable; // Encapsulation of data
public void myPublicMethod() {
// Accessible from any other class
}
}

Static

  • static modifier is used to create class-level variables or methods that exist independently of any instance of the class.
  • It’s commonly used for utility methods or constants that are shared across all instances of the class.

Example:

public class MathUtils {
public static final double PI = 3.14159; // Constant value
public static int add(int a, int b) {
return a + b;
}
}

Final

  • final modifier is used to create constants, prevent inheritance, or ensure immutability.
  • It can be applied to variables, methods, or classes.
  • For variables, final ensures that their value cannot be changed after initialization.
  • For methods, final prevents overriding by subclasses.
  • For classes, final prevents subclassing.
public class Circle {
private final double radius; // Immutable variable
public Circle(double radius) {
this.radius = radius;
}
public final double getArea() { // Method cannot be overridden
return Math.PI * radius * radius;
}
}

Combining Modifiers

  • Modifiers can be combined to achieve specific behaviors. For example, public static final is often used to define constants accessible from anywhere.
  • private static can be used for class-level utility methods that do not depend on instance variables.
public class Constants {
public static final int MAX_SIZE = 100; // Public constant
private static int counter = 0; // Private static variable
public static int getNextId() { // Public static method
return counter++;
}
}

In summary, different combinations of modifiers in Java are used to achieve encapsulation, class-level behavior, immutability, and control over access to classes, variables, and methods. Understanding when and how to apply these modifiers is essential for writing clean, maintainable, and secure Java code.

Ok!

Java’s class hierarchy is like a digital family tree, where classes inherit traits from their ancestors and pass them on to their descendants. With generalization, classes share common characteristics, while specialization allows for unique attributes. Inheritance fosters code reuse and abstraction, while interfaces define common behaviors. Abstract and concrete classes shape the hierarchy, promoting modularity and extensibility. Access modifiers like public and private control visibility, while static and final provide class-level functionality. Understanding these concepts empowers coders to craft flexible, modular, and maintainable Java applications for the digital age. B…Y…E…😊

TIP: Click on the smiley to go to the next article! It may not work now, but will definitely work once I publish the next one🖐

Learning, building, and writing about data. Power BI, SQL, Python, Fabric. IITM Data Science. I turn real-world problems into clear, useful insights.