Pronto, aparecerá una nueva característica de sintaxis en los próximos registros de Java 14 . Después de estudiar la vista previa , que describe brevemente cómo se ven las grabaciones y con "lo que comen", me atreví a adaptar el documento al ruso para Habr. A quién le importa, bienvenido a cat.
Resumen
Las entradas le permiten ampliar las capacidades de Java. Proporcionan una sintaxis concisa para declarar clases que son simples portadoras de conjuntos de datos persistentes e inmutables.
Razones y objetivos
Las quejas de que "Java es demasiado detallado" y que necesita "ceremonia" con él son bastante comunes. La razón de esto son las clases que están diseñadas solo para almacenar un determinado conjunto de datos. Para escribir correctamente una clase de este tipo, debe escribir una gran cantidad de código formal, repetitivo y propenso a errores: constructores, captadores y establecedores, equals (), hashCode (), toString (), etc. Los desarrolladores a veces hacen trampa y no anulan equals () y hashCode (), lo que a su vez puede conducir a un comportamiento inusual o problemas con la depuración. O, cuando los desarrolladores no desean declarar otra clase más, prescriben una alternativa, pero no del todo adecuada, simplemente porque tiene la "forma correcta".
Los entornos de desarrollo ayudarán a registrar la mayor parte del código en la clase, pero no ayudarán al desarrollador a leer este código para navegar rápidamente entre docenas de líneas de código repetitivo y comprender que esta clase es un soporte de datos ordinario. Los conjuntos de datos estándar de modelado de código Java deben ser simples de escribir, comprender y validar.
A primera vista, puede parecer que los registros están destinados a reducir el código de la plantilla. Ponemos el objetivo semántico en ellos:
"modelar datos como datos" (modelar datos como datos). Si la semántica es correcta, el código de la plantilla hará todo por sí mismo sin la participación del desarrollador. Después de todo, declarar conjuntos de datos persistentes debe ser fácil, claro y conciso.
Metas que no fueron
No nos propusimos el objetivo de "declarar la guerra" en el código repetitivo. En particular, no teníamos la intención de resolver el problema de las clases mutables utilizando la convención de nomenclatura de componentes JavaBean. Aunque las propiedades, la metaprogramación y la generación de código basada en anotaciones a menudo se sugieren como "soluciones" a este problema, agregar estas características tampoco era nuestro objetivo.
Descripción
Las entradas son un nuevo tipo de declaración de tipo en Java. Al igual que enum, la escritura es una clase funcionalmente limitada. Anuncia su vista y proporciona una API que se basa en esa vista. Las entradas no separan la API de la presentación y, a su vez, son concisas.
La entrada contiene un nombre y una descripción del estado. La descripción del estado declara los componentes de este registro. Opcionalmente, el registro puede tener un cuerpo. Por ejemplo:
record Point(int x, int y) { }
Dado que los registros semánticos son portadores de datos simples, reciben automáticamente elementos estándar:
- Campo final privado para cada componente del estado;
- Un método de lectura pública para cada componente de estado con el mismo nombre y tipo que el componente;
- Un constructor público que coincida con la firma del registro; inicializa cada campo desde el argumento correspondiente;
- Implementaciones de equals () y hashCode (), que dicen que dos registros son iguales si son del mismo tipo y contienen el mismo estado;
- Una implementación de toString (), que incluye una representación de cadena de todos los componentes de grabación con sus nombres.
En otras palabras, la presentación del registro se basa completamente en una descripción del estado. Además, según el estado del registro, se produce la formación de equals (), hashCode () y toString ().
Limitaciones
Los registros no pueden heredar ninguna otra clase y no pueden declarar campos de objeto, excepto los campos finales privados que corresponden a componentes de estado. Cualquier otro campo declarado debe ser estático. Estas limitaciones aseguran que la descripción del estado en sí mismo defina la vista.
Las entradas son finales y no pueden ser abstractas. Estas restricciones indican que la API de registro se define solo por una descripción de estado y no se puede extender más tarde con otra clase o registro.
Los componentes de grabación son finales. Esta restricción implementa el principio de "sin cambios por defecto", que se usa ampliamente para conjuntos de datos.
Además de las limitaciones mencionadas anteriormente, los registros se comportan como clases ordinarias: pueden declararse como de nivel superior o anidados, pueden ser genéricos, pueden implementar interfaces. Los registros se crean llamando al nuevo operador. El cuerpo de escritura puede declarar métodos estáticos, campos estáticos, bloques de inicialización estática, constructores, métodos de instancia, bloques de inicialización de instancia y tipos anidados. Se pueden anotar un registro y componentes de estado individuales. Si el registro está anidado, entonces es estático; Esto elimina la situación con instancias anidadas que podrían agregar automáticamente estado al registro.
Entradas declaradas explícitamente
Aunque la implementación estándar de getters, así como los métodos equals (), hashCode () y toString (), son aceptables para la mayoría de los casos de uso, el desarrollador tiene la opción de anular la implementación estándar. Sin embargo, debe tener especial cuidado al anular los métodos equals / hashCode.
Se presta especial atención a la declaración explícita del constructor canónico, cuya firma coincide con la descripción del estado del registro. Se puede declarar un constructor sin una lista formal de parámetros: en este caso, se supone que coincide con la descripción del estado, y cualquier campo de registro se inicializa implícitamente al cerrar el cuerpo del constructor de los parámetros formales correspondientes (esto. X = x) en la salida. Esto permite que el constructor canónico solo verifique y ajuste sus parámetros, así como omitir la inicialización de campo explícito. Por ejemplo:
record Range(int lo, int hi) { public Range { if (lo > hi) throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi)); } }
Gramática
RecordDeclaration: {ClassModifier} record TypeIdentifier [TypeParameters] (RecordComponents) [SuperInterfaces] [RecordBody] RecordComponents: {RecordComponent {, RecordComponent}} RecordComponent: {Annotation} UnannType Identifier RecordBody: { {RecordBodyDeclaration} } RecordBodyDeclaration: ClassBodyDeclaration RecordConstructorDeclaration RecordConstructorDeclaration: {Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName [Throws] ConstructorBody
Anotaciones para componentes de grabación
Las anotaciones de anotación se pueden aplicar a componentes de grabación si se aplican a componentes, parámetros, campos o métodos. Las anotaciones de anuncios que se aplican a cualquiera de estos componentes se aplican a las declaraciones implícitas de cualquier elemento requerido.
Las anotaciones de tipo que cambian los tipos de componentes de registro se extienden a tipos en declaraciones implícitas de elementos requeridos (por ejemplo, parámetros de constructor, declaraciones de campo y métodos). Las declaraciones explícitas de los elementos necesarios deben coincidir exactamente con el tipo del componente correspondiente del registro, sin incluir anotaciones de tipo.
API de reflexión
Los siguientes métodos públicos se agregarán a
java.lang.Class :
- RecordComponent [] getRecordComponents ()
- boolean isRecord ()
El
método getRecordComponents () devuelve una matriz
java.lang.reflect.RecordComponent , donde
java.lang.reflect.RecordComponent es una nueva clase.
Los elementos de esta matriz corresponden a los componentes del registro y van en el mismo orden en que se declaran en el registro. Se puede extraer información adicional de cada
RecordComponent en la matriz, incluyendo nombre, tipo, genérico, así como su valor.
El método
isRecord () devuelve
verdadero si esta clase se declara como un registro. (Similar al método
isEnum () ).
Alternativas
Los registros se pueden definir como la forma condicional de tuplas. En lugar de registros, podemos usar tuplas estructurales. Aunque las tuplas ofrecen formas más ligeras de expresar algunos conjuntos de datos, el resultado suele ser menos informativo:
- El principio principal de la filosofía Java es que los nombres son importantes . Las clases y sus elementos llevan nombres que son relevantes para su contenido, mientras que las tuplas y sus componentes no. Es decir, la clase Person con las propiedades firstName y lastName es más comprensible y confiable que la tupla anónima de String y String .
- Las clases admiten validación de estado a través de sus constructores, las tuplas no. Algunos conjuntos de datos, como los rangos numéricos, tienen invariantes a los que luego se puede hacer referencia si el constructor los utiliza;
- Las clases pueden tener un comportamiento basado en su estado; La combinación de estado y comportamiento hace que el comportamiento en sí sea más explícito y accesible. Las tuplas, al ser solo un conjunto de datos, no ofrecen esa oportunidad.
Dependencias
Los registros van bien con
los tipos aislados (JEP 360) ; Junto con los tipos aislados, los registros forman una construcción, a menudo llamados
tipos de datos algebraicos. Además, las entradas mismas permiten
la coincidencia de patrones . Debido a que los registros asocian sus API con descripciones de estado, en última instancia, también podemos obtener patrones de deconstrucción para registros y usar la información de clases aisladas en una
declaración de cambio .