kotlin 手册
class类的使用
java中的类声明
☕️
public class MainActivity extends AppCompatActivity {
@override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
}
对应kotlin的写法就是:
🏝️
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
}
}
有这么几点不同:
- Java中的public在kotlin中可以省略, kotlin的类默认是public的.
- 类的继承的写法, Java里用的是extends, 而在kotlin里使用:, 但其实 : 不仅可以表示继承某个类, 还可以表示Java中的implement 某个接口.
- kotlin把构造函数单独用了一个constructor关键字来和其他的fun做区分.
- java里面@override是注解的形式, kotlin里的override变成了关键字.
- kotlin里的类默认是final的, 也就是不能被其他类去继承,要想让这个类可以被继承, 需要加上open关键字, 下面这样的MainActivity就可以被继承了.
- kotlin里创建类实例的时候, 不需要new关键字.
- 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
}
}
两点不同:
- java中的构造器和类同名, kotlin中使用constructor关键字表示.
- kotlin中构造器没有public修饰, 因为默认可见性就是公开的.
主构造器和次构造器的概念
写在类名后面的构造器叫做主构造器, 写在类中的构造器被称为次构造器.
class User constructor(name: String) {
//下面的name就是上面构造器的入参name
var name: String = name
}
主构造器有3个特点:
- constructor构造器移到了类名后面
- 类的成员变量name可以引用主构造器中的参数name的值
- 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中有不少这样的使用案例.
用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