Java集合

48 阅读14分钟

Java集合,也叫容器,主要由两大接口派生而来

Collection接口,主要用于存放单一元素,下面还有三个主要的子接口:List、Set、Queue

Map接口,主要存放键值对;

List,Set,Queue,Map的区别

List:存储的元素是有序的、可重复的

Set:存储的元素不可重复

Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的

Map:使用键值对存储,key是无序的、不可重复,value是无序的、可重复,每个键最多映射到一个值

如何选用集合

需要根据集合的特点来选择使用:

需要根据键值获取到元素的值时就选用Map接口下的集合:

需要排序:TreeMap

不需要排序:HashMap

需要保证线程安全:ConcurrentHashMap

只需要存放元素值时就选用Collection接口下的集合

需要保证元素唯一时选择实现Set接口的集合,比如:TreeSet或HashSet

不需要保证元素唯一时就选择实现List接口的集合,比如:ArrayList或LinkedList

集合和数组的差别

当我们需要存储一组类型相同的数据时,数组是最常用且最基本的容器之一。但是,使用数组存储对象存在一些不足之处,因为在实际开发中,存储的数据类型多种多样且数量不确定。这时,Java集合就排上用场了。与数组相比,Java集合提供了更灵活、更有效的方法来存储多个数据对象。Java集合框架中的各种集合类和接口可以存储不同类型和数量的对象,同时还具有多样化的操作方式。Java集合的优势在于它们的大小可变、支持泛型、具有内建算法等。总的来说,Java集合提高了数据的存储和处理灵活性,可以更好的适应现代软件开发中多样化的数据需求,并支持高质量的代码编写

List

ArrayList和Array(数组)的区别 ArrayList内部基于动态数组实现,比Array(静态数组)使用起来更加灵活:

ArrayList会根据实际存储的元素动态地扩容或缩容,而Array被创建之后就不能改变它的长度了。

ArrayList允许使用泛型来确保类型安全,Array不可以

ArrayList中只能存储对象。对于基本数据类型,需要使用其对应的包装类。Array可以直接存储基本数据类型,也可以存储对象

ArrayList支持插入、删除、遍历等常见操作,并且提供丰富的API操作方法,Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力

ArrayList创建时不需要指定大小,而Array创建时必须指定大小

ArrayList可以添加null值吗

ArrayList可以存储任何类型的对象,包括null值,不建议向ArrayList中添加null值,null值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针。

ArrayList插入和删除的时间复杂度

对于插入:

头部插入:插入到头部,所有元素向后移一位,时间复杂度O(n)

尾部插入:当没有达到ArrayList容量极限的时候,直接插入尾部,时间复杂度为O(1),当达到了容量极限,需要扩容,将数组中所有元素拷贝到另一个数组中,时间复杂度为O(n),然后再插入尾部

指定位置插入:需要将目标位置之后的所有元素后移一位,再插入元素到指定位置,平均需要移动n/2个元素,时间复杂度O(n)

对于删除:

头部删除:所有元素向前移动一个位置,时间复杂度O(n)

尾部删除:O(1)

指定位置删除:指定位置之后的元素向前移动一个位置,平均移动n/2个元素,时间复杂度为O(n)

LinkedList插入和删除的时间复杂度

头部插入/删除:只需要修改头节点的指针即可完成插入/删除操作,时间复杂度为O(1)

尾部插入:只需要修改尾节点的指针即可完成插入/删除操作,时间复杂度为O(1)

指定位置插入:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要遍历平均n/2个元素,时间复杂度为O(n)

ArrayList和LinkedList的区别

是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全

底层数据结构:ArrayList底层使用的是Object数组,LinkedList底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环)

插入和删除是否受元素位置的影响:

ArrayList采用的数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e)方法的时候,ArrayList会默认将指定元素追加到此列表的末尾,这种情况的时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话,时间复杂度就是O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n - i)个元素都要执行向后/向前移一位的操作

LinkedList采用链表存储,所以在头尾插入或删除元素不受元素位置的影响,时间复杂度为O(1),如果是要在指定位置 i 插入和删除元素的话,时间复杂度为O(n),因为要先移动到指定位置再插入和删除。

是否支持快速随机访问:LinkedList不支持高效的随即元素访问,而ArrayList(实现了RandomAccess接口)支持,快速随机访问就是通过元素序号快速获取元素对象(对应get(int index)方法)

内存空间占用:ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放后继和直接前驱以及数据)

注意:

我们再项目中一般是不会使用到LinkedList的,需要用到LinkedList的场景几乎都可以使用ArrayList代替,并且性能通常会更好,LinkedList的作者自己都不用

RandomAccess接口:

实际上RandomAccess接口中什么都没有定义。所以,这个接口只是用于标识实现这个接口的类具有随机访问功能。

ArrayList实现了RandomAccess接口,而LinkedList没有实现,为什么?ArrayList底层是数组,而LinkedList底层是链表,数组天然支持随机访问,时间复杂度为O(1),所以称为快速随机访问,链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为O(n),所以不支持快速随机访问。ArrayList实现了RandomAccess接口,就表明了他具有快速随机访问功能,RandomAccess接口只是标识,并不是说ArrayList实现RandomAccess接口才具有快速随机访问功能的。

Set

Comparable和Comparator的区别 Comparable和Comparator接口都是Java中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥重要作用

Comparable接口实际上是出自java.lang包,它有一个compareTo(Object obj)方法用来排序

Compartor接口实际上是出自java.util包,它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法

我们可以理解为Comparable是内部比较器,Comparator是外部比较器

下面是一个示例:

Comparable:

Comparable是一个对象本身就已经支持自比较所需要实现的接口,如String,Integer自己就实现了Comparable接口,可完成比较大小操作。自定义类要在加入list容器中后能够排序,也可以实现Comparable接口,在使用Collections类的sort方法排序时,若不指定Comparator,那就以自然顺序排序,自然排序就是实现Comparable接口设定的排序方式,因此我们需要在实现了Comparable接口的类中重写compareTo(T o)方法


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Comparable<Person> {
    private String name;
    private int age;

	// 比较规则,按年龄升序
    @Override
    public int compareTo(Person o) {
        return Integer.compare(this.age, o.age);
    }
}

public static void main(String[] args) {
  Person p1 = new Person("张三", 20);
  Person p2 = new Person("李四", 30);
  Person p3 = new Person("王五", 10);
  ArrayList<Person> people = new ArrayList<>();
  people.add(p1);
  people.add(p2);
  people.add(p3);
  people.sort(Person::compareTo);
  people.forEach(System.out::println);
}
Person(name=王五, age=10)
Person(name=张三, age=20)
Person(name=李四, age=30)

Compartor:

Comparator被称为外部比较器,是因为如果我们需要对某个类进行排序,但是该类本身不支持排序,我们可以另外定义一个实现了Comparator接口的类,来作为类A的比较器,这个比较器只需要实现Comparator接口即可

List<Integer> list2 = Arrays.asList(5, 3, 2, 4, 1);
Collections.sort(list2, (a, b) -> b - a);
log.info("{}",list2);

以匿名内部类的方式实现

Comparable和Comparator都是用来实现集合中元素比较、排序的,只是Comparable 是在集合内部定义的方法实现的排序,Comparator是在集合外部实现的排序。所以想实现排序,就需要在集合外定义Comparator接口的方法或在集合内实现Comparable接口的方法

比较的方法,例如用x.compareTo(y),来比较x和y的大小。如果返回负数,意味着x比y小;返回0,意味着x等于y,返回正数,意味着x大于y

无序性和不可重复性指的是什么?

无序性:是指元素并非是按照索引下标来排序的,而是根据元素的hash值来决定的

不可重复性:指的是元素添加时使用equals判断返回false,需要重写equals方法和hashCode方法

HashSet、LinkedHashSet和TreeSet的异同

三个都是Set接口的实现类,都能保证元素唯一,且都是线程不安全的

三者的底层数据结构不同,HashSet的底层数据结构是HashMap,LinkedHashSet底层数据结构是链表和哈希表,元素的插入和取出满足FIFO,TreeSet底层数据结构是红黑树,且元素是有序的(插入到TreeSet的元素必须实现Comparable接口或有Comparator外部定制排序的方法,使用外部定制排序需要将Comparator的实现类当作形参传递给TreeSet,在插入时默认会以元素的升序排序)

三者的差异导致有不同的使用场景,当不需要保证元素插入和取出的顺序时可以使用HashSet,当需要保证元素的插入和取出的顺序为FIFO时,可以使用LinkedHashSet,当涉及到排序场景的时候,使用TreeSet(支持自然排序和自定义排序)

Queue

Queue和Deque的区别

Queue是单端队列,只允许一端插入另一端删除,符合先进先出(FIFO)规则

Queue扩展了Collection的接口,根据因为容量问题而导致操作失败后处理方式不同,可以分为两类方法:一种在操作失败后会抛出异常,另一种则会返回特殊值

Queue接口抛出异常返回特殊值
插入队尾add(E e)offer(E e)
删除队尾remove()poll()
查询队首元素element()peek()

Deque是双端队列,在队列的两端都可以插入或删除元素

Deque扩展了Queue的接口,增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理的不同分为两类:

Deque接口抛出异常返回特殊值
插入队首addFirst(E e)offerFirst(E e)
插入队尾addLast(E e)offerLast(E e)
删除队首removeFirst()pollFirst()
删除队尾removeLast()pollLast()
查询队首元素getFirst()peekFirst()
查询队尾元素getLast()peekLast()

事实上,Deque还提供了pop和push等其他方法,可以用于模拟栈

ArrayDeque与LinkedList的区别

ArrayDeque和LinkedList都实现了Deque接口,两者都具有队列的功能,两者的区别如下:

ArrayDeque是基于可变长的数组和双指针来实现,而LinkedList则通过链表来实现

ArrayDeque不支持存储null数据(直接抛出空指针),但LinkedList支持

ArrayDeque插入时可能存在扩容,不过均摊后插入操作依然为O(1),虽然LinkedList不需要扩容,但是每次插入数据时需要申请新的堆空间,均摊性能较差

从性能的角度上,选用ArrayDeque来实现队列要比LinkedList更好,此外,ArrayDeque也可以用于实现栈

PriorityQueue优先队列

PriorityQueue与Queue的区别是元素出队顺序是和元素优先级有关的,即总是优先级高的元素先出队列

下面是PriorityQueue的特点:

利用二叉堆的数据结构来实现,底层使用可变长的数组来存储数据

通过堆元素的上浮和下沉,实现了在O(logn)的时间复杂度内插入元素和删除堆顶元素

它是非线程安全的,且不支持存储null和non-comparable对象

默认是小顶堆,但可以接收一个Comparator作为构造参数,来自定义元素优先级的先后

什么是BlockingQueue

BlockingQueue(阻塞队列)是一个接口,继承自Queue,BlockingQueue阻塞的原因是其支持当队列没有元素时一直阻塞,直到有元素;还支持,如果队列满了,一直等到队列可以放入新元素时再放入

BlockingQueue常用于生产者-消费者模型中,生产者线程会向队列中添加数据,消费者线程会从队列中取出数据进行处理

BlockingQueue的实现类有哪些?

ArrayBlockingQueue:使用数组实现的有界阻塞队列,在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制

LinkedBlockingQueue:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定,则默认为Integer.MAX_VALUE。和ArrayBlockingQueue不同的是,它仅支持非公平的锁访问机制

PriorityBlockingQueue:支持优先排序的无界阻塞元素必须实现Comparable接口或者在构造函数中传入Comparator对象,并且不能插入 null 元素。

SynchronousQueue:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。因此,SynchronousQueue通常用于线程之间的直接传递数据。

DelayQueue:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队

ArrayBlockingQueue和LinkedBlockingQueue的区别

ArrayBlockingQueue 和 LinkedBlockingQueue 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:

底层实现:ArrayBlockingQueue 基于数组实现,而 LinkedBlockingQueue 基于链表实现。

是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue 创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。

锁是否分离: ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以防止生产者和消费者线程之间的锁争夺。

内存占用:ArrayBlockingQueue 需要提前分配数组内存,而 LinkedBlockingQueue 则是动态分配链表节点内存。这意味着,ArrayBlockingQueue 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue 则是根据元素的增加而逐渐占用内存空间。