第3章 第3节 Java基础 - 必知必会(下)

大家好,很高兴我们可以继续学习交流Java高频面试题。本小节是Java基础篇章的第三小节,主要讲述Java中的Exception与Error,JIT编译器以及值传递与引用传递的知识点。熟练掌握本小节的知识点,可以帮助大家更好的复习与掌握Java面试中的基础类题目,对我们的实际开发也会有很大的帮助。

 

(1)Java中的Exception和Error有什么区别?

答: Exception和Error的主要区别可以概括如下。

  • Exception是程序正常运行中预料到可能会出现的错误,并且应该被捕获并进行相应的处理,是一种异常现象
  • Error是正常情况下不可能发生的错误,Error会导致JVM处于一种不可恢复的状态,不需要捕获处理,比如说OutOfMemoryError

 

解析:

Exception又分为了运行时异常和编译时异常

编译时异常(受检异常)表示当前调用的方法体内部抛出了一个异常,所以编译器检测到这段代码在运行时可能会出异常,所以要求我们必须对异常进行相应的处理,可以捕获异常或者抛给上层调用方。

运行时异常(非受检异常)表示在运行时出现的异常,常见的运行时异常包括:空指针异常,数组越界异常,数字转换异常以及算术异常等。

前边说到了异常Exception应该被捕获,我们可以使用try – catch – finally 来处理异常,并且使得程序恢复正常。

那么我们捕获异常应该遵循哪些原则呢?

  • 尽可能捕获比较详细的异常,而不是使用Exception一起捕获。
  • 当本模块不知道捕获之后该怎么处理异常时,可以将其抛给上层模块。上层模块拥有更多的业务逻辑,可以进行更好的处理。
  • 捕获异常后至少应该有日志记录,方便之后的排查。
  • 不要使用一个很大的try – catch包住整段代码,不利于问题的排查。

 

这些都是笔者血淋淋的教训,捕获到异常,却看不出是哪里抛出的,这才是绝望。然后,我们再来看一个容易混淆的异常与错误知识点:

NoClassDefFoundError 和 ClassNotFoundException 有什么区别?

答:从名字中,我们可以看出前者是一个错误,后者是一个异常。我们先来看下JDK中对ClassNotFoundException异常的阐述:

第3章 第3节 Java基础 - 必知必会(下)

大概意思就是在说,当我们使用例如Class.forName方法来动态的加载该类的时候,传入了一个类名,但是其并没有在类路径中被找到的时候,就会报ClassNotFoundException异常。出现这种情况,一般都是类名字传入有误导致的。

我们再来看下JDK中对该错误NoClassDefFoundError的阐述:

第3章 第3节 Java基础 - 必知必会(下)

大概意思是这样的,如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。但是要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError。出现这种情况,一般是由于打包的时候漏掉了部分类或者Jar包被篡改已经损坏。

 

(2)JIT编译器有了解吗?

答:前面我们谈到了Java是一种先编译,后解释执行的语言。那么我们就来说下何为JIT编译器吧。

JIT编译器全名叫Just In Time Compile 也就是即时编译器,把经常运行的代码作为"热点代码"编译成与本地平台相关的机器码,并进行各种层次的优化。JIT编译除了具有缓存的功能外,还会对代码做各种优化,包括逃逸分析、锁消除、 锁膨胀、方法内联、空值检查消除、类型检测消除以及公共子表达式消除等。
 

解析:
JIT编译器属于Java基础中的比较有深度的题目了,回答出来算是一个亮点了。既然说到了JIT编译器,我们来看下JIT对代码优化使用到的逃逸分析技术吧。
 

逃逸分析:

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。JIT编译器的优化包括如下:

  • 同布省略:也就是锁消除,当JIT编译器判断不会产生并发问题,那么会将同步synchronized去掉
  • 标量替换

 

我们先来解释下标量和聚合量的基本概念。

  • 标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。

  • 聚合量(Aggregate)是还可以分解的数据。Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

 

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。标量替换的好处就是对象可以不在堆内存进行分配,为栈上分配提供了良好的基础。
 

那么逃逸分析技术存在哪些缺点呢?

    技术不是特别成熟,分析的过程也很耗时,如果没有一个对象是不逃逸的,那么就得不偿失了。
 

(3)Java中的值传递和引用传递可以解释下吗?

答:值传递和引用传递的解释可以概括如下。

  • 值传递,意味着传递了对象的一个副本,即使副本被改变,也不会影响源对象。
  • 引用传递,意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象的改变会反映到所有的对象上。

 

解析:

我们先来看一个值传递的例子:

复制代码

1

2

3

4

5

6

7

8

9

10

public class Test {

    public static void main(String[] args)  {

        int x=0;

        change(x);

        System.out.println(x);

    }

    static void change(int i){

        i=7;

    }

}

毫无疑问,上边的代码会输出0。因为如果参数是基本数据类型,那么是属于值传递的范畴,传递的其实是源对象的一个copy副本,不会影响源对象的值。

我们再来分析一个引用传递的例子:

复制代码

1

2

3

4

5

6

7

8

9

10

public class Test {

    public static void main(String[] args)  {

        StringBuffer x = new StringBuffer("Hello");

        change(x);

        System.out.println(x);

    }

    static void change(StringBuffer i) {

        i.append(" world!");

    }

}

通过运行程序,输出为Hello world!接下来我们通过图片来分析下程序执行过程种的内存变化吧。

第3章 第3节 Java基础 - 必知必会(下)

由图中我们可以看出x和i指向了同样的内存地址,那么i.append操作将直接修改了内存地址里边的值,所以当方法结束,局部变量i消失,先前变量x所指向的内存值已经发生了变化,所以输出为Hello world!

接着,我们修改下change方法,代码如下所示:

复制代码

1

2

3

4

5

6

7

8

9

10

11

public class Test {

    public static void main(String[] args)  {

        StringBuffer x = new StringBuffer("Hello");

        change2(x);

        System.out.println(x);

    }

    static void change2(StringBuffer i) {

        i = new StringBuffer("hi");

        i.append(" world!");

    }

}

先给出答案,上边Demo的输出为Hello,我们依然来画图分析内存变化。

第3章 第3节 Java基础 - 必知必会(下)

由图中我们可以看出来,在函数change2中将引用变量i重新指向了堆内存中另一块区域,下边都是对另一块区域进行修改,所以输出是Hello。

最后,我们继续升级该题目代码如下:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

public class Test {

    public static void main(String[] args)  {

        StringBuffer sb = new StringBuffer("Hello ");

        System.out.println("Before change, sb = " + sb);

        changeData(sb);

        System.out.println("After change, sb = " + sb);

    }

    public static void changeData(StringBuffer strBuf) {

        StringBuffer sb2 = new StringBuffer("Hi,I am ");

        strBuf = sb2;

        sb2.append("World!");

    }

}

那么这个题目的输出是什么呢?相信大家都可以给出正确的答案。

请大家将这个题目的答案以及内存分析过程评论在本节文章的下边,我会在下一小节中给出参考答案分析哦~

 

(4)Java中的其余经典基础面试题目:

限于文章篇幅,我将《Java基础 - 必知必会》三个小节中没有讲解到的常见基础面试题目罗列在此,大家有想要交流的可以直接在评论区留言。

  • StringBuffer与StringBuilder的区别?
  • Java中的泛型的理解
  • Java序列化与反序列化的过程
  • equals和hashCode方法的关系?
  • Java和C++的区别有哪些?
  • 静态与非静态的区别?
  • Java中equals方法和==的区别?

 

总结:

限于篇幅,笔者尽量将最常见的面试题目与大家进行了交流与分享。有的题目可能解析比较简单,仅仅起到了一个抛砖引玉的作用,因为每一个知识点都可能需要阐述一篇文章。从下一节开始,我们开始交流学习Java基础中的三大集合,这几乎是面试中的必考知识点,在难度和深度上有所加深,希望我们可以一起进步。

限于作者水平,文章中难免会有不妥之处。大家在学习过程中遇到我没有表达清楚或者表述有误的地方,欢迎随时在文章下边指出,我会及时关注,随时改正。另外,大家有任何话题都可以在下边留言,我们一起交流探讨。