从外部看,Kotlin似乎简化了Android开发,却丝毫没有引入新的困难:该语言与Java兼容,因此即使是大型Java项目也可以逐步翻译成它,而不会打扰任何人,对吗? 但是,如果您看得更深,则每个盒子中都有一个双底,而在梳妆台中有一个秘密的门。 编程语言是太复杂的项目,因此无法加以细微的细微差别地组合。
当然,这并不意味着“一切都不好,并且您不需要将Kotlin与Java一起使用”,而是意味着您应该了解细微差别并加以考虑。 在我们的
Mobius会议上,Sergei Ryabov
讨论了如何在Kotlin上编写可从Java方便地访问的代码。 观众非常喜欢这份报告,我们不仅决定发布视频,而且还为Habr制作了文本版本:
我已经写了Kotlin三年多了,现在只写了这篇文章,但是起初我将Kotlin拖到了现有的Java项目中。 因此,以我的方式提出的“如何将Java和Kotlin结合在一起”问题经常出现。
通常,当您将Kotlin添加到项目中时,您可以看到它是如何...
compile 'rxbinding:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-design:xyx' compile 'autodispose:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-archcomponents:xyz'
...变成这样:
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'
最近几年的细节:最受欢迎的库会获取包装器,以便可以从Kotlin更加习惯地使用它们。
如果您是用Kotlin编写的,那么您会知道Java 6中提供了很酷的扩展功能,内联函数,lambda表达式。这很酷,它吸引了我们到Kotlin,但是出现了问题。 该语言最大,最广为人知的功能之一就是与Java的互操作性。 如果考虑到所有列出的功能,那么为什么不只用Kotlin编写库呢? 它们都可以与Java完美兼容,并且您不需要支持所有这些包装器,每个人都会感到满意和满足。
但是,当然,实际上,并不是所有的事情都像小册子中那样乐观,总是有“小字体属性”,在Kotlin和Java的交界处有锐利的边缘,今天我们将对此稍作讨论。
锋利的边缘
让我们从差异开始。 例如,您知道在Kotlin中没有关键字volatile,synchronized,strictfp,transient吗? 它们被位于kotlin.jvm包中的相同名称的注释替换。 因此,大多数讨论将围绕该程序包的内容进行。
有
Timber-如此臭名昭著的
Zheka Vartanov的记录器库的摘要。 它使您可以在应用程序中的任何地方使用它,并且要将日志发送到的所有内容(发送到logcat或服务器以进行分析或崩溃报告等)都将变成插件。
例如,假设我们要编写一个仅用于分析的类似库。 也脱离。
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() )
我们采用相同的构造模式,只有一个入口点-这就是Google Analytics(分析)。 我们可以在此处发送事件,添加插件并查看我们已经在此处添加的内容。
插件是一个插件接口,可抽象出特定的分析API。
实际上,Event类包含键和我们发送的属性。 这里的报告不是关于使用单调是否值得的,因此,我们不要繁殖一个holivar,但是我们将观察如何梳理整个过程。
现在稍微下潜。 这是在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)
原则上,它看起来像预期的那样。 方法的一个入口就是静态。 没有参数的事件,带有属性的事件。 我们检查是否有插件,将一个空插件推入其中以进行某种“空运行”运行。 或添加其他一些插件,显示它们,依此类推。 通常,我希望标准用户案例到目前为止一切都清楚。
现在,让我们看看当我们执行相同的操作时在Java中会发生什么:
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());
带有INSTANCE的大惊小怪立即涌入我的眼睛,不知不觉中出现了带有属性的默认参数的显式值,有些带有愚蠢名称的getter。 由于我们通常都聚集在这里,将其转换为与Kotlin先前的文件类似的文件,因此,让我们经历一下我们不喜欢的每时每刻,然后尝试以某种方式进行调整。
让我们从事件开始。 我们从第二行删除Colletions.emptyMap()参数,并弹出编译器错误。 这是什么原因呢?
data class Event( val name: String, val context: Map<String, Any> = emptyMap() )
我们的构造函数有一个默认参数,我们会将值传递给该参数。 我们从Java到Kotlin,从逻辑上来说,假设默认参数的存在会生成两个构造函数:一个带有两个参数的完整构造函数,另一个是只能指定名称的部分构造函数。 显然,编译器并不这么认为。 让我们看看他为什么认为我们错了。
我们用于分析Kotlin如何转变为JVM字节码的所有曲折的主要工具-Kotlin字节码查看器。 在Android Studio和IntelliJ IDEA中,它位于工具-Kotlin-显示Kotlin字节码菜单中。 您只需按Cmd + Shift + A并在搜索栏中输入Kotlin字节码。

令人惊讶的是,在这里,我们看到了Kotlin类正在变成的字节码。 我不希望您对字节码有很好的了解,最重要的是,IDE开发人员也不希望这样。 因此,他们对按钮进行了反编译。
单击它之后,我们看到了大致如此的Java代码:
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; }
我们看到了我们的字段,getter,带有两个参数名称和上下文的预期构造函数,一切正常。 在下面,我们看到了第二个构造函数,这里有一个意外的签名:不是带有一个参数,而是出于某种原因带有四个。
在这里您可能会感到尴尬,但您可以爬得更深一些,四处逛逛。 开始理解后,我们将了解DefaultConstructorMarker是Kotlin标准库中的私有类,在此处添加了该类,以免与我们的书面构造函数产生冲突,因为我们无法用手设置DefaultConstructorMarker类型的参数。 关于int var3的有趣之处在于我们应该使用哪些默认值的位掩码。 在这种情况下,如果位掩码匹配两者,我们知道未设置var2,未设置属性,并且使用默认值。
我们如何解决这种情况? 为此,我已经谈到了该软件包中的一个神奇的注解@JvmOverloads。 我们必须将其挂在构造函数上。
data class Event @JvmOverloads constructor( val name: String, val context: Map<String, Any> = emptyMap() )
她会怎么做? 让我们转向相同的工具。 现在,我们看到了完整的构造函数,以及带有DefaultConstructorMarker的构造函数,瞧瞧,只有一个参数的构造函数,现在可以从Java中获得:
@JvmOverloads public Event(@NotNull String name) { this.name, (Map)null, 2, (DefaultConstructorMarker)null); }
而且,正如您所看到的,他将所有带有默认参数的工作委托给带有位掩码的构造函数。 因此,我们不会产生有关需要在此放置什么默认值的信息,我们只是将所有内容委托给一个构造函数。 真好 我们检查一下从Java方面得到的结果:编译器很高兴,并且没有感到愤怒。
接下来,让我们看看我们不喜欢的东西。 我们不喜欢这种实例,它在IDEA中是紫色的老茧。 我不喜欢紫色:)

事实证明,让我们检查一下。 让我们再次看一下字节码。
例如,我们突出显示init函数,并确保init的生成确实不是静态的。

也就是说,无论怎么说,我们都需要使用此类的实例并在其上调用这些方法。 但是我们可以强制所有这些方法的生成都是静态的。 有一个很棒的注释@JvmStatic。 让我们将其添加到init并发送函数,然后检查编译器对此的看法。
我们看到static关键字已添加到public final init(),并且使我们免于使用INSTANCE。 我们将在Java代码中对此进行验证。
编译器现在告诉我们,我们正在从INSTANCE上下文中调用静态方法。 可以更正此问题:按Alt + Enter,选择“清理代码”,瞧,INSTANCE消失了,一切看起来都和Kotlin一样:
Analytics.send(new Event("only_name_event"));
现在,我们有了使用静态方法的方案。 在我们需要的地方添加此注释:

并评论:如果我们拥有的方法显然是实例方法,那么例如具有属性的对象,并不是一切都那么明显。 字段本身(例如插件)是静态生成的。 但是,getter和setter只能作为实例方法使用。 因此,对于属性,还需要添加此批注以将设置程序和获取程序设置为静态。 例如,我们看到isInited变量,向其添加@JvmStatic批注,现在我们在Kotlin字节码查看器中看到isInited()方法已变为静态,一切都很好。
现在,让我们转到Java代码,进行“清理”,除分号和“新”一词外,其他一切看起来都像Kotlin,好吧,您将不会摆脱它们。
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());
下一步:我们看到这个笨拙的getHasPlugins getter同时带有两个前缀。 当然,我不是英语的鉴赏家,但在我看来,这里暗示着其他含义。 为什么会这样呢?
正如他们与Kotlin紧密了解的那样,根据JavaBeans规则生成用于getter和setter的属性名称。 这意味着,getter通常带有get前缀,setter带有设置前缀。 但是有一个例外:如果您有一个布尔字段,并且其名称的前缀为is,则该getter的前缀为is。 在上面的isInited字段的示例中可以看到。
不幸的是,布尔值字段并非总是应该通过is来调用。 isPlugins不能完全满足我们希望按名称显示的语义。 我们怎么样
对于我们来说并不难,因为这里有我们自己的注释(如您已经理解的那样,我今天经常会重复此说明)。 @JvmName批注允许您指定我们想要的任何名称(Java自然支持)。 添加:
@JvmStatic val hasPlugins @JvmName("hasPlugin") get() = plugins.isNotEmpty()
让我们检查一下Java语言:getHasPlugins方法不再存在,但是hasPlugins本身就是一件不错的事情。 这再次用一个注释解决了我们的问题。 现在我们解决所有注释!
如您所见,这里我们将标注直接放在吸气剂上。 这是什么原因呢? 由于该属性下有很多东西,因此尚不清楚@JvmName适用于什么。 如果将注释转移到val hasPlugins本身,则编译器将无法理解将其应用于什么。
但是,Kotlin还可以指定在其中正确使用注释的位置。 您可以指定目标获取器,整个文件,参数,委托,字段,属性,接收器扩展功能,设置器和设置器参数。 在我们的例子中,getter很有趣。 而且,如果您这样做,则其效果与将批注悬挂在get上的效果相同:
@get:JvmName("hasPlugins") @JvmStatic val hasPlugins get() = plugins.isNotEmpty()
因此,如果您没有自定义的吸气剂,则可以将其直接附加到属性上,一切都会好起来的。
让我们感到困惑的下一点是“ Analytics.INSTANCE.getEMPTY_PLUGIN()”。 在这里,这个问题甚至不再是英语的,而仅仅是:为什么? 答案大致相同,但首先是一个简短的介绍。
为了使字段恒定,您有两种方法。 如果将常量定义为原始类型或String以及对象内部,则可以使用const关键字,然后将不生成getter-setter和其他内容。 这将是一个普通的常数-私有的final静态-并将被内联,即绝对是Java的东西。
但是,如果要从与字符串不同的对象中创建一个常量,则将无法使用const这个词。 根据它,这里有val EMPTY_PLUGIN = EmptyPlugin(),显然产生了可怕的吸气剂。 我们可以使用注解重命名@JvmName,删除此get前缀,但仍保留使用方括号的方法。 因此,旧的解决方案将不起作用,我们正在寻找新的解决方案。
为此,这里有一个@JvmField注释,它表示:“我不想在这里使用吸气剂,我不想使用二传手,请让我成为一个领域。” 将其放在val EMPTY_PLUGIN前面,并验证所有内容是否正确。

Kotlin字节码查看器显示了您当前在文件中站立的突出显示的片段。 现在,我们站在EMPTY_PLUGIN上,您会看到这里在构造函数中编写了某种初始化。 事实是,吸气剂不再存在,并且只能用于记录。 而且,如果单击反编译,我们会看到“公共静态最终EmptyPlugin EMPTY_PLUGIN”已出现,这正是我们所实现的。 真好 我们检查是否一切都令所有人满意,尤其是使编译器满意。 您需要安抚的最重要的事情是编译器。
泛型
让我们从代码中休息一下,看看泛型。 这是一个非常热门的话题。 还是滑溜溜的,谁再也不想那样了。 Java有其自身的复杂性,但Kotlin是不同的。 首先,我们关注变化。 这是什么
可变性是将有关类型层次结构的信息从基本类型传递到派生类(例如到容器或泛型)的一种方式。 在这里,我们有Animal和Dog类,它们之间有着非常明显的联系:Dog是一个子类型,Animal是一个子类型,箭头来自该子类型。

它们的派生有什么联系? 让我们看一些情况。
第一个是Iterator。 为了确定什么是子类型,什么是子类型,我们将遵循替换规则Barbara Liskov。 它可以表述为:“该子类型应该不需要更多,而提供更多。”
在我们的情况下,Iterator唯一要做的就是为我们提供键入的对象,例如Animal。 如果我们在某处接受Iterator,则可以将Iterator放在其中,并从next()方法获取Animal,因为狗也是Animal。 我们提供的不是更少,而是更多,因为狗是亚型。

我重复一遍:我们仅从这种类型读取数据,因此,此处保留了类型与子类型之间的关系。 这种类型称为协变。
另一种情况:动作。 Action是一个不返回任何内容,使用一个参数的函数,而我们只写Action,也就是说,它从我们那儿取走了一只狗或动物。

因此,这里我们不再提供,而是需求,我们不再需要。 这意味着我们的依赖性正在改变。 “没有更多”,我们有动物(动物少于狗)。 这些类型称为逆变。
第三种情况-例如,ArrayList,我们从中读取和写入。 因此,在这种情况下,我们违反了其中一项规则,我们需要更多的记录(狗,而不是动物)。 这样的类型没有任何关系,它们被称为不变式。

因此,在Java中,当它是在1.5版之前设计的(泛型出现的地方)时,默认情况下,它们使数组协变。 这意味着您可以将字符串数组分配给对象数组,然后将其传递到需要对象数组的方法的某个位置,然后尝试将对象推入那里,尽管这是字符串数组。 一切都会落到你身上。
从痛苦的经验中得知,这是不可能完成的,因此在设计泛型时,他们决定“我们将使集合保持不变,我们将对其不做任何事情”。
最后,事实证明,在这种看似显而易见的事情中,一切都应该没事,但实际上却没事:
但是,我们需要以某种方式确定毕竟可以做到的:如果只从这张纸上阅读,为什么不可以在此处转移狗的清单呢? 因此,可以使用通配符来描述此类型将具有哪种变化:
List<Dog> dogs = new ArrayList<>(); List<? extends Animal> animals = dogs;
如您所见,这种变化在使用地点(我们指定了狗)中指明。 因此,这称为使用地点差异。
这有什么缺点? 不利的一面是,无论您在哪里使用API,都必须指定这些令人恐惧的通配符,这在代码中非常有用。 但是在Kotlin中,由于某种原因,这种事情开箱即用,并且您不需要指定任何内容:
val dogs: List<Dog> = ArrayList() val animals: List<Animal> = dogs
这是什么原因呢? 实际上,床单是不同的。 Java中的List表示写作,而在Kotlin中则是只读的,并非暗示。 因此,原则上我们可以立即说我们只是从这里阅读,因此我们可以协变。 这是在类型声明中精确设置的,用out关键字替换了通配符:
interface List<out E> : Collection<E>
这称为声明站点差异。 因此,我们将所有内容都显示在一个地方,并且在使用它的地方,我们不再涉及这个主题。 这是nishtyak。
返回代码
让我们回到深处。 这里我们有addPlugins方法,它需要一个List:
@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());
由于Kotlin中的List是协变的,因此我们可以在此处轻松传递插件继承人的列表。 一切正常,编译器不在乎。 但是,由于我们在声明所有内容的地方都有一个声明位置差异,因此在使用阶段就无法控制与Java的连接。 但是,如果我们真的要在那里有一个插件表,而又不想有任何继承人,会发生什么呢? 没有修饰符,但是呢? 是的,有一个注释。 注释称为@JvmSuppressWildcards,也就是说,默认情况下,我们认为这是带通配符的类型,该类型是协变的。
@JvmStatic fun addPlugins(plugs: List<@JvmSuppressWildcards Plugin>) { plugs.forEach { addPlugin(it) } }
说到抑制通配符,我们抑制了所有这些问题,并且签名实际上发生了变化。 更重要的是,我将向您展示字节码中的所有内容:

现在,我将从代码中删除注释。 这是我们的方法。 您可能知道类型擦除存在。 而且在您的字节码中,没有关于一般类属问题的信息。 但是编译器会遵循此规则,并在字节码的注释中对其进行签名:这是带有问题的类型。

现在,我们再次插入注释,然后看到这就是我们的类型而无需质疑。

现在,我们之前的代码将完全停止编译,因为我们砍掉了通配符。 您可以自己看到。
我们找出了协变类型。 现在情况正好相反。
我们认为List有一个问题。
很显然,假设此工作表从getPlugins返回时,也会出现问题。这是什么意思?这意味着我们将无法对其进行写入,因为类型是协变的,而不是协变的。让我们看一下Java中正在发生的事情。 final List<Plugin> plugins = Analytics.getPlugins(); displayPlugins(plugins); Analytics.getPlugins().add(new EmptyPlugin());
没有人会为最后一行写东西而感到愤怒,这意味着这里有人错了。如果我们查看字节码,我们将确信我们的怀疑的准确性。我们没有挂断任何注释,并且出于某种原因毫无疑问地挂起了类型。惊喜基于此。 Kotlin假定自己是一种实用的语言,因此在设计所有这些语言时,都收集了统计信息,因为Java中通常使用通配符。事实证明,输入是最常允许的方差,即使类型协变。好吧,这对我们希望List能够在其中放置任何Plugin继承人的页面很有用。相反,在这里我们要返回的地方是纯类型:因为有一个Plugin工作表,它将被返回。这里我们直接看到一个例子。似乎有点违反直觉,但它在代码中生成的精彩注释就不那么多了,因为这是最常见的用例,并且,如果您不使用任何笑话,那么一切都将立即可用。但是在这种情况下,我们发现这种情况不适合我们,因为我们不希望在那里记录任何东西。而且我们也不希望Java能够做到这一点。在Kotlin中,这里的List是一种只读类型,我们无法在其中编写任何内容,但是我们的库的客户端来自Java,并将其中的所有内容塞满了-谁愿意呢?因此,我们将强制此方法返回带通配符的List。我们可以弄清楚如何做。添加我们说的@JvmWildcard批注:为我们生成一个带有问题的类型,一切都非常简单。现在,让我们看看在这个地方Java会发生什么。 Java说“你在做什么?”:
在这里,我们甚至可以强制转换为正确的列表<?扩展Plugin>,但她仍然说“你在做什么?” 而且,从原则上讲,这种情况到目前为止适合我们。但是有一个脚本小孩说:“我看到了源代码,它是一个开放源代码,我知道这里有一个ArrayList,我会砍死你。” 一切都会正常,因为确实存在ArrayList,而且他知道可以在其中写入什么内容。 ((ArrayList<Plugin>) Analytics.getPlugins()).add(new EmptyPlugin());
因此,当然要保留很酷的注释,但是您仍然需要使用防御性复制,而防御性复制早已为人所知。Soryan,如果没有脚本小子,请不要打扰您。 @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中,没有检查过的异常。我们在文档中说:您可以在这里扔。好吧,我们扔,扔,扔。但是由于某些原因,Java不喜欢它。说:“但是由于某种原因,投掷不是您的签名,先生”:
但是我怎么能在这里添加一些东西,在这里,科特林?好吧,您知道答案了……有一个@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编写的插件对所有信息都感到满意,该插件提供了有关异常的信息。一切正常,编译。原则上,这几乎是带有注释的所有内容,但是在使用@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) }) }
假设在Java中我们不在乎,请删除注释。错误,现在IDE在两个函数中都显示错误。您认为这是什么原因?是的,没有注释,它们是用相同的名称生成的,但是这里写成一个在列表上,另一个在列表上。是的,键入擦除。我们甚至可以检查这种情况:
就我所知,您已经知道所有顶级函数都是在静态上下文中生成的。如果没有此注释,我们将尝试从List生成printReversedSum,并且在List的另一个下方。因为Kotlin编译器了解泛型,但Java字节码却不了解。因此,这是唯一的情况,不需要Kotlin.jvm包中的注释来使Java变得更好和方便,而不是Java既方便又方便。我们设置了一个新名称-一旦我们使用字符串,然后使用串联-一切正常,现在一切都可以编译了。和第二个用户案例。与此相关。我们有反向扩展功能。 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) }
此反向编译为称为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")
现在,Java编译器不喜欢ReverserKt,但是可以预期,我们将其替换为ReverserUtils,每个人都很高兴。当您想在一个类,一个facade下的一个类下收集多个顶级文件方法时,这种“用户案例2.1”就很常见。例如,关于您,您不希望从SumsKt调用上述sums.kt的方法,但是您希望这一切都是关于从ReverserUtils进行反转和抽动。然后,我们在其中添加这个奇妙的@JvmName批注,并写上“ ReverserUtils”,原则上一切正常,您甚至可以尝试编译该东西,但是没有。尽管环境不会事先警告您,但是当您尝试进行编译时,他们会告诉我们“您想在同一个程序包中使用相同的名称ata生成两个类”。需要做什么?在此程序包中添加最后一个注释@JvmMultifileClass,它表示多个文件的内容将变成一个类,也就是说,此文件只有一个外观。在这两种情况下,我们都添加“ @file:JvmMultifileClass”,并且您可以用ReverserUtils替换SumsKt,每个人都很高兴-相信我。完成注释!我们与您讨论了此软件包以及所有注释。原则上,从它们的名称中已经很清楚了它们各自的用途。在某些棘手的情况下,例如,@ JvmName在Kotlin中甚至很容易使用。Kotlin特有的
但这很可能不是您想知道的全部。同样重要的是要注意如何使用Kotlin特定的东西。例如,内联函数。它们在Kotlin中内联,并且似乎可以从Java中以字节码访问它们吗?事实证明,它们会,一切都会好起来,并且这些方法实际上可用于Java。虽然,例如,如果您编写仅Kotlin的项目,但这不会很好地影响您的dex数量限制。因为在Kotlin中不需要它们,但实际上它们将以字节码显示。接下来,请注意Reified类型参数。此类参数特定于Kotlin,它们仅适用于内联函数,并允许您通过反射来逆转Java中不可用的hack。由于这仅是Kotlin的东西,因此它仅适用于Kotlin,不幸的是,在Java中,您不能使用带有已整形的函数。java.lang.Class。如果我们想反映一点,并且我们的库也适用于Java,则需要对其进行支持。让我们来看一个例子。我们有了这样的“我们的改造”,很快就写在了我的膝盖上(我不明白这些人写了这么长时间): 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类,有一种方法适用于Kotlin KClass,您不需要执行两种不同的实现,可以使用扩展属性从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,那么它将很好地编译为在Java中返回void的函数,反之亦然。您可以与此互换使用。但这一切都在lambda开始返回单位的地方结束。如果有人使用Scala,则在Scala中会有一个回车和一个小的返回一定值的接口的购物车,以及同一回车和一个不返回任何值的接口的购物车,即是无效的。但是在科特林,事实并非如此。Kotlin只有22个接口,它们接受一组不同的参数并返回某些内容。因此,返回Unit的lambda不会返回void,而是返回Unit。这就施加了局限性。返回Unit的lambda是什么样的?现在,在此代码片段中查看她。互相认识。 inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) }
从Kotlin使用它:一切都很好,我们甚至使用了方法引用,如果可以的话,并且它读起来很完美,我们的眼睛并不冷漠。 private fun useMisc() { listOf(1, 2, 3, 4).forEachReversed(::println) println(reversedClassName<String>()) }
Java中发生了什么?在Java中,发生以下独木舟: 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。在我们使用注释进行所有操作之后,这可能真的是第一眼。不幸的是,您将不得不从此处返回Unit实例。无论如何,您都可以为null,没有人需要它。我的意思是,没有人需要返回值。让我们更进一步:Typealiases也是一个相当具体的东西,它只是别名或同义词,只能从Kotlin获得,而且不幸的是,在Java中,您将使用这些别名下的内容。这要么是三次封闭的泛型垃圾,要么是某种嵌套类。 Java程序员已经习惯了这一点。现在介绍有趣的部分:可见性。更确切地说,是内部可见性。您可能知道,在Kotlin中没有私有的包,如果您编写时不带任何修饰符,它将是公共的。但是有内部的。内部是一件棘手的事情,我们现在甚至要看一下。在翻新中,我们有一个内部验证方法。 internal fun validate(): Retrofit { println("!!!!!! internal fun validate() was called !!!!!!") return this }
不能从Kotlin调用它,这是可以理解的。Java发生了什么?我们可以打电话验证吗?内部公开化对您来说也许不是秘密。如果您不相信我,请相信Kotlin字节码查看器。
这确实是公开的,但是签名如此糟糕,以至于暗示一个人可能并不完全认为这种蠕变会爬进公共API。如果有人格式化了80个字符,则此方法甚至可能不适合一行。在Java中,我们现在有: final Api api = retrofit .validate$production_sources_for_module_library_main() .create(Api.class); api.sendMessage("Hello from Java"); }
让我们尝试编译这种情况。因此,至少它不会编译,还不错。我们可以在这里停止,但是让我向您解释一下。如果我这样做怎么办? final Api api = retrofit .validate$library() .create(Api.class); api.sendMessage("Hello from Java"); }
然后编译。于是出现了一个问题,“为什么?”我能说什么...魔术!因此,非常重要的一点是,如果将一些重要的内容粘贴到内部,这是不好的,因为它将泄漏到您的公共API中。而且,如果脚本小子配备了Kotlin字节码查看器,那就不好了。在具有内部可见性的方法中,请勿使用任何非常重要的内容。如果您想要更多的快乐,那么我建议您做两件事。为了更方便地使用字节码并阅读它,我建议您从Zhenya Vartanov那里获得一份报告,其中提供了一个免费视频,尽管该视频来自SkillsMatter事件。很酷还有一个很老的系列Christophe Bales撰写的三篇文章中,介绍了Kotlin的不同功能。一切都写在这里很酷,有些东西现在无关紧要,但总的来说,它非常可理解。Kotlin字节码查看器和所有这些都一样。谢谢你
如果您喜欢此报告,请注意:12月8日至9日,新的Mobius将在莫斯科举行,那里还将有很多有趣的事情。有关该程序的已知信息位于该站点上,可以在该站点上购买票。