別再踩雷了惠毁!看完這篇文章扰付,避開Java Stream流式編程常見的坑

Java8 由Oracle在2014年發(fā)布,是繼Java5之后最具革命性的版本了仁讨。

Java8吸收其他語言的精髓帶來了函數(shù)式編程羽莺,lambda表達式,Stream流等一系列新特性洞豁,學會了這些新特性盐固,可以讓你實現(xiàn)高效編碼優(yōu)雅編碼。

1. Stream是什么丈挟?

Stream是Java8新增的一個接口刁卜,允許以聲明性方式處理數(shù)據(jù)集合。Stream不是一個集合類型不保存數(shù)據(jù)曙咽,可以把它看作是遍歷數(shù)據(jù)集合的高級迭代器(Iterator)蛔趴。

Stream操作可以像Builder一樣逐步疊加,形成一條流水線例朱。流水線一般由數(shù)據(jù)源+零或者多個中間操作+一個終端操作所構(gòu)成孝情。中間操作可以將流轉(zhuǎn)換成另外一個流,比如使用filter過濾元素洒嗤,使用map映射提取值箫荡。

Stream與lambda表達式密不可分,本文默認你已經(jīng)掌握了lambda基礎(chǔ)知識渔隶。

2. Stream的特點

  • 只能遍歷(消費)一次羔挡。Stream實例只能遍歷一次,終端操作后一次遍歷就結(jié)束,再次遍歷需要重新生成實例绞灼,這一點類似于Iterator迭代器利术。
  • 保護數(shù)據(jù)源。對Stream中任何元素的修改都不會導致數(shù)據(jù)源被修改低矮,比如過濾刪除流中的一個元素氯哮,再次遍歷該數(shù)據(jù)源依然可以獲取該元素。
  • 懶商佛。filter, map 操作串聯(lián)起來形成一系列中間運算,如果沒有一個終端操作(如collect)這些中間運算永遠也不會被執(zhí)行姆打。

3. 創(chuàng)建Stream實例的方法

(1)使用指定值創(chuàng)建Stream實例

// of為Stream的靜態(tài)方法
Stream<String> strStream = Stream.of("hello", "java8", "stream");
// 或者使用基本類型流
IntStream intStream = IntStream.of(1, 2, 3);

(2)使用集合創(chuàng)建Stream實例(常用方式)

// 使用guava庫良姆,初始化一個不可變的list對象
ImmutableList<Integer> integers = ImmutableList.of(1, 2, 3);
// List接口繼承Collection接口,java8在Collection接口中添加了stream方法
Stream<Integer> stream = integers.stream();

(3)使用數(shù)組創(chuàng)建Stream實例

// 初始化一個數(shù)組
Integer[] array = {1, 2, 3};
// 使用Arrays的靜態(tài)方法stream
Stream<Integer> stream = Arrays.stream(array);

(4)使用生成器創(chuàng)建Stream實例

// 隨機生成100個整數(shù)
Random random = new Random();
// 加上limit否則就是無限流了
Stream<Integer> stream = Stream.generate(random::nextInt).limit(100);

(5)使用迭代器創(chuàng)建Stream實例

// 生成100個奇數(shù)幔戏,加上limit否則就是無限流了
Stream<Integer> stream = Stream.iterate(1, n -> n + 2).limit(100);
stream.forEach(System.out::println);

(6)使用IO接口創(chuàng)建Stream實例

// 獲取指定路徑下文件信息玛追,list方法返回Stream類型
Stream<Path> pathStream = Files.list(Paths.get("/"));

4. Stream常用操作

Stream接口中定義了很多操作,大致可以分為兩大類闲延,一類是中間操作痊剖,另一類是終端操作;

(1)中間操作

中間操作會返回另外一個流垒玲,多個中間操作可以連接起來形成一個查詢陆馁。

中間操作有惰性,如果流上沒有一個終端操作合愈,那么中間操作是不會做任何處理的叮贩。

下面介紹常用的中間操作:

map操作

map是將輸入流中每一個元素映射為另一個元素形成輸出流。

// 初始化一個不可變字符串
List<String> words = ImmutableList.of("hello", "java8", "stream");
// 計算列表中每個單詞的長度
List<Integer> list = words.stream()
        .map(String::length)
        .collect(Collectors.toList());
// output: 5 5 6
list.forEach(System.out::println);

flatMap操作

List<String[]> list1 = words.stream()
        .map(word -> word.split("-"))
        .collect(Collectors.toList());

// output: [Ljava.lang.String;@59f95c5d, 
//             [Ljava.lang.String;@5ccd43c2
list1.forEach(System.out::println);

納里佛析?你預期是List, 返回卻是List<String[]>, 這是因為split方法返回的是String[]

這個時候你可以想到要將數(shù)組轉(zhuǎn)成stream, 于是有了第二個版本

Stream<Stream<String>> arrStream = words.stream()
        .map(word -> word.split("-"))
        .map(Arrays::stream);

// output: java.util.stream.ReferencePipeline$Head@2c13da15, 
// java.util.stream.ReferencePipeline$Head@77556fd
arrStream.forEach(System.out::println);

還是不對益老,這個問題使用flatMap扁平流可以解決,flatMap將流中每個元素取出來轉(zhuǎn)成另外一個輸出流

Stream<String> strStream = words.stream()
        .map(word -> word.split("-"))
        .flatMap(Arrays::stream)
        .distinct();
// output: hello java8 stream
strStream.forEach(System.out::println);

filter操作

filter接收Predicate對象寸莫,按條件過濾捺萌,符合條件的元素生成另外一個流。

// 過濾出單詞長度大于5的單詞膘茎,并打印出來
List<String> words = ImmutableList.of("hello", "java8", "hello", "stream");
words.stream()
        .filter(word -> word.length() > 5)
        .collect(Collectors.toList())
        .forEach(System.out::println);
// output: stream

(2)終端操作

終端操作將stream流轉(zhuǎn)成具體的返回值桃纯,比如List,Integer等披坏。常見的終端操作有:foreach, min, max, count等慈参。

foreach很常見了,下面舉一個max的例子刮萌。

// 找出最大的值
List<Integer> integers = Arrays.asList(6, 20, 19);
integers.stream()
        .max(Integer::compareTo)
        .ifPresent(System.out::println);
// output: 20

5. 實戰(zhàn):使用Stream重構(gòu)老代碼

假如有一個需求:過濾出年齡大于20歲并且分數(shù)大于95的學生驮配。

使用for循環(huán)寫法:

private List<Student> getStudents() {
    Student s1 = new Student("xiaoli", 18, 95);
    Student s2 = new Student("xiaoming", 21, 100);
    Student s3 = new Student("xiaohua", 19, 98);
    List<Student> studentList = Lists.newArrayList();
    studentList.add(s1);
    studentList.add(s2);
    studentList.add(s3);
    return studentList;
}
public void refactorBefore() {
    List<Student> studentList = getStudents();
    // 使用臨時list
    List<Student> resultList = Lists.newArrayList();
    for (Student s : studentList) {
        if (s.getAge() > 20 && s.getScore() > 95) {
            resultList.add(s);
        }
    }
    // output: Student{name=xiaoming, age=21, score=100}
    resultList.forEach(System.out::println);
}

使用for循環(huán)會初始化一個臨時list用來存放最終的結(jié)果,整體看起來不夠優(yōu)雅和簡潔。

使用lambda和stream重構(gòu)后:

public void refactorAfter() {
    List<Student> studentLists = getStudents();
    // output: Student{name=xiaoming, age=21, score=100}
   studentLists.stream().filter(this::filterStudents).forEach(System.out::println);
}
private boolean filterStudents(Student student) {
    // 過濾出年齡大于20歲并且分數(shù)大于95的學生
    return student.getAge() > 20 && student.getScore() > 95;
}

使用filter和方法引用使代碼清晰明了壮锻,也不用聲明一個臨時list琐旁,非常方便。

6. 使用Stream常見的誤區(qū)

(1)誤區(qū)一:重復消費stream對象

stream對象一旦被消費猜绣,不能再次重復消費灰殴。

List<String> strings = Arrays.asList("hello", "java8", "stream");
Stream<String> stream = strings.stream();
stream.forEach(System.out::println); // ok
stream.forEach(System.out::println); // IllegalStateException

上述代碼執(zhí)行后報錯:

java.lang.IllegalStateException: stream has already been operated upon or closed

(2)誤區(qū)二:修改數(shù)據(jù)源

在流操作的過程中嘗試添加新的string對象,結(jié)果報錯:

List<String> strings = Arrays.asList("hello", "java8", "stream");
// expect: HELLO JAVA8 STREAM WORLD, but throw UnsupportedOperationException
strings.stream()
        .map(s -> {
            strings.add("world");
            return s.toUpperCase();
        }).forEach(System.out::println);

注意:一定不要在操作流的過程中修改數(shù)據(jù)源掰邢。

總結(jié)

java8 流式編程在一定程度上可以使代碼變得優(yōu)美牺陶,不過也要避開常見的坑,如:不要重復消費對象辣之、不要修改數(shù)據(jù)源掰伸。

-- END --

日常求贊:你好技術(shù)人,先贊再看養(yǎng)成習慣怀估,你的贊是我前進道路上的動力狮鸭,下期更精彩。

作者:愛笑的架構(gòu)師
鏈接:https://juejin.im/post/6887072885291089934
來源:掘金

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末多搀,一起剝皮案震驚了整個濱河市歧蕉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌康铭,老刑警劉巖惯退,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異从藤,居然都是意外死亡蒸痹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門呛哟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叠荠,“玉大人,你說我怎么就攤上這事扫责¢欢Γ” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵鳖孤,是天一觀的道長者娱。 經(jīng)常有香客問我,道長苏揣,這世上最難降的妖魔是什么黄鳍? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮平匈,結(jié)果婚禮上框沟,老公的妹妹穿的比我還像新娘藏古。我一直安慰自己,他們只是感情好忍燥,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布拧晕。 她就那樣靜靜地躺著,像睡著了一般梅垄。 火紅的嫁衣襯著肌膚如雪厂捞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天队丝,我揣著相機與錄音靡馁,去河邊找鬼。 笑死机久,一個胖子當著我的面吹牛臭墨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吞加,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尽狠!你這毒婦竟也來了衔憨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤袄膏,失蹤者是張志新(化名)和其女友劉穎践图,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沉馆,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡码党,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了斥黑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揖盘。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锌奴,靈堂內(nèi)的尸體忽然破棺而出兽狭,到底是詐尸還是另有隱情,我是刑警寧澤鹿蜀,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布箕慧,位于F島的核電站,受9級特大地震影響茴恰,放射性物質(zhì)發(fā)生泄漏颠焦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一往枣、第九天 我趴在偏房一處隱蔽的房頂上張望伐庭。 院中可真熱鬧粉渠,春花似錦、人聲如沸似忧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盯捌。三九已至淳衙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饺著,已是汗流浹背箫攀。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幼衰,地道東北人靴跛。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像渡嚣,于是被迫代替她去往敵國和親梢睛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361