Kotlin语言------一文了解
Posted 战国剑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin语言------一文了解相关的知识,希望对你有一定的参考价值。
Kotlin语言作为一种完全兼容Java的语言,在2017年,被Google指定为官方开发语言,并宣布后续将以Kotlin作为android的第一开发语言。相对于Java,Kotlin解决了许多的痛点,使用会更简洁。
Kotlin目前应用最为广泛的,也是在Android上。Google官方在后续官网的Android更新都有提供Java和Kotlin双版本,并且目前许多的开源库都以Kotlin写就。因此了解Kotlin还是很有必要的。
本文会将Kotlin中基础以及与Java的不同部分做分析,主要按以下的三个类别做整理。
基础部分:常量、变量、类、方法、循环控制、接口、继承、集合;
语言特色部分:内联函数、高阶函数、lambda、泛型实化、泛型协变、泛型逆变、单例、扩展方法、标准库方法等;
新概念部分:协程。
一、基础介绍
1、常量与变量
Kotlin中常量的定义方式是val,变量的定义方式是var,不可变量定义方式为 const val。
以下是定义的例子:
//常量
val valMain = "val in Main"
//变量
var int:Int = 1
//const 定义的常量(const 只能定义在 val前)
const val constVal:String = "CONST_VAL"
无论是常量还是变量,定义的基础格式都是:var/val 名称 类型 = 具体赋值。
我们可以很语义化的读出这个变量/常量的意思,比如 var int:Int = 1,就是:变量int是Int类型,值等于1。常量也是类似的。这种声明与Java是不一致的,Java是把类型声明放在首位的。
另外,为什么会有两个常量的定义:val、const val?
通过反编译可以看到:
const val 可见性为public final static,可以被其他类和文件直接访问。
val 可见性为private final static,并且val 会生成方法getNormalObject(),通过方法调用访问。
二者可见性不同,使用范围不同。如果是通用的常量定义,建议使用const val,定义在统一外层文件,便于使用与查找。
Kotlin中数据类型的定义与Java中也是有较大的改变。Kotlin中,在数据类型的定义上,取消了基础数据类型的定义,全部都是以类的形式对外展示的。
var int:Int = 1
var double:Double = 1.00
var float:Float = 1.0F
var string:String = "str"
var shot:Short = 127
var bool:Boolean = true
var char:Char = 'a'
Java中的基础类型int,在Kotlin中用Int表示、float用Float表示。这样在编码上,对开发人员统一。
Kotlin的编译器,会在编译的时候,自动将基础类型的格式做转换,比如把Int转为int,Float转为float。
2、循环与判断、控制语句
2-1、循环:其中、while的用法与Java一致。for语句有特殊的写法。
1、正序遍历
for (i in 0..5){
println(i) //print 0 1 2 3 4 5
}
2、逆序遍历
for(i in 5 downTo 0){
println(i) //print 5 4 3 2 1 0
}
3、遍历带上步长
for (i in 0..5 step 2){
println(i) //print 0 2 4
}
4、遍历不包括最后一个元素
for (i in 0 until 5){
println(i) //print 0 1 2 3 4
}
2-2、判断、控制语句:
ifelse与Java是一样的用法。
这里介绍一个范围表达式:range表达式,格式是 in a..b ,用来判断某个数是否在这个范围内。这个在前面的循环中已经有使用到。
另一个常用的表达是是:when表达式(有if/switch语句的地方,如果有需要,都可以用when取代)
这里做一个if语句和when语句的比对
val testIf: Int =1
1、if语句
if(testIf==1){
println("is one")
}else{
println("is other")
}
2、when语句
when(testIf){
1-> println("is one")
else -> println("is other")
}
when表达式的入参,可以是任何数。when语句还有其他写法,比如when{},具体可以手动实验。
2-3、Null判断
Kotlin中,鼎鼎大名的还有一个特色,就是它的安全机制,也就是null安全。
Kotlin中,为了避免NPE问题,除非有特殊规定,否则变量都不可为null。
但是Kotlin中,肯定还是会存在null,因为我们也会用到。
Kotlin中区分了可空类型和非可空类型。
可空类型:var a:String?
非可空类型:var b:String
如果你要使用可空类型,做更多操作,那么你要加上语句:?. 这个就是Kotlin的安全调用符。
a?.count() ,这样的调用,在a为null是,是不会执行后续的count()函数的。只有当a不为null,才会执行,这就保证了程序中不容易出现NPE。
但有些情况下,你知道该变量不可能为null。那么你可以这么写:a!!.count() ,这里的 !!. 叫做非空断言运算符,这样就必定会执行后续的count()函数,无论前面的a是否为null。
看个例子:
val test:String ?= null
val s = test?.count()
println(s) // print null
test!!.count() // npe exception
3、方法(函数)
Kotlin中,方法的定义方式如下:
private fun toastInfo(name:String,age:Int):String{
return "my name is $name, age is $age"
}
修饰符+fun关键字(表示函数/方法)+方法名称+(入参和入参类型)+冒号+返回类型。
上述方法的意思就是:私有函数,名称为toastInfo,入参是name(String类型)和age(Int类型),返回值是String类型。
这是Kotlin中函数最基本的格式,当然Kotlin中是可以用高阶函数的,这会在后面介绍。
4、类
Kotlin中的类与Java中的类,还是有较大差别的。
还是以代码方式来说明差异,在代码上标明了5点的差异:
//1、open 可继承的类
//2、主构造函数为空的时候,可以省略类名后的()
open class Person {
var name = "bill"
//3、方法要被继承,需要open
open fun display() {
println("person's name is $name")
}
}
//4、继承、接口实现的标志都是:,与java的 extends 有差别
class Student(var age: Int) : Person() {
override fun display() {
println("The student's info --> name:$name,age:$age")
}
}
fun main() {
//5、实例化,不需要new
val person = Person()
println(person.name)
val student = Student(18)
student.display()
}
1、Kotlin的类,默认是不可继承的。相当于Java中的 final class,以保证类的数据安全性。
2、类的实例化,去除了Java中的new关键字。
3、继承与实现接口的标志,统一都是冒号(:)。
4、类中的方法要被继承,也需要加上open标志。
5、构造方法的写法发生了改变。
关于构造方法的写法,上面代码中,只是展示了一个如果主构造函数的参数为空的时候,可以省略类名后面的括号。Kotlin中,还有次级构造方法的概念,详细介绍如下:
5-1、主构造函数的写法
以下的差别主要在于:如果在主构造函数的入参中,有var或者val标志,该参数就是类的属性,否则就只是一个临时的值参传入。
//空主构造函数
open class InitTest {
var field1 = 0
val field2 = ""
}
//主构造函数存在,传入两个临时变量
class InitTest1(_name: String, _age: Int) {
var name = _name
var age = _age
}
//函数属性 直接定义 用var 或者 val,空函数体
class InitTest2(var name: String, var age: Int)
//函数属性 直接定义 用var 或者 val,有函数体
class InitTest3(var name: String, var age: Int) {
fun initTest3() {
println("name is $name,age is $age")
}
}
//参数有默认赋值
class InitTest4(var name: String, var sex: String = "男", var age: Int) {
fun initTest4() {
println("name is $name,age is $age,sex is $sex")
}
}
5-2、次级构造函数
次级构造函数,最后总要调用到主构造函数,下方代码中的:
//次级构造函数1
constructor(_age1: Int):this("mock",_age1)
//次级构造函数2
constructor(name1: String):this(name1,0)
这两个次级构造函数中的this(xx,xx)表示调用了主构造函数。
open class Init(var name:String,_age:Int){
var age = _age
var sex:String
init {
sex = ""
}
//次级构造函数1
constructor(_age1: Int):this("mock",_age1)
//次级构造函数2
constructor(name1: String):this(name1,0)
}
以上是Kotlin类与Java类的主要差异点。Kotlin类,既然可以用open修饰,其实还有更多其他的修饰符,如data、object等,后续介绍。
5、接口与抽象类
这里主要介绍接口。Kotlin的抽象类写法与Java是一致的,但是接口也实现了部分抽象类的功能,接口可以有默认实现了。
下方接口DoSome中的drink方法有默认实现了!这对于代码的简化是显而易见的。日常我们的代码为了实现公共的接口,经常会有很多的空实现,在Kotlin中就不会有这个问题。
//1、接口 interface
interface DoSome{
fun eat()
//该接口方法有默认实现了
fun drink(){
println("drink water")
}
}
class Do1 :DoSome{
override fun eat() {
TODO("Not yet implemented")
}
override fun drink() {
super.drink()
}
}
class Do2 :DoSome{
override fun eat() {
TODO("Not yet implemented")
}
}
//2、抽象类 abstract
abstract class DoAny{
fun eat(){
}
abstract fun drink()
}
class DoAny1:DoAny(){
override fun drink() {
TODO("Not yet implemented")
}
}
6、集合
Kotlin很大程度上,简化了集合的写法,提供了多种的集合数据提供方式。
常用的集合的结构有List、Map、Set、Array等,对这几种集合收集数据的方式,做了简化处理:
//list
val list:List<String> = listOf("1","2")
//set
val setList:Set<String> = setOf("5","6")
//array
val intArray:IntArray = intArrayOf(1,2)
//map
val map:Map<Int,String> = mapOf(1 to "one",2 to "two")
集合也默认是不可变集合,可以通过toMutable式的函数,做可变转化。相近集合类型之间,也可以做到转化。
//可变集合的转化:从不可变 专为 可变
val setList:Set<String> = setOf("5","6")
val toMutableSet = setList.toMutableSet()
//List->Set的转化
val toMutableList1 = toMutableSet.toMutableList()
toMutableList1.add("7")
val message = toMutableList1.toMutableSet()
集合还提供了许多的语法糖方法,比如过滤、最大最小值、排序等,操作十分方便。
二、语言特色
Kotlin相比Java,在很多地方做了优化与改进,这些特性都归为语言特色来介绍。
1、高阶函数与lambda
高阶函数是Kotlin的一大特色。Java在低版本上,是不支持这种特性的(Java7中仍不支持)。
高阶函数就是指:方法的入参或者返回值,可以是函数。
1-1:函数作为入参
这里用一个数字运算的例子来说明:
operation这个函数中,第三个入参是一个函数:这个入参函数的参数名是func,类型是:(Int,Int)->Int,也就是两个整型入参,返回一个整型(这里比较绕,多看几遍就明白了)。
函数test1、test2、test3都是两个整型入参,返回一个整型的形式,只是内部运算方式不同。
//高阶函数:函数作为入参
//func:(Int,Int)->Int 一个函数作为入参
//数据运算,运算的方法主体实现,等待外部传入
fun operation(a: Int, b: Int, func: (Int, Int) -> Int): Int {
return func(a, b)
}
fun test1(a: Int, b: Int): Int {
return a + b
}
fun test2(a: Int, b: Int): Int {
return a - b
}
fun test3(a: Int, b: Int): Int {
return a * b
}
如何使用?调用方式就是这样的:operation(3,4,::test1),其中test1前面的::表示函数引用。
fun main() {
//带函数名称的入参。函数引用方式传入 ::
println("operation test1 = ${operation(3, 4, ::test1)}")
println("operation test2 = ${operation(3, 4, ::test2)}")
println("operation test3 = ${operation(3, 4, ::test3)}")
}
Kotlin中有很多的高阶函数作为入参使用的例子,很多扩展函数的入参都是一个lambda,也就是一个匿名函数。我们手动写一个also的例子(关于扩展类,后续有再介绍):
//1、T 泛型类 T. 泛型类的扩展方法。T可以是任意类
//2、block:(T) -> Unit,表示入参是block,类型是一个函数,该函数的入参是T的实例,返回值是空
//3、myalso函数,最后的返回值是T,也就是实例本身
//4、函数体中的block(this),表示调用函数,入参this表示T的实例
fun <T> T.myalso(block:(T) -> Unit):T{
block(this)
return this
}
//测试类
class TestInfo{
var info:String = ""
}
fun main() {
//lambda入参形式的also 怎么写
val testInfo = TestInfo()
testInfo.info = "my info"
val result = testInfo.myalso {
it.info = "your info"
}
println("operation lambda myalso = ${result.info}")
}
这里主要是模仿系统的also函数,看我们高阶函数的作用。这个例子可以后续回头再看。
1-2:函数作为返回值
函数作为返回值,我们可以将它赋值给一个变量或者常量,这个变量或者常量,就具有了函数的特征,仔细看下方的函数调用方式。
//高阶函数:函数返回值
//该函数返回一个:入参为Int,出参为空的函数(Int)->Unit
//该函数返回的写法,一个lambda = { num:Int -> println(num) }
fun getFun():(Int)->Unit{
return { num:Int -> println(num) }
}
//funReturn是一个函数变量
val funReturn = getFun()
fun main() {
//调用方式1
getFun()(2)
//调用方式2
funReturn(3)
}
以上是高阶函数的例子,高阶函数可以让代码更灵活(如果使用不当,也容易造成代码混乱...)
2、内联函数
这是一个Java中没有的新概念。
引用一段来自https://www.jianshu.com/p/ab877fe72b40的说明:
当我们使用
lambda
表达式时,它会被正常地编译成匿名类。这表示每调用一次lambda
表达式,一个额外的类就会被创建,并且如果lambda
捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这会带来运行时的额外开销,导致使用lambda
比使用一个直接执行相同代码的函数效率更低。如果使用
inline
修饰符标记一个函数,在函数被调用的时候编译器并不会生成函数调用的代码,而是 使用函数实现的真实代码替换每一次的函数调用。
它的主要作用是实现代码的替换,避免资源的浪费。另外、内联函数不要递归方法上声明。
它的另一个作用就是后续要介绍的泛型实化上。
看一个例子,你就明白内联函数的作用:
我们定义了两个方法:testInlineFun,这是一个内联方法。testCommonFun是一个普通方法。
我们对这个文件做反编译后,可以看到下方的注释代码。在main中,声明了内联的函数,直接被拷贝进了main方法中,普通的方法则是正常调用。
package com.wudaokou.kotlinuse
inline fun testInlineFun(){
println("testInlineFun print")
}
fun testCommonFun(){
println("testCommonFun print")
}
fun main() {
println("main start")
testInlineFun()
testCommonFun()
}
/* show kotlin bytecode
public final class Kotlin11_inlineKt {
public static final void testInlineFun() {
int $i$f$testInlineFun = 0;
String var1 = "testInlineFun print";
boolean var2 = false;
System.out.println(var1);
}
public static final void testCommonFun() {
String var0 = "testCommonFun print";
boolean var1 = false;
System.out.println(var0);
}
public static final void main() {
String var0 = "main start";
//将testInlineFun方法的内容拷贝到main方法里了
boolean var1 = false;
System.out.println(var0);
int $i$f$testInlineFun = false;
String var4 = "testInlineFun print";
boolean var2 = false;
System.out.println(var4);
//此处调用方法testCommonFun
testCommonFun();
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}*/
3、泛型实化
这也是Kotlin中的一个新概念。 在Java中,Java 泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型。所以Java中,T.class , T instanceOf xx 等是无法使用。
在Kotlin中,对这种情况,有了改进。如果这个泛型所在的方法是一个内联方法,并且泛型用reified修饰,那么就可以实现T.class、T is xx等取值和判断。原因在于:在编译时就会将内联函数的代码替换到实际调用的地方,所以对于内联函数/方法来说是不存在泛型的擦除的。
如下方,getType函数中,我们直接返回了T的类型。
//泛型实化 :inline 内联 reified 修饰关键字(具体化)
inline fun <reified T> getType() = T::class.java
fun main() {
val type = getType<String>()
val type1 = getType<Int>()
println("type --> $type")
println("type1 --> $type1")
//result:
// type --> class java.lang.String
//type1 --> class java.lang.Integer
}
4、泛型协变与逆变
这个可以直接看这篇解析,赞:https://www.jianshu.com/p/0c2948f7e656
文章对Kotlin和Java做了详细对比,也很清晰的看到了Kotlin中的改进在哪里。
对此也写了一个例子,供参考:
fun main() {
//1、MutableList 中用out修饰后,作为生产者,无法add操作。add是一个消费者方法。
val ls:MutableList<out Fruit> = listOf(Orange(1), Orange(2)).toMutableList()
println(ls[0].name)
// ls.add(Orange(3)) //Out-projected type 'MutableList<out Fruit>' prohibits the use of 'public abstract fun add(element: E)
// ls.add(Fruit("")) //Out-projected type 'MutableList<out Fruit>' prohibits the use of 'public abstract fun add(element: E)
//2、MutableList 中用in修饰后,作为消费者,可以add操作。add是一个消费者方法。但取值被限制了,无法取得有效的类。
val ls1:MutableList<in Fruit> = listOf(Orange(1),Orange(2)).toMutableList()
//此时拿到的是Any?类型,并无实际意义
val get = ls1.get(0)
println(get)
//add 操作
ls1.add(Orange(4))
}
5、单例
Kotlin中,对单例类的写法做了很大的改进。直接将类的关键字class改为object就可以了,这样就表示是一个单例类。
除此之外,object关键字还有其他三种用法:普通类中嵌套单例类、伴生类、object关键字的表达式。
//普通类
open class MyClass{
open fun testClass(){
println("testClass")
}
}
//普通类中含有object
class MyObjectInClass{
fun testClass(){
println("testClass")
}
companion object ObjectInClass{
fun testObjectInClass(){
println("testObjectInClass")
}
}
}
//单例类
object MyObjectClass{
fun testObject(){
println("testObject")
}
}
//普通类中嵌套单例类
class MyClassWithObject{
object ClassWithObject{
fun testClassWithObject(){
println("testClassWithObject")
}
}
fun testObject(){
println("testObject")
}
}
上面是object的几种用法,注意:伴生类一个类中只能有一个。
这里看下如何使用它们?这里定义了多个接口,用于object对象表达式的演示。在代码里可以看到,object对象表达示例和Java的匿名类还是有挺大的区别的。object对象表达式,可以同时继承类、实现多个接口。
interface DoSome{
fun getSome():String
fun doSome()
}
interface DoSome1{
fun getSome1():String
fun doSome1()
}
fun doSomeThing(doSome:DoSome){
doSome.getSome()
doSome.doSome()
}
fun doSomeThing1(doSome1:DoSome1){
doSome1.getSome1()
doSome1.doSome1()
}
fun doSomeThing2(myClass: MyClass){
myClass.testClass()
}
fun main() {
//单例类的使用
MyObjectClass.testObject()
//普通类的使用
val myClass = MyClass()
myClass.testClass()
//伴生类的使用 - 一个类中,只能有一个伴生类
val myObjectInClass = MyObjectInClass()
myObjectInClass.testClass()
MyObjectInClass.testObjectInClass()
//普通类中嵌套单例类
MyClassWithObject.ClassWithObject.testClassWithObject()
//对象表达式 - setOnClick 等,可继承一个类以及同时实现多个接口
val objectTest = object : MyClass(),DoSome, DoSome1 {
override fun testClass() {
super.testClass()
}
override fun getSome(): String {
println("getSome()")
return "getSome()"
}
override fun doSome() {
println("doSome()")
}
override fun getSome1(): String {
println("getSome1()")
return "getSome1()"
}
override fun doSome1() {
println("doSome1()")
}
}
println("test do some")
doSomeThing(objectTest)
println("test do some1")
doSomeThing1(objectTest)
println("test super class")
doSomeThing2(objectTest)
}
6、扩展
这又是一个很重要的特性,前面文章中的myalso,就是扩展方法的一种写法。
Kotlin 可以对一个类的属性和方法进行扩展,且不需要继承或使用装饰者模式。
扩展是一种静态行为,对被扩展的类代码本身不会造成任何影响。
假如,Kotlin中一个类是普通类,它不允许继承。那么如果想往这个类中新增方法有什么办法呢?这就可以用扩展方法:
下面这个例子可以看到如果要定义一个扩展函数,真的很简单:类名.方法名 即可。
一般情况下,定义的类的扩展函数,可以放到外层或者顶层,便于查看和使用。
class ExtClass{
fun test(){
println("test")
}
}
//这个就是类ExtClass的扩展函数
fun ExtClass.myExtFun(){
println("my Ext fun")
}
fun main() {
//本文件中定义 ext
val extClass = ExtClass()
extClass.test()
extClass.myExtFun()
//外部定义ext 导入
extClass.test2()
val str = "my str"
str.myFun()
}
8、标准库方法
Kotlin的标准库中,提供了很多便捷的方法。这里以一些常用方法来举例。标准库的定义方式,很多也是扩展方法。
这里主要是关于 run \\ with \\ T.run \\ T.let \\ T.also \\ T.apply 范围函数的示例。可参考注释来看各种标准库方法的作用。
class Info {
var name: String? = null
var age: Int = 0
override fun toString(): String {
return "$name --- $age"
}
}
//关于 run \\ with \\ T.run \\ T.let \\ T.also \\ T.apply 范围函数
fun main() {
val info = Info()
//run 返回最后一行作为返回值
//run0 返回空
val run = run{
println("only print line")
}
println(run::class.java)
//run1 返回整型
val run1 = run{
0
}
println(run1::class.java)
//with 无返回值
val with = with(info){
name = "bob"
age = 18
}
println(info.toString())
println(with::class.java)
//T.run 扩展函数 无返回值
val TRun = info.run {
name = "cell"
age = 19
}
println(info.toString())
println(TRun::class.java)
//T.let 扩展函数 无返回值
val let = info.let {
it.name = "allen"
it.age = 20
}
println(info.toString())
println(let::class.java)
//T.also 扩展函数 返回自身
val also = info.also {
it.name = "jack"
it.age = 21
}
println(info.toString())
println(also::class.java)
//T.apply 扩展函数 返回自身
val apply = info.apply {
name = "pony"
age = 22
}
println(info.toString())
println(apply::class.java)
}
三、协程
协程建议看这篇解析:https://www.cnblogs.com/mengdd/p/kotlin-coroutines-basics.html
很清晰的描述了它的原理和使用方式。
概括下协程容易弄混的地方:
0、挂起函数是协程的特色函数,它的挂起和重入,类比线程的阻塞和可运行。
1、一个协程中的函数,是顺序执行的。
2、协程阻塞只阻塞本协程,不会对其他协程和线程产生影响。
3、协程可以有多个子协程并行执行。
4、协程可以提高线程的利用率。线程中可以开多个协程,比如10万个。
5、协程在android中,可以很方便的把异步方式改为同步方式调用。
6、协程可以配合android的许多特性,增加生命周期等。
7、协程可以和网络库等配合使用(retrofit新版,已有支持协程)。
协程主要用法有三种:
GlobalScope、runBlocking、CoroutineScope。CoroutineScope是实际应用中最常用的。其他两种多用于测试。
协程的简单例子:
package com.wudaokou.kotlinuse
import kotlinx.coroutines.*
fun testGlobalScopeDelayLong(){
//1、delay 较长时间
println("start ")
GlobalScope.launch {
println("start GlobalScope ,thread = ${Thread.currentThread().name}")
delay(5000)
println("end GlobalScope ,thread = ${Thread.currentThread().name}")
}
println("main end ,thread = ${Thread.currentThread().name}")
Thread.sleep(6000)
}
fun testGlobalScopeDelayShort(){
//2、delay 较短时间
println("start ")
GlobalScope.launch {
println("start GlobalScope ,thread = ${Thread.currentThread().name}")
delay(1000)
println("end GlobalScope ,thread = ${Thread.currentThread().name}")
}
println("main end ,thread = ${Thread.currentThread().name}")
Thread.sleep(1500)
}
fun testRunBlocking(){
//3、runBlocking
println("start ")
runBlocking {
println("start GlobalScope ,thread = ${Thread.currentThread().name}")
delay(1000)
println("end GlobalScope ,thread = ${Thread.currentThread().name}")
}
println("main end ,thread = ${Thread.currentThread().name}")
}
fun testCoroutineScope(){
//4、CoroutineScope
println("start ")
CoroutineScope(Dispatchers.Default).launch {
println("CoroutineScope start ,thread = ${Thread.currentThread().name}")
delay(3000)
println("CoroutineScope end ,thread = ${Thread.currentThread().name}")
}
println("main end ,thread = ${Thread.currentThread().name}")
Thread.sleep(4000)
}
fun main() {
testCoroutineScope()
}
可以运行上面的实例,看具体效果。
以上是Kotlin语言的一些基础以及有别于Java的特性。更多的Kotlin用法,需要在上面的基础上,自身去拓展了。
以上是关于Kotlin语言------一文了解的主要内容,如果未能解决你的问题,请参考以下文章