從Java到Kotlin(四)

陳子豪發表於2018-02-11

物件與泛型

目錄

  • 1.物件
    1.1 匿名類與物件
    1.2 靜態類成員與伴生物件
  • 2.泛型
    2.1 型變
    2.2 型別投影
    2.3 泛型函式
    2.4 泛型約束

1.物件

1.1 匿名類與物件表示式

Java中有匿名類這個概念,指的是在建立類時無需指定類的名字。在Kotlin中也有功能相似的“匿名類”,叫做物件,舉個例子:

Java匿名類

public class Login {

    private String userName;

    public Login(String userName) {
        this.userName = userName;
    }

    public void printlnUserName() {
        System.out.println(userName);
    }
}

public class JavaActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        printlnUserName(new Login("Czh") {
            @Override
            public void printlnUserName() {
                super.printlnUserName();
            }
        });
    }

    public void printlnUserName(Login login) {
        login.printlnUserName();
    }
}
複製程式碼

Kotlin實現上面的程式碼,要用關鍵字object建立一個繼承自某個(或某些)型別的匿名類的物件,如下所示:

class KotlinActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //object是一個物件,該物件繼承自上面的Login
        printlnUserName(object : Login("Czh") {
            override fun printlnUserName() {
            }    
        })
    }

    fun printlnUserName(login: Login) {
        login.printlnUserName()
    }
}
複製程式碼

物件object還可以實現介面,如下所示:

//View.OnClickListener是一個interface
button.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
    }
})
複製程式碼

物件和類一樣,只能有一個父類,但可以實現多個介面,多個超型別跟在冒號:後面用逗號,分隔。 如果只想建立一個物件,不繼承任何類,不實現任何介面,可以這樣寫:

fun foo(){
    val abc = object {
            var a = 1
            var b = 2
    }
    Toast.makeText(this, "${abc.a}${abc.b}", Toast.LENGTH_SHORT).show()
}
複製程式碼

執行程式碼,檢視結果:

從Java到Kotlin(四)
請注意,匿名物件可以用作只在本地和私有作用域中宣告的型別。如果你使用匿名物件作為公有函式的返回型別或者用作公有屬性的型別,那麼該函式或屬性的實際型別會是匿名物件宣告的超型別,如果你沒有宣告任何超型別,就會是 Any。在匿名物件中新增的成員將無法訪問。如下所示:

class User {
    // 私有函式,所以其返回型別是匿名物件型別
    private fun getUserName() = object {
        val userName = "Czh"
    }

    // 公有函式,所以其返回型別是 Any
    fun getAge() = object {
        val age = 22
    }

    fun get() {
        getUserName().userName
        //getAge().age //編譯錯誤
    }
}
複製程式碼
  • 內部類訪問作用域內的變數

就像 Java 匿名內部類一樣,Java可以用final宣告變數,使匿名內部類可以使用來自包含它的作用域的變數。如下所示:

final int age = 22;
printlnUserName(new Login() {
    @Override
    public void printlnUserName() {
        //因為age用final宣告,所以不能修改
        if (age == 22){
            return;
        }
  }
});
複製程式碼

而Kotlin在匿名物件中可以任意訪問或修改變數age,如下所示:

var age = 22
printlnUserName(object : Login() {
    override fun printlnUserName() {
        age = 23
        Toast.makeText(this@MainActivity, "$age", Toast.LENGTH_SHORT).show()
    }
})
複製程式碼

執行程式碼,檢視結果:

從Java到Kotlin(四)

1.2 伴生物件

Java中有靜態類成員,而Kotlin中沒有,要實現像靜態類成員的功能,就要用到伴生物件。

Java靜態成員:

class User {
    static User instance = new User();

    public void printlnUser() {
    }
}
//呼叫
User.instance.printlnUser()
複製程式碼

Kotlin類內部的物件宣告可以用 companion 關鍵字標記:

class User {
    companion object {
        var instance = User()
    }

    fun printlnUser() {
    }
}
//呼叫
User.instance.printlnUser()
複製程式碼

泛型

2.1型變

Java泛型

public class Box<T> {
    public T value;

    public Food(T t) {
        value = t;
    }
}

new Box<String>("123");
new Box<Integer>(1);
複製程式碼

對應的Kotlin泛型

class Box<T>(t: T) {
    var value = t
}
var box: Box<String> = Box("123")
var box2: Box<Int> = Box(123)
複製程式碼

可以看出Java跟Kotlin定義泛型的方法都是差不多的,不同的是Java中的泛型有萬用字元,而Kotlin沒有。舉個例子:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;//編譯錯誤
複製程式碼

Java編譯器不認為List是List的子類,所以編譯不通過。那我們換種寫法:

List<String> strings = new ArrayList<String>();
List<Object> objects = new ArrayList<Object>();
objects.addAll(strings);//編譯通過
複製程式碼

為什麼呼叫addAll()方法就能編譯通過呢,看一下他的原始碼:

boolean addAll(Collection<? extends E> c);
複製程式碼

Java泛型提供了問號?萬用字元,上面的<? extends E>代表此方法接受 E 或者 E 的 一些子型別物件的集合。所以可以通過addAll()方法把List賦值給List。

Kotlin的泛型沒有提供萬用字元,取而代之的是outin修飾符。先舉個例子:

//用out修飾T
class Box<out T> {
}
複製程式碼

從Java到Kotlin(四)
(紅色波浪線標記處為編譯錯誤)

//用in修飾T
class Box<in T> {
}
複製程式碼

從Java到Kotlin(四)
(紅色波浪線標記處為編譯錯誤)

對比上面兩段程式碼可以看出,用out來修飾T,只能消費T型別,不能返回T型別; 用in來修飾T,只能返回T型別,不能消費T型別。簡單來說就是 in 是消費者, out 是生產者。

####2.2 型別投影 上面說到了outin修飾符,如果我們不用他們來修飾泛型,會出現這種情況:

class Box<T> {
}
複製程式碼

從Java到Kotlin(四)
編譯不通過,因為Array對於型別T是不可變的,所以Box和Box誰也不是誰的子型別,所以編譯不通過。對於這種情況,我們還是可以用outin修飾符來解決,但不是用來修飾Box,如下所示:

fun test(strs: Box<Any>) {
    var objects: Box<in String> = strs
    //編譯通過
}

fun test2(strs: Box<String>) {
    var objects: Box<out Any> = strs
    //編譯通過
}
複製程式碼

上面的解決方式叫做型別投影,Box相當於 Java 的 Box<? extends Object>、Box相當於 Java 的 Box<? super Object>。

2.3 泛型函式

不僅類可以有型別引數。函式也可以有。型別引數要放在函式名稱之前:

fun <T> singletonList(item: T): List<T> {
    // ……
}

//呼叫
val l = singletonList<Int>(1)
singletonList(l)
複製程式碼

類似於Java的泛型方法:

public <T> T singletonList(T item) {
    // ……
}

//呼叫
singletonList(1);
複製程式碼

2.4 泛型約束

泛型約束能夠限制泛型引數允許使用的型別,如下所示:

Kotlin程式碼

fun <T : Comparable<T>> sort(list: List<T>) {
}

sort(1) //編譯錯誤
sort(listOf(1)) //編譯通過
複製程式碼

上述程式碼把泛型引數允許使用的型別限制為 List

Java中也有類似的泛型約束,對應的程式碼如下:

public static <T extends Comparable> List<T> sort(List<T> list){
}
複製程式碼

如果沒有指定泛型約束,Kotlin的泛型引數預設型別上界是Any,Java的泛型引數預設型別上界是Object


總結

本篇文章對比了Java匿名類、靜態類與Kotlin物件的寫法和兩種語言中對泛型的使用。相對來說,Kotlin還是在Java的基礎上作了一些改進,增加了一些語法糖,更靈活也更安全。

參考文獻:
Kotlin語言中文站、《Kotlin程式開發入門精要》

推薦閱讀:
從Java到Kotlin(一)為什麼使用Kotlin
從Java到Kotlin(二)基本語法
從Java到Kotlin(三)類和介面
從Java到Kotlin(四)物件與泛型
從Java到Kotlin(五)函式與Lambda表示式
從Java到Kotlin(六)擴充套件與委託
從Java到Kotlin(七)反射和註解
從Java到Kotlin(八)Kotlin的其他技術
Kotlin學習資料總彙


更多精彩文章請掃描下方二維碼關注微信公眾號"AndroidCzh":這裡將長期為您分享原創文章、Android開發經驗等! QQ交流群: 705929135

從Java到Kotlin(四)

相關文章