Perché Kotlin? Otto funzionalità che potrebbero convincere gli sviluppatori Java a cambiare

Rilasciato ufficialmente nel 2016, Kotlin ha attirato molta attenzione negli ultimi anni, soprattutto da quando Google ha annunciato il suo supporto per Kotlin come alternativa a Java su piattaforme Android. Con la decisione recentemente annunciata di rendere Kotlin il linguaggio preferito per Android, potresti chiederti se è ora di iniziare a imparare un nuovo linguaggio di programmazione. Se è così, questo articolo potrebbe aiutarti a decidere.

La cronologia delle versioni di Kotlin

Kotlin è stato annunciato nel 2011, ma la prima versione stabile, la versione 1.0, non è apparsa fino al 2016. Il linguaggio è gratuito e open source, sviluppato da JetBrains con Andrey Breslav che funge da principale progettista del linguaggio. Kotlin 1.3.40 è stato rilasciato a giugno 2019.

A proposito di Kotlin

Kotlin è un moderno linguaggio di programmazione di tipo statico che presenta costrutti di programmazione sia orientati agli oggetti che funzionali. È destinato a diverse piattaforme, inclusa la JVM, ed è completamente interoperabile con Java. In molti modi, Kotlin è come potrebbe apparire Java se fosse progettato oggi. In questo articolo presento otto funzionalità di Kotlin che credo gli sviluppatori Java saranno entusiasti di scoprire.

  1. Sintassi pulita e compatta
  2. Sistema di tipo unico (quasi)
  3. Nulla di sicurezza
  4. Funzioni e programmazione funzionale
  5. Classi di dati
  6. Estensioni
  7. Sovraccarico dell'operatore
  8. Oggetti di primo livello e pattern Singleton

Ciao mondo! Kotlin contro Java

Il listato 1 mostra l'obbligatorio "Hello, world!" funzione scritta in Kotlin.

Listato 1. "Hello, world!" a Kotlin

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

Per quanto semplice, questo esempio rivela le principali differenze rispetto a Java.

  1. mainè una funzione di primo livello; cioè, le funzioni di Kotlin non devono essere annidate all'interno di una classe.
  2. Non ci sono public staticmodificatori. Sebbene Kotlin abbia modificatori di visibilità, il valore predefinito è publice può essere omesso. Anche Kotlin non supporta il staticmodificatore, ma in questo caso non è necessario perché mainè una funzione di primo livello.
  3. A partire da Kotlin 1.3, il parametro array-of-strings per mainnon è richiesto e può essere omesso se non utilizzato. Se necessario, sarebbe dichiarato come args : Array.
  4. Non è specificato alcun tipo di ritorno per la funzione. Dove Java utilizza void, Kotlin utilizza Unite se il tipo restituito di una funzione è Unit, può essere omesso.
  5. Non ci sono punti e virgola in questa funzione. In Kotlin, i punti e virgola sono facoltativi e quindi le interruzioni di riga sono significative.

Questa è una panoramica, ma c'è molto di più da imparare su come Kotlin differisce da Java e, in molti casi, lo migliora.

1. Sintassi più pulita e compatta

Java è spesso criticato per essere troppo prolisso, ma un po 'di verbosità può essere tuo amico, soprattutto se rende il codice sorgente più comprensibile. La sfida nella progettazione del linguaggio è ridurre la verbosità pur mantenendo la chiarezza, e penso che Kotlin faccia molto per affrontare questa sfida.

Come hai visto nel Listato 1, Kotlin non richiede punto e virgola e consente di omettere il tipo restituito per le Unitfunzioni. Consideriamo alcune altre funzionalità che aiutano a rendere Kotlin un'alternativa più pulita e compatta a Java.

Inferenza di tipo

In Kotlin puoi dichiarare una variabile come var x : Int = 5, oppure puoi usare la versione più breve ma altrettanto chiara var x = 5. (Sebbene Java ora supporti le vardichiarazioni, quella funzionalità non è apparsa fino a Java 10, molto tempo dopo che la funzionalità era apparsa in Kotlin.)

Kotlin ha anche valdichiarazioni per le variabili di sola lettura, che sono analoghe alle variabili Java che sono state dichiarate come final, il che significa che la variabile non può essere riassegnata. Il listato 2 fornisce un esempio.

Listato 2. Variabili di sola lettura in Kotlin

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

Proprietà contro campi

Dove Java ha i campi, Kotlin ha le proprietà. Le proprietà vengono dichiarate e si accede in un modo simile ai campi pubblici in Java, ma Kotlin fornisce implementazioni predefinite delle funzioni accessor / mutator per le proprietà; cioè, Kotlin fornisce get()funzioni per valproprietà ed entrambe get()e set()funzioni per varproprietà. Versioni personalizzate di get()e set()possono essere implementate quando necessario.

La maggior parte delle proprietà in Kotlin avrà campi di supporto, ma è possibile definire una proprietà calcolata , che è essenzialmente una get()funzione senza un campo di supporto. Ad esempio, una classe che rappresenta una persona potrebbe avere una proprietà per dateOfBirthe una proprietà calcolata per age.

Importazioni predefinite rispetto a importazioni esplicite

Java importa implicitamente le classi definite nel pacchetto java.lang, ma tutte le altre classi devono essere importate esplicitamente. Di conseguenza, molti file di origine Java iniziano importando classi di raccolta da java.util, classi di I / O da java.ioe così via. Per impostazione predefinita, Kotlin implicitamente le importazioni kotlin.*, che è grosso modo analoga a Java importazione java.lang.*, ma anche Kotlin importazioni kotlin.io.*, kotlin.collections.*, e le classi di diversi altri pacchetti. Per questo motivo, i file di origine Kotlin normalmente richiedono meno importazioni esplicite rispetto ai file di origine Java, specialmente per le classi che utilizzano raccolte e / o I / O standard.

Nessuna chiamata a "nuovo" per i costruttori

In Kotlin, la parola chiave newnon è necessaria per creare un nuovo oggetto. Per chiamare un costruttore, usa semplicemente il nome della classe tra parentesi. Il codice Java

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

potrebbe essere scritto come segue in Kotlin:

 var s = Student(...) 

Modelli di stringa

Le stringhe possono contenere espressioni modello , che sono espressioni che vengono valutate con i risultati inseriti nella stringa. Un'espressione modello inizia con un segno di dollaro ($) e consiste in un nome semplice o in un'espressione arbitraria tra parentesi graffe. I modelli di stringa possono abbreviare le espressioni di stringa riducendo la necessità di concatenazione esplicita di stringhe. Ad esempio, il seguente codice Java

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

potrebbe essere sostituito dal codice Kotlin più breve ma equivalente.

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

Si estende e implementa

I programmatori Java sanno che una classe può extendun'altra classe e implementuna o più interfacce. In Kotlin, non c'è differenza sintattica tra questi due concetti simili; Kotlin usa i due punti per entrambi. Ad esempio, il codice 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 } 

Successivamente, ho confrontato le prestazioni delle due versioni di Kotlin con quelle di Java con doublee Java con Double, eseguendo tutti e quattro i benchmark sul mio laptop attuale. Poiché c'è una piccola quantità di "rumore" nell'esecuzione di ogni benchmark, ho eseguito tutte le versioni tre volte e ho calcolato la media dei risultati, che sono riassunti nella Tabella 1.

Tabella 1. Prestazioni di runtime del benchmark di moltiplicazione di matrici

Risultati a tempo (in secondi)
Giava

( double)

Giava

( 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 l'aiuto di IntelliJ IDEA, ho convertito la versione Java del benchmark SciMark in Kotlin. IntelliJ IDEA convertito automaticamente double[]e int[]in Java in DoubleArraye IntArrayin Kotlin. Ho quindi confrontato la versione Java utilizzando le primitive con la versione Kotlin utilizzando DoubleArraye IntArray. Come prima, ho eseguito entrambe le versioni tre volte e ho calcolato la media dei risultati, che sono riassunti nella Tabella 2. Ancora una volta la tabella mostra risultati approssimativamente confrontabili.

Tabella 2. Prestazioni di runtime del benchmark SciMark

Prestazioni (in Mflops)
Giava Kotlin
1818.22 1815.78