# 集合的概述 (重点)

[TOC]

  • 集合继承体系视图 (需要记住)

image-20220901175038918

# 1. 集合的由来
  • 当需要在 java 程序中记录单个数据内容时,则声明一个变量
  • 当需要在 java 程序中记录多个类型相同的数据内容时,声明一个一维数组
  • 当需要在 java 程序中记录多个类型不同的数据内容时,则创建一个对象
  • 当需要在 java 程序中记录多个类型相同的对象数据时,创建一个对象数组
  • 当需要在 java 程序中记录多个类型不同的对象数据时,则准备一个集合
# 1.1 集合的框架结构
  • java 中集合框架顶层框架是:java.util.Collection 集合和 java.util.Map 集合
  • 其中 Collection 集合中存取元素的基本单位是:单个元素
  • 其中 Map 集合中存取元素的基本单位是:单对元素

# 1.2Collection 集合 (重点)

# 1.2.1 基本概念
  • java.util.Collection 接口是 List 接口,Queue 接口以及 Set 接口的父接口,因此该接口里定义的方法既可用于操作 List 集合,也可用于操作 Queue 集合和 Set 集合
# 1.2.2 常用的方法 (熟练,记住)
方法声明功能介绍
boolean add(E e)向集合中添加元素
boolean addAll(Collection<?extends E> c)用于将参数指定集合 c 中的所有元素添加到当前集合中
boolean contains(Object o)判断是否包含指定对象
boolean containsAll(Collection<>>c)判断是否包含参数指定的所有对象
boolean retainAll(Collection<?>c)保留当前集合中存在且参数集合中存在的所有对象
boolean remove(Object o)从集合中删除对象
boolean removeAll(Collection<>>c)从集合中删除参数指定的所有对象
void clear()清空集合
int size()返回包含对象的个数
boolean isEmpty()判断是否为空
boolean equals(Object o)判断是否相等
int hashCode()获取当前集合的哈希码值
Object[] toArray()将集合转换为数组
Iterator<E> iterator()获取当前集合的迭代器
Collection collection = new ArrayList();
    collection.add("A");
    collection.add("B");
    collection.add("C");
    Collection collection1 = new ArrayList();
    collection1.add("java");
    collection1.add("ja");
    // 使用 add 和使用 addAll 将指定集合添加到该集合中,输出格式如下
    collection1.addAll(collection);
    System.out.println(collection1);//[java, ja, A, B, C]
	// 将指定集合的一整个集合全部添加到该集合中,而 addAll 只是拿了全部的元素
    collection1.add(collection); 
    System.out.println(collection1);//[java, ja, A, B, C, [A, B, C]]

image-20220901193321585

# contains 判断两个对象时输出为 false 的问题?😓
  • 问题二:为什么 String 判断的时候为 true?
  • 解答:因为 String 类的 equals 已经被重写为比较的值所以它们使用 contains 判断时是值,既然是值那么当然相同,在用自己的类的时候要重写自己的 equals 和 hashCode 方法
Collection collection = new ArrayList();
        collection.add(new Person("张飞",30));
        //contains: 判断该集合中是否包含该元素,但是打印结果为:false?
        //                     传入的对象数据     0       长度
        // 源码:int indexOfRange (Object o, int start, int end) {
        //         Object 类型的数组
        //        Object[] es = elementData;
        //        if (o == null) { // 判断是否为空
        //            for (int i = start; i < end; i++) {
        //                  判断为空则执行返回下标
        //                if (es[i] == null) {
        //                    return i;
        //                }
        //            }
        //        } else {// 如果不为空则执行
        //            for (int i = start; i < end; i++) {
        //              通过 equals 方法来判断这两个对象是否相同
        //                if (o.equals(es[i])) {
        //                      返回数据的下标
        //                    return i;
        //                }
        //            }
        //        }
        //        return -1;
        //    }
        // 该方法判断不为空时其中执行了 equals 方法可以看下 equals 的源码:
        //public boolean equals(Object obj) {
        //        return (this == obj);
        // }
        // 此方法是判断的两个对象的地址值是否相同由此可见为什么这么判断时为 false 了,因为它们的地址值不同
        // 当然了通过这个也可得知,我们可以重写 equals 跟 hashCode 方法来决定它是对是错
        boolean f = collection.contains(new Person("张飞",30));
        System.out.println(f);//false
  • 赋值时的数据类型问题,集合默认为 Object 的类型
Collection collection = new ArrayList();
        collection.add(new String("A"));
        collection.add(Integer.valueOf(1));
        // 直接赋值但是它们的数据类型却是 引用数据类型的
        collection.add("a");// 常量池
        collection.add(1);// 包装类机制
# 使用 add 与 addAll 时 contains 与 containsAll 判断结果
  • add: 将指定集合的整体赋值到该集合中 : [1, [2]]
  • addAll: 将指定集合中所有元素赋值到该集合中 : [1, 2]
  • contains: 判断该集合中是否包含指定集合的整体,而集合的整体要删除就是全删除比如: remove(Collection集合对象名)
  • containsAll: 判断该集合中是否包含指定集合的所有元素,Collection 类型所以只能赋值集合对象判断是包含所有元素缺一不可
Collection collection = new ArrayList();
        collection.add(1);
        Collection collection1 = new ArrayList();
        collection1.add(2);
        collection.addAll(collection1);
        collection.add(collection1);
        // 判断该集合中是否包含指定集合所有元素 [1, 2]
        // 和添加一样判断的是 2
        boolean f = collection.containsAll(collection1);
        // 判断该集合中是否包含指定集合的整体 [1, [2]] 程序从上往下执行时此时集合内容为:[1, 2, [2]]
        // 和添加一样判断的是 [2]
        boolean f1 = collection.contains(collection1);
        //                       add     addAll
        System.out.println(f);//false     true
        System.out.println(f1);//true     false
# retainAll : 该集合与指定集合之间取交集元素,并赋值到该集合中,如果集合发生改变则返回 true, 否则返回 false 取集合交集相同的元素返回布尔值 (确保改变的集合有一个是不相同的)
Collection ar = new ArrayList();
        Collection br = new ArrayList();
        ar.add(1);
        ar.add(2);
        br.add(2);
        br.add(3);
        // 取该集合与指定集合之间的交集 (相同) 元素,发生改变返回 true
        boolean f = ar.retainAll(br);
        System.out.println(f);//true
        System.out.println(ar);//[2] 改变 (交集 (相同)) 的集合
        System.out.println(br);//[2,3] 参与交集运算的集合,只是参与者
        Collection ar1 = new ArrayList();
        Collection br1 = new ArrayList();
        ar1.add(1);
        br1.add(1);
        boolean f1 = ar1.retainAll(br1);
        System.out.println(f1);//false , 交集的两个集合进行交集后该集合中的元素没有发生改变还是 1
        // 如果指定集合与该集合中的元素不相同的话则会将该集合中元素改变为空集合
        System.out.println(ar1);//1
        System.out.println(br1);//1
# remove 删除集合中的元素:删除对象元素问题😅
Collection ar = new ArrayList();
        Person a = new Person("张飞", 30);
        System.out.println("a = " + a.hashCode()); // 重写 equals 与 hashCode 前:34616417 重写后:24616417
        ar.add(a);
        Person b = new Person("张飞", 30);
        System.out.println("b = " + b.hashCode()); // 重写 equals 与 hashCode 前:54316295 重写后:24616417
        boolean f = ar.contains(b); // 重写 equals 与 hashCode 前:false 重写后:true
        System.out.println("contains = " + f);
        boolean f1 = ar.remove(new Person("张飞",30));
        System.out.println("remove = " + f1);// 重写 equals 与 hashCode 前:false       重写 equals 与 hashCode 后:true
# clear,size,isEmpty 方法

clear: 清空集合中所有元素

size: 返回集合元素个数 (集合大小)

isEmpty: 判断集合是否为空,也就是没有任何元素

Collection ar = new ArrayList();
        ar.add(1);
        System.out.println("集合现状: "+ar);
        System.out.println(0 == ar.size() ? "集合为null" : "集合存在元素");
        System.out.println(ar.isEmpty() ? "集合为null" : "集合存在元素");
        ar.clear();
        System.out.println("集合已被清空 : "+ar);
        System.out.println(0 == ar.size() ? "集合为null" : "集合存在元素");
        System.out.println(ar.isEmpty() ? "集合为null" : "集合存在元素");
# equals: 两个集合之间的值,如果两个集合之间的 size (个数) 与 element (元素) 都不相同则返回 false
Collection ar = new ArrayList();
        Collection br = new ArrayList();
        ar.add(1);
        ar.add(2);
        br.add(1);
        br.add(2);
        br.add(2);
        // 集合中重写 equals 比较的是两个集合之间的值,如果两个集合之间的 size (个数) 与 element (元素) 其中一个条件不满足都返回 false
        boolean f = ar.equals(br);
        System.out.println(f);
# toArray 与 Arrays.asList 方法
  • toArray: 将集合转换为数组
  • Array.asList: 将数组转换为集合
Collection<String> collection = new ArrayList();
        collection.add("A");
        collection.add("B");
        collection.add("C");
        // 数组与集合之间的转换。通常认为集合是用于取代数组的结构
        Object[] x = collection.toArray();
        System.out.println("集合转换为数组后: "+ Arrays.toString(x));
        Collection x1 = Arrays.asList(x);
        System.out.println("将数组转换为集合: "+x1);

# 2 Iterator 接口 (重点)

# 2.1 基本概念
  • java.util.Iterator 接口主要用于描述迭代器对象,可以遍历 Collection 集合中的所有元素
  • java.util.Collection 接口继承 Iterator 接口,因此所有实现 Collection 接口的实现类都可以使用该迭代器对象
# 2.2 常用方法
方法声明功能介绍
boolean hasNext()判断集合中是否又可以迭代 / 访问的元素
E next()用于取出一个元素并指向下一个元素
void remove()用于删除访问到的最后一个元素
# Iterator 迭代器遍历集合元素
  • 迭代器集合遍历

  • 使用迭代器打印出 toString 的效果

Collection collection = new ArrayList();
        collection.add("A");
        collection.add("B");
        collection.add(new Person("张飞",30));
        System.out.println("-------输出语句打印集合元素-------\n");
        Iterator i = collection.iterator();
        System.out.println(i.hasNext());
        System.out.println("print: "+i.next());
        System.out.println(i.hasNext());
        System.out.println("print: "+i.next());
        System.out.println(i.hasNext());
        System.out.println("print: "+i.next());
//        System.out.println(i.next());//Exception : NoSuchElementException
        System.out.println("-------迭代器遍历打印集合元素---------\n");
        // 重置指针,因为它的位置已经在最低了
        i = collection.iterator();
        // 使用迭代器循环遍历集合元素
        while(i.hasNext()){
            System.out.println("Iterator: "+i.next());
        }
        System.out.println("--------StringBuilder模拟toString打印元素--------\n");
        StringBuilder builder = new StringBuilder();
        // 重置指针
        i = collection.iterator();
        builder.append("builder = [");
        // 使用迭代器循环遍历集合元素
        while(i.hasNext()){
            Object x = i.next();
            // 判断如果 hasNext 为 false 也就意味着后面没有元素则将最后一个元素和] 添加到 StringBuilder 中
            if(! i.hasNext()){
                builder.append(x).append("]");
            }else
            builder.append(x).append(",").append(" ");
        }
        System.out.println(builder);

# 3 for each 循环 (重点)

# 3.1 基本概念
  • java5 推出了增强型 for 循环语句,可以应用数组和集合的遍历
  • 是经典迭代的 "简化版"
# 3.2 语法格式
for(元素类型 变量名 : 数组/集合对象名){
	循环体;
}
# 3.3 执行流程
  • 不断地从数组 / 集合中取出一个元素赋值给变量名并执行循环体,直到取完所有元素为止

# 4 List 集合 (重中之重)

# 4.1 基本概念
  • java.util.List 集合是 Collection 集合的子集合,该集合中允许有重复的元素并且有先后放入次序

  • 该集合的主要实现类有:ArrayList 类,LinkedList 类,Stack 类,Vector 类

  • 其中 ArrayList 类的底层是采用动态数组进行数据管理的,支持下标访问,增删元素不便

  • 子集合和当前集合公用同一块内存空间,就是通过一个返回方法得到的当前集合的子集合如果删除这个子集合中的元素或者添加其这个集合与子集合都会改变:方法体验在 [标题:删除 subList 截取中的元素原来的集合对象会发生改变吗?] 中

    image-20220902115002667

  • 其中 LinkedList 类的底层是采用双向循环链表进行数据管理的,访问不便,增删元素方便

  • 可以认为 ArrayList 和 LinkedList 的方法在逻辑上完全一样,只是在性能上有一定的差别,ArrayList 更适合于遍历而 LinkedList 更适用于插入和删除,在性能要求不是特别苛刻的情形下可以忽略这个差别

  • 其中 Stack 类的底层是采用动态数组进行数据管理的,该类主要用于描述一种具有后进先出特征的数据结构叫做栈 (last in first out LIFO)

  • 其中 Vector 类的底层是采用动态数组进行数据管理的,该类与 ArrayList 类相比属于线程安全的类,效率比较低

# 4.2 常用的方法
方法声明功能介绍
void add(int index,E element)向集合中指定位置添加元素
boolean addAll(int index,Collection<? extends E>c)向集合中添加所有元素
E get(int index)从集合中获取指定位置元素
int indexOf(Object o)查找参数指定的对象
int lastIndexOf(Object o)反向查找参数指定的对象
E set(int index,E element)修改指定位置的元素
E remove(int index)删除指定位置的元素
List<E> subList(int fromindex,int tolndex)用于获取子 List
# 遍历删除集合元素的问题:
  • 使用 for 循环,通过集合 size 获取长度遍历这个集合
  • 方法体重使用 remove (i) 遍历删除元素
  • 问题解释:没删除一个元素 size 的值会减少一个,而且前面的元素被删除后面的元素向前补位
List<String> list = new ArrayList<>();
        list.add(0,"A");
        // 向集合末尾添加元素
        list.add(1,"B");
        // 向集合中间添加元素
        list.add(1,"C");
        list.add(3,"D");
        System.out.println(list);//[A, B, C]
        String t = list.get(0);
        System.out.println(t);//A
        System.out.println("---------------------------");
        for(int i = 0;i < list.size();i ++){
            // 每次删除,size 减一,元素向前补位
            System.out.println(list.remove(i));// 删除成功的元素: A B
        }
        System.out.println(list);// 删除留下的元素: [C, D]
  • 问题解决方案
List<String> list = new ArrayList<>();
        list.add(0,"A");
        // 向集合末尾添加元素
        list.add(1,"B");
        // 向集合中间添加元素
        list.add(1,"C");
        list.add(3,"D");
        System.out.println(list);//[A, B, C]
        String t = list.get(0);
        System.out.println(t);//A
        System.out.println("-------------删除元素remove详解--------------");
        // 方式一:逆向删除
        for(int i = list.size() - 1;i >= 0;i--){
            // 从后向前删除集合中每一个元素,解决了向前补位的问题
            list.remove(i);
        }
        System.out.println(list);//[]
//        方式二:去掉 i++ 的多余操作
//        因为每次删除元素集合大小都在减少所以,可以不使用 i--, 不然就会删除一个元素跳过一个元素
        for(int i = 0;i < list.size();){
            list.remove(i);
        }
        System.out.println(list);//[]
        // 方式三:原地删除
        for(int i = 0;i < list.size();){
            // 既然每次删除后元素都会向前部位,那么就只删除它 0 的位置将其全部删除
            list.remove(0);
        }
        System.out.println(list);//[]
# subList: 截取起始和结尾区间的元素
List<String> list = new ArrayList<>();
        list.add(0,"A");
        // 向集合末尾添加元素
        list.add(1,"B");
        // 向集合中间添加元素
        list.add(1,"C");
        list.add(3,"D");
        System.out.println("-------------subList-------------");
        // 包含 1 不包含 3
        List<String> list1 = list.subList(1,3);
        System.out.println(list1);//[B, C]
# 问题:删除 subList 截取中的元素原来的集合对象会发生改变吗?
  • 解释:因为公用同一块内存所以原来对象也会被删除
  • 代码如下:
List<String> list = new ArrayList<>();
        list.add(0,"A");
        // 向集合末尾添加元素
        list.add(1,"B");
        // 向集合中间添加元素
        list.add(1,"C");
        list.add(3,"D");
        String ar = list.set(3,"E");
        System.out.println("set: "+ar);
        System.out.println("list修改后 删除前: "+list);
        System.out.println("-------------subList-------------");
        // 包含 1 不包含 3
        List<String> list1 = list.subList(1,3);
        System.out.println("subList: "+list1);//[B, C]
        System.out.println("list1删除前: "+list1);
        String  t = list1.remove(0);
        System.out.println("remove:=>=>=> [被删除的元素: "+t+"]");
        System.out.println("list1删除后: "+list1);
        // 因为公用同一块内存所以将 List1 中的 C 删除之后 list 中也就没有了 C
        System.out.println("list删除后: "+list);
        list1.add("L");// 子集合,来源于 subList
        list1.add("j");// 子集合,来源于 subList
        System.out.println(list);// 当前集合,打印结果 [A, B, L, j, E]
# Stack: 介绍😏

push : 向栈中存储元素

peek : 获取该栈的栈顶元素

pop : 从栈顶开始,移除并返回该元素

  • 案例题目

准备一个 Stack 集合,将数据 11,22,33,44,55 依次入栈并打印,然后查看栈顶元素并打印,然后将栈顶所有数据依次出栈并打印

Stack<Integer> st = new Stack();
        Stack<Integer> sf = new Stack();
        System.out.println("---------向st中存储元素----------\n");
        for(int i = 1;i <= 5;i ++){
            Integer x = st.push(i * 11);
            System.out.println("入栈元素: "+x);
        }
        System.out.println("------------获取的st栈顶的元素-------------\n");
        Integer v = st.peek();
        System.out.println("栈顶元素: "+v);
        // 因为每次将元素出栈后,size 的大小就会减少所以将这个 size 的值赋值给一个变量来记录
        int len = st.size();
        for(int i = 0;i < len;i ++){
            Integer c = st.pop();
            System.out.println("出栈元素: "+c);
        }

再准备一个 Stack 对象,将数据从第一个栈中取出来放入第二个栈中,然后再从第二个栈中取出并打印

Stack<Integer> st = new Stack();
        Stack<Integer> sf = new Stack();
        System.out.println("---------向st中存储元素----------\n");
        for(int i = 1;i <= 5;i ++){
            Integer x = st.push(i * 11);
            System.out.println("入栈元素: "+x);
        }
        System.out.println("------------获取的st栈顶的元素-------------\n");
        Integer v = st.peek();
        System.out.println("栈顶元素: "+v);
        System.out.println("---------将st中的元素取出存到sf中----------\n");
        System.out.println("           加载中,正在努力转移元素中🤔....... /%               \n");
        for(int i = 0;i < st.size();){
            Integer x1 = st.pop();
            Integer f = sf.push(x1);
        }
        System.out.println("------------获取的sf栈顶的元素-------------\n");
        Integer v1 = sf.peek();
        System.out.println("栈顶元素: "+v1);
        System.out.println("---------取出sf中的元素----------\n");
        // 每次将栈中取出元素 size 的值就会减少,将 size 的值赋值给一个变量来记录则不会随着栈的元素减少而减少
        int len = sf.size();
        for(int i = 0;i < len;i ++){
            Integer r = sf.pop();
            System.out.println(r);
        }

# 5 Queue 集合 (重点)

# 5.1 基本概念
  • java.util.Queue 集合是 Collection 集合的子集合,与 List 集合属于平级关系
  • 该集合的主要用于描述具有先进先出特征的数据结构,叫做队列 (first in first out FIFO)
  • 该集合的主要实现类是 LInkedList 类,因为该类在增删方面比较有优势
# 5.2 常用的方法
方法声明功能介绍
boolean offer(E e)将一个对象添加至队尾,若添加成功则返回 true
E poll()从队首删除并返回一个元素
E peek()返回队首的元素
  • 案例题目

    准备一个 Queue 集合,将数据 11,22,33,44,55 依次入队并打印,然后查看队首元素并打印,然后将队列中所有数据依次出队并打印

Queue<Integer> ue = new LinkedList();
        for(int i = 1;i <= 5;i ++){
            // 向集合中添加元素
            ue.offer(i * 11);
            System.out.println("入队元素:=>"+ue);
        }
        System.out.println("-----------------------------");
        // 获取集合队首元素
        Integer r = ue.peek();
        System.out.println("队首元素:=>"+r);
        System.out.println("-----------------------------");
        int len = ue.size();
        for(int i = 0;i < len;i ++){
            System.out.println("出队元素:=>"+ue);
            // 出队移除并返回元素
            Integer x = ue.poll();
        }

打印结果: image-20220903114124827

# 6 Deque 集合

  • 支持两端元素插入和移除的线性集合
  • deque 是 "双端队列" 的缩写,通常发音为 "deck"
  • 大多数 deque 实现对它们可能包含的元素数量没有固定的限制
  • 但是这个接口支持容量限制的 Deque 以及那些没有固定大小限制的 Deque
  • 这个接口定义了访问 deque 容器两端元素的方法
  • 提供了插入,删除和检查元素的方法
  • 这些方法以两种形式存在,一个在操作失败时抛出异常
  • 另一个返回特殊值,(null 或 false, 取决于操作)
  • 插入操作后一种形式是专门为使用容量受限的 Deque 实现而设计的
  • 在大多数实现中,插入操作不会失败

抓住关键词:支持两端元素插入和移除的线性集合,双端队列,包含的元素数量没有固定的限制,访问 deque 容器两端元素的方法

# 常用方法
方法声明功能介绍
addFirst(e)向集合开头添加元素
offerFirst(e)向集合开头添加元素
addLast(e)向集合尾部添加元素
offerLast(e)向集合尾部添加元素
removeFirst()删除集合开头的元素
removeLast()删除集合尾部的元素
pollFirst()移除并返回集合开头的元素
pollLast()移除并返回集合尾部的元素
getFirst()获取集合开头的元素
getLast()获取集合尾部的元素
peekFirst()获取集合开头元素
peekLast()获取集合尾部元素
pop()移除并返回队首元素
add()向集合中添加元素
# 与 List 不同

与 List 接口不同,该接口不支持对元素的索引访问

null 注意事项

  1. 虽然不严格要求 Deque 实现禁止空元素的插入,但是强烈建议不要这么做

  2. 强烈建议任何允许 null 元素的 Deque 实现的用户不要利用插入 null 的能力

  3. 这是因为 null 被各种方法用作为特殊的返回值,以指示 deuqe 为空

因为 null 个各种方法作为特殊的返回值,所以建议不要存储 null 值

# 子类,ArrayDeque 分析

翻译过的。官方的代码

/*
	存储 deque 容器元素的数组。deque 的容量是数组的长度,总是 2 的幂。
	数组永远不允许被填满,除非在一个 addX 方法中,数组在被填满后立即被调整大小
	(参见 doubleCapacity),从而避免头尾绕成相等。
	我们还保证所有不包含 deque 元素的数组单元格始终为空。
*/
	transient Object[] elements;// 非私有以简化嵌套类访问
/*
	deque 容器头部元素的索引 (即将被 remove () 或 pop () 移除的元素); 或者如果 deque 为空,则等于 tail 的任意数。 -- elements 的头部
*/
    transient int head;
/*
	下一个元素将被添加到 deque 容器尾部的索引 (通过 addLast (E), add (E),
	或 push (E))。 -- 可以理解为 elements 的末端
*/
    transient int tail;
    /*
    我们将用于新创建 deque 的最小容量。一定是 2 的幂。 最小的初始化容量
    */
	private static final int MIN_INITIAL_CAPACITY = 8;

transient 关键字是指,该数据只能在内存中使用,而无法保存到本地的文件中

# 图解数据存储过程

从下图初始化的 Deque 结构中很明显可以看出为什么我们不能插入 null, 因为失利后各个元素的默认值就是 null, 从而插入 null 的话会造成一下情况:下次在该位置插入元素时无法确定该位置是否存在元素

从尾部插入两个元素,注意头 (head) 尾 (tail) 指针的位置

如果从头部插入数据,注意头 (head) 尾 (tail) 指针的位置

注意 head 和 tail 指向的地方是否有数据存储

  • 线程不安全

我们可以看到,ArrayDeque 中的方法都没有加 synchronized 的关键字,故而是线程不安全

# 7 泛型机制 (熟悉)

泛型 -> 广泛的类型

# 7.1 基本概念
  • 通常情况下刻意存放不同类型的对象,是因为将所有对象看做 Object 类型放入的,因此从集合中取出元素时也是 Object 类型,为了表达该元素真实的数据类型,则需要强制类型转换,而强制类型转换可能会引发类型转换异常
  • 为了避免上述错误的发生,从 Java5 开始增加泛型机制,也就是在集合名称的右侧开始使用 <数据类型> 的方式来明确要求该集合中可以存放的元素类型,若放入其它类型的元素则编译报错
  • 泛型只在编译时期有效,在运行时期下区分是什么类型
//java7 开始的新特性:菱形特性,就是前面加上泛型的类型后面不需要加
        List<String> list = new ArrayList<>();
List<String> list = new ArrayList<>();
        list.add("1");
        list.add("a");
        List<String> list1 = new ArrayList<>();
        list1.add("java");
        // 两个集合类型相同时,允许使用 = 符号来让 list 集合的元素覆盖 list1 中的元素
        list1 = list;
        System.out.println(list1);
  • 如果两个集合泛型类型不同则不能使用这种方式,则否
# 7.2 底层原理
  • 泛型的本质就是参数化类型,也就是让数据类型作为参数传递,其中 E 相当于参数负责占位,而使用集合时 <> 中的数据类型相当于实际参数,用于给参数 E 进行初始化,从而使得集合中所有 E 被实际参数替换,由于实际参数可以传递各种各样广泛的数据类型,因此得名为泛型

    • 如:

      // 其中 i 叫做形式参数,负责占位 其中 E 叫做形式参数,负责占位

​ // int i = 10; E = String;

​ // int i = 20; E = Integer;

public static void show(int i){ public interface List<E>{

​ .... ....

} }

​ // 其中 10 叫做实际参数,负责给形式参数初始化 // 其中 String 叫做实际参数

show(10) List<String> lt1 = ...

show(20) List<String> lt2 = ...

# 7.3 自定义泛型接口
  • 泛型接口和普通接口的区别就是后面添加了类型参数列表,可以有多个类型参数,如:<E,T,...> 等.
# 7.4 自定义泛型类
  • 泛型类和普通类的区别就是类名后面添加了类型参数列表,可以有多个类型参数,如 <E,T,...> 等.
  • 实例化泛型类型时应该指定具体的数据类型,并且引用数据类型而不是基本数据类型
  • 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型
  • 父类必须是 "富二代" , 子类除了指定或保留父类的泛型,还可以增加自己的泛型
//public class SubPerson extends Person {// 不保留泛型并没有指定类型,此时 Person 类中 T 默认为 Object 类型,擦除泛型
//public class SubPerson extends Person<String>{// 不保留泛型但指定了泛型的类型,此时 Person 类中的 T 被指定为 String 类型
//public class SubPerson<T> extends Person<T>{// 保留父类的泛型,可以在构造对象时来指定 T 的类型
public class SubPerson<T,T1> extends Person<T>{// 保留父类的泛型,同时在子类定义新的泛型
   
}
# 7.5 自定义泛型方法
  • 泛型方法就是我们输入参数的时候,输入的是泛型参数,而不是具体的参数,我们在调用这个泛型方法的时候需要对泛型参数进行实例化
  • 泛型方法的格式:
[访问权限] <泛型> [返回值类型] (方法名)([泛型标识,参数名称]){
   方法体;
}
  • 在静态方法中使用泛型参数的时候,需要我们把静态方法定义为泛型方法
// 不是泛型方法,该方法不能使用 static 关键字修饰,因为该方法中的 T 需要在 new 对象时才能明确类型,静态方法优先于静态方法产生所以会报错,只是一个返回泛型类型数据的方法,跟自定义泛型方法不同,泛型在创建对象时明确泛型的数据类型
public /*static*/ T getT() {
   return t;
}
// 自定义泛型方法:自定义方法实现将参数指定数组中的所有元素打印出来
// 泛型方法类型默认为 Object 类型
public static <T1> void printArray(T1[] arr){
   for(T1 i : arr){
      System.out.println(i);
   }
}
# 7.6 泛型在继承上的体现
  • 如果 B 是 A 的一个子类或子接口,而 G 是具有泛型声明的类或接口,则 G<B> 并不是 G<A > 的子类型!

    比如:String 是 Object 的子类,但是 List<String> 并不是 List<Object > 的子类

# 7.7 通配符的使用
  • 有时候我们希望传入的类型在一个指定的范围内,此时就可以使用泛型通配符了
  • 如:之前传入的类型要求为 Integer 类型,但是后来业务需要 Integer 的父类 Number 类也可以传入
  • 泛型中有三种通配符的形式:

<?> 无限制通配符:表示我们可以传入任意类型的参数 (并不是说任意类型的对象!)

<? extends E> 表示类型的上界是 E, 只能是 E 或者是 E 的子类

<? super E> 表示类型的下界是 E, 只能是 E 或者是 E 的父类

// 泛型在继承上的体现
/*
 * 如果 B 是 A 的一个子类或子接口,而 G 是具有泛型声明的类或接口,则 G<B > 并不是 G<A > 的子类型!
 * 比如:String 是 Object 的子类,但是 List<String > 并不是 List<Object > 的子类
 */
List<Animal> list = new ArrayList<>();
List<Cat> list1 = new ArrayList<>();
// 试图将 list1 的数值赋值给 list, 也就是发生 List<Cat > 类型向 List<Animal > 类型的转换
list = list1; //:Error 类型之间不具备父子类关系
System.out.println("----------------------------------------");
// 使用通配符作为泛型类型的公共父类
List<?> list3 = new ArrayList<>();
list3 = list;// 可以发生 List<Animal > 类型到 List<?> 类型的转换
list3 = list1;// 可以发生 List<Cat > 类型到 List<?> 类型的转换
// 向公共父类中添加元素和获取元素,?是任意类型所以不能指定对象添加
list3.add(new Animal());//Error: 不能存放 Animal 类型的对象
list3.add(new Cat());//Error: 不能存放 Dog 类型的对象,不支持元素的添加操作
Object o = list3.get(0);// 支持元素的获取操作,全部当做 Object 类型来处理
// 泛型在继承上的体现
/*
 * 如果 B 是 A 的一个子类或子接口,而 G 是具有泛型声明的类或接口,则 G<B > 并不是 G<A > 的子类型!
 * 比如:String 是 Object 的子类,但是 List<String > 并不是 List<Object > 的子类
 */
List<Animal> list = new ArrayList<>();
List<Cat> list1 = new ArrayList<>();
// 试图将 list1 的数值赋值给 list, 也就是发生 List<Cat > 类型向 List<Animal > 类型的转换
//list = list1; :Error 类型之间不具备父子类关系
System.out.println("----------------------------------------");
// 使用通配符作为泛型类型的公共父类
List<?> list3 = new ArrayList<>();
list3 = list;// 可以发生 List<Animal > 类型到 List<?> 类型的转换
list3 = list1;// 可以发生 List<Cat > 类型到 List<?> 类型的转换
// 向公共父类中添加元素和获取元素
//list3.add (new Animal ());//Error: 不能存放 Animal 类型的对象
//list3.add (new Cat ());//Error: 不能存放 Dog 类型的对象,不支持元素的添加操作
Object o = list3.get(0);// 支持元素的获取操作,全部到做 Object 类型来处理
System.out.println("----------------------------------------");
// 使用有限制的通配符进行使用
//extends:上界是子类
List<? extends Animal> list4 = new ArrayList<>();
List<? extends Cat> list5 = new ArrayList<>();
// 不支持元素的添加操作
//list4.add(new Animal());
//list4.add(new Cat());
//list4.add(new Object());
list4 = list5;// 允许的操作,子类向父类转换
// 获取元素
Animal animal = list4.get(0);
System.out.println("----------------------------------------");
//super:下界是父类
List<? super Animal> list5 = new ArrayList<>();
List<? super Cat> list6 = new ArrayList<>();
//list5.add (new Person ());//Person 是人是动物的父类,不能添加父类对象
// 可以添加自己,子类关系的对象
list5.add(new Animal());
list5.add(new Cat());
list6 = list5;// 父类向子类转换
//list5.add (new Object ());// 超出了 Animal 类型范围
Object x = list5.get(0);

# 8.Set 集合 (熟悉)

8.1 基本概念

  • java.util.Set 集合是 Collection 集合的子集合,与 List 集合平级
  • 该集合中元素没有先后放入次序,且不允许重复
  • 该集合的主要实现类是:HashSet 类和 TreeSet 类以及 LinkedHashSet 类
  • 其中 HashSet 类的底层是采用哈希表进行数据管理的
  • 其中 TreeSet 类的底层是采用红黑树进行数据管理的
  • 其中 LinkedHashSet 类与 HashSet 类的不同之处在于内部维护了一个双向链表,链表中记录了元素的迭代顺序,也就是元素插入集合中的先后顺序,因此便于迭代

8.2 常用的方法

  • 参考 Collection 集合中的方法即可

  • 案例题目

    准备一个 Set 集合指向 HashSet 对象,向该集合中添加元素 "two" 并打印,再向集合中添加元素 "one" 并打印,再向集合中添加元素 "three" 并打印,再向集合中添加 "one" 并打印

8.3 元素放入 HashSet 集合的原理

  • 使用元素调用 hashCode 方法获取对应的哈希码值,再由某种哈希算法算出该元素在数组中的索引位置
  • 若该位置没有元素,则将该元素直接放入即可
  • 若该位置有元素,则使用新元素与已有元素依次比较哈希码值,若哈希码值不相同,则将该元素直接放入
  • 若新元素与已有元素的哈希码值相同,则使用新元素调用 equals 方法与已有元素依次比较
  • 若相等则添加元素失败,否则将元素直接放入即可

image-20220911212931638

  • 思考:为什么要求重写 equals 方法后要重写 hashCode 方法呢?

  • 解析:

    当两个元素调用 equals 方法相等时证明这两个元素相同,重写 hashCode 方法后保证这两个元素得到的哈希码值相同,由同一个哈希算法生成的索引位置相同,此时只需要与该索引位置已有元素比较即可,从而提高效率并避免重复元素的出现.

# 8.4TreeSet 集合的概念
  • 二叉树主要指每个节点最多只有两个子节点的树形结构
  • 满足以下 3 个特征的二叉树叫做有序二叉树
    • a. 左子树中的任意节点元素都小于根节点元素值
    • b. 右子树中的任意节点元素都大于根节点元素值
    • c. 左子树和右子树的内部也遵守上述规则
  • 由于 TreeSet 结婚的底层采用二叉树进行数据管理,当有新元素插入到 TreeSet 集合时,需要使用新元素与集合中已有的元素依次比较来确定新元素的合理位置
  • 比较元素大小的规则有两种方式:

使用元素的自然排序进行比较并排序,让元素类型实现 java.lang.Comparable 接口

​ 使用比较器规则进行比较并排序,构造 TreeSet 集合时传入 java.util.Comparator 接口

  • 自然排序的规则比较单一,而比较器的规则比较多元化,而且比较器优先于自然排序

元素类实现 Comparable 自然排序接口,如果重写 compareTo 方法返回 0

将此对象与指定的对象进行比较以获得顺序,返回负整数,零或正整数,因为此对象小于,等于或大于指定对象

然而这个 TreeSet 集合中将只添加一个元素默认为其余元素都是相同的则不添加

元素类

public class ShiliTreeSet implements Comparable<ShiliTreeSet>{
	private String name;
	private int age;
	
	public ShiliTreeSet() {
		super();
	}
	
	public ShiliTreeSet(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public int compareTo(ShiliTreeSet o) {
		// TODO Auto-generated method stub
		return 0;
	}
	@Override
	public String toString() {
		return "ShiliTreeSet [name=" + name + ", age=" + age + "]";
	}
	
}

测试类

public class TreeSetText {
	public static void main(String[]args){
		TreeSet<String> set1 = new TreeSet<>();
		set1.add("a");
		set1.add("b");
		set1.add("c");
		System.out.println("set1: "+set1);
		System.out.println("----------------------------------");
		TreeSet<ShiliTreeSet> set = new TreeSet<>();
		set.add(new ShiliTreeSet("张三",18));
		set.add(new ShiliTreeSet("李四",20));
		set.add(new ShiliTreeSet("刘桑",30));
		System.out.println("set: "+set);
	}
}

运行结果

image-20220914220536780

1 和 0 还有 - 1

public int compareTo(ShiliTreeSet o) {
		// TODO Auto-generated method stub
//		return 0;// 调用对象就是新增加的对象,添加的对象都为 0, 只添加一个其余都不添加
//		return -1;// 调用对象小于参数对象,新添加的对象往后放,旧元素在前面
		return 1;// 调用对象大于参数对象,
	}

不建议使用 1 和 0 还有 - 1 这样 无脑的方式来进行比较,建议 String 类型使用 compareTo 比较,基本数据类型使用 -

@Override
	public int compareTo(ShiliTreeSet o) {
		// TODO Auto-generated method stub
//		return 0;// 调用对象就是新增加的对象,添加的对象都为 0, 只添加一个其余都不添加
//		return -1;// 调用对象小于参数对象,新添加的对象往后放,旧元素在前面
//		return 1;// 调用对象大于参数对象,
//		return this.getName ().compareTo (o.getName ());// 比较姓名
		return this.getAge() - o.getAge();// 比较年龄
	}

将 compareTo 重写方法中添加条件比如如果姓名相同则使用它们两个的年龄来进行比较

@Override
	public int compareTo(ShiliTreeSet o) {
		// TODO Auto-generated method stub
//		return 0;// 调用对象就是新增加的对象,添加的对象都为 0, 只添加一个其余都不添加
//		return -1;// 调用对象小于参数对象,新添加的对象往后放,旧元素在前面
//		return 1;// 调用对象大于参数对象,
//		return this.getName ().compareTo (o.getName ());// 比较姓名
//		return this.getAge () - o.getAge ();// 比较年龄
		
		// 判断如果姓名相等那么就按照年龄进行排序
//		int i = this.getName().compareTo(o.getName());
//		if(i == 0){
//			return this.getAge() - o.getAge();
//		}
		// 简化 如果姓名相等按照年龄排序
		int i = this.getName().compareTo(o.getName());
		return i == 0 ? this.getAge() - o.getAge() : i;
	}

运行结果

image-20220915074458393

测试类

自然排序的规则比较单一,而比较器的规则比较多元化,而且比较器优先于自然排序

public class TreeSetText {
	public static void main(String[]args){
		// 匿名内部类:接口 / 父类类型 引用变量 = new 接口 / 父类类型 (){方法的重写};
		Comparator<ShiliTreeSet> comparator = new Comparator<ShiliTreeSet>(){
			@Override
			public int compare(ShiliTreeSet o1, ShiliTreeSet o2) {//o1 表示新增加的对象,o2 表示集合已有的对象
				// TODO Auto-generated method stub
				return o1.getAge() - o2.getAge();// 表示按照年龄比较
			}
		};
//		TreeSet<ShiliTreeSet> set = new TreeSet<>();
		TreeSet<ShiliTreeSet> set = new TreeSet<>(comparator);
		set.add(new ShiliTreeSet("b",21));
		set.add(new ShiliTreeSet("a",20));
		set.add(new ShiliTreeSet("d",30));
		set.add(new ShiliTreeSet("c",19));
		set.add(new ShiliTreeSet("c",15));
		System.out.println("set: "+set);
	}
}

运行结果

image-20220915165434406

一个自然排序和一个比较器,但是 TreeSet 优先于比较器的方式来进行排序

使用 Lambda 表达式的形式写书 Comparator 比较器

public class TreeSetText {
	public static void main(String[]args){
		// 匿名内部类:接口 / 父类类型 引用变量 = new 接口 / 父类类型 (){方法的重写};
//		Comparator<ShiliTreeSet> comparator = new Comparator<ShiliTreeSet>(){
//			@Override
//			public int compare (ShiliTreeSet o1, ShiliTreeSet o2) {//o1 表示新增加的对象,o2 表示集合已有的对象
//				// TODO Auto-generated method stub
//				return o1.getAge () - o2.getAge ();// 表示按照年龄比较
//			}
//		};
		// 从 java8 开始支持 Lambda 表达式书写格式 (参数列表)->{方法体}
		Comparator<ShiliTreeSet> com = ((o1,o2)->{
			return o1.getAge() - o2.getAge();
		});
//		TreeSet<ShiliTreeSet> set = new TreeSet<>();
//		TreeSet<ShiliTreeSet> set = new TreeSet<>(comparator);
		TreeSet<ShiliTreeSet> set = new TreeSet<>(com);
		set.add(new ShiliTreeSet("b",21));
		set.add(new ShiliTreeSet("a",20));
		set.add(new ShiliTreeSet("d",30));
		set.add(new ShiliTreeSet("c",19));
		set.add(new ShiliTreeSet("c",15));
		System.out.println("set: "+set);
	}
}

# 9Map 集合 (重点)

# 9.1 基本概念
  • java.util.Map<K,V> 集合中存取元素的基本单位是:单对元素,其中类型参数如下:

    K - 此映射所维护的键 (key) 的类型,相当与目录

    V - 映射值 (Value) 的类型,相当于内容

  • 该集合中 key 是不允许重复的,而且一个 key 只能对应一个 value

  • 该集合的主要实现类有:HashMap 类,TreeMap 类,LinkedHashMap 类,Hashtable 类,Prperties 类

  • 其中 HashMap 类的底层是采用哈希表进行数据管理的

  • 其中 TreeMap 类的底层是采用红黑树进行数据管理的

  • 其中 LinkedHashMap 类与 HashMap 类的不同之处在于内部维护了一个双向链表,链表中记录了元素的迭代顺序,也就是元素插入集合中的先后顺序,因此便于迭代

  • 其中 Hashtable 类是古老的 Map 实现类,与 HashMap 类相比属于线程安全的类 且不允许 null 作为 key 或者 value 的值

  • 其中 Properties 类是 Hashtable 类的子类,该对象用于处理属性文件,key 和 value 都是 String 类型的

  • Map 集合是面向查询优化的数据结构,在大数据量情况下有着优良的查询性能

  • 经常用于根据 key 检索 value 的业务场景

Set 调用 add 的底层源码

public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }

可以看到调用 add 方法时传入了 e 数据和一个 PRESENT 这是什么呢

// Dummy value to associate with an Object in the backing Map
	// 与支持 Map 中的对象关联的虚拟值
    private static final Object PRESENT = new Object();

这个东西创建了一个 Object 对象

image-20220915175506350

实际上是在 map 上添加了一个元素

# 9.2 常用的方法
方法声明功能介绍
V put(k key,V value)将 key-value 对存入 Map, 若集合中已经包含该 key, 则替换该 key 所对应的 value 返回值为该 key 原来所对应的 value, 若没有则返回 null
V get(Object key)返回与参数 key 所对应的 value 对象,如果不存在则返回 null
boolean containsKey(Object key)判断集合中是否包含指定的 key
boolean containsValue(Object value)判断集合中是否包含指定的 value
V remove(Object key)根据参数指定的 key 进行删除
Set<K> KeySet()返回此映射中包含的键的 Set 视图
Collection<V> values()返回此映射中包含的值的 Set 视图
Set<Map.Entry<K,V>> entrySet()返回此映射中包含的映射的 Set 视图
# 9.3 元素放入 HashMap 集合的原理
  • 使用元素的 key 调用 hashCode 方法获取对应的哈希码值,再由某种哈希码算法计算在数组中的索引位置
  • 若该位置没有元素,则将该键值对直接放入即可
  • 若该位置有元素,则使用 key 与已有元素依次比较哈希值,若哈希值不相同,则将该元素直接放入
  • 若 key 与已有元素的哈希值相同,则使用 key 调用 equals 方法与已有元素依次比较
  • 若相等则将对应的 value 修改,否则将键值对直接放入即可
# 9.4 相关的常量
  • DEFULT_INITIAL_CAPACITY:HashMap 的默认容量是 16

  • DEFAULT_CLOAD_FACTOR:HashMap 的默认加载因子是 0.75

  • threshold: 扩容的临界值,该数值为:容量 * 填充因子,也就是 12

  • TREEIFY_THRESHOLD: 若 Buket 中链表长度大于该默认值则转换为红黑树存储,该数值是 8

  • MIN_TREEIY_CAPACITY : 桶中的 Node 被树化时最小的 hash 表容量,该数值是 64

  • 案例题目:

​ 准备一个 HashMap 集合,统计字符串 "123","456","789","123","456" 中每个数字字符串出现的次数并打印出来如:

​ 123 出现了 2 次

​ 456 出现了 2 次

​ 789 出现了 1 次

# 10 Collections 类

10.1 基本概念

  • java.util.Collections 类主要提供了对集合操作或者返回集合的静态方法

10.2 常用的方法

方法声明功能介绍
static <T extends Object & Comparable<? super T>>Tmax(Collection<? extends T>coll)根据元素的自然顺序返回给定的集合的最大元素
static <T> T max(Collection<? extends T>coll,Comparator<? superT>comp)根据指定比较器引发的顺序返回给定集合的最大元素
static <? extends Object & Comparable<?superT>>T min(Collection<?extends T>coll)根据元素的自然顺序返回给定集合的最小元素
static<T>T min(Collection<?extends T>coll,Comparator<? super T>comp)根据指定比较器引发的顺序返回给定集合的最小元素
static<T> void copy(List<? super T>dest,List<? extends T>src)将一个列表中的所有元素复制到另一个列表中
方法声明功能介绍
--
static void reverse(List<?>list)反转指定列表中元素的顺序
static void shuffle(List<?>list)使用默认的随机源随机置换指定的列表
static <T extends Comparable<? super T>>void sort(List<T>list)根据其元素的自然排序将指定列表安升序排序
static <T>void sort(List<T>list,Comparator<? super T>c)根据指定比较器指定的顺序对指定列表进行排序
static void swap(List<?>list,int i,int j)交换指定列表中指定位置
List<Integer> list = Arrays.asList(10,15,28,30,50);
		System.out.println("获取集合中最大的元素: "+Collections.max(list));//[50]
		System.out.println("获取集合中最小的元素: "+Collections.min(list));//[10]
		Collections.reverse(list);
		System.out.println("反转后的集合: "+list);//[50, 30, 28 ,15, 10]
		Collections.swap(list, 0, 4);
		System.out.println("交换位置后的集合: "+list);//[10, 30, 28, 15, 50]
		Collections.sort(list);
		System.out.println("排序后的集合: "+list);//[10, 15, 28, 30, 50]
		Collections.shuffle(list);
		System.out.println("随机打乱后的集合: "+list);//[随机]
		List<Integer> list1 = Arrays.asList(new Integer[5]);
		Collections.copy(list1,list);// 前面随机放置元素索引这里拷贝元素的位置不确定
		System.out.println("将list集合元素拷贝到list1集合中: "+list1);//[?, ?, ?, ?, ?]