记吃到的Kotlin语法糖总结

最近在着手写一个TornadoFx的项目,接触了之前从未写过的Kotlin,吃到了大量的语法糖,给我一种“到处充满了Lambda”的既视感。

很多东西一个花括号就能搞定,隐藏了很多“当场实现接口”、“覆写方法”的繁琐语法,确实写起来非常的舒服,写篇文章总结一下。

目前来说,我体会到的主要有这些语法糖:

  • 构造块
    替代了构造函数,而Java中的有参构造被移到了类名右边的圆括号内:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//java
public class XXXBo{
public XXXBo(){

}
public XXXBo(int i){

}
}

//kotlin
class XXXBo(var i: Int) {
init{

}
}

顺带提一下延迟加载,Kotlin要求显式声明在声明时不赋值的变量:
(读起来可能有点拗口,意思是默认情况下如果变量声明不赋值,是一个编译期错误)

1
2
//编译错误:Property must be initialized or be abstract
//var bo:XXXBo;

只有加入lateinit 关键字才允许之后在其他地方赋值:

1
lateinit var bo:XXXBo;

延迟加载机制能和DI框架很好的配合,虽然Spring的写法是这样的:

1
var bo:XXXBo by di();

需要和延迟加载区分的一个机制是懒加载,代码是这样的:

1
2
3
val bo by lazy{
XXXBo(param1);
}

注意,懒加载用于val变量,一个非常对口的应用场景就是单例模式,如果在单例模式中使用lateinit虽然也可以实现,但是会增加不必要的逻辑。

  • 分号省略

代码略;每一行代码的分号可以省略,(不知道编译那边是怎么做的……)

  • 类型推断+使用var来声明变量

这个在类名很长的时候兼职就是拯救世界……

1
2
3
4
5
6
//java
XXXBo bo = new XXXBo();
final XXXBo bo = new XXXBo();
//kotlin
var bo = XXXBo();
val bo = XXXBo();
  • 进一步简化的try-with-resource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//java6
try{
InputStream is = FileInputStream(filename);
}catch(Exception e){

}finally{
is.close();
}
//java7+
try (InputStream is = FileInputStream(filename)) {

}catch(Exception e){

}
//kotlin
FileInputStream(filename).use {
//使用it对象来使用这个流本身
var properties:Properties = Properties();
properties.load(it)
}

这个use()方法本质是调用close()方法,见这个回答

  • 对NPE进行了最大限度的消除:强制null检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//java
XXXBo bo = null;
bo.getXXX();//NPE

//kotlin
//编译错误:Null can not be a value of a non-null type XXXBo
//var bo:XXXBo = null;

var bo:XXXBo? = null;

//编译错误:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type XXXBo
//bo.count;

if(bo !=null){
bo.count;
}
  • 对Get/Set进行最大程度的消除(数据类):

Java里其实有个叫Lombok的东西干了差不多的事,在编译器自动生成Getter Setter equals() toString()这么些方法。
不过Kotlin里调用get/set方法也可以不用方法名,而是直接.属性名。
顺带一提,Kotlin的主方法由于没有static方法,用的是伴生对象,并且方法加入@JvmStatic注解。
不过IDEA似乎没法用psvm来快速生成了,也不知道是复杂了还是简单了(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//java
public class XXXBo{
private Integer id;
//getter setter
}
psvm{
new XXXBo().getId();
}
//kotlin
data class XXXBo(val id:Int){

}
companion object {
@JvmStatic
fun main(args: Array<String>) {
XXXBo().id;
}
}
  • 语言层面的TODO支持:
1
2
3
4
5
6
7
8
9
10
11
12
13
//java

//TODO xxx
//完全依赖IDE语法高亮,对实际编译的代码来说就是空代码

//kotlin
//标准库提供TODO函数,执行此处代码会抛出一个异常

TODO()

//@kotlin.internal.InlineOnly
//public inline fun TODO(): Nothing = throw NotImplementedError()

  • KDoc(对位JavaDoc)支持Markdown语法

详见链接

jOOQ比起MyBatis的便利之处

单位新项目用了新的持久化框架,叫jOOQ

用惯MyBatis的我一开始有些不适应,但是写过新项目之后,回去维护老项目,确实感觉基于XML的MyBatis编码效率低下。

首先,两边同样有自动生成代码的工具。
MyBatis生成的有Dao层和pojo,需要配置生成哪张表的pojo,(随口一提,公司的生成器配置可能有bug,生成XML时并不会清空之前的XML内容,需要手动清空/删除一次。)

生成的dao提供的方法有:

  • 根据主键获取
  • 插入
  • 可选插入(pojo对应字段不是null的时候才设值)
  • 更新
  • 可选更新(不是null的时候才更新值)
  • 逻辑删除

pojo则是数据库的各字段以及get/set方法,值得一提的是每个字段上会有DDL里的列备注,但是会充斥大量空行,阅读起来较为困难。

而jOOQ的代码生成器一次性生成所有表的dao层和pojo。dao层全部继承了一个叫DAOImpl的抽象类,提供了以下方法:

  • 新增单个pojo
  • 新增多个pojo(可变参数)
  • 新增多个pojo(集合)
  • 用以上三种方式更新、物理删除pojo
  • 根据主键判断是否存在
  • 获取表中记录总数
  • 获取整张表
  • 利用Java8的Optional来获取(大概是允许参数为null?)
  • 获取主键

还有一些看不太懂的方法:

  • private /* non-final */ Condition equal(Field<?>[] pk, Collection<T> ids)
  • private /* non-final */ List<R> records(Collection<P> objects, boolean forUpdate)
  • private /* non-final */ RecordListenerProvider[] providers(final RecordListenerProvider[] providers, final Object object)

而每个表不同的dao实现类也有各自的方法:

  • 根据主键(项目里就是id)获取
  • 根据多个id获取多个记录(可变参数)
  • 根据每个唯一索引获取记录
  • 根据每个列获取多条记录,也支持可变参数

比MyBatis丰富很多,唯一的缺陷是没有自动生成的逻辑删除方法,初次维护项目很容易根据直觉使用delete方法,需要自动生成Service层代码进行封装。

生成的pojo则兼容了JPA的注解(目前没有在项目里用上),这方面有点hibernate的画风? 较MyBatis生成的pojo要紧凑很多,少了很多无谓的空行。
当业务变化,建立新表,jOOQ生成的代码可以快速满足大部分业务。

说完自动生成的代码,接下来谈谈编写业务代码的复杂度。
贴一段使用MyBatis的传统项目里的单表分页查询代码:

首先需要编写XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="对应的dao接口类名">
<select id="selectByPageQuery"
resultMap="自动生成的Mapper里的ResultMap">
select * from 表
where is_deleted = 0
order by id desc
LIMIT #{itemIndex}, #{pageSize}
</select>

<select id="countByPageQuery" resultType="java.lang.Integer">
select count(*) from 表
where is_deleted = 0
LIMIT #{itemIndex}, #{pageSize}
</select>
</mapper>

由于多个Mapper需要公用LIMIT #{itemIndex}, #{pageSize} 这段语句(还有一些权限控制的语句),事实上代码是这样的:

1
<include refid="com.***.dao.mapper.fragment.PageMapper.pageLimit"/>

所以无法使用注解方式的SQL。
接下来是接口:

1
2
3
4
public interface 表SimpleSelectDao {
int countByPageQuery(PageQuery query);
List<自动生成的实体类> selectByPageQuery(PageQuery query);
}

其中PageQuery是公司封装的分页类,不贴代码了,可以从SQL里看出用到的字段。
然后再在Service层使用:

1
2
3
4
5
...
int count = parcelTransferRecordSimpleSelectDao.countByPageQuery(query);
query.setItemTotal(count);
List<TxParcelTransferRecord> records = parcelTransferRecordSimpleSelectDao.selectByPageQuery(query);
...

相对于基于XML的MyBatis,jOOQ在业务变动时编写代码的效率更高,同样是分页获取代码,只需要7行:

说明:
Pageable是Spring 5的分页相关类,专门解决从前端传参到数据库查询的分页问题,不需要自己实现分页框架了。
Tuple2是公司内部实现的数据结构,用来返回两个不同类型的数据。
SelectConditionStepdsl是jOOQ提供的类和对象,其中dsl的类是DSLContext,用于执行SQL,改写生成器后可以使用Spring注入。

1
2
3
4
5
6
7
8
9
public Tuple2<List<表实体类>, Integer> pageVoById(Pageable pageable) {
SelectConditionStep<表Record类> step = dsl.selectFrom(表的枚举)
.where(表的枚举.IS_DELETED.eq(false));
List<表实体类> list = step.orderBy(表的枚举.ID.desc())
.limit((int) pageable.getOffset(), pageable.getPageSize())
.fetchInto(表实体类.class);
int count = dsl.fetchCount(step);
return new Tuple2<>(list, count);
}

这段代码出现在Service层,用惯之后很符合人类直觉,发挥了SQL人类可读性好的优势。

jOOQ封装了sql,将所有的字段和SQL语句化为Java代码,当然也可以传入完整或者部分的sql执行,自由度很高,同时有Java强类型的优势加持,屏蔽掉了MyBatis的ResultMapper环节,每一步SQL执行虽然要使用一些复杂的类和对象(例如SelectConditionStep),对习惯使用强类型语言的java码狗来说很有“安全感”。

相比jOOQ,基于XML的MyBatis代码编写繁琐,手写SQL经常漏掉order by id desc(页面上刚刚新增的记录在最前面)和is_deleted = 0(其实这是低级失误),查找SQL需要跳转两次(这还是装了IDE插件的情况),在多表查询时还要编写复杂的ResultMap,体验确实差很多。