java_虚拟机java基础

Java集合类

2017-10-16  本文已影响47人  今有所思

1.Java集合框架

集合类主要分为两大类:CollectionMap

(1)Collection是List、Set等集合高度抽象出来的接口,它包含了这些集合的基本操作,它主要又分为两大部分:ListSet

1.List接口通常表示一个列表(数组、队列、链表、栈等),其中的元素可以重复,常用实现类为ArrayListLinkedList,另外还有不常用的Vector。另外,LinkedList还是实现了Queue接口,因此也可以作为队列使用。

2.Set接口通常表示一个集合,其中的元素不允许重复(通过hashcode和equals函数保证),常用实现类有HashSetTreeSet,HashSet是通过Map中的HashMap实现的,而TreeSet是通过Map中的TreeMap实现的。另外,TreeSet还实现了SortedSet接口,因此是有序的集合(集合中的元素要实现Comparable接口,并覆写Comparator函数才行)。LinkedHashSet维护的是保持了插入顺序的链接列表。

我们看到,抽象类AbstractCollection、AbstractList和AbstractSet分别实现了Collection、List和Set接口,这就是在Java集合框架中用的很多的适配器设计模式,用这些抽象类去实现接口,在抽象类中实现接口中的若干或全部方法,这样下面的一些类只需直接继承该抽象类,并实现自己需要的方法即可,而不用实现接口中的全部抽象方法。

(2)Map是一个映射接口,其中的每个元素都是一个key-value键值对,同样抽象类AbstractMap通过适配器模式实现了Map接口中的大部分函数,TreeMapHashMapWeakHashMap等实现类都通过继承AbstractMap来实现,另外,不常用的HashTable直接实现了Map接口,它和Vector都是JDK1.0就引入的集合类。

(3)Iterator是遍历集合的迭代器(不能遍历Map,只用来遍历Collection),Collection的实现类都实现了iterator()函数,它返回一个Iterator对象,用来遍历集合,ListIterator则专门用来遍历List。而Enumeration则是JDK1.0时引入的,作用与Iterator相同,但它的功能比Iterator要少,它只能再Hashtable、Vector和Stack中使用。

(4)ArraysCollections是用来操作数组、集合的两个工具类,例如在ArrayList和Vector中大量调用了Arrays.Copyof()方法,而Collections中有很多静态方法可以返回各集合类的synchronized版本,即线程安全的版本,当然了,如果要用线程安全的结合类,首选Concurrent并发包下的对应的集合类。

2.与Java集合框架相关的有哪些最好的实践?

(1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用数组而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用LinkedHashMap。如果我们不想重复,我们应该使用Set。

(2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。

(3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。

(4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。

(5)使用JDK提供的不可变类作为Mapkey,可以避免自己实现hashCode()和equals()。

(6)尽可能使用Arrays和Collections工具类,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。

3.ArrayList和LinkedList的底层实现原理,使用场景

LinkedeList和ArrayList都实现了List接口,但是它们的工作原理却不一样。它们之间最主要的区别在于ArrayList是可改变大小的数组,而LinkedList是双向链表。

LinkedList和ArrayList的差别主要来自于ArrayList和LinkedList数据结构的不同。

1)因为ArrayList是基于索引的数据结构,它使用索引在数组中搜索和读取数据是很快的。ArrayList获取数据的时间复杂度是O(1),但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据。

2)相对于ArrayList,LinkedList插入是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。

3)类似于插入数据,删除数据时,LinkedList也优于ArrayList

4) LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。

4.ArrayList与Vector的区别

这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的。ArrayList与Vector的区别,主要包括两个方面:

(1)同步性

Vector是线程安全的,而ArrayList是线程序不安全的

(2)数据增长

Vector默认增长为原来2倍,而ArrayList的是增长为原来的1.5。ArrayList与Vector都可以设置初始的空间大小,Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。

5.HashMap详解

HashMap要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。

Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。

size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数量threshold的区别。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。

因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

(1)确定哈希桶数组索引位置

Step1:取hashcode值;Step2:高位参与运算;Step3:取模运算

(2)put方法


①判断键值对数组table是否为空或为null,则执行resize()进行扩容;

②根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

(3)扩容机制

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

1 void resize(int newCapacity) {//传入新的容量

2Entry[] oldTable = table;//引用扩容前的Entry数组

3int oldCapacity = oldTable.length;

4if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的数组大小如果已经达到最大(2^30)了

5threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了

6return;

7}

9Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组

10transfer(newTable);//!!将数据转移到新的Entry数组里

11table = newTable;//HashMap的table属性引用新的Entry数组

12threshold = (int)(newCapacity * loadFactor);//修改阈值

13}

这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

1 void transfer(Entry[] newTable) {

2Entry[] src = table;//src引用了旧的Entry数组

3int newCapacity = newTable.length;

4for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组

5Entry e = src[j];//取得旧Entry数组的每个元素

6if (e != null) {

7src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)

8do {

9Entry next = e.next;

10int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置

11e.next = newTable[i]; //标记[1]

12newTable[i] = e;//将元素放在数组上

13e = next;//访问下一个Entry链上的元素

14} while (e != null);

15}

16}

17}

6.Hashtable和HashMap的区别

二者的存储结构和解决冲突的方法都是相同的。

(1)HashMap允许key和value为null,而HashTable不允许。

(2)Hashtable是同步的,而HashMap不是。所以HashMap适合单线程环境,Hashtable适合多线程环境。

(3)Hashtable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

为什么HashTable的默认大小和HashMap不一样

Hashtable比较古老,直接使用了除留余数法,那么就需要设置容量起码不是偶数(除(近似)质数求余的分散效果好),而JDK开发者选了11。

HashMap中的length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

(4)哈希值的使用不同,Hashtable直接使用对象的hashCode。

Hashtable:

inthash = key.hashCode();

intindex= (hash &0x7FFFFFFF) % tab.length;

(5)Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

Hashtable:

intnewCapacity = (oldCapacity <<1) +1;

(6)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是Hashtable不是这样的,它的顺序是不可预知的。

(7)Hashtable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用CocurrentHashMap。

7.HashSet、TreeMap、TreeSet、LinkedHashMap的实现原理

HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个PRESENT。

private static final Object PRESENT = new Object();

HashSet中add方法调用的是底层HashMap中的put()方法,而如果是在HashMap中调用put,首先会判断key是否存在,如果key存在则修改value值,如果key不存在则插入这个key-value。而在set中,因为value值没有用,也就不存在修改value值的说法,这样HashSet中就不存在重复值。

TreeMap的实现使用了红黑树数据结构,也就是一棵自平衡的排序二叉树,这样就可以保证快速检索指定节点。对于TreeMap而言,它采用一种被称为“红黑树”的排序二叉树来保存Map中每个Entry ——每个Entry都被当成“红黑树”的一个节点对待。

TreeSet类似。

LinkedHashMap继承HashMap,此实现与HashMap的不同之处在于,LinkedHashMap维护着一个双向循环链表。此链表定义了迭代顺序,该迭代顺序通常就是将存放元素的顺序。

8.数组中Arrays.sort的排序方法是什么

基本类型:采用调优的快速排序;

对象类型:采用改进的归并排序。

Colletions.sort(list)Arrays.sort(T[])

Colletions.sort()实际会将list转为数组,然后调用Arrays.sort(),排完了再转回List。

JDK8里,List有自己的sort()方法了,像ArrayList就直接用自己内部的数组来排,而Linked List, CopyOnWriteArrayList还是要复制出一份数组。

上一篇下一篇

猜你喜欢

热点阅读