Kotlin極簡教程:第10章 Kotlin與Java互操作

Kotlin is 100% interoperable with Java™ and Android™

在前面的章節中,我們已經學習了Kotlin的基礎語法、類型系統、泛型與集合類、面向對象與函數式編程等主題,在上一章中我們還看到了Kotlin提供的輕量級併發編程模型:協程的相關內容。

從本章開始到在後面的章節中,我們將進入工程代碼的實戰。我們將在後面分別介紹Kotlin集成SpringBoot開發服務端Web項目、使用Kotlin開發Android項目,以及使用Kotlin來寫前端JavaScript代碼的等主題。

Kotlin 的競爭優勢在於它並不是完全隔離於 Java 語言。它基本上是可與 Java 100% 互操作的。這樣,Kotlin就可以站在整個Java生態巨人的肩上,向着更遠大的前程前進。

本章我們就讓我們一起來學習下Kotlin與Java的互操作。

Kotlin 調用 Java示例

Kotlin 很像 Java。它長得不像 Clojure 或者 Scala 那麼奇怪(承認現實把,這兩種語言就是挺奇怪的)。所以我們學 Kotlin 應該很快。這門語言顯然就是寫給 Java 開發者來用的。

Kotlin 在設計之初就考慮了與 Java 的互操作性。我們可以從 Kotlin 中自然地調用現存的 Java 代碼。例如,下面是一個Kotlin調用Java中的Okhttp庫的代碼:

package com.easy.kotlin

import okhttp3.*
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit

object OkhttpUtils {
    fun get(url: String): String? {
        var result: String? = ""
        val okhttp = OkHttpClient.Builder()
                .connectTimeout(1, TimeUnit.HOURS)
                .readTimeout(1, TimeUnit.HOURS)
                .writeTimeout(1, TimeUnit.HOURS)
                .build()

        val request = Request.Builder()
                .url(url)
                .build()

        val call = okhttp.newCall(request)

        try {
            val response = call.execute()
            result = response.body()?.string()
            val f = File("run.log")
            f.appendText(result!!)
            f.appendText("\n")

        } catch (e: IOException) {
            e.printStackTrace()
        }

        return result
    }
}

Kotlin調用Java代碼跟Groovy一樣流暢自如(但是不像Groovy那樣“怎麼寫都對,但是一運行就報錯”,因爲Groovy是一門動態類型語言,而Kotlin則是一門強類型的靜態類型語言)。我們基本不需要改變什麼就可以直接使用Java中的API庫。

並且在 Java 代碼中也可以很順利地調用 Kotlin 代碼:

package com.easy.kotlin;

import com.alibaba.fastjson.JSON;

public class JSONUtils {
    public static String toJsonString(Object o) {
        return JSON.toJSONString(o);
    }

    public static void main(String[] args) {
        String url = "http://www.baidu.com";
        String result = OkhttpUtils.INSTANCE.get(url);
        System.out.println(result);
    }
}

因爲Kotlin跟Java本是兩門語言,所以在互相調用的時候,會有一些特殊的語法。這裏的使用Java調用Kotlin的object對象函數的語法就是OkhttpUtils.INSTANCE.get(url), 我們看到這裏多了個INSTANCE 。

我們甚至也可以在一個項目中同時使用Kotlin和Java兩 種語言混合編程。我們可以在下一章中看到,我們在一個SpringBoot工程中同時使用了Kotlin和Java兩種語言進行混合開發。

下面我們來繼續介紹 Kotlin 調用 Java 代碼的一些細節。

Kotlin使用Java的集合類

Kotlin的集合類API很多就是直接使用的Java的API來實現的。我們在使用的時候,毫無違和感,自然天成:

@RunWith(JUnit4::class)
class KotlinUsingJavaTest {
    @Test fun testArrayList() {
        val source = listOf<Int>(1, 2, 3, 4, 5)
        // 使用Java的ArrayList
        val list = ArrayList<Int>()
        for (item in source) {
            list.add(item) // ArrayList.add()
        }
        for (i in 0..source.size - 1) {
            list[i] = source[i] // 調用 get 和 set
        }
    }
}

Kotlin調用Java中的Getter 和 Setter

在Java中遵循這樣的約定: getter 方法無參數並以 get 開頭,setter 方法單參數並以 set 開頭。在 Kotlin 中我們可以直接表示爲屬性。 例如,我們寫一個帶setter和getter的Java類:

package com.easy.kotlin;

import java.util.Date;

public class Product {
    Long id;
    String name;
    String category;
    Date gmtCreated;
    Date gmtModified;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public Date getGmtCreated() {
        return gmtCreated;
    }

    public void setGmtCreated(Date gmtCreated) {
        this.gmtCreated = gmtCreated;
    }

    public Date getGmtModified() {
        return gmtModified;
    }

    public void setGmtModified(Date gmtModified) {
        this.gmtModified = gmtModified;
    }
}

然後,我們在Kotlin可以直接使用屬性名字進行get和set操作:

@RunWith(JUnit4::class)
class ProductTest {
    @Test fun testGetterSetter() {
        val product = Product()
        product.name = "賬務系統"
        product.category = "金融財務類"
        product.gmtCreated = Date()
        product.gmtModified = Date()
        println(JSONUtils.toJsonString(product))
        Assert.assertTrue(product.getName() == "賬務系統")
        Assert.assertTrue(product.name == "賬務系統")
        Assert.assertTrue(product.getCategory() == "金融財務類")
        Assert.assertTrue(product.category == "金融財務類")
    }
}

當然,我們還可以像在Java中一樣,直接調用像product.getName()、product.setName(“Kotlin”)這樣的getter、setter方法。

調用Java中返回 void 的方法

如果一個 Java 方法返回 void,那麼從 Kotlin 調用時中返回 Unit

public class Admin {
    String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Admin{" +
            "name='" + name + '\'' +
            '}';
    }
}

我們這樣調用

val setReturn = admin.setName("root")
println(setReturn)

將輸出:kotlin.Unit

空安全和平臺類型

我們知道Java 中的任何引用都可能是null,這樣我們在使用 Kotlin調用來自 Java 的對象的時候就有可能會出現空安全的問題。

Java 聲明的類型在 Kotlin 中會被特別對待並稱爲平臺類型(platform types )。對這種類型的空檢查會放寬,因此它們的安全保證與在 Java 中相同。

請看以下示例:

@RunWith(JUnit4::class)
class CallingJavaNullSafe {
    @Test fun testCallingJavaNullSafe() {
        val product = Product()
        // product.name = null
        product.category = "金融財務類"
        product.gmtCreated = Date()
        product.gmtModified = Date()
        println(JSONUtils.toJsonString(product))

        val name = product.name
        println("product name is ${name}")

        val eqName = name == "賬務系統"
        println(eqName)

        name.substring(1)
    }
}

上面的代碼可以正確編譯通過。Kotlin編譯器對來自Java的空值name(平臺類型)放寬了空檢查name.substring(1)。但是這樣的空指針異常仍然會在運行時拋出來。

運行上面的代碼,我們可以看到輸出:

{"category":"金融財務類","gmtCreated":1500050426817,"gmtModified":1500050426817}
product name is null
false

null cannot be cast to non-null type java.lang.String
kotlin.TypeCastException: null cannot be cast to non-null type java.lang.String
    at com.easy.kotlin.CallingJavaNullSafe.testCallingJavaNullSafe(CallingJavaNullSafe.kt:27)

我們沒有設置name的值,在Java它就是null。我們在Kotlin代碼中使用了這個name進行計算,我們可以看出:

val eqName = name == "賬務系統"
println(eqName)

可以正確輸出false。這表明Kotlin的判斷字符串是否相等已經對null的情況作了判斷處理,這樣的代碼如果在Java中調用 name.equals("賬務系統") 就該拋空指針異常了。

但是當我們直接使用name這個值來調用name.substring(1)的時候,Kotlin編譯器不會檢查這個空異常,但是運行時還是要報錯的:null cannot be cast to non-null type java.lang.String

如果我們不想看到這樣的異常,而是當name是null的時候,安靜的輸出null,直接使用Kotlin中的空安全的調用 .?

name?.substring(1)

這樣,運行的時候不會拋出異常,直接安靜的返回null。

平臺類型

平臺類型不能在程序中顯式表述,因此在語言中沒有相應語法。 然而,編譯器和 IDE 有時需要(在錯誤信息中、參數信息中等)顯示他們,所以我們用一個助記符來表示他們:

  • T! : 表示 T 或者 T?

  • (Mutable) Collection<T>! : 表示 “可以可變或不可變、可空或不可空的 T 的 Java 集合”

  • Array<(out) T>! : 表示“可空或者不可空的 T(或 T 的子類型)的 Java 數組”

Kotlin與Java中的類型映射

Kotlin 特殊處理一部分 Java 類型。這樣的類型不是“按原樣”從 Java 加載,而是 映射 到相應的 Kotlin 類型。

映射只發生在編譯期間,運行時表示保持不變。

Java 的原生類型映射到相應的 Kotlin 類型:

Java 類型 Kotlin 類型
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

Java中的一些內置類型也會作相應的映射:

Java 類型 Kotlin 類型
java.lang.Object kotlin.Any!
java.lang.Cloneable kotlin.Cloneable!
java.lang.Comparable kotlin.Comparable!
java.lang.Enum kotlin.Enum!
java.lang.Annotation kotlin.Annotation!
java.lang.Deprecated kotlin.Deprecated!
java.lang.CharSequence kotlin.CharSequence!
java.lang.String kotlin.String!
java.lang.Number kotlin.Number!
java.lang.Throwable kotlin.Throwable!

Java 的裝箱原始類型映射到對應的可空Kotlin 類型:

Java 類型 Kotlin 類型
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Integer kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Character kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?

另外,用作類型參數的Java類型映射到Kotlin中的平臺類型:
例如,List<java.lang.Integer> 在 Kotlin 中會成爲 List<Int!>

集合類型在 Kotlin 中可以是隻讀的或可變的,因此 Java 集合類型作如下映射:
(下表中的所有 Kotlin 類型都在 kotlin.collections包中):

Java 類型 Kotlin 只讀類型 Kotlin 可變類型 加載的平臺類型
Iterator Iterator MutableIterator (Mutable)Iterator!
Iterable Iterable MutableIterable (Mutable)Iterable!
Collection Collection MutableCollection (Mutable)Collection!
Set Set MutableSet (Mutable)Set!
List List MutableList (Mutable)List!
ListIterator ListIterator MutableListIterator (Mutable)ListIterator!
Map Map MutableMap (Mutable)Map!
Map.Entry Map.Entry MutableMap.MutableEntry (Mutable)Map.(Mutable)Entry!

Java 的數組映射:

Java 類型 Kotlin 類型
int[] kotlin.IntArray!
String[] kotlin.Array<(out) String>!

Kotlin 中使用 Java 的泛型

Kotlin 的泛型與 Java 有點不同。當將 Java 類型導入 Kotlin 時,我們會執行一些轉換:

Kotlin 的泛型 Java 的泛型 說明
Foo! Foo Java 的通配符轉換成類型投影
Foo Foo

Kotlin與Java 中的數組

與 Java 不同,Kotlin 中的數組是非型變的,即 Kotlin 不允許我們把一個 Array<String> 賦值給一個 Array<Any>

Java 平臺上,持有原生數據類型的數組避免了裝箱/拆箱操作的開銷。

在Kotlin中,對於每種原生類型的數組都有一個特化的類(IntArrayDoubleArrayCharArray 等)來實現同樣的功能。它們與 Array 類無關,並且會編譯成 Java 原生類型數組以獲得最佳性能。

Java 可變參數

Java 類有時聲明一個具有可變數量參數(varargs)的方法來使用索引。

public class VarArgsDemo<T> {
    static VarArgsDemo vad = new VarArgsDemo();

    public static void main(String... agrs) {
        System.out.println(vad.append("a", "b", "c"));
        System.out.println(vad.append(1, 2, 3));
        System.out.println(vad.append(1, 2, "3"));
    }

    public String append(T... element) {
        StringBuilder result = new StringBuilder();
        for (T e : element) {
            result.append(e);
        }
        return result.toString();
    }
}

在Kotlin中,我們使用展開運算符 * 來傳遞這個varargs:

@RunWith(JUnit4::class)
class VarArgsDemoTest {
    @Test fun testVarArgsDemo() {
        val varArgsDemo = VarArgsDemo<Any?>()
        val array = arrayOf(0, 1, 2, 3)
        val result = varArgsDemo.append(*array)
        println(result)
    }
}

運行輸出:0123

非受檢異常

在 Kotlin 中,所有異常都是非受檢的(Non-Checked Exceptions),這意味着編譯器不會強迫你捕獲其中的任何一個。而在Java中會要求我們捕獲異常,例如下面的代碼:

Kotlin極簡教程

也就是說,我們需要寫類似下面的try catch代碼塊:

try {
    jsonUtils.parseObject("{}");
} catch (Exception e) {
    e.printStackTrace();
}

然而在Kotlin中情況就不是這樣子了:當我們調用一個聲明受檢異常的 Java 方法時,Kotlin 不會強迫你做任何事情:

@Test fun testNonCheckedExceptions() {
    val jsonUtils = JSONUtils()
    jsonUtils.parseObject("{}")
}

但是,我們在運行的時候,還是會拋異常:

com.easy.kotlin.CallingJavaNullSafe > testNonCheckedExceptions FAILED
    java.lang.Exception at CallingJavaNullSafe.kt:34

Kotlin的不受檢異常,這樣也會導致運行時拋出異常。關於異常的處理,該處理的終歸還是要處理的。

對象方法

Java中的java.lang.Object定義如下:

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
    public final native Class<?> getClass();
    public native int hashCode();
    public boolean equals(Object obj) {
        return (this == obj);
    }
    protected native Object clone() throws CloneNotSupportedException;
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    public final native void notify();
    public final native void notifyAll();
    public final native void wait(long timeout) throws InterruptedException;
    public final void wait(long timeout, int nanos) throws InterruptedException {...}
    public final void wait() throws InterruptedException {
        wait(0);
    }
    protected void finalize() throws Throwable { }
}

當 Java 類型導入到 Kotlin 中時,類型 java.lang.Object 的所有引用都成了 AnyAny只聲明瞭 toString()hashCode()equals() 函數。怎樣才能用到 java.lang.Object 的其他成員方法呢?下面我們來看下。

wait()/notify()

《Effective Java》 第 69 條中建議優先使用併發工具(concurrency utilities)而不是 wait()notify()。因此,類型 Any 的引用沒有提供這兩個方法。

如果我們真的需要調用它們的話,可以將其轉換爲 java.lang.Object來使用:

(foo as java.lang.Object).wait()

getClass()

要取得對象的 Java 類,我們可以在類引用上使用 java 擴展屬性,它是Kotlin的反射類kotlin.reflect.KClass的擴展屬性。

val fooClass = foo::class.java

上面的代碼使用了自 Kotlin 1.1 起支持的綁定類引用。我們也可以使用 javaClass 擴展屬性。

val fooClass = foo.javaClass

clone()

要覆蓋 clone(),需要繼承 kotlin.Cloneable


class Example : Cloneable {
    override fun clone(): Any { …… }
}

要謹慎地改寫clone方法。

finalize()

要覆蓋 finalize(),我們只需要聲明它即可,不用再寫 override關鍵字:

class C {
    protected fun finalize() {
        // 終止化邏輯
    }
}

訪問靜態成員

Java 類的靜態成員會形成該類的“伴生對象”。我們可以直接顯式訪問其成員。例如:

一個帶靜態方法的Java類

public class JSONUtils {
    public static String toJsonString(Object o) {
        return JSON.toJSONString(o);
    }
}

我們在Kotlin代碼可以直接這樣調用:

@RunWith(JUnit4::class)
class JSONUtilsTest {
    @Test fun testJSONUtils() {
        val userService = UserServiceImpl()
        val user = userService.findByName("admin")
        Assert.assertTrue(user.name == "admin")

        val userJson = JSONUtils.toJsonString(user)
        println(userJson)
        Assert.assertTrue(userJson == "{\"name\":\"admin\",\"password\":\"admin\"}")
    }
}

上面我們提到過,如果是反過來調用,Java調用Kotlin中的object對象類中的函數,需要使用object的 對象名.INSTANCE 來調用函數。

Kotlin與Java 的反射

我們可以使用 instance::class.javaClassName::class.java 或者 instance.javaClass 通過 java.lang.Class 來進入 Java 的反射類java.lang.Class, 之後我們就可以使用Java中的反射的功能特性了。

代碼示例:

@RunWith(JUnit4::class)
class RefectClassTest {
    @Test fun testGetterSetter() {
        val product = Product()
        val pClz = product::class.java
        println(pClz.canonicalName)
        pClz.declaredFields.forEach { println(it) }
        pClz.declaredMethods.forEach {
            println(it.name);
            it.parameters.forEach { println(it) }
        }
    }
}

運行上面的代碼輸出:

com.easy.kotlin.Product
java.lang.Long com.easy.kotlin.Product.id
java.lang.String com.easy.kotlin.Product.name
java.lang.String com.easy.kotlin.Product.category
java.util.Date com.easy.kotlin.Product.gmtCreated
java.util.Date com.easy.kotlin.Product.gmtModified
getName
setName
java.lang.String arg0
getId
setId
java.lang.Long arg0
setCategory
java.lang.String arg0
getGmtCreated
setGmtCreated
java.util.Date arg0
getGmtModified
setGmtModified
java.util.Date arg0
getCategory

SAM 轉換

我們在Kotlin中,要某個函數做某件事時,會傳一個函數參數給它。 而在Java中,並不支持傳送函數參數。通常Java的實現方式是將動作放在一個實現某接口的類中,然後將該類的一個實例傳遞給另一個方法。在大多數情況下,這些接口只有單個抽象方法(single abstract method),在Java中被稱爲SAM類型。

例如:Runnable接口:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

在 Java 8中我們也通常稱之爲函數式接口。

Kotlin 支持 SAM 轉換。Kotlin 的函數字面值可以被自動的轉換成只有一個非默認方法的 Java 接口的實現,只要這個方法的參數類型能夠與這個 Kotlin 函數的參數類型相匹配。

我們可以這樣創建 SAM 接口的實例:

val runnable = Runnable { println("執行測試") } // Kotlin 調用Java的SAM接口方法

測試代碼:

@RunWith(JUnit4::class)
class SAMFunctionalInterfaceTest {
    @Test fun testSAMFunctionalInterface() {
        val runnable = Runnable { println("執行測試") }
        val thread = Thread(runnable)
        thread.start()
    }
}

要注意的是,SAM 轉換隻適用於接口,而不適用於抽象類,即使這些抽象類也只有一個抽象方法。

還要注意,此功能只適用於 Java 互操作;因爲 Kotlin 具有合適的函數類型,所以不需要將函數自動轉換爲 Kotlin 接口的實現。

Java使用了Kotlin的關鍵字

一些 Kotlin 關鍵字在 Java 中是有效標識符:in、 object、 is等等。

如果一個 Java 庫使用了 Kotlin 關鍵字作爲方法,我們可以通過反引號(`)字符轉義它來調用該方法。例如我們有個Java類,其中有個is方法:

public class MathTools {

    public boolean is(Object o) {
        return true;
    }

}

那麼我們在Kotlin代碼這樣調用這個is方法:

@RunWith(JUnit4::class)
class MathToolsTest {
    @Test fun testISKeyWord(){
        val b = MathTools().`is`(1)
    }
}

Java 調用 Kotlin

Java 同樣也可以調用 Kotlin 代碼。但是要多用一些註解語法。

Java訪問Kotlin屬性

Kotlin 屬性會編譯成以下 Java 元素:

  • 一個 getter 方法,名稱通過加前綴 get 算出;
  • 一個 setter 方法,名稱通過加前綴 set 算出(只適用於 var 屬性);
  • 一個與屬性名稱相同的私有字段。

例如,下面的Kotlin類:

class Department {
    var id: Long = -1L
    var name: String = "Dept"
}

會被編譯成對應的 Java 代碼:

public final class Department {
   private long id = -1L;
   @NotNull
   private String name = "Dept";

   public final long getId() {
      return this.id;
   }

   public final void setId(long var1) {
      this.id = var1;
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }
}

我們可以看出,在Kotlin中的Long類型被編譯成Java中的原生的long了。
我們在Java代碼這樣調用:

@RunWith(JUnit4.class)
public class JavaCallingKotlinCodeTest {
    @Test
    public void testProperty() {
        Department d = new Department();
        d.setId(1);
        d.setName("技術部");

        Assert.assertTrue(1 == d.getId());
        Assert.assertTrue("技術部".equals(d.getName()));

    }
}

另外,如果Kotlin的屬性名以 is 開頭,則使用不同的名稱映射規則:

  • getter 的名稱直接使用屬性名稱
  • setter 的名稱是通過將 is 替換爲 set 獲得。

例如,對於屬性 isOpen,其 getter 會稱做 isOpen(),而其 setter 會稱做 setOpen()

這一規則適用於任何類型的屬性,並不僅限於 Boolean

代碼示例:

Kotlin代碼

class Department {
    var id: Long = -1L
    var name: String = "Dept"
    var isOpen:Boolean = true
    var isBig:String = "Y"
}

Java調用Kotlin的測試代碼:

@Test
public void testProperty() {
    Department d = new Department();
    d.setId(1);
    d.setName("技術部");
    d.setBig("Y");
    d.setOpen(true);

    Assert.assertTrue(1 == d.getId());
    Assert.assertTrue("技術部".equals(d.getName()));
    Assert.assertTrue("Y".equals(d.isBig()));
    Assert.assertTrue(d.isOpen());

}

Java調用Kotlin的包級函數

package com.easy.kotlin 包內的 KotlinExample.kt 源文件中聲明的所有的函數和屬性,包括擴展函數,都將編譯成一個名爲 com.easy.kotlin.KotlinExampleKt 的 Java 類中的靜態方法。

代碼示例:

Kotlin的包級屬性、函數代碼:

package com.easy.kotlin

fun f1() {
    println("I am f1")
}

fun f2() {
    println("I am f2")
}

val p: String = "PPP"

fun String.swap(index1: Int, index2: Int): String {
    val strArray = this.toCharArray()
    val tmp = strArray[index1]
    strArray[index1] = strArray[index2]
    strArray[index2] = tmp

    var result = ""
    strArray.forEach { result += it }
    return result
}

fun main(args: Array<String>) {
    println("abc".swap(0, 2))
}

編譯成對應的Java的代碼:

public final class KotlinExampleKt {
   @NotNull
   private static final String p = "PPP";

   public static final void f1() {
      String var0 = "I am f1";
      System.out.println(var0);
   }

   public static final void f2() {
      String var0 = "I am f2";
      System.out.println(var0);
   }

   @NotNull
   public static final String getP() {
      return p;
   }

   @NotNull
   public static final String swap(@NotNull String $receiver, int index1, int index2) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      char[] var10000 = $receiver.toCharArray();
      Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toCharArray()");
      char[] strArray = var10000;
      char tmp = strArray[index1];
      strArray[index1] = strArray[index2];
      strArray[index2] = tmp;
      Object result = "";
      char[] $receiver$iv = strArray;

      for(int var7 = 0; var7 < $receiver$iv.length; ++var7) {
         char element$iv = $receiver$iv[var7];
         result = result + element$iv;
      }

      return result;
   }

   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      String var1 = swap("abc", 0, 2);
      System.out.println(var1);
   }
}

我們可以看到,Kotlin中的擴展函數

fun String.swap(index1: Int, index2: Int): String

被編譯成

public static final String swap(@NotNull String $receiver, int index1, int index2)

Kotlin中的String. 接收者被當做Java方法中的第一個參數傳入。

Java調用Kotlin包級屬性、函數的測試代碼:

@Test
public void testPackageFun() {
    KotlinExampleKt.f1();
    KotlinExampleKt.f2();
    System.out.println(KotlinExampleKt.getP());
    KotlinExampleKt.swap("abc",0,1);
}

運行輸出:

I am f1
I am f2
PPP
bac

另外,要注意的這裏生成的類KotlinExampleKt,我們不能使用new來創建實例對象:

KotlinExampleKt example = new KotlinExampleKt();// 報錯

報如下錯誤:

error: cannot find symbol
        KotlinExampleKt example = new KotlinExampleKt();
                                  ^
  symbol:   constructor KotlinExampleKt()
  location: class KotlinExampleKt
1 error

在編程中,我們推薦使用Kotlin默認的命名生成規則。如果確實有特殊場景需要自定義Kotlin包級函數對應的生成Java類的名字,我們可以使用 @JvmName 註解修改生成的 Java 類的類名:

@file:JvmName("MyKotlinExample")

package com.easy.kotlin

fun f3() {
    println("I am f3")
}

fun f4() {
    println("I am f4")
}

val p2: String = "PPP"

測試代碼:

MyKotlinExample.f3();
MyKotlinExample.f4();

實例字段

我們使用 @JvmField 註解對Kotlin中的屬性字段標註,表示這是一個實例字段(Instance Fields),Kotlin編譯器在處理的時候,將不會給這個字段生成getters/setters方法。

class Department {
    var id: Long = -1L
    var name: String = "Dept"
    var isOpen: Boolean = true
    var isBig: String = "Y"

    @JvmField var NO = 0
}

映射成Java的代碼就是:

public final class Department {
   private long id = -1L;
   @NotNull
   private String name = "Dept";
   private boolean isOpen = true;
   @NotNull
   private String isBig = "Y";
   @JvmField
   public int NO;

   public final long getId() {
      return this.id;
   }

   public final void setId(long var1) {
      this.id = var1;
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public final boolean isOpen() {
      return this.isOpen;
   }

   public final void setOpen(boolean var1) {
      this.isOpen = var1;
   }

   @NotNull
   public final String isBig() {
      return this.isBig;
   }

   public final void setBig(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.isBig = var1;
   }
}

我們在Java中調用的時候,就直接使用這個屬性實例字段NO

System.out.println(d.NO = 10);

靜態字段

Kotlin中在命名對象或伴生對象中聲明的 屬性:

class Department {
    ...
    companion object {
        var innerID = "X001"
        @JvmField
        var innerName = "DEP"
    }
}

innerID、innerName這兩個字段的區別在於可見性上:

@NotNull
private static String innerID = "X001";
@JvmField
@NotNull
public static String innerName = "DEP";

這個私有的innerID通過Companion對象來封裝,提供出public的getInnerID() 、setInnerID來訪問:

public static final class Companion {
   @NotNull
   public final String getInnerID() {
      return Department.innerID;
   }

   public final void setInnerID(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      Department.innerID = var1;
   }

   private Companion() {
   }

   // $FF: synthetic method
   public Companion(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

我們在Java訪問的innerID時候,是通過Companion來訪問:

Department.Companion.getInnerID()

而我們使用@JvmField註解的字段innerName ,Kotlin編譯器會把它的訪問權限設置是public的,這樣我們就可以這樣訪問這個屬性字段了:

Department.innerName

靜態方法

Kotlin 中,我還可以將命名對象或伴生對象中定義的函數標註爲 @JvmStatic,這樣編譯器既會在相應對象的類中生成靜態方法,也會在對象自身中生成實例方法。

跟靜態屬性類似的,我們看下面的代碼示例:

class Department {
    ...
    companion object {
        var innerID = "X001"
        @JvmField
        var innerName = "DEP"

        fun getObjectName() = "ONAME"
        @JvmStatic
        fun getObjectID() = "OID"
    }
}

編譯器編譯之後,反編譯成的對應的Java代碼:

public final class Department {
   ...
   @JvmStatic
   @NotNull
   public static final String getObjectID() {
      return Companion.getObjectID();
   }

   public static final class Companion {
      ...
      @NotNull
      public final String getObjectName() {
         return "ONAME";
      }

      @JvmStatic
      @NotNull
      public final String getObjectID() {
         return "OID";
      }
     ...
   }
}

在Java中調用的代碼如下:

Department.Companion.getObjectID(); // OK
Department.Companion.getObjectName(); // OK, 唯一的工作方式
Department.getObjectID(); // ALSO OK
Department.getObjectName(); // ERROR

這些註解語法是編譯器爲了更加方便Java調用Kotlin代碼提供的一些簡便技巧。這樣可使得Java中調用Kotlin代碼更加自然優雅些。

可見性

Kotlin 的可見性與Java的可見性的映射關係如下表所示:

Kotlin中的聲明 Java中的聲明
private private
protected protected
internal public
public public

例如下面的Kotlin代碼:

class ProgrammingBook {
    private var isbn: String = "978-7-111-44250-9"
    protected var author: String = "Cay"
    public var name: String = "Core Java"
    internal var pages: Int = 300

    private fun findISBN(): String = "978-7-111-44250-9"
    protected fun findAuthor(): String = "Cay"
    public fun findName(): String = "Core Java"
    internal fun findPages(): Int = 300
}

對應的Java的代碼是:

public final class ProgrammingBook {
   private String isbn = "978-7-111-44250-9";
   @NotNull
   private String author = "Cay";
   @NotNull
   private String name = "Core Java";
   private int pages = 300;

   @NotNull
   protected final String getAuthor() {
      return this.author;
   }

   protected final void setAuthor(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.author = var1;
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public final int getPages$production_sources_for_module_chapter10_interoperability_main() {
      return this.pages;
   }

   public final void setPages$production_sources_for_module_chapter10_interoperability_main(int var1) {
      this.pages = var1;
   }

   private final String findISBN() {
      return "978-7-111-44250-9";
   }

   @NotNull
   protected final String findAuthor() {
      return "Cay";
   }

   @NotNull
   public final String findName() {
      return "Core Java";
   }

   public final int findPages$production_sources_for_module_chapter10_interoperability_main() {
      return 300;
   }
}

我們可以看到Kotlin中的可見性跟Java中的基本相同。

生成默認參數值函數的重載

我們在Kotlin中寫一個有默認參數值的 Kotlin 方法,它會對每一個有默認值的參數都生成一個重載函數。這樣的Kotlin函數,在 Java 中調用的話,只會有一個所有參數都存在的完整參數簽名方法可見。如果我們希望Java像Kotlin中一樣可以調用多個重載,可以使用@JvmOverloads註解。

下面我們來通過一個實例對比兩者的區別:

這是一段Kotlin代碼:

class OverridesFunWithDefaultParams {
    fun f1(a: Int = 0, b: String = "B") {

    }

    @JvmOverloads fun f2(a: Int = 0, b: String = "B") {

    }
}

函數f1 和 f2 都帶有默認參數。測試代碼如下:

@Test
public void testOverridesFunWithDefaultParams() {
    OverridesFunWithDefaultParams ofdp = new OverridesFunWithDefaultParams();
    ofdp.f1(1, "a");
    ofdp.f2();
    ofdp.f2(2);
    ofdp.f2(2, "b");
}

這就是@JvmOverloads註解的作用,編譯器會處理這個註解所標註的函數,併爲之生成額外的重載函數給Java調用。

檢查Kotlin中異常

如上所述,Kotlin 沒有受檢異常。即像下面像這樣的 Kotlin 函數:

class CheckKotlinException {
    fun thisIsAFunWithException() {
        throw Exception("I am an exception in kotlin")
    }
}

在Java中調用,編譯器是不會檢查這個異常的:

@Test
public void testCheckKotlinException() {
    CheckKotlinException cke = new CheckKotlinException();
    cke.thisIsAFunWithException();// Java編譯器不檢查這個Kotlin中的異常
}

當然,在運行時,這個異常還是會拋出來。然後,如果我們想要在 Java 中調用它並捕捉這個異常,我們可以給Kotlin中的函數加上註解@Throws(Exception::class), 就像下面這樣:

@Throws(Exception::class)
fun thisIsAnotherFunWithException() {
    throw Exception("I am Another exception in kotlin")
}

然後,我們在Java中調用的時候,Java編譯器就會檢查這個異常:

Kotlin極簡教程

最後,我們的代碼就需要捕獲該異常並處理它。

完整的示例代碼如下:

package com.easy.kotlin

class CheckKotlinException {
    fun thisIsAFunWithException() {
        throw Exception("I am an exception in kotlin")
    }

    @Throws(Exception::class)
    fun thisIsAnotherFunWithException() {
        throw Exception("I am Another exception in kotlin")
    }
}

測試代碼:

package com.easy.kotlin;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class CheckKotlinExceptionTest {
    @Test
    public void testCheckKotlinException() {
        CheckKotlinException cke = new CheckKotlinException();
        cke.thisIsAFunWithException();// Java編譯器不檢查這個Kotlin中的異常

        // Kotlin中顯示聲明瞭異常,Java編譯器會檢查這個異常
        // cke.thisIsAnotherFunWithException();
        try {
            cke.thisIsAnotherFunWithException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Nothing 類型

在Kotlin中Nothing類型是一個特殊的類型,它在 Java 中沒有的對應的類型。在使用 Nothing 參數的地方會生成一個原始類型。

例如下面的Kotlin代碼:

fun emptyList(): List<Nothing> = listOf()

對應到Java代碼中是:

@NotNull
public final List emptyList() {
   return CollectionsKt.emptyList();
}

Kotlin中的List<Nothing> 映射爲原生類型List

Kotlin與Java對比

在前面的內容裏,我們已經看到了Java與Kotlin的互操作的基本方式。爲了更好的認識Java與Kotlin這兩門語言,我們在這裏給出一些基本功能,同時使用Java與Kotlin來實現的代碼實例。通過橫向對比,從中我們可以看出它們的異同。

(此處可整理成表格形式)

打印日誌

  • Java
System.out.print("Java");
System.out.println("Java");
  • Kotlin
print("Kotlin")
println("Kotlin")

其實,Kotlin中的println函數是一個內聯函數,它其實就是通過封裝java.lang.System類的System.out.println來實現的。

@kotlin.internal.InlineOnly
public inline fun print(message: Any?) {
    System.out.print(message)
}

常量與變量

  • Java
String name = "KotlinVSJava";
final String name = "KotlinVSJava";
  • Kotlin
var name = "KotlinVSJava"
val name = "KotlinVSJava"

null聲明

  • Java
String otherName;
otherName = null;
  • Kotlin
var otherName : String?
otherName = null

空判斷

  • Java
if (text != null) {
    int length = text.length();
}
  • Kotlin
text?.let {
    val length = text.length
}
// 或者
val length = text?.length

在Kotlin中,我們只使用一個問號安全調用符號就省去了Java中煩人的if - null 判斷。

字符串拼接

  • Java
String firstName = "Jack";
String lastName = "Chen";
String message = "My name is: " + firstName + " " + lastName;
  • Kotlin
val firstName = "Jack"
val lastName = "Chen"
val message = "My name is: $firstName $lastName"

Kotlin中使用$${}(花括號裏面是表達式的時候)佔位符來實現字符串的拼接,這個比在Java中每次使用加號來拼接要方便許多。

換行

  • Java
String text = "First Line\n" +
              "Second Line\n" +
              "Third Line";
  • Kotlin
val text = """
        |First Line
        |Second Line
        |Third Line
        """.trimMargin()

三元表達式

  • Java
String text = x > 5 ? "x > 5" : "x <= 5";
  • Kotlin
val text = if (x > 5)
              "x > 5"
           else "x <= 5"

操作符

  • java
final int andResult  = a & b;
final int orResult   = a | b;
final int xorResult  = a ^ b;
final int rightShift = a >> 2;
final int leftShift  = a << 2;
  • Kotlin
val andResult  = a and b
val orResult   = a or b
val xorResult  = a xor b
val rightShift = a shr 2
val leftShift  = a shl 2

類型判斷和轉換(顯式)

  • Java
if (object instanceof Car) {
}
Car car = (Car) object;
  • Kotlin
if (object is Car) {
}
var car = object as Car

類型判斷和轉換 (隱式)

  • Java
if (object instanceof Car) {
   Car car = (Car) object;
}
  • Kotlin
if (object is Car) {
   var car = object // Kotlin智能轉換
}

Kotlin的類型系統具備一定的類型推斷能力,這樣也省去了不少在Java中類型轉換的樣板式代碼。

Range區間

  • Java
if (score >= 0 && score <= 300) { }
  • Kotlin
if (score in 0..300) { }

更靈活的case語句

  • Java
public String getGrade(int score) {
    String grade;
    switch (score) {
        case 10:
        case 9:
            grade = "A";
            break;
        case 8:
        case 7:
        case 6:
            grade = "B";
            break;
        case 5:
        case 4:
            grade = "C";
            break;
        case 3:
        case 2:
        case 1:
            grade = "D";
            break;
        default:
            grade = "E";
    }
    return grade;
}
  • Kotlin
fun getGrade(score: Int): String {
    var grade = when (score) {
        9, 10 -> "A"
        in 6..8 -> "B"
        4, 5 -> "C"
        in 1..3 -> "D"
        else -> "E"
    }
    return grade
}

for循環

  • Java
for (int i = 1; i <= 10 ; i++) { }

for (int i = 1; i < 10 ; i++) { }

for (int i = 10; i >= 0 ; i--) { }

for (int i = 1; i <= 10 ; i+=2) { }

for (int i = 10; i >= 0 ; i-=2) { }

for (String item : collection) { }

for (Map.Entry<String, String> entry: map.entrySet()) { }
  • Kotlin
for (i in 1..10) { }

for (i in 1 until 10) { }

for (i in 10 downTo 0) { }

for (i in 1..10 step 2) { }

for (i in 10 downTo 1 step 2) { }

for (item in collection) { }

for ((key, value) in map) { }

更方便的集合操作

  • Java
final List<Integer> listOfNumber = Arrays.asList(1, 2, 3, 4);
final Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "Jack");
map.put(2, "Ali");
map.put(3, "Mindorks");
  • Kotlin
val listOfNumber = listOf(1, 2, 3, 4)
val map = mapOf(1 to "Jack", 2 to "Ali", 3 to "Mindorks")

遍歷

  • Java
// Java 7 
for (Car car : cars) {
  System.out.println(car.speed);
}

// Java 8+
cars.forEach(car -> System.out.println(car.speed));

// Java 7 
for (Car car : cars) {
  if (car.speed > 100) {
    System.out.println(car.speed);
  }
}

// Java 8+
cars.stream().filter(car -> car.speed > 100).forEach(car -> System.out.println(car.speed));
  • Kotlin
cars.forEach {
    println(it.speed)
}

cars.filter { it.speed > 100 }
      .forEach { println(it.speed)}

方法(函數)定義

  • Java
void doSomething() {
   // 實現
}

void doSomething(int... numbers) {
   // 實現
}
  • Kotlin
fun doSomething() {
   // 實現
}

fun doSomething(vararg numbers: Int) {
   // 實現
}

帶返回值的方法(函數)

  • Java
int getScore() {
   // logic here
   return score;
}
  • Kotlin
fun getScore(): Int {
   // logic here
   return score
}

// 單表達式函數
fun getScore(): Int = score

另外,Kotlin中的函數是可以直接傳入函數參數,同時可以返回一個函數類型的。

constructor 構造器

  • Java
public class Utils {

    private Utils() { 
      // 外部無法來調用實例化
    }
    
    public static int getScore(int value) {
        return 2 * value;
    }
    
}
  • Kotlin
class Utils private constructor() {

    companion object {
    
        fun getScore(value: Int): Int {
            return 2 * value
        }
        
    }
}

// 或者直接聲明一個object對象
object Utils {

    fun getScore(value: Int): Int {
        return 2 * value
    }

}

JavaBean與Kotlin數據類

這段Kotlin中的數據類的代碼:

data class Developer(val name: String, val age: Int)

對應下面這段Java實體類的代碼:

  • Java
public final class Developer {
   @NotNull
   private final String name;
   private final int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public Developer(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   @NotNull
   public final String component1() {
      return this.name;
   }

   public final int component2() {
      return this.age;
   }

   @NotNull
   public final Developer copy(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      return new Developer(name, age);
   }

   // $FF: synthetic method
   // $FF: bridge method
   @NotNull
   public static Developer copy$default(Developer var0, String var1, int var2, int var3, Object var4) {
      if((var3 & 1) != 0) {
         var1 = var0.name;
      }

      if((var3 & 2) != 0) {
         var2 = var0.age;
      }

      return var0.copy(var1, var2);
   }

   public String toString() {
      return "Developer(name=" + this.name + ", age=" + this.age + ")";
   }

   public int hashCode() {
      return (this.name != null?this.name.hashCode():0) * 31 + this.age;
   }

   public boolean equals(Object var1) {
      if(this != var1) {
         if(var1 instanceof Developer) {
            Developer var2 = (Developer)var1;
            if(Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

本章小結

本章我們一起學習了Kotlin與Java的互操作,同時我們用一些簡單的示例對比了它們的異同。在這之中,我們能感受到Kotlin的簡潔、優雅。 我們可以用更少的代碼來實現更多的功能。 另外,在IDEA中,我們可以直接使用Kotlin插件來直接進行Java代碼與Kotlin代碼之間的轉換(雖然,有些情況下需要我們手工再去稍作改動)。

Kotlin的定位本身之一就是官網首頁重點強調的:100% interoperable with Java™。它並不是scala那樣另起爐竈, 將類庫(例如,集合類)都自己實現了一遍。kotlin是對現有Java的增強,通過擴展方法給java提供了很多諸如fp之類的特性, 但同時始終保持對java的兼容。

而在Java生態領域最爲人知的Spring框架,在最新的Spring 5中對Kotlin也作了支持(參看:introducing-kotlin-support-in-spring-framework-5-0)。 當前,作爲Spring大家族中最引人注目的非Spring Boot莫屬了。我們即將在下一章中介紹Kotlin集成Spring Boot來開發服務端Web項目。

本章示例代碼:https://github.com/EasyKotlin/chapter10_interoperability

原文鏈接:https://github.com/EasyKotlin

發佈了273 篇原創文章 · 獲贊 266 · 訪問量 123萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章