Trilha de Kotlin · parte 9 de 59
Encapsulamento, visibilidade, object e companion
Aprenda a esconder o estado de uma classe atrás de ações que protegem as regras, com visibilidade, object, companion object e object expression.
iniciantePublicada em · 12 min de leitura
, , object e companion
Imagine o caixa de um mercadinho que deixou a gaveta do dinheiro em cima do balcão, com um aviso: “pode se servir”. No fim do dia, a gaveta e o caderno de contas não batem, mesmo sem ninguém ser desonesto. Encapsulamento é trancar a gaveta e atender pelo balcão: o cliente pede, e o caixa confere antes de mexer no dinheiro. Em código, a gaveta é o estado do objeto, e o balcão são as funções que ele oferece.
o mapa da aula
A seção 1 mostra um saldo que fica negativo sem que ninguém decida isso. As seções 2 a 5 tratam de esconder dados, com visibilidade, propriedades e métodos que protegem regras. As seções 6 a 8 cobrem object, companion object, const val e . Fecham a aula os exercícios, os erros comuns, a lista do que não foi confirmado e o glossário.
Você deve saber declarar classes, propriedades, init e métodos, como em Classes, objetos e propriedades, e já usou require e check em Exceções e primeiro teste automatizado. Todos os exemplos usam uma conta bancária.
Os trechos não têm linha package. Cada posição de erro (linha:coluna) conta a partir da primeira linha do arquivo, então cole cada trecho num Main.kt sem pacote.
1. o problema de deixar o estado aberto
A conta tem uma regra que o programa não escreve em lugar nenhum: o saldo não pode ficar negativo. Com uma var pública, qualquer código altera o saldo.
class Conta(var saldo: Int)
fun main() {
val conta = Conta(100)
conta.saldo -= 150
println("saldo: ${conta.saldo}")
}
saldo: -50
Nada reclamou. A regra existia só na cabeça de quem escreveu o saque. Uma regra que precisa valer o tempo todo para um objeto se chama , e o encapsulamento existe para que a própria classe a garanta.
2. encapsulamento: esconder o estado e expor ações
Encapsulamento é esconder os dados atrás de operações que protegem as regras. Quem está de fora pede um saque, e a conta decide.
Pelo caminho laranja, o código de fora pede um saque, e a classe debita o saldo pelo caminho verde. O caminho rosa tenta ler conta.saldo, e a seta para antes da propriedade, que é private: só a classe lê e escreve.
3. modificadores de visibilidade
Um modificador é a palavra escrita antes de uma declaração para dizer de onde ela pode ser usada. Sem nenhum, vale public, como a documentação confirma. private restringe o membro à própria classe, e, numa declaração de nível de arquivo, ao arquivo.
class Conta {
private var saldo: Int = 0
fun depositar(valor: Int) {
require(valor > 0) { "depósito precisa ser positivo: $valor" }
saldo += valor
}
fun saldoAtual(): Int = saldo
}
fun main() {
val conta = Conta()
conta.depositar(50)
println("saldo: ${conta.saldoAtual()}")
}
saldo: 50
Com saldo privado, o main abaixo tenta ler a propriedade de fora:
class Conta {
private var saldo: Int = 0
}
fun main() {
val conta = Conta()
println("saldo: ${conta.saldo}")
}
e: .../src/main/kotlin/Main.kt:7:29 Cannot access 'var saldo: Int': it is private in 'Conta'.
A posição 7:29 aponta para o saldo depois do ponto.
protected também esconde o membro de fora, mas deixa as subclasses enxergá-lo. Ele depende de herança, assunto da aula Herança e polimorfismo. internal vale para o inteiro, que é o código compilado junto. Num projeto de um só módulo, quase não há diferença em relação a public. O efeito aparece entre módulos, na fase 6, na aula Gradle Kotlin DSL e multi-módulo.
| Modificador | Quem enxerga |
|---|---|
public | todo código que enxerga a declaração (padrão) |
private | a própria classe; no nível de arquivo, o arquivo |
protected | a classe e as subclasses (ver Herança e polimorfismo) |
internal | todo o módulo |
4. propriedades: setter privado, personalizados
private set deixa a leitura pública e restringe a escrita à classe. Num setter personalizado, field é o : o identificador que guarda o valor da propriedade. Em field = value.trim(), cada atribuição passa pelo trim.
class Conta(titularInicial: String) {
var titular: String = titularInicial
set(value) {
field = value.trim()
}
var saldo: Int = 0
private set
fun depositar(valor: Int) {
saldo += valor
}
}
fun main() {
val conta = Conta(" Ana Lima ")
println("titular: '${conta.titular}'")
conta.titular = " Bia "
println("titular: '${conta.titular}'")
conta.depositar(80)
println("saldo: ${conta.saldo}")
}
titular: ' Ana Lima '
titular: 'Bia'
saldo: 80
O título inicial veio com espaços. O setter roda a cada atribuição, mas a documentação diz que não roda na inicialização, então titularInicial entrou direto na propriedade. O valor inicial precisa ser tratado na declaração ou no construtor. A correção aplica o trim na declaração:
class Conta(titularInicial: String) {
var titular: String = titularInicial.trim()
set(value) {
field = value.trim()
}
var saldo: Int = 0
private set
fun depositar(valor: Int) {
saldo += valor
}
}
fun main() {
val conta = Conta(" Ana Lima ")
println("titular: '${conta.titular}'")
conta.titular = " Bia "
println("titular: '${conta.titular}'")
conta.depositar(80)
println("saldo: ${conta.saldo}")
}
titular: 'Ana Lima'
titular: 'Bia'
saldo: 80
Com private set, a atribuição de fora não compila. A classe abaixo reduz o exemplo ao saldo:
class Conta {
var saldo: Int = 0
private set
}
fun main() {
val conta = Conta()
conta.saldo = 10
}
e: .../src/main/kotlin/Main.kt:8:11 Cannot access 'saldo': it is private in 'Conta'.
5. expor comportamento, não dados
Um private set impede a escrita de fora, mas não garante a regra dentro da própria classe. Por isso a conta oferece ações. require confere o argumento, check confere o estado, e cada um lança uma exceção diferente.
class Conta(saldoInicial: Int) {
var saldo: Int = saldoInicial
private set
fun sacar(valor: Int) {
require(valor > 0) { "valor do saque precisa ser positivo: $valor" }
check(valor <= saldo) { "saldo insuficiente: saldo $saldo, saque $valor" }
saldo -= valor
}
}
fun main() {
val conta = Conta(100)
conta.sacar(30)
println("saldo após saque: ${conta.saldo}")
for (valor in listOf(-10, 500)) {
try {
conta.sacar(valor)
} catch (e: IllegalArgumentException) {
println("recusado pelo require: ${e.message}")
} catch (e: IllegalStateException) {
println("recusado pelo check: ${e.message}")
}
}
println("saldo final: ${conta.saldo}")
}
saldo após saque: 70
recusado pelo require: valor do saque precisa ser positivo: -10
recusado pelo check: saldo insuficiente: saldo 70, saque 500
saldo final: 70
A subtração só roda depois das duas verificações. A figura mostra o caminho.
O caminho verde leva ao saldo novo. Os dois caminhos rosa terminam em exceção, e o saldo fica como estava.
Pergunta de entrevistaComo você protegeria o saldo de uma conta contra alteração direta?
Deixaria o estado privado e exporia só ações, como depositar e sacar. A propriedade teria private set, para que a leitura seja pública e a escrita passe só pelas funções. Essas funções validam a regra, por exemplo, um saque acima do saldo lança erro. Assim nenhum código externo consegue deixar a conta num estado inválido. Um setter público em tudo espalha a regra pelo projeto inteiro.
6. object: uma instância só
Um object cria uma única instância com nome, o que se chama . A documentação diz que ele é inicializado no primeiro acesso, de forma thread-safe. A mensagem do init aparece na primeira vez que Juros é usado.
object Juros {
init {
println("Juros foi criado")
}
fun calcular(saldo: Int, taxaPercentual: Int): Int = saldo * taxaPercentual / 100
}
object Caixa {
var totalSacado = 0
}
class Conta(saldoInicial: Int) {
var saldo: Int = saldoInicial
private set
fun sacar(valor: Int) {
require(valor > 0) { "valor do saque precisa ser positivo: $valor" }
check(valor <= saldo) { "saldo insuficiente" }
saldo -= valor
Caixa.totalSacado += valor
}
}
fun simularSaque(conta: Conta, valor: Int) {
conta.sacar(valor)
}
fun main() {
println("antes de usar Juros")
println("juros de 1000 a 2%: ${Juros.calcular(1000, 2)}")
val ana = Conta(100)
simularSaque(ana, 40)
println("cenário 1, total sacado: ${Caixa.totalSacado}")
val bia = Conta(100)
bia.sacar(10)
println("cenário 2, total sacado: ${Caixa.totalSacado}")
}
antes de usar Juros
Juros foi criado
juros de 1000 a 2%: 20
cenário 1, total sacado: 40
cenário 2, total sacado: 50
Juros é um bom uso, porque não guarda nada que mude. Caixa é o problema. Com um var público, o total vale para o programa inteiro, e o segundo cenário herda o primeiro. É o mesmo problema do objeto Estado da aula Exceções e primeiro teste automatizado: um object com var é uma variável global com outro nome.
7. : o que pertence à classe
Algumas coisas pertencem à classe, e não a cada objeto criado: uma constante, ou uma função que cria o objeto. Em muitas linguagens, isso se chama membro estático. Em Kotlin, o lugar desses membros é o companion object, acessado pelo nome da classe.
No exemplo, o construtor é private: o modificador fica entre o nome da classe e a palavra constructor. Conta.abrir confere o titular antes de criar a conta.
class Conta private constructor(val titular: String) {
var saldo: Int = SALDO_INICIAL
private set
companion object {
const val SALDO_INICIAL = 0
const val TAMANHO_MINIMO_TITULAR = 3
fun abrir(titular: String): Conta {
require(titular.length >= TAMANHO_MINIMO_TITULAR) { "titular curto demais: '$titular'" }
return Conta(titular)
}
}
}
fun main() {
val conta = Conta.abrir("Ana Lima")
println("${conta.titular}: saldo ${conta.saldo}")
println("tamanho mínimo do titular: ${Conta.TAMANHO_MINIMO_TITULAR}")
try {
Conta.abrir("Al")
} catch (e: IllegalArgumentException) {
println("recusado: ${e.message}")
}
}
Ana Lima: saldo 0
tamanho mínimo do titular: 3
recusado: titular curto demais: 'Al'
Se o código de fora tentar Conta("Ana Lima"), o compilador recusa a chamada:
class Conta private constructor(val titular: String)
fun main() {
val conta = Conta("Ana Lima")
}
e: .../src/main/kotlin/Main.kt:4:17 Cannot access 'constructor(titular: String): Conta': it is private in 'Conta'.
O caminho da figura começa em main, e só o verde chega ao construtor.
const val é resolvida na compilação, por isso só aceita String e tipos primitivos, e precisa estar no nível de arquivo, num object ou num companion object. Uma chamada de função não serve. Com a declaração abaixo, o compilador responde:
fun gerarLimite(): Int = 500
const val LIMITE = gerarLimite()
fun main() {
println("limite: $LIMITE")
}
e: .../src/main/kotlin/Main.kt:3:20 Const 'val' initializer must be a constant value.
8. object expression: objeto anônimo
Uma object expression é um objeto sem nome, criado e usado no mesmo lugar. A documentação diz que seus membros só ficam visíveis quando a função ou a propriedade que o devolve é local ou private. Numa função pública, o tipo devolvido vira o supertipo declarado, ou Any, e os membros novos somem para quem chama:
fun criarResumo() = object {
val titular = "Ana"
val saldo = 60
}
fun main() {
val resumo = criarResumo()
println("${resumo.titular} tem ${resumo.saldo}")
}
e: .../src/main/kotlin/Main.kt:8:23 Unresolved reference 'titular' on receiver of type 'Any'.
e: .../src/main/kotlin/Main.kt:8:45 Unresolved reference 'saldo' on receiver of type 'Any'.
Dentro de main, onde o objeto não sai por uma função pública, os membros funcionam e o programa imprime Ana tem 60. O assunto continua na fase 3 da trilha.
9. exercício guiado: a conta protegida
Pegue a conta da seção 1, com var saldo público, e faça estas mudanças, nesta ordem:
- Troque a propriedade
saldopor uma com leitura pública e escrita privada. - Crie
depositar(valor)esacar(valor), comrequirepara valor positivo echeckpara saldo suficiente no saque. - Torne o construtor privado e crie
abrir(titular)numcompanion object, recusando titular com menos de 3 letras.
Ao final, main deve abrir uma conta para “Ana Lima”, depositar 100, sacar 30, tentar sacar 500 e imprimir o saldo final. Faça antes de olhar a resposta. A resposta abaixo foi rodada.
class Conta private constructor(val titular: String) {
var saldo: Int = 0
private set
fun depositar(valor: Int) {
require(valor > 0) { "depósito precisa ser positivo: $valor" }
saldo += valor
}
fun sacar(valor: Int) {
require(valor > 0) { "valor do saque precisa ser positivo: $valor" }
check(valor <= saldo) { "saldo insuficiente: saldo $saldo, saque $valor" }
saldo -= valor
}
companion object {
const val TAMANHO_MINIMO_TITULAR = 3
fun abrir(titular: String): Conta {
require(titular.length >= TAMANHO_MINIMO_TITULAR) { "titular curto demais: '$titular'" }
return Conta(titular)
}
}
}
fun main() {
val conta = Conta.abrir("Ana Lima")
conta.depositar(100)
conta.sacar(30)
println("${conta.titular}: saldo ${conta.saldo}")
try {
conta.sacar(500)
} catch (e: IllegalStateException) {
println("recusado: ${e.message}")
}
println("saldo final: ${conta.saldo}")
}
Ana Lima: saldo 70
recusado: saldo insuficiente: saldo 70, saque 500
saldo final: 70
exercício curto: saldo inicial negativo
Faça a classe Conta(saldoInicial: Int) recusar saldo inicial negativo com require, e tente criar uma conta com -20 capturando a exceção. Faça antes de olhar a resposta, que foi rodada.
class Conta(saldoInicial: Int) {
init {
require(saldoInicial >= 0) { "saldo inicial negativo: $saldoInicial" }
}
var saldo: Int = saldoInicial
private set
}
fun main() {
val ana = Conta(50)
println("saldo inicial: ${ana.saldo}")
try {
Conta(-20)
} catch (e: IllegalArgumentException) {
println("recusado: ${e.message}")
}
}
saldo inicial: 50
recusado: saldo inicial negativo: -20
10. erros comuns e boas práticas
tudo public
Iniciantes costumam deixar tudo public, porque o compilador não reclama. Depois, qualquer parte do programa passa a depender de uma propriedade interna. Comece fechado e abra só o necessário.
setter público em tudo
Uma var pública tem setter público, e isso anula o private set da seção 4. Se o valor só muda por uma ação, exponha o método. Se nunca muda depois de criado, use val.
object guardando estado mutável
O Caixa da seção 6 é o exemplo. A documentação garante a criação thread-safe, mas não trata das alterações feitas depois por vários trechos. Esse tema entra na fase 4. Até lá, pergunte se o valor que muda pertence mesmo a todo o programa.
companion como depósito de utilitários
O companion serve a constantes e fábricas ligadas à classe. Quando vira gaveta de funções sem relação com ela, como formatar um valor em reais, a classe perde o sentido. Uma função que não usa dados da classe costuma ficar no nível de arquivo.
11. glossário
- Encapsulamento: esconder os dados de um objeto atrás de operações que protegem suas regras.
- Invariante: regra que precisa valer o tempo todo para um objeto.
- Visibilidade: alcance de uma declaração, definido pelos modificadores.
- Propriedade: declaração com
valouvar, no corpo da classe ou no construtor. - Getter e setter: funções que leem e escrevem uma propriedade.
- Campo de apoio (field): identificador, dentro de getter ou setter, que guarda o valor da propriedade.
- require: lança
IllegalArgumentExceptionquando a condição é falsa. Confere argumentos. - check: lança
IllegalStateExceptionquando a condição é falsa. Confere o estado. - package: linha que declara o pacote do arquivo. Os trechos desta aula não a têm.
- Singleton: classe com uma única instância, criada por
object. - Companion object: objeto dentro de uma classe, acessado pelo nome dela.
- const val: constante resolvida na compilação, de tipo
Stringou primitivo. - Object expression: objeto anônimo criado onde é usado.
- Módulo: código compilado junto, como um projeto Gradle.
o que não foi confirmado ou medido
Os trechos rodaram com OpenJDK 25, Kotlin 2.4.21 e Gradle 9.7.0 pelo wrapper. Uma repetição com JDK 21 deu as mesmas saídas. Não testei outras versões do JDK.
As regras de protected com código Java e de internal entre módulos vêm só da documentação. Não as executei.
A documentação garante que a criação de um object é thread-safe. Ela não diz se os métodos de um object que alteram estado podem ser chamados por várias threads ao mesmo tempo, e eu não testei isso.
Não verifiquei os menus do IntelliJ, e a aula não os usa.
Fontes
Consultadas em 04/11/2026.
- Kotlin, modificadores de visibilidade: https://kotlinlang.org/docs/visibility-modifiers.html
- Kotlin, declarações de object, companion objects e object expressions: https://kotlinlang.org/docs/object-declarations.html
- Kotlin, propriedades (getters e setters, field, private set, const val): https://kotlinlang.org/docs/properties.html