Suporte aritmético não assinado do Java 8
1. Visão geral
Desde o início do Java, todos os tipos de dados numéricos são assinados. Em muitas situações, no entanto, é necessário usar valores sem sinal. Por exemplo, se contarmos o número de ocorrências de um evento, não queremos encontrar um valor negativo.
O suporte para aritmética não assinada finalmente fez parte do JDK a partir da versão 8. This support came in the form of the Unsigned Integer API, primarily containing static methods in the Integer and Long classes.
Neste tutorial, examinaremos esta API e forneceremos instruções sobre como usar números sem sinal corretamente.
2. Representações de nível de bits
Para entender como lidar com números assinados e não assinados, vamos dar uma olhada em sua representação no nível de bits primeiro.
In Java, numbers are encoded using the two’s complement system. Esta codificação implementa muitas operações aritméticas básicas, incluindo adição, subtração e multiplicação, da mesma forma, se os operandos são assinados ou não.
As coisas devem ficar mais claras com um exemplo de código. Para fins de simplicidade, usaremos variáveis do tipo de dados primitivosbyte. As operações são semelhantes para outros tipos numéricos integrais, comoshort,int oulong.
Suponha que temos algum tipobyte com o valor de100. Este número tem a representação binária0110_0100.
Vamos dobrar esse valor:
byte b1 = 100;
byte b2 = (byte) (b1 << 1);
O operador de deslocamento para a esquerda no código fornecido move todos os bits na variávelb1 uma posição para a esquerda, tecnicamente tornando seu valor duas vezes maior. A representação binária da variávelb2 será então1100_1000.
Em um sistema de tipo sem sinal, este valor representa um número decimal equivalente a2^7 + 2^6 + 2^3 ou200. No entanto,in a signed system, the left-most bit works as the sign bit. Portanto, o resultado é -2^7 + 2^6 + 2^3, ou-56.
Um teste rápido pode verificar o resultado:
assertEquals(-56, b2);
Podemos ver que os cálculos dos números assinados e não assinados são os mesmos. Differences only appear when the JVM interprets a binary representation as a decimal number.
As operações de adição, subtração e multiplicação podem trabalhar com números não assinados sem exigir nenhuma alteração no JDK. Outras operações, como comparação ou divisão, tratam números assinados e não assinados de maneira diferente.
É aqui que a API de número inteiro não assinado entra em jogo.
3. A API Unsigned Integer
A API de número inteiro não assinado fornece suporte para aritmética de número inteiro não assinado no Java 8. A maioria dos membros desta API são métodos estáticos nas classesIntegereLong.
Os métodos nessas classes funcionam da mesma maneira. Portanto, vamos nos concentrar apenas na classeInteger, deixando de fora a classeLong por brevidade.
3.1. Comparação
A classeInteger define um método denominadocompareUnsigned para comparar números sem sinal. This method considers all binary values unsigned, ignoring the notion of the sign bit.
Vamos começar com dois números nos limites do tipo de dadosint:
int positive = Integer.MAX_VALUE;
int negative = Integer.MIN_VALUE;
Se compararmos esses números como valores com sinais,positive é obviamente maior do quenegative:
int signedComparison = Integer.compare(positive, negative);
assertEquals(1, signedComparison);
Ao comparar números como valores não assinados, o bit mais à esquerda é considerado o bit mais significativo em vez do bit de sinal. Assim, o resultado é diferente, compositive sendo menor quenegative:
int unsignedComparison = Integer.compareUnsigned(positive, negative);
assertEquals(-1, unsignedComparison);
Deveria ficar mais claro se dermos uma olhada na representação binária desses números:
-
MAX_VALUE →0111_1111…1111
-
MIN_VALUE →1000_0000…0000
Quando o bit mais à esquerda é um bit de valor regular,MIN_VALUE é uma unidade maior queMAX_VALUE no sistema binário. Este teste confirma que:
assertEquals(negative, positive + 1);
3.2. Divisão e Módulo
Assim como a operação de comparação,the unsigned division and modulo operations process all bits as value bits. Os quocientes e remanescentes são, portanto, diferentes quando realizamos essas operações em números com e sem sinal:
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. Análise
Ao analisar umString usando o métodoparseUnsignedInt,the text argument can represent a number greater than MAX_VALUE.
Um valor grande como esse não pode ser analisado com o métodoparseInt, que só pode lidar com a representação textual de números deMIN_VALUE aMAX_VALUE.
O seguinte caso de teste verifica os resultados da análise:
Throwable thrown = catchThrowable(() -> Integer.parseInt("2147483648"));
assertThat(thrown).isInstanceOf(NumberFormatException.class);
assertEquals(Integer.MAX_VALUE + 1, Integer.parseUnsignedInt("2147483648"));
Observe que o métodoparseUnsignedInt pode analisar uma string indicando um número maior do queMAX_VALUE, mas falhará ao analisar qualquer representação negativa.
3.4. Formatação
Semelhante à análise, ao formatar um número, uma operação não assinada considera todos os bits como bits de valor. Consequentemente,we can produce the textual representation of a number about twice as large as MAX_VALUE.
O seguinte caso de teste confirma o resultado da formatação deMIN_VALUE em ambos os casos - com e sem sinal:
String signedString = Integer.toString(Integer.MIN_VALUE);
assertEquals("-2147483648", signedString);
String unsignedString = Integer.toUnsignedString(Integer.MIN_VALUE);
assertEquals("2147483648", unsignedString);
4. Prós e contras
Muitos desenvolvedores, especialmente aqueles provenientes de um idioma que suporta tipos de dados não assinados, como C, acolhem a introdução de operações aritméticas não assinadas. No entanto,this isn’t necessarily a good thing.
Há duas razões principais para a demanda por números não assinados.
Primeiro, há casos em que um valor negativo nunca pode ocorrer e o uso de um tipo não assinado pode impedir esse valor em primeiro lugar. Em segundo lugar, com um tipo sem sinal, podemosdouble the range of usable positive values em comparação com sua contraparte assinada.
Vamos analisar a razão por trás do apelo por números não assinados.
Quando uma variável deve ser sempre não negativa, um valor menor que0 pode ser útil para indicar uma situação excepcional.
Por exemplo, o métodoString.indexOf retorna a posição da primeira ocorrência de um certo caractere em uma string. O índice -1 pode denotar facilmente a ausência desse caractere.
A outra razão para números não assinados é a expansão do espaço de valor. No entanto,if the range of a signed type isn’t enough, it’s unlikely that a doubled range would suffice.
No caso de um tipo de dados não ser grande o suficiente, precisamos usar outro tipo de dados que suporte valores muito maiores, como usarlong em vez deint, ouBigInteger em vez delong.
Outro problema com a API Unsigned Integer é que a forma binária de um número é a mesma, independentemente de ser assinado ou não. Portanto, éeasy to mix signed and unsigned values, which may lead to unexpected results.
5. Conclusão
O suporte à aritmética não assinada em Java veio a pedido de muitas pessoas. No entanto, os benefícios trazidos não são claros. Devemos ter cuidado ao usar esse novo recurso para evitar resultados inesperados.
Como sempre, o código-fonte deste artigo está disponívelover on GitHub.