Publicado por Clément Béra, ingeniero de software sénior
Los registros son una nueva característica de Java para clases auxiliares de datos inmutables introducidas en Java 16 y Android 14. Para usar registros en Android Studio Flamingo, necesita un SDK de Android 14 (nivel de API 34), de ahí la clase java.lang. . frasco. Está disponible en la revisión 4 del SDK “Android UpsideDownCake Preview”. Los registros son esencialmente clases con propiedades inmutables y métodos hashCode, equals y toString implícitos basados en los campos de datos subyacentes. En este sentido, son muy similares a las clases de datos de Kotlin. Para declarar un registro de persona con campos de nombre de cadena y edad int para completar en un registro de Java, use el siguiente código:
@JvmRecord clase de datos Persona (nombre de valor: Cadena, edad de valor: Int) |
El archivo build.gradle también debe ampliarse para usar el SDK y el origen y el destino de Java correctos. Actualmente, se requiere la vista previa de Android UpsideDownCake, pero cuando se lance el SDK final de Android 14, use “compileSdk 34” y “targetSdk 34” en lugar de la versión de vista previa.
android { compileSdkPreview “UpsideDownCake” defaultConfig { targetSdkPreview “UpsideDownCake” } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = ’17’ } } |
Los registros no necesariamente agregan valor a las clases de datos en los programas puros de Kotlin, pero permiten que los programas de Kotlin interactúen con las bibliotecas de Java cuyas API incluyen registros. Para los programadores de Java, esto permite que el código de Java use registros. Use el siguiente código para declarar el mismo registro en Java:
registro público Persona(String nombre, int edad) {} |
Aparte de las banderas y los atributos de registro, el registro de persona es aproximadamente equivalente a la siguiente clase descrita usando el código fuente de Kotlin:
class PersonEquivalent(val name: String, val age: Int) { override fun hashCode() : Int { return 31 * (31 * PersonEquivalent::class.hashCode() + name.hashCode()) + Integer.hashCode(edad) } override fun equals(other: Any?) : Boolean { if (other == null || other !is PersonEquivalent) { return false } return name == other.name && age == other.age } override fun toString() : String { return String.format( PersonEquivalent::class.java.simpleName + “[name=%s, age=%s]”, nombre, edad.toString() ) } } println(Persona(“Juan”, 42).toString()) >>> Persona[name=John, age=42] |
Los métodos hashCode, equals y toString se pueden anular en una clase de registro, anulando efectivamente los métodos generados por el tiempo de ejecución de JVM. En este caso, el comportamiento lo define el usuario para estos métodos.
Discos sin azúcar
Dado que los registros no son compatibles con ningún dispositivo Android hoy en día, el motor de eliminación de azúcar D8/R8 necesita eliminar los registros: transforma el código del registro en un código compatible con Android VM. La eliminación de azúcar de registros implica transformar el registro en una clase más o menos equivalente, sin generar ni compilar fuentes. La siguiente fuente de Kotlin muestra una aproximación del código generado. Para mantener pequeño el tamaño del código de la aplicación, los registros se eliminan para que los métodos auxiliares se compartan entre registros.
class PersonDesuugared(val name: String, val age: Int) { fun getFieldsAsObjects(): Array for (i in fieldNamesSplit.indices) { builder .append(fieldNamesSplit[i]) .append(“=”) .append(valores del campo[i]) if (i != fieldNamesSplit.size – 1) { builder.append(“, “) } } builder.append(“]”) return builder.toString() } } } } |
Registros de contracción
V8 asume que los métodos predeterminados hashCode, equals y toString generados por javac en realidad representan el estado interno del registro. Por lo tanto, si se minimiza un campo, los métodos deben reflejarlo; toString debe imprimir el nombre minimizado. Si se elimina un campo, por ejemplo porque tiene un valor constante en todas las instancias, los métodos deberían reflejar esto; el campo es ignorado por los métodos hashCode, equals y toString. Cuando R8 usa la estructura de registro en los métodos generados por javac, como al buscar los campos en el registro o inspeccionar la estructura del registro impreso, usa la reflexión. Como con cualquier uso de la reflexión, debe escribir mantener las reglas Informar a la envoltura retráctil de uso reflectante para que pueda preservar la estructura.
En nuestro ejemplo, supongamos que la edad es la constante 42 en la aplicación mientras que el nombre no es una constante en la aplicación. Entonces toString devuelve diferentes resultados según las reglas que establezca:
Persona(“Juan”, 42).toString(); >>> persona[name=John, age=42]
>>> un[a=John] >>> persona[b=John] >>> un[name=John] >>> un[a=John, b=42] >>> persona[name=John, age=42] |
Casos de uso reflexivo
Mantener el comportamiento toString
Digamos que tiene un código que usa la impresión exacta de registros y espera que permanezca igual. Para esto, debe mantener todo el contenido de los campos del registro con una regla como:
-keep,allowshrinking clase Persona -keepclassmembers,allowoptimization clase Persona { |
Esto asegura que si el registro de Persona se mantiene en la salida, cualquier llamada a toString produce exactamente la misma cadena que tendría en el programa original. Por ejemplo:
Persona(“Juan”, 42).toString(); >>> Persona[name=John, age=42] |
Sin embargo, si solo desea seguir imprimiendo los campos realmente utilizados, puede permitir que los campos no utilizados se eliminen o reduzcan con allowshrinking:
-keep,allowshrinking clase Persona -keepclassmembers,allowshrinking,allowoptimization clase Persona { |
Con esta regla, el compilador elimina el campo de edad:
Persona(“Juan”, 42).toString(); >>> Persona[name=John] |
Conservar miembros de registro para una búsqueda cuidadosa
Si necesita acceder reflexivamente a un miembro de registro, generalmente necesita acceder a su método de acceso. Para esto, debe mantener el método de inicio de sesión:
-keep,allowshrinking class Person -keepclassmembers,allowoptimization class Person { java.lang.String name(); } |
Ahora, si las instancias de Persona están en el programa residual, puede buscar de forma segura la existencia del elemento de acceso reflexivamente:
Persona(“Juan”, 42)::clase.java.getDeclaredMethod(“nombre”).invoke(obj); >>> Juan |
Tenga en cuenta que el código anterior accede al campo de registro utilizando el descriptor de acceso. Para acceder directamente al campo, debe mantener el campo en sí:
-keep,allowshrinking class Person -keepclassmembers,allowoptimization class Person { java.lang.String name; } |
Construya los sistemas y registre la clase
Si está utilizando un sistema de compilación que no sea AGP, el uso de los registros puede requerir que adapte su sistema de compilación. La clase java.lang.Record no está presente hasta Android 14, que se introdujo en el SDK mediante la revisión 4 de “Android UpsideDownCake Preview”. D8/R8 presenta com.android.tools.r8.RecordTag, una clase vacía, para indicar que una subclase de registro es un registro. RecordTag se usa para que las declaraciones que hacen referencia a java.lang.Record se puedan reescribir directamente eliminando el azúcar para hacer referencia a RecordTag y seguir funcionando (instancia de, firmas de método y campo, etc.).
Esto significa que cada compilación que contiene una referencia a java.lang.Record genera una clase RecordTag sintética. En una situación en la que una aplicación se divide en fragmentos, cada fragmento se compila en un archivo dex y los archivos dex se juntan sin fusionarse con la aplicación de Android, esto podría conducir a la duplicación de la clase RecordTag.
Para evitar el problema, cualquier compilación D8 intermedia genera la clase RecordTag como un sintético global, en una salida diferente a la del archivo dex. El paso de fusión de dex puede fusionar los sintéticos globales correctamente para evitar un comportamiento inesperado en tiempo de ejecución. Cualquier sistema de compilación que use multicompilación, como fragmentación o salidas intermedias, debe ser compatible con los sintéticos globales para que funcionen correctamente. AGP es totalmente compatible con los registros desde la versión 8.1.