1. 程式人生 > 程式設計 >使用Stream實現對程式碼的簡化

使用Stream實現對程式碼的簡化

使用Stream實現對程式碼的簡化

摘要

\quad Java8同樣引入了另一個特別有用的操作,那就是Stream,也就是常說的流。首先我們看JDK中對Stream的定義:

意思大概就是一個有序和並行操作的元素的序列,聽起來還是很拗口,簡單來說就是可將一組資料想象成為一條水流,從上游流向下游,而Collection介面中正好有Stream這個方法,所以實現了Collection介面的集合都可以通過轉換為Stream後,去做一些過濾、排序、查詢等操作。

1.舉個例子

\quad 有一個User物件,它的引數包括name,age。

public class User {
    String name;
    int age;

    public String getName
() { return name; } public int getAge() { return age; } public User(String name,int age) { this.name = name; this.age = age; } 複製程式碼

需求是找出其中姓為張的使用者的名字,結果按照年齡從小到大排序。
這裡我先不使用流,用更加直接的方法實現這個功能,程式碼如下:

public static List<User> filterUser(List<User> list) {
        List<User> result = new ArrayList<>();
        for
(User user : list ) { if (user.getName().startsWith("張")) { result.add(user); } } return result; } public static List<String> sortByAge(List<User> list) { List<String> stringList = new ArrayList<>(); Collections.sort(list,new Comparator<User>() { @Override public int compare(User o1,User o2) { if
(o1.getAge() < o2.getAge()) { return -1; } else if (o1.getAge() > o2.getAge()) { return 1; } else { return 0; } } }); for (User user : list ) { stringList.add(user.getName()); } return stringList; } 複製程式碼

呼叫之後執行:

public static void main(String[] args) {
        List<User> list = Arrays.asList(
                new User("張三",20),new User("張麻子",23),new User("李四",21),new User("趙武",19));
        System.out.println(sortByAge(filterUser(list)));
    }
複製程式碼

可以看到整個過程不是很難,但是卻洋洋灑灑的寫了30多行,而且寫的不是很直觀。那麼如果用流來實現呢? 程式碼如下:

public static List<String> filterAndSortUser(List<User> list) {
        return list.stream().filter(user -> user.getName().startsWith("張"))
                .sorted(comparing(User::getAge))
                .map(User::getName)
                .collect(Collectors.toList());
    }
複製程式碼

沒錯,只要5行,看起來就比前面那一大段程式碼舒服,並且每一步都能通過方法名知道在幹什麼,filter-過濾/sorted-排序/map-將引數型別對映成另一種型別/collect-將流中的元素收整合一個List。
\quad 那麼這個過程是怎麼實現的呢,首先將實現了Collection介面的List轉換成Stream,對這個流我們可以實現若干多箇中間操作,即返回流的操作,但是最後需要用一個返回非Stream的終結操作來終結這個操作,這條流到這就結束了。
\quad 這個過程我們可以使用Java Stream Debugger外掛來檢視:

根據圖中的提示,在打好斷點,進入流方法後,點選除錯介面的流偵錯程式按鈕:

選擇上面的這排操作,即可看到整個執行過程:

為了加深印象,可以自己試著寫更多的例子:

 1.'編寫一個方法,統計"年齡大於等於60的使用者中,名字是兩個字的使用者數量'
 
    public static int countUsers(List<User> users) {
        return (int) users.stream().filter(user -> user.age >= 60)
                .filter(user -> user.name.length() == 2)
                .count();
    }

2. '編寫一個方法,篩選出年齡大於等於60的使用者,然後將他們按照年齡從大到小排序,將他們的名字放在一個LinkedList中返回'

    public static LinkedList<String> collectNames(List<User> users) {
        return users.stream().filter(user -> user.age >= 60)
                .sorted(Comparator.comparing(User::getAge))
                .map(User::getName)
                .collect(Collectors.toCollection(LinkedList::new));
    }
    
3.'判斷一段文字中是否包含關鍵詞列表中的文字,如果包含任意一個關鍵詞,返回true,否則返回false'

    // 例如,text="catcatcat,boyboyboy",keywords=["boy","girl"],返回true
    // 例如,text="I am a boy",keywords=["cat","dog"],返回false
    public static boolean containsKeyword(String text,List<String> keywords) {
        return keywords.stream().anyMatch(text::contains);
    }

4.'返回一個從部門名到這個部門的所有使用者的對映。同一個部門的使用者按照年齡進行從小到大排序'

    // 例如,傳入的employees是[{name=張三,department=技術部,age=40 },{name=李四,age=30 },// {name=王五,department=市場部,age=40 }]
    // 返回如下對映:
    //    技術部 -> [{name=李四,{name=張三,age=40 }]
    //    市場部 -> [{name=王五,age=40 }]
    public static Map<String,List<Employee>> collect(List<Employee> employees) {
        return employees.stream().sorted(Comparator.comparing(Employee::getAge))
                .collect(Collectors.groupingBy(Employee::getDepartment));
    }
    
5.'使用流的方法,把訂單處理成ID->訂單的對映'

    // 例如,傳入引數[{id=1,name='肥皂'},{id=2,name='牙刷'}]
    // 返回一個對映{1->Order(1,'肥皂'),2->Order(2,'牙刷')}
    public static Map<Integer,Order> toMap(List<Order> orders) {
        return orders.stream().collect(Collectors.toMap(Order::getId,order -> order));
    }

6.' 使用流的方法,把所有長度等於1的單詞挑出來,然後用逗號連線起來'

    // 例如,傳入引數words=['a','bb','ccc','d','e']
    // 返回字串a,d,e
    public static String filterThenConcat(Set<String> words) {
        return words.stream().filter(s -> s.length() ==1)
                .collect(Collectors.joining(","));
    }
複製程式碼

Javadoc中的Collectors類中也舉出了幾個使用流的例項,有興趣可以看一下。

2.須知概念

\quad 我學到這的時候,也就只是知道流這麼個東西可以簡化程式碼而已,但是光知道怎麼用未免太浮躁了,基本概念還是必須掌握滴。

2.1流跟陣列的差異體現在哪裡?
\quad 1.流不會儲存元素。這些元素可能會儲存在底層的集合中,或者說是按需生成的。
\quad 2.流的操作不會對資料來源作出修改,對流的操作只是可能產生新的流。
\quad 3.流的操作是儘可能的惰性的。意思就是直至需要流的結果時,才會執行對流的操作。以前面的例子來說,也就是直到執行collect、count、findfirst等終止流的操作時,前面的操作才會去執行。

2.2建立流的方法有哪些?
\quad 1.實現了Collection介面的類,可以直接使用.stream()方法建立流。
\quad 2.對於陣列,可以使用Stream.of方法建立流。
舉例:

 Stream<String> stringStream = Stream.of(s.split("XXX"));
 Stream<String> stringStream = Stream.of("up","down","left","right");
 //從陣列中的指定區間建立流
 Stream<String> stringStream = Arrays.stream(array,from,to)
複製程式碼

\quad 3.對於建立無限流的話,可以使用generate方法(接受一個Supplier 物件,返回一個流)或者iterate方法(接受一個UnaryOperator物件,返回一個流)。
舉例:

Stream<String> stringStream = Stream.generate(() -> "s");
Stream<BigInteger> integerStream = Stream.iterate(BigInteger.ZERO,n->n.add(BigInteger.ONE));
複製程式碼

\quad 4.對於空的流直接Stream.enpty方法即可。
\quad 5.Pattern類中的splitAsStream方法。
舉例:

Stream<String> stringStream = Pattern.compile("XX").splitAsStream(XXXX);
複製程式碼

2.3流是執行緒安全的麼?
\quad 如果是單獨一個Stream,那肯定不會出現執行緒安全的問題。但是我們知道,不僅有Stream,還有ParallelStream即併發的流,那麼這個併發流是不是安全的呢?Demo一下:

public static void threadTest() {
        Stream<BigInteger> integerStream = Stream.iterate(BigInteger.ZERO,n -> n.add(BigInteger.ONE));
        integerStream.parallel().limit(100).map(bigInteger -> bigInteger+" ").forEach(System.out::print);
    }
複製程式碼

很簡單從0到99打印出來這些數字,為了方便檢視,數字之間加空格。結果如下:

\quad可以看出這不是我們所預期的那樣,從0到99順序列印數字。由於傳遞給forEach的函式會在多個併發執行緒中執行,所以列印的順序是不會被保證的,所以顯而易見parallel不是執行緒安全的。下面是Javadoc中的原話:

* <p>The behavior of this operation is explicitly nondeterministic.
     * For parallel stream pipelines,this operation does <em>not</em>
     * guarantee to respect the encounter order of the stream,as doing so
     * would sacrifice the benefit of parallelism.  For any given element,the
     * action may be performed at whatever time and in whatever thread the
     * library chooses.  If the action accesses shared state,it is
     * responsible for providing the required synchronization.
複製程式碼

對於這種情況呼叫collect方法即可:

System.out.println(integerStream.parallel().limit(100).collect(Collectors.toList()));
複製程式碼

3.總結

\quad 流確實方便了很多操作,像過濾、排序、對映、分組、收整合列表等,並且裡面有很多非常好用的API可以供我們使用。但是金無足赤,濫用流的話程式碼的可讀性就會大大下降,有興趣的可以看下Effective Java 的第45條 "明智慎用的選擇Stream",想必會有啟發的。