Java8新特性之Stream的API常用函数使用及说明

因为学习了Lambda表达式,再学习Java8新特性之Stream的API常用函数使用及说明,两者结合起来,代码就可以玩得更溜,下面做Stream的一些常用函数使用及说明,方日后温习,如果发现有不足之处,请多多指教。

什么是Stream(流)?

Stream是数据渠道,用于操作数据源(如:集合,数组等)转成的元素序列,对集合中的每个元素进行一系列并行或者串行的流水线操作。

Stream的作用是什么(为什么需要Stream)?

Stream是Java8的一大亮点,它与java.io包中的InputStream、OutputStream是完全不同的概念;也不同于解析XML出来的Stream。
Stream是java对集合对象功能的增强,对集合对象进行各种非常方便、高效的聚合操作,或者对大指数据操作。Stream的API同样借助于Lambda表达式(支持函数式编程,代码非常简洁),提高编程效率和程序的可读性,同时它还提供了并行和串行这两种模式进行汇聚操。默认情况能充分利用cpu的资源(Stream操作是延迟执行的,需要结果的时候才执行)。

Stream操作的三步曲

  1. 创建Stream:通过一个数据源(如:数组,集合),获到一个流。
  2. 中间操作:一个中间操作链(如:filter,map等),对数据源的数据进行处理。
  3. 终止操作:一个终止操作,执行中间操作链,并产生结果。
Stream的步骤.png

Stream创建的四种方式

  1. Collection提供了两个方法,分别为stream()和parallelStream()。

    //通过List集合创建Stream
    List<String> list = new ArrayList<>();
    Stream<String> stream1 = list.stream();//获取一个顺序流
    Stream<String> parallelStream = list.parallelStream(); //获取一个并行流
    
  2. 通过Arrays中的静态方法stream()获取一个数组流。

    //通过Arrays中的静态方法stream()获取一个数组流
    Student[] students = new Student[2];
    Stream<Student> stream = Arrays.stream(students);
    
  3. 通过Stream类中的静态方法of()。注意:这里可以是字符串,数组,集合等其他数据类型。

    //通过Stream类中的静态方法of()。注意:这里可以是字符串,数组,集合等其他数据类型。
    Stream<String> stream3 = Stream.of("java", "lambda", "stream", "6666");
    
  4. 创建无限流。

    //使用iterate()创建无限流,这个通常和limit()一起使用,限制流中元素的个数。
    //iterate()接受一个种子值,和一个UnaryOperator(例如 f)。然后种子值成为Stream的第一个元素,
    //f(seed) 为第二个,f(f(seed)) 第三个,以此类推.
    Stream<Integer> iterate = Stream.iterate(0, x -> x + 1);
    iterate.limit(5).forEach(System.out::println);
    
    //使用generate()创建无限流,通常跟limit()一起使用,限制流中元素的个数。
    //可以根据任何计算方式来生成,通过实现Supplier接口,你可以自己来控制流的生成。
    Stream<Long> generate = Stream.generate(new Supplier<Long>() {
        long a = 1, b = 2;
        @Override
        public Long get() {
            long temp = a + b;
            a = b;
            b = temp;
            return a;
        }
    });
    generate.limit(10).forEach(System.out::println);
    

Stream的中间操作

中间操作符(如:filter,map等),多个中间操作可以连接起来形成一条流水线形式对数据源的数据进行处理。注意:如果未触发终止操作(下面会进行介绍),中间操作是不会执行任何处理,在触发终止操作时会一次性全部执行中间操作链并产生结果。

中间操作有状态之分:无状态——指元素的处理不受之前元素的影响;有状态——指该操作只有拿到所有元素之后才能继续下去。

中间操作

Stream的终止操作(结束操作)

一个终止操作,执行中间操作链,并产生结果。终止操作有状态之分:非短路——指必须处理所有元素才能得到最终结果;短路——指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果。

终止操作

Stream的实际操作

我们了解了Java8新特性Stream的API后,开始以代码实践操作一遍。

一、中间操作就是对容器(集合)的处理过程,包括:筛选(filter、limit、distinct、skip...),映射(map、flatMap...),排序(sorted...),消费(peek..)等 。先写一个学生科目分数对象,以便操作。

public class StudentSubject {
    private String studentName;
    private String subject;
    private Integer score;

    public StudentSubject(String studentName, String subject, Integer score) {
        this.studentName = studentName;
        this.subject = subject;
        this.score = score;
    }
    public String getStudentName() {
        return studentName;
    }
    public void setStudentName(String studentName) {
        this.studentName = studentName;
    }
    public String getSubject() {
        return subject;
    }
    public void setSubject(String subject) {
        this.subject = subject;
    }
    public Integer getScore() {
        return score;
    }
    public void setScore(Integer score) {
        this.score = score;
    }
    @Override
    public String toString() {
        return "StudentSubject{" +
                "studentName='" + studentName + '\'' +
                ", subject='" + subject + '\'' +
                ", score=" + score +
                '}';
    }
     @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        final StudentSubject ss = (StudentSubject) obj;
        if (this == ss) {
            return true;
        } else {
            return (this.studentName.equals(ss.studentName));
        }
    }
    @Override
    public int hashCode() {
        int hash_code = 7;
        hash_code = 13 * hash_code+ (studentName == null ? 0 : studentName.hashCode());
        return hash_code;
    }
}

注意:因为中间操作链在没有执行终止操作前是不会执行的,所有下面都是以执行forEach终止操作来验证中间操作链的使用。

筛选(filter、limit、distinct、skip...)

  • filter(),过滤掉某些元素。如:获取大90的科目信息

    StudentSubject s1 = new StudentSubject("张三", "语文", 85);
    StudentSubject s2 = new StudentSubject("小李", "语文", 91);
    StudentSubject s3 = new StudentSubject("张三", "数学", 95);
    StudentSubject s4 = new StudentSubject("小李", "数学", 95);
    StudentSubject s5 = new StudentSubject("张三", "英语", 92);
    
    List<StudentSubject> list = new ArrayList<>();
    list.add(s1);
    list.add(s2);
    list.add(s3);
    list.add(s4);
    list.add(s5);
    
    //获取大90的科目信息
    list.stream().filter(item->item.getScore()>90).forEach(System.out::println);
    
    //输出的结果为
    StudentSubject{studentName='小李', subject='语文', score=91}
    StudentSubject{studentName='张三', subject='数学', score=95}
    StudentSubject{studentName='小李', subject='数学', score=95}
    StudentSubject{studentName='张三', subject='英语', score=92}
    
  • limit(),截断流,使其元素不超过给定的数量。

    //截流,取前面三个元素信息
    list.stream().limit(3).forEach(System.out::println);
    
    //打印结果
    StudentSubject{studentName='张三', subject='语文', score=85}
    StudentSubject{studentName='小李', subject='语文', score=91}
    StudentSubject{studentName='张三', subject='数学', score=95}
    
  • distinct(),通过流所生成的元素的hashCode()和equals()去除重复元素。

    注意:这里有坑,要让distinct起作用,就必须在对应实体类中重写HashCode和equals方法。

    //通过流所生成的元素的hashCode()和equals()去除重复元素。
    list.stream().distinct().forEach(System.out::println);
    //打印结果
    StudentSubject{studentName='张三', subject='语文', score=85}
    StudentSubject{studentName='小李', subject='语文', score=91}
    
    List<String> stringList = Arrays.asList("aa","bb","cc","aa","bb","cc");
    stringList.stream().distinct().forEach(System.out::println);
    //打印结果
    aa
    bb
    cc
    
  • skip(), 跳过元素,返回一个扔掉了前N个元素的流。 如果流中元素N个,则是返回一个空流。与limit(n)互补。

    //扔掉前面三个元素
    list.stream().skip(3).forEach(System.out::println);
    //打印结果
    StudentSubject{studentName='小李', subject='数学', score=95}
    StudentSubject{studentName='张三', subject='英语', score=92}
    

映射(map、flatMap...)

  • map(),接收一个函数作为参数,该函数会被应用到每一个元素上,并将其他映射成一个新的元素。

    //>=95分的就输入科目信息,否则就为null
    list.stream().map(item->{
        if(item.getScore()>=95){
            return item;
        }
        return null;
    }).forEach(System.out::println);
    //打印结果
    null
    null
    StudentSubject{studentName='张三', subject='数学', score=95}
    StudentSubject{studentName='小李', subject='数学', score=95}
    null
    
    //>=95分的就输入true,否则就为false
    list.stream().map(item->item.getScore()>=95).forEach(System.out::println);
    //打印结果
    false
    false
    true
    true
    false
        
    //写法2 涉及到collect终止操作,下面会详细说。
    //>=95分的就输入科目信息,否则就为null
    List<StudentSubject> studentSubjectStream = list.stream().map(item -> {
        if (item.getScore() >= 95) {
            return item;
        }
        return null;
    }).collect(Collectors.toList());
    studentSubjectStream.forEach(System.out::println);
    //>=95分的就输入true,否则就为false
    List<Boolean> booleanStream = list.stream().map(item -> item.getScore() >= 95)
        .collect(Collectors.toList());
    booleanStream.forEach(System.out::println);
    
  • mapToInt

    //mapToInt 跟map都类似的,只是类型被限定为IntStream。
    IntSummaryStatistics intSummaryStatistics = list.stream().mapToInt(person-> person.getScore()).summaryStatistics();
    System.out.println("最大分数:"+intSummaryStatistics.getMax()); //最大值
    System.out.println("最小分数:"+intSummaryStatistics.getMin()); //最小值
    System.out.println("分数总和:"+intSummaryStatistics.getSum()); //总计
    System.out.println("信息条数:"+intSummaryStatistics.getCount());//个数
    System.out.println("平均分数:"+intSummaryStatistics.getAverage());//平均数返回的是double类型
    
  • mapToLong、mapToDouble 跟map都类似的,只是类型被限定为不同类型而已。这里就不举例了。注意:没有float类型。

  • flatMap(..) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。这个过程也称为元素拍平拍扁。

    //把每学生科目分数信息转成一个新有流
    list.stream().flatMap(item->{
        //将每个元素转换成一个stream
        String[] info = {item.getStudentName()+"_"+item.getSubject()+"_"+item.getScore()};
        Stream<String> newStream = Arrays.stream(info);
        return newStream;
    }).forEach(System.out::println);
    //打印结果
    张三_语文_85
    小李_语文_91
    张三_数学_95
    小李_数学_95
    张三_英语_92
    
  • flatMapToInt() 跟flatMap的作用一样 只是类型被限定为IntStream。

    //flatMapToInt(..) 和 flatMap的作用一样 只是类型被限定为IntStream
    IntStream intStream =list.stream()
        .flatMapToInt(item ->IntStream.of(item.getScore()));
    intStream.forEach(System.out::println);
    //打印结果
    85
    91
    95
    95
    92
    
  • faltMapToLong、faltMapToDouble 跟faltMap都类似的,只是类型被限定为不同类型而已。这里就不举例了。注意:没有float类型。

排序(sorted...)

  • sorted(), 排序 底层依赖Comparable 实现,也可以提供自定义比较器。

    //sorted(), 排序,底层依赖Comparable实现,也可以提供自定义比较器。
    //底层依赖Comparable
    Stream.of(1,1,0,8,2,4,6).sorted().forEach(System.out::println);
    list.stream()
        //自定义比较器  按分数由高到低
        .sorted((ss1,ss2)->ss1.getScore()<ss2.getScore()?1:ss1.getScore()==ss2.getScore()?0:-1)
        .forEach(System.out::println);
    //打印结果
    0
    1
    1
    2
    4
    6
    8
    StudentSubject{studentName='张三', subject='数学', score=95}
    StudentSubject{studentName='小李', subject='数学', score=95}
    StudentSubject{studentName='张三', subject='英语', score=92}
    StudentSubject{studentName='小李', subject='语文', score=91}
    StudentSubject{studentName='张三', subject='语文', score=85}
    
    

消费(peek...)

  • peek() 挑选,如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。可以理解为提前消费。

    //所有同学所有科目都是100分,哈哈
    list.stream().peek(item->item.setScore(100)).forEach(System.out::println);
    //打印结果
    StudentSubject{studentName='张三', subject='语文', score=100}
    StudentSubject{studentName='小李', subject='语文', score=100}
    StudentSubject{studentName='张三', subject='数学', score=100}
    StudentSubject{studentName='小李', subject='数学', score=100}
    StudentSubject{studentName='张三', subject='英语', score=100}
    

二、终止操作,执行中间操作链,并产生结果。当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。之后如果想要操作就必须新打开流。终止操作包括:循环遍历操作(forEach、forEachOrdered)、收集操作(collect..)、 匹配与聚合操作(allMatch、max、min...)

循环遍历操作

  • forEach()、forEachOrdered(),遍历操作 ,对每个数据遍历迭代。

    forEach()在上面的例子已使用过,就不举例了。注意:forEach是一个终端操作,参数也是一个函数,它会迭代中间操作完成后的每一个数据。

    forEachOrdered适用于并行流的情况下进行迭代,能确保迭代的有序性。如下面例子:

    Stream.of(1,3,7,5,4,9,8,-1)
        .parallel()
        .forEachOrdered(item->
                        System.out.println(Thread.currentThread().getName()+": "+item));
    //打印的结果
    ForkJoinPool.commonPool-worker-4: 1
    ForkJoinPool.commonPool-worker-2: 3
    ForkJoinPool.commonPool-worker-2: 7
    ForkJoinPool.commonPool-worker-2: 5
    ForkJoinPool.commonPool-worker-2: 4
    ForkJoinPool.commonPool-worker-2: 9
    ForkJoinPool.commonPool-worker-2: 8
    ForkJoinPool.commonPool-worker-2: -1
    

    parallel()提供并行操作的支持。并行这一块知识,先不在这里深入。

收集操作

  • collect(),收集操作,接收一个Collector接口的实现,将流转换为其他形式(收集到List,Set,Map等容器中)。

    //collect:接收一个Collector实例,将流中元素收集成另外一个数据结构。
    System.out.println("==========将分数 装成list=========");
    //将分数 装成list
    List<Integer> scoreList = list.stream().map(StudentSubject::getScore)
        .collect(Collectors.toList());
    scoreList.forEach(System.out::println);
    
    System.out.println("=========转成set==========");
    //转成set
    Set<Integer> scoreSet = list.stream().map(StudentSubject::getScore)
        .collect(Collectors.toSet());
    scoreSet.forEach(System.out::println);
    
    System.out.println("=========转成map==========");
    //转成map,注:key不能相同,否则报错
    Map<String,Integer> map = list.stream()
        .collect(Collectors.toMap(StudentSubject::getSubject, StudentSubject::getScore));
    map.forEach((k,v)->System.out.println(k+":"+v));
    
    System.out.println("=========字符串分隔符连接==========");
    //字符串分隔符连接
    String subjectName = list.stream().map(StudentSubject::getSubject)
        .collect(Collectors.joining(",", "(", ")"));
    System.out.println(subjectName);
    
    //打印结果
    ==========将分数 装成list=========
    85
    95
    92
    =========转成set==========
    85
    92
    95
    =========转成map==========
    数学:95
    语文:85
    英语:92
    =========字符串分隔符连接==========
    (语文,数学,英语)
    

    注意下面的两种写法的不同之处。

    //写法一
    Stream.of("java", "ios", "android", "h5", "rn")
                    .collect(Collectors.toSet()) //set 容器
                    .forEach(e -> System.out.println(e));
    //写法二
    Set<String> setList = Stream.of("java", "ios", "android", "h5", "rn")
                    .collect(Collectors.toSet()) ;
    setList.forEach(e -> System.out.println(e));
    

    写法一中collect和forEach同时使用了终止操作符,大家都会想到终止操作不是只能用一次就终止了么?其实forEach不仅仅是Stream中得操作符还是各种集合中得一个语法糖。

匹配、聚合操作

  • allMatch:匹配操作,接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
    noneMatch:匹配操作,接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
    anyMatch:匹配操作,接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
    findFirst:查找操作,返回流中第一个元素
    findAny:查找操作,返回流中的任意元素
    count:聚合操作,返回流中元素的总个数
    max:匹配操作,返回流中元素最大值
    min:匹配操作,返回流中元素最小值

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
    boolean allMatch = list.stream().allMatch(e -> e > 10);
    System.out.println("allMatch: "+allMatch);
    boolean noneMatch = list.stream().noneMatch(e -> e > 10);
    System.out.println("noneMatch: "+noneMatch);
    boolean anyMatch = list.stream().anyMatch(e -> e > 5);
    System.out.println("anyMatch: "+anyMatch);
    Integer findFirst = list.stream().findFirst().get();
    System.out.println("findFirst: "+findFirst);
    Integer findAny = list.stream().findAny().get();
    System.out.println("findAny: "+findAny);
    long count = list.stream().count();
    System.out.println("count: "+count);
    Integer max = list.stream().max(Integer::compareTo).get();
    System.out.println("max: "+max);
    Integer min = list.stream().min(Integer::compareTo).get();
    System.out.println("min: "+min);
    
    //打印结果
    allMatch: false
    noneMatch: true
    anyMatch: true
    findFirst: 1
    findAny: 1
    count: 9
    max: 9
    min: 1
    

归约操作

  • reduce(),归约操作,把整个数据流的值归约为一个值(比如对所有元素求和,乘啊等)。(如:count、max、min操作的底层就是运用了归约操作)

    Optional reduce(BinaryOperator accumulator):

    第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。

    T reduce(T identity, BinaryOperator accumulator):

    流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。

    U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner):

    在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行归约。

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
    
    Integer v = list.stream().reduce((x1, x2) -> x1 + x2).get();
    System.out.println("v:"+v);
    Integer v1 = list.stream().reduce(10, (x1, x2) -> x1 + x2);
    System.out.println("v1:"+v1);
    
    Integer v2 = list.stream().reduce(0,
                                      (x1, x2) -> {
                                          System.out.println("stream accumulator: x1:" + x1 + " x2:" + x2);
                                          return x1 - x2;
                                      },
                                      (x1, x2) -> {
                                          System.out.println("stream combiner: x1:" + x1 + " x2:" + x2);
                                          return x1 * x2;
                                      });
    System.out.println("v2:"+v2);
    //并行流
    Integer v3 = list.parallelStream().reduce(0,
                                              (x1, x2) -> {
                                                  System.out.println("parallelStream accumulator: x1:" + x1 + " x2:" + x2);
                                                  return x1 - x2;
                                              },
                                              (x1, x2) -> {
                                                  System.out.println("parallelStream combiner: x1:" + x1 + " x2:" + x2);
                                                  return x1 * x2;
                                              });
    System.out.println("v3:"+v3);
    
    //打印的结果
    v:120
    v1:130
    v2:-120
    v3:-2004310016
    

转换操作

  • toArray(),转成数组,可以提供自定义数组生成器。

    Object aa[] = Stream.of("android","java","IOS").toArray();
            
    StudentSubject s1 = new StudentSubject("张三", "语文", 85);
    StudentSubject s2 = new StudentSubject("张三", "数学", 95);
    StudentSubject s3 = new StudentSubject("张三", "英语", 92);
    StudentSubject s4 = new StudentSubject("张三", "物理", 92);
    List<StudentSubject> list = Arrays.asList(s1,s2,s3,s4);
    //调用的stream的toArray的函数
    String[] ss = list.stream().toArray(str -> new String[list.size()]);
    String[] ss1 = list.stream().toArray(String[]::new);
    Object[] obj1 = list.stream().toArray();
    //直接调用的List接口的toArray函数
    String[] ss2 = list.toArray(new String[list.size()]);
    Object[] obj2 = list.toArray();
    

    前三个,是调用的stream的toArray的函数,后面的两个,是直接调用的List接口的toArray函数。

总结

  • 了解Stream含义
  • 了解Stream的作用
  • 了解Stream的操作步骤
  • 对Stream的Api的常用函数进行操作及汇总

注意

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

推荐阅读更多精彩内容