1. == 和 equals
- == : 比较栈内存中的值(基本数据类型比较变量值,引用类型比较内存对象的地址)
- equals : Object默认采用 == 比较,一般情况下会重写此方法
默认生成的 equals 方法(生成即是重写后的):
例:
2. ArrayList和LinkedList的区别
ArrayList: 基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制 : 因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能,甚至超过 LinkedList (LinkedList需要创建大量的node对象).
LinkedList : 基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询,需要逐一遍历.而遍历LinkedList必须使用迭代器进行(iterator)遍历,不能使用for循坏,因为每次for循坏中需要通过get(i)取得某一元素时都需要对list重新遍历,性能消耗大.并且在使用indexOf等返回元素索引时,会对集合进行遍历,结果为空时也会遍历整个集合.
3. ConcurrentHashMap的扩容机制
JDK1.7 :
- 1.7版本的ConcurrentHashMap基于Seqment分段实现
- 每个Seqment相对于一个小型的HashMap
- 每个Seqment内部会进行扩容,和HashMap的扩容逻辑类型
- 先生成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Seqment内部单独判断的,判断是否超过阈值
JDK1.8 :
- 1.8版本的ConcurrentHashMap不再基于Seqment实现
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容,那么该线程一起进行扩容
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
- ConcurrentHashMap支持多个线程同时扩容
- 扩容之前也会先生成一个新的数组
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组元素的转移工作
4. ConcurrentHashMap在JDK1.7和1.8的区别
JDK1.7 :
数据结构 : ReentrantLock + Seqment + HashEntry,一个Seqment 中包含一个HashEntry数组,每个HashEntry又是一个链表结构.
元素查询 : 二次哈希,第一次哈希定位到Seqment ,第二次哈希定位到元素所在的链表的头部.
锁 : Seqment 分段锁,Seqment 继承了ReentrantLock,锁定操作的Seqment,其他的Seqment不受影响,并发度为Seqment个数,可以通过构造函数指定,数组扩容不会影响其他的Seqment. get方法没有加锁,而是采用了volatile修饰来保证可见性.
JDK1.8 :
数据结构 : synchronized + CAS + Node + 红黑树,Node的val和next都用volatile修饰来保证可见性.
查找、替换、赋值操作使用CAS来保证并发安全.
锁 : 锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容.
读操作无锁 : Node的val和next都用volatile修饰,读写线程对该变量互相可见.数组用volatile修饰,保证扩容时被读线程感知到.
5. CopyOnWriteArrayList的底层原理
- CopyOnWriteArrayList内部也是用数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行.
- 写操作会加锁,防止出现并发写入丢失数据的问题
- 写操作结束后会把原数组指向新数组
- CopyOnWriteArrayList允许在写操作时来读取数据,提高了读性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景.
6. final
作用:
- 修饰类 : 表示类不可被继承.
- 修饰方法 : 表示方法不可被子类覆盖,但是可以重载.
- 修饰变量 : 表示变量一旦被赋值就不可以更改它的值.
(1) : 修饰成员变量
- 如果final修饰的是类变量,只能在静态初始化代码块中指定初始值或者声明该类变量时指定初始值.
- 如果final修饰的是成员变量,可以在非静态初始化代码块声明该变量或者构造函数中指定初始值.
(2) : 修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员指定初始化值.因此,使用final修饰局部变量时,即可以在定义时指定默认值(后续不得再次对该值进行更改),也可以不指定默认值,而在后面的代码块中对final变量进行初始化(仅一次).
(3) 修饰基本类型数据和引用类型数据
- 如果是基本类型的变量,则数值一旦在初始化之后便不能更改.
- 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是引用的值是可变的.
为什么局部内部类和匿名内部类只能访问局部final变量?
内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的进行完毕而销毁.
所以,当外部类的方法结束时,局部变量就会被销毁,但是内部类对象可能还存在着对外部类局部变量的引用.所以就会出现一个问题 : 内部类对象引用了一个被销毁的变量.为了解决这个问题,就将局部变量赋值一份作为内部类的成员变量,这样当局部变量销毁时,内部类依然可以访问改变量(实际访问的是改变量的备份副本).
至此依然存在一个问题 : 就是怎么保证外部类的局部变量和复制的副本数据怎么保证数据一致性.于是,就将外部类的局部变量设置为final,对它进行初始化后避免后续被修改.
7. hashcode和equals
hashcode()的作用是获取哈希码,也称散列码,实际返回一个int整数,这个哈希码的作用是确定该对象在哈希表的索引位置.hashcode()定义在JDK的Object.java中,java中的任何类都包含后hashcode()函数.
- 如果两个对象相等,则hashcode()一定是相同的
- 两个对象相等,对两个对象分别调用eauals()方法都返回为true
- 两个对象有相同的hashcode()值,它们也不一定是相等的
- 因此,equals()方法被覆盖过,则hashcode()方法也必须覆盖
- hashcode()的默认行为是对堆上对象生成独特值,如果没有重写hashcode(),则改class的两个对象无论如何都不会相等.
8. HashMap的扩容机制原理
1.7版本
- 生成新数组
- 遍历老数组的每个位置上链表上每个元素
- 取每一个元素key,并基于数组长度计算出每个元素在新数组的下标
- 将元素添加到新数组中去
- 所有元素转移完成之后,将新数组赋值给HashMap对象的table属性
1.8版本
- 生成新数组
- 遍历老数组中的每个位置上的链表或者红黑树
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红黑树,则先遍历红黑树,先计算红黑树中每个元素在新数组的下标位置
统计每个下标位置的元素个数
如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根据节点添加到新数组的对应位置
如果该位置下的元素个数没有超过8,则生成一个链表,并将链表的头节点添加到数组的对应位置 - 所有元素转移完成之后,将新数组赋值给HashMap对象的table属性
9. java中的异常体系
java中所有的异常都来自于顶级父类Throwable
Throwable下有2个子类Exception和Error
Error是程序无法处理的错误,一旦出现,程序将被迫停止运行
Exception不会导致程序停止,又分为2部分RunTimeException运行时异常和CheckedException检查异常
RunTimeException发生在程序运行过程中,会导致程序当前线程执行失败
CheckedException发生在程序编译过程中,会导致程序编译不通过
10. List和Set的区别
- List : 有序,按对象进入的顺序保存对象,可重复,允许多个null元素对象,可以使用iterator取出所有元素
- Set : 无序,不可重复,最多允许有一个null元素对象,取元素时只能用iterator接口取得所有元素,再逐一遍历各个元素
11. Jdk1.7到Jdk1.8 java虚拟机发生了什么变化
1.7中存在永久代,1.8中没有永久代,替代它的是元空间,元空间所占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是 : 不管是永久代还是元空间都是方法区的具体实现,之所以元空间所占的内存改为本地内存,官方说法是为了和JRockit统一,不过额外存在一些原因,比如方法区所存储的类信息通常是比较难以确定的,所以对于方法区的大小是比较难以指定的,太小了容易出现方法区溢出,太大了又会占用太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占内存.
12. 说一下HashMap的Put方法
- 根据key通过哈希算法与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(1.8是Node对象)并放入该位置
- 如果数组下标位置元素不为空,则需要分情况讨论 :
(1) 如果是1.7,则先判断是否需要扩容,如果要扩容则进行扩容,如果不需要扩容则生成Entry对象,并使用头插法添加当前位置的链表中.
(2) 如果是1.8,则会先判断当前位置上Node类型,判断是红黑树还是链表
i : 如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树是否存在当前key,如果存在则更新value。
ii: 如果此位置上的Node是链表节点,则key和value封装为一个链表Node并通过尾插法插入到链表的最后位置,因为是尾插法,所以需要遍历整个链表,在遍历的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果超过了8,那么则会将该链表转成红黑树。
iii: 将key和value封装为Node插入到链表挥着红黑树中后,再判断是否需要进行扩容,如果需要则进行扩容,不需要则直接结束Put方法。
13. 接口和抽象类的区别
- 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法。
- 抽象类中成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型
- 抽象类只能基础一个,接口可以实现多个。
接口的设计目的是对类的行为进行约束,也就是提供一种机制,可以强制要求不同的类具有相同的行为。即只约束了行为的有无,但不对如何实现进行限制。
抽象类的设计目的是代码的复用,当不同的类具有相同的行为时,且其中一部分行为的实现方式一致时,可以让这些类派生于一个抽象类(抽象类不允许实例化)。
14. 深拷贝和浅拷贝
深拷贝和浅拷贝都是指对对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
- 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象。
- 深拷贝的指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象。
15. 重载和重写的区别
- 重载 : 发生在同一个类中,方法名必须相同,参数类型不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
- 重写 : 发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类,如果父类方法访问修饰符为private,则子类就不能重写该方法。
16. 锁升级过程
Java的锁是针对对象来上锁,其中锁状态的记录主要存储在_mark(markword)中。
- 偏向锁:在锁对象的对象头记录当前获取到该锁的线程id,该线程下次如果又来获取锁,就可以直接获取到。
- 轻量级锁:由偏向锁升级而来,当一个线程获取到该锁后,此时这把锁是是偏向锁,如果有第二个线程来竞争锁,偏向锁就会升级到轻量级锁。(之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过CAS自旋来实现,不会阻塞线程)
- 如果自旋次数过多(大于10次)依然没有获取到锁,则升级为重量级锁,重量级锁会导致线程阻塞。
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较耗时间。自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循坏获取,如果获取到了则表示获取到了锁,整个过程中,线程一直处于运行状态,相对而言没有使用太多的操作系统资源,比较轻量。
锁升级过程:
17. MySQL的数据类型
数据类型:
整数类型:
18. Java的数据类型
19. 线程池参数的含义
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- TimeUnit:时间单位
- BlockingQueue:线程池任务队列
- ThreadFactory:创建线程的工厂
- RejectedExecutionHandler:拒绝策略
20. Innodb的索引实现
21. Innodb的索引为什么是B+树?
22. volatile关键字的作用
在并发场景下,存在三大特性:原子性、有序性、可见性。volatile关键字用来修饰对象的属性,在并发场景下可以保证对这个属性的可见性,对于加了volatile关键字的属性,在进行修改时,会直接将CPU高级缓存中的数据写回主内存中,对这个变量的读取也会直接从主内存中读取,从而保证可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会精致指令重排,所以也能保证有序性。