สวัสดี! เราดำเนินชุดการบรรยายเกี่ยวกับยาชื่อสามัญต่อไป ก่อนหน้านี้เราเข้าใจในแง่ทั่วไปว่ามันคืออะไรและทำไมจึงจำเป็น วันนี้เราจะพูดถึงคุณลักษณะบางอย่างของยาชื่อสามัญและดูข้อผิดพลาดบางประการเมื่อใช้งาน ไป! ในการบรรยายครั้งล่าสุดเราได้พูดถึงความแตกต่างระหว่างGeneric TypesและRaw Types ในกรณีที่คุณลืม Raw Type คือคลาสทั่วไปที่ถูกลบประเภทออกไปแล้ว
List list = new ArrayList();
นี่คือตัวอย่าง ในที่นี้เราไม่ได้ระบุประเภทของออบเจ็กต์ที่จะวางในList
. หากเราพยายามสร้างList
และเพิ่มวัตถุเข้าไป เราจะเห็นคำเตือนใน IDa:
“Unchecked call to add(E) as a member of raw type of java.util.List”.
แต่เรายังพูดถึงข้อเท็จจริงที่ว่ายาชื่อสามัญปรากฏเฉพาะในภาษาเวอร์ชัน Java 5 เท่านั้น เมื่อถึงเวลาเปิดตัวโปรแกรมเมอร์ได้เขียนโค้ดจำนวนมากโดยใช้ Raw Types และเพื่อที่จะไม่หยุดทำงานความสามารถในการ การสร้างและทำงานกับ Raw Types ใน Java ยังคงอยู่ อย่างไรก็ตาม ปัญหานี้กลับกลายเป็นว่ากว้างขึ้นมาก ดังที่คุณทราบโค้ด Java จะถูกแปลงเป็น bytecode พิเศษ ซึ่งจะถูกเรียกใช้งานโดยเครื่องเสมือน Java และหากในระหว่างกระบวนการแปล เราใส่ข้อมูลเกี่ยวกับประเภทพารามิเตอร์ลงใน bytecode มันจะทำลายโค้ดที่เขียนก่อนหน้านี้ทั้งหมด เพราะก่อน Java 5 ไม่มีประเภทพารามิเตอร์อยู่! เมื่อทำงานกับยาชื่อสามัญ มีคุณลักษณะที่สำคัญอย่างหนึ่งที่คุณต้องจำไว้ เรียกว่าการลบแบบ สาระสำคัญอยู่ที่ความจริงที่ว่าไม่มีข้อมูลเกี่ยวกับประเภทพารามิเตอร์ถูกเก็บไว้ในชั้นเรียน ข้อมูลนี้มีเฉพาะในขั้นตอนการคอมไพล์เท่านั้น และจะถูกลบ (ไม่สามารถเข้าถึงได้) เมื่อรันไทม์ หากคุณพยายามใส่วัตถุผิดประเภทลงในList<String>
คอมไพลเลอร์จะส่งข้อผิดพลาด นี่คือสิ่งที่ผู้สร้างภาษาทำได้โดยการสร้างยาชื่อสามัญ - ตรวจสอบในขั้นตอนการคอมไพล์ แต่เมื่อโค้ด Java ทั้งหมดที่คุณเขียนกลายเป็น 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()
ที่เราสร้างTestClass<Integer>
นั่นคือคุณภาพT
เราจะมีInteger
. แต่ในขณะเดียวกันcreateAndAdd2Values()
เราก็ส่งผ่านตัวเลขDouble
และวัตถุ ไปยัง String
เมธอด คุณคิดว่าโปรแกรมของเราจะใช้งานได้หรือไม่? ท้ายที่สุดแล้ว เราได้ระบุเป็นประเภทพารามิเตอร์Integer
แต่String
แน่นอนว่าไม่สามารถส่งไปที่Integer
! ลองใช้วิธีการmain()
และตรวจสอบ เอาต์พุตคอนโซล: 22.111 สตริงทดสอบ ผลลัพธ์ที่ไม่คาดคิด! ทำไมสิ่งนี้ถึงเกิดขึ้น? แม่นยำเพราะการลบประเภท ในระหว่างการคอมไพล์โค้ด ข้อมูลเกี่ยวกับประเภทพารามิเตอร์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) {
//ошибка компиляции! 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
และอ็อบเจ็กต์ของคลาสลูกทั้งหมด ลงในอาร์เรย์ของอ็อบเจ็กต์ Х
ได้ ดังนั้นObjects
คุณสามารถใส่อะไรก็ได้ลงในอาร์เรย์ได้ ในบรรทัดที่ 4 เราจะแทนที่องค์ประกอบ เดี่ยวของอาร์เรย์objects (List<String>)
ด้วยรายการ List<Integer>
ด้วยเหตุนี้ เราจึงใส่List<Integer>
อาร์เรย์ของเราซึ่งมีไว้เพื่อจัดเก็บList<String>
! เราจะพบข้อผิดพลาดเฉพาะเมื่อโค้ดถึงบรรทัดที่ 5 เท่านั้น ข้อยกเว้นจะเกิดขึ้นระหว่างการทำงานของClassCastException
โปรแกรม ดังนั้นจึงมีการห้ามการสร้างอาร์เรย์ดังกล่าวในภาษา Java ซึ่งช่วยให้เราหลีกเลี่ยงสถานการณ์ดังกล่าวได้
ฉันจะข้ามการลบประเภทได้อย่างไร
เราได้เรียนรู้เกี่ยวกับการลบประเภทแล้ว มาลองโกงระบบกัน! :) งาน: เรามีคลาสTestClass<T>
ทั่วไป เราจำเป็นต้องสร้างวิธีการcreateNewT()
ที่จะสร้างและส่งคืนวัตถุประเภทТ
ใหม่ แต่นี่เป็นไปไม่ได้ใช่ไหม? ข้อมูลเกี่ยวกับประเภททั้งหมดТ
จะถูกลบในระหว่างการคอมไพล์ และในขณะที่โปรแกรมกำลังทำงานอยู่ เราจะไม่สามารถค้นหาได้ว่าเราต้องสร้างออบเจ็กต์ประเภทใด ในความเป็นจริงมีวิธีที่ยุ่งยากวิธีหนึ่ง คุณอาจจำได้ว่ามีคลาสในClass
Java เมื่อใช้มัน เราจะได้คลาสของอ็อบเจ็กต์ใดๆ ของเรา:
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 เป็นคลาสทั่วไป! เอกสารระบุว่า: “T คือประเภทของคลาสที่สร้างแบบจำลองโดยอ็อบเจ็กต์คลาสนี้” หากเราแปลสิ่งนี้จากภาษาเอกสารเป็นภาษามนุษย์ นั่นหมายความว่าคลาสสำหรับอ็อบเจ็กต์Integer.class
ไม่ได้เป็นเพียงClass
แต่Class<Integer>
เป็น ประเภทของวัตถุstring.class
ไม่ใช่แค่Class
, Class<String>
ฯลฯ หากยังไม่ชัดเจน ให้ลองเพิ่มพารามิเตอร์ type ให้กับตัวอย่างก่อนหน้า:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
//ошибка компиляции!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
//ошибка компиляции!
Class<Double> classString2 = String.class;
}
}
และตอนนี้ด้วยการใช้ความรู้นี้ เราสามารถหลีกเลี่ยงการลบประเภทและแก้ไขปัญหาของเราได้! ลองรับข้อมูลเกี่ยวกับประเภทพารามิเตอร์กัน ชั้นเรียนจะมีบทบาทMySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("Объект секретного класса успешно создан!");
}
}
ต่อไปนี้คือวิธีที่เราใช้โซลูชันของเราในทางปฏิบัติ:
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();
}
}
เอาต์พุตคอนโซล:
Объект секретного класса успешно создан!
เราเพียงแค่ส่งพารามิเตอร์คลาสที่ต้องการไปยังตัวสร้างคลาสทั่วไปของเรา:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
ด้วยเหตุนี้ เราจึงบันทึกข้อมูลเกี่ยวกับประเภทพารามิเตอร์และป้องกันไม่ให้ถูกลบ เป็นผลให้เราสามารถสร้างวัตถุได้T
! :) นี่เป็นการสรุปการบรรยายของวันนี้ การลบประเภทเป็นสิ่งที่ควรคำนึงถึงเสมอเมื่อทำงานกับข้อมูลทั่วไป สิ่งนี้ดูไม่สะดวกนัก แต่คุณต้องเข้าใจว่ายาชื่อสามัญไม่ได้เป็นส่วนหนึ่งของภาษา Java เมื่อถูกสร้างขึ้น นี่เป็นคุณสมบัติที่เพิ่มเข้ามาในภายหลังซึ่งช่วยเราสร้างคอลเลกชันที่พิมพ์และตรวจจับข้อผิดพลาดในขั้นตอนการคอมไพล์ ภาษาอื่นบางภาษาที่มีข้อมูลทั่วไปตั้งแต่เวอร์ชัน 1 ไม่มีการลบประเภท (เช่น C#) อย่างไรก็ตาม เรายังศึกษายาชื่อสามัญไม่จบ! ในการบรรยายครั้งต่อไป คุณจะได้ทำความคุ้นเคยกับคุณสมบัติต่างๆ มากมายในการทำงานกับสิ่งเหล่านี้ ในระหว่างนี้ คงจะดีไม่น้อยหากได้แก้ไขปัญหาสองสามข้อ! :)
GO TO FULL VERSION