主要是kotlin上函數的特色:命名參數、默認參數值、頂層函數和屬性、擴展方法和擴展屬性(本質上是靜態函數高級語法糖),和能消除重複代碼(DRY)的局部函數。數據庫
讓咱們從一個經常使用例子出發,java集合都有默認的toString()
方法,但它的輸出是固定格式化的,有時並非你所須要的([1,2,3]
),如要自定義字符串的前綴、後綴,和分隔符時,通常定義個方法,並傳入參數:編程
fun <T> joinToString(collection: Collection<T>,separator:String,prefix:String,postfix:String):String{
val result = StringBuilder(prefix)
for ((index,element) in collection.withIndex()){
if (index>0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
複製代碼
使用:json
val list = listOf("kotlin","java")
println(joinToString(list,",","{","}")) // {kotlin,java}
複製代碼
關注的第一個問題就是函數的可讀性,如上面的使用joinToString(list,",","{","}")
,難以看出這些string對應的是什麼參數(雖然能夠藉助ide)。kotlin能夠優雅的調用,便可以顯式的標明一些參數的名稱,如joinToString(list,",",prefix = "{",postfix = "}")
,直接在調用時指明瞭prefix
和postfix
,能清晰明瞭的分辨出來。bash
注: 爲避免混淆,當指明瞭一個參數名稱後,那它以後的全部參數都要顯式指明名稱app
java函數的另外一個廣泛存在的問題是,一些類的重載函數實在太多了(如java.lang.Thread便有8個構造方法),致使參數名和類型被不斷重複。ide
kotlin則使用了默認參數值,能夠在聲明函數的時候,指定參數的默認值,就能夠避免重複建立函數。如上面的joinToString
函數,大多數狀況下,能夠不加前綴或者後綴並用逗號分隔,因此把它們設爲默認值:函數
//只改變了參數聲明
fun <T> joinToString(collection: Collection<T>,separator:String=",",prefix:String="",postfix:String=""):String{
val result = StringBuilder(prefix)
for ((index,element) in collection.withIndex()){
if (index>0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
複製代碼
能夠像java多重重載函數同樣地調用:工具
println(joinToString(list)) // 打印:kotlin,java
println(joinToString(list,";")) // 打印:kotlin;java
println(joinToString(list,prefix = "{",postfix = "}")) // 打印:kotlin;java
複製代碼
java中,全部的代碼都必須寫成類的函數。有時存在一個基本的對象,但你不想經過實例函數來添加操做,讓它的API繼續膨脹,結果就是,會把這些函數寫成靜態,並交由不包含任何狀態和實例函數的類保管,如JDK中的Collections
,或者本身代碼中,一些以Util
做爲後綴的工具類。post
kotlin能夠直接把函數或屬性放在代碼文件頂層,而不用從屬於任何的類。(依然是包內成員,若是須要從包外訪問它,則須要import(可使用as更改導入的名字),但再也不須要額外包一層。
kotlin屬性也能夠放在文件頂層,相似java靜態字段; 若想像java的public final static
同樣聲明一個常量,kotlin可使用const val
修飾
使用方式在下面給出↓
kotlin的一大特點是,能夠平滑地與現有代碼集成。擴展函數能夠在類的外面定義一個類的成員函數,如咱們添加一個方法擴展String
類型,計算一個字符串的最後一個字符並返回:(像成員函數同樣地調用)
import...
fun String.lastChar():Char = this.get(length-1) //this能夠省略
...
fun main(){
//像成員函數同樣地調用
println("kotlin".lastChar()) // 打印: n
}
複製代碼
擴展函數的聲明,與普通函數區別就是,把你要擴展的類或者接口名稱,反到即將添加的函數名簽名,這個類被稱爲接收者類型,如例子中的String
;用來調用這個擴展函數的那個對象,被叫作 接收者對象,如例子中的「kotlin」
在擴展函數中,能夠直接訪問被擴展類或接口的其餘方法和屬性(如例子中的String.get方法),就好像是在這個類中定義同樣。
對於開始的字符串例子,如今能夠這麼定義使用:
fun <T> Collection<T>.joinToString(separator:String=",",prefix:String="",postfix:String=""):String{
val result = StringBuilder(prefix)
for ((index,element) in withIndex()){
if (index>0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
...
val list = listOf("kotlin","java")
println(list.joinToString(prefix = "{",postfix = "}")) //kotlin;java
複製代碼
擴展函數本質上,是java的靜態函數的高效語法糖,最終是被轉爲一個接收該對象類型的靜態函數,所以是不能被繼承重寫的。
注: 當類的成員函數和擴展函數有相同簽名時,成員函數會優先使用
和擴展函數同樣,kotlin也支持擴展屬性,使用相似,如:
var StringBuilder.lastChar:Char
get() = get(length -1)
set(value) {
this.setCharAt(length-1,value)
}
...
val sb = StringBuilder("kotlin?")
println(sb.lastChar) // 打印: ?
sb.lastChar = '!'
println(sb) // 打印: kotlin!
複製代碼
注: 擴展屬性是沒有支持字段存儲的
提升代碼質量標準之一:不要重複你本身的代碼(DRY)。kotlin提供了一個整潔的方案,局部函數:能夠在函數中嵌套函數。
讓咱們來看怎麼用局部函數解決常見的代碼重複問題,例子中,saveUser函數用於將user信息存到數據庫中,並確保user對象含有有效數據
class User(val id:Int,val name:String,val address:String)
複製代碼
fun saveUser(user:User){
if (user.name.isEmpty()){
throw IllegalArgumentException("Can't save user ${user.id} :empty Name")
}
if (user.address.isEmpty()){
throw IllegalArgumentException("Can't save user ${user.id} :empty Address")
}
//保存到數據庫中
...
}
複製代碼
函數saveUser中,存在重複的字段檢查,當檢查字段增多,代碼會顯得特別臃腫,可利用局部函數提取重複代碼:
fun saveUser(user:User){
//可省略局部函數的user參數
fun validate(user: User,value:String,fieldName:String){
if (value.isEmpty()){
throw IllegalArgumentException("Can't save user ${user.id} :empty $fieldName")
}
}
validate(user,value = user.name,fieldName = "Name")
validate(user,value = user.address,fieldName = "Address")
//保存到數據庫中
}
複製代碼
聲明瞭一個局部函數validate提取重複的檢查邏輯,因局部函數能夠訪問到所在函數的全部參數和變量,因此,能夠去掉冗餘的User參數:
fun saveUser(user:User){
//去掉冗餘的User參數
fun validate(value:String,fieldName:String){
if (value.isEmpty()){
throw IllegalArgumentException("Can't save user ${user.id} :empty $fieldName")
}
}
validate(value = user.name,fieldName = "Name")
validate(value = user.address,fieldName = "Address")
//保存到數據庫中
}
複製代碼
繼續改進,能夠將user的驗證擴展成擴展函數
fun User.validateBeforeSave(){
fun validate(value:String,fieldName:String){
if (value.isEmpty()){
throw IllegalArgumentException("Can't save user $id :empty $fieldName")
}
}
validate(value = name,fieldName = "Name")
validate(value = address,fieldName = "Address")
}
fun saveUser(user:User){
user.validateBeforeSave()
//保存到數據庫中
}
複製代碼
再次代表,擴展函數能夠很大程度優化代碼。
注:擴展函數也能夠被聲明爲局部函數,但通常不建議多層嵌套,因深度嵌套的局部函數每每會讓人太費解
kotlin接口和類的實現與java仍是有一點區別的,如接口能夠包含屬性聲明;kotlin的聲明默認是public final 的;此外,嵌套類默認不是內部類,即靜態的,沒有包含懟外部類的隱式引用
kotlin中的接口與java8中的類似,能夠包含抽象方法,和非抽象方法的實現(與java8中默認方法相似);同時能夠有屬性聲明,但沒有包含任何狀態,即沒有支持字段來保存(後面有講)
接口聲明:
interface Clickable{
fun click() //抽象方法
fun showOff() = println("I'm clickable") //帶默認實現的方法
}
複製代碼
kotlin在類名後面用冒號來代替java的extends和implements關鍵字。和java同樣,一個類能夠實現任意多個接口,但只能繼承一個類
kotlin用override修飾符標註被重寫的方法和屬性,同時,override修飾符是強制要求的
class Button:Clickable{
override fun click() {
println("button clicked")
}
}
複製代碼
若同時實現兩個接口,且兩個接口含有相同的函數簽名,且都有默認實現,則kotlin會強制要求提供你本身的實現
interface Clickable{
fun click()
fun showOff() = println("I'm clickable")
}
interface Focusable{
fun focus()
fun showOff() = println("I'm focusable")
}
class Button:Clickable,Focusable{
override fun click() {
println("button clicked")
}
override fun focus() {
println("button clicked")
}
override fun showOff() {
//使用尖括號加父類名字的「super」代表了你想要調用哪一個父類方法
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
複製代碼
調用父類實現,kotlin也使用了關鍵字super
,使用尖括號加父類名字的「super」代表了你想要調用哪一個父類方法
kotlin中,類,方法和屬性默認是final的,即不可重寫。若是要容許建立子類,須要使用open修飾符來標示這個類,屬性或方法也要添加。
open class RichButton:Clickable{ //這個類是open的,便可繼承的
fun disable(){} //這個函數是final的,子類不可重寫
open fun animate(){} //這個函數是open的,子類可重寫
override fun click() {} //重寫的成員默認是open的,除非顯式標註 final
}
複製代碼
注: 重寫的成員默認是open的,除非顯式標註爲final
同java同樣,能夠將一個類聲明爲abstract,這種類能夠有一些沒被實現而且須要在子類實現的抽象成員(用abstract修飾),抽象成員不能有實現,與java基本一致。
注: abstract始終是open的,同時,接口也始終是open的,都不能聲明爲final。
修飾符 | 相關成員 | 批註 |
---|---|---|
final | 不能被重寫 | 類中成員默認使用 |
open | 能夠被重寫 | 須要明確標示 |
abstract | 必須被重寫 | 只能在抽象類中使用;抽象成員不能有實現 |
override | 重寫父類或接口中的成員 | 若是沒有使用final明確代表,重寫的成員默認是open的 |
與java可見性區別:
internal
,表示「只在模塊內部可見」修飾符 | 類成員 | 頂層聲明 |
---|---|---|
public(默認) | 全部地方可見 | 全部地方可見 |
internal | 模塊中可見 | 模塊中可見 |
protected | 類和子類中可見 | ---- |
private | 類中可見 | 文件中可見 |
kotlin中,默認嵌套類不能訪問外部類實例,即至關於靜態內部類,沒有隱式擁有外部類的實例。若是要把變成一個內部類來持有一個外部類的引用的話,須要使用inner
修飾符。在kotlin中引用外部類實例語法也與java不一樣,需使用this@Outer
(java是Outer.this)
class Outer{
inner class Inner{ //聲明爲inner
fun getOuterReference():Outer = this@Outer //引用外部類實例
}
}
複製代碼
密封類:包含有限數量的類的繼承結構。
在使用when
表達式的時候,老是提供一個else
分支很不方便,若是處理的是sealed類的子類,則能夠再也不須要提供默認分支,且當sealed添加一個子類時,有返回值的when表達式會編譯失敗,並告訴你哪裏必須修改
sealed class Expr{
class Num:Expr() //括號構造方法下面講解
class Sum:Expr()
class Plus:Expr()
}
data class Out(val s:String) :Expr() //後續添加的子類
fun eval(e:Expr):String{
when(e){
is Expr.Num -> return "num"
is Expr.Sum -> return "Sum"
is Expr.Plus -> return "plus"
is Out-> return "plus" //必須把後續添加的子類放到分支裏,不然報錯
}
}
複製代碼
注: sealed修飾的這個類始終是open的。且不能用於聲明sealed接口
類的構造方法區分了主構造方法和從構造方法。同時也容許在初始化語句塊中添加額外的初始化邏輯
一個簡單類的聲明:
class User(val nickname:String)
複製代碼
這段被括號圍起來的語句塊就叫作主構造方法,主要兩個目的,代表構造方法參數和使用這些參數初始化的屬性。完成一樣事情最明確代碼以下:
class User constructor(_nickname:String){ //帶一個參數的主構造方法
val nickname:String //屬性
init { //初始化代碼塊
nickname = _nickname
}
}
複製代碼
class User(_nickname:String){
val nickname = _nickname
}
複製代碼
class User(val nickname:String = "John")
複製代碼
對於子類,須要初始化父類,可在類聲明中使用父類的構造方法:
open class Button
class RadioButton:Button()
複製代碼
這就是爲何在父類名稱後面還須要一個空的括號;接口沒有構造方法,因此不用加括號
open class View{
constructor(context:Context?){ //從構造方法
println("this is view1")
}
constructor(context: Context?,attr:AttributeSet?){
println("this is view2")
}
}
複製代碼
class MyView:View{
constructor(context: Context?):this(context,null){ //this委託
println("this is MyView1")
}
constructor(context: Context?,attr: AttributeSet?):super(context,attr){ //初始化基類
println("this is MyView2")
}
}
複製代碼
interface User{
val nickName:String
}
複製代碼
重寫屬性有三種方式
class User1(override val nickName: String) :User //有支持字段存儲
class User2(val email:String):User{
override val nickName: String //沒有支持字段存儲
get() = email.substringBefore("@")
}
class User3(val id:Int):User{
override val nickName = "$id" //有支持字段存儲
}
複製代碼
interface User{
val email:String
val nickName:String //並無持有支持字段和狀態
get() = email.substringBefore("@")
}
複製代碼
屬性能夠自定義訪問器getter/setter以提供被訪問或修改時額外邏輯。假設須要在修改時輸出日誌:
class User(val name:String){
var address = "unSpecified"
set(value) {
println("address was changed")
field = value // filed表示支持的字段值
}
}
複製代碼
field標識符在getter/setter表示支持字段的值,在getter中,只能讀取值,在setter中,既能讀取也能修改。
注: 能夠只修改一個訪問器,另外一個會自動用默認的實現
4 有無支持字段的判斷:訪問屬性的方式不依賴因而否含有支持字段。若是顯式引用或者使用默認訪問器實現,編譯器會爲屬性生產支持字段。若是提供了一個自定義的訪問器而且沒有使用field(若是是val,就是getter,若是是var,就是getter和setter),支持字段就不會被呈現出來。
5 訪問器的可見性默認與屬性的可見性相同,但能夠修改,如:
class User(val name:String){
var address = "unSpecified"
private set //修改setter的可見性爲private
}
複製代碼
data class Person(val name:String){
object NameComparator:Comparator<Person>{ //單一實例
override fun compare(p0: Person?, p1: Person?): Int {
if (p0 ==null || p1 == null)
return 0
return p0.name.compareTo(p1.name)
}
}
}
...
//使用
Person.NameComparator.compare(Person("John"),Person("Cap"))
複製代碼
kotlin中的類不能擁有靜態成員,java的static關鍵字並非kotlin語言的一部分。做爲替代,通常使用頂層函數和對象聲明。大多數狀況下推薦頂層函數,但頂層函數沒法訪問private成員,像工廠方法這種例子就得使用對象聲明。
class A{
companion object{
fun bar() = println("called")
}
}
...
A.bar() // 輸出 called,像靜態方法同樣調用
複製代碼
注:本質上也是被編程成了經過靜態字段持有的的單一實例類
class User private constructor(val name: String){
companion object{
fun newFaceBookUser(email:String) = User(email.substringBefore('@'))
fun newMarvelUser() = User("Cap")
}
}
複製代碼
能夠經過類名來調用companion object的方法
val facebook = User.newFaceBookUser("xxx@163.com")
val marvel = User.newMarvelUser()
複製代碼
class Person(val name:String){
companion object Loader{
fun fromJSON(jsonText:String):Person = ...
}
}
複製代碼
則能夠經過兩種方式使用
val person = person.Loader.fromJSON(...)
val person2 = person.fromJSON(...)
複製代碼
interface JSONFactory<T>{
fun fromJSON(json:String):T
}
class Hero private constructor(val heroName:String){
companion object Loader:JSONFactory<Hero>{
override fun fromJSON(json: String): Hero {
return Hero("IronMan")
}
}
}
fun <T> loadFromJson(jsonFactory: JSONFactory<T>) = jsonFactory.fromJSON("John")
//使用,直接將包含它的類的名字做爲實現了改接口的對象實例來使用
loadFromJson(Hero)
複製代碼
class Person(val name:String){
companion object{
}
}
//擴展函數顯式指明默認名字companion
fun Person.companion.fromJSON(json:String):Person{
...
}
複製代碼
使用時,仍能夠直接用包含的類名來調用
val p = Person.fromJSON(json)
複製代碼
object關鍵字不只能用來聲明單例對象,還能用來聲明匿名對象。
與java匿名對象不一樣的是:
class MainActivity : AppCompatActivity() {
val c:String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var clickCount = 0
val view :View= findViewById(R.id.action)
//這裏的匿名對象表達式能夠被lambda代替
view.setOnClickListener(object :View.OnClickListener{
override fun onClick(p0: View?) {
clickCount ++ //不須要final
println(c)
}
})
}
}
複製代碼
匿名對象也能夠直接存儲到一個變量中
val listener = object :View.OnClickListener{
override fun onClick(p0: View?) {
...
}
}
複製代碼