design - valores - objetos mutables e inmutables java
Mutable vs objetos inmutables (10)
Estoy tratando de mover mi cabeza alrededor de objetos mutables vs inmutables. El uso de objetos mutables recibe mucha mala prensa (por ejemplo, devolver un conjunto de cadenas de un método), pero me cuesta entender cuáles son los impactos negativos de esto. ¿Cuáles son las mejores prácticas para usar objetos mutables? ¿Deberías evitarlos siempre que sea posible?
Objetos inmutables vs. colecciones inmutables
Uno de los puntos más finos en el debate sobre los objetos mutables frente a los inmutables es la posibilidad de extender el concepto de inmutabilidad a las colecciones. Un objeto inmutable es un objeto que a menudo representa una única estructura lógica de datos (por ejemplo, una cadena inmutable). Cuando tiene una referencia a un objeto inmutable, el contenido del objeto no cambiará.
Una colección inmutable es una colección que nunca cambia.
Cuando realizo una operación en una colección mutable, entonces cambio la colección en su lugar, y todas las entidades que tienen referencias a la colección verán el cambio.
Cuando realizo una operación en una colección inmutable, se devuelve una referencia a una nueva colección que refleja el cambio. Todas las entidades que tienen referencias a versiones anteriores de la colección no verán el cambio.
Las implementaciones inteligentes no necesariamente necesitan copiar (clonar) toda la colección para proporcionar esa inmutabilidad. El ejemplo más simple es la pila implementada como una lista individualmente vinculada y las operaciones push / pop. Puede reutilizar todos los nodos de la colección anterior en la nueva colección, agregando solo un nodo para el push y clonando nodos para el pop. La operación push_tail en una lista vinculada individualmente, por otro lado, no es tan simple o eficiente.
Variables / referencias inmutables vs. mutables
Algunos lenguajes funcionales toman el concepto de inmutabilidad para referirse a los objetos, permitiendo solo una única asignación de referencia.
- En Erlang esto es cierto para todas las "variables". Solo puedo asignar objetos a una referencia una vez. Si tuviera que operar en una colección, no podría reasignar la nueva colección a la referencia anterior (nombre de la variable).
- Scala también construye esto en el lenguaje con todas las referencias que se declaran con var o val , vals solo es una asignación única y promueve un estilo funcional, pero vars permite una estructura de programa más parecida a c o java.
- Se requiere la declaración var / val, mientras que muchos lenguajes tradicionales usan modificadores opcionales como final en java y const en c.
Facilidad de desarrollo vs. rendimiento
Casi siempre la razón para usar un objeto inmutable es promover la programación libre de efectos secundarios y el razonamiento simple sobre el código (especialmente en un entorno altamente concurrente / paralelo). No tiene que preocuparse de que otra entidad modifique los datos subyacentes si el objeto es inmutable.
El principal inconveniente es el rendimiento. Aquí hay un informe sobre una prueba simple que hice en Java comparando algunos objetos inmutables vs. objetos mutables en un problema de juguete.
Los problemas de rendimiento son discutibles en muchas aplicaciones, pero no en todas, por lo que muchos paquetes numéricos grandes, como la clase Numpy Array en Python, permiten actualizaciones in situ de grandes matrices. Esto sería importante para las áreas de aplicación que hacen uso de grandes operaciones de matriz y vector. Estos grandes problemas paralelos a los datos y computacionalmente intensivos logran una gran aceleración al operar en el lugar.
Bueno, hay un par de aspectos en esto. Número uno, los objetos mutables sin identidad de referencia pueden causar errores en momentos impares. Por ejemplo, considere un Bean Person
con un método equals
basado en el valor:
Map<Person, String> map = ...
Person p = new Person();
map.put(p, "Hey, there!");
p.setName("Daniel");
map.get(p); // => null
La instancia de Person
se "pierde" en el mapa cuando se utiliza como clave porque es hashCode
y la igualdad se basa en valores mutables. Esos valores cambiaron fuera del mapa y todos los hash se volvieron obsoletos. A los teóricos les gusta insistir en este punto, pero en la práctica no he encontrado que sea demasiado problemático.
Otro aspecto es la lógica "razonabilidad" de su código. Este es un término difícil de definir, que abarca todo, desde la legibilidad hasta el flujo. Genéricamente, debería poder ver un fragmento de código y comprender fácilmente lo que hace. Pero más importante que eso, deberías poder convencerte de que hace lo que hace correctamente . Cuando los objetos pueden cambiar independientemente en diferentes "dominios" de código, a veces se vuelve difícil hacer un seguimiento de qué es y por qué ("acción espeluznante a distancia"). Este es un concepto más difícil de ejemplificar, pero es algo que a menudo se enfrenta en arquitecturas más grandes y complejas.
Finalmente, los objetos mutables son asesinos en situaciones concurrentes. Cada vez que accede a un objeto mutable desde hilos separados, tiene que lidiar con el bloqueo. Esto reduce el rendimiento y hace que su código sea mucho más difícil de mantener. Un sistema lo suficientemente complicado hace que este problema sea tan desproporcionado que se vuelva casi imposible de mantener (incluso para los expertos en concurrencia).
Los objetos inmutables (y más particularmente, las colecciones inmutables) evitan todos estos problemas. Una vez que tenga en cuenta cómo funcionan, su código se convertirá en algo que es más fácil de leer, más fácil de mantener y menos propenso a fallar de maneras extrañas e impredecibles. Los objetos inmutables son incluso más fáciles de probar, debido no solo a su fácil maquetabilidad, sino también a los patrones de código que tienden a imponer. En resumen, ¡son una buena práctica para todos!
Dicho esto, no soy un fanático en este asunto. Algunos problemas simplemente no se modelan bien cuando todo es inmutable. Pero creo que deberías tratar de empujar la mayor cantidad posible de tu código en esa dirección, suponiendo, por supuesto, que estás usando un lenguaje que hace que esta sea una opinión defendible (C / C ++ hace esto muy difícil, al igual que Java) . En resumen: las ventajas dependen un poco de su problema, pero yo preferiría la inmutabilidad.
Consulte esta publicación en el blog: http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . Explica por qué los objetos inmutables son mejores que los mutables. En breve:
- los objetos inmutables son más simples de construir, probar y usar
- objetos verdaderamente inmutables son siempre seguros para subprocesos
- ayudan a evitar el acoplamiento temporal
- su uso es libre de efectos secundarios (sin copias defensivas)
- se evita el problema de mutabilidad de identidad
- ellos siempre tienen atomicidad de falla
- son mucho más fáciles de almacenar
Debes especificar de qué idioma estás hablando. Para lenguajes de bajo nivel como C o C ++, prefiero usar objetos mutables para conservar el espacio y reducir la pérdida de memoria. En los lenguajes de nivel superior, los objetos inmutables hacen que sea más fácil razonar sobre el comportamiento del código (especialmente el código de subprocesos múltiples) porque no existe una "acción espeluznante a distancia".
Los medios inmutables no se pueden cambiar, y mutable significa que puede cambiar.
Los objetos son diferentes a los primitivos en Java. Las primitivas están compiladas en tipos (booleano, int, etc.) y los objetos (clases) son tipos creados por el usuario.
Las primitivas y los objetos pueden ser mutables o inmutables cuando se definen como variables miembro dentro de la implementación de una clase.
Mucha gente piensa que las primitivas y variables de objeto que tienen un modificador final frente a ellas son inmutables, sin embargo, esto no es exactamente así. Tan final casi no significa inmutable para las variables. Ver ejemplo aquí
http://www.siteconsortium.com/h/D0000F.php .
Los objetos inmutables son un concepto muy poderoso. Quitan gran parte de la carga de tratar de mantener objetos / variables consistentes para todos los clientes.
Puede usarlos para objetos no polimórficos de bajo nivel, como una clase CPoint, que se utilizan principalmente con semántica de valores.
O puede usarlos para interfaces polimórficas de alto nivel, como una función IF que representa una función matemática, que se utiliza exclusivamente con semántica de objetos.
La mayor ventaja: inmutabilidad + semántica de objetos + punteros inteligentes que hacen que la propiedad del objeto no sea un problema, todos los clientes del objeto tienen su propia copia privada por defecto. Implícitamente, esto también significa un comportamiento determinista en presencia de concurrencia.
Desventaja: cuando se usa con objetos que contienen muchos datos, el consumo de memoria puede convertirse en un problema. Una solución a esto podría ser mantener las operaciones en un objeto simbólico y hacer una evaluación perezosa. Sin embargo, esto puede conducir a cadenas de cálculos simbólicos, que pueden influir negativamente en el rendimiento, si la interfaz no está diseñada para acomodar operaciones simbólicas. Algo que definitivamente debemos evitar en este caso es devolver grandes trozos de memoria de un método. En combinación con operaciones simbólicas encadenadas, esto podría generar un consumo masivo de memoria y una degradación del rendimiento.
Entonces, los objetos inmutables son definitivamente mi principal forma de pensar sobre el diseño orientado a objetos, pero no son un dogma. Resuelven muchos problemas para los clientes de objetos, pero también crean muchos, especialmente para los implementadores.
Si devuelve referencias de una matriz o cadena, entonces el mundo exterior puede modificar el contenido de ese objeto y, por lo tanto, convertirlo en un objeto mutable (modificable).
Si un tipo de clase es mutable, una variable de ese tipo de clase puede tener varios significados diferentes. Por ejemplo, supongamos que un objeto foo
tiene un campo int[] arr
, y contiene una referencia a un int[3]
contiene los números {5, 7, 9}. Aunque se conoce el tipo de campo, hay al menos cuatro cosas diferentes que puede representar:
Una referencia potencialmente compartida, a todos sus titulares les importa solo que encapsule los valores 5, 7 y 9. Si
foo
quiere quearr
encapsule valores diferentes, debe reemplazarlo por una matriz diferente que contenga los valores deseados. Si uno quiere hacer una copia defoo
, puede darle a la copia una referencia aarr
o una nueva matriz que contenga los valores {1,2,3}, lo que sea más conveniente.La única referencia, en cualquier parte del universo, a una matriz que encapsula los valores 5, 7 y 9. conjunto de tres ubicaciones de almacenamiento que en este momento contienen los valores 5, 7 y 9; si
foo
quiere que encapsule los valores 5, 8 y 9, puede cambiar el segundo elemento en esa matriz o crear una nueva matriz que contenga los valores 5, 8 y 9, y abandonar la anterior. Tenga en cuenta que si uno desea hacer una copia defoo
, debe en la copia reemplazararr
con una referencia a una nueva matriz para quefoo.arr
permanezca como la única referencia a esa matriz en cualquier parte del universo.Una referencia a una matriz que es propiedad de algún otro objeto que la haya expuesto a
foo
por alguna razón (por ejemplo, quizás quiera almacenar allí algunos datos). En este escenario,arr
no encapsula el contenido de la matriz, sino su identidad . Debido a que reemplazararr
con una referencia a una nueva matriz cambiaría totalmente su significado, una copia defoo
debería contener una referencia a la misma matriz.Una referencia a una matriz de la cual
foo
es el único propietario, pero a la que las referencias pertenecen a otro objeto por algún motivo (p. Ej., Quiere que el otro objeto almacene datos allí, la otra cara del caso anterior). En este escenario,arr
encapsula tanto la identidad de la matriz como su contenido. Reemplazararr
con una referencia a una nueva matriz cambiaría totalmente su significado, pero tener una copia de clon referir afoo.arr
violaría la suposición de quefoo
es el único propietario. Por lo tanto, no hay forma de copiarfoo
.
En teoría, int[]
debería ser un buen tipo simple bien definido, pero tiene cuatro significados muy diferentes. Por el contrario, una referencia a un objeto inmutable (por ejemplo, String
) generalmente solo tiene un significado. Gran parte del "poder" de los objetos inmutables proviene de ese hecho.
Un objeto mutable es simplemente un objeto que puede modificarse después de haber sido creado / instanciado, frente a un objeto inmutable que no puede ser modificado (ver la página de Wikipedia sobre el tema). Un ejemplo de esto en un lenguaje de programación son las listas y las tuplas de Pitones. Las listas se pueden modificar (p. Ej., Se pueden agregar nuevos elementos después de su creación) mientras que las tuplas no.
Realmente no creo que haya una respuesta clara sobre cuál es mejor para todas las situaciones. Ambos tienen su lugar.
Las instancias mutables se pasan por referencia.
Las instancias inmutables se pasan por valor.
Ejemplo abstracto Supongamos que existe un archivo llamado txtfile en mi disco duro. Ahora, cuando me pides txtfile , puedo devolverlo en dos modos:
- Cree un acceso directo a txtfile y pas atajo a usted, o
- Toma una copia para txtfile y copia para tí.
En el primer modo, el txtfile devuelto es un archivo mutable, porque cuando usted hace cambios en el archivo de acceso directo, también realiza cambios en el archivo original. La ventaja de este modo es que cada atajo devuelto requiere menos memoria (en la RAM o en el disco duro) y la desventaja es que todos (no solo yo, el propietario) tenemos permisos para modificar el contenido del archivo.
En el segundo modo, el txtfile devuelto es un archivo inmutable, porque todos los cambios en el archivo recibido no hacen referencia al archivo original. La ventaja de este modo es que solo yo (propietario) puede modificar el archivo original y la desventaja es que cada copia devuelta requiere memoria (en RAM o en HDD).