Java 容器类

本贴最后更新于 1707 天前,其中的信息可能已经物是人非

写在前面

这篇短文不是一天就可以写出来的,特殊情况一直没有开学,就在家里面重新看了一遍 《Java 核心技术》 ,我觉得 Java 集合类这一大块是真的很重要,就想着一边看一边写点东西来记一下。

引言

在 Java 的运行过程中,当运行环境符合某种条件的时候,程序就会不自觉的产生新的对象,但是在此之前,你并不知道,到底会产生哪一种对象,到底会产生几个对象,可能是几个,也有可能是几十、几百个对象。既然是这样,那我们就不能像平常那样采用:

String a = "xxx";

因为你根本就不知道到底会产生多少个这样的引用,所以,这里要引入一种概念:泛型程序设计

1.什么是泛型

1.1 不用泛型导致的问题

在 Java SE5 之前,编译器是允许往容器中插入一个错误的类型的,假如说现在有一个用来装载 Apple 对象的有一个 ArrayList 容器,但是现在我往里面添加了一个 Orange 对象:

public static void main(String[] args) {

        // 创建 ArrayList 容器
        ArrayList arrayList = new ArrayList();
        //添加 Apple 对象
        arrayList.add(new Apple());
        //添加 Orange 对象
        arrayList.add(new Orange());
        
    }

这个地方编译器没有报错!!!!!因为 ArrayList 底层实际上就是一个 Object 数组,理论上来说是可以添加任何的元素或者对象的,但是,我们在调用 get()方法的时候,返回的统统都是 Object 对象,或许,你的同事并不知道这个 Object 对象到底对应的是哪一个对象,是 Apple 么?还是 Orange?很有可能出现下面这种情况:

 public static void main(String[] args) {

        // 创建 ArrayList 容器
        ArrayList arrayList = new ArrayList();
        //添加 Apple 对象
        arrayList.add(new Apple());
        //添加 Orange 对象
        arrayList.add(new Orange());

        Apple apple = (Apple) arrayList.get(1);
        System.out.println(apple);

    }

毫无疑问,这个地方运行的时候是百分之百报错的,这只是简单的举一个例子,我们现在可以看得到索引为 1 的对象就是一个 Orange 对象而不是一个 Apple 对象,如果容器里面装载的元素很少,我们可以用肉眼来检查,但是生产换将可能一个容器会装载上万个元素或者对象,这怎么让我们用肉眼检查呢?有没有一种方法可以让 Java 虚拟机在编译 Class 字节码的时候就把错误返回给我们,就可以避免运行时异常了呢?

1.2 引入泛型

从 Java SE5 开始,在定义容器的时候,支持加入泛型约束:

ArrayList<Apple> apples = new ArrayList<>();

像上面这样,在定义 ArrayList 容器的时候,多了一对 <> 里面放置我们需要约束的对象,通常的 ArrayList 底层都是 Object[] 数组,但是现在我们这样写,就相当于定义了一个 String[] 数组,里面只能装载 String 对象,我们可以进行以下操作:

public static void main(String[] args) {

        ArrayList<Apple> apples = new ArrayList<>();

        apples.add(new Apple());

        Apple apple = apples.get(0);

    }

这里可以看到,当我们在 apples 中取出对象的时候,没有进行强制转换,因为 JVM 知道,你的这一个 ArrayList 就是只能装载 String 类及其子类。所以就不用我们来添加强制转换了,但当我们尝试着往里面添加一个 Orange 对象时,就会出现以下错误:

image.png

编译器就自动的将这种错误识别出来了,这样将后面的运行时异常转变为了编译时异常,更利于程序员排错。

关于泛型的详细剖析,等有时间了再写一写

2.Java 集合

2.1 集合接口与实现分离

和现代的数据结构类库常见的做法一样,Java 集合类库都将集合接口和实现一一分离,Java 集合框架为不同类型的集合定义了大量接口:

image.png

集合有两个基本接口:Collection 和 Map。 我们已经看到,可以用以下方法在集合中插入元素:
boolean add(E element)
不过,由于映射包含键 / 值对,所以要用 put 方法来插人:
V put(K key, V value)

这个相当于就是集合的两大派系了。

2.2 具体集合实现

下面就来详细说一下实现这两大接口具体的实现类,除了以 Map 结尾的类之外,其余所有的类全都实现了 Collection 接口,而以 Map 结尾的类都实现了 Map 接口。

以 Map 结尾的类都拓展了 AbstractMap 抽象类,其余所有的都拓展了 AbstractCollection 抽象类。

image.png

2.2.1ArrayList

ArrayList 是一个非常典型的数组列表,ArrayList 封装了一个动态再分配的对象数组,初始化 ArrayList 容器的时候,默认的就是一个空的数组,当往里面添加第一个元素时,这个 ArrayList 就会被赋予大小为 10 的容量,随着运行过程中往里面不断添加元素,ArrayList 容量不够时,就会重新初始化一个容量为当前 ArrayList 容量的 1.5 倍的新数组,然后将旧的 ArrayList 元素全部拷贝到新的 ArrayList 里面,旧的 ArrayList 就会被 GC 回收。更多详情请看初探 ArrayList 动态扩容

2.2.1.1ArrayList 基本用法
  1. 初始化
// 初始化一个不指定泛型的 ArrayList
        ArrayList arrayList = new ArrayList();
        
        // 初始化一个指定泛型为 String 的 ArrayList
        ArrayList<String> arrayList1 = new ArrayList<>();
        
        // 初始化一个容量为 4 的 ArrayList
        ArrayList<String> arrayList2 = new ArrayList<>(4);
  1. ArrayList 里面的一些常用方法:
        // 添加一个元素
        arrayList1.add("111");

        // 添加一个另集合到集合
        arrayList1.addAll(arrayList2);

        // 获取索引为0的元素
        String s = arrayList1.get(0);
        
        // 清空当前的 ArrayList
        arrayList1.clear();

        // lambda 表达式遍历集合
        arrayList1.forEach(e ->{
            System.out.println(e);
        });

        // Foreach遍历集合
        for (String s1 : arrayList1) {
            System.out.println(s1);
        }

        // 删除索引为0的元素
        arrayList1.remove(0);

        // 判断是否包含 111 元素
        arrayList1.contains("111");

        // 获取迭代器
        Iterator<String> iterator = arrayList1.iterator();
        while (iterator.hasNext()){
            // dosomething with the ArrayList
        }
2.2.1.2ArrayList 的优点
  1. 由于 ArrayList 底层由数组实现,所以在查找元素的时候就可以通过下标来进行查找,比如 get 方法就是通过数组的下标来进行查找的。或者是我们在进行遍历的时候,其底层是对一个数组进行遍历,也是高效的。
  2. ArrayList 还是一个不同步的数组列表,这意味着,当一个线程在对 ArrayList 进行操作时,你不必担心它会被另外的线程修改你的数据然后造成异常。
2.2.1.3ArrayList 的缺点

ArrayList 在储存的过程中,每一个元素的位置都是很重要的,其最终实现就是向一个数组中增删改查里面的元素。当我们要在数组中插入一个新的元素时,从插入的这个位置开始计算,后面所有的元素都需要往后移动一位给这个需要插入的元素腾出一个位置来。同理,当删除一个元素的时候,后面的元素也都需要往前移动一位才可以,倘若我们的数组中有大量元素,那么这样的操作就会非常的耗时。

image.png

2.2.2LinkedList

不同于 ArrayList,LinkedList 的底层是用双向链表这种数据结构来实现的,他的底层不再是使用数组实现,LinkedList 类中装有一个内部类 Node

 private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

这个 Node 类中包含了三个局部变量

  1. item
    item 表示的才是 LinkedList 里面真正保存的元素或者对象
  2. next
    这个表示的是当前节点的下一个节点,最后一个节点的 next 就是 null
  3. prev
    这个表示的是当前节点的上一个节点,第一个节点的 prev 就是 null
2.2.2.1LinkedList 基本用法
1.初始化
public static void main(String[] args) {

        // 初始化一个不指定泛型的 LinkedList
        LinkedList linkedList = new LinkedList();
        // 初始化一个指定泛型为 String 的 LinkedList
        LinkedList<String> strings = new LinkedList<>();
        // 初始化一个泛型为 String 的 LinkedList,然后将一个数组添加进去
        new LinkedList<String>(new ArrayList<>());

    }
2.基本语法

LinkedList()
构造一个空链表。

  • LinkedList(Col 1 ection elements)
    构造一个链表, 并将集合中所有的元素添加到这个链表中。
  • void addFirst(E element)
  • void addLast(E element)
    将某个元素添加到列表的头部或尾部。
  • E getFirst()
  • E getLast()
    返回列表头部或尾部的元素。
  • E removeFirst()
  • E removeLast()
    删除并返回列表头部或尾部的元素。
2.2.2.2LinkedList 的优点

尽管数组在连续的存储位置上存放对象引用, 但链表却将每个对象存放在独立的结点中。每个结点还存
放着序列中下一个结点的引用。在 Java 程序设计语言中,所有链表实际上都是双向链接的
(doubly linked)—即每个结点还存放着指向前驱结点的引用。

从链表中间删除一个元素是一个很轻松的操作, 即需要更新被删除元素附近的链接:

image.png

将一个元素从链表中删除

image.png

将一个元素添加到链表中

2.2.2.3LinkedList 的缺点

LinkedList 查找元素的效率不如 ArrayList,比较适合用于写多查少的环境,而 ArrayList 恰恰相反,适用于查多写少的环境。

2.2.3ArrayDeque

2.2.4HashSet

2.2.5TreeSet

2.2.6EnumSet

2.2.7LinkedHashSet

2.2.8PriorityQueue

2.2.9HashMap

HashMap 是一种典型的 <K,V> 数据结构,这个可以说是 Java 当中用的最多的映射了,HashMap 实现了 Map 接口。散列映射是对键进行散列,数映射根据键的顺序将元素组织为一个搜索树。散列或比较函数仅仅只用于键。与键关联的值不进行散列或比较。

Map 接口中提供了一个 put 方法可以放我们在 HashMap 中新增一条映射

image.png

HashMap 也是支持添加泛型来帮助 JVM 编译检查的。

那么,既然是离散型存储,那在新增一条映射的时候,到底应该存在 HashMap 当中的哪一个位置呢?

假如说需要添加进去的一条映射,键的哈希码是 76268 ,那么这个键存在的位置就是键的哈希码对 Map 的容量取余数,得到的数就是这个键的索引,比如现在 Map 的容量是 128(默认是 16),那么这个键的索引就是 76268%128 = 108,或许你的运气很好,这个位置刚好没有其他元素占用,那么这个新的键就自然而然的会插入到这个里面来,当然,有时候会遇到这个位置已经被占用的情况,这个时候就需要将这个新对象和原来这个位置已经有的对象进行比较,如果有的哈希码是一样的,那么久直接更新这个键所关联的值,如果是不一样的,那就会在这个基础上就会再使用链表关联。

image.png

HashMap 默认的容量是 16,在 HashMap 中,有一个成员变量叫填装因子,默认是 0.75,什么意思呢?就是说当 HashMap 的已经使用的容量达到总容量的百分之七十五的时候,HashMap 就会自动扩容为当前容量的 2 倍,所以说,HashMap 的容量只能是 2 的整数幂这么多。

image.png

2.2.9.1HashMap 常用方法

Map 接口中的方法

  • V get(Object key) 获取与键对应的值;返回与键对应的对象, 如果在映射中没有这个对象则返回 null。 键可以为 null。
  • default V getOrDefault(Object key, V defaultValue) 获得与键关联的值;返回与键关联的对象, 或者如果未在映射中找到这个键, 则返回 defaultValue。
  • V put(K key, V value) 将键与对应的值关系插入到映射中。如果这个键已经存在, 新的对象将取代与这个键 对应的旧对象。这个方法将返回键对应的旧值。如果这个键以前没有出现过则返回 null。键可以为 null, 但值不能为 null。
  • void putAl 1(Map entries) 将给定映射中的所有条目添加到这个映射中。
  • boolean containsKey(Object key) 如果在映射中已经有这个键, 返回 true。
  • boolean containsValue(Object value) 如果映射中已经有这个值, 返回 true。
  • default void forEach(BiConsumer action) 对这个映射中的所有键 / 值对应用这个动作。

HashMap 中的方法

  • HashMap()
  • HashMap(int initialCapacity)
  • HashMap(int initialCapacity, float 1 oadFactor) 用给定的容量和装填因子构造一个空散列映射(装填因子是一个 0.0 〜 1.0 之间的数 值。这个数值决定散列表填充的百分比。一旦到了这个比例, 就要将其再散列到更大 的表中)。默认的装填因子是 0.75。

2.2.10TreeMap

2.2.11EnumMap

2.2.12LinkedHashMap

2.2.13WeakHashMap

2.2.14IdentityHashMap

  • ArrayList
    5 引用 • 10 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖
  • HashMap
    19 引用 • 25 回帖
  • HashSet
    2 引用 • 4 回帖

相关帖子

4 回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Gouzhong1223
    作者

    我一次性写不了这么多,只有慢慢写了,争取在学校叫开学之前更完

  • 为什么你的 Apple 可以是蓝的,绿的,墨绿的,亮绿 的,

    1 回复
  • Gouzhong1223
    作者

    就是改了配色而已,大一的时候无聊,我记得专门拿了半天来该配色哈哈哈哈哈

    1 回复
  • 为什么如此认真的回答了这个问题,哈哈哈哈哈哈哈哈哈哈