入门学习_Kotlin

Overview

由于kotlin和Java之间的高兼容性,使得kotlin适用于服务端、客户端、前端以及数据科学等多个领域。同时,从Java转向kotlin的学习曲线也更加平滑。究其本质,kotlin和Java一样都是将源代码编译成字节码,从而可以运行在虚拟机之上。此外,对于服务端开发而言,kotlin支持协程,相比Java中的线程,协程更加轻量级。因此,在硬件资源相等的情况下,使用协程可以大大地提高服务端应用的伸缩性。

kotlin基于内联函数提供Lambda,使得基于kotlin编写的应用程序运行速度更快。

kotlin在语言级别上支持协程。因此,基于kotlin提供的协程编写的应用程序用户体验更加流畅,且更具伸缩性。

基础入门

基础语法

  • 包的定义与导入
    相比Java,区别在于不需要以分号结尾,同时目录无需和包一一匹配。
  • 变量和常量的定义
    需要注意的是,kotlin中存在top-level的变量和常量,在整个kotlin文件中的任何地方都可访问和使用。
var variant_name : variant_type //局部变量必须初始化,全局变量无须初始化
val variant_name : variant_type //局部常量必须初始化,全局变量无须初始化
  • 方法定义
fun function_name(var_name: var_type, var_name1: var_type1, ...): return_type {
	//funciton_body
}
  • 方法参数可以设置默认值
fun function1(a: Int = 1, b: String = "default value") {
	// method body
}
fun function2() {
	function1() //不传参数,则默认使用方法定义中指定的参数值
}
  • 可空以及判空
    kotlin中的类型默认是不可空的,除非显式地在类型后面紧接一个问号,表示该类型可以为null。
fun toString(): Int? {
	//funciton body
}
//第一个参数不能为null,第二个参数可以为null
fun test(param1: String, param2: Int?) {
	//function body
}
//Java代码
test(null, 1) //这里运行时会抛出NPE

注意:通过Java调用kotlin的方法时,如果传入的参数为null,但是kotlin方法的参数类型没有显式地声明为Nullable,则运行时会出现空指针异常(NPE)。因此,建议如果kotlin中的方法如果是提供给其他开发人员使用的,则统一把方法参数声明为Nullable,方法体内部作不可为null的处理,避免调用方调用出现运行时异常!

  • 类型判断以及类型转换
    相比Java的instanceof关键字,kotlin中使用关键字is来判断某个实例是否为某一指定类型。
fun typeCheckAndCast(obj: Any) {
	if(obj is String) {
		println("obj is a String and its length is ${obj.length}")//当判断条件成立时,代码块中已经自动将obj转换为String类型,因此可以直接调用String的方法
		return
	}
	var len = obj.length // 这句代码编译时会报错,因为经过上述的判断,这里的obj并没有具体的类型,因此不可以调用String里的方法 
}

fun typeCheckAndCast_1(obj: Any) {
	if(obj !is String) {
		println("obj is not a String")
		return
	}
	var len1 = obj.length // 这句代码可以正常编译并执行,因为经过上面的判断,这里的obj已经自动转换为String,因此可以直接调用String的方法
}

fun typeCheckAndCast_2(obj: Any) {
	if(obj is String && obj.length > 0) { //由于&&是短路运算符,因此当左边成立的时候,右边可以直接将obj默认为String类型的实例进行使用
		println("obj is not a String")
	}
}
  • 字符串模板
    kotlin中的字符串拼接更为高级,kotlin中的 ‘’ 等同于Java中的 “” ,kotlin中的 “” 可以通过$来拼接想要的值,kotlin中的 ‘’’ ‘’’ 可以保留字符串的格式,比如换行。
var a = 1
str = "the value of variant a is $a" //str is "the value of variant a is 1"
a = 3
str = "${str.replace("is", "was")} , but now is $a"  // str is "the value of variant a was 1, but now is 3"
  • kotlin中的循环

for循环

val fruits = listOf("apple", "banana", "kiwifruit")// kotlin中的listOf返回的是一个不可变的list,即不可以向其中添加、删除、修改元素
//类似于Java中的for-each
for (fruit in fruits) {
    println(fruit)
}
for (index in fruits.indices) {
	println("the index is $index , the element is ${fruits[index]}")
}

while循环

val fruits = listOf("apple", "banana", "kiwifruit")
var index = 0
while(index < fruits.size) {
	println("the index is $index , the element is ${fruits[index]}")
	index++
}
  • when语句(类似于Java中的swith语句,但是使用起来更为灵活)
fun testhen(obj: Any) : String?= 
	when(obj) {
		1 -> "one"
		"Hello" -> "hello"
		is String -> "string"
		is Long -> "long"
		else -> null
	}
  • range(a…b)
fun testRange() {
	var a = 1
	var items = listOf(1, 2, 3)
	if(a in 1..2) {
		println("$a is in range of [1,2]")
	}

	if(a !in 1 until 2) {
		println("$a is not in range of [1,2)")
	}
	if(a !in items.indices) {
		println("$a is not in range of 0 to ${items.length - 1}")
	}
	for(index in 1..10) {
		println("the element is $index")
	}
}
  • kotlin中的集合操作(链式调用结合Lambda)
// 集合提供的操作
fun testCollectionOperation() {
	val fruits = listOf("apple", "banana", "apricot", "avocado", "kiwifruit")
	fruit.filter(it.startsWith("a"))
		.sortedBy(it)
		.map(it.toUpperCase())
		.forEach(println(it))
}
// 检查指定元素是否在集合中
fun checkValueOfCollection() {
	val fruits = listOf("apple", "banana", "apricot", "avocado", "kiwifruit") // 只读的list
	var firstFruit = fruit.firstOrNull() ?: "unknown fruit"
	if("apple" in fruits) {
		println("apple is in fruits")
	}
	if("pear" !in fruits) {
		println("pear is not in fruits")
	}
}
// 遍历map中的键值对
fun iterateMap() {
	val fruits = mapOf("apple" to 1, "banana" to 2, "apricot" to 3) //只读的map
	for((name, priority) in fruits) {
		println("$name -> $priority")
	}
}
  • 应用程序的入口函数:相比Java而言,区别在于main函数没有参数
fun main() {
	//function_body 
}

基本类型

对于开发人员来说,kotlin中不存在基本类型,任何声明的变量都引用了一个对象,可以通过变量调用方法以及访问属性。也就是说,对于Java中的基本类型,比如数值类型、字符类型、字符串类型以及数组等,在kotlin中都是对应一个常规的类型。

  • kotlin通过内置的类型来表示number类型,如Byte、Short、Int、Long、Float、Double等。在kotlin中对一个变量赋值一个整数,kotlin会根据整数的大小所处的范围,将该变量指向对应的整数类型。
var i = 2 //Int
var l = 3000000000 //Long
var l1 = 1L //Long
var s: Short = 3 //Short
var b: Byte = 2 //Byte
var f = 1.0f //Float
var d = 1.0 //Double
  • kotlin中无法对数值类型的变量自动进行类型转换
fun printDouble(num: Double) {
	println(num)
}
var i = 1
var f = 1.0f
var l = 1L

printDouble(1.0)
// 以下调用运行都会报错,类型匹配异常
// printDouble(i) 
// printDouble(f)
// printDouble(l)
  • kotlin无法自动将向上转型,如Byte转为Int。但是可以通过调用转换类型的方法,如toInt()等方法进行转换
var b: Byte = 1
// var i: Int = b //这是编译不通过的
var i: Int = b.toInt()
  • Array数组类型
var arr = arrayOf(1, 2, 3) //生成数组[1, 2, 3]
var arr1 = arrayOfNulls(2) //生成数组[null, null]
val asc = Array(3) { i -> (i * i).toString() } //生成数组["0", "1", "4"]
val intArr = IntArray(5) // 生成数组[0, 0, 0, 0, 0]
val intArr1 = IntArray(5) { 42 } // 生成数组[42, 42, 42, 42, 42]
val intArr2 = IntArray(5) { it * 1} // 生成数组[0, 1, 2, 3, 4]
  • 字符串类型
    字符串常量分为两种:通过\n实现换行 和 通过""" “”"来实现换行
val s = "Hello, world!\n" // \n实现换行
// 三引号实现换行
val text = """ 
    for (c in "foo")
        print(c)
"""

代码组织结构

  • 包管理:
    kotlin基于包管理文件,文件中包含top-level的变量、常量、方法以及类的声明。不同包下使用其他包下的类或者方法等,需要进行导入。如果出现类名或者方法名冲突,可以在导包的时候进行别名处理,保证本文件内的名称不发生冲突。
import org.example.Message
import org.test.Message as testMessage // testMessage stands for 'org.test.Message'
  • 跳转关键字:
    return:函数级别
    break:循环体级别
    continue:循环级别
    label@:标签,搭配return、break或continue使用
loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (...) break@loop //跳出指定的循环体,继续执行循环体外的代码
    }
}
//循环体外的代码

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{
        if (it == 3) return@lit // 从foreach退出,继续执行1处的代码
        print(it)
    }
    print(" done with explicit label") // 1
}

类与实例

  • 类的声明
    成员字段:
    字段的定义和初始化和方法中的变量定义和初始化基本一致,都是通过var定义变量,val定义常量。不同的地方在于,类的属性定义会生成对应的getter和setter方法,其中,对于val字段,没有setter方法。可以对setter或者getter方法进行权限修饰符的添加,如下代码所示:
class FieldsDemo {
	var str: String? = null
		private get
		private set
	val i: Int = 1
		private get
	 // private set // val变量没有setter
	var size: Int = 3
		set(value) {
			isEmpty = value
			if(value) {
				size = 0
			}
		}
	// 通过get的返回值类型推测属性的类型
	val inEmpty get() = {
		this.size == 0
	}
}

编译期常量
通过const关键字修饰的属性,其值在编译期就是已知的。const修饰的属性需要满足:

  1. top-level或者在object或companion object中定义
  2. 初始化的值为基本类型或者String类型
  3. 不可以自定义getter

延迟初始化属性和变量
如果变量或属性在声明时,没有特殊说明,默认是不可为null的。这时就需要在构造函数中对其进行初始化。但是这样并不便于开发,因此,可以采用延迟初始化来提高开发的灵活性。通过lateinit关键字修饰某个成员变量,即可实现延迟初始化的效果。注意,当该变量还没初始化的时候,使用该变量将会抛出使用前未初始化的异常。

class LateInitDemo {
	lateinit var str: String
	@setup fun setup() {
		str = "late inited"
	}
}
  • 类的构造函数
    构造函数包含主要构造函数和次要构造函数。主要构造函数只有一个(相当于Java中的无参构造函数),次要构造函数有多个。主要构造函数中的参数必须在init代码块中进行初始化,次要构造函数必须调用主要构造函数,即如下示例:
class Person(val name: String) {// 不加权限修饰符,默认是public
    var children: MutableList<Person> = mutableListOf<Person>();
    init{
		println("the init block is executed")
	}
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}
  • kotlin中的单例模式(非线程安全)
class Singleton private constructor() {
	private singleton: Singleton?
	init{
		println("the init bolck is executed")
	}
	companion object{
	@JvmStatic fun getInstance(): Singleton{
			if(singleton == null) {
				singleton = Singleton()
			}
			return singleton
		}	
	}
}
  • 继承
    kotlin中,如果某个类需要作为基类,必须以关键字open修饰该类。同样地,方法如果可以被重写,也需要以关键字open进行修饰。继承父类的子类,其构造函数必须调用父类对应的构造函数,如下代码所示:
open class Base {
	constructor(a: int)
	open fun method1() {
		println("this is the method of Base")
	}
}
class Sub : Base{
	constructor(a: int): super(a){
	}
	override fun method1() {
		println("this is the method of Sub")
	}
}

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
  • 属性重写:类似方法的重写,可以被重写的属性需要关键字open来修饰
open class Base {
	open val filed: Int = 1
	open val filed1: Int = 2
	open var filed2: Int = 3
}

class SubClass : Base {
	override val filed: Int = 0
	override var filed1: Int = 1
	override var filed2: Int = 2
	//override val filed2: Int = 2 //重写的属性不可以由var变为val,原因是属性的方法只能增加,不能减少。val重写为var,相当于增加了set方法
}
  • 子类的方法中调用父类的方法:可以通过super关键字进行调用,但是,内部类想要调用外部类的父类的方法则需要基于super结合@outer_class来实现:
open class Rectangle {
	val borderColor: String get() = "black"
	open fun draw() { 
		println("Drawing a rectangle") 
	}
}
class FilledRectangle: Rectangle() {
    override fun draw() { /* ... */ }
    val borderColor: String get() = "white"  
      
    inner class Filler {
        fun fill() { /* ... */ }
        fun drawAndFill() {
            super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
        }
    }
}

接口与抽象类

接口与抽象类的区别在于,接口中不能存储状态,但是可以定义变量,以及为变量重写getter方法。除此以外,接口与抽象类都以定义抽象方法以及方法的实现。

interface Demo {
	val str: String // 缺省修饰符abstract
	val prop: String 
		get() = "demo"
	fun method1()
	fun method2() {
		// body
	}
}

class DemoImpl : Demo {
	override val str: String = "override filed"
	override fun method1() {
		println("this is the implemention")
	}
}

可见性修饰符

koltin中一共有四种可见性修饰符,分别是private、protected、internal和public。缺省的可见性修饰符为public。

  • top-level的可见性修饰符:属性、方法、类、单例、接口等都可以定义为包级别,也就是在一个包下可以直接定义这些成员。针对top-level的成员,可见性修饰符只能使用private、internal以及public。public修饰的top-level成员,可以在同一程序的任意代码中使用;private修饰的top-level成员,只能在其定义所处的文件中使用;internal修饰的则可以在同一module中使用。
  • 类和接口内的可见性修饰符:private、protected、internal和public都可以使用,其中private、protected以及public的可见性与Java一致,internal则对在同一模块中的client可见。其中,kotlin中外部类无法访问内部类中的私有成员。
  • 局部变量、函数以及类不可以使用可见性修饰符
  • kotlin中的module:module是指一同被编译的多个kotlin文件

扩展函数

  • kotlin中可以通过扩展来为一个已经定义好的类添加新的函数。类似地,kotlin也允许通过扩展来为一个已有类添加新的属性。但是,这种扩展是并不是通过在已实现的类对应的文件中添加代码来实现,只是对实例对象扩展了能力。
package my.extension
//扩展函数需要在函数名前加上前缀
fun MutableList<Int>.swap(index1: Int, index2: Int) {
	val temp = this[index1]
	this[index1] = this[index2]
	this[index2] = temp
}

val mutableList = mutableListOf(1, 2, 3)
mutableList.swap(0, 1)
println(mutableList) // 2, 1, 3

//针对元素为任意类型的MutableList进行扩展
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
	val temp = this[index1]
	this[index1] = this[index2]
	this[index2] = temp
}
  • 扩展函数的调用是编译期决定的,与Java中的多态性不同。
open class Fruit
class Apple : Fruit()

fun Fruit.printName() = "Fruit"
fun Apple.printName() = "Apple"
//无论传入是Fruit还是Apple的实例,最终输出的都是 Fruit
fun printFruitName(fruit: Fruit) {
	println(fruit.printName())
}
printFruitName(Apple()) // 这里打印到控制台的内容是 Fruit
  • 当一个类的成员函数和扩展函数的方法签名相同时,调用的实际函数永远都是成员函数。而扩展函数如果是重载了一个成员函数,则不会出现这样的问题。
  • 利用扩展函数,可以实现可空类型实例的某些方法的安全调用,避免出现NPE。
fun Any?.toString(): String {
	if(this == null) {
		return "null"
	}
	return toString()
}
  • 同样地,kotlin中可以扩展属性,但是扩展的属性只能通过显式地定义getter和setter方法来进行访问和赋值
  • 伴生对象也可以扩展函数和扩展属性
class CompanionClass {
	companion object { }
}
fun CompanionClass.Companion.extendMethod() {
	println("this is the companion pbject extend function")
}
fun main() {
	CompanionClass.extendMethod()
}
  • 扩展函数的作用域:通常定义扩展函数都是top-level的,因此,在同一包下可以直接使用,但是,在其他包内调用扩展函数需要先导入这个函数,才能够使用
package example.extension.scope
fun <T> List<T>.swap(index1: Int, index2: Int) {
	val temp = this[index1]
	this[index1] = this[index2]
	this[index2] = temp
}

package example.extension.client
import example.extension.scope.swap
fun main() {
	val list = listOf(1, 2, 3)
	list.swap(0, 2)
}
  • 类中定义其他类的扩展函数:在类B中定义目标类A的扩展函数,该扩展函数既可以调用目标类A的成员函数,还可以调用类B的成员函数。此外,定义类B的成员函数可以通过类A的实例来调用扩展函数
class A(val name: String) {
    fun printName() { print(name) }
}

class B(val a: A, val num: Int) {
     fun printNum() { print(num) }

     fun A.printString() {
         printName()   // calls A.printName()
         print(":")
         printNum()   // calls B.printPort()
         toString()   // calls A.toString()
		 this@B.toString() // calls B.toString()
     }

     fun printNumForA() {
         a.printString()   // calls the extension function
     }
}

fun main() {
    B(A("kotlin"), 443).printNumForA()
}

数据类

kotlin中通过data关键字声明数据类,data声明的数据类,kotlin会为其自动生成一些函数,包括equals()/hashCode()、toString()、copy()以及componentN()等函数。但是,定义数据类需要满足一下条件:

  1. 主构造函数必须包含至少一个参数
  2. 主构造函数的参数必须声明为val或var
  3. data修饰的类不能被abstract, open, sealed等关键字修饰,且不能为内部类
  4. 1.1之前,data修饰的类只能实现接口

需要注意的是,kotlin为data生成的函数只针对主构造函数中定义的参数。因此,可以通过在类的主体中定义属性来避免属性被kotlin用于生成对应的函数。

嵌套类与内部类

kotlin中的内部类是基于嵌套类的,kotlin中将定义在一个类(如A)中的另一个类(如B)成为嵌套类,嵌套类的声明与普通类一样,只需要class关键字修饰即可。但是,如果使用inner关键字修饰类B,则类B就成为内部类。内部类与嵌套类的区别在于:内部类可以访问外部类的成员变量和成员函数,而嵌套类不可以。这是因为,内部类的实例保存了指向外部类实例的引用。基于内部类,还可以定义匿名内部类,如果匿名内部类的父接口只包含一个方法,此时可以使用lambda来进行编写。
package my.inner.class
class OuterClass {
	private var str = "outer class member"
	//内嵌类
	class NestedClass {
		fun getStr(): String {
			return "nested class member"
		}
	}
	//内部类
	inner class InnerClass {
		fun getStr(): String {
			return str
		}
	}
	//参数可以传入匿名内部类
	fun addListener(view: View, listener: OnclickListener) {
		view.addListener(listener)
	}

	fun main() {
		addListener(object: OnclickListener {
			override fun onClick(){
				//body
			}
		})
	}
}

枚举类

kotlin中的枚举类支持定义成员变量、抽象方法(枚举类定义,枚举常量实现)以及实现接口等功能。

package my.enum
enum class MyEnum(var value: Int) {//定义成员变量
	SMALL(1) {
		override fun nextGrade() = MIDDLE //实现抽象方法
	}
	MIDDLE(2) {
		override fun nextGrade() = LARGE
	}
	LARGE(3) {
		override fun nextGrade() = SMALL
	}

	abstract fun nextGrade(): MyEnum
}

class Main() {
	fun main() {
		val enums = MyEnum.values()
		for(en: MyEnum in enums) {
			println(en)
		}
		println(MyEnum.valueOf(1)) // print SMALL
	}
}

object关键字

object既可以用于声明匿名内部类,又可以用于定义单例,并且object实现的单例是线程安全的。通过object实现的单例,可以直接通过类名访问单例中定义的成员。此外,object还用于定义伴生对象,伴生对象中可以通过@JvmStatic来定义静态方法。

package my.object
object SingleTon { //延迟初始化,只有当第一次使用的使用才会进行初始化
	fun printStr() {
		// body
	}
}

class Outer() {
	companion object Inner() { //当Outer被加载到JVM时,会被初始化,相当于Outer的静态成员
		fun method1() = "method1"
	}

	fun main() {
		println(Outer.method1()) // 类似于Java中的静态成员,但是运行时却不是以静态成员的身份执行,而是以一个实例的方法进行执行
	}
}

类的别名(typealias)

通过关键字typealias来为一个类定义别名,可以帮助开发人员简化代码的编写

typealias FunctionHandler = (Int, String) -> Unit
class Outer {
	inner class Inner
}
typealias OI = Outer.Inner

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}

委托机制以及委托属性

  • 委托机制

  • 委托属性

函数与Lambda

  • 函数定义
    相比Java中的函数定义,kotlin中的函数通过fun关键字来定义,参数声明方式为变量名: 变量类型,此外最大的不同在于可以在函数定义时,为函数的参数设置默认值,设置默认值之后,函数的调用可以不用传递具有默认值的参数,但是需要注意的是,可以省略的参数一定是在不带默认值的参数之后,这样才不会引发歧义。此外,调用函数的时候可以用键值对的形式传递参数值,这样更具可读性。函数的返回值类型必须在定义函数时显式指定,除非返回的类型为Unit,或者函数体直接以表达式的形式来定义函数体。
fun method1(var1: Int = 3, var2: String, var3: Int = 1): Unit {
	println("var1 : $var1, var2: $var2, var3: $var3")
}
fun method2(var1: Int = 1, var2: () -> Unit) {
	var()
}
fun <T> method3(var1: Boolean = true, vararg vars: T): List<T> {
	val result = ArrayList<T>()
	for (item in vars) { //vars is implemented by Array
		result.add(item)
	}
	return result
}
class Main {
	fun main() {
		method1(var2 = "var2")
		method1(var1 = 1, var2 = "var2", var3 = 3)
		method2(var2 = {
			println("the variant var2 is executed")
		})
	}
}
  • 本地函数
    kotlin与Java中一样,允许定义成员函数,此外,kotlin还允许在函数中定义其他函数,定义top-level函数,定义扩展函数等。其中,本地函数可以使用在外部函数定义的本地变量。
  • 高阶函数和Lambda
    在kotlin中,高阶函数是指某个函数的参数为函数或者返回的类型为函数,这样的函数称之为高阶函数。使用Lambda的代价是造成内存开销,因为每个Lambda都对应一个实例对象,因此,如果能够搭配inline内联函数来使用高阶函数,这样就会降低Lambda使用带来的内存开销,而使用内联函数也会导致编译之后的代码量增加,因此不适合对函数体大的Lambda进行内联,除非这个Lambda在高阶函数中用于循环调用。

Reference

kotlin官方文档
kotin语言中文站

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值