J2EE
目录
一、UML,基于面向对象的统一建模语言,关于UML的学习可以看这篇博文,下图反映出了各集合之间的关系。
前言:分享关于我对Collection接口下的List接口的学习总结!
一、UML,基于面向对象的统一建模语言,关于UML的学习可以看这篇博文,下图反映出了各集合之间的关系。
二、Vector
①特点:同步的,单线程,速度慢,安全。
②底层数据结构:数组
③方法:
package com.zwf.test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) throws Exception {
//实例化Vector集合,指定泛型String类型
Vector<String> v=new Vector<>();
for (int i = 1; i < 101; i++) {
//添加元素
v.add(""+i);
//当前操作的Vector集合中低层存放数据的容器的长度
getVeLen(v,i);
}
//遍历
//取得Vector中的所有元素
Enumeration<String> elements = v.elements();
//如果含有更多元素
while(elements.hasMoreElements()) {
//打印下一个元素
System.out.println(elements.nextElement());
}
}
/**
* 返回当前操作的Vector集合中低层存放数据的容器的长度
* @param v 正在操作的集合
* @throws Exception 直接抛出最大异常
*/
public static void getVeLen(Vector<String> v,int i) throws Exception {
Field field = v.getClass().getDeclaredField("elementData");
field.setAccessible(true);
Object obj = field.get(v);
Object[] elementData = (Object[])obj;
System.out.println(i+"当前操作的Vector集合中低层存放数据的容器的长度:"+elementData.length);
}
}
我们点进Vector方法源码中可以看到,Vector这个类中的所有方法都被synchronized关键字修饰,所以Vector具有同步,安全的特性。源码图如下:
为了有效看出效果,我在getVelen方法中添加了一个参数就是该循环添加元素时的下标
for循环执行打印结果如下图:
Vector的底层数据结构是数组,从打印结果可看出,每次将数据进行扩容是变成上一次长度的两倍。
三、ArrayList
①特点:有序,可重复,可以储存null值,有下标
②底层数据结构:数组
方法:
package com.zwf.test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) throws Exception {
//实例化集合
ArrayList<String> li=new ArrayList<String>();
for (int i = 1; i < 101; i++) {
//添加元素
li.add(""+i);
//打印底层容器长度
getArrayLen(li, i);
}
//删除
System.out.println(li.remove(1));//输出结果:2
System.out.println(li.remove("1"));//输出结果:true
//查询
System.out.println(li.remove(1));//输出结果:4
System.out.println(li.remove("1"));//输出结果:false
}
/**
* 返回该集合底层容器当时的长度
* @param li 正在操作的集合
* @param i 下标
* @throws Exception 抛出异常
*/
public static void getArrayLen(ArrayList<String> li,int i) throws Exception {
Field field = li.getClass().getDeclaredField("elementData");
field.setAccessible(true);
Object obj = field.get(li);
Object[] elementData = (Object[])obj;
System.out.println(i+"当前操作的ArrayList集合中低层存放数据的容器的长度:"+elementData.length);
}
}
输出结果为2-->true-->4-->false的原因:
①代码的执行顺序是从上往下的,集合长度是可变的。
②第一个移除将下标为1的元素2给移除掉了,该方法是返回对应下标的元素,因此结果为2
②第一个移除将下标为1的元素2给移除掉了,该方法是返回对应下标的元素,因此结果为2
③第二个移除是将元素为1的元素给移除掉了,该集合中在这是仅仅被移除了元素2,元素1还存在,所以返回true
④第三移除移除的下标是1,为什么对应的元素变成4了呢?ArrayList集合的长度可变,在执行完②③后集合下标为1的元素是4
⑤元素1在②中已经被移除了,所有该方法找不到对应元素进行移除,所有结果为false
三种遍历方式
//1.forEach
for (Object obj : list) {
System.out.println(obj);
}
//2.for循环
for (int i = 0; i < al.size(); i++) {
System.out.println(al.get(i));
}
//3.iterator迭代器
Iterator it = al.iterator();
while(it.hasNext()) {
int obj = (Integer)it.next();
System.out.println(it.next());
}
注意呐!!!
①如果我们使用forEach或者迭代器遍历中又使用遍历的方法来移除集合中的元素,会报以下错误
出现原因:不允许的并发修改操作,遍历计数器的不均等导致
一个对象在不允许多个线程同时对其进行修改,一个迭代器正在迭代遍历这个集合时又出现一个额外的迭代器进行修改操作,这时就会报这个错。
我们来看源码:
//list.remove(index) 源码
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
//AbstractList类的hasNext()和next()方法源码
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
private void checkForComodification() {
if (this.modCount != l.modCount)
throw new ConcurrentModificationException();
}
我们在遍历时,next()中调用了checkForComodification(),在remove()方法中也调用了该方法,在在删除时modCount被改变所以会导致modCount!=expectedModCount,这是就会抛出该错误。
解决方法:在迭代器内部使用迭代器本身进行删除操作。
iterator.remove();
②利用for循环下标进行移除,根据集合长度可变的特性可得集合长度在不断变短,所有要注意下标越界错误,可以利用for循环方法来删除集合中下标为偶数或者技术的元素。
代码如下:
ArrayList<String> li=new ArrayList<String>();
for (int i = 1; i < 101; i++) {
li.add(""+i);
}
//移除下标为偶数的元素。集合内容是1-100,所以该操作可以是移除集合中奇数
for (int i = 0; i < 50; i++) {
li.remove(i);
}
//移除下标为奇数的元素
//for (int i = 0; i < 50; i++) {
// li.remove(i+1);
//}
for (String s : li) {
System.out.println(s);
}
去重复
package com.zwf.test;
import java.util.ArrayList;
public class Test2 {
public static void main(String[] args) {
ArrayList al = new ArrayList<>();
// 自定义的对象
al.add(new Person("zs", "18"));
al.add(new Person("ls", "22"));
al.add(new Person("ww", "21"));
al.add(new Person("mazi", "18"));
al.add(new Person("zs","18"));
//打印集合的长度、集合中的对象
System.out.println(al.size());
System.out.println(al;
//去重复操作
ArrayList newAll = repeatList(al);
//打印去重复后的集合长度、集合中的对象
System.out.println(newAll.size());
System.out.println(newAll);
}
/**
* 去重复的方法
* @param al 要进行去重复操作的集合
* @return 去重复之后的一个新集合
*/
private static ArrayList repeatList(ArrayList al) {
ArrayList newAll = new ArrayList<>();
for (Object obj : al) {
if (!newAll.contains(obj)) {
newAll.add(obj);
}
}
return newAll;
}
}
/**
* 定义一个Person类
* @author zjjt
*
*/
class Person {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
public Person(String name, String age) {
super();
this.name = name;
this.age = age;
}
public Person() {
super();
}
/**
* 重写equals方法
*/
@Override
public boolean equals(Object obj) {
//如果obj属于Person类
if(obj instanceof Person) {
//强转为Person类,注意:需要先判断属于Person类才能进行该操作,否则会报错
Person p = (Person)obj;
//打印这是谁和是正在比较
System.out.println(this.getName() +"---equals---"+ p.getName());
//返回比较后的结果 相等为true 否则false
return this.getName().equals(p.getName())
&& this.getAge() == p.getAge();
}
return false;
}
}
打印结果如下图:
通过打印的结果成功将重复的Person("zs","18")去除掉了。这里要注意了:
这里需要补充一个equals和==的区别
①equals:引用类型:Object比较的是地址值,但是继承了他的类大多数是比较成员变量的值是否相等,即比较是否是同一个对象。可以理解为比较内容
②== : 引用类型:比较的是地址是否相同 基本数据类型:比较值是否相同
去重复原理:
补充:java中的new操作时新开辟了一个空间给这个对象,所有会导致地址不同,java中的指针概念并没有C语言那么强,进行了一些伪装。
1、不重写equals方法,Person的比较会调用系统中的equals方法
//contains(obj)源码
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//进行元素判断
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//equals(E)源码
public boolean equals(Object obj) {
return (this == obj);
}
contains调用indexOf调用equals ,所以集合的contains()方法最后调用的equals方法比较的是地址。
2、重写equals,调用该equals方法
我们可以更具我们的需求来重写我们的equals方法来达到我们想要的效果。
四、LinkedList
①底层数据结构:链表
②方法,实现了List接口,List有的方法Vector、ArrayList、LinkedList都具备。
③可以作为FIFO(先进先出)的队列,作为LIFO(后进先出)的栈
具有队列堆栈特性的容器代码:
package com.zwf.test;
import java.util.LinkedList;
import java.util.List;
/**
* 利用LinkedList完成具有队列、堆栈特性的容器
* @author zjjt
*
*/
public class Test3 {
public static void main(String[] args) {
//实例LinkedList集合用来作为队列和堆栈的底层数据结构
LinkedList<String> li=new LinkedList<String>();
//实例化手撸队列对象
DuiLie dl=new DuiLie(li);
dl.push("a");
dl.push("b");
dl.push("c");
System.out.println(dl.pop());
System.out.println(dl.pop());
System.out.println(dl.pop());
System.out.println("-----------------------");
DuiZhan dz=new DuiZhan(li);
dz.push("a");
dz.push("b");
dz.push("c");
System.out.println(dz.pop());
System.out.println(dz.pop());
System.out.println(dz.pop());
}
}
/**
* 队列类
* @author zjjt
*
*/
class DuiLie {
/**
* 定义该堆列的数据结构类型
*/
LinkedList<String> li=null;
/**
* 有参构造
* @param li
*/
public DuiLie(LinkedList<String> li) {
this.li=li;
}
/**
* 移除元素方法
* @return 被移除的元素
*/
public String pop() {
return li.removeFirst();
}
/**
* 增加元素方法
* @param obj 需要的元素
*/
public void push(Object obj) {
li.addLast((String)obj);
}
}
/**
* 堆栈类
* @author zjjt
*
*/
class DuiZhan{
/**
* 定义堆栈的数据结构类型
*/
LinkedList<String> li=null;
/**
* 有参构造
* @param li
*/
public DuiZhan(LinkedList<String> li) {
this.li=li;
}
/**
* 移除元素方法
* @return 被移除的元素
*/
public String pop() {
return li.removeLast();
}
/**
* 增加元素方法
* @param obj 需要增加的元素
*/
public void push(Object obj) {
li.addLast((String)obj);
}
}
解析:
①队列的输出调用了removeFirst()方法,返回出链表第一个元素,并且将其移除,那么下一个元素就变成了第一个,不断的移除第一个实现了FIFO特性。
②堆栈的输出调用了removeLast()方法,同理得每次调用都是不断的从链表末尾进行移除实现LIFO特性。
输出结果:
可以验证队列FIFO(先进先出)的特性,堆栈LOFI(后进先出)的特性。
思考
在这里我们可以思考一个问题,哪一个集合来做我们的堆栈和列队更加合适?
五、ArrayList和LinkedList数据结构图解
ArrayList:数组结构
LinkedList:链表结构
扩容原理
ArrayList的负载因子为0.5
注意:扩容并不是在基础上增加长度,底层数据结构是数组(长度不变),是定义一个长度为 原长度*1.5 的新数组,然后将原数组原封不动的赋值到新数组中,然后把ArrayList的地址指向新数组完成扩容过程。
1.7和1.8版本初始化不同:1.7初始化就是创建容量为10的数组,1.8则是在第一次新增元素时再扩容。
迭代器遍历原理
iterator.hasNext() 返回boolean,如果有下一个元素,光标指向下一位。
那么当一个迭代器进行遍历到其中一个元素时,如果迭代器停止了,之后再进行遍历,那么迭代器是从哪个元素开始呢?代码如下:
Iterator it=li.iterator();
while(it.hasNext()) {
String obj=(String)it.next();
if(obj.equals("50")) {
System.out.println(obj);
break;
}
System.out.println(obj);
}
System.out.println("-------------");
while(it.hasNext()) {
String obj=(String)it.next();
System.out.println(obj);
}
执行后输出结果:
由此可以证明游标是会保留在它跑到过的位置,下次再遍历如果还有下一个元素则从下一个元素开始遍历。如果新建了一个迭代器就是另外有一个新的迭代遍历。使用要注意并发问题。
两个集合的区别:
ArrayList:增加,删除慢。查询、修改快
LinkedList:增加,删除快。查询、修改慢
速度不同原因分析
①增加、删除:
在ArrayList的b,c中增加一个元素g,b之后的元素都将向后移动一位,然后将g插入到空出来的位置。如果后面元素非常多,那么将是一个很大的操作,性能会有所降低。删除同理。
在LinkedList中进行增加,由于链表结构的特点。只需要断开b,c之间的联系,建立b和g,c和g的联系即可,发生改动的元素少,速度快。
②查询
Vector,ArrayList,LinkedList都是实现了List接口,所以他们的语法都一样。
所以ArrayList和LinkedList都会有get(下标)方法
易导致误区:LinkedList中的get(下标)方法难道也拥有下标?
我个人觉得这里将下标理解为索引其实更好一些,ArrayList中的get(index)方法,由于具备索引,所以调用该方法时会根据索引直接找到对应下标的元素然后返回。而LinkedList的该方法是从第一个开始找到第i(下标)个元素后返回。速度:ArrayList>LinkedList
③修改
修改肯定嘚先查询到然后在替换,所以速度:ArrayList>LinkedList
六、总结
将上面的内容进行吸收之后,List相关的面试题大部分是能够解决的,还有部分面试题之后会做补充。如果发现有错误的地方请及时指出,谢谢呐!祝大家工作顺利,学业有成!