Module-2
Module-2
Inheritance: Inheritance Hierarchies, super keyword, final keyword, final classes, and
methods. Polymorphism: dynamic binding, method overriding. Abstraction: abstract
classes and methods. The Object class– Object Cloning – Inner Classes- Garbage
Collection - Finalize Method.
What is Inheritance?
Inheritance allows a class (called a subclass or derived class) to acquire properties and
behaviors (methods) from another class (called a superclass or base class). This mechanism
promotes code reusability and establishes a natural hierarchy between classes.
Example:
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
// Subclass
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
Benefits of Inheritance
Code Reusability
Inheritance enables classes to reuse code from existing classes, reducing redundancy and
promoting efficiency. This not only saves time but also minimizes the chances of errors that
might occur when rewriting similar code.
Modularity and Maintainability
By organizing code into a hierarchy of classes, inheritance promotes a modular approach to
software design. This makes the code easier to understand, maintain, and modify. Changes
made to a base class automatically propagate to all its subclasses, reducing the effort required
for maintenance and updates.
Extensibility
New functionality can be added to existing code without modifying it, adhering to the open-
closed principle of SOLID. This makes it easier to extend the capabilities of a system without
risking the stability of existing code.
Polymorphism
Inheritance is the foundation for polymorphism, allowing objects of different classes to be
treated as objects of a common base class. This enables more flexible and dynamic code.
Logical Hierarchy and Organization
Inheritance helps in creating a logical structure for your code, mirroring real-world
relationships between objects. This can make the overall design of a system more intuitive
and easier to understand.
Reduced Redundancy
Since child classes inherit features from the parent class, there is less need to write the same
code repeatedly. This reduces redundancy and keeps the code cleaner and more concise.
Faster Development
Because much of the code is reused from the superclass, development can be faster. You don't
have to write the same code multiple times; instead, you write it once in the superclass and
inherit it in the subclasses.
Drawbacks of Inheritance
1. Tight Coupling Between Classes
Inheritance creates a strong dependency between parent and child classes. Changes in
the superclass can inadvertently affect all subclasses, making the system less flexible
and harder to maintain.
2. Increased Complexity
Deep or extensive inheritance hierarchies can make codebases more complex and
harder to understand. Developers may struggle to trace the flow of execution or
determine where specific behaviors are defined.
3. Fragile Base Class Problem
Modifications to a base class can unintentionally break the functionality of derived
classes, especially if those subclasses rely on the base class's implementation details.
This issue is known as the "fragile base class" problem.
4. Violation of Encapsulation
Inheritance can expose the internal workings of a class to its subclasses, potentially
violating the principle of encapsulation. Subclasses may access or modify protected
members of the superclass, leading to unintended side effects.
5. Reduced Flexibility and Reusability
Once a subclass inherits from a superclass, it inherits all of its properties and methods,
even those that may not be relevant. This can lead to subclasses carrying unnecessary
baggage, reducing flexibility and reusability.
6. Overriding Issues
While method overriding allows subclasses to provide specific implementations, it
can also introduce bugs if not done carefully. Overriding methods without a clear
understanding of the superclass's behavior can lead to inconsistent or unexpected
outcomes.
7. Performance Overhead
Inheritance can introduce performance overhead due to dynamic method dispatch and
increased memory usage, especially in languages that support multiple inheritance or
deep hierarchies.
8. Difficulty in Refactoring
Refactoring code that uses inheritance can be challenging. Changes to a superclass
may necessitate changes in all subclasses, increasing the risk of introducing bugs
during the refactoring process.
Inheritance Hierarchies
An inheritance hierarchy is a tree-like structure that represents the relationships between
classes in OOP. At the top is the most general class, and as you move down, classes become
more specialized.
Object
└── Animal
├── Mammal
│ └── Dog
└── Bird
└── Sparrow
In this hierarchy:
Dog is a subclass of Mammal, which is a subclass of Animal.
Each subclass inherits attributes and methods from its superclass.
Benefits of Using Inheritance Hierarchies
Code Reusability: Common code can be written in the superclass and reused in
subclasses.
Improved Maintainability: Changes in the superclass automatically propagate to
subclasses.
Logical Structure: Reflects real-world relationships and promotes organized code.
Drawbacks
Tight Coupling: Subclasses are dependent on superclasses, which can lead to issues if
the superclass changes.
Complexity: Deep inheritance hierarchies can become difficult to manage and
understand.
Limited Flexibility: In some cases, composition (having classes contain instances of
other classes) might be more appropriate than inheritance.
Types of Inheritance
Single Inheritance: A subclass inherits from one superclass.
Example: class Dog extends Animal
// Superclass
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
// Subclass
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
Multilevel Inheritance: A subclass inherits from a superclass, and then another subclass
inherits from that subclass.
Example: class Dog extends Mammal, class Mammal extends Animal
// Base class
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
// Intermediate subclass
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
// Derived subclass
class Puppy extends Dog {
void weep() {
System.out.println("The puppy weeps.");
}
}
// Main class to test the inheritance
public class TestMultilevelInheritance {
public static void main(String[] args) {
Puppy puppy = new Puppy();
puppy.eat(); // Inherited from Animal
puppy.bark(); // Inherited from Dog
puppy.weep(); // Defined in Puppy
}
}
This demonstrates multilevel inheritance, where Puppy inherits from Dog, which in turn
inherits from Animal. As a result, an instance of Puppy has access to methods from all three
classes.
// Subclass 1
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
// Subclass 2
class Cat extends Animal {
void meow() {
System.out.println("The cat meows.");
}
}
2. Final Methods
A method declared as final cannot be overridden by subclasses. This ensures that the
method's behavior remains consistent across all instances.
Example:
class Animal {
public final void makeSound() {
System.out.println("Animal makes a sound");
}
}
3. Final Classes
A class declared as final cannot be subclassed. This is useful for creating immutable classes
or preventing inheritance for security or design reasons.
Example:
public final class MathUtility {
public static final double PI = 3.14159;
}
Benefits of Using final
Immutability: Ensures that variables, methods, or classes cannot be modified after
their initial definition, promoting consistency and reliability.
Security: Prevents unauthorized changes to critical code components, enhancing
security.
Performance Optimization: Allows the Java Virtual Machine (JVM) to optimize
code more effectively, as it can make assumptions about the immutability of final
elements.
Design Clarity: Conveys the developer's intent, making the code easier to understand
and maintain.
Polymorphism:
Polymorphism is a fundamental concept in Object-Oriented Programming (OOP) that allows
objects of different classes to be treated as instances of a common superclass. It enables a
single interface to represent different underlying forms (data types), enhancing flexibility and
reusability in code.
Types of Polymorphism
1. Compile-Time Polymorphism (Static Binding)
Also known as method overloading, compile-time polymorphism occurs when multiple
methods have the same name but differ in parameters (number, type, or order). The method to
be invoked is determined at compile time.
Example:
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
In this example, the add method is overloaded to handle both integer and double parameters.
Benefits of Polymorphism
Code Reusability: Write generic code that works with objects of different classes.
Extensibility: Easily add new classes without modifying existing code.
Maintainability: Simplifies code updates and maintenance by centralizing common
behavior.
Flexibility: Allows for dynamic method invocation, enabling more adaptable code
structures.
Method overriding
In method overriding, a subclass defines a method with the same signature (name, return
type, and parameter list) as a method in its superclass. The overriding method in the subclass
provides a specific implementation that replaces the superclass's version when invoked on an
object of the subclass.
Key Characteristics:
Same Method Signature: The method in the subclass must have the same name,
return type, and parameters as the method in the superclass.
Inheritance Required: Method overriding occurs between a superclass and a
subclass.
Runtime Polymorphism: The method to be invoked is determined at runtime based
on the object's actual type.
Example
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal();
myAnimal.sound(); // Outputs: Animal makes a sound
Abstraction allows developers to define the structure of classes and methods without
providing complete implementations. It helps in managing complexity by exposing only
relevant details to the user and hiding the internal workings.
Abstract Classes
An abstract class is a class that cannot be instantiated on its own and is declared using the
abstract keyword. It may contain both abstract methods (without implementation) and
concrete methods (with implementation).
Key Features:
Cannot be instantiated: You cannot create objects of an abstract class directly.
Can contain constructors: Abstract classes can have constructors, which are called
when a subclass is instantiated.
Can have member variables and methods: They can include fields and methods
with or without implementations.
Supports inheritance: Other classes can extend abstract classes and provide
implementations for abstract methods.
Example
abstract class Animal {
abstract void makeSound();
Characteristics:
No method body: Abstract methods end with a semicolon and do not have a body.
Must be in abstract classes: They can only be declared within abstract classes.
Must be overridden: Subclasses are required to provide concrete implementations
for all abstract methods.
Example:
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // Outputs: Bark
dog.eat(); // Outputs: This animal eats food.
}
}
In this example, Dog extends the abstract class Animal and provides an implementation for
the makeSound() method.
While both abstract classes and interfaces are used to achieve abstraction, they have distinct
differences:
Abstract Classes:
Can have both abstract and concrete methods.
Can have member variables with any access modifier.
Supports constructors.
A class can extend only one abstract class.
Interfaces:
All methods are implicitly abstract (until Java 8, which introduced default and static
methods).
All variables are implicitly public, static, and final.
Cannot have constructors.
A class can implement multiple interfaces.
Choose abstract classes when you want to share code among several closely related classes,
and interfaces when you want to define a contract that can be implemented by unrelated
classes.
Object class
In Java, the Object class serves as the ultimate superclass for all other classes. Located in the
java.lang package, it provides a set of fundamental methods that are inherited by every Java
class, either directly or indirectly. Understanding these methods is crucial for effective Java
programming.
Example:
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
MyClass other = (MyClass) obj;
return this.id == other.id;
}
hashCode()
Purpose: Returns an integer hash code value for the object.
Usage: Essential for objects used in hash-based collections like HashMap or HashSet.
Contract with equals(): Equal objects must have the same hash code.
Example:
@Override
public int hashCode() {
return Objects.hash(id);
}
3.toString()
Purpose: Provides a string representation of the object.
Default Behavior: Returns a string consisting of the class name followed by the
object's hash code.
Typical Override: To return a more informative and readable string.
Example:
@Override
public String toString() {
return "MyClass{id=" + id + "}";
}
4.getClass()
Purpose: Returns the runtime class of the object.
Usage: Useful for reflection and obtaining class metadata.
Example:
Class<?> clazz = myObject.getClass();
System.out.println("Class name: " + clazz.getName());
5. clone()
Purpose: Creates and returns a copy of the object.
Requirements:
The class must implement the Cloneable interface.
Override the clone() method with public access.
Example:
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
6. finalize()
Purpose: Called by the garbage collector before reclaiming the object's memory.
Note: This method is deprecated in Java 9 and later due to unpredictability and performance
issues.
inner classes
In Java, inner classes are classes defined within another class. They enable logical grouping
of classes that are only used in one place, enhancing encapsulation and readability. Java
supports several types of inner classes, each serving distinct purposes.
Types of Inner Classes in Java
1. Member Inner Class
A non-static class defined within another class.
Access: Can access all members (including private) of the outer class.
Instantiation:
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
2. Static Nested Class
A static class defined within another class.
Access: Can access only static members of the outer class.
Instantiation:
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
Old Generation (Tenured Generation): Holds objects that have survived multiple
garbage collection cycles in the Young Generation.
Permanent Generation (PermGen): Used in earlier Java versions to store metadata about
classes. Replaced by Metaspace in Java 8 and later.
finalize() method
In Java, the finalize() method was traditionally used to perform cleanup operations before an
object was reclaimed by the garbage collector. However, its usage has been fraught with
issues, leading to its deprecation in Java 9 and plans for removal in future releases.
What is finalize()?
The finalize() method is defined in the Object class and is invoked by the garbage collector
before an object is destroyed. It was intended for releasing resources such as file handles or
network connections.
@Override
protected void finalize() throws Throwable {
try {
// Cleanup operations
} finally {
super.finalize();
}
}
Why finalize() Is Deprecated
The deprecation of finalize() stems from several inherent problems:
Unpredictable Timing: There's no guarantee when, or even if, finalize() will be
executed, making resource management unreliable.
Performance Issues: Objects with finalizers take longer to be garbage collected,
leading to potential memory leaks and performance degradation.
Security Risks: Finalizers can be exploited to resurrect objects, posing security
vulnerabilities.
Complexity and Errors: Overriding finalize() correctly is challenging, and mistakes
can lead to subtle bugs.
Due to these issues, the Java community recommends avoiding finalize() in favor of more
reliable resource management techniques.