简介
Kotlin 是一个基于 JVM 的新的编程语言,由 JetBrains 开发。
Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。
在Google I/O 2017中,Google 宣布 Kotlin 成为 Android 官方开发语言。
本文内容
本文介绍了Java和Kotlin语法区别,对于一些容易混淆的地方作了简单介绍。属于菜鸟级别教程。
基础语法
1.定义包
和Java不同的是Kotlin源文件路径可以和包名不一致。例如,Test.kt这支源文件可以放在项目的com文件夹下,但是包名可以写为package com.sela.kotlin。
文件名和class名也可以不一样。例如,文件名为Test.kt,里面可以这样定义:
class Hello
2.定义函数
Java 写法:
public int add(int a,int b){
return a+b;
}
Kotlin写法:
fun add(a:Int,b:Int):Int{
return a + b
}
可见性修饰符默认为public,注意返回值类型和参数类型声明差异,语句结束不用加;
Kotlin不返回任何值的写法:
fun printSth(a:Int):Unit{
println("print $a")
}
Unit可以省略:
fun printSth(a:Int){
println("print $a")
}
Kotlin可以将表达式作为函数体,返回类型自动推断:
fun add(a:Int,b:Int) = a + b
3.定义变量
- 局部变量只声明,不赋值,在编译阶段Java和Kotlin都会报错(不调用这个变量的话,不会报错)。
- 类的成员变量只声明,不赋值,Java中会被赋一个初始值,Kotlin会编译报错。提示必须初始化,否则需要将类定义为abstract。如果将此变量定义为lateinit,即延时初始化,会提示lateinit不能修饰原始类型。init代码块里面定义的变量,并不是类的成员变量,所以,函数里面不能调用。
- Java变量定义写法:
可变变量
String name = "test";
不可变变量
final String name = "test";
- Kotlin变量定义写法:
可变变量:
var name:String = "test"
变量可以被推断出类型:
var name = "test"
不可变变量:
val name:String = "test"// lateinit不能修饰val的变量
- Kotlin成员变量的完整写法(注意这里的成员二字即类的属性变量):
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
与可变变量不同的是不可变变量不允许有setter。
访问propertyName变量,实际上是调用getter方法。同样地给propertyName赋值,实际上是调用setter方法。
val isEmpty: Boolean
get() = this.size == 0 //this代表的是isEmpty所在的类实例
Noted:val修饰的变量不能有setter方法
val类型的变量不用赋初始值,var必须赋值,否则会报错:
var isTrue:Boolean = false
get() = this.size == 0
set(value){
field = value
}
继续解释下get和set,可以将get和set看做普通的函数,get有返回值和变量定义的类型一致,set没有返回值。
var isTrue:Boolean = false
get(){
return this.size == 0
}
set(value){
field = !value
return
}
这个field叫做backing field,只有修改field值后,才会真正改变。在set方法内部没有引用过
field的话,不能给isTrue初始化,否则会编译失败。在get内部不能引用本身,会导致死循环,引
起异常。
基本类型
1.常见类型
Java中有八种基本类型:
- byte:8位
- short:16位
- int:32位
- long:64位
- float:32位
- double:64位
- char:单一的 16 位 Unicode 字符
- boolean:一位
- 整数的默认类型是 int
- 浮点型不存在默认不默认这种情况,因为定义 float 类型时必须在数字后面跟上 F 或者 f。
- boolean不能强制转换
- 在把容量大的类型转换为容量小的类型时必须使用强制类型转换,有可能溢出或损失精度
int i =128;
byte b = (byte)i;- 运算中,不同类型的数据先转化为同一类型,然后进行运算
*容量小的类型转换为容量大的类型,可以自动转换
char c1='a';
int i1 = c1; // 自动转换
Kotlin中的基本类型:
- Byte:8位
- Short:16位
- Int:32位
- Long:64位
- Float:32位
- Double:64位
- Char
- Boolean:
- Array:
- String:字符串的元素可以使用索引运算符访问: s[i]
- 在 Kotlin 中,所有东西都是对象。较小的类型不能隐式转换为较大的类型。也就是不能将Int类型的变量赋值给Long型变量。前7种类型都可以用某种方法转化如toByte,toInt,toLong,toChar等。
- Kotlin中的字符串可以用三个引号括起来,内部没有转义并且可以包含换行和任何其他字符。Kotlin中的字符串可以用“+”连接起来,如果想连接其他类型的变量可以利用字符串模板:
var age:Int = 18
var name:String = "someone"
var info = "age is $age and name is $name"- Kotlin中的强制转换是用as操作符:
var view = findViewById(R.id.textview) as TextView
如果R.id.textview是一个ImageView类型的话,执行时会有异常。可以参考如下:
var view:TextView? = findViewById(R.id.textview) as? TextView
这样不会导致ClassCastException,如果R.id.textview是一个ImageView类型,那么view变量最终值是null。
2.控制流
Java中支持:for,while,do...while,switch,if...else
Kotlin支持:for,while,if...else,when
主要介绍下Kotlin的用法:
- 和Java不同的是,if是一个表达式,可以有返回值。如:
val max = if (a > b) a else b
或者
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}//最后的a和b作为返回值
Noted:if作为表达式必须要有else
- when类似于Java的switch,又有区别,when也是可以作为表达式使用的,有下面四种使用方法:
1.when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意这个块
print("x is neither 1 nor 2")
}
}
2.when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
3.fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
4.when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
- for循环:
for(item in lists){
xxxxxx
}
- while用法和Java一样,不多介绍
面向对象
1.类的定义
Kotlin定义类的写法:
class Person private constructor(name:String) {
var mName = name
init{
println("$name")
}
constructor(age:Int):this(""){
}
private constructor(color:Long):this(22){
}
constructor(name: String,age: Int):this(name){
}
}
- class关键字前面可以添加可见性修饰符,默认为public
- 类名字Person后面的constructor为主构造器,没有修饰符或者注解修饰可以省略constructor关键字
- mName变量的初始化,属于主构造器的一部分,所以可以引用name变量
- init代码块也属于主构造器的一部分,也可以引用name变量
- {}里面的其他constructor表示次构造器,如果有主构造器,那么次构造器必须直接或者间接引用到主构造器。次构造函数的参数不能用var或val声明,也就是次构造函数不能产生新的变量
- 如果没有主构造函数,也会隐式地创建一个无参构造函数,不过我们无法调用无参的构造函数实例化。
3.类实例化
Java写法:
Person p = new Person("sth",11);
Kotlin写法:
var p:Person = Person("sth",11)
4.继承
Java中对象的父类是Object,Kotlin中是Any。但是Any除了 equals()、hashCode()和toString()外没有任何成员。
- 继承的语法:
open class Base(p: Int)
class Derived(p: Int) : Base(p)
类上的 open 标注与 Java 中 final 相反,它允许其他类从这个类继承。默认情况下,在 Kotlin 中所有的类都是 final。
- 如果派生类有一个主构造函数,其基类型必须用基类的主构造函数参数初始化。
- 如果派生类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。
- 派生类主和次构造函数都没有的话,必须在类的头部初始化基类,如
open class Person{
constructor(age:Int){
}
constructor(name:String){
}
}
class Woman:Person("default"){
}
- Kotlin和Java都不允许继承多个基类
5.覆盖
- Kotlin中方法必须用open修饰才可以被覆盖,默认是final的。子类中必须用override修饰,否则也会报错。
- 成员变量的覆盖和方法是一样的,基类用open修饰,子类用override修饰。
- var 成员变量可以覆盖一个 val 成员,反之则不行。可以这样理解,var比val多了一个set方法。
- 子类和基类中有一模一样的方法,基类没有open,会编译报错。
- Kotlin的接口包含抽象方法的声明,也包含实现。这样会出现一种Java不会出现的问题,这个问题就是基类和接口有一模一样的方法。在子类覆盖此方法时,如何指明调用基类的方法还是接口的方法。看下面的例子:
interface KotlinPersonInterface {
fun test(){
}
fun print()
}
open class Person{
open fun test(){
}
open fun print(){
}
}
class Woman:Person(),KotlinPersonInterface{
override fun test() {
super<KotlinPersonInterface>.test()//注意这种用法
super<Person>.test()//注意这种用法
}
override fun print() {
super.print()
}
}
6.接口作为返回值
发现Kotlin和Java接口作为返回值有如下区别:
public interface JavaInterface {
void print();
}
interface KotlinInterface {
fun print()
}
fun returnInterfaceJava():JavaInterface = JavaInterface {
println("return InterfaceJava")
} // OK没问题
// Kotlin接口必须用object表达式来表示
fun returnInterfaceKotlin():KotlinInterface = object :KotlinInterface{
override fun print() {
println("---------")
}
}
7.可见性修饰符
Java有四种:
- public:包内及包外的任何类均可以访问
- protected:包内的任何类,及包外的那些继承了此类的子类才能访问
- 默认:包内的任何类都可以访问它,包外的任何类都不能访问
- private:包内包外的任何类均不能访问
Kotlin也是有四种:
- public:默认值,什么都不写表示是public。
- protected:和Java不同的是,即使在同一个包内,也不能访问protected方法。比private多了子类可以调用这一范围。
- internal:包内,保外都可以访问。
- private:只在这个类内部可见。
8.扩展
从这节往后的概念Java中是没有的,大家可以去官网查找资料,这里只是将知识点拎出来,方便查阅。
Kotlin提供了一种新的方法不用实现父类就可以实现继承。这个新方法通过“扩展”来实现。Kotlin支持扩展方法和扩展成员变量属性。
函数的扩展
- 扩展的语法:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to the list
this[index1] = this[index2]
this[index2] = tmp
}
val l = mutableListOf(1, 2, 3)
l.swap(0, 2)
Noted:this代表的是调用swap方法的实例,在上面的例子中表示变量l
- 扩展是静态的:
这里静态的意思是调用扩展方法的那个对象(.前面的对象)的类型是由声明的类型决定不是由运行时的类型决定。
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
这里printFoo(D())打印出来的是c不是d。
- 扩展方法和本身定义的方法一模一样时,怎么办?亲生的优先
- 扩展方法可以加在空实例上,在扩展方法内部判断是否为空
fun Any?.toString(): String {
if (this == null){
println("this is null")
return "null"
}
println("this is not null")
return toString()
}
"null"不是Any的子类,也不属于任何类型,它没有toString方法。上面这段代码就是给null添加
了一个toString()方法,返回值是字符串"null"。
注意下面的结果:
var p1:Person = Person("","")
p1.toString()
var p2:Person ?= Person("","")
p2.toString()
结果为:Any is not null。
p2因为声明为可空,它调用的是扩展的方法,这也证实了扩展是静态的这一说法
成员变量扩展
有于扩展并没有真的插入到类里面去,所以成员变量扩展的时候不能访问backing field。那就没办法给这个成员变量赋值,成员变量只能是不可变的变量即val,如下:
val <T> List<T>.lastIndex: Int
get() = size - 1
伴生对象扩展
class MyClass {
companion object {
fun create() = MyClass()
fun play(){
println("play fun")
}
}
}
fun MyClass.Companion.foo() {
//给MyClass的伴生对象扩展一个foo方法
}
调用foo方法:
1.MyClass.Companion.foo()
2.MyClass.foo() //一个类只能有一个伴生对象
扩展声明为类成员
可以在一个类内部给另外一个类扩展方法,在扩展方法内部,调用一个共同拥有的方法,优先调用扩展类对应的方法。
class C{
D.foo(){
toString()//调用的是D的toString方法
}
}
有点儿晕晕乎乎地,总结下:
- 某个类有一个扩展方法funA,类内部也有同样的方法funA,那么类内的方法优先
- classA类有个扩展方法被声明为classB类的成员方法,在方法内部调用一个classA和classB都有的方法,那么classA的方法优先,毕竟在扩展方法内部的this指的是classA。
9.数据类
Kotlin 可以创建一个只包含数据的类,关键字为 data。
- 主构造函数至少有一个参数
- 主构造函数的所有参数必须用var或者val修饰
- 数据类不能用abstract, open, sealed or inner修饰
- 可以继承基类,可以实现接口
10.密封类
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。
- 可以有子类,但是必须和子类在同一个文件里面
- 是抽象的,不能实例化,构造函数是private的
- 可以在里面定义方法和抽象方法
- 普通类继承密封类,密封类可以在普通类的:后面实例化
sealed class SealedClass{
constructor(name:String)
abstract fun print()
}
class A:SealedClass("someone"){
override fun print() {
}
}
11.嵌套类和内部类
嵌套类
class Outer{
private var age:Int = 17
class Test{
}
}
实例化Test方法:Outer.Test(),Test内部不能访问age变量
- 嵌套类和Java中的静态内部类一样
内部类
class Outer{
private var age:Int = 17
inner class Test{
}
}
实例化Test方法:Outer().Test(),Test内部可以访问age变量,因为内部类Test持有外部类的引
用。this@Outer表示外部类对象。用innter修饰的内部类和Java中的内部类一致
- 内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。
- 内部类可以无限制访问外部类的成员变量和方法,外部类访问内部类的成员变量和方法需要通过内部类实例来访问。
匿名内部类
Kotlin的匿名内部类实例用object表达式:
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
//...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
})
下面一段话摘抄自Kotlin官网:
If the object is an instance of a functional Java interface (i.e. a Java interface with a single abstract method), you can create it using a lambda expression prefixed with the type of the interface.
这句话的信息量有点儿大。之前一直不懂,为啥其他人的代码都是这么写,不会报错:
button.setOnClickListener{
println("do sth......")
}
我写的代码就不行呢?原因就在这里了,functional Java interface是Java 8的新特性,里面只允许有一个抽象方法。只有function java interface才可以用作lambda表达式,事实证明只有一个抽象方法不用functionalinterface添加注解的Java接口也是可以用lambda表达式的。kotlin的接口不能用lambda表达式,只能用object表达式。
kotlin类调用kotlin匿名内部类写法:
KotlinClass().test(object :KotlinInterface{
override fun print() {
}
})
Kotlin类调用Java匿名内部类写法:
KotlinClass().testJavaInterface(JavaInterface {
//...
})
Java类调用Java匿名内部类写法:
JavaClass().test({
})
JavaClass().test(JavaInterface {
})
JavaClass().test {
}
Java类调用Kotlin匿名内部类写法:
JavaClass().test(object:KotlinInterface{
override fun print() {
}
})
12.委托
类委托
自己的事情交给别人做,自己和别人要共同实现或继承某类
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 输出 10
}
Derived的实例要做的事情交给构造方法里的b去做,b是BaseImpl的实例。当Derived的实例要调用print方法时,实际是调用了BaseImpl的print方法。
Noted:如果我们为 Derived 添加 override fun print() { print("abc") },该程序会输出“abc”而不是“10”。
属性委托
一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。语法格式为:
val/var <属性名>: <类型> by <表达式>
by 关键字之后的表达式就是委托, 属性的 get() 方法(以及set() 方法)将被委托给这个对象的 getValue() 和 setValue() 方法。属性委托不必实现任何接口, 但必须提供 getValue() 函数(对于 var属性,还需要 setValue() 函数)。
例子:
class Example {
var p:String by Delegate()
val str = "sela"
}
class Delegate {
operator fun getValue(thisRef:Any?,property: KProperty<*>):String{
println("委托中的第一个参数 ${thisRef}")
var result = (thisRef as Example).str
println("委托中看是否可以去到Example中其他的变量$result")
return ""
}
operator fun setValue(thisRef: Any?,property: KProperty<*>,string: String){
}
}
fun main(args: Array<String>) {
var example = Example()
println("main方法中example是$example")
example.p
}
结果:
main方法中example是com.sela.kotlinjvm.java.kotlin.kotlin.Example@37a71e93
委托中的第一个参数 com.sela.kotlinjvm.java.kotlin.kotlin.Example@37a71e93
委托中看是否可以去到Example中其他的变量sela
Delegate中的getValue第一个参数代表被委托的成员变量所在类的对象。
Kotlin提供的几种委托:
- lazy委托
val p2:String by lazy(){
"aaa"
}
lazy不是一个关键字操作符,是高阶函数。定义如下:我们可以传递lambda表达式给lazy函数,p2只能是val变量,因为这里再也装不下setValue方法的实现了。
lazy委托懒在第一次调用get()会执行lambda并记录结果,后面再调用get()不会重新执行lambda,直接将记录的结果返回。
- Observable委托
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
Delegates.observable接受两个参数“初始值”和修改值时的处理程序。这个处理程序可以用lambda表达式,有三个参数:被赋值的属性、旧值和新值。
13.对象表达式和对象声明
Kotlin 用对象表达式和对象声明来实现创建一个对某个类做了轻微改动的类的对象,且不需要去声明一个新的子类。
对象表达式
- 和Java的匿名内部类作用类似,用在方法的参数中。
- 可以继承于某个基类或者实现其他接口,和Java不同的是可以同时继承一个基类和多个接口。
- 任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
- 使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问。
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}
- 对象表达式中的代码可以访问来自包含它的作用域的变量,Kotlin不必是final
对象声明
Kotlin中的对象声明有点儿像Java里面用的工具类,不用实例化即可调用里面的方法。定义语法如下:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}
val allDataProviders: Collection<DataProvider>
get() = // ……
}
- object 关键字后跟一个名称,对象声明不是一个表达式,不能用在赋值语句的右边。
- 引用方法,直接使用其名称即可:
DataProviderManager.registerDataProvider(……)
- 有超类型:
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ……
}
override fun mouseEntered(e: MouseEvent) {
// ……
}
}
- 对象声明不能在局部作用域(即直接嵌套在函数内部)。
伴生对象
伴生对象是对象声明的一种特殊情况
- 普通对象声明:
class MyClass {
object Factory {
fun create(): MyClass = MyClass()
}
}
调用方法:MyClass.Factory.create()
- 在object前面添加一个companion关键字就称为伴生对象:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
调用方法:MyClass.create()
这样也没毛病:MyClass.Factory.create()
一个类里面只能有一个伴生对象。就像一夫一妻制。
伴生对象的名称可以省略:
class MyClass {
companion object {
fun create(): MyClass = MyClass()
}
}
调用方法:MyClass.Companion.create()
MyClass.create()好像也没问题
- 伴生对象也可以实现接口和继承基类:
interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
- object表达式是object:className,interfaceName
- object声明是object name:SuperName,InterfaceName
- Kotlin的匿名内部类用object表达式
- companion object是object声明的特殊化
其他
1.空安全
kotlin中类型系统区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用)。 例如,String 类型的常规变量不能容纳 null:
var a: String = "abc"
a = null // 编译错误
如果要允许为空,我们可以声明一个变量为可空字符串,写作 String?:
var b: String? = "abc"
b = null // ok
调用 a 的方法或者访问它的属性,它保证不会导致空指针。访问 b 的同一个属性,是不安全的,并且编译器会报告一个错误。
下面展示如何解决这一编译错误:
- b?.length:如果 b 非空,就返回 b.length,否则返回 null,这个表达式的类型是 Int?
- Elvis 操作符:val l = b?.length ?: -1
如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 - !!操作符:val l = b!!.length
如果b为空直接抛出异常