原创

Java面试题一

基础篇

1 Java基础

1.1 面向对象的特征有哪些方面?

  • 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

  • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java 与模式》或《设计模式精解》中关于桥梁模式的部分)。

  • 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。

  • 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B系统有多种提供服务的方式,但一切对 A 系统来说都是透明的(就像电动剃须刀是 A 系统,它的供电系统是 B 系统,B 系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A 系统只会通过 B 类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

1.2 short s1 = 1; s1 = s1 + 1;有错吗? short s1 = 1; s1 += 1;有错吗?

  • 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
  • 对于 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

2.3 &和&&的区别?

  • &运算符有两种用法:(1)按位与;(2)逻辑与。
  • &&运算符是短路与运算。
  • 逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为:username != null &&!username.equals(“”),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

2.4 解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、Survivor(又可分为 From Survivor 和 To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、”hello”和常量都是放在常量池中,常量池是方法区的一部分,。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。

String str = new String("hello");

上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而”hello”这个字面量是放在方法区的。

  • 补充 1:较新版本的 Java(从 Java 6 的某个更新开始)中,由于 JIT 编译器的发展和”逃逸分析”技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。
  • 补充 2:运行时常量池相当于 Class 文件常量池具有动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String类的 intern()方法就是这样的。

看看下面代码的执行结果是什么并且比较一下 Java 7 以前和以后的运行结果是否一致。

String s1 = new StringBuilder("go").append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern() == s2);

2 多线程

2.1 编写多线程程序有几种实现方式?

Java 5 以前实现多线程有两种实现方法:一种是继承 Thread 类;另一种是实现Runnable 接口。两种方式都要通过重写 run()方法来定义线程的行为,推荐使用后者,因为 Java 中的继承是单继承,一个类有一个父类,如果继承了 Thread 类就无法再继承其他类了,显然使用 Runnable 接口更为灵活。

补充:Java 5 以后创建线程还有第三种方式:实现 Callable 接口,该接口中的 call方法可以在线程执行结束时产生一个返回值,代码如下所示:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyTask implements Callable<Integer> {
    private int upperBounds;

    public MyTask(int upperBounds) {
        this.upperBounds = upperBounds;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1; i <= upperBounds; i++) {
            sum += i;
        }
        return sum;
    }

    class Test {
        public static void main(String[] args) throws Exception {
            List<Future<Integer>> list = new ArrayList<>();
            ExecutorService service = Executors.newFixedThreadPool(10);
            for(int i = 0; i < 10; i++) {
                list.add(service.submit(new MyTask((int) (Math.random() *100))));
            }
        int sum = 0;
        for(Future<Integer> future : list) {
            // while(!future.isDone()) ;
            sum += future.get();
        }
        System.out.println(sum);
        }
}

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

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

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

2.3 线程的 sleep()方法和 yield()方法有什么区别?

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  2. 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
  3. sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
  4. sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。

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

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

2.5 线程的基本状态以及状态之间的关系?

file

其中 Running 表示运行状态,Runnable 表示就绪状态(万事俱备,只欠CPU),Blocked 表示阻塞状态,阻塞状态又有多种情况,可能是因为调用 wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了 sleep()方法或 join()方法等待休眠或其他线程结束,或是因为发生了 I/O 中断。

2.6 简述 synchronized 和 java.util.concurrent.locks.Lock的异同?

Lock 是 Java 5 以后引入的新的 API,和关键字 synchronized 相比主要相同点:Lock 能完成 synchronized 所实现的所有功能;主要不同点:Lock 有比synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且最好在 finally 块中释放(这是释放外部资源的最好的地方)。

2.7 什么是线程池(thread pool)?

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

  1. newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  2. newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  4. newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  5. newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

2.8 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?

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

2.9 synchronized 关键字的用法?

synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用 synchronized(对象) { … }定义同步代码块,或者在声明方法时将 synchronized 作为方法的修饰符。

2.10 举例说明同步和异步。

如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

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

启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

正文到此结束
相关文章
该篇文章的评论功能已被站长关闭
本文目录