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
Torne-o privado por padrão! Apenas torne as coisas públicas se elas precisarem ser acessadas pela lado de fora.
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 {
// Privado — dados internos
private int internalCounter;
private String secretKey;
// Público — parte da interface
public void doSomething() {
// Usa um método auxiliar privado
validateData();
}
// Auxiliar interno privado
private void validateData() {
// ...
}
}A palavra-chave final
A palavra-chave final significa que uma variável não pode ser alterada após ser definida:
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
Uma classe pode definir dois tipos de membros:
-
Membros de instância: pertencentes a cada objeto (cada instância tem sua própria cópia);
-
Membros estáticos: pertencentes à classe (uma cópia compartilhada do tipo inteiro).
Simplificando um pouco: os "membros da instância" pertencem a objetos, enquanto os "membros estáticos" pertencem à própria classe e são compartilhados por todos os objetos desse tipo.
Declaração
/* (modificador de acesso) */ static ... memberName; Exemplo
class Data {
public int x; // Membro instaciado
public static int y = 1000; // Membro estático
// Membro instaciado:
// pode ser acessado tanto por membros estáticos ou não
public void foo() {
x = 100; // OK — igual a this.x = 100;
y = 100; // OK — igual a Data.y = 200;
}
// Membros estáticos:
// não pode ser acessado por variáveis não estáticas
public static void bar() {
x = 100; // Erro: uma variável não estática X não pode ser referenciada a partir de um contexto estático.
y = 100; // OK
}
}Acessando membros estáticos
Data data = new Data();
data.x = 1000; // OK
data.y = 1000; // OK-ish — implementação não recomendada. É preferível realizar a referência via Data.y.
Data.y = 1000; // OK — boas práticas
Data.x = 1000; // Erro: não é possível acessar variáveis de instância em um contexto estáticoCampos estáticos
Um campo estático representa um membro de dados vinculado ao tipo da classe, em vez de pertencer à instância individual do objeto. Os campos estáticos são armazenados em um local de memória específico, compartilhado entre todas as instâncias de objetos criadas.
É declarado o seguinte:
/* (modificador de acesso) (opcional) */ static /* final/volatile (opcional) */ fieldName;Pegaremos o mesmo exemplo da classe Data e adicionaremos este construtor:
public Data() {
y++; // lembre-se de que isso é o mesmo que Data.y++;
}// Cada instância de Data possuirá uma cópia privada do membro de instância x.
// Entretanto, todas apontarão para a mesma localização de memória em relação ao membro 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
// ... e assim por dianteMétodos estáticos
Os métodos estáticos representam, essencialmente, um membro funcional vinculado a um determinado tipo de classe.
A partir da classe Data, considere o método de instância fooe o método estático bar.
Acesse os métodos com:
Data d1 = new Data();
d1.foo(); // Método de instância: acessível SOMENTE por um objeto
Data.bar(); // Método estático: acessível sem um objetoInicializador estático
Use um bloco de inicialização estática para executar a lógica de configuração no momento do carregamento inicial da classe:
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.
