การแนะนำ
เริ่มต้นด้วย JSE 5.0, generics ถูกเพิ่มเข้าไปในคลังแสงของภาษา Javagenerics ใน java คืออะไร?
Generics เป็นกลไกพิเศษของ Java สำหรับการนำการเขียนโปรแกรมทั่วไปไปใช้ ซึ่งเป็นวิธีการอธิบายข้อมูลและอัลกอริทึมที่ช่วยให้คุณทำงานกับประเภทข้อมูลต่างๆ ได้โดยไม่ต้องเปลี่ยนคำอธิบายของอัลกอริทึม เว็บไซต์ Oracle มีบทช่วยสอนแยกต่างหากสำหรับข้อมูลทั่วไป: " บทเรียน " เพื่อให้เข้าใจยาชื่อสามัญ ก่อนอื่นคุณต้องหาสาเหตุว่าเหตุใดจึงจำเป็นและให้อะไร ส่วน " ทำไมต้องใช้ Generics " ของบทช่วยสอนบอกว่ามีจุดประสงค์สองประการคือการตรวจสอบประเภทที่แข็งแกร่งกว่าในเวลาคอมไพล์ และขจัดความจำเป็นในการร่ายอย่างชัดเจน
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
รหัสนี้จะทำงานได้ดีอย่างสมบูรณ์ แต่ถ้าเจ้านายมาหาเราและพูดว่า "สวัสดีชาวโลก!" เป็นวลีที่ใช้มากเกินไปและคุณต้องส่งคืนเฉพาะคำว่า "สวัสดี" หรือไม่ เราจะลบโค้ดที่เชื่อมระหว่าง", world!" ดูเหมือนว่าจะไม่เป็นอันตรายเพียงพอใช่ไหม แต่เราได้รับข้อผิดพลาดในเวลาคอมไพล์:
error: incompatible types: Object cannot be converted to String
ปัญหาคือในรายการของเราเก็บวัตถุ สตริงเป็นลูกหลานของObject (เนื่องจากคลาส Java ทั้งหมดสืบทอดโดยปริยายObject ) ซึ่งหมายความว่าเราต้องการนักแสดงที่ชัดเจน แต่เราไม่ได้เพิ่ม ระหว่างการดำเนินการต่อข้อมูล เมธอดString.valueOf(obj)แบบคงที่จะถูกเรียกโดยใช้วัตถุ ในที่สุด มันจะเรียก เมธอด toStringของคลาสอ็อบเจกต์ กล่าวอีกนัยหนึ่งList ของ เรามีObject ซึ่งหมายความว่าทุกที่ที่เราต้องการประเภทเฉพาะ (ไม่ใช่Object ) เราจะต้องทำการแปลงประเภทด้วยตนเอง:
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println("-" + (String)str);
}
}
}
อย่างไรก็ตาม ในกรณีนี้ เนื่องจากListรับวัตถุ จึงสามารถจัดเก็บได้ไม่เพียงแค่String s แต่ยังรวมถึงInteger s แต่สิ่งที่เลวร้ายที่สุดคือคอมไพเลอร์ไม่เห็นสิ่งผิดปกติที่นี่ และตอนนี้เราจะได้รับข้อผิดพลาด AT RUN TIME (เรียกว่า "ข้อผิดพลาดรันไทม์") ข้อผิดพลาดจะเป็น:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
คุณต้องยอมรับว่าสิ่งนี้ไม่ดีนัก และทั้งหมดนี้เป็นเพราะคอมไพเลอร์ไม่ใช่ปัญญาประดิษฐ์ที่สามารถเดาเจตนาของโปรแกรมเมอร์ได้อย่างถูกต้องเสมอไป Java SE 5 แนะนำชื่อสามัญเพื่อให้เราบอกคอมไพเลอร์เกี่ยวกับความตั้งใจของเรา — เกี่ยวกับประเภทที่เราจะใช้ เราแก้ไขรหัสของเราโดยบอกคอมไพเลอร์ว่าเราต้องการอะไร:
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println("-" + str);
}
}
}
อย่างที่คุณเห็น เราไม่จำเป็นต้อง cast ไปที่String อีก ต่อไป นอกจากนี้ เรายังมีวงเล็บมุมล้อมรอบอาร์กิวเมนต์ประเภท ตอนนี้คอมไพเลอร์จะไม่อนุญาตให้เราคอม ไพล์คลาสจนกว่าเราจะลบบรรทัดที่เพิ่ม 123 ในรายการ เนื่องจากเป็นจำนวนเต็ม และจะบอกเราเช่นนั้น หลายคนเรียกยาชื่อสามัญว่า "น้ำตาลสังเคราะห์" และมันก็ถูกต้อง เนื่องจากหลังจากคอมไพล์ยาสามัญแล้ว พวกมันจะกลายเป็นการแปลงประเภทเดียวกันจริงๆ ลองดูที่ bytecode ของคลาสที่คอมไพล์: คลาสที่ใช้การโยนอย่างชัดเจนและคลาสที่ใช้ชื่อสามัญ: 
ประเภทดิบ
เมื่อพูดถึงยาสามัญ เรามีสองประเภทเสมอ: ประเภทพารามิเตอร์และประเภทดิบ ประเภท Raw คือประเภทที่ละเว้น "การชี้แจงประเภท" ในวงเล็บมุม:

import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello, World");
String data = list.get(0);
System.out.println(data);
}
}
แต่นี่เป็นการผสมผสานสไตล์ใหม่เข้ากับรูปแบบทั่วไปและรูปแบบเก่าที่ไม่มีสิ่งเหล่านี้ และนี่เป็นสิ่งที่ไม่พึงปรารถนาอย่างยิ่ง เมื่อคอมไพล์โค้ดด้านบน เราได้รับข้อความต่อไปนี้:
Note: HelloWorld.java uses unchecked or unsafe operations
ในความเป็นจริง เหตุผลที่คุณต้องเพิ่มเพชรที่นี่ดูเหมือนจะไม่สามารถเข้าใจได้ แต่นี่คือตัวอย่าง:
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
คุณจะจำได้ว่าArrayListมีตัวสร้างที่สองที่ใช้คอลเลกชันเป็นอาร์กิวเมนต์ และนี่คือสิ่งที่น่ากลัวซ่อนอยู่ หากไม่มีไวยากรณ์เพชร คอมไพเลอร์ไม่เข้าใจว่ากำลังถูกหลอก ด้วยไวยากรณ์ของไดมอนด์ ดังนั้น กฎข้อที่ 1 คือ: ใช้ไวยากรณ์ไดมอนด์กับประเภทพารามิเตอร์เสมอ มิฉะนั้น เราเสี่ยงที่จะพลาดตำแหน่งที่เราใช้ประเภทข้อมูลดิบ เพื่อกำจัดคำเตือน "ใช้การดำเนินการที่ไม่ตรวจสอบหรือไม่ปลอดภัย" เราสามารถใช้ คำอธิบายประกอบ @SuppressWarnings("unchecked")ในเมธอดหรือคลาส แต่ลองคิดดูว่าทำไมคุณถึงตัดสินใจใช้มัน จำกฎข้อที่หนึ่ง บางทีคุณอาจต้องเพิ่มอาร์กิวเมนต์ประเภท
วิธีการทั่วไปของ Java
Generics ช่วยให้คุณสร้างเมธอดที่มีพารามิเตอร์ประเภทพารามิเตอร์และประเภทผลตอบแทน ส่วนที่แยกออกมาสำหรับความสามารถนี้ในบทช่วยสอนของ Oracle: " Generic Methods " สิ่งสำคัญคือต้องจำไวยากรณ์ที่สอนในบทช่วยสอนนี้:- ประกอบด้วยรายการพารามิเตอร์ประเภทภายในวงเล็บมุม
- รายการพารามิเตอร์ประเภทจะอยู่ก่อนประเภทการส่งคืนของเมธอด
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
ถ้าคุณดู คลาส Utilคุณจะเห็นว่ามีเมธอดทั่วไปสองเมธอด ด้วยความเป็นไปได้ของการอนุมานประเภท เราจึงสามารถระบุประเภทโดยตรงกับคอมไพเลอร์ หรือเราจะระบุประเภทเองก็ได้ ตัวเลือกทั้งสองแสดงในตัวอย่าง ยังไงก็ตาม ไวยากรณ์นั้นสมเหตุสมผลมากหากคุณคิดเกี่ยวกับมัน เมื่อประกาศเมธอดทั่วไป เราระบุพารามิเตอร์ประเภทก่อนเมธอด เพราะหากเราประกาศพารามิเตอร์ประเภทหลังเมธอด JVM จะไม่สามารถระบุประเภทที่จะใช้ได้ ดังนั้น ก่อนอื่นเราประกาศว่าเราจะใช้ พารามิเตอร์ประเภท Tจากนั้นเราบอกว่าเราจะส่งคืนประเภทนี้ โดยปกติUtil.<Integer>getValue(element, String.class)จะล้มเหลวโดยมีข้อผิดพลาด:ประเภทที่เข้ากันไม่ได้: ไม่สามารถแปลง Class<String> เป็น Class<Integer>ได้ เมื่อใช้วิธีการทั่วไป คุณควรจำการลบประเภทไว้เสมอ ลองดูตัวอย่าง:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
สิ่งนี้จะทำงานได้ดี แต่ตราบใดที่คอมไพเลอร์เข้าใจว่าประเภทการส่งคืนของเมธอดที่ถูกเรียกคือInteger แทนที่คำสั่งเอาต์พุตของคอนโซลด้วยบรรทัดต่อไปนี้:
System.out.println(Util.getValue(element) + 1);
เราได้รับข้อผิดพลาด:
bad operand types for binary operator '+', first type: Object, second type: int.
กล่าวอีกนัยหนึ่ง เกิดการลบประเภท คอมไพลเลอร์เห็นว่าไม่มีใครระบุประเภท ดังนั้นประเภทจึงถูกระบุเป็นObjectและเมธอดล้มเหลวโดยมีข้อผิดพลาด
ชั้นเรียนทั่วไป
ไม่เพียงแต่เมธอดเท่านั้นที่สามารถกำหนดพารามิเตอร์ได้ ชั้นเรียนได้เช่นกัน ส่วน"ประเภททั่วไป"ของบทช่วยสอนของ Oracle มีไว้สำหรับสิ่งนี้ ลองพิจารณาตัวอย่าง:
public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
ทุกอย่างเรียบง่ายที่นี่ หากเราใช้คลาสทั่วไป พารามิเตอร์ type จะถูกระบุหลังชื่อคลาส ตอนนี้มาสร้างอินสแตนซ์ของคลาสนี้ใน เมธอด หลัก :
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
รหัสนี้จะทำงานได้ดี คอมไพเลอ ร์เห็นว่ามีรายการตัวเลขและชุดของสตริง แต่ถ้าเรากำจัดพารามิเตอร์ประเภทและทำสิ่งนี้:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
เราได้รับข้อผิดพลาด:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
นี่เป็นการลบประเภท เนื่องจากคลาสไม่ใช้พารามิเตอร์ประเภทอีกต่อไป คอมไพลเลอร์จึงตัดสินใจว่า เนื่องจากเราผ่าน List แล้ววิธีการที่มีList<Integer>จึงเหมาะสมที่สุด และเราล้มเหลวด้วยข้อผิดพลาด ดังนั้นเราจึงมีกฎ #2: หากคุณมีคลาสทั่วไป ให้ระบุพารามิเตอร์ประเภทเสมอ
ข้อ จำกัด
เราสามารถจำกัดประเภทที่ระบุในเมธอดทั่วไปและคลาส ตัวอย่างเช่น สมมติว่าเราต้องการให้คอนเทนเนอร์ยอมรับอาร์กิวเมนต์ประเภทตัวเลข เท่านั้น คุณลักษณะนี้อธิบายไว้ใน ส่วน Bounded Type Parametersของบทช่วยสอนของ Oracle ลองดูตัวอย่าง:
import java.util.*;
public class HelloWorld {
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
อย่างที่คุณเห็น เราได้จำกัดพารามิเตอร์ type ไว้ที่ คลาส Number /interface หรือระดับล่างสุด โปรดทราบว่าคุณสามารถระบุได้ไม่เพียงแค่คลาส แต่ยังรวมถึงอินเทอร์เฟซด้วย ตัวอย่างเช่น:
public static class NumberContainer<T extends Number & Comparable> {
Generics ยังรองรับไวด์การ์ด พวกเขาแบ่งออกเป็นสามประเภท:
- สัญลักษณ์แทนขอบเขตบน — < ? ขยายหมายเลข >
- สัญลักษณ์แทนไม่จำกัด — < ? >
- สัญลักษณ์แทนขอบเขตล่าง — < ? จำนวนเต็มซุปเปอร์ >
- ใช้ สัญลักษณ์ แทนการขยายเมื่อคุณได้รับค่าจากโครงสร้างเท่านั้น
- ใช้super wildcard เมื่อคุณใส่ค่าลงในโครงสร้างเท่านั้น
- และอย่าใช้สัญลักษณ์แทนเมื่อคุณทั้งคู่ต้องการรับและวางจาก/ไปยังโครงสร้าง

public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello, World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
แต่ถ้าคุณแทนที่การยืดขยายด้วยsuperทุกอย่างก็เรียบร้อยดี เนื่องจากเราเติมรายการด้วยค่าก่อนแสดงเนื้อหา รายการจึงเป็นผู้บริโภค ดังนั้นเราจึงใช้ super
มรดก
Generics มีคุณสมบัติที่น่าสนใจอีกอย่างหนึ่ง: การสืบทอด วิธีการทำงานของการสืบทอดสำหรับข้อมูลทั่วไปอธิบายไว้ใน " Generics, Inheritance และ Subtypes " ในบทช่วยสอนของ Oracle สิ่งสำคัญคือการจดจำและรับรู้สิ่งต่อไปนี้ เราไม่สามารถทำสิ่งนี้ได้:
List<CharSequence> list1 = new ArrayList<String>();
เนื่องจากการสืบทอดทำงานแตกต่างกับยาสามัญ: 
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
อีกครั้งทุกอย่างง่ายที่นี่ List<String>ไม่ใช่ลูกหลานของList<Object>แม้ว่าStringจะ เป็นลูกหลานของObject เพื่อเสริมสิ่งที่คุณได้เรียนรู้ เราขอแนะนำให้คุณดูบทเรียนวิดีโอจากหลักสูตร Java ของเรา
บทสรุป
ดังนั้นเราจึงรีเฟรชความทรงจำเกี่ยวกับยาชื่อสามัญ หากคุณไม่ค่อยใช้ประโยชน์จากความสามารถอย่างเต็มที่ รายละเอียดบางอย่างจะคลุมเครือ ฉันหวังว่าบทวิจารณ์สั้น ๆ นี้จะช่วยกระตุกความจำของคุณ เพื่อให้ได้ผลลัพธ์ที่ดียิ่งขึ้น เราขอแนะนำให้คุณทำความคุ้นเคยกับเนื้อหาต่อไปนี้:
อ่านเพิ่มเติม: |
---|
GO TO FULL VERSION