表达式:fun 方法名(参数名1: 数据类型, 参数名2: 数据类型): 返回值数据类型{
}
fun test(a: Int): String {
return "";
}
// 将表达式作为函数体,花括号可以省略
fun test1(a: Int): Int = 1;
// 函数返回无意义的值Unit,返回值可省略
fun printSum(a: Int, b: Int) {
println("sum of $a and $b is ${a + b}")
}
// else不可省略
fun test2(a: Int, b: Int): Int {
if (a > b) {
return a
} else {
return b
}
}
fun test3(a: Int, b: Int): Int = if (a > b) a else b
fun test8(s1: String, s2: String): Int {
val i1 = test7(s1)
val i2 = test7(s2)
return if (i1 != null && i2 != null) i1 * i2 else 0
}
fun test4(a: String?): String {
return a?.length.toString() ?: "0";
}
fun test5(file: File?): String {
return file?.absolutePath ?: "xx";
}
fun test6(file: File?) {
file ?: throw IllegalArgumentException()
}
fun test7(s: String?): Int? = s?.toInt() ?: 0
1、必须加else
2、如果对象是class类型的,则必须用 is
3、object / companion object这种类型的,则不用加 is
class A{
}
object B{
}
// else不可省略
fun test9(s: Any): Int = when (s) {
"1" -> 1
"2" -> 2
is A -> 3
B -> 4
else -> 0
}
fun test10(file: File?) {
var let = file.let {
it?.absolutePath
it?.name
}
val also = file.also {
it?.absolutePath
it?.name
999
}
with(file) {
this?.absolutePath
[email protected](file)
this?.name
}
file.run {
this?.absolutePath
this?.name
}
file.apply {
this?.absoluteFile
this?.absolutePath
this?.name
}
}
fun test11(file: File?): String = file.let { it?.absolutePath } ?: ""
// a b互换值
fun test12() {
var a = 1;
var b = 2;
a = b.also { b = a }
}
fun test13(obj: Any): String? {
return if (obj is String) obj else null
}
fun test14() {
val list = listOf("a", "b", "c")
for (li in list) {
println(li)
}
for (index in list.indices) {
println("$index ---${list.get(index)}")
}
list.forEach {
}
val map = mapOf("a" to "aa", "b" to "bb", 1 to "cc")
map.forEach {
println("${it.key} ${it.value}")
}
map.entries.forEach {
println("${it.key} ${it.value}")
}
}
fun test15(a: Int): Int {
return if (a in 1..10) a else 0
}
1、当参数类型是array时,vararg可直接传入。
2、当参数类型是vararg时,array传入时需要在前面加上 * 号。
申明变量的关键字:val 和 var
区别:val表示只读,默认只有getter方法,var表示读写,默认有getter和setter方法
// ?表示可为null,kotlin里默认都不可为null
var name: String? = null
如果不想赋值为null,可用 lateinit 关键字
lateinit var file:File
1、默认public,可省略不写
2、kotlin中的类默认是不可被扩展修改的,类似于java的final修饰,不可被继承,要想可被继承,可用open修饰符。
3、kotlin中的接口(interface)和抽象类(abstract)默认是public和open的。
4、接口和抽象类中的成员变量,在实现类或子类中必须实现
interface IDemo {
var text:String
fun iTest()
}
abstract class AbsDemo {
abstract fun absTest()
}
// 非抽象类必须用open修饰,才可被继承
open class BaseDemo {
// 非抽象方法必须用open修饰,才可被重写
open fun baseTest() {
}
var file: File? = null
}
class Demo : IDemo, BaseDemo() {
// 必须实现
override var text: String = ""
get() = field
set(value) {
field = value
}
override fun iTest() {
}
override fun baseTest() {
super.baseTest()
// 可访问父类的成员变量
file
}
}
5、另类创建接口实现类的方法,无需创建这个kt类,类似于内部类中的匿名内部类。
fun test() {
val v = object : IDemo {
override var text: String
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
set(value) {}
override fun iTest() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}
// 如果类A需要实现接口B,可以不用在A上面申明实现B
val a = object : A(),B{
}
// 或者我们仅仅只是需要一个对象而已,不想去创建这个kt类
fun test() {
val obj = object {
var a: Int? = null
}
println(obj.a)
}
1、kotlin分为主构造器和次构造器,主构造器写在类名后面
2、如果主构造器没有被任何修饰符或者注解修饰,可省略 constructor构造器可用
3、构造器可用public protected private修饰
4、构造器参数中,可用var修饰参数,这时候参数可变成成员变量使用,类似于java中的 this.value = value;如果没有任何修饰,仅仅只是参数,不可被使用。
5、次构造器需要委托到主构造器,用 : this() 或者 : super()表示
6、如果父类的主构造器是有参构造,子类也必须传入参数
7、当是内部类时,this代表内部类对象引用,this@MainActivity代表外部类对象引用,相当于java中的MainActivity.this
public open class BaseDemo constructor(a: String) {
open fun baseTest() {
}
var file: File? = null
}
class Demo @SuppressWarnings constructor(var name: String, var age: Int) : IDemo, BaseDemo(name) {
fun test() {
println(name)
}
constructor(name: String, age: Int, sex: Int) : this(name, age) {
}
override fun iTest() {
}
override fun baseTest() {
super.baseTest()
file
}
}
1、kotlin的内部类必须用 inner class 描述
2、内部类可使用外部类的成员变量和方法
class Demo @SuppressWarnings constructor(var name: String, var age: Int) : IDemo, BaseDemo(name) {
fun test() {
println(name)
}
constructor(name: String, age: Int, sex: Int) : this(name, age) {
}
override fun iTest() {
}
override fun baseTest() {
super.baseTest()
file
}
inner class Demo2 constructor(var ctx: Context) : AbsDemo() {
override fun absTest() {
println(name)
iTest()
baseTest()
}
}
}
3、匿名内部类
t.setOnClickListener(object : View.OnClickListener {
override fun onClick(p0: View?) {
println("aa")
println("bb")
}
})
t.setOnClickListener { v ->
println("aa")
println("bb")
}
t.setOnTouchListener { view, motionEvent ->
println("aa")
println("bb")
return@setOnTouchListener true
}
1、kotlin中的常量不能直接申明在class类中,必须申明在object类或者companion object类中
2、用object必须申明类名,可单独使用,也可放在class类里使用;而companion object表示伴生类,必须伴随着class类使用
3、object和class的区别就是使用方式不同
// 获取Config实例:val config = Config()
// 获取A实例:val a = Config.A
class Config {
object A{
const val name:String = "xxx"
}
}
// Config.name
object Config {
const val name:String = "xxx"
}
// Config.name
class Config {
companion object{
const val name:String = "aaa"
}
}
1、kotlin中的getter和setter默认是隐藏的,如果需要自定义,在方法里做一些逻辑处理,或者比如实现单例等,可自己实现
2、set() 和 get() 必须跟随着成员变量,field代表了方法上面的成员变量,是一对一的关系
class User {
var name: String? = null
set(value) {
field = value
}
get() = field
var age: Int? = null
set(value) {
field = value
}
get() = field
lateinit var file:File
constructor(name: String?, age: Int?) {
this.name = name
this.age = age
}
}
Kotlin 能够扩展一个类的新功能而无需继承该类
声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀
fun String?.empty(): Boolean {
return if (this == null || this.length <= 0) true else false
}
override var text: String = ""
get() = field
set(value) {
if (value.empty()) {
field = "xxx"
} else {
field = value
}
}
这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象)。即this代表了调用empty()方法的那个对象。
1、我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data
2、主构造函数需要至少有一个参数;
3、主构造函数的所有参数需要标记为 val 或 var;
4、数据类不能是抽象、开放、密封或者内部的
data class Student constructor(var name:String){
}
当存在判断条件时,在java中一般使用的是if else,或者switch,在kotlin中,或者使用when,这时候必须要加上else ->,是不能省略的。
或者用枚举Enum。
但是枚举存在一个限制条件,就是只能示例一个对象,而且不能携带参数。
为了解决以上问题,就出现了密封类sealed。
1、密封类用sealed表示。
2、密封类的子类必须申明在密封类内部或者同一文件中。因为密封类的构造方法是私有的。
3、密封类无法实例化。
sealed class UiOp {
object Show: UiOp()
object Hide: UiOp()
class TranslateX(val px: Float): UiOp()
class TranslateY(val px: Float): UiOp()
}
fun execute(view: View, op: UiOp) = when (op) {
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
is UiOp.TranslateX -> view.translationX = op.px
is UiOp.TranslateY -> view.translationY = op.px
}
1、out 相当于java里面的 extend>
2、in 相当于java里面的 super>
inline 就是我们常说的内联。这个关键字会在编译期间起作用。如果一个函数是 inline 的,那么编译器会在编译的时候,把这个函数复制到调用处。
比如如下代码:
// Kotlin
fun main(args: Array) {
multiplyByTwo(5)
}
fun multiplyByTwo(num: Int) : Int {
return num * 2
}
他进行反编译之后的等价 Java 代码如下:
// Java
public static final void main(@NotNull String[] args) {
multiplyByTwo(5);
}
public static final int multiplyByTwo(int num) {
return num * 2;
}
可以看到,不加 inline 的方法,编译成字节码,然后再反编译成等价 java 代码,得到的结果是一个普通的方法。这个跟我们的常识是吻合的。
但是,当我们把方法用 inline 修饰了之后,会发生什么呢?
比如如下代码中,我们把 multiplyByTwo 用 inline 参数修饰了一下:
// Kotlin
fun main(args: Array) {
multiplyByTwo(5)
}
inline fun multiplyByTwo(num: Int) : Int {
return num * 2
}
反编译得到的结果如下:
// Java
public static final void main(@NotNull String[] args) {
int num$iv = 5;
int var10000 = num$iv * 2;
}
public static final int multiplyByTwo(int num) {
return num * 2;
}
可以看到,inline 中的方法,被复制到了调用方。这就是 inline 威力强大的地方!
1、类委托
interface IBy {
fun test() : Int
}
class ByImpl : IBy {
override fun test(): Int {
return 1
}
}
class ByProxy constructor(by:IBy) : IBy by by{
override fun test(): Int {
return 2
}
}
println(ByProxy(ByImpl()).test())
2
2、属性委托
语法格式:
val/var <属性名>:<类型> by <表达式>
by关键字之后的表达式就是委托,属性的get()方法(以及set()方法)将被委托给这个对象的getValue()和setValue()方法。属性委托不必实现任何接口,但是必须提供getValue()函数(对于var属性,还需要setValue()函数)。
如果申明的是val,则只需要实现getValue()
如果申明的是var,则只需要实现getValue()和setValue()
class ByDelegate {
operator fun getValue(nothing: Nothing?, property: KProperty<*>): Int {
return 1
}
operator fun setValue(nothing: Nothing?, property: KProperty<*>, i: Int) {
}
}
var a:Int by ByDelegate()
顾名思义:函数类型,就是函数的类型。
// (Int, Int) ->Float
// ↑ ↑ ↑
fun add(a: Int, b: Int): Float { return (a+b).toFloat() }
将函数的 参数类型 和 返回值类型 抽象出来后,就得到了 函数类型。
(Int, Int) -> Float 就代表了参数类型是 两个 Int 返回值类型为 Float 的函数类型。
高阶函数是将函数用作参数或返回值的函数。
上面的话有点绕,直接看例子吧。如果将 Android 里点击事件的监听用 Kotlin 来实现,它就是一个典型的高阶函数。
// 函数作为参数的高阶函数
// ↓
fun setOnClickListener(l: (View) -> Unit) { ... }
Lambda 可以理解为函数的简写。
fun onClick(v: View): Unit { ... }
setOnClickListener(::onClick)
// 用 Lambda 表达式来替代函数引用
setOnClickListener({v: View -> ...})
看到这,如果你没有疑惑,那恭喜你,这说明你的悟性很高,或者说你基础很好;如果你感觉有点懵,那也很正常,请看后面详细的解释。
刚接触到高阶函数和 Lambda 的时候,我就一直有个疑问:为什么要引入 Lambda 和 高阶函数?这个问题,官方文档里没有解答,因此我只能自己去寻找。
这个问题站在语言的设计者角度会更明了,让我们看个实际的例子,这是 Android 中的 View 定义,我省略了大部分代码:
// View.java
private OnClickListener mOnClickListener;
private OnContextClickListener mOnContextClickListener;
// 监听手指点击事件
public void setOnClickListener(OnClickListener l) {
mOnClickListener = l;
}
// 为传递这个点击事件,专门定义了一个接口
public interface OnClickListener {
void onClick(View v);
}
// 监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListener l) {
getListenerInfo().mOnContextClickListener = l;
}
// 为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener {
boolean onContextClick(View v);
}
Android 中设置点击事件和鼠标点击事件,分别是这样写的:
// 设置手指点击事件
image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gotoPreview();
}
});
// 设置鼠标点击事件
image.setOnContextClickListener(new View.OnContextClickListener() {
@Override
public void onContextClick(View v) {
gotoPreview();
}
});
请问各位小伙伴有没有觉得这样的代码很啰嗦?
现在我们假装自己是语言设计者,让我们先看看上面的代码存在哪些问题:
定义方:每增加一个方法,就要新增一个接口:OnClickListener,OnContextClickListener
调用方:需要写一堆的匿名内部类,啰嗦,繁琐,毫无重点
仔细看上面的代码,开发者关心的其实只有一行代码:
gotoPreview();
如果将其中的核心逻辑抽出来,这样子才是最简明的:
image.setOnClickListener { gotoPreview() }
image.setOnContextClickListener { gotoPreview() }
Kotlin 语言的设计者是怎么做的?是这样:
1、用函数类型替代接口定义
与上面 View.java 的等价 Kotlin 代码如下:
//View.kt
var mOnClickListener: ((View) -> Unit)? = null
var mOnContextClickListener: ((View) -> Unit)? = null
fun setOnClickListener(l: (View) -> Unit) {
mOnClickListener = l;
}
fun setOnContextClickListener(l: (View) -> Unit) {
mOnContextClickListener = l;
}
2、用 Lambda 表达式作为函数参数
image.setOnClickListener{v ->
gotoPreview();
}
以上做法有以下的好处:
定义方:减少了两个接口类的定义
调用方:代码更加简明
细心的小伙伴可能已经发现了一个问题:Android 并没有提供 View.java 的 Kotlin 实现,为什么我们 Demo 里面可以用 Lambda 来简化事件监听?
// 在实际开发中,我们经常使用这种简化方式
setOnClickListener { gotoPreview() }
原因是这样的:由于 OnClickListener 符合 SAM 转换的要求,因此编译器自动帮我们做了一层转换,让我们可以用 Lambda 表达式来简化我们的函数调用。
那么,SAM 又是个什么鬼?
SAM(Single Abstract Method),顾名思义,就是:只有一个抽象方法的类或者接口,但在 Kotlin 和 Java8 里,SAM 代表着:只有一个抽象方法的接口。符合 SAM 要求的接口,编译器就能进行 SAM 转换:让我们可以用 Lambda 表达式来简写接口类的参数。
注:Java8 中的 SAM 有明确的名称叫做:函数式接口(FunctionalInterface)。
FunctionalInterface 的限制如下,缺一不可:
必须是接口,抽象类不行
该接口有且仅有一个抽象的方法,抽象方法个数必须是1,默认实现的方法可以有多个。
也就是说,对于 View.java 来说,它虽然是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener 符合 SAM 转换的条件,所以会自动做以下转换:
转换前:
public void setOnClickListener(OnClickListener l)
转换后:
fun setOnClickListener(l: (View) -> Unit)
// 实际上是这样:
fun setOnClickListener(l: ((View!) -> Unit)?)
((View!) -> Unit)?代表,这个参数可能为空。
当 Lambda 表达式作为函数参数的时候,有些情形下是可以简写的,这时候可以让我们的代码看起来更简洁。然而,大部分初学者对此也比较头疼,同样的代码,能有 8 种不同的写法,确实也挺懵的。
要理解 Lambda 表达式的简写逻辑,其实很简单,那就是:多写。
各位小伙伴可以跟着我接下来的流程来一起写一写:
这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类:
image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
gotoPreview(v)
}
})
如果我们删掉 object 关键字,它就是 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:
image.setOnClickListener(View.OnClickListener { view: View? ->
gotoPreview(view)
})
上面的 View.OnClickListener 被称为: SAM Constructor—— SAM 构造器,它是编译器为我们生成的。Kotlin 允许我们通过这种方式来定义 Lambda 表达式。
思考题:
这时候,View.OnClickListener {} 在语义上是 Lambda 表达式,但在语法层面还是匿名内部类。这句话对不对?
由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor的,所以它也可以被删掉。
image.setOnClickListener({ view: View? ->
gotoPreview(view)
})
由于 Kotlin 支持类型推导,所以 View? 可以被删掉:
image.setOnClickListener({ view ->
gotoPreview(view)
})
当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it。
image.setOnClickListener({ it ->
gotoPreview(it)
})
Kotlin Lambda 的 it 是可以被省略的:
image.setOnClickListener({
gotoPreview(it)
})
当 Kotlin Lambda 作为函数的最后一个参数时,Lambda 可以被挪到外面:
image.setOnClickListener() {
gotoPreview(it)
}
当 Kotlin 只有一个 Lambda 作为函数参数时,() 可以被省略:
image.setOnClickListener {
gotoPreview(it)
}
按照这个流程,在 IDE 里多写几遍,你自然就会理解了。一定要写,看文章是记不住的。
将函数的参数类型和返回值类型抽象出来后,就得到了函数类型。
(View) -> Unit 就代表了参数类型是 View ,返回值类型为 Unit 的函数类型。
如果一个函数的参数或者返回值的类型是函数类型,那这个函数就是高阶函数。很明显,我们刚刚就写了一个高阶函数,只是它比较简单而已。
Lambda 就是函数的一种简写
fun View.setOnClickListener(l: (View) -> Unit) {
l.invoke(this)
}
使用:
val view = View(this@MainActivity)
view.setOnClickListener {
startActivity()
}
假设有多个参数怎么办
fun View.setOnClickListeners(test:Int,l: (View) -> Boolean) {
l.invoke(this)
}
// 多个高阶函数
fun View.setOnClickListeners(test: Int, l: (View) -> Boolean,l2: (View) -> Boolean) {
this.let { l.invoke(this) }
}
还是一样的道理,将函数类型的函数单独放在花括号里:
// 写法1
val view = View(this@MainActivity)
view.setOnClickListeners(0, {
println("-----------$it")
return@setOnClickListeners true
})
// 写法2
val view = View(this@MainActivity)
view.setOnClickListeners(0) {
println("-----------$it")
return@setOnClickListeners true
}
// 多个高阶函数
val view = View(this@MainActivity)
view.setOnClickListeners(0, {
println("-----------$it")
return@setOnClickListeners true
}, {
return@setOnClickListeners false
})
看图,大家知道kotlin中任何东西都是一个对象
那么图2中阴影部分的代码块其实就是一个对象,对象的引用其实就是图1中的 l1,那么要想图2的回调被执行,就需要图1中的l11去执行。即:
fun View.setOnClickListeners(l1: (View) -> Boolean, l2: View.() -> Boolean) {
l1(this)
// 或者
l1.invoke(this)
}
大家看到这里,会有疑问,为什么要传个this呢?是因为l1是函数类型参数,接收的是扩展对象参数,返回的是Boolean类型。而扩展对象参数View就是调用该扩展方法的调用者,这是在讲kotlin扩展函数中明确的。
也就是说view.setOnClickListeners中的view对象会传到扩展函数中的this,这个对象又作为函数类型参数的接收值回调到view.setOnClickListeners中去:
view.setOnClickListeners({ v ->
println("----111-------$v")
return@setOnClickListeners true
}
这个v就是回调回来的view对象,也可以这么写:
看到as的智能提示,这个对象用 it 表示,所以写 v 报错了,正确的是:
view.setOnClickListeners({
println("----111-------$it")
return@setOnClickListeners true
}
如果函数类型中接收参数是多个怎么办,比如:
这时候就不能简写lambda表达式了,会报错,也就没了 it
所以:it 用于 函数类型中: 函数只有一个参数 。 it表示 参数对象,并且回调返回
为什么要引出这个东西呢?
大家看上面的扩展函数中,l1后面还有个函数类型的参数,l2
我们在定义高阶扩展函数的时候 ,某个参数是函数类型,假如想要把 扩展对象传递给 这个函数类型 。可以通过 两种方式 定义 函数类型
1、f: (T) -> Unit , 函数类型的参数为 扩展对象类型。
2、f: T.() -> Unit ,函数类型为带接收者的函数类型,接收者和扩展对象一致
先不考虑这两者的区别
由上面可以知道:
1、{ }花括号里调用必须加(),()里调用直接调用对象引用,无需加()。
2、l1 是不带接受者的,也就是 l1 不被view对象持有,所以不能通过this调用 l1 ,l1要想执行,得自己去执行,而 l2是带接受者的,被view对象持有,而view对象又可以用this表示,即可以用this去调用 l2,也可以 l2 自己执行。
看到这大家可能疑问,那都自己执行自己不就行了吗,其实真正的区别不是体现在这里,而是体现在调用方的回调里:
我先把 l1的第二个参数去掉
所以:this 用于 带接受者的函数类型中,表示接受者,接收者和扩展对象一致,没有回调返回
虽然他们的对象地址都一样,但是描述的意义不一样。
如果将 l2 的返回值类型Boolean换成Unit,apply还可以简写:
如果接收者不和扩展对象一致,会怎么样呢
this就调用不了 l2
如果在此基础上,给 l2加个传入参数呢?
调用方:
这里怎么会同时有个 this 和 it呢,前面已经说过了,this代表了函数类型接受者,it
代表了唯一一个函数类型的传入参数,当然,也可以这么写:
结果显而易见: