สวัสดี! เราดำเนินการต่อชุดบทเรียนของเราเกี่ยวกับยาชื่อสามัญ ก่อนหน้านี้เรามีความคิดทั่วไปว่าพวกเขาคืออะไรและเหตุใดจึงจำเป็น วันนี้เราจะเรียนรู้เพิ่มเติมเกี่ยวกับคุณลักษณะบางอย่างของยาชื่อสามัญและการทำงานกับพวกเขา ไปกันเถอะ!
ในบทเรียนที่แล้วเราได้พูดถึงความแตกต่างระหว่างประเภททั่วไปและประเภทดิบ ประเภทดิบคือคลาสทั่วไปที่ถูกลบประเภทแล้ว
เอกสารระบุว่า "T - ประเภทของคลาสที่จำลองโดยวัตถุคลาสนี้" การแปลสิ่งนี้จากภาษาของเอกสารเป็นคำพูดธรรมดา เราเข้าใจดีว่าคลาสของ

List list = new ArrayList();
นี่คือตัวอย่าง ที่นี่เราไม่ได้ระบุว่าจะวางวัตถุประเภทใดในList
. หากเราพยายามสร้างList
และเพิ่มวัตถุเข้าไป เราจะเห็นคำเตือนใน IDEA:
"Unchecked call to add(E) as a member of raw type of java.util.List".
แต่เรายังได้พูดคุยเกี่ยวกับความจริงที่ว่าชื่อสามัญปรากฏเฉพาะใน Java 5 เมื่อเวอร์ชันนี้เปิดตัว โปรแกรมเมอร์ได้เขียนโค้ดจำนวนมากโดยใช้ประเภทดิบ ดังนั้นคุณลักษณะของภาษานี้จึงไม่สามารถหยุดทำงานได้ และความสามารถในการ สร้างประเภทดิบใน Java ถูกรักษาไว้ อย่างไรก็ตาม ปัญหากลับกลายเป็นว่าลุกลามมากขึ้น อย่างที่คุณทราบ โค้ด Java จะถูกแปลงเป็นรูปแบบคอมไพล์พิเศษที่เรียกว่า bytecode ซึ่งจากนั้น Java virtual machine จะดำเนินการ แต่ถ้าเราใส่ข้อมูลเกี่ยวกับพารามิเตอร์ประเภทใน bytecode ในระหว่างกระบวนการแปลง มันจะทำลายโค้ดที่เขียนก่อนหน้านี้ทั้งหมด เพราะไม่มีพารามิเตอร์ประเภทก่อน Java 5! เมื่อทำงานกับยาชื่อสามัญ มีแนวคิดที่สำคัญอย่างหนึ่งที่คุณต้องจำไว้ เรียกว่าประเภทลบ. หมายความว่าคลาสไม่มีข้อมูลเกี่ยวกับพารามิเตอร์ประเภท ข้อมูลนี้มีให้ใช้งานระหว่างการคอมไพล์เท่านั้น และจะถูกลบ (ไม่สามารถเข้าถึงได้) ก่อนรันไทม์ หากคุณพยายามใส่วัตถุผิดประเภทใน ของคุณList<String>
คอมไพลเลอร์จะสร้างข้อผิดพลาด นี่คือสิ่งที่ผู้สร้างภาษาต้องการให้บรรลุเมื่อพวกเขาสร้างชื่อสามัญ: การตรวจสอบเวลาคอมไพล์ แต่เมื่อโค้ด Java ทั้งหมดของคุณเปลี่ยนเป็น bytecode ก็จะไม่มีข้อมูลเกี่ยวกับพารามิเตอร์ประเภทอีกต่อไป ใน bytecode List<Cat>
รายการแมวของคุณไม่แตกต่างจากList<String>
สตริง ใน bytecode ไม่มีอะไรบอกว่าcats
เป็นรายการของCat
วัตถุ ข้อมูลดังกล่าวจะถูกลบระหว่างการคอมไพล์ — เฉพาะความจริงที่ว่าคุณมีList<Object> cats
รายการเท่านั้นที่จะลงเอยด้วยรหัสไบต์ของโปรแกรม มาดูกันว่ามันทำงานอย่างไร:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
เราสร้างTestClass
คลาส ทั่วไปของเราเอง มันค่อนข้างเรียบง่าย: จริงๆ แล้วมันเป็น "คอลเลกชัน" เล็กๆ ของวัตถุ 2 ชิ้น ซึ่งจะถูกจัดเก็บทันทีเมื่อวัตถุถูกสร้างขึ้น มี 2 T
ช่อง เมื่อcreateAndAdd2Values()
เมธอดถูกดำเนินการ อ็อบเจกต์ที่ส่งผ่านทั้งสองรายการ ( Object a
และObject b
ต้องแปลงเป็นT
ประเภท แล้วจึงเพิ่มลงใน ออบเจกต์ TestClass
ในmain()
เมธอด เราสร้าง a TestClass<Integer>
นั่นคือInteger
อาร์กิวเมนต์ type จะแทนที่Integer
พารามิเตอร์ประเภท นอกจากนี้ เรายังส่ง a Double
และ a String
ไป ยัง วิธีcreateAndAdd2Values()
การ คุณคิดว่าโปรแกรมของเราจะใช้ได้หรือไม่ ท้ายที่สุด เราระบุInteger
เป็นอาร์กิวเมนต์ type แต่String
ไม่สามารถส่งไปยัง an ได้อย่างแน่นอนInteger
!main()
วิธีการและตรวจสอบ เอาต์พุตคอนโซล:
22.111
Test String
ที่คาดไม่ถึง! ทำไมสิ่งนี้ถึงเกิดขึ้น? เป็นผลมาจากการลบประเภท ข้อมูลเกี่ยวกับInteger
อาร์กิวเมนต์ประเภทที่ใช้ในการยกตัวอย่างTestClass<Integer> test
วัตถุของเราถูกลบเมื่อรวบรวมโค้ด สนามกลายเป็นTestClass<Object> test
. อาร์กิวเมนต์ ของเราDouble
และString
ถูกแปลงเป็นObject
วัตถุอย่างง่ายดาย (ไม่ได้แปลงเป็นInteger
วัตถุอย่างที่เราคาดไว้!) และเพิ่มลงTestClass
ใน ต่อไปนี้เป็นอีกตัวอย่างง่ายๆ แต่เปิดเผยมากของการลบประเภท:
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
เอาต์พุตคอนโซล:
true
true
ดูเหมือนว่าเราสร้างคอลเลกชันที่มีอาร์กิวเมนต์ที่แตกต่างกันสามประเภท — String
, และ คลาสInteger
ของเราเอง Cat
แต่ในระหว่างการแปลงเป็น bytecode ทั้งสามรายการจะกลายเป็นList<Object>
ดังนั้นเมื่อโปรแกรมทำงาน มันจะบอกเราว่าเราใช้คลาสเดียวกันในทั้งสามกรณี
พิมพ์การลบเมื่อทำงานกับอาร์เรย์และชื่อสามัญ
มีจุดสำคัญที่ต้องเข้าใจอย่างชัดเจนเมื่อทำงานกับอาร์เรย์และคลาสทั่วไป (เช่นList
) คุณควรคำนึงถึงสิ่งนี้ด้วยเมื่อเลือกโครงสร้างข้อมูลสำหรับโปรแกรมของคุณ ยาสามัญอาจมีการลบประเภท ไม่มีข้อมูลเกี่ยวกับพารามิเตอร์ประเภทที่รันไทม์ ในทางตรงกันข้าม อาร์เรย์รู้และสามารถใช้ข้อมูลเกี่ยวกับประเภทข้อมูลเมื่อโปรแกรมกำลังทำงาน การพยายามใส่ประเภทที่ไม่ถูกต้องลงในอาร์เรย์จะทำให้เกิดข้อยกเว้น:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
เอาต์พุตคอนโซล:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
เนื่องจากมีความแตกต่างอย่างมากระหว่างอาร์เรย์และทั่วไป พวกเขาอาจมีปัญหาด้านความเข้ากันได้ เหนือสิ่งอื่นใด คุณไม่สามารถสร้างอาร์เรย์ของอ็อบเจกต์ทั่วไปหรือแม้แต่อาร์เรย์ที่กำหนดพารามิเตอร์ได้ ฟังดูสับสนเล็กน้อยหรือไม่? ลองมาดูกัน ตัวอย่างเช่น คุณไม่สามารถทำสิ่งนี้ใน Java:
new List<T>[]
new List<String>[]
new T[]
หากเราพยายามสร้างอาร์เรย์ของList<String>
วัตถุ เราได้รับข้อผิดพลาดในการรวบรวมที่บ่นเกี่ยวกับการสร้างอาร์เรย์ทั่วไป:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
// Compilation error! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
แต่ทำไมจึงทำเช่นนี้? เหตุใดจึงไม่อนุญาตการสร้างอาร์เรย์ดังกล่าว ทั้งหมดนี้เพื่อความปลอดภัยของประเภท หากคอมไพเลอร์ให้เราสร้างอาร์เรย์ของออบเจกต์ทั่วๆ ไป เราอาจสร้างปัญหามากมายให้ตัวเราเองได้ นี่คือตัวอย่างง่ายๆ จากหนังสือ "Effective Java" ของ Joshua Bloch:
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
ลองจินตนาการว่าการสร้างอาร์เรย์แบบเดียวกันนั้นList<String>[] stringLists
ได้รับอนุญาตและจะไม่สร้างข้อผิดพลาดในการคอมไพล์ หากสิ่งนี้เป็นจริง ต่อไปนี้คือสิ่งที่เราสามารถทำได้: ในบรรทัดที่ 1 เราสร้างอาร์เรย์ของรายการList<String>[] stringLists
: อาร์เรย์ของเรามีหนึ่งList<String>
. ในบรรทัดที่ 2 เราสร้างรายการตัวเลข: List<Integer>
. ในบรรทัดที่ 3 เรากำหนดList<String>[]
ตัวแปร ของ Object[] objects
เรา ภาษา Java อนุญาตสิ่งนี้: อาร์เรย์ของX
วัตถุสามารถจัดเก็บX
วัตถุและวัตถุของคลาสย่อยX
ทั้งหมด คุณสามารถใส่อะไรก็ได้ในObject
อาร์เรย์ ในบรรทัดที่ 4 เราแทนที่องค์ประกอบเดียวของobjects()
อาร์เรย์ (a List<String>
) ด้วยList<Integer>
a ดังนั้นเราจึงใส่List<Integer>
อาร์เรย์ที่มีไว้เพื่อจัดเก็บเท่านั้นList<String>
วัตถุ! เราจะพบข้อผิดพลาดเมื่อเรารันบรรทัดที่ 5 เท่านั้น A ClassCastException
จะถูกส่งไปที่รันไทม์ ด้วยเหตุนี้ จึงมีการเพิ่มข้อห้ามในการสร้างอาร์เรย์ดังกล่าวใน Java สิ่งนี้ช่วยให้เราหลีกเลี่ยงสถานการณ์ดังกล่าวได้
ฉันจะหลีกเลี่ยงการลบประเภทได้อย่างไร
เราได้เรียนรู้เกี่ยวกับการลบประเภท มาลองหลอกระบบกัน! :) ภารกิจ: เรามีTestClass<T>
คลาส ทั่วไป เราต้องการเขียนcreateNewT()
เมธอดสำหรับคลาสนี้ซึ่งจะสร้างและส่งคืนT
วัตถุ ใหม่ แต่มันเป็นไปไม่ได้ใช่ไหม? ข้อมูลทั้งหมดเกี่ยวกับT
ประเภทจะถูกลบระหว่างการคอมไพล์ และในขณะรันไทม์ เราไม่สามารถกำหนดประเภทของวัตถุที่เราต้องการสร้างได้ มีวิธีหนึ่งที่ยุ่งยากในการทำเช่นนี้ คุณคงจำได้ว่า Java มีClass
คลาส เราสามารถใช้มันเพื่อกำหนดคลาสของวัตถุใดๆ ของเรา:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
เอาต์พุตคอนโซล:
class java.lang.Integer
class java.lang.String
แต่นี่คือแง่มุมหนึ่งที่เรายังไม่ได้พูดถึง ในเอกสารประกอบของ Oracle คุณจะเห็นว่าคลาส Class เป็นคลาสทั่วไป! 
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
ออบเจกต์ไม่ใช่แค่Class
แต่แทนที่จะClass<Integer>
เป็น ประเภทของString.class
วัตถุไม่ใช่แค่Class
แต่แทนที่จะเป็นClass<String>
ฯลฯ หากยังไม่ชัดเจน ให้ลองเพิ่มพารามิเตอร์ประเภทในตัวอย่างก่อนหน้า:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
// Compilation error!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
// Compilation error!
Class<Double> classString2 = String.class;
}
}
และตอนนี้ เมื่อใช้ความรู้นี้ เราสามารถหลีกเลี่ยงการลบประเภทและทำงานของเราให้สำเร็จได้! ลองรับข้อมูลเกี่ยวกับพารามิเตอร์ประเภท อาร์กิวเมนต์ประเภทของเราจะเป็นMySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
และนี่คือวิธีที่เราใช้โซลูชันของเราในทางปฏิบัติ:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
เอาต์พุตคอนโซล:
A MySecretClass object was created successfully!
เราเพิ่งส่งอาร์กิวเมนต์คลาสที่จำเป็นไปยังตัวสร้างคลาสทั่วไปของเรา:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
สิ่งนี้ทำให้เราสามารถบันทึกข้อมูลเกี่ยวกับอาร์กิวเมนต์ประเภท ป้องกันไม่ให้ถูกลบทั้งหมด เป็นผลให้เราสามารถสร้างT
วัตถุ! :) ด้วยเหตุนี้ บทเรียนของวันนี้ก็จบลง คุณต้องจำการลบประเภทเสมอเมื่อทำงานกับยาชื่อสามัญ วิธีแก้ไขปัญหานี้ดูไม่ค่อยสะดวกนัก แต่คุณควรเข้าใจว่ายาสามัญไม่ได้เป็นส่วนหนึ่งของภาษา Java เมื่อสร้างขึ้น ฟีเจอร์นี้ซึ่งช่วยให้เราสร้างคอลเล็กชันที่กำหนดพารามิเตอร์และตรวจจับข้อผิดพลาดระหว่างการคอมไพล์ได้ถูกนำมาใช้ในภายหลัง ในภาษาอื่นบางภาษาที่รวมชื่อสามัญจากเวอร์ชันแรก จะไม่มีการลบประเภท (เช่น ใน C#) อย่างไรก็ตาม เรายังศึกษายาชื่อสามัญไม่จบสิ้น! ในบทเรียนถัดไป คุณจะได้ทำความคุ้นเคยกับคุณสมบัติเพิ่มเติมของยาชื่อสามัญ สำหรับตอนนี้ จะเป็นการดีที่จะแก้ปัญหาสองอย่าง! :)
GO TO FULL VERSION