iX 10/2017
S. 56
Titel
Java-Alternativen
Aufmacherbild

Ein Überblick zu den wichtigsten JVM-Programmiersprachen

Fremdsprachenkurs

Groovy, Scala, Clojure, Ceylon, Kotlin und Fantom sollen frischen Wind auf die Java Virtual Machine bringen. Doch welche Programmiersprache ist für welchen Anwendungsfall sinnvoll, und was macht sie jeweils aus?

Die Java Virtual Machine (JVM) hat sich im letzten Jahrzehnt von einer Laufzeitumgebung für Java-Programme zu einer polyglotten VM gewandelt. Neben Java gibt es auf ihr inzwischen etliche andere Programmiersprachen. Es existieren sowohl Portierungen bekannter Sprachen wie Python oder Ruby als auch Neuentwicklungen. Der vorliegende Artikel beschäftigt sich mit sechs Programmiersprachen, die für die JVM entwickelt wurden und zu Bytecode kompilieren. Xtend und ähnliche Projekte, in denen ein Compiler die Quellen in Java-Code überführt (auch als Transpilierung bezeichnet), sollen nicht berücksichtigt werden.

Einige Beispiele stellen Besonderheiten und interessante Konzepte der unterschiedlichen Programmiersprachen vor, wobei nur wenige davon exklusiv in einer Sprache vorhanden sind. Sie beeinflussen sich vielmehr gegenseitig. Daher ist es umso wichtiger, den Überblick über den aktuellen Stand zu behalten.

Groovy

Groovy ist eine dynamisch typisierte Programmiersprache, deren Entwicklung 2003 begann. Eine statische Typprüfung ist in ihr optional möglich. Der Einstieg gestaltet sich recht einfach, da die meisten Java-Programme auch in Groovy gültig sind. Im folgenden Codeausschnitt definieren Entwickler mit def eine Funktion und interpolieren den ausgegebenen String beziehungsweise ersetzen die enthaltene Variable. Es ist zudem ersichtlich, dass Semikolons und Klammern beim Methodenaufruf optional sind:

def sayHello(name) {
    println "Hallo $name!"
}
sayHello "JavaLand"

Eine der Stärken von Groovy ist die breite Unterstützung von Collections. So gibt es einen Typ Range, der ein Intervall erzeugt. Im nächsten Beispiel ist der Typ explizit angegeben, was sich insbesondere bei Methodenparametern zur besseren Dokumentation empfiehlt. Die Methode collect (der Name stammt aus Smalltalk und ist in anderen Programmiersprachen auch als map bekannt) ruft eine Funktion für jedes Element einer Collection auf und erzeugt aus den Rückgabewerten eine neue Collection. Im Codeausschnitt wurde das durch den impliziten Parameter it referenzierte Element mit 2 multipliziert.

Range range = 1..10
def doubled = range.collect {
    it * 2
}

Wenn alle Elemente einer Collection auszuwählen sind, ist der Star-Dot-Operator nützlich:

def list = ["Star", "Dot", "Operator"]
def loweredList = list*.toLowerCase()

Das Programm ruft für alle Elemente der Liste die Funktion toLowerCase auf. Auch für Maps gibt es in Groovy vielfältige Verwendungsmöglichkeiten, es lassen sich beispielsweise Funktionen in ihnen speichern und anschließend aufrufen:

def map = [
    function: {
        println "Eine Funktion in einer Map!"
    }
]
map.function(). 

Ihr Einsatz ist sinnvoll, wenn das Erstellen einer Klasse zu viel Overhead birgt, beispielsweise wenn ein Objekt für Tests zu mocken ist.

Listing 1: Groovy vereinfacht die Behandlung von Nullwerten

def conference = [name: "JavaLand"]
// ternärer Operator
println conference.name ?
    conference.name : "Unknown"
// Elvis-Operator
println conference.name ?: "Unknown"
// Safe-Navigation-Operator
conference.name?.toLowerCase()

Der ungewollte Zugriff auf Nullwerte und die daraus resultierende Null-Pointer-Exception gehören zu den am häufigsten auftretenden Fehlerquellen in Java. Groovy bietet mit dem Elvis-Operator (der übrigens so heißt, weil er an die Haartolle von Elvis Presley erinnert) eine vereinfachte Form des ternären Operators an, um beispielsweise einen Standardwert zu verwenden, wenn eine Variable null ist (null ist in Groovy false). Der in Listing 1 gezeigte Operator ist also äquivalent zum Elvis-Operator. Darüber hinaus erspart der Safe-Navigation-Operator die Überprüfung auf Nullwerte bei der Navigation eines Objektgraphen, da er null zurückgibt, sobald ein Nullwert vorkommt.

Als abschließendes Beispiel sollen noch kurz die Builder vorgestellt werden, die das Erstellen von verschachteltem Markup erleichtern. Das folgende Codebeispiel zeigt, wie sich eine einfache HTML-Seite mit dem XML-Markup-Builder generieren lässt:

def builder =
    new groovy.xml.MarkupBuilder()
builder.html {
    body {
        a(href: 'http://groovy-lang.org',
            "Mit Groovy erstellt")
    }
}

Groovy ist eine ausgereifte Sprache, die einige Verbesserungen gegenüber Java mitbringt. Viele Projekte setzen sie als Ergänzung ein, um bestimmte Teile einer Applikation einfacher entwickeln zu können. So sind Tests, Skripte oder domänenspezifische Sprachen ein oft genannter Anwendungsfall für Groovy. Aber auch als Hauptsprache ist Groovy aufgrund der zahlreichen Features und der optionalen statischen Typprüfung eine gute Wahl. Zur Entwicklung von Webapplikationen bietet sich mit Grails ein robustes und durchdachtes Framework an.

Scala

Scala ist – wie Groovy – im Jahr 2003 entstanden. Ein Team um Martin Odersky an der Schweizer Universität EPFL (École polytechnique fédérale de Lausanne) wollte eine Programmiersprache schaffen, die statisch typisiert ist und objektorientierte mit funktionalen Paradigmen vereint. Im folgenden Codebeispiel ist die statische Typisierung beim Methodenparameter ersichtlich, allerdings kommt hier die Postfix-Notation zum Einsatz, das heißt, der Typ steht nach dem Variablenbezeichner:

def sayHello(name: String) {
    println(s"Hallo $name!")
}
sayHello("JavaLand")

In Scala gibt es die Konvention, dass Klammern bei Funktionsaufrufen mit Seiteneffekten stets mit angegeben werden, damit deutlich ist, dass es sich nicht um einen Operator handelt. Der Nebeneffekt von sayHello ist die Ausgabe auf der Konsole.

Obwohl Scala statisch typisiert ist, lässt sich die Typangabe in vielen Fällen weglassen. Der Compiler bestimmt dann den passenden Typ (Typinferenz). Im nächsten Beispiel weiß er etwa, dass setB vom Typ Set[Int], ist und die Typangabe kann entfallen. Bei doubleB ist klar, dass ein Int zurückgegeben wird. Bei Methodenparametern wird der Typ nicht inferiert und ist immer explizit anzugeben:

val setA: Set[Int] = Set(1, 2, 3)
val setB = Set(1, 2, 3)
def doubleA(i: Int): Int = i * 2
def doubleB(i: Int) = i * 2

Ein mächtiges Sprachkonstrukt in Scala sind die for-Ausdrücke. Wie im folgenden Code gezeigt, lassen sie sich zum Iterieren (von 1 bis 10) und Filtern (alle geraden Zahlen) verwenden:

val even = for {
    i <- 1 to 10
    if i % 2 == 0
} yield i
assert(List(2, 4, 6, 8, 10) == even)

Wurde ein passendes Element gefunden, kann das Programm es mit yield zurückgeben, sodass sich am Ende eine neue Collection mit allen passenden Elementen erzeugen lässt.

Listing 2: Pattern Matching in Scala

case class Conference(name: String)
val javaLand = Conference("JavaLand")
javaLand match {
    case Conference("JavaLand") =>
        println("JavaLand!")
    case _ =>
        println("Default Case")
}

Pattern Matching ist die generalisierte Form des bekannten switch-Statements, allerdings ohne Fall-through. Da sich ganze Objektgraphen matchen lassen, eröffnen sich vielfältige Möglichkeiten. In Listing 2 ist eine Konferenz mit einem bestimmten Namen zu suchen. Dafür wird eine case-Klasse erstellt, die sich in einer Fallunterscheidung verwenden lässt, da der Scala-Compiler sie entsprechend aufbereitet.

Scala bietet mit Traits die Möglichkeit, einzelne Aspekte auszulagern und in Klassen unterzubringen (sogenannte Mixins). Das Vorgehen stellt eine Form der Mehrfachvererbung dar, die allerdings durch Delegation realisiert wird und so das Diamantenproblem geschickt umgeht. Dabei werden die super-Aufrufe dynamisch gebunden und in eine lineare Abfolge gebracht. Die Bindung von super ist so zur Laufzeit eindeutig (nämlich immer an den nächsten Trait in der Hierarchie). Dank der Linearisierung lassen sich Mehrdeutigkeiten vermeiden und die Vorteile der Mehrfachvererbung wie die feingranulare Aufteilung in Aspekte nutzen. Im folgenden Beispiel werden die Merkmale „schwimmen“ und „fliegen“ in Traits ausgelagert und lassen sich dann einmischen. Hier ist ein Pinguin ein Vogel, der um den Aspekt „schwimmen“ erweitert wird, ein Adler hingegen ist ein fliegender Vogel: