Set集合

前面我们已经介绍了列表,我们接着来看Set集合,这种集合类型比较特殊,我们先来看看Set的定义:

java 复制代码
public interface Set<E> extends Collection<E> {
    // Set集合中基本都是从Collection直接继承过来的方法,只不过对这些方法有更加特殊的定义
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);

    //添加元素只有在当前Set集合中不存在此元素时才会成功,如果插入重复元素,那么会失败
    boolean add(E e);

    //这个同样是删除指定元素
    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    //同样是只能插入那些不重复的元素
    boolean addAll(Collection<? extends E> c);
  
    boolean retainAll(Collection<?> c);
    boolean removeAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();

    //这个方法我们同样会放到多线程中进行介绍
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT);
    }
}

我们发现接口中定义的方法都是Collection中直接继承的,因此,Set支持的功能其实也就和Collection中定义的差不多,只不过:

  • 不允许出现重复元素
  • 不支持随机访问(不允许通过下标访问)

首先认识一下HashSet,它的底层就是采用哈希表实现的(我们在这里先不去探讨实现原理,因为底层实质上是借用的一个HashMap在实现,这个需要我们学习了Map之后再来讨论)我们可以非常高效的从HashSet中存取元素,我们先来测试一下它的特性:

java 复制代码
public static void main(String[] args) {
    Set<String> set = new HashSet<>();
    System.out.println(set.add("AAA"));   //这里我们连续插入两个同样的字符串
    System.out.println(set.add("AAA"));
    System.out.println(set);   //可以看到,最后实际上只有一个成功插入了
}
image-20221003211330129

在Set接口中并没有定义支持指定下标位置访问的添加和删除操作,我们只能简单的删除Set中的某个对象:

java 复制代码
public static void main(String[] args) {
    Set<String> set = new HashSet<>();
    System.out.println(set.add("AAA"));
    System.out.println(set.remove("AAA"));
    System.out.println(set);
}

由于底层采用哈希表实现,所以说无法维持插入元素的顺序:

java 复制代码
public static void main(String[] args) {
    Set<String> set = new HashSet<>();
    set.addAll(Arrays.asList("A", "0", "-", "+"));
    System.out.println(set);
}
image-20221003211635759

那要是我们就是想要使用维持顺序的Set集合呢?我们可以使用LinkedHashSet,LinkedHashSet底层维护的不再是一个HashMap,而是LinkedHashMap,它能够在插入数据时利用链表自动维护顺序,因此这样就能够保证我们插入顺序和最后的迭代顺序一致了。

java 复制代码
public static void main(String[] args) {
    Set<String> set = new LinkedHashSet<>();
    set.addAll(Arrays.asList("A", "0", "-", "+"));
    System.out.println(set);
}
image-20221003212147700

还有一种Set叫做TreeSet,它会在元素插入时进行排序:

java 复制代码
public static void main(String[] args) {
    TreeSet<Integer> set = new TreeSet<>();
    set.add(1);
    set.add(3);
    set.add(2);
    System.out.println(set);
}
image-20221003212233263

可以看到最后得到的结果并不是我们插入顺序,而是按照数字的大小进行排列。当然,我们也可以自定义排序规则:

java 复制代码
public static void main(String[] args) {
    TreeSet<Integer> set = new TreeSet<>((a, b) -> b - a);  //同样是一个Comparator
    set.add(1);
    set.add(3);
    set.add(2);
    System.out.println(set);
}

目前,Set集合只是粗略的进行了讲解,但是学习Map之后,我们还会回来看我们Set的底层实现,所以说最重要的还是Map。本节只需要记住Set的性质、使用即可。

Map映射

什么是映射?我们在高中阶段其实已经学习过映射(Mapping)了,映射指两个元素的之间相互“对应”的关系,也就是说,我们的元素之间是两两对应的,是以键值对的形式存在。

39e19f3e-04e8-4c43-8fb5-6d5288a7cdf8

而Map就是为了实现这种数据结构而存在的,我们通过保存键值对的形式来存储映射关系,就可以轻松地通过键找到对应的映射值,比如现在我们要保存很多学生的信息,而这些学生都有自己的ID,我们可以将其以映射的形式保存,将ID作为键,学生详细信息作为值,这样我们就可以通过学生的ID快速找到对应学生的信息了。

image-20221003213157956

在Map中,这些映射关系被存储为键值对,我们先来看看Map接口中定义了哪些操作:

java 复制代码
//Map并不是Collection体系下的接口,而是单独的一个体系,因为操作特殊
//这里需要填写两个泛型参数,其中K就是键的类型,V就是值的类型,比如上面的学生信息,ID一般是int,那么键就是Integer类型的,而值就是学生信息,所以说值是学生对象类型的
public interface Map<K,V> {
    //-------- 查询相关操作 --------
  
  	//获取当前存储的键值对数量
    int size();

    //是否为空
    boolean isEmpty();

    //查看Map中是否包含指定的键
    boolean containsKey(Object key);

    //查看Map中是否包含指定的值
    boolean containsValue(Object value);

    //通过给定的键,返回其映射的值
    V get(Object key);

    //-------- 修改相关操作 --------

    //向Map中添加新的映射关系,也就是新的键值对
    V put(K key, V value);

    //根据给定的键,移除其映射关系,也就是移除对应的键值对
    V remove(Object key);


    //-------- 批量操作 --------

    //将另一个Map中的所有键值对添加到当前Map中
    void putAll(Map<? extends K, ? extends V> m);

    //清空整个Map
    void clear();


    //-------- 其他视图操作 --------

    //返回Map中存放的所有键,以Set形式返回
    Set<K> keySet();

    //返回Map中存放的所有值
    Collection<V> values();

    //返回所有的键值对,这里用的是内部类Entry在表示
    Set<Map.Entry<K, V>> entrySet();

    //这个是内部接口Entry,表示一个键值对
    interface Entry<K,V> {
        //获取键值对的键
        K getKey();

        //获取键值对的值
        V getValue();

        //修改键值对的值
        V setValue(V value);

        //判断两个键值对是否相等
        boolean equals(Object o);

        //返回当前键值对的哈希值
        int hashCode();

        ...
    }

    ...
}

当然,Map中定义了非常多的方法,尤其是在Java 8之后新增的大量方法,我们会在后面逐步介绍的。

我们可以来尝试使用一下Map,实际上非常简单,这里我们使用最常见的HashMap,它的底层采用哈希表实现:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "小明");   //使用put方法添加键值对,返回值我们会在后面讨论
    map.put(2, "小红");
    System.out.println(map.get(2)); //使用get方法根据键获取对应的值
}
image-20221003214807048

注意,Map中无法添加相同的键,同样的键只能存在一个,即使值不同。如果出现键相同的情况,那么会覆盖掉之前的:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "小明");
    map.put(1, "小红");   //这里的键跟之前的是一样的,这样会导致将之前的键值对覆盖掉
    System.out.println(map.get(1));
}
image-20221003214807048

为了防止意外将之前的键值对覆盖掉,我们可以使用:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "小明");
    map.putIfAbsent(1, "小红");   //Java8新增操作,只有在不存在相同键的键值对时才会存放
    System.out.println(map.get(1));
}

还有,我们在获取一个不存在的映射时,默认会返回null作为结果:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "小明");   //Map中只有键为1的映射
    System.out.println(map.get(3));  //此时获取键为3的值,那肯定是没有的,所以说返回null
}

我们也可以为这种情况添加一个预备方案,当Map中不存在时,可以返回一个备选的返回值:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "小明");
    System.out.println(map.getOrDefault(3, "备胎"));   //Java8新增操作,当不存在对应的键值对时,返回备选方案
}

同样的,因为HashMap底层采用哈希表实现,所以不维护顺序,我们在获取所有键和所有值时,可能会是乱序的:

java 复制代码
public static void main(String[] args) {
    Map<String , String> map = new HashMap<>();
    map.put("0", "十七张");
    map.put("+", "牌");
    map.put("P", "你能秒我");
    System.out.println(map);
    System.out.println(map.keySet());
    System.out.println(map.values());
}
image-20221003220156062

如果需要维护顺序,我们同样可以使用LinkedHashMap,它的内部对插入顺序进行了维护:

java 复制代码
public static void main(String[] args) {
    Map<String , String> map = new LinkedHashMap<>();
    map.put("0", "十七张");
    map.put("+", "牌");
    map.put("P", "你能秒我");
    System.out.println(map);
    System.out.println(map.keySet());
    System.out.println(map.values());
}
image-20221003220458539

实际上Map的使用还是挺简单的,我们接着来看看Map的底层是如何实现的,首先是最简单的HashMap,我们前面已经说过了,它的底层采用的是哈希表,首先回顾我们之前学习的哈希表,我们当时说了,哈希表可能会出现哈希冲突,这样保存的元素数量就会存在限制,而我们可以通过连地址法解决这种问题,最后哈希表就长这样了:

image-20220820221104298

实际上这个表就是一个存放头结点的数组+若干结点,而HashMap也是这样的,我们来看看这里面是怎么定义的:

java 复制代码
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  
  	...
    
  	static class Node<K,V> implements Map.Entry<K,V> {   //内部使用结点,实际上就是存放的映射关系
        final int hash;
        final K key;   //跟我们之前不一样,我们之前一个结点只有键,而这里的结点既存放键也存放值,当然计算哈希还是使用键
        V value;
        Node<K,V> next;
				...
    }
  	
  	...
  
  	transient Node<K,V>[] table;   //这个就是哈希表本体了,可以看到跟我们之前的写法是一样的,也是头结点数组,只不过HashMap中没有设计头结点(相当于没有头结点的链表)
  
  	final float loadFactor;   //负载因子,这个东西决定了HashMap的扩容效果
  
  	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; //当我们创建对象时,会使用默认的负载因子,值为0.75
    }
  
  	...     
}

可以看到,实际上底层大致结构跟我们之前学习的差不多,只不过多了一些特殊的东西:

  • HashMap支持自动扩容,哈希表的大小并不是一直不变的,否则太过死板
  • HashMap并不是只使用简单的链地址法,当链表长度到达一定限制时,会转变为效率更高的红黑树结构

我们来研究一下它的put方法:

java 复制代码
public V put(K key, V value) {
  	//这里计算完键的哈希值之后,调用的另一个方法进行映射关系存放
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)  //如果底层哈希表没初始化,先初始化
        n = (tab = resize()).length;   //通过resize方法初始化底层哈希表,初始容量为16,后续会根据情况扩容,底层哈希表的长度永远是2的n次方
  	//因为传入的哈希值可能会很大,这里同样是进行取余操作
  	//(n - 1) & hash 等价于 hash % n 这里的i就是最终得到的下标位置了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);   //如果这个位置上什么都没有,那就直接放一个新的结点
    else {   //这种情况就是哈希冲突了
        Node<K,V> e; K k;
        if (p.hash == hash &&   //如果上来第一个结点的键的哈希值跟当前插入的键的哈希值相同,键也相同,说明已经存放了相同键的键值对了,那就执行覆盖操作
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;   //这里直接将待插入结点等于原本冲突的结点,一会直接覆盖
        else if (p instanceof TreeNode)   //如果第一个结点是TreeNode类型的,说明这个链表已经升级为红黑树了
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  //在红黑树中插入新的结点
        else {
            for (int binCount = 0; ; ++binCount) {  //普通链表就直接在链表尾部插入
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);  //找到尾部,直接创建新的结点连在后面
                    if (binCount >= TREEIFY_THRESHOLD - 1) //如果当前链表的长度已经很长了,达到了阈值
                        treeifyBin(tab, hash);			//那么就转换为红黑树来存放
                    break;   //直接结束
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))  //同样的,如果在向下找的过程中发现已经存在相同键的键值对了,直接结束,让p等于e一会覆盖就行了
                    break;
                p = e;
            }
        }
        if (e != null) { // 如果e不为空,只有可能是前面出现了相同键的情况,其他情况e都是null,所有直接覆盖就行
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;   //覆盖之后,会返回原本的被覆盖值
        }
    }
    ++modCount;
    if (++size > threshold)   //键值对size计数自增,如果超过阈值,会对底层哈希表数组进行扩容
        resize();   //调用resize进行扩容
    afterNodeInsertion(evict);
    return null;  //正常插入键值对返回值为null
}

是不是感觉只要前面的数据结构听懂了,这里简直太简单。根据上面的推导,我们在正常插入一个键值对时,会得到null返回值,而冲突时会得到一个被覆盖的值:

java 复制代码
public static void main(String[] args) {
    Map<String , String> map = new HashMap<>();
    System.out.println(map.put("0", "十七张"));
    System.out.println(map.put("0", "慈善家"));
}
image-20221003224137177

现在我们知道,当HashMap的一个链表长度过大时,会自动转换为红黑树:

710c1c38-95a8-493d-8645-067b991af908

但是这样始终治标不治本,受限制的始终是底层哈希表的长度,我们还需要进一步对底层的这个哈希表进行扩容才可以从根本上解决问题,我们来看看resize()方法:

java 复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;   //先把下面这几个旧的东西保存一下
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;  //这些是新的容量和扩容阈值
    if (oldCap > 0) {  //如果旧容量大于0,那么就开始扩容
        if (oldCap >= MAXIMUM_CAPACITY) {  //如果旧的容量已经大于最大限制了,那么直接给到 Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return oldTab;  //这种情况不用扩了
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)   //新的容量等于旧容量的2倍,同样不能超过最大值
            newThr = oldThr << 1; //新的阈值也提升到原来的两倍
    }
    else if (oldThr > 0) // 旧容量不大于0只可能是还没初始化,这个时候如果阈值大于0,直接将新的容量变成旧的阈值
        newCap = oldThr;
    else {               // 默认情况下阈值也是0,也就是我们刚刚无参new出来的时候
        newCap = DEFAULT_INITIAL_CAPACITY;   //新的容量直接等于默认容量16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //阈值为负载因子乘以默认容量,负载因子默认为0.75,也就是说只要整个哈希表用了75%的容量,那么就进行扩容,至于为什么默认是0.75,原因很多,这里就不解释了,反正作为新手,这些都是大佬写出来的,我们用就完事。
    }
    ...
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;   //将底层数组变成新的扩容之后的数组
    if (oldTab != null) {  //如果旧的数组不为空,那么还需要将旧的数组中所有元素全部搬到新的里面去
      	...   //详细过程就不介绍了
    }
}

是不是感觉自己有点了解HashMap的运作机制了,其实并不是想象中的那么难,因为这些东西再怎么都是人写的。

而LinkedHashMap是直接继承自HashMap,具有HashMap的全部性质,同时得益于每一个节点都是一个双向链表,在插入键值对时,同时保存了插入顺序:

java 复制代码
static class Entry<K,V> extends HashMap.Node<K,V> {   //LinkedHashMap中的结点实现
    Entry<K,V> before, after;   //这里多了一个指向前一个结点和后一个结点的引用
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

这样我们在遍历LinkedHashMap时,顺序就同我们的插入顺序一致。当然,也可以使用访问顺序,也就是说对于刚访问过的元素,会被排到最后一位。

当然还有一种比较特殊的Map叫做TreeMap,就像它的名字一样,就是一个Tree,它的内部直接维护了一个红黑树(没有使用哈希表)因为它会将我们插入的结点按照规则进行排序,所以说直接采用红黑树会更好,我们在创建时,直接给予一个比较规则即可,跟之前的TreeSet是一样的:

java 复制代码
public static void main(String[] args) {
    Map<Integer , String> map = new TreeMap<>((a, b) -> b - a);
    map.put(0, "单走");
    map.put(1, "一个六");
    map.put(3, "**");
    System.out.println(map);
}
image-20221003231135805

现在我们倒回来看之前讲解的HashSet集合,实际上它的底层很简单:

java 复制代码
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{

    private transient HashMap<E,Object> map;   //对,你没看错,底层直接用map来做事

    // 因为Set只需要存储Key就行了,所以说这个对象当做每一个键值对的共享Value
    private static final Object PRESENT = new Object();

    //直接构造一个默认大小为16负载因子0.75的HashMap
    public HashSet() {
        map = new HashMap<>();
    }
		
  	...
      
    //你会发现所有的方法全是替身攻击
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

    public int size() {
        return map.size();
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }
}

通过观察HashSet的源码发现,HashSet几乎都在操作内部维护的一个HashMap,也就是说,HashSet只是一个表壳,而内部维护的HashMap才是灵魂!就像你进了公司,在外面花钱请别人帮你写公司的业务,你只需要坐着等别人写好然后你自己拿去交差就行了。所以说,HashSet利用了HashMap内部的数据结构,轻松地就实现了Set定义的全部功能!

再来看TreeSet,实际上用的就是我们的TreeMap:

java 复制代码
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    //底层需要一个NavigableMap,就是自动排序的Map
    private transient NavigableMap<E,Object> m;

    //不用我说了吧
    private static final Object PRESENT = new Object();

    ...

    //直接使用TreeMap解决问题
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
		
  	...
}

同理,这里就不多做阐述了。

我们接着来看看Map中定义的哪些杂七杂八的方法,首先来看看compute方法:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "A");
    map.put(2, "B");
    map.compute(1, (k, v) -> {   //compute会将指定Key的值进行重新计算,若Key不存在,v会返回null
        return v+"M";     //这里返回原来的value+M
    });
  	map.computeIfPresent(1, (k, v) -> {   //当Key存在时存在则计算并赋予新的值
      return v+"M";     //这里返回原来的value+M
    });
    System.out.println(map);
}

也可以使用computeIfAbsent,当不存在Key时,计算并将键值对放入Map中:

java 复制代码
public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "A");
    map.put(2, "B");
    map.computeIfAbsent(0, (k) -> {   //若不存在则计算并插入新的值
        return "M";     //这里返回M
    });
    System.out.println(map);
}

merge方法用于处理数据:

java 复制代码
public static void main(String[] args) {
    List<Student> students = Arrays.asList(
            new Student("yoni", "English", 80),
            new Student("yoni", "Chiness", 98),
            new Student("yoni", "Math", 95),
            new Student("taohai.wang", "English", 50),
            new Student("taohai.wang", "Chiness", 72),
            new Student("taohai.wang", "Math", 41),
            new Student("Seely", "English", 88),
            new Student("Seely", "Chiness", 89),
            new Student("Seely", "Math", 92)
    );
    Map<String, Integer> scoreMap = new HashMap<>();
  	//merge方法可以对重复键的值进行特殊操作,比如我们想计算某个学生的所有科目分数之后,那么就可以像这样:
    students.forEach(student -> scoreMap.merge(student.getName(), student.getScore(), Integer::sum));
    scoreMap.forEach((k, v) -> System.out.println("key:" + k + "总分" + "value:" + v));
}

static class Student {
    private final String name;
    private final String type;
    private final int score;

    public Student(String name, String type, int score) {
        this.name = name;
        this.type = type;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public String getType() {
        return type;
    }
}

replace方法可以快速替换某个映射的值:

java 复制代码
public static void main(String[] args) {
    Map<Integer , String> map = new HashMap<>();
    map.put(0, "单走");
    map.replace(0, ">>>");   //直接替换为新的
    System.out.println(map);
}

也可以精准匹配:

java 复制代码
public static void main(String[] args) {
    Map<Integer , String> map = new HashMap<>();
    map.put(0, "单走");
    map.replace(0, "巴卡", "玛卡");   //只有键和值都匹配时,才进行替换
    System.out.println(map);
}

包括remove方法,也支持键值同时匹配:

java 复制代码
public static void main(String[] args) {
    Map<Integer , String> map = new HashMap<>();
    map.put(0, "单走");
    map.remove(0, "单走");  //只有同时匹配时才移除
    System.out.println(map);
}

是不是感觉学习了Map之后,涨了不少姿势?

Stream流

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

image-20221003232832897

它看起来就像一个工厂的流水线一样!我们就可以把一个Stream当做流水线处理:

java 复制代码
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("A");
    list.add("B");
    list.add("C");
  
  	//移除为B的元素
  	Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()){
        if(iterator.next().equals("B")) iterator.remove();
    }
  
  	//Stream操作
    list = list     //链式调用
            .stream()    //获取流
            .filter(e -> !e.equals("B"))   //只允许所有不是B的元素通过流水线
            .collect(Collectors.toList());   //将流水线中的元素重新收集起来,变回List
    System.out.println(list);
}

可能从上述例子中还不能感受到流处理带来的便捷,我们通过下面这个例子来感受一下:

java 复制代码
public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
  	list.add(3);

    list = list
            .stream()
      			.distinct()   //去重(使用equals判断)
            .sorted((a, b) -> b - a)    //进行倒序排列
            .map(e -> e+1)    //每个元素都要执行+1操作
            .limit(2)    //只放行前两个元素
            .collect(Collectors.toList());

    System.out.println(list);
}

当遇到大量的复杂操作时,我们就可以使用Stream来快速编写代码,这样不仅代码量大幅度减少,而且逻辑也更加清晰明了(如果你学习过SQL的话,你会发现它更像一个Sql语句)

注意:不能认为每一步是直接依次执行的!我们可以断点测试一下:

java 复制代码
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);

list = list
        .stream()
        .distinct()   //断点
        .sorted((a, b) -> b - a)
        .map(e -> {
            System.out.println(">>> "+e);   //断点
            return e+1;
        })
        .limit(2)   //断点
        .collect(Collectors.toList());

实际上,stream会先记录每一步操作,而不是直接开始执行内容,当整个链式调用完成后,才会依次进行,也就是说需要的时候,工厂的机器才会按照预定的流程启动。

接下来,我们用一堆随机数来进行更多流操作的演示:

java 复制代码
public static void main(String[] args) {
    Random random = new Random();  //没想到吧,Random支持直接生成随机数的流
    random
            .ints(-100, 100)   //生成-100~100之间的,随机int型数字(本质上是一个IntStream)
            .limit(10)   //只获取前10个数字(这是一个无限制的流,如果不加以限制,将会无限进行下去!)
            .filter(i -> i < 0)   //只保留小于0的数字
            .sorted()    //默认从小到大排序
            .forEach(System.out::println);   //依次打印
}

我们可以生成一个统计实例来帮助我们快速进行统计:

java 复制代码
public static void main(String[] args) {
    Random random = new Random();  //Random是一个随机数工具类
    IntSummaryStatistics statistics = random
            .ints(0, 100)
            .limit(100)
            .summaryStatistics();    //获取语法统计实例
    System.out.println(statistics.getMax());  //快速获取最大值
    System.out.println(statistics.getCount());  //获取数量
    System.out.println(statistics.getAverage());   //获取平均值
}

普通的List只需要一个方法就可以直接转换到方便好用的IntStream了:

java 复制代码
public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    list.stream()
            .mapToInt(i -> i)    //将每一个元素映射为Integer类型(这里因为本来就是Integer)
            .summaryStatistics();
}

我们还可以通过flat来对整个流进行进一步细分:

java 复制代码
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("A,B");
    list.add("C,D");
    list.add("E,F");   //我们想让每一个元素通过,进行分割,变成独立的6个元素
    list = list
            .stream()    //生成流
            .flatMap(e -> Arrays.stream(e.split(",")))    //分割字符串并生成新的流
            .collect(Collectors.toList());   //汇成新的List
    System.out.println(list);   //得到结果
}

我们也可以只通过Stream来完成所有数字的和,使用reduce方法:

java 复制代码
public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    int sum = list
            .stream()
            .reduce((a, b) -> a + b)   //计算规则为:a是上一次计算的值,b是当前要计算的参数,这里是求和
            .get();    //我们发现得到的是一个Optional类实例,通过get方法返回得到的值
    System.out.println(sum);
}

可能,作为新手来说,一次性无法接受这么多内容,但是在各位以后的开发中,就会慢慢使用到这些东西了。

正在加载页面,请稍后...