Java Stream(流) api介紹

Java Stream(流) api介紹

流是Java API的新成員畴蒲,它允許你以聲明性方式處理數(shù)據(jù)集合(通過查詢語句來表達咕宿,而不
是臨時編寫一個實現(xiàn))烂翰。就現(xiàn)在來說,你可以把它們看成遍歷數(shù)據(jù)集的高級迭代器休溶。

1.篩選瘪阁、切片 filter、distinct邮偎、limit管跺、skip

//filter 篩選
List<Integer> collect = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
    .filter(x -> x > 6)
    .collect(Collectors.toList());

//distinct 去重
List<Integer> collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9)
    .distinct()
    .collect(Collectors.toList());

//limit 截取
List<Integer> collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9)
    .limit(4)
    .collect(Collectors.toList());

//skip 跳過
List<Integer> collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9)
    .skip(4)
    .collect(Collectors.toList());

//可以使用 limit 和 skip對集合進行分頁
int pageNo = 2;
int pageSize = 5;
List<Integer> collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9)
    .skip((pageNo - 1) * pageSize)
    .limit(pageSize).collect(Collectors.toList());

//peek 
List<Integer> collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9)
    .peek(x-> System.out.println(x))
    .collect(Collectors.toList());

2.映射 map、flatmap

map可以把一個對象映射成另一個不同的對象

//map 把 Integer 映射成了 String
List<String> collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).map(x -> String.valueOf(x)).collect(Collectors.toList());

flatmap 扁平流 可以把不同層次的流合成一個流

//建立一個對象
class Trade{
    private String tid;
    //包含另一個對象的列表
    private List<Order> orders;

    public List<Order> getOrders() {
        return orders;
    }

    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }

    public String getTid() {
        return tid;
    }

    public void setTid(String tid) {
        this.tid = tid;
    }
}
class Order{
    private String oid;

    public Order(String oid) {
        this.oid = oid;
    }

    public String getOid() {
        return oid;
    }

    public void setOid(String oid) {
        this.oid = oid;
    }

    @Override
    public String toString() {
        return "Order{" +
                "oid='" + oid + '\'' +
                '}';
    }
}

public static void main(String[] args) {
        Trade trade = new Trade();
        ArrayList<Order> orders = new ArrayList<>();
        orders.add(new Order("order1"));
        orders.add(new Order("order2"));
        trade.setTid("trade");
        trade.setOrders(orders);

        Trade trade2 = new Trade();
        ArrayList<Order> orders2 = new ArrayList<>();
        orders2.add(new Order("order3"));
        orders2.add(new Order("order4"));
        trade2.setTid("trade2");
        trade2.setOrders(orders2);
    
        ArrayList<Trade> trades = new ArrayList<>();
        trades.add(trade);
        trades.add(trade2);
    
        //flatMap入?yún)魅攵鄠€流組合成一個
        List<Order> collect = trades.stream().flatMap(x -> x.getOrders().stream()).collect(Collectors.toList());
        System.out.println(collect);
}

//輸出 order 對象組成的集合
>> [Order{oid='order1'}, Order{oid='order2'}, Order{oid='order3'}, Order{oid='order4'}]
    
//還可以用扁平流拆分單詞成單獨的字母
Stream.of("add", "one").flatMap(x -> Arrays.stream(x.split(""))).collect(Collectors.toList());

3.查找禾进、匹配(終端) allMatch豁跑、anyMatch、noneMatch泻云、findFirst艇拍、findAny

//anyMatch 流中是否有一個元素能匹配給定的謂詞
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).anyMatch(x -> x == 4)
    
//allMatch 能不能匹配所有得元素
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).allMatch(x -> x < 1000);

//noneMatch 確保流中沒有任何元素與給定的謂詞匹配
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).noneMatch(x -> x < 1000);

//findAny findAny方法將返回當前流中的任意元素 在利用短路找到結果時立即結束
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).filter(x -> x < 1000).findAny();

//findFirst 方法將返回當前流中的第一個元素
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).filter(x -> x < 1000).findFirst();

以上操作有些回返回Optional對象

Optional里面幾種可以迫使你顯式地檢查值是否存在或處理值不存在狐蜕,使用Optional可以避免和null檢查相關的bug。

  • isPresent()將在Optional包含值的時候返回true, 否則返回false卸夕。

  • ifPresent(Consumer block)會在值存在的時候執(zhí)行給定的代碼塊层释。

  • T get()會在值存在時返回值,否則拋出一個NoSuchElement異常快集。

  • T orElse(T other)會在值存在時返回值贡羔,否則返回一個默認值。

4.歸約(終端)reduce

使用reduce操作來表達更復雜的查詢个初,此類查詢需要將流中所有元素反復結合起來乖寒,得到一個值,比如一個Integer院溺。

//歸約求和 
//沒有初始值的情況返回一個 Optional
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).reduce((x, y) -> x + y);

//有初始值就直接返回一個數(shù)值 
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).reduce(0,(x, y) -> x + y);

//找最大或者最小值
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).reduce(0,(x, y) -> x < y ? x : y)  
    
//或者 
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).reduce(Integer::min);

廣義的歸約

//還沒看懂楣嘁,跟并發(fā)有關
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).parallel().reduce(5, (x, y) -> x+y, (x, y) -> x);

5.數(shù)值流

如果 Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).reduce(Integer::sum); 語句中的數(shù)據(jù)類型是Integer,那么計算總和的時候里面暗含著裝箱成本珍逸,每個Integer都必須拆箱成一個原始類型逐虚, 再進行求和。為了解決這個問題谆膳,java8引入了三個原始類型特化流來解決這個問題痊班,分別是IntStreamDoubleStream摹量,LongStream

//該方法將會返回一個IntStream的原始類型的流,在操作計算就不會有裝箱成本了馒胆,DubleStream缨称,LongStream也是類似
IntStream intStream = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).mapToInt(x -> x);

//原始類型流特化計算
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).mapToInt(x->x).sum();

6.構建流

除了用集合生成流,還有如下其他方式生成

6.1 由值創(chuàng)建流

你可以使用靜態(tài)方法Stream.of祝迂,通過顯式值創(chuàng)建一個流睦尽。它可以接受任意數(shù)量的參數(shù)。例如型雳,以下代碼直接使用Stream.of創(chuàng)建了一個字符串流

//把字符串轉(zhuǎn)大寫当凡,并打印出來
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); 
stream.map(String::toUpperCase).forEach(System.out::println); 

// 可以使用empty得到一個空流
Stream<String> emptyStream = Stream.empty();

6.2 數(shù)組創(chuàng)建流

你可以使用靜態(tài)方法Arrays.stream從數(shù)組創(chuàng)建一個流。它接受一個數(shù)組作為參數(shù)纠俭。例如沿量, 你可以將一個原始類型int的數(shù)組轉(zhuǎn)換成一個IntStream

int[] numbers = {2, 3, 5, 7, 11, 13}; 
int sum = Arrays.stream(numbers).sum(); 

6.1 文件流

//java8 io支持從文件讀取數(shù)據(jù)生成流,下面可以查詢讀取文件里有多少個不同的單詞
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("d://test.txt"), Charset.defaultCharset())){
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
        .distinct()
        .count();
} catch(IOException e){

}

6.1 無限流

可以使用函數(shù)創(chuàng)建無限流冤荆,Stream API提供了兩個靜態(tài)方法來從函數(shù)生成流:Stream.iterate和Stream.generate朴则。

//生成一個無窮的偶數(shù)列表,iterate是可以根據(jù)上一個參數(shù)不斷進行迭代
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

//生成一個無窮的隨機數(shù)列表钓简,與iterate不一樣的是乌妒,函數(shù)是不需要入?yún)⒌男谙耄恳粋€都是有方法生成
Stream.generate(Math::random).limit(5).forEach(System.out::println);

7.收集器和高級歸約

collect 不僅僅是用來把Stream中所有的元素結合成一個List,還是一個歸約操作撤蚊,就像reduce一樣可以接 受各種做法作為參數(shù)古掏,將流中的元素累積成一個匯總結果。

collect 是歸約操作
collector 收集器接口
collectors 是java內(nèi)置的已經(jīng)實現(xiàn)的collector接口的工具類方法集合侦啸,提供了很多靜態(tài)工廠方法

7.1 匯總

//返回數(shù)據(jù)元素的個數(shù) 
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).collect(Collectors.counting()); >> 9
    
//返回最小值槽唾,需要傳入一個比較器,返回的是一個Option匹中,防止stream為空時拋出異常
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).collect(Collectors.minBy(Comparable::compareTo)); >> 1
    
//返回最大值夏漱,其他同上
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).collect(Collectors.MaxBy(Comparable::compareTo)); >> 9
    
//求和,同類型的 還有 summigLong summingDouble
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).collect(Collectors.summingInt(x -> x));

//平均數(shù)顶捷,不存在值時返回0 同樣還有 averagingLong averagingDouble
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).collect(Collectors.averagingInt(x -> x));

//還有一個更強大的收集器挂绰,Collectors.summarizingInt 返回一個IntSummaryStatistics對象,打印出來如下,返回當前數(shù)據(jù)的總個數(shù)服赎,和葵蒂,最小值,最大值和平均數(shù)
//同樣的還有 summarizingDouble summarizingDouble
//>> IntSummaryStatistics{count=9, sum=46, min=1, average=5.111111, max=9}
IntSummaryStatistics collect = Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).collect(Collectors.summarizingInt(x -> x));

連接字符串

//可以把字符串連接起來
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).map(String::valueOf).collect(Collectors.joining()); >> 123556789
//一個參數(shù)可以指定連接符
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).map(String::valueOf).collect(Collectors.joining("-")); >> 1-2-3-5-5-6-7-8-9
//三個參數(shù)可以指定連接符和前綴后綴
Stream.of(1, 2, 3, 5, 5, 6, 7, 8, 9).map(String::valueOf).collect(Collectors.joining("-","@","&")); >> @1-2-3-5-5-6-7-8-9&

7.2分組

可以用Collectors.groupingBy工廠方法返回的收集器對數(shù)據(jù)進行分組重虑,groupingBy也可以和其他收集器合起來實現(xiàn)一些非常復雜的效果

//使用trade作為例子
Trade trade = new Trade();
trade.setTid("trade");

Trade trade2 = new Trade();
trade2.setTid("trade2");

Trade trade3 = new Trade();
trade3.setTid("trade");

ArrayList<Trade> trades = new ArrayList<>();
trades.add(trade);
trades.add(trade2);
trades.add(trade3);

// groupingBy 會把集合按tid進行歸約 groupingBy可以生成一個map
Map<String, List<Trade>> collect1 = trades.stream().collect(Collectors.groupingBy(Trade::getTid));

//可以傳入第二個分組函數(shù)進行二級分組践付,理論上還可以一直嵌套下去進行多級分組
Map<String, Map<String, List<Trade>>> collect = trades.stream().collect(Collectors.groupingBy(Trade::getTid, Collectors.groupingBy(Trade::getTid)));

//還可以聯(lián)合其他收集器一起使用
trades.stream().collect(Collectors.groupingBy(Trade::getTid, Collectors.counting()));

//還可以把groupingBy和mapping收集器結合起來,映射成一個list缺厉,甚至一個set
Map<String, List<Integer>> trade1 = trades.stream().collect(Collectors.groupingBy(Trade::getTid, Collectors.mapping(x ->        {
    if (x.getTid().equals("trade")) {
        return 1;
    }
    return 0;
}, Collectors.toList())));

8.自定義收集器

只需實現(xiàn) Collectors 接口中的方法就可以自己實現(xiàn)一個收集器

public class CollectorExa<T> implements Collector<T, List<T>, List<T>> {

    /**
     * 在調(diào)用時它會創(chuàng)建一個空的累加器實例永高,供數(shù)據(jù)收集過程使用。
     */
    @Override
    public Supplier<List<T>> supplier() {
        return ()->new ArrayList<>();
    }

    /**
     *  將元素添加到結果容器
     *  當遍歷到流中第n個元素時提针,這個函數(shù)執(zhí)行
     * 時會有兩個參數(shù):保存歸約結果的累加器(已收集了流中的前 n?1 個項目)命爬,還有第n個元素本身。
     * 該函數(shù)將返回void辐脖,因為累加器是原位更新饲宛,即函數(shù)的執(zhí)行改變了它的內(nèi)部狀態(tài)以體現(xiàn)遍歷的
     * 元素的效果。
     */
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list,item)->list.add(item);
    }

    /**
     * combiner方法會返回一個供歸約操作使用的函數(shù)嗜价,它定義了對
     * 流的各個子部分進行并行處理時艇抠,各個子部分歸約所得的累加器要如何合并。對于toList而言久锥,
     * 這個方法的實現(xiàn)非常簡單家淤,只要把從流的第二個部分收集到的項目列表加到遍歷第一部分時得到
     * 的列表后面就行了:
     */
    @Override
    public BinaryOperator<List<T>> combiner() {
        return  (list1, list2) -> {
                    list1.addAll(list2);
                    return list1; };
    }

    /**
     * 在遍歷完流后,finisher方法必須返回在累積過程的最后要調(diào)用的一個函數(shù)瑟由,以便將累加
     * 器對象轉(zhuǎn)換為整個集合操作的最終結果媒鼓。
     */
    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }
    /**
     * 最后一個方法——characteristics會返回一個不可變的Characteristics集合,它定義
     * 了收集器的行為——尤其是關于流是否可以并行歸約,以及可以使用哪些優(yōu)化的提示绿鸣。
     *
     * Characteristics是一個包含三個項目的枚舉疚沐。
     *
     * UNORDERED——歸約結果不受流中項目的遍歷和累積順序的影響。
     * CONCURRENT——accumulator函數(shù)可以從多個線程同時調(diào)用潮模,且該收集器可以并行歸
     * 約流亮蛔。如果收集器沒有標為UNORDERED,那它僅在用于無序數(shù)據(jù)源時才可以并行歸約擎厢。
     * IDENTITY_FINISH——這表明完成器方法返回的函數(shù)是一個恒等函數(shù)究流,可以跳過。這種
     * 情況下动遭,累加器對象將會直接用作歸約過程的最終結果芬探。這也意味著,將累加器A不加檢
     * 查地轉(zhuǎn)換為結果R是安全的厘惦。
     */
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
    }
}
// 嘗試使用
Stream.of(5,4,5,6,7).collect(new CollectorExa<>());

9.并行流

將流轉(zhuǎn)為并行流只需要中間調(diào)用一個 .parallel() 方法即可偷仿,調(diào)用之后流內(nèi)會設置一個標志,同樣的調(diào)用 sequential()可以把它編程順序流宵蕉,但是下面的操作需要注意

//此方法并不會先并行酝静,在順序執(zhí)行,在并行羡玛,由于流是惰性計算的别智,所以是否并行只會看最后一次調(diào)用,所以這個流是整體是并行的
//使用的線程池大小為運行機器上的內(nèi)核數(shù)稼稿,使用并行流不一定會比順序流更加高效薄榛,有可能會更慢,因為并行本身會消耗資源
Stream.parallel() 
 .filter(...) 
 .sequential() 
 .map(...) 
 .parallel() 
 .reduce(); 

10.Java Stream流的基石 monad 让歼、構造一個monad函數(shù)

monad范疇學上的解釋是 一個子函子上的幺半群

我理解的是把類型封裝起來敞恋,對原始類型的操作轉(zhuǎn)變成對封裝類型的操作 a+b ==> f(a)+f(b),推薦圖解 Monad - 阮一峰的網(wǎng)絡日志 (ruanyifeng.com)

//一個簡單的monad函數(shù)
public interface MonadExample<T> {

    static <T> MonadExampleImp<T> of(T value){
        return new MonadExampleImp<>(value);
    }

    <R> MonadExample<R> map(Function<T, R> function);

    Optional<T> Has(T value);

    class MonadExampleImp<T> implements MonadExample<T>{
        private T value;

        public MonadExampleImp(T value) {
            this.value = value;
        }

        @Override
        public <R> MonadExample<R> map(Function<T, R> function) {
            return new MonadExampleImp<>(function.apply(value));
        }

        @Override
        public Optional<T> Has(T value) {
            if(value.equals(this.value)){
                return Optional.of(this.value);
            }
            return Optional.empty();
        }

        public static void main(String[] args) {
            Optional<String> has = MonadExample.of(4)
                    .map(x -> String.valueOf(4))
                    .map(x -> String.valueOf(4))
                    .map(x -> String.valueOf(4))
                    .Has("3");
            System.out.println(has.orElse("-1"));
        }
    }
}

github有人寫了一個對try catch進行monad封裝的類庫 jasongoodwin/better-java-monads (github.com)

利用這個類庫可以把try catch延后處理是越,讓整個流的運算顯得更加合理

<!--maven引入依賴-->
<dependency>
    <groupId>com.jason-goodwin</groupId>
    <artifactId>better-monads</artifactId>
    <version>0.4.0</version>
</dependency>

//如果流里面有異常需要拋出,只能按下面的的方式寫
List<Integer> old = Stream.of(5).map(x -> {
    try {
        Thread.sleep(4);
    } catch (InterruptedException e) {
        return -1;
    }
    return x;
}).collect(Collectors.toList());

//如果用類庫之后可以碌上,可以用一種更加合理的方式去處理異常
List<Integer> collect = Stream.of(5).map((x) -> Try.ofFailable(() -> {
    Thread.sleep(400);
    return x;
})).map(x -> x.orElse(-1)).collect(Collectors.toList());

------------ 本文內(nèi)容參考如下-------------

《Java8 實戰(zhàn)》

圖解 Monad - 阮一峰的網(wǎng)絡日志 (ruanyifeng.com)

jasongoodwin/better-java-monads (github.com)

JavaLambdaInternals

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末倚评,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子馏予,更是在濱河造成了極大的恐慌天梧,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霞丧,死亡現(xiàn)場離奇詭異呢岗,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門后豫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悉尾,“玉大人,你說我怎么就攤上這事挫酿」姑校” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵早龟,是天一觀的道長惫霸。 經(jīng)常有香客問我,道長葱弟,這世上最難降的妖魔是什么壹店? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮芝加,結果婚禮上硅卢,老公的妹妹穿的比我還像新娘。我一直安慰自己妖混,他們只是感情好老赤,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著制市,像睡著了一般抬旺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上祥楣,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天开财,我揣著相機與錄音,去河邊找鬼误褪。 笑死责鳍,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的兽间。 我是一名探鬼主播历葛,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嘀略!你這毒婦竟也來了恤溶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帜羊,失蹤者是張志新(化名)和其女友劉穎咒程,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讼育,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡帐姻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年稠集,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饥瓷。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡剥纷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扛伍,到底是詐尸還是另有隱情筷畦,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布刺洒,位于F島的核電站鳖宾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逆航。R本人自食惡果不足惜鼎文,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望因俐。 院中可真熱鬧拇惋,春花似錦、人聲如沸抹剩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澳眷。三九已至胡嘿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钳踊,已是汗流浹背衷敌。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拓瞪,地道東北人缴罗。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像祭埂,于是被迫代替她去往敵國和親面氓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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