40道一线互联网公司高频面试题(附答案!)

Java 基础 40
语言特性 12
Q1:Java 语言的优势?
① 平台无关性,摆脱硬件束缚,"一次编写,处处运行"。
② 相对安全的内存管理和访问机制,避免大部份内存泄漏和指针越界。
③ 热点代码检测和运行时编译及优化,使程序随运行时间增加得到更高性能。
④ 完善的应用程序接口,支持第三方类库。
Q2:Java 如何实现平台无关?
JVM: Java 编译器可生成与计算机体系结构无关的字节码指令,字节码文件不只能够轻易地在任何机器上解释执行,还能够动态地转换成本地机器代码,转换是由 JVM 实现的,JVM 是平台相关的,屏蔽了不一样操做系统的差别。
语言规范: 基本数据类型大小有明确规定,例如 int 永远为 32 位,而 C/C++ 中多是 16 位、32 位,也多是编译器开发商指定的其余大小。Java 中数值类型有固定字节数,二进制数据以固定格式存储和传输,字符串采用标准的 Unicode 格式存储。
Q3:JDK 和 JRE 的区别?
JDK: Java Development Kit,开发工具包。提供了编译运行 Java 程序的各类工具,包括编译器、JRE 及经常使用类库,是 JAVA 核心。
JRE: Java Runtime Environment,运行时环境,运行 Java 程序的必要环境,包括 JVM、核心类库、核心配置工具。
Q4:Java 按值调用仍是引用调用?
按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。
Java 老是按值调用,方法获得的是全部参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,若是传递了一个 int 值 ,改变值不会影响实参,由于改变的是值的一个副本。
能够改变对象参数的状态,但不能让对象参数引用一个新的对象。若是传递了一个 int 数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。
Q5:浅拷贝和深拷贝的区别?
浅拷贝: 只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象,不安全。
深拷贝: 彻底拷贝基本数据类型和引用数据类型,安全。
Q6:什么是反射?
在运行状态中,对于任意一个类都能知道它的全部属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。缺点是破坏了封装性以及泛型约束。反射是框架的核心,Spring 大量使用反射。
Q7:Class 类的做用?如何获取一个 Class 对象?
在程序运行期间,Java 运行时系统为全部对象维护一个运行时类型标识,这个信息会跟踪每一个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。
获取 Class 对象:① 类名.class 。②对象的 getClass方法。③ Class.forName(类的全限定名)。
Q8:什么是注解?什么是元注解?
注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override 标识一个方法是重写方法。
元注解是自定义注解的注解,例如:
@Target:约束做用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。
@Rentention:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。
@Documented:代表这个注解应该被 javadoc 记录。
Q9:什么是泛型,有什么做用?
泛型本质是参数化类型,解决不肯定对象具体类型的问题。泛型在定义处只具有执行 Object 方法的能力。
泛型的好处:① 类型安全,放置什么出来就是什么,不存在 ClassCastException。② 提高可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。③ 代码重用,合并了同类型的处理代码。
Q10:泛型擦除是什么?
泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,由于虚拟机没有泛型类型对象,全部对象都属于普通类。例如定义 List 或 List,在编译后都会变成 List 。
定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。若是没有限定类型就会替换为 Object,若是有限定类型就会替换为第一个限定类型,例如 `` 会使用 A 类型替换 T。
Q11:JDK8 新特性有哪些?
lambda 表达式:容许把函数做为参数传递到方法,简化匿名内部类代码。
函数式接口:使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
方法引用:能够引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
接口:接口能够定义 default 修饰的默认方法,下降了接口升级的复杂性,还能够定义静态方法。
注解:引入重复注解机制,相同注解在同地方能够声明屡次。注解做用范围也进行了扩展,可做用于局部变量、泛型、方法异常等。
类型推测:增强了类型推测机制,使代码更加简洁。
Optional 类:处理空指针异常,提升代码可读性。
Stream 类:引入函数式编程风格,提供了不少功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。
日期:加强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操做。
JavaScript:提供了一个新的 JavaScript 引擎,容许在 JVM上运行特定 JavaScript 应用。
Q12:异常有哪些分类?
全部异常都是 Throwable 的子类,分为 Error 和 Exception。Error 是 Java 运行时系统的内部错误和资源耗尽错误,例如 StackOverFlowError 和 OutOfMemoryError,这种异常程序没法处理。
Exception 分为受检异常和非受检异常,受检异常须要在代码中显式处理,不然会编译出错,非受检异常是运行时异常,继承自 RuntimeException。
受检异常:① 无能为力型,如字段超长致使的 SQLException。② 力所能及型,如未受权异常 UnAuthorizedException,程序可跳转权限申请页面。常见受检异常还有 FileNotFoundException、ClassNotFoundException、IOException等。
非受检异常:① 可预测异常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,这类异常应该提早处理。② 需捕捉异常,例如进行 RPC 调用时的远程服务超时,这类异常客户端必须显式处理。③ 可透出异常,指框架或系统产生的且会自行处理的异常,例如 Spring 的 NoSuchRequestHandingMethodException,Spring 会自动完成异常处理,将异常自动映射到合适的状态码。
数据类型 5
Q1:Java 有哪些基本数据类型?
数据类型 内存大小 默认值 取值范围
字节 1个 (字节)0 -128 ~ 127
短 2个 (短)0 -215 ~ 215-1
整型 4个 0 -231 ~ 231-1
long 8层 0升 -263 ~ 263-1
浮动 4个 0.0F ±3.4E+38(有效位数 6~7 位)
双 8层 0.0D ±1.7E+308(有效位数 15 位)
烧焦 英文 1B,中文 UTF-8 占 3B,GBK 占 2B。 '\ u0000' '\ u0000'〜'\ uFFFF'
布尔值 单个变量 4B / 数组 1B 假 真假
JVM 没有 boolean 赋值的专用字节码指令,boolean f = false 就是使用 ICONST_0 即常数 0 赋值。单个 boolean 变量用 int 代替,boolean 数组会编码成 byte 数组。
Q2:自动装箱/拆箱是什么?
每一个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其他基本数据类型的包装类都是首字母大写便可。
自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
比较两个包装类数值要用 equals ,而不能用 == 。
Q3:String 是不可变类为何值能够修改?
String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是建立一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。
Q4:字符串拼接的方式有哪些?
① 直接用 + ,底层用 StringBuilder 实现。只适用小数量,若是在循环中使用 + 拼接,至关于不断建立新的 StringBuilder 对象再转换成 String 对象,效率极差。
② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 建立一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。以后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 做为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +。
③ 使用 StringBuilder 或 StringBuffer,二者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 肯定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。
Q5:String a = "a" + new String("b") 建立了几个对象?
常量和常量拼接还是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。
使用字面量时只建立一个常量池中的常量,使用 new 时若是常量池中没有该值就会在常量池中新建立,再在堆中建立一个对象引用常量池中常量。所以 String a = "a" + new String("b") 会建立四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。
面向对象 10
Q1:谈一谈你对面向对象的理解
面向过程让计算机有步骤地顺序作一件事,是过程化思惟,使用面向过程语言开发大型项目,软件复用和维护存在很大问题,模块之间耦合严重。面向对象相对面向过程更适合解决规模较大的问题,能够拆解问题复杂度,对现实事物进行抽象并映射为开发对象,更接近人的思惟。
例如开门这个动做,面向过程是 open(Door door),动宾结构,door 做为操做对象的参数传入方法,方法内定义开门的具体步骤。面向对象的方式首先会定义一个类 Door,抽象出门的属性(如尺寸、颜色)和行为(如 open 和 close),主谓结构。
面向过程代码松散,强调流程化解决问题。面向对象代码强调高内聚、低耦合,先抽象模型定义共性行为,再解决实际问题。
Q2:面向对象的三大特性?
封装是对象功能内聚的表现形式,在抽象基础上决定信息是否公开及公开等级,核心问题是以什么方式暴漏哪些信息。主要任务是对属性、数据、敏感行为实现隐藏,对属性的访问和修改必须经过公共接口实现。封装使对象关系变得简单,下降了代码耦合度,方便维护。
迪米特原则就是对封装的要求,即 A 模块使用 B 模块的某接口行为,对 B 模块中除此行为外的其余信息知道得应尽量少。不直接对 public 属性进行读取和修改而使用 getter/setter 方法是由于假设想在修改属性时进行权限控制、日志记录等操做,在直接访问属性的状况下没法实现。若是将 public 的属性和行为修改成 private 通常依赖模块都会报错,所以不知道使用哪一种权限时应优先使用 private。
继承用来扩展一个类,子类可继承父类的部分属性和行为使模块具备复用性。继承是"is-a"关系,可以使用里氏替换原则判断是否知足"is-a"关系,即任何父类出现的地方子类均可以出现。若是父类引用直接使用子类引用来代替且能够正确编译并执行,输出结果符合子类场景预期,那么说明两个类符合里氏替换原则。
多态以封装和继承为基础,根据运行时对象实际类型使同一行为具备不一样表现形式。多态指在编译层面没法肯定最终调用的方法体,在运行期由 JVM 动态绑定,调用合适的重写方法。因为重载属于静态绑定,本质上重载结果是彻底不一样的方法,所以多态通常专指重写。
Q3:重载和重写的区别?
重载指方法名称相同,但参数类型个数不一样,是行为水平方向不一样实现。对编译器来讲,方法名称和参数列表组成了一个惟一键,称为方法签名,JVM 经过方法签名决定调用哪一种重载方法。无论继承关系如何复杂,重载在编译时能够根据规则知道调用哪一种目标方法,所以属于静态绑定。
JVM 在重载方法中选择合适方法的顺序:① 精确匹配。② 基本数据类型自动转换成更大表示范围。③ 自动拆箱与装箱。④ 子类向上转型。⑤ 可变参数。
重写指子类实现接口或继承父类时,保持方法签名彻底相同,实现不一样方法体,是行为垂直方向不一样实现。
元空间有一个方法表保存方法信息,若是子类重写了父类的方法,则方法表中的方法引用会指向子类实现。父类引用执行子类方法时没法调用子类存在而父类不存在的方法。
重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大,必须加 @Override 。
Q4:类之间有哪些关系?
类关系 描述 权力强侧 举例
继承 父子类之间的关系:is-a 父类 小狗继承于动物
实现 接口和实现类之间的关系:can-do 接口 小狗实现了狗叫接口
组合 比聚合更强的关系:contains-a 总体 头是身体的一部分
聚合 暂时组装的关系:has-a 组装方 小狗和绳子是暂时的聚合关系
依赖 一个类用到另外一个:depends-a 被依赖方 人养小狗,人依赖于小狗
关联 平等的使用关系:links-a 平等 人使用卡消费,卡能够提取人的信息
Q5:Object 类有哪些方法?
equals:检测对象是否相等,默认使用 == 比较对象引用,能够重写 equals 方法自定义比较规则。equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。
hashCode:散列码是由对象导出的一个整型值,没有规律,每一个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,通常须要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,所以 hashCode 是对象相等的必要不充分条件。
toString:打印对象时默认的方法,若是没有重写打印的是表示对象值的一个字符串。
*clone:clone 方法声明为 protected,类只能经过该方法克隆它本身的对象,若是但愿其余类也能调用该方法必须定义该方法为 public。若是一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,通常重写 clone 方法须要实现 Cloneable 接口并指定访问修饰符为 public。
finalize:肯定一个对象死亡至少要通过两次标记,若是对象在可达性分析后发现没有与 GC Roots 链接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。若是有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中从新与引用链上的对象创建关联就会在第二次标记时被移出回收集合。因为运行代价高昂且没法保证调用顺序,在 JDK 9 被标记为过期方法,并不适合释放资源。
getClass:返回包含对象信息的类对象。
wait / notify / notifyAll:阻塞或唤醒持有该对象锁的线程。
Q6:内部类的做用是什么,有哪些分类?
内部类可对同一包中其余类隐藏,内部类方法能够访问定义这个内部类的做用域中的数据,包括 private 数据。
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。
静态内部类: 属于外部类,只加载一次。做用域仅在包内,可经过 外部类名.内部类名 直接访问,类内只能访问外部类全部静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还能够定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。
成员内部类: 属于外部类的每一个对象,随对象一块儿加载。不能够定义静态成员和方法,可访问外部类的全部内容。
局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,做用范围仅在声明类的代码块中。
匿名内部类: 只用一次的没有名字的类,能够简化代码,建立的对象类型至关于 new 的类的子类类型。用于实现事件监听和其余回调。
Q7:访问权限控制符有哪些?
访问权限控制符 本类 封装形式 包外子类 任何地方
上市 √ √ √ √
受保护的 √ √ √ ×
无 √ √ × ×
私人的 √ × × ×
Q8:接口和抽象类的异同?
接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。
语法维度 抽象类 接口
成员变量 无特殊要求 默认 public static final 常量
构造方法 有构造方法,不能实例化 没有构造方法,不能实例化
方法 抽象类能够没有抽象方法,但有抽象方法必定是抽象类。 默认 public abstract,JDK8 支持默认/静态方法,JDK9 支持私有方法。
继承 单继承 多继承
Q9:接口和抽象类应该怎么选择?
抽象类体现 is-a 关系,接口体现 can-do 关系。与接口相比,抽象类一般是对同类事物相对具体的抽象。
抽象类是模板式设计,包含一组具体特征,例如某汽车,底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅材质能够根据不一样级别配置存在不一样实现。
接口是契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁均可以实现它,但必须遵照接口的约定。例如全部车辆都必须实现刹车这种强制规范。
接口是顶级类,抽象类在接口下面的第二层,对接口进行了组合,而后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分红多个接口,再利用抽象类去实现这些,方便后续的扩展和重构。
例如 Plane 和 Bird 都有 fly 方法,应把 fly 定义为接口,而不是抽象类的抽象方法再继承,由于除了 fly 行为外 Plane 和 Bird 间很难再找到其余共同特征。
Q10:子类初始化的顺序
① 父类静态代码块和静态变量。② 子类静态代码块和静态变量。③ 父类普通代码块和普通变量。④ 父类构造方法。⑤ 子类普通代码块和普通变量。⑥ 子类构造方法。
集合 7
Q1:说一说 ArrayList
ArrayList 是容量可变的非线程安全列表,使用数组实现,集合扩容时会建立更大的数组,把原有数组复制到新数组。支持对元素的快速随机访问,但插入与删除速度很慢。ArrayList 实现了 RandomAcess 标记接口,若是一个类实现了该接口,那么表示使用索引遍历比迭代器更快。
elementData是 ArrayList 的数据域,被 transient 修饰,序列化时会调用 writeObject 写入流,反序列化时调用 readObject 从新赋值到新对象的 elementData。缘由是 elementData 容量一般大于实际存储元素的数量,因此只需发送真正有实际值的数组元素。
size 是当前实际大小,elementData 大小大于等于 size。
**modCount **记录了 ArrayList 结构性变化的次数,继承自 AbstractList。全部涉及结构变化的方法都会增长该值。expectedModCount 是迭代器初始化时记录的 modCount 值,每次访问新元素时都会检查 modCount 和 expectedModCount 是否相等,不相等就会抛出异常。这种机制叫作 fail-fast,全部集合类都有这种机制。
Q2:说一说 LinkedList
LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。除继承 AbstractList 外还实现了 Deque 接口,这个接口具备队列和栈的性质。成员变量被 transient 修饰,原理和 ArrayList 相似。
LinkedList 包含三个重要的成员:size、first 和 last。size 是双向链表中节点的个数,first 和 last 分别指向首尾节点的引用。
LinkedList 的优势在于能够将零散的内存单元经过附加引用的方式关联起来,造成按链路顺序查找的线性结构,内存利用率较高。
Q3:Set 有什么特色,有哪些实现?
Set 不容许元素重复且无序,经常使用实现有 HashSet、LinkedHashSet 和 TreeSet。
HashSet 经过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,全部 Key 都使用相同的 Value ,一个名为 PRESENT 的 Object 类型常量。使用 Key 保证元素惟一性,但不保证有序性。因为 HashSet 是 HashMap 实现的,所以线程不安全。
HashSet 判断元素是否相同时,对于包装类型直接按值比较。对于引用类型先比较 hashCode 是否相同,不一样则表明不是同一个对象,相同则继续比较 equals,都相同才是同一个对象。
LinkedHashSet 继承自 HashSet,经过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。
TreeSet 经过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
Q4:TreeMap 有什么特色?
TreeMap 基于红黑树实现,增删改查的平均和最差时间复杂度均为 O(logñ) ,最大特色是 Key 有序。Key 必须实现 Comparable 接口或提供的 Comparator 比较器,因此 Key 不容许为 null。
HashMap 依靠 hashCode 和 equals 去重,而 TreeMap 依靠 Comparable 或 Comparator。TreeMap 排序时,若是比较器不为空就会优先使用比较器的 compare 方法,不然使用 Key 实现的 Comparable 的 compareTo 方法,二者都不知足会抛出异常。
TreeMap 经过 put 和 deleteEntry 实现增长和删除树节点。插入新节点的规则有三个:① 须要调整的新节点老是红色的。② 若是插入新节点的父节点是黑色的,不须要调整。③ 若是插入新节点的父节点是红色的,因为红黑树不能出现相邻红色,进入循环判断,经过从新着色或左右旋转来调整。TreeMap 的插入操做就是按照 Key 的对比往下遍历,大于节点值向右查找,小于向左查找,先按照二叉查找树的特性操做,后续会从新着色和旋转,保持红黑树的特性。
Q5:HashMap 有什么特色?
JDK8 以前底层实现是数组 + 链表,JDK8 改成数组 + 链表/红黑树,节点类型从Entry 变动为 Node。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。
table 数组记录 HashMap 的数据,每一个下标对应一条链表,全部哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。
HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,若是两个元素 key 的 hash 值同样,就会发生哈希冲突,被放到同一个链表上,为使查询效率尽量高,键的 hash 值要尽量分散。
HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。
Q6:HashMap 相关方法的源码?
JDK8 以前
hash:计算元素 key 的散列值
① 处理 String 类型时,调用 stringHash32 方法获取 hash 值。
② 处理其余类型数据时,提供一个相对于 HashMap 实例惟一不变的随机值 hashSeed 做为计算初始量。
③ 执行异或和无符号右移使 hash 值更加离散,减少哈希冲突几率。
indexFor:计算元素下标
将 hash 值和数组长度-1 进行与操做,保证结果不会超过 table 数组范围。
get:获取元素的 value 值
① 若是 key 为 null,调用 getForNullKey 方法,若是 size 为 0 表示链表为空,返回 null。若是 size 不为 0 说明存在链表,遍历 table[0] 链表,若是找到了 key 为 null 的节点则返回其 value,不然返回 null。
② 若是 key 为 不为 null,调用 getEntry 方法,若是 size 为 0 表示链表为空,返回 null 值。若是 size 不为 0,首先计算 key 的 hash 值,而后遍历该链表的全部节点,若是节点的 key 和 hash 值都和要查找的元素相同则返回其 Entry 节点。
③ 若是找到了对应的 Entry 节点,调用 getValue 方法获取其 value 并返回,不然返回 null。
put:添加元素
① 若是 key 为 null,直接存入 table[0]。
② 若是 key 不为 null,计算 key 的 hash 值。
③ 调用 indexFor 计算元素存放的下标 i。
④ 遍历 table[i] 对应的链表,若是 key 已存在,就更新 value 而后返回旧 value。
⑤ 若是 key 不存在,将 modCount 值加 1,使用 addEntry 方法增长一个节点并返回 null。
resize:扩容数组
① 若是当前容量达到了最大容量,将阈值设置为 Integer 最大值,以后扩容再也不触发。
② 不然计算新的容量,将阈值设为 newCapacity x loadFactor 和 最大容量 + 1 的较小值。
③ 建立一个容量为 newCapacity 的 Entry 数组,调用 transfer 方法将旧数组的元素转移到新数组。
transfer:转移元素
① 遍历旧数组的全部元素,调用 rehash 方法判断是否须要哈希重构,若是须要就从新计算元素 key 的 hash 值。
② 调用 indexFor 方法计算元素存放的下标 i,利用头插法将旧数组的元素转移到新数组。
JDK8
hash:计算元素 key 的散列值
若是 key 为 null 返回 0,不然就将 key 的 hashCode 方法返回值高低16位异或,让尽量多的位参与运算,让结果的 0 和 1 分布更加均匀,下降哈希冲突几率。
put:添加元素
① 调用 putVal 方法添加元素。
② 若是 table 为空或长度为 0 就进行扩容,不然计算元素下标位置,不存在就调用 newNode 建立一个节点。
③ 若是存在且是链表,若是首节点和待插入元素的 hash 和 key 都同样,更新节点的 value。
④ 若是首节点是 TreeNode 类型,调用 putTreeVal 方法增长一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,不然往右子树查找,找到空位后执行两个方法:balanceInsert 方法,插入节点并调整平衡、moveRootToFront 方法,因为调整平衡后根节点可能变化,须要重置根节点。
⑤ 若是都不知足,遍历链表,根据 hash 和 key 判断是否重复,决定更新 value 仍是新增节点。若是遍历到了链表末尾则添加节点,若是达到建树阈值 7,还须要调用 treeifyBin 把链表重构为红黑树。
⑥ 存放元素后将 modCount 加 1,若是 ++size > threshold ,调用 resize 扩容。
get :获取元素的 value 值
① 调用 getNode 方法获取 Node 节点,若是不是 null 就返回其 value 值,不然返回 null。
② getNode 方法中若是数组不为空且存在元素,先比较第一个节点和要查找元素的 hash 和 key ,若是都相同则直接返回。
③ 若是第二个节点是 TreeNode 类型则调用 getTreeNode 方法进行查找,不然遍历链表根据 hash 和 key 查找,若是没有找到就返回 null。
resize:扩容数组
从新规划长度和阈值,若是长度发生了变化,部分数据节点也要从新排列。
从新规划长度
① 若是当前容量 oldCap > 0 且达到最大容量,将阈值设为 Integer 最大值,return 终止扩容。
② 若是未达到最大容量,当 oldCap << 1 不超过最大容量就扩大为 2 倍。
③ 若是都不知足且当前扩容阈值 oldThr > 0,使用当前扩容阈值做为新容量。
④ 不然将新容量置为默认初始容量 16,新扩容阈值置为 12。
从新排列数据节点
① 若是节点为 null 不进行处理。
② 若是节点不为 null 且没有next节点,那么经过节点的 hash 值和 新容量-1 进行与运算计算下标存入新的 table 数组。
③ 若是节点为 TreeNode 类型,调用 split 方法处理,若是节点数 hc 达到6 会调用 untreeify 方法转回链表。
④ 若是是链表节点,须要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0 的部分不须要作处理,不然须要放到新的下标位置上,新下标 = 旧下标 + 旧容量。
Q7:HashMap 为何线程不安全?
JDK7 存在死循环和数据丢失问题。
数据丢失:
并发赋值被覆盖: 在 createEntry 方法中,新添加的元素直接放在头部,使元素以后能够被更快访问,但若是两个线程同时执行到此处,会致使其中一个线程的赋值被覆盖。
已遍历区间新增元素丢失: 当某个线程在 transfer 方法迁移时,其余线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后,table 数组引用指向了 newTable,新增元素丢失。
新表被覆盖: 若是 resize 完成,执行了 table = newTable,则后续元素就能够在新表上进行插入。但若是多线程同时 resize ,每一个线程都会 new 一个数组,这是线程内的局部对象,线程之间不可见。迁移完成后resize 的线程会赋值给 table 线程共享变量,可能会覆盖其余线程的操做,在新表中插入的对象都会被丢弃。
死循环: 扩容时 resize 调用 transfer 使用头插法迁移元素,虽然 newTable 是局部变量,但原先 table 中的 Entry 链表是共享的,问题根源是 Entry 的 next 指针并发修改,某线程尚未将 table 设为 newTable 时用完了 CPU 时间片,致使数据丢失或死循环。
JDK8 在 resize 方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据。可用 ConcurrentHashMap 或 Collections.synchronizedMap 包装成同步集合。
IO风格6
Q1:同步/异步/阻塞/非阻塞 IO 的区别?
同步和异步是通讯机制,阻塞和非阻塞是调用状态。
同步 IO 是用户线程发起 IO 请求后须要等待或轮询内核 IO 操做完成后才能继续执行。异步 IO 是用户线程发起 IO 请求后能够继续执行,当内核 IO 操做完成后会通知用户线程,或调用用户线程注册的回调函数。
阻塞 IO 是 IO 操做须要完全完成后才能返回用户空间 。非阻塞 IO 是 IO 操做调用后当即返回一个状态值,无需等 IO 操做完全完成。
Q2:什么是 BIO?
BIO 是同步阻塞式 IO,JDK1.4 以前的 IO 模型。服务器实现模式为一个链接请求对应一个线程,服务器须要为每个客户端请求建立一个线程,若是这个链接不作任何事会形成没必要要的线程开销。能够经过线程池改善,这种 IO 称为伪异步 IO。适用链接数目少且服务器资源多的场景。
Q3:什么是 NIO?
NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个链接请求对应一个线程,客户端链接请求会注册到一个多路复用器 Selector ,Selector 轮询到链接有 IO 请求时才启动一个线程处理。适用链接数目多且链接时间短的场景。
同步是指线程仍是要不断接收客户端链接并处理数据,非阻塞是指若是一个管道没有数据,不须要等待,能够轮询下一个管道。
核心组件:
Selector: 多路复用器,轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前须要将 Channel 注册到 Selector,注册后会获得一个 SelectionKey,经过 SelectionKey 获取 Channel 和 Selector 相关信息。
Channel: 双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要经过 Buffer 来读写数据,也能够和其余 Channel 交互。
Buffer: 缓冲区,本质是一块可读写数据的内存,用来简化数据读写。Buffer 三个重要属性:position 下次读写数据的位置,limit 本次读写的极限位置,capacity 最大容量。
使用步骤:向 Buffer 写数据,调用 flip 方法转为读模式,从 Buffer 中读数据,调用 clear 或 compact 方法清空缓冲区。
flip 将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。
clear 将读转为写模式(用于读彻底部数据的状况,把 position 置 0,limit 设为 capacity)。
compact 将读转为写模式(用于存在未读数据的状况,让 position 指向未读数据的下一个)。
通道方向和 Buffer 方向相反,读数据至关于向 Buffer 写,写数据至关于从 Buffer 读。
Q4:什么是 AIO?
AIO 是 JDK7 引入的异步非阻塞 IO。服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操做系统先完成 IO 操做后再通知服务器应用来直接使用准备好的数据。适用链接数目多且链接时间长的场景。
异步是指服务端线程接收到客户端管道后就交给底层处理IO通讯,本身能够作其余事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。
实现方式包括经过 Future 的 get 方法进行阻塞式调用以及实现 CompletionHandler 接口,重写请求成功的回调方法 completed 和请求失败回调方法 failed。
Q5:java.io 包下有哪些流?
主要分为字符流和字节流,字符流通常用于文本文件,字节流通常用于图像或其余文件。
字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。字符流和字节流都有对应的缓冲流,字节流也能够包装为字符流,缓冲流带有一个 8KB 的缓冲数组,能够提升流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。
Q6:序列化和反序列化是什么?
Java 对象 JVM 退出时会所有销毁,若是须要将对象及状态持久化,就要经过序列化实现,将内存中的对象保存在二进制流中,须要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,所以属于类属性的静态变量不会被序列化。
常见的序列化有三种:
Java 原生序列化
实现 Serializabale 标记接口,Java 序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能通常。序列化和反序列化必须保持序列化 ID 的一致,通常使用 private static final long serialVersionUID 定义序列化 ID,若是不设置编译器会根据类的内部实现自动生成该值。若是是兼容升级不该该修改序列化 ID,防止出错,若是是不兼容升级则须要修改。
Hessian 序列化
Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流能够被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型,不依赖外部描述文件,用一个字节表示经常使用基础类型,极大缩短二进制流。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。Hessian 会把复杂对象全部属性存储在一个 Map 中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,所以子类值会被父类覆盖。
JSON 序列化
JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程当中抛弃了类型信息,因此反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
序列化一般会使用网络传输对象,而对象中每每有敏感数据,容易遭受攻击,Jackson 和 fastjson 等都出现过反序列化漏洞,所以不须要进行序列化的敏感属性传输时应加上 transient 关键字。transient 的做用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。java

总结了一些2020年的面试题,这份面试题的包含的模块分为19个模块,分别是: Java基础、容器、多线程、反射、对象拷贝、JavaWeb异常、网络、设计模式、Spring/SpringMVC、SpringBoot/SpringCloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM。
获取如下资料,关注公众号:【有故事的程序员】。
记得点个关注+评论哦~程序员

相关文章
相关标签/搜索