DEV Community

Henrique Matheus Alves Pereira
Henrique Matheus Alves Pereira

Posted on • Edited on

Por que o Java converte números inteiros maiores que 127 para valores negativos em castings do tipo byte?

Imagem 01 — Exemplo de “modelagem” de um número inteiro em um byte.

Pegue um cafézinho, vamos conversar

Tropecei na conversão e continuei (~andando~) programando

Durante meu dia a dia no trabalho, é recorrente trabalhar com conjuntos de dados em bytes. Em um desses dias, armazenei um número inteiro "modelando-o" (casting) em uma variável que esperava receber um byte, semelhantemente ao exemplo da Imagem 01.

Quando fui verificar o valor contido nessa variável, percebi o valor negativo, convertido e armazenado nela.

A princípio, não entendi a razão disso.

Depois de pesquisar, descobri que, internamente, o Java implementa o algoritmo do Complemento de Dois para alguns tipos primitivos de dados, como o de bytes.

Resumidamente, a função dele é converter um número inteiro positivo para outro número inteiro negativo, mantendo fixo o comprimento máximo de sua representação binária.

Photo by Amanda Sandlin on Unsplash.

Acabou o cafézinho, vamos a obra

Definição do algoritmo original do Complemento de Dois

Antes de entender como o Java está implementando o algoritmo, vamos conhecer um pouquinho da definição do algoritmo original, no trecho abaixo.

"Two’s complement is a mathematical operation to reversibly convert a positive binary number into a negative binary number with equivalent negative value, using the binary digit with the greatest place value as the sign to indicate whether the binary number is positive or negative.
It is used in computer science as the most common method of representing signed (positive, negative, and zero) integers on computers, and more generally, fixed point binary values.
When the most significant bit is 1, the number is signed as negative; and when the most significant bit is 0 the number is signed as positive."

Fonte [1]


GIF 01 — Imagem animada contendo uma pessoa com a mente caótica após ler a explicação técnica.

Se você não entendeu a definição do algoritmo, não se preocupe. Vamos ver a estrutura lógica, detalhada abaixo.

Lógica do algoritmo original do Complemento de Dois

Observação: Vamos considerar o nome “decimalToParser” para o número inteiro.

  1. O decimalToParser deve ser inteiro (positivo ou negativo);

  2. Utilize a Equação 01:

    secondComplement=[(2n)decimalToParser]secondComplement = [ (2 ^ n) - | decimalToParser | ]
    Equação 01 — Fórmula para calcular o segundo complemento do algoritmo de Complemento de Dois.
  3. Obtenha o valor binário do resultado da Equação 01;

  4. Inverta o valor binário do resultado;

  5. Identifique o bit mais significativo (MSB) do valor binário invertido. O bit mais significativo está posicionado na extremidade esquerda;

  6. Verificar o valor do bit mais significativo:

    • Se for zero (“0”), o valor final do segundo complemento deve ser positivo;
    • Se for zero (“1”), o valor final do segundo complemento deve ser negativo;

Parâmetros da Equação 01:

  • secondComplement é o valor calculado do segundo complemento;
  • 2 é o número indicativo de um sistema binário;
  • n é o número indicativo do tamanho de bits (explicado abaixo);

Observação: O valor de decimalToParser está em módulo na equação 01.


Ainda não entendeu?
Não se preocupe, eu trouxe exemplos abaixo 😂 😂 😂 😂

GIF 02 — Imagem animada mostrando uma pessoa tranquilizando alguém em estado de desespero.

Algoritmo original: Exemplo da lógica de cálculo

Utilizando o valor (207 em decimal) que foi convertido (-49 em bytes) na Imagem 01, acompanhe a demonstração de cálculo do algoritmo em seis etapas, conforme mencionado acima, na Imagem 02 abaixo.

Imagem 02 — Demonstração de cálculo em seis etapas do algoritmo original do Complemento de Dois.

Algoritmo original: Exemplo de implementação de cálculo no Java

Utilizando a mesma estrutura da demonstração da Imagem 02, acompanhe abaixo a implementação em Java.

Código 01 — Implementação em Java da demonstração realizada na Imagem 02.

Se você quiser contribuir com o código acima no Gist, acesse:
TwosComplement.java


A implementação do algoritmo utilizada no Java

Definição do tipo de dados primitivo de byte

Primeiramente, é necessário entender a estrutura de dados primitivos, por isso considere a definição de byte na documentação do Java:

“byte: The byte data type is an 8-bit signed two's complement integer. It has a minimum value of -128 and a maximum value of 127 (inclusive).
The byte data type can be useful for saving memory in large arrays, where the memory savings actually matters.
They can also be used in place of int where their limits help to clarify your code; the fact that a variable's range is limited can serve as a form of documentation."

Fonte [2]

Para ficar mais fácil de perceber a diferença, podemos resumir a definição acima comparando os tamanhos de tipos de dados mencionados:

Imagem 03 — Exemplo do tamanho em bits dos tipos primitivos int e byte no Java.

Então, como diz no trecho citado da documentação, o objetivo da utilização do tipo de dados de byte é para economizar a memória interna disponível no hardware. Além disso, limitar a largura de bits dos dados implica em aumentar a performance do processamento ou de cálculos em baixo nível.


Algoritmo implementado no java: Exemplo em código

Código 02 — Implementação do algoritmo conforme padrão do Java.

Se você quiser contribuir com o código acima no Gist, acesse:
JavaTwosComplement.java


Exemplo executável

A vida do dev não se resume a teoria, não é mesmo? 😂 😂 😂 😂

Por isso, eu também trouxe um exemplo executável para simular a estrutura explicada acima, dos dois algoritmos.

Código 03 — Classe em Java com método de cálculo do complemento de dois implementado.

Se você quiser contribuir com o código acima, basta clicar em "Open on Replit".


Tá, mas qual é a vantagem de utilizar esse algoritmo?

1. Padronização para menor consumo de dados

Conforme foi explicado anteriormente, o Java define que o tipo primitivo byte possui o tamanho de oito bits, por padrão.

Ou seja, quando necessários, os cálculos a serem realizados com esses dados serão mais simples de serem realizados e exigirão bem pouco do hardware.


2. Soma e subtração sem verificar o sinal dos operandos

O algoritmo original do Complemento de Dois considera que todo número positivo será representado por um número negativo e vice-versa. Esse número negativo corresponde a outro decimal calculado pela Equação 01.


Considerando a afirmação acima do algoritmo original, a Imagem 04 abaixo mostra o exemplo do cálculo entre um número positivo e outro negativo.

Para realizar esse cálculo, não é necessário verificar o sinal de cada operando. Basta apenas encontrar o complemento de dois do número negativo e realizar a soma com o valor binário do número positivo, considerando as regras de soma binária.

O algoritmo ainda diz que a soma de um valor binário de um número decimal com o binário do seu complemento de dois deve resultar em zero. E essa verificação é demonstrada no item 1.2 da Imagem 04.

Imagem 04 — Exemplo de operação de soma / subtração utilizando o Complemento de Dois.

Fontes

01 - Artigo na Wikipedia ;

02 - Java makes use of the "two's complement" for representing signed numbers | by Jean Villete ;

03 - Two's complement solution very detailed explanation | by kapkan ;


Ficou alguma dúvida ou tem sugestões?

Comente aqui embaixo ou me chame em alguma das minhas redes.

Valeu! ✌🏻

Top comments (2)

Collapse
 
wldomiciano profile image
Wellington Domiciano

Gostei muito do detalhamento no seu artigo.

Contudo, há algumas afirmações que eu acho que podem induzir ao erro.

Vc diz:

... o Java implementa o algoritmo do Complemento de Dois para o tipo primitivo de dados de bytes.

e

O Java utiliza o algoritmo do Complemento de Dois para valores decimais maiores que 127. Logo, todo número maior que 127 e menor que 256 será representado por um número negativo.

Mas a verdade é que todos os valores integrais são representados usando o complemento de dois, o que inclui byte, short, int e long.

The integral types are byte, short, int, and long, whose values are 8-bit, 16-bit, 32-bit and 64-bit signed two's-complement integers, respectively, and char, whose values are 16-bit unsigned integers representing UTF-16 code units.

FONTE: docs.oracle.com/javase/specs/jls/s...

Com isso em mente, a explicação para o por que 207 é representado como -49 quando convertido para byte é bem mais simples.

Ao realizar o casting para byte, o Java simplesmente trunca a forma binária de 207 deixando apenas os 8 bytes menos significativos.

207 em binário é 11001111 ou, considerando toda a extensão de 32 bits, 00000000000000000000000011001111.

Quando vc faz o casting para byte, ele pega apenas os bytes 11001111 e, como o byte mais à esquerda é 1, sempre que ele faz uma operação que envolva números integrais, ele considera que todos os outros 24 bytes à esquerda são 1, 11111111111111111111111111001111.

Eu usei este código para fazer esta observação:

class Main {
  public static void main(String... args) {
    int a = 207;
    int b = (byte) a;

    System.out.printf("%32s\n", Integer.toBinaryString(a));
    System.out.printf("%32s\n", Integer.toBinaryString(b));
    System.out.printf("%32s\n", Integer.toBinaryString(-49));
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
henriqueotogami profile image
Henrique Matheus Alves Pereira

Muito obrigado pela sua contribuição!

Você tem razão.