Java各种集合类知识总结(超级详细!!!)

集合

数组和集合的区别?

  1. 集合可以动态保存任意多个对象,使用比较方便,数组是固定长度的数据结构
  2. 集合提供了一系列方便的操作对象的方法: add、remove、 set、 get,数组则直接访问元素
  3. 数组可以包含基本数据类型和对象,而集合只能包含对象。

集合框架图:

//1.集合主要分两组(单列集合,双列集合)
//2.Collection接口有两个重要的子接口 List Set,它们的实现子类都是单列集合(集合中存放单个元素)
//3.Map接口实现子类属于双列集合,存放的k-v

Collection:
在这里插入图片描述

Map:
在这里插入图片描述

Iterator接口:

collection继承了iterator接口,任何集合类都可以用增强for循环遍历!

public interface Iterator<E> {
    boolean hasNext();//是否存在下一个

    E next();//逐个访问集合中元素

    default void remove() {//删除上一个next的元素
        throw new UnsupportedOperationException("remove");
    }

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

Collection接口:

  1. collection实现子类可以存放多个元素,每个元素可以是Object

  2. 有些Collection的实现类,可以存放重复的元素,有些不可以

  3. 有些Collection的实现类,有些是有序的(List), 有些不是有序(Set)

  4. Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的

    public abstract class AbstractCollection<E> implements Collection<E> 
    

遍历元素的方式

方式1-使用Iterator迭代器

//hasNext()//判断是否有下一个元素
while(iterator.hasNext()){
	//next()作用:1.下移 2.将下移以后集合位置上的元素返回
}
        Collection col = new ArrayList();
        col.add(new Book("三国演义","罗贯中",10.9));
        col.add(new Book("红楼梦","曹雪芹",9.8));
        col.add(new Book("活着","余光华",6.6));
	    //System.out.println("col="+col);
        //逐个遍历
        //1.先得到col对应的迭代器
        Iterator iterator = col.iterator();
        //2.使用while循环遍历
        while (iterator.hasNext()) {//判断是否还有数据
            //返回下一个元素,类型是Object
            Object obj = iterator.next();
            System.out.println("obj=" + obj);//看obj的运行类型
        }
        //快捷键,快速生成while循环:itit
        //显示所有的快捷键的快捷键 ctrl+j
        //3.当退出while循环后,这时iterator迭代器,指向最后的元素
        //iterator.next();//NoSuchELementException
        //4.如果希望再次遍历,需要重置迭代器
        iterator = col.iterator();
        System.out.println("===第二次遍历===");
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println("obj=" + obj);
        }

方式2-增强for循环遍历

Collection col = new ArrayList();
col.add(new Book("三国演义","罗贯中",10.9));
col.add(new Book("红楼梦","曹雪芹",9.8));
col.add(new Book("活着","余光华",6.6));

//1.使用增强for,在 Collection集合
//2.增强for,底层仍然是iterator
//3.增强for可以理解成简化版本的迭代器遍历
//4.快捷方式I
for (Object book:col) {
    System.out.println("book=" + book);
}

List接口:

在这里插入图片描述

//List集合类中元素有序(及添加顺序和取出顺序一致),且可重复
List list = new ArrayList();
list.add("猪八戒");
list.add("哪吒");
list.add("金刚葫芦娃");
list.add("大头儿子");
System.out.println("list="+list);
//2.List集合中的每个元素都有其对应的顺序索引,即支持索引
//索引是从0开始的
System.out.println(list.get(3));
//3.操作元素的一些方法
//1) void add(int index, Object ele):在index位置插入ele元素
//2) boolean addAll(int index, Collection eles):从index位置开始将
//eles中的所有元素添加进来
//3) Object get(int index):获取指定index位置的元素
//4) int indexOf(Object obj):返回obj在集合中首次出现的位置
//5) int lastIndexOf(Object obj):返回obj在当前集合中未次出现的位置
//6) Object remove(int index):移除指定index位置的元素,并返回此元
//素
//7) Objeqt set(int index, Object ele);:设置指定index位置的元素为ele,
//相当于是替换.
//8) List subList(int fromIndex, int tolndex):返回从fromIndex到
//tolndex位置的子集合

List的三种遍历方式

方式1-使用迭代器

Iterator iterator= list.iterator();
while (iterator.hasNext()) {
    Object object =  iterator.next();
    System.out.println("object="+object);
}

方式2-增强for循环

for (Object object : list) {
    System.out.println("object="+object);
}

方式3-普通for循环

for (int i = 0; i < list.size(); i++) {
    System.out.println("object="+list.get(i));
}

ArrayList(不是线程安全的)

  1. ArrayList中维护了一个0bject类型的数组elementData. [debug 看源码] transient Object[] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列化
public ArrayList() {
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//执行list.add (1)先确定是否要扩容 (2)然后在执行赋值
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);//该方法确定minCapacity(1)第一次扩容为10
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;//记录集合被修改的次数

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)//如果elementDate的大小不够,就调用grow()去扩容
        grow(minCapacity);
}
//使用扩容机制来确定要扩容到多大
//第一次newCapacity = 10
//第二次及其以后,按照1.5倍扩容
//扩容使用的是Arrays.copyof()
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  1. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0, 第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1 .5倍
  2. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小, 如果需要扩容,则直接扩容elementData为1.5倍。

LinkedList(不是线程安全的)

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
  //...
}

LinkedList 实现了以下接口:

LinkedList 类图

LinkedList 中的元素是通过 Node 定义的:

private static class Node<E> {
    E item;// 节点值
    Node<E> next; // 指向的下一个节点(后继节点)
    Node<E> prev; // 指向的前一个节点(前驱结点)

    // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

遍历linkedlist方法:

public class Main {
    public static void main(String[] args) {
        //add()和remove()方法在失败的时候会抛出异常(不推荐)
        Queue<String> queue = new LinkedList<String>();
        //添加元素
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
        queue.offer("d");
        queue.offer("e");
        for(String q : queue){
            System.out.println(q);
        }
        System.out.println("===");
        System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
        for(String q : queue){
            System.out.println(q);
        }
        System.out.println("===");
        System.out.println("element="+queue.element()); //返回第一个元素 
        for(String q : queue){
            System.out.println(q);
        }
        System.out.println("===");
        System.out.println("peek="+queue.peek()); //返回第一个元素 
        for(String q : queue){
            System.out.println(q);
        }
    }

ArrrayList和LinkedList的区别?

  • 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;

  • 底层数据结构:ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)

  • 插入和删除是否受元素位置的影响:

    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

    • LinkedList 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)addFirst(E e)addLast(E e)removeFirst()removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element)remove(Object o),remove(int index)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。

  • 是否支持快速随机访问:LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

  • 内存空间占用:ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!就连 LinkedList 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 LinkedList

Vector底层结构和ArrayList的比较:

在这里插入图片描述

Set接口:

元素是无序的,查找速度快。

HashSet

  1. 实际上是HashMap
public HashSet() {//HashSet的构造器
    map = new HashMap<>();
}

​ 2.可以存放null值,但是只能有一个null

​ 3.不保证元素有序性,取决于hash后,在确定索引的结果。

4.不能有重复的元素。
添加元素的流程
HashSet hashSet = new HashSet();
hashSet.add("java");
//1.执行HashSet()
public HashSet() {
    map = new HashMap<>();
}
//2.执行add()
public boolean add(E e) {//e="java"
        return map.put(e, PRESENT)==null;
    //private static final Object PRESENT = new Object();
}
//3.执行put(),该方法会执行hash(key) 得到key对应的hash值 算法h = key.hashCode()  ^ (h >>> 16) 无符号右移动16位
public V put(K key, V value) {//key = "java" value = PRESENT 共享
        return putVal(hash(key), key, value, false, true);
}
//4.执行putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;//定义辅助变量
    //table就是HashMap的一个数组,类型是Node[]
    //if语句表示如果当前table是null,或者大小=0
    //就是第一次扩容,到16个空间
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象,赋给p
    //(2)判断p 是否为null
    //(2.1)如果p 为null,表示还没有存放元素,就创建一个Node(key = "java" value = PRESENT)
    //(2.2)就放在该位置tab[i] = newNode(hash,key,value,null)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //在需要辅助变量的时候,再创建。
        Node<K,V> e; K k;
        //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
        //并且满足下面两个条件之一:
        //(1)准备加入的key 和 p指向的Node结点的Key是同一个对象
        //(2)p指向的Node结点的Key的equals()和准备加入的key比较后相同 就不能加入
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //再判断p是不是一颗红黑树,如果是一颗红黑树,就调用putTreeVal,来进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//如果table对应索引位置,已经是一个链表,就使用for循环比较
            //(1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后,注意在把元素添加到链后,立即判断,该链表是否已经达到8个结点,就调用treefyBi(),对当前这个链表进行树化(转成红黑树),注意,在转成红黑树时,要进行判断,判断条件
            //(2)依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {//如果整个链表,没有和它相同,就加到该链表的最后
                    p.next = newNode(hash, key, value, null);
                    //加入后,判断当前链表的个数,是否已经到8个,到8个后,就调用treeifyBin进行红黑树转换
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&//如果在循环比较过程中,发现有相同,就break,只是替换value
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;//替换key对应的值
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;//每增加一个Node,就size++
    if (++size > threshold)//12->24->24如果size大于临界值就扩容
        resize();//扩容
    afterNodeInsertion(evict);
    return null;
}
  1. HashSet底层是HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75 = 12
  2. 如果table数组使用到了临界值12,就会扩容到16* 2 = 32,新的临界值就是32*0.75 = 24,依次类推
  3. 在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8 ),并且table的大>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制、

练习:定义Employee类,属性有姓名和年龄,加入到hashset中,如果姓名和年龄相等,则不加入。

public class HashSetExercise_ {
    public static void main(String[] args) {
        Employee jack = new Employee("jack", 10);
        Employee mark = new Employee("jack", 10);
        Employee june = new Employee("june", 15);

        HashSet hashSet = new HashSet();
        hashSet.add(jack);
        hashSet.add(mark);//加入不了,因为姓名和年龄存在
        hashSet.add(june);

        System.out.println(hashSet);
    }
}

class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age && Objects.equals(name, employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

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

LinkedHashSet

  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
  3. LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
  4. LinkedHashSet不允许添重复元素

Map接口:

  1. Map 与Collection并列存在。用于保存具有映射关系的数据:Key-VaLue(双列元素)
  2. Map中的key和vaLue 可以是任何引用类型的数据,会封装到HashMap$Node 对象中
  3. Map中的key不允许重复,原因和HashSet一样,前面分析过源码。
  4. Map中的vaLue 可以重复
  5. Map的key可以为null,value也可以为null,注意key为null,只能有一个,value为null可以有多个。
  6. 常用String类作为map的key,但并不是只能用String。
  7. key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value。
  8. Map存放数据的key-value示意图,一对k-v是放在一个HashNode中的, 有因为Node实现了Entry 接口,有些书上也说一对k-v就是一个Entry。
Map hashMap = new HashMap();
hashMap.put("no1", "张无忌");
hashMap.put("no2", "赵敏");
//1. k-v最后是HashMap$Node node = newNode (hash, key, vaLue, nuLl)
//2. k-v为了方便程序员的遍历,还会创建EntrySet 集合,该集合存放的元素的类型Entry, 而一个Entry
//   对象就有k,v EntrySet<Entry<K,V>> 即: transient Set<Map 。Entry<K, V>> entrySet;
//3.在entrySet中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node
//  这是因为HashMap$Node implements Map.Entry
//4.当把HashMap$Node对象存放到entrySet就方便我们的遍历,因为Map.entry提供一个K getKey();
//  还提供一个V getValue();
Set set = hashMap.entrySet();
System.out.println(set.getClass());//HashMap$EntrySet
for (Object object : set) {
    //System.out.println(entry.getClass());//HashMap$Node
    //为了从HashMap$Node 取出K-v
    //1.先做一个向下转型
    Map.Entry entry = (Map.Entry) object;
    System.out.println("key=" + entry.getKey() + " value=" + entry.getValue());
}

HashMap(不是线程安全)

扩容机制和HashSet一样

  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75.
  3. 当添加key-val时,通过key的哈希值得到在table的索引.然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
  4. 第1次添加,则需要扩容table容量为16,临界值(threshold)为12.
  5. 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍即24,依次类推
  6. 在Java8中,如果一条链表的元素个数超过 TREEIFY THRESHOLD(默认是8),并且table的大小>= MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)

HashTable(是线程安全的)

  1. 存放的元素是键值对:即K-V
  2. hashtable的键和值都不能为null,否则会抛出NullPointerException
  3. hashTable使用方法基本上和HashMap-样
  4. Hashtable是通过使用了 synchronized 关键字来保证其线程安全
  5. 简单看下底层结构
//1.底层有数组Hashtable$Entry[] 初始化大小为11
//2. 临界值threshold 8 = 11 * 0.75
//3.扩容:按照自己的扩容机制来进行即可
Hashtable hashtable = new Hashtable();

hashtable.put("中国","chinese");
//hashtable.put(null,"Japan");//Exception in thread "main" java.lang.NullPointerException
//hashtable.put("usa",null);//Exception in thread "main" java.lang.NullPointerException

LinkedHashMap

LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。

LinkedHashMap实现HashMap+LinkedList

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements SequencedMap<K,V>
{ }

初始化:

public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
public LinkedHashMap() {
        super();
        accessOrder = false;
    }
public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

开发中如何选择集合实现类

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

1)先判断存储的类型(一组对象[单列]或组键值对[双列])

2)一组对象: Collection接口

  • 允许重复: List

    • 增删多: LinkedList [底层维护了一个双向链表]

    • 改查多: ArrayList [底层维护Object类型的可变数组]

  • 不允许重复: Set

    • 无序: HashSet [底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)]

    • 排序: TreeSet

    • 插入和取出顺序一致: LinkedHashSet , 维护数组+双向链表

3)一组键值对: Map,

  • 键无序: HashMap [底层是:哈希表jdk7:数组+链表,jdk8: 数组+链表+红黑树]

  • 键排序: TreeMap

  • 键插入和取出顺序一致: LinkedHashMap

  • 读取文件Properties

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值