簡介
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"http:// 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為空直接拋出異常