69 votes

Pourquoi '(int)(char)(byte)-2' produit-il 65534 en Java ?

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

J'ai rencontré cette question lors d'un test technique pour un emploi. La sortie est de 65534. Ce comportement ne concerne que les valeurs négatives ; 0 et les nombres positifs donnent la même valeur, c'est-à-dire celle entrée dans le SOP. L'encodage d'octet est insignifiant ici ; j'ai essayé sans. Ma question est donc la suivante : que se passe-t-il exactement ici ?

130voto

raphw Points 6008

Il y a quelques conditions préalables sur lesquelles nous devons nous mettre d'accord avant que vous puissiez comprendre ce qui se passe ici. En comprenant les points suivants, le reste est une simple déduction :

  1. Tous les types primitifs de la JVM sont représentés par une séquence de bits. Le site int est représenté par 32 bits, le type char y short par 16 bits et les byte est représenté par 8 bits.

  2. Tous les nombres de la JVM sont signés, où le char est le seul "nombre" non signé. Lorsqu'un nombre est signé, le le plus haut est utilisé pour représenter le signe de ce nombre. Pour ce bit le plus élevé, 0 représente un nombre non négatif (positif ou nul) et 1 représente un nombre négatif. De même, avec les nombres signés, une valeur négative est _inversé_ à l'ordre d'incrémentation des nombres positifs. Par exemple, un nombre positif byte est représentée en bits comme suit :

    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

    tandis que l'ordre des bits pour les nombres négatifs est inversé :

    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

    Cette notation inversée explique également pourquoi la plage négative peut accueillir un nombre supplémentaire par rapport à la plage positive où cette dernière comprend la représentation du nombre 0 . Rappelez-vous, tout ceci n'est qu'une question de interpréter un petit motif. Vous pouvez noter les nombres négatifs différemment, mais cette notation inversée pour les nombres négatifs est assez pratique car elle permet des transformations assez rapides comme nous pourrons le voir dans un petit exemple plus tard.

    Comme mentionné, cela ne s'applique pas à la char type. Le site char représente un caractère Unicode avec une "gamme numérique" non négative de 0 a 65535 . Chacun de ces chiffres fait référence à un 16-bits Unicode valeur.

  3. Lors de la conversion entre le int , byte , short , char y boolean la JVM doit ajouter ou tronquer des bits.

    Si le type cible est représenté par plus de bits que le type à partir duquel il est converti, la JVM remplit simplement les emplacements supplémentaires avec la valeur du bit le plus élevé de la valeur donnée (qui représente la signature) :

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

    Grâce à la notation inversée, cette stratégie fonctionne également pour les nombres négatifs :

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

    De cette façon, le signe de la valeur est conservé. Sans entrer dans les détails de l'implémentation de cette méthode pour une JVM, notez que ce modèle permet à un casting d'être réalisé par une méthode bon marché opération de changement ce qui est manifestement avantageux.

    Une exception à cette règle est élargissement a char qui est, comme nous l'avons déjà dit, non signé. Une conversion de a char est toujours appliquée en remplissant les bits supplémentaires avec 0 car nous avons dit qu'il n'y a pas de signe et donc pas besoin d'une notation inversée. Une conversion d'un char à un int est donc exécuté comme :

    |            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

    Lorsque le type original comporte plus de bits que le type cible, les bits supplémentaires sont simplement coupés. Tant que la valeur d'origine aurait tenu dans la valeur cible, cela fonctionne bien, comme par exemple pour la conversion suivante d'un short à un 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

    Toutefois, si la valeur est trop grand o trop petit cela ne fonctionne plus :

    |     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

    C'est pourquoi le rétrécissement des moulages donne parfois des résultats étranges. Vous pouvez vous demander pourquoi le rétrécissement est implémenté de cette manière. Vous pourriez argumenter qu'il serait plus intuitif que la JVM vérifie la plage d'un nombre et préfère convertir un nombre incompatible en la plus grande valeur représentable du même signe. Cependant, cela nécessiterait ramification ce qui est une opération coûteuse. Ceci est particulièrement important, car cette notation en complément à deux permet des opérations arithmétiques peu coûteuses.

Avec toutes ces informations, nous pouvons voir ce qui se passe avec le nombre -2 dans votre exemple :

|           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

Comme vous pouvez le voir, le byte est redondant, car le lancer vers les char couperait les mêmes morceaux.

Tout cela est aussi spécifié par le JVMS si vous préférez une définition plus formelle de toutes ces règles.

Une dernière remarque : La taille des bits d'un type ne représente pas nécessairement le nombre de bits réservés par la JVM pour représenter ce type dans sa mémoire. En fait, la JVM ne fait pas de distinction entre les types suivants boolean , byte , short , char y int types. Ils sont tous représentés par le même type de JVM, la machine virtuelle se contentant d'émuler ces castings. Sur la pile d'opérandes d'une méthode (c'est-à-dire toute variable au sein d'une méthode), toutes les valeurs des types nommés consomment 32 bits. Ceci n'est toutefois pas vrai pour les tableaux et les champs d'objets que tout implémenteur JVM peut manipuler à sa guise.

35voto

Chris K Points 5756

Il y a deux choses importantes à noter ici,

  1. un char est non signé, et ne peut pas être négatif
  2. Le transfert d'un octet vers un char implique d'abord un transfert caché vers un int, conformément à la règle de l'art. Spécification du langage Java .

Ainsi, le moulage de -2 en un int nous donne 1111111111111111111111111111111110. Remarquez que le signe de la valeur de complément à deux a été étendu par un un ; cela n'arrive que pour les valeurs négatives. Lorsque nous le réduisons ensuite à un char, le int est tronqué en

1111111111111110

Enfin, le moulage de 11111111111110 en un int est élargi par un zéro, plutôt qu'un un, car la valeur est maintenant considérée comme positive (car les caractères ne peuvent être que positifs). Ainsi, l'élargissement des bits laisse la valeur inchangée, mais à la différence du cas de valeur négative inchangée en valeur. Et cette valeur binaire, lorsqu'elle est imprimée en décimal, est 65534.

30voto

Jacob Mattison Points 32137

A char a une valeur comprise entre 0 et 65535, donc lorsque vous convertissez un négatif en char, le résultat est le même que si vous soustrayez ce nombre de 65536, ce qui donne 65534. Si vous l'avez imprimé comme un char il essaierait d'afficher le caractère unicode représenté par 65534, mais lorsque vous convertissez en int vous obtenez en fait 65534. Si vous commencez par un nombre supérieur à 65536, vous obtiendrez des résultats tout aussi "déroutants", dans lesquels un grand nombre (par exemple 65538) se retrouvera petit (2).

6voto

Roy Folkker Points 129

Je pense que le moyen le plus simple d'expliquer cela est de le décomposer dans l'ordre des opérations que vous effectuez.

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. Vous prenez simplement une valeur signée de 32 bits.
  2. Vous le convertissez ensuite en une valeur signée de 8 bits.
  3. Lorsque vous tentez de le convertir en une valeur non signée de 16 bits, le compilateur effectue une conversion rapide en une valeur signée de 32 bits,
  4. Ensuite, on le convertit en 16 bits sans maintenir le signe.
  5. Lors de la conversion finale en 32 bits, il n'y a pas de signe, donc la valeur ajoute zéro bit pour maintenir la valeur.

Donc, oui, quand vous regardez de cette façon, l'octet lancé est significatif (académiquement parlant), bien que le résultat soit insignifiant (joie de la programmation, une action significative peut avoir un effet insignifiant). L'effet de rétrécissement et d'élargissement tout en maintenant le signe. La conversion en char rétrécit, mais ne s'élargit pas au signe.

(Veuillez noter que j'ai utilisé un # pour indiquer le bit signé, et comme indiqué, il n'y a pas de bit signé pour char, car c'est une valeur non signée).

J'ai utilisé des parenthèses pour représenter ce qui se passe réellement en interne. Les types de données sont en fait regroupés dans leurs blocs logiques, mais s'ils étaient vus comme dans int, leurs résultats seraient ce que les parenthèses symbolisent.

Les valeurs signées s'élargissent toujours avec la valeur du bit signé. Les valeurs non signées s'élargissent toujours avec le bit désactivé.

*Ainsi, l'astuce (ou les problèmes) est que l'expansion en int à partir de byte, maintient la valeur signée lorsqu'elle est élargie. Qui est ensuite rétrécie au moment où elle touche le char. Cela désactive alors le bit signé.

Si la conversion en int n'avait pas eu lieu, la valeur aurait été 254. Mais elle a lieu, donc elle ne l'est pas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X