Cuidados com transferência de tipos em Java

Ana Cortez - Sep 11 - - Dev Community

Java é ua linguagem fortemente tipada, mas ainda assim é possível transferir valores entre variáveis primitivas de tipos diferentes. Por exemplo, eu posso atribuir o valor de um int para um double sem problemas, contanto que a capacidade de armazenamento do tipo que recebe o valor aguente.

Veja abaixo qual o tamanho que cada tipo primitivo comporta:

Image description

Transferir valor para um tipo com maior capacidade de armazenamento tem um nome técnico: "widening conversion". O termo em português é geralmente traduzido como "conversão de ampliação" ou "conversão de alargamento". Ele se refere ao processo em que um valor de um tipo de dado menor ou mais restrito é convertido para um tipo maior ou mais abrangente, sem perda de informação.

Mas, e se eu quiser transferir o valor para um tipo com menor capacidade de armazenamento? O compilador do Java não gosta disso, mas ele permitirá se você fizer um "cast", como no exemplo abaixo.

double decimal = 65.9;
int i = (int) decimal; //aqui ele perde a casa decimal e vira 65
char c = (char) i; //aqui ele vira a letra A (que corresponde a 65)
Enter fullscreen mode Exit fullscreen mode

Se o tamanho do valor que irá para o novo tipo exceder os limites desse tipo, algo mais dramático pode acontecer. Um int i = 10 cabe em uma variável byte, pois ela comporta 8 bits em um intervalo de -128 a +127. Porém, e se eu quiser colocar um int i = 128 em uma variável do tipo byte... haverá perda de informação.

public class Main
{
    public static void main(String[] args) {
        int i = 128;
        byte b = (byte) i;

        System.out.println(b); // o valor de b agora é -128 :S
    }
}
Enter fullscreen mode Exit fullscreen mode

Autoboxing

No post passado [leia ele aqui], eu falei um pouco das classes Wrapper. Como exemplo, eu tinha escrito Integer.parse(i) = imagine que i é um tipo
primitivo int.

Atualmente, utilizar o método parse do Wrapper não é mais encorajado, pois está depreciado. Para transformar um primitivo em uma classe Wrapper e, desta forma, utilizar métodos built-in, é recomendado fazer um "autoboxing", como no exemplo:

Character ch = 'a';
Integer i = 10;
Enter fullscreen mode Exit fullscreen mode

Note que é uma abordagem mais direta. Simplesmente atribui-se o valor logo de uma vez.

Para fazer o caminho inverso e retomar o dado como tipo primitivo, você pode fazer o "unboxing", utilizando o método valueOf:

Integer i = 10;
int j = Integer.valueOf(i);
Enter fullscreen mode Exit fullscreen mode

Fazer o Wrapper de um primitivo, como eu já disse no post anterior, tem a vantagem de permitir utilizar os métodos da classe e facilitar a vida na hora de trabalhar com os dados.

A versão wrapper de um primitivo pode se parecer muito com ele à primeira-vista, mas a JVM não trata um objeto e um primitivo da mesma forma, não se esqueça. Lembre-se que os primitivos vão para a Stack e os objetos para a Heap [relembre aqui].

Em termos de performance, claro que recuperar dados de um primitivo é menos custoso para o computador, visto que o valor é armazenado diretamente, e não por referência. É muito mais rápido pegar um dado pronto do que ficar juntando os pedacinhos dele na memória.

Mas há casos que utilizar um Wrapper será indispensável. Por exemplo, quando você quiser trabalhar com a classe ArrayList. Ela só aceita objetos como parâmetros, e não valores primitivos.

A flexibilidade que essa transformação de primitivo para objeto e vice-versa traz é muito bacana da linguagem. Mas precisamos ficar atentos a essas armailhas discutidas aqui e muitas outras.

Apenas para chocar a sociedade (rs) vou dar o exemplo de um caso problemático envolvendo o comportamento inesperado de um código ao trabalhar com overloading (ainda não fiz post de overloading, mas vou fazer. Basicamente, overloading ocorre quando um método possui diferentes assinaturas).

Esse caso foi citado no livro "Java Efetivo", do Joshua Bloch.

public class SetListTest {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();

        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }

        for (int i = 0; i < 3; i++) {
            set.remove(i);
            list.remove(i); // como corrigir: list.remove((Integer) i);
        }

        System.out.println(set + " " + list);

    }
Enter fullscreen mode Exit fullscreen mode

Neste programa, o objetivo era adicionar a um set e a uma lista valores inteiros de -3 a 2 [-3, -2, -1, 0, 1, 2]. Em seguida, excluir os valores positivos [0, 1 e 2]. Mas, se você rodar esse código, você vai perceber que o set e o list não apresentaram o mesmo resultado. O set retorna [-3, -2, -1], como esperado. Já o list retorna [-2, 0, 2].

Isso acontece porque a chamada para o método built-in remove(i) da classe List trata o i como um tipo primitivo int, e nada mais. O método, por sua vez, remove os elementos na posição i.

Já a chamada para o método remove(i) da classe Set chama um overload que recebe como parâmetro um objeto Integer, convertendo automaticamente i, que era originalmente um int, para Integer. O comportamento desse método, por sua vez, exclui do conjunto os elementos que tiverem valor igual a i (e não o índice igual a i) - note que o tipo esperado tanto para o set quanto para o list era Integer. (Set set / List list). Por isso que o overloading escolhido para o método remove, da classe Set, fez a conversão para Integer.

Enquanto o comportamento de remove em List é excluir pelo índice, o do remove em Set é excluir pelo valor. Tudo por conta de um overloading de remove que recebe Integer.

. . . .
Terabox Video Player