Java常见问题的简单解法
提示:总结于LeetCode书籍《Java常见问题的简单解法》,全书其实就是用Stream去通过函数式编程,更加简洁,快速,高效的解决实际问题。
文章目录
第三章 流式操作
一.流的创建
可以采用Stream.of,Stream.iterate,Stream generate 等静态方法创建流。
1.Stream.of
Stream.of的方法定义:
static <T> Stream<T> of(T... values)
在java.util.Arrays类定义的stream方法有实现该方法:
@SafeVarargs
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
@SafeVarargs注解 的作用:of的参数为可变参数,类型为T。所以我们通常传一个数组作为参数时,该数组里面数据类型都是一致的,但是如果出现不一致,没有@SafeVarargs注解,则会有编译警告,加上了则没有。
利用Stream.of()方法创建流栗子:
String names = Stream.of("Gomez", "Morticia", "Wednesday", "Pugsley")
.collect(Collectors.joining(","));
System.out.println(names);
// 打印Gomez,Morticia,Wednesday,Pugsley
利用Arrays.stream方法创建流
String[] munsters = { "Herman", "Lily", "Eddie", "Marilyn", "Grandpa" };
String names = Arrays.stream(munsters)
.collect(Collectors.joining(","));
System.out.println(names);
// 打印Herman,Lily,Eddie,Marilyn,Grandpa
2.Stream.iterate
Stream.iterate方法的定义:
tatic <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
iterate方法返回的是一个无限顺序的有序流。利用Stream.iterate方法创建流栗子:
List<BigDecimal> nums =
Stream.iterate(BigDecimal.ONE, n -> n.add(BigDecimal.ONE) )
.limit(10)
.collect(Collectors.toList());
System.out.println(nums);
// 打印[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Stream.iterate(LocalDate.now(), ld -> ld.plusDays(1L))
.limit(10)
.forEach(System.out::println)
// 打印从当日开始之后的10天
3.Stream.generate
Stream.generate方法的定义:
static <T> Stream<T> generate(Supplier<T> s)
Stream.generate返回一个无限连续的无序流。利用Math.random创建随机流:
Stream.generate(Math::random)
.limit(10)
.forEach(System.out::println)
二.reduce
在认识reduce之前,使用sum,count,max,min,average等规约操作:
String[] strings = "this is an array of strings".split(" ");
long count = Arrays.stream(strings)
.count();
System.out.println("There are " + count + " strings");
int totalLength = Arrays.stream(strings)
.mapToInt(String::length)
.sum();
System.out.println("The total length is " + totalLength);
OptionalDouble ave = Arrays.stream(strings)
.mapToInt(String::length)
.average();
System.out.println("The average length is " + ave);
OptionalInt max = Arrays.stream(strings)
.mapToInt(String::length)
.max();
OptionalInt min = Arrays.stream(strings)
.mapToInt(String::length)
.min();
reduct为接口Stream下的方法,其定义为:
/**
* identity:提供一个用于循环计算的初始值
* accumulator:计算的累加器,他的类型BinaryOperator也是
* 一个函数式接口,单一抽象方法为 apply,具体定义为:
* R apply(T t, U u);
* t:上次函数计算的返回值
* u:为Stream中的元素,apply的实现就是把这俩个值做计算后
* 返回给了t(实现通常在lambda表达式中根据具体需求实现)
*/
T reduce(T identity, BinaryOperator<T> accumulator);
使用reduct的一些实例:
/**
* T reduce(T identity, BinaryOperator<T> accumulator);
* identity:它允许用户提供一个循环计算的初始值。
* accumulator:计算的累加器,
*/
private static void testReduce() {
//T reduce(T identity, BinaryOperator<T> accumulator);
System.out.println("给定个初始值,求和");
System.out.println(Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum + item));
System.out.println(Stream.of(1, 2, 3, 4).reduce(100, Integer::sum));
System.out.println("给定个初始值,求min");
System.out.println(Stream.of(1, 2, 3, 4).reduce(100, (min, item) -> Math.min(min, item)));
System.out.println(Stream.of(1, 2, 3, 4).reduce(100, Integer::min));
System.out.println("给定个初始值,求max");
System.out.println(Stream.of(1, 2, 3, 4).reduce(100, (max, item) -> Math.max(max, item)));
System.out.println(Stream.of(1, 2, 3, 4).reduce(100, Integer::max));
//Optional<T> reduce(BinaryOperator<T> accumulator);
// 注意返回值,上面的返回是T,泛型,传进去啥类型,返回就是啥类型。
// 下面的返回的则是Optional类型
System.out.println("无初始值,求和");
System.out.println(Stream.of(1, 2, 3, 4).reduce(Integer::sum).orElse(0));
System.out.println("无初始值,求max");
System.out.println(Stream.of(1, 2, 3, 4).reduce(Integer::max).orElse(0));
System.out.println("无初始值,求min");
System.out.println(Stream.of(1, 2, 3, 4).reduce(Integer::min).orElse(0));
}
注:该栗子摘于博客:https://blog.csdn.net/luzhensmart/article/details/85008631
三.peek
peek方法可以对流进行调试,达到在处理流时可以查看各个元素的值的效果。
举个栗子:
public int sumDoublesDivisibleBy3(int start, int end) {
return IntStream.rangeClosed(start, end)
.peek(n -> System.out.printf("original: %d%n", n))
.map(n -> n * 2)
.peek(n -> System.out.printf("doubled : %d%n", n))
.filter(n -> n % 3 == 0)
.peek(n -> System.out.printf("filtered: %d%n", n))
.sum();
}
//打印后的结果为:
original: 100
doubled : 200
original: 101
doubled : 202
original: 102
doubled : 204
filtered: 204
...
original: 119
doubled : 238
original: 120
doubled : 240
filtered: 240
四.count与counting
Stream接口中定义了count默认方法,他能返回long型数据:
long count = Stream.of(3, 1, 4, 1, 5, 9, 2, 6, 5).count();
System.out.printf("There are %d elements in the stream%n", count);
//There are 9 elements in the stream
而Collectors类定义了一种类似的方法counting:
count = Stream.of(3, 1, 4, 1, 5, 9, 2, 6, 5)
.collect(Collectors.counting());
System.out.printf("There are %d elements in the stream%n", count);
当我们需要删选一些条件,并且得到各个条件下的数量,那么counting方法就很使用了,比如partitioningBy,他有俩个重载方法:
//已传递一个参数,且为Predicate类型,前面我们已经讨论过、、Predicate接口可以通过条件删选,将流一分为二
public static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
return partitioningBy(predicate, toList());
}
//此方法俩个参数,第一个一样,第二个传了Collector接口,那么我们就可以直接对每个条件进行数据汇总,得到各个条件下的数据总数量
public static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream) {
}
接下来 就每种情况各举栗子:
//一个参数的
// 创建一个包含人名称的流(英文名和中文名)
Stream<String> stream = Stream.of("Alen", "Hebe", "Zebe", "张成瑶", "钟其林");
// 通过判断人名称的首字母是否为英文字母,将其分为两个不同流
final Map<Boolean, List<String>> map = stream.collect(Collectors.partitioningBy(s -> {
// 如果是英文字母,则将其划分到英文人名,否则划分到中文人名
int code = s.codePointAt(0);
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
}));
// 输出分组结果
map.forEach((isEnglishName, names) -> {
if (isEnglishName) {
System.out.println("英文名称如下:");
} else {
System.out.println("中文名称如下:");
}
names.forEach(name -> System.out.println("\t" + name));
});
//输出结果如下:
//中文名称如下:
// 张成瑶
// 钟其林
//英文名称如下:
// Alen
// Hebe
// Zebe
//该栗子摘于博客:https://blog.csdn.net/zebe1989/article/details/83054026
//俩个参数的栗子
long count = Stream.of(3, 1, 4, 1, 5, 9, 2, 6, 5).count();
Map<Boolean, Long> numberLengthMap = strings.stream()
.collect(Collectors.partitioningBy(
s -> s.length() % 2 == 0, ➊
Collectors.counting())); ➋
numberLengthMap.forEach((k,v) -> System.out.printf("%5s: %d%n", k, v));
//打印结果如下:
// false: 4
// true: 8
五.anyMatch、allMatch 与noneMatch
anyMatch:判断条件里,任意一个元素成功,返回true allMatch:判断条件里,所有元素成功 ,返回true noneMatch:判断条件里,所有的元素都不成功,返回true 举个栗子:
List<String> strs = Arrays.asList("a", "a", "a", "a", "b");
boolean aa = strs.stream().anyMatch(str -> str.equals("a"));
boolean bb = strs.stream().allMatch(str -> str.equals("a"));
boolean cc = strs.stream().noneMatch(str -> str.equals("a"));
long count = strs.stream().filter(str -> str.equals("a")).count();
System.out.println(aa);// TRUE
System.out.println(bb);// FALSE
System.out.println(cc);// FALSE
System.out.println(count);// 4
//栗子摘于博客地址:https://blog.csdn.net/qq_28410283/article/details/80783946?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control
需要特别注意的是流可能为空的情况,如果流为空的话,上述栗子则不正确了。废话不多说,直接上代码:
System.out.println(Stream.empty().allMatch(s ->true));//true
System.out.println(Stream.empty().allMatch(s ->false));//true
System.out.println(Stream.empty().noneMatch(s ->true));//true
System.out.println(Stream.empty().noneMatch(s ->false));//true
System.out.println(Stream.empty().anyMatch(s ->true));//false
System.out.println(Stream.empty().anyMatch(s ->false));//false
System.out.println("-----------------");
List list = new ArrayList<>();
System.out.println(list.stream().allMatch(s ->true));//true
System.out.println(list.stream().allMatch(s ->false));//true
System.out.println(list.stream().noneMatch(s ->true));//true
System.out.println(list.stream().noneMatch(s ->false));//true
System.out.println(list.stream().anyMatch(s ->true));//false
System.out.println(list.stream().anyMatch(s ->false));//false
上述栗子可以看出来:对于allMath和noneMatch方法,流为空时将放回true,anyMatch流为空时放回false,他们都和具体的实现无关
六.flatMap与map方法
map和flatMap方法均传入Function作为参数。map的签名如下:
<R> Stream<R> map(Function<? super T,? extends R> mapper)
Function传入一个输入,将其转换为一个输出。map方法则将一个T类型的输入装换为一个R类型的输出。那么他和flatMap有哪些区别呢?
1.原理
map,flatMap他们在处理数据的过程是怎样的呢?看下面的栗子:
String[] words = new String[]{"Hello","World"};
List<String[]> a = Arrays.stream(words)
.map(word -> word.split(""))
.distinct()
.collect(toList());
a.forEach(System.out::print);
//打印结果为:[Ljava.lang.String;@12edcd21[Ljava.lang.String;@34c45dca (返回一个包含两个String[]的list)
该栗子map返回的流实际上是Stream<String[]>类型的。下图是上方代码stream的运行流程:
第二中方式:flatMap(对流扁平化处理):
String[] words = new String[]{"Hello","World"};
List<String> a = Arrays.stream(words)
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(toList());
a.forEach(System.out::print);
//输出结果:HelloWorld
使用flatMap方法的效果是,各个数组并不是分别映射一个流,而是映射成流的内容,所有使用map(Array::stream)时生成的单个流被合并起来,即扁平化为一个流。下图是运行flatMap的stream的运行过程:
该栗子摘于博客:https://blog.csdn.net/liyantianmin/article/details/96178586?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control
2.运用场景
列举一个打印所有客户预订单的栗子:
//客户
public class Customer {
private String name;
//一个客户可能有多个订单
private List<Order> orders = new ArrayList<>();
public Customer(String name) {
this.name = name;
}
public String getName() { return name; }
public List<Order> getOrders() { return orders; }
public Customer addOrder(Order order) {
orders.add(order);
return this;
}
}
//订单
public class Order {
private int id;
public Order(int id) {
this.id = id;
}
public int getId() { return id; }
}
public class Commodity{
public static void main(String[] args){
Customer sheridan = new Customer("Sheridan");
Customer ivanova = new Customer("Ivanova");
Customer garibaldi = new Customer("Garibaldi");
sheridan.addOrder(new Order(1))
.addOrder(new Order(2))
.addOrder(new Order(3));
ivanova.addOrder(new Order(4))
.addOrder(new Order(5));
List<Customer> customers = Arrays.asList(sheridan, ivanova, garibaldi);
//将顾客映射到订单,使用map
customers.stream().map(Customer::getOrders).forEach(System.out::println);
//打印为:
//[Order@7699a589, Order@58372a00, Order@4dd8dc3]
//[Order@6d03e736, Order@568db2f2]
//[]
customers.stream().map(customer -> customer.getOrders().stream()).forEach(System.out::println);
//打印为:
//java.util.stream.ReferencePipeline$Head@16b98e56
//java.util.stream.ReferencePipeline$Head@7ef20235
//java.util.stream.ReferencePipeline$Head@27d6c5e0
}
}
显然上述得到的打印结果不是我们想要的,我们希望的是可以答应每一个用户的所有订单。我们就可以借用flatMap来完成了,我们先看下flatMap的签名:
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
对于每一个泛型T,函数生成的是Stream而不仅仅是R。我们看下用flatMap完成上述需求:
customers.stream()//Stream<Customer>
.flatMap(customer -> customer.getOrders().stream()) //Stream<Order>
.forEach(System.out::println);
//打印结果为:Order{id=1}, Order{id=2}, Order{id=3}, Order{id=4}, Order{id=5}
七.流的合并
假设我们从多个信息源获取到数据,且希望使用流来处理其中的每个元素。一种方案是采用Stream接口定义的concat方法,签名如下:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
使用concat拼接流的简单例子:
Stream<String> first = Stream.of("a", "b", "c").parallel();
Stream<String> second = Stream.of("X", "Y", "Z");
List<String> strings = Stream.concat(first, second)
.collect(Collectors.toList());
List<String> stringList = Arrays.asList("a", "b", "c", "X", "Y", "Z");
System.out.println(strings);
System.out.println(stringList);
//打印结果皆为:[a, b, c, X, Y, Z]
如果要增加三个流:
Stream<String> first = Stream.of("a", "b", "c").parallel();
Stream<String> second = Stream.of("X", "Y", "Z");
Stream<String> third = Stream.of("alpha", "beta", "gamma");
List<String> strings = Stream.concat(Stream.concat(first, second), third)
.collect(Collectors.toList());
List<String> stringList = Arrays.asList("a", "b", "c",
"X", "Y", "Z", "alpha", "beta", "gamma");
虽然嵌套可行,当时这样操作构建流需要谨慎,因为访问一个深度拼接流中的元素可能导致深层调用链(deep call chain)甚至抛出StackOverflowException,换言之,concat方法实际上构建了一个流的二叉树,使用过多就难以处理。另一种解决方案就是使用reduce。我们来看下使用reduce来如何拼接多个流:
Stream<String> first = Stream.of("a", "b", "c").parallel();
Stream<String> second = Stream.of("X", "Y", "Z");
Stream<String> third = Stream.of("alpha", "beta", "gamma");
Stream<String> fourth = Stream.empty();
List<String> strings = Stream.of(first, second, third, fourth)
//第一个参数:初始值
//第二个参数:使用concat拼接流
.reduce(Stream.empty(), Stream::concat)
.collect(Collectors.toList());
List<String> stringList = Arrays.asList("a", "b", "c",
"X", "Y", "Z", "alpha", "beta", "gamma");
虽然代码更加简洁了,但是并不能解决现在的栈溢出问题。鉴于此,在合并多个流时,使用flatMap方法成为一种自然而然的解决方案:
Stream<String> first = Stream.of("a", "b", "c").parallel();
Stream<String> second = Stream.of("X", "Y", "Z");
Stream<String> third = Stream.of("alpha", "beta", "gamma");
Stream<String> fourth = Stream.empty();
//Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式
List<String> strings = Stream.of(first, second, third, fourth)
.flatMap(Function.identity())
.collect(Collectors.toList());
List<String> stringList = Arrays.asList("a", "b", "c",
"X", "Y", "Z", "alpha", "beta", "gamma");
Function.identity:返回t -> T,即本身。举一个栗子,如已经有List,利用stream()获取一个键值对Map<id,user>:
// 构造Map键值对,key:Integer, value:IndexEntity
// key为指标实体的id,value为对应的指标实体
Map<Integer, IndexEntity> map = indexEntities.stream().collect(Collectors.toMap(IndexEntity::getId, Function.identity()));
注:该栗子摘于博客:https://blog.csdn.net/m0_38072683/article/details/105478175?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-1&spm=1001.2101.3001.4242