Kotlin语法笔记(27) -Kotlin 与 Java 共存(二)

本文涉及的产品
公网NAT网关,每月750个小时 15CU
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
简介: 本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。若需快速入门,建议查阅“简洁”系列教程。本文重点探讨Kotlin与Java共存的高级话题,包括属性访问、空安全、泛型处理、同步机制及SAM转换等,助你在项目中逐步引入Kotlin。

本系列学习教程笔记属于详细讲解Kotlin语法的教程,需要快速学习Kotlin语法的小伙伴可以查看“简洁” 系列的教程

快速入门请阅读如下简洁教程:
Kotlin学习教程(一)
Kotlin学习教程(二)
Kotlin学习教程(三)
Kotlin学习教程(四)
Kotlin学习教程(五)
Kotlin学习教程(六)
Kotlin学习教程(七)
Kotlin学习教程(八)
Kotlin学习教程(九)
Kotlin学习教程(十)

Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)

Kotlin 与 Java 共存(二)
上一期我们简单讨论了几个 Java 调用 Kotlin 的场景,这一期我们主要讨论相反的情况。

  1. 属性
    如果 Java 类存在类似 setXXX 和 getXXX 的方法,Kotlin 会聪明地把他们当做属性来使用,例如:

public class DataClass {
private int id;

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}

}
Kotlin 的访问也非常简单:

fun main(args: Array) {
val dataClass = DataClass()
dataClass.id = 0
println(dataClass.id)
}

  1. 空安全
    空安全这个特性使得 Kotlin 对变量的值要求更严格了,由于 Java 变量通常没有这样的信息,因此 Kotlin 在访问 Java 变量或者 Java 方法时,变量、方法的参数和返回值的空与否由我们自己决定,编译器不再进行约束。例如定义这么一个 Java 类:

public class NullSafetyJava {
public String getData(){
return null;
}
}
在 Kotlin 当中访问它:

fun main(args: Array) {
val nullSafetyJava = NullSafetyJava()
val data = nullSafetyJava.data
}
这个 data 的类型被称作 『平台类型(Platform Type)』,它既可以为可空类型,也可以为不可空类型,这一切取决于我们自己的决定。

val dataCanBeNull: String? = data
val dataCannotBeNull: String = data
这样在编译期是允许的,不过如果在运行时由于 data 为 null,那你就会遇到异常:

Exception in thread "main" java.lang.IllegalStateException: data must not be null
at net.println.kt15.NullSafetyKt.main(NullSafety.kt:10)
如果继承 Java 类,父类的方法参数也是平台类型,也需要我们根据实际情况来判断是否可空。例如:

public abstract class NullSafetyAbsClass {
public abstract String formatDate(Date date);
}
在Kotlin中继承这个类时,formatDate 方法的参数和返回值的类型我们根据情况写:

class NullSafetySubClass : NullSafetyAbsClass(){
override fun formatDate(date: Date?): String? {
return date?.toString()
}
}

fun main(args: Array) {
val nullSafetySubClass = NullSafetySubClass()
val formattedDate: String? = nullSafetySubClass.formatDate(Date())
println(formattedDate)
}
这样直接写可控类型当然是没有问题的。如果你确信它可以不为空,那么你也可以直接写不可控类型:

class NullSafetySubClass : NullSafetyAbsClass(){
override fun formatDate(date: Date): String {
return date.toString()
}
}

fun main(args: Array) {
val nullSafetySubClass = NullSafetySubClass()
val formattedDate: String = nullSafetySubClass.formatDate(Date())
println(formattedDate)
}
当然,对于这种情况,我们在 Java 也已经开始采用一些『曲线救国』的方式来弥补这一不足,比如 采用 JetBrains 的 @Nullable 和 @NotNull 注解以及 Android 当中类似的注解支持,我们可以把我们的 Java 代码改写成这样:

public abstract class NullSafetyAbsClass {
public abstract @NotNull String formatDate(@NotNull Date date);
}

  1. 泛型
    整体上来讲,Kotlin 的泛型与 Java 的没什么大的差别,Java 的 ? 在 Kotlin 中变成了 *,毕竟 ? 在 Kotlin 当中用处大着呢。另外,Java 泛型的上限下限的表示法在 Kotlin 当中变成了 out 和 in。

不过,由于 Java 1.5 之前并没有泛型的概念,为了兼容旧版本,1.5 之后的 Java 仍然允许下面的写法存在:

List list = new ArrayList();
list.add("Hello");
list.add(1);
for (Object o : list) {
System.out.println(o);
}
而对应的,在 Kotlin 当中要用 List<*> 来表示 Java 中的 List —— 这本身没什么问题。那么我们现在再看一段代码:

public abstract class View

{
protected P presenter;
}

public abstract class Presenter{
protected V view;
}
这个其实是现在比较流行的 MVP 设计模式的一个简单的接口,我希望通过泛型来绑定 V-P,并且我可以通过泛型参数在运行时直接反射实例化一个 presenter 完成 V-P 的实例注入,这在 Java 当中是没有问题的。

那么在 Kotlin 当中呢? 因为我们知道 View 和 Presenter 都有泛型参数的,所以我们在 Kotlin 当中就要这么写:

abstract class View

>{
protected abstract val presenter: P
}

abstract class Presenter>>>{
protected abstract val view: V
}
一直写下去,最终陷入了死循环。编译器给出的解释是:

This type parameter violates the Finite Bound Restriction.

在 @zhangdatou 给我发邮件之前,我曾一直对此耿耿于怀,Kotlin 这么优秀的语言怎么还会有做不到的事情呢。原来不是做不到,而是我没有想到:

abstract class View>>
abstract class Presenter>>

class MyView: View()
class MyPresenter: Presenter()
实际上我们需要 View 的泛型参数 P 只要是 Presenter 的子类即可,并且要求这个泛型参数 P 本身对应的泛型参数也需要是 View 的子类,而这个 View 其实就是最初的那个 View,那么这个 View 的泛型参数当然就是 P 了。有点儿绕,但这么写出来明显感觉比 Java 安全靠谱多了。

  1. Synchronized 和 volatile
    在 Kotlin 当中,这两个关键字被削去了王位,成为平民。也许是设计者们觉得这二位作为关键字出现有点儿太重了,虽然不再身居要职,不过却也是不可或缺。

Synchronized 有两个版本,用于函数的版本是个注解:

var num: Int = 0

@Synchronized fun count(){
num++
}
用于代码块的则是一个函数:

var num: Int = 0

fun count(){
synchronized(num){
num++
}
}
@kotlin.internal.InlineOnly
public inline fun synchronized(lock: Any, block: () -> R): R {
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
monitorEnter(lock)
try {
return block()
}
finally {
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
monitorExit(lock)
}
}
volatile 的命运差不多,也变成了一个注解:

@Volatile var num: Int = 0

  1. SAM 转换
    看名字一头雾水,其实就是对于只有一个方法的 Java 接口,Kotlin 可以用一个 Lambda 表达式来简化它的书写,例如:

fun main(args: Array) {
val worker = Executors.newCachedThreadPool()
worker.execute {
println(System.currentTimeMillis())
}
}
execute 方法传入了一个 Runnable 接口的实例,不过看样子却是一个 Lambda 表达式。不过这里也是有一个需要注意的点的,既然是『转换』,那么 Java 的 execute 方法接受到的实例就一定不是我们看到的这个 Lambda 表达式实例了,换句话说,我们就算每次传入同一个 Lambda 表达式实例,那么 Java 的 execute 方法收到的也并不是同一个对象。举个例子:

public class SAMInJava {
private ArrayList runnables = new ArrayList();

public void addTask(Runnable task){
    runnables.add(task);
    System.out.println("after add: " + task + ", we " + runnables.size() + " in all.");
}

public void removeTask(Runnable task){
    runnables.remove(task);
    System.out.println("after remove: " + task + ", only " + runnables.size() + " left.");
}

}
然后我们在 Kotlin 当中这么写:

fun main(args: Array) {
val samInJava = SAMInJava()
val lambda = {
println("Hello")
}

samInJava.addTask(lambda)
samInJava.addTask(lambda)
samInJava.addTask(lambda)
samInJava.addTask(lambda)

samInJava.removeTask(lambda)
samInJava.removeTask(lambda)
samInJava.removeTask(lambda)
samInJava.removeTask(lambda)

}
运行结果呢?

after add: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@6ce253f1, we have 1 in all.
after add: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@53d8d10a, we have 2 in all.
after add: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@e9e54c2, we have 3 in all.
after add: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@65ab7765, we have 4 in all.
after remove: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@1b28cdfa, only 4 left.
after remove: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@eed1f14, only 4 left.
after remove: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@7229724f, only 4 left.
after remove: net.println.kt15.SAMConversionKt$sam$Runnable$9855366b@4c873330, only 4 left.
每次 Java 的 add 和 remove 方法收到的都是不同的实例,所以 remove 方法根本没有起到作用。

  1. 小结
    除了这些之外还有一些细节,比如异常的捕获,集合类型的映射等等,大家可以自行参考官方文档即可。在了解了这些之后,你就可以放心大胆的在你的项目中慢慢渗透 Kotlin,让你的代码逐渐走向简洁与精致了。

最后,作为这一系列视频的最后一集,我还想要告诉大家有关 Android 开发的视频可能会在年后开始筹备,公众号在后续的这段时间内会推送我为大家准备的 Kotlin 的一些有意思的文章,请大家继续关注,并与我互动,谢谢大家!

相关文章
|
2月前
|
存储 Java 开发者
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
本文详细介绍了 Java 中 `toString()` 方法的重写技巧及其重要
57 10
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
|
27天前
|
数据采集 前端开发 JavaScript
如何利用Java和Kotlin实现动态网页内容抓取
如何利用Java和Kotlin实现动态网页内容抓取
|
2月前
|
前端开发 JavaScript Java
Java构建工具-maven的复习笔记【适用于复习】
这篇文档由「潜意识Java」创作,主要介绍Maven的相关知识。内容涵盖Maven的基本概念、作用、项目导入步骤、依赖管理(包括依赖配置、代码示例、总结)、依赖传递、依赖范围以及依赖的生命周期等七个方面。作者擅长前端开发,秉持“得之坦然,失之淡然”的座右铭。期待您的点赞、关注和收藏,这将是作者持续创作的动力! [个人主页](https://blog.csdn.net/weixin_73355603?spm=1000.2115.3001.5343)
47 3
|
3月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
3月前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
106 0
|
10天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
125 60
【Java并发】【线程池】带你从0-1入门线程池
|
6天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
53 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
22天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
89 14
|
25天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
54 13