Stream是Java 8新增的重要特性, 它提供函數式編程支持並允許以管道方式操作集合. 流操作會遍歷數據源, 使用管道式操作處理數據后生成結果集合, 這個過程通常不會對數據源造成影響.
函數對象
使用Stream進行函數式編程時經常需要將操作作為參數傳入流方法中, 函數對象即將方法或lambda表達式作為對象.
import java.util.stream.Collectors;
List list = Arrays.asList(-1,0,1,2,3).stream()
.filter(x -> x>0)
.collect(Collectors.toList());
上述示例中filter
的參數x -> x>0
即為一個lambda表達式.
lambda表達式語法通常為(args)->{body}
, 返回值的類型自動推定:
(int a, int b) -> {
if (a>b) {
return a+b;
}
else {
return a * b;
}
}
參數的類型也可以自動推定:
(a, b) -> {
return a+b;
}
在只有一條語句的情況下{}
可省略, 返回值類型與語句主體表達式一致.
(a, b) -> a+b
允許使用空參數:
() -> {System.out.println("Hello World!")}
::
運算符用於將方法表示為函數對象:
List<Item> items = new ArrayList<>();
items.add(new Item("a"));
items.add(new Item("b"));
List<String> list = items.stream()
.map(Item::getMsg)
.collect(Collectors.toList());
for(String str : list) {
System.out.println(str);
}
流的創建
可以使用集合類的stream()
或者parallelStream()
方法創建流:
import java.util.stream.Stream;
Stream<String> s1 = Arrays.asList("a","b","c").stream();
Stream<String> s2 = Arrays.asList("a","b","c").parallelStream();
java.util.stream.Stream
是一個interface
, 各種管道中間操作的返回值都是它的實現類, 這允許我們方便地進行參數傳遞.
Stream
的靜態方法of()
也可以用來創建流:
Stream.of(new int[]{1,2,3});
Arrays
也提供了創建流的靜態方法stream()
:
Arrays.stream(new int[]{1,2,3})
一些類也提供了創建流的方法:
IntStream.range(start, stop);
BufferedReader.lines();
Random.ints();
中間操作
流操作是惰性執行的, 中間操作會返回一個新的流對象, 當執行終點操作時才會真正進行計算.下面介紹流的中間操作. 除非傳入的操作函數有副作用, 函數本身不會對數據源進行任何修改.
distinct
distinct
保證數據源中的重復元素在結果中只出現一次, 它使用equals()
方法判斷兩個元素是否相等.
List<String> list = Stream.of("a","b","c","b")
.distinct()
.collect(Collectors.toList());
filter
filter
根據傳入的斷言函數對所有元素進行檢查, 只有使斷言函數返回真的元素才會出現在結果中. filter
不會對數據源進行修改.
List<Integer> list = IntStream.range(1,10).boxed()
.filter( i -> i % 2 == 0)
.collect(Collectors.toList());
java.util.Objects
提供了空元素過濾的工具:
List<MyItem> list = items.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
map
map
方法根據傳入的mapper函數對元素進行一對一映射, 即數據源中的每一個元素都會在結果中被替換(映射)為mapper函數的返回值.
List<String> list = Stream.of('a','b','c')
.map( s -> s.hashCode())
.collect(Collectors.toList());
flatMap
與map
不同flatMap
進行多對一映射, 它要求若數據源的元素類型為R
, 則mapper函數的返回值必須為Stream<R>
.
flatMap
會使用mapper函數將數據源中的元素一一映射為Stream
對象, 然后把這些Stream
拼裝成一個流.因此我們可以使用flatMap
進行合並列表之類的操作:
List<Integer> list = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
)
.flatMap(l -> l.stream())
.collect(Collectors.toList());
peek
peek
方法會對數據源中所有元素進行給定操作, 但在結果中仍然是數據源中的元素. 通常我們利用操作的副作用, 修改其它數據或進行輸入輸出.
List<String> list = Stream.of('a','b','c')
.map(s -> System.out.println(s))
.collect(Collectors.toList());
sorted
sorted
方法用於對數據源進行排序:
List<Integer> list = Arrays.asList(1,2,3,4,5,6).stream()
.sorted((a, b) -> a-b)
.collect(Collectors.toList());
使用java.util.Comparator
是更方便的方法, 默認進行升序排序:
class Item {
int val;
public Item(int val) { this.val = val; }
public int getVal() { return val; }
}
List<Item> list = Stream.of(
new Item(1),
new Item(2),
new Item(3)
)
.sorted(Comparator.comparingInt(Item::getVal))
.collect(Collectors.toList());
使用reversed()
方法進行降序排序:
List<Item> list = Stream.of(
new Item(1),
new Item(2),
new Item(3)
)
.sorted(Comparator.comparingInt(Item::getVal).reversed())
.collect(Collectors.toList());
limit
limit(int n)
當流中元素數大於n時丟棄超出的元素, 否則不進行處理, 達到限制流長度的目的.
skip
skip(int)
返回丟棄了前n個元素的流. 如果流中的元素小於或者等於n,則返回空的流
終點操作
reduce
reduce(accumulator)
是最基本的終點操作之一, 操作函數accumulator
接受兩個參數x
,y
返回r
.
reduce
首先將數據源中的兩個元素x1
和x2
傳給accumulator
得到r1
, 然后將r1
和x3
傳入得到r2
. 如此進行直到處理完整個數據流.
reduce
方法還可以接受一個參數代替x1
作為起始值:
Integer sum = Stream.of(1,2,3,4,5).reduce(0, (x, y) -> x +y);
String concat = Stream.of("a", "b", "c", "d").reduce("", String::concat);
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0)
.reduce(Double.MAX_VALUE, Double::min);
collect
collect
是使用最廣泛的終點操作, 也上文中多次出現:
List<String> list = Stream.of("a","b","c","b")
.distinct()
.collect(Collectors.toList())
toList()
將流轉換為List
實例, 是最常見的用法, java.util.Collectors
類中還有求和, 計算均值, 取最值, 字符串連接等多種收集方法.
forEach
forEach
方法對流中所有元素執行給定操作, 沒有返回值.
Stream.of(1,2,3,4,5).forEach(System.out::println);
其它
-
count()
返回流中的元素數 -
toArray()
: 轉換為數組
並發問題
除非顯式地創建並行流, 否則默認創建的都是串行流.Collection.stream()
為集合創建串行流,而Collection.parallelStream()
創建並行流.
stream.parallel()
方法可以將串行流轉換成並行流,stream.sequential()
方法將流轉換成串行流.
流可以在非線程安全的集合上創建, 流操作不應該對非線程安全的數據源產生任何副作用, 否則將發生java.util.ConcurrentModificationException
異常.
List<String> list = new ArrayList(Arrays.asList("a", "b"));
list = list.stream().forEach(s -> l.add("c")); // cause exception
對於線程安全的容器不會存在這個問題:
List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("a", "b"));
list = list.stream().forEach(s -> l.add("c")); // safe operation
當然作者建議Stream操作不要對數據源進行任何修改. 當然, 修改其它數據或者輸入輸出是允許的:
list.stream().forEach(s -> {
set.add(s);
System.out.println(s);
});
理想的管道操作應該是無狀態且與訪問順序無關的. 無狀態是指操作的結果只與輸入有關, 下面即是一個有狀態的操作示例:
State state = getState();
List<String> list = new ArrayList(Arrays.asList("a", "b"));
list = list.stream().map(s -> {
if (state.isReady()) {
return s;
}
else {
return null;
}
});
無狀態的操作保證無論系統狀態如何管道的行為不變, 與順序無關則有利於進行並行計算.
函數式接口
函數式接口會將簽名匹配的函數對象(lambda表達式或方法)視作接口的實現。
@FunctionalInterface
interface Greeter
{
void hello(String message);
}
函數式接口中有且只有一個非抽象方法。
Greeter greeter = message -> System.out.println("Hello " + message);
這在 Java 8 之前通常使用匿名內部類實現的:
Greeter greeter = new Greeter() {
@Override
public void hello(String message) {
System.out.println("Hello " + message);
}
};
Java 8 將已有的一些接口實現為函數式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.util.Comparator
- java.lang.reflect.InvocationHandler
- java.io.FileFilter
- java.nio.file.PathMatcher
java.util.function
中定義了一些常用的函數式接口:
- Consumer: 接受參數無返回
Consumer<T>
->void accept(T t)
;BiConsumer<T,U>
->void accept(T t, U u);
DoubleConsumer
->void accept(double value);
- Supplier: 不接受參數有返回
Supplier<T>
->T get();
DoubleSupplier
->double getAsDouble();
- Function: 接受參數並返回
Function<T, R>
->R apply(T t);
BiFunction<T, U, R>
->R apply(T t, U u);
DoubleFunction<R>
->R apply(double value);
DoubleToIntFunction
->int applyAsInt(double value);
BinaryOperator<T>
extendsBiFunction<T,T,T>
- Predicate: 接受參數返回boolean
Predicate<T>
->boolean test(T t);
BiPredicate<T, U>
->boolean test(T t, U u);
DoublePredicate
->boolean test(double value);
默認構造器可以作為supplier: Supplier<Item> supplier = Item::new;