【Java进阶】啥都能“装”的集合

156 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


集合框架

所谓集合,是 Java 中提供的一种容器,可以用来存储多个不同的数据,根据不同存储方式形成的体系结构,称为集合框架体系。我们可以简单理解为能够装下很多同种类型的一种容器。首先,我们可以想一下,之前我们已经知道有了数组这么一个东西了,为什么还需要集合呢?第一,数组长度不可变,集合长度可变;第二,将元素封装成容器,便于开发者直接调用。

b53391c2992340ac45768d2b9472b6ca.jpg

1.Collection

1.1 概述

Collection是单例集合的顶层接口,继承自Iterable,JDK不提供此接口的任何直接实现,但提供更具体的子接口(如Set、List)实现。

1.2 创建方式

  • 通过多态的方式创建 Collection<Object> o = new ArrayList<>();
  • 使用具体实现类创建 ArrayList<Object> o = new ArrayList<>();

1.3 常用方法

方法名说明
boolean add(E e)添加元素
boolean remove(Object o)移除指定元素
void clear()清空元素
boolean contains(Object o)判断是否存在指定元素
boolean isEmpty()判断是否为空
int size()获取集合长度

2.List

2.1 概述

List集合是一种有序、可含有重复元素的集合,可以精确控制列表中每个元素的插入位置。能够通过索引直接访问元素,并搜索列表中的元素。

说明:有序是指存储和取出的元素顺序是一致的,并不是排序。(以下同理)

2.2 特有方法

方法名说明
void add(int index, E element)指定位置插入指定元素
E remove(int index)删除指定索引处的元素,返回被删除的元素
E set(int index, E element)修改指定索引处的元素,返回被修改的元素
E get(int index)返回指定索引处的元素

2.3 List子类

(1)ArrayList:底层数据结构是数组,特点是查询快、增删慢

(2)LinkedList:底层数据结构是双向链表,特点是查询慢、增删快

LinkedList特有方法:

方法名说明
void addFirst(E, e)在头部插入指定元素
void addLast(E, e)在尾部插入指定元素
E getFirst()返回第一个元素
E getLast()返回最后一个元素
E removeFirst()删除并返回第一个元素
E removeLast()删除并返回最后一个元素

(3)Vector:底层数据结构是数组,是线程安全的,但效率较低,使用较少

3.Set

3.1 概述

Set集合是一种不包含重复元素的集合。由于该集合不带有索引的方法,所以不能使用普通for循环进行遍历

3.2 哈希值

(1)哈希值:哈希值是JDK根据对象的地址字符串数字计算出来的int类型数值。

(2)获取对象哈希值的方法int hashCode()

(3)对象哈希值的特点

  • 同一个对象多次调用hashCode()方法返回的哈希值是相同的
  • 默认情况下,不同对象的哈希值不相同,但可以通过重写hashCode()方法实现不同对象的哈希值相同

特例

"重地".hashCode(); // 哈希值为1179395

"通话".hashCode(); // 哈希值为1179395

3.3 Set子类

(1)HashSet:底层数据结构是哈希表,是一种无序集合,即不保证存储和取出元素的顺序一致

HashSet的底层其实是HashMap,而HashMap的底层是哈希表,所以存储到HashSet里面的元素实际上是存储到了HashMap的key中

(2)LinkedHashSet:底层数据结构是双向链表哈希表,是一种有序集合,由链表保证元素有序,由哈希表保证元素唯一

(3)TreeSet:底层数据结构是自平衡的排序二叉树,元素按照一定规则进行排序,具体排序方式取决于构造方法

TreeSet的底层其实是TreeMap,而TreeMap的底层是自平衡的排序二叉树,所以存储到TreeSet里面的元素实际上是存储到了TreeMap的key中

4.泛型

4.1 概述

泛型是JDK 5中引入的特性,本质是参数化类型。它提供了编译时类型安全检测机制,能够在编译时检测到非法类型。

参数化类型就是将类型由原来的具体的类型参数化,然后在使用或者调用时传入具体类型。这种参数类型可以用在类、方法、接口中,分别被称为泛型类、泛型方法、泛型接口。

简单地说,泛型就是一种未知的数据类型。当我们不确定使用什么数据类型时,就可以使用泛型,泛型也可以看成是一个变量,用来传递数据类型。

4.2 好处

  • 将运行时期的问题提前到编译期间
  • 避免强制类型转换

4.3 泛型类

  • 格式:修饰符 class 类名<类型>{}
  • 范例public class Generic<T>{} - T可以是任意标识,常见的还有E、K、V等

4.4 泛型方法

  • 格式:修饰符 <类型> 返回值类型 方法名(类型 变量名){}
  • 范例public <T> void show(T t){}

4.5 泛型接口

  • 格式:修饰符 interface 接口名 <类型> {}
  • 范例public interface Generic<T>{}

5.Map

5.1 概述

  • Interface Map<K,V> K:键的类型;V:值的类型
  • 键不能重复,值可以重复,每个键最多映射到一个值

5.2 创建方式

  • 通过多态的方式
  • 通过具体实现类

5.3 常用方法

方法名说明
V put(K key,V value)添加元素
V remove(Object key)根据键删除键值对元素
void clear()移除所有键值对元素
boolean containsKey(Object key)判断集合是否包含指定的键
boolean containsValue(Object value)判断集合是否包含指定的值
boolean isEmpty()判断集合是否为空
int size()获取集合的长度
V get(Object key)根据键获取值
Set<K> keySet()获取所有键的集合
Collection<V> values()获取所有值的集合
Set<Map.Entry<K,V>> entrySet()获取所有键值对对象的集合

5.4 Map子类

  • HashMap:底层数据结构是哈希表
  • Hashtable:底层数据结构是哈希表,是线程安全的,效率较低,使用较少
  • Properties线程安全的,并且key和value只能存储字符串String
  • TreeMap:底层数据结构是自平衡的排序二叉树,TreeMap集合中的key自动按照大小顺序排序
5.5 遍历方式

(1)方式一:键集合→键→值

  • 获取所有键的集合【keySet()方法】
  • 遍历键的集合,获取每一个键【增强for循环】
  • 根据键获取值【get()方法】

(2)方式二:键值对集合→键值对→键和值

  • 获取所有键值对对象的集合【Set<Map.Entry<K,V>> entrySet()】
  • 遍历键值对对象的集合,获取每一个键值对对象【增强for】
  • 根据键值对对象获取键和值【getKey()和getValue()】

5.其它

5.1 集合中存放的是元素的地址(或者说是元素的引用),不能存储基本数据类型

能够添加基本数据类型,是因为系统会进行自动装箱,即由基本数据类型转换为对应的包装类,但里面存储的依然是地址

5.2 contains()方法是通过equals()方法判断集合中是否包含了某个元素的方法

5.3 JDK 8之前,哈希表是数组和单向链表的结合体。JDK 8之后,引入了红黑树,当单向链表上的元素超过8个时,单向链表会变成红黑树数据结构;若红黑树上的节点数量小于6时,会重新把红黑树变成的单向链表数据结构。


拓展1:迭代器(Iterator)

(1)概述

Java Iterator并不是一个集合,而是一种用于访问集合的方法,可以用于迭代ArrayList、HashSet等集合。迭代器只能用于遍历集合,不能对集合进行添加、修改、删除操作(ListIterator除外)。

原因:Java认为在迭代的过程中,迭代器的容量应当保持不变。

原理:Java容器中保留了一个称为modCount的域,每次对容器进行修改,modCount就会加1。当调用iterator方法时,返回的迭代器会记录当前的modCount,随后在迭代器遍历过程中会检查这个值,一旦发现这个值发生变化,就说明对容器做了修改,就会抛异常(ConcurrentModificationException)。

(2)常用方法

方法名说明
boolean hasNext()判断迭代是否有下一个元素
E next()获取指针位置的下一个元素,获取后指向下一个位置
void remove()移除迭代器指向的Collection中的元素

(3)并发修改异常(ConcurrentModificationException)

原因:不允许在迭代的过程中改变集合的长度(增加或删除元素)

如果需要在迭代过程删除元素,则需要使用迭代器的remove()方法,并只能使用迭代器遍历

拓展2:增强for循环

// 定义格式
for(ElementType element: arrayName){};
// 快捷键,输入num.for后回车
int[] num ={1, 2, 3, 4, 5};
for (int i : num) {
    System.out.println(num[i]);
}

拓展3:HashSet集合保证元素唯一性

总共会进行两个判断(以JDK 8之前为例:哈希表=数组+链表):

第一个判断:调用hashCode()方法计算出对象的哈希值,再通过哈希算法/哈希函数计算出对象对应的下标(即存储位置),若该位置没有元素,则进行存储;若该位置有元素,则进行下一个判断;

第二个判断:遍历单向链表,调用equals()方法比较存储对象和单向链表上的元素的内容是否一样,如果全部返回false,则将元素添加到链表尾部;如果有一个返回true,则将该节点的value覆盖掉。

HashSet集合要保证元素唯一性,则需要重写hashCode()和equals() 原因: 对于两个相同内容的不同对象,默认的hashCode()方法会将它们生成不同的哈希值,对于不同的哈希值,很可能会被直接存储到集合中,所以需要重写hashCode()方法。 equals()方法默认比较的是内存地址,因此对于相同地址的不同内容的元素,会导致相同地址的元素无法被存储,所以也需要重写equals()方法。