kotlin手册

kotlin 手册

class类的使用

java中的类声明

☕️
public class MainActivity extends AppCompatActivity {
    @override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
}

对应kotlin的写法就是:

🏝️
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
}

有这么几点不同:

  1. Java中的public在kotlin中可以省略, kotlin的类默认是public的.
  2. 类的继承的写法, Java里用的是extends, 而在kotlin里使用:, 但其实 : 不仅可以表示继承某个类, 还可以表示Java中的implement 某个接口.
  3. kotlin把构造函数单独用了一个constructor关键字来和其他的fun做区分.
  4. java里面@override是注解的形式, kotlin里的override变成了关键字.
  5. kotlin里的类默认是final的, 也就是不能被其他类去继承,要想让这个类可以被继承, 需要加上open关键字, 下面这样的MainActivity就可以被继承了.
  6. kotlin里创建类实例的时候, 不需要new关键字.
  7. kotlin定义接口使用interface关键字.
🏝️
open class MainActivity : AppCompatActivity() {

}

在继承类的时候, 对被继承类后的()的理解.

🏝️
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
}

上面这种方式就相当于给MainActivity类声明了一个无参的构造方法, 是一种简单的写法, 它就等价于下面这样:

🏝️
//注意这里AppCompatActivity后面没有'()'
class MainActivity : AppCompatActivity {
    constructor() {
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
}
构造器Constructor

java里

public class User {
    int id;
    String name;
    
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

kotlin里

class User {
    val id: Int
    val name: String
    
    constructor(id: Int, name: String) {
        //上面这里没有public
        this.id = id
        this.name = name
    }
}

两点不同:

  1. java中的构造器和类同名, kotlin中使用constructor关键字表示.
  2. kotlin中构造器没有public修饰, 因为默认可见性就是公开的.
主构造器和次构造器的概念

写在类名后面的构造器叫做主构造器, 写在类中的构造器被称为次构造器.

class User constructor(name: String) {
    //下面的name就是上面构造器的入参name
    var name: String = name
}

主构造器有3个特点:

  1. constructor构造器移到了类名后面
  2. 类的成员变量name可以引用主构造器中的参数name的值
  3. kotlin中, 一个类最多只能有1个主构造器(也可以没有), 而次构造器的个数是没有限制的.

主构造器中的参数除了可以在类的成员变量中使用外, 还可以在init代码块中使用.


class User constructor(name: String) {
    var name: String
    init {
        this.name = name
    }
}

其中, init代码块是紧跟在主构造器之后执行的, 这是因为主构造器本身没有代码体, init代码块就充当了主构造器代码体的功能.
另外, 如果类中有主构造器, 那么其他的次构造器都需要通过this关键字调用主构造器, 可以直接调用或者是通过别的次构造器间接调用, 如果不调用, IDE就会报错:

class User constructor(var name: String) {
    constructor(name: String, id: Int) {
        //这里写就会报错, Primary constructor call expected.
    }   
}

当一个类同时有主构造器和次构造器的时候, 需要这样写:

class User constructor(var name: String) {
    //直接调用主构造器
    constructor(name: String, id: Int) : this(name) {
        ...
    }
    
    //通过上一个次构造器, 间接调用主构造器.
    constructor(name: String, id: Int, age: Int) : this(name, id) {
        ...
    }


}

通常情况下, 主构造器中的constructor关键字可以省略.

class User(name: String) {
    var name: String = name
}

还有一个特别的用法, 可以在主构造器里用var关键字来直接快速的给类声明出N个同名的成员变量.

class User(var name: String) {
    ...
}

等价于:

class User(name: String) {
    var name: String = name
}

如果在主构造的参数声明时加上var或者val, 就等价于在类中创建了该名称的属性(property), 并且初始值就是主构造器中的该参数的值.

init关键字, 用于初始化代码块

在java中

public class User {
    {
        //初始化代码块, 先于下面的构造方法执行
    }
    public User() {
    }


}

在kotlin中

class User {
    init {
        //初始化代码块, 先于下面的构造方法执行
    }
    
    constructor() {
        ...
    }

}


静态类, 静态方法和静态成员变量

就是使用object关键字

object Sample {
    val name = "a name"
}

它的意思很直接: 创建一个类, 并且创建一个这个类的对象, 这个就是object的意思: 对象.
在代码中如果要使用这个对象, 直接通过它的类名就可以访问:

Sample.name

这不就是单例么, 所以在kotlin中创建单例不用像java中那么复杂, 只需要把class换成object就可以了.
java中:

public class A {
    private static A sInstance;
    
    public static A getInstance() {
        if (sInstance == null) {
            sInstance = new A();
        }
        return sInstance;
    }

}

在kotlin中就可以很简单的这样写:

//class 换成了object
object A {
    val number: Int = 1
    fun method() {
        println("A.method()")
    }
}

如果想让上面的method()静态方法在java文件中调用, 那就在method()方法前加上注解, @JvmStatic

用object修饰的对象中的变量和函数都是静态的, 但有时候, 我们只想让类中的一部分函数和变量是静态的该怎么做呢.
这时候就可以使用companion object.

class A {
    companion object {
        var c: Int = 0
    }

}

也就是说java中的静态变量和静态方法的等价写法是: companion object 变量和函数.

top-level property / function 声明

其实就是把属性和函数的声明不写在class里面, 这个在kotlin里是准许的.

package com.hencoder.plus
// 属于package, 不在class/object内

fun topLevelFunction() {
}

上面这些工具方法就可以定义为顶层方法, 比如dp2dx()这样的方法.这样IDE就可以通过代码提示快速让开发人员找到合适的方法.

常量的声明 const 关键字
class Sample {
    companion object {
        const val CONST_NUMBER = 1
    }

}

const val CONST_SECOND_NUMBER = 2

就记住一点就行, kotlin中只有基本类型和String类型可以声明为常量.

is 和 as 关键字

is关键字等同于java中的instanceOf关键字.
as关键字用于把一个类强制转换为它的某个子类.

fun main() {
    var activity: Activity = NewActivity()
    (activity as NewActivity).action()
}

但上面这样做, 如果强转成了一个错误的类型, 就会抛出异常.
这时候我们可以使用 as? 来解决.

fun main() {
    var activity: Activity = NewActivity()
    //'(activity as? NewActivity)' 之后是一个可空类型的对象, 所以, 需要使用?.来调用.
    (activity as? NewActivity)?.action()
}

它的意思就是说如果强转成功就执行之后的调用, 如果强转不成功就不执行后面的调用.

by 关键字

在声明一个类的时候, 这个类可能要实现某个接口, 这时候在接口的右边写上by关键字.所起的功能就是把类对这个接口的实现, 委托给了指定的对象.
当你想让某个类实现某个接口, 但不想关心这个接口的大部分实现方法, 只想给它做一些功能扩展, 那么就用by关键字来个接口委托, 让这个接口的大部分实现方法由指定对象来进行插件式提供.

还有一种用法是对这个接口中的某个方法单独进行定制修改, 这样这个类的对象对这个接口的实现就以新定制的方法实现为准, 就不交给委托对象了, 也就是说委托对象对这个接口的某个实现就被废除了.

这里有2个典型的使用场景.
比如要实现一个用户列表的类, 它需要实现List接口中的方法, 例如get(), set(), remove()之类的. 但除了这些通用的方法外, 还需要新增加两个定制方法, 一个是查找高风险用户的列表, 另一个是按照年龄对列表进行排序.
这时候就可以用到接口委托的这种方法来处理.

//注意: by关键字的后面是一个对象.
class UserList(private val list: List<User>) : List<User> by list {
    fun highRiskUsers() : List<User> {...}
    fun sortWithAge() {...}
}

//在创建这个类对象的时候, 入参list就是by后面的list对象.

当然, 这就要求在构造UserList的时候, 传入一个实现了List<User>接口的对象进来.

例如, 下面这个demo code

// 创建接口
interface Base {
    fun print()
}

// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通过by关键字创建委托类
class Derived(b: Base) : Base by b

//使用的方法
fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print() //这里输出结果是10
}

在sdk中有不少这样的使用案例.


image.png
用data class 定义数据类

在定义类的时候, 加上data关键字, 这样就定义出一个数据类, 用于专门保存数据使用. 好处是它会自动为这个类实现toString(), equals(), hashCode()这些方法, 这样在直接打印这个类的对象的时候, 就不会只打印对象的地址, 而是根据类的成员变量的值, 打印这个对象的内容. 同时, 使用data class也可以让代码的可读性更好一些.

data class Person(val name: String = "Android") {
  var age: Int = 0
}
如何写一个新类, 继承自某个已经存在的类

例如, SqAlertDialog的构造方法如下:

  protected SqAlertDialog(Context context) {
      super(context, R.style.NoTitleDialog);
      mContext = context;
  }

那么写一个继承自这个类的声明就应该是下面这样的.

class UpdateReminderGuideDialog(context: Context) : SqAlertDialog(context) {

}
enum类的使用

这块内容还没有总结, 后面再补充. ahking17.

方法(函数)

函数的声明

以fun关键字开头.
返回值的类型写在函数和参数的后面.

fun cook(name: String) : Food {
  ...
}

如果没有返回值该怎么办? Java里是返回void, kotlin里是返回Unit, 并且可以省略.

函数的简化写法, 使用 = 连接返回值

fun area(width: Int, height: Int): Int {
    return width * height
}

这种只有一行代码的函数, 可以简化写成.

fun area(width: Int, height: Int): Int = width * height

以上是函数有返回值时的情况, 对于没有返回值的情况, 可以理解为返回值是Unit.

fun sayHi(name: String) {
  println("Hi " + name)
}

可以简化成下面这样:

fun sayHi(name: String) = println("Hi " + name)

函数的参数支持有默认值

fun sayHi(name: String = "world") = println("Hi " + name)

这就等价于用Java写的重载方法, 当调用sayHi函数时, 参数是可选的.

sayHi("kaixue.io")
sayHi() //使用了默认值 "world"

使用函数的命名参数来调用函数

fun sayHi(name: String = "world", age: Int) {
  ...
}

sayHi(age = 21)

这种显式地指定了参数的名字和参数值得方式, 就叫做命名参数.
kotlin中的每一个函数参数都可以作为命名参数.
再来看一个有非常多参数的函数的例子:

fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
    ...
}

当函数中有非常多的参数时, 调用该函数就会写成这样:

sayHi("world", 21, false, true, false)

上面这样可读性就比较差, 可以改成使用命名参数的调用方式:

sayHi(name = "world", age = 21, isStudent = false, isFat = true, isTall = false)

与之对应的概念就是我们使用使用的所谓位置参数, 但是要注意的是, 所有位置参数都应该放在第一个命名参数之前, 不然IDE就会报编译错误.

本地函数(嵌套函数), 在函数里再定义一个函数.

fun login(user: String, password: String, illegalStr: String) {
  fun validate(value: String) {
      if (value.isEmpty()) {
          throw IllegalArgumentException(illegalStr)
      }
      validate(user, illegalStr)
      validate(password, illegalStr)
  }

}

这里我们将共同的验证逻辑放进了嵌套函数validate中, 并且login函数之外的其他地方无法访问这个嵌套函数.

内置函数

let函数

使用场景: 方便对某个对象的多次连续调用做统一的判空处理

// 使用java的情况下
if (mVar != null) {
  mVar.function1();
  mVar.function2();
  mVar.function3();
}

// 使用kotlin (不使用let函数的情况下)
mVar?.function1()
mVar?.function2()
mVar?.function3()

// 使用kotlin (使用let函数)
// 方便了统一判空的处理 & 确定了mVar变量的作用域

mVar?.let {
    it.function1()
    it.function2()
    it.function3()
}

上面的it就代表mVar对象.
这样写的好处是当对一个对象连续调用它的几个方法的时候, 用这种写法可以省的连续去写mVar这个对象, 代码看起来更舒服些.

also函数

和let函数作用一样, 区别就是返回值不同.
let函数: 返回值 = 最后一行或是return的表达式, 作为返回值.
also函数: 返回值 = 传入的对象的本身.

//let函数
var result = mVar.let {
                        it.function1()
                        it.function2()
                        it.function3()
                        999
                  }
// 最终结果 = 返回999 给变量result

//also函数
var result = mVar.also {
                        it.function1()
                        it.function2()
                        it.function3()
                        999
                  }
// 最终结果 = 返回一个mVar对象给变量result
with函数

当需要连续调用同一个对象的多个方法或是属性时, 可以省去写对象名, 在 {} 中直接调用方法名或是属性即可, 这样就省的去写对象名了. 代码能更简洁些.

with(object) {
  // ...
}

//返回值 = 函数块的最后一行 或是return表达式.
// 此处要调用people的name和age属性
//kotlin用法
val people = People("carson", 25)
with(people) {
  println("my name is $name, I am $age years old")
}

//java用法
User people = new People("carson", 25);
String var1 = "my name is " + people.name + ", I am " + people.age + " years old";
System.out.println(var1);
run函数

同时结合let和with两个函数的作用.

  • 调用同一个对象的多个方法或属性时, 可以省去重复写对象名, 直接调用方法名或属性即可.
  • 统一做了判空处理.
object.run {
  ...
}
// 返回值 = 函数块的最后一行 或是 return表达式

使用示例

//此处要调用people的name和age属性, 且要判空
//kotlin
val people = People("carson", 25)
people?.run {
  println("my name is $name, I am $age years old")
}

//对比java 
User people = new People("carson", 25);
String var1 = "my name is " + people.name + ", I am " + people.age + " years old";
System.out.println(var1);
apply函数

与run函数的作用是一样的, 区别仅在于返回值.

  • run函数的最后一行的结果是返回值
  • apply函数返回传入的对象的本身.
    示例:
// run函数
val people = People("carson", 25)
val result = people?.run {
  println("my name is $name, I am $age years old")
  999
}
// 最终结果 = 返回999 给变量result.

// apply函数
val people = People("carson", 25)
val result = people?.apply{
  println("my name is $name, I am $age years old")
  999
}
// 最终结果 = 返回一个people对象给变量result.

变量, 属性

变量的声明
//java
View v;

//kotlin
var v: View

kotlin中使用var关键字来声明一个变量, 并且先写变量名后写它的类型, 中间用:分割.
但是如果这样写, IDE会报错.

class Sample {
    var v: View
    //这样写IDE会报如下错误.
    //Property must be initialized or be abstract
}

这是因为kotlin的变量是没有默认值的, 而java的类成员变量是有默认值的.
但如果简单的设置一个null值给kotlin变量也是不行的.

class Sample {
    var v: View = null
    //这样写IDE仍然会报错.
    //Null can not be a value of a non-null type View
}

这就涉及到kotlin的空安全设计.
kotlin的空安全设计, 就是通过IDE来检查代码, 避免调用空对象的方法, 进而引发NullPointerException.
在kotlin里面, 所有的变量默认都是不准许为空的.
如果要解除这个限制, 就需要在类型后面加一个?

class User {
    var name : String? = null
}

这种类型之后加?的写法, 在kotlin里叫做可空类型.
但直接对这个变量用.调用, 就会报下面的错误.

var view: View? = null
view.setBackgroundColor(Color.RED)
//这样写会报错, Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type View?
view?.setBackgroundColor(Color.RED)

?.的作用是告诉编译器, 在运行时检查这个对象, 如果为null就不调用了, 如果不为null才调用它的方法.
也可以使用 !!.的方式来调用.

view!!.setBackgroundColor(Color.RED)

意思是告诉编译器, 我保证这里的view一定是非空的, 编译器你不要帮我做检查了, 有什么后果我自己来承担, 不要在编译时报语法错误.

但在大多数情况下, 给一个变量初始化为null没有任何意思, 因为我们并不会去使用一个空对象, 也就是说我很确定我用的时候绝对不为空, 但第一时间我没法给它赋值.
kotlin给我们提供了一个选项: 延迟初始化.

lateinit var view: View

override fun onCreate(...) {
    ...
    view = findViewById(R.id.tvContent)
}

这个lateinit的意思是: 告诉编译器我没法第一时间就初始化, 但我肯定会在使用它之前完成初始化的. 它的作用就是让IDE不要对这个变量检查初始化和报错. 换句话说, 加了这个lateinit关键字, 这个变量的初始化就全靠你自己了, 编译器不帮你检查了.

变量的类型推断

如果你在声明的时候就赋值, 那不写变量类型也行.

var name: String = "Mike"
var name = "Mike"
val 和var

var是variable的缩写, val是value的缩写.
val 和 java 中的final类似.

val size = 10
属性委托 by 和 by lazy
by lazy 的使用

简单说, 就是实现属性的懒加载, 也就是说只有这个属性被使用的时候才会计算这个属性的值, 避免在初始化的时候就去计算属性值, 这样可以提高一些性能, 还有一个特性是对这个属性的多次访问操作, 只会在第一次访问的时候去计算属性的值. 后面再访问这个属性的时候, 就直接使用之前计算好的值就可以了, 这样也可以提高一些性能.

// play.kotlinlang.org 是个好网站
val testValue: String by lazy {
    println("computed! ---- 只调用一次.")
    "Hello"
}

fun main() {
      val name = "world"
      println(name)

      println(testValue)
      println(testValue)
      println(testValue)

}

上次访问了3次testValue的值, 但实际上, 只在第一次访问的时候计算一次这个属性的值.
输出结果是:

world
computed! ---- 只调用一次.
Hello
Hello
Hello

说白了, 使用了by lazy关键字的变量, 对它值的计算就只算一次就可以了, 之后这个值就被缓存起来了, 不会再重复计算, 可以提高些代码的运行效率.

by 的使用

数组和集合

数组
val strs: Array<String> = arrayOf("a", "b", "c")
println(strs[0])
strs[1] = "B"

这里有个比较偏的概念, 叫做协变(covariance)特性. 其实指的就是子类数组对象不能赋值给父类的数组变量.

val strs: Array<String> = arrayOf("a", "b", "c")
val anys: Array<Any> = strs //compile-error: Type mismatch.

Any 就是和java中的Object概念一样, 是任何类的基类.

集合

kotlin和java一样有三种集合类型: List, Set 和 Map.

List
// java 中
List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c");
//kotlin
val strList = listOf("a", "b", "c")

而且 kotlin中的List多了一个特性: 支持covariant (协变). 也就是说, 可以把子类的List赋值给父类的List变量.

val strs: List<String> = listOf("a", "b", "c")
val anys: List<Any> = strs // success

而这在 Java 中是会报错的:

List<String> strList = new ArrayList<>();
List<Object> objList = strList; // compile error: incompatible types

在kotlin play中做了验证: kotlin中数组Array 是不支持协变的, 但集合类List是支持协变的.

Set
//java 中
Set<String> strSet = new HashSet<>();
strSet.add("a");
strSet.add("b");
strSet.add("c");
//kotlin中
val strSet = setOf("a", "b", "c")

和List 类似, Set 同样具备 covariant (协变) 特性.

Map

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容