[ java基础 ] 集合: 扩容机制&底层源码分析

集合的框架体系

Java 的集合类很多,主要分为两大类,如图

  • Collection的实现类

image-20220110110405573

  • Map的实现类

image-20220110110636467

说明:

  1. 集合主要是两组(单列集合 , 双列集合)
  2. Collection 接口有两个重要的子接口 List Set , 他们的实现子类都是单列集合
  3. Map 接口的实现子类 是双列集合,存放的== K-V==

Collection 接口

Collection 接口的特点

  1. Collection实现子类可以存放多个元素,每个元素可以是Object
  2. 有些Collection的实现类, (List集合类)可以存放重复的元素,有些(Set集合类)不可以
  3. 有些Collection的实现类, (List集合类)是有序的,有些(Set集合类)是无序的
  4. Collection接口没有直接的实现子类,是通过他的子接口Set和List来实现的

Collection接口常用方法

  1. add:添加单个元素
  2. remove:删除指定元素
  3. contains:查找元素是否存在
  4. size:获取元素个数
  5. isEmpty:判断是否为空
  6. clear:清空
  7. addAll:添加多个元素
  8. containsAll:查找多个元素是否都存在
  9. removeAll:删除多个元素

Collection接口遍历方式

迭代器遍历

基本介绍
image-20220211161718477

  1. Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。
  2. 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
  3. Iterator的结构[看下图]
  4. Iterator仅用于遍历集合,Iterator本身并不存放对象。

image-20220110112439414.png
image-20220211162212267

注意:

  1. 在while循环语句中使用remove()方法只能使用iterator.remove();即迭代器本身的remove方法,使用接口的remove方法会抛异常
  2. 快捷键,快速生成 while => itit
  3. 显示所有的快捷键的的快捷键 ctrl + j
  4. 当退出 while 循环后 , 这时 iterator 迭代器,指向最后的元素
  5. 如果希望再次遍历,需要重置我们的迭代器

增强for

增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator,本质一样。只能用于遍历集合或数组。

  • 基本语法
    for(元素类型 元素名 : 集合名或数组名){
    访问元素
    }

提示:
快捷键:大写的i

List接口

image-20220211162415361.png

基本介绍

List接口是Collection接口的子接口

  1. List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
  2. List集合中的每个元素都有其对应的顺序索引,即支持索引
  3. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  4. JDK API中List接口的实现类如上图,常用的有:ArrayList , LinkedList和Vector

常用方法

  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. Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换.
  8. List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合[前闭后开)

三种遍历方式 [ArrayList, LinkedList,Vector]

  1. 方式一:使用iterator
    Iterator iter = col.iterator();
    while(iter.hasNext()){
    Object o = iter.next();
    }
  2. 方式二:使用增强for
    for(Object o:col){
    }
  3. 方式三:使用普通for
    for(int i=0;i<list.size();i++){
    Object object = list.get(i);
    System.out.println(object);
    }

说明:使用LinkedList完成 使用方式和ArrayList 一样

练习

冒泡排序

public class ListExercise02 {
public static void main(String[] args) {
//List list = new ArrayList();
List list = new LinkedList();
//List list = new Vector();
list.add(new Book("红楼梦", "曹雪芹", 100));
list.add(new Book("西游记", "吴承恩", 10));
list.add(new Book("水浒传", "施耐庵", 19));
list.add(new Book("三国", "罗贯中", 80));
//list.add(new Book("西游记", "吴承恩", 10));
//如何对集合进行排序
//遍历
for (Object o : list) {
System.out.println(o);
}
//冒泡排序
sort(list);
System.out.println("==排序后==");
    for (Object o : list) {
System.out.println(o);
}
}
//静态方法
//价格要求是从小到大
public static void sort(List list) {
int listSize = list.size();
for (int i = 0; i < listSize - 1; i++) {
for (int j = 0; j < listSize - 1 - i; j++) {
//取出对象 Book
Book book1 = (Book) list.get(j);
Book book2 = (Book) list.get(j + 1);
if (book1.getPrice() > book2.getPrice()) {//交换
list.set(j, book2);
list.set(j + 1, book1);
}
}
}
}
}
class Book{}

ArrayList

基本介绍

  1. permits all elements,including null,ArrayList 可以加入null ,并且多个
  2. ArrayList是由数组来实现数据存储的
  3. ArrayList基本等同于Vector,除了ArrayList是==线程不安全(执行效率高)==看源码.
    在多线程情况下,不建议使用ArrayList

ArrayList扩容机制源码分析

先说结论,再分析源码:

  1. ArrayList中维护了一个Object类型的数组elementData. [debug看源码]
    transient object[ ] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列化
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍
  3. 如果使用的是指定大小的(有参)构造器,则初始elementData容量为指定大小,如果需要扩容
    则直接扩容elementData为1.5倍

示意图:

image-20220110120142487.png

image-20220110120148144
案例演示:
字节码:

ArrayList list = new ArrayList(8);
        //使用for给list集合添加 1-10数据
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }

debug源码:

  • 初始化一个指定长度的对象数组
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
  • 扩容:list.add()[debug]
    image-20220110162332354.png

扩容关键语句:

  1. calculateCapacity方法中的:(判断数组为空,初始化大小为10)

return Math.max(DEFAULT_CAPACITY, minCapacity);//默认值=10

  1. grow方法中的:(若加入后的(所需)容量小于当前数组大小,1.5倍扩容)

int newCapacity = oldCapacity + (oldCapacity >> 1);

Vector

  1. Vector类的定义说明
    image-20220211163028576
  2. Vector底层亦是一个对象数组,protected Object[] elementData;
  3. Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized
    public synchronized E get (int index){
    if (index>=elementCount)
    throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
    }
  4. 在开发中,需要线程同步安全时,考虑使用Vector

Vector扩容机制源码分析

案例演示:
字节码:

Vector vector = new Vector();
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }
        vector.add(100);

Vector vector=new Vector();
无参构造器=>this(10)=>单参构造器=>this(初始容量,单次扩充容量)=>双参构造器

image-20220110164253394.png

扩容解读:

image-20220110164253394.png

关键:
扩容机制:
单参或无参构造器默认隐藏参数capacityIncrement(解释为容量扩充)为0,若指定该参数不为0,则扩容机制发生改变,由原来的默认2倍扩容改为增加capacityIncrement容量

grow方法中:int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);

Vector 和 ArrayList 的比较

image-20220110165537251.png

LinkedList

介绍:

  • LinkedList底层实现了双向链表和双端队列特点
  • 可以添加任意元素(元素可以重复),包括null
  • 线程不安全,没有实现同步

说明:

  1. LinkedList底层维护了一个双向链表.
  2. LinkedList中维护了两个属性first和last ,分别指向首节点和尾节点
  3. 每个节点(Node对象),里面又维护了prey, next, item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表.
  4. 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高

image-20220211163239353

底层操作机制

CRUD

添加元素

image-20220110171056019.png

即处理链表尾结点:

声明初始化新的结点=>将其定义为尾结点=>若老尾结点为空(即链表为空)则将其定义为头结点=>若老尾结点不为空则挂在老的next上

删除元素

linkedList.remove();//默认删除第一个结点

image-20220110172602217.png

处理链表头结点:

提取待删结点item=>清空待删结点的item和next(垃圾处理)=>下一个结点变为头结点=>若下一个结点为空,则头尾皆空=>否则清空它的pre=>返回删除结点的item

ArrayList 和 LinkedList 的比较

image-20220211163437643
如何选择ArrayList和LinkedList:

  1. 如果我们改查的操作多,选择ArrayList
  2. 如果我们增删的操作多,选择LinkedList
  3. 一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
  4. 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择

Set 接口

介绍:

  • 无序(添加和取出的顺序不一致),没有索引
  • 不允许重复元素,所以最多包含一个null
  • JDK API中Set接口的实现类有:

image-20220211163534610

注意:取出的顺序虽然不是添加的顺序,但是他是固定的

常用方法

常用方法和 Collection 接口一样.

两种遍历方式

同Collection的遍历方式一样,因为Set接口是Collection接口的子接口。

  1. 可以使用迭代器
  2. 增强for
  3. 不能使用索引的方式来获取.

HashSet(重点)

扩容机制源码分析

HashSet无参构造器(也可指定初始容量参数)=>调用HashMap构造器(数组table默认为null)=>加载因子=默认值0.75初始化
image-20220110175321501.png

add方法源码:
image-20220110192928958
关键语句:
hashSet.add=>map.put=>putval(该方法包含元素添加语句:newNode那两句)

说明:
其中putval方法形参调用hash方法,hash方法体调用hashCode方法
而putval方法体中关键方法为==resize()==方法即扩容方法:具体表现为:
当table为空或size大小大于阈值threshold时,调用resize方法扩容table

两种情况区别:

  • 数组为空时,newCap = DEFAULT_INITIAL_CAPACITY;

    //默认初始容量DEFAULT_INITIAL_CAPACITY = 1 << 4;

  • 数组长度大于0时,newCap = oldCap << 1;

image-20220110194231761

扩容总结(前提:索引对应的链表没有进化成红黑树):

正常情况(key的哈希值不同):元素添加到不同索引,table从null开始,第一次扩容到16,当size(元素个数)超过阈值即(0.75*length),直接2倍扩容;

同哈希值情况:元素添加到同一链表上,当该链表结点超过8个开始调用treeifyBin方法每次添加元素到该链表都2倍扩容,当数组容量扩到64以上直接树化;

图文说明:
image-20220211163748436

注意:为达到添加对象同哈希值的目的,案例中的标准类的可方法重写equals和hashCode

LinkedHashSet

介绍:

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

image-20220211201518465.png

底层机制

构造器:

Set set=new LinkedHashSet();

构造器底层走的是形参(initialCapacity)为16的HashSet构造器,再走LinkdedHashMap的构造器

添加元素机制

原则和hashSet一样

但是在走到HashMap层的putval方法中,添加结点后将结点连接成双向链表的语句在末尾的afterNodeInsertion(evict);

总结:

  1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致

  2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)

  3. LinkedHashSet 底层结构 (数组table+双向链表)

  4. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry

  5. 数组是 HashMap$Node[] 存放的元素/数据

    是 LinkedHashMap$Entry类型

  6. 继承关系是在内部类完成.[源码如下]

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);
    }
}

Map

Map 接口实现类的特点

注意:这里以JDK8为例

介绍:

  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
  7. key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
  8. Map存放数据的key-value示意图,一对k-v是放在一个HashMap$Node中的,又因为Node实现了Entry接口,有些书上也说一对k-v就是一个Entry[如图]
    image-20220211202234851.png

Map 接口常用方法

  1. put:添加或替换(key相同)
  2. remove:根据键删除映射关系
  3. get:根据键获取值
  4. size:获取元素个数
  5. isEmpty:判断个数是否为 0
  6. clear:清除 k-v
  7. containsKey:查找键是否存在

Map 接口遍历方法

image-20220110204746663
4种Map遍历方法如下:

  • containsKey:查找键是否存在
  • keySet:获取所有的键
  • entrySet:获取所有关系
  • values:获取所有的值

总结:本质上三种方法得到的集合都是实现了Set接口的集合(由于value可以重复所以其实现的是Collection接口不是Set接口),其中entrySet中的元素,更是使用了多态的方式创建的,只是为了让其使用父接口的两个方法getkey,getvalue

位置说明:

  1. Set set = map.entrySet();//HashMap$EntrySet
  2. Set set1 = map.keySet();//HashMap内部类
  3. Collection values = map.values();//HashMap内部类

具体表现为:

  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,这是因为 static class Node<K,V> implements Map.Entry<K,V>
  4. 当把 HashMap$Node 对象 存放到 entrySet 就方便我们的遍历, 因为 Map.Entry 提供了重要方法K getKey(); V getValue();

具体遍历方法:三种内部类(接口)集合的遍历分别迭代器和增强for:3*2=6种

HashMap

总结:

  1. Map接口的常用实现类:HashMap, Hashtable和Properties
  2. HashMap是Map接口使用频率最高的实现类.
  3. HashMap是以key-val对的方式来存储数据(HashMap$Node类型)
  4. key不能重复,但是值可以重复,允许使用null键和null值。
  5. 如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(jdk8的hashMap底层数组+链表+红黑树)
  7. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized

底层机制及源码剖析

image-20220110210500575
扩容机制:[和HashSet相同]

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

添加机制(源码)

添加元素(key不同)的原理同hashSet原理一样,key相同则替换value,

具体操作由HashMap层的putVal方法中的末尾语句实现:

if (e != null) { // existing mapping for key
	V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
}

扩容机制

初始容量为0(无参构造器),第一次由resize方法扩容到16,再由临界值2倍扩容或树化

Hashtable

image-20220110211920480.png

分析源码:

  • Hashtable的put方法中健壮性判断,value为空直接抛空指针异常,而在调用key的方法时,hashCode方法没有重写,用的是Object类的hashCode方法,会抛空指针异常,前面HashMap的hashCode方法重写过:三元运算,key为null返回0
  • 调用put方法=>Hashtable的put方法(计算hash=hashCode)(key相同则替换value)=>put中调用addEntry方法(添加元素)=>rehash(是否扩容):int newCapacity = (oldCapacity << 1) + 1;

替换语句:entry.value = value;

添加语句:tab[index] = new Entry<>(hash, key, value, e);

扩容机制

构造器:

  • 无参(this10)=>构造Entry<>[initialCapacity]的数组

无参,默认赋值10;有参赋值0,默认改为1;

Hashtable 和 HashMap 对比

image-20220110213119633.png

Properties

  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
  2. 他的使用特点和Hashtable类似
  3. Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象并进行读取和修改
  4. 说明:工作后~.properties文件通常作为配置文件,这个知识点在io流举例,

*读写Properties配置文件

https://blog.youkuaiyun.com/jason_bone_/article/details/122871488

TreeSet&TreeMap

TreeSet

image-20220110233050720

底层维护的是类型为TreeMap的静态内部类(Entry<K,V>)对象的链表

无参构造器:自然排序

有参构造器(比较器:匿名内部类):定制排序

image-20220110230244258
说明:TreeSet的有参构造器将比较器对象,赋给了 TreeSet的底层的 TreeMap的属性this.comparator

添加元素:

treeSet.add()=>TreeMap.put()=>内部调用比较器的重写的compare方法给key找位置然后加入到链表:比较结果为负则新结点放左边,结果为0即相同则不添加

image-20220110233309262
重写案例:

TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //下面 调用String的 compareTo方法进行字符串大小比较
                //如果老韩要求加入的元素,按照长度大小排序
                //return ((String) o2).compareTo((String) o1);
                return ((String) o1).length() - ((String) o2).length();
            }
        });

TreeMap

和treeSet原理相同,TreeSet中默认值value=0,比较key,key相同则替换value

关键句:return t.setValue(value);

总结

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

  1. 先判断存储的类型(一组对象[单列]或一组健值对[双列])
  2. 一组对象[单列]:Collection接口
    • 允许重复:List
      • 增删多:LinkedList [底层维护了一个双向链表]
      • 改查多:ArrayList [底层维护Object类型的可变数组]
    • 不允许重复:Set
      • 无序:HashSet [底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)]
      • 排序:TreeSet
      • 插入和取出顺序一致:LinkedHashSet,维护数组+双向链表
  3. 一组键值对[双列]:Map
    • 键无序: HashMap [底层是:哈希表jdk7:数组十链表,jdk8:数组+链表十红黑数]
    • 键排序:TreeMap
    • 键插入和取出顺序一致: LinkedHashMap
    • 读取文件 Properties

补充

Collections工具类

说明:

  • Collections是一个操作Set,List和Map等集合的工具类
  • Collections 中提供了一系列静态方法对集合元素进行排序,查询和修改等操作

API方法:

  1. reverse(List):反转List中元素的顺序
  2. shuffle(List):对List集合元素进行随机排序
  3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
  4. sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
  5. swap(List, int, int):将指定list集合中的i处元素和j处元素进行交换
  6. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  7. Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  8. Object min(Collection)
  9. Object min(Collection,Comparator)
  10. int frequency(Collection,Object)
  11. void copy(List dest,List src):将src中的内容复制到dest中
ArrayList dest = new ArrayList();
//为了完成一个完整拷贝,我们需要先给dest 赋值,大小和list.size()一样
for (int i = 0; i < list.size(); i++) {
    dest.add(null);//""
}
//拷贝
Collections.copy(dest,list);
System.out.println("拷贝出dest="+dest);
  1. boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

Arrays工具类

  1. 通过字符串的形式打印数组public static String toString(int[] a)

  2. 通过原来的数组复制指定长度新数组public static int[] copyOf(int[] original,int newLength)

  3. 针对数组进行排序public static void sort(int[] a)

    自然顺序比较器Comparable< T >

    定制顺序比较器Comparator< T >

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值