跳到主要内容

Kotlin 函数

这篇文章我们将一起来认识 Kotlin 的函数,Kotlin 中的函数可以说比 Java 中的函数更优,解决 Java 函数调用中存在一些问题。此外还会介绍 Java 中没有的函数,比如顶层函数、中缀函数等等。

1. 函数命名参数

1.1 java中函数调用存在什么问题?

在 Java 开发程序的过程中,经常会去调用一些方法,有的人设计方法的参数有很多,而且参数命名有时候还不太规范(不能达到见名知意),并且设计的时候几个相同的参数类型还是挨着的。这个实际上给调用者带来了很大的困扰和麻烦,谨慎的程序猿会定位到这个函数定义的地方,大概看了下参数的调用的顺序以及每个参数的函数。特别当这个方法被打包成一个 lib 的时候,查看就比较麻烦。而且相同类型参数挨着很容易对应错。一起看个例子:

 // A开发的函数接口顺序(调用者必须按照这个顺序传)
static String joinToString(List<Integer> nums, String prex, String sep, String postfix) {
StringBuilder builder = new StringBuilder(prex);
for (int i = 0; i < nums.size(); i++) {
builder.append(i);
if (i < nums.size() - 1) {
builder.append(sep);
}
}
builder.append(postfix);
return builder.toString();
}

// B开发的函数接口顺序(调用者必须按照这个顺序传)
static String joinToString(List<Integer> nums, String prex, String postfix, String sep) {
StringBuilder builder = new StringBuilder(prex);
for (int i = 0; i < nums.size(); i++) {
builder.append(i);
if (i < nums.size() - 1) {
builder.append(sep);
}
}
builder.append(postfix);
return builder.toString();
}

//假如现在修改一下,拼接串前缀或分隔符,仅从外部调用是无法知道哪个参数是前缀、分隔符、后缀
public static void main(String[] args) {
//后面传入的三个字符串顺序很容易传错,并且外部调用者如果不看具体函数定义根本很难知道每个字符串参数的含义,特别公司中一些远古代码,可能还得打成库的代码,点进去看实现肯定蛋疼。
//调用A的接口
System.out.println(joinToString(Arrays.asList(new Integer[]{1, 3, 5, 7, 9}), "<", ",", ">"));
//调用B的接口
System.out.println(joinToString(Arrays.asList(new Integer[]{1, 3, 5, 7, 9}), "<", ">", ","));
}

其实针对以上问题,AndroidStudio 的给我们做了个很好的优化提示,但是在 3.0 之前是没有这个提示的,如图:

图片描述

实际上 jetBrains 实际上把这个提示是直接融入了他们开发的Kotlin语言中,力图开发者尽量让在语法的层面上少犯错误,少走弯路,更加注重于代码本身的实现;让你直接在语法的层面上就更加明确,减少疑惑性。

1.2 kotlin函数命名参数解决问题

针对以上遇到的问题 Kotlin 可以很好的解决,在 Kotlin 函数中有这么一种参数叫做命名参数,它能允许在函数调用的地方指定函数名,这样就能很好使得调用地方的参数和函数定义参数一一对应起来,不会存在传递参数错乱问题。

//kotlin一个函数的接口满足以上三种顺序调用的接口,准确来说是参数列表中任意参数顺序组合的调用
fun joinToString(nums: List<Int>, prex: String, sep: String, postfix: String): String {
val builder = StringBuilder(prex)
for (i in nums.indices) {
builder.append(i)
if (i < nums.size - 1) {
builder.append(sep)
}
}
builder.append(postfix)
return builder.toString()
}
fun main(args: Array<String>) {
//调用kotlin函数接口,符合A的接口需求,且调用更加明确
println(joinToString(nums = listOf(1, 3, 5, 7, 9), prex = "<", sep = ",", postfix = ">"))
//调用kotlin函数接口,满足B的接口需求,且调用更加明确
println(joinToString(nums = listOf(1, 3, 5, 7, 9), prex = "<", postfix = ">", sep = ","))
}

图片描述

2. 函数默认值参数

2.1 java中函数重载存在什么问题

无论是在 Java 或者 C++ 中都有函数重载一说,函数重载目的为了针对不同功能业务需求,然后暴露不同参数的接口,包括参数列表个数,参数类型,参数顺序。

也就是说几乎每个不同需求都得一个函数来对应,随着以后的扩展,这个类中的相同名字函数会堆成山,而且每个函数之间又存在层级调用,函数与函数之间的参数列表差别有时候也是细微的,所以在调用方也会感觉很疑惑。举个例子(Android图片加载框架我们都习惯于再次封装一次,以便调用方便)

fun ImageView.loadUrl(url: String) {//ImageView.loadUrl这个属于扩展函数,后期会介绍,暂时可以先忽略
loadUrl(Glide.with(context), url)
}

fun ImageView.loadUrl(requestManager: RequestManager, url: String) {
loadUrl(requestManager, url, false)
}

fun ImageView.loadUrl(requestManager: RequestManager, url: String, isCrossFade: Boolean) {
ImageLoader.newTask(requestManager).view(this).url(url).crossFade(isCrossFade).start()
}

fun ImageView.loadUrl(urls: List<String>) {
loadUrl(Glide.with(context), urls)
}

fun ImageView.loadUrl(requestManager: RequestManager, urls: List<String>) {
loadUrl(requestManager, urls, false)
}

fun ImageView.loadUrl(requestManager: RequestManager, urls: List<String>, isCrossFade: Boolean) {
ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).start()
}

fun ImageView.loadRoundUrl(url: String) {
loadRoundUrl(Glide.with(context), url)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, url: String) {
loadRoundUrl(requestManager, url, false)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, url: String, isCrossFade: Boolean) {
ImageLoader.newTask(requestManager).view(this).url(url).crossFade(isCrossFade).round().start()
}

fun ImageView.loadRoundUrl(urls: List<String>) {
loadRoundUrl(Glide.with(context), urls)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, urls: List<String>) {
loadRoundUrl(requestManager, urls, false)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, urls: List<String>, isCrossFade: Boolean) {
ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).round().start()
}
//调用的地方
activity.home_iv_top_banner.loadUrl(bannerUrl)
activity.home_iv_top_portrait.loadUrl(portraitUrls)
activity.home_iv_top_avatar.loadRoundUrl(avatarUrl)
activity.home_iv_top_avatar.loadRoundUrl(avatarUrls)

2.2 kotlin函数默认值参数解决问题

针对以上例子的那么重载方法,实际上交给 Kotlin 只需要一个方法就能解决实现,并且调用的时候非常方便。实际上在Kotlin中还存在一种函数参数叫做默认值参数。它就可以解决函数重载问题,并且它在调用的地方结合我们上面所讲的命名参数一起使用会非常方便和简单。

//学完命名参数和默认值参数函数,立即重构后的样子
fun ImageView.loadUrl(requestManager: RequestManager = Glide.with(context)
, url: String = ""
, urls: List<String> = listOf(url)
, isRound: Boolean = false
, isCrossFade: Boolean = false) {
if (isRound) {
ImageLoader.newTask(requestManager).view(this).url(urls).round().crossFade(isCrossFade).start()
} else {
ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).start()
}
}
//调用的地方
activity.home_iv_top_banner.loadUrl(url = bannerUrl)
activity.home_iv_top_portrait.loadUrl(urls = portraitUrls)
activity.home_iv_top_avatar.loadUrl(url = avatarUrl, isRound = true)
activity.home_iv_top_avatar.loadUrl(urls = avatarUrls, isRound = true)

在 Kotlin 中,当调用一个 Kotlin 定义的函数时,可以显示地标明一些参数的名称,而且可以打乱顺序参数调用顺序,因为可以通过参数名称就能唯一定位具体对应参数。通过以上代码发现 Kotlin 的默认值函数完美解决函数重载问题,而命名函数解决了函数调用问题,并且实现任意顺序指定参数名调用函数的参数。

2.3 java与kotlin互调时函数重载应该注意哪些问题

使用 @JvmOverloads 注解解决 Java 调用 Kotlin 重载函数问题:

@JvmOverloads //@JvmOverloads注解
fun <T> joinString(
collection: Collection<T> = listOf(),
separator: String = ",",
prefix: String = "",
postfix: String = ""
): String {
return collection.joinToString(separator, prefix, postfix)
}
//调用的地方
fun main(args: Array<String>) {
//函数使用命名参数可以提高代码可读性
println(joinString(collection = listOf(1, 2, 3, 4), separator = "%", prefix = "<", postfix = ">"))
println(joinString(collection = listOf(1, 2, 3, 4), separator = "%", prefix = "<", postfix = ">"))
println(joinString(collection = listOf(1, 2, 3, 4), prefix = "<", postfix = ">"))
println(joinString(collection = listOf(1, 2, 3, 4), separator = "!", prefix = "<"))
println(joinString(collection = listOf(1, 2, 3, 4), separator = "!", postfix = ">"))
println(joinString(collection = listOf(1, 2, 3, 4), separator = "!"))
println(joinString(collection = listOf(1, 2, 3, 4), prefix = "<"))
println(joinString(collection = listOf(1, 2, 3, 4), postfix = ">"))
println(joinString(collection = listOf(1, 2, 3, 4)))
}

Kotlin 调用 Java 不能使用命名参数和默认值参数:

在 Kotlin 中函数使用命名参数即使在 Java 重载了很多构造器方法或者普通方法,在 Kotlin 中调用 Java中的方法是不能使用命名参数的,不管你是 JDK 中的函数或者是 Android 框架中的函数都是不允许使用命名参数的。

3. 顶层函数

3.1 顶层函数替代java中的static函数

我们都知道静态函数内部是不包含状态的,也就是所谓的纯函数,它的输入仅仅来自于它的参数列表,而它的输出也仅仅依赖于它参数列表。设想一下这样开发情景,有时候我们并不想利用实例对象来调用函数,所以我们一般会往静态函数容器类中添加静态函数,如此反复,这样无疑是让这个类容器膨胀。

而在 Kotlin 中则认为一个函数或方法有时候并不是属于任何一个类,它可以独立存在。所以在 Kotlin中类似静态函数和静态属性会去掉外层类的容器,一个函数或者属性可以直接定义在一个 Kotlin 文件的顶层中,在使用的地方只需要 import 这个函数或属性即可。

3.2 顶层函数和属性的基本使用

在 Koltin 中根本不需要去定义一些没有意义包裹静态函数的容器类,它们都被顶层文件给替代。我们只需要定义一个 Kotlin File,在里面定义好一些函数(注意:不需要 static 关键字)。那么这些函数就可以当做静态函数来使用:

package com.imooc.kotlin.top

import java.math.BigDecimal
//这个顶层函数不属于任何一个类,不需要类容器,不需要static关键字
fun formateFileSize(size: Double): String {
if (size < 0) {
return "0 KB"
}

val kBSize = size / 1024
if (kBSize < 1) {
return "$size B"
}

val mBSize = kBSize / 1024
if (mBSize < 1) {
return "${BigDecimal(kBSize.toString()).setScale(1, BigDecimal.ROUND\_HALF\_UP).toPlainString()} KB"
}

val mGSize = mBSize / 1024
if (mGSize < 1) {
return "${BigDecimal(mBSize.toString()).setScale(1, BigDecimal.ROUND\_HALF\_UP).toPlainString()} MB"
}

val mTSize = mGSize / 1024
if (mTSize < 1) {
return "${BigDecimal(mGSize.toString()).setScale(1, BigDecimal.ROUND\_HALF\_UP).toPlainString()} GB"
}
return "${BigDecimal(mTSize.toString()).setScale(1, BigDecimal.ROUND\_HALF\_UP).toPlainString()} TB"
}

//测试顶层函数,实际上Kotlin中main函数和Java不一样,它可以不存在任何类容器中,可以直接定义在一个Kotlin 文件中
//另一方面也解释了Kotlin中的main函数不需要了static关键字,实际上它自己就是个顶层函数。
fun main(args: Array<String>) {
println("文件大小: ${formateFileSize(15582.0)}")
}

3.3 顶层函数的原理

通过以上例子思考一下顶层函数在 JVM 中是怎么运行的?仅仅是在Kotlin中使用这些顶层函数,那么可以不用细究。但是 Java 和 Kotlin 混合开发模式,那么就有必要深入内部原理。都知道 Kotlin 和 Java 互操作性是很强的,所以就衍生出了一个问题:在 Kotlin 中定义的顶层函数,在Java可以调用吗?答案肯定是可以的。要想知道内部调用原理很简单,我们只需要把上面例子代码反编译成 Java 代码就一目了然了:

package com.imooc.kotlin.top;

import java.math.BigDecimal;
import kotlin.Metadata;
import kotlin.jvm.JvmName;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
mv = {1, 1, 9},
bv = {1, 0, 2},
k = 2,
d1 = {"\u0000\u001c\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0006\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\b\u0002\u001a\u000e\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0003\u001a\u0019\u0010\u0004\u001a\u00020\u00052\f\u0010\u0006\u001a\b\u0012\u0004\u0012\u00020\u00010\u0007¢\u0006\u0002\u0010\b¨\u0006\t"},
d2 = {"formateFileSize", "", "size", "", "main", "", "args", "", "([Ljava/lang/String;)V", "production sources for module Function"}
)
@JvmName(//注意这里多了注解
name = "FileFormatUtil"
)
public final class FileFormatUtil {//这里生成的类名就是注解中自定义生成的类名了
@NotNull
public static final String formateFileSize(double size) {
if(size < (double)0) {
return "0 KB";
} else {
double kBSize = size / (double)1024;
if(kBSize < (double)1) {
return "" + size + " B";
} else {
double mBSize = kBSize / (double)1024;
if(mBSize < (double)1) {
return "" + (new BigDecimal(String.valueOf(kBSize))).setScale(1, 4).toPlainString() + " KB";
} else {
double mGSize = mBSize / (double)1024;
if(mGSize < (double)1) {
return "" + (new BigDecimal(String.valueOf(mBSize))).setScale(1, 4).toPlainString() + " MB";
} else {
double mTSize = mGSize / (double)1024;
return mTSize < (double)1?"" + (new BigDecimal(String.valueOf(mGSize))).setScale(1, 4).toPlainString() + " GB":"" + (new BigDecimal(String.valueOf(mTSize))).setScale(1, 4).toPlainString() + " TB";
}
}
}
}
}

public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var1 = "文件大小: " + formateFileSize(15582.0D);
System.out.println(var1);
}
}

通过以上的代码可以总结出两点内容:

  • 1、顶层文件会反编译成一个容器类。(类名一般默认就是顶层文件名+"Kt"后缀,注意容器类名可以自定义)
  • 2、顶层函数会反编译成一个 static 静态函数,如代码中的 formateFileSize 和 main 函数。

4. 中缀函数

中缀调用实际上就是把原来只有一个参数的函数调用简化成两个操作直接使用类似中缀运算符调用,省略了类名或者 对象名+"."+函数名 调用方式:

例子1 初始化 map

package com.imooc.kotlin.infix

//普通利用Pair()初始化一个map
fun main(args: Array<String>) {
val map = mapOf(Pair(1, "A"), Pair(2, "B"), Pair(3, "C"))
map.forEach { key, value ->
println("key: $key value:$value")
}
}

//利用to函数初始化一个map
fun main(args: Array<String>) {
val map = mapOf(1.to("A"), 2.to("B"), 3.to("C"))
map.forEach { key, value ->
println("key: $key value:$value")
}
}

//利用to函数中缀调用初始化一个map
fun main(args: Array<String>) {
val map = mapOf(1 to "A", 2 to "B", 3 to "C")//to实际上一个返回Pair对象的函数,不是属于map结构内部的运算符,但是to在语法层面使用很像中缀运算符调用
map.forEach { key, value ->
println("key: $key value:$value")
}
}

例子2 字符串比较

//普通使用字符串对比调用StringUtils.equals(strA, strB)
fun main(args: Array<String>) {
val strA = "A"
val strB = "B"
if (StringUtils.equals(strA, strB)) {//这里对比字符串是了apache中的StringUtils
println("str is the same")
} else {
println("str is the different")
}
}

//利用中缀调用sameAs对比两个字符串
fun main(args: Array<String>) {
val strA = "A"
val strB = "B"
if (strA sameAs strB) {//中缀调用 sameAs
println("str is the same")
} else {
println("str is the different")
}
}

4.1 中缀函数基本使用

中缀调用使用非常简单,准确来说它使用类似加减乘除运算操作符的使用。调用结构: A (中缀函数名) B 例如:element into list。

4.2 中缀调用原理

to 中缀函数为例分析:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

图片描述

分析: 使用infix关键字修饰的函数,传入 A,B 两个泛型对象,“A.to(B)” 结构,是一种特殊结构暂时把它叫做带接收者的结构,以至于后面的 this 就是指代 A,并且函数的参数只有一个,返回的是一个 Pair对象,this 指代 A,that 就是传入的 B 类型对象。

4.3 中缀函数需要注意的问题

  1. 前面所讲 to, into,sameAs 实际上就是函数调用,如果把 infix 关键字去掉,那么也就纯粹按照函数调用方式来。比如 1.to("A"), element.into(list) 等,只有加了中缀调用的关键字 infix 后,才可以使用简单的中缀调用例如 1 to “A”, element into list 等。
  2. 并不是所有的函数都能写成中缀调用,中缀调用首先必须满足一个条件就是函数的参数只有一个。然后再看这个函数的参与者是不是只有两个元素,这两个元素可以是两个数,可以是两个对象,可以是集合等。

5. 小结

到这里有关 Kotlin 中的函数基本介绍完毕,后面将会继续 Kotlin 中的扩展函数,这也是一个非常好用的语法特性。