Trabalhando com números

Trabalhar com números em Java pode ser bem desafiador, pois ha bastantes truques para conhecer.

Tipos numéricos

Java define tipos para os números de 8, 16, 32 e 64 bits. Estes tipos não são considerados objetos e sim, tipo primitivos.

Tipos Primitivos

A diferença entre Java e algumas outras linguagens - especialmente as chamadas linguagens de script - é o suporte a tipos numéricos como tipos primitivos. Neste contexto, tipo primitivo significa que não é um objeto. O uso de tipos primitivos aumenta a eficiência da JVM e permite que ela funcione até em ambientes com pouca memória e poder de processamento (como nas plataformas JME e JavaCard ).

Por isto se diz que Java não é uma linguagem puramente orientada a objetos já que é possível programar sem eles utilizando apenas tipos primitivos.

Este conceito pode mudar no futuro, após os esforços do Projeto Vahalla.

Os tipos numéricos em Java são:

  • byte - ocupa 8 bits (1 byte): Representa apenas números inteiros. O menor número possível representar com um byte é -128 e o maior é 127. Repare que 8 bits podem produzir 256 números diferentes que são exatamente quantos números existem entre -128 e 127.

  • short - ocupa 16 bits (2 bytes). Representa apenas números inteiros. O menor número possível representar com um short é -32.768 e o maior é 32.767.

  • char - ocupa 16 bits (2 bytes) e representa apenas números inteiros, contudo ao contrário do short só aceita números positivos. O menor número possível representar é 0 e o maior é 65536. O char é um tipo numérico especial utilizado para representar caracteres unicode, contudo ele é considerado um tipo numérico em Java.

  • int - ocupa 32 bits (4 bytes). Representa apenas números inteiros. O menor número possível representar é -2.147.483.648 e o maior é 2.147.483.647.

  • long - ocupa 64 bits (8 bytes). Representa apenas números inteiros. O menor número possível representar é -9.223.372.036.854.775.808 e o maior 9.223.372.036.854.775.807.

  • float - ocupa 32 bits (4 bytes). Pode representar números inteiros ou não. O menor valor possível representar é 1.40239846x10-46 e o maior é 3.40282347x1038. Em Java o float é um número de ponto flutuante de 32 bits, que extritamente obdece a norma IEEE 754-1985.

  • double - ocupa 64 bits ( 8 bytes). Pode representar números inteiros ou não. O menor valor possível representar é 4.94065645841246544x10-324 e o maior é 1.7976931348623157×10308. Em Java o double é um número de ponto flutuante de 64 bits, que extritamente obdece a norma IEEE 754-1985.

Representações de inteiros

Java permite representar literais de números inteiros em 4 bases: decimal, hexadecimal, octal e binária:

  • Decimal - é a representação padrão e aquela a que estamos habituados. Simplesmente escrevemos 42.

  • Hexadecimal - Para especificar a representação em hexadecimal (base 16), começamos o número com um zero e um x e fica 0x42 ( que não é a mesma coisa que 42)

  • Octal - Para a representação octal (base 8), começamos o número com um zero apenas e fica 042 ( que é diferente de 42 e 0x42).

  • Binária - Para a representação binária (base 2), começamos o número com um zero um 0b e fica 0b011101.

A representação binária só aceita 0 e 1 e é uma representação possível apenas a partir do Java 7.

Warning
Cuidado! Em Java um zero à esquerda significa o uso de uma base diferente da decimal. Felizmente, você irá necessitar usar esta representação muito raramente.

Operações Aritméticas

Java, provê as quatro operações aritméticas básicas: soma, subtração, multiplicação e divisão estão disponíveis diretamente através dos operadores + , , * e / respetivamente. Além destas temos ainda a operação modulo através do operador %.

As operações aritemeticas com inteiros, em Java, têm regras específicas.

Operações aritméticas sobre inteiros

A JVM sempre irá converter o número para um inteiro de 32 bits para fazer um cálculo, exceto no caso de long em que 64 bits serão utilizados. Experimente fazer este calculo em Java:

	byte a = 1;
	byte b = 2;
	byte c = a + b;

Ao executar este código terá um surpresa. A JVM irá informá-lo que não é possível atribuir um int a um byte. Porquê?

Como foi dito, a e b serão implicitamente convertidos para int antes de serem somados. As operações aritméticas apenas acontecem em 32 ou 64 bits.

Se tivermos a certeza que cabe, então podemos forçar a operação com um cast, assim:

byte c = (byte) (a + b);

Contudo, imagine que a é 70 e b é 80. O resultado será 150 que é maior que o maior número que um byte pode representar (127). Ao fazer o cast o resultado é inesperado. Vejamos com mais atenção como se processa.

O número 70 em binário se representa como 01000110 e 80 como 01010000 . Para somar o java transforma para int com 32 bits. 70 fica então 0000000001000110 e 80 fica 0000000001010000 . A soma é então efetuada e o resultado é 150, em binário 0000000010010110. Ok. Agora como fazemos isso caber em um byte de volta? Sim, cortamos os zeros à esquerda e ficamos como o valor 10010110 em byte. Mas que valor está aqui representado?

Como vimos, à exceção do char todos os tipos numéricos em java têm sinal, ou seja, podem representar números negativos ou positivos. Como o java controla o sinal? Através do último bit (o mais à esquerda). Se 0 é positivo, se 1 é negativo. Repare que 70 e 80 têm um 0 no bit mais à esquerda (porque são positivos). Contudo, o resultado da nossa soma começa com 1. Isso significa que é um número negativo. É o número -22. Com certeza não é 150. Não é o número que esperávamos.

A lição a aprender daqui é que para fazer cálculos temos que escolher um tipo numérico apropriado. Como a JVM sempre converte para int para fazer a conta, usar int é o mais natural e comum. Exceto, é claro, quando é necessário usar o long . Na prática, não são muitas as vezes que precisamos de um long.

Uma outra armadilha é a divisão de inteiros, chamada obviamente: divisão inteira. Quanto é 5/2?

Se respondeu 2.5 está enganado. Na divisão inteira o resultado sempre será um número inteiro. Na realidade o resultado, 5/2 é 2. A divisão inteira representa o quociente entre dois inteiros que também é um inteiro. Para conhecermos o resto da divisão usamos o operador modulo ( % ). Assim, 5%2 é 3.

O que acontece se dividirmos por zero ? Uma ArimethicException é o que acontece.

Em matemática de inteiros não é possível dividir por zero. Cuidado com isto.

Operações aritméticas sobre decimais

Para resolver as "limitações" com a matemática de divisão de inteiros os primitivos float e double podem ser utilizados

Se uma das parcelas for um inteiro ( byte, short, int , etc. ) e outro um double , o inteiro será promovido a double com o mesmo valor antes da conta ser feita. Lembre-se que double podem representar todos os mesmos valores de byte, short e int. Mas float não pode representar os mesmos valores de int. Por sua vez, double não pode representar todos os valores de long.

Bits Inteiros

Como regra é importante lembrar que se um tipo de ponto flutuante tem N bits, ele não pode conter todos os valores de um tipo de um número inteiro de N bits ou superior. Portanto, float`não pode conter todos os números representáveis por `int, nem por long. E, double`não pode conter todos os números representáveis por `long. A razão disto, é que em um tipo de ponto flutuante de N bits alguns bits - a mantissa - são usados para representar o valor, mas alguns - o expoente - são usados para representar a potência de 2 a ser multiplicada por esse valor. double`tem 53 bits de mantissa, então só pode conter valores de `long que usem 53 bits ou menos. double`tem 23 bits de mantissa, então só pode conter valores de `int ou long que usem 23 bits ou menos.

A linguagem e a plataforma irão permitir converter qualquer tipo de número inteiro para um de ponto flutuante, mas alguns outros valores serão apenas aproximados. Então cuidado com operações entre inteiros e não inteiros.

E o que acontece se dividirmos um double ou float por zero? Ao contráiro do caso dos inteiros, nenhuma exceção será lançada. Um valor será obtido. O valor especial chamado Infinity (infinito) é o resultado. Este valor é estranho, mas matematicamente aceitável como resultado. Operações com Infinity resulta em NaN ( Not A Number, não é um número). Este ainda mais estranho valor é aceite na especificação IEEE 754-1985. Este valor significa que fizemos uma operação que leva a perder a conexão lógica das operações.

A lição a tirar daqui é que utilizando double ou float um cálculo nunca lançará excepções. Contudo, isso não significa que o resultado esteja certo ou sequer que seja um número.

Para resolver este, e outros, problemas precisamos apelar para a criação de objetos que sejam inteligentes e consigam driblar as limitações do padrão IEEE 754-1985.

Objetos Numéricos

O que fazer quando o número que queremos representar é maior que um long? O que fazer quando queremos realizar operações sem perda de exatidão?

A resposta é utilizar um padrão de projeto - o ValueObject - e criar um objeto que represente o valor que queremos com as propriedades que queremos.

Para representar um número maior que um long o Java disponibiliza a classe BigInteger. Estes números muito grandes (128 bits ou mais) são muito importantes em criptografia moderna para formar chaves fortes. Sem a existência desde objeto seria impossível implementar criptografia em Java. Além disso BigInteger oferece um conjunto de métodos que permitem realizar operações com objetos deste tipo de forma semelhante ao que fazemos com int ou long e algumas extra como isProbablePrime que determina se um dado número é um número primo. Números primos são muito importantes em criptografia dai a necessidade deste tipo de método.

Para representar números decimais o Java oferece a classe BigDecimal. Esta classe permite realizar todas as operações aritméticas - exceto divisão - sem perda de exatidão. BigDecimal é o substituto natural de double e float e deve ser usado sempre que a perda de precisão não for aceitável tal como em sistema que lidam com dinheiro. Contudo, neste caso específico de lidar com dinheiro, ainda existe o problema do arredondamento e o de somar dinheiro em moedas diferentes. Para resolver o problema da representação de dinheiro a melhor solução é usar o padrão Money (que inteligentemente nem precisa de BigDecimal).

BigDecimal conta com vários modos de arredondamento que podem ser estipulados no momento da operação ou via um objeto de contexto MathContext que pode ser usado ao longo de várias operações.

Ambas classes BigDecimal e BigInteger herdam de Number . Number é a raiz para todos os objetos numéricos padrão na plataforma Java e permite que todos eles sejam convertidos para os tipos numéricos primitivos por métodos como doubleValue e integerValue, por exemplo.

Objetos para primitivos

Classes que herdam de Number são conhecidos como objetos Wrapper (Encapsuladores). Estes objetos são equivalentes às suas contrapartes primitivas, mas não contém métodos para realizar operações aritméticas. Contudo, contêm muitos métodos uteis para converter primitivos para objetos e primitivos de, e para, String. Assim para cada primitivo existe um objeto que herda de Number, a saber: Byte , Short , Character , Integer , Long , Float e Double . Integer é a versão objeto de int e Character a versão objeto de char. Os outros são a versão do tipo primitivo com o mesmo nome.

Poder utilizar números primitivos como se fossem objetos têm muitas vantagens. Por exemplo, podemos guarda um conjunto de número num objeto List ou em um Map .Sendo que esta é uma funcionalidade muito útil, a partir da versão Java 5, o Java inclui as funcionalidades de Auto-Boxing e Auto-Unboxing.

Auto-boxing significa que o compilador deteta quando um número primitivo é sendo utilizado onde se espera um objeto e automaticamente encapsula (coloca na caixa – boxes) esse primitivo no seu wrapper correspondente. A operação de auto-unboxing é o inverso e acontece quando o compilador deteta que um primitivo é esperado onde um objeto é utilizado.

Embora as operações de auto-boxing sejam uteis elas podem ser perigosas se não forem usadas corretamente. Os dois motivos principais para isso são:

  1. Um objeto pode ser null. Ao realizar o unboxing o compilador escolhe um dos métodos de Number para o primitivo certo. Por exemplo, se for esperado um double o compilador invoca objecto.doubleValue() . O problema está em que se o objeto for null esta operação irá causar uma exceção NullPointerException quando o código for executado. A operação de unboxing mascara este problema dos olhos desatentos e pode causar muitos problemas. Prefira sempre fazer as operações de boxing explicitamente.

  2. Como Number não contém métodos para realizar operações aritméticas sempre que um wrapper de um primitivo for utilizado numa operação o compilador irá incluir duas operações de unboxing para converter os objetos para primitivos, fazer o cálculo, e converter de volta para um objeto com uma operação de auto-boxing. São três, ou mais, operações de boxing desnecessárias que causam a criação de objeto à toa. Embora as JVM modernas não se engasguem com isso é considerado uma má prática de programação e um abuso da funcionalidade de auto-boxing totalmente desproposital. Use sempre primitivos enquanto estiver realizando cálculos. Sobretudo se para os cálculos estiver usando algum tipo de laço for ou while.

Operações avançadas sobre inteiros: operações binárias

Além das operações aritméticas é possível realizar operações bit-a-bit sobre todos os tipos primitivos inteiros. Estas operações são muito uteis ao trabalha com criptografia, manipulação de imagem ou de arquivos. Os operadores binários são o & ( operador E) , | (operador OU), ^ ( operador XOR) e ! (operador NOT). Não confundir com os operadores lógicos && e || que apenas atuam sobre o tipo primitivo boolean que, em Java, não é um número.

Mas o que fazer quando queremos manipular mais de 64 bits? Para resolver isso você pode usar BigInteger que também conta com operações bit-a-bit ou utilizar BitSet . BitSet é uma classe especializada em trabalhar com conjuntos de bits e fornece operações para operar sobre eles de forma mais eficiente e além do limite de 64 bits.

Operações avançadas sobre decimais e a classe java.util.Math

Nem só de somas e multiplicações vivem as operações matemáticas. Funções mais complexas como seno e co-seno, potencia, raiz quadrada e logaritmo são operações comuns e necessárias em vários ramos.

Java dá suporte a estas operações através da classe java.util.Math . Esta classe apenas contém métodos estáticos úteis para estes tipos de operações e outras funções genérica como valor absoluto (abs) , sinal (sgn) e arredondamento (round).

Estas funções trabalham com double e como vimos isso pode significar problemas. Contudo, normalmente, para este tipo avançado de operações isso não é um problema muito grave, mas como sempre acontece ao trabalhar com double, surpresas desagradáveis podem acontecer. Portanto, utilize com cautela.

Unicode e Character

O Java disponibiliza o tipo numérico sem sinal chamado char . Este tipo é especial porque o Java estabelece uma correspondência intrínseca entre cada char e um dos caracteres da tabela Unicode com o mesmo número. Por isso os valores de char são escritos usando letras tal como 'a' ou '@' ou '4'.

O interessante é que, sendo um tipo numérico, podemos realizar aritmética com estes valores, ou ainda misturar com inteiros e fazer, por exemplo:

	char resultado = 'a' + '4';
	char letraB = 'a' + 1;
Esta operação curiosa representa andar pela tabela Unicode um número de "casas" para a direita.
Assim, 'a' + 1 é 'b' e 'b' + 1 é 'c' e 'a' + 2 é 'c'.  Estas operações são úteis ao manipular textos e são a base da classe  `String`. A classe String nada mais é que uma classe que encapsula e manipula um array de `char` para compor textos.

É verdade que um char representa um caracter unicode, mas apenas e só porque o Java faz o mapeamento intrinsecamente. Na realidade das coisas char é apenas um número que apenas pode ser positivo, o que faz dele o único tipo unsigned do java.

Instanciando objetos de valor

Todos os objetos que herdam de Number contam com construtores para a instanciação. Contudo, isso é um legado da versão 1.4 e anteriores do java que não deve ser usado na prática. Para instanciar um objeto de valor deve se usado o método valueOf() . Este método é, normalmente, sobrecarregado para aceitar argumentos primitivos e String . Para os objetos de valor que representam valores inteiros (Byte, Short, Integer , BigInteger e Long) o uso de valueOf() é bastante simples, descomplicado e direto. Contudo, para os tipos decimais (Double, Float e BigDecimal) o uso de valueOf() pode ser bastante enganador. A regra a seguir para os tipos decimais é sempre deve ser preferido o uso do método valueOf() que recebe um objeto String Desta forma não ha perda de precisão, mesmo quando usamos Double e Float.

	// incorreto
	BigDecimal big = BigDecimal.valueof(1.23456);

	// correto
	BigDecimal big = BigDecimal.valueof("1.23456");

Apenas use as outras sobrecargas de valueOf() quando não conseguir ter acesso a uma representação em String

Poderiamos esperar que os construtores destes objetos fossem mais amigáveis e cuidassem dos casos estranhos, conforme a propria documentação javadoc refere, esse não é o caso. O que significa que a atenção deve ser redobrada ao trabalhar com números decimais.

Convertendo entre objetos de valor

A conversão entre primitivos é feita através da operação de cast, contudo para objetos isso não é possível. Para converter entre os diferentes objetos de valor devemos recorrer à conversão para primitivos e o uso de valueOf(). Esta regra é válida para os valores inteiros, mas como sempre não se aplica para os tipos decimais.

Poderiamos pensar, por exemplo, que o codigo a seguir é uma forma legitima de converter um objeto Double para BigDecimal.

	// diretamente do valor primitivo

	BigDecimal big = BigDecimal.valueof(1.23456);

	// diretamente do wrapper
	Double d = Double.valueof(1.23456);

	BigDecimal big = BigDecimal.valueof(d.doubleValue());

Contudo, a forma seguinte - conforme explicado no Javadoc de BigDecimal, é preferivel.

	// diretamente do valor primitivo

	BigDecimal big = BigDecimal.valueof(Double.toString(1.23456));

	// diretamente do wrapper
	Double d = Double.valueof(1.23456);

	BigDecimal big = BigDecimal.valueof(d.toString());

Esta conversão só funciona para os valores de double e float que <class>BigDecimal</class> aceita. Para valores como NaN(Not a Number) e infinity não ha conversão possível.

Comparações

Para os tipos primitivos o java ainda suporta os operadores de comparação: == ( igual), > (maior),< (menor) , >= (maior ou igual), (menor ou igual) e != (diferente).

Para tipos inteiros funciona perfeitamente, mas para double e float não é suficiente. Para uma explicação detalhanda do porquê leia Comparando Objetos em Java, mas essencialmente os operadores normais não funcionam corretamente para comparar double e float e é necessário apelar para o método compare que se encontra no wrapper respetivo.

A moral da história é simples. Utilize apenas primitivos inteiros ( byte, short, char, int, long) para números inteiros e BigDecimal para decimais. Não use double nem float a menos que você saiba muito bem o que está fazendo. Se acha que sabe, https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html(pense melhor).

Para valores monetários utilize o padrão Money, já que nenhum dos tipos primitivos, nem nenhums dos objetos de valor asseguram as regras necessárias às operações aritméticas com dinheiro.

Bibliografia

[1] How Computers Use Numbers (https://mabi.land/numbers/)

[2] What Every Computer Scientist Should Know About Floating-Point Arithmetic (https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)

[3] Java theory and practice: Where’s your point? (Sobre double e float) (http://www.ibm.com/developerworks/java/library/j-jtp0114/)

Scroll to Top