mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
Change file position
This commit is contained in:
parent
ec60cfa9b8
commit
271c551daf
12
README.md
12
README.md
@ -74,14 +74,10 @@
|
||||
|
||||
### 容器
|
||||
|
||||
* **常见问题总结:**
|
||||
* [这几道Java集合框架面试题几乎必问](docs/java/这几道Java集合框架面试题几乎必问.md)
|
||||
* [Java 集合框架常见面试题总结](docs/java/Java集合框架常见面试题总结.md)
|
||||
* **源码分析:**
|
||||
* [ArrayList 源码学习](docs/java/ArrayList.md)
|
||||
* [【面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制](docs/java/ArrayList-Grow.md)
|
||||
* [LinkedList 源码学习](docs/java/LinkedList.md)
|
||||
* [HashMap(JDK1.8)源码学习](docs/java/HashMap.md)
|
||||
* [这几道Java集合框架面试题几乎必问](docs/java/这几道Java集合框架面试题几乎必问.md)
|
||||
* [ArrayList 源码学习](docs/java/collection/ArrayList.md)
|
||||
* [LinkedList 源码学习](docs/java/collection/LinkedList.md)
|
||||
* [HashMap(JDK1.8)源码学习](docs/java/collection/HashMap.md)
|
||||
|
||||
### 并发
|
||||
|
||||
|
@ -1,353 +0,0 @@
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
1. [List,Set,Map三者的区别及总结](#list,setmap三者的区别及总结)
|
||||
1. [Arraylist 与 LinkedList 区别](#arraylist-与-linkedlist-区别)
|
||||
1. [ArrayList 与 Vector 区别(为什么要用Arraylist取代Vector呢?)](#arraylist-与-vector-区别)
|
||||
1. [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别)
|
||||
1. [HashSet 和 HashMap 区别](#hashset-和-hashmap-区别)
|
||||
1. [HashMap 和 ConcurrentHashMap 的区别](#hashmap-和-concurrenthashmap-的区别)
|
||||
1. [HashSet如何检查重复](#hashset如何检查重复)
|
||||
1. [comparable 和 comparator的区别](#comparable-和-comparator的区别)
|
||||
1. [Comparator定制排序](#comparator定制排序)
|
||||
1. [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序)
|
||||
1. [如何对Object的list排序?](#如何对object的list排序)
|
||||
1. [如何实现数组与List的相互转换?](#如何实现数组与list的相互转换)
|
||||
1. [如何求ArrayList集合的交集 并集 差集 去重复并集](#如何求arraylist集合的交集-并集-差集-去重复并集)
|
||||
1. [HashMap 的工作原理及代码实现](#hashmap-的工作原理及代码实现)
|
||||
1. [ConcurrentHashMap 的工作原理及代码实现](#concurrenthashmap-的工作原理及代码实现)
|
||||
1. [集合框架底层数据结构总结](#集合框架底层数据结构总结)
|
||||
1. [- Collection](#--collection)
|
||||
1. [1. List](#1-list)
|
||||
1. [2. Set](#2-set)
|
||||
1. [- Map](#--map)
|
||||
1. [集合的选用](#集合的选用)
|
||||
1. [集合的常用方法](#集合的常用方法)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
|
||||
## <font face="楷体">List,Set,Map三者的区别及总结</font>
|
||||
- **List:对付顺序的好帮手**
|
||||
|
||||
List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
|
||||
- **Set:注重独一无二的性质**
|
||||
|
||||
不允许重复的集合。不会有多个元素引用相同的对象。
|
||||
|
||||
- **Map:用Key来搜索的专家**
|
||||
|
||||
使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
|
||||
|
||||
|
||||
## <font face="楷体">Arraylist 与 LinkedList 区别</font>
|
||||
Arraylist底层使用的是数组(存读数据效率高,插入删除特定位置效率低),LinkedList 底层使用的是双向链表数据结构(插入,删除效率特别高)(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别:); 详细可阅读JDK1.7-LinkedList循环链表优化。学过数据结构这门课后我们就知道采用链表存储,插入,删除元素时间复杂度不受元素位置的影响,都是近似O(1)而数组为近似O(n),因此当数据特别多,而且经常需要插入删除元素时建议选用LinkedList.一般程序只用Arraylist就够用了,因为一般数据量都不会蛮大,Arraylist是使用最多的集合类。
|
||||
|
||||
## <font face="楷体">ArrayList 与 Vector 区别</font>
|
||||
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector
|
||||
,代码要在同步操作上耗费大量的时间。Arraylist不是同步的,所以在不需要同步时建议使用Arraylist。
|
||||
|
||||
## <font face="楷体">HashMap 和 Hashtable 的区别</font>
|
||||
1. HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized修饰。
|
||||
|
||||
2. 因为线程安全的问题,HashMap要比HashTable效率高一点,HashTable基本被淘汰。
|
||||
3. HashMap允许有null值的存在,而在HashTable中put进的键值只要有一个null,直接抛出NullPointerException。
|
||||
|
||||
Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java5或以上的话,请使用ConcurrentHashMap吧
|
||||
|
||||
## <font face="楷体">HashSet 和 HashMap 区别</font>
|
||||

|
||||
|
||||
## <font face="楷体">HashMap 和 ConcurrentHashMap 的区别</font>
|
||||
[HashMap与ConcurrentHashMap的区别](https://blog.csdn.net/xuefeng0707/article/details/40834595)
|
||||
|
||||
1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
|
||||
2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
|
||||
|
||||
## <font face="楷体">HashSet如何检查重复</font>
|
||||
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版)
|
||||
|
||||
**hashCode()与equals()的相关规定:**
|
||||
1. 如果两个对象相等,则hashcode一定也是相同的
|
||||
2. 两个对象相等,对两个equals方法返回true
|
||||
3. 两个对象有相同的hashcode值,它们也不一定是相等的
|
||||
4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
|
||||
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
|
||||
|
||||
**==与equals的区别**
|
||||
|
||||
1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
|
||||
2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
|
||||
3. ==指引用是否相同 equals()指的是值是否相同
|
||||
|
||||
## <font face="楷体">comparable 和 comparator的区别</font>
|
||||
- comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
|
||||
- comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序
|
||||
|
||||
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().
|
||||
|
||||
### <font face="楷体">Comparator定制排序<font face="楷体">
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* TODO Collections类方法测试之排序
|
||||
* @author 寇爽
|
||||
* @date 2017年11月20日
|
||||
* @version 1.8
|
||||
*/
|
||||
public class CollectionsSort {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
ArrayList<Integer> arrayList = new ArrayList<Integer>();
|
||||
arrayList.add(-1);
|
||||
arrayList.add(3);
|
||||
arrayList.add(3);
|
||||
arrayList.add(-5);
|
||||
arrayList.add(7);
|
||||
arrayList.add(4);
|
||||
arrayList.add(-9);
|
||||
arrayList.add(-7);
|
||||
System.out.println("原始数组:");
|
||||
System.out.println(arrayList);
|
||||
// void reverse(List list):反转
|
||||
Collections.reverse(arrayList);
|
||||
System.out.println("Collections.reverse(arrayList):");
|
||||
System.out.println(arrayList);
|
||||
/*
|
||||
* void rotate(List list, int distance),旋转。
|
||||
* 当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将
|
||||
* list的前distance个元素整体移到后面。
|
||||
|
||||
Collections.rotate(arrayList, 4);
|
||||
System.out.println("Collections.rotate(arrayList, 4):");
|
||||
System.out.println(arrayList);*/
|
||||
|
||||
// void sort(List list),按自然排序的升序排序
|
||||
Collections.sort(arrayList);
|
||||
System.out.println("Collections.sort(arrayList):");
|
||||
System.out.println(arrayList);
|
||||
|
||||
// void shuffle(List list),随机排序
|
||||
Collections.shuffle(arrayList);
|
||||
System.out.println("Collections.shuffle(arrayList):");
|
||||
System.out.println(arrayList);
|
||||
|
||||
// 定制排序的用法
|
||||
Collections.sort(arrayList, new Comparator<Integer>() {
|
||||
|
||||
@Override
|
||||
public int compare(Integer o1, Integer o2) {
|
||||
return o2.compareTo(o1);
|
||||
}
|
||||
});
|
||||
System.out.println("定制排序后:");
|
||||
System.out.println(arrayList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
### <font face="楷体">重写compareTo方法实现按年龄来排序</font>
|
||||
```java
|
||||
package map;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class TreeMap2 {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO Auto-generated method stub
|
||||
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
|
||||
pdata.put(new Person("张三", 30), "zhangsan");
|
||||
pdata.put(new Person("李四", 20), "lisi");
|
||||
pdata.put(new Person("王五", 10), "wangwu");
|
||||
pdata.put(new Person("小红", 5), "xiaohong");
|
||||
// 得到key的值的同时得到key所对应的值
|
||||
Set<Person> keys = pdata.keySet();
|
||||
for (Person key : keys) {
|
||||
System.out.println(key.getAge() + "-" + key.getName());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
|
||||
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
|
||||
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
|
||||
|
||||
class Person implements Comparable<Person> {
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO重写compareTo方法实现按年龄来排序
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Person o) {
|
||||
// TODO Auto-generated method stub
|
||||
if (this.age > o.getAge()) {
|
||||
return 1;
|
||||
} else if (this.age < o.getAge()) {
|
||||
return -1;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## <font face="楷体">如何对Object的list排序</font>
|
||||
- 对objects数组进行排序,我们可以用Arrays.sort()方法
|
||||
- 对objects的集合进行排序,需要使用Collections.sort()方法
|
||||
|
||||
|
||||
## <font face="楷体">如何实现数组与List的相互转换</font>
|
||||
List转数组:toArray(arraylist.size()方法;数组转List:Arrays的asList(a)方法
|
||||
```java
|
||||
List<String> arrayList = new ArrayList<String>();
|
||||
arrayList.add("s");
|
||||
arrayList.add("e");
|
||||
arrayList.add("n");
|
||||
/**
|
||||
* ArrayList转数组
|
||||
*/
|
||||
int size=arrayList.size();
|
||||
String[] a = arrayList.toArray(new String[size]);
|
||||
//输出第二个元素
|
||||
System.out.println(a[1]);//结果:e
|
||||
//输出整个数组
|
||||
System.out.println(Arrays.toString(a));//结果:[s, e, n]
|
||||
/**
|
||||
* 数组转list
|
||||
*/
|
||||
List<String> list=Arrays.asList(a);
|
||||
/**
|
||||
* list转Arraylist
|
||||
*/
|
||||
List<String> arrayList2 = new ArrayList<String>();
|
||||
arrayList2.addAll(list);
|
||||
System.out.println(list);
|
||||
```
|
||||
## <font face="楷体">如何求ArrayList集合的交集 并集 差集 去重复并集</font>
|
||||
需要用到List接口中定义的几个方法:
|
||||
|
||||
- addAll(Collection<? extends E> c) :按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾
|
||||
实例代码:
|
||||
- retainAll(Collection<?> c): 仅保留此列表中包含在指定集合中的元素。
|
||||
- removeAll(Collection<?> c) :从此列表中删除指定集合中包含的所有元素。
|
||||
```java
|
||||
package list;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*TODO 两个集合之间求交集 并集 差集 去重复并集
|
||||
* @author 寇爽
|
||||
* @date 2017年11月21日
|
||||
* @version 1.8
|
||||
*/
|
||||
public class MethodDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO Auto-generated method stub
|
||||
List<Integer> list1 = new ArrayList<Integer>();
|
||||
list1.add(1);
|
||||
list1.add(2);
|
||||
list1.add(3);
|
||||
list1.add(4);
|
||||
|
||||
List<Integer> list2 = new ArrayList<Integer>();
|
||||
list2.add(2);
|
||||
list2.add(3);
|
||||
list2.add(4);
|
||||
list2.add(5);
|
||||
// 并集
|
||||
// list1.addAll(list2);
|
||||
// 交集
|
||||
//list1.retainAll(list2);
|
||||
// 差集
|
||||
// list1.removeAll(list2);
|
||||
// 无重复并集
|
||||
list2.removeAll(list1);
|
||||
list1.addAll(list2);
|
||||
for (Integer i : list1) {
|
||||
System.out.println(i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## <font face="楷体">HashMap 的工作原理及代码实现</font>
|
||||
|
||||
[集合框架源码学习之HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56)
|
||||
|
||||
## <font face="楷体">ConcurrentHashMap 的工作原理及代码实现</font>
|
||||
|
||||
[ConcurrentHashMap实现原理及源码分析](http://www.cnblogs.com/chengxiao/p/6842045.html)
|
||||
|
||||
|
||||
## <font face="楷体">集合框架底层数据结构总结</font>
|
||||
### - Collection
|
||||
|
||||
#### 1. List
|
||||
- Arraylist:数组(查询快,增删慢 线程不安全,效率高 )
|
||||
- Vector:数组(查询快,增删慢 线程安全,效率低 )
|
||||
- LinkedList:链表(查询慢,增删快 线程不安全,效率高 )
|
||||
|
||||
#### 2. Set
|
||||
- HashSet(无序,唯一):哈希表或者叫散列集(hash table)
|
||||
- LinkedHashSet:链表和哈希表组成 。 由链表保证元素的排序 , 由哈希表证元素的唯一性
|
||||
- TreeSet(有序,唯一):红黑树(自平衡的排序二叉树。)
|
||||
|
||||
### - Map
|
||||
- HashMap:基于哈希表的Map接口实现(哈希表对键进行散列,Map结构即映射表存放键值对)
|
||||
- LinkedHashMap:HashMap 的基础上加上了链表数据结构
|
||||
- HashTable:哈希表
|
||||
- TreeMap:红黑树(自平衡的排序二叉树)
|
||||
|
||||
|
||||
## <font face="楷体">集合的选用</font>
|
||||
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
|
||||
|
||||
2018/3/11更新
|
||||
## <font face="楷体">集合的常用方法</font>
|
||||
今天下午无意看见一道某大厂的面试题,面试题的内容就是问你某一个集合常见的方法有哪些。虽然平时也经常见到这些集合,但是猛一下让我想某一个集合的常用的方法难免会有遗漏或者与其他集合搞混,所以建议大家还是照着API文档把常见的那几个集合的常用方法看一看。
|
||||
|
||||
会持续更新。。。
|
||||
|
||||
**参考书籍:**
|
||||
|
||||
《Head first java 》第二版 推荐阅读真心不错 (适合基础较差的)
|
||||
|
||||
《Java核心技术卷1》推荐阅读真心不错 (适合基础较好的)
|
||||
|
||||
《算法》第四版 (适合想对数据结构的Java实现感兴趣的)
|
||||
|
439
docs/java/collection/Java集合框架常见面试题.md
Normal file
439
docs/java/collection/Java集合框架常见面试题.md
Normal file
@ -0,0 +1,439 @@
|
||||
<!-- TOC -->
|
||||
|
||||
- [剖析面试最常见问题之Java基础知识](#剖析面试最常见问题之java基础知识)
|
||||
- [说说List,Set,Map三者的区别?](#说说listsetmap三者的区别)
|
||||
- [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别)
|
||||
- [**补充内容:RandomAccess接口**](#补充内容randomaccess接口)
|
||||
- [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表)
|
||||
- [ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?](#arraylist-与-vector-区别呢为什么要用arraylist取代vector呢)
|
||||
- [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧)
|
||||
- [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别)
|
||||
- [HashMap 和 HashSet区别](#hashmap-和-hashset区别)
|
||||
- [HashSet如何检查重复](#hashset如何检查重复)
|
||||
- [HashMap的底层实现](#hashmap的底层实现)
|
||||
- [JDK1.8之前](#jdk18之前)
|
||||
- [JDK1.8之后](#jdk18之后)
|
||||
- [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方)
|
||||
- [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题)
|
||||
- [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别)
|
||||
- [ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#concurrenthashmap线程安全的具体实现方式底层具体实现)
|
||||
- [JDK1.7(上面有示意图)](#jdk17上面有示意图)
|
||||
- [JDK1.8 (上面有示意图)](#jdk18-上面有示意图)
|
||||
- [comparable 和 Comparator的区别](#comparable-和-comparator的区别)
|
||||
- [Comparator定制排序](#comparator定制排序)
|
||||
- [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序)
|
||||
- [集合框架底层数据结构总结](#集合框架底层数据结构总结)
|
||||
- [Collection](#collection)
|
||||
- [1. List](#1-list)
|
||||
- [2. Set](#2-set)
|
||||
- [Map](#map)
|
||||
- [如何选用集合?](#如何选用集合)
|
||||
- [集合的选用](#集合的选用)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
# 剖析面试最常见问题之Java基础知识
|
||||
|
||||
## 说说List,Set,Map三者的区别?
|
||||
|
||||
- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
|
||||
- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。
|
||||
- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
|
||||
|
||||
## Arraylist 与 LinkedList 区别?
|
||||
|
||||
- **1. 是否保证线程安全:** ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
|
||||
|
||||
- **2. 底层数据结构:** Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
|
||||
|
||||
- **3. 插入和删除是否受元素位置的影响:** ① **ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。**
|
||||
|
||||
- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。
|
||||
|
||||
- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
|
||||
|
||||
### **补充内容:RandomAccess接口**
|
||||
|
||||
```
|
||||
public interface RandomAccess {
|
||||
}
|
||||
```
|
||||
|
||||
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
|
||||
|
||||
在binarySearch()方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法
|
||||
|
||||
```
|
||||
public static <T>
|
||||
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
|
||||
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
|
||||
return Collections.indexedBinarySearch(list, key);
|
||||
else
|
||||
return Collections.iteratorBinarySearch(list, key);
|
||||
}
|
||||
```
|
||||
|
||||
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!
|
||||
|
||||
**下面再总结一下 list 的遍历方式选择:**
|
||||
|
||||
- 实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,
|
||||
- 未实现RandomAccess接口的ist, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环
|
||||
|
||||
### 补充内容:双向链表和双向循环链表
|
||||
|
||||
**双向链表:** 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
|
||||
|
||||

|
||||
|
||||
**双向循环链表:** 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。
|
||||
|
||||

|
||||
|
||||
## ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?
|
||||
|
||||
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
|
||||
|
||||
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
|
||||
|
||||
## 说一说 ArrayList 的扩容机制吧
|
||||
|
||||
详见笔主的这篇文章:[通过源码一步一步分析ArrayList 扩容机制](ArrayList-Grow.md)
|
||||
|
||||
## HashMap 和 Hashtable 的区别
|
||||
|
||||
1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
|
||||
2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
|
||||
3. **对Null key 和Null value的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
|
||||
4. **初始容量大小和每次扩充容量大小的不同 :** ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
|
||||
5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
|
||||
|
||||
**HasMap 中带有初始容量的构造函数:**
|
||||
|
||||
```java
|
||||
public HashMap(int initialCapacity, float loadFactor) {
|
||||
if (initialCapacity < 0)
|
||||
throw new IllegalArgumentException("Illegal initial capacity: " +
|
||||
initialCapacity);
|
||||
if (initialCapacity > MAXIMUM_CAPACITY)
|
||||
initialCapacity = MAXIMUM_CAPACITY;
|
||||
if (loadFactor <= 0 || Float.isNaN(loadFactor))
|
||||
throw new IllegalArgumentException("Illegal load factor: " +
|
||||
loadFactor);
|
||||
this.loadFactor = loadFactor;
|
||||
this.threshold = tableSizeFor(initialCapacity);
|
||||
}
|
||||
public HashMap(int initialCapacity) {
|
||||
this(initialCapacity, DEFAULT_LOAD_FACTOR);
|
||||
}
|
||||
```
|
||||
|
||||
下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。
|
||||
|
||||
```java
|
||||
/**
|
||||
* Returns a power of two size for the given target capacity.
|
||||
*/
|
||||
static final int tableSizeFor(int cap) {
|
||||
int n = cap - 1;
|
||||
n |= n >>> 1;
|
||||
n |= n >>> 2;
|
||||
n |= n >>> 4;
|
||||
n |= n >>> 8;
|
||||
n |= n >>> 16;
|
||||
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
|
||||
}
|
||||
```
|
||||
|
||||
## HashMap 和 HashSet区别
|
||||
|
||||
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone() `、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
|
||||
|
||||
| HashMap | HashSet |
|
||||
| :------------------------------: | :----------------------------------------------------------: |
|
||||
| 实现了Map接口 | 实现Set接口 |
|
||||
| 存储键值对 | 仅存储对象 |
|
||||
| 调用 `put()`向map中添加元素 | 调用 `add()`方法向Set中添加元素 |
|
||||
| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, |
|
||||
|
||||
## HashSet如何检查重复
|
||||
|
||||
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版)
|
||||
|
||||
**hashCode()与equals()的相关规定:**
|
||||
|
||||
1. 如果两个对象相等,则hashcode一定也是相同的
|
||||
2. 两个对象相等,对两个equals方法返回true
|
||||
3. 两个对象有相同的hashcode值,它们也不一定是相等的
|
||||
4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
|
||||
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
|
||||
|
||||
**==与equals的区别**
|
||||
|
||||
1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
|
||||
2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
|
||||
3. ==指引用是否相同 equals()指的是值是否相同
|
||||
|
||||
## HashMap的底层实现
|
||||
|
||||
### JDK1.8之前
|
||||
|
||||
JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
|
||||
|
||||
**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。**
|
||||
|
||||
**JDK 1.8 HashMap 的 hash 方法源码:**
|
||||
|
||||
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
|
||||
|
||||
```
|
||||
static final int hash(Object key) {
|
||||
int h;
|
||||
// key.hashCode():返回散列值也就是hashcode
|
||||
// ^ :按位异或
|
||||
// >>>:无符号右移,忽略符号位,空位都以0补齐
|
||||
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
|
||||
}
|
||||
```
|
||||
|
||||
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
|
||||
|
||||
```
|
||||
static int hash(int h) {
|
||||
// This function ensures that hashCodes that differ only by
|
||||
// constant multiples at each bit position have a bounded
|
||||
// number of collisions (approximately 8 at default load factor).
|
||||
|
||||
h ^= (h >>> 20) ^ (h >>> 12);
|
||||
return h ^ (h >>> 7) ^ (h >>> 4);
|
||||
}
|
||||
```
|
||||
|
||||
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
|
||||
|
||||
所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
|
||||
|
||||
[](https://camo.githubusercontent.com/eec1c575aa5ff57906dd9c9130ec7a82e212c96a/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f332f32302f313632343064626363333033643837323f773d33343826683d34323726663d706e6726733d3130393931)
|
||||
|
||||
### JDK1.8之后
|
||||
|
||||
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
|
||||
|
||||
[](https://camo.githubusercontent.com/20de7e465cac279842851258ec4d1ec1c4d3d7d1/687474703a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f31382d382d32322f36373233333736342e6a7067)
|
||||
|
||||
> TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
|
||||
|
||||
**推荐阅读:**
|
||||
|
||||
- 《Java 8系列之重新认识HashMap》 :<https://zhuanlan.zhihu.com/p/21673805>
|
||||
|
||||
## HashMap 的长度为什么是2的幂次方
|
||||
|
||||
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
|
||||
|
||||
**这个算法应该如何设计呢?**
|
||||
|
||||
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。**
|
||||
|
||||
## HashMap 多线程操作导致死循环问题
|
||||
|
||||
主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
|
||||
|
||||
详情请查看:<https://coolshell.cn/articles/9606.html>
|
||||
|
||||
## ConcurrentHashMap 和 Hashtable 的区别
|
||||
|
||||
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
|
||||
|
||||
- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
|
||||
- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
|
||||
|
||||
**两者的对比图:**
|
||||
|
||||
图片来源:<http://www.cnblogs.com/chengxiao/p/6842045.html>
|
||||
|
||||
HashTable: [](https://camo.githubusercontent.com/b8e66016373bb109e923205857aeee9689baac9e/687474703a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f31382d382d32322f35303635363638312e6a7067)
|
||||
|
||||
**JDK1.7的ConcurrentHashMap:** [](https://camo.githubusercontent.com/443af05b6be6ed09e50c78a1dca39bf75acb106d/687474703a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f31382d382d32322f33333132303438382e6a7067)**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):** [](https://camo.githubusercontent.com/2d779bf515db75b5bf364c4f23c31268330a865e/687474703a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f31382d382d32322f39373733393232302e6a7067)
|
||||
|
||||
## ConcurrentHashMap线程安全的具体实现方式/底层具体实现
|
||||
|
||||
### JDK1.7(上面有示意图)
|
||||
|
||||
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
|
||||
|
||||
**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。
|
||||
|
||||
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
|
||||
|
||||
```
|
||||
static class Segment<K,V> extends ReentrantLock implements Serializable {
|
||||
}
|
||||
```
|
||||
|
||||
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
|
||||
|
||||
### JDK1.8 (上面有示意图)
|
||||
|
||||
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))
|
||||
|
||||
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
|
||||
|
||||
## comparable 和 Comparator的区别
|
||||
|
||||
- comparable接口实际上是出自java.lang包 它有一个 `compareTo(Object obj)`方法用来排序
|
||||
- comparator接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序
|
||||
|
||||
一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`.
|
||||
|
||||
### Comparator定制排序
|
||||
|
||||
```java
|
||||
ArrayList<Integer> arrayList = new ArrayList<Integer>();
|
||||
arrayList.add(-1);
|
||||
arrayList.add(3);
|
||||
arrayList.add(3);
|
||||
arrayList.add(-5);
|
||||
arrayList.add(7);
|
||||
arrayList.add(4);
|
||||
arrayList.add(-9);
|
||||
arrayList.add(-7);
|
||||
System.out.println("原始数组:");
|
||||
System.out.println(arrayList);
|
||||
// void reverse(List list):反转
|
||||
Collections.reverse(arrayList);
|
||||
System.out.println("Collections.reverse(arrayList):");
|
||||
System.out.println(arrayList);
|
||||
|
||||
// void sort(List list),按自然排序的升序排序
|
||||
Collections.sort(arrayList);
|
||||
System.out.println("Collections.sort(arrayList):");
|
||||
System.out.println(arrayList);
|
||||
// 定制排序的用法
|
||||
Collections.sort(arrayList, new Comparator<Integer>() {
|
||||
|
||||
@Override
|
||||
public int compare(Integer o1, Integer o2) {
|
||||
return o2.compareTo(o1);
|
||||
}
|
||||
});
|
||||
System.out.println("定制排序后:");
|
||||
System.out.println(arrayList);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
原始数组:
|
||||
[-1, 3, 3, -5, 7, 4, -9, -7]
|
||||
Collections.reverse(arrayList):
|
||||
[-7, -9, 4, 7, -5, 3, 3, -1]
|
||||
Collections.sort(arrayList):
|
||||
[-9, -7, -5, -1, 3, 3, 4, 7]
|
||||
定制排序后:
|
||||
[7, 4, 3, 3, -1, -5, -7, -9]
|
||||
```
|
||||
|
||||
### 重写compareTo方法实现按年龄来排序
|
||||
|
||||
```java
|
||||
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
|
||||
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
|
||||
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
|
||||
|
||||
public class Person implements Comparable<Person> {
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO重写compareTo方法实现按年龄来排序
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Person o) {
|
||||
// TODO Auto-generated method stub
|
||||
if (this.age > o.getAge()) {
|
||||
return 1;
|
||||
} else if (this.age < o.getAge()) {
|
||||
return -1;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
|
||||
pdata.put(new Person("张三", 30), "zhangsan");
|
||||
pdata.put(new Person("李四", 20), "lisi");
|
||||
pdata.put(new Person("王五", 10), "wangwu");
|
||||
pdata.put(new Person("小红", 5), "xiaohong");
|
||||
// 得到key的值的同时得到key所对应的值
|
||||
Set<Person> keys = pdata.keySet();
|
||||
for (Person key : keys) {
|
||||
System.out.println(key.getAge() + "-" + key.getName());
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
5-小红
|
||||
10-王五
|
||||
20-李四
|
||||
30-张三
|
||||
```
|
||||
|
||||
## 集合框架底层数据结构总结
|
||||
|
||||
### Collection
|
||||
|
||||
#### 1. List
|
||||
|
||||
- **Arraylist:** Object数组
|
||||
- **Vector:** Object数组
|
||||
- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) 详细可阅读[JDK1.7-LinkedList循环链表优化](https://www.cnblogs.com/xingele0917/p/3696593.html)
|
||||
|
||||
#### 2. Set
|
||||
|
||||
- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素
|
||||
- **LinkedHashSet:** LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
|
||||
- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树。)
|
||||
|
||||
### Map
|
||||
|
||||
- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
|
||||
- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931)
|
||||
- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
|
||||
- **TreeMap:** 红黑树(自平衡的排序二叉树)
|
||||
|
||||
## 如何选用集合?
|
||||
|
||||
## 集合的选用
|
||||
|
||||
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
|
@ -1,273 +0,0 @@
|
||||
|
||||
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
- [Arraylist 与 LinkedList 异同](#arraylist-与-linkedlist-异同)
|
||||
- [补充:数据结构基础之双向链表](#补充:数据结构基础之双向链表)
|
||||
- [ArrayList 与 Vector 区别](#arraylist-与-vector-区别)
|
||||
- [HashMap的底层实现](#hashmap的底层实现)
|
||||
- [JDK1.8之前](#jdk18之前)
|
||||
- [JDK1.8之后](#jdk18之后)
|
||||
- [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别)
|
||||
- [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方)
|
||||
- [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题)
|
||||
- [HashSet 和 HashMap 区别](#hashset-和-hashmap-区别)
|
||||
- [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别)
|
||||
- [ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#concurrenthashmap线程安全的具体实现方式底层具体实现)
|
||||
- [JDK1.7(上面有示意图)](#jdk17(上面有示意图))
|
||||
- [JDK1.8 (上面有示意图)](#jdk18-(上面有示意图))
|
||||
- [集合框架底层数据结构总结](#集合框架底层数据结构总结)
|
||||
- [Collection](#collection)
|
||||
- [1. List](#1-list)
|
||||
- [2. Set](#2-set)
|
||||
- [Map](#map)
|
||||
- [推荐阅读:](#推荐阅读:)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Arraylist 与 LinkedList 异同
|
||||
|
||||
- **1. 是否保证线程安全:** ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
|
||||
- **2. 底层数据结构:** Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别:); 详细可阅读[JDK1.7-LinkedList循环链表优化](https://www.cnblogs.com/xingele0917/p/3696593.html)
|
||||
- **3. 插入和删除是否受元素位置的影响:** ① **ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。**
|
||||
- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。
|
||||
- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
|
||||
- **6.补充内容:RandomAccess接口**
|
||||
|
||||
```java
|
||||
public interface RandomAccess {
|
||||
}
|
||||
```
|
||||
|
||||
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
|
||||
|
||||
在binarySearch()方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法
|
||||
|
||||
```java
|
||||
public static <T>
|
||||
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
|
||||
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
|
||||
return Collections.indexedBinarySearch(list, key);
|
||||
else
|
||||
return Collections.iteratorBinarySearch(list, key);
|
||||
}
|
||||
|
||||
```
|
||||
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!
|
||||
|
||||
|
||||
**下面再总结一下 list 的遍历方式选择:**
|
||||
|
||||
- 实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,
|
||||
- 未实现RandomAccess接口的ist, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环
|
||||
|
||||
|
||||
### 补充:数据结构基础之双向链表
|
||||
|
||||
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如下图所示,同时下图也是LinkedList 底层使用的是双向循环链表数据结构。
|
||||
|
||||

|
||||
|
||||
|
||||
## ArrayList 与 Vector 区别
|
||||
|
||||
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
|
||||
|
||||
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
|
||||
|
||||
|
||||
## HashMap的底层实现
|
||||
|
||||
### JDK1.8之前
|
||||
|
||||
JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
|
||||
|
||||
**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。**
|
||||
|
||||
**JDK 1.8 HashMap 的 hash 方法源码:**
|
||||
|
||||
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
|
||||
|
||||
```java
|
||||
static final int hash(Object key) {
|
||||
int h;
|
||||
// key.hashCode():返回散列值也就是hashcode
|
||||
// ^ :按位异或
|
||||
// >>>:无符号右移,忽略符号位,空位都以0补齐
|
||||
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
|
||||
}
|
||||
```
|
||||
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
|
||||
|
||||
```java
|
||||
static int hash(int h) {
|
||||
// This function ensures that hashCodes that differ only by
|
||||
// constant multiples at each bit position have a bounded
|
||||
// number of collisions (approximately 8 at default load factor).
|
||||
|
||||
h ^= (h >>> 20) ^ (h >>> 12);
|
||||
return h ^ (h >>> 7) ^ (h >>> 4);
|
||||
}
|
||||
```
|
||||
|
||||
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
|
||||
|
||||
所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
### JDK1.8之后
|
||||
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
|
||||
|
||||

|
||||
|
||||
>TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
|
||||
|
||||
**推荐阅读:**
|
||||
|
||||
- 《Java 8系列之重新认识HashMap》 :[https://zhuanlan.zhihu.com/p/21673805](https://zhuanlan.zhihu.com/p/21673805)
|
||||
|
||||
## HashMap 和 Hashtable 的区别
|
||||
|
||||
1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
|
||||
2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
|
||||
3. **对Null key 和Null value的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
|
||||
4. **初始容量大小和每次扩充容量大小的不同 :** ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
|
||||
5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
|
||||
|
||||
**HasMap 中带有初始容量的构造函数:**
|
||||
|
||||
```java
|
||||
public HashMap(int initialCapacity, float loadFactor) {
|
||||
if (initialCapacity < 0)
|
||||
throw new IllegalArgumentException("Illegal initial capacity: " +
|
||||
initialCapacity);
|
||||
if (initialCapacity > MAXIMUM_CAPACITY)
|
||||
initialCapacity = MAXIMUM_CAPACITY;
|
||||
if (loadFactor <= 0 || Float.isNaN(loadFactor))
|
||||
throw new IllegalArgumentException("Illegal load factor: " +
|
||||
loadFactor);
|
||||
this.loadFactor = loadFactor;
|
||||
this.threshold = tableSizeFor(initialCapacity);
|
||||
}
|
||||
public HashMap(int initialCapacity) {
|
||||
this(initialCapacity, DEFAULT_LOAD_FACTOR);
|
||||
}
|
||||
```
|
||||
|
||||
下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。
|
||||
|
||||
```java
|
||||
/**
|
||||
* Returns a power of two size for the given target capacity.
|
||||
*/
|
||||
static final int tableSizeFor(int cap) {
|
||||
int n = cap - 1;
|
||||
n |= n >>> 1;
|
||||
n |= n >>> 2;
|
||||
n |= n >>> 4;
|
||||
n |= n >>> 8;
|
||||
n |= n >>> 16;
|
||||
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
|
||||
}
|
||||
```
|
||||
|
||||
## HashMap 的长度为什么是2的幂次方
|
||||
|
||||
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash` ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
|
||||
|
||||
**这个算法应该如何设计呢?**
|
||||
|
||||
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。**
|
||||
|
||||
## HashMap 多线程操作导致死循环问题
|
||||
|
||||
主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
|
||||
|
||||
详情请查看:<https://coolshell.cn/articles/9606.html>
|
||||
|
||||
|
||||
## HashSet 和 HashMap 区别
|
||||
|
||||
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。)
|
||||
|
||||

|
||||
|
||||
## ConcurrentHashMap 和 Hashtable 的区别
|
||||
|
||||
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
|
||||
|
||||
- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
|
||||
- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
|
||||
|
||||
**两者的对比图:**
|
||||
|
||||
图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html
|
||||
|
||||
HashTable:
|
||||

|
||||
|
||||
JDK1.7的ConcurrentHashMap:
|
||||

|
||||
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点
|
||||
Node: 链表节点):
|
||||

|
||||
|
||||
## ConcurrentHashMap线程安全的具体实现方式/底层具体实现
|
||||
|
||||
### JDK1.7(上面有示意图)
|
||||
|
||||
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
|
||||
|
||||
**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。
|
||||
|
||||
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
|
||||
|
||||
```java
|
||||
static class Segment<K,V> extends ReentrantLock implements Serializable {
|
||||
}
|
||||
```
|
||||
|
||||
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
|
||||
|
||||
### JDK1.8 (上面有示意图)
|
||||
|
||||
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))
|
||||
|
||||
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
|
||||
|
||||
|
||||
|
||||
## 集合框架底层数据结构总结
|
||||
### Collection
|
||||
|
||||
#### 1. List
|
||||
- **Arraylist:** Object数组
|
||||
- **Vector:** Object数组
|
||||
- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
|
||||
详细可阅读[JDK1.7-LinkedList循环链表优化](https://www.cnblogs.com/xingele0917/p/3696593.html)
|
||||
|
||||
#### 2. Set
|
||||
- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素
|
||||
- **LinkedHashSet:** LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
|
||||
- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树。)
|
||||
|
||||
### Map
|
||||
- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
|
||||
- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931)
|
||||
- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
|
||||
- **TreeMap:** 红黑树(自平衡的排序二叉树)
|
||||
|
||||
|
||||
|
||||
|
||||
### 推荐阅读:
|
||||
|
||||
- [jdk1.8中ConcurrentHashMap的实现原理](https://blog.csdn.net/fjse51/article/details/55260493)
|
||||
- [HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)
|
||||
- [HASHMAP、HASHTABLE、CONCURRENTHASHMAP的原理与区别](http://www.yuanrengu.com/index.php/2017-01-17.html)
|
||||
- [ConcurrentHashMap实现原理及源码分析](https://www.cnblogs.com/chengxiao/p/6842045.html)
|
||||
- [java-并发-ConcurrentHashMap高并发机制-jdk1.8](https://blog.csdn.net/jianghuxiaojin/article/details/52006118#commentBox)
|
Loading…
x
Reference in New Issue
Block a user