Pular para o conteúdo

Trilha de Kotlin · parte 8 de 59

Classes, objetos e propriedades

Classes como moldes e objetos como instâncias, com construtor primário, propriedades, init e identidade em Kotlin, com saídas medidas no JDK 25 e no JDK 21.

iniciantePublicada em · 12 min de leitura

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

Classes, objetos e propriedades

Uma forma de biscoito em formato de estrela serve para fazer vinte biscoitos. Cada um sai com o próprio glacê e a própria queimadura na borda. A forma define o formato, e cada biscoito é um resultado concreto. Na programação orientada a objetos, a é a forma e o objeto é cada biscoito.

Esta é a primeira aula de POO da trilha, então não suponha nada. Vamos escrever uma classe Playlist, com dono, nome e músicas, e usá-la do começo ao fim.

o mapa da aula

As seções 1 a 5 apresentam classe, objeto e construtor. As seções 6 a 9 cobrem propriedades, init, construtores secundários e this. As seções 10 e 11 tratam de identidade e toString. Depois vêm exercícios, erros comuns e o glossário.

Visibilidade e object ficam para Encapsulamento, visibilidade, object e companion, e herança para Herança e polimorfismo. data class e sealed têm aulas próprias, data class, enum e value class e sealed e when exaustivo, e aparecem aqui só como referência.

Os trechos rodaram com Kotlin 2.4.21 e Gradle 9.7.0, em dois JDKs (25 e 21) no mesmo Mac. As saídas foram copiadas do terminal. Não testei os cliques do IntelliJ.

1. o problema que a POO resolve

Imagine guardar, para cada playlist, o nome, o dono e as músicas em variáveis soltas, com funções que recebem a lista como argumento. Com duas playlists, aparecem nomeTreino e nomeEstudo, e nada impede entregar a lista de estudo à função do treino. Para o compilador, são duas listas de texto. A regra de que a playlist precisa de nome vive só na cabeça de quem escreveu.

A orientação a objetos junta dados e regras numa peça só, a classe.

2. classe e objeto

Uma classe descreve um tipo de coisa: quais dados ela guarda e quais operações ela faz. Um objeto é uma instância dessa classe, um item concreto com valores próprios.

A figura mostra isso. O main chama Playlist(...), e a classe, com estado e comportamento, gera três objetos com dono, nome e número de músicas próprios.

O main chama a classe Playlist, que tem estado e comportamento, e três objetos saem dela, cada um com dono, nome e músicas próprios
figura 1 · Uma classe gera três objetos a partir do mesmo molde

O grupo tracejado é a classe. Os pontos nas setas são as criações de objetos. O main aponta para a classe porque é ela que produz os três.

3. estado e comportamento

Um objeto combina estado e comportamento. O estado são os dados que ele guarda agora, como o nome e a quantidade de músicas. O comportamento são as operações que ele sabe fazer, como adicionar uma música. Se a pergunta é “como ele está agora”, a resposta é estado. Se é “o que ele consegue fazer”, a resposta é comportamento. O estado fica nas propriedades, que a seção 4 apresenta.

4. declarar uma classe e o construtor primário

Uma classe começa com class e o nome, com letra maiúscula. Os parênteses depois do nome formam o construtor primário, a lista do que a classe exige para ser criada. Com val ou var na frente de um parâmetro, ele vira uma , acessada com o ponto, como em treino.nome. val é só de leitura, e var aceita nova atribuição. Sem nenhum dos dois, o parâmetro existe só durante a criação.

class Playlist(val dono: String, var nome: String)

fun main() {
    val treino = Playlist("Ana", "Treino")
    println("${treino.dono}: ${treino.nome}")
    treino.nome = "Treino pesado"
    println("${treino.dono}: ${treino.nome}")
}

A troca do nome funciona porque nome é var:

Ana: Treino
Ana: Treino pesado

A propriedade dono é val, e tentar trocá-la não compila:

class Playlist(val dono: String, var nome: String)

fun main() {
    val treino = Playlist("Ana", "Treino")
    treino.dono = "Bia"
}
e: file:///.../src/main/kotlin/Main.kt:5:12 'val' cannot be reassigned.

Lido em português, o erro diz que a palavra val não permite reatribuição. Se o valor deve mudar, use var.

5. criar objetos sem new

Para criar um objeto, chame a classe como uma função: Playlist("Ana", "Treino"). Não existe new, palavra que você pode ter visto em Java. A chamada devolve uma referência para o objeto, assunto da seção 10.

6. propriedades com valor padrão e calculadas

A versão completa da classe:

class Playlist(val dono: String, var nome: String = "Sem título") {
    val musicas = mutableListOf<String>()
    var reproducoes = 0

    val total: Int
        get() = musicas.size

    init {
        require(nome.isNotBlank()) { "a playlist precisa de um nome" }
    }

    fun adicionar(musica: String) {
        musicas.add(musica)
    }

    fun tocar() {
        reproducoes++
    }

    fun renomear(nome: String) {
        this.nome = nome
    }

    override fun toString(): String = "Playlist de $dono: $nome (total: $total)"
}

musicas e reproducoes recebem valor inicial na declaração. musicas recebe mutableListOf<String>(), uma lista que pode crescer e só aceita textos (a aula de Coleções e lambdas mostra as listas com mais calma). Como a lista muda por dentro, musicas é val. reproducoes é var, porque é um contador.

total não guarda nada. O get() calcula a contagem a cada leitura, por isso é uma propriedade calculada. Se fosse guardada, cada adicionar precisaria atualizá-la, e alguém acabaria esquecendo.

O parâmetro nome tem valor padrão, como nas funções (Funções). O main usa o padrão:

fun main() {
    val rascunho = Playlist("Ana")
    val treino = Playlist("Ana", "Treino")
    println(rascunho.nome)
    println("músicas no início: ${treino.total}")
    treino.adicionar("Eye of the Tiger")
    treino.adicionar("Stronger")
    println("músicas depois: ${treino.total}")
    println("reproduções: ${treino.reproducoes}")
}
Sem título
músicas no início: 0
músicas depois: 2
reproduções: 0

7. o bloco init

O bloco init roda quando o construtor executa. Aqui ele usa require, que lança IllegalArgumentException se a condição for falsa. O try abaixo usa a classe da seção 6, e " " tem só um espaço, então é recusado:

fun main() {
    try {
        Playlist("Ana", " ")
    } catch (e: IllegalArgumentException) {
        println("não criou: ${e.message}")
    }
}
não criou: a playlist precisa de um nome

As propriedades e os blocos init rodam na ordem em que aparecem no corpo da classe. Por isso musicas, declarada antes, já existe quando o init roda.

Argumentos chegam ao construtor primário, as propriedades ficam prontas e o init recusa nome em branco antes de o objeto existir
figura 2 · Construtor primário: propriedades e init antes do objeto

O ramo vermelho é a falha: a variável não recebe objeto algum.

8. construtores secundários

Um construtor secundário é declarado com constructor e delega ao primário com : this(...). O exemplo usa outra classe, de duas linhas:

class Playlist(val dono: String, val musicas: List<String>) {
    constructor(dono: String, primeiraMusica: String) : this(dono, listOf(primeiraMusica))
}

fun main() {
    val treino = Playlist("Ana", "Stronger")
    println("${treino.dono}: ${treino.musicas.size}")
}
Ana: 1

O compilador escolhe pelo tipo do argumento: "Stronger" é String, então entra o secundário. Com listOf("Stronger", "Eye of the Tiger"), o primário é escolhido, e a mesma linha imprime Ana: 2. Parâmetros com valor padrão no primário costumam bastar, e um secundário só se justifica quando a forma de criar for diferente.

9. métodos e this

Uma função declarada na classe é um método, e lê as propriedades diretamente. Use this quando um parâmetro tem o mesmo nome de uma propriedade. Sem ele, nome = nome tenta reatribuir o próprio parâmetro, e o compilador recusa:

class Playlist(val dono: String, var nome: String = "Sem título") {
    fun renomear(nome: String) {
        nome = nome
    }
}

fun main() {
    val p = Playlist("Ana", "Treino")
    p.renomear("Corrida")
    println(p.nome)
}
e: file:///.../src/main/kotlin/Main.kt:3:9 'val' cannot be reassigned.

A mensagem cita val porque o parâmetro é só de leitura. Com this.nome = nome, a atribuição vai para a propriedade. Uso na classe da seção 6:

fun main() {
    val treino = Playlist("Ana", "Treino")
    treino.renomear("Treino pesado")
    treino.adicionar("Stronger")
    treino.tocar()
    println(treino)
}
Playlist de Ana: Treino pesado (total: 1)

10. identidade, igualdade e referências

Uma variável não guarda o objeto em si, e sim uma referência para ele, como um número de apartamento escrito num papel. Quem tem o papel bate na mesma porta. O papel pode ser copiado, mas a casa é uma só. Para saber se duas variáveis apontam para o mesmo objeto, use ===. Para saber se os objetos são iguais, use ==, que chama equals. Numa classe comum, o equals padrão compara a identidade, então dois objetos com as mesmas propriedades continuam sendo dois:

fun main() {
    val a = Playlist("Ana", "Treino")
    val b = Playlist("Ana", "Treino")
    val c = a
    println("a == b: ${a == b}")
    println("a === b: ${a === b}")
    println("a == c: ${a == c}")
    println("a === c: ${a === c}")
    println("mesmo dono e nome: ${a.dono == b.dono && a.nome == b.nome}")
}
a == b: false
a === b: false
a == c: true
a === c: true
mesmo dono e nome: true
Duas variáveis apontam para o mesmo objeto e uma terceira para outro com as mesmas propriedades; === dá true e false, e == dá false
figura 3 · Duas variáveis apontam para o mesmo objeto e uma terceira para outro objeto igua

a e c apontam para o mesmo objeto, que pulsa na figura. b aponta para outro, com as mesmas propriedades. a === c dá true, a === b dá false, e a == b também dá false, porque o equals padrão compara identidade. A última caixa compara as propriedades à mão: batem, e mesmo assim são dois objetos.

11. toString

Quando você passa um objeto para println, o Kotlin chama toString(). Toda classe herda essa função de Any, e a versão herdada imprime o nome da classe, um @ e um número em hexadecimal. Para Cancao, sem toString próprio, a saída começa com Cancao@.

Esse número vem do hashCode padrão do objeto. Neste Mac, três execuções com JDK 25 deram o mesmo valor, e uma com JDK 21 deu outro. Ele pode mudar de uma execução para outra ou de uma JVM para outra, então não use esse valor para nada.

Para trocar o texto, escreva override fun toString(): String = .... A palavra override diz que a nova versão substitui a herdada, e a herança é o assunto de Herança e polimorfismo.

Pergunta de entrevistaQual a diferença entre == e === em Kotlin?

Em Kotlin, == compara valores, e por baixo chama equals. === compara identidade: as duas referências apontam para o mesmo objeto. Eu uso == para comparar conteúdo, e === quando a identidade importa, como num objeto único ou num cache. Um detalhe que costuma cair em entrevista: um Int? guardado como objeto pode dar false com === mesmo tendo o mesmo valor.

12. exercício guiado: remover e verificar se a playlist está vazia

Acrescente à classe da seção 6 a propriedade calculada estaVazia, com get() = musicas.isEmpty(), e o método remover, que chama musicas.remove(musica):

    val estaVazia: Boolean
        get() = musicas.isEmpty()

    fun remover(musica: String) {
        musicas.remove(musica)
    }

O main abaixo confere o resultado:

fun main() {
    val treino = Playlist("Ana", "Treino")
    println("vazia? ${treino.estaVazia}")
    treino.adicionar("Stronger")
    println("vazia? ${treino.estaVazia}")
    treino.remover("Stronger")
    println(treino)
}
vazia? true
vazia? false
Playlist de Ana: Treino (total: 0)

13. erros comuns e boas práticas

Estes são erros de quem começa com classes. O primeiro é o clássico: todo mundo esquece o val uma vez, e quem diz que nunca esqueceu ainda não escreveu classe suficiente.

esquecer val ou var no parâmetro

Com class Cancao(titulo: String), o código musica.titulo não compila, porque titulo não virou propriedade:

class Cancao(titulo: String)

fun main() {
    val musica = Cancao("Stronger")
    println(musica.titulo)
}
e: file:///.../src/main/kotlin/Main.kt:5:20 Unresolved reference 'titulo' on receiver of type 'Cancao'.

Traduzindo a mensagem, titulo não foi encontrado num objeto do tipo Cancao. Escreva val titulo: String para resolver.

achar que objetos com as mesmas propriedades são iguais

A seção 10 cobre o caso: dois objetos com o mesmo dono e o mesmo nome são dois objetos, e == dá false. Para comparar propriedades, veja data class, enum e value class.

usar var sem necessidade

Em musicas, o que muda é o conteúdo, e val basta. Com var musicas, qualquer trecho poderia trocar a lista inteira, e as músicas sumiriam sem que nenhum adicionar aparecesse. Reserve var para o que aponta para outro valor, como um contador. Um var público também deixa qualquer código trocar o valor de fora da classe. Como fechar esse acesso, em Encapsulamento, visibilidade, object e companion.

validar só no init

O init roda uma vez, na criação, e depois sai de cena. Quem valida só ali deixa passar playlists sem nome. Com val p = Playlist("Ana", "Treino"), a chamada p.renomear("") seguida de println(p) imprime Playlist de Ana: (total: 0), sem erro. Valide também em renomear.

declarar a propriedade depois do init que a usa

A ordem importa. O init abaixo usa uma propriedade declarada depois dele:

class Lista(val dono: String) {
    init {
        println("músicas: ${musicas.size}")
    }
    val musicas = mutableListOf<String>()
}

fun main() {
    Lista("Ana")
}
e: file:///.../src/main/kotlin/Main.kt:3:29 Variable 'musicas' must be initialized.

Traduzida, a mensagem diz que musicas precisa estar inicializada antes de ser usada. Declare as propriedades antes do init.

14. exercícios com respostas

  1. Sem rodar, o que imprime treino == estudo quando as duas foram criadas com Playlist("Ana", "Treino")? A resposta é false, pelos motivos da seção 10.
  2. Crie Playlist("Ana", "Corrida"), chame tocar() três vezes e imprima reproducoes. A resposta é 3.
  3. Sem rodar, x.dono = "Bia" compila para val x = Playlist("Ana")? E x.nome = "Corrida"? A resposta é não no primeiro, porque dono é val, e sim no segundo, porque nome é var.

15. glossário

  • Classe: molde que descreve os dados e as operações de um tipo de objeto.
  • Propriedade: variável do objeto, declarada com val ou var.
  • Identidade e igualdade: ser o mesmo objeto (===) e ser equivalente (==, que chama equals).

o que não foi confirmado ou medido

  • JDK e sistema: os trechos rodaram com JDK 25 e JDK 21, neste Mac, com Kotlin 2.4.21 e Gradle 9.7.0. As saídas e as posições de erro foram iguais nos dois, exceto o hash da seção 11. Outros JDKs, Windows e Linux não foram testados.
  • Hash do toString: depende da JVM, e a aula não afirma qual valor aparece na sua máquina.
  • IntelliJ: os cliques (criar a classe pelo menu, renomear com atalho, ver o erro no editor) não foram testados.
  • Documentação: as fontes abaixo não foram reabertas para a conferência final desta aula.
  • Mensagens de erro: o caminho foi encurtado para .../src/main/kotlin/Main.kt, e o resto é do compilador.

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
?