Java集合框架介绍,包含List, Queue,Map,集合的遍历,Stream编程等

211 阅读19分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情


前言

本文将为大家介绍Java集合框架相关知识,首先为大家介绍Java集合框架体系,然后针对具体的常用集合接口(如 List, Queue,Map等)展开详细的介绍。

Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

一、集合框架体系

首先我们来了解一下Java中的集合框架体系。

下图是Java集合框架体系结构图:

image.png

java的集合框架就是给我们提供了一套更加方便的存储数据的类而已。

集合的目的是方便的存储和操作数据,其实说到底无非就是增删改查

Java中的常用接口:

List( 列表)线性表:

  • 和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

Set(表)也是线性表:

  • 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

Map(映射):

  • Map(映射)用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以是任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。
  • List,Set都是继承自Collection接口,Map则不是

下面也将会针对这些常用的接口进行详细的介绍,包含对它们的一些实现类的介绍。

二、List

接下来介绍一下Java中List的两个子类:ArrayList与LinkedList。

1.ArrayList

下面我们先来看看ArrayList的特点:

  • 有序可重复 ,底层是动态数组实现 —— 线程不安全
  • 添加数据,如果是基本数据类型会自动装箱为包装类,第一次添加数组长度为10
  • 存储的数据超过数组长度,会自动进行数组扩容,扩容为原来的1.5倍
  • 集合长度有限,最大容量默认为: Integer.MAX_VALUE-8
  • 为什么空8位出来: 1、存储Headerwords;2、避免一些机器内存溢出,减少出错几率,所以少分配;3、最大还是能支持到Integer.MAX_VALUE(2^31 -1)
 public class Test{
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        // 1、添加元素  ——  如果数据是基本数据类型会自动装箱为包装类
        list.add(1);
        list.add("a");
        list.add(true);
        list.add(10.1);
        list.add(2);
        // 2、删除元素 —— 根据下标删除
        list.remove(1);
        // 根据对象删除
        list.remove(new Integer(2));
        // 3、修改
        list.set(1,"b");
        // 4、遍历集合
        print(list);
    }
    public static void print(ArrayList list){
        System.out.println("-------- for循环 ----------");
        for (int i = 0; i < list.size() ; i++) {
            System.out.print(list.get(i) + "\t");
        }
        System.out.println();
        System.out.println("=========== 迭代器循环 =========");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.print(iterator.next() + "\t");
        }
        System.out.println();
    }
}

我们需要注意的是:

1、arraylist是基于数组实现的。

2、默认容量是10,每次扩容是1.5倍的扩容(oldCapacity + (oldCapacity >> 1))。

2.LinkedList

下面我们继续看看LinkedList的特点:

  • 底层是 双向链表 实现 —— 线程不安全
  • 第一次添加对象,把对象添加到Node节点中,falst指向第一个节点,不存在索引
  • 集合长度无限
//  创建 LinkedList 对象
LinkedList list = new LinkedList();
list.add("111"); //添加一个新元素,默认尾部添加
list.add("222");
list.add("333");
list.addFirst("444"); //在头部添加一个新元素
list.addLast("555"); //在尾部添加一个新元素
Object data1=list.removeFirst(); //移除头部元素并返回值
System.out.println(data1); 
Object data2=list.removeLast(); //移除尾部元素并返回值
System.out.println(data2);
list.getFirst(); // 获取头部元素并返回值
list.getLast(); // 获取尾部元素并返回值
for(Object object : list){
    System.out.println(object);
}
//  其他方法的使用与 ArrayList一致

3. ArrayList和LinkedList的对比

下面我们来对ArrayList和LinkedList从顺序添加元素,使用for循环迭代获取元素,使用迭代器迭代获取,头插元素,随机删除,以及它们自带的排序算法等方面进行对比。

(1)顺序添加

@Test
public void testArrayListAdd(){
    List<Integer> list = new ArrayList<>();
    Long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add((int)(Math.random()*100));
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    用时243毫秒。

@Test
public void testLinkedListAdd(){
    List<Integer> list = new LinkedList<>();
    Long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add((int)(Math.random()*100));
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    用时2524毫秒。

(2)使用for循环迭代获取

@Test
public void testArrayListFor(){
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10000000; i++) {
        list.add((int)(Math.random()*100));
    }
    System.out.println("开始------");
    Long start = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
        list.get(i);
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    用时2毫秒。

@Test
public void testLinkedListFor(){
    List<Integer> list = new LinkedList<>();
    for (int i = 0; i < 10000000; i++) {
        list.add((int)(Math.random()*100));
    }
    System.out.println("开始------");
    Long start = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
        list.get(i);
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    无法计算时间。

(3)使用迭代器迭代获取

@Test
public void testArrayListIterator(){
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10000000; i++) {
        list.add((int)(Math.random()*100));
    }
    System.out.println("开始------");
    Long start = System.currentTimeMillis();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()){
        iterator.next();
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    开始------
	用时4毫秒。

@Test
public void testLinkedListIterator(){
    List<Integer> list = new LinkedList<>();
    for (int i = 0; i < 10000000; i++) {
        list.add((int)(Math.random()*100));
    }
    System.out.println("开始------");
    Long start = System.currentTimeMillis();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()){
        iterator.next();
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}

结果:
    开始------
	用时42毫秒。

(4)头插

@Test
public void testArrayListAddHeader(){
    List<Integer> list = new ArrayList<>();
    Long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add(0,(int)(Math.random()*100));
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    无法算出,太慢

@Test
public void testLinkedListAddHeader(){
    List<Integer> list = new LinkedList<>();
    Long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
        list.add(0,(int)(Math.random()*100));
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    用时2487毫秒。

(5)随机删除

@Test
public void testLinkedListDel(){
    List<Integer> list = new LinkedList<>();
    for (int i = 0; i < 10000000; i++) {
        list.add(0,(int)(Math.random()*100));
    }
    Long start = System.currentTimeMillis();
    // 不用管为啥,这就是排序,复制过来用就行,写个冒泡也行
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()){
        if(iterator.next()>5000000){
            iterator.remove();
        }
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    用时45毫秒。

@Test
public void testArrayListDel(){
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10000000; i++) {
        list.add(0,(int)(Math.random()*100));
    }
    Long start = System.currentTimeMillis();
    // 不用管为啥,这就是排序,复制过来用就行,写个冒泡也行
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()){
        if(iterator.next()>5000000){
            iterator.remove();
        }
    }
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    太慢,时间没出来

(6)自带的排序方法

排序比较耗费资源,所以我们把量级调整到了十万。

@Test
public void testArrayListSort(){
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
        list.add(0,(int)(Math.random()*100));
    }
    Long start = System.currentTimeMillis();
    // 不用管为啥,这就是排序,复制过来用就行,写个冒泡也行
    list.sort(Comparator.comparingInt(num -> num));
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}
结果:
    用时49毫秒。

@Test
public void testLinkedListSort(){
    List<Integer> list = new LinkedList<>();
    for (int i = 0; i < 100000; i++) {
        list.add(0,(int)(Math.random()*100));
    }
    Long start = System.currentTimeMillis();
    // 不用管为啥,这就是排序,复制过来用就行,写个冒泡也行
    list.sort(Comparator.comparingInt(num -> num));
    Long end = System.currentTimeMillis();
    System.out.printf("用时%d毫秒。",end-start);
}

结果:
    用时53毫秒。

(7)总结

数组查询快,插入慢。链表插入慢,查询快。

  • 但是经过测试,尾插反而是数组快,而尾插的使用场景极多。

  • 测试了各种迭代,遍历方法,ArrayList基本都是比LinkedList要快。

  • 随机插入,链表会快很多,确实有一些特殊的场景LinkedList更合适,比如以后我们学的过滤器链。

  • 随机删除,链表的效率也是无比优于数组,如果我们存在需要过滤删除大量随机元素的场景也能使用linkedlist。

  • 我们工作中的使用还是以ArrayList为主,因为它的使用场景最多。

  • 数据结构的实现不同。 ArrayList是动态数组实现,LinkedList是双向链表实现

  • List item查询的效率。ArrayList有下标查询速度快,而LinkedList底层是链表结构,查询需从头开始依次往下查找

  • 增删的效率。在排除增删前后两头的情况下,LinkedList的速度比ArrayList的快,因为LinkedList的添加和删除节点,只要其他节点的前驱后继指引值发生变化,而ArrayList增加删除会更改数组的结构

4.Vector

  • 数组结构实现,查询快、增删慢。
  • JDK1.0版本,线程安全、运行效率比ArrayList较慢。
public class TestVector {
    public static void main(String[] args) {
        //创建集合
        Vector vector=new Vector<>();
        //1添加元素
        vector.add("草莓");
        vector.add("芒果");
        vector.add("西瓜");
        System.out.println("元素个数:"+vector.size());
        //2删除
//        vector.remove(0);
//        vector.remove("西瓜");
//        vector.clear();
        //3遍历
        //使用枚举器
        Enumeration en=vector.elements();
        while(en.hasMoreElements()) {
            String  o=(String)en.nextElement();
            System.out.println(o);
        }
        //4判断
        System.out.println(vector.contains("西瓜"));
        System.out.println(vector.isEmpty());
        //5vector其他方法
        //firsetElement、lastElement、elementAt();        
    }
}

5.Stack

  • 模拟 栈 的实现 —— 底层用数组存储数据
  • 特点是:先进后出
public class Demo05 {
    public static void main(String[] args) {
        Stack stack = new Stack();
        // 添加元素
        // Stack类的add方法与push方法区别:
        //         add方法返回的是boolean类型的值
        //         push方法返回的是当前添加的元素
        stack.push("a");
        stack.push("b");
        stack.add("c");
        stack.push("d");
        // 集合中的元素个数
        System.out.println(stack.size());
        // 弹出栈顶元素
        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }
}

三、Queue

接下来我们详细了解一下Queue这个接口,主要介绍对于Queue的使用。

1.队列的定义

以下是对于队列的一些相关概念的定义:

  • 队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
  • 队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
  • 队头(first):允许删除的一端,又称队首。
  • 队尾(Rearlsat:允许插入的一端。
  • 空队列:不包含任何元素的空表。

2.队列的使用

下面我们通过代码对队列的使用进行介绍,包括:向队列添加元素,查询队列元素的个数,查询头部元素,打印队列头部元素等。

public class Demo01 {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        queue.add("小明"); // 向队列添加元素,当队列满时,add方法抛出unchecked异常,而offer方法返回false。
        queue.offer("小黑");
        queue.offer("小蓝");
        queue.offer("小而");
        // 集合中的个数 
        System.out.println(queue.size()); // 4
        // 查询头部元素:如果队列为空,element方法会抛出异常,peek方法会返回null
        System.out.println(queue.element()); // 小明
        System.out.println(queue.peek()); // 小明
        // 打印队列头  
        System.out.println(queue.poll()); // 小明 ,如果队列为空,remove方法会抛出异常,poll方法会返回null。
        System.out.println(queue.size()); // 3
        System.out.println(queue.remove()); // 小黑
    }
}

四、Set

下面来介绍一下Set接口,其包含TreeSet与HashSet等实现类,都会逐一为大家进行介绍。

1.TreeSet

对于TreeSet的定义如下:

定义:

  • 可排序的集合;
  • 不能存储空对象;
  • 底层是红黑树;
  • 使用TreeSet 存储对象必须要使用自然排序或者比较器

自然排序

  • 要存储的对象必须实现了 Comparable 接口,并重写了 compareTo()
  • 在 compareTo() 方法中 制定了比较规则
  • 存储的对象 通过 与 之前的节点对比,返回正数则在 节点的右子树;负数则存储在左子数;返回 0 不存储对(重复了)
// 实体类实现接口
public class Person implements Comparable<Person> {
    private int id;
    private String name;
    private int age;
    /**
     * 重写CompereTo 接口
     *      要求:  id 从大到小
     *             id相同,age从小到大
     * @param o
     * @return
     */
    @Override
    public int compareTo(Person o) {
        if (this.id == o.getId()){
            return this.age-o.getAge() ;
        }
        return o.getId() - this.id ;
    }
}
// 测试
public class Demo02 {
    public static void main(String[] args) {
        TreeSet<Person> people = new TreeSet<>();
        people.add(new Person(1,"ls",44));
        people.add(new Person(1,"zs",12));
        people.add(new Person(8,"t7",14));
        people.add(new Person(7,"t7",14));
        people.forEach(item -> System.out.println(item));
    }
}

比较器

  • 在创建TreeSet集合的时候,通过构造方法传递实现了Comparator接口的实现类对象
  • 实现类对象要重写了compare()方法,在方法里面制定排序规则
// 比较器
public class MyCompartor implements Comparator<Person> {
    /**
     * id从小到大
     * id相同,age从大到小
     * @param o1 存储的对象
     * @param o2 上一个节点的对象
     * @return
     */
    @Override               // 11         88
    public int compare(Person o1, Person o2) {
        if (o1.getId() != o2.getId())
            return o1.getId()-o2.getId();
        return o2.getAge()-o1.getAge();
    }
}
// 测试
public class Demo03 {
    public static void main(String[] args) {
        // 通过构造方法传入 比较器
/*
        TreeSet<Person> people = new TreeSet<>(new MyCompartor());
        people.add(new Person(1,"ls",44));
        people.add(new Person(1,"zs",12));
        people.add(new Person(8,"t7",14));
        people.add(new Person(7,"t7",14));
        people.forEach(item -> System.out.println(item));
*/
        // 匿名内部类
        TreeSet<Person> people = new TreeSet<>(new Comparator<Person>(){
            @Override
            public int compare(Person o1, Person o2) {
                if (o1.getId() != o2.getId())
                    return o1.getId()-o2.getId();
                return o2.getAge()-o1.getAge();
            }
        });
        people.add(new Person(1,"ls",44));
        people.add(new Person(1,"zs",12));
        people.add(new Person(8,"t7",14));
        people.add(new Person(7,"t7",14));
        people.forEach(item -> System.out.println(item));
    }
}

下面我们对自然排序与比较器进行比较:

自然排序 VS 比较器

  • 比较器的优先级别高于自然排序
  • 自然排序需要存储的对象类结构发生改变(实现了 Comparable 接口,并重写compereTo方法)
  • 自然排序的可读性更高,并且排序规则不可见
  • 比较器会创建多一个类

2.HashSet

在介HashSet之前,我们需要了解一下有序与无序的概念。

什么是有序和无序?

  • 有序: 根据添加的顺序先后,遍历时以同样的顺序输出
  • 无序: 遍历输出 与 添加时的顺序无关(HashSet是无序的

版本:

  • JDK1.7 : 底层 hash表(不可变) + 链表
  • JDK1.8 : 底层 hash表(动态数组) + 链表 + 红黑树

重点:

  • 存储的对象通过hashCode()得到hash值,根据hash值找到在hash表中对应的位置
  • 再根据equals() 判断是否内容相等,内容相等则不存储
  • 当hash表中的对象超过容量的负载因子数时,进行数组扩容,容量为原来的2倍
  • 当一个链表中的对象超过8个,则会转换为红黑树
public class Demo02 {
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        hashSet.add(12);
        hashSet.add(11);
        hashSet.add(22);
        hashSet.add(22);
        // lambda表达式 ,打印结果
        hashSet.forEach(item -> System.out.println(item));
        // 输出:22 11 12 ,说明HashSet是无序的
    }
}

五、Map

1.HashMap

hashmap的实现是比较复杂的。

image.png 我们先看一张图,了解一下hashmap的存储结构:

  • 第一步:hashmap构造时(其实不是构造的时候)会创建一个长度为16数组,名字叫table,也叫hash表;

  • 第二步:hashmap在插入数据的时候,首先根据key计算hashcode,然后根据hashcode选择一个槽位。假设hashmap使用取余的方式计算。(事实上,hashmap不是)我们都知道hashcode会返回一个int值,使用int值除以16取余就能得到一个0~15的数字,就能去定一个具体的槽位。

  • 第三步:确定了具体的槽位之后,我们就会封装一个node(节点),里边保存了hash,key,value等数据存入这个槽中。

  • 第四步:当存入新的数据的时候,使用新的hash计算的槽位发现已经有了数据,这个现象叫做hash碰撞,会以链表的形式存储。

  • 第五步:当链表的个数超过了8个,链表开始树化,变成一个红黑树。

特点:

  • 无序,key不可重复

简单使用:

对于HashMap的使用时,存储元素时会遵循以下规则:

  • key,value都可以为null, key为null的时候存储在HashMap的第一个位置
  • 当key相同时,新的value会覆盖旧的value
public class Demo01 {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        // 1、添加
        map.put("a",13);
        map.put("b",12);
        map.put("c",11);
        map.put("d",678);
        map.put("a",99);  // key重复,后面添加的会覆盖前面的
        System.out.println(map); //{a=99, b=12, c=11, d=678}
        // 2、删除
        // 根据key删除
        map.remove("a");
        // 根据key value 删除
        map.remove("c",11);
        // 3、修改
        // 根据key 修改value
        map.replace("b",88);
        // 4、打印
        System.out.println(map); //{b=88, d=678}
        // 5、遍历 for不可用
        System.out.println("----------- 使用keyset方法转换为保存key值的set集合 ------------");
        // 将 map 中的key值存储到 set集合中,set能调用的遍历方法他也能
        Set<String> set1 = map.keySet();
        Iterator<String> iterator1 = set1.iterator();
        while (iterator1.hasNext()){
            String key = iterator1.next();
            System.out.println("key:"+key +",value:"+map.get(key));
        }
        System.out.println("----------- 使用entrySet方法转换为保存key,value 的Set集合 ------------");
        // 将key,value 保存再Map类的内部类 Entry 类中
        Set<Map.Entry<String, Integer>> set2 = map.entrySet();
        Iterator<Map.Entry<String, Integer>> iterator2 = set2.iterator();
        while (iterator2.hasNext()){
            Map.Entry<String, Integer> entry = iterator2.next();
            System.out.println("key:"+entry.getKey() + ",value:"+entry.getValue());
        }
    }
}

2.TreeMap

特点:

  • 底层是红黑树 ,无序 key不可重复 简单使用:

对于TreeMap的使用时,存储元素时会遵循以下规则:

  • 存储映射的关系过程中,需要key使用 自然排序或传递比较器(比较器根据key进行排序)
  • key不能为null ,value可以为null
  • 添加相同 key 时,新的value会覆盖旧的value(底层:自然排序的compareTo() 或比较器compare() 方法返回值为0,调用t.setValue(value),实现新的值覆盖旧的值)
public class Demo02 {
    public static void main(String[] args) {
        System.out.println("--------------TreeMap-----------------");
        TreeMap<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("a",1);
        treeMap.put("a",2);
        // treeMap.put(null,1);
        treeMap.put("b",null);
        System.out.println(treeMap);
        //{a=2, b=null}
        System.out.println("--------------HashMap-----------------");
        HashMap<String, Integer> hashMap = new HashMap<>();
        hashMap.put("a",11);
        hashMap.put(null,11);
        hashMap.put("b",null);
        System.out.println(hashMap);
        //{null=11, a=11, b=null}
    }
}

3.Hashtable

特点:

  • 无序,key不可重复

简单使用:

  • key,value都不能为空

以下代码展示了向Hashtable中添加元素的操作:

public class Demo03 {
    public static void main(String[] args) {
        Hashtable<String, Integer> hashtable = new Hashtable<>();
        hashtable.put("a",1);
        hashtable.put("c",2);
        //hashtable.put(null,2);    NullPointerException
        //hashtable.put("d",null);  NullPointerException
        System.out.println(hashtable);
    }
}

六.集合工具类Collections

Collections是一个工具类,它给我们提供了一些常用的好用的操作集合的方法。

Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象实现同步控制等方法。

排序操作:排序相关的、顺序相关的功能

  • reverse():反转指定列表中元素的顺序
  • shuffle():使用默认随机源对指定列表进行置换(每一次的顺序都不一样)或者打乱顺序 :打乱集合顺序
  • sort():根据指定比较器产生的顺序对指定列表进行排序(默认从小到大)或者在指定列表的指定位置处交换元素。注意:sort(list)有使用前提:被排序的集合元素必须实现了Comparable接口,重写接口中的compareTo方法定义排序的规则;
  • swap():交换一下顺序 rotate():根据指定的距离轮换指定列表中的元素

查找和替换操作

  • max():根据元素的自然顺序,返回给定 collection 的最大元素
  • min():根据元素的自然顺序,返回给定 collection 的最小元素
  • replaceAll():使用另一个值替换列表中出现的所有某一指定值 frequency()返回指定 collection中等于指定对象的元素数
  • binarySearch():使用二分搜索法搜索指定列表,以获得指定对象。
  • indexOfSubList():查找子列表在列表中第一次出现的位置,没有返回-1
  • lastIndexOfSubList():查找子列表在列表中最后一次出现的位置,没有返回-1
  • fill():使用指定元素替换指定列表中的所有元素

同步控制

  • Collections类中提供了多个synchronized…()方法,这些方法可以将指定集合包装成线程同步(线程安全)的集合,从而可以解决多线程并发访问集合时的线程安全问题。
  • Java中常用的集合框架中的实现类 ArrayList、Linkedlist、 HashSet、TreeSet、HashMap和TreeMap 都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。
  • Collections提供了多个类方法可以把它们包装成线程同步的集合。

代码示例:

public class CollectionsTest {
    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        list1.add(9);
        list1.add(2);
        list1.add(2);
        Integer[] arr = {7,6,9};
// addAll(Collection<? super T> c, T... elements) 将所有指定的元素添加到指定的集合。
        Collections.addAll(list1,4,5,6);
        Collections.addAll(list1,arr);
        // 打印集合
        System.out.println(list1);//[9, 2, 2, 4, 5, 6, 7, 6, 9]
        // 对集合元素进行排序
        Collections.sort(list1);
        System.out.println(list1);//[2, 2, 4, 5, 6, 6, 7, 9, 9]
    }
}

七、集合的遍历

1、普通for循环

能够使用普通for循环的前提是必须可以通过下标获取数据,List天然满足这个特性。

public class ListTest {
    public static void main(String[] args) {
        public List<String> names;
        names = new ArrayList<>();
        names.add("lucy");
        names.add("tom");
        names.add("jerry");
        for (int i = 0; i < names.size(); i++) {
            System.out.println(names.get(i));
        }
    }
}

同理:

我们将 names = new ArrayList<>();改为 names = new LinkedList<>();也是可以的。

思考问题:

hashmap和hashset怎么进行遍历?它们没有下标啊。

这里就必须使用迭代器了。

2、迭代器

(1)迭代器介绍

迭代器其实是一种思想。

先看一下迭代器这个接口:

public interface Iterator<E> {
    // 是不是有下一个
    boolean hasNext();
    // 拿到下一个
    E next();
    // 你可以继承重写这个方法,否则将抛出异常
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

例如:

image.png

小丽拿了一篮子苹果,你想把小丽的苹果分给大家吃。

  • 我:小丽,篮子里还有吗?
  • 小丽: 有呢。 hasNext()
  • 我:给我。
  • 小丽:好呢。 next()
  • 小丽:哎,这个坏了,我扔了吧! remove()

其实小丽就是我们所说的迭代器。

(2)迭代器的使用

还是上边的例子:

@Test
public void testIterator(){
    Iterator<String> iterator = names.iterator();
    // 每次都判断一下是不是有下一个,有的话,继续遍历
    while (iterator.hasNext()){
        // 获取下一个
        String name = iterator.next();
        System.out.println(name);
    }
}

当然换成LinkedList也是可行的。

看看hashSet,居然也行

/**
 * @author itnanls
 * @date 2021/7/16
 **/
public class SetTest {

    public Set<String> names;

    @Before
    public void add() {
        names = new HashSet<>();
        names.add("lucy");
        names.add("tom");
        names.add("jerry");
    }

    @Test
    public void testIterator(){
        Iterator<String> iterator = names.iterator();
        // 每次都判断一下是不是有下一个,有的话,继续遍历
        while (iterator.hasNext()){
            // 获取下一个
            String name = iterator.next();
            System.out.println(name);
        }
    }
}

再看看hashmap,也是可以的

public class MapTest {

    public Map<String,String> user;

    @Before
    public void add() {
        user = new HashMap<>();
        user.put("username","ydlclass");
        user.put("password","ydl666888");
    }

    @Test
    public void testIterator(){
        // 拿到一个存有所有entry的set集合。
        // entry就是一个个的节点node
        Set<Map.Entry<String, String>> entries = user.entrySet();
        
        Iterator<Map.Entry<String, String>> iterator = entries.iterator();
        while (iterator.hasNext()){
            Map.Entry<String, String> next = iterator.next();
            System.out.println(next.getKey());
            System.out.println(next.getValue());
        }
    }
}

也可以先获取一个key的set即可,再用迭代器进行遍历。

这种方式相当于遍历了两次,效率低。

@Test
public void testIterator2(){
    // 获取一个含有所有keyset集合,去迭代
    Set<String> keys = user.keySet();
    Iterator<String> iterator = keys.iterator();

    while (iterator.hasNext()){
        String key = iterator.next();
        System.out.println(key);
        System.out.println(user.get(key));
    }
}

千万别以为迭代器牛逼的不行,其实迭代器只是个接口,每个对象都要有对应的实现。

简单的看一下arraylist的实现

private class Itr implements Iterator<E> {

        // 只要有标没到最后一个就行
        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
                // 大概率就是使用游标控制下一个的位置
                int i = cursor;
                // 其实就是返回了下一个
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                // 直接把当前的删除就行了
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

3、增强for循环

Java提供了一种语法糖(用起来甜甜的,很简单)去帮助我们遍历,叫增强for循环:

List、Set都可以使用这种方式进行遍历:

@Test
public void testEnhancedFor(){
    for (String name : names){
        System.out.println(name);
    }
}

Map使用这样的写法:

@Test
public void testEnhancedFor(){
    for (Map.Entry<String,String> entry : user.entrySet()){
        System.out.println(entry.getKey());
        System.out.println(entry.getValue());
    }
}

增强for循环其实也是使用了迭代器。我们可以在ArrayList中的迭代器中打一个断点,debug运行一下即可。

增强for循环只是一种语法糖,用起来甜甜的简单而已。

image.png

4、迭代中删除元素

有同一个题目:我想把下边的集合中的lucy全部删除?

public void add() {
    List<String> names = new ArrayList<>();
    names.add("tom");
    names.add("lucy");
    names.add("lucy");
    names.add("lucy");
    names.add("jerry");
}

(1)for循环中删除

public void testDelByFor(){
    for (int i = 0; i < names.size(); i++) {
        if("lucy".equals(names.get(i))){
            names.remove(i);
        }
    }
    System.out.println(names);
}

结果:
[tom, lucy, jerry]

我们发现并没有删除干净,中间的lucy好像被遗忘了。

image.png

合适的解决方式有两种:

第一种:回调指针

for (int i = 0; i < names.size(); i++) {
    if("lucy".equals(names.get(i))){
        names.remove(i);
        // 回调指针:
        i--;
    }
}
System.out.println(names);


结果:
[tom, jerry]  

第二种:逆序遍历

for (int i = names.size()-1; i > 0; i--) {
    if("lucy".equals(names.get(i))){
        names.remove(i);
    }
}
System.out.println(names);

结果:
[tom, jerry] 

但是最好的删除方法是使用迭代器。

(2)使用迭代器删除元素

public static void main(String[] args){
    Iterator<String> iterator = names.iterator();
    while (iterator.hasNext()){
        // 记住next(),只能调用一次,因为每次调用都会选择下一个
        String name = iterator.next();
        if("lucy".equals(name)){
            iterator.remove();
        }
    }
    System.out.println(names);
}

八、Stream编程

​ Java8中的Stream是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的 聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API借助于同样新出现的Lambda表达式,极大的提高编程效率和程序可读性。同时,它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势。通常,编写并行代码很难而且容易出错, 但使用Stream API无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。

​ 我觉得我们可以将流看做流水线,这个流水线是处理数据的流水线,一个产品经过流水线会有一道道的工序就如同对数据的中间操作,比如过滤我不需要的,给数据排序能,最后的终止操作就是产品从流水线下来,我们就可以统一打包放入仓库了。

​ 当我们使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换 → 执行操作获取想要的结果。每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换) ,这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示:

Stream有几个特性:

  1. Stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  2. Stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
  3. Stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

image-20210908180935125

1、Stream流的创建

(1)Stream可以通过集合数组创建。

1、通过 java.util.Collection.stream() 方法用集合创建流,我们发现

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();

(2)使用java.util.Arrays.stream(T[] array)方法用数组创建流

int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);

(3)使用Stream的静态方法:of()、iterate()、generate()

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);

Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);

2、Stream的终止操作

为了方便我们后续的使用,我们先初始化一部分数据:


public class Person {
    private String name;  // 姓名
    private int salary; // 薪资
    private int age; // 年龄
    private String sex; //性别
    private String area;  // 地区

    public Person() {
    }

    public Person(String name, int salary, int age, String sex, String area) {
        this.name = name;
        this.salary = salary;
        this.age = age;
        this.sex = sex;
        this.area = area;
    }
}

初始化数据,我们设计一个简单的集合和一个复杂的集合。

public class LambdaTest {

    List<Person> personList = new ArrayList<Person>();
    List<Integer> simpleList = Arrays.asList(15, 22, 9, 11, 33, 52, 14);

    @Before
    public void initData(){
        personList.add(new Person("张三",3000,23,"男","太原"));
        personList.add(new Person("李四",7000,34,"男","西安"));
        personList.add(new Person("王五",5200,22,"女","太原"));
        personList.add(new Person("小黑",1500,33,"女","上海"));
        personList.add(new Person("狗子",8000,44,"女","北京"));
        personList.add(new Person("铁蛋",6200,36,"女","南京"));
    }
}

(1)遍历/匹配(foreach/find/match)

将数据流消费掉

@Test
public void foreachTest(){
    // 打印集合的元素
    simpleList.stream().forEach(System.out::println);
    // 其实可以简化操作的
    simpleList.forEach(System.out::println);
}


@Test
public void findTest(){
    // 找到第一个
    Optional<Integer> first = simpleList.stream().findFirst();
    // 随便找一个,可以看到findAny()操作,返回的元素是不确定的,
    // 对于同一个列表多次调用findAny()有可能会返回不同的值。
    // 使用findAny()是为了更高效的性能。如果是数据较少,串行地情况下,一般会返回第一个结果,
    // 如果是并行的情况,那就不能确保是第一个。
    Optional<Integer> any = simpleList.parallelStream().findAny();
    System.out.println("first = " + first.get());
    System.out.println("any = " + any.get());
}

@Test
public void matchTest(){
    // 判断有没有任意一个人年龄大于35岁
    boolean flag = personList.stream().anyMatch(item -> item.getAge() > 35);
    System.out.println("flag = " + flag);

    // 判断是不是所有人年龄都大于35岁
    flag = personList.stream().allMatch(item -> item.getAge() > 35);
    System.out.println("flag = " + flag);
}

(2)归集(toList/toSet/toMap)

因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toListtoSettoMap比较常用。

下面用一个案例演示toListtoSettoMap

@Test
public void collectTest(){
    // 判断有没有任意一个人年龄大于35岁
    List<Integer> collect = simpleList.stream().collect(Collectors.toList());
    System.out.println(collect);
    Set<Integer> collectSet = simpleList.stream().collect(Collectors.toSet());
    System.out.println(collectSet);
    Map<Integer,Integer> collectMap = simpleList.stream().collect(Collectors.toMap(item->item,item->item+1));
    System.out.println(collectMap);
}

(3) 统计(count/averaging/sum/max/min)

@Test
public void countTest(){
    // 判断有没有任意一个人年龄大于35岁
    long count = new Random().ints().limit(50).count();
    System.out.println("count = " + count);
    OptionalDouble average = new Random().ints().limit(50).average();
    average.ifPresent(System.out::println);
    int sum = new Random().ints().limit(50).sum();
    System.out.println(sum);
}

案例:获取员工工资最高的人

Optional<Person> max = personList.stream().max((p1, p2) -> p1.getSalary() - p2.getSalary());
max.ifPresent(item -> System.out.println(item.getSalary()));

里边的比较器可以改为:Comparator.comparingInt(Person::getSalary)

(4)归约(reduce)

归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。

案例:求Integer集合的元素之乘积。

@Test
public void reduceTest(){
    Integer result = simpleList.stream().reduce(1,(n1, n2) -> n1*n2);
    System.out.println(result);
}

(5)接合(joining)

joining可以将Stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。

@Test
public void joiningTest(){
		List<String> list = Arrays.asList("A", "B", "C");
		String string = list.stream().collect(Collectors.joining("-"));
		System.out.println("拼接后的字符串:" + string);
	}
}

(6)分组(partitioningBy/groupingBy)

  • 分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。
  • 分组:将集合分为多个Map,比如员工按性别分组。

image-20210908182643127

案例:将员工按薪资是否高于8000分为两部分;将员工按性别和地区分组

@Test
public void groupingByTest(){
    // 将员工按薪资是否高于8000分组
    Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
    // 将员工按性别分组
    Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex));
    // 将员工先按性别分组,再按地区分组
    Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
    System.out.println("员工按薪资是否大于8000分组情况:" + part);
    System.out.println("员工按性别分组情况:" + group);
    System.out.println("员工按性别、地区:" + group2);
}

3、Stream中间操作

(1)筛选(filter)

该操作符需要传入一个function函数

筛选出simpleList集合中大于17的元素,并打印出来

simpleList.stream().filter(item -> item > 17).forEach(System.out::println);

筛选员工中工资高于8000的人,并形成新的集合。

List<Person> collect = personList.stream().filter(item -> item.getSalary() > 8000).collect(Collectors.toList());
System.out.println("collect = " + collect);

(2)映射(map/flatMap)

映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为mapflatMap

  • map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

案例:将员工的薪资全部增加1000。

personList.stream().map(item -> {
    item.setSalary(item.getSalary()+1000);
    return item;
}).forEach(System.out::println);

将simpleList转化为字符串list

List<String> collect = simpleList.stream().map(num -> Integer.toString(num))
                .collect(Collectors.toList());

(3)排序(sorted)

sorted,中间操作。有两种排序:

  • sorted():自然排序,流中元素需实现Comparable接口
  • sorted(Comparator com):Comparator排序器自定义排序

案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序

@Test
public void sortTest(){
    // 按工资升序排序(自然排序)
    List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
        .collect(Collectors.toList());
    // 按工资倒序排序
    List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
        .map(Person::getName).collect(Collectors.toList());
    // 先按工资再按年龄升序排序
    List<String> newList3 = personList.stream()
        .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName)
        .collect(Collectors.toList());
    // 先按工资再按年龄自定义排序(降序)
    List<String> newList4 = personList.stream().sorted((p1, p2) -> {
        if (p1.getSalary() == p2.getSalary()) {
            return p2.getAge() - p1.getAge();
        } else {
            return p2.getSalary() - p1.getSalary();
        }
    }).map(Person::getName).collect(Collectors.toList());

    System.out.println("按工资升序排序:" + newList);
    System.out.println("按工资降序排序:" + newList2);
    System.out.println("先按工资再按年龄升序排序:" + newList3);
    System.out.println("先按工资再按年龄自定义降序排序:" + newList4);
}

(4)peek操作

peek的调试作用

@Test
public void peekTest(){
    // 在stream中间进行调试,因为stream不支持debug
    List<Person> collect = personList.stream().filter(p -> p.getSalary() > 5000)
        .peek(System.out::println).collect(Collectors.toList());
    // 修改元素的信息,给每个员工涨工资一千
    personList.stream().peek(p -> p.setSalary(p.getSalary() + 1000))
        .forEach(System.out::println);
}

(5)其他操作

流也可以进行合并、去重、限制、跳过等操作。

@Test
public void otherTest(){
    // distinct去掉重复数据   
    // skip跳过几个数据
    // limit限制使用几个数据
    simpleList.stream().distinct().skip(2).limit(3).forEach(System.out::println);
}

//  11,11,22,22,11,23,43,55,78
//  去重  11,22,23,43,55,78
//  掉过两个  23,43,55,78
// 使用3个    23,43,55

后记

以上呢就是为大家介绍的关于Java集合框架的知识,首先为大家简单介绍了Java集合框架体系,然后针对具体的常用集合接口(如 List, Queue,Map等)展开详细的介绍,还有集合工具类Collections(包含其中的一系列常用的操作集合的方法),集合的遍历(包含普通for循环,迭代器,增强for循环,迭代中删除元素),还有Stream编程(Stream流的创建,Stream的终止操作,Stream中间操作)。

希望本文的内容能够使你有所收获,如果你想继续深入的学习数据结构与算法相关的知识,或想深入的学习Java相关的知识与技术,可以参考:

Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~