Java集合框架

1 集合框架概述

Java 集合框架提供了丰富的数据结构,用于不同的应用场景。它们可以帮助开发人员高效地操作和管理数据,提供了多种遍历、查找、排序等功能。

1.1 集合和数组的区别

长度固定 vs 长度可变
数组的长度在创建时就确定,并且不可改变。
集合的长度是可变的,可以根据需要动态地增加或减少元素。这使得集合更加灵活,适用于需要动态管理元素的情况。

类型限制 vs 泛型支持
数组可以存储同一种类型的元素,但一旦确定类型后,数组只能存储该类型的元素。
集合可以存储不同类型的元素,也支持使用泛型来指定集合中存储的元素类型,使得类型安全更高。

元素类型
数组可以存储基本数据类型(如 int、char 等)和对象类型(如类、接口等)。
集合只能存储对象类型,不能直接存储基本数据类型,但可以使用对应的包装类。

内存管理
数组在创建时会分配一段连续的内存空间,所有元素都存储在这个内存块中。当数组长度发生变化时,需要重新分配内存并进行数据迁移。
集合的内存管理由 Java 虚拟机负责,它可以根据需要动态调整内存分配,无需手动管理内存。

API 和功能
数组提供的操作有限,主要包括获取、设置元素值以及遍历等基本功能。
集合提供了丰富的 API,包括添加、删除、查找、遍历、排序等多种功能,不同类型的集合类还提供了各自特有的功能和性能优化。

性能和效率
数组的访问速度相对较快,因为元素存储在连续的内存中。
集合的性能因具体实现而异,不同类型的集合类在不同场景下的性能也会有所差异。

1.2 Java集合框架图

![在这里插入图片描述](https://img-blog.csdnimg.cn/d3d24d83125e4d5aa983c888f67e08fa.png

List 接口
ArrayList:适用于需要快速随机访问元素的场景,不涉及频繁的插入和删除操作。
LinkedList:适用于频繁的插入和删除操作,以及需要在集合中间进行操作的场景。
Vector:类似于 ArrayList,但是是线程安全的,适用于多线程环境。

Set 接口
HashSet:适用于存储不重复元素的场景,不保证元素的顺序。
LinkedHashSet:在 HashSet 的基础上保留了元素的插入顺序。
TreeSet:适用于需要对元素进行排序的场景,元素按照自然顺序或自定义排序规则进行排序。

Queue 接口
LinkedList:可以用作队列或双端队列,适用于先进先出(FIFO)或后进先出(LIFO)的场景。
PriorityQueue:适用于需要根据元素的优先级进行处理的场景,优先级队列实现了最小堆的数据结构。

Map 接口
HashMap:适用于存储键值对,并且键不重复的场景,不保证键值对的顺序。
LinkedHashMap:在 HashMap 的基础上保留了键值对的插入顺序。
TreeMap:适用于需要按照键的自然顺序或自定义排序规则进行排序的场景。
HashTable:类似于 HashMap,但是是线程安全的,适用于多线程环境。

其他集合类:
HashSet 和 HashMap 的变种:EnumSet、EnumMap、IdentityHashMap 等。
WeakHashMap:适用于需要在没有引用指向键时,自动回收键值对的场景。

2 Collection接口

Collection 接口是 Java 集合框架中最基本的接口之一,它是所有集合类的父接口,定义了集合的基本操作和方法。Collection 接口提供了一组通用的方法,用于操作集合中的元素,例如添加、删除、遍历等。

  • JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如: Set和List)实现。
  • 在 Java5 之前, Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理; 从 JDK 5.0 增加了泛型以后, Java 集合可以记住容器中对象的数据类型。

1.接口方法
boolean add(E e): 将指定的元素添加到集合中。

boolean remove(Object o): 从集合中移除指定元素。

boolean contains(Object o): 判断集合是否包含指定元素。

boolean isEmpty(): 判断集合是否为空。

int size(): 返回集合中元素的数量。

void clear(): 清空集合中的所有元素。

Iterator iterator(): 返回用于遍历集合的迭代器。

boolean addAll(Collection<? extends E> c): 将另一个集合中的所有元素添加到当前集合中。

boolean removeAll(Collection<?> c): 从当前集合中移除与另一个集合共有的元素。

boolean retainAll(Collection<?> c): 保留当前集合与另一个集合共有的元素,移除其他元素。

boolean containsAll(Collection<?> c): 判断当前集合是否包含另一个集合中的所有元素。

Object[] toArray(): 将集合中的元素转换为数组。

T[ ] toArray(T[ ] a): 将集合中的元素转换为指定类型的数组。

boolean equals(Object obj):集合是否相等

2.Collection的使用案例

	@Test
    public void test1(){
        Collection c = new ArrayList();
        c.add("kong");
        c.add("de");
        c.add(1);
        
        System.out.println(c.isEmpty());
        System.out.println(c.size());
        
        c.remove("de");
        System.out.println(c);
    }

3 Iterator接口

Iterator 接口在 Java 中用于遍历集合(Collection)中的元素,提供了一种统一的访问方式,无需关心集合的底层实现。它是一个迭代器,通过它可以依次访问集合中的元素,并且在遍历时可以对元素进行增删操作。

  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
  • Iterator 仅用于遍历集合, Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

常用方法
boolean hasNext(): 返回 true 如果仍有元素可以迭代,否则返回 false。
E next(): 返回迭代的下一个元素。
void remove(): 从迭代器指向的集合中移除迭代器返回的最后一个元素(可选操作)。

案例:

    @Test
    public void test() {
        Collection c1 = new ArrayList();
        //添加元素
        c1.add("hello");
        c1.add("world");
        c1.add(123); 
        c1.add(456);

        //iterator循环集合
        Iterator it = c1.iterator();
        
        while (it.hasNext()) {
            Object obj = it.next();
            if ("tom".equals(obj)) {
                it.remove();
            }
            System.out.println(obj);
        }
        
		//增强for循环,底层是Iterator循环
		for (Object obj : c1) {
            System.out.println(obj);
        }
		
		//itco
		for (Iterator iterator = c1.iterator(); iterator.hasNext(); ) {
            Object obj = iterator.next();
            System.out.println(obj);
        }
        
        System.out.println(c1);
    }     

4 List接口

List 接口是 Java 集合框架中的一个主要接口,表示一个有序的、可重复的元素集合。它继承自 Collection 接口,并添加了与索引相关的方法,允许通过索引访问和操作集合中的元素。

  • 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • JDK API中List接口的实现类常用的有: ArrayList、 LinkedList和Vector。

常用方法

  • int size(): 返回列表中的元素数量。

  • boolean isEmpty(): 判断列表是否为空。

  • boolean contains(Object o): 判断列表是否包含指定元素。

  • boolean add(E e): 将元素添加到列表的末尾。

  • void add(int index, E element): 在指定索引位置插入元素。

  • E get(int index): 获取指定索引位置的元素。

  • E set(int index, E element): 替换指定索引位置的元素。

  • E remove(int index): 移除指定索引位置的元素。

  • int indexOf(Object o): 返回指定元素的第一个出现位置的索引。

  • int lastIndexOf(Object o): 返回指定元素的最后一个出现位置的索引。

  • List subList(int fromIndex, int toIndex): 返回指定范围的子列表。

案例:

	@Test
    public void test5() {
        List list = new ArrayList();

        list.add("a");
        list.add("b");
        list.add("a");
        list.add("a");
        list.add(0, 1);
        list.add(0, 2);
        list.add(0, 3);

        System.out.println(list);

        System.out.println(list.get(3));

        System.out.println(list.indexOf("a"));
        System.out.println(list.lastIndexOf("a"));
        
        list.remove(0);
        list.remove("a");

        System.out.println(list);
        System.out.println(list.subList(0, 3));
    }

结果:

[3, 2, 1, a, b, a, a]
a
3
6
[2, 1, b, a, a]
[2, 1, b]

4.1 ArrayList

ArrayList 是 Java 集合框架中 List 接口的一个常见实现类,内部实现使用数组进行存储,可以自动调整大小,集合扩容时会创建更大的数组空间,把原有数据复制到新数组中。 ArrayList 支持对元素的快速随机访问,但是插入与删除时速度通常很慢,因为这个过程很有可能需要移动其他元素。

  • ArrayList的JDK1.8之前与之后的实现区别?
    • JDK1.7: ArrayList像饿汉式,直接创建一个初始容量为10的数组
    • JDK1.8: ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个初始容量为10的数组
  • Arrays.asList(…) 方法返回的 List 集合, 既不是 ArrayList 实例,也不是Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合。
	/**
     * 1. 有序:插入顺序
     * 2. 允许重复
     * 3. 底层是数组,默认大小10,每次扩容1.5倍
     *      - jdk1.7:默认大小10
     *      - jdk1.8 默认大小0,第一次使用时初始化成10
     * 4:添加和删除,性能差,随机遍历性能好
     */

4.2 LinkedList

LinkedList 的本质是双向链表。与 ArrayList 相比 , LinkedList 的插入和删除速度更快,但是随机访问速度很慢。除继承 AbstractList 抽象类外, LinkedList 还实现了另一个接口 Deque ,即 double-ended queue。这个接口同时具有队列和枝的性质。LinkedList 包含 3 个重要的成员 size 、first、 last 。 size 是双向链表中节点的个数。first 和 last 分别指向第一个和最后一个节点的引用。 LinkedList 的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序序查找的线性结构,内存利用率较高。

新增方法:

  • void addFirst(Object obj): 在链表开头插入指定的元素
  • void addLast(Object obj): 在链表末尾插入指定的元素
  • Object getFirst(): 获取链表的第一个元素
  • Object getLast(): 获取链表的最后一个元素
  • Object removeFirst(): 移除并返回链表的第一个元素
  • Object removeLast() : 移除并返回链表的最后一个元素

LinkedList: 双向链表, 内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。 Node除了保存数据,还定义了两个变量:

  • prev变量记录前一个元素的位置
  • next变量记录下一个元素的位置
    /**
     * 1. 底层原理:基于双向链表实现
     * 2. 插入和删除快,遍历性能差
     * 3. 可以直接操作头和尾
     * 4. 有序(插入顺序),允许重复
     */
    @Test
    public void test() {
        LinkedList list = new LinkedList();

        list.add("c");
        list.add("b");
        list.add("a");

        User u1 = new User("马云", 45);
        User u2 = new User("马化腾", 38);
        list.add(u1);
        list.add(u2);

        list.addFirst("1");
        list.addLast("2");

        System.out.println(list.getLast());
        
        for (Object o : list) {
            System.out.println(o);
        }
        System.out.println(list);
    }

结果:

2
1
c
b
a
User{name='马云', age=45}
User{name='马化腾', age=38}
2
[1, c, b, a, User{name='马云', age=45}, User{name='马化腾', age=38}, 2]

4.3 Vector

Vector是Java中的一个类,它实现了List接口,并提供了一个动态数组的实现。它的所有方法都是同步的,因此在多线程环境下可以安全地进行操作。

  • Vector 是一个古老的集合, JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。

  • 在各种List中,最好把ArrayList作为最佳选择。当插入、删除频繁时,使用LinkedList; 由于同步的开销,Vector总是比ArrayList慢,所以尽量避免使用。

  • 新增方法:

    • void addElement(Object obj):将指定的元素添加到 Vector 的末尾。
    • void insertElementAt(Object obj,int index):在指定的索引位置插入指定的元素。原有位置上的元素和后面的元素都会向后移动。
    • void setElementAt(Object obj,int index): 将指定索引位置的元素替换为新的元素。
    • void removeElement(Object obj): 从 Vector 中移除指定的元素。如果存在多个相同的元素,只会移除第一个匹配的元素。
    • void removeAllElements() : 移除 Vector 中的所有元素,使其为空集合。

4.3.1 面试题

请问ArrayList/LinkedList/Vector的异同? 谈谈你的理解? ArrayList底层是什么?扩容机制? Vector和ArrayList的最大区别?

  • ArrayList和LinkedList的异同
    二者都线程不安全,相对线程安全的Vector,执行效率高。此外, ArrayList是实现了基于动态数组的数据结构, LinkedList基于链表的数据结构。对于随机访问get和set, ArrayList优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove, LinkedList比较占优势,因为ArrayList要移动数据。
  • ArrayList和Vector的区别
    Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。 Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。 Vector还有一个子类Stack。

5 Set接口

Set 是不允许出现重复元素的集合类型。 Set 体系最常用的是 HashSet 、TreeSet和 LinkedHashSet 三个集合类。 HashSet 从源码分析是使用 HashMap 来实现的,只是Value 固定为一个静态对象,使用 Key 保证集合元素的唯一性,但它不保证集合元素的顺序。 TreeSet 也是如此,从源码分析是使用 TreeMap 来实现的,底层为树结构,在添加新元素到集合中时,按照某种比较规则将其插入合适的位置 ,保证插入后的集合仍然是有序的。LinkedHashSet 继承自 HashSet , 具有 HashSet 的优点,内部使用链表维护了元素插入顺序。

5.1 HashSet

HashSet是Java中的一个集合类,它是基于哈希表(Hash Table)实现的,用于存储一组不重复的元素。HashSet继承自AbstractSet类,并且实现了Set接口。HashSet内部使用哈希表来存储元素,通过哈希函数将元素映射到哈希表的存储位置。当需要使用一个集合来存储一组元素,并且不关心元素的顺序且不允许重复时,可以选择使用HashSet。

HashSet的特点如下:

  • 元素唯一性:HashSet中不会包含重复的元素。如果尝试向HashSet中添加已经存在的元素,则操作会被忽略。
  • 无序性:HashSet中的元素没有特定的顺序。具体的元素顺序在不同的迭代中可能会有所变化。
  • 允许空元素:HashSet允许存储一个空元素(null)。

案例:

	@Test
    public void test(){
        List list = new ArrayList();
        list.add("kong");
        list.add("kong");
        list.add(18);
        System.out.println(list);

        //无序,不重复
        Set set = new HashSet();
        set.add("kong");
        set.add("kong");
        set.add(18);
        System.out.println(set);
    }

结果:

[kong, kong, 18]
[18, kong]

5.1.1 HashSet原理

原理:

  • HashSet底层是HashMap实现的,只用key,而value存储一个静态的new Object

  • HashSet 集合判断两个元素相等的标准: 两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。

  • 对于存放在Set容器中的对象, 对应的类一定要重写equals()和hashCode(Objectobj)方法,以实现对象相等规则。即: “相等的对象必须具有相等的散列码” 。

  • 向HashSet中添加元素的过程:

    • 当向 HashSet 集合中存入一个元素时, HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值, 然后根据 hashCode 值, 通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。 (这个散列函数会与底层数组的长度相计算得到在数组中的下标, 并且这种散列函数计算还尽可能保证能均匀存储元素, 越是散列分布,该散列函数设计的越好)
    • 如果两个元素的hashCode()值相等, 会再继续调用equals方法, 如果equals方法结果为true, 添加失败; 如果为false, 那么会保存该元素, 但是该数组的位置已经有元素了(哈希冲突),那么会通过链表的方式继续链接。当链表长度超过一定阈值(默认为8)时,链表将会转换为红黑树
    • 如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等, hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
  • HashSet底层也是HashMap, 初始容量为16, 当如果使用率超过0.75, (16*0.75=12)就会扩大容量为原来的2倍。 (16扩容为32, 依次为64,128…等)

重写hashCode和equals原则

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
  • 重写equals方法的时候一般都需要同时重写hashCode方法。 通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

5.1.2 HashSet案例1

HashSet存入数据的过程:

  1. 得到对象的hashCode,hashCode%16,决定落到那个槽位(默认16个槽位)Entry[16]
  2. 再次添加对象,如果对象的hashCode和第一次添加的相同,执行equals
  3. 如果equals返回true,hashCode相同,equals相同,添加失败
  4. 如果eques返回false,hashCode相同,形成一个链表

User.java

public class User {

    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        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 String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; //内存地址相同返回true
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == name;
    }

    /**
     ** 注:**为什么hashCode中,用31这个数子?
     *
     *- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
     *- 并且31只占用5bits,相乘造成数据溢出的概率较小。
     *- 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。 (提高算法效率)
     *- 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除! (减少冲突)
     */
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

测试:

    @Test
    public void test(){
        Set set = new HashSet();
        User u1 = new User("kong", 18);
        User u2 = new User("kong", 18);
        User u3 = new User("dai", 22);
        User u4 = new User("kong", 20);


        set.add(u1);
        set.add(u2);

        set.add(u3);
        set.add(u4);

        for (Object o : set) {
            System.out.println(o);
        }
    }

结果:

User{name='kong', age=20}
User{name='kong', age=18}
User{name='dai', age=22}

5.1.3 HashSet案例2

Person类已经按照id和name重写了hashCode()和equals()方法

public class HashSetDemo1 {
	@Test
    public void test1(){
        Person p1 = new Person(1001,"AA");
        Person p2 = new Person(1002,"BB");
        HashSet set = new HashSet();
        set.add(p1);
        set.add(p2);

        System.out.println(set);//输出2个对象

        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);//输出2个对象

        set.add(new Person(1001,"CC"));
        System.out.println(set);//输出3个对象

        set.add(new Person(1001,"AA"));
        System.out.println(set);//输出4个对象
    }
}
class Person{
    public int id;
    public String name;
    
    public Person() {
    }

    public Person(int id, String name) {
        this.name = name;
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, id);
    }
}

运行结果:

[Person{id=1002, name='BB'}, Person{id=1001, name='AA'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]

解释说明:

1. 首先,创建了两个 Person 对象 p1 和 p2,并将它们添加到 HashSet 集合 set 中。由于 HashSet 不允许重复元素,两个对象会被成功添加。
2. 打印 set,输出的结果会显示添加的两个 Person 对象。
3. 修改 p1 的名字为 “CC”,然后尝试从 set 中移除 p1。由于 HashSet 使用哈希值来定位元素,此时 p1 的哈希值发生了变化,导致集合无法准确定位并删除 p1。因此,set.remove(p1) 不会成功。
4. 打印 set,输出的结果会显示两个 Person 对象,因为 p1 没有被成功移除。
5. 创建一个新的 Person 对象,与之前的 p1 哈希值相同,但内容不同。由于 HashSet 不允许重复元素,这个新对象会被成功添加到 set。
6. 打印 set,输出的结果会显示三个 Person 对象,分别是 p2、p1(因为之前的移除操作失败了)以及新创建的对象。
7. 创建一个新的 Person 对象,与之前的 p1 内容相同,但哈希值不同。这个对象会被成功添加到 set,因为 HashSet 使用哈希值来判断元素是否重复。
8. 打印 set,输出的结果会显示四个 Person 对象,包括之前的三个以及这个新创建的对象。

大致示意图:
在这里插入图片描述

调试结果:
在这里插入图片描述

5.2 LinkedHashSet

LinkedHashSet 是 Java 集合框架中的一个类,它是 HashSet 的一个变种,具有与 HashSet 相同的无重复元素的特性,但是它还保持了插入顺序。

以下是 LinkedHashSet 的一些特点:

  1. 无重复元素: LinkedHashSet 和 HashSet 一样,不允许有重复的元素。
  2. 保持插入顺序: LinkedHashSet 在内部使用一个哈希表和双向链表来维护元素的插入顺序,因此它会按照元素插入的顺序来返回元素。
  3. 性能: LinkedHashSet插入性能略低于 HashSet,因为它需要维护链表来保持插入顺序,但在迭代访问 Set 里的全部元素时有很好的性能。
    在这里插入图片描述

案例:

	@Test
    public void test(){
        Set set = new LinkedHashSet();
        set.add("a");
        set.add("b");
        set.add("a"); //重复的元素将被忽略
        set.add("3");
        set.add("1");
        set.add("2");

        for (Object o : set) {
            System.out.println(o);
        }
    }

结果:

a
b
3
1
2

5.3 TreeSet

TreeSet 是 SortedSet 接口的实现类, TreeSet 可以确保集合元素处于排序状态。并且使用红黑树(一种自平衡二叉搜索树)来存储元素。

以下是 TreeSet 的一些特点和使用方式:

  1. 无重复元素: 与其他 Set 实现一样,TreeSet 不允许重复的元素。
  2. 有序集合: TreeSet 中的元素是有序的,它们根据元素的自然顺序进行排序,或者根据提供的比较器进行排序。
  3. 红黑树: TreeSet 内部使用红黑树来存储元素,这使得插入、删除和查找元素的操作都具有较好的性能。
  4. 自然排序: 如果元素实现了 Comparable 接口,或者在创建 TreeSet 时提供了一个比较器,TreeSet 将会按照这个顺序进行排序。

案例:

	@Test
    public void test1(){
        TreeSet set = new TreeSet();

        set.add("b");
        set.add("b");
        set.add("a");
        set.add("c");
        set.add("1");
        set.add("3");
        set.add("2");

        for (Object o : set) {
            System.out.println(o);
        }
    }

结果:

1
2
3
a
b
c

5.3.1 自然排序

TreeSet 两种排序方法: 自然排序和定制排序。默认情况下, TreeSet 采用自然排序。

  • 自然排序: TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
  • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口。

不然会由于底层代码 Comparable<? super K> k = (Comparable<? super K>) key; 不能转换为Comparable类型而报错:java.lang.ClassCastException: c_TreeSet.Person cannot be cast to java.lang.Comparable

  • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
  • Comparable 的典型实现:
    • BigDecimal、 BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
    • Character:按字符的 unicode值来进行比较
    • Boolean: true 对应的包装类实例大于 false 对应的包装类实例
    • String:按字符串中字符的 unicode 值进行比较
    • Date、Time:后边的时间、日期比前面的时间、日期大
  • 向TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。
  • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。
  • 自然排序器方法compare返回值
    • 0:相等,添加失败
    • -1:小,放到左子树
    • 1:大,放到右子树

案例:
User类实现Comparable 接口

public class User implements Comparable {
    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        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 String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if(o instanceof User){
            User user = (User)o;
            int compare = this.name.compareTo(user.name);
            if(compare != 0){
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输入的类型不匹配");
        }
    }
}

测试:

public static void main(String[] args) {
        User u1 = new User("aaa", 35);
        User u2 = new User("bbb", 36);
        User u3 = new User("ccc", 50);
        User u4 = new User("ccc", 30);
        User u5 = new User("ddd", 20);
        User u6 = new User("ddd", 20);

        TreeSet set = new TreeSet();
        set.add(u1);
        set.add(u2);
        set.add(u3);
        set.add(u4);
        set.add(u5);
        set.add(u6);

        for (Object o : set) {
            System.out.println(o);
        }
}

结果:

User{name='aaa', age=35}
User{name='bbb', age=36}
User{name='ccc', age=30}
User{name='ccc', age=50}
User{name='ddd', age=20}

5.3.2 定制排序

  • TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。 需要重写compare(T o1,T o2)方法。
  • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
  • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
  • 定制排序器方法compare返回值
    • 0:相等,添加失败
    • -1:小,放到左子树
    • 1:大,放到右子树

案例:

public class TreeSetDemo {
    public static void main(String[] args) {
        User u1 = new User("aaa", 35);
        User u2 = new User("bbb", 35);
        User u3 = new User("ccc", 35);
        User u4 = new User("ccc", 30);
        User u5 = new User("ccc", 74);
        User u6 = new User("eee", 39);
        User u7 = new User("fff", 3);
        User u8 = new User("ggg", 2);
        User u9 = new User("ggg", 1);

        //定制排序器,匿名内部类
        Comparator com = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                User u1 = (User) o1;
                User u2 = (User) o2;
                return u1.getName().compareTo(u2.getName());
            }
        };

        TreeSet set = new TreeSet(com);
        set.add(u1);
        set.add(u2);
        set.add(u3);
        set.add(u4);
        set.add(u5);
        set.add(u6);
        set.add(u7);
        set.add(u8);
        set.add(u9);

        for (Object o : set) {
            System.out.println(o);
        }
    }
}

结果:

User{name='aaa', age=35}
User{name='bbb', age=35}
User{name='ccc', age=35}
User{name='eee', age=39}
User{name='fff', age=3}
User{name='ggg', age=2}

6 Map接口

Map 集合是以 Key-Value 键值对作为存储元素实现的哈希结构, Key 按某种哈希函数计算后是唯一的,Value 则是可以重复的。
Map 类提供三种 Collection 视图,在集合框架图中, Map 指向 Collection 的箭头仅表示两个类之间的依赖关系 。
可以使用keySet()查看所有的 Key,使用 values()查看所有的 Value ,使用 entrySet()查看所有的键值对。
最早用于存储键值对的 Hashtable 因为性能瓶颈已经被淘汰,而如今广泛使用的 HashMap ,线程是不安全的。 ConcurrentHashMap 是线程安全的,在 JDK8 中进行了锁的大幅度优化,体现出不错的性能。在多线程并发场景中,优先推荐使用ConcurrentHashMap ,而不是 HashMap 。 TreeMap 是 Key 有序的 Map 类集合。
在这里插入图片描述

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value
  • Map 中的 key 和 value 都可以是任何引用类型的数据
  • Map 中的 key 用Set来存放, 不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
  • Map接口的常用实现类: HashMap、 TreeMap、 LinkedHashMap和Properties。 其中, HashMap是 Map 接口使用频率最高的实现类
  • Map的基本结构如下:
    在这里插入图片描述

常用方法:

添加、 删除、修改操作:

  • Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
  • void putAll(Map m):将m中的所有key-value对存放到当前map中
  • Object remove(Object key):移除指定key的key-value对,并返回value
  • void clear():清空当前map中的所有数据

元素查询的操作:

  • Object get(Object key):获取指定key对应的value
  • boolean containsKey(Object key):是否包含指定的key
  • boolean containsValue(Object value):是否包含指定的value
  • int size():返回map中key-value对的个数
  • boolean isEmpty():判断当前map是否为空
  • boolean equals(Object obj):判断当前map和参数对象obj是否相等

元视图操作的方法:

  • Set keySet():返回所有key构成的Set集合
  • Collection values():返回所有value构成的Collection集合
  • Set entrySet():返回所有key-value对构成的Set集合

6.1 HashMap

  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序
  • 所有的key构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
  • 所有的value构成的集合是Collection:无序的、可以重复的。所以, value所在的类要重写: equals()
  • 一个key-value构成一个entry
  • 所有的entry构成的集合是Set:无序的、不可重复的
  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
  • HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

6.1.1 HashMap的JDK 8存储结构

JDK 7及以前版本: HashMap是数组+链表结构(即为链地址法)
在这里插入图片描述

添加元素的过程:向HashMap中添加entry1(key, value), 需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到), 此哈希值经过处理以后, 得到在底层Entry[]数组中要存储的位置i。 如果位置i上没有元素, 则entry1直接添加成功。 如果位置i上已经存在entry2(或还有链表存在的entry3, entry4), 则需要通过循环的方法, 依次比较entry1中key和其他的entry。 如果彼此hash值不同, 则直接添加成功。 如果hash值相同, 继续比较二者是否equals。 如果返回值为true, 则使用entry1的value去替换equals为true的entry的value。 如果遍历一遍以后, 发现所有的equals返回都为false,则entry1仍可添加成功。 entry1指向原有的entry元素。总的来说,每次添加元素通常会成功。

JDK 8版本发布以后: HashMap是数组+链表+红黑树实现。
在这里插入图片描述

  • HashMap的内部存储结构其实是数组+链表+树的结合。 当实例化一个HashMap时, 会初始化initialCapacity和loadFactor, 在put第一对映射关系时, 系统会创建一个长度为initialCapacity的Node数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素, 即一个Node对象, 但每一个Node对象可以带一个引用变量next, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Node链。 也可能是一个一个TreeNode对象, 每一个TreeNode对象可以有两个叶子结点left和right, 因此, 在一个桶中, 就有可能生成一个TreeNode树。 而新添加的元素作为链表的last, 或树的叶子结点。

6.1.2 HashMap源码中的重要常量

  • DEFAULT_INITIAL_CAPACITY : 默认16,HashMap的默认容量
  • MAXIMUM_CAPACITY :默认2^30, HashMap的最大支持容量
  • DEFAULT_LOAD_FACTOR:默认0.75, HashMap的默认加载因子
  • TREEIFY_THRESHOLD:默认8,Bucket中链表长度大于该默认值,转化为红黑树
  • UNTREEIFY_THRESHOLD: 默认6,Bucket中红黑树存储的Node小于该默认值,转化为链表
  • MIN_TREEIFY_CAPACITY: 默认64,桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
  • table: 存储元素的数组,总是2的n次幂
  • entrySet: 存储具体元素的集合
  • size: HashMap中存储的键值对的数量
  • modCount: HashMap扩容和结构改变的次数。
  • threshold: 扩容的临界值, =容量*填充因子
  • loadFactor: 填充因子

6.1.3 HashMap扩容

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过16* 0.75=12(这个值就是代码中的threshold值, 也叫做临界值)的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。

HashMap 扩容的主要目的是为了降低哈希冲突,保持哈希表的装载因子(Load Factor)在一个合适的范围内,通常在 0.75 左右。装载因子表示哈希表中实际存储的元素数量与数组长度的比率,如果装载因子过高,哈希冲突增多,性能下降;如果装载因子过低,会浪费空间。

6.1.4 HashMap树化和链化

当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

树化
TREEIFY_THRESHOLD:树化的阈值,默认8
UNTREEIFY_THRESHOLD: 链化的阈值,默认6
MIN_TREEIFY_CAPACITY: 最小树化的容量,默认64,如果元素个数没有超过这个阈值(64),即使链超过8,会先进行扩容,超过64才进行树化。

比如下面所有元素的hashCode相同

  • 第一个元素,扩容到16
  • 第二个元素,形成链,链里面2个元素
  • …一直到链中有8个元素
  • 第9个元素,执行resize扩容到32
  • 第10个元素,执行resize扩容到64
  • 第11个元素,执行treeifyBin()方法树化
    • Node(next) 链
    • TreeNode(left,right)红黑树

链化:链的数量小于6,执行untreeify方法变成链

6.1.5 面试题:HashMap的容量,为什么必须是2的n次幂?

确定数组中的槽位须采用哈希取模算法,而HashMap使用 (n-1) & hash 得到槽位地址,这个运算n必须是2的n次幂

结论当容量是2的n次幂的时候(16,32…) hash % n = (n-1) & hash,因为位运算性能高

如果初始容量不是2的n次幂,HashMap调用tableSizeFor自动转换成大于这个数最小的2的n次幂

在这里插入图片描述

6.2 LinkedHashMap

  • LinkedHashSet和LinedHashMap的关系,从逻辑上这两个集合实现方式完全一致,只是LinkedHashSet使用LinkedHashMap实现,只有key,而value是一个静态的空对象
  • 底层使用链表实现
  • 有序(插入顺序),没有重复的集合
   public static void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap();
        map.put("zzz",1);
        map.put("zzz",1);
        map.put("yyy",1);
        map.put("xxx",1);

        map.put("111",1);
        map.put("222",1);

        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
    }

结果:

zzz=1
yyy=1
xxx=1
111=1
222=1

双向链表的结构:
在这里插入图片描述

6.3 TreeMap

  • TreeSet使用TreeMap实现,只是value使用静态空对象,只是用key实现TreeSet
  • TreeMap存储 Key-Value 对时, 需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
  • TreeSet底层使用红黑树结构存储数据
  • TreeMap 的 Key 的排序:
    • 自然排序: TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0,1,-1
    • 0:对象相等,添加失败
    • -1:比对象小,添加到左边
    • 1:比对象打,添加到右边

6.4 Hashtable

Hashtable 是 Java 中的一个散列表实现,它是一个早期的数据结构,用于存储键值对。在现代 Java 中,更常用的是 HashMap。

  • Hashtable 是线程安全的,多个线程可以同时读取和写入 Hashtable 实例,而不需要额外的同步措施。这是通过在每个方法上使用 synchronized 关键字来实现的。
  • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
  • 与HashMap不同, Hashtable 不允许使用 null 作为 key 和 value
  • 与HashMap一样, Hashtable 也不能保证其中 Key-Value 对的顺序
  • Hashtable判断两个key相等、两个value相等的标准, 与HashMap一致。

示例:

public class HashtableDemo {

    public static void main(String[] args) {
        Hashtable table = new Hashtable();

        table.put("a", 1);
        table.put("a", 11);
        table.put("b", 2);
        table.put("1", 3);
        table.put("2", 4);

        for (Object o : table.entrySet()) {
            System.out.println(o);
        }
    }
}

结果:

b=2
a=11
2=4
1=3

6.5 Properties

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  • 由于属性文件里的 key、 value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
  • 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

在项目下创建JDBC.properties文件

username = root
password = 123456
jdbc.url = jdbc:mysql
jdbc.driver = com.mysql.driver.Driver

测试

public class PropertiesDemo {
    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        //文件在user.dir目录下,可以使用下面相对目录
        FileInputStream in  = new FileInputStream("jdbc.properties");
        properties.load(in);

        System.out.println(properties.getProperty("username"));
        System.out.println(properties.getProperty("password"));
        System.out.println(properties.getProperty("jdbc.url"));
        System.out.println(properties.getProperty("jdbc.driver"));
        in.close();
    }
}

结果:

root
123456
jdbc:mysql
com.mysql.driver.Driver

7 Collections

Collections是Java中提供的一个工具类,位于java.util包下,用于操作集合(Collection)和映射(Map)等数据结构。它提供了一组静态方法,用于对集合进行排序、查找、填充、反转等操作。

Collections类的常用方法包括:

  1. 排序操作: (均为static方法)
  • reverse(List): 反转 List 中元素的顺序
  • shuffle(List): 对 List 集合元素进行随机排序
  • sort(List): 根据元素的自然顺序对指定 List 集合元素按升序排序
  • sort(List, Comparator): 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  • swap(List, int, int): 将指定 list 集合中的 i 处元素和 j 处元素进行交换
  1. 查找、替换
  • Object max(Collection): 根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection, Comparator): 根据 Comparator 指定的顺序,返回给定集合中的最大元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
  • Object min(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素
  • int frequency(Collection, Object): 返回指定集合中指定元素的出现次数
  • void copy(List dest,List src):将src中的内容复制到dest中
  • boolean replaceAll(List list, Object oldVal, Object newVal): 使用新值替换List 对象的所有旧值

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

  • synchronizedCollection(Collection c): 返回一个线程安全的集合。
  • synchronizedList(List list): 返回一个线程安全的列表。
  • synchronizedSet(Set s): 返回一个线程安全的集合。

示例:

public class CollectionsDemo {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add("z");
        list.add("x");
        list.add("y");
        list.add("a");
        list.add("a");
        list.add("1");
        list.add("2");

        System.out.println("正常输出"+list);

        Collections.reverse(list);
        System.out.println("反转 =" + list);

        Collections.shuffle(list);
        System.out.println("随机排序 =" + list);
        //自然排序必须类型一致
        Collections.sort(list);
        System.out.println("自然排序 =" + list);

        Comparator com = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                String s1 = (String) o1;
                String s2 = (String) o2;
                return -s1.compareTo(s2);
            }
        };

        Collections.sort(list, com);
        System.out.println("定制排序 =" + list);

        Collections.swap(list, 1, 2);
        System.out.println("x、y交换 =" + list);

        System.out.println("自然排序中最大=" + Collections.max(list));

        System.out.println("a的个数=" + Collections.frequency(list, "a"));

        List syncList = Collections.synchronizedList(list);
        syncList.add("3");

        System.out.println("线程安全的集合="+syncList);
    }
}

结果:

正常输出=[z, x, y, a, a, 1, 2]
反   转=[2, 1, a, a, y, x, z]
随机排序=[y, a, z, a, x, 1, 2]
自然排序=[1, 2, a, a, x, y, z]
定制排序=[z, y, x, a, a, 2, 1]
x、y交换=[z, x, y, a, a, 2, 1]
自然排序中最大=z
a的个数=2
线程安全的集合=[z, x, y, a, a, 2, 1, 3]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值