Sprachen für die JVM im Zusammenspiel mit Java

Neuere Sprachen für die Java-Plattform wie Groovy, Scala oder Clojure lassen sich statt oder in Kombination mit Java einsetzen. Mit ihnen können Java-Entwickler ihre tägliche Arbeit spürbar verbessern. Denn Java enthält durchaus einige Defizite, die sich auch in neuen Versionen wie Java 7 oder 8 nicht beseitigen lassen.

In Pocket speichern vorlesen Druckansicht 16 Kommentare lesen
Lesezeit: 19 Min.
Von
  • Kai Wähner
Inhaltsverzeichnis

Neuere Sprachen für die Java-Plattform wie Groovy, Scala oder Clojure lassen sich statt oder in Kombination mit Java einsetzen. Mit ihnen können Java-Entwickler ihre tägliche Arbeit spürbar verbessern. Denn Java enthält durchaus einige Defizite, die Oracle aufgrund der geforderten Abwärtskompatibilität auch in neuen Versionen wie Java 7 oder 8 nicht beseitigen kann.

Die Java-Plattform umfasst weit mehr als nur die Programmiersprache Java. Sie besteht auch aus Bibliotheken wie JDBC, JMX oder Swing, Deployment-Techniken, Tools wie den Java-Compiler sowie der Java Virtual Machine (JVM), der den Bytecode der Java-Programme ausführenden Laufzeitumgebung. Zudem bietet die Plattform:

  • Lauffähigkeit auf allen wichtigen Systemen,
  • Ausgereiftheit und hohe Stabilität,
  • eine gute Performance,
  • vielfältige Konfigurierbarkeit wie Garbage-Collection- oder Heap-Einstellungen,
  • eine große Entwickler-Community mit qualitativ hochwertigen Bibliotheken und
  • eine große Verbreitung.

Die Sprache Java verfügt jedoch auch über einige Eigenschaften, die nicht für alle Einsatzszenarien hilfreich sind. Beispielsweise ist Java streng statisch typisiert, dynamische Fähigkeiten sind erst mal nicht vorhanden. Die Sprache ist darüber hinaus nicht vollständig objektorientiert und besitzt implizit keine funktionalen Konzepte. Des Weiteren verwendet die Sprache für die Parallelprogrammierung ein komplexes Programmiermodell mit Threads und Locks. Aufgrund garantierter vollständiger Rückwärtskompatibilität ist eine Flexibilität hinsichtlich Optimierungen und Neuerungen schwierig zu leisten.

Das hinterlässt eine Lücke, in die andere auf der Java-Plattform laufende Sprachen stoßen. Diese sogenannten JVM-Sprachen leisten das, was bei .NET mit C, C++, C#, F# oder Silverlight (XAML) schon lange üblich ist: Sprachauswahl nach dem Motto "Right tool for the right job". Voraussetzung hierfür ist, dass die neuen JVM-Sprachen "nur" kompatiblen Bytecode generieren müssen, um auf der JVM einsetzbar zu sein.

Bekannteste Vertreter der JVM-Sprachen sind wohl Groovy, Scala, der Lisp-Dialekt Clojure sowie die Portierungen anderer Sprachen wie Jython (Python), JRuby (Ruby), Rhino (JavaScript) und Erjang (Erlang). Mit Ceylon, Fantom und Kotlin sind außerdem gleich drei neue Sprachen in der Entstehung, die zurzeit die Gunst der Öffentlichkeit zu gewinnen suchen. Daneben existieren etwa 240 weitere JVM-Sprachen – oftmals jedoch nur mit Forschungscharakter. Die Sprachen gehen viele der geschilderten Defizite Javas unterschiedlich an. Beispielsweise nutzen einige gänzlich andere Ideen, etwa dynamische Typisierung, Mehrfachvererbung oder funktionale Konzepte.

Dieser Artikel zeigt, warum Entwickler neben Java auch andere Programmiersprachen auf der Java-Plattform einsetzen sollten. Dabei ist es nicht immer sinnvoll, gleich das gesamte Projekt in einer anderen Sprache zu realisieren – oft ist nur ihr Einsatz in einzelnen Teilbereichen sinnvoll.

Hat man weniger Quellcode zu schreiben, bedeutet das nicht nur weniger Aufwand in der Entwicklung, sondern auch einen geringeren Aufwand für das Lesen, Analysieren, Refactoring und Debugging des Codes. Fast alle wichtigen JVM-Sprachen bringen diese Eigenschaft gegenüber Java ins Spiel. Beispielsweise können Entwickler mit ihnen oft Punkte und Klammern bei Methodenaufrufen sowie Strichpunkte einfach weglassen. Durch das Überladen von Operatoren lassen sich zudem lange Methodennamen ersetzen. Zusätzliche Sprachfunktionen wie das Weglassen von Typen bei dynamischen Sprachen oder automatische Typinferenz bei statischen Sprachen sind ebenfalls anzutreffen. Ein gutes Beispiel für wenig Quellcode ist die native Unterstützung bei der XML-Verarbeitung, wie das folgende XML-Dokument zeigt:

<books>
<book isbn="...">
<title>Groovy in Action</title>
<author>Dierk Koenig</author>
<author>Paul King</author>
...
</book>
<book isbn="...">
...
</book>
</books>

Soll der Titel des zweiten Buchs ausgegeben werden, reichen dazu beispielsweise in Groovy folgende zwei Zeilen aus:

root = new XmlSlurper().parse('books.xml')
println root.book[1].title

Auf den entsprechenden Java-Code – egal ob mit JAXB (Java Architecture for XML Binding) oder einer anderen Bibliothek – wird aus Platzgründen verzichtet.

Zeitgemäße JVM-Sprachen bieten eine hohe Flexibilität. Oft ist beispielsweise eine viel feinere Modularisierung von Paketen und Klassen möglich, und eine Datei kann mehrere Klassen oder Interfaces enthalten. Auch eine Verschachtelung ist denkbar. In Java würde das unübersichtlichen Code erzeugen, da Klassen viel Boilerplate Code wie Konstruktoren oder Getter- beziehungsweise Setter-Methoden erfordern. Die alternativen JVM-Sprachen benötigen das nicht zwangsweise. Daher kann solch eine Strukturierung oftmals durchaus Sinn ergeben und für eine bessere Übersicht sorgen. Insbesondere beim Einarbeiten in neue Bibliotheken und APIs fällt es dann viel einfacher, wenn man das gesamte Beispiel trotz zahlreicher Klassen in eine Datei schreiben kann. Zusätzlich ermöglichen sogenannte Mixins, eine Art Mehrfachvererbung, ebenfalls eine höhere Flexibilität.

Dynamisch typisierte Sprachen stehen für eine hohe Flexibilität, da sie Verhalten ohne Rekompilierung oder Redeployment zur Laufzeit ändern können. Mittlerweile lassen sie sich mit dem JSR 292 (Supporting Dynamically Typed Languages on the Java Platform) aus Java 7 deutlich einfacher und performanter in die JVM integrieren. Damit können Entwickler Änderungen ohne aufwendige Releasezyklen durchführen. Durch den schon länger etablierten JSR 223 (Scripting for the Java Platform) können sie zudem nur einzelne Skripte von Java aus aufrufen und in Skript-Engines ausführen.

Meta-Programmierung ermöglicht es dynamischen Sprachen sogar, kompilierten Bytecode zu ändern und neue Funktionen hinzufügen – egal von welcher Sprache der Bytecode stammt. Im folgenden Beispiel erweitert Groovy die Java-Klasse Integer um die Methode getSquareRoot, die die Quadratwurzel berechnet. Dazu wird in einem Closure die delegate-Referenz übergeben, welche den Wert des Inputs enthält. Die Methode sqrt der Java-Klasse Math führt die Berechnung durch.

Integer.metaClass.getSquareRoot = { ->
Math.sqrt(delegate)
}

// 16 ist ein Java-Integer, der die Methode squareRoot eigentlich
// gar nicht besitzt, dennoch bietet Groovy nun diese Methode an.
assert 4 == 16.squareRoot

Bei aufwendigeren Beispielen gestaltet sich auch die Meta-Programmierung deutlich komplexer. Oftmals kann der Entwickler aber einfach nur auf die Funktionen vorhandener Bibliotheken zugreifen. Ein Beispiel aus der Praxis sind die dynamischen Finder-Methoden des Groovy-Frameworks Grails. Für jede Datenbank-Entität wird für jede Kombination von Attributen wie Name oder Wohnort dynamisch ein Finder erzeugt, der eine entsprechende Datenbankabfrage durchführt. Der Entwickler muss Code nicht selbst schreiben, sondern nur die dynamisch zur Laufzeit generierte Methode aufrufen. Im genannten Beispiel gäbe es neben vielen anderen die Methoden findByName, findAllByNameAndWohnort und findAllByNameOrWohnort.

Doch auch statisch typisierte Sprachen können Teile der dynamischen Meta-Programmierung simulieren, zu sehen an der impliziten Typumwandlung in Scala. Das folgende Beispiel wandelt einen String automatisch in eine Integer-Klasse um.

implicit def str2Int(str: String) = str.toInt

Dadurch ist beispielsweise die Berechnung 10 - "3" möglich und würde 7 als Ergebnis zurückgeben, obwohl Integer gar keine Methode zum Subtrahieren eines Strings anbietet. Ohne die implizite Typumwandlung würde daher eine Exception geschmissen werden.

Auch das Build-Management profitiert von den JVM-Sprachen. Ein Build automatisiert diverse Aufgaben, um lauffähige Software zu generieren, beispielsweise das Kompilieren von Quellcode, das Einbinden externer Bibliotheken oder die Ausführung von Tests. Gradle ist ein Groovy-Tool, das die Flexibilität des Entwicklers erhöht. Es kombiniert die Mächtigkeit von Ant mit der Einfachheit von Maven. Bereits bekannte Ant-Tasks oder Maven-Repositories lassen sich damit einfach integrieren und weiterverwenden. Dank Groovy ist das Erstellen eines Builds oder die Implementierung von eigenen Erweiterungen mit Gradle allerdings wesentlich flexibler als bei Ant oder Maven. Beispielsweise lassen sich einfach Java-nahe, aber dennoch betriebssystemunabhängige Skripte schreiben. Für zukünftige, flexible Builds sollten Entwickler Gradle daher als Alternative in Betracht ziehen.

Die alternativen JVM-Sprachen können in einigen Situationen die Komplexität deutlich reduzieren, indem sie dem Entwickler ein höheres Abstraktionslevel anbieten. Ein gutes Einsatzgebiet ist eine interne domänenspezifische Sprache (Domain Specific Languages, DSL). Das ist eine formale, speziell für ein bestimmtes Problemfeld implementierte Sprache. Im besten Fall kann den DSL-Code sogar eine Person schreiben, die gar kein Entwickler ist.

Im Gegensatz zu Java lassen sich mit den JVM-Sprachen dank Features wie Currying, impliziter Typumwandlung, automatischer Typinferenz oder dem Weglassen unnötiger Zeichen einfache und gut lesbare DSLs erstellen. Diese fühlen sich oftmals fast wie eine natürliche Sprache an. Beispielweise können Tester einfach Unit-Tests schreiben, auch wenn sie keine Programmierexperten sind. Statt dem Java-Ausdruck

assertTrue(map.containsKey('a'));

lässt sich beispielsweise folgender klarer, sprechender Scala-Code schreiben:

map should contain key 'a' 

Ein weiteres Einsatzgebiet, das grundsätzlich hohe Komplexität erfordert und aufgrund von Multi-Core-Prozessoren in Zukunft zunehmend wichtiger wird, ist die Nebenläufigkeit. Java verwendet hierzu die Thread API und das Konzept des "Shared Memory", bei dem mehrere Prozesse auf die gleichen Variablen zugreifen können. Die Konsequenz sind oft Deadlocks, Race Conditions oder andere Probleme, die mit Locks und Synchronisation zu lösen sind. Dank Mutex-Verfahren kann immer nur ein Prozess einen Lock zu einem Zeitpunkt besitzen. Die Komplexität ist allerdings enorm hoch, Debugging und Fehlersuche sind zeitaufwendig. Auch die Vereinfachungen der letzten Java-Versionen im java.util.concurrent-Paket lösen dieses Problem nicht, obwohl mittlerweile immerhin auf die Low-level-Thread-Konstrukte wait, notify und join verzichten werden kann.

Einige JVM-Sprachen setzen daher statt auf das "Shared Memory"- auf das "Share Nothing"-Verfahren und ein höheres Abstraktionslevel. Dabei werden funktionale Konzepte wie unveränderliche Objekte eingesetzt. Beispielsweise wurde Clojure von vornherein explizit für Multithreading-Umgebungen entworfen. Das äußert sich vor allem in einer expliziten Trennung von Identitäten und Werten, die in der objektorientierten Welt miteinander verknüpft werden. Alle Werte und Datenstrukturen sind in Clojure von Grund auf unveränderlich. Identitäten sind nur innerhalb von Transaktionen veränderbar.

Clojure baut zwar implizit auf Java-Threads auf, kommt aber dennoch ohne explizite Locks und Synchronisierung aus, um sichere Nebenläufigkeit ohne Race Conditions zu realisieren. Der folgende Codeschnipsel zeigt das durch die Nutzung von Refs und des dosync-Makros gemäß Software Transactional Memory (STM).

(def acc1 (ref (make-account 1000 "alice")))
(def acc2 (ref (make-account 1000 "bob")))

(defn transfer [from to amount]
(dosync (alter from withdraw amount) (alter to deposit amount)))

(defn run-thread-fn [f]
"Runs function f in a new thread"
(.start (new Thread f)))

(run-thread-fn #(transfer acc1 acc2 100))
(list acc1 acc2)

Die Funktion transfer wird entweder ganz oder gar nicht ausgeführt. Neben STM existieren in Clojure noch weitere Konzepte für Parallelverarbeitung wie Atome und Agenten. Ausführliche Erläuterungen zu den nebenläufigen Konzepten von Clojure finden sich hier.

Leider ist Clojure für Java-Entwickler durch seine funktionale Herkunft und eine Lisp-ähnliche Syntax relativ aufwendig zu erlernen. Andere Sprachen wie Groovy oder Scala mit objektorientiertem Hintergrund eignen sich oftmals besser. Diese bieten mit Aktoren ebenfalls implizit eine Alternative für die einfache Umsetzung von Nebenläufigkeit. Im Gegensatz zu Clojure existieren zwar veränderliche Zustände, Aktoren tauschen Informationen aber ausschließlich über unveränderliche Nachrichten aus. Zustände werden nur innerhalb der Aktor-Instanzen verwaltet. Locks oder Synchronisierung werden ebenfalls nicht benötigt. Dynamische Sprachen setzen gleichfalls auf dieses Konzept. Während Groovy Aktoren mit der GPars-Bibliothek durch einfach Annotationen realisiert, sind sie in Erlang sogar ein implizites Kernkonstrukt.

Auch Aktoren bieten ein paar Fallstricke, die Komplexität von Locks und Synchronisierung erreichen sie aber nicht. Der Vollständigkeit halber sei erwähnt, dass STM und Aktoren sich auch in Kombination mit Java-APIs verwenden lassen, allerdings mit deutlich mehr Boilerplate Code und dadurch höherer Komplexität.

Der Einsatz alternativer JVM-Sprachen kann für eine Konsolidierung der IT-Landschaft sorgen. So braucht man für Build-Management, Skripte und dynamische Datenbankabfragen nicht mehr spezifische Sprachen wie XML, Shell oder PL/SQL verwenden, denn diese Aufgaben lassen sich mit der Programmiersprache selbst umsetzen. Während es mit Java unmöglich ist, "alles aus einer Hand" zu realisieren, kann das mit Sprachen wie Groovy oder Scala durchaus eine Option sein.

Nun stellt sich die Frage, welche modernen Sprachen neben Java eingesetzt werden sollen. Hier sind neben den geschäftsspezifischen Anforderungen einige Kriterien zu beachten: Zu prüfen wären unter anderem der Reifegrad der Sprache, die Verfügbarkeit stabiler Tools und Bibliotheken sowie die Frage, ob die Sprache schon reif für den Unternehmenseinsatz ist.

Ebenso spielt ihre Verbreitung eine wichtige Rolle. Dafür sollte geprüft werden, ob für die jeweilige Sprache überhaupt Entwickler verfügbar sind beziehungsweise wie leicht sich die eigenen weiterbilden lassen. Ein weiterer Indikator für die Verbreitung ist eine große Community sowie ein ausreichendes Maß an Fachliteratur, Tutorials, Blogs, Artikeln und Konferenzvorträgen. Hinsichtlich des Reifegrades und der Verbreitung sind neben Portierungen etablierter Sprachen wie JRuby oder Jython insbesondere Groovy und Scala empfehlenswert, da auf diese die genannten Kriterien zutreffen.

Ein weiterer wichtiger Aspekt bei der Auswahl der Sprache ist die Frage nach den Vorkenntnissen der Entwickler in ähnlichen Sprachen. Werden beispielsweise funktionale Konzepte benötigt, wird ein Lisp-Entwickler mit Clojure keine Schwierigkeiten haben, während ein Java-Entwickler wohl eher auf Scala setzen wird.

Auch Java-Nähe ist ein wichtiger Faktor, um den Einstieg zu erleichtern. Der folgende Code zeigt schon an einem kleinen Beispiel, wie groß die Unterschiede sein können. Während Groovy nahezu gleiche und Scala ähnliche Syntax bieten, haben viele andere Sprachen wie JRuby oder Clojure eine etwas oder gar stark abweichende Syntax.

import java.util.*; // Java 
Date today = new Date(); // Java

today = new Date() // Groovy

import java.util._ // Scala
var today = new Date // Scala

java_import 'java.util.Date' # JRuby
today = Date.new # JRuby

(import '(java.util Date)) ; Clojure
(def today (new Date)) ; Clojure

Letztlich sollte die Sprache gut im Umgang mit Java sein und einen echten Mehrwert schaffen. Daher empfiehlt es sich, neben Java zumindest eine Sprache mit impliziter Unterstützung für funktionale Konzepte wie Clojure oder Scala und eine dynamisch typisierte Sprache wie Groovy, JRuby oder Jython für Features wie Meta-Programmierung in petto zu haben.

Die Einstiegshürde ist häufig ein politischer Aspekt, denn die Einführung neuer Sprachen ist sowohl gegenüber Kunden als auch Vorgesetzten zu "verkaufen". Letztlich ist die Angst vor einer neuen Sprache meist unbegründet, denn die Komplexität ist nicht höher zu bewerten als die Einführung eines WSDL-Compilers für SOAP-Webservices oder des GWT-Compilers zur JavaScript-Generierung inklusive der benötigten Konzepte. Deshalb sollte eine einzige weitere Bibliothek für ein Groovy-, Scala- oder anderes JAR auch keine Bedenken verursachen – es muss schließlich kein Code angepasst werden.

Neben dem politischen Aspekt besitzen moderne JVM-Sprachen aber auch einige konzeptionelle und technische Probleme, die nicht unerwähnt bleiben sollen. So ist die Plattformintegration ein wichtiges Thema und von Beginn an zu beachten. Zwar generiert jede JVM-Sprache Bytecode. Dieser ist jedoch nicht immer 100 Prozent kompatibel mit Java. Es sind nämlich alle in Java nicht vorgesehenen Sprachfeatures zu unterstützen. Beispielsweise sind Groovys Mixins, Scalas erweiterte Zugriffsrechte (wie private[this]) oder Clojures Makros sowie viele weitere Konstrukte in kompatiblen Bytecode zu übertragen. Umso problematischer wird es, wenn man nicht die Integration mit Java, sondern zwischen zwei anderen JVM-Sprachen benötigt. Daniel Spiewak erläutert beispielsweise, wie aufwendig die Kommunikation zwischen JRuby und Scala ist. Allerdings darf nicht unerwähnt bleiben, dass die Probleme bekannt sind und sich meistens durch einfache Workarounds lösen lassen. So wirkt ein Java-Interface als Schnittstelle oft Wunder.

Ein weiteres Problem ist, dass dank der zusätzlichen Sprachfunktionen gegenüber Java oftmals mit viel Mächtigkeit geworben wird, beispielsweise durch Meta-Programmierung oder implizite Typumwandlung. Allerdings kann dadurch schnell Code entstehen, der durch die mächtigen Sprachmittel kaum mehr verständlich zu lesen oder debuggen ist. Es gilt also, die Features einer Sprache gewissenhaft an den passenden Stellen einzusetzen. Ein sinnvoller Einsatz ist beispielsweise die Realisierung einer internen DSL. Wie komplex der innere Code ist, interessiert den Nutzer der DSL oftmals nicht.

Neben der Mächtigkeit ist außerdem zu beachten, dass die Entwickler die neuen Konzepte verstehen. So können viele Java-Entwickler mit unveränderlichen Objekten, Rekursion und Aktoren aus funktionaler Programmierung kaum etwas anfangen geschweige denn diese effizient einsetzen. Dynamische Sprachen bringen zusätzliche Probleme mit sich. Da im Quellcode keine Typen anzugeben sind und Fehler erst zur Laufzeit erkannt werden, ist der Quellcode schwerer lesbar und wartbar.

Hinzu kommt, dass die Unterstützung durch die gängigen Entwicklungsumgebungen zwar zunehmend besser wird, aber bei weitem noch nicht das Niveau von Java erreicht hat. Auch Tools zur Codeanalyse wie Sonar sind oft nicht kompatibel.

Der Einsatz moderner JVM-Sprachen neben Java ist in vielen Situationen durchaus sinnvoll und schafft Mehrwert. Wichtig ist dabei, dass im neuen Projekt nicht vollständig umgestiegen werden soll oder muss, sondern diese Sprachen als gute Ergänzungen für die Java-Plattform zu sehen sind. Java bleibt in den nächsten Jahren die Kernsprache der Java-Plattform. Allerdings werden sich um Java herum weitere moderne JVM-Sprachen in konkreten Einsatzgebieten aufgrund ihrer Vorteile etablieren.

Wichtig ist, dass in einem Projekt unterschiedliche Konzepte verfügbar sind. So sollte das Entwicklerteam neben der Objektorientierung mit Java auch eine funktionale und eine dynamische Sprache in petto haben, um möglichst flexibel auf die Anforderungen reagieren zu können.

Für den Einstieg empfiehlt es sich, nicht auf die Referenzbücher zurückzugreifen, die alle Features der jeweiligen Sprache bis ins kleinste Detail erläutern. Besser geeignet sind Bücher wie "Making Groovy Java" oder "Grundkurs Funktionale Programmierung mit Scala", da diese ausgehend vom Wissen eines Java-Entwicklers auf die neue Sprache und deren neue Konzepte eingehen. Nach der Einarbeitung lassen sich die Sprachen zunächst in internen Projekten einsetzen, bevor sie später allein oder in Kombination mit Java an geeigneten Stellen auf die Kundenprojekte losgelassen werden.

Kai Wähner
ist als IT-Consultant bei der MaibornWolff et al GmbH tätig. Seine Schwerpunkte liegen in den Bereichen Java EE, SOA und Cloud Computing.

  • Kenneth A. Kousen; Making Java Groovy; Manning (Early Access Edition) 2011-12
  • Lothar Piepmeyer; Grundkurs funktionale Programmierung mit Scala; Hanser, 2010
  • Stefan Tilkov; Gelungene Mischung; Clojure: Ein pragmatisches Lisp für die JVM; Artikel auf heise Developer
  • Stefan Kamphausen, Tim Oliver Kaiser; Moore in mind Parallelprogrammierung mit Clojure; Artikel auf heise Developer
  • Heiko W. Rupp; Polyglotte Bohnen; Java-6-Scripting mit JRuby; Artikel auf heise Developer

(ane)