Java 容器

本文详细介绍了Java中的容器概念及其重要性,列举了List、Set、Map等常见容器的特性和使用方法,并对比了不同容器间的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是容器&&为什么使用容器

我们在实际使用时,会使用到成千上万的数据。这些数据存储在内存中不是杂乱无章的,而是通过各种数据结构组织而成的。容器实际上就是存储数据的一块内存区域,使用容器实际上就是通过java标准库中定义的结构、方法来实现数据的存储以及操作。java就提供了很多容器,多个容器的数据结构不同。但是,结构不重要,重要的是能够存储东西,能够判断,获取把容器共性的内容不断往上提取,最终形成容器的继承体系---->Collection。

容器有哪些呢。Java容器主要可以划分为5个部分:List列表、Set集合、Map映射、queue队列、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)。

为什么要使用容器呢?

通常,程序总是在运行时才能确定要创建的对象,甚至是对象的类型。为了解决这个问题,需要在任意时刻任意位置创建任意数量的对象。大多数语言都提供某种方法来解决这个问题,Java使用容器来解决这个问题。容器也称集合类,基本的类型是List、Set、Queue、Map,但由于Java类库中使用了Collection关键字来代表某一接口,所以一般用容器来称呼这些集合类。Java容器工具包位置是java.util.*。

二、容器的实现

JAVA常见的各个容器的继承关系如下图。在这里插入图片描述
List、Set和Queue接口都是继承自Collection,Collection接口继承自Iterator接口

(一)List

List是接口,其中在ArrayList和LinkedList中分别实现了List。
List 接口继承 Collection,存储的是有序、可重复的数据,以元素安插的次序来放置元素,不会重新排列。

函数功能
int size();返回数据个数
boolean isEmpty();是否为空
boolean contains(Object o);包含某函数
Iterator iterator();用于生成迭代器
Object[] toArray();转化为数组
boolean remove(Object o)移除某元素
boolean containsAll是否包含另一个List中所有的元素
boolean addAll(Collection<? extends E> c);添加另一个List中所有元素
boolean removeAll(Collection<?> c);移除掉另一个List中所有元素
boolean retainAll(Collection<?> c);只保留另一个List中所有元素
int indexOf(Object o);返回在list中的下标
E remove(int index);删除某一位置的元素
void add(int index, E element);添加某一位置的元素
E set(int index, E element);设置某一位置的元素
E get(int index);获取某一位置的元素
void clear();删除所有元素
default void sort(Comparator<? super E> c)排序

1.ArrayList

ArrayList 是 List 的子类,对应的数据结构为数组。

底层实现上,是使用数组来实现的,因此增删效率低,但查询效率高。但是ArrayList是线程不安全的。

ArrayList与普通数组的一个重要区别是:当想申请一个不确定长度的数组时,应当使用ArrayList。
ArrayList会在初始化时被设置为一个固定长度的数组,但是当数组的长度不足以装下时,就会进行扩容。扩容的步骤为:
(1)首先新建一个数据,扩大size(通常是原大小的1.5倍)
(2)然后将原数组的内容复制到新数组中
(3)将老数组的地址指向新数组
在这里插入图片描述

2.LinkedList

LinkedList对应的数据结构是双向链表,特点:查询效率低,增删效率高,线程不安全。
在这里插入图片描述

3. vector Stack

为什么要谈到这两个呢?
ArrayList继承自AbstractList,Vector同样继承自AbstractList,Stack继承自Vector。
LinkedList继承自AbstractSequentialList,而AbstractSequentialList继承自AbstractList。
Abstract继承自List。
因此,Vector和Stack也具有List的这些接口属性。

(1)vector

Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。
Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。

vector的实现:
(a) Vector实际上是通过一个数组去保存数据的。当我们构造Vector时;若使用默认构造函数,则Vector的默认容量大小是10。
(b) 当Vector容量不足以容纳全部元素时,Vector的容量会增加。若容量增加系数 >0,则将容量的值增加“容量增加系数”;否则,将容量大小增加一倍。
© Vector的克隆函数,即是将全部元素克隆到一个数组中。
(d) Vector中的操作是线程安全的,因为加了同步锁。
(e) 除此之外,其实现与ArrayList基本一样。

(2)Stack

Stack对应的数据结构为栈,即先进后出(FIFO)的一个结构。除了具有Vector的属性外(所以其实现方法也是数组,也是线程安全的),还添加了以下方法来实现栈的处理。

方法描述
public E push(E item)将元素入栈
public synchronized E pop()将元素出栈
public synchronized E peek()查看栈顶元素
public boolean empty()栈是否为空
public synchronized int search(Object o)查找元素并返回其到栈顶的偏移量(1-based)

(二)Map

实现 Map 接口的类用来存储键(key)-值(value) 对,Map 类中存储的键-值对通过键来标识, 所以键值不能重复。Map的特点为:无序,键不可重复,值不可重复。

在这里插入图片描述

1.HashMap

(1)HashMap的特点

HashMap: 线程不安全,效率高. 允许 key 或 value 为 null。相较于数组、链表而言效率非常高,在不发生hash冲突的前提下可以一次定位到。时间复杂度常数阶O(1)。
HashMap的结构图如下图。HashMap是一个位桶+链表的设计模式,每一个键-值对都存储在一个Entry结构中,Entry中包含了hash、key、value、next四个值。hash中存储的是通过hash计算得到的这个Entry的哈希值,next则指向同哈希值的下一个Entry对象。

(2)HashMap的结构

HashMap的主体是一个Entry数组,每一个数组元素都是具有不同Hash值的Entry对象。一个设计得比较好的hash函数可以使得所有的键-值对都计算出不同的哈希值,这样就可以被放入到Entry数组中的不同位置。但是,难免会出现hash冲突,也就是不同的键-值对计算得到了相同的hash值,这时就会将这个新得到的Entry通过链表的方法连接在相同hash值的Entry的后面。
在这里插入图片描述

(3)HashMap中数组的扩容

这个Entry数组的大小并不是一成不变的,如果数据增多时,显然会出现大量的哈希碰撞,这样查询的效率依然会非常低。因此,当数据增多时,会对Entry进行扩容,并且重新计算hash值。那么这个标志需要扩容的标准是什么呢?在HashMap中存在两个参数:初始容量initialCapacity(默认为16),负载因子loadFactor(默认为0.75)。
每次使用put(…)在Map中添加新的键/值时,该函数都会检查是否需要增加内部数组的容量。为此,地图HashMap了2个数据:
(1)Map的size:它表示HashMap中的条目数。每次添加或删除条目时,都会更新此值。
(2)一个阈值:等于(内部数组的容量)* loadFactor,并在每次调整内部数组的大小后刷新
在添加新的Entry之前,put(…)检查size是否大于阈值,以及是否重新创建大小加倍的新数组。

负载因子必须要设计合理,如果过高:链表大量形成;如果过小:数组利用率低,需要经常扩容,代价高。

(4)Hash值的计算

另一个问题是,Hash值是如何计算的呢?
在这里插入图片描述
HashMap的size每次扩容都是2倍,因此size必然2^n大小,这样的设计是有原因的。

在这里插入图片描述
根据这一算法,2^n的大小减去1,其二进制格式一定是一个后面n位全为1的值,这样的话,在进行与操作后,hash值的后面n位将会被全部保留下来,最大程度上减少了冲突的可能性。

(5)添加了红黑树的HashMap

在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(log2 n))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)。

如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap(实现红黑数的数据结构)实现来替换掉它。这样做的结果会更好,是O(log2 n),而不是糟糕的O(n)。利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
红黑树的具体实现可以参考https://www.cnblogs.com/xiaohouye/p/11381700.html。

2.HashTable

HashTable与HashMap基本上是一样的,但是它是线程安全的,因此效率会更低一点。
HashMap与HashMap的区别有:
(1)继承的父类不同
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
(2)线程安全性不同
HashTable是线程安全的,但是HashMap不是。
(3)contains方法
HashMap中删除了contains方法,用containsKey和containsValue代替,而HashTable中仍然保存了contains,和containsValue功能相同。
(4)Key值
HashMap允许键为null,但是只能有一个,HashTable不允许为null。
(5)hash值计算
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算。
(6)数组大小以及扩容方式
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

3.TreeMap

HashMap继承的是AbstractMap,而TreeMap则继承的SortedMap,因此当希望获得存储的数据是被排序的时候,使用TreeMap。

4.几种Map的区别

下面针对各个实现类的特点做一些说明:

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。

(二)Set

Set 接口继承 Collection,使用自己内部的一个排列机制。Set 接口中的元素无序不可重复:不包含重复元素,最多包含一个 null,先来和后到的元素对于set没有差别。
Set的函数接口与List一致。

Set包括HashSet,TreeSet

1.HashSet

HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 是按照哈希值来存的所以取数据也是按照哈希值取得。
HashSet不放入重复元素,这主要使用使用hashcode()和equals()两个函数:
当需要把对象加入HashSet时,HashSet会使用对象的hashCode来判断对象加入的位置。同时也会与其他已经加入的对象的hashCode进行比较,如果没有相等的hashCode,HashSet就会假设对象没有重复出现。因此我们自定义类的时候需要重写hashCode,来确保对象具有相同的hashCode值。

如果元素(对象)的hashCode值相同,并不意味着两个对象是相同的,会继续使用equals 进行比较。如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复.新元素可以存入。

HashSet具有以下特点:
(1)不能保证元素的排列顺序,顺序有可能发生变化
(2)不是同步的
(3)集合元素可以是null,但只能放入一个null

2.TreeSet

TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。
obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。
如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0
定制排序
自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。

最重要:

1、TreeSet 是二差树实现的,Treeset中的数据是自动排好序的,不允许放入null值。

2、HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。

3、HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。

(三)Queue

https://www.cnblogs.com/lemon-flm/p/7877898.html

(五)Iterator

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值