Kotlin筆記(二)
這一篇是Kotlin的基礎,主要包含一下幾點:
1)宣告函式、變數、類、列舉以及屬性
2)Kotlin中的控制結構
3)智慧轉換
4)丟擲和處理異常
1、基本要素:函式和變數
在這節主要介紹組成Kotlin程式的基本要素:函式和變數。
1.1程式設計師學習一門語言的開始,列印Hello,world!
fun main(args: Array<String>) {
println("Hello world")
}
從這樣簡單的一小段程式碼中觀察到哪些特性和語法?看看下面這個列表:
1)關鍵字fun用來宣告一個函式。沒錯,Kotlin程式設計有很多樂趣(fun)!
2)引數的型別寫在它的名稱後面。稍後會看到,變數的宣告也是這樣。
3)函式可以定義在檔案的最外層,不需要把它放在類中。
4)陣列就是類。和Java不同,Kotlin沒有宣告陣列型別的特殊語法。
5)使用println代替了System.out.println。Kotlin標準庫給Java標準庫函式提供了許多語法更加簡潔的包裝,而println就是其中一個。
6)和許多其他現代語言一樣,可以省略每行程式碼結尾的分號。
1.2函式
/** * 程式碼塊函式體 */ fun max(a:Int, b:Int):Int{ return if(a > b ) a else b }
函式的宣告以關鍵字fun開始,函式名稱緊隨其後:這個例子中函式名稱是max,接下來是括號括起來的引數列表,引數列表後面跟著函式的返回型別,它們之間用一個冒號隔開。
表示式和語句
在Kotlin中,if是表示式,而不是語句。語句和表示式的區別在於,表示式有值,並且能作為另一個表示式的一部分使用;而語句總是包圍著它程式碼塊中的頂層元素,並且沒有自己的值。在Java中所有的控制結構都是語句。而在Kotlin中,除了(for、do和do/while)以外大多數控制結構都是表示式。這種結合控制結構和其他表示式的能力讓我們可以簡明扼要的表示許多常見的模式。
另一方面,Java中的賦值操作是表示式,在Kotlin中反而變成了語句。這有助於避免比較賦值之間的混淆,而這種混淆是常見的錯誤來源。
表示式函式體
可以讓前面的函式變的更加簡單。因為他的函式體是由單個表示式構成的,可以用這個表示式作為完整的函式體,並去掉花括號和return語句:
/**
* 表示式函式體
*/
fun max2(a:Int,b:Int) = if(a > b) a else b
fun max3(a:Int,b:Int): Int = if(a > b) a else b
如果函式體寫在花括號中,我們說這個函式有程式碼塊體。如果他直接返回了一個表示式,它就是表示式體。
為什麼在max3函式中可以不宣告返回型別?作為一門靜態型別語言,Kotlin不是要求每個表示式都應該在編譯期具有型別嗎?事實上,每個變數和表示式都有型別,每個函式都有返回型別。但是對錶達式體函式來說,編譯器會分析作為函式體的表示式,並把它的型別作為函式的返回型別,即使沒有顯示的寫出來。這種分析通常稱作型別推導。注意,只有表示式體函式的返回型別可以省略。
1.3 變數
在Java中宣告變數的時候會以型別開始。在Kotlin中這樣是行不通的,因為許多變數宣告的型別都可以省略。所以Kotlin中以關鍵字開始,然後是變數名稱,最後可以加上型別(不加也可以)
val question = "The Ultimate Question of Life, the Universe, and Everything"
val answer = 42
val answer2: Int = 42
和表示式體函式一樣,如果你不指定變數的型別,編譯器會分析初始化器表示式的值,並把它的型別作為變數的型別。
如果變數沒有初始化器,需要顯式的制定它的型別。
val answer3: Int
answer3 = 43
如果不能提供可以賦給這個變數值得資訊,編譯器就無法推斷出它的型別。
可變變數和不可變變數
宣告變數的關鍵字有兩個:
1)val(來自value)不可變引用。使用val宣告的變數不能再初始化後再次賦值。它對應的是Java的fianl變數。
2)var(來自variable)可變引用。這種變數的值可以被改變。這種宣告對應的是普通(非final)的Java變數。
預設情況下,應該儘量地使用val關鍵字來宣告所有的Kotlin變數,僅在必要的時候換成var。使用不可變引用、不可變物件及無副作用的函式讓你的程式碼更接近函數語言程式設計風格。
在定義類val變數的程式碼塊執行期間,val變數只能進行唯一一次初始化。但是,如果編譯器能確保只有唯一一次初始化語句會被執行,可以根據條件使用不同的值來初始化它:
val message: String
if(canPerformOperation()){
message = "Success"
}else{
message = "Failed"
}
注意:儘管val引用自身是不可變得,但是他指向的物件可能是可變的。例子:
val languages = arrayListOf("Java")
languages.add("Kotlin")
即使var關鍵字允許變數改變自己的值,但他的型別卻改變不了。例子:
使用字串字面值會發生錯誤,因為他的型別(String)不是期望的型別(Int)。編譯器只會根據初始化器來推斷變數的型別,在決定型別的時候是不會考慮後續的賦值操作。如果需要在變數中儲存不匹配型別的值,必須受到把值轉換或強制轉換到正確的型別。
1.4更簡單的字串格式化:字串模板
這個例子介紹了一個新特性,叫做字串模板。在程式碼中,你聲明瞭一個變數oldest,並在後面的字串字面值中使用了它。Kotlin讓你可以在字串字面值中引用區域性變數,只需要在變數名稱前面加上字元$。這等價於Java中的字串連結("Hello ," + oldest + "!"),效率一樣但是更加緊湊。還可以引用複雜的表示式,而不是簡單的變數名稱,只需要把表示式用花括號括起來:
2、類和屬性
面向物件程式設計對我們來說可能不是什麼新鮮的概念,你也許非常熟悉類的抽象機制。Kotlin這方面的概念你也會覺得似曾相識,但是你會發現許多常見的任務使用更少的程式碼就可以完成。先來看一個簡單的JavaBean類People目前它只有一個屬性,name。
public class People {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在Java中,構造方法的方法體常常包含完全重複的程式碼:它把引數賦值給有著相同名稱的欄位。在Kotlin中,這種邏輯不用這麼多的樣板程式碼就可以表達。Kotlin中的People類.
class People(val name: String)
看起來不錯,不是嗎?如果你試過其他的一些現代JVM語言,你也許見過類似的事情。這種類通常被叫做值物件。
注意從Java到Kotlin的轉換過程中public修飾符消失了。在Kotlin中,public是預設的可見性,所以你能省略它。
2.1屬性
類的概念就是把資料和處理詩句的程式碼封裝成一個單一的實體。在Java中,資料儲存在欄位中,通常還是私有的。途觀想讓類的使用者訪問到資料,得提供訪問器方法:一個getter和一個setter方法。在Java中,欄位和其他訪問器的組合常常叫做屬性,而許多框架嚴重依賴這個概念。在Kotlin中,屬性是頭等的語言特性,完全代替了欄位和訪問器方法。在類中宣告一個屬性和宣告一個變數一樣:使用val和var關鍵字。宣告val的屬性是隻讀的,而var屬性是可變的。
class People{
val name: String = "hello world"
var isMarred: Boolean? = null
}
基本上,當你宣告屬性的時候,你就聲明瞭對應的訪問器(只讀屬性有一個getter,而可寫屬性既有getter也有setter)。訪問器的預設實現非常簡單:建立一個儲存值得欄位,以及返回值得getter和更新值得setter。但是如果需要,可以宣告自定義的訪問器,使用不同的邏輯來計算和更新屬性的值。
val people = People("zhangsan",true)
println(people.isMarred)
println(people.name)
現在,可以直接引用屬性,不再需要呼叫getter。邏輯沒有變化,但程式碼更簡潔了。
2.2自定義訪問器
/**
* 自定義訪問器
*/
class Rectangle(val height:Int,val width:Int){
val isSquare:Boolean
get() {
return height == width
}
}
屬性isSquare不需要欄位來儲存它的值。它只有一個自定義實現的getter。他的值是每次訪問屬性的時候計算數來的。這個get方法還可以這樣寫:
/**
* 自定義訪問器
*/
class Rectangle(val height:Int,val width:Int){
val isSquare: Boolean
get() = height == width
var isNotSquare: Boolean = false
set(v) { !isSquare}
}
3表示和處理選擇:列舉和“when”
3.1宣告列舉類
enum class Color{
RED,ORANGE,YELLOW,GREEN,BLUE,INDIGO,VIOLET
}
這是極少數Kotlin宣告比Java使用了更多關鍵字的例子:Kotlin用了enum class兩個關鍵字,而Java只有enum一個關鍵字。
宣告一個帶屬性的列舉類:
enum class Color(val r:Int,val g:Int,val b :Int){
RED(255,0,0),ORANGE(255,165,0),YELLOW(255,255,0),
GREEN(0,255,255),BLUE(0,0,255),INDIGO(75,0,130),
VIOLET(238,130,238);//這是Kotlin語法中唯一必須使用分號的地方:如果要在列舉類中定義任何方法,就要使用分號把列舉常量列表和方法 定義分開
fun rgb() = (r * 256 + g) * 256 + b
}
列舉常量用的宣告構造方法和屬性的語法與之前常規類一樣。
3.2使用when處理列舉類
在Java中可以用switch語句完成,而Kotlin對應的結構是when。和if相似,when是一個有返回值的表示式,因此可以寫一個直接返回when表示式的表示式體函式。
使用when來選擇正確的列舉值
fun getMnemonic(color:Color) = when(color){
Color.RED -> "Richard"
Color.ORANGE -> "of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
程式碼根據傳進來的color值找到對應的分支。和Java不一樣,你不需要再每個分支都寫上break語句(在Java中遺漏break通常會導致bug),如果匹配成功,只有對應的分支會執行。也可以把多個值合併到同一個分支,只需要使用逗號隔開這些值。在一個when分支上合併多個選項:
/**
* 在一個when分支上合併多個選項
*/
fun getWarmth(color:Color) = when(color){
Color.RED,Color.ORANGE,Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE,Color.INDIGO,Color.VIOLET -> "cold"
}
匯入列舉常量後不用限定詞就可以訪問
/**
* 匯入列舉常量後不用限定詞就可以直接訪問
* 匯入的語法:import com.houde.first.Color.*
*/
import com.houde.first.Color.*
fun getWarmth2(color: Color) = when(color){
RED , ORANGE , YELLOW -> "warm"
GREEN -> "neutral"
BLUE , INDIGO , VIOLET -> "cold"
}
3.3在when結構中使用任意物件
Kotlin中的when結構比Java中的switch強大的多。switch要求必須使用常量(列舉常量、字串或者數字字面值)作為分支條件,和它不一樣,when預習使用任何物件。在when分支中使用不同的物件:
/**
* when強大的地方在於可以是任何表示式,而咋Java中只能是字串,數字,列舉常量
* 這種呼叫一次就會生成一個set物件,
*/
fun mix(c1:Color,c2:Color) = when(setOf(c1,c2)){
setOf(RED,YELLOW) -> "ORANGE"
setOf(YELLOW,BLUE) -> "GREEN"
setOf(BLUE,VIOLET) -> "INDIGO"
else -> throw Exception("Dirty color")
}
如果顏色c1和c2分別為RED和YELLOW(反過來也可以),它們混合後的結果就是ORANGE,依次類推。這個地方使用了set比較這個調色盤。Kotlin標準函式庫中有一個setOf函式可以創建出一個Set,它會包含所有指定為函式實參的物件。set這種集合的條目順序並不重要,只要兩個set中包含一樣的條目,它們就是相等的。所以如果setOf(c1,c2)和setOf(RED,YELLOW)是相等的,意味著c1是RED和c2是YELLOW的時候相等,反過來也成立。
when表示式把它的實參依次和所有分支匹配,直到某個分支滿足條件。這裡setOf(c2,c2)被用來檢查是否和分支條件相等:先和setOf(RED,YELLOW)比較,然後是其他顏色的set,一個接一個。如果沒有其他的分支滿足條件,else分支會執行。
3.4使用不帶引數的when
/**
* 不帶引數的when
* 這種不用生成額外的物件,但是理解有點困難
*/
fun mixOptimized(c1: Color,c2: Color) = when{
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> "ORANGE"
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> "GREEN"
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> "INDIGO"
else -> throw Exception("Dirty color")
}
3.5智慧轉換:合併型別檢查和轉換
在Kotlin中,使用is檢查來判斷一個變數是否是某種型別。is檢查和Java中的instanceOf相似。但是在Java中,如果已經檢查過一個變數是某種型別並且要把它當做這種型別來訪問成員時,在instanceOf檢查之後還需要顯示的加上型別轉換。在Kotlin中,編譯器幫我們完成了這樣的工作。如果檢查過一個變數是某種型別,後面就不再需要轉換它,可以就把它當做你檢查過的型別使用。事實上編譯器為你執行類型別轉換,我們把這種行為成為智慧轉換。
/**
* 智慧轉換:合併型別檢查和轉換
*/
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
/**
* 在kotlin中,你要使用is檢查來判斷一個變數是否是某種型別
* is檢測跟Java中的instanceOf相似,但是is判斷為true,不用型別轉換就是轉換為判斷的型別
*/
fun eval(e:Expr):Int{
if(e is Num){
return e.value
}
if(e is Sum){
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("Unknown expression")
}
智慧轉換隻在變數經過is檢查且之後不再發生變化的情況下有效。當你對一個類的屬性進行智慧轉換的時候,就像這個例子中一樣,這個屬性必須是一個val屬性,而且不能有自定義的訪問器。否則,每次對屬性的訪問是否都能返回同樣的值將無從驗證。使用as關鍵字來表示到特定型別的顯示轉換:
val n = e as Num
3.6重構:用when代替if
/**
* 把if換成when
*/
fun eval2(e:Expr):Int = when(e){
is Num -> e.value
is Sum -> eval2(e.left) + eval2(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
如果if分支中只有一個表示式,花括號是可以省略的。如果if分支是一個程式碼塊,程式碼塊中的最後一個表示式會被作為結果返回。
/**
* 使用when代替if層疊
*/
fun eval3(e: Expr):Int =
when(e){
is Num ->
e.value
is Sum ->
eval3(e.right) + eval3(e.left)
else ->
throw IllegalArgumentException("Unknown expression")
}
when表示式並不僅限於檢查值是否相等,那是之前看到的。而這裡使用了另外一種when分支的形式,允許檢查when實參值型別。當分支邏輯太過複雜時,可以使用程式碼塊作為分支體
3.7程式碼塊作為if和when的分支
if和when都可以使用程式碼塊作為分支體。這種情況下,程式碼塊中的最後一個表示式就是結果。如果要在例子函式中加入日誌,可以在程式碼塊中實現它並像之前一樣返回最後的值。
/**
* 使用分支中含有混合操作的when
*/
fun evalWithLogging(e: Expr):Int = when(e){
is Num -> {
println("num : ${e.value}")
e.value
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum = ${left + right}")
left + right
}
else -> throw IllegalArgumentException("Unknown expression")
}
規則——程式碼塊中最後的表示式就是結果,在所有使用程式碼塊並期望得到一個結果的地方成立。
4.迭代事物:while迴圈和for迴圈
4.1while迴圈
Kotlin有while迴圈和do-while迴圈,它們的語法和Java中相應的迴圈沒有什麼區別。
4.2迭代數字:區間和數列
區間本質上就是兩個值之間的間隔,這兩個值通常是數字:一個起始值,一個結束值,使用 .. 運算子來表示區間:
val oneToTen = 1..10
注意Kotlin的區間是包含的或者閉合的,意味著第二個值始終是區間的一部分。用整數區間做最基本的事情就是迴圈迭代其中所有值。如果能迭代區間中所有的值,這樣的區間被稱作數列。例子是一個叫Fizz-Buzz的遊戲。編寫一個程式從1到100.當遇到數字為3的倍數的時候,點選“Fizz”替代數字,5的倍數用“Buzz”代替,既是3的倍數又是5的倍數點選“FizzBuzz”。
fun fizzBuzz(i:Int) = when{
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> "$i"
}
/**
* ..表示區間包左又包右
* in表示在集合或區間之內
* !in表示不在集合或區間之內
* until 表示區間包左不包右
* downTo 下降到
* step 步長 step 必須>0 否則拋異常java.lang.IllegalArgumentException: Step must be positive, was: -1.
*
*/
fun main(args: Array<String>) {
for(i in 1..100 step 2)
println(fizzBuzz(i))
for(i in 100 downTo 1 step 2)
println(fizzBuzz(i))
}
4.3迭代map
初始化並迭代map
fun main(args: Array<String>) {
val binaryReps = TreeMap<Char,String> ()
for(c in 'a'..'f'){
binaryReps[c] = Integer.toBinaryString(c.toInt())
}
for((letter,binary) in binaryReps){
println("$letter = $binary")
}
}
程式碼展示裡for迴圈允許展開迭代中的集合的元素(在這個例子中,展開map的鍵值對集合)。把展開的結果儲存到了兩個獨立的變數中:letter的鍵,binary的值。根據鍵來訪問和更新map的簡明語法。可以使用map[key]度氣質,並使用map[key] = value設定它們,而不是呼叫get和put。
可以用這樣展開的語法在迭代集合的同時跟蹤當前項的下標。不需要建立一個單獨的變數來儲存下標並手動增加它,程式碼如下:
val list = arrayListOf("10","11","12","13","14","15")
for((index,element) in list.withIndex()){
println("$index = $element")
}
結果
4.4使用in檢查集合和區間成員
使用in運算子來檢查一個值是否在區間中,或者它的逆運算,!in,來檢查這個值是否不在區間中。
val list = arrayListOf("10","11","12","13","14","15")
println("10 in list = ${"10" in list}")
println("10 in list = ${"10" !in list}")
in運算子和!in也使用於when表示式。
/**
* 用in 檢查作為when分支
*/
fun recognize(c: Char) = when(c){
in 'a'..'z',in 'A'..'Z' -> "It's a letter"
in '0'..'9' -> "It's a digit"
else -> "I don't known"
}
區間也不僅限於字元。假如有一個支援例項比較操作的任意類(實現了java.lang.Comparable介面),就能建立這種型別的物件的區間。如果是這樣的區間,並不能列舉出這個區間中的所有物件。想想這種情況:例如,是否可以列舉出java和kotlin之間的所有字串呢?答案是不能。但是仍然可以使用in運算子檢查一個其他的物件是否屬於這個區間:
println("Kotlin" in "Java".."Scale")
true
注意,這裡字串是按照字面表順序進行比較的,因為String就是這樣實現Comparable介面的。
in檢查同樣適用於集合:
println("Kotlin" in setOf("Java" , "Scale"))
false
5.Kotlin中的異常
Kotlin中的異常處理和Java以及其他語言的處理方式相似。一個函式可以正常結束,也可以在出現錯誤的情況下丟擲異常。方法的呼叫者能捕獲到這個異常並處理它;如果沒有被處理,異常會沿著呼叫棧再次丟擲。
Kotlin中異常處理語句的基本形式和Java類似,丟擲異常的方式也不例外:
if(number !in 0..100){ throw IllegalArgumentException("A percentage value must be between 0 and 100 : $number") } 和所有其他的類一樣,不必使用new關鍵字來建立異常例項。和Java不同的是,Kotlin中throw結構是一個表示式,能作為另一個表示式的一部分使用:val percentage = if (number in 0..100){ number }else{ throw IllegalArgumentException("A percentage value must be between 0 and 100 : $number") } 在這個例子中,如果條件滿足,程式的行為是正確的,而percentage變數會用number初始化。否則,異常將會被丟擲,而變數也不會初始化。
5.1 try catch 和finally
和Java一樣,使用帶有catch和finally子句的try結構來處理異常。會在下面的程式碼清單中看到這個結構,這個例子從給定的檔案中讀取一行,嘗試把它解析成一個數字,返回數字:或者當這一行不是有效數字時返回null。
/** * 像在Java中使用try */ fun readNumber(reader:BufferedReader): Int? { try { val line = reader.readLine() return Integer.parseInt(line) }catch (e:NumberFormatException){ return null }finally { reader.close() } } 和Java最大的區別是throws子句沒有出現在程式碼中:如果用Java來寫這個函式,你會顯示地在函式聲明後寫上throws IOException。你需要這樣做的原因是IOException是一個受檢異常。在Java中,這種異常必須顯示地處理這個函式的受檢異常。如果呼叫另外一個函式,需要處理這個函式的受檢異常,或者宣告自己的函式也丟擲這些異常。和其他JVM語言一樣,Kotlin並不區分受檢異常和未受檢異常。不用制定函式丟擲異常,而且可以處理也可以不處理異常。這種設計師基於Java中使用異常的實踐做出的決定。經驗顯示這些Java規則常常導致許多毫無意義的重新丟擲或者忽略異常的程式碼,而且這些規則不能總是保護你免受可能發生的錯誤。
5.3 try作為表示式
/** * try 作為表示式 */ fun readNumber2(reader:BufferedReader) { val number =try { Integer.parseInt(reader.readLine()) } catch (e: NumberFormatException) { null } finally { reader.close() } println(number) }
Kotlin中的try關鍵字就像if和when一樣,引入了一個表示式,可以把它的值賦給一個變數。不同於if,總是需要用花括號把語句主體括起來。和其他語句一樣,如果其他主體包含多個表示式,那麼整個try表示式的值就是最後一個表示式的值。
如果一個try程式碼塊執行一切正常,程式碼塊中最後一個表示式就是結果。如果捕獲到一個異常,相應catch程式碼塊中最後一個表示式就是結果。如上面的例子如果丟擲異常,捕獲到NumberFormatException,結果值就是null。