文章目录
集合就是一个容器
类似于数组,用来存储一组数据,但是数组一旦定义,长度将不能再变化。
然而在我们的实际开发中,经常需要保存一些变长的数据集合,于是,我们需 要一些能够动态增长长度的容器来保存我们的数据。
也有我们对数据的存储逻辑可能是各种各样的,于是就需要各种各样的数据结构。
Java当中对于各种数据结构的具体实现,就利用的是集合。
Java集合类是被定义在Java.util包中,它主要包含了4种集合,分别为List、Queue、Set和Map,每种集合的具体分类下图:
看一下机构体系图:
Collection接口是单例的。Map接口是双列的。List接口是数据可重复的。Set接口是数据不可重复的。
Collection接口
在Collection接口当中,是定义了存取的一组对象的方法,其子接口Set和List分别定义了存储方式。
Set中的数据对象是没有顺序且不可以重复的。
List中的数据对象是有顺序的且可以重复的。
Collection当中的常用方法:
- add()向集合中添加元素
- isEmpty()查看集合当中是否有值(有-false、无-true)
- remove()删除集合当中指定的元素,若有多的,就删除第一个存在的(删除了-true、无的话-false)
- size()返回集合当前总共有多少个元素
- clear()删除集合当中所有的元素
- contains()查看集合当中是否包含这个值,返回(true/false)
import java.util.ArrayList;
import java.util.Collection;
public class Demo01 {
public static void main(String[] args) {
//因为接口不能创建对象,所以在这使用的是它子类List接口的子类ArrayList实现类来创建的
//在创建的时候,虽然可以不用指定泛型存储,也能存储进去,但是取的时候及不方便,所以要明确
Collection<Integer> collection = new ArrayList();
collection.add(4);//add方法先集合当中添加元素
collection.add(5);
collection.add(6);
collection.add(4);
System.out.println(collection);
System.out.println("size()方法得到集合当前元素的数量:" + collection.size());
System.out.println("isEmpty()方法得到当前集合是否含有元素(有-false、无-true)" + collection.isEmpty());
System.out.println("执行remove()语句,若成功删除返回true,否则就false,删除4结果为:" + collection.remove(4));
System.out.println("执行remove(4)语句后集合结果为:" + collection);
System.out.println("执行contains()方法,查看5这个元素知否包含在集合当中:" + collection.contains(5));
collection.clear();
System.out.println("执行clear()删除所有元素方法后,集合的元素:" + collection);
}
}
两个集合之间的方法:
- addAll(Collection<? extends E> c) 将两个集合合起来
- containsAll(Collection<?> c) 调用的集合包含指定集合的所有元素,就返回true。
- removeAll(Collection<?> c) 删除指定集合中包含的所调用此方法集合的元素。(调用此方法的集合若有改变就返回true,否则就是false)
- retainAll(Collection<?> c) 只保留两个集合当中公共的元素(若调用方法的集合有改变,就返回true,否则就是false)
import java.util.ArrayList;
import java.util.Collection;
public class Demo02 {
public static void main(String[] args) {
Collection<Integer> collection1 = new ArrayList();
collection1.add(1);
collection1.add(2);
collection1.add(3);
Collection<Integer> collection2 = new ArrayList();
collection2.add(2);
collection2.add(3);
collection2.add(4);
System.out.println("初始collection1元素为:" + collection1);
System.out.println("初始collection1元素为:" + collection2);
System.out.println("addAll()方法,将collection2中的元素添加到collection1当中," +
"若collection1发生了改变就返回true:" + collection1.addAll(collection2));
System.out.println("addAll()方法后collection1集合的元素为:" + collection1);
System.out.println("使用continueAll()方法判断collection2中的元素是否包含在collection1中,是-true:"
+ collection1.containsAll(collection2));
System.out.println("使用continueAll()方法判断collection1中的元素是否包含在collection2中,否-false:"
+ collection2.containsAll(collection1));
System.out.println("removeAll()方法就是删除collection1当中所有collection2中的元素," +
"若collection1中元素右变换,就返回true,否则就是false:" + collection1.removeAll(collection2));
System.out.println("removeAll()方法处理后的collection1集合的内容:" + collection1);
//在这给collection当中重新添加一下元素
collection1.add(2);
collection1.add(3);
System.out.println("现在重新添加后collection1集合的内容:" + collection1);
System.out.println("retainAll()方法就是提取出在collection1当中公共的collection2的元素," +
"若collection1发生了改变就返回true,否则就是false。现在是:" + collection1.retainAll(collection2));
System.out.println("retainAll()方法后collection1的元素都有:" + collection1);
}
}
集合转为数组的方法:
toArray()
返回一个包含此集合中所有元素的数组。
toArray(T[] a)
返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class Demo03 {
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList();
collection.add(1);
collection.add(2);
collection.add(3);
System.out.println(collection);
Object[] objects = collection.toArray();
//转换为Integer型的数组,转换为数组就可以使用数组里面的方法了
System.out.println(Arrays.toString(objects));
//转换为Integer型的数组,在后面调用toArray方法的时候传入参数
Integer[] integers = collection.toArray(new Integer[collection.size()]);
System.out.println(Arrays.toString(integers));
}
}
List接口
List接口是非常常用的一个数据类型,它是有序的Collection。 按照的插入的顺序来讲是有序的。
一共有三个实现类。分别就是ArrayList、Vector和LinkedList。
ArrayList集合
ArrayList是基于数组来实现的,它增删慢,查询快。但线程不安全的。
ArrayList是使用比较广泛的一个List实现类,其内部的数据结构时基于数组来实现的,提供了对List的增加(add)、删除(remove)和访问(get)功能。
ArrayList的缺点是元素必须连续存储的,当需要ArrayList的中间位置插入或者删除元素的时候,需要将待插入或删除的节点的所有元素进行移动,其修改代价较高,因此ArrayList是不适合随机插入和删除的操作,它更适合随机查找和遍历的操作。
ArrayList是不需要再定义式指定数组的长度,在数组长度不能满足存储需求的时候,ArrayList会创建一个新的更大的数组并将数组中已有的数据复制到新的数组当中。
ArrayList集合的常用方法和底层源码
ArrayList集的创建
方式一:
ArrayList arrayList = new ArrayList();
在创建ArrayList集合它的底层代码是:我们就可以知道它初始创建的底层数组长度是10。
方式二:
ArrayList arrayList = new ArrayList(int initialCapacity);
在初始创建的时候,就给定集合底层数组的长度。
方式三:
ArrayList arrayList = new ArrayList(Collection<? extends E> c);
传进的是一个集合。
在创建的时候,就也要考虑创建的时候它的底层数组的长度是给多少的。
数组的添加add();
是由两种添加方式,默认的是在集合的末尾进行添加。
也是可以指定位置进行添加的。
import java.util.ArrayList;
public class Demo04 {
public static void main(String[] args) {
/*
默认无参的构造方法。默认不创建底层的数组,当添加第一个元素时,创建一个长度为10的数组
*/
ArrayList<String> arrayList = new ArrayList();
arrayList.add("Xin_");
arrayList.add("Chen_");
arrayList.add(0,"Yu_");
arrayList.add("Chen_");
arrayList.add("Xi");
System.out.println(arrayList);
}
}
在add()添加的时候,因为底层是数组,所以在这就要注意扩容的机制。
具体如何扩容,这个就直接看源代码:(层层调用)
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 void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
所以我们直接看ensureExplicitCapacity()方法里面。当minCapacity长度要大于当前集合的长度的时候,就执行grow()方法进行具体的扩容。
int newCapacity = oldCapacity + (oldCapacity >> 1);
就是通过这个代码,可以明确的看出右移一位就是减少一半,将它的值和原来的长度合在一起,就得到了新的长度,新长度为原来的1.5倍。
集合的获取get()
get()方法,获得指定索引位置的元素,因为底层是数组,所以就直接可以获得对应的值。
remove()方法
remove(int index)删除对应位置的元素,删除后,就后面的元素一点一点往前移。
remove(object o)从列表当中删除指定元素的第一个出现的。
removeAll(Collection<?> c)从当前列表当中删除指定集合当中的所有元素。
removeIf(Predicate<? super E> filter) 根据指定元素删除指定的所有元素。
import java.util.ArrayList;
import java.util.function.Predicate;
public class Demo05 {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Xin_");
arrayList.add("Xin_");
arrayList.add("Chen_");
arrayList.add("Yu_");
arrayList.add("Chen_");
arrayList.add("Yu_");
System.out.println(arrayList);
//删除指定元素的,但只能删除第一个出现的。
System.out.println("删除指定元素,多个的话,只删除第一个,有删除返回true。现在:"
+ arrayList.remove("Xin_"));
System.out.println("删除后的集合为:" + arrayList);
//根据条件删除指定的元素
//这里是一个匿名的内部类
arrayList.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.equals("Chen_");
}
});
System.out.println("根据条件(\"Chen_\")删除后的集合为:" + arrayList);
}
}
removeRange(int fromIndex, int toIndex)方法
这个也是删除的操作,它删除的是一个集合当中一个区间的元素。
removeRange()这个方法是只能在ArranyList的子类当中才能使用。
set(int index, E element)
将集合当中指定位置的元素进行替换
sort(Comparator<? super E> c)
将集合当中元素进行一个排序。
在这里也是使用了一个匿名的内部类的写法
subList(int fromIndex, int toIndex)
返回列表当中指定的 fromIndex (包括)和 toIndex之间的数据元素。返回值为List型。
import java.util.ArrayList;
import java.util.Comparator;
public class Demo07 {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("d");
arrayList.add("c");
arrayList.add("b");
arrayList.add("e");
arrayList.add("a");
System.out.println("创建的原集合为:" + arrayList);
//替换原集合当中第一个索引位置的元素
arrayList.set(0, "e");
System.out.println("使用set()方法替换集合当中第一个索引位置的元素后的集合为:"
+ arrayList);
//使用sort()方法对集合进行排序
arrayList.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println("使用sort()方法对集合进行一个排序,使用内部类的形式进行排序:"
+ arrayList);
//使用subList对集合进行截取,返回一个List的返回值
System.out.println("使用subList()方法对集合进行截屏结果为List:" + arrayList.subList(0, 3));
}
}
Vector集合
基于数组实现,增删慢,查询快,线程安全的
Vector的数据结构时和ArrayList一样的,都是基于数组来实现的,不同的是Vector是支持线程同步的,即就是同一时刻只允许一个线程对Vector进行写操作(新增、删除、修改),以保证多线程的环境下数据的一致性。但需要频繁的对Vector实例进行加锁和释放锁的操作。因此Vector整体上的读写效率是比ArrayList慢一点的。
Vector的底层是和ArrayList集合的底层方法实现都是一样的,只不过它的所有方法是被synchronized同步锁所修饰着,所以它也是个线程安全的。
LinkedList集合
LinkedList是采用双向链表结构存储元素的,在堆LinkedList进行插入和删除操作时,只需要在对应的节点上插入或删除元素,并将下一个节点元素的下一个节点的指针指向该节点即可,数据改动较少,因此随机插入和删除效率很高。但在对LinkedList进行随机访问时,需要从链表头部一直变量到该节点为止,所以随机访问速度较慢,除此之外,LinkedList还提供了在List接口中未定义的方法,用于操作链表头部和尾部的元素。因此有时是可以被当做堆栈和队列使用的。
LinkedList集合的底层源码
LinkedList的常用方法和ArrayList的常用方法使用是几乎一样的,所以在这我们只对它的底层源码的实现来看看就可以了。
因为LinkedList的底层是由双向链表来实现的,所以它就有了前后指针,前指针指向当前元素前面的数据位置,后指针指向后面得一个元素的数据位置。
所以在增加、删除的时候,只要调整指针的指向就可以了。
增加:
删除 / 查找:
Queue接口
Queue是队列结构,在Java中常用的队列如下:
● ArrayBlockingQueue:基于数组的数据结构实现的有界阻塞队列。
● LinkedBlockingQueue:基于链表的数据结构实现的有界阻塞队列。
● PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
● DelayQueue:支持延迟操作的无界阻塞队列。
● SynchronousQueue:用于线程同步的阻塞队列。
● LinkedTransferQueue:基于链表数据结构实现的无界阻塞队列。
● LinkedBlockingDeque:基于链表数据结构实现的双向阻塞队列。
Set接口
Set的核心特征是独一无二,适用于存储无序且值不相等的元素。对象的相等性在本质上是对象的HashCode值相同,Java依据对象的内存地址计算出对象的HashCode值。
如果想要比较两个对象是否相等,则必须同时覆盖对象的hashCode方法和equals方法,并且hashCode方法和equals方法的返回值必须相同。
Set接口中的类也都是直接调取的是Map中的方法:
所以在Set这的底层代码我在这就不再做详细的解说了,到Map哪做详细的解说。
HashSet
由HashMap实现,是无序的
HashSet存放的是散列值,它是按照元素的散列值来存取元素的。
元素的散列值是通过元素hashCode方法计算得到的,HashSet首先要判断两个元素的散列值是否相等,如果散列值相等,则接着通过equals方法进行比较。如果equals方法的返回值也为true,则HashSet就将其视为同一个元素;如果equals方法返回的结果为false,HashSet将其就视为不同的元素。
底层是使用哈希表+链表+红黑树
import java.util.HashSet;
public class HashSetDemo01 {
public static void main(String[] args) {
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
hashSet.add(5);
hashSet.add(10);
hashSet.add(22);
hashSet.add(1);
System.out.println(hashSet);
}
}
因为在HashSet当中都使用的是HashMap当中的键,因为键是不能重复的,所以在Set当中就是独一无二,不能重复的。
上面为什么首先是通过hashCode进行判断,再通过equals进行判断,两个返回都为true的时候,才认为是相同的元素。
首先只用equals()方法进行判读,因为equals()方法是将里面的都一个一个拿出来相互进行比较,就拿字符串来说,当一个字符串特别长的时候,只拿equals进行判断,显然是特别浪费时间的。所以先来hashCode进行判断。但是hashCode进行判断的时候,会出现一个问题,那即是hashCode相同的两个元素并不一定就是相同的元素。
在这里“通话”和“重地”这两个hashCode的值就是一样的,当然这样的情况是特别少的,所以在进行判断两个值是否一样的时候,就将这两个判断方法向结合,当两个返回的结果都为true的时候,就可以断定这两个元素的值是相等的。
当然在我们平常使用的时候,集合当中是封装的是一个对象,默认的hashCode和equals方法比较的是对象的地址,所以这样比较是肯定不行的。在这时,我们就是重写这两个方法。
比如我们现在就给集合当中添加由我们自己创建的StudentDemo类创建的对象时候。
import java.util.HashSet;
public class HashSetDemo02 {
public static void main(String[] args) {
HashSet<StudentDemo> hashSet = new HashSet<>();
StudentDemo s1 = new StudentDemo("张三",18);
StudentDemo s2 = new StudentDemo("李四",19);
StudentDemo s3 = new StudentDemo("王五",20);
StudentDemo s4 = new StudentDemo("赵六",21);
StudentDemo s5 = new StudentDemo("王五",20);
hashSet.add(s1);
hashSet.add(s2);
hashSet.add(s3);
hashSet.add(s4);
hashSet.add(s5);
//size()方法获得当前集合的长度
System.out.println(hashSet.size());
}
}
接下来看我们自己创建的StudentDemo类
public class StudentDemo {
private String name;
private Integer age;
@Override
public int hashCode() {
System.out.println("调用了hashCode方法");
int res = name.hashCode() + age.hashCode();
return res;
}
@Override
public boolean equals(Object obj) {
System.out.println("调用了equals方法");
StudentDemo student = (StudentDemo) obj;
if (!age.equals(student.age) && !name.equals(student.name)) {
return false;
}
return true;
}
public StudentDemo(String name, Integer age) {
this.name = name;
this.age = age;
}
}
在这个类当中我们重写了hashcode和equals方法,并且由我们自己书写了判断的功能。
然后执行我们的主代码。一共添加了5条数据,但是其中有两个对象里面的内容是一样的,如果正常执行,它会调用Object类当中的hashCode和equals方法,判断的是对象的地址,那么必然不同,就会总共添加5条数据,现在我们自己来重写了hashCode和equals方法。就会根据我们所写的来进行判断。
这里根据我们自己写的,就只会添加四条数据。对对象中内容重复的做出来判断。
HashSet的无序性在下一篇说Map的时候细说。
TreeSet
是由二叉树来实现的。
TreaaSet是基于二叉树的原理对新添加的对象按照指定的顺序排列(升序、降序),每添加一个对象都会进行排序,并将对象插入二叉树的指定位置。
Integer和String等基本数据类型可以直接根据TreeSet的默认排序就那些存储,而自定义的数据类型必须实现Comparable接口,并且覆写其中的compareTo函数才可以按照预定的顺序进行存储。
若覆写compare函数,则在升序时在this.对象小于指定对象的条件下返回-1,在降序时在this. 对象大于指定对象的条件下返回1,等于就返回0。
底层是使用红黑树(红黑数是一种自平衡的二叉树)
它的方法也都是直接调用了TreeMap来实现的。
import java.util.TreeSet;
public class TreeSetDemo01 {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>();
set.add(3);
set.add(2);
set.add(5);
set.add(1);
set.add(4);
for (Integer res : set) {
System.out.print(res + "\t");
}
}
}
它的底层实现就是一颗树的样子:取的时候就是按照前序遍历来取的。
也正是存取的时候,是要按照一定的顺序来进行,像基本的数据类型都本身是已继承了comparable接口实现的。所以在对我们自己写的封装类进行TreeSet存储的时候,就要对自己写的封装类进行comparable接口的继承,并且自己写入判断的方法。
在这里我以age进行大小的比较排序
当然TreeSet中也是不会出现重复的元素,在TreeSet当中,判断重复的对象依据是进行比较排序的元素。
LinkedHashSet
继承HashSet、HashMap实现数据存储,双向链表记录顺序
LinkedHashSet在底层使用LinkedHashMap进行元素的存储,它继承了HashSet所有的方法和操作,因此LinkedHashSet的实现比较简单。只提供了4个方法,并通过传递一个标识参数来调用父类的构造器,在底层构造一个LinkedHashMap来记录数据访问,其他相关操作与父类HashSet相同,直接调用父类HashSet的方法即可。
循环遍历集合和迭代器
List接口集合的普通迭代
在这我就是要ArrayList来做为演示(其他都一样)
for循环只能对list集合使用,而set因为set因为数的结构,也就没有像list里面通过索引来找到某个元素。
for循环、增强for循环
import java.util.ArrayList;
public class Demo09 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
System.out.println("通过for循环进行遍历");
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + "\t");
}
System.out.println();
System.out.println("通过增强for循环进行遍历");
for (Integer item : list) {
System.out.print(item + "\t");
}
System.out.println();
}
}
iterator()方法,在list和set接口中都可以使用
创建一个集合器:
集合调用Iterator()方法,返回值就是迭代器属性。
迭代器遍历(Iterator):
这个迭代方式也是专门为集合所创建使用的。
在代码演示前,先认识一个方法:iterator()方法,返回的是一个Iterator型的变量。
就是以正确的顺序返回改列表当中的元素迭代器。
迭代器就是为了实现上面两个循环中对数据的处理的会出现的问题。(比如在循环当中就无法对数据进行删除,会报一个异常)
所以就要使用迭代器来具体实现在循环当中对数据的一些操作。
当然在迭代器内也有它对应的方法
方法 | 意义 |
---|---|
hasNext() | 判断下一个是否有值返回true / false |
next() | 返回集合中的下一个元素 |
remove() | 删除集合当中的当前元素 |
forEachRemaining(Consumer<? super E> action) | 给定指定的操作 |
import java.util.ArrayList;
import java.util.Iterator;
public class Demo10 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
//定义一个迭代器,将集合传进去
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) { //hasNext()方法判断下一元素是否有元素
if (iterator.next().equals(1)) {//next()得到当前的元素
iterator.remove();//remove()删除当前元素
}
}
System.out.println(list);
}
}
import java.util.ArrayList;
import java.util.ListIterator;
public class Demo10 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
//定义一个迭代器,将集合传进去
//在这进行删除操作
ListIterator<Integer> iterator = list.listIterator();
while (iterator.hasNext()) { //hasNext()方法判断下一元素是否有元素
if (iterator.next().equals(1)) {//next()得到当前的元素
iterator.remove();//remove()删除当前元素
}
}
System.out.println("删除集合当中等于1的元素:" + list);
//在这进行添加操作
ListIterator<Integer> iterator1 = list.listIterator();
while (iterator1.hasNext()) {
iterator1.next();
iterator1.add(10);
}
System.out.println("给集合当中每一个元素的后面加上10:" + list);
//在这进行逆序查询操作。
//在创建的时候传入一个索引的位置。
ListIterator<Integer> iterator2 = list.listIterator(list.size());
System.out.print("对集合当中的元素进行昵称排序:");
while (iterator2.hasPrevious()) {
System.out.print(iterator2.previous() + "\t");
}
}
}
listIterator()方法,在list接口中使用
创建一个集合器:
集合调用listIterator()方法,返回值就是迭代器属性。
因为是list接口专有的,所以这个方法就是listIterator()方法。返回的是一个ListIterator型的变量。
生成listIterator()方法是有两种的,一种是控制,一种是传入一个索引位置。因为爱listIterator是可以逆序查询的。
listIterator()
listIterator(int index)
当然在这个类当中,也是有自己专门的定义的方法对数据的操作。
方法 | 意义 |
---|---|
add(E e) | 添加 |
hasNext() | 从前往后遍历时判断判断集合下一个是有值返回:boolean |
hasPrevious() | 从后往前遍历的时候,判断上一个是否有值:Boolean |
next() | 返回列表下一个元素值,返回值E |
nextIndex() | 返回next()调用的元素的索引 |
previous() | 返回列表上一元素值,返回值E |
previousIndex | 返回previous()调用的元素的索引 |
remove() | 在列表当中删除由nect()和previous()方法调出的值 |
set(E e) | 用指定的元素(e)替换next()和previous()调出的元素 |
使用流进行遍历
import java.util.ArrayList;
import java.util.function.Consumer;
import java.util.stream.Stream;
public class Demo11 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
//使用流就行遍历
Stream<Integer> stream = list.stream();
stream.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.print(integer + "\t");
}
});
}
}
当然这的流也可以写成函数的形式
stream.forEach(t -> System.out.println(t));