问题引入
假如你是一个厨师,你有一份菜单列表(DishMenu),你需要一份低卡路里(Caloric低于400),并且按卡路里数值排序的top3(最低的前3个)的仅包含菜名的菜单列表,你会怎么做?
这个问题也贯穿了开发中大量需要处理list、set、map之类的集合操作,是非常非常基础的问题。而Stream就是处理集合的。
相关数据类:
import java.util.*; public class Dish { private final String name; private final boolean vegetarian; private final int calories; private final Type type; public Dish(String name, boolean vegetarian, int calories, Type type) { this.name = name; this.vegetarian = vegetarian; this.calories = calories; this.type = type; } public String getName() { return name; } public boolean isVegetarian() { return vegetarian; } public int getCalories() { return calories; } public Type getType() { return type; } public enum Type { MEAT, FISH, OTHER } @Override public String toString() { return name; } public static final List<Dish> menu = Arrays.asList( new Dish("pork", false, 800, Dish.Type.MEAT), new Dish("beef", false, 700, Dish.Type.MEAT), new Dish("chicken", false, 390, Dish.Type.MEAT), new Dish("french fries", true, 530, Dish.Type.OTHER), new Dish("rice", true, 350, Dish.Type.OTHER), new Dish("season fruit", true, 120, Dish.Type.OTHER), new Dish("pizza", true, 550, Dish.Type.OTHER), new Dish("prawns", false, 380, Dish.Type.FISH), new Dish("salmon", false, 450, Dish.Type.FISH)); }
传统开发模式
老派java开发者的做法:
- 1、先筛出低于400卡路里的列表(来个for遍历);
- 2、再排个序;
- 3、只显示菜名,选取前3;
- 4、组装结构list,输出。
//老派java开发者 public static List<String> getLowCaloricDishesNamesInOldJava7(List<Dish> dishes){ List<Dish> lowCaloricDishes = new ArrayList<>(); //1、先筛出低于400卡路里的列表(来个for遍历); for(Dish d: dishes){ if(d.getCalories() < 400){ lowCaloricDishes.add(d); } } //2、再排个序; List<String> lowCaloricDishesName = new ArrayList<>(); Collections.sort(lowCaloricDishes, new Comparator<Dish>() { public int compare(Dish d1, Dish d2){ return Integer.compare(d1.getCalories(), d2.getCalories()); } }); //3、只显示菜名,选取前3; int i = 3; for(Dish d: lowCaloricDishes){ if (i>0) { lowCaloricDishesName.add(d.getName()); } else { break; } i--; } //4、组装结构list,输出。 return lowCaloricDishesName; }
让我们看看,我们为了实现业务逻辑使用了以下技巧
- 1、动用了lowCaloricDishes这个中间别表变量;
- 2、为了输出名称,还动用了lowCaloricDishesName列表变量;
- 3、为了排序,用了匿名类;
- 4、为了只显示前top3,还用了int i变量;
- 5、为了退出循环,还使用了break;
- 6、在for循环里使用了--这种操作符;
看起来蛮正常的,但如果业务逻辑再复杂一些,就会对可读性造成困难。
Stream流的方式
public static List<String> getLowCaloricDishesNames(List<Dish> dishes){ return dishes.stream() .filter(d -> d.getCalories() < 400) .sorted(comparing(Dish::getCalories)) .map(Dish::getName) .limit(3) .collect(toList()); }
6行代码解决问题,非常清晰,指向性和可维护性非常好
输出:
---
season fruit
rice
prawns
传统模式和流接口的区别
外部迭代和内部迭代
- 使用for、for-each方式,借助中间变量进行迭代和使用iterator迭代器的方式都是外部迭代;
- 使用流就是内部迭代;
- 在使用外部迭代时,每个步骤,都要编写代码来处理,不容易维护,并行处理(需要和同步synchronized做艰苦的斗争)非常棘手;
- 在使用内部迭代时,可以透明的处理并行,顺序也非常优化,代码可读性也更高;
流操作
中间操作
- filter
- map
- limit
- sorted
- distinct
- ...
终端操作
- forEach
- count
- collect
- reduce
- ...
流的使用场景
筛选和切片
谓词筛选
就是我们以前选400卡路里的那个例子,可以用Lambda,也可以用方法引用
return dishes.stream() .filter(d -> d.getCalories() < 400) .collect(toList());
筛选去重
形成不重复的结果
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4); numbers.stream() .filter(i -> i % 2 == 0) .distinct() .forEach(System.out::println);
截取
List<Dish> dishesLimit3 = menu.stream() .filter(d -> d.getCalories() > 300) .limit(3) .collect(toList());
跳过
List<Dish> dishesSkip2 = menu.stream() .filter(d -> d.getCalories() > 300) .skip(2) .collect(toList()); dishesSkip2.forEach(System.out::println);
映射map
使用map
map用于对集合的每个元素使用函数,有输入,有输出,在上面的例子,输入是晒选后的菜单,输出是只选取菜名
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());
System.out.println(dishNames);
使用flatmap
流的扁平化处理,举个例子,比如想把数组["Hello","World"]中的单词去除重复,输出数组["H","e","l","o","w","r","d"]
如果不了解flatmap,可能会这样写:
List<String> wordsWorng = Arrays.asList("Hello", "World"); List<String[]> wordResult = wordsWorng.stream() .map(w -> w.split("")) .distinct() .collect(toList());
实际上,map是对数组元素中的2个元素,分别使用split方法,输出还是两个元素组成的数组,两个数组元素肯定是不相同的,所以结果是错的
结果是:List<String []> : ["Hello","World"],完全和预想不同
使用flatmap来打碎数组,形成字符流
words.stream() .flatMap((String line) -> Arrays.stream(line.split(""))) .distinct() .forEach(System.out::println);
实际上,flatmap操作将两个数组元素打散,形成了一个Stream<String>为“HelloWorld”,然后对Stream<String>进行去重操作,所以结果是正确的。
一言以蔽之,flatmap就是把整个流中的所有值(或数组中的元素)全部“拍扁”,全部换成另一个流,然后再连接起来。
我们要处理两个数组的组合,比如[1,2,3] + [3,4] => [(1,3),(1,4),(2,3),(2,4),(3,3),(3,4)],并且两个数的和必须被3整除。、
如果用两个map,是得不到正确结果的,必须使用flatmap
List<Integer> numbers1 = Arrays.asList(1,2,3,4,5); List<Integer> numbers2 = Arrays.asList(6,7,8); List<int[]> pairs = numbers1.stream() .flatMap((Integer i) -> numbers2.stream() .map((Integer j) -> new int[]{i, j}) ) .filter(pair -> (pair[0] + pair[1]) % 3 == 0) .collect(toList()); pairs.forEach(pair -> System.out.println("(" + pair[0] + ", " + pair[1] + ")"));
规约reduce
reduce将把流中的元素组合起来,比如求和、取最大值。
List<Integer> numbers = Arrays.asList(3,4,5,1,2); int sum = numbers.stream().reduce(0, (a, b) -> a + b); System.out.println(sum); int sum2 = numbers.stream().reduce(0, Integer::sum); System.out.println(sum2); int max = numbers.stream().reduce(0, (a, b) -> Integer.max(a, b)); System.out.println(max); Optional<Integer> min = numbers.stream().reduce(Integer::min); min.ifPresent(System.out::println); int calories = menu.stream() .map(Dish::getCalories) .reduce(0, Integer::sum); System.out.println("Number of calories:" + calories); };
实践练习
有以下2个类,分别是交易员和交易。
需要完成:
- 1、找出2011年的所有交易,并且由低到高排序,由高到低排序也来一下;
- 2、交易员都在什么城市工作过,输出列表;
- 3、查找所有来自剑桥的交易员,按姓名排序;
- 4、返回所有交易员的姓名,按字母排序;
- 5、有没有米兰的交易员?
- 6、打印所有剑桥交易员的全部交易额;
- 7、最高交易额是什么?
- 8、最小额的交易?
public class Trader{ private String name; private String city; public Trader(String n, String c){ this.name = n; this.city = c; } public String getName(){ return this.name; } public String getCity(){ return this.city; } public void setCity(String newCity){ this.city = newCity; } public String toString(){ return "Trader:"+this.name + " in " + this.city; } } public class Transaction{ private Trader trader; private int year; private int value; public Transaction(Trader trader, int year, int value) { this.trader = trader; this.year = year; this.value = value; } public Trader getTrader(){ return this.trader; } public int getYear(){ return this.year; } public int getValue(){ return this.value; } public String toString(){ return "{" + this.trader + ", " + "year: "+this.year+", " + "value:" + this.value +"}"; } }
1、找出2011年的所有交易,并且由低到高排序,由高到低排序也来一下;
//#1 2011年由低到高排序 List<Transaction> answ1 = transactions.stream() .filter((Transaction t) -> t.getYear()==2011) .sorted(comparing(Transaction::getValue)) .collect(toList()); System.out.println(answ1); //#1 2011年由低到高排序 List<Transaction> answ1Desc = transactions.stream() .filter((Transaction t) -> t.getYear()==2011) .sorted(comparing(Transaction::getValue).reversed()) .collect(toList()); System.out.println(answ1Desc);
输出:
[{Trader:Brian in Cambridge, year: 2011, value:300}, {Trader:Raoul in Cambridge, year: 2011, value:400}]
[{Trader:Raoul in Cambridge, year: 2011, value:400}, {Trader:Brian in Cambridge, year: 2011, value:300}]
2、交易员都在什么城市工作过,输出列表;
//#2 方法1 List<String> answ2 = transactions.stream() .map( (Transaction t) -> t.getTrader().getCity()) .distinct() .collect(toList()); System.out.println(answ2); //#2 方法2 List<Trader> trades = new ArrayList<>(); trades.add(raoul); trades.add(mario); trades.add(alan); trades.add(brian); List<String> answ21 = trades.stream() .map(m -> m.getCity()) .distinct() .collect(toList()); System.out.println(answ21);
3、查找所有来自剑桥的交易员,按姓名排序;
//#3 方法1 List<Trader> answ3 = transactions.stream() .map((Transaction tra) -> tra.getTrader()) .filter((Trader t) -> "Cambridge".equals(t.getCity())) .sorted(comparing(Trader::getName)) .distinct() .collect(toList()); System.out.println(answ3); //#3 方法2 List<Trader> answ32 = transactions.stream() .map(Transaction::getTrader) .filter((Trader t) -> "Cambridge".equals(t.getCity())) .sorted(comparing(Trader::getName)) .distinct() .collect(toList()); System.out.println(answ32); //#3 方法3,这是错误的方法,因为没有把Transaction转换成Trader List<Transaction> answ33 = transactions.stream() .filter((Transaction t) -> "Cambridge".equals(t.getTrader().getCity())) .sorted((Transaction a,Transaction b)->a.getTrader().getName().compareTo(b.getTrader().getName())) .distinct() .collect(toList()); System.out.println(answ33);
4、返回所有交易员的姓名字符串(一个字符串即可),按字母排序;
//#4 方法1 String answ4 = transactions.stream() .map((Transaction t)->t.getTrader().getName()) .distinct() .sorted() .reduce("", (n1, n2) -> n1 +"/"+ n2); System.out.println(answ4); //#4 方法2 List<String> answ42 = transactions.stream() .map((Transaction t)->t.getTrader().getName()) .distinct() .sorted() .collect(toList()); String answ422 = answ42.stream().reduce("", (a,b)->a +"/"+ b); System.out.println(answ422); //#4 方法3 String answ43 = trades.stream() .map(m -> m.getName()) .distinct() .sorted() .reduce("", (n1, n2) -> n1 +"/"+ n2); System.out.println(answ43);
5、有没有米兰的交易员?
// #5 方法1 boolean answ5 = transactions.stream() .anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan")); System.out.println(answ5);
6、打印所有剑桥交易员的全部交易额;
// #6 方法1 List<Transaction> answ6 = transactions.stream(). filter(transaction -> transaction.getTrader().getCity().equals("Cambridge")) .collect(toList()); System.out.println(answ6); // #6 方法2 transactions.stream(). filter(transaction -> transaction.getTrader().getCity().equals("Cambridge")) //.map((Transaction t)->t.getValue()) .forEach(System.out::println);
7、最高交易额是什么?
// #7 方法1 int answ7 = transactions.stream() .map(Transaction::getValue) .reduce(0, Integer::max); System.out.println(answ7); // #7 方法2 Optional<Transaction> answ72 = transactions.stream() .max(comparing(Transaction::getValue)); System.out.println(answ72);
8、最小额的交易?
// #8 方法1 Optional<Transaction> answ8 = transactions.stream() .min(comparing(Transaction::getValue)); System.out.println(answ8); // #8 方法2 int answ82 = transactions.stream() .map(Transaction::getValue) .reduce(Integer.MAX_VALUE,Integer::min); System.out.println(answ82);
数值流
用于方便合计、取值、计数等操作
例子
public class NumericStreams{ public static void main(String...args){ List<Integer> numbers = Arrays.asList(3,4,5,1,2); Arrays.stream(numbers.toArray()).forEach(System.out::println); int calories = menu.stream() .mapToInt(Dish::getCalories) .sum(); System.out.println("Number of calories:" + calories); // max and OptionalInt OptionalInt maxCalories = menu.stream() .mapToInt(Dish::getCalories) .max(); int max; if(maxCalories.isPresent()){ max = maxCalories.getAsInt(); } else { // we can choose a default value max = 1; } System.out.println(max); // numeric ranges IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0); System.out.println(evenNumbers.count()); Stream<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed() .flatMap(a -> IntStream.rangeClosed(a, 100) .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0).boxed() .map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)})); pythagoreanTriples.forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2])); } public static boolean isPerfectSquare(int n){ return Math.sqrt(n) % 1 == 0; } }
由函数生成无限流
使用无限遍历生成斐波那契元组序列
// fibonnaci with iterate Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1],t[0] + t[1]}) .limit(10) .forEach(t -> System.out.println("(" + t[0] + ", " + t[1] + ")")); Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1],t[0] + t[1]}) .limit(10) . map(t -> t[0]) .forEach(System.out::println);