JAVA1.8新特性Stream流
今天我們來學習一下Java 8 的新特新—>Stream流;
Stream流
stream流是Java8的新特性,它也是有關於集合的新api;
Stream 作為 Java 8 的一大亮點,它與 java.io 包裡的 InputStream 和 OutputStream 是完全不同的概念。Java 8 中的 Stream 是對集合(Collection)物件功能的增強,它專注於對集合物件進行各種非常便利、高效的聚合操作,或者大批量資料操作;
Stream API 藉助於同樣新出現的 Lambda 表示式,極大的提高程式設計效率和程式可讀性;
下面我們用一個例子來引入Stream流的操作:
Stream流的資料操作特性
這裡有一個集合
List<Integer> list = Arrays.asList(1,2,3,4,5);
我們要寫一個方法找偶數,返回一個新的list包含結果;
用我們以前的方法做的話就會比較繁瑣:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
System.out.println(exec(list, new Predicate<Integer> (){
@Override
public boolean test(Integer i) {
return i % 2 == 0;
}
}));
//System.out.println(exec(list, i -> i % 2 == 0));
}
// 設計模式中,策略模式
public static List<Integer> exec(List<Integer> list, Predicate<Integer> predicate) {
List<Integer> list2 = new ArrayList<>();
for(Integer i :list) {
if(predicate.test(i)) {
list2.add(i);
}
}
return list2;
}
在這裡我們使用了設計模式中的策略模式,將它的處理方法拿出來,可以對它的使用方法進行主動的編寫,同時我們用了一個新的模式Predicate,這個模式叫做斷言模式,顧名思義就是對我們要進行的處理進行斷言操作,斷定它能進行的功能;比如上面的例子就是我們求出了list集合中所有的偶數,那我們如果不想對他進行求偶操作呢?比如我們想求出集合中所有大於三的資料呢?
這個時候我們就可以改變這個斷言,重新寫入想要的操作;
System.out.println(exec(list, new Predicate<Integer>(){
@Override
public boolean test(Integer i) {
return i >3;
}
在不用斷言模式的時候,我們甚至需要新寫一個方法,然後呼叫它,有了斷言模式,我們就可以主動的給exec這個方法傳入我們想要的策略;
甚至我們可以對斷言進行簡寫;即lambda表示式;
這裡我們可以看到這個介面是一個函式式介面,就是單方法介面;所以我們完全可以用lambda表示式給他寫入斷言;
System.out.println(exec(list,i -> i % 2 == 0);
System.out.println(exec(list,i -> i > 3);
用了lambda表示式就大大減少了我們的程式碼量;
可是這也是比較麻煩的 ;而且在之前我們遍歷集合的時候就需要迴圈,或者 Iterator 迭代器來遍歷;這也都是很浪費時間和空間的;
那有沒有一種方法,是我們不用呼叫方法,直接用語句來獲得所有的偶數呢?這個時候我們就需要用到jdk1.8的新特性,Stream流,將陣列元素變為一條資料流,然後對這個資料流進行操作,過濾或者收集想要的資料;
- 特點:
-
Stream 不是集合元素,它不是資料結構並不儲存資料,它是有關演算法和計算的,它更像一個高階版本的 Iterator。原始版本的 Iterator,使用者只能顯式地一個一個遍歷元素並對其執行某些操作;高階版本的 Stream,使用者只要給出需要對其包含的元素執行什麼操作,比如 “過濾掉長度大於 10 的字串”、“獲取每個字串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的資料轉換。
-
Stream 就如同一個迭代器(Iterator),單向,不可往復,資料只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返。
-
Stream 的資料來源本身可以是無限的。
Stream()類的方法
- filter (過濾器)
這個過濾器的方法就是對流中的資料一個一個的進行篩選,看看它是不是符合規則,如果不符合就攔截住,也就是捨棄,如果符合要求,則讓他通過,進行下一步的操作,說白了就像一個濾網一樣,我們可以設定過濾的規則,然後它對資料流進行篩選;
比如上面的例子來說我們就可以用這樣的方法;
List<Integer> list = Arrays.asList(1,2,3,4,5,6);
List<Integer> stream = list.stream().filter( i ->
i%2 == 0).collect(Collectors.toList());
System.out.print(stream);
我們可以看到filter的引數就是一個Predicae<>斷言;也就是說我們可以給它裡面傳一個lambda表示式,就是過濾的規則; 同時它返回的又是一個Stream流物件,這時我們又可以用它的collect(收集器),對過濾下來的資料收集起來,把他存入一個List集合;然後這時候我們輸出得到的這個集合,就是我們收集的資料;
- map (對映)
對映的, lambda把原有的元素轉換為另一個元素, 不會改變個數;
上面的filter過濾器就是給把我們的資料按我們的規則篩選出來,那map()方法就是將我們的元素按照規則對映成另外的元素;比如我們要得到上面的list集合中的所有元素的2倍的集合,這時候我們就需要用到map()方法;
List<Integer> list4 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> collect = list4.stream().map(i ->
i * 2).collect(Collectors.toList());
System.out.println(collect);
這只是簡單的對映,我們又得到了需要的資料,同時原集合仍然存在;我們再看看map()方法的引數;
這裡我們需要一個Function()引數,這個模式的方法lambda表示式的寫法就是一個引數,返回一個結果;
map(i -> i * 2)
這句對映就很直觀的幫我們解釋了Function介面的特性;
- == flatMap(扁平化對映)==
這個類就好比是流的型別轉換;
比如我們現在定義了一個list集合,它的元素是字串陣列;
List<String[]> list = new ArrayList<>();
list.add(new String[]{"張三", "李四"});
list.add(new String[]{"王五", "張三"});
list.add(new String[]{"錢七", "周八"});
如果我們想將它裡面的每個字串提取出來,組成一個新的集合,按照原來的方法我們就需要兩重遍歷,先遍歷集合,後遍歷陣列,得到每一個字串元素,再新建一個集合,將它們存進去;
List<String> list2 = new ArrayList<>();
for (String[] strings : list) {
for (String string : strings) {
list2.add(string);
}
}
System.out.println(list2);
可如果用flatmap方法做扁平化對映時就特別簡單,我們一句話就可以得到這個集合;
List<String> list3 = list.stream().flatMap( s ->
Arrays.stream(s)).collect(Collectors.toList());
flatmap的引數同樣是一個Function,只是這次我們傳入的引數是list集合的元素String[]陣列,把它對映成一個數組流Arrays.stream,然後collect收集起來;這個方法就很方便;
它就可以用在我們平時大規模資料轉換的時候,用流的方式,得到需要的格式;
- forEach
遍歷流,接收一個Consumer
list3.stream().forEach( (a) -> {
System.out.println(a);
} );
- map的流遍歷
接收一個BiConsumer
Map<String, String> map = new HashMap<>();
map.put("a", "張");
map.put("b", "李");
map.forEach( (key, value) -> {
System.out.println("key:" +key + " value:" + value);
} );
重要模式
上面說了很多種的模式,再這裡我們統一進行一下說明;
-
Predicate 斷言介面
對應的lambda 一個引數,返回結果是boolean
(a) -> { return true|false; } -
BiPredicate 雙引數斷言
對應的lambda 兩個引數,返回結果是boolean
(a, b) -> { return true|false; } -
Function 函式介面
對應的lambda 一個引數,一個返回結果,引數和返回結果的型別可以不一樣 -
BiFunction 雙引數函式介面
兩個引數,一個結果
(a, b) -> { 根據ab返回一個結果} -
Consumer 消費介面
一個引數 沒有結果
(a) -> { 不需要return } -
BiConsumer 雙引數消費介面
兩個引數,沒有結果
(a,b) -> { 不需要return } -
Supplier 生產者介面
沒有引數,返回一個結果
() -> {return 結果}
有了這些模式知識的幫忙我們就可以更好的理解Stream()中方法的引數需求了;
7. 其它常見api
求個數count()
System.out.println(list3.stream().count());
去除重複distinct()
System.out.println(list3.stream().distinct().collect(toList()));
獲取最大最小值(引數需要一個比較器)
// 返回的是Optional 型別,怕集合為空時,沒有合法的最大值
List<String> list4 = Arrays.asList("zhang", "li", "zhao", "wang");
System.out.println(list4.stream().max((a, b) -> a.compareTo(b)));
System.out.println(list4.stream().min((a, b) -> a.compareTo(b)));
如果是數字流,除了最大最小值外,還有平均值,和
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).max());
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).min());
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).average());
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).sum());
有了上面的知識我們來做一個比較複雜的需求:
我們定義了一個學生類,它的屬性包括名字,性別,所在城市;
public class Student {
private String name;
private String sex;
private String city;
public Student(String name, String sex, String city) {
this.name = name;
this.sex = sex;
this.city = city;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", city='" + city + '\'' +
'}';
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
然後我們定義一個集合存放學生類的物件
List<Student> students = Arrays.asList(
new Student("zhang", "男", "西安"),
new Student("li", "男", "西安"),
new Student("wang", "女", "北京"),
new Student("zhao", "女", "上海"),
new Student("zhou", "男", "北京")
);
這時我們如果想要按照性別進行分組儲存呢?(男生存在一起,女生存在一起)
按照我們本來的方法需要定義map陣列,按照鍵值對形式,將性別和物件形成對映,然後儲存起來;
具體程式碼:
Map<String, List<Student>> map2 = new HashMap<>();
for (Student student : students) {
if (student.getSex().equals("男")) {
List<Student> nan = map2.get("男");
if (nan == null) {
nan = new ArrayList<>();
map2.put("男", nan);
}
nan.add(student);
} else {
List<Student> nv = map2.get("女");
if (nv == null) {
nv = new ArrayList<>();
map2.put("女", nv);
}
nv.add(student);
}
}
System.out.println(map2);
我們發現上面的邏輯很複雜,我們也很容易出錯,同時這只是我們按照性別分組,那如果類別特別多的話,比如我們按照所在城市分組呢?難道我們每一個都要進行判斷嗎?顯然這是不現實的,這時我們就必須用Stream流來實現了:
//按照性別分組
Map<String, List<Student>> map3 = students.stream().collect(
Collectors.groupingBy( s -> s.getSex() ));
System.out.println(map3);
這裡我們用了groupingBy方法,傳入的引數是一個Function,分組的標準就是我們function的返回屬性;
//按照所在城市分組
Map<String, List<Student>> map4 = students.stream().collect(
Collectors.groupingBy( s -> s.getCity()));
System.out.println(map4);
答案:
{
女=[Student{name=‘wang’, sex=‘女’, city=‘北京’}, Student{name=‘zhao’, sex=‘女’, city=‘上海’}],
男=[Student{name=‘zhang’, sex=‘男’, city=‘西安’}, Student{name=‘li’, sex=‘男’, city=‘西安’}, Student{name=‘zhou’, sex=‘男’, city=‘北京’}]
}
{
上海=[Student{name=‘zhao’, sex=‘女’, city=‘上海’}],
西安=[Student{name=‘zhang’, sex=‘男’, city=‘西安’}, Student{name=‘li’, sex=‘男’, city=‘西安’}],
北京=[Student{name=‘wang’, sex=‘女’, city=‘北京’}, Student{name=‘zhou’, sex=‘男’, city=‘北京’}]
}