¿Por qué Kotlin? Ocho funciones que podrían convencer a los desarrolladores de Java de cambiar

Lanzado oficialmente en 2016, Kotlin ha atraído mucha atención en los últimos años, especialmente desde que Google anunció su soporte para Kotlin como alternativa a Java en plataformas Android. Con la decisión recientemente anunciada de hacer de Kotlin el idioma preferido para Android, es posible que se pregunte si es hora de comenzar a aprender un nuevo lenguaje de programación. Si ese es el caso, este artículo podría ayudarlo a decidir.

Historial de lanzamientos de Kotlin

Kotlin se anunció en 2011, pero la primera versión estable, la versión 1.0, no apareció hasta 2016. El lenguaje es gratuito y de código abierto, desarrollado por JetBrains con Andrey Breslav como diseñador principal del lenguaje. Kotlin 1.3.40 se lanzó en junio de 2019.

Sobre Kotlin

Kotlin es un lenguaje de programación moderno de tipo estático que presenta construcciones de programación funcional y orientada a objetos. Se dirige a varias plataformas, incluida la JVM, y es totalmente interoperable con Java. En muchos sentidos, Kotlin es el aspecto que tendría Java si se diseñara hoy. En este artículo presento ocho características de Kotlin que creo que los desarrolladores de Java estarán encantados de descubrir.

  1. Sintaxis limpia y compacta
  2. Sistema de tipo único (casi)
  3. Seguridad nula
  4. Funciones y programación funcional
  5. Clases de datos
  6. Extensiones
  7. Sobrecarga del operador
  8. Objetos de nivel superior y el patrón Singleton

¡Hola Mundo! Kotlin versus Java

El Listado 1 muestra el obligatorio "¡Hola, mundo!" función escrita en Kotlin.

Listado 1. "¡Hola, mundo!" en Kotlin

 fun main() { println("Hello, world!") } 

Tan simple como es, este ejemplo revela diferencias clave con Java.

  1. maines una función de nivel superior; es decir, las funciones de Kotlin no necesitan estar anidadas dentro de una clase.
  2. No hay public staticmodificadores. Si bien Kotlin tiene modificadores de visibilidad, el valor predeterminado es publicy puede omitirse. Kotlin tampoco admite el staticmodificador, pero no es necesario en este caso porque maines una función de nivel superior.
  3. Desde Kotlin 1.3, el parámetro array-of-strings para mainno es obligatorio y puede omitirse si no se utiliza. Si es necesario, se declarará como args : Array.
  4. No se especifica ningún tipo de retorno para la función. Donde usa Java void, usa Kotlin Unit, y si el tipo de retorno de una función es Unit, puede omitirse.
  5. No hay punto y coma en esta función. En Kotlin, los puntos y comas son opcionales y, por lo tanto, los saltos de línea son significativos.

Esa es una descripción general, pero hay mucho más que aprender sobre cómo Kotlin se diferencia de Java y, en muchos casos, lo mejora.

1. Sintaxis más limpia y compacta

A menudo se critica a Java por ser demasiado detallado, pero algo de verbosidad puede ser tu amigo, especialmente si hace que el código fuente sea más comprensible. El desafío en el diseño del lenguaje es reducir la verbosidad mientras se conserva la claridad, y creo que Kotlin hace mucho para enfrentar este desafío.

Como vio en el Listado 1, Kotlin no requiere punto y coma y permite omitir el tipo de retorno para Unitfunciones. Consideremos algunas otras características que ayudan a hacer de Kotlin una alternativa más limpia y compacta a Java.

Inferencia de tipo

En Kotlin puedes declarar una variable como var x : Int = 5, o puedes usar la versión más corta pero igual de clara var x = 5. (Si bien Java ahora admite vardeclaraciones, esa característica no apareció hasta Java 10, mucho después de que apareciera en Kotlin).

Kotlin también tiene valdeclaraciones para variables de solo lectura, que son análogas a las variables de Java que se han declarado como final, lo que significa que la variable no se puede reasignar. El Listado 2 da un ejemplo.

Listado 2. Variables de solo lectura en Kotlin

 val x = 5 ... x = 6 // ERROR: WILL NOT COMPILE 

Propiedades versus campos

Donde Java tiene campos, Kotlin tiene propiedades. Las propiedades se declaran y se accede a ellas de manera similar a los campos públicos en Java, pero Kotlin proporciona implementaciones predeterminadas de funciones de acceso / mutador para las propiedades; es decir, Kotlin proporciona get()funciones para valpropiedades get()y set()funciones y funciones para varpropiedades. Se pueden implementar versiones personalizadas de get()y set()cuando sea necesario.

La mayoría de las propiedades en Kotlin tendrán campos de respaldo, pero es posible definir una propiedad calculada , que es esencialmente una get()función sin un campo de respaldo. Por ejemplo, una clase que representa a una persona puede tener una propiedad para dateOfBirthy una propiedad calculada para age.

Importaciones predeterminadas versus explícitas

Java importa implícitamente las clases definidas en el paquete java.lang, pero todas las demás clases deben importarse explícitamente. Como resultado, muchos archivos fuente de Java comienzan importando clases de colección, clases de java.utilE / S java.io, etc. Por defecto, Kotlin implícitamente las importaciones kotlin.*, lo que es más o menos análoga a la importación de Java java.lang.*, pero también Kotlin importaciones kotlin.io.*, kotlin.collections.*y las clases de otros paquetes. Debido a esto, los archivos fuente de Kotlin normalmente requieren menos importaciones explícitas que los archivos fuente de Java, especialmente para las clases que usan colecciones y / o E / S estándar.

Sin llamada a 'nuevo' para constructores

En Kotlin, la palabra clave newno es necesaria para crear un nuevo objeto. Para llamar a un constructor, simplemente use el nombre de la clase entre paréntesis. El código Java

 Student s = new Student(...); // or var s = new Student(...); 

podría escribirse de la siguiente manera en Kotlin:

 var s = Student(...) 

Plantillas de cadenas

Las cadenas pueden contener expresiones de plantilla , que son expresiones que se evalúan con los resultados insertados en la cadena. Una expresión de plantilla comienza con un signo de dólar ($) y consta de un nombre simple o una expresión arbitraria entre llaves. Las plantillas de cadenas pueden acortar las expresiones de cadenas al reducir la necesidad de una concatenación explícita de cadenas. Como ejemplo, el siguiente código Java

 println("Name: " + name + ", Department: " + dept); 

podría ser reemplazado por el código Kotlin más corto pero equivalente.

 println("Name: $name, Department: $dept") 

Extiende e implementa

Los programadores de Java saben que una clase puede ser extendotra clase y implementuna o más interfaces. En Kotlin, no hay diferencia sintáctica entre estos dos conceptos similares; Kotlin usa dos puntos para ambos. Por ejemplo, el código Java

 public class Student extends Person implements Comparable 

would be written more simply in Kotlin as follows:

 class Student : Person, Comparable 

No checked exceptions

Kotlin supports exceptions in a manner similar to Java with one big difference–Kotlin does not have checked exceptions. While they were well intentioned, Java's checked exceptions have been widely criticized. You can still throw and catch exceptions, but the Kotlin compiler does not force you to catch any of them.

Destructuring

Think of destructuring as a simple way of breaking up an object into its constituent parts. A destructuring declaration creates multiple variables at once. Listing 3 below provides a couple of examples. For the first example, assume that variable student is an instance of class Student, which is defined in Listing 12 below. The second example is taken directly from the Kotlin documentation.

Listing 3. Destructuring examples

 val (_, lName, fName) = student // extract first and last name from student object // underscore means we don't need student.id for ((key, value) in map) { // do something with the key and the value } 

'if' statements and expressions

In Kotlin, if can be used for control flow as with Java, but it can also be used as an expression. Java's cryptic ternary operator (?:) is replaced by the clearer but somewhat longer if expression. For example, the Java code

 double max = x >= y ? x : y 

would be written in Kotlin as follows:

val max = if (x >= y) then x else y 

Kotlin is slightly more verbose than Java in this instance, but the syntax is arguably more readable.

'when' replaces 'switch'

My least favorite control structure in C-like languages is the switch statement. Kotlin replaces the switch statement with a when statement. Listing 4 is taken straight from the Kotlin documentation. Notice that break statements are not required, and you can easily include ranges of values.

Listing 4. A 'when' statement in Kotlin

 when (x) { in 1..10 -> print("x is in the range") in validNumbers -> print("x is valid") !in 10..20 -> print("x is outside the range") else -> print("none of the above") } 

Try rewriting Listing 4 as a traditional C/Java switch statement, and you will get an idea of how much better off we are with Kotlin's when statement. Also, similar to if, when can be used as an expression. In that case, the value of the satisfied branch becomes the value of the overall expression.

Switch expressions in Java

Java 12 introduced switch expressions. Similar to Kotlin's when, Java's switch expressions do not require break statements, and they can be used as statements or expressions. See "Loop, switch, or take a break? Deciding and iterating with statements" for more about switch expressions in Java.

2. Single type system (almost)

Java has two separate type systems, primitive types and reference types (a.k.a., objects). There are many reasons why Java includes two separate type systems. Actually that's not true. As outlined in my article A case for keeping primitives in Java, there is really only one reason for primitive types--performance. Similar to Scala, Kotlin has only one type system, in that there is essentially no distinction between primitive types and reference types in Kotlin. Kotlin uses primitive types when possible but will use objects if necessary.

So why the caveat of "almost"? Because Kotlin also has specialized classes to represent arrays of primitive types without the autoboxing overhead: IntArray, DoubleArray, and so forth. On the JVM, DoubleArray is implemented as double[]. Does using DoubleArray really make a difference? Let's see.

Benchmark 1: Matrix multiplication

In making the case for Java primitives, I showed several benchmark results comparing Java primitives, Java wrapper classes, and similar code in other languages. One of the benchmarks was simple matrix multiplication. To compare Kotlin performance to Java, I created two matrix multiplication implementations for Kotlin, one using Array and one using Array . Listing 5 shows the Kotlin implementation using Array.

Listing 5. Matrix multiplication in Kotlin

 fun multiply(a : Array, b : Array) : Array { if (!checkArgs(a, b)) throw Exception("Matrices are not compatible for multiplication") val nRows = a.size val nCols = b[0].size val result = Array(nRows, {_ -> DoubleArray(nCols, {_ -> 0.0})}) for (rowNum in 0 until nRows) { for (colNum in 0 until nCols) { var sum = 0.0 for (i in 0 until a[0].size) sum += a[rowNum][i]*b[i][colNum] result[rowNum][colNum] = sum } } return result } 

A continuación, comparé el rendimiento de las dos versiones de Kotlin con el de Java con doubley Java con Double, ejecutando los cuatro puntos de referencia en mi computadora portátil actual. Como hay una pequeña cantidad de "ruido" al ejecutar cada punto de referencia, ejecuté todas las versiones tres veces y promedié los resultados, que se resumen en la Tabla 1.

Tabla 1. Rendimiento en tiempo de ejecución de la referencia de multiplicación de matrices

Resultados cronometrados (en segundos)
Java

( double)

Java

( Double)

Kotlin

( DoubleArray)

Kotlin

( Array)

7.30 29,83 6,81 15,82

I was somewhat surprised by these results, and I draw two takeaways. First, Kotlin performance using DoubleArray is clearly superior to Kotlin performance using Array, which is clearly superior to that of Java using the wrapper class Double. And second, Kotlin performance using DoubleArray is comparable to--and in this example slightly better than--Java performance using the primitive type double.

Clearly Kotlin has done a great job of optimizing away the need for separate type systems--with the exception of the need to use classes like DoubleArray instead of Array.

Benchmark 2: SciMark 2.0

My article on primitives also included a second, more scientific benchmark known as SciMark 2.0, which is a Java benchmark for scientific and numerical computing available from the National Institute of Standards and Technology (NIST). The SciMark benchmark measures performance of several computational routines and reports a composite score in approximate Mflops (millions of floating point operations per second). Thus, larger numbers are better for this benchmark.

Con la ayuda de IntelliJ IDEA, convertí la versión Java del punto de referencia SciMark a Kotlin. IntelliJ IDEA convierte automáticamente double[]y int[]en Java para DoubleArrayy IntArrayen Kotlin. Luego comparé la versión de Java usando primitivas con la versión de Kotlin usando DoubleArrayy IntArray. Como antes, ejecuté ambas versiones tres veces y promedié los resultados, que se resumen en la Tabla 2. Una vez más, la tabla muestra resultados aproximadamente comparables.

Tabla 2. Rendimiento en tiempo de ejecución del punto de referencia SciMark

Rendimiento (en Mflops)
Java Kotlin
1818.22 1815,78