1.シンタックスシュガーとは
シンタックスシュガーとは、プログラミング言語に特定の文法を追加することを指します。この文法は言語の機能に影響を与えませんが、プログラマーが使用する方が便利です。たとえば、Javaのループごとに、コードをより簡潔にするためにコードを記述するときに、このように記述することができます。ただし、クラスファイルにコンパイルすると、forは通常のループステートメントになります(goto、ifなどのバイトコードによって実装されます)。
私が共有したいのは、バイトコードを逆コンパイルして表示することによって、Javaの一般的なシンタックスシュガーがどのように実現されるかを理解することです。
2.糖衣構文の実現原理を分析する方法
では、シンタックスシュガーの背後にある実装原理をどのように分析するのでしょうか。前述のように、Javaコードファイルがクラスファイルにコンパイルされた後、構文糖衣は通常の基本文法になります(このプロセスは糖衣構文と呼ばれます)。次に、クラスファイルを分析することで、糖衣が解決された後の基本的な文法を理解し、Javaがこれらの糖衣構文をどのようにサポートしているかを知ることができます。
クラスファイルの分析クラスファイル
はバイナリファイルです。直接理解するには、その組織構造に精通している必要があります。したがって、通常は逆コンパイルツールを使用してクラスファイルを読み取り可能な形式に逆コンパイルしてから調査を行います。
javapコマンドはJDKで提供されており、クラスファイルを比較的読みやすいバイトコード形式に変換できます。この方法でクラスファイルを分析すると、最も包括的な情報を取得できますが、それを読む前に、バイトコード関連の多くの知識を理解する必要もあります。
javapコマンドに加えて、クラスファイルをJavaコードに直接逆コンパイルできる多くのサードパーティツールがあります。このようにして得られた結果は非常に読みやすいです。IDEAはこのような逆コンパイルツールを統合しており、クラスファイルをIDEAで直接開いて、逆コンパイルの結果を確認できます。
- javapコマンド
- 考え
以下では、上記の2つの方法を使用して、Javaのシンタックスシュガーの一部を分析します。
3.Javaのシンタックスシュガー
3.1各ループ
(1)それぞれが配列
Javaコードをトラバースするため
int[] arr = {
1,2,3,4};
for(int i : arr) {
System.out.println(i);
}
逆コンパイル後
int[] var1 = new int[]{
1, 2, 3, 4};
int var2 = 0;
int[] var3 = var1;
int var4 = var1.length;
for(int var5 = 0; var5 < var4; ++var5) {
int var6 = var3[var5];
var2 += var6;
}
逆コンパイル後、foreachループが通常のforループになることがわかります。トラバースするたびに、より基本的なforループによって配列が実現されることが理解できます。
3.2ボクシングと開封
例として整数と整数の相互変換を取り上げます
Javaコード:
int num1 = 1000;
Integer num2 = num1;
int num3 = num2;
逆コンパイル後:
short var1 = 1000;
Integer var2 = Integer.valueOf(var1);
int var3 = var2;
逆コンパイルの結果から、ボクシングプロセス、つまりInteger.valueOfメソッドの使用を確認できます。しかし、整数から整数への変換の実現原理がわかりません。
結論を確認するには、javapコマンドを使用して下位レベルのバイトコードを確認する必要があります。
javap -v BoxingAndUnboxing
Classfile /C:/Users/xiaozhigang/Desktop/Java语法糖/BoxingAndUnboxing.class
Last modified 2019-11-14; size 400 bytes
MD5 checksum 769b615cb1893668acb2c7660c6a233c
Compiled from "BoxingAndUnboxing.java"
public class BoxingAndUnboxing
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Methodref #15.#16 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #15.#17 // java/lang/Integer.intValue:()I
#4 = Class #18 // BoxingAndUnboxing
#5 = Class #19 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 BoxingAndUnboxing.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #20 // java/lang/Integer
#16 = NameAndType #21:#22 // valueOf:(I)Ljava/lang/Integer;
#17 = NameAndType #23:#24 // intValue:()I
#18 = Utf8 BoxingAndUnboxing
#19 = Utf8 java/lang/Object
#20 = Utf8 java/lang/Integer
#21 = Utf8 valueOf
#22 = Utf8 (I)Ljava/lang/Integer;
#23 = Utf8 intValue
#24 = Utf8 ()I
{
public BoxingAndUnboxing();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: sipush 1000
3: istore_1
4: iload_1
5: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: astore_2
9: aload_2
10: invokevirtual #3 // Method java/lang/Integer.intValue:()I
13: istore_3
14: return
LineNumberTable:
line 3: 0
line 4: 4
line 6: 9
line 7: 14
}
SourceFile: "BoxingAndUnboxing.java"
上記のバイトコードの実行プロセスを分析してみましょう。まず、Javaのランタイムデータ領域のスタックを思い出してください。次の図に示すように、スタックフレームは、各メソッドが実行されるときに割り当てられます。スタックフレームには、オペランドスタックとローカル変数テーブルの2つの部分が含まれます。上記のバイトコードの主なメソッド部分のほとんどは、これら2つの構造の操作です。
バイトコードから、ボクシング操作がIntegerのvalueOfメソッドを介して実装されていることがわかります。
3.3switchステートメントは文字列型をサポートします
(1)switchステートメントは文字列型をサポートします
Javaコード:
String str = "hello";
switch(str) {
case "world":
System.out.println("world");
break;
case "hello":
System.out.println("hello");
break;
default:
break;
}
逆コンパイル後:
String var1 = "hello";
byte var3 = -1;
switch(var1.hashCode()) {
case 99162322:
if (var1.equals("hello")) {
var3 = 1;
}
break;
case 113318802:
if (var1.equals("world")) {
var3 = 0;
}
}
switch(var3) {
case 0:
System.out.println("world");
break;
case 1:
System.out.println("hello");
}
ご覧のとおり、Javaは、文字列のhashCode()メソッドとequals()メソッドを介して、スイッチによる文字列のサポートを実装しています。switchはint型をサポートしており、hashCodeメソッドの戻り値はint型であることがわかっています。hashCodeを検索した後、equalsメソッドを使用して、ハッシュの競合を回避するかどうかを判断します。
(2)バイトコードからの切り替えの実装原理の簡単な分析
例1:
public int tableSwitchTest(int num) {
switch(num) {
case 100: return 0;
case 101: return 1;
case 104: return 4;
default: return -1;
}
}
javap -cコマンドを使用して、バイトコードを分析します。
public int tableSwitchTest(int);
Code:
0: iload_1
1: tableswitch {
// 100 to 104
100: 36
101: 38
102: 42
103: 42
104: 40
default: 42
}
36: iconst_0
37: ireturn
38: iconst_1
39: ireturn
40: iconst_4
41: ireturn
42: iconst_m1
43: ireturn
明らかに、バイトコードのtableswitchは、Java構文のswitchステートメントを実装します。100:36は、オペランドが100の場合、36行目にジャンプして実行することを意味します。これはiconst_0です。ここではireturnです。つまり、0が返されます。等々。しかし、Javaコードを比較すると、問題が見つかります。バイトコードに102と103が余分に含まれています。これは、コンパイラがケースの値を分析するためです。ケースの値が比較的コンパクトで、途中に障害がほとんどないかまったくない場合は、テーブルスイッチを使用してスイッチケースを実装します。障害がある場合は、連続性を埋めるのに役立つ偽のケースが生成されます。これにより、O(1)時間計算量の検索を実現できます。ケースは連続的に入力されているため、カーソルから一度に見つけることができます。
例2:
public int lookupSwitchTest(int num) {
switch(num) {
case 1: return 1;
case 100: return 100;
case 10: return 10;
default: return -1;
}
}
javap -cコマンドを使用して、バイトコードを分析します。
public int lookupSwitchTest(int);
Code:
0: iload_1
1: lookupswitch {
// 3
1: 36
10: 41
100: 38
default: 44
}
36: iconst_1
37: ireturn
38: bipush 100
40: ireturn
41: bipush 10
43: ireturn
44: iconst_m1
45: ireturn
この例のスイッチの実装ではlookupswitchを使用しています。これは、この例のケース値の分布が比較的まばらであるためです。上記のケースを埋める方法を使用するのは明らかに不合理です。lookupswitchのキーはソートされており、検索時にバイナリ検索で実現できます。時間計算量はO(logN)です。
したがって、結論は次のとおりです。switch-caseステートメントが比較的まばらな場合、コンパイラーはlookupswitch命令を使用して実装します。それ以外の場合、コンパイラーはtableswitchを使用して実装します。
3.4ジェネリック
Javaコード
HashMap<String, String> map = new HashMap<String, String>();
map.put("name", "xiaoming");
String name = map.get("name");
System.out.println(name);
逆コンパイル後
HashMap var1 = new HashMap();
var1.put("name", "xiaoming");
String var2 = (String)var1.get("name");
System.out.println(var2);
逆コンパイル後、ジェネリック型HashMap <String、String>は消え、通常型HashMapのみが消えました。get操作中に、オブジェクト型を必要な文字列型に変換するために、強制型変換が実行されました。
3.5メソッド可変長パラメーター
Javaコード
public static void main(String[] args) {
print("hello","world");
}
public static void print(String...args) {
for(int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
逆コンパイル後:
public static void main(String[] var0) {
print("hello", "world");
}
public static void print(String... var0) {
for(int var1 = 0; var1 < var0.length; ++var1) {
System.out.println(var0[var1]);
}
}