Pular para o conteúdo

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

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

, , 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.

O código de fora chama sacar, público, que debita o saldo privado da classe Conta; a leitura direta de conta.saldo é barrada.
figura 1 · Fronteira entre o código de fora e a classe Conta

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.

ModificadorQuem enxerga
publictodo código que enxerga a declaração (padrão)
privatea própria classe; no nível de arquivo, o arquivo
protecteda classe e as subclasses (ver Herança e polimorfismo)
internaltodo 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.

Ao sacar, require e check recusam valor inválido e saldo insuficiente antes da única linha que altera o saldo; as duas recusas param o caminho.
figura 2 · Saque: e antes de alterar o saldo

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'.
Conta.abrir confere o titular com require antes do construtor privado; titular curto lança IllegalArgumentException e nenhuma conta é criada.
figura 3 · Companion object: a fábrica confere o titular antes do construtor privado

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:

  1. Troque a propriedade saldo por uma com leitura pública e escrita privada.
  2. Crie depositar(valor) e sacar(valor), com require para valor positivo e check para saldo suficiente no saque.
  3. Torne o construtor privado e crie abrir(titular) num companion 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 val ou var, 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 IllegalArgumentException quando a condição é falsa. Confere argumentos.
  • check: lança IllegalStateException quando 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 String ou 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.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?