1.类
申明一个类:
class Person{}
如果这个类没有方法,可以省略:
class Person
;
都不用写,语法很简洁
2.构造函数
如果类中有一些属性,我们也可以这样申明一个类
class Person(var name: String,var age:Int)
这里的属性申明时,可以是var
也可以是val
,这里的(var name: String,var age:Int)
,叫做主构造函数,主构造其实就是为了我们像这样申明一个类,我们例子中省略了constructor
,那如果我们要对这些参数做初始化呢?主构造里面是不支持任何代码逻辑的,只能通过初始代码块
来完成初始化的操作:
class Person (var name: String,var age:Int){
var nameAge:String? = null
init {
nameAge = name+age;
}
}
当我们不想用这种主构造的时候,或者我们有多个构造函数时,就得使用到次构造函数,使用次构造时,必须要把参数传递给主构造
class Person (var name: String,var age:Int){
var nameAge:String? = null
init {
nameAge = name+age;
}
constructor(name: String, age: Int,from:String):this(name,age){
nameAge = nameAge()
}
fun nameAge() = "nameAge"
}
次构造里面可以直接调用其他函数,那主构造,初始化代码块和次构造执行的顺序是怎么样的?我们反编译下就明白了:
public final class Person {
@Nullable
private String nameAge;
@NotNull
private String name;
private int age;
public Person(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
this.nameAge = this.name + this.age;
}
public Person(@NotNull String name, int age, @NotNull String from) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(from, "from");
this(name, age);
this.nameAge = this.nameAge();
}
}
撇开get,set
我省略掉了,最终发编出来的代码,初始化代码块是包含在构造函数当中的,所以,当我们调用一个次构造函数,最终执行顺序是主构造->初始化代码块->次构造函数
.
3.创建对象
kotlin里面省去了new
关键字,创建对象,简单粗暴var person = Person("Pantheon",20)
4.继承
在第一章的时候简单介绍过一点kotlin的继承,kotlin里面默认创建出来的类都是不可变的,也就是都是由final
修饰的,所以一个类如果需要被继承,需要加上open
关键字,而接口则天生需要被实现,所以不需要如此:
interface My{
fun doSth()
}
open class MyClass(val sth: String){
fun doSth():String = "do${sth}"
}
class MyImpl:My{
override fun doSth() {
println("doSth")
}
}
class MyClassImpl(val mySth: String): MyClass(mySth) {
}
class MyClassConstruct:MyClass{
constructor():super("other thing")
}
而继承的语法在kotlin中也发生了改变,继承类或者接口都是用:
来表示继承关系,对于构造函数的处理的思想则和java类似,只不过可以在主构造中传递参数.
在上述例子中,也演示了如何实现[覆盖]一个方法.也就是对于实现[覆盖]的方法,需要加上override
关键字.
5.属性
kotlin因为其简洁的语法特性,导致原本很多比较简单的概念变的复杂起来,比如我们在构造方法中,提到的,主构造和次构造,属性这一个本来不复杂的内容,也因为它简洁的语法而变得复杂.
我们可以直接把属性写在构造方法中,可以是主构造也可以是次构造,最终编译器都会帮助我们生成属性字段,并且生成set,get
方法:
class Person(var name: String,var age:Int)
我们在使用时,可以像访问public
属性一样访问这些字段
val person = Person("xxxx", 10)
println(person.name)
如果我们需要在body体内自己定义属性:
class Person{
var name:String? = null;
var age:Int = 18;
}
如果创建的时候就知道值是多少,可以用不可空变量申明,如果不知道值是多少,那就用可空变量来什么.
kotlin的set,get
方法比较奇葩,语法类似c#
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
举个例子:
class Person{
private var name:String? = null
get() = name
set(value) {
field = parse(value)
}
var age:Int = 18;
}
这个field
叫做back field
,其实就是指代当前的这个属性,如果用下面这种方式赋值,会陷入无止境的递归
class Person{
private var name:String?
get() = name
set(value) {
name = parse(value)
}
var age:Int = 18;
}
fun main(vararg args:String) {
val person = Person()
person.name = "pppp"
println(person.name)
}
反编译下:
public final class Person {
private int age = 18;
@Nullable
public final String getName() {
return this.getName();
}
public final void setName(@Nullable String value) {
this.setName(value);
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
}
可以看到最终get
方法生成的方法是去掉自己,而set
也是掉自己,这个时候我们如果想给属性赋值,只能用back filed
,这个和c#
的思想是一致的.
还有一个隐藏属性的backing property
感觉用处不大,不介绍了.
我们再刚刚介绍说,如果一个字段是可空的,需要用可空类型来申明,并且赋上一个默认null
值,那我们想一下这个常见,在IOC
中,我们注入的某一个属性,在申明上,它没有默认值,是可空类型,但是使用上,我们知道,一般不会空的,如果我们用可空类型来申明,那得写一堆?
,非常不美观,这里就出现了lateinit
关键字:
class Person{
lateinit var name:String
}
用lateinit
申明的属性,可以是非空类型,且没有默认值.但是我们想一下,如果我创建完了,不复制,那不一样会报空指针?
fun main(vararg args:String) {
val person = Person()
println(person.name)
}
报错如下:
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized
我们可以通过如下方式,判断字段是否初始化
val person = Person()
if (person::name.isInitialized1){
println("-----")
}
lateinit
其实在生成字节码时,只在get
方法上加了个是不是空值的判断,是空,抛出异常,不是则返回值.
6.代理模式
设计模式中有一个很重要的设计模式,叫做代理设计模式,代理类和实际类都实现同一个接口,访问代理对象的方法时,再去访问实际对象的方法,在kotlin中,用一个by
关键字实现了这个逻辑:
interface DoSth{
fun doSth();
}
class DoSthImpl:DoSth {
override fun doSth() {
println("DoSthImpl")
}
}
class DoSthProxy(sth: DoSth):DoSth by sth ;
fun main() {
var proxy = DoSthProxy(DoSthImpl())
proxy.doSth()
}
代理对象持有实际对象的引用,通过主构造函数传入,就可以很快速的出代理模式,代理类可以重写代理方法,但是没有办法获取到实际对象的引用(虽然传入了)
class DoSthProxy(sth: DoSth):DoSth by sth {
override fun doSth() {
println("proxy")
}
}
代理类感觉用处不算大,实际使用中,以改变字节码的代理技术使用居多,而且代理对象还不能调用实际对象的方法,要么就重写,要么就只是做代理,个人感觉功能很弱.
7.代理属性
代理属性说的是这么一个功能,假如类里面有个属性,我们再访问它或者改变它的值时,可以通过代理来做一些事情,比如lazy
赋值,当我们首次访问时,通过lazy
后面的表达式来赋值
class Lazy{
val name:String by lazy {
println("computed!")
calName()
}
fun calName():String = "name"
}
lazy
属性需要申明为不可变类型val
,上面这段代码name
属性的初始值则由calName
来提供,接近于java的init
方法,但是比init
方法要复杂.by
后面也可以跟个对象,代表该属性的初始值由该对象的某个方法来提供,看下这段代码:
class DelegateDemo{
var delegate1:MyDelegate by Delegate()
var delegate2:MyDelegate by Delegate()
}
class MyDelegate
class Delegate{
operator fun getValue(delegateDemo: DelegateDemo, property: KProperty<*>): MyDelegate {
println("Delegate getValue:${property.name}")
return MyDelegate()
}
operator fun setValue(delegateDemo: DelegateDemo, property: KProperty<*>, myDelegate: MyDelegate) {
println("Delegate setValue:${property.name}")
}
}
fun main() {
var demo = DelegateDemo();
demo.delegate1
demo.delegate2
demo.delegate2 = MyDelegate()
}
运行的结果如下:
Delegate getValue:delegate1
Delegate getValue:delegate2
Delegate setValue:delegate2
也就是说,代理属性如果由某个对象提供,其实就是由这个对象提供了该属性的get,set
方法,也就是该类需要定义两个方法,getValue和setValue
.
代理属性也可以将自己的值代理给别的属性,也就是自己的值,由别的属性来提供:
var topLevelInt: Int = 10
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
因为by
后面跟的是表达式,所以这里是获取的函数引用,那哪些属性可以被代理呢:
- 全局变量,或者是同类的其他属性
- 其他类的其他属性
还有一个功能是观察属性
,比如属性中的值发生了变化,可以获取到值得变化:
class Lazy{
var name:String by Delegates.observable("name"){
property, oldValue, newValue ->
println("${property.name} has changed,before:${oldValue},now:${newValue}")
}
}
fun main() {
val lazy = Lazy();
lazy.name = "Pan"
}
Delegates.observable
有两个参数,第一个是初始值,第二个是个lambda表达式,执行的结果:
name has changed,before:name,now:Pan
代理属性的功能还远远不止这么些,还有map代理属性,本地代理属性等等功能,似乎这些功能在实际编码中用的不多,不做过多介绍了,有兴趣可以看看官网的介绍.
8.SAM
SAM这个东西有点类似于java的lambada
定义的functional interface
,定义如下:
fun interface Even{
fun invoke(num:Int):Boolean
}
fun main() {
var even:Even = Even { it % 2 == 0 }
even.invoke(10)
}
其实功能和函数类型有点冲突,而且还得特地定义出一个接口出来,感觉用处不是很大.
9.Data class
data class类似于java14中的record class
,专门用于承载数据,和普通的class相比有以下的几个区别:
- 自动重写
equals
和hashcode
方法 - 重写了
toString
方法,只输出主构造的属性 - 提供了
component1
-componentN
的方法,用于获取属性,获取的顺序和属性申明的顺序一致,这些方法不能被直接调用,如果有回忆之前讲函数多返回时,就用的data class,kotlin就是根据component1
-componentN
这些方法来推断出返回的值和类型. - 提供了一个
copy
方法 - 主构造必须含有一个参数,并且必须标明是
val
还是var
类型,这在普通的class里面是不必要的 - 不能由这些关键字修饰
abstract, open, sealed, or inner.
申明一个data class,并创建实例
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
val person = Person("Pantheon").apply { age = 10 }
val copy = person.copy(name = "Lucy")
println(copy)
}
data class的属性可以是在主构造中也可以是在body体内,输出结果
Person(name=Lucy)
10.sealed class
密封类的特性是这样,被sealed
修饰的类或者接口,在最早的版本中,不能在该类申明的包以外被继承和实现,比如我引用了一个第三方类库的包中包含sealed
class,我想继承它,是不行的,所以才有了密封类的说法,它除了不让其他人实现外,还有一个比较实用的功能就是类似于java中enum
的使用,但是比enum
功能强大:
sealed class Week(val day: String) {
class Monday : Week("monday");
object Tuesday : Week("tuesday");
object Thursday : Week("thursday") {
override fun today() {
println("hah...today is $day")
}
}
class Friday : Week("friday") {
fun friday() {
println("saturday is coming.....")
}
}
open fun today() {
println("today is $day")
}
}
fun main() {
Week.Monday().today()
Week.Tuesday.today()
Week.Thursday.today()
Week.Friday().friday()
}
-------------------
today is monday
today is tuesday
hah...today is thursday
saturday is coming.....
从上面代码可以看到,密封类里的对象和enum
区别在于,enum
对象永远只有一个,而密封类里面可以是class
,也可以是object
,是可以创建出多个对象,而继承密封类的实现,也可以自己定义的方法,通过 Week.Friday()
方法调用返回的类型是Friday
类型,而不是Week
,所以可以调用friday()
这个方法,这和enum
是最大的区别,因为enum
获取的实例就是enum
定义出来的类型.
密封类还可以和when
和is
配合使用,接近于java中对enum
的Switch case
判断:
fun whatDay(week: Week) = when(week){
is Week.Monday -> println("Monday")
is Week.Friday -> println("Friday")
Week.Thursday -> println("Thursday")
Week.Tuesday -> println("Tuesday")
}
fun main() {
whatDay(Week.Thursday)
}
11.泛型(in,out,where)
要说kotlin的泛型,不得不先说java的泛型,java泛型因为在运行的时候会丢失泛型,所以为了防止发生类型安全的问题,java的泛型是不允许协变的
List<String> lists = new ArrayList<>();
//编译错误 泛型不能协变
List<Object> objects = lists;
假如我们就是有需要这样的赋值的操作呢?这个时候就要用到两个关键字,一个extends
,一个super
List<? super Object> lists = new ArrayList<>();
//编译通过
List<Object> objects = lists;
java里面把对泛型变量的操作划分成了两块,一个是读操作,一个是写操作,使用extends
的泛型,只能读,不能写,而super
关键字只能写,不能读
List<? extends Object> objects = new ArrayList<>();
//编译报错,extend只能用在读的场景
objects.add("1111");
//编译正确
Object o = objects.get(0);
List<? super Fruit> objects = new ArrayList<>();
objects.add(new Apple());
//error,类型只能是object类型
Fruit object = objects.get(0);
PECS原则
如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
这两个关键字就刚刚对应kotlin里面的in,out
关键字,in
代表添加数据,也就是对应的是super
关键字,而out
则代表获取数据,对应的是extend
关键字
open class Fruit;
class Apple:Fruit();
var list:ArrayList<in Fruit> = ArrayList()
//error
val fruit:Fruit = list.get(0)
//ok
list.add(Apple())
var list:ArrayList<out Fruit> = ArrayList()
//ok
val fruit:Fruit = list.get(0)
//error
list.add(Apple())
当泛型类型有多个时,需要有到where
关键字
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
12.object 表达式
kotlin中,如果对于一些只需要创建一次的对象,或者对于一些不需要复用的实现,可以用object
关键字来申明和创建该对象,比如:
fun main() {
var pantheon = object {
var name:String = "Pantheon";
fun sayHello(){
println("hello,$name")
}
}
pantheon.sayHello()
}
这个例子可以让我们看到object的背后运作机制,它不仅仅是创建了一个匿名类,而且还用这个匿名类直接给我们创建出来了一个对象.
object也可以去实现某些特定的接口:
var pantheon = object:ObjectInterface {
var name:String = "Pantheon";
fun sayHello(){
println("hello,$name")
}
override fun doSth() {
println("doSth")
}
}
pantheon.sayHello()
pantheon.doSth()
13. companion object
kotlin中没有static
关键字,class
的static 方法和static属性由companion object
来提供,比如Java中
public class StaticClass {
public static final String name = "Pantheon";
public static void sayHello(){
System.out.println("Hello "+name);
}
}
它的写法在kotlin中就等价于:
class StaticClass{
companion object{
val name = "Pantheon"
fun sayHello(){
println("Hello,$name")
}
}
}
之前在拓展函数中也讲过,类的静态拓展方法是需要加在companion object
,在receiver申请处需要声明成class.companion
.