HashMap in Java

Let’s break down how HashMap works internally (Java 8+ implementation).


What Is a HashMap?

HashMap<K, V> is a hash table–based implementation of the Map interface.

It stores data as:

key → value

It provides average O(1) time complexity for:

  • put()

  • get()

  • remove()


Core Data Structure

Internally, HashMap uses:

Array of buckets

Each bucket contains:

  • LinkedList (before Java 8)

  • LinkedList OR Red-Black Tree (Java 8+)

Structure simplified:

Node<K,V>[] table

Each array index is called a bucket.


How put() Works Internally

When you do:

map.put("apple", 10);

Step 1: Calculate Hash

It calls:

int hash = key.hashCode();

Then applies a hash spreading function:

(hash ^ (hash >>> 16))

This improves distribution.


Step 2: Calculate Bucket Index

index = (n - 1) & hash

Where:

  • n = array length

  • & = bitwise AND (faster than modulo)

This determines where the entry will go.


Step 3: Insert into Bucket

Now 3 cases:

Case 1: Bucket is empty

→ Insert new node.

Case 2: Same key already exists

→ Replace value.

Case 3: Collision (different key, same index)

→ Add node to:

  • LinkedList (if small)

  • Red-Black Tree (if too many collisions)


What Is a Collision?

A collision happens when:

Different keys → Same bucket index

Example:

hash("FB") == hash("Ea")

Both go to same bucket.

HashMap handles collisions by chaining.


Treeification (Java 8 Improvement)

If a bucket has more than:

8 nodes

It converts the linked list into a:

Red-Black Tree

Why?

  • LinkedList lookup = O(n)

  • Red-Black Tree lookup = O(log n)

This prevents performance degradation.


How get() Works

When you call:

map.get("apple");

Steps:

  1. Compute hash

  2. Find bucket index

  3. Traverse bucket:

    • If LinkedList → iterate

    • If Tree → binary search

  4. Compare using:

key.equals(existingKey)

Important:
equals() is used to find the correct key — not ==.


Resizing (Rehashing)

Default initial capacity:

16

Default load factor:

0.75

Resize condition:

size > capacity × loadFactor

So:

16 × 0.75 = 12

When 13th element is added:
→ capacity doubles to 32
→ all elements are rehashed


Why hashCode() and equals() Matter

HashMap uses:

  1. hashCode() → to find bucket

  2. equals() → to find exact key inside bucket

If you override one,
you MUST override the other.

Otherwise, HashMap breaks.


Internal Node Structure (Simplified)

static class Node<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

For tree buckets:

TreeNode extends Node

Time Complexity Summary

OperationAverageWorst Case
put()O(1)O(log n)
get()O(1)O(log n)
remove()O(1)O(log n)

Worst case happens when many collisions occur.


Not Thread Safe

HashMap is not synchronized.

For concurrency use:

  • ConcurrentHashMap

  • Collections.synchronizedMap()


Tip

If we ask “How does HashMap work?” we could say in this order:

  1. Array of buckets

  2. hashCode()

  3. Index calculation

  4. Collision handling

  5. Treeification (Java 8)

  6. Resize mechanism

  7. equals() importance


Difference between == and .equals() in Java

In Java, == and .equals() are both used to compare things — but they compare different aspects of objects.


== (Reference Comparison)

== checks whether two references point to the exact same object in memory.

It does NOT check if the contents are equal.

Example:

String a = new String("hello");
String b = new String("hello");

System.out.println(a == b); // false

Even though both contain "hello", they are different objects in memory.

However:

String a = "hello";
String b = "hello";

System.out.println(a == b); // true

Because Java uses the String Pool, both references point to the same object.


.equals() (Content Comparison)

.equals() checks whether two objects are logically equal (same content).

For StringInteger, and many other classes, .equals() is overridden to compare values.

Example:

String a = new String("hello");
String b = new String("hello");

System.out.println(a.equals(b)); // true

Because the contents are the same.


Important Difference Summary

Feature==.equals()
ComparesMemory addressObject content
Works for primitives?✅ Yes❌ No
Works for objects?✅ Yes✅ Yes
Can be overridden?❌ No✅ Yes

Special Case: Primitives

For primitive types (intdoubleboolean, etc.):

int x = 5;
int y = 5;

System.out.println(x == y); // true

== compares actual values.

.equals() cannot be used with primitives.


Important Warning (Null Safety)

String str = null;
str.equals("hello"); // ❌ NullPointerException

Safer way:

"hello".equals(str); // ✅ Safe

Real-World Rule of Thumb

  • Use == for primitives

  • Use .equals() for object value comparison

  • Use == for objects only when you want to check if they are the same instance



Technical Debt

 

What Is Technical Debt?

Technical debt is the future cost you incur when you choose a faster or easier solution now instead of a better long-term one.

The term was popularized by Ward Cunningham.

Think of it like financial debt:

  • You borrow time now (ship faster)

  • You pay interest later (harder changes, more bugs, slower velocity)

Not all technical debt is bad. Sometimes it’s a strategic decision.


Types of Technical Debt

Intentional (Strategic)

You knowingly cut corners to:

  • Hit a deadline

  • Validate a feature

  • Test market demand

This is acceptable — if tracked and repaid.


Unintentional (Accidental)

  • Poor design decisions

  • Lack of experience

  • No architecture thinking

  • Rushed hacks that became permanent

This is dangerous because it grows silently.


Bit Rot / Entropy

  • Dependencies outdated

  • Tests flaky

  • Architecture no longer fits new features

  • Code becomes harder to understand over time

Even good systems accumulate this.


How Technical Debt Hurts a Backend Java System

In backend Java (especially Spring apps), debt often shows as:

  • Huge service classes

  • Tight coupling between layers

  • Massive @Transactional methods

  • Anemic domain models

  • Duplicate validation logic

  • Hard-to-test business logic

  • Slow build times

  • Fear of changing core modules

Symptoms:

  • Each feature takes longer than the previous one

  • Bug rate increases

  • Onboarding new devs is slow

  • Refactoring feels risky

That’s interest compounding.


How to Manage Technical Debt

Make It Visible

Untracked debt becomes permanent.

Track it:

  • Jira tickets labeled “Tech Debt”

  • Architecture backlog

  • Code comments with ticket references (not TODO graveyards)

If it’s not visible, it won’t get prioritized.


Distinguish Good vs Bad Debt

Ask:

  • Did we choose this consciously?

  • Does it still serve a purpose?

  • Is the interest acceptable?

Sometimes debt is fine if:

  • The feature might be removed

  • The system is temporary

  • The cost of fixing exceeds benefit


Pay It Gradually (Best Strategy)

Don’t schedule “Refactor Everything” sprints unless architecture is collapsing.

Instead follow the Boy Scout Rule (from Clean Code):

Leave the code cleaner than you found it.

While adding features:

  • Extract methods

  • Rename variables

  • Reduce duplication

  • Add missing tests

Small continuous improvements beat massive rewrites.


Refactor When Touching the Code

Best moment to reduce debt:

  • When modifying the same module

  • When adding related features

  • When fixing a recurring bug

You already understand that area — cheapest time to improve it.


Protect With Tests First

Never refactor risky areas without:

  • Unit tests

  • Integration tests

Tests reduce refactoring fear.

No tests → write minimal safety tests first.


Measure the Impact

You don’t manage what you don’t measure.

Signals of growing debt:

  • Deployment frequency decreasing

  • Lead time increasing

  • Rising bug count

  • Growing PR size

  • Developers avoiding certain modules

If velocity drops over time, debt may be the cause.


Avoid the Rewrite Trap

Full rewrites are often:

  • Emotionally satisfying

  • Technically risky

  • Business dangerous

Most large rewrites fail because:

  • Requirements change

  • Hidden complexity emerges

  • New system accumulates new debt

Prefer incremental refactoring.


Practical Backend Java Example

Bad debt pattern:

@Transactional
public void processOrder(OrderRequest request) {
   validate(request);
   User user = userRepository.findById(...);
   inventoryService.checkStock(...);
   paymentService.charge(...);
   emailService.send(...);
}

Over time:

  • More conditionals added

  • More dependencies injected

  • Harder to test

  • Bugs appear in edge cases

Managing debt here might mean:

  • Extracting orchestration layer

  • Moving business rules into domain model

  • Introducing events

  • Reducing transaction scope

  • Writing focused unit tests


When to Pay Debt

Pay it when:

  • It slows feature delivery

  • It causes recurring bugs

  • It blocks architectural evolution

  • It increases onboarding time

  • The interest > business value gained

Don’t pay it when:

  • Code is stable and rarely touched

  • System will be deprecated soon

  • Business priorities are elsewhere


Senior Engineer Mindset

Technical debt isn’t a failure.

It’s:

  • A trade-off decision

  • A business conversation

  • A risk management problem

The real problem is unmanaged debt.


When to Refactor a Code

Refactoring isn’t about rewriting code because it feels “ugly.” It’s about improving design without changing behavior—at the right time.

Here’s when you decide to refactor a code.


1. When You See “Code Smells”

Refactor when you notice structural problems that make the code harder to maintain:

  • Duplication – Same logic repeated in multiple places

  • Long methods – Functions doing too many things

  • Large classes – One class handling multiple responsibilities

  • Deep nesting / complex conditionals

  • Poor naming – Variables like datatempx

  • Shotgun surgery – Small feature change requires editing many files

  • Feature envy – A method using another class’s data more than its own

If the code makes you hesitate before modifying it, that’s a signal.


2. The Best Time: While Adding or Changing Features

The ideal moment to refactor is:

When you're already touching the code.

This follows the Boy Scout Rule (popularized in Clean Code by Robert C. Martin):

“Leave the code cleaner than you found it.”

Why?

  • You already understand that part of the system.

  • You reduce future friction.

  • You avoid large risky rewrites.


3. When Change Is Becoming Expensive

Ask yourself:

  • Does every small change feel risky?

  • Are bugs appearing in unexpected places?

  • Are tests hard to write?

  • Does onboarding new developers take too long?

If yes → the design is slowing you down → refactor.


4. When You Have Tests

Refactor only when behavior is protected by tests.

If there are no tests:

  1. Write tests first.

  2. Then refactor.

Without tests, refactoring becomes guessing.


5. When Complexity Is Growing Faster Than Value

Sometimes code “works fine” but is getting harder to understand.

A useful heuristic:

  • If it takes longer to understand the code than to write it → refactor.

  • If adding a simple feature requires major mental overhead → refactor.


When NOT to Refactor

  • Right before a critical release

  • Without understanding the system

  • Just for aesthetic reasons

  • In stable legacy code that rarely changes

  • When there’s no test safety net

Refactoring is a tool—not a hobby.


A Practical Rule of Thumb

Refactor when at least one of these is true:

  1. You are modifying the code anyway.

  2. The code is actively slowing development.

  3. The design blocks new features.

  4. Bugs keep recurring in the same area.


Strategic vs Opportunistic Refactoring

Opportunistic – Small improvements during feature work
Strategic – Planned refactoring sprint to fix structural issues

Use strategic refactoring when:

  • Architecture is the bottleneck

  • Technical debt is measurable and costly

  • The team agrees it's needed


Simple Decision Framework

Ask:

  1. Does it work?

  2. Is it hard to change?

  3. Will it need to change again?

If:

  • Works + won’t change → leave it.

  • Works + will change → refactor.

  • Doesn’t work → fix first, then refactor.


Here’s when you should refactor in a Java backend, and what that usually looks like in practice.


1. Your Service Class Is Doing Too Much

Smell:

A single UserService:

  • Validates input

  • Talks to repository

  • Maps DTOs

  • Handles transactions

  • Sends emails

  • Logs metrics

That violates Single Responsibility Principle (from Clean Code by Robert C. Martin*).

Refactor when:

  • Methods exceed ~30–40 lines

  • Constructor has 6+ dependencies

  • Unit tests are painful to write

Fix:

Split into:

  • UserValidator

  • UserMapper

  • UserRepository

  • EmailService

  • Keep UserService as orchestrator


2. Duplicate Logic Across Controllers or Services

Smell:

Same validation or mapping repeated in:

  • UserController

  • AdminUserController

  • BatchUserProcessor

Refactor when:

You copy-paste code even once.

Fix:

Extract shared component:

@Component
public class UserValidator { ... }

Duplication spreads bugs.


3. Complex Conditionals (Especially in Business Logic)

Smell:

if (type.equals("A")) {
   ...
} else if (type.equals("B")) {
   ...
} else if (type.equals("C")) {
   ...
}

Refactor when:

Adding a new type requires editing this method.

Fix:

Use polymorphism or strategy pattern.

Example:

interface PricingStrategy {
    BigDecimal calculate(Order order);
}

Add new implementations instead of editing existing logic.


4. Anemic Domain Model (Very Common in Spring Apps)

If your entities are just:

class Order {
   private BigDecimal amount;
   private String status;
}

And all logic is in services:

→ That’s a sign to refactor.

Move behavior into the domain:

public void cancel() {
   if (status.equals("SHIPPED")) {
       throw new IllegalStateException();
   }
   this.status = "CANCELLED";
}

Refactor when business rules are scattered everywhere.


5. Tests Are Hard to Write

If unit tests require:

  • 5 mocks

  • Complex setup

  • Spring context loading

That’s a design smell.

Refactor toward:

  • Constructor injection

  • Small classes

  • Pure functions when possible

If you need @SpringBootTest for simple logic → refactor.


6. Transaction Boundaries Are Confusing

Common backend issue:

@Transactional
public void processOrder() {
   updateInventory();
   chargeCard();
   sendEmail();
}

If failures cause inconsistent states:

→ Time to refactor.

Split orchestration and side effects.
Consider domain events.


7. Repository Layer Is Leaking Into Business Logic

If you see this in many services:

userRepository.findById(id).orElseThrow(...)

Repeated everywhere?

Refactor into:

public User getRequiredUser(Long id)

Encapsulate repository behavior.


8. Adding Features Feels Risky

Ask yourself:

  • Does adding one endpoint require touching 5 files?

  • Does changing DB schema break many services?

  • Are regressions common?

If yes → refactor before adding more features.


When NOT to Refactor for Java Backend

Don’t refactor when:

  • System is stable and rarely changed

  • You’re days before production release

  • No tests exist

  • It’s legacy code no one touches


Practical Rule for Backend Java

Refactor when:

  • Class > 300 lines

  • Method > 40 lines

  • Constructor > 5 dependencies

  • Cyclomatic complexity feels hard to reason about

  • You fear touching the code


Senior Developer Heuristic

If reading a method requires scrolling → refactor.
If understanding a change requires jumping across many classes → refactor.
If business rules are not obvious → refactor.



Abstraction vs Encapsulation

 In Java, abstraction and encapsulation are both core Object-Oriented Programming (OOP) concepts, but they serve different purposes.



Abstraction

What it means:

Abstraction is hiding implementation details and showing only essential features.

It focuses on what an object does, not how it does it.


How it’s achieved in Java:

  • abstract classes

  • interfaces


Java Example (Abstraction using interface)

interface Payment {
    void pay(double amount);  // only method declaration (what to do)
}

class CreditCardPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

public class Main {
    public static void main(String[] args) {
        Payment payment = new CreditCardPayment();
        payment.pay(100);
    }
}


Explanation:

  • The Payment interface defines what should be done (pay()).

  • The implementation details (how payment is processed) are hidden inside CreditCardPayment.

  • The user of Payment doesn’t need to know how it works internally.



Encapsulation

What it means:

Encapsulation is wrapping data (variables) and methods together and restricting direct access to some of the object’s components.

It focuses on data protection.


How it’s achieved in Java:

  • private variables

  • Public getters and setters


Java Example (Encapsulation)

class BankAccount {
    private double balance;  // hidden data

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(500);
        System.out.println(account.getBalance());
    }
}


Explanation:

  • balance is private → cannot be accessed directly.

  • Access is controlled through deposit() and getBalance().

  • This protects the object’s internal state.



 Key Differences

AbstractionEncapsulation
Hides implementation detailsHides internal data
Focuses on behavior (what)Focuses on data protection
Achieved using interfaces & abstract classesAchieved using access modifiers
Design-level conceptImplementation-level concept


Simple Analogy

  • Abstraction → Driving a car: You use the steering wheel and pedals without knowing how the engine works.

  • Encapsulation → The car’s engine is hidden under the hood so you can’t directly modify its parts.


HashMap in Java

Let’s break down how   HashMap   works internally (Java 8+ implementation). What Is a HashMap? HashMap<K, V>  is a  hash table–based  ...