Queue
1.1 特点
1, Queue接口是Collection一个子接口, 描述的数据结构是队列
2, Queue存储元素是有序
3, Queue 允许存储重复元素
4, Queue 不允许存储null元素( LinkedList除外)
Queue的删除操作, 是以返回一个null值作为 标记, 标记没有元素存储了
1.2 API
1.2.1 注意
1, 如果Queue存储元素的时候, 没有位置/空间 可以存储了, 如果使用add方法抛出异常, 如果使用offer返回布尔值 假
2, 如果Queue存储元素,没有存储任何内容/里面是空的 如果使用remove方法抛出异常, 如果使用poll 会返回null
3, 如果Queue存储元素,没有存储任何内容/里面是空的 如果使用element方法抛出异常, 如果使用peek会返回null
2 Deque
2.1 特点
1, Deque接口是Queue接口的一个子接口
2, Deque描述的数据结构是: 双端队列, 普通队列, 栈
3, 有序
4, 允许重复元素
5, 不允许存储null ( LinkedList除外)
2.2 API
api很多,自己看文档
3 ArrayQueue
1, ArrayDeque 是Deque接口的一个具体子实现
2, ArrayDeque描述的数据结构是: 普通队列, 双端队列, 栈
3, 底层是个数组: 循环数组
4, 默认的初始容量16 , 扩容机制2倍 --> 底层数组长度是2的幂值
5, 有序
6, 允许存储重复元素
7, 不允许存储null
8, 线程不安全
9, 在给定长度的构造方法里, 如果我们给定一个长度是小于8的话底层数组创建一个长度为8的数组, 如果给定的值大于等于8 , 那么这个被创建的底层数组的长度是大于给定值的最小的2的幂值 --> 底层数组长度是2的幂值 (比较方便于用位运算取模, 计算速度更快)
注意:如果给了16,那创建的数组长度是32
循环数组的好处, 不需要在每次删除之后, 移动n-1个元素, 仅需要移动头标记
3.2 构造方法
4 BlockingQueue: 阻塞队列
什么是阻塞队列:
如果队列的大小是一定的, 当队列存满的时候, 添加线程阻塞,
当队列为空的时候, 删除线程阻塞
4.1 特点
阻塞队列
有一些阻塞方法
阻塞队列, 要求有大小
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.add("zs");
queue.add("ls");
queue.add("wu");
// 容量只有3,这时使用put添加就会导致阻塞
queue.put("zl");
}
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.add("zs");
queue.add("ls");
queue.add("wu");
Mythread mythread = new Mythread(queue);
mythread.start(); // 这一步要放在前面,不然你阻塞了,后面的代码运行不到
// 容量只有3,这时使用put添加就会导致阻塞
queue.put("zl");
}
}
class Mythread extends Thread{
ArrayBlockingQueue<String> queue;
public Mythread(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.remove();
}
}
阻塞队列:如果队列的大小是一定的,当队列存满的的时候,添加线程阻塞,当队列为空的时候,删除线程阻塞
阻塞队列一般用于生产者消费者模式,只能这么执行多任务,如果要进来就必须等我完成任务,比如分布式服务器之间通信,一个服务器可以一直发,但接收的服务器维护阻塞队列来接收,避免数据过大导致服务器瘫痪
阻塞队列构造方法要求有大小,不然自己扩容就失去阻塞的意义了
5 Map
Java集合类分为两套集合体系, 一个Collection 一个是Map
Collection存储的是单个元素
Map 存储的是Key-Value数据/ 键值对
在网页上下载东西的过程,其实就是通过key获得一个value
5.1 Map的特点
1, Map集合体系的顶级接口
2, 存储的是key-value数据 (我们在研究Map下的集合类的时候, 关注的核心点在key上)
3, 有的子实现key是有序的, 有的子实现key是无序的,
4, key是不允许重复的(重复的定义不一样, hashMap不允许存储哈希值相同的)
5, 有的子实现key允许存储null, 有的子实现key是不允许存储null
5.2 API
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<>();
map.put("zs", "13");
map.put("ls", "14");
map.put("wu", "15");
//System.out.println(map);
/*
// 是视图方法,如果改动set会反映到原map中
Collection set = map.keySet();
System.out.println(set);
set.remove("ls");
*/
/*
// value()也是视图方法,会对原来的map产生影响
Collection values = map.values();
values.remove("13");
System.out.println(values);
System.out.println(map);
*/
// 视图 返回成对的key-value数据
// 关于Map.Entry<K,V>,是Map接口的内部接口:
Collection<Map.Entry<String, String>> collection = map.entrySet();
for (Map.Entry<String, String> entry : collection) {
//System.out.println(entry);
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
6 HashMap: 重点, 必问
补充一下:
无符号右移:>>>
右移:>>
6.1 HashMap的结构
数组 + 链表 + 红黑树
常问点:HashMap和HashTable的区别,到时候我再补充
6.2 特点
HashMap的时间复杂度可以达到O(1)
- 1, HashMap是Map接口一个具体实现
- 2, HashMap的底层结构是 数组 + 链表 + 红黑树 (红黑树是jdk1.8版本新加的结构)
- 3, 数组的默认初始长度 16(第一添加元素产生长度为16,如果只定义一个HashMap不添加元素,底层数组不初始化,开始添加元素才初始化),数组的扩容机制(扩为原来的2倍) --> 数组长度必定2的幂值 --> 位运算
- 4, HashMap存储key是无序的,而且这个顺序会动态变化
- 5, HashMap不允许存储重复的key值 , 对于HashMap 的key的重复定义是什么? :
- 6, HashMap允许存户 null 键,对应的hash值为0
- 7, HashMap线程不安全
- 8, HashMap的默认加载因子是0.75(我们也可以在构造方法中传入加载因子, 建议在0.5-1之间)
什么是加载因子? 饱和度
数组存储元素(key-value数据)到一定容量的时候, 就会扩容数组, 以避免链表和红黑树过多/过长导致存储效率降低
扩容的阈值 = 加载因子 * 数组长度, 不是说在数组中存了阈值个位置就扩容,而是包含所有链表红黑树元素超过阈值就扩容,用空间换时间
假如:
数组长度 16
默认加载因子 0.75
阈值: 12(第13个往里存的时候就会扩容)注意: HashMap是怎么存储key-value数据的
1, HashMap在存储一份key-value数据的时候, 先把key取出来, 计算
2, key 经过计算, 得到一个 int类型 hash值
3, 把这个经过计算的hash值和HashMap的数组长度取模, 得到下标
4, 如果这个下标位置, 没有元素, 把这份key-value数据, 存到这个下标位置(存储的是一个结点类型, 这个结点类型里面包含key, value …)
5, 如果这个下标位置, 已经存储了结点, 判断key值和这个下标位置的这些已经存在结点中的key值是否重复, 如果重复不添加, 如果不重复, 添加到这个位置(链表的尾部)
6, 如果链表过长, 把链表转化为红黑树
注意: 从单纯的数学领域(当下), hash
Hash算法
MD4
MD5: 王小云
SHA1:
SHA2
····
- 9, HashMap中 hash值的计算:
Hash = (h = key.hashCode()) ^ (h >>> 16);
(key.hashCode()调用的是Object自带的hashCode方法)
从主观上, 我们希望每一个key都可以散列到数组的不同位置, 实际上是没有办法保证的
实际上在我们hash值取下标(和数组长度取模)的过程中, 只有hash值的低位才会有用, 也就意味着,我们最终在hashmap中数组上所散列的位置依赖于 hash的低位
所以h >>> 16位的原因是因为, 希望高位数也能参加到取模运算中来, 已达到充分散列的效果(希望)
1010101010110
0000000101010 - 10, 如果我们在构造方法里给定一个长度, 那么HashMap会创建一个底层数组长度大于等于给定值的最小的2的幂值的一个数组
10 -> 16
16 -> 16
20 -> 32
(ArrayDeque是产生大于, 而非大于等于,给16会创建32) - 11, 我们存储到数组位置的key-value数据, 实际上数组存储的是一个Node类型的结点, 这个Node类型包含四个参数: hash, key, value, next
- 12, 在HashMap中判断key重复的判断条件:
判断两个要比较的key-value数据的key的hash值是否一样, 如果hash值一样, 会进一步判断, key值是否直接相等或者相equals
// 即使很幸运通过两个对象的hash值算出下标是一样的,也必须先判断hash值,再看key值是否相同
// 所以没有重写hashCode,只要对象地址值不一样,就会被看成两个元素
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
- 13, 也就意味着, 如果我们用一个普通的java对象来充当key的时候, 我们希望比较的是这个对象里面的存储的内容, 那么要根据这个对象里面的参数重写hashCode 和 equals方法(仅重写equals是不行的)
如果两个key的hashCode一样 并且 两个对象相equals, 那么对于hashmap来说这两个key就是重复的
Eg: 如下: 如果我们希望这两个User对象被hashmap看做是同一个key值的话, 需要重写User类型的hashCode 和 equals方法, (按照 name和age重写)
public class Demo {
public static void main(String[] args) {
HashMap<User, String> map = new HashMap<>();
// 二者的地址不相等,但内容相等
map.put(new User("zs", 18), "20");
map.put(new User("zs", 18), "20");
System.out.println(map); // 此时map中只有一对元素
}
}
class User{
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 14, 如果要存储的key-value数据, 这个key值已经在HashMap上存在, 那么我们会用新的这份key-value数据的value来覆盖已经存储到HashMap上的(已经存储的重复的)key-value的value值 (key值不会覆盖)
map.put(new User("zs", 18), "20");
map.put(new User("zs", 18), "90");
System.out.println(map); // {User{name='zs', age=18}=90}
- 15, 在HashMap存储中, 如果多个key-value数据散列到同一个下标位置, key值也不重复, 那么这个位置就会构建出一个单链表, 如果单链表过长, 那么这个链表要转化为红黑树.
过长意味着: 链表长度超过8达到9的时候
(算上新添加的结点) - 16, 在HashMap中, 某一个数组位置链表长度超过8达到9的时候, 一定会转化为红黑树吗?
不一定,
// 有两个选择
// 如果底层数组长度是小于64的, 那么我们会对底层数组扩容
// 如果底层数组长度是大于等于64的, 会对链表进行转化转化成红黑树
扩容,
key-value的hash是不变的, hash来源于key的hashCode
数组长度变了
取模结果变化 --> 存储位置有可能变化
- 17, 如果一个key-value数据, 原本在下标为x的位置, 当发生扩容的时候, 他只可能重新散列到两个位置: (原因是因为数组长度是2的幂值)
原本的x位置
X + 旧长度的位置
Eg: 数组长度16
原本 3
扩容 : 数组长度 32
这个元素: 3, 19
–
- 18, 红黑树什么时候转化会链表?
有两种情况, 会导致红黑树转化会链表
1, 删除元素的时候(删除的是红黑树上的元素),
2, 在扩容的时候, 有可能会导致红黑树转化回链表(扩容会把一个元素位置的数据拆成两个位置)
删除的时候判断逻辑(红黑树 --> 链表)
根节点, 根节点的左右子结点, 根节点的左节点的左节点, 这个四个结点只要有一个是null, 那么就会在删除操作的时候, 把红黑树转化为链表
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
扩容的时候判断逻辑(红黑树 --> 链表)
由于扩容的时候, 红黑树的结点会散列成两部分(旧位置, 旧位置+旧长度), 这两部分经过散列之后, 任一个位置的key-value数据是小于等于6的话, 就要由红黑树转化回链表
- 19, 存储到红黑树(特殊的二叉搜索树) 是要能比较大小, 在hashmap的红黑树中,怎么比较key值的大小那, 就是通过key值的hash比较大小(int)
–
- 20, 已经存储到HashMap中的key-value数据, 不要通过引用来修改key值
(可以先删除, 再重新添加)
6.3 构造方法
6.4 API
一点补充
前后端不分离:服务器要把数据和前端合并,服务器压力大
前后端分离:浏览器先去从前端服务器获取前端框架,前端代码会告诉你数据的位置在哪里,然后再从别的服务器那数据将前端填充,用户来完成前端和后端数据的合并,但是这样浏览器的消耗会相对大一点
key-value数据有自我描述性,在网络传输中适用