详解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集合性能对比总结

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

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

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

猜你喜欢

转载自blog.csdn.net/q251932440/article/details/142619802