Skip to content

第 1 章 Kotlin 是什么 & 为什么这么火

学习目标:能用一句话向产品经理讲清楚「Kotlin 到底是个什么玩意儿」;能向面试官讲清楚「为啥 Google 要钦定 Kotlin 而不是继续用 Java」;理解 Kotlin 三大杀手锏 —— 空安全、扩展函数、协程 —— 各自解决了什么痛点。


1.1 一段「家谱」:Kotlin 从哪来

1.1.1 编程语言简史(30 秒版本)

1995 ── Java 1.0 (Sun Microsystems)         ┐
2000 ── C# 1.0  (Microsoft 仿 Java)          │
2005 ── Scala 1.0 (函数式 + 面向对象)         │  Java 的"竞品"陆续登场
2007 ── Clojure 1.0 (Lisp on JVM)            │
2009 ── Go 1.0 (Google)                      │
2011 ── ★ Kotlin 项目启动 (JetBrains)         ┘
2016 ── Kotlin 1.0 正式发布
2017 ── 🎉 Google I/O 宣布 Kotlin 为 Android 一等公民
2019 ── Google 进一步宣布 "Kotlin First"(新 API 优先 Kotlin 写)
2020 ── kotlinx.coroutines 1.4 协程稳定
2023 ── Kotlin 1.9,K2 编译器进入 Beta
2024 ── Kotlin 2.0 发布,K2 编译器正式版(编译速度提升 ~2 倍)

1.1.2 为什么 JetBrains 要造一门新语言?

故事很有意思。JetBrains 是写 IntelliJ IDEA 的那家公司,他们自己内部用 Java 写产品,写得头很大,痛点 4 条:

   ┌────────────────────────────────────────────────────────┐
   │  Java 的 4 个痛                                         │
   ├────────────────────────────────────────────────────────┤
   │  1. NullPointerException 满天飞,号称"十亿美元错误"        │
   │  2. 一个简单的 POJO 要写 50 行 getter/setter/equals      │
   │  3. 缺少函数式 API(Java 8 才有 Stream,太晚了)           │
   │  4. 异步代码靠回调地狱(Thread/Future/CompletableFuture) │
   └────────────────────────────────────────────────────────┘

他们调研了一圈:

  • Scala:太复杂了,编译慢得离谱(团队"看着进度条上厕所")
  • Groovy:动态类型,IDE 自动补全的体验差
  • Clojure:Lisp 风格,团队学习曲线太陡
  • C#:好是好,但被 .NET 绑死

最终结论是 "那不如自己造一个"。2010 年立项,2011 年公开命名为 Kotlin(取自俄罗斯圣彼得堡附近的一座岛 Kotlin Island —— 跟 Java = 印尼爪哇岛对应)。

1.1.3 Google 为什么钦定它?

2017 年 Google I/O 大会震惊业界 —— 安卓官方第一语言不再独尊 Java,Kotlin 成为一等公民。这背后有两个原因:

   原因 1:Oracle 起诉 Google 用 Java API 侵权
   ──────────────────────────────────────────────
   2010 年 Oracle 收购 Sun(即 Java 的所有者),随即起诉 Google
   "Android 用 Java API 没付授权费"。这场官司打了 11 年(直到
   2021 年 Google 才胜诉)。Google 急需一个"Plan B"语言。

   原因 2:Java 在 Android 上更新太慢
   ──────────────────────────────────────────────
   Android 长期卡在 Java 7 / Java 8 部分特性,没法用现代 Java。
   而 Kotlin 由 JetBrains 维护,可以快速演进、立即可用。

于是 Kotlin 一举成为「Android 官方推荐 + JVM 后端可用 + 跨平台 KMP」的多面手。


1.2 Kotlin 是什么:一句话讲清

Kotlin 是一门运行在 JVM 上的、静态类型的、跨平台的、现代化的、跟 Java 100% 互操作的通用编程语言。

这句话信息量很大,逐词拆开看:

含义通俗类比
运行在 JVM 上编译成 .class 字节码跟 Java 共用同一台"虚拟机",所以能复用整个 Java 生态
静态类型编译期就检查类型写代码时 IDE 能秒标红错误,不像 Python 要等运行时
跨平台JVM / Android / JS / Native / Wasm同一套代码可以跑在多个平台
现代化函数式 + 面向对象 + null 安全 + 协程Java 的"豪华增强版"
跟 Java 互操作Kotlin 调 Java 不用任何转换;反之亦然老项目可以一行行渐进迁移
通用编程语言不限领域后端、Android、CLI、脚本、数据处理都能写

1.2.1 它是怎么跑起来的?

                 ┌──────────────┐    kotlinc       ┌──────────────┐
                 │   .kt 源码    │ ───────────────► │  .class 字节码 │
                 └──────────────┘   (Kotlin 编译器) └───────┬──────┘

                                                       JVM  ▼
                                                  ┌──────────────┐
                                                  │  运行时执行   │
                                                  └──────────────┘
   Kotlin Multiplatform 的另两条腿:
   .kt ─► Kotlin/JS    ─► .js   ─► 浏览器
   .kt ─► Kotlin/Native ─► .exe  ─► macOS / Linux / iOS / Windows

关键事实:Kotlin 编译出来的 .class 文件跟 Java 编译出来的 .class 文件长得几乎一样,运行时它们就是同一种字节码,所以"互操作"是天然的,不是后期勉强适配的。


1.3 Kotlin 三大杀手锏

讲了那么多背景,我们来看 Kotlin 真正"打动人心"的 3 个特性。这也是面试官最爱问的"为什么 Kotlin 比 Java 好"。

1.3.1 杀手锏 1:Null 安全 —— 干掉 NPE

痛点:Java 的 NullPointerException

java
// Java 代码 —— 看起来人畜无害
String name = user.getProfile().getName();
System.out.println(name.length());

// 运行时如果 user 是 null 或 profile 是 null,就 💥 NullPointerException
// 这种 bug 编译期完全发现不了,全靠测试 + 线上炸 + 哭

Kotlin 怎么救你

Kotlin 把「这个值会不会是 null写进了类型系统

kotlin
// 这样写 —— 编译器拒绝传 null 给 name
val name: String = "Alice"
// name = null  // ❌ 编译报错

// 想要可空?类型后面加个问号
val nullableName: String? = null  // ✅ OK,类型变成 String?

// 用可空类型时,编译器强制你处理 null 的情况
val length = nullableName.length         // ❌ 编译报错:你不能直接点
val length = nullableName?.length         // ✅ 安全调用:null 时返回 null
val length = nullableName?.length ?: 0    // ✅ Elvis 操作符:null 时给默认值
val length = nullableName!!.length        // ⚠️ 强制非空:null 时抛异常(自杀式)

📌 生活化类比:Java 的所有变量都是「可能没装东西的快递盒」,每次拆都要心惊胆战;Kotlin 的盒子分两种 —— 保证装了东西的(String可能没装的(String?,盒子上贴了标签,拆错了编译期就报错,不用等运行时翻车。

1.3.2 杀手锏 2:扩展函数 —— 给别人的类加方法

痛点:Java 的 StringUtils.isBlank()

java
// Java —— 想给 String 加个 toCamelCase() 方法?
// 类是 java.lang.String,你又改不了,只能写一个工具类:
public class StringUtils {
    public static String toCamelCase(String s) { /* ... */ }
}

// 调用起来像这样,丑得要死:
String result = StringUtils.toCamelCase(input);

Kotlin 怎么救你

kotlin
// 直接给 String 类"扩展"一个方法
fun String.toCamelCase(): String {
    return this.split("_").joinToString("") { it.replaceFirstChar { c -> c.uppercase() } }
}

// 调用起来跟内置方法一模一样:
val result = "hello_kotlin_world".toCamelCase()  // → "HelloKotlinWorld"

📌 生活化类比:Java 让你"想给别人家的房子加阳台,只能在自家院子搭个铁架子";Kotlin 让你直接"在别人家房子上加个看起来像原装的阳台",路过的人看不出区别。

1.3.3 杀手锏 3:协程 —— 写同步代码,跑出异步效果

痛点:Java 的回调地狱

java
// Java 异步调用 —— 一层一层套回调
fetchUser(userId, user -> {
    fetchOrders(user.getId(), orders -> {
        fetchPayments(orders.get(0).getId(), payments -> {
            // 缩进已经爆炸,再嵌套一层就爬不出来了
        });
    });
});

Kotlin 怎么救你

kotlin
// 看起来跟同步代码一样,实际是非阻塞的
suspend fun loadUserData(userId: String) {
    val user = fetchUser(userId)              // 不阻塞线程
    val orders = fetchOrders(user.id)         // 不阻塞线程
    val payments = fetchPayments(orders[0].id)  // 不阻塞线程
    println(payments)
}

📌 生活化类比:传统线程像「每来 1 个客人,餐厅就雇 1 个服务员盯着他」,客人多了服务员不够用;协程像「1 个服务员同时看 100 桌,谁举手就过去」,因为大部分时间客人都在自己吃菜(IO 等待中)。


1.4 Kotlin vs Java 全方位对比

维度JavaKotlin谁赢
Null 安全NPE 满天飞类型系统保证Kotlin 大胜
数据类50 行模板 + Lombok 凑活data class 一行Kotlin 大胜
函数式Stream API(Java 8+)内置完整集合操作Kotlin 略胜
异步CompletableFuture / 虚拟线程协程Kotlin 略胜
类型推断var(Java 10+,仅局部)val/var 全场景Kotlin 略胜
编译速度略慢(K2 后已追平)Java 略胜
生态成熟度30 年历史10 年历史Java 大胜
学习曲线简单粗暴特性多但设计自洽平手
面试题数量海量中等Java 多
招聘市场后端主流Android 主流 + 后端在涨看方向

1.4.1 一段代码看尽差距

需求:定义一个用户类,包含 ID、姓名、年龄;要支持 equals / hashCode / toString。

Java 版本(不用 Lombok):

java
public class User {
    private final String id;
    private final String name;
    private final int age;

    public User(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age &&
               Objects.equals(id, user.id) &&
               Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }

    @Override
    public String toString() {
        return "User{id='" + id + "', name='" + name + "', age=" + age + '}';
    }
}

Kotlin 版本:

kotlin
data class User(val id: String, val name: String, val age: Int)

同一个类,Java 38 行,Kotlin 1 行。这就是为什么很多人转 Kotlin 之后回不去 Java 了。


1.5 Kotlin 适用场景全景图

                          ★ Kotlin 全场景能力雷达 ★

                            Android 开发 ⭐⭐⭐⭐⭐

                        服务端       │       工具脚本
                        ⭐⭐⭐⭐ ─────┼──── ⭐⭐⭐

                        ─────────  │  ─────────

                          数据科学 │ 跨平台 KMP
                          ⭐⭐⭐    │   ⭐⭐⭐⭐

                              前端(KJS)
                                ⭐⭐

1.5.1 主战场 1:Android 开发

  • 语言地位:Google 官方"Kotlin First",新 Jetpack 库优先 Kotlin API
  • 配套:Jetpack Compose(声明式 UI)、Coroutines、Flow、Room、Hilt
  • 就业:Android 招聘 90% 要求 Kotlin

1.5.2 主战场 2:JVM 后端

  • 框架:Spring Boot 原生支持 Kotlin;Ktor(JetBrains 自家轻量框架)
  • 优势:协程 + 数据类 + 不可变集合,写微服务非常顺手
  • 大厂案例:京东、字节、Pinterest、Atlassian

1.5.3 跨界场景:Kotlin Multiplatform (KMP)

  • 理念:iOS / Android / Web / Server 共享业务逻辑代码
  • 现状:2023 年 KMP 已稳定,Netflix、Philips、McDonald's 在用
  • 限制:UI 层一般还是各端原生,业务逻辑层共享

1.5.4 小众但好用:Gradle 构建脚本

  • build.gradle(Groovy)换成 build.gradle.kts(Kotlin)
  • IDE 有完整类型提示,再也不用对着 Groovy 文档"猜方法名"

1.6 学 Kotlin 之前你需要知道的 5 件事

1.6.1 你需要 Java 基础吗?

不严格需要,但有的话快很多。Kotlin 几乎所有概念在 Java 里都能找到对应:

Kotlin 概念Java 对应
valfinal 变量
classclass
interfaceinterface
Int / Stringint / String
包 / import包 / import

完全没基础也没关系,本笔记不假设你会 Java。

1.6.2 Kotlin 文件长啥样?

kotlin
// 文件名:Hello.kt
package com.example.hello   // 包名(可选)

import kotlin.math.PI       // 导入(按需)

// 顶层函数(不用写 class)
fun main() {
    println("Hello, Kotlin!")
    println("π ≈ $PI")
}

📌 跟 Java 不同的一点:Java 里"什么都得放在 class 里",Kotlin 允许函数和变量直接在文件顶层 —— 写小工具脚本特别方便。

1.6.3 Kotlin 文件的 3 种典型用法

   ┌─────────────────────────────────────────────────────────┐
   │  1. .kt 文件          - 普通源代码                         │
   │  2. .kts 文件         - Kotlin 脚本(无需编译,直接 kotlinc 跑)│
   │  3. .gradle.kts 文件  - Gradle 构建脚本                    │
   └─────────────────────────────────────────────────────────┘

1.6.4 Kotlin 跟 Android 的关系

Kotlin ≠ Android。Kotlin 是一门通用语言,可以脱离 Android 单独跑。但在中文圈"Kotlin"这个词常常被默认是"安卓 Kotlin",造成误解。本笔记只讲 Kotlin 语言本身,不涉及 Android 框架。

1.6.5 学完后你需要继续学的方向

   ┌──────────────────────────────────────────────────────────┐
   │  方向 A:Android   → Jetpack Compose + Coroutines + Hilt │
   │  方向 B:后端      → Ktor / Spring Boot + Exposed         │
   │  方向 C:跨平台    → Kotlin Multiplatform (KMP)           │
   │  方向 D:DSL       → 阅读 Gradle Kotlin DSL / Kotest 源码 │
   └──────────────────────────────────────────────────────────┘

1.7 章末小结

记住这张图,第 1 章你就毕业了:

                        ★ 第 1 章核心知识图谱 ★

        ┌─────────────────────────┼──────────────────────────┐
        │                         │                          │
   ┌────▼─────┐             ┌────▼──────┐              ┌────▼────┐
   │ 是什么   │             │ 为什么火    │              │ 适用场景 │
   ├──────────┤             ├───────────┤              ├─────────┤
   │ JVM 语言 │             │ Null 安全  │              │ Android │
   │ 静态类型 │             │ 扩展函数   │              │ 后端     │
   │ 跨平台   │             │ 协程        │              │ KMP     │
   │ Java 互操│             │ 简洁语法    │              │ 脚本     │
   └──────────┘             └───────────┘              └─────────┘

🎤 1.8 章末面试题(10 道高频题)

Q1. Kotlin 跟 Java 是什么关系?编译后是什么?

:Kotlin 是 JetBrains 开发的运行在 JVM 上的语言,编译产物是 Java 字节码(.class)。它跟 Java 100% 互操作 —— Kotlin 可以直接调 Java 类,反过来 Java 也能调 Kotlin 类。两者编译出来后在 JVM 看来是一样的字节码。


Q2. Google 为什么把 Kotlin 升为 Android 官方语言?

:3 个主要原因:

  1. Oracle 起诉 Java API 侵权,Google 急需一个不受 Oracle 控制的 Plan B 语言;
  2. Android 长期卡在 Java 7 / Java 8 子集,无法跟上 Java 现代特性,而 Kotlin 由 JetBrains 自主演进,可以立即用上现代特性;
  3. Kotlin 解决了 Java 的几大痛点(NPE、模板代码、缺少协程),开发体验显著提升。

Q3. Kotlin 解决了 Java 的哪些核心痛点?

痛点Java 现状Kotlin 解法
NullPointerException无类型保护类型系统区分 TT?
模板代码多POJO 50 行 / Lombok 凑活data class 一行
异步代码难写回调地狱 / CompletableFuture协程(写同步形式)
函数式 API 弱Java 8 才有 Stream集合 + 序列原生支持
工具方法别扭静态工具类 StringUtils.foo(s)扩展函数 s.foo()

Q4. valvar 有什么区别?

  • val = value,只读引用(类似 Java 的 final)—— 一旦初始化,引用本身不能换,但引用的对象内部状态可以变(比如 val list = mutableListOf<Int>() 后还能 list.add(1))。
  • var = variable,可变引用,可以重新赋值。

💡 最佳实践:默认全部用 val,只有真的需要重新赋值才用 var。这能避免大量隐藏 bug。


Q5. Kotlin 是怎么实现"空安全"的?

:核心是把"是否可空"写进类型本身

  • String 是非空类型,编译器禁止给它赋 null
  • String? 是可空类型,可以是 null,但访问它的方法时编译器强制你处理 null 的情况(用 ?.?:!!if (x != null) 智能转换)。

这等于把"运行时炸"提前到了"编译时报错",从源头消除了 NPE。


Q6. Kotlin 跟 Java 互操作时,有什么需要注意的"坑"?

:最大的坑是 平台类型(Platform Type)。Kotlin 调 Java 时,Java 类型系统不区分可空非空(除非 Java 方法上加了 @Nullable/@NotNull 注解),Kotlin 编译器把这种类型标为 String!(带感叹号),既能当非空也能当可空 —— 写错了运行时仍可能 NPE。

💡 解决方法:调 Java API 时尽量声明明确的可空性(val name: String? = javaApi.getName()),不要图省事。


Q7. Kotlin 适合做什么?不适合做什么?

  • 适合:Android 开发、JVM 后端服务(Spring/Ktor)、Gradle 脚本、跨平台 KMP、CLI 工具
  • 不适合
    • 系统编程(Kotlin/Native 当前生态弱于 Rust/Go)
    • 高性能计算(GC + JVM 启动开销)
    • 数据科学(Python 生态碾压)
    • 游戏引擎开发(C++/C# 主流)

Q8. Kotlin 协程跟 Java 线程有什么区别?

维度Java ThreadKotlin 协程
本质操作系统线程用户态调度的"轻量任务"
创建成本1MB 栈 / 个几 KB / 个
数量上限几千几百万
阻塞代价占用一个 OS 线程suspend 时让出线程
切换内核态调度用户态调度(编译器协作)

简单说:协程是"写起来像同步、跑起来像异步"的语法糖 + 状态机。

⚠️ 注意:Java 21 之后引入了"虚拟线程"(Project Loom),思想跟协程类似,所以未来差距会缩小。


Q9. Kotlin 编译产物跟 Java 完全一样吗?

几乎一样,但不完全一样

  • Kotlin 编译出来也是 JVM 字节码,跑在同一个 JVM 上;
  • 但 Kotlin 编译器会额外生成一些辅助类(如 Kotlin Metadata 注解),用来在反射时还原 Kotlin 特有信息(如 data class 的属性、可空性);
  • 协程会被编译成状态机字节码(CPS 变换),看起来跟同步代码字节码完全不同。

所以从 Java 反编译 Kotlin 代码会看到一些"奇怪"的辅助类和 Continuation 参数。


Q10. 如果你已经会 Java,学 Kotlin 大概要多久?

  • 基础语法(变量、控制流、类、函数):1 天
  • 流畅写代码(含 Lambda / 集合 API / 数据类):1 周
  • 写出"地道 Kotlin"(idiomatic):1 个月
  • 协程精通(含 Flow / 异常处理 / 取消):3 个月

💡 关键点:会写 Kotlin 不等于会"地道 Kotlin"。前期容易"用 Java 思维写 Kotlin",比如该用 let 的地方还在写 if (x != null)。靠多读优秀开源项目能加速这个过程。


下一章 → 第 2 章 · 环境搭建 & 第一行 Kotlin →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 1 章 · Kotlin 是什么 — 第一段示例
 *
 * 这段代码体现了 Kotlin 的几个特点,对照注释能快速感受跟 Java 的差别。
 *
 * 运行方式(在装好 Kotlin CLI 之后):
 *     kotlinc HelloKotlin.kt -include-runtime -d hello.jar
 *     java -jar hello.jar
 *
 * 也可以直接复制到 https://play.kotlinlang.org/ 在线运行。
 */

// ---------- 1. 顶层函数:不用包在 class 里 ----------
fun main() {
    println("Hello, Kotlin!")

    // ---------- 2. 变量:val 只读 / var 可变 ----------
    val name: String = "Alice"      // 显式类型
    val age = 25                    // 自动推断为 Int
    var counter = 0                 // var 才能后续重新赋值
    counter += 1

    // ---------- 3. 字符串模板:$ 直接插值 ----------
    println("$name, $age 岁,counter = $counter")
    println("一年后她 ${age + 1} 岁了")

    // ---------- 4. Null 安全:类型系统区分可空与不可空 ----------
    val nickname: String? = null            // 注意问号
    val length = nickname?.length ?: 0      // ?. 安全调用 + ?: Elvis 默认值
    println("昵称长度 = $length")

    // ---------- 5. 集合 + 函数式操作 ----------
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val doubled = numbers
        .filter { it % 2 == 0 }     // 只留偶数
        .map { it * 2 }             // 每个 ×2
    println("偶数翻倍: $doubled")     // → [4, 8, 12, 16, 20]

    // ---------- 6. data class:一行定义包含 equals / hashCode / toString 的类 ----------
    data class User(val id: String, val name: String, val age: Int)

    val u1 = User("u001", "Alice", 25)
    val u2 = User("u001", "Alice", 25)
    println("u1 == u2 ? ${u1 == u2}")    // true(结构相等)
    println(u1)                          // User(id=u001, name=Alice, age=25)

    // copy 一个改了年龄的副本,常用于不可变数据更新
    val u3 = u1.copy(age = 26)
    println("u3 = $u3")

    // ---------- 7. 扩展函数:给已有类加方法 ----------
    fun String.shout(): String = this.uppercase() + "!"
    println("hello".shout())            // → HELLO!

    // ---------- 8. when 表达式:Java switch 的现代版 ----------
    val grade = when (age) {
        in 0..6   -> "幼儿"
        in 7..17  -> "少年"
        in 18..40 -> "青年"
        in 41..65 -> "中年"
        else      -> "老年"
    }
    println("$name 属于 $grade")
}
markdown
# Java vs Kotlin —— 同样需求两种写法

## 需求 1:定义一个 User 类(含 equals / hashCode / toString)

### Java(不用 Lombok)

```java
public class User {
    private final String id;
    private final String name;
    private final int age;

    public User(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age &&
               Objects.equals(id, user.id) &&
               Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() { return Objects.hash(id, name, age); }

    @Override
    public String toString() {
        return "User{id='" + id + "', name='" + name + "', age=" + age + '}';
    }
}

Kotlin

kotlin
data class User(val id: String, val name: String, val age: Int)

需求 2:从用户列表里筛出成年人,按年龄倒序

Java

java
List<User> adults = users.stream()
    .filter(u -> u.getAge() >= 18)
    .sorted((a, b) -> Integer.compare(b.getAge(), a.getAge()))
    .collect(Collectors.toList());

Kotlin

kotlin
val adults = users
    .filter { it.age >= 18 }
    .sortedByDescending { it.age }

需求 3:异步获取用户信息然后处理

Java(CompletableFuture 链)

java
fetchUser(userId)
    .thenCompose(user -> fetchOrders(user.getId()))
    .thenCompose(orders -> fetchPayments(orders.get(0).getId()))
    .thenAccept(payments -> System.out.println(payments))
    .exceptionally(ex -> {
        ex.printStackTrace();
        return null;
    });

Kotlin(协程)

kotlin
try {
    val user = fetchUser(userId)
    val orders = fetchOrders(user.id)
    val payments = fetchPayments(orders[0].id)
    println(payments)
} catch (e: Exception) {
    e.printStackTrace()
}

看起来像同步代码,实际是非阻塞执行,不会卡线程。

HelloKotlin.kt ↗ · JavaVsKotlin.md ↗