
各位 Android 彭于晏,周五好!
前几天,我和同事吃饭的时候,他突然问我:你知道 Kotlin 那个 internal 是咋回事儿么?
我:当然知道,不过在说这个之前,我们应该都复习一下其他的可见性修饰符,你等我,周五给你出文章!
Kotlin 常见修饰符
可见性修饰符决定了程序中类、对象、接口、函数和属性的可访问范围。
Kotlin 一共提供四种可见性修饰符:public、private、protected 和 internal。它们各自服务于不同场景,为代码元素的访问边界提供了清晰定义。
默认情况下,如果一个声明没有显式写出修饰符,它会被视为 public。
Kotlin 社区对于"默认修饰符是 public"这一设计一直颇有微词,很多人认为应该主动声明你要暴露的接口,而非默认全部暴露。
Public
public 修饰符表示这个声明可以从程序中的任何地方访问。这也是所有顶层声明和成员声明的默认可见性。例如:
kotlin
class PublicExample {
public fun greet() = "Hello, skydoves!"
}
这里的 greet 函数可以被任意其他类或文件访问。
Private
private 修饰符会把访问范围限制在同一个文件内(对于顶层声明),或者限制在其所在类或对象内部(对于成员声明)。它非常适合用来做封装。
kotlin
class PrivateExample {
private val secret = "Hidden from the outside"
fun revealSecret() = secret
}
在这个例子里,secret 属性只能在 PrivateExample 类内部访问。
Protected
protected 与 private 很像,但它允许子类访问成员。它只能用于类成员,不能用于顶层声明。
kotlin
open class Parent {
protected fun protectedMethod() = "Accessible in subclasses"
}
class Child : Parent() {
fun useProtected() = protectedMethod()
}
这里的 protectedMethod 可以在 Child 类内部访问,但不能在整个类继承体系之外访问。
Internal
internal 修饰符会把声明的访问范围限制在同一个模块内。这很适合在模块内部共享实现,同时阻止其他模块从外部直接访问。
kotlin
internal class InternalExample {
internal fun internalFunction() = "Available within the module"
}
internalFunction 可以在同一模块中的任意位置访问,但不能被其他模块访问。
在实际的 Android 开发中,一个模块通常对应着你定义的一个 Gradle module。可以这么理解:internal 对于模块内部相当于 public,而对于模块外部相当于 private。
总结
Kotlin 的可见性修饰符 public、private、protected 和 internal,为代码元素的访问控制提供了非常细致的粒度。
声明默认是 public,但你可以根据设计需求使用其他修饰符来进行封装或限制访问。合理使用这些修饰符,有助于提升代码的可读性、安全性以及模块化程度。
进阶: internal 到底怎么实现的
internal 是 Kotlin 在封装层面非常实用的一个特性。各位彭于晏一定好奇,internal 是如何工作的?
因为 Kotlin 最终会编译成 JVM 字节码,而 JVM 层面上并没有 internal 这个级别的可见性,那么它到底是怎么实现的呢?
internal 表示某个类、函数或属性在同一模块内是公开的,但对于其他模块中的代码来说则是不可见、不可访问的。
这种机制特别适合把代码共享给库的内部实现使用,同时又不把它暴露为库的公共 API。
更准确地说,一个模块是一起编译的一组 Kotlin 文件,例如:
- 一个 IntelliJ IDEA 模块。
- 一个 Maven 项目。
- 一个 Gradle source set(例外是 test source set 可以访问 main 中的
internal声明)。 - 一组通过一次
<kotlinc>Ant 任务调用完成编译的文件。
我们知道,JVM 本身并没有"模块可见性"这个概念。它只认识 public、protected、private 和包私有(默认)这几种访问级别。
为了实现 internal,Kotlin 编译器采用了一种很巧妙的方式:它会把 internal 声明编译成 public,但同时给它们一个经过改写的名称。
先看一个简单的 Kotlin 模块示例(比如一个库),其中包含一个 internal 函数。
kotlin
// 这个函数在当前模块内是公开的,但不应该被
// 使用该库的其他模块访问。
internal fun aUsefulInternalFunction() {
println("This is an internal utility function.")
}
// 一个会使用 internal 函数的 public 类。
class PublicApi {
fun doWork() {
// 这个调用是合法的,因为它发生在同一个模块中。
aUsefulInternalFunction()
}
}
当这个模块被编译成 .jar 文件之后,如果我们去查看它的字节码(或者把它反编译成 Java),就会看到一个很有意思的变化。
java
import kotlin.Metadata;
// 这个文件会被编译成一个带有 'Kt' 后缀的类。
public final class MyLibraryKt {
// 1. 该函数会被编译为 'public'。
// 2. 函数名会发生改写,模块名会被追加到后面。
public static final void aUsefulInternalFunction$MyLibrary_module() {
String message = "This is an internal utility function.";
System.out.println(message);
}
}
// PublicApi 类的字节码会显示它调用的是改写后的名字。
public final class PublicApi {
public final void doWork() {
MyLibraryKt.aUsefulInternalFunction$MyLibrary_module();
}
}
-
它会变成 public :在字节码层面,
internal函数实际上会被声明为public,这是一种妥协。因为同一模块中的代码即便分布在不同的包(package)里,也必须能够相互访问。在 JVM 支持的访问级别中,只有public能确保这种跨包的可见性,尤其是在单个编译产物(如.jar文件)内部。 -
名称改写(Name Mangling) :这才是"隐藏"机制的核心。Kotlin 编译器会给原始函数名追加一个后缀,通常格式是
$[module_name]。在上面的例子中,假设模块名为MyLibrary_module,那么函数aUsefulInternalFunction最终就会变成aUsefulInternalFunction$MyLibrary_module。
这种 public + 改写后名称 的策略,会通过两种方式来落实 internal 可见性:
-
对于 Java 使用者 :一个 Java 开发者想要使用这个库时,通常不会轻易调用到这个函数。因为他们在 IDE 的自动补全里看不到
aUsefulInternalFunction()。虽然理论上可以手动调用MyLibraryKt.aUsefulInternalFunction$MyLibrary_module(),但这个改写后的名称已经非常明确地告诉你:这是一段内部的、不稳定的 API,不应该被依赖。换句话说,它已经被有效地"隐藏"在正常使用路径之外。 -
对于 Kotlin 使用者(位于另一个模块中) :这里体现的是 Kotlin 编译器本身的语义约束能力。当你在另一个 Kotlin 模块中尝试使用这个库时,编译器会从源码或者
.jar中的元数据里得知这个声明原本带有internal修饰符。如果你写下aUsefulInternalFunction(),编译器会立刻发现它属于另一个模块的 internal 声明,并直接给出一个编译期错误,提示这个函数不可访问。它甚至不会尝试去把它解析成那个改写后的名字。
这是一套非常漂亮的双重策略:一方面,Kotlin 编译器会对其他 Kotlin 模块严格执行 internal 规则;另一方面,名称改写又为 Java 和其他 JVM 语言树起了一个明显的"请勿使用"标志。
同样的原理也适用于 internal 类。它在字节码层面会被编译成一个 public 类,而它的 internal 方法在必要时也可能通过名称改写来避免冲突。不过 Kotlin 编译器依然会阻止其他模块中的代码访问这个 public 类。
一点想法
关于 internal 修饰符,在日常的 Android/Kotlin 开发中,我有以下几点体会:
-
SDK 与开源库开发的利器
在开发提供给第三方使用的 SDK 或基础组件库时,
internal是最完美的封装工具。我们可以自由地在模块内部拆分架构、提取公共基类和工具类,而完全不用担心它们会"污染"对外的 Public API。对外暴露的 API 越少,你的维护成本就越低,向后兼容的压力也就越小。 -
多模块架构(Modularization)的最佳实践
在现代 Android 提倡的多模块/组件化架构中,
internal能强迫我们思考模块的边界。写下新类或函数时,默认给它加上internal(甚至是private)是一个非常好的习惯。这能有效防止其他模块产生不必要的隐式依赖,当你想重构模块内部逻辑时,不用再满世界搜索还有谁调用了这个方法。 -
对 Java 防御的终极手段(@JvmSynthetic)
虽然 Kotlin 编译器的 Name Mangling 已经让 Java 调用变得非常丑陋,达到了"防君子不防小人"的目的,但 Java 依然可以强行调用改名后的方法。如果你开发的是一个纯 Kotlin 库,并且绝对不想让 Java 代码调用某个内部方法,可以结合
@JvmSynthetic注解使用,这会让该声明在 Java 源码层面彻底隐身,连调用的可能都不给。
合理使用 internal,你的架构会变得更加内聚和清爽。