JDK1.8-Stream API使用

一、前言

  在目前用到的JDK8的功能当中,毫无疑问Stream的使用是最多的,所以通过这篇文章来学习总结一下。
  首先,Java8的Stream是对集合对象操作的API,它专注于对集合对象进行各种非常便利,高效的聚合操作或者大批量操作,从而减少代码的复杂度。借助于lambda表达式,极大的提高编程效率和程序可读性。并且Stream支持串行和并行两种模式,使我们无需编写太多代码,就可以很方便的写出高性能的并发程序。

二、Stream结构及构建

public interface Stream<T> extends BaseStream<T, Stream<T>>
public interface BaseStream<T, S extends BaseStream<T, S>>
    extends AutoCloseable { {

可以看到,Stream继承自BaseStream接口,而BaseStream又继承自AutoCloseable接口,顾名思义,AutoCloseable负责流的自动关闭。

我们这里来了解下生成Stream的几种常用方式:

// 1. 借助Stream的of方法
Stream stream = Stream.of("a", "b", "c");
String [] strArray = new String[] {"a", "b", "c"};
// 2. 通过数组生成Stream
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. 通过集合来生成Stream
List<String> list = Arrays.asList(strArray);
stream = list.stream();

而对于基础数值类型,目前提供了三种对应的包装类型Stream:IntStreamLongStreamDoubleStrem,当然我们也可以使用 Stream<Integer>Stream<Long>Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

三、Stream使用

先说下Stream的类型,Stream一般情况下包含了两个类型:中间操作(Intermediate)和结束操作(Terminal):

  1. Intermediate,所谓的中间操作,就是说每次调用做一些处理之后会返回一个新的Stream,这类操作都是惰性的,也就是说并没有真正开始流的遍历。这些操作包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel等;
  2. Terminal,一个Stream只能执行一次结束操作,而且只能是最后一个操作,执行terminal之后,Stream被消费掉了,并且产生了一个结果,这些操作包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny等;

  再简单说下Stream流的特点,Stream其实有点类似于迭代器,每个Stream只能操作一次,操作过之后就不能再操作该对象了,也就是一种单向的,不可重复操作的对象。

2. map方法

该方法的作用就是将input Stream的每一个元素,按照一定规则处理之后,映射成output Stream的另一个元素,相当于一对一的输入输出,平时的时候该方式使用较多。比如我们将字符串数组中所有的对象转为大写:

List<String> list = Arrays.asList("stream", "map");
Stream<String> stream = list.stream();
List<String> newList = stream.map(input -> input.toUpperCase()).collect(Collectors.toList());

因为Stream只能使用一次,如果我们再操作的话就是抛出异常:

List<String> newList = stream.map(input -> input.toUpperCase()).collect(Collectors.toList());
newList = stream.map(input -> input.toLowerCase()).collect(Collectors.toList());
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
    at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
    at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
    at java.util.stream.ReferencePipeline$3.<init>(ReferencePipeline.java:187)
    at java.util.stream.ReferencePipeline.map(ReferencePipeline.java:186)
3. mapToInt/mapToLong/mapToDouble方法

顾名思义,这就是将对应的Stream转为IntStream,LongStream,DoubleStream

List<Integer> list = Arrays.asList(100, 200);
IntStream intStream = list.stream().mapToInt(input -> input);
4. flatMap方法

前面说过的map方法是一对一的输入输出,而flatMap方法则是一种一对多的映射关系。

Stream<List<Integer>> inputStream = Stream.of(
        Arrays.asList(1),
        Arrays.asList(2, 3),
        Arrays.asList(4, 5, 6)
);
Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream());
List<Integer> list = outputStream.collect(Collectors.toList());
System.out.println(list);
[1,2,3,4,5,6]

flatMap 是对input Stream 中的层级进行结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面都是单个的数字。我们再来简单看下map方法的对应实现:

// inputStream不变
Stream<Stream<Integer>> stream = inputStream.map(childList -> childList.stream());
List<List<Integer>> list = stream.map(input -> input.collect(Collectors.toList())).collect(Collectors.toList());
System.out.println(list);
[[1], [2, 3], [4, 5, 6]]

从这里可以大致看出它们的区别,对于flatMap来说,它的输入输出大致如下:

{{1,2},{3,4},{5,6}}  -> flatMap  -> {1,2,3,4,5,6}

而对map方法来说,则是:

{{1,2},{3,4},{5,6}}  -> map -> {1,2}, {3,4},{5,6}
5. filter方法

该方法用于对Stream中的元素按照某些条件进行过滤,过滤后的元素生成一个新的元素,比如过滤数组中的偶数:

Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Stream.of(sixNums).filter(n -> (n % 2 == 0)).forEach(num -> System.out.print(num + " "));
2 4 6 
6. foreach方法

类似于for循环,用于遍历Stream中的每个元素,比较简单,可能需要注意的是,forEach 不能修改自己包含的本地变量值,也不能使用 break/return 之类的关键字提前结束循环:

Stream<String> stream = Stream.of("hello", "world");
// 方式1
stream.forEach(num -> System.out.print(num));
// 方式2
stream.forEach(System.out::print);
7. findFirst

返回Stream对象的第一个元素,由于返回的是Optional,所以返回的值有可能为空:

Stream<String> stream = Stream.of("hello", "world");
Optional<String> optional = stream.findFirst();
String name = optional.map(String::toLowerCase).orElse("");
System.out.println(name);

Optional是jdk8提供的一种用于优雅的解决 NullPointExecption 的方式,等下篇文章我们来学习一下。

8. reduce方法

这个方法的作用主要是把Stream中的元素组合起来,比如说字符串拼接,数值类型的求和等都是特殊的reduce操作,并且我们可以根据重载方法选择是否有初始值。

// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和的另一种形式
int sum = Stream.of(1, 2, 3, 4).reduce(0, (a,b) -> a+b);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F")
        .filter(x -> x.compareTo("Z") > 0)
        .reduce("", String::concat);

上面代码例如第一个示例的 reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator,这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),返回的是 Optional,请留意这个区别。

9. limit/skip方法

limit方法用于返回Stream元素的前n个元素,而skip方法是跳过前n个元素返回剩余的元素:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
list.stream().limit(8).forEach(System.out::print);
System.out.println();
list.stream().limit(8).skip(3).forEach(System.out::print);
12345678
45678
10. sorted

sorted方法是用于对Stream元素进行排序的,我们可以按照默认的自然排序规则进行排序, 也可以指定具体的比较器来进行排序:

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

Stream的sorted方法比数组的排序更强之处在于,你可以首先对 Stream 进行各类 map、filter、limit、skip操作之后再进行排序:

List<Integer> list = Arrays.asList(5, 7, 1, 4, 2, 6, 3, 8, 9, 10);
list.stream().limit(5).sorted().forEach(System.out::print);
System.out.println();
list.stream().limit(5).sorted(Comparator.reverseOrder()).forEach(System.out::print);
12457
75421
11. min/max/distinct方法

min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。我们来看一下获取最小值的方式:

List<Integer> list = Arrays.asList(5, 7, 1, 4, 2, 6, 3, 8, 9, 10);
// 通过Stream的min方法
Integer min2 = list.stream().min(Comparator.naturalOrder()).get();
// 通过IntStrem的min方法
Integer min1 = list.stream().mapToInt(input -> input).min().getAsInt();

而distinct方法是用于过滤重复数据的:

List<Integer> list = Arrays.asList(5, 7, 1, 7, 2, 6, 3, 9, 9, 10);
list.stream().distinct().forEach(System.out::print);
571263910
12. allMatch/anyMatch/noneMatch方法
  • allMatch, 对Stream中的所有元素进行判断,全部满足条件的时候返回true,只要有一个不符合条件就返回false;
  • anyMatch,Stream中只要有一个满足条件,就返回true;
  • noneMatch,Stream中没有一个元素满足条件,这时返回true;
List<Integer> list = Arrays.asList(5, 7, 1, 7, 2, 6, 3, 9, 9, 10);
boolean isAllRight = list.stream().allMatch(input -> (input > 0));
boolean isAnyRight = list.stream().anyMatch(input -> (input > 2));
boolean isNoneRight = list.stream().noneMatch(input -> (input < 0));
System.out.println("isAllRight:" + isAllRight + " isAnyRight:" + isAnyRight + " isNoneRight:" + isNoneRight);
isAllRight:true isAnyRight:true isNoneRight:true
13. peek方法

  peek方法,会生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数。比如说foreach方法是一个terminal 操作,执行之后,Stream就消费掉了,我们无法对一个Stream进行两次操作,而peek方法作为intermediate 操作,则可以达到类似的目的:

Stream.of("one", "two", "three", "four")
    .filter(e -> e.length() > 3)
    .peek(e -> System.out.println("Filtered value: " + e))
    .map(String::toUpperCase)
    .peek(e -> System.out.println("Mapped value: " + e))
    .collect(Collectors.toList());
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR

如上,我们可以在遍历列表的时候,先打印字符串,再将该字符串转成大写再打印出来。

另外,根据API的说明,该方法主要用于调试,方便debug查看Stream内进行处理的每个元素。

14. forEachOrdered方法

  forEachOrdered方法和 forEach 方法功能一样,都是用于遍历Stream,不同的地方在于并行流的处理上。并行的时候 forEach 方法为了效率,它的顺序和Stream元素的顺序不一定完全一样,而forEachOrdered 方法的顺序则是和Stream元素的顺序是一样的。

List<String> list = Arrays.asList("x", "y", "z");

list.parallelStream().forEach(x -> System.out.print(" " + x));
System.out.println();
list.parallelStream().forEachOrdered(x -> System.out.print(" " + x));
System.out.println();

//输出的顺序不一定(效率更高)
Stream.of("AAA", "BBB", "CCC").parallel().forEach(s -> System.out.print(" " + s));
System.out.println();
//输出的顺序与元素的顺序严格一致
Stream.of("AAA", "BBB", "CCC").parallel().forEachOrdered(s -> System.out.print(" " + s));
 y x z
 x y z
 BBB CCC AAA
 AAA BBB CCC
15. toArray方法

这个方法比较简单,就是返回对应的数组,该方法默认是返回Object数组,不过我们可以使用它的重载方法返回对应格式的数组:

Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);

对应例子:

List<String> list = Arrays.asList("x", "y", "z");
Object[] objects = list.stream().toArray();
Integer[] arrays = list.stream().toArray(Integer[]::new);
16. count方法

count方法表示获取Stream流中元素的数量,返回long类型:

// 打印 4 
long num = Stream.of(1, 2, 3, 4).count();
// 打印 3
long num = Stream.of(1, 2, 3, 4).limit(3).count();
17. findAny方法

findAny方法表示从流中随便选择一个元素,该方法返回的值是不稳定的:

Integer num = Stream.of(1, 2, 3, 4).findAny().get();
18. collect方法

collect方法我们前面已经接触过,有两个方法,我们先看一下简单的那个:

<R, A> R collect(Collector<? super T, A, R> collector);

在前文中,我们使用map方法对流进行处理之后,返回的还是一个Stream,而此时我们是无法我们的集合操作的,这时候就需要将流重新转换为集合框架中对应的集合,那么这时候我们就可以通过该方法来实现:

List<String> list = Arrays.asList("hello", "world").stream().collect(Collectors.toList());

该方法接收一个Collector类型的参数,但幸运的是Java8给我们提供了Collector的工具类:Collectors,这其中已经定义了一些静态工厂方法,比如Collectors.toCollection() 生成集合,Collectors.toList()生成List,Collectors.toSet() 生成Set等,Collectors是个很好的工具类,封装了许多操作,后续我们再来介绍。

接下来,再简单看下该方法的另一个重载方法:

<R> R collect(Supplier<R> supplier,
      BiConsumer<R, ? super T> accumulator,
      BiConsumer<R, R> combiner);

该方法比较复杂,我们先简单分析下,等以后如果用到了,再来仔细研究。该方法有三个参数:Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。来简单看一下例子吧:

List<Integer> nums = Arrays.asList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
        collect(() -> new ArrayList<Integer>(),
                (list, item) -> list.add(item),
                (list1, list2) -> list1.addAll(list2));

使用方法引用来优化下该例子:

List<Integer> nums = Arrays.asList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
        collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

接下来,说下该方法:该方法是将一个Integer类型的List,先过滤掉为null的元素,然后把剩下的元素放到新的List中。再来看一下这些参数:

    1. 第一个函数生成一个新的ArrayList实例;
    1. 第二个函数接受两个参数,第一个是前面生成的ArrayList对象,第二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。第二个函数被反复调用直到原stream的元素被消费完毕;
    1. 第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;

这么来看,这个方法是有点复杂,并且单看这个例子的话,是完全可以使用上面那个重载方法然后借助Collectors.toList来实现的。对这个方法的了解就到这了,等以后如果用到了,再来更新。

接下来是Stream的静态方法,这些静态方法目的都是为了创建Stream流。

1. of方法

Stream的of方法用来构建有序的Stream对象,有两个方法,提供单个对象及多个对象的构建:

public static<T> Stream<T> of(T t)
public static<T> Stream<T> of(T... values) 

比如说:

Stream stream = Stream.of("a");
IntStream intStream= IntStream.of(1, 2, 3);
2. builder方法

  通过使用Stream.builder方法生成Builder对象,Builder对象是Stream的可变构造器,也称为流构造器,该对象允许单独生成元素并添加到构造器,然后来生成流,来避免使用ArrayList作为临时缓冲区产生的复制开销。
  流构建器有一个生命周期,它从一个构建阶段开始,在这个阶段中可以添加元素,然后过渡到一个构建阶段,在这个阶段之后,可能不会添加元素。构建阶段从调用build()方法开始,该方法创建一个有序流,其元素是按照添加到流构建器的顺序添加到流构建器的元素。

Stream.Builder builder = Stream.builder();
builder.accept("hello");
builder.add("world");
Stream stream = builder.build();
stream.forEach(input -> System.out.print(input + " "));

//或者
Stream<String> streamBuilder = Stream.<String>builder().add("hello").add("world").build();
hello world 
3. empty方法

创建一个不包含任何元素的有序的Stream流:

Stream<Integer> stream = Stream.empty();
4. iterate方法

Stream的iterate方法和reduce方法有点像,接受一个种子值,和一个 UnaryOperator(例如 f),然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推:

// 比如生成等差数列 0 3 6 9 12 15 18 21 24 27 
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));

同样,iterate也是无限的,在进行iterate的时候,必须要有limit这样的操作来限制大小,但iterate生成的Stream是连续且有序的。

5. generate方法

  通过实现 Supplier 接口,我们可以自己来控制流的生成,这种情形通常用于随机数、常量的 Stream,把 Supplier 实例传递给 Stream.generate() ,这种生成的 Stream流是无限的,所以我们必须使用limit等方法来限制Stream的大小,并且通过generate方法生成的Stream是无序的;

// 生成10个随机数
Stream.generate(new Random()::nextInt).limit(10).forEach(System.out::println);
//另外一种方式
IntStream.generate(() -> (int) (System.nanoTime() % 100)).limit(10).forEach(System.out::println);
6. concat方法

返回两个Stream流连接的流:

public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) 
Stream<Integer> stream1 = Stream.of(1, 2, 3, 4);
Stream<Integer> stream2 = Stream.of(5, 6, 7, 8);
Stream.concat(stream1, stream2).forEach(System.out::print);

四、IntStream LongStream DoubleStream补充

因为这三个Stream中的操作属于数值操作,所以它们中有些方法Stream中并没有,我们也来简单介绍下。由于这三个Stream都差不多,我们就以IntStream来进行举例。

1. sum/min/max/count/average方法

min,max,count方法Stream中都有,只不过在IntStream中这些方法的参数和返回值可能和Stream方法的返回值有稍许的不同,不过功能是一样的。

// 计算min
IntStream.of(1, 2, 3, 4).min().getAsInt();
IntStream.of(1, 2, 3, 4).reduce(0, Integer::min);

// 计算max
IntStream.of(1, 2, 3, 4).max().getAsInt();
IntStream.of(1, 2, 3, 4).reduce(0, Integer::max);

sum方法的话,就是用于计算Stream中元素值的和,同样也可也使用reduce方法来代替:

int sum = IntStream.of(1, 2, 3, 4).sum();
int sum = IntStream.of(1, 2, 3, 4).reduce(0, Integer::sum);

而average方法是用于计算平均值的,返回的是OptionalDouble类型:

// 计算平均值
double average = IntStream.of(1, 2, 3, 4).average().getAsDouble();
2. summaryStatistics方法

该方法用于获取Stream流的各项汇总数据,我们直接看例子就明白了:

// 各种计算值的汇总数据
IntSummaryStatistics summaryStatistics = IntStream.of(1, 2, 3, 4).summaryStatistics();
// 平均值,元素个数,最大值,最小值,总和
System.out.println(summaryStatistics.getAverage());
System.out.println(summaryStatistics.getCount());
System.out.println(summaryStatistics.getMax());
System.out.println(summaryStatistics.getMin());
System.out.println(summaryStatistics.getSum());
3. asLongStream/asDoubleStream方法

这两个方法比较简单,就是转为对应的LongStream流和DoubleStream流。

4. boxed方法

基础类型的装箱操作,比如将int类型装箱称为Integer类型:

Stream<Integer> stream = IntStream.of(1, 2, 3, 4).boxed();
5. range方法

range方法是IntStream中的静态方法,用于构建某段范围的IntStream流:

public static IntStream range(int startInclusive, int endExclusive) 

创建的Stream流包含开始值 startInclusive(inclusive),但不包含结束值 endExclusive(exclusive):

IntStream intStream = IntStream.range(1, 5);
// 1 2 3 4 
intStream.forEach(x -> System.out.print(x + " "));
6. rangeClosed方法

rangeClosed方法和range方法唯一的不同就是,创建的Stream流既包含开始值,又包含结束值,这点从参数命名上就可以知道。不得不说,该方法参数的命名很规范,值得我们学习:

public static IntStream rangeClosed(int startInclusive, int endInclusive)

对应的实例:

IntStream intStream = IntStream.rangeClosed(1, 5);
// 1 2 3 4 5 
intStream.forEach(x -> System.out.print(x + " "));

五、BaseStream中的方法

上面忘了说了,BaseStream作为Stream的底层接口,有几个方法值得了解一下:

1. parallel方法

返回一个并行的且等效流,可能返回该流本身,因为该Stream已经是并行的,或者该Stream的底层状态被修改为了并行。

2. isParallel方法

判断该Stream是否是并行的:

IntStream intStream = IntStream.rangeClosed(1, 5);
// false
boolean isParallel = intStream.isParallel();
// true
isParallel = intStream.parallel().isParallel();
3. iterator/spliterator方法

这两个方法就比较简单了,iterator就是返回迭代器对象,而spliterator则是返回一个并行的迭代器对象;

4. unordered方法

  返回一个无序的等效的Stream,可能返回的是Stream本身,因为该Stream已经是无序的,或者该Stream的底层状态被修改为了无序。当不考虑流的顺序时,可以使用无序的Stream来进行操作,这样可以加快一些方法的执行速度,提高一些性能,一般用于并行的时候。对于unordered方法有个小问题可参考:https://stackoverflow.com/questions/21350195/stream-ordered-unordered-problems/38038578
https://coderanch.com/t/692966/certification/unordered-streams

本文参考自:
官方Api
Java 8 中的 Streams API 详解
Java8初体验(二)Stream语法详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容