Navigation
Java Full Course: Mastering the Language Object Oriented Programming in Java
Object Oriented Programming in Java
(Complete Notes | Beginner to Advanced | Professional & Exam-Oriented | Masterclass)
Table of Contents
1. Introduction to Object Oriented Programming
Java is fundamentally an object oriented language. At its core, it models real world entities as objects that combine data (attributes) and behavior (methods). This approach makes programs more modular, reusable, and easier to understand.
The four main pillars of object oriented programming are:
- Encapsulation – Bundling data and methods together, and hiding internal details.
- Inheritance – Creating new classes based on existing ones, promoting code reuse.
- Polymorphism – Allowing objects to take many forms, enabling flexible and extensible code.
- Abstraction – Hiding complex implementation details and exposing only essential features.
In Java, these concepts are implemented using classes, objects, interfaces, and packages. Mastering OOP is essential for building scalable, maintainable, and robust applications.
In the following sections, we will explore each pillar in depth, along with related topics like constructors, access modifiers, method overloading, overriding, abstract classes, and interfaces.
2. Classes and Objects
Java is an object oriented programming language, meaning it organizes code around objects rather than functions and logic alone. An object is a self contained entity that consists of state (data) and behavior (methods). A class is a blueprint or template from which objects are created.
- Class – Defines the structure (fields) and capabilities (methods) that its objects will have.
- Object – An instance of a class, created at runtime, with its own copy of the instance fields.
2.1 Defining a Class
A class definition contains fields (variables) and methods (functions) that operate on those fields.
Syntax:
Example:
- The
publickeyword is an access modifier (discussed later). - Fields are declared like regular variables but inside the class.
- Methods define behaviors.
2.2 Creating Objects (Instantiating a Class)
Objects are created using the new keyword, which allocates memory for the object and returns a reference to it.
Syntax:
Example:
myCaris a reference variable that holds the memory address of the object.new Car()invokes the constructor (see below).
Accessing fields and methods:
2.3 Memory Allocation: Heap and Stack
- Heap – All objects are stored in the heap. When you write
new Car(), memory is allocated on the heap for that object. - Stack – Local variables (including reference variables) are stored on the stack. The reference variable
myCarlives on the stack and holds the address of the object on the heap.
When a method is called, a new stack frame is created. When the method ends, the frame is popped. Objects on the heap remain until they are no longer referenced (garbage collected).
2.4 Constructors
A constructor is a special method that initializes an object when it is created. It has the same name as the class and no return type (not even void).
a) Default Constructor
If you do not provide any constructor, Java provides a default constructor with no parameters that initializes fields to default values (0, false, null).
b) Parameterized Constructor
You can define constructors with parameters to set initial values.
Usage:
c) Constructor Overloading
You can define multiple constructors with different parameter lists.
d) The this Keyword in Constructors
this refers to the current object. It is used to distinguish instance variables from parameters with the same name, or to call another constructor.
Calling another constructor:
[!IMPORTANT]
this() must be the first statement in a constructor.
2.5 Methods
Methods define the behavior of objects. They can be instance methods (belong to an object) or static methods (belong to the class).
a) Instance Methods
- Operate on instance fields.
- Can access other instance methods and fields.
b) Static Methods
- Declared with the
statickeyword. - Belong to the class, not to any object.
- Cannot directly access instance fields or methods (they can only access static members).
Calling a static method: Car.showDescription();
c) Method Overloading
Multiple methods can have the same name but different parameter lists (type, number, or order).
The compiler determines which method to call based on the arguments.
d) Return Types
A method can return a value using the return keyword. The return type must be specified.
If no value is returned, use void.
2.6 Access Modifiers
Access modifiers control the visibility of classes, fields, methods, and constructors.
| Modifier | Same Class | Same Package | Subclass (different package) | Any Class |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
- private – Accessible only within the same class.
- default (no modifier) – Accessible within the same package.
- protected – Accessible within the same package and by subclasses (even in different packages).
- public – Accessible from anywhere.
[!TIP]
Best practice: Keep fields private and provide getter/setter methods for controlled access (encapsulation).
2.7 The this Keyword
this is a reference to the current object. It is used for:
- Distinguishing instance variables from parameters:
this.brand = brand; - Calling another constructor:
this(...); - Passing the current object as a parameter:
someMethod(this); - Returning the current object from a method (method chaining):
2.8 Garbage Collection
Java automatically manages memory. When an object is no longer reachable (no references pointing to it), the garbage collector reclaims its memory.
- The
finalize()method (deprecated in recent Java) was used for cleanup before garbage collection. - To explicitly suggest garbage collection, use
System.gc()(but it's not guaranteed).
Example of object becoming eligible for GC:
2.9 Complete Example
Output:
2.10 Common Pitfalls and Best Practices
| Pitfall | Explanation / Solution |
|---|---|
| Forgetting to use new | Car myCar; only declares a reference, no object. Use new. |
| Accessing uninitialized fields | Fields have default values (0, false, null), but objects referenced may be null. Always initialize before use. |
| Using == for object equality | == compares references, not content. Use equals() for content. |
| Not providing getters/setters | Direct field access violates encapsulation. Use accessor methods. |
| Exposing mutable objects | Returning a reference to a mutable object (e.g., an array) breaks encapsulation. Return a copy or use immutable types. |
| Constructors with too many parameters | Use builder pattern or refactor. |
| Static methods accessing instance fields | Causes compilation error. Use instance methods for instance fields. |
Best Practices:
- Keep fields
privateand provide getters/setters. - Use meaningful class and method names.
- Use constructors to ensure objects are properly initialized.
- Avoid unnecessary mutable state; prefer immutability where possible.
- Use
thisonly when needed (e.g., to disambiguate). - Follow the Single Responsibility Principle – a class should have one reason to change.
2.11 Key Points to Remember
- A class is a blueprint; an object is an instance.
- Objects are created with
new. - Constructors initialize new objects; if none is provided, a default constructor exists.
- Fields can be instance (per object) or static (shared across all objects).
- Methods can be instance or static.
thisrefers to the current object.- Access modifiers control visibility.
- Encapsulation hides internal state and exposes controlled access.
- Garbage collector automatically reclaims unreachable objects.
3. Inheritance in Java
3.1 What is Inheritance?
Inheritance is a mechanism in object‑oriented programming where one class (the child or subclass) acquires the properties (fields) and behaviors (methods) of another class (the parent or superclass). It establishes an is‑a relationship: a subclass is a specialized version of the superclass.
Benefits:
- Code reusability – common code is written once in the superclass.
- Method overriding – subclasses can provide specific implementations.
- Polymorphism – enables flexible and extensible designs.
- Hierarchical classification – models real‑world relationships.
3.2 Syntax and Basic Example
Use the extends keyword to create a subclass.
Example:
Doginheritsnameandeat()fromAnimal.Dogadds its own methodbark().
3.3 Types of Inheritance in Java
Java supports single inheritance for classes – a class can extend only one direct superclass. However, it supports multiple inheritance of type through interfaces.
a) Single Inheritance
One subclass inherits from one superclass.
b) Multilevel Inheritance
A class extends a subclass, creating a chain.
C inherits from both B and indirectly from A.
c) Hierarchical Inheritance
Multiple subclasses extend the same superclass.
d) Multiple Inheritance (via interfaces)
A class can implement multiple interfaces, achieving multiple inheritance of behavior.
[!IMPORTANT] Java does not allow a class to extend more than one class (no multiple inheritance of classes) to avoid the diamond problem (ambiguity when two parent classes have the same method).
3.4 The super Keyword
super is used to refer to the immediate superclass. It can be used for:
- Accessing superclass fields (if hidden by subclass fields).
- Invoking superclass methods (overridden ones).
- Calling superclass constructors.
Output for new Car():
Rules for super:
super()must be the first statement in a constructor.- If not explicitly written, the compiler inserts
super()(calling the no‑arg superclass constructor). - If the superclass has no no‑arg constructor, you must explicitly call a parameterized constructor using
super(...).
3.5 Method Overriding
Method overriding occurs when a subclass provides a specific implementation of a method already defined in its superclass.
Rules:
- The method name, return type, and parameters must exactly match (covariant return types are allowed – subclass can return a subtype).
- The access modifier cannot be more restrictive than the superclass method (e.g., if superclass method is
public, subclass cannot make itprivate). finalmethods cannot be overridden.staticmethods are not overridden; they are hidden (method hiding).abstractmethods must be overridden unless the subclass is also abstract.
Example:
[!TIP]
The @Override annotation is optional but recommended. It tells the compiler that the method is intended to override a superclass method, helping catch errors (e.g., misspelling).
3.6 Constructors in Inheritance
- The superclass constructor is always called before the subclass constructor body executes.
- If you don't explicitly call a superclass constructor, the compiler inserts
super()(the no‑arg constructor). - If the superclass does not have a no‑arg constructor, you must explicitly call a parameterized constructor using
super(...)as the first statement in the subclass constructor.
3.7 final Classes and Methods
- A
finalclass cannot be extended. - A
finalmethod cannot be overridden.
3.8 Abstract Classes
An abstract class is a class that cannot be instantiated. It may contain abstract methods (without body) that must be overridden by concrete subclasses.
- Abstract classes can have constructors (called when subclass is instantiated).
- If a subclass does not implement all abstract methods, it must also be
abstract.
3.9 The Object Class
Every class in Java implicitly extends java.lang.Object (unless it already extends another class). Object provides basic methods:
toString()equals(Object)hashCode()getClass()clone()finalize()(deprecated)wait(),notify(),notifyAll()
Overriding toString() and equals() is common.
3.10 Polymorphism and Inheritance
Polymorphism (many forms) allows a reference of a superclass type to hold an object of a subclass type. The actual method executed is determined at runtime (dynamic method dispatch).
This enables writing flexible code that works with any subclass of a given superclass.
3.11 Complete Example
3.12 Common Pitfalls and Best Practices
| Pitfall | Explanation / Solution |
|---|---|
| Forgetting super() | If superclass lacks a no‑arg constructor, you must call a parameterized super(...). |
| Overriding without @Override | Accidentally creating a new method instead of overriding. Use the annotation. |
| Calling overridden method in constructor | The overridden method might be called before subclass fields are initialized. Avoid calling overridable methods in constructors. |
| Using protected or public fields | Exposes internal details; prefer private with getters/setters. |
| Deep inheritance hierarchies | Hard to maintain; prefer composition over inheritance when possible. |
| Confusing method overriding with overloading | Overriding changes behavior in subclass; overloading adds variants. |
| Not using polymorphism | Writing code that references concrete classes reduces flexibility. Program to interfaces/superclasses. |
Best Practices:
- Favor composition over inheritance unless an is‑a relationship truly exists.
- Keep inheritance hierarchies shallow.
- Use
@Overrideconsistently. - Use
protectedonly when subclasses genuinely need direct access. - Declare methods
finalif they should not be overridden. - Use abstract classes to provide common base behavior with some implementation.
3.13 Key Points to Remember
- Inheritance is an is‑a relationship (e.g., Dog is an Animal).
- Java supports single inheritance for classes; multiple inheritance is achieved via interfaces.
extendskeyword creates a subclass.superaccesses superclass members and constructors.- Method overriding allows a subclass to provide a specific implementation.
- Constructors are not inherited; superclass constructors are called first.
- Every class extends
Objectimplicitly. - Abstract classes cannot be instantiated; they may contain abstract methods.
- Polymorphism allows using a superclass reference to refer to a subclass object.
4. Encapsulation in Java
4.1 What is Encapsulation?
Encapsulation is one of the four fundamental OOP concepts (the others being inheritance, polymorphism, and abstraction). It refers to the bundling of data (fields) and methods (behavior) that operate on that data into a single unit (a class). It also involves hiding the internal state of an object and requiring all interaction to occur through well‑defined interfaces.
In Java, encapsulation is typically achieved by:
- Declaring fields as
privateto prevent direct external access. - Providing
publicmethods (getters and setters) to access and modify the fields.
The main idea is: the internal representation of an object is hidden from the outside world.
4.2 Why Encapsulation?
Encapsulation provides several important benefits:
- Data Hiding – The internal state cannot be accessed directly; only controlled methods can change it.
- Controlled Access – Getters and setters can include validation, logging, or other logic.
- Flexibility – You can change the internal implementation without affecting code that uses the class (as long as the public interface remains the same).
- Maintainability – Changes are localized; bugs are easier to isolate.
- Security – Sensitive data can be protected from accidental or malicious modification.
- Reusability – Well‑encapsulated classes are easier to reuse in different contexts.
4.3 How to Achieve Encapsulation
The typical pattern:
Now the Person class hides its fields and exposes controlled access.
4.4 Access Modifiers and Their Role
Access modifiers determine the visibility of classes, fields, methods, and constructors. For encapsulation, we use them to restrict access.
| Modifier | Same Class | Same Package | Subclass (different package) | Any Class |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
- private – The most restrictive. Fields are almost always private. Only methods within the same class can access them.
- protected – Allows access to subclasses (and same package). Used when you want to give inheriting classes access.
- default (no modifier) – Package‑private. Accessible only within the same package.
- public – Accessible everywhere. Use sparingly; typically for the class itself and its public interface (methods).
4.5 Getters and Setters (Accessors and Mutators)
- Getter – returns the value of a private field. Often named
getFieldName()(for non‑boolean) orisFieldName()(for boolean). - Setter – modifies the value of a private field, optionally with validation. Often named
setFieldName().
Example with boolean:
Validation inside setters:
Logic inside getters:
4.6 Immutability as a Form of Encapsulation
An immutable class is one whose state cannot be changed after construction. This is the ultimate form of encapsulation because the object is read‑only. To create an immutable class:
- Declare all fields
privateandfinal. - Do not provide setters.
- Do not allow subclasses (make the class
finalor use private constructors with factory methods). - If fields are mutable (e.g., collections), return defensive copies or unmodifiable views.
Example:
4.7 Protecting Mutable Fields
When a field is a reference to a mutable object (e.g., an array or a Date), simply returning that reference breaks encapsulation because the caller can modify the object.
Solution: Return a defensive copy or an unmodifiable view.
Example with array:
Example with List:
4.8 Complete Example
Usage:
4.9 Common Pitfalls
| Pitfall | Explanation / Solution |
|---|---|
| Exposing mutable objects directly | Return defensive copies or unmodifiable wrappers. |
| Providing unnecessary setters | Not all fields need setters; some may be immutable after construction. |
| Setters without validation | Can lead to invalid object states. Always validate in setters. |
| Using public fields | Breaks encapsulation entirely; avoid. |
| Returning this in methods that modify state | Allows method chaining but can expose mutable state; ensure you're okay with that. |
| Leaking references in constructors | Storing a reference to an externally mutable object (e.g., a passed array) without copying. |
| Not making fields private | Even protected fields can be accessed by subclasses, breaking encapsulation. Use private and protected getters/setters if needed. |
4.10 Best Practices
- Declare fields private – always, unless you have a very good reason not to.
- Provide getters only where needed – don't expose data unnecessarily.
- Provide setters only when modification should be allowed – and include validation.
- Use defensive copying for mutable objects passed in or returned.
- Consider immutability – if a class is meant to be read‑only, make it immutable.
- Use package‑private or protected access only when designing for extension within the same package.
- Keep the public interface minimal – hide implementation details.
4.11 Key Points to Remember
- Encapsulation bundles data with methods and hides internal state.
- Achieved by
privatefields andpublicmethods (getters/setters). - Provides data hiding, controlled access, and maintainability.
- Setters can include validation to ensure object integrity.
- Getters should return copies or unmodifiable views for mutable fields.
- Immutable classes (all fields final, no setters) are a strong form of encapsulation.
- Encapsulation is not just about hiding fields; it's about designing a clear contract between a class and its clients.
Encapsulation is the foundation of modular, robust software. By carefully controlling access to internal state, you create classes that are easier to use, test, and evolve.
5. Polymorphism in Java
5.1 What is Polymorphism?
Polymorphism (from Greek poly = many, morph = form) is the ability of an object to take on many forms. In Java, polymorphism allows a single interface (method name, class reference) to be used for different underlying implementations. It is one of the four pillars of object‑oriented programming (along with encapsulation, inheritance, and abstraction).
Polymorphism enables:
- Code reusability – write once, use with many types.
- Flexibility – easily extend systems with new classes.
- Maintainability – changes in one part do not break other parts.
Java supports two types of polymorphism:
- Compile‑time polymorphism (method overloading)
- Runtime polymorphism (method overriding)
5.2 Compile‑Time Polymorphism (Method Overloading)
Method overloading allows a class to have multiple methods with the same name but different parameter lists (number, type, or order of parameters). The appropriate method is determined at compile time based on the arguments passed.
a) Rules for Overloading
- Method name must be the same.
- Parameter list must differ (number, type, or order).
- Return type may be different, but it alone is not sufficient to distinguish overloaded methods.
- Access modifiers can be different.
- Can occur in the same class or in a subclass (but then it's not overriding; it's a separate overload).
b) Examples
c) Varargs and Overloading
Varargs can cause ambiguity if not careful.
d) Return Type Not Considered
5.3 Runtime Polymorphism (Method Overriding)
Method overriding allows a subclass to provide a specific implementation of a method already defined in its superclass. The correct method is chosen at runtime based on the actual object type, not the reference type.
a) Rules for Overriding
- Method signature (name + parameter list) must be exactly the same.
- Return type must be the same or a covariant subtype (Java 5+).
- Access modifier cannot be more restrictive than the superclass method.
finalmethods cannot be overridden.staticmethods are hidden, not overridden.abstractmethods must be overridden unless the subclass is also abstract.- Use
@Overrideannotation to let the compiler verify.
b) Example
c) Covariant Return Types
A subclass can return a subtype of the superclass method's return type.
d) Access Modifiers in Overriding
- Cannot reduce visibility:
public→publiconly;protected→publicorprotected. - Can increase visibility:
protected→publicis allowed.
5.4 Polymorphism with Interfaces and Abstract Classes
Both abstract classes and interfaces enable polymorphic references.
Example with interface:
Example with abstract class:
5.5 Polymorphic References and Casting
A reference variable of a superclass can refer to an object of any subclass. This is a polymorphic reference.
To call methods specific to the subclass, you need downcasting (explicit cast).
instanceof is used to check the actual type before downcasting to avoid ClassCastException.
5.6 Dynamic Method Dispatch
Dynamic method dispatch is the mechanism by which Java decides at runtime which overridden method to execute based on the actual object type, not the reference type.
The decision is made by the JVM at runtime, enabling polymorphic behavior.
5.7 Benefits of Polymorphism
- Flexibility – Write code that works with the superclass type; it automatically works with any subclass.
- Extensibility – New subclasses can be added without modifying existing code (Open/Closed Principle).
- Code reuse – Common logic can be placed in superclass, specialized in subclasses.
- Simplified API – A single method name can handle different types (e.g.,
draw()for shapes).
5.8 Method Overloading vs. Method Overriding
| Feature | Overloading | Overriding |
|---|---|---|
| Purpose | Same name, different parameters | Redefine inherited method |
| Binding | Compile‑time (static) | Runtime (dynamic) |
| Occurs in | Same class or subclass (different signature) | Subclass only (same signature) |
| Return type | May differ, but not used for resolution | Must be same or covariant |
| Access modifier | Can be different | Cannot be more restrictive |
| final method | Can be overloaded | Cannot be overridden |
| static method | Can be overloaded | Cannot be overridden (hidden) |
| private method | Can be overloaded | Cannot be overridden (not inherited) |
5.9 Common Pitfalls and Best Practices
| Pitfall | Explanation / Solution |
|---|---|
| Calling overridden method in constructor | The overridden method may be called before subclass fields are initialized. Avoid calling overridable methods in constructors. |
| Forgetting @Override | Mistakenly creating a new method instead of overriding. Use the annotation. |
| Confusing overloading with overriding | Overloading changes parameters; overriding keeps the same signature. |
| Using instanceof excessively | Overuse may indicate poor design; often polymorphism should handle the logic. |
| Downcasting without checking | Can cause ClassCastException. Always use instanceof (or patten matching for instanceof in newer Java). |
| Returning mutable objects in overridden methods | Breaks encapsulation; return defensive copies or immutable views. |
Best Practices:
- Favor polymorphism over
instanceofand downcasting. - Use
@Overridefor clarity and safety. - Keep method signatures stable when designing for extension.
- Prefer interfaces for polymorphic behavior to allow multiple inheritance of type.
- Design with the Liskov Substitution Principle – subclasses should be substitutable for their superclasses without altering the correctness of the program.
5.10 Complete Example
Output:
5.11 Key Points to Remember
- Polymorphism means “many forms” – a single interface can represent different actual implementations.
- Compile‑time polymorphism = method overloading – same method name, different parameters.
- Runtime polymorphism = method overriding – subclass provides specific implementation.
- Overriding uses dynamic method dispatch: the method of the actual object is called, not the reference type.
- @Override annotation helps prevent mistakes.
- Covariant return types allow a subclass to return a more specific type.
- Polymorphic references (superclass type, subclass object) are the foundation of polymorphic code.
- Use
instanceofsparingly; prefer polymorphic method calls. - Polymorphism promotes loose coupling, extensibility, and maintainability.
Polymorphism is a powerful tool for writing flexible, reusable code. When combined with inheritance and abstraction, it forms the basis of many design patterns and robust software architectures.
6. Abstraction in Java
6.1 What is Abstraction?
Abstraction is the process of hiding implementation details and exposing only essential features to the user. In object‑oriented programming, abstraction allows you to focus on what an object does rather than how it does it.
Java provides two main ways to achieve abstraction:
- Abstract classes – classes that cannot be instantiated and may contain abstract methods (methods without a body).
- Interfaces – contracts that specify a set of methods that implementing classes must provide.
Abstraction helps reduce complexity, increase code reusability, and create well‑defined contracts between different parts of a program.
6.2 Abstract Classes
a) Definition
An abstract class is a class declared with the abstract keyword. It can contain:
- Abstract methods – declared without an implementation (only signature).
- Concrete methods – fully implemented methods.
- Fields (instance variables), constructors, static methods, etc.
b) Syntax
c) Rules for Abstract Classes
- You cannot instantiate an abstract class (no
new). - If a class contains at least one abstract method, it must be declared
abstract. - A subclass of an abstract class must implement all inherited abstract methods, unless it is also
abstract. - Abstract classes can have constructors (called when a concrete subclass is instantiated).
- Abstract classes can have
finalmethods (cannot be overridden) andstaticmethods.
d) Example
Output:
6.3 Interfaces
a) Definition
An interface is a reference type that defines a contract of methods that a class must implement. Prior to Java 8, interfaces contained only public abstract methods and public static final constants. From Java 8 onward, interfaces can also contain:
- Default methods – methods with a default implementation (using
defaultkeyword). - Static methods – methods belonging to the interface.
- Private methods (Java 9+) – to share code between default methods.
b) Syntax
c) Rules for Interfaces
- All fields in an interface are implicitly
public static final. - All methods (unless
default,static, orprivate) are implicitlypublic abstract. - A class implements an interface using the
implementskeyword. - A class can implement multiple interfaces.
- An interface can extend multiple interfaces (using
extends). - Interfaces cannot have constructors.
d) Example
6.4 Abstract Class vs. Interface (Java 8+)
| Feature | Abstract Class | Interface (Java 8+) |
|---|---|---|
| Keyword | abstract | interface |
| Instantiation | Cannot be instantiated | Cannot be instantiated |
| Multiple inheritance | A class can extend only one abstract class | A class can implement many interfaces |
| Fields | Can have instance variables (non‑final) | All fields are public static final (constants) |
| Constructors | Can have constructors | No constructors |
| Method types | Abstract, concrete, static, final | Abstract (default), default, static, private |
| Access modifiers | Can have all (private, protected, etc.) | Methods are public (except private) |
| When to use | When classes share a common base with state | When defining a capability or contract |
6.5 When to Use Abstract Class vs. Interface
Use an abstract class when:
- You have a common base with shared fields or methods that need to be inherited.
- You want to provide a partial implementation that subclasses can extend.
- You need constructors to initialize common state.
- The relationship is a clear is‑a hierarchy (e.g., Vehicle → Car, Motorcycle).
Use an interface when:
- You want to define a contract that unrelated classes can implement (e.g.,
Serializable,Comparable). - You need multiple inheritance of type (a class can implement several interfaces).
- You want to provide a capability (e.g.,
Flyable,Swimmable) that can be mixed into various classes. - You want to leverage default methods to evolve an API without breaking existing implementations.
6.6 Advanced Interface Features
a) Default Methods (Java 8)
Allow adding new methods to interfaces without breaking existing implementing classes. They can be overridden if needed.
b) Static Methods (Java 8)
Belong to the interface, called using the interface name.
c) Private Methods (Java 9)
Used to share common code between default methods, keeping the interface clean.
d) Functional Interfaces
An interface with exactly one abstract method is a functional interface. It can be used with lambda expressions (Java 8+). The @FunctionalInterface annotation is optional but recommended.
6.7 Complete Example: Abstraction in Action
Output:
6.8 Common Pitfalls and Best Practices
| Pitfall | Explanation / Solution |
|---|---|
| Using abstract classes when interfaces suffice | Leads to tight coupling and prevents multiple inheritance. Prefer interfaces for capabilities. |
| Adding abstract methods without considering backward compatibility | Breaks existing subclasses. Use default methods in interfaces to evolve APIs. |
| Misusing default methods | Default methods can cause diamond problem if multiple interfaces provide the same default method. The implementing class must override. |
| Making everything abstract | Not all code needs abstraction; over‑abstraction increases complexity. |
| Forgetting that abstract classes can have constructors | Subclass constructors must call super() to initialize abstract class fields. |
| Using instanceof to check interface implementation | Polymorphism should handle most cases; use instanceof sparingly. |
Best Practices:
- Prefer interfaces to define contracts; use abstract classes only when you need to share code or state.
- Keep interfaces small and focused (Interface Segregation Principle).
- Use
defaultmethods carefully; they should provide a reasonable default that most implementations can use. - Annotate functional interfaces with
@FunctionalInterfaceto enforce single abstract method. - Favor composition over inheritance; use abstraction to define clear boundaries.
6.9 Key Points to Remember
- Abstraction hides implementation details and exposes only the essential behavior.
- Abstract classes – cannot be instantiated; can have fields, constructors, and concrete methods.
- Interfaces – define a contract; from Java 8 onward, can have
default,static, andprivatemethods. - A class can extend only one abstract class but implement multiple interfaces.
- Use abstract classes when classes share a common base with state.
- Use interfaces to define capabilities or to achieve multiple inheritance of type.
- Abstraction enables loose coupling, testability, and maintainability.
Abstraction is a fundamental tool for designing clean, modular Java applications. Mastering abstract classes and interfaces will help you write code that is both flexible and easy to evolve.
