当前位置: 首页 > article >正文

详解Java中的Collection单列集合(从底层到用法超详细解析和细节分析)

⭕在 Java 中,集合框架是开发过程中最常用的数据结构之一,其中 Collection 接口是整个集合框架的基础。Collection 是处理单列数据的接口,它定义了一些通用的操作,允许对一组对象进行操作。今天我们将深入介绍 Java 中的单列集合 Collection 接口,以及它的常见子接口和实现类。

在谈论集合时,我们都有一个问题,那就是数组和集合有什么区别?

相同点

  • 集合和数组都是容器,可以用来存储多个数据

不同点

  • 数组长度是不可变的,集合的长度是可以变的

  • 集合只能存储引用数据类型,如果要存储基本数据类型,需要存对应的包装类

  • 集合中集成了很多实用的数据处理方法,提供了功能各异的集合以达到提高程序运行效率的目的

想要深入了解Collection集合,我们就得先了解其在整个集合类的体系结构中的位置


一、集合体系结构图(Collection在集合体系中的位置)


二、创建Colletion对象的方式

由集合体系结构图可知Collection类是单列集合的顶层接口,所以不能通过它直接创建对象,我们需要用到其子接口的实现类或者通过多态的方式创建其对象。

2.1 多态的方式

2.2 具体实现类


三、Collection 核心(常用)方法

方法名称返回类型描述
boolean add(E e)boolean向集合中添加元素。如果集合因为此调用发生了变化,则返回 true
boolean remove(Object o)boolean从集合中删除指定元素。如果集合包含该元素并成功移除,则返回 true
boolean contains(Object o)boolean判断集合中是否包含指定的元素。如果集合包含此元素,则返回 true
int size()int返回集合中元素的数量。
Iterator<E> iterator()Iterator<E>返回一个用于遍历集合中元素的迭代器。
void clear()void清空集合中的所有元素。
boolean isEmpty()boolean判断集合是否为空。如果集合为空,则返回 true

3.1 add方法

向集合中添加元素。如果集合因为此调用发生了变化,则返回 true

细节:

  • 如果往List系列集合中添加元素永远返回true,因为List系列集合元素可重复
  • 如果往List系列集合中添加元素如果元素已经存在,则会返回false

3.2 remove方法

从集合中删除指定元素。如果集合包含该元素并成功移除,则返回 true,如果元素不存在则返回false

细节:因为Collection里面定义的是共性的方法(Set系列集合也要适用),所以此时不能通过索引进行删除,这里面具体涉及多态中方法重写和重载的区别,可以参考我另外一篇文章

在多态的方法调用中为什么会出现“左边编译左边运行”的现象icon-default.png?t=O83Ahttps://blog.csdn.net/q251932440/article/details/142509834?spm=1001.2014.3001.5501

3.3 clear方法

清空集合中的所有元素。

3.4 contains方法

细节:contains方法依赖equals方法进行判断,所以如果集合中存储自定义对象时,需要在JavaBean类中重写equals方法。

Student中重写的equals方法:

3.5 size方法

返回集合中元素的数量。

3.6 isEmpty方法

判断集合是否为空。如果集合为空,则返回 true

3.7 iterator方法

返回一个用于遍历集合中元素的迭代器。(在下文迭代器中介绍)


四、Collection集合的遍历

Collection集合中通用的遍历方法有三种(均不依赖索引):

  • 迭代器遍历
  • 增强for遍历
  • Lambda表达式遍历

4.1 迭代器遍历

迭代器:是集合专门用来遍历的工具。

在源码中可以看到,迭代器Iterator是一个接口类,所以不能直接创建其对象来使用,需要通过集合对象中的iterator()方法返回一个Iterator接口实现类的对象

1. 接口无法直接创建对象

接口无法直接实例化是因为它没有实现方法的具体细节。接口只是一个契约,定义了类必须实现的方法。比如,Iterator 接口定义了 hasNext()next()remove() 方法,但并没有提供这些方法的具体实现。

2. ArrayListiterator() 方法

当你调用 ArrayListiterator() 方法时,它内部实际上是返回了一个 匿名内部类具体的类 的实例,这个实例实现了 Iterator 接口。也就是说,虽然我们只看到了 Iterator 接口,但它实际是 ArrayList 内部某个私有类的对象,该类实现了 Iterator 接口的所有方法。

ArrayList 源代码中,iterator() 方法大概像这样实现:

public Iterator<E> iterator() {
    return new Itr();  // 返回一个实现了 Iterator 接口的类的实例
}

private class Itr implements Iterator<E> {
    // 实现 Iterator 接口的方法,如 hasNext(), next(), remove()
    public boolean hasNext() {
        // 具体实现
    }

    public E next() {
        // 具体实现
    }

    public void remove() {
        // 具体实现
    }
}

4.1.1 迭代器(Iterator)常用方法表格

方法名称返回类型描述
boolean hasNext()boolean判断集合中是否还有下一个元素。如果有,返回 true;否则返回 false。通常用于循环控制。
E next()E返回集合中的下一个元素,并将迭代器的指针移动到下一个元素。如果已经没有元素可返回,调用该方法会抛出 NoSuchElementException 异常。
void remove()void移除迭代器当前指向的元素(即最近一次调用 next() 方法返回的元素)。这是可选操作,如果集合不支持 remove() 方法,调用时会抛出异常。

4.1.2 示例1-遍历:

细节:迭代遍历结束后,指针不会复位,如果还想遍历则要新建一个迭代器

4.1.3 示例2-遍历的过程中删除元素

移除迭代器当前指向的元素(即最近一次调用 next() 方法返回的元素

如果要删除,用迭代器的remove方法,如果用集合的方法进行增加或者删除会报错

正确用法:

4.1.4 迭代器使用的注意点

  • 迭代器遍历完毕后,指针不会复位,如果还要继续遍历,需要新建一个迭代器
  • 指针处已经没有元素仍要执行next方法,系统会报错 'NoSuchElementException' (空元素异常)
  • 循环中,只能使用一次next方法,如果在一个循环中多次调用next方法,元素总数为奇数的时候也会有 'NoSuchElementException' (空元素异常)报错的风险
  • 迭代遍历的过程中(循环里)不能用集合的方法进行增删改查
  • 数组不能直接使用迭代器进行遍历,需要使用需转换为集合

4.2 增强for遍历

  • 增强for于JDK5后问世,其内部原理是一个Iterator迭代器
  • 所有单列集合和数组才能使用增强for进行遍历

作用:简化数组和集合的遍历,增加安全性

格式:

for(集合/数组中元素的数据类型 变量名 : 集合/数组名) {

// 已经将当前遍历到的元素封装到变量中了,直接使用变量即可

}

快捷:集合/数组.for 回车——自动生成增强for代码块

4.2.1 示例:

4.2.2 增强for注意点

增强for里面的Student student是一个第三方变量,student是其变量名,改变该变量值,不会影响集合中的数据

验证:

4.3 Lambda表达式遍历

利用forEach方法,结合Lambda表达式进行遍历(其底层是增强for)

作用:简化代码

4.3.1 示例:

使用forEach后,使用Lambda表达式前:

使用Lambda表达式后:

4.3.2 使用剖析 

通过查看源码可以知道,forEach方法需要传入的形参是一个Consumer接口类型的数据,而且是一个函数式接口。所以我们在传入参数的时候需要传入一个Consumer接口的实现类对象,因此采用了匿名内部类的方式创建其实现类对象并传入到方法中,然后根据Lambda表达式格式改写成Lambda表达式以简化代码。

4.3.3 注意:

修改student一样不会改变集合的值,和增强for一样(因为底层就是一个增强for)

4.4 三种遍历方法的使用场景


五、Collection子接口——List集合

5.1 List集合的特点

  • 存取有序(指的是存和取得顺序一样,跟排序不同
  • 可以重复
  • 有索引

List继承了Collection,但List仍然是一个接口类,如果想要创建List集合,则需要创建List接口得实现类对象 (ArrayList、LinkedList),例如:

5.2 List集合的特有方法

  • List继承了Collection,拥有Collection集合的所有方法
  • 因为List集合支持索引,索引新增了一些处理索引的方法方法介绍

方法列表:

方法名描述
void add(int index,E element)在此集合中的指定位置插入指定的元素
E remove(int index)删除指定索引处的元素,返回被删除的元素
E set(int index,E element)修改指定索引处的元素,返回被修改的元素
E get(int index)返回指定索引处的元素

5.2.1 add方法

在List集合中有两种add方法:

  • Collection中的add
  • void add(int index,E element)  在此集合中的指定位置插入指定的元素

这里重点分析第二种

代码示例:

打印结果:

由上述代码可知,使用add指定添加索引后,原本在该索引的元素会往后退一位让出索引,其后面的元素都会依次往后移动

5.2.2 remove方法

List集合中remove方法也有两种:

  • Collection中的remove
  • E remove(int index)  删除指定索引处的元素,返回被删除的元素

这里仍然重点分析第二种

代码示例:

标识代码块中的打印结果:

由上述代码可知,当使用remove删除一个元素的时候,被删除所在元素的位置就会空了,其后面的元素会自动依次向前移动

细节问题

当创建一个整数类型的集合时,采用remove方法删除会使用删除索引的remove方法还是使用删除元素的remove方法?

代码演示:

如果一定要使用其删除1这个元素,就需要进行手动装箱:

5.2.3 set方法

5.2.4 get方法

5.3 List集合遍历方式及对比

5.3.1 迭代器遍历

5.3.2 列表迭代器

往前迭代那个方法有局限性,因为迭代器一开始默认实在0索引处的(基本不用)

遍历过程中添加元素:

5.3.3 增强for

5.3.4 Lambda表达式

5.3.5 普通for循环

5.4 List的实现类——LinkedList

5.4.1 底层核心步骤

  1. 刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null

  2. 添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值

  3. 添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值

LinkedList集合底层是链表结构实现的,查询慢,增删快

5.4.2 特有方法

但是如果操作首尾元素,速度也是非常快的,所以LinkedList多了一些操作首尾元素的方法:

方法名说明
public void addFirst(E e)在该列表开头插入指定的元素
public void addLast(E e)将指定的元素追加到此列表的末尾
public E getFirst()返回此列表中的第一个元素
public E getLast()返回此列表中的最后一个元素
public E removeFirst()从此列表中删除并返回第一个元素
public E removeLast()从此列表中删除并返回最后一个元素

但是这些方法用得比较少,在Collection和List的方法中也基本能实现。

5.5 List的实现类——ArrayList

5.5.1 底层核心步骤:

  • 创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。
    • 数组名字:elementDate,定义变量size。
  • size这个变量有两层含义(添加元素,添加完毕后,size++):
    • ①:元素的个数,也就是集合的长度
    • ②:下一个元素的存入位置

当添加第一个元素的时候,底层会创建一个新的长度为10的数组

  • 扩容时机一:
    • 当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15.再把所有的元素,全拷贝到新数组中。
    • 如果继续添加数据,这个长度为15的数组也满了,那么下次还会继续扩容,还是1.5倍。
  • 扩容时机二:
    • 如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。

六、Collection子接口——Set集合

Set中的方法基本和Collection中的方法一致

6.1 Set集合的特点

  • 数据存取顺序不一致(LinkedHashSet除外)
  • 不可存储重复元素(可以利用这个特点来对数据去重)
    • add方法的返回值在Set中奏效了,如果重复的元素添加进集合,会添加失败并返回false
  • 没有索引(遍历时不能使用普通for)

6.1.1 Set实现类与子实现类的特点

6.2 Set集合遍历方式

与Collection一样:

  • 迭代器
  • 增强for
  • Lambda表达式

6.2.1 迭代器遍历

6.2.2 增强for

6.2.3 Lambda表达式

6.3 Set的实现类——HashSet

6.3.1 特点

  • 底层数据结构是哈希表(JDK8以后由数组+链表+红黑树组成) 
    • 默认加载因子:填充的元素达到数组长度的75%就扩容一倍
  • 存取无序
  • 不可以存储重复元素
  • 没有索引 
  • 增删改查的性能都很好

6.3.2 哈希值

JDK根据对象的地址或者字符串或者数字算出来的int类型的数值

⭕哈希值的获取

Object类中的public int hashCode():返回对象的哈希值

⭕哈希值的特点
  • 同一个对象多次调用hashCode()方法返回的哈希值是相同的

  • 默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象而属性值相同的哈希值相同String等java已经定义好的对象已经重写了该方法

  • 在小部分情况下,不同属性值或者不同地址值计算出来的哈希值也有可能一样(哈希碰撞)

    • 因此为了避免哈希碰撞导致部分元素丢失,通常也要重写自定义对象(String等java已经定义好的对象已经重写了该方法)中的equals方法

      • 原因:这样一来,即使哈希值相同的元素还会被进一步被equals方法确认是否为重复元素,放防止直接被丢掉不存。

6.3.3 代码示例

学生类:
package test01;

import java.util.Objects;

public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
测试类:
package test01;

import java.util.HashSet;
import java.util.Set;

public class SetTest01 {
    public static void main(String[] args) {
        Set<Student> set1 = new HashSet<>();
        Student s1 = new Student("zhangsan",11);
        Student s2 = new Student("lisi",13);
        Student s3 = new Student("wangwu",12);

        set1.add(s1);
        set1.add(s2);
        set1.add(s3);

        set1.forEach(student -> System.out.println(student));
    }
}

6.3.4 HashSet子类——LinkedHashSet

LinkedHashSet与HashSet不同的是:LinkedHashSet集合存取元素是有序的

为什么存取有序?

LinkedHashSet其底层数据结构依然是哈希表,只是每个元素又额外增加了一个双链表机制记录存储的顺序。

通过以下代码展示,我们可以知道LinkedHashSet集合存取元素是有序的

如果以后数据要去重,我们使用哪一个集合?

6.4 Set的实现类——TreeSet

6.4.1 特点

  • 可以将元素按照规则排序(以下必选其一,不然获取集合会报错)
    • 实现Comparable接口进行自然排序
    • 使用比较排序器Comparator
  • 不可存储重复元素(依赖上述两种比较方法实现的,因此不需要类重写HashCode和equals方法
  • 没有索引
  • 底层数据结构为红黑树

6.4.2 在没有排序时的TreeSet集合:

当我们添加自定义对象进集合后,TreeSet不知道排序规则,会出现如下报错(类转换异常

如果我们添加整数类型的对象进TreeSet中,会得到以下结果,其数据从小到大排列起来了

那为什么会出现这种状况呢?TreeSet的排序规则是什么呢?

6.4.3 TreeSet的默认排序规则

TreeSet的排序都是要实现Comparable接口进行自然排序或者使用比较排序器Comparator来进行的,之所以java中的一些定义好的数据类如Integer、String等能够直接排序(有默认排序规则)是因为java已经在这些类上实现了Comparable接口。

Integer源码

  • 对于数值类型(如Integer、Double):默认按照从小到大顺序进行排序
  • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序(从小到大)
    • 剖析字符串排序:
  • 自定义对象,需要实现Comparable接口进行自然排序或者使用比较排序器Comparator

那么接下来我们就开始介绍这两种排序方法。

6.4.4 TreeSet的排序方法

⭕自然排序Comparable接口的使用 
实现步骤
  1. 使用空参构造创建TreeSet集合

    用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
  2. 自定义的Student类实现Comparable接口

    自然排序,就是让元素所属的类实现Comparable接口,指定要对比的类型,重写compareTo(T o)方法
  3. 重写接口中的compareTo方法(指定排序规则

    重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
代码示例:
  • 存储学生对象并遍历,创建TreeSet集合使用无参构造方法

  • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序

学生类:

package test01;

import java.util.Objects;

public class Student implements Comparable<Student>{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }

    @Override
    public int compareTo(Student o) {
        //大于0说明this.age大,排在后面
        int result = this.age - o.age;
        //this.name.compareTo(o.name),因为this.name是一个字符串对象
        //调用字符串对象可以用到字符串类型中定义好的compareTo方法
        result = result == 0 ? this.name.compareTo(o.name) : result;
        return result;
    }
}

测试类:

package test01;

import java.util.Set;
import java.util.TreeSet;

public class TreeSetTest01 {
    public static void main(String[] args) {
        Set<Student> t1 = new TreeSet<>();
        Student s1 = new Student("zhangsan",11);
        Student s2 = new Student("lisi",13);
        Student s3 = new Student("wangwu",11);
        Student s4 = new Student("linchuqiao",21);

        t1.add(s1);
        t1.add(s2);
        t1.add(s3);
        t1.add(s4);

        for (Student s : t1) {
            System.out.println(s);
        }
    }
}

运行结果:

⭕比较排序器Comparator的使用

Comparator也是一个接口,但是需要在带参构造方法使用

实现步骤
  • 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的

  • 比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法

  • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写

代码示例:

要修改默认排序,先对字符串长度排序

package test01;

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetTest02 {
    public static void main(String[] args) {
        Set<String> set = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                int result = o1.length() - o2.length();
                return result == 0 ? o1.compareTo(o2) : result;
            }
        });

        set.add("guangzhou");
        set.add("chongqing");
        set.add("beijing");
        set.add("shanghai");

        for (String s : set) {
            System.out.println(s);
        }
    }
}

运行结果:

String原先实现了Comparable接口,但是为了修改默认排序采用了Comparator比较器,由此可知第二种和第一种方法同时存在的话会遵循第二种方法(Comparator比较器)。

⭕两种排序方法总结
  • 自然排序: 自定义类实现Comparable接口,重写compareTo方法,根据返回值进行排序

  • 比较器排序: 创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序

  • 在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序(一般是需要修改已经在源码中写好的排序规则,如String等类型的对象

两种方式中关于返回值的规则:

  • 如果返回值为负数,表示当前存入的元素是较小值,存左边

  • 如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存

  • 如果返回值为正数,表示当前存入的元素是较大值,存右边


七、全文总结

Collection 接口及其子接口 ListSet 构成了 Java 集合框架的核心部分,它们通过不同的实现类为我们提供了灵活的方式来处理数据。理解各个集合类的特点和适用场景,能够帮助开发者高效地组织和操作数据。

7.1 Collection集合性能对比总结

操作ArrayListLinkedListHashSetTreeSet
添加元素较快较快(首尾)较慢(排序)
删除元素较慢较快(首尾)较慢
查找元素较慢(首尾快)较慢
元素存取顺序有序有序无序可排序

在实际开发中,选择合适的集合类是非常重要的。对于需要频繁查找、插入和删除的场景,应根据数据结构的特点选择合适的实现类。

通过本文的介绍,你应该能够初步掌握 Java 中 Collection 单列集合的基础知识,并且学会如何选择适合的集合类进行开发。在实际项目中,合理使用集合将有助于提升代码的性能和可维护性噢!~


http://www.kler.cn/news/329505.html

相关文章:

  • SpringBoot3脚手架
  • NodeJS下载、安装及环境配置教程,内容详实
  • 【PostgreSQL】入门篇——介绍表的创建、主键、外键、唯一约束和检查约束的概念及其应用
  • 基于元神操作系统实现NTFS文件操作(一)
  • 【GESP】C++一级练习BCQM3022,输入-计算-输出-3
  • C#中Socket通信常用的方法
  • (k8s)kubernetes中ConfigMap和Secret
  • MySQl查询分析工具 EXPLAIN ANALYZE
  • MongoDB的查询/超详细
  • Python基础语句教学
  • 基于SpringBoot+Vue+MySQL的旅游管理系统
  • pytorch数据读入
  • 常用设计模式之单例模式、策略模式、工厂模式
  • TCP三次握手四次挥手详解
  • HTML5--裸体回顾
  • testRigor测试用例模板记录
  • 从AR眼镜到智能巡检:XR技术的演变与未来潜力
  • 华为仓颉语言入门(7):深入理解 do-while 循环及其应用
  • 利用Java easyExcel库实现高效Excel数据处理
  • mysql学习教程,从入门到精通,SQL GROUP BY 子句(31)
  • 一起了解计算机神经网络
  • 【Linux】第一个小程序——进度条实现
  • 数据分析-29-基于pandas的窗口操作和对JSON格式数据的处理
  • 解决Github打不开或速度慢的问题
  • 职业技能大赛-单元测试笔记(参数化)分享
  • OpenHarmony(鸿蒙南向)——平台驱动指南【DAC】
  • 【floor报错注入】
  • 《深度学习》自然语言处理 统计、神经语言模型 结构、推导解析
  • 【css】如何设计出具有权威性的“机构”网页
  • OpenAI 推理模型 O1 研发历程:团队访谈背后的故事