Números

Trabalhar com números pode ser uma tarefa complicada e frustrante para quem começa a programar, pois existem alguns comportamentos que não são óbvios e não são iguais aos que nos ensinam na matemática da escola.

Número em um computador

Um computador utiliza um conjunto de bits para representar números. Isso é feito de forma bastante inteligente e eficaz utilizando representações em base 2.

Ao trabalhar com números num computador sabemos que não temos como presentar todos os números possíveis já que não temos suficientes bits para isso. Contudo, a maioria das aplicações não necessita de números muito grandes e normalmente 32 bits são suficientes para representar muitas das quantidades que utilizamos no dia a dia.

A representação em base 2 é simples e funciona bem para números inteiros. Mas muitas das aplicações que estamos interessados necessitam de números decimais - os números com vírgulas.

Existem muitas formas de codificar números com vírgulas mas podem se dividir em duas famílias: número de virgular fixa, e números de vírgula flutuante. Aqui "vírgula" se refere ao separador de decimais, que em computação também é chamado "ponto".

Acontece que representações de ponto flutuante são mais comuns e o padrão mais comum para os condificar é o padrão IEEE 754-1985. Este padrão estabelece como utilizar os bits para representar quantidades decimais. Os computadores modernos normalmente entendem 32 e 64 bits. Processadores especiais podem utilizar 16 bits - como utilizado para Machine Learning.

Quanto mais bits, mais números podemos representar e mais exatidão podemos obter. Contudo, enquanto na matematica é sempre possivel achar um terceiro número real entre dois números dados - os reais são densos -, na representação binária IEEE 754-1985 isso nem sempre é possível. Existem números que simplesmente não podem ser representados. Isto é origem de vários problemas e bugs dífíceis de detetar e resolver.

Tipos numéricos

A maioria das plataformas e linguagens modernas disponibilizam tipos que representam os números inteiros e de ponto flutuante. Nem todas as plataformas oferecem toda a gama de opções. Os diferentes tipos distribuem-se em familias conforme várias propriedades.

  • Primitivo vs Objeto - em algumas linguagens orientadas a objetos tipos numéricos são tratados como instancias de objetos ou apenas padrões de bits primitivos. Algumas linguagens não fazem distinção clara, mas não permitem que um primitivo seja criado.

  • Numero de bits - os tipos são diferenciados com relação a quantos bits ocupam. Desta forma o programador pode escolher o tipo mais compacto e poupar memória.

  • Inteiro, ponto flutuante, ou outro - os tipos são diferenciados pelo padrão usado na implementação.

  • Sinal - alguns tipos não têm sinal e só podem representar valores zero ou positivos.

O número de bits que ocupa varia normalmente varia entre 8 e 64 com algumas exeções indo até 128. Os valores corresponedem com potências de dois. Os nomes dos tipos variam de linguagem para linguagem. Usaremos a nomenclatura de prefixar o número de bits com os prefixo int, float ou uint. Assim uint16, por exemplo, seria um número, inteiro, apenas positivo, com 16 bits. Por outro lado, float64 , por exemplo, seria um número de vírgula flutuante, com 64 bits.

A seguir um pequeno resumo dos tipos numéricos principais de algumas plataformas:

Java

.NET

JavaScript

Dart

uint8

não

sim

não

não

uint16

sim (char)

sim

não

não

uint32

não

sim

não

não

uint64

não

sim

não

não

int8

sim (byte)

sim (byte)

não

não

int16

sim (short)

sim (short)

não

não

int32

sim (int)

sim (int)

parcialmente

não

int64

sim (long)

sim (long)

não

sim (int)

float32

sim (float)

sim (float)

não

não

float64

sim (double)

sim (double)

sim (number)

sim (double)

Entre parentesis está o nome dado ao tipo, na plataforma. A plataforma Javascript dá suporte a inteiros de 32 bits em situações muito particulares, especialmente as relacionadas a operações que manipulam os bits em si, como AND e OR e não a operações matemáticas.

Representações Literais

Os tipos numéricos são muito usados, e para serem de facil uso, é necessário que a linguagem ofereça literais numéricos. Ou seja, que o número possa ser escrito diretamente no código. Para escrever um número é necessário definir uma base. Estamos habituados à base decimal com os algarismos de 0 a 9 e quando pensamos em um número normalmente pensamos na sua representação decimal 42 ou 3.14156.

Algumas linguagems oferecem outras bases, úteis em dominios especificos. As linguagens podem oferecer, além da base decimal as bases: octal, hexadecimal e binária.

Normalmente estas representações de base alternativa apenas se aplicam a números inteiros.

Operações Aritméticas

As plataformas também oferecem suporte às quatro operações aritméticas básicas: soma, subtração, multiplicação e divisão; através dos operadores + , , * e / respetivamente. Além destas temos ainda a operação modulo através do operador %.

Algumas linguagens oferecem ainda operações especiais como potenciação com o operador ** ou ^.

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

Operações aritméticas sobre inteiros

Nem todas as operações aritméticas sobre inteiros resultam num inteiro, por exemplo, cinco dividido por dois não é um inteiro. Então o operador de divisão para números inteiros, nao realmente encontra o quociente real entre eles, mas o número inteiro mais aproximado. No caso 5/2 é apenas 2 e não 2.5 como poderia esperar.

Por outro lado, para inteiros, não ha o conceito de divisão por zero. O resultado desta operação não está definido matematicamente. Então, se tentar dividir por zero, uma exceção irá acontecer.

Operações aritméticas sobre decimais

Para resolver as "limitações" com a matemática de divisão de inteiros os números de vírgula flutuante são usados. Estes são construtos mais úteis que os inteiros quando os valores não são exatos, mas são construtos informáticos que não seguem à risca as propriedades matematicas que estamos habituados. Isto pode trazer muitos problemas inexperados e ser fonte de bugs.

Os números de vírgula flutuante não têm limitações na divisão. Dividir 5 por 2 resulta em 2.5 como esperado. Também não ha problema em dividir por zero. Nenhuma exceção será lançada, mas será devolvido um número especial chamado Infinity (infinito).

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 números de vírgula flutuante num cálculo nunca lançará exceções. Contudo, isso não significa que o resultado esteja certo ou sequer que seja um número.

Em particular, este tipo de números não é bom para representar potências negativas de dez, ou seja, números como 0,1 ou 0,01 …​

Para provar isso execute estes dois códigos:

	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

Matematicamente, somar 0.1 dez vezes é a mesma coisa que multiplicar 0.1 por dez. Contudo, na prática, com números de vírgula flutuante, não é. Imagine, agora, que são centavos que estamos somando. Dinheiro é perdido na soma. Nada bom.

Não é aritmética que lhe ensinaram na escola

A aritmética utilizada no padrão IEEE 754-1985 não tem as mesmas propriedades que a matemática que nos ensinam na escola. As duas dão os mesmos resultados na maior parte das operações, mas enquanto a matemática que nos ensinam na escola pode representar qualquer número, os padrões IEEE 754-1985 só podem representam uma quantidade limitada de números. Existem, portanto, números que não podem ser representados no padrão IEEE 754-1985, caso em que o número representável, mais próximo, será usado. Isto significa que nem sempre o resultado de uma operação é o número correto e sim o número mais próximo do correto que é possível representar.

Para resolver este, e outros, problemas precisamos apelar para a criação de objetos que seja inteligentes e consigam driblar as limitações do padrão IEEE 754-1985. Normalmente isto significa sair do mero padrão de bits e partir para classes de objetos mais sofisticadas.

Objetos Numéricos

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

Existem operações - como criptogrfia - que precisam de números muito grandes.

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.

Cada linguagem resolve esta questão do seu jeito. É comum tem um tipo chamado BigInt ou BigInteger que representa um inteiro com um número indertimado de algarismos.

Tendo um objeto como este, é natural definir outra classe irmã BigDecimal que permite representar número decimais através de um inteiro e da posição da vírgula nesse inteiro.

Big

O prefixo Big denota um tipo que pode ser arbitrariamente grande ou pequeno apenas limitado pela memória disponível.

Estes tipos de representação, quando disponíveis, permitem realizar operações que não seriam possiveis com os tipos baseados em representação binária, mas nem eles resolvem todos os problemas. A divisão ainda é um problema, mesmo utilizando estes tipos. A representação de dinheiro, pode ser resolvida com o uso de um BigDecimal, mas é melhor endereçada pelo padrão Money (que inteligentemente nem precisa de BigDecimal).

Por fim, ha o problema da performance. Como os tipos não são padrões bits, a execução das operações é mais lenta que aquela conseguida nativamente.

A tabela a seguir mostra tipos especiais que extendem a capacidade numérica das linguagens.

Java

.NET

JavaScript

Dart

bigInt

sim (BigInteger)

sim (BigInteger)

sim (BigInt)

sim (BigInt)

bigDecimal

sim (BigDecimal)

não

não

não

decimal128

não

sim (decimal)

não

não

complex

não

sim (Complex)

não

não

Interessante notar que a plataforma .NET escolheu uma caminho bem diferente. Primeiro, escolheu não prover uma classe BigDecimal e sim um tipo baseado em 128 bits - decimal - para representar valores decimais. Segundo, escolheu prover um tipo Complex para números complexos onde a parte real e a parte imaginária são representadas por tipos de 64 bits cada.

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 lógicos.

Nem todas as plataformas permitem utilizar estas operações binárias sobre números.

Operações avançadas sobre decimais e a classe 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.

Normalmente estas operações operam apenas sobre número de ponto flutuante de 64 bits e estão disponíveis como métodos estáticos em uma classe do SDK. Tradicionalmente esta classe é chamada de Math.

Unicode e Character

Plataformas modernas utilizam Unicode para representar characteres que são a base para construir textos. Unicode é uma forma de codificação de caracteres que utiliza 32 bits, mas existem formas compactas para usar apenas 16 bits.

Este tipo especial de número de 16 bis usado como referência do código unicode um character é normalmente chamado char, mas não existe em todas as linguagens.

Comparações

Para os tipos primitivos baseados em padrões de bits as linguagens disponibilizam operadores de comparação como == ( igual), > (maior),< (menor) , >= (maior ou igual) , (menor ou igual) e != (diferente).

Estes operadores funciona para tipos inteiros e para tipos de ponto flutuante. Contudo, para tipos de ponto flutuante pode não ser suficiente já que os operadores == ( igual) e != (diferente) comparam o padrão de bits e conforme a IEEE 754-1985, mais de um padrão de bits pode representar o mesmo número. Então dados dois números que são numericamente equivalentes, estes operadores são falhar em reconhecer isso se os seus bits não forem exatamente os mesmos. Por isto, nunca compara números de ponto flutuante com == ( igual) e != (diferente).

As linguagens oferecem outra forma de comparação baseada na interface Comparable<T> que define um único método compareTo(T other). Esta interface permite comparar objetos numéricos. Para tipos primitivos normalmente existem métodos estáticos em Math ou em outros objetos para realizar a mesma operação. Esta operação realmente compara o valor matemático e não como é representado.

Uso Geral

Em geral, tente não utilizar números de ponto flutuante para operações que necessitam de exatidão, como lidar com dinheiro ou com medidas. Prefira as classes com representações mais poderosas, mesmo que mais lentas.

Curiosamente todos os números que podemos escrever num programa, são números racionais, contudo quase nenhuma plataforma oferece o tipo correspondente a um número racional. Considere usar uma biblioteca de terceiros que o implemente, ou implemente o seu, com base na implementação de BigInt presente na sua plataforma.

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