Java 8 Unsigned Arithmetic Support

1. Übersicht

Seit Beginn von Java sind alle numerischen Datentypen signiert. In vielen Situationen müssen jedoch vorzeichenlose Werte verwendet werden. Wenn wir beispielsweise die Anzahl der Vorkommen eines Ereignisses zählen, möchten wir keinen negativen Wert feststellen.

Die Unterstützung für vorzeichenlose Arithmetik war ab Version 8 endlich Teil des JDK. Diese Unterstützung erfolgte in Form der unsigned Integer API, die hauptsächlich statische Methoden in den Klassen Integer und Long enthielt .

In diesem Tutorial gehen wir diese API durch und geben Anweisungen zur korrekten Verwendung von vorzeichenlosen Zahlen.

2. Darstellungen auf Bitebene

Um zu verstehen, wie mit vorzeichenbehafteten und vorzeichenlosen Zahlen umgegangen wird, werfen wir zunächst einen Blick auf deren Darstellung auf Bitebene.

In Java werden Zahlen mit dem Komplementsystem der beiden codiert. Diese Codierung implementiert viele grundlegende arithmetische Operationen, einschließlich Addition, Subtraktion und Multiplikation, auf dieselbe Weise, unabhängig davon, ob die Operanden vorzeichenbehaftet oder vorzeichenlos sind.

Mit einem Codebeispiel sollten die Dinge klarer werden. Der Einfachheit halber verwenden wir Variablen vom byteprimitiven Datentyp. Operationen sind für andere integrale numerische Typen ähnlich, wie z. B. kurz , int oder lang .

Angenommen, wir haben ein Typbyte mit dem Wert 100 . Diese Nummer hat die binäre Darstellung 0110_0100 .

Verdoppeln wir diesen Wert:

byte b1 = 100; byte b2 = (byte) (b1 << 1);

Der Linksverschiebungsoperator im angegebenen Code verschiebt alle Bits in der Variablen b1 um eine Position nach links, wodurch sein Wert technisch doppelt so groß wird. Die binäre Darstellung der Variablen b2 ist dann 1100_1000 .

In einem System ohne Vorzeichen repräsentiert dieser Wert eine Dezimalzahl, die 2 ^ 7 + 2 ^ 6 + 2 ^ 3 oder 200 entspricht . Dennoch in einem signierten System, das Bit arbeitet als Vorzeichenbit am meisten links. Daher ist das Ergebnis -2 ^ 7 + 2 ^ 6 + 2 ^ 3 oder -56 .

Ein schneller Test kann das Ergebnis überprüfen:

assertEquals(-56, b2);

Wir können sehen, dass die Berechnungen von vorzeichenbehafteten und vorzeichenlosen Zahlen gleich sind. Unterschiede treten nur auf, wenn die JVM eine Binärdarstellung als Dezimalzahl interpretiert.

Die Additions-, Subtraktions- und Multiplikationsoperationen können mit vorzeichenlosen Zahlen arbeiten, ohne dass Änderungen im JDK erforderlich sind. Andere Operationen wie Vergleich oder Division behandeln vorzeichenbehaftete und vorzeichenlose Zahlen unterschiedlich.

Hier kommt die API ohne Vorzeichen ins Spiel.

3. Die Integer-API ohne Vorzeichen

Die API für vorzeichenlose Ganzzahlen bietet Unterstützung für vorzeichenlose Ganzzahlarithmetik in Java 8. Die meisten Mitglieder dieser API sind statische Methoden in den Klassen Integer und Long .

Die Methoden in diesen Klassen funktionieren ähnlich. Wir werden uns daher nur auf die Integer- Klasse konzentrieren und die Long- Klasse der Kürze halber weglassen.

3.1. Vergleich

Die Integer- Klasse definiert eine Methode namens compareUnsigned , um vorzeichenlose Zahlen zu vergleichen. Diese Methode berücksichtigt alle Binärwerte ohne Vorzeichen und ignoriert den Begriff des Vorzeichenbits.

Beginnen wir mit zwei Zahlen an den Grenzen des Datentyps int :

int positive = Integer.MAX_VALUE; int negative = Integer.MIN_VALUE;

Wenn wir diese Zahlen als vorzeichenbehaftete Werte vergleichen, ist positiv offensichtlich größer als negativ :

int signedComparison = Integer.compare(positive, negative); assertEquals(1, signedComparison);

Beim Vergleichen von Zahlen als vorzeichenlose Werte wird das Bit ganz links anstelle des Vorzeichenbits als das höchstwertige Bit betrachtet. Das Ergebnis ist also anders, wobei positiv kleiner als negativ ist :

int unsignedComparison = Integer.compareUnsigned(positive, negative); assertEquals(-1, unsignedComparison);

Es sollte klarer sein, wenn wir uns die binäre Darstellung dieser Zahlen ansehen:

  • MAX_VALUE -> 0111_1111_… _1111
  • MIN_VALUE -> 1000_0000_… _0000

Wenn das Bit ganz links ein reguläres Wertbit ist , ist MIN_VALUE eine Einheit größer als MAX_VALUE im Binärsystem. Dieser Test bestätigt Folgendes:

assertEquals(negative, positive + 1);

3.2. Division und Modulo

Genau wie bei der Vergleichsoperation verarbeiten die vorzeichenlosen Divisions- und Modulooperationen alle Bits als Wertbits. Die Quotienten und Reste unterscheiden sich daher, wenn wir diese Operationen an vorzeichenbehafteten und vorzeichenlosen Zahlen ausführen:

int positive = Integer.MAX_VALUE; int negative = Integer.MIN_VALUE; assertEquals(-1, negative / positive); assertEquals(1, Integer.divideUnsigned(negative, positive)); assertEquals(-1, negative % positive); assertEquals(1, Integer.remainderUnsigned(negative, positive));

3.3. Parsing

Wenn ein Parsen String unter Verwendung der parseUnsignedInt Methode, kann der Text Argument eine Zahl größer als repräsentieren MAX_VALUE .

Ein solcher großer Wert kann nicht mit der parseInt- Methode analysiert werden, die nur die Textdarstellung von Zahlen von MIN_VALUE bis MAX_VALUE verarbeiten kann .

Der folgende Testfall überprüft die Analyseergebnisse:

Throwable thrown = catchThrowable(() -> Integer.parseInt("2147483648")); assertThat(thrown).isInstanceOf(NumberFormatException.class); assertEquals(Integer.MAX_VALUE + 1, Integer.parseUnsignedInt("2147483648"));

Notice that the parseUnsignedInt method can parse a string indicating a number larger than MAX_VALUE, but will fail to parse any negative representation.

3.4. Formatting

Similar to parsing, when formatting a number, an unsigned operation regards all bits as value bits. Consequently, we can produce the textual representation of a number about twice as large as MAX_VALUE.

The following test case confirms the formatting result of MIN_VALUE in both cases — signed and unsigned:

String signedString = Integer.toString(Integer.MIN_VALUE); assertEquals("-2147483648", signedString); String unsignedString = Integer.toUnsignedString(Integer.MIN_VALUE); assertEquals("2147483648", unsignedString);

4. Pros and Cons

Many developers, especially those coming from a language that supports unsigned data types, such as C, welcome the introduction of unsigned arithmetic operations. However, this isn't necessarily a good thing.

There are two main reasons for the demand for unsigned numbers.

First, there are cases for which a negative value can never occur, and using an unsigned type can prevent such a value in the first place. Second, with an unsigned type, we can double the range of usable positive values compared to its signed counterpart.

Let's analyze the rationale behind the appeal for unsigned numbers.

When a variable should always be non-negative, a value less than 0 may be handy in indicating an exceptional situation.

For instance, the String.indexOf method returns the position of the first occurrence of a certain character in a string. The index -1 can easily denote the absence of such a character.

The other reason for unsigned numbers is the expansion of the value space. However, if the range of a signed type isn't enough, it's unlikely that a doubled range would suffice.

In case a data type isn't large enough, we need to use another data type that supports much larger values, such as using long instead of int, or BigInteger rather than long.

Another problem with the Unsigned Integer API is that the binary form of a number is the same regardless of whether it's signed or unsigned. It's therefore easy to mix signed and unsigned values, which may lead to unexpected results.

5. Conclusion

Die Unterstützung für vorzeichenlose Arithmetik in Java ist auf Wunsch vieler Menschen gekommen. Die damit verbundenen Vorteile sind jedoch unklar. Wir sollten bei der Verwendung dieser neuen Funktion Vorsicht walten lassen, um unerwartete Ergebnisse zu vermeiden.

Wie immer ist der Quellcode für diesen Artikel auf GitHub verfügbar.