4. JDK8的集合流

之前寫了JDK8集合流的入門以及篩選,也就是集合流使用的打開和中間操作。這次帶來的是不同的收集數據的方式。

本節代碼GitHub地址:https://github.com/WeidanLi/Java-jdk8-collect

一、準備:

還是老規矩,使用菜單進行示例。(代碼的話建議拷貝這部分)

二、收集器簡介

收集器即收集東西的容器,它用于使用集合流的時候的終端操作,即我們在日常的業務邏輯中把流進行過濾也好,進行篩選也好,然后我們總該要有一個容器可以存放這些過濾后的元素。這時候的收集器就派上用場了。如代碼所示一個最簡單的收集器的使用實例(當然我感覺平時應該沒人這么無聊)

 /**
 * 收集器是可以收集一個流中的數據的一個容器
 * cn.liweidan.collect.Demo01#demo01
 */
@Test
public void demo01(){
    List<Dish> collect = dishList.stream().collect(Collectors.toList());
    System.out.println(collect);
}

三、JDK8提供的預定義收集器

官方為我們提供了預定義的收集器的一些常用的必要功能,分別有:

  • 元素規約與匯總
  • 元素分組
  • 元素分區

1. 計算元素的個數counting()

counting()用于總結流中元素的個數,有兩種寫法,分別如代碼所示。counting()使用起來還是比較簡單的。

/**
 * cn.liweidan.collect.Demo01#demo02()
 * counting()使用
 */
@Test
public void demo02(){
    /* 查詢卡路里大于400的菜單的個數 */
    Long count = dishList.stream().filter(dish -> dish.getColories() > 400).collect(Collectors.counting());
    System.out.println("卡路里大于400的菜單個數:" + count);
    
    /* 第二種寫法 */
    count = dishList.stream().filter(dish -> dish.getColories() > 400).count();
    System.out.println("卡路里大于400的菜單個數:" + count);
}

2.查找流中的元素某個屬性的最大值或者最小值

我們通常需要去拿到一個對象集合中對象某個屬性進行查詢最大和最小值,按照JDK8以前的寫法是需要先去遍歷集合中所有的對象,去讀取某個屬性的值,然后去記錄最大值或者最小值進行記錄,全部遍歷完成以后把該值進行返回。這種寫法,描述起來也麻煩,寫起來也麻煩。

JDK8的流中就可以比較方便的拿到上面的需求。只需要在流中定義需要什么東西,當流完成以后就可以取到所需要的值了。不過我們需要先定義一個比較器,來告訴JVM我們需要個什么值。

/**
 * cn.liweidan.collect.Demo01#demo03()
 * 取出最大值以及最小值
 */
@Test
public void demo03(){
    /* 定義一個卡路里比較器 */
    Comparator<Dish> comparator = Comparator.comparingInt(Dish::getColories);
    /* Collectors.maxBy(comparator)即取出流中的最大值 */
    Optional<Dish> collect = dishList.stream().collect(Collectors.maxBy(comparator));
    System.out.println(collect.get());
    /* Collectors.minBy(comparator)即取出流中的最小值 */
    Optional<Dish> collect1 = dishList.stream().collect(Collectors.minBy(comparator));
    System.out.println(collect1.get());
}

不過有時候我們需要最大值以及最小值在一個流中取出,這時候我們可以使用后面的分組進行實現。

4. 匯總

匯總即對集合中元素的某個屬性進行統計,如菜單中的所有卡路里的匯總。

/**
 * cn.liweidan.collect.Demo01#demo04()
 * 匯總:對集合中所有菜單的卡路里進行統計計算
 */
@Test
public void demo04(){
    int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
    System.out.println(collect);
}

示例中我們使用了summingInt方法,當然Collectors還提供了Long和Double方法對Long和Double進行統計。

匯總不僅僅包括sum,還包括了平均數、最大值最小值等等。Collectors同時還定義了averagingInt以及IntSummaryStatistics來分別拿出元素屬性的均值和所有統計數據(包括最大值、最小值、均值等)

/**
 * 查詢菜單集合中卡路里的平均值以及所有統計數據
 */
@Test
public void demo05(){
    /* 查詢所有菜單卡路里的平均值 */
    Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
    System.out.println("卡路里均值:" + collect);
    /* 查詢菜單中所有的匯總數據 */
    IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories));
    System.out.println(collect1);// IntSummaryStatistics{count=9, sum=4200, min=120, average=466.666667, max=800}
}

5. joining連接字符串

joining方法可以把自動調用對象的toString方法,然后把字符串連接在一起,如果需要使用分隔符,只要把分隔符傳遞給該方法就可以了。

/**
 * joining連接字符串
 */
@Test
public void demo06(){
    String collect = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
    System.out.println(collect);
    // pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
}

6. 實現自定義歸約--reduce使用

像蚊帳之前講的均值、最值這些操作,其實都是官方對reduce常用方法的封裝,如果官方提供的這些方法不能夠滿足要求的話,那么就需要我們自己來自定義reduce的實現了。

reduce需要傳入三個參數:

  • 第一個參數是規約操作的起始值,即如果要統計總值的時候,那么起始值是0

  • 第二個參數就是要調用的對象的方法了,即菜單的卡路里值

  • 第三個參數就是一個BinaryOperator操作了,在這里定義我們拿到的值的操作方式。即相加。

  • 也可以直接只傳需要的操作,去除前兩個參數。

/**
 * cn.liweidan.collect.Demo01#demo07()
 * Collectors.reducing的使用
 */
@Test
public void demo07(){
    /**
     * 取出卡路里最大的菜單
     */
    Optional<Dish> collect = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getColories() > d2.getColories() ? d1 : d2));
    System.out.println(collect.get());

    /**
     * 計算菜單總卡路里值
     */
    Integer integer1 = dishList.stream().collect(Collectors.reducing(0,// 初始值
            Dish::getColories,// 轉換函數
            Integer::sum));// 累積函數
    System.out.println(integer1);

    Integer integer2 = dishList.stream().map(Dish::getColories).reduce(Integer::sum).get();
    System.out.println(integer2);

    int sum = dishList.stream().mapToInt(Dish::getColories).sum();// 推薦
    System.out.println(sum);

}

在計算總和的時候,推薦使用mapToInt,因為可以免去自動裝箱拆箱的性能消耗。

四、分組

1. 簡單分組

我們經常需要對數據進行分組,特別是在數據庫操作的時候。當我們需要從一個集合中進行分組,代碼會變得十分復雜,分組功能剛好能夠解決這個問題。我們可以對菜單中的類型進行分組,也可以根據卡路里的大小對菜單進行自定義的分組。

/**
 * 簡單分組
 */
@Test
public void test01(){
    /** 按照屬性類型進行分組 */
    Map<Dish.Type, List<Dish>> collect = dishList.stream().collect(Collectors.groupingBy(Dish::getType));
    System.out.println(collect);
    // {FISH=[Dish(name=prawns, vegetarain=false, colories=300, type=FISH),
    // Dish(name=salmon, vegetarain=false, colories=450, type=FISH)],
    // OTHER=[Dish(name=french fries, vegetarain=true, colories=530, type=OTHER),
    // Dish(name=rice, vegetarain=true, colories=350, type=OTHER),
    // Dish(name=season fruit, vegetarain=true, colories=120, type=OTHER),
    // Dish(name=pizza, vegetarain=true, colories=550, type=OTHER)],
    // MEAT=[Dish(name=pork, vegetarain=false, colories=800, type=MEAT),
    // Dish(name=beef, vegetarain=false, colories=700, type=MEAT), Dish(name=chicken, vegetarain=false, colories=400, type=MEAT)]}

    /** 自定義簡單的分組方式 */
    Map<CaloricLevel, List<Dish>> map = dishList.stream().collect(Collectors.groupingBy(d -> {
        /** 此處寫if的時候注意要顧及到所有的情況 */
        if(d.getColories() <= 400){
            return CaloricLevel.DIET;
        }else if (d.getColories() <= 700){
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    }));
    System.out.println(map);
    // {FAT=[Dish(name=pork, vegetarain=false, colories=800, type=MEAT)],
    // NORMAL=[Dish(name=beef, vegetarain=false, colories=700, type=MEAT), Dish(name=french fries, vegetarain=true, colories=530, type=OTHER), Dish(name=pizza, vegetarain=true, colories=550, type=OTHER), Dish(name=salmon, vegetarain=false, colories=450, type=FISH)],
    // DIET=[Dish(name=chicken, vegetarain=false, colories=400, type=MEAT), Dish(name=rice, vegetarain=true, colories=350, type=OTHER), Dish(name=season fruit, vegetarain=true, colories=120, type=OTHER), Dish(name=prawns, vegetarain=false, colories=300, type=FISH)]}
}

2. 多級分組

如果我們需要進行多級分組,比如根據菜單的類型分組的情況下又要根據卡路里大小進行分組。那么我們可以在groupingBy中再傳入第二個groupingBy。

/**
 * 多級分組
 */
@Test
public void test02(){
    Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = 
            dishList.stream().collect(Collectors.groupingBy(Dish::getType, 
                    Collectors.groupingBy(d -> {
        if (d.getColories() <= 400) {
            return CaloricLevel.DIET;
        } else if (d.getColories() <= 700) {
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    })));
    System.out.println(collect);
}

4. 按子組收集數據

在上一節的第二個參數傳遞的是一個groupingBy,但是收集器的第二個參數可以傳入其他的收集器,以便可以達到手機子組數據的目的。比如我們可以計算每種菜單分類的個數,傳入一個counting

/**
 * 多級分組收集數據
 */
@Test
public void test03(){
    /** 計算每一種品類的菜單個數 */
    Map<Dish.Type, Long> typeLongMap = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
    System.out.println(typeLongMap);
}

5. 按照謂詞分區

partitioningBy:通過傳遞一個條件只有true和false的結果的表達式,返回的結果中包括true(滿足條件)的集合以及false(不滿足條件)的集合。
比如,篩選出來質數以及非質數,那么我們可以傳遞一個表達式或者一個方法,返回的是true和false,true表示是質數,false表示非質數的集合。和按子組收集數據的區別就是,這里還可以收集到不滿足條件的所有元素集合。

/**
 * 將數字按質數以及非質數分區
 */
@Test
public void test08(){
    int demoInt = 100;
    Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, demoInt).boxed()
            .collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));
    System.out.println(collect);
}

public boolean isPrime(int candidate){
    /** 通過傳遞的數字進行開方,我們只需要對傳遞的數字與開方的數字進行比對即可,計算次數會減少 */
    int candidateRoot = (int) Math.sqrt((double) candidate);
    /** 產生一個從2開始到開方跟的數字的數據流,與該數據流的每一個元素進行求余 */
    return IntStream.rangeClosed(2, candidateRoot)
            .noneMatch(i -> candidate % i == 0);// 表示沒有一個元素與開方根的數字求余等于0的
}

五、Collect靜態工廠方法表

工廠方法 返回類型 用途 示例
toList List<T> 把流中所有的項目收集到List dishList.collect(Collectors.toList())
toSet Set<T> 把流中所有的項目收集到Set dishList.collect(Collectors.toSet())
toCollection Collection<T> 把流中所有項目收集到給定的供應源創建的集合 dishList.collect(Collectors. toCollection(), ArrayList::new)
counting Long 計算出來流中元素的個數 dishList.collect(Collectors.counting)
summingInt Integer 計算出來集合中元素的某個屬性的和 int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
averagingInt Double 計算出集合中元素某個屬性的均值 Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
summarizingInt IntSummaryStatistics 計算出集合中元素某個屬性的統計值,包括最值、均值、總和等 IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories));
joinging String 連接流中每個元素調用toString進行拼接,使用傳遞的分隔符進行分割 String collect = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
maxBy Optional<T> 通過傳遞的比較器收集元素中屬性最大的值,如果流為空則返回Optional.empty() Optional<Dish> collect = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getColories() > d2.getColories() ? d1 : d2));
minBy Optional<T> 通過傳遞的比較器收集元素中屬性最小的值,如果流為空則返回Optional.empty()
reducing 歸約操作產生的類型 從一個作為累加器的起始值開始,利用BinaryOperator與流中的元素逐個結合,從而將流規約為單個值 Integer integer1 = dishList.stream().collect(Collectors.reducing(0,Dish::getColories,Integer::sum));
collectingAndThen 轉換函數返回的類型 包裹另外一個收集器,對其結果進行轉換
groupingBy Map<K, List<T>> 對流中元素的每個值進行分組
partitioningBy Map<boolean, List<T>> 對流中元素的每個值進行分區

六、開發自定義收集器

方式一

如果我們需要開發自己的自定義收集器的時候,需要讓我們自己的收集器去實現Collector接口。
Collector接口一共有五個方法去自己實現,現在我們用開發我們自己的ToList收集器為例,寫一個我們自己的收集器。

package cn.liweidan.custom.collector;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

/**
 * <p>Desciption:自定義ToList收集器</p>
 * CreateTime : 2017/7/10 下午6:37
 * Author : Weidan
 * Version : V1.0
 */
public class MyListCollector<T> implements Collector<T, List<T>, List<T>> {

    /*
        第一個泛型指的是需要收集的流的泛型
        第二個泛型指的是累加器在收集時候的類型
        第三個泛型指的是返回的類型(可能不是集合,比如counting())
     */

    /**
     * 建立一個新的結果容器
     * @return
     */
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    /**
     * 將元素累加到容器中去
     * @return
     */
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> list.add(item);
    }

    /**
     * 對結果容器進行最終轉換(如需要轉換成Long返回,則在這一步體現)
     * @return
     */
    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();// 此處無需進行轉換,直接返回此函數即可
    }

    /**
     * 對每個子流中的數據進行規約操作
     * 即在集合流中,處理器會將集合流進行不停地分割,分割到一定的很多的小子流的時候,再進行操作
     * 在這一步就是將每一個小流中的元素合并到一起
     * @return
     */
    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) ->{
            list1.addAll(list2);
            return list1;
        };
    }

    /**
     * 這個方法是定義流返回的情況,一共有三種情況,存放于Characteristics枚舉中
     * UNORDERED:規約結果不受項目的遍歷和累計順序的影響
     * CONCURRENT:accumulator函數可以從多個線程去調用。如果收集器沒有標記UNORDERED那他僅用在無需數據源才可以規約
     * INDENTITY_FINISH:表明完成器方法返回的是一個恒等函數,可以跳過。標記這種情況則表示累加器A可以不加檢查的轉換為累加器B
     * @return
     */
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT, Characteristics.IDENTITY_FINISH));
    }
}

現在對我們自己的收集器進行測試,這里與自帶的收集器的區別就是我沒有定義工廠模式去拿到toList收集器的實例,而是需要自己手動new出來。

@Test
public void test(){
    List<Dish> collect = dishList.stream().collect(new MyListCollector<Dish>());
    System.out.println(collect);
}

方式二

方式二比較簡單,但是功能也稍微差一點。就是通過使用collect方法的重載方法進行自定義收集器,并不需要去實現Collector接口。

/**
 * 使用方式二進行自定義收集
 */
@Test
public void test02(){
    ArrayList<Object> collect = dishList.stream().collect(
            ArrayList::new, // 相當于方式一的supplier()方法,用于創建一個容器
            List::add,// 相當于方式一的accumulator方法,用于迭代遍歷每個元素進行加入容器
            List::addAll// 規約并行中所有的容器
    );
    System.out.println(collect);
}

另外值得注意的是,這個方法并不能傳遞任何關于characteristics的信息,也就是說,默認已經給我們設定為INDENTITY_FINISH以及CONCURRENT了。

七、開發自己的質數收集器

在前面我們已經試驗過一個質數收集器了,在這里使用自定義收集器再收集一次一定范圍內的質數。在之前,我們是使用小于被測數的平方根的數字進行對比,到了這里我們再做進一步的優化,就是只拿小于被測數的平方根的質數作為除數。

PrimeNumberCollector:

package cn.liweidan.custom.collector2;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;

/**
 * <p>Desciption:質數收集器</p>
 * CreateTime : 2017/7/11 上午10:43
 * Author : Weidan
 * Version : V1.0
 */
public class PrimeNumberCollector implements Collector<Integer,
                                            Map<Boolean, List<Integer>>,
                                        Map<Boolean, List<Integer>>> {

    public static <A> List<A> takeWhile(List<A> list, Predicate<A> p){
        int i = 0;
        for (A a : list) {
            if(!p.test(a)){
                return list.subList(0, i);
            }
            i++;
        }
        return list;
    }

    /**
     * 拿到所有的質數,以及被測數字。取出小于被測數的平方根與所有質數比較,只拿被測數與小于平方根的質數做計算
     * @param primes
     * @param candidate
     * @return
     */
    public static boolean isPrime(List<Integer> primes, int candidate) {
        int candidateRoot = (int) Math.sqrt((double) candidate);
        return takeWhile(primes, i -> i <= candidateRoot)
                .stream()
                .noneMatch(p -> candidate % p == 0);
    }

    @Override
    public Supplier<Map<Boolean, List<Integer>>> supplier() {
        return () -> new HashMap<Boolean, List<Integer>>(){{
            put(true, new ArrayList<>());
            put(false, new ArrayList<>());
        }};
    }

    @Override
    public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
        return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
            acc.get(isPrime(acc.get(true), candidate))
                    .add(candidate);
        };
    }

    @Override
    public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
        return null;
    }

    @Override
    public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
    }
}

測試:

@Test
public void test01(){
    Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, 100).boxed().collect(new PrimeNumberCollector());
    System.out.println(collect);
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,826評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,732評論 25 708
  • 昨天寫文章寫著寫著睡著了,看來,真是老了,咳咳,誰來攙我一把,老身走不動了……怎么還沒人來摻我,算了,我還是自己走...
    飛雪_飄渺閱讀 326評論 4 3
  • 昨晚夢到一個漂亮的不像話的男孩子,好像,是我的兒子。 很高興,又在夢里有疑惑:是我的兒子嗎? 今早想起來,依然高興...
    顧嶠閱讀 99評論 0 0
  • 天高云淡鄉間好,蔬果俱鮮。 圍坐爐邊,約會尋常縷縷煙。 風輕日暖深春意,眾鳥和弦。 細水流年,人在閑中便是仙。
    悠游魚閱讀 302評論 0 1