5. 認識Stream

5.1 什麼是Stream

Stream 是Java 8非常重要的一個API,它提供了許多現成的過濾、排序、轉換、收集等方法,並支援並行(parallel)處理,可以幫助我們更高效地處理資料。

許多人寫程式時,判斷只想到用if、要遍歷元素只想到for或while,而沒有善用語言本身提供的工具,原因常常是對語言本身不熟悉,例如在大學生作業或菜鳥工程師中,常見到這種「波動拳」寫法:

而對軟體設計模式(例如網站常用MVC模式)沒有概念,就會產生出像這種JSP中的麵條式代碼,HTML版面代碼跟JSP業務代碼混雜:


<html>
<head>
<title>Login example</title>
<%!
private boolean validateUser(String userName, String password) {
    ...
}
%>
...
</head>
<body>
<%
   if (request.getMethod().equals("GET") ) { %>
   <form method="POST" target="login.jsp">
   <!-- login form fields go here -->
   </form> <% }
   else {
     String userName = request.getParameter("username");
     String password = request.getParameter("password");
      if (validateUser(userName, password)) {
       response.sendRedirect("mainpage.jsp");
     }
     ...
  } %>
</body>
</html>

最糟的則像是這種Servlet中的麵條式代碼,同時有者Java、HTML、Javascript、CSS,難以想像要修改HTML竟是要打開一個Java檔案,也難以用debug工具來幫忙除錯:


public class HelloWorld extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                        throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<script>");
		out.println("function getData(){
                retrun ajax.get(document.getElementById(\"input_text\").value);
         }");
        out.println("</script>")
		out.println("<style>");
		out.println("body {background: gary}");
        out.println("</style>")
        out.println("<title>Welcome to our Site</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1> please input your data</h1>");
        out.println("</body>");
        out.println("</html>");
        out.close();
    }
}

上面這些代碼只會造成後續除錯的困難及維護的成本,要跟別人合作也會有困難,就像一台車子雖然會動,但線路跟零件沒有遵循一般規範的位置去擺放,到一般修車廠變得沒有人會修,後續的維護及升級也會造成困擾,無法因應需求的改變,軟體的生命週期變短,常常結果就是砍掉重練。

假設我們有個英文句子,我們要去統計其中有多少個長度大於3的單字,以往的做法大概是像這樣:


package exec05;

public class WordsCount {

    public static void main(String[] args) {
        String st = "Java is the best language in the world, and we all love it!";
        String[] words = st.split("[\\P{L}]+"); //非英文字母做為分隔
        long count = 0;

        for (String w : words) {
            if (w.length() > 3) {
                count++;
            }
        }

        System.out.println(count);
    }
}

也就是將單字陣列一個個做迭代,找出符合條件的單字並統計數量。若改為Stream的做法呢?


long count = Stream.of(words).filter(w -> w.length() > 3).count();

上面的做法則是先將陣列轉為Stream,再在過濾方法塞入判斷條件,再算出符合的數目,是不是很簡潔呢?Stream設計的精神在於,我們只需要告訴Stream「做什麼」,而不需要去知道「怎麼做」。

甚至可以並行處理,利用多執行緒運算來提高效率!


long count = Stream.of(words).parallel().filter(w -> w.length() > 3).count();

基本上Stream的使用可分為三個步驟:

1.建立Stream

2.使用中繼操作(Intermediate operations)

3.使用終結操作(Terminal operations)

5.2 建立Stream

建立Stream有許多方式,以下列舉幾個常用的方法:


 Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);

Stream<Integer> stream = Stream.of(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9});

Stream<String> stream = Arrays.stream(new String[] {"I", "You", "He"});

List<String> list = Arrays.asList("a1", "a2", "a3");
Stream<String> stream = list.stream();

Set<String> set = new HashSet<>(Arrays.asList("john", "mary", "steve"));
set.stream().forEach(System.out::println);

Stream<Double> stream = Stream.generate(Math::random); //無限個亂數
Stream<Integer> stream = Stream.iterate(1, e -> e*2); //1, 2, 4, 8, 16.....無限

IntStream stream = IntStream.range(1, 10);

IntStream stream = "abcde".codePoints(); //unicode值
IntStream stream = "abcde".chars(); //utf-16值

Stream<String> stream = Pattern.compile("\\s+").splitAsStream("a b c d e");

Path p = Paths.get("c:\\words.txt");
try (Stream stream = Files.lines(p)){ //自動關閉資源
    stream.forEach(System.out::println);
} catch (IOException ex) {
    ...
}

5.3 Stream常用的函式介面

Stream的方法常會用到幾個特定的函式介面,這些介面通常都存放於java.util.function套件之中,大致上可分為Supplier、Consumer、Function、Predicate。


Supplier<T>: (void) → T (提供者,只出不進)

BooleanSupplier: (void) → boolean (提供者,只boolean出不進)

IntSupplier: (void) → int (提供者,只int出不進)

LongSupplier: (void) → long (提供者,只long出不進)

DoubleSupplier: (void) → double (提供者,只double出不進)


Consumer<T>: (T) → void (消費者,只進不出)

BiConsumer<T, U>: (T, U) → void (消費者,只二進不出)

IntConsumer: (int) → void (消費者,只int進不出)

LongConsumer: (long) → void (消費者,只long進不出)

DoubleConsumer: (double) → void (消費者,只double進不出)

ObjIntConsumer<T>: (T, int) → void (消費者,只T+int進不出)

ObjLongConsumer<T>: (T, long) → void (消費者,只T+long進不出)

ObjDoubleConsumer<T>: (T, double) → void (消費者,只T+double進不出)


Function<T, R>: (T) → R (函數,一進一出)

BiFunction<T, U, R>: (T, U) → R (函數,二進一出)

UnaryOperator<T>: (T) → T (函數,同一進同一出) (Function的子介面)

BinaryOperator<T>: (T, T) → T (函數,同二進同一出) (BiFunction的子介面)

IntFunction<T>: (int) → T (函數,一int進一出)

LongFunction<T>: (long) → T (函數,一long進一出)

DoubleFunction<T>: (double) → T (函數,一double進一出)

ToIntFunction<T>: (T) → int (函數,一進一int出)

ToLongFunction<T>: (T) → long (函數,一進一long出)

ToDoubleFunction<T>: (T) → double (函數,一進一double出)


Predicate<T>: (T) → Boolean (判斷,一進一布林值出)

BiPredicate<T, U>: (T, U) → Boolean (判斷,二進一布林值出)

IntPredicate: (int) → Boolean (判斷,一int進一布林值出)

LongPredicate: (long) → Boolean (判斷,一long進一布林值出)

DoublePredicate: (double) → Boolean (判斷,一double進一布林值出)