互联网 Java 工程师面试题(Java 面试题二)

48、运行时异常与受检异常有何异同/h2>

答: 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常 操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就 不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可 能因使用的问题而引发。Java 编译器要求方法必须声明抛出可能发生的受检异常, 但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对 象程序设计中经常被滥用的东西,在 Effective Java 中对异常的使用给出了以下指 导原则:

  • 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调 用者为了正常的控制流而使用异常)
  • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
  • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
  • 优先使用标准的异常
  • 每个方法抛出的异常都要有文档
  • 保持异常的原子性
  • 不要在 catch 中忽略掉捕获到的异常

49、列出一些你常见的运行时异常/h2>

答:

  • ArithmeticException(算术异常)
  • ClassCastException (类转换异常)
  • IllegalArgumentException (非法参数异常)
  • IndexOutOfBoundsException (下标越界异常)
  • NullPointerException (空指针异常)
  • SecurityException (安全异常)

50、阐述 final、finally、finalize 的区别。

答:

  • final:修饰符(关键字)有三种用法:如果一个类被声明为 final,意味 着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。将 变量声明为 final,可以保证它们在使用中不被改变,被声明为 final 的变量必须 在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为 final 的方 法也同样只能使用,不能在子类中被重写。
  • finally:通常放在 try…catch…的后面构造总是执行代码块,这就意味着 程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以 将释放外部资源的代码写在 finally 块中。
  • finalize:Object 类中定义的方法,Java 中允许使用 finalize()方法在垃 圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收 集器在销毁对象时调用的,通过重写 finalize()方法可以整理系统资源或者执行 其他清理工作。

51、类 ExampleA 继承 Exception,类 ExampleB 继承 ExampleA。

有如下代码片断:

**请问执行此段代码的输出是什么/p>

答: 输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型], 抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的 异常)

面试题 – 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书)

52、List、Set、Map 是否继承自 Collection 接口/h2>

答: List、Set 是,Map 不是。Map 是键值对映射容器,与 List 和 Set 有明显的区别, 而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List 是线性结构的容器,适用于按数值索引访问元素的情形。

53、阐述 ArrayList、Vector、LinkedList 的存储性能和特性。

答: ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的 数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉 及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 中的方法由 于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上ArrayList 差,因此已经是 Java 中的遗留容器。LinkedList 使用双向链表实现存 储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索 引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更 高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录项的前后项即可,所以插入速度较快。Vector 属于遗留容器(Java 早期的版本中 提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties 都是遗留容器),已经不推荐使用,但是由于 ArrayList 和 LinkedListed 都是非 线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类 Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这 是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强 实现)。

补充:遗留容器中的 Properties 类和 Stack 类在设计上有严重的问题,Properties 是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个 Hashtable 并将其两个泛型参数设置为 String 类型,但是 Java API 中的 Properties 直接继承了 Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是 Has-A 关系而不是 Is-A 关系,另一方面容器都属于工具类,继承工具 类本身就是一个错误的做法,使用工具类最好的方式是 Has-A 关系(关联)或 Use-A 关系(依赖)。同理,Stack 类继承 Vector 也是不正确的。Sun 公司的工 程师们也会犯这种低级错误,让人唏嘘不已。

54、Collection 和 Collections 的区别/h2>

答: Collection 是一个接口,它是 Set、List 等容器的父接口;Collections 是个一个 工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、 排序、线程安全化等等。

55、List、Map、Set 三个接口存取元素时,各有什么特点/h2>

答: List 以特定索引来存取元素,可以有重复元素。Set 不能存放重复元素(用对象的 equals()方法来区分元素是否重复)。Map 保存键值对(key-value pair)映射, 映射关系可以是一对一或多对一。Set 和 Map 容器都有基于哈希存储和排序树的 两种实现版本,基于哈希存储的版本理论存取时间复杂度为 O(1),而基于排序树 版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达 到排序和去重的效果。

56、TreeMap 和 TreeSet 在排序时如何比较元素Collections 工具类中的 sort()方法如何比较元素/h2>

答: TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比 较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。 TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元 素进行排序。Collections 工具类的 sort 方法有两种重载的形式,第一种要求传入 的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;第二 种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是 Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于 一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对 回调模式的应用(Java 中对函数式编程的支持)。

例子 1:

例子 2:

57、Thread 类的 sleep()方法和对象的 wait()方法都可以让线 程暂停执行,它们有什么区别/h2>

答: sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程 暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保 持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第 66 题中的线 程状态转换图)。wait()是 Object 类的方法,调用对象的 wait()方法导致当前线 程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用 对象的 notify()方法(或 notifyAll()方法)时才能唤醒等待池中的线程进入等锁池 (lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线 程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数 据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线 程是进程的一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运 行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程 在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编 程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友 好的,因为它可能占用了更多的 CPU 资源。当然,也不是线程越多,程序的性能 就越好,因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js 就采用了单线程异步 I/O 的工作模式。

58、线程的 sleep()方法和 yield()方法有什么区别/h2>

答:

① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的 线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的 机会;

② 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转 入就绪 (ready)状态;

③ sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异 常;

④ sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。

59、当一个线程进入一个对象的 synchronized 方法 A 之后, 其它线程是否可进入此对象的 synchronized 方法 B/h2>

答: 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静 态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入 A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注 意不是等待池哦)中等待对象的锁。

60、请说出与线程同步以及线程调度相关的方法。

答:

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用 此方法要处理 InterruptedException 异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并 不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且 与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给 所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

提示:关于 Java 多线程和并发编程的问题,建议大家看我的另一篇文章《关于 Java 并发编程的总结和思考》。 补充:Java 5 通过 Lock 接口提供了显式的锁机制(explicit lock),增强了灵活 性以及对线程的协调。Lock 接口中定义了加锁(lock())和解锁(unlock())的方 法,同时还提供了 newCondition()方法来产生用于线程之间通信的 Condition 对 象;此外,Java 5 还提供了信号量机制(semaphore),信号量可以用来限制对 某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信 号量的许可(调用 Semaphore 对象的 acquire()方法);在完成对资源的访问后, 线程必须向信号量归还许可(调用 Semaphore 对象的 release()方法)。

下面的例子演示了 100 个线程同时向一个银行账户中存入 1 元钱,在没有使用同 步机制和使用同步机制情况下的执行情况。

  • 银行账户类:
  • 存钱线程类:
  • 测试类:

在没有同步的情况下,执行结果通常是显示账户余额在 10 元以下,出现这种状况 的原因是,当一个线程 A 试图存入 1 元的时候,另外一个线程 B 也能够进入存款 的方法中,线程 B 读取到的账户余额仍然是线程 A 存入 1 元钱之前的账户余额, 因此也是在原来的余额 0 上面做了加 1 元的操作,同理线程 C 也会做类似的事情, 所以最后 100 个线程执行结束时,本来期望账户余额为 100 元,但实际得到的通 常在 10 元以下(很可能是 1 元哦)。解决这个问题的办法就是同步,当一个线程 对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行 操作,代码有如下几种调整方案:

  • 在银行账户的存款(deposit)方法上同步(synchronized)关键字
  • 在线程调用存款方法时对银行账户进行同步
  • 通过 Java 5 显示的锁机制,为每个银行账户创建一个锁对象,在存款操 作进行加锁和解锁的操作

互联网 Java 工程师面试题(Java 面试题二)

来源:程序猿难

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年3月25日
下一篇 2020年3月25日

相关推荐