Java面试题目!硬核实践!这些不为人熟知的字节码指令你真的了解了吗

Exception table:
        from    to  target type
            0     6     8   any


# 装箱拆箱

在刚开始学习 Java 语言的你,可能会被自动装箱和拆箱搞得晕头转向。Java 中有 8 种基本类型,但鉴于 Java 面向对象的特点,它们同样有着对应的 8 个包装类型,比如 int 和 Integer,包装类型的值可以为 null,很多时候,它们都能够相互赋值。

我们使用下面的代码从字节码层面上来观察一下:

public class Box {
   public Integer cal() {
       Integer a = 1000;
       int b = a * 10;
       return b;
   }
}


上面是一段简单的代码,首先使用包装类型,构造了一个值为 1000 的数字,然后乘以 10 后返回,但是中间的计算过程,使用了普通类型 int。

public java.lang.Integer read();
   descriptor: ()Ljava/lang/Integer;
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=3, args_size=1
        0: sipush        1000
        3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        6: astore_1
        7: aload_1
        8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
       11: bipush        10
       13: imul
       14: istore_2
       15: iload_2
       16: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       19: areturn


通过观察字节码,我们发现赋值操作使用的是 Integer.valueOf 方法,在进行乘法运算的时候,调用了 Integer.intValue 方法来获取基本类型的值。在方法返回的时候,再次使用了 Integer.valueOf 方法对结果进行了包装。

这就是 Java 中的自动装箱拆箱的底层实现。

但这里有一个 Java 层面的陷阱问题,我们继续跟踪 Integer.valueOf 方法。

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


这个 IntegerCache,缓存了 low 和 high 之间的 Integer 对象,可以通过 -XX:AutoBoxCacheMax 来修改上限。

下面是一道经典的面试题,请考虑一下运行代码后,会输出什么结果?

public class BoxCacheError{
   public static void main(String[] args) {
       Integer n1 = 123;
       Integer n2 = 123;
       Integer n3 = 128;
       Integer n4 = 128;
       System.out.println(n1 == n2);
       System.out.println(n3 == n4);
   }


当我使用 java BoxCacheError 执行时,是 true,false;当我加上参数 java -XX:AutoBoxCacheMax=256 BoxCacheError 执行时,结果是 true,ture,原因就在于此。

# 数组访问

我们都知道,在访问一个数组长度的时候,直接使用它的属性 .length 就能获取,而在 Java 中却无法找到对于数组的定义。

比如 int[] 这种类型,通过 getClass(getClass 是 Object 类中的方法)可以获取它的具体类型是 [I。

其实,数组是 JVM 内置的一种对象类型,这个对象同样是继承的 Object 类。

我们使用下面一段代码来观察一下数组的生成和访问。

public class ArrayDemo {
   int getValue() {
       int[] arr = new int[]{
               1111, 2222, 3333, 4444
       };
       return arr[2];
   }
   int getLength(int[] arr) {
       return arr.length;
   }
}


首先看一下 getValue 方法的字节码。

int getValue();
   descriptor: ()I
   flags:
   Code:
     stack=4, locals=2, args_size=1
        0: iconst_4
        1: newarray       int
        3: dup
        4: iconst_0
        5: sipush        1111
        8: iastorae
        9: dup
       10: iconst_1
       11: sipush        2222
       14: iastore
       15: dup
       16: iconst_2
       17: sipush        3333
       20: iastore
       21: dup
       22: iconst_3
       23: sipush        4444
       26: iastore
       27: astore_1
       28: aload_1
       29: iconst_2
       30: iaload
       31: ireturn


可以看到,新建数组的代码,被编译成了 newarray 指令。数组里的初始内容,被顺序编译成了一系列指令放入:

*   sipush 将一个短整型常量值推送至栈顶;
*   iastore 将栈顶 int 型数值存入指定数组的指定索引位置。

> 为了支持多种类型,从操作数栈存储到数组,有更多的指令:bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore。

数组元素的访问,是通过第 28 ~ 30 行代码来实现的:

*   aload_1 将第二个引用类型本地变量推送至栈顶,这里是生成的数组;
*   iconst_2 将 int 型 2 推送至栈顶;
*   iaload 将 int 型数组指定索引的值推送至栈顶。

值得注意的是,在这段代码运行期间,有可能会产生 
ArrayIndexOutOfBoundsException,但由于它是一种非捕获型异常,我们不必为这种异常提供异常处理器。

我们再看一下 getLength 的字节码,字节码如下:

int getLength(int[]);
   descriptor: ([I)I
   flags:
   Code:
     stack=1, locals=2, args_size=2
        0: aload_1
        1: arraylength
        2: ireturn


可以看到,获取数组的长度,是由字节码指令 arraylength 来完成的。

# foreach

无论是 Java 的数组,还是 List,都可以使用 foreach 语句进行遍历,比较典型的代码如下:

import java.util.List;
public class ForDemo {
    void loop(int[] arr) {
        for (int i : arr) {
            System.out.println(i);
        }
    }
    void loop(List arr) {
        for (int i : arr) {
            System.out.println(i);
        }
    }


虽然在语言层面它们的表现形式是一致的,但实际实现的方法并不同。我们先看一下遍历数组的字节码:

void loop(int[]);
   descriptor: ([I)V
   flags:
   Code:
     stack=2, locals=6, args_size=2
        0: aload_1
        1: astore_2
        2: aload_2
        3: arraylength
        4: istore_3
        5: iconst_0
        6: istore        4
        8: iload         4
       10: iload_3
       11: if_icmpge     34
       14: aload_2
       15: iload         4
       17: iaload
       18: istore        5
       20: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       23: iload         5
       25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       28: iinc          4, 1
       31: goto          8
       34: return


可以很容易看到,它将代码解释成了传统的变量方式,即 for(int i;i<length;i++) 的形式。

而 List 的字节码如下:

void loop(java.util.List<java.lang.Integer>);
   Code:
      0: aload_1
      1: invokeinterface #4,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      6: astore_2-
      7: aload_2
      8: invokeinterface #5,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
     13: ifeq          39
     16: aload_2
     17: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
     22: checkcast     #7                  // class java/lang/Integer
     25: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     28: istore_3
     29: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     32: iload_3
     33: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
     36: goto          7
     39: return


它实际是把 list 对象进行迭代并遍历的,在循环中,使用了 Iterator.next() 方法。

使用 jd-gui 等反编译工具,可以看到实际生成的代码:

void loop(List paramList) {
   for (Iterator iterator = paramList.iterator(); iterator.hasNext(); ) {
     int i = ((Integer)iterator.next()).intValue();
     System.out.println(i);
   }
 }


# 注解

注解在 Java 中得到了广泛的应用,Spring 框架更是由于注解的存在而起死回生。注解在开发中的作用就是做数据约束和标准定义,可以将其理解成代码的规范标准,并帮助我们写出方便、快捷、简洁的代码。

那么注解信息是存放在哪里的呢?我们使用两个 Java 文件来看一下其中的一种情况。

**MyAnnotation.java**

MyAnnotation.java
public @interface MyAnnotation {
}


**AnnotationDemo**

@MyAnnotation
public class AnnotationDemo {
   @MyAnnotation
   public void test(@MyAnnotation  int a){
   }
}


下面我们来看一下字节码信息。

{
 public AnnotationDemo();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object.""😦)V
        4: return
     LineNumberTable:
       line 2: 0
 public void test(int);
   descriptor: (I)V
   flags: ACC_PUBLIC
   Code:
     stack=0, locals=2, args_size=2
        0: return
     LineNumberTable:
       line 6: 0
   RuntimeInvisibleAnnotations:
     0: #11()
   RuntimeInvisibleParameterAnnotations:
     0:
       0: #11()
}
SourceFile: “AnnotationDemo.java”
RuntimeInvisibleAnnotations:
 0: #11()


可以看到,无论是类的注解,还是方法注解,都是由一个叫做 
RuntimeInvisibleAnnotations 的结构来存储的,而参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的。

# 小结

本篇文章我们简单介绍了一下工作中常见的一些问题,并从字节码层面分析了它的原理,包括异常的处理、finally 块的执行顺序;以及隐藏的装箱拆箱和 foreach 语法糖的底层实现。

由于 Java 的特性非常多,这里不再一一列出,但都可以使用这种简单的方式,一窥究竟。可以认为本篇文章属于抛砖引玉,给出了一种学习思路。

另外,也可以对其中的性能和复杂度进行思考。可以注意到,在隐藏的装箱拆箱操作中,会造成很多冗余的字节码指令生成。那么,这个东西会耗性能吗?答案是肯定的,但是也不必纠结于此。


## 结语

小编也是很有感触,如果一直都是在中小公司,没有接触过大型的互联网架构设计的话,只靠自己看书去提升可能一辈子都很难达到高级架构师的技术和认知高度。向厉害的人去学习是最有效减少时间摸索、精力浪费的方式。

我们选择的这个行业就一直要持续的学习,又很吃青春饭。

虽然大家可能经常见到说程序员年薪几十万,但这样的人毕竟不是大部份,要么是有名校光环,要么是在阿里华为这样的大企业。年龄一大,更有可能被裁。

小编整理的学习资料分享一波!

送给每一位想学习Java小伙伴,用来提升自己。**[想要资料的可以点击这里免费获取](https://gitee.com/vip204888/java-p7)**
![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/ac4ca613fb2ff57bc54f486886349feb.png)

中小公司,没有接触过大型的互联网架构设计的话,只靠自己看书去提升可能一辈子都很难达到高级架构师的技术和认知高度。向厉害的人去学习是最有效减少时间摸索、精力浪费的方式。

我们选择的这个行业就一直要持续的学习,又很吃青春饭。

虽然大家可能经常见到说程序员年薪几十万,但这样的人毕竟不是大部份,要么是有名校光环,要么是在阿里华为这样的大企业。年龄一大,更有可能被裁。

小编整理的学习资料分享一波!

送给每一位想学习Java小伙伴,用来提升自己。**[想要资料的可以点击这里免费获取](https://gitee.com/vip204888/java-p7)**
[外链图片转存中...(img-L2AmUHwX-1628138129507)]

> 本文到这里就结束了,喜欢的朋友可以帮忙点赞和评论一下,感谢支持!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值