Kotlin
A concise multiplatform language developed by JetBrains
Avaliação do Kotlin em projetos reais
Postagem convidada de Urs Peter, Engenheiro de Software Sênior e Instrutor de Kotlin certificado pela JetBrains. Para leitores que preferirem uma maneira mais estruturada de adquirir conhecimentos sobre Kotlin, Urs também dirige o Programa de Qualificação em Kotlin da Xebia Academy.
Esta é a segunda postagem da série O guia definitivo para a adoção bem-sucedida do Kotlin em um ambiente dominado pelo Java, que acompanha o processo de adoção do Kotlin por equipes reais, desde a curiosidade de um só desenvolvedor até uma transformação geral da empresa.
Leia a primeira parte: Como iniciar a adoção do Kotlin por desenvolvedores em Java
O estágio de avaliação: o Kotlin como mais que uma brincadeira
Depois que você sentir segurança sobre o Kotlin em testes, será a hora de uma avaliação mais substancial. Há duas abordagens principais:
- Desenvolver um novo microsserviço ou aplicativo em Kotlin
- Ampliar ou converter um aplicativo em Java já existente
1. Desenvolver um novo microsserviço ou aplicativo em Kotlin
Começar do zero com um novo aplicativo ou microsserviço oferece a experiência completa do Kotlin, sem as limitações de um código antigo. Esta abordagem costuma fornecer a melhor experiência de aprendizado e demonstra mais claramente os pontos fortes do Kotlin.
Dica de profissional: Obtenha a ajuda de um especialista durante este estágio. Embora desenvolvedores tenham uma confiança natural em suas habilidades, evitar erros iniciais, na forma de Kotlin ao estilo do Java e de uma falta de bibliotecas para Kotlin, pode evitar meses de dívida técnica.

É assim que você pode evitar armadilhas comuns ao usar Kotlin a partir de um ambiente de Java:
Armadilha: escolher um framework diferente do que você usa em Java.
Dica: Continue com o seu framework atual.
O mais provável é que você estivesse usando o Spring Boot com Java; então, use-o com o Kotlin, também. O Spring Boot tem suporte de primeira ao Kotlin, de modo que não há nenhum benefício adicional em usar algo diferente. Além disso, você seria forçado a aprender não apenas uma nova linguagem, mas também um novo framework, o que só traria mais complexidade, sem oferecer nenhuma vantagem.
Importante: o Spring interfere com o princípio de “herança proposital” do Kotlin, o que exige que você marque explicitamente as classes como abertas para poder estendê-las.
Para evitar ter que adicionar a palavra-chave “open” a todas as classes relacionadas ao Spring (como @Configuration e outras), use o seguinte plug-in de build: https://kotlinlang.org/docs/all-open-plugin.html#spring-support. Se você criar um projeto do Spring com a conhecida ferramenta on-line “initializr”, do Spring, esse plug-in de build já estará configurado para você.
Armadilha: escrever em Kotlin ao estilo do Java e usando APIs comuns do Java, em vez da biblioteca-padrão do Kotlin:
Esta lista pode ser muito longa; então, vamos nos concentrar nas armadilhas mais comuns:
Armadilha 1: usar o Java Stream, em vez das Kotlin Collections
Dica: Sempre use as Kotlin Collections.
As Kotlin Collections têm total interoperabilidade com as Java Collections, mas vêm equipadas com funções de alta ordem, simples, diretas e ricas em recursos, que tornam o Java Stream obsoleto.
Veja abaixo um exemplo que visa selecionar os 3 produtos mais vendidos por faturamento (preço × número vendido), agrupados por categoria:
Java
record Product(String name, String category, double price, int sold){}
List products = List.of(
new Product("Lollipop", "sweets", 1.2, 321),
new Product("Broccoli", "vegetable", 1.8, 5);
Map<String, List> top3RevenueByCategory =
products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingDouble(
(Product p) -> p.price() * p.sold())
.reversed())
.limit(3)
.toList()
)
)
);
Kotlin
val top3RevenueByCategory: Map<String, List> =
products.groupBy { it.category }
.mapValues { (_, list) ->
list.sortedByDescending { it.price * it.sold }.take(3)
}
A interoperabilidade do Kotlin com o Java permite que você trabalhe com classes e records do Java como se fossem nativos do Kotlin, embora você também possa usar uma classe (de dados) do Kotlin como alternativa.
Armadilha 2: continuar usando tipos “Optional” no Java.
Dica: Adote os tipos “Nullable”.
Uma das principais razões pelas quais desenvolvedores em Java mudam para o Kotlin é o suporte incorporado a valores nulos, que dá adeus às NullPointerExceptions. Assim, tente usar apenas tipos “Nullable”. Não use mais tipos “Optional”. Você ainda tem tipos “Optional” nas suas interfaces? É assim que você se livra deles facilmente, convertendo-os em tipos “Nullable”:
Kotlin
//Let’s assume this repository is hard to change, because it’s a library you depend on
class OrderRepository {
//it returns Optional, but we want nullable types
fun getOrderBy(id: Long): Optional = …
}
//Simply add an extension method and apply the orElse(null) trick
fun OrderRepository.getOrderByOrNull(id: Long): Order? =
getOrderBy(id).orElse(null)
//Now enjoy the safety and ease of use of nullable types:
//Past:
val g = repository.getOrderBy(12).flatMap { product ->
product.goody.map { it.name }
}.orElse("No goody found")
//Future:
val g = repository.getOrderByOrNull(12)?.goody?.name ?: "No goody found"
Armadilha 3: continuar usando wrappers estáticos.
Dica: Adote métodos de extensão.
Os métodos de extensão oferecem muitas vantagens:
- Eles tornam o seu código muito mais fluente e legível que os wrappers.
- A complementação de código é capaz de encontrá-los, o que não acontece com os wrappers.
- Como os métodos de extensão precisam ser importados, eles permitem que você use os recursos adicionados de forma seletiva, em um trecho específico do seu aplicativo.
Java
//Very common approach in Java to add additional helper methods
public class DateUtils {
public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatted(LocalDateTime dateTime,
DateTimeFormatter formatter) {
return dateTime.format(formatter);
}
public String formatted(LocalDateTime dateTime) {
return formatted(dateTime, DEFAULT_DATE_TIME_FORMATTER);
}
}
//Usage
formatted(LocalDateTime.now());
Kotlin
val DEFAULT_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
//Use an extension method, including a default argument, which omits the need for an overloaded method.
fun LocalDateTime.asString(
formatter: DateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER): String =
this.format(formatter)
//Usage
LocalDateTime.now().formatted()
Tenha em mente que Kotlin oferece métodos e variáveis de nível superior. Isso implica que é possível simplesmente declarar, por exemplo, DEFAULT_DATE_TIME_FORMATTER como sendo de nível superior, sem precisar associá-lo a um objeto, como acontece no Java.
Armadilha 4: usar as APIs do Java (de forma desajeitada)
Dica: Use o elegante equivalente do Kotlin.
A biblioteca-padrão do Kotlin usa métodos de extensão, que tornam as bibliotecas do Java muito mais amigáveis, apesar de a implementação subjacente ainda ser em Java. Quase todas as principais bibliotecas e frameworks de terceiros, como o Spring, fizeram o mesmo.
Exemplo de biblioteca-padrão:
Java
String text;
try (
var reader = new BufferedReader(
new InputStreamReader(new FileInputStream("out.txt"),
StandardCharsets.UTF_8))) {
text = reader
.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
System.out.println("Downloaded text: " + text + "n");
Kotlin
//Kotlin has enhanced the Java standard library with many powerful extension methods, like on java.io.*, which makes input stream processing a snap due to its fluent nature, fully supported by code completion
val text = FileInputStream("path").use {
it.bufferedReader().readText()
}
println("Downloaded text: $textn");
Exemplo no Spring:
Java
final var books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body( new ParameterizedTypeReference<List>(){}); // ⇦ inconvenient ParameterizedTypeReference
Kotlin
import org.springframework.web.client.body
val books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body<List>() //⇦ Kotlin offers an extension that only requires the type without the need for a ParameterizedTypeReference
Armadilha 5: usar um arquivo separado para cada classe pública
Dica: Combine classes públicas relacionadas em um só arquivo.
Isso permite ter uma boa compreensão de como um (sub)domínio está estruturado, sem ter que navegar por dúzias de arquivos.
Java

Kotlin
//For domain classes consider data classes - see why below
data class User(val email: String,
//Use nullable types for safety and expressiveness
val avatarUrl: URL? = null,
var isEmailVerified: Boolean)
data class Account(val user:User,
val address: Address,
val mfaEnabled:Boolean,
val createdAt: Instant)
data class Address(val street: String,
val city: String,
val postalCode: String)
Armadilha 6: adotar o paradigma mutável de programação
DIca: adote a imutabilidade — o padrão no Kotlin
A tendência é clara em muitas linguagens de programação, incluindo o Java: a imutabilidade está ganhando da mutabilidade.
O motivo é simples: a imutabilidade evita efeitos colaterais indesejados, tornando o código mais seguro, previsível e fácil de raciocinar. Ela também simplifica a concorrência, pois dados imutáveis podem ser compartilhados livremente entre threads, sem risco de condições de corrida.
É por isso que a maioria das linguagens modernas, incluindo o Kotlin, ou enfatizam a imutabilidade como padrão, ou a encorajam fortemente. No Kotlin, a imutabilidade é o padrão, embora a mutabilidade continue sendo uma opção, quando realmente necessária.
Aqui está um guia rápido do power pack de imutabilidade do Kotlin:
1. Use val, em vez de var
Prefira usar val, em vez de var. Se você usar var quando val poderia ter sido usado, o IntelliJ IDEA lhe enviará uma notificação.
2. Use classes de dados (imutáveis) com copy(...)
Em classes relacionadas a domínios, use classes data com val. As classes data do Kotlin costumam ser comparadas aos records do Java. Embora haja alguma sobreposição, as classes data oferecem um recurso matador: copy(...), cuja ausência torna muito trabalhoso transformar record, o que muitas vezes é necessário na lógica de negócios:
Java
//only immutable state
public record Person(String name, int age) {
//Lack of default parameters requires overloaded constructor
public Person(String name) {
this(name, 0);
}
//+ due to lack of String interpolation
public String sayHi() {
return "Hello, my name is " + name + " and I am " + age + " years old.";
}
}
//Usage
final var jack = new Person("Jack", 42);
jack: Person[name=Jack, age=5]
//The issue is here: transforming a record requires manually copying the identical state to the new instance ☹️
final var fred = new Person("Fred", jack.name);
Kotlin
//also supports mutable state (var)
data class Person(val name: String,
val age: Int = 0) {
//string interpolation
fun sayHi() = "Hi, my name is $name and I am $age years old."
}
val jack = Person("Jack", 42)
jack: Person(name=Jack, age=42)
//Kotlin offers the copy method, which, due to the ‘named argument’ feature, allows you to only adjust the state you want to change 😃
val fred = jack.copy(name = "Fred")
fred: Person(name=Fred, age=42)
Então, use classes “data” quando relacionadas a domínios, sempre que possível. Sua natureza imutável garante uma experiência segura, concisa e tranquila ao trabalhar com o núcleo do seu aplicativo.
Dica: prefira coleções imutáveis, em vez de mutáveis
Coleções imutáveis têm vantagens claras quanto à segurança das threads, podem ser passadas adiante com segurança e são mais fáceis de raciocinar. Embora Java ofereça alguns recursos de imutabilidade para coleções, é perigoso usar esses recursos, porque podem facilmente causar exceções em tempo de execução:
Java
List.of(1,2,3).add(4); ❌unsafe 😬! .add(...) compiles, but throws UnsupportedOperationException
Kotlin
//The default collections in Kotlin are immutable (read-only) listOf(1,2,3).add(4); //✅safe: does not compile val l0 = listOf(1,2,3) val l1 = l0 + 4 //✅safe: it will return a new list containing the added element l1 shouldBe listOf(1,2,3,4) //✅
O mesmo se aplica ao uso de Collections.unmodifiableList(...), que não só é inseguro, mas também requer alocação adicional:
Java
class PersonRepo {
private final List cache = new ArrayList();
// Java – must clone or wrap every call
public List getItems() {
return Collections.unmodifiableList(cache); //⚠️extra alloc
}
}
//Usage
personRepo.getItems().add(joe) ❌unsafe 😬! .add(...) can be called but throws UnsupportedOperationException
Kotlin
class PersonRepo {
//The need to type ‘mutable’ for mutable collections is intentional: Kotlin wants you to use immutable ones by default. But sometimes you need them:
private val cache: MutableList = mutableListOf()
fun items(): List = cache //✅safe: though the underlying collection is mutable, by returning it as its superclass List, it only exposes the read-only interface
}
//Usage
personRepo.items().add(joe) //✅safe:😬! Does not compile
Quando se trata de concorrência, deve-se preferir usar estruturas de dados imutáveis, incluindo coleções. No Java, é preciso um esforço adicional com coleções especiais, que oferecem uma API diferente ou limitada, como CopyOnWriteArrayList. Por outro lado, no Kotlin, List, que é somente para leitura, funciona para quase todos os casos de uso.
Se você precisar de coleções mutáveis e seguras para threads, o Kotlin oferece coleções persistentes (persistentListOf(...), persistentMapOf(...)), que compartilham todas a mesma interface poderosa.
Java
ConcurrentHashMap persons = new ConcurrentHashMap();
persons.put("Alice", 23);
persons.put("Bob", 21);
//not fluent and data copying going on
Map incPersons = new HashMap(persons.size());
persons.forEach((k, v) -> incPersons.put(k, v + 1));
//wordy and data copying going on
persons
.entrySet()
.stream()
.forEach(entry ->
entry.setValue(entry.getValue() + 1));
Kotlin
persistentMapOf("Alice" to 23, "Bob" to 21)
.mapValues { (key, value) -> value + 1 } //✅same rich API like any other Kotlin Map type and not data copying going on
Armadilha 7: continuar usando builders (ou pior ainda, tentar usar o Lombok)
Dica: Use argumentos nomeados.
Builders são muito comuns no Java. Embora sejam convenientes, adicionam mais código, são inseguros e aumentam a complexidade. Eles não têm utilidade no Kotlin, pois um único recurso da linguagem os torna obsoletos: os argumentos nomeados.
Java
public record Person(String name, int age) {
// Builder for Person
public static class Builder {
private String name;
private int age;
public Builder() {}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(name, age);
}
}
}
//Usage
new JPerson.Builder().name("Jack").age(36).build(); //compiles and succeeds at runtime
new JPerson.Builder().age(36).build(); //❌unsafe 😬: compiles but fails at runtime.
Kotlin
data class Person(val name: String, val age: Int = 0) //Usage - no builder, only named arguments. Person(name = "Jack") //✅safe: if it compiles, it always succeeds at runtime Person(name = "Jack", age = 36) //✅
2. Ampliar ou converter um aplicativo em Java já existente
Se você não tiver a opção de desenvolver do zero uma aplicação para experimentar o Kotlin, a solução é adicionar novos recursos ou módulos inteiros em Kotlin a uma base de código já existente em Java. Graças à interoperabilidade transparente entre o Kotlin e o Java, você pode escrever código Kotlin que parece ser Java para ser chamado a partir de código Java. Esta abordagem permite:
- Uma migração gradual, sem uma reprogramação dramática
- Testes do Kotlin no mundo real, no seu contexto específico
- Aumentar a confiança da equipe através de código de produção em Kotlin
Em vez de começar com alguma coisa qualquer, considere estas abordagens diferentes:
De fora para dentro:
Comece pela parte das “folhas” do seu aplicativo — por exemplo, controlador, tarefa em lote, etc. — e depois vá avançando para o domínio central. Isso lhe dará estas vantagens:
- Isolamento em tempo de compilação: as classes das “folhas” raramente têm outras coisas dependendo delas; então, você pode mudá-las para Kotlin e ainda fazer build com o resto do sistema inalterado.
- Menos edições em cascata. Uma interface de usuário ou um controlador convertido pode chamar código existente no domínio do Java quase sem alterações, graças à interoperabilidade transparente.
- Solicitações de pull menores, revisões mais fáceis. Você pode migrar arquivo a arquivo ou recurso a recurso.
De dentro para fora:
Começar pelo núcleo e depois prosseguir para as camadas externas costuma ser uma abordagem mais arriscada, pois isso elimina as vantagens da abordagem de fora para dentro, mencionadas acima. Porém, é uma abordagem viável nos seguintes casos:
- Núcleo muito pequeno ou autocontido. Se a sua camada de domínio consistir em apenas algumas classes simples de Java (POJOs) e serviços, pode ser barato convertê-la logo no início e isso pode liberar imediatamente construtos idiomáticos (classes de dados e de valores, hierarquias seladas).
- A arquitetura já está para ser redesenhada, mesmo. Se você estiver planejando refatorar invariantes ou introduzir padrões de projeto orientado por domínios (DDD), tais como objetos de valor ou agregados, durante a migração, às vezes pode ser mais limpo reprojetar primeiro o domínio em Kotlin.
- Contratos de segurança estrita contra valores nulos. Colocar o Kotlin no núcleo transforma o domínio em uma “fortaleza segura contra valores nulos”. As camadas externas em Java ainda podem enviar valores nulos, mas os limites se tornam explícitos e mais fáceis de vigiar.
Módulo a módulo
- Se a sua arquitetura estiver organizada por recursos, em vez de em camadas, e os módulos tiverem um tamanho administrável, então é uma boa estratégia converter os módulos um a um.
Recursos de linguagem para converter Java em Kotlin
O Kotlin oferece uma variedade de recursos — principalmente anotações — que permitem que o seu código em Kotlin se comporte como Java nativo. Isso é especialmente valioso em ambientes híbridos, nos quais Kotlin e Java coexistam na mesma base de código.
Kotlin
class Person @JvmOverloads constructor(val name: String,
var age: Int = 0) {
companion object {
@JvmStatic
@Throws(InvalidNameException::class)
fun newBorn(name: String): Person = if (name.isEmpty())
throw InvalidNameException("name not set")
else Person(name, 0)
@JvmField
val LOG = LoggerFactory.getLogger(KPerson.javaClass)
}
}
Java
//thanks to @JvmOverloads an additional constructor is created, propagating Kotlin’s default arguments to Java
var john = new Person("John");
//Kotlin automatically generates getters (val) and setters (var) for Java
john.setAge(23);
var name = ken.getName();
//@JvmStatic and @JvmField all accessing (companion) object fields and methods as statics in Java
//Without @JvmStatic it would be: Person.Companion.newBorn(...)
var ken = Person.newBorn("Ken");
//Without @JvmField it would be: Person.Companion.LOG
Person.LOG.info("Hello World, Ken ;-)");
//@Throws(...) will put the checked Exception in the method signature
try {
Person ken = Person.newBorn("Ken");
} catch (InvalidNameException e) {
//…
}
Kotlin
@file:JvmName("Persons")
package org.abc
@JvmName("prettyPrint")
fun Person.pretty() =
Person.LOG.info("$name is $age old")
Java
//@JvmName for files and methods makes accessing static fields look like Java: without it would be: PersonKt.pretty(...) Persons.prettyPrint(ken)
O conversor de Java para Kotlin do IntelliJ IDEA
O IntelliJ IDEA oferece um conversor de Java para Kotlin; então, teoricamente, essa ferramenta pode fazer isso para você. Porém, o código resultante está longe de ser perfeito; assim, use-o apenas como ponto de partida. A partir daí, converta-o em uma representação mais apropriada para o Kotlin. Discutiremos mais este assunto na postagem final desta série do blog: Fatores de sucesso para a adoção do Kotlin em larga escala.
Muito provavelmente, usar o Java como ponto de partida fará você escrever em Kotlin com o estilo do Java, o que lhe trará alguns benefícios, mas não liberará o poderoso potencial do Kotlin. Por isso, escrever um novo aplicativo é a abordagem que prefiro.
Em seguida nesta série
Esta parte da nossa série de postagens no blog, O guia definitivo para a adoção bem-sucedida do Kotlin em um ambiente dominado pelo Java, demonstrou como experimentos com o Kotlin podem evoluir para código de produção. Nossa próxima postagem se concentrará no lado humano da adoção: convencer os seus colegas. Ela explicará como apresentar argumentos claros e orientados pelo código, como guiar novos desenvolvedores e criar uma comunidade de Kotlin pequena, mas duradoura dentro da sua equipe.
Artigo original em inglês por:
