Java基础学习(十):集合
- Java基础学习(十):集合
- 概念
- 数据结构
- 泛型
- Collection
- List
- ArrayList
- LinkedList
- Set
- HashSet
- LinkedHashSet
- TreeSet
本文为个人学习记录,内容学习自 黑马程序员
概念
-
数组长度固定,不够灵活,因此出现了集合
-
集合 vs 数组
- 长度:数组的长度固定,集合长度可变(会在添加数据时自动扩容,删除数据时自动压缩)
- 存储类型:数组可以存储所有数据类型,集合只能存储引用数据类型,如果要存储基本数据类型的话需要将其转换成对应包装类
-
集合可以分成两大类:单列集合Collection 和 双列集合Map
-
单列集合中的常用集合:
- List 系列集合:添加的元素是有序、可重复、有索引的
- Set 系列集合:添加的元素是无序、不重复、无索引的
- 下图中 Collection,List,Set为接口,其余为实现类,箭头表示继承/实现关系
数据结构
-
数据结构是计算机底层存储、组织数据的方式
-
数据结构是为了更加方便地管理和使用数据,需要结合具体的应用场景进行选择,合适的数据结构能带来更高的运行或存储效率
-
常见数据结构:栈,队列,数组,链表,二叉树,二叉查找树,平衡二叉树,红黑树
-
栈:后进先出,先进后出
-
队列:先进先出,后进后出
-
数组:查询快,增删慢
- 查询速度快:通过地址值找到数组,再通过索引找到元素,查询任意数据耗时相同,元素在内存中连续存储
- 删除效率低:要将原始的数据删除,同时后面每个数据前移
- 添加效率低:添加位置后地每个元素后移,再添加元素
-
链表:查询慢,增删快
-
链表中的每一个元素称为结点/节点(node),每个结点都是独立对象,在内存中不连续,结点中存储具体数据和下一个结点的地址
-
查询速度慢:无论哪个数据都要从头开始查找
-
删除和添加效率高:只要修改结点指向的地址就可以了
-
单向链表 vs 双向链表:双向链表中不仅存储了具体值和下一个结点地址,还存储了上一结点地址,能提高查找效率
-
-
树
-
基本概念:
-
树中的每个 节点 都是一个独立对象
-
节点 A 如果下方连接了节点 B 和 C,则称节点 A 为父节点,节点 B 和 C 从左到右排列时称 B 为左子节点,C 为右子节点
-
节点中存储了当前节点的数据、父节点地址、左子节点地址和右子节点地址,如果不存在则记为 null
-
将节点的子节点数量称为 度,例如二叉树中任意节点的度<= 2
-
将树的总层数称为 树高
-
将树的最顶层节点称为 根节点
-
将根节点的左子节点及其以下的内容称为 根节点的左子树,同理还有 根节点的右子树,其它节点也有左子树和右子树的概念
-
-
-
二叉树
- 满足任意节点最多有两个子节点的树为二叉树
- 普通二叉树的弊端:没有顺序要求,查找效率低下
- 二叉树遍历方式(适用于所有二叉树):
- 前序遍历:从根节点开始,按照当前节点-左子节点-右子节点的顺序遍历,例如上面的树遍历顺序为 20-18-16-19-23-22-24
- 中序遍历:从最左边的子节点开始,按照左子节点-当前节点-右子节点的顺序遍历,例如上面的树遍历顺序为 16-18-19-20-22-23-24,对二叉查找树进行中序遍历可以得到从小到大排列的数据,因此这种方法最为常用
- 后序遍历:从最左边的子节点开始,按照左子节点-右子节点-当前节点的顺序遍历,例如上面的树遍历顺序为 16-19-18-22-24-23-20
- 层序遍历:从根节点开始一层层遍历,例如上面的树遍历顺序为 20-18-23-16-19-22-24
-
二叉查找树:
- 又称二叉排序树或者二叉搜索树
- 满足三个条件:任意节点最多有两个子节点,任意节点左子树上的值都小于当前节点,任意节点右子树上的值都大于当前节点
- 数据存储规则:小的存左边,大的存右边,一样的不存
- 二叉查找树查找方式:从根节点开始查,如果目标值小于当前节点值,则向左子树查找,如果大于当前节点值,则向右子树查找
- 二叉查找树的优势:相比于普通二叉树提高了查找效率
- 二叉查找树的弊端:可能存在左子树和右子树高度相差过大的情况,会大大降低查找效率
-
平衡二叉树
-
平衡二叉树就是在二叉查找树的基础上,规定任意节点左右子树高度差不超过1
-
平衡二叉树的实现机制 —— 旋转机制
- 旋转:分成左旋(逆时针旋转)和右旋(顺时针旋转)
- 触发时机:当添加一个节点后,该树不再是一颗平衡二叉树时触发旋转
-
平衡二叉树旋转流程:
-
确定支点:从添加的节点开始,不断往父节点找不平衡的节点作为支点,将支点及其下的内容作为一个整体旋转,其余不变
-
确定旋转方向:将支点视为根节点,根据插入的节点位置分成四种情况:
- 左左:当根节点左子树的左子树有节点插入导致不平衡时,只需要一次右旋
- 左右:当根节点左子树的右子树有节点插入导致不平衡时,需要先将根节点的左子树单独进行一次左旋,旋转后就相当于”左左“的插入情况了,这时再进行一次右旋即可
- 右右:当根节点右子树的右子树有节点插入导致不平衡时,只需要一次左旋
- 右左:当根节点右子树的左子树有节点插入导致不平衡时,需要先将根节点的右子树单独进行一次右旋,旋转后就相当于”右右“的插入情况了,这时再进行一次左旋即可
-
进行旋转:如果是左旋,就将原来的右子节点升级到支点所在位置,原来的支点降级为新支点的左子节点;如果是右旋,就将原来的左子节点升级到支点所在位置,原来的支点降级为支点的右子节点
-
复杂情况:如果在左旋时,原来的右子节点已经有左子节点了,不会将该左子节点一块升级,而是作为降级后的新左子节点的右子节点;右旋同理
-
-
-
红黑树
-
红黑树是一种自平衡的二叉查找树,但它并不是平衡二叉树
-
红黑树的每个节点上都有存储位表示节点的颜色,每一个节点可以是红或者黑
-
红黑树不是高度平衡的,它的平衡是通过红黑规则实现的
-
红黑树 vs 平衡二叉树:平衡二叉树是高度平衡的,查找效率很高,但添加数据时由于可能存在多次旋转,添加效率不高;红黑树不一定是高度平衡的,但增删改查效率都很高
-
红黑树的红黑规则:
- 每一个节点或者是红色的,或者是黑色的
- 根节点必须是黑色
- 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,叶节点为黑色的
- 如果某一个节点是红色的,那么它的子节点必须是黑色的(不能出现两个红色节点相连的情况)
- 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
-
红黑树添加节点的规则:
-
默认颜色:添加节点默认是红色的
-
添加细则如下:
-
注意:“把父/祖父设置为当前节点再进行判断”:指的是对添加的节点进行处理后,还要再将父/祖父视为添加的节点,再次进行上述判断和处理
-
-
泛型
-
JDK5 引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
-
格式:
<数据类型>
-
泛型的出现:在使用集合时,如果不存在泛型,那么集合中的元素可以是任何引用数据类型,在底层都是使用 Object obj = input;来实现的,而多态是无法使用子类特有的方法的,虽然可以通过强制类型转换转换回子类对象,但每种类型都要强转实在太麻烦了。因此泛型被用于约束数据类型,虽然在输入时仍然是使用 Object obj = input;的伪泛型,但在输出时会自动帮我们强转成约束类型
-
优势:1. 统一数据类型 2. 把运行时期可能出现的问题提前到了编译时期
-
注意事项:
- 泛型只能支持引用数据类型
- 指定泛型的具体类型后,传递数据时可以传入该类型或者其子类类型
- 创建集合对象时如果不指定泛型,默认是 Object 类型
- 泛型中可以指定多个变量,如 <E, T>
-
泛型可以在很多地方定义:当泛型写在类后面时,是泛型类;写在方法上面时,是泛型方法;写在接口后面时,是泛型接口
-
泛型类
-
使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类
-
格式:
修饰符 class 类名<类型> {}
-
说明:此处的 E 用于记录数据类型,一般写成 T(Type)、E(Element)、K(Key)、V(Value)等
-
示例:
public class MyArrayList<E> {Object[] obj = new Object[10];int size;public boolean add(E e) {obj[size] = e;return true;} }
-
-
泛型方法
-
使用场景:当一个方法中,某个变量的数据类型不确定时,可以定义带有泛型的方法
-
格式:
修饰符 <类型> 返回值类型 方法名(形参) {}
-
示例:
public class MyArrayList {public static <E> void add(E e) {// 方法体} }
-
-
泛型接口
-
使用场景:当一个接口中,某个变量的数据类型不确定时,可以定义带有泛型的接口
-
格式:
修饰符 interface 接口名<类型> {}
-
两种使用方式:
-
实现类给出具体类型
public class MyArrayList implements List<String> {// 重写方法,方法中不存在泛型 }
-
实现类延续泛型,创建实现类对象时再确定类型
public class MyArrayList<E> implements List<E> {// 重写方法,方法中存在泛型 }
-
-
-
-
泛型的通配符
-
? 表示不确定的类型
-
在 ? 的基础上可以进一步限定类型
-
?
:表示可以传递任何类型注意:使用 ? 表示能传递任何类型时,不需要在修饰符和返回值类型之间写泛型
public static void method(ArrayList<?> list) {// 方法体 }
-
? extends E
:表示可以传递 E 或者 E 所有的子类类型public static void method(ArrayList<? extends GrandFather> list) {// 方法体 }
-
? super E
:表示可以传递 E 或者 E 所有的父类类型public static void method(ArrayList<? super Son> list) {// 方法体 }
-
-
-
应用场景:如果在定义类、方法、接口时存在不确定的类型,就可以定义泛型类、泛型方法、泛型接口;如果类型不确定,但能传递的参数在某个继承结构中,就可以使用泛型的通配符
Collection
-
Collection 是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的
-
常用方法:
方法名 说明 public boolean add(E e) 把给定的对象添加到当前集合中 public void clear() 清空集合中所有的元素 public boolean remove(E e) 把给定的对象从当前集合中删除 public boolean contains(Object obj) 判断当前集合中是否包含给定的对象 public boolean isEmpty() 判断当前集合是否为空 public int size() 返回集合中元素的个数 -
注意事项:
- add() 方法的返回值表征添加成功还是失败,如果是 List 系列的集合返回值一定是 true,如果是 set 系列的集合需要看要添加的元素是否已经存在,若已经存在则会添加失败,返回值为 false
- remove() 方法的返回值表征删除成功还是失败,删除成功返回 true,若要删除的元素不存在则会删除失败返回 false
- contains() 方法底层是调用 equals() 方法来判断是否存在的,所以当集合中存储自定义对象时,需要重写自定义对象的 equals() 方法,否则判断是否存在时不会根据属性值判断,而是根据地址值判断
-
遍历方式:迭代器遍历,增强型 for 循环遍历,Lambda 表达式遍历
-
需要注意,由于 Set 系列集合是没有索引的,因此 Collection 集合无法使用普通 for 循环进行遍历
-
迭代器遍历
-
路径:
java.util.Iterator
-
迭代器是集合专用的遍历方式
-
Collection 集合获取迭代器对象
方法名 说明 Iterator<E> iterator() 返回迭代器对象,默认指向当前集合的 0 索引 -
Iterator 常用方法:
方法名 说明 boolean hasNext() 判断当前位置是否有元素,有元素返回 true,否则返回 false E next() 获取当前位置元素,并将迭代器对象移向下一个位置 default void remove() 从集合中删除迭代器对象返回的最后一个元素(当前指向元素的前一个元素) -
注意事项:
- 若迭代器当前指向的位置没有元素,调用 next() 方法会报错:NoSuchElementException
- 迭代器遍历完指针不会复位
- 迭代器遍历时,不能用集合的方法进行增加或删除元素,但可以用迭代器自身的方法删除元素
-
示例:
ArrayList<String> list = new ArrayList<>(); Iterator<String> it = list.iterator(); while (it.hasNext()) {String str = it.next(); }
-
-
增强型 for 循环遍历
-
增强型 for 循环的底层就是通过迭代器实现的,是为了简化迭代器的代码书写才出现的
-
只有单列集合和数组能够使用增强型 for 循环遍历
-
示例:
ArrayList<String> list = new ArrayList<>(); // 注意事项:str是一个第三方变量,就算修改它的值也不会对数组/集合的元素发生影响 for (String str: list) {System.out.println(str); }
-
-
Lambda 表达式遍历
-
得益于 JDK8 提出的 Lambda 表达式,提供了一种更简单的遍历集合方式
-
使用方法:
方法名 说明 default void forEach(Consumer<? super T> action): 结合 Lambda 遍历集合 -
不采用 Lambda 表达式时:
ArrayList<String> list = new ArrayList<>(); list.forEach(new Consumer<String>() {@Overridepublic void accept(String s) { // s依次表示集合中的每一个元素,同样也是第三方变量System.out.println(s);} });
-
采用 Lambda 表达式的完整形式时(可以根据具体情况继续简化):
ArrayList<String> list = new ArrayList<>(); list.forEach((String s) -> {System.out.println(s);} );
-
-
总结:通常使用增强 for 循环或者 Lambda 表达式实现遍历,如果需要在遍历时删除元素才使用迭代器遍历
-
List
-
特点:有序 —— 存和取的元素顺序一致,有索引 —— 可以通过索引操作元素,可重复 —— 存储的元素可以重复
-
List 集合在继承了 Collection 的基础上,多了很多索引操作的方法
-
List 中特有的常见方法:
方法名 说明 void add(int index, E element) 在此集合中的指定位置插入指定的元素 E remove(int index) 删除指定索引处的元素,返回被删除的元素 E set(int index, E element) 修改指定索引处的元素,返回被修改的元素 E get(int index) 返回指定索引处的元素 -
注意事项:
List 集合有两个 remove() 方法,一个的形参是 int 类型的索引,另一个是元素的类型,前者是根据索引删除元素,后者是根据元素值删除元素。当元素类型是 Integer 时,实参为 int 类型的数据时两种方法都可以调用,此时优先调用“实参类型和形参类型完全一致的方法”,也就是说优先根据索引删除元素
-
List 集合的遍历方式:迭代器遍历,列表迭代器遍历,增强型 for 循环遍历,Lambda 表达式遍历,普通 for 循环遍历
-
迭代器遍历,增强型 for 循环遍历,Lambda 表达式遍历和 Collection 中的遍历一致
-
普通 for 循环遍历
ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < list.size(); i++) {String s = list.get(i); }
-
列表迭代器遍历
-
路径:
java.util.ListIterator
-
List 集合获取迭代器对象
方法名 说明 ListIterator<E> listIterator() 返回列表迭代器对象,默认指向当前集合的 0 索引 -
ListIterator 常用方法:
方法名 说明 boolean hasNext() 判断当前位置是否有元素,有元素返回 true,否则返回 false E next() 获取当前位置元素,并将迭代器对象移向下一个位置 void add(E e) 将指定元素插入到集合中,插入位置为当前指向的位置,其余元素后置 void remove() 从集合中删除迭代器对象返回的最后一个元素
-
-
总结:在遍历时需要删除元素时使用迭代器,在遍历时需要添加元素时使用列表迭代器,仅仅想遍历时使用增强 for 循环或者Lambda 表达式,如果遍历时需要操作索引则使用普通 for 循环
-
ArrayList
-
路径:
java.util.ArrayList
-
底层数据结构是 数组
-
定义格式:
集合类型<数据类型> 集合名 = new 集合类型<数据类型>();
// <E>:泛型,用于限定集合中的数据类型,E表示具体的数据类型;泛型可以省略,省略后该集合能添加任意类型的数据 // 在JDK7之后,后面的String可以省略,但是尖括号不能省略 // 调用无参构造时,默认生成一个初始容量为 10 的空列表 ArrayList<String> list = new ArrayList<String>();
-
常用方法:
方法名 说明 boolean add(E e) 添加元素,返回值表示是否添加成功 boolean remove(E e) 删除指定元素,返回值表示是否删除成功 E remove(int index) 删除指定索引的元素,返回被删除元素 E set(int index, E e) 修改指定索引下的元素,返回原来的元素 E get(int index) 获取指定索引的元素 int size() 返回集合长度,也就是集合中元素的个数 ArrayList<String> list = new ArrayList<>(); list.add("A"); // 由于add方法返回值一定为true,因此不需要用到该返回值 list.add("B"); list.add("C"); list.add("A"); System.out.println(list); // 输出为 [A, B, C, A] boolean result = list.remove("A"); // 当需要删除的元素存在时返回true,否则返回false System.out.println(list); // 输出为 [B, C, A],表明存在多个要删除的元素时,只删除第一个 String str = list.set(1, "D"); System.out.println(list); // 输出为 [B, D, A]
-
示例:使用集合存储基本数据类型 —— 包装类的应用
ArrayList<Integer> list = new ArrayList<>(); list.add(1); // 在JDK5之后int和Integer可以互相转化
-
扩展 —— 底层原理:
- 利用空参创建的集合,在底层创建一个默认长度为 0 的数组
- 添加第一个元素时,底层会创建一个新的长度为 10 的数组
- 存满时,扩容为原来的 1.5 倍
- 如果一次添加多个元素,扩容 1.5 倍后还是放不下,则具体扩容大小以添加元素为准
LinkedList
-
路径:
java.util.LinkedList
-
底层数据结构是 双向链表
-
由于双向链表操作首尾元素时速度很快,因此 LinkedList 中提供了很多直接操作首尾元素的特有 API
-
常用特有方法:
特有方法 说明 public void addFirst(E e) 在该列表开头插入指定的元素 public void addLast(E e) 将指定的元素追加到此列表的末尾 public E getFirst() 返回此列表中的第一个元素 public E getLast() 返回此列表中的最后一个元素 public E removeFirst() 从此列表中删除并返回第一个元素 public E removeLast() 从此列表中删除并返回最后一个元素
Set
- 特点:无序 —— 存取顺序不一致,无索引 —— 没有带索引的方法,不重复 —— 可以用来去重复
- Set 是一个接口,其中的方法基本上与 Collection 中的 API 一致
HashSet
- 路径:
java.util.HashSet
- 特点:无序,不重复,无索引
- 底层数据结构是 哈希表
- 哈希表是一种对于增删改查数据性能都较好的结构,在 JDK8 之前哈希表由数组+链表组成,从 JDK8 开始由数组+链表+红黑树组成
- 哈希值:对象的整数表现形式
- 哈希值是根据 hashCode() 方法计算出来的 int 类型的整数
- 该方法定义在 Object 类中,所有对象都可以调用,默认使用地址值进行计算,因此不同对象的哈希值是不同的
- 一般情况下,会重写 hashCode() 方法,利用对象内部的属性值计算哈希值,因此只要属性值相同哈希值就相同
- 在小部分情况下,不同属性或者不同地址计算出来的哈希值也有可能一样(哈希碰撞)
- HashSet 的底层原理
- 创建集合对象并添加元素:
- 创建一个默认长度为 16 ,默认加载因子为 0.75 的数组(加载因子用于数组扩容)
- 根据元素的哈希值跟数组的长度计算出应存入的位置:
int index = (数组长度 - 1) & 哈希值
- 判断当前位置是否为 null,如果是 null 则直接存入,否则调用 equals() 方法比较对象内部的属性值,如果属性值一样就直接舍弃,如果不一样会存入数组形成链表
- 形成链表的具体方式根据 JDK 版本有所不同,在 JDK8 之前是将新元素存入数组,然后将老元素挂在新元素下面;从 JDK8 开始变成了新元素直接挂在老元素下面
- 从 JDK8 开始,当链表长度超过8,且数组长度大于等于64时,链表会自动转换成红黑树
- 注意事项:如果集合中存储的是自定义对象,那么必须重写 hashCode() 和 equals() 方法
- 创建集合对象并添加元素:
LinkedHashSet
- 路径:
java.util.LinkedHashSet
- 特点:有序,不重复,无索引
- 底层数据结构是 哈希表,只是每个元素又额外多了一个双向链表的机制记录存储的顺序
- LinkedHashSet 的有序:HashSet 在遍历时是根据数组索引对数组中的元素和链表进行挨个输出的,由于哈希值的存在这种遍历是无序的;而 LinkedHashSet 引入了双向链表记录了存储顺序(每个元素都记录了前一个、后一个元素的地址值),因此能够有序输出
TreeSet
-
路径:
java.util.TreeSet
-
特点:可排序,不重复,无索引
-
底层数据结构是 红黑树
-
TreeSet 可排序:按照元素的默认规则排序
- 对于数值类型:默认按照从小到大的顺序进行排序
- 对子字符、字符串类型:按照字符在 ASCII 码表中的数字升序进行排序,字符串对字符逐个比较,例如:"aaa" < "ab" < "aba"
- 对于自定义类:必须指定比较规则,否则在添加元素时会报错
-
自定义类指定比较规则:
-
使用原则:默认使用第一种,只有当第一种方法不能满足比较规则时才使用第二种。比如需要使用自定义规则对字符串进行排序时,由于在 java 中已经对字符串进行了第一种方法的实现(默认按升序排列),此时就需要使用第二种方法
-
两种方法的区别:第一种方法采用的是 TreeSet 类的空参构造,第二种方法采用的是有参构造且传入比较器
-
方法一:默认排序/自然排序:Javabean 类实现 Comparable 接口指定比较规则
// 实现Comparable接口,指定泛型为自身 public class Student implements Comparable<Student> {private int age;// 构造方法public Student() {}public Student(int age) {this.age = age;}// get/set方法public int getAge() {return age;}public void setAge(int age) {this.age = age;}// 重写抽象方法@Overridepublic int compareTo(Student o) {// this表示当前要添加的元素,o表示已经在红黑树中的元素,比较时从根节点开始比// 返回值为负表明当前要添加的元素是小的,存在左边;返回值为正表明当前要添加的元素是大的,存在右边;返回值为0舍弃// 此处按照年龄升序排列int result = this.getAge() - o.getAge();return result;} }
-
方法二:比较器排序:创建 TreeSet 对象时,传递比较器 Comparator 指定规则
示例:按照字符串长度进行升序排列
// 由于Comparator是接口,使用匿名内部类的方式传递参数(也可以使用Lambda表达式简化) TreeSet<String> set = new TreeSet<>(new Comparator<String>() {// o1表示当前要添加的元素,o2表示已经在红黑树中的元素,比较时从根节点开始比// 返回值为负表明当前要添加的元素是小的,存在左边;返回值为正表明当前要添加的元素是大的,存在右边;返回值为0舍弃// 此处按照字符串长度升序排列@Overridepublic int compare(String o1, String o2) {int result = o1.length() - o2.length();return result;} });
-
-
总结:不同集合的使用场景:
- ArrayList:集合的元素可以重复时
- LinkedList:集合中元素可以重复,且增删操作明显多于查询
- HashSet:如果想对集合中的元素去重
- LinkedHashSet:如果想对集合中的元素去重,且保证存取顺序
- TreeSet:如果想对集合中的元素去重并进行排序