ART世界探险(2) - 从java byte code说起

简介: 我们先看一下Java字节码的指令集,然后从一些简单指令,从java字节码一直讲到ARM64机器码!让大家先有一个整体的概念。

探索ART世界(2) - 从java byte code说起

Dalvik时代,如果不做JIT的话,只需要了解java字节码和Dalivk的字节码就够了。但是,到了ART时代,我们可能还要至少学习两种新东西:一个是编译后端的IR中间代码。比如,我们假如使用LLVM做为编译后端的话,需要做从dex到LLVM IR的转换工作。这个IR可能还不只一层,比如分中层的MIR和底层的LIR。
最后,我们还得了解机器指令。仅就ARM来说,现在是64位时代了,我们需要了解的就是AArch64和AArch32两种状态下的A64,A32,Thumb2和Thumb四种指令集,还有NEON指令扩展等。

Java字节码指令集一览

我们先看一下Android提供的Java指令集简表,在这个网址可以看到:http://androidxref.com/6.0.1_r10/xref/dalvik/docs/java-bytecode.html

一共200条指令。不过大家千万别被这么多指令吓到啊,相对来说绝大部分指令还是非常简单的。我们用一讲的时间就可以讲个大概,细节将来遇到再说。倒是后面我们讲ARM指令的时候篇幅搞不好要长一点。

字节码 10进制值(相当于序号) 助记符
0x00 0 nop
0x01 1 aconst_null
0x02 2 iconst_m1
0x03 3 iconst_0
0x04 4 iconst_1
0x05 5 iconst_2
0x06 6 iconst_3
0x07 7 iconst_4
0x08 8 iconst_5
0x09 9 lconst_0
0x0a 10 lconst_1
0x0b 11 fconst_0
0x0c 12 fconst_1
0x0d 13 fconst_2
0x0e 14 dconst_0
0x0f 15 dconst_1
0x10 16 bipush
0x11 17 sipush
0x12 18 ldc
0x13 19 ldc_w
0x14 20 ldc2_w
0x15 21 iload
0x16 22 lload
0x17 23 fload
0x18 24 dload
0x19 25 aload
0x1a 26 iload_0
0x1b 27 iload_1
0x1c 28 iload_2
0x1d 29 iload_3
0x1e 30 lload_0
0x1f 31 lload_1
0x20 32 lload_2
0x21 33 lload_3
0x22 34 fload_0
0x23 35 fload_1
0x24 36 fload_2
0x25 37 fload_3
0x26 38 dload_0
0x27 39 dload_1
0x28 40 dload_2
0x29 41 dload_3
0x2a 42 aload_0
0x2b 43 aload_1
0x2c 44 aload_2
0x2d 45 aload_3
0x2e 46 iaload
0x2f 47 laload
0x30 48 faload
0x31 49 daload
0x32 50 aaload
0x33 51 baload
0x34 52 caload
0x35 53 saload
0x36 54 istore
0x37 55 lstore
0x38 56 fstore
0x39 57 dstore
0x3a 58 astore
0x3b 59 istore_0
0x3c 60 istore_1
0x3d 61 istore_2
0x3e 62 istore_3
0x3f 63 lstore_0
0x40 64 lstore_1
0x41 65 lstore_2
0x42 66 lstore_3
0x43 67 fstore_0
0x44 68 fstore_1
0x45 69 fstore_2
0x46 70 fstore_3
0x47 71 dstore_0
0x48 72 dstore_1
0x49 73 dstore_2
0x4a 74 dstore_3
0x4b 75 astore_0
0x4c 76 astore_1
0x4d 77 astore_2
0x4e 78 astore_3
0x4f 79 iastore
0x50 80 lastore
0x51 81 fastore
0x52 82 dastore
0x53 83 aastore
0x54 84 bastore
0x55 85 castore
0x56 86 sastore
0x57 87 pop
0x58 88 pop2
0x59 89 dup
0x5a 90 dup_x1
0x5b 91 dup_x2
0x5c 92 dup2
0x5d 93 dup2_x1
0x5e 94 dup2_x2
0x5f 95 swap
0x60 96 iadd
0x61 97 ladd
0x62 98 fadd
0x63 99 dadd
0x64 100 isub
0x65 101 lsub
0x66 102 fsub
0x67 103 dsub
0x68 104 imul
0x69 105 lmul
0x6a 106 fmul
0x6b 107 dmul
0x6c 108 idiv
0x6d 109 ldiv
0x6e 110 fdiv
0x6f 111 ddiv
0x70 112 irem
0x71 113 lrem
0x72 114 frem
0x73 115 drem
0x74 116 ineg
0x75 117 lneg
0x76 118 fneg
0x77 119 dneg
0x78 120 ishl
0x79 121 lshl
0x7a 122 ishr
0x7b 123 lshr
0x7c 124 iushr
0x7d 125 lushr
0x7e 126 iand
0x7f 127 land
0x80 128 ior
0x81 129 |lor
0x82 130 ixor
0x83 131 lxor
0x84 132 iinc
0x85 133 i2l
0x86 134 i2f
0x87 135 i2d
0x88 136 l2i
0x89 137 l2f
0x8a 138 l2d
0x8b 139 f2i
0x8c 140 f2l
0x8d 141 f2d
0x8e 142 d2i
0x8f 143 d2l
0x90 144 d2f
0x91 145 i2b
0x92 146 i2c
0x93 147 i2s
0x94 148 lcmp
0x95 149 fcmpl
0x96 150 fcmpg
0x97 151 dcmpl
0x98 152 dcmpg
0x99 153 ifeq
0x9a 154 ifne
0x9b 155 iflt
0x9c 156 ifge
0x9d 157 ifgt
0x9e 158 ifle
0x9f 159 if_icmpeq
0xa0 160 if_icmpne
0xa1 161 if_icmplt
0xa2 162 if_icmpge
0xa3 163 if_icmpgt
0xa4 164 if_icmple
0xa5 165 if_acmpeq
0xa6 166 if_acmpne
0xa7 167 goto
0xa8 168 jsr
0xa9 169 ret
0xaa 170 tableswitch
0xab 171 lookupswitch
0xac 172 ireturn
0xad 173 lreturn
0xae 174 freturn
0xaf 175 dreturn
0xb0 176 areturn
0xb1 177 return
0xb2 178 getstatic
0xb3 179 putstatic
0xb4 180 getfield
0xb5 181 putfield
0xb6 182 invokevirtual
0xb7 183 invokespecial
0xb8 184 invokestatic
0xb9 185 invokeinterface
0xba 186 (unused)
0xbb 187 new
0xbc 188 newarray
0xbd 189 anewarray
0xbe 190 arraylength
0xbf 191 athrow
0xc0 192 checkcast
0xc1 193 instanceof
0xc2 194 monitorenter
0xc3 195 monitorexit
0xc4 196 wide
0xc5 197 multianewarray
0xc6 198 ifnull
0xc7 199 ifnonnull
0xc8 200 goto_w
0xc9 201 jsr_w

反汇编,学指令

如果一个一个指令地讲下来,估计大家都睡着了。所以我们都过反汇编我们写的代码的方式来学习,学得差不多了,我们再把指令串一下。一切以实用为先,我们尝试一下吧。

首先我们还是以上一讲的empty3例子说起。
我们首先用javap工具反汇编一下BuildConfig那个类:

javap -c com.yunos.system.empty3.BuildConfig

反汇编出来的代码如下:

Compiled from "BuildConfig.java"
public final class com.yunos.system.empty3.BuildConfig {
  public static final boolean DEBUG;

  public static final java.lang.String APPLICATION_ID = "com.yunos.system.empty3";

  public static final java.lang.String BUILD_TYPE = "debug";

  public static final java.lang.String FLAVOR = "";

  public static final int VERSION_CODE = 1;

  public static final java.lang.String VERSION_NAME = "1.0";

  public com.yunos.system.empty3.BuildConfig();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 6: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/yunos/system/empty3/BuildConfig;

  static {};
    Code:
       0: ldc           #2                  // String true
       2: invokestatic  #3                  // Method java/lang/Boolean.parseBoolean:(Ljava/lang/String;)Z
       5: putstatic     #4                  // Field DEBUG:Z
       8: return
    LineNumberTable:
      line 7: 0
}

BuildConfig构造方法

我们先看BuildConfig构造这段:

       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

第一条是aload_指令,这个家族共有4条指令,aload_0, aload_1, aload_2, aload_3.指令代码从0x2a到0x2d。
这条指令的含义是:从局部变量中,取第n个的值。取第0个就是aload_0,第1个是aload_1。
如果取第4个怎么办?这时候一个字节的指令不够用了,另有一个aload指令,后面接一个字节提供数字。
相当于,aload_0是aload 0指令的简写,但是aload 0占两个字节,而aload_0占一个字节。

第二条指令是invokespecial,调用对象实例的方法,尤其是调用super方法,私有方法和构造方法。因为这里是要调用父类的初始化方法,所以正好该是invokespecial.

第三条是return,不带值返回。

一共就这3条指令,还是挺好理解的吧?

BuildConfig的静态部分

学习了3条指令的,我们再学一个4条指令的:

  static {};
    Code:
       0: ldc           #2                  // String true
       2: invokestatic  #3                  // Method java/lang/Boolean.parseBoolean:(Ljava/lang/String;)Z
       5: putstatic     #4                  // Field DEBUG:Z
       8: return
    LineNumberTable:
      line 7: 0

第一条,ldc,从常量池中将常量读出来压入栈中。JVM是基于栈的,操作数都是从栈上取,结果也压到栈里面去。
第二条,invokestatic,这是我们学到的第二条invoke类指令了,上一条是invokespecial,这个顾名思义,就是调用静态方法专用的指令。
第三条,putstatic,将栈中的值,放到静态域中。
第四条,return,无数据返回。

流程很简单,先从常量池将true读出来放到栈里,然后调用Boolean.parseBoolean,参数就是刚才放入栈的true字符串,解析好之后的值又入栈。接着,putstatic从栈里读取这个boolean的值,写到DEBUG这个域中。最后返回。

class的基本结构

因为是入门文章,暂时我们先不讲class文件各模块的细节,只是先有个感性认识:

  • 常量池:class中有常量池,有很多指令是操作常量池的。将常量池中的值读出来放到栈中。
  • 方法:class文件中,方法是有专门存储模块的,invoke集指令去调用的时候,从中去查找。
  • 域:不管是静态域还是对象实例中的普通域,我们有很多指令是用来操作它们的。
  • 栈:JVM最重要的结构就是这个栈,大部分的操作都是通过这个栈来操作。后面学习Dalvik指令的时候我们会看到,比起JVM中基本都是栈操作的这种指令,Dalvik大量使用了寄存器。

运算指令

下面我们再看另一大类的指令,运算相关的指令。
我们还是老办法,先写个例子,然后再反汇编,看它背后的故事。我们先写个最简单的加法运算:

package com.yunos.xulun.testcppjni2;

public class TestART {
    public static int add(int a, int b){
        return a+b;
    }
}

反汇编之后是这样的:

Compiled from "TestART.java"
public class com.yunos.xulun.testcppjni2.TestART {
  public com.yunos.xulun.testcppjni2.TestART();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 3: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/yunos/xulun/testcppjni2/TestART;

  public static int add(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: iadd
       3: ireturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       4     0     a   I
          0       4     1     b   I
}

默认生成的构造方法,以后我们就略过不提了。
这个加法运算,一共4条指令:

  1. iload_0,从栈顶第0个位置取一个整数
  2. iload_1,从栈顶第1个位置取一个整数
  3. iadd,将这两个整数相加
  4. ireturn,返回一个整数。

运算指令多,是因为指令级没办法做泛型,针对每种类型数据都得做一条指令,所以,加法这一个操作,就得4条指令,分别对应整型,长整型,单精度,双精度:

指令码 序号 助记符
0x60 96 iadd
0x61 97 ladd
0x62 98 fadd
0x63 99 dadd

指令中,第一个字符为i的对应整型,l是长整型,f是单精度,d是双精度。
当然不光加法是这样,减法,乘法,除法也是一样。从栈上读数,转化成什么类型,也是4种类型都要支持。

类型转换

那么一个问题来了,既然只有4种类型的计算指令,其它类型怎么办?
JVM提供了一堆类型转换的指令来满足这个需求。
有一些类型直接连转换都省了,比如short和byte,在JVM里,就是当int来处理。

我们做个试验:

    public static int sub(int a, short b){
        return a-b;
    }

反汇编了之后发现,一个int跟short,或者是两个short相减,跟两个int做减法就没有区别:

  public static int sub(int, short);
    Code:
       0: iload_0
       1: iload_1
       2: isub
       3: ireturn
    LineNumberTable:
      line 9: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       4     0     a   I
          0       4     1     b   S

所以,以后大家就用int吧,不是数组的话,short跟byte也是int。
为什么?因为栈就是以int为单位的啊!寄存器的还值得拆成两个用,栈真就不需要了。

我们再看一个带类型转换的:

    public static long mul(int a, byte b){
        return a*b;
    }

反汇编之后,出现一条将整型转成长整型的i2l指令。

  public static long mul(int, byte);
    Code:
       0: iload_0
       1: iload_1
       2: imul
       3: i2l
       4: lreturn
    LineNumberTable:
      line 13: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0     a   I
          0       5     1     b   B

因为返回值也是长整型了,所以返回指令变成lreturn了。

趁热打铁,我们强势切入Dalvik指令

对JVM指令有了初步的理解之后,我们绝不沾沾自喜,迅速看看Dalvik指令是什么样子的。

先从记忆中把BuildConfig那段翻出来,JVM是这样的:

       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

我们看看,转成Dalvik是什么样的:

00052c:                                        |[00052c] com.yunos.system.empty3.BuildConfig.<init>:()V
00053c: 7010 1100 0000                         |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@0011
000542: 0e00                                   |0003: return-void

看完了之后有没有会心一笑?invokespecial指令换了个名,叫invoke-direct,带一个v0寄存器的参数,所以aload_0省了。
return换了个更贴切的名字:return-void。我们前面学过了ireturn,lreturn,这个不带值的return,确实叫return-void很合适。

再对比另一段:

       0: ldc           #2                  // String true
       2: invokestatic  #3                  // Method java/lang/Boolean.parseBoolean:(Ljava/lang/String;)Z
       5: putstatic     #4                  // Field DEBUG:Z
       8: return

对应过来是:

000508:                                        |[000508] com.yunos.system.empty3.BuildConfig.<clinit>:()V
000518: 1a00 4a00                              |0000: const-string v0, "true" // string@004a
00051c: 7110 1000 0000                         |0002: invoke-static {v0}, Ljava/lang/Boolean;.parseBoolean:(Ljava/lang/String;)Z // method@0010
000522: 0a00                                   |0005: move-result v0
000524: 6a00 0200                              |0006: sput-boolean v0, Lcom/yunos/system/empty3/BuildConfig;.DEBUG:Z // field@0002
000528: 0e00                                   |0008: return-void

ldc变成了const-string,就是换个名,多个v0寄存器。invokestatic多了个"-",也是多了个寄存器参数。
因为invoke-static返回的值是在栈里,所以需要一条额外的move-result指令将栈顶值放入寄存器。
putstatic变成了sput,加上类型,变成sput-boolean。
最后return-void.

好,我们再看下,加,减,乘的那几个:

0f477c:                                        |[0f477c] com.yunos.xulun.testcppjni2.TestART.add:(II)I
0f478c: 9000 0102                              |0000: add-int v0, v1, v2
0f4790: 0f00                                   |0002: return v0

iadd变成了add-int指令,带有三个寄存器参数,v1和v2是两个加数,和放在v0中。
ireturn变成return v0

减法以此类推:

0f47ac:                                        |[0f47ac] com.yunos.xulun.testcppjni2.TestART.sub:(IS)I
0f47bc: 9100 0102                              |0000: sub-int v0, v1, v2
0f47c0: 0f00                                   |0002: return v0

乘法的增加一条类型转换:

0f4794:                                        |[0f4794] com.yunos.xulun.testcppjni2.TestART.mul:(IB)J
0f47a4: 9200 0203                              |0000: mul-int v0, v2, v3
0f47a8: 8100                                   |0002: int-to-long v0, v0
0f47aa: 1000                                   |0003: return-wide v0

i2l换了个马甲叫int-to-long。
lreturn变成了return-wide。

最后收尾,ARM指令

我们最后看下几个计算函数生成的机器代码吧:

add的机器代码

    CODE: (code_offset=0x0050151c size_offset=0x00501518 size=76)...
      0x0050151c: d1400bf0    sub x16, sp, #0x2000 (8192)
      0x00501520: b940021f    ldr wzr, [x16]
      suspend point dex PC: 0x0000
      0x00501524: f81e0fe0    str x0, [sp, #-32]!
      0x00501528: f9000ffe    str lr, [sp, #24]
      0x0050152c: b9002be1    str w1, [sp, #40]
      0x00501530: b9002fe2    str w2, [sp, #44]
      0x00501534: 79400250    ldrh w16, [tr](state_and_flags)
      0x00501538: 35000130    cbnz w16, #+0x24 (addr 0x50155c)
      0x0050153c: b9402be0    ldr w0, [sp, #40]
      0x00501540: b9402fe1    ldr w1, [sp, #44]
      0x00501544: 0b010002    add w2, w0, w1
      0x00501548: b90013e2    str w2, [sp, #16]
      0x0050154c: b94013e0    ldr w0, [sp, #16]
      0x00501550: f9400ffe    ldr lr, [sp, #24]
      0x00501554: 910083ff    add sp, sp, #0x20 (32)
      0x00501558: d65f03c0    ret
      0x0050155c: f9421e5e    ldr lr, [tr, #1080](pTestSuspend)
      0x00501560: d63f03c0    blr lr
      suspend point dex PC: 0x0000
      0x00501564: 17fffff6    b #-0x28 (addr 0x50153c)

核心就这一条add w2, w0, w1,其余都是折腾栈和寄存器。指令用的是w[n]而不是x[n],进行的是32位的加法。

减法

    CODE: (code_offset=0x0050160c size_offset=0x00501608 size=76)...
      0x0050160c: d1400bf0    sub x16, sp, #0x2000 (8192)
      0x00501610: b940021f    ldr wzr, [x16]
      suspend point dex PC: 0x0000
      0x00501614: f81e0fe0    str x0, [sp, #-32]!
      0x00501618: f9000ffe    str lr, [sp, #24]
      0x0050161c: b9002be1    str w1, [sp, #40]
      0x00501620: b9002fe2    str w2, [sp, #44]
      0x00501624: 79400250    ldrh w16, [tr](state_and_flags)
      0x00501628: 35000130    cbnz w16, #+0x24 (addr 0x50164c)
      0x0050162c: b9402be0    ldr w0, [sp, #40]
      0x00501630: b9402fe1    ldr w1, [sp, #44]
      0x00501634: 4b010002    sub w2, w0, w1
      0x00501638: b90013e2    str w2, [sp, #16]
      0x0050163c: b94013e0    ldr w0, [sp, #16]
      0x00501640: f9400ffe    ldr lr, [sp, #24]
      0x00501644: 910083ff    add sp, sp, #0x20 (32)
      0x00501648: d65f03c0    ret
      0x0050164c: f9421e5e    ldr lr, [tr, #1080](pTestSuspend)
      0x00501650: d63f03c0    blr lr
      suspend point dex PC: 0x0000
      0x00501654: 17fffff6    b #-0x28 (addr 0x50162c)

除了加法换成了减法:sub w2, w0, w1,其余基本一样啊。

乘法

    CODE: (code_offset=0x0050158c size_offset=0x00501588 size=88)...
      0x0050158c: d1400bf0    sub x16, sp, #0x2000 (8192)
      0x00501590: b940021f    ldr wzr, [x16]
      suspend point dex PC: 0x0000
      0x00501594: f81e0fe0    str x0, [sp, #-32]!
      0x00501598: f9000ffe    str lr, [sp, #24]
      0x0050159c: b9002be1    str w1, [sp, #40]
      0x005015a0: b9002fe2    str w2, [sp, #44]
      0x005015a4: 79400250    ldrh w16, [tr](state_and_flags)
      0x005015a8: 35000190    cbnz w16, #+0x30 (addr 0x5015d8)
      0x005015ac: b9402be0    ldr w0, [sp, #40]
      0x005015b0: b9402fe1    ldr w1, [sp, #44]
      0x005015b4: 1b017c02    mul w2, w0, w1
      0x005015b8: b9000fe2    str w2, [sp, #12]
      0x005015bc: b9400fe0    ldr w0, [sp, #12]
      0x005015c0: 93407c01    sxtw x1, w0
      0x005015c4: f800c3e1    stur x1, [sp, #12]
      0x005015c8: f840c3e0    ldur x0, [sp, #12]
      0x005015cc: f9400ffe    ldr lr, [sp, #24]
      0x005015d0: 910083ff    add sp, sp, #0x20 (32)
      0x005015d4: d65f03c0    ret
      0x005015d8: f9421e5e    ldr lr, [tr, #1080](pTestSuspend)
      0x005015dc: d63f03c0    blr lr
      suspend point dex PC: 0x0000
      0x005015e0: 17fffff3    b #-0x34 (addr 0x5015ac)

首先,是mul指令:mul w2, w0, w1
另外,还有一条是将32位整数转成64位的长整型,请注意,32位的w寄存器之外,64位的x寄存器出来干活了。
sxtw x1, w0:是将w0中的32位值扩展成64位的值,结果放在x1 64位寄存器中。

基本概念我们先说这么多,分支,异常等高级话题,下面分别讨论。
最后我们会cover到完整的指令集。

目录
相关文章
|
6月前
|
Java
java 读取文件 获取byte[]字节 并执行Gzip的压缩和解压
java 读取文件 获取byte[]字节 并执行Gzip的压缩和解压
51 0
|
10月前
|
Java
Java开发篇- 从BigDecimal的divide的异常说起
在过去做项目的某一天中,突然有小伙伴说两个BigDecimal的数据相除(divide)报错了,觉得不可能,然后问他是怎么编写的,他说很简单呀,就是new了2个BigDecimal,然后相除的结果赋值给另外一个BigDecimal对象。听起来觉得没有问题,正常来说,2个Integer(int),2个Double(double)都不会报错,然后问是什么异常,说是一个很奇怪的异常
367 0
|
3天前
|
数据采集 前端开发 Java
Java医院绩效考核系统源码maven+Visual Studio Code一体化人力资源saas平台系统源码
医院绩效解决方案包括医院绩效管理(BSC)、综合奖金核算(RBRVS),涵盖从绩效方案的咨询与定制、数据采集、绩效考核及反馈、绩效奖金核算到科到组、分配到员工个人全流程绩效管理;将医院、科室、医护人员利益绑定;全面激活人才活力;兼顾质量和效益、长期与短期利益;助力医院降本增效,持续改善、优化收入、成本结构。
14 0
|
3月前
|
搜索推荐 Java
JAVA排序之选择排序 带图和code
JAVA排序之选择排序 带图和code
20 0
JAVA排序之选择排序 带图和code
|
7月前
|
Java
【Java异常】ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2 JDWP exit erro
【Java异常】ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2 JDWP exit erro
92 0
|
9月前
|
缓存 NoSQL Redis
java.io.IOException: java.lang.RuntimeException: unable to find class for code 253
原因: 跟踪了一下所调用的底层接口,发现是redis缓存的问题,登录redis,刷新redis
47 0
|
9月前
|
存储 Java 计算机视觉
java 之byte
当涉及到处理数据时,Java 提供了多种数据类型,其中包括 `byte` 类型。在本文中,我们将深入探讨 Java 中的 `byte` 数据类型,了解它的特点、用途以及在编程中的实际应用。
|
9月前
|
Java
Java中 String与基本数据类型,包装类,char[],byte[]之间的转换
Java中 String与基本数据类型,包装类,char[],byte[]之间的转换
59 0
|
11月前
|
Java C++ iOS开发
macos上VS Code上配置Python、Java、C++环境变量
macos上VS Code上配置Python、Java、C++环境变量
195 0
macos上VS Code上配置Python、Java、C++环境变量
|
11月前
|
Java C++
VS Code配置Java开发环境
VS Code配置Java开发环境
103 0