2019年4月23日

Redis Client - 使用Lettuce

Redis是一套基於key-value的in-memory database,常見的用途之一,是拿來作為Application Server的快取系統。
Lettuce則是一個強調thread-safe的Java Redis Client。

Redis參數設定

可以使用XML或Annotation兩種方式,來設定Redis的IP、Port。

方法(一):建立XML設定檔

以下假設專案使用webapp,並將XML檔案放在src/main/resource底下,取名為lettuce.xml

完整XML檔案範例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- beans settings -->

</beans>

Sentinel設定

若要連線至3個Sentinel:

  • 192.168.1.32:27030
  • 192.168.1.32:27031
  • 192.168.1.32:27032

範例如下:

<bean id="sentinelConfig" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
    <constructor-arg name="master" value="mymaster" />
    <constructor-arg name="sentinelHostAndPorts">
        <set>
            <value>192.168.1.32:27030</value>
            <value>192.168.1.32:27031</value>
            <value>192.168.1.32:27032</value>
        </set>
    </constructor-arg>
</bean>

Connection設定

Java Redis Client使用lettuce,如下:

<bean id="lettuceConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
    <constructor-arg ref="sentinelConfig" />
</bean>

RedisTemplate設定

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="lettuceConnectionFactory"  />

方法(二):Annotation

建立一個類別,並於class前加上 @Configuration

//...
import org.springframework.context.annotation.Configuration;
//...

@Configuration
public class SpringDataRedisLettuceConfig {
//...
}

Connection Pool 設定

@Bean
public GenericObjectPoolConfig genericObjectPoolConfig() {
    GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
    genericObjectPoolConfig.setMaxIdle(8);
    genericObjectPoolConfig.setMinIdle(0);
    genericObjectPoolConfig.setMaxTotal(8);
    genericObjectPoolConfig.setMaxWaitMillis(100000);
    return genericObjectPoolConfig;
}

Sentinel設定

若要連線至3個Sentinel:

  • 192.168.1.32:27030
  • 192.168.1.32:27031
  • 192.168.1.32:27032

範例如下:

@Bean
public RedisSentinelConfiguration sentinelConfig()  {
    Set<String> sentinels = new HashSet<>();
    sentinels.add("192.168.1.32:27030");
    sentinels.add("192.168.1.32:27031");
    sentinels.add("192.168.1.32:27032");
    RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration("mymaster", sentinels);
    return redisSentinelConfiguration;
}

Connection設定

Java Redis Client使用lettuce,如下:

@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisSentinelConfiguration sentinelConfig, GenericObjectPoolConfig genericObjectPoolConfig)  {

    LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
            .commandTimeout(Duration.ofMillis(10000))
            .poolConfig(genericObjectPoolConfig)
            .build();

    LettuceConnectionFactory factory = new LettuceConnectionFactory(sentinelConfig, clientConfig);
    return factory;
}

RedisTemplate設定

設定KeySerializer使用StringRedisSerializer

@Bean
RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(lettuceConnectionFactory);
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    redisTemplate.setKeySerializer(stringRedisSerializer);
    redisTemplate.setHashKeySerializer(stringRedisSerializer);
    return redisTemplate;
}

在Java中使用RedisTemplate操作Redis

  • 若剛才使用方法(一):建立XML設定檔

由於XML檔案位於src/main/resource/lettuce.xml,此處先使用ClassPathXmlApplicationContext取得RedisTemplate物件:

ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] { "lettuce.xml" });
RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);

  • 若剛才使用方法(二):Annotation的話,則只要確定Spring的root-context.xml中,有設定掃描相關的package即可:
...
<context:component-scan base-package="tw.com.maxkit.cdc.configuration" />
...

設定KeySerializer

設定KeySerializer為StringRedisSerializer

RedisSerializer<String> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);

因為無論是Key還是Value,預設都使用JdkSerializationRedisSerializer;如此一來,假設原本程式中寫入的key為mykey,若沒有將KeySerializer為StringRedisSerializer,寫入資料後,進console查看keys資料如下:

192.168.1.32:7030> keys *
1) "\xac\xed\x00\x05t\x00\x05mykey"

若有將KeySerializer為StringRedisSerializer時,寫入資料後,進console查看keys資料如下

192.168.1.32:7030> keys *
1) "mykey"

兩筆資料將會是不同的key,因此KeySerializer應一開始就設定好,並保持一致

此外,Value也可以設定使用StringRedisSerializer:

redisTemplate.setValueSerializer(stringSerializer);

不過,如果需要將序列化的物件存入,那就不應將ValueSerializer設為StringRedisSerializer。

透過Operations操作Redis

ValueOperations

取得ValueOperations物件

ValueOperations<String, String> vops = redisTemplate.opsForValue();

寫入字串

vops.set("vops_key","vops_value");

讀取字串

String vops_value =  vops.get("vops_key");

寫入/讀取物件

如同之前提到,寫入的Value預設是使用JdkSerializationRedisSerializer。因此也可以寫入有implements Serializable的物件

ValueOperations<String, TestUser> vops = redisTemplate.opsForValue();

vops.set("user:001", new TestUser("Allen", 20));

TestUser user001 = vops.get("user:001");

TestUser需implements Serializable,如下:

public class TestUser implements Serializable {
    private String name;
 private int age;
 public TestUser(String name, int age) {
// ...

ListOperations

建立ListOperations物件

ListOperations<String, String> listOp = redisTemplate.opsForList();

寫入list

long result = listOp.leftPush(key, value);

讀取list

List<String> values = lop.range(key, 0, -1);

不難發現,多數函數名稱都與原本的Redis指令很接近;若已熟悉Redis的話,使用Lettuce也能很快就上手。

刪除key

刪除資料時,直接使用redisTemplate的delete即可。

redisTemplate.delete(key)

2019年4月22日

Project Lombok

Lombok Project 是一個很小的 Java Annotation Library,目的是消除 Java 中一直不斷發生的重複程式碼,以 Java Bean 為例,最常見的就是需要在撰寫 class member 後,還需要寫一堆重複的 getter/setter,雖然 IDE 可以協助產生這些程式碼,但遇到欄位名稱修改,或是增刪欄位時,還是會造成困擾。

Installation

參考 Lombok 網頁的 Install 部分,基本上針對 project,就參考 Build tools 的部分,以我們使用來說,是看 maven。

在 Maven POM 裡面加上這個 dependency library,就可以使用了。

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.2</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

另外,通常 project 會搭配某個 IDE 進行開發,我們在 Intellij IDEA 安裝 Lombok plugin。

  • 在 Plugins 功能中,Browse repositories
  • 然後搜尋 Lombok Plugin
  • Install Plugin
  • Restart IDEA

因為 Lombok 只會將程式碼產生在編譯後的 class 裡面,在 IDE 必須要搭配 Plugin,才能偵測到 Lombok Plugin,並在編譯前了解 Lombok 會產生的 method/code,安裝了 Plugin 才不會看到編譯 Error 的警告。

Features

@Getter @Setter
@Getter
@Setter
private boolean employed = true;

@Setter(AccessLevel.PROTECTED)
private String name;

就等於以下的 Java Code

private boolean employed = true;
private String name;

public boolean isEmployed() {
    return employed;
}

public void setEmployed(final boolean employed) {
    this.employed = employed;
}

protected void setName(final String name) {
    this.name = name;
}
@NonNull

會在 setter 產生 null check,發生 null 時,會產生 NullPointerException。

@Getter
@Setter
@NonNull
private String lastname;

等同以下 java code

public void setLastname(String lastname) {
    if (lastname == null) throw new java.lang.NullPointerException("lastname");
    this.lastname = lastname;
}

public String getLastname() {
    return lastname;
}
@ToString
@ToString(callSuper=true,exclude="employed")
public class TestBean {
    private boolean employed = true;
    private String name;
    private String lastname;
}

等同以下 java code

public class TestBean {
   private boolean employed = true;
    private String name;
    private String lastname;
    
    @java.lang.Override
    public java.lang.String toString() {
        return "TestBean("super=" + super.toString() +
            ", name =" + name +
            ", lastname =" + lastname + ")";
    }
}
@EqualsAndHashCode

產生 equals 與 hashCode

@EqualsAndHashCode(callSuper=true, exclude={"employed"})
public class TestBean extends Person {
    private boolean employed = true;
    private String name;
    private String lastname;
}

等同以下 java code

public class TestBean extends Person {
    private boolean employed = true;
    private String name;
    private String lastname;
    
    @java.lang.Override
    public boolean equals(final java.lang.Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        if (!super.equals(o)) return false;
        final TestBean other = (TestBean)o;
        if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
        if (this.lastname == null ? other.lastname != null : !this.lastname.equals(other.lastname)) return false;
        return true;
    }
    
    @java.lang.Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = result * PRIME + super.hashCode();
        result = result * PRIME + (this.name == null ? 0 : this.name.hashCode());
        result = result * PRIME + (this.lastname == null ? 0 : this.lastname.hashCode());
        return result;
    }
}
@Data

這是 Lombok 最常用的 annotation,包含了 @ToString, @EqualsAndHashCode, @Getter, @Setter 的功能。

@Cleanup

可確保 resource 會被 released,但 Java 7 以後已經有了 auto resource management 的功能,所以可以不使用這個功能。

@Synchronized

會產生一個 locking field,也就是 $lock 物件,用來同步該 method

private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

@Synchronized
public String synchronizedFormat(Date date) {
    return format.format(date);
}

等同以下 java code

private final java.lang.Object $lock = new java.lang.Object[0];
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY");

public String synchronizedFormat(Date date) {
    synchronized ($lock) {
        return format.format(date);
    }
}
@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

@NoArgsConstructor 會產生沒有 parameters 的 constructor,但如果有 final field 時,則無法使用這個 annotation,會發生編譯錯誤。

但可以改用 @NoArgsConstructor(force = true),這會將所有 final fields 初始化為 0/false/null。

@RequiredArgsConstructor 會產生一個 constructor,針對每一個需要特殊處理的 fields,都會產生一個參數,例如 final field,以及標記為 @NonNull field。

@AllArgsConstructor 會產生一個 constructor,class 裡面每一個 field 都產生一個對應的 constructor 參數

javap

編譯後的 class 可利用 javap 工具反組譯,並瞭解 Lombok 產生的 class 裡面包含的內容。

$ javap TestBean.class
Compiled from "TestBean.java"
public class test.TestBean extends test.Person {
  public test.TestBean();
  public boolean equals(java.lang.Object);
  protected boolean canEqual(java.lang.Object);
  public int hashCode();
}

Summary

其他的 annotaion 可參考 features 網頁的說明,但明顯最常用的是 @Data,這個功能就能減輕不少工作。

References

Reducing Boilerplate Code with Project Lombok

2019年4月15日

Kotlin Higher-order functions: lambdas as parameters and return values

higher-order functions: 自訂 function 且使用 lambda 為 parameters 或是 return values

inline functions: 可減輕使用 lambda 的 performance overhead

宣告 higher-order functions

例如 list.filter { x > 0 } 以 predicate function 為參數,就是一種 higher-order function

Function types

要知道如何定義 lambda 為參數的 function

我們已經知道,再不需要定義 type 也能使用 lambda

val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }

compiler 是將程式轉換為

// 以兩個 Int 為參數,回傳 Int
val sum: (Int, Int) -> Int = { x, y -> x + y }
// 沒有參數,沒有 return value
val action: () -> Unit = { println(42) }

val sum: (Int, Int) -> Int 其中 (Int, Int) 是 parameter types,後面的 Int 是 return type

如果可以回傳 null,就這樣定義

var canReturnNull: (Int, Int) -> Int? = { null }

宣告為 funcation type with a nullable return type

var funOrNull: ((Int, Int) -> Int)? = null

callback 是有參數名稱的 function type

fun performRequest(
       url: String,
       callback: (code: Int, content: String) -> Unit
) {
    /*...*/
}

fun main(args: Array<String>) {
    val url = "http://kotl.in"
    performRequest(url) { code, content -> /*...*/ }
    performRequest(url) { code, page -> /*...*/ }
}
calling functions passed as arguments

twoAndTree 的參數中 operation: (Int, Int) -> Int 是 function type

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

fun main(args: Array<String>) {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}

另一個例子

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if ( predicate(element) ) sb.append(element)
    }
    return sb.toString()
}

fun main(args: Array<String>) {
    println("ab1c".filter { it in 'a'..'z' })
}

fun String.filter(predicate: (Char) -> Boolean): String

  • 前面的 String 是 receiver type
  • predicate 是 parameter name
  • (Char) -> Boolean 是 function type
Using function type from Java

kotlin 宣告的有 function type 為參數的 function

/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
    println(f(42))
}

在 Java 可用 lambda 使用

public class ProcessTheAnswerLambda {
    public static void main(String[] args) {
        processTheAnswer(number -> number + 1);
    }
}

舊版 java 要用 anonymous class

import static ch08.ProcessTheAnswer.ProcessTheAnswer.*;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

/* Java */

public class ProcessTheAnswerAnonymous {
    public static void main(String[] args) {
        processTheAnswer(
            new Function1<Integer, Integer>() {
                @Override
                public Integer invoke(Integer number) {
                    System.out.println(number);
                    return number + 1;
                }
            });
    }
}

在 java 使用 forEach

import java.util.ArrayList;
import java.util.Collections;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import java.util.List;

/* Java */

public class UsingForEach {
    public static void main(String[] args) {
        List<String> strings = new ArrayList();
        strings.add("42");
        CollectionsKt.forEach(strings, s -> {
           System.out.println(s);
           return Unit.INSTANCE;
        });
    }
}
default and null values for parameters with function types

先前 joinToString 的實作

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        transform: (T) -> String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}

缺點是 transform 裡面永遠會使用 toString 轉換字串

修改為 nullable parameter of a function type

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        transform: ((T) -> String)? = null
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        val str = transform?.invoke(element)
            ?: element.toString()
        result.append(str)
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}
returning functions from functions

計算 cost of shipping depending on the selected shipping method,實作 logic variant,回傳對應的 function

enum class Delivery { STANDARD, EXPEDITED }

class Order(val itemCount: Int)

// 宣告回傳 function 的 function
fun getShippingCostCalculator(
        delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        // return lambdas from the function
        return { order -> 6 + 2.1 * order.itemCount }
    }

    return { order -> 1.2 * order.itemCount }
}

fun main(args: Array<String>) {
    val calculator =
        getShippingCostCalculator(Delivery.EXPEDITED)
    // 呼叫 returned function
    println("Shipping costs ${calculator(Order(3))}")
}

另一個例子,GUI contract management application,需要根據 UI 狀態,決定要顯示哪些 contracts

data class Person(
        val firstName: String,
        val lastName: String,
        val phoneNumber: String?
)

class ContactListFilters {
    var prefix: String = ""
    var onlyWithPhoneNumber: Boolean = false

    // 宣告產生 function 的 function
    fun getPredicate(): (Person) -> Boolean {
        val startsWithPrefix = { p: Person ->
            p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
        }
        if (!onlyWithPhoneNumber) {
            return startsWithPrefix
        }
        return { startsWithPrefix(it)
                    && it.phoneNumber != null }
    }
}

fun main(args: Array<String>) {
    val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
                          Person("Svetlana", "Isakova", null))
    val contactListFilters = ContactListFilters()
    with (contactListFilters) {
        prefix = "Dm"
        onlyWithPhoneNumber = true
    }
    println(contacts.filter(
        contactListFilters.getPredicate()))
}
removing duplication through lambdas

analyzes visits to a website: SiteVisit 儲存 path of each visit, duration, OS

data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }

val log = listOf(
    SiteVisit("/", 34.0, OS.WINDOWS),
    SiteVisit("/", 22.0, OS.MAC),
    SiteVisit("/login", 12.0, OS.WINDOWS),
    SiteVisit("/signup", 8.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)

要計算windows 的平均使用時間,直接寫一個 function

val averageWindowsDuration = log
    .filter { it.os == OS.WINDOWS }
    .map(SiteVisit::duration)
    .average()

fun main(args: Array<String>) {
    println(averageWindowsDuration)
}

改寫,讓 averageDurationFor 可傳入 OS 參數

fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }.map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor(OS.WINDOWS))
    println(log.averageDurationFor(OS.MAC))
}

同時計算 ios, android

val averageMobileDuration = log
    .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
    .map(SiteVisit::duration)
    .average()

fun main(args: Array<String>) {
    println(averageMobileDuration)
}

以 function 動態調整過濾的條件

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
        filter(predicate).map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor {
        it.os in setOf(OS.ANDROID, OS.IOS) })
    println(log.averageDurationFor {
        it.os == OS.IOS && it.path == "/signup" })
}

inline function: 減輕 lambda 的 overhead

how inlining works

宣告 function 為 inline,就表示會直接替代 code,而不是用呼叫 function 的方式

以下是 synchronized 的做法,鎖定 lock 物件,執行 action,最後 unlock

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

val l = Lock()
synchronized(l) {
    // ...
}

因宣告為 inline,以下這些 code

fun foo(l: Lock) {
    println("Before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")
}

synchronized(l) 會轉換為

    l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
inline function 的限制

並非每一個使用 lambdas 的 function 都可以改為 inline

當 function 為 inlined,body of the lambda expression 傳入當作參數,會直接替換為 inline function,如果 function 直接被呼叫,就可以 inline,但如果需要儲存 function,後面才要使用,就不能用 inline

inlining collection operations

kotlin 的 filter 是定義為 inline, map 也是,因為 inline,就不會產生額外的 classes or objects

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun main(args: Array<String>) {
    println(people.filter { it.age < 30 })
    
    println(people.filter { it.age > 30 }.map(Person::name))
}
什麼時候要用 inline functon

使用 inline 只會在 function 使用 lambda 為參數的狀況下,改進效能

JVM 有支援 inlining support,他會分析 code,並嘗試 inline code

除了避免產生多餘的 class for each lambda 及 object for the lambda instance,另外 JVM 還不夠聰明,能夠找出所有可以 inline 的狀況

Using inlined lambdas for resource management

另一個 lambda 能減少重複程式碼的狀況是 resource management,取得 Resource -> 使用 -> release

Resource 可能是 file, lock, database transaction ....

通常會用 try/finally statement 包裝起來

kotlint 提供 "withLock" function,可處理 synchronized 的工作

val l: Lock = ...

l.withLock {
    // access the resource protected by this lock
}

withLock 是這樣實作的

fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

java 提供 try-with-resources statement

/* Java */
static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
        new BufferedReader(new FileReader(path))) {
            return br.readLine();
    }
}

kotlin 沒有對應的語法,但可以用 "use" 搭配 lambda 提供相同的功能

import java.io.BufferedReader
import java.io.FileReader
import java.io.File

fun readFirstLineFromFile(path: String): String {
    BufferedReader(FileReader(path)).use { br ->
        return br.readLine()
    }
}

control flow in higher-order functions

return statements in lambdas: return from an enclosing function

在 list of Person 裡面尋找 Alice

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    for (person in people) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

將 for 改為 forEach,在 lambda function 中留下 return,這個 return 是 non-local return,這種 return 只對 function 使用 inlined lambda function 為參數有作用

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

因為 forEach 的 lambda function 是用 inlined,因此可使用 return

returning form lambdas: return with label

local return 類似 break 的功能,會中斷 lambda 的執行,

區分 local return 與 non-local,可使用 labels,可將 lambda expression 加上 label,並在 return 時參考此 label

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") {
            println("Found!")
            return@label
        }
    }
    // 永遠會執行這一行
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

也可以用 lambda 做為 label

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return@forEach
        }
    }
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

也可以將 this 加上 label

fun main(args: Array<String>) {
    // implicit receiver 為 this@sb
    println(StringBuilder().apply sb@{
       listOf(1, 2, 3).apply {
           // this 參考到 closest implicit receiver in the scope
           this@sb.append(this.toString())
       }
    })
}
anonymous functions: local returns by default

使用 anonymous function 替代 lambda expression

return 會參考到 closest functon: 也就是 anonymous function

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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

另一個例子

people.filter(fun (person): Boolean {
    return person.age < 30
})

people.filter(fun (person) = person.age < 30)

References

Kotlin in Action