写在前面:现在工作越来越不好找,搞不好突然就会离开开发。为了在离开时候,如果想要写kotlin还能想起来怎么写,所以写了一篇教程给自己。
先来一个kotlin中文站,可以用来看看kotlin新版本出了什么新特性。还可以看看b站的benny老师,他在国内的kotlin推广做了很多事。
val a = 1
var b = ""
// 下面这句代码会编译错误
// var b = 1
val a: Float = 1f
kotlin会自动推断变量的类型,一个变量在声明后,编译器就已经确定了该变量的类型,后续是无法更改变量的类型的。可以在变量名臣后面使用":变量类型"来指定变量类型,就像上面的Float一样。
使用val声明的变量不可以被重新赋值,使用var声明的变量可以被重新赋值。
java的byte、short、int、long、char、boolean、float、double在kotlin里面,是以大写字母开头体现的,并且在kotlin里面,没有Integer、Character类型。
在kotlin里面,一个String如果想要转换成Int,可以使用toInt方法,toInt本质也是调用Integer.parseInt方法。顺便一提,在kotlin里面,也可以调用parseInt方法,不过需要这样调用,java.lang.Integer.parseInt。
提到toInt,就顺便提一个kotlin在声明Int时特有的问题。如果Int的最高位是1时,Int不会将这个值视为负数,而是当做Long。此时,就需要使用toInt转换成Int。
val white:Int = 0xffffffff.toInt()
long类型和java一的点不同,在java里面,long类型可以使用大小写L作为结尾,但小写l总会和数字1看错,所以kotlin不允许用小写l,只能用大写L。
除了基本数据有一点不同之外,大部分类型都和java一样,因为kotlin可以直接使用java的api,但有一些还是有点不同。
0x、0b:kotlin同样支持在声明数字时,用0x表示16进制,用0b表示二进制,还能用_隔开数字而不影响数值。
数组:提到List,就顺便将数组说一下。在kotlin里面,如果想要使用数组,可以使用IntArray或Array。八大基本数据类型都有自己的Array,其他类型需要使用Array。Array在new时,还是比较麻烦的,以IntArray为例:
IntArray(1){ it ->
2
}
这里的1就是Array的长度,2就是要返回的值,这里的意思就是返回一个长度为1,第0个元素为2的数组。
it就是index,这里的it->可以去掉,{}默认的名称就是it,可以修改为其他名称,比如index,这个下面会具体说明,现在只要知道能这样用即可,其他类型的数组使用的方式也是一样。如果要问,IntArray和Array有什么区别,我记得看过相关博客,说是IntArray的性能更好,所以如果使用基本类型的数组,就使用相应的Array,其他情况再用Array。
从上面的使用方式也可以看到,数组使用的方式特别麻烦,如果length很长,那就是需要判断所有index,这显然是不合理的,所以kotlin也提供了arrayOf这样的api。有intArrayOf和arrayOf等方式。
intArrayOf(1, 2, 3)
arrayOf(1, 2, 3)
从上面可以看到初始化一个对象没有使用new关键字,在kotlin里面,没有new关键字,所以想要创建对象时不需要使用new。再补充一下,如果一行只写一句代码,不需要写";“,只有一行写多句代码和在声明枚举类时,才需要用”;"。
val a = 1;val b = 1
enum class Word{
A,
B;
fun test(){
}
}
如果没有在B后面补一个";",就没办法编写其他代码。
类型转换,在java里面,想要判断变量是否为某种类型可以使用instanceof关键字判断,而在kotlin,需要使用“is”进行判断。而kotlin和java有点不同,在java里面,即使结果为true,还是需要手动转型一次,但在kotlin里面就不需要手动转型。
val a: Any = ""
if(a is String){
a.substring(0)
}
只要为true,就可以直接使用String的api。
kotlin还有强转的关键字:as。这个as和java的强转还有点不一样。
val a: Any = ""
val b: Int? = a as? Int
as可以在后面带一个"?",这样做的意思是,如果是该类型,就转型,如果不是就返回空。
借助这个特性,可以用来判断强转是否成功,比如:
val a: Any = ""
(a as? Int)?.plus(2)
如果转型成功,就+2,否则就什么都不做。
数字的位操作:在java中,有左移、右移和无符号右移,在kotlin中,不能使用"<<“、”>>“、”>>>"来操作位移,只能使用“shl”来实现左移、"shr"来实现右移,“ushr"实现无符号右移。还有"and"作为”&"的代替,“or"作为”|"的代替。
判断字符串是否相等:在java里面,判断字符串是否相等是比较麻烦的,不但要调用equals方法,还要考虑控安全。到了kotlin,就不用想那么多了,直接使用"=="就可以了。
在idea里面,可以双击shift搜索show kotlin bytecode,就可以发现,之所以使用==就可以避免很多问题,是因为kotlin做了多空的判断。
public static boolean areEqual(Object first, Object second) {
return first == null ? second == null : first.equals(second);
}
但这样做其实就是占用了"==“操作符,如果想要判断两个对象的内存地址是否相等,需要使用三等号”==="。
字符串拼接:在java里面,字符串拼接是很麻烦的,kotlin可以使用$在"“里面拼接字符串。还能使用”{}“写大量代码,并在最后一行返回字符串。如果想要”$“符号,需要使用”\"转义。
val a = 1
"a$a"
val a = 2
"a${
if(a == 1){
"b"
}else "c"
}"
除此之外,kotlin还能使用三引号。用三引号的字符串,可以保留字符串原本字符串的样式。
println("""
123
456
""".trimIndent())
// 结果
123
456
如果把trimIndent去掉,123和456前后的空行和空格就还会保留。
空安全:kotlin还有一个空安全这个特性,有了空安全,写代码方便了不少。空安全有几种用法
// 基础用法,调用字段/方法前使用问号,只有不为空才会继续执行
val str: String? = "abc"
str?.length
// 如果可以保证不为空,就可以使用!!
str!!.length
// 如果在声明时没有写声明可能为空,在使用时就不用写问号
// 下面这两种方式在使用时都不需要使用问号
val str: String = "abc"
val str = "abc"
str.length
如果在声明时使用了"?“,那就必须赋值,并且在用该字段时,也需要一直用”?“,就算上一句代码用了”?",下一句还代码还是需要使用,这是kotlin考虑到多线程的情况,反例
class MyActivity: Activity(){
private var text_tv: TextView? = null
override onCreate(){
text_tv = findViewById(R.id.text_tv)
text_tv?.text = "aaa"
text_tv?.text = "bbb"
}
}
可以看到,每次调用都需要用"?",这样真的很麻烦,虽然也可以用also、let等方法解决,但这里先用latainit解决。
lateinit关键字,如果一个变量在构建方法时为空,但会在类的某个方法初始化,就可以用lateinit关键字,这个在android开发中很常见。
class MyActivity: Activity(){
private lateinit var text_tv: TextView
override onCreate(){
text_tv = findViewById(R.id.text_tv)
text_tv.text = "aaa"
}
}
可以看到,用了lateinit之后,就不需要使用问号。但如果想要判断一个字段是否初始化了,需要使用isLateinit
this::text_tv.isLateinit
kotlin还有一个语法:“?:”。这个语法很好用,可以看一下例子:
fun test(str: String?){
str ?:return
}
这里str ?:return的意思就是,如果str为空就return,否则就继续执行。这个语法还有另一个用途:
fun test(str: String?): String{
str ?:return ""
}
如果有返回值,还能这样用,除此之外还有:
fun test(str: String?): String{
val newStr = str ?:return ""
}
还能用来赋值,如果不为空,就将值赋给newStr,否则就返回。
有了这个特性,就不再需要编写if(xxxx)return这样的代码,只需一行代码就可以搞定。
如果java的属性或方法在返回值上面加了@NotNull的注解,当kotlin去调用该属性/方法时,就无需使用"?"。如果不加就可能为空,可调用可不调用。如果加了@Nullable注解,就必须使用。
相对的,如果kotlin返回一个不为空的对象,java访问时,就会有@NotNull的注解,返回为空的对象就会有@Nullable的注解。
权限修饰符:kotlin的权限修饰符有private、protected、internal和pulbic。默认的权限修饰符是public,internal表示模块可见。比如项目中引用了moduleA,这个module里面某些类或方法使用了internal修饰符,在当前module就用不了。kotlin里面没有java中包可见的修饰符。
静态变量:kotlin种没有static修饰符,想要使用静态变量只能使用伴生对象。具体用法:
class Test {
companion object {
// const表示编译期常量,也就是说,该变量的值必须在编译时就可以确定的
// 使用了const修饰符的变量,在java中可以正常调用。比如这里就是:Test.VALUE_1
const val VALUE_1 = 1
// 如果没有使用const修饰符,就变成了这样:Test.Companion.getVALUE_2()
val VALUE_2 = 2;
// 此时,可以使用JvmStatic注解,但调用起来还是不好看,Test.getVALUE_3()
@JvmStatic
val VALUE_3 = 3
// 如果使用JvmField注解,就和VALUE_1一样了,Test.VALUE_4。
@JvmField
val VALUE_4 = 4
}
}
如果不是编译期常量,又必须被java调用,才有必要加上注解。如果不是编译期常量,而只被kotlin调用,那去掉const修饰符就行,就像VALUE_2。在kotlin中调用VALUE_2也只是:Test.VALUE_2。
可能有人会觉得kotlin特意搞这么一个出来反而更麻烦,和java的static没有区别,但实际开发下来给我的感受就是,使用了伴生对象之后,就可以将静态变量全部放在伴生对象里面。在java里面,只要使用了static就可以变成静态变量,这就很容易在开发中,将静态变量和非静态变量写在一起,这加大了维护的难度。而在kotlin里面,想要找静态变量只需到本省对象里面找就可以了,不用整个类文件找一遍。
泛型:kotlin的泛型和java泛型基本一样,只是换了新的写法和多了reified关键字。基本用法:
class Test<T>(value: T)
// 在使用时,可以指定泛型的类型
Test<Int>(1)
// 如果该类或者方法有某个参数使用到该泛型,并且传值了,可以省略泛型
Test(1)
泛型的边界:kotlin可以使用
class Test<T: CharSequence>(value: T)
Test("")
// 如果给泛型边界加上"?",在传参时还能传null,并且不需要在T旁边写"?"
class Test<T: CharSequence?>(value: T)
Test(null)
如果有多个边界,可以使用where关键字。多个边界只能有一个是类,其他的只能是接口,并且类必须放在最前面
class Test<T>() where T: CharSequence, T: View.OnClickListener, T: View.OnTouchListener
在kotlin中,如果声明了变量类型,在调用返回值为泛型的方法时,可以不用写泛型,这在安卓开发中很常见
// 声明了text_tv的类型时
val text_tv: TextView = findViewById(R.id.text_tv)
// 如果没有声明text_tv的类型,就必须指定泛型
val text_tv = findViewById<TextView>(R.id.text_tv)
真实的泛型,java中的泛型是伪泛型,而kotlin提供了reified关键字还实现真实的泛型。不过所谓真实的泛型,本质是通过代码内联还实现,并没有脱离java伪泛型。
// 一般情况下,启动Activity需要使用MainActivity::class.java来指定class对象
startActivity(Intent(this, MainActivity::class.java))
// 声明一个真实泛型的startActivity
inline fun <reified T: Activity> startActivity(context: Context){
startActivity(Intent(context, T::class.java))
}
// 使用
startActivity<MainActivity>(this)
可以看到,使用了reified关键字之后,就可以直接使用T的class。在声明reified泛型时,必须在方法前面写上inline关键字,这个关键字的作用下面会讲。
协变和逆变
无边界通配符,kotlin使用<*>来表示无边界通配符。
class Test<T>(value: T)
val test: Test<*> = Test(1)
匿名函数:只要使用kotlin,就会无时无刻在使用匿名函数,匿名函数是kotlin非常重要的特性,先看怎么声明。
// 声明
val function: (Int) -> Unit = {
}
// 使用
function(1)
function.invoke(1)
(Int) ->) Unit。这里的Int就是第1个参数的类型,Unit就是返回类型,匿名函数也没有任何参数也可以有多个参数,没记错的话,kotlin最多支持声明22个参数的匿名函数。在调用时,可以直接fucntion(…),也可以调用invoke方法。
在function里面,想要使用Int这个参数有2种方式:
val function: (Int) -> Unit = {
// it就是参数默认的名称
it + 1
}
// 也可以使用 xxx -> 这种方式来命名
val function: (Int) -> Unit = { i ->
i + 1
}
如果有1个以上的参数,就必须显示声明参数名称:
// 如果没有显示声明参数的名称,编译会报错
val function: (Int, String) ->Unit = {
}
// 有多个参数名称时,用","隔开
val function: (Int, String) ->Unit = { i, s ->
}
还能给参数提供一个名称:
// 在类型前面写名称,这样在写到 Unit = {之后,编译器就会提醒参数的默认名称
val function: (i: Int, s: String) ->Unit = { i, s ->
}
匿名函数也可以为空
// 在外部套一个括号,并加一个“?”,就可以声明一个空的匿名函数
var function: ((Int) -> Unit)? = null
// 再在其他地方初始化这个参数
function = {
}
上面基本就讲完匿名函数的声明方式,但有什么用?看一看forEach的代码就知道有什么用了
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit
val list = listOf(1, 2, 3)
list.forEach { }
可以看到,forEach方法有一个action参数,这个参数的结构和上面提到的声明方式是一样的。
在kotlin中之所以可以使用list.forEach这种方式来遍历一个list,就是因为kotlin提供了这么一个方法。而且kotlin的标准库里面在,这样的代码还有很多,下面讲到kotlin标准库时,就讲有哪些常用的方法。
并且日常开发中,我们也可以根据自己的需要,定义大量的匿名函数来解决我们的需求。
typealias:可以给一个匿名函数命名,比如:
typealias OnParentClickListener = (parentPosition: Int) -> Unit
typealias OnChildClickListener = (parentPosition: Int, childPosition: Int) -> Unit
使用
private var onParentClickListener: OnParentClickListener? = null
private var onChildClickListener: OnChildClickListener? = null
holder.itemView.setOnClickListener {
onParentClickListener?.invoke(parentPosition)
}
holder.itemView.setOnClickListener {
onChildClickListener?.invoke(parentPosition, childPosition)
}
元组:元组的概念我就不写了,不懂的查一下百度。我没有使用过其他编程语言,所以不清楚其他编程语言的元组要怎么用,所以不知道kotlin使用元组的方式是否和其他编程语言一样,我就是说一下怎么在kotlin里面使用元组。
先看一简单用法
val (v1, v2) = 1 to "a"
这里的to会将1和a变成了一个Pair对象,该对象是一个data class,data class会为当前类生成component1、component2…componentN的方法,来实现元组的功能。
这里的v1的类型就是Int,v2就是String。
这样有什么用?元组的写法使用有用不同的人有不同的看法,但Pair对象绝对有用,有时我们只是需要一个对象来存储某个几个值,没到非要声明一个对象不可,此时Pair可能就够用了。
kotlin还提供了Triple还声明元组,但Triple没有to这样的方法,需要手动创建。
val (v1, v2, v3) = Triple(1,"a",true)
然后有一点需要注意,这里的v1、v2和v3虽然用了括号,但还是属于当前大括号的作用域,所以这行代码的上下都不能有v1这样的变量,否则就有变量重名的问题。
再看看两种自定义元组的方式:
fun main() {
val test = Test()
val (v1, v2, v3, v4, v5, v6, v7) = test
}
class Test {
operator fun component1() = 1
operator fun component2() = "a"
operator fun component3() = true
operator fun component4() = 1
operator fun component5() = 1
operator fun component6() = 1
operator fun component7() = 1
}
只需在前面写operator并且方法名称是componentN就可以。
fun main() {
val test = Test("","",0L,0L)
val (v1, v2, v3, v4, ) = test
data class Test(val name: String, val company: String, val startDate: Long, val stopDate:Long)
data class也可以,data class下面会重点介绍,这里只是提到元组,才写出来。
属性委托:属性委托使用by关键字,通过属性委托,就能将任务交给委托的对象来执行。kotlin提供了ReadOnlyProperty和ReadWriteProperty用声明属性委托,好像还有其他方式,但我不会用,不过只要知道用by就是属性委托就行了。来一个lazy看看属性委托有什么用:
class MyActivity: Activity(){
val tex_tv by lazy{
findViewById<TextView>(R.id.text_tv)
}
override fun onCreate(savedInstanceState: Bundle?) {
text_tv.text = "aaa"
text_tv.text = "bbb"
}
}
上面的by lazy意思是延迟加载,在声明时,不会调用lazy里面的findViewById,只有在第一次使用时才会调用,第二次使用则不会。onCreate里面的代码,在第一次t调用text_tv时,发现这个变量为空,所以就调用lazy里面的代码初始化text_tv,第二次调用text时,text_tv已经初始化完成,所以就不会调用了。具体可以看看lazy里面的代码,里面的代码逻辑并不复杂。
上面还提到ReadOnlyProperty和ReadWriteProperty这种方式,我们可以自定义属性委托,将一些重复任务交给委托类来执行,拿我之前在工作的例子:
// 继承ReadWriteProperty,这个类有两个泛型,第一个是this,也就是要使用的外部类,第二个是属性类型,这里CharSequence也就是String
class ContentDescriptionValueDelegate: ReadWriteProperty<View, CharSequence?>{
// 获取属性的值,第一个参数是thisRef,类型就是第一个泛型,返回值的类型是第二个泛型
// 可以看到,这里的代码等于同view.getContentDescription().toString,只要记住返回值是这样就行,该方法的作用下面会讲
override fun getValue(thisRef: View, property: KProperty<*>): CharSequence? = thisRef.contentDescription?.toString()
// 这里就是设置值,调用委托的变量设置值,最终会执行这里的代码
// 这里的代码就是view.setContentDescription(value),并且还调用了getParent执行了其他代码
override fun setValue(thisRef: View, property: KProperty<*>, value: CharSequence?) {
thisRef.apply {
contentDescription = value
parent?.requestSendAccessibilityEvent(this, AccessibilityEvent.obtain(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION).also {
it.contentDescription = value
})
}
}
}
class AccessibilityTestLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0):
LinearLayout(context, attrs, defStyleAttr) {
// 这里的CharSequence?就是上面的第二个泛型,其实:CharSequence?可以省略。
// 可以看到,使用了属性委托之后,该变量的返回值是CharSequence,而不是ContentDescriptionValueDelegate
// 而在new ContentDescriptionValueDelegate时,也不需要将this传进去,只要外部类是第一个泛型就行
var contentDescriptionValue :CharSequence? by ContentDescriptionValueDelegate()
}
// MainActivity
val accessibilityTestLayout = AccessibilityTestLayout(this)
// 再看看实际使用,既然contentDescriptionValue的类型是CharSequence,那当然可以直接赋值
// 但实际上,这里编译器帮我们干了很多事,实际编译出来的代码会调用setValue方法,但这无所谓
// 虽然这是语法糖,但好用就不行,只要知道这个过程中发生了什么就行
// 所以这行代码就调用了setContentDescription方法和getParent...方法
accessibilityTestLayout.contentDescriptionValue = ""
// 如果没有赋值,就相当于调用了getValue方法,最终就会执行上面的view.getContentDescription()方法
// 而对于这里来说,只要调用contentDescriptionValue这个属性就行,非常方便。
accessibilityTestLayout.contentDescriptionValue
再说一下上面这些代码的作用,这样以后遇到重复代码时,才会考虑是否应该用属性委托去掉重复代码。
Delegate的setValue的作用就是设置了contentDescription并通知Parent View自己的contentDescription更新了。
这里如果不这样做,也可以写工具类去做,但不管怎么样都要有一个方法来统一处理,否则就需要编写重复代码。此时,属性委托就是方式之一。而具体要用什么方式,就具体问题具体分析。
再看看一个安卓相关的应用:
class MyOnClickListener(): View.OnClickListener{
override fun onClick(v: View?) {
}
}
val aaOnClick = MyOnClickListener()
class CoroutinesTestActivity : AppCompatActivity(), View.OnClickListener by aaOnClick {
}
可以看到Activity的onClick交给了aaOnClick这个外部变量去做。如果在开发中,多个控件的点击事件由一个类去做,代码就可以这样写。
val result = if(flag) 1 else 0
val result = if(flag) {
code...
1
} else {
code...
0
}
val result = if(flag){
code...
1
} else 0
kotlin的if-else,可以将最后一行作为返回值
for:kotlin的for和java完全不一样,kotlin的for循环本质是执行forEach,至于为什么这样说留到扩展方法讲。下面是几种for的用法:
// .. 表示i的取值范围为[0, 10]
for(i in 0.. 10){}
// until 表示i的取值范围为[0, 10)
for(i in 0 until 10){}
// step 2 表示每次递增2,所以这里的值是0 2 4 6 8 10
for(i in 0.. 10 step 2){}
// 上面这三种方式都是递增
// 下面这两种方式都是递减,10 downTo 0的取值范围内是[10, 0]
// stop 2就是每次递减2
for(i in 10 downTo 0){}
for(i in 10 downTo 0 step 2){}
val list = listOf(1, 2, 3)
// 这里就是遍历list所有的元素
for(item in list){}
// 也可以使用list的forEach扩展方法
list.forEach { }
when:kotlin用when代替了java的swich,kotlin的when比java的switch好用很多,语法不再那么啰嗦,功能也变得更强大。
val month = 1
var monthStr = ""
when(month){
1 ->{
monthStr = "一"
}
2 -> monthStr = "二"
3,4 -> monthStr = "3"
}
这是基础用法,可以看到,可以写大括号,也可以不写,如果只有一行代码,可以直接不写。再看3,4那里,意思是,如果是3或4,就执行后面的代码。但这种方式还是太麻烦了,可以简写:
val monthStr = when(month){
1 ->{
"1"
}
2 -> "2"
3,4 -> "3"
else -> "0"
}
直接在when前面声明monthStr,但这种方式就必须将额外情况写出来。最后的else就是java switch的defalut。可以看到,也支持多种形式,可以像if那样将最后一行返回。
java的switch还支持String类型,而kotlin是支持在when使用所有类型。不过有一点需要提前说清楚,kotlin虽然支持所有类型,但编译出来的代码可能会变成if-else,没有switch该有的性能。但这无所谓,因为会编译出这样的代码,说明本身就没办法用switch语法。而用kotlin的when,比较大的作用是让代码逻辑更加清晰,从而不用在一堆if-else里面寻找目标代码。
就比如上面的例子,在第2行就返回,看代码的人一看,就知道when里面是在获取实际的值,如果不关心代码细节,看到这样的代码之后,就不用看when里面的代码,而是看其他代码。后面如果关心代码实现,再回头看when里面的代码也不迟。
val any: Any = ""
val str = when(any){
is String -> "String"
is Int -> "Int"
else -> "unknow"
}
// 上面这种写法,还能这样
val str = when{
any is String -> "String"
any is Int -> "Int"
else -> "unknow"
}
// 将when括号里面的代码去掉,然后将any写到判断那里
// 既然可以将when括号的内容去掉,那就还能直接拿when当if-else使用
// 此时会自上而下匹配,这里虽然有两个结果为true,但state1的判断放在state2上面,所以会返回state1的结果,不会返回state2的结果
val state1 = "s1_1"
val state2 = "s2_1"
val result = when{
state1 == "s1_1" -> "s11"
state2 == "s2_1" -> "s22"
else -> "error"
}
可以看到,代码这样写了之后,就更加简洁了,而不用写一堆if-else。
try-catch:try-catch也可以将最后一行返回,具体如下:
val numberStr = "1"
val number = try {
numberStr.toInt()
}catch(e: Exception){
0
}
这里例子就是尝试将String转换为Int,如果转换失败就会抛异常执行catch的代码,此时可以写0作为返回值。
// 这个方法的返回值是Unit
fun test1(){}
// 写了:Int就表示返回值是InT
fun test1(): Int{}
// 可以在下面写return
fun test1(): Int{
return 1
}
// 也可以不写大括号,直接在=并写返回值
fun test1(): Int = 1
// 还能去掉返回类型,直接写返回值,kotlin会自动推断返回类型
fun test1() = 1
// 这种用法再结合if或when,就能代码看起来非常简洁
fun test(state: String) = when(state){
"success" -> 0
"error" -> 1
else -> 2
}
方法重写:kotlin方法重写时,必须写override关键字,否则会编译报错,而不是像java那样,写一个可有可无的注解。
override fun onCreate(savedInstanceState: Bundle?) {}
参数默认值:kotlin可以给方法的参数设置默认值:
fun test(i: Int, s: String = "")
// s设置了默认值,所以只需要传第一个参数就可以了
test(2)
fun test(i: Int =1, s: String = "")
// 如果两个都设置了默认值,就不需要传参数了
test()
fun test(i: Int =1, s: String )
// 如果第一个设置了默认值,并且不想传值,就必须写明要传值的变量,比如这里就是s
test(s = "1")
fun test(i: Int, s: String)
// 即使没有使用参数默认值,也可以通过指定参数名称来改变传参顺序
test(s = "1", i = 1)
可变数组:kotlin的可变数组和java的一样,只是多了参数默认值之后,有一点不一样:
// 使用vararg来声明可变参数
fun test(i: Int, s: String, vararg intArray: Int)
// 正常情况下,使用起来和java一样
test(1, "a", 1,2,3)
// 将可变数组声明在第一个参数
fun test( vararg intArray:Int,i: Int ,s:String)
// 想给i传值,就必须使用i =,s同理
test(1, 2, 3, i = 1,s = "a",)
// 将可变数组声明在第二个参数
fun test( i: Int ,vararg intArray:Int,s:String)
// 第一个值会被视为i,并且也没办法通过i = 来给i设值
test(1, 2, 3,s = "a")
方法引用,kotlin也可以使用方法引用,用法和java差不多:
class Test{
fun test(){
Thread(::a)
}
fun a(){}
}
这里Thread需要一个Runnable对象,此时直接传入a的方法引用。
匿名函数:在方法里面声明匿名函数有一些特殊的情况。如果将匿名函数方在方法的最后一个参数,就可以写在括号外部。如果只有一个参数,并且这个参数是匿名函数,括号都可以省略,这个可以看到List的forEach方法:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit
// 在调用forEach方法时,不需要写括号
listOf(1).forEach { }
// 如果还有其他参数,那就必须写括号
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C
// 可以看到,mapTo第一个参数不是匿名函数,第二个才是,此时括号就没办法省略
val list =ArrayList<Int>()
listOf(1).mapTo(list){}
如果有多个匿名函数,或者匿名函数不是作为最有一个参数,就只能将匿名函数写在括号里面:
fun test(action1: ()-> Unit, action2:()->Unit){}
test({}){}
fun test(action: () -> Unit, i: Int) {}
test({}, 1)
SAM转换:在java8之后,单一方法的接口称为SAM(Single Abstract Method)接口,java8通过Lanbda可以大大简化对SAM的调用,kotlin也是一样的。假设要设置onClick,可以有:
// 使用object关键字创建匿名内部类
test_btn.setOnClickListener(object: View.OnClickListener{
override fun onClick(v: View?) {
}
})
// 使用SAM简化
test_btn.setOnClickListener(View.OnClickListener{
})
// 简化
test_btn.setOnClickListener({
})
// 再简化
test_btn.setOnClickListener{}
将匿名函数作为返回值:
fun main() {
test().invoke()
test()()
}
fun test(): () -> Unit{
return {
println("test")
}
}
如果有多个参数:
fun test(): (Int, String) ->Unit{
return { i, s ->
println("test")
}
}
还能:
val action = {}
fun test(): () ->Unit{
return action
}
函数是kotlin的一等公民:在java中,不能直接在文件里面定义方法,而在kotlin里面,就可以直接定义方法和变量。比如:
public inline fun <T> listOf(): List<T> = emptyList()
如果点击看listOf的源码,就会发现listOf没有依托于任何外部类,而是在kt文件独立存在的,kotlin存在大量的这样的方法。
// 变量也可以独立存在
public val <T> List<T>.lastIndex: Int
inline:用inline修饰方法,方法就会变成一个内联方法,修饰类就会变成一个内联类。如果一个方法带有inline关键字,在编译完成后,该方法所有的代码会出现在调用的方法里面,而不只是调用该方法。怎么理解?就拿上面的listOf举例:
listOf<Int>()
上面的listOf由于没有传参数,所以调用的是emptyList,所以这里也不传参数,然后看看编译后的代码。
public final class TestKt {
public static final void main() {
CollectionsKt.emptyList();
}
public static void main(String[] var0) {
main();
}
}
编译后就是直接调用emptyList,这就是inline的作用。inline可以减少代码中的方法调用,不过这也会导致编译后的代码比编写的代码多得多。再看看forEach编译后的代码:
listOf(1,2,3).forEach {
println(it)
}
编译后:
public static final void main() {
Iterable $this$forEach$iv = (Iterable)CollectionsKt.listOf(new Integer[]{1, 2, 3});
int $i$f$forEach = false;
Iterator var2 = $this$forEach$iv.iterator();
while(var2.hasNext()) {
Object element$iv = var2.next();
int it = ((Number)element$iv).intValue();
int var5 = false;
System.out.println(it);
}
}
可以看到,并没有调用forEach方法,而是创建一个Iterator对象,再从Iterator里面遍历出所有对象。
infix关键字:infix关键字可以用来实现中缀函数,先看一个kotlin官方提供的中缀函数。
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
// 使用
val pair :Pair<Int, String> = 1 to "s"
可以看到,在调用to方法时,不需要使用括号,这就是infix关键字的作用。使用infix定义的方法,必须有且只有一个参数,是否有返回值都有可以。这个特性在日常开发中用得比较少,但偶尔可能有作用,所以提出来。
operator关键字:使用operator修饰的方法,可以使用kotlin提供的预定义符号进行调用,而不用使用方法名称。因此,在调用时,不需要使用括号。一个常见的例子:
listOf(1)[0]
这里的[0]就是预定义操作符之一,对着方括号的左边按住ctrl键+鼠标左键就可以发现,可以查看这个方括号的源码,源码是:
public operator fun get(index: Int): E
这个方法有operator关键字,名称是get。前面说了,operator可以使用预定义的操作符,同样的,也只能使用预定义的名称。具体有:
一元操作符:
符号 | 方法名称 |
---|---|
“+” | unaryPlus |
“-” | unaryMinus |
“!” | not |
算数操作符:
符号 | 方法名称 |
---|---|
“+” | plus |
“-” | minus |
“*” | times |
“/” | div |
“%” | rem |
比较操作符:
符号 | 方法名称 |
---|---|
”==“ / “!=” | equals |
“<”/“<=”/“>”/“>=” | “compareTo” |
集合操作符:
符号 | 方法名称 |
---|---|
“[]” | get |
“[]” | set |
“in” | contains |
“…” | rangeTo |
"…"表示的是一个区间,取值范围为:[n, m]。
一些例子:
// unaryMinus:这是compose的一个例子。
value class Dp(val value: Float) : Comparable<Dp> {
...
inline operator fun unaryMinus() = Dp(-value)
}
// 使用
-Dp(1f)
-1.dp
// "not"虽然也是一个操作符,但开发中用得比较少,更多的是直接调用not方法
// 使用”not“,就不用在编写完代码之后还将光标提到前面写"!"
// 而且“not"在任何地方都可以用,只要想要取反,都可以调用"not"
val str = "str"
str.isEmpty().not()
// 算数操作符我在18年开发时用过一次,需要对经纬度做加减乘除,所以我定义了下面这些方法
class LatLng(val lat: Float, val lng: Float)
operator fun LatLng.plus(latLng: LatLng): LatLng{
return LatLng(lat + latLng.lat, lng + latLng.lng)
}
operator fun LatLng.minus(latLng: LatLng): LatLng{
return LatLng(lat - latLng.lat, lng - latLng.lng)
}
// 这样就可以对经纬度做加减操作了
LatLng(1f, 1f) + LatLng(2f, 2f)
// 比较操作符最常见的例子就是String的equals操作,当使用 == 判断字符串是否相等时
// 就是在调用equals这个operator方法在判断
// compareTo也是一个很实用的操作符,而实际上,一个类只要实现了Comparable,就可以直接使用"<" ">"等操作符来比较,而不用使用operator关键字
data class LatLng(val lat: Float, val lng: Float): Comparable<LatLng>{
override fun compareTo(other: LatLng): Int {
return 0
}
}
LatLng(1f, 1f) < LatLng(2f, 2f)
// 再看一个比较有用的例子,通过下面这种方式,就可以比较两个BigDecimal,而不需要调用compareTo方法
BigDecimal.ZERO <= BigDecimal.ONE
// BigDecimal还有plus的operator,所以可以
BigDecimal.ZERO + BigDecimal.ONE
// 数组操作符也很实用,最常见的例子当然是最集合的操作,还能用这种方式对String进行操作
// 想要获取某个字符,不再需要调用indexOf,而是像在使用数组那样操作
val str = "str"
str[0]
// ".."符号可以用来声明一个IntRange
val range = 1 .. 3
// 只是用".."确实没什么用,但如果再加上"in"操作符就会变成很好用
// 再借助“in”操作符,就可以轻松的判断一个数字是否在某个区间里面
// 如果是java,还需要判断是否大于等于1,小于等于3
val result = 1 in 1..3
// 继承一个类必须后边写上括号,如果有参数就必须传参数,比如:AppCompatActivity()
// 如果有多个继承/实现类,使用","隔开。这里的CoroutineScope是一个接口
class CoroutinesTestActivity : AppCompatActivity(), CoroutineScope
// 这个类就有多个参数,所以必须使用括号。在括号里面通过xx: XX这样就可以声明一个参数
// 在将参数传给被继承类时,就可以使用这些参数
// 使用 @JvmOverloads 就可以让这个类生成多个构造方法,而不用写多个构造方法
class AccessibilityTestLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr)
// 也可以使用constructor声明多个构造方法
class MyView : View{
constructor(context: Context):super(context){
val a = 1
}
constructor(context: Context, attrs: AttributeSet?):super(context, attrs){
val b = 1
}
}
// 设置构造方法的可见性
// 可以通过这样将构造方法设置为private
class MyView private constructor(context: Context): View(context)
// 或者这样
class MyView : View{
private constructor(context: Context):super(context){
val a = 1
}
}
// 如果在声明类时就声明了构造方法,那要怎么初始化类的参数?使用init关键字
class MyView constructor(context: Context) : View(context) {
init {
val a = 1
}
}
// 在声明时顺便声明成员变量
// 只要在变量前面加val/var,这个变量就是一个成员变量,可以在类的任何地方使用
class Person(val name: String, val age: Int)
// 如果不希望被外部访问,还能加上权限修饰符
class Person(private val name: String, private val age: Int)
// 抽象类的声明方式和java是一样的
abstract class BasePerson(private val name: String, private val age: Int)
open关键字:kotlin在声明类时,类会有一个final的修饰符,这使得声明的类没办法被继承,如果希望一个类被继承,需要使用open关键字。成员变量和方法也是一样,如果希望一个方法能够被重写,也必须使用open关键字。
class BaseTest{
}
class Test1: BaseTest(){
}
如果没有使用open关键字,编译会报错:This type is final, so it cannot be inherited from.
在这句提示下面,还有一个可以点击的Button,内容为:Make ‘BaseTest’ ‘open’。
open class BaseTest{
}
class Test1: BaseTest(){
}
加了open之后就没问题了。
成员变量和方法
open class BaseTest{
open val a = 1
open fun test(){
}
}
class Test1: BaseTest(){
override val a = 2
override fun test(){
}
}
inner关键字:在kotlin里面,内部类默认为静态内部类,所以不会持有外部类的引用,如果希望这个类变成非静态内部类,需要使用inner修饰符,最常见的就是在Activity里面创建Hander。
如果没有使用inner,引用外部类的变量会编译报错。
var a = 1
class MyHandler: Handler(){
override fun handleMessage(msg: Message) {
a++
}
}
在class前面加一个inner就没问题了。
var a = 1
inner class MyHandler: Handler(){
override fun handleMessage(msg: Message) {
a++
}
}
一个文件编写多个class:在java里面,一个类文件只能有一个public类和多个没有public的类。而kotlin就可以在一个kt文件里面定义多个class,并且还能定义方法。
接口方法的默认实现:java1.7的接口方法是不允许编写代码的,只有在java8才能在接口方法里面编写代码,但也必须使用default关键字。在kotlin里面,在定义接口方法的同时,也可以直接提供方法实现。
data class,data class可以用来声明一些实体类,使用了data class之后,就会自动生成hashCode和equals方法,还会提供componentN方法,先看看怎么用:
// 这样会编译报错,构造方法必须有参数
data class Person()
data class Person(val name: String, val age: Int)
println(Person("a",1))
// 打印结果:Person(name=a, age=1)
data class还会自动生成深拷贝的方法,如果想要深拷贝一个对象,就不用自己手动编写长长的拷贝代码。
val p1 = Person("str",1)
val p2 = p1.copy()
val p3 = p1.copy("str2")
在深拷贝时,还能为某个变量重新赋值。
如果使用show kotlin bytecode就会发现,短短的一行代码,kotlin就会生成很多代码,这里我就不将代码贴出来了。
变量的get/set方法,在kotlin里面,可以给变量设置get和set方法,这样就不用编写额外的方法,而且用起来也比较方便。
class Test {
// 如果是val,就知只能重写get方法,不能重写set方法
val result1: Boolean
get() {
println("result1 set")
return System.currentTimeMillis() % 2 == 0L
}
val result2: Boolean
get() = System.currentTimeMillis() % 2 == 0L
var result3: Boolean = false
// field就是变量本身,通过field = value这种方式对变量赋值
set(value) {
if(field == value) {
return
}
field = value
}
get() = field
}
main(){
val test = Test()
println(test.result1)
test.result3 = true
println(test.result3)
}
// result1 set
// false
// result3 get
// true
而这个特性还不止于此,在kotlin使用java的get/set方法时,也能变成像在使用变量一样。
// 这两行代码本质上就是在调用TextView的setText和getText方法
test_tv.text = "aaa"
val txt = test_tv.text
而如果一个只有get方法,而没有set方法,也能使用这个特性
// java code
public class JavaTest {
public int getA(){
return 1;
}
}
// kotlin code
val test = JavaTest()
val a = test.a
// 由于没有set方法,所以不能这样用,否则会编译报错
// test.a = 1
object关键字,object关键字有两个用途,一个是声明匿名内部类,另一个是声明单例对象。先看匿名内部类:
// 想要声明匿名内部类就只能用object关键字。如果父类是一个类就补上括号,如果是接口就不用
val click = object :View.OnClickListener{
override fun onClick(v: View?) {
}
}
// 这里的onClick还能简写成
val click = OnClickListener { }
声明一个单例对象:
object Single{}
// 声明成单例对象之后,甚至可以写出这种意义不明的代码,只是调用该对象,但什么都不做
Single
object Single{
fun test(){
}
}
// 不需要声明伴生对象,就可以使用调用test方法
Single.test()
// 变量的用法也一样
fun main() {
Single.VALUE_1
Single.VALUE_2
}
object Single {
val VALUE_1 = "v"
// 声明编译期常量也不用在伴生对象里面声明
const val VALUE_2 = "v"
}
密封类:使用sealed声明。密封类和枚举有点像,但又不完全相同。密封类的子类可以携带自己独有的状态参数以及行为方法来记录更多的实现信息以完成更多的功能,而枚举类的参数只能和声明类一样。
没有使用密封类时:
abstract class NetworkCode(val code: Int)
class SuccessCode: NetworkCode(0)
class ErrorCode: NetworkCode(1)
val networkCode: NetworkCode = SuccessCode()
val networkState = when(networkCode){
is SuccessCode -> "success"
is ErrorCode -> "failed"
else -> "failed"
}
可以看到,还需要写else,否则就会编译报错
如果使用密封类,就不用写else了。并且构造方法可以随意定义,不需要像枚举那样只有固定的构造方法。
sealed class NetworkCode(val code: Int)
class SuccessCode(val str: String): NetworkCode(0)
class ErrorCode(val i: Int): NetworkCode(1)
val networkCode: NetworkCode = SuccessCode("")
val networkState = when(networkCode){
is SuccessCode -> "success"
is ErrorCode -> "failed"
}
inline class和value class:inline class可以用来对一个某个变量的行为进行封装,并且还能避免创建对象。比如:
fun main() {
val age = Age(-1)
age.getValidateAge()
}
inline class Age(val age: Int){
fun getValidateAge(): Int{
return if(age < 0){
0
}else age
}
fun isZero() = age == 0
}
这里声明一个Age的inline class,并在main方法调用。看看编译后的代码是什么:
public final class Age {
public static int constructor_impl/* $FF was: constructor-impl*/(int age) {
return age;
}
public static final int getValidateAge_impl/* $FF was: getValidateAge-impl*/(int $this) {
return $this < 0 ? 0 : $this;
}
}
int age = Age.constructor-impl(-1);
Age.getValidateAge-impl(age);
可以看到,编译后虽然还是声明了Age对象,但只是调用Age对象的静态方法,并没有创建Age对象。
在使用inline class时需要注意:
inline class是在kotlin1.3版本被引入的,在kotlin1.5之后,inline class进入稳定版本,kotlin引入value class,inline class被弃用。如果在1.5以上的版本使用inline class,会有一个warning:
'inline' modifier is deprecated. Use 'value' instead
value class对inline class进行了一些优化,目前,value&nsbp;class的用法和inline class是一样的,也只能使用一个参数。未来的kotlin版本可能会支持构造方法多个参数的value class。
在使用value class时,还需要在声明类时使用@JvmInline注解。
data class Person(var name: String, var age: Int, private var priateAge: Int)
val Person.aaaAge: Int get() = age
var Person.bbbAge: Int
get() = age
set(value) {
age = value
// 编译报错
priateAge = value
}
main(){
val person = Person("",1,2)
val bbbAAge = person.bbbAge
person.bbbAge = 3
}
var/val Xxxx.yyy get()/set()。
可以用var或val修饰,val必须提供get方法,var必须提供get和set方法,扩展属性不能操作被扩展的属性,因为扩展属性本质是对该属性生成getter和setter方法,并没有实际存在该属性。
而无论是扩展属性还是扩展方法,都可以访问到被扩展类的public属性和方法,所以可以在set方法里面调用age变量,如果是private属性是没办法调用的。再看看编译后的代码:
Person person = new Person("", 1, 2);
int bbbAAge = getBbbAge(person);
setBbbAge(person, 3);
public static final int getBbbAge(@NotNull Person $this$bbbAge) {
Intrinsics.checkNotNullParameter($this$bbbAge, "$this$bbbAge");
return $this$bbbAge.getAge();
}
public static final void setBbbAge(@NotNull Person $this$bbbAge, int value) {
Intrinsics.checkNotNullParameter($this$bbbAge, "$this$bbbAge");
$this$bbbAge.setAge(value);
}
扩展属性本质是静态调用,扩展函数也是一样的,后面就不重复赘述。
扩展函数语法:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
fun Xxx.yyy()。
知道语法之后,日常开发就可以自己定义一些扩展函数,因为kotlin的标准库不可能满足所有的开发需求。
扩展属性/函数如果定义在一个普通的kotlin文件,作用域就是整个项目。扩展函数还能定义在类里面,这样作用域就只是该类,外部访问不到这些扩展函数。比如:
fun main() {
val person = Person("",0)
// 编译报错,访问不到该方法
person.isZero()
}
data class Person(var name: String, var age: Int){
fun test(){
val isAgeZero = age.isZeor()
}
fun Int.isZeor() = this == 0
}
// 写法1
var action: (() -> Unit)? = null
// 写法2
var action: Functiuon0<Unit>? = null
// 使用
action = {}
可以看到,两者可以用同一种用法,这里的Funtion0是什么?Funtion0就是kotlin匿名函数的底层对象,所有的() -> Unit这样的语法,最终都会编译成Funtion0这种形式。
0就是0个参数,后面的泛型就是返回类型。如果有1个参数,就是Funtion1,Function最多支持到22。
看看Funtion的源码:
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
/** A function that takes 3 arguments. */
public interface Function3<in P1, in P2, in P3, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3): R
}
/** A function that takes 4 arguments. */
public interface Function4<in P1, in P2, in P3, in P4, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4): R
}
/** A function that takes 5 arguments. */
public interface Function5<in P1, in P2, in P3, in P4, in P5, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5): R
}
可以看到,每个对象都有invoke方法,所以在调用时,也可以调用invoke方法。具体可以看看Functions.kt这个文件,可以看到最多支持到Funtion22。
val regex = ""
val str = "str"
str.replace(regex,"")
如果不调用toRegex,那就不是做正则替换,只有调用了toRegex才可以
val regex = "".toRegex()
val str = "str"
str.replace(regex,"")
java的replace方法是不能传入正则表达式的,只有调用replaceAll才可以传入正则表达式,而kotlin直接调用replace方法,相应的,kotlin没有replaceAll方法。
// also
val list = ArrayList<Int>().also{
it.add(1)
it.add(2)
}
// let
val value = 1f / 3
val valueStr: String = value.let { DecimalFormat("0.00").format(value) }
// let配合as关键字,还能在开发中少编写一些代码,如果转型失败,也只会返回null。此时,不要继续执行就行了
val str: Any = "str"
str.let{ it as? String }?.let{ "$it, str1" }?.also{
println(it)
}
run、with、apply:他们都可以在代码块里面访问到调用类的pubic属性和函数。run和with都是将最后一行作为返回值,apply是返回调用者本身。
data class Person(var name: String, var age: Int)
val str1: String = Person("",0).run {
name = "a"
age = 1
""
}
val str2: String = with(Person("", -0)){
name = "a"
age = 1
""
}
Person("", 0).apply {
name = "a"
age = 1
}
这几个方法在android中非常实用,经常用在RecyclerView的onBindViewHolder里面,一般就是:
vh.apply{
entity.apply{
name_tv.text = name
}
}
这样就不用每次都调用ViewHolder和Entity了。
takeIf:如果false,就返回null。
parentFile.listFiles()?.takeIf { it.isEmpty() }?.also {
parentFile.delete()
}
可以看到,如果文件列表为空,就继续执行,最后删除文件。而如果为null就不继续执行,是一个非常实用的方法。
runCatching:如果执行的过程中出现异常,不会直接抛异常,而是将异常捕获起来,再自己决定要怎么处理这个问题。
fun String.safeToInt(defaultValue: Int): Int{
return this.runCatching {
toInt()
}.onSuccess {
// code
}.onFailure {
// code
}.getOrDefault(defaultValue)
}
这里用this调用runCatching之后就可以直接用代码块里面使用String的toInt,执行runCatching之后,返回的是一个Result对象,可以调用该对象的一些方法对结果进行处理。而如果查看Result对象的源码,就可以发现Result是一个value class,所以在这个过程中不会创建对象。
除了上面提到的几个方法,runCatching还有其他方法,可以自己去看看。
buildString:会创建一个StringBuilder,可以在代码块里面调用append方法,返回值是String。
val str: String = buildString {
append("a")
append("b")
}
joinToString:将数组或列表的元素组合成String。比如:
// 打印:1, 2, 3
listOf(1, 2, 3).joinToString().also(::println)
这个方法有很多个参数,常用参数有第一个和最后一个。第一个参数是分隔符,默认是:", "。最后第一个是返回值,可以不传,会调用对象的toString方法,也可以使用该方法重写拼装成String的逻辑。
repeat:重复n次。
// it就是次数,从0开始
repeat(10){
printlnt(it)
}
TODO:这是一个非常有意思的方法。在java里面,如果一个方法想要防止自己忘记实现,通常可以用todo进行注释,而到了kotlin,就有了TODO方法。使用TODO方法之后,代码不但会变成蓝色,在执行到这行代码之后,还会抛出异常。如果觉得需要通过异常这种方式来提醒自己,就可以这样。
目前就这些方法吧,平时我在开发中,我也不会留意自己调用了哪些方法,需要的时候自然会想起来,下面是list和Map相关的方法。
先说清楚,list有的方法,array大部分也有,所以array也可以调用同样的方法。
listOf相关的方法:通过可变数组构建一个list。除了listOf,还有mutableListOf、arrayListOf、emptyList、linkedSetOf。这些都是构建一个普通的list。
listOfNotNull相关的方法:这个方法会将传入的元素过滤一下,保证list里面没有空元素,是一个非常实用的方法,例子:
val realSecond = it / 1000L
val second = realSecond % 60
val min = realSecond % 3600 / 60
val hour = realSecond % 86400 / 3600
val day = realSecond / 86400
listOfNotNull(
day.takeIf {it != 0L}?.let {it to "天"},
hour.takeIf {it != 0L}?.let {it to "小时"},
min.takeIf {it != 0L}?.let {it to "分钟"},
second.takeIf {it != 0L}?.let {it to "秒"},
).joinToString("") {"${it.first}${it.second}"}
这里的it类型是Long,这里的需求就是计算出几天几时几分几秒。使用listOfNotNull和takeIf就可以保证list里面的数据不为空且数值不是0,然后再调用joinToString方法拼装成一个字符串。
forEach相关的方法:用来遍历list。
// forEach
listOf(1, 2, 3).forEach{}
// forEachIndexed
listOf(1, 2, 3).forEachIndexed { index, it -> }
// indices,通过indices方法可以来遍历list的角标
listOf(1, 2, 3).indices.forEach { }
map:进行类型转换。
// 这样就可以将一个List转换成一个List
listOf(1, 2, 3).map{ it.toString }
// mapNotNull,过滤空数据,注意,mapNotNull之前的空数据还会保留,只有map过程中的空数据才会被过滤掉
listOf(1,2, 3).mapNotNull { it.takeIf { it % 2 != 0 }?.toString() }.forEach(::println)
// 1 3
// mapTo
// 1 2 3
val list = ArrayList<String>()
listOf(1, 2, 3).mapTo(list){it.toString()}
list.forEach(::println)
map还有mapIndexed、mapIndexedNotNull等方法,从名字也可以看出有什么用,我就不提供示例代码了。
filter:过滤数据
listOf(1, 2, 3).filter { it % 2 == 1 }.forEach(::println)
// 1 3
// filterNot则是相反的结果
listOf(1, 2, 3).filterNot { it % 2 == 1 }.forEach(::println)
// 2
listOf(1, 2,null, 3).filterNotNull().forEach(::println)
// 1 2 3
listOf(1, 2,"str", 3).filterIsInstance<Int>().forEach(::println)
// 1 2 3
filter还有filterTo、filterIndexed等方法。
get相关的方法:get相关里面,有一个是getOrNull方法。我认为这类方法是非常重要的,使用这类方法,可以有效防止越界异常。
val list = listOf(1, 2, 3)
// 两种写法
list.get(0)
list[0]
// 获取第1个元素
list.first()
// 如果不希望获取时发生越界,可以调用firstOrNull。此时,如果没第1个元素,就会返回空
list.firstOrNull()
// 获取最后一个元素
list.last()
list.lastOrNull()
first和last都有一个带有匿名函数参数的方法,意思是,从头/伟寻找符合要求的元素,如果找到了,就会返回,否则就抛异常
val i = list.first { it % 2 == 0 }
它们也有orNull方法,如果不希望抛异常,就可以使用orNull方法。
除了first和last,也有getOrNull和getOrElse方法,
list.getOrNull(4)
val result = list.getOrElse(4){5}
println(result)
// 5
在日常开发中,如果需要通过角标获取元素,我更喜欢用orNull方法,这样就可以尽量避免程序在运行时出现异常。在调用orNull之后,再调用“?”判断即可,写起来也不费劲。
take和takeLast:take就是获取前n个元素。takeLast就是获取后n个元素。和sublist是一样的,但在这里,需要传一个n的参数。
groupBy:从方法名称就可以看出,这个方法是做数据归集的。归集后是一个Map
data class Person(val name: String, val age: Int)
fun main() {
listOf(
Person("str", 1),
Person("str1", 2),
Person("str2", 1),
Person("str3", 2),
Person("str4", 1),
).groupBy { it.age }.forEach{
println("${it.key}, ${it.value}")
}
}
1, [Person(name=str, age=1), Person(name=str2, age=1), Person(name=str4, age=1)]
2, [Person(name=str1, age=2), Person(name=str3, age=2)]
这里的key就是age,value就是List
find方法:这里的find和上面提到的first{}有点像,都是从list里面寻找合适的元素。但区别是:find当找不到时,会返回空,而不是抛出异常。
any:从list寻找合适的元素,如果找到,返回true,都没有找到返回false。适合用来判断list是否有某个元素符合要求。
all:遍历list所有的元素,如果所有元素都符合要求,返回true,否则返回false。使用用来判断list里面所有的元素是否符合要求。
List还有很多方法, 我也没办法一一例举,剩下的可以自己随便点list的某个方法,然后看看kt文件里面还有什么方法。
Map相关的方法:
构建map:有一种非常简单的构建map的方法。
mapOf(
1 to "a",
2 to "b",
3 to "c",
)
使用to就可以轻松地构建一个map。
遍历map:遍历map不再需要向java那样麻烦,只需调用map的forEach方法,就可以轻松遍历。
map.forEach { key, value ->
println("$key, $value")
}
map.forEach {
println("${it.key}, ${it.value}")
}
for((key, value) in map){
println("$key, $value")
}
get方法:get就没有getOrNull这样的方法,只有getOrDefault和getOrElse方法。而map获取元素和设置元素还能像在使用array一样:
val map = HashMap()
map[1] = "a"
val a = map[1]
map方法:,Map的map方法返回的是List
val map = mapOf(1 to "a", 2 to "b", 3 to "c",)
map.map { "${it.key}${it.value}" }.forEach(::println)
// 1a 2b 3c
除了上面提到的方法,map还有filter、any、all等方法可以使用,但相对来说,没有List的方法那么多。
File相关的方法:kotlin还为File提供了几个特别有用的方法
writer和bufferWriter:
val file = File("")
val writer = file.writer()
writer.write("")
writer.close()
val writer = file.bufferedWriter()
writer.write("")
writer.newLine()
writer.close()
可以看到,想要使用文件写入数据,只需简简单单的一句writer或bufferedWriter,就可以完成,而不用像java那样写一堆代码。
readLines:readLines也是一个非常实用的方法,只需一行,就能将一个文件读到内存中。
val file = File("")
val lines:List<String> = file.readLines()
如果想要合并lines,就可以使用上面提到的joinToString方法
lines.joinToString("")
delete:kotlin提供了删除整个文件夹的api,想要删除一整个文件夹,只需调用deleteRecursively方法即可,而不用手动做文件收集再删除。
copy:既然删除可以删除整个文件夹,那复制也可以复制整个文件夹,可以调用copyRecursivel来复制整个文件夹。
walk方法:walk方法可以收集整个文件夹里面所有的文件。walk还有一个walkBottomUp方法,意思是倒序排序。
使用了kotlin写代码之后,就要用好的kotlin的特性,可以去知乎查一下kotlin的奇淫技巧,有一些编码方式虽然不是很推荐,但可以打开的自己脑洞,帮助自己用好kotlin的特性。
像扩展函数也要多用,必要时,也要自己定义扩展函数,我在开发中自己定义的扩展函数是非常多的。还要用好if-else、try-catch、when这些表达式,这些可以将最后一行作为返回值,而when的语法变为更加简洁,所以是非常好用的。
如果还是带着java的思维在写kotlin的代码,那开发效率和编写出来的代码就和用java差不多。有些人在用kotlin之后,还是带着java的思维在编码,事后就说kotlin也不怎么样啊。
就先写这些,这篇文章我也是边写边想写出来的,而不是去查看官方文档,如果想要看看还有什么内容漏了,可以看看kotlin中文站,后面我想到什么内容也会补上来。