Skip to content

Kotlin 基础 (JVM之下的Kotlin)

  • Kotlin从入门到算了
  • 一个用一点,学一点的渐进式开发语言

image

1. 空安全机制

语法,使用问号(?)在语法层面去建立空安全机制

kotlin
// 这是一个String?类型
user?.email
user?.name

在Java中,null是类型可能出现的一种状态,举例子来说,

java
class User {
    private String id;
    private String password;
    
    public User(String id, String password) {
        this.id = id;
        this.password = password;
    }
    
    public String getId() {
        return this.id;
    }
    
    public String getPassword() {
        return this.password;
    }
}
java
// psvm
User user = new User("id", null)

// 实际上来说,在这一步,拿到的id和passsword就是一个可为空的String
// 因为在Java中,null直接内置在类型之中
String id = user.getId();
String password = user.getPassword();

// 处理相应功能
if (id != null) {
    id.function();
} else {
    System.out.println("id为空")
}

if (password != null) {
    password.function();
} else {
    System.out.println("password为空")
}

在Java中,null的这种可能放在了类型取出来的时候的其中一种可能性里面,这样导致每次使用的时候,我们作为开发者都需要判断一下这种可能性,以去除空的可能性。

在一些没有特定对null有处理的业务场景下,大部分的开发者都会忘记判断null的情况,如下面的这种情况

java
// 这种代码及其容易在不判空的情况直接调用id.function(),
// 因为状态中没有要求开发者对 (id == null)这种情况做业务处理
// 而且Java IDE一般也不会针对这种情况做Warning处理
// 除非使用了@NotNull这种注解,才能够让开发者有意识去处理这种状态
if (id != null) {
    id.function();
}

// 如果是这种使用方式
id.function()
// 当id为空的时候,无法调用function()方法,将会在运行时阶段出现NullPointException
// 这就是大名鼎鼎的NPE,空指针异常问题

而在Kotlin中,null是不再是类型中的默认状态,除非主动声明可空情况,否则一律不允许为空。

Kotlin需要开发者在编写代码的时候,非必要则变量不可为空,把null的情况在语法层面抽离出来。如果不去对null状态处理,在编译阶段直接以ERROR的形式报错

String? 和 String是两种东西,涉及到一个空安全的问题,这个安全,用一种语法层面的手段让开发者在编译阶段必须处理空类型的可能性,与Java相比,空类型是进入到运行阶段才出问题,这样存在线上隐患。

kotlin
data class User(
    val id: String,
    val password: String?
)

// psvm
val user = User("id", null)

user.id.function() // 不需要处理null的情况,因为在不表明可空的状态,Kotlin在语法层面不允许这个类型为null
user.password?.function()

// 如果是一种有绝对把握,这个password不可能为空,也可以使用强制方式,获取到它的非空类型
user.password!!.function()

// 如果需要处理String? Null这种状态
user.password?.let {
    it.function()
} ?: run {
    System.out.println("password为空")
}
  • Kotlin的空指针隐患,是指将开发不判空的坏习惯,在编译阶段使用语法强制要求在编译阶段去处理掉。
  • 而针对内存泄漏、生命周期不合理管理导致的空指针异常,目前来看语法层面还做不到这样的强制性处理。
kotlin
// MVP
// View
interface ICallback {
    fun callback(message: String)
}

class View : ICallback {
    private val presenter: Presenter = Presenter(this)
    
    override fun onViewCreated() {
        presenter.fetchData()
    }

    override fun callback(message: String) {
        mBinding.tvInfo.text = message
    }
}

// Presenter
class Presenter(
    val iCallback: ICallback,
) {    
    fun fetchData() {
        Single.create<String>(object : SingleOnSubscribe<String> {
            override fun subscribe(emitter: SingleEmitter<String?>) {
                Thread.sleep(2000L) // 假装这是一个耗时操作
                emitter.onSuccess("Hello World")
            }
        }).subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object: SingleObserver<String> {
                override fun onSubscribe(d: Disposable) = Unit
                
                override fun onSuccess(result: String) {
                    iCallback.callback(result)
                }

                override fun onError(e: Throwable) = Unit
            })
    }
}
  • 类似于上面的这个请求,在View实际销毁时,请求还在继续运行,当数据回调到View层的时候,就会出现iCallback为空的情况。

  • 这个情况是Kotlin的判空机制也无法避免的,必须使用正确的生命周期维护去避免这个问题。

  • 真正的MVP取消网络请求的方式,请参考这篇文章

2. Kotlin的类型变动

json
- 基础类型
1.Java boolean -> Boolean
2.Java int -> Int
3.Java float -> Float
- 包装类型
1.Java Boolean -> Boolean?
2.Java Integer -> Int?
3.Java Float -> Float?

3. 类型推导机制

java
// 在Java1.8 环境下,Java是不支持类型推导的,必须使用类型申明,如:
final List<String> list = new ArrayList<>();
final String word = "Good Morning! I am the best handsome boy in QUIN~~~";
final boolean doIHandsome = true;
// etc...

这种代码显得非常臃肿。

而且在实际开发的过程中,开发者可能只知道自己需要的那一个方法,方法具体的类型开发者一下是不清楚的,必须调完方法之后,开发者才能反应过来自己需要的是一个什么样的类型。

java
final String bannerType = adverts.getBannerType();

如同上面的这块代码,开发者可能知道开发者做数据上报或者参数请求的时候,开发者需要一个bannerType这个参数,但是这个参数是int还是String,开发者一下反应不过来的,使用Java 1.8的情况下,需要你先知道这是一个什么样的类型,再去拿出这个东西,显得就很麻烦。

而在Kotlin中,支持类型推导机制,不需要知道这个变量是什么类型,它会根据后面的内容自动推导变量的类型。

kotlin
val bannerType = adverts.getBannerType()
  • 如果有了解过JavaScript或者Python这种动态语言,可能会觉得Kotlin在语法上和他们很相似,这很容易陷入另外一个误区。
  • 实际上来讲,这个和JavaScript、Python那些动态类型语言又不太相同,Kotlin依旧还是静态语言,这种类型推导只会存 在第一次初始化变 量的时候才运转,在之后的重新赋值过程中,它是不允许类型的。
python
// Python代码运行下面的逻辑是不会报错的
word = "I am Python Code"
word = 10
word = False
kotlin
// Kotlin代码运行下面是会异常的
var word = "I am Kotlin Code"
word = 10
word = false

4. 变与不变

  • Java的默认选择变
  • Kotlin的默认选择不变

Java中经常遇到的一个问题:忘记写final class、final int这种标识符,去限制某一个类为不可继承、某一个变量为不可变

而在Kotlin中,val和非open不可继承直接处理掉了这个问题

kotlin
// 以下代码会在代码编写阶段直接出ERROR提示
class Aimo {
}

class Quin: Aimo {
}

必须把Aimo改成抽象类、或者加上open的关键字,否则为不可继承类 如下:

kotlin
open class Aimo

abstract class Aimo

同理,在Kotlin之中,针对变量的,kotlin使用了val和var去区分这一个问题

kotlin
// 可变变量
var keyword = "Could you tell me a story about Milan Kundera?"
keyword = "Okay, but I'll tell you when I'm free."

// 不可变变量
val keyword = "Do you know who is albert camus?"
// keyword在这里将不可再去改变
  • Kotlin的这种写法,很好地约束了一个变量的可变性、一个类的继承性。
  • 开发者必须有清晰意识,去意识一个变量、类的可变性以及不可变量,防止在实际开发过程中一些不需要变化的变量增加一个可变性,导致代码在实际的开发过程中,有很大的变动性,这对于之后的维护,将会是一个灾难性的问题。
  • 而在Java,虽然也有这种机制,就是上面说过的final int,但由于不是强制性要求开发者必须这样写、以及看起来很臃肿,导致很多开发者主观上不愿意或者客观上忘了写,这就会导致代码进入一个难以维护的状态。

5. 被简化的class写法

基于class之下重写了的data class

java
public class User {
    @NotNull
    private final String userId;
    
    @NotNull
    private final String password;
    
    private final int age;

    public User(@NotNull String userId, @NotNull String password, int age) {
        this.userId = userId;
        this.password = password;
        this.age = age;
    }

    @NotNull
    public String getUserId() {
        return userId;
    }

    @NotNull
    public String getPassword() {
        return password;
    }

    public int getAge() {
        return age;
    }
}

Kotlin的写法如下所示:

kotlin
data class User(
    val userId: String,
    val password: String,
    val age: Int
)

与Java对比,Kotlin实现了一句顶一万句

class之下的copy语法

6. Kotlin的语法变化

6.1 消失的三目表达式(使用if-else来表达)

java
// Java Code
view.setVisibility(showOrHide ? View.VISIBLE : View.GONE);
kotlin
// Kotlin Code
view.visibility = if (showOrHide) View.VISIBLE else View.GONE

6.2 when,一个增强版的switch

Java中,switch只能针对一些基本类型、String这种简单类型去列举,而在Kotlin中的when,可以针对一切去列举,包括条件状态也可以

kotlin
when (Calendar.getInstance().get(Calendar.HOUR_OF_DAY)) {
    // 6:00 to 11:59
    in (6..11) -> TimeStage.Morning()
    // 12:00 to 17:59
    in (12..17) -> TimeStage.Afternoon()
    // 18:00 to 19:59
    in (18..19) -> TimeStage.Nightfall()
    // 20:00 to 23:59
    in (20..23) -> TimeStage.Evening()
    // 00:00 to 05:59
    else -> TimeStage.Midnight()
}

6.3 参数默认值:简化了Java需要使用重载的实现

java
public class QUINTextView {
    public void setViewParams(@NotNull Params params) {
        setViewParams(params, 0f);
    }

    public void setViewParams(@NotNull Params params, float bias) {
        this.setParams(params);
        this.setBias(bias);
    }
}
kotlin
class TextView {
    fun setViewParams(
        params: Params,
        bias: Float = 0f,
    ) {
        this.setParams(params)
        this.setBias(bias)
    }
}

7. Kotlin新增语法

7.1 属性委托实现

在Java里面,如果需要使用委托,需要如下实现

java
public interface ISource {
    String fetchA();

    String fetchB();
}

// 普通实现
public class NormalSource implements ISource {
    public NormalSource() {

    }

    @Overide
    public String fetchA() {
        return "普通A方法";
    }

    @Overide
    public String fetchB() {
        return "普通B方法";
    }
}

// 需要更加符合特定业务的fetchB,但是fetchA和之前的实现一样就行
// 使用委托模式
public class SuperSource implements ISource {
    private final NormalSource normalSource;

    public SuperSource() {
        this.normalSource = new NormalSource();
    }

    @Overide
    public String fetchA() {
        // 假设这里需要使用到的实现和NormalSource
        return normalSource.fetchA();
    }

    @Overide
    public String fetchB() {
        // 假设这里有一个比NormalSource更加适合当前业务的实现
        return "超级B方法";
    }
}
java
// 实际调用的时候 psvm
final var normalSource = new NormalSource();
final var superSource = new SuperSource();

normalSource.fetchA(); // 普通A方法
normalSource.fetchB(); // 普通B方法

superSource.fetchA(); // 普通A方法
superSource.fetchB(); // 超级B方法

而在Kotlin上,只需要使用by即可

kotlin
interface ISource {
    fun fetchA(): String

    fun fetchB(): String
}

class NormalSource: ISource {
    override fun fetchA(): String = "普通A"

    override fun fetchB(): String = "普通B"
}

class SuperSource(normalSource: NormalSource = NormalSource()): ISource by normalSource {
    override fun fetchB(): String = "超级B"
}
kotlin
// 实际调用的时候 psvm
val normalSource = NormalSource();
val superSource = SuperSource();

normalSource.fetchA() // 普通A方法
normalSource.fetchB() // 普通B方法

superSource.fetchA() // 普通A方法
superSource.fetchB() // 超级B方法

更详细的内容可以看这个扔物线的这个视频

7.2 拓展函数

针对原有的类,直接在它的基础上,拓展一个新的方法

kotlin
fun String.log(tag: String) {
    // this就是自己
    Log.d(tag, this)
}

7.3 中缀函数

kotlin
infix fun Int.add(other: Int): Int = (this + others)

// psvm
3 add 5
kotlin
data class Person(
    val age: Int
)

infix fun Person.and(other: Person): Int = (this.age + other.age)
infix fun Person.averageAge(other: Person): Float = (this.age + other.age) / 2

// psvm
val littleMing = Person(age: 30)
val littleRed = Person(age: 26)

val totalAge = (littleMing and littleRed) // 30 + 26 = 56 总共年龄

val averageAge = (littleMing averageAge littleRed) // (30 + 26) / 2 = 28 平均年龄

这只是一个例子,实际开发过程中,这个内容可以用来优化一些复杂的逻辑,将计算机开发语言优化成为自然语言。

7.4 by lazy 延迟函数(基于委托和拓展函数实现的一个语法糖功能)

by lazy的作用,是声明一个变量,在它实际调用的时候,再去初始化这个变量。

  • 好处:类里面,部分变量在程序实际运行过程中,它并没有进入到一个调用的状态,但它属于一个在类初始化阶段就声明并实例化的变量,使用by lazy的方式去声明这个内容,可以减少内存的分配。

实际使用方式:

kotlin
class DataSource {
    private val calendar: Calendar by lazy {
        Calendar.getInstance()
    }
    
    fun fetchDate(): Date = calendar.time
}

这个calendar的实例化,发生在fetchData的时机

具体源码:

kotlin
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

SynchronizedLazyImpl具体实现

kotlin
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer

    @Volatile
    private var _value: Any? = UNINITIALIZED_VALUE

    // final field to ensure safe publication of 'SynchronizedLazyImpl' itself through
    // var lazy = lazy() {}
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}
  • 可以理解为,内部加了一个变量,去存储记录一下value有没有实例了,如果已经实例化了,直接返回,如果没有实例化,就运行initializer这个方法,然后再把拿到的值赋值给value,并将value返回出去。
  • 和我们根据LiveData自定义SingleLiveEvent的思路相似,利用了Kotlin的by关键字、扩展函数实现的一个内容

8.来自Phomemo的一个例子

  • 这个例子,主要就是出现在Java缺少空安全机制,需要进行大量的判空行为,而这些都是重复性的工作,在代码里面体现大量的判空操作,远不如Kotlin的?来得便捷

  • 以Java来写的话,大概在140行左右

java
public class PremiumMineHeadFragment extends BaseMemberMineHeadFragment<PremiumMineHeadFragmentBinding> {

    @Nullable
    @Override
    protected TextView getTvMileage() {
        return (mBinding != null) ? mBinding.tvMileage : null;
    }

    @Nullable
    @Override
    protected View getCTASubscription() {
        return (mBinding != null) ? mBinding.ctaVipSubscribe : null;
    }
    
    // .....

    @Nullable
    @Override
    public PageData getPageData() {
        PageData data = new PageData();
        data.setPageCode("xxxxx");
        data.setPageName("xxxxx");
        data.setTitle("xxxxx");
        return data;
    }
}
kotlin
class PremiumMineHeadFragmentByKt: BaseMemberMineHeadFragment<PremiumMineHeadFragmentBinding>() {
    override fun getTvMileage(): TextView? = mBinding?.tvMileage

    override fun getCTASubscription(): View? = mBinding?.ctaVipSubscribe
    
    // ...

    override fun getPageData(): PageData? = PageData().also {
        it.pageCode = "ph00010"
        it.pageName = "my"
        it.title = "我的"
    }

    companion object {
        fun newInstance(isPurchase: Boolean): PremiumMineHeadFragmentByKt {
            val bundle = Bundle()
            bundle.putBoolean(IS_MEMBER_FLAG, isPurchase)
            
            val premiumMineHeadFragment = PremiumMineHeadFragmentByKt()
            premiumMineHeadFragment.arguments = bundle
            return premiumMineHeadFragment
        }
        
        // 更简洁一点
        fun newInstance(isPurchase: Boolean): PremiumMineHeadFragmentByKt = PremiumMineHeadFragmentByKt().also {
            it.arguments = Bundle().also { bundle -> 
                bundle.putBoolean(IS_MEMBER_FLAG, isPurchase)
            }
        }
    }
}
  • 回到抽象类
  • 在@NotNull感知到了这个可空类之后,Java需要进行大量的判空处理
  • 如下,使用了Optional.ofNullable(xxx).ifPresenter(),去避免出现空指针异常的情况
java
public abstract class BaseMemberMineHeadFragment<B extends ViewBinding> extends MainToolbarFragment<B>
        implements ScreenTrackAction {
        
    @Nullable
    protected abstract View getCTASubscription();
 
    private void initEvent() {
        Optional.ofNullable(this.getCTASubscription())
            .ifPresent(ctaVipSubscribe -> {
                // 跳转购买会员
                ctaVipSubscribe.setOnClickListener(view -> {
                    ViewUtil.avoidFastClick(view);
                    this.navigateToPaymentSubMember();
                });
            });
            
        // --- 或者换成这种手动判空
        if (this.getCTASubscription() != null) {
            this.getCTASubscription().setOnClickListener(v -> {
                ViewUtil.avoidFastClick(v)
                this.navigateToPaymentSubMember()
            })
        }
    }
}

如果使用Kotlin的话,它的写法可以简洁为:

kotlin
abstract class BaseMemberMineHeadFragmentKt<B: ViewBinding>: MainToolbarFragment<B>(), ScreenTrackAction {
    abstract fun getCTASubscription(): View?
    
    // Kotlin的抽象类中,还可以让属性作为抽象点
    abstract val ctaSubscription: View?

    private fun initEvent() {
        // 使用抽象方法的时候
        this.getCTASubscription()?.setOnClickListener { v ->
            ViewUtil.avoidFastClick(v)
            this.navigateToPaymentSubMember()
        }
        
        // 使用抽象属性的时候
        ctaSubscription?.setOnClickListener {
            ViewUtil.avoidFastClick(v)
            this.navigateToPaymentSubMember()
        }
    }
}

随便写写的,喜欢就好。 使用VitePress构建