Java

Relaciona las abstracciones presentadas en Kotlin con sus mecanismos idiomáticos en Java 25 LTS. La guía compara responsabilidades de diseño y garantías, no solo sintaxis.

Modelado de dominio y datos

Cada lenguaje ofrece herramientas distintas para dar forma a un dominio: distinguir qué garantías son del compilador y cuáles son solo convención evita errores de diseño.

Equivalencia desde Kotlin

Clases de valor

Cercana

Un record de un componente separa EvaluationBudget de un int en tiempo de compilación y valida el invariante en su constructor compacto. A diferencia de Kotlin, la JVM siempre asigna una instancia de este record.

Referencia Kotlin: Declarar un tipo nominal para un dato primitivo y validar su invariante al construirse.

Mecanismo
record de un componente con validación en el constructor compacto.
Idea conservada
El tipo distingue EvaluationBudget de int en tiempo de compilación y rechaza valores no positivos al construirse.
Riesgo de diseño
Suponer que un record de un componente se representa igual que un int en tiempo de ejecución, cuando siempre existe una instancia asignada.
Recomendación
Usa un record de un componente cuando el dominio necesite distinguir EvaluationBudget de int, sin prometer una representación sin asignación.
Convención del curso
Valida el invariante en el constructor compacto, no en un método aparte.
Mecanismo del lenguaje
record con un constructor compacto para validar.

Diferencias relevantes

  • Kotlin puede evitar la asignación del wrapper en tiempo de ejecución bajo ciertas condiciones; un record de Java siempre asigna una instancia.
  • Ambos son inmutables y comparan por valor, no por identidad.

El constructor compacto rechaza un presupuesto no positivo.

Kotlin
@JvmInline
value class EvaluationBudget(val remaining: Int) {
    init {
        require(remaining > 0) { "El presupuesto debe ser positivo." }
    }
}

val budget = EvaluationBudget(1000) // remaining = 1000
Java
record EvaluationBudget(int remaining) {
    EvaluationBudget {
        if (remaining <= 0) throw new IllegalArgumentException("El presupuesto debe ser positivo.");
    }
}

var budget = new EvaluationBudget(1000); // remaining = 1000

Fuentes

Equivalencia desde Kotlin

Construcción controlada

Cercana

Un constructor privado restringe la creación directa desde fuera de la clase. Una fábrica estática asigna un nombre a la operación de construcción y garantiza que se preserve el invariante.

Referencia Kotlin: Un constructor privado y una fábrica validan el invariante.

Mecanismo
Constructor privado y fábrica estática. El constructor restringe la creación directa desde fuera de la clase; la fábrica ofrece una operación de construcción con nombre.
Riesgo de diseño
Dejar una ruta de construcción sin validar. Todos los constructores y fábricas accesibles deben preservar el mismo invariante.
Convención del curso
Mantén una única política de validación y haz que todas las rutas de construcción la respeten. Usa nombres como of, from, parse o tryParse según la intención de la operación.

Diferencias relevantes

  • Java usa métodos static donde Kotlin usaría un companion object.
  • Una clase tradicional debe implementar explícitamente igualdad, hash y representación si forman parte de su contrato.

Impide crear una edad negativa.

package atom.domain;

final class Age {
    private final int value;

    private Age(int value) {
        this.value = value;
    }

    static Age of(int value) {
        if (value < 0) throw new IllegalArgumentException();
        return new Age(value);
    }
}

Fuentes

Equivalencia desde Kotlin

Clases de datos

Cercana

Un record representa un conjunto fijo de componentes. Genera igualdad, hash, texto y accesores, pero no promete una copia ni una inmutabilidad profunda.

Referencia Kotlin: data class genera operaciones de valor conocidas.

Mecanismo
record
Riesgo de diseño
Suponer que record Experiment(List<Double> values) vuelve inmutable la lista.
Recomendación
Copia defensivamente colecciones mutables en el constructor compacto con List.copyOf.

Diferencias relevantes

  • Un record no genera copy; la instancia actualizada se construye explícitamente.
  • Un componente puede referenciar una colección mutable aunque el campo no se pueda reasignar.

Modela una medición con igualdad por sus datos.

record Measurement(String name, int value) {}

Fuentes

Equivalencia desde Kotlin

Singletons

Cercana

Java no tiene una declaración object; una constante de enum proporciona una instancia única por cargador de clases e inicialización segura.

Referencia Kotlin: object declara una instancia única del lenguaje.

Mecanismo
constante de enum
Riesgo de diseño
Usar un singleton como contenedor de configuración mutable o como sustituto de inyección de dependencias.
Recomendación
Resérvalo para comportamiento sin estado o estrategias constantes; inyecta las dependencias que cambian entre experimentos.

Diferencias relevantes

  • El acceso se escribe Minimize.INSTANCE.
  • La unicidad es relativa al cargador de clases y no justifica estado global mutable.

Expone una configuración compartida única.

enum Settings { INSTANCE; }

Fuentes

Equivalencia desde Kotlin

Enumeraciones

Cercana

Un enum modela un conjunto cerrado de constantes homogéneas. Es útil cuando los casos comparten estructura y no requieren payload incompatible.

Referencia Kotlin: enum class declara constantes nombradas y puede contener datos o comportamiento.

Mecanismo
enum
Riesgo de diseño
Usar un enum para casos como Completed(bestValue) y Failed(reason).
Recomendación
Usa enum para nombres finitos homogéneos y tipos sellados cuando cada caso tiene datos o comportamiento propios.

Diferencias relevantes

  • La sintaxis es muy cercana a Kotlin; las diferencias relevantes aparecen en APIs y pattern matching.
  • Las alternativas con datos distintos se modelan mejor mediante una jerarquía sellada.

Representa un estado finito de una tarea.

enum Status { NEW, DONE }

Fuentes

Equivalencia desde Kotlin

Jerarquías selladas

Directa

Una interfaz sealed declara explícitamente qué tipos pueden implementarla mediante permits, o de forma inferida cuando esos tipos están en el mismo archivo. Un switch con pattern matching sobre esa interfaz es exhaustivo: si aparece un tercer caso sin manejar, la compilación falla.

Referencia Kotlin: Declarar una familia cerrada de alternativas para que el compilador exija manejar cada caso.

Mecanismo
Interfaz sealed con permits y un switch con pattern matching exhaustivo.
Idea conservada
El compilador exige manejar Completed y Failed; si se agrega un tercer caso sin actualizar el switch, la compilación falla.
Riesgo de diseño
Agregar una rama default al switch, lo que oculta el error de compilación cuando aparece un tercer caso sin manejar.
Recomendación
Si agregas una rama default al switch, el compilador deja de avisarte cuando aparece un nuevo caso sin manejar.
Convención del curso
No agregues una rama default a un switch exhaustivo.
Mecanismo del lenguaje
sealed interface con permits (explícito o inferido) y pattern matching for switch.

Diferencias relevantes

  • Las variantes se declaran con permits en vez de anidarse léxicamente dentro de la jerarquía como en Kotlin.
  • permits puede omitirse cuando todas las variantes están declaradas en el mismo archivo fuente: el compilador las infiere.
  • El switch exhaustivo sobre tipos sellados es una característica reciente del lenguaje (pattern matching for switch).

El switch exhaustivo cubre Completed y Failed sin una rama default.

Kotlin
sealed interface SearchResult {
    data class Completed(val bestValue: Double) : SearchResult
    data class Failed(val reason: String) : SearchResult
}

fun describe(result: SearchResult): String = when (result) {
    is SearchResult.Completed -> "Completed(${result.bestValue})"
    is SearchResult.Failed -> "Failed(${result.reason})"
}
Java
package atom.sealedhierarchies.explicitpermits;

sealed interface SearchResult permits Completed, Failed {}

record Completed(double bestValue) implements SearchResult {}

record Failed(String reason) implements SearchResult {}

final class Describe {
    private Describe() {}

    static String describe(SearchResult result) {
        return switch (result) {
            case Completed c -> "Completed(" + c.bestValue() + ")";
            case Failed f -> "Failed(" + f.reason() + ")";
        };
    }
}

permits inferido: el compilador lo deduce porque las tres variantes están en este mismo archivo.

package atom.sealedhierarchies.inferredpermits;

sealed interface SearchResult {}

record Completed(double bestValue) implements SearchResult {}

record Failed(String reason) implements SearchResult {}

enum NotStarted implements SearchResult { INSTANCE }

Fuentes

Equivalencia desde Kotlin

Alias de tipos

Sin equivalente directo

Java no ofrece un alias de tipos. Una interfaz funcional o un record introduce una frontera nominal, por lo que no es una sustitución transparente de typealias.

Referencia Kotlin: typealias crea un nombre alternativo, no un nuevo tipo nominal.

Mecanismo
sin alias de tipo; usar interfaz o record
Riesgo de diseño
Presentar una interfaz o un record como si fuera solo un nombre alternativo para un tipo existente.
Recomendación
Introduce un tipo nominal únicamente si el dominio necesita una frontera real; de otro modo escribe la firma explícita.

Diferencias relevantes

  • El mecanismo alternativo cambia la identidad de tipo y puede requerir una conversión explícita.
  • Una interfaz funcional puede además documentar excepciones o métodos auxiliares del dominio.

Da un nombre legible a una firma de transformación.

interface Transform { int apply(int value); }

Fuentes