欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java集合学习笔记

程序员文章站 2024-02-16 10:41:40
...

Java集合学习笔记

1.集合简述

1.1集合基本概念

  集合本身就是容器,用于存储元素(实际上存储的是引用)。不同的集合实体类的底层数据结构是不一样的,如ArrayList的底层数据结构是数组,LinkedList的底层数据结构是双向链表,HashMap的底层数据结构是哈希表。常见的集合接口是Collection和Map。两者明显的区别在于,前者存储的值是单一的,可以遍历,后者存储的值是键值对的方式,不可遍历。如下图:

Java集合学习笔记

  这里需要再次强调的是,不同的集合底层依赖的数据结构是不一样的,也就是说元素的组织方式不一样,要根据具体的情况,选择适当的集合。(PS:加强数据结构的学习和复习

1.2主要集合继承图谱

Collection

Java集合学习笔记

Map

Java集合学习笔记


2.Collection

2.0引言

  Collection接口继承了Iterable接口,该接口保证了Collection任意的实现类都拥有一种能力–可迭代(遍历),最明显的体现就是 foreach 循环可以实现Collection的任意实现类的遍历。

List<String> arr = new ArrayList<>();
arr.add("1");
arr.add("2");
arr.add("3");

for(String s:arr){
    System.out.println(s);
}

结果

1

2

3

事实上,foreach循环适用于数组以及任何实现了Iterable接口的类,以ArrayList为例,上面的foreach循环等价于

 Iterator<String> iterator = arr.iterator();
 while (iterator.hasNext()){
     String s = iterator.next();
     System.out.println(s);
}

其中hasNext()和next()都是接口Iterator的重要方法,下面讲述Iterable和Iterator接口。


2.1Iterable和Iterator

  Iterable的字面意思是可迭代的,可遍历的,表示一种能力,任何集合如果想能被遍历,那么就必须实现这个接口

public interface Iterable<T> {
    // main method
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

  而里面核心的方法 Iterator<T> iterator();必须被实现。该方法会返回一个Iterator,即迭代器,所以如果要让自定义的集合能被遍历,除了重写Iterable接口以外,还需要重写Iterator接口。

public interface Iterator<E> {
   
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

Iterator迭代器接口可以按照下面的图理解

Java集合学习笔记

  1. hasNext方法用于查看该集合是否还有剩余元素,如果有就返回true,没有就返回false。

  2. next方法指向下一个元素,并返回上一个元素。这个方法唯一的问题就是可能并没有下一个元素,当并没有下一个元素时,该方法将抛出 NoSuchElementException.所以为了保证每一次next都没有问题,那么应该预先用hasNext()方法判断是否有剩余元素。

  3. remove用于删除上一个元素,这个方法依赖于next方法,调用一次next方法后,才能使用remove方法,否则就会抛出IllegalStateException。因为最开始并不存在上一个元素,所以必须调用next方法,其次当调用一次remove方法后,上一个元素已经被删掉了,不存在上一个元素,所以必须调用next方法。

  4. 还需注意的是,获取迭代器之后,如果修改集合的结构(比如添加元素,删除元素等,当然除了迭代器的remove方法除外),然后再使用迭代器遍历,则会出现java.util.ConcurrentModificationException

    List<String> arr = new ArrayList<>();
            arr.add("1");
            arr.add("2");
            arr.add("3");
    
            Iterator<String> iterator = arr.iterator();
    //        arr.remove(2); wrong code
    //        arr.add("4"); wrong code
            while (iterator.hasNext()){
                System.out.println(iterator.next());
            }
    

    这也是为什么不能在foreach循环中轻易修改集合结构的原因。


2.2自定义ArrayList

  自己定义可遍历的集合的话,比如自定义ArrayList,必须实现Iterable接口和需要一个Iterator的实现类。这里需要知道的是上面提到三个异常``NoSuchElementException,IllegalStateException,ConcurrentModificationException`都是非受检异常,你自己处理不处理其实无所谓,为了规范还是自己处理一下。

Java集合学习笔记

下面的代码仿照ArrayList来写的,主要方法如下所示

Java集合学习笔记

MyArrayList

package com.lordbao.iterator.MyArrayList;

import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Consumer;

/**
 * @Author Lord_Bao
 * @Date 2020/9/3 8:40
 * @Version 1.0
 */

public class MyArrayList<T> implements Iterable<T> {

    //表示数组的元素个数  也代表下一个默认待插入的位置的下标
    private int size;

    //默认容量为10
    private static final int DEFAULT_CAPACITY = 10;

    //空数组,无参构造函数调用,或是初始化容量为0时,将是下面这个数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //MyArrayList  的 底层数据结构  数组
    private Object[] elementData;

    public MyArrayList() {
        elementData = EMPTY_ELEMENTDATA;
    }

    public MyArrayList(int initialCapacity) {
        if (initialCapacity == 0) elementData = EMPTY_ELEMENTDATA;
        else if (initialCapacity > 0) elementData = new Object[initialCapacity];
        else
            throw new IllegalArgumentException("InitialCapacity " + initialCapacity + " can't be below 0");
    }

    public boolean add(T t) {
        return add(size, t);
    }

    public boolean add(int index, T t) {
        addIndexCheck(index);
        ensureCapacity();
        //刚好在size处插入元素 也满足
        for (int j = size - 1; j >= index; j--) {
            elementData[j + 1] = elementData[j];
        }
        elementData[index] = t;
        size++;
        return true;

    }


    public T getLast() {
        return get(size - 1);
    }

    public T get(int index) {
        getOrRemoveIndexCheck(index);
        return (T) elementData[index];
    }

    public T remove(int index) {
        getOrRemoveIndexCheck(index);
        T result = (T) elementData[index];
        for (int i = index + 1; i <= size - 1; i++) {
            elementData[i - 1] = elementData[i];
        }
        elementData[size - 1] = null;
        size--;
        return result;
    }

    public T removeLast() {
        return remove(size - 1);
    }

    public boolean isEmpty(){
        return size==0;
    }

    public int size(){
        return size;
    }


    private void getOrRemoveIndexCheck(int index) {
        if (index < 0 || index >= size)
            throw new IllegalArgumentException("Index " + index + " out of bounds");
    }

    private void addIndexCheck(int index) {
        if (index < 0 || index > size)
            throw new IllegalArgumentException("Index " + index + " out of bounds");
    }


    private void ensureCapacity() {
        //如果是空数组,就扩容
        if (elementData == EMPTY_ELEMENTDATA) {
            elementData = new Object[DEFAULT_CAPACITY];
        } else {
            //检查容量是否充足
            ensureCapacity(size);
        }
    }

    private void ensureCapacity(int size) {
        //如果数组存储的元素达到数组容量则进行扩容
        if (size >= elementData.length)
            expandCapacity();
    }

    private void expandCapacity() {
        //扩容2倍
        int newCapacity = elementData.length << 2;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }


    @Override
    public Iterator<T> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<T> {

        int pointer;
        int lastRet=-1;//上一个被返回的元素坐标,如果没有就是-1

        @Override
        public boolean hasNext() {
            return pointer < size;
        }

        @Override
        public T next() {
            if (pointer>=size)
                throw new NoSuchElementException();

            //记录最近一次返回的元素坐标
            lastRet = pointer;
            return (T) elementData[pointer++];
        }

        @Override
        public void remove() {
            if (lastRet <0)
                throw new IllegalStateException();
            MyArrayList.this.remove(lastRet);

            pointer = lastRet; //这样的目的是下一次next以后,pointer仍然指向正确
            lastRet = -1;//一次删除之后,必须next,这样才能修改lastRet来继续删除下一个元素
        }

        @Override
        public void forEachRemaining(Consumer<? super T> action) {
            //暂时不写
        }
    }
}

测试代码

package com.lordbao.iterator.MyArrayList;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * @Author Lord_Bao
 * @Date 2020/9/3 8:41
 * @Version 1.0
 */
public class Main {

    public static void main(String[] args) {
        MyArrayList<String> arr = new MyArrayList<>(1);
        arr.add("h");
        arr.add("i");

        Iterator<String> iterator = arr.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            System.out.println(next);
        }

        //增强for循环 等价于上面的while
        for (String s : arr) {
            System.out.println(s);
        }

    }
}

结果

h

i

h

i


2.3Collection常见方法

Java集合学习笔记

测试上面部分方法

package com.lordbao.collection;

/**
 * @Author Lord_Bao
 * @Date 2020/9/1 10:50
 * @Version 1.0
 */


import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;

/**
 *  用于测试Collection接口常见的方法
 *
 * boolean add(E e)
 * boolean remove(Object o)
 * boolean  contains(Object o)
 * void clear()
 * int size()
 * boolean isEmpty()
 * Object[] toArray()
 * iterator<E> iterator()
 */
public class Main1 {

    public static void main(String[] args) {
        Collection<Man> arr = new ArrayList<>();
        //add
        arr.add(new Man("jack"));
        arr.add(new Man("rose"));
        arr.add(new Man("loki"));
        arr.add(new Man("jenny"));
        //remove 底层会调用equals
        arr.remove(new Man("loki"));
        //iterator
        Iterator<Man> iterator = arr.iterator();
        while (iterator.hasNext()){
            Man man = iterator.next();
            System.out.println(man);
        }
        //contains  底层会调用equals
        System.out.println(arr.contains(new Man("jack")));

        //toArray
        Object[] objects = arr.toArray();
        System.out.println(objects[0]);

        //clear size isEmpty
        arr.clear();
        System.out.println(arr.size());
        System.out.println(arr.isEmpty());


    }
}

class  Man{
    private String name;

    public Man() {
    }

    public Man(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Man man = (Man) o;
        return Objects.equals(name, man.name);
    }

    @Override
    public String toString() {
        return "Man{" +
                "name='" + name + '\'' +
                '}';
    }
}

这里需要主要的是contains和remove底层会调用equals方法,怎么判断元素是否相等需要自己去拿捏。

测试结果

Man{name=‘jack’}
Man{name=‘rose’}
Man{name=‘jenny’}
true
Man{name=‘jack’}
0
true


2.4List

  List继承自Collection,它的元素的最大特点是有序可重复,即存储的顺序和遍历的顺序是相同的,重复指的是相同的元素可以重复存储。

  List最常见的实现类是ArrayList和LinkedList以及Vector。ArrayList和Vector的底层都是数组,前者不是线程安全的,后者是线程安全的,但是Vector效率比较低下,并且已经有新的方式保证线程安全,所以ArrayList应用更加广泛。


2.4.1List常见的方法

Java集合学习笔记

测试代码

package com.lordbao.list;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * @Author Lord_Bao
 * @Date 2020/9/1 16:18
 * @Version 1.0
 */
public class Main {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //  add(int index , E e)
        list.add(0,"world");
        list.add(0,"the");
        list.add(0,"hello");
        //打印
        list.forEach(System.out::println);

        System.out.println("---------------------------------");
        //set(int index,E e)
        list.set(0,"hi");
        list.forEach(System.out::println);
        System.out.println("---------------------------------");

        //E get(int index)
        System.out.println(list.get(0));
        // E remove(int index)
        System.out.println(list.remove(0));
        //  indexOf   lastIndexOf
        System.out.println(list.indexOf("the"));
        System.out.println(list.lastIndexOf("the"));
    }
}

结果

hello
the

world

hi
the

world

hi
hi
0
0


2.4.2ArrayList

  ArrayList的底层是数组,数组的特点就是随机访问快,增删效率较低(涉及移动大量元素),而且数组分配的空间是连续的,当数组大到一定程度的时候,就要考虑存储问题。

  ArrayList的底层是数组,但元素增加的时候,就要考虑扩容问题,而扩容就会涉及移动元素,所以怎么扩容也是一个很重要的问题。


2.4.3LinkedList

  LinkedList底层是双向链表,双向链表的特单是增删新的节点效率高,但是访问元素的效率就要低一点。在空间存储上,双向链表的节点不是连续的。


2.4.4Vector

  Vector底层也是数组,它是线程安全的,但是效率比较低,事实上,可以通过Collections工具类,让ArrayList变得线程安全.List<String> list = Collections.synchronizedList(new LinkedList<>());


2.5Set

  Set没有特别的方法,这里不讲。

2.5.1HashSet

  HashSet底层采用的是HashMap。参见HashMap

2.5.2TreeSet

  TreeSet底层采用的是TreeMap。参见TreeMap


3.Map

  Map以键值对的形式存储数据,它的键部分就是Set,无序不可重复。

3.1Map常见的方法

常见的方法测试如下

package com.lordbao.map;

import java.util.*;

/**
 * @Author Lord_Bao
 * @Date 2020/9/2 8:09
 * @Version 1.0
 */

/**
 * 测试常见的方法
 */
public class Main {
    public static void main(String[] args) {
        Map<Integer,String> map =new HashMap<>();
        //存
        map.put(1,"hello");
        map.put(2,"jsjsis");
        map.put(3,"the");
        map.put(4,"world");

        //取
        System.out.println(map.get(1));

        //删
        System.out.println(map.remove(2));

        //检查是否包含  key  和 value
        System.out.println(map.containsKey(1));
        System.out.println(map.containsValue("hello"));

        //获得Key部分 并遍历
        Set<Integer> integerSet = map.keySet();
        for (Integer integer : integerSet) {
            System.out.println(integer);
        }

        //获得values部分 并遍历
        Collection<String> values = map.values();
        for (String s:values){
            System.out.println(s);
        }

        // k  和  v封装成一个集合
        Set<Map.Entry<Integer, String>> set = map.entrySet();
        for (Map.Entry<Integer,String> node:set){
            System.out.println("key="+node.getKey()+",value="+node.getValue());
        }

        //清空  查看map元素个数 查看map是否为空
        map.clear();
        System.out.println(map.size());
        System.out.println(map.isEmpty());

    }
}



3.2HashMap

  HashMap的底层数据结构是哈希表,哈希表是数组和链表的产物。下面重点说HashMap的两个方法

get(Object key) ,put(K key,V value)

  为什么HashMap的Key是不可重复的呢?

  因为每次调用put方法的时候,底层会调用Key的hashcode方法,然后计算出hash值,最后再通过哈希算法获得数组的下标,如果下标的元素是null,那么证明没有值存储,那么Key 和Value封装成头结点放在这个下标处。如果下标的元素不是null,那么就会遍历该下标下的链表,调用equals方法,如果都为false,则将Key 和Value封装成结点放在链表末尾,否则证明有重复Key存在,不执行put操作。

  get方法的原理是类似的,计算出数组下标,如果下标处的元素为null,说明没有值,返回null,否则沿着链表一次调用Key的equals方法进行判断,如果都为false,则返回null。

  综上所述,HashMap的不可重复的原因是底层调用了equals方法,保证Key不存在重复,而下标的计算实际是过滤了很多不满足条件的节点。

Java集合学习笔记

  为什么说HashMap的Key是无序的呢?

  其实存储Key的过程是比较复杂的,要经过运算,这个存储的过程是无序的,而取值是有一定顺序的,比如从每个数组元素的的链表进行遍历。存储无序,取值有序,就会照成存储值的时候是一种顺序,取值的时候是另一种顺序。

终上所述,如果要让HashMap发挥作用,那么Key部分就必须要合理重写equals和hashcode方法

3.3Properties

  Properties继承自HashTable,Hashtable的底层是哈希表,类似于ArrayList和Vector的关系,HashTable是线程安全的,而HashMap不是线程安全的,不过可以通过下面的方法,使得HashMap是线程安全的。

 Map<String,String> map= new HashMap<>();
map = Collections.synchronizedMap(map);

  Proerties的Key和Value都是存储String,该类常用来读取以键值对存储的配置文件


3.4TreeMap

  TreeMap的底层是二叉树,这部分数据结构不熟悉,有时间去学习和复习。TreeMap的Key部分是TreeSet,这个集合也是无序不可重复,不可重复的原因不清楚,猜测是进行了equals判断,无序是因为存储数据的时候依赖的是二叉树,取出数据的时候是大小有序的。

  使用TreeMap,就需要保证Key部分实现了Comparable接口,或者是new TreeMap集合的时候,传入一个实现了Comparator接口的实现类。

下面以TreeSet为例,按照Worker工资由高到低,同工资按照姓氏升序进行排序。

package com.lordbao.map;

import java.util.*;

/**
 * @Author Lord_Bao
 * @Date 2020/9/2 14:06
 * @Version 1.0
 */
public class TestTreeSet {

    public static void main(String[] args) {

  
        //方法1
        Set<Worker> set = new TreeSet<>();
        set.add(new Worker("Adams",1000));
        set.add(new Worker("Athens",1000));
        set.add(new Worker("Bob",1200));

        for (Worker worker : set){
            System.out.println(worker);
        }
        System.out.println("");
        //方法2(可以使用Lambda表达式)
        Set<Worker> set1 =  new TreeSet<>(new Comparator<Worker>() {
            @Override
            public int compare(Worker o1, Worker o2) {
                if (o1 == o2) return 0;
                if (o1 == null) return -1;
                if (o2==null) return 1;

                if (o1.getSalary() == o2.getSalary()) return o1.getName().compareTo(o2.getName());

                return o2.getSalary() -o1.getSalary();

            }
        });

        set1.add(new Worker("Adams",1000));
        set1.add(new Worker("Athens",1000));
        set1.add(new Worker("Bob",1200));

        for (Worker worker : set1){
            System.out.println(worker);
        }
    }
}

class  Worker implements Comparable<Worker>{
    private String name="";
    private  int salary=0;

    @Override
    public String toString() {
        return "Worker{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                '}';
    }

    public Worker() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    public Worker(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    //按照工资降序排列,工资相同,名字升序排列
    @Override
    public int compareTo(Worker o) {
        if (o==null) return 1;

        if (salary == o.salary) return name.compareTo(o.name);

        return o.salary-salary;
    }
}

结果

Worker{name=‘Bob’, salary=1200}
Worker{name=‘Adams’, salary=1000}
Worker{name=‘Athens’, salary=1000}

Worker{name=‘Bob’, salary=1200}
Worker{name=‘Adams’, salary=1000}
Worker{name=‘Athens’, salary=1000}


4.总结

  集合这一部分,大概分为Collection和Map,根据实际情况去选用合适的集合。这部分的数据结构有些不熟悉,有些熟悉的也模棱两可的,所以数据结构还需要加强。

Java集合学习笔记