Java数据结构浅析

 

程序 = 数据结构 + 算法

本文概述Java中常用的数据结构,并简述其使用场景

1. 数据结构的定义

数据结构是一种逻辑意义,指的是逻辑上的数据组织方式及相应的处理,与数据在磁盘的具体存储方式不完全相关。磁盘存储数据的方式可能是顺序存储也可能是链式存储。

逻辑上的数据组织方式有:队列、树、图、哈希等。

数据的处理:增删改查、遍历  。

2. 数据结构的分类

以数据是否存在前继和后继对数据结构做出如下分类 :

线性结构:0至1个直接前继和直接后继。如:顺序表(数组)、链表、栈、队列。

树结构:0至1个直接前继和0至n(n大于等于2)个直接后继。

图结构:0至n(n大于等于2)个直接前继和直接后继。

哈希结构:没有直接前继和直接后继,通过特定的哈希函数将索引与存储的值关联起来。

3.Java中的集合

List集合:存在明确的上一个和下一个元素,也存在明确的第一个元素和最后一个元素

  ArrayList:非线程安全,内部使用数组进行存储;扩容时需要创建新的数组,并复制数据。访问数据较快,插入和删除较慢;

  LinkedList:本质是双向链表,添加和删除速度较快,随机访问较慢。

Queue(队列)集合:队列是一种先进先出的数据结构,也是线性表的一种,只允许在表的一端获取数据,在另一端插入数据

  自BlockingQueue(阻塞队列)发布后,队列多在高并发场景作为Buffer(数据缓冲区)使用

Map集合:Key-Value键值对作为存储元素实现的哈希结构,Key按哈希函数计算后保证唯一,Value则是可重复的

  HashMap不是线程安全的,ConcurrentHashMap是线程安全的,TreeMap是Key有序的Map集合类

Set集合:不允许出现重复元素的集合类型

  HashSet是使用HashMap来实现的,TreeSet是使用TreeMap实现的;LinkedHashSet继承自HashSet,具有HashSet的优点,内部使用链表维护元素的插入顺序。

 * 集合初始化

集合初始化时指定集合初始值的大小可以减少扩容成本,有助于提高性能。

  ArrayList默认初始值为10,每次扩容的大小大约为之前大小的1.5倍,oldCapacitiy + (oldCapacitiy >> 1) 

  HashMap默认初始值为16,扩容时需重建hash表,非常影响性能。HashMap中有两个重要参数:Capacity和Load Factor(值默认为0.75),

HashMap基于这两个数的乘积表示能放入集合的元素个数;HashMap的容量是在第一次put时才创建,并不是在new时分配。

4.Java中的数组

  数组是一种顺序表,下标从0开始。数组是固定容量大小的,数组内的值可以修改,但数组大小不能改变。

  * 数组转集合

  Arrays.asList返回的是Arrays的内部类ArrayList,此内部类仍指向原数组,并不是java.util.ArrayList集合,在进行修改操作时会抛异常。

  正确的转换方式:  

List<Object> objectList = new java.util.ArrayList<Object>(ArrayList.asList(数组));

  集合转数组时注意数组类型与集合数据类型保持一致,数组的大小为集合大小list.size()。

5. 集合与泛型

  引入泛型是为了保证类型安全,集合间相互赋值时是传递引用,直接操作赋值后的集合会影响原集合。

  List  :无任何类型限制和赋值限定

  List<Object>:用法等同于List,但是在接受其他泛型赋值时会编译报错;List<Integer>不能赋值给List<Object>

  List<?>  :通配符集合,可以接受任何类型的集合引用赋值,但可以remove和clear;常作为形参或返回值类型    

	public void collectionTest() {
		List<Integer> listInteger = new ArrayList<Integer>(10);
		listInteger.add(1);
		
		List list = listInteger;
		list.add("hello");

		for(Object obj : listInteger ) {
                        //打印结果为: 1 hello
                        //原集合的值被修改了,不使用泛型可能造成隐藏Bug
			System.out.println(obj);
		}
		
		List<?> a = listInteger;
                //add编译报错
		a.add(new Integer(2));
	}

  List<? extends T> :类型为T或T的子类;Get First,适用于消费集合元素为主的场景

  List<? super T>  :类型为T或T的父类;Put First,适用于生产集合元素为主的场景

6. 元素的比较

  6.1 Comparable和Comparator

    约定俗成,比较器的返回结果:小于的情况返回-1;等于的情况返回0;大于的情况返回1。

    Comparable接口:与自己比,比较方法是compareTo

    Comparator接口:平台性质的比较器,比较方法是compare

    Arrays.sort中使用了TimSort算法,该算法是归并排序和插入排序优化后的算法,时间复杂度最优可达O(n),平均时间复杂度为O(nlogn)

  6.2 hashCode和equals

    hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。

    若只复写equals方法,调用equals比较对象时,实质比较的是equals方法中的属性是否相同,并不判断对象本质是否相等。

    若同时复写equals方法和hashCode,则调用equals时相当于使用"=="

    尽量避免通过实例对象引用来调用equals方法,否则容易抛出空指针异常,建议使用Objects中的equals方法,源码如下:    

public static boolean equals(Object a, Object b) {
  return (a==b) || (a != null && a.equals(b));  
}

7. fail-fast与fail-safe机制

  fail-fast机制是集合中常见的错误检测机制,即检查集合的元素在遍历过程中的一致性,若遍历时集合产生了变化则产生fail-fast。java.util包下的所有集合类都是fail-fast的,ArrayList.subList()方法产生的子列表与主列表间会相互影响,如产生子列表后再删除主列表元素会导致子列表的操作异常。

  在遍历删除集合元素时,使用Iterator机制;多线程并发的场景应加锁,源码如下:    

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
     synchronized(对象) {
        String item = iterator.next();
        if (删除元素的条件) {
           iterator.remove();
        }
    }
}        

  fail-safe机制可简单理解副本快照的模式,遍历时对副本进行操作,修改操作在原始数据上进行,修改完成后同步到副本中。concurrent包中的集合都是fail-safe的。

  COW(Copy-On-Write):读写分离的设计思想,修改数据时复制一个新的集合,在新集合上进行修改,完成后将旧集合的引用指向新集合。仅适用于读多写极少的场景,若频繁进行写操作会导致效率急速下降;另一缺点为数据的实时性无法保证。  

/**
*  此代码执行时间为几十s,使用ArrayList执行时间为ms级别
*/
public static void main(String[] args){
    List<Long> copy = new CopyOnWriteArrayList<Long>();
    
    long start = System.nanoTime();
    for(int i=0; i < 20 * 10000; i++ ) {
        copy.add(System.nanoTime());
    }  
}

8. Map集合

    ConcurrentHashMap在性能上与HashMap区别不大,但是Key-Value均不能为空,两者互换使用时需注意空指针(NPE)问题。

  

     8.1 树

          节点的深度指当前节点到根节点的距离,节点的高度指当前节点到最远子叶子节点的距离。

     二叉树

        每个节点至多有两个子节点的树称为二叉树。

     平衡二叉树

        任何节点的左右子树高度差不超过1,空树或只有根节点的树也是平衡二叉树。

     二叉查找树(Binary Search Tree)

        二叉查找树的所有节点满足该节点左子树的值均小于该节点,右子树的值均大于该节点。

        遍历方式:前序、中序、后序

           (1)在任何递归子树中,左节点一定在右节点之前遍历;

      (2)前中后序仅指父节点在遍历时的位置顺序。

   二叉查找树在数据改变时容易失衡,导致操作效率变低;为了保证二叉树的平衡性,有如下算法实现,如AVL树,红黑树,SBT等等,现在常用的是红黑树。

    AVL树通过对不平衡的树进行左旋或者右旋以达到平衡,属于平衡二叉树,在删除数据后可能发生大量旋转导致时间成本增加。

              红黑树也是通过左旋或右旋以实现平衡,非绝对平衡。红黑树的特点(有红必有黑,红红不相连):

      (1)节点非黑即红

      (2)根节点必须为黑

      (3)所有NIL节点都是黑色的(Nothing In Leaf,叶子节点上虚构的子节点)

      (4)红色节点不相邻

      (5)任何递归子树内,根节点到叶子节点的所有路径上包含相同数目的黑色节点

    8.2 TreeMap

  TreeMap的Key是有序不重复的,支持获取头尾元素。TreeMap依靠Comparable或Comparator来实现Key的去重;HashMap使用hashCode和equals方法去重。

   8.3 HashMap

  HashMap的两个主要问题:死链问题和扩容数据丢失。(多线程高并发场景)

  哈希类集合的三个基本概念:

    

  HashMap高并发场景新增对象丢失的原因:

    * 并发赋值时被覆盖

    * 已遍历区间新增元素会丢失

    * “新表”被覆盖

    * 迁移丢失。在迁移过程中,有并发时,next被提前置成null

  HashMap死链形成原因(数据在桶内以链表的形式存储):

    死链形成的原因是链表中指向下一个元素的next值被并发修改,导致形成对象间互链或对象自己互链。

  8.4 ConcurrentHashMap

    JDK对该类进行的优化:

      (1)取消分段锁机制,进一步降低冲突概率。

      (2)引入红黑树结构。同一个哈希槽上的元素超过一定阈值后,单向链表转化为红黑树结构。元素数量减少到一定的个数时,红黑树会退化成单向链表。

      (3)使用了更加优化的方式统计集合内元素数量。使用CAS机制。

    CAS(Compare And Swap):JDK提供的非阻塞原子性操作,通过硬件保证了比较-更新操作的原子性。解决轻微冲突的多线程并发场景下使用锁造成性能损耗的一种机制。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 JDK中的Unsafe类提供了一系列的compareAndSwap方法。

posted @ 2019-07-03 10:34  北辰Root  阅读(429)  评论(0编辑  收藏  举报