java集合框架详解

java集合框架详解

tree排序hash不排序 set不可重复 list有序

  • 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应
    该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
  • 如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其
    效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
  • 要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
  • 尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将
    ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
    集合只能存储引用数据类型,基本数据类型会被自动封装为封装类

排序

实现Compare接口与Comparator接口的类,都是为了对象实例数组排序的方便,因为可以
直接调用
java.util.Arrays.sort(对象数组名称),可以自定义排序规则。
不同之处:
1 排序规则实现的方法不同
    Comparable接口的方法:compareTo(Object o)
    Comparator接口的方法:compare(T o1, To2)
2 类设计前后不同
Comparable接口用于在类的设计中使用;排序java.util.Arrays.sort(对象)
Comparator接口用于类设计已经完成,还想排序(Arrays);创建比较器实现Comparator接口实现compare(T o1, To2)方法;排序java.util.Arrays.sort(对象,比较器implements Comparator)

ArrayList和LinkedList有何区别?

ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。

  • ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访
    问,复杂度为O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节
    点相连接。所以,尽管有使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到
    索引的节点然后返回元素,时间复杂度为O(n),比ArrayList要慢。
  • 与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元
    素被插入到中间的时候,不会涉及改变数组的大小,或更新索引。
  • LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节
    点的引用
    *Vector会在你不需要进行线程安全的时候,强制给你加锁,导致了额外开销,所以慢慢被弃用了。 *
    ①Vector所有方法都是同步,有性能损失。
    ②Vector早期版本出现的。通常用于兼容旧库
    ③Vector初始length是10 超过length时 以100%比率增长,相比于ArrayList更多消耗内
    存。

Hashmap底层

哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结
点。那么这些元素是按照什么样的规则存储到数组中呢。

一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组,下标为12的位置。实际上是调用indexFor方法。

计算机中直接求余效率不如位运算,源码中做了优化hash&(length-1),在length是2的n次方的情况下hash%length==hash&(length-1);

因为当length是2的n次方时 如= 16时,length – 1 = 15 即1111不会浪费空间,1&0 0&0都是0,只有1&1的时候是1,如果存在0的话那么与操作这位永远为0。

如1000(8)1001(9)对 8-1(0111)位运算 是0000 0001 0和1 但是如果 是7-1(1110)的话最后一位永远是0 8和9运算后都是0000了,都是0

其实链表是为了解决hash冲突建立的 完美情况不发生冲突就是一个数组
// 存储时:
int hash = key.hashCode();
int index = hash % Entry[].length;
Entry[index] = value;
// 取值时:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];
put
当两个key通过hash%Entry[].length得到的index相同时,HashMap会这样做:B.next =
A,Entry[0] = B,也就是说数组中存储的是最后插入的元素。但是在1.8中是添加在链表尾部
get
先定位到数组元素,再遍历该元素处的链表。
null key总是存放在Entry[]数组的第一个元素。
扩容
HashMap提供了三个构造函数:

  • HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
  • HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空
    HashMap。
  • HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的
    空 HashMap。

扩容的两个条件map中元素数量大于阈值(初始容量*默认加载因子)且发生哈希冲突时数组长度扩容为原长度的2倍
两个极端情况

  • 就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。

  • 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。

如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太
小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75
resize方法扩容数组 在新数组中重新进行分配数据。
1.8新特性

当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题

JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的“数组(Entry)+链表”改为“数组(Node数组可能是链表或红黑树)+链表+红黑树”。JDK 1.8的HashMap当链表节点较少时仍然是以链表存在,当链表节点较多时(大于8)会调用treeifyBin()树形化方法转为红黑树,当节点少于6时会还原回链表。
树形化操作主要做了这些事;

  • 根据哈希表中元素个数确定是扩容还是树形化
  • 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
  • 然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
TREEIFY_THRESHOLDUNTREEIFY_THRESHOLDMIN_TREEIFY_CAPACITY
一个桶的树化阈值一个树的链表还原阈值哈希表的最小树形化容量
static final int TREEIFY_THRESHOLD = 8static final int UNTREEIFY_THRESHOLD = 6static final int MIN_TREEIFY_CAPACITY = 64
当桶中元素个数超过这个值时需要使用红黑树节点替换链表节点当扩容时,桶中元素个数小于这个值就会把树形的桶元素 还原(切分)为链表结构当哈希表中的容量大于这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化

1.8和1.7之间的主要区别
(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(2)扩容后数据存储位置的计算方式也不一样:

  1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&
  2. 而在JDK1.8的时候是是0的话索引没变,是1的话索引变成“原索引+oldCap”
    在这里插入图片描述

HashMap和HashTable有何不同?

  • HashMap允许key和value为null,而HashTable不允许。
  • HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境,
    HashTable适合多线程环境。
  • 在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺
    序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是
    不可预知的。
  • HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key
    的Enumeration进行遍历,它不支持fail-fast。
  • HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用
    CocurrentHashMap。

TreeMap和LinkedHashMap 有何不同?

  • TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
  • LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

LinkedHashMap 在日常开发商家竞争力时用过,可以在需要一个有序的map时使用。

concurrenthashmap

HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是
synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,相当于将所有的
操作串行化。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一
种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存
储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和
HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个
HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对
HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

初始化
传入的参数有initialCapacity,loadFactor,concurrencyLevel这三个。
initialCapacity表示新创建的这个ConcurrentHashMap的初始容量,也就是元素个
数。默认值为static final int DEFAULT_INITIAL_CAPACITY = 16;
loadFactor表示负载因子,。默认值为static final float DEFAULT_LOAD_FACTOR
= 0.75f;
concurrencyLevel表示并发级别,这个值用来确定Segment的个数,Segment的个
数是大于等于concurrencyLevel的第一个2的n次方的数。
get
ConcurrentHashMap可以做到读取数据不加锁
get方法里将要使用的共享变量都定义成volatile

第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。

接下来就是根据hash和key对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。对hash链进行遍历不需要加锁的原因在于链指针next是final的。但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在 table数组中的值。这使得getFirst(hash)可能返回过时的头结点,例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。
size
Segment里的全局变量count是一个volatile变量,每个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了
定位Segment
会进行二次hash然后定位到segment,segmentFor方法根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask 进行与操作

		//源码
		int sshift = 0;
        int ssize = 1;
        while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
            ++sshift;
            ssize <<= 1;
        }
        int segmentShift = 32 - sshift;
        int segmentMask = ssize - 1;

初始化segmentShift和segmentMask。这两个全局变量在定位segment时的哈希算法里需要使用,sshift等于ssize从1向左移位的次数,在默认情况下concurrencyLevel等于16,1需要向左移位移动4次,所以sshift等于4。segmentShift用于定位参与hash运算的位数,segmentShift等于32减sshift,所以等于28

在jdk1.8中主要改进
1.8的并发控制使用Synchronized和CAS来操作。例如对于put操作,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(链表表头或树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。

1.8之后Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本,新版本使用和HashMap一样的数据结构每个数组位置使用一个锁现在锁定的是一个Node头节点(注意,synchronized锁定的是头结点),减小了锁的粒度,性能和冲突都会减少。

1.8中concurrencyLevel只影响初始容量,后续的并发度大小依赖于table数组的大小。

将原先table数组+单向链表的数据结构,变更为Node数组+单向链表+红黑树的结构。
CAS算法简介
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。   
CAS 操作中包含三个操作数 —— 需要读写的内存值(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存值V与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

list扩容

int newCapacity = oldCapacity + (oldCapacity >> 1)+1;
">>"右移符号,所以是除以2,所以新的容量是就的1.5倍+1
Arrays.copyOf 调用 System.arraycopy 扩充数组
set的底层就是map实现的 key和value相同
用Iterator模式实现遍历集合
Collection有一个重要的方法:iterator(),返回一个Iterator(迭代器),用于遍历集合的所
有元素。Iterator模式可以把访问逻辑从不同的集合类中抽象出来,从而避免向客户端暴露集
合的内部结构。典型的用法如下:
Iterator it = collection.iterator();// 获得一个迭代器
while(it.hasNext()) {
Object obj = it.next();// 得到下一个元素
}
不需要维护遍历集合的“指针”,所有的内部状态都由Iterator来维护,而这个Iterator由集
合类通过工厂方法生成。
每一种集合类返回的Iterator具体类型可能不同,但它们都实现了Iterator接口,因此,我们
不需要关心到底是哪种Iterator,它只需要获得这个Iterator接口即可,这就是接口的好处,
面向对象的威力。
要确保遍历过程顺利完成,必须保证遍历过程中不更改集合的内容(Iterator的remove()方法
除外),所以,确保遍历可靠的原则是:只在一个线程中使用这个集合,或者在多线程中对
遍历代码进行同步。
由Collection接口派生的两个接口是List和Set。

集合工具Collections

Collections.sort(list);//list集合进行元素的自然顺序排序。
Collections.sort(list,new ComparatorByLen());//按指定的比较器方法排序。
Collections.max(list);//返回list中字典顺序最大的元素。
int index = Collections.binarySearch(list,“zz”);//二分查找,返回角标。
Collections.reverseOrder();//逆向反转排序。
Collections.shuffle(list);//随机对list中的元素进行位置的置换。、
将非同步集合转成同步集合的方法:Collections中的 XXX synchronizedXXX(XXX);
Collections.synchronizedList(list);
Collections.synchronizedMap(map);
原理:定义一个类,将集合所有的方法加同一把锁后返回。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值