Java基础之数据结构和集合源码

本文介绍了数据结构的基本概念和类型,包括集合、线性结构、树形结构和图形结构。详细讲解了数组、链表(单向链表和双向链表)、二叉树的实现,并探讨了栈和队列的使用。此外,文章还深入分析了ArrayList和LinkedList在Java中的实现,讨论了它们的扩容机制和适用场景。最后,文章剖析了HashMap的内部工作原理,包括哈希冲突处理和扩容策略,并对比了JDK7和JDK8的差异。
摘要由CSDN通过智能技术生成

1. 数据结构

1.1 数据结构概念:(了解)
  • 数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构物理结构以及他们之间的关系
  • 并对这种结构定义相应的运算,目的是加快程序的执行速度、减少内存占用的空间
1.2 数据结构的研究对象
1.2.1 数据之间的逻辑关系
  • 集合结构
  • 线性结构:一对一关系
  • 树形结构:一对多关系
  • 图形结构:多对多关系
1.2.2 数据存储结构(物理结构)
  • 顺序结构

  • 链式结构

  • 索引结构

  • 散列结构

  • 开发中,我们更习惯下面的方式理解结构

    • 线性表(一对一关系):一维数组、单向链表、双向链表、栈、队列
    • 树(一对多关系):各种树。如:二叉树,B+树
    • 图(多对多关系)
    • 哈希表:如HashMap、HashSet
1.2.3 相关的算法操作
  • 分配资源,建立结构,释放结构
  • 插入和删除
  • 获取和遍历
  • 修改和排序
1.3 常见存储结构之:数组

略。

1.4 常见存储结构之:链表

链表中的基本单位是:节点(Node)

1.4.1 单向链表
class Node{  
    Object data;  
    Node next;  
  
    public Node(Object data){  
        this.data = data;  
    }  
}  
  
//创建对象:  
Node node1 = new Node("AA");  
Node node2 = new Node("BB");  
node1.next = node2;  
1.4.2 双向链表
class Node{  
    Node prev;  
    Object data;  
    Node next;  
  
    public Node(Object data){  
        this.data = data;  
    }  
  
    public Node(Node prev,Object data,Node next){  
        this.prev = prev;  
        this.data = data;  
        this.next = next;  
    }  
}  
  
//创建对象  
Node node1 = new Node(Null,"AA",null);  
Node node2 = new Node(node1,"BB",null);  
Node node3 = new Node(node2,"CC",null);  
  
node1.next = node2;  
mode2.next = node3;  
1.5 常见存储结构之:二叉树
class TreeNode{  
    TreeNode left;  
    Object data;  
    TreeNode right;  
  
    public TreeNode(Object data){  
        this.data = data  
    }  
    public TreeNode(TreeNode left,Object date ,TreeNode right){  
        this.left = left;  
        this.data = date;  
        this.right = right;  
    }  
}  
  
//创建对象  
TreeNode node1 = new TreeNode(null,"AA",null);  
TreeNode leftNode = new TreeNode(null,"BB",null);  
TreeNode rightNode = new TreeNode(null,"CC",null);  
  
node1.left = leftNode;  
node1.right = rightNode;  

或者

class TreeNode{  
    TreeNode parent;  
    TreeNode left;  
    Object data;  
    TreeNode right;  
  
    public TreeNode(Object data){  
        this.data = data;  
    }  
  
    public TreeNode(Tree left,Object data,TreeNode right){  
        this.left = left;  
        this.data = data;  
        this.right = right;  
    }  
  
    public TreeNode(TreeNode parent,Tree left,Object data,TreeNode right){  
            this.parent = parent;  
            this.left = left;  
            this.data = data;  
            this.right = right;  
        }  
}  
  
//创建对象  
TreeNode node1 = new TreeNode(null,null,"AA",null);  
TreeNode leftNode = new TreeNode(node1,null,"BB",null);  
TreeNode rightNode = new TreeNode(node1,null,"CC",null);  
  
node1.left = leftNode;  
node1.right = rightNodel  
1.6 常见存储结构之:栈
  • stack,先进后出,后进先出
  • first in last out/FILO —> LIFO
  • 属于抽象数据类型(ADT) Abstract Data Type
  • 可以使用数组或链表来构建

数组实现栈

class Stack{  
    Object[] values;  
    int size; //记录储存的元素的个数  
  
    public Stack(int length){  
        values = new Object[length];  
    }  
  
    //入栈  
    public void push(Object ele){  
        if(size>=length){  
            throw new RuntimeException("栈空间已满,入栈失败");  
        }  
        value[size] = ele;  
        size++;  
    }  
  
    //出栈  
    public Object pop(){  
        if(size <=0 ){  
            throw new RuntimeException("栈空间已空,出栈失败");  
        }  
        Object obj = value[size - 1];  
        values[size - 1] = null;  
        size--;  
        return obj;  
    }  
}  
1.7 常见存储结构之:队列
  • queue,先进先出,first in first out/FIFO

  • 属于抽象数据类型(ADT)

  • 可以使用数组或链表来构建

  • 数组实现队列

class Queue{  
    Object[] values;  
    int size;//确定储存元素的个数  
  
    public Queue(int length){  
        values = new Object[length];  
    }  
  
    //入列  
    public void add(Object ele){  
        if(size>=length){  
            throw new RuntimeException("队列已满,无法添加");  
        }  
  
        values[size] = ele;  
        size ++;  
    }  
  
    //出列  
    public Object get(){  
        if(size <= 0){  
            throw new RuntimeException("队列已空,无法获取");  
        }  
  
        Object obj = values[0];  
  
        //数据前移  
        for(i = 0;i<size-1;i++){  
            values[i] = values[i+1];  
        }  
  
        //最后一个元素置空  
        values[size - 1] = null;  
        size--;  
        return obj;  
    }  
}  
  

2. List接口下的实现类的源码剖析

2.1 ArrayList
2.1.1 ArrayList的特点:
  • 实现了List接口,存储有序的、可重复的数据
  • 底层使用了Object[] 数组存储
  • 线程不安全的
2.1.2 ArrayList源码解析:

jdk7版本:(以jdk1.7.0_07为例)

如下代码的执行,底层会初始化数组,数组的长度为10。

Object[] elementData = new Object[10]  
ArrayList<String> list = new ArrayList<>();  
  
list.add("AA"); // elementData[0] = "AA";  
list.add("BB"); // elementData[1] = "BB"; 
...

当要添加第11个元素的时候,底层的elementDate数组已满,则需要扩容。默认为原来长度的1.5倍。
并将原有数组的元素赋值到新的数组中

jdk8版本:(以jdk1.8.0_271为例)

如下代码的执行,底层会初始化数组。

Object[] elementData = new Object[]{};  
ArrayList<String> list = new ArrayList<>();  
  
list.add("AA"); // elementData[0] = "AA"; 首次添加元素数,会初始化数组elementData = new Object[10];  
list.add("BB"); // elementData[1] = "BB";  
...  

当要添加第11个元素的时候,底层的elementDate数组已满,则需要扩容。默认为原来长度的1.5倍。
并将原有数组的元素赋值到新的数组中

小结:
jdk1.7.0_07版本中:ArrayList类似于饿汉式
jdk1.8.0_271版本中:ArrayList类似于懒汉式

2.2 Vector
2.2.1 Vector的特点:
  • 实现了List接口,存储有序的、可重复的数据
  • 底层使用了Object[] 数组存储
  • 线程安全的
2.2.2 Vector源码解析:(以jdk1.8.0_271为例) 了解
Vector v = new Vector(); //底层初始化数组,长度为10 Object[] elementData = new Object[10]  
v.add("AA"); //elementData[0] = "AA";  
v.add("BB"); //elementData[1] = "BB";  
...  

当要添加第11个元素的时候,底层的elementDate数组已满,则需要扩容。默认为原来长度的2倍。

2.3 LinkedList
2.3.1 LinkedList的特点:
  • 实现了List接口,存储有序的、可重复的数据
  • 底层使用了双向链表存储
  • 线程安全的
2.3.2 LinkedList在jdk8中的源码解析:
LinkedList<String> list = new LinkedList(); //底层没做啥  
list.add("AA"); //将“AA”封装到一个Node对象1中,list对象的属性first,last都指向此Node对象1  
list.add("BB"); //将“BB”封装到一个Node对象2中,对象1和对象2构成一个双向链表,同时last指向对象2  
  ...

因为LinkedList使用的是双向链表,不需要考虑扩容问题

内部Node的声明

private static class Node<E> {  
	E item;        
	Node<E> next;        
	Node<E> prev;
}  
2.4 LinkedList是否存在扩容问题?
  • 不存在
2.5 启示与开发建议
  1. Vector基本不使用了

  2. ArrayList底层使用数组结构,查找和添加(尾部添加)操作效率高,时间复杂度为O(1)
    删除和插入操作效率低,时间复杂度为O(n)

  3. LinkedList底层使用双向链表,删除和插入操作效率高,时间复杂度为O(1)
    查找和添加操作效率低,时间复杂度O(n),添加有可能是O(1)的

  4. 如果选择了ArrayList的前提下,new ArrayList();底层创建长度为10的数组
    new ArrayList(int capacity):底层创建指定capacity的数组

  • 如果开发中,大体确定数组的长度,推荐使用ArrayList(int capacity)这个构造器,避免底层多次扩充、复制数组

3. Map接口下的实现类的源码剖析 #java重点 #java掌握 #java熟练使用

3.1 HashMap
3.1.1 HashMap中元素的特点
  • 所有的key彼此之间是不可重复的、无序的。所以key就构成了一个Set集合。—> key所在的类要求重写hashCode()和equals()方法
  • 所有的value彼此之间是可重复、无序的。所以value就构成了一个Collection集合。 —> value所在的类要重写equals()方法
  • 一个key-value就构成了一个entry(node)
  • 所有的entry彼此之间是不可重复的、无序的。所以entry就构成了一个Set集合。
3.1.2 HashMap源码解析(极高极高的!!!)

jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):

//底层是数组+对象链表的结构  
//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16]; 2的四次幂  
HashMap<String,Integer> map = new HashMap<>();  
...  
//"AA"和78封装到一个Entry对象中,将此对象添加到table数组中  
map.put("AA",78)''  
...  

添加/修改的过程: #java重点

  • 将(key1,value1)添加到当前的map中

  • 首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。

  • 哈希值2再金果果某种算法(indexFor())之后,就确定了(key1,value1)其在数组table中的索引位置i

    • 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功 —>情况1
    • 如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2 —>哈希冲突
      • 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。 —>情况2
      • 如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals(),要调用key1所在类的equals(),将key2作为参数传递进去
        • 调用equals(),返回false:则(key1,value1)添加成功 —>情况3
        • 调用equals(),返回tru,则key1和key2是相同的。默认情况下value1替换原有value2
  • 说明:

  • 情况1,将(key1,value1)存放到数组的索引i的位置

  • 情况2,情况3:(key1,value1)元素与现有的(key2,value2)构成单向链表结构,(key1,value1)指向(key2,value2)

  • 随着不断的添加元素,在满足如下条件的情况下,则会进行扩容

(size >= threshold) && (null != table\[bucketIndex]) //bucketIndex=i,即超过12且出现链表了!!!
  • 当元素的个数达到临界值(->数组的长度 * 加载因子(16*0.75=12))时,就考虑扩容。默认临界值为12
  • 默认扩容为原来的2倍。

jdk8与jdk7的不同之处(以jdk1.8.0_271为例):

  • 创建HashMap的实例后,底层并没有初始化table数组,当首次添加(key,value)时,进行判断,如果发现table尚未初始化,则对数组进行初始化
  • HashMap,底层定义了Node内部类,替换了jdk7中的Entry内部类。意味着,我们创建的是Node[]
  • 如果当前的(key,value)经过一系列的判断后,可以添加到当前的数组角标i中,若角标i中有元素,jdk7中是新的指向旧的(头插法),jdk8中是旧的指向新的(尾插法)”七上八下“
  • 数组+单向链表 —> 数组+单向链表+红黑树。
  • 如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上的多个元素改为使用红黑树的结构进行储存
  • 进行put、get、remove操作的时间复杂度为O(logn),比单向链表的O(n)。性能更好
  • 什么时候使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树退化为单向链表
3.1.2 属性/字段
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16  
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量  1 << 30  
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认加载因子  
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化  
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表  
  
//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。  
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容  
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64  
  
transient Node<K,V>[] table; //数组  
transient int size;  //记录有效映射关系的对数,也是Entry对象的个数  
int threshold; //阈值,当size达到阈值时,考虑扩容  
final float loadFactor; //加载因子,影响扩容的频率  
3.2 LinkedHashMap
3.2.1 LinkedHashMap 与 HashMap 的关系:
  • LinkedHashMap 是 HashMap的子类
  • LinkedHashMap 在 HashMap使用的数组+单向链表+红黑树的基础上,又增加了一对双向链表,记录添加的(key,value)的先后顺序。便于我们遍历所有的node;

LinkedHashMap重写了HashMap的如下方法:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {  
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);  
    linkNodeLast(p);  
    return p;  
}
3.2.2 底层结构:

LinkedHashMap内部定义了一个Entry

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);  
    }  
}
3.3 HashSet和LinkedHashSet的源码分析
  • HashSet底层使用的是HashMap
  • LinkedHashSet底层使用的是LinkedHashMap
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值