java 基础知识(三)
Arraylist与Vector的区别
这几天工作有点忙,有很多代码需要写,更新文章有点慢,说声抱歉,前几天有人反馈LinkedList的文章不太看得懂,临时准备补两篇文章。
前几篇文章我们重点说了ArrayLIst,是时候放出这张图了。
![](https://pic3.zhimg.com/80/v2-76c3c04de2e8609c488fa0081fb99c26_720w.png)
这张图里的内容对我们学习Java来说,非常的重要,白色的部分是需要去了解的,黄色部分是我们要去重点了解的,不但要知道怎么去用,至少还需要读一次源码。绿色部分内容已经很少用了,但在面试题中有可能会问到,我们来看一个经常出现的面试题:Arraylist与Vector的区别是什么?
首先我们给出标准答案:
1、Vector是线程安全的,ArrayList不是线程安全的。
2、ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍。
看上图Vector和ArrayList一样,都继承自List,来看一下Vector的源码
![](https://pic2.zhimg.com/80/v2-053bbb6039358c7cde9a8decaa0435ad_720w.png)
实现了List接口,底层和ArrayList一样,都是数组来实现的。分别看一下这两个类的add方法,首先来看ArrayList的add源码
![](https://pic2.zhimg.com/80/v2-b19db72823330f12eed436798e816591_720w.png)
再看Vector的add源码
![](https://pic1.zhimg.com/80/v2-0fd2ad6622b8c236dc0497a4f02efb00_720w.png)
方法实现都一样,就是加了一个synchronized的关键字,再来看看其它方法,先看ArrayList的remove方法
![](https://pic3.zhimg.com/80/v2-f170ed726954b56297d66f18723b1e22_720w.png)
再看Vector的remove方法
![](https://pic3.zhimg.com/80/v2-4e8ad400f1cb2aeef1f4b58ad4a544be_720w.png)
方法实现上也一样,就是多了一个synchronized关键字,再看看ArrayList的get方法
![](https://pic2.zhimg.com/80/v2-33fca77d43ecdb99234456ee032e31e5_720w.png)
Vector的get方法
![](https://pic3.zhimg.com/80/v2-a6d3bbf74f90e3f31e607a831ae40aee_720w.png)
再看看Vector的其它方法
![](https://pic2.zhimg.com/80/v2-f70120e40384d3fc4947085f08f37d9d_720w.png)
无一例外,只要是关键性的操作,方法前面都加了synchronized关键字,来保证线程的安全性。当执行synchronized修饰的方法前,系统会对该方法加一把锁,方法执行完成后释放锁,加锁和释放锁的这个过程,在系统中是有开销的,因此,在单线程的环境中,Vector效率要差很多。(多线程环境不允许用ArrayList,需要做处理)。
至于底层数组的扩容区别,这里就不带着大家读源码了,有兴趣的朋友大家自己读吧,底层代码几乎是一样的,不同的只是计算后的新数组长度不一致。
和ArrayList和Vector一样,同样的类似关系的类还有HashMap和HashTable,StringBuilder和StringBuffer,后者是前者线程安全版本的实现。希望以后大家在面试过程中,能说出个因为所以,而不是一味的去背面试题,唯有理解,无需再背。
注:关于线程安全性,后续文章会说,这里只是简单说这两个类不一样的地方。
Java数据结构之线性表
这篇文章我们来说说Java里一个很重要的数据结构——线性表,还是这张图,线性表对应着下图里的List。
![](https://pic3.zhimg.com/80/v2-a59df74b378ae40099a2206c16eae91a_720w.png)
红框里的内容就是线性表的大家族了,其中黄色部分是要重点了解的,线性表里的元素是按线性排列的(这里的线性指逻辑上的) 线性表分为两大类,分别是顺序表和链表:
一、顺序表
顺序表中的数据元素存储是连续的,内存划分的区域也是连续的。存储结构如下图:
![](https://pic4.zhimg.com/80/v2-ef9c513a7a4f197144321a9c79697dbb_720w.png)
我们的ArrayList底层是数组实现的,底层元素在内存中是按顺序排列的,ArrayList是Java中顺序表的体现。
二、链表
链表在物理存储上通常是非连续、非顺序的方式存储的,数据元素的逻辑顺序是通过链表中的引用来实现的。
1、单向链表
很简单,内存中的对象是随机分布的,对象不但存储了张三、李四等数据,还持有一个next引用,指向下一个对象,来确定一组对象的逻辑顺序。
![](https://pic2.zhimg.com/80/v2-4a2ccf15ad532a0a0c243945bf9c1cad_720w.png)
2、循环链表
也很简单,和单向链表一样,只不过最后一个对象的next又指向了第一个对象。
![](https://pic1.zhimg.com/80/v2-91488f7cade76fee1002017b2d5cbf34_720w.png)
3、双向链表
不但持有next引用,指向下一个对象,还持有一个prev引用,指向上一个对象。
![](https://pic4.zhimg.com/80/v2-233e9330cada82fa0cbf5bcab9fbc4f7_720w.png)
记住双向链表这个图,很重要,下一篇文章我们要讲的LinkedList就是以双向链表的方式实现的。
三、栈和队列
栈和队列是两种比较特殊的线性表。
1、栈
栈是一种操作受限制的线性表。其限制是仅允许在线性表的尾部进行添加和删除操作,这一端被称为栈顶,另一端称为栈底。向一个栈添加新元素叫压栈,删除元素又称为出栈。
![](https://pic3.zhimg.com/80/v2-e45cc164ad4d4fa03b20d6469afb756a_720w.png)
如上图,把赵六放入到栈中叫压栈,不放入赵六,直接取出(删除)王五的过程叫出栈,只能从栈顶放入和删除元素。本文第一张图里的Stack就是栈在Java中的实现。举个例子,最后洗好的盘子都是叠放在最上面的,但每次用的时候都是从最上面拿,最先洗好的盘子反而不容易用到。
2、队列
队列也是一种操作受限制的线性表。只能从头部删除(取出)元素,从队尾添加元素,进行删除操作的端称为队头。
![](https://pic1.zhimg.com/80/v2-7a337823381617bf734e46888034d724_720w.png)
看上面的图,只能从队尾添加元素,队头取出(删除)元素。本文第一张图里的Queue就是队列的体现,Queue是基于链表来体现的。注意,Queue是一个接口,直接写如下代码是会报错的
Queue queue = new Queue();//会报错,Queue是接口,不允许实例化
正确的用法是
Queue queue = new LinkedList();// 正确的用法,基于链表来实现
队列的链表实现是通过子类LinkedList来实现的,Queue接口收窄了LinkedList的访问权限,只提供从队尾,队头等的操作。
为了加深大家的印象,我举一个例子,恶心了一点,但保证大家能记住,大家在喝啤酒的过程中,正常去厕所小解的,这个过程叫做队例。喝多了吐出来的过程,叫做栈。
以上就是Java线性表的介绍,面试中会经常被问起,后续文章会把重点都说一下,希望对大家能有所帮助。
LinkedList初探
在前面的文章里,我们讲了数组和ArrayList,在现实中,不管什么系统,如果不考虑性能的话,用其中的一个就可以完成所有工作,那为什么不用它们来进行所有的数据存储呢?
在数组/ArrayList中读取和存储(get/set)的性能非常高,为O(1),但插入(add(int index, E element))和删除(remove(int index))却花费了O(N)时间,效率并不高。
今天我们来看Java中的另一种List即LinkedList,LinkedList是基于双向链表来实现的,关于链表的知识我们Java数据结构之线性表 - 知乎专栏一文中有过介绍,HashMap和链表也有关系,所以我们要先讲它,话不多说,上代码。
![](https://pic1.zhimg.com/80/v2-dd882f9596c2f578d740c34c422d4f5c_720w.png)
这段代码和我们之前的往ArrayList添加元素的代码基本上是一模一样的,只是修改了红框的内容为LinkedList,这时候再往里添加元素,调用的就是LinkedList里面的add方法了。在之前的 面向对象 一文中我们已经说过了,这是多态的体现,利用好多态,在编码过程中,我们可以少修改很多东西,忘记了的朋友可以回过头去看一下。一看到new这个关键字,我们脑海里应该是,这货在堆内存中开辟了一块空间,我们先从构造函数入手吧。
![](https://pic2.zhimg.com/80/v2-e389bcfa9b1d93c00da6daa32e0f1b1d_720w.png)
满怀希望的打开构造函数,好伤心,里面没有任何逻辑,只能从成员变量入手。
![](https://pic4.zhimg.com/80/v2-52fcf459f29176f450c28cb2ff127587_720w.png)
发现三个成员变量,size就不多说了,大家猜一下就知道是LinkedList的逻辑长度,初始化为0,并持有两个Node引用,first看名字一猜就是第一个,last看名字就是最后一个,我们先来画一画
![](https://pic4.zhimg.com/80/v2-383f405b2da901ec2a15bb41b2f515ef_720w.png)
初始化完了,在堆内存中就是这个样子,size为0。引用类型的成员变量初始化为null,再来看一看这个Node是什么东东
![](https://pic3.zhimg.com/80/v2-c5e31c6e2aeb187e0b07532d41e6012e_720w.png)
这是一个内部静态私有类,该类只能在LinkedList中访问,先记住它,debug看一下
![](https://pic4.zhimg.com/80/v2-c2ccac91c4032bf14e3fb9f5a5d6f883_720w.png)
和我们图中一致,我们继续执行码里的add方法,看源码
![](https://pic4.zhimg.com/80/v2-d2d16c32c3c3c9595b715c9318fa7eab_720w.png)
很普通,e是我们往里添加的Person对象“张三”,继续跟踪linkLast方法:
![](https://pic4.zhimg.com/80/v2-ff2efb640c70064ba35161a2a8f9645b_720w.png)
第一次往LinkedList里添加元素,我们看上图11-1就知道,first为null,last也为null,把我们的Person对象“张三”传给了Node的构造函数,再看Node的构造函数:
![](https://pic1.zhimg.com/80/v2-af25ff1aefe4d25368b931ae8117bf98_720w.png)
用Person张三为入参构造了一个Node对象,好了,又到了画图的时候
![](https://pic3.zhimg.com/80/v2-f6d529d8a762e691baa542cc7628b2ba_720w.png)
老规矩,debug一下:
![](https://pic3.zhimg.com/80/v2-840e0adf5de8d4cd64075e0f980f6c5a_720w.png)
和我们图中所画的一致,我们继续添加“李四”这个Person对象,再打开源码分析一下。
![](https://pic1.zhimg.com/80/v2-f3c1008c491de465d3e2e473d1d6e51c_720w.png)
张三这个Node指向新new出来的Node对象,再看Node是怎么创建的
![](https://pic3.zhimg.com/80/v2-e59ea80468be5280e388a4eac3b76962_720w.png)
创建Node对象,新new出来的Node对象的prev引用指向包含Person张三的Node对象。item引用指向Person李四对象,继续画图:
![](https://pic4.zhimg.com/80/v2-90031819bb499f45a1f2057fe9342aaf_720w.png)
看上图,原来的next引用指向新new出来的Node,同时新new出来的Node的prev引用指向原来的Node对象,item指向新new出来的Person李四这个对象,同时perList这个LinkedList对象的last引用指向新new出来的这个Node,再debug看一下
![](https://pic3.zhimg.com/80/v2-b2bc6f9c5d93f0720daef7b70ba4da1e_720w.png)
好的,继续添加“王五”,“赵六”
![](https://pic4.zhimg.com/80/v2-4d1b775031e67974a4b2bf4c361d7cbb_720w.png)
很简单,没有了底层数组,新增加了一个Node对象,记录了Person的内容,每个Node对象都持有next引用(下一个)和prev引用(上一个),其实就是之前 Java数据结构之线性表 - 知乎专栏 一文里介绍的双向链表,这个图看起来有点乱,多年前我在读这段代码的时候,差点晕过去了,又是next,prew,first,last,容易乱,因此大家在学习源码的过程中,有不明白的地方,找张纸和笔画一画,就清晰了。放张简化版的图,方便大家理解。
![](https://pic3.zhimg.com/80/v2-133cbc952f16dccbde926dd8b21e290e_720w.png)
夜深了,先休息了,大家有什么看不明白的地方,可以在评论区留言,本文在写作过程中如果有什么勘误,还希望细心的读者提出来,下一篇我们研究LinkedList的查找、插入与删除,并引入时间复杂度来分析。
LinkedList元素的删除原理
上一篇文章我们说了LinkedList,并说了往里添加了元素。这篇文章我们来说说LinkedList元素的删除,话不多说,上代码,还是那个Person类
![](https://pic1.zhimg.com/80/v2-55de64bfcae2e4b48e78647811da63a0_720w.png)
还是那两个属性,name,age,提供了一些简单的get与set方法。写我们的main方法
![](https://pic1.zhimg.com/80/v2-8b1b4eba061f2a51daed02d8057f4eb4_720w.png)
和前文一样,new了一个LinkedList,并往里添加了四个元素,看过前文的朋友都知道现在LinkedList目前在堆内存中的样子如下图:
![](https://pic2.zhimg.com/80/v2-99e579b1d1f02664d09aa97189550b6d_720w.png)
现在我们来删除王五这个用户,运行一下
![](https://pic2.zhimg.com/80/v2-4426844c94688e1258f677d9df641c19_720w.png)
看一下结果
![](https://pic2.zhimg.com/80/v2-e50f83c1c0cf5791170a39850e03f969_720w.png)
好奇怪,打印返回的删除状态居然是false,代码中明明删掉王五了,为什么打印的结果还是4?如果经常看本专栏文章的人,大概已经猜到了原因,这个王五是新new出来的,并不是perList里的王五,Person这个类没有重写equals方法,删除元素依赖于equals方法,到底是不是呢,我们来看一下源码:
![](https://pic4.zhimg.com/80/v2-f771fb8a1a0ab71607fb78d4f3000767_720w.png)
不出所料,又看到了熟悉的equals,真是无处不在,这段代码其实是从第一个Node节点(first节点)开始对比item的值,如果equals成功就执行unlink()方法,并返回删除成功的布尔值true。我们画一画查找王五的这个过程。
![](https://pic3.zhimg.com/80/v2-f835263857700c413290a32d8f91820e_720w.png)
大致就是这个样子,就大家仔细看图里的文字描述,现在我们来看看unlink()方法都做了啥
![](https://pic2.zhimg.com/80/v2-6b3305913fcb32bf1e11b3d0a482a6d9_720w.png)
注意,在本示例中,上图的黑色字体注释部分不会执行。好吧,我承认,多年前我看这段代码被绕晕了,prev.next,next.prev都是些什么鬼啊!苍天啊!大地啊!
别怕,上面代码看似烧脑但是逻辑相当简单,就是把包含王五的这个Node从双向链表中移出来,然后把王五相邻的两个Node的next和prev重新指向一下,我们画一下图:
![](https://pic2.zhimg.com/80/v2-578f58033cf890d66a179a23a207f019_720w.png)
简单吧,debug一下看是不是和图中画的一致,测试代码前别忘了在Person里重写equals方法
![](https://pic2.zhimg.com/80/v2-a8328976e97db9e39a69c571b540d82d_720w.png)
debug看一下,已经删除了王五,size也更新成了3。
![](https://pic1.zhimg.com/80/v2-2b09cd9614d77594a34b4b906cfea67c_720w.png)
打印结果也和期待中一样,打印删除状态也为true了。
![](https://pic4.zhimg.com/80/v2-1a4b741bcf7ce18ffee6d9c4f7f14503_720w.png)
说到这儿,我们再来看一下ArrayList以对象方式删除元素的源码,来和LinkedList比较一下
![](https://pic3.zhimg.com/80/v2-c8f5db21923aa529886675ecb0e019f6_720w.png)
再看fastRemove()方法
![](https://pic1.zhimg.com/80/v2-206a4aad1a838a49654a9a6672133454_720w.png)
大体上一致,两者都在元素中循环查找,LinkedList是把Node(包含Person)从链表的移出(通过修改上下节点的引用来实现),ArrayList删除底层数组元素后又把底层数组都往前复制了一格内容(忘记了的朋友可以复习一下,传送门:ArrayList的元素删除),现在我们来比较一下这两者间的时间复杂度。
假设要删除的元素都在这两个List中的第n位置,由于两者都循环查找了n次,省略循环查找这个步骤,说以我们直接看删除,前面的一系列文章中我们已经讲过了,由于ArrayList删除元素后,底层数组要往前复制一格,ArrayList底层数组删除元素时间复杂度为O(n)。再来看LinkedList,LinkedList底层链表删除元素只是简单的修改了一下引用地址,时间复杂度为O(1)。
由以上推断看来,LinkedList的删除效率似乎要好很多,实际真的如此吗?答案是不一定。下一篇文章我们将写一段代码来分析一下,LinkedList和ArrayList在删除元素时的真实效率。
以上我们说的删除用的是List的如下API
public boolean remove(Object o);
LinkedList还有一种删除方式,用下标方式删除,如下
public E remove(int index);
下一篇文章一起讲解。
注:示例中,用对象的方式来删除元素,只是想告诉大家,这种删除方式是用equals方法来查找元素进而删除的,实际工作中很少遇到需要new一个对象去删除的情况。不建议一上来就重写equals方法,除非你有特殊的需求。如果重写了equals方法,请一并重写hashCode方法,这个问题在说说Java里的equals(中)一文中已经说过了。