【Java基础】Java8新特性(二):Streams API及使用

文章目录

引言

什么是Streams?

它不是集合元素,不是数据结构,并不保存数据

一个高级版本的迭代器(Iterator)

  1. Streams会隐式地在内部进行遍历,做出相应的数据转换
  2. 单向的,不可往复
    即数据只能遍历一次,遍历过一次后即用尽了
  3. 可以并行化操作
    parallelStream()方法,依赖于 Java7 中引入的 Fork/Join 框架来拆分任务和加速处理过程

数据源本身可以是无限的(集合是有限的)

Streams的用途?

作为一个高级版本的迭代器(Iterator),当然是用来对数据源进行遍历操作的。

疑问:遍历操作用for循环不就好了?

示例:打印与年龄超过 65 岁的买家进行交易的卖家姓名,并按姓名排序:

  • 利用for循环处理:
//找到与年龄超过 65 岁的买家进行交易的卖家
Set<Seller> sellers = new HashSet<>();
for (Txn t : txns) {
    if (t.getBuyer().getAge() >= 65)
        sellers.add(t.getSeller());
}
// 按姓名排序
List<Seller> sorted = new ArrayList<>(sellers);
Collections.sort(sorted, new Comparator<Seller>() {
    public int compare(Seller a, Seller b) {
        return a.getName().compareTo(b.getName());
    }
});
// 打印
for (Seller s : sorted)
    System.out.println(s.getName()); 
  • 利用Stream处理:
txns.stream()
    .filter(t -> t.getBuyer().getAge() >= 65)
    .map(Txn::getSeller)
    .distinct()
    .sorted(comparing(Seller::getName))
    .map(Seller::getName)
    .forEach(System.out::println);

由上示例可见:

Streams API方式的代码组织结构更清晰,可读性更强。我们只需指定“做什么”,而“如何做”交给Streams API就可以了。

从性能优化层面看:

for循环为外部迭代,其“串行”、“依次”特性使得集合框架无法对控制流进行优化。而Streams API为类库的内部迭代,类库可以进行各种优化操作(乱序执行、惰性求值、并行等),提升迭代性能。

1 Stream的组成

当我们使用一个流的时候,通常包括三个基本步骤:

  1. 获取一个数据源(source);
  2. 数据转换;
  3. 执行操作获取想要的结果。

每次转换原有 Stream 对象(数据源)不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示:
【Java基础】Java8新特性(二):Streams API及使用
流的操作类型包括:

编号 操作类型 说明 常见操作
1 Intermediate a.其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。b.一个流可以后面跟随0个或多个 intermediate 操作。c.该类操作都是惰性化的。就是说,仅仅调用到这类方法,并没有真正开始流的遍历 map (mapToInt, flatMap等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered
2 Terminal a.一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。b.这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。c.急性操作,流的操作步骤可以理解为:数据源—惰性操作—惰性操作—急性操作 forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator
3 short-circuiting a.当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。b.具体的:1)、对于一个 intermediate 操作,如果它接受的是一个无限大的 Stream,但返回一个有限的新 Stream。2)、对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。 anyMatch、allMatch、noneMatch、findFirst、findAny、limit

这里放一个简单的流操作示例:

int sum = widgets.stream()
				 .filter(w -> w.getColor() == RED)
 				 .mapToInt(w -> w.getWeight())
 				 .sum();

2 Stream操作详解

2.1 流的构造与转换

2.1.1 流的构造

流的构造主要有三种方式。
1 通过Stream.of()方法

		String[] nameArray = {"Tom", "Jerry"};
        Stream<String> stream = Stream.of(nameArray);

2 [Collections].stream()方法

		String[] nameArray = {"Tom", "Jerry"};
        List<String> list = Arrays.asList(nameArray);
        Stream<String> stream = list.stream();

3 数值流
对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。

		IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
		IntStream.range(1, 3).forEach(System.out::println);
		IntStream.rangeClosed(1, 3).forEach(System.out::println);

2.1.2 流的转换

流既然能通过其它的数据类型进行构造,同样的也能转换成其它的数据结构,常见的有如下三种。
1 转换成数组

		String[] toArray = stream.toArray(String[]::new);	

2 转换成集合

		// transform to List
		List<String> stringList = stream.collect(Collectors.toList());
		// transform to set
        Set<String> stringSet = stream.collect(Collectors.toSet());
        // transform to map
        Map<String, Integer> stringMap = stream.collect(Collectors.toMap(String::toUpperCase, String::length));

注意:当通过collect(Collectors.toMap())转换成Map的时候,如果出现的Key重复的情况,会报异常。

3 转换成String

 		String toString = stream.collect(Collectors.joining()).toString();

2.2 流的详细操作

map
功能:生成 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。

		//平方数
		List<Integer> nums = Arrays.asList(1, 2, 3, 4);
		List<Integer> squareNums = nums.stream().
		map(n -> n * n).
		collect(Collectors.toList());
		 
		//转换为大写
		List<String> output = wordList.stream().
		map(String::toUpperCase).
		collect(Collectors.toList());

flatMap
功能:把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

		Stream<List<Integer>> inputStream = Stream.of(
		 Arrays.asList(1),
		 Arrays.asList(2, 3),
		 Arrays.asList(4, 5, 6)
		 );
		Stream<Integer> outputStream = inputStream.
		flatMap((childList) -> childList.stream());

filter
根据条件进行元素的过滤。

		 // 获取文章中的全部单词
		 List<String> output = reader.lines().
		 flatMap(line -> Stream.of(line.split(REGEXP))).
		 filter(word -> word.length() > 0).
		 collect(Collectors.toList());

forEach
功能:接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

		//找出男性并打印姓名
		 
		//Java 8
		personlist.stream()
		 .filter(p -> p.getGender() == Person.Sex.MALE)
		 .forEach(p -> System.out.println(p.getName()));
		
		//Pre-Java 8
		for (Person p : personlist) {
		 if (p.getGender() == Person.Sex.MALE) {
		 System.out.println(p.getName());
		 }
		}

注意:forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。

reduce
功能: 对Stream 元素进行组合运算(提供一个起始值,然后依照BinaryOperator运算规则,和前面 Stream 的第1个、第2个、第 n 个元素组合)。

		// 字符串连接,concat = "ABCD"
		String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); 
		// 求最小值,minValue = -3.0
		double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); 
		// 求和,sumValue = 10, 有起始值
		int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
		// 求和,sumValue = 10, 无起始值
		sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
		// 过滤,字符串连接,concat = "ace"
		concat = Stream.of("a", "B", "c", "D", "e", "F");
	 	filter(x -> x.compareTo("Z") > 0).
	 	reduce("", String::concat);

sum、min、max、average

		List<Integer> integers = Arrays.asList(1, 2, 3, 4);
		//sum操作
		Integer result = integers.stream().mapToInt(w -> w).sum();
		//等价于reduce操作
		Integer result = integers.stream().reduce(0, (a, b) -> a + b);

注意:Stream 中的reduce、max/min、findFirst、findAny等操作返回的结果类型为:Optional.

optional

  • Optional作为一个容器,它可能含有某值,或者不包含;
  • 使用它的目的是尽可能避免 NullPointerException
    在更复杂的 if (element != null) 的情况中,使用 Optional 代码的可读性更好;而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

limit/skip
功能:limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。

public void testLimitAndSkip () {
            List<Person> persons = new ArrayList();
            for (int i = 1; i <= 10000; i++) {
                Person person = new Person(i, "name" + i);
                persons.add(person);
            }
            List<String> personList2 = persons.stream().
                    map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
            System.out.println(personList2);
        }
        private class Person {
            public int no;
            private String name;

            public Person(int no, String name) {
                this.no = no;
                this.name = name;
            }

            public String getName() {
                System.out.println(name);
                return name;
            }
        }

输出:

	name1
	name2
	name3
	name4
	name5
	name6
	name7
	name8
	name9
	name10
	[name4, name5, name6, name7, name8, name9, name10]

sorted
功能:排序。比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。

		//找出全文的单词,转小写,并排序
		List<String> words = br.lines().
		 	flatMap(line -> Stream.of(line.split(" "))).
		 	filter(word -> word.length() > 0).
		 	map(String::toLowerCase).
		 	distinct().
		 	sorted().
		 	collect(Collectors.toList());
		br.close();
		System.out.println(words);

Match
匹配元素,包括allMatch, anyMatch匹配等。

		List<Person> persons = new ArrayList();
		persons.add(new Person(1, "name" + 1, 10));
		persons.add(new Person(2, "name" + 2, 21));
		persons.add(new Person(3, "name" + 3, 34));
		persons.add(new Person(4, "name" + 4, 6));
		persons.add(new Person(5, "name" + 5, 55));
		//allMatch
		boolean isAllAdult = persons.stream().
		 	allMatch(p -> p.getAge() > 18);
		System.out.println(isAllAdult); // false
		//anyMatch
		boolean isThereAnyChild = persons.stream().
		 	anyMatch(p -> p.getAge() < 12);
		System.out.println(isThereAnyChild); // true
		//noneMatch
		boolean isNoChild = persons.stream().
		 	anyMatch(p -> p.getAge() > 60);
		System.out.println(isNoChild); // true

3 Guava的transform()

以Lists.transform()为例进行说明

3.1 Guava的Lists.transform()返回的结果具有“视图(View)”特性:

  • view和它所依赖的数据结构共享底层数据,当底层数据结构修改时,view会变化
  • 当view修改时(前提是view可以修改),底层数据结构也会修改
    而Stream每次转换原有Stream对象不改变,返回一个新的 Stream 对象,不修改自己所封装的底层数据结构的数据,如果希望“transform后不要返回视图,而得到独立拷贝”,*上的建议这样:
	//返回不可变列表(推荐)
	ImmutableList.copyOf(Lists.transform(targetlist, function));    
	
	//返回可变列表
	Lists.newArrayList(Lists.transform(targetlist, function)); 

3.2 Stream进行数据结构转换更加方便

4 项目中的实例

4.1 实例1

	/**
	 * 远程加载所有区域信息,并转为不可变Map
	 *
	 * @return
	 * @throws GatewayException
	 */
	private Map<Integer, AreaInfoMsg> loadAreas() throws GatewayException {
	    Map<Integer, AreaInfoMsg> areaMap = groupGeoGateway.getAllAreas()
	            .values().stream().flatMap(List::stream)
	            .collect(Collectors.toMap(AreaInfoMsg::getId, e -> e));
	    return ImmutableMap.copyOf(areaMap);
	}
	 
	/**
	 * 获取所有区域信息
	 * @return
	 */
	public Map<Integer,List<AreaInfoMsg>> getAllAreas() throws GatewayException {
	    logger.debug("load areas from groupgeo now");
	    try {
	        return groupGeoThriftClient.mapArea();
	    } catch (TException e) {
	        throw new GatewayException("Failed loading areas from groupgeo");
	    }
	}

4.2 实例2

	//分批异步请求
	private ListenableFuture<Map<Integer, SinaiPoiModel>> innerBatchSubmit(List<Integer> manyIds) {
	    List<List<Integer>> lists = Lists.partition(manyIds, BATCH_LIMIT);
	    //分批提交,忽略单次提交异常
	    List<ListenableFuture<Map<Integer, SinaiPoiModel>>> futures = Lists.transform(lists,
	            ids -> {
	                try {
	                    return innerSubmit(ids);
	                } catch (GatewayException e) {
	                    return Futures.immediateFailedCheckedFuture(e);
	                }
	            });
	    //多次提交的结果合并为一个异步结果
	    return Futures.transform(
	            Futures.successfulAsList(futures),
	            mapList -> mapList.stream().filter(Objects::nonNull)
	                    .collect(HashMap::new, Map::putAll, Map::putAll)
	    );
	}

5 Stream的不足

  1. 遍历操作无法提前结束循环(break/return)

  2. 不支持对其元素进行直接操作或直接访问,而只支持通过声明式操作在其上进行运算然后得到结果

  3. 不推荐Web程序采用并行流(parallelStream

List<String> names = people.parallelStream()
                           .map(Person::getName)
                           .collect(toList());

parallelStream会调用JVM内全局共享的一个 Fork/Join 框架,里面默认会启动“当前机器CPU核数”个线程。也就是说,并行流最好仅用于“纯CPU计算”——因为一旦某个网络请求把其中某个线程阻塞了,那就是JVM全局共享的“CPU核数那么多的线程”就会被阻塞

上一篇:Java使用手册-01 Basic I/O 封面


下一篇:ElasticSearch修改多层结构中的数据附java代码