CodeGym /จาวาบล็อก /สุ่ม /Java Generics: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ
John Squirrels
ระดับ
San Francisco

Java Generics: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ

เผยแพร่ในกลุ่ม

การแนะนำ

เริ่มต้นด้วย JSE 5.0, generics ถูกเพิ่มเข้าไปในคลังแสงของภาษา Java

generics ใน java คืออะไร?

Generics เป็นกลไกพิเศษของ Java สำหรับการนำการเขียนโปรแกรมทั่วไปไปใช้ ซึ่งเป็นวิธีการอธิบายข้อมูลและอัลกอริทึมที่ช่วยให้คุณทำงานกับประเภทข้อมูลต่างๆ ได้โดยไม่ต้องเปลี่ยนคำอธิบายของอัลกอริทึม เว็บไซต์ Oracle มีบทช่วยสอนแยกต่างหากสำหรับข้อมูลทั่วไป: " บทเรียน " เพื่อให้เข้าใจยาชื่อสามัญ ก่อนอื่นคุณต้องหาสาเหตุว่าเหตุใดจึงจำเป็นและให้อะไร ส่วน " ทำไมต้องใช้ Generics " ของบทช่วยสอนบอกว่ามีจุดประสงค์สองประการคือการตรวจสอบประเภทที่แข็งแกร่งกว่าในเวลาคอมไพล์ และขจัดความจำเป็นในการร่ายอย่างชัดเจน Generics in Java: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ - 1มาเตรียมตัวสำหรับการทดสอบในคอมไพเลอร์ Java ออนไลน์ ของ Tutorialspoint อันเป็นที่รักของเรากันเถอะ สมมติว่าคุณมีรหัสต่อไปนี้:

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 ของคลาสที่คอมไพล์: คลาสที่ใช้การโยนอย่างชัดเจนและคลาสที่ใช้ชื่อสามัญ: Generics in Java: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ - 2หลังจากการคอมไพล์แล้ว ข้อมูลทั่วไปทั้งหมดจะถูกลบ สิ่งนี้เรียกว่า " การลบแบบ "" การลบประเภทและชื่อสามัญได้รับการออกแบบให้เข้ากันได้กับ JDK เวอร์ชันเก่าในขณะเดียวกันก็อนุญาตให้คอมไพเลอร์ช่วยกำหนดประเภทใน Java เวอร์ชันใหม่ได้พร้อมกัน

ประเภทดิบ

เมื่อพูดถึงยาสามัญ เรามีสองประเภทเสมอ: ประเภทพารามิเตอร์และประเภทดิบ ประเภท Raw คือประเภทที่ละเว้น "การชี้แจงประเภท" ในวงเล็บมุม: Generics in Java: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ - 3ในมือ ประเภทพารามิเตอร์มี "ความชัดเจน": Generics in Java: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ - 4อย่างที่คุณเห็น เราใช้โครงสร้างที่ผิดปกติ ซึ่งทำเครื่องหมายด้วยลูกศรในภาพหน้าจอ นี่คือไวยากรณ์พิเศษที่เพิ่มเข้ามาใน Java SE 7 เรียกว่า " เพชร " ทำไม วงเล็บเหลี่ยมเป็นรูปสี่เหลี่ยมข้าวหลามตัด: < > นอกจากนี้ คุณควรทราบด้วยว่าไวยากรณ์ของไดมอนด์เกี่ยวข้องกับแนวคิดของ " การอนุมานประเภท " ท้ายที่สุดคอมไพเลอร์เห็น<>ทางด้านขวา ดูที่ด้านซ้ายของตัวดำเนินการกำหนด ซึ่งจะพบประเภทของตัวแปรที่มีการกำหนดค่า จากสิ่งที่พบในส่วนนี้ ระบบจะเข้าใจประเภทของค่าทางด้านขวา ในความเป็นจริง หากระบุประเภททั่วไปไว้ทางด้านซ้าย แต่ไม่ให้ทางด้านขวา คอมไพลเลอร์สามารถอนุมานประเภทได้:

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 ยังรองรับไวด์การ์ด พวกเขาแบ่งออกเป็นสามประเภท: การใช้สัญลักษณ์แทนของคุณควรเป็นไปตามหลักการGet-Put สามารถแสดงได้ดังนี้:
  • ใช้ สัญลักษณ์ แทนการขยายเมื่อคุณได้รับค่าจากโครงสร้างเท่านั้น
  • ใช้super wildcard เมื่อคุณใส่ค่าลงในโครงสร้างเท่านั้น
  • และอย่าใช้สัญลักษณ์แทนเมื่อคุณทั้งคู่ต้องการรับและวางจาก/ไปยังโครงสร้าง
หลักการนี้เรียกว่าหลักการ Producer Extends Consumer Super (PECS) นี่คือตัวอย่างเล็กๆ จากซอร์สโค้ดสำหรับเมธอด Collections.copyGenerics in Java: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ - 5 ของ Java และนี่คือตัวอย่างเล็กๆ น้อยๆ ของสิ่งที่ใช้ไม่ได้:

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>();
เนื่องจากการสืบทอดทำงานแตกต่างกับยาสามัญ: Generics in Java: วิธีใช้วงเล็บเหลี่ยมในทางปฏิบัติ - 6และนี่คืออีกตัวอย่างที่ดีที่จะล้มเหลวโดยมีข้อผิดพลาด:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
อีกครั้งทุกอย่างง่ายที่นี่ List<String>ไม่ใช่ลูกหลานของList<Object>แม้ว่าStringจะ เป็นลูกหลานของObject เพื่อเสริมสิ่งที่คุณได้เรียนรู้ เราขอแนะนำให้คุณดูบทเรียนวิดีโอจากหลักสูตร Java ของเรา

บทสรุป

ดังนั้นเราจึงรีเฟรชความทรงจำเกี่ยวกับยาชื่อสามัญ หากคุณไม่ค่อยใช้ประโยชน์จากความสามารถอย่างเต็มที่ รายละเอียดบางอย่างจะคลุมเครือ ฉันหวังว่าบทวิจารณ์สั้น ๆ นี้จะช่วยกระตุกความจำของคุณ เพื่อให้ได้ผลลัพธ์ที่ดียิ่งขึ้น เราขอแนะนำให้คุณทำความคุ้นเคยกับเนื้อหาต่อไปนี้:
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION