¿Qué son los objetos inmutables?. Un objeto inmutable es aquel que una vez definido e inicializado no puede ser modificado. Cuando llamamos a los métodos que acceden a los objetos, (getters por ejemplo) crean copias de los objetos, pero ningun metodo de ellos permite modificar el estado del objeto. Un ejemplo de esto es la clase String, ya que una vez creado el objeto no puede ser modificado. ¿Enserio?, pero qué pasa con los métodos como trim() que remueven los espacios al inicio y al final de la cadena, ¿Estos métodos modifican el estado de la cadena no?, bueno en realidad no. Si existen espacios al principio o al final de la cadena, el método trim() retorna una nueva cadena eliminando los espacios a los extremos de la cadena, evitando así modificar el objeto original.
Existen muchas ventajas de la creación de objetos inmutables. Veamos algunos teniendo como referencia la clase String.
- Los objetos inmutables son más seguros que los objetos mutables. Una vez tu has validado su valor puedes estar seguro que nadie más lo ha modificado a tus espaldas (por ejemplo por otro código), por lo tanto son menos propenso a errores. Para una instancia, donde tienes referencia a un String y encuentra que tiene los caracteres “contenido”, si retienes la referencia y la usas despues, puedes estar seguro que aún contienen los caracteres “contenido” ya que no puede ser modificada.
- Los objetos inmutables son seguros (thread-safe). Para la instancia, un hilo puede acceder a un objeto String sin la preocupación de que pueda ser modificado por otro hilo cuando este otro lo accese ya que al ser un objeto inmutable no puede ser modificado.
- Los objetos inmutables que tienen el mismo estado pueden ahorrar espacio ya que comparten el estado internamente. Por ejemplo cuando el contenido de dos String son el mismo, estos comparten el mismo contenido interno. Puedes usar el método intern() para verificarlo.
String str1 = new String("contents");
String str2 = new String("contents");
System.out.println("str1 == str2 is " + (str1 == str2));
System.out.println("str1.intern() == str2.intern() is "
+ (str1.intern() == str2.intern()));
// this code prints:
str1 == str2 is false
str1.intern() == str2.intern() is true.
Definiendo Clases Inmutables
Mantén estos aspectos en mente cuando quieras crear tus propios objetos inmutables.
- Definir las propiedades como final e inicializarlas en el constructor. Para los tipos primitivos definidos como final, no hay posibilidad de modificar el estado después de que fueron inicializado. Para los tipos referenciados no es posible cambiar la referencia.
- Para tipos referenciados que son mutables debes tomar algunos aspectos más para asegurar su inmutabilidad. ¿Por qué?. Incluso si marcas una referencia mutable como final es posible que los miembros puedan hacer referencia a objetos fuera de la clase, o pueden ser referenciados por otros.
- Asegurate que ningún método pueda cambiar el contenido dentro de esos objetos mutables.
- No compartas la referencia fuera de la clase, por ejemplo retornar un valor desde métodos en esa clase. Si las referencias a esos campos son accesibles desde fuera de la clases, estos pueden terminar siendo modificados.
- Si debes retornar una referencia, retorna una copia del objeto (una copia profunda) de manera que el objeto original quede intacto incluso si el contenido del objeto retornado fue modificado.
- Crear métodos que permitan el acceso (getters) pero no métodos que permitan la mutación del objeto (setters).
- En caso de necesitar hacer cambios en el objeto, crear un nuevo e inmutable objeto con los cambios requeridos y devolver la referencia.
- Declarar la clase como final. ¿Por que?, bueno declarar una clase como final evita que esta sea heredable, por lo tanto no permite que sus métodos sean override (anulados) ni modificar los campos.
Vamos a revisar la clase String para ver cómo estos aspectos se toman en cuenta en esta implementación.
- Todos los campos son privados. El constructor String inicializa los campos.
- Existen métodos tales como trim, concat o substring que necesitan modificar el contenido del objeto String. Para garantizar la inmutabilidad tales métodos retornan un nuevo objeto con el contenido modificado.
- La clase String es final, por lo que no puedes extender de ella ni hacer un override sobre sus métodos.
El siguiente ejemplo muestra sólo los métodos relevantes para ilustrar cómo definir una clase inmutable.
// Point is a mutable class
class Point {
private int xPos,
yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public String toString() {
return "x = " + xPos + ", y = " + yPos;
}
int getX() {
return xPos;
}
int getY() {
return yPos;
}
}
// ImmutableCircle is an immutable class – the state of its objects
// cannot be modified once the object is created
public final class ImmutableCircle {
private final Point center;
private final int radius;
public ImmutableCircle(int x, int y, int r) {
center = new Point(x, y);
radius = r;
}
public String toString() {
return "center: " + center + " and radius = " + radius;
}
public int getRadius() {
return radius;
}
public Point getCenter() {
// return a copy of the object to avoid
// the value of center changed from code outside the class
return new Point(center.getX(), center.getY());
}
public static void main(String[] s) {
System.out.println(new ImmutableCircle(10, 10, 20));
}
// other members are elided ...
}
Este programa imprime:
center: x = 10, y = 10 and radius = 20
Notese algunos aspectos de la clase InmutableCircle.
- La clase es declarada como final que previene la herencia y el overriding sobre los métodos.
- Los miembros de la clase son marcados como final y son todos ellos privados.
- Cómo center es un campo mutable, el método getCenter() devuelve una copia del objeto Point.