《Java常见问题解法》第三章 流式操作

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


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值