Skip to content

Latest commit

 

History

History
1052 lines (743 loc) · 49.3 KB

File metadata and controls

1052 lines (743 loc) · 49.3 KB

Java

说说你对oop的理解,结合项目说一下

oop面向对象编程
oop的特点是封装继承多态
oop优点让程序易维护易复用易扩展

结合项目来说的话以mvp框架为例吧
在mvp框架中在p层会与v层m层建立双向连接让v层与m层彻底断开联系p层的类我们一般会设计一个 baseP 这样的父类然后各个业务模块的p层子类继承自这个 baseP体现了oop继承特性baseP设计的时候我们是需要持有v层接口的实现为了方便扩展一般我们定义泛型 T extends baseView 接口v层base接口)。当p层的子类需要到某个具体v层接口的时候在给这个泛型T传入实际接口类体现了oop多态特效p层的这个类我们会封装一些方法暴露出去当v层需要某种能力的时候就可以调用这个p层的对象的方法让这个p层对象帮我们实现体现了oop封装特性

重写和重载区别

1.重写(Override):父类与子类之间的多态表现
其实就是在子类中把父类本身有的方法重新写一遍
例如a extends A
A有个方法 set(){ "AAA" }
a重写了A中的set方法set(){ "aaa" }

2.重载(Overload):一个类中多态表现
多个具有不同的参数个数或者类型的同名函数返回值类型可随意不能以返回类型作为重载函数的区分标准),调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性例如AView(Context c)
AView(Context cAttributeSet att)
AView(Context cAttributeSet att, int style)

https://blog.csdn.net/qunqunstyle99/article/details/81007712

并发编程

(并发的更多问题:Java并发编程 I - 并发问题的源头

  1. 为什么会出现并发问题?

  2. volatile能解决并发中的什么问题?

  3. ThreadLocal怎么保证线程唯一?

  4. sleep与wait的区别。+2

  5. synchronize修饰方法和修饰静态方法的区别。+2

  6. 写出一个死锁的例子。

  7. List的加锁要如何加。

  8. 多线程加锁的几种方法。+2

  9. 开启线程的三种方式?

  10. 线程和进程的区别?+2

  11. run()和start()方法区别

  12. 两个进程同时要求写或者读,能不能实现?如何防止进程的同步?

JVM

运行时数据区域java虚拟机在执行程序时会把它所管理的内存划分成若干个不同的数据区域运行时数据区域包括方法区运行时常量池java堆虚拟机栈
本地方法栈
程序计数器

线程共享方法区
1方法区存储类信息常量静态变量即时编译期编译后的代码

2存储对象实例数组堆大小设置最大上限:-Xmx初始内存大小分配:-Xms线程私有程序计数器虚拟机栈本地方法栈

1程序计数器指向当前线程正在执行的字节码指令地址
为什么需要程序计数器线程切换的时候需要记录当前线程执行的位置确保多线程情况下程序的政策执行2虚拟机栈存储当前线程运行方法所需要的数据指令返回地址虚拟机栈里边存储的是栈帧栈帧里边内容包括局部变量表操作数栈返回地址动态连接局部变量表LocalVariableTable):第一个位置存储的是this类对象本身),之后存储的是方法的传参操作数栈Code):一大堆指令程序就是依靠一条条指令执行实现的返回地址记录返回地址方法正常执行完后告诉虚拟机需要返回到哪里比如a方法里边执行了b方法b的栈帧的返回地址就会记录a方法的地址当b方法执行完了就会回到a方法对应的地方。(如果执行中发生异常是去到异常处理器表动态连接用来实现多态的在类加载的时候记录当前具体是哪个实例的方法比如BC类继承自ABC分别实现了A中的aa方法BC实现的aa方法是有区别的aa方法中的动态连接就是为了记录当前实例到底是B还是C注意虚拟机栈是有大小的大小设置:-Xss),如果递归很深可能会导致虚拟机栈溢出StackOverflowError)。

3本地方法栈存储当前线程所使用的native方法的信息当线程里边有调用native方法的时候并不会在虚拟机栈中创建栈帧而是在本地方法栈中创建虚拟机中的对象
虚拟机遇到一条new指令时1检查加载
先执行相应的类加载过程2分配内存
	指针碰撞
		接下来虚拟机将为新生对象分配内存为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来如果Java堆中内存是绝对规整的所有用过的内存都放在一边空闲的内存放在另一边中间放着一个指针作为分界点的指示器那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离这种分配方式称为指针碰撞”。
	空闲列表
		如果Java堆中的内存并不是规整的已使用的内存和空闲的内存相互交错那就没有办法简单地进行指针碰撞了虚拟机就必须维护一个列表记录上哪些内存块是可用的在分配的时候从列表中找到一块足够大的空间划分给对象实例并更新列表上的记录这种分配方式称为空闲列表”。
		选择哪种分配方式由Java堆是否规整决定而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定并发安全
		除如何划分可用空间之外还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为即使是仅仅修改一个指针所指向的位置在并发情况下也并不是线程安全的可能出现正在给对象A分配内存指针还没来得及修改对象B又同时使用了原来的指针来分配内存的情况CAS机制
		解决这个问题有两种方案一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性分配缓冲
		另一种是把内存分配的动作按照线程划分在不同的空间之中进行即每个线程在Java堆中预先分配一小块私有内存也就是本地线程分配缓冲Thread Local Allocation Buffer,TLAB),如果设置了虚拟机参数 -XX:UseTLAB在线程初始化时同时也会申请一块指定大小的内存只给当前线程使用这样每个线程都单独拥有一个Buffer如果需要分配内存就在自己的Buffer上分配这样就不存在竞争的情况可以大大提升分配效率当Buffer容量不够的时候再重新从Eden区域申请一块继续使用TLAB的目的是在为新对象分配内存空间时让每个Java应用线程能在使用自己专属的分配指针来分配空间减少同步开销TLAB只是让每个线程有私有的分配指针但底下存对象的内存空间还是给所有线程访问的只是其它线程无法在这个区域分配而已当一个TLAB用满分配指针top撞上分配极限end了),就新申请一个TLAB3内存空间初始化注意不是构造方法内存分配完成后虚拟机需要将分配到的内存空间都初始化为零值(如int值为0boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用程序能访问到这些字段的数据类型所对应的零值4设置
接下来虚拟机要对对象进行必要的设置例如这个对象是哪个类的实例如何才能找到类的元数据信息对象的哈希码对象的GC分代年龄等信息这些信息存放在对象的对象头之中5对象初始化
在上面工作都完成之后从虚拟机的视角来看一个新的对象已经产生了但从Java程序的视角来看对象创建才刚刚开始所有的字段都还为零值所以一般来说执行new指令之后会接着把对象按照程序员的意愿进行初始化这样一个真正可用的对象才算完全产生出来虚拟机优化逃逸分析
几乎所有的对象都是在堆中分配的但是也有例外逃逸分析可以在栈上创建对象
基本思想对于线程私有的对象将它打散分配在栈上而不分配在堆上好处是对象跟着方法调用自行销毁不需要进行垃圾回收可以提高性能逃逸分析的目的是判断对象的作用域是否会逃逸出方法体注意任何可以在多个线程之间共享的对象一定都属于逃逸对象public void test(int x,inty ){
String x = “”;
User u = ….
….. 
}
User类型的对象u就没有逃逸出方法testpublic  User test(int x,inty ){
String x = “”;
User u = ….
….. 
return u;
}
User类型的对象u就逃逸出方法test如何启用栈上分配(+是开启,-是关闭)
-XX:+DoEscapeAnalysis开启逃逸分析(默认打开)
-XX:+UseTLAB 开启本地线程分配缓冲默认打开只有开启了 本地线程分配缓冲才能使用栈上分配)

-XX:+EliminateAllocations标量替换(默认打开,)

-XX:+PrintGC开启GC日志


测试a方法内创建一个对象然后a方法循环调用一亿次关闭逃逸分析会出现频繁gc最终一亿次执行时长大概接近1秒这一亿个对象都是在堆创建的导致出现大量gc开启逃逸分析一亿次执行时长大概78毫秒栈上创建只创建了一次对象创建到了缓存中重复执行a方法直接从缓存取了

GC

说说对垃圾回收器的理解。+3
理解自动将Java堆中不需要的对象进行回收
  
堆内存划分:(新生代:老年代 = 1:2新生代PSYoungGenEden区:两个S区 = Eden:From:To = 8:1:1
老年代ParOldGen堆内存分配策略1对象优先在Eden分配如果说Eden内存空间不足就会发生Minor GC
2大对象直接进入老年代大对象需要大量连续内存空间的对象比如很长的字符串或大型数组3长期存活的对象将进入老年代默认15岁,-XX:MaxTenuringThreshold调整
	对象头会记录对象的年龄当对象在S区中被复制一次年龄就会+1当达到最大年龄晋升到老年区4动态对象年龄判定虚拟机并不是永远地要求对象的年龄必须达到最大年龄才能晋升老年代如果在S区中相同年龄所有对象大小的总和大于S区的一半年龄大于或等于该年龄的对象就可以直接进入老年代5空间分配担保新生代中有大量的对象存活S区不够用了当出现大量对象在MinorGC后仍然存活的情况最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保把S区无法容纳的对象直接进入老年代GC如何判断对象的存活?- 可达性分析算法GC如果进行回收的?- 复制回收算法标记-清除算法标记-整理算法
堆内存是怎么分配的?
堆内存的划分:(新生代:老年代 = 1:2新生代PSYoungGenEden区:两个S区 = Eden:From:To = 8:1:1
老年代ParOldGen堆内存分配策略1对象优先在Eden分配如果说Eden内存空间不足就会发生Minor GC
2大对象直接进入老年代大对象需要大量连续内存空间的对象比如很长的字符串或大型数组3长期存活的对象将进入老年代默认15岁,-XX:MaxTenuringThreshold调整
	对象头会记录对象的年龄当对象在S区中被复制一次年龄就会+1当达到最大年龄晋升到老年区4动态对象年龄判定虚拟机并不是永远地要求对象的年龄必须达到最大年龄才能晋升老年代如果在S区中相同年龄所有对象大小的总和大于S区的一半年龄大于或等于该年龄的对象就可以直接进入老年代5空间分配担保新生代中有大量的对象存活S区不够用了当出现大量对象在MinorGC后仍然存活的情况最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保把S区无法容纳的对象直接进入老年代GC策略新生代区内存不够会触发Minor GC老年代区内存不够会触发Full GC
新生代中3个区(Eden区 + 两个S区)为什么内存比例是8:1:1 ?
1在新生代区中绝大部分对象的生命都是很短暂的也就是说并不需要按照1:1的比例来划分内存空间2经长期研究测算出当内存使用超过98%以上时内存就应被minor gc回收一次但是实际应用中如果真到98%才GC可能就来不及了所以保险起见当内存使用到达90%的时候就gc留10%的内存放存活的对象3这预留下来的这10%的空间称为S区有两个s区 s1  s0),S区是用来存储新生代GC后存活下来的对象而GC算法使用的是复制回收算法需要1:1的内存空间也就是需要占用总内存的20%,留下了80%给Eden区4每次GC范围是Eden区+一个S区。(比例是eden:s1:s0=8:1:1=8:1:1这里的eden区80%)和其中的一个S区10%) 合起来共占据90%,GC就是清理的他们始终保持着其中一个S区是空留的保证GC的时候复制存活的对象有个存储的地方8:1:1的优点提高内存的利用率只浪费了10%的内存如果存活的对象超过10%的内存怎么办空间担保空间担保是老年代区如果S区放不下了会把多出来的部分放到老年代区
GC如何判断对象是否需要回收?+2
可达性分析算法通过GC Roots的对象作为起始点从这些节点开始向下搜索搜索所走过的路径称为引用链Reference Chain),当一个对象到GC Roots没有任何引用链相连时则证明此对象是不可达到GC会回收它
什么对象可以作为GC Roots?
1虚拟机栈栈帧中的本地变量表中引用的对象2方法区中类静态属性引用的对象3方法区中常量引用的对象4本地方法栈中JNI即一般说的Native方法引用的对象可达例子1static Obj a = new Obj(); //静态对象,是GC Roots
main(){
	Obj b = a;
	Obj c = b;
	Obj d = c;
	Obj e = a;
}

a -> b -> c -> d
  -> e
(bcde是可达的方法执行完后GC也不会回收它们)

可达例子2main(){
	Obj a = new Obj(); //GC Roots
	Obj b = a;
	Obj c = b;
}
(方法执行完之前abc都是可达的)

不可达例子Obj a = new Obj(); //不是GC Roots
main(){
	Obj b = a;
	Obj c = b;
}

a -> b -> c
(bc是不可达的方法执行完后GC会回收它们)
GC如果进行回收的?
垃圾回收算法复制回收算法将可用内存按容量划分为大小相等的两块区域每次只使用其中的一块当这一块的内存快用完了就将还存活着的对象复制到另外一块上面然后再把已使用过的内存空间一次清理掉这样使得每次都是对整个半区进行内存回收内存分配时也就不用考虑内存碎片等复杂情况优点实现简单运行高效不会出现内存碎片缺点内存缩小了一半利用率低需要内存复制使用场景新生代区使用。

------------------------------------------------------------------

标记-清除算法分为标记清除两个阶段首先标记出所有需要回收的对象在标记完成后统一回收所有被标记的对象它的主要不足空间问题标记清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作优点内存利用率百分百无需内存复制缺点会出现内存碎片。

------------------------------------------------------------------

标记-整理算法先标记出所有需要回收的对象在标记完成后后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向一端移动然后直接清理掉端边界以外的内存优点内存利用率百分百不会出现内存碎片缺点需要内存复制Stop The World现象GC事件发生过程中会产生应用程序的停顿停顿产生时整个应用程序线程都会被暂停1可达性分析算法中枚举GC Roots会导致所有Java执行线程停顿2完成GC后会切回原来的线程由于切线程的过程也是耗时的如果频繁GC频繁地切线程也会造成卡顿GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数单线程中的GC收集器Serial新生代复制回收算法SerialOld老年代标记整理算法多线程中的GC收集器ParNew新生代复制回收算法)- 搭配CMS垃圾回收器的首选 
ParallelScavenge新生代复制回收算法ParallelOld老年代标记整理算法CMS老年代标记清除算法)- 并行与并发收集器 - 互联网后端目前主流的垃圾回收器
G1新生代 + 老年代)- 并行与并发收集器 - jdk1.7加入

除了G1是结合体之外其他的垃圾回收器都是搭配使用的都是新生代一个老年代一个CMS
收集器是一种以获取最短回收停顿时间为目标的收集器目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上这类应用尤其重视服务的响应速度希望系统停顿时间最短以给用户带来较好的体验CMS收集器就非常符合这类应用的需求CMS收集器是基于标记清除算法实现的它的运作过程相对于前面几种收集器来说更复杂一些整个过程分为4个步骤包括1初始标记-短暂仅仅只是标记一下GC Roots能直接关联到的对象速度很快。 - 会暂停业务线程
2并发标记-和用户的应用程序同时进行 - 并发标记不会暂停业务线程
3重新标记-短暂为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始标记阶段稍长一些但远比并发标记的时间短。- 会暂停业务线程
4并发清除 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作浮动垃圾由于CMS并发清理阶段用户线程还在运行着伴随程序运行自然就还会有新的垃圾不断产生这一部分垃圾出现在标记过程之后CMS无法在当次收集中处理掉它们只好留待下一次GC时再清理掉这一部分垃圾就称为浮动垃圾”。

语法相关

  1. final、finally、finalize()的区别。+3

  2. 说说Java四种引用,以及用到的场景。+2

    四种引用强引用
    
    软引用SoftReference):系统将要发生OOM之前这些对象就会被回收用途例如为了让图片列表更快地显示可以把它们直接加载到内存中软引用它们当系统内存不够即将OOM的时候让系统释放掉这些图片的内存保证程序的正常运行大不了图片看不了而已不至于闪退弱引用WeakReference):只能生存到下一次垃圾回收之前GC发生时不管内存够不够都会被回收注意软引用和弱引用可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存当系统内存不足的时候缓存中的内容是可以被释放的实际运用WeakHashMapThreadLocal虚引用幽灵引用最弱被垃圾回收的时候收到一个通知
  3. 弱引用与强引用的区别 ,怎么判断一个弱引用被回收了 。

  4. StringBuffer、StringBuilder的区别。+2

  5. ==和equals和hashCode的区别。+2

  6. String a="a"与String a = new String("a")的区别。+2

  7. int、char、long各占多少字节数

  8. int与integer的区别

  9. 谈谈对java多态的理解。+2

  10. 什么是内部类?内部类的作用

  11. 泛型中extends和super的区别。

  12. 说一下泛型原理,并举例说明。

  13. Serializable 和Parcelable 的区别。+2

  14. 父类的静态方法能否被子类重写

  15. 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

  16. 静态内部类的设计意图

  17. 成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项目中的应用

  18. 抽象类和接口区别

  19. 抽象类的意义

  20. 抽象类与接口的应用场景

  21. 抽象类是否可以没有方法和属性?

  22. 接口的意义

  23. Java中String的了解

  24. String为什么要设计成不可变的?

  25. Object类的equal和hashCode方法重写,为什么?

集合相关

  1. Arraylist和Linklist的区别。+3

  2. HashMap扩容条件中链表转红黑树的条件是什么?

  3. HashMap扩容条件为什么要2的指数次幂,如果输入17会是多少容量?(跟hashcode有关,输入17得出结果是32)

  4. CurrentHashMap 读写锁是如何实现的。(无hash冲突用CAS插入,有则用synchronize加锁插入。当链表长度大于8且数据长度>=64的时候会用红黑树代替链表)

  5. List,Set,Map的区别

  6. List和Map的实现方式以及存储方式

  7. ConcurrentHashMap的实现原理

  8. ArrayMap和HashMap的对比

  9. HashTable实现原理

  10. TreeMap具体实现

  11. HashMap和HashTable的区别

  12. HashMap与HashSet的区别

  13. HashSet与HashMap怎么判断集合元素重复?

  14. 二叉树的深度优先遍历和广度优先遍历的具体实现

  15. 堆和树的区别

  16. 什么是深拷贝和浅拷贝

  17. 如何防止 Java 源码被反编译

其他

  1. 动态代理传入的参数有哪些?非接口类能实现动态代理吗?ASM的原理是什么?
  2. utf-8编码中的中文占几个字节;int型几个字节?
  3. 静态代理和动态代理的区别,什么场景使用?

Map相关

hashmap的时间复杂度
putget操作最好情况是O(1),最差情况是O(n)

put操作的流程:
① 取模运算算出数组下标检查数组该下标是否有元素如果没有直接new一个entey节点插入到数组中 -> O(1)
③ 数组下标已经有元素并且元素个数小于8则调用equals方法比较是否存在相同名字的key不存在则new一个entry插入都链表尾部 -> O(n)
④ 数组下标已经有元素并且元素个数大于8则调用equals方法比较是否存在相同名字的key不存在则new一个entry插入都链表尾部 -> O(logn)
   红黑树查询的时间复杂度是O(logn)
hashmap什么情况下才会扩容
当HashMap中的元素越来越多的时候碰撞的几率也就越来越高因为数组的长度是固定的),所以为了提高查询的效率就要对HashMap的数组进行扩容
  
loadFactor的默认值为0.75数组大小为16也就是说默认情况下那么当HashMap中元素个数超过16*0.75=12的时候就把数组的大小扩展为2*16=32即扩大一倍然后重新计算每个元素在数组中的位置而这是一个非常消耗性能的操作所以如果我们已经预知HashMap中元素的个数那么预设元素的个数能够有效的提高HashMap的性能
说说HashMap的hash方法的作用?(hash方法原理分析)
hash方法作用是均匀散列

hash()方法解析1Object#hashCode2取模算法hashcode ^ (h >>> 16))
static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

1Object#hashCode返回固定长度的摘要
hash算法任意长度的输入 转换成 固定长度的摘要输出这种转换是一种压缩映射所有不同的输入可能会散列成相同的摘要输出java hash的实现方式基于内存地址生成利用位移生成随机数随机数自增java hashcode存储对象的hashCode()未重写时会返回一个由随机数算法生成的值因为对象的hashCode不可变所以需要存到对象头中当再次调用该方法时会直接返回对象头中的hashcode如果重写了hashCode(),对象头的code会失效并且也不会把code存对象头了为什么要用到hashCode方法一个对象想要插入一个集合中首先得知道这个集合是否存在该对象那怎么去集合中查找呢常规方法是调用equals方法来逐个比较但是如果集合数据量庞大逐个比较效率会很低集合存储基本上离不开数值下标如果能够直接通过下标拿到对象效率就非常高的所有需要用到唯一数值便有了将对象转化为数值的hashCode方法2取模算法hashcode ^ (hashcode >>> 16))(为了均匀散列表的下标降低极端情况的出现从而导致链表拉长为什么要右移位为什么右移位16为什么要异或右移位右移位16取int类型的一半将二进制数对半切开。(提高运算性能异或降低极端情况的出现HashMap如何根据hash值找到数组中的对象get方法中有这样一段代码first = tab[(n - 1) & hash](tab为数组n为数组长度假设数组长度为16n-1也就是15),并且不做取模算法直接使用对象的hashCode来做下标对象A hashCode1000010001110001000001111000000对象B hashCode011101110011100010100001010000015 & 对象A的hashCode  -->  0
15 & 对象B的hashCode  -->  0
为啥?????因为AB hashCode后边一大堆000000这样的散列结果太让人失望了很明显不是一个好的散列算法但是如果我们将hashCode值右移16位也就是取int类型的一半刚好将该二进制数对半切开并且使用位异或这样的话就能避免我们上面的情况的发生总的来说使用位移16位和异或就是防止这种极端情况但是一些极端情况下还是有问题比如10000000000000000000000000 1000000000100000000000000这两个数如果数组长度是16那么即使右移16位在异或hash 值还是会重复但是为了性能对这种极端情况JDK选择了性能毕竟这是少数情况为了这种情况去增加 hash 时间性价比不高
为什么get()、put()要用&运算来算下标?公式:(n - 1) & hash
n为数组长度下标肯定是在n范围内的怎么把能够把hash计算变成 0 - n-1 的范围之内呢除法求余取模这些方法速度都不快最快的还是位运算a % b == (b-1) & a ,当b是底数是2的真数时等式成立N = 2的n次幂2是底数n为对数N为真数)。

例如4 % 4 = 0  --->  (4-1) & 4 = 0
5 % 4 = 1  --->  (4-1) & 5 = 1

非2的n次幂
4 % 3 = 1  --->  (3-1) & 4 = 0
5 % 3 = 2  --->  (3-1) & 5 = 0
6 % 3 = 0  --->  (3-1) & 4 = 2
7 % 3 = 1  --->  (3-1) & 5 = 3
8 % 3 = 2  --->  (3-1) & 5 = 0

因为2的n次幂 - 1二进制全是1例如 3 -> 11    7 -> 111
用全是1的二进制数正好可以做掩码&运算的时候结果取决于hash值由于hash值是均匀散列的所以结果也是均匀散列
为什么HashMap容器大小最好设置成2的次幂?(为什么HashMap默认的容器大小是16)
hash算法的目的是为了让hash值均匀的分布在数组中如果不使用2的幂次方作为数组的长度就会失去了hash均匀分布作用造成不同的key值全部进入到相同的数组下标中形成链表性能急剧下降我们一定要保证 &运算 中的二进制位全为1才能最大限度的利用hash值HashMap容量大小设置多少最好知道数据量的情况最好给容器合理的大小能避免动态扩容带来的性能损耗动态扩容除了搬运数据耗时之外还可能导致成倍的内存浪费)。
容量大小最好是2的次幂 同时 还需要根据负载系数来权衡设置例如需要存2个数据HashMap默认的负载系数是0.75
2 / 0.752.67 那么容量最好设置为4虽然会造成内存浪费但是可以避免动态扩容如果你预计大概会插入 12 条数据的话那么初始容量为16简直是完美一点不浪费而且也不会扩容
HashMap的链表、红黑树是如何转换的?
put操作的时候发生hash冲突后并且数组下标有元素的情况下就会判断元素类型是 TreeNode 还是 NodeTreeNode代表为红黑树Node代表链表链表的遍历插入逻辑中如果链表长度大于8了就会转换为红黑树走treeifyBin方法转换)。
//TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash);

并不是走treeifyBin方法就一定会变成红黑树的还会先判断数组长度有没有大于64如果没有的话优先动态扩容重新离散元素为什么会优先扩容而不是优先转红黑树呢因为红黑树虽然查询效率高了但是插入效率不高//MIN_TREEIFY_CAPACITY = 64
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

为什么不直接使用红黑树呢红黑树为了维护平衡需要进行左旋右旋操作而单链表不需要如果元素小于8个查询成本高新增成本低如果元素大于8个查询成本低新增成本高由于红黑树也会存在高成本的地方所有Hashmap在转换红黑树前会先做动态扩容判断就是为了通过牺牲空间来换时间的红黑树一个自平衡的二叉查找树二叉查找树性质1 左子树上所有结点均小于或等于它的根结点性质2 右子树上所有结点均大于或等于它的根结点性质3 右子树也分别为二叉排序树。

“查找” -> 二分查找思想 -> 查出结果所需的最大次数就是二叉树的高度 -> 时间复杂度O(logn)
但是有极端的情况会让二叉查找树退化为链表依次插入根节点为9如下五个节点7,6,5,4,3依照二叉查找树的特性7,6,5,4,3一个比一个小那么就会成一条直线也就是成为了线性的查询时间复杂度变成了O(n)。红黑树登场!!!

红黑树性质1 节点是红色或黑色性质2 根节点是黑色性质3 每个叶节点NIL节点空节点是黑色的性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点红黑树通过变色旋转来维护红黑树的规则变色就是让黑的变成红的红的变成黑的旋转又分为左旋转右旋转”。
HashMap为什么是线程不安全?会出现什么问题?
1.7 扩容操作 -> 死循环数据丢失
死循环转移元素使用的是头插法链表的顺序会翻转这是形成死循环的关键点头插法:
[1]-a-b-null
-------------
[1]-null
[2]-b-a-null


线程1 
[1]-b-null取出了a但是还没来得及丢到[2],就挂起了)
[2]-null
线程2 
[1]-null
[2]-b-a-null线程2又扩容了并且走完扩容逻辑了然后挂起线程1
[1]-null
[2]-a-b-a把之前取出来的ab.next = aa-b-a形成了死循环1.8 put操作 -> 数据覆盖
扩容操作转移元素使用的是尾插法不会出现1.7的死循环线程A和线程B同时进行put操作刚好这两条不同的数据hash值算出来的数组下标一样并且该位置数据为null所以这线程AB都会往这下标插入数据假设一种情况线程A进入后还未进行数据插入时挂起而线程B正常执行正常插入数据然后线程A恢复继续插入问题出现线程A会覆盖线程B的数据尾插法:
[1]-a-b-null
-------------
[1]-null
[2]-a-b-null
HashMap能不能用二维数组实现?
二维数组就是相当于数组+数组而Hashmap实现是数组+链表这里可以把链表替换成数组数组访问速度是O(1),链表是O(N)。二维数组在查询速度上完胜但是会有以下的问题1浪费空间大多数情况下hash表并不存在大量冲突二维数组会浪费内存2动态扩容麻烦二维需要考虑到两个数组的扩容问题3二次散列第二维的那个数组要用到O(1)的速度就必须要给value值进行散列散列又会出现冲突问题valueA散列出2valueB也散列出2导致冲突解决方法就只能用开放寻地发如果不想二次散列就只能跟老老实实用O(N)速度一个个遍历了
说说LinkedHashMap(HashMap + 双向链表)特点:有序的
在HashMap的基础上继承自HashMap),多维护了一个双向链表用来保证元素的有序性关键属性Entry<K,V> header; //头结点(双向链表的入口)
boolean accessOrder; //false插入顺序,true访问顺序,也就是访问次数.

元素类继承自HashMap.Entry<K,V>
并且扩展了beforeafter
	Entry<K, V> before;
	NEtry<K, V> after;
beforeafter是用于维护Entry插入的先后顺序的正是beforeafter和head的存在才形成了双向链表

1重写了init方法为header进行初始化。(在HashMap构造函数中调用了这个init方法header中hash值为-1其他都为nullbeforeafter指向自己header不在数组table中的head的目的就是为了记录第一个插入的元素2并没有重写HashMap的put方法而是只重写了put方法逻辑中调用的子方法addEntry()和createEntry()
数据插入逻辑也是使用HashMap的逻辑addEntry()和createEntry()目的是为了将插入后元素的beforeafter与header绑定在一起形成双链表3重写了HashMap的get方法
通过HashMap的get方法拿到数据之后会判断accessOrder标志位如果accessOrder为true将该元素转移到双向链表的尾部实现访问顺序public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }

4迭代器LinkedHashIterator遍历的是双向链表拿到head从head开始遍历
说说ConcurrentHashMap的原理
JDK1.7 SegmentReentrantLock)+ HashEntry
ConcurrentHashMap维护着一个Segment对象数组一个Segment就相当于一个HashMapSegment里包含一个HashEntry数组每个数组元素能链成一条链表采取锁分段技术每一个Segment就好比一个自治区读写操作高度自治Segment之间互不影响case1 不同Segment的并发写入可以并发执行case2 同一Segment的一写一读可以并发执行case3 同一Segment的并发写入会上锁保证一个线程操作其他线程等待由此可见当中每个Segment各自持有一把锁Segment继承自ReentrantLock)。在保证线程安全的同时降低了锁的粒度让并发操作效率更高Get方法为输入的Key做Hash运算得到hash值通过hash值定位到对应的Segment对象再次通过hash值定位到 Segment 当中数组的具体位置Put方法为输入的Key做Hash运算得到hash值通过hash值定位到对应的Segment对象获取可重入锁
再次通过hash值定位到Segment当中数组的具体位置插入或覆盖HashEntry对象释放锁

读写时均需要二次散列首先定位到Segment之后定位到Segment内的具体数组下标Size方法每个Segment都各自加锁那么在调用size()的时候怎么解决一致性的遍历所有的Segment把Segment的元素数量累加起来把Segment的修改次数累加起来判断所有Segment的总修改次数是否大于上一次的总修改次数如果大于说明统计过程中有修改重新统计尝试次数+1如果不是说明没有修改统计结束如果尝试次数超过阈值则对每一个Segment加锁再重新统计再次判断所有Segment的总修改次数是否大于上一次的总修改次数由于已经加锁次数一定和上次相等释放锁统计结束乐观锁 + 悲观锁乐观地认为Size过程中不会有修改当尝试多次次数才无奈转为悲观锁住所有Segment保证强一致性。

================================================

JDK1.8 Synchronized + CAS + Node锁的粒度更小//sizeCtl变量:chm内部数组的状态
//0:sizeCtl为0,代表数组未初始化
//正数:该值代表当前数组的阈值(跟hashmap的一样,capacity*loadFactor)
//-1:代表数组正在初始化
//负数且不是-1:代表数组正在扩容,该数为-(n+1),表示当前有n个线程在共同完成扩容操作
private transient volatile int sizeCtl;

//baseCount +  CounterCell[]是用来统计size的
private transient volatile CounterCell[] counterCells;
private transient volatile long baseCount;

//存储K,V数据
transient volatile Node<K,V>[] table;

Get方法1计算hash值定位到table下标位置如果是首节点符合就返回
2如果遇到扩容的时候会调用标志正在扩容节点ForwardingNode的find方法查找该节点匹配就返回
3以上都不符合的话就往下遍历节点匹配就返回否则最后就返回null


Put方法初始化操作并不是在构造函数实现的而是在put操作中实现进行自旋死循环):1-5步骤
1如果table为就先调用initTable()来进行初始化懒初始化如果其他线程正在初始化sizeCtl<0),那么调用Thread.yield()挂起当前线程等其他线程执行完后再继续工作2如果没有hash冲突就直接CAS插入table被volatile修饰可以保证可见性3如果还在进行扩容操作就先进行扩容
	走helpTransfer()方法没有加锁或者挂起线程操作利用多个线程一起帮助进行扩容提高扩容效率而不是只有检查到要扩容的线程进行扩容4如果存在hash冲突就加锁这个元素synchronized来保证线程安全链表形式就直接遍历到尾端插入红黑树就按照红黑树结构插入5最后一个如果该链表的数量大于阈值8就要先转换成黑红树的结构break再一次进入循环;

6如果添加成功就调用addCount()方法统计size并且检查是否需要扩容Size方法baseCount +  CounterCell[]里所有value值
在扩容和addCount()方法就已经把baseCountCounterCell[]处理好了Put方法里就有addCount()。


addCount()方法解析更新baseCountCounterCell[]
多个线程进行put要进行size++操作那么是不是用一个原子类就完事了这样太慢了需要一个线程累加完才到下一个baseCount + CounterCell[],先尝试对baseCount进行CAS成功就更新了baseCount值如果累加baseCount失败那么直接在CounterCell[],拿到当前线程的hashcode运算出下标给下标做累加这样就能实现多线程累加了transfer()方法解析利用多个线程一起进行扩容
ForwardingNode一个特殊的Node节点hash值为-1其中存储nextTable的引用只有table发生扩容的时候才会发挥作用作为一个占位符放在table中表示当前节点为null或则已经被移动节点从table移动到nextTable大体思想是遍历复制的过程首先根据运算得到需要遍历的次数i然后利用tabAt方法获得i位置的元素f初始化一个forwardNode实例fwd如果f == null则在table中的i位置放入fwd这个过程是采用Unsafe.compareAndSwapObjectf方法实现的很巧妙的实现了节点的并发移动如果f是链表的头节点就构造一个反序链表把他们分别放在nextTable的i和i+n的位置上移动完成采用Unsafe.putObjectVolatile方法给table原位置赋值fwd如果f是TreeBin节点也做一个反序处理并判断是否需要untreeify把处理的结果分别放在nextTable的i和i+n的位置上移动完成同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd
说说Android的SparseArray
SparseArray系列SparseArraySparseBooleanArraySparseIntArraySparseLongArrayLongSparseArrayHashMap -> key - 泛型value - 泛型
SparseArray -> key - intvalue - Object
SparseArray在使用上受到了很大的约束嘛这样约束的意义何在呢SparseArray中的优秀设计延迟删除机制删除设置标志位”,来延迟删除实现数据位的复用);
二分查找的返回值处理在空间不足时利用gc函数一次性压缩空间提高效率SparseArray应用场景1数据量不大最好在千级以内数据量大的情况下性能并不明显将降低至少50%)
2key必须为int类型这中情况下的HashMap可以用SparseArray代替

SparseArray原理分析属性private static final Object DELETED = new Object(); //删除标志位
private boolean mGarbage = false; //是否垃圾回收
private int[] mKeys; //升序数组
private Object[] mValues;

添加public void append(int key, E value) {
        if (mSize != 0 && key <= mKeys[mSize - 1]) {
        	//当mSize不为0并且不大于mKeys数组中的最大值时,因为mKeys是一个升序数组,最大值即为mKeys[mSize-1]
	        //直接执行put方法,否则继续向下执行
            put(key, value);
            return;
        }

		//当垃圾回收标志mGarbage为true并且当前元素已经占满整个数组,执行gc进行空间压缩
        if (mGarbage && mSize >= mKeys.length) {
            gc();
        }

        //当数组为空,或者key值大于当前mKeys数组最大值的时候,在数组最后一个位置插入元素。
        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        mSize++;
    }

    public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key); //二分查找
        if (i >= 0) { //查找到
            mValues[i] = value;
        } else { //没有查找到
            i = ~i; //取反,拿到要插入的下标。
            if (i < mSize && mValues[i] == DELETED) { //元素要添加的位置正好==DELETED,直接覆盖它的值即可。
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
			//垃圾回收,但是空间压缩后,mValues数组和mKeys数组元素有变化,需要重新计算插入的位置
            if (mGarbage && mSize >= mKeys.length) {
                gc();

                //重新计算插入的位置
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
			//在i位置插入元素
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

    //ContainerHelpers#binarySearch
    static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;

        while (lo <= hi) {
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];

            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid; // 找到了
            }
        }
        //没找到,直接取反然后返回。
        //取反就变成负数了,外部只需要判断大于0就能够知道找没找到了
        //然后外部还可以再取反拿回没命中的这个下标位置,这个下标位置就是将要插入数据的位置。
        return ~lo;
    }

    例子假设我们有个数组 3 4 6 7 8用二分查找来查找元素5
	初始lo=0 hi=4
	第一次循环mid=(lo+hi)/2=2 2位置对应6 6>5 查找失败下一轮循环 lo=0 hi=1
	第二次循环mid=(lo+hi)/2=0 0位置对应3 3<5 查找失败下一轮循环 lo=2 hi=1
	lo>hi 循环终止
	最终 lo=2 即5需要插入的下标位置


    //GrowingArrayUtils#insert 把目标下标后面的元素后移,再插入元素。(如果容量不够会进行动态库容)
    public static int[] insert(int[] array, int currentSize, int index, int element) {
        if (currentSize + 1 <= array.length) {//如果数组长度能够容下直接在原数组上插入
        	//调用了Java 的native方法,把array 从index开始的数复制到index+1上,
        	//复制长度是currentSize - index
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            //空出来的那个位置直接放入我们要存入的值,
            //也不是空出来,其实index上还是有数的,
            // 比如:{2,3,4,5,0,0}从index=1开始复制,复制长度为5,复制后的结果就是{2,3,3,4,5,0}了
            array[index] = element;
            return array;
        }

		//这就是扩容了,新建了一个数组,长度*2
        int[] newArray = new int[growSize(currentSize)]; 
        
        //新旧数组拷贝,先拷贝最佳位置之前的到新数组
        System.arraycopy(array, 0, newArray, 0, index);
        
        newArray[index] = element;//直接在新数组上赋值
        //然后拷贝旧数组最佳位置index起的所有数到新数组里面,
        //只是做了分段拷贝而已
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        
        return newArray;
    }


查找二分查找找到了返回value给你没找到返回你自己传的notFoundValuepublic E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

删除没有实际删除size值也不改?????
	public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) { //直接替换成DELETED对象
                mValues[i] = DELETED;
                mGarbage = true; //开启回收标志位
            }
        }
    }

获取大小先触发gc一次再返回size值没毛病
	public int size() {
        if (mGarbage) {
            gc();
        }

        return mSize;
    }


gc方法快慢指针思想将DELETED元素排挤出数组private void gc() {
        int n = mSize;//压缩前数组的容量
        int o = 0;//压缩后数组的容量,初始为0
        int[] keys = mKeys;//保存新的key值的数组
        Object[] values = mValues;//保存新的value值的数组

        for (int i = 0; i < n; i++) {
            Object val = values[i];
            if (val != DELETED) {//如果该value值不为DELETED,也就是没有被打上“删除”的标签
                if (i != o) {//如果前面已经有元素打上“删除”的标签,那么 i 才会不等于 o
	                //将 i 位置的元素向前移动到 o 处,这样做最终会让所有的非DELETED元素连续紧挨在数组前面
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;//释放空间
                }
                o++;
            }
        }
        mGarbage = false; //恢复垃圾回收标志位
        mSize = o; //更新size值,回收之后数组的大小
    }
说说Android的ArrayMap<K, V>
ArrayMap内部跟SparseArray一样也是使用两个数组进行数据存储一个数组记录key的hash值另外一个数组记录Value值它和SparseArray一样也会对key使用二分法进行从小到大排序在添加删除查找数据的时候都是先使用二分查找法得到相应的index然后通过index来进行添加查找删除等操作所以应用场景和SparseArray的一样ArrayMap应用场景1数据量不大最好在千级以内数据量大的情况下性能并不明显将降低至少50%)