Skip to main content

Section 3.5 Encapsulation & Abstraction

Think about organizing a game’s code. There are two ways to do it:

Subsection 3.5.1 Two Ways: Keep or Package Pieces

Way 1: Keep pieces and rules separate (Traditional Approach)
// Everything is scattered and global
class GameState {
  // Data just floating around
  static int playerHealth = 100;
  static int monsterHealth = 100;
  static int potionCount = 3;

  // Rules are separate from data
  static void healPlayer(int amount) {
    playerHealth += amount;    // Which player's health?
    potionCount--;            // Easy to forget this!
  }

  static void healMonster(int amount) {
    monsterHealth += amount;   // Copy-pasted code
  }

  public static void main(String[] args) {
    // Easy to make mistakes:
    playerHealth = -50;        // Negative health?
    potionCount = healPlayer(20);  // Wrong return type!
    healMonster(monsterHealth);    // Wrong parameter!
  }
}
Way 2: Package pieces with their rules
      // Organize things into classes
      // We define our own data types
public class Player {
    private int health = 100;

    public void heal(int amount) {
        health += amount;  // This Player's health
    }
}
  // Organize things into classes
  // We have a separate class for running the program
  class GameState {
 
  public static void main(String[] args) {
    Player hero = new Player(20);
    Player villan = new Player(10);

   // Clear which player we're modifying
    hero.heal(20);
    villan.heal(5);
  }
}
The second way is called Object-Oriented Programming (OOP). It bundles data with the code that works on that data. Java helps us do this through:
  1. Encapsulation: Protect data from invalid changes
    private int health;  // Only accessible through methods
    
  2. Abstraction: Hide complex details behind simple commands
    player.heal(5);     // Don't care HOW it heals
    
Let’s learn to write code this way!

Subsection 3.5.2 Beyond Simple Classes

Our Player class works, but it has a problem: anyone can break the rules. In a real game, this could be disastrous:
Player hero = new Player(100, 50);

// During gameplay:
hero.health = -999;     // Player becomes invincible!
hero.gold = 1000000;    // Instant millionaire...
monster.health = 0;     // Kill without fighting
We need three things:
  1. Protection: Stop direct access to data
    private int health;  // Now hero.health = -999 won't compile!
    
  2. Controls: Provide safe ways to modify data
    public void heal(int amount) {
        if (amount > 0) {         // Only positive healing allowed
            health += amount;     // Safely change health
        }
    }
    
  3. Rules: Enforce game mechanics
    public void takeDamage(int amount) {
        health = Math.max(0, health - amount);  // Never go below 0
    }
    
This is where OOP shines: we can make the compiler help enforce our rules. Let’s see how!

Subsection 3.5.3 Understanding Encapsulation

Encapsulation has two key parts:
  1. Bundling related data and methods together in a class
  2. Protecting that data so it can only be changed in valid ways
When we encapsulate properly:
  • Related code stays together (easier to understand)
  • Data can only be changed through methods (maintains validity)
  • Implementation details can be changed without affecting other code
Let’s see how Java helps us achieve this:

Subsubsection 3.5.3.2 Protection: Making Sure Data Changes Safely

public class Player {
    private int health;  // Only Player methods can change this

    // Safe way to take damage - health can't go below 0
    public void takeDamage(int amount) {
        if (amount > health) {
            health = 0;        // Player is defeated
        } else {
            health = health - amount;
        }
    }
}
Now players can’t have negative health, which might break game rules!

Subsubsection 3.5.3.3 Interface: Making Your Class Easy to Use

BankAccount myAccount = new BankAccount("12345");
myAccount.deposit(100);     // Put money in
myAccount.withdraw(50);     // Take money out
double money = myAccount.getBalance();  // Check how much you have
Compare to trying to work with raw data:
myAccount.balance = myAccount.balance + 100;  // Easy to make mistakes!
Good method names act like clear labels on buttons!

Subsubsection 3.5.3.4 Invariants: Rules That Must Always Be True

public class Player {
  private int health;
  private int maxHealth = 100;

  public void setHealth(int newHealth) {
    // Check all our rules:
    if (newHealth < 0) {
      health = 0;
    } else if (newHealth > maxHealth) {
      health = maxHealth;
    } else {
      health = newHealth;
    }
  }
}
These rules are always enforced—no accidental breakage!
Think about a vending machine:
  • It bundles the snacks, prices, and coin slot together
  • It protects the snacks behind glass
  • It has a simple interface (insert money, press B4)
  • It maintains rules (no money = no snack)
When we write classes this way:
  • Related data and code stay together
  • Data can’t be changed incorrectly
  • Other code has clear ways to interact
  • Rules are always followed
Now you’re not just hiding data—you’re building a reliable, easy-to-use machine!

Subsection 3.5.4 Different Ways to Enforce Rules

When protecting data, we have several tools:
  1. Constructor Checks: Stop invalid objects from being created
    public Player(int startHealth) {
        if (startHealth < 0) {
            startHealth = 1;  // Fix invalid input
        }
        health = startHealth;
    }
    
  2. Method Guards: Fix invalid inputs
    public void heal(int amount) {
        if (amount < 0) {
            amount = 0;  // Ignore negative healing
        }
        health += amount;
    }
    
  3. Range Enforcement: Keep values in bounds
    public void setHealth(int newHealth) {
        health = Math.max(0, Math.min(newHealth, MAX_HEALTH));
    }
    
  4. Complete Rejection: Return success/failure
    public boolean withdraw(int amount) {
        if (amount > balance) {
            return false;  // Can't withdraw more than you have
        }
        balance -= amount;
        return true; // success
    }
    
Choose based on what makes sense for your situation.

Subsection 3.5.5 Abstraction: What Users Need vs. What Code Does

Imagine walking into a restaurant:
  • You order “a cheeseburger”
  • Kitchen handles complex details (grill temp, cooking time, etc.)
  • You don’t need to know HOW they make it
That’s abstraction in code:
public class Restaurant {
    // Store kitchen details inside the class (encapsulation)
    private double grillTemp;   // Current temperature of grill
    private int burgerCount;    // How many burgers we can make
    private int grillSpace;     // How many burgers fit on grill
    
    // Simple interface for customers:
    public boolean orderCheeseburger() {
        // First check if we can make the burger
        if (burgerCount <= 0) {
            return false;    // Can't make burger - out of ingredients
        }
        
        // Complex steps hidden from customer:
        setGrillTemp(375);              // Heat the grill
        boolean success = cookPatty();   // Try to cook the burger
        
        if (success) {
            burgerCount = burgerCount - 1;  // Use up ingredients
            return true;                     // Burger ready!
        }
        return false;                        // Something went wrong
    }
    
    // Helper method - customers don't need to see this
    private boolean cookPatty() {
        if (grillSpace > 0) {           // If there's room on grill
            grillSpace = grillSpace - 1; // Take up one spot
            return true;                 // Cooking successful
        }
        return false;                    // Grill is full
    }
}

// Customer just needs to know:
Restaurant store = new Restaurant();
boolean gotBurger = store.orderCheeseburger();  // Don't care about the details!
Abstraction lets us:
  1. Hide complex details (grill, inventory, cooking steps)
  2. Show simple interfaces (just “orderCheeseburger”)
  3. Change implementation without affecting users
Why is this different from encapsulation?
  • Encapsulation: Protects data from invalid changes
  • Abstraction: Hides complexity behind simple interfaces
Together they create reliable, easy-to-use code!
public class Player {
    private int health;            // Encapsulation
    private double healBonus;      // (protect the data)

    public void heal(int amount) { // Abstraction
        // Complex healing logic hidden from users:
        applyHealingBuffs();
        checkStatusEffects();
        updateHealth(amount);      // Users don't see these steps
    }
}

Subsection 3.5.6 Looking Ahead: More OOP Features

You might notice some repetition in our game:
class Player {
    private int health;
    public void takeDamage(int amount) { ... }
}

class Monster {
    private int health;
    public void takeDamage(int amount) { ... }
}

class Building {
    private int health;
    public void takeDamage(int amount) { ... }
}
Writing the same health/damage code for every destructible thing seems wasteful! OOP has two more powerful features:
  1. Inheritance: Write the health/damage code once, share it
  2. Polymorphism: Treat Players, Monsters, and Buildings the same when dealing damage
We’ll explore these later. For now, focus on:
  • Encapsulation: Protect your data (like private health)
  • Abstraction: Hide complex details (like how damage is calculated)
These fundamentals will prepare you for advanced features ahead!

Subsection 3.5.7 Encapsulation & Abstraction in Practice

Let’s build a BankAccount that’s both safe and easy to use. We’ll add features step by step:
public class BankAccount {
  // Step 1: Protect the data (Encapsulation)
  private double balance;
  private double interestRate;

  // Step 2: Control object creation
  public BankAccount(double initBalance) {
    if (initBalance < 0) {
      initBalance = 0;  // Start at 0 if negative
    }
    this.balance = initBalance;
    this.interestRate = 0.01;  // 1% interest
  }

  // Step 3: Simple interface, complex implementation (Abstraction)
  public boolean deposit(double amount) {
    // Validate input
    if (amount < 0) {
      return false;  // Can't deposit negative amounts
    }

    // Update balance
    balance += amount;
    return true; // success
  }

  public boolean withdraw(double amount) {
    // Validate input
    if (amount < 0 || amount > balance) {
      return false;  // Invalid amount
    }

    // Update balance
    balance -= amount;
    return true;
  }

  // Step 4: Safe way to check balance
  public double getBalance() {
    return balance;
  }

  // Step 5: Hidden interest calculation
  private void addInterest() {
    balance += balance * interestRate;
  }
}
Try using it:
BankAccount acct = new BankAccount(100);
acct.deposit(50);   // Simple to use!
What’s happening behind the scenes?
  1. Input validation
  2. Balance update
  3. Transaction logging
  4. Interest calculation
  5. Owner notification
But users don’t need to know any of that! They just see:
  • deposit(amount) → money goes in
  • withdraw(amount) → money comes out
  • getBalance() → check total
This is the power of combining encapsulation and abstraction:
  • Encapsulation keeps the data safe
  • Abstraction keeps the usage simple

Subsection 3.5.8 From Design Recipe to OOP

Remember the design recipe steps?
  1. Data definitions
  2. Constraints
  3. Methods that work with that data
OOP helps enforce these:
// Design Recipe says: "health must be ≥ 0"
public class Player {
  private int health;   // Data definition + protection

  public void heal(int amt) {  // Methods enforce constraints
    if (amt < 0) return;     // No negative healing
    health = Math.min(100,   // Can't exceed max
             health + amt);
  }
}
The compiler becomes your ally:
  • Private fields → Can’t break data definitions
  • Methods → Enforce constraints automatically
  • Tests → Verify that rules work

Subsection 3.5.9 What’s Next: Objects in Memory

When we do:
Player hero = new Player(100);
Player backup = hero;
backup.health -= 50;  // Does this affect hero?
We need to understand:
  • How Java stores objects
  • What happens when we copy them
  • How method calls work with objects
That’s our next topic: Object References!

Subsection 3.5.10 Exercises: Encapsulation & Abstraction

Checkpoint 3.5.1. Multiple Choice: Encapsulation vs. Abstraction.

Which statement best distinguishes encapsulation from abstraction?
  • Encapsulation protects data by bundling it with methods and restricting direct access, while abstraction hides complex implementation details behind a simpler interface.
  • Exactly. Encapsulation is about bundling and protection, whereas abstraction is about hiding complexity.
  • Encapsulation means we cannot write constructors; abstraction means we cannot have any private fields.
  • Incorrect. Constructors and private fields are independent of these definitions.
  • Encapsulation forces all data to be global, while abstraction forbids using methods longer than 5 lines.
  • No. Encapsulation encourages hiding data, not making it global, and there’s no rule about method line limits.
  • There is no real difference. Both terms refer to exactly the same concept in Java.
  • They are related but distinct concepts.

Checkpoint 3.5.2. True/False: Private Fields.

    Marking a field private means code outside the class cannot access it directly, forcing all modifications through the class’s methods.
  • True.

  • Correct! Making a field private enforces that only the class itself can read or write that field, which is a key part of encapsulation.
  • False.

  • Correct! Making a field private enforces that only the class itself can read or write that field, which is a key part of encapsulation.

Checkpoint 3.5.3. Short Answer: Handling Negative Health.

In the section, we saw that a Player class can prevent negative health by restricting access to its health field. In 1–2 sentences, explain how you would enforce the “health ≥ 0” rule if someone tries p1.setHealth(-999) or p1.takeDamage(1000).
Solution.
Sample Solution: I’d make health private and provide methods like setHealth or takeDamage that check if the new health would be negative, then set it to 0 instead. This ensures outsiders can’t directly write p1.health = -999, preserving the invariant “health ≥ 0.”

Checkpoint 3.5.4. Parsons: Encapsulate the Player’s Data.

Below is a short snippet of “scattered” code for a game. Rearrange the following lines to create a single, valid Player class with private fields and methods heal and takeDamage enforcing “health ≥ 0.”
Hint: This code should compile as one class. There is only one correct arrangement of braces and method definitions.

Checkpoint 3.5.5. Multiple Choice: Ways to Enforce Constraints.

In “Different Ways to Enforce Rules,” we saw strategies like constructor checks, method guards, and range enforcement. Which approach forcibly rejects or denies an invalid operation, often returning success/failure as a boolean?
  • Constructor Checks
  • Constructor checks can fix or adjust invalid data at creation, but they don’t inherently “reject” an operation with a boolean return.
  • Range Enforcement
  • Range enforcement might clamp or adjust the data, but it’s not necessarily rejecting with success/failure.
  • Complete Rejection (returning success/failure)
  • Right! “Complete Rejection” is where a method returns false if the operation fails, rather than automatically fixing or adjusting it.
  • Method Guards
  • Method guards can correct or clamp input, but they don’t always return success/failure.

Checkpoint 3.5.6. Reflection: OOP vs. Non-OOP Approaches.

Think back to the “Two Ways: Keep or Package Pieces” example. In 2–4 sentences, explain how making a Player class with private data and methods (like heal) helps prevent the mistakes we saw in the scattered code (where direct global variables were easy to misuse).
Solution.
Sample Solution: A Player class keeps all relevant data and logic together, so we can’t just accidentally do playerHealth = -999 or pass the wrong variable order. private fields force changes through safe methods that enforce “health ≥ 0.” This cuts down on guesswork and makes the code clearer by grouping data and actions in one place.
You have attempted of activities on this page.