Java — Class Hierarchy
Basics of Java —Part V
Java Class Hierarchy — an Example!
Consider the following sample of a class hierarchy in Java.
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:
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
Objectclass, which is the superclass of all classes in Java. - The
Objectclass provides several methods that are common to all objects, such astoString(),equals(), andhashCode(). - All classes in Java either directly or indirectly inherit from the
Objectclass.
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
Shapeserves as a blueprint for various geometric shapes. It defines common attributes and methods that are shared by all shapes. - The
Shapeclass contains an abstract methodarea()that represents the area of the shape. Each subclass must provide its own implementation of thearea()method. - Additionally, the
Shapeclass may contain concrete methods or attributes that are common to all shapes.
abstract class Shape {
abstract double area();
}Concrete Class — Rectangle
- A concrete class
Rectanglerepresents a specific geometric shape, namely a rectangle. It provides a concrete implementation of thearea()method defined in theShapeclass. - The
Rectangleclass contains attributeslengthandwidthto 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:
Shapeis an abstract class that defines common behavior for all shapes. It cannot be instantiated directly, and itsarea()method must be implemented by subclasses.Rectangleis a concrete class that represents a specific shape, providing a concrete implementation of thearea()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 sameObjectinstance asobjects[0]. - Therefore, the comparison
objects[0].equals(objects[1])returnstruebecause both elements of the array refer to the sameObjectinstance. - The comparison
objects[0].equals(objects[2])still returnsfalsebecauseobjects[2]refers to a differentObjectinstance.
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?
Student student = (Student) obj;: This line casts theObjectparameterobjto aStudentobject. This is done because theequals()method signature requires anObjectparameter, but we want to compareStudentobjects specifically. This line assumes that theobjparameter is indeed an instance of theStudentclass. If it's not, aClassCastExceptionwill be thrown at runtime.id == student.id: This line compares theidattribute of the currentStudentobject (this) with theidattribute of theStudentobject (student) obtained from theobjparameter. It uses the==operator to check if the twoidvalues are equal.name.equals(student.name): This line compares thenameattribute of the currentStudentobject (this) with thenameattribute of theStudentobject (student) obtained from theobjparameter. It uses theequals()method of theStringclass to compare thenamevalues. This is done because strings are objects, and comparing strings with==compares their references (memory addresses), not their actual content. Usingequals()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
publicandprivateare access modifiers used to control access to classes, variables, and methods.publicallows unrestricted access to the class, variable, or method from any other class.privaterestricts 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
staticmodifier 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
finalmodifier is used to create constants, prevent inheritance, or ensure immutability.- It can be applied to variables, methods, or classes.
- For variables,
finalensures that their value cannot be changed after initialization. - For methods,
finalprevents overriding by subclasses. - For classes,
finalprevents 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 finalis often used to define constants accessible from anywhere. private staticcan 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🖐








