1、基础语法与面向对象
1.1 重载与重写的区别
- 重载是对象的方法之间,它们方法名相同,但方法的参数列表不同
- 重写是父子类(包括接口与实现类)中两个同名方法,它们方法名相同,且方法的参数列表相同
- 重载在编译阶段,由编译器根据传递给方法的参数来区分方法,例如
- 而重写是在运行阶段,由虚拟机解释器去获取引用对象的实际类型,根据类型才能确定该调用哪个方法,例如
- 有没有发生重写,可以使用 @Override 来检查
MyObject obj = ... obj.test(123); // 应该是调用 test(int x) 这个方法 obj.test("abc"); // 应该是调用 test(String x) 这个方法
Super obj = ... obj.test(); // 到底是调用父类,还是子类的 test 方法,必须检查引用对象的实际类型才能确定
P.S.
- 括号内的说明是为了严谨,自己知道就行,回答时不必说出,这样比较简洁
- 个人觉得,在回答方法重载时,不必去细说什么参数的类型、个数、顺序,就说参数列表不同就完了
- 个人觉得,重点在于点出:重载是编译时由编译器来区分方法,而重写是运行时由解释器来区分方法
- 语法细节,问了再说,不问不必说
- 重写时,子类方法的访问修饰符要 >= 父类方法的访问修饰符
- 重写时,子类方法抛出的检查异常类型要 <= 父类方法抛出的检查异常类型,或子类不抛异常
- 重写时,父子类的方法的返回值类型要一样,或子类方法返回值是父类方法返回值的子类
1.2 == 与 equals 的区别
- 对于基本类型,== 是比较两边的值是否相同
- 对于引用类型,== 是比较两边的引用地址是否相同,用来判断是否引用着同一对象
- equals 要看实现
- Object.equals(Object other) 的内部实现就是 ==,即判断当前对象和 other 是否引用着同一对象
- 比如 String,它的内部实现就是去比较两个字符串中每个字符是否相同,比较的是内容
- 比如 ArrayList,它的内部实现就是去比较两个集合中每个元素是否 equals,比较的也是内容
1.3 String,StringBuilder 和 StringBuffer 的区别
- 它们都可以用来表示字符串对象
- String 表示的字符串是不可变的,而后两者表示的字符串是内容可变的(可以增、删、改字符串里的内容)
- StringBuilder 不是线程安全的,StringBuffer 是线程安全的,而 String 也算是线程安全的
适用场景
- 大部分场景下使用 String 就足够了
- 如果有大量字符串拼接的需求,建议用后两者,此时
- 此字符串对象需要被多线程同时访问,用 StringBuffer 保证安全
- 此字符串对象只在线程内被使用,用 StringBuilder 足够了
另外针对 String 类是 final 修饰会提一些问题,把握下面几点
- 本质是因为 String 要设计成不可变的,final 只是条件之一
- 不可变的好处有很多:线程安全、可以缓存等
1.4 说说 Java 中的异常
异常的重要继承关系如图所示,其中
- Throwable 是其它异常类型的顶层父类
- Error 表示无法恢复的错误,例如 OutOfMemoryError 内存溢出、StackOverflowError 栈溢出等
- 这类异常即使捕捉住,通常也无法让程序恢复正常运行
- Exception 表示可恢复的错误,处理方式有两种
- 一是自己处理,用 catch 语句捕捉后,可以进行一些补救(如记录日志、恢复初始状态等)
- 二是用 throw 语句将异常继续抛给上一层调用者,由调用者去处理
- Exception 有特殊的子类异常 RuntimeException,它与 Exception 的不同之处在于
- Exception 被称之为检查异常,意思是必须在语法层面对异常进行处理,要么 try-catch,要么 throws
- RuntimeException 和它的子类被称为非检查异常(也可以翻译为字面意思:运行时异常),在语法层面对这类异常并不要求强制处理,不加 try-catch 和 throws 编译时也不会提示错误
- 常见的非检查异常有
- 空指针异常
- 算术异常(例如整数除零)
- 数组索引越界异常
- 类型转换异常
- ...
2、集合类
2.1 你知道的数据结构有哪些
线性结构
- 动态数组:相对于普通数组可以扩容
- java 中 ArrayList 就属于动态数组
- 数组的特点是其中元素是连续存储的
- 链表:由多个节点链在一起
- java 中的 LinkedList 就属于链表
- 链表的特点是其中元素是不连续存储的,每次需要根据当前节点,才能找到相邻节点
- 栈:符合 First In Last Out(先进后出)规则
- java 中的 LinkedList 可以充当栈
- 它的 push 方法向栈顶添加元素
- 它的 pop 方法从栈顶移除元素
- 它的 peek 方法从栈顶获取元素(不移除)
- 队列:符合 First In First Out(先进先出)规则
- java 中 LinkedList 也可以充当队列
- 它的 offer 方法用来向队列尾部添加元素(入队)
- 它的 poll 方法用来从队列头部移除元素(出队)
非线性结构
- 优先级队列:在队列基础上增加了优先级,队列会根据优先级调整元素顺序,保证优先级高的元素先出队
- java 中 PriorityQueue 可以作为优先级队列
- 它底层用大顶堆或小顶堆来实现
- 它适用于实现排行榜、任务调度等编码
- 它特别适合于流式数据的处理,利用它能够大大节省内存
- Hash 表(哈希表,也叫散列表):由多对 key - value 组成,会根据 key 的 hash 码把它们分散存储在数组当中,其中 key 的 hash 码与数组索引相对应
- java 中的 HashMap,Hashtable 都属于哈希表
- 它特别适用于实现数据的快速查找
- 红黑树:可以自平衡的二叉查找树,相对于线性结构来说,拥有更好的性能
- java 中的 TreeMap 属于红黑树
- 跳表:多级链表结构,也能达到与红黑树同级的性能,且实现更为简单
- java 中的 ConcurrentSkipListMap 用跳表结构实现
- redis 中的 SortedSet 也是用跳表实现
- B+ 树:可以自平衡的 N 叉查找树
- 关系型数据库的索引常用 B+ 树实现
P.S.
- 以上数据结构不必全部掌握,根据自己实际情况,捡熟悉的回答即可
- 以上仅是这些数据结构的简述,关于它们的详细讲解,请参考黑马《数据结构与算法》课程:
2.2 说说 java 中常见的集合类
重要的集合接口以及实现类参考下图
classDiagram class Collection {<<interface>>} class List {<<interface>>} class Set {<<interface>>} class Map { <<interface>> entrySet()* keySet()* values()* } Collection <|-- List Collection <|-- Set List <|.. ArrayList List <|.. LinkedList List <|.. Vector Set <|.. HashSet Map <|.. HashMap Map <|.. TreeMap Map <|.. Hashtable Map <|.. ConcurrentHashMap HashMap <|.. LinkedHashMap Set <-- Map Collection <-- Map |
|
接口
- 接口四个:Collection、List、Set、Map,它们的关系:
- Collection 是父接口,List 和 Set 是它的子接口
- Map 接口与其它接口的关系
- Map 调用 entrySet(),keySet() 方法时,会创建 Set 的实现
- Map 调用 values() 方法时,会用到 Collection 的实现
List 实现(常见三个)
- ArrayList 基于数组实现
- 随机访问(即根据索引访问)性能高
- 增、删由于要移动数组元素,性能会受影响
- 【进阶】但如果增、删操作的是数组尾部不牵涉移动元素
- LinkedList 基于链表实现
- 随机访问性能低,因为需要顺着链表一个个才能访问到某索引位置
- 增、删性能高
- 【进阶】说它随机访问性能低是相对的,如果是头尾节点,无论增删改查都快
- 【进阶】说它增删性能高也是有前提的,并没有包含定位到该节点的时间,把这个算上,增删性能并不高
- Vector 基于数组实现
- 相对于前两种 List 实现是线程安全的
- 【进阶】一些说法说 Vector 已经被舍弃,这是不正确的
Set 实现
- HashSet 内部组合了 HashMap,利用 Map key 唯一的特点来实现 Set
- 集合中元素唯一,注意需要为元素实现 hashCode 和 equals 方法
- 【进阶】Set 的特性只有元素唯一,有些人说 Set 无序,这得看实现,例如 HashSet 无序,但TreeSet 有序
Map 实现(常见五个)
- HashMap 底层是 Hash 表,即数组 + 链表,链表过长时会优化为红黑树
- 集合中 Key 要唯一,并且它需要实现 hashCode 和 equals 方法
- LinkedHashMap 基于 HashMap,只是在它基础上增加了一个链表来记录元素的插入顺序
- 【进阶】这个链表,默认会记录元素插入顺序,这样可以以插入顺序遍历元素
- 【进阶】这个链表,还可以按元素最近访问来调整顺序,这样可以用来做 LRU Cache 的数据结构
- TreeMap 底层是红黑树
- Hashtable 底层是 Hash 表,相对前面三个实现来说,线程安全
- 【进阶】它的线程安全实现方式是在 put,get 等方法上都加了 synchronized,锁住整个对象
- ConcurrentHashMap 底层也是 Hash 表,也是线程安全的
- 【进阶】它的 put 方法执行时仅锁住一个链表,并发度比 Hashtable 高
- 【进阶】它的 get 方法执行不加锁,是通过 volatile 保证数据的可见性
P.S.
- 未标注的是必须记住的部分
- 标注【进阶】的条目是该集合比较有特色的地方,回答出来就是加分项,不过也根据自己情况来记忆
2.3 HashMap 原理(数据结构)
底层数据结构:数组+链表+红黑树
接下来的回答中要点出数组的作用,为啥会有冲突,如何解决冲突
- 数组:存取元素时,利用 key 的 hashCode 来计算它在数组中的索引,这样在没有冲突的情况下,能让存取时间复杂度达到 O(1)
- 冲突:数组大小毕竟有限,就算元素的 hashCode 唯一,数组大小是 n 的情况下要放入 n+1 个元素,根据鸽巢原理,肯定会发生冲突
- 解决冲突:一种办法就是利用链表,将这些冲突的元素链起来,当然在在此链表中存取元素,时间复杂度会提高为 O(n)
接下来要能说出为什么在链表的基础上还要有红黑树
- 树化目的是避免链表过长引起的整个 HashMap 性能下降,红黑树的时间复杂度是 O(log{n})
有一些细节问题可以继续回答,比如树化的时机【进阶】
- 时机:在数组容量达到 >= 64 且 链表长度 >= 8 时,链表会转换成红黑树
- 如果树中节点做了删除,节点少到已经没必要维护树,那么红黑树也会退化为链表
2.4 HashMap 原理(扩容)
扩容因子:0.75 也就是 3/4
- 初始容量 16,当放入第 13 个元素时(超过 3/4)时会进行扩容
- 每次扩容,容量翻倍
- 扩容后,会重新计算 key 对应的桶下标(即数组索引)这样,一部分 key 会移动到其它桶中
2.5 HashMap 原理(方法执行流程)
以 put 方法为例进行说明
- 产生 hash 码。
- 先调用 key.hashCode() 方法
- 为了让哈希分布更均匀,还要对它返回结果进行二次哈希,这个结果称为 hash
- 二次哈希就是把 hashCode 的高 16 位与低 16 位做了个异或运算
- 搞定数组。
- 如果数组还不存在,会创建默认容量为 16 的数组,容量称为 n
- 否则使用已有数组
- 计算桶下标。
- 利用 (n - 1) & hash 得到 key 对应的桶下标(即数组索引)
- 也可以用 hash % n 来计算,但效率比前面的方法低,且有负数问题
- 用 (n - 1) & hash 有前提,就是容量 n 必须是 2 的幂(如 16,32,64 ...)
- 计算好桶下标后,分三种情况
- 如果该桶位置还空着,直接根据键值创建新的 Node 对象放入该位置即可
- 如果该桶是一条链表,沿着链表找,看看是否有值相同的 key,有走更新,没有走新增
- 走新增逻辑的话,是把节点链到尾部(尾插法)
- 新增后还要检查链表是否需要树化,如果是,转成红黑树
- 新增的最后要检查元素个数 size,如果超过阈值,要走扩容逻辑
- 如果该桶是一棵红黑树,走红黑树新增和更新逻辑,同样新增的最后要看是否需要扩容
P.S.
- 以上讲解基于 jdk 1.8 及以上版本的 HashMap 实现
- 考虑到 jdk 1.7 已经很少使用了,故不再介绍基于 1.7 的 HashMap,有需求可以看 b 站黑马面试视频