Kotlin 分享 筆記(二)
上次,我們對kotlin進行了整體的介紹,並且對kotlin基本的語法如變量,函數等都有了大致的瞭解,這次我們將會了解下kotlin對類與對象的支持,kotlin是如何使用註解,反射等相關工具的。
Class & Object
構造函數
在kotlin中,我們定義類是非常方便的
final class Demo public constructor(name: String, address: String) {
init {
val name = name
}
val address = address
}
- 在kotlin中,class或interface 默認是final類型的,即不可以被繼承或實現的
- kotlin中,構造函數可以存在一個主構造函數和多個次構造函數,如果類中存在主構造函數,則次構造函數必須直接或者間接的委託給主構造函數
- 主構造函數中的參數只能用在初始化塊
init
和類體中 - 如果一個類沒有聲明主構造函數活着次構造函數,將會默認提供一個不帶參數的主構造函數
- 如果在主構造函數上存在描述符,將必須顯示寫出
constructor
關鍵字 - 類的構造函數默認是public修飾的
- 如果主構造函數中聲明的參數使用
var / val
關鍵字,則該參數將成爲該類的屬性
經過上面幾點,我們可以將這個Demo
類改寫成��代碼
class Demo(val name: String, val address: String)
⚠️ 次構造函數的參數不允許用 val / var
關鍵字描述, 因爲只是型參, 如��代碼是錯誤的
class Demo{
constructor(val name:String){ //這是次構造函數,次構造函數也要用constructor關鍵字
// 編譯錯誤,不可以用`val`
}
}
創建類的實例 val demo = Demo("gc","china")
kotlin 去掉了new 關鍵字
屬性和方法
與java一樣,我們可以在類中定義方法和屬性,同時他們都是默認final關鍵字描述的,即不可繼承的
class Demo(name: String) {
final val name = name // 可以不寫final
final fun function(): String = name // 可以不寫final
}
與java不一樣的是,kotlin爲我們默認提供屬性的getter,setter
val demo = Demo("gc")
demo.name // 即默認調用getter
當然,我們可以爲屬性自定義getter, setter val 定義的屬性不允許自定義setter方法
class Demo(name: String) {
var name: String = name
get() = "hello ${field}"
set(value) {
field = "hello ${value}"
}
}
這裏要注意幾個地方:
field
被稱爲該屬性的幕後字段,爲什麼我們在get方法中返回的是field
而不是name
呢?因爲如果我們返回name
,則又會調用name
屬性的get方法,如此遞歸到棧溢出,field
僅僅是存儲name
的值,不包裝get/set- set中的value參數是自定義的,不過通常用
value
, set中參數沒有類型,get中沒有返回值,因爲這些都可以在屬性上得知
接口
kotlin 中的接口與java相似,不過kotlin中提供了更豐富的接口功能,它允許接口定義屬性,但接口中的屬性不能保存狀態,也不應該保存狀態,所以接口中的屬性不能初始化且不能使用背後字段
interface Demo {
val name: String
val address: String
get() = "hello"
fun function1(): String {
return address
}
fun function2()
}
抽象類
kotlin中的抽象類與java一致
abstract class DemoII {
// 與java一致
}
數據類
kotlin 提供了數據類的概念,簡化代碼(與java中@Data的作用一致)
data class Demo(val name: String, val address: String)
fun main(args: Array<String>) {
val demo = Demo("gc", "china")
//生成toString
println(demo.toString())
//生成component
val (name, address) = demo
println("name: ${name}, address: ${address}")
//生成copy 深度拷貝
val demoI = demo.copy(name = "gc2")
println(demoI.toString())
}
/* out
Demo(name=gc, address=china)
name: gc, address: china
Demo(name=gc2, address=china)
*/
嵌套類
class Demo{
private val name:String = "gc"
class Demo1{
println(name) //ERROR
fun foo() = "gc"
}
}
val demo1 = Demo.Demo1().foo()
嵌套類中,裏面的類不可以訪問外面的變量或方法
內部類
class Demo{
private val name:String = "gc"
inner class Demo1{
println(name) //ok
fun foo() = "gc"
}
}
val demo1 = Demo().Demo1().foo() //這裏與嵌套類也不一樣
枚舉類
enum class Demo(val name:String){
GC("gc")
}
伴生對象
因爲kotlin中沒有static關鍵字, 如果想要實現對應的效果,可以使用半生對象,初始化時機也與static一致
class Demo{
companion object Factory{ //可以省略對象名 Factory
fun create(): Demo = Demo()
}
}
// val demo = Demo.create()
單例類
object DemoUtil {
fun isValidId(id: Long?) = id?.let { id > 0 } ?: false
}
// DemoUtil.isValidId(10)
在kotlin中,這種定義也叫做對象聲明,當且僅當使用這個對象中的方法或者屬性時候初始化(單例模式中的懶漢模式)
繼承
kotlin中的繼承與java最大的區別是 koltin支持多繼承
- 如果父類中沒有構造函數(包括主構造和次構造),則子類中繼承的時候調用父類的空構造函數
open class Demo {
}
class DemoI : Demo() {
}
當然,接口沒有構造函數,所以可以這麼寫
interface Demo {
}
class DemoI : Demo {
}
- 如果父類中存在構造函數,且子類中不存在主構造函數,則次構造函數必須顯示使用
super
關鍵字
class Demo(name:String){
}
class DemoI : Demo{
constructor(name:String, address:String) : super(name){
}
}
- 子類覆蓋父類中的方法必須用
override
關鍵字顯示聲明
open class Demo {
open fun function() = "hello world"
}
class DemoI : Demo() {
override fun function() = "hahaha"
}
屬性與方法 同樣可以被繼承, 但要注意的是 一個 val 標註的屬性不可以被 var 標註的屬性重寫,反之可以
kotlin 中多繼承是被允許的,如果想要調用某一父類指定的方法,可以使用super<*> 關鍵字
open class DemoI {
open fun function() = "hello world"
}
open class DemoII {
open fun function() = "hello china"
}
class Demo : DemoI(), DemoII(){
override fun function() {
return super<DemoI>.function() + super<DemoII>.function()
}
}
組合
class Demo {
fun function(){
println("hello world")
}
}
class DemoI(val demo:Demo){
fun function(){
demo.function()
}
}
委託
kotlin 原聲支持類,方法,屬性委託, 這裏暫時只會提到類委託
interface Projector {
fun show(content:String)
}
class SonyProjector: Projector {
override fun show(content:String){
println ("sony show ${content}")
}
}
class Computor (projector: Projector): Projector by projector // 這裏發生了委託
fun main(args: Array<String>) {
val sonyProjector: Projector = SonyProjector()
val computor: Computor = Computor(sonyProjector)
computor.show("hello worlds")
println(Computor::class)
}
繼承,組合,委託
對於這三種類與類之間的組裝形式,網上有很多人去爭辯哪個更優於哪個,我個人認爲三者之間沒有優劣,僅僅是場景不同的產物。
繼承很明顯的存在於親子之間的關係,比如父親和孩子,鳥與麻雀之間的關係,這種是無論是具有血緣關係的直系親屬還是生物之間的進化,都可以用繼承關係描述;組合則強調的是整體與部分的關係,比如車和輪胎,車和車坐等,大物品是由小物品組裝起來的,用組合相對較好;而委託則更注重行爲上,比如電腦與投影儀,電腦是個體,投影儀也是個體,兩個既沒有進化關係,也沒有組裝關係(並不是缺了投影儀,電腦不能工作),電腦只會把顯示任務委託給投影儀。
其實代碼中的世界與人類世界一樣,比如繼承,組裝,委託,比如工廠模式,訂閱者模式,這些無不是在人類社會進化總結出來最終映射到代碼世界的, 所以說,人類世界中的規律如果很好的應用在寫代碼上,也就不用費盡心機的去研究什麼解耦了,因爲現在這個社會便是最好解耦的結果。
泛型
koltin 對泛型的支持比java更爲方便,雖然兩者對泛型都是做類型擦除操作,但kotlin無論是在語法上還是在語義上都比java更加人性化
爲什麼要存在類型擦除
原因也和大多數的Java讓人不爽的點一樣——兼容性。由於泛型並不是從Java誕生就存在的一個特性,而是等到SE5才被加入的,所以爲了兼容之前並未使用泛型的類庫和代碼,不得不讓編譯器擦除掉代碼中有關於泛型類型信息的部分,這樣最後生成出來的代碼其實是『泛型無關』的,我們使用別人的代碼或者類庫時也就不需要關心對方代碼是否已經『泛化』,反之亦然。
在編譯器層面做的這件事(擦除具體的類型信息),使得Java的泛型先天都存在一個讓人非常難受的缺點:
在泛型代碼內部,無法獲得任何有關泛型參數類型的信息
容器類的協變與逆變
無論是在java還是在kotlin中, Object obj = new String("gc")
是合法的,但是String str = new Object()
是非法的,也就是說用父類去裝子類的實例是沒有問題的,但反之卻不行。且這是可以被理解的。
在java中, ArrayList<Object> l = new ArrayList<String>()
是不被編譯通過的,因爲 `ArrayList<Object>
並非 ArrayList<String>
的父類。
但這便不被某些人理解, ��代碼來表明這爲什麼不被允許:
ArrayList<Object> objects = new ArrayList<String>();
for (Object object : objects) {
object = new Integer(1);
}
如果 ArrayList<Object>
是 ArrayList<String>
的父類,就會出現上面的代碼而且被編譯通過,這是很危險的事情, 且與數組協變造成的危險一樣大。
所以在java中,我們要這樣來解決上面的問題, 但是要以犧牲修改容器內容爲代價。
ArrayList<? extends Object> objects = new ArrayList<String>();
objects.add(new String("gc")); // 對其寫入的時候異常
如果想要一個對應 寫操作 的協變
ArrayList<? supper String> objects = new ArrayList<Object>();
objects.add(new String("gc"));
String s = objects.get(0);// 但讀卻同樣被犧牲
因爲java的類型擦除,java在編譯階段對泛型做了強有力的校驗,而這種校驗導致我們不得不加入通配符並且符合操作規格來進行代碼編寫
class A<T> {
private T l = null;
public A(T l) {
this.l = l;
}
public T getContent() {
return l;
}
}
public class Main {
public static void main(String[] args) {
/*
* 這裏我們寫的類雖然只進行的讀操作,但是在調用的時候也不能將 A<String> 附值給 A<Object>
*/
A<? extends Object> a = new A<String>("gc");
}
}
我們看下kotlin的代碼:
interface Source<out T>
fun function(arg: Source<String>) {
val a : Source<Any> = arg
}
kotlin 在泛型上對於類型擦除的檢查與java做了不同方向上的處理,koltin 去除了 在定義變量的時候使用通配符 ?
的方法,增加了聲明處泛型的方法,如果在使用在變量的時候僅僅是安全的get操作,那麼可以在改變量的聲明處增加out
關鍵字處理,由此得到的好處是 Source<Any>
是Source<String>
的父類。
註解 & 反射
註解
kotlin 中註解 與 java 註解100%兼容, 語法上也一致
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy
使用註解也與java類似
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
反射
kotlin 的反射和java 的也很類似,不同的是 kotlin::class 獲取的是kotlin提供的類 KClass 類型,java.getClass() 獲取的是java的類,當然 kotlin::class.java 也能獲取到同樣的結果
其他的知識點同java的知識點, 我們看段個小demo
這個demo的要實現的功能:
- 讀取項目根目錄下的文件內容,將文件中的內容通過一定規格映射給對應的類
註解類:
@Target(AnnotationTarget.PROPERTY)
@Retention
annotation class MyAnno(val fileName: String = "application.properties", val propertyName: String)
解析類:
class AnnoExpres(val obj: Any) {
init {
}
fun expression() {
val clazz = obj.javaClass.kotlin
clazz.memberProperties.forEach { property ->
val mutableProp = try {
property as KMutableProperty<*>
} catch (e: Exception) {
null
} ?: return@forEach
val propertyName = mutableProp.name
val propertyType = mutableProp.returnType.toString().removePrefix("kotlin.")
mutableProp.annotations.forEach {
if (it is MyAnno) {
when (propertyType) {
"String" -> mutableProp.setter.call(obj, getFileContent(it.fileName)[propertyName] ?: "")
"Int" -> mutableProp.setter.call(obj, getFileContent(it.fileName)[propertyName]?.toInt() ?: 0)
"Boolean" -> mutableProp.setter.call(obj, getFileContent(it.fileName)[propertyName]?.toBoolean() ?: false)
}
}
}
}
}
private fun getFileContent(fileName: String): Map<String, String> {
val lines = InputStreamReader(AnnoExpres::class.java.classLoader.getResourceAsStream(fileName)).readLines()
return lines.filter {
!StringUtils.isEmpty(it)
}.filter {
!it.startsWith("#")
}.map {
it.split("=")[0] to if (it.split("=").size > 1) it.split("=")[1] else ""
}.toMap()
}
model類:
/*
* 通過註解,我們知道 讀取application.properties文件,並且將對應的字段映射到對應的屬性上
*/
class User {
@MyAnno(propertyName = "name") var name: String = ""
@MyAnno(propertyName = "age") var age: Int = 0
@MyAnno(propertyName = "man") var man: Boolean = false
override fun toString(): String {
return "User(name='$name', age=$age, man='$man')"
}
}
Main:
fun main(args: Array<String>) {
val user = User()
val anno = AnnoExpres(user)
anno.expression()
println(user)
}
application.properties 文件內容:
# test
name="zhangjie"
age=23
man=true
下面是運行結果
User(name='"zhangjie"', age=23, man='true')
總結
隨着對kotlin的深入, 越來越感覺kotlin像是java的語法糖,也可能是因爲初學者的原因,對kotlin的設計理念還不瞭解,不過既然java與kotlin都可以起到相同的作用,實現相同的東西,就算是語法糖,我們爲什麼不適用更方便簡潔的語法糖呢? 我也打算在日常代碼中加入對kotlin的使用,但也不會放棄對java的深入,因爲她們都跑在JVM上~
下面我們看一段用kotlin寫出來的代碼,只看外觀和寫作感受來講的話,不覺得很爽麼?
import com.example.html.* // 具體的聲明參見下文
fun result(args: Array<String>) =
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
// 一個元素, 指定了屬性, 還指定了其中的文本內容
a(href = "http://kotlinlang.org") {+"Kotlin"}
// 混合內容
p {
+"This is some"
b {+"mixed"}
+"text. For more see the"
a(href = "http://kotlinlang.org") {+"Kotlin"}
+"project"
}
p {+"some text"}
// 由程序生成的內容
p {
for (arg in args)
+arg
}
}
}