Java Wrappers e Autoboxing

Os tipos primitivos (int, double, char, boolean…) são simples e rápidos, mas não são objetos: não têm métodos, não podem ser null e não entram nas coleções do Java. Para cada um deles existe uma classe "embrulho", um wrapper. Nesta aula você conhece essas classes, os métodos úteis que elas trazem e duas armadilhas famosas: o == que funciona até 127 e o null que vira NullPointerException.

Você vai aprender
  • Quais são os wrappers (Integer, Double, Character, Boolean…) e para que servem.
  • O que o Java faz sozinho no autoboxing e no unboxing.
  • Converter texto em número com Integer.parseInt, Integer.valueOf, Double.parseDouble.
  • Métodos e constantes úteis: Integer.MAX_VALUE, Character.isDigit, Integer.toBinaryString…
  • Por que Integer a = 128, b = 128; a == b dá false (o cache de -128 a 127).
  • O NullPointerException ao desembrulhar null.
  • Por que as coleções, como ArrayList, usam wrappers.

A ideia antes do código

Imagine um presente. O presente em si (o valor 29) é a coisa que importa. Mas, para mandar pelo correio, ele precisa estar numa caixa, com etiqueta e espaço para o endereço. A caixa é o wrapper ("embrulho", em inglês): um objeto que guarda um valor primitivo dentro e acrescenta o que objetos têm, como métodos e a possibilidade de "não ter nada dentro" (null).

Colocar o valor na caixa se chama boxing ("encaixotar"); tirar se chama unboxing ("desencaixotar"). O Java faz as duas coisas sozinho na maioria das vezes, e por isso o nome autoboxing.

Os oito wrappers

PrimitivoWrapperRepare
byteByte
shortShort
intIntegerNome completo, não Int
longLong
floatFloat
doubleDouble
charCharacterNome completo, não Char
booleanBoolean

Regra para lembrar: o wrapper é o nome do primitivo com letra maiúscula, menos dois, que têm o nome por extenso: Integer e Character. Todos ficam em java.lang, então não precisam de import.


Autoboxing e unboxing

Veja no JShell o Java colocando e tirando o valor da caixa sem você pedir:

$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> Integer idade = 29;
idade ==> 29

jshell> int copia = idade;
copia ==> 29

jshell> /exit
|  Goodbye
Integer idade = 29;
Autoboxing: 29 é um int, mas a variável é Integer. O Java embrulha sozinho, como se você tivesse escrito Integer.valueOf(29).
int copia = idade;
Unboxing: idade é um objeto Integer, e a variável é int. O Java desembrulha sozinho, como se fosse idade.intValue().

Isso acontece em atribuições, ao passar argumentos para métodos, ao devolver valores e em contas: idade + 1 desembrulha idade, soma como int e, se o resultado for para uma variável Integer, embrulha de novo. Na maior parte do tempo você nem percebe. As armadilhas aparecem quando você compara caixas ou quando a caixa está vazia; elas estão mais adiante.


Por que as coleções usam wrappers

As coleções do Java (listas, conjuntos, mapas) guardam objetos. Um int não é objeto, então isto não compila:

Este código tem um erro de propósito.
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<int> notas = new ArrayList<>();
    }
}
$ java Main.java
Main.java:5: error: unexpected type
        ArrayList<int> notas = new ArrayList<>();
                  ^
  required: reference
  found:    int
1 error
error: compilation failed

"Tipo inesperado: exigido uma referência (um tipo de objeto), encontrado int". Entre os < > só cabem classes. Com o wrapper, funciona, e o autoboxing deixa o resto natural:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> notas = new ArrayList<>();
        notas.add(7);
        notas.add(9);
        notas.add(10);
        int soma = 0;
        for (int n : notas) {
            soma += n;
        }
        System.out.println("Notas: " + notas);
        System.out.println("Soma: " + soma);
    }
}
$ java Main.java
Notas: [7, 9, 10]
Soma: 26

notas.add(7) embrulha o 7 num Integer; o for (int n : notas) desembrulha cada um de volta para int. Você vai estudar o ArrayList a fundo em ArrayList; por enquanto, guarde a regra: nas coleções, use o wrapper (ArrayList<Integer>, ArrayList<Double>, HashMap<String, Integer>).


Texto para número (e de volta)

O uso mais comum dos wrappers no dia a dia são os métodos estáticos de conversão. Texto vem de todo lugar: do teclado, de arquivos, de argumentos do programa.

$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> Integer.parseInt("42")
$1 ==> 42

jshell> Integer.valueOf("42")
$2 ==> 42

jshell> Double.parseDouble("9.5")
$3 ==> 9.5

jshell> Double.parseDouble("9,5")
|  Exception java.lang.NumberFormatException: For input string: "9,5"
|        at FloatingDecimal.check (FloatingDecimal.java:2324)
|        at FloatingDecimal.readJavaFormatString (FloatingDecimal.java:1969)
|        at FloatingDecimal.parseDouble (FloatingDecimal.java:110)
|        at Double.parseDouble (Double.java:971)
|        at (#4:1)

jshell> Boolean.parseBoolean("true")
$5 ==> true

jshell> Boolean.parseBoolean("sim")
$6 ==> false

jshell> String.valueOf(42)
$7 ==> "42"

jshell> Integer.toString(42)
$8 ==> "42"

jshell> /exit
|  Goodbye
MétodoRecebeDevolveObservação
Integer.parseInt(texto)Stringint (primitivo)O mais usado. Texto inválido: NumberFormatException
Integer.valueOf(texto)String (ou int)Integer (objeto)Use quando precisa do objeto; aproveita o cache (veja abaixo)
Double.parseDouble(texto)StringdoubleExige ponto decimal: "9,5" falha
Long.parseLong, Short.parseShort…Stringo primitivoUm para cada tipo
Boolean.parseBoolean(texto)Stringbooleantrue só para "true" (sem diferenciar maiúsculas); qualquer outra coisa dá false, sem exceção
String.valueOf(x) ou Integer.toString(x)númeroStringO caminho de volta. "" + x também funciona

Dois detalhes que derrubam muita gente: o parseDouble não entende a vírgula brasileira (para isso, veja Formatar Texto), e o parseBoolean nunca reclama: "sim", "1" e "verdadeiro" viram false em silêncio. Para tratar texto inválido no parseInt, use try e catch.

Missão: as idades chegaram como texto. Do jeito que está, o + cola os textos em vez de somar. Use Integer.parseInt dentro do laço para que a soma dê 85 (troque também o tipo de soma).

public class Main {
    public static void main(String[] args) {
        String[] idades = {"29", "34", "22"};
        String soma = "";
        for (String texto : idades) {
            soma += texto;
        }
        System.out.println("Soma: " + soma);
    }
}
public class Main {
    public static void main(String[] args) {
        String[] idades = {"29", "34", "22"};
        int soma = 0;
        for (String texto : idades) {
            soma += Integer.parseInt(texto);
        }
        System.out.println("Soma: " + soma);
    }
}

Constantes e métodos úteis

As classes wrapper também são "caixas de ferramentas" para o tipo delas:

$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> Integer.MAX_VALUE
$1 ==> 2147483647

jshell> Integer.MIN_VALUE
$2 ==> -2147483648

jshell> Long.MAX_VALUE
$3 ==> 9223372036854775807

jshell> Integer.parseInt("ff", 16)
$4 ==> 255

jshell> Integer.toBinaryString(10)
$5 ==> "1010"

jshell> Integer.compare(3, 7)
$6 ==> -1

jshell> Integer.max(3, 7)
$7 ==> 7

jshell> Character.isDigit('7')
$8 ==> true

jshell> Character.isLetter('7')
$9 ==> false

jshell> Character.isLetter('a')
$10 ==> true

jshell> Character.toUpperCase('a')
$11 ==> 'A'

jshell> Character.isWhitespace(' ')
$12 ==> true

jshell> Character.getNumericValue('7')
$13 ==> 7

jshell> /exit
|  Goodbye
MembroO que faz
Integer.MAX_VALUE / MIN_VALUEO maior e o menor int (veja int, long, short, byte). Existem em todos os wrappers numéricos
Integer.parseInt(texto, base)Lê um número em outra base: 16 (hexadecimal), 2 (binário)…
Integer.toBinaryString(n), toHexString(n)Escreve o número em binário ou hexadecimal
Integer.compare(a, b)Negativo se a < b, zero se iguais, positivo se a > b (usado para ordenar, veja Comparable e Comparator)
Integer.max, min, sumMaior, menor, soma (iguais aos do Math)
Character.isDigit(c), isLetter(c), isLetterOrDigit(c)Testa o tipo de caractere
Character.isUpperCase(c), toUpperCase(c), toLowerCase(c)Maiúsculas e minúsculas
Character.isWhitespace(c)Espaço, tab, quebra de linha
Character.getNumericValue('7')O valor do dígito (7), e não o código do caractere (55)
Double.isNaN(x)Testa se é o "não é um número" (veja double e float)

Armadilha 1: == entre wrappers e o cache de -128 a 127

Este é um dos comportamentos mais estranhos do Java, e cai em entrevista de emprego. Rode e compare:

public class Main {
    public static void main(String[] args) {
        Integer a = 127, b = 127;
        System.out.println("127 == 127? " + (a == b));
        Integer c = 128, d = 128;
        System.out.println("128 == 128? " + (c == d));
        System.out.println("128 equals 128? " + c.equals(d));
    }
}
$ java Main.java
127 == 127? true
128 == 128? false
128 equals 128? true

127 == 127 dá true, mas 128 == 128 dá false. Por quê?

  1. Com objetos, == não compara o conteúdo; compara se são a mesma caixa (o mesmo objeto na memória). É a mesma história das Strings (veja Operadores de Comparação e A Classe Object).
  2. O autoboxing usa Integer.valueOf, que mantém um cache: caixas prontas para os números de -128 a 127, os mais usados. Pediu 127? Recebe sempre a mesma caixa do cache. Então a e b são o mesmo objeto, e == dá true.
  3. 128 está fora do cache. Cada autoboxing cria uma caixa nova. c e d têm o mesmo valor, mas são duas caixas diferentes, e == dá false.
  4. equals compara o valor dentro da caixa: true nos dois casos.

O perigo é que o programa funciona em todos os testes com números pequenos (códigos 1, 2, 3…) e quebra em produção quando os códigos passam de 127. A regra é simples: para comparar wrappers, use equals (ou desembrulhe para primitivo antes de usar ==).

Missão: crie Integer a = 500, b = 500;, veja que a == b dá false e depois compare do jeito certo, para dar true.

Integer a = 500, b = 500;
a == b
a.equals(b)

No JShell real, essa sessão fica assim:

$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> Integer a = 500, b = 500;
a ==> 500
b ==> 500

jshell> a == b
$3 ==> false

jshell> a.equals(b)
$4 ==> true

jshell> /exit
|  Goodbye

Armadilha 2: desembrulhar null

Uma variável Integer pode valer null, "caixa nenhuma". Um int não pode. Quando o Java tenta desembrulhar null para int, não há valor nenhum para tirar:

public class Main {
    public static void main(String[] args) {
        Integer nota = null;
        System.out.println("Nota: " + nota);
        int n = nota;
        System.out.println("Dobro: " + n * 2);
    }
}
$ java Main.java
Nota: null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local1>" is null
	at Main.main(Main.java:5)

A mensagem entrega o que o Java faz por baixo dos panos: o unboxing int n = nota; é, na verdade, nota.intValue(), e chamar um método em null é NullPointerException. Repare que a linha 5 nem tem um "ponto" visível: o método chamado foi escrito pelo compilador. (O <local1> é a variável nota; veja O que são Exceções.) Mostrar o null, na linha 4, não deu problema: o + com texto escreve null.

O caso mais comum no mundo real é um HashMap que não tem a chave pedida: o get devolve null.

import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> estoque = new HashMap<>();
        estoque.put("maçã", 12);
        int macas = estoque.get("maçã");
        System.out.println("Maçãs: " + macas);
        int kiwis = estoque.get("kiwi");
        System.out.println("Kiwis: " + kiwis);
    }
}
$ java Main.java
Maçãs: 12
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.HashMap.get(Object)" is null
	at Main.main(Main.java:9)

"…porque o valor devolvido por HashMap.get(Object) é null". Para evitar: guarde o resultado num Integer e teste if (kiwis != null), ou use estoque.getOrDefault("kiwi", 0), que devolve 0 quando a chave não existe.


Primitivo ou wrapper: qual usar

SituaçãoUse
Contas, contadores, variáveis locais, atributos comunsPrimitivo (int, double): mais rápido, nunca é null
Dentro de coleções e genericsWrapper (obrigatório): ArrayList<Integer>
O valor pode "não existir" (nota ainda não lançada)Wrapper, aceitando null (com cuidado), ou Optional
Converter textoMétodos estáticos do wrapper: Integer.parseInt
Comparar dois wrappersequals, nunca ==

Quando dá errado

"int cannot be converted to Long" e "double cannot be converted to Integer"
public class Main {
    public static void main(String[] args) {
        Long populacao = 5;
        Integer preco = 3.5;
    }
}
$ java Main.java
Main.java:3: error: incompatible types: int cannot be converted to Long
        Long populacao = 5;
                         ^
Main.java:4: error: incompatible types: double cannot be converted to Integer
        Integer preco = 3.5;
                        ^
2 errors
error: compilation failed

O que aconteceu: o autoboxing só embrulha um valor na caixa do próprio tipo: int vai para Integer, long para Long. Ele não converte e embrulha ao mesmo tempo. Com primitivos, long x = 5; funciona (o int é alargado), mas Long x = 5; não.

Como resolver: use um literal do tipo certo: Long populacao = 5L; (o L faz o número ser long). Para o preço, use Double preco = 3.5; (ou, sendo dinheiro, BigDecimal).

"unexpected type … required: reference found: int"

O que aconteceu: um primitivo foi usado entre < >, como em ArrayList<int>.

Como resolver: troque pelo wrapper: ArrayList<Integer>.

NullPointerException com intValue(), doubleValue(), booleanValue()

O que aconteceu: um wrapper null foi desembrulhado, numa atribuição a primitivo, numa conta ou num if com Boolean.

Como resolver: descubra de onde veio o null (a mensagem costuma dizer: uma variável, o retorno de um método) e teste antes de usar, ou use um valor padrão.

Comparação que funcionava e parou de funcionar

O que aconteceu: dois Integer (ou Long) comparados com ==. Funciona até 127 por causa do cache e falha com valores maiores.

Como resolver: a.equals(b), ou Objects.equals(a, b) se algum deles puder ser null.


Referência

ItemExemploResultado
AutoboxingInteger x = 29;Integer.valueOf(29)
Unboxingint y = x;x.intValue() (NPE se x for null)
Texto → intInteger.parseInt("42")42
Texto → IntegerInteger.valueOf("42")42 (objeto)
Texto → doubleDouble.parseDouble("9.5")9.5
Número → textoString.valueOf(42)"42"
LimitesInteger.MAX_VALUE2147483647
CacheInteger.valueOf, de -128 a 127Mesma caixa: == dá true só nessa faixa
Comparara.equals(b)Compara os valores

Exercícios

1. O wrapper do char

Qual é a classe wrapper do tipo char?

2. Texto para int

Complete para transformar o texto em int:

int qtd = Integer.("12");

3. Lista de decimais

Complete o tipo da lista de preços:

ArrayList<> precos = new ArrayList<>();

4. O que Integer x = 1000, y = 1000; System.out.println(x == y); mostra?

5. O que acontece em Integer n = null; int m = n + 1;?

6. Quanto vale Boolean.parseBoolean("sim")?

Missão final: os dois códigos são 1500, mas o programa diz que são diferentes. Corrija a comparação.

public class Main {
    public static void main(String[] args) {
        Integer codigoPedido = 1500;
        Integer codigoNota = 1500;
        if (codigoPedido == codigoNota) {
            System.out.println("Códigos iguais");
        } else {
            System.out.println("Códigos diferentes");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Integer codigoPedido = 1500;
        Integer codigoNota = 1500;
        if (codigoPedido.equals(codigoNota)) {
            System.out.println("Códigos iguais");
        } else {
            System.out.println("Códigos diferentes");
        }
    }
}

Resumo

  • Cada primitivo tem um wrapper: Byte, Short, Integer, Long, Float, Double, Character, Boolean.
  • Autoboxing (primitivo → wrapper) e unboxing (wrapper → primitivo) são automáticos, mas só entre um tipo e o seu próprio wrapper (Long x = 5; não compila).
  • Coleções só guardam objetos: ArrayList<Integer>, nunca ArrayList<int>.
  • Integer.parseInt, Double.parseDouble e companhia convertem texto; String.valueOf faz o caminho de volta.
  • == entre wrappers compara caixas, não valores; funciona de -128 a 127 só por causa do cache. Use equals.
  • Desembrulhar null lança NullPointerException (Cannot invoke "java.lang.Integer.intValue()").