前言
之前探讨过的 sealed class 和 sealed interface 存在 module 的限制,但其主要用于密封 class 的扩展和 interface 的实现。
如果没有这个需求只需要限制 module 的话,使用 Kotlin 中独特的 internal 修饰符即可。
本文将详细阐述 internal 修饰符的特点、原理以及 Java 调用的失效问题,并以此为切入点网罗 Kotlin 中所有修饰符,同时与 Java 修饰符进行对比以加深理解。
internal 修饰符
open 修饰符
default、private 等修饰符
针对扩展函数的访问控制
Kotlin 各修饰符的总结
internal 修饰符
修饰符,modifier,用作修饰如下对象。以展示其在 module 间、package 间、file 间、class 间的可见性。
- 顶层 class、interface
- sub class、interface
- 成员:属性 + 函数
特点
internal 修饰符是 Kotlin 独有的,其在具备了 Java 中 public 修饰符特性的同时,还能做到类似包可见(package private)的限制。只不过范围更大,变成了模块可见(module private)。
首先简单看下其一些基本特点:
上面的特性可以看出来,其不能和 private 共存
Modifier ‘internal’ is incompatible with ‘private’
可以和 open 共存,但 internal 修饰符优先级更高,需要靠前书写。如果 open 在前的话会收到如下提醒:
Non-canonical modifiers order
其子类只可等同或收紧级别、但不可放宽级别,否则
‘public’ subclass exposes its ‘internal’ supertype XXX
说回其最重要的特性:模块可见,指的是 internal 修饰的对象只在相同模块内可见、其他 module 无法访问。而 module 指的是编译在一起的一套 Kotlin 文件,比如:
一个 IntelliJ IDEA 模块;
一个 Maven 项目;
一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明);
一次 <kotlinc> Ant 任务执行所编译的一套文件。
而且,在其他 module 内调用被 internal 修饰对象的话,根据修饰对象的不同类型、调用语言的不同,编译的结果或 IDE 提示亦有差异:
比如修饰对象为 class 的话,其他 module 调用时会遇到如下错误/提示
Kotlin 中调用:
Cannot access ‘xxx’: it is internal in ‘yyy.ZZZ’
Java 中调用:
Usage of Kotlin internal declaration from different module
修饰对象为成员,比如函数的话,其他 module 调用时会遇到如下错误/提示
Kotlin 中调用:
Cannot access ‘xxx’: it is internal in ‘yyy.ZZZ’(和修饰 class 的错误一样)
Java 中调用:
Cannot resolve method 'xxx’in ‘ZZZ’
你可能会发现其他 module 的 Kotlin 语言调用 internal 修饰的函数发生的错误,和修饰 class 一样。而 Java 调用的话,则是直接报找不到,没有 internal 相关的说明。
这是因为 Kotlin 针对 internal 函数名称做了优化,导致 Java 中根本找不到对方,而 Kotlin 还能找到是因为编译器做了优化。
假使将函数名称稍加修改,改为 fun$moduleName 的话,Java 中错误/提示会发生变化,和修饰 class 时一样了:
Kotlin 中调用:
Cannot access ‘xxx’: it is internal in ‘yyy.ZZZ’(仍然一样)
Java 中调用:
Usage of Kotlin internal declaration from different module
优化
前面提到了 Kotlin 会针对 internal 函数名称做优化,原因在于:
internal 声明最终会编译成 public 修饰符,如果针对其成员名称做错乱重构,可以确保其更难被 Java 语言错误调用、重载。
比如 NonInternalClass 中使用 internal 修饰的 internalFun() 在编译成 class 之后会被编译成 internalFun$test_debug()。
class NonInternalClass { internal fun internalFun() = Unit fun publicFun() = Unit } public final class NonInternalClass { public final void internalFun$test_debug() { } public final void publicFun() { } }
Java 调用的失效
前面提到 Java 中调用 internal 声明的 class 或成员时,IDE 会提示不应当调用跨 module 调用的 IDE 提示,但事实上编译是可以通过的。
这自然是因为编译到字节码里的是 public 修饰符,造成被 Java 调用的话,模块可见的限制会失效。这时候我们可以利用 Kotlin 的其他两个特性进行限制的补充:
使用 @JvmName ,给它一个 Java 写不出来的函数名
@JvmName(" zython") internal fun zython() { }
Kotlin 允许使用 ` 把一个不合法的标识符强行合法化,而 Java 无法识别这种名称
internal fun ` zython`() { }
open 修饰符
除了 internal,Kotlin 还拥有特殊的 open 修饰符。首先默认情况下 class 和成员都是具备 final 修饰符的,即无法被继承和复写。
如果显式写了 final 则会被提示没有必要:
Redundant visibility modifier
如果可以被继承或复写,需要添加 open 修饰。(当然有了 open 自然不能再写 final,两者互斥)
open 修饰符的原理也很简单,添加了则编译到 class 里即不存在 final 修饰符。
下面抛开 open、final 修饰符的这层影响,着重讲讲 Kotlin 中 default、public、protected、private 的具体细节以及和 Java 的差异。
default、private 等修饰符
除了 internal,open 和 final,Kotlin 还拥有和 Java 一样命名的 default
、public
、protected
、private
修饰符。虽然叫法相同,但在可见性限制的具体细节上存在这样那样的区别。
default
和 Java default visibility 是包可见(package private)不同的是,Kotlin 中对象的 default visibility 是随处可见(visible everywhere)。
public
就 public 修饰符的特性而言,Kotlin 和 Java 是相同的,都是随处可见。只不过 public 在 Kotlin 中是 default visibility,Java 则不是。
正因为此 Kotlin 中无需显示声明 public,否则会提示:Redundant visibility modifier。
protected
Kotlin 中 protected 修饰符和 Java 有相似的地方是可以被子类访问。但也有不同的地方,前者只能在当前 class 内访问,而 Java 则是包可见。
如下在同一个 package 并且是同一个源文件内调用 protected 成员会发生编译错误。
Cannot access ‘i’: it is protected in ‘ProtectedMemberClass’
// TestProtected.kt open class ProtectedMemberClass { protected var i = 1 } class TestProtectedOneFile { fun test() { ProtectedMemberClass().run { i = 2 } } }
private
Kotlin 中使用 private 修饰顶级类、成员、内部类的不同,visibility 的表现也不同。
当修饰成员的时候,其只在当前 class 内可见。否则提示:
“Cannot access ‘xxx’: it is private in ‘XXX’”
当修饰顶级类的时候,本 class 能看到它,当前文件也能看到,即文件可见(file private)的访问级别。事实上,private 修饰顶级对象的时候,会被编译成 package private,即和 Java 的 default 一样。
但因为 Kotlin 编译器的作用,同 package 但不同 file 是无法访问 private class 的。
Cannot access ‘XXX’: it is private in file
当修饰的非顶级类,即内部类的话,即便是同文件也无法被访问。比如下面的 test 函数可以访问 TestPrivate,但无法访问 InnerClass。
Cannot access ‘InnerClass’: it is private in ‘TestPrivate’
// TestPrivate.kt private class TestPrivate { private inner class InnerClass { private var name1 = "test" } } class TestPrivateInOneFile: TestGrammar { override fun test() { TestPrivate() TestPrivate().InnerClass() // error } }
另外一个区别是,Kotlin 中外部类无法访问内部类的 private 成员,但 Java 可以。
Cannot access ‘xxx’: it is private in ‘InnerClass’
针对扩展函数的访问控制
private 等修饰符在扩展函数上也有些需要留意的地方。
扩展函数无法访问被扩展对象的 private / protected 成员,这是可以理解的。毕竟其本质上是静态方法,其内部需要调用实例的成员,而该静态方法是脱离定义 class 的,自然不允许访问访问仅类可见的、子类可见的对象
Cannot access ‘xxx’: it is private in ‘XXX’
Cannot access ‘yyy’: it is protected in ‘XXX’
只可以针对 public 修饰的类添加 public 级别的扩展函数,否则会收到如下的错误
‘public’ member exposes its ‘private-in-file’ receiver type TestPrivate
扩展函数的原理使得其可以针对目标 class 做些处理,但变相地将文件可见、模块可见的 class 放宽了可见性是不被允许的。但如果将扩展函数定义成 private / internal 是可以通过编译的,但这个扩展函数的可用性会受到限制,需要留意。
Kotlin 各修饰符的总结
对 Kotlin 中各修饰符进行简单的总结:
default 情况下:
等同于 final,需要声明 open 才可扩展,这是和 Java 相反的扩展约束策略
等同于 public 访问级别,和 Java 默认的包可见不同
正因为此,Kotlin 中 final 和 public 无需显示声明
protected 是类可见外加子类可见,而 Java 则是包可见外加子类可见
private 修饰的内部类成员无法被外部类访问,和 Java 不同
internal 修饰符是模块可见,和 Java 默认的包可见有相似之处,也有区别
下面用表格将各修饰符和 Java 进行对比,便于直观了解。
参考资料
https://kotlinlang.org/docs/java-to-kotlin-interop.html#visibility
https://www.educba.com/kotlin-internal/
https://sebhastian.com/kotlin-internal-modifier/
https://ice1000.org/2017/11-12-KtInternalJavaTranslation.html
https://stackoverflow.com/questions/54605129/what-is-the-internal-kotlin-modifier-in-byte-code