Desde el exterior, puede parecer que Kotlin ha simplificado el desarrollo de Android sin introducir nuevas dificultades: el lenguaje es compatible con Java, por lo que incluso un gran proyecto de Java puede traducirse gradualmente sin molestar a nadie, ¿verdad? Pero si miras más profundamente, en cada caja hay un doble fondo, y en el tocador hay una puerta secreta. Los lenguajes de programación son proyectos demasiado complejos para combinarse sin matices complicados.
Por supuesto, esto no significa "todo está mal y no necesita usar Kotlin con Java", sino que debe conocer los matices y tenerlos en cuenta. En nuestra conferencia de
Mobius , Sergei Ryabov
habló sobre cómo escribir código en Kotlin al que se podría acceder cómodamente desde Java. Y al público le gustó tanto el informe que no solo decidimos publicar un video, sino que también hicimos una versión de texto para Habr:
He estado escribiendo Kotlin durante más de tres años, ahora solo en él, pero al principio arrastré a Kotlin a proyectos Java existentes. Por lo tanto, la pregunta "cómo unir Java y Kotlin" en mi forma surgió con bastante frecuencia.
A menudo, cuando agrega Kotlin a un proyecto, puede ver cómo esto ...
compile 'rxbinding:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-design:xyx' compile 'autodispose:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-archcomponents:xyz'
... se convierte en esto:
compile 'rxbinding:xyx' compile 'rxbinding-kotlin:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-appcompat-v7-kotlin:xyx' compile 'rxbinding-design:xyx' compile 'rxbinding-design-kotlin:xyx' compile 'autodispose:xyz' compile 'autodispose-kotlin:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-kotlin:xyz' compile 'autodispose-android-archcomponents:xyz' compile 'autodispose-android-archcomponents-kotlin:xyz'
Los detalles de los últimos años: las bibliotecas más populares adquieren envoltorios para que puedan ser utilizados desde Kotlin de manera más idiomática.
Si escribió en Kotlin, entonces sabe que hay funciones de extensión interesantes, funciones en línea, expresiones lambda que están disponibles en Java 6. Y esto es genial, nos atrae a Kotlin, pero surge la pregunta. Una de las características más grandes y publicitadas del lenguaje es la interoperabilidad con Java. Si tiene en cuenta todas las características enumeradas, ¿por qué no simplemente escribir bibliotecas en Kotlin? Todos funcionarán perfectamente con Java, y no necesitará admitir todos estos contenedores, todos estarán contentos y contentos.
Pero, por supuesto, en la práctica, no todo es tan color de rosa como en los folletos, siempre hay un "pequeño atributo de fuente", hay bordes afilados en la unión de Kotlin y Java, y hoy hablaremos un poco sobre esto.
Bordes afilados
Comencemos con las diferencias. Por ejemplo, ¿sabe que en Kotlin no hay palabras clave volátiles, sincronizadas, estrictas, transitorias? Se reemplazan por anotaciones del mismo nombre ubicadas en el paquete kotlin.jvm. Por lo tanto, la mayor parte de la conversación tratará sobre el contenido de este paquete.
Existe
Timber , una abstracción de la biblioteca sobre los madereros del famoso
Zheka Vartanov . Le permite usarlo en todas partes en su aplicación, y todo lo que desee enviar registros (a logcat, o a su servidor para análisis, o informes de fallos, etc.) se convierte en complementos.
Imaginemos, por ejemplo, que queremos escribir una biblioteca similar, solo para análisis. También desconectarse.
object Analytics { fun send(event: Event) {} fun addPlugins(plugs: List<Plugin>) {} fun getPlugins(): List<Plugin> {} } interface Plugin { fun init() fun send(event: Event) fun close() } data class Event( val name: String, val context: Map<String, Any> = emptyMap() )
Tomamos el mismo patrón de construcción, tenemos un punto de entrada: esto es Analytics. Podemos enviar eventos allí, agregar complementos y ver lo que ya hemos agregado allí.
Plugin es una interfaz de plugin que abstrae una API analítica específica.
Y, de hecho, la clase de evento que contiene la clave y nuestros atributos que enviamos. Aquí el informe no trata sobre si vale la pena usar singletones, así que no criemos un holivar, sino que veremos cómo peinar todo esto.
Ahora una pequeña inmersión. Aquí hay un ejemplo del uso de nuestra biblioteca en Kotlin:
private fun useAnalytics() { Analytics.send(Event("only_name_event")) val props = mapOf( USER_ID to 1235, "my_custom_attr" to true ) Analytics.send(Event("custom_event", props)) val hasPlugins = Analytics.hasPlugins Analytics.addPlugin(EMPTY_PLUGIN)
En principio, se ve como se esperaba. Un punto de entrada, los métodos se llaman estáticos. Evento sin parámetros, evento con atributos. Verificamos si tenemos complementos, colocamos un complemento vacío allí para hacer algún tipo de ejecución de "ejecución en seco". O agregue algunos otros complementos, muéstrelos, etc. En general, casos de usuarios estándar, espero que todo esté claro hasta ahora.
Ahora veamos qué sucede en Java cuando hacemos lo mismo:
private static void useAnalytics() { Analytics.INSTANCE.send(new Event("only_name_event", Collections.emptyMap())); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.INSTANCE.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.INSTANCE.getHasPlugins(); Analytics.INSTANCE.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN());
El alboroto con INSTANCE se apresura de inmediato en mis ojos, que se extiende, la presencia de valores explícitos para el parámetro predeterminado con atributos, algunos captadores con nombres tontos. Dado que, en general, nos hemos reunido aquí para convertir esto en algo similar al archivo anterior con Kotlin, entonces veamos cada momento que no nos gusta e intentemos adaptarlo de alguna manera.
Comencemos con el evento. Eliminamos el parámetro Colletions.emptyMap () de la segunda línea y aparece un error del compilador. ¿Cuál es la razón de esto?
data class Event( val name: String, val context: Map<String, Any> = emptyMap() )
Nuestro constructor tiene un parámetro predeterminado al que le pasamos el valor. Venimos de Java a Kotlin, es lógico suponer que la presencia de un parámetro predeterminado genera dos constructores: uno completo con dos parámetros y otro parcial, para el cual solo se puede especificar el nombre. Obviamente, el compilador no lo cree así. Veamos por qué piensa que estamos equivocados.
Nuestra herramienta principal para analizar todos los giros y vueltas de cómo Kotlin se convierte en un bytecode JVM - Kotlin Bytecode Viewer. En Android Studio e IntelliJ IDEA, se encuentra en el menú Herramientas - Kotlin - Mostrar código de bytes Kotlin. Simplemente puede presionar Cmd + Shift + A y escribir Kotlin Bytecode en la barra de búsqueda.

Aquí, sorprendentemente, vemos un bytecode de en lo que se está convirtiendo nuestra clase de Kotlin. No espero que tenga un excelente conocimiento del código de bytes, y lo más importante, los desarrolladores de IDE tampoco lo esperan. Por lo tanto, hicieron un botón Descompilar.
Después de hacer clic en él, vemos un código Java tan bueno:
public final class Event { @NotNull private final String name; @NotNull private final Map context; @NotNull public final String getName() { return this.name; } @NotNull public final Map getContext() { return this.context; } public Event(@NotNull String name, @NotNull Map context) { Intrinsics.checkParameterIsNotNull(name, "name"); Intrinsics.checkParameterIsNotNull(context, "context"); super(); this.name = name; this.context = context; }
Vemos nuestros campos, captadores, el constructor esperado con dos parámetros nombre y contexto, todo sucede bien. Y a continuación vemos el segundo constructor, y aquí está con una firma inesperada: no con un parámetro, sino por alguna razón con cuatro.
Aquí puedes sentirte avergonzado, pero puedes escalar un poco más y hurgar. Comenzando a entender, entenderemos que DefaultConstructorMarker es una clase privada de la biblioteca estándar de Kotlin, agregada aquí para que no haya conflictos con nuestros constructores escritos, ya que no podemos establecer un parámetro de tipo DefaultConstructorMarker con nuestras manos. Y lo interesante de int var3 es la máscara de bits de los valores predeterminados que debemos usar. En este caso, si la máscara de bits coincide con las dos, sabemos que var2 no está establecida, nuestros atributos no están establecidos y usamos el valor predeterminado.
¿Cómo podemos solucionar la situación? Para hacer esto, hay una anotación milagrosa @JvmOverloads del paquete del que ya hablé. Tenemos que colgarlo en el constructor.
data class Event @JvmOverloads constructor( val name: String, val context: Map<String, Any> = emptyMap() )
¿Y qué hará ella? Pasemos a la misma herramienta. Ahora vemos nuestro constructor completo, y el constructor con DefaultConstructorMarker, y, he aquí, un constructor con un parámetro, que ahora está disponible desde Java:
@JvmOverloads public Event(@NotNull String name) { this.name, (Map)null, 2, (DefaultConstructorMarker)null); }
Y, como puede ver, delega todo el trabajo con parámetros predeterminados a nuestro constructor con máscaras de bits. Por lo tanto, no producimos información sobre el valor predeterminado que necesitamos poner allí, simplemente delegamos todo en un constructor. Agradable Verificamos lo que obtenemos del lado de Java: el compilador está contento y no indignado.
Veamos lo que no nos gusta a continuación. No nos gusta este INSTANCE, que en IDEA es un insensible de color morado. No me gusta el color morado :)

Vamos a comprobar, debido a lo que resulta. Miremos el bytecode nuevamente.
Por ejemplo, destacamos la función init y nos aseguramos de que init se genere, no estático.

Es decir, digamos lo que uno pueda decir, necesitamos trabajar con una instancia de esta clase y llamar a estos métodos sobre ella. Pero podemos obligar a la generación de todos estos métodos a ser estáticos. Hay una maravillosa anotación @JvmStatic para esto. Añádalo al init y envíe funciones y verifiquemos qué piensa el compilador al respecto ahora.
Vemos que la palabra clave estática se ha agregado a public final init (), y nos hemos ahorrado de trabajar con INSTANCE. Verificaremos esto en el código Java.
El compilador ahora nos dice que estamos invocando el método estático desde el contexto INSTANCE. Esto se puede corregir: presione Alt + Intro, seleccione "Código de limpieza" y listo, INSTANCE desaparece, todo se ve casi igual que en Kotlin:
Analytics.send(new Event("only_name_event"));
Ahora tenemos un esquema para trabajar con métodos estáticos. Agregue esta anotación donde sea que nos importe:

Y el comentario: si los métodos que tenemos son obviamente los métodos de instancia, entonces, por ejemplo, con propiedades, no todo es tan obvio. Los campos en sí (por ejemplo, complementos) se generan como estáticos. Pero getters y setters funcionan como métodos de instancia. Por lo tanto, para las propiedades, también debe agregar esta anotación para que los establecedores y captadores sean estáticos. Por ejemplo, vemos la variable isInited, le agregamos la anotación @JvmStatic y ahora vemos en Kotlin Bytecode Viewer que el método isInited () se ha vuelto estático, todo está bien.
Ahora vamos al código de Java, "para limpiarlo", y todo se parece a Kotlin, excepto los puntos y comas y la palabra nuevo, bueno, no los eliminará.
public static void useAnalytics() { Analytics.send(new Event("only_name_event")); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.getHasPlugins(); Analytics.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN());
Siguiente paso: vemos este getHasPlugins tontamente llamado getter con dos prefijos a la vez. Por supuesto, no soy un gran conocedor del idioma inglés, pero me parece que hay algo más implícito aquí. ¿Por qué está pasando esto?
Como sabían de cerca con Kotlin, los nombres de propiedad para captadores y definidores se generan de acuerdo con las reglas de JavaBeans. Esto significa que los getters generalmente estarán con prefijos get, setters con prefijos establecidos. Pero hay una excepción: si tiene un campo booleano y su nombre tiene el prefijo is, entonces el getter tendrá como prefijo is. Esto se puede ver en el ejemplo del campo isInited anterior.
Desafortunadamente, lejos de siempre, los campos booleanos deben llamarse a través de is. isPlugins no satisfaría lo que queremos mostrar semánticamente por nombre. Como seamos
Y no es difícil para nosotros, para esto tenemos nuestra propia anotación (como ya entendiste, a menudo repetiré esto hoy). La anotación @JvmName le permite especificar cualquier nombre que queramos (naturalmente soportado por Java). Añádelo:
@JvmStatic val hasPlugins @JvmName("hasPlugin") get() = plugins.isNotEmpty()
Veamos qué obtuvimos en Java: el método getHasPlugins ya no está allí, pero hasPlugins es algo por sí mismo. Esto resolvió nuestro problema, nuevamente, con una anotación. ¡Ahora resolvemos todas las anotaciones!
Como puede ver, aquí ponemos la anotación directamente en el captador. ¿Cuál es la razón de esto? Con el hecho de que debajo de la propiedad hay mucho de todo, y no está claro a qué se aplica @JvmName. Si transfiere la anotación a val hasPlugins, el compilador no entenderá a qué aplicarla.
Sin embargo, Kotlin también tiene la capacidad de especificar dónde se usan las anotaciones directamente en él. Puede especificar el getter de destino, el archivo completo, el parámetro, el delegado, el campo, las propiedades, las funciones de extensión del receptor, el setter y el parámetro setter. En nuestro caso, getter es interesante. Y si te gusta esto, tendrá el mismo efecto que cuando colgamos la anotación en get:
@get:JvmName("hasPlugins") @JvmStatic val hasPlugins get() = plugins.isNotEmpty()
En consecuencia, si no tiene un captador personalizado, puede adjuntarlo directamente a su propiedad, y todo estará bien.
El siguiente punto que nos confunde un poco es "Analytics.INSTANCE.getEMPTY_PLUGIN ()". Aquí el asunto ya no es ni siquiera en inglés, sino simplemente: ¿POR QUÉ? La respuesta es casi la misma, pero primero una pequeña introducción.
Para hacer un campo constante, tiene dos formas. Si define una constante como un tipo primitivo o como una Cadena, y también dentro del objeto, entonces puede usar la palabra clave const, y luego no se generarán getter-setters y otras cosas. Será una constante ordinaria, estática final privada, y estará en línea, es decir, una cosa Java absolutamente ordinaria.
Pero si desea hacer una constante de un objeto que sea diferente de la cadena, entonces no podrá usar la palabra const para esto. Aquí tenemos val EMPTY_PLUGIN = EmptyPlugin (), de acuerdo con eso, obviamente se generó ese captador terrible. Podemos renombrar @JvmName con una anotación, eliminar este prefijo get, pero aún así sigue siendo un método, entre paréntesis. Entonces, las soluciones antiguas no funcionarán, estamos buscando nuevas.
Y aquí para esto la anotación @JvmField, que dice: "No quiero getters aquí, no quiero setters, hazme un campo". Póngalo delante de val EMPTY_PLUGIN y verifique que todo sea cierto.

Kotlin Bytecode Viewer muestra la pieza resaltada en la que se encuentra actualmente en el archivo. Ahora estamos parados en EMPTY_PLUGIN, y ve que aquí se escribe algún tipo de inicialización en el constructor. El hecho es que el captador ya no está allí y el acceso a él es solo para grabar. Y si hace clic en descompilar, vemos que ha aparecido "public static final EmptyPlugin EMPTY_PLUGIN", esto es exactamente lo que hemos logrado. Agradable Verificamos que todo complace a todos, en particular al compilador. Lo más importante que necesita para apaciguar es el compilador.
Genéricos
Tomemos un descanso del código y veamos los genéricos. Este es un tema muy candente. O resbaladizo, a quien ya no le gusta eso. Java tiene sus propias complejidades, pero Kotlin es diferente. En primer lugar, nos preocupa la variación. Que es esto
La variabilidad es una forma de transferir información sobre una jerarquía de tipos de tipos básicos a derivados, por ejemplo, a contenedores o genéricos. Aquí tenemos las clases de animales y perros con una conexión muy obvia: el perro es un subtipo, el animal es un subtipo, la flecha proviene del subtipo.

¿Y qué conexión tendrán sus derivados? Veamos algunos casos.
El primero es Iterator. Para determinar qué es un subtipo y qué es un subtipo, nos guiaremos por la regla de sustitución Barbara Liskov. Se puede formular de la siguiente manera: "el subtipo no debería requerir más y no proporcionar menos".
En nuestra situación, lo único que hace Iterator es darnos objetos mecanografiados, por ejemplo, Animal. Si aceptamos Iterator en algún lugar, podemos poner a Iterator allí y obtener Animal del siguiente método (), porque el perro también es Animal. Proporcionamos no menos, sino más, porque un perro es un subtipo.

Repito: solo estamos leyendo de este tipo, por lo tanto, la relación entre el tipo y el subtipo se conserva aquí. Y estos tipos se llaman covariantes.
Otro caso: acción. Action es una función que no devuelve nada, toma un parámetro y solo escribimos en Action, es decir, nos quita un perro o un animal.

Por lo tanto, aquí ya no proporcionamos, sino que exigimos, y no debemos exigir más. Esto significa que nuestra dependencia está cambiando. "No más" tenemos Animal (Animal menos que un perro). Y tales tipos se llaman contravariantes.
Hay un tercer caso, por ejemplo, ArrayList, del cual leemos y escribimos. Por lo tanto, en este caso, violamos una de las reglas, requerimos más para un registro (un perro, no un animal). Tales tipos no están relacionados de ninguna manera, y se llaman invariantes.

Entonces, en Java, cuando se diseñó antes de la versión 1.5 (donde aparecían los genéricos), por defecto hicieron que las matrices fueran covariantes. Esto significa que puede asignar una matriz de cadenas a la matriz de objetos, luego pasarla en algún lugar al método donde se necesita la matriz de objetos e intentar empujar el objeto allí, aunque esta es una matriz de cadenas. Todo te caerá a ti.
Habiendo aprendido por amarga experiencia que esto no se puede hacer, al diseñar genéricos, decidieron "haremos que las colecciones sean invariables, no haremos nada con ellas".
Y al final resulta que en una cosa tan aparentemente obvia todo debería estar bien, pero en realidad no está bien:
Pero necesitamos determinar de alguna manera qué, después de todo, podemos: si solo estamos leyendo esta hoja, ¿por qué no hacer posible transferir la lista de perros aquí? Por lo tanto, es posible caracterizar con comodín qué tipo de variación tendrá este tipo:
List<Dog> dogs = new ArrayList<>(); List<? extends Animal> animals = dogs;
Como puede ver, esta variación se indica en el lugar de uso, donde asignamos los perros. Por lo tanto, esto se llama variación de sitio de uso.
¿Cuáles son las desventajas de esto? El lado negativo es que debe especificar estos comodines de miedo donde sea que use su API, y todo esto es muy fructífero en el código. Pero en Kotlin por alguna razón, tal cosa funciona de forma inmediata, y no necesita especificar nada:
val dogs: List<Dog> = ArrayList() val animals: List<Animal> = dogs
¿Cuál es la razón de esto? Con el hecho de que las sábanas son realmente diferentes. Lista en Java significa escritura, mientras que en Kotlin es de solo lectura, no implica. Por lo tanto, en principio, podemos decir de inmediato que solo estamos leyendo desde aquí, por lo tanto, podemos ser covariantes. Y esto se establece precisamente en la declaración de tipo con la palabra clave out reemplazando el comodín:
interface List<out E> : Collection<E>
Esto se llama varianza del sitio de declaración. Por lo tanto, indicamos todo en un solo lugar, y donde lo usamos, ya no tocamos este tema. Y esto es nishtyak.
Volver al código
Volvamos a nuestras profundidades. Aquí tenemos el método addPlugins, se necesita una Lista:
@JvmStatic fun addPlugins (plugs: List<Plugin>) { plugs.forEach { addPlugin(it) } } , , List<EmptyPlugin>, , : <source lang="java"> final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("Alog"), new SegmentPlugin());
Debido al hecho de que List en Kotlin es covariante, podemos pasar fácilmente la lista de herederos de plugins aquí. Todo funcionará, al compilador no le importa. Pero debido al hecho de que tenemos una variación del sitio de declaración donde especificamos todo, no podemos controlar la conexión con Java en la etapa de uso. Pero, ¿qué sucede si realmente queremos una hoja de complementos allí, no queremos herederos allí? No hay modificadores para esto, pero ¿qué? Así es, hay una anotación. Y la anotación se llama @JvmSuppressWildcards, es decir, por defecto creemos que aquí hay un tipo con comodín, el tipo es covariante.
@JvmStatic fun addPlugins(plugs: List<@JvmSuppressWildcards Plugin>) { plugs.forEach { addPlugin(it) } }
Hablando SuppressWildcards, suprimimos todas estas preguntas, y nuestra firma realmente cambia. Incluso más que eso, te mostraré cómo se ve todo en bytecode:

Eliminaré la anotación del código por ahora. Aquí está nuestro método. Probablemente sepa que existe ese tipo de borrado. Y en su código de bytes no hay información sobre qué tipo de preguntas hubo, bueno, genéricos en general. Pero el compilador sigue esto y lo firma en los comentarios al código de bytes: y este es el tipo con la pregunta.

Ahora volvemos a insertar la anotación y vemos que este es nuestro tipo sin cuestionarnos.

Ahora nuestro código anterior dejará de compilarse precisamente porque cortamos los comodines. Puedes verlo por ti mismo.
Identificamos tipos covariantes. Ahora lo contrario es cierto.
Creemos que List tiene una pregunta.
Es obvio suponer que cuando esta hoja regrese de getPlugins, también será con una pregunta. ¿Qué significa esto? Esto significa que no podremos escribir en él, porque el tipo es covariante y no contravariante. Echemos un vistazo a lo que está sucediendo en Java. final List<Plugin> plugins = Analytics.getPlugins(); displayPlugins(plugins); Analytics.getPlugins().add(new EmptyPlugin());
, - , , - . , . , - .
. Kotlin , , , , , wildcards Java. , , . , List, Plugin. , , , : Plugin, .
. , , usecase, - , .
, , , - . , Java. Kotlin List — read only-, , Java — ? , List wildcard. , . @JvmWildcard : , . , Java . Java « ?»:

List<? extends Plugin>, « ?» , , . script kiddie, « , , , ArrayList, ». , ArrayList , .
((ArrayList<Plugin>) Analytics.getPlugins()).add(new EmptyPlugin());
, , , defensive-, - . , , , script kiddies .
@JvmStatic fun getPlugins(): List<@JvmWildcard Plugin> = plugin.toImmutableList()
, @JvmSuppressWildcard , , , , .
, . , : .
Java. , :
@Override public void send(@NotNull Event event) throws IOException
:
interface Plugin { fun init() fun send(event: Event)
Kotlin checked exception. : . , , . Java -. : « Throws - , »:

-, Kotlin? , …
@Throws, . throws- . , IOExeption:
open class EmptyPlugin : Plugin { @Throws(IOException::class) override fun send(event: Event) {}
:
interface Plugin { fun init() @Throws(IOException::class) fun send(event: Event)
? , Java, exception, . , . , - , , @JvmName. .
, Java . …
package util fun List<Int>.printReversedSum() { println(this.foldRight(0) { it, acc -> it + acc }) } @JvmName("printReversedConcatenation") fun List<String>.printReversedSum() { println(this.foldRight(StringBuilder()) { it, acc -> acc.append(it) }) }
Supongamos que en Java no nos importa aquí, elimine la anotación. Errores, ahora el IDE muestra un error en ambas funciones. ¿Cuál crees que es la razón de esto? Sí, sin anotaciones, se generan con el mismo nombre, pero aquí está escrito que uno está en la Lista, el otro en la Lista. Derecha, tipo borrado. Incluso podemos verificar este caso:
, , top-level c. printReversedSum List, List. Kotlin- , Java- . , kotlin.jvm , Java , , Kotlin . — , concatenation — , .
. . extension- reverse.
inline fun String.reverse() = StringBuilder(this).reverse().toString() inline fun <reified T> reversedClassName() = T::class.java.simpleName.reverse() inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) }
reverse , ReverserKt.
private static void useUtils() { System.out.println(ReverserKt.reverse("Test")); SumsKt.printReversedSum(asList(1, 2, 3, 4, 5)); SumsKt.printReversedConcatenation(asList("1", "2", "3", "4", "5")); }
, , . , , Java, - . . ? , @JvmName, , .
, , , , , .
@file:Suppress("NOTHING_TO_INLINE") @file:JvmName("ReverserUtils")
Ahora al compilador de Java no le gusta ReverserKt, pero se espera que lo reemplacemos con ReverserUtils y todos estén contentos. Y este "caso de usuario 2.1" es un caso frecuente cuando desea recopilar varios de sus métodos de archivos de nivel superior en una clase, en una fachada. Por ejemplo, sobre usted, no desea que los métodos de los sums.kt anteriores se invoquen desde SumsKt, pero desea que esto se trate de revertir y contraer desde ReverserUtils. Luego agregamos esta maravillosa anotación @JvmName allí, escriba "ReverserUtils", en principio, todo está bien, incluso puede intentar compilar esto, pero no., , « , -». ? @JvmMultifileClass, , , .
"@file:JvmMultifileClass", SumsKt ReverserUtils, — . !
, . , , . , , , @JvmName Kotlin.
Kotlin-
, , . , Kotlin- .
, inline-. Kotlin , , Java ? , , , Java. , , Kotlin-only , dex count limit. Kotlin , .
Reified type parameters. Kotlin, - , Java . Kotlin-only , Kotlin, Java reified, .
java.lang.Class. , Java, . . « Retrofit», ( , ):
class Retrofit private constructor( val baseUrl: String, val client: Client ) { fun <T : Any> create(service: Class<T>): T {...} fun <T : Any> create(service: KClass<T>): T { return create(service.java) } }
, Java, , KClass, , extension-, KClass Class, Class KClass ( Kotlin, ).
, . Kotlin- KClass, Reified-, :
inline fun <reified T : Any> create(): T { return create(T::class.java.java)
. Kotlin , .
val api = retrofit.create(Api::class) val api = retrofit.create<Api>() , ::class . Reified-, -.
Unit. Unit, , void Java, . . , . - Scala, Scala , - , , , void.
Pero en Kotlin esto no es. Kotlin tiene solo 22 interfaces que aceptan un conjunto diferente de parámetros y devuelven algo. Por lo tanto, la lambda que devuelve Unidad no devolverá nada, sino Unidad. Y esto impone sus limitaciones. ¿Cómo se ve la lambda que devuelve la Unidad? Ahora, mírala en este fragmento de código. Llegar a conocerse. inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) }
Utilizándolo desde Kotlin: todo está bien, incluso utilizamos una referencia de método, si podemos, y se lee perfectamente, nuestros ojos no son insensibles. private fun useMisc() { listOf(1, 2, 3, 4).forEachReversed(::println) println(reversedClassName<String>()) }
¿Qué está pasando en Java? En Java, sucede la siguiente canoa: private static void useMisc() { final List<Integer> list = asList(1, 2, 3, 4); ReverserUtils.forEachReversed(list, integer -> { System.out.println(integer); return Unit.INSTANCE; });
- , - . Void , . , void, . , , , . , Unit . null, . , .
: Typealiases — , , Kotlin, Java, , , . , - . Java- .
Ahora para la parte interesante: visibilidad. O más bien, visibilidad interna. Probablemente sepa que en Kotlin no hay paquete privado, si escribe sin modificadores, será público. Pero hay interna. Lo interno es algo tan complicado que incluso lo veremos ahora. En Retrofit, tenemos un método de validación interno. internal fun validate(): Retrofit { println("!!!!!! internal fun validate() was called !!!!!!") return this }
Kotlin, . Java? validate? , , internal public. , Kotlin bytecode viewer.

public, , , , , , API . - 80 , .
Java :
final Api api = retrofit .validate$production_sources_for_module_library_main() .create(Api.class); api.sendMessage("Hello from Java"); }
. , , , . , let me explain this to you. , ?
final Api api = retrofit .validate$library() .create(Api.class); api.sendMessage("Hello from Java"); }
. « ?» … MAGIC!
, - internal, , API. script kiddie Kotlin Bytecode Viewer, . internal visibility.
, . , ,
, , SkillsMatter. .
, Kotlin-. , - , . Kotlin bytecode viewer .
Gracias
, : 8-9 Mobius , . — , .