John Squirrels
ระดับ
San Francisco

พิมพ์ลบ

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

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 เป็นคลาสทั่วไป! ประเภทการลบ - 3

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

เอกสารระบุว่า "T - ประเภทของคลาสที่จำลองโดยวัตถุคลาสนี้" การแปลสิ่งนี้จากภาษาของเอกสารเป็นคำพูดธรรมดา เราเข้าใจดีว่าคลาสของ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#) อย่างไรก็ตาม เรายังศึกษายาชื่อสามัญไม่จบสิ้น! ในบทเรียนถัดไป คุณจะได้ทำความคุ้นเคยกับคุณสมบัติเพิ่มเติมของยาชื่อสามัญ สำหรับตอนนี้ จะเป็นการดีที่จะแก้ปัญหาสองอย่าง! :)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION