Letzte Änderung: 13.10.2024
Sowohl Java als auch Rust sind statisch typisierte Programmiersprachen, d.h. die Datentypen von Variablen und in Funktionssignaturen müssen schon zur Kompilierzeit eindeutig festgelegt sein. Unterschiede gibt es aber im Umfang der mitgelieferten Basis-Datentypen und der vom Compiler bereitgestellten Unterstützung.
Java kennt acht sogenannte primitive Datentypen. Diese haben einen endlichen Wertebereich aus skalaren Werten, d.h. sie setzen sich nicht aus mehreren Werten zusammen.
Java | Rust | Länge | Wertebereich |
---|---|---|---|
byte | i8 | 8 Bit | -128 bis 127 |
short | i16 | 16 Bit | -32.768 bis 32.767 |
int | i32 | 32 Bit | -2.147.483.648 bis 2.147.483.647 |
long | i64 | 64 Bit | ca. -9*1018 bis 9*1018 |
float | f32 | 32 Bit | ca. -3*1038 bis 3*1038, -∞, ∞, NaN |
double | f64 | 64 Bit | ca. -10308 bis 10308, -∞, ∞, NaN |
boolean | bool | 8 Bit | true , false |
char | char | Java: 16 Bit Rust: 32 Bit |
Die numerischen Literale in Java und in Rust unterscheiden sich nur geringfügig:
Java | Rust | Bedeutung |
---|---|---|
123 | 123 | Dezimal, wird als int bzw. i32 interpretiert |
123_456_789 | 123_456_789 | Dezimal mit optischem Trenner |
1234L | 1234i64 | Dezimaler long -Wert |
12.3f | 12.3f32 | Gleitkommazahl mit einfacher Genauigkeit |
12.3 | 12.3 | Gleitkommazahl, wird als double bzw. f64 interpretiert |
1.23e2 | 1.23e2 | Gleitkommazahl in Exponentialdarstellung |
0x1a | 0x1a | Hexadezimal, wird als int bzw. i32 interpretiert |
0757 | 0o757 | Oktal, wird als int bzw. i32 interpretiert |
0b11110000 | 0b11110000 | Binär, wird als int bzw. i32 interpretiert |
'C' | 'C' | Zeichen |
Zur besseren Lesbarkeit dürfen numerische Literale das visuelle Trennzeichen _
enthalten. Es hat keine weitere Bedeutung.
Java und Rust führen bei numerischen Literalen eine automatische Typkonvertierung durch, d.h. das Literal 12
lässt sich problemlos einer Variablen vom Typ byte
bzw. u8
zuweisen.
Bei der Zuweisung einer Variable an eine andere Variable konvertiert Java den Datentyp automatisch, wenn der Ziel-Datentyp einen größeren Wertebereich hat. Rust ist hier hingegen strikt und führt keine automatische Typkonvertierung durch, sondern verlangt stets eine explizite Typkonvertierung (casting):
Beispiel in Java:
int a = 123; long b = a; // automatische Typkonvertierung von int nach long
Dasselbe in Rust:
let a: i32 = 123; let b: i64 = a; // Typfehler! let b: i64 = a as i64; // Korrekt mit expliziter Typkonvertierung
In Java gibt es zu jedem primitiven Datentyp eine entsprechende Wrapper-Klasse, zu int
gehört beispielsweise Integer
. Die Wrapper-Klassen werden benötigt, um u.a. Zahlen in generisch typisierten Datenstrukturen wie List<T>
und Set<T>
verwenden zu können, also an Stellen, an denen nur Objekttypen verwendet werden können:
List<int> a; // Nicht erlaubt! List<Integer> a; // Erlaubt
Um den Umgang mit den Wrapper-Klassen möglichst einfach zu gestalten, enthält der Java-Compiler eine Unterstützung, die Werte primitiver Datentypen automatisch in eine Wrapper-Klasseninstanz umwandelt (sog. autoboxing) und umgekehrt (sog. unboxing):
int a = 123; Integer b = a; // autoboxing int -> Integer int c = b; // unboxing Integer -> int
Rust kennt diese Unterscheidung nicht, sämtliche Datentypen können direkt in Typparameter von generischen Datenstrukturen eingesetzt werden:
let a: Vec<i32> = vec![1, 2, 3];
Sowohl Java als auch Rust unterstützen Arrays als Datentypen. Im Unterschied zu Java haben Array-Typen in Rust stets eine explizit angegebene Länge. Zudem werden Arrays in Java immer im Haldenspeicher (engl. heap) abgelegt, während Rust diese auf dem Stapelspeicher (engl. stack) unterbringt.
Java | Rust |
---|---|
int[] a; | Keine Entsprechung in Rust, da keine feste Länge |
int[] a = { 1, 2, 3, 4, 5 }; | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
int[] a = new int[10]; | let a: [i32; 10] = [0; 10]; |
int thirdValue = a[2]; | let third_value: i32 = a[2]; |
Aufzählungen (engl. enumerations, kurz: enum) sehen in Java und Rust recht ähnlich aus. Rust bietet eine deutlich umfangreichere Syntax, worauf hier aber nicht weiter eingegangen wird.
Java | Rust |
---|---|
enum Orientation { SOUTH, WEST, NORTH, EAST } | enum Orientation { SOUTH, WEST, NORTH, EAST } |
Orientation orientation = Orientation.SOUTH; | let orientation = Orientation::SOUTH; |
Java | Rust | Bedeutung |
---|---|---|
void | () | Einheitstyp, bei Funktionen ohne Rückgabewert verwendet |
String | &str | Zeichenkette |
In Java sind alle Variablen standardmäßig veränderbar, d.h. einer Variable kann jederzeit ein anderer Wert zugewiesen werden. Um eine Konstante in Java zu deklarieren, muss man zusätzlich das Schlüsselwort final
angeben. In Rust ist es genau umgekehrt, wie folgende Beispiele zeigen. (In den Beispielen weiter oben wurde diese Feinheit zugunsten der besseren Lesbarkeit ignoriert.)
Java | Rust | Bedeutung |
---|---|---|
int a = 5; | let mut a = 5; | Veränderbare Variable |
final int a = 5; | let a = 5; | Konstante |
Funktionen sehen in Java und in Rust fast gleich aus, abgesehen davon, dass diese in Java stets im Kontext einer Klasse definiert werden.
Java | Rust |
---|---|
void printHello() { | fn print_hello() { |
int add(int a, int b) { | fn add(a: i32, b: i32) -> i32 { |
Java und Rust enthalten auch funktionale Sprachelemente: Neben den oben genannten Datentypen gibt es Typen, die eine Funktionssignatur beschreiben, also die Datentypen der Parameter einer Funktion sowie den Datentyp des Rückgabewerts. Mit Hilfe dieser Funktionstypen kann man eine Funktion auch als Wert übergeben (sog. Funktionszeiger).
In Java wird ein Funktionstyp als Interface mit einer einzigen Methode deklariert. Nachfolgend ein Beispiel für einen Funktionstyp, einer entsprechenden Implementierung mittels eines Lamdaausdrucks und dem Aufruf der Funktion:
public interface Addierer<T> { T add(T a, T b); } Addierer<Integer> intAddierer = (a, b) -> a + b int sum = intAddierer(1, 4);
In Rust sieht es wie folgt aus:
type Addierer<T> = fn (T, T) -> T; let i32addierer: Addierer<i32> = |a, b| a + b; let sum: i32 = i32addierer(1, 4);
In Java können Klassen voneinander erben (Schlüsselwort extends). Rust hat aber keine Entsprechung dafür. Konzentriert man sich auf Interfaces und Klassen ohne Vererbung, lässt sich das durchaus einigermaßen in Rust nachbilden.
Beispiel in Java:
interface HasArea { double area(); } class Square implements HasArea { final double width; Square(double width) { this.width = width; } double area() { return width * width; } } class MyClass { void printArea(HasArea hasArea) { System.out.println(hasArea.area()); } }
Die Umsetzung in Rust sieht wie folgt aus:
trait HasArea { // enspricht dem Interface HasArea fn area(&self) -> f64; } struct Square { // entspricht der Klasse Square width: f64, } impl Square { fn new(size: f64) -> Self { // Konstruktor der Klasse Square Square { width: size } } } impl HasArea for Square { // entspricht der Implementierung des Interfaces fn area(&self) -> f64 { self.width * self.width } } fn print_area(has_area: &impl HasArea) { // akzeptiert alle Strukturen die HasArea implementieren println!("{}", has_area.area()); }
In Java können sämtliche Objektreferenzen auch den Wert null
haben. Im Gegensatz dazu erlaubt Rust keine null
-Werte. Will man null
-Werte dennoch in Rust nachbilden, verwendet man meist den Typ Option<T>
.
Beispiel in Java, bei dem die Variable addr
den Wert null
haben kann:
Address addr = getAddress(); if (addr != null) { System.out.println(addr); }
Umsetzung in Rust:
let addr: Option<Address> = get_address_option(); if let Some(addr_value) = addr { println!("{addr_value}"); }