Java进阶06 集合
一、集合及其体系结构
集合是一个长度可变的容器
1、集合的体系结构
1.1 单列集合
-
单列集合使用add()方法添加集合元素,一次只能添加一个元素。
-
单列集合均实现了Collection接口,该接口还有两个子接口List和Set。
-
List接口
List集合的特点是存取有序、有索引、可以存储重复的;包含ArrayList、LinkedList两个集合
-
Set接口
Set集合的特点是存取无序、没有索引、不可以存储重复的;包含TreeSet、HashSet、LinkedHashSet
-
1.2 双列集合
-
双列集合使用put()方法添加集合元素,一次可以添加两个元素。
-
双列集合均实现了Map接口
-
双列集合包括:TreeMap、HashMap、LinkedHashMap
二、Collection的使用
方法名 | 说明 |
---|---|
public boolean add(E e) | 把给定的对象添加到当前集合中,返回是否添加成功 |
public void clear() | 清空集合中所有的元素 |
public boolean remove(E e) | 把给定的对象在当前集合中删除,返回是否删除成功 |
public boolean contains(Object obj) | 判断当前集合中是否包含给定的对象 |
public boolean isEmpty() | 判断当前集合是否为空 |
public int size() | 返回集合中元素的个数/集合的长度 |
注意事项:
-
remove()、contains()底层都是依赖equals方法
-
clear()是清空集合中所有元素,不是销毁集合容器。清空后还是可以继续往集合中添加元素的
三、集合遍历方式(5种)
1、普通for循环
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
}
2、迭代器遍历
2.1 Collection接口的方法
方法 | 说明 |
---|---|
public Iterator<E> iterator() | 获取遍历集合的迭代器对象 |
public boolean hasNext() | 判断集合中是否还有元素 |
public E next() | 取出集合中元素,并且将指针向后移动一位 |
2.2 迭代器遍历
public class CollectionDemo2 {
public static void main(String[] args) {
//多态创建集合容器,左边为接口引用,右边为实现类对象
Collection<Student> c = new ArrayList<>();
c.add(new Student("张三", 23));
c.add(new Student("李四", 24));
c.add(new Student("王五", 25));
// 1. 获取迭代器 其实这句代码相当于 Iterator<Student> it = new Itr();
Iterator<Student> it = c.iterator();
// 2. 循环的判断, 集合中是否还有元素
while (it.hasNext()) {
// 3. 通过迭代器取出集合的元素
Student stu = it.next();
System.out.println(stu.getName() + "---" + stu.getAge());
//这样调用会出现信息错乱!!!
System.out.println(it.next().getName() + "---" + it.next().getAge());
}
}
}
注意:next()方法每调用一次,迭代器指针会后移一位,就会把不同集合元素的信息拼接到一起打印,为了避免这种信息错乱,建议在循环中,next()只调用一次
2.3 迭代器源码分析
private class Itr implements Iterator<E> {
//定义游标,表示指针指向
int cursor;
public boolean hasNext() {
//判断指针值是否等于集合长度
return cursor != size;
}
public E next() {
//定义i变量记录当前指针所指向的元素下标
int i = cursor;
//指针后移
cursor = i + 1;
//返回i队应下标所记录的元素值
return (E) elementData[lastRet = i];
}
}
3、增强for循环
增强for循环是JDK5之后出现的,其内部原理就是一个Iterator迭代器,它简化迭代器的代码书写,是迭代器遍历的语法糖。
3.1 格式
for(元素的数据类型 变量名 : 数据或者集合){
}
快捷键 需要迭代的集合.for再回车
3.2 Demo
public class CollectionDemo3 {
public static void main(String[] args) {
Collection<Student> c = new ArrayList<>();
c.add(new Student("张三", 23));
c.add(new Student("李四", 24));
c.add(new Student("王五", 25));
//增强for循环遍历集合
for (Student stu : c) {
System.out.println(stu);
}
System.out.println("----------------------");
//增强for循环遍历数组
int[] nums = {11, 22, 33};
for (int num : nums) {
System.out.println(num);
}
}
}
注意细节:增强for循环遍历数组时,循环变量直接代表每一份元素,并不是下标。为了避免和出错和fori搞混,这个循环变量我们一般不会取名为i
4、foreach方法
//遍历集合
default void forEach(Consumer<? super I>action)
跟进源码后发现该方法需要的参数Consumer是一个接口类型,那么我们就要传入该接口的实现类对象,可以创建一个并传入。然而源码中验证该接口还是一个函数式接口,因此可以传入匿名内部类,还可以将其改写成Lambda表达式。
public class CollectionDemo4 {
public static void main(String[] args) {
Collection<Student> c = new ArrayList<>();
c.add(new Student("张三",23));
c.add(new Student("李四",24));
c.add(new Student("王五",25));
//匿名内部类写法
c.forEach(new Consumer<Student>() {
@Override
public void accept(Student student) {
System.out.println(student);
}
});
//Lambda表达式写法
c.forEach(s-> System.out.println(s));
}
}
5、ListIterator遍历
继承了Iterator,是List集合派系所特有的迭代器,遍历方式与Iterator遍历类似,但也有其特殊之处:它内部含有hasPrevious()方法和previous()方法,可以配合使用进行倒序遍历,前提是必须要先正序遍历让指针移至最后,否则倒叙遍历没有效果
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
ListIterator<String> it = list.listIterator();
//正序遍历
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
System.out.println("---------------------------------");
//倒序遍历,前提必须先正序遍历让指针后移至最后,否则没有效果
while (it.hasPrevious()) {
String s = it.previous();
System.out.println(s);
}
}
四、List接口
list接口因为支持索引,所以多了很多索引操作的独特API
方法名 | 说明 |
---|---|
void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
五、数据结构
数据结构是计算机底层存储、组织数据的方式,是指数据相互之间是以什么方式排列在一起的
1、栈和队列
栈 | 队列 |
---|---|
一端开口(栈顶),一端封闭(栈底) | 两端均开口 |
栈顶出入栈 | 队尾入队,队头出队 |
后进先出 | 先进先出 |
2、数组和链表
数组 | 链表 |
---|---|
内存连续区域 | 在内存中游离,不连续 |
查询速度快:通过地址和索引定位,查任意数据耗时相同 | 查询速度慢:没有索引,无论查询哪个数据都要从头遍历 |
增删效率低:增删有可能大批量的移动数组中其他元素 | 增删效率相对数组快:增删不用移动大量元素,只需修改指针即可 |
-
单链表&双链表
链表元素在内存中是游离的,其中每个结点是独立的对象,在内存中不是连续的,每个结点有自己的存储地址,包含其存储的具体数据值和下一个结点的地址。见名知义,单链表即链接方向是单向的,对链表的访问要通过顺序读取从头部开始。双链表的链接方向是双向的,即每个数据结点中都有两个指针,分别指向直接后继和直接前驱。因此双向链表首尾操作极快!!!
六、ArrayList类&LinkedList类
1、ArrayList类
ArrayList底层是基于数组实现的,所以查询元素快,增删相对慢
1.1 ArrayList长度可变原理
ArrayList底层是数据结构,数组默认长度为10;当数组添加满了之后,会自动扩容为1.5倍,扩容时会先将原数组数据拷贝到新数组中,再将新元素添加到新数组
1.2 ArrayList源码解析
①使用空参构造器创建的集合,在底层创建一个默认长度为0的数组;
②添加第一个元素时,底层会创建一个新的长度为10的数组
③存满时,会扩容1.5倍
2、LinkedList类
LinkedList底层基于双链表实现的,查询元素慢,增删首尾元素是非常快的
特有方法 | 说明 |
---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回此列表中的第一个元素 |
public E getLast() | 返回此列表中的最后一个元素 |
public E removeFirst() | 从此列表中删除并返回第一个元素 |
public E removeLast() | 从此列表中删除并返回最后一个元素 |
-
注意:LinkedList的get()方法,表面看起来是根据索引获取元素,实际并非如此。它的原理很简答,是通过遍历链表来查找指定索引的元素。具体来说,get()方法从链表的表头开始遍历,它经过一个节点,就将计数器加一。当计数器的值等于要查找的索引时,get()方法就返回该节点的元素值,否则继续遍历直到表尾。
七、泛型
JDK5引入泛型,可以在编译阶段约束操作的数据类型,并进行检查。使用泛型的好处是:统一数据类型,将运行期的错误提升到了编译期。泛型中只能编写引用型数据,如果不指定泛型的具体类型,则系统默认创建Object对象
1、泛型类
1.1 使用场景
当类中的属性或是方法却无法确定具体类型时,可以设计泛型类
1.2 确定具体类型
在创建对象的时候确定到具体数据类型
//泛型类
public class ArrayList<E>{
private E e;
public E getE(){
return e;
}
public void setE(E e){
this.e = e;
}
}
public static void main(String[] args){
//创建对象,指定类型为Integer
Student<Integer> stu = new Student<>;
}
2、泛型方法
2.1 非静态泛型方法
泛型是根据类的泛型去匹配的
public class ArrayList<E>{
public boolean add(E e){
}
}
2.2 静态泛型方法
需要声明出自己独立的泛型
public static<T> void printArray(T[] array){}
public class Demo3 {
public static void main(String[] args) {
Integer[] arr1 = {11,22,33};
Double[] arr2 = {11.1,22.2,33.3};
String[] arr3 = {"张三","李四","王五"};
printArray(arr1);
printArray(arr2);
printArray(arr3);
}
//该方法在main函数中调用,因此必须是static修饰,又想接收各种类型,所以自己定义独立的泛型
private static<T> void printArray(T[] arr) {
System.out.print("[");
for (int i = 0; i < arr.length-1; i++) {
System.out.print(arr[i]+",");
}
System.out.println(arr[arr.length-1]+"]");
}
}
3、泛型接口
3.1 使用场景
接口中的某个抽象方法确定不了参数的具体类型,就可以声明泛型,让该方法的泛型去匹配接口的泛型
3.2 确定具体数据类型
类实现接口时,如果接口带有泛型,有两种操作方式
-
类实现接口的时候,直接确定类型
-
实现类延续接口的泛型,等创建对象的时候再确定
//泛型接口
interface Inter<E>{
//抽象方法的参数匹配接口的泛型
void show(E e);
}
//类实现接口的时候直接指定类型为String
class InterAImpl implements Inter<String>{
@Override
public void show(String s) {
}
}
//实现类延用接口泛型,则在该实现类创建对象的时候一定要给出具体类型
class InterBImpl<E> implements Inter<E>{
@Override
public void show(E e) {
}
}
4、泛型通配符
书写位置在<>内,有以下三种用法
-
?:任意类型
-
?extends E:只能接收E或者E的子类
-
?super E:只能接收E或者E的父类