Kotlin基礎:望文生義的Kotlin集合操做

這是該系列的第二篇,系列文章目錄以下:java

  1. Kotlin基礎:白話文轉文言文般的Kotlin常識程序員

  2. Kotlin基礎:望文生義的Kotlin集合操做算法

  3. Kotlin實戰:用實戰代碼更深刻地理解預約義擴展函數bash

  4. Kotlin實戰:使用DSL構建結構化API去掉冗餘的接口方法數據結構

  5. Kotlin基礎:屬性也能夠是抽象的app

  6. Kotlin進階:動畫代碼太醜,用DSL動畫庫拯救,像說話同樣寫代碼喲!ide

  7. Kotlin基礎:用約定簡化相親函數

有沒有那麼一種代碼,從頭至尾讀一遍就能清晰的明白語義?就好像在閱讀英語文章同樣。這篇文章就試着用這樣望文生義的代碼來實現業務需求,剖析 kotlin 語言特性所帶來的簡潔及其背後原理。知識點包括序列,集合操做,主構造方法,可變參數,默認參數,命名參數,for循環,數據類。本着實用主義,不會面面俱到地展開知識點全部的細節(這樣會很無趣),而是隻講述和實例有關的方面。post

該系列每一篇例子用到的知識點會在上一篇的基礎上擴充,若遇到不了解的語法也能夠移步上一篇查閱。性能

業務需求以下:假設如今須要基於學生列表過濾出全部學生的選修課(課時數 < 70),輸出時按課時數升序排列,課時數相等的再按課程名字母序排列,並寫課程名的第一個字母。

數據類

先得聲明數據實體類,java的代碼以下:

課程實體類

public class Course {
    private String name ;
    private int period ;
    private boolean isMust;

    public String getName() {
        return name;
    }

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

    public int getPeriod() {
        return period;
    }

    public void setPeriod(int period) {
        this.period = period;
    }

    public boolean isMust() {
        return isMust;
    }

    public void setMust(boolean must) {
        isMust = must;
    }

    @Override
    public String toString() {
        return "Course{" +
                "name=‘" + name + '\'' + ", period=" + period + ", isMust=" + isMust + '}’;
    }
}
複製代碼

學生實體類

public class Student {
    private String name;
    private int age;
    private boolean isMale ;
    private List<Course> courses ;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean isMale() {
        return isMale;
    }

    public void setMale(boolean male) {
        isMale = male;
    }

    public List<Course> getCourses() {
        return courses;
    }

    public void setCourses(List<Course> courses) {
        this.courses = courses;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' + ", age=" + age + ", isMale=" + isMale + ", courses=" + courses + '}‘;
    }
}
複製代碼

代碼略長,其實關鍵信息只有兩個:類名+屬性,其他部分都是模版代碼,因此 kotlin 將數據類的定義縮減成一行代碼:

data class Course constructor(var name: String, var period: Int, var isMust: Boolean = false)

data class Student constructor(var name: String, var age: Int, var isMale: Boolean, var courses: List<Course> = listOf())
複製代碼
  • data是保留字,用於修飾一個類,代表該類只包含數據而不包含行爲,便是 java 中的 Bean 類。
  • 類的聲明格式以下:
修飾詞 class 類名 constructor(主構造函數參數列表){類體}
複製代碼
  • class保留字用於聲明類。
  • constructor保留字用於聲明類的主構造方法,它至關於把 java 中的類聲明和構造函數聲明合併到了一行,下面的兩段代碼是徹底等價的:
//java
class A(){
    private int i
    A(int i){
        this.i = i;
    }
}

//kotlin
class A constructor(var i: Int)
複製代碼
  • 主構造方法顯示地聲明瞭類的成員屬性和其數據類型,這裏包含的隱藏信息是,當構造A的實例時,傳入構造方法的 Int 值會被賦值給成員 i。
  • 除了簡單地爲成員賦值,主構造方法不包含其餘任何邏輯。(當須要特殊的初始化邏輯時須要使用別的方法,之後會講到~)
  • 當沒有可見性修飾符修飾主構造方法時,能夠省去constructor保留字,因此上面的數據類能夠簡化成:
data class Course(var name: String, var period: Int, var isMust: Boolean = false)

data class Student(var name: String, var age: Int, var isMale: Boolean, var courses: List<Course> = listOf())
複製代碼

這裏還展現了一種在 java 中不支持的特性:參數默認值Course類的isMust屬性的默認值是false,這減小了重載構造函數的數量,由於在 java 中只能經過重載來實現:

public Course{
    public Course(String name,int period,boolean isMust){
        this.name = name;
        this.period = period;
        this.isMust = isMust;
    }
    
    public Course(String name,int period){
        return Course(name,period,false);
    }
}
複製代碼

在簡簡單單的一句類聲明的背後,編譯器會自動爲咱們建立全部咱們須要的方法,包括:

  • setter() 和 getter()
  • equals() 和 hashCode()
  • toString()
  • copy()

其中copy()會基於對象現有屬性值構建一個新對象。

構建集合

有了數據實體類後,就能夠構建數據集合了,讓咱們來構建一個包含4個學生的列表,java 代碼以下(其實直接跳過這段代碼也是不錯的選擇,由於它很冗長並且可讀性差):

Student student1 = new Student();
student1.setName("taylor");
student1.setAge(33);
student1.setMale(false);
List<Course> courses1 = new ArrayList<>();
Course course1 = new Course();
course1.setName("pysics");
course1.setPeriod(50);
course1.setMust(false);
Course course2 = new Course();
course2.setName("chemistry");
course2.setPeriod(78);
courses1.add(course1);
courses1.add(course2) ;
student1.setCourses(courses1);

Student student2 = new Student();
student2.setName("milo");
student2.setAge(20);
student2.setMale(false);
List<Course> courses2 = new ArrayList<>();
Course course3 = new Course();
course3.setName("computer");
course3.setPeriod(50);
course3.setMust(true);
student2.setCourses(courses2);

List<Student> students = new ArrayList<>();
students.add(student2);
students.add(student1);
...
複製代碼

我只寫了2個學生構建代碼,不想再寫下去了。。。你能不能一眼看出它到底在構建啥嗎?

仍是看看 kotlin 是怎麼玩的吧:

val students = listOf(
    Student("taylor", 33, false, listOf(Course("physics", 50), Course("chemistry", 78))),
    Student("milo", 20, false, listOf(Course("computer", 50, true))),
    Student("lili", 40, true, listOf(Course("chemistry", 78), Course("science", 50))),
    Student("meto", 10, false, listOf(Course("mathematics", 48), Course("computer", 50, true)))
)
複製代碼

就算是第一次接觸 kotlin ,必定也看懂這是在幹嗎。

  • 得益於參數默認值,對於同一個Course構造函數,可傳入2個參數Course("physics", 50),也可傳入3個參數Course("computer", 50, true)
  • listOf()是 kotlin 標準庫中的方法,這個方法極大簡化了構建集合的代碼,看下它的源碼:
public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
複製代碼
  • vararg保留字用於修飾可變參數,表示這個該函數能夠接收任意數量的該類參數。
  • listOf()的返回值是 kotlin 中的List類型。

一眼看去,咱們就能知道這段代碼構建了一個列表,列表中構建了4個學生實例,在構建學生實例的同時構建了一系列課程實例。

可是構建學生時,傳入的布爾值是什麼語義?猜想多是年齡,在 IDE 跳轉功能的幫助下,能夠方便地到Student定義處確認一下。但若是在網頁端進行 Code Review 時就沒有這麼好的條件了。

有什麼辦法在方法調用處就指明參數的語義?

命名參數功能就是爲此而生,上面的代碼還能夠這樣寫:

val students = listOf(
    Student("taylor", 33, isMale = false, courses = listOf(Course("physics", 50), Course("chemistry", 78))),
    Student("milo", 20, isMale = false, courses = listOf(Course("computer", 50, true))),
    Student("lili", 40, isMale = true, courses = listOf(Course("chemistry", 78), Course("science", 50))),
    Student("meto", 10, isMale = false, courses = listOf(Course("mathematics", 48), Course("computer", 50, true)))
)
複製代碼

能夠在參數前經過加變量名 =的方式來顯示指明參數語義,同時這對變量的命名也提出了更高的要求。

做爲程序員的咱們,絕大部分時間不是在寫而是在讀別人或本身的代碼。就好像語文閱卷老師要讀大量做文同樣,若是字跡潦草,段落不清晰,就是在給本身給老師添麻煩。一樣的,達意的命名,一致的縮進,語義清晰的調用,讓本身和同事賞心悅目。(這也是 kotlin 爲啥能提升產生效率的緣由,由於它更簡潔,更可讀)

操縱集合

下一個步驟是操縱集合,直接上 kotlin :

val friends = students
        .flatMap { it.courses }
        .toSet()
        .filter { it.period < 70 && !it.isMust }
        .map {
            it.apply {
                name = name.replace(name.first(), name.first().toUpperCase())
            }
        }
        .sortedWith(compareBy({ it.period }, { it.name }))
複製代碼

掃了一遍,在不少陌生函數裏面有一個上篇講解過的apply(),它作的事情是將集合中的每一個元素中的name屬性的第一個字符換成大寫。

在 java 中(8.0之前),爲了操縱集合元素,必然要用for循環遍歷集合。但在上面的代碼中,沒有發現相似的遍歷操做,那 kotlin 是如何獲取集合中元素的?

map()

kotlin 標準庫中預約了不少集合操縱方法,上面用到的map()就是其中一個,它的源碼以下:

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
複製代碼

map()是一個Iterable類的擴展函數,這個類表示一個能夠被迭代的對象,CollectionList都是繼承自它:

/**
 * Classes that inherit from this interface can be represented as a sequence of elements that can
 * be iterated over.
 * @param T the type of element being iterated over. The iterator is covariant on its element type.
 */
public interface Iterable<out T> {
    /**
     * Returns an iterator over the elements of this object.
     */
    public operator fun iterator(): Iterator<T>
}

public interface Collection<out E> : Iterable<E> {
    ...
}

public interface List<out E> : Collection<E> {
    ...
}
複製代碼

map()內會新建一個ArrayList類型的集合(它是一箇中間臨時集合)並傳給mapTo()

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}
複製代碼

這裏出現了一個熟悉的保留字for,它in搭配後和 java 中的for-each語義相似。

原來map()內部使用了for循環遍歷源集合,並在每一個元素上應用了transform這個變換,最後將變換後的元素加入臨時集合中並將其返回。

因此map()函數的語義是:在集合的每個元素上應用一個自定義的變換

filter()

map()函數前調用了filter(),源碼以下:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}
複製代碼

相似的,它也會構建一個臨時集合來暫存運算的中間結果,在遍歷源集合的同時應用條件判斷predicate,當知足條件時纔將源集合元素加入到臨時集合。

因此filter()的語義是:只保留知足條件的集合元素

toSet()

filter()以前調用是toSet()

public fun <T> Iterable<T>.toSet(): Set<T> {
    if (this is Collection) {
        return when (size) {
            0 -> emptySet()
            1 -> setOf(if (this is List) this[0] else iterator().next())
            else -> toCollection(LinkedHashSet<T>(mapCapacity(size)))
        }
    }
    return toCollection(LinkedHashSet<T>()).optimizeReadOnlySet()
}

public fun <T, C : MutableCollection<in T>> Iterable<T>.toCollection(destination: C): C {
    for (item in this) {
        //重複元素會添加失敗
        destination.add(item)
    }
    return destination
}
複製代碼

遍歷源集合的同時藉助LinkedHashSet來實現元素的惟一性。

因此toSet()的語義是:將集合元素去重

flatMap()

在調用鏈的最開始,調用的是flatMap()

public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
    return flatMapTo(ArrayList<R>(), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
    for (element in this) {
        val list = transform(element)
        destination.addAll(list)
    }
    return destination
}
複製代碼

flatMap()的源碼和map()很是類似,惟一的區別是,transform這個變換的結果是一個集合類型,而後會把該集合整個加入到臨時集合。

flatMap()作了兩件事情:先對源集合中每一個元素作變換(變換結果是另外一個集合),而後把多個集合合併成一個集合。這樣的操做很是適用於集合中套集合的數據結構,就好像本例中的學生實例存放在學生列表中,而每一個學生實例中包含課程列表。經過先變換後平鋪的操做能夠方便地將學生列表中的全部課程平鋪開來。

因此flatMap()的語義是:將嵌套集合中的內層集合鋪開

asSequence()

由於每一個操縱集合的函數都會新建一個臨時集合以存放中間結果。

爲了更好的性能,有沒有什麼辦法去掉臨時集合的建立?

序列就是爲此而生的,用序列改寫上面的代碼:

val friends = students.asSequence()
        .flatMap { it.courses.asSequence() }
        .filter { it.period < 70 && !it.isMust }
        .map {
            it.apply {
                name = name.replace(name.first(), name.first().toUpperCase())
            }
        }
        .sortedWith(compareBy({ it.period }, { it.name }))
        .toSet()
複製代碼

經過調用asSequence()將本來的集合轉化成一個序列,序列將對集合元素的操做分爲兩類:

  1. 中間操做
  2. 末端操做

從返回值上看,中間操做返回的另外一個序列,而末端操做返回的是一個集合(toSet()就是末端操做)。

從執行時機上看,中間操做都是惰性的,也就說中間操做都會被推遲執行。而末端操做觸發執行了全部被推遲的中間操做。因此將toSet()移動到了末尾。

序列還會改變中間操做的執行順序,若是不用序列,n 箇中間操做就須要遍歷集合 n 遍,每一遍應用一個操做,使用序列以後,只須要遍歷集合 1 遍,在每一個元素上一會兒應用全部的中間操做。

若是用 java 實現上述集合操做的話,須要定義一個不是太簡單的算法,定神分析一番才能明白業務需求,而 kotlin 的代碼就好像把需求翻譯成了英語,順着讀完代碼就能明白語義。這種 「望文生義」 的效果,真是 java 不能比擬的。

知識點總結

  • 經過data關鍵詞配合主構造函數,kotlin 能夠用一行代碼聲明數據類。
  • 主構造方法是一個用於爲類屬性賦初始值的構造方法。它經過constructor保留字和類頭聲明在同一行。
  • 保留字vararg用於聲明可變參數,帶有可變參數的方法能夠接收任意個數的參數。
  • 能夠經過=在聲明方法時爲參數設置默認值,以減小重載函數。
  • 能夠經過變量名 =語法在方法調用的時候添加命名參數,增長方法調用的可讀性。
  • kotlin 標準庫預約義了不少處理集合的方法,其中
    • filter()的語義是:只保留知足條件的集合元素
    • toSet()的語義是:將集合元素去重
    • flatMap()的語義是:將嵌套集合中的內層集合鋪開
    • map()函數的語義是:在集合的每個元素上應用一個自定義的變換
    • asSequence()用於將一連串集合操做變成序列,以提高集合操做性能。
相關文章
相關標籤/搜索