Java对事物的操作都是基于对象的形式,为了方便操作多个对象就诞生了集合。
Java为我们提供了一套完整的容器类库,这些容器可以用于存取各种类型的对象,并且长度是可变的,我们把这些类称为集合。集合都位于java.util包中。
集合的组织架构
泛型
泛型一般常用来规定集合的数据类型,类似方法中的参数。强制集合中可存放的数据的类型是泛型规定好的数据类型。这样可以提高程序的安全级别和稳定。
语法:
类名/接口名<引用数据类型>
标红的地方就是泛型定义方式,多个直接用逗号分隔。
List
List接口是一个有序的集合,使用此接口能够精确控制每个元素存放的位置,能够通过索引(类似数组的下标)来访问List集合中的元素对象,而且允许有相同的元素同时存在。List接口下常见的实现类有:ArrayList、LinkedList、Vector。
基础方法
- boolean add(Element e); // 增加元素
- E get(int index); // 获取元素
- E remove(int index); // 删除元素
- E set(int index, Element e); // 修改对应元素
- int indexOf(Element e); // 返回指定元素对应的下标
特点
- 有序集合,按照插入元素的顺序排序。
- 可存放重复元素。
- 无固定长度。
ArrayList (常用)
该类实现了List接口,是List的具体实现类之一。底层数据结构是数组结构,线程不安全,增删慢,查找快。是日常开发中使用最多的集合,常用来查询数据,遍历数据。
声明方式:
- List<Element> 变量名 = new ArrayList<ELement>();
- ArrayList<Element> 变量名 = new ArrayList<ELement>();
<ELement>是泛型,用来限定集合中的数据类型。一个集合中只允许出现同一种数据类型的元素对象。
声明方式1是Java多态-父类型引用子类型的一种体现。这种实例化后的对象中只有父类中的属性和方法,不包含子类中独有的属性和方法。
LinkedList
该类实现了List接口,是List的具体实现类之一,主要用来创建链表数据。增删快,查找慢。LinkedList是双向链表,可以向链表的头、尾或指定元素前后插入。
声明方法:
- List<Element> list = new LinkedList<Element>();
- LinkedList<Element> list = new LinkedList<Element>();
特有方法:
- void addFirst(Element e); // 向集合首位增加元素
- void addLast(Element e); // 向集合末位增加元素
- E getFirst(); // 获取集合首位元素
- E getLast(); // 获取集合末位元素
Vector
该类和ArrayList非常类似,是线程安全的,在多线程的情况下使用。效率比ArrayList低。
Map
Map接口是一个以键值对存储的集合,每个元素都由键和值构成,记录的是键与值对应的关系。常见实现类有HashMap、HashTable。
Map<K,V>
K:表示集合的键的数据类型。
V:表示值的数据类型。
基础方法:
- V put(K key, V value); // 向map中增加键值对
- V get(K key); // 获取map中对应键的值
- V remove(K key); // 删除map中对应键和值
- Set<K> keyset();// 获取map中已存在键的集合
- Collection<V> values();// 获取map中已存在值的集合
特点
- Map中的数据都是以键值对的形式存在的。
- 是无序的、键不可重复的集合。
HashMap(常用)
HashMap是Map接口的一种具体实现类,基于Hash算法实现的Map接口。底层的数据结构是数组+链表构成的Hash散列表。无序且非线程安全。
声明:
- Map<String, String> map = new HashMap<String, String>();
- HashMap<String, String> map = new HashMap<String, String>();
特点:
- HashMap的键不可以重复,值可以重复。
- HashMap允许键或值为null。
Entry
Entry是Map接口的嵌套接口,Entry会将将Map中的键值对封装成了对象,可以独立使用。常用于Map集合的遍历。
HashTable
Hashtable也是Map接口的一种具体实现类,HashTable是线程安全且键不允许为null。
Set
Set接口是无序且不可重复元素的集合接口,其子类均无法存放相同元素。常见实现有HashSet、LinkedHashSet、TreeSet。
HashSet
LinkedHashSet
TreeSet
集合的遍历
size()
size()方法获取List集合已有元素的长度。
fori(掌握)
适用于List集合的遍历,类似遍历数组的方式,通过控制索引(下标)来实现集合的遍历。
foreach(常用)
foreach是for循环的一种增强形式。在进行引用数据类型的集合遍历时会很方便,可减少对象赋值的操作。适用于List、Map集合的遍历。
语法:
for(数据类型 变量名 : 表达式){
}
数据类型:是集合中泛型限定的数据类型。
变量名:是局部变量,在循环体内操作。
表达式:一般常为集合或数组。
List集合遍历(常用)
Map集合(常用)
KeySet()方式
思路:通过map.keySet()的方法可以获取map中全部的key,然后根据当前遍历的key获取map中对应的值,从而达到对map集合的遍历。
entrySet()方式
思路:通过map.entrySet()的方法获取map中全部的键值对集合,然后根据Entry可以获取对应的键或值。
entry.getKey(); // 获取对应键值对的键
entry.getValue();// 获取对应键值对的值
推荐使用此方法,尤其是Map中数据大时。
Iterator 迭代器
是集合框架提供的一种遍历集合的方式。
使用步骤:
- 创建迭代器实例。
- 通过.hasNext()方法判断是否有下一个元素。
通过.next()方法获取下一个元素。
注意
- 在进行数组或集合的遍历时,要避免对数组或集合的增、删操作。
否则可能会出现删除不完整的情况。
- for循环的底层实现就是迭代器、所以迭代器的运行效率是最高的。
- 迭代器遍历集合操作不方便,所以通常使用fori或foreach的方式。
Collenctions
Collenctions是集合的工具类,专门针对集合进行操作。
常用方法
void reverse(List list); // 反转
void shuffle(List list); // 随机排序
void sort(List list); // 按自然排序的升序排序
void sort(List list, Comparator c); //定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j),交换两个索引位置的元素
List<->Array
Object[] toArray(); // 将集合转为数组
Arrays.asList(Object[] array);// 将数组转为集合 使用Arrays工具类
List遍历删除问题
当我们在fori时使用list.remove(index)删除list中元素时,可能会导致元素删除不完整的情况。
使用foreach时会抛异常ConcurrentModificationException(并发修改异常)
正确方式:
- 使用迭代器删除,效率高。
- 遍历完后使用removeAll(list)删除,效率不高。
数组和链表区别/ArrayList和LinkedList
通过上面的学习我们了解到ArrayList的特点是增删慢,查找快,而LinkedList的特点是增删快,查找慢。ArrayList和LinkedList同为List接口下的实现,造成它俩特点相反的根本原因就是它俩的数据结构不同决定的,ArrayList基于数组实现而LinkedList基于链表实现。
数组
数组特点:有序,固定长度,可重复,增删慢,查找快。
数组原理:数组在向系统申请内存空间时,申请的是一段具有固定长度(所以我们在声明数组时必须指定数组的长度)、连续的(所以数组具有有序性)内存空间。使用索引(index)可以快速定位数组中元素的位置(查找快)。但是数组在增加或删除元素时为了保证数组连续性需要对操作元素后方的所有元素的索引位置进行改变(增删慢)。
链表
链表特点:有序,无固定长度,可重复,增删快,查找慢。
链表原理:链表在向系统申请内存空间时,申请的是无固定长度,且非连续的内存空间。在获取某一个节点时必须从根节点开始遍历查找(查找慢),在增加/删除时只需要将当前节点链入/移除即可(增删快)。
单链表:当前节点中只指向下一个节点信息。
双链表:当前节点中同时指向上一个和下一个节点的信息。
根节点:头结点,链表最开始的位置。
尾节点:下一个节点信息为null的节点,在链表的尾部。
HashMap数据结构
HashMap的数据结构是基于Hash算法的散列表,简单说就是数组+链表(单链表)的形式。HashMap利用数组和链表的各自的优势来保证HashMap在put()和get()时的高效。
数组是HashMap的主体,当进行put(key, value)操作时,HashMap首先会根据Key的hashCode值计算Key存储的位置(计算公式:key的HashCode值%当前HashMap的size())。计算出来位置后,HashMap会判断当前位置是否为null,如果为null,则将key和value包装成Entry存入链表。如果当前位置不为null,HashMap会遍历当前位置的链表,将链表中的元素和key进行equals()值的比较,发现一样的Key则进行覆盖操作,没有一样的Key则将该Entry链入链表。在Java8中当HashMap中的某个链表数超过16则将链表替换为红黑树。特殊的,当Key为null时,HashMap只会将null插入到数组的第0个位置。
HashMap在进行get()操作时和put()操作类似,也是先根据key的HashCode值计算要获取元素的存储位置。当该位置值为null时,直接返回null,不为null时将key.equals()与链表中的Entry的Key进行匹配返回,没有匹配成功则返回null。
Hash碰撞
HashMap在进行put()的时候,如果相同数组位置的链表元素个数大于1,我们就可以认为是发生了Hash碰撞。HashMap解决Hash碰撞的方式就是将碰撞的元素在链表中链入,这种方式就称之为拉链法。
集合的扩容(掌握)
List、Set、Map都是无固定长度的,但是它们都有初始长度。当集合对象中存的元素超过集合的扩容因子后,集合就会进行自动扩容。
ArrayList
初始容量:10
扩容因子:1,当容量达到上限后才会触发扩容。
扩容公式:old + (old>>1)[>>1是位运算,右移一位相当于原数的1/2]
如:一次扩容为15、二次扩容为22
Vector
初始容量:10
扩容因子:1,当容量达到上限后才会触发扩容。
扩容公式:old*2
如:一次扩容为20、二次扩容为40
HashSet
初始容量:16
扩容因子:0.75,当容量达到最大容量的75%时就会触发扩容。
扩容公式:old*2
如:一次扩容32,二次扩容64
HashMap
初始容量:16
扩容因子:0.75,当容量达到最大容量的75%时就会触发扩容。
扩容公式:old*2
如:一次扩容32,二次扩容64
我们也可以通过在创建集合对象时指定集合的初始容量。