Dlaczego Kotlin? Osiem funkcji, które mogą przekonać programistów Java do zmiany

Kotlin, oficjalnie wydany w 2016 roku, cieszy się w ostatnich latach dużym zainteresowaniem, zwłaszcza, że ​​Google ogłosił wsparcie dla Kotlina jako alternatywy dla Javy na platformach Android. Wraz z niedawno ogłoszoną decyzją, aby Kotlin był preferowanym językiem dla Androida, możesz się zastanawiać, czy nadszedł czas, aby rozpocząć naukę nowego języka programowania. W takim przypadku ten artykuł może pomóc w podjęciu decyzji.

Historia wydania Kotlina

Kotlin został ogłoszony w 2011 roku, ale pierwsze stabilne wydanie, wersja 1.0, pojawiło się dopiero w 2016 roku. Język jest darmowy i open source, opracowany przez JetBrains, a Andrey Breslav był jego głównym projektantem języka. Kotlin 1.3.40 został wydany w czerwcu 2019 roku.

O Kotlinie

Kotlin to nowoczesny, statycznie typowany język programowania, który oferuje zarówno konstrukcje programowania obiektowego, jak i funkcjonalne. Jest przeznaczony dla kilku platform, w tym JVM, i jest w pełni kompatybilny z Javą. Pod wieloma względami Kotlin jest tym, jak mogłaby wyglądać Java, gdyby została zaprojektowana dzisiaj. W tym artykule przedstawiam osiem funkcji Kotlin, które moim zdaniem programiści Java będą podekscytowani odkryciem.

  1. Czysta, zwarta składnia
  2. System jednego typu (prawie)
  3. Zerowe bezpieczeństwo
  4. Funkcje i programowanie funkcyjne
  5. Klasy danych
  6. Rozszerzenia
  7. Przeciążanie operatorów
  8. Obiekty najwyższego poziomu i wzorzec Singleton

Witaj świecie! Kotlin kontra Java

Listing 1 pokazuje obowiązkowe „Hello, world!” funkcja napisana w Kotlinie.

Listing 1. „Witaj, świecie!” w Kotlinie

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

Ten przykład ujawnia kluczowe różnice w porównaniu z Javą.

  1. mainjest funkcją najwyższego poziomu; to znaczy, funkcje Kotlina nie muszą być zagnieżdżane w klasie.
  2. Nie ma żadnych public staticmodyfikatorów. Chociaż Kotlin ma modyfikatory widoczności, wartość domyślna to publici można ją pominąć. Kotlin również nie obsługuje staticmodyfikatora, ale nie jest on potrzebny w tym przypadku, ponieważ mainjest funkcją najwyższego poziomu.
  3. Od wersji Kotlin 1.3 parametr tablica ciągów znaków dla mainnie jest wymagany i można go pominąć, jeśli nie jest używany. W razie potrzeby zostanie zadeklarowany jako args : Array.
  4. Nie określono typu zwracanego dla funkcji. Tam, gdzie używa Java, używa voidKotlin Unit, a jeśli zwracanym typem funkcji jest Unit, można go pominąć.
  5. W tej funkcji nie ma średników. W Kotlinie średniki są opcjonalne, dlatego podziały wierszy są znaczące.

To przegląd, ale można dowiedzieć się o wiele więcej o tym, jak Kotlin różni się od Javy iw wielu przypadkach ją ulepsza.

1. Czystsza, bardziej zwarta składnia

Java jest często krytykowana za to, że jest zbyt rozwlekła, ale pewna szczegółowość może być twoim przyjacielem, zwłaszcza jeśli sprawia, że ​​kod źródłowy jest bardziej zrozumiały. Wyzwaniem w projektowaniu języka jest zmniejszenie gadatliwości przy zachowaniu przejrzystości i myślę, że Kotlin robi długą drogę, aby sprostać temu wyzwaniu.

Jak widzieliśmy na Listingu 1, Kotlin nie wymaga średników i pozwala na pominięcie zwracanego typu dla Unitfunkcji. Rozważmy kilka innych funkcji, które pomagają uczynić Kotlin czystszą, bardziej kompaktową alternatywą dla Javy.

Wnioskowanie o typie

W Kotlinie możesz zadeklarować zmienną jako var x : Int = 5, lub możesz użyć krótszej, ale równie przejrzystej wersji var x = 5. (Chociaż Java obsługuje teraz vardeklaracje, ta funkcja pojawiła się dopiero w Javie 10, długo po pojawieniu się funkcji w Kotlinie).

Kotlin ma również valdeklaracje zmiennych tylko do odczytu, które są analogiczne do zmiennych Java, które zostały zadeklarowane jako final, co oznacza, że ​​zmiennej nie można ponownie przypisać. Listing 2 podaje przykład.

Listing 2. Zmienne tylko do odczytu w Kotlinie

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

Właściwości a pola

Tam, gdzie Java ma pola, Kotlin ma właściwości. Właściwości są deklarowane i dostępne w sposób podobny do pól publicznych w Javie, ale Kotlin zapewnia domyślne implementacje funkcji akcesora / mutatora dla właściwości; to znaczy, Kotlin zapewnia get()funkcje dla valwłaściwości get()oraz set()funkcje i funkcje dla varwłaściwości. W razie potrzeby można zaimplementować dostosowane wersje get()i set().

Większość właściwości w Kotlin będzie miała pola zapasowe, ale można zdefiniować obliczoną właściwość , która jest zasadniczo get()funkcją bez pola zapasowego. Na przykład klasa reprezentująca osobę może mieć właściwość for dateOfBirthi obliczoną właściwość for age.

Import domyślny a import jawny

Java niejawnie importuje klasy zdefiniowane w pakiecie java.lang, ale wszystkie inne klasy muszą być jawnie importowane. W rezultacie wiele plików źródłowych Java zaczyna się od zaimportowania klas kolekcji z java.util, klas we / wy z java.ioitd. Domyślnie Kotlin niejawnie importuje kotlin.*, czyli z grubsza analogiczne do importowania Java java.lang.*, ale również import Kotlin kotlin.io.*, kotlin.collections.*, oraz zajęcia z kilkoma innymi pakietami. Z tego powodu pliki źródłowe Kotlin zwykle wymagają mniej jawnych importów niż pliki źródłowe Java, szczególnie w przypadku klas, które używają kolekcji i / lub standardowych operacji we / wy.

Brak wezwania do „nowego” dla konstruktorów

W Kotlinie słowo kluczowe newnie jest potrzebne do stworzenia nowego obiektu. Aby wywołać konstruktor, po prostu użyj nazwy klasy z nawiasami. Kod Java

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

można by zapisać w Kotlinie następująco:

 var s = Student(...) 

Szablony ciągów

Ciągi mogą zawierać wyrażenia szablonowe , które są wyrażeniami obliczanymi z wynikami wstawionymi do ciągu. Wyrażenie szablonowe zaczyna się od znaku dolara ($) i składa się z prostej nazwy lub dowolnego wyrażenia w nawiasach klamrowych. Szablony ciągów mogą skracać wyrażenia ciągów, zmniejszając potrzebę jawnej konkatenacji ciągów. Jako przykład, poniższy kod Java

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

można by zastąpić krótszym, ale równoważnym kodem Kotlin.

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

Przedłuża i wdraża

Programiści Java wiedzą, że klasa może być extendinną klasą i implementjednym lub większą liczbą interfejsów. W Kotlinie nie ma żadnej syntaktycznej różnicy między tymi dwoma podobnymi pojęciami; Kotlin używa dwukropka w obu przypadkach. Na przykład kod 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 } 

Następnie porównałem wydajność dwóch wersji Kotlina z wydajnością Javy doublei Javy z Double, wykonując wszystkie cztery testy porównawcze na moim obecnym laptopie. Ponieważ podczas wykonywania każdego testu porównawczego występuje niewielki „szum”, trzykrotnie przeprowadziłem wszystkie wersje i uśredniłem wyniki, które podsumowano w tabeli 1.

Tabela 1. Wydajność testu porównawczego mnożenia macierzy w czasie wykonywania

Wyniki czasowe (w sekundach)
Jawa

( double)

Jawa

( 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.

Z pomocą IntelliJ IDEA przekonwertowałem wersję Java benchmarku SciMark na Kotlin. IntelliJ IDEA automatycznie konwertowane double[]iw int[]Javie do DoubleArrayi IntArrayw Kotlin. Następnie porównałem wersję Java używającą prymitywów z wersją Kotlin przy użyciu DoubleArrayi IntArray. Tak jak poprzednio, trzykrotnie uruchomiłem obie wersje i uśredniłem wyniki, które podsumowano w Tabeli 2. Ponownie tabela pokazuje z grubsza porównywalne wyniki.

Tabela 2. Wydajność testu porównawczego SciMark w czasie wykonywania

Wydajność (w Mflops)
Jawa Kotlin
1818,22 1815,78