Curso de Java - Aula 8
Material de apoio para a vídeo-aula nº 8 do Curso de Java ministrado no Ponto G++, contendo toda a base teórica para um sólido acompanhamento da aula.
Java Curiosa & Divertida
Ocorrência de "overflow" e "underflow" em Java
Como já vimos quando estudamos os tipos primitivos inteiros de Java, quais sejam, o byte
, o short
, o int
e o long
, eles possuem limites fixos, para que "caibam" dentro da memória finita dos computadores. Quando esses limites são ultrapassados, ocorre um erro, denominado "overflow" quando o limite superior for ultrapassado, ou "underflow" quando se tratar do limite inferior.
Um sistema binário de n bits permite a representação de 2n valores possíveis. Na figura abaixo, podemos ver que num sistema de apenas 3 bits, podem ser representados 23 valores possíveis, ou seja, 8 valores, de 0 a 7, neste sistema muito simples, de 3 bits sem sinal.
Se quisermos um sistema que represente também números negativos, tempos que encontrar uma forma de dividir a gama de valores possíveis de serem representados em dois grupos: um para representar os números positivos, e outro para representar os negativos. O método mais simples para isso é denominado "Sinal e Magnitude".
Nesta metodologia, dividimos a quantidade de valores possíveis exatamente ao meio. A primeira metade, cujos valores começam com 0 (zero), serão os números positivos e a segunda metade, cujos números começam com 1 (um), serão os negativos. Vejam sua representação gráfica na próxima figura, e observem que a primeira parte do número binário representa o sinal e a segunda parte a magnitude, vindo daí o nome do método.
Todavia, além da existência e um estranho "zero negativo", conceito inexistente na matemática, um sistema como este seria muito difícil de ser implementado a nível de hardware. Um método mais engenhoso e adequado é o de complemento de dois, que examinaremos a seguir.
Neste método, obtém-se um número negativo invertendo-se os bits do seu equivalente positivo e somando 1 ao resultado, o que corresponde, no sistema binário ao complemento de dois do número considerado, vindo daí a denominação deste método.
Consideremos, então, o número binário 000 no nosso sistema simplificado de três bits. Seguindo a regra, primeiro invertemos os bits, obtendo 111. Em seguida, somamos 1 ao resultado. Como a aritmética aqui é binária, o resultado seria 1000. Como nosso sistema numérico só possui três bits, o quarto bit é descartado e o resultado é 000. Ou seja, eliminamos o problema do "zero negativo".
A representação binária 001 (1 em decimal) resulta em 110, após a inversão dos bits, e em 111, depois de somar 1 ao resultado e assim sucessivamente. Podemos então montar uma tabela contendo todos os valores positivos e negativos possíveis de serem representados no nosso sisteminha numérico de três bits, conforme vemos na próxima figura.
Ademais, a implementação dessa sistemática a nível de hardware é extremamente simples e eficiente, pois uma das duas coisas mais fáceis do mundo para os processadores é "ligar" e "desligar" bits e, a segunda, é somar.
É importante observar algumas situações peculiares decorrentes da aplicação desta metodologia:
-
O valor de -1, tem sempre todos os bits iguais a 1, sejam quantos forem os bits do tipo considerado. Para o tipo primitivo
int
da linguagem Java, por exemplo, todos os seus 32 bits serão iguais a 1, para representar o -1. -
O menor número negativo sempre começa com 1 e todos os demais bits são iguais a 0. (V. na figura acima a representação do -4, ou 100 em binário).
-
O maior número positivo sempre começa com 0 e todos os demais bits são iguais a 1. (V. na figura acima a representação do 3, ou 011 em binário).
Outra grande vantagem do sistema de complemento de dois é que podemos usar a operação de adição para subtrair dois valores, bastando usar o valor negativo correspondente no subtraendo. Por exemplo, para subtrair 2 de 3, cujo resultado é 1 temos, na representação binária no nosso sistema simplificado de três bits, 011 + 110 = 1001 (lembre-se que estamos usando aritmética binária, onde 1 + 1 = 10, isto é, o mesmo conceito do "vai um" quando fazemos as contas no sistema decimal). Como no nosso sistema só dispomos de três bits para representação dos números, o quarto bit é descartado e o resultado é 001, equivalente a 1 na base decimal, que foi o resultado da nossa subtração.
Vejamos agora o que acontece, quando ultrapassamos o limite do tipo numérico considerado. No nosso sisteminha de três bits, esse limite é 3. Se efetuarmos a adição de 3 (011 em binário) com 2 (010 em binário), a soma será 5 e, portanto, fora do limite do sistema. O resultado da aritmética binária é: 011 + 010 = 101, ou seja, estamos somando dois números positivos (iniciam com 0) e obtemos um resultado negativo (inicia com 1). Esta é exatamente a situação chamada "overflow", representada na figura a seguir.
Na situação inversa, quando adicionamos dois números negativos e obtemos um número positivo, ocorre a situação denominada "underflow", muito parecida com a anterior e, portanto, dispensando maiores explicações. A próxima figura apresenta sua representação.
Testando "overflow" e "underflow"
Os limites dos tipos inteiros são bem definidos na linguagem Java, de forma a assegurar sua portabilidade, e podem ser vistos na próxima figura:
Usaremos o programa a seguir para simular uma situação de "overflow", ultrapassando o limite para o tipo inteiro, ao adicionar 20 a Integer.MAX_VALUE
, método que retorna aquele limite. O tipo Integer
é o que denominamos wrapper
e corresponde a uma classe equivalente ao tipo primitivo int
. É um assunto um pouco mais avançado que iremos estudar em detalhes mais adiante, mas aproveitamos para apresentá-lo agora, para que você já vá se acostumando.
public class Overflow { public static void main(String[] args) { int valor = 20; int total = 0; total = Integer.MAX_VALUE; total += valor; // mesmo que total = total + valor if ((valor ^ total) < 0) throw new ArithmeticException("integer overflow"); System.out.printf("Total: %d%n", total); } }
Ao executarmos este programa, ocorre um erro em tempo de execução ao ser ultrapassado o limite do tipo int
e é lançada uma Exceção (Exception). Este também é um assunto muito importante que iremos estudar em detalhes mais à frente. O que interessa, por ora, é observarmos que o programa identifica precisamente a ocorrência do "overflow", objeto do nosso estudo. A saída será:
Exception in thread "main" java.lang.ArithmeticException: integer overflow
at Overflow.main(Overflow.java:10)
A linha do programa que identifica a ocorrência do "overflow" — if ((valor ^ total) < 0)
usa o operador lógico XOR BITWISE (^
) e exige uma explicação mais detalhada. A operação Bitwise XOR de dois números retorna um número em que a comparação entre dois bits diferentes resulta em 1 e dos bits iguais resulta em 0. Observe na figura a seguir, no nosso sistema exemplificativo de três bits, que o XOR
do incremento e da soma de dois números, sempre retornará um valor negativo, ou seja menor do que zero, satisfazendo a condição do programa para lançar a exceção.
A simulação da situação de "underflow" é bem parecida, mudando apenas o limite que está sendo ultrapassado para o menor valor negativo (Integer.MIN_VALUE
).
public class Underflow { public static void main(String[] args) { int valor = -20; int total = 0; total = Integer.MIN_VALUE; total += valor; if ((valor ^ total) < 0) throw new ArithmeticException("integer underflow"); System.out.printf("Total: %d%n", total); } }
Exception in thread "main" java.lang.ArithmeticException: integer underflow
at Underflow.main(Underflow.java:10)
Na figura a seguir podemos ver um exemplo da ocorrência do "underflow" e o resultado correspondente numa operação Bitwise XOR:
A última versão do JDK (Java 8), acrescenta vários métodos para gerar exceções nos casos da ocorrência de "overflow" ou "underflow", evitando a necessidade de implementarmos esses testes. São os seguintes:
Vejam no código abaixo como podemos detectar a condição de "overflow" de uma forma bem mais simples, usando o método Math.addExact
:
public class OverflowUsandoExact { public static void main(String[] args) { int valor = 20; int total = 0; total = Integer.MAX_VALUE; total = Math.addExact(total, valor); System.out.printf("Total: %d%n", total); } }
Percebam que o resultado é exatamente o mesmo:
Exception in thread "main" java.lang.ArithmeticException: integer overflow
at java.lang.Math.addExact(Unknown Source)
at OverflowUsandoExact.main(OverflowUsandoExact.java:7)
Tipos primitivos de ponto flutuante
Os computadores representam os números reais de uma maneira muito mais complexa e engenhosa do que o método de complemento de dois que já estudamos. Seu entendimento, contudo, é muito importante para efetuar otimizações nos aplicativos que venhamos a desenvolver.
Nas nossas primeiras aulas de matemática, aprendemos que existem seis conjuntos numéricos, representados na figura abaixo:
Uma característica dos conjuntos numéricos é que eles são infinitos, ou seja podem representar números cada vez maiores ou cada vez menores infinitamente. Isso é uma coisa impossível de representar na memória finita dos computadores. Para contornar a questão, estabeleceram-se limites para valores positivos e negativos, como já estudamos anteriormente com relação aos tipos inteiros.
Quando falamos então no conjunto dos números reais, essa questão de infinito é ainda mais sensível, pois há infinitas representações numéricas não apenas nos extremos, mas entre cada número, com aproximações cada vez maiores, além dos números irracionais, infinitos em si mesmos, como o célebre π, para o qual, segundo a Wikipedia, já foi calculado, num super-computador, um número de cinco trilhões de dígitos, equivalente a 6 terabytes de dados!
Uma das formas de resolver a questão da aproximação infinita é o arredondamento. Poderíamos, por exemplo, representar o número 1,999999999999999999 por 2,0, com uma perda praticamente desprezível no arredondamento, para a maioria das aplicações.
Outro problema que precisa ser resolvido, é com relação à representação dos números fracionários. Na base 10 a que estamos habituados, usamos o ponto decimal (ou vírgula), para sua representação, sendo que os valores à esquerda do ponto são multiplicados por potências de 10 e, à direita, por frações de 10, representando décimos, centésimos, etc. Há situações, contudo, em que a fração não resulta numa divisão exata, originando as dízimas periódicas. É o caso, por exemplo, de ⅓, cujo resultado é 0.333…, uma dízima periódica.
No sistema de base dois, a representação é semelhante. A diferença é que os números à esquerda do ponto decimal são multiplicados por potências de 2 e os números à direita por frações de 2, havendo também a ocorrência de dízimas periódicas. É o caso de 0.1 (um décimo), cuja representação é exata no sistema decimal, mas no sistema binário situa-se entre 1/8 e 1/16 e, assim, sem representação exata. Isso pode ser bem compreendido na próxima figura: