從 internal 修飾符一探 kotlin 的可見性控制
theme: smartblue
前言
之前探討過的 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 進行對比,便於直觀瞭解。
修飾符 | Kotlin 中適用場景 | Kotlin | Java |
| ------------ | --------------- | --------------------------------------------------------------------- | ------------------------------------- |
| (default) | 隨處可見的類、成員 | = public + final | 對象包可見 |
| public | 同上 | = (default) ; 對象隨處可見; 無需顯示聲明 | 對象隨處可見 |
| protected | 自己和子類可見 | 對象類可見 + 子類可見 | 對象包可見 + 子類可見 |
| private | 自己和當前文件可見 | 修飾成員:對象類可見; 修飾頂級類:對象源文件可見; 外部類無法訪問內部類的 private 成員 | 對象類可見; 外部類可以訪問內部類的 private 成員 |
| internal
| module 內使用的類、成員 | 對象模塊可見; 子類只可等同或收緊級別、但不可放寬級別 | - |
| open
| 可擴展 | 對象可擴展; 和 final 互斥; 優先級低於 internal、protected 等修飾符 | - |
| final | 不可擴展 | = (default) ; 對象不可擴展、複寫; 無需顯示聲明 | 對象不可擴展、複寫
參考資料
- http://kotlinlang.org/docs/java-to-kotlin-interop.html#visibility
- http://medium.com/@HugoMatilla/kotlin-basics-visibility-modifiers-public-internal-protected-and-private-c3bf972aee11
- http://www.educba.com/kotlin-internal/
- http://sebhastian.com/kotlin-internal-modifier/
- http://ice1000.org/2017/11-12-KtInternalJavaTranslation.html
- http://stackoverflow.com/questions/54605129/what-is-the-internal-kotlin-modifier-in-byte-code
- 面試題:Android 中 Intent 採用了什麼設計模式?
- 終於理解~Android 模塊化裏的資源衝突
- 跳槽、換房、堅持輸出,與你分享我匆忙的 2022~
- 別搞錯了,nonTransitiveRClass 不能解決資源衝突!
- 從 internal 修飾符一探 kotlin 的可見性控制
- 一文吃透 Kotlin 中眼花繚亂的函數家族...
- 開發這麼久,gradle 和 gradlew 啥區別、怎麼選?
- 能説一説 Kotlin 中 lateinit 和 lazy 的區別嗎?
- M1 Pro 折騰一年的收穫:一堆“哇塞”的技巧和 App
- 如何打造車載語音交互:Google Voice Interaction 給你答案
- Android 車機初體驗:Auto,Automotive 傻傻分不清楚?
- 深入分析 Android 系統返回手勢的實現原理
- Android 13 返回導航大變更:返回鍵徹底廢棄 可預見型返回手勢
- 從顯示 Tap 位置的原理一探 Android Input 系統
- Android 嵌套 Intent 的隱患以及解決方案
- Android 13 針對 Intent filters 安全的再加強
- Android 13 新的換行策略和針對日文的優化
- 電子廠裏撂了挑子,我默默自學起了Android|2021 年中總結
- 鴻蒙Harmony談了這麼久,和Android到底啥區別?
- Jetpack新成員SplashScreen:打造全新的App啟動畫面