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
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 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.
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
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
- Sem rodar, o que imprime
treino == estudoquando as duas foram criadas comPlaylist("Ana", "Treino")? A resposta éfalse, pelos motivos da seção 10. - Crie
Playlist("Ana", "Corrida"), chametocar()três vezes e imprimareproducoes. A resposta é3. - Sem rodar,
x.dono = "Bia"compila paraval x = Playlist("Ana")? Ex.nome = "Corrida"? A resposta é não no primeiro, porquedonoéval, e sim no segundo, porquenomeé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
valouvar. - Identidade e igualdade: ser o mesmo objeto (
===) e ser equivalente (==, que chamaequals).
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.
- Kotlin, classes: https://kotlinlang.org/docs/classes.html
- Kotlin, propriedades: https://kotlinlang.org/docs/properties.html
- Kotlin, igualdade: https://kotlinlang.org/docs/equality.html
Relacionados
- Ambiente e primeiro programa
- Funções
- Controle de fluxo
- Coleções e lambdas
- Exceções e primeiro teste automatizado
- Encapsulamento, visibilidade, object e companion
- Herança e polimorfismo
- data class, enum e value class
- Interfaces, delegação e classes open
- sealed e when exaustivo
- Nova ordem da trilha de Kotlin
- Trilha de Kotlin