🔥 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:Java从入门到大牛
🌠 首发时间:2023年7月29日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生
目录
集合体系概述
集合体系结构
单列集合和双列集合
- Collection 代表单列集合,每个元素(数据)只包含一个值
- Map 代表双列集合,每个元素包含两个值,即一个键值对
Collection集合体系
Collection集合特点
List 系列集合:添加的元素是有序的、可重复的、有索引的
- ArrayList、LinkedList:有序、可重复、有索引
Set 系列集合:添加的元素是无序的、不重复的、无索引的
- HashSet:无序、不重复、无索引
- LinkedHashSet:有序、不重复、无索引
- TreeSet:按照大小默认升序排序、不重复、无索引
Collection的常用方法
为什么要先学 Collection 的常用方法 ?
因为 Collection 是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的
Collection 的常见方法如下:
具体应用
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
/**
目标:掌握Collection集合的常用API
*/
public class CollectionTest2API {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>(); // 多态写法
// 1.public boolean add(E e):添加元素, 添加成功返回true。
c.add("java1");
c.add("java1");
c.add("java2");
c.add("java2");
c.add("java3");
System.out.println("c: " + c);
// 2.public void clear():清空集合的元素。
//c.clear();
//System.out.println("c: " + c);
// 3.public boolean isEmpty():判断集合是否为空 是空返回true,反之。
System.out.println("isEmpty: " + c.isEmpty()); // false
// 4.public int size():获取集合的大小。
System.out.println("size: " + c.size());
// 5.public boolean contains(Object obj):判断集合中是否包含某个元素。
System.out.println("contains(\"java1\"): " + c.contains("java1")); // true
System.out.println("contains(\"Java1\"): " + c.contains("Java1")); // false
// 6.public boolean remove(E e):删除某个元素:如果有多个重复元素默认删除前面的第一个!
System.out.println("remove(\"java1\"): " + c.remove("java1"));
System.out.println("c: " + c);
// 7.public Object[] toArray():把集合转换成数组
Object[] arr1 = c.toArray();
System.out.println("arr1: " + Arrays.toString(arr1));
String[] arr2 = c.toArray(new String[c.size()]);
System.out.println("arr2: " + Arrays.toString(arr2));
System.out.println("--------------------------------------------");
// 把一个集合的全部数据倒入到另一个集合中去。
Collection<String> c1 = new ArrayList<>();
c1.add("java1");
c1.add("java2");
Collection<String> c2 = new ArrayList<>();
c2.add("java3");
c2.add("java4");
c1.addAll(c2); // 就是把c2集合的全部数据倒入到c1集合中去。
System.out.println("c1: " + c1);
System.out.println("c2: " + c2);
}
}
Collection的遍历方式
迭代器
迭代器概述
迭代器是用来遍历集合的专用方式(数组没有迭代器),在 Java 中迭代器的代表是 Iterator
Collection集合获取迭代器的方法
Iterator迭代器中的常用方法
具体应用
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
目标:Collection集合的遍历方式一:使迭代器Iterator遍历
*/
public class CollectionDemo01 {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>();
c.add("小明");
c.add("小红");
c.add("小刚");
System.out.println(c);
// 使用迭代器遍历集合
// 1、从集合对象中获取迭代器对象
Iterator<String> it = c.iterator();
// 2、我们应该使用循环结合迭代器遍历集合
while (it.hasNext()){
String ele = it.next();
System.out.println(ele);
}
}
}
增强for
增强版for循环的格式
for (元素的数据类型 变量名 : 数组或者集合) {
}
Collection<String> c = new ArrayList<>();
...
for (String s : c) {
System.out.println(s);
}
- 增强 for 可以用来遍历集合或者数组
- 增强 for 遍历集合,本质就是迭代器遍历集合的简化写法
增强for修改变量值会出现什么问题 ?
修改增强 for 中的变量值不会影响到集合中的元素
lambda表达式
Lambda表达式遍历集合
得益于 JDK8 开始的新技术 Lambda 表达式,提供了一种更简单、更直接的方式来遍历集合
需要使用 Collection 的如下方法来完成
具体应用
import java.util.ArrayList;
import java.util.Collection;
/**
目标:Collection集合的遍历方式三:JDK8开始新增的Lambda表达式
*/
public class CollectionDemo03 {
public static void main(String[] args) {
Collection<String> c = new ArrayList<>();
c.add("小明");
c.add("小红");
c.add("小刚");
c.forEach(s -> System.out.println(s));
System.out.println("-----------------------");
c.forEach(System.out::println);
}
}
案例:遍历集合中的自定义对象
需求:展示多部电影信息
分析
- 每部电影都是一个对象,多部对象要使用集合装起来
- 遍历集合中的电影对象,输出每部电影的详情信息
案例代码实现
Movie 类
public class Movie {
private String name;
private double score;
private String actor;
public Movie() {
}
public Movie(String name, double score, String actor) {
this.name = name;
this.score = score;
this.actor = actor;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
@Override
public String toString() {
return "Movie{" +
"name='" + name + '\'' +
", score=" + score +
", actor='" + actor + '\'' +
'}';
}
}
测试类
import java.util.ArrayList;
import java.util.Collection;
/**
* 目标:完成电影信息的展示
*/
public class CollectionTest04 {
public static void main(String[] args) {
// 1、创建一个集合容器负责存储多部电影对象
Collection<Movie> movies = new ArrayList<>();
movies.add( new Movie("《肖生克的救赎》" , 9.7 , "罗宾斯"));
movies.add( new Movie("《霸王别姬》" , 9.6 , "张国荣、张丰毅"));
movies.add( new Movie("《阿甘正传》" , 9.5 , "汤姆.汉克斯"));
System.out.println(movies);
for (Movie movie : movies) {
System.out.println("电影名:" + movie.getName());
System.out.println("评分:" + movie.getScore());
System.out.println("主演:" + movie.getActor());
System.out.println("---------------------------------------------");
}
}
}
执行结果
集合存储对象的原理
可以发现,集合中存储的是元素对象的地址
List集合
特点、特有方法
List系列集合特点:有序、可重复、有索引
- ArrayList:有序、可重复、有索引
- LinkedList:有序、可重复、有索引
List集合的特有方法
List 集合因为支持索引,所以多了很多与索引相关的方法,当然,Collection 的功能 List 也都继承了
具体应用
import java.util.ArrayList;
import java.util.List;
/**
目标:掌握List系列集合的特点,以及其提供的特有方法
*/
public class ListTest1 {
public static void main(String[] args) {
// 1.创建一个ArrayList集合对象(有序、可重复、有索引)
List<String> list = new ArrayList<>(); // 一行经典代码
list.add("蜘蛛精");
list.add("至尊宝");
list.add("至尊宝");
list.add("牛夫人");
System.out.println(list); // [蜘蛛精, 至尊宝, 至尊宝, 牛夫人]
// 2.public void add(int index, E element): 在某个索引位置插入元素。
list.add(2, "紫霞仙子");
System.out.println(list);
// 3.public E remove(int index): 根据索引删除元素,返回被删除元素
System.out.println(list.remove(2));
System.out.println(list);
// 4.public E get(int index): 返回集合中指定位置的元素。
System.out.println(list.get(3));
// 5.public E set(int index, E element): 修改索引位置处的元素,修改成功后,会返回原来的数据
System.out.println(list.set(3, "牛魔王"));
System.out.println(list);
}
}
遍历方式
List集合支持的遍历方式
- for 循环(因为 List 集合有索引)
- 迭代器
- 增强 for 循环
- Lambda 表达式
具体应用
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
拓展:List系列集合的遍历方式
*/
public class ListTest2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("糖宝宝");
list.add("蜘蛛精");
list.add("至尊宝");
//(1)for循环
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
System.out.println("--------------------");
//(2)迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
System.out.println("--------------------");
//(3)增强for循环(foreach遍历)
for (String s : list) {
System.out.println(s);
}
System.out.println("--------------------");
//(4)JDK 1.8开始之后的Lambda表达式
list.forEach(s -> {
System.out.println(s);
});
}
}
ArrayList集合的底层原理
ArrayList的特点
- 基于数组实现的,数组的特点就是查询快、增删慢
- 查询速度快(注意:是根据索引查询数据快):查询数据通过地址值和索引定位,查询任意数据耗时相同
- 删除效率低:可能需要把后面很多的数据进行前移
- 添加效率极低:可能需要把后面很多的数据后移,再添加数据;或者也可能需要进行数组的扩容
ArrayList集合的底层原理
- 利用无参构造器创建的集合,会在底层创建一个默认长度为 0 的数组
- 添加第一个元素时,底层会创建一个新的长度为 10 的数组
- 存满时,会扩容 1.5 倍
- 如果一次添加多个元素,1.5 倍还放不下,则新创建数组的长度以实际为准
ArrayList集合适合的应用场景
- ArrayList 适合:根据索引查询数据,比如根据随机索引取数据(高效),或者数据量不是很大时
- ArrayList 不适合:数据量大的同时,又要频繁地进行增删操作
LinkedList集合的底层原理
LinkedList集合的底层原理
-
基于双链表实现的
-
特点:查询慢,增删相对较快,但对首尾元素进行增删改查的速度是极快的
LinkedList新增了很多首尾操作的特有方法
LinkedList集合的应用场景
- 可以用来设计队列,队列的特点是先进先出、后进后出,队列只是在首尾增删元素,所以用 LinkedList 来实现很合适
- 可以用来设计栈,栈的特点是后进先出、先进后出,栈只是在首部增删元素,用 LinkedList 来实现很合适
具体应用
import java.util.LinkedList;
/**
* 目标:掌握LinkedList集合的使用
*/
public class ListTest3 {
public static void main(String[] args) {
// 1、创建一个队列
LinkedList<String> queue = new LinkedList<>();
// 入队
queue.addLast("第1号");
queue.addLast("第2号");
queue.addLast("第3号");
queue.addLast("第4号");
System.out.println(queue);
// 出队
System.out.println(queue.removeFirst());
System.out.println(queue.removeFirst());
System.out.println(queue.removeFirst());
System.out.println(queue);
System.out.println("--------------------------------------------------");
// 2、创建一个栈对象
LinkedList<String> stack = new LinkedList<>();
// 压栈(push)
stack.push("第1颗子弹"); // addFirst
stack.push("第2颗子弹");
stack.push("第3颗子弹");
stack.push("第4颗子弹");
System.out.println(stack);
// 出栈(pop)
System.out.println(stack.pop()); // removeFirst
System.out.println(stack.pop());
System.out.println(stack);
}
}
Set集合
特点
Set 系列集合特点:无序——添加数据的顺序和获取出的数据顺序不一致;不重复;无索引
- HashSet:无序、不重复、无索引
- LinkedHashSet:有序、不重复、无索引
- TreeSet:排序、不重复、无索引
注意:Set 要用到的常用方法,基本上就是 Collection 提供的,自己几乎没有额外新增一些常用功能
HashSet集合的底层原理
什么是哈希值 ?
-
就是一个 int 类型的数值,Java 中每个对象都有一个哈希值
-
Java 中的所有对象,都可以调用 Object 类提供的 hashCode 方法,返回该对象自己的哈希值
public int hashCode(): 返回对象的哈希码值
对象哈希值的特点
- 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的
- 不同的对象,它们的哈希值一般不相同,但也有可能会相同(哈希碰撞)
HashSet集合的底层原理
- 基于哈希表实现
- 哈希表是一种增删改查数据性能都较好的数据结构
哈希表
- JDK8 之前,哈希表 = 数组+链表
- JDK8 开始,哈希表 = 数组+链表+红黑树
JDK8之前HashSet集合的底层原理,基于哈希表:数组+链表
- 创建一个默认长度为 16 的数组,默认加载因子为 0.75,数组名为 table
- 使用元素的哈希值对数组的长度求余计算出应存入的位置
- 判断当前位置是否为 null,如果是 null 直接存入
- 如果不为 null,表示当前位置有元素,则调用 equals 方法比较。相等,则不存;不相等,则存入数组
JDK8 之前,新元素存入数组,占据老元素位置,老元素挂下面
JDK8 开始之后,新元素直接挂在老元素下面 - 当数组存满到 16*0.75=12 时,就自动扩容,每次扩容为原先的两倍
如果数组快占满了,会出什么问题?该咋办?
链表会过长,导致查询性能降低,这时候就需要扩容了
JDK8开始HashSet集合的底层原理,基于哈希表:数组+链表+红黑树
JDK8 开始,当链表长度超过 8,且数组长度 >= 64时,自动将链表转成红黑树
深入理解HashSet集合去重复的机制
HashSet 集合默认不能对内容一样的两个不同对象去重复
比如有内容一样的两个学生对象存入到 HashSet 集合中去,HashSet 集合是不能去重复的
怎么让 HashSet 集合能够实现对内容一样的两个不同对象也能去重复?
如果希望 Set 集合认为两个内容一样的对象是重复的,必须重写对象的 hashSet() 和 equals() 方法
案例:Set集合去重复
需求:创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合,要求学生对象的成员变量值一样,我们就认为是同一个对象
分析
- 定义学生类,创建 HashSet 集合对象,创建学生对象
- 把学生添加到集合
- 在学生类中重写两个方法,hashCode() 和 equals(),自动生成即可
- 遍历集合(增强for)
自动生成步骤:在编辑器界面右击鼠标,选择 Generate,再选择 equals() and hashCode() 即可
代码示例
Student 类
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;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Double.compare(student.height, height) == 0 && Objects.equals(name, student.name);
}
// 只要两个对象内容一样,返回的哈希值就是一样的
@Override
public int hashCode() {
// 姓名 年龄 身高计算哈希值的
return Objects.hash(name, age, 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 org.w3c.dom.ls.LSOutput;
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);
for(Student s : students) {
System.out.println(s);
}
}
}
LinkedHashSet集合的底层原理
- 依然是基于哈希表(数组、链表、红黑树)实现的
- 但是,它的每个元素都额外地多了一个双链表的机制记录它前后元素的值
TreeSet集合
- 特点:不重复、无索引、可排序(默认升序排序,按照元素的大小,由小到大排序)
- 底层是基于红黑树实现的排序
注意:
- 对于数值类型:Integer、Double 默认按照数值本身的大小进行升序排序
- 对于字符串类型:默认按照首字符的编号升序排序
- 对于自定义类型如 Student 对象,TreeSet 默认是无法直接排序的
自定义排序规则
TreeSet 集合存储自定义类型的对象时,必须指定排序规则,支持如下两种方式来指定比较规则
-
方式一:让自定义的类(如学生类)实现 Comparable 接口,重写里面的 compareTo 方法来指定比较规则
public class Student implements Comparable<Student>
-
方式二:通过调用 TreeSet 集合有参数构造器,可以设置 Comparator 对象(比较器对象,用于指定比较规则)
public TreeSet(Comparator<? super E> comparator)
两种方式中,关于返回值的规则:
- 如果认为第一个元素大于第二个元素,返回正整数即可
- 如果认为第一个元素小于第二个元素,返回负整数即可
- 如果认为第一个元素等于第二个元素,返回 0 即可,此时 TreeSet 集合只会保留一个元素,认为两者重复
注意:如果类本身有实现 Comparable 接口,TreeSet 集合同时也自带比较器,默认使用集合自带的比较器排序
具体应用
import java.util.Set;
import java.util.TreeSet;
/**
* 目标:掌握TreeSet集合的使用。
*/
public class SetTest4 {
public static void main(String[] args) {
Set<Integer> set1 = new TreeSet<>();
set1.add(6);
set1.add(5);
set1.add(5);
set1.add(7);
System.out.println(set1);
// TreeSet就近选择自己自带的比较器对象进行排序
// Set<Student> students = new TreeSet<>(new Comparator<Student>() {
// @Override
// public int compare(Student o1, Student o2) {
// // 需求:按照身高升序排序
// return Double.compare(o1.getHeight() , o2.getHeight());
// }
// });
Set<Student> students = new TreeSet<>(( o1, o2) -> Double.compare(o1.getHeight() , o2.getHeight()));
students.add(new Student("蜘蛛精",23, 169.7));
students.add(new Student("紫霞",22, 169.8));
students.add(new Student("至尊宝",26, 165.5));
students.add(new Student("牛魔王",22, 183.5));
System.out.println(students);
}
}
集合的并发修改异常问题
集合的并发修改异常
- 使用迭代器遍历集合时,又同时在删除集合中的数据,程序就会出现并发修改异常的错误
- 由于增强 for 循环遍历集合就是迭代器遍历集合的简化写法,因此,使用增强 for 循环遍历集合,又在同时删除集合中的数据时,程序也会出现并发修改异常的错误
怎么保证遍历集合的同时删除数据而不出bug?
- 使用迭代器遍历集合,但用迭代器自己的删除方法删除数据即可
- 如果能用 for 循环遍历时,可以倒着遍历并删除;或者从前往后遍历,但删除元素后做
i--
的操作
注意:使用增强 for 和 Lambda 表达式无法解决 bug
具体应用
import java.util.*;
/**
* 目标:理解集合的并发修改异常问题,并解决。
*/
public class CollectionTest1 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("王麻子");
list.add("小李子");
list.add("李爱花");
list.add("张全蛋");
list.add("晓李");
list.add("李玉刚");
System.out.println(list);
// 需求:找出集合中全部带“李”的名字,并从集合中删除。
// 使用for循环遍历集合并删除集合中带李字的名字
// for (int i = 0; i < list.size(); i++) {
// String name = list.get(i);
// if(name.contains("李")){
// list.remove(name);
// i--;
// }
// }
// System.out.println(list);
// 需求:找出集合中全部带“李”的名字,并从集合中删除。
Iterator<String> it = list.iterator();
while (it.hasNext()){
String name = it.next();
if(name.contains("李")){
// list.remove(name); // 并发修改异常的错误
it.remove(); // 删除迭代器当前遍历到的数据,每删除一个数据后,相当于也在底层做了i--
}
}
System.out.println(list);
}
}