¿Por qué Java hace que tantas cosas sean inmutables?

A medida que Java adquiere más características de un lenguaje de programación funcional, se aleja con cuidado de los objetos mutables.

¿Por qué las nuevas funciones de Java enfatizan los tipos de objetos inmutables?

Por ejemplo, al principio de la historia de Java, los desarrolladores vieron la especificación JavaBeans, que enfatizaba la creación y el uso de objetos mutables mediante los métodos set y get, pero los objetos String siempre han sido inmutables. Luego, Java 8 reemplazó la antigua API java.util.Date, que creaba objetos mutables, con la nueva API java.time inmutable, donde todos los objetos son inmutables. Luego, Java 14 obtuvo una vista previa y Java 16 agregó Registros, con todos los campos inmutables. ¿Hay alguna tendencia aquí? ¿Que pasa con eso?

Para responder a la pregunta, este artículo analiza cadenas, fechas y registros.

(Si está buscando una respuesta tl; dr, la inmutabilidad ofrece seguridad para los subprocesos, mientras que las mejoras continuas en el rendimiento de JVM a menudo aceleran la creación de nuevos objetos en lugar de cambiar los viejos).

Cuerdas inmutables y cosas
Al principio, estaba Java 1.0, y dentro de él estaba la clase String. La clase String es final y no tiene métodos mutantes. Una vez que se ha construido una cadena, nunca cambia. Los métodos que en algunos lenguajes pueden cambiar la cadena, como toUpperCase (), en Java crean una nueva cadena con los cambios deseados aplicados. Una cadena es inmutable, punto, punto final (a menos que traiga algún código nativo usando JNI para modificar uno).

El equipo que creó Java entendió claramente la necesidad de objetos String inmutables: si los objetos String pudieran modificarse, todo el modelo de seguridad de Java se rompería. A continuación, se muestra un bosquejo de un posible ataque.


Good Thread: Open File xyz.
  InputStream Constructor; call security manager.
    Security manager - Read file xyz - Permission is OK.
Bad Thread wakes up at just this moment.
  Changes file name string from 'xyz' to '/etc/passwd'
  Yields the CPU
Good Thread
  InputStream Constructor: pass /etc/passwd to operating system open syscall
Bad Thread examines memory buffer for useful information to steal


Además, la inmutabilidad de los objetos String puede conducir a un mejor rendimiento de multiproceso, ya que estos objetos no requieren sincronización o verificación cuando se utilizan en código multiproceso. La aplicación y la JVM pueden dar por sentado que el valor de un objeto String nunca cambiará.

El primer equipo de Java no aplicó la misma noción de inmutabilidad a los objetos de datos normales, como las fechas. Si lo hubieran hecho, Java podría haber sido un lenguaje de programación funcional (FP) impopular en lugar de un lenguaje de programación orientada a objetos (OOP) popular, y yo no habría escrito esto.

Un poco sobre programación funcional. FP es un paradigma de desarrollo de software, al igual que OOP. No existe una descripción acordada de la PF, pero generalmente se considera que la PF incluye lo siguiente:

Código como datos: el código puede tratarse como datos y puede crearse, pasarse y devolverse desde funciones.
Las funciones puras no tienen efectos secundarios, es decir, no hay datos globales.
Los datos son inmutables.
Java nunca puede convertirse en un lenguaje FP puro; simplemente hay demasiado código Java existente que usa setters y getters. Sin embargo, Java tampoco puede convertirse en un lenguaje puro de programación orientada a objetos; los ocho tipos primitivos de Java lo aseguran. (Compare con Python, en el que incluso el entero inferior es un tipo de objeto).

Sin embargo, Java ha adoptado algunas de las prácticas de FP, o al menos se está moviendo en esa dirección. Los objetos de cadena son inmutables, como se discutió anteriormente, y los objetos de registro también lo son, como se describe a continuación. Las referencias a métodos y lambdas de Java se acercan bastante al concepto de código como datos. Puede obtener más información en los numerosos libros sobre programación funcional para desarrolladores de Java.

JavaBeans y sus setters y getters
Java irrumpió en escena en 1995 y, poco después, los desarrolladores vieron la llegada de la especificación JavaBeans. ¿Que es eso? Para citar esa especificación original, “Un Java Bean es un componente de software reutilizable que se puede manipular visualmente en una herramienta de creación”. (En ese momento, los mercados de navegadores y aplicaciones de escritorio visual estaban en la mira de Java; el mercado empresarial del lado del servidor todavía se estaba moviendo dentro del alcance).

Para decirlo en términos más modernos, un JavaBean es un objeto Java razonablemente simple con las siguientes propiedades:

Debe proporcionar métodos set y get para cada propiedad, generalmente respaldados por un campo privado.
Debe esperar ser serializado en tiempo de diseño o tiempo de ejecución o ambos, y así implementar java.io.Serializable.
Debería usar un modelo de eventos estándar, subclasificando java.util.EventObject cuando sus datos cambian.
Debe proporcionar soporte para la personalización en tiempo de diseño, como en una aplicación de creación de GUI si es un bean visual.
El modelo y la especificación de JavaBeans han tenido una enorme influencia en el ecosistema de Java, guiando el desarrollo de API competidoras como Spring Beans y Enterprise JavaBeans. También han llevado a generaciones de programadores Java a creer que el estado natural de las cosas es que los objetos tengan un estado mutable y establezcan y obtengan métodos para cada propiedad. Bueno, eso es normal para OOP, pero no se presta bien a un estilo funcional de programación.

La programación funcional utiliza objetos inmutables.
Los JavaBeans son objetos mutables.
¿Cual es mejor? Tanto el paradigma OOP como el FP tienen su lugar, pero parece claro que la tendencia es hacia el desarrollo al estilo FP con objetos inmutables. Un ejemplo de esto en Java es la API de fecha / hora actual. Otro es la estructura del registro.

La API de fecha / hora
La biblioteca de fecha / hora, java.time, introducida con Java 8, se basa casi en su totalidad en el paquete Joda Time de Stephen Colebourne (la J se pronuncia como la J en Java). Joda Time es una de varias alternativas a las clases java.util.Date originales de Java 1.0.

¿Realmente el mundo necesitaba otro paquete de fecha / hora? La lista de cosas que no funcionan con el manejo de fecha / hora de Java 1.0 podría llenar una página web completa. De hecho, lo ha hecho.

Ignore las rarezas como tener que sumar y restar 1900 para trabajar con el año actual. Ignore que una fecha de Java 1.0 en realidad no representa una fecha. En su lugar, céntrese en la mutabilidad del paquete original.

Suponga que está ejecutando una aplicación multiproceso y tiene una función que usa un objeto java.util.Date heredado. Este objeto se pasó a su función desde otro lugar; su función no tiene idea de dónde vino, o dónde podría ser referenciada en otro hilo.

Además, suponga que está procesando ese objeto de fecha mientras algún otro hilo está modificando el mismo objeto. Las llamadas a getYear (), getMonth () o getDay (), ya sea que se realicen explícitamente o dentro de un formateador de fecha o printf (), podrían entremezclarse fácilmente con otro hilo que cambie los campos, dando como resultado una fecha inexacta o, peor aún, una fecha inválida como el 31 de febrero.

Estos problemas son reducidos a pulpa por la API java.time más nueva y muy superior. Todas las clases de fecha son inmutables e incluso las clases de formato de fecha son seguras para subprocesos. Como muchos desarrolladores entusiastas pueden atestiguar, cambiar a la nueva API hace que su código de manejo de fechas sea mucho más confiable.

Hay un capítulo sobre cómo usar la API java.time en mi libro de cocina de Java. A continuación, se muestra un ejemplo sencillo.


LocalDate now = LocalDate.now();
LocalDate nextWeek = now.plusDays(7);    // This day next week
LocalDate hastings = LocalDate.of(1066, 10, 14);
Period ago = Period.between(hastings, now);
System.out.printf(
  "The Battle of Hastings was fought %d years and %d months ago\n",
  ago.getYears(), ago.getMonths());

Las clases LocalDate y LocalDateTime no tienen una zona horaria asociada; ZonedDate y ZonedDateTime hacen. Como muestra el ejemplo anterior, el código espera y presenta fechas en el orden inequívoco año-mes-día. Existen, por supuesto, formateadores que pueden analizar y mostrar fechas como mm / dd / aaaa, dd / mm / aaaa u otros formatos (según la configuración regional o personalizada). El calendario predeterminado es el calendario gregoriano (occidental); Se admiten media docena de otros calendarios.

La conclusión clave de este artículo es que es muy posible crear una API potente y completa en la que todos los objetos proporcionados sean inmutables y, como la clase String, modificar una fecha devuelve un nuevo objeto con el valor cambiado.

Tipos de registro
Debido a que muchas clases de datos pequeñas y medianas realmente no necesitan ser mutables en absoluto, Java introdujo la primera vista previa del tipo de registro (que fue vista previa en Java 14 y finalizada para Java 16). Un tipo de registro es una clase de datos inmutable cuya creación requiere mucho menos trabajo que una clase normal; El compilador genera todos los métodos y descriptores de acceso comunes que necesita.

Los objetos de un tipo de registro se pueden usar como objetos normales: construidos con nuevos, pasados, usados ​​en colecciones y separados con métodos getter.

Aquí hay un registro de persona, completo en una línea.

public record Person(String firstName, String lastName, String telNum) {};

Después de compilar este archivo de una línea y mostrar sus aspectos externos con javap, se pueden hacer algunas observaciones interesantes.


$ javac -d /tmp /tmp/Person.java
$ javap -cp /tmp Person
Compiled from "Person.java"
public final class Person extends java.lang.Record {
  public Person(java.lang.String, java.lang.String, java.lang.String);
  public final java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.String firstName();
  public java.lang.String lastName();
  public java.lang.String telNum();
}
$

¿Qué revela esto?

Un tipo de registro es una clase que se extiende a Record de java.lang.
El compilador ha proporcionado los métodos toString (), hashCode () y equals (), que funcionan desde el primer momento; Estos métodos pueden anularse si la forma en que funcionan no es la adecuada para su aplicación. Se pueden agregar otros métodos según sea necesario.
El compilador ha proporcionado un constructor y descriptores de acceso para todos los campos del registro. No utilizan el patrón tradicional getFieldName (); en su lugar, usan el nombre de cada campo. No hay constructores parciales, ya que un registro está destinado a contener una representación completa del estado de una entidad determinada.
Cree una instancia de esta clase de registro llamando a su constructor, de la siguiente manera:

Person p = new Person(“Robin”, “Smith”, “555-1212”);

En JShell puede ver lo siguiente:


jshell> public record Person(String firstName, String lastName, String telNum) {};
|  created record Person
jshell> Person p = new Person("Robin", "Smith", "555-1212");
p ==> Person[firstName=Robin, lastName=Smith, telNum=555-1212]
jshell> p.firstName()
$4 ==> "Robin"
jshell>

¿Qué sucede si necesita cambiar el contenido de un registro? No es posible agregar un método de conjunto, como se muestra en este extracto de JShell, ya que todos los campos son finales.


jshell> public record Person(String firstName, String lastName, String telNum) {
  void setTelNum(String telNum) { this.telNum = telNum;}}
|  Error:
|  cannot assign a value to final variable telNum
|  public record Person(String firstName, String lastName, String telNum) {
  void setTelNum(String telNum) { this.telNum = telNum;}}
jshell>

En su lugar, debe crear una nueva instancia. Suponga que la persona cambió su número de teléfono.

p = new Person(p.firstName(), p.lastName(), newPhoneNumber);

La noción de volver a crear un objeto puede parecer aterradora (o al menos terriblemente ineficiente) para quienes se plantean en JavaBeans, pero recuerde que casi todas las versiones del JDK mejoran el rendimiento. Los ingenieros que hacen avanzar el JDK trabajan particularmente duro en la creación rápida de objetos y la recolección de basura. El resultado es que la creación y recreación, especialmente de objetos de datos de corta duración, como los que viven solo dentro de una llamada a un método, son muy rápidas hoy en día.

Por cierto, si la funcionalidad de cambio de número de teléfono es necesaria en varios lugares de su aplicación, agregue un método de fábrica de copiar y cambiar, como lo hacen la clase String y la API java.time.


$ cat PersonModDemo.java
package structure;

public class PersonModDemo {

  public record PersonMod(String name, String email, String phoneNumber) {

    public PersonMod withPhoneNumber(String number) {
      return new PersonMod(name(), email(), number);
    }
  }

  public static void main(String[] args) {
    PersonMod p = new PersonMod("Robin Smith", "robin_smith@example.com", "555-1234");
    System.out.println(p);
    p = p.withPhoneNumber("555-9876");
    System.out.println(p);
  }
}
$ java PersonModDemo.java
PersonMod[name=Robin Smith, email=robin_smith@example.com, phoneNumber=555-1234]
PersonMod[name=Robin Smith, email=robin_smith@example.com, phoneNumber=555-9876]
$

Conclusión
Los objetos inmutables ahora son más simples de crear en comparación con los JavaBeans con todo su bagaje de establecedores y captadores, casi tan fáciles de trabajar y seguros contra modificaciones (posiblemente concurrentes).

Aunque están representadas por solo unas pocas clases en las primeras versiones de Java, encontrará que las API inmutables son cada vez más uno de los hilos que se entrelazan en el rico tapiz que es el presente y el futuro de Java. Los objetos inmutables conducen a una mayor confiabilidad del software y un mejor rendimiento de subprocesos múltiples. Espere ver más API inmutables en el futuro a medida que Java continúa creciendo y madurando.