JavaSE知识点整理---集合篇

在这里插入图片描述


1. 数组与集合的区别?


在Java中,数组和集合都是容器,都可以用来存储多个数据,它们在使用的时候有一些区别:

1、类型:在Java中数组是最基本的数据结构,在内存中是线性存储的,可以用来存储基本数据类型(如int,double等)和引用类型(如对象)。集合(Collection),在Java中是一种接口,它是一种存储对象的容器,只能存储引用类型的数据。

// 数组
int[] arr1 = new int[5]; //int型数组
String[] arr2 = new String[5]; //对象数组

// 集合对象
List<String> list = ArrayList<>(); //存储Stirng类型数据,如果不指定泛型,它默认类型为object

2、长度:数组在初始化时需要指定长度(大小),一旦定义,其长度无法改变。而集合的长度是可变的,提供了add()、remove()等方法操作数据;(动态扩容、缩容)

int[] arr = new int[5];  //数组长度为5

3、操作数据:数组可以通过索引直接访问和修改元素,具有较高的访问效率,时间复杂度O(1)。而集合类通常提供了一系列方法来操作元素,例如添加、删除、查找等,对元素的访问需要通过方法调用。

// 1.数组通过索引方式操作数据
int[] arr = new int[5]; //int类似数组,默认值为0
// 添加数据
arr[1]=18;
// 覆盖数据
arr[1]=20;
// 访问数据
System.out.println(arr[1]);//20
// 获取数组长度
System.out.println(arr.length);
  
// 2.集合通过方法调用方式操作数据
List<String> list = ArrayList<>();
// 添加数据
list.add("张三");
list.add("李四");
list.add("王五");
// 删除数据
list.remove(2);
// 获取数据
System.out.println(list.get(1));//李四
// 获取集合中元素的个数
System.out.println(list.size());//2	

4、内存分配方式:数组在内存中是连续存储的,占用的内存空间是固定的。而集合类中的元素可以在内存中不连续存储,可以根据需要动态分配内存空间。

5、功能扩展性:集合类提供了丰富的方法和功能,例如排序、查找、遍历等,可以方便地进行各种操作。而数组的功能相对较少,需要自己编写代码实现相应的功能。

6、泛型:集合支持泛型,可以指定存储的元素类型,提供了更好的类型安全性。(避免类型转换问题)


小结:

  • 数组和集合都是容器,可以存储多个数据。
  • 数组的长度是固定的,一旦创建,无法改变。
  • 集合的长度是可变的,可以动态添加或删除元素。
  • 数组可以存储基本类型数据和引用类型数据,而集合只能存储引用类型数据。如果要存储基本类型数据,需要使用对应的包装类。
  • 数组可以通过索引方式直接访问元素,而集合通常通过方法调用进行元素的操作和访问。

总的来说,数组适用于长度固定且元素类型简单的情况,而集合适用于长度可变且元素类型复杂的情况。

单列集合:

Snipaste_2023-08-07_10-47-20

双列集合:

Snipaste_2023-08-07_11-46-15


2. List、Set、Map的区别?


List、Set和Map是Java中常用的集合接口,它们有以下区别:

  • List是一个有序的集合,可以包含重复元素。它允许按照插入顺序访问集合中的元素,并且可以根据索引位置进行操作。常见的实现类有ArrayList、LinkedList 和 Vector。

  • Set是一个不允许包含重复元素的集合。它不保证元素的顺序,即不按照插入顺序存储元素。常见的实现类有HashSet、TreeSet 和LinkedHashSet。

    • HashSet:基于哈希表实现,不保证元素的顺序。
    • TreeSet:基于红黑树实现,按照元素的自然顺序或指定的比较器进行排序。
    • LinkedHashSet:基于哈希表和链表实现,保持元素的插入顺序。
  • Map是一种键值对的集合,每个键都是唯一的。它允许通过键来访问和操作对应的值。常见的实现类有HashMap、TreeMap和LinkedHashMap、ConcurrentHashMap。

    • HashMap:基于哈希表实现,不保证键值对的顺序。

    • TreeMap:基于红黑树实现,按照键的自然顺序或指定的比较器进行排序。

    • LinkedHashMap:基于哈希表和双向链表实现,保持键值对的插入顺序。

    • ConcurrentHashMap:线程安全的Map。

ConcurrentHashMap:

ConcurrentHashMap位于juc包,它是哈希表的线程安全版本,并且对HashMap进行改进。相比于HashMap,在多线程环境下,ConcurrentHashMap提供了更好的并发性能和线程安全性,主要通过以下几个方面来实现:

  • 分段锁:ConcurrentHashMap将整个哈希表分成多个段(Segment),每个段维护着一个独立的哈希表。不同的线程可以同时访问不同的段,从而提高并发性能。每个段都有自己的锁,只有在修改该段的时候才需要加锁,这样可以减小锁的粒度,提高并发性能。
  • CAS操作:ConcurrentHashMap使用了CAS(Compare and Swap)操作来保证线程安全。CAS是一种无锁的原子操作,它可以在不使用锁的情况下实现对共享变量的原子操作。
  • volatile修饰符:ConcurrentHashMap使用volatile修饰符来保证可见性。volatile修饰的变量对所有线程可见,当一个线程修改了volatile变量的值,其他线程可以立即看到最新的值。

ConcurrentHashMap的使用方式与HashMap类似,可以通过put、get、remove等方法来操作元素。但需要注意的是,虽然ConcurrentHashMap是线程安全的,但在某些情况下,仍然需要额外的同步措施来保证一致性。


3. ArrayList、LinkedList、Vector的区别?


1、数据结构层面:

  • ArrayList底层基于动态数组实现的,适合随机访问元素。
  • LinkedList底层基于双向链表实现,适合数据的频繁插入和删除操作。
  • Vector和ArrayList类似,底层也是基于动态数组实现的,Vector大部分方法都是线程同步的。(JDK1.0就诞生了)

image-20230807164540707

2、线程安全层面:

  • ArrayList和LinkedList在多线程场景下都是不安全的(不提供内置的同步机制),需要外部同步(同步方法或者锁)。

    • 如何保证ArrayList的线程安全:https://blog.csdn.net/qq_44544908/article/details/129020075
  • Vector是线程安全的,因为它的大部分方法都是同步的,但在性能上不如ArrayList和LinkedList。

3、性能层面:

  • ArrayList在查询操作方面性能好,因为它底层基于动态数组实现,元素有索引,元素内存空间连续分配,查询操作的时间复杂度是O(1)。在不扩容的情况下,尾部添加元素的性能也很好,时间复杂度为O(1),而ArrayList在列表的中间或头部插入和删除元素时,需要进行移位操作,所以性能较差,时间复杂度是O(n),n是列表的长度。
  • LinkedList在插入或删除操作性能好(双向链表),查询操作性能较差。
  • Vector由于同步处理,性能较差,不推荐在非线程安全环境中使用。

示例:ArrayList、LinkedList、Vector的使用

package cn.z3inc.list;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 测试ArryList、LinkedList、Vector的使用
 *
 * @author 白豆五
 * @version 2023/8/7
 * @since JDK8
 */
public class ListDemo1 {
    
    
    public static void main(String[] args) {
    
    
        // ArrayList
        List<String> arrayList = new ArrayList<>();
        Collections.addAll(arrayList, "a", "b", "c", "d", "e", "f", "g"); // 批量添加
        System.out.println("arrayList = " + arrayList); // arrayList = [a, b, c, d, e, f, g]
        arrayList.remove(0);
        System.out.println("arrayList = " + arrayList); // arrayList = [b, c, d, e, f, g]

        // LinkedList
        List<String> linkedList = new ArrayList<>();
        Collections.addAll(linkedList, "a", "b", "c", "d", "e", "f", "g"); // 批量添加
        System.out.println("linkedList = " + linkedList); // linkedList = [a, b, c, d, e, f, g]
        linkedList.remove(0);
        System.out.println("linkedList = " + arrayList); // arrayList = [b, c, d, e, f, g]

        // Vector
        List<String> vector = new Vector<>();
        Collections.addAll(vector, "a", "b", "c", "d", "e", "f", "g"); // 批量添加
        System.out.println("vector = " + vector); // vector = [a, b, c, d, e, f, g]
        vector.remove(0);
        System.out.println("vector = " + vector); // vector = [b, c, d, e, f, g]
    }
}

运行结果:

image-20230807173955141

小节:ArrayList底层基于动态数组实现,适用于随机访问遍历元素和尾部添加元素,但在中间插入和删除时性能较差;LinkedList底层基于双向链表实现,适用于插入和删除操作,但随机访问的性能较差;Vector与ArrayList类似但线程安全,性能较差,一般在多线程环境下使用。


4. Java集合的快速失败机制【fail-fast】


1、概念:

  • “fail-fast” 是Java集合类的一种错误检测机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如 某一个线程通过迭代器遍历集合时,如果其他线程对集合进行结构上的修改(添加、删除元素),则会抛出ConcurrentModificationException并发修改异常。

  • “fail-fast” 机制可以及时发现问题,避免后续操作基于错误的数据继续进行,但它并不能保证线程安全。因为它并不能阻止其他线程修改集合,只能在访问时进行检测。

2、原理:

  • “fail-fast” 机制的实现是通过记录集合在结构上被修改的次数来实现的。每个集合中都有一个modCount 字段,用于记录集合的修改次数。每当对集合进行结构上的修改时,modCount 就会加1。而在进行迭代时,迭代器会存储一个 expectedModCount 值,用于记录迭代器对集合进行修改的次数。

  • 在迭代过程中,每次调用 hasNext()next() 方法时,会通过比较迭代器的expectedModCount和集合的modCount值来判断集合是否被修改过,如果两者不相等,则说明在迭代过程中有其他线程修改了集合,会立即抛出 ConcurrentModificationException异常。

image-20230807181905133 image-20230807183149494


示例:测试并发修改异常

package cn.z3inc.list;

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

/**
 * 测试并发修改异常
 *
 * @author 白豆五
 * @version 2023/8/7
 * @since JDK8
 */
public class ListDemo2 {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "c", "d", "e");
        // 获取list的迭代器
        Iterator<String> it = list.iterator();

        // 遍历
        //it.hasNext()判断是否有下一个元素
        //it.next()获取元素并将指针向后移动一位
        while (it.hasNext()) {
    
    
            System.out.println(it.next());
            list.add("f");
        }
    }
}

运行结果:

image-20230807183941683


5. List接口常用方法


添加元素:

  • boolean add(E e):向列表尾部添加一个元素。
  • add(int index, E element):向列表的指定位置处插入一个元素。
  • addAll(int index,Collection<? extends E> c):把另一个集合中的所有元素按照指定位置插入到当前列表中。

获取元素:

  • E get(int index):获取列表中指定索引处的元素。

  • int indexOf(Object o):获取列表中指定元素第一次出现的索引。

  • int lastIndexOf(Object o):获取列表中指定元素最后一次出现的索引。

删除元素:

  • E remove(int index):从列表中删除指定索引处的元素,返回值是被删除的元素。
  • boolean remove(Object obj):从列表中删除指定的元素(只删除列表中首次出现的元素)。

修改元素:

  • E set(int index,E element):修改列表中指定索引处的元素,返回值是被修改的元素。

其他方法:

  • int size():返回列表中的元素个数。
  • boolean isEmpty():用于判断列表是否为空。
  • boolean contains(Object o):用于判断列表是否包含指定的元素。
  • void clear():清除列表中的所有元素。
  • Object[] toArray():把列表转成数组
  • Iterator<E> iterator():获取用于遍历列表的迭代器。

6. List的三种遍历方式


Collection集合支持两种遍历方式:

  • Iterator迭代器。
  • 增强for循环。

List接口继承了Collection接口,所以以上两种遍历方式也适用于List,除次以外,List集合还支持for循环+get(索引)方式遍历。

  • Iterator迭代器。
  • 增强for循环。
  • for循环+get(索引)。

方式一: 使用迭代器(Iterator)进行遍历

迭代器是一种设计模式,它提供了一种统一的方式来遍历集合中的元素,而无需暴露底层数据结构的细节。通过使用迭代器,我们可以按顺序访问集合中的每个元素,迭代器模式封装了集合的内部结构,使得遍历过程更加简单、安全和灵活。

迭代器模式在Java中被广泛应用,例如在集合类(如List、Set)和Map类中都提供了迭代器来遍历元素。同时,我们也可以自定义实现迭代器接口来支持自定义的数据结构的遍历。

示例:

package cn.z3inc.list;

import org.junit.Test;

import java.util.*;

/**
 * List集合3种遍历方式
 *
 * @author 白豆五
 * @version 2023/8/7
 * @since JDK8
 */
public class ListNBForDemo1 {
    
    
    @Test
    public void testIterator() {
    
    
        // 1.创建list集合
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "c");
        
        // 2.获取list集合的迭代器对象
        Iterator<String> it = list.iterator();

        // 3.遍历
        // hasNext() 判断是否有下一个元素
        // next() 获取下一个元素
        while (it.hasNext()) {
    
    
            String item = it.next();
            System.out.println(item);
        }
    }
}

运行结果:

image-20230807222730158

  • list.iterator():获取list集合的迭代器对象
  • hasNext(): 判断是否有下一个元素
  • next(): 获取下一个元素

方式二: 使用增强For进行遍历(foreach)

使用foreach遍历数组的底层实现是普通的for循环,而使用foreach遍历集合的底层实现是迭代器。

public class ListNBForDemo2 {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "c");

        // 使用增强for遍历list集合
        for (String item : list) {
    
    
            System.out.println(item);
        }
        
		// 简写
        //list.forEach(item -> System.out.println(item));
        //list.forEach(System.out::println);
    }
}

运行结果:

image-20230807223845773

通过编译查看源码:javac -encoding utf-8 xxx.java

image-20230807224120183

image-20230807224305730

image-20230807224342613


方式三:for循环+get(索引)方式遍历

public class ListNBForDemo2 {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "c");

        for (int i = 0; i < list.size(); i++) {
    
    
            System.out.println(list.get(i));
        }
    }
}

7. ArrayList的扩容机制


在Java中,ArrayList 基于动态数组实现的,它可以根据需求自动扩容。当我们向ArrayList添加元素时,如果当前的容量不足以容纳新的元素时,ArrayList会自动进行扩容。

1、初始容量:

我们知道创建ArrayList对象时如果不指定长度,默认初始容量为10。

其实在创建一个空的ArrayList对象时,并没有立即初始化一个长度为10的数组,而是定义了一个空数组,直到我们第一次调用add方法添加元素时,才会初始化一个长度为10 的数组。

所以,当我们创建一个空的ArrayList对象时,并不会立即占用10个元素的空间,而是在真正需要存储元素时动态地进行扩容。

image-20230808213930418

2023-08-08 21 28 36

这种延迟初始化的方式可以节省内存空间,因为在创建ArrayList对象时,并不总是需要预先分配一个固定大小的数组。只有在真正需要存储元素时,才会进行数组的初始化和扩容操作。(如果已经确定容量大小,并且容量大于10个,可以调用ArrayList的第二个构造器,避免频繁扩容)


2、扩容过程:

当ArrayList的元素个数超过当前容量时,就需要进行扩容操作。ArrayList的扩容操作是通过一个名为grow的私有方法来实现的。

grow()方法首先计算新的容量大小,这个大小是旧容量的1.5倍(旧容量的一半再加上旧容量)。然后,通过Arrays.copyOf方法创建一个新的数组,并将旧数组中的元素复制到新数组中。

image-20230809093404148

// 扩容机制
private void grow(int minCapacity) {
    
    
    // 老容量长度
    int oldCapacity = elementData.length;
    // 新容量长度 = 老容量长度+(老容量长度/2),右移一位相当于除以2
    // 先扩容1.5倍,当新容量长度还是不够用,则直接使用所需要的长度minCapacity作为数组的长度。
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 数组拷贝
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 扩容时机:是在add方法中通过调用 ensureCapacityInternal() 方法来触发的,而且只有当我们添加的元素个数超过了当前的容量时,扩容操作才会进行。

  • 扩容性能:扩容操作会消耗一定的时间,需要频繁创建新数组,拷贝元素,销毁老数组。因此,如果我们事先知道要存储的元素个数,可以通过构造函数或者ensureCapacity方法来手动设置ArrayList的初始容量,避免频繁扩容。

ArrayList的扩容机制能够动态地适应数据量的变化,但是扩容操作会消耗一定的时间和资源。因此,在使用ArrayList时,我们应该尽量避免频繁的扩容,以提高性能和效率。


8. HashSet是如何保证数据唯一的?


在日常开发中,如果使用HashSet存储自定义对象,而且想要保证数据的唯一性,那么对象所属类必须重写equals()和hashCode()方法。

1、内部结构:

HashSet 底层基于 HashMap(也可以理解成哈希表) 来实现的,它实际上存储的是 HashMap 的 key。当我们把一个元素添加到 HashSet 中时,实际上是将这个元素作为 HashMap 的 key 存储,而 value 是一个固定的 Object 实例。

image-20230809174642439


2、hashcode()方法:

当我们向 HashSet 添加一个元素时,首先会调用这个元素的 hashCode() 方法来计算哈希值。这个哈希值决定了元素在HashSet内部存放的位置。如果该位置上没有哈希值相同的元素,那么就将该元素存储到HashSet中;如果该位置上有哈希值相同的元素,就会产生哈希冲突(哈希碰撞),那么该位置会采用链表或红黑树来管理具有相同哈希值的元素。


3、equals()方法:

  • 为了解决哈希冲突问题,会进一步调用元素的 equals() 方法和该位置上的所有元素进行比较。(比较对象是否相同)
  • 只有当 hashCode() 相同且 equals() 返回 true 时,HashSet 才会认为这两个对象是相同的,并且不会重复存储。

在这里插入图片描述


4、为什么这样设计:

  • 通过hashCode() 可以快速判断两个对象是否可能相同,因为计算哈希值相对较快,但它可能不精确。
  • 通过equals() 方法可以精确判断两个对象是否真正相同,但它的执行可能会相对较慢。(因为它需要进行更多的比较操作)

这样设计可以在大多数情况下快速定位元素,并且只有在发生哈希碰撞时才需要执行equals()方法进行更精确的比较。


示例:测试HashSet数据唯一性

package cn.z3inc.set;

import java.util.HashSet;
import java.util.Objects;

/**
 * 测试HashSet数据唯一性
 
 * @author 白豆五
 * @version 2023/8/10
 * @since JDK8
 */
public class HashSetDemo01 {
    
    
    public static void main(String[] args) {
    
    
        HashSet<Person> set = new HashSet<>();
        set.add(new Person("白豆五",18));
         //即使是两个不同的对象,但它们的内容相同,HashSet也会认为它们是相同的,不进行存储
        set.add(new Person("白豆五",18));
        System.out.println(set);
    }
}

class Person{
    
    
    private String name;
    private int age;

    public Person() {
    
    
    }

    public Person(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 boolean equals(Object o) {
    
     // 重写equals方法
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
    
     // 重写hashCode方法
        return Objects.hash(name, age);
    }

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

image-20230810110510476

注意事项:当我们重写 equals() 方法时,一定要重写 hashCode() 方法,以保持两者的契约。否则,当两个对象相等但哈希码不同时,HashSet 可能会存储两个相等的对象。


9. HashMap的实现原理?


1、HashMap的基本概念:

HashMap 底层是基于哈希表(也称散列表)来存储键值对的,不同JDK版本哈希表结构不太一样。HashMap能够在O(1)的时间复杂度下完成插入、删除和定位等操作。

HashMap继承关系:

image-20230810205439281

HashMap的特点:

  • 键具备哈希特性:哈希表,数组+链表/红黑树。
  • 查询速度非常快、增删速度也不慢。
  • 键要唯一:键所属的类要重写hashCode和equals方法,如果键重复了,旧值会被覆盖。
  • 键无序:不能保证存入和取出的顺序是一致的。
  • 键无索引:不能通过索引的方式获取键。
  • 允许存储null键和null值。(在使用时需要避免空指针异常)
  • 线程不同步,不安全,但效率高。

HashMap的常量:

/**
 * 缺省 table数组初始大小为16, 2的4次方
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * table数组的最大容量,1073741824 (10亿)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 缺省负载因子大小,0.75
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 树化阈值8
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 树降级成为链表的阈值6
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 树化的另一个参数,当table数组的长度大于64时,才会允许树化
 */
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap的成员变量:

/**
 * 哈希表(散列表)
 */
transient Node<K, V>[] table;

/**
 * 存放所有键值对的集合
 */
transient Set<Map.Entry<K, V>> entrySet;

/**
 * 哈希表中元素个数
 */
transient int size;

/**
 * 当前哈希表结构的修改次数(用于实现fail-fast机制)
 */
transient int modCount;

/**
 * 扩容阈值,当哈希表中元素个数大于threshold时,就会触发扩容(提高查找性能)
 * threshold = capacity(数组长度) * loadFactor(0.75)
 */
int threshold;

/**
 * 负载因子
 */
float loadFactor;

HashMap的构造方法:

/**
 * HashMap的有参构造方法
 *
 * @param initialCapacity 初始容量大小
 * @param loadFactor      负载因子
 */
public HashMap(int initialCapacity, float loadFactor) {
    
    
    // 这3个if判断都是校验参数的合法性
    // 如果初始容量小于0,则抛出异常
    if (initialCapacity < 0) {
    
    
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    }
    // 如果初始容量大于最大容量(10个亿那个数),则将初始容量设置为最大容量
    if (initialCapacity > MAXIMUM_CAPACITY) {
    
    
        initialCapacity = MAXIMUM_CAPACITY; // 限制数组最大长度为10个亿那个数
    }

    // 如果负载因子小于等于0或者负载因子是NaN ,则抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
    
    
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    }
    // 给负载因子、初始容量赋值
    this.loadFactor = loadFactor;
    // 返回一个扩容阈值,扩容阈值一定是2的次方数(如16、 32、 64、 128)
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * HashMap的有参构造方法
 *
 * @param initialCapacity 初始容量大小
 */
public HashMap(int initialCapacity) {
    
    
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


/**
 * HashMap的无参构造方法
 */
public HashMap() {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 给负载因子设置为0.75
}

猜你喜欢

转载自blog.csdn.net/qq_46921028/article/details/132152437