集合..

1.集合

java.util中包含一个集合框架 里面内置了很多的数据结构 比如:ArrayList、List、HashMap等等
数据结构是储存和组织数据的方式 但是这边我们重点讲的是如何使用 而并非如何实现 我以往的笔记中有讲到如何实现

2.ArrayList使用

1.数组的局限性

1.无法动态扩容
在数组创建以后 就不能随意修改数组容量
2.操作元素过程中不够面向对象
数组内置的数组操作太少了 所以不够面向对象 比如不能够如ArrayList一样通过remove(int index)删除指定位置处的元素

这样一对比的话 那么动态数组的好处自然就多了:
1.他是一个可以动态扩容的数组
2.他里面封装了很多操作数组元素的方法

2.ArrayList的常用方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以下是一个案例:
如果对一个原始类型进行数组的添加操作的话 那么他支持添加所有类型的元素 因为原始类型会将泛型替换成Object类型 但是其实这种做法不推荐 推荐的是写明泛型的具体类型
在这里插入图片描述

1.retainAll方法

list1.retainAll(list2)强调删除list1中除了list2以外的所有元素

public class Main {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        list1.add(11);
        list1.add(22);
        list1.add(33);
        List<Integer> list2 = new ArrayList<>();
        list2.add(22);
        list2.add(44);
        list1.retainAll(list2);
        System.out.println(list1);
    }
}

我们可以从结果得出 list1真的删除了list2以外的所有元素

2.toArray方法

该方法就是将动态数组转换成普通数组
根据对象数组的特点 我们不能够让子类引用指向父类对象
下面这个代码是可以编译通过的 我想提一嘴的就是 toArray()方法的返回值是Object[]类型

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(11);
        list.add(22);
        list.add(33);
        Object[] array1 = list.toArray();
        System.out.println(array1);
        System.out.println(Arrays.toString(array1));
    }
}

由于toArray()的返回值是Object[]类型的 所以你不能够将其赋值给Integer[]类型的数组 就算强转也不行 是因为子类引用不能够指向父类对象 否则会报ClassCastException异常

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(11);
        list.add(22);
        list.add(33);
        Integer[] array1 = (Integer[])list.toArray();
        System.out.println(array1);
        System.out.println(Arrays.toString(array1));
    }
}

但是如果非要将Object类型的数组赋值给Integer类型的数组 也不是不行 toArray有一个重载方法 支持传入一个类型参数(泛型) 返回相同类型的返回值
我们只需要传入一个没有开辟空间的Integer数组即可 那么现在就可以返回一个Integer类型的数组 也无需进行强转操作了

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(11);
        list.add(22);
        list.add(33);
        Integer[] array1 = list.toArray(new Integer[0]);
        System.out.println(array1);
        System.out.println(Arrays.toString(array1));
    }
}

3.ArrayList的遍历操作

1.带有索引的遍历
有两种写法:一种是提前保存List的长度 一种是每次遍历都获取一次List的长度
这两种写法各有好处 前者适用于遍历过程中数组元素的个数不会发生改变的场景 并且相较于后者来说 他可以节省时间(因为后者在每次遍历都需要多进行一次size的获取)
后者则更适用于遍历过程中数组元素个数发生改变的场景

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        int size = li.size();
        for(int i = 0; i < size; ++i){
            System.out.println(li.get(i));
        }
        for(int i = 0; i < li.size(); ++i){
            System.out.println(li.get(i));
        }
    }
}

2.迭代器遍历
迭代器遍历的原理是内置了一个游标 他旨在指向数组中的元素 当他指向了数组的容量位置处时 hasNext()就会返回false 而next()方法则是会获取当前游标所指向的元素 并且更新游标指向为下一个元素

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        Iterator<Integer> it = li.iterator();// 创建一个迭代器
        while(it.hasNext()){
            Integer i = it.next();
            System.out.println(i);
        }
    }
}

3.增强for循环 即for-each循环

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        for(Integer i: li){
            System.out.println(i);
        }
    }
}

4.调用List中的forEach方法 也可以完成对该集合的遍历操作

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
    }
}

其实匿名类还可以简化成Lambda表达式 前提是匿名类实现的是函数式接口 从jdk源码来看 其实现的的确是函数式接口

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.forEach((integer) -> {
            System.out.println(integer);
        });
    }
}

还可以继续简化

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.forEach((integer) -> System.out.println(integer));
    }
}

还可以继续简化

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.forEach(System.out::println);
    }
}

其实2、3、4虽然不能够直接获取到每一次遍历的元素的位置 但是可以通过一些间接的手段获取
我们就以第四个案例进行举例

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.forEach(new Consumer<Integer>() {
            int index = 0;
            @Override
            public void accept(Integer integer) {
                System.out.println(integer + "_" + index++);
            }
        });
    }
}

结果显示 可以成功获取 但是值得注意的是forEach方法中每次循环获取元素的时候 是不会进行index的重定义操作的 index的定义只有在创建匿名类的时候才有 其他的时候都是在上一次调用的基础上进行累加的结果

4.for-each格式(增强for循环)

其实他的本质就是使用了迭代器(如果我们使用了foreach进行遍历数组的遍历操作的话 那么编译器会调用集合中的Iterator方法获取一个迭代器 然后通过这个迭代器完成遍历操作)
而且for(元素类型 变量名:数组\Iterable)
冒号右边的可以是普通的数组 也可以是Iterable本身以及他的子类
在这里插入图片描述
从上图中 我们可以看出 接口和抽象类都不能被实例化(即创建类对象) 可以实例化的是非抽象类 为绿色所标记
而且我们利用多态技术的话 那么就可以通过父类引用指向子类对象 子类对象只能够是绿色标记的 并且父类和子类可以差不止一代
我可以为你举若干个例子

public class Main {
    public static void main(String[] args) {
        Collection<Integer> cl = new ArrayList<>();
        cl.add(11);
        cl.add(22);
        cl.add(33);
        for(Integer i: cl){
            System.out.println(i);
        }
    }
}

上述这个例子可以通过编译

public class Main {
    public static void main(String[] args) {
        Iterable<Integer> il = new ArrayList<>();
        // il.add(11);// error
        for(Integer i: il){
            System.out.println(i);
        }
    }
}

然而上述这个例子虽然可以使用增强for循环 但是他不可以通过add方法往动态数组中添加元素 以前的多态中 父类和子类共有add方法 并且他调用的时候取决于子类 但是现在由于Iterable中没有add方法 所以肯定就使用不了add方法

5.自定义Iterable、Iterator

现在有这么一个需求 就是我有一个教师类 然后里面储存了很多学生 我想要遍历这些学生 并且每一次遍历都打印他们

public class ClassRoom {
    // 现在我定义一个学生数组 用于储存教室里的学生
    private String[] students;
    // 然后我可以提供一个构造方法 用于初始化这个私有成员
    public ClassRoom(String... students){
        this.students = students;
    }
    public String[] getStudents(){
        return students;
    }
}
public class Main {
    public static void main(String[] args) {
        ClassRoom cr = new ClassRoom("jack", "sandy");
        for(String str: cr.getStudents()){
            System.out.println(str);
        }
    }
}

但是我现在有一个想法就是 可不可以直接增强for循环cr来代替学生数组呢?
前面我们说了 冒号右边的位置可以放置普通数组 也可以放置Iterable类型
那么我们只需要让ClassRoom实现一下Iterable即可

public class ClassRoom implements Iterable<String>{
    // 现在我定义一个学生数组 用于储存教室里的学生
    private String[] students;
    // 然后我可以提供一个构造方法 用于初始化这个私有成员
    public ClassRoom(String... students){
        this.students = students;
    }
    public String[] getStudents(){
        return students;
    }

    @Override
    public Iterator<String> iterator() {
        return null;
    }
}

从上述代码可以发现 我们实现了Iterable接口以后 他要求我们需要实现Iterator这个方法 并且返回值为Iterator类型 即迭代器
这时候 其实我们可以定义一个内部类实现Iterator这个接口 并且实现他的抽象方法
并且我们都知道jdk内置的迭代器中有一个游标cursor 所以我们在我们自定义的ClassRoomIterator中也需要定义一个这个成员
还有就是他里面是可以直接访问外部类的成员students 原因在于如果想要有内部类实例的话 就要先有外部类实例 有了外部类实例 你就可以访问外部类的成员

public class ClassRoom implements Iterable<String>{
    // 现在我定义一个学生数组 用于储存教室里的学生
    private String[] students;
    // 然后我可以提供一个构造方法 用于初始化这个私有成员
    public ClassRoom(String... students){
        this.students = students;
    }
    public String[] getStudents(){
        return students;
    }

    @Override
    public Iterator<String> iterator() {
        return new ClassRoomIterator();
    }
    private class ClassRoomIterator implements Iterator<String>{
        private int cursor;

        @Override
        public boolean hasNext() {
            return cursor < students.length;
        }

        @Override
        public String next() {
            return students[cursor++];
        }
    }
}
public class Main {
    public static void main(String[] args) {
        ClassRoom cr = new ClassRoom("jack", "sandy");
        Iterator<String> it = cr.iterator();
        while(it.hasNext()){
            System.out.println(it.next());
        }
    }
}

上述代码一运行以后 可以发现 我们是可以通过迭代器进行遍历的
但是别忘了我们的目标是通过增强for循环实现遍历操作

public class ClassRoom implements Iterable<String>{
    // 现在我定义一个学生数组 用于储存教室里的学生
    private String[] students;
    // 然后我可以提供一个构造方法 用于初始化这个私有成员
    public ClassRoom(String... students){
        this.students = students;
    }
    public String[] getStudents(){
        return students;
    }

    @Override
    public Iterator<String> iterator() {
        return new ClassRoomIterator();
    }
    private class ClassRoomIterator implements Iterator<String>{
        private int cursor;

        @Override
        public boolean hasNext() {
            return cursor < students.length;
        }

        @Override
        public String next() {
            return students[cursor++];
        }
    }
}
public class Main {
    public static void main(String[] args) {
        ClassRoom cr = new ClassRoom("jack", "sandy");
        for(String str: cr){
            System.out.println(str);
        }
    }
}

其结果显示 依然可以完成遍历操作 也进一步地说明了增强for循环底层确实运用了迭代器

6.遍历的注意点

我现在有一个需求为:
想要在遍历数组的过程中挨个删除元素 最后达到删除所有元素的目的
1.如果我们采用的是带索引的方法去遍历数组的话 那么不管是有没有注意到数组元素个数随着遍历操作而更新的问题 都无济于事

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        for(int i = 0; i < li.size(); ++i){
            li.remove(i);
        }
        System.out.println(li);
    }
}

考虑到数组元素个数的更新的情况下 打印结果是[22, 44] 你可以一步一步进行推理

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        int len = li.size();
        for(int i = 0; i < len; ++i){
            li.remove(i);
        }
        System.out.println(li);
    }
}

如果是没有考虑到数组元素个数更新的情况下 报出索引越界异常 原因你也可以一步一步推导
2.如果我们采用的是增强for循环的话 那么就会报ConcurrentModificationException 即并发修改异常

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        for(Integer i: li){
            li.remove(i);
        }
        System.out.println(li);
    }
}

3.由于增强for循环的本质就是迭代器循环 所以说迭代器循环的效果和上一个循环的效果一致

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        Iterator<Integer> i = li.iterator();
        while(i.hasNext()){
            li.remove(i.next());
        }
        System.out.println(li);
    }
}

4.使用forEach的循环方式 其结果也是报出并发修改异常

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        li.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                li.remove(integer);
            }
        });
        System.out.println(li);
    }
}

那么我们已经尝试了所有能够尝试过的遍历手段 其结果统统都不可取 那么我们应该如何解决这个需求呢?
真正的解决方法就是通过迭代器的方法进行遍历 在进行删除的时候使用迭代器的删除方法 而不是集合的删除方法

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        Iterator<Integer> i = li.iterator();
        while(i.hasNext()){
            i.next();
            i.remove();
        }
        System.out.println(li);
    }
}

我们可以看到 本质上Iterator.remove不是根据游标来删除元素 而是根据lastRet删除的元素
将当前游标所指向的元素进行删除

在使用迭代器、forEach(三种)遍历集合元素的过程中 如果我们使用集合自带的方法对集合长度进行修改的话(比如:add、remove) 那么有可能会报ConcurrentModificationException(并发修改异常)
那么为什么会使用集合自带的方法就会出现并发修改异常 而使用迭代器的方法不会呢?
我可以举一个例子 你就明白了
在这里插入图片描述
从上图可知 游标的当前指向为索引3下的44 他的下一个目标就是索引4下的55 但是如果现在我通过集合自带的add方法往索引1处插入一个77 那么就会变成以下情景
在这里插入图片描述
原本我的下一个预期目标是55的 但是由于你使用了集合自带的添加方法导致我的下一个预期目标改变 从而影响了我游标的正常运行 所以你在使用迭代器遍历的过程中使用集合自带的方法是很不利的

那么他是怎么立马检测到你的集合修改行为的呢
也就是说 其实他在创建迭代器的时候 就会将modCount的初始化设置为预期值 如果你以后在遍历的过程中一旦有出现任何集合的修改长度的操作的话 那么modCount就会执行++操作 从而与预期的modCount不相匹配 因为在每一次遍历中的next方法开始处都会去检查一下当前的modCount是否和预期的modCount一致 如果不一致的话 那么就会抛出并发修改异常
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
那我们可能会很奇怪 为什么集合的自身修改长度的方法不行 那迭代器的就可以呢?
其实迭代器的修改长度方法是每调用一次修改长度的方法(add或者remove) 也同样会使得modCount++ 但是自增完成以后 他还会去修改预期的modCount为当前的modCount 所以说下次在调用next方法的时候 开头进行检测的时候 就可以保证预期的modCount和实际的modCount一致了 也就不会抛出并发修改异常了

现在我们有另外一个需求:就是删除集合中的偶数(其实知道了遍历集合的过程中需要用到迭代器自身的长度修改方法来修改长度以后 就容易多了)

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        Iterator<Integer> it = li.iterator();
        while(it.hasNext()){
            if(it.next() % 2 == 0){
                it.remove();
            }
        }
        System.out.println(li);
    }
}

但是其实还有效率更高的做法 就是用位运算替代取模运算 因为我们知道位运算的效率明显是比取模运算要高的

public class Main {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(11);
        li.add(22);
        li.add(33);
        li.add(44);
        li.add(55);
        Iterator<Integer> it = li.iterator();
        while(it.hasNext()){
            if((it.next() & 1) == 0){
                it.remove();
            }
        }
        System.out.println(li);
    }
}

如何通过位运算判断一个数是否为偶数或者奇数?
就是通过判断最后一位二进制位是否是1还是0来判断是否是奇数还是偶数
原理在于除了最后一位是奇数以外 其他二进制位统统都是偶数 所以只要保持最后一位为0的话 那么其他位的情况不管怎么样 其加和结果都必然是偶数 如果有最后一位参与的加和 其结果必然都是奇数

forEach方法中也内置了一个监测modCount预期和实际是否一致的方法 一旦不一致 他也会报并发修改异常

7.ListIterator

前面我们都知道 在迭代器遍历集合的过程中 我们是不能够通过集合自身的方法去修改集合的长度 但是允许使用迭代器的集合长度修改方法去修改集合的长度 但是其实我们也只能够使用remove方法 因为并没有提供add方法供我们使用 所以说我们需要使用之前迭代器的子类 因为子类在原来迭代器的基础上添加了一些功能 比如原来迭代器没有的add方法
在这里插入图片描述
红框内是ListIterator相比于Iterator新增的功能

以下是一些ListIterator的相关实例:在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这有点神奇 在遍历过程中打印的结果和后面整体打印集合的结果不一样
这就要去深究一下迭代器add方法的原理了
在这里插入图片描述
从源码我们可以知道 在执行add方法的过程中 他先是获取当前游标所指的索引 然后将元素添加到该索引处 同时及时更新了游标指向 防止干扰他的正常运行 所以你才会看到在遍历过程中打印it.next的结果是如此的
后面整体打印集合的结果是因为我们添加元素所导致的

8.容量相关方法在这里插入图片描述

先来说一下缩容方法trimToSize:
在这里插入图片描述
我们可以看到他的操作是:如果元素个数小于数组的容量时 需要分情况讨论 如果元素个数为0的话 那么就让旧数组指向一个空数组 如果不为0的话 那么就执行Arrays.copyOf()方法 具体的操作就是申请一块元素个数长度的新数组 然后将旧数组的元素拷贝到新数组中 然后让旧数组指向新数组
他的使用场景就是在空间不够用并且数组占据了大量未使用的空间时 那么我们就可以考虑使用缩容操作了 这样可以节省空间

再来说一下扩容操作:
我们之前有谈到过扩容方法ensureCapacity 我们可以具体到源码里面看一下
在这里插入图片描述
然后进入ensureExplicitCapacity中查看
在这里插入图片描述
接着进入grow里面查看
在这里插入图片描述
我们可以看到 你如果想要检查某个容量是否超出现在的容量的话 他会先去调用ensureCapacity 然后在里面调用ensureExplicitCapacity 接着如果你的容量超过了当前的容量的话 那么他就会调用grow方法 具体的操作就是通过旧容量获取一个新容量(新容量是旧容量的1.5倍) 接着会判断预期的新容量和实际的新容量的大小 以大的新容量为新数组的容量 接着就会将旧数组的元素拷贝到新数组中 最后让旧数组指向新数组即可

现在我有一个需求:
我想要储存大容量的数据 我的最合适的做法并不是调用ArrayList的空参方法去创建一个动态数组 之后在添加元素 因为这样的做法会涉及到很多次的扩容方法的调用 会消耗性能(主要体现在时间方面的大量消耗)
我最合适的做法是调用带参方法创建一个动态数组 我传递的参数就是数组初始的容量 这样就可以避免很多次的扩容方法的调用 而是直接通过我传递的参数创建数组 用起来储存元素

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> al = new ArrayList<>();
        for(int i = 0; i < 10000; ++i){
            al.add(i);
        }
    }
}

上述做法不可取 因为涉及到很多次的扩容调用

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> al = new ArrayList<>(10000);
        for(int i = 0; i < 10000; ++i){
            al.add(i);
        }
    }
}

同样的一个需求 上述代码明显合理多了

但是我现在要修改一下需求:
就是我创建了之后 不想要立马储存大容量的数据 那么这时候 显然利用传入的容量创建数组的做法不可取 因为你不是立马储存大容量数据的话 那么你可能会占据一段时间的未使用的内存 你占据了没有使用 相当于浪费了 而且就算你不使用 其他操作也不见得不使用 所以我们应该是等到什么时候你要储存大量数据的时候 你才分配大量的内存

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> al = new ArrayList<>(10000);
        for(int i = 0; i < 5; ++i){
            al.add(i);
        }
        for(int i = 0; i < 10000; ++i){
            al.add(i);
        }
    }
}

上述这种做法不适用于这个需求 因为你明显没有在申请之后立马使用他们 导致很多内存闲置 但是如果在这期间有其他很需要内存的操作 你却又占据着很多未使用的内存 明显就是占着茅坑不拉屎

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> al = new ArrayList<>();
        for(int i = 0; i < 5; ++i){
            al.add(i);
        }
        al.ensureCapacity(al.size() + 10000);
        for(int i = 0; i < 10000; ++i){
            al.add(i);
        }
    }
}

最合适的做法就是你什么时候要储存大规模的数据 你什么时候在去申请

3.ArrayList的扩容原理

你可以进入源码中查看一下他的ensureCapacity方法
可以发现 扩容操作的情景是当添加元素的时候超出数组的容量
具体说明一下这个方法:
首先他会根据旧容量来获取新容量 一般新容量是旧容量的1.5倍 而且这个获取的过程不是直接通过乘法运算得到的(加法、减法、位运算效率是比乘法、除法、取模要高的) 所以这边他是通过位运算计算得到的
接着他会根据新容量创建一个新数组 然后将旧数组的元素拷贝到新数组中 接着就会将改变旧数组的指向 让其指向新数组对象

还得说明一下的是:
在jdk源码中 ArrayList的最小容量是10
如果是对象数组的话 那么数组中的每一个元素都是地址值 而不是真正的内容 然后再由每一个地址值指向真正的内容

3.LinkedList

在jdk中 我们所指的LinkedList是双向链表
他实现了List接口
他的api和ArrayList类似 所以他的使用和ArrayList也和类似

4.LinkedList vs ArrayList

我们可以从查找、添加、删除这些操作来对比一下这两个集合的效率:
1.查找
ArrayList:
由于数组在查找元素时 会直接获取到欲查找元素的位置(通过首地址+偏移量x数组元素占用字节数) 然后直接定位到欲查找元素 而不是通过首元素一个一个往后查找
效率高
LinkedList:
不能像数组那样直接定位到待查找元素 而是只能够从首元素一个一个往后遍历
效率低
2.添加
往尾部进行插入操作 ArrayList和LinkedList效率差不多 都比较高 属于O(1)级别的

往头部进行插入操作 ArrayList效率低(需要挪动头部后面的所有元素) LinkedList效率高

往任意位置执行插入操作 ArrayList和LinkedList效率差不多
他们的平均时间复杂度分别是:
(1 + 2 + 3 + …… + n) / n = n / 2 + 1 / 2 = n
(1 + 2 + 3 + …… + n / 2) x 2 / n = 1 / 2 x (n / 2 + 1) = n / 4 + 1 / 2 = n
3.删除
往尾部执行删除操作 ArrayList和LinkedList效率差不多 都比较高 属于O(1)级别的

往头部执行删除操作 ArrayList效率低(需要挪动头部后面的所有元素) LinkedList效率高

往任意位置执行删除操作 他们两者的效率差不多
他们的平均时间复杂度分别是:
(1 + 2 + 3 + …… + n) / n = n / 2 + 1 / 2 = n
(1 + 2 + 3 + …… + n / 2) x 2 / n = 1 / 2 x (n / 2 + 1) = n / 4 + 1 / 2 = n
4.总结
ArrayList的优势
查找、往尾部执行添加、往尾部执行删除
LinkedList的优势
往尾部执行添加、往头部执行添加、往尾部执行删除、往头部执行删除

如果频繁的在尾部执行添加、删除操作的话 那么ArrayList和LinkedList均可以选择
如果频繁的在头部执行添加、删除操作的话 那么建议使用LinkedList
如果频繁的在任意位置执行添加、删除操作的话 建议也使用LinkedList(前面我们说过两者皆可使用 但是这边推荐使用链表的理由为链表比动态数组多了头部的优势 所以总体上还是链表比较推荐)
如果频繁的执行查找操作的话 那么建议使用ArrayList
5.ArrayList和LinkedList在内存方面的差别:
LinkedList会频繁的进行内存的开辟和销毁 但是不会造成内存的浪费 需要多少就申请多少
ArrayList开辟和销毁内存的次数较少 但是可能会造成内存的极大浪费(可以通过缩容操作解决)

5.Stack和Queue

1.栈

只能够在一端(栈顶)执行操作
往栈中添加元素 叫做入栈 即push
从栈中移除元素 叫做出栈 即pop
栈遵循后进先出原则 即LIFO
栈(数据结构)和栈空间(内存)不一样

1.常用方法

在这里插入图片描述

2.使用

在这里插入图片描述

2.队列

只能够在头尾两端执行操作
从队尾添加元素 叫做入队
从队头移除元素 叫做出队
遵循先进先出的原则 即FIFO

1.常用方法

在这里插入图片描述

2.使用

注意:Queue是一个接口 不能实例化 所以只能采用多态的方式 比较常用的方式是指向链表(队列强调在头尾执行增删操作 而链表比动态数组在头尾执行增删操作中存在巨大优势)
在这里插入图片描述

6.HashSet和TreeSet

1.HashSet

1.使用

在这里插入图片描述
从结果中可以看出 元素在set中是无序的 而且不会存在重复元素

由于set不支持通过索引进行访问 所以不支持带索引的遍历方式 但是支持其他三种遍历方式(set可以使用迭代器的原因在于他实现了Iterable接口)
在这里插入图片描述
可以明显发现 遍历并打印的结果显然也是无序的

2.用途

支持去重

public class Main {
	
	public static void main(String[] args) {
		String[] strs = {"Jack", "Rose", "Kate", "Jack"};
		Set<String> hs = new HashSet<>();
		for(String str: strs) {
			hs.add(str);
		}
		System.out.println(hs);
	}

}

结果显示可以达到去重的效果 但是仍然是无序的

3.LinkedHashSet

就是在HashSet的基础上记录元素的添加顺序 就是能够保持打印顺序和添加顺序一致 但是依然有去重的效果
在这里插入图片描述

2.TreeSet

TreeSet仍然是去重的、不能够通过索引访问的
而且在这个的基础上 还要求元素必须具备可比较性 打印顺序默认以从小到大的顺序为准
在这里插入图片描述

1.自定义比较方式

既然TreeSet的元素需要具备可比较性 那么我们其实可以在元素原有比较方式的基础上进行覆盖 实现新的比较方式
比如Integer默认的比较方式是将Integer数组按升序的顺序进行排列
但是我们可以将其设置为按照降序的顺序进行排列
在这里插入图片描述

7.HashMap和TreeMap

HashMap储存的是键值对 Map的意思为映射 有些编程语言也叫做字典

1.HashMap

1.使用

在这里插入图片描述
从打印结果可以看出
map不允许储存重复的key(但是可以储存重复的value) 如果添加了重复的key的话 那么会采取后覆盖前的方式进行处理
并且map的打印顺序和添加顺序没有必然联系

2.遍历

1.每一次遍历先取出键 然后再去映射中寻找符合键的键值对 从中取出值 这样效率没有一次性取出键值对的效率高

public class Main {
	
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<>();
		map.put("Jack", 11);
		map.put("Rose", 22);
		map.put("Kate", 33);
		for(String str: map.keySet()) {
			System.out.println(str + "=" + map.get(str));// 整体效率比较低
		}
	}

}

2.获取map中所有值的集合 但是这种方式只能够遍历map中的值 不能够遍历键

public class Main {
	
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<>();
		map.put("Jack", 11);
		map.put("Rose", 22);
		map.put("Kate", 33);
		Collection<Integer> c = map.values();
		for(Integer i: c) {
			System.out.println(i);
		}
	}

}

3.获取所有键值对的set集合 然后利用遍历set的方式遍历所有的键值对 这种方式用于获取遍历每一个键值对是推荐的

public class Main {
	
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<>();
		map.put("Jack", 11);
		map.put("Rose", 22);
		map.put("Kate", 33);
		Set<Entry<String, Integer>> set = map.entrySet();
		for(Entry<String, Integer> entry: set) {
			System.out.println(entry.getKey() + "=" + entry.getValue());
		}
	}

}

4.使用map的forEach方法进行遍历 这种方式在遍历map中的键值对的时候同样是推荐的

public class Main {
	
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<>();
		map.put("Jack", 11);
		map.put("Rose", 22);
		map.put("Kate", 33);
		map.forEach((key, value) -> System.out.println(key + "=" + value));
	}

}

3.LinkedHashMap

同LinkedHashSet一样 他同样记录了添加元素时的顺序 所以打印顺序和添加顺序有着必要的联系

public class Main {
	
	public static void main(String[] args) {
		Map<String, Integer> map = new LinkedHashMap<String, Integer>();
		map.put("Jack", 11);
		map.put("Rose", 22);
		map.put("Kate", 33);
		map.put("Jack", 44);
		map.put("Jack", 55);
		map.forEach((key, value) -> System.out.println(key + "=" + value));
	}

}

结果为Jack=11 Rose=22 Kate=33 符合我们的预期

2.TreeMap

同TreeSet一样 要求元素的key具备可比较性 并且打印顺序和比较逻辑的顺序一致
在这里插入图片描述

8.List vs Set vs Map

1.List的特点

可以储存重复的元素
可以通过索引访问元素

2.Set的特点

不可以储存重复的元素
不可以通过索引访问元素

3.Map的特点

不可以储存重复的key 但是可以储存重复的value
不可以通过索引访问key-value(Entry类型)

4.Set底层

Set底层是基于Map实现的
HashSet底层是基于HashMap实现的
LinkedHashSet底层是基于LinkedHashMap实现的
TreeSet底层是基于TreeMap实现的在这里插入图片描述
可以看到 在构造HashSet的时候 他其实就是去构造了一个HashMap
在这里插入图片描述
而且在实现add方法的时候 他就是调用map中的put方法添加了一个键值对 只不过他保持了value为一个常量Object对象 Set中的元素就是Map中的键值对的键

9.Collections

java.util.Collections是一个常用工具类 里面内置了很多常用的静态方法 所以我们可以通过Collections类名调用这些常用的静态方法
在这里插入图片描述
你要注意了 参数中的Collection指的是其本身或者他的子类 比如List、Set等等
reverseOrder返回的是一个包含逆序排列比较逻辑的比较器

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axihaihai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值