Neste tutorial, vamos explorar interfaces em Kotlin de forma completa e prática. Interfaces definem um contrato que classes devem seguir, sem impor uma hierarquia rígida de herança. Diferente de Java até a versão 7, as interfaces em Kotlin podem conter implementações padrão de métodos e até propriedades com acessores. Você aprenderá a declarar e implementar interfaces, usar default methods, definir properties em interfaces, trabalhar com herança múltipla de interfaces e aplicar interface delegation.
Declaração e Implementação Básica
Uma interface é declarada com a palavra-chave interface e pode conter métodos abstratos (sem corpo) e métodos com implementação padrão. Uma classe implementa uma interface usando a mesma sintaxe de herança, com dois-pontos.
interface Autenticavel {
fun autenticar(senha: String): Boolean
fun obterIdentificador(): String
}
class UsuarioSistema(
val nome: String,
private val senhaHash: String
) : Autenticavel {
override fun autenticar(senha: String): Boolean {
return senha.hashCode().toString() == senhaHash
}
override fun obterIdentificador(): String {
return "user-${nome.lowercase().replace(" ", "-")}"
}
}
fun realizarLogin(entidade: Autenticavel, senha: String) {
if (entidade.autenticar(senha)) {
println("Login bem-sucedido: ${entidade.obterIdentificador()}")
} else {
println("Falha na autenticação para: ${entidade.obterIdentificador()}")
}
}
fun main() {
val usuario = UsuarioSistema("Maria Souza", "12345".hashCode().toString())
realizarLogin(usuario, "12345") // Login bem-sucedido: user-maria-souza
realizarLogin(usuario, "errada") // Falha na autenticação para: user-maria-souza
}
Interfaces promovem o princípio de programar para uma abstração, não para uma implementação concreta. A função realizarLogin aceita qualquer objeto que implemente Autenticavel, seja um usuário, um serviço externo ou qualquer outra entidade.
Default Methods (Métodos com Implementação Padrão)
Em Kotlin, interfaces podem fornecer implementações padrão para seus métodos. Classes que implementam a interface podem usar a implementação padrão ou sobrescrevê-la.
interface Logger {
val prefixo: String
get() = "LOG"
fun log(mensagem: String) {
println("[${prefixo}] ${timestamp()}: $mensagem")
}
fun erro(mensagem: String) {
println("[${prefixo}-ERRO] ${timestamp()}: $mensagem")
}
fun debug(mensagem: String) {
println("[${prefixo}-DEBUG] ${timestamp()}: $mensagem")
}
// Método privado auxiliar (Kotlin 1.4+)
private fun timestamp(): String {
return java.time.LocalTime.now().toString().substringBefore(".")
}
}
class ServicoDeUsuarios : Logger {
override val prefixo = "USUARIOS"
fun criarUsuario(nome: String) {
log("Criando usuário: $nome")
// Lógica de criação...
log("Usuário $nome criado com sucesso")
}
fun excluirUsuario(nome: String) {
log("Tentando excluir: $nome")
erro("Operação de exclusão não implementada")
}
}
fun main() {
val servico = ServicoDeUsuarios()
servico.criarUsuario("Carlos")
servico.excluirUsuario("Carlos")
}
Default methods permitem que interfaces forneçam comportamento reutilizável sem forçar cada implementador a reescrever código idêntico. Isso é especialmente poderoso quando combinado com propriedades que as classes podem personalizar, como o prefixo no exemplo.
Properties em Interfaces
Interfaces em Kotlin podem declarar propriedades. Elas podem ser abstratas (sem valor) ou ter um getter padrão. Interfaces não podem manter estado (não têm backing field), mas podem definir acessores que calculam valores.
interface Dimensionavel {
val largura: Double // abstrata: implementador deve fornecer
val altura: Double // abstrata: implementador deve fornecer
// Property com getter padrão (calculada)
val area: Double
get() = largura * altura
val perimetro: Double
get() = 2 * (largura + altura)
val ehQuadrado: Boolean
get() = largura == altura
}
class Retangulo(
override val largura: Double,
override val altura: Double
) : Dimensionavel
class Quadrado(lado: Double) : Dimensionavel {
override val largura = lado
override val altura = lado
}
fun imprimirDimensoes(forma: Dimensionavel) {
println("Largura: ${forma.largura} | Altura: ${forma.altura}")
println("Área: ${forma.area} | Perímetro: ${forma.perimetro}")
println("É quadrado: ${forma.ehQuadrado}")
println()
}
fun main() {
imprimirDimensoes(Retangulo(10.0, 5.0))
// Largura: 10.0 | Altura: 5.0 | Área: 50.0 | Perímetro: 30.0 | É quadrado: false
imprimirDimensoes(Quadrado(7.0))
// Largura: 7.0 | Altura: 7.0 | Área: 49.0 | Perímetro: 28.0 | É quadrado: true
}
Observe que Retangulo não precisa de corpo algum — as propriedades da interface são satisfeitas pelo construtor primário com override. Isso mostra como Kotlin pode ser conciso sem perder expressividade.
Herança Múltipla de Interfaces
Uma classe em Kotlin pode implementar múltiplas interfaces. Quando duas interfaces fornecem implementação padrão para o mesmo método, a classe deve resolver a ambiguidade explicitamente.
interface Serializavel {
fun serializar(): String
fun formato(): String {
return "JSON"
}
}
interface Imprimivel {
fun imprimir() {
println("Imprimindo documento...")
}
fun formato(): String {
return "PDF"
}
}
class Relatorio(
val titulo: String,
val conteudo: String
) : Serializavel, Imprimivel {
override fun serializar(): String {
return """{"titulo": "$titulo", "conteudo": "$conteudo"}"""
}
// OBRIGATÓRIO: resolver ambiguidade do método 'formato'
override fun formato(): String {
// Pode escolher um, combinar ambos, ou ter lógica própria
val formatoSerial = super<Serializavel>.formato()
val formatoImpressao = super<Imprimivel>.formato()
return "$formatoSerial / $formatoImpressao"
}
override fun imprimir() {
super.imprimir() // Chama a implementação padrão de Imprimivel
println("Relatório: $titulo")
}
}
fun main() {
val relatorio = Relatorio("Vendas Q1", "Total: R$150.000")
println(relatorio.serializar())
// {"titulo": "Vendas Q1", "conteudo": "Total: R$150.000"}
relatorio.imprimir()
// Imprimindo documento...
// Relatório: Vendas Q1
println("Formato: ${relatorio.formato()}")
// Formato: JSON / PDF
}
A sintaxe super<NomeDaInterface>.metodo() permite chamar especificamente a implementação padrão de uma interface quando há ambiguidade. O compilador obriga você a resolver o conflito, evitando o problema do “diamante” que pode ocorrer em linguagens com herança múltipla de classes.
Interface Delegation (Delegação de Interface)
A delegação é um padrão de design poderoso onde, em vez de implementar uma interface manualmente, você delega todas as chamadas para outro objeto. Kotlin suporta delegation nativamente com a palavra-chave by.
interface Repositorio<T> {
fun salvar(item: T)
fun buscar(id: String): T?
fun listar(): List<T>
fun remover(id: String): Boolean
}
class RepositorioEmMemoria<T> : Repositorio<T> {
private val dados = mutableMapOf<String, T>()
private var contador = 0
override fun salvar(item: T) {
dados["id-${++contador}"] = item
}
override fun buscar(id: String): T? = dados[id]
override fun listar(): List<T> = dados.values.toList()
override fun remover(id: String): Boolean = dados.remove(id) != null
}
// Delega para 'repositorio' e adiciona logging
class RepositorioComLog<T>(
private val repositorio: Repositorio<T>
) : Repositorio<T> by repositorio {
// Sobrescreve apenas os métodos que precisam de logging
override fun salvar(item: T) {
println("[LOG] Salvando item: $item")
repositorio.salvar(item)
println("[LOG] Item salvo com sucesso")
}
override fun remover(id: String): Boolean {
println("[LOG] Removendo item: $id")
val resultado = repositorio.remover(id)
println("[LOG] Remoção ${if (resultado) "bem-sucedida" else "falhou"}")
return resultado
}
// buscar() e listar() são delegados automaticamente
}
fun main() {
val repo = RepositorioComLog(RepositorioEmMemoria<String>())
repo.salvar("Kotlin Brasil")
repo.salvar("Tutorial de Interfaces")
println("Total de itens: ${repo.listar().size}") // 2
println("Itens: ${repo.listar()}")
repo.remover("id-1")
}
Delegation é a alternativa do Kotlin ao padrão Decorator. Em vez de herdar de uma classe concreta, você compõe comportamento delegando a implementação e sobrescrevendo apenas o que precisa. Essa abordagem favorece composição sobre herança, que é uma das boas práticas mais recomendadas em design orientado a objetos.
Interfaces como Contratos de API
Interfaces são fundamentais para criar código testável e desacoplado. Ao programar contra interfaces, você pode facilmente trocar implementações, criar mocks para testes e seguir princípios SOLID.
interface ServicoDeEmail {
fun enviar(destinatario: String, assunto: String, corpo: String): Boolean
}
class SmtpEmailService : ServicoDeEmail {
override fun enviar(destinatario: String, assunto: String, corpo: String): Boolean {
println("Enviando via SMTP para $destinatario: $assunto")
return true
}
}
class MockEmailService : ServicoDeEmail {
val emailsEnviados = mutableListOf<String>()
override fun enviar(destinatario: String, assunto: String, corpo: String): Boolean {
emailsEnviados.add("$destinatario: $assunto")
return true
}
}
class NotificacaoService(private val emailService: ServicoDeEmail) {
fun notificar(email: String, mensagem: String) {
emailService.enviar(email, "Notificação", mensagem)
}
}
fun main() {
// Em produção
val prodService = NotificacaoService(SmtpEmailService())
prodService.notificar("user@email.com", "Bem-vindo!")
// Em testes
val mockEmail = MockEmailService()
val testService = NotificacaoService(mockEmail)
testService.notificar("teste@email.com", "Teste")
println("Emails enviados: ${mockEmail.emailsEnviados}")
}
Erros Comuns
Tentar armazenar estado em interfaces. Interfaces não podem ter backing fields. Uma propriedade em interface pode ter um getter, mas não pode inicializar um valor diretamente como val x = 5.
Esquecer de resolver conflitos. Quando duas interfaces definem o mesmo método com implementação padrão, a classe deve sobrescrever e decidir qual usar. Ignorar isso causa erro de compilação.
Usar interface quando classe abstrata seria melhor. Se você precisa de estado compartilhado (properties com backing field) e construtores, uma classe abstrata é mais apropriada. Interfaces são ideais para contratos sem estado.
Delegation com estado mutável. Ao usar by para delegação, lembre-se de que o objeto delegado é compartilhado. Mudanças de estado no delegado afetam todos que possuem referência a ele.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a usar interfaces em Kotlin de forma completa: declaração e implementação, default methods, properties, herança múltipla, delegation e uso como contratos de API. Interfaces são fundamentais para escrever código desacoplado, testável e bem arquitetado.
O próximo passo é estudar null safety em Kotlin, uma das funcionalidades mais importantes da linguagem que elimina erros de NullPointerException. Recomendamos também explorar generics para criar interfaces parametrizadas e extension functions para estender interfaces existentes. Com domínio de interfaces e herança, você possui uma base sólida de OOP para construir projetos Kotlin robustos e escaláveis.