深入理解?Java?函數(shù)式編程系列?第?4部分?使用?Vavr?進(jìn)行函數(shù)式編程

在本系列的上一篇文章中對 Java 平臺(tái)提供的 Lambda 表達(dá)式和流做了介紹。受限于 Java 標(biāo)準(zhǔn)庫的通用性要求和二進(jìn)制文件大小荚斯,Java 標(biāo)準(zhǔn)庫對函數(shù)式編程的 API 支持相對比較有限妖啥。函數(shù)深入理解 Java 函數(shù)式編程系列 第 1 部分 函數(shù)式編程思想概論的聲明只提供了 Function 和 BiFunction 兩種霉颠,流上所支持的操作的數(shù)量也較少。為了更好地進(jìn)行函數(shù)式編程荆虱,我們需要第三方庫的支持蒿偎。Vavr 是 Java 平臺(tái)上函數(shù)式編程庫中的佼佼者朽们。

Vavr 這個(gè)名字對很多開發(fā)人員可能比較陌生。它的前身 Javaslang 可能更為大家所熟悉诉位。Vavr 作為一個(gè)標(biāo)準(zhǔn)的 Java 庫骑脱,使用起來很簡單。只需要添加對 io.vavr:vavr 庫的 Maven 依賴即可苍糠。Vavr 需要 Java 8 及以上版本的支持叁丧。本文基于 Vavr 0.9.2 版本,示例代碼基于 Java 10岳瞭。

元組

元組(Tuple)是固定數(shù)量的不同類型的元素的組合拥娄。元組與集合的不同之處在于,元組中的元素類型可以是不同的瞳筏,而且數(shù)量固定稚瘾。元組的好處在于可以把多個(gè)元素作為一個(gè)單元傳遞。如果一個(gè)方法需要返回多個(gè)值姚炕,可以把這多個(gè)值作為元組返回摊欠,而不需要?jiǎng)?chuàng)建額外的類來表示。根據(jù)元素?cái)?shù)量的不同柱宦,Vavr 總共提供了 Tuple0些椒、Tuple1 到 Tuple8 等 9 個(gè)類。每個(gè)元素類都需要聲明其元素類型捷沸。如 Tuple2<String, Integer>表示的是兩個(gè)元素的元組摊沉,第一個(gè)元素的類型為 String,第二個(gè)元素的類型為 Integer痒给。對于元組對象说墨,可以使用 _1、_2 到 _8 來訪問其中的元素苍柏。所有元組對象都是不可變的尼斧,在創(chuàng)建之后不能更改。

元組通過接口 Tuple 的靜態(tài)方法 of 來創(chuàng)建试吁。元組類也提供了一些方法對它們進(jìn)行操作棺棵。由于元組是不可變的,所有相關(guān)的操作都返回一個(gè)新的元組對象熄捍。在 清單 1 中烛恤,使用 Tuple.of 創(chuàng)建了一個(gè) Tuple2 對象。Tuple2 的 map 方法用來轉(zhuǎn)換元組中的每個(gè)元素余耽,返回新的元組對象缚柏。而 apply 方法則把元組轉(zhuǎn)換成單個(gè)值。其他元組類也有類似的方法碟贾。除了 map 方法之外币喧,還有 map1轨域、map2、map3 等方法來轉(zhuǎn)換第 N 個(gè)元素杀餐;update1干发、update2 和 update3 等方法用來更新單個(gè)元素。

清單 1. 使用元組

Tuple2<String, Integer> tuple2 = Tuple.of("Hello", 100);

Tuple2<String, Integer> updatedTuple2 = tuple2.map(String::toUpperCase, v -> v * 5);

String result = updatedTuple2.apply((str, number) -> String.join(", ",

str, number.toString()));

System.out.println(result);

雖然元組使用起來很方便史翘,但是不宜濫用枉长,尤其是元素?cái)?shù)量超過 3 個(gè)的元組。當(dāng)元組的元素?cái)?shù)量過多時(shí)恶座,很難明確地記住每個(gè)元素的位置和含義搀暑,從而使得代碼的可讀性變差。這個(gè)時(shí)候使用 Java 類是更好的選擇跨琳。

函數(shù)

Java 8 中只提供了接受一個(gè)參數(shù)的 Function 和接受 2 個(gè)參數(shù)的 BiFunction。Vavr 提供了函數(shù)式接口 Function0桐罕、Function1 到 Function8脉让,可以描述最多接受 8 個(gè)參數(shù)的函數(shù)。這些接口的方法 apply 不能拋出異常功炮。如果需要拋出異常溅潜,可以使用對應(yīng)的接口 CheckedFunction0、CheckedFunction1 到 CheckedFunction8薪伏。

Vavr 的函數(shù)支持一些常見特征滚澜。

組合

函數(shù)的組合指的是用一個(gè)函數(shù)的執(zhí)行結(jié)果作為參數(shù),來調(diào)用另外一個(gè)函數(shù)所得到的新函數(shù)嫁怀。比如 f 是從 x 到 y 的函數(shù)设捐,g 是從 y 到 z 的函數(shù),那么 g(f(x))是從 x 到 z 的函數(shù)塘淑。Vavr 的函數(shù)式接口提供了默認(rèn)方法 andThen 把當(dāng)前函數(shù)與另外一個(gè) Function 表示的函數(shù)進(jìn)行組合萝招。Vavr 的 Function1 還提供了一個(gè)默認(rèn)方法 compose 來在當(dāng)前函數(shù)執(zhí)行之前執(zhí)行另外一個(gè) Function 表示的函數(shù)。

在清單 2 中存捺,第一個(gè) function3 進(jìn)行簡單的數(shù)學(xué)計(jì)算槐沼,并使用 andThen 把 function3 的結(jié)果乘以 100。第二個(gè) function1 從 String 的 toUpperCase 方法創(chuàng)建而來捌治,并使用 compose 方法與 Object 的 toString 方法先進(jìn)行組合岗钩。得到的方法對任何 Object 先調(diào)用 toString,再調(diào)用 toUpperCase肖油。

清單 2. 函數(shù)的組合

Function3< Integer, Integer, Integer, Integer> function3 = (v1, v2, v3)

-> (v1 + v2) * v3;

Function3< Integer, Integer, Integer, Integer> composed =

function3.andThen(v -> v * 100);

int result = composed.apply(1, 2, 3);

System.out.println(result);

// 輸出結(jié)果 900

Function1< String, String> function1 = String::toUpperCase;

Function1< Object, String> toUpperCase = function1.compose(Object::toString);

String str = toUpperCase.apply(List.of("a", "b"));

System.out.println(str);

// 輸出結(jié)果[A, B]

部分應(yīng)用

在 Vavr 中兼吓,函數(shù)的 apply 方法可以應(yīng)用不同數(shù)量的參數(shù)。如果提供的參數(shù)數(shù)量小于函數(shù)所聲明的參數(shù)數(shù)量(通過 arity() 方法獲裙乖稀)周蹭,那么所得到的結(jié)果是另外一個(gè)函數(shù)趋艘,其所需的參數(shù)數(shù)量是剩余未指定值的參數(shù)的數(shù)量。在清單 3 中凶朗,F(xiàn)unction4 接受 4 個(gè)參數(shù)瓷胧,在 apply 調(diào)用時(shí)只提供了 2 個(gè)參數(shù),得到的結(jié)果是一個(gè) Function2 對象棚愤。

清單 3. 函數(shù)的部分應(yīng)用

Function4< Integer, Integer, Integer, Integer, Integer> function4 =

(v1, v2, v3, v4) -> (v1 + v2) * (v3 + v4);

Function2< Integer, Integer, Integer> function2 = function4.apply(1, 2);

int result = function2.apply(4, 5);

System.out.println(result);

// 輸出 27

柯里化方法

使用 curried 方法可以得到當(dāng)前函數(shù)的柯里化版本搓萧。由于柯里化之后的函數(shù)只有一個(gè)參數(shù),curried 的返回值都是 Function1 對象宛畦。在清單 4 中瘸洛,對于 function3,在第一次的 curried 方法調(diào)用得到 Function1 之后次和,通過 apply 來為第一個(gè)參數(shù)應(yīng)用值反肋。以此類推,通過 3 次的 curried 和 apply 調(diào)用踏施,把全部 3 個(gè)參數(shù)都應(yīng)用值石蔗。

清單 4. 函數(shù)的柯里化

Function3<Integer, Integer, Integer, Integer> function3 = (v1, v2, v3)

-> (v1 + v2) * v3;

int result =

function3.curried().apply(1).curried().apply(2).curried().apply(3);

System.out.println(result);

記憶化方法

使用記憶化的函數(shù)會(huì)根據(jù)參數(shù)值來緩存之前計(jì)算的結(jié)果。對于同樣的參數(shù)值畅形,再次的調(diào)用會(huì)返回緩存的值养距,而不需要再次計(jì)算。這是一種典型的以空間換時(shí)間的策略日熬」餮幔可以使用記憶化的前提是函數(shù)有引用透明性。

在清單 5 中竖席,原始的函數(shù)實(shí)現(xiàn)中使用 BigInteger 的 pow 方法來計(jì)算乘方耘纱。使用 memoized 方法可以得到該函數(shù)的記憶化版本。接著使用同樣的參數(shù)調(diào)用兩次并記錄下時(shí)間怕敬。從結(jié)果可以看出來揣炕,第二次的函數(shù)調(diào)用的時(shí)間非常短,因?yàn)橹苯訌木彺嬷蝎@取結(jié)果东跪。

清單 5. 函數(shù)的記憶化

Function2<BigInteger, Integer, BigInteger> pow = BigInteger::pow;

Function2<BigInteger, Integer, BigInteger> memoized = pow.memoized();

long start = System.currentTimeMillis();

memoized.apply(BigInteger.valueOf(1024), 1024);

long end1 = System.currentTimeMillis();

memoized.apply(BigInteger.valueOf(1024), 1024);

long end2 = System.currentTimeMillis();

System.out.printf("%d ms -> %d ms", end1 - start, end2 - end1);

注意畸陡,memoized 方法只是把原始的函數(shù)當(dāng)成一個(gè)黑盒子,并不會(huì)修改函數(shù)的內(nèi)部實(shí)現(xiàn)虽填。因此丁恭,memoized 并不適用于直接封裝本系列第二篇文章中用遞歸方式計(jì)算斐波那契數(shù)列的函數(shù)。這是因?yàn)樵诤瘮?shù)的內(nèi)部實(shí)現(xiàn)中斋日,調(diào)用的仍然是沒有記憶化的函數(shù)牲览。

Vavr 中提供了一些不同類型的值。

Option

Vavr 中的 Option 與 Java 8 中的 Optional 是相似的恶守。不過 Vavr 的 Option 是一個(gè)接口第献,有兩個(gè)實(shí)現(xiàn)類 Option.Some 和 Option.None贡必,分別對應(yīng)有值和無值兩種情況。使用 Option.some 方法可以創(chuàng)建包含給定值的 Some 對象庸毫,而 Option.none 可以獲取到 None 對象的實(shí)例仔拟。Option 也支持常用的 map、flatMap 和 filter 等操作飒赃,如清單 6 所示利花。

清單 6. 使用 Option 的示例

Option<String> str = Option.of("Hello");

str.map(String::length);

str.flatMap(v -> Option.of(v.length()));

Either

Either 表示可能有兩種不同類型的值,分別稱為左值或右值载佳。只能是其中的一種情況炒事。Either 通常用來表示成功或失敗兩種情況。慣例是把成功的值作為右值蔫慧,而失敗的值作為左值挠乳。可以在 Either 上添加應(yīng)用于左值或右值的計(jì)算藕漱。應(yīng)用于右值的計(jì)算只有在 Either 包含右值時(shí)才生效欲侮,對左值也是同理。

在清單 7 中肋联,根據(jù)隨機(jī)的布爾值來創(chuàng)建包含左值或右值的 Either 對象。Either 的 map 和 mapLeft 方法分別對右值和左值進(jìn)行計(jì)算刁俭。

清單 7. 使用 Either 的示例

import io.vavr.control.Either;

import java.util.concurrent.ThreadLocalRandom;

public class Eithers {

? private static ThreadLocalRandom random =

ThreadLocalRandom.current();

? public static void main(String[] args) {

? ? Either<String, String> either = compute()

? ? ? ? .map(str -> str + " World")

? ? ? ? .mapLeft(Throwable::getMessage);

? ? System.out.println(either);

? }

? private static Either<Throwable, String> compute() {

? ? return random.nextBoolean()

? ? ? ? ? Either.left(new RuntimeException("Boom!"))

? ? ? ? : Either.right("Hello");

? }

}

Try

Try 用來表示一個(gè)可能產(chǎn)生異常的計(jì)算橄仍。Try 接口有兩個(gè)實(shí)現(xiàn)類,Try.Success 和 Try.Failure牍戚,分別表示成功和失敗的情況侮繁。Try.Success 封裝了計(jì)算成功時(shí)的返回值,而 Try.Failure 則封裝了計(jì)算失敗時(shí)的 Throwable 對象如孝。Try 的實(shí)例可以從接口 CheckedFunction0宪哩、Callable、Runnable 或 Supplier 中創(chuàng)建第晰。Try 也提供了 map 和 filter 等方法锁孟。值得一提的是 Try 的 recover 方法,可以在出現(xiàn)錯(cuò)誤時(shí)根據(jù)異常進(jìn)行恢復(fù)茁瘦。

在清單 8 中品抽,第一個(gè) Try 表示的是 1/0 的結(jié)果,顯然是異常結(jié)果甜熔。使用 recover 來返回 1圆恤。第二個(gè) Try 表示的是讀取文件的結(jié)果。由于文件不存在腔稀,Try 表示的也是異常盆昙。

清單 8. 使用 Try 的示例

Try<Integer> result = Try.of(() -> 1 / 0).recover(e -> 1);

System.out.println(result);

Try<String> lines = Try.of(() -> Files.readAllLines(Paths.get("1.txt")))

? ? .map(list -> String.join(",", list))

? ? .andThen((Consumer<String>) System.out::println);

System.out.println(lines);

Lazy

Lazy 表示的是一個(gè)延遲計(jì)算的值羽历。在第一次訪問時(shí)才會(huì)進(jìn)行求值操作,而且該值只會(huì)計(jì)算一次淡喜。之后的訪問操作獲取的是緩存的值秕磷。在清單 9 中,Lazy.of 從接口 Supplier 中創(chuàng)建 Lazy 對象拆火。方法 isEvaluated 可以判斷 Lazy 對象是否已經(jīng)被求值跳夭。

清單 9. 使用 Lazy 的示例

Lazy<BigInteger> lazy = Lazy.of(() ->

BigInteger.valueOf(1024).pow(1024));

System.out.println(lazy.isEvaluated());

System.out.println(lazy.get());

System.out.println(lazy.isEvaluated());

數(shù)據(jù)結(jié)構(gòu)

Vavr 重新在 Iterable 的基礎(chǔ)上實(shí)現(xiàn)了自己的集合框架。Vavr 的集合框架側(cè)重在不可變上们镜。Vavr 的集合類在使用上比 Java 流更簡潔币叹。

Vavr 的 Stream 提供了比 Java 中 Stream 更多的操作∧O粒可以使用 Stream.ofAll 從 Iterable 對象中創(chuàng)建出 Vavr 的 Stream颈抚。下面是一些 Vavr 中添加的實(shí)用操作:

groupBy:使用 Fuction 對元素進(jìn)行分組。結(jié)果是一個(gè) Map嚼鹉,Map 的鍵是分組的函數(shù)的結(jié)果贩汉,而值則是包含了同一組中全部元素的 Stream。

partition:使用 Predicate 對元素進(jìn)行分組锚赤。結(jié)果是包含 2 個(gè) Stream 的 Tuple2匹舞。Tuple2 的第一個(gè) Stream 的元素滿足 Predicate 所指定的條件,第二個(gè) Stream 的元素不滿足 Predicate 所指定的條件线脚。

scanLeft 和 scanRight:分別按照從左到右或從右到左的順序在元素上調(diào)用 Function赐稽,并累積結(jié)果。

zip:把 Stream 和一個(gè) Iterable 對象合并起來浑侥,返回的結(jié)果 Stream 中包含 Tuple2 對象姊舵。Tuple2 對象的兩個(gè)元素分別來自 Stream 和 Iterable 對象。

在清單 10 中寓落,第一個(gè) groupBy 操作把 Stream 分成奇數(shù)和偶數(shù)兩組括丁;第二個(gè) partition 操作把 Stream 分成大于 2 和不大于 2 兩組;第三個(gè) scanLeft 對包含字符串的 Stream 按照字符串長度進(jìn)行累積伶选;最后一個(gè) zip 操作合并兩個(gè)流史飞,所得的結(jié)果 Stream 的元素?cái)?shù)量與長度最小的輸入流相同。

清單 10. Stream 的使用示例

Map<Boolean, List<Integer>> booleanListMap = Stream.ofAll(1, 2, 3, 4, 5)

? ? .groupBy(v -> v % 2 == 0)

? ? .mapValues(Value::toList);

System.out.println(booleanListMap);

// 輸出 LinkedHashMap((false, List(1, 3, 5)), (true, List(2, 4)))

Tuple2<List<Integer>, List<Integer>> listTuple2 = Stream.ofAll(1, 2, 3, 4)

? ? .partition(v -> v > 2)

? ? .map(Value::toList, Value::toList);

System.out.println(listTuple2);

// 輸出 (List(3, 4), List(1, 2))

List<Integer> integers = Stream.ofAll(List.of("Hello", "World", "a"))

? ? .scanLeft(0, (sum, str) -> sum + str.length())

? ? .toList();

System.out.println(integers);

// 輸出 List(0, 5, 10, 11)

List<Tuple2<Integer, String>> tuple2List = Stream.ofAll(1, 2, 3)

? ? .zip(List.of("a", "b"))

? ? .toList();

System.out.println(tuple2List);

// 輸出 List((1, a), (2, b))

Vavr 提供了常用的數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)考蕾,包括 List祸憋、Set、Map肖卧、Seq蚯窥、Queue、Tree 和 TreeMap 等。這些數(shù)據(jù)結(jié)構(gòu)的用法與 Java 標(biāo)準(zhǔn)庫的對應(yīng)實(shí)現(xiàn)是相似的拦赠,但是提供的操作更多巍沙,使用起來也更方便。在 Java 中荷鼠,如果需要對一個(gè) List 的元素進(jìn)行 map 操作句携,需要使用 stream 方法來先轉(zhuǎn)換為一個(gè) Stream,再使用 map 操作允乐,最后再通過收集器 Collectors.toList 來轉(zhuǎn)換回 List矮嫉。而在 Vavr 中,List 本身就提供了 map 操作牍疏。清單 11 中展示了這兩種使用方式的區(qū)別蠢笋。

清單 11. Vavr 中數(shù)據(jù)結(jié)構(gòu)的用法

List.of(1, 2, 3).map(v -> v + 10); //Vavr

java.util.List.of(1, 2, 3).stream()

? .map(v -> v + 10).collect(Collectors.toList()); //Java 中 Stream

模式匹配

在 Java 中,我們可以使用 switch 和 case 來根據(jù)值的不同來執(zhí)行不同的邏輯鳞陨。不過 switch 和 case 提供的功能很弱昨寞,只能進(jìn)行相等匹配。Vavr 提供了模式匹配的 API厦滤,可以對多種情況進(jìn)行匹配和執(zhí)行相應(yīng)的邏輯援岩。在清單 12 中,我們使用 Vavr 的 Match 和 Case 替換了 Java 中的 switch 和 case掏导。Match 的參數(shù)是需要進(jìn)行匹配的值享怀。Case 的第一個(gè)參數(shù)是匹配的條件,用 Predicate 來表示趟咆;第二個(gè)參數(shù)是匹配滿足時(shí)的值凹蜈。$(value) 表示值為 value 的相等匹配,而 $() 表示的是默認(rèn)匹配忍啸,相當(dāng)于 switch 中的 default。

清單 12. 模式匹配的示例


String input = "g";

String result = Match(input).of(

? ? Case($("g"), "good"),

? ? Case($("b"), "bad"),

? ? Case($(), "unknown")

);

System.out.println(result);

// 輸出 good

在清單 13 中履植,我們用 $(v -> v > 0) 創(chuàng)建了一個(gè)值大于 0 的 Predicate计雌。這里匹配的結(jié)果不是具體的值,而是通過 run 方法來產(chǎn)生副作用玫霎。

清單 13. 使用模式匹配來產(chǎn)生副作用

int value = -1;

Match(value).of(

? ? Case($(v -> v > 0), o -> run(() -> System.out.println("> 0"))),

? ? Case($(0), o -> run(() -> System.out.println("0"))),

? ? Case($(), o -> run(() -> System.out.println("< 0")))

);

// 輸出<? 0

總結(jié)

當(dāng)需要在 Java 平臺(tái)上進(jìn)行復(fù)雜的函數(shù)式編程時(shí)凿滤,Java 標(biāo)準(zhǔn)庫所提供的支持已經(jīng)不能滿足需求。Vavr 作為 Java 平臺(tái)上流行的函數(shù)式編程庫庶近,可以滿足不同的需求翁脆。本文對 Vavr 提供的元組、函數(shù)鼻种、值反番、數(shù)據(jù)結(jié)構(gòu)和模式匹配進(jìn)行了詳細(xì)的介紹。下一篇文章將介紹函數(shù)式編程中的重要概念 Monad。

參考資源

參考 Vavr 的官方文檔罢缸。

查看 Vavr 的 Java API 文檔篙贸。


?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市枫疆,隨后出現(xiàn)的幾起案子爵川,更是在濱河造成了極大的恐慌,老刑警劉巖息楔,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寝贡,死亡現(xiàn)場離奇詭異,居然都是意外死亡值依,警方通過查閱死者的電腦和手機(jī)圃泡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳞滨,“玉大人洞焙,你說我怎么就攤上這事≌玻” “怎么了澡匪?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長褒链。 經(jīng)常有香客問我唁情,道長,這世上最難降的妖魔是什么甸鸟? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮兵迅,結(jié)果婚禮上抢韭,老公的妹妹穿的比我還像新娘。我一直安慰自己恍箭,他們只是感情好刻恭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扯夭,像睡著了一般鳍贾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上交洗,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天骑科,我揣著相機(jī)與錄音,去河邊找鬼构拳。 笑死咆爽,一個(gè)胖子當(dāng)著我的面吹牛梁棠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伍掀,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掰茶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜜笤?” 一聲冷哼從身側(cè)響起濒蒋,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎把兔,沒想到半個(gè)月后沪伙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡县好,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年围橡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缕贡。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翁授,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晾咪,到底是詐尸還是另有隱情收擦,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布谍倦,位于F島的核電站塞赂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏昼蛀。R本人自食惡果不足惜宴猾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叼旋。 院中可真熱鬧仇哆,春花似錦、人聲如沸夫植。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽偷崩。三九已至,卻和暖如春撞羽,著一層夾襖步出監(jiān)牢的瞬間阐斜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工诀紊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谒出,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像笤喳,于是被迫代替她去往敵國和親为居。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容