函数式接口
一般只包含一个方法声明的接口。(特别的这种接口中可以含有完整的静态方法,和default方法)
新增的四个基本类型函数式接口
- 函数型 function<S, T> 需要两个泛型分别为入参和返回值
- 断言型 predicate 会得到一个Boolean型 的结果,
- 消费型 consumer 只有入参没有返回值
- 供给型 supplier 只有返回值没有入参
函数式接口,在使用时,都将可以被简化为lambda表达式 一般形式 ()->{}
流式编程
例如
List<User> list = new ArrayList<>();
list.stream()
.filter(boolean)
.map()
.limit(2)
.sort(Compareable)
集合类,调用stream方法,变成流运算模式,链式编程来对集合中的元素进行各种运算
HashMap
HashMap本质上,是一个node节点的数组,每存储一个k-v就会占用一个node节点。
它在每次存储时都会为每一个节点计算index(数组下标)值。然后存储到计算出的指定位置。
计算方式
n = tab.length;
index = (n - 1) & HashCode(Key);
由于哈希算法很诡异,传入不同的key可能会算出相同的结果(即hash冲突)。所以往HashMap中put值的时候,会有可能发生 正要被存储的 元素 计算出来的节点位置上,已经有元素了! 这时候会去比较 节点上原有的key和 即将被存储的key是否相等,如果相等则直接覆盖掉原节点中的k-v(这个方法有返回值,会把原节点的value值返回),如果不相等则会使用尾插法在这个节点向下形成单链表。
源码写那么复杂,都是在考虑较极端的场景,一直发生hash冲突,在同一个节点位置造成单链表的长度达到8(源码指定的)时,则将单链表转化为红黑树(一种特例的二叉搜索树)
扩容时 会重新计算所有节点的index 为了使元素分布更均匀。
1.8以前,冲突形成单链表时用尾插法,扩容用头插法,这将有可能造成单链表的特例,环! 出现了环时,再获取值就会死循环。
1.8以后均使用尾插法,则链表的指向性保持不变避免了这个问题
初始化的大小为何指定为2的n次方?因为index的计算公式
2的n次方减1 的二进制结果就是 11…1
如 7 为 111
15 为 1111
这时候按位与 & 运算 效率将极其高
因为只与二进制hash值的后几位有关。
加载因子0.75 是综合考虑了 以下两点
- 降低hash冲突的概率性(泊松分布, 概率为百万分之一)
- 提高空间利用率(HashMap存储的元素个数达到 初始长度*加载因子时,下一个元素put就会进行扩容了)
未完待续。。。