集合概述
集合与数组的区别:
① 长度:
数组的长度是固定的
集合的长度是可变的(自动伸缩)
② 存储类型:
数组可以存基本数据类型,也可以存引用数据类型
集合可以存引用数据类型,但若要存基本数据类型,需要将其转换为对应的包装类
创建集合的对象:
泛型:限定集合中存储数据的类型
以ArrayList示例:
ArrayList<E> list = new ArrayList<>(); //E为引用数据类型或者包装类
打印对象时,不是打印地址值,而是集合中存储的数据内容,在展示的时候会拿[]把所有的数据进行包裹
基本数据类型对应的包装类:
| 基本数据类型 | 对应的包装类 |
|---|---|
| byte | Byte |
| short | Short |
| char | Character |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| boolean | Boolean |
泛型
泛型:是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
泛型的格式:<数据类型>
注意:泛型只能支持引用数据类型
推出泛型的原因:如果没有给集合指定类型,默认认为所有的数据类型都是Object类型,此时可以给集合添加任意的数据类型,带来一个坏处:我们在获取数据的时候,无法使用它的特有行为(多态的缺点)。此时推出了泛型,可以在添加数据的时候就把类型进行统一,而且我们在获取数据的时候,也不用强转
泛型的好处:
① 统一数据类型
② 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来
扩展知识点:Java中的泛型是伪泛型
当数据添加到集合中时,只是在添加前检查了数据的数据类型,但是添加完毕后,集合还是会把这些数据当作Object来处理,只是在往外获取时,会将数据按照泛型来进行类型的强转
代码中的体现:

泛型的细节:
① 泛型中不能写基本数据类型
② 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
③ 如果不写泛型,类型默认是Object
泛型可以在很多地方进行定义:
类后面——泛型类
方法上面——泛型方法
接口后面——泛型接口
泛型类:
使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类
格式:
修饰符 class 类名<类型> {
}
举例:
public class ArrayList<E> { //创建该类对象时,E就确定类型
}
此处E可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成:T、E、K、V
泛型方法:
方法中形参类型不确定时,有两种方案解决
方案①:使用类名后面定义的泛型(所有方法都可以使用)
方案②:在方法申明上定义自己的泛型(只有本方法能用)
格式:
修饰符<类型> 返回值类型 方法名(类型 变量名) {
}
举例:
public<T> void show(T t) {
}
此处T可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成:T、E、K、V
泛型接口:
格式:
修饰符 interface 接口<类型> {
}
举例:
public interface List<E> {
}
使用带泛型的接口的方式:
① 实现类给出具体类型
② 实现类延续泛型,创建对象时再确定
举例:
方式一:
public class MyArrayList<E> implements List<String> {
...
}
//创建对象
MyArrayList list = new MyArrayList();
方式二:
public class MyArrayList<E> implements List<E> {
...
}
//创建对象时需要确定类型
MyArrayList<String> list = new MyArrayList<>();
泛型的继承和通配符:
泛型不具备继承性,但是数据具备继承性
//Fu继承于Ye,Zi继承于Fu
ArrayList<Ye> list1 = new ArrayList<>();
ArrayList<Fu> list2 = new ArrayList<>();
ArrayList<Zi> list3 = new ArrayList<>();
ArrayList<Student> list4 = new ArrayList<>();
//method(list1);
//method(list2); 报错
//method(list3); 报错
/* public static void method(ArrayList<Ye> list) {
...
} 这段代码会报错,因为泛型不具备继承性*/
ArrayList<Ye> list = new ArrayList<>();
list.add(new Ye());
list.add(new Fu());
list.add(new Zi());
//代码不会报错,因为数据具备继承性
泛型的通配符:
?也表示不确定的类型,它可以进行类型的限定
? extends E:表示可以传递E或者E所有的子类类型
? super E:表示可以传递E或者E所有的父类类型
应用场景:
① 我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口
② 如果类型不确定,但是能知道以后只能传递某个继承体系,就可以使用泛型的通配符
集合体系结构
单列集合:在添加数据时,每次只能添加一个元素
双列集合:在添加数据时,每次添加的是一对数据
单列集合体系结构:

List系列集合:添加的元素是有序(指的是存和取的顺序,和之前的由小到大或由大到小排序不一样)、可重复、有索引
Set系列集合:添加的元素是无序、不重复、无索引
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,因为List系列是允许元素重复的
② 如果我们要往Set系列集合中添加数据,如果当前要添加的元素不存在,方法返回true,表示添加成功;如果当前要添加的元素已经存在,方法返回false,表示添加失败,因为Set系列集合不允许重复
remove方法细节:
① 因为Collection里面定义的是共性的方法,所以此时不能通过索引进行删除,只能通过元素的对象进行删除
② 方法会有一个布尔类型的返回值,删除成功返回true,删除失败返回false
contains方法细节:底层是以来equals方法进行判断是否存在的。所以,如果集合中存储的是自定义对象,也想通过contains方法来判断是否包含,那么在javabean类中,一定要重写equals方法
Collection的遍历方式:
1.迭代器遍历:
迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式
Collection集合获取迭代器:
| 方法名称 | 说明 |
|---|---|
Iterator<E>iterator() | 返回迭代器对象,默认指向当前集合的0索引 |
Iterator中的常用方法:
| 方法名称 | 说明 |
|---|---|
| boolean hasNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
| E next() | 获取当前位置的元素,并将迭代器对象移向下一个位置 |
Iterator<String> it = list.iterator();
while(it.hasNext()) {
String str = it.next();
System.out.println(str);
}
细节:
① 如果迭代器已经遍历完毕,再次调用next方法时,会报错NoSuchElementException
② 迭代器遍历完毕,指针不会复位,如果要继续第二次遍历集合,只能再次创建新的迭代器对象
③ 循环中只能用一次next方法,如果使用多次,可以会发生第一点的报错
④ 迭代器遍历时,不能用集合的方法进行增加或者删除,如果实在要删除,可以用迭代器提供的remove方法进行删除。只有当迭代器遍历结束时,才可以用集合的方法进行增加或者删除(如果使用集合的方法进行增加或删除,由于集合的结构发生了变化,应该去重新获取迭代器,但循环下一次的时候并没有重新获取迭代器,因此会报异常)
2.增强for遍历:
① 增强for的底层就是迭代器,为了简化迭代器的代码书写的
② 它是JDK5之后出现的,其内部原理就是一个Iterator迭代器
③ 只有所有的单列集合和数组才能用增强for进行遍历
格式:
for(元素的数据类型 变量名 : 数组或者集合) {
}
举例:
for(String s : list) {
System.out.println(s);
}
细节:修改增强for中的变量,不会改变集合中原本的数据
3.Lambda表达式遍历:
得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式
| 方法名称 | 说明 |
|---|---|
default void forEach(Consumer<? super T> action) | 结合lambda遍历集合 |
底层原理:也会自己遍历集合,依次得到每一个元素,把得到的每一个元素,传递给下面的accept方法
举例:
Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add("wangwu");
/*
coll.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
*/
coll.forEach(s -> System.out.println(s));
三种遍历方式的使用场景:
迭代器:在遍历的过程中需要删除元素,就要使用迭代器
增强for、Lambda:仅仅想遍历,那么使用增强for或Lambda表达式
List
List集合的特点:
有序:存和取的元素顺序一致
有索引:可以通过索引操作元素
可重复:存储的元素可以重复
List集合的特有方法:
Collection的方法List都继承了
List集合因为有索引,所以多了很多索引操作的方法
| 方法名称 | 说明 |
|---|---|
| void add(int index, E element) | 在此集合中的指定位置插入指定的元素 |
| E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
| E set(int index, E element) | 修改指定索引处的元素,返回被修改的元素 |
| E get(int index) | 返回指定索引处的元素 |
add方法细节:原来索引上的元素会依次往后移
remove方法细节:List系列集合中有两个删除方法,一个是直接删除元素,一个是通过索引进行删除,而若出现集合内部的数据元素类型为Integer时,list.remove(1)表示的意思是删除索引为1的元素,而不是删除1这个元素。因为在调用方法的时候,如果方法出现了重载现象,优先调用实参跟形参类型一致的那个方法。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1); //删除索引为1的元素
List集合的遍历方式:
1.迭代器遍历:
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
Iterator<String> it = list.iterator();
while(it.hasNext()) {
String str = it.next();
System.out.println(str);
}
2.增强for遍历:
for(String s : list) {
System.out.println(s);
}
3.Lambda表达式遍历:
list.forEach(s -> System.out.println(s));
4.普通for循环遍历:
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
5.列表迭代器遍历:
ListIterator<String> it = list.listIterator(); //获取一个列表迭代器对象
while(it.hasNext()) {
String str = it.next();
if("bbb".equals(str)) {
it.add("qqq"); //[aaa, bbb, qqq, ccc]
}
}
列表迭代器额外添加了一个方法add,在遍历的过程中,可以添加元素
五种遍历方式对比:
迭代器遍历:在遍历的过程中需要删除元素,请使用迭代器
列表迭代器:在遍历的过程中需要添加元素,请使用列表迭代器
增强for遍历、Lambda表达式:仅仅想遍历,那么使用增强for或Lambda表达式
普通for:如果遍历的时候想操作索引,可以用普通for
ArrayList
ArrayList常用成员方法:
| 方法名 | 说明 |
|---|---|
| boolean add(E e) | 添加元素,返回值表示是否添加成功 |
| boolean remove(E e) | 删除指定元素,返回值表示是否删除成功 |
| E remove(int index) | 删除指定索引的元素,返回被删除元素 |
| E set(int index, E e) | 修改指定索引下的元素,返回原来的元素 |
| E get(int index) | 获取指定索引的元素 |
| int size() | 集合的长度,也就是集合中元素的个数 |
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系列集合添加的元素是无序、不重复、无索引的
无序:存取顺序不一致
不重复:可以去除重复
无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素
Set接口中的方法基本上与Collection的API一致
HashSet
HashSet集合底层采取哈希表存储数据
哈希表是一种对于增删改查数据性能都较好的结构
哈希表组成:
JDK8之前:数组 + 链表
JDK8开始:数组 + 链表 + 红黑树
哈希值:
是根据hashCode方法算出来的int类型的整数
该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值
对象的哈希值特点:
如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
在小部分情况,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)
HashSet底层原理:
① 创建一个默认长度16,默认加载因子为0.75(指的是当存入元素数量为数组长度的0.75倍时,数组会扩容),数组名为table
② 根据元素的哈希值跟数组的长度计算出应存入的位置
int index = (数组长度 - 1) & 哈希值;
③ 判断当前位置是否为null,如果是null直接存入
④ 如果位置不为null,表示有元素,则调用equals方法比较属性值
⑤ 一样:不存;不一样:存入数组,形成链表
JDK8以前:新元素存入数组,老元素挂在新元素下面
JDK8以后:新元素直接挂在老元素下面
JDK8以后:当链表长度超过8,而且数组长度大于等于64时,自动转换为红黑树
如果集合中存储的是自定义对象,必须要重写hashCode和equals方法

HashSet的三个问题:
① HashSet为什么存和取的顺序不一样?
因为取的时候,是从数组的0索引开始,依次遍历每个位置上的链表,一直遍历到数组最后,而存的时候并非一定是按照数组索引顺序而存入的,是由哈希值计算出的位置决定的
② HashSet为什么没有索引?
因为数组中一个位置上可能有多个元素,因此不好分配索引
③ HashSet是利用什么机制保证去重的?
HashCode方法和equals方法
LinkedHashSet
特点:有序(存储和取出的元素顺序一致)、不重复、无索引
原理:底层数据结构依然是哈希表,只是每个元素又额外地多了一个双向链表的机制记录存储的顺序,遍历的时候是从头指针开始遍历,依次遍历到尾指针

在以后如果要数据去重,我们默认使用HashSet,如果要求去重且存取有序,才使用LinkedHashSet
TreeSet
特点:不重复、无索引、可排序(按照元素的默认规则(由小到大)进行排序)
原理:TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
TreeSet集合默认的规则:
对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序
对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序
TreeSet的两种比较方式:
如果是自定义对象,则需要使用以下方式进行排序,否则往TreeSet里添加元素时会报错
方式一:默认排序 / 自然排序:Javabean类实现Comparable接口指定比较规则
需要类实现Comparable接口,重写里面的抽象方法,再指定比较规则
@Override
public int compareTo(Student o) {
//根据年龄进行排序
return this.getAge() - o.getAge();
}
/*
this:表示当前要添加的元素
o:表示已经在红黑树存在的元素
返回值:
负数:认为要添加的元素是小的,存左边
正数:认为要添加的元素是大的,存右边
0:认为要添加的元素已经存在,舍弃
*/
方式二:比较器排序:创建TreeSet对象时候,传递比较器Comparator指定规则
使用原则:默认使用第一种,如果第一种不能满足当前需求,就使用第二种
若两种方式同时使用了,是以第二种为准
TreeSet<String> ts = new TreeSet<>(new Comparator<String>) { //创建对象时传递一个比较器对象
@override
public int compare(String o1, String o2) {
//按照长度排序
int i = o1.length() - o2.length();
//如果一样长则按照首字母排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
}
}
使用场景
1.如果想要集合中的元素可重复
用ArrayList集合,基于数组的(用的最多)
2.如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
用LinkedList集合,基于链表的
3.如果想对集合中的元素去重
用HashSet集合,基于哈希表的(用的最多)
4.如果想对集合中的元素去重,而且保证存取顺序
用LinkedHashSet集合,基于哈希表和双向链表,效率低于HashSet
5.如果想对集合中的元素进行排序
第二种为准
TreeSet<String> ts = new TreeSet<>(new Comparator<String>) { //创建对象时传递一个比较器对象
@override
public int compare(String o1, String o2) {
//按照长度排序
int i = o1.length() - o2.length();
//如果一样长则按照首字母排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
}
}
本文详细介绍了Java集合框架中的关键概念,如集合与数组的区别、泛型的使用及其优点、单列集合与双列集合的结构、以及ArrayList、LinkedList、Set、HashSet、LinkedHashSet和TreeSet等集合的使用场景、方法和遍历方式。重点讲解了泛型在编译期类型校验和避免异常的优点。

被折叠的 条评论
为什么被折叠?



