A very brief overview of Kotlin for Java developers
Kotlin is a Java-compatible programming language that has increased in popularity very quickly over the last few years. The popularity has been enhanced -- to some extent -- by the adoption of Kotlin by Google for Android app development. The language does have some features that make Kotlin very suitable for this application.
In this article, I provide the briefest of brief overviews of the Kotlin language, for experienced Java developers. At the end I will try -- and largely fail -- to explain why Kotlin does not have feature parity with Java.
Note:
This article is based on version 1.5.31 of the standard Kotlin compiler.
Compilation and code generation
The Kotlin compiler is written in Java, and outputs Java bytecode
(although a version that outputs native code is now available).
The bytecode is written into .class
files with the same
format as those generated by javac
. By default, the
Kotlin compiler produces bytecode that is compatible with Java 1.8.
Although this default can be overridden, I don't think that other language
features become available by selecting a later bytecode target.
Kotlin code has a type model that is quite similar to Java's and, to a
large extent,
direct calls can be made from Kotlin code to Java code.
The Kotlin compiler tool kotlinc
can both compile and
run Kotlin code -- in both cases the work is actually done
by a Java JVM. kotlinc
has many of the same command-line
switches as javac
: -classpath
, -Werror
,
etc.
kotlinc
can output to a self-contained ("fat") JAR file, or
to a directory structure, by specifying the -d
switch, as for javac
.
As in Java development, the generated class file structure
will mirror the application's
package structure.
The Java byte code generated by kotlinc
can be executed with
a Java JVM -- there is no specific Kotlin runtime. However, there
is a standard Kotlin library that might need to be included with the
compiled bytecode, and this will not be done automatically. You'll need
to use the -include-runtime
switch. Many developers come to
the false impression that -include-runtime
means to include
a virtual machine, that is, to produce code that is independent of
Kotlin or Java. This is not true -- "runtime" is a misnomer: it means
only to include supporting classes in the generated output. Unless you're
using the native-code compiler, you'll always need a Java JVM to run
Kotlin code, and you might need to bundle the Kotlin "runtime" (classes)
as well.
Package hierarchy
Kotlin's semantic packaging is essentially the same as Java's -- both in language syntax and in the structure of the generated bytecode. In both languages, a source file typically starts with
package foo.bar;although the different syntax of Kotlin means that the semicolon is not necessary. The
import
keyword has exactly the same
function in Kotlin as it does in Java.
Kotlin files have mandatory naming rules, as is the case
with Java (but not C or C++).
Source files are expected to end with .kt
.
Generated bytecode files will always have names ending in .class
.
Kotlin's basic syntax
Kotlin's language syntax is similar to that of Java and C++, but not that similar. Superficially it looks a little more like Pascal, but it's block-structure rules are exactly like those of Java and C++, and not at all like Pascal's. As in Java/C++, language blocks are indicated using curly brackets -- {} -- and these are used consistently and uniformly. The Kotlin syntax means that statements do not normally need to be separated using semicolons.
However, a consequence of using a syntax that does not rely on delimiters is that, unfortunately, end-of-line markers have syntactic significance. There are places in which an opening curly bracket cannot be the first non-whitespace token on a line. This is a nuisance for developers (like me) who prefer to put block-marking tokens like curly brackets on lines of their own. Sometimes this works in Kotlin, and sometimes it doesn't. Personally, I don't like compilers to dictate how I lay out my code; but I guess Python programmers learn to live with it. In any event, Kotlin has less layout flexibility than Java.
Kotlin supports the same flow control and loop constructs as Java, although with a somewhat different syntax.
There isn't really space here to describe the language syntax in detail -- see the Kotlin manual for details.
Kotlin types
Kotlin has the same primitive data types as Java, except that it has
(yipee!!) unsigned types like UByte
and UShort
.
The Kotlin language does not distinguish between true primitive
data types (like Java's int
) and class-based data types
(like Integer
). At the language level, Kotlin's primitives
are all objects.
Using object primitives avoids some of the type-conversion nastiness
that we find
in Java -- the kind that was supposed to be avoided by "autoboxing".
The autoboxing still happens, but it's largely invisible. A Kotlin
Int
will be represented as a Java int
where
possible, and an Integer
object where it isn't.
There's a little more strictness in assignment of short types in Kotlin, compared to Java. For example, this is legal Java:
char c = 'a'; c++;But the corresponding Kotlin code:
val c: Char = 'a' c++fails. Although this kind of code is widespread, it is right that the compiler should complain about it, because there is no guarantee that
c + 1
will fit into an object of Char
range.
Kotlin expects the result of an operation on Char
or
Short
to be assigned to Short
or
Int
respectively. No doubt this will cause annoyance, but
from a code quality point of view, it's the right thing to do.
I guess this is unavoidable, but a Kotlin Char
has
16-bit signed range, like a Java char
. This makes
a Kotlin Char
unsuitable in general for storing a Unicode
code point, whose value might not fit into 16 bits. There's a lot of
bad Java code floating about that was written without understanding this
fact, which the Java platform specification does nothing to avoid.
Kotlin could have taken a stand and fixed this problem but, probably,
only by limiting Java compatibility.
Kotlin has notional support for ranged integer types, that is, integers that are constrained to take certain values only. However, at the time of writing, this feature is essentially undocumented, and it's not clear to me how effectively it is implemented.
Primitives and objects can be collected into arrays, whose semantics are exactly as for Java, although the language syntax is a little different.
The Kotlin equivalent of Java's void
is Unit
.
Variables
Kotlin's use of variables is much the same as Java's, but there's enough syntactic difference to cause confusion. Like Java, Kotlin is statically typed, meaning that the type of a variable will not change after it is defined. Like modern (11+) versions of Java, Kotlin also supports type inference -- you don't need to state the type in a definition if it is clear to the compiler.
Definitions are Pascal-like:
var answer:Int = 42 var answer2 = 42 // Also OK var answer3:Double = 42 // Fails, because 42 is an integerUnlike Java, Kotlin will warn if a variable is defined but never used
Read-only variables (constants, essentially) are introduced using
the val
keyword, while var
denotes an
ordinary (assignable) variable. The const
keyword in
Kotlin functions rather like final
in Java, denoting
a variable whose value is fixed at compile time.
Kotlin's class model
Kotlin's class model is essentially the same as Java's, as are the
language keywords associated with it. Essentially the same member visibility
and scope rules apply, with the notable exception that there is no support
for static
member functions (more on this later).
As in Java, Kotlin classes support only single
inheritance, but can simulate multiple inheritance through
the use of interfaces. Of course, this is not the only use of
interfaces. As in Java, Kotlin classes can be abstract.
Kotlin classes have a notion of primary constructor. This is a specialized constructor that can also define member variables. This use of primary constructors fits nicely with the idea of "data classes".
A data class is a bit of syntactic candy to make a common class use-case more expressive. Consider this implementation:
data class Complex (val real: Double, val imag: Double) { }Defining
Complex
as a data class has the effect that
the arguments to the primary constructor automatically become
member variables.
Thus a caller can refer to the object's
.real
and .imag
members without additional
definition. The constructor does not need to be implemented
by the programmer: the
member variables are assigned automatically from the constructor
arguments when the class is instantiated.
Defining a data class also provides additional features.
For example, the compiler will implement
boilerplate hashCode()
and
equals()
methods that may, or may not, be useful
in a particular application.
The syntax for instantiating a class in Kotlin is somewhat different
to that of Java: there is no new
keyword. Instead, a new
instance is obtained by calling the constructor as if it were a
method.
One construct that Kotlin provides, and that Java does not, is
the sealed class. A sealed class has fixed
subclasses, all known at compile time, and defined in the same source
file. This construct provides a handy and efficient way to use
subclasses as choices, like a more flexible version of an
enum
(which Kotlin also supports).
Another feature exclusive to Kotlin is extension classes. Unlike Java, Kotlin allows methods to be added to classes from outside the class definition. A potentially useful application of this feature is to extend classes in libraries, for which source code is not available, or where you simply don't want to modify the library (because it's a managed dependency). Developer surveys have showed that many Kotlin developers rate extension classes as the language's most significant feature, and a strong reason for adoption.
Kotlin has no static
keyword, so does not support Java-style
static member functions. If you want to define a method that has class,
rather than instance, scope, you can define a companion object
.
This is essentially a singleton instance of the class, for which Kotlin
provides syntactic support. The use of singleton classes is, of course,
widespread in Java; the difference is that these are first-class language
constructs in Kotlin.
In short, although Kotlin's class model is essentially the same as Java's, the syntax will be more compact and readable for many common use-cases.
Function call mechanism
Kotlin's function call mechanism is identical to Java's. In particular, when an object is passed as an argument to a function, the function can change the contents of the object, if the object allows that. There is no notion of "const correctness" in Kotlin, just as there is not in Java. That is, there's no way to specify that a particular method will not change the contents of an object passed to it, and no way to specify that a particular method does not change the contents of the object of which it is a member.
Kotlin, like Java, lacks a way to pass primitive data elements by reference.
The lack of const-correctness and an ability to pass primitives by reference are irksome to C++ developers; but Java developers have, presumably, learned to live within these limitations, and won't be bothered by the absence of these features from Kotlin.
Kotlin's non-class functions
Kotlin does not require that functions be defined only as member functions of classes, as Java does. It's perfectly permissible to create a Kotlin source file that contains only function definitions.
This makes Kotlin more immediately "teachable" than Java, because "Hello, World" is simply this:
fun main() { println("Hello, World!") }Running this simple program at the command line amounts to:
$ kotlinc test.kt $ java TestKt Hello, World!Note that
TestKT
is the automatically-generated
class name provided by the Kotlin compiler, since the
main
method is not in a class.
Note also that the main
function does not need to
take any formal parameters, if the program does not actually
process command-line arguments, nor does it need to define
a return value.
To an absolute beginner, this all looks much simpler than Java --
no boilerplate class, no static void main
...
For better or worse, though, Kotlin does not allow a minimum
program of the form:
println("Hello, World!")As both Perl and Python do. I taught introductory programming for years, to all kinds of students, and I remain unconvinced that the simplicity of the very earliest programming example has a large impact on student success; but I could be wrong. In any event, Kotlin is a more teachable language in the earlier stages than Java, but less so than Python, probably not to a huge extent.
What happens to non-class functions, in practice, is that they are turned into static, public member functions of a class whose name is based on the source filename. When a non-class function is called, the Kotlin compiler turns the function call into a method call on this public class.
Of course, to do this the compiler will have to locate the function name in the source files that are available to it. If the same function name exists in more than one file, then the call is rejected as a "conflicting overload".
It's possible to call from Kotlin code a static method in any Java class, by denoting the classname, and such a call has exactly the same syntax as it does in Java. However, Kotlin itself does not really have static methods as part of its language syntax, so it isn't possible (in an elegant way) to call a Kotlin non-class method by referencing its automatically-generated class name.
To some extent, the abilty of Kotlin to define functions without
classes supports the built-in Kotlin functions
like println
. These methods are not implemented by some
complicated search of the objects available in the Java runtime, although
it might be nice if there were such a mechanism. Instead, these built-in
in functions are implemented in the Kotlin standard library as
inline
functions:
public actual inline fun println(message: Any?) { System.out.println(message) }The
inline
modifier here denotes a straightforward
replacement of the call to println
with a call
to System.out.println
which, as I said, is a legitimate
call in Kotlin.
Inline functions
Like C, and unlike Java, Kotlin allows user functions to be declared
as inline
-- this feature doesn't apply only to internal
functions. An inline
function is substituted directly into
the code of the caller, removing the overhead of a function call.
In a language like C that is traditionally compiled completely to machine code
ahead of execution, inlining functions can have significant benefits
in skilled hands. In Java, strong arguments were raised against
providing a comparable feature, because inlining was held to be the job
of the just-in-time compiler. No version of the standard Java compiler
supports inline function declaration, and it's not clear to me that
it offers much benefit in Kotlin, except perhaps as a way of reducing
code complexity a little. The Kotlin compiler does have the decency to warn
if you're using inline
in a way that is unlikely to
improve throughput.
Exceptions
Unsurprisingly, Kotlin handles exceptions much like Java. However, Kotlin does not support "checked" exceptions. It is never necessary to specify that a method catch or propagate a particular exception; if the exception is not caught and handled somewhere, eventually the JVM will shut down. Kotlin exceptions thus all function like "runtime" exceptions in Java.
The lack of checked exceptions seems to me a disadvantage from a code quality point of view, although I'm aware of arguments in favour of avoiding them. These arguments all seem to center on the fact that developers tend to use checked exceptions in a careless and half-hearted way. That's no doubt true, but it seems a bad reason to throw away a potentially powerful feature. In any case, most C++ compilers don't support checked exceptions either -- Java seems to stand alone here.
Operator overloading
Unlike Java, Kotlin supports full programmer-defined operator overloading. It also supports the same lazy, ill-considered default overloading of Java. For example, it's still possible to concatenate a string and the string form of a number using the "+" operator, in defiance of all logic.
Still, programmer-defined operator overloading is a big deal in some kinds of programming, and its absence from Java is regrettable. For example, in mathematical work it would be useful to express calculations on complex numbers with ordinary math symbols: "+", "*", etc. For example, it would be nice to be able to write:
val a = Complex (1.0, 2.0) val b = Complex (3.0, 4.0) val c: Complex = a + bHappily, in Kotlin, we can. We define the
Complex
class like
this:
data class Complex (val real: Double, val imag: Double) { operator fun plus (other: Complex): Complex { return Complex (real + other.real, imag + other.imag) } // Other complex number functions... }
operator fun
defines a function that implements
an operator, "plus" in this case.
This is a feature that provides enough rope for us to hang ourselves. There's
nothing to stop us overloading operators in ways that make no sense
-- using minus
to represent an addition, for example.
Of course, it's always possible to write unhelpful code if
we are really determined to do so.
Most of the built-in operators can be overloaded. For better or worse,
Kotlin's overloading is not as flexible as that in C++. For example,
we can't define a new kind of an array, and provide our own
implementation of the []
index -- C++ does allow
this.
Incidentally, the C++ standard template library overloads the bit shift (<<) operator to represent data transfer. This was a terrible idea -- overloading an operator with a new function completely unrelated to the old one -- but one that is quite visually expressive. Kotlin does not allow this but, because it doesn't use punctuation for shift operations, it would be pointless to do so.
Kotlin lambdas and functional programming
Much is made of the fact that Kotlin accepts blocks of program code as first-class syntactic structures. That is, a piece of code -- perhaps one that takes arguments and returns a value -- can be assigned to a variable, or passed to another function. Conventionally, a function that takes another function (not the result of a function call) as an argument is called a higher order function. This all has the potential to be very useful but, in fact, recent versions of Java have similar features. It isn't clear to me how much Kotlin has the lead in this area.
In any event, here is a canonical example of defining a higher-order function in Kotlin. This function takes an array, and a function that defines a transformation to apply to each array element.
fun applyToArray (array: Array<Double>, operation: (Double) -> Double) { for (i in array.indices) array[i] = operation (array[i]); }
The function applyToArray
takes two arguments. The first
is simply the array -- in this case it is an array of Double
values to which a transformation is to be applied.
The second argument is a function called operation
.
We don't know (at this point) what the function does, but the declaration
says that it takes an argument of Double
type, and returns
a Double
result. Thus applyToArray
can
apply to every member of the array any single-argument function.
Note that this function changes the array -- this is important
because an alternative approach might be to return a new array containing
the modified values. Either approach is valid, and either might be useful in
particular circumstances.
Here's how we might call operation
to square every
member of an array.
val a = arrayOf<Double> (1.0, 2.0, 3.0) // Square all the elements applyToArray (a, { elem: Double -> elem * elem })Here we're calling
applyToArray
and passing this
function: { elem: Double -> elem * elem }
. This
is an anonymous function -- it has all that a function
should, apart from a name. We don't have to use this
formulation; sometimes a named function is more readable:
fun square (x: Double): Double { return x * x; } // ... applyToArray (a, ::square)It's crucial to understand that we aren't passing to the
applyToArray
function the result of evaluating some other function -- we are
passing the function itself. Or, at least, we're passing some kind
of reference to it, that allows it to be called repeatedly.
Because this kind of thing is so useful, you probably won't be surprised
to learn that similar higher order functions are already defined
for arrays in the Kotlin language. So we could square every element
in array a
like this:
a.forEachIndexed { i, elem: Double -> a[i] = elem * elem }There are other "functional programming" techniques available in Kotlin, and increasingly in Java. There's no question that these are powerful techniques, that have the potential to make code much more compact. My own feeling, though, is that these techniques are often overused, with the result that code becomes difficult to read and maintain. There aren't -- in my view -- all that many cases where it makes sense to use functional programming structures in what is, after all, a procedural language. However, there are a few, well-known instances where these techniques can lead to a huge improvement in both the readability and compactness of code.
A good example is in applying actions to user interface gestures. It's not uncommon for a user interface to have hundreds of elements, each of which the user can interact with in many different ways. Each of these interactions leads to a specific action, which must be specified in code.
The boilerplate way to specify the behaviour of a user interface in Java Swing (or Android, at least until recently) was to apply a listener to each user interface element; each listener would take the form of an anonymous inner class, of which certain methods would be overridden. Every time I see code like this -- line after line of anonymous class boilerplate -- my heart sinks: it's incomprehensible, and impossible to maintain.
A more modern approach, using lambda functions, might have constructs like this:
exitButton.setClickAction (::exitProgram);Until you've worked extensively with the alternative, it's hard to appreciate just how radical an improvement this is -- at least for Java. We've been able to do the same thing in C using function pointers for forty years. What we can't easily do in C, but can readily do in Kotlin, is to specify user interface actions that take parameters and return results.
That Kotlin makes it easy to code user interface actions is perhaps one of the reasons it has been so rapidly adopted for Android -- but it probably isn't the only reason: see the next section.
Coroutines and threads
Kotlin supports the same thread model as Java: you can create a
subclass of Thread
, or implement the Runnable
interface. Such constructs generally create independent operating system
threads, just as they do in Java -- with all the problems of
locking and synchronization that entails.
Kotlin also supports coroutines as a first-class language feature. Coroutines offer a kind of cooperative multithreading, and feature strongly in languages like Lua and Python, that don't have "true" multithreading. Since Kotlin does support true multithreading -- because the JVM does -- it's perhaps a little odd that coroutines feature so strongly. However, there are situations in which coroutines are preferable to threads.
I suspect that many Java developers will be unfamiliar with coroutines, so here is a (Kotlin) example.
fun main() { runBlocking { launch { while (true) { delay (100L) println("CR1") } } launch { while (true) { delay (100L) println("CR2") } } } }Running this code will result in "CR1" and "CR2" being written to the console, in strict order, indefinitely. There are two
while
loops, that run as separate coroutines, each of which just delays
and then prints its text in an endless loop.
This formulation certainly appears to be creating two independent
threads, one in each launch
block. It works because the
delay()
method is not simply a time delay -- it's an
opportunity to wake up other coroutines that are also blocking
(in a delay, or in some other way). The method Thread.sleep()
is simply a delay, and any task switching that happened during
the delay period would be the result of thread scheduling in the
operating system. The two coroutines, however, run in the same
thread -- this is something you can prove by running the
program and using jstack
to dump a list of running
threads.
This is "cooperative multithreading" because coroutines have to be
aware that they are running as coroutines, and take the necessary
steps to ensure that other coroutines get a share of the CPU.
If we remove the delay
call in one of the coroutines
above, that coroutine wouldn't just get a bigger share of the CPU,
it would get all the CPU, because there would be nothing
to switch execution to a different coroutine.
Coroutines are potentially very useful in implementing event-driven user interfaces. Typically the user interface of a particular application will be managed on a single thread, so actions initiated by the user interface must complete quickly. If this doesn't happen, then the whole user interface stalls. If the user starts an action that takes a significant time -- creating a network connection, for example -- then the whole program will appear unresponsive. Unless specific provision is made, it won't even be possible to cancel the long-running action, because the user interface won't respond to the user.
This problem can be solved, in principle, by putting long-running actions into separate threads, and allowing the user interface to continue running. Unfortunately, doing this raises the problem of communicating between threads, and ensuring that the threads synchronize properly.
Coroutines potentially offer a better solution because they are not, in fact, threads, even though they allow different parts of a program to execute in a way that appears concurrent. The flow of execution between coroutines is deterministic and predictable, in a way that is not the case for true threads. This means that there are fewer opportunities for race conditions, and none at all for thread deadlocks.
Because coroutines are, by their very nature, cooperative, a user
interface based on coroutines must be co-routine aware. However, so
must any other action that might block. In the example above,
if we replace the delay()
call with a Thread.sleep()
,
the entire application fails -- the sleep()
method is
not coroutine-aware.
The Android user interface model is one that is well-suited to programming using coroutines. This, again, is perhaps a reason why Kotlin has become so closely associated with Android.
Finally, it's worth bearing in mind that coroutines can be implemented in Java. This must be true, because Kotlin code is running on a JVM. However, at present coroutines are not a first-class feature of Java, and most features in the standard Java runtime library will not integrate well with a coroutine implementation.
Null safety
Kotlin takes a much stronger line on the handling of null values than Java does. It's very difficult to see the infamous "null pointer exception" in Kotlin applications, because situations that might lead to it can be trapped at compile time.
It's perfectly legitimate to assign null
to a variable or
function argument in Kotlin, but the code must specifically denote
where this is allowed. The question mark (?) symbol indicates that
a variable or argument may be null. This snippet of code will not
compile:
fun foo (x: Int) { //... Do something on x } fun main() { var x: Int? = null foo (x) }
The value of x
in main
is allowed to be
null
, but the argument to foo()
is not.
The compiler will report that you can't assign an Int
to an Int?
-- they are, in a sense, different types of
variable.
This method of handling null values is not foolproof, because Kotlin can call Java code, which does not have the same checks. Still, it's a welcome step forward in terms of improving static code quality.
Maven compatibility
Maven is a build tool and dependency manager that is widely used by Java developers. Recent versions of Maven are compatible with Kotlin, and the use of Kotlin and Java in the same project is well documented.
Debugging
Because Kotlin applications run on a Java JVM, all the Java debugging methods we have come to know and (?)love work identically with Kotlin. We can collect thread and heap dumps, look at GC logs, use a Java debugger to insert breakpoints, etc.
In practice, however, there is not the same degree of correspondence between source code and bytecode for Kotlin as there is for Java. Some Kotlin features that are easy to express as source code result in the generation of very complicated bytecode, supported by very complicated libraries. A case in point is coroutines -- there's really no clear mapping between the source code we want to debug, and its implementation as bytecode. Looking at a thread dump from an application that uses coroutines is not at all revealing.
Some IDE tools have debuggers specifically for Kotlin, and I imagine that this trend will continue.
Discussion
As a long-time Java developer, I find Kotlin a strange creature. I suspect that most experienced Java programmers could become proficient with Kotlin in a few hours -- there's almost a one-to-one mapping between Java features and Kotlin features. Some Java developers will struggle with coroutines and, I suspect, many have not yet fully embraced lamdas and functional programming. But that's not the fault of Kotlin -- these are well-established programming paradigms, and there's nothing particularly awkward about their Kotlin implementations.
And yet...
Does Kotlin offer any improvement over Java? Its proponents talk about improved developer productivity, and it's certainly true that many common design patterns can be expressed quickly and concisely with Kotlin. Kotlin does seem to require less boilerplate code than Java, but I suspect most Java developers use IDE tooling to generate all the boilerplate.
Kotlin seems to me a language that was developed in a highly opinionated way, and with very specific applications in mind. I find it almost impossible to account for some of the design decisions.
For example, Kotlin has certain features that suggest that static code quality is a priority -- strict assignment rules for integers of different size, warnings about unused variables, null safety. At the same time, the language sheds checked exceptions, and does not take the opportunity to implement const correctness -- which requires exactly the same kind of static checking as null safety does.
Kotlin implements coroutines and traditional threads as first-class language features -- I suspect it is the only mainstream programming language that does this. I'm sure there's a good reason for this, and it's called Android.
It almost looks as if Kotlin has been designed specifically to develop Android apps. The static code quality features are particularly important here, because run-time debugging on Android is horrific. Android needs concise, expressive ways to code user interface handlers, and the functional programming elements of Kotlin work well here. There's no need to provide checked exceptions because, if an exception is not trapped, it will just shut the app down -- and that's something that Android apps have to be designed to tolerate anyway.
Nevertheless, I find the inclusion of operator overloading a little strange. This is a feature I welcome, and whose absence from Java I lament; and yet I doubt that it will be of much interest to many developers. The maintainers of the Java language have steadfastly refused to include this feature. In fact, it's one of the few C++ features that was deliberately excluded from Java, that has not crept back in over time. Similarly, I don't fully understand why the designers of Kotlin would decide to include unsigned integer types, while the Java designers decided not to. Either decision is comprehensible; it's just not clear why Java would go one way and Kotlin the other. The reason for including extension classes is also not very clear to me, although this has proven to be a popular feature.
I suspect that, like Perl, Kotlin -- and Java, to some extent -- is a language of expediency. It seems to have a set of features that make it easy and safe to code constructs that are particular important for Android development.
Still, there are uses for Kotlin outside Android. Apache Camel -- an integration framework for middleware applications -- now has native support for Kotlin, as does the Wildfly application server. Clearly there are reasons to prefer it over Java, but whether those reasons are technical or expedient, I really couldn't say.