天天看点

随记——java基础——集合——202006141. JAVA集合框架概述2. Collection接口方法3. Iterator迭代器接口4. Collection子接口一:List5. Collection子接口二:Set6. Map接口7. Collections工具类

尚硅谷_Java零基础教程-java入门必备-初学者基从入门到精通全套完整版(宋红康主讲) P513-564

文章目录

  • 1. JAVA集合框架概述
  • 2. Collection接口方法
  • 3. Iterator迭代器接口
  • 4. Collection子接口一:List
    • ArrayList源码分析
    • LinkedList源码分析
    • List接口中常用方法
  • 5. Collection子接口二:Set
    • *hashCode()的重写
    • LinkedHashSet
    • TreeSet
  • 6. Map接口
    • HashMap底层实现
      • jdk7
      • jdk8
    • HashMap底层源码分析——jdk7
    • HashMap底层源码分析——jdk8
    • LinkedHashMap底层实现
    • Properties
  • 7. Collections工具类
    • 操作List
    • 同步控制

1. JAVA集合框架概述

  • 一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而java集合就像一种容器,可以动态地把多个对象的引用放入容器中。
    • 数组在内存存储方面的特点:
      • 数组初始化之后,长度就确定了。
      • 数组声明的类型,就决定了进行元素初始化时的类型
    • 数组在存储数据方面的弊端:
      • 长度不可变,不便于扩展。
      • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数。
      • 数组存储的数据是有序的、可以重复的。——存储数据的特点单一。
  • JAVA集合类可以用于存储数量不等的多个对象,还可以用于保存具有映射关系的关联数组。
  • 集合的使用场景,例如:
    • JSON对象或JSON数组(客户端)<==>Java对象或Java对象构成的List(服务器端)
  • Java集合可分为Collection和Map两个体系

    |---- Collection接口:单列数据,定义了存取一组对象的方法的集合

       |---- List:元素有序、可重复

       |---- Set:元素无序、可重复

    |---- Map接口:双列数据,保存具有映射关系的“key-value”键值对的集合。

       |---- HashMap, LinkedHashMap, TreeMap, Hashtable, Properties

    随记——java基础——集合——202006141. JAVA集合框架概述2. Collection接口方法3. Iterator迭代器接口4. Collection子接口一:List5. Collection子接口二:Set6. Map接口7. Collections工具类

2. Collection接口方法

package java.util;

import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;



public interface Collection<E> extends Iterable<E> {
    // Query Operations

    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);

    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

    boolean retainAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

           
  • contains调用的是equals方法。
  • retainAll求交集
  • equals方法要求当前集合和形参集合的元素都相同。
  • 数组–>集合: Arrays.asList();
    • Arrays.asList(new int[]{123,456}) //size()=1,识别为一个元素,类型为int[]
    • Arrays.asList(new Integer[]{123,456}) //size()=2,识别为两个元素

3. Iterator迭代器接口

  • Iterator对象被称为迭代器(设计模式的一种),主要用于遍历Collections集合中的元素。
  • GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
  • 类似“公交上的售票员”
  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterate()方法,那么所有实现了Collection接口的集合类都有一个iterate()方法,用以返回一个实现了Iterator接口的对象。
  • Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
  • 集合对象每次调用iterate()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
Iterator iterator = collection.iterator();
        while (iterator.hasNext()){
            //next():1.指针下移 2.将下移后指向的元素返回
            System.out.println(iterator.next());
        }
           

错误方式一:

while (iterator.next()!=null){
            System.out.println(iterator.next());
        }
           

错误方式二:死循环

while (collection.iterator().hasNext()){
            System.out.println(collection.iterator());
        }
           
  • remove()方法
    • Iterator 可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,而不是集合对象的remove方法。
    • 如果还未调用next()或在上一次调用next方法之后以及调用了remove方法,再调用remove会报

      IllegalStateException

例如:

while (iterator.hasNext()){
            //next():1.指针下移 2.将下移后指向的元素返回
            System.out.println(iterator.next());
            iterator.remove();
            iterator.remove();
        }
           
  • foreach内部调用了迭代器。

    注意:

String[] arr = new String[]{"MM","MM","MM"};

//        for (int i = 0; i < arr.length; i++) {
//            arr[i] = "GG";
//        }//[GG, GG, GG]

        for (String s: arr) {
            s = "GG";
        }//[MM, MM, MM]

        System.out.println(Arrays.toString(arr));
           

4. Collection子接口一:List

  • 鉴于java中数组用来存储数据的局限性,通常使用List替代数组。
  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • List接口实现类常用的有:ArrayList,LinkedList,Vector(?常用)
    • Vector底层使用Object[]数组,线程安全
    • ArrayList底层使用Object[]数组
    • LinkedList底层使用双向链表

ArrayList源码分析

  • ArrayList扩容机制(jdk7)
    • 空参构造器创建一个为10的Object[]数组elementData
    • 当添加元素使得当前size == elementData.length时,扩容为原来数组的1.5倍,将原来数组的元素copy到新数组。
  • ArrayList扩容机制(jdk8)
    • 空参构造器,elementData初始化为{}
    • 第一次调用add()时,底层才创建了长度为10的数组,并将add的数据添加到位置上。
    • 后续扩容与jdk7无异
  • “相当于”从饿汉式变成懒汉式。
  • jdk9源码:

    初始化(空参构造器):

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
           
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
           

添加元素

public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
           

调用内部add:

private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
           

扩容:

private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
           

调用内部扩容:

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); //1.5倍
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
           

LinkedList源码分析

  • 空参构造器:first和last都为null
  • add() 和addLast()都调用内部linkLast()(linkFirst与之对应)
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++;
    }
           
  • Vector底层创建长度为10的数组。扩容为2倍。

List接口中常用方法

  • subList(int fromIndex,int toIndex):类似substring,返回一个子list
  • 常用:增删改查、遍历
  • 注意:
    • list.remove(2)

      :删除下标为2的元素
    • list.remove(new Integer(2))

      删除元素2

5. Collection子接口二:Set

|---- Set接口

  |---- HashSet:线程不安全,可以存储null值

    |----LinkedHashSet:可以按照添加的顺序遍历

  |----TreeSet:可以排序

  • Set接口中没有定义额外的方法。
  • 无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据hash值决定。
  • 不可重复性:equals判断
  • 向set中添加的数据,其所在的类一定要重写equals和hashcode方法。并且重写的equals和hashcode方法尽可能保持一致性。

*hashCode()的重写

重写hashCode()的基本原则:

  • 在程序运行时,同一个对象多次调用hashCode()返回相同的值
  • 当两个对象equals()方法返回true时,这两个对象的hashCode()的返回值也应相等。
  • 对象中用作equals()比较的Field,都应该用来计算hashCode值。

代码示例:

class Enn{
    int id;
    String name;
    
	@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Enn enn = (Enn) o;
        return id == enn.id &&
                Objects.equals(name, enn.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}
           
public static int hash(Object... values) {
        return Arrays.hashCode(values);
    }
           
public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }
           

为什么有31这个数字?

  • 选择系数时要尽可能大,因为计算出来的hash地xiangc址越大,hash冲突的概率就越低。(减少冲突)
  • 31只占用5bits,相乘造成数据溢出的概率较小。
  • 31可以由i<<5-1得到,效率较高。(提升效率)
  • 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和1和被乘数的因数整除。(减少冲突)

LinkedHashSet

  • LinkedHashSet是HashSet的子类。
  • HashSet底层是用HashMap实现的,其中,所有key的value都指向同一个静态的Object对象。
// Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
           
随记——java基础——集合——202006141. JAVA集合框架概述2. Collection接口方法3. Iterator迭代器接口4. Collection子接口一:List5. Collection子接口二:Set6. Map接口7. Collections工具类

TreeSet

  • 向TreeSet添加的对象,要求是相同类的对象
TreeSet treeSet = new TreeSet();
        treeSet.add(1);
        treeSet.add(1l); //报错
        treeSet.add(1.0d);
        treeSet.add(1.0f);
        for (Object o:treeSet         ) {
            System.out.println(o);
        }
           

报错:

java.lang.ClassCastException

  • 两种排序方式:自然排序和定制排序
    • 注意:TreeSet在自然排序和定制排序中,比较两个对象是否相同的标准,compareTo() / compare() 返回0,不再是equals()方法。

6. Map接口

|---- Map接口

  |---- HashMap:线程不安全,可以存储null的key和value值

    |----LinkedHashMap:可以按照添加的顺序遍历。在原有的hashmap结构上,添加了一对指针,指向前一个和后一个。对于频繁的遍历操作,效率高于hashmap。

  |----TreeMap:可以排序。采用红黑树。

  |----Hashtable:作为古老的实现类,线程安全,效率低,不可以存储null的key和value值

    |----Properties

  • Map中的key:无序、不可重复,使用Set存储所有的key。——key所在的类要重写equals和hashcode(HashMap,不包括TreeMap)
  • Map中的value:无序、可重复,使用Collection存储所有的value
  • Map中的entry:无序、不可重复,使用Set存储所有的entry

HashMap底层实现

jdk7

  1. new HashMap<>():实例化后,创建一个长度是16的一维数组 Entry<K,V>[] table
  2. map.put(k1,k2):调用k1所在类的hashCode()计算k1哈希值,此哈希值经过某种算法计算之后,得到在Entry数组中的存放位置。
    1. 如果此位置上的数据不为空,则添加数据。
    2. 如果不为空,比较key和已经存在的一个或多个数据的hash值:如果都不相同,则添加数据;如果hash值相同,则进一步比较(调用k1的equals方法),如果false,添加成功;如果true,替换该value值。
    3. 上面第2步中,添加数据以链表的形式存储。

jdk8

  1. new HashMap<>():没有创建一个长度是16的一维数组
  2. jdk底层数组是 Node<K,V>[]
  3. 首次调用put方法,底层创建长度为16的数组。
  4. 底层结构增加了红黑树:当数组某一个索引位置上链表长度超过8时且当前数组长度超过64,则该索引位置上所有数据改为红黑树存储。

HashMap底层源码分析——jdk7

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

           

调用:

public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                        initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                        loadFactor);

            //Find a power of 2 >= initialCapacity
            int capacity = 1;
            while (capacity<initialCapacity)
                capacity <<= 1;

            this.loadFactor = loadFactor;
            threshold = Math.min(capacity*loadFactor,MAXIMUM_CAPACITY);
            table = new Entry[capacity];
            useAltHashing = sun.misc.VM.isBooted() && 
                    (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
            init();
        }
           

put()方法:

(略)

addEntry方法:

void addEntry(int hash, K key ,V value, int bucketIndex){
	//超出临界值且要存入的位置非空时
	if((size>=threshold) && (null != table[bucketIndex])){
		resize(2*table.length);
		bucketIndex = indexFor(hash,table.length);
	}
	createEntry(hash,key,value,bucketIndex);
}
           

resize()方法:扩容

HashMap底层源码分析——jdk8

put()方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//添加数据
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果要插入的节点的hash值等于当前位置的节点p的hash值,且key值也相等(==),或(key值不为空)key值相等(equals)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //用e保存p节点
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//该位置上第一个hash值不等,遍历该位置其它节点
                for (int binCount = 0; ; ++binCount) {
                	// 如果p.next是null,把数据添加的p的next                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判断是否变红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果遍历到的节点e的key值与key相等,跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //被替换掉的节点e不为空,返回它原来的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //替换掉该位置的value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
           

resize()方法:扩容

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // 第一次进入
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
        	//遍历旧的table,复制数据到新的table
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //该位置只有一个节点,直接复制到新位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果e是树形结构(即红黑树)
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 即此位置是链表结构
                    else { // preserve order
                    	// newCap是oldCap的两倍,将新表看作两个区,lo区,和hi区,根据节点在高位上的值(e.hash & oldCap)决定去哪个区。那么一个链表将分成两个链表,用四个变量表示它们的首尾
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //此时的e去lo区
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //此时的e去hi区
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

           

链表变红黑树

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //表为空,扩容;或表长度<MIN_TREEIFY_CAPACITY(default 64),不建议变成红黑树,先进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
           

LinkedHashMap底层实现

  • LinkedHashMap中的内部类Entry继承自HashMap.Node,并增加了before, after来实现双向链表
  • 子类的newNode()方法会覆盖掉父类的newNode()方法,那么在LinkedHashMap中调用HashMap的方法中,新生成节点调用的是LinkedHashMap的newNode()方法
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
           
  • HashMap预留了给LinkedHashMap 覆盖的方法():用于处理双向链表相关操作
// Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
           

accessOrder生成的时候默认为false;accessOrder为ture时,表示当访问某个数据之后,会把这个数据挪到末尾。

/**
     * The iteration ordering method for this linked hash map: {@code true}
     * for access-order, {@code false} for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;
           

Properties

  • Properties是Hashtable的子类,用于处理属性文件
  • 由于属性文件中的key、value都是字符串类型,所以Properties里的key和value都是字符串类型
  • 存取数据时,建议使用setProperty方法和getProperty方法。
Properties properties = new Properties();
        properties.load(new FileInputStream("jdbc.properties"));
        String user = properties.getProperty("user");
        properties.setProperty("user","123");
           

7. Collections工具类

Collections是操作Collection和Map的工具类

操作List

ArrayList<Object> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        Collections.reverse(list); // 反转顺序
        Collections.shuffle(list); //随即排序
        Comparator<Object> comparator = new Comparator<>() {
            @Override
            public int compare(Object o1, Object o2) {
                return 0;
            }
        };
        Collections.sort(list, comparator);//排序
        Collections.swap(list,0,1);//交换元素
        Collections.max(list,comparator);//最大值
        Collections.frequency(list,1);//对象出现的次数
        /*
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");
         */
        List<Object> des = Arrays.asList(new Object[list.size()]);
        Collections.copy(list, des);//复制,
        Collections.replaceAll(list, (Integer)1,(Integer)2);//替换
    
           

同步控制

把List、Map、Set等转成线程安全的

List<Object> synList = Collections.synchronizedList(list);
Map<Integer, Object> synMap = Collections.synchronizedMap(new HashMap<Integer, Object>() {
        });