写在前面:
找工作告一段落,期间经历了很多事情,也思考了许多问题,最后也收获了一些沉甸甸的东西 —— 成长和来自阿里、百度、京东(sp)、华为等厂的Offer。好在一切又回到正轨,接下来要好好总结一番才不枉这段经历,遂将此过程中笔者的一些笔试/面试心得、干货发表出来,与众共享之。在此特别要感谢CSDN以及广大朋友的支持,我将坚持记录并分享自己所学、所想、所悟,央请大家不吝赐教,提出您宝贵的意见和建议,以期共同探讨提高。
摘要:
本文对面试过程中经常会被问到的一些关于Java基础问题进行了梳理和总结,包括 JVM虚拟机、常用容器、设计原则与模式以及Java语言特性等基础知识点,一方面方便自己温故知新,另一方面也希望为找工作的同学们提供一个复习参考。考虑到篇幅太长,现将 《面试/笔试第五弹 —— Java面试问题集锦》 一文分为上下两篇:《面试/笔试第五弹 —— Java面试问题集锦(上篇)》和《面试/笔试第五弹 —— Java面试问题集锦(下篇)》。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
1、Struts2和SpringMVC的区别
设计理念:前者为有状态的Action(均为多例),Action对象属性字段承载请求、响应,后者一般为无状态的Controller,请求直接封装到方法的参数中;
集中访问点不同:都属于前端控制器,用于接收请求、处理请求和生成响应,但集中访问点不同,前者为Filter,后者为Servlet;
请求处理粒度不同:前者一个Action对应一个请求上下文,后者一个方法对应一个请求上下文,因此更容易实现Rest;
拦截器机制不同:Struts2和SpringMVC的拦截器机制均是对AOP理念的应用,但Struts2的interceptor机制是通过代理机制(ActionProxy)+责任链模式实现的,而SpringMVC的interceptor机制实现比较简单,其通过循环的方式在handler处理请求前后分别调用preHandle()方法和postHandle()方法对请求和响应进行处理,与Spring AOP、责任链模式等基本无关;
对Ajax的支持不同:前者需要插件或者手动转化,而后者集成了对Ajax请求的处理(HttpMessageConverter);
与Spring的整合:前者需要插件,后者无缝整合(子容器);
配置/效率:后者几乎是零配置,开发效率更高。
2、Spring中IoC的理解
关键词:超级大工厂,对象控制权,解耦对象间的依赖关系
超级大工厂,对象控制权由调用者移交给容器,使得调用者不必关心对象的创建和管理,专注于业务逻辑开发;
优秀的解耦方式,解耦对象间的依赖关系,避免通过硬编码的方式耦合在一起;
底层实现:反射机制;
3、Spring中AOP的理解
关键词:模块化、交叉关注点、横切性质的系统级业务
一种新的模块化方式,专门处理系统各模块中的交叉关注点问题,将具有横切性质的系统级业务提取到切面中,与核心业务逻辑分离(解耦);
便于系统的扩展,符合开-闭原则;
动态AOP的实现,Java动态代理(接口代理)与cglib(类代理),具体由Bean后处理器生成代理;
AOP理念实践:Spring AOP,Java Web Filter,Struts2 Interceptor, SpringMVC Interceptor,…
4、JVM 基础
1). 内存模型
(1). 程序计数器: 线程私有,CPU调度的基本单位,用于保证线程切换(程序能够在多线程环境下连续执行);
(2). 栈(服务Java方法虚拟机栈、服务Native方法的本地方法栈):线程私有,局部变量/引用,栈深度(SOF)/无法申请内存(OOM);
(3). 堆(Java代码可及的Java堆和JVM自身使用的方法区):线程共享,对象分配和回收主要区域,OOM
更多关于JVM内存模型相关内容,请参见博文《JVM 内存模型概述》。
2). 垃圾回收机制
(1). Stop-the-World
JVM由于要执行GC而停止了应用程序的执行称之为Stop-the-World,该情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。
(2). Java堆的回收
关于Java对象的回收主要考虑以下两个问题:哪些对象可以被回收、怎么回收(有哪些回收算法以及有哪些垃圾回收器)。
判断对象是否可被回收:引用计数法(相互引用)、可达性算法(对象的引用链是否可达,GCRoots)
垃圾回收算法:标记-清除算法(内存碎片)、复制算法(垃圾回收较为频繁、对象存活率较低的新生代)、标记-整理算法(垃圾回收不频繁、对象存活率较高的老年代)、分代收集算法
垃圾回收器:串行收集器(新生代、老年代)、并行收集器(新生代、老年代)、并行清除收集器(并发,新生代,追求高吞吐)、CMS收集器(并发,老年代,标记-清除算法,追求低停顿)、G1垃圾收集器(整个堆,追求低停顿)
GC Roots一般包括虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中类静态属性引用的对象、方法区中常量引用的对象。
分代收集算法的基本思想是 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。Minor GC 发生频率较高,不一定等 Eden区满了才触发;Major GC在老年代满时触发,对年轻代和老年代进行回收。
(3). 方法区的回收
对常量池的回收
对类型的卸载:该类的所有实例被回收,该类的ClassLoader被回收,不存在对该类的Class对象的引用
更多关于JVM垃圾回收机制相关内容,请参见博文《图解Java垃圾回收机制》。
3). OOM/SOF
OOM for Heap:内存泄露(GC Roots的引用链,对象的生命周期超出预期)或者内存溢出(调节JVM参数 -Xms,-Xmx 等)
OOM for Stack:一般在单线程程序中不会出现;在多线程环境下,无法申请到足够的内存去创建线程
SOF for Stack:程序是否有深度递归
OOM for Perm :用到像Spring等框架的时候,常常会动态地生成大量的类导致永久代不够用而导致OutOfMemoryError: PermGen Space异常(调大 -XX:MaxPermSize)
4、JVM 调优
JVM 调优的主要目标是使系统具有 高吞吐 、低停顿 的特点,其优化手段应从两方面着手:Java虚拟机 和 Java应用程序。前者指根据应用程序的设计通过虚拟机参数控制虚拟机逻辑内存分区的大小以使虚拟机的内存与程序对内存的需求相得益彰;后者指优化程序算法,降低GC负担,提高GC回收成功率。
以下是一些常用的JVM调优工具:
Jconsole 与 Visual VM
JConsole 与 Visual VM 都是JDK自带的 Java 性能分析器,可以从命令行或在 GUI shell 中运行,从而可以轻松使用 JConsole来监控 Java 应用程序性能和跟踪 Java 中的代码,其可以从JAVA_HOME/bin/这个目录下找到。使用 Jconsole 监测死锁示例如下:
6、责任链(CoR)模式
目的:请求的发送者与请求的处理者解耦,便于动态的重新组织链和分配责任
角色:抽象处理者、具体处理者、客户端
传统责任链(CoR)模式的缺点在于:具体处理角色存在着共同的实现责任链结构的行为行为,即责任链的建立和指派包含在实现角色的类中,并没有抽象出来,这直接导致责任链的指派不够灵活。因此,改进的CoR模式为:使用AOP理念将责任链结构的实现用切面抽象出来,使得各个对象只关注自身必须实现的功能性需求,准确地分离出责任链模式中不同角色的共同行为,例如,
改进后责任链(CoR)模式的应用是比较广泛的,包括 Java Web Filter(链式调用),Struts2 Interceptor(Action代理)和SpringMVC等。
7、单例模式
单例模式核心在于为整个系统提供一个唯一的实例,为整个系统提供一个全局访问点。单例模式从实现上可以分为饿汉式单例和懒汉式单例两种,前者天生就是线程安全的,后者则需要考虑线程安全性,常见的线程安全的懒汉式单例的实现有内部类式和双重检查式两种。下面给出单例模式几种常见的形式:
(1). 饿汉式单例
// 饿汉式单例
public class Singleton1 {
// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();
// 私有的构造方法
private Singleton1(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getSingleton1(){
return singleton1;
}
}
(2). 懒汉式单例
// 懒汉式单例
public class Singleton2 {
// 指向自己实例的私有静态引用
private static Singleton2 singleton2;
// 私有的构造方法
private Singleton2(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton2 getSingleton2(){
// 被动创建,在真正需要使用时才去创建
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
(3). 线程安全的懒汉式单例 —— 内部类方式
public class Singleton {
//静态私有内部类
private static class InnerClass {
private final static Singleton instance = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return InnerClass.instance;
}
}
内部类方式线程安全懒汉式单例的内在原理在于:虚拟机会保证一个类的类构造器
(4). 线程安全的懒汉式单例 —— 双重检查方式
public class Singleton {
// volatile: 防止指令重排序
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
// 第一次检查
if(instance == null){
// 只在最初几次会进入该同步块,提高效率
synchronized(Singleton.class){
// 第二次检查
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
8、类的生命周期及其初始化时机
类的生命周期主要包括加载、链接、初始化、使用和卸载五个阶段,如下图所示:
其中,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化,包括:
1). 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时:注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
使用new关键字实例化对象的时候;
读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
调用一个类的静态方法的时候。
2). 对类进行反射调用时:使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3). 初始化子类时:当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4). 虚拟机启动时:当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5). 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
9、类加载过程中各阶段的作用
1、 加载(Loading)
(1). 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
(2). 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3). 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
2、 验证(Verification):验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3、准备(Preparation):准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
4、解析(Resolution):解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
5、初始化(Initialization):初始化阶段是执行类构造器
10、对象的创建过程
在Java中,创建一个对象常常需要经历如下几个过程:父类的类构造器
11、双亲委派模型
双亲委派模型很好地解决了类加载器的统一加载问题:越基础的类由越上层的加载器进行加载,进而保证Java类型体系中最基础的行为,防止应用程序变得混乱。比如,java.lang.Object 类总是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类型(是否是同一类型由类加载器与类本身共同决定)。
12、异常机制
Java体系中异常的组织分类如下图所示,所有异常类型的根类为 Throwable,具体包括两大类:Error 与 Exception。其中,Error是指程序无法处理的错误,表示运行应用程序中较严重问题;Exception是指程序本身可以处理的错误,具体可分为运行时异常(派生于 RuntimeException 的异常) 和 其他异常。
此外,从异常是否必须需要被处理的角度来看,异常又可分为不受检查异常和受检查异常两种情况:
下面着重介绍一下使用finally子句,在对应的try子句执行的前提下,finally 子句总会被执行。并且,finally子句 总是在诸如return、break、throw和continue等控制转移语句之前执行。看以下几个经典例子:
1)、 try子句执行,finally子句必然执行
// 代码片段1
public class Test {
public static void main(String[] args) {
try {
System.out.println("try block");
return ;
} finally {
System.out.println("finally block");
}
}
}/* Output:
try block
finally block
*///:~
2)、 try子句执行,finally子句必然执行,且在控制转移语句之前执行
// 代码片段2
public class Test {
public static void main(String[] args) {
System.out.println("reture value of test() : " + test());
}
public static int test(){
int i = 1;
try {
System.out.println("try block");
i = 1 / 0;
return 1;
}catch (Exception e){
System.out.println("exception block");
return 2;
}finally {
System.out.println("finally block");
}
}
}/* Output:
try block
exception block
finally block
reture value of test() : 2
*///:~
3)、 try子句执行,finally子句必然执行,且在控制转移语句throw子句之前执行
// 代码片段3
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
// Using ‘return’ inside the finally block
// will silence any thrown exception.
return;
}
}
} ///:~
更多关于Java异常机制的相关内容请参见笔者博文《 Java 异常模型综述》。
13、七大设计原则
单一职责原则:高内聚,一个类只做它该做的事情;
接口隔离原则:接口小而专,避免大而全;
依赖倒置原则:依赖抽象而非实现,面向接口编程;
里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能;
开闭原则:Open for Extension, Closed for Modification,例如 AOP,代理模式,适配器模式就是其经典应用;
迪米特法则:高内聚,低耦合;
14、代理模式
根据代理类的创建时机和创建方式的不同,我们可以将代理模式分为静态代理和动态代理两种形式,其中,在程序运行前就已经存在的编译好的代理类是为静态代理,在程序运行期间根据需要动态的创建代理类及其实例来完成具体的功能是为动态代理。其中,代理对象的作用如下:
代理对象存在的价值主要用于拦截对真实业务对象的访问;
代理对象应该具有和目标对象(真实业务对象)相同的方法,即实现共同的接口或继承于同一个类;
代理对象应该是目标对象的增强,否则我们就没有必要使用代理了。
JDK 动态代理是动态代理模式的经典实现,主要包括三个角色对象:Subject (接口)、被代理的类以及InvocationHandler接口(一般持有被代理对象),例如:
public class ProxyHandler implements InvocationHandler {
private Object proxied; // 被代理对象
public ProxyHandler(Object proxied) {
this.proxied = proxied;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 在转调具体目标对象之前,可以执行一些功能处理
System.out.println("前置增强处理: yoyoyo...");
// 转调具体目标对象的方法(三要素:实例对象 + 实例方法 + 实例方法的参数)
Object obj = method.invoke(proxied, args);
// 在转调具体目标对象之后,可以执行一些功能处理
System.out.println("后置增强处理:hahaha...");
return obj;
}
}
// 真实对象real
Subject real = new RealSubject();
// 生成real的代理对象
Subject proxySubject = (Subject) Proxy.newProxyInstance(
Subject.class.getClassLoader(), new Class[] { Subject.class },
new ProxyHandler(real));
proxySubject.doSomething();
但是,JDK动态代理只能完成对接口的代理,而不能完成对类的代理,关键原因为:Java只允许单继承。具体地,代理对象proxySubject的类型为“com.sun.proxy.$Proxy0”,这恰好印证了proxySubject对象是一个代理对象。除此之外,我们还发现代理对象proxySubject所对应的类继承自java.lang.reflect.Proxy类,这也正是JDK动态代理机制无法实现对class的动态代理的原因。
15、迭代器模式
迭代器模式是与集合共生共死的。一般来说,我们只要实现一个容器,就需要同时提供这个容器的迭代器,使用迭代器的好处是:封装容器的内部实现细节,对于不同的集合,可以提供统一的遍历方式,简化客户端的访问和获取容器内数据。
特别需要注意的是,在迭代器模式中,具体迭代器角色和具体容器角色是耦合在一起的 —— 遍历算法是与容器的内部细节紧密相关的。为了使客户程序从与具体迭代器角色耦合的困境中脱离出来,避免具体迭代器角色的更换给客户程序带来的修改,迭代器模式抽象了具体迭代器角色,使得客户程序更具一般性和重用性,这被称为多态迭代。
在 Java Collection FrameWork中,提供的具体迭代器角色是定义在容器角色中的内部类,这样便保护了容器的封装。但是,同时容器也提供了遍历算法接口,并且你可以扩展自己的迭代器。大家考虑一个问题,为什么一定要去实现 Iterable 这个接口呢? 为什么不直接实现 Iterator接口 呢?
看一下 JDK 中的集合类,比如 List一族或者Set一族,都是实现了 Iterable 接口,但并不直接实现 Iterator 接口。仔细想一下这么做是有道理的:因为 Iterator接口的核心方法 next() 或者 hasNext() 是依赖于迭代器的当前迭代位置的。若 Collection 直接实现 Iterator 接口,势必导致集合对象中包含当前迭代位置的数据(指针)。当集合在不同方法间被传递时,由于当前迭代位置不可预置,那么 next() 方法的结果会变成不可预知。除非再为 Iterator接口 添加一个 reset() 方法,用来重置当前迭代位置。但即使这样,Collection 也只能同时存在一个当前迭代位置(不能同时多次迭代同一个序列:必须要等到当前次迭代完成并reset后,才能再一次从头迭代)。 而选择实现 Iterable 接口则不然,每次调用都会返回一个从头开始计数的迭代器(Iterator),因此,多个迭代器间是互不干扰的。
16、适配器模式
适配器模式将一个类的接口转换成客户期望的另一个接口,让原本不兼容的接口可以合作无间。也就是说,适配器模式用于实现新、老接口之间的转换与适配,其魅力在于:不改变原有接口,却还能使用新接口的功能。
适配器模式主要包含以下四个角色,其内涵分别为:
Target: 客户所期待的接口;
Adaptee: Adapter 所包装的对象,即被适配的类(适配者);
Adapter: 一个用于包装不兼容接口的对象的包装类,通过包装一个需要适配的对象,把原接口转换成目标接口;
Client: 客户端;
适配器模式的三个特点:
适配器对象实现原有接口;
适配器对象组合一个实现新接口的对象(这个对象也可以不实现一个接口,只是一个单纯的对象);
对适配器原有接口方法的调用被 委托 给新接口的实例的特定方法。
适配器模式在Java中最经典的应用为IO中字节流与字符流的转换:
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)
更多关于适配器模式的内容详见一个示例让你明白适配器模式和
运用适配器模式应对项目中的变化两篇博文。
17、模板方法模式
模板方法模式是一种基于继承的代码复用技术,是一种类行为型模式,其核心在于:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法模式的经典应用包括 HibernateTemplate,AQS 等,其优点包括:
封装不变部分,扩展可变部分;
提取公共代码,便于维护;
行为由父类控制,子类实现。
18、策略模式
策略模式属于对象的行为模式,其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,核心思想是:面向接口编程。
策略模式的经典应用包括Spring的PlatfromTransactionManager,JDK 排序策略 (不同的Comparator)等,其优点包括:
算法可以自由切换,避免使用多重条件判断;
扩展性良好。
1). 策略模式与模板方法的区别
对于策略模式而言,一个“策略”是一个整体的(完整的)算法,算法是可以被整体替换的;而模板方法只能被替换其中的特定点,算法流程是固定不可变的。在思想和意图上看,模板方法更加强调:
定义一条线(算法流程),线上的多个点是可以变化的(具体实现在子类中完成),线上的多个点一定是会被执行的,并且一定是按照特定流程被执行的。
算法流程只有唯一的入口,对于点的访问是受限的。
19、Java 自动装箱、拆箱机制
Java为每种基本数据类型都提供了对应的包装器类型。所谓自动装箱机制就是自动将基本数据类型转换为包装器类型,而自动拆箱机制就是自动将包装器类型转换为基本数据类型。在JDK中,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx代表对应的基本数据类型)。但是,
Integer、Short、Byte、Character、Long 这几个类的valueOf方法的实现是类似的,有限可列举,共享[-128,127];
Double、Float的valueOf方法的实现是类似的,无限不可列举,不共享;
Boolean的valueOf方法的实现不同于以上的整型和浮点型,只有两个值,有限可列举,共享;
(1). 什么时候装箱/拆箱?
至于什么时候装箱,什么时候拆箱主要取决于:在当前场景下,你需要的是引用类型还是原生类型。若需要引用类型,但传进来的值是原生类型,则自动装箱(例如,使用equals方法时传进来原生类型的值);若需要的是原生类型,但传进来的值是引用类型,则自动拆箱(例如,使用运算符进行运算时,操作数是包装类型)。
更多关于Java自动装箱与拆箱机制的内容可以参见笔者的博文《Java 原生类型与包装器类型深度剖析》。
20、内部类
内部类指的是在一个类的内部所定义的类,类名不需要和源文件名相同。在Java中,内部类是一个编译时的概念,一旦编译成功,内部类和外部类就会成为两个完全不同的类,共有四种类型:
成员内部类:成员内部类是外围类的一个成员,是依附于外围类的,所以,只有先创建了外围类对象才能够创建内部类对象。也正是由于这个原因,成员内部类也不能含有 static 的变量和方法;
静态内部类:静态内部类,就是修饰为static的内部类,该内部类对象不依赖于外部类对象,就是说我们可以直接创建内部类对象,但其只可以直接访问外部类的所有静态成员和静态方法;
局部内部类:局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效;
匿名内部类:定义匿名内部类的前提是,内部类必须要继承一个类或者实现接口,格式为 new 父类或者接口(){定义子类的内容(如函数等)}。也就是说,匿名内部类最终提供给我们的是一个 匿名子类的对象。
1)、内部类的作用
//父类Example1
public class Example1 {
public String name() {
return "rico";
}
}
//父类Example2
public class Example2 {
public int age() {
return 25;
}
}
//实现多重继承的效果
public class MainExample {
//内部类Test1继承类Example1
private class Test1 extends Example1 {
public String name() {
return super.name();
}
}
//内部类Test2继承类Example2
private class Test2 extends Example2 {
public int age() {
return super.age();
}
}
public String name() {
return new Test1().name();
}
public int age() {
return new Test2().age();
}
public static void main(String args[]) {
MainExample mexam = new MainExample();
System.out.println("姓名:" + mexam.name());
System.out.println("年龄:" + mexam.age());
}
}/* Output:
姓名:rico
年龄:25
*///:~
21、equals, hashCode, ==
== 用于判断两个对象是否为同一个对象或者两基本类型的值是否相等;
equals 用于判断两个对象内容是否相同;
hashCode 是一个对象的 消息摘要函数,一种 压缩映射,其一般与equals()方法同时重写;若不重写hashCode方法,默认使用Object类的hashCode方法,该方法是一个本地方法,由 Object 类定义的 hashCode 方法会针对不同的对象返回不同的整数。
1). equals与hashCode的区别
一般来讲,equals 这个方法是给用户调用的,而 hashcode 方法一般用户不会去调用 ;
当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode()设计,而且要遵守前面所说的几个原则。
2). 在HashMap中使用可变对象作为Key带来的问题
HashMap用Key的哈希值来存储和查找键值对,如果HashMap Key的哈希值在存储键值对后发生改变,那么Map可能再也查找不到这个Entry了。也就是说,在HashMap中可变对象作为Key会造成 数据丢失。因此,
在HashMap中尽量使用不可变对象作为Key,比如,使用String、Integer等不可变类型用作Key是非常明智的或者使用自己定义的不可变类。
如果可变对象在HashMap中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了,例如,可以只根据对象的标识属性生成HashCode。
public class MutableSafeKeyDemo {
public static void main(String[] args) {
Employee emp = new Employee(2);
emp.setName("Robin");
// Put object in HashMap.
Map map = new HashMap<>();
map.put(emp, "Showbasky");
System.out.println(map.get(emp));
// Change Employee name. Change in 'name' has no effect
// on hash code.
emp.setName("Lily");
System.out.println(map.get(emp));
}
}
class Employee {
// It is specified while object creation.
// Cannot be changed once object is created. No setter for this field.
private int id;
private String name;
public Employee(final int id) {
this.id = id;
}
public final String getName() {
return name;
}
public final void setName(final String name) {
this.name = name;
}
public int getId() {
return id;
}
// Hash code depends only on 'id' which cannot be
// changed once object is created. So hash code will not change
// on object's state change
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (id != other.id)
return false;
return true;
}
}
3). 重写equals但不重写HashCode会出现的问题
在使用Set时,若向其加入两个相同(equals返回为true)的对象,由于hashCode函数没有进行重写,那么这两个对象的hashCode值必然不同,它们很有可能被分散到不同的桶中,容易造成重复对象的存在。
4). JDK中equals()方法实现逻辑
最后 比较内容是否一致
更多关于Java中关于相等的内容可以参见笔者的博文《Java 中的 ==, equals 与 hashCode 的区别与联系》。
22、什么是不可变对象
一个不可变对象应该满足以下几个条件:
基本类型变量的值不可变;
引用类型变量不能指向其他对象;
引用类型所指向的对象的状态不可变;
除了构造函数之外,不应该有其它任何函数(至少是任何public函数)修改任何成员变量;
任何使成员变量获得新值的函数都应该将新的值保存在新的对象中,而保持原来的对象不被修改。
23、Java的序列化/反序列化机制
(1)、使用Serializable序列化/反序列化
将实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象,序列化可以弥补不同操作系统之间的差异。其中,需要注意以下几点:
需要序列化的对象必须实现Serializable接口;
只有非静态字段和非transient字段进行序列化,与字段的可见性无关;
序列化/反序列化的实质上操纵的是一个对象图;
public class Student implements Cloneable, Serializable {
private int id;
public Student(Integer id) {
this.id = id;
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
public static void main(String[] args) throws Exception {
Constructor constructor = Student.class
.getConstructor(Integer.class);
Student stu3 = constructor.newInstance(123);
// 写对象
ObjectOutputStream output = new ObjectOutputStream(
new FileOutputStream("student.bin"));
output.writeObject(stu3);
output.close();
// 读对象
ObjectInputStream input = new ObjectInputStream(new FileInputStream(
"student.bin"));
Student stu5 = (Student) input.readObject();
System.out.println(stu5);
}
}
此外,Java中常用到的序列化方法还有 XML、JSON 等,此不赘述。