摘要:上下文比如,接受它傳遞的方法的參數(shù),或者接受它的值得局部變量中表達(dá)式需要類型稱為目標(biāo)類型。但局部變量必須顯示的聲明,或?qū)嶋H上就算。換句話說,表達(dá)式只能捕獲指派給它們的局部變量一次。注捕獲實(shí)例變量可以被看作捕獲最終局部變量。
由于第三章的內(nèi)容比較多,而且為了讓大家更好的了解Lambda表達(dá)式的使用,也寫了一些相關(guān)的實(shí)例,可以在Github或者碼云上拉取讀書筆記的代碼進(jìn)行參考。類型檢查、類型推斷以及限制
當(dāng)我們第一次提到Lambda表達(dá)式時(shí),說它可以為函數(shù)式接口生成一個(gè)實(shí)例。然而,Lambda表達(dá)式本身并不包含它在實(shí)現(xiàn)哪個(gè)函數(shù)式接口的信息。為了全面了解Lambda表達(dá)式,你應(yīng)該知道Lambda的實(shí)際類型是什么。
類型檢查Lambda的類型是從使用Lambda上下文推斷出來的。上下文(比如,接受它傳遞的方法的參數(shù),或者接受它的值得局部變量)中Lambda表達(dá)式需要類型稱為目標(biāo)類型。
同樣的Lambda,不同的函數(shù)式接口有了目標(biāo)類型的概念,同一個(gè)Lambda表達(dá)式就可以與不同的函數(shù)接口關(guān)聯(lián)起來,只要它們的抽象方法能夠兼容。比如,前面提到的Callable,這個(gè)接口代表著什么也不接受且返回一個(gè)泛型T的函數(shù)。
同一個(gè)Lambda可用于多個(gè)不同的函數(shù)式接口:
Comparatorc1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); ToIntBiFunction c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); BiFunction c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());;
是的,ToIntFunction和BiFunction都是屬于函數(shù)式接口。還有很多類似的函數(shù)式接口,有興趣的可以去看相關(guān)的源碼。
到目前為止,你應(yīng)該能夠很好的理解在什么時(shí)候以及在哪里使用Lambda表達(dá)式了。它們可以從賦值的上下文、方法調(diào)用(參數(shù)和返回值),以及類型轉(zhuǎn)換的上下文中獲得目標(biāo)類型。為了更好的了解Lambda表達(dá)的時(shí)候方式,我們來看看下面的例子,為什么不能編譯:
Object o = () -> {System.out.println("Tricky example");};
答案:很簡單,我們都知道Object這個(gè)類并不是一個(gè)函數(shù)式接口,所以它不支持這樣寫。為了解決這個(gè)問題,我們可以把Object改為Runnable,Runnable是一個(gè)函數(shù)式接口,因?yàn)樗挥幸粋€(gè)抽象方法,在上一節(jié)的讀書筆記中我們有提到過它。
Runnable r = () -> {System.out.println("Tricky example");};
你已經(jīng)見過如何利用目標(biāo)類型來檢查一個(gè)Lambda是否可以用于某個(gè)特定的上下文。其實(shí),它也可以用來做一些略有不同的事情:tuiduanLambda參數(shù)的類型。
類型推斷我們還可以進(jìn)一步的簡化代碼。Java編譯器會從上下文(目標(biāo)類型)推斷出用什么函數(shù)式接口來匹配Lambda表達(dá)式,這意味著它也可以推斷出適合Lambda的簽名,因?yàn)楹瘮?shù)描述符可以通過目標(biāo)類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達(dá)式的參數(shù)類型,這樣就可以在Lambda與法中省去標(biāo)注參數(shù)類型。換句話說,Java編譯器會向下面這樣推斷Lambda的參數(shù)類型:
// 參數(shù)a沒有顯示的指定類型 ListgreenApples = filter(apples, a -> "green".equals(a.getColor()));
Lambda表達(dá)式有多個(gè)參數(shù),代碼可獨(dú)行的好處就更為明顯。例如,你可以在這用來創(chuàng)建一個(gè)Comparator對象:
// 沒有類型推斷,顯示的指定了類型 ComparatorcApple1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); // 有類型推斷,沒有現(xiàn)實(shí)的指定類型 Comparator cApple2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
有時(shí)候,指定類型的情況下代碼更易讀,有時(shí)候去掉它們也更易讀。并沒有說哪個(gè)就一定比哪個(gè)好,需要根據(jù)自身情況來選擇。
使用局部變量我們迄今為止所介紹的所有Lambda表達(dá)式都只用到了其主體里的參數(shù)。但Lambda表達(dá)式也允許用外部變量,就像匿名類一樣。他們被稱作捕獲Lambda。例如:下面的Lambda捕獲了portNumber變量:
int portNumber = 6666; Runnable r3 = () -> System.out.println(portNumber);
盡管如此,還有一點(diǎn)點(diǎn)小麻煩:關(guān)于能對這些變量做什么有一些限制。Lambda可以沒有限制地捕獲(也就是在主體中引用)實(shí)例變量和靜態(tài)變量。但局部變量必須顯示的聲明final,或?qū)嶋H上就算final。換句話說,Lambda表達(dá)式只能捕獲指派給它們的局部變量一次。(注:捕獲實(shí)例變量可以被看作捕獲最終局部變量this)。例如,下面的代碼無法編譯。
int portNumber = 6666; Runnable r3 = () -> System.out.println(portNumber); portNumber = 7777;
portNumber是一個(gè)final變量,盡管我們沒有顯示的去指定它。但是,在代碼編譯的時(shí)候,編譯器會自動(dòng)給這個(gè)變量加了一個(gè)final,起碼我看反編譯后的代碼是有一個(gè)final的。
對于局部變量的限制
你可能會有一個(gè)疑問,為什么局部變量會有這些限制。第一個(gè),實(shí)例變量和局部變量背后的實(shí)現(xiàn)有一個(gè)關(guān)鍵不同。實(shí)例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個(gè)線程中使用,則使用Lambda的線程,可能會在分配該變量的線程將這個(gè)變量回收之后,去訪問該變量。因此,Java在訪問自由局部變量是,實(shí)際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅復(fù)制一次那就沒什么區(qū)別了,因此就有了這個(gè)限制。
現(xiàn)在,我們來了解你會在Java8代碼中看到的另一個(gè)功能:方法引用??梢园阉鼈円暈槟承㎜ambda的快捷方式。
方法引用方法引用讓你可以重復(fù)使用現(xiàn)有的方法,并像Lambda一樣傳遞它們。在一些情況下,比起用Lambda表達(dá)式還要易讀,感覺也更自然。下面就是我們借助Java8 API,用法引用寫的一個(gè)排序例子:
// 之前 apples.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); // 之后,方法引用 apples.sort(Comparator.comparing(Apple::getWeight));
酷,使用::的代碼看起來更加簡潔。在此之前,我們也有使用到過,它的確看起來很簡潔。
管中窺豹方法引用可以被看作僅僅調(diào)用特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個(gè)Lambda代表的只是:“直接調(diào)用這個(gè)方法”,那最好還是用名稱來調(diào)用它,而不是去描述如何調(diào)用它。事實(shí)上,方法引用就是讓你根據(jù)已有的方法實(shí)現(xiàn)來創(chuàng)建Lambda表達(dá)式。但是,顯示地指明方法的名稱,你的代碼可讀性會更好。它是如何工作的?當(dāng)你需要使用方法引用是,目標(biāo)引用放在分隔符::前,方法的名稱放在后面。 例如,Apple::getWeight就是引用了Apple類中定義的getWeight方法。請記住,不需要括號,因?yàn)槟銢]有實(shí)際調(diào)用這個(gè)方法。方法引用就是用Lambda表達(dá)式(Apple a) -> a.getWeight()的快捷寫法。
我們接著來看看關(guān)于Lambda與方法引用等效的一些例子:
Lambda:(Apple a) -> a.getWeight() 方法引用:Apple::getWeight Lambda:() -> Thread.currentThread().dumpStack() 方法引用:Thread.currentThread()::dumpStack Lambda:(str, i) -> str.substring(i) 方法引用:String::substring Lambda:(String s) -> System.out.println(s) 方法引用:System.out::println
你可以把方法引用看作是Java8中個(gè)一個(gè)語法糖,因?yàn)樗喕艘徊糠执a。
構(gòu)造函數(shù)引用對于一個(gè)現(xiàn)有的構(gòu)造函數(shù),你可以利用它的名稱和關(guān)鍵字new來創(chuàng)建它的一個(gè)引用:ClassName::new。如果,一個(gè)構(gòu)造函數(shù)沒有參數(shù),那么可以使用Supplier來創(chuàng)建一個(gè)對象。你可以這樣做:
Supplierc1 = Apple::new; Apple apple = c1.get();
這樣做等價(jià)于
Supplierc1 = () -> new Apple(); Apple apple = c1.get();
如果,你的構(gòu)造函數(shù)的簽名是Apple(Integer weight),那么可以使用Function接口的簽名,可以這樣寫:
Functionc2 = Apple::new; Apple a2 = c2.apply(120);
這樣做等價(jià)于
Functionc2 = (weight) -> new Apple(weight); Apple a2 = c2.apply(120);
如果有兩個(gè)參數(shù)Apple(weight, color),那么我們可以使用BiFunction:
BiFunctionc3 = Apple::new; Apple a3 = c3.apply(120, "red");
這樣做等價(jià)于
BiFunctionc3 =(weight, color) -> new Apple(weight, color); Apple a3 = c3.apply(120, "red");
到目前為止,我們了解到了很多新內(nèi)容:Lambda、函數(shù)式接口和方法引用,接下來我們將把這一切付諸實(shí)踐。
Lambda和方法引用實(shí)戰(zhàn)為了更好的熟悉Lambda和方法引用的使用,我們繼續(xù)研究開始的那個(gè)問題,用不同的排序策略給一個(gè)Apple列表排序,并需要展示如何把一個(gè)圓使出報(bào)的解決方案變得更為簡明。這會用到我們目前了解到的所有概念和功能:行為參數(shù)化、匿名類、Lambda表達(dá)式和方法引用。我們想要實(shí)現(xiàn)的最終解決方案是這樣的:
apples.sort(comparing(Apple::getWeight));第1步:代碼傳遞
很幸運(yùn),Java8的Api已經(jīng)提供了一個(gè)List可用的sort方法,我們可以不用自己再去實(shí)現(xiàn)它。那么最困難部分已經(jīng)搞定了!但是,如果把排序策略傳遞給sort方法呢?你看,sort方法簽名是這樣的:
void sort(Comparator super E> c)
它需要一個(gè)Comparator對象來比較兩個(gè)Apple!這就是在Java中傳遞策略的方式:它們必須包裹在一個(gè)對象利。我們說sort的行為被參數(shù)化了了:傳遞給他的排序策略不同,其行為也會不同。
可能,你的第一個(gè)解決方案是這樣的:
public class AppleComparator implements Comparator{ @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } } apples.sort(new AppleComparator());
它確實(shí)能實(shí)現(xiàn)排序,但是還需要去實(shí)現(xiàn)一個(gè)接口,并且排序的規(guī)則也不復(fù)雜,或許它還可以簡化一下。
第2步:使用匿名類或許你已經(jīng)想到了一個(gè)簡化代碼的辦法,就是使用匿名類而且每次使用只需要實(shí)例化一次就可以了:
apples.sort(new Comparator() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } });
看上去確實(shí)簡化一些,但感覺還是有些啰嗦,我們接著繼續(xù)簡化:
第3步:使用Lambda表達(dá)式我們可以使用Lambda表達(dá)式來替代匿名類,這樣可以提高代碼的簡潔性和開發(fā)效率:
apples.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
太棒了!這樣的代碼看起來很簡潔,原來四五行的代碼只需要一行就可以搞定了!但是,我們還可以使這行代碼更加的簡潔!
第4步:使用方法引用使用Lambda表達(dá)式的代碼確實(shí)簡潔了不少,那你還記得我們前面說的方法引用嗎?它是Lambda表達(dá)式的一種快捷寫法,相當(dāng)于是一種語法糖,那么我們來試試糖的滋味如何:
apples.sort(Comparator.comparing(Apple::getWeight));
恭喜你,這就是你的最終解決方案!這樣的代碼比真的很簡潔,這比Java8之前的代碼好了很多。這樣的代碼比較簡短,它的意思也很明顯,并且代碼讀起來和問題描述的差不多:“對庫存進(jìn)行排序,比較蘋果的重量”。
復(fù)合(組合)Lambda表達(dá)式的有用方法Java8的好幾個(gè)函數(shù)式接口都有為方便而設(shè)計(jì)的的方法。具體而言,許多函數(shù)式接口,比如用于傳遞Lambda表達(dá)式的Comparator、Function和Predicate都提供了允許你進(jìn)行復(fù)合的方法。這是什么意思呢?在實(shí)踐中,這意味著你可以把多個(gè)簡單的Lambda復(fù)合成復(fù)雜的表達(dá)式。比如,你可以讓兩個(gè)謂詞之間做一個(gè)or操作,組合成一個(gè)更大的謂詞。而且,你還可以讓一個(gè)函數(shù)的結(jié)果成為另一個(gè)函數(shù)的輸入。你可能會想,函數(shù)式接口中怎么可能有更多的方法?(畢竟,這違背了函數(shù)式接口的定義,只能有一個(gè)抽象方法)還記得我們上一節(jié)筆記中提到默認(rèn)方法嗎?它們不是抽象方法。關(guān)于默認(rèn)方法,我們以后在進(jìn)行詳細(xì)的了解吧。
比較復(fù)合器還記剛剛我們對蘋果的排序嗎?它只是一個(gè)從小到大的一個(gè)排序,現(xiàn)在我們需要讓它進(jìn)行逆序??纯磩倓偡椒ㄒ玫拇a,你會發(fā)現(xiàn)它貌似無法進(jìn)行逆序??!不過不用擔(dān)心,我們可以讓它進(jìn)行逆序,而且很簡單。
1.逆序
想要實(shí)現(xiàn)逆序其實(shí)很簡單,需要使用一個(gè)reversed()方法就可以完成我們想要的逆序排序:
apples.sort(Comparator.comparing(Apple::getWeight).reversed());
按重量遞減排序,就這樣完成了。這個(gè)方法很有用,而且用起來很簡單。
2.比較器鏈
上面的代碼很簡單,但是你仔細(xì)想想,如果存在兩個(gè)一樣重的蘋果誰前誰后呢?你可能需要再提供一個(gè)Comparator來進(jìn)一步定義這個(gè)比較。比如,再按重量比較了兩個(gè)蘋果之后,你可能還想要按原產(chǎn)國進(jìn)行排序。thenComparing方法就是做這個(gè)用的。它接受一個(gè)函數(shù)作為參數(shù)(就像comparing方法一樣),如果兩個(gè)對象用第一個(gè)Comparator比較之后還是一樣,就提供第二個(gè)Comparator。我們又可以優(yōu)雅的解決這個(gè)問題了:
apples.sort(Comparator.comparing(Apple::getWeight).reversed() .thenComparing(Apple::getCountry));復(fù)合謂詞
謂詞接口包括了三個(gè)方法: negate、and和or,讓你可以重用已有的Predicate來創(chuàng)建更復(fù)雜的謂詞。比如,negate方法返回一個(gè)Predicate的非,比如蘋果不是紅的:
private staticList filter(List list, Predicate predicate) { List result = new ArrayList<>(); for (T t : list) { if (predicate.test(t)) { result.add(t); } } return result; } List apples = Arrays.asList(new Apple(150, "red"), new Apple(110, "green"), new Apple(100, "green")); // 只要紅蘋果 Predicate apple = a -> "red".equals(a.getColor()); // 只要紅蘋果的非 Predicate notRedApple = apple.negate(); // 篩選 List appleList = filter(apples, notRedApple); // 遍歷打印 appleList.forEach(System.out::println);
你可能還想要把Lambda用and方法組合起來,比如一個(gè)蘋果即是紅色的又比較重:
PredicateredAndHeavyApple = apple.and(a -> a.getWeight() >= 150);
你還可以進(jìn)一步組合謂詞,表達(dá)要么是重的紅蘋果,要么是綠蘋果:
PredicateredAndHeavyAppleOrGreen = apple.and(a -> a.getWeight() >= 150) .or(a -> "green".equals(a.getColor()));
這一點(diǎn)為什么很好呢?從簡單的Lambda表達(dá)式出發(fā),你可以構(gòu)建更復(fù)雜的表達(dá)式,但讀起來仍然和問題陳述的差不多!請注意,and和or方法是按照表達(dá)式鏈中的位置,從左向右確定優(yōu)先級的。因此,a.or(b).and(c)可以看作(a || b) && c。
函數(shù)復(fù)合最后,你還可以把Function接口所代表的Lambda表達(dá)式復(fù)合起來。Function接口為此匹配了andThen和compose兩個(gè)默認(rèn)方法,它們都會返回Function的一個(gè)實(shí)例。
andThen方法會返回一個(gè)函數(shù),它先對輸入應(yīng)用一個(gè)給定函數(shù),再對輸出應(yīng)用另一個(gè)函數(shù)。假設(shè),有一個(gè)函數(shù)f給數(shù)字加1(x -> x + 1),另外一個(gè)函數(shù)g給數(shù)字乘2,你可以將它們組合成一個(gè)函數(shù)h:
Functionf = x -> x + 1; Function g = x -> x * 2; Function h = f.andThen(g); // result = 4 int result = h.apply(1);
你也可以類似地使用compose方法,先把給定的函數(shù)左右compose的參數(shù)里面給的那個(gè)函數(shù),然后再把函數(shù)本身用于結(jié)果。比如在上一個(gè)例子用compose的化,它將意味著f(g(x)),而andThen則意味著g(f(x)):
Functionf1 = x -> x + 1; Function g1 = x -> x * 2; Function h1 = f1.compose(g1); // result1 = 3 int result1 = h1.apply(1);
它們的關(guān)系如下圖所示:
compose和andThen的不同之處就是函數(shù)執(zhí)行的順序不同。compose函數(shù)限制先參數(shù),然后執(zhí)行調(diào)用者,而andThen限制先調(diào)用者,然后再執(zhí)行參數(shù)。
總結(jié)在《Java8實(shí)戰(zhàn)》第三章中,我們了解到了很多概念關(guān)鍵的念。
Lambda表達(dá)式可以理解為一種匿名函數(shù):它沒有名稱,但有參數(shù)列表、函數(shù)主體、返回類型,可能還有一個(gè)可拋出的異常列表。
Lambda表達(dá)式讓我們可以簡潔的傳遞代碼。
函數(shù)式接口就是僅僅只有一個(gè)抽象方法的接口。
只有在接受函數(shù)式接口的地方才可以使用Lambda表達(dá)式。
Lambda表達(dá)式允許你直接內(nèi)聯(lián),為函數(shù)式接口的抽象方法提供實(shí)現(xiàn),并且將整個(gè)表達(dá)式作為函數(shù)式接口的一個(gè)實(shí)例。
Java8自帶一些常用的函數(shù)式接口,在java.util.function包里,包括了Predicate
為了避免裝箱操作,等于Predicate
Lambda表達(dá)式所需要代表的類型稱為目標(biāo)類型。
方法引用可以讓我們重復(fù)使用現(xiàn)有的方法實(shí)現(xiàn)并且直接傳遞它們。
Comparator、Predicate和Function等函數(shù)式接口都有幾個(gè)可以用來結(jié)合Lambda表達(dá)式的默認(rèn)方法。
第三章的內(nèi)容確實(shí)很多,而且這一章的內(nèi)容也很重要,如果你有興趣那么請慢慢的看,最好自己能動(dòng)手寫寫代碼否則過不了多久就會忘記了。
第三章筆記中的代碼:Github: chap3
Gitee: chap3
如果,你對Java8中的新特性很感興趣,你可以關(guān)注我的公眾號或者當(dāng)前的技術(shù)社區(qū)的賬號,利用空閑的時(shí)間看看我的文章,非常感謝!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/76774.html
摘要:之前,使用匿名類給蘋果排序的代碼是的,這段代碼看上去并不是那么的清晰明了,使用表達(dá)式改進(jìn)后或者是不得不承認(rèn),代碼看起來跟清晰了。這是由泛型接口內(nèi)部實(shí)現(xiàn)方式造成的。 # Lambda表達(dá)式在《Java8實(shí)戰(zhàn)》中第三章主要講的是Lambda表達(dá)式,在上一章節(jié)的筆記中我們利用了行為參數(shù)化來因?qū)Σ粩嘧兓男枨?,最后我們也使用到了Lambda,通過表達(dá)式為我們簡化了很多代碼從而極大地提高了我們的...
摘要:實(shí)戰(zhàn)讀書筆記第一章從方法傳遞到接著上次的,繼續(xù)來了解一下,如果繼續(xù)簡化代碼。去掉并且生成的數(shù)字是萬,所消耗的時(shí)間循序流并行流至于為什么有時(shí)候并行流效率比循序流還低,這個(gè)以后的文章會解釋。 《Java8實(shí)戰(zhàn)》-讀書筆記第一章(02) 從方法傳遞到Lambda 接著上次的Predicate,繼續(xù)來了解一下,如果繼續(xù)簡化代碼。 把方法作為值來傳遞雖然很有用,但是要是有很多類似與isHeavy...
摘要:第三個(gè)問題查找所有來自于劍橋的交易員,并按姓名排序。第六個(gè)問題打印生活在劍橋的交易員的所有交易額。第八個(gè)問題找到交易額最小的交易。 付諸實(shí)戰(zhàn) 在本節(jié)中,我們會將迄今學(xué)到的關(guān)于流的知識付諸實(shí)踐。我們來看一個(gè)不同的領(lǐng)域:執(zhí)行交易的交易員。你的經(jīng)理讓你為八個(gè)查詢找到答案。 找出2011年發(fā)生的所有交易,并按交易額排序(從低到高)。 交易員都在哪些不同的城市工作過? 查找所有來自于劍橋的交易...
摘要:依舊使用剛剛對蘋果排序的代碼?,F(xiàn)在,要做的是篩選出所有的綠蘋果,也許你會這一個(gè)這樣的方法在之前,基本上都是這樣寫的,看起來也沒什么毛病。但是,現(xiàn)在又要篩選一下重量超過克的蘋果。 《Java8實(shí)戰(zhàn)》-讀書筆記第一章(01) 最近一直想寫點(diǎn)什么東西,卻不知該怎么寫,所以就寫寫關(guān)于看《Java8實(shí)戰(zhàn)》的筆記吧。 第一章內(nèi)容較多,因此打算分幾篇文章來寫。 為什么要關(guān)心Java8 自1996年J...
摘要:但是到了第二天,他突然告訴你其實(shí)我還想找出所有重量超過克的蘋果?,F(xiàn)在,農(nóng)民要求需要篩選紅蘋果。那么,我們就可以根據(jù)條件創(chuàng)建一個(gè)類并且實(shí)現(xiàn)通過謂詞篩選紅蘋果并且是重蘋果酷,現(xiàn)在方法的行為已經(jīng)取決于通過對象來實(shí)現(xiàn)了。 通過行為參數(shù)化傳遞代碼 行為參數(shù)化 在《Java8實(shí)戰(zhàn)》第二章主要介紹的是通過行為參數(shù)化傳遞代碼,那么就來了解一下什么是行為參數(shù)化吧。 在軟件工程中,一個(gè)從所周知的問題就是,...
閱讀 1813·2023-04-25 21:50
閱讀 2450·2019-08-30 15:53
閱讀 791·2019-08-30 13:19
閱讀 2777·2019-08-28 17:58
閱讀 2495·2019-08-23 16:21
閱讀 2730·2019-08-23 14:08
閱讀 1401·2019-08-23 11:32
閱讀 1470·2019-08-22 16:09