π Premium Read: Access my best content on Medium member-only articles β deep dives into Java, Spring Boot, Microservices, backend architecture, interview preparation, career advice, and industry-standard best practices.
β Some premium posts are free to read β no account needed. Follow me on Medium to stay updated and support my writing.
π Top 10 Udemy Courses (Huge Discount): Explore My Udemy Courses β Learn through real-time, project-based development.
βΆοΈ Subscribe to My YouTube Channel (172K+ subscribers): Java Guides on YouTube
This guide covers the top 10+ best practices, focusing on SOLID principles and OOP (Object-Oriented Programming) concepts to write better Java code.
1οΈβ£ Follow SOLID Principles for Better Code Design
The SOLID principles improve code maintainability and extensibility by reducing tight coupling.
β Best Practice: Follow all five SOLID principles when designing your classes.
S: Single Responsibility Principle (SRP)
A class should have only one reason to change.
πΉ Example: Correcting SRP Violation
// β Bad Practice: One class handling multiple responsibilities
class Order {
void calculateTotal() { /* Business logic */ }
void printInvoice() { /* UI logic */ }
void saveToDatabase() { /* Persistence logic */ }
}
// β
Good Practice: Separating responsibilities into different classes
class Order {
double calculateTotal() { return 100.0; }
}
class InvoicePrinter {
void print(Order order) { /* UI logic */ }
}
class OrderRepository {
void save(Order order) { /* Persistence logic */ }
}
π‘ Each class has a single responsibility, making it easier to maintain.
O: Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification.
πΉ Example: Avoiding Direct Modification in a Shape Class
// β Bad Practice: Modifying existing code every time we add a new shape
class Shape {
String type;
}
// β
Good Practice: Using polymorphism for extension
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
private double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {
private double width, height;
Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override
double area() { return width * height; }
}
π‘ New shapes can be added without modifying existing code.
L: Liskov Substitution Principle (LSP)
Subclasses should be replaceable without affecting the program.
πΉ Example: Preventing LSP Violations
// β Bad Practice: Violating Liskov by altering expected behavior
class Bird {
void fly() { System.out.println("Flying"); }
}
class Penguin extends Bird {
@Override
void fly() { throw new UnsupportedOperationException("Penguins can't fly!"); }
}
// β
Good Practice: Separating behaviors properly
interface FlyingBird {
void fly();
}
class Sparrow implements FlyingBird {
public void fly() { System.out.println("Flying"); }
}
class Penguin { /* Penguins donβt extend FlyingBird */ }
π‘ A subclass should never alter the expected behavior of a superclass.
I: Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
πΉ Example: Avoiding Interface Pollution
// β Bad Practice: Forcing all birds to implement fly()
interface Bird {
void eat();
void fly();
}
// β
Good Practice: Segregating interfaces
interface FlyingBird {
void fly();
}
interface NonFlyingBird {
void walk();
}
class Crow implements FlyingBird {
public void fly() { System.out.println("Flying"); }
}
class Ostrich implements NonFlyingBird {
public void walk() { System.out.println("Walking"); }
}
π‘ Each class only implements what it actually needs.
D: Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
πΉ Example: Avoiding Tight Coupling
// β Bad Practice: High-level class depends on a low-level concrete class
class MySQLDatabase {
void connect() { System.out.println("Connected to MySQL"); }
}
class Application {
private MySQLDatabase database = new MySQLDatabase(); // Tight coupling
}
// β
Good Practice: Using an interface for abstraction
interface Database {
void connect();
}
class MySQLDatabase implements Database {
public void connect() { System.out.println("Connected to MySQL"); }
}
class Application {
private Database database;
Application(Database database) { this.database = database; }
void start() { database.connect(); }
}
π‘ Now, the Application
class can work with any Database
implementation.
2οΈβ£ Use Encapsulation to Protect Data
Encapsulation hides internal details and ensures that the object state is only modified through controlled methods.
β Best Practice: Declare fields private and provide getter and setter methods.
πΉ Example: Proper Encapsulation
class BankAccount {
private double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
π‘ Encapsulation helps maintain data integrity and prevents unintended modifications.
3οΈβ£ Prefer Composition Over Inheritance
Inheritance can create deep, inflexible hierarchies. Composition provides more flexibility and avoids tight coupling.
β Best Practice: Use Has-A relationships instead of Is-A when appropriate.
πΉ Example: Using Composition Instead of Inheritance
class Engine {}
class Car {
private Engine engine; // Has-A relationship
Car(Engine engine) {
this.engine = engine;
}
}
π‘ Composition promotes reusability and avoids deep inheritance chains.
4οΈβ£ Favor Interfaces Over Abstract Classes
Why?
Interfaces provide better flexibility and support multiple inheritance, unlike abstract classes.
β Best Practice: Use interfaces for behavioral contracts and abstract classes for common functionality.
πΉ Example: Using Interfaces for Flexibility
interface Flyable {
void fly();
}
class Bird implements Flyable {
public void fly() {
System.out.println("Bird is flying.");
}
}
π‘ Prefer interfaces when defining behaviors shared across multiple unrelated classes.
5οΈβ£ Avoid Using Static Methods in OOP Design
Why?
Static methods break encapsulation and make unit testing difficult. They should be used sparingly for utility functions only.
β Best Practice: Use dependency injection instead of static methods.
πΉ Example: Avoiding Static Methods for Business Logic
// β Bad Practice: Using static methods
class UserService {
public static void registerUser(String username) {
System.out.println(username + " registered.");
}
}
// β
Good Practice: Using dependency injection
class UserService {
void registerUser(String username) {
System.out.println(username + " registered.");
}
}
π‘ Static methods should be used mainly for utility classes like Math
or Collections
.
6οΈβ£ Use Polymorphism to Write Flexible Code
Why?
Polymorphism allows different implementations to be used interchangeably, making the code more maintainable.
β Best Practice: Rely on method overriding and dynamic method dispatch for flexibility.
πΉ Example: Using Polymorphism Correctly
class Animal {
void makeSound() {
System.out.println("Some sound...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal myPet = new Dog();
myPet.makeSound(); // Output: Bark!
}
}
π‘ Polymorphism makes your code more extendable and avoids unnecessary conditionals.
7οΈβ£ Follow Proper Naming Conventions
Why?
Readable names improve maintainability and make code self-explanatory.
β Best Practice: Use meaningful names for classes, methods, and variables.
πΉ Example: Proper Naming Conventions
// β Bad Practice
int x;
void doSomething() {}
// β
Good Practice
int accountBalance;
void processTransaction() {}
π‘ Follow Java naming conventions to improve code readability.
8οΈβ£ Use final
Keyword Where Necessary
Why?
The final
keyword helps prevent unintended modifications and enhances code stability.
β
Best Practice: Use final
for constants, method parameters, and immutable classes.
πΉ Example: Using final
Effectively
final class Constants {
public static final double PI = 3.14159;
}
π‘ Mark fields as final
when their values should not change after initialization.
9οΈβ£ Use toString()
, equals()
, and hashCode()
Properly
Why?
Overriding these methods ensures correct object comparisons and debugging output.
β
Best Practice: Always override toString()
, equals()
, and hashCode()
in custom classes.
πΉ Example: Implementing toString()
and equals()
class Person {
private String name;
public Person(String name) { this.name = name; }
@Override
public String toString() {
return "Person{name='" + name + "'}";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return name.equals(person.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
π‘ Overriding these methods makes debugging and object comparisons easier.
π Write Unit Tests for Object-Oriented Code
Why?
Unit testing ensures reliability and helps detect bugs early.
β Best Practice: Use JUnit or TestNG to test classes and methods.
πΉ Example: Writing a Simple JUnit Test
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class BankAccountTest {
@Test
void depositShouldIncreaseBalance() {
BankAccount account = new BankAccount(100);
account.deposit(50);
assertEquals(150, account.getBalance());
}
}
π‘ Testing ensures code correctness and prevents regression issues.
1οΈβ£1οΈβ£ Use Dependency Injection Instead of Hard-Coded Dependencies
Hard-coded dependencies make your code difficult to test and maintain. Dependency Injection (DI) provides flexibility and testability.
β Best Practice: Inject dependencies instead of instantiating them inside a class.
πΉ Example: Constructor-Based Dependency Injection
class EmailService {
void sendEmail(String message) {
System.out.println("Email sent: " + message);
}
}
class NotificationService {
private EmailService emailService;
// β
Injecting dependency via constructor
NotificationService(EmailService emailService) {
this.emailService = emailService;
}
void notifyUser(String message) {
emailService.sendEmail(message);
}
}
π‘ This allows easy swapping of implementations and better testing.
1οΈβ£2οΈβ£ Follow DRY (Don't Repeat Yourself) Principle
DRY reduces redundancy and improves code maintainability.
β Best Practice: Extract repeated logic into reusable methods or classes.
πΉ Example: Applying DRY Principle
// β Bad Practice: Duplicate logic
class OrderService {
void placeOrder() {
System.out.println("Validate order...");
System.out.println("Process payment...");
System.out.println("Send notification...");
}
}
// β
Good Practice: Extracting into separate reusable methods
class OrderService {
void placeOrder() {
validateOrder();
processPayment();
sendNotification();
}
private void validateOrder() { System.out.println("Validate order..."); }
private void processPayment() { System.out.println("Process payment..."); }
private void sendNotification() { System.out.println("Send notification..."); }
}
π‘ DRY keeps your code modular and easier to update.
π Summary: Top 12 Best Practices for Java OOP
β
Follow SOLID principles for better code design.
β
Use encapsulation to protect data integrity.
β
Prefer composition over inheritance for flexibility.
β
Favor interfaces over abstract classes for behavior definition.
β
Avoid static methods in OOP-based code.
β
Leverage polymorphism to make code more flexible.
β
Follow proper naming conventions for clarity.
β
Use final
to prevent unintended modifications.
β
Override toString()
, equals()
, and hashCode()
.
β
Write unit tests to validate OOP code.
β
Use dependency injection to decouple classes.
β
Follow the DRY principle to avoid redundancy.
These best practices will help you write better, scalable, and maintainable Java applications. ππ₯
Comments
Post a Comment
Leave Comment