Type erasure ใน Scala

โปรดทราบ : ผมมีความเข้าใจเรื่องนี้ไม่มาก หากมีข้อผิดพลาดรบกวนชี้แนะด้วยครับ

หลังจากที่เราได้คุยกันไป ถึงเรื่อง TypeTag และ ClassTag ใน Scala เลยเกิดความสงสัยว่าจริงๆ แล้วใน Scala มีการลบ Generic ออกไปจริงๆในระดับคอมไพล์เลอร์หรือว่าแค่ตัว JVM ไม่สามารถใช้งานได้เฉยๆ เลยปรึกษากับ อ.ชาญวิทย์ อาจารย์แนะนำให้เอา javap disassembling มันออกมาดู ถ้าเราขียนโค้ด

class ScalaDisassembler (word: List[String])

จากนั้นลองคอมไพล์

$ scalac scalaDisassembler.scala

จากนั้นลองใช้ javap เพื่อ disassembling ไฟล์ ScalaDisassembler.class แล้วจะได้ได้

$ javap ScalaDisassembler.class 
Compiled from "scalaDisassembler.scala"
public class ScalaDisassembler {
public ScalaDisassembler(scala.collection.immutable.List<java.lang.String>);
}

เอ ทำไมมันเก็บ Generic ไว้หล่ะ ทีนี้เอาใหม่ลอง ลองแก้โค้ดเป็น

class ScalaDisassembler (word: List[String]){
val defineInsideListOfInt: List[Int] = List(1,2)
val defineInsideArrayOfInt: Array[Int] = Array(1,2)
}

จากนั้นก็คอมไพล์แล้วสั่ง javap

$ scalac scalaDisassembler.scala
$ javap ScalaDisassembler.class
Compiled from "scalaDisassembler.scala"
public class ScalaDisassembler {
public scala.collection.immutable.List<java.lang.Object> defineInsideListOfInt();
public int[] defineInsideArrayOfInt();
public ScalaDisassembler(scala.collection.immutable.List<java.lang.String>);
}

เอ๊ะถ้า Define ข้างใน Generic หาย แต่ถ้ารับเป็น Parameter ดันทำได้ซะงั้น แต่แอบแปลกใจอีกอย่างอยู่นะว่าทำไม Scala ใช้พอคอมไพล์แล้วได้ Primitive data type เพราะเคยอ่านเจอว่า Type ใน Scala อยู่ในลักษณะ Object แต่ตัวนี้เอาไว้ก่อนจะเห็นว่าถ้าเราใช้พารามิเตอร์มาเปรียบเทียบค่าที่มีความเป็น Generic ใน Scala อาจจะทำได้นะ แต่ตัวแปรปกติทำไม่ได้เพราะมันมองของที่เป็น อยู่ภายใต้มันเป็น java.lang.Object หมด ยังไม่ชัวร์ลองสั่ง javap -c ขึ้นมาดู

$ javap -c ScalaDisassembler.class
Compiled from "scala.scala"
public class ScalaDisassembler {
public scala.collection.immutable.List<java.lang.Object> defineInsideListOfInt();
Code:
0: aload_0
1: getfield #16 // Field defineInsideListOfInt:Lscala/collection/immutable/List;
4: areturn
public int[] defineInsideArrayOfInt();
Code:
0: aload_0
1: getfield #21 // Field defineInsideArrayOfInt:[I
4: areturn
public ScalaDisassembler(scala.collection.immutable.List<java.lang.String>);
Code:
0: aload_0
1: invokespecial #26 // Method java/lang/Object."<init>":()V
4: aload_0
5: getstatic #32 // Field scala/collection/immutable/List$.MODULE$:Lscala/collection/immutable/List$;
8: getstatic #37 // Field scala/Predef$.MODULE$:Lscala/Predef$;
11: iconst_2
12: newarray int
14: dup
15: iconst_0
16: iconst_1
17: iastore
18: dup
19: iconst_1
20: iconst_2
21: iastore
22: invokevirtual #41 // Method scala/Predef$.wrapIntArray:([I)Lscala/collection/mutable/WrappedArray;
25: invokevirtual #45 // Method scala/collection/immutable/List$.apply:(Lscala/collection/Seq;)Lscala/collection/immutable/List;
28: putfield #16 // Field defineInsideListOfInt:Lscala/collection/immutable/List;
31: aload_0
32: iconst_2
33: newarray int
35: dup
36: iconst_0
37: iconst_1
38: iastore
39: dup
40: iconst_1
41: iconst_2
42: iastore
43: putfield #21 // Field defineInsideArrayOfInt:[I
46: return
}

งงจนต้องร้องขอชีวิตกันเลยทีเดียว แต่ก็พอจะเข้าใจได้ว่าตัวแปร defineInsideListOfInt มอง Generic ของมันเป็น java.lang.Object ส่วน defineInsideArrayOfInt ไม่ได้มองของเป็น Generic แต่มันมองเป็น int[]

ทีนี้ลอง Java บ้างดีกว่าจะได้ข้อสรุปแบบไหน ผมลองสร้างไฟล์ JavaDisassembler.java ขึ้นมามีโค้ดดังนี้ (เราไม่สนใจว่าโค้ดจะทำงานได้ไหม)

import java.util.*;
public class JavaDisassembler{
public JavaDisassembler(List<String> word){};
List<Integer> defineInsideListOfInt = Arrays.asList(1,2);
int[] defineInsideArrayOfInt = {1,2};
}

จากนั้นก็คอมไพล์แล้วสั่ง javap

$javac JavaDisassembler.java
$ javap JavaDisassembler.class
Compiled from "JavaDisassembler.java"
public class JavaDisassembler {
java.util.List<java.lang.Integer> defineInsideListOfInt;
int[] defineInsideArrayOfInt;
public JavaDisassembler(java.util.List<java.lang.String>);
}

อ้าวเฮ้ยยยย!! ไม่เหมือนที่คุยกันไว้นี่หน่า นึกว่าจะไม่ติด Generic มานะเนี่ย ดันติดมาด้วย แต่ถ้าลองเพิ่ม method main เข้าไป

import java.util.*;
public class JavaDisassembler{
public JavaDisassembler(List<String> word){};
List<Integer> defineInsideListOfInt = Arrays.asList(1,2);
int[] defineInsideArrayOfInt = {1,2};
public static void main(String[] args){
List<Integer> defineInsideMainListOfInt = Arrays.asList(1,2);
}
}

ทีนี้ปรากฏว่าไม่ออกซะแล้วอยู่ลึกเกิน

$ javap JavaDisassembler.class
Compiled from "JavaDisassembler.java"
public class JavaDisassembler {
java.util.List<java.lang.Integer> defineInsideListOfInt;
int[] defineInsideArrayOfInt;
public JavaDisassembler(java.util.List<java.lang.String>);
public static void main(java.lang.String[]);
}

ต้องใช้ javap -v

$ javap -c JavaDisassembler.class
Compiled from "JavaDisassembler.java"
public class JavaDisassembler {
java.util.List<java.lang.Integer> defineInsideListOfInt;
int[] defineInsideArrayOfInt;
public JavaDisassembler(java.util.List<java.lang.String>);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_2
6: anewarray #2 // class java/lang/Integer
9: dup
10: iconst_0
11: iconst_1
12: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: aastore
16: dup
17: iconst_1
18: iconst_2
19: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: aastore
23: invokestatic #4 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
26: putfield #5 // Field defineInsideListOfInt:Ljava/util/List;
29: aload_0
30: iconst_2
31: newarray int
33: dup
34: iconst_0
35: iconst_1
36: iastore
37: dup
38: iconst_1
39: iconst_2
40: iastore
41: putfield #6 // Field defineInsideArrayOfInt:[I
44: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: anewarray #2 // class java/lang/Integer
4: dup
5: iconst_0
6: iconst_1
7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: aastore
11: dup
12: iconst_1
13: iconst_2
14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: aastore
18: invokestatic #4 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
21: astore_1
22: return
}

ผมอ่านไม่เข้าใจเท่าไหร่แต่เท่าที่ดู Method constructor code 23 และ main() code 18 แล้วมันแปลงเป็น java.lang.Object ผมคิดว่าตัวอย่างนี้อ่านไบต์โค้ดยากเกินไป และยังมีความซับซ้อนเรื่องของ Array ใน Java เป็น Object แต่ไม่ได้มีคลาสจริงๆ ตัว JVM จะสร้างคลาสให้ในช่วง Runtime (ตัว JVM จริงๆ เลย) เพื่อให้ได้ประสิทธิภาพ

สรุปว่า
ใน Scala ถ้าเป็นพารามิเตอร์ของ Contructor และ Return type ดูเหมือน Generic จะยังคงเหลือไว้อยู่ ส่วนการกำหนดค่าตัวแปรแบบปกตินั้นไม่เหลือ Generic type ไว้
ใน Java ตัวแปรระดับคลาสและ Contructor ดูเหมือนจะเก็บ Generic type เอาไว้
แต่อย่างไรก็ตามการที่มี Type information เช่น Generic หรือชื่อตัวแปรเหลืออยู่ใน .class ก็ไม่ได้หมายความว่า JVM มันจะเอาไปใช้ได้ เนื่องจากเรื่อง Backward compatibility ที่ต้องให้โค้ดทำงานได้ใน JVM ที่ต่ำกว่า
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.