面试问题碎碎念
二进制指数退避算法
1)确定基本退避时间(基数),一般定为2τ,也就是一个争用期时间,对于以太网就是51.2μs
2)定义一个参数K,为重传次数,K=min[重传次数,10],可见K≤10
3)从离散型整数集合[0,1,2,……,(2^k-1)]中,随机取出一个数记做R
那么重传所需要的退避时间为R倍的基本退避时间:即:T=R×2τ。
4)同时,重传也不是无休止的进行,当重传16次不成功,就丢弃该帧,传输失败,报告给高层协议
编译器为什么要做指令重排呢
而一段代码并不是由单条指令就可以执行完毕的,而是通过流水线来执行多条指令。
计算机对指令的执行使用到了流水线技术。流水线技术是一种将指令分解为多步,并让不同指令的各步操作重叠,从而实现几条指令并行处理。
指令1 IF ID EX MEN WB
指令2 IF ID EX MEN WB
指令的每一步都由不同的硬件完成,假设每一步耗时1ms,执行完一条指令需耗时5ms,
每条指令都按顺序执行,那两条指令则需10ms。
但是通过流水线在指令1刚执行完IF,执行IF的硬件立马就开始执行指令2的IF,这样指令2只需要等1ms,两个指令执行完只需要6ms,效率是不是提升巨大!
现有R1,R2,R3三个寄存器,
LW R1,B IF ID EX MEN WB(加载B到R1中)
LW R2,C IF ID EX MEN WB(加载C到R2中)
ADD R3,R2,R1 IF ID × EX MEN WB(R1,R2相加放到R3)
SW A,R3 IF ID x EX MEN WB(把R3 的值保存到变量A)
在ADD指令执行中有个x,表示中断、停顿,ADD为什么要在这里停顿一下呢?因为这时C还没加载到R2中,只能等待,而这个等待使得后边的所有指令都会停顿一下。
这个停顿可以避免吗?
当然是可以的,通过指令重排就可以实现,再看一下下面的例子:
要执行
A=B+C;
D=E-F;
通过将D=E-F执行的指令顺序提前,从而消除因等待加载完毕的时间。
通过指令重排可以优化指令的执行顺序,如消除缓存,减少指令的执行时间。
HTTPS如何实现加密,安全传输的?(SSL/TSL协议的运行方式)
1、作用
不使用SSL/TSL通信的HTTP,都是使用的明文进行通信的,是不安全的可能带来以下安全问题
(1)、窃听风险:中间人获取通信内容
(2)、篡改风险:中间人修改通信内容
(3)、冒充风险:中间人冒充通信对方
使用SSL/TSL通信的HTTPS,针对上面HTTP产生的安全问题,希望解决
(1)、将信息由明文传输变成加密传输,解决窃听风险
(2)、具有校验机制,被篡改可以及时发现,解决篡改风险
(3)、使用数字证书,解决冒充风险
2、关键概念
对称加密
对称加密使用通俗的语言讲,就像我们平时使用的钥匙,要把锁,使用A加密之后,就得使用A进行解锁,加锁与解锁都是使用A密钥
非对称加密
非对称加密的意思是,有两把密钥,分别是公钥(public key)A 和 私钥(private key)A',使用A加密,只用使用A'才能解密;反之,使用A'加密,只有使用A才能解密;它们二者都具有加密与解密的功能。
3、几种加密方式的比较以及存在的问题
现有浏览器(Browser)与 服务器(Server)
使用“对称加密”方式
服务器以“明文”的方式传给浏览器自己的密钥A,浏览器收到后,使用密钥A对数据包进行加密,然后传输给服务器,服务器收到后使用自己的密钥A进行解密。过程简单,但是安全问题很明显,我在中间截获,轻而易举就会获得密钥A,然后对于后来传输的数据随便处理了!!
问题出在哪?是因为明文传输公钥A,可能会被截获!!而浏览器又不可能实现存储所有网站的密钥!!
使用“非对称加密”方式
服务器以“明文”的方式传给浏览器自己的公钥A,浏览器收到后,使用公钥A对数据包进行加密,然后传输给服务器,服务器收到后使用自己的密钥A'进行解密。过程简单,但是安全问题很明显,我在中间截获,轻而易举就会获得密钥A,然后对于后来传输的数据随便处理了!!
问题出在哪?是因为明文传输公钥A,可能会被截获!!而浏览器又不可能实现存储所有网站的公钥!!
使用“改良的非对称加密”方式
现在浏览器和服务器双方各自有有属于自己的公钥与私钥,浏览器:公钥A,私钥A' ; 服务器:公钥B,私钥B' ;
(1)、服务器使用“明文”的方式给浏览器自己的公钥B
(2)、浏览器使用“明文”的方式给服务器自己的公钥A
(3)、服务器给浏览器发数据,服务器使用A进行加密,由于只有浏览器有私钥A',所以只用浏览器才能解密,才能查看数据
(4)、浏览器给服务器发数据,浏览器使用B进行加密,由于只有服务器有私钥B',所以只有服务器才能解密,才能查看数据
问题出在哪?HTTPS并没有使用这一种方式,原因在于非对称加密方式比较耗费时间,特别是较大数据的时候,而对称加密的方式相比之下要快的多,于是是否能将 对称加密和非对称加密 这两种方式结合起来呢??
使用“对称加密+非对称加密”的方式
现在服务器有非对称密钥:公钥B和密钥B'; 浏览器有对称密钥X
(1)、服务器使用“明文”的方式传给浏览器自己的公钥B
(2)、浏览器使用公钥B加密,携带自己的对称密钥X,传给服务器
(3)、因为只用服务器端拥有私钥B',所以只用服务器能够解密使用公钥B加密后的数据,从而拿到浏览器的对称密钥X
(4)、至此,服务器和浏览器之间的数据传送都使用对称密钥进行加密,实现安全传送
问题出在哪?有什么安全问题?(中间人攻击)
(1)、服务器使用“明文”的方式传给浏览器自己的公钥B
(2)、中间人劫持,保存服务器的公钥B,并且,莫名顶替将自己的公钥M传给浏览器
(3)、浏览器收到后,误以为是服务器传送过来的,使用公钥M加密,携带自己的对称密钥X,传给服务器
(4)、中间人继续劫持,使用自己的私钥解密浏览器传来的数据,从而得到浏览器的对称密钥X,然后使用服务器的公钥B加密,将浏览器的对称密钥X传送给服务器
(5)、服务器,收到中间人传来的加密数据,使用自己的私钥B’解密,得到浏览的对称密钥X,还傻呵呵的以为是浏览器发给自己的呢!!
(6)、浏览器与服务器丝毫没有察觉到中间人的存在,故发送的数据中间人可以随便支配!!
所以问题是?浏览器并不知道“明文”传送给自己的公钥是中间人的还是服务器的,中间人可以获取公钥并且更改公钥,进而对双向传输的内容进行篡改伪造等,如果确定自己收到的公钥是服务器的呢?由此引入“数字证书”!
4、数字证书的引入
数字证书概念
数字证书的引入就是确认传给浏览器的公钥就是服务器的,即证明公钥是属于服务器,就像我们的身份证一样。服务器的网站在使用HTTPS之前,必须想CA机构申请一份“数字证书”,数字证书上包含:证书持有者、证书持有者的公钥信息。
服务器的网站将数字证书发送给浏览器就可以了,浏览器就会获取其公钥信息。但是,数字证书被截获了怎么办?里面的公钥信息并篡改了怎么办?因此我们引入了“数字签名”!!
数字签名概念
数字签名是用来确保明文 数字证书 没有被修改的一种手段,如何证明?见下面数字签名的生成过程。
数字签名的生成
如图所示,数字签名生成步骤如下:
(1)、CA机构拥有一对非对称密钥
(2)、CA机构对明文的数字证书进行hash,得到散列值
(3)、CA机构,使用自己的私钥加密以上生成的散列值
CA机构
通俗的讲,CA机构即 Certificate Authority证书授权机构,就像我们的办理身份证的政府一样是可以信赖的机构
5、如何配合数字证书实现HTTPS(流程)?
浏览如何验证收到的数字证书是服务器发过来的,没有并修改呢?
如图所示浏览收到传来的数字证书(数字证书明文+数字签名)后,
(1)、处理数字签名,使用CA机构的公钥解密获得散列hash值
(2)、处理数字证书,使用证书明文里面说的hash算法对数字证书明文进行hash,得到散列hash值
(3)、比对两者的散列值是否相等,如果相等,则表明证书可以信赖,没有被中间人截获篡改
假设数字证书被中间人劫持修改里面的明文?
如果中间人劫持了数字证书,修改了里面数字证书的明文,但是注意,中间人并没有CA机构的“私钥”,因此并不能产生相对应的数字签名,因此即使修改了里面的明文,到了浏览器这边也会被出现,从而,判断此证书不可信(也就是此时传来的公钥可能不是服务器网站的公钥)
你怎么拥有CA机构的公钥?你怎么确定你自己的公钥是正确可信的?
上文直接写了使用CA机构的公钥解密,但是我怎么会有CA机构的公钥呢?原因是操作系统和浏览器会预装一下它们信任 的“根证书”,其中有CA机构的根证书,因此也就有了CA机构的公钥了!!而除非你的CA机构被篡改了,否则一定是正确可信的!!
假设数字证书被中间人劫持将证书换成自己服务器网站的证书呢( 将数字证书(明文+数字签名)整个掉包换了怎么办 )?
这样就更不需要担心了,数字证书里面包含有关于服务器网站的信息,浏览器可以通过这个数字整数的持有者判断是不是我们请求的服务器网站
6、HTTPS在每次的请求中都要使用SSL/TLS,进行握手传送密钥?
不用,可以使用session技术,每次请求都要进行一次密钥传输显然是比较耗时的,服务器会为每一个和自己建立连接的浏览器维护一个session,在TSL握手阶段传给浏览器session id,浏览器生成密钥之后,将密钥传给服务器网站,服务器网站会将传来的密钥信息保存在session中,之后浏览器的每次请求只需要携带session id即可,服务器网站就会根据session id查找密钥信息进行加密与解密的操作,这样就不需要每次都重新制作与传送密钥了!!
强、软、弱、虚引用的区别和使用
强引用(StrongReference)
StringBuildersb=newStringBuilder();上面通过在堆中创建实例,然后赋值给栈中局部变量 sb 的方式 就是 强引用。强引用有如下特点:
- 强引用可以直接访问目标对象
- 强引用(存在)指向的对象任何时候都不会被回收,JVM宁愿抛出OOM异常,也不会回收。
- 强引用可能会导致内存泄漏
内存泄漏(memory leak) 是指 程序申请内存后,无法释放已申请的内存空间,这样的泄漏积少成多,memory leak 会导致 out of memory .
链接:https://zhuanlan.zhihu.com/p/85576999
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
软引用(SoftReference)
软引用对应的类为 java.lang.ref.SoftReference, 一个软引用中的对象,不会很快被JVM回收,JVM会根据当前堆的使用情况来判断何时回收,当堆的使用率超过阈值时,才回去回收软引用中的对象。
先通过一个例子来了解一下软引用:
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj);
//删除强引用
obj = null;
//调用gc
System.gc();
System.out.println("gc之后的值:" + softRef.get()); // 对象依然存在
软引用也可以和一个引用队列联合使用,如果软引用中的对象(obj)被回收,那么软引用会被 JVM 加入关联的引用队列中。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj,queue);
//删除强引用
obj = null;
//调用gc
System.gc();
System.out.println("gc之后的值: " + softRef.get()); // 对象依然存在
//申请较大内存使内存空间使用率达到阈值,强迫gc
byte[] bytes = new byte[100 * 1024 * 1024];
//如果obj被回收,则软引用会进入引用队列
Reference<?> reference = queue.remove();
if (reference != null){
System.out.println("对象已被回收: "+ reference.get()); // 对象为null
}
引用队列(ReferenceQueue)作用
Queue的意义在于我们在外部可以对queue中的引用进行监控,当引用中的对象被回收后,我们可以对引用对象本身继续做一些清理操作,因为我们引用对象(softRef)也占有一定的资源。
链接:https://zhuanlan.zhihu.com/p/85576999
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
弱引用(WeakReference)
弱引用中的对象具有很短的声明周期,因为在系统GC时,只要发现弱引用,不管堆空间是否足够,都会将对象进行回收。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用的简单使用:
Object obj = new Object();
WeakReference weakRef = new WeakReference<Object>(obj);
//删除强引用
obj = null;
System.out.println("gc之后的值:" + weakRef.get()); // 对象依然存在
//调用gc
System.gc();
System.out.println("gc之后的值:" + weakRef.get()); // 对象为null
弱引用也可以和一个引用队列联合使用,如果弱引用中的对象(obj)被回收,那么软引用会被 JVM 加入关联的引用队列中。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
WeakReference weakRef = new WeakReference<Object>(obj,queue);
//删除强引用
obj = null;
System.out.println("gc之后的值: " + weakRef.get()); // 对象依然存在
//调用gc
System.gc();
//如果obj被回收,则软引用会进入引用队列
Reference<?> reference = queue.remove();
if (reference != null){
System.out.println("对象已被回收: "+ reference.get()); // 对象为null
}
软引用和弱引用都非常适合保存那些可有可无的缓存数据,当内存不足时,缓存数据被回收(再通过备选方案查询),当内存充足时,也可以存在较长时间,起到加速的作用。
应用
- WeakHashMap
当key只有弱引用时,GC发现后会自动清理键和值,作为简单的缓存表解决方案。
- ThreadLocal
ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。
虚引用(PhantomReference)
虚引用 就是 形同虚设 ,它并不能决定 对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收,因为虚引用的对象可以看做没有引用对象指向他,因此随时可以被回收。因此,虚引用主要用来跟踪对象被垃圾回收器的回收,清理被销毁对象的相关资源。PhantomReference的 get() 方法永远返回null ,而且只提供了与引用队列同用的构造函数。所以虚引用必须和引用队列一同使用,当垃圾回收器回收对象时发现还有虚引用就把虚引用放到队列里面,引用队列的引用对象也占用资源,可以进一步进行清理。
Map<Object, String> map = new HashMap<>();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference phantomRef = new PhantomReference<Object>(obj,queue);
map.put(obj,"obj val");
new CheckRefQueue(queue,map).start();
//删除强引用
obj = null;
Thread.sleep(1000);
int i = 1;
while (true){
System.out.println("第"+i+"次gc");
System.gc();
Thread.sleep(1000);
}
public class CheckRefQueue extends Thread {
private ReferenceQueue queue;
private Map<Object, String> map;
public CheckRefQueue(ReferenceQueue queue, Map<Object, String> map) {
this.queue = queue;
this.map = map;
}
@Override
public void run() {
// 等待,直到对象呗回收
Reference reference = queue.remove();
// 释放引用对象的引用
map.remove(reference.get());
}
}
总结
如何判断一个对象是否可以垃圾回收器被回收?
两大算法
1.引用计数法
每个对象都维护一个引用计数器,每个此对象被引用的时候就将计数器加一,当对象被取消引用的时候就将计数器减一,如果计数器为0,那么认为这个对象是垃圾,可以被回收。
引用计数法的缺点:
每个对象都有一个引用计数器,维护这个引用计数器有一定的资源消耗
无法解决循环引用的问题(假如说A对象中引用了B,B对象中引用了A,其他的所有对象都没有引用A和B对象,这个时候A和B对象其实就是垃圾,可以被回收,但是由于采用引用计数法,A和B对象的引用计数器都为1,所以垃圾回收器认为A和B对象不是垃圾,无法对他们进行回收)
2.GC roots可达性算法
目前主流的商用JVM都是通过可达性分析来判断对象是否可以被回收的。
这个算法的基本思路是:
通过一系列被称为「GC
Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC
Roots没有任何引用链相连,就说明该对象不可达,即可以被回收
初看这段话是不是一脸懵呢?笔者当初也是的,完全不知道什么意思,后面才慢慢理解。
要想理解可达性算法,首先要想明白几个问题:
1、什么是对象可达?
对象可达指的就是:双方存在直接或间接的引用关系。 根可达或GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。
如下代码:
public class MyObject { private String objectName;//对象名 private MyObject refrence;//依赖对象 public MyObject(String objectName) { this.objectName = objectName; } public MyObject(String objectName, MyObject refrence) { this.objectName = objectName; this.refrence = refrence; } public static void main(String[] args) { MyObject a = new MyObject("a"); MyObject b = new MyObject("b"); MyObject c = new MyObject("c"); a.refrence = b; b.refrence = c; new MyObject("d", new MyObject("e")); } }
创建了5个对象,他们之间的引用关系如图:
假设a是GC Roots的话,那么b、c就是可达的,d、e是不可达的。
2、GC Roots是什么?
垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。
然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。
那么,到底什么是GC Roots呢?
GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。
只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛。
当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC
Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。
即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。
GC Roots是一种特殊的对象,是Java程序在运行过程中所必须的对象,而且是根对象。
那么,哪些对象可以成为GC Roots呢?
3、哪些对象可以作为GC Roots?
可以作为GC Roots的对象可以分为两大类:全局对象和执行上下文。
下面就一起来理解一下为什么这几类对象可以被作为GC Roots。
1、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
2、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。
3、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。
4、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。
5、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。
尾巴
总结,可达性分析就是JVM首先枚举根节点,找到一些为了保证程序能正常运行所必须要存活的对象,然后以这些对象为根,根据引用关系开始向下搜寻,存在直接或间接引用链的对象就存活,不存在引用链的对象就回收。
java四种权限修饰符
访问权限修饰符
①public:意为公开的,访问权限最高,可以跨包访问。
②protect:意为受保护的,权限次之,可以在同包和子/父类中访问。
③default:意为默认的,一般不写,权限次之,可以在同包中访问。
④private:意为私有的,权限最低,只能在本类中访问。
所以,为了保证安全性,一般把访问权限降到最低。
四种访问权限修饰符如下图:
2.abstract(抽象)修饰符
①abstract修饰类,会使这个类成为一个抽象类,这个类将不能生成对象实例,但可以做为对象变量声明的类型,也就是编译时类型,抽象类就像当于一类的半成品,需要子类继承并覆盖其中的抽象方法。
②abstract修饰方法,会使这个方法变成抽象方法,也就是只有声明(定义)而没有实现,实现部分以”;”代替。需要子类继承实现(覆盖)。
举例:
abstract class E{ public void method1(){ /*code*/ }; public abstract void method2();//public abstract 可以省略 } class F extends E{ //实现抽象方法method2 void method2(){ //写具体实现的代码 } //method1不是抽象方法,不需要具体实现,但如果写了具体实现的方法会覆盖原有方法 }
最后再主方法里面定义一个父类引用指向子类对象,就会发生多态现象,比如
E e=new F();//E是抽象父类,F是E的继承类
e.show();//实际调用了子类里面的method2()方法
总结:
①抽象类和普通类差不多,只是不能用new实例化,必须要继承。
②抽象的方法所在类一定是抽象类;抽象类中的方法不一定都是抽象方法。
③抽象类中的抽象方法必须在子类中写具体实现的方法;如果抽象类中的方法是具体方法,那么重写该方法会覆盖原方法。
3.final修饰符
final变量必须被显式初始化,并且只能被赋值一次值
final修饰基本类型变量的时候, 该变量不能重新赋值
final修饰引用类型变量的时候, 该变量不能重新指向其他对象
final修饰的方法为最终的方法, 该方法不能被重写
private类型的方法都默认为是final方法,因而也不能被子类重写
final修饰的类为最终的类, 不能被继承
4.static修饰符
如果声明了静态方法或变量,值是放在方法区,因为方法区是一个数据共享区;所以不管什么变量访问它,都是同一份.
在静态方法中不能直接访问实例方法和实例变量.
在静态方法中不能使用this和super关键字.
静态方法不能被abstract修饰.
静态的成员变量可以使用类名或者是对象进行访问,非静态成员变量只能使用对象进行访问.
静态函数可以直接访问静态的成员,但是不能够直接访问非静态成员.,非静态函数可以访问静态和非静态成员.
当类被加载时,静态代码块只能被执行一次。类中不同的静态方法代码块按他们在类中出现的顺序被依次执行.
当多个修饰符连用时,修饰符的顺序可以颠倒,不过作为普遍遵守的编码规范,通常把访问控制修饰符放在首位,其次是static或abstact修饰符,接着就是其他的修饰符.
//这些修饰符放在一起是无效的
abstract与private
abstract与final
abstract与static
Java intern() 方法
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
语法
public String intern()
参数
-
无
返回值
一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。
实例
public class Test {
public static void main(String args[]) {
String Str1 = new String("www.runoob.com");
String Str2 = new String("WWW.RUNOOB.COM");
System.out.print("规范表示:" );
System.out.println(Str1.intern());
System.out.print("规范表示:" );
System.out.println(Str2.intern());
}
}
以上程序执行结果为:
规范表示:www.runoob.com
规范表示:WWW.RUNOOB.COM
Java 判断两个对象是否相等
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true (即,认为这两个对象相等)。
举个例子:
说明:
- String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
- 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
为什么重写equals()方法就必须重写hashcode()方法?
equals()方法和hashcode()方法都属于Object类,在Java中,所有的类都是Object类的子类,也就是说,任何Java对象都可调用Object类的方法。
equals()方法:
public boolean equals(Object obj) {
return (this == obj);
}
很明显,该方法就是用来判断两个对象是否是同一个对象。在Object类源码中,其底层是使用了“==”来实现,也就是说通过比较两个对象的内存地址是否相同判断是否是同一个对象,实际上,该equals()方法通常没有太大的实用价值。而我们往往需要用equals()来判断 2个对象在逻辑上是否等价,而非验证它的唯一性。这样我们在实现自己的类时,就要重写equals()方法。
hashcode()方法:
public native int hashCode();
一提到hashcode,很自然就想到哈希表。将某一key值映射到表中的一个位置,从而达到以O(1)的时间复杂度来查询该key值。Object类源码中,hashCode()是一个native方法,哈希值的计算利用的是内存地址。
我们看一下Object类中关于hashCode()方法的注释
1.在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
2.如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
3.如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
通俗的讲,注释中第二点和第三点的含义就是equals()和hashcode()方法要保持相当程度的一致性,equals()方法相等,hashcode()必须相等;反之,equals方法不相等,hashcode可以相等,可以不相等。但是两者的一致有利于提高哈希表的性能。
所以,源码注释来看,两方法的同时重写是很必要的。
实际来看,不同时重写如何?
equals()相等的的两个等价对象因为hashCode不同,所以在hashmap中的table数组的下标不同,从而这两个对象就会同时存在于集合中,在调用hashmap集合中的方法时就会出现逻辑的错误,也就是,你的equals()方法也“白白”重写了。
因此,对于“为什么重写equals()就一定要重写hashCode()方法?”这个问题应该是有个前提,就是你需要用到HashMap,HashSet等Java集合。用不到哈希表的话,其实仅仅重写equals()方法也可以吧。而工作中的场景是常常用到Java集合,所以Java官方建议重写equals()就一定要重写hashCode()方法。
hashCode()与equals()的相关规定
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
Java中Integer类型装箱拆箱及其注意点
Java中,一般地,我们使用数据类型的时候,会直接使用int,double,byte,long等内置类型,但是在某些场合下,我们会遇到需要使用对象而非内置数据类型的情况,需要引用对象(为啥要拆箱和装箱)。 Java语言为每一个内置数据类型提供了包装类,这些包装类包括Intger,Double等,此文我们拿Integer进行分析。
这种由编译器特别支持的包装称为装箱,所以当内置数据类型被当做对象使用的时候,编译器会把内置类型装箱为包装类。相似的,编译器也可以把一个对象拆箱为内置类型。
public class Test{
public static void main(String args[]){
Integer x = 5;
x = x + 10;
System.out.println(x);
}}
在以上代码中,当我们为Integer类型的x赋值5的时候,编译器对5进行了装箱操作,相当于调用了Integer.valueOf(5)。而在x=x+10时候,编译器又对x进行了拆箱操作,因为只有内置类型才支持+操作。特别要注意的一点是,对于-128到127之间的数值,装箱后都会放在内存里进行重用,也就是说Integer a = 5和Integer b = 5,系统都会去复用同一个对象,所以a和b实际上指向同一个对象的地址。但如果超过了这个规定的访问,系统会重新new一个Integer对象出来,如Integer a = 555和Integer b = 555,此时a和b两个对象便指向了不同对象的地址。
sql中的字段,char与varchar这个是很多初学者都会疑惑的,今天我特地总结了一下,也是当作复习.
首先明确的是,char的长度是不可变的,而varchar的长度是可变的,定义一个char[10]和varchar[10],如果存进去的是‘zhihu’,那么char所占的长度依然为10,初‘zhihu’外,后面跟5个空格,而varchar就立马把长度变为5了,取数据的时候,char类型的要用trim()去掉多余的空格,而varchar是不需要的。
尽管如此,char的存取数度还是要比varchar要快得多,因为其长度固定,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以难免会有多余的空格占位符占据空间,可谓是以空间换取时间效率,而varchar是以空间效率为首位的。char的存储方式是,对英文字符(ASCII)占用1个字节,对一个汉字占用两个字节;而varchar的存储方式是,对每个英文字符占用2个字节,汉字也占用2个字节。两者的存储数据都非unicode的字符数据。
日期类型datetime和timestamp区别在哪里?
一、相同点
datetime和timestamp都可以表示 YYYY-MM-DD HH:MM:SS 这种年月日时分秒格式的数据。
并且从MySQL5.6.4之后这两者都可以包含秒后的小数部分,精度最高为微妙(6位)。
这里有一个点需要注意,就是在MySQL5.6.4之前,这两个是都表示不了小数的。
二、不同点
接下来来说下他们的不同点。
1)存储范围不同:
datetime的存储范围是 1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999,而timestamp的范围是 1970-01-01 00:00:01.000000到 2038-01-19 03:14:07.999999(准备的来讲应该是UTC范围);
如果我们存储timestamp的时候,存了不在它范围内的时间值时,会直接抛出异常。
2)时区相关:
datetime存储与时区无关(准备来说是datetime只支持一个时区,就是存储时当前服务器的时区),而timestamp存储的是与时区有关。MySQL在存储TIMESTAMP时,会先将时间从当前服务器的时区转换为UTC(世界协调时)以进行存储,然后查询时从UTC转换为当前时区以进行返回。也就是说使用timestamp进行存储的时间返回的时候会随着数据库的时区而发生改变。而datetime的存储则与时区无关,数据是什么就存储什么,也就返回什么。
3)存储大小:
在5.6.4之前,datetime存储占用8个字节,而timestamp是占用4字节;但是在5.6.4之后,由于这两个类型允许有小数部分,所以占用的存储空间和以前不同;MySQL规范规定,datetime的非小数部分需要5个字节,而不是8个字节,而timestamp的非小数部分是需要4个字节,并且这两个部分的小数部分都需要0到3个字节,具体取决于存储值的小数秒精度。
聚簇索引和非聚簇索引
Mysql 索引实现:
聚簇索引: 索引 和 数据文件为同一个文件。非聚簇索引: 索引 和 数据文件分开的索引。
MyISAM & InnoDB 都使用B+Tree索引结构。但是底层索引存储不同,MyISAM 采用非聚簇索引,而InnoDB采用聚簇索引。
MyISAM索引原理:采用非聚簇索引-MyISAM myi索引文件和myd数据文件分离,索引文件仅保存数据记录的指针地址。叶子节点data域存储指向数据记录的指针地址。(底层存储结构: frm -表定义、 myi -myisam索引、 myd-myisam数据)
InnoDB索引原理:
采用聚簇索引- InnoDB数据&索引文件为一个idb文件,表数据文件本身就是主索引,相邻的索引临近存储。 叶节点data域保存了完整的数据记录(数据[除主键id外其他列data]+主索引[索引key:表主键id])。 叶子节点直接存储数据记录,以主键id为key,叶子节点中直接存储数据记录。(底层存储结构: frm -表定义、 ibd: innoDB数据&索引文件)
https://www.cnblogs.com/henuliulei/p/15306257.html
主键索引和普通索引有什么区别?
在 MySQL 中, 索引是在存储引擎层实现的, 所以并没有统⼀的索引标准, 由于 InnoDB 存储引擎在 MySQL数据库中使⽤最为⼴泛, 下⾯以 InnoDB 为例来分析⼀下其中的索引模型.在 InnoDB 中, 表都是根据主键顺序以索引的形式存放的, InnoDB 使⽤了 B+ 树索引模型,所以数据都是存储在 B+ 树中的, 如图所示:
从图中可以看出, 根据叶子节点内容不同,索引类型分为主键索引和非主键索引.
主键索引也被称为聚簇索引,叶子节点存放的是整行数据; 而非主键索引被称为二级索引,叶子节点存放的是主键的值.
如果根据主键查询, 只需要搜索ID这颗B+树
而如果通过非主键索引查询, 需要先搜索k索引树, 找到对应的主键, 然后再到ID索引树搜索一次, 这个过程叫做回表.
总结, 非主键索引的查询需要多扫描一颗索引树, 效率相对更低.
尽管在输出中调用intern方法并没有什么效果,但是实际上后台这个方法会做一系列的动作和操作。在调用”ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。
可以看下面一个范例:
String str1 = "a"; String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; String str5 = new String("ab"); System.out.println(str5.equals(str3)); System.out.println(str5 == str3); System.out.println(str5.intern() == str3); System.out.println(str5.intern() == str4);
得到的结果:
true false true false
为什么会得到这样的一个结果呢?我们一步一步的分析。
String a = new String("ab"); String b = new String("ab"); String c = "ab"; String d = "a" + "b"; String e = "b"; String f = "a" + e; System.out.println(b.intern() == a); System.out.println(b.intern() == c); System.out.println(b.intern() == d); System.out.println(b.intern() == f); System.out.println(b.intern() == a.intern());
运行结果:
false true true false true
由运行结果可以看出来,b.intern() == a和b.intern() == c可知,采用new 创建的字符串对象不进入字符串池,并且通过b.intern() == d和b.intern() == f可知,字符串相加的时候,都是静态字符串的结果会添加到字符串池,如果其中含有变量(如f中的e)则不会进入字符串池中。但是字符串一旦进入字符串池中,就会先查找池中有无此对象。如果有此对象,则让对象引用指向此对象。如果无此对象,则先创建此对象,再让对象引用指向此对象。
当研究到这个地方的时候,突然想起来经常遇到的一个比较经典的Java问题,就是对比equal和==的区别,当时记得老师只是说“==”判断的是“地址”,但是并没说清楚什么时候会有地址相等的情况。现在看来,在定义变量的时候赋值,如果赋值的是静态的字符串,就会执行进入字符串池的操作,如果池中含有该字符串,则返回引用。
执行下面的代码:
String a = "abc"; String b = "abc"; String c = "a" + "b" + "c"; String d = "a" + "bc"; String e = "ab" + "c"; System.out.println(a == b); System.out.println(a == c); System.out.println(a == d); System.out.println(a == e); System.out.println(c == d); System.out.println(c == e);
运行的结果:
true true true true true true