32

写给服务器端Java开发人员的Kotlin简介

 5 years ago
source link: http://www.infoq.com/cn/articles/intro-kotlin-java-developers?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文要点

  • Kotlin为JVM平台带来了编译时空检查、功能切面和富有表达力的语法
  • Kotlin可以与Java互操作,可以逐步引入到现有的Java项目中
  • 对于拥有大量样板文件和逻辑的项目,Kotlin是一个不错的选择
  • Kotlin很好地集成了流行的框架,包括Spring和Hibernate
  • Kotlin可以通过消除样板文件来显著减少Java代码库的大小

Kotlin简介

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引入了许多改进,这些改进对代码的可读性产生了重大影响。举几个例子:

  • 分号是可选的
  • 大括号在没有用处的情况下是可选的
  • Getter/Setter是可选的
  • 一切都是对象——如果需要,在后台自动使用原语
  • 表达式: 表达式 求值时返回结果

在Kotlin中,所有的 函数 都是表达式,因为它们至少返回 Unit 。控制流语句如 iftrywhen (类似于 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) }

还有一些其他的改进,我们将继续讨论。

把Kotlin引入Java项目

循序渐进

考虑到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 ,这是一个处理条件流的强大结构。它比 ifswitch 语句的功能要强大得多。任意逻辑都可以使用 when 进行条理的组织。

注意,在Kotlin版本中,我们从未声明返回变量。这是可能的,因为Kotlin允许我们使用 whentry 作为表达式。

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%。

把Kotlin加入Java构建

MavenGradle 通过插件支持Kotlin。Kotlin代码被编译成Java类并包含在构建过程中。 Kobalt 等比较新的构建工具看起来也很有前景。Kobalt受Maven/Gradle启发,但完全是用Kotlin编写的。

首先,将Kotlin插件依赖项添加到 MavenGradle 构建文件中。

如果你使用的是Spring和JPA,你还应该添加 kotlin-springkotlin-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

Spring是Kotlin的早期支持者之一,在2016年首次增加支持。 Spring 5 利用Kotlin提供更简洁的DSL。你可以认为,现有的Java Spring代码无需任何更改就可继续运行。

Kotlin中的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 是一个内联声明的属性。

编译时常量可以在注解中使用,通常,这有助于避免拼写错误。

final类处理

Kotlin类默认为 final 的。它提倡将继承作为一种有意识的设计选择。这在Spring AOP中是行不通的,但也不难弥补。我们需要将相关类标记为 open —— Kotlin的非 final 关键字。

IntelliJ会给你一个友好的警告。

1intro-kotlin-java-developers-2-1539373380685.jpg

你可以通过使用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

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特性:

属性

通过使用属性语法,我们就不必显式地定义 gettersetter 了。这减少了混乱,使我们能够专注于数据模型。

类型推断

在我们可以提供初始值的情况下,我们可以跳过类型规范,因为它可以被推断出来。例如:

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"
)

这两个属性都作为构造函数的参数传入。 equalshashCodetoString 是由数据类提供的。

但是,数据类不提供默认构造函数。这是对于Hibernate而言是个问题,它使用默认构造函数来创建实体对象。这里,我们可以利用 kotlin-jpa 插件,它为JPA实体类生成额外的零参数构造函数。

在JVM语言领域,Kotlin的与众不同之处在于,它不仅关注工程的优雅性,而且解决了现实世界中的问题。

采用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中执行上述操作,我们会得到一个错误:

1intro-kotlin-java-developers-1539373380274.jpg

类型别名

这是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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK