到今天为止已经学完了所有的集合了,也是时候总结一下了,这段时间赶课有点紧,也没来得及更新博客,今天就总结一下java中常用的集合。
一. 集合的引入
在学习集合之前,一直在使用的“容器”是数组,数组是用于存储一组相同的数据类型的容器。
好处: 可以将大量的变量存储在容器中,不需要单独的命名,用一个整体的数组名字+元素在容器中的相对位置来对容器中的所有的元素进行操作。如 int b = a[1], double d = d[0]……
但是当我们需要存储一个对象中的数据,例如 小明:男,23岁等内容,普通的数组已经无法满足这个需求,因为数组适用于存储一组相同的数据类型的容器。
此时,我们就用到了对象类型的数组
顾名思义,在对象类型的数组中,存储的都是应用数据类型,数组中的每个元素都是某个对象的地址。
但是数组也有自己的弊端:
- 无法继续添加新的元素,容量已经确定。
- 无法删除容器中的元素,只能修改已有的元素值(指的是没有删除方法,只能通过覆盖删除)。
- 功能太少
解决方法:
- 定义某个引用类型,包含一个数组的成员变量,成员变量私有,不让外界直接访问。
- 提供公共的访问方式,定义操作数组的各种方法。
- 添加功能:若数组已经满了,就申请新的数组,将原数组拷贝到新的数组中。
- 删除功能:将删除的位置,使用后面的元素覆盖,并且元素个数减一,计数器减一。
- 是否包含功能:在方法中对数组进行遍历,比较判断是否包含。
根据上述的需求,我们来实现一下这个类。
public class MyCollection {
private Object[] o = new Object[10];// 定义一个Object类型的数组,因为Object是所有类的父类,可以存储任意类型的数据。
private int count = 0;// 元素计数器。
// 添加元素的方法
public void add(Object obj) {
if (o.length - 1 == count) {//当数组的长度-1和数组中元素个数相同时数组扩容。
Object[] newo = new Object[count * 2];//创建一个新的数组,数组长度是现有元素个数的两倍。
for (int i = 0; i < o.length; i++) {
newo[i] = o[i];//循环将老的数组中的内容复制给新的数组。
}
o = newo;//将老的数组的引用指向新的数组(也就是赋值)。
}
o[count] = obj;
count++;
}
// 删除元素的方法
public void remove(int index) {
for (int i = index; i < count; i++) {
o[i] = o[i + 1];// 将后面的元素往前面挪,覆盖要删除的元素。
}
count--;// 删除元素后,元素总个数减一。
}
// 获取元素的方法
public Object get(int index) {
return o[index];// 获取数组元素的方法
}
//判断是否存在某个元素的方法
public boolean exist(Object obj) {
for(int i = 0; i < count; i++) {
if(o[i].equals(obj)) {
return true;
}
}
return false;
}
// 返回这个容器元素占用的长度。
public int length() {
return count;
}
//返回这个容器的总长度
public int size() {
return o.length;
}
}
在将来使用中,使用者可以直接调用这些方法,就不需要知道方法的实现方式,操作起来非常方便,这就是集合。
二.集合与数组的比较
相同点
- 都是容器
- 都是为了方便操作大量的数据,为了避免给大量变量起名字,都可以通过操作容器的有规律的简单的数据(索引),来操作没规律的复杂的数据(元素)。
不同点
- 存储的内容不同
数组:即可以存储基本的类型,也可以存储引用类型的。
集合:仅可以存储引用类型,(存储进去的基本数据类型都变成了包装类来存储)。 - 存储的数量不同
数组:固定大小,无法扩容。
集合:可以根据数据的多少扩容。 - 支持的功能不同
数据:只支持修改。
集合:支持增删改查,各种判断。
三.集合的体系结构
- 集合由于存储了大量的元素,根据元素和元素的关系,有不同的存储逻辑,有了不哦他能过的集合接口。
- 同一种逻辑关系,在内存中有不同的实现方式,又有不同的实现类,形成了一个集合的实现类。
- 分类:
单列集合:每个元素都是单独的个体。
双列集合:成对出现的集合元素。
四.Collection
Collection是单列集合的顶层接口,定义了单列集合中应该拥有的方法,但不能创建对象,需要实现类来测试Collection中的方法
常用的方法:
1. add(Object obj) //添加
2. remove(Object obj) //删除
3.clear() //清空集合
4.size() //元素个数
5.isEmpty() //判空
6.contain() //判断是否存在该对象
Collection中带all的方法
-
说明,都是用于和另外一个集合做运算的。
-
罗列:
addAll(Collection c) 将参数集合中的所有元素都添加到调用者的集合中。 removeAll(Collection c) 将参数集合中的所有元素在调用者集合中删除。 containsAll(Collection c) 判断参数集合中的所有元素是否都存在与调用者集合中。 retainAll(Collection c) 将参数集合中的所有元素,都从调用者集合中删除,调用者集合中,只保留也存在于参数集合中的那些元素
五.集合的遍历
第一种遍历方式:转为Object [] 数组
方法:Object[] toArray();
通过从集合转为数组,通过遍历数组间接的遍历集合。
第二种方法: 迭代器
迭代器:在java中,在集合中用于一个一个遍历元素的对象,用于帮助我们遍历当前集合对象的。
迭代器的获取:
Collection接口中,定义了一个获取迭代器对象的方法
Iterator iterator();
迭代器的使用:
- Iterator接口,用于定义迭代器对象应该具有什么功能。
- iterator方法返回的不是接口,而是Iterator接口的实现类对象。
- 接口中有3个方法:
hasNext()
判断是否有下一个元素
next()
获取下一个元素
remove()
删除迭代器当前正在迭代的元素
代码示例:
public class Test3 {
public static void main(String[] args) {
Collection c = new ArrayList(); //接口不能创建对象,要使用实现类。
c.add("abc");
c.add("xyz");
c.add(123);
//获取迭代器对象:
Iterator it = c.iterator();
//使用迭代器中的方法进行遍历集合
while(it.hasNext()) {
System.out.println(it.next());
}
}
/*
* 打印结果:
* abc
* xyz
* 123
*/
}
迭代器的注意事项:
- 虽然多次调用的都是next方法,但是每次获取的内容不相同的(因为迭代器在调用next方法时,会移动迭代器的位置,到下一个元素)。
- 两个方法的比较: 1、hashNext不会移动迭代器 2、next会将迭代器移动到下一个元素的位置。
- 一定不要只判断一次hasNext,调用多次next方法。
代码示例:
public class Test3 {
//多次调用next方法:
public static void main(String[] args) {
Collection c = new ArrayList();
c.add(new Person(23, "zhangsan"));
c.add(new Person(24, "lisi"));
c.add(new Person(25, "wangwu"));
Iterator it = c.iterator();
while (it.hasNext()) {
it.next();
System.out.println(((Person)it.next()).getName() + " " + ((Person)it.next()).getAge());
}
}
/*
* 打印结果:lisi 25
*/
}
五.List接口
概述:
是Collection集合中的有序的子接口,用于定义那些具有顺序,元素具有位置的集合应该拥有的功能
特点:
- 有索引(给每个元素有了一个位置的标记)。
- 有序(有先后顺序)。
- 可以重复
特有方法:
1、add(int index, Object obj)
在指定索引上,添加元素
2、remove(int index)
删除指定索引上的元素
3、set(int index, Object obj)
将索引为index的位置,修改为obj值
4、get(int index)
获取索引为index的值
List的遍历:
List和Collection不同,List中的每个元素都具有自己的位置,因此可以通过索引来遍历List集合。
- 1.索引遍历
public static void printList(List list) {
for (int i = 0; i < list.size(); i++) {
Object obj = list.get(i);
System.out.println(obj);
}
}
- 2.迭代器遍历:
List也是Collection的一个子接口,自然也可以使用迭代器遍历
public class Test3 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
Iterator it = list.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
注意事项:
在使用迭代器遍历集合的同时,使用了集合对象对集合元素进行增删,会导致并发修改异常。
public class Test3 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("x");
Iterator it = list.iterator();
while (it.hasNext()) {
Object obj = it.next();
if (obj.equals("x")) {
list.add("java");
}
}
System.out.println(list);
}
}
Concurrent Modification Exception
并发 修改 异常
解决方法:(只能针对List解决)
1、集合遍历,集合增删:只能针对List解决。
集合遍历:List的特有遍历方式。
集合增删:add、remove
2、迭代器遍历,迭代器增删:只能针对List解决。
迭代器遍历:获取迭代器,使用迭代器的hashNext和next方法遍历即可。
迭代器删除:迭代器中有一个remove,可以删除当前迭代的元素。
迭代器增加:迭代器中没有增加的方法,只能获取一个特殊的迭代器。
- 3.列表迭代器
1.ListIterator:List接口特有的迭代器所实现的接口。
2.列表迭代器的获取:
在List集合中,有一个特有方法,listIterator()
3.使用:
add(Object obj)
在当迭代的位置,添加元素
hasPrevious()
判断是否有上一个元素
previous()
获取上一个元素
set(Object obj)
将当前迭代的位置,替换为obj元素
代码示例:
public class Test3 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("x");
ListIterator it = list.listIterator();//ListIterator
System.out.println(it.next());
System.out.println(it.previous());//读取上一个
System.out.println(list);
it.set("it.set");//特有的set方法
System.out.println(list);
}
}
六.List的实现类
1.List是一个接口,在接口中定义了有序集合的应该具有的功能
2.如果要实现这个接口,有不同的方式(定义一个表格、通过物理位置、通过记录下一个元素),根据不同的实现方式,就有不同的实现类
3.实现类:
Vector 已经过时的类,jdk1.0,线程安全,速度太慢,顺序存储,数组实现
Arraylist 线程不安全,顺序存储,数组实现,查询修改快,增删慢
LinkedList 线程不安全,链式存储,节点实现,查询修改慢,增删快
Vector已经过时就不在这里说了,直接从ArrayList开始。
ArrayList:
1、使用最频繁的一个单列集合
2、线程不安全,顺序存储,数组实现,查询修改快,增删慢
3、没有特有的方法,使用List接口中的方法
4、顺序存储、数组实现的特点。
ArrayList的特点:
- 数组都是连续存储的,都挨在一起。
- 数组中每个元素分配的空间大小都是相同的。
- 可以计算出要访问的索引的内存地址。
- 可以通过一次寻址直接找到要访问的元素。
- 无论数组有多大,无论要访问的索引有多大,访问的代价都是相同的,与访问的集合的规模无关。
- 综上,查询的速度非常快。
代码示例:
import java.util.ArrayList;
public class Demo09_ArrayList {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
System.out.println(list.get(1));
list.add(0, "x");
System.out.println(list);
}
}
ArrayList的缺点:
- 所有元素都挨在一起,所以无法直接删除元素。
- 添加元素时,只能把后面的元素都向后移动一个位置,给新的元素腾出位置才能添加元素。
- 由于集合中可能存储很多的元素,添加或删除一个元素的代价就需要移动很多很多的元素。
- 代价和集合的规模有关, 集合的规模越大,代价就越大,增删的速度就越慢。
LinkedList
线程不安全,链式存储,节点实现,查询修改慢,增删快
LinkedList的特点:
使用节点实现,每个元素都是一个节点,节点中分为 两个数据域,分别存储前后节点的地址,和数据域,用来存储数据
LinkedList的优势:
- 增删元素时,只需要修改前后两个节点的记录地址的部分,其余的节点都不需要动。
- 和集合的规模无关。
- 综上,增删速度非常快
代码示例:
import java.util.LinkedList;
public class Demo10_LinkedList {
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.add("zhangsan");
list.add("lisi");
list.add("wangwu");
list.add("zhaoliu");
list.add("zhouqi");
list.add("guoba");
list.add("hejiu");
list.add("chishi");
System.out.println(list.get(5));
list.add(0, "xiaoer");
System.out.println(list);
}
}
LinkedList的缺点:
- 查找指定元素时需要根据前一个节点来获取后一个节点的地址,根据这个地址在去寻找下一个元素的节点,在获取下一个元素的地址,知道寻找到指定的元素。
- 寻址次数非常多,寻址的次数和集合的规模有关,集合元素越多寻址的次数就越多,效率越低。
LinkedList的特有方法
1.由于在LinkedList中,维护和记录了整个集合的头部和尾部的节点地址,操作头部和尾部特别容易,代价非常低,提供了大量操作头部和尾部的方法
2.罗列:
addFirst(Object obj)
addLast(Object obj)
removeFirst()
removeLast()
getFirst()
getLast()
这次就先到这里,在Set集合的时候需要用到泛型的知识,下次会把详细的写一下,再来看Set集合。