UD2 · Sección 8 — Wrappers y la clase Objects
| Dato | Valor |
|---|---|
| Unidad Didáctica | UD2 — Programación Orientada a Objetos con la API de Java |
| Resultado de Aprendizaje | RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos. |
| Sección | 8 de 10 — Wrappers y la clase Objects |
| Evaluación | 1.ª Evaluación |
| Duración estimada | 2 horas lectivas |
| Criterios de evaluación cubiertos | CE 2.2 (escribe programas simples), CE 2.3 (instancia objetos a partir de clases predefinidas), CE 2.4 (utiliza métodos y propiedades), CE 2.5 (llamadas a métodos estáticos), CE 2.6 (utiliza parámetros), CE 2.7 (incorpora y utiliza librerías de objetos) |
| Prerrequisitos | UD1 completa, UD2 Secciones 1 a 7 |
| Paquete del código de ejemplo | dam.programacion.ud2 |
| Herramientas | JDK 25, IntelliJ IDEA Community, Maven 3.9.x |
1. Punto de partida
Hasta ahora has trabajado con dos categorías de tipos en Java: los primitivos (int, double, boolean, char y sus variantes), que son eficientes pero no son objetos, y las clases (String, Random, LocalDate), que son objetos pero no son primitivos. En esta sección vas a conocer el puente entre ambos mundos: las clases wrapper (envoltorio), que permiten tratar los tipos primitivos como si fueran objetos cuando hace falta.
También vas a conocer la clase Objects, una caja de herramientas con métodos estáticos para trabajar con objetos de forma segura: comprobar si son null, compararlos, calcular su hash, etc. Es una clase que te acompañará durante toda tu carrera, especialmente cuando empieces a crear tus propias clases en UD4.
Es una sección corta pero conceptualmente importante: cierra el bloque de "clases predefinidas del API de Java" y prepara el terreno para los temas de Maven (Sección 9) y los ejercicios (Sección 10).
2. Wrappers: tipos primitivos como objetos
2.1. Qué son y por qué existen
Cada tipo primitivo de Java tiene una clase wrapper asociada que lo "envuelve" en un objeto:
| Primitivo | Wrapper | Paquete |
|---|---|---|
byte |
Byte |
java.lang |
short |
Short |
java.lang |
int |
Integer |
java.lang |
long |
Long |
java.lang |
float |
Float |
java.lang |
double |
Double |
java.lang |
boolean |
Boolean |
java.lang |
char |
Character |
java.lang |
Observa que Integer no se llama Int y Character no se llama Char: son las dos excepciones al patrón "capitalizar el nombre del primitivo".
¿Por qué existen? Porque Java es un lenguaje que distingue claramente primitivos de objetos. Hay situaciones donde solo se pueden usar objetos:
- Colecciones (las verás en UD6):
ArrayList<int>no compila, hay que usarArrayList<Integer>. - Genéricos (UD6/UD7): los tipos genéricos solo admiten objetos.
- Reflection y APIs avanzadas que trabajan con
Object. - Atributos nullable: un
intno puede sernull; unIntegersí. Útil para representar "valor ausente" (por ejemplo, un campo opcional de un formulario). - Constantes y utilidades:
Integer.MAX_VALUE,Integer.parseInt(String),Double.isNaN(double)...
Analogía del sobre. Imagina que un primitivo es una moneda suelta y un wrapper es la misma moneda dentro de un sobre. La moneda es más fácil de manejar y contar sin sobre, pero hay sitios (por ejemplo, un buzón de correo) donde solo aceptan sobres, no monedas sueltas. El wrapper es el sobre que te permite meter la moneda en sitios que solo aceptan objetos.
2.2. Crear wrappers
Hay tres formas de crear un wrapper (las veremos con Integer, pero aplica a todos):
Recomendación: usa valueOf o autoboxing; nunca new Integer(...). Desde Java 9 el constructor está marcado como deprecated (obsoleto) y puede eliminarse en versiones futuras.
2.3. Autoboxing y unboxing
Java convierte automáticamente entre primitivos y wrappers cuando hace falta:
- Autoboxing: primitivo → wrapper (lo hace el compilador).
- Unboxing: wrapper → primitivo.
El compilador inserta las llamadas a valueOf y a xxxValue() automáticamente. Es muy cómodo, pero conviene saber que está pasando por debajo.
2.4. Riesgos del autoboxing
Riesgo 1: NullPointerException al desempaquetar null
El compilador traduce int valor = n; como int valor = n.intValue();, y como n es null, intentar llamar a un método sobre null lanza NullPointerException. Es una trampa clásica.
Riesgo 2: confusión con ==
¿Qué pasa aquí? Java cachea los Integer entre -128 y 127 (lo que se llama Integer Cache). Cuando usas autoboxing con valores en ese rango, te devuelve la misma instancia; fuera del rango, crea objetos nuevos. Por eso a == b es true (misma referencia cacheada) pero c == d es false (referencias distintas).
Regla de oro: para comparar wrappers por contenido, usa siempre .equals(), nunca ==.
Riesgo 3: sobrecoste de rendimiento
Cada autoboxing crea un objeto (salvo en el rango cacheado). En bucles críticos, esto puede afectar al rendimiento:
La primera versión crea un millón de objetos Integer que el GC tiene que limpiar. La segunda no crea ninguno. Para bucles críticos, usa primitivos.
3. Métodos útiles de los wrappers
3.1. Constantes
Cada wrapper expone constantes con los límites del tipo:
Estas constantes son public static final (lo verás en UD4): están asociadas a la clase, no a instancias, y su valor no cambia.
3.2. Conversión desde String: parseXxx
Ya vimos Integer.parseInt en la Sección 2. Aquí tienes la tabla completa:
| Método | Qué devuelve | Ejemplo |
|---|---|---|
Integer.parseInt(String) |
int |
Integer.parseInt("42") → 42 |
Long.parseLong(String) |
long |
Long.parseLong("9999999999") |
Double.parseDouble(String) |
double |
Double.parseDouble("3.14") |
Float.parseFloat(String) |
float |
Float.parseFloat("3.14f") |
Boolean.parseBoolean(String) |
boolean |
Boolean.parseBoolean("true") |
Byte.parseByte(String) |
byte |
|
Short.parseShort(String) |
short |
|
Character.digit(char, int) |
int |
Conversión de un dígito en una base |
Si el String no se puede parsear (por ejemplo Integer.parseInt("abc")), se lanza NumberFormatException. Más adelante, en UD3, aprenderás a gestionarla con try-catch.
3.3. Conversión a String: toString
Integer.toHexString, toBinaryString y toOctalString son muy útiles para depurar y para trabajar a bajo nivel.
3.4. Conversión entre tipos numéricos
Los wrappers ofrecen métodos xxxValue() para convertir entre tipos:
Estos métodos son de instancia (necesitas un objeto) y devuelven el primitivo correspondiente.
3.5. Double y Float: valores especiales
Recuerda: NaN no es igual a sí mismo, así que nunca compares con == para detectarlo. Usa siempre Double.isNaN o Float.isNaN.
3.6. compareTo
Para comparar wrappers por orden:
Útil para ordenar colecciones de wrappers (UD6).
4. La clase Objects
Objects (en java.util) es una clase de utilidades con métodos estáticos para trabajar con objetos. No tiene relación con Object (la clase raíz de Java); el nombre es confuso pero es lo que hay.
4.1. Por qué existe
Trabajar con referencias que pueden ser null es una fuente constante de bugs en Java. Objects ofrece métodos para hacerlo de forma segura y expresiva.
4.2. Métodos principales
| Método | Qué hace |
|---|---|
Objects.isNull(Object) |
Devuelve true si el argumento es null. Equivalente a obj == null |
Objects.nonNull(Object) |
Devuelve true si el argumento no es null. Equivalente a obj != null |
Objects.requireNonNull(Object) |
Lanza NullPointerException si el argumento es null; si no, lo devuelve |
Objects.requireNonNull(Object, String mensaje) |
Igual, con mensaje personalizado |
Objects.requireNonNullElse(T, T defaultValue) |
Devuelve el argumento si no es null; si no, el valor por defecto |
Objects.equals(Object a, Object b) |
Compara dos objetos de forma segura ante null |
Objects.toString(Object) |
Devuelve "null" si el argumento es null; si no, llama a toString() |
Objects.toString(Object, String defaultValue) |
Igual, pero con valor por defecto personalizado |
Objects.hash(Object...) |
Calcula el hash de varios campos (lo verás en UD4) |
Objects.compare(Object a, Object b, Comparator) |
Compara dos objetos con un comparador |
4.3. requireNonNull: validar argumentos
Es probablemente el método más útil de la clase. Permite validar que un argumento no sea null al inicio de un método, fallando rápido si lo es:
Aquí he adelantado la sintaxis de una clase propia (constructor, atributo, this), que verás en UD4. Lo importante ahora es la idea: requireNonNull valida al inicio y falla pronto, en vez de esperar a que el null cause problemas más adelante.
Analogía del portero de discoteca. requireNonNull es como el portero de una discoteca: comprueba el DNI en la entrada y, si no es válido, no te deja pasar. Sin portero, entrarías, te sentarás, pedirías una bebida, y en algún momento alguien se daría cuenta del problema. Mejor fallar en la entrada.
4.4. equals seguro ante null
Objects.equals devuelve true si ambos son null, false si solo uno lo es, y a.equals(b) en caso contrario. Es la forma recomendada de comparar objetos cuando cualquiera de los dos puede ser null.
4.5. toString seguro ante null
Útil para imprimir objetos que pueden ser null sin que el programa reviente.
4.6. isNull y nonNull
Son métodos puramente cosméticos: hacen lo mismo que obj == null y obj != null, pero se ven más "expresivos" cuando se usan con streams o method references (UD6/UD7):
En código normal, s == null sigue siendo perfectamente válido.
5. Cuándo usar wrappers y cuándo primitivos
| Situación | Recomendación |
|---|---|
| Variables locales, parámetros, atributos normales | Primitivos (más rápidos y sin riesgo de NPE) |
Colecciones (List<Integer>, Map<String, Double>) |
Wrappers (las colecciones solo admiten objetos) |
| Campos opcionales que pueden ser "ausentes" | Wrappers (un int no puede ser null) |
Constantes como MAX_VALUE, parseInt |
Wrappers (son estáticos de la clase) |
| Bucles críticos de rendimiento | Primitivos (sin autoboxing) |
APIs genéricas que requieren Object |
Wrappers |
Regla práctica: usa primitivos por defecto. Solo usa wrappers cuando sea estrictamente necesario (colecciones, nulabilidad, genéricos).
flowchart TD
A["Necesito un numero"] --> B{"Es para una coleccion<br/>o generico?"}
B -->|Si| C["Wrapper (Integer, Double, ...)"]
B -->|No| D{"Necesito que pueda ser null<br/>(valor ausente)?"}
D -->|Si| C
D -->|No| E["Primitivo (int, double, ...)"]
C --> F{"Lo uso en un bucle critico?"}
F -->|Si| G["Cuidado con autoboxing<br/>mide el rendimiento"]
F -->|No| H["OK, usa wrapper"]
style C fill:#ef6c00,color:#fff,stroke:#e65100
style E fill:#2e7d32,color:#fff,stroke:#1b5e20
6. Ejemplo integrador
Programa que demuestra los conceptos de la sección: creación de wrappers, autoboxing/unboxing, constantes, parseo, conversión y métodos de Objects.
Salida esperada:
7. Errores comunes
| Error | Qué pasa | Cómo evitarlo |
|---|---|---|
Usar new Integer(42) |
Funciona pero está deprecated | Usa Integer.valueOf(42) o autoboxing |
Comparar wrappers con == |
Funciona a veces (cache) y falla otras | Usa siempre .equals() |
Hacer unboxing de null |
NullPointerException |
Comprueba null antes de asignar a un primitivo |
| Usar wrappers en bucles críticos | Sobrecoste por autoboxing | Usa primitivos en bucles críticos |
Comparar NaN con == |
Siempre devuelve false, incluso NaN == NaN |
Usa Double.isNaN(x) |
Confundir Objects con Object |
Object es la raíz; Objects es utilidades |
Recuerda: Objects siempre en plural |
Llamar a toString() sobre null |
NullPointerException |
Usa Objects.toString(obj) o Objects.toString(obj, default) |
Olvidar que Integer.parseInt("abc") lanza excepción |
El programa revienta | Usa try-catch (lo verás en UD3) |
| Usar wrappers "por defecto" | Memoria y rendimiento innecesarios | Usa primitivos salvo que necesites objetos |
8. Resumen
| Concepto | Idea clave |
|---|---|
| Wrapper | Clase que envuelve un primitivo: Integer, Double, Boolean, Character... |
| Autoboxing | Primitivo → wrapper automático |
| Unboxing | Wrapper → primitivo automático |
valueOf(x) |
Forma recomendada de crear wrappers (aprovecha cache) |
new Integer(x) |
Deprecated desde Java 9; evitar |
Integer.MAX_VALUE / MIN_VALUE |
Constantes de los límites del tipo |
parseXxx(String) |
Conversión desde String a primitivo |
Integer.toHexString / toBinaryString / toOctalString |
Conversión a otras bases |
xxxValue() |
Conversión entre tipos numéricos |
Double.isNaN(x) |
Detectar NaN (no usar == NaN) |
| Integer Cache | Integer entre -128 y 127 se cachea; afecta a == |
Objects.requireNonNull(obj, msg) |
Validar que un argumento no es null |
Objects.equals(a, b) |
Comparación segura ante null |
Objects.toString(obj, default) |
toString seguro ante null |
Objects.isNull / Objects.nonNull |
Versiones expresivas de == null / != null |
| Regla práctica | Primitivos por defecto; wrappers solo cuando sea necesario |
9. Cuestionario de autoevaluación
Preguntas
-
¿Qué es una clase wrapper y por qué existen en Java?
-
Nombra los wrappers de los 8 tipos primitivos. ¿Cuáles NO siguen el patrón de "capitalizar el nombre del primitivo"?
-
¿Cuál es la diferencia entre
Integer.valueOf(42),new Integer(42)y la asignación directaInteger i = 42? ¿Cuál es la forma recomendada? -
¿Qué es el autoboxing y el unboxing? Pon un ejemplo de cada uno.
-
¿Por qué el siguiente código imprime
false? -
¿Por qué
c.equals(d)en el código anterior sí devuelvetrue? -
¿Qué valor devuelve
Integer.parseInt("abc")? ¿Qué ocurre? -
Escribe una línea que convierta el
String"3.14"a undouble. -
¿Por qué
Double.NaN == Double.NaNdevuelvefalse? ¿Cómo se comprueba entonces si un valor esNaN? -
¿Qué hace
Objects.requireNonNull(obj, "mensaje")y cuándo se suele usar? -
¿Qué devuelven
Objects.toString(null)yObjects.toString(null, "por defecto")? -
¿Por qué el siguiente código lanza
NullPointerException? -
¿Qué clase usarías para representar la edad de un cliente en un campo opcional de un formulario que puede estar vacío:
intoInteger? ¿Por qué? -
¿Qué método de
Integerdevuelve la representación binaria de un número? ¿Y la hexadecimal? -
Di si cada uno de estos métodos es estático o de instancia:
Integer.parseInt,Integer.valueOf,Integer.MAX_VALUE,i.intValue(),Objects.requireNonNull.
Soluciones
-
Un wrapper es una clase que envuelve un valor primitivo en un objeto. Existen porque Java distingue claramente primitivos de objetos, y hay situaciones donde solo se pueden usar objetos: colecciones, genéricos, APIs que requieren
Object, campos que pueden sernull(valor ausente) y para exponer constantes y utilidades estáticas. -
Byte,Short,Integer,Long,Float,Double,Boolean,Character. Los que no siguen el patrón sonInteger(deint) yCharacter(dechar). -
Las tres formas crean un
Integercon valor 42, pero: Integer.valueOf(42): recomendado. Aprovecha la cache de Integer para valores entre -128 y 127.new Integer(42): crea siempre un objeto nuevo. Deprecated desde Java 9.Integer i = 42: autoboxing; el compilador lo traduce aInteger.valueOf(42). Recomendado por concisión.
La forma recomendada es valueOf o autoboxing.
-
Autoboxing: conversión automática de primitivo a wrapper, hecha por el compilador. Ejemplo:
Integer i = 5;(el compilador insertaInteger.valueOf(5)). Unboxing: conversión automática de wrapper a primitivo. Ejemplo:int n = i;(el compilador insertai.intValue()). -
Porque
Integercachea los valores entre -128 y 127. Para valores fuera de ese rango (como 200), el autoboxing crea objetos nuevos, con referencias distintas.==compara referencias, no contenidos, así que devuelvefalse. -
Porque
equalscompara por contenido, no por referencia. Para valores dentro del rango cacheado,==también funcionaría por accidente, peroequalses siempre fiable. -
No devuelve nada: lanza
NumberFormatExceptionen tiempo de ejecución, porque"abc"no es un número entero válido. Más adelante, en UD3, verás cómo gestionarla contry-catch. -
double d = Double.parseDouble("3.14"); -
Porque el estándar IEEE 754 define que
NaNno es igual a sí mismo (es una propiedad matemática del "no es un número"). Para comprobarlo, usaDouble.isNaN(x)oFloat.isNaN(x). -
Lanza
NullPointerExceptionsiobjesnull; si no, devuelveobj. Se usa al inicio de métodos y constructores para validar argumentos y fallar pronto con un mensaje claro, en vez de esperar a que elnullcause un error misterioso más adelante. -
Objects.toString(null)devuelve la cadena"null".Objects.toString(null, "por defecto")devuelve"por defecto". Ninguno de los dos lanza excepción. -
Porque el compilador traduce
int valor = n;comoint valor = n.intValue();, y comonesnull, intentar llamar a un método sobrenulllanzaNullPointerException. Es una trampa clásica del unboxing. -
Integer, porque un primitivointno puede sernull. Si el campo es opcional, necesitas distinguir "edad no introducida" (null) de "edad 0" (que sería un valor válido). ConInteger, puedes representar ambos casos. -
Binaria:
Integer.toBinaryString(int). Hexadecimal:Integer.toHexString(int). (También existeInteger.toOctalStringpara octal.) -
Todos estáticos excepto
i.intValue(), que es de instancia (necesita un objetoIntegerconcreto sobre el que operar).Integer.MAX_VALUEes una constantestatic final, también asociada a la clase.
Evaluación del cuestionario
| Aciertos | Nivel |
|---|---|
| 14-15 | Dominas la sección. Adelante con la siguiente. |
| 10-13 | Bien, repasa los puntos fallados. |
| 6-9 | Repasa la sección, especialmente autoboxing y la trampa de == con wrappers. |
| Menos de 6 | Vuelve a leer con calma y ejecuta el ejemplo integrador paso a paso en IntelliJ. |
10. Información ampliada y próximos pasos
- Javadoc oficial de
Integer(Java 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html - Javadoc oficial de
Objects: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Objects.html - Artículo "Why does Java need Wrapper Classes?" (Baeldung): https://www.baeldung.com/java-wrapper-classes
- Artículo "Autoboxing and Unboxing in Java" (Baeldung): https://www.baeldung.com/java-autoboxing-unboxing
Próximos pasos:
- Practica en IntelliJ. Copia el ejemplo integrador y modifícalo: prueba a descomentar la línea que lanza
NullPointerExceptionconrequireNonNull, observa el mensaje, prueba a comparar distintos valores deIntegercon==y con.equals(). - Experimenta con
NaNyInfinity. Calcula0.0 / 0.0,1.0 / 0.0,-1.0 / 0.0,Math.sqrt(-1). Comprueba conDouble.isNaNeDouble.isInfinite. - Avanza a la Sección 9 de UD2. Maven: gestión de dependencias externas. El repositorio Central de Maven, cómo añadir una dependencia al
pom.xml, primer contacto con dependencias externas usando JUnit 5, y el ciclo de vida de Maven (compile,test,package,install). Es la sección que abre la puerta al ecosistema de librerías Java más allá del JDK.