Is kotlin better than java? An introduction

Anyone who deals with JVM languages ​​can actually no longer avoid Kotlin. Already a defacto standard in Android development, the language developed by JetBrains is now being used more and more in other areas. One main reason is certainly the almost perfect interoperability with Java, but also the good tooling and of course the modern features of the language itself keep the Kotlin fan base growing. This article takes a closer look at the biggest improvements over Java.

In software development, it is primarily about solving the customer’s problems as quickly as possible, but at the same time safely. Programming languages ​​are just tools and the best solution is the one that doesn’t need any code.

When Java came onto the market over 20 years ago, the developers of the language made exactly this promise and largely kept it. As James Gosling wrote at the time: ” Java is a blue collar language. It’s not a PhD thesis material but a language for a job.  ” But the requirements and preferences of the developer community are changing. For example, while checked exceptions, primitive types or zero 20 years ago were still considered good ideas, today we see it differently. The amount of code that one has to write with Java is no longer up-to-date (with Java7 (Diamond Operator) and Java8 (Lambdas) this has already improved, for Java10 type inference for local variables is also planned). This is exactly where Kotlin comes in.

Kotlin: Zero as part of the type system

Kotlin does a lot better than Java. First of all, one can roughly say that a class in Kotlin consists of approx. 20-30 percent fewer lines of code without being less legible. So are z. B. Explicit type declarations and optional semicolons, lambdas – and functional concepts in general – fit better into the overall picture. Getters and setters are generated by the compiler. Furthermore, Kotlin brings many things with him that you previously had to bring into the project as an external dependency with libraries like Lombok. So z. For example, for classes marked with data , useful toString () , equals () , hashCode () and copy () methods are also generated.

In addition, with Kotlin NullPointerExceptions are largely a thing of the past. In Kotlin, zero is part of the type system. Parameters, properties or return values ​​are either nullable or not. If they are not, the compiler prevents null from being put in, set, or returned. These optional types come with a marked.

var text: String = “Hello, Kotlin!” // this text can never be set to “null”
var nullableText: String? = null // this one already

// paramter is nullable, but not return value -> null cannot be returned
fun addDoubles (left: Double, right: Double?): Int {
  if (right == null) {
    return left.toInt ()
 } else {
   return left.toInt () + right.toInt ()
 }
}

Zero checks can often be completely avoided by using the safe call operator known from Groovy used. In fact, if the object whose method is called is nullable, the compiler will force it to use this operator instead of the regular access operator to use.

var nullableText: String? = zero
var reversedText: String? = nullableText? .reversed ()

In this example, if the text is indeed null , the entire expression evaluates to null and the reversedText is null as well . The type of the variable without it would therefore not have been possible to define it at this point.

This operator can also be used several times in a row:

val name = person? .name? .toUpperCase ()

In addition to the Safe Call operator, there is also the Elvis operator, with which quick zero-or-default queries can be made:

val name = person.name?: “Unknown”

If person.name = zero , the variable name is assigned the value Unknown .

In contrast to the JSR-305 annotations ( @Nullable , @NonNull ), it is not at all possible in Kotlin to use null if this is not explicitly stated. A type that can take the value null is a different type than its non-nullable counterpart. If a string is expected as a parameter, no string? be added. But if a string? is expected, it is still possible to use a string . So is it more comfortable with Kotlins to work as for example with an optional . It is not necessary to put extra values ​​in oneOptionally to wrap to adapt them to the API.

The safe call operator is particularly useful in combination with the letfunction from the standard library, as it can be used as an alternative to the classic null check:

nullableText? .let ({
 // This block will only be executed if `nullableText` is not` null`
})

The curly brackets in the parameter list mean that there is a lambda at this point. (() -> {…}  in Java ) . If the last parameter is a function, the typical round brackets of the function call can – and should – be omitted:

nullableText? .let {
...
}

The let function takes a function as its only parameter. It is thus a Higher-order function with a parameter of type (T) -> R . Functions are independent types in Kotlin, SAM interfaces as in Java are not required. In this case, the parameter R is the nullableText itself and returns Unit as the result , which roughly corresponds to void  in Java. Any parameters of a function type are simply written within the brackets in front of the arrow -> . So is z. B. (String) -> Int a function that accepts a string and returns an Int .

Smart casts in Kotlin: intelligent casting

If you look at the example function addDoubles () above, you notice that with right.toInt () the safe call operator is not used at all, although the parameter is nullable. In this case, the compiler still does not complain. 
This is due to another feature of Kotlin, the so-called smart casts: The compiler knows from the previous zero check that right can no longer be zero afterwards and casts it implicitly from String? to string . It’s very practical that this works not only with zero but with all types of casts!

override fun equals (other: Any?): Boolean {
  if (this === other) return true

  // is = the Kotlin equivalent to instanceof
  if (other! is Message) return false

  // in java I would have to cast explicitly:
  // return this.id == ((Message) other) .id`
  return this.id == other.id
 }

In this implementation of the equals () method of a Message class, other is automatically treated as a Message after the is check . Unlike in Java, no explicit cast is required to access the ID. 

Immutability for everyday life

In addition to zero security and smart casts, another feature ensures more security in everyday programming: Kotlin’s language design promotes and facilitates immutability. The simplest example is the creation of unchangeable variables with valinstead of var . Although final should be used generously even in Java, this is due to the additional keyword too much work and therefore often not the case.

The more important example of immutability in Kotlin is the design of the Collections API. Unlike z. B. Scala does not include its own collection classes with Kotlin. The Java collections just continue to work as usual and are supplemented by a number of additional operations. However, the interfaces and thus the hierarchy of the collections look a little different. When I try to add a new element to a List <String> list , it won’t work:

val words: List <String> = ...
words.add (“Immutable”) // compile error

The List , Map and Set interfaces do not have any methods that can change the underlying object. Kotlin differentiates between changeable and read-only collections (technically, a map is not a collection at all, but falls into a similar category of data structures).

To fill a collection with data, I need a variable of the MutableCollection type , e.g. B. a MutableList .

val words: MutableList <String> = mutableListOf (...)
words.add (“mutable”) // works
Fig. 1: Hierarchy.  © Lovis Möller
Fig. 1: Hierarchy. © Lovis Möller

However, it is seldom necessary to work with mutable collections and to manipulate them directly, since all collections have been extended with a whole set of practical functions. These include the classic list operations, filter , map , reduce and fold . But also more specific ones like filterIsInstance , groupBy , sumBy or takeWhile .

Getting the shortest string in a list could be a simple task . In Java you would do it like this:

String shortest = items.stream ()
.min (Comparator.comparing (item -> item.length ()))
.get ();

While in Kotlin a simple one

val shortest = items.minBy {it.length}

is sufficient.

This notation – which is shorter in many places – becomes even more impressive when the problem becomes a little more complicated. Would you like For example, to determine the names of all employees in a certain salary group, the call in Java would look something like this:

List <String> namesOfLevel1Employees = allEmployees.stream ()
                 .filter (p -> p.getSalaryGroup () == SalaryGroup.L1)
                 .map (p -> p.getName ())
                 .collect (Collectors.toList ());

And Kotlin:

val namesOfLevel1Employees = allEmployees
                          .filter {it.salaryGroup == SalaryGroup.L1}
                          .map {it.name}

The fact that neither streams nor collectors are necessary saves you a lot of paperwork without losing expressiveness.

A new collection is always returned with all of these operations. Since the original collection itself is not changed, the operations also work on the read-only collections List , Set or Map . In this way, Kotlin facilitates the handling and use of immutable data structures. In practice you rarely need a changeable collection. The many predefined extension functions for collections help enormously here. In addition, I can always specify List , Set or Map as return value in the interface of my object , even though a changeable collection is used internally.

Extension functions for collections

If Kotlin uses the same collections internally as Java, then the question arises as to how it can be that they offer so many more methods than Java collections. The answer to this question is: Extension Functions.

With extension functions, classes can be expanded with functionality without having to resort to inheritance. This is especially useful if you are not the author of these classes yourself. The filter function described above could then e.g. B. implemented like this:

fun <T> List <T> .filter (predicate: (T) -> Boolean): List <T> {
 val destination = mutableListOf <T> ()
  for (element in this) {
    if (predicate (element)) destination.add (element)
  }
  return destination
}

Since the standard library already provides this function for all collections, this is not necessary, as I said. The example shows how extension functions are defined. They look like normal function definitions, only that the type to be extended (in this case List <T> ) is prefixed with a period to the function name.

The so-called receiver can be accessed within the function via this. The receiver is the object on which the function will later be called ( List <T> ). A great advantage of such extensions is that they can be used to avoid static functions and entire utility classes that are difficult to read.

The most frequently used utility function is probably Collections.sort () . This was also converted into an extension for Kotlin. So instead of Collections.sort (myList, myComparator); having to call, I can work with the object in a much more natural way. Sorting is now an operation on the list itself: myList.sortBy (myComparator) . Extension functions also improve the reading flow, which is particularly clear with more complex constructs:

Text text = TextUtils.italic (TextUtils.capitalize ("Hello," + TextUtils.underline (person.name)));

This concatenation of static functions must be read from the inside out in order to fully understand it (so many brackets!). The counterpart with Extension Functions, on the other hand, is used in the same order in which it was written (with $ {property} can be +chains as string1 + string3 “,” + avoid – this technique String interpolation is called):

val text: Text = "Hello, $ {person.name.underline ()}". capitalize (). italic ()

The whole thing becomes even more readable if you abstract away the details and choose a name that is suitable for the domain.
The example above, the filtering of all employees of a certain salary level, becomes an extension function for List <Employee> :

fun List <Employee> .collectLevel1EmployeeNames (): List <String> {
  return this .filter {it.salaryGroup == SalaryGroup.L1}
                   .map {it.name}
}

This function can then simply be used in the code:

val names = allEmployees.collectLevel1EmployeeNames ()

Extension functions might seem a bit like black magic at first glance. How on earth can classes (and interfaces!) Be extended without using inheritance? Is some kind of wrapper created internally, which then delegates? In fact, there is nothing magical about extensions, it is just a simple compiler trick: The bytecode for an extension corresponds to the bytecode generated by a static utility function. This means that the Employee example is similar to the following Java code:

public static List <String> collectLevel1EmployeeNames (List <Employee> receiver) {
   return receiver.filter (....). map (....)
}

Extension functions are thus compiled into static utility functions that have an additional parameter receiver . This receiver is always the object on which I called the extension.

Extension functions are everywhere

Of course, extensions are not limited to collections. Such functions can be written for every class and every interface. The standard library delivers e.g. B. the let , which I already described at the beginning of the article. The let function allows you to change the scope of a variable. As mentioned above, this is especially useful when the variable can be null:

var text: String? = ...
text? .let {
    println (it)
}

The nullable variable text is transferred here to the variable it . This variable can no longer be zero , since the entire block is only executed if text is not zero . This it is – borrowed again from Groovy – a convention in Kotlin. If a lambda has only one parameter, it does not have to be specified explicitly, but can be used as it instead.

Another special feature of the letfunction is that it can also return a value.

val fullName = person? .let {
    "$ {It.firstName} $ {it.lastName}"
}

The last expression in this lambda is returned (without return ). In this case a string that concatenates the two properties firstName and lastName with the help of string interpolation.

In addition to let , there are other extensions of this type, for example apply :

val redCar = Car (). apply {
  this.color = Color.RED
  this.ps = 172
  doors = 3
}

In contrast to let , apply does not use it , but this . As usual for this, this thiscan also be omitted ( doors = 3 ). What makes this function so useful is that it returns the receiver without my having to explicitly write return this . For the example, this means: First a Car object is created. Then color, PS and number of doors are set. In the end, exactly this Car object is returned. So the variable redCarwill be exactly this carObject assigned.

Many people report that thanks to Kotlin they have more fun programming again.

With apply , initializations of an object stay close together in the same block of code, even if the API wasn’t originally designed that way. Since this code block is a lambda, it can also be stored in a variable and used at a later point in time:

val redCarConfig: Car. () -> Unit = {
  this.color = Color.RED
  this.ps = 172
  doors = 3
}

val newCar = Car (). apply (redCarConfig)

The apply function can therefore be seen as a lightweight alternative to the Builder Pattern, as the construction and representation of an object are separated from one another so that the same construction process can be used over and over again. The type of the redCarConfig variable is Car. () -> Unit. This notation defines that this is not a normal function, but an extension function for Car , which the unitreturns. You could applySo call it a “higher-order extension function”, since it is itself an extension function that expects another extension function as a parameter. Without this construction it would not be possible to access the receiver as this within the apply block .

Conclusion

I hope that this article gives a good impression of why it is “worthwhile” to use Kotlin in a project, or at least to try it out. Many of the ideas in Kotlin can also be found in other programming languages. Properties, extensions, zero security are nothing new and even old hat for some languages. But Kotlin brings these and many other elegant and useful language features to the JVM in an unprecedentedly simple and accessible way. Everyone has to decide for themselves whether Kotlin is the better Java. However, many people report that thanks to Kotlin they have more fun programming again.

However, Kotlin is not limited to the JVM, but also transpils into JavaScript or WebAssembly and can now also be tried out as Kotlin / Native for native programming (e.g. under iOS).

Kotlin may not be material for a PhD thesis, but Kotlin is definitely a modern language ” for a job “






ITGAIN Consulting GmbH

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

جميع الحقوق محفوظة لموقع كيفاش 2024