Java se面试题

200 阅读16分钟

Java se面试题

Object

Object 类是 Java 中的一个特殊类,它是所有类的父类,Java 中的类都直接或间接的继承自 Object 类。

Object 类的常用方法如下:

  • equals():对比两个对象是否相同
  • getClass():返回一个对象的运行时类
  • hashCode():返回该对象的哈希码值
  • toString():返回该对象的字符串描述
  • wait():使当前的线程等待
  • notify():唤醒在此对象监视器上等待的单个线程
  • notifyAll():唤醒在此对象监视器上等待的所有线程
  • clone():克隆一个新对象

Java 中可以多继承吗?

Java 中只能单继承,但可以实现多接口。

覆盖和重载有哪些区别?

覆盖和重载的区别如下:

  • 覆盖(Override)是指子类对父类方法的一种重写,只能比父类抛出更少的异常,访问权限不能比父类的小,被覆盖的方法不能是 private,否则只是在子类中重新定义了一个方法;
  • 重载(Overload)表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同。

构造方法有哪些特征?

构造方法的特征如下:

  • 构造方法必须与类名相同;
  • 构造方法没有返回类型(void 也不能有);
  • 构造方法不能被继承、覆盖、直接调用;
  • 类定义时提供了默认的无参构造方法;
  • 构造方法可以私有,外部无法使用私有构造方法创建对象。

构造函数能不能被覆盖?能不能被重载?

构造函数可以重载,但不能覆盖。

Java 中的 this 和 super 有哪些区别?

this 和 super 都是 Java 中的关键字,起指代作用,在构造方法中必须出现在第一行,它们的区别如下。

  • 基础概念:this 是访问本类实例属性或方法;super 是子类访问父类中的属性或方法。
  • 查找范围:this 先查本类,没有的话再查父类;super 直接访问父类。
  • 使用:this 单独使用时,表示当前对象;super 在子类覆盖父类方法时,访问父类同名方法。

在静态方法中可以使用 this 或 super 吗?为什么?

在静态方法中不能使用 this 或 super,因为 this 和 super 指代的都是需要被创建出来的对象,而静态方法在类加载的时候就已经创建了,所以没办法在静态方法中使用 this 或 super。

静态方法的使用需要注意哪些问题?

静态方法的使用需要注意以下两个问题:

  • 静态方法中不能使用实例成员变量和实例方法;
  • 静态方法中不能使用 this 和 super。

final 修饰符的作用有哪些?

final 修饰符作用如下:

  • 被 final 修饰的类不能被继承;
  • 被 final 修饰的方法不能被重写;
  • 被 final 修饰的变量不能被修改。

覆盖 equals() 方法的时候需要遵守哪些规则?

Oracle 官方的文档对于 equals() 重写制定的规则如下。

  • 自反性:对于任意非空的引用值 x,x.equals(x) 返回值为真。
  • 对称性:对于任意非空的引用值 x 和 y,x.equals(y) 必须和 y.equals(x) 返回相同的结果。
  • 传递性:对于任意的非空引用值 x、y 和 z,如果 x.equals(y) 返回值为真,y.equals(z) 返回值也为真,那么 x.equals(z) 也必须返回值为真。
  • 一致性:对于任意非空的引用值 x 和 y,无论调用 x.equals(y) 多少次,都要返回相同的结果。在比较的过程中,对象中的数据不能被修改。
  • 对于任意的非空引用值 x,x.equals(null) 必须返回假。

在 Object 中 notify() 和 notifyAll() 方法有什么区别?

notify() 方法随机唤醒一个等待的线程,而 notifyAll() 方法将唤醒所有在等待的线程。

抽象类的特性

  • 抽象类不能被初始化
  • 抽象类可以有构造方法
  • 抽象类的子类如果为普通类,则必须重写抽象类中的所有抽象方法
  • 抽象类中的方法可以是抽象方法或普通方法
  • 一个类中如果包含了一个抽象方法,这个类必须是抽象类
  • 子类中的抽象方法不能与父类中的抽象方法同名
  • 抽象方法不能为 private、static、final 等关键字修饰
  • 抽象类中可以包含普通成员变量,访问类型可以任意指定,也可以使用静态变量(static)

接口能不能有方法体?

JDK 8 之前接口不能有方法体,JDK 8 之后新增了 static 方法和 default 方法,可以包含方法体。

抽象类中能不能包含方法体?

抽象类中可以包含方法体。抽象类的构成也可以完全是包含方法体的普通方法,只不过这样并不是抽象类最优的使用方式。

抽象类能不能被实例化?为什么?

答:抽象类不能被实例化,因为抽象类和接口的设计就是用来规定子类行为特征的,就是让其他类来继承,是多态思想的一种设计体现,所以强制规定抽象类不能被实例化。

抽象方法可以被 private 修饰吗?为什么?

抽象方法不能使用 private 修饰,因为抽象方法就是要子类继承重写的,如果设置 private 则子类不能重写此抽象方法,这与抽象方法的设计理念相违背,所以不能被 private 修饰。

抽象类和接口有什么区别?

抽象类和接口的区别,主要分为以下几个部分。

  • 默认方法
  • 抽象类可以有默认方法的实现
  • JDK 8 之前接口不能有默认方法的实现,JDK 8 之后接口可以有默认方法的实现
  • 继承方式
  • 子类使用 extends 关键字来继承抽象类
  • 子类使用 implements 关键字类实现接口
  • 构造器
  • 抽象类可以有构造器
  • 接口不能有构造器
  • 方法访问修饰符
  • 抽象方法可以用 public / protected / default 等修饰符
  • 接口默认是 public 访问修饰符,并且不能使用其他修饰符
  • 多继承
  • 一个子类只能继承一个抽象类
  • 一个子类可以实现多个接口

List 和 Set 有什么区别?

区别分为以下几个方面:

  • List 允许有多个 null 值,Set 只允许有一个 null 值;
  • List 允许有重复元素,Set 不允许有重复元素;
  • List 可以保证每个元素的存储顺序,Set 无法保证元素的存储顺序。

哪种集合可以实现自动排序?

TreeSet 集合实现了元素的自动排序,也就是说无需任何操作,即可实现元素的自动排序功能。

Vector 和 ArrayList 初始化大小和容量扩充有什么区别?

Vector 和 ArrayList 的默认容量都为 10,源码如下。

Vector 默认容量源码:

public Vector() {
    this(10);
}

ArrayList 默认容量源码:

private static final int DEFAULT_CAPACITY = 10;

Vector 容量扩充默认增加 1 倍 ,capacityIncrement 为初始化 Vector 指定的,默认情况为 0。

ArrayList 容量扩充默认增加大概 0.5 倍 ,是原来1.5倍(oldCapacity + (oldCapacity >> 1))。

Vector、ArrayList、LinkedList 有什么区别?

这三者都是 List 的子类,因此功能比较相似,比如增加和删除操作、查找元素等,但在性能、线程安全等方面表现却又不相同,差异如下:

  • Vector 是 Java 早期提供的动态数组,它使用 synchronized 来保证线程安全,如果非线程安全需要不建议使用,毕竟线程同步是有性能开销的;
  • ArrayList 是最常用的动态数组,本身并不是线程安全的,因此性能要好很多,与 Vector 类似,它也是动态调整容量的,只不过 Vector 扩容时会增加 1 倍,而 ArrayList 会增加 50%;
  • LinkedList 是双向链表集合,因此它不需要像上面两种那样调整容量,它也是非线程安全的集合。

Vector、ArrayList、LinkedList 使用场景有什么区别?

Vector 和 ArrayList 的内部结构是以数组形式存储的,因此非常适合随机访问,但非尾部的删除或新增性能较差,比如我们在中间插入一个元素,就需要把后续的所有元素都进行移动。

LinkedList 插入和删除元素效率比较高,但随机访问性能会比以上两个动态数组慢。

Collection 和 Collections 有什么区别?

Collection 和 Collections 的区别如下:

  • Collection 是集合类的上级接口,继承它的主要有 List 和 Set;
  • Collections 是针对集合类的一个帮助类,它提供了一些列的静态方法实现,如 Collections.sort() 排序、Collections.reverse() 逆序等。

没有继承 Collection 接口的是?

Map

LinkedHashSet 如何保证有序和唯一性?

LinkedHashSet 底层数据结构由哈希表和链表组成,链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。

HashSet 是如何保证数据不可重复的?

HashSet 的底层其实就是 HashMap,只不过 HashSet 实现了 Set 接口并且把数据作为 K 值,而 V 值一直使用一个相同的虚值来保存。

由于 HashMap 的 K 值本身就不允许重复,并且在 HashMap 中如果 K/V 相同时,会用新的 V 覆盖掉旧的 V,然后返回旧的 V,那么在 HashSet 中执行这一句话始终会返回一个 false,导致插入失败,这样就保证了数据的不可重复性。

如何用程序实现后进先出的栈结构?

可以使用集合中的 Stack 实现,Stack 是标准的后进先出的栈结构,使用 Stack 中的 pop() 方法返回栈顶元素并删除该元素。

LinkedList 中的 peek() 和 poll() 有什么区别?

peek() 方法返回第一个元素,但不删除当前元素,当元素不存在时返回 null;poll() 方法返回第一个元素并删除此元素,当元素不存在时返回 null。

Comparable 和 Comparator 有哪些区别?

Comparable 和 Comparator 的主要区别如下:

  • Comparable 位于 java.lang 包下,而 Comparator 位于 java.util 包下;
  • Comparable 在排序类的内部实现,而 Comparator 在排序类的外部实现;
  • Comparable 需要重写 CompareTo() 方法,而 Comparator 需要重写 Compare() 方法;
  • Comparator 在类的外部实现,更加灵活和方便。

Arrays.asList(array)返回的其实不是真正的ArrayList,不能对其进行增删元素

Map

Map 简介
Map 常用的实现类如下:

  • Hashtable:Java 早期提供的一个哈希表实现,它是线程安全的,不支持 null 键和值,因为它的性能不如 ConcurrentHashMap,所以很少被推荐使用。
  • HashMap:最常用的哈希表实现,如果程序中没有多线程的需求,HashMap 是一个很好的选择,支持 null 键和值,如果在多线程中可用 ConcurrentHashMap 替代。
  • TreeMap:基于红黑树的一种提供顺序访问的 Map,自身实现了 key 的自然排序,也可以指定 Comparator 来自定义排序。
  • LinkedHashMap:HashMap 的一个子类,保存了记录的插入顺序,可在遍历时保持与插入一样的顺序。

HashMap 重要方法

1 添加方法:put(Object key, Object value)

执行流程如下:

  • 对 key 进行 hash 操作,计算存储 index;
  • 判断是否有哈希碰撞,如果没碰撞直接放到哈希桶里,如果有碰撞则以链表的形式存储;
  • 判断已有元素的类型,决定是追加树还是追加链表,当链表大于等于 8 时,把链表转换成红黑树;
  • 如果节点已经存在就替换旧值;
  • 判断是否超过阀值,如果超过就要扩容。

2 获取方法:get(Object key)

执行流程如下:

  • 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点;
  • 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对。

Map 常见实现类有哪些?

Map 的常见实现类如下列表:

  • Hashtable:Java 早期提供的一个哈希表实现,它是线程安全的,不支持 null 键和值,因为它的性能不如 ConcurrentHashMap,所以很少被推荐使用;
  • HashMap:最常用的哈希表实现,如果程序中没有多线程的需求,HashMap 是一个很好的选择,支持 null 键和值,如果在多线程中可用 ConcurrentHashMap 替代;
  • TreeMap:基于红黑树的一种提供顺序访问的 Map,自身实现了 key 的自然排序,也可以指定的 Comparator 来自定义排序;
  • LinkedHashMap:HashMap 的一个子类,保存了记录的插入顺序,可在遍历时保持与插入一样的顺序。

使用 HashMap 可能会遇到什么问题?如何避免?

HashMap 在并发场景中可能出现死循环的问题,这是因为 HashMap 在扩容的时候会对链表进行一次倒序处理,假设两个线程同时执行扩容操作,第一个线程正在执行 B→A 的时候,第二个线程又执行了 A→B ,这个时候就会出现 B→A→B 的问题,造成死循环。
解决的方法:升级 JDK 版本,在 JDK 8 之后扩容不会再进行倒序,因此死循环的问题得到了极大的改善,但这不是终极的方案,因为 HashMap 本来就不是用在多线程版本下的,如果是多线程可使用 ConcurrentHashMap 替代 HashMap。

TreeMap 怎么实现根据 value 值倒序?

使用 Collections.sort(list, new Comparator<Map.Entry<String, String>>() 自定义比较器实现,先把 TreeMap 转换为 ArrayList,在使用 Collections.sort() 根据 value 进行倒序,完整的实现代码如下。

HashMap 有哪些重要的参数?用途分别是什么?

HashMap 有两个重要的参数:容量(Capacity)和负载因子(LoadFactor)。

  • 容量(Capacity):是指 HashMap 中桶的数量,默认的初始值为 16。
  • 负载因子(LoadFactor):也被称为装载因子,LoadFactor 是用来判定 HashMap 是否扩容的依据,默认值为 0.75f,装载因子的计算公式 = HashMap 存放的 KV 总和(size)/ Capacity。

HashMap 和 Hashtable 有什么区别?

HashMap 和 Hashtable 区别如下:

  • Hashtable 使用了 synchronized 关键字来保障线程安全,而 HashMap 是非线程安全的;
  • HashMap 允许 K/V 都为 null,而 Hashtable K/V 都不允许 null;
  • HashMap 继承自 AbstractMap 类;而 Hashtable 继承自 Dictionary 类。

什么是哈希冲突?

当输入两个不同值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

有哪些方法可以解决哈希冲突?

哈希冲突的常用解决方案有以下 4 种。

  • 开放定址法:当关键字的哈希地址 p=H(key)出现冲突时,以 p 为基础,产生另一个哈希地址 p1,如果 p1 仍然冲突,再以 p 为基础,产生另一个哈希地址 p2,循环此过程直到找出一个不冲突的哈希地址,将相应元素存入其中。
  • 再哈希法:这种方法是同时构造多个不同的哈希函数,当哈希地址 Hi=RH1(key)发生冲突时,再计算 Hi=RH2(key),循环此过程直到找到一个不冲突的哈希地址,这种方法唯一的缺点就是增加了计算时间。
  • 链地址法:这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
  • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

HashMap 使用哪种方法来解决哈希冲突(哈希碰撞)?

HashMap 使用链表和红黑树来解决哈希冲突,详见本文 put() 方法的执行过程。

HashMap 的扩容为什么是 2^n ?

这样做的目的是为了让散列更加均匀,从而减少哈希碰撞,以提供代码的执行效率。

有哈希冲突的情况下 HashMap 如何取值?

如果有哈希冲突,HashMap 会循环链表中的每项 key 进行 equals 对比,返回对应的元素。相关源码如下:

do {
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k)))) // 比对时还是先看 hash 值是否相同、再看地址或 equals
        return e; // 如果当前节点 e 的键对象和 key 相同,那么返回 e
} while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环

为什么重写 equals() 时一定要重写 hashCode()?

因为 Java 规定,如果两个对象 equals 比较相等(结果为 true),那么调用 hashCode 也必须相等。如果重写了 equals() 但没有重写 hashCode(),就会与规定相违背,比如以下代码(故意注释掉 hashCode 方法):

HashMap 在 JDK 7 多线程中使用会导致什么问题?

HashMap 在 JDK 7 中会导致死循环的问题。因为在 JDK 7 中,多线程进行 HashMap 扩容时会导致链表的循环引用,这个时候使用 get() 获取元素时就会导致死循环,造成 CPU 100% 的情况。

HashMap 在 JDK 7 和 JDK 8 中有哪些不同?

HashMap 在 JDK 7 和 JDK 8 的主要区别如下。

  • 存储结构:JDK 7 使用的是数组 + 链表;JDK 8 使用的是数组 + 链表 + 红黑树。
  • 存放数据的规则:JDK 7 无冲突时,存放数组;冲突时,存放链表;JDK 8 在没有冲突的情况下直接存放数组,有冲突时,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。
  • 插入数据方式:JDK 7 使用的是头插法(先将原位置的数据移到后 1 位,再插入数据到该位置);JDK 8 使用的是尾插法(直接插入到链表尾部/红黑树)。

总结

通过本文可以了解到:

  • Map 的常用实现类 Hashtable 是 Java 早期的线程安全的哈希表实现;
  • HashMap 是最常用的哈希表实现,但它是非线程安全的,可使用 ConcurrentHashMap 替代;
  • TreeMap 是基于红黑树的一种提供顺序访问的哈希表实现;
  • LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,可在遍历时保持与插入一样的顺序。

HashMap 在 JDK 7 可能在扩容时会导致链表的循环引用而造成 CPU 100%,HashMap 在 JDK 8 时数据结构变更为:数组 + 链表 + 红黑树的存储方式,在没有冲突的情况下直接存放数组,有冲突,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。