Java集合框架全面解析

Java集合框架可以分为两条大的支线:

①、Collection,主要由 List、Set、Queue 组成:

List代表有序、可重复的集合,典型代表就是封装了动态数组的ArrayList和封装了链表的Linked List。

Set代表了无序、不可重复的集合,典型代表是HashSet和TreeSet;

Queue代表队列,典型代表是双端队列ArrayDeque,以及优先级队列PriorityQueue。

②、Map,代表键值对的集合,典型代表就是 HashMap

01.List

List的特点是存取有序,可以存放重复的元素,可以用下标对元素进行操作。

1)ArrayList

增删改查:

复制代码
// 创建一个集合
        ArrayList<String> list = new ArrayList<String>();
// 添加元素
        list.add("董李阳");
        list.add("沉默");
        list.add("陈清扬");

// 遍历集合 for 循环
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
// 遍历集合 for each
        for (String s : list) {
            System.out.println(s);
        }

// 删除元素
        list.remove(1);
// 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

// 修改元素
        list.set(1, "董李阳plus");
// 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

结果:

简单介绍下ArrayList的特征。

  • ArrayList 是由数组实现的,支持随机存取,也就是可以通过下标直接存取元素;
  • 从尾部插入和删除元素会比较快捷,从中间插入和删除元素会比较低效,因为涉及到数组元素的复制和移动;
  • 如果内部数组的容量不足时会自动扩容,因此当元素非常庞大的时候,效率会比较低。

2)LinkedList

增删改查:

复制代码
// 创建一个集合
        LinkedList<String> list = new LinkedList<String>();
// 添加元素
        list.add("董李阳");
        list.add("沉默");
        list.add("陈清扬");

// 遍历集合 for 循环
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
// 遍历集合 for each
        for (String s : list) {
            System.out.println(s);
        }

// 删除元素
        list.remove(1);
// 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

// 修改元素
        list.set(1, "董李阳plus");
// 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

结果:

介绍下LinkedList:

  • LinkedList 是由双向链表实现的,不支持随机存取,只能从一端开始遍历,直到找到需要的元素后返回;
  • 任意位置插入和删除元素都很方便,因为只需要改变前一个节点和后一个节点的引用即可,不像 ArrayList 那样需要复制和移动数组元素;
  • 因为每个元素都存储了前一个和后一个节点的引用,所以相对来说,占用的内存空间会比 ArrayList 多一些。

3)Vector和Stack

List的实现类还有一个Vector,是一个元老级的类,比ArrayList出现得更早。ArrayList和Vector非常相似,只不过Vector是线程安全的,像 get、set、add 这些方法都加了 synchronized 关键字,就导致执行效率会比较低,所以现在已经很少用了。

来看看add方法的源码就明白了:

复制代码
    /**
     * Appends the specified element to the end of this Vector.
     *
     * @param e element to be appended to this Vector
     * @return {@code true} (as specified by {@link Collection#add})
     * @since 1.2
     */
    public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
    }

翻译:

将指定元素附加到Vector的末尾。

这种加了同步方法的类,注定会被淘汰掉,就像StringBuilder取代StringBuffer那样。JDK源码也说了: 如果不需要线程安全,建议使用ArrayList代替Vector。

Stack是Vector的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(再get、set、add方法的基础上追加了pop【返回并移除栈顶的元素】、peek【只返回栈顶元素】等方法),所以叫栈。

并且有趣的是:peek:有瞥一眼的意思。就像是打开栈的盖子,看了最上面的。

下面是方法的源码,增删改查省略。

复制代码
    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }
    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }
    public E push(E item) {
        addElement(item);

        return item;
    }

不过由于Stack执行效率低(从代码可以看到加了synchronized关键字),就被双端队列ArrayDeque取代了。

02、Set

Set的特点是存取无序,不可以存放重复的元素,不可以用下标对元素进行操作(说明不是基于数组的),和List有很多不同。

1)HashSet

HashSet其实是由HashMap实现的,只不过值由一个固定的Object对象填充,而键用于操作。来简单看一下它的源码。

复制代码
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    @java.io.Serial
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
//省略:还有很多的其他的。
}

实际开发中,HashSet并不常用,比如,如果我们需要按照顺序存储一组元素,那么ArrayList和LinkedList可能更适合;如果我们需要存储键值对并根据键值查找,那么HashMap更合适。

来段增删改查体验:

复制代码
// 创建一个新的HashSet
        HashSet<String> set = new HashSet<>();

// 添加元素
        set.add("董李阳");
        set.add("王二");
        set.add("陈清扬");

// 输出HashSet的元素个数
        System.out.println("HashSet size: " + set.size()); // output: 3

// 判断元素是否存在于HashSet中
        boolean containsWanger = set.contains("王二");
        System.out.println("Does set contain '王二'? " + containsWanger); // output: true

// 删除元素
        boolean removeWanger = set.remove("王二");
        System.out.println("Removed '王二'? " + removeWanger); // output: true

// 修改元素,需要先删除后添加
        boolean removeChenmo = set.remove("董李阳");
        boolean addBuChenmo = set.add("不是董李阳");
        System.out.println("Modified set? " + (removeChenmo && addBuChenmo)); // output: true

// 输出修改后的HashSet
        System.out.println("HashSet after modification: " + set); // output: [陈清扬, 不是董李阳]

结果:

HashSet主要是用于去重,比如,我们需要统计一篇文章中有多少个不重复的单词,就可以使用HashSet来实现。

复制代码
// 创建一个 HashSet 对象
        HashSet<String> set = new HashSet<>();

// 添加元素
        set.add("董李阳");
        set.add("王二");
        set.add("陈清扬");
        set.add("董李阳");
        set.add("王二");


// 输出 HashSet 的元素个数
        System.out.println("HashSet size: " + set.size()); // output: 3

// 遍历 HashSet
        for (String s : set) {
            System.out.println(s);
        }

结果:

从上面的例子可以看出,HashSet会自动去重,因为他是用HashMap实现的,HashMap的键是唯一的(哈希值),相同的键的值会覆盖掉原来的值,于是第二次时候,就会覆盖掉第一次。

2)LinkedHashSet

LinkedHashSet虽然继承自HashSet,其实是由LinkedHashMap实现的。

这里是LinkedHashSet的无参构造方法:

复制代码
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    public LinkedHashSet() {
        super(16, .75f, true);
    }
//其余省略。
}

super 的意思是它将调用父类的HashSet的一个有参构造方法:

复制代码
 public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{   
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
//其余省略
}

从这里找到了LinkedHashMap。

来一段LinkedHashMap的增删改查

复制代码
        LinkedHashSet<String> set = new LinkedHashSet<>();

// 添加元素
        set.add("董李阳");
        set.add("王二");
        set.add("陈清扬");

// 删除元素
        set.remove("王二");

// 修改元素
        set.remove("董李阳");
        set.add("董李阳plus");

// 查找元素
        boolean hasChenQingYang = set.contains("陈清扬");
        System.out.println("set包含陈清扬吗?" + hasChenQingYang);

看看结果:

LinkedHashSet是一种基于哈希表实现的Set接口,它继承自HashSet,并且使用链表维护了元素的插入顺序。因此,它既有HashSet的快速查找、插入和删除操作的有点,又可以维护元素的插入顺序。

3)TreeSet

与TreeMap相似,TreeSet是一种基于红黑树实现的有序集合,它实现了SortedSet接口,可以自动对集合中的元素进行排序。按照键的自然顺序或指定的比较器顺序进行排序。

增删改查:

复制代码
// 创建一个 TreeSet 对象
        TreeSet<String> set = new TreeSet<>();

// 添加元素
        set.add("董李阳");
        set.add("王二");
        set.add("陈清扬");
        System.out.println(set); // 输出 [沉默, 王二, 陈清扬]

// 删除元素
        set.remove("王二");
        System.out.println(set); // 输出 [沉默, 陈清扬]

// 修改元素:TreeSet 中的元素不支持直接修改,需要先删除再添加
        set.remove("陈清扬");
        set.add("陈青阳");
        System.out.println(set); // 输出 [沉默, 陈青阳]

// 查找元素
        System.out.println(set.contains("董李阳")); // 输出 true
        System.out.println(set.contains("王二")); // 输出 false

看看结果:

需要注意的是,TreeSet不允许null元素,否则会抛出NullPotinterException异常。

总体来说,Set集合不是关注的重点,因为底层都是由Map实现的,为什么用Map实现呢?

Map的键不允许重复、无序。

03)、Queue

Queue,也就是队列,通常遵循先进先出(FIFO)的原则,新元素插入到队列的尾部,访问元素返回队列的头部。

1)ArrayDeque

从名字可以看出,ArrayDeque是一个基于数组实现的双端队列,为了满足可以同时再数组两端插入或删除元素的需求,数组必须是循环的,也就是说,数组的任何一点都可以看作是起点(终点)。

head指向队首的第一个有效的元素,tail指向队尾第一个可以插入元素的空位,因为是循环数组,所以head不一定是从0开始,tail也不一定总是比head大。

来一段ArrayDeque的增删改查:

复制代码
// 创建一个ArrayDeque
        ArrayDeque<String> deque = new ArrayDeque<>();

// 添加元素
        deque.add("董李阳");
        deque.add("王二");
        deque.add("陈清扬");

// 删除元素
        deque.remove("王二");

// 修改元素
        deque.remove("董李阳");
        deque.add("董李阳plus");

// 查找元素
        boolean hasChenQingYang = deque.contains("陈清扬");
        System.out.println("deque包含陈清扬吗?" + hasChenQingYang);
    }

看看结果:

2)LinkedList

LinkedList一般应该归再List下,只不过,他也实现了Deque接口,可以作为队列来使用。等于说,LinkedLIst同时实现了Stack、Queue、PriorityQueue的所有功能。

复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//省略
}

换句话说,LinkedList和ArrayDeque都是Java集合框架中的双向队列(deque),它们都支持再队列的两端进行元素的插入和删除操作。不过,LinkedList和ArrayDeque在实现上有一些不同:

底层实现方式不同:LinkedList是基于链表实现的,而ArrayDeque是基于数组实现的。

随机访问的效率不同:由于底层实现方式的不同,LinkedList对于随机访问的效率较低,时间复杂度为O(n),而ArrayDeque可以通过下标随机访问元素,时间复杂度为O(1)。

迭代器的效率不同:LinkedList对于迭代器的效率比较低,因为需要通过链表进行遍历,时间复杂度为O(n),而 ArrayDeque 的迭代器效率比较高,因为可以直接访问数组中的元素,时间复杂度为 O(1)。

内存占用不同:由于LinkedList是基于链表实现的,它在存储元素时候需要额外的空间来存储链表节点,因此内存占用相对较高,而ArrayDeque是基于数组实现的,内存占用相对较低。

因此,在选择使用 LinkedList 还是 ArrayDeque 时,需要根据具体的业务场景和需求来选择。如果需要在双向队列的两端进行频繁的插入和删除操作,并且需要随机访问元素,可以考虑使用 ArrayDeque;如果需要在队列中间进行频繁的插入和删除操作,可以考虑使用 LinkedList。

来一段LinkedList作为队列时候的增删改查,注意和它作为List时候又很大的不同。

复制代码
       // 创建一个 LinkedList 对象
        LinkedList<String> queue = new LinkedList<>();

// 添加元素
        queue.offer("董李阳");
        queue.offer("王二");
        queue.offer("陈清扬");
        System.out.println(queue); // 输出 [沉默, 王二, 陈清扬]

// 删除元素
        queue.poll();
        System.out.println(queue); // 输出 [王二, 陈清扬]

// 修改元素:LinkedList 中的元素不支持直接修改,需要先删除再添加
        String first = queue.poll();
        queue.offer("王大二");
        System.out.println(queue); // 输出 [陈清扬, 王大二]

// 查找元素:LinkedList 中的元素可以使用 get() 方法进行查找
        System.out.println(queue.get(0)); // 输出 陈清扬
        System.out.println(queue.contains("董李阳")); // 输出 false

// 查找元素:使用迭代器的方式查找陈清扬
// 使用迭代器依次遍历元素并查找
        Iterator<String> iterator = queue.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("陈清扬")) {
                System.out.println("找到了:" + element);
                break;
            }
        }

看看结果:

在使用LinkedList作为队列时,可以使用offer()方法将元素添加到队列的末尾,使用poll()方法从队列的头部删除元素。另外,由于LinkedList是链表结构,不支持随机访问元素,因此不能使用下标访问元素,需要使用迭代器或者poll()方法依次遍历元素。

3)PriorityQueue

PriorityQueue是一种优先级队列,它的出队顺序与元素的优先级有关,执行remove或者poll方法,返回的总是优先级最高的元素。

复制代码
// 创建一个 PriorityQueue 对象
        PriorityQueue<String> queue = new PriorityQueue<>();

// 添加元素
        queue.offer("董李阳");
        queue.offer("王二");
        queue.offer("陈清扬");
        System.out.println(queue); // 输出 [董李阳, 王二, 陈清扬]

// 删除元素
        queue.poll();
        System.out.println(queue); // 输出 [王二, 陈清扬]

// 修改元素:PriorityQueue 不支持直接修改元素,需要先删除再添加
        String first = queue.poll();
        queue.offer("张三");
        System.out.println(queue); // 输出 [张三, 陈清扬]

// 查找元素:PriorityQueue 不支持随机访问元素,只能访问队首元素
        System.out.println(queue.peek()); // 输出 张三
        System.out.println(queue.contains("陈清扬")); // 输出 true

// 通过 for 循环的方式查找陈清扬
        for (String element : queue) {
            if (element.equals("陈清扬")) {
                System.out.println("找到了:" + element);
                break;
            }
        }

看看结果吧:

要想有优先级,元素就需要实现Comparable接口或者Comparator接口。

这里先来一段通过实现Comparator接口,按照年龄姓名排序的优先级队列吧。

复制代码
package com.example;


import java.util.*;

class Student {
    private String name;
    private int chineseScore;
    private int mathScore;
    public Student(String name, int chineseScore, int mathScore) {
        this.name = name;
        this.chineseScore = chineseScore;
        this.mathScore = mathScore;
    }
    public String getName() {
        return name;
    }
    public int getChineseScore() {
        return chineseScore;
    }
    public int getMathScore() {
        return mathScore;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", 总成绩=" + (chineseScore + mathScore) +
                '}';
    }
}
class StudentComparator implements Comparator<Student> {
    @Override
    public int compare(Student s1, Student s2) {
        // 比较总成绩
        return Integer.compare(s2.getChineseScore() + s2.getMathScore(),
                s1.getChineseScore() + s1.getMathScore());
    }
}
public class Test {
    public static void main(String[] args) {
        // 创建一个按照总成绩排序的优先级队列
        PriorityQueue<Student> queue = new PriorityQueue<>(new StudentComparator());
        // 添加元素
        queue.offer(new Student("董李阳", 80, 90));
        System.out.println(queue);
        queue.offer(new Student("陈清扬", 95, 95));
        System.out.println(queue);
        queue.offer(new Student("小驼铃", 90, 95));
        System.out.println(queue);
        queue.offer(new Student("沉默", 90, 80));
        System.out.println("==================排序之后=======================");
        while (!queue.isEmpty()) {
            System.out.println(queue.poll() + " ");
        }
    }
}

Student是一个学生对象,包含姓名、语文成绩,数学成绩。

StudentComparator实现了Comparator接口,对总成绩做了一个排序。

PriorityQueue是一个优先级队列,参数为StudentComparator,然后添加了四个学生对象进去。

看看结果:

使用offer方法添加元素,最后使用while循环遍历元素(通过poll方法取出元素),从结果可以看到,PriorityQueue按照学生的总成绩由高到低进行了排序。

04)、Map

Map保存的是键值对,键要求保持唯一性,值可以重复。

1)HashMap

HashMap实现了Map接口,可以根据键快速地查找对应的值---通过哈希函数将键映射到哈希表中的一个索引位置,从而实现快速访问。

这里先大致了解以下HashMap的特点:

HashMap中的键和值都可以为null,如果键为null,则将该键映射到哈希表的第一个位置。

可以使用迭代器或者forEach方法遍历HashMap中的键值对。

HashMap有一个初始容量和一个负载因子。初始容量是指哈希表的初始大小,负载因子是指哈希表在扩容之前可以存储的键值对数量与哈希表大小的比率,默认的初始容量是16,负载因子是0.75(有没有想起数据接口书籍中的这个词汇呢?并且也是0.75)

来个简单的增删改查:

复制代码
// 创建一个 HashMap 对象
        HashMap<String, String> hashMap = new HashMap<>();

// 添加键值对
        hashMap.put("董李阳", "dongliyang");
        hashMap.put("王二", "wanger");
        hashMap.put("陈清扬", "chenqingyang");

// 获取指定键的值
        String value1 = hashMap.get("董李阳");
        System.out.println("董李阳对应的值为:" + value1);

// 修改键对应的值
        hashMap.put("董李阳", "newdongliyang");
        String value2 = hashMap.get("董李阳");
        System.out.println("修改后董李阳对应的值为:" + value2);

// 删除指定键的键值对
        hashMap.remove("王二");

// 遍历 HashMap
        for (String key : hashMap.keySet()) {
            String value = hashMap.get(key);
            System.out.println(key + " 对应的值为:" + value);
        }

看看结果吧:

2)LinkedHashMap

HashMap已经非常强大了,但它是无序的。如果我们需要一个有序的Map,就要用到LinkedHashMap。LinkedHashMap是HashMap的子类,它使用链表来记录插入/访问元素的顺序。

LinkedHashMap可以看作是HashMap+LinkedList的合体,它使用了哈希表来存储数据,又使用了双向链表来维持顺序。

来一个简单的例子:

复制代码
        // 创建一个 LinkedHashMap,插入的键值对为 董李阳 王二 陈清扬
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("董李阳", "dongliyang");
        linkedHashMap.put("王二", "wanger");
        linkedHashMap.put("陈清扬", "chenqingyang");

// 遍历 LinkedHashMap
        for (String key : linkedHashMap.keySet()) {
            String value = linkedHashMap.get(key);
            System.out.println(key + " 对应的值为:" + value);
        }

来看看输出结果:

从结果可以看的出来,LinkedHashMap维持了键值对的插入顺序。

为了和LinkedHashMap对比,我们用同样的数据来实验以下HashMap。

复制代码
        // 创建一个HashMap,插入的键值对为 董李阳王二 陈清扬
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("董李阳", "dongliyang");
        hashMap.put("王二", "wanger");
        hashMap.put("陈清扬", "chenqingyang");

// 遍历 HashMap
        for (String key : hashMap.keySet()) {
            String value = hashMap.get(key);
            System.out.println(key + " 对应的值为:" + value);
        }

看看结果:

可以发现HashMap没有维持键值对的插入顺序。

3)TreeMap

TreeMap实现了SortedMap接口,可以自动将键值按照自然顺序或指定的比较器顺序排序,并保证其元素的顺序。内部使用红黑树来实现键的排序和查找。

同样来增删改查:

复制代码
        // 创建一个 TreeMap 对象
        Map<String, String> treeMap = new TreeMap<>();

// 向 TreeMap 中添加键值对
        treeMap.put("董李阳", "dongliyang");
        treeMap.put("王二", "wanger");
        treeMap.put("陈清扬", "chenqingyang");

// 查找键值对
        String name = "董李阳";
        if (treeMap.containsKey(name)) {
            System.out.println("找到了 " + name + ": " + treeMap.get(name));
        } else {
            System.out.println("没有找到 " + name);
        }

// 修改键值对
        name = "王二";
        if (treeMap.containsKey(name)) {
            System.out.println("修改前的 " + name + ": " + treeMap.get(name));
            treeMap.put(name, "newWanger");
            System.out.println("修改后的 " + name + ": " + treeMap.get(name));
        } else {
            System.out.println("没有找到 " + name);
        }

// 删除键值对
        name = "陈清扬";
        if (treeMap.containsKey(name)) {
            System.out.println("删除前的 " + name + ": " + treeMap.get(name));
            treeMap.remove(name);
            System.out.println("删除后的 " + name + ": " + treeMap.get(name));
        } else {
            System.out.println("没有找到 " + name);
        }

// 遍历 TreeMap
        for (Map.Entry<String, String> entry : treeMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

看看结果:

与HashMap不同的是,TreeMap会按照键的顺序来进行排序。

复制代码
// 创建一个 TreeMap 对象
Map<String, String> treeMap = new TreeMap<>();

// 向 TreeMap 中添加键值对
treeMap.put("c", "cat");
treeMap.put("a", "apple");
treeMap.put("b", "banana");

// 遍历 TreeMap
for (Map.Entry<String, String> entry : treeMap.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

看看结果:

与HashMap不同的是,TreeMap会按照键的顺序来进行排序。

当读者速览完之后,如果对这些增啥改查使用不熟悉,建议去自己的代码工具中敲敲。

好记性不如烂笔头!!!

相关推荐
qq_433554549 分钟前
C++ STL编程 vector空间预留、vector高效删除、vector数据排序、vector代码练习
开发语言·c++
随风奔跑的十八岁13 分钟前
java 破解aspose.words 18.6 使用
java·linux·word转pdf·aspose-words
居然是阿宋33 分钟前
C语言的中断 vs Java/Kotlin的异常:底层机制与高级抽象的对比
java·c语言·kotlin
sco52821 小时前
SpringBoot 自动装配原理 & 自定义一个 starter
java·spring boot·后端
曼岛_1 小时前
[Java实战]Spring Boot 快速配置 HTTPS 并实现 HTTP 自动跳转(八)
java·spring boot·http
_Itachi__1 小时前
LeetCode 热题 100 543. 二叉树的直径
java·算法·leetcode
风吹落叶32571 小时前
线程的一些事(2)
java·java-ee
宸汐Fish_Heart1 小时前
Python打卡训练营Day22
开发语言·python
菜狗想要变强1 小时前
C++ STL入门:vecto容器
开发语言·c++
是代码侠呀1 小时前
飞蛾扑火算法matlab实现
开发语言·算法·matlab·github·github star·github 加星