二、容器
1. Java容器都有哪些?
2. Collection和Collections区别?
Collection:类集接口是集合框架层次结构中根接口,里面提供了很多方法让它的实现类去实现(主要接口:List、Set、其他);
Collectios:是一个工具类,但它提供了很多静态方法,专门为 Collection接口的实现类和 Map接口的实现类服务。
3. List、Set、Map之间的区别?
【1】List、Set、Map区别:List有序且可重复,Set无序且不可重复,Map无序且不允许 key重复,LinkedHashSet有序且不重复;(Set、map无序,但 TreeSet、TreeMap是有序的)
【2】带 Tree的 都不能添加空元素(因为要排序),否则空指针异常;
【3】Hash、Tree和 HashMap、TreeMap: hash比 Tree效率高,只有排序时用 Tree(一般用 hash,排序时用 Tree);(HashMap、TreeMap:对一个 key集合进行有序的遍历时用 TreeMap)
【4】有没有有顺序的 Map实现类,如果有,它们是怎么保证有序的?:TreeMap、LinkedHashMap是有序的:TreeMap默认升序,LinkedHashMap则记录了插入顺序。
【4】栈、队列的特点: 栈是先进后出,队列是先进先出;
【3】Set中元素不可重复(Set底层是 Map的 key),用 equals()判断是否重复:==比较基本数据类型时比较值是否相同,比较引用数据类型时比较的是引用是否相同;equals本质也是比较引用是否相同(只是String、Integer等类重写了 equals方法,所以一般情况下 equals方法比较的是值是否相同)。Set中存放的是对象的引用,所以用 equals方法来判断 set中元素是否重复。
3_1. List、Set、Map三个接口存取元素时,各有什么特点?
List 使用 get(index)取值,也可使用 Iterator、toArray 取值;
Set 只能使用 Iterator、toArray 取值;
Map 使用 get(key)取值,可使用 entrySet 取全部映射,也可使用 ketSet 取键集合、values 取值集合。
3_2. set中元素不可重复,用什么方法来区分是否重复?
判断 set中元素是否重复视具体情况而定:
① HashSet使用 equals 比较;
② TreeSet使用 compareTo比较;
4. ArrayList和 LinkedList
ArrayList和 LinkedList
同: 两者都实现了 List接口;
异:
① ArrayList底层是动态数组,LinkedList底层是双向链表;
② ArrayList作随机访问和遍历查询时效率高,它可以以 O(1)的时间复杂度对元素进行随机访问;LinkedList是以双向链表的形式存储数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是 O(n);
③ LinkedList作插入、删除时效率高,直接改变 Node指向,不需要像数组那样重新计算大小或是更新索引;
④ LinkedList比 ArrayList更占内存,因为 LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向后一个元素。
4_1. ArrayList和 Vector
ArrayList和 Vector
同: 都使用了数组的方式存储数据(底层是数组);都实现了 List接口,有序且可重复(可有多个 null);
异:
① 同步性:Vector使用了 Synchronized来实现线程同步,是线程安全的,ArrayList是非线程安全的,效率高,性能上优于 Vector;(需要同步时,一般用 Collections工具类来构造出同步的 ArrayList,而不用 Vector)
② 扩容大小:ArrayList增长为原来的 0.5倍,Vector增长为原来的 1倍;
4_2. ArrayList和 Array
4_3. 如何实现数组和List之间的转换
比较常见:通过Arrays.asList()创建一个list集合
List<String> players = Arrays.asList("Tom","Jay","June", "July");
List<Person> people = Arrays.asList(
new Person("Charlotte", "Neward", 39),
new Person("Michael", "Neward", 19),
new Person("Jeff", "Brown", 43)
);
4_4. 如何实现集合反转
利用 Collections类的 reverse方法:Collections.reverse(list);
5. HaspMap和 HashTable
【 HaspMap和 HashTable: 】
同: 都属于 Map接口类(即都实现了 Map接口);
异:
① 同步性:HashTable是线程同步的,HaspMap是非线程同步的,效率高;(需要同步时,往往不用 HashTable,而使用 ConcurrentHashMap)
② 是否允许 null:HashMap允许一个 null键和多个 null值;HashTable不允许 null键和 null值;
③ contains方法:HashTable有 contains()方法;HashMap把 HashTable的 contains()方法去掉了,改为 containsKey()、containsValue();
④ 继承不同:
HashTable<K,V> extends Dictionary<K,V>;
HashMap<K,V> extends Abstract<K,V>;
5_1. HaspMap和 TreeMap
6. HaspMap的实现原理 以及 Hash碰撞
为什么会将链表转红黑树的值设定为8 & ConcurrentHashMap在1.8之后没采用分段锁? :红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
HaspMap的实现原理:HashMap是 数组+链表+红黑树(JDK1.8增加了红黑树部分) 实现的,基于 Hash算法,通过 put(key,value)存储,get(key)来获取。当传入 key时,HashMap会根据 key.hashCode()计算出 hash值,根据 hash值将 value保存在 bucket里。当计算的 hash值相同时,我们称之为 Hash冲突(Hash碰撞),当发生 Hash碰撞 时,HashMap再用 equals()来比较 key是否相同:如果 hash相同, key也相同,value直接覆盖;如果 hash相同,key不同, HashMap的做法是用链表和红黑树存储相同 hash值的 value,当 hash冲突的个数比较少,先在相应的桶中形成一个链表结构,使用链表进行存储,否则使用红黑树。
JDK1.8 引入红黑树:当拉链过长时,会严重影响 HashMap的性能,于是在 JDK1.8中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap的性能。
Hash碰撞:对象(即 key)的 hashCode相同。发生 Hash碰撞即对象的 hashCode相同时,HashMap再用 equals()来比较 key是否相同:如果 hash相同, key也相同,value直接覆盖;如果 hash相同,key不同,HashMap的做法是用链表和红黑树存储相同 hash值的 value,当 hash冲突的个数比较少,先在相应的桶中形成一个链表结构,使用链表进行存储,否则使用红黑树。(因为 HashMap的 key唯一,如果添加相同的 key值,value会被覆盖)(HashMap很少用到 equals(),只有当发生哈希碰撞时才会执行)(key是由 hashCode和 equals两者来判断是否相同)。
map使用 keySet() 取键集合,values() 取值集合;使用 entrySet() 取全部映射,再getKey()\getValue();如下 eg:
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
System.out.pringln(entry.getKey(),entry.getValue());
}
若只需要 map中的键或值,通过 map.keySet()/map.values():
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
for(Integer key:map.keySet()){
System.out.pringln(key);
}
for(Integer value:map.values()){
System.out.pringln(values);
}
【1】HashTable为什么是线程安全的?:因为 HashTable内部为所有的操作都加了同步锁(synchronized);
【2】为什么加了 synchronized锁会导致效率降低?:因为 加了同步锁时其他线程再访问,必须等到前一个线程释放了锁才能使用;
【3】公司有些大数据量需要用到 HashTable,但又想保证执行效率怎么办?:用 ConcurrentHashMap;
7. ConcurrentHashMap的实现原理
【1】ConcurrentHashMap的实现原理:ConcurrentHashMap的内部实现进行了锁分离(或锁分段),将数据分段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如 size()和 containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁,所以它的锁粒度小于同步的 HashMap;同时,ConcurrentHashMap的 get() 操作也是无锁的。除非读到的值是空的才会加锁重读,我们知道 HashTable容器的 get方法是需要加锁的,那么ConcurrentHashMap的 get操作是如何做到不加锁的呢?原因是它的 get方法里将要使用的共享变量都定义成 volatile。(在 HashMap的基础上,将数据分段存储,ConcurrentHashMap由多个 Segment组成,每个 Segment都有把锁()Segment下包含很多 Node,也就是我们的键值对了))
【2】ConcurrentHashMap是如何保证线程安全的?:使用分段锁 segment;
【3】为什么使用分段锁比同步锁效率高?:分段锁就是把 map分成多个 segment去处理,进行 put操作时,根据 hashcode找到对应的锁,彼此间不受影响,所以效率高;而同步锁锁住的是整个 hashTable,所以效率低。
7. 高并发下使用 ConcurrentHashMap
参考:https://www.cnblogs.com/yueshutong/p/9696216.html
总结与思考:
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的Segment+ReentrantLock+HashEntry,到JDK1.8版本中Synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考:
1. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点);
2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了;
3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档;
4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:①因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了;②JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然;③在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据;
【1】并发 Map:在多线程环境下使用Map,一般也可以使用 Collections.synchronizedMap()方法得到一个线程安全的 Map。但是在高并发的情况下,这个Map的性能表现不是最优的。由于 Map 是使用相当频繁的一个数据结构,因此 JDK 中便提供了一个专用于高并发的 Map 实现 ConcurrentHashMap。
【2】为什么不能在高并发下使用HashMap?
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
【3】ConcurrentHashMap的优势:
ConcurrentHashMap的内部实现进行了锁分离(或锁分段),将数据分段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁,
所以它的锁粒度小于同步的 HashMap;同时,ConcurrentHashMap的 get() 操作也是无锁的。除非读到的值是空的才会加锁重读,我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成 volatile。(在 HashMap的基础上,将数据分段存储,ConcurrentHashMap由多个 Segment组成,每个 Segment都有把锁(Segment下包含很多 Node,也就是我们的键值对了))
锁分离: 首先将数据分段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
【4.1】 volatile和synchronized特点:
首先需要理解线程安全的两个方面:执行控制和内存可见:
执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。
volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。
对于volatile关键字,当且仅当满足以下所有条件时可使用:
1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在具有其他变量的不变式中。
【4.2】volatile和 synchronized的区别:
① volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。(volatile关键字就是要告诉编译器,这个变量是经常改变的,而且编译时不要进行代码优化,所以每次读写的时候都要到它所在的地址去读取)
② volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的(volatile是变量修饰符,synchronized是修饰类、方法、代码段)
③ volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
④ volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
⑤ volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
8. HaspSet的实现原理
9. Queue中的 poll()和 remove()区别
10. 哪些集合是线程安全的
11. 迭代器 Iterator
12. 怎么确保一个集合不能被修改?
13. 如何对集合进行排序?
参考一:Java中对集合进行排序的两种方法
参考二:java如何对map进行排序详解(map集合的使用)
参考三:random.nextInt()的用法
- 使用Collections.sort(lists);
- 使用 Collections.sort(lists,Comparator);(常用!)
对于1:lists中的保存的对象的类需要实现Comparable接口,重写其中的compareTo()方法;
对于2:使用匿名内部类,自定义比较器:
Collections.sort(cells,new Comparator<Cell>(){//此处创建了一个匿名内部类
@Override
public int compare(Cell o1,Cell o2){
return o1.getY() - o2.getY();
}