1 Collection
Collection是其他类的父接口,是更广泛的类别,包含了所有集合类型,本身不提供实现,其他如List、Set等等都继承该类
<E>
为泛型,表示该集合中存放的数据类型,注意必须是引用数据类型(如String),而不能是基本数据类型(如string)
Collection定义了一些基本的操作方法,比如添加元素 add()
,删除元素remove()
,检查集合是否为空isEmpty()
等等,确保集合行为的一致性
初始化集合:
Collection<String> collection = new ArrayList<>();
增加元素:
collection.add("111");
collection.add("222");
System.out.println(collection);
删除数组中首个匹配到的元素:
collection.remove("111");
System.out.println(collection);
复制集合:
Collection<String> collection1 = new ArrayList<>(collection);
System.out.println(Objects.equals(collection,collection1);
清空集合:
collection.clear();
判断集合是否为空:
System.out.println(collection.isEmpty());
System.out.println(collection1.isEmpty());
获取集合内元素数量:
System.out.println(collection1.size());
讲集合转换成数组:
我们可以使用Object[]:
Object[] strings = collection1.toArray(new Object[0]);
for(Object object2 : strings){
System.out.println(object2);
}
但不推荐这么做,我们往往需要确定的数据类型格式:
String[] strings = collection1.toArray(new String[0]);
for (String object2 : strings) {
System.out.println(object2);
}
2 List
2.1 ArrayList
2.1.1 方法示例
List中的元素是有序的,并且可以存在重复元素
对于List部分,实际使用与Collection类似,不再赘述:
ArrayList<String> arrayList = new ArrayList<>();
//List基本方法
//添加元素
arrayList.add("丁真");
arrayList.add("谷爱凌");
arrayList.add("蔡徐坤");
arrayList.add("吴亦凡");
System.out.println(arrayList);
//删除元素
arrayList.remove(2);
System.out.println(arrayList);
//修改元素
arrayList.set(0,"丁真珍珠");
//查询元素
System.out.println(arrayList.get(0));
//获取列表大小
System.out.println(arrayList.size());
//克隆列表
ArrayList<String> arrayList1 = (ArrayList<String>) arrayList.clone();
System.out.println(arrayList1);
//清空列表
arrayList.clear();
//检测是否为空
System.out.println(arrayList.isEmpty());
运行结果:
[丁真, 丁真, 谷爱凌, 蔡徐坤, 吴亦凡] //添加元素
[丁真, 丁真, 蔡徐坤, 吴亦凡] //删除元素
丁真珍珠 //修改元素
4 //获取列表长度
[丁真珍珠, 丁真, 蔡徐坤, 吴亦凡] //克隆列表
true //检测是否为空
2.1.2 底层实现
ArrayList的底层基于数组:
private String data[];
我们使用变量size保存集合内元素的总长度(数量):
private static int size;
我们首先创建两个构造函数,用于数组的初始化:
- 如果传入形参 ,则创建一个指定长度的数组,否则将创建一个长度为0的数组(后续会动态扩容)
public ArrayListDemo() {
data = new String[0];
}
public ArrayListDemo(int length) {
if (length < 0)
throw new IllegalArgumentException("列表长度不可以小于0");
data = new String[length];
}
然后我们新建方法来对数组新增数据:
- 在插入数据之前,我们需要先检查数组长度内知否已放满数据,如果是,我们需要先完成数组扩容,扩容后的新长度为其自身长度 + 自身长度向右移一位(位运算),额外扩大其自身一半的长度(长度为16的数组扩容后为24),
private void grow() {
if (data.length == 1){
Arrays.copyOf(data,data.length + 1);
}else {
Arrays.copyOf(data,data.length + (data.length >> 1));
}
}
- 由于ArrayList是有序数组,我们需要两个
add()
方法,分别应对是否指定数组索引的情况 - 若没有指定索引,我们只需要在数组末尾加入数据,再让变量size自增即可
- 如果指定了索引,我们需要先判断数据是否非法,数组的索引不能小于0,也不能超出数组长度,否则都会抛出异常,而后我们需要把数组索引以及它后面的内容全部向后挪一位,最后将数据放置在指定的索引处,并让变量size自增
public void add(String content) {
if (size >= data.length) {
grow();
}
data[size] = content;
size++;
}
public void add(int index, String content) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引值 " + index + " 越界了");
}
if (size >= data.length) {
grow();
}
//方法一
//for (int i = size - 1; i >= index; i--) {
// data[i + 1] = data[i];
//}
//方法二(推荐)
System.arraycopy(data,index,data,index + 1,size - index);
data[index] = content;
size++;
}
- 补充说明:
System
类中的arraycopy()
方法更适合数组中部分内容的复制(挪动),而Array
类中的copyOf()
方法更适合复制整个数组
若想查询某个元素的索引值,我们需要提供带查询元素的内容,然后在数组中遍历查询第一个符合该值的元素的索引值,并将其返回,如果找不到,就返回-1,并在之后的代码中根据返回值做出回应:
private int indexOf(int userInput) {
for (int i = 0; i < size; i++) {
if (Objects.equals(userInput,data[i])){
return i;
}
}
return -1;
}
若想查询某个元素的内容,我们需要提供待查询元素的索引值,若数据合法,则返回数组对应索引元素的值
private String get(int userInput) {
if (userInput < 0 || userInput >= size){
throw new IndexOutOfBoundsException("索引越界了");
}
return data[userInput];
}
若想修改某个元素,我们需要提供待修改元素的索引值,若数据合法,则返回将传来的String类型值赋值给对应数组索引,即可完成数组元素更新
public void set(String content,int index){
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("索引越界了");
}
data[index] = content;
}
如果想删除数组中的某个元素,我们需要提供待删除的元素索引,如果索引的值合法,我们可以截取数组中待删除元素以后的内容,并让其往前覆盖一个单位,以完成删除某个元素的同时保证数组数据连续
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("索引值 " + index + " 越界了");
}
//方法一
// for (int i = index; i < size - 1; i++) {
// data[i] = data[i + 1];
// }
//方法二(推荐)
System.arraycopy(data,index + 1,data,index,size - index - 1);
size--;
}
2.2 LinkedList
2.2.1 方法示例
节约时间,这里暂时略过,详情见底层实现
2.2.2 底层实现
我们首先要定义一些私有全局变量,他们分别记录了链表的长度、首节点和尾节点:
private int size;
private Node first;
private Node last;
之后,我们建立一个内部类Node
,该类包含每个节点指向上一个节点的引用(我们称其为指针)、指向下一个节点的引用(同上)和该节点保存的数据,该类还提供一个构造函数,接受的形参包括了上述三个节点信息,并把他们存储在当前节点
在LinkedList
中,每一个节点都是独立的对象,我们在对节点进行创建、读取、修改、删除等操作时,通过这些节点对象来完成操作,"这允许我们动态的对链表进行添加节点、删除节点等操作,而不需要大量移动元素",但请注意,这句话仅仅是对链表概念的解释(包括回答面试问题),我们几乎在所有需要用到集合的场景中都使用ArrayList
,而非LinkedList
,LinkedList
看似灵活高效,在实际应用中大量数据产生的大量对象,反而会导致LinkedList的效率并不如ArrayList
代码如下:
class Node {
String item;
Node prev;
Node next;
public Node(String item, Node prev, Node next) {
this.item = item;
this.prev = prev;
this.next = next;
}
}
首先说明的是:first为指向头节点(首节点)的引用,last为指向尾节点的引用,prev为当前节点指向上一个节点的引用,next为当前节点指向下一个节点的引用,item为当前节点存储的数据。为了方便理解,我们后续都使用英文描述节点
当我们向LinkedList
添加元素时,我们需要建立两个方法,分别对应有无提供待插入索引值
若无索引值,分为两种情况
-
若当前链表长度为0,我们需要新建一个节点,并将该链表的first节点和last节点都指向它,最后将记录链表长度的变量size+1
-
若当前链表长度不为0,我们需新建一个节点,将当前链表的last节点的next指向它,并将它的prev指向当前链表的last节点。当建立当前链表last节点与新加入节点的联系后,我们再将当前量表的last节点指向当前节点,这样就完成了链表新增节点,最后将记录链表长度的变量size+1
代码如下:
public void add(String content) {
Node node = new Node(content, null, null);
if (size == 0){
this.first = node;
}else {
this.last.next = node;
node.prev = this.last;
}
this.last = node;
size++;
}
若有索引值,我们需要先判断索引的合法性,他不能小于索引下限值0,也不能超过当前链表长度,而后也分为两类情况第一类:
- 若索引值等于当前链表长度,就等于在链表末尾新增节点,也就是上述无索引值传参的
add()
方法,那么直接调用另一个方法,之后使用return来结束方法,完成节点添加操作,不再赘述
第二类:
若索引值不等于当前链表长度,我们先创建一个Node
对象node,并通过构造函数的方式将形参中的数据存入节点。而后再次分为两种情况:
-
若待插入的索引值为0,将当前节点的next指向当前链表的first节点,并将first节点的next指向当前节点node,这样就把当前节点与当前列表first节点建立了联系,最后再将链表的头节点指向当前节点node,完成对链表头部添加节点的操作
-
- 若索引值不为0,就再次新建一个节点对象node1,并将其指向待插入索引值对应的链表节点(原节点)
- 之后我们首先将原节点node1的上一个节点的next指向新节点node,然后把新节点的prev指向原节点的prev,这样一来我们就切断了源节点node1与他上一个节点的连接,并让上一个节点与新节点node建立连接
- 然后将新节点node的next指向原节点node1,并将原节点node1的prev指向新节点node,这样一来我们在切断node1与上一个节点的基础上,建立新节点node与原节点node1的连接。
- 这样宏观表现就是我们将断开原节点的上一个节点的连接,让上一个节点与新节点连接,而新节点再与原节点连接,达到再指定索引位置插入新节点的效果
最终代码如下:
public void add(String content,int index){
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("索引越界了");
}
if (index == size){
add(content);
return;
}
Node node = new Node(content,null,null);
if (index == 0){
node.next = this.first;
this.first.prev = node;
this.first = node;
}else {
Node node1 = getNode(index);
node1.prev.next = node;
node.prev = node1.prev;
node.next = node1;
node1.prev = node;
}
size++;
}
在上述代码中,我们在查询原节点时用到了getNode()
方法,下面我们将对它展开说明:
在调用getNode()
方法时,我们需要传入索引值进行查询,首先同样需要检查数据合法性,索引不能小于0,也不能超过链表长度
LinkList
是双向循环链表,我们需要从链表的首位作为循环的起点,一般从头节点开始,在查询到期望数据后,将其节点对象返回,代码如下:
private Node getNode(int index) {
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("索引越界了");
}
Node node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
接下来我们该如何修改链表内容呢,当我们调用set()
方法时,需要传入节点数据、节点索引两个参数,首先检查索引合法性,他不能小于0,也不能超过数组长度
之后传入索引值调用getNode()
方法,拿到返回的节点对象,并将想要修改的数据赋值给这个对象的item
同理如果我们想要根据节点数据,查询匹配到的第一个索引值,可以调用indexOf()
方法,由于这里不需要操作索引值,不用担心越界,也就不用再检查合法性
在这个方法中,我们首先创建一个节点对象,并将其指向链表的头节点,之后遍历链表,每次循环都进行一次比对,如果匹配成功,就直接返回i值作为索引值,并结束循环,每一次比对结束后都要让当前节点对象node指向下一个节点,这样才会一条路遍历下去直到尾节点
- 从运行效率的角度思考,我们在对比两个数据时,首先对比他的哈希值,如果哈希值都不同,那么数据一定不相同,直接哒咩,但哈希值相同,数据也不一定相同哦,这个时候我们再考虑它是否为null值,如果不是,再使用
equals()
方法进行字符比对,也就是if((a == b) || (a != null && a.equals(b)))
,而Objects类中已经按照我们上述的条件重写了equals()
方法,因此我们在对比数据时直接调用Objects.equals()
即可,Objects
类中equals()
方法源码如下图:
代码如下:
public int indexOf (String content){
Node node = first;
for (int i = 0; i < size; i++){
if (Objects.equals(node.item,content)){
return i;
}
node = node.next;
}
return -1;
}
当我们想要删除某个节点,我们调用remove()
方法时需要传入待删除节点的索引,所以我们需要先判断索引值是否越界,它不能小于0,也不能大于节点长度,而后分为三种情况:
- 如果待删除结点的索引值为0,也就是我们需要删除链表的头节点。我们将链表的first节点指向first节点的next,再将更新后链表的first节点的prev(也就是已经被丢弃的那个节点)设为null即可
- 如果待删除节点的索引值在0之后,在最后一个节点之前,也就是数学中的[0,n-1),由于索引值从0开始,所以n-1,也就是size-1的值就是为尾节点对应的索引。所以说我们要删除的是尾节点,与第一种情况类似,我们需要先将链表的last节点指向倒数第二个节点,也就是last节点的prev,然后将更新后链表的last节点的next(也就是已经被丢弃的那个节点)设为null
- 如果索引值为前两种之外的情况,既不是头节点,也不是尾节点,那就是链表中间的节点,在数学上用(0,n-1)表示。我们先新建一个节点对象node,通过
getNote()
方法找到想要删除的节点,并让node指向它,之后我们让将要被删除的节点对象node的prev指向待删除节点node的next(也就是让待删除的那个节的与它上一个节点断开连接,让上一个节点绕过待删除节点连接它的下一个节点),然后我们让待删除节点node的next节点的prev指向待删除节点node的prev(也就是让待删除的那个节点与它下一个节点断开连接,让下一个节点绕过待删除的那个节点去连接他的的上一个节点),这样一套流程下来,我们就断开了想要删除的那个结点与链表中的上下连接关系,并让除此之外断开的链表重新连接起来,达到删除的目的。最后让记录链表长度的变量size自减一次即可,这样我们就完成了删除指定索引节点的操作,代码如下:
public void remove(int index){
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("索引越界了");
}
if (index == 0){
this.first = this.first.next;
this.first.prev = null;
}else if (index == size - 1){
this.last = this.last.prev;
this.last.next = null;
}else {
Node node = getNode(index);
node.prev.next = node.next;
node.next.prev = node.prev;
}
size--;
}
如果我们想要清空链表,不要挨个每个节点都清空,由于我们不要这个链表了,以后也不用它了,那我们直接把这个链表的头节点和尾节点都设置为null,这样等于让链表中间的所有节点都与这个链表断开了连接,最后再将用于记录链表长度的变量size设为0,一套下来就达到宏观上清空链表的效果
有的朋友可能心中有疑问,为什么在这里我们不遍历删除节点,而是直接丢弃呢,这样不会浪费内存空间吗?答案是不会的,首先这么做宏观上能快速高效的达到我们想要的结果,而在代码和系统层面,所有的节点都是Node类的对象,当整个程序不存在任何指向这些对象的引用时,JVM自带的内存垃圾回收系统会自动释放内存,所以完全不需要我们来操作
代码如下:
public void clean(){
this.first = this.last = null;
size = 0;
}