Trabalhando com Números

Este texto foi atualizado e doado ao projeto Code Design World.
Leia a versão mais atualizada.

Trabalhar com números em Java pode ser uma tarefa complicada e frustrante para quem está começando.

Tipos

Java, tal como a maioria das linguagens modernas, tem vários tipos numéricos. Cada um caracterizado pelo numero de bits que ocupa. A diferença entre java e algumas outras linguagens – especialmente as chamadas linguagens de script – é o suporte a tipos numéricos como tipos primitivos. “tipo primitivo” significa que não é um objeto. Isto aumenta a eficiente da JVM e permite que ela funcione até em ambientes com pouca memória e poder de processamento. Desde ponto de vista Java não é uma linguagem puramente orientada a objetos já que é possível programa sem eles utilizando apenas tipos primitivos. É claro que isso é ridiculamente mais difícil e é como se faziam programas à 40 anos atrás ou mais.

Os tipos primitivos 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 exactamente quantos númentos existem entre -128 e 127.

short – ocupa 16 bits ( 2 bytes). Representa apenas números inteiros. O menor número possivel 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 numeros positivos. O menor numero possivel representar é 0 e o maior é 65.536. O char é um tipo numérico especial e veremos porquê, contudo ele é um tipo numérico. Isso tem grandes implicações na prática.

int – ocupa 32 bits ( 4 bytes). Representa apenas números inteiros. O menor número possivel 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 possivel 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 possivel representar é 1.40239846×10-46
e o maior é 3.40282347×1038. Em java o float é garantidamente implementado conforme a norma IEEE 754-1985 o que significa que o calculo pode ser repetido em qualquer hardware ou sistema operacional. Isto é muito importante. A plataforma .NET não garante isto, nem C++.

double– ocupa 64 bits ( 8 bytes). Pode representar números inteiros ou não. O menor valor possível representar é 4.94065645841246544 x10-324 e o maior é 1.7976931348623157×10308. Em java o double é garantidamente implementado conforme a norma IEEE 754-1985 o que significa que o calculo pode ser repetido em qualquer hardware ou sistema operacional. Isto é muito importante. A plataforma .NET não garante isto, nem C++.

Operações

As quatro operações básicas : soma, subtração, multiplicação e divisão estão disponiveis diretamente atravez dos operadores + , – , * e / respetivamente. Além destas temos ainda a operação modulo através do operador %.

Embora todos os tipos possam ser utilizados em cálculos existem regras especiais que é necessário conhecer para não ter surpresas.

Operações aritméticas sobre inteiros

A JVM sempre irá converter o número para um int para fazer um cálculo, excepto no caso do long em que o long será utilizado.  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 é possivel atribuir um int a um byte.  Porquê ?  Como foi dito, a e b serão convertidos para int para poderem ser somados. O resultado será portanto um int, e um int ( 2 bytes)  não cabe em um byte.

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 numero 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 à esquera e ficamos como o valor 10010110 em byte. Hummm… 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 numeros negativos ou positivos. Como o java controla o sinal ? Através do ultimo bit ( o mais à esquerda). Se 0 é positivo, se 1 e 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 signficia que é um numero negativo. É o numero -22. Humm… concerteza não é 150. Não é o número que esperávamos.

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

Uma outra pegadinha é a divisão de inteiros, chamada obviamente : divisão inteira.  Quanto é  5/2 ?   Se respondeu 2,5 está enganado.  Na divisão inteira nunca o resultado será um número não inteiro. Assim, 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.  É comum utilizar o operador / esperar que o resultado seja um numero decimal. Nunca será. Cuidado com isto.

E o que acontece se dividirmos por zero ? Uma ArimethicException é o que acontece. Em matemática de inteiros não é possivel dividir por zero. Cuidado com isto.

Representações

Em java os literias numéricos ( os numeros que você escreve directamente no código) podem ser escritos em três bases diferentes : decimal, octal e hexadecimal.

Decimal é a representação padrão e aquela a que estamos habituados. Simplesmente escrevemos 42, por exemplo, e pronto. Para especificar a representação em hexadecimal começamos o numero com um zero e um ‘x’ e fica 0x42 ( que não é a mesma coisa que 42)

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).

Cuidado! em Java um zero à esquerda significa que está usando a base octal e não será desprezado como acontece em outras linguagens. Felizmente raramente você irá necessitar usar esta representação.

Operações aritméticas sobre decimais

Para resolver o problemas com matemática de inteiros os primitivos float e double podem ser utilizados para obter cálculos com virgulas. Assim 5d/2d = 2,5 ( o d é para indicar “double”). Se um 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 float e double podem representar os mesmos valores int e long respetivamente.  Assim teremos 5 /2d = 2,5 ( onde 5 é inteiro)

E o que acontece se dividirmos por zero ? Nada. Um valor será obtido. O valor especial chamado Infinity (infinito) é o resultado. Este valor é estranho, mas matemáticamente 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 .

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

Um problema grave com o double é que ele não é bom para representar potências negativas de 10. ou seja, números como 0,1 ou 0,01 …  etc..

Para provar isso execute estes dois codigos:

double a = 0.1;

double r = a * 10;

System.out.println ( r ); // imprime 10

e por outro lado

double a = 0.1;
double c = 0;

for (int i=0 ; i<10 ; i++){
c = c + a;
}

System.out.println ( r ); // imprime 0.9999999999999999

Ora, metematicamente , somar 0.1 dez vezes é a mesma coisa que multiplicar 0.1 por dez. Contudo, na prática, com double, não é.

Agora imagine que são centavos de dolar que estamos somando. Estamos perdendo dinheiro na soma. Nada bom.

Para resolver este problemas precisamos apelar para a criação de objetos que seja inteligentes e consigam driblar este problema.

Objetos Numéricos

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

A resposta do Java é utilizar um padrão de projeto – o ValueObject – e criar um objecto 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 impossivel 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úmero decimais o Java oferece a classe BigDecimal. Esta classe permite realizar todas as operações aritméticas sem perda de exatidão. BigDecimal é o substituto natural de double e deve ser usado sempre que a perda de precisão não for aceitável tal como em sistema que lidam com dinheiro.

BigDecimal conta com vários modos de arredondamento que podem se estipulados no momento da operação ou via um objecto 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 na plataforma Java e permite que todos eles sejam convertidos para os tipos numéricos primitivos através de métodos como doubleValue e integerValue,por exemplo.

BigDecimal é uma boa escolha para representar quantidades monetárias em vez de double ou float. Contudo ainda existe o problema do arredondamento e o de somar dinheiro em moedas diferentes. Para resolver o problema do dinheiro a melhor solução é usar o padrão Money ( que inteligentemente nem precisa de BigDecimal, basta um long)

Objectos para primitivos

Outras classes que herdam de Number são conhecidos como objectos Wrapper (Encapsuladores). Estes objectos são equivalentes às suas contrapartes primitivas mas não contém métodos para realizar operações. Contudo, contém muitos  métodos  uteis para converter primitivos para objectos e primitivos de e para String.

Assim para cada primitivo existe um objecto que herda de Number, a saber: Byte, Short, Character, Integer, Long , Float e Double. Integer é a versão objecto de int e Character a versão objecto de char.

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

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

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

  • Um objecto 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 objecto for null esta operação irá causar uma exceção NullPointerException quando o códig for executado. A operação de unboxing mascara este problema dos olhos desatentos e pode causar muitos problemas.
  • Um outro problema relaciona-se às operações aritméticas. Como Number não contém métodos para realizar operações sempre que um wrapper de um primitivo for utilizado em uma 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 objecto 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 objecto à 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 quanto estiver realizando calculos. Sobretudo se para os calculos 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 é possivel realizar operações bit-a-bit sobre todos os tipos primitivos inteiros. Estas operações são muit 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 ( aqueles que existem em um long). 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.

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

Nem só de somas e multipliacaçõ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 Math. Esta classe apenas contém métodos estáticos uteis para estes tipos de operações e outras 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 estas funções com cuidado e atenção.

Character

Como vimos o Java disponibiliza o tipo numérico sem sinal char.  Este tipo é especial porque o Java estabece uma correspodência intrinseca entre cada char e um dos caracteres da tabela Unicode com o mesmo numero.  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 este valores e fazer por exemplo ‘a’ + ‘4’ ou ainda misturar com inteiros e fazer ‘a’ + 1.  Esta operação curiosa representa anda pela tabela Unicode um numero de “casas” para a direita. Assim, ‘a’ + 1 é ‘b’ e ‘b’ + 1 é ‘c’ e ‘a’ + 2 é ‘c’.  Estas operações são uteis ao manipular textos e são a base da classe String – talvez a classe mais usada em Java.

A classe String nada mais é que uma classe que encapsula e manipula um array de char para compor textos.

Então, é 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.

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 é bem suficiente. Isto porque os operadores de comparação atuam sobre a representação binário do numero e essa, para double e float, pode ser diferente mesmo quando o numero representado é o mesmo.

Por outro lado devido à existência de valores especiais para estes tipos como NaN não ha garantia que as regras comuns para inteiros se apliquem nesses casos. O caso de NaN é típico porque um NaN sempre é diferente de outro NaN. Então x == x pode ser falso se x for NaN. Isto é impensável para valores inteiros e foje um pouco da logica que utilizamos no dia a dia, embora, matemáticamente faça todo o sentido.

Para comparar double e float é preciso apelar para um método especial que se encontra no respetivo wrapper. O método compare() permite comparar realmente os valores e não os bits. Este tipo de subtileza é o que torna trabalhar com estes tipos numéricos perigoso, pois a maioria dos programadores irá utilizar > ,< ou == sem pensar duas vezes. Quando o programa começar a apresentar erros de calculo ou ordenação ninguém saberá porquê.

Para os wrapper que implementa Number a comparação é simples já que Number implementa Comparable mas mesmo aqui ha pegadinhas pois , por exemplo, Double implementa equals como doubleValue() == doubleValue(). Assim, mesmo com wrappers é preciso usar o método especial compareTo de Double ( ou Float).

A moral da história é simples. Utilize apenas primtiivos inteiros ( byte, short, char, int, long) para numeros inteiros e BigDecimal para decimais. Não use double e float a menos que você saiba muito bem o que está fazendo. Utilizar estes tipos é o equivalente computacional de fazer malabarismos com motosserras com as mãos empreganadas de oleo lubrificante. Só para especialistas…

Referencias

Java theory and practice: Where’s your point? (Sobre double e float)

URL:http://www.ibm.com/developerworks/java/library/j-jtp0114/

Um pensamento em “Trabalhando com Números”

  1. Sérgio parabéns pelo tutorial. Nunca vi nenhum material desse tipo tão bem explicado de maneira tão simples e prática. Continue assim. Forte abraço!

Deixe um comentário