1 集合面试题

1.1 List 和 Set 的区别

List , Set 都是继承自 Collection 接口
List 特点:元素有放入顺序,元素可重复 ,
Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位 置是有该元素的 HashCode 决定的,其位置其实是固定的,加入Set 的 Object 必须定义 equals ()方法 ,另外list 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想 要的值。)
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

1.2 HashSet 是如何保证不重复的

向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合 equles 方法比较。
HashSet 中的 add ()方法会使用 HashMap 的 add ()方法。以下是 HashSet 部分源码:

  1. private static final Object PRESENT = new Object();
  2. private transient HashMap<E,Object> map;
  3. public HashSet() {
  4. map = new HashMap<>();
  5. }
  6. public boolean add(E e) {
  7. return map.put(e, PRESENT)==null;
  8. }

HashMap 的 key 是唯一的,由上面的代码可以看出 HashSet 添加进去的值就是作为 HashMap 的key。
所以不会 重复( HashMap 比较key是否相等是先比较 hashcode 在比较 equals )。
HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法

  1. //HashSet是对HashMap的简单包装
  2. public class HashSet<E>
  3. {
  4. ......
  5. private transient HashMap<E,Object> map;//HashSet里面有一个HashMap
  6. // Dummy value to associate with an Object in the backing Map
  7. private static final Object PRESENT = new Object();
  8. public HashSet() {
  9. map = new HashMap<>();
  10. }
  11. ......
  12. public boolean add(E e) {//简单的方法转换
  13. return map.put(e, PRESENT)==null;
  14. }
  15. ......
  16. }

1.3 HashMap 是线程安全的吗,为什么不是线程安全的

不是线程安全的; 如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位 置还没有其他的数据。所以这两个线程都会进入我在上面标记为1的代码中。
假设一种情况,线程A通过if判断,该 位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句 里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执 行,现在线程A直接在该位置插入而不用再判断。
这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线 程不安全情况。
本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能 就直接给覆盖了。
上面所说的是一个图来解释可能更加直观。如下面所示,两个线程在同一个位置添加数据,后面添加的数据就覆盖 住了前面添加的。
如果上述插入是插入到链表上,如两个线程都在遍历到最后一个节点,都要在最后添加一个数据,那么后面添加数 据的线程就会把前面添加的数据给覆盖住。则
在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组。

1.4 HashMap 的扩容过程

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—-即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更 多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。
当然 Java 里的数组是无法自动扩容的,方法 是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

  1. HashMap hashMap=new HashMap(cap);

cap =3, hashMap 的容量为4;
cap =4, hashMap 的容量为4;
cap =5, hashMap 的容量为8;
cap =9, hashMap 的容量为16;
如果 cap 是2的n次方,则容量为 cap ,否则为大于 cap 的第一个2的n次方的数。

1.4.1 ArrayList自动扩容?

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
image.png

1.5 HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

HashMap结构图
在 JDK1.7 及之前的版本中, HashMap 又叫散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候, 就将对应节点以链表的形式存储。
JDK1.8 中,当同一个hash值( Table 上元素)的链表节点数不小于8时,将不再以单链表的形式存储了,会被 调整成一颗红黑树。
这就是 JDK7 与 JDK8 中 HashMap 实现的最大区别。
其下基于 JDK1.7.0_80 与 JDK1.8.0_66 做的分析
JDK1.7中
使用一个 Entry 数组来存储数据,用key的 hashcode 取模来决定key会被放到数组里的位置,如果 hashcode 相 同,或者 hashcode 取模后的结果相同( hash collision ),那么这些 key 会被定位到 Entry 数组的同一个 格子里,这些 key 会形成一个链表。
在 hashcode 特别差的情况下,比方说所有key的 hashcode 都相同,这个链表可能会很长,那么 put/get 操作 都可能需要遍历这个链表,也就是说时间复杂度在最差情况下会退化到 O(n)
JDK1.8中
使用一个 Node 数组来存储数据,但这个 Node 可能是链表结构,也可能是红黑树结构

  • 如果插入的 key 的 hashcode 相同,那么这些key也会被定位到 Node 数组的同一个格子里。
  • 如果同一个格子里的key不超过8个,使用链表结构存储。
  • 如果超过了8个,那么会调用 treeifyBin 函数,将链表转换为红黑树。

那么即使 hashcode 完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销
也就是说put/get的操作的时间复杂度最差只有 O(log n)
听起来挺不错,但是真正想要利用 JDK1.8 的好处,有一个限制:
key的对象,必须正确的实现了 Compare 接口
如果没有实现 Compare 接口,或者实现得不正确(比方说所有 Compare 方法都返回0)
那 JDK1.8 的 HashMap 其实还是慢于 JDK1.7 的 。
image.png
简单的测试数据如下:
向 HashMap 中 put/get 1w 条 hashcode 相同的对象
JDK1.7: put 0.26s , get 0.55s
JDK1.8 (未实现 Compare 接口): put 0.92s , get 2.1s
但是如果正确的实现了 Compare 接口,那么 JDK1.8 中的 HashMap 的性能有巨大提升,这次 put/get 100W条 hashcode 相同的对象
JDK1.8 (正确实现 Compare 接口,): put/get 大概开销都在320 ms 左右

1.6 Arrays.sort 和 Collections.sort 实现原理 和区别

Collection和Collections区别 java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。
java.util.Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、 线程安全等操作。 然后还有混排(Shuffling)、反转(Reverse)、替换所有的元素(fill)、拷贝(copy)、返 回Collections中最小元素(min)、返回Collections中最大元素(max)、返回指定源列表中最后一次出现指定目 标列表的起始位置( lastIndexOfSubList )、返回指定源列表中第一次出现指定目标列表的起始位置 ( IndexOfSubList )、根据指定的距离循环移动指定列表中的元素(Rotate);
事实上Collections.sort方法底层就是调用的array.sort方法,

  1. public static void sort(Object[] a) {
  2. if (LegacyMergeSort.userRequested)
  3. legacyMergeSort(a);
  4. else
  5. ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
  6. }
  7. //void java.util.ComparableTimSort.sort()
  8. static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen)
  9. {
  10. assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
  11. int nRemaining = hi - lo;
  12. if (nRemaining < 2)
  13. return; // Arrays of size 0 and 1 are always sorted
  14. // If array is small, do a "mini-TimSort" with no merges
  15. if (nRemaining < MIN_MERGE) {
  16. int initRunLen = countRunAndMakeAscending(a, lo, hi);
  17. binarySort(a, lo, hi, lo + initRunLen);
  18. return;
  19. }
  20. }

legacyMergeSort (a):归并排序 ComparableTimSort.sort() : Timsort 排序
Timsort 排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法
Timsort的核心过程
TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分 区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这 些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保 存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。
综上述过程,Timsort算法的过程包括
(0)如何数组长度小于某个值,直接用二分插入排序算法
(1)找到各个run,并入栈
(2)按规则合并run

1.7 LinkedHashMap 的应用

于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used) 最近最少使用简单缓存。
也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在 LinkedHashMap 上扩展的。

1.8 集合有哪些类?

  • Set
    • TreeSet 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
    • HashSet 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
    • LinkedHashSet 具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
  • List
    • ArrayList 基于动态数组实现,支持随机访问。
    • Vector 和 ArrayList 类似,但它是线程安全的。
    • LinkedList 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
  • Queue

    • LinkedList 可以用它来实现双向队列。
    • PriorityQueue 基于堆结构实现,可以用它来实现优先队列。

      1.9 ArrayList的底层?

      ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。
      image.png

      2 Java基础面试题

      2.1 final finally finalize

  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。

  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。

    2.2 对象的四种引用

    强引用 只要引用存在,垃圾回收器永远不会回收

    1. Object obj = new Object();
    2. User user=new User();

    可直接通过obj取得对应的对象 如 obj.equels(new Object()); 而这样 obj 对象对后面 new Object 的一个强 引用,只有当 obj 这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
    软引用 非必须引用,内存溢出之前进行回收,可以通过以下代码实现

    1. Object obj = new Object();
    2. SoftReference<Object> sf = new SoftReference<Object>(obj);
    3. obj = null;
    4. sf.get();//有时候会返回null

    这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象 时,则返回null; 软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的 真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
    弱引用 第二次垃圾回收时回收,可以通过如下代码实现

    1. Object obj = new Object();
    2. WeakReference<Object> wf = new WeakReference<Object>(obj);
    3. obj = null;
    4. wf.get();//有时候会返回null
    5. wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

    弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时, 将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记。
    ThreadLocal 中有使用到弱引用,

    1. public class ThreadLocal<T> {
    2. static class ThreadLocalMap {
    3. static class Entry extends WeakReference<ThreadLocal<?>> {
    4. /** The value associated with this ThreadLocal. */
    5. Object value;
    6. Entry(ThreadLocal<?> k, Object v) {
    7. super(k);
    8. value = v;
    9. }
    10. }
    11. //....
    12. }
    13. //.....
    14. }

    虚引用 垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

    1. Object obj = new Object();
    2. PhantomReference<Object> pf = new PhantomReference<Object>(obj);
    3. obj=null;
    4. pf.get();//永远返回null
    5. pf.isEnQueued();//返回是否从内存中已经删除

    虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引 用。虚引用主要用于检测对象是否已经从内存中删除。

    2.3 Java获取反射的三种方法

    1.通过new对象实现反射机制
    2.通过路径实现反射机制
    3.通过类名实现反射机制

    1. public class Student {
    2. private int id;
    3. String name;
    4. protected boolean sex;
    5. public float score;
    6. }
    7. public class Get {
    8. //获取反射机制三种方式
    9. public static void main(String[] args) throws ClassNotFoundException {
    10. //方式一(通过建立对象)
    11. Student stu = new Student();
    12. Class classobj1 = stu.getClass();
    13. System.out.println(classobj1.getName());
    14. //方式二(所在通过路径-相对路径)
    15. Class classobj2 = Class.forName("fanshe.Student");
    16. System.out.println(classobj2.getName());
    17. //方式三(通过类名)
    18. Class classobj3 = Student.class;
    19. System.out.println(classobj3.getName());
    20. }
    21. }

    2.4 Java反射机制

    JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
    image.png
    Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每 个类都实现了 Member 接口)。
    这些类型的对象时由 JVM 在运行时创建的,用以表示未知类里对应的成员。
    这样你就可以使用 Constructor 创建新的对象,用 get() 和 set() 方法读取和修改与 Field 对象关联的字段,用 invoke() 方法调用与 Method 对象关联的方法。
    另外,还可以调用 getFields() getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。
    这样匿名对象的信息 就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

    1. import java.lang.reflect.Constructor;
    2. public class ReflectTest {
    3. public static void main(String[] args) throws Exception {
    4. Class clazz = null;
    5. clazz = Class.forName("com.jas.reflect.Fruit");
    6. Constructor<Fruit> constructor1 = clazz.getConstructor();
    7. Constructor<Fruit> constructor2 = clazz.getConstructor(String.class);
    8. Fruit fruit1 = constructor1.newInstance();
    9. Fruit fruit2 = constructor2.newInstance("Apple");
    10. }
    11. }
    12. class Fruit{
    13. public Fruit(){
    14. System.out.println("无参构造器 Run...........");
    15. }
    16. public Fruit(String type){
    17. System.out.println("有参构造器 Run..........." + type);
    18. }
    19. }

    运行结果: 无参构造器 Run……….. 有参构造器 Run………..Apple

    2.4.1 getName、getCanonicalName与getSimpleName的区别?

  • getSimpleName:只获取类名

  • getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。
  • getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。

    2.5 Cloneable 接口实现原理

    Cloneable接口是Java开发中常用的一个接口, 它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中, 注意,这里所说的“拷贝”拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。
    在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方 法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要 具备拷贝功能的类实现Cloneable接口,并重写clone()方法,就可以通过调用clone()方法的方式简洁地实现实例 拷贝功能
    深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问 题,但是我们在这幸好用的是Java。
    虽然Java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们 还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。
    浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
    深拷 贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
    举例来说更加清楚:对象 A1 中包含对 B1 的引 用, B1 中包含对 C1 的引用。浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引 用。
    深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 ( B1 的 copy )的引用, B2 中包含 对 C2 ( C1 的 copy )的引用。
    若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝。

    2.6 异常分类以及处理机制

    Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
    Throwable又派生出Error类和Exception类。 错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。
    因此,程序员应该关注Exception为父类的分支下的各种异常类。
    异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用, 是异常处理的核心
    总体上我们根据 Javac 对异常的处理要求,将异常类分为二类。
    非检查异常( unckecked exception ): Error 和 RuntimeException 以及他们的子类。
    javac 在编译时, 不会提示和发现这样的异常,不要求在程序处理这些异常。
    所以如果愿意,我们可以编写代码处理(使用 try… catch…finally )这样的异常,也可以不处理。
    对于这些异常,我们应该修正代码,而不是去通过异常处理器处 理 。
    这样的异常发生的原因多半是代码写的有问题。
    如除0错误 ArithmeticException ,错误的强制类型转换错 误 ClassCastException ,数组索引越界 ArrayIndexOutOfBoundsException ,使用了空对象 NullPointerException 等等。
    检查异常( checked exception ):除了 Error 和 RuntimeException 的其它异常。
    javac 强制要求程序员 为这样的异常做预备处理工作(使用 try…catch…finally 或者 throws )。
    在方法中要么用 try-catch 语句捕 获它并处理,要么用throws子句声明抛出它,否则编译不会通过。
    这样的异常一般是由程序的运行环境导致的。因 为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样 的异常时刻准备着。
    如 SQLException , IOException , ClassNotFoundException 等。
    需要明确的是:检查和非检查是对于 javac 来说的,这样就很好理解和区分了。

    2.7 wait 和 sleep 的区别

    源码如下

    1. public class Thread implements Runnable {
    2. public static native void sleep(long millis) throws InterruptedException;
    3. public static void sleep(long millis, int nanos) throws InterruptedException {
    4. if (millis < 0) {
    5. throw new IllegalArgumentException("timeout value is negative");
    6. }
    7. if (nanos < 0 || nanos > 999999) {
    8. throw new IllegalArgumentException(
    9. "nanosecond timeout value out of range");
    10. }
    11. if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
    12. millis++;
    13. }
    14. sleep(millis);
    15. }
    16. //...
    17. }
    18. public class Object {
    19. public final native void wait(long timeout) throws InterruptedException;
    20. public final void wait(long timeout, int nanos) throws InterruptedException {
    21. if (timeout < 0) {
    22. throw new IllegalArgumentException("timeout value is negative");
    23. }
    24. if (nanos < 0 || nanos > 999999) {
    25. throw new IllegalArgumentException(
    26. "nanosecond timeout value out of range");
    27. }
    28. if (nanos > 0) {
    29. timeout++;
    30. }
    31. wait(timeout);
    32. }
    33. //...
    34. }

    1、 sleep 来自 Thread 类,和 wait 来自 Object 类。
    2、最主要是sleep方法没有释放锁,而wait方法释放了 锁,使得其他线程可以使用同步控制块或者方法。
    3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使 用范围)
    4、 sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常
    (1)sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可 运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中过 程中有可能被其他对象调用它的 interrupt() ,产生 InterruptedException 异常,如果你的程序不捕获这个异 常,线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语 句块(可能还有 finally 语句块)以及以后的代码。
    注意
    sleep() 方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep() 让t对象进入 sleep ,这样 的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程 (
    (2)wait 属于 Object 的成员方法,一旦一个对象调用了wait方法,必须要采用 notify() 和 notifyAll() 方法 唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有 同步资源,而不限于这个被调用了 wait() 方法的对象。 wait() 方法也同样会在 wait 的过程中有可能被其他对 象调用 interrupt() 方法而产生 。

    2.8 数组在内存中如何分配

    对于 Java 数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目:
    静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如:

    1. //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4
    2. String[] computers = {"Dell", "Lenovo", "Apple", "Acer"}; //①
    3. //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3
    4. String[] names = new String[]{"多啦A梦", "大雄", "静香"}; //②
    5. 动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如:
    6. //只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null
    7. String[] cars = new String[4]; //③

    因为 Java 数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况如下图所 示:
    由上图可知,静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,
    而动态初始化 方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配 了初始化值 null ,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值 也是对应的默认值0。

    2.9 对equal()和hashCode()的理解?

  • 为什么在重写 equals 方法的时候需要重写 hashCode 方法?

因为有强制的规范指定需要同时重写 hashcode 与 equal 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。

  • 有没有可能两个不相等的对象有相同的 hashcode?

有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。

  • 两个相同的对象会有不同的 hash code 吗?

不能,根据 hash code 的规定,这是不可能的。

2.10 final、finalize 和 finally 的不同之处?

  • final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。
  • Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。
  • finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。

    2.11 String、StringBuffer与StringBuilder的区别?

    第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。
    第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。

    2.12 this() & super()在构造方法中的区别?

  • 调用super()必须写在子类构造方法的第一行, 否则编译不通过

  • super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行
  • 尽管可以用this调用一个构造器, 却不能调用2个
  • this和super不能出现在同一个构造器中, 否则编译不通过
  • this()、super()都指的对象,不可以在static环境中使用
  • 本质this指向本对象的指针。super是一个关键字

    2.13 Java异常类层次结构?

  • Throwable是 Java 语言中所有错误与异常的超类。

    • Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
    • Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。

image.png

  • 运行时异常

都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

  • 非运行时异常 (编译异常)

是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。