从 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


相关文章
|
Java 编译器 Kotlin
Kotlin学历之可见性修饰符
Kotlin学历之可见性修饰符
85 0
Kotlin学历之可见性修饰符
|
Java Kotlin
kotlin 类及其成员的可见性
一、首先来看一个Kotlin和Java的比较 Kotlin Java 比较 private private ...
1074 0
|
Java Kotlin 编译器
Kotlin学习(四)—— 类和对象,继承,覆盖,抽象类,属性和字段,接口,可见性修饰符,扩展
一.类和对象 1. 类 Kotlin和java的类的声明都是一样的,用class表示,比如 class TestClass { } 如果是空类的话,大括号都可以省了 2.
1652 0
|
7天前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
26 4
|
1月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
67 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
27天前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
26 8
|
1月前
|
安全 Java Android开发
探索Android应用开发中的Kotlin语言
【7月更文挑战第19天】在移动应用开发的浩瀚宇宙中,Kotlin这颗新星以其简洁、安全与现代化的特性,正迅速在Android开发者之间获得青睐。从基本的语法结构到高级的编程技巧,本文将引导读者穿梭于Kotlin的世界,揭示其如何优化Android应用的开发流程并提升代码的可读性与维护性。我们将一起探究Kotlin的核心概念,包括它的数据类型、类和接口、可见性修饰符以及高阶函数等特性,并了解这些特性是如何在实际项目中得以应用的。无论你是刚入门的新手还是寻求进阶的开发者,这篇文章都将为你提供有价值的见解和实践指导。
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
1月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
99 2
|
1月前
|
Android开发 Kotlin
Android面试题之kotlin中怎么限制一个函数参数的取值范围和取值类型等
在Kotlin中,限制函数参数可通过类型系统、泛型、条件检查、数据类、密封类和注解实现。例如,使用枚举限制参数为特定值,泛型约束确保参数为Number子类,条件检查如`require`确保参数在特定范围内,数据类封装可添加验证,密封类限制为一组预定义值,注解结合第三方库如Bean Validation进行校验。
37 6