24-08-04 JavaSE java集合详解

24-08-04 JavaSE 集合详解

理解集合

传统的数组每次都要提前设置好容量,而且从某些方面来讲只能存储同一类型的元素,如果存储大小不够的话只能重新开辟一个新数组然后把元素移动到新数组中,非常麻烦

public class test01 {
    public static void main(String[] args) {
        Integer[] integers = new Integer[1];
        integers[0] = new Integer(1);
        Integer[] integers1 = new Integer[integers.length + 1];
        for (int i = 0; i < integers.length; i++) {
            integers1[i] = integers[i];
        }
    }
}

那么集合的作用是什么呢,集合有如下好处:

  1. 可以动态保存任意多个对象

  2. 提供一系列方便的操作对象的方法

  3. 使用集合添加,删除新元素的代码简洁明了

java集合的体系框架

java集合类分为两大类,一个是单列集合Collection,一个是双列集合Map,下面是它们的继承关系图。

image-20240804120635977

image-20240804120940775

Collection类

Collection接口的常用方法

由于collection接口不能被实例化,因此用它的子类ArrayList来演示。

public class CollectionMethod {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        //添加元素
        list.add("hx");
        list.add(10);
        list.add(true);
        System.out.println("list=" + list);
        //删除元素
        list.remove("hx");
        list.remove(0);
        System.out.println("list=" + list);
        //查询元素
        System.out.println(list.contains(true));
        //返回集合大小
        System.out.println(list.size());
        //判断集合是否为空
        System.out.println(list.isEmpty());
        //清空集合
        list.clear();
        System.out.println("list=" + list);
        //在集合中添加另外一个集合
        List list2 = new ArrayList();
        list2.add("hxx");
        list2.add(2);
        list.addAll(list2);
        System.out.println("list=" + list);
        //在集合中查询是否存在某一集合
        System.out.println(list.containsAll(list2));
        //在集合中删除在某一集合中存在的元素
        list.removeAll(list2);
        System.out.println("list=" + list);
    }
}

list=[hx, 10, true]
list=[true]
true
1
false
list=[]
list=[hxx, 2]
true
list=[]

list方法在使用的过程中可以任意添加与删除不同类型的对象,同时可以支持一次插入多个,删除多个,查找多个元素的集合,这是数组没有的特点。

集合的遍历

iterator迭代器

我们可以通过集合的iterator()方法返回一个iterator类对象,使用这个对象的hasnext方法与next方法可以实现对集合的遍历,其中,hasnext方法是检查迭代器指向的位置还有没有元素,next方法负责下移并返回这个元素,搭配while循环即可实现对集合的遍历。

public class CollectionIterator {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        Collection collection = new ArrayList();
        collection.add(new Person("小明",20));
        collection.add(new Person("小红",20));
        collection.add(new Person("小芳",20));
        Iterator iterator = collection.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }

    }
}
class Person {
    private String name;
    private int age;

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

image-20240804131858504

注意,通过快捷键itit即可快速生成while循环部分,此外,如果我们在while循环以后继续使用next方法,编译器会抛出一个异常,因为此时集合后面已经没有元素了。

image-20240804132052519

因此,next方法一定要在hasnext方法后面使用,也就是只有经过检查后面还有元素才能进行后移并返回元素。

那么如果我们要让迭代器现在重新指向开头该怎么办呢,其实很简单,只需要

 iterator = collection.iterator();

也就是为迭代器再返回一遍iterator对象,这样就可以重新遍历一次了。

增强for循环

遍历的第二种方法我们可以使用一种名叫增强for循环的方法。

public class CollectionFor {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        Collection collection = new ArrayList();
        collection.add(new Person("小明",20));
        collection.add(new Person("小红",20));
        collection.add(new Person("小芳",20));
        for (Object object : collection) {
            System.out.println(object);
        }

    }
}

其实增强for循环的底层使用的还是iterator的那一套hasnext与next方法,这些通过调试我们都可以发现,还有这个增强for循环不止可以用在集合中,还可以用在普通数组中,它的快捷键是I。

List类

List类的常用方法

在理解list方法前,我们要先明白list集合的一些特性:

  1. list集合类中的元素是有序的,即添加顺序与取出顺序一致,并且可以重复
  2. list集合中每个元素都有自己的索引
  3. list接口是Collection的子接口,collection有的方法它也有,但是set的方法它不一定有。
  4. list接口有许多具体实现的类,但是我们常用的list实现类有ArrayList,LinkList,Vector这三类

下面是list的一些常用方法

public class ListMethod {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("红色");
        list.add(true);
        list.add(6);
        list.add('a');
        System.out.println(list);
        //在index位置插入元素
        list.add(0,"在0位置新插入的元素");
        System.out.println(list);
        List list1 = new ArrayList();
        list1.add("1号");
        list1.add("2号");
        //在index位置插入集合
        list.addAll(0,list1);
        System.out.println(list);
        //获取指定位置元素
        Object object = list.get(2);
        System.out.println(object);
        //返回对象首次出现的位置
        int i = list.indexOf(true);
        System.out.println(i);
        //返回对象最后一次出现的位置
        list.add(true);
        int i1 = list.lastIndexOf(true);
        System.out.println(i1);
        //移除指定位置元素并返回此元素
        Object remove = list.remove(2);
        System.out.println(remove);
        System.out.println(list);
        //替换index位置元素
        Object set = list.set(2, false);
        System.out.println(set);
        System.out.println(list);
        //返回从fromindex-toindex位置的子集合
        List list2 = list.subList(2, 4);
        System.out.println(list2);
    }
}

image-20240804140416169

List的三种遍历方法

除了我们上面说到的iterator迭代器已经增强for循环以外,list集合还支持使用普通的for循环遍历。

public class ListFor {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("红色");
        list.add(true);
        list.add(6);
        list.add('a');
        //1. Iterator迭代器遍历
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }

        //2. 增强for循环遍历
        for (Object object : list) {
            System.out.println(object);
        }

        //3. 普通for循环遍历
        for (int i = 0; i < list.size(); i++) {
            Object object  = list.get(i);
            System.out.println(object);
        }

    }
}

而且这三种方法适用于list的所有子类实现,包括像ArrayList,LinkList,Vector什么的。

List的排序

下面是list的两种排序方法,分别是使用接口编程与使用冒泡排序的方法

public class ListExercise02_2 {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book("海底两万里",80,"MIKE"));
        list.add(new Book("海底三万里",60,"MIKE01"));
        list.add(new Book("海底四万里",100,"MIKE02"));
        sort(list);
        for (Object object :list) {
            System.out.println(object);
        }

    }
    public static void sort(List list) {
        for (int i = 0; i < list.size()-1; i++) {
            for (int j = 0; j < list.size()-1-i; j++) {
                Book book1 = (Book) list.get(j);
                Book book2 = (Book) list.get(j+1);
                if(book1.getPrice()>book2.getPrice()){
                    list.set(j,book2);
                    list.set(j+1,book1);
                }
            }
        }
    }
}

在冒泡排序的代码中我们没有使用temp变量,而是直接使用set方法将集合中的两个对象交换位置,然后我们使用增强for循环输出集合。

public class ListExercise02 {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book("海底两万里",80,"MIKE"));
        list.add(new Book("海底三万里",60,"MIKE01"));
        list.add(new Book("海底四万里",100,"MIKE02"));
        list.sort(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                Book book1 = (Book)o1;
                Book book2 = (Book)o2;
                if(book1.getPrice()-book2.getPrice()<0)
                {
                    return -1;
                }
                if(book1.getPrice()-book2.getPrice()>0)
                {
                    return 1;
                }
                return 0;
            }
        });
        System.out.println(list);
    }


}

在接口排序中,我们使用匿名内部类实例化接口,考虑到价格是一个double,但是系统只有返回int类型的compare方法,使用我们使用两个if语句,通过计算得出的小数返回具体的整数。

ArrayList类

ArrayList类的注意事项

  1. ArrayList不止可以存不同类型的对象,也可以存空值null
  2. ArrayList是由数组实现数据存储的
  3. Arraylist基本等同于Vector,除了它线程不安全外,但是它的效率高,在多线程的情况下,不建议使用ArrayList。

Arraylist的底层结构与源码分析

  1. ArrayList在底层维护一个Object类型的数组elementData,它的修饰符transient是短暂的,瞬间的意思,表示该属性不会被序列化。

    image-20240804152212424

  2. 当创建一个ArrayList对象时候,如果调用的是无参构造器,那么初始的elementdata容量为0,第一次添加元素,那么elementData的容量会扩大到10,如果需要再次扩容,每次为前一次的1.5倍。

  3. 如果使用的是指定大小的int类型的构造器,每次再次扩容的大小也是前一次的1.5倍。

下面我们来用一个例子讲解ArrayList的底层机制

SuppressWarnings({"all"})
public class ArrayListSource {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        for (int i = 0; i <= 10; i++) {
            list.add(i);//在这里设置断点
        }

        for (int i = 11; i < 15 ; i++) {
            list.add(i);
        }
        list.add(100);
        list.add(200);
        list.add(null);
    }
}

当程序执行到list.add时候,首先是进行自动装箱操作

image-20240804161125846

紧接着进入add方法屏幕截图 2024-08-04 161153

ensureCapacityInternal(size + 1); 意思是验证一下容量够不够,此时容量为0,肯定装不进去,那么我们进入ensureCapacityInternal(size + 1)里面看看会执行什么操作。

image-20240804161410418

在ensureCapacityInternal方法中调用calculateCapacity方法,参数分别为element数组与minCapacity,minCapacity的值为size+1,也就是1,我们接下来看calculateCapacity

image-20240804161539093

如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就返回DEFAULT_CAPACITY与10的较大的值,其中DEFAULT_CAPACITY为提前设置好的10,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组,所以会进入if语句然后返回一个10。紧着着回到上一步调用ensureExplicitCapacity(10));

image-20240804161410418

我们进入函数

image-20240804162234920

modCount指的是数组被修改的次数,这用于多线程的监控,同时minCapacity变为10,如果10-现在数组的长度大于0就说明数组需要扩容,进入grow方法

image-20240804164928487

在grow方法中我们可以清楚地看到为什么数组后面每次会扩大1.5倍,原来是因为本身加上半身的一半(右移一位),然后使用copyof方法将值赋过去,其余的用null补全。

Vector类

Vector定义与说明

  1. Vector底层也是一个数组对象。
  2. Vector是线程同步的,即线程安全的,Vector操作方法带有synchronized.
  3. 在开发中,需要线程安全时候,考虑使用Vector。

Vector扩容的源码解析

我们将通过下面一段代码来理解vector的扩容机制

image-20240805130747851

如下,我们为其设定了三个断点,现在进行调试,首先调用的是vector的构造函数,由于我们没有赋初值,这里编译器自动为我们赋值10大小。

image-20240805130838126

然后是我们熟悉的自动装箱过程

image-20240805130946340

add函数首先判断一下大小够不够装,如果不够就要扩容,,需要调用ensureCapacityHelper函数

屏幕截图 2024-08-05 131001

在这个函数中,判断最小需要的大小1和目前数组长度10之差小不小于0,如果小于,就启动grow进行扩容,显然我们现在10大小的容量足够我们经历for循环的装箱。

但是,当vector容器满了,我现在还要add一个100进去,那情况可能就不一样了。

前面的所有步骤都一样,但是不同的是这次进入了if语句调用了grow方法,因为需要大小是11,而数组长度是10,小于0.

image-20240805131550642

进入grow语句,我们直接看新的大小是怎么计算出来的,它是由old(10)+一个三元运算符得出的结果而来,那个三元运算符为

((capacityIncrement > 0) ? capacityIncrement : oldCapacity);

由于capacityIncrement已知等于0,所以三元运算符部分会再返回一个old,最后得出新的new=2*old。所以每次扩容的大小是前一次的二倍。

Vector与ArrayList的比骄

image-20240805131945685

感觉有点像StringBuilder与StringBuffer哈哈。。。

LinkedList类

LinkedList说明

  1. LinkList底层实现了双向链表与双端队列的特点
  2. 可以添加任意元素,也可以重复,包括null。
  3. 线程不安全,没有实现同步与互斥。
  4. LinkList底层维护两个属性first与last,分别指向首结点与尾结点。
  5. 每个结点里面又维护了pre,next,item三个属性,pre指向前一个结点,next指向后一个结点,最终实现双向链表。
  6. LinkList的插入与删除不是通过数组来完成的,所以效率比较高。

下面是我们模拟的一个简单的双向链表,演示其遍历与插入元素的功能。

public class LinkList_ {
    public static void main(String[] args) {
        Node hong = new Node("小红");
        Node ming = new Node("小明");
        Node fang = new Node("小芳");
        //建立联系
        hong.next = ming;
        ming.next = fang;

        fang.pre = ming;
        ming.pre = hong;

        Node first = hong;
        Node Last = fang;
        //从头遍历
        System.out.println("从头遍历");
        while(true)
        {
            if(first == null) break;
            System.out.println(first);
            first = first.next;
        }
        //从尾遍历
        System.out.println("从尾遍历");
        while(true)
        {
            if(Last == null) break;
            System.out.println(Last);
            Last = Last.pre;
        }
        //插入一个元素
        System.out.println("插入一个元素");
        Node qiang = new Node("小强");
        hong.next = qiang;
        ming.pre = qiang;
        qiang.next = ming;
        qiang.pre = hong;
        first = hong;
        while(true)
        {
            if(first == null) break;
            System.out.println(first);
            first = first.next;
        }
    }
}
class Node {
    public Node pre;
    public Node next;
    public Object item;

    public Node(Object item) {
        this.item = item;
    }

    @Override
    public String toString() {
        return "Node{" +
                "item=" + item +
                '}';
    }
}

从头遍历
Node{item=小红}
Node{item=小明}
Node{item=小芳}
从尾遍历
Node{item=小芳}
Node{item=小明}
Node{item=小红}
插入一个元素
Node{item=小红}
Node{item=小强}
Node{item=小明}
Node{item=小芳}

LinkedList的add方法底层源码解析

image-20240805142359741

首先进入的是一个无参的构造函数,什么都没有,为空。

image-20240805142532456

然后还是装箱,将int类型变为Integer类型对象才可以

image-20240805142743798

下一步就是add函数了

image-20240805142809303

add函数中调用了linkLast方法,继续深入

image-20240805142852355

我们来到了真正插入对象的部分—linklist方法,首先让结点l指向last指向的位置,为空,然后创建一个新节点,把数放进去,next与pre为空,再让last指向这个结点,由于l为空,所以进入if语句,first也指向这个新节点。

image-20240805143553854

我们回到最初,现在来插入第二个看看

image-20240805143626368

其他过程一样,我们直接看最后。

image-20240805142852355

与上次不一样的是,这次last没有指向空,所以结点l指向第一个结点,新建的结点的pre也指向第一个结点(根据Node的构造器,如下)

image-20240805143854236

由于l已经不为空了,所以执行else语句,将第一个结点的Last指向这个新节点。最后就像下面这样。

image-20240805144225164

题外话,由于同属于List实现子集,所以Linkedlist也可以使用for,增强for以及iterator迭代器实现输出。

List集合的选择

Arrayist与LinkedList的比较

底层结构增删的效率改查的效率
Arraylist可变数组较低,数组扩容较高
LinkedList双向链表较高,通过链表追加较低
  1. 改查多,选ArrayList
  2. 增删多,选LinkedList
  3. 一般项目80%-90%都是查询,因此大部分情况下用ArrayList
  • 17
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值