Project Valhalla: Value Types in Java

Bei Java findet man bis einschließlich der kommenden Version 9 den Ansatz "Alles ist ein Objekt (bis auf die acht eingebauten Typen)". Mit dem Projekt Valhalla wird sich das ändern: Künftig lassen sich die Konzepte der Programmierung "Objekt" und "Wert" gemeinsam und gleichberechtigt verwenden.

In Pocket speichern vorlesen Druckansicht 22 Kommentare lesen
Lesezeit: 12 Min.
Von
  • Henning Schwentner
Inhaltsverzeichnis

Objekt und Wert sind zwei grundsätzliche Konzepte der Programmierung. Wer eine Programmiersprache entwirft, muss sich für eines der beiden entscheiden. Bei Java findet man bis einschließlich der kommenden Version 9 den Ansatz "Alles ist ein Objekt (bis auf die acht eingebauten Typen)". Mit dem Projekt Valhalla wird sich das ändern: Künftig lassen sich beide Konzepte gemeinsam und gleichberechtigt verwenden.

Java ist eine objektorientierte Programmiersprache. Objekte sind demnach das zentrale Konzept. Doch was kennzeichnet ein Objekt? Erstens hat es eine Identität. Zweitens existiert es in der Zeit. Das heißt, es wird erzeugt und gegebenenfalls irgendwann wieder vernichtet. Drittens hat es einen Zustand, der sich über seine Lebenszeit ändern kann. Ein Objekt ist also etwas Konkretes.

Betrachtet sei das am Objekttyp "Konto". Ein Objekt dieses Typs ist "Konto mit der Nummer 4711". Es wurde irgendwann angelegt (da wurde das Objekt erzeugt) und könnte gekündigt werden – dann würde das Objekt zerstört. Während seiner Lebenszeit kann sich der Kontostand erhöhen und verringern, also der Zustand ändern.

Mehr Infos

Neues aus der Java-Welt

Für 2017 sind mit Java EE 8 und Java 9 neue Versionen der Programmierplattform angekündigt. Eine lose Folge von Artikeln zu den wichtigsten Neuerungen wird auf heise Developer Einblicke in die nahe, hier auch ferne Java-Zukunft geben.

Was ist nun ein Wert? Er hat keine Identität, keinen Zustand und existiert unabhängig von der Zeit. Ein Wert ist demnach eine Abstraktion. Wenn man zum Beispiel alle Paare in der Welt (das sind Objekte) anschaut, könnte eine Abstraktion davon das Konzept (der Wert) "2" sein. Trennt sich ein Paar, hat das keinen Einfluss auf das Konzept. Selbst wenn sich alle Paare trennen sollten, beeinträchtigt das die Abstraktion (den Wert) "2" nicht. Die Idee "2" existiert außerhalb der Zeit, ändert sich nicht und hat auch keine Identität. "2" ist also ein typischer Wert.

Sowohl für Objekte als auch für Werte lassen sich Typen definieren. (Erinnerung an die Programmiersprachentheorie: Ein Datentyp ist eine Menge von Werten plus die darauf definierten Operationen.) Diese unterteilt man dann in Objekt- und Werttypen.

Soweit die allgemeine Sicht. Aber was bedeutet das für Java-Programmierer? Objekttypen (und dadurch Objekte) werden in Java mit Klassen definiert. Sie sind dort Referenztypen. Daraus folgt: Variablen können nie ein Objekt selbst, sondern immer nur eine Referenz auf ein Objekt enthalten. Die Identität eines Objekts wird durch die Referenz (d. h. Speicheradresse) repräsentiert. Den Vergleich mit einer Identität implementiert man mit den Operatoren "==" und "!=". Die Prüfung auf Gleichheit lässt sich pro Typ durch Überschreiben der Methode equals() definieren.

Um herauszufinden, wo Objekte existieren, muss man sich anschauen, wie Programme ausgeführt werden. Ein Programm (auch Prozess genannt) besteht grob aus zwei Speicherbereichen – dem Call Stack und dem Heap. Der Stack funktioniert so: Ein laufendes Programm befindet sich immer an genau einer Stelle seines Codes (wenn man Multithreading außer Acht lässt). Bei Aufruf einer Methode werden ihre Parameter auf den Stack gelegt und Speicher für die lokalen Variablen reserviert. Im Anschluss führt es die Methode aus, letztere arbeitet also mit Daten. Ist die Methode abgearbeitet, werden ihre Parameter und lokalen Variablen abgeräumt und der Rückgabewert auf den Stack gelegt. Dann befindet man sich wieder in der aufrufenden Methode. Wie groß der (Stack-)Speicherbedarf eines Methodenaufrufs ist, lässt sich schon zur Übersetzungszeit feststellen.

Der Heap ist dagegen einfach ein ungeordneter Haufen Speicher, aus dem das Programm sich beliebig große Stücke reservieren kann. Wie viel Heap-Speicher es genau braucht, zeigt sich erst zur Laufzeit. Um das Abräumen ungenutzter Speicherbereiche müssen sich Entwickler (oder der Garbage Collector) im Gegensatz zum Call Stack selbst kümmern. Die Garbage Collection kostet wertvolle Performance.

Nun zurück zur Frage, wo Objekte "leben". Durch den Einsatz des Operators new legt das Programm sie auf dem Heap an.

Und wie ist es bei Werten? Sie liegen direkt im Speicher der Variable – es sind eben keine Referenztypen. Zwei Variablen mit der gleichen Belegung zählen als gleich. Eine Identität gibt es bei Werten nicht. Werte liegen deshalb direkt auf dem Call Stack vor – oder direkt in den Objekten, zu denen sie gehören.

Als Werttypen gibt es in Java nur die acht vordefinierten (sog. eingebauten) Typen. Das sind boolean, byte, char, int, short, long, float und double. Variablen enthalten immer den Wert selbst. (In Java gibt es keine Zeiger oder Referenzen auf Werte.) Gleichheit wird durch den Vergleich des Variableninhalts geprüft. Die Operatoren "==" und "!=" implementieren darum bei Werten eine Prüfung auf Gleichheit.

Selbstdefinierte Werttypen gibt es in Java im Gegensatz zu beispielsweise C# bisher nicht – und zwar weder technische wie "unsigned" oder "complex" noch fachliche wie "Postleitzahl" oder "Geldbetrag". Eine Krücke ist dafür die Verwendung des Musters der "value-based classes". Solche Klassen haben nur finale Felder. Ihre Methoden dürfen nur erkunden und nicht den Zustand ändern. Außerdem muss die Methode equals() so implementiert sein, dass sie nur die Belegung der Felder vergleicht und nicht von der Identität abhängt.

Damit wurde zumindest die Werteigenschaft der Unveränderlichkeit abgebildet. Allerdings sind die Exemplare der "value-based classes" immer noch Objekte (wenn sie auch Werte abbilden). Das heißt, sie existieren auf dem Heap. Daraus folgen zwei Nachteile. Erstens haben sie immer noch eine Identität. Zwei Objekte, die gleich, aber nicht identisch sind, liegen an unterschiedlichen Adressen und haben also unterschiedliche Referenzen. Entwickler dürfen bei diesem Muster den Operator "==" nicht verwenden, weil die Variablen die Referenzen enthalten. Zweitens ist durch die Referenz mehr Speicher nötig. Das ist gerade bei kleinen Dingen spürbar.

Dramatisch wird es, wenn man nicht nur einen einzelnen Wert hat, sondern ein Array von Werten. Ein Beispiel: Bei einem int-Array liegen die Werte einfach hintereinander im Speicher (int ist hier der eingebaute Typ). Ein Array von Integer dagegen ist ein Array von Referenzen auf Integer. Eine int-Variable belegt 32 Bit, eine Referenz hat auf einem 64-Bit-System 64. Hier wird dreimal so viel Speicher (64 Bit für die Referenz plus 32 für den eigentlichen Wert) belegt, wie nötig. Außerdem liegen zwar die Referenzen in einer Reihe im Speicher, aber die referenzierten Objekte sind dort beliebig verteilt. Das und der Aufwand, der für das Dereferenzieren nötig ist, verlangsamen die Iteration über die Daten.

Diese Probleme hat auch das Java-Team bei Oracle erkannt und Maßnahmen ergriffen. Das Ziel von Project Valhalla ist es, benutzerdefinierte Werttypen als "first-class citizens" in die Sprache einzuführen. Dazu sind Änderungen einerseits an der Programmiersprache Java, andererseits an der JVM vorzunehmen. Im Folgenden geht es allein um die Modifikationen in der Sprache. Weil noch nicht klar ist, wie die endgültige Syntax aussehen wird, verwendet das Projekt eine bewusst hässliche Strohmann-Syntax. Damit will das Team Diskussionen dazu verschieben. Das Motto des Projekts ist: "Works like an int – codes like a class."

Wenn man sich ein Koordinatensystem vorstellt, dann ist eine Koordinate ein typischer Wert. Sie enthält zwei Felder, je eines für die Position auf der x- und auf der y-Achse. Mit der neuen Strohmann-Syntax wäre ein Werttyp Coordinate wie folgt zu implementieren:

final __ByValue class Coordinate {
private final int _x;
private final int _y;

public Coordinate(int x, int y) {
_x = x;
_y = y;
}

public boolean equals(Coordinate other) {
return _x == other._x
&& _y == other._y;
}
}

Dabei fällt sofort der Modifier __ByValue ins Auge. Hier soll ein neues Schlüsselwort eingeführt werden, dass eine Klasse als Werttyp kennzeichnet. Außerdem gelten die gleichen Randbedingungen wie für "value-based classes". Also nur finale Felder, die alle im Konstruktor zu initialisieren sind. Zustandsändernde Methoden sind verboten. Die Methode equals() vergleicht nur Felder und nicht Identitäten. Zusätzlich dürfen Werttypen keine Subtypen haben, sind also selbst auch final. Der Teil "...codes like a class" stimmt also.

Wie sieht es auf der Benutzungsseite aus? Gilt hier wirklich "Works like an int..."? Koordinaten kann man mit

Coordinate coordinate = __MakeValue(10, 8);

erzeugen. Hier gibt es wieder ein Strohmann-Schlüsselwort. Man kann sich fragen, ob das wirklich noch ein Erzeugen ist oder nicht mehr ein Belegen. Deshalb ist in Diskussion, wie __MakeValue umgesetzt werden wird. Analog zu Objekten ginge es mit new:

Coordinate coordinate = new Coordinate(10, 8);

Das ist aber irgendwie irreführend, weil ja nicht – wie new suggeriert – Speicher auf dem Heap belegt wird. Die zweite Möglichkeit ist deshalb, das Schlüsselwort einfach wegzulassen:

Coordinate coordinate = Coordinate(10, 8);

Und vielleicht sogar auch den Typ:

coordinate coordinate = (10, 8);

Das zugrunde liegende Problem ist, dass es keine Literale für selbstdefinierte Typen gibt.

Für den Speicher gilt mit Valhalla Folgendes: Die Exemplare von Werttypen liegen direkt im Speicher der Variable. So müssen sie nicht auf dem Heap sitzen, sondern können direkt im Stack existieren. Der Speicherbedarf für Referenzen entfällt. Außerdem ist kein Dereferenzierungsaufwand zu betreiben.

Die Operatoren "==" und "!=" sollen so implementiert werden, dass sie die Methode equals() aufrufen. Damit lassen sie sich für die Prüfung auf Gleichheit verwenden.

Eine weitere offene Frage ist noch, welchen Default-Wert Variablen von Werttypen haben. Bei Referenztypen ist der Standardwert "null", bei den eingebauten Typen "false" oder die passende Variante von 0. Die derzeit favorisierte Möglichkeit ist es, die Felder einer Wertvariablen wieder mit ihrem jeweiligen Default-Wert zu belegen. Der der Beispielklasse wäre dann Coordinate(0, 0).

Eine weitere spannende Frage ist, wie Value Types mit Generics zusammenspielen. Es besteht die Hoffnung, dass neben so etwas wie List<Coordinate> endlich List<int> möglich wird. Weil das größere Änderungen nach sich zieht, wurde hierfür der eigene JEP 218 (JDK Enhancement Proposal) aufgesetzt.

Offen ist auch noch, wie das Projekt mit Boxing umgeht. Für jeden eingebauten Typen gibt es einen Referenztypen, der beim Autoboxing zum Einsatz kommt (z. B. Float als Referenztyp für float). Beide kann man gut durch die Groß- und Kleinschreibung unterscheiden. Coordinate ist aber ja schon als Werttyp großgeschrieben. Wie der passende Referenztyp heißen wird, ist noch ungeklärt.

Als Abschluss bietet es sich an, zu schauen, wie andere Programmiersprachen das Problem lösen. In C++ können Entwickler mit Zeigern auf den Heap zugreifen. Dadurch können sie sich bei einer Variable aussuchen, ob sie auf dem Stack oder auf dem Heap liegen soll und das unabhängig davon, ob es sich um einen Wert oder ein Objekt handelt. Die Unveränderlichkeitseigenschaft können sie durch const-Felder und nicht zustandsverändernde Methoden erreichen. (Das ist dann die C++-Variante der "value-based class".)

C# hat einen Mechanismus, der ebenfalls "value types" heißt. Mit dem Schlüsselwort struct lassen sich Werttypen definieren. Sie haben allerdings lediglich die Eigenschaft, dass sie keine Referenztypen sind, also auf dem Stack liegen können. Unveränderlichkeit wird für die Value Types bei C# nicht gefordert. Will man sie erreichen, bietet sich wieder das Muster "value-based class" an.

Mit Projekt Valhalla wird ein von vielen ersehnter Mechanismus in Java eingeführt. Die Umsetzung hat Einfluss auf die Performance. Noch spannender ist allerdings, dass es auch die "immutable"-Eigenschaft von Werten erzwingt. Ein Feature, dass zum Beispiel C# (das ja mit seinen Structs "value types" hat) nicht unterstützt. Wer an noch mehr technischen Details interessiert ist, dem empfiehlt sich die Lektüre des Dokuments "State of the Values" der Programmiersprachen-Gurus John Rose, Brian Goetz und Guy Steele.

Der Wermutstropfen bleibt: Wann das Feature kommt, steht in den Sternen. Die offizielle Aussage vom Java-Team dazu ist: "Irgendwann nach Version 9."

Henning Schwentner
liebt Programmieren. Diese Leidenschaft lebt er als Softwarearchitekt und Berater bei der WPS – Workplace Solutions in Hamburg aus. Seine Projekte sind agil und in Programmiersprachen wie Java und C#, aber auch ABAP. geschrieben. Ihn interessieren die Evolution von Programmiersprachen, langlebige Softwarearchitekturen und große Refactorings.
(ane)