1. บทนำ
Java Generics ถูกนำมาใช้ใน JDK 5.0 โดยมีจุดประสงค์เพื่อลดจุดบกพร่องและเพิ่มเลเยอร์พิเศษของสิ่งที่เป็นนามธรรมในประเภทต่างๆ
บทความนี้เป็นบทแนะนำสั้น ๆ เกี่ยวกับ Generics ใน Java เป้าหมายเบื้องหลังและวิธีใช้เพื่อปรับปรุงคุณภาพของโค้ดของเรา
2. ความต้องการยาสามัญ
ลองจินตนาการสถานการณ์ที่เราต้องการสร้างรายการใน Java เพื่อเก็บจำนวนเต็ม ; เราสามารถถูกล่อลวงให้เขียน:
List list = new LinkedList(); list.add(new Integer(1)); Integer i = list.iterator().next();
น่าแปลกที่คอมไพเลอร์จะบ่นเกี่ยวกับบรรทัดสุดท้าย ไม่ทราบว่าชนิดข้อมูลถูกส่งกลับ คอมไพเลอร์จะต้องมีการแคสต์อย่างชัดเจน:
Integer i = (Integer) list.iterator.next();
ไม่มีสัญญาใดที่สามารถรับประกันได้ว่าประเภทการส่งคืนของรายการเป็นจำนวนเต็ม รายการที่กำหนดสามารถเก็บวัตถุใด ๆ เรารู้เพียงว่าเรากำลังดึงรายการโดยการตรวจสอบบริบท เมื่อดูประเภทมันสามารถรับประกันได้ว่าเป็นวัตถุเท่านั้นดังนั้นจึงต้องมีการร่ายอย่างชัดเจนเพื่อให้แน่ใจว่าประเภทนั้นปลอดภัย
หล่อนี้อาจจะน่ารำคาญเรารู้ว่าชนิดข้อมูลในรายการนี้เป็นจำนวนเต็ม นักแคสยังถ่วงโค้ดของเรา อาจทำให้เกิดข้อผิดพลาดรันไทม์ที่เกี่ยวข้องกับประเภทหากโปรแกรมเมอร์ทำผิดพลาดกับการแคสต์อย่างชัดเจน
มันจะง่ายกว่ามากถ้าโปรแกรมเมอร์สามารถแสดงเจตจำนงในการใช้ประเภทเฉพาะและคอมไพเลอร์สามารถรับรองความถูกต้องของประเภทดังกล่าวได้ นี่คือแนวคิดหลักที่อยู่เบื้องหลังยาชื่อสามัญ
มาแก้ไขบรรทัดแรกของข้อมูลโค้ดก่อนหน้าเป็น:
List list = new LinkedList();
ด้วยการเพิ่มตัวดำเนินการเพชรที่มีประเภทเรา จำกัด ความเชี่ยวชาญของรายการนี้เฉพาะประเภทจำนวนเต็มเช่นเราระบุประเภทที่จะเก็บไว้ในรายการ คอมไพเลอร์สามารถบังคับใช้ประเภทในเวลาคอมไพล์
ในโปรแกรมขนาดเล็กสิ่งนี้อาจดูเหมือนเป็นเรื่องเล็กน้อยอย่างไรก็ตามในโปรแกรมขนาดใหญ่สิ่งนี้สามารถเพิ่มความแข็งแกร่งอย่างมีนัยสำคัญและทำให้โปรแกรมอ่านง่ายขึ้น
3. วิธีการทั่วไป
วิธีการทั่วไปคือวิธีการที่เขียนขึ้นด้วยการประกาศวิธีเดียวและสามารถเรียกใช้ด้วยอาร์กิวเมนต์ประเภทต่างๆ คอมไพเลอร์จะตรวจสอบความถูกต้องว่าจะใช้แบบใด นี่คือคุณสมบัติบางประการของวิธีการทั่วไป:
- เมธอดทั่วไปมีพารามิเตอร์ type (ตัวดำเนินการเพชรล้อมรอบชนิด) ก่อนประเภทการส่งคืนของการประกาศเมธอด
- พารามิเตอร์ประเภทสามารถกำหนดขอบเขตได้ (ขอบเขตจะอธิบายต่อไปในบทความ)
- วิธีการทั่วไปสามารถมีพารามิเตอร์ประเภทต่างๆคั่นด้วยเครื่องหมายจุลภาคในลายเซ็นของวิธีการ
- Method body สำหรับวิธีการทั่วไปก็เหมือนกับวิธีปกติ
ตัวอย่างของการกำหนดวิธีการทั่วไปในการแปลงอาร์เรย์เป็นรายการ:
public List fromArrayToList(T[] a) { return Arrays.stream(a).collect(Collectors.toList()); }
ในตัวอย่างก่อนหน้านี้ไฟล์ ในลายเซ็นของวิธีการที่แสดงให้เห็นว่าวิธีการที่จะจัดการกับทั่วไปประเภทT สิ่งนี้จำเป็นแม้ว่าเมธอดจะคืนค่าเป็นโมฆะ
ดังที่ได้กล่าวไว้ข้างต้นวิธีการนี้สามารถจัดการกับประเภททั่วไปได้มากกว่าหนึ่งประเภทซึ่งในกรณีนี้จะต้องเพิ่มประเภททั่วไปทั้งหมดลงในลายเซ็นของวิธีการตัวอย่างเช่นหากเราต้องการแก้ไขวิธีการข้างต้นเพื่อจัดการกับประเภทTและประเภทGควรเขียนดังนี้:
public static List fromArrayToList(T[] a, Function mapperFunction) { return Arrays.stream(a) .map(mapperFunction) .collect(Collectors.toList()); }
เรากำลังส่งผ่านฟังก์ชันที่แปลงอาร์เรย์ที่มีองค์ประกอบประเภทTไปยังรายการด้วยองค์ประกอบประเภทGตัวอย่างเช่นการแปลงจำนวนเต็มเป็นการแสดงสตริง :
@Test public void givenArrayOfIntegers_thanListOfStringReturnedOK() { Integer[] intArray = {1, 2, 3, 4, 5}; List stringList = Generics.fromArrayToList(intArray, Object::toString); assertThat(stringList, hasItems("1", "2", "3", "4", "5")); }
เป็นที่น่าสังเกตว่าคำแนะนำของ Oracle คือการใช้อักษรตัวพิมพ์ใหญ่เพื่อแสดงประเภททั่วไปและเลือกตัวอักษรที่สื่อความหมายมากขึ้นเพื่อแสดงประเภทที่เป็นทางการตัวอย่างเช่นใน Java Collections Tใช้สำหรับประเภทKสำหรับคีย์Vสำหรับค่า
3.1. Generics ที่ถูกผูกไว้
ดังที่กล่าวไว้ก่อนหน้านี้พารามิเตอร์ประเภทสามารถถูก จำกัด ขอบเขต Bounded หมายถึง " จำกัด " เราสามารถ จำกัด ประเภทที่วิธีการยอมรับได้
ตัวอย่างเช่นเราสามารถระบุว่าเมธอดยอมรับประเภทและคลาสย่อยทั้งหมด (ขอบเขตบน) หรือประเภทซูเปอร์คลาสทั้งหมด (ขอบเขตล่าง)
ในการประกาศประเภทขอบเขตบนเราใช้คำหลักขยายหลังจากประเภทตามด้วยขอบเขตบนที่เราต้องการใช้ ตัวอย่างเช่น:
public List fromArrayToList(T[] a) { ... }
คีย์เวิร์ดขยายที่นี่ใช้เพื่อหมายความว่าประเภทTขยายขอบเขตบนในกรณีของคลาสหรือใช้ขอบเขตบนในกรณีของอินเทอร์เฟซ
3.2. หลายขอบเขต
ประเภทยังสามารถมีขอบเขตบนได้หลายแบบดังนี้:
ถ้าหนึ่งในประเภทที่ขยายโดยTเป็นคลาส (เช่นNumber ) จะต้องใส่ไว้ในรายการขอบเขตก่อน มิฉะนั้นจะทำให้เกิดข้อผิดพลาดเวลาคอมไพล์
4. การใช้สัญลักษณ์แทนกับ Generics
สัญลักษณ์แทนจะแสดงด้วยเครื่องหมายคำถามใน Java“ ? ” และใช้เพื่ออ้างถึงประเภทที่ไม่รู้จัก สัญลักษณ์ตัวแทนเป็นประโยชน์อย่างยิ่งเมื่อใช้ยาชื่อสามัญและสามารถนำมาใช้เป็นชนิดพารามิเตอร์ แต่ครั้งแรกที่มีความสำคัญทราบเพื่อพิจารณา
เป็นที่ทราบกันดีว่าObjectเป็น supertype ของคลาส Java ทั้งหมดอย่างไรก็ตามคอลเล็กชันของObjectไม่ใช่ supertype ของคอลเล็กชันใด ๆ
ตัวอย่างเช่นListไม่ใช่ supertype ของListและการกำหนดตัวแปรชนิดListให้กับตัวแปรชนิดListจะทำให้คอมไพเลอร์ผิดพลาด นี่คือการป้องกันความขัดแย้งที่อาจเกิดขึ้นได้หากเราเพิ่มประเภทที่แตกต่างกันในคอลเล็กชันเดียวกัน
กฎเดียวกันนี้ใช้กับคอลเล็กชันประเภทและประเภทย่อยใด ๆ ลองพิจารณาตัวอย่างนี้:
public static void paintAllBuildings(List buildings) { buildings.forEach(Building::paint); }
ถ้าเรานึกภาพประเภทย่อยของอาคารตัวอย่างเช่นบ้านเราจะใช้วิธีนี้กับรายการบ้านไม่ได้แม้ว่าบ้านจะเป็นประเภทย่อยของอาคารก็ตาม หากเราจำเป็นต้องใช้วิธีนี้กับประเภทสิ่งปลูกสร้างและประเภทย่อยทั้งหมดไวด์การ์ดที่มีขอบเขตสามารถทำเวทมนตร์ได้:
public static void paintAllBuildings(List buildings) { ... }
ตอนนี้วิธีนี้จะใช้ได้กับ type Buildingและประเภทย่อยทั้งหมด สิ่งนี้เรียกว่าสัญลักษณ์แทนขอบเขตบนโดยที่ประเภทอาคารคือขอบเขตด้านบน
นอกจากนี้ยังสามารถระบุสัญลักษณ์แทนด้วยขอบเขตล่างโดยที่ประเภทที่ไม่รู้จักจะต้องเป็นซุปเปอร์ไทป์ของประเภทที่ระบุ ขอบเขตล่างสามารถระบุได้โดยใช้ซูเปอร์คีย์เวิร์ดตามด้วยประเภทเฉพาะตัวอย่างเช่นหมายถึงประเภทที่ไม่รู้จักซึ่งเป็นซูเปอร์คลาสของT (= T และผู้ปกครองทั้งหมด)
5. พิมพ์ Erasure
Generics ถูกเพิ่มเข้าไปใน Java เพื่อให้แน่ใจว่าประเภทมีความปลอดภัยและเพื่อให้แน่ใจว่า generics จะไม่ทำให้เกิดโอเวอร์เฮดที่รันไทม์คอมไพเลอร์จะใช้กระบวนการที่เรียกว่าtype erasure on generics ในเวลาคอมไพล์
Type erasure จะลบพารามิเตอร์ประเภททั้งหมดและแทนที่ด้วยขอบเขตหรือด้วยObjectหากพารามิเตอร์ type ไม่ถูกผูกไว้ ดังนั้น bytecode หลังการคอมไพล์จึงมีเฉพาะคลาสอินเตอร์เฟสและเมธอดปกติเท่านั้นจึงมั่นใจได้ว่าไม่มีการสร้างชนิดใหม่ การหล่อที่เหมาะสมจะถูกนำไปใช้กับประเภทวัตถุในเวลาคอมไพล์
นี่คือตัวอย่างของการลบประเภท:
public List genericMethod(List list) { return list.stream().collect(Collectors.toList()); }
With type erasure, the unbounded type T is replaced with Object as follows:
// for illustration public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } // which in practice results in public List withErasure(List list) { return list.stream().collect(Collectors.toList()); }
If the type is bounded, then the type will be replaced by the bound at compile time:
public void genericMethod(T t) { ... }
would change after compilation:
public void genericMethod(Building t) { ... }
6. Generics and Primitive Data Types
A restriction of generics in Java is that the type parameter cannot be a primitive type.
For example, the following doesn't compile:
List list = new ArrayList(); list.add(17);
To understand why primitive data types don't work, let's remember that generics are a compile-time feature, meaning the type parameter is erased and all generic types are implemented as type Object.
As an example, let's look at the add method of a list:
List list = new ArrayList(); list.add(17);
The signature of the add method is:
boolean add(E e);
And will be compiled to:
boolean add(Object e);
Therefore, type parameters must be convertible to Object. Since primitive types don't extend Object, we can't use them as type parameters.
However, Java provides boxed types for primitives, along with autoboxing and unboxing to unwrap them:
Integer a = 17; int b = a;
So, if we want to create a list which can hold integers, we can use the wrapper:
List list = new ArrayList(); list.add(17); int first = list.get(0);
The compiled code will be the equivalent of:
List list = new ArrayList(); list.add(Integer.valueOf(17)); int first = ((Integer) list.get(0)).intValue();
Future versions of Java might allow primitive data types for generics. Project Valhalla aims at improving the way generics are handled. The idea is to implement generics specialization as described in JEP 218.
7. Conclusion
Java Generics เป็นส่วนเสริมที่มีประสิทธิภาพในภาษา Java เนื่องจากทำให้งานของโปรแกรมเมอร์ง่ายขึ้นและมีข้อผิดพลาดน้อยลง Generics บังคับใช้ความถูกต้องของประเภทในเวลารวบรวมและที่สำคัญที่สุดคือเปิดใช้งานการใช้อัลกอริทึมทั่วไปโดยไม่ทำให้แอปพลิเคชันของเรามีค่าใช้จ่ายเพิ่มเติม
ซอร์สโค้ดที่มาพร้อมกับบทความมีอยู่ใน GitHub