一、函数的基本用法
函数这个词相信各位不会陌生,而Kotlin中也是使用了众多函数的,正所谓面向函数编程。
1.1 与Java声明方式的区别
从最常见的onCreate方法来看看Java和Kotlin的区别
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声明输入参数的格式为“变量类型 变量名称”,而Kotlin声明输入参数的格式为“变量名称:变量类型”。
(6)Kotlin引入空安全机制,如果某个变量允许为空,就需要在变量类型后面加个问号“?”。
1.2 输入参数的格式
Kotlin的函数写法与Java的传统写法区别很大,刚从Java开发Android转Kotlin开发Android的朋友会不适应,但是后面你就会明白Kotlin的优势了,这也是Google为什么大力推荐的原因,好了,话不多少,实践是检验真理的唯一标准。示例代码如下:
//没有输入参数,也没有输出参数 fun getEmpty(){ tv_title.text = "空空如也" tv_result.text = "" } //只有输入参数 fun getInput(egg:Int,leek:Double,water:String,shell:Float){ tv_title.text = "两个鸡蛋,一把韭菜,一些矿泉水" tv_result.text = "" } //输入参数存在空值 fun getCanNull(egg:Int,leek:Double,water:String?,shell:Float){ tv_result.text = if(water!= null) "两个鸡蛋,一把韭菜,一些矿泉水" else "没有水" tv_result.text = "" }
代码中有三个方法,第一个没有入参没有返回参数,第二个只有入参,第三个入参中存在空值,下面用三个按钮来调用一下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:textColor="#000" android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:textColor="#000" android:id="@+id/tv_result" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <LinearLayout android:gravity="center" android:layout_marginTop="20dp" android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:id="@+id/btn_one" android:text="One" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/btn_two" android:text="Two" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/btn_three" android:text="Three" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> </LinearLayout>
代码中使用
btn_one.setOnClickListener { getEmpty() } btn_two.setOnClickListener { getInput(2,11.11,"水",100f) } btn_three.setOnClickListener { getCanNull(2,11.11,null,100f) }
运行效果图如下:
没有输入参数
有输入参数
输入参数有空值的
1.3 输出参数的格式
输出参数由函数,函数在Kotlin中怎么定义的呢,这个跟Java就不太一样了,代码如下:
//声明变量 var Tests:Int //定义函数 fun Test():Int
Kotlin设计师的初衷就是把函数当成一个特殊的变量,既然函数被当作一种特殊的变量,同时每个变量都有变量类型,假如函数存在返回参数,那么自然把返回参数的类型作为该函数的变量类型,要是函数不存在返回参数,也就是Java中的返回void,Java中使用void表示不存在返回参数,而Kotlin的返回参数是一定存在的,即使开发者不声明任何返回参数,Kotlin函数也会默认返回一个Unit类型的对象,代码如下:
fun getEmpty():Unit{ tv_title.text = "空空如也" tv_result.text = "" }
这个Unit是代表无需返回具体的值,所以Kotlin代码中往往会省略掉,所以你写不写都可以(PS:那你说这么多废话干啥!!!),但增加Unit类型的目的是让函数定义完全符合变量定义的形式,若函数需要具体的输出对象,则一样要在函数末尾使用关键字“return”来返回参数值,代码如下:
//只有输出参数 fun getOutput():String{ tv_title.text = "空空如也" var result:String = "汪峰" return result }
接下来定义一个同时包含入参和出参的函数,代码如下:
//同时具备入参和出参的函数 fun getInputAndOutput(one:String,two:String,three:String):String{ tv_title.text = "人物包括 $one $two $three " var result:String = "三国" return result } btn_one.setOnClickListener { tv_result.text = getInputAndOutput("刘备","曹操","孙权") }
效果图如下:
二、输入参数的变化
2.1 默认参数
先来写一个函数
fun getFourBook( info: String, first: String, second: String, third: String, fourth: String ): String { var answer: String = "$info 、$first 、$second 、$third 、$fourth" return answer } var isOdd = true//如果从初始赋值中能够知道变量类型,就无须显示指定该变量的类型 btn_test.setOnClickListener { tv_result.text = if (isOdd) getFourBook( "四大名著是:","《三国演义》", "《水浒传》", "《红楼梦》", "《西游记》" ) else getFourBook("四书五经中的四书是:","《大学》", "《中庸》", "《论语》", "《孟子》") isOdd = !isOdd }
运行效果如下
奇次数点击如下
偶次数点击如下
我这一顿操作的意义何在呢?不是说默认参数吗?请听我慢慢道来,Kotlin中引入了默认参数的概念,允许在定义函数时直接指定输入参数的默认值。如果调用函数没有给出某参数的具体指,系统就自动对该参数赋予默认值,从而免去每次都要手动赋值的麻烦,那么怎么写默认参数呢?只需在声明输入参数时在其后面加上等号及其默认值即可,如下所示:
fun getFourBook( info: String = "四大名著是:", first: String = "《三国演义》", second: String = "《水浒传》", third: String = "《红楼梦》", fourth: String = "《西游记》" ): String { var answer: String = "$info 、$first 、$second 、$third 、$fourth" return answer } btn_test.setOnClickListener { tv_result.text = getFourBook("中国四大名著是:") }
运行效果如下:
就是这样的神奇,请注意在Java中是不能这么写的。
2.2 命名参数
如果觉得参数的默认值内容不够完整,想加入新的值,比如书的作者,可以这样写:
btn_test.setOnClickListener { tv_result.text = getFourBook("中国四大名著是:","罗贯中写的《三国演义》") }
这一步是没有问题了,加入我要改第四本书的值呢?n难道要把前三本书的值也都写上去吗?那也太鸡肋了吧,出于这个考虑🤔,Kotlin又引进了命名函数的概念,说的是调用函数时可以指定某个参数的名称及其数值,格式如“参数名=参数值”,演示代码如下:
这个地方为什么用截图而不是直接贴代码呢,因为我觉得这个引用方式比较酷,还用蓝色标出来了。我们运行一下看看结果吧,不然就是银样镴枪头,中看不中用。
运行效果如下:
很明显,Kotlin并非浪得虚名,献丑了~
2.3 可变参数
上面的参数都是固定的个数,现在说到可变参数就是随时添加,在Java中,如果不确定参数个数的话通常用“Object…args”的形式,那么Kotlin中呢?当然是新增关键字了,就是vararg,(PS:你看像不像吧var 和 arg 拼起来的单词),表示其后的参数个数是不确定的,而Kotlin会把可变参数当成是一个数组,开发者需要循环取出每个参数值进行处理,代码如下:
这个地方不贴代码,因为我希望有人能敲一边,而不是一味的复制和粘贴,那样是没有用的。
偶次数点击效果图如下
刚才我们用的时候字符串,接下来用数组来试一下
var isOdd = true fun getFourBook( info: String = "四书指的是:", first: String = "《大学》", second: String = "《中庸》", third: String = "《论语》", fourth: String = "《孟子》", vararg otherArray:Array<String> ): String { var answer: String = "$info 、$first 、$second 、$third 、$fourth" //先遍历每个数组 for(array in otherArray){ //再遍历某个数组中的所有元素 for(item in array){ answer = "$answer, $item" } } return answer } btn_test.setOnClickListener { tv_result.text = if(isOdd) getFourBook("四书五经中的四书指的是:") else getFourBook("四书五经六艺全指什么","《大学》","《中庸》","《论语》", "《孟子》", arrayOf("《诗经》","《尚书》","《礼记》","《周易》","《春秋》"), arrayOf("《易》","《书》","《诗》","《礼》","《乐》","《春秋》")) isOdd = !isOdd }
运行效果如下:
三、特殊函数
3.1 泛型函数
我们先声明几个泛型
var int_array:Array<Int> = arrayOf<Int>(1,2,3) var long_array:Array<Long> = arrayOf<Long>(1,2,3) var float_array:Array<Float> = arrayOf<Float>(1.0f,2.0f,3.0f)
看起来是不是很眼熟呢?注意到尖括号内部制定了数组元素的类型,这正是泛型的写法“<>”。由“Array<变量类型>”声明而来的变量可称作泛型变量,至于等号后面的arrayOf*便是泛型函数。定义泛型函数时,需要在函数名称前面添加“”,表示以T声明的参数(包括输入参数和输出参数),其参数类型必须在函数调用时指定,代码示例如下:
//Kotlin允许定义全局函数,即函数可在单独的kt文件中定义,然后其他地方也能直接调用 fun <T> appendString(tag:String,vararg otherInfo:T?):String{ var str:String = "$tag:" //遍历可变参数中的泛型变量,将其转换为字符串再拼接到一起 for(item in otherInfo){ str = "$str${item.toString()}," } return str } var count = 0 btn_test.setOnClickListener { tv_result.text = when(count%3){ 0 -> appendString<String>("四大名著","《三国演义》", "《水浒传》","《红楼梦》","《西游记》") 1 -> appendString<Int>("小于10的素数",2,3,5,7) else -> appendString<Double>("键盘的价格",39.99,128.00,246.57) } count++ }
运行效果如下:
字符串:
整数:
双精度数
3.2 内联函数
什么是内联函数呢?举个例子,Int、Float和Double都继承自Number类,但是假如定义一个输入参数形式为setArrayNumber(array:Array< Number >)的函数,它并不接受Array< Int >或者Array< Double >的入参。如果要让该方法同时接收整型和双精度的数组入参,就得指定泛型变量T来自于基类Number,即将“< T >”改为“< reified T : Number>”,同时在fun前面添加关键字inline,表示该函数属于内联函数,内联函数在编译的时候回在调用处把该函数的内部代码直接复制一份,调用多少次复制多少份,而非普通函数那样仅仅提供一个函数的访问地址。
//普通函数 fun setArrayNumber(array: Array<Number>) { var str: String = "数组元素一次排列" for(item in array){ str = str + item.toString() + ", " } tv_result.text = str } //只有内联函数才能被具体化 请注意,这个inline是全局的,所以不能写在函数方法里面,要写在外面 inline fun <reified T:Number> setArrayStr(array: Array<T>){ var str:String ="数组元素依次排列" for (item in array){ str = str + item.toString() + ", " } tv_result.text = str }
上面的泛型函数兼内联函数setArrayStr在定义的时候比较麻烦,不过外部的调用方式没有发生改变,调用代码如下
var int_array:Array<Int> = arrayOf<Int>(1,2,3) var float_array:Array<Float> = arrayOf<Float>(1.0f,2.0f,3.0f) var double_array:Array<Double> = arrayOf<Double>(3.14,2.54,3.45) //Kotlin进行循环调用时,要求参数类型完全匹配,所以即使Int继承自Number类,也不能调用setArrayNumber方法传送Int类型 var count = 0 btn_test.setOnClickListener { when(count%3){ 0 -> setArrayStr<Int>(int_array) 1 -> setArrayStr<Float>(float_array) else -> setArrayStr<Double>(double_array) } count++ }
运行效果如下:
整数
浮点
双精度
3.3 简化函数
简化函数可以用一个数学题来演示,比如 5!=54321,用Kotlin代码来看
fun test(n: Int): Int { if (n <= 1) n else n * test(n - 1) return 0 }
然后可以通过三元表达式进一步简化
fun test(n: Int): Int = if (n <= 1) n else n * test(n - 1)
一行代码解决问题
3.4 尾递归函数
尾递归函数是什么意思呢,它指的是函数末尾的返回值重复调用了自身函数。此时要在fun前面加上关键字tailrec,它告诉编译器这是一个尾递归函数,则编译器会相应进行优化,从而提高程序性能。
比如余弦不动点,即可通过尾递归函数来实现,下面是代码示例
四、增强系统函数
4.1扩展函数
使用Java开发时,虽然系统自带的类已经提供了许多方法,然而经常还是无法完全满足业务需求,此时开发者往往要写一个工具类来补充相关的处理功能,长此以往,工具类越来越多,也越来越管理,针对于这个情况,Kotlin推出了扩展函数得概念,扩展函数允许开发者给系统类补写新的方法,而无须另外编写额外的工具类,比如系统自带的数组Array提供了求最大值的max方法,也提供了进行排序的sort方法,可以并未提供交换数组元素的方法,我们可以试着给Array数组来添加新的交换方法,也就是一个扩展函数。
如下所示:
fun Array<Int>.swap(pos1:Int,pos2:Int){ val tmp = this[pos1] //this表示数组自身 this[pos1] = this[pos2] this[pos2] = tmp }
不过这函数的缺点也很明显,就是它声明了扩展自Array< Int >,就不能用于浮点数组和双精度数组及其他的数组,所以,为了增强交换函数的通用性,必须把swap改写为泛型函数,即用T代替Int,改动代码如下:
fun <T> Array<T>.swap(pos1:Int,pos2:Int){ val tmp = this[pos1] //this表示数组自身 this[pos1] = this[pos2] this[pos2] = tmp }
扩展函数已经写好了,接下来使用一下吧
val array:Array<Double> = arrayOf(1.0,2.0,3.0,4.0) btn_test.setOnClickListener { //下标为0和3的两个数组元素进行交换 //array可以是整型数组,也可以是双精度数组 array.swap(0,3) var str:String = "" for (item in array){ str = str + item.toString() + "," } tv_result.text = "数组元素排列结果为:$str" }
运行效果如下图
可以看到已经换了位置了,这就是扩展函数。
4.2 日期时间函数
在日常的开发中,除了数组之外,日期和时间的相关操作也是很常见的,比如获取当前日期、获取当前时间、获取指定格式的日期时间等,基本上每一个采取Java编码的Android工程都需要一个DateUtil.java的工具类,工具类代码如下:
public class DateUtil { //获取当前完整的日期和时间 public static String getNowDateTime(){ SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); return sdf.format(new Date()); } //获取当前日期 public static String getNowDate(){ SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd"); return sdf.format(new Date()); } //获取当前日期 public static String getNowTime(){ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); return sdf.format(new Date()); } //获取当前日期(精确到毫秒) public static String getNowTimeDetail(){ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); return sdf.format(new Date()); } //将时间戳转化为对应的时间(10位或者13位都可以) public static String formatTime(long time){ String times = null; if(String.valueOf(time).length()>10){// 10位的秒级别的时间戳 times = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time * 1000)); }else {// 13位的秒级别的时间戳 times = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time); } return times; } //将时间字符串转为时间戳字符串 public static String getStringTimestamp(String time) { String timestamp = null; try { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Long longTime = sdf.parse(time).getTime()/1000; timestamp = Long.toString(longTime); } catch (ParseException e) { e.printStackTrace(); } return timestamp; }
日期时间格式的定义说明
时间格式内部其余的横线“-”、空格“ ”、冒号“:”、点号“.”等字符仅仅是连接符,方便观看各种单位的时间数字而已,国内,也可以使用形如“yyyy年dd日HHmm分ss秒”的时间格式
现在利用Kotlin的扩展函数就无须书写专门的DateUtil工具类,定义函数如下:
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.getNowDateTimeDetail():String{ val sdf = SimpleDateFormat("yyyy-MM-dd 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) }
然后我们再调用这些函数,代码如下:
var count:Int = 0 btn_test.setOnClickListener { tv_result.text = "扩展函数:" + when(count%5){ 0 -> "当前日期时间为${Date().getNowDateTime()}" 1 -> "当前日期为${Date().getNowDate()}" 2 -> "当前时间为${Date().getNowTime()}" 3 -> "当前毫秒时间为${Date().getNowDateTimeDetail()}" else -> "当前中文日期时间为${Date().getFormatTime("yyyy年MM月dd日 HH时mm分ss秒")}" } count++ }
运行效果图如下
日期时间
日期
时间
毫秒
中文格式时间
4.3 单例对象
单例对象实际上进一步简化了扩展函数得使用过程,比如我们之前定义的扩展时间函数,我们用单例写一下,关键字是object,代码如下
object DateUtil { var nowDateTime: String = "" get() { var sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") return sdf.format(Date()) } var nowDate: String = "" get() { var sdf = SimpleDateFormat("yyyy-MM-dd") return sdf.format(Date()) } var nowTime: String = "" get() { var sdf = SimpleDateFormat("HH:mm:ss") return sdf.format(Date()) } var nowTimeDetail: String = "" get() { var sdf = SimpleDateFormat("yyyy-MM-dd 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()) } }
然后是调用代码:
var count: Int = 0 btn_test.setOnClickListener { tv_result.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秒")}" } count++ }
运行效果,和之前的一样的,只是调用方式发生了改变。