📌本篇博客是Java笔记整理集合篇的第一篇✨,在上周毛毛张分享过一张二叉树的遍历的文章,毛毛张在刷题的过程,里面涉及到集合中的Stack、Queue、List、LinkedList和ArrayList相关的知识点,相信大家在看完这篇博客之后再去刷LeetCode的相关题目的时候就会得心应手😁
🔖在没看到毛毛张这篇博客之前,大家跟着视频的学习是从
ArrayList
集合开始,先有一个初步的认识,但是毛毛张今天将从自顶向下,从ArrayList的顶层接口Collection
开始介绍,首先介绍整个集合的继承体系✅,再来分说每个具体的实现类的使用💥
文章目录
0.数据结构概述
🎃数据结构概述:数据结构是计算机底层存储、组织数据的方式。是指数据相互之间是以什么方式排列在一起的。数据结构是为了更加方便的管理和使用数据,需要结合具体的业务场景来进行选择。一般情况下,精心选择的数据结构可以带来更高的运行或者存储效率
1.数据结构之栈和队列【记忆】
- 栈结构
后进先出,先进后出 - 队列结构
先进先出,后进后出
2.数据结构之数组和链表【记忆】
- 数组结构
查询快、增删慢 - 队列结构
查询慢、增删快
1.集合
1.1 概述
- 定义:集合是一个存放对象的引用的容器
- 在Java中集合位于java.util包下
1.2 数组和集合的区别【5点】
- 相同点
- 数组和集合都是Java中的容器,可以存储多个数据
- 不同点
- 数组的长度是固定的,集合的长度是可变的
- 数组只能存储相同数据类型的数据,可以是基本数据类型,也可以是引用类型
- 集合可以存储不同数据类型的对象的引用,但一般情况下,我们会使用泛型来约定只使用1种数据类型
- 集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类
1.3 集合的分类【2类4种】
- Java中集合主要分为两类四种:
- 单列集合Collection
- List集合
- Set 集合
- Queue集合
- 双列集合Map
- 单列集合Collection
- 继承关系图:
-
大致分类:
-
单列集合Collection
-
双列集合Map
-
1.3 总结
- Java 集合框架主要包括两种类型的容器:一种是集合(Collection),存储一个元素集合;另一种是图(Map),存储键/值对映射
- 单列集合的根接口是Collection,它又继承了迭代接口Iterable
- Collection 接口又有 3 种子类型:List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类
- List接口常用的实现类有:ArrayList、LinkedList、Vector
- Set接口常用的实现类有:HashSet、LinkedHashSet、TreeSet
- Queue接口常用的实现类有:LinkedList、ArrayBlockQueue
- Collection 接口又有 3 种子类型:List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类
- 双列集合的根接口是Map,双列集合没有继承迭代接口Iterable
- Map接口常用的实现类有:HashMap、HashTable、TreeMap
- 单列集合的根接口是Collection,它又继承了迭代接口Iterable
- 集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
- 接口: 是代表集合的抽象数据类型。之所以定义多个接口,是为了以不同的方式操作集合对象
- 实现(类): 是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构。
- 算法: 是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
- 集合体系框架如图所示:
2 Collection
2.1 概述
- Collection是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素,并且里面的存储的元素是无序的、可重复的
- List系列集合:添加的元素是有序、可重复、有索引
- Set系列集合:添加的元素是无序、不重复、无索引
- JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如Set、List和Queue)实现
- Collection是一个接口,不能直接创建它的对象,只能通过多态的方式创建它的实现类的对象。其具体的实现类有:ArrayList、LinkedList、Vector、HashSet、LinkedHashSet、TreeSet
2.2 常用方法
方法名 | 说明 |
---|---|
int size() | 返回集合中的元素个数 |
boolean isEmpty() | 判断集合是否为空,如果为空则返回true |
boolean add(E e) | 将指定元素添加到集合中 |
boolean addAll(Collection<? extends E> c) | 将指定集合中的所有元素添加到此集合中 |
boolean remove(Object o) | 从集合中移除指定的元素 |
boolean removeIf(Object o) | 根据条件进行移除 |
boolean contains(Object o) | 判断集合中是否存在指定的元素 |
void clear() | 清空集合中的元素 |
Stream<E> stream() | 返回以此集合的Stream流对象 |
Object[] toArray() | 返回包含此集合中所有元素的数组 |
-
代码演示:
/*添加基本数据类型*/ import java.util.ArrayList; import java.util.Collection; public class Test { public static void main(String[] args) { /* | boolean add(E e) | 添加元素 | | boolean remove(Object o) | 从集合中移除指定的元素 | | boolean removeIf(Object o) | 根据条件进行移除 | | void clear() | 清空集合中的元素 | | boolean contains(Object o) | 判断集合中是否存在指定的元素 | | boolean isEmpty() | 判断集合是否为空 | | int size() | 集合的长度,也就是集合中元素的个数 | //注意点: Collection是一个接口,我们不能直接创建他的对象 所以,现在我们学习他的方法时,只能创建他实现类的对象 实现类:ArrayList */ //目的:为了学习Collection接口里面的方法 //自己在做一些练习的时候,还是按照之前的方式去创建对象 Collection<String> coll = new ArrayList<>(); //1.添加元素 //细节1:如果我们要往List系列集合中添加数据,那么永远返回true,因为List系列时允许元素重复的 //细节2: 如果我们要往Set系列集合中添加数据,如果当前要添加的元素不存在,方法返回true,表示添加成功 // 如果当前要添加的元素已经存在,方法返回false,表示添加失败 // 因为Set系列的集合不允许重复元素 coll.add("aaa"); coll.add("a.txt"); coll.add("aaa"); coll.add("ccc"); System.out.println(coll); //2.清空 //coll.clear(); //System.out.println(coll); //3.删除 //细节1:因为Collection里面定义的是共性的方法,所以此时不能通过索引进行删除。只能通过元素的对象进行删除。 //细节2:方法会有一个布尔类型的返回值,删除成功返回true,删除失败返回false //如果要删除的元素不存在,就会删除失败 coll.remove("aaa"); System.out.println(coll); //4.判断元素是否包含 //细节:底层是依赖equals方法进行判断是否存在的 //所以,如果集合中存储的是自定义对象,也想通过contains方法来判断是否包含,那么在javabean类中,一定要重写equals方法。 boolean result = coll.contains("aaa"); System.out.println(result); //5.判断集合是否非空 boolean result2 = coll.isEmpty(); System.out.println(result2); //6.获取集合的长度 System.out.println(coll.size()); } }
/*添加应用数据类型*/ /* Student.java */ import java.util.Objects; public class Student { private String name; private int age; public Student() { } public Student(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "Student{name = " + name + ", age = " + age + "}"; } @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 && Objects.equals(name, student.name); } } /* Test.java */ import java.util.ArrayList; import java.util.Collection; public class Test { public static void main(String[] args) { //1.创建集合的对象 Collection<Student> coll = new ArrayList<>(); //2.把学生对象添加到集合当中 coll.add(new Student("zhangsan", 23)); coll.add(new Student("lisi", 24)); coll.add(new Student("wangwu", 25)); //3.判断集合中某一个对象是否包含 Student s = new Student("zhangsan", 23); //因为contains方法在底层依赖equals方法判断对象是否一致的 //如果存的是自定义对象,没有重写equals方法,那么默认使用Object类中的equals方法进行判断, //而Object类中的equals方法,依赖地址值进行判断 //如果同姓名同年龄,就认为是同一个学生 //所以,需要在自定义的Javabean类中,重写equals方法就可以了 System.out.println(coll.contains(s)); } }
2.3 遍历方法【3种】
- Collection迭代器有三种遍历方式:
- 方式1:迭代器遍历
- 方式2:forEach方法+Lambda表达式
- 方式3:增强for,其底层就是使用迭代器进行遍历
- 方式1和方式2主要实现了迭代接口Iterable
方法 说明 Iterator<T> iterator()
返回此集合中元素的迭代器 void forEach(Consumer<? super T> action)
对迭代的每个元素执行给定操作,直到处理迭代所有元素或操作引发异常
2.3.1 迭代器遍历
-
迭代器在
Java
中的类是Iterator
,迭代器是集合专用的遍历方式 -
Iterator
类中的常用成员方法方法 说明 boolean hasNext()
判断当前位置是否有元素可以取出,有元素返回true,没有元素返回false E next()
获取当前位置的元素,并将迭代器对象移向下一个位置 void remove()
删除迭代器对象当前指向的元素 -
Collection
集合获取迭代器的成员方法方法 说明 Iterator<E> iterator()
返回此集合中元素的迭代器,通过集合对象的 iterator()
方法得到,默认指向当前集合的0索引
该方法是实现了其迭代接口Iterable
中的方法 -
迭代器遍历的实现步骤:
- 获取迭代器对象
- 判断迭代器指针指向的位置是否有元素
- 如果有就获取该元素,并将迭代器指向下一个元素
-
代码演示:
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class Test { public static void main(String[] args) { /* Collection系列集合三种通用的遍历方式: 1.迭代器遍历 2.增强for遍历 3.lambda表达式遍历 迭代器遍历相关的三个方法: Interator<E> iterator() :获取下一个迭代器对象 boolean hasNext() :判断当前指向的位置是否有元素 E next() :获取当前指向的元素并移动指针 */ //1.创建集合并添加元素 Collection<String> coll = new ArrayList<>(); coll.add("aaa"); coll.add("a.txt"); coll.add("ccc"); coll.add("ddd"); //2.获取迭代器对象 //迭代器就好比是一个箭头,默认指向集合的0索引 Iterator<String> it = coll.iterator(); //3.利用循环不断的去获取集合中每一个元素 while (it.hasNext()) { //4.next方法的两件事情:获取元素并移动指针 String str = it.next(); System.out.println(str); } } }
-
迭代器中删除的方法
public class IteratorDemo2 { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("b"); list.add("c"); list.add("d"); Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); if("b".equals(s)){ //指向谁,那么此时就删除谁. it.remove(); } } System.out.println(list); } }
-
注意细节:
1.报错NoSuchElementException
:迭代器的指针指向了最后没有元素的位置
2.迭代器遍历完毕,指针不会复位
3.循环中只能用一次next()
方法
4.迭代器遍历时,不能用集合的方法进行增加或者删除public class A04_CollectionDemo4 { public static void main(String[] args) { /* 迭代器的细节注意点: 1.报错NoSuchElementException 2.迭代器遍历完毕,指针不会复位 3.循环中只能用一次next方法 4.迭代器遍历时,不能用集合的方法进行增加或者删除 暂时当做一个结论先行记忆,在今天我们会讲解源码详细的再来分析。 如果我实在要删除:那么可以用迭代器提供的remove方法进行删除。 如果我要添加,暂时没有办法。(只是暂时) */ //1.创建集合并添加元素 Collection<String> coll = new ArrayList<>(); coll.add("aaa"); coll.add("bbb"); coll.add("ccc"); coll.add("ddd"); //2.获取迭代器对象 //迭代器就好比是一个箭头,默认指向集合的0索引处 Iterator<String> it = coll.iterator(); //3.利用循环不断的去获取集合中的每一个元素 while(it.hasNext()){ //4.next方法的两件事情:获取元素并移动指针 String str = it.next(); System.out.println(str); } //当上面循环结束之后,迭代器的指针已经指向了最后没有元素的位置 //System.out.println(it.next());//NoSuchElementException //迭代器遍历完毕,指针不会复位 System.out.println(it.hasNext()); //如果我们要继续第二次遍历集合,只能再次获取一个新的迭代器对象 Iterator<String> it2 = coll.iterator(); while(it2.hasNext()){ String str = it2.next(); //迭代器遍历时,不能用集合的方法进行增加或者删除 if("bbb".equals(str)){ //coll.remove("bbb"); it2.remove(); } System.out.println(str); } } }
2.3.2 forEach方法 + lambda表达式
-
forEach方法:该方法是实现了迭代接口
Iterable
接口后继承的方法方法 说明 void forEach(Consumer<? super T> action)
对 Iterable
每个元素执行给定操作,直到处理Iterable
所有元素或操作引发异常。 -
代码演示:
import java.util.ArrayList; import java.util.Collection; import java.util.function.Consumer; public class Test { public static void main(String[] args) { /* Collection系列集合三种通用的遍历方式: 1.迭代器遍历 2.增强for遍历 3.lambda表达式遍历 lambda表达式遍历: default void forEach(Consumer<? super T> action): */ //1.创建集合并添加元素 Collection<String> coll = new ArrayList<>(); coll.add("aaa"); coll.add("a.txt"); coll.add("ccc"); coll.add("ddd"); //2.利用匿名内部类的形式 //底层原理: //其实也会自己遍历集合,依次得到每一个元素 //把得到的每一个元素,传递给下面的accept方法 //s依次表示集合中的每一个元素 coll.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }); //lambda表达式 coll.forEach(s -> System.out.println(s)); } }
2.3.3 增强for
-
介绍:
- 增强
for
循环是为了简化迭代器代码而出现的,其底层实际上是使用迭代器进行遍历。 JDK5
引入了增强for
循环,其内部机制是基于Iterator
迭代器实现的。- 只有实现了
Iterable
接口的类才能够使用增强for
循环和迭代器,包括单列集合和数组。 - 增强
for
循环大大简化了数组和集合的遍历代码,使得代码更加简洁和易读。
- 增强
-
格式
for(集合/数组中元素的数据类型 变量名 : 集合/数组名) { // 已经将当前遍历到的元素封装到变量中了,直接使用变量即可 }
-
代码
public class MyCollectonDemo1 { public static void main(String[] args) { //1.创建集合并添加元素 Collection<String> coll = new ArrayList<>(); coll.add("aaa"); coll.add("bbb"); coll.add("ccc"); coll.add("ddd"); //2.利用增强for进行遍历 //1.数据类型一定是集合或者数组中元素的类型 //2.str仅仅是一个变量名而已,在循环的过程中,依次表示集合或者数组中的每一个元素 //3.coll就是要遍历的集合或者数组 for (String str : coll) { System.out.println(s); } } }
-
细节点注意:修改增强for中的变量,不会改变集合中原本的数据
for(String str : coll){ str = "q"; }
2.4 迭代器源码分析
- 迭代器遍历相关的三个方法:
Iterator<E> iterator()
:获取一个迭代器对象boolean hasNext()
:判断当前指向的位置是否有元素E next()
:获取当前指向的元素并移动指针
- 图解:
2.List集合
2.1 概述
- List集合的概述
- 有序集合,这里的有序指的是存取顺序
- 用户可以精确控制列表中每个元素的插入位置,用户可以通过整数索引访问元素,并搜索列表中的元素
- 与Set集合不同,列表通常允许重复的元素
- 常用的实现类有:ArrayList、LinkedList、Vector
- List集合的特点:简记:存取有序、可重复、有索引
- 集合中的元素允许重复
- 集合中的元素是有顺序的,各元素插入的顺序就是各元素的顺序
- 集合中的元素可以通过索引来访问或者设置
2.2 List集合特有方法
-
特有方法:
方法名 描述 void add(int index, E element)
在此集合中的指定位置插入指定的元素 E remove(int index)
删除指定索引处的元素,并返回被删除的元素 E set(int index, E element)
修改指定索引处的元素,并返回被修改的元素 E get(int index)
返回指定索引处的元素 default void sort(Comparator<? super E> c)
根据指定规则对列表进行排序 List<E> subList(int fromIndex, int toIndex)
返回列表指定的指定索引区间的子列表,包头不包尾 -
示例代码
import java.util.ArrayList; import java.util.List; public class Test { public static void main(String[] args) { /* | void add(int index,E element) | 在此集合中的指定位置插入指定的元素 | | E remove(int index) | 删除指定索引处的元素,返回被删除的元素 | | E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 | | E get(int index) | 返回指定索引处的元素 | */ //1.创建一个集合 List<String> list = new ArrayList<>(); //2.添加元素 list.add("aaa"); list.add("a.txt"); list.add("ccc"); list.add("ddd"); //| void add(int index,E element) | 在此集合中的指定位置插入指定的元素 | //细节:原来索引上的元素会依次往后移 list.add(1, "QQQ"); //| E remove(int index) | 删除指定索引处的元素,返回被删除的元素 | String remove = list.remove(0); System.out.println(remove); //| E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 | String result = list.set(0, "QQQ"); System.out.println(result); //| E get(int index) | 返回指定索引处的元素 | String s = list.get(0); System.out.println(s); //3.打印集合 System.out.println(list); } }
2.3 List遍历方式【5种】
-
List的五种遍历方式及其选择策略:
- 迭代器遍历:在遍历过程中需要删除元素,请使用迭代器遍历
- 列表迭代器:在遍历过程中需要添加元素,请使用列表迭代器
- 增强for遍历:仅仅想遍历,那么使用增强for或者Lambda表达式
- forEach方法 + Lambda表达式:
- 普通for:如果遍历的时候想操作索引,可以用普通for
-
代码示例:
//创建集合并添加元素 List<String> list = new ArrayList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); //1.迭代器 /*Iterator<String> it = list.iterator(); while(it.hasNext()){ String str = it.next(); System.out.println(str); }*/ //2.增强for //下面的变量s,其实就是一个第三方的变量而已。 //在循环的过程中,依次表示集合中的每一个元素 /* for (String s : list) { System.out.println(s); }*/ //3.Lambda表达式 //forEach方法的底层其实就是一个循环遍历,依次得到集合中的每一个元素 //并把每一个元素传递给下面的accept方法 //accept方法的形参s,依次表示集合中的每一个元素 //list.forEach(s->System.out.println(s) ); //4.普通for循环 //size方法跟get方法还有循环结合的方式,利用索引获取到集合中的每一个元素 /*for (int i = 0; i < list.size(); i++) { //i:依次表示集合中的每一个索引 String s = list.get(i); System.out.println(s); }*/ // 5.列表迭代器 //获取一个列表迭代器的对象,里面的指针默认也是指向0索引的 //额外添加了一个方法:在遍历的过程中,可以添加元素 ListIterator<String> it = list.listIterator(); while(it.hasNext()){ String str = it.next(); if("bbb".equals(str)){ //qqq it.add("qqq"); } } System.out.println(list);
2.4 细节点注意:
- List集合两种删除方式
- 直接删除指定元素
- 通过索引进行删除
- 注意细节:当List集合种的元素是
Integer
类型的时候,使用remove(i)
方法,此时remove
方法会执行会删除指定元素i
的操作,即remove
方法不会自动装箱- 在调用方法的时候,如果出现了重载现象,优先调用实参跟形参类型一致的那个方法
代码示例:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
//List系列集合中的两个删除的方法
//1.直接删除元素
//2.通过索引进行删除
//1.创建集合并添加元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//2.删除元素
//请问:此时删除的是1这个元素,还是1索引上的元素?
//为什么?
//因为在调用方法的时候,如果出现了重载现象
//优先调用,实参跟形参类型一致的那个方法
//即remove方法不会自动装箱
list.remove(1);
//手动装箱,手动把基本数据类型的1,变成Integer类型
Integer i = Integer.valueOf(2);
list.remove(i);
System.out.println(list);
}
}
3.ArrayList集合
3.1 概述
- 特点:
- 长度可以变化,只能存储引用数据类型
- 底层是数组结构实现,查询快、增删慢
- 泛型的使用:用于约束集合中存储元素的数据类型
- 集合和数组的优势对比:
- 长度可变
- 添加数据的时候不需要考虑索引,默认将数据添加到末尾
- 类间关系图
3.2 ArrayList类常用方法
3.2.1 构造方法
方法名 | 说明 |
---|---|
public ArrayList() | 创建一个空的集合对象 |
3.2.2 成员方法
方法 | 说明 |
---|---|
public boolean add(要添加的元素) | 将指定的元素追加到此集合的末尾 |
public boolean remove(要删除的元素) | 删除指定元素,返回值表示是否删除成功 |
public E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
public E set(int index, E element) | 修改指定索引处的元素,返回被修改的元素 |
public E get(int index) | 返回指定索引处的元素 |
public int size() | 返回集合中的元素的个数 |
3.2.3 示例代码
public class ArrayListDemo02 {
public static void main(String[] args) {
//创建集合
ArrayList<String> array = new ArrayList<String>();
//添加元素
array.add("hello");
array.add("world");
array.add("java");
//public boolean remove(Object o):删除指定的元素,返回删除是否成功
// System.out.println(array.remove("world"));
// System.out.println(array.remove("javaee"));
//public E remove(int index):删除指定索引处的元素,返回被删除的元素
// System.out.println(array.remove(1));
//IndexOutOfBoundsException
// System.out.println(array.remove(3));
//public E set(int index,E element):修改指定索引处的元素,返回被修改的元素
// System.out.println(array.set(1,"javaee"));
//IndexOutOfBoundsException
// System.out.println(array.set(3,"javaee"));
//public E get(int index):返回指定索引处的元素
// System.out.println(array.get(0));
// System.out.println(array.get(1));
// System.out.println(array.get(2));
//System.out.println(array.get(3)); //?????? 自己测试
//public int size():返回集合中的元素的个数
System.out.println(array.size());
//输出集合
System.out.println("array:" + array);
}
}
3.3 ArrayList遍历
3.3.1 遍历字符串
public class ArrayListDemo3 {
public static void main(String[] args) {
/*
创建一个存储字符串的集合,存储3个字符串元素,使用程序实现在控制台遍历该集合
*/
//1.创建集合对象
ArrayList<String> list = new ArrayList<>();
//2.添加元素
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
//3.遍历
//快捷键:
//list.fori 正向遍历
//list.forr 倒着遍历
System.out.print("[");
for (int i = 0; i < list.size(); i++) {
//i 依次表示集合里面的每一个索引
if(i == list.size() - 1){
//最大索引
System.out.print(list.get(i));
}else{
//非最大索引
System.out.print(list.get(i) + ", ");
}
}
System.out.print("]");
}
}
3.3.2 遍历基本数据类型
- 注意细节:集合中只能存储引用数据类型,因此在指定指定集合中添加基本数据类型的时候,需要将基本数据类型变成相应的包装类
int - - > Integer
char - - > Character
public class ArrayListDemo4 {
public static void main(String[] args) {
//1.创建集合
ArrayList<Integer> list = new ArrayList<>();
//2.添加元素
//jdk5之后 int Integer 之间是可以相互转化的
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
//3.遍历集合
System.out.print("[");
for (int i = 0; i < list.size(); i++) {
if (i == list.size() - 1) {
System.out.print(list.get(i));
} else {
System.out.print(list.get(i) + ",");
}
}
System.out.println("]");
}
}
3.3.3 遍历引用数据类型
public class ArrayListDemo4 {
public static void main(String[] args) {
/*
创建一个存储学生对象的集合,存储3个学生对象,使用程序实现在控制台遍历该集合
*/
//1.创建集合对象,用来存储数据
ArrayList<Student> list = new ArrayList<>();
//2.创建学生对象
Student s1 = new Student("zhangsan",16);
Student s2 = new Student("lisi",15);
Student s3 = new Student("wangwu",18);
//3.把学生对象添加到集合中
list.add(s1);
list.add(s2);
list.add(s3);
//4.遍历
for (int i = 0; i < list.size(); i++) {
//i 依次表示集合中的每一个索引
Student stu = list.get(i);
System.out.println(stu.getName() + ", " + stu.getAge());
}
}
}
3.4 案例:查找用户的索引
public class ArrayListDemo6 {
public static void main(String[] args) {
/*
需求:
1,main方法中定义一个集合,存入三个用户对象。
用户属性为:id,username,password
2,要求:定义一个方法,根据id查找对应的学生信息。
如果存在,返回索引
如果不存在,返回-1
*/
//1.创建集合对象
ArrayList<User> list = new ArrayList<>();
//2.创建用户对象
User u1 = new User("heima001", "zhangsan", "123456");
User u2 = new User("heima002", "lisi", "1234");
User u3 = new User("heima003", "wangwu", "1234qwer");
//3.把用户对象添加到集合当中
list.add(u1);
list.add(u2);
list.add(u3);
//4.调用方法,通过id获取对应的索引
int index = getIndex(list, "heima001");
System.out.println(index);
}
//1.我要干嘛? 根据id查找对应的学生信息
//2.我干这件事情需要什么才能完成? 集合 id
//3.方法的调用处是否需要继续使用方法的结果?
//要用必须返回,不要用可以返回也可以不返回
//明确说明需要有返回值 int
public static int getIndex(ArrayList<User> list, String id) {
//遍历集合得到每一个元素
for (int i = 0; i < list.size(); i++) {
User u = list.get(i);
String uid = u.getId();
if(uid.equals(id)){
return i;
}
}
//因为只有当集合里面所有的元素都比较完了,才能断定id是不存在的。
return -1;
}
}
3.5 案例:判断用户的是否存在
public class ArrayListDemo5 {
public static void main(String[] args) {
/*
需求:
1,main方法中定义一个集合,存入三个用户对象。
用户属性为:id,username,password
2,要求:定义一个方法,根据id查找对应的学生信息。
如果存在,返回true
如果不存在,返回false
*/
//1.定义集合
ArrayList<User> list = new ArrayList<>();
//2.创建对象
User u1 = new User("heima001","zhangsan","123456");
User u2 = new User("heima002","lisi","12345678");
User u3 = new User("heima003","wangwu","1234qwer");
//3.把用户对象添加到集合当中
list.add(u1);
list.add(u2);
list.add(u3);
//4.调用方法,查询id是否存在
boolean result = contains(list, "heima001");
System.out.println(result);
}
//定义在测试类中的方法需要加static
//1.我要干嘛? 我要根据id查询学生是否存在
//2.我干这件事情,需要什么才能完成? 集合 id
//3.方法的调用处是否需要使用方法的结果?
//如果要用,必须返回,如果不用,可以返回也可以不返回
//但是本题明确说明需要返回
public static boolean contains(ArrayList<User> list, String id){
//循环遍历集合,得到集合里面的每一个元素
//再进行判断
for (int i = 0; i < list.size(); i++) {
//i 索引 list.get(i); 元素
User u = list.get(i);
//判断id是否存在,我是拿着谁跟谁比较
//需要把用户对象里面的id拿出来再进行比较。
String uid = u.getId();
if(id.equals(uid)){
return true;//return 关键字:作用就是结束方法。
}
}
//只有当集合里面所有的元素全部比较完毕才能认为是不存在的。
return false;
}
}
3.6 ArrayList源码分析
扩容机制
核心步骤:
- 创建ArrayList对象的时候,虚拟机会在底层利用空参构造创建了一个默认长度为0的数组
- ArrayList类的两个重要成员变量
- elementDate:数组名
- size:数组大小,这个变量有两层含义
①:元素的个数,也就是集合的长度
②:下一个元素的存入位置
- ArrayList类的两个重要成员变量
- 添加第一个元素时,底层会创建一个新的长度为10的数组,添加完毕后,size++
- 图解:
- 图解:
扩容时机一:
3. 当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15;再把所有的元素,全拷贝到新数组中;如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍
扩容时机二:
4. 一次性添加多个数据,扩容1.5倍不够,怎么办呀?:如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。
举个例子:在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据很显然,10扩容1.5倍,变成15,还是不够,怎么办?:此时新数组的长度,就以实际情况为准,就是110
- 添加多个元素图解:
4.Queue与Deque【队列与双端队列】
4.1 Queue:队列
4.1.1 概述
- 队列通常(但不一定)以FIFO(先进先出)方式对元素进行排序,其中的例外是优先级队列,它根据提供的比较器对元素进行排序,或者元素的自然顺序,以及LIFO队列(或堆栈),它们对元素LIFO(后进先出)进行排序。 无论使用什么顺序,队列的头部是通过调用
remove()
或poll()
删除的元素。 - 在FIFO队列中,所有新元素都插入队列的尾部
4.1.2 方法摘要
方法 | 说明 |
---|---|
boolean add(E e) | 将指定的元素插入此队列 |
E element() | 检索但不删除此队列的头部。 |
boolean offer(E e) | 入队 |
E peek() | 检索但不删除此队列的头部 |
E poll() | 出队 |
E remove() | 检索并删除此队列的头部 |
4.2 Deque:双端队列
4.2.1 概述
- 此接口扩展了
Queue
接口,当deque
用作队列时,会产生FIFO
(先进先出)行为,元素在双端队列的末尾添加并从头开始删除, 继承自Queue
接口的方法与Deque
方法完全等效 Deques
也可以用作LIFO
(后进先出)堆栈,应优先使用此接口,而不是旧版Stack
, 当deque
用作堆栈时,元素将从双端队列的开头推出并弹出, 堆栈方法相当于Deque
方法,- 当
deque
用作队列或堆栈时,peek
方法同样有效;在任何一种情况下,元素都是从双端队列的开头绘制的
4.2.2 方法摘要
方法 | 说明 |
---|---|
int size() | 返回此双端队列中的元素数 |
void push(E e) | 从此双端队列表示的堆栈中压入一个元素 |
E pop() | 从此双端队列表示的堆栈中弹出一个元素 |
boolean contains(Object o) | 如果此双端队列包含指定的元素,则返回 true |
boolean addAll(Collection<? extends E> c) | 在此双端队列的末尾添加指定集合中的所有元素 |
boolean offerFirst(E e) | 将指定元素插入此双端队列的前面,除非它违反容量限制 |
boolean offerLast(E e) | 在此双端队列的末尾插入指定的元素,除非它违反容量限制 |
E pollLast() | 检索并删除此双端队列的最后一个元素,如果此双端队列为空,则返回 null |
E pollFirst() | 检索并删除此双端队列的第一个元素,如果此双端队列为空,则返回 null |
E peekFirst() | 检索但不删除此双端队列的第一个元素,如果此双端队列为空,则返回 null |
E peekLast() | 检索但不删除此双端队列的最后一个元素,如果此双端队列为空,则返回 null |
E getFirst() | 检索但不删除此双端队列的第一个元素 |
E getLast() | 检索但不删除此双端队列的最后一个元素 |
boolean remove(Object o) | 从此双端队列中删除第一次出现的指定元素 |
E removeFirst() | 检索并删除此双端队列的第一个元素 |
boolean removeFirstOccurrence(Object o) | 从此双端队列中删除第一次出现的指定元素 |
E removeLast() | 检索并删除此双端队列的最后一个元素 |
boolean removeLastOccurrence(Object o) | 从此双端队列中删除最后一次出现的指定元素 |
5.LinkedList集合
5.1 概述
- LinkedList 继承了 AbstractSequentialList 类,同时实现了List、Queue、Deque接口,可进行列表的相关操作
- LinkedList 同时还可以作为队列使用,进行队列相关的操作(这个特征在使用Java语言刷LeetCode的时候有大用)
- LinkedList 实现了 Cloneable 接口,可实现克隆
- LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输
- LinkedList底层是双链表数据结构实现,优点是适合动态插入和删除元素,缺点是随机查找和遍历速度比较慢
5.2 特有方法
-
特有方法:
方法名 说明 public void addFirst(E e)
在该列表开头插入指定的元素 public void addLast(E e)
将指定的元素追加到此列表的末尾 public E getFirst()
返回此列表中的第一个元素 public E getLast()
返回此列表中的最后一个元素 public E removeFirst()
从此列表中删除并返回第一个元素 public E removeLast()
从此列表中删除并返回最后一个元素 public boolean offer(E e)
向链表末尾添加元素,返回是否成功,成功为 true,失败为 false。 public boolean offerFirst(E e)
头部插入元素,返回是否成功,成功为 true,失败为 false。 public boolean offerLast(E e)
尾部插入元素,返回是否成功,成功为 true,失败为 false。 -
示例代码
public class MyLinkedListDemo4 { public static void main(String[] args) { LinkedList<String> list = new LinkedList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); // public void addFirst(E e) 在该列表开头插入指定的元素 //method1(list); // public void addLast(E e) 将指定的元素追加到此列表的末尾 //method2(list); // public E getFirst() 返回此列表中的第一个元素 // public E getLast() 返回此列表中的最后一个元素 //method3(list); // public E removeFirst() 从此列表中删除并返回第一个元素 // public E removeLast() 从此列表中删除并返回最后一个元素 //method4(list); } private static void method4(LinkedList<String> list) { String first = list.removeFirst(); System.out.println(first); String last = list.removeLast(); System.out.println(last); System.out.println(list); } private static void method3(LinkedList<String> list) { String first = list.getFirst(); String last = list.getLast(); System.out.println(first); System.out.println(last); } private static void method2(LinkedList<String> list) { list.addLast("www"); System.out.println(list); } private static void method1(LinkedList<String> list) { list.addFirst("qqq"); System.out.println(list); } }
5.3 LinkedList源码分析
- 核心步骤如下:
- 刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null
- 添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值
- 添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值
- 图解:
6. Vector与Stack
6.1 Vector
Vector也是List接口的实现类,内部也是通过数组来实现。
Vector类的代码声明如下所示:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}
与ArrayList不同的是,Vector是线程安全的,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。不过这也造成Vector的缺点:实现线程的同步需要额外的花费,因此它的访问速度会比ArrayList慢一些。
可以认为Vector是ArrayList在多线程环境下的实现版本。
所以Vector类的使用方法和ArrayList基本一样,只需修改下声明处的代码即可:
List<String> platformList = new Vector<>();
由于要支持线程同步,因此Vector类的很多方法都有synchronized关键字,如下所示:
public synchronized boolean isEmpty() {
return elementCount == 0;
}
public synchronized int size() {
return elementCount;
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
6.2 Stack
6.2.1 概述
Stack
类表示后进先出(LIFO)对象堆栈。 它通过五个操作扩展了类Vector
,允许将向量视为堆栈。 提供了通常的push
和pop
操作,以及在堆栈顶部项目中的peek
的方法,用于测试堆栈是否为empty
的方法,以及用于项目的堆栈的方法以及发现它的距离search
是从顶部。Deque
接口及其实现提供了更完整和一致的LIFO堆栈操作集,应优先使用此类Deque<Integer> stack = new ArrayDeque<Integer>();
6.2.2 方法摘要
构造方法
构造器 | 描述 |
---|---|
Stack() | 创建一个空堆栈 |
成员方法摘要
方法 | 描述 |
---|---|
boolean empty() | 测试此堆栈是否为空 |
E peek() | 查看此堆栈顶部的对象,而不将其从堆栈中删除 |
E pop() | 移除此堆栈顶部的对象,并将该对象作为此函数的值返回 |
E push(E item) | 将项目推到此堆栈的顶部 |
int search(Object o) | 返回对象在此堆栈上的从1开始的位置 |
7.ArrayList、LinkedList、Vector的区别
7.1 相同点
ArrayList、LinkedList、Vector都实现了List接口,所以使用方式很类似,通过上面的示例也能发现这一点。
7.2 不同点
但是ArrayList、LinkedList、Vector的内部实现方式不同,也就导致了它们之间是有区别的。
7.2.1 存储结构
ArrayList和Vector是基于数组实现的,LinkedList是基于双向链表实现的。
这也就导致ArrayList适合随机查找和遍历,而LinkedList适合动态插入和删除元素。
关于数组和双向链表,这里不做详解,后续会单独写篇文章总结。
7.2.2 线程安全性
ArrayList和LinkedList是线程不安全的,Vector是线程安全的。
Vector可以看做是ArrayList在多线程环境下的另一种实现方式,这也导致了Vector的效率没有ArraykList和LinkedList高。
如果要在并发环境下使用ArrayList或者LinkedList,可以调用Collections类的synchronizedList()方法:
Collections.synchronizedList(platformList);
7.2.3 扩容机制
ArrayList和Vector都是使用Object类型的数组来存储数据的,ArrayList的默认容量是0,Vector的默认容量是10。
空说无凭,我们先看下ArrayList的使用示例:
List<String> strArrayList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
strArrayList.add(String.valueOf(i));
}
执行的ArrayList构造函数的源码为:
transient Object[] elementData;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
再看下Vector的使用示例:
List<String> strVector = new Vector<>();
for (int i = 0; i < 30; i++) {
strVector.add(String.valueOf(i));
}
执行的Vector构造函数的源码为:
protected Object[] elementData;
protected int capacityIncrement;
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
当向这两种类型中添加元素时,若容量不够,就会进行扩容,扩容的本质是产生一个新数组,将原数组的数据复制到新数组,再将新的元素添加到新数组中,使用的方法是Arrays.copyOf()
,其中ArrayList扩容后的容量是之前的1.5倍,Vector默认情况下扩容后的容量是之前的2倍。
仍然使用上面的ArrayList的例子:
List<String> strArrayList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
strArrayList.add(String.valueOf(i));
}
在执行完List<String> strArrayList = new ArrayList<>();
后,此时strArrayList的容量是0,
然后在添加第1个元素时,strArrayList的容量会扩容为容量10,
当添加第11个元素时,strArrayList的容量会扩容为容量15,
当添加第16个元素时,strArrayList的容量会扩容为容量22,
如果还需要扩容,依次会扩容到33–>49。
看下ArrayList的源码,就明白为什么会这样扩容:
private static final int DEFAULT_CAPACITY = 10;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
最核心的代码就是int newCapacity = oldCapacity + (oldCapacity >> 1);
,所以ArrayList扩容后的容量是之前的1.5倍。
再看下上面的Vector例子:
List<String> strVector = new Vector<>();
for (int i = 0; i < 30; i++) {
strVector.add(String.valueOf(i));
}
在执行完List<String> strVector = new Vector<>();
后,此时strVector的容量是10,
当添加第11个元素时,strVector的容量会扩容为容量20,
当添加第21个元素时,strVector的容量会扩容为容量40,
如果还需要扩容,依次会扩容到80–>160。
看下Vector的源码,就明白为什么会这样扩容:
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
最核心的代码就是int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
,所以Vector默认情况下扩容后的容量是之前的2倍。
7.2.4 效率
ArrayList随机查找和遍历的效率会高一些,但动态插入和删除元素的效率会低一些。
LinkedList动态插入和删除元素的效率会高一些,但随机查找和遍历的效率会低一些。
如果需要在多线程下操作集合元素,建议使用Vector,否则的话,建议使用ArrayList。
码就是int newCapacity = oldCapacity + (oldCapacity >> 1);
,所以ArrayList扩容后的容量是之前的1.5倍。
再看下上面的Vector例子:
List<String> strVector = new Vector<>();
for (int i = 0; i < 30; i++) {
strVector.add(String.valueOf(i));
}
在执行完List<String> strVector = new Vector<>();
后,此时strVector的容量是10,
当添加第11个元素时,strVector的容量会扩容为容量20,
当添加第21个元素时,strVector的容量会扩容为容量40,
如果还需要扩容,依次会扩容到80–>160。
看下Vector的源码,就明白为什么会这样扩容:
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
最核心的代码就是int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
,所以Vector默认情况下扩容后的容量是之前的2倍。
7.2.4 效率
ArrayList随机查找和遍历的效率会高一些,但动态插入和删除元素的效率会低一些。
LinkedList动态插入和删除元素的效率会高一些,但随机查找和遍历的效率会低一些。
如果需要在多线程下操作集合元素,建议使用Vector,否则的话,建议使用ArrayList。