Day1.概述&List族 -Java集合

Java集合框架结构图

在这里插入图片描述

Iterable接口

Iterable接口是Collection这一块的祖宗级别的接口。实现Iterable()接口会重写iterator()方法,可以返回一个迭代器(Iterator)。
Iterable接口:
在这里插入图片描述

Iterator迭代器可以遍历集合,并且可以删除集合中的元素。

List<Student> list = new ArrayList<>();
        list.add(new Student("male"));
        list.add(new Student("female"));
        list.add(new Student("female"));
        list.add(new Student("male"));

        System.out.println(list);
        //遍历删除,除去男生
        Iterator<Student> iterator = list.iterator();
        while (iterator.hasNext()) {
            Student student = iterator.next();
            if ("male".equals(student.getGender())) {
                iterator.remove();//使用迭代器的删除方法删除
            }
        }
        System.out.println(list);

像这种使用迭代器进行遍历 删除元素是可行的,也是开发中推荐使用的。

如果我们将上面的iterator.remove()换成list.remove(student)将会报ConcurrentModificationException异常。
原因:使用迭代器遍历,却使用集合的删除元素的结果。
总结干货:
1.使用高级For循环遍历删除/增加元素时,如果再某次循环的过程中进行了删除或增加元素的操作,并在此次操作中break或return结束了循环操作,不报异常。
2.使用高级For循环遍历删除/增加元素时,除了1这种情况外,其他必报ConcurrentModificationException异常。

验证:

        List<Student> list = new ArrayList<>();
        list.add(new Student("male"));
        list.add(new Student("female"));
        list.add(new Student("female"));
        list.add(new Student("male"));

        System.out.println(list);
        //遍历删除,除去男生
        for (Student student : list) {
            if("male".equals(student.gender)){
                list.remove(student);
                //break;
            }
        }


        System.out.println(list);

报错原因未知,我猜测是两个对象不能同时使用集合。
既然都看到这儿了,我们再研究一下普通for循环删除:


  List<Student> list = new ArrayList<>();
        list.add(new Student("male"));
        list.add(new Student("female"));
        list.add(new Student("female"));
        list.add(new Student("male"));

        System.out.println(list);
        //遍历删除,除去女生
        for (int i = 0; i < list.size(); i++) {
            Student student = list.get(i);
            if("female".equals(student.gender)){
                list.remove(i);
                i--;  //关键
            }
        }

        System.out.println(list);

注意:执行完删除操作后要将索引-1,否则会出现:"假如A和B同时满足条件,A被删除 B一定会被留"下来的问题。因为List集合中每个元素都有其对应的顺序索引。
还有就是,使用iterator对元素进行遍历时,iterator并不是把集合元素本身传递给迭代变量,而是把集合元素的值传递给它(就像参数传递是值传递,基本数据类型传递的是值,引用类型传递的仅仅是对象的引用变量),所以修改迭代变量的值对集合元素没有任何影响。

public class IteratorExample {
    public static void main(String[] args){
        List<String> list =Arrays.asList("java语言","C语言","C++语言");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();//集合元素的值传给了迭代变量,仅仅传递了对象引用。保存的仅仅是指向对象内存空间的地址
            next ="修改后的";
            System.out.println(next);
            
        }
        System.out.println(list);
    }

}
输出结果:
修改后的
修改后的
修改后的
[java语言, C语言, C++语言]
总结
  1. Iterable接口是Collection这一块的祖宗接口,重写其中的iterator()方法返回Iterator迭代器;
  2. 通过Iterator类中next()和hasNest()方法迭代容器。且迭代过程中iterator.remove()可以删除元素;
  3. 迭代器遍历时,如果使用list.remove(stu)的方式删除元素,会抛异常;
  4. 如果使用增强For遍历元素,只有在增加或删除元素时break或者return结束循环不报异常,其他情况必报异常;
  5. 普通For循环不会报异常,但是删除元素后要将索引-1;
  6. iterator遍历的时候是把值传递给迭代变量,所以修改迭代变量的值对集合元素没有任何影响;

扯了这么多,我们再来看看Collection接口

Collection接口

在文章开头那张图中可以看出Collection接口也算是老一辈的接口了,它是List Set Queue的父接口,它里面的方法适用于其子类,所以它的方法都是比较基础的:
在这里插入图片描述
看一下retainAll()方法


  // 创建一个数组
        ArrayList<Integer> numbers = new ArrayList<>();

        // 往数组中添加元素
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        System.out.println("ArrayList: " + numbers);

        // 创建一个HashSet对象
        HashSet<Integer> primeNumbers = new HashSet<>();

        // 往 HashSet添加元素
        primeNumbers.add(2);
        primeNumbers.add(3);
        primeNumbers.add(5);
        System.out.println("HashSet: " + primeNumbers);

        // arraylist调用的retainAll()方法 所以会在arraylist中保留公共的元素
        numbers.retainAll(primeNumbers);
        System.out.println("共有元素: " + numbers);

     ArrayList: [1, 2, 3]
     HashSet: [2, 3, 5]
     共有元素: [2, 3]
总结
  1. Collection接口是List Set Queue的父接口,里面定义了比较基础的方法:增删查 clear() isEmpty() size() retainAll() toArray();
  2. List Set Queue都可以与另一种集合取交集 也可以计算元素个数 清空 转换为指定类型的数组;

List集合

List集合是一个元素有序 可重复的集合,集合中的每一个元素都有其顺序索引List集合默认以元素的添加顺序来设置元素的索引。

除了Collection集合中的方法外,List中常用的方法有:


void add(int index, Object element)
在列表的指定位置插入指定元素(可选操作)

boolean addAll(int index, Collection<? extends E> c)
将集合c 中的所有元素都插入到列表中的指定位置index处

Object get(index)
返回列表中指定位置的元素

int indexOf(Object o)
返回此列表中第一次出现的指定元素的索引 
如果此列表不包含该元素,则返回 -1

int lastIndexOf(Object o)
返回此列表中最后出现的指定元素的索引
如果列表不包含此元素,则返回 -1


Object remove(int index)
移除列表中指定位置的元素

Object set(int index, Object element)
用指定元素替换列表中指定位置的元素

List subList(int fromIndex, int toIndex)
返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的所有集合元素组成的子集

除此之外,Java8还为List接口添加了如下两个默认方法。

void replaceAll(UnaryOperator operator)
根据operator指定的计算规则重新设置List集合的所有元素

void sort(Comparator c)
根据Comparator参数对List集合的元素排序
List集合判断元素相等的标准

List判断两个对象相等只要通过equals()方法比较返回true即可。
例如,创建一个Book类 重写其中的equals()方法

public class Book {
    public String name;

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Book other = (Book) obj;
        if (this.name == other.name) {
            return true;
        } 
        return false;
    }
}

向List集合中加入book1对象,然后调用remove(Object o)方法(从集合中删除指定对象),这个时候参数传入book2。

 public static void main(String[] args){
        Book book1 = new Book();
        book1.name = "Java必知必会";
        Book book2 = new Book();
        book2.name = "Java必知必会";
        List<Book> list = new ArrayList<Book>();
        list.add(book1);
        list.remove(book2);
        System.out.println(list.size());
    }

输出结果:0
可见,book1对象从集合中删除了,这表明List集合判断两个对象是否相等只要通过equals()方法比较返回true即可。

ListIterator

List接口中还提供了一个listIterator()方法,该方法返回一个ListIterator对象。
ListIterator接口在Iterator接口的基础上增加了以下几个方法:

boolean hasPrevious()
如果以逆向遍历列表。如果迭代器有上一个元素,则返回 true

Object previous()
返回迭代器的前一个元素

void add(Object o)
将指定的元素插入列表(可选操作)

与Iterator接口相比,ListIterator接口增加了向前迭代和增加元素的功能

小结
  1. List是一个元素可重复且有序的集合,其中的每个元素都有其顺序索引(这就是为什么for循环遍历list时 删除元素后循环变量i要-1);
  2. List中提供了许多与索引有关的方法:add() get() remove() indexOf() set() sort() subList()等等;
  3. List接口判断两个元素是否相等的依据是:equals()是否返回true
  4. List接口中提供了一个listIterator()方法,ListIterator接口相比于Iterator接口提供了向前迭代和增加元素的功能
  5. Java8为List接口新增了replaceAll()sort()方法;

ArrayList与Vector

ArrayList与Vector的用法功能非常相似。它们都是基于数组实现的List类,所以ArrayList和Vector底层都封装了一个动态的 允许再分配的Obejct[]数组。ArrayList和Vector对象使用initalCapacity变量来设置该数组的长度,当向ArrayList或Vector中添加元素超过了该数组的长度的话,它们的initalCapacity会自动增加。下面是JDK 1.8 ArrayList的源码:

ArrayList的本质

当使用List<Book> list=new ArrayList<>(30);创建ArrayList集合时,


//动态Object数组,用来保存加入到ArrayList的元素
Object[] elementData;

//ArrayList的构造函数,传入参数为数组大小
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
             //创建一个对应大小的数组对象
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //传入数字为0,将elementData 指定为一个静态类型的空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

当以List<Book> list = new ArrayList<Book>();方式创建ArrayList集合时,不指定集合的大小


    /**
     *Constructs an empty list with an initial capacity of ten。意思是:构造一个空数组,默认的容量为10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

加粗样式
当向数组中添加元素list.add(book1);时:
先调用add(E e)方法

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 数组的大小增加1
        elementData[size++] = e;
        return true;
    }

在该方法中,先调用了一个ensureCapacityInternal()方法,顾名思义:该方法用来确保数组中是否还有足够容量。
ensureCapacityInternal()方法中先经过一系列方法(不必关心),最后有个判断:如果剩余容量足够存放这个数据,则进行下一步,如果不够,则需要执行一个重要的方法:

 private void grow(int minCapacity) {
        //......省略部分内容  主要是为了生成大小合适的newCapacity
       //下面这行就是进行了数组扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

由此,我们就清楚地明白了,ArrayList是一个动态扩容的数组(每次扩大到原来的1.5倍大小),Vector也同样如此。

如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initalCapacity的大小,这样可以提高性能。

此外,ArrayList还提供了两个额外的方法来调整其容量大小:

  1. void ensureCapacity(int minCapacity)
  2. void trimToSize():将此 ArrayList 实例的容量调整为列表的当前大小。
ArrayList的遍历
  1. 迭代器遍历
  2. for循环
  3. 增强for循环

遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低。

ArrayList和Vector的区别
  1. ArrayList是线程不安全的,Vector是线程安全的。
  2. Vector的性能比ArrayList差。
Stack

Stack与Vector一样,是线程安全的,但是性能较差,尽量少用Stack类。如果要实现栈”这种数据结构,可以考虑使用LinkedList

小结
  1. ArrayList和Vector底层都是动态的 允许再分配的Object[]数组,底层都使用initalCapacity变量来设置该数组的长度,当数组元素数超过数组的长度的话,会实现自动扩容;
  2. ArrayList的默认初始大小为10,扩容每次增长1.5倍;
  3. 如果事先知道要保存多少个元素的话,可以在创建的时候就指定其大小,这样可以提高性能;
  4. ensureCapacity()和trimTosize()可以调整其容量大小;
  5. ArrayList的遍历方式:迭代器 for 增强for。其中使用索引速度最快,使用迭代器最慢;
  6. ArrayList是线程不安全的,Vector是线程安全的,前者效率高;
  7. Vector的子类Stack也是线程安全的 效率低。如果要使用栈这种数据结构,建议使用LinkedList;

LinkedList

LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“"来使用,也可以当成"队列"来使用。

ArrayList内部是以数组的形式来保存元素的,因此随机访问集合元素时速度快;而LinkedList内部以链表的形式来保存元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能好。

由于LinkedList双端队列的特性,所以新增了一些方法。

void addFirst(E e):将指定元素插入此列表的开头
void addLast(E e): 将指定元素添加到此列表的结尾
E getFirst(E e): 返回此列表的第一个元素
E getLast(E e): 返回此列表的最后一个元素

boolean offerFirst(E e): 在此列表的开头插入指定的元素。
boolean offerLast(E e): 在此列表末尾插入指定的元素。
E peekFirst(E e): 获取但不移除此列表的第一个元素;如果此列表为空,则返回 nullE peekLast(E e): 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 nullE pollFirst(E e): 获取并移除此列表的第一个元素;如果此列表为空,则返回 nullE pollLast(E e): 获取并移除此列表的最后一个元素;如果此列表为空,则返回 nullE removeFirst(E e): 移除并返回此列表的第一个元素。
boolean removeFirstOccurrence(Objcet o): 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
E removeLast(E e): 移除并返回此列表的最后一个元素。
boolean removeLastOccurrence(Objcet o): 从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

下面我们就以阅读源码的方式来了解LinkedList内部是怎样维护链表的。

LinkedList本质

LinkedList调用默认构造函数,创建一个空链表。维护了一个表头,表尾的Node对象的变量。LinkedList实现了双向队列的功能,即可向表头加入元素,也可以向表尾加入元素。

//成员变量:表头,表尾
transient Node<E> first;
transient Node<E> last;
//默认构造函数,表示创建一个空链表
public LinkedList() {
    }

下面来了解Node类的具体情况

private static class Node<E> {
        //表示集合元素的值
        E item;
       //指向下个元素
        Node<E> next;
     //指向上个元素
        Node<E> prev;
...................................省略
		//双向链表???
    }

下面以增加操作,具体了解LinkedList的工作原理。

public boolean add(E e) {
        linkLast(e);
        return true;
    }

调用linkLast(e);方法,默认向表尾节点加入新的元素

void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

更新表尾节点,建立连接。其他操作类似,维护了整个链表。

下面具体来看,如何将“双向链表和索引值联系起来的”?

 public E get(int index) {
        checkElementIndex(index);//检查索引是否有效
        return node(index).item;
    }

调用了node(index)方法返回了一个Node对象,其中node(index)方法具体如下

Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

类似于二分查找。
首先会比较“index”和“双向链表长度的1/2”;若前者小,则从链表头开始往后查找,直到index位置;否则,从链表末尾开始先前查找,直到index位置。这就是“双线链表和索引值联系起来”的方法。

到此我们便会明白,LinkedList在插入、删除元素时性能比较出色,随机访问集合元素时性能较差,因为查找比较消耗性能。

LinkedList遍历方式

LinkedList支持多种遍历方式。

  1. 通过迭代器遍历LinkedList
  2. 通过快速随机访问遍历LinkedList
  3. 通过for循环遍历LinkedList
  4. 通过pollFirst()遍历LinkedList
  5. 通过pollLast()遍历LinkedList
  6. 通过removeFirst()遍历LinkedList
  7. 通过removeLast()遍历LinkedList

其中采用逐个遍历的方式,效率比较高。采用随机访问的方式去遍历LinkedList的方式效率最低。

LinkedList也是非线程安全的。

小结
  1. LinkedList底层维护了一个链表,但是LinkedList同样是List的实现类,也可以使用索引获取其中元素;
  2. 创建LinkedList时底层创建了一个空的双向链表,添加元素时就像在链表后端增加元素一样;
  3. 双线链表如何与索引联系起来?首先判断索引值与链表一半的大小关系,然后再进行查找。类似于二分查找;
  4. LinkedList相比与ArrayList新增了removeFirst() pollFirst()这种遍历方式,其中通过索引遍历的效率最低,而逐个遍历的效率最高;
  5. LinkedList也是线程不安全的;
ArrayList与LinkedList性能对比

ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。ArrayList应使用随机访问(即,通过索引序号访问)遍历集合元素。
LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。LinkedList应使用采用逐个遍历的方式遍历集合元素。
如果涉及到“动态数组”、“栈”、“队列”、“链表”等结构,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
(01) 对于需要快速插入,删除元素,应该使用LinkedList。
(02) 对于需要快速随机访问元素,应该使用ArrayList。
(03) 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值