1. 集合遍历

根据前面所学的内容可知,如果想要使用某个数据结构来存储一些类型的元素,我们可以选择数组或者Java提供的多种类型的集合,如ArrayList、HashSet和HashMap等。针对于不同的类型,遍历的方法也有多种可供选择,下面我们依次回顾一下。

  • 数组的遍历:数组的遍历可以使用传统的for循环(对比于for-each而言)或者是增强for循环,即for-each循环

    1. public class ListDemo {
    2. public static void main(String[] args) {
    3. int[] array = new int[]{1, 3, 10, 6, 2};
    4. for (int i = 0; i < array.length; i++) {
    5. System.out.println(array[i]);
    6. }
    7. for(int ele:array){
    8. System.out.println(ele);
    9. }
    10. }
    11. }
  • 单列集合的遍历:常用的单列集合有List类型和Set类型两种,集合遍历的方法有for-each循环和使用迭代器两种

    • 使用for-each循环

      1. public class ListDemo {
      2. public static void main(String[] args) {
      3. List<String> list = new ArrayList<>();
      4. list.add("Forlogen");
      5. list.add("kobe");
      6. list.add("James");
      7. Iterator<String> iter = list.iterator();
      8. while (iter.hasNext()){
      9. System.out.println(iter.next());
      10. }
      11. for(String ele:list){
      12. System.out.println(ele);
      13. }
      14. }
      15. }

浅析Java中的Collection 浅析Java中的Iterator

  • 双列集合的遍历:常用的双列集合有Map等,针对Map又有两种方法遍历集合中的元素
    • 使用Set<K> keySet()获取集合中的所有键,然后使用get(Object key)根据指定的键获取对应的值 ```java import java.util.HashMap; import java.util.Map; import java.util.Set;

public class ListDemo { public static void main(String[] args) { Map m = new HashMap<>(); m.put(10, “Forlogen”); m.put(23, “James”); m.put(24, “kobe”);

  1. Set<Integer> keys = m.keySet();
  2. for(Integer ele : keys){
  3. System.out.println(m.get(ele));
  4. }
  5. }

}


      - 使用Map集合内部的Entry对象来进行遍历:首先使用`entrySet()`获取保存Entry对象的Set集合。然后遍历集合中的Entry对象,使用`getKey()`和`getValue()`获取每个对象的键和值
```java
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapMain {
    public static void main(String[] args) {
        Map<Integer,String> m = new HashMap<>();
        m.put(23, "James");
        m.put(24, "kobe"); 

        Set<Map.Entry<Integer, String>> entries = m.entrySet();
        for (Map.Entry ele: entries) {
            System.out.println(ele.getKey() + " = " + ele.getValue()); 
        }
    }
}

浅析Java中的Map

从上面的总结中可以看出,针对于不同的数据结构有着多种遍历方法可供选择,但循环遍历有什么弊端呢,或者说有没有更好的方式来遍历元素呢?要理解这个问题,我们需要问自己一个问题:我们遍历元素目的是为了什么?不管是简单的直接输出遍历得到的每个元素,还是对每个元素再执行附加的更多操作,我们的目的都是使用遍历得到的元素,而不关心是使用什么样的方式进行遍历

如下所示,针对于数组的遍历来说,我们的目的是使用遍历得到的array[i],而不管是用传统的for循环还是for-each循环。具体到传统的for循环,for循环的语法表示的是怎么做(从头到尾依次取数组中对应索引的元素),而循环体表示的是做什么(直接输出数组元素)。前者是方式,后者是目的!

int[] array = new int[]{1, 3, 10, 6, 2};
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
}

下面我们通过一个更复杂的例子来感受一下,什么是只关心做什么而不关心怎么做。假设现有一个String类型的集合,我们希望对集合中的元素进行过滤,分三步操作:

  • 选出长度大于3的元素
  • 选出字符串首字母为”b”的元素
  • 最后遍历输出过滤后集合中的元素

如果使用循环遍历,我们只能这样做:

import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Forlogen");
        list.add("kobe");
        list.add("James");
        list.add("ball");
        list.add("bill");

        // 1
        List<String> list2 = new ArrayList<>();
        for(String ele:list){
            if (ele.length() > 3){
                list2.add(ele);
            }
        }

        // 2
        List<String> list3 = new ArrayList<>();
        for (String ele: list2) {
            if (ele.startsWith("b")){
                list3.add(ele);
            }
        }

        // 3
        for(String ele:list3){
            System.out.println(ele);
        }
    }
}

从上面的代码中可以看出,整个过程使用了3个ArrayList存储元素,以及使用了3次的循环遍历。而我们的目的只是为过滤集合中的元素,因此能否有更好的方式来完成上面同样的任务呢?

根据前面所学的函数式编程思想Lambda表达式函数式接口可知,它们关注的就是做什么而不是怎么做。对比来看,发现我们的诉求和函数式编程思想是一致的。因此,Java在JDK8之后引入了Stream流来实现这种诉求,方便用户使用更优雅的代码完成相同的功能。例如,如果上面的示例使用Stream流来做,可以写成如下的形式:

import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Forlogen");
        list.add("kobe");
        list.add("James");
        list.add("ball");
        list.add("bill");

        list.stream().filter((name)->name.length() > 3)
                        .filter((name)->name.startsWith("b"))
                        .forEach((name)-> System.out.println(name));
    }
}

我们只需要一行代码就可以代替上面三个for-each循环所做的事,不仅优雅,而且高效。


2. 流式思想

如何理解Stream流所代表的流式思想呢?首先,我们要将其和前面所学的IO流中的流区分开,然后再去理解流式思想。根据上面集合遍历的例子可以看出,流式思想极像生产中的流水线,每个流模型都只负责它所精通的工作,同时不需要额外的内存空间存储所处理后的结果。以我们生活中的做饭为例,做一顿饭需要买菜、洗菜、切菜、炒菜和装盘,每个步骤都是使用上一个步骤的结果,但是每个步骤处理完菜之后就交给下一个步骤,自己并不会留着菜。

Steam流- 1 - 图1
source

其实流式思想广泛在多个领域使用,如数据科学中数据预处理的pipeline,NLP或者CV领域中对于数据集中数据的预处理等等。只要我们设计好了整体的pipeline,我们只需要关心最后得到的结果,而不必在意中间的处理步骤。以NLP中对于语料库中数据的预处理为例,通常需要执行去除无用字符→分词→词干提取→构建词汇表等操作,我们需要使用的是整个pipeline处理后的结果,而不关注中间步骤数据被处理成什么样,因此中间步骤也没有必要保存处理后的结果。

3. Stream流

3.1 概念

pipeline中的每一个阶段都是一个流模型,通过调用pipeline中设计的方法可以实现一个流到另一个流的转换。Java中的Stream流可以看成是一个来自数据源的元素队列,其中:

  • 元素:指特定类型的对象,所有的元素会形成一个队列,但Stream流并不存储元素
  • 数据源:指流中数据的来源,如数组、集合等

Stream流具有两个基础特征:

  • Pipelining:从Stream的思想可知,中间的每个操作都会返回流对象本身,多个操作就可以构成类似于流水线的模型
  • 内部迭代:不同于之前的遍历方法,Stream流可以直接调用遍历方法

因此,Java中要想使用Stream流,通过需要三个步骤:

  • 获取Stream流中的数据源
  • 执行Stram流中的转换操作
  • 获取想要的结果

每次转换原有的Stream对象并不发生改变,而是返回一个新的流对象。

3.2 流的获取

Java中获取Stream流有两种方式:

  • 所有的Collection集合都可以通过stream()获取对应的流
    如ArrayList的stream()源码如下:

    default Stream<E> stream() {
      return StreamSupport.stream(spliterator(), false);
    }
    
  • 使用java.util.stream.Stream<T>接口中的静态方法of()获取数组对应的流,由于方法中的参数为可变参数,因此可以传递数组

    static <T> Stream<T> of (T...values)
    

对于单列集合来说,可以直接使用stream()方法获取流:

List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();

Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();

对于Map这样的双列集合来说,它需要先通过keySet()获取键、通过values()获取值或者使用entrySet()获取所有键值之间的映射关系,由于它们的返回值都是Set集合,因此可以再使用stream()获取流。

// Map集合需要先转换为Set集合,再通过set集合的stream()获取流
Map<Integer, String> map = new HashMap<>();
// 获取键
Set<Integer> keys = map.keySet();
Stream<Integer> steam3 = keys.stream();
// 获取值
Collection<String> values = map.values();
Stream<String> stream4 = values.stream();
// 获取键值映射关系
Set<Map.Entry<Integer, String>> entries = map.entrySet();
Stream<Map.Entry<Integer, String>> stream5 = entries.stream();

对于不同类型的数组来说,可以使用Stream接口中的of()来获取流。

Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
Integer[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream7 = Stream.of(arr);

Stream流中包含有众多的方法,这些方法可以分为两类:

  • 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用,如filter()limit()
  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此无法再调用其他的流模型,如count()forEach()

3.3 forEach

Stream流中的forEach虽然看起来像和for-each循环完成的是同样的工作,但原理有所不同。方法接收一个Consumer接口函数,会将每一个流元素将给该函数进行处理。

void forEach(Consumer<? super T> action)

3.4 filter

filter方法用于对Stream流中的数据进行过滤,它接收的是一个Predicate接口函数,然后对于每一个流元素进行判断,返回的流中只保留符合条件的元素。

Stream<T> filter(Predicate<? super T> predicate)

3.5 map

map方法将流中的元素映射到另一个流中,它使用Function接口函数来实现元素类型的转换,从而实现流之间的元素的映射。

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

3.6 count

count方法是一个终结方法,它用于统计Stream流中元素的个数。

long count()

3.7 limit

limit方法用于截取流中的元素,只取用前n个。它是一个延迟方法,只是对流中的元素进行截取,返回一个新的流。

Stream<T> limit(long maxSize)

如果当前集合长度大于maxSize则进行截取,否则不进行操作

3.8 skip

skip方法用于跳过元素,返回的是一个新的流。

Stream<T> skip(long n)

如果流的当前长度大于n,则跳过前n个,否则得到一个长度为0的新流。

3.9 concat

concat方法用于合并两个流,它是Stream接口中的静态方法,因此可以使用接口名直接调用。

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

完整的实验代码:

import java.util.stream.Stream;

public class SomeFuncInStream {
    public static void main(String[] args) {
        UsingForEach();
        System.out.println("-----------------");
        UsingFilter();
        System.out.println("-----------------");
        UsingMap();
        System.out.println("-----------------");
        UsingCount();
        System.out.println("-----------------");
        UsingLimit();
        System.out.println("-----------------");
        UsingSkip();
        System.out.println("-----------------");
        UsingConcat();
    }

    private static void UsingConcat() {
        Stream<String> stream = Stream.of("Forlogen", "kobe");
        Stream<String> stream1 = Stream.of("James", "ball");
        Stream.concat(stream, stream1).forEach(name-> System.out.println(name));
    }

    private static void UsingSkip() {
        Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
        stream.skip(1).forEach(name-> System.out.println(name));
    }

    private static void UsingLimit() {
        Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
        stream.limit(2).forEach(name-> System.out.println(name));
    }

    private static void UsingCount() {
        Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
        long count = stream.filter(name -> name.length() > 4).count();
        System.out.println(count);
    }

    private static void UsingMap() {
        Stream<String> stream = Stream.of("1", "2", "3");
        stream.map(s->Integer.parseInt(s)).forEach(x-> System.out.println(x));

    }

    private static void UsingFilter() {
        Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
        stream.filter(name->name.length() > 4).forEach(name-> System.out.println(name));
    }

    private static void UsingForEach() {
        Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
        stream.forEach(name -> System.out.println(name));
    }

}

输出为:

Forlogen
kobe
James
-----------------
Forlogen
James
-----------------
1
2
3
-----------------
2
-----------------
Forlogen
kobe
-----------------
kobe
James
-----------------
Forlogen
kobe
James
ball

浅析Java中的函数式接口