集合
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,下面又有三个主要的子接口:List
、Set
和 Queue
。
Java 集合框架如下图所示:
Collection接口
概念:对象的容器,定义了对多个对象进项操作的的常用方法。可实现数组的功能。
与数组的区别:数组长度固定,集合长度不固定;数组可以存储基本类型和引用类型,集合只能存储引用类型。
位置: java.util.*
接口中的方法
boolean add(Object obj) //添加一个对象。
boolean addAll(Collection c) //讲一个集合中的所有对象添加到此集合中。
void clear() //清空此集合中的所有对象。
boolean contains(Object o) //检查此集合中是否包含o对象。
boolean equals(Object o) //比较此集合是否与指定对象相等。
boolean isEmpty() //判断此集合是否为空。
boolean remove(Object o) //在此集合中移除o对象。
int size() //返回此集合中的元素个数。
Object[] toArray() //将此集合转换成数组。
/*** Collection接口的使用(一)* 1.添加元素* 2.删除元素* 3.遍历元素* 4.判断*/
public class Demo1{pubic static void main(String[] args){//创建集合Collection collection = new ArrayList();
// * 1.添加元素Collection.add("苹果");Collection.add("西瓜");Collection.add("榴莲");System.out.println("元素个数:"+collection.size());System.out.println(collection);
// * 2.删除元素collection.remove("榴莲");System.out.println("删除之后:"+collection.size());
// * 3.遍历元素//3.1 使用增强for for(Object object : collection){System.out.println(object);}//3.2 使用迭代器(迭代器专门用来遍历集合的一种方式)//hasnext();判断是否有下一个元素//next();获取下一个元素//remove();删除当前元素Iterator iterator=collection.Itertor();while(iterator.hasnext()){String object=(String)iterator.next();System.out.println(s);//删除操作//collection.remove(s);引发错误:并发修改异常//iterator.remove();应使用迭代器的方法
// * 4.判断System.out.println(collection.contains("西瓜"));//trueSystem.out.println(collection.isEmpty());//false}}
}
Collection子接口
List集合
特点:有序、有下标、元素可以重复
/*** List子接口的使用(一)* 特点:1.有序有下标 2.可以重复* * 1.添加元素* 2.删除元素* 3.遍历元素* 4.判断* 5.获取位置*/
public class Demo3 {public static void main(String[] args) {List list=new ArrayList<>();//1.添加元素list.add("tang");list.add("he");list.add(0,"yu");//插入操作System.out.println("元素个数:"+list.size());System.out.println(list.toString());//2.删除元素list.remove(0);//list.remove("yu");结果同上System.out.println("删除之后:"+list.size());System.out.println(list.toString());//3.遍历元素//3.1 使用for遍历for(int i=0;i<list.size();++i) {System.out.println(list.get(i));}//3.2 使用增强forfor(Object object:list) {System.out.println(object);}//3.3 使用迭代器Iterator iterator=list.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}//3.4使用列表迭代器,listIterator可以双向遍历,添加、删除及修改元素。ListIterator listIterator=list.listIterator();//从前往后while (listIterator.hasNext()) {System.out.println(listIterator.next()); }//从后往前(此时“遍历指针”已经指向末尾)while(listIterator.hasPrevious()) {System.out.println(listIterator.previous());}//4.判断System.out.println(list.isEmpty());System.out.println(list.contains("tang"));//5.获取位置System.out.println(list.indexOf("tang"));}
}
/*** List子接口的使用(二)* 1.添加元素* 2.删除元素* 3.遍历元素* 4.判断* 5.获取位置*/
public class Demo4 {public static void main(String[] args) {List list=new ArrayList();//1.添加数字数据(自动装箱)list.add(20);list.add(30);list.add(40);list.add(50);System.out.println("元素个数:"+list.size());System.out.println(list.toString());//2.删除元素list.remove(0);//list.remove(20);很明显数组越界错误,改成如下//list.remove(Object(20));//list.remove(new Integer(20));System.out.println("元素个数:"+list.size());System.out.println(list.toString());//3-5不再演示,与之前类似//6.补充方法subList,返回子集合,含头不含尾List list2=list.subList(1, 3);//[1,3)System.out.println(list2.toString()); }
}
ArrayList【重点】
ArrayList是List接口的实现类,也是开发当中最常用的集合类
- 数组结构实现,查询快、增删慢;
- JDK1.2版本,运行效率快、线程不安全。
/*** ArrayList的使用* 存储结构:数组;* 特点:查找遍历速度快,增删慢。* 1.添加元素* 2.删除元素* 3.遍历元素* 4.判断* 5.查找*/
public class Demo5 {public static void main(String[] args) {ArrayList arrayList=new ArrayList<>();//1.添加元素Student s1=new Student("唐", 21);Student s2=new Student("何", 22);Student s3=new Student("余", 21);arrayList.add(s1);arrayList.add(s2);arrayList.add(s3);System.out.println("元素个数:"+arrayList.size());System.out.println(arrayList.toString());//2.删除元素arrayList.remove(s1);//arrayList.remove(new Student("唐", 21));//注:这样可以删除吗(不可以)?显然这是两个不同的对象。//假如两个对象属性相同便认为其是同一对象,那么如何修改代码?//romove的底层是调用对象的equals方法//所以想对象属性相同就修改,就必须修改对象的equals方法// 若一定要写包含任何类型的集合,可以这样写,Object就是包含任何类
// ArrayList<Object> list1=new ArrayList<>();// 1.获取某个索引位置处的元素值get(index)String e=list.get(4);System.out.println(e);// 2.获取集合的大小size()System.out.println(list.size());// 3.完成遍历for(int i = 0; i < list.size(); i++) {System.out.println(list.get(i));}// 4.删除某个索引位置的元素值,并返回被删除的元素值remove(index)System.out.println(list);String e2=list.remove(1);System.out.println("e2=" + e2);System.out.println(list);// 5.直接删除元素值,remove(value) 删除成功返回true,删除失败返回false,如果存在多个相同的元素,只会删除第一次出现的目标元素System.out.println(list.remove("hwq"));System.out.println(list);// 6.修改某个索引位置处的元素值,并会返回被修改的元素 set(index,element(要修改的值))String name=list.set(0,"牛");System.out.println(name);System.out.println(list);// 7.contains:查找元素是否存在System.out.println(list.contains("牛"));// 8.clear 清空集合list.clear();// 9.isEmpty 判断是否为空System.out.println(list.isEmpty());// 10.addAll:添加多个元素ArrayList list1 = new ArrayList();list1.add("红雷梦");list1.add("三国");list.addAll(list1);System.out.println(list);// 11.containsAll 查找多个元素System.out.println(list.containsAll(list1));// 12.removeAll:删除多个元素list.removeAll(list1);System.out.println(list);}
}
ArrayList源码分析【重点】
ArrayList
是Java集合框架中常用的动态数组实现,它提供了动态增长的能力,允许在数组尾部进行快速的随机访问。下面我们将对 ArrayList
的源码进行分析,深入了解其实现原理和关键方法。
1. 类声明和继承关系
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// ...
}
ArrayList
类继承自AbstractList
,实现了List
接口,同时还实现了RandomAccess
(随机访问标记接口)、Cloneable
(可克隆接口)、Serializable
(可序列化接口)。
2. 成员变量
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
elementData
:存储元素的数组。DEFAULTCAPACITY_EMPTY_ELEMENTDATA
:空数组,用于延迟数组的初始化。DEFAULT_CAPACITY
:默认初始容量。MAX_ARRAY_SIZE
:数组的最大容量。
3. 构造方法
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: " +initialCapacity);}
}public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {this.elementData = EMPTY_ELEMENTDATA;}
}
ArrayList
提供了多个构造方法,包括默认构造方法、指定初始容量的构造方法以及接收Collection
的构造方法。- 在默认构造方法中,使用了延迟加载的技术,初始时
elementData
为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
也就是空数组这点非常重要‼️,只有在第一次add添加元素时才会初始化为默认容量(10)的数组。
4. 核心方法
4.1 add
方法
public boolean add(E e) {ensureCapacityInternal(size + 1);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++;if (minCapacity - elementData.length > 0)grow(minCapacity);
}private void grow(int minCapacity) {// overflow-conscious codeint 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);
}
add
方法:添加元素,确保容量足够,如果需要,进行数组扩容。ensureCapacityInternal
方法:确保容量足够的内部方法,负责调用ensureExplicitCapacity
进行实际的扩容操作。calculateCapacity
方法:计算容量的方法,处理空数组的情况。grow
方法:数组扩容的核心方法,通过Arrays.copyOf
进行扩容,新容量为原容量的1.5倍。
理解过程
//这是一个空的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这段源码说明当你没有向集合中添加任何元素时,集合容量为0。那么默认的10个容量怎么来的呢?
这就得看看add方法的源码了:
public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;
}
假设你new了一个数组,当前容量为0,size当然也为0。这时调用add方法进入到ensureCapacityInternal(size + 1);
该方法源码如下:
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
该方法中的参数minCapacity传入的值为size+1也就是 1,接着我们再进入到calculateCapacity(elementData, minCapacity)
里面:
private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}
上文说过,elementData就是存放元素的数组,当前容量为0,if条件成立,返回默认容量DEFAULT_CAPACITY
也就是10。这个值作为参数又传入ensureExplicitCapacity()
方法中,进入该方法查看源码:
private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0) // 当需要的空间大于现在数组的容量就要考虑扩容grow(minCapacity); //数组扩容
}
我们先不要管modCount这个变量。
因为elementData数组长度为0,所以if条件成立,调用grow方法,重要的部分来了,我们再次进入到grow方法的源码中:
private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length; // 现在数组的容量0int newCapacity = oldCapacity + (oldCapacity >> 1); // 新数组的容量0if (newCapacity - minCapacity < 0) // 成立newCapacity = minCapacity;//newCapacity = 10if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);
}
这个方法先声明了一个oldCapacity变量将数组长度赋给它,其值为0;又声明了一个newCapacity变量其值为oldCapacity+一个增量,可以发现这个增量是和原数组长度有关的量,当然在这里也为0。第一个if条件满足,newCapacity的值为10(这就是默认的容量,不理解的话再看看前面)。第二个if条件不成立,也可以不用注意,因为MAX_ARRAY_SIZE的定义如下:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
这个值太大了以至于第二个if条件没有了解的必要。
最后一句话就是为elementData数组赋予了新的长度,Arrays.copyOf()方法返回的数组是新的数组对象,原数组对象不会改变,该拷贝不会影响原来的数组。copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。
这时候再回到add的方法中,接着就向下执行elementData[size++] = e;到这里为止关于ArrayList就讲解得差不多了,当数组长度为10的时候你们可以试着过一下源码,查一下每次的增量是多少(答案是每次扩容为原来的1.5倍)
总结
存储元素的结构是Object[]数字,具体的Object由new ArrayList();的T决定
没有给定初始容量,默认初始容量为10,当添加元素后才初始化为10
达到额定容量,每次扩容为原来的1.5倍,比如初始容量为10,当添加第11个元素的时候,发现容量不足了,就会调用grow方法进行扩容,扩容后容量为(10 + 10 / 2) = 15 ,如果初始容量为33,则扩容后的容量为(33 + 33 / 2) = 49,会舍弃小数部分,因为底层使用了位运算 >> 1
Vector
Vector在实际开发中使用较少,为了保证线程安全,很多方法使用了synchronized而牺牲了性能,大部分源码和ArrayList相同,这里就不深入介绍了
- 数组结构实现,查询快、增删慢;
- JDK1.0版本,运行效率慢、线程安全。
/*** Vector的演示使用* *1.添加数据*2.删除数据*3.遍历*4.判断*/
public class Demo1 {public static void main(String[] args) {Vector vector=new Vector<>();//1.添加数据vector.add("tang");vector.add("he");vector.add("yu");System.out.println("元素个数:"+vector.size());//2.删除数据/** vector.remove(0); vector.remove("tang");*///3.遍历//使用枚举器Enumeration enumeration=vector.elements();while (enumeration.hasMoreElements()) {String s = (String) enumeration.nextElement();System.out.println(s);}//4.判断System.out.println(vector.isEmpty());System.out.println(vector.contains("he"));//5. Vector其他方法//firstElement() lastElement() ElementAt();}
}
LinkedList
- 特点:链表结构实现,增删快,查询慢
/*** LinkedList的用法* 存储结构:双向链表* 1.添加元素* 2.删除元素* 3.遍历* 4.判断*/
public class Demo2 {public static void main(String[] args) {LinkedList linkedList=new LinkedList<>();Student s1=new Student("唐", 21);Student s2=new Student("何", 22);Student s3=new Student("余", 21);//1.添加元素linkedList.add(s1);linkedList.add(s2);linkedList.add(s3);linkedList.add(s3);System.out.println("元素个数:"+linkedList.size());System.out.println(linkedList.toString());//2.删除元素/** linkedList.remove(new Student("唐", 21));* System.out.println(linkedList.toString());*///3.遍历//3.1 使用forfor(int i=0;i<linkedList.size();++i) {System.out.println(linkedList.get(i));}//3.2 使用增强forfor(Object object:linkedList) {Student student=(Student) object;System.out.println(student.toString());}//3.3 使用迭代器Iterator iterator =linkedList.iterator();while (iterator.hasNext()) {Student student = (Student) iterator.next();System.out.println(student.toString());}//3.4 使用列表迭代器(略)//4. 判断System.out.println(linkedList.contains(s1));System.out.println(linkedList.isEmpty());System.out.println(linkedList.indexOf(s3));}
}
LinkedList源码分析
LinkedList首先有三个属性:
- 链表大小:
transient int size = 0;
- (指向)第一个结点/头结点:
transient Node<E> first;
- (指向)最后一个结点/尾结点:
transient Node<E> last;
关于Node类型我们再进入到类里看看:
private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}
首先item存放的是实际数据;next指向下一个结点而prev指向上一个结点。
Node带参构造方法的三个参数分别是前一个结点、存储的数据、后一个结点,调用这个构造方法时将它们赋值给当前对象。
LinkedList是如何添加元素的呢?先看看add方法:
public boolean add(E e) {linkLast(e);return true;
}
进入到linkLast方法:
void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;
}
假设刚开始new了一个LinkedList对象,first和last属性都为空,调用add进入到linkLast方法。
首先创建一个Node变量 l 将last(此时为空)赋给它,然后new一个newNode变量存储数据,并且它的前驱指向l,后继指向null;再把last指向newNode。如下图所示:
如果满足if条件,说明这是添加的第一个结点,将first指向newNode:
至此,LinkedList对象的第一个数据添加完毕。假设需要再添加一个数据,我们可以再来走一遍,过程同上不再赘述,图示如下:
ArrayList和LinkedList区别
- ArrayList:必须开辟连续空间,查询快,增删慢。
- LinkedList:无需开辟连续空间,查询慢,增删快。
总结
在不能提前确定容器大小的情况下可以使用List接口实现的集合类,ArrayList适合频繁查询、修改的场景,不适合频繁增删的场景;而LinkedList则相反
如果觉得本篇文章对你有帮助,可否点个小赞😺;篇幅较长建议收藏🫠;关注一手等待后续更新更多干货🚀