集合
学习内容
什么是集合?
- 集合其实就是一个容器,用来存放引用类型的数据
- 使用不同的集合等同于使用不同的数据结构
集合用来存放什么?
- 集合中存放了引用类型(对象的内存地址),集合不能存储基本数据类型
集合分类
- 以单个的方式存储元素 ——> java.util.Collection(超级父接口)
- 以键值对的方式存储元素 ——> java.util.Map(超级符接口)
Collection集合
以单个的方式存储元素
Collection集合继承关系
Iterator接口
想要迭代集合就得先了解集合的迭代器Iterator
- 包路径: java.util.Iterator<E>
- 方法:
方法 | 功能 |
---|---|
boolean hasNext() | 判断迭代器指向的集合中是否还有元素可迭代 |
E next() | 返回迭代的下一个元素 |
void remove() | 从迭代器指向的 collection 中移除迭代器返回的最后一个元素 并更新迭代器中保存的集合修改次数 |
迭代集合
通过Collection集合继承结构图可以看出,Colletion集合实现了Iterable接口,表示所有的集合都是可迭代的,每个集合都有一个iterator()方法,集合调用这个方法会返回一个迭代器(Iterator),下面我们用ArrayList集合来测试一下这个迭代器
public static void main(String[] args) {
// 给arrayList初始化容量5
ArrayList arrayList = new ArrayList(5);
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
// 获取集合的迭代器
Iterator it = arrayList.iterator();
// 迭代集合,并向控制台输出集合中的元素
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
}
对于迭代器Iterator的底层工作原理是怎样的呢,我们来看下面的这张图片
foreach的底层实现是迭代器
package com.jsoft.collection;
import java.util.ArrayList;
public class TestIterator {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
// 使用foreach遍历list集合
for(String str : list) {
System.out.println(str);
}
}
}
上面的代码编译后字节码文件的反编译结果如下,可以看出foreach底层实现是迭代器
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jsoft.collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public Test() {
}
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("1");
Iterator it = list.iterator();
while(it.hasNext()) {
String eleStr = (String)it.next();
if ("1".equals(eleStr)) {
it.remove();
}
}
System.out.println(list);
}
}
集合结构发生变化时迭代器需要重新获取
使用Collection集合的iterator()方法,会返回一个Iterator的实例(集合内部类Itr的实例),这个实例又被称作为迭代器,这个迭代器实例在实例化时将集合结构修改次数用变量(expectedModCount)保存起来(看下图,Java8)
可以把这个迭代器看作是一个 “集合快照”,并且在集合结构发生改变时,迭代器对象中的(expectedModCount变量)并未一起更新,所以需要重新获。
如果迭代器并未重新获取,在迭代器遍历集合的下一个元素时(调用next()时),检测到迭代器中保存的修改次数和集合中保存的修改次数不同时,会抛出ConcurrentModificationException(并发修改异常),具体请看下图(Java8)
使用迭代器迭代集合删除元素时,要使用Iterator的remove()
引子——为什么使用迭代器迭代集合时,不能使用集合中的remove()方法去删除元素?
使用集合的remove()方法并不会更新此集合迭代器对象中保存的集合的修改次数,使用remove()删除元素后,会在下一次调用next()时会抛出异常
使用Iterator的remove()方法时,会连同迭代器中保存的修改次数一同更新。(下图Java8)
- 使用集合中的remove()删除元素
package com.jsoft.collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("1");
// 获取list集合的迭代器对象
Iterator<String> it = list.iterator();
// 使用it迭代list集合
while(it.hasNext()) {
// 获取迭代器指向的集合的下一个元素
String eleStr = it.next();
// 使用集合list的remove()删除所有的"1"
if ("1".equals(eleStr)) {
list.remove(eleStr);
}
}
// 向控制台中打印结果
System.out.println(list);
}
}
结果如下,在代码的第19行(调用next()时)抛出异常
- 使用Iterator(迭代器)的remove()
package com.jsoft.collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("1");
// 获取list集合的迭代器对象
Iterator<String> it = list.iterator();
// 使用it迭代list集合
while(it.hasNext()) {
// 获取迭代器指向的集合的下一个元素
String eleStr = it.next();
// 使用迭代器it的remove()删除所有的"1"
if ("1".equals(eleStr)) {
it.remove();
}
}
// 向控制台中打印结果
System.out.println(list);
}
}
结果如下,并未抛出异常
总结一下
在使用foreach和迭代器遍历/迭代集合时,不要使用集合的remove()方法删除元素
- 对于迭代器迭代集合时,要使用自身的remove()方法去删除元素
- 对于foreach遍历集合时,不要删除元素
Colleciton接口
- 包路径:java.util.Collection<E>
- 方法:
contains()和remove()注意点
集合中contains()、remove()方法底层都调用了equals()方法,因为Object中equal()使用“=="进行比较,所以需要重写equals()方法,设置类型实例相等条件
List接口
- 包路径: java.util.List<E>
- List接口特有方法
ArrayList类
- ArrayList集合采用了数组这种数据结构,底层实际是一个Object[]
- ArrayList集合默认容量为10,在添加第一个元素时将这个默认容量给底层数组
- ArrayList集合扩容:扩容到原先的1.5倍
- 数组这种数据结构优缺点:
- 优点:检索元素效率极高,因为数组元素在内存中存储是连续的,并且数组中每个元素所占空间大小都是相同的。我们知道了数组的内存地址,知道了数组元素的下标,知道了数组元素所占字节大小,就可以通过一个数学表达式计算出指定下标元素的内存地址,所以数组检索元素效率极高
- 缺点:随机增删效率较低,为了保证数组元素在内存中是连续的这种特点,我们随机添加或删除一个元素,会涉及到后面所有元素整体向前或向后位移操作,所以效率较低;不不能存储大容量数据,因为很难在内存空间中找到一份大且连续的空间
ArrayList集合的默认容量
通过注释我们可以了解到ArrayList底层是elementData(Object[]),并且在添加第一个元素时,任何elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA都将扩展为DEFAULT_CAPACITY
我们来看具体代码
结论: ArrayList集合默认容量为10,在添加第一个元素时将这个默认容量给底层数组
LinkedList类
- 底层采用了双向链表这种数据结构
- 无默认容量
- 双向链表这种结构的优缺点
- 优点:随机增删元素效率较高,由于双向链表中的元素(节点)在内存空间中的内存地址是不连续的,随机增删元素并不涉及到整体元素位移操作,所以效率高
- 缺点:检索元素效率极低,由于双向链表中的元素(节点)在内存空间中的内存地址是不连续的,想要检索一个元素需要从头节点开始依此遍历,所以效率极低
Vector类
- 底层采用了数组这种数据结构
- 默认容量为10
- 数组扩容:扩容原来的2倍
- 与ArrayList的区别 ----> 线程安全(Vector中所有的方法都被synchronized关键字修饰)
已过时,由于Verctor中的方法都被synchronized修饰所以效率较低,现在保证线程安全有其他的方式了。----> 使用java.util.Collections中的静态方法
Set接口
Set集合存储元素的特点:无序无可重复
- 包路径:java.util.Set<E>
- 方法 ----> 继承自Collection接口的方法,Set接口无特有方法
HashSet类
- HashSet集合底层采用了哈希表这种数据结构
- HashSet在实例化时,底层实际实例化了个HashMap
- 往HashSet集合中存储元素,实际存放到了HashMap集合的key部分
SortedSet接口
SortSet集合中继承了HashSet接口的特点(也是无序不可重复的),并且SortSet集合中的元素都是可排序的(自动排序)
TreeSet类
- TreeSet集合继承了SortedSet接口的特点(无序不可重复可自动排序)
- TreeSet集合底层采用了二叉树这种数据结构
- TreeSet在实例化时,底层实际实例化了个TreeMap
- 往TreeSet集合中存储元素,实际时存放到了TreeMap集合的key部分