本文已参与「新人创作礼」活动,一起开启掘金创作之路。
集合框架
所谓集合,是 Java 中提供的一种容器,可以用来存储多个不同的数据,根据不同存储方式形成的体系结构,称为集合框架体系。我们可以简单理解为能够装下很多同种类型的一种容器。首先,我们可以想一下,之前我们已经知道有了数组这么一个东西了,为什么还需要集合呢?第一,数组长度不可变,集合长度可变;第二,将元素封装成容器,便于开发者直接调用。
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()方法。