Hash函数
比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。 这个函数可以简单描述为:存储位置 = f(关键字)
hash冲突:如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞
哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法
而HashMap即是采用了链地址法,也就是数组+链表的方式。
在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组
hash函数的设计 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。
右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
-
一定要尽可能降低hash碰撞,越分散越好;
-
算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;
https://blog.csdn.net/weixin_41759020/article/details/105156685
https://blog.csdn.net/woshimaxiao1/article/details/83661464
1.HashMap的红黑树
HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式。(总元素大于64 不然会进行扩容操作)
为什么要引入红黑树呢,JDK1.7 1.8新特性 1.7最坏情况下 链表的查询速度太慢
红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。 还有选择6和8的原因是: 中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。 链表的由来:Hash碰撞:不同的元素通过hash算法可能会得到相同的hash值,如果都放同一个桶里,后面放进去的就会覆盖前面放的,所以为了解决hash碰撞时元素被覆盖的问题,就有了在桶里放链表。
②红黑树的由来:假设现在HashMap集合中大多数的元素都放到了同一个桶里(由hash值计算而得的桶的位置相同),那么这些元素就在这个桶后面连成了链表。现在需要查询某个元素,那么此时的查询效率就很慢了,它是在做链表查询( O(N) 的查询效率)。为了解决这个问题,就引入了红黑树( log(n) 的查询效率):当链表到达一定长度时就在链表的后面创建红黑树。
③其实,“尽量避免hash 冲突,让元素较为均匀的放置到每个桶”才是查询效率最高的( O(1) 的查询效率
当链表已经有8个节点了,此时再新链上第9个节点,在成功添加了这个新节点之后,立马做链表转红黑树”。
数据插入原理
-
判断数组是否为空,为空进行初始化;
-
不为空,计算 k 的 hash 值,通过
(n - 1) & hash计算应当存放在数组中的下标 index; -
查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
-
存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
-
如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
-
如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
-
插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。
JDK1.8 优化成直接把链表拆成高位和低位两条,通过位运算来决定放在原索引处或者原索引加原数组长度的偏移量处。
红黑树的拆分和链表的逻辑基本一致,不同的地方在于,重新映射后,会将红黑树拆分成两条链表,根据链表的长度,判断需不需要把链表重新进行树化。(红黑树会先成为链表,再进行树化)
2.红黑树的定义
1.自平衡的二叉查找树,根节点是黑色。 3.每个叶子节点都是黑色的空节点(NIL节点)。 4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
-
每个结点是黑色或者红色。
-
根结点是黑色。
-
每个叶子结点(NIL)是黑色。 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]
-
如果一个结点是红色的,则它的子结点必须是黑色的。
-
每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
-
基本操作 旋转和变色 都是为了维持红黑树的五条特性!
-
红黑树是介于AVL(平衡二叉树)和二叉查找树的中间,AVL的操作过于复杂,花费时间多
3.JDK8的新特性!
jdk1.8新特性知识点:
在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化 原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode 方法,计算出哈希码值,经过哈希算法算成数组的索引值,如果对应的索引处没有元素,直接存放,如果有对象在,那么比较它们的equals方法比较内容 如果内容一样,后一个value会将前一个value的值覆盖,如果不一样,在1.7的时候,后加的放在前面,形成一个链表,形成了碰撞,在某些情况下如果链表 无限下去,那么效率极低,碰撞是避免不了的 加载因子:0.75,数组扩容,达到总容量的75%,就进行扩容,但是无法避免碰撞的情况发生 在1.8之后,在数组+链表+红黑树来实现hashmap,当碰撞的元素个数大于8时 & 总容量大于64,会有红黑树的引入 除了添加之后,效率都比链表高,1.8之后链表新进元素加到末尾
jdk1.8中在计算新位置的时候并没有跟1.7中一样重新进行hash运算,而是用了原位置+原数组长度这样一种很巧妙的方式,而这个结果与hash运算得到的结果是一致的,只是会更块。\
ConcurrentHashMap (锁分段机制),concurrentLevel,jdk1.8采用CAS算法(无锁算法,不再使用锁分段),数组+链表中也引入了红黑树的使用
-
Lambda表达式 *
-
函数式接口 ***
Supplier函数式接口
Consumer函数式接口
Function函数式接口
Predicate函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
-
*方法引用和构造器调用
-
Stream API
-
接口中的默认方法和静态方法
在JDK1.8中很多接口会新增方法,为了保证1.8向下兼容,1.7版本中的接口实现类不用每个都重新实现新添加的接口方法,引入了default默认实现,static的用法是直接用接口名去调方法即可。当一个类继承父类又实现接口时,若后两者方法名相同,则优先继承父类中的同名方法,即“类优先”,如果实现两个同名方法的接口,则要求实现类必须手动声明默认实现哪个接口中的方法。
静态方法,只能通过接口名调用,不可以通过实现类的类名或者实现类的对象调用。default方法,只能通过接口实现类的对象来调用。
-
新时间日期API
4.查找法
二分查找 插值查找 斐波拉契查找
5.Java >>>无符号右移
int a = Integer.MAX_VALUE;
int b = 1000;
System.out.println((a+b)/2);
System.out.println((a+b)>>1);
System.out.println((a+b)>>>1);
result:
-1073741324
-1073741325
1073742323
6.重写equals 必须重写hashcode
我们知道判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。
比较一个集合中是否存在某元素
所以说hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。
equals原则:
-
自反性:x.equals(x)必须返回true。
-
对称性:x.equals(y)与y.equals(x)的返回值必须相等。
-
传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。
-
一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变。
-
非null:x不是null,y为null,则x.equals(y)必须为false。
-
写出一个好的equals方法
1)显示参数命名为otherObject,稍后需要将他转化成一个名为other的变量。
2)检测this和otherObject是否引用同一个对象:if(otherObject == this) return true;
3)检测otherObject是否为null,如果为null,则返回fals。
4)比较this和otherObject是否属于同一个类,如果equals的语义在每个子类中都有改变,那么就使用getClass()检测;如果所有的子类都有相同的语义,就用instanceof检测。
5)将otherObject转化成相应类型的变量:ClassName other = (ClassName)otherObject;
6)开始对所有需要比对的域进行比对,使用==比较基本类型,使用equals比对对象,如果所有的域都匹配,那么返回true,否则,返回false。
Collection集合
https://www.cnblogs.com/pam-sh/p/12663952.html#_label0
Set:
HashSet: 采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
TreeSet【还未看】: 采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
Map:
HashMap: 与 HashSet 对应,也是无序的,O(1)。
LinkedHashMap: 这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。
LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序
TreeMap: 是有序的,本质是用二叉搜索树来实现的。
TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较
List:
ArrayList LInkedList vector stack
一、ArrayList,优势在于随机访问元素,但是在List的中间插入和移除元素时较慢;LinkedList,通过代价较低的在List中间进行的插入和删除操作,提高了优化的顺序访问,而在随机访问方面相对比较慢。
二、注意扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组。可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。
Arrays.asList 只能修改修改 不能add remove 因为这个返回的是它自己定义的内部类!相当于数组的引用,会进行同步修改
(1)该方法适用于对象型数据的数组(String、Integer...)
(2)该方法不建议使用于基本数据类型的数组(byte,short,int,long,float,double,boolean)
(3)该方法将数组与List列表链接起来:当更新其一个时,另一个自动更新
(4)不支持add()、remove()、clear()等方法
LinkedList 是一个继承于AbstractSequentialList的双向链表。实现了List、Deque、Cloneable、Java.io.serializable接口,它也可以被当作堆栈、队列或双端队列进行操作,。 LinkedList 实现 List 接口,能对它进行队列操作。 LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。 LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。 LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
由上面可以得到:LinkedList 是非线程安全的,集合中的元素允许为空,保存的元素为有序的,实现了List接口,则允许集合中的元素是可以重复的。
迭代器遍历。
LinkedList<Integer> list = new LinkedList<>();
list.add(1);list.add(2);
for(Iterator iter = list.iterator(); iter.hasNext();)
{
System.out.println(iter.next());
}
什么是fail-fast,什么是fail-safe,有什么区别吗? fail-fast字面意思,快速失败,也就是当集合类遇到数据问题时时立刻抛出异常,常规的集合类都是如此,在多线程同时操作时会立刻抛出concurrentmodificationexception
fail-safe当集合类在多线程环境下处理的时候,遇到问题时,不会抛出异常,而是继续执行下去。
fail-safe机制会在复制原集合的一份数据出来,然后在复制的那份数据遍历。
因此,虽然fail-safe不会抛出异常,但存在以下缺点:
-
复制时需要额外的空间和时间上的开销。
-
不能保证遍历的是最新内容。
HashMap与HashTable区别
HashMap 与 Hashtable 的关系,就像 ArrayList 与 Vector,以及 StringBuilder 与 StringBuffer。
Hashtable 是早期 JDK 提供的接口,HashMap 是新版的;它们之间最显著的区别,就是 Hashtable 是线程安全的,HashMap 并非线程安全。
这是因为 Java 5.0 之后允许数据结构不考虑线程安全的问题,因为实际工作中我们发现没有必要在数据结构的层面上上锁,加锁和放锁在系统中是有开销的,内部锁有时候会成为程序的瓶颈。
所以 HashMap, ArrayList, StringBuilder 不再考虑线程安全的问题,性能提升了很多,但安全性能却降低了
另外一个区别就是:HashMap 允许 key 中有 null 值,Hashtable 是不允许的。这样的好处就是可以给一个默认值。
ConcurrentHashMap
HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大
ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。
ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。
每一个节点锁住自己所在的点,而不干涉其他的部分。
https://blog.csdn.net/shlgyzl/article/details/102811098
ConcurrentHashMap 是线程安全的HashMap集合
-
相对于hashMap来说,ConcurrentHashMap保证线程的安全性,当HashMap暴露在多个线程中的时候,可能会导致HashMap集合内部的数据发生错误的改变,导致很多链接丢失或者数据不对。
-
相对于hashTable来说,虽然hashTbale也是线程安全的,但是性能方面,HashTable远不如ConcurrentHashMap,HashTable要保持线程安全是将类的字节码加锁,这样性能会很差,而ConcurrentHashMap由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
2、当ConcurrentHashMap上有一个线程在做写入操作,如果另外一个线程是去其他的segment里读数据的话,此时是允许操作的,但是如果此时这个线程是去相同的segment中做读操作的话,就不允许此次读取,只有当那个写入操作的线程完成后,才能执行读操作
3、ConcurrentHashMap有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。
7.arraycopy native
直接从内存中拷贝字节过来 比数组移动要快得多
那就是memcpy(void *destin, void *source, unsigned n),函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中
8.异常种类
Java里面异常分为两大类:checkedexception(检查异常)和unchecked exception(未检查异常),对于未检查异常也叫RuntimeException(运行时异常),对于运行时异常,java编译器不要求你一定要把它捕获或者一定要继续抛出,但是对checkedexception(检查异常)要求你必须要在方法里面或者捕获或者继续抛出。
9.Java多态
-
子类对象可以当作父类对象使用(子类的引用可以赋值给父类变量),
-
父类的引用如果是:
-
子类的实例, 可以通过强转赋值给子类引用
-
父类的实例, 不能被子类引用
其实细想new出的对象之间本身是不能转换的, 我们赋值的是类的引用,
总的来说就是父类变量可以引用父类实例, 也可以引用子类实例,
子类变量只能引用子类实例
构造函数不可以被static final 修辞 有参构造函数的参数可以是任何对象 包括它自己
10.Shell
echo ${#abc} 查看字符串长度
echo ${#argv[*]} 查看数组长度