java 集合
1:java集合框架中的接口:
Collection接口:存放的是元素;
Map接口:存放的是键值对;
继承关系:
Collection
├List(有序的,可以重复元素)
│├LinkedLis(get, remove, add 既可以在头部也可以在尾部,所以可以当做stack,queue, deque用; 没有同步方法)
│├ArrayList(不是同步的)
│└Vector(类似于ArrayList, 但是同步的)
│ └Stack
└Set
Map
├Hashtable(同步的)
├HashMap(非同步的)
└WeakHashMap(如果一个key不再被外部所引用,那么该key就可以被GC回收。)
Map通过initial capacity和load factor两个参数调整性能。
作为key的对象必须实现hashcode和equals方法。
List集合是有序集合,集合中的元素可以重复,访问集合中的元素可以根据元素的索引来访问。
Set集合是无序集合,集合中的元素不可以重复,访问集合中的元素只能根据元素本身来访问(也是不能集合里元素不允许重复的原因)。
Map集合中保存Key-value对形式的元素,访问时只能根据每项元素的key来访问其value。
2 为什么集合类没有实现Cloneable和Serializable接口?
cloneable其实就是一个标记接口,只有实现这个接口后,然后在类中重写Object中的clone方法,然后通过类调用clone方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportedException(克隆不被支持)异常。
首先说明Serializable主要作用将类的实例持久化保存,序列化就是保存,反序列化就是读取。保存也不一定保存在本地,也可以保存到远方。类一定要实现Serializable才可
应该集合中的元素自己实现,定义自己的行为。
3 如何判断一个类是否继承实现另外一个类。
boolean result = Serializable.class.isAssignableFrom(String.class);
4 什么是迭代器(Iterator)?
它可以使得对于序列类型的数据结构的遍历行为与被遍历的对象分离,即我们无需关心该序列的底层结构是什么样子的。只要拿到这个对象,使用迭代器就可以遍历这个对象的内部.
主要方法: *.iterator() 得到迭代器, hasNext( ) / next( ) / remove(删除后,指向下一个)List<String> list = new ArrayList<>();
list.add("a"); Iterator<String> it = list.iterator(); while(it.hasNext()) { System.out.println(it.next()); }
///////////////////////////////////////////////////////////////////////
///////////////////////下面迭代删除元素会抛异常///////////////////////////////////////////////
String[] str = {"1","2","3"};
list = Arrays.asList(str);
Iterator iter = list.iterator();
while(iter.hasNext()) {
Object value = iter.next();
if("1".equals(value))
iter.remove();
}
///////////////////////////////////////////////////////////////////////
///////////////////////下面迭代删除元素会正常执行///////////////////////////////////////////////
list = new ArrayList();
list.add("1");
Iterator iter = list.iterator();
while(iter.hasNext()) {
Object value = iter.next();
if("1".equals(value))
iter.remove();
}
原因是:
new ArrayList()和Arrays.asList(str)是不同的list, remove方法的定义不一样
迭代器优点:提供统一的迭代方法;可以在对客户透明的情况下,提供不同的迭代方法;快速失败机制,防止多线程下迭代的不安全操作。
5 快速失败&&安全失败:
(注意:以下的集合的修改是指,集合结构的变化,增加、删除节点,修改某一个节点的对象的内容不算是集合结构的变化。)
快速失败(fail-fast):
在用迭代器遍历一个集合时,遍历过程中,其他线程对集合内容进行修改(增,删等对集合结构方法变化的更改),则
会抛出异常ConCurrentModificationException。迭代器在遍历的时候直接访问集合的元素,并且在遍历过程中使用一modCount变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器用hasNext()/next()遍历一个元素的之前,都会检测modCount,如果改过,就抛出异常。Java.util包中的集合都是快速失败的,不能在多线程下发生并发修改。
安全失败(fail-safe):
如果在迭代器遍历concurrentHashMap的过程中,往map中增/删/改,是不会抛出异常。但是会导致不一致的问题。采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问,而是先复制原集合内容,在拷贝的集合上进行遍历。缺点是:不能访问修改后的内容,
java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
安全失败迭代器在迭代中被修改,不会抛出任何异常,因为它是在集合的克隆对象迭代的,所以任何对原集合对象的结构性修改都会被迭代器忽略,但是这类迭代器有一些缺点,其一是它不能保证你迭代时获取的是最新数据,因为迭代器创建之后对集合的任何修改都不会在该迭代器中更新,还有一个缺点就是创建克隆对象在时间和内存上都会增加一些负担。
6 ConcurrentHashMap是弱一致性的
ConcurrentHashMap与HashTable都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
那么既然ConcurrentHashMap那么优秀,为什么还要有Hashtable的存在呢?ConcurrentHashMap能完全替代HashTable吗?
HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
那么什么是强一致性和弱一致性呢?
get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素。
(补充:a、数据的可见性
直接进入正题,concurrentHashMap相信用的人也很多,因为在数据安全性上确实比HashMap好用,在性能上比hashtable也好用。大家都知道线程在操作一个变量的时候,比如i++,jvm执行的时候需要经过两个内存,主内存和工作内存。那么在线程A对i进行加1的时候,它需要去主内存拿到变量值,这个时候工作内存中便有了一个变量数据的副本,执行完这些之后,再去对变量真正的加1,但是此时线程B也要操作变量,并且逻辑上也是没有维护多线程访问的限制,则很有可能在线程A在从主内存获取数据并在修改的时候线程B去主内存拿数据,但是这个时候主内存的数据还没有更新,A线程还没有来得及讲加1后的变量回填到主内存,这个时候变量在这两个线程操作的情况下就会发生逻辑错误。
b、原子性
原子性就是当某一个线程A修改i的值的时候,从取出i到将新的i的值写给i之间线程B不能对i进行任何操作。也就是说保证某个线程对i的操作是原子性的,这样就可以避免数据脏读。
c、volatile的作用
Volatile保证了数据在多线程之间的可见性,每个线程在获取volatile修饰的变量时候都回去主内存获取,所以当线程A修改了被volatile修饰的数据后其他线程看到的一定是修改过后最新的数据,也是因为volatile修饰的变量数据每次都要去主内存获取,在性能上会有些牺牲。)
HashMap在多线程的场景下是不安全的,hashtable虽然是在数据表上加锁,纵然数据安全了,但是性能方面确实不如HashMap。那么来看看concurrentHashMap是如何解决这些问题的。
concurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作(这些操作与HashMap逻辑都是一样的,不同的是concurrentHashMap在执行这些操作的时候加入了重入锁ReentrantLock),这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。通俗的讲就是concurrentHashMap由多个hashtable组成。
7 Iterator和ListIterator区别
(简单说: list专用)
Iterator可以用来遍历Set,Map和List,但是ListItrator只能用来遍历List。
Iterator对集合只能向前遍历(hasNext(), Next()).ListIterator是双向都可以遍历。
ListIterator实现了Iterator接口,并包含了其他功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引。
8 HashMap的工作原理
键值对形式存储元素。需要一个hash函数,使用hashCode() 和equals()方法来向集合添加,检索元素。
Put(key, value) : 计算key的hash值,然后把键值对存储在集合中合适对索引上。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。
Get(key): 首先是用hashcode找到桶,然后key,equals(),判断是否相等。
重要特性是:容量(默认初始值是32),负载因子(默认是0.75),阈值是负载因子乘以容量,如果大于阈值,就会扩容。扩容极限。建议如果在开始的时候知道元素的个数,设定初始值。
容量:桶的数量。
负载因子:有多少桶被占用了。
(补充一个java 8之后hashmap的一个改进:原来在同一个桶里的元素用linkedlist连接起来,后来java 8 后,做了改进:如果元素个数>=8, 就使用tree结构,否则用list: 用treeNode的原理大概:treeNode是一个树形结构,其hash值是有大小的排序的,leftchild都小于当前节点的Hash值;rightchild的hash值大于当前的hash值,这样就可以用二分查找的办法来加速查找。)
9 HashMap VS Hashtable
都实现了Map接口,很相似。有如下不同点:
hashMap的键和值可以是null,hasttable不允许键或值是null.
HashMap不是同步的(适合单线程,是快速失败的),hashtable(适合多线程)是同步的。
HashMap提供了键集合;而Hashtable提供了对键的枚举。
Hashtable是遗留的类。
10 数组(Array)和列表(ArrayList)有什么区别?各自的应用场合?
Array:可以存放基本类型+对象类型;ArrayList:only对象类型(因为LIst里的元素必须继承自object)。
List<Object> list = new ArrayList<>();
// 如下: 可以放各种类型 list.add(1); // 装箱 list.add(false); list.add("a");
Array大小固定的; ArrayList是动态变化的。
ArrayList提供了很多方法: addAll(), removeall(), iterator()等等。
对于基本类型,集合使用自动装箱来减少代码量;但是,当处理固定大小的基本数据类型的时候,这种方式比较慢。
java提供了二者互相转化的方法:
list.toArray();
Arrays.asList(arr);
11 ArrayList VS LinkedList
都是实现了List接口。
ArrayList底层是数组。可以以o(1)时间复杂度对元素进行随机访问。
LinkedList: 是以元素列表的形式存储数据,链表。查找的时间复杂度是0(n),但是插入,添加,删除更快。
LinkedList更费内存,因为要存储指针。
12 Comparable 和 Comparator?
实现了Comparable,表明可以比较元素,可以用SORT排序。Comparator看做是算法的实现,将算法和数据分离。
用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。
Comparable接口:只包含了一个compareTo()方法,可以进行排序。
Comparator接口:包含compare()和 equals()两个方法。
二者用法不同:比如在集合中的元素可以自己实现Comparable,这样就可以用Collections.sort()调用进行排序;如果想定义格外的排序方法,可以实现Comparator接口,Collections.sort()另外一个重载方法可以接收一个compareator.
13 java集合类的最佳实践
根据需要正确的选择要使用的。比如:元素大小固定,事先知道,用 Array 而不是ArrayList
对于一些容器(hashMap):如果知道容量,可以提前给一个初始容量,避免扩容,再哈西的性能损耗。
为了类型安全,可读性和健壮性总是要使用泛型,泛型可以避免ClassCastException
用JDK提供的不变类(immutable class)作为Map的键可以避免为我们的类实现hashCode() 和
equals() 方法。
编程的时候接口优于实现。
底层的集合实际上是空的情况下,返回长度是0的集合或数组,不要返回null( 非常好的建议)。
14 java优先级队列(Priority Queue)
每次出队的是优先级最高的。是用大顶堆实现的。要提供一个比较器comparator,否则按照自然顺序排列。
Queue<Test> priQueue = new PriorityQueue<Test>(11,OrderIsdn); // 指定一个comparator
// 默认队列大小是11
Test t1 = new Test("t1",1);
Test t3 = new Test("t3",3);
priQueue.add(t3);
基于优先级堆无界队列,它的元素按照自然顺序排序,在创建的时候可以提供一个比较器。不允许null值。不是线程安全的,入队和出对的时间复杂度是log(n).
15 什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
如何做到线程安全:
四种方式 sychronized关键字
1. sychronized method(){} // 同步方法
2. sychronized (objectReference) {/*block*/} // 同步块
3. static synchronized method(){} // static 同步方法
4. sychronized(classname.class) // 指定同步类
其中1和2是代表锁当前对象,即一个对象就一个锁,3和4代表锁这个类,即这个类的锁。要注意的是sychronized method()不是锁这个函数,而是锁对象,即:如果这个类中有两个方法都是sychronized,那么只要有两个线程共享一个该类的reference,每个调用这两个方法之一,不管是否同一个方法,都会用这个对象锁进行同步。
注意:long 和double是简单类型中两个特殊的咚咚:java读他们要读两次,所以需要同步。
16 如何权衡用无序数组还是有序数组
有序数组查找的时间复杂度是O(logn),但是插入操作的时间复杂度是O(n).
无序的是O(n),但是插入的时间复杂度是O(1).
17 Java中,int 是否是类型安全的。
Int 的赋值是原子的,但是i++不是。
18 HashSet VS TreeSet
HashSet 元素是无序的,最多放入一个NULL值,add(), remove(), contains(), 这些方法的复杂度都是O(1)
TreeSet: 树形结构实现,元素是有序的,不能放入NULL值。 Add(), remove(), contains()复杂度是log(n).
2 hash处理冲突方式?HashMap采用的什么办法?
开放地址法: 线性探测 、 线性补偿法、伪随机数法。
拉链法:HashMap采用链地址法解决冲突:数组+链表到方式。
再散列:
建公共溢出区:
一 ArrayList和LinkedList都实现了List接口,不同点:
(1)ArrayList是基于索引的数据接口(动态数组,和数组不同,数组是new的时候就确认大小了。),它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是 O(n);看如下的get(i)的源码就能发现这种区别: List<String> array = new ArrayList<>(); get/set都是随机访问,所以ArrayList效率高;
array.set(3, "newvalue");
List<String> linked = new LinkedList<>();
linked.get(4);
linked.set(3, "newvalue");
(2)相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。 ArrayList在执行add后,要移动很多元素; remove后也是; 但是Linked不用;(注意,前面说的不准确: ArrayList想要在指定位置插入或删除元素时,主要耗 时的是System.arraycopy动作,会移动index后面所有的元素;LinkedList主耗时的是要先通过for循环找到index,然后直接插入或删除。这就导致了两者并非一定谁快谁慢。当数据量较大时,大约在容量的1/10处开始,LinkedList的效率就开始没有ArrayList效率高了,特别到一半以及后半的位置插入时,LinkedList效率明显要低于ArrayList,而且数据量越大,越明显。
我看了源码: 比如list.remove(index), 对于arrayList可以直接找到元素,然后把index后面的元素往前拷贝;LinkedList用的是双向链表,所以先判断index是在前半部还是后半部,如果前半部,就从list的header往后找;否则从最后一个往前找。)
List<String> array = new ArrayList<>();
array.add("aa");
array.add(2, "String");//在指定位置插入元素
array.remove(5);//移除指定位置的元素
List<String> linked = new LinkedList<>();
linked.add("bb");
linked.add(2, "String"); //在指定位置插入元素
linked.remove(5); //移除指定位置的元素
(3)LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
(4) LinkedList是个双向链表,它同样可以被当作栈(一端add/remove)、队列(一端add,另外一端remove)或双端队列(双端add/remove)来使用。
java中的数据存储方式有两种结构,一种是数组,另一种就是链表,前者的特点是连续空间,寻址迅速,但是在增删元素的时候会有较大幅度的移动,所以数组的特点是查询速度快,增删较慢;而链表由于空间不连续,寻址困难,增删元素只需修改指针,所以链表的特点是查询速度慢、增删快。那么有没有一种数据结构来综合一下数组和链表以便发挥他们各自的优势?答案就是哈希表
HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。
HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快.
当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。
当两个对象的hashcode相同会发生什么? 因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中.
如果两个键的hashcode相同,你如何获取值对象?当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点.
如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
你了解重新调整HashMap大小存在什么问题吗?当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢。
为什么String, Interger这样的wrapper类适合作为键?String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
我们可以使用自定义的对象作为键吗?当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
我们可以使用CocurrentHashMap来代替Hashtable吗?这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。
HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
一 最大的区别就是ConcurrentHashMap是线程安全的,hashMap不是线程安全的
二 ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。
在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中:
1: 为什么HashMap容量是2的幂?什么是负载因子?
容量:桶的数量。
负载因子:元素个数/容量。
是2的幂的原因: 让元素加入到桶后,分布的更均匀。
2: 为什么String, Integer这样的wrapper类适合作为key?
因为是不可变的,final的,而且重新写了hash()和 equals().、不可变是必要的:为了计算hashcode,防止key的值变化。自己定义的类如果能满足这个要求可以作为map的key。
3:HashMap默认容量是16, 默认装填因子是0.75。
4:HashSet VS TreeSet?
|——SortedSet接口——TreeSet实现类
Set接口— |——HashSet实现类
|——LinkedHashSet实现类
HashSet: 不保证排列顺序/不是同步的/最多能放一个null
当向hashset存入一个元素,HashSet会调用hashCode(),来决定存放位置。
在HashSet中比较两个对象是否相等的方法是:先比较两个对象的hashCode()值是否相等,如果不相等就认为两个对象是不相等的,如果两个对象的hashCode相等就继续调用equals()方法进一步判断两个对象是否相等,如果equals()方法返回true认为两个对象相等,返回false认为两个对象不相等。
TreeSet是一个有序集合,元素中安升序排序,缺省是按照自然顺序进行排序,意味着TreeSet中元素要实现Comparable接口;我们可以构造TreeSet对象时,传递实现了Comparator接口的比较器对象.
HashSet是基于Hash算法实现的,其性能通常优于TreeSet,我们通常都应该使用HashSet.
在我们需要确定是否相等的时候用到HashSet; 排序的功能时,我门才使用TreeSet;
2: java 集合类与数组的区别和联系?
区别:java集合类长度是动态的,数组则是固定的;
java集合类中存的是类对象,数组可以放对象也可以是基本类型;
集合类的各种操作方法更多
array支持多维数组
联系:toArry() 和 Arrays.asList() 二者可以互相转化, 代码如下
String[] strArr = new String[]{"aa", "bb"};
List<String> list1 = Arrays.asList(strArr);
String[] b = (String[]) list1.toArray();
5: 集合类使用最佳实践
●如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
●如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高;如果多个线程可能同时操作一个类,应该使用同步的类。
●要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
●尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
6:为何Collection不从Cloneable和Serializable接口继承
Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。很多Collection实现有一个公有的clone方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为Collection是一个抽象表现。重要的是实现。
当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。
在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。
7:Map不继承Collection接口?集合
8:iterator?
Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。
Iterator接口有三个方法: next, hasNext, remove
9.Enumeration和Iterator接口的区别?
Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。但是,与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。
迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者从集合中移除元素,而Enumeration不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。
10.Iterater和ListIterator之间有什么区别?
(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。
(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
11.遍历一个List有哪些不同的方式?
List<String> list1 = new ArrayList<String>();
for (String str : list1) // 用for-each的方式
{
System.out.print(str);
}
Iterator<String> it = list1.iterator(); // 用迭代器
while(it.hasNext())
{
System.out.print(it.next());
}
Iterator<String> listIt = list1.listIterator(); // 用list的专用迭代器
while(listIt.hasNext())
{
System.out.print(listIt.next());
}
使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。
12: 什么是fail-fase, 快速失败?
每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。
13:hashCode()和equals()方法有何重要性?
HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则:
(1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
(2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。
14:是否可以使用任何类作为hashmap的key?
参考13, hashCode()和 equals()方法要满足规则的不可变类。
15: map可以得到3个set 集合
注意这3个集合可以用iterator支持remove操作,但是不支持对集合的add相关操作。
Map<String, String> ma = new HashMap<String, String>();
ma.put("key", "value");
Collection<String> values = ma.values();
Set<String> keyset = ma.keySet();
Set<Entry<String, String>> entrySet = ma.entrySet();
ma.put("key", "value");
Collection<String> values = ma.values();
//values.add("fdsa"); 不可以
Set<String> keyset = ma.keySet();
Iterator<String> it12 = keyset.iterator();
while(it12.hasNext())
{
String fa = it12.next();
it12.remove(); // 对集合的remove操作会影响原来的map, 同样,map的变化也会反映到集合中。 当迭代器遍历的时候,如果map发生变化,会抛出异常。
}
16: 集合的排序
Collections.sort(list<T>, c); // T类型要implements comparable接口。
Collections.sort(list<T>, Comparator<? extend T>); //定义新的comparator函数。
Arrays.sort(); // 对数组进行排序
(排序以前用的是归并排序,1.7后改了)
17: 为甚ConcurrentHashMap效率高?
CAS:
ReentrantLock:
UNSAFE:
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。
上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor
public V put(K key, V value) { // 当插入第一个元素的时候,需要先初始化数组大小 if (table == EMPTY_TABLE) { inflateTable(threshold); } // 如果 key 为 null,感兴趣的可以往里看,最终会将这个 entry 放到 table[0] 中 if (key == null) return putForNullKey(value); // 1. 求 key 的 hash 值 int hash = hash(key); // 2. 找到对应的数组下标 int i = indexFor(hash, table.length); // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在, // 如果有,直接覆盖,put 方法返回旧值就结束了 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // 4. 不存在重复的 key,将此 entry 添加到链表中,细节后面说 addEntry(hash, key, value, i); return null;
Java7 ConcurrentHashMap
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。
整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
ava8 HashMap
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。
为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
来一张图简单示意一下吧:
注意,上图是示意图,主要是描述结构,不会达到这个状态的,因为这么多数据的时候早就扩容了。
下面,我们还是用代码来介绍吧,个人感觉,Java8 的源码可读性要差一些,不过精简一些。
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。
18 hashmap扩容
HashMap在底层数据结构上采用了数组+链表+红黑树(java 8),通过散列映射来存储键值对数据因为在查询上使用散列码(通过键生成一个数字作为数组下标,这个数字就是hash code)所以在查询上的访问速度比较快,HashMap最多允许一对键值对的Key为Null,允许多对键值对的value为Null。它是非线程安全的.
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
19 为什么hashmap初始化数组的大小是2的n次方的大小?
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。
看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。
所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):