08 — Encapsulamento e modificadores de acesso
Aprenda a proteger e controlar o acesso aos dados para as suas classes.
O encapsulamento consiste em ocultar os detalhes internos de uma classe e determinar como os seus dados podem ser acessados ou modificados. Isso evita que erros aconteçam e o seu código se torna mais simples de manter e atualizar.
O problema sem o encapsulamento
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;
// Ops! Alguém pode quebrar as regras
player.health = 500; // Vida excedente!
player.health = -50; // Vida negativa!
player.name = ""; // Sem nome!
}
}Sem proteção, qualquer um pode definir valores inválidos!
Modificadores de acesso
O Java tem palavras-chave que controlam quem pode acessar os membros da sua classe:
| Modificador | Classe | Pacote | Subclasse | Mundo |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (nenhum) | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Por enquanto, concentre-se em:
public— Qualquer pessoa pode acessarprivate— Apenas essa classe pode acessar
Tornando propriedades privadas
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;
}
}Não é mais possível acessar as propriedades diretamente:
Player player = new Player("Alice", 100);
player.health = 500; // ❌ Erro! Health é privadoMétodos Getters e Setters
Para acessar propriedades privadas, crie os métodos getter e setter:
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 – retorna o valor
public int getHealth() {
return health;
}
// Setter – define o valor com validação
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;
}
}Agora você pode interagir com segurança com o objeto:
Player player = new Player("Alice", 100);
player.setHealth(150); // Limitado automaticamente a 100
System.out.println(player.getHealth()); // 100
player.setHealth(-20); // Definido automaticamente para 0
System.out.println(player.getHealth()); // 0Siga as convenções de nomenclatura do Java:
- Getter:
get+ nome da propriedade (capitalizado); - Setter:
set+ nome da propriedade (capitalizado); - Boolean:
is+ nome da propriedade (capitalizado).
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) { }Vantagens do encapsulamento
1. Validação
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. Propriedades apenas de leitura
Às vezes, você não quer um setter:
public class Monster {
private String id; // Deve permanecer inalterável
private int health;
public Monster(String id, int health) {
this.id = id;
this.health = health;
}
// Apenas getter, sem setter!
public String getId() {
return id;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
}3. Propriedades calculadas
Os métodos Getters não precisam retornar um campo diretamente:
public class Player {
private int health;
private int maxHealth;
public int getHealth() {
return health;
}
// Propriedade calculada
public double getHealthPercentage() {
return (health * 100.0) / maxHealth;
}
// Propriedade calculada
public boolean isLowHealth() {
return getHealthPercentage() < 25;
}
}Exemplos práticos
Item com durabilidade
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 + " está quebrado(a)!");
return;
}
durability--;
System.out.println(name + " usado(a). Durabilidade: " + durability);
if (durability <= 0) {
broken = true;
System.out.println(name + " quebrou!");
}
}
public void repair() {
durability = maxDurability;
broken = false;
System.out.println(name + " reparado(a)!");
}
// Getters
public String getName() {
return name;
}
public int getDurability() {
return durability;
}
public boolean isBroken() {
return broken;
}
public double getDurabilityPercentage() {
return (durability * 100.0) / maxDurability;
}
}Exemplo de conta bancária
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(amount + "x ouro(s) adicionado(s)");
}
}
public boolean spendGold(int amount) {
if (amount > gold) {
System.out.println("Ouro insuficiente!");
return false;
}
gold -= amount;
System.out.println(amount + "x ouro(s) usado(s)");
return true;
}
public int getGold() {
return gold;
}
public int getTotalValue() {
// 1 ouro = 100 pratas
return gold * 100 + silver;
}
}Sistema de proteção de bloco
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("Bloco desbloqueado");
} else {
System.out.println("Você não é o dono deste bloco!");
}
}
// Somente Getters — a posição e owner não devem mudar
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;
}
}Quando usar Private e Public
Make it private by default! Only make things public if they need to be accessed from outside.
Private:
- Dados internos (health, position, inventory);
- Métodos de ajuda usados apenas na classe;
- Tudo que precise de validação.
Public:
- Métodos que definem o comportamento da classe;
- Construtor;
- Métodos que outras classes precisam utilizar.
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() {
// ...
}
}A palavra-chave final
final means a variable can't be changed after it's set:
public class Player {
private final String id; // Deve permanecer inalterado após ser criado
private String name; // Alterável
private int health; // Alterável
public Player(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
// Não há setId() — termina aqui!
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}A palavra-chave static
Membros estáticos
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.
Declaração
/* (modificador de acesso) */ static ... memberName; Exemplo
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 contextCampos estáticos
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.
É declarado o seguinte:
/* (modificador de acesso) (opcional) */ static /* final/volatile (opcional) */ 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 onMétodos estáticos
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 objectInicializador estático
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 = "Inicializando..."
System.out.println(msg);
b = 4;
// ... inicialização complexa que não pode ser feita em uma única expressão
}
}Exercícios práticos
-
Crie uma classe
BankAccount:- Propriedades privadas:
accountNumber,balance; - Construtor para definir o número da conta;
- Métodos:
deposit(),withdraw(),getBalance(); - Validação: não é possível sacar mais do que o saldo;
- O número da conta deve ser apenas leitura.
- Propriedades privadas:
-
Crie uma classe
Door:- Propriedades privadas:
isLocked,keyCode; - Construtor para definir o código-chave;
- Métodos:
lock(),unlock(String code),isLocked(); - O método
unlock()funciona apenas com o código correto; - O código deve ser
private(não o exponha!).
- Propriedades privadas:
-
Crie uma classe
PlayerStats:- Propriedades privadas:
strength,defense,speed; - Construtor para definir todas as estatísticas;
- Getters para todas as estatísticas;
- Método:
getPowerLevel()que retornastrength+defense+speed; - As estatísticas não podem ser negativas ou superiores a 100.
- Propriedades privadas:
-
Refatore uma classe: pegue uma de suas classes da lição anterior e adicione o encapsulamento adequado:
- Prive todas as propriedades;
- Adicionar
gettersesettersapropriados; - Adicione a validação quando for necessário.