Rust-lernen.de

Java-Datentypen in Rust

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-Datentypen in Rust

Primitive Datentypen in Java und deren Entsprechung in Rust

Java kennt acht sogenannte primitive Datentypen. Diese haben einen endlichen Wertebereich aus skalaren Werten, d.h. sie setzen sich nicht aus mehreren Werten zusammen.

JavaRustLängeWertebereich
bytei88 Bit-128 bis 127
shorti1616 Bit-32.768 bis 32.767
inti3232 Bit-2.147.483.648 bis 2.147.483.647
longi6464 Bitca. -9*1018 bis 9*1018
floatf3232 Bitca. -3*1038 bis 3*1038, -∞, ∞, NaN
doublef6464 Bitca. -10308 bis 10308, -∞, ∞, NaN
booleanbool8 Bittrue, false
charcharJava: 16 Bit
Rust: 32 Bit

Numerische Literale

Die numerischen Literale in Java und in Rust unterscheiden sich nur geringfügig:

JavaRustBedeutung
123123Dezimal, wird als int bzw. i32 interpretiert
123_456_789123_456_789Dezimal mit optischem Trenner
1234L1234i64Dezimaler long-Wert
12.3f12.3f32Gleitkommazahl mit einfacher Genauigkeit
12.312.3Gleitkommazahl, wird als double bzw. f64 interpretiert
1.23e21.23e2Gleitkommazahl in Exponentialdarstellung
0x1a0x1aHexadezimal, wird als int bzw. i32 interpretiert
07570o757Oktal, wird als int bzw. i32 interpretiert
0b111100000b11110000Binä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.

Automatische Typkonvertierung

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

Wrapper-Klassen

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];

Array

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.

JavaRust
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ählung

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.

JavaRust
enum Orientation { SOUTH, WEST, NORTH, EAST }enum Orientation { SOUTH, WEST, NORTH, EAST }
Orientation orientation = Orientation.SOUTH;let orientation = Orientation::SOUTH;

Weitere Datentypen

JavaRustBedeutung
void()Einheitstyp, bei Funktionen ohne Rückgabewert verwendet
String&strZeichenkette

Variablen und Konstanten

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.)

JavaRustBedeutung
int a = 5;
var a = 5;
let mut a = 5;Veränderbare Variable
final int a = 5;
final var a = 5;
let a = 5;Konstante

Funktionen und Funktionstypen

Funktionen sehen in Java und in Rust fast gleich aus, abgesehen davon, dass diese in Java stets im Kontext einer Klasse definiert werden.

JavaRust
void printHello() {
    System.out.println("Hallo");
}
fn print_hello() {
    println!("Hallo");
}
int add(int a, int b) {
    return a + b;
}
fn add(a: i32, b: i32) -> i32 {
    a + b
}

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);

Objektorientierte Programmierung (OOP)

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());
}

Umgang mit null-Werten

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}");
}

Quellen