从 internal 修饰符一探 kotlin 的可见性控制

简介: 从 internal 修饰符一探 kotlin 的可见性控制

前言

之前探讨过的 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 一样命名的 defaultpublicprotectedprivate修饰符。虽然叫法相同,但在可见性限制的具体细节上存在这样那样的区别。

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 进行对比,便于直观了解。

1672193130177.png

参考资料

https://kotlinlang.org/docs/java-to-kotlin-interop.html#visibility

https://medium.com/@HugoMatilla/kotlin-basics-visibility-modifiers-public-internal-protected-and-private-c3bf972aee11

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


相关文章
|
18天前
|
Java Kotlin
Kotlin教程笔记(13) - 类及成员的可见性
Kotlin教程笔记(13) - 类及成员的可见性
32 3
|
23天前
|
Java Kotlin
​ Kotlin教程笔记(13) - 类及成员的可见性
​ Kotlin教程笔记(13) - 类及成员的可见性
|
1月前
|
Java Kotlin
Kotlin - 类及成员的可见性
Kotlin - 类及成员的可见性
|
1月前
|
Java Kotlin
Kotlin - 类及成员的可见性
Kotlin - 类及成员的可见性
32 5
|
1月前
|
Java Kotlin
​ Kotlin教程笔记(13) - 类及成员的可见性
​ Kotlin教程笔记(13) - 类及成员的可见性
49 5
|
2月前
|
Java Kotlin
Kotlin教程笔记(13) - 类及成员的可见性
Kotlin教程笔记(13) - 类及成员的可见性
59 4
|
2月前
|
Java Kotlin
Kotlin教程笔记(13) - 类及成员的可见性
Kotlin教程笔记(13) - 类及成员的可见性
29 0
|
Java 编译器 Kotlin
Kotlin学历之可见性修饰符
Kotlin学历之可见性修饰符
100 0
Kotlin学历之可见性修饰符
|
Java Kotlin
kotlin 类及其成员的可见性
一、首先来看一个Kotlin和Java的比较 Kotlin Java 比较 private private ...
1085 0
|
Java Kotlin 编译器
Kotlin学习(四)—— 类和对象,继承,覆盖,抽象类,属性和字段,接口,可见性修饰符,扩展
一.类和对象 1. 类 Kotlin和java的类的声明都是一样的,用class表示,比如 class TestClass { } 如果是空类的话,大括号都可以省了 2.
1674 0