Java面试题:基础

Java面试题:基础


1. 说一下ArrayList底层实现方式

  1. ArrayList 通过数组实现,一旦我们实例化 ArrayList 无参数构造函数默认 为数组初始化长度为 10
  2. add 方法底层实现如果增加的元素个数超过了 10 个,那么 ArrayList 底层会 新生成一个数组,长度为原数组的 1.5 倍+1,然后将原数组的内容复制到新数 组当中,并且后续增加的内容都会放到新数组当中。当新数组无法容纳增加的 元素时,重复该过程。是一旦数组超出长度,就开始扩容数组。扩容数组调用 的方法 Arrays.copyOf(objArr, objArr.length + 1);

2.说一下LinkedList底层实现方式

LinkedList 底层的数据结构是基于双向循环链表的,且头结点中不存放数据, 如下:

LinkedList实现一

既然是双向链表,那么必定存在一种数据结构——我们可以称之为节点,节点 实例保存业务数据,前一个节点的位置信息和后一个节点位置信息,如下图所 示:

LinkedList数据结构


3.说一下HashMap底层实现方式

HashMap 是由数组+链表组成

put 方法底层实现: 通过 key 的 hash 值%Entry[].length 得到该存储的下标位置,如果多个 key 的 hash 值%Entry[].length 值相同话就就会存储到该链表的后面。


4.关于HashMap面试题

  • “你用过 HashMap 吗?” “什么是 HashMap?你为什么用到它?”

    HashMap 可以接受 null 键值和值,而 Hashtable 则不能;

    HashMap 是非 synchronized所以HashMap 很快;

    以及 HashMap 储存的是键值对等等。

  • “你知道 HashMap 的工作原理吗?” “你知道 HashMap 的 get()方法的工作原理 吗?”

    “HashMap 是基于 hashing 的原理,我们使 用 put(key, value)存储对象到 HashMap 中,使用 get(key)从 HashMap 中获取 对象。当我们给 put()方法传递键和值时,我们先对键调用 hashCode()方法, 返回的 hashCode 用于找到 bucket 位置来储存 Entry 对象。”这里关键点在于 指出, HashMap 是在 bucket 中储存键对象和值对象,作为 Map.Entry。这一点 有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅 只在 bucket 中存储值的话,你将不会回答如何从 HashMap 中获取对象的逻辑。

  • “当两个对象的 hashcode 相同会发生什么?”

    “因为 hashcode 相同,所以它们的 bucket 位置相同, ‘碰撞’ 会发生。因为 HashMap 使用链表存储对象,这个 Entry(包含有键值对 的 Map.Entry 对象)会存储在链表中。”这个答案非常的合理,虽然有很多种处 理碰撞的方法,这种方法是最简单的。

  • “如果两个键的 hashcode 相同,你如何获取值对象?”

    找到 bucket 位置之后,会调用 keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

    许多情况下,面试者会在这个环节中出错,因为他们混淆了 hashCode()和 equals()方法。因为在此之前 hashCode()屡屡出现,而 equals()方法仅仅在获 取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作 final 的对象,并且采用合适的 equals()和 hashCode()方法的话,将会减少碰 撞的发生,提高效率。不可变性使得能够缓存不同键的 hashcode,这将提高整 个获取对象的速度,使用 String, Interger 这样的 wrapper 类作为键是非常好 的选择。

  • “如果 HashMap 的大小超过了负载因子(load factor)定义的容量,怎么办?”

    默认的负载 因子大小为 0.75,也就是说,当一个 map 填满了 75%的 bucket 时候,和其它集 合类(如 ArrayList 等)一样,将会创建原来 HashMap 大小的两倍的 bucket 数 组,来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。这个过 程叫作 rehashing,因为它调用 hash 方法找到新的 bucket 位置。

  • “你了解重新调整 HashMap 大小存在什么问题吗?”

    当重新调整 HashMap 大小的时候,存在条件竞争(race condition),因为如果两个线程都发 现 HashMap 需要重新调整大小了,它们会同时试着调整大小。在调整大小的过 程中,存储在链表中的元素的次序会反过来,因为移动到新的 bucket 位置的时 候, HashMap 并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾 部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

  • 为什么 String, Interger 这样的 wrapper 类适合作为键?

    String, Interger 这样的 wrapper 类作为 HashMap 的键是再适合不过了,而且 String 最为常用。因为 String 是不可变的,也是 final 的,而且已经重写了 equals()和 hashCode()方法了。其他的 wrapper 类也有这个特点。不可变性是 必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和 获取时返回不同的 hashcode的话,那么就不能从 HashMap中找到你想要的对象。 不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个 field 声明成 final 就能保证 hashCode 是不变的,那么请这么做吧。因为获取对象的时候要 用到 equals()和 hashCode()方法,那么键对象正确的重写这两个方法是非常重 要的。如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会 小些,这样就能提高 HashMap 的性能。

  • 我们可以使用自定义的对象作为键吗?

    只要它遵守了 equals()和 hashCode()方法的定义规则,并且当对象插入到 Map 中之后将不会 再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件, 因为当它创建之后就已经不能改变了

  • 我们可以使用 CocurrentHashMap 来代替 Hashtable 吗?

    可以,因为CocurrentHashMap性能更加,而且HashTable已经被淘汰了,不要再新代码中应用它,也可从CocurrentHashMap分段锁回答。

总结

  • HashMap 的工作原理

    HashMap 基于 hashing 原理,我们通过 put()和 get()方法储存和获取对象。当 我们将键值对传递给 put()方法时,它调用键对象的 hashCode()方法来计算 hashcode,让后找到 bucket 位置来储存值对象。当获取对象时,通过键对象的 equals()方法找到正确的键值对,然后返回值对象。 HashMap 使用链表来解决 碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap 在 每个链表节点中储存键值对对象。

  • 当两个不同的键对象的 hashcode 相同时会发生什么

    它们会储存在同一个 bucket 位置的链表中。键对象的 equals()方法用来找到 键值对。 因为 HashMap 的好处非常多,我曾经在电子商务的应用中使用 HashMap 作为缓存。因为金融领域非常多的运用 Java,也出于性能的考虑,我们会经常用到 HashMap 和 ConcurrentHashMap。


5.ArrayList集合加入一万条数据,应该怎么提高效率?

因为 ArrayList 的底层是数组实现,并且数组的默认值是 10,如果插入 10000 条要不断的扩容,耗费时间,所以我们调用 ArrayList 的指定容量的构造 器方法 ArrayList(int size) 就可以实现不扩容,就提高了性能


6.IO和NIO的区别


7.多线程的安全问题如何保证?

我们在考虑过线程安全问题的时候,要避免不必要的同步;过多的同步会造成死锁以及昂贵的锁竞争代价。除此以外,程序设计的时候,如果不得已面临线程安全的情况,要采取对应的措施;能保证线程的机制是锁机制、 AQS 机制,对象头补齐机制等;根据环境和特定的业务挑选最合适的方法。 我们常用的有三种,同步代码块,同步方法,同步锁

  1. 同步代码块:

    在代码块声明上 加上 synchronized

    synchronized (锁对象) {
    	//可能会产生线程安全问题的代码
    }
    

    同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对 象才能够保证线程安全。

  2. 同步方法:

    在方法声明上加上 synchronized

    public synchronized void method(){
    	//可能会产生线程安全问题的代码
    }
    

    同步方法中的锁对象是 this 静态同步方法: 在方法声明上加上 static synchronized 静态同步方法中的锁对象是 类名.class

  3. 同步锁

    Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手 动获取锁和释放锁。

除了 sync 锁之外, Java 提供了一些线程安全容器,要活用这些容器。 对于 Java 而言,线程安全工具已经足够多了。基本上能满足需求。


8.反射怎么理解?说一下反射经典的应用

反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可 能之前用不到所以不用加载到 jvm,而是在运行时根据需要才加载,这样的好 处对于服务器来说不言而喻,举个例子我们的项目底层有时是用 mysql,有时 用 oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假 设 com.java.dbtest.myqlConnection, com.java.dbtest.oracleConnection 这两个类我们要用,这时候我们的程序就写得比较动态化,通过 Class tc = Class.forName(“com.java.dbtest.TestConnection”);通过类的全类名让 jvm 在服务器中找到并加载这个类,而如果是 oracle 则传入的参数就变成另一个 了。这时候就可以看到反射的好处了,这个动态性就体现出 java 的特性了!举 个例子,使用 spring 中会发现当你配置各种各样的 bean 时,是以配置文件的 形式配置的,你需要用到哪些 bean 就配哪些, spring 容器就会根据你的需求 去动态加载,你的程序就能健壮地运行。


9.编写多线程程序的几种实现方式(多线程的创建方式)?

  1. 通过实现Thread类;
  2. 通过Runnable接口(推荐使用,因为Java中是单继承,一个类只有一个父类,若继承了Thread类,就无法再继承其他类,显然实现Runnable接口更为灵活)
  3. 通过实现Callable接口(Java5之后)

10.启动一个线程是调用run()方法还是start()方法?

启动一个线程是调用start()方法,使线程所代表的的虚拟机处于可运行状态,这意味着它可以有JVM调用并执行,这并不意味着线程就会立即运行.

run()方法是线程启动后要进行回调(callback).


11.sleep()和wait()有什么区别?

  1. 每个对象都有一个锁来控制同步访问,Synchronize关键字可以和对象的锁交互,来实现同步方法或同步块.

    1. sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁.);
    2. wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行.(注意:notify的作用相当于叫醒睡醒的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);
  2. sleep()方法可以在任何地方使用;

    wait()方法则只能在同步方法或同步块中使用;

  3. sleep()是线程类Thread的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;

    wait()Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/ notifyAll()唤醒指定的线程过着所有线程,才会进入锁池,再次获得对象锁才会进入运行状态;


12.线程的五种状态和转换方式?

线程从创建、运行到结束总是处于下面的五个状态之一:新建状态就绪状态运行状态阻塞状态死亡状态;

  1. 新建状态(NEW)

    当使用new操作符创建一个线程时,此时程序还没有开始运行线程中的代码;

  2. 就绪状态(Runnable)

    一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法.当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法.当start()方法返回后,线程就处于就绪状态.

    处于就绪状态的线程并不一定立即运行线程.因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态.因此此时可能有多个线程处于就绪状态.对多个就绪状态的线程是由Java运行时系统的线程调度程序来调度的.

  3. 运行状态(running)

    当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法

  4. 阻塞状态(blocked)

    线程运行过程中,可能由于各种原因进入阻塞状态:

    1. 线程通过调用sleep方法进入睡眠状态;
    2. 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
    3. 线程试图得到一个锁,但是该锁正被其他线程持有;
    4. 线程在等待某个触发条件.

    所谓的阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态.

  5. 死亡状态(dead)

    有两个原因会导致线程死亡;

    1. run()方法正常退出而自然死亡;
    2. 一个未捕获的异常终止了run()方法而使线程猝死;

    为了确定线程在当前是否存活着(就是要么可运行的,要么是被阻塞了),需要使用isAlive()方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false;

五种线程状态和转换方式

13.什么是死锁?

两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁.结果两个进程都陷入无限等待中.

14.java虚拟机的运行机制?

java虚拟机的运行机制

https://blog.csdn.net/u011546655/article/details/52175550


持续更新中…


转载请注明原地址,宋德凌的博客:http://CoderOfSong.github.io 谢谢!

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦