4.1 函数的基本作用
4.1.1 与Java声明函数方式的区别
那么Kotlin对函数的使用跟Java相比,有哪些区别呢?先从最常见的onCreate方法入手,看看二者都有哪些区别,下 面是Java编写的onCreate函数代码:
//java
override
public void onCreate(Bundle savedInstanceState){
...
}
//Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
....
}
两相对比,可以看到二者主要有以下几点区别:
(1) Java使 用“@Override"表示该函数重载父类的方法,而Kotlin使 用小写的“override"在同一行表达重载操作。
(2) Java使 用“public"表示该函数是公共方法,而Kotlin默认 函数就是公开的,所以省略了关键字“public"。
(3)Java使用“void"表示该函数没有返回参数,而Kotlin不存在关键字“void",若无返回参数,则不用特别说明。
(4) Kotlin新增了关键字“fun”, 表示这里是函数定义,其格式类似于Java的关键字“class”,而Java不存 在关键字“fun”。
(5) Java声明输入参数的格式为“变量类型变量名称”,而Kotin声 明输入参数的格式为“变量名称:变量类型”。
(6) Kotlin引入 了空安全机制,如果某个变量允许为空,就需要在变量类型后面加个问号“?”。
4.1.2输入参数的格式
//& 没有输入参数,也没有输出参数
fun getDinnerEmpty() {
tv_process.text = "只有空盘子哦"
tv_result.text = ""
}
//只有输入参数
fun getDinnerInput(egg: Int, leek: Double, water: String, shell: Float) {
tv_process.text = "食材包括:两个鸡蛋,一把韭菜,几瓢水"
tv_result.text = ""
}
//输入参数存在空值
fun getDinnerCanNull(egg: Int, leek: Double, water: String?, shell: Float) {
tv_process.text = if (water != null) "食材包括:两个鸡蛋,一把韭菜,几瓢水" else "没有水就没法做汤了"
tv_result.text = ""
}
注意:函数的调用跟在Java中一样
4.1.3 输出参数的格式
Java代码中,函数的返回参数类型在函数名称前面指定,形如“publicintmain..)”,但在Kotlin中,返回参数类型却在右括号后面指定,形如“fun main.(..):Int。对于习惯了Java 的开发者而言,Kotlin的这 种写法着实别扭,为了方便记忆,我们姑且把函数当作一种特殊的变量,那么定义函数就跟定义变量是同一种写法。
/**
* 跟变量的声明格式一样:
* var i:Int
*
* 函数的声明
* fun main():Int
*/
既然函数被当作-一种特殊的变量,同时每个变量都有变量类型,假如函数存在返回参数,那么自然把返回参数的类型作为该函数的变量类型;要是函数不存在返回参数,也就是Java中的返回void,那该怎么办?这里得澄清一-下,Java使用void表示不存在返回参数,然而Kotlin的返回参数是一定存在的,即使开发者不声明任何返回参数,Kotlin函数也会默认返回一个Unit类型的对象。
//无任何返回参数-->关键字Unit
//Unit类型表示没有返回参数,也可直接省略Unit声明
fun getDinnerUnit(): Unit {
tv_process.text = "只有空盘子哦"
tv_result.text = ""
}
有返回值的函数:
//有输出参数-->return关键字
fun getDinnerOutput(): String {
tv_process.text = "只有空盘子哦"
var dinner: String = "巧妇难为无米之炊"
return dinner
}
4.2 输入参数的变化
4.2.1 默认参数
允许在定义函数的时直接指定输入参数的默认值。如果调用函数时没有给出某参数的具体指,系统就自动对改参数赋予默认值。
格式:在声明输入参数后面加上等号及其默认值
//带默认参数的函数定义
fun getFourBigDefault(
general: String,
first: String = "造纸术",
second: String = "印刷术",
third: String = "火药",
fourth: String = "指南针"
): String {
var answer: String = "$general:$first,$second,$third,$fourth"
return answer
}
//调用
btn_default_fun.setOnClickListener {
btn_default_fun.text = getFourBigDefault("中国的伟大发明\n")
}
4.2.2 命名参数
如果不满意参数的默认值,也可在调用函数时输入新的值,例如四大发明的默认值不包含它们的发明者,现在想增加显示造纸术的发明者蔡伦,则调用getFourBigDefault函数时,注意给第二个参数填写指定的描述文字,代码示例如下:
//给第二个参数填写指定的描述文字
btn_default_fun2.setOnClickListener {
btn_default_fun.text = getFourBigDefault("中国的伟大发明\n", "蔡伦发明的造纸术")
}
有时想要变更的参数并非第一一个默认参数,比如第二个默认参数的“印刷术”,为了解决这个不合理的地方,Kotlin又引进了命名参数的概念,说的是调用函数时可以指定某个参数的名称及其数值,具体格式形如“参数名=参数值这样。就前述的给“印刷术”改名而言,具体到Kotlin编码上面,可参见以下的示例代码: .
//有时候想要变更的并非第一个默认参数,而是指定参数,Kotlin引进了命名参数的概念
//格式:“参数名=参数值”
btn_default_fun3.setOnClickListener {
btn_default_fun.text = getFourBigDefault("中国的伟大发明\n", second = "活字印刷术")
}
4.2.3 可变参数
这种随时添加的概念对应于函数定义里面的可变参数,在Java体系中,可变参数采用“0bje... args' '的形式;在Kotlin体系中, 新增了关键字vararg,表示其后的参数个数是不确定的。以可变的字符串参数为例,Java的 写法为“Strig... args",而Kotlin的写法为“vararg args: String?"。函数内部在解析的时候,Kotlin会 把可变参数当作-一个数组,开发者需要循环取出每个参数值进行处理,对应的Kotlin演示代码如下所示:
/**
* 可变参数--参数的个数不确定--使用关键字vararg
* Java中可变的字符串参数为例-->“String ...args”
* Kotlin可变参数的写法为-->vararg args:String?
* 函数内部解析的时候,Kotlin会把可变参数当做一个数组,开发者需要循环取出每个参数值进行处理
*/
var isOdd: Boolean = false
fun getFourBigVararg(
general: String,
first: String = "造纸术",
second: String = "印刷术",
third: String = "火药",
fourth: String = "指南针",
vararg otherArray: String?
): String {
var answer: String = "$general:$first,$second,$third,$fourth"
//循环取出可变参数包含的所有字段
for (item in otherArray) {
answer = "$answer,$item"
}
return answer
}
btn_default_fun4.setOnClickListener {
btn_default_fun4.text = if (isOdd) getFourBigVararg("古代的四大发明")
else getFourBigVararg("古代的七大发明", "造纸术", "印刷术", "火药", "指南针", "丝绸", "瓷器", "茶叶")
isOdd = !isOdd
}
可变参数是数组的情况下-->因为可变参数相当于数组,所以先遍历可变参数,在遍历某个数组中的所有元素
如此一来,可变参数就成了可变的数组参数,同样声明数组参数时也要加上vararg前缀,告诉编译器后面的数组个数是变化的。对应的函数声明代码修改如下:
fun getFourBigVarargArray(
general: String,
first: String = "造纸术",
second: String = "印刷术",
third: String = "火药",
fourth: String = "指南针",
vararg otherArray: Array<String>
): String {
var answer: String = "$general:$first,$second,$third,$fourth"
//先遍历每个数组
for (array in otherArray) {
//在遍历某个数组中的所有元素
for (item in array)
answer = "$answer,$item"
}
return answer
}
btn_default_fun5.setOnClickListener {
btn_default_fun5.text = if (isOdd) getFourBigVarargArray("古代的四大发明") else
getFourBigVarargArray(
"古代的N大发明", "造纸术", "印刷术", "火药", "指南针", arrayOf("丝绸", "瓷器", "茶叶")
, arrayOf("国画", "中医", "武术")
)
isOdd = !isOdd
}
总结一下,Kotlin引入了默认参数的概念,并加以扩展允许通过命名参数指定修改某个参数值,而Java是 不存在默认参数概念的。另外,Kotlin对Java的可变 参数功能进行了增强,不但支持普通类型的可变参数,而且支持数组类型的可变参数。
4.3 几种特殊的函数
4.3.1泛型函数
定义泛型函数的时候,的再函数名称前面添加"<T>",表示以T声明的参数(包括输入参数和输出参数),其参数类型必须在函数调用时指定。
/**
* 定义泛型函数的时候,的再函数名称前面添加"<T>",表示以T声明的参数(包括输入参数和输出参数),
* 其参数类型必须在函数调用时指定。
*/
//Kotlin允许定义全局函数,即函数可在单独的kt文件中定义,然后其他地方也能直接调用
var count: Int = 0
fun <T> appendString(tag: String, vararg otherInfo: T?): String {
var str: String = "$tag:"
//遍历可变参数中的泛型变量,将其转换为字符串再拼接到一起
for (item in otherInfo) {
str = "$str${item.toString()}"
}
return str
}
btn_default_fun6.setOnClickListener {
btn_default_fun6.text = when (count % 3) {
0 -> appendString<String>("古代四大发明", "造纸术", "印刷术", "火药", "指南针")
1 -> appendString<Int>("小于10的素数", 2, 3, 5, 7)
else -> appendString<Double>("烧钱的日子", 5.20, 6.18, 11.11, 12.12)
}
count++
}
4.3.2 内联函数
注意到前面定义泛型函数appendString时,是把它作为一个全局函数,也就是在类外面定义,而不在类内部定义。因为类的成员函数依赖于类,只有泛型类(又称模板类)才能拥有成员泛型函数,而普通类是不允许定义泛型函数的,否则编译器会直接报错。不过有个例外情况,如果参数类型都是继承自某种类型,那么允许在定义函数时指定从这个基类泛化开,凡是继承自该基类的子类,都可以作为输入参数进行函数调用,反之则无法调用函数。
举个例子,Int、 Float和Double 都继承自Number类,但是假如定义一个输入参数形式为setArrayNumber(array:Array<Number>)的函数,它并不接受Array <Int>或者Array<Double>的入参。如果要让该方法同时接收整型和双精度的数组入参,就得指定泛型变量T来自于基类Number,即将“<T>”改为“<reified T:Number>”,同时在fun前面添加关键字inline,表示该函数属于内联函数。
/**
* 内联函数 :规定泛型的继承自某个类型,可单独定义在类的外部
*/
inline fun <reified T : Number> setArrayStr(array: Array<T>): String {
var str: String = "数组元素依次排列:"
for (item in array) {
str = "$str ${item.toString()},"
}
return str
}
//调用
var int_array: Array<Int> = arrayOf(1, 2, 3)
var foult_array: Array<Float> = arrayOf(1.0f, 2.0f, 3.0f)
var double_array: Array<Double> = arrayOf(11.11, 22.22, 33.33)
//只有内联函数才可以被具体化
btn_default_fun7.setOnClickListener {
when (count % 3) {
0 -> btn_default_fun7.text = setArrayStr(int_array)
1 -> btn_default_fun7.text = setArrayStr(foult_array)
else -> btn_default_fun7.text = setArrayStr(double_array)
}
count++
}
4.3.3 简化函数
在“4.1.3输出参数的格式”中提到了可将函数当作一种特殊的变量,既然变量通过等号赋值,那么函数也允许使用等号对输出参数赋值。具体地说,如果一个函数的表达式比较简单,一两行代码就可以搞定,那么Kotin允许使用等号代替大括号。
//列如:求N!
fun factorial(n: Int): Int {
if (n <= 1) return n
else return n * factorial(n - 1)
}
//简化
fun factorialSimple(n: Int): Int = if (n <= 1) n else n * factorial(n - 1)
4.3.4 尾递归函数
4.3.3小节的阶乘函数只是一个普通的递归函数,Kotlin体系还存在一种特殊的递归函数,名叫尾递归函数,它指的是函数末尾的返回值重复调用了自身函数。此时要在fun前面加上关键字tailrec,告诉编译器这是一个尾递归函数,则编译器会相应进行优化,从而提高程序性能。
/**
* 指的时函数末尾的返回值重复调用了自身函数。此时要在fun前面加上关键字tailrec
* 告诉编译器这是一个尾递归函数,则编译器会相应进行优化,从而提高性能
*/
tailrec fun findFixPoint(x: Double = 1.0): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))
4.3.5 高阶函数
由于函数可以当做特殊变量,如果把A函数作为B函数的输入参数,此时,因为B函数的输入参数内嵌了A函数,故而B函数被称作为高阶函数,对应的A函数则为高阶函数的函数参数,又称函数变量。
为了解释地更加清楚些,接下来看一个例子。对于一个数组变量,若想求得该数组元素的最大值,则可以调用该数组的max方法。现在有一个字符串数组,类型为Array<String>,倘若调用该数组的max方法,返回的并非最长的字符串,而是按首字母排序在字母表最靠后的那个字符串。比如有个字符串数组为arrayOf("How" , "do", "you" ,"do", "I'm ", "Fine'"),调用max方法获得的字符串为“you”,而不是长度最长的那个字符串“I'm”。
当然,也可以写一个单独的函数专门判断字符串长度,然而要是哪天需要其他比较大小的算法,难道又得再写一个全新的比较函数?显然这么做的代价不菲,所以Kotlin引入了高阶函数这个秘密武器,直接把这个比较算法作为参数传进来,由开发者在调用高阶函数时再指定具体的算法函数。就获取字符串数组内部的最大值而言,实现该功能框架的高阶函数代码如下所示:
//4.3.5 高阶函数
/**
* 由于函数可以当做特殊变量,如果把A函数作为B函数的输入参数,
* 此时,因为B函数的输入参数内嵌了A函数,故而B函数被称作为高阶函数
*/
//maxCustom()是高阶函数,这里的greater函数就像是个变量
//greater函数有两个输入参数,返回布尔类型的输出参数
//如果第一个参数大于第二个参数,就认为greater返回true,否则返回false
fun <T> maxCustom(array: Array<T>, greater: (T, T) -> Boolean): T? {
var max: T? = null
for (item in array) {
if (max == null || greater(item, max))
max = item
}
return max
}
//调用
var string_array: Array<String> = arrayOf("Hao", "do", "you", "do", "I'm", "fine")
btn_default_fun8.setOnClickListener {
btn_default_fun8.text = when (count % 4) {
//string_array.max()返回的时you
0 -> "字符串数组的默认最大值为${string_array.max()}"
//string_array.max()对应的高阶函数是maxCustom同时也是泛型函数,所以要在函数名称后面加上<String>
1 -> "字符串数组按长度比较的最大值为${maxCustom<String>(string_array, { a, b -> a.length > b.length })}"
//因为系统可以根据string_array判断泛型函数采用了String类型,故而函数名称后面的<String>也可以省略掉
2 -> "字符串数组的默认最大值(使用高阶函数)为${maxCustom(string_array, { a, b -> a > b })}"
else -> "字符串数组按去掉空格在比较长度的最大值为${maxCustom(string_array, { a, b -> a.trim().length > b.trim().length })}"
}
count++
}
image.png
输出结果:
image.png
image.png
image.png
image.png
4.4增强系统函数
4.4.1 扩展函数
Kotlin推出了扩展函数的概念,扩展函数允许开发者给系统类补写新的方法,而无须另外编写额外的工具类。比如系统自带的数组Array提供了求最大值的max方法,也提供了进行排序的sort方法,可是并未提供交换数组元素的方法。于是我们打算给Array数组类增加新的交换方法,也就是添加一个扩展函数swap。与普通函数定义不同的是,要在swap函数名称前面加上前缀“Array<Int>.",表示该函数扩展自系统类Array<Int>。下 面是用于交换数组元素的swap函数定义代码: .
/**
* 概念:系统自带的类提供的方法无法满足日常开发需求,于是乎开发者往往编写了很多工具类,由于工具类繁多,难以管理。
* Kotlin推出了扩展函数的概念,扩展函数允许开发者给系统类补写新的方法,而无需编写额外的工具类。
*/
//例如:给数组Array新增交换的方法。
fun <T> Array<T>.swap(pos1: Int, pos2: Int) {
//this表示数组变量自身
val temp = this[pos1]
this[pos1] = this[pos2]
this[pos2] = temp
}
val array: Array<Double> = arrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
btn_default_fun9.setOnClickListener {
//下标为0和3的两个数组元素进行交换
array.swap(0, 3)
btn_default_fun9.text = setArrayStr<Double>(array)
}
image.png
4.4.2扩展高阶函数
“4.3.5高阶函数”小节中提到的maxCustom同时结合了高阶函数和泛型函数的写法,其实还可以给它加上扩展函数的功能。由于该函数的目的是求数组元素的最大值,因此不妨将该函数扩展到Array<T>中去,扩展后的高阶函数代码示例如下:
/**
* 扩展高阶函数
* 扩展:扩展类的方法
* 高阶:将函数作为变量传递
*/
fun <T> Array<T>.maxCustomize(greater: (T, T) -> Boolean): T? {
var max: T? = null
for (item in this) {
if (max == null || greater(item, max)) {
max = item
}
}
return max
}
//扩展高阶函数
//扩展函数+高阶函数
btn_default_fun10.setOnClickListener {
btn_default_fun10.text = when (count % 4) {
0 -> "字符串数组的默认最大值为${string_array.max()}"
1 -> "字符串数组按长度比较的最大值为${string_array.maxCustomize<String>({ a, b -> a.length > b.length })}"
2 -> "字符串数组的默认最大值(使用高阶函数)为${string_array.maxCustomize({ a, b -> a > b })}"
else -> "字符串数组按去掉空格在比较长度的最大值为${string_array.maxCustomize({ a, b -> a.trim().length > b.trim().length })}"
}
count++
}
4.4.3 日期时间函数--扩展日期函数
现在利用Kotlin的扩展函数就无须书写专门的DateUtil工具类,直接写几个系统日期类Date的扩展函数即可实现日期时间格式转换的功能。改写后的Date类扩展函数举例如下:
//方法名称前面的Date.表示该方法扩展自Date类
//返回的日期时间格式形如2019-08-19 10:00:00
fun Date.getNowDateTime(): String {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
return sdf.format(this)
}
//
//只返回日期字符串
fun Date.getNowDate(): String {
val sdf = SimpleDateFormat("yyyy-MM-dd")
return sdf.format(this)
}
//只返回时间字符串
fun Date.getNowTime(): String {
val sdf = SimpleDateFormat("HH:mm:ss")
return sdf.format(this)
}
//返回详细的时间字符串,精确到毫秒
fun Date.getNowTimeDetail(): String {
val sdf = SimpleDateFormat("HH:mm:ss.SSS")
return sdf.format(this)
}
//返回开发者指定格式的日期时间字符串
fun Date.getFormatTime(format: String = ""): String {
var ft: String = format
val sdf = if (!ft.isEmpty()) SimpleDateFormat(ft)
else SimpleDateFormat("yyyyMMddHHmmss")
return sdf.format(this)
}
btn_default_fun11.setOnClickListener {
btn_default_fun11.text = when (count++ % 5) {
0 -> "当前日期时间为${Date().getNowDateTime()}"
1 -> "当前日期为${Date().getNowDate()}"
2 -> "当前时间为${Date().getNowTime()}"
3 -> "当前毫秒时间为${Date().getNowTimeDetail()}"
else -> "当前中文日期时间为${Date().getFormatTime("yyyy年MM月dd日HH时mm分ss秒")}"
}
}
4.4单利对象
虽然扩展函数已经实现日期信息的获取,但是它的调用方式稍显烦琐,况且这些函数必须从某个已存在的类扩展而来,倘若没有可依赖的具体类,也就无法书写扩展函数。所以,Java编码常见的***Util工具类,某种程度上反而更灵活、适应面更广,那么Kotlin有没有专门]的工具类写法呢?
鉴于此,Kotlin将 工具类的用法提炼了出来,既然这个东西仅仅是作为工
具,那么一旦制定了规格就不会再改变了,不能构造也不能修改。故而Kotin使用对象关键字object加以修饰,并称之为“单例对象”,其实就相当于Java的工具类。
下面是采取单例对象改写后的日期时间工具代码:
import java.text.SimpleDateFormat
import java.util.*
object DateUtil {
////返回的日期时间格式形如2019-08-19 10:00:00
val nowDateTime: String
get() {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
return sdf.format(Date())
}
//只返回日期字符串
val nowDate: String
get() {
val sdf = SimpleDateFormat("yyyy-MM-dd")
return sdf.format(Date())
}
//只返回时间字符串
val nowTime: String
get() {
val sdf = SimpleDateFormat("HH:mm:ss")
return sdf.format(Date())
}
//返回详细的时间字符串,精确到毫秒
val nowTimeDetail: String
get() {
val sdf = SimpleDateFormat("HH:mm:ss.SSS")
return sdf.format(Date())
}
//返回开发者指定格式的日期时间字符串
fun getFormatTime(format: String = ""): String {
var ft: String = format
val sdf = if (!ft.isEmpty()) SimpleDateFormat(ft)
else SimpleDateFormat("yyyyMMddHHmmss")
return sdf.format(Date())
}
}
外部若要访问单例对象的变量值,直接调用“对象名称.变量名称”即可
/**
* 相当于Java的工具类写法
* & 使用到了object关键字
* & 单利对象包含两部分:1、字符串的声明变量 2、获取变量值的get方法
*/
btn_default_fun12.setOnClickListener {
btn_default_fun12.text = when (count++ % 5) {
0 -> "当前日期时间为${DateUtil.nowDateTime}"
1 -> "当前日期为${DateUtil.nowDate}"
2 -> "当前时间为${DateUtil.nowTime}"
3 -> "当前毫秒时间为${DateUtil.nowTimeDetail}"
else -> "当前中文日期时间为${DateUtil.getFormatTime("yyyy年MM月dd日HH时mm分ss秒")}"
}
}
}
网友评论