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.
- 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 == bdáfalse(o cache de -128 a 127). - O
NullPointerExceptionao desembrulharnull. - 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
| Primitivo | Wrapper | Repare |
|---|---|---|
byte | Byte | |
short | Short | |
int | Integer | Nome completo, não Int |
long | Long | |
float | Float | |
double | Double | |
char | Character | Nome completo, não Char |
boolean | Boolean |
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é umint, mas a variável éInteger. O Java embrulha sozinho, como se você tivesse escritoInteger.valueOf(29). - int copia = idade;
- Unboxing:
idadeé um objetoInteger, e a variável éint. O Java desembrulha sozinho, como se fosseidade.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:
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étodo | Recebe | Devolve | Observação |
|---|---|---|---|
Integer.parseInt(texto) | String | int (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) | String | double | Exige ponto decimal: "9,5" falha |
Long.parseLong, Short.parseShort… | String | o primitivo | Um para cada tipo |
Boolean.parseBoolean(texto) | String | boolean | true só para "true" (sem diferenciar maiúsculas); qualquer outra coisa dá false, sem exceção |
String.valueOf(x) ou Integer.toString(x) | número | String | O 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
| Membro | O que faz |
|---|---|
Integer.MAX_VALUE / MIN_VALUE | O 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, sum | Maior, 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ê?
- 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). - 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ãoaebsão o mesmo objeto, e==dátrue. - 128 está fora do cache. Cada autoboxing cria uma caixa nova.
cedtêm o mesmo valor, mas são duas caixas diferentes, e==dáfalse. equalscompara o valor dentro da caixa:truenos 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ção | Use |
|---|---|
| Contas, contadores, variáveis locais, atributos comuns | Primitivo (int, double): mais rápido, nunca é null |
| Dentro de coleções e generics | Wrapper (obrigatório): ArrayList<Integer> |
| O valor pode "não existir" (nota ainda não lançada) | Wrapper, aceitando null (com cuidado), ou Optional |
| Converter texto | Métodos estáticos do wrapper: Integer.parseInt |
| Comparar dois wrappers | equals, nunca == |
Quando dá errado
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).
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.
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
| Item | Exemplo | Resultado |
|---|---|---|
| Autoboxing | Integer x = 29; | Integer.valueOf(29) |
| Unboxing | int y = x; | x.intValue() (NPE se x for null) |
Texto → int | Integer.parseInt("42") | 42 |
Texto → Integer | Integer.valueOf("42") | 42 (objeto) |
Texto → double | Double.parseDouble("9.5") | 9.5 |
| Número → texto | String.valueOf(42) | "42" |
| Limites | Integer.MAX_VALUE | 2147483647 |
| Cache | Integer.valueOf, de -128 a 127 | Mesma caixa: == dá true só nessa faixa |
| Comparar | a.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:
3. Lista de decimais
Complete o tipo da lista de preços:
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>, nuncaArrayList<int>. Integer.parseInt,Double.parseDoublee companhia convertem texto;String.valueOffaz o caminho de volta.==entre wrappers compara caixas, não valores; funciona de -128 a 127 só por causa do cache. Useequals.- Desembrulhar
nulllançaNullPointerException(Cannot invoke "java.lang.Integer.intValue()").