Kotlin是JVM上比较新的语言之一,来自IntelliJ开发商JetBrains。它是一种静态类型语言,旨在提供一种混合OO和FP的编程风格。Kotlin编译器生成的字节码与JVM兼容,可以在JVM上运行及与现有的库互操作。2017年,谷歌支持将其用于Android开发,Kotlin获得了重大突破。
JetBrains有一个明确的目标:让Kotlin成为一种多平台语言,并提供100%的Java互操作性。Kotlin最近的成功和成熟水平为它进入服务器端提供了一个很好的机会。
选择Kotlin的理由
许多语言都试图成为更好的Java。Kotlin在语言和生态系统方面做得都很好。成为更好的Java,同时又要保护JVM和巨大的库空间,这是一场姗姗来迟的进化。这种方法与来自JetBrains和谷歌的支持相结合,使它成为一个真正的竞争者。让我们来看看Kotlin带来的一些特性。
类型推断 —— 类型推断是一等特性。Kotlin 推断 变量的类型,而不需要显式指定。在需要明确类型的情况下,也可以指定类型。
通过引入 var 关键字,Java 10也在朝着类似的方向发展。虽然表面看起来类似,但它的范围仅限于局部变量,不能用于字段和方法签名。
严格空检查—— Kotlin将可空代码流视为 编译 时错误。它提供了额外的语法来处理空检查。值得注意的是,它提供了链式调用中的NPE保护。
与Java互操作—— Kotlin在这方面明显优于其他JVM语言。它可以与Java无缝地交互。可以在Kotlin中导入框架中的Java类并使用,反之亦然。值得注意的是,Kotlin集合可以与Java集合互操作。
不变性—— Kotlin鼓励使用不可变的数据结构。常用的数据结构( Set/ List/ Map )是不可变的,除非显式地声明为可变的。变量也被指定为不可变( val )和可变( var )。所有这些变化对状态可管理性的影响是显而易见的。
简洁而富有表达力的语法 —— Kotlin引入了许多改进,这些改进对代码的可读性产生了重大影响。举几个例子:
在Kotlin中,所有的 函数 都是表达式,因为它们至少返回 Unit 。控制流语句如 if 、 try 和 when (类似于 switch )也是表达式。例如:
String result = null; try { result = callFn(); } catch (Exception ex) { result = “”; } becomes: val result = try { callFn() } catch (ex: Exception) { “” }
循环支持范围,例如:
for (i in 1..100) { println(i) }
还有一些其他的改进,我们将继续讨论。
考虑到Java的互操作性,建议循序渐进地将Kotlin添加到现有的Java项目中。主流产品的支持项目通常是不错的选择。一旦团队感到舒适了,他们就可以评估自己是否更喜欢完全切换。
选择哪类项目好?
所有的Java项目都可以从Kotlin中获益。但是,具有以下特征的项目可以使决策更简单。
包含大量DTO或模型/实体对象的项目—— 这对于处理CRUD或数据转换的项目非常典型。此类项目往往充斥着 getter/setter 。这里可以利用Kotlin的属性大幅简化类。
大量依赖实用工具类的项目—— Java中的实用工具类通常是为了弥补Java中顶级函数的缺乏。在许多情况下,这包括含全局无状态 public static 函数。这些可以分解成纯函数。更进一步,Kotlin支持类似Function类型这样的FP结构和高阶函数,这可以用来使代码更易于维护和测试。
类中逻辑复杂的项目—— 这些项目容易受到空指针异常(NPE)的影响,而这是Kotlin很好地解决了的其中一个问题。通过让语言分析可能导致NPE的代码路径为开发人员提供支持。Kotlin的 when 结构(一个更好的 switch )在这里非常有用,可以将嵌套的逻辑树分解为可管理的函数。对变量和集合的不变性支持有助于简化逻辑,避免由于引用泄漏而导致难以查找的错误。虽然上面的一些功能可以通过Java实现,但Kotlin的优势在于升级了这些范例,并使它们保持简洁一致。
让我们在这里暂停一下,看一个典型的Java逻辑片段以及对应的Kotlin实现:
public class Sample { public String logic(String paramA, String paramB) { String result = null; try { if (paramA.length() > 10) { throw new InvalidArgumentException(new String[]{"Unknown"}); } else if ("AB".equals(paramA) && paramB == null) { result = subLogicA(paramA + "A", "DEFAULT"); } else if ("XX".equals(paramA) && "YY".equals(paramB)) { result = subLogicA(paramA + "X", paramB + "Y"); } else if (paramB != null) { result = subLogicA(paramA, paramB); } else { result = subLogicA(paramA, "DEFAULT"); } } catch (Exception ex) { result = ex.getMessage(); } return result; } private String subLogicA(String paramA, String paramB) { return paramA + "|" + paramB; } }
对应的Kotlin实现:
fun logic(paramA: String, paramB: String?): String { return try { when { (paramA.length > 10) -> throw InvalidArgumentException(arrayOf("Unknown")) (paramA == "AB" && paramB == null) -> subLogicA(paramA + "A") (paramA == "XX" && paramB == "YY") -> subLogicA(paramA + "X", paramB + "X") else -> if (paramB != null) subLogicA(paramA, paramB) else subLogicA(paramA) } } catch (ex: Exception) { ex.message ?: "UNKNOWN" } } private fun subLogicA(paramA: String, paramB: String = "DEFAULT"): String { return "$paramA|$paramB" }
虽然这些代码片段在功能上是等效的,但是它们有一些明显的区别。
logic() 函数不需要包含在类中。Kotlin提供了顶级函数。这开辟了一个广阔的空间,鼓励我们去思考是否真的需要一个对象。单独的纯函数更容易测试。这为团队提供了采用更简洁的函数方法的选项。
Kotlin引入了 when ,这是一个处理条件流的强大结构。它比 if 或 switch 语句的功能要强大得多。任意逻辑都可以使用 when 进行条理的组织。
注意,在Kotlin版本中,我们从未声明返回变量。这是可能的,因为Kotlin允许我们使用 when 和 try 作为表达式。
在 subLogicA 函数中,我们可以在函数声明中为 paramB 指定一个默认值。
private fun subLogicA(paramA: String, paramB: String = "DEFAULT"): String {
现在,我们可调用任何一个函数签名了:
subLogicA(paramA, paramB)
或者
subLogicA(paramA)
# In this case the paramB used the default value in the function declaration
现在,逻辑更容易理解了,代码行数减少了约35%。
Maven 和 Gradle 通过插件支持Kotlin。Kotlin代码被编译成Java类并包含在构建过程中。 Kobalt 等比较新的构建工具看起来也很有前景。Kobalt受Maven/Gradle启发,但完全是用Kotlin编写的。
首先,将Kotlin插件依赖项添加到 Maven 或 Gradle 构建文件中。
如果你使用的是Spring和JPA,你还应该添加 kotlin-spring 和 kotlin-jpa 编译器插件 。项目的编译和构建没有任何明显的差异。
如果要为Kotlin代码库生成JavaDoc则需要 这个插件 。
有针对IntelliJ和Eclipse Studio的IDE插件,但正如我们所预料的那样,Kotlin的开发和构建工具从IntelliJ关联中获益良多。从社区版开始,该IDE对Kotlin提供了一等支持。其中一个值得注意的特性是,它支持将现有的Java代码自动转换为Kotlin。这种转换很准确,而且是一种很好的学习Kotlin惯用法的工具。
因为我们将Kotlin引入了现有的项目中,所以框架兼容性是一个问题。Kotlin完美融入了Java生态系统,因为它可以编译成Java字节码。一些流行的框架已经宣布支持Kotlin,包括Spring、Vert.x、Spark等。让我们看下Kotlin和Spring及Hibernate一起使用是什么样子。
Spring是Kotlin的早期支持者之一,在2016年首次增加支持。 Spring 5 利用Kotlin提供更简洁的DSL。你可以认为,现有的Java Spring代码无需任何更改就可继续运行。
Spring注释和AOP都是开箱即用的。你可以像注解Java一样注解Kotlin类。考虑下面的服务声明片段。
@Service @CacheConfig(cacheNames = [TOKEN_CACHE_NAME], cacheResolver = "envCacheResolver") open class TokenCache @Autowired constructor(private val repo: TokenRepository) {
这些是标准的Spring注解:
@Service: org.springframework.stereotype.Service
@CacheConfig: org.springframework.cache
注意, constructor 是类声明的一部分。
@Autowired constructor(private val tokenRepo: TokenRepository)
Kotlin将其作为主构造函数,它可以是类声明的一部分。在这个实例中, tokenRepo 是一个内联声明的属性。
编译时常量可以在注解中使用,通常,这有助于避免拼写错误。
Kotlin类默认为 final 的。它提倡将继承作为一种有意识的设计选择。这在Spring AOP中是行不通的,但也不难弥补。我们需要将相关类标记为 open —— Kotlin的非 final 关键字。
IntelliJ会给你一个友好的警告。
你可以通过使用maven插件 all open 来解决这个问题。这个插件可以 open 带有特定注解的类。更简单的方法是将类标记为 open 。
Kotlin严格执行 null 检查。它要求初始化所有标记为不可空的属性。它们可以在声明时或构造函数中初始化。这与依赖注入相反——依赖注入在运行时填充属性。
lateinit 修饰符允许你指定属性将在使用之前被初始化。在下面的代码片段中,Kotlin相信 config 对象将在首次使用之前被初始化。
@Component class MyService { @Autowired lateinit var config: SessionConfig }
虽然 lateinit 对于自动装配很有用,但我建议谨慎地使用它。另一方面,它会关闭属性上的编译时空检查。如果在第一次使用时是 null 仍然会出现运行时错误,但是会丢失很多编译时空检查。
构造函数注入可以作为一种替代方法。这与Spring DI可以很好地配合,并消除了许多混乱。例如:
@Component class MyService constructor(val config: SessionConfig)
这是Kotlin引导你遵循最佳实践的一个很好的例子。
Hibernate和Kotlin可以很好地搭配使用,不需要做大的修改。一个典型的实体类如下所示:
@Entity @Table(name = "device_model") class Device { @Id @Column(name = "deviceId") var deviceId: String? = null @Column(unique = true) @Type(type = "encryptedString") var modelNumber = "AC-100" override fun toString(): String = "Device(id=$id, channelId=$modelNumber)" override fun equals(other: Any?) = other is Device && other.deviceId?.length == this.deviceId?.length && other.modelNumber == this.modelNumber override fun hashCode(): Int { var result = deviceId?.hashCode() ?: 0 result = 31 * result + modelNumber.hashCode() return result } }
在上面的代码片段中,我们利用了几个Kotlin特性:
通过使用属性语法,我们就不必显式地定义 getter 和 setter 了。这减少了混乱,使我们能够专注于数据模型。
在我们可以提供初始值的情况下,我们可以跳过类型规范,因为它可以被推断出来。例如:
var modelNumber = "AC-100"
modelNumber 属性会被推断为 String 类型。
如果我们稍微仔细地看下 toString() 方法,就会发现它有与Java有一些不同:
override fun toString(): String = "Device(id=$id, channelId=$modelNumber)"
它没有返回语句。这里,我们使用了Kotlin表达式。对于返回单个表达式的函数,我们可以省略花括号,通过等号赋值。
"Device(id=$id, channelId=$modelNumber)"
在这里,我们可以更自然地使用模板。Kotlin允许在任何字符串中嵌入 ${表达式} 。这消除了笨拙的连接或对 String.format 等外部辅助程序的依赖。
在 equals 方法中,你可能已经注意到了这个表达式:
other.deviceId?.length == this.deviceId?.length
它用==符号比较两个字符串。在Java中,这是一个长期存在的问题,它将字符串视为相等测试的特殊情况。Kotlin最终修复了这个问题,始终把==用于结构相等测试(Java中的 equals() )。把===用于引用相等检查。
Kotlin还提供一种特殊类型的类,称为数据类。当类的主要目的是保存数据时,这些类就特别适合。数据类会自动生成 equals() 、 hashCode() 和 toString() 方法,进一步减少了样板文件。
有了数据类,我们的最后一个示例就可以改成:
@Entity @Table(name = "device_model") data class Device2( @Id @Column(name = "deviceId") var deviceId: String? = null, @Column(unique = true) @Type(type = "encryptedString") var modelNumber: String = "AC-100" )
这两个属性都作为构造函数的参数传入。 equals 、 hashCode 和 toString 是由数据类提供的。
但是,数据类不提供默认构造函数。这是对于Hibernate而言是个问题,它使用默认构造函数来创建实体对象。这里,我们可以利用 kotlin-jpa 插件,它为JPA实体类生成额外的零参数构造函数。
在JVM语言领域,Kotlin的与众不同之处在于,它不仅关注工程的优雅性,而且解决了现实世界中的问题。
解决Java中的NPE是Kotlin的主要目标之一。将Kotlin引入项目时,显式空检查是最明显的变化。
Kotlin通过引入一些新的操作符解决了空值安全问题。Kotlin的 ? 操作符就提供了空安全调用,例如:
val model: Model? = car?.model
只有当 car 对象不为空时,才会读取 model 属性。如果 car 为空, model 计算为空。注意 model 的类型是 Model? ——表示结果可以为空。此时,流分析就开始起作用了,我们可以在任何使用 model 变量的代码中进行NPE编译时检查。
这也可以用于链式调用:
val year = car?.model?.year
下面是等价的Java代码:
Integer year = null; if (car != null && car.model != null) { year = car.model.year; }
一个大型的代码库会省掉许多这样的 null 检查。编译时安全自动地完成这些检查可以节省大量的开发时间。
在表达式求值为空的情况下,可以使用Elvis操作符( ?: )提供默认值:
val year = car?.model?.year ?: 1990
在上面的代码片段中,如果 year 最终为 null ,则使用值 1990 。如果左边的表达式为空,则 ?: 操作符取右边的值。
Kotlin以Java 8的功能为基础构建,并提供了一等函数。一等函数可以存储在变量/数据结构中并传递出去。例如,在Java中,我们可以返回函数:
@FunctionalInterface interface CalcStrategy { Double calc(Double principal); } class StrategyFactory { public static CalcStrategy getStrategy(Double taxRate) { return (principal) -> (taxRate / 100) * principal; } }
Kotlin让这个过程变得更加自然,让我们可以清晰地表达意图:
// Function as a type typealias CalcStrategy = (principal: Double) -> Double fun getStrategy(taxRate: Double): CalcStrategy = { principal -> (taxRate / 100) * principal } 当我们深入使用函数时,事情就会发生变化。下面的Kotlin代码片段定义了一个生成另一个函数的函数: val fn1 = { principal: Double -> { taxRate: Double -> (taxRate / 100) * principal } }
我们很容易调用 fn1 及结果函数:
fn1(1000.0) (2.5)
输出
25.0
虽然以上功能在Java中也可以实现,但并不直接,并且包含样板代码。
提供这些功能是为了鼓励团队尝试FP概念,开发出更符合要求的代码,从而得到更稳定的产品。
注意,Kotlin和Java的lambda语法略有不同。这在早期可能会给开发人员带来烦恼。
Java代码:
( Integer first, Integer second ) -> first * second
等价的Kotlin代码:
{ first: Int, second: Int -> first * second }
随着时间的推移,情况就变得明显了,Kotlin支持的应用场景需要修改后的语法。
Kotlin最被低估的优点之一是它可以减少项目中的文件数量。Kotlin文件可以包含多个/混合类声明、函数和枚举类等其他结构。这提供了许多Java没有提供的可能性。另一方面,它提供了一种新的选择——组织类和函数的正确方法是什么?
在《代码整洁之道》一书中,Robert C Martin打了报纸的比方。好代码应该读起来和报纸一样——高级结构在文件上部,越往下面越详细。这个文件应该讲述一个紧凑的故事。Kotlin的代码布局从这个比喻中可见一斑。
建议是——把相似的东西放在一起——放在更大的上下文里。
虽然Kotlin不会阻止你放弃“结构(structure)”,但这样做会使后续的代码导航变得困难。组织东西要以它们之间的关系和使用顺序为依据,例如:
enum class Topic { AUTHORIZE_REQUEST, CANCEL_REQUEST, DEREG_REQUEST, CACHE_ENTRY_EXPIRED } enum class AuthTopicAttribute {APP_ID, DEVICE_ID} enum class ExpiryTopicAttribute {APP_ID, REQ_ID} typealias onPublish = (data: Map<String, String?>) -> Unit interface IPubSub { fun publish(topic: Topic, data: Map<String, String?>) fun addSubscriber(topic: Topic, onPublish: onPublish): Long fun unSubscribe(topic: Topic, subscriberId: Long) } class RedisPubSub constructor(internal val redis: RedissonClient): IPubSub { ...}
在实践中,通过减少为获得全貌而需要跳转的文件数量,可以显著减少脑力开销。
一个常见的例子是Spring JPA库,它使包变得混乱。可以把它们重新组织到同一个文件中:
@Repository @Transactional interface DeviceRepository : CrudRepository<DeviceModel, String> { fun findFirstByDeviceId(deviceId: String): DeviceModel? } @Repository @Transactional interface MachineRepository : CrudRepository<MachineModel, String> { fun findFirstByMachinePK(pk: MachinePKModel): MachineModel? } @Repository @Transactional interface UserRepository : CrudRepository<UserModel, String> { fun findFirstByUserPK(pk: UserPKModel): UserModel? }
上述内容的最终结果是代码行数(LOC)显著减少。这直接影响了交付速度和可维护性。
我们统计了Java项目中移植到Kotlin的文件数量和代码行数。这是一个典型的REST服务,包含数据模型、一些逻辑和缓存。在Kotlin版本中,LOC减少了大约50%。开发人员在跨文件浏览和编写样板代码上消耗的时间明显减少。
编写简洁的代码是一个宽泛的话题,这取决于语言、设计和技术的结合。然而,Kotlin提供了一个良好的工具集,为团队的成功奠定了基础。下面是一些例子。
类型推断最终会减少代码中的噪音。这有助于开发人员关注代码的目标。
类型推断可能会增加我们跟踪正在处理的对象的难度,这是一种常见的担忧。从实际经验来看,这种担忧只在少数情况下有必要,通常少于5%。在大多数情况下,类型是显而易见的。
下面的例子:
LocalDate date = LocalDate.now(); String text = "Banner";
变成了:
val date = LocalDate.now() val text = "Banner"
在Kotlin中,也可以指定类型:
val date: LocalDate = LocalDate.now() val text: String = "Banner"
值得注意的是,Kotlin提供了一个全面的解决方案。例如,在Kotlin中,我们可以将函数类型定义为:
val sq = { num: Int -> num * num }
另一方面,Java 10通过检查右边表达式的类型推断类型。这引入了一些限制。如果我们尝试在Java中执行上述操作,我们会得到一个错误:
这是Kotlin中一个方便的特性,它允许我们为现有类型分配别名。它不引入新类型,但允许我们使用替代名称引用现有类型,例如:
typealias SerialNumber = String
SerialNumber 现在是 String 类型的别名,可以与 String 类型互换使用,例如:
val serial: SerialNumber = "FC-100-AC"
和下面的代码等价:
val serial: String = "FC-100-AC"
很多时候, typealias 可以作为一个“ 解释变量 ”,提高清晰度。考虑以下声明:
val myMap: Map<String, String> = HashMap()
我们知道 myMap 包含字符串,但我们不知道这些字符串表示什么。我们可以通过引入 String 类型的别名来澄清这段代码:
typealias ProductId = String typealias SerialNumber = String
现在,上述 myMap 的声明可以改成:
val myMap: Map<ProductId, SerialNumber> = HashMap()
上面两个 myMap 的定义是等价的,但是对于后者,我们可以很容易地判断 Map 的内容。
Kotlin编译器用底层类型替换了类型别名。因此, myMap 的运行时行为不受影响,例如:
myMap.put(“MyKey”, “MyValue”)
这种钙化的累积效应是减少了难以捉摸的Bug。在大型分布式团队中,错误通常是由于未能沟通意图造成的。
早期获得吸引力通常是引入变革的最困难的部分。从确定合适的实验项目开始。通常,有一些早期的采用者愿意尝试并编写最初的Kotlin代码。在接下来的几周里,更大的团队将有机会查看这些代码。人们早期的反应是避免新的和不熟悉的东西。变革需要一些时间来审视。通过提供阅读资源和技术讲座来帮助评估。在最初的几周结束时,更多的人可以决定在多大程度上采用。
对于熟悉Java的开发人员来说,学习曲线很短。以我的经验来看,大多数Java开发人员在一周内都能高效地使用Kotlin。初级开发人员可以在没有经过特殊培训的情况下使用它。以前接触过不同语言或熟悉FP概念会进一步减少采用时间。
从1.1版本开始,“协同例程(Co-routine)”就可以用在Kotlin中了。在概念上,它们类似于JavaScript中的 async/await 。它们允许我们在不阻塞线程的情况下挂起流,从而降低异步编程中的复杂性。
到目前为止,它们还被标记为实验性的。协同例程将在1.3版本中从实验状态毕业。这带来了更多令人兴奋的机会。
Kotlin的路线图在Kotlin Evolution and Enhancement Process( KEEP )的指导下制定。请密切关注这方面的讨论和即将发布的特性。
Baljeet Sandhu 是一名技术负责人,拥有丰富的经验,能够为从制造到金融的各个领域提供软件。他对代码整洁、安全和可扩展的分布式系统感兴趣。Baljeet目前为 HYPR 工作,致力于构建非集中式的认证解决方案,以消除欺诈,提高用户体验,实现真正的无密码安全。
查看英文原文: An Introduction to Kotlin for Serverside Java Developers