John Squirrels
ระดับ
San Francisco

Generics ใน Java

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

Java Generic คืออะไร

Generics เป็นประเภทที่มีพารามิเตอร์ เมื่อสร้างประเภททั่วไป คุณไม่เพียงแต่ระบุประเภทเท่านั้น แต่ยังระบุประเภทข้อมูลที่จะใช้ด้วย ฉันเดาว่าตัวอย่างที่ชัดเจนที่สุดอยู่ในใจของคุณแล้ว: ArrayList! นี่คือวิธีที่เรามักจะสร้างขึ้นในโปรแกรม:

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<String> myList1 = new ArrayList<>();
       myList1.add("Test String 1");
       myList1.add("Test String 2");
   }
}
อย่างที่คุณอาจเดา คุณลักษณะของรายการนี้คือเราไม่สามารถยัดทุกอย่างลงในรายการได้: มันใช้งานได้เฉพาะกับอ็อบเจกต์สตริง ตอนนี้เรามาพูดนอกเรื่องเล็กน้อยเกี่ยวกับประวัติของ Java และพยายามตอบคำถามว่า "ทำไม" ในการทำเช่นนี้ เราจะเขียนคลาส ArrayList เวอร์ชันที่เรียบง่ายของเราเอง รายการของเรารู้วิธีเพิ่มข้อมูลและดึงข้อมูลจากอาร์เรย์ภายในเท่านั้น:

public class MyListClass {

   private Object[] data;
   private int count;

   public MyListClass() {
       this.data = new Object[10];
       this.count = 0;
   }

   public void add(Object o) {
       this.data[count] = o;
       count++;
   }

   public Object[] getData() {
       return data;
   }
}
สมมติว่าเราต้องการให้ list เก็บเฉพาะInteger s เราไม่ได้ใช้ประเภททั่วไป เราไม่ต้องการรวมการตรวจสอบ "instanceof Integer " ที่ชัดเจนในเมธอดadd() หากเราทำเช่นนั้น ทั้งคลาสของเราจะเหมาะสำหรับInteger เท่านั้น และเราจะต้องเขียนคลาสที่คล้ายกันสำหรับประเภทข้อมูลอื่น ๆ ในโลก! เราจะพึ่งพาโปรแกรมเมอร์ของเรา และเพียงแค่แสดงความคิดเห็นในโค้ดเพื่อให้แน่ใจว่าพวกเขาจะไม่เพิ่มสิ่งที่เราไม่ต้องการ:

// Use this class ONLY with the Integer data type
public void add(Object o) {
   this.data[count] = o;
   count++;
}
โปรแกรมเมอร์คนหนึ่งพลาดความคิดเห็นนี้และใส่สตริงหลายตัวในรายการตัวเลขโดยไม่ได้ตั้งใจแล้วคำนวณผลรวม:

public class Main {

   public static void main(String[] args) {

       MyListClass list = new MyListClass();
       list.add(100);
       list.add(200);
       list.add("Lolkek");
       list.add("Shalala");

       Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
       System.out.println(sum1);

       Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
       System.out.println(sum2);
   }
}
เอาต์พุตคอนโซล:

300 
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer 
      at Main.main (Main.java:14)
อะไรคือส่วนที่แย่ที่สุดของสถานการณ์นี้? ไม่ใช่ความประมาทเลินเล่อของโปรแกรมเมอร์อย่างแน่นอน ส่วนที่แย่ที่สุดคือโค้ดที่ไม่ถูกต้องไปอยู่ในสถานที่สำคัญในโปรแกรมของเราและคอมไพล์สำเร็จ ตอนนี้เราจะพบจุดบกพร่องไม่ใช่ขณะเขียนโค้ด แต่พบระหว่างการทดสอบเท่านั้น (และนี่คือสถานการณ์กรณีที่ดีที่สุด!) การแก้ไขจุดบกพร่องในขั้นตอนต่อๆ ไปของการพัฒนามีค่าใช้จ่ายสูงกว่ามาก ทั้งในแง่ของเงินและเวลา นี่คือสิ่งที่ generics เป็นประโยชน์ต่อเรา: คลาส generic ช่วยให้โปรแกรมเมอร์ผู้โชคร้ายตรวจพบข้อผิดพลาดได้ทันที โปรแกรมจะไม่รวบรวม!

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Integer> myList1 = new ArrayList<>();
      
       myList1.add(100);
       myList1.add(100);
       myList1.add ("Lolkek"); // Error!
       myList1.add("Shalala"); // Error!
   }
}
โปรแกรมเมอร์รู้ทันความผิดพลาดของตนเองและเก่งขึ้นทันที อย่างไรก็ตาม เราไม่ต้องสร้าง คลาส List เอง เพื่อดูข้อผิดพลาดประเภทนี้ เพียงลบวงเล็บมุมแล้วพิมพ์ ( <Integer> ) จาก ArrayList ธรรมดา!

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

      List list = new ArrayList();

      list.add(100);
      list.add(200);
      list.add("Lolkek");
      list.add("Shalala");

       System.out.println((Integer) list.get(0) + (Integer) list.get(1));
       System.out.println((Integer) list.get(2) + (Integer) list.get(3));
   }
}
เอาต์พุตคอนโซล:

300 
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer 
     at Main.main(Main.java:16)
กล่าวอีกนัยหนึ่ง แม้จะใช้กลไก "แบบเนทีฟ" ของ Java เราก็สามารถทำผิดพลาดและสร้างคอลเล็กชันที่ไม่ปลอดภัยได้ อย่างไรก็ตาม หากเราวางโค้ดนี้ลงใน IDE เราจะได้รับคำเตือน: "ไม่ได้เลือกการเรียกเพื่อเพิ่ม (E) ในฐานะสมาชิกของประเภท raw ของ java.util.List" เราได้รับแจ้งว่ามีบางอย่างผิดพลาดเมื่อเพิ่มรายการ ไปยังคอลเลกชันที่ไม่มีประเภททั่วไป แต่คำว่า "ประเภทดิบ" หมายถึงอะไร? ประเภทดิบคือคลาสทั่วไปที่ถูกลบประเภทแล้ว กล่าวอีกนัยหนึ่งList myList1เป็นประเภทข้อมูลดิบ สิ่งที่ตรงกันข้ามกับประเภท rawคือประเภททั่วไป — คลาสทั่วไปที่มีการระบุประเภทพารามิเตอร์ ตัวอย่างเช่นList<String> myList1. คุณอาจถามว่าทำไมภาษาจึงอนุญาตให้ใช้ประเภท raw ? เหตุผลนั้นง่าย ผู้สร้างของ Java ออกจากการสนับสนุนประเภท rawในภาษาเพื่อหลีกเลี่ยงการสร้างปัญหาความเข้ากันได้ เมื่อถึงเวลาที่ Java 5.0 เปิดตัว (ชื่อสามัญปรากฏตัวครั้งแรกในเวอร์ชันนี้) มีการเขียนโค้ดจำนวนมากโดยใช้ประเภท rawแล้ว เป็นผลให้กลไกนี้ยังคงรองรับมาจนถึงทุกวันนี้ เราได้กล่าวถึงหนังสือคลาสสิกของ Joshua Bloch เรื่อง "Effective Java" ซ้ำแล้วซ้ำเล่าในบทเรียน ในฐานะหนึ่งในผู้สร้างภาษา เขาไม่ได้ข้ามประเภทดิบและประเภททั่วไปในหนังสือของเขา generics ใน Java คืออะไร?  - 2บทที่ 23 ของหนังสือมีชื่อที่คมคายมาก: "อย่าใช้ประเภทดิบในรหัสใหม่" นี่คือสิ่งที่คุณต้องจำ เมื่อใช้คลาสทั่วไป ห้ามเปลี่ยนประเภท ทั่วไปเป็นประเภทดิบ

วิธีการทั่วไป

Java ให้คุณกำหนดพารามิเตอร์แต่ละเมธอดโดยสร้างสิ่งที่เรียกว่าเมธอดทั่วไป วิธีการดังกล่าวมีประโยชน์อย่างไร? เหนือสิ่งอื่นใด พวกมันมีประโยชน์ตรงที่ช่วยให้คุณทำงานกับพารามิเตอร์เมธอดประเภทต่างๆ หากตรรกะเดียวกันสามารถนำไปใช้กับประเภทต่างๆ ได้อย่างปลอดภัย วิธีทั่วไปอาจเป็นทางออกที่ดี ลองพิจารณาตัวอย่างง่ายๆ นี้: สมมติว่าเรามีรายการชื่อmyList1 เราต้องการลบค่าทั้งหมดออกจากรายการและเติมช่องว่างทั้งหมดด้วยค่าใหม่ นี่คือลักษณะของชั้นเรียนของเราด้วยวิธีการทั่วไป:

public class TestClass {

   public static <T> void fill(List<T> list, T val) {
       for (int i = 0; i < list.size(); i++)
           list.set(i, val);
   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       strings.add("Old String 1");
       strings.add("Old String 2");
       strings.add("Old String 3");

       fill(strings, "New String");

       System.out.println(strings);

       List<Integer> numbers = new ArrayList<>();
       numbers.add(1);
       numbers.add(2);
       numbers.add(3);

       fill(numbers, 888);
       System.out.println(numbers);
   }
}
ให้ความสนใจกับไวยากรณ์ มันดูผิดปกติเล็กน้อย:

public static <T> void fill(List<T> list, T val)
เราเขียน <T> ก่อนประเภทการส่งคืน สิ่งนี้บ่งชี้ว่าเรากำลังจัดการกับวิธีการทั่วไป ในกรณีนี้ เมธอดจะรับพารามิเตอร์ 2 ตัวเป็นอินพุต: รายการของอ็อบเจกต์ T และอ็อบเจกต์ T ที่แยกจากกัน เมื่อใช้ <T> เรากำหนดพารามิเตอร์ของประเภทพารามิเตอร์ของเมธอด: เราไม่สามารถผ่านรายการสตริงและจำนวนเต็มได้ รายการของสตริงและสตริง รายการของจำนวนเต็มและจำนวนเต็ม รายการของ วัตถุ Cat ของเราเอง และ วัตถุ Cat อื่น นั่นคือสิ่งที่เราต้องทำ เมธอดmain()แสดงให้เห็นว่า เมธอด fill()สามารถใช้ทำงานกับข้อมูลประเภทต่างๆ ได้ อย่างไร ขั้นแรก เราใช้วิธีที่มีรายการสตริงและสตริงเป็นอินพุต จากนั้นตามด้วยรายการจำนวนเต็มและจำนวนเต็ม เอาต์พุตคอนโซล:

[New String, New String, New String] [888, 888, 888]
ลองนึกภาพถ้าเราไม่มีเมธอดทั่วไปและต้องการตรรกะของ เมธอด fill()สำหรับ 30 คลาสที่แตกต่างกัน เราจะต้องเขียนวิธีเดียวกัน 30 ครั้งสำหรับประเภทข้อมูลที่แตกต่างกัน! แต่ด้วยวิธีการทั่วไป เราสามารถนำรหัสของเรากลับมาใช้ใหม่ได้! :)

ชั้นเรียนทั่วไป

คุณไม่จำกัดเฉพาะคลาสทั่วไปที่มีให้ในไลบรารี Java มาตรฐาน — คุณสามารถสร้างคลาสของคุณเองได้! นี่คือตัวอย่างง่ายๆ:

public class Box<T> {

   private T t;

   public void set(T t) {
       this.t = t;
   }

   public T get() {
       return t;
   }

   public static void main(String[] args) {

       Box<String> stringBox = new Box<>();

       stringBox.set("Old String");
       System.out.println(stringBox.get());
       stringBox.set("New String");

       System.out.println(stringBox.get());
      
       stringBox.set(12345); // Compilation error!
   }
}
คลาส Box<T> ของเราเป็นคลาสทั่วไป เมื่อเรากำหนดประเภทข้อมูล ( <T> ) ระหว่างการสร้าง เราจะไม่สามารถวางวัตถุประเภทอื่นในนั้นได้อีก สามารถดูได้จากตัวอย่าง เมื่อสร้างออบเจกต์ของเรา เราระบุว่ามันจะทำงานกับสตริงได้:

Box<String> stringBox = new Box<>();
และในโค้ดบรรทัดสุดท้าย เมื่อเราพยายามใส่หมายเลข 12345 ในช่อง เราได้รับข้อผิดพลาดในการคอมไพล์! มันง่ายมาก! เราได้สร้างคลาสทั่วไปของเราเองแล้ว! :) ด้วยเหตุนี้ บทเรียนของวันนี้ก็จบลง แต่เราไม่ได้บอกลายาชื่อสามัญ! ในบทเรียนต่อไป เราจะพูดถึงคุณลักษณะขั้นสูงเพิ่มเติม อย่าเพิ่งหายไปไหน! ) เพื่อเสริมสิ่งที่คุณได้เรียนรู้ เราขอแนะนำให้คุณดูบทเรียนวิดีโอจากหลักสูตร Java ของเรา
ขอให้ประสบความสำเร็จในการเรียน! :)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION