集合
引入
认识集合
- 集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,开发中也非常常用。
- 如下图所示,这些都是集合
集合体系结构
- 集合可以分为单列集合(collection),和双列结合(map)
- Collection代表单列集合,每个元素(数据)只包含一个值。
- Map代表双列集合,每个元素包含两个值(键值对)。
单列集合体系结构
Collection<E>为父接口,下面有两个子接口List<E>、Set<E>,分别用于实现两种类型的集合- Collection集合特点
- ArrayList、LinekdList :有序、可重复、有索引。
- Set系列集合:添加的元素是无序、不重复、无索引。
- HashSet: 无序、不重复、无索引;
- LinkedHashSet: 有序、不重复、无索引。
- TreeSet:按照大小默认升序排序、不重复、无索引。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Demo091 {
public static void main(String[] args) {
//目标:演示集合特点
//List系列:有序,可重复,有索引
List<String> list = new ArrayList<>();
list.add("a");
list.add("a");
list.add("b");
list.add("c");
list.add("x");
list.add("h");
//list添加元素顺序就是排序顺序
System.out.println("list的数据:"+list); //a,a,b,c,x,h
//根据索引访问
System.out.println("下标0的元素值:"+list.get(0));
//set系列:无序,不重复,无索引
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("e");
set.add("e");
set.add("c");
set.add("d");
//无序
System.out.println("set的数据:"+set);//a b c d e
//无索引
// set.get(0) 报错,没有这样的方法,所以无索引
}
}
Collection集合
Collection集合常用的方法
- 为啥要先学Collection的常用方法?
- Collection是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的。
- Collection是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的。
package com.itheima._10集合常用方法;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
// 目标:搞清楚Collection提供的通用集合功能。
// 添加4个字符串元素
// 打印集合
// 获取集合的元素个数 size()方法
// 删除集合元素 remove(元素)
// 判断集合是否为空 isEmpty()
// 清空集合 clear()
// 判断集合中是否存在某个元素数据 contains(元素)
// 把集合转换成数组 toArray();
// 打印数组 Arrays.toString(数组)
public class Demo101 {
public static void main(String[] args) {
// 目标:搞清楚Collection提供的通用集合功能。
// 添加4个字符串元素
Collection<String> c = new ArrayList<>();
c.add("黑马");
c.add("java");
c.add("程序");
c.add("AI");
// 打印集合
System.out.println(c);
// 获取集合的元素个数 size()方法
System.out.println("集合的元素个数:"+c.size());
// 删除集合元素 remove(元素)
c.remove("AI");
System.out.println("移除AI元素后集合所有数据:"+c);
// 判断集合是否为空 isEmpty()
System.out.println("集合是否为空:"+c.isEmpty()); //false
// 清空集合 clear()
c.clear();
System.out.println("清空后,是否为空:"+c.isEmpty()); //true
// 判断集合中是否存在某个元素数据 contains(元素)
c.add("黑马");
c.add("java");
c.add("程序");
c.add("AI");
System.out.println("判断集合中是否有AI:"+c.contains("AI")); //true
System.out.println("判断集合中是否有php:"+c.contains("php")); //false
// 把集合转换成数组 toArray();
String[] array = c.toArray(new String[c.size()]);
// 打印数组 Arrays.toString(数组)
System.out.println(array); //打印内存地址
System.out.println(Arrays.toString(array));
}
}
单列集合遍历6种方式
Collection的遍历方式一:迭代器遍历
- 迭代器是用来遍历集合的专用方式(数组没有迭代器),在Java中迭代器的代表是Iterator。
- Collection集合获取迭代器的方法
方法名称 说明 Iterator iterator() 返回集合中的迭代器对象,该迭代器对象默认指向当前集合的第一个元素 - Iterator迭代器中常用方法
方法名称 说明 boolean hasNext() 询问当前位置是否有元素存在,存在返回true ,不存在返回false E next() 获取当前位置的元素,并同时将迭代器对象指向下一个元素处。
List<String> c = new ArrayList<>();
c.add("a");
c.add("b");
c.add("c");
c.add("d");
//遍历方式1:迭代器
// 语法1:获取迭代器 集合对象.iterator()返回集合中的迭代器对象,
// 语法2:获取迭代器 迭代器对象.hasNext() 返回当前位置是否有值,有返回true,否则返回false
// 语法3:获取迭代器 迭代器对象.next() 返回当前位置的元素值并且移动位置指向下一个元素
System.out.println("遍历方式1:迭代器");
Iterator<String> iterator;
iterator = c.iterator();
while(iterator.hasNext()){
String e = iterator.next();
System.out.println(e);
}
- 通过迭代器获取集合的元素,如果取元素越界会出现NoSuchElementException异常
Collection的遍历方式二:增强for循环
List<String> c = new ArrayList<>();
c.add("a");
c.add("b");
c.add("c");
c.add("d");
for (元素的数据类型 变量名 : 数组或者集合) {
}
Collection<String> c = new ArrayList<>();
...
for(String s : c)
System.out.println(s);
}
- 增强for可以用来遍历集合或者数组。
- 增强for遍历集合,本质就是迭代器遍历集合的简化写法。
List<String> c = new ArrayList<>(); c.add("a"); c.add("b"); c.add("c"); c.add("d"); //遍历方式2:fori遍历 System.out.println("遍历方式2:fori遍历"); for (int i = 0; i < c.size(); i++) { System.out.println(c.get(i)); } //遍历方式3:增强for遍历 System.out.println("遍历方式3:增强for遍历"); for (String s : c) { System.out.println(s); }
Collection集合的遍历方式三:Lambda表达式(内部类)
- 得益于JDK 8开始的新技术Lambda表达式,提供了一种更简单、更直接的方式来遍历集合。
- 需要使用Collection的如下方法来完成
方法名称 说明 default void forEach(Consumer<? super T> action) 结合lambda遍历集合 - 这里使用了
<?superT>下限统配泛型,限制了最小能处理的类型,避免精度丢失 - foreach的源码可以看到,本质上是一个fori循环
Collection<String> lists = new ArrayList<>();
...
lists.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
lists.forEach(s -> {
System.out.println(s);
});
//lists.forEach(s -> System.out.println(s));
List<String> c = new ArrayList<>();
c.add("a");
c.add("b");
c.add("c");
c.add("d");
//遍历方式5:lambda方式
System.out.println("遍历方式5:lambda方式");
c.forEach(s -> System.out.println(s));
//这个lambda中实现的接口中的成员方法是以调用一个已有对象的成员方法来实现的,
//可以简写为方法引用的形式
方法引用(实例方法引用格式:实例对象::实例方法)
::是方法引用运算符,用于简化 lambda 表达式。当 lambda 表达式中实现的方法只是调用一个对象或静态类中已有的方法时,可以用::替代。对象::方法或静态类::方法,参数可以省略
List<String> c = new ArrayList<>();
c.add("a");
c.add("b");
c.add("c");
c.add("d");
//遍历方式6:方法引用(实例方法引用格式:实例对象::实例方法)
System.out.println("遍历方式5:lambda方式");
c.forEach(System.out::println);
//这里System.out实际上就是一个输出对象,调用了里面的println方法
并发修改集合带来的问题
认识并发修改异常问题
-
遍历集合的同时又存在增删集合元素的行为时可能出现业务异常,这种现象被称之为并发修改异常问题。
-
用一个场景来说明
- 需求:现在假如购物车中存储了如下这些商品:Java入门,宁夏枸杞,黑枸杞,人字拖,特级枸杞,枸杞子。现在用户不想买枸杞了,选择了批量删除。
- 分析:
- 后台使用ArrayList集合表示购物车,存储这些商品名。
- 遍历集合中的每个数据,只要这个数据包含了“枸杞”则删除它。
- 输出集合看是否已经成功删除了全部枸杞数据了。
-
首先是fori的方案,这个方案用来说明并发修改带来的问题:
public class Test4 { public static void main(String[] args) { //遍历的同时删除元素导致异常问题,也叫并发修改问题 //并发:多线程运行 //并发修改: 一个线程在遍历,另一个在删除 //fori次数循环演示遍历同时删除的问题: 由于位置迁移部分数据没有删除 test1(); } //fori次数循环:实现遍历同时删除元素 private static void test1() { ArrayList<String> list = new ArrayList<>(); list.add("Java入门"); list.add("宁夏枸杞"); list.add("黑枸杞"); list.add("人字拖"); list.add("特级枸杞"); list.add("枸杞子"); //删除包含枸杞的数据 for (int i = 0; i < list.size(); i++) { // System.out.println(list.get(i));//i从0开始 if(list.get(i).contains("枸杞")){ list.remove(list.get(i)); } } System.out.println(list); //所有元素["Java入门","宁夏枸杞","黑枸杞","人字拖","特级枸杞","枸杞子"] //预期:["Java入门","人字拖"] //------------------------Debug结果------------------------ //遍历第1次:["Java入门","宁夏枸杞","黑枸杞","人字拖","特级枸杞","枸杞子"],i=0 //遍历第2次:["Java入门","黑枸杞","人字拖","特级枸杞","枸杞子"],i=1 //遍历第3次:["Java入门","黑枸杞","人字拖","特级枸杞","枸杞子"],i=2 //遍历第4次:["Java入门","黑枸杞","人字拖","枸杞子"],i=3 //遍历第5次:["Java入门","黑枸杞","人字拖","枸杞子"],i=4, 4<4不符合条件退出循环 //真实结果:[Java入门, 黑枸杞, 人字拖, 枸杞子] } }- 可以看到由于ArrayList是动态数组,所以在移除数组元素的时候,后面的元素会向前位移,数组的长度也会发生改变,与此同时,我们的循环操作还在继续
- 可以把循环操作和循环内的操作看作一个线程
- 可以把AarrayList在移除元素后动态调整数组看作一个线程
- 由于两个线程是并发的,所以在移除ArrayList中的元素时,元素会发生位移,和下标错位
- 可以看到由于ArrayList是动态数组,所以在移除数组元素的时候,后面的元素会向前位移,数组的长度也会发生改变,与此同时,我们的循环操作还在继续
-
其次是迭代器没有使用迭代器类中的remove方法,而是使用ArrayList中的remove方法
public class Test4 { public static void main(String[] args) { //遍历的同时删除元素导致异常问题,也叫并发修改问题 //并发:多线程运行 //并发修改: 一个线程在遍历,另一个在删除 test4(); } //迭代器循环演示遍历同时删除的问题 private static void test4() { ArrayList<String> list = new ArrayList<>(); list.add("Java入门"); list.add("宁夏枸杞"); list.add("黑枸杞"); list.add("人字拖"); list.add("特级枸杞"); list.add("枸杞子"); //删除包含枸杞的数据 Iterator<String> iterator = list.iterator(); while(iterator.hasNext()){ String next = iterator.next(); if(next.contains("枸杞")){ list.remove(next); } } System.out.println(list); } }- 会直接报异常
- 可以点入迭代器源码和ArrayList中查看,迭代器中next和ArrayListremove的源码
- ArrayList中的remove方法,里面的modCount自增了
- 迭代器中的next方法,可以看到Iterator中的next方法是会检查modCount是否修改过的,如果被修改直接抛异常
- ArrayList中的remove方法,里面的modCount自增了
- 会直接报异常
-
对于增强for循环并发修改导致的元素位移下标错位问题,本质是还是和fori的问题一样的,但这种遍历方式导致的问题无法解决,因为我们无法更改遍历方式,也无法拿到遍历的指针
public class Test4 { public static void main(String[] args) { //遍历的同时删除元素导致异常问题,也叫并发修改问题 //并发:多线程运行 //并发修改: 一个线程在遍历,另一个在删除 test6(); } //增强for循环演示遍历同时删除的问题 private static void test6() { ArrayList<String> list = new ArrayList<>(); list.add("Java入门"); list.add("宁夏枸杞"); list.add("黑枸杞"); list.add("人字拖"); list.add("特级枸杞"); list.add("枸杞子"); //删除包含枸杞的数据 for (String s : list) { if(s.contains("枸杞")){ list.remove(s); } } System.out.println(list); } }
解决并发修改异常问题的方案
-
如果集合支持索引,可以使用for循环遍历,每删除数据后做i--;或者可以倒着遍历
public class Test4 { public static void main(String[] args) { //遍历的同时删除元素导致异常问题,也叫并发修改问题 // test1(); } private static void test2() { ArrayList<String> list = new ArrayList<>(); list.add("Java入门"); list.add("宁夏枸杞"); list.add("黑枸杞"); list.add("人字拖"); list.add("特级枸杞"); list.add("枸杞子"); //删除包含枸杞的数据 for (int i = 0; i < list.size(); i++) { // System.out.println(list.get(i));//i从0开始 if(list.get(i).contains("枸杞")){ list.remove(list.get(i)); i--; // 使用i--抵消位移 } } System.out.println(list); } }public class Test4 { public static void main(String[] args) { //遍历的同时删除元素导致异常问题,也叫并发修改问题 // test3(); } //解决fori问题方案2:从后往前删除 private static void test3() { ArrayList<String> list = new ArrayList<>(); list.add("Java入门"); list.add("宁夏枸杞"); list.add("黑枸杞"); list.add("人字拖"); list.add("特级枸杞"); list.add("枸杞子"); //删除包含枸杞的数据 for (int i = list.size()-1; i >=0; i--) { // System.out.println(list.get(i));//i从0开始 if(list.get(i).contains("枸杞")){ list.remove(list.get(i)); } } System.out.println(list); } } -
可以使用迭代器遍历,并用迭代器提供的删除方法删除数据。注意,不要使用集合中的remove方法,这样会使Iterator中的next方法抛出异常
public class Test4 { public static void main(String[] args) { //遍历的同时删除元素导致异常问题,也叫并发修改问题 test5(); } private static void test5() { ArrayList<String> list = new ArrayList<>(); list.add("Java入门"); list.add("宁夏枸杞"); list.add("黑枸杞"); list.add("人字拖"); list.add("特级枸杞"); list.add("枸杞子"); //删除包含枸杞的数据 Iterator<String> iterator = list.iterator(); while(iterator.hasNext()){ String next = iterator.next(); if(next.contains("枸杞")){ // list.remove(next); iterator.remove(); } } System.out.println(list); } } -
注意:增强for循环/Lambda遍历均不能解决并发修改异常问题,因此增它们只适合做数据的遍历,不适合同时做增删操作。
List集合
List集合的特点和功能
- List接口下所有子类集合的特点:有序,可重复,有索引
- List接口下所有子类集合的区别:底层实现不同(数据结构不同)!适合的场景不同!
List集合的特点
List集合的特有方法
- List集合因为支持索引,所以多了很多与索引相关的方法,当然,Collection的功能List也都继承了。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Demo011 {
public static void main(String[] args) {
// 目标:掌握List系列集合独有的功能。
List<String> list = new ArrayList<>(); // 多态 一行经典代码
list.add("Java");
list.add("Css");
list.add("HTML");
list.add("Java");
System.out.println(list); // [Java, Css, HTML, Java]
// 1、插入数据到指定索引。add(索引,元素)、
list.add(2,"AI");
System.out.println(list);
// 2、根据索引删除数据。remove(索引)
list.remove(2);
System.out.println(list);
// 3、修改某个索引位置处的数据 set(索引,修改后的数据)
list.set(2,"AI");
System.out.println(list);
// 4、根据索引取数据。get(索引)
System.out.println(list.get(2));
}
}
List集合支持的遍历方式
-
因为List是有序的,所以有下标,几乎支持所有循环
- for循环(因为List集合有索引)
- 迭代器
- 增强for循环
- Lambda表达式
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Demo011 { public static void main(String[] args) { // 目标:掌握List系列集合独有的功能。 List<String> list = new ArrayList<>(); // 多态 一行经典代码 list.add("Java"); list.add("Css"); list.add("HTML"); list.add("Java"); System.out.println(list); // [Java, Css, HTML, Java] // 5、fori循环遍历 System.out.println("fori循环遍历"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } // 6、增强for循环遍历 System.out.println("增强for循环遍历"); for (String s : list) { System.out.println(s); } // 7、迭代器遍历 System.out.println("迭代器遍历"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ String data = iterator.next(); System.out.println(data); } // 8、Lambda表达式遍历 System.out.println("Lambda表达式遍历"); list.forEach(s -> System.out.println(s)); } }
ArrayList底层数据结构与实现原理
- ArrayList底层是基于数组存储数据的
- LinkedList底层是基于链表存储数据的
- 因为ArrayList是用数组实现的,所以有以下优缺点:
- 查询速度快(注意:是根据索引查询数据的速度快):查询数据通过地址值和索引定位,通过索引查询任意数据耗时相同。
- 增删数据效率低:可能需要把后面很多数据进行前移。
- ArrayList的扩容机制:
- 初始化长度为0
- 第一次添加第一个元素:拓展机制初始化为10
- 添加第11个元素,即为ArrayList所使用的数组,容积不够的的时,1.5倍数拓展
- 除了初始化和第一次添加元素,ArrayList每次扩容都是创建一个新的1.5倍大的数组,再将旧数组的元素。
-扩容过程如下方源码所示
LinkedList底层数据结构与实现原理
- LinkedList底层是基于链表存储数据的。
- 链表的特点:
- 链表中的数据是一个一个独立的结点组成的,结点在内存中是不连续的,每个结点包含数据值和下一个结点的地址。
- 链表的删除插入操作都是通过改变指针来做到的,比如:
- 数据C对应的下一个数据地址指向数据E
- 数据D删除
- 链表的特点1:查询慢,无论查询哪个数据都要从头开始找。
- 链表的特点2:链表增删相对快
- 链表中的数据是一个一个独立的结点组成的,结点在内存中是不连续的,每个结点包含数据值和下一个结点的地址。
- LinkedList基于双链表实现的
- 会有以下特点:
- 双向链表对比单链表多了头尾指针和反方向的指针,可以看到linkedList的源码中看到,是可以直接拿到头尾指针的:
- 因此,双向链表查询慢,增删相对较快,但对首尾元素进行增删改查的速度是极快的,对某个元素相邻的元素进行增删改查的速度也是极快的
- 因此,LinkedList新增了:很多首尾操作的特有方法
方法名称 说明 public void addFirst(E e) 在该列表开头插入指定的元素 public void addLast(E e) 将指定的元素追加到此列表的末尾 public E getFirst() 返回此列表中的第一个元素 public E getLast() 返回此列表中的最后一个元素 public E removeFirst() 从此列表中删除并返回第一个元素 public E removeLast() 从此列表中删除并返回最后一个元素
- 双向链表对比单链表多了头尾指针和反方向的指针,可以看到linkedList的源码中看到,是可以直接拿到头尾指针的:
LinkedList底层数据结构与实现原理
LinkedList的应用场景之一:可以用来设计队列
- 先进先出,后进后出
- 相比LinkedList链表来实现,用ArrayList动态数组来实现,在出队的时候,会照成后面排队的元素批量位移,时间复杂度会从O(1)变为O(n)
import java.util.LinkedList;
/**
//目标:使用LinkedList实现队列(先进先出,后进后出)
//入队方法(在队尾添加节点 addLast)和出队方法(移除头节点 removeFirst)
//注意: 还有单独的入队(offer(数据))和出队方法(peek()),
//模拟:1号、2号、3号、4号、5号数据入队
//模拟:1号、2号、3号、4号、5号数据出队
//目标:使用LinkedList实现栈(先进后出,后进先出)
//入栈方法push(),出栈方法pop()
//模拟:1号、2号、3号、4号、5号数据入栈
//模拟:1号、2号、3号、4号、5号数据出栈
*/
public class Demo131 {
public static void main(String[] args) {
//目标:使用LinkedList实现队列(先进先出,后进后出)
//入队方法(在队尾添加节点 addLast)和出队方法(移除头节点 removeFirst)
//模拟:添加1号、2号、3号、4号、5号数据
LinkedList<String> queueList = new LinkedList<>();
System.out.println("1号、2号、3号、4号、5号数据入队");
queueList.addLast("1号");
queueList.addLast("2号");
queueList.addLast("3号");
queueList.addLast("4号");
queueList.addLast("5号");
System.out.println("1号、2号、3号、4号、5号数据出队");
String data = queueList.removeFirst();
System.out.println("第一个出队数据:"+data);//1号
data = queueList.removeFirst();
System.out.println("第二个出队数据:"+data);//2号
data = queueList.removeFirst();
System.out.println("第三个出队数据:"+data);//3号
data = queueList.removeFirst();
System.out.println("第四个出队数据:"+data);//4号
data = queueList.removeFirst();
System.out.println("第五个出队数据:"+data);//5号
}
}
- 注意:有单独的入队(offer(数据))和出队方法(poll()),查看对头(peek())
- System.out.println(queueList.poll());//单独出队方法
public class Demo131 {
public static void main(String[] args) {
//目标:使用LinkedList实现队列(先进先出,后进后出)
//入队方法(在队尾添加节点 addLast)和出队方法(移除头节点 removeFirst)
//模拟:添加1号、2号、3号、4号、5号数据
LinkedList<String> queueList = new LinkedList<>();
System.out.println("1号、2号、3号、4号、5号数据入队");
queueList.offer("1号");
queueList.offer("2号");
queueList.offer("3号");
queueList.offer("4号");
queueList.offer("5号");
System.out.println("1号、2号、3号、4号、5号数据出队");
String data = queueList.poll();
System.out.println("第一个出队数据:"+data);//1号
data = queueList.poll();
System.out.println("第二个出队数据:"+data);//2号
data = queueList.poll();
System.out.println("第三个出队数据:"+data);//3号
data = queueList.poll();
System.out.println("第四个出队数据:"+data);//4号
data = queueList.poll();
System.out.println("第五个出队数据:"+data);//5号
}
}
LinkedList的应用场景之一:可以用来设计栈
- 先进后出,后进先出
import java.util.LinkedList;
/**
//目标:使用LinkedList实现队列(先进先出,后进后出)
//入队方法(在队尾添加节点 addLast)和出队方法(移除头节点 removeFirst)
//注意: 还有单独的入队(offer(数据))和出队方法(peek()),
//模拟:1号、2号、3号、4号、5号数据入队
//模拟:1号、2号、3号、4号、5号数据出队
//目标:使用LinkedList实现栈(先进后出,后进先出)
//入栈方法push(),出栈方法pop()
//模拟:1号、2号、3号、4号、5号数据入栈
//模拟:1号、2号、3号、4号、5号数据出栈
*/
public class Demo131 {
public static void main(String[] args) {
//目标:使用LinkedList实现栈(先进后出,后进先出)
LinkedList<String> queueList2 = new LinkedList<>();
System.out.println("1号、2号、3号、4号、5号数据入栈");
queueList2.push("1号");
queueList2.push("2号");
queueList2.push("3号");
queueList2.push("4号");
queueList2.push("5号");
System.out.println("1号、2号、3号、4号、5号数据出栈");
data = queueList2.pop();
System.out.println("第一个出栈数据:"+data);//5号
data = queueList2.pop();
System.out.println("第二个出栈数据:"+data);//4号
data = queueList2.pop();
System.out.println("第三个出栈数据:"+data);//3号
data = queueList2.pop();
System.out.println("第四个出栈数据:"+data);//2号
data = queueList2.pop();
System.out.println("第五个出栈数据:"+data);//1号
}
}
Set集合
Set集合的特点
- 在Collection集合体系中的位置
- Set系列集合特点:
- 无序(子类通过双链表改造后有序):添加数据的顺序和获取出的数据顺序不一致;
- 不重复;
- 无索引;
- Set的子类集合特点:
- HashSet : 无序、不重复、无索引。
- LinkedHashSet:有序、不重复、无索引。
- TreeSet:排序、不重复、无索引。
- 注意:Set要用到的常用方法,基本上就是Collection提供的!!自己几乎没有额外新增一些常用功能!
HashSet集合的底层原理
Hash值
- 为什么往HashSet集合中存储的数据是无序,不重复,无索引的?需要先了解一个概念叫哈希值
- 哈希值就是一个int类型的随机值,Java中每个对象都有一个哈希值。
- Java中的所有对象,都可以调用Obejct类提供的
hashCode()方法,返回该对象自己的哈希值;public int hashCode():返回对象的哈希码值。 - 对象哈希值的特点:
- 同一个对象多次调用hashCode()方法返回的哈希值是相同的。
- 不同的对象,它们的哈希值大概率不相等,但也有可能会相等(哈希碰撞)。
- 哈希值的取值范围为int (-21亿多 ~ 21亿多)可以对应45亿个对象,当对象数超出这个范围就会发生哈希碰撞
底层原理
- HashSet是基于哈希表存储数据的
- 哈希表
- JDK8之前,哈希表 = 数组+链表
- JDK8开始,哈希表 = 数组+链表+红黑树
- 哈希表是一种增删改查数据,性能都较好的数据结构
- HashSet添加元素的方式
- 创建一个默认长度16的数组,默认加载因子为0.75,数组名table
- 使用元素的哈希值对数组的长度做运算计算出应存入的位置
- 判断当前位置是否为null,如果是null直接存入
- 如果不为null,表示有元素(哈希冲突),则调用equals方法比较
- 相等,则不存;不相等,则存入数组
- JDK 8之前,新元素存入数组,占老元素位置,老元素挂下面
- JDK 8开始之后,新元素直接挂在老元素下面
- 相等,则不存;不相等,则存入数组
- HashSet的扩容机制
- 会计算(数组长度*影响因子的值)的值,如果超出则按照2倍进行扩容
- 在JDK8之后当链表长度超过8,且数组长度>=64时,自动将链表转成红黑树
红黑树的特点
- 红黑树,就是可以自平衡的二叉树
- 红黑树是一种增删改查数据性能相对都较好的结构。
HashSet集合元素的去重操作
- 前面我们学习了
HashSet存储元素的原理,依赖于两个方法:- 一个是
hashCode方法用来确定在底层数组中存储的位置 - 另一个是用
equals方法判断新添加的元素是否和集合中已有的元素相同
- 一个是
- 要想保证在
HashSet集合中没有重复元素,除了包装类型中已经重写了hashCode和equals方法,我们需要重写元素类的hashCode和equals方法。 - 举个例子:
- 需求:创建一个存储学生对象的集合,存储多个学生对象,要求:多个学生对象的成员变量值相同时,我们就认为是同一个对象,要求只保留一个。
- 分析:
- 定义学生类,创建HashSet集合对象, 创建学生对象
- 把学生添加到集合
- 在学生类中重写两个方法,hashCode()和equals(),自动生成即可
- 遍历集合(增强for)
- 如果没有重写
hashCode和equals方法,则hashCode生成的哈希值是根据对象的地址来生成,equals比对的也是地址,是无法实现去重功能的 - 可以使用快捷键
alt+insert自动生成一个类的
import java.util.Objects; public class Student { private String name; private int age; private double height; public Student() { } public Student(String name, int age, double height) { this.name = name; this.age = age; this.height = height; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } @Override public String toString() { return "Student{" + "name='" + name + ''' + ", age=" + age + ", height=" + height + '}'; } }import java.util.HashSet; import java.util.Set; /** * 目标:自定义的类型的对象,比如两个内容一样的学生对象,如果让HashSet集合能够去重复! */ public class SetTest3 { public static void main(String[] args) { Set<Student> students = new HashSet<>(); Student s1 = new Student("至尊宝", 28, 169.6); Student s2 = new Student("蜘蛛精", 23, 169.6); Student s3 = new Student("蜘蛛精", 23, 169.6); System.out.println(s2.hashCode()); System.out.println(s3.hashCode()); Student s4 = new Student("牛魔王", 48, 169.6); students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students); } }- 重写了equals和hashCode方法
import java.util.Objects; public class Student { private String name; private int age; private double height; public Student() { } public Student(String name, int age, double height) { this.name = name; this.age = age; this.height = height; } // 只要两个对象内容一样就返回true @Override public boolean equals(Object o) { if (this == o) return true;// 如果两个引用指向了同一个地址则直接返回True if (o == null || getClass() != o.getClass()) return false; //类型不同直接返回false Student student = (Student) o; // 类型相同,则将被向上转型的o转换为this的类型 return age == student.age && Double.compare(student.height, height) == 0 && Objects.equals(name, student.name); // 调用各个类型的比较方法比较熟悉值,这里Double.compare是double类型的比较方法,Object.equals是比较字符串 } // 只要两个对象内容一样,返回的哈希值就是一样的。 @Override public int hashCode() { // 姓名 年龄 身高计算哈希值的 return Objects.hash(name, age, height); //使用Objects类中的hash方法,而不是Object类中的hashCode(这是我们正在重写的) } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } @Override public String toString() { return "Student{" + "name='" + name + ''' + ", age=" + age + ", height=" + height + '}'; } }
LinkedHashSet的底层原理
-
linkedList在collection集合体系中的位置
-
接下来,我们再学习一个
HashSet的子类LinkedHashSet类。LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:- 每次添加元素尾指针指向的元素会有一根指针指向添加的元素,而添加的元素会有一个指针指向尾指针指向的元素(相对它上一个元素),尾指针指向最后添加的元素
- 哈希冲突形成的链表和红黑树也是一样会建立双向链表
- 每次添加元素尾指针指向的元素会有一根指针指向添加的元素,而添加的元素会有一个指针指向尾指针指向的元素(相对它上一个元素),尾指针指向最后添加的元素
TreeSet集合
- 特点:不重复、无索引、可排序(默认升序排序 ,按照元素的大小,由小到大排序)
- 底层是基于红黑树实现的排序。
- 注意:
- 对于数值类型:Integer , Double,默认按照数值本身的大小进行升序排序。
- 对于字符串类型:默认按照首字符的编号升序排序。
- 对于自定义类型如Student对象,TreeSet默认是无法直接排序的。
- 如果往
TreeSet集合中存储自定义类型的元素,比如说Teacher类型,则需要我们自己指定排序规则,否则会出现异常。
public class SetDemo3 { public static void main(String[] args) { // 目标:搞清楚TreeSet集合对于自定义对象的排序 Set<Teacher> teachers = new TreeSet<>(); teachers.add(new Teacher("老陈", 20, 6232.4)); teachers.add(new Teacher("dlei", 18, 3999.5)); teachers.add(new Teacher("老王", 22, 9999.9)); teachers.add(new Teacher("老李", 20, 1999.9)); System.out.println(teachers); } }原因是
TreeSet不知道按照什么条件对Teacher对象来排序
TreeSet自定义排序规则
- TreeSet集合存储自定义类型的对象时,必须指定排序规则,支持如下两种方式来指定比较规则。
- 方式一:让自定义的类(如学生类)实现Comparable接口,重写里面的compareTo方法来指定比较规则。
// 1、对象类实现一个Comparable比较接口,重写compareTo方法,指定大小比较规则 import java.util.Objects; public class Teacher implements Comparable<Teacher>{ private String name; private int age; private double salary; public Teacher(String name, double salary, int age) { this.name = name; this.salary = salary; this.age = age; } public Teacher() { } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public void setSalary(double salary) { this.salary = salary; } public String getName() { return name; } public int getAge() { return age; } public double getSalary() { return salary; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Teacher teacher = (Teacher) o; return age == teacher.age && Double.compare(salary, teacher.salary) == 0 && Objects.equals(name, teacher.name); } @Override public int hashCode() { return Objects.hash(name, age, salary); } @Override public String toString() { return "Teacher{" + "name='" + name + ''' + ", age=" + age + ", salary=" + salary + '}' + "\n"; } // t2.compareTo(t1) // t2 == this 比较者 // t1 == o 被比较者 // 规定1:如果你认为左边大于右边 请返回正整数 // 规定2:如果你认为左边小于右边 请返回负整数 // 规定3:如果你认为左边等于右边 请返回0 // 默认就会升序。 @Override public int compareTo(Teacher o) { // 按照年龄升序 //if(this.getAge() > o.getAge()) return 1; //if(this.getAge() < o.getAge()) return -1; //return 0; return this.getAge() - o.getAge(); // 升序 //return o.getAge() - this.getAge(); // 降序 } } - 方式二:public TreeSet(Comparator c)集合自带比较器Comparator对象,指定比较规则
(会优先使用直接给TreeSet变量的比较规则,即为就近原则)
public class SetDemo3 { public static void main(String[] args) { // 目标:搞清楚TreeSet集合对于自定义对象的排序 Set<Teacher> teachers = new TreeSet<>(new Comparator<Teacher>() { @Override public int compare(Teacher o1, Teacher o2) { return Double.compare(o2.getSalary(), o1.getSalary()); // 薪水升序 } }); // 排序,不重复,无索引 // 简化形式 //Set<Teacher> teachers = new TreeSet<>((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary())); // 排序,不重复,无索引 teachers.add(new Teacher("老陈", 20, 6232.4)); teachers.add(new Teacher("dlei", 18, 3999.5)); teachers.add(new Teacher("老王", 22, 9999.9)); teachers.add(new Teacher("老李", 20, 1999.9)); System.out.println(teachers); } }- 如果用
return int(o2.getSalary()-o1.getSalary());,会导致只有小数不同的参数排序失效 - 所以这里使用了
return Double.compare(o2.getSalary(), o1.getSalary());这是一个专门用来定义Double类型的排序方式,可以看下Double类中的compare的源码 - 运行后结果
- 如果用
Map集合
什么是双列集合有哪些特点
- 所谓双列集合,就是说集合中的元素是一对一对的。Map集合中的每一个元素是以
key=value的形式存在的,一个key=value就称之为一个键值对,而且在Java中有一个类叫Entry类,Entry的对象用来表示键值对对象。 - 所有的Map集合有如下的特点:键不能重复,值可以重复,每一个键只能找到自己对应的值。
- 下面我们先写一个
Map集合,保存几个键值对,体验一下Map集合的特点- Map特点/HashMap特点:无序,键不重复,无索引,键值对都可以是null, 值不做要求(可以重复)
- LinkedMap特点:有序,不重复,键无索引,键值对都可以是null, 值不做要求(可以重复)
- TreeMap: 按照键可排序,不重复,键无索引
public class MapDemo1 { public static void main(String[] args) { // 目标:认识Map集合的体系特点。 // 1、创建Map集合 // Map特点/HashMap特点:无序,不重复,无索引,键值对都可以是null, 值不做要求(可以重复) // LinkedMap特点:有序,不重复,无索引,键值对都可以是null, 值不做要求(可以重复) // TreeMap: 按照键可排序,不重复,无索引 Map<String,Integer> map = new HashMap<>(); // 一行经典代码 //Map<String,Integer> map = new LinkedHashMap<>(); map.put("嫦娥", 20); map.put("女儿国王", 31); map.put("嫦娥", 28) ; map.put("铁扇公主", 38); map.put("紫霞", 31); map.put(null, null); System.out.println(map); // {null=null, 嫦娥=28, 铁扇公主=38, 紫霞=31, 女儿国王=31} } } - Map集合在什么业务场景下使用?
- 在表达购物车中商品和商品数量时可以使用 , 统计文字出现次数时可以使用。
- 总之: 需要存储一一对应的数据时,就可以考虑使用Map集合来做
- Map常见体系如下:
Map集合的常用方法
- 为什么要先学习Map的常用方法 ?
- Map是双列集合的祖宗,它的功能是全部双列集合都可以继承过来使用的。
方法名称 说明 public V put(K key,V value) 添加元素 public int size() 获取集合的大小 public void clear() 清空集合 public boolean isEmpty() 判断集合是否为空,为空返回true , 反之返回false public V get(Object key) 根据键获取对应值 public V remove(Object key) 根据键删除整个元素 public boolean containsKey(Object key) 判断是否包含某个键 public boolean containsValue(Object value) 判断是否包含某个值 public Set keySet() 获取全部键的集合 public Collection values() 获取Map集合的全部值
- Map是双列集合的祖宗,它的功能是全部双列集合都可以继承过来使用的。
Map集合的遍历方式
键找值
- 先获取Map集合全部的键,再通过遍历键来找值
方法名称 说明 public Set keySet() 获取所有键的集合 public V get(Object key) 根据键获取其对应的值 public class MapTraverseDemo3 { public static void main(String[] args) { // 目标:掌握Map集合的遍历方式一:键找值。 Map<String,Integer> map = new HashMap<>(); map.put("嫦娥", 20); map.put("女儿国王", 31); map.put("嫦娥", 28); map.put("铁扇公主", 38); map.put("紫霞", 31); System.out.println(map); // {嫦娥=28, 铁扇公主=38, 紫霞=31, 女儿国王=31} // 1、提起Map集合的全部键到一个Set集合中去 Set<String> keys = map.keySet(); // 2、遍历Set集合,得到每一个键 for (String key : keys) { // 3、根据键去找值 Integer value = map.get(key); System.out.println(key + "=" + value); } } }
键值对
-
把“键值对“看成一个整体进行遍历,,这种遍历方式更加符合面向对象的思维。
-
前面我们给大家介绍过,Map集合是用来存储键值对的,而每一个键值对实际上是一个Entry对象
-
原理: 是直接获取每一个Entry对象,把Entry存储扫Set集合中去,再通过Entry对象获取键和值
-
涉及如下方法:
Map提供的方法 说明 Set<Map.Entry<K, V>> entrySet() 获取所有“键值对”的集合 - 每一个键值对都是一个对象,每一个对象都有单独的地址hashCode()方法没有被重写,因此获得的哈希值也是不同的equals比较结果也不同,因此是不会被去重的
Map.Entry提供的方法 说明 K getKey() 获取键 V getValue() 获取值 public class MapTraverseDemo4 { public static void main(String[] args) { // 目标:掌握Map集合的遍历方式二:键值对。 Map<String,Integer> map = new HashMap<>(); map.put("嫦娥", 20); map.put("女儿国王", 31); map.put("嫦娥", 28); map.put("铁扇公主", 38); map.put("紫霞", 31); System.out.println(map); // {嫦娥=28, 铁扇公主=38, 紫霞=31, 女儿国王=31} // 1、把Map集合转换成Set集合,里面的元素类型都是键值对类型(Map.Entry<String, Integer>) /** * map = {嫦娥=28, 铁扇公主=38, 紫霞=31, 女儿国王=31} * ↓ * map.entrySet() * ↓ * Set<Map.Entry<String, Integer>> entries = [(嫦娥=28), (铁扇公主=38), (紫霞=31), (女儿国王=31)] * entry */ Set<Map.Entry<String, Integer>> entries = map.entrySet(); // 2、遍历Set集合,得到每一个键值对类型元素 for (Map.Entry<String, Integer> entry : entries) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + "=" + value); } } } -
Set<Map.Entry<String, Integer>> = map.entrySet();详解,这里用到了泛型嵌套- 这里指定需要
Set<Map.Entry<String,Integer>>类型的对象,且这个Set引用指定了泛型为Map.Entry<String,Integer>的接口引用,即为指定的是Map的**内部类(内部接口)**的类型,而这个内部类也通过泛型指定了内部需要的类型String,Integer - 而
map.entrySet();实际上是调用了HashMap类中对接口Set<Map.Entry<String,Integer>>的实现,这里是父类接口的引用持有了子类的对象,而是实现这个嵌套接口是通过先实现Set指定的泛型中的map接口,比如创建一个hashMap对象,于此同时实现里面的Entry抽象方法,Entry方法内会实现Set创建Set集合,即为最后返回的使用有map键值对对象组成的Set集合- 可以看到Map里面有抽象方法entrySet()
- 先到Mpa的实现Hashmap我们可以先看内部接口Entry可以看到也被实现了,当然也可以从Map的内部接口转到实现Entry的方法
- 创建HashMap集合即会实现map接口,同时也会实现Map中的抽象方法、内部类(接口),我们可以从抽象方法entrySet跳转到HashMap的实现类中查看
- 最后将对象返回到引用中:
- 可以看到Map里面有抽象方法entrySet()
- 这里指定需要
Lambda+forEach遍历
-
Map集合的第三种遍历方式,需要用到下面的一个方法
forEach,而这个方法是JDK8版本以后才有的。调用起来非常简单,最好是结合的lambda表达式一起使用。方法名称 说明 default void forEach(BiConsumer<? super K, ? super V> action) 结合lambda遍历Map集合 public class MapTraverseDemo5 { public static void main(String[] args) { // 目标:掌握Map集合的遍历方式三:Lambda。 Map<String,Integer> map = new HashMap<>(); map.put("嫦娥", 20); map.put("女儿国王", 31); map.put("嫦娥", 28); map.put("铁扇公主", 38); map.put("紫霞", 31); System.out.println(map); // {嫦娥=28, 铁扇公主=38, 紫霞=31, 女儿国王=31} // 1、直接调用Map集合的forEach方法完成遍历 //map.forEach(new BiConsumer<String, Integer>() { // @Override // public void accept(String key, Integer value) { // System.out.println(key + "=" + value); // } //}); //结合lambda使用 map.forEach((k,v) -> System.out.println(k + "=" + v)); } }
HashMap集合的底层原理
-
实际上:原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已,值对象用一个展位对象代替。
-
HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的
-
JDK8之前,哈希表 = 数组+链表
-
JDK8开始,哈希表 = 数组+链表 +红黑树
-
哈希表是一种增删改查数据,性能都较好的数据结构。
-
LinkedHashMap的底层原理
- 实际上:原来学习的LinkedHashSet集合的底层原理就是LinkedHashMap。
- 底层数据结构依然是基于哈希表实现的,只是每个键值对元素又额外的多了一个双链表的机制记录元素顺序(保证有序)。
TreeMap的使用
-
特点:不重复、无索引、可排序(按照键的大小默认升序排序,只能对键排序)
-
原理:TreeMap跟TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序
-
对于非包装类型的对象要自定拍寻规则,TreeMap集合同样也支持两种方式来指定排序规则
- 让类实现Comparable接口,重写比较规则。
- TreeMap集合有一个有参数构造器,支持创建Comparator比较器对象,以便用来指定比较规则。
- 让类实现Comparable接口,重写比较规则。