学点java字节码更易于理解一些特殊的java语法效果

很多人学习java不会去理解字节码指令,觉得能看懂字节码对写好java代码没什么帮助,但其实不然,不少java语法效果其实可以通过字节码来理解的,掀开字节码的面纱能更好的理解java代码的语法和实际执行结果之间的关系,看了本文会让你有一种字节码也不过如此的感觉。

名字解释

由于本文涉及java字节码指令,如果有一点计算机栈和堆的基本认知会更好理解,不清楚该知识点也能大概意会,不影响大家理解其大致的原理。这里简单介绍下本文涉及的几个常见的指令,以便更方便的阅读。

指令中反复出现字符 含义 举例
a 对引用操作 aload_1:读取栈顶一个32b大小的引用
i 对栈上int长度的空间进行操作 iload_1:读取栈顶一个32b大小的int值 istore_1:把一个int值赋值给栈顶的int变量
l 对栈上long长度的空间进行操作 lload_2:读取栈顶一个long长度(2个32b大小)的long值 lstore_2:把一个long值赋值给栈顶long长度(2个32b大小)的变量
store 把某个值赋值给栈顶的变量 参考astore_1istore_1lstore_2
load 读取栈顶的值 参考aload_1iload_1lload_2
eq 是否等于1(小于为-1,等于为0,大于为1,false为0,true为1) ifeq #10:如果等于1则继续执行紧跟的指令,否则跳转到第10个32b位置的指令处继续执行
ne eq反过来 ifne #12: 跟上面是反过来的意思
1 栈上int长度的空间(32b) 参考aload_1iload_1istore_1
2 栈上long长度的空间(64b) 参考lload_2lstore_2
invokestatic 调用静态方法
invokevirtual 调用实例方法
invokespecial 调用构造方法、父类方法、重写的子类方法

1. 通过java字节码理解代码执行顺序

案例一:

java 复制代码
public class Test {
    private final static Logger log = LoggerFactory.getLogger(Test.class);

    // 写法一
    public void log1(FakeObj fakeObj) {
        // 日志框架会自己判断日志级别,如果最低日志级别是info,则该日志不会被打印出来
        log.debug("params={}", JSONObject.toJSONString(fakeObj));
    }
    // 写法二
    public void log2(FakeObj fakeObj) {
        if (log.isDebugEnabled()) {
            log.debug("params={}", JSONObject.toJSONString(fakeObj));
        }
    }
}

上面的代码编译后,使用javap -v Test.class命令查看其对应的字节码如下:

java 复制代码
  public void log1(org.example.int_to_long.FakeObj);
    Code:
         0: getstatic     #7               // Field log:Lorg/slf4j/Logger;
         3: ldc           #13              // String params={}
         5: aload_1
         6: invokestatic  #15              // Method com/alibaba/fastjson/JSONObject.toJSONString:(Ljava/lang/Object;)Ljava/lang/String;
         9: invokeinterface #21,  3        // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;Ljava/lang/Object;)V
        14: return

  public void log2(org.example.int_to_long.FakeObj);
    Code:
         0: getstatic     #7               // Field log:Lorg/slf4j/Logger;
         3: invokeinterface #27,  1        // InterfaceMethod org/slf4j/Logger.isDebugEnabled:()Z
         8: ifeq          25               // if如果上一句执行结果是0(即false)则跳转到行号25处,是1(tru
)则继续
        11: getstatic     #7               // Field log:Lorg/slf4j/Logger;
        14: ldc           #13              // String params={}
        16: aload_1
        17: invokestatic  #15              // Method com/alibaba/fastjson/JSONObject.toJSONString:(Ljava/lang/Object;)Ljava/lang/String;
        20: invokeinterface #21,  3        // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;Ljava/lang/Object;)V
        25: return

通过字节码可以看出,log1中第6行是执行toJSONString()方法,然后执行的log.debug,而log2方法中是先执行isDebugEnabled后,根据其结果再执行toJSONString()log.debug,由于生产环境中一般不开启debug日志级别,所以log2方法可以在不开启debug日志级别时不执行toJSONString(),故log2的性能更好。但是对于infowarnerror则不需要这样增加日志级别判断了,因为大部分时候生产环境都会开启这3个日志级别的。

2. 通过java字节码理解装箱和拆箱到底是怎样的

2.1 赋值时自动拆箱可能抛NullPointerException

java 复制代码
public class Test {
    public static void main(String[] args){
        Long a = null;
        long b = a;
    }
}

上面的代码在把变量a赋值给b时就会抛出NullPointerException,使用javap -v Test.class命令查看其对应的字节码如下:

java 复制代码
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1        // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aconst_null             // 把常量null的引用入栈
       1: astore_1                // 给变量a赋值为null
       2: aload_1                 // 读取变量a的值
       3: invokevirtual #2        // Method java/lang/Long.longValue:()J
       6: lstore_2                // 把变量a转成long的结果赋值到临时变量b上
       7: return
}

可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是把变量a赋值给b,显然由于anull,所以调用longValue()方法自然会抛空指针了。

2.2 方法传参时自动拆箱抛NullPointerException

java 复制代码
public class Test {
    public static void main(String[] args){
        Long a = null;
        increment(a);
    }

    public static long increment(long n){
        return n++;
    }
}

上面把变量a传参给方法increment时就会抛NullPointerException,还是用javap -v Test.class查看其字节码如下:

java 复制代码
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1        // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aconst_null
       1: astore_1
       2: aload_1
       3: invokevirtual #2        // Method java/lang/Long.longValue:()J
       6: invokestatic  #3        // Method increment:(J)J
       9: pop2
      10: return

  public static long increment(long);
    Code:
       0: lload_0
       1: dup2
       2: lconst_1
       3: ladd
       4: lstore_0
       5: lreturn
}

可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是真正的执行increment,显然由于anull,所以调用longValue()方法也会抛空指针了。

2.3 用于大小比较或运算时拆箱抛NullPointerException

java 复制代码
public class Test {
    public static void main(String[] args){
        Long n1 = null;
        long n2 = 1L;
        if(n1 == n2){
        }
    }
}

同样用javap -v Test.class查看字节码内容如下:

java 复制代码
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1        // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aconst_null
       1: astore_1
       2: lconst_1
       3: lstore_2
       4: aload_1
       5: invokevirtual #2        // Method java/lang/Long.longValue:()J
       8: lload_2
       9: lcmp
      10: ifne          13
      13: return
}

同样的在行号8的字节码通过Long.longValue()方法把变量n1Long转换成了long,然后行号8为加载变量n2,行号9才是比较两个long(注意不是比较Long)。

3. int和long直接计算时结果到底是int还是long

3.1 int+long计算

java 复制代码
public class Test {
    public static void main(String[] args) {
        int i = 1;
        long l = 1L;
        // 看看这里到底是调用下面的哪个print方法
        print(i+l);
    }

    private static void print(int i) {
        System.out.println("i=" + i);
    }

    private static void print(long l) {
        System.out.println("l=" + l);
    }
}

这段代码输出结果是l=1 ,即第二个print方法,同样用javap -v Test.class查看字节码内容如下:

java 复制代码
  public static void main(java.lang.String[]);
    Code:
      stack=4, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: lconst_1
         3: lstore_2
         4: iload_1            // 加载变量i
         5: i2l                // 把变量i从int转成long
         6: lload_2            // 加载变量l
         7: ladd               // 计算i+l
         8: invokestatic  #7   // Method print:(J)V
        11: return

这说明编译器在编译的时候就已经把i+l计算后的类型固定为long了,所以调用的直接就是第二个print方法。

3.2 int和long的三元组计算

java 复制代码
public class Test {
    public static void main(String[] args) {
        int i = 1;
        long l = 1;
        print(true ? i : l);
    }

    private static void print(int i) {
        System.out.println("i=" + i);
    }

    private static void print(long l) {
        System.out.println("l=" + l);
    }
}

这段代码输出结果仍然是l=1 ,即第二个print方法,同样用javap -v Test.class查看字节码内容:

java 复制代码
  public static void main(java.lang.String[]);
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: lconst_1
         3: lstore_2
         4: iload_1            // 加载变量i
         5: i2l                // 把变量i从int转成long
         6: invokestatic  #7   // Method print:(J)V
         9: return

可见尽管print方法的入参实际是变量int类型的i, 编译器仍然会把i强转成long,然后固定调用第二个print方法

4. 字符串相加跟StringBuilder其实是一样的效果

java 复制代码
public class Test {
    public void test1(String a, String b) {
        String str = a + b;
        System.out.println(str);
    }
}

同样用javap -v Test.class查看字节码内容如下:

java 复制代码
  public void test1(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: aload_1                // 加载变量a
         1: aload_2                // 加载变量B
         2: invokedynamic #7,  0   // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
         7: astore_3
         8: getstatic     #11      // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_3
        12: invokevirtual #17      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: return

SourceFile: "Test.java"
BootstrapMethods:
  0: #41 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #39 \u0001\u0001
InnerClasses:
  public static final #52= #48 of #50;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

test1方法字节码的第2行是调用的 StringConcatFactory.makeConcatWithConstants方法,这个是,String; 这个指令实际上是在运行时动态地创建一个 StringBuilder,并将拼接的字符串常量加入其中,最终返回拼接后的字符串,也就是说跟我们自己直接写StringBuilder其实是差不多的,只是写起来更简洁一些 。

(本文提到的字节码中的行号其实是字节码指令占用空间的偏移量,为了便于理解,暂以行号来理解 ,同时为了方便阅读字节码,移除了字节码中的flagsdescriptorLineNumberTableLocalVariableTableStackMapTablestatic静态语句块的内容)

相关推荐
BBB努力学习程序设计2 小时前
Java 8日期时间API完全指南:告别Date和Calendar的混乱时代
java
不能只会打代码2 小时前
力扣--3433. 统计用户被提及情况
java·算法·leetcode·力扣
知青先生2 小时前
E9项目调试方式
java·ide
本地运行没问题2 小时前
从零散编译到一键打包:Maven如何重塑Java构建流程
java
10km2 小时前
java:延迟加载实现方案对比:双重检查锁定 vs 原子化条件更新
java·延迟加载·双重检查锁定
星浩AI2 小时前
AI 并不懂文字,它只认向量:一文搞懂 Embedding
后端
独自归家的兔2 小时前
千问通义plus - 代码解释器的使用
java·人工智能
程序员博博2 小时前
这才是vibe coding正确的打开方式 - 手把手教你开发一个MCP服务
javascript·人工智能·后端
90后的晨仔2 小时前
阿里云服务器如何给子账号设置指定具体的那一台服务器?
后端