Warum erzeugt '(int) (char) (byte) -2' in Java 65534?

70

Ich bin auf diese Frage im technischen Test für einen Job gestoßen. Gegeben das folgende Codebeispiel:

public class Manager {
    public static void main (String args[]) {
        System.out.println((int) (char) (byte) -2);
    }
}

Es gibt die Ausgabe als 65534.

Dieses Verhalten wird nur für negative Werte angezeigt. 0 und positive Zahlen ergeben den gleichen Wert, dh den in SOP eingegebenen Wert. Das hier gegossene Byte ist unbedeutend; Ich habe es ohne versucht.

Meine Frage ist also: Was genau ist hier los?

MangoCar
quelle
Die Tatsache, dass die byteBesetzung das Ergebnis nicht ändert, bedeutet nicht, dass sie nichts tut ...
Narmer
char cast macht hier alles, ich habe keine Ahnung, was Byte Cast vorhat ... kannst du mir sagen, was es hier macht?
MangoCar
3
Versuchen Sie zu System.out.println((int)(char)(byte)-130)sehen, ob es "nur" 65536-130 ist. Dann lies @Chris K Antwort und arbeite es aus! :)
Narmer
Oh, und wiederholen Sie es ohne die byteBesetzung!
Narmer
@Narmer Hier (byte)ändert sich tatsächlich das Ergebnis, so dass es eine andere Situation ist.
glglgl

Antworten:

130

Es gibt einige Voraussetzungen, auf die wir uns einigen müssen, bevor Sie verstehen können, was hier passiert. Mit dem Verständnis der folgenden Aufzählungspunkte ist der Rest ein einfacher Abzug:

  1. Alle primitiven Typen innerhalb der JVM werden als eine Folge von Bits dargestellt. Der intTyp wird durch 32 Bit dargestellt, die charund shortTypen durch 16 Bit und der byteTyp wird durch 8 Bit dargestellt.

  2. Alle JVM-Nummern sind signiert, wobei der charTyp die einzige vorzeichenlose "Nummer" ist. Wenn eine Nummer signiert ist, wird das höchste Bit verwendet, um das Vorzeichen dieser Nummer darzustellen. Stellt für dieses höchste Bit 0eine nicht negative Zahl (positiv oder null) und 1eine negative Zahl dar. Außerdem wird bei vorzeichenbehafteten Zahlen ein negativer Wert (technisch als Zweierkomplementnotation bezeichnet ) in die Inkrementierungsreihenfolge positiver Zahlen invertiert . Beispielsweise wird ein positiver Wert in Bits wie folgt dargestellt:byte

    00 00 00 00 => (byte) 0
    00 00 00 01 => (byte) 1
    00 00 00 10 => (byte) 2
    ...
    01 11 11 11 => (byte) Byte.MAX_VALUE
    

    während die Bitreihenfolge für negative Zahlen invertiert ist:

    11 11 11 11 => (byte) -1
    11 11 11 10 => (byte) -2
    11 11 11 01 => (byte) -3
    ...
    10 00 00 00 => (byte) Byte.MIN_VALUE
    

    Diese invertierte Notation erklärt auch, warum der negative Bereich eine zusätzliche Zahl im Vergleich zu dem positiven Bereich enthalten kann, in dem letzterer die Darstellung der Zahl enthält 0. Denken Sie daran, all dies ist nur eine Frage der Interpretation eines Bitmusters. Sie können negative Zahlen unterschiedlich notieren, aber diese invertierte Notation für negative Zahlen ist sehr praktisch, da sie einige ziemlich schnelle Transformationen ermöglicht, wie wir später in einem kleinen Beispiel sehen werden.

    Wie bereits erwähnt, gilt dies nicht für den charTyp. Der charTyp repräsentiert ein Unicode-Zeichen mit einem nicht negativen "numerischen Bereich" von 0bis 65535. Jede dieser Zahlen bezieht sich auf einen 16-Bit-Unicode- Wert.

  3. Wenn zwischen den Umwandlung int, byte, short, charund booleanTypen muss die JVM , um entweder mehr oder truncate Bits.

    Wenn der Zieltyp durch mehr Bits als der Typ dargestellt wird, von dem er konvertiert wird, füllt die JVM die zusätzlichen Slots einfach mit dem Wert des höchsten Bits des angegebenen Werts (der die Signatur darstellt):

    |     short   |     byte    |
    |             | 00 00 00 01 | => (byte) 1
    | 00 00 00 00 | 00 00 00 01 | => (short) 1
    

    Dank der invertierten Notation funktioniert diese Strategie auch für negative Zahlen:

    |     short   |     byte    |
    |             | 11 11 11 11 | => (byte) -1
    | 11 11 11 11 | 11 11 11 11 | => (short) -1
    

    Auf diese Weise bleibt das Vorzeichen des Wertes erhalten. Beachten Sie, dass dieses Modell die Ausführung eines Gusses durch einen billigen Schichtvorgang ermöglicht, was offensichtlich vorteilhaft ist, ohne auf Einzelheiten der Implementierung für eine JVM einzugehen.

    Eine Ausnahme von dieser Regel ist die Erweiterung eines charTyps, der, wie bereits erwähnt, nicht signiert ist. Eine Konvertierung von a charwird immer angewendet, indem die zusätzlichen Bits mit gefüllt werden, 0da wir gesagt haben, dass es kein Vorzeichen gibt und daher keine invertierte Notation erforderlich ist. Eine Umwandlung von a charin an intwird daher durchgeführt als:

    |            int            |    char     |     byte    |
    |                           | 11 11 11 11 | 11 11 11 11 | => (char) \uFFFF
    | 00 00 00 00 | 00 00 00 00 | 11 11 11 11 | 11 11 11 11 | => (int) 65535
    

    Wenn der ursprüngliche Typ mehr Bits als der Zieltyp hat, werden die zusätzlichen Bits lediglich abgeschnitten. Solange der ursprüngliche Wert in den Zielwert gepasst hätte, funktioniert dies einwandfrei, beispielsweise für die folgende Konvertierung von a shortin a byte:

    |     short   |     byte    |
    | 00 00 00 00 | 00 00 00 01 | => (short) 1
    |             | 00 00 00 01 | => (byte) 1
    | 11 11 11 11 | 11 11 11 11 | => (short) -1
    |             | 11 11 11 11 | => (byte) -1
    

    Wenn der Wert jedoch zu groß oder zu klein ist , funktioniert dies nicht mehr:

    |     short   |     byte    |
    | 00 00 00 01 | 00 00 00 01 | => (short) 257
    |             | 00 00 00 01 | => (byte) 1
    | 11 11 11 11 | 00 00 00 00 | => (short) -32512
    |             | 00 00 00 00 | => (byte) 0
    

    Aus diesem Grund führen verengte Gussteile manchmal zu seltsamen Ergebnissen. Sie fragen sich vielleicht, warum die Verengung auf diese Weise implementiert wird. Sie könnten argumentieren, dass es intuitiver wäre, wenn die JVM den Bereich einer Zahl überprüfen und lieber eine inkompatible Zahl auf den größten darstellbaren Wert desselben Zeichens setzen würde. Dies würde jedoch eine Verzweigung erfordern, was eine kostspielige Operation ist. Dies ist besonders wichtig, da die Komplementnotation dieser beiden billige arithmetische Operationen ermöglicht.

Mit all diesen Informationen können wir sehen, was mit der Nummer -2in Ihrem Beispiel passiert :

|           int           |    char     |     byte    |
| 11 11 11 11 11 11 11 11 | 11 11 11 11 | 11 11 11 10 | => (int) -2
|                         |             | 11 11 11 10 | => (byte) -2
|                         | 11 11 11 11 | 11 11 11 10 | => (char) \uFFFE
| 00 00 00 00 00 00 00 00 | 11 11 11 11 | 11 11 11 10 | => (int) 65534

Wie Sie sehen können, ist die byteUmwandlung redundant, da die Umwandlung in chardie gleichen Bits schneiden würde.

All dies wird auch von der JVMS festgelegt , wenn Sie eine formellere Definition all dieser Regeln bevorzugen.

Eine letzte Bemerkung: Die Bitgröße eines Typs repräsentiert nicht unbedingt die Anzahl der Bits, die von der JVM für die Darstellung dieses Typs in ihrem Speicher reserviert werden. Als Tatsächlich unterscheiden die JVM nicht zwischen boolean, byte, short, charund intTypen. Alle von ihnen werden durch denselben JVM-Typ dargestellt, bei dem die virtuelle Maschine lediglich diese Castings emuliert. Auf dem Operandenstapel einer Methode (dh einer Variablen innerhalb einer Methode) verbrauchen alle Werte der genannten Typen 32 Bit. Dies gilt jedoch nicht für Arrays und Objektfelder, die jeder JVM-Implementierer nach Belieben verarbeiten kann.

Rafael Winterhalter
quelle
4
Sie können einen Link zum Zweierkomplement verwenden (auch auf SO ). Der größte Vorteil ist IMO, dass Sie durch Addition ( a - b = a + (-b)) eine Subtraktion durchführen können . Das Hinzufügen funktioniert genauso wie bei vorzeichenlosen Ganzzahlen.
Palec
1
Solltest du nicht geschrieben haben (char) 65534oder (char) 0xFFFEstatt (char) 0x65534in der letzten Tabelle?
FrankPl
@FrankPI: Ich wollte Unicode-Notation schreiben, danke für den Hinweis. Ich habe auch den Link hinzugefügt. Im Allgemeinen bearbeiten Sie einfach meinen Beitrag, wenn Sie sich eine Verbesserung vorstellen können.
Rafael Winterhalter
1
Diese Zeile kann einen Fehler haben:00 00 00 00 | => (byte) -1
Ben Voigt
Eine großartige Zusammenfassung der Funktionsweise von Casting. Die Leute vergessen in diesen Tagen des billigen Gedächtnisses, was die Größen der Typen wirklich bedeuten.
Michael Shopsin
35

Hier sind zwei wichtige Dinge zu beachten:

  1. Ein Zeichen ist nicht signiert und kann nicht negativ sein
  2. Das Umwandeln eines Bytes in ein Zeichen beinhaltet zunächst eine versteckte Umwandlung in ein Int gemäß der Java-Sprachspezifikation .

Wenn Sie also -2 in ein int umwandeln, erhalten Sie 111111111111111111111111111110. Beachten Sie, wie der Komplementwert der beiden mit einer Eins vorzeichenerweitert wurde. das passiert nur bei negativen werten. Wenn wir es dann auf ein Zeichen eingrenzen, wird das int auf abgeschnitten

1111111111111110

Schließlich wird das Umwandeln von 1111111111111110 in ein Int mit Null und nicht mit Eins etwas erweitert, da der Wert jetzt als positiv betrachtet wird (da Zeichen nur positiv sein können). Durch Verbreitern der Bits bleibt der Wert unverändert, im Gegensatz zum Fall mit negativem Wert jedoch unverändert. Und dieser Binärwert, wenn er dezimal gedruckt wird, ist 65534.

Chris K.
quelle
Warum erzeugt das Casting eines 8-Bit byteauf ein 16-Bit charein 16-Bit-Zwei-Komplement von -2, das in einem 65534 aufgelöst wird int? Bezieht sich das alles auf zwei Komplemente? Ich meine, die Füllung von 1 in der charBesetzung wie geht das?
Narmer
2
Danke @Narmer, ein ausgezeichneter Punkt. Ich habe die Antwort mit einem Verweis auf die Java-Sprachspezifikation aktualisiert, die erklärt, wie das Umwandeln von Byte in Zeichen erfolgt. Es geht über ein int.
Chris K
Ja, Ihre Antwort ist die informativste und erklärendste. Sie sollte die Antwort auf diese Frage sein.
Narmer
Die Vorzeichenerweiterung erfolgt in diesem Fall für alle Nummern. Es kommt einfach so vor, dass bei einer positiven Zahl das Vorzeichenbit 0 ist. Es gibt keine spezielle Regel für negative Zahlen.
Indiv
@indiv, ich habe die Antwort getwittert, um die Bit-Erweiterung von Null und Eins klarer zu machen.
Chris K
30

A charhat einen Wert zwischen 0 und 65535. Wenn Sie also ein Negativ in char umwandeln, entspricht das Ergebnis dem Subtrahieren dieser Zahl von 65536, was zu 65534 führt. Wenn Sie es als a drucken char, wird versucht, das Unicode-Zeichen anzuzeigen dargestellt durch 65534, aber wenn Sie auf werfen int, erhalten Sie tatsächlich 65534. Wenn Sie mit einer Zahl beginnen, die über 65536 liegt, sehen Sie ähnlich "verwirrende" Ergebnisse, bei denen eine große Zahl (z. B. 65538) klein wird ( 2).

Jacob Mattison
quelle
Ist die Reichweite eines Zeichens nicht 0-65535?
JamesB
Du hast recht - das hat sich geändert. Die Subtraktion erfolgt aus dem Gesamtbereich von 65536, was jedoch bedeutet, dass das High-End 65535 beträgt.
Jacob Mattison
6

Ich denke, der einfachste Weg, dies zu erklären, wäre, es in die Reihenfolge der Operationen aufzuteilen, die Sie ausführen

Instance | #          int            |     char    | #   byte    |    result   |
Source   | 11 11 11 11 | 11 11 11 11 | 11 11 11 11 | 11 11 11 10 | -2          |
byte     |(11 11 11 11)|(11 11 11 11)|(11 11 11 11)| 11 11 11 10 | -2          |
int      | 11 11 11 11 | 11 11 11 11 | 11 11 11 11 | 11 11 11 10 | -2          |
char     |(00 00 00 00)|(00 00 00 00)| 11 11 11 11 | 11 11 11 10 | 65534       |
int      | 00 00 00 00 | 00 00 00 00 | 11 11 11 11 | 11 11 11 10 | 65534       |
  1. Sie nehmen einfach einen vorzeichenbehafteten 32-Bit-Wert.
  2. Sie konvertieren es dann in einen 8-Bit-Wert mit Vorzeichen.
  3. Wenn Sie versuchen, es in einen vorzeichenlosen 16-Bit-Wert zu konvertieren, schleicht sich der Compiler schnell in einen vorzeichenbehafteten 32-Bit-Wert um.
  4. Konvertieren Sie es dann in 16 Bit, ohne das Vorzeichen beizubehalten.
  5. Wenn die endgültige Konvertierung in 32-Bit erfolgt, gibt es kein Vorzeichen, sodass der Wert null Bits addiert, um den Wert beizubehalten.

Also, ja, wenn Sie es so betrachten, ist die Byte-Besetzung signifikant (akademisch gesehen), obwohl das Ergebnis unbedeutend ist (Freude an der Programmierung, eine signifikante Aktion kann einen unbedeutenden Effekt haben). Der Effekt der Verengung und Verbreiterung unter Beibehaltung des Zeichens. Wobei sich die Umwandlung in char verengt, sich aber nicht erweitert, um zu unterschreiben.

(Bitte beachten Sie, dass ich ein # verwendet habe, um das vorzeichenbehaftete Bit zu kennzeichnen. Wie bereits erwähnt, gibt es kein vorzeichenbehaftetes Bit für char, da es sich um einen vorzeichenlosen Wert handelt.)

Ich habe Parens verwendet, um darzustellen, was tatsächlich intern passiert. Die Datentypen sind tatsächlich in ihren logischen Blöcken zusammengefasst, aber wenn sie wie in int betrachtet werden, sind ihre Ergebnisse das, was die Parens symbolisieren.

Vorzeichenbehaftete Werte erweitern sich immer mit dem Wert des vorzeichenbehafteten Bits. Unsigned erweitert sich immer mit dem Bit aus.

* Der Trick (oder die Fallstricke) besteht darin, dass die Erweiterung von Byte auf int den vorzeichenbehafteten Wert beibehält, wenn sie erweitert wird. Was sich dann verengt, sobald es den Saibling berührt. Dies schaltet dann das vorzeichenbehaftete Bit aus.

Wenn die Konvertierung in int nicht stattgefunden hätte, wäre der Wert 254 gewesen. Aber das tut es, also ist es nicht.

Roy Folkker
quelle