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.

limites binarios

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.

sinal magnitude

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.

complemento de dois

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.

overflow

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.

underflow

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:

inteiros

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á:

Saída no Console
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.

bitwise xor

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);
    }
}
Saída no Console
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:

xor underflow

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:

exact math jse8

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:

Saída no Console
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:

ponto flutuante

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:

dizima binaria


comments powered by Disqus