08 - Encapsulation and Access Modifiers
Learn how to protect and control access to your class data.
Encapsulation is about hiding the internal details of a class and controlling how its data is accessed and modified. This prevents bugs and makes your code more maintainable.
The Problem Without Encapsulation
public class Player {
public String name;
public int health;
public int maxHealth;
}
public class Main {
public static void main(String[] args) {
Player player = new Player();
player.health = 100;
player.maxHealth = 100;
// Oops! Someone can break the rules
player.health = 500; // Health over maximum!
player.health = -50; // Negative health!
player.name = ""; // Empty name!
}
}Without protection, anyone can set invalid values!
Access Modifiers
Java has keywords that control who can access your class members:
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (none) | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
For now, focus on:
public- Anyone can accessprivate- Only this class can access
Making Properties Private
public class Player {
private String name;
private int health;
private int maxHealth;
public Player(String name, int maxHealth) {
this.name = name;
this.health = maxHealth;
this.maxHealth = maxHealth;
}
}Now you can't access properties directly:
Player player = new Player("Alice", 100);
player.health = 500; // ❌ Error! health is privateGetters and Setters
To access private properties, create getter and setter methods:
public class Player {
private String name;
private int health;
private int maxHealth;
public Player(String name, int maxHealth) {
this.name = name;
this.health = maxHealth;
this.maxHealth = maxHealth;
}
// Getter - returns the value
public int getHealth() {
return health;
}
// Setter - sets the value with validation
public void setHealth(int health) {
if (health < 0) {
this.health = 0;
} else if (health > maxHealth) {
this.health = maxHealth;
} else {
this.health = health;
}
}
public String getName() {
return name;
}
public int getMaxHealth() {
return maxHealth;
}
}Now you can safely interact with the object:
Player player = new Player("Alice", 100);
player.setHealth(150); // Automatically capped at 100
System.out.println(player.getHealth()); // 100
player.setHealth(-20); // Automatically set to 0
System.out.println(player.getHealth()); // 0Follow Java naming conventions:
- Getter:
get+ property name (capitalized) - Setter:
set+ property name (capitalized) - Boolean:
is+ property name (capitalized)
private int health;
public int getHealth() { }
public void setHealth(int health) { }
private boolean alive;
public boolean isAlive() { }
public void setAlive(boolean alive) { }
private String name;
public String getName() { }
public void setName(String name) { }Benefits of Encapsulation
1. Validation
public class Item {
private int durability;
private int maxDurability;
public void setDurability(int durability) {
if (durability < 0) {
this.durability = 0;
} else if (durability > maxDurability) {
this.durability = maxDurability;
} else {
this.durability = durability;
}
}
public boolean isBroken() {
return durability <= 0;
}
}2. Read-Only Properties
Sometimes you don't want a setter:
public class Monster {
private String id; // Should never change
private int health;
public Monster(String id, int health) {
this.id = id;
this.health = health;
}
// Getter only - no setter!
public String getId() {
return id;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
}3. Computed Properties
Getters don't have to return a field directly:
public class Player {
private int health;
private int maxHealth;
public int getHealth() {
return health;
}
// Computed property
public double getHealthPercentage() {
return (health * 100.0) / maxHealth;
}
// Computed property
public boolean isLowHealth() {
return getHealthPercentage() < 25;
}
}Practical Examples
Item with Durability
public class Tool {
private String name;
private int durability;
private int maxDurability;
private boolean broken;
public Tool(String name, int maxDurability) {
this.name = name;
this.durability = maxDurability;
this.maxDurability = maxDurability;
this.broken = false;
}
public void use() {
if (broken) {
System.out.println(name + " is broken!");
return;
}
durability--;
System.out.println(name + " used. Durability: " + durability);
if (durability <= 0) {
broken = true;
System.out.println(name + " broke!");
}
}
public void repair() {
durability = maxDurability;
broken = false;
System.out.println(name + " repaired!");
}
// Getters
public String getName() {
return name;
}
public int getDurability() {
return durability;
}
public boolean isBroken() {
return broken;
}
public double getDurabilityPercentage() {
return (durability * 100.0) / maxDurability;
}
}Bank Account Example
public class PlayerWallet {
private int gold;
private int silver;
public PlayerWallet() {
this.gold = 0;
this.silver = 0;
}
public void addGold(int amount) {
if (amount > 0) {
gold += amount;
System.out.println("Added " + amount + " gold");
}
}
public boolean spendGold(int amount) {
if (amount > gold) {
System.out.println("Not enough gold!");
return false;
}
gold -= amount;
System.out.println("Spent " + amount + " gold");
return true;
}
public int getGold() {
return gold;
}
public int getTotalValue() {
// 1 gold = 100 silver
return gold * 100 + silver;
}
}Protected Block System
public class ProtectedBlock {
private int x, y, z;
private String type;
private String owner;
private boolean locked;
public ProtectedBlock(int x, int y, int z, String type, String owner) {
this.x = x;
this.y = y;
this.z = z;
this.type = type;
this.owner = owner;
this.locked = true;
}
public boolean canBreak(String playerName) {
if (!locked) {
return true;
}
return playerName.equals(owner);
}
public void unlock(String playerName) {
if (playerName.equals(owner)) {
locked = false;
System.out.println("Block unlocked");
} else {
System.out.println("You don't own this block!");
}
}
// Getters only - position and owner shouldn't change
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public String getOwner() {
return owner;
}
public boolean isLocked() {
return locked;
}
}When to Use Private vs Public
Make it private by default! Only make things public if they need to be accessed from outside.
Private:
- Internal data (health, position, inventory)
- Helper methods used only within the class
- Anything that needs validation
Public:
- Methods that define the class's behavior
- Constructor
- Methods other classes need to call
public class Example {
// Private - internal data
private int internalCounter;
private String secretKey;
// Public - part of the interface
public void doSomething() {
// Uses private helper method
validateData();
}
// Private - internal helper
private void validateData() {
// ...
}
}The final Keyword
final means a variable can't be changed after it's set:
public class Player {
private final String id; // Can't change after creation
private String name; // Can change
private int health; // Can change
public Player(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
// No setId() - it's final!
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}The static Keyword
Static Members
A class can define two kinds of members:
-
Instance members — owned by each object (each instance has its own copy).
-
Static members — owned by the class (one shared copy for the entire type).
Put simply: instance members belong to objects; static members belong to the class itself and are shared by all objects of that type.
Declaration
/* (access modifier) */ static ... memberName; Example
class Data {
public int x; // Instanced member
public static int y = 1000; // Static member
// Instanced member:
// can access to both static and non-static members
public void foo() {
x = 100; // OK - same as this.x = 100;
y = 100; // OK - same as Data.y = 200;
}
// Static member:
// cannot access to non-static variables
public static void bar() {
x = 100; // Error: non-static variable x cannot be renference from a static context
y = 100; // OK
}
}Accessing static members
Data data = new Data();
data.x = 1000; // OK
data.y = 1000; // OK-ish - not really suggested; it's better to use Data.y
Data.y = 1000; // OK - best practice
Data.x = 1000; // Error: cannot access instanced variables in a static contextStatic Fields
A static field represents a data member owned by the class type rather then the object. Static fields are also stored in a specific memory location that's been shared between all the object instances that are created.
It is declared as following:
/* (access modifier) (optional) */ static /* final/volatile (optional) */ fieldName;Let's take the same Data class example and add this constructor:
public Data() {
y++; // remember that's the same as Data.y++;
}// Every instance of Data will have a private copy of the instanced member x
// However it will point to the same location in memory for the member y
Data d1 = new Data(); // y = 1001
d1.x = 5;
Data d2 = new Data(); // y = 1002
d2.x = 25;
Data d3 = new Data(); // y = 1003
// ... and so onStatic Methods
Static methods essentially represent a function member of a certain class type
From the Data class remember the function (instanced method) foo and (static method) bar
One can access those methods via:
Data d1 = new Data();
d1.foo(); // Instanced method: Accessible ONLY by an object
Data.bar(); // Static method: accessible without an objectStatic Initializer
Use a static initializer block to run initialization logic when the class is first loaded:
class OtherData {
private static int a = 12;
private static int b;
private static String msg;
static {
msg = "Initialization..."
System.out.println(msg);
b = 4;
// ... complex initialization that can't be done in a single expression
}
}Practice Exercises
-
Create a
BankAccountClass:- Private properties: accountNumber, balance
- Constructor to set account number
- Methods: deposit(), withdraw(), getBalance()
- Validation: can't withdraw more than balance
- Account number should be read-only
-
Create a
DoorClass:- Private properties: isLocked, keyCode
- Constructor to set the key code
- Methods: lock(), unlock(String code), isLocked()
- unlock() only works with correct code
- Code should be private (don't expose it!)
-
Create a
PlayerStatsClass:- Private properties: strength, defense, speed
- Constructor to set all stats
- Getters for all stats
- Method: getPowerLevel() that returns strength + defense + speed
- Stats can't be negative or over 100
-
Refactor a Class: Take one of your classes from the previous lesson and add proper encapsulation:
- Make all properties private
- Add appropriate getters and setters
- Add validation where needed
Last updated on