¿Cuándo usar primitive vs class en Java?

54

Veo que Java tiene Boolean (clase) vs boolean (primitivo). Del mismo modo, hay un entero (clase) vs int (primitivo). ¿Cuál es la mejor práctica sobre cuándo usar la versión primitiva frente a la clase? ¿Debería usar siempre la versión de clase, a menos que tenga una razón específica (¿rendimiento?) ¿Cuál es la forma más común y aceptada de usar cada uno?

Casey Patton
fuente
En mi opinión, esas no son clases, son cajas. Solo están allí para que pueda usar primitivas donde se requieren objetos, es decir, en colecciones. No puede agregar dos enteros (puede fingir, pero en realidad Java es auto boxing | unboxing los valores para usted).
stonemetal

Respuestas:

47

En el ítem 5, de Java efectivo, Joshua Bloch dice

La lección es clara: prefiera las primitivas a las primitivas en caja y tenga cuidado con el autoboxing no intencional .

Un buen uso para las clases es usarlas como tipos genéricos (incluidas las clases de Colección, como listas y mapas) o cuando desea transformarlas a otro tipo sin conversión implícita (por ejemplo, la Integerclase tiene métodos doubleValue()o byteValue().

Editar: La razón de Joshua Bloch es:

// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
         sum += i;
    }
    System.out.println(sum);
}

Este programa obtiene la respuesta correcta, pero es mucho más lento de lo debido debido a un error tipográfico de un carácter. La variable sumse declara como a en Longlugar de a long, lo que significa que el programa construye alrededor de 2 ^ 31 Longinstancias innecesarias (aproximadamente una por cada vez que long ise agrega Long sum). Cambiar la declaración de suma de Longa longreduce el tiempo de ejecución de 43 segundos a 6,8 segundos en mi máquina.

m3th0dman
fuente
2
¡Sería útil si mencionaras las razones de Bloch, en lugar de solo citar su conclusión!
vaughandroid
@Baqueta He editado la publicación. La razón es el rendimiento.
m3th0dman
Eso está un poco más claro, gracias. Me siento justificado al publicar mi propia respuesta ahora. :)
vaughandroid
Puede que encuentre interesante la arquitectura lmax : "Se necesitó un poco más de inteligencia para subir otro orden de magnitud. Hay varias cosas que el equipo de LMAX encontró útiles para llegar allí. Una fue escribir implementaciones personalizadas de las colecciones de Java que fueron diseñadas ser amigable con la memoria caché y tener cuidado con la basura. Un ejemplo de esto es usar primitivos java largos como claves de hashmap con una implementación de mapa respaldada por matriz especialmente escrita ".
Miradas Es como si algo JIT deben manejar
DeFreitas
28

La práctica estándar es ir con los primitivos, a menos que se trate de genéricos (¡asegúrese de conocer el autoboxing y unboxing !).

Hay varias buenas razones para seguir la convención:

1. Evitas errores simples:

Hay algunos casos sutiles, no intuitivos, que a menudo atrapan a los principiantes. Incluso los codificadores experimentados se equivocan y cometen estos errores a veces (¡espero que esto sea seguido de juramentos cuando depuran el código y encuentran el error!).

El error más común es usar en a == blugar de a.equals(b). La gente está acostumbrada a las a == bprimitivas, por lo que se hace fácilmente cuando se usan los contenedores de objetos.

Integer a = new Integer(2);
Integer b = new Integer(2);
if (a == b) { // Should be a.equals(b)
    // This never gets executed.
}
Integer c = Integer.valueOf(2);
Integer d = Integer.valueOf(2);
if (c == d) { // Should be a.equals(b), but happens to work with these particular values!
    // This will get executed
}
Integer e = 1000;
Integer f = 1000;
if (e == f) { // Should be a.equals(b)
    // Whether this gets executed depends on which compiler you use!
}

2. Legibilidad:

Considere los siguientes dos ejemplos. La mayoría de la gente diría que el segundo es más legible.

Integer a = 2;
Integer b = 2;
if (!a.equals(b)) {
    // ...
}
int c = 2;
int d = 2;
if (c != d) {
    // ...
}

3. Rendimiento:

El hecho es que es más lento usar los contenedores de objetos para primitivas que solo usar las primitivas. Está agregando el costo de la creación de instancias de objetos, llamadas a métodos, etc. a cosas que usa en todo el lugar .

La cita de Knuth "... digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal", en realidad no se aplica aquí. Estaba hablando de optimizaciones que hacen que el código (o sistema) sea más complicado: si está de acuerdo con el punto 2, ¡esta es una optimización que hace que el código sea menos complicado!

4. Es la convención:

Si realiza diferentes elecciones estilísticas para el 99% de los otros programadores de Java, hay 2 desventajas:

  • Encontrará que el código de otras personas es más difícil de leer. El 99% de los ejemplos / tutoriales / etc. por ahí usarán primitivas. Cada vez que lea uno, tendrá la sobrecarga cognitiva adicional de pensar en cómo se vería en el estilo al que está acostumbrado.
  • Otras personas encontrarán su código más difícil de leer. Siempre que haga preguntas sobre Stack Overflow, tendrá que examinar las respuestas / comentarios preguntando "¿por qué no está usando primitivas?". Si no me crees, solo mira las batallas que la gente tiene sobre cosas como la colocación de corchetes, ¡que ni siquiera afecta el código generado!

Normalmente, enumeraría algunos contrapuntos, pero honestamente no puedo pensar en ninguna buena razón para no ir con la convención aquí.

vaughandroid
fuente
2
No sugiera a las personas con las que comparar objetos ==. Los objetos deben compararse con equals().
Tulains Córdova
2
@ user61852 ¡No estaba sugiriendo eso como algo que hacer, sino como un error común que se comete! ¿Debo aclarar eso?
vaughandroid
Sí, no mencionas que los objetos deberían compararse con equals()... les das una solución para que comparar objetos con ==los resultados esperados.
Tulains Córdova
Buen punto. Lo cambié
vaughandroid
Agregué equals()el segundo fragmento de código y cambié mi voto.
Tulains Córdova
12

Normalmente voy con los primitivos. Sin embargo, una peculiaridad de usar clases como Integery Booleanes la posibilidad de asignar nulla esas variables. Por supuesto, esto significa que tiene que hacer nullverificaciones todo el tiempo, pero aún mejor obtener una NullPointerException que tener errores lógicos debido al uso de alguna into booleanvariable que no se ha inicializado correctamente.

Por supuesto, desde Java 8 puede (y probablemente debería) ir un paso más allá y, en lugar de, por ejemplo Integer, podría usar Optional<Integer>variables que podrían o no tener un valor.

Además, presenta la posibilidad de utilizar nullpara asignar a esas variables un valor " desconocido " o " comodín ". Esto puede ser útil en algunas situaciones, por ejemplo, en Ternary Logic . O puede que desee comprobar si un determinado objeto coincide con alguna plantilla; en este caso, puede usar nullpara aquellas variables en la plantilla que pueden tener cualquier valor en el objeto.

tobias_k
fuente
2
(No fue mi voto negativo, pero ...) Java ya detecta variables no inicializadas, y no le permitirá leer una variable hasta que cada ruta de código que conduzca a su uso haya asignado definitivamente un valor. Por lo tanto, no gana mucho con la capacidad de asignar nullpor defecto. Por el contrario: sería mejor no "inicializar" la variable. Establecer cualquier valor predeterminado, incluso null, cierra el compilador ... pero también evita que detecte la falta de asignación útil en todas las rutas de código. Por lo tanto, un error que el compilador podría haber detectado, pasa al tiempo de ejecución.
cHao
@cHao Pero, ¿qué pasa si no hay un valor predeterminado sensible para inicializar la variable? Puede establecerlo en 0.0, o -1, o Integer.MAX_VALUE, o False, pero al final no sabe si ese es el valor predeterminado o un valor real que se asignó a esa variable. En casos donde esto es importante, tener un nullvalor allí podría ser más claro.
tobias_k
No es más claro, es más fácil decirle al compilador que no le advierta sobre una lógica poco clara hasta que un error ya se haya propagado. : P En los casos en que no haya un valor predeterminado razonable, no inicialice la variable. Solo configúrelo una vez que tenga un valor razonable para poner allí. Esto permite que Java lo detenga en el momento de la compilación si su valor puede no estar configurado correctamente.
cHao
@cHao Quise decir, puede haber casos en los que no puedas inicializar la variable y tengas que lidiar con ella en tiempo de ejecución. En tales casos, un "valor predeterminado" como "nulo", que puede identificarse claramente como predeterminado, o "no inicializado", podría ser mejor que cualquier inicialización en tiempo de compilación que también podría ser un valor válido.
tobias_k
¿Tienes ese caso en mente? Los casos en los que puedo pensar están en los límites de la interfaz (por ejemplo, como parámetros o tipos de retorno) ... pero incluso allí, serían mucho mejores si se envuelven. (Los desnudos nullvienen con una gran cantidad de problemas, incluida la paranoia nula). Dentro de una función, una variable que en realidad no se ha inicializado en el punto de uso generalmente indica casos no cubiertos. (El análisis de asignación definida es simplista, por lo que son posibles los falsos positivos. Pero a menudo puede resolverlos simplificando la lógica, por lo tanto.)
cHao
2

En palabras simples:

Utiliza los contenedores cuando necesitas agregar cosas a las colecciones.

Las colecciones no pueden contener primitivas.

Tulains Córdova
fuente
0

Java presenta autoboxing como señaló m3th0dman. Piense en el nivel más bajo posible y verá que el autoboxing (dentro o fuera) de un valor primitivo implicará ciclos de reloj gastados en algunas tareas que no necesita si está trabajando con tipos de datos nativos alrededor de su aplicación.

Como regla general, debe intentar utilizar tipos de datos nativos siempre que sea posible.

Silvarion
fuente