1.java有哪几种设计模式?
单例概念:只能创建单个的实例对象
class Student{
//当多线程并发访问时,拿到的是不是同一个对象?是,因为在类加载的时候对象创建完毕后保存在内存中 类加载只会进行一次
private static final Student s =new Student();
public static Student getInstance(){
//s=new Student();
return s;
}
private Student(){}//private: 私有的 只能在本类中访问
}
//懒汉式
class Worker{
private static Worker w;
//什么时候需要对象时什么时候创建
//加锁之后线程安全,效率低
public static synchronized Worker getInstance(){
if(w==null)w=new Worker();
return w;
}
private Worker(){}
}
创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。
结构型模式:把类或对象结合在一起形成一个更大的结构。
行为型模式:类和对象如何交互,及划分责任和算法。
单例模式:某个类只能有一个实例,提供一个全局的访问点。
简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。
工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。
抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。
建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。
原型模式:通过复制现有的实例来创建新的实例。
适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。
组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。
装饰模式:动态的给对象添加新的功能。
代理模式:为其他对象提供一个代理以便控制这个对象的访问。
亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。
外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
观察者模式:对象间的一对多的依赖关系。
备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
中介者模式:用一个中介对象来封装一系列的对象交互。
命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
2.反射
1.类的对象: 基于一个已经存在的类创建的对象
2.类对象:类加载的产物 类对象的模板为Class
3.类对象的获取方式
a) 通过类的对象获取类对象(getClass())
b)通过类名.class属性
c)通过Class.forName(“全限定名”)
4. 三种获取类对象的方式会不会类加载?
都会进行类加载
5. 通过反射(类对象)能不能获取类的对象?会不会调用构造方法?
可以通过newInstance()方法创建类的对象。会调用构造方法
3.线程
1. 进程和线程的区别。
(1) 概念:
① 进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资
源的基本单位;一个进程对应一个应用程序,现在的计算机都是支持多进程的,在同一个操作系统中,可
以同时启动多个进程。
②线程:是进程的一个执行单元,是进程内部的调度实体,也被称为轻量级的进程。
(2) 细节区别
①进程之间的内存空间是相互独立的;线程之间共享主内存,而每个线程都有独立的工作内存
②线程占用的资源比进程少很多,创建一个线程比进程开销小,相对效率高
③进程比线程更健壮,多线程程序只要有一个程序奔溃,整个进程也就奔溃;而一个进程奔溃并不会对另一
个进程造成影响。
2.请你描述并发和并行的区别
(1) 并发(concurrency):指在同一个时刻只有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具 有多个进程同时执行的效果,但是微观上并不是同时执行的,只是把时间分成若干,使多个进程快速交替的执行。
(2) 并行(parallelism):指在同一时刻,有多条指令在多个处理器上同时执行。
3.举例说明同步和异步。
(1) 概念:(同步和异步通常用来形容一次方法调用)
①同步:方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。(生活商场购物)
理解:你要回家,你没带钥匙,你家没人,敲门没人开门,那么你只有等家人回来了在进门。
② 异步:方法调用一旦开始,方法的调用者就会立即返回,调用者就可以继续后续的操作。(网购)
理解:早晨送牛奶的大叔,每天都会把牛奶送到你家,有时候你家有人直接送进去,有时候没人他
会放在门口的那个牛奶盒子里面,然后去下一家,这个就和异步一样的性质。
(2) 场景:
①同步:如果程序中存在临界资源,例如正在写的数据之后可能被另一个线程读到,或是正在读的数据可能
已经被另一个线程进行写操作,那么这些数据必须进行同步读取。
②异步:当应用程序在调用某对象一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回,
此时应该使用异步操作。在很多情况下采用异步途径往往更有效率。
注:同步通常就是指阻塞式操作;而异步通常是非阻塞式操作。
4. 写出多线程的创建方式
(1) 继承 Thread 类
① 用类 MyThread 继承 Thread 类,同时覆盖 run 方法(线程执行的任务代码写在 run 方法中)
② 创建 MyThread 对象,即获取线程对象 t1 : MyThread t1 = new MyThread();
③ 利用 run 开启线程:t1.run(); //开启线程,JVM 自动运行 run 方法
(2) 实现 Runnable 接口
1 用类 MyTarget 实现 Runnable 接口,同时实现 run 方法(线程任务代码写在 run 方法中)
2 创建 MyTarget 对象,获取目标对象 tg:MyTarget tg = new MyTarget();
3 基于线程父类 Thread,创建线程对象,同时将目标对象作为参数传递给线程对象: Thread t2 = new Thread(tg);
4 利用 run 开启线程:t2.run(); //开启线程,JVM 自动运行 run 方法
(3) 使用线程池(ExecutorService)、Callable 接口、Future 实现具有返回值的多线程
1 获取线程池对象 es
2 利用匿名内部类获取 Callable 类型的对象 c1,将线程任务代码写在 call 方法中
3 利用 Future<call 方法的返回值类型> f=es.submit(c1); 将任务提交给线程池
4 通过 f.get(); 获取返回值
5.简述 Java 中的 sleep(long ms)和 wait()方法的区别
(1) Sleep(long ms)方法会让当前线程进入休眠,从而处于有限期等待状态,并让当前线程释放 CPU;
通常会用于暂停线程。·
(2) wait()方法会让当前线程处于无限期等待状态,并让当前线程释放 CPU,同时释放锁标记;通常用于线程间交互。
6.简述 synchronized 和 volatile 的区别。
(1) 含义:
① volatile:一旦一个共享的变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,则具备以下两层
含义:
1) 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他 线程来说就是立即可见的
2) 禁止进行指令的重排,保证有序性;程序执行到 volatile 变量的读操作或者写操作时,在其前面的语 句中,更改操作肯定已经完成,且结果已经对后面的操作可见。
② synchronized 则是锁定临界资源对象,一旦线程获取临界资源对象的锁标记,则执行完同步代码块中的 所有代码才释放锁标记,同时其他线程处于阻塞状态
(2) 总结细节的不同:
① volatile 仅能用在变量级别;synchronized 则可以使用在变量、方法和类级别
② volatile 仅能实现变量的修改可见性,但是并不能保证原子性; synchronized 可以保证变量的可见性和原子性 ③ volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞
7请叙述一下对象线程池的理解?并列举常用的线程池有哪些。
(1) 线程池是将多个线程对象放在一个容器中,当使用时不再利用 new 线程对象,而是直接去线程池中获取直接使
用即可,并且能被重复使用;节省不断创建线程的时间,提高代码的执行效率
(2) 常用的线程池
①Executors.newSingleThreadExecutor():创建一个单线程的线程池,此线程池保证所有任务的执行顺序按
照任务的提交顺序执行。
适应场景:适用于需要保证任务顺序执行;并且在任意时间点,不会有多个线程活动的场景。
②Executors.newFixedThreadExecutor(int n):创建固定线程数量的线程池,该线程池中的线程数量保持
不变。当有新任务提交时,线程池中若有空闲线程,则立即执行;若没有,则新的任务会被暂存在一个任
务队列中,待有线程空闲时,便处理任务队列中的任务。
适应场景:适用于为了满足资源管理需求,而需要限制当前线程的数量的应用场景,它适用于负载比较重
的服务器。
③Executors.newCachedThreadPool():创建一个可以根据实际情况调整线程数量的线程池,线程池中的线
程数量不确定。若有空闲的线程可以复用,则会优先使用可复用的线程;若所有线程都在工作,又有新的
任务提交,则会创建新的线程处理任务,没有个数上限。
适应场景:大小无界的线程池,适用于执行很多的短期任务的小程序,或者负载较轻的服务器。
8.简述 synchronized 和 java.util.concurrent.locks.Lock 的区别。
(1) Lock 能完成 synchronized 所能实现 Lock 相对 synchronized 线程有更直观的语义和更好的性能
(2) Lock 是重入锁,即一个线程可以多次获取同一个锁,重入锁的性能相对 synchronized 较高;不过从 JDK6.0
开始,JDK 在 synchronized 做了大量的优化,使得两者性能差距缩减
(3) Lock 需要利用 unlock()方法在 finally 中手动释放锁标记;而 synchronized 可以自动释放锁标记
9. 用 Java 写一个会导致死锁的程序,遇到死锁,你将如何解决?
①改变加锁的顺序
②利用 Lock 中的 tryLock()方法试图获取锁标记
③尽可能避免锁的嵌套使用
10.线程有哪几种状态?
1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态
。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,
并不是说执行了t.start()此线程立即就会执行;
3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,
即进入到运行状态。注:就
绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,
停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
Java WEB
JVM
集合
1.HashMap和HashTable区别?
- 线程安全性不同
HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronize的,在多线程并发的情况下,可以直接使用HashTable,但是使用HashMap时必须自己增加同步处理。
- 是否提供contains方法
HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。
- key和value是否允许null值
Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
- 数组初始化和扩容机制
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
2.TreeSet和HashSet区别
HashSet是采用hash表来实现的。其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为O(1)的方法。
TreeSet是采用树结构实现(红黑树算法)。元素是按顺序进行排列,但是add()、remove()以及contains()等方法都是复杂度为O(log (n))的方法。它还提供了一些方法来处理排序的set,如first(),last(),headSet(),tailSet()等等。
3.String buffer和String build区别?
1、StringBuffer与StringBuilder中的方法和功能完全是等价的。
2、只是StringBuffer中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而StringBuilder没有这个修饰,可以被认为是线程不安全的。
3、在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低
4.Final,Finally,Finalize
final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。
finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。
5.== 与equals的区别?
== : 如果比较的是基本数据类型,那么比较的是变量的值
如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)
equals:如果没重写equals方法比较的是两个对象的地址值。
如果重写了equals方法后我们往往比较的是对象中的属性的内容
equals方法是从Object类中继承的,默认的实现就是使用==
6.如果遍历数组,什么情况下选用foreach,什么情况下选用for循环?
当如果你的操作中涉及到[下标]操作时,用for最好。
当你只是查看元素的内容,那么选foreach更简洁一些。
7.如果遍历Collection系列集合,什么情况下选用foreach,是否能选用for循环?
首先考虑使用foreach,如果该集合也有索引信息的话,也可以通过for来操作,如果没有下标的信息,就不要用for。即,如果该集合的物理结构是数组的,那么可以用for,如果物理结构是链式,那么使用下标操作效率很低。
如果遍历Collection系列集合,什么情况下选用foreach,什么情况下使用Iterator?
8.如果遍历Collection系列集合,什么情况下选用foreach,什么情况下使用Iterator?
如果只是查看集合的元素,使用foreach,代码会更简洁。
但是如果要涉及到在遍历集合的同时根据某种条件要删除元素等操作,那么选用Iterator。
9明确使用Iterator迭代器
Collection c = ....;
Iterator iter = c.iterator();
while(iter.hashNext()){
Object obj = iter.next();
//...
}
Iterator 接口的方法:
(1)boolean hasNext()
(2)Object next()
(3)void remove()
10.List的实现类们的区别
ArrayList、Vector、LinkedList、Stack
(1)ArrayList、Vector:都是动态数组
Vector是最早版本的动态数组,线程安全的,默认扩容机制是2倍,支持旧版的迭代器Enumeration
ArrayList是后增的动态数组,线程不安全的,默认扩容机制是1.5倍
(2)动态数组与LinkedList的区别
动态数组:底层物理结构是数组
优点:根据[下标]访问的速度很快
缺点:需要开辟连续的存储空间,而且需要扩容,移动元素等操作
LinkedList:底层物理结构是双向链表
优点:在增加、删除元素时,不需要移动元素,只需要修改前后元素的引用关系
缺点:我们查找元素时,只能从first或last开始查找
(3)Stack:栈
是Vector的子类。比Vector多了几个方法,能够表现出“先进后出或后进先出”的特点。
①Object peek():访问栈顶元素
②Object pop():弹出栈顶元素
③push():把元素压入栈顶
(4)LinkedList可以作为很多种数据结构使用
单链表:只关注next就可以
队列:先进先出,找对应的方法
双端队列(JDK1.6加入):两头都可以进出,找对应的方法
栈:先进后出,找对应的方法
建议:虽然LinkedList是支持对索引进行操作,因为它实现List接口的所有方法,但是我们不太建议调用类似这样的方法,因为效率比较低
11.Set
Set系列的集合:不可重复的
Set系列的集合,有有序的也有无序的。HashSet无序的,TreeSet按照元素的大小顺序遍历,LinkedHashSet按照元素的添加顺序遍历。
12.Set 实现类的特点
(1)HashSet:
底层是HashMap实现。添加到HashSet的元素是作为HashMap的key,value是一个Object类型的常量对象PRESENT。
依赖于元素的hashCode()和equals()保证元素的不可重复,存储位置和hashCode()值有关,根据hashCode()来算出它在底层table数组中的[index]
(2)TreeSet
底层是TreeMap实现。添加到TreeSet的元素是作为TreeMap的key,value是一个Object类型的常量对象PRESENT。
依赖于元素的大小,要么是java.lang.Comparable接口compareTo(Object obj),要么是java.util.Comparator接口的compare(Object o1, Object o2)来比较元素的大小。认为大小相等的两个元素就是重复元素。
(3)LinkedHashSet
底层是LinkedHashMap。添加到LinkedHashSet的元素是作为LinkedHashMap的key,value是一个Object类型的常量对象PRESENT。
LinkedHashSet是HashSet的子类,比父类多维护了元素的添加顺序。
当且仅当,你既想要元素不可重复,又要保证元素的添加顺序时,再使用它。
13Map概述
用来存储键值对,映射关系的集合。所有的Map的key都不能重复。
键值对、映射关系的类型:Entry类型
Entry接口是Map接口的内部接口。所有的Map的键值对的类型都实现了这个接口。
HashMap中的映射关系,是有一个内部类来实现Entry的接口,JDK1.7是一个叫做Entry的内部类实现Entry接口。
JDK1.8是一个叫做Node的内部类实现Entry接口。
TreeMap中的映射关系,是有一个内部类Entry来实现Entry的接口
14.Map的API
(1)put(Object key, Object value):添加一对映射关系
(2)putAll(Map m):添加多对映射关系
(3)clear():清空map
(4)remove(Object key):根据key删除一对
(5)int size():获取有效元素的对数
(6)containsKey(Object key):是否包含某个key
(7)containsValue(Object value):是否包含某个value
(8)Object get(Object key):根据key获取value
(9)遍历相关的几个方法
Collection values():获取所有的value进行遍历
Set keySet():获取所有key进行遍历
Set entrySet():获取所有映射关系进行遍历
15.Map的实现类们的区别
(1)HashMap:
依据key的hashCode()和equals()来保证key是否重复。
key如果重复,新的value会替换旧的value。
hashCode()决定了映射关系在table数组中的存储的位置,index = hash(key.hashCode()) & table.length-1
HashMap的底层实现:JDK1.7是数组+链表;JDK1.8是数组+链表/红黑树
(2)TreeMap
依据key的大小来保证key是否重复。key如果重复,新的value会替换旧的value。
key的大小依赖于,java.lang.Comparable或java.util.Comparator。
(3)LinkedHashMap
依据key的hashCode()和equals()来保证key是否重复。key如果重复,新的value会替换旧的value。
LinkedHashMap是HashMap的子类,比HashMap多了添加顺序
16.HashMap添加过程
(1)当第一次添加映射关系时,数组初始化为一个长度为16的**HashMap N o d e ∗ ∗ 的 数 组 , 这 个 H a s h M a p Node**的数组,这个HashMap Node∗∗的数组,这个HashMapNode类型是实现了java.util.Map.Entry接口
(2)在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中
JDK1.8关于hash(key)方法的实现比JDK1.7要简洁。 key.hashCode() ^ key.Code()>>>16;
(3)计算index = table.length-1 & hash;
(4)如果table[index]下面,已经有映射关系的key与我要添加的新的映射关系的key相同了,会用新的value替换旧的value。
(5)如果没有相同的,
①table[index]链表的长度没有达到8个,会把新的映射关系添加到链表的尾
②table[index]链表的长度达到8个,但是table.length没有达到64,会先对table进行扩容,然后再添加
③table[index]链表的长度达到8个,并且table.length达到64,会先把该分支进行树化,结点的类型变为TreeNode,然后把链表转为一棵红黑树
④table[index]本来就已经是红黑树了,那么直接连接到树中,可能还会考虑考虑左旋右旋以保证树的平衡问题
(6)添加完成后判断if(size > threshold ){
①会扩容
②会重新计算key的hash
③会重新计算index
}
2、remove(key)
(1)计算key的hash值,用这个方法hash(key)
(2)找index = table.length-1 & hash;
(3)如果table[index]不为空,那么就挨个比较哪个Entry的key与它相同,就删除它,把它前面的Entry的next的值修改为被删除Entry的next
(4)如果table[index]下面原来是红黑树,结点删除后,个数小于等于6,会把红黑树变为链表
关于HashMap的面试问题
1、HashMap的底层实现
答:JDK1.7是数组+链表,JDK1.8是数组+链表/红黑树
2、HashMap的数组的元素类型
答:java.util.Map$Entry接口类型。
JDK1.7的HashMap中有内部类Entry实现Entry接口
JDK1.8的HashMap中有内部类Node和TreeNode类型实现Entry接口
3、为什么要使用数组?
答:因为数组的访问的效率高
4、为什么数组还需要链表?或问如何解决hash或[index]冲突问题?
答:为了解决hash和[index]冲突问题
(1)两个不相同的key的hashCode值本身可能相同
(2)两个hashCode不相同的key,通过hash(key)以及 hash & table.length-1运算得到的[index]可能相同
那么意味着table[index]下可能需要存储多个Entry的映射关系对象,所以需要链表
5、HashMap的数组的初始化长度
答:默认的初始容量值是16
6、HashMap的映射关系的存储索引index如何计算
答:hash & table.length-1
7、为什么要使用hashCode()? 空间换时间
答:因为hashCode()是一个整数值,可以用来直接计算index,效率比较高,用数组这种结构虽然会浪费一些空间,但是可以提高查询效率。
8、hash()函数的作用是什么
答:在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中
JDK1.8关于hash(key)方法的实现比JDK1.7要简洁。 key.hashCode() ^ key.Code()>>>16; 因为这样可以使得hashCode的高16位信息也能参与到运算中来
9、HashMap的数组长度为什么一定要是2的幂次方
答:因为2的n次方-1的二进制值是前面都0,后面几位都是1,这样的话,与hash进行&运算的结果就能保证在[0,table.length-1]范围内,而且是均匀的。
10、HashMap 为什么使用 &按位与运算代替%模运算?
答:因为&效率高
11、HashMap的数组什么时候扩容?
答:JDK1.7版:当要添加新Entry对象时发现(1)size达到threshold(2)table[index]!=null时,两个条件同时满足会扩容
JDK1.8版:当要添加新Entry对象时发现(1)size达到threshold(2)当table[index]下的结点个数达到8个但是table.length又没有达到64。两种情况满足其一都会导致数组扩容
而且数组一旦扩容,不管哪个版本,都会导致所有映射关系重新调整存储位置。
12、如何计算扩容阈值(临界值)?
答:threshold = capacity * loadfactor
13、loadFactor为什么是0.75,如果是1或者0.1呢有什么不同?
答:1的话,会导致某个table[index]下面的结点个数可能很长
0.1的话,会导致数组扩容的频率太高
14、JDK1.8的HashMap什么时候树化?
答:当table[index]下的结点个数达到8个但是table.length已经达到64
15、JDK1.8的HashMap什么时候反树化?
答:当table[index]下的树结点个数少于6个
16、JDK1.8的HashMap为什么要树化?
答:因为当table[index]下的结点个数超过8个后,查询效率就低下了,修改为红黑树的话,可以提高查询效率
17、JDK1.8的HashMap为什么要反树化?
答:因为因为当table[index]下树的结点个数少于6个后,使用红黑树反而过于复杂了,此时使用链表既简洁又效率也不错
18、作为HashMap的key类型重写equals和hashCode方法有什么要求
(1)equals与hashCode一起重写
(2)重写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。
(3)重写hashCode()的注意事项
- 如果equals返回true的两个对象,那么hashCode值一定相同,并且只要参与equals判断属性没有修改,hashCode值也不能修改;
如果equals返回false的两个对象,那么hashCode值可以相同也可以不同;
如果hashCode值不同的,equals一定要返回false;
hashCode不宜过简单,太简单会导致冲突严重,hashCode也不宜过于复杂,会导致性能低下;
19、为什么大部分 hashcode 方法使用 31?
答:因为31是一个不大不小的素数
20、请问已经存储到HashMap中的key的对象属性是否可以修改?为什么?
答:如果该属性参与hashCode的计算,那么不要修改。因为一旦修改hashCode()已经不是原来的值。
而存储到HashMap中时,key的hashCode()–>hash()–>hash已经确定了,不会重新计算。用新的hashCode值再查询get(key)/删除remove(key)时,算的hash值与原来不一样就不找不到原来的映射关系了。
21、所以为什么,我们实际开发中,key的类型一般用String和Integer
答:因为他们不可变。
22、为什么HashMap中的Node或Entry类型的hash变量与key变量加final声明?
答:因为不希望你修改hash和key值
23、为什么HashMap中的Node或Entry类型要单独存储hash?
答:为了在添加、删除、查找过程中,比较hash效率更高,不用每次重新计算key的hash值
24、请问已经存储到HashMap中的value的对象属性是否可以修改?为什么?
答:可以。因为我们存储、删除等都是根据key,和value无关。
25、如果key是null是如何存储的?
答:会存在table[0]中
有惊喜哟~
关注公众号,随时学习最新内容,并与博主零距离交流!
持续更新中。。。