JavaSE面试知识整理

2016/09/08 面试

介绍JavaSE的一些基础面试知识

1、线程、进程的基本概念和它们之间的区别。

线程的基本概念

  • 线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

进程的基本概念

  • 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 进程的概念主要有两点: 第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。 第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

进程和线程的区别

  • 调度:线程作为CPU调度和分配的基本单位,进程作为操作系统分配资源的基本单位。
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源(或只拥有比不可少的资源,如程序计数器,一组寄存器和栈),但可以访问隶属于进程的资源,即与其他线程共享资源。
  • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
  • 进程可以独立运行,线程必须依赖进程运行
  • 一个程序至少有一个进程,一个进程至少有一个线程。

线程和进程的优缺点

  • 线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

2、Java中线程的几种状态、并作详细说明。

  • 创建(new): 通过New关键字创建了Thread类(或其子类)的对象,此时并没有调用对象的start()方法。
  • 就绪(runnable): 调用start()方法后,运行run()方法之前,等待获取CPU的使用权。
  • 运行(running): 就绪状态的线程获取了CPU时间片,执行程序代码。
  • 阻塞(blocked):阻塞的情况分三种:
    • 等待阻塞:运行( running )的线程执行 wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。
    • 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
    • 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。

    当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。

  • 消亡(dead): 线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。

    参考图示

线程状态图

线程状态图

线程状态图

注意:sleep(long mills):会释放CPU资源,但不会释放锁。wait():会释放CPU资源和锁资源。

3、volatile关键字说明。

volatile 关键字用在多线程同步中,可保证读取的可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的读取值,而非 cache 中。

但多个线程对 volatile 的写操作,无法保证线程安全。 例如:假如线程1,线程2 在进行 read, load 操作中,发现主内存中 count 的值都是5,那么都会加载这个最新的值,在线程1堆 count 进行修改之后,会 write 到主内存中,主内存中的 count 变量就会变为 6 ; 线程2由于已经进行 read, load 操作,在进行运算之后,也会更新主内存 count 的变量值为 6 ; 导致两个线程及时用 volatile 关键字修改之后,还是会存在并发的情况。

4、Java集合泛型类关系图。

Java集合框架图 Java集合框架图 Java集合框架图

5、Collection 和 Collections的区别?

  • Collection是一个接口,所有的集合类(除Map外)都要继承(实现)自该接口。它提供了对集合对象进行基本操作的通用接口方法。
  • Collections是一个包装类,它包含有各种有关集合操作的静态多态方法。(Collections是一个工具类,不能实例化)。

6、Java流类图结构。

Java IO 流图

7、访问修饰符public,private,protected,以及不写(默认)时的区别?

修饰符 当前类 同 包 子 类 其他包
public
protected ×
default × ×
private × × ×

8、int和Integer有什么区别?

Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是 Integer,从 Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

Java 为每个原始类型提供了包装类型:

  • 原始类型: boolean,char,byte,short,int,long,float,double
  • 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

扩展:说明下列代码的输出结果(结果:true,false)

public class Test03 {
    public static void main(String[] args) {
        Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
        System.out.println(f1 == f2);
        System.out.println(f3 == f4);
    }
}

解析: 如果不明就里很容易认为两个输出要么都是true要么都是false。首先需要注意的是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的 == 运算比较的不是值而是引用。

装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如果看看 valueOf 的源代码就知道发生了什么。

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

IntegerCache 是 Integer 的内部类,其代码如下所示:

/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */
 
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];
 
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
 
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
 
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
 
        private IntegerCache() {}
    }

简单的说,如果整型字面量的值在 -128 到 127 之间,那么不会new新的 Integer 对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1==f2 的结果是 true,而 f3==f4 的结果是 false。

9、&和&&的区别?

& 运算符有两种用法:(1)按位与;(2)逻辑与。

&& 运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true。&& 之所以称为短路运算是因为,如果 && 左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用 && 而不是 &。

举例:在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为:username != null && !username.equals(“”),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

10、什么是死锁?产生死锁的原因是什么?解决死锁的方法有哪些?

死锁定义: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁发生的四个必要条件:

  • 互斥条件:

    指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

  • 请求和保持条件:

    指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  • 剥夺条件:

    指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  • 环路等待条件:

    指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

产生原因:

  • 系统资源的竞争 通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

    竞争不可抢占性资源引起死锁:通常系统中所拥有的不可抢占性资源其数量不足以满足多个进程的运行需要,使得进程在运行过程中,会因争夺资源而陷入僵局。

  • 进程推进顺序不当引起死锁

    进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。

    信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

死锁处理策略

  1. 预防死锁

    设置某些限制条件,破坏产生死锁的四个必要条件中的一个或几个,以防止发生死锁。

  2. 避免死锁

    在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁。

  3. 死锁的检测及解除

    无需釆取任何限制性措施,允许进程在运行过程中发生死锁。通过系统的检测机构及时 地检测出死锁的发生,然后釆取某种措施解除死锁。

    预防死锁和避免死锁都属于事先预防策略,但预防死锁的限制条件比较严格,实现起来 较为简单,但往往导致系统的效率低,资源利用率低;避免死锁的限制条件相对宽松,资源 分配后需要通过算法来判断是否进入不安全状态,实现起来较为复杂。

死锁处理策略 各种可能模式 主要优点 主要缺点  
死锁预防 保守,宁可资源闲置 一次请求所有资源,资 源剥夺,资源按序分配 适用于做突发式处理 的进程,不必进行剥夺 效率低,进程初始化时 间延长;剥夺次数过多; 不便灵活申请新资源
死锁避免 是”预防“和”检测“ 的折中(在运行时判断是 否可能死锁) 寻找可能的安全允许 顺序 不必进行剥夺 必须知道将来的资源 需求;进程不能被长时间 阻塞
死锁检测 宽松,只要允许就分配 资源 定期检查死锁是否已 经发生 不延长进程初始化时 间,允许对死锁进行现场 处理 通过剥夺解除死锁,造 成损失

11、String和StringBuilder、StringBuffer的区别?

  • String是只读字符串,即不可变字符串。StringBuilder/StringBuffer的可变字符串,底层基于数组的方式实现。
  • 在对字符串进行操作(修改)时,String会产生新的对象,而StringBuilder/StringBuffer不会。
  • 由于在对字符串进行操作时,StringBuilder/StringBuffer不会产生新的对象,这样在频繁的对字符串进行修改等操作时,StringBuilder/StringBuffer效率更高。
  • StringBuilder线程不安全、StringBuffer线程安全。故StringBuilder效率更高。
  • 由于String是不可变的,所以String类型对象可以在编译期间得到优化,多个String对象可以共享同一个字符串值。这也是为什么下列程序的结果为false true true
String s1 = "Programming";
String s2 = new String("Programming");
String s3 = "Program" + "ming";
System.out.println(s1 == s2);  //false
System.out.println(s1 == s3);  //true
System.out.println(s1 == s1.intern());  //true  

12、String s = new String(“xyz”);创建了几个字符串对象?

两个对象,一个是静态区的”xyz”,一个是用new创建在堆上的对象。

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

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

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

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

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

14、sleep()方法和wait()方法的区别?

  • sleep()方法是Thread类的方法,wait() 方法是Object类的方法。
  • sleep() 不会释放对象锁,wait() 方法会释放对象锁。
  • sleep() 方法会抛出InterruptedException异常,wait()方法不会。

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

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

  • wait():

    使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁。

  • sleep():

    使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常。

  • notify():

    唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关。

  • notityAll():

    唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。

16、Java实现多线程的几种方式?

继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。

17、获得一个类的类对象有哪些方式?

  • 类型.class, 例如:String.class
  • 对象.getClass(), 例如: “hello”.getClass()
  • Class.forName(), 例如:Class.forName(“java.lang.String”)

18、如何通过反射创建对象?

1.通过对象调用 newInstance() 方法, 例如:String.class.newInstance().

2.通过类对象的 getConstructor() 或 getDeclaredConstructor() 方法获得构造器(Constructor)对象并调用其 newInstance() 方法创建对象,例如:String.class.getConstructor(String.class).newInstance(“Hello”).

19、用Java写一个单例类?

  • 饿汉式单例:
public class Singleton {
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}
  • 懒汉式单例:
public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static synchronized Singleton getInstance(){
        if (instance == null) instance  new Singleton();
        return instance;
    }
}

注意:实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。

20、高逼格冒泡排序。

接口:

import java.util.Comparator;
/**
 * 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换)
 * @author骆昊
 *
 */
public interface Sorter {
   /**
    * 排序
    * @param list 待排序的数组
    */
   public <T extends Comparable<T>> void sort(T[] list);
   /**
    * 排序
    * @param list 待排序的数组
    * @param comp 比较两个对象的比较器
    */
   public <T> void sort(T[] list, Comparator<T> comp);
}

实现类:

import java.util.Comparator;

/**
 * 冒泡排序
 * 
 * @author骆昊
 *
 */
public class BubbleSorter implements Sorter {

    @Override
    public <T extends Comparable<T>> void sort(T[] list) {
        boolean swapped = true;
        for (int i = 1, len = list.length; i < len && swapped; ++i) {
            swapped = false;
            for (int j = 0; j < len - i; ++j) {
                if (list[j].compareTo(list[j + 1]) > 0) {
                    T temp = list[j];
                    list[j] = list[j + 1];
                    list[j + 1] = temp;
                    swapped = true;
                }
            }
        }
    }
    @Override
    public <T> void sort(T[] list, Comparator<T> comp) {
        boolean swapped = true;
        for (int i = 1, len = list.length; i < len && swapped; ++i) {
            swapped = false;
            for (int j = 0; j < len - i; ++j) {
                if (comp.compare(list[j], list[j + 1]) > 0) {
                    T temp = list[j];
                    list[j] = list[j + 1];
                    list[j + 1] = temp;
                    swapped = true;
                }
            }
        }
    }
}

21、Java折半查找。

折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是O(logN)。

import java.util.Comparator;

public class MyUtil {

   public static <T extends Comparable<T>> int binarySearch(T[] x, T key) {
      return binarySearch(x, 0, x.length- 1, key);
   }

   // 使用循环实现的二分查找
   public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {
      int low = 0;
      int high = x.length - 1;
      while (low <= high) {
          int mid = (low + high) >>> 1;
          int cmp = comp.compare(x[mid], key);
          if (cmp < 0) {
            low= mid + 1;
          }
          else if (cmp > 0) {
            high= mid - 1;
          }
          else {
            return mid;
          }
      }
      return -1;
   }

   // 使用递归实现的二分查找
   private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) {
      if(low <= high) {
        int mid = low + ((high -low) >> 1);
        if(key.compareTo(x[mid])== 0) {
           return mid;
        }
        else if(key.compareTo(x[mid])< 0) {
           return binarySearch(x,low, mid - 1, key);
        }
        else {
           return binarySearch(x,mid + 1, high, key);
        }
      }
      return -1;
   }
}

Search

    Post Directory