第七章 集合

集合概述

主要掌握LinkedList,ArrayList,Vector,TreeSet,HashSet,LinkedHashSet

Collection接口

Collection接口的通用方法

boolean add(E e);			向集合中添加元素
int size();					获取集合中元素个数
boolean addAll(Collection c);	将参数集合中所有元素全部加入当前集合
boolean contains(Object o);	判断集合中是否包含对象o
boolean remove(Object o);		从集合中删除对象o
void clear();				清空集合
boolean isEmpty();			判断集合中元素个数是否为0
Object[] toArray();			将集合转换成一维数组

Collection的遍历

第一步:获取当前集合依赖的迭代器对象
Iterator it = collection.iterator();
第二步:编写循环,循环条件是:当前光标指向的位置是否存在元素。
while(it.hasNext()){}
第三步:如果有,将光标指向的当前元素返回,并且将光标向下移动一位。
Object obj = it.next();

SequencedCollection接口

所有的有序集合都实现了SequencedCollection接口

SequencedCollection接口是Java21版本新增的。
SequencedCollection接口中的方法:
void addFirst(Object o):向头部添加
void addLast(Object o):向末尾添加
Object removeFirst():删除头部
Object removeLast():删除末尾
Object getFirst():获取头部节点
Object getLast():获取末尾节点
SequencedCollection reversed(); 反转集合中的元素
ArrayList,LinkedList,Vector,LinkedHashSet,TreeSet,Stack 都可以调用这个接口中的方法。

泛型

泛型是Java5的新特性,属于编译阶段的功能。
泛型可以让开发者在编写代码时指定集合中存储的数据类型
泛型作用:
类型安全:指定了集合中元素的类型之后,编译器会在编译时进行类型检查,如果尝试将错误类型的元素添加到集合中,就会在编译时报错,避免了在运行时出现类型错误的问题。
代码简洁:使用泛型可以简化代码,避免了繁琐的类型转换操作。比如,在没有泛型的时候,需要使用 Object 类型来保存集合中的元素,并在使用时强制类型转换成实际类型,而有了泛型之后,只需要在定义集合时指定类型即可。
在集合中使用泛型
Collection<String> strs = new ArrayList<String>();
这就表示该集合只能存储字符串,存储其它类型时编译器报错。
并且以上代码使用泛型后,避免了繁琐的类型转换,集合中的元素可以直接调用String类特有的方法。
Java7的新特性:钻石表达式
Collection<String> strs = new ArrayList<>();
泛型的使用:在静态方法上定义泛型
在类上定义的泛型,在静态方法中无法使用。如果在静态方法中使用泛型,则需要再方法返回值类型前面进行泛型的声明。
语法格式:<泛型1, 泛型2, 泛型3, ...> 返回值类型 方法名(形参列表) {}

泛型的使用:在接口上定义泛型
语法格式:interface 接口名<泛型1,泛型2,...> {}
例如:public interface Flayable<T>{}
实现接口时,如果知道具体的类型,则:public class MyClass implements Flyable<Bird>{}
实现接口时,如果不知道具体的类型,则:public class MyClass<T> implements Flyable<T>{}
/*public void shopping(T type){

    }*/

/**
     * 在静态方法上使用泛型之前,类型必须是提前定义好之后才能用。
     * 在静态方法中定义泛型需要在返回值类型前面定义/声明。
     *
     */
public static <T> void shopping(T type){

}

public static <E> void print(E[] elts){
    for (E elt : elts){
        System.out.println(elt);
    }
}

public static void main(String[] args) {
    /*Customer<String> c = new Customer<String>();
        c.shopping("abc");*/

    //Customer.shopping();

    String[] strs = {"zhangsan","lisi"};
    Customer.print(strs);
}

泛型通配符

泛型是在限定数据类型,当在集合或者其他地方使用到泛型后,那么这时一旦明确泛型的数据类型,那么在使用的时候只能给其传递和数据类型匹配的类型,否则就会报错。
有的情况下,我们在定义方法时,根本无法确定集合中存储元素的类型是什么。为了解决这个“无法确定集合中存储元素类型”问题,那么Java语言就提供了泛型的通配符。
通配符的几种形式:
1. 无限定通配符,<?>,此处“?”可以为任意引用数据类型。
2. 上限通配符,<? extends Number>,此处“?”必须为Number及其子类。
3. 下限通配符,<? super Number>,此处“?”必须为Number及其父类。
/**
 * 注意,以下讲解内容是泛型通配符。这个是站在使用泛型的角度来说的。不属于泛型定义的相关内容。
 * 别人把泛型定义好了,我来使用。使用的时候可以使用泛型通配符。
 *
 * 1. 无限定通配符,<?>,此处“?”可以为任意引用数据类型。
 * 2. 上限(界)通配符,<? extends Number>,此处“?”必须为Number及其子类。
 * 3. 下限(界)通配符,<? super Number>,此处“?”必须为Number及其父类。
 */
public class GenericTest03 {

    public static void print(ArrayList<?> list){}

    public static void print2(ArrayList<? extends Number> list){}

    public static void print3(ArrayList<? super String> list){}

    public static void print4(ArrayList<? super B> list){}

    public static void main(String[] args) {
        print(new ArrayList<String>());
        print(new ArrayList<Object>());
        print(new ArrayList<Integer>());
        print(new ArrayList<User>());

        print2(new ArrayList<Integer>());
        print2(new ArrayList<Double>());
        print2(new ArrayList<Byte>());

        // 编译报错
        //print2(new ArrayList<String>());

        print3(new ArrayList<Object>());
        print3(new ArrayList<String>());
        print3(new ArrayList<String>());

        // 编译报错
        //print3(new ArrayList<A>());

        print4(new ArrayList<B>());
        print4(new ArrayList<A>());
        // 报错
        //print4(new ArrayList<C>());

    }
}


class A {

}

class B extends A {

}

class C extends B {

}

迭代时删除元素

迭代集合时删除元素
使用“集合对象.remove(元素)”:会出现ConcurrentModificationException异常。
使用“迭代器对象.remove()”:不会出现异常。
关于集合的并发修改问题
想象一下,有两个线程:A和B。A线程负责迭代遍历集合,B线程负责删除集合中的某个元素。当这两个线程同时执行时会有什么问题?
如何解决并发修改问题:fail-fast机制
fail-fast机制又被称为:快速失败机制。也就是说只要程序发现了程序对集合进行了并发修改。就会立即让其失败,以防出现错误。
fail-fast机制是如何实现的?以下是源码中的实现原理:
集合中设置了一个modCount属性,用来记录修改次数,使用集合对象执行增,删,改中任意一个操作时,modCount就会自动加1。
获取迭代器对象的时候,会给迭代器对象初始化一个expectedModCount属性。并且将expectedModCount初始化为modCount,即:int expectedModCount = modCount;
当使用集合对象删除元素时:modCount会加1。但是迭代器中的expectedModCount不会加1。而当迭代器对象的next()方法执行时,会检测expectedModCount和modCount是否相等,如果不相等,则抛出:ConcurrentModificationException异常。
当使用迭代器删除元素的时候:modCount会加1,并且expectedModCount也会加1。这样当迭代器对象的next()方法执行时,检测到的expectedModCount和modCount相等,则不会出现ConcurrentModificationException异常。
注意:虽然我们当前写的程序是单线程的程序,并没有使用多线程,但是通过迭代器去遍历的同时使用集合去删除元素,这个行为将被认定为并发修改。
结论:迭代集合时,删除元素要使用“迭代器对象.remove()”方法来删除,避免使用“集合对象.remove(元素)”。主要是为了避免ConcurrentModificationException异常的发生。注意:迭代器的remove()方法删除的是next()方法的返回的那个数据。remove()方法调用之前一定是先调用了next()方法,如果不是这样的,就会报错。

List接口

特有方法

List接口特有方法:(在Collection和SequencedCollection中没有的方法,只适合List家族使用的方法,这些方法都和下标有关系。)
void add​(int index, E element) 在指定索引处插入元素
E set​(int index, E element); 修改索引处的元素
E get​(int index); 根据索引获取元素(通过这个方法List集合具有自己特殊的遍历方式:根据下标遍历)
E remove​(int index); 删除索引处的元素
int indexOf​(Object o); 获取对象o在当前集合中第一次出现时的索引。
int lastIndexOf​(Object o); 获取对象o在当前集合中最后一次出现时的索引。
List<E> subList​(int fromIndex, int toIndex); 截取子List集合生成一个新集合(对原集合无影响)。[fromIndex, toIndex)
static List<E> of​(E... elements); 静态方法,返回包含任意数量元素的不可修改列表。(获取的集合是只读的,不可修改的。)
// 获取一个不可修改的集合,只读的集合。
List<Integer> nums = List.of(1, 2, 3, 43, 45, 5, 6, 76, 7);

// 尝试修改(出现异常,该集合是不可修改的,只读的。)
//nums.set(0, 110); // java.lang.UnsupportedOperationException

List接口特有迭代

特有的迭代方式
ListIterator<E> listIterator(); 获取List集合特有的迭代器(该迭代器功能更加强大,但只适合于List集合使用)
ListIterator<E> listIterator(int index); 从列表中的指定位置开始,返回列表中元素的列表迭代器
ListIterator接口中的常用方法:
boolean hasNext(); 		判断光标当前指向的位置是否存在元素。
E next();				将当前光标指向的元素返回,然后将光标向下移动一位。
void remove();			删除上一次next()方法返回的那个数据(删除的是集合中的)。remove()方法调用的前提是:你先调用next()方法。不然会报错。
void add​(E e);			添加元素(将元素添加到光标指向的位置,然后光标向下移动一位。)
boolean hasPrevious();	判断当前光标指向位置的上一个位置是否存在元素。
E previous();			获取上一个元素(将光标向上移动一位,然后将光标指向的元素返回)
int nextIndex();		获取光标指向的那个位置的下标
int previousIndex();	获取光标指向的那个位置的上一个位置的下标
void set​(E e);			修改的是上一次next()方法返回的那个数据(修改的是集合中的)。set()方法调用的前提是:你先调用了next()方法。不然会报错。
package com.powernode.javase.collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

/**
 * java.util.ListIterator:List接口专用的迭代器。Set集合不能使用。
 *      boolean hasNext();     判断光标当前指向的位置是否存在元素。
 *      E next();            将当前光标指向的元素返回,然后将光标向下移动一位。
 *      void remove();        删除上一次next()方法返回的那个数据(删除的是集合中的)。remove()方法调用的前提是:你先调用next()方法。不然会报错。
 *
 *      void add(E e);        添加元素(将元素添加到光标指向的位置,然后光标向下移动一位。)
 *      boolean hasPrevious();  判断当前光标指向位置的上一个位置是否存在元素。
 *      E previous();         获取上一个元素(将光标向上移动一位,然后将光标指向的元素返回)
 *      int nextIndex();       获取光标指向的那个位置的下标
 *      int previousIndex();    获取光标指向的那个位置的上一个位置的下标
 *      void set(E e);        修改的是上一次next()方法返回的那个数据(修改的是集合中的)。set()方法调用的前提是:你先调用了next()方法。不然会报错。
 */
public class ListIteratorTest {
    public static void main(String[] args) {
        // 创建集合List
        List<String> names = new ArrayList<>();
        // 添加元素
        names.add("zhangsan");
        names.add("lisi");
        names.add("wangwu");
        names.add("zhaoliu");
        // 使用普通的通用迭代器遍历
        /*Iterator<String> it = names.iterator();
        while(it.hasNext()){
            String name = it.next();
            System.out.println(name);
        }*/
        // 使用ListIterator进行遍历
        // 测试的add方法
        /*ListIterator<String> li = names.listIterator();
        while (li.hasNext()) {
            String name = li.next();
            if("lisi".equals(name)){
                li.add("李四");
            }
            System.out.println(name);
        }*/

        // 测试hasPrevious()方法
        // 判断是否有上一个
        // 判断当前光标指向位置的上一个位置是否存在元素。
        /*ListIterator<String> li = names.listIterator();
        System.out.println("光标当前指向的位置的上一个位置是否有元素:" + li.hasPrevious());

        while (li.hasNext()) {
            String name = li.next();
            System.out.println(name);
        }

        System.out.println("光标当前指向的位置的上一个位置是否有元素:" + li.hasPrevious());*/

        // E previous(); 获取上一个元素(将光标向上移动一位,然后将光标指向的元素返回)
        /*ListIterator<String> li = names.listIterator();
        while (li.hasNext()) {
            String name = li.next();
            System.out.println(name);
        }

        System.out.println("========================");

        System.out.println(li.previous()); // zhaoliu
        System.out.println(li.previous()); // wangwu
        System.out.println(li.previous()); // lisi
        System.out.println(li.previous()); // zhangsan
        //System.out.println(li.previous()); // java.util.NoSuchElementException*/

        //int nextIndex(); 获取光标指向的那个位置的下标
        /*ListIterator<String> li = names.listIterator();
        while (li.hasNext()) {
            String name = li.next();
            if("lisi".equals(name)){ // 当前取出的元素是"lisi"
                System.out.println(li.nextIndex()); //2
                // int previousIndex(); 获取光标指向的那个位置的上一个位置的下标
                System.out.println(li.previousIndex()); // 1
            }
            System.out.println(name);
        }*/

        // void set(E e);修改的是上一次next()方法返回的那个数据(修改的是集合中的)。set()方法调用的前提是:你先调用了next()方法。不然会报错。
        ListIterator<String> li = names.listIterator();

        // set方法不能随意使用,set调用的前提是:之前调用了next()或者previous()
        // li.set("xxxxxxxxxxxxxx"); // java.lang.IllegalStateException

        /*while (li.hasNext()) {
            String name = li.next();
            // 在这里时可以调用set方法
            if("lisi".equals(name)) {
                li.set("李四");
            }
            System.out.println(name);
        }

        System.out.println(names);*/


        // remove方法:通过迭代器去删除
        //li.remove(); // java.lang.IllegalStateException(调用迭代器的remove方法之前也是需要调用了next()/previous()方法。)
        //删除上一次next()方法返回的那个数据(删除的是集合中的)。remove()方法调用的前提是:你先调用next()方法。不然会报错。
        while (li.hasNext()) {
            String name = li.next();
            if("lisi".equals(name)){
                // 删除
                li.remove();
            }
        }

        System.out.println(names);

    }
}

List接口使用Comparator排序

回顾数组中自定义类型是如何排序的?
所有自定义类型排序时必须指定排序规则。(int不需要指定,String不需要指定,因为他们都有固定的排序规则。int按照数字大小。String按照字典中的顺序)
如何给自定义类型指定排序规则?让自定义类型实现java.lang.Comparable接口,然后重写compareTo方法,在该方法中指定比较规则。
List集合的排序
default void sort​(Comparator<? super E> c);  对List集合中元素排序可以调用此方法。
sort方法需要一个参数: java.util.Comparator。我们把这个参数叫做比较器。这是一个接口。
如何给自定义类型指定比较规则?可以对Comparator提供一个实现类,并重写compare方法来指定比较规则。
当然,Comparator接口的实现类也可以采用匿名内部类的方式。

ArrayList

ArrayList集合底层采用了数组这种数据结构。
ArrayList集合优点:
底层是数组,因此根据下标查找元素的时间复杂度是O(1)。因此检索效率高。
ArrayList集合缺点:
随机增删元素效率较低。不过只要数组的容量还没满,对末尾元素进行增删,效率不受影响。
ArrayList集合适用场景:
需要频繁的检索元素,并且很少的进行随机增删元素时建议使用。
ArrayList默认初始化容量?
从源码角度可以看到,当调用无参数构造方法时,初始化容量0,当第一次调用add方法时将ArrayList容量初始化为10个长度。
ArrayList集合扩容策略?
底层扩容会创建一个新的数组,然后使用数组拷贝。扩容之后的新容量是原容量的1.5倍。
ArrayList集合源码分析:
属性分析
构造方法分析(使用ArrayList集合时最好也是预测大概数量,给定初始化容量,减少扩容次数。)
添加元素
修改元素
插入元素
删除元素

手写单向链表

public class MyLinked<E> {
    /**
     * 元素个数
     */
    private int size;
    /**
     * 单向链表的头结点
     */
    private Node<E> first;

    /**
     * 构建一个空链表
     */
    public MyLinked() {

    }


    /**
     * 获取集合中元素的个数
     * @return 个数
     */
    public int size(){
        return size;
    }

    /**
     * 向单向链表的末尾添加一个元素。
     * @param data 数据
     */
    public void add(E data) {
        // 如果first是空,表示是一个空链表
        if(first == null){
            first = new Node<>(data, null);
            size++;
            return;
        }
        // 找到末尾结点
        Node<E> last = findLast();
        last.next = new Node<>(data, null);
        size++;
    }

    /**
     * 找到单向链表的末尾结点
     * @return 末尾结点
     */
    private Node<E> findLast() {
        if(first == null){
            // 空链表
            return null;
        }
        // 程序指定到这里,first肯定不是null,不是一个空链表
        // 假设第一个结点就是最后一个结点。
        Node<E> last = first;
        while(last.next != null){
            // 把last.next看做是最后一个结点
            last = last.next;
        }
        return last;
    }

    /**
     * 将元素添加到指定索引处
     * @param index 下标
     * @param data 数据
     */
    public void add(int index, E data) {
        // 这里按说应该进行下标的检查,防止越界。
        // 创建新的结点对象
        Node<E> newNode = new Node<>(data, null);
        // 后期再写node(int)方法,这个方法可以根据下标找到对应的结点对象
        Node<E> prev = node(index - 1);
        newNode.next = prev.next;
        prev.next = newNode;
        size++;
    }

    /**
     * 返回索引处的结点对象
     * @param index 索引
     * @return 结点对象
     */
    private Node<E> node(int index) {
        // 假设头结点是下一个结点
        Node<E> next = first;
        for (int i = 0; i < index; i++) {
            next = next.next;
        }
        return next;
    }

    /**
     * 删除指定索引处的元素
     * @param index 索引
     */
    public void remove(int index) {
        // 检查index是否越界
        // 假如删除的结点是头结点
        if(index == 0){
            Node<E> oldFirst = first;
            first = first.next;
            oldFirst.next = null;
            return;
        }
        // 删除的不是头结点
        // 被删除的结点的上一个结点
        Node<E> prev = node(index - 1);
        // 获取被删除的那个结点
        Node<E> removed = node(index);
        prev.next = removed.next;
        removed.next = null;
        removed.item = null;
        size--;
    }

    /**
     * 修改指定索引处的数据
     * @param index 索引
     * @param data 数据
     */
    public void set(int index, E data) {
        Node<E> node = node(index);
        node.item = data;
    }

    /**
     * 根据下标获取数据
     * @param index 下标
     * @return 数据
     */
    public E get(int index) {
        return node(index).item;
    }

    /**
     * 单向链表当中的结点(建议定义为静态内部类。)
     */
    private static class Node<E> {
        /**
         * 数据
         */
        E item;
        /**
         * 下一个结点的内存地址
         */
        Node<E> next;

        /**
         * 构造一个结点对象
         * @param item 结点中的数据
         * @param next 下一个结点的内存地址。
         */
        public Node(E item, Node<E> next) {
            this.item = item;
            this.next = next;
        }
    }
}

栈数据结构

LIFO原则(Last In,First Out):后进先出
实现栈数据结构,可以用数组来实现,也可以用双向链表来实现。
用数组实现的代表是:Stack、ArrayDeque
Stack:Vetor的子类,实现了栈数据结构,除了具有Vetor的方法,还扩展了其它方法,完成了栈结构的模拟。不过在JDK1.6(Java6)之后就不建议使用了,因为它是线程安全的,太慢了。Stack中的方法如下:
E push(E item):压栈
E pop():弹栈(将栈顶元素删除,并返回被删除的引用)
int search(Object o):查找栈中元素(返回值的意思是:以1为开始,从栈顶往下数第几个)
E peek():窥视栈顶元素(不会将栈顶元素删除,只是看看栈顶元素是什么。注意:如果栈为空时会报异常。)
ArrayDeque
E push(E item)
E pop()
用链表实现的代表是:LinkedList
LinkedList
E push(E item)
E pop()
/**
 * java.util.Stack:底层是数组,线程安全的,JDK1.6不建议用了。
 * java.util.ArrayDeque:底层是数组(实现LIFO的同时,又实现了双端队列。)
 * java.util.LinkedList:底层是双向链表(实现LIFO的同时,又实现了双端队列。)
 * 以上三个类都实现了栈数据结构。都实现了LIFO。
 */
public class StackTest {
    public static void main(String[] args) {
        // 创建栈
        Stack<String> stack1 = new Stack<>();
        LinkedList<String> stack2 = new LinkedList<>();
        ArrayDeque<String> stack3 = new ArrayDeque<>();

        // 压栈
        stack1.push("1");
        stack1.push("2");
        stack1.push("3");
        stack1.push("4");

        // seach方法
        System.out.println("位置:" + stack1.search("1"));

        // 弹栈
        System.out.println(stack1.pop());
        System.out.println(stack1.pop());
        System.out.println(stack1.pop());
        // 窥视
        System.out.println("此时栈顶元素:" + stack1.peek());
        //System.out.println(stack1.pop());
        //java.util.EmptyStackException
        //System.out.println(stack1.pop());

        System.out.println("====================");

        // 压栈
        stack2.push("1");
        stack2.push("2");
        stack2.push("3");
        stack2.push("4");
        // 弹栈
        System.out.println(stack2.pop());
        System.out.println(stack2.pop());
        System.out.println(stack2.pop());
        System.out.println(stack2.pop());

        System.out.println("====================");

        // 压栈
        stack3.push("1");
        stack3.push("2");
        stack3.push("3");
        stack3.push("4");
        // 弹栈
        System.out.println(stack3.pop());
        System.out.println(stack3.pop());
        System.out.println(stack3.pop());
        System.out.println(stack3.pop());
    }
}

队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,队列是一种操作受限制的线性表。进行插入操作(入口)的端称为队尾,进行删除操作(出口)的端称为队头。
队列的插入操作只能在队尾操作,队列的删除操作只能在队头操作,因此队列是一种先进先出(First In First Out)的线性表,简称FIFO表。
Queue接口是一种基于FIFO(先进先出)的数据结构,而Deque接口则同时支持FIFO和LIFO(后进先出)两种操作。因此Deque接口也被称为“双端队列”。
Java集合框架中队列的实现:
链表实现方式:LinkedList
数组实现方式:ArrayDeque
LinkedList和ArrayDeque都实现了Queue、Deque接口,因此这两个类都具备队列和双端队列的特性。
LinkedList底层是基于双向链表实现的,因此它天然就是一个双端队列,既支持从队尾入队,从队头出队,也支持从队头入队,从队尾出队。用Deque的实现方式来说,就是它既实现了队列的offer()和poll()方法,也实现了双端队列的offerFirst()、offerLast()、pollFirst()和pollLast()方法等。
ArrayDeque底层是使用环形数组实现的,也是一个双端队列。它比LinkedList更加高效,因为在数组中随机访问元素的时间复杂度是O(1),而链表中需要从头或尾部遍历链表寻找元素,时间复杂度是O(N)。循环数组:index = (start + i) % capacity
Queue接口基于Collection扩展的方法包括:
boolean offer(E e); 入队。
E poll(); 出队,如果队列为空,返回null。
E remove(); 出队,如果队列为空,抛异常。
E peek();	查看队头元素,如果为空则返回null。
E element(); 查看对头元素,如果为空则抛异常。
Deque接口基于Queen接口扩展的方法包括:
以下2个方法可模拟队列:
boolean offerLast(E e); 	从队尾入队
E pollFirst(); 			从队头出队
以下4个方法可模拟双端队列:
boolean offerLast(E e); 	从队尾入队
E pollFirst(); 			从队头出队	
boolean offerFirst(E e); 	从队头入队
E pollLast(); 			从队尾出队
另外offerLast+pollLast或者pollFirst+offerFirst可以模拟栈数据结构。或者也可以直接调用push/pop方法。
// 队尾进,队头出
// 创建双端队列对象
//Deque<String> deque1 = new ArrayDeque<>();
Deque<String> deque1 = new LinkedList<>();
deque1.offerLast("1");
deque1.offerLast("2");
deque1.offerLast("3");
deque1.offerLast("4");

System.out.println(deque1.pollFirst());
System.out.println(deque1.pollFirst());
System.out.println(deque1.pollFirst());
System.out.println(deque1.pollFirst());

// 队头进,队尾出
deque1.offerFirst("a");
deque1.offerFirst("b");
deque1.offerFirst("c");
deque1.offerFirst("d");

System.out.println(deque1.pollLast());
System.out.println(deque1.pollLast());
System.out.println(deque1.pollLast());
System.out.println(deque1.pollLast());

Map

Map继承结构

Map集合以key和value的键值对形式存储。key和value存储的都是引用。
Map集合中key起主导作用。value是附属在key上的。
SequencedMap是Java21新增的。
LinkedHashMap和TreeMap都是有序集合。(key是有序的)
HashMap,Hashtable,Properties都是无序集合。(key是无序的)
Map集合的key都是不可重复的。key重复的话,value会覆盖。
HashSet集合底层是new了一个HashMap。往HashSet集合中存储元素实际上是将元素存储到HashMap集合的key部分。HashMap集合的key是无序不可重复的,因此HashSet集合就是无序不可重复的。HashMap集合底层是哈希表/散列表数据结构,因此HashSet底层也是哈希表/散列表。
TreeSet集合底层是new了一个TreeMap。往TreeSet集合中存储元素实际上是将元素存储到TreeMap集合的key部分。TreeMap集合的key是不可重复但可排序的,因此TreeSet集合就是不可重复但可排序的。TreeMap集合底层是红黑树,因此TreeSet底层也是红黑树。它们的排序通过java.lang.Comparable和java.util.Comparator均可实现。
LinkedHashSet集合底层是new了一个LinkedHashMap。LinkedHashMap集合只是为了保证元素的插入顺序,效率比HashSet低,底层采用的哈希表+双向链表实现。
根据源码可以看到向Set集合中add时,底层会向Map中put。value只是一个固定不变的常量,只是起到一个占位符的作用。主要是key。

Map接口的常用方法

V put(K key, V value);				添加键值对
void putAll(Map<? extends K,? extends V> m);	添加多个键值对
V get(Object key);					通过key获取value
boolean containsKey(Object key);			是否包含某个key
boolean containsValue(Object value);		是否包含某个value
V remove(Object key);					通过key删除key-value
void clear();						清空Map
int size();							键值对个数
boolean isEmpty();					判断是否为空Map
Collection<V> values();				获取所有的value
Set<K> keySet();						获取所有的key
Set<Map.Entry<K,V>> entrySet();			获取所有键值对的Set视图。
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3);	静态方法,使用现有的key-value构造Map
// 创建Map集合
Map<Integer,String> maps = new HashMap<>();

// 存放元素
maps.put(1, "张三");
maps.put(2, "李四");
maps.put(3, "王五");
maps.put(4, "赵六");

// 遍历Map集合(第一种方式)
// 思路:获取Map集合的所有key,然后遍历每个key,通过key获取value。
/*Set<Integer> keys = maps.keySet();
Iterator<Integer> it = keys.iterator();
while (it.hasNext()) {
    Integer key = it.next();
    String value = maps.get(key);
    System.out.println(key + "=" + value);
}*/

// for-each
/*Set<Integer> keys = maps.keySet();
for(Integer key : keys){
    System.out.println(key + "=" + maps.get(key));
}*/

// 遍历Map集合(第二种方式)
// 这种方式效率较高,建议使用。
/*Set<Map.Entry<Integer, String>> entries = maps.entrySet();
Iterator<Map.Entry<Integer, String>> it = entries.iterator();
while (it.hasNext()) {
    Map.Entry<Integer, String> entry = it.next();
    Integer key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + "=" + value);
}*/

// for-each
for(Map.Entry<Integer, String> entry : maps.entrySet()){
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

HashMap

HashMap集合的key是无序不可重复的。
无序:插入顺序和取出顺序不一定相同。
不可重复:key具有唯一性。
向HashMap集合中put时,key如果重复的话,value会覆盖。
HashMap集合的key具有唯一性,向key部分插入自定义的类型会怎样?如果自定义的类型重写equals之后会怎样???
HashMap底层的数据结构是:哈希表/散列表
哈希表是一种查询和增删效率都很高的一种数据结构,非常重要,在很多场合使用,并且面试也很常见。必须掌握。
哈希表如何做到的查询和增删效率都好的呢,因为哈希表是“数组 + 链表”的结合体。数组和链表的结合不是绝对的。
哈希表可能是:数组 + 链表,数组 + 红黑树, 数组 + 链表 + 红黑树等。
HashMap集合底层部分源码:

哈希表存储原理

概念:
哈希表:一种数据结构的名字。
哈希函数:
通过哈希函数可以将一个Java对象映射为一个数字。(就像现实世界中,每个人(对象)都会映射一个身份证号(哈希值)一样。)
也就是说通过哈希函数的执行可以得到一个哈希值。
在Java中,hashCode()方法就是哈希函数。
也就是说hashCode()方法的返回值就是哈希值。
一个好的哈希函数,可以让散列分布均匀。
哈希值:也叫做哈希码。是哈希函数执行的结果。
哈希碰撞:也叫做哈希冲突。
当两个对象“哈希值%数组长度”之后得到的下标相同时,就发生了哈希冲突。
如何解决哈希冲突?将冲突的挂到同一个链表上或同一个红黑树上。
以上描述凡是“哈希”都可以换为“散列”。
重点:
存放在HashMap集合key部分的元素必须同时重写hashCode+equals方法。
equals返回true时,hashCode必须相同。

HashMap可以存null

最开始使用单向链表解决哈希冲突。如果结点数量 >= 8,并且table的长度 >= 64。单向链表转换为红黑树。当删除红黑树上的结点时,结点数量 <= 6 时。红黑树转换为单向链表。

LinkedHashMap

LinkedHashMap集合和HashMap集合的用法完全相同。
不过LinkedHashMap可以保证插入顺序。
LinkedHashMap集合因为可以保证插入顺序,因此效率比HashMap低一些。
LinkedHashMap是如何保证插入顺序的?底层采用了双向链表来记录顺序。
LinkedHashMap集合底层采用的数据结构是:哈希表 + 双向链表。
LinkedHashMap集合的key是:有序不可重复。key部分也需要同时重写hashCode + equals。
key的取值可以为null,key如果相同,value也是覆盖。
/**
 * 1.LinkedHashMap是HashMap集合的子类
 * 2.LinkedHashMap几乎和HashMap集合的用法一样。
 * 3.只不过LinkedHashMap集合可以保证元素的插入顺序。(有序的)
 * 4.LinkedHashMap集合是:有序不可重复。
 * 5.LinkedHashMap集合的key也需要同时重写hashCode + equals。
 * 6.LinkedHashMap集合底层的数据结构是:哈希表 + 双向链表。
 */
public class LinkedHashMapTest01 {
    public static void main(String[] args) {
        // 创建一个有序不可重复的Map集合。
        Map<Integer, String> map = new LinkedHashMap<>();

        // 有序:插入顺序和取出顺序一致。
        map.put(100, "张三1");
        map.put(101, "张三2");
        map.put(5, "张三3");
        map.put(3000, "张三4");
        map.put(88, "张三5");
        map.put(66, "张三6");
        map.put(66, "张三X");
        map.put(null, null);

        // 遍历
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for(Map.Entry<Integer, String> entry : entries){
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
    }
}

Hashtable

Hashtable和HashMap一样,底层也是哈希表。
Hashtable是线程安全的,方法上都有synchronized关键字。使用较少,因为保证线程安全有其他方式。
Hashtable的初始化容量:11。默认加载因子:0.75
Hashtable的扩容策略:2倍。
Hashtable中有一些传统方法,这些方法不属于集合框架:
Enumeration keys();		获取所有key的迭代器
Enumeration elements();	获取所有value的迭代器
Enumeration的相关方法
boolean hasMoreElements();	是否含有元素
E nextElement(); 获取元素
Hashtable和HashMap集合的区别:
HashMap集合线程不安全,效率高,key和value允许null。
Hashtable集合线程安全,效率低,key和value不允许null。

Properties

Properties被称为属性类。通常和xxx.properties属性文件一起使用。
Properties的父类是Hashtable。因此Properties也是线程安全的。
Properties不支持泛型,key和value只能是String类型。
Properties相关方法:
Object setProperty(String key, String value);		和put方法一样。
String getProperty(String key);				通过key获取value
Set<String> propertyNames();		
/**
 * 1.这里先作为了解。因为后面再IO流当中还是需要使用Properties的,到那个时候就理解了。
 * 2.java.util.Properties,我们一般叫做:属性类。
 * 3.Properties继承Hashtable,所以Properties也是线程安全的。Properties也是一个Map集合。
 * 4.Properties属性类一般和java程序中的属性配置文件联合使用,属性配置文件的扩展名是:xxxxxxx.properties
 * 5.Properties类不支持泛型。key和value是固定类型,都是String类型。
 * 6.目前需要掌握的Properties三个方法:
 *      String value = pro.getProperty("name");
 *      pro.setProperty("name", "value");
 *      Enumeration names = pro.propertyNames();
 */
public class PropertiesTest {
    public static void main(String[] args) {
        // 创建一个属性类对象
        Properties pro = new Properties();

        // 往属性类对象中存储key和value,类似于map.put(k, v)
        pro.setProperty("jdbc.driver", "com.mysql.jdbc.Driver");
        pro.setProperty("jdbc.user", "root");
        pro.setProperty("jdbc.password", "123123");
        pro.setProperty("jdbc.url", "jdbc:mysql://localhost:3306/powernode");

        // 通过key获取value
        String driver = pro.getProperty("jdbc.driver");
        String user = pro.getProperty("jdbc.user");
        String password = pro.getProperty("jdbc.password");
        String url = pro.getProperty("jdbc.url");

        System.out.println(driver);
        System.out.println(user);
        System.out.println(password);
        System.out.println(url);

        // 获取所有的key
        Enumeration<?> names = pro.propertyNames();
        while (names.hasMoreElements()) {
            String name = (String)names.nextElement();
            String value = pro.getProperty(name);
            System.out.println(name + "=" + value);
        }
    }
}

TreeMap

TreeMap底层就是红黑树。
TreeMap和HashMap用法一样,只不过需要key排序的时候,就可以使用TreeMap。
TreeMap的key不能是null。
让TreeMap集合的key可排序,有两种方式:
第一种方式:key实现了Comparable接口,并且提供了compareTo方法,在该方法中添加了比较规则。(比较规则不变的话建议这种。)
第二种方式:创建TreeMap集合时,传一个比较器,比较器实现Comparator接口,在compare方法中添加比较规则。

哪些集合不能存null

/**
 * 都是哪些集合不能添加null。哪些可以???
 *
 * Hashtable的key和value都不能为null。
 * Properties的key和value都不能为null。
 * TreeMap的key不能为null。
 * TreeSet不能添加null。
 */
public class TreeMapTest04 {
    public static void main(String[] args) {
        Map<Integer, String> map = new TreeMap<>();

        // java.lang.NullPointerException
        // TreeMap集合的key不能为null
        //map.put(null, "abc");

        // TreeMap集合的value可以是null
        map.put(1, null);

        Properties pro = new Properties();
        // java.lang.NullPointerException
        //pro.setProperty(null, "abc");
        // java.lang.NullPointerException
        //pro.setProperty("abc", null);
    }
}

Set

Set接口继承Collection,没有任何新增任何方法。
Set接口常用实现类包括:HashSet、LinkedHashSet、TreeSet。
通过源码得知:HashSet底层就是HashMap,往HashSet集合中存储元素,实际上是放到了HashMap集合的key部分。因此放在HashSet集合中的元素,要同时重写hashCode+equals。底层当然也是哈希表。HashSet集合存储元素特点:无序不可重复。
通过源码得知:LinkedHashSet底层就是LinkedHashMap。所以底层是“哈希表+双向链表”。LinkedHashSet集合存储元素特点:有序不可重复。有序指的是存进去的顺序和取出的顺序一样。放进去的元素也需要重写hashCode+equals。
通过源码得知:TreeSet底层就是TreeMap。所以底层也是红黑树。TreeSet集合存储元素特点:有序不可重复。有序表示可排序。放在TreeSet集合中元素要想排序,要么存储的元素实现Comparable接口,要么在构造TreeSet集合的时候传一个比较器。TreeSet中不能存放null。

HashSet

/**
 * HashSet集合底层特点:无序不可重复。
 * 放在HashSet集合中的元素需要同时重写hashCode + equals
 */
public class HashSetTest {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<>();

        // 无序不可重复
        set1.add(100);
        set1.add(100);
        set1.add(100);
        set1.add(120);
        set1.add(99);
        set1.add(1);
        set1.add(888);
        set1.add(666);

        System.out.println(set1);

        Set<String> set2 = new HashSet<>();
        set2.add("bbc");
        set2.add("bbc");
        set2.add("abc");
        set2.add("acc");
        set2.add("abb");
        set2.add("acc");

        System.out.println(set2);

        Set<Vip> vips = new HashSet<>();

        Vip vip1 = new Vip("11111111", "zhangsan", 20);
        Vip vip2 = new Vip("11111111", "zhangsan", 20);
        Vip vip3 = new Vip("11111111", "zhangsan", 20);
        Vip vip4 = new Vip("11111112", "zhangsan", 20);

        vips.add(vip1);
        vips.add(vip2);
        vips.add(vip3);
        vips.add(vip4);

        System.out.println(vips);
    }
}

LinkedHashSet

/**
 * LinkedHashSet集合特点:有序不可重复。
 * 同样需要重写hashCode + equals
 * 有序:插入顺序有保障。
 */
public class LinkedHashSetTest {
    public static void main(String[] args) {

        Set<Integer> set1 = new LinkedHashSet<>();

        set1.add(100);
        set1.add(2);
        set1.add(300);
        set1.add(111);
        set1.add(222);

        for(Integer value : set1){
            System.out.println(value);
        }
    }
}

TreeSet

/**
 * TreeSet中不能存储null。
 * TreeSet集合存储元素特点:有序不可重复。
 * 不可重复:hashCode + equals需要重写。
 * TreeSet集合有序:可排序。
 *
 * 两种排序的方式:
 *      第一种:存放在HashSet集合中的元素实现 java.lang.Comparable接口。
 *      第二种:创建HashSet集合的时候,给构造方法传递一个比较器: java.util.Comparator的实现类。
 */
public class TreeSetTest {
    public static void main(String[] args) {
        /*Set<Vip> vips = new TreeSet<>();

        vips.add(new Vip("123451", "张三1", 20));
        vips.add(new Vip("123452", "张三2", 21));
        vips.add(new Vip("123453", "张三3", 19));
        vips.add(new Vip("123454", "张三4", 18));
        vips.add(new Vip("123455", "张三5", 50));

        for(Vip vip : vips){
            System.out.println(vip);
        }*/


        Set<Vip> vips2 = new TreeSet<>(new Comparator<Vip>() {
            @Override
            public int compare(Vip o1, Vip o2) {
                return o2.getAge() - o1.getAge();
            }
        });

        vips2.add(new Vip("123451", "张三1", 20));
        vips2.add(new Vip("123452", "张三2", 21));
        vips2.add(new Vip("123453", "张三3", 19));
        vips2.add(new Vip("123454", "张三4", 18));
        vips2.add(new Vip("123455", "张三5", 50));

        for(Vip vip : vips2){
            System.out.println(vip);
        }


    }
}

面试题

// 创建HashSet集合(底层HashMap,哈希表数据结构)
        HashSet<Student> set = new HashSet<>();
        // 创建Student对象
        Student stu = new Student("张三", 18);
        // 添加Student对象
        set.add(stu);
        // 又添加了新的Student对象
        set.add(new Student("李四", 21));
        System.out.println(set);
        // 将张三学生的名字修改为王五
        // 虽然修改了,但是这个节点Node还是采用了之前 张三 的哈希值
        stu.setName("王五");
        // 问题1:请问是否删除了HashSet集合中的stu对象呢???
        // 不能删除
        set.remove(stu);
        //System.out.println(set);
        // 问题2:添加以下Student对象是否成功???
        // 可以添加成功
        set.add(new Student("王五", 18));
        //System.out.println(set);
        // 问题3:添加以下Student对象是否成功???
        // 可以添加成功
        set.add(new Student("张三", 18));
        System.out.println(set);

Collections工具类

针对List集合又准备了排序方法:sort
混排,打乱顺序:shuffle
反转:reverse
替换所有元素:fill
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java老狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值