一、类的构造器
类构造器的基本写法:
//var name:String 这种方式在定义参数的同时把它的类属性定义了,这个参数是类全局可见
//age:Int 只能是构造器内可见(包括init块,属性初始化)
class Person(var name:String,age:Int)
//init块 类似于主构造器的方法体
class Person(var name:String,age:Int){
var age:Int
init{
this.age = age
}
}
复制代码
类的副构造器,观察下面代码:
abstract class Animal
//Person后面定义的就是主构造器
class Person(var name:String,var age:Int):Animal(){
constructor(age:Int):this("Unknown",age){ //这里调用主构造器确保了构造路径是唯一的
//定义了主构造器后在类的内部再定义的构造器就被称为副构造器
}
}
复制代码
主构造器默认参数:
abstract class Animal
class Person
@JvmOverloads //此注解可以使主构造器默认参数在java代码中可以以重载的形式调用
constructor(var name:String = "Unknown",var age:Int)
:Animal(){
}
//这种写法是Kotlin中比较推荐的写法
复制代码
不定义主构造器写法:
class Person:Animal{
var name:String
var age:Int
constructor(name:String,age:Int){
this.name = name
this.age = age
}
}
复制代码
这种写法和Java很像,但Kotlin不推荐这样写,因为会可能有多个构造器造成多条构造路径而增加复杂度,但为了兼容Java特性,Kotlin还是允许这样的写法。
二、类与成员的可见性类型介绍
public:和Java相同,是公开可见,可修饰类、成员以及顶级声明。
internal:模块内可见,可修饰类、成员以及顶级声明。
protected:类内及子类可见,修饰成员。
private:类或文件内可见,可修饰类、成员以及顶级声明。
三、类属性的延迟初始化
延迟初始化的原因:
1、类属性必须要在构造时初始化;
2、某些成员只有在类构造之后才会被初始化使用,如果我们不延迟初始化,那我们只能把属性声明为空类型而声明为null,显然这样做会使代码结构非常不好,至少很多情况我们还需要不断判空。
lateinit关键字
1、lateinit会让编译器忽略变量的初始化,不支持Int等基本类型;
2、开发者必须能够在完全确定变量值的生命周期下使用lateinit;
3、不要在复杂的逻辑中使用lateinit,它会让代码变得更加脆弱。
使用案例:在Android开发我们往往先声明一个控件变量,然后在onCreate中初始化获得
//这里若不使用lateinit则需要把textView声明为可控类型,后调用必须要加?符号判空
private lateinit var textView:TextView
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.act_main)
textView = findViewById(R.id.textView)
}
复制代码
lazy方式延迟初始化
//只有在textView首次被访问时才会执行
private val textView by lazy {
findViewById<TextView>(R.id.textView)
}
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.act_main)
textView.text = "Hello Kotlin"
}
复制代码
这种方式初始化和声明内聚,无需声明可空类型,是延迟初始化最被推崇的方式。
四、代理Delegate
代理是什么呢?
代理就是A代替B去处理C事件,口语化一点就是我代替你处理它,主体是我、你、它。
我们先来看看接口代理:接口代理就是对象X代替当前类A实现接口B的方法。观察下面代码:
//声明一个接口Api
interface Api {
fun a()
fun b()
fun c()
}
class ApiWrapper(val api:Api)
:Api by api{ //Api by api 就是对象api代替类ApiWrapper实现Api
override fun c() { //这样就不需要实现a()和b()
println("Hello Kotlin")
api.c() //对于对象api的唯一要求就是实现被代理的接口
}
}
复制代码
属性代理
属性代理只需要实现getValue和setValue方法即可。
下面我们看看通过lazy实现的属性代理:
fun main() {
val person = Person("zhang san")
println(person.firstName)
}
class Person(val name:String){
val firstName by lazy { name.split(" ")[0] }
}
打印结果:
zhang
复制代码
通过lazy能够代理属性是因为lazy本来就是一个函数,返回的是一个lazy对象:
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
复制代码
lazy对象本身就是一个代理,它还实现了getValue方法,所以接口Lazy的实例代理了对象Person的实例的属性firstName的getter:
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
复制代码
下面我们再举个一般情况的例子方便理解:
//实现一个代理类,实现getter和setter方法
class Delegate{
//thisRef 是属性所处在的对象 property就是属性
operator fun getValue(thisRef: Any?, property: KProperty<*>):String{ //String是属性的类型
return "getValue ${property.name}"
}
//当确定了代理属性类 thisRef: Any?可以直接换成 person:Perosn
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("他是${value}公司的")
}
}
class Person{
var company:String by Delegate()
}
fun main() {
val person = Person()
person.company = "Google"
println(person.company)
}
打印结果:
他是Google公司的
getValue company
复制代码
这里先调用person.company = "Google"赋值了,所以先调用了setValue方法。
observable属性代理
观察下面代码:
class StateManager{
var state:Int by Delegates.observable(0){ kProperty: KProperty<*>, oldValue: Int, newValue: Int ->
println("State change from $oldValue to $newValue")
}
}
fun main() {
val stateManager = StateManager()
stateManager.state = 9
}
打印结果:
State change from 0 to 9
复制代码
Delegates.observable(0)实际上市创建了另外一个对象,这个对象的类型是ObservableProperty<V>
,并通过实现ReadWriteProperty<Any?, T>
接口获得getValue和setValue方法,每次设置属性的时候就会去执行lamba表达式的内容,获取到属性是哪一个以及前后改变的值。
五、单例
Kotlin单例的实现使用object
关键字
object Singleton{
}
复制代码
定义单例时,类加载就同时实例化对象了,所以Singleton既是类名也是对象名,object成员的访问和Java的基本无异:
object Singleton{
var a:Int = 2
//静态成员修饰
@JvmStatic var x:Int = 2
//不生成setter和getter方法
@JvmField var x:Int = 2
fun b(){}
}
fun main() {
Singleton.a
Singleton.b()
}
复制代码
对于Kotlin中生成静态成员直接使用@JvmStatic
注解,这个对于Kotlin没有任何影响,主要是Java使用时object的成员直接按照Java静态成员生成字节码,对Kotlin内部使用无任何影响,Java调用object的成员可直接视同调用静态成员一般。这里要注意是@JvmStatic和 @JvmField这两个注解只能在Java平台去使用。
普通类的静态成员 companion object
companion object
是在类内部的伴生对象,与该普通类同名的object
class Foo{
companion object{
//生成的静态属性变量
@JvmField var a = 1
@JvmStatic fun xFun(){}
}
}
复制代码
该类和方法的写法和下面Java的实现等价
public class Foo {
public static void xFun(){}
}
复制代码
object不能定义构造器,但是可以实现init块、类继承和接口。
六、内部类
Kotlin中静态内部类和非静态内部类和Java中的声明不太一样,Kotlin中默认创建的内部类是静态的,使用inner关键字修饰的是非静态内部类
class Outer{
//默认不使用任何关键字是静态内部类
class StaticInner{}
//添加inner修饰后变为非静态内部类
inner class Inner{}
}
复制代码
内部类的实例化
fun main(){
//非静态 内部类实例化 需要先获取外部类的的实例
val inner = Outer().Inner();
//静态内部类实例化
val staticInner = Outer.StaticInner()
}
复制代码
内部object
class OuterObject{
//内部object 不存在非静态的情况,故不可用inner修饰,因为object一旦给定义好就实例化了
object StaticInnerObject{}
}
fun main(){
//内部object
val staticInnerObject = OuterObject.StaticInnerObject
}
复制代码
匿名内部类: 匿名内部类顾名思义就是没有名字的内部类,一般的格式写法是:object:类或者接口,实际Android开发中,我们在监听控件点击世界是的点击回调使用的就是匿名内部类,这种情况我们一般不会声明一个类创建一个对象。观察下面代码:
textView.setOnClickListener(object: View.OnClickListener{
override fun onClick(p0: View?) {
TODO("Not yet implemented")
}
})
复制代码
匿名内部类的一般形式如下:
object:Runnable,Cloneable{ //匿名内部类可以继承父类或者实现多个接口
override fun run() {
}
}
复制代码
这里要注意的是如果匿名内部类引用了外部对象,隐式持有外部类对象,要考虑会不会造成内存泄漏的问题,如果匿名内部类定义在静态方法、静态内部类或者顶级函数里面,不用考虑内存泄漏的问题。
七、data class数据类
数据类,顾名思义就是设计用来存储数据用的,定义数据类只需要在class前面加上data关键字,这里需要注意的是data class
的主构造函数必须至少有一个参数且所有参数要标记为var
或者val
,另外数据类不能是抽象的,开放的、或者是内部的。下面看如何构造一个data class
data class Book(
val id: Long,
val bookName: String,
val author: Person
)
data class Person(val name: String, val age: Int)
复制代码
这里看到Book的结构和我们定义JavaBean结构非常类似:
public class Book {
public long id;
public String name;
public Person person;
public static class Person{
public String name;
public int age;
}
}
复制代码
但两者并不是等价的,就是说Kotlin的data class不能直接当成JavaBean来用。这是为什么呢?
原因是定义在Book主构造器中的属性又被称为component,所有的属性都是基于component来实现,每一个属性都会有一个对应的componentN()方法,(N就是属性依次对应的1、2、3、4...这里可以通过Android Studio菜单栏Tools中Kotlin选项中的show Kotlin Bytecode查看字节码可以看到有component1()、component2()和component3(),这些方法都是编译器帮我们生成的,实际上它还自动生成了 equals/hashCode/toString/copy
等方法),而component是不可以自定义Getter或者Setter的,所以也就不能直接当JavaBean使用了。那么这个componentN()方法的作用是什么的?
componentN()的作用是用于解构的,解构的意思就是把当前这个数据类对象解构成几个变量,解构语法观察下面代码:
fun main(){
val book = Book(1234,"Kotlin入门与精通", Person("JetBrains",25))
//假设我只想要1、3两个位置的变量只需要改成 val(bookId,_,person) = book
val(bookId,bookName) = book
println("编号${bookId}的bookName是$bookName")
}
打印结果:
编号1234的bookName是Kotlin入门与精通
复制代码
val(bookId,bookName) = book
就是解构的语法,它解构的同时声明了两个变量,依次把component1()赋给了bookId,component2()赋给了bookName。
数据类的继承关系是只能继承别人,不能被继承,如果要实现被继承以及作为JavaBean这种形式使用,需要借助编译器的插件来实现数据类的无参构造方法以及去掉编译后的final关键字。
NoArg和AllOpen插件
NoArg插件解决的是Kotlin转换成java没有无参构造方法的问题,这个无参构造方法时编译生成的,编译前无法在代码里访问到的。
AllOpen插件解决的是把Kotlin代码转换成java后final去掉的问题,这样可以让数据类被继承实现它的getter/setter方法,从而实现一些所需的业务逻辑。
NoArg和AllOpen插件的使用:
添加依赖,Android项目下的build.gradle:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.60'
id 'org.jetbrains.kotlin.plugin.allopen' version '1.3.60'
}
noArg {
//true 会执行init块代码 默认为false
invokeInitializers = true
annotations "com.qisan.kotlinstu.Poko"
}
allOpen {
annotations "com.qisan.kotlinstu.Poko"
}
复制代码
定义一个注解:
package com.qisan.kotlinstu
/**
* Created by QiSan
* package com.qisan.kotlinstu
*/
annotation class Poko
复制代码
对数据类添加注解:
@Poko
data class Book(
val id: Long,
val bookName: String,
val author: Person
) {
init {
println("我是数据类Book中的init")
}
}
@Poko
class Person(val name: String, val age: Int)
fun main() {
//通过反射获取都Book的实例
val book = Book::class.java.newInstance()
}
打印结果:
我是数据类Book中的init
复制代码
通过查看运行后生成的字节码可以看到部分final修饰去掉了,compontentN()和copy方法依旧是final修饰,但是这个时候的数据类已经可以被继承:
public class Book {
private final long id;
@NotNull
private final String bookName;
@NotNull
private final Person author;
public long getId() {
return this.id;
}
@NotNull
public String getBookName() {
return this.bookName;
}
@NotNull
public Person getAuthor() {
return this.author;
}
public Book(long id, @NotNull String bookName, @NotNull Person author) {
Intrinsics.checkNotNullParameter(bookName, "bookName");
Intrinsics.checkNotNullParameter(author, "author");
super();
this.id = id;
this.bookName = bookName;
this.author = author;
String var5 = "我是数据类Book中的init";
System.out.println(var5);
}
}
复制代码
八、枚举类
Kotlin中枚举类的声明:
enum class State{
idle,Busy
}
fun main(){
//和Java不一样的是直接通过属性访问,Java是方法,其他无异
println("枚举名字${State.idle.name}")
println("枚举序号${State.Busy.ordinal}")
}
打印结果:
枚举名字idle
枚举序号1
复制代码
枚举类定义构造器:
//定义构造器后,每个枚举的实例都需要调用构造器传一个参数进去
enum class State(val id:Int){
idle(0),Busy(1)
}
复制代码
枚举类实现接口:
enum class State : Runnable {
idle, Busy;
override fun run() {
TODO("Not yet implemented")
}
}
复制代码
也可以每个枚举实现接口方法执行不同操作:
enum class State : Runnable {
idle{
override fun run() {
TODO("Not yet implemented")
}
},Busy{
override fun run() {
TODO("Not yet implemented")
}
}
}
fun main() {
//调用实现的方法
State.idle.run()
}
复制代码
需要注意的是枚举的父类是Enum,所以不能继承其他类。
枚举类的条件分支:
val state:State = State.idle
val value = when(state){
State.idle -> {0}
State.Busy -> {1}
}
复制代码
枚举的区间:
enum class Color{
White,Red,Green,Blue,Yellow,Black
}
fun main() {
val colorRange = Color.White ..Color.Yellow
val colorValue = Color.Green
println("Green in colorRange:${colorValue in colorRange}")
}
打印结果:
Green in colorRange:true
复制代码
九、密封类
密封类的概念:
密封类是一种特殊的抽象类,它首先是一个抽象类,其次才是密封类,且它的子类需定义在与自身相同的文件中,密封类的子类个数是有限的,在某种意义上,它们是枚举类的扩展。要声明一个密封类,需要在类名使用sealed修饰符修饰。
需要注意的是:sealed不能修饰 interface ,abstract class(会报 warning,但是不会出现编译错误)
观察以下代码:
//声明一个密封类
sealed class Expr
//定义了三种状态 Const Sum NotANumber
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
//这里就是类似Java中的switch...case语句
fun eval(expr: Expr): Double = when (expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
fun main() {
val c1 = Const(2.0)
val c2 = Const(5.0)
println("c1 + c2 = ${eval(Sum(c1,c2))}")
}
打印结果:
c1 + c2 = 7.0
复制代码
十、内联类
inline
关键字所修饰的类为内联类。
内联类的概念:
内联类是对某一个类的包装,类似于Java装箱类型的一种,编译器会尽可能使用被包装的类型进行优化。如何优化呢?有时候,我们定义一个类,里面只有一个属性和操作该属性的方法,那我们就可以把它定义成内联类,编译后就可以生成静态方法,通过类名点方法就可以调用,不用创建类实例,这样就不会增加对象存储到JVM堆上,而存储和使用对象实例都会有性能损失,虽然单个来算很少,但是项目叠加起来还是可能会对代码运行质量带来不少的影响,所以内联类的作用就是节省类创建对象的开销。
内联类必须含有唯⼀的⼀个属性在主构造函数中初始化,属性需要使用val修饰。在运⾏时,将使⽤这个唯⼀属性来表⽰内联类的实例。内联类可以实现接口,但不能继承父类也不能被继承。
内联类的编译优化举例:
//定义一个内联类
inline class PlayerState(val state: String) {
fun setPlayState() = when (state) {
"stop" -> {
println("play stop")
0
}
"prepare" -> {
println("play prepare")
1
}
"play" -> {
println("play start")
2
}
"onPuase" -> {
println("play onPuase")
3
}
else -> {
println("play error")
-1
}
}
}
fun main() {
val state = PlayerState("play")
println(state.setPlayState())
}
打印结果:
play start
2
复制代码
我们查看编译成Java后运行的字节码可以看到:
public final class InlineClassKt {
public static final void main() {
String state = PlayerState.constructor-impl("play");
int var1 = PlayerState.setPlayState-impl(state);
System.out.println(var1);
}
}
复制代码
编译器生成了类似Java中的静态方法,所以整个调用过程没有堆对象的产生,因为都是直接通过PlayerState类直接调用方法,故节省了对象创建的内存消耗。