Java集合之List集合(下)

简介: Java集合之List集合(上)

Java集合之List集合(上)https://developer.aliyun.com/article/1386301

代码证明:

public static void main(String[] args) throws Exception {
    List list = new ArrayList();
    for (int i = 0; i < 100; i++) {
      System.out.print(i + "==");
      list.add(i);
      elementDataLength(list); // 调用 elementDataLength 方法输出当前容量
    }
  }
  /**
   * 定义一个名为 "elementDataLength" 的方法,该方法用于使用反射获取 ArrayList 中存储元素的底层数组的长度并输出
   * 
   */
  public static void elementDataLength(List list) throws Exception {
    // 使用反射获取 ArrayList 对象的成员变量 elementData
    Field ed = list.getClass().getDeclaredField("elementData");
    ed.setAccessible(true); // 设置允许访问私有变量
    Object[] o = (Object[]) ed.get(list); // 获取 ArrayList 中存储元素的底层数组对象的引用
    // 输出 ArrayList 的容量信息,即底层数组的长度
    System.out.println("当前List集合容量为:" + o.length);
  }
//输出的内容:
0==当前List集合容量为:10
1==当前List集合容量为:10
2==当前List集合容量为:10
3==当前List集合容量为:10
4==当前List集合容量为:10
5==当前List集合容量为:10
6==当前List集合容量为:10
7==当前List集合容量为:10
8==当前List集合容量为:10
9==当前List集合容量为:10
10==当前List集合容量为:15
11==当前List集合容量为:15
12==当前List集合容量为:15
13==当前List集合容量为:15
14==当前List集合容量为:15
15==当前List集合容量为:22
16==当前List集合容量为:22
17==当前List集合容量为:22
18==当前List集合容量为:22
19==当前List集合容量为:22
20==当前List集合容量为:22
21==当前List集合容量为:22
22==当前List集合容量为:33
23==当前List集合容量为:33
24==当前List集合容量为:33
25==当前List集合容量为:33
26==当前List集合容量为:33
27==当前List集合容量为:33
28==当前List集合容量为:33
29==当前List集合容量为:33
30==当前List集合容量为:33
31==当前List集合容量为:33
32==当前List集合容量为:33
33==当前List集合容量为:49
34==当前List集合容量为:49
35==当前List集合容量为:49
36==当前List集合容量为:49
37==当前List集合容量为:49
38==当前List集合容量为:49
39==当前List集合容量为:49
40==当前List集合容量为:49
41==当前List集合容量为:49
42==当前List集合容量为:49
43==当前List集合容量为:49
44==当前List集合容量为:49
45==当前List集合容量为:49
46==当前List集合容量为:49
47==当前List集合容量为:49
48==当前List集合容量为:49
49==当前List集合容量为:73
50==当前List集合容量为:73
51==当前List集合容量为:73
52==当前List集合容量为:73
53==当前List集合容量为:73
54==当前List集合容量为:73
55==当前List集合容量为:73
56==当前List集合容量为:73
57==当前List集合容量为:73
58==当前List集合容量为:73
59==当前List集合容量为:73
60==当前List集合容量为:73
61==当前List集合容量为:73
62==当前List集合容量为:73
63==当前List集合容量为:73
64==当前List集合容量为:73
65==当前List集合容量为:73
66==当前List集合容量为:73
67==当前List集合容量为:73
68==当前List集合容量为:73
69==当前List集合容量为:73
70==当前List集合容量为:73
71==当前List集合容量为:73
72==当前List集合容量为:73
73==当前List集合容量为:109
74==当前List集合容量为:109
75==当前List集合容量为:109
76==当前List集合容量为:109
77==当前List集合容量为:109
78==当前List集合容量为:109
79==当前List集合容量为:109
80==当前List集合容量为:109
81==当前List集合容量为:109
82==当前List集合容量为:109
83==当前List集合容量为:109
84==当前List集合容量为:109
85==当前List集合容量为:109
86==当前List集合容量为:109
87==当前List集合容量为:109
88==当前List集合容量为:109
89==当前List集合容量为:109
90==当前List集合容量为:109
91==当前List集合容量为:109
92==当前List集合容量为:109
93==当前List集合容量为:109
94==当前List集合容量为:109
95==当前List集合容量为:109
96==当前List集合容量为:109
97==当前List集合容量为:109
98==当前List集合容量为:109
99==当前List集合容量为:109

我们可以看到内部容量不足时,ArrayList会将原数组长度乘以1.5进行扩容。

注意,由于 ArrayList 底层使用的是数组,因此一旦创建了 ArrayList,它的大小就是固定的。而当元素添加到 ArrayList 中时,如果底层数组已满,则需要创建一个更大的数组,并将原有元素复制到新数组中,这会带来一定的性能损耗。

因此,在实际开发中,建议在创建 ArrayList 时设置一个合适的初始化容量,避免在运行时频繁进行扩容操作,同时也可避免浪费过多空间资源。

拿以上例子稍作修改,将ArrayList容量改为50:

public static void main(String[] args) throws Exception {
    List list = new ArrayList(50);
    for (int i = 0; i < 100; i++) {
      System.out.print(i + "==");
      list.add(i);
      elementDataLength(list); // 调用 elementDataLength 方法输出当前容量
    }
  }
  /**
   * 定义一个名为 "elementDataLength" 的方法,该方法用于使用反射获取 ArrayList 中存储元素的底层数组的长度并输出
   * 
   */
  public static void elementDataLength(List list) throws Exception {
    // 使用反射获取 ArrayList 对象的成员变量 elementData
    Field ed = list.getClass().getDeclaredField("elementData");
    ed.setAccessible(true); // 设置允许访问私有变量
    Object[] o = (Object[]) ed.get(list); // 获取 ArrayList 中存储元素的底层数组对象的引用
    // 输出 ArrayList 的容量信息,即底层数组的长度
    System.out.println("当前List集合容量为:" + o.length);
  }
//输出的内容:
0==当前List集合容量为:50
1==当前List集合容量为:50
2==当前List集合容量为:50
3==当前List集合容量为:50
4==当前List集合容量为:50
5==当前List集合容量为:50
6==当前List集合容量为:50
7==当前List集合容量为:50
8==当前List集合容量为:50
9==当前List集合容量为:50
10==当前List集合容量为:50
11==当前List集合容量为:50
12==当前List集合容量为:50
13==当前List集合容量为:50
14==当前List集合容量为:50
15==当前List集合容量为:50
16==当前List集合容量为:50
17==当前List集合容量为:50
18==当前List集合容量为:50
19==当前List集合容量为:50
20==当前List集合容量为:50
21==当前List集合容量为:50
22==当前List集合容量为:50
23==当前List集合容量为:50
24==当前List集合容量为:50
25==当前List集合容量为:50
26==当前List集合容量为:50
27==当前List集合容量为:50
28==当前List集合容量为:50
29==当前List集合容量为:50
30==当前List集合容量为:50
31==当前List集合容量为:50
32==当前List集合容量为:50
33==当前List集合容量为:50
34==当前List集合容量为:50
35==当前List集合容量为:50
36==当前List集合容量为:50
37==当前List集合容量为:50
38==当前List集合容量为:50
39==当前List集合容量为:50
40==当前List集合容量为:50
41==当前List集合容量为:50
42==当前List集合容量为:50
43==当前List集合容量为:50
44==当前List集合容量为:50
45==当前List集合容量为:50
46==当前List集合容量为:50
47==当前List集合容量为:50
48==当前List集合容量为:50
49==当前List集合容量为:50
50==当前List集合容量为:75
51==当前List集合容量为:75
52==当前List集合容量为:75
53==当前List集合容量为:75
54==当前List集合容量为:75
55==当前List集合容量为:75
56==当前List集合容量为:75
57==当前List集合容量为:75
58==当前List集合容量为:75
59==当前List集合容量为:75
60==当前List集合容量为:75
61==当前List集合容量为:75
62==当前List集合容量为:75
63==当前List集合容量为:75
64==当前List集合容量为:75
65==当前List集合容量为:75
66==当前List集合容量为:75
67==当前List集合容量为:75
68==当前List集合容量为:75
69==当前List集合容量为:75
70==当前List集合容量为:75
71==当前List集合容量为:75
72==当前List集合容量为:75
73==当前List集合容量为:75
74==当前List集合容量为:75
75==当前List集合容量为:112
76==当前List集合容量为:112
77==当前List集合容量为:112
78==当前List集合容量为:112
79==当前List集合容量为:112
80==当前List集合容量为:112
81==当前List集合容量为:112
82==当前List集合容量为:112
83==当前List集合容量为:112
84==当前List集合容量为:112
85==当前List集合容量为:112
86==当前List集合容量为:112
87==当前List集合容量为:112
88==当前List集合容量为:112
89==当前List集合容量为:112
90==当前List集合容量为:112
91==当前List集合容量为:112
92==当前List集合容量为:112
93==当前List集合容量为:112
94==当前List集合容量为:112
95==当前List集合容量为:112
96==当前List集合容量为:112
97==当前List集合容量为:112
98==当前List集合容量为:112
99==当前List集合容量为:112

我们可以看到这次只扩容了两次,从50-75、从75-112,这大大减少了性能损耗,所以在创建 ArrayList 时设置一个合适的初始化容量是非常重要的。

4.5 ArrayList去重

4.5.1字符串去重

使用contains方法

public static void main(String[] args) {
          List list = new ArrayList();
          list.add("a");
          list.add("b");
          list.add("c");
          System.out.println("目前集合容器中的元素:"+list);//目前集合容器中的元素:[a, b, c]
          if (!list.contains("b")){//如果不包含“b”
              list.add("b");//才增加
          }
          System.out.println("目前集合容器中的元素:"+list);//目前集合容器中的元素:[a, b, c]
      }
}

4.5.2对象去重

public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Student("zs",16));
        list.add(new Student("ls",17));
        list.add(new Student("ww",18));
        System.out.println("目前集合容器中的元素:"+list);
        //目前集合容器中的元素:[com.xqx.demo.Student@33909752, com.xqx.demo.Student@55f96302, com.xqx.demo.Student@3d4eac69]
        if (!list.contains(new Student("ls",17))){//不包含才新增
            list.add(new Student("ls",17));
        }
        System.out.println("目前集合容器中的元素:"+list);
        //目前集合容器中的元素:[com.xqx.demo.Student@33909752, com.xqx.demo.Student@55f96302, com.xqx.demo.Student@3d4eac69, com.xqx.demo.Student@42a57993]
        if (list.contains(new Student("ls",17))){//包含才移除
            list.remove(new Student("ls",17));
        }
        System.out.println("目前集合容器中的元素:"+list);
        //目前集合容器中的元素:[com.xqx.demo.Student@33909752, com.xqx.demo.Student@55f96302, com.xqx.demo.Student@3d4eac69, com.xqx.demo.Student@42a57993]
    }
}
class Student{
    private String name;
    private int age;
    public Student(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;
    }
为什么 student(‘ls’,17) 并没有新增与移除?

由于在 Java 中,比较对象是否相等是基于对象的地址进行的,而不是基于对象的属性值进行的。而实例化后,会新开一个地址。所以即使两个student的属性值相等,但实例化后地址不同,那么这两个对象就也不是相等的。

解决方案

重写equals方法即可

public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Student("zs",16));
        list.add(new Student("ls",17));
        list.add(new Student("ww",18));
        System.out.println("目前集合容器中的元素:"+list);
  //目前集合容器中的元素:[com.xqx.demo.Student@33909752, com.xqx.demo.Student@55f96302, com.xqx.demo.Student@3d4eac69]
        if (!list.contains(new Student("ls",17))){//不包含才新增
            list.add(new Student("ls",17));
        }
        System.out.println("目前集合容器中的元素:"+list);
        //目前集合容器中的元素:[com.xqx.demo.Student@33909752, com.xqx.demo.Student@55f96302, com.xqx.demo.Student@3d4eac69]
        if (list.contains(new Student("ls",17))){//包含才移除
            list.remove(new Student("ls",17));
        }
        System.out.println("目前集合容器中的元素:"+list);
        //目前集合容器中的元素:[com.xqx.demo.Student@33909752, com.xqx.demo.Student@3d4eac69]
    }
}
class Student{
    private String name;
    private int age;
    public Student(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) {
        System.out.println("调用了equals方法。。。");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && name.equals(student.name);
    }

在重写的equals方法中,我们定义了只有姓名和年龄都相同时,才认为两个学生相等。因此,当通过新建一个Student对象来判断是否存在于ArrayList中时,即使该对象的内存地址不同,只要该对象的姓名和年龄与列表中某个元素的姓名和年龄相等,就会被视为存在于ArrayList中,从而避免了重复添加元素。而在删除元素时,由于通过该对象的equals方法判断该对象是否在ArrayList中存在,因此也可以正常进行删除。

总结

Java中的引用类型在判断相等性时,是根据对象在内存中的地址进行比较的。如果不重写equals方法,那么,由于ArrayList中存储的是对象的引用,所以相当于比较的是每个对象在内存中的地址是否相同,只有两个对象的地址相同时才会被视为相等。而我们通常认为,只有两个对象的属性值相同时才能被视为相等。因此,我们需要根据对象的实际比较规则重写equals方法。

五、LinkedList

5.1 LinkedList概述

5.1.1概念

LinkedList也是Java中的一个常用的集合类,实现了List接口,底层使用的是双向链表数据结构

与ArrayList不同,LinkedList在内部存储元素时,不是使用连续的内存空间,而是使用一个链表来存储元素。

5.1.2数据结构

LinkedList底层采用的是双向链表(doubly linked list) 数据结构。链表中的每个节点(结点)都由两个部分组成,一部分是存储数据元素的值域,另一部分是指向前一个节点和后一个节点的指针(引用)。对于双向链表来说,除了一个指向前一个节点的指针外,还有一个指向后一个节点的指针

5.2LinkedList的特点

5.2.1随机访问性能较差:

LinkedList的随机访问性能较差,因为在链表中要从头开始遍历链表,直到找到目标元素。所以如果在代码中需要频繁进行随机访问元素的操作,LinkedList可能不是一个最佳的选择。

5.2.2添加/删除操作快:

由于LinkedList底层使用双向链表,因此它的添加和删除操作非常快,因为只需要更改指针的指向即可,不需要像ArrayList一样重新分配数组空间,而且LinkedList还支持在指定位置插入和删除元素。

5.2.3需要额外空间:

链表中每个节点都需要额外存储到前一个和后一个节点的指针,因此比数组等其他数据结构需要更多的内存空间。

5.2.4适用于队列和双端队列:

LinkedList还可以支持队列和双端队列的功能,如在链表头部或尾部添加或删除元素,实现队列和双端队列的常见操作。

  • 双端队列(Deque,即Double Ended Queue的缩写)是一种允许在队列的两端进行插入和删除操作的数据结构。双端队列可以从队列的头部和尾部添加和移除元素。

5.3常用方法

方法名 说明
addFirst(E element) 将元素添加到列表的开头
getFirst(): 返回列表的第一个元素。
getLast(): 返回列表的最后一个元素。
removeFirst(): 删除并返回列表的第一个元素。
removeLast(): 删除并返回列表的最后一个元素。

5.4堆栈和队列

5.4.1堆栈

堆栈(Stack)是一种具有后进先出特性的数据结构。在堆栈结构中,新的元素总是被添加到栈顶(也就是最新元素),而只有在栈顶的元素才可以被移除。

堆栈最典型的例子是计算器的运算操作,先输入的操作数最后被计算,而最后输入的操作数最先被计算。对于堆栈结构,我们可以在程序中使用push(添加)和pop(移除)方法来实现后进先出的存储和读取数据。

5.4.2队列

队列(Queue)是一种具有先进先出特性的数据结构。在队列结构中,新的元素总是从队列的末尾添加到队列中,然后从队列的开头进行访问或删除。

队列最典型的例子是排队。排队的人总是按先来后到的顺序排队,当前面的人离开队列之后,后面的人才能够进入。对于队列结构,我们可以使用offer()方法将新的元素添加到队尾,使用poll()方法从队列开头移除元素。

5.5常见面试题:用LinkedList完成一个堆栈容器

package com.xqx.demo;
import java.util.LinkedList;
public class demo3 {
    public static void main(String[] args) {
      //创建集合
        LinkedList ll = new LinkedList();
        //添加元素
        ll.add("lisi");
        ll.add("zs");
        ll.add("ww");
        //实例化堆栈
        DuiZhan DuiZhan = new DuiZhan(ll);
        System.out.println(DuiZhan.pop());
        System.out.println(DuiZhan.pop());
        System.out.println(DuiZhan.pop());
    }
}
class DuiZhan{
    LinkedList ll = null;
    public DuiZhan(LinkedList ll){// 堆栈的构造函数,接受一个 LinkedList 对象作为参数,用于存储堆栈元素
        this.ll = ll;
    }
    //压栈
    public void add(String a){
        ll.add(a);
    }
    //弹栈
    public String pop(){
        return (String) ll.removeLast(); 从链表末尾移除一个元素并返回其值
    }
}

考察核心

  • 考察LinkedList的api方法
  • 考察堆栈/队列的数据结构特点

六、ArrayList和LinkedList的比较

  • 由于ArrayList的数据结构为数组,所以查询修改快,新增删除慢;而LinkedList的数据结构为链表结构,所以查询修改慢,新增删除快
  • ArrayList是基于数组实现的动态数组,在内存中有连续的空间,可以通过下标访问元素,由于数组需要提前分配一定大小的空间,因此当元素数量增多之后,可能会导致数组空间不足需要重新分配数组,这种情况下可能会出现内存空间浪费;相比之下,LinkedList是基于链表实现的,每个元素都有一个引用指向下一个元素,不需要提前分配空间,因此能够更加灵活地插入和删除元素。然而,链表在内存中是不连续的,每个元素的引用占用额外的内存空间。由于链表中每个元素都需要有一个指向下一个元素的引用,因此在存储同样数量的元素时,LinkedList通常会占用比ArrayList更大的内存空间

好啦,今天的分享就到此为止!希望你看完本篇文章有所收获,祝你变得更强!!!

目录
相关文章
|
29天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
40 6
|
29天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
38 3
|
29天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
9天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
18 2
|
9天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
13天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
13天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
13天前
|
Java 开发者
|
26天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
53 5
|
24天前
|
Java 程序员 编译器
Java|如何正确地在遍历 List 时删除元素
从源码分析如何正确地在遍历 List 时删除元素。为什么有的写法会导致异常,而另一些不会。
18 3

热门文章

最新文章