Designing Reusable Object-Oriented Class Structures in Java
Course: Core Java Training
Topic: Designing Reusable Object-Oriented Class Structures
Course Link: Core Java Training Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to Reusable Object-Oriented Class Structures
Designing reusable object-oriented class structures means creating Java classes that can be used in different parts of an application or in multiple applications without repeatedly rewriting the same logic. A reusable class has a clear responsibility, protects its data, exposes appropriate methods, and works effectively with other classes.
Object-oriented programming (OOP) organizes software around classes and objects. A class defines data and behavior, while an object is an instance created from that class. Concepts such as encapsulation, abstraction, inheritance, and polymorphism help developers organize code into understandable and reusable components.
For example, an e-commerce application might contain classes such as Product, Customer, ShoppingCart, and Order. Each class manages a specific responsibility and can cooperate with the others to complete a business operation.
A good reusable design avoids unnecessary duplication, limits dependencies between classes, and makes future changes easier. Java's object-oriented programming learning resources provide additional information about classes, objects, inheritance, and interfaces.
2. Learning Objectives
After studying this topic, you should be able to:
- Understand reusable class structures in Java.
- Identify the responsibilities of classes and objects.
- Apply encapsulation and abstraction to improve class design.
- Use inheritance and polymorphism when appropriate.
- Understand composition, aggregation, and association.
- Design reusable interfaces and abstract classes.
- Reduce code duplication and unnecessary dependencies.
- Apply basic SOLID principles to object-oriented design.
- Organize related classes using Java packages.
- Build a practical reusable class structure for a business application.
3. What Is a Reusable Class?
A reusable class is a class designed to perform a well-defined responsibility so that other parts of a program can use it without copying its implementation.
For example, a DiscountCalculator class could calculate a discount for different products and orders. Instead of repeating the discount formula throughout the application, developers can call a method from the same class wherever the calculation is needed.
Example: Discount Calculator
class DiscountCalculator {
public double calculateDiscount(double price, double percentage) {
if (price < 0 || percentage < 0 || percentage > 100) {
throw new IllegalArgumentException("Invalid price or percentage");
}
return price * percentage / 100;
}
}
public class Main {
public static void main(String[] args) {
DiscountCalculator calculator = new DiscountCalculator();
double discount1 = calculator.calculateDiscount(1000, 10);
double discount2 = calculator.calculateDiscount(2000, 15);
System.out.println("First discount: " + discount1);
System.out.println("Second discount: " + discount2);
}
}
Output
First discount: 100.0
Second discount: 300.0
Explanation
DiscountCalculator has one clear responsibility.
- The calculation logic is written once and reused for different prices.
- The method validates its inputs before calculating the discount.
- The class can be used by checkout, billing, or reporting code without duplicating the formula.
4. Characteristics of a Good Reusable Class
- Single responsibility: The class focuses on one closely related area of functionality.
- Encapsulation: Internal state is protected from uncontrolled access.
- Clear public API: Public methods are easy to understand and use.
- Low coupling: The class depends on as few implementation details of other classes as practical.
- High cohesion: Its fields and methods contribute to a common purpose.
- Validation: Invalid inputs are handled consistently.
- Extensibility: The design allows appropriate changes without requiring widespread modifications.
- Testability: The class can be tested independently or through well-defined collaborators.
5. Understanding Classes and Objects
A class is a blueprint that defines the data and behavior of objects. An object is a concrete instance of a class, created using the new keyword for ordinary classes.
Example
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void displayDetails() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
}
}
public class Main {
public static void main(String[] args) {
Student student1 = new Student("Aarav", 20);
Student student2 = new Student("Priya", 22);
student1.displayDetails();
student2.displayDetails();
}
}
Output
Name: Aarav
Age: 20
Name: Priya
Age: 22
The Student class defines the structure, while student1 and student2 refer to separate objects with their own state.
Reusability begins when one class definition can create many objects without requiring a separate class for every student.
6. Applying Encapsulation for Reusable Design
Encapsulation combines data and related behavior within a class while controlling how other code accesses its internal state. It commonly uses private fields and public methods.
Without encapsulation, external code may change an object's fields directly and place the object in an invalid state. A reusable class should protect important rules, also called invariants, through its constructors and methods.
Example: Bank Account
class BankAccount {
private String accountHolder;
private double balance;
public BankAccount(String accountHolder, double openingBalance) {
if (accountHolder == null || accountHolder.isBlank()) {
throw new IllegalArgumentException("Account holder is required");
}
if (openingBalance < 0) {
throw new IllegalArgumentException("Opening balance cannot be negative");
}
this.accountHolder = accountHolder;
this.balance = openingBalance;
}
public String getAccountHolder() {
return accountHolder;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive");
}
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal must be positive");
}
if (amount > balance) {
return false;
}
balance -= amount;
return true;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("Neha", 5000);
account.deposit(1000);
account.withdraw(1500);
System.out.println("Account holder: " + account.getAccountHolder());
System.out.println("Balance: " + account.getBalance());
}
}
Output
Account holder: Neha
Balance: 4500.0
Why This Design Is Reusable
- The balance cannot be modified directly by unrelated classes.
- Deposit and withdrawal rules are centralized.
- The same class can represent many bank accounts.
- The implementation can evolve without requiring client code to manipulate the balance field directly.
7. Abstraction and Clear Public Interfaces
Abstraction means exposing essential operations while hiding unnecessary implementation details. It helps users of a class understand what they can do without needing to know how every operation works internally.
For example, an application can call paymentProcessor.processPayment() without handling every low-level step involved in communicating with a payment provider.
Example: Payment Interface
interface PaymentProcessor {
boolean processPayment(double amount);
}
class DemoPaymentProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
System.out.println("Processing payment of Rs. " + amount);
return true;
}
}
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new DemoPaymentProcessor();
processor.processPayment(1200);
}
}
Output
Processing payment of Rs. 1200.0
The caller depends on the PaymentProcessor contract rather than needing to know the concrete class's internal implementation. The example uses a demonstration processor, not a real payment integration.
8. Inheritance and Code Reuse
Inheritance allows a subclass to extend one superclass using the extends keyword. The subclass can reuse accessible behavior from its parent and add or specialize functionality.
Inheritance is useful when a genuine is-a relationship exists. For example, a Car is a type of Vehicle. It is less appropriate when the relationship is merely that one object uses another.
Example: Vehicle Hierarchy
class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public void start() {
System.out.println(brand + " vehicle is starting");
}
}
class Car extends Vehicle {
public Car(String brand) {
super(brand);
}
public void openTrunk() {
System.out.println("Car trunk opened");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car("Toyota");
car.start();
car.openTrunk();
}
}
Output
Toyota vehicle is starting
Car trunk opened
The Car class reuses the constructor and start() behavior from Vehicle and adds its own operation.
When to Use Inheritance
- There is a clear subtype relationship.
- The subclass can honor the behavior promised by the superclass.
- Shared behavior is genuinely common to the parent type.
- The hierarchy is not being created solely to avoid a small amount of code duplication.
9. Composition: Reuse Through Collaboration
Composition is a design technique in which one object contains a reference to another object and delegates part of its work to that collaborator. It represents a has-a relationship.
Composition often provides more flexibility than inheritance because a class can reuse a collaborator without becoming a subtype of that collaborator's class.
Example: Car and Engine
class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private final Engine engine;
public Car(Engine engine) {
if (engine == null) {
throw new IllegalArgumentException("Engine is required");
}
this.engine = engine;
}
public void startCar() {
engine.start();
System.out.println("Car is ready");
}
}
public class Main {
public static void main(String[] args) {
Engine engine = new Engine();
Car car = new Car(engine);
car.startCar();
}
}
Output
Engine started
Car is ready
Explanation
- The
Engine class manages engine-related behavior.
- The
Car class uses an Engine rather than inheriting from it.
- The engine is supplied through the constructor, making the dependency explicit.
- The engine implementation can be changed later if the design uses an appropriate abstraction.
Composition is particularly useful for reusable services such as logging, notifications, validation, reporting, and data access.
10. Association, Aggregation, and Composition
These terms describe relationships between objects and help developers decide how classes should collaborate.
| Relationship |
Meaning |
Example |
| Association |
Two objects know about or interact with each other. |
A teacher teaches a student. |
| Aggregation |
A whole references parts that can exist independently. |
A department has employees who can exist independently of that department. |
| Composition |
A whole owns its component as part of its internal structure or lifecycle model. |
An order contains order-line objects as part of its structure. |
The exact lifecycle relationship depends on the application's domain and implementation. These terms are conceptual design tools, not merely different Java keywords.
11. Using Interfaces to Build Flexible Class Structures
Interfaces define contracts that can be implemented by multiple classes. They help reusable designs avoid depending on one specific implementation.
Example: Notification Service
interface NotificationService {
void send(String recipient, String message);
}
class EmailNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
System.out.println("Email to " + recipient + ": " + message);
}
}
class SmsNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
System.out.println("SMS to " + recipient + ": " + message);
}
}
class AlertManager {
private final NotificationService notificationService;
public AlertManager(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void sendAlert(String recipient, String message) {
notificationService.send(recipient, message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService service = new EmailNotificationService();
AlertManager manager = new AlertManager(service);
manager.sendAlert("[email protected]", "Your order is ready");
}
}
Output
Email to [email protected]: Your order is ready
The AlertManager class depends on the NotificationService interface. To use SMS instead, the application can provide an SmsNotificationService object without changing the core alert logic.
12. Polymorphism and Reusable Methods
Polymorphism allows different objects to be used through the same superclass or interface type. When an overridden instance method is called, the implementation corresponding to the actual object is selected at runtime.
Example: Shape Calculator
interface Shape {
double calculateArea();
}
class Circle implements Shape {
private final double radius;
public Circle(double radius) {
if (radius <= 0) {
throw new IllegalArgumentException("Radius must be positive");
}
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private final double length;
private final double width;
public Rectangle(double length, double width) {
if (length <= 0 || width <= 0) {
throw new IllegalArgumentException("Dimensions must be positive");
}
this.length = length;
this.width = width;
}
@Override
public double calculateArea() {
return length * width;
}
}
public class Main {
static void printArea(Shape shape) {
System.out.printf("Area: %.2f%n", shape.calculateArea());
}
public static void main(String[] args) {
printArea(new Circle(3));
printArea(new Rectangle(4, 5));
}
}
Output
Area: 28.27
Area: 20.00
The printArea() method can work with any suitable Shape implementation. A new shape can be introduced without writing a new version of the method for each shape type.
13. Avoiding Code Duplication
Code duplication occurs when the same business rule or operation is implemented in several places. If the rule changes, developers must find and update every copy, increasing the chance of inconsistent behavior.
Example of Duplicate Logic
class OnlineOrder {
double calculateTax(double price) {
return price * 0.18;
}
}
class StoreOrder {
double calculateTax(double price) {
return price * 0.18;
}
}
Both classes repeat the same tax calculation. If the application needs a different rate, each copy may need to be updated.
Improved Design
class TaxCalculator {
public double calculateTax(double price, double taxRate) {
if (price < 0 || taxRate < 0) {
throw new IllegalArgumentException("Values cannot be negative");
}
return price * taxRate;
}
}
class OnlineOrder {
private final TaxCalculator taxCalculator;
public OnlineOrder(TaxCalculator taxCalculator) {
this.taxCalculator = taxCalculator;
}
public double calculateTax(double price, double taxRate) {
return taxCalculator.calculateTax(price, taxRate);
}
}
class StoreOrder {
private final TaxCalculator taxCalculator;
public StoreOrder(TaxCalculator taxCalculator) {
this.taxCalculator = taxCalculator;
}
public double calculateTax(double price, double taxRate) {
return taxCalculator.calculateTax(price, taxRate);
}
}
Both order classes reuse one calculator. In a production application, the tax rate should come from an appropriate business rule or configuration source rather than being arbitrarily duplicated across the application.
14. Single Responsibility Principle
The Single Responsibility Principle (SRP) states that a class should have one cohesive responsibility and one primary reason to change.
For example, a class that calculates an invoice, saves it to a database, sends an email, and formats a PDF has several unrelated responsibilities. Changes to storage or email delivery could affect the same class that contains billing rules.
Less Maintainable Design
class InvoiceManager {
public void calculateTotal() {
System.out.println("Calculating invoice total");
}
public void saveToDatabase() {
System.out.println("Saving invoice");
}
public void sendEmail() {
System.out.println("Sending invoice email");
}
}
Improved Design
class InvoiceCalculator {
public double calculateTotal(double subtotal, double tax) {
return subtotal + tax;
}
}
class InvoiceRepository {
public void save() {
System.out.println("Saving invoice");
}
}
class InvoiceEmailService {
public void send() {
System.out.println("Sending invoice email");
}
}
Each class has a more focused responsibility. The application can change email delivery without changing invoice calculation logic.
15. Open/Closed Principle
The Open/Closed Principle states that software components should be open for extension but closed for modification in the sense that new behavior should often be added through new implementations rather than repeatedly changing stable code.
Example: Discount Strategies
interface DiscountStrategy {
double applyDiscount(double price);
}
class NoDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double price) {
return price;
}
}
class TenPercentDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double price) {
return price * 0.90;
}
}
class Checkout {
private final DiscountStrategy discountStrategy;
public Checkout(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double getFinalPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
return discountStrategy.applyDiscount(price);
}
}
public class Main {
public static void main(String[] args) {
Checkout checkout = new Checkout(new TenPercentDiscount());
System.out.println(checkout.getFinalPrice(1000));
}
}
Output
900.0
A new discount strategy can be added as another implementation of DiscountStrategy. The checkout class does not need a new conditional branch for every new strategy.
16. Dependency Inversion and Dependency Injection
The Dependency Inversion Principle encourages high-level business logic to depend on abstractions instead of concrete implementation details. Dependency injection is a technique for supplying an object's dependencies from outside rather than constructing every dependency internally.
Example
interface MessageSender {
void sendMessage(String message);
}
class ConsoleMessageSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println(message);
}
}
class ReminderService {
private final MessageSender sender;
public ReminderService(MessageSender sender) {
this.sender = sender;
}
public void remind(String message) {
sender.sendMessage(message);
}
}
public class Main {
public static void main(String[] args) {
MessageSender sender = new ConsoleMessageSender();
ReminderService service = new ReminderService(sender);
service.remind("Meeting starts at 10 AM");
}
}
Output
Meeting starts at 10 AM
The reminder service does not create its own sender. It receives one through its constructor, making the dependency visible and easier to replace during testing.
17. Prefer Composition Over Inheritance When Appropriate
Composition is often preferable when a class needs to use another object's behavior without representing a subtype of that object. Inheritance is useful for genuine subtype relationships, while composition provides flexible collaboration between independent components.
| Aspect |
Inheritance |
Composition |
| Relationship |
Is-a |
Has-a or uses-a |
| Mechanism |
extends |
Object references and delegation |
| Reuse |
Reuses inherited accessible behavior |
Reuses behavior through a collaborator |
| Flexibility |
Can create strong coupling to a superclass |
Can allow collaborator implementations to be changed |
| Example |
Car extends Vehicle |
Car contains an Engine |
Do not avoid inheritance entirely. Choose the relationship that accurately represents the domain and produces the clearest design.
18. Designing Immutable Classes
An immutable object cannot have its observable state changed after construction. Immutability can simplify reusable class design because callers do not need to worry about unexpected changes to the object's state.
Example: Product
final class Product {
private final String name;
private final double price;
public Product(String name, double price) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Product name is required");
}
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
public class Main {
public static void main(String[] args) {
Product product = new Product("Keyboard", 1500);
System.out.println(product.getName());
System.out.println(product.getPrice());
}
}
Output
Keyboard
1500.0
The class is declared final, its fields are private final, and it exposes no mutator methods. Because its fields are immutable values, callers cannot change the product's name or price after construction.
19. Organizing Reusable Classes with Packages
Packages group related Java classes and interfaces into namespaces. They help organize large projects, reduce naming conflicts, and define package-level access boundaries.
Example Package Structure
com.store.model
Product.java
Customer.java
Order.java
com.store.service
CheckoutService.java
DiscountService.java
com.store.repository
ProductRepository.java
OrderRepository.java
com.store.notification
NotificationService.java
EmailNotificationService.java
Declaring a Package
package com.store.model;
public class Product {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
Other classes can import com.store.model.Product and use its public API. In normal Java source organization, the package declaration appears at the beginning of the source file, before import declarations.
20. Designing Class Relationships
Before creating classes, identify the application's main concepts and how they interact. Avoid creating one large class that performs every operation.
For an online shopping system, a possible structure is:
| Class or Interface |
Responsibility |
Product |
Stores product details such as name and price. |
Customer |
Represents a customer and relevant customer information. |
ShoppingCart |
Manages selected products and quantities. |
Order |
Represents a placed order and its line items. |
DiscountStrategy |
Defines the discount calculation contract. |
PaymentProcessor |
Defines payment-processing behavior. |
CheckoutService |
Coordinates the checkout workflow. |
OrderRepository |
Defines how orders are saved and retrieved. |
NotificationService |
Defines customer notification behavior. |
This structure separates data models, business logic, external operations, and coordination responsibilities. Each class can be developed and tested more independently.
21. Practical Project: Reusable Shopping Cart System
Project objective: Build a small Java shopping cart that uses reusable classes, encapsulation, composition, and interfaces.
Step 1: Create the Product Class
class Product {
private final String name;
private final double price;
public Product(String name, double price) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
Step 2: Create the ShoppingCart Class
import java.util.ArrayList;
import java.util.List;
class ShoppingCart {
private final List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
if (product == null) {
throw new IllegalArgumentException("Product is required");
}
products.add(product);
}
public double calculateTotal() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}
public void displayProducts() {
for (Product product : products) {
System.out.println(product.getName() + ": Rs. " + product.getPrice());
}
}
}
Step 3: Create the Main Class
public class Main {
public static void main(String[] args) {
Product keyboard = new Product("Keyboard", 1500);
Product mouse = new Product("Mouse", 700);
ShoppingCart cart = new ShoppingCart();
cart.addProduct(keyboard);
cart.addProduct(mouse);
cart.displayProducts();
System.out.println("Total: Rs. " + cart.calculateTotal());
}
}
Output
Keyboard: Rs. 1500.0
Mouse: Rs. 700.0
Total: Rs. 2200.0
Project Explanation
Product represents an individual product and protects its fields.
ShoppingCart stores and manages multiple product objects.
- The cart uses composition-like collaboration by holding references to product objects.
- The total calculation is implemented once and can be reused.
- The classes can be extended with quantities, discounts, checkout, and persistence as new requirements emerge.
This is an educational example. A production shopping cart would also need to consider quantities, currency and rounding rules, product availability, pricing changes, and checkout validation.
22. Unit Testing Reusable Classes
Reusable classes should behave consistently across different callers. Unit tests help verify their behavior independently of the rest of the application.
Example: Testing a Calculator Method
class Calculator {
public int add(int first, int second) {
return first + second;
}
}
A simple test can verify that the method returns the expected value:
public class CalculatorTestExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
int actual = calculator.add(10, 20);
int expected = 30;
if (actual != expected) {
throw new AssertionError("Expected " + expected + ", got " + actual);
}
System.out.println("Test passed");
}
}
Output
Test passed
This is a basic assertion example rather than a full unit-testing framework. Larger Java projects commonly use JUnit to organize tests, setup, assertions, and test execution.
23. Common Mistakes in Class Design
| Mistake |
Why It Is a Problem |
Better Approach |
| Making every field public |
External code can change state without validation. |
Keep fields private and expose suitable methods. |
| Creating one giant class |
Unrelated responsibilities become difficult to maintain. |
Separate cohesive responsibilities into different classes. |
| Using inheritance only to reuse code |
It may create an incorrect subtype relationship and tight coupling. |
Consider composition or delegation. |
| Duplicating business rules |
Copies can become inconsistent when requirements change. |
Centralize shared rules in a suitable class or service. |
| Depending on concrete implementations everywhere |
Changing implementations requires widespread edits. |
Use interfaces and dependency injection where appropriate. |
| Creating too many tiny classes without purpose |
Unnecessary abstraction can make the code harder to follow. |
Create classes when they represent a clear concept or responsibility. |
| Ignoring input validation |
Objects can enter invalid states. |
Validate constructor arguments and method inputs. |
| Using unclear method names |
Callers cannot easily understand the public API. |
Use descriptive names that express intent. |
24. Best Practices for Reusable Object-Oriented Design
- Start by identifying the domain concepts and responsibilities.
- Give each class a clear purpose and cohesive methods.
- Keep implementation details private unless they must be exposed.
- Use constructors to establish valid initial state.
- Prefer immutable fields and objects when practical.
- Use inheritance only when a genuine subtype relationship exists.
- Prefer composition when a class needs to collaborate with another object.
- Define interfaces around meaningful contracts.
- Inject replaceable dependencies rather than constructing them everywhere.
- Reduce duplication, but avoid abstractions that do not solve a real problem.
- Organize related classes into meaningful packages.
- Write unit tests for important business rules.
- Document public methods and any important assumptions.
- Refactor when design problems become evident rather than predicting every possible future requirement.
25. Practice Exercises
Exercise 1: Employee Management
Create an Employee class with private fields for name, employee ID, and salary. Add validation and methods to display employee details.
Exercise 2: Reusable Tax Calculator
Create a TaxCalculator class with a method that accepts a price and tax rate. Validate inputs and test it with multiple values.
Exercise 3: Notification System
Create a NotificationService interface and implement email and SMS notification classes. Pass the implementation into a reusable alert manager.
Exercise 4: Composition
Create a Car class that uses an Engine object. Make the engine a constructor dependency and delegate the startup operation to it.
Exercise 5: Shopping Cart
Extend the shopping cart project to support product quantities, subtotal calculations, and a reusable discount strategy.
Exercise 6: Refactoring
Take a class that calculates totals, saves data, and sends messages. Split it into cohesive classes and explain how each new class improves maintainability.
26. Interview Questions and Answers
Q1. What is a reusable class in Java?
Answer: A reusable class provides a clearly defined responsibility that can be used in multiple contexts without copying its implementation.
Q2. What are the main principles of object-oriented programming?
Answer: The four commonly taught pillars are encapsulation, abstraction, inheritance, and polymorphism.
Q3. What is encapsulation?
Answer: Encapsulation combines data and behavior in a class and controls access to internal state, often using private fields and public methods.
Q4. What is abstraction?
Answer: Abstraction exposes essential behavior while hiding unnecessary implementation details.
Q5. What is the difference between inheritance and composition?
Answer: Inheritance represents an is-a relationship, while composition represents a has-a relationship in which an object uses or contains another object.
Q6. Why is composition often preferred over inheritance?
Answer: Composition can reduce coupling to a superclass and make collaborators easier to replace. Inheritance remains appropriate for genuine subtype relationships.
Q7. What is polymorphism?
Answer: Polymorphism allows different object types to be used through a common superclass or interface, with overridden instance methods selected at runtime.
Q8. What is an interface?
Answer: An interface defines a contract that implementing classes agree to support.
Q9. What is the Single Responsibility Principle?
Answer: It recommends giving a class one cohesive responsibility and one primary reason to change.
Q10. What is the Open/Closed Principle?
Answer: It encourages designs that support adding new behavior through extensions, such as new implementations, without repeatedly modifying stable existing code.
Q11. What is dependency injection?
Answer: Dependency injection supplies an object's dependencies from outside, commonly through a constructor or setter, instead of requiring the object to construct every dependency itself.
Q12. Why should fields often be private?
Answer: Private fields prevent unrelated code from modifying an object's state directly and allow the class to enforce validation rules.
Q13. What is high cohesion?
Answer: High cohesion means that a class's data and methods are closely related to its primary responsibility.
Q14. What is low coupling?
Answer: Low coupling means that classes have fewer unnecessary dependencies on the internal details of other classes.
Q15. How do packages support reusable design?
Answer: Packages organize related classes and interfaces into namespaces, helping manage larger applications and reduce naming conflicts.
27. Quick Revision Checklist
- A class defines the structure and behavior of its objects.
- Reusable classes provide clear, cohesive responsibilities.
- Encapsulation protects internal state.
- Abstraction exposes essential behavior through a clear API.
- Inheritance supports reuse through valid subtype relationships.
- Composition supports reuse through collaboration and delegation.
- Interfaces define contracts for interchangeable implementations.
- Polymorphism allows code to work with different implementations through common types.
- SOLID principles help guide maintainable class design.
- Packages organize classes into logical groups.
- Validation helps objects maintain valid state.
- Unit tests help verify reusable behavior consistently.
28. Conclusion
Designing reusable object-oriented class structures is an essential Core Java skill. Good designs organize data and behavior into cohesive classes, protect internal state, reduce unnecessary dependencies, and reuse functionality through appropriate inheritance, composition, and interfaces.
Reusable code is not simply code that appears in several places. It is code with a clear purpose, a stable and understandable contract, and behavior that can be used safely in different contexts. By practicing encapsulation, polymorphism, dependency injection, and sound class relationships, you can build Java applications that are easier to test, maintain, and extend.
Learn More with JustAcademy
Continue learning Java programming and object-oriented design through the Core Java Training Course.
To explore the course and training options, register for a course demo.
Visit the JustAcademy website for additional courses and learning resources.