java - son - ¿Por qué necesitamos una clase inmutable?
principios inmutables (18)
No puedo obtener cuáles son los escenarios donde necesitamos una clase inmutable.
¿Alguna vez has enfrentado alguno de esos requisitos? o puede darnos algún ejemplo real donde deberíamos usar este patrón.
¿Por qué clase inmutable?
Una vez que se crea una instancia de un objeto, no se puede cambiar el estado en la vida. Lo cual también lo hace seguro para hilos.
Ejemplos:
Obviamente String, Integer y BigDecimal, etc. Una vez que se crean estos valores, no se pueden cambiar en la vida.
Caso de uso: una vez que se crea el objeto de conexión de la base de datos con sus valores de configuración, es posible que no necesite cambiar su estado donde puede usar una clase inmutable
Hay varias razones para la inmutabilidad:
- Seguridad del subproceso: los objetos inmutables no se pueden cambiar ni puede cambiar su estado interno, por lo que no es necesario sincronizarlo.
- También garantiza que todo lo que envíe (a través de una red) debe estar en el mismo estado que se envió anteriormente. Significa que nadie (fisgón) puede venir y agregar datos aleatorios en mi conjunto inmutable.
- También es más fácil de desarrollar. Usted garantiza que no existirán subclases si un objeto es inmutable. Por ejemplo, una clase
String
.
Por lo tanto, si desea enviar datos a través de un servicio de red y desea tener la garantía de que su resultado será exactamente igual al que envió, configúrelo como inmutable.
Java efectiva por Joshua Bloch describe varias razones para escribir clases inmutables:
- Simplicidad: cada clase está en un solo estado
- Thread Safe - porque el estado no se puede cambiar, no se requiere sincronización
- Escribir en un estilo inmutable puede conducir a un código más robusto. Imagínese si las cadenas no fueran inmutables; Cualquier método getter que devuelva un String requeriría que la implementación creara una copia defensiva antes de que se devolviera el String; de lo contrario, un cliente puede romper accidental o maliciosamente ese estado del objeto.
En general, es una buena práctica hacer un objeto inmutable a menos que haya graves problemas de rendimiento como resultado. En tales circunstancias, los objetos constructores mutables se pueden usar para construir objetos inmutables, por ejemplo, StringBuilder
Java es prácticamente una y todas las referencias. Algunas veces se hace referencia a una instancia varias veces. Si cambia esa instancia, se reflejará en todas sus referencias. A veces simplemente no desea tener esto para mejorar la solidez y la seguridad de los hilos. Entonces, una clase inmutable es útil, por lo que uno se ve obligado a crear una nueva instancia y reasignarla a la referencia actual. De esta manera, la instancia original de las otras referencias permanece intacta.
Imagine cómo se vería Java si String
fuera mutable.
Las clases inmutables son en general mucho más simples de diseñar, implementar y usar correctamente . Un ejemplo es String: la implementación de java.lang.String
es significativamente más simple que la de std::string
en C ++, principalmente debido a su inmutabilidad.
Un área particular donde la inmutabilidad marca una gran diferencia es la concurrencia: los objetos inmutables pueden compartirse de forma segura entre varios hilos , mientras que los objetos mutables deben ser seguros para hilos a través de un diseño e implementación cuidadosos; por lo general, esta tarea no es trivial.
Actualización: Effective Java 2nd Edition aborda este tema en detalle - ver Ítem 15: Minimizar la mutabilidad .
Ver también estas publicaciones relacionadas:
Las estructuras de datos inmutables también pueden ayudar al codificar algoritmos recursivos. Por ejemplo, di que estás tratando de resolver un problema 3SAT . Una forma es hacer lo siguiente:
- Elija una variable no asignada.
- Dale el valor de VERDADERO. Simplifique la instancia eliminando las cláusulas que ahora están satisfechas y repita para resolver la instancia más simple.
- Si la recursión en el caso VERDADERO falló, entonces asigne esa variable FALSO en su lugar. Simplifique esta nueva instancia y repita para resolverla.
Si tiene una estructura mutable para representar el problema, cuando simplifique la instancia en la rama TRUE, deberá:
- Mantenga un registro de todos los cambios que realice y deshaga todos una vez que se dé cuenta de que el problema no se puede resolver. Esto tiene una gran sobrecarga porque su recursión puede ser bastante profunda, y es difícil de codificar.
- Haga una copia de la instancia y luego modifique la copia. Esto será lento porque si su recursión es de algunas docenas de niveles profundos, tendrá que hacer muchas copias de la instancia.
Sin embargo, si lo codifica de una manera inteligente, puede tener una estructura inmutable, donde cualquier operación devuelve una versión actualizada (pero aún inmutable) del problema (similar a String.replace
- no reemplaza la cadena, simplemente le da uno nuevo). La forma ingenua de implementar esto es hacer que la estructura "inmutable" simplemente copie y cree una nueva en cualquier modificación, reduciéndola a la segunda solución cuando tenga una mutable, con toda esa sobrecarga, pero puede hacerlo de una manera más forma eficiente.
Las otras respuestas parecen enfocarse en explicar por qué la inmutabilidad es buena. Es muy bueno y lo uso siempre que sea posible. Sin embargo, esa no es tu pregunta . Tomaré su pregunta punto por punto para tratar de asegurarme de que está obteniendo las respuestas y los ejemplos que necesita.
No puedo obtener cuáles son los escenarios donde necesitamos una clase inmutable.
"Necesidad" es un término relativo aquí. Las clases inmutables son un patrón de diseño que, como cualquier paradigma / patrón / herramienta, está ahí para facilitar la construcción de software. De manera similar, se escribió mucho código antes de que apareciera el paradigma OO, pero me cuente entre los programadores que "necesitan" OO. Las clases inmutables, como OO, no son estrictamente necesarias , pero voy a actuar como si las necesitara.
¿Alguna vez has enfrentado alguno de esos requisitos?
Si no está mirando los objetos en el dominio del problema con la perspectiva correcta, es posible que no vea un requisito para un objeto inmutable. Puede ser fácil pensar que un dominio problemático no requiere clases inmutables si no está familiarizado con cuándo usarlas de manera ventajosa.
A menudo uso clases inmutables cuando pienso en un objeto dado en mi dominio de problema como un valor o instancia fija . Esta noción a veces depende de la perspectiva o punto de vista, pero idealmente, será fácil pasar a la perspectiva correcta para identificar buenos objetos candidatos.
Puede obtener una mejor idea de dónde los objetos inmutables son realmente útiles (si no es estrictamente necesario) asegurándose de leer en varios libros / artículos en línea para desarrollar un buen sentido de cómo pensar acerca de las clases inmutables. Un buen artículo para comenzar es la teoría y la práctica de Java: ¿mutar o no mutar?
Trataré de dar un par de ejemplos debajo de cómo se pueden ver los objetos en diferentes perspectivas (mutable vs inmutable) para aclarar lo que quiero decir con perspectiva.
... ¿pueden darnos algún ejemplo real donde deberíamos usar este patrón?
Como usted pidió ejemplos reales, le daré algunos, pero primero comencemos con algunos ejemplos clásicos.
Objetos de valor clásico
Cadenas y enteros y a menudo se consideran valores. Por lo tanto, no es sorprendente encontrar que la clase String y la clase contenedora Integer (así como las otras clases contenedoras) son inmutables en Java. Por lo general, se considera que un color es un valor, por lo tanto, es una clase de color inmutable.
Contraejemplo
Por el contrario, un automóvil no suele considerarse como un objeto de valor. Modelar un automóvil generalmente significa crear una clase que tiene un estado cambiante (odómetro, velocidad, nivel de combustible, etc.). Sin embargo, hay algunos dominios donde el auto puede ser un objeto de valor. Por ejemplo, un automóvil (o específicamente un modelo de automóvil) podría considerarse como un objeto de valor en una aplicación para buscar el aceite de motor adecuado para un vehículo dado.
Jugando a las cartas
¿Alguna vez escribiste un programa de naipes? Yo si. Podría haber representado una carta de juego como un objeto mutable con un traje mutable y rango. Una mano de draw-poker podría ser 5 instancias fijas donde reemplazar la 5ta carta en mi mano significaría mutar la 5ta instancia de naipes en una nueva carta cambiando su ivars de palo y rango.
Sin embargo, tiendo a pensar en una carta de juego como un objeto inmutable que tiene un traje y un rango fijos inalterables una vez creados. Mi mano de Draw Draw sería 5 instancias y reemplazar una carta en mi mano implicaría descartar una de esas instancias y agregar una nueva instancia aleatoria a mi mano.
Proyección del mapa
Un último ejemplo es cuando trabajé en algún código de mapa donde el mapa podría mostrarse en varias projections . El código original tenía el mapa usar una instancia de proyección fija, pero mutable (como la tarjeta de juego mutable anterior). Cambiar la proyección del mapa significaba mutar los ivars de la instancia de proyección del mapa (tipo de proyección, punto central, zoom, etc.).
Sin embargo, sentí que el diseño era más simple si pensaba en una proyección como un valor inmutable o una instancia fija. Cambiar la proyección del mapa significaba hacer que el mapa hiciera referencia a una instancia de proyección diferente en lugar de mutar la instancia de proyección fija del mapa. Esto también simplificó la captura de proyecciones con nombre, como MERCATOR_WORLD_VIEW
.
Los Hashmaps son un ejemplo clásico. Es imperativo que la clave de un mapa sea inmutable. Si la clave no es inmutable, y cambia un valor en la clave de modo que hashCode () daría como resultado un nuevo valor, el mapa ahora está roto (una clave ahora está en la ubicación incorrecta en la tabla hash).
Los objetos inmutables son instancias cuyos estados no cambian una vez iniciados. El uso de tales objetos es específico del requisito.
La clase inmutable es buena para el almacenamiento en caché y es segura para subprocesos.
No necesitamos clases inmutables, per se, pero ciertamente pueden facilitar algunas tareas de programación, especialmente cuando se trata de varios hilos. No tiene que realizar ningún bloqueo para acceder a un objeto inmutable, y cualquier hecho que ya haya establecido sobre dicho objeto continuará siendo cierto en el futuro.
Por inmutabilidad puede estar seguro de que el comportamiento no cambia, con eso se obtiene la ventaja añadida de realizar operaciones adicionales:
Puede usar múltiples núcleos / procesamiento (procesamiento concurrente ) con facilidad (ya que la secuencia ya no importará).
Puede hacer el almacenamiento en caché para una operación costosa (ya que está seguro de lo mismo
resultado).Puede hacer la depuración a gusto (ya que el historial de ejecución no será una preocupación
nunca más)
También nos dan una garantía. La garantía de inmutabilidad significa que podemos ampliarlos y crear nuevos patrones de eficiencia que de otro modo no serían posibles.
Tomemos un caso extremo: constantes enteras. Si escribo una afirmación como "x = x + 1", quiero ser 100% confidente de que el número "1" de alguna manera no se convertirá en 2, sin importar lo que ocurra en ningún otro lugar en el programa.
Ahora bien, las constantes enteras no son una clase, pero el concepto es el mismo. Supongamos que escribo:
String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);
Parece lo suficientemente simple. Pero si Strings no fuera inmutable, entonces tendría que considerar la posibilidad de que getCustomerName pudiera cambiar customerId, de modo que cuando llamo a getCustomerBalance, obtengo el saldo para un cliente diferente. Ahora podría decir: "¿Por qué en el mundo alguien que escribe una función getCustomerName hace que cambie la identificación? Eso no tendría sentido". Pero eso es exactamente donde podrías tener problemas. La persona que escribe el código anterior podría considerar simplemente obvio que las funciones no cambiarían el parámetro. Luego viene alguien que tiene que modificar otro uso de esa función para manejar el caso donde un cliente tiene varias cuentas con el mismo nombre. Y dice: "Ah, aquí está esta práctica función de obtener el nombre del cliente que ya está buscando el nombre. Haré que eso cambie automáticamente el ID a la siguiente cuenta con el mismo nombre, y lo pondré en un bucle ..." Y entonces su programa comienza misteriosamente no funciona. ¿Sería ese un mal estilo de codificación? Probablemente. Pero es precisamente un problema en los casos en que el efecto secundario NO es obvio.
La inmutabilidad simplemente significa que una cierta clase de objetos son constantes, y podemos tratarlos como constantes.
(Por supuesto, el usuario podría asignar un "objeto constante" diferente a una variable. Alguien puede escribir String s = "hola"; y luego escribir s = "adiós"; a menos que haga que la variable sea definitiva, no puedo estar seguro que no está siendo modificado dentro de mi propio bloque de código. Al igual que las constantes enteras, me aseguran que "1" es siempre el mismo número, pero no que "x = 1" nunca se modificará escribiendo "x = 2". Pero yo Puede ser de confianza que si tengo un control para un objeto inmutable, que ninguna función a la que lo apruebe pueda cambiarlo en mí, o que si hago dos copias de él, que un cambio en la variable que contiene una copia no cambiará el otro. Etc.
Una característica de las clases inmutables que aún no se ha invocado: almacenar una referencia a un objeto de clase profundamente inmutable es un medio eficiente de almacenar todo el estado contenido en él. Supongamos que tengo un objeto mutable que usa un objeto profundamente inmutable para contener 50K de información de estado. Supongamos, además, que deseo hacer en 25 ocasiones una "copia" de mi objeto original (mutable) (por ejemplo, para un buffer "deshacer"); el estado podría cambiar entre operaciones de copia, pero generalmente no. Hacer una "copia" del objeto mutable simplemente requeriría copiar una referencia a su estado inmutable, por lo que 20 copias simplemente equivaldrían a 20 referencias. Por el contrario, si el estado se mantuviera en un valor de 50 K de objetos mutables, cada una de las 25 operaciones de copia tendría que producir su propia copia de 50 K de datos; tener todas las 25 copias requeriría retener más de un mego de datos en su mayoría duplicados. Aunque la primera operación de copia produciría una copia de los datos que nunca cambiará, y las otras 24 operaciones podrían, en teoría, simplemente referirse a eso, en la mayoría de las implementaciones no habría forma de que el segundo objeto pidiera una copia del información para saber que ya existe una copia inmutable (*).
(*) Un patrón que a veces puede ser útil es que los objetos mutables tengan dos campos para mantener su estado: uno en forma mutable y otro en forma inmutable. Los objetos se pueden copiar como mutables o inmutables, y comenzarían a funcionar con uno u otro conjunto de referencia. Tan pronto como el objeto desea cambiar su estado, copia la referencia inmutable a la mutable (si no se ha hecho ya) e invalida la inmutable. Cuando el objeto se copia como inmutable, si no se establece su referencia inmutable, se creará una copia inmutable y la referencia inmutable lo señalará. Este enfoque requerirá unas cuantas operaciones de copia más que una "copia completa en escritura" (por ejemplo, pidiendo copiar un objeto que ha sido mutado ya que la última copia requeriría una operación de copia, incluso si el objeto original nunca más ha sido mutado). ) pero evita las complejidades de enhebrado que conlleva FFCOW.
Una de las razones de la "necesidad" de clases inmutables es la combinación de pasar todo por referencia y no tener soporte para vistas de solo lectura de un objeto (es decir, const
C ++).
Considere el caso simple de una clase que tiene soporte para el patrón del observador:
class Person {
public string getName() { ... }
public void registerForNameChange(NameChangedObserver o) { ... }
}
Si la string
no fuera inmutable, sería imposible para la clase Person
implementar registerForNameChange()
correctamente, porque alguien podría escribir lo siguiente, modificando efectivamente el nombre de la persona sin activar ninguna notificación.
void foo(Person p) {
p.getName().prepend("Mr. ");
}
En C ++, getName()
devuelve una const std::string&
tiene el efecto de regresar por referencia e impedir el acceso a los mutadores, lo que significa que las clases inmutables no son necesarias en ese contexto.
Usar la palabra clave final no necesariamente hace que algo sea inmutable:
public class Scratchpad {
public static void main(String[] args) throws Exception {
SomeData sd = new SomeData("foo");
System.out.println(sd.data); //prints "foo"
voodoo(sd, "data", "bar");
System.out.println(sd.data); //prints "bar"
}
private static void voodoo(Object obj, String fieldName, Object value) throws Exception {
Field f = SomeData.class.getDeclaredField("data");
f.setAccessible(true);
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(obj, "bar");
}
}
class SomeData {
final String data;
SomeData(String data) {
this.data = data;
}
}
Solo un ejemplo para demostrar que la palabra clave "final" está ahí para evitar el error del programador, y no mucho más. Mientras que reasignar un valor que carece de una palabra clave final puede suceder fácilmente por accidente, llegar a esta longitud para cambiar un valor tendría que hacerse intencionalmente. Está ahí para la documentación y para evitar el error del programador.
Voy a atacar esto desde una perspectiva diferente. Encuentro que los objetos inmutables me hacen la vida más fácil al leer el código.
Si tengo un objeto mutable, nunca estoy seguro de cuál es su valor si alguna vez se usa fuera de mi alcance inmediato. Digamos que creo MyMutableObject
en las variables locales de un método, lo lleno con valores y luego lo paso a otros cinco métodos. CUALQUIERA de esos métodos puede cambiar el estado de mi objeto, por lo que debe ocurrir una de estas dos cosas:
- Tengo que hacer un seguimiento de los cuerpos de cinco métodos adicionales mientras pienso en la lógica de mi código.
- Tengo que hacer cinco copias defensivas inútiles de mi objeto para asegurarme de que se pasen los valores correctos a cada método.
El primero hace que el razonamiento sobre mi código sea difícil. El segundo hace que mi código gane rendimiento: básicamente estoy imitando un objeto inmutable con semántica de copia en escritura de todos modos, pero lo hago todo el tiempo, ya sea que los métodos llamados realmente modifiquen el estado de mi objeto.
Si, en cambio, utilizo MyImmutableObject
, puedo estar seguro de que lo que establezco es lo que serán los valores durante la vida de mi método. No hay una "acción espeluznante a distancia" que lo cambie por debajo de mí y no hay necesidad de que haga copias defensivas de mi objeto antes de invocar los otros cinco métodos. Si los otros métodos quieren cambiar las cosas para sus propósitos , tienen que hacer la copia, pero solo lo hacen si realmente tienen que hacer una copia (en lugar de hacerlo antes de todas y cada una de las llamadas al método externo). Me reservo los recursos mentales para hacer un seguimiento de los métodos que ni siquiera están en mi archivo fuente actual, y evito al sistema la sobrecarga de hacer innecesariamente copias defensivas por las dudas.
(Si salgo del mundo Java y, por ejemplo, en el mundo C ++, entre otros, puedo hacerme aún más complicado. Puedo hacer que los objetos aparezcan como si fueran mutables, pero detrás de las escenas los clonan transparentemente en cualquier tipo de cambio de estado, eso es copiar-escribir-sin que nadie sea más sabio.)
de Effective Java; Una clase inmutable es simplemente una clase cuyas instancias no se pueden modificar. Toda la información contenida en cada instancia se proporciona cuando se crea y se corrige durante el tiempo de vida del objeto. Las bibliotecas de la plataforma Java contienen muchas clases inmutables, incluidas String, las clases primitivas en recuadro y BigInteger y BigDecimal. Hay muchas buenas razones para esto: las clases inmutables son más fáciles de diseñar, implementar y usar que las clases mutables. Son menos propensos a errores y son más seguros.