JavaRush /จาวาบล็อก /Random-TH /ทฤษฎียาชื่อสามัญในภาษา Java หรือวิธีใส่วงเล็บในทางปฏิบัติ...
Viacheslav
ระดับ

ทฤษฎียาชื่อสามัญในภาษา Java หรือวิธีใส่วงเล็บในทางปฏิบัติ

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

การแนะนำ

เริ่มต้นด้วย JSE 5.0 ข้อมูลทั่วไปถูกเพิ่มเข้าไปในคลังแสงภาษา Java
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 1

ยาชื่อสามัญใน Java คืออะไร?

Generics (ทั่วไป) เป็นวิธีพิเศษของภาษา Java สำหรับการนำการเขียนโปรแกรมทั่วไปไปใช้: วิธีพิเศษในการอธิบายข้อมูลและอัลกอริธึมที่ช่วยให้คุณทำงานกับข้อมูลประเภทต่างๆ ได้โดยไม่ต้องเปลี่ยนคำอธิบาย บนเว็บไซต์ Oracle มีบทช่วยสอนแยกต่างหากเกี่ยวกับข้อมูลทั่วไป: “ บทเรียน: ข้อมูลทั่วไป

ขั้นแรก เพื่อทำความเข้าใจเกี่ยวกับยาชื่อสามัญ คุณต้องเข้าใจว่าเหตุใดจึงจำเป็นต้องใช้ยาเหล่านี้และสิ่งที่พวกเขาให้ไว้ ในบทช่วยสอนในส่วน " เหตุใดจึงต้องใช้ Generics " กล่าวกันว่าจุดประสงค์ประการหนึ่งคือการตรวจสอบประเภทเวลาคอมไพล์ที่แข็งแกร่งยิ่งขึ้น และขจัดความจำเป็นในการแคสต์อย่างชัดเจน
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 2
มาเตรียม คอมไพเลอร์ 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 ในกรณีของเรา List จะเก็บรายการของอ็อบเจ็กต์ประเภท Object เนื่องจาก String เป็นลูกหลานของ Object (เนื่องจากคลาสทั้งหมดได้รับการสืบทอดโดยปริยายจาก Object ใน Java) จึงจำเป็นต้องมีการคัดเลือกนักแสดงที่ชัดเจน ซึ่งเราไม่ได้ทำ และเมื่อทำการต่อเข้าด้วยกัน เมธอดคงที่ String.valueOf(obj) จะถูกเรียกบนอ็อบเจ็กต์ ซึ่งในที่สุดจะเรียกเมธอด toString บนอ็อบเจ็กต์ นั่นคือรายการของเรามีวัตถุ ปรากฎว่าในกรณีที่เราต้องการประเภทเฉพาะ ไม่ใช่ 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 เท่านั้น แต่ยังเก็บ Integer อีกด้วย แต่สิ่งที่แย่ที่สุดคือในกรณีนี้คอมไพเลอร์จะไม่เห็นสิ่งผิดปกติ และที่นี่เราจะได้รับข้อผิดพลาดระหว่างการดำเนินการ (พวกเขายังบอกด้วยว่าได้รับข้อผิดพลาด "ที่รันไทม์") ข้อผิดพลาดจะเป็น: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String เห็นด้วย ไม่น่าพอใจที่สุด และทั้งหมดนี้เป็นเพราะคอมไพเลอร์ไม่ใช่ปัญญาประดิษฐ์และไม่สามารถเดาทุกสิ่งที่โปรแกรมเมอร์หมายถึงได้ เพื่อบอกคอมไพเลอร์เพิ่มเติมเกี่ยวกับประเภทที่เราจะใช้ Java SE 5 ได้แนะนำgenerics มาแก้ไขเวอร์ชันของเราโดยบอกคอมไพเลอร์ว่าเราต้องการอะไร:
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);
		}
	}
}
ดังที่เราเห็นแล้ว เราไม่ต้องการการร่ายแบบ String อีกต่อไป นอกจากนี้ ขณะนี้เรายังมีวงเล็บมุมที่ใช้ใส่ข้อมูลทั่วไป ตอนนี้คอมไพเลอร์จะไม่อนุญาตให้คอมไพล์คลาสจนกว่าเราจะลบการบวก 123 ออกจากรายการ เนื่องจาก นี่คือจำนวนเต็ม เขาจะบอกเราอย่างนั้น หลายคนเรียกยาชื่อสามัญว่า "น้ำตาลวากยสัมพันธ์" และพวกมันก็พูดถูก เนื่องจากยาชื่อสามัญจะกลายเป็นวรรณะเดียวกันเมื่อรวบรวม มาดูไบต์โค้ดของคลาสที่คอมไพล์แล้ว: ด้วยการแคสต์แบบแมนนวลและการใช้ข้อมูลทั่วไป:
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 3
หลังจากการรวบรวม ข้อมูลใด ๆ เกี่ยวกับยาชื่อสามัญจะถูกลบ สิ่งนี้เรียกว่า "การลบประเภท" หรือ " การลบประเภท " การลบประเภทและข้อมูลทั่วไปได้รับการออกแบบเพื่อให้มีความเข้ากันได้แบบย้อนหลังกับ JDK เวอร์ชันเก่า ในขณะที่ยังคงอนุญาตให้คอมไพลเลอร์ช่วยในการอนุมานประเภทใน Java เวอร์ชันใหม่กว่า
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 4

ประเภทดิบหรือประเภทดิบ

เมื่อพูดถึงยาชื่อสามัญ เราจะมีสองหมวดหมู่เสมอ: ประเภทประเภท (ประเภททั่วไป) และประเภท “ดิบ” (ประเภทดิบ) ประเภท Rawคือประเภทที่ไม่ได้ระบุ "ตัวระบุ" ในวงเล็บมุม:
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 5
ประเภทที่พิมพ์จะตรงกันข้าม โดยมีการระบุ "คำชี้แจง":
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 6
ดังที่เราเห็น เราใช้การออกแบบที่แปลกตา โดยมีลูกศรกำกับอยู่ในภาพหน้าจอ นี่เป็นไวยากรณ์พิเศษที่ถูกเพิ่มเข้ามาใน 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 ยังมี Constructor ตัวที่สองที่รับคอลเลกชันเป็นอินพุต และนี่คือที่ที่การหลอกลวงอยู่ หากไม่มีไวยากรณ์ของเพชร คอมไพเลอร์จะไม่เข้าใจว่ามันถูกหลอก แต่สำหรับเพชรแล้ว ดังนั้นกฎ #1 : ใช้ไวยากรณ์เพชรเสมอหากเราใช้ประเภทที่พิมพ์ มิฉะนั้นเราเสี่ยงที่จะพลาดตรงที่เราใช้แบบดิบ เพื่อหลีกเลี่ยงคำเตือนในบันทึกที่ “ใช้การดำเนินการที่ไม่ได้ตรวจสอบหรือไม่ปลอดภัย” คุณสามารถระบุคำอธิบายประกอบพิเศษเกี่ยวกับวิธีการหรือคลาสที่กำลังใช้อยู่ได้: @SuppressWarnings("unchecked") การระงับถูกแปลเป็น การระงับ กล่าวคือ เพื่อระงับคำเตือนอย่างแท้จริง แต่ลองคิดดูว่าทำไมคุณถึงตัดสินใจระบุมัน? จำกฎข้อที่หนึ่งและบางทีคุณอาจต้องเพิ่มการพิมพ์
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 7

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

ข้อมูลทั่วไปอนุญาตให้คุณพิมพ์วิธีการ มีส่วนแยกต่างหากสำหรับคุณลักษณะนี้ในบทช่วยสอนของ Oracle: “ วิธีการทั่วไป ” จากบทช่วยสอนนี้ สิ่งสำคัญคือต้องจำไวยากรณ์:
  • รวมรายการพารามิเตอร์ที่พิมพ์ไว้ในวงเล็บมุม
  • รายการพารามิเตอร์ที่พิมพ์จะอยู่ก่อนวิธีที่ส่งคืน
ลองดูตัวอย่าง:
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 เราจะเห็นวิธีการพิมพ์สองวิธีในนั้น ด้วยการอนุมานประเภท เราสามารถจัดเตรียมคำจำกัดความประเภทให้กับคอมไพลเลอร์ได้โดยตรง หรือเราจะระบุเองก็ได้ ทั้งสองตัวเลือกจะแสดงอยู่ในตัวอย่าง อย่างไรก็ตาม ไวยากรณ์ค่อนข้างสมเหตุสมผลหากคุณลองคิดดู เมื่อพิมพ์ method เราจะระบุ generic ก่อน method เพราะถ้าเราใช้ generic หลัง method Java จะไม่สามารถระบุได้ว่าจะใช้ type ใด ดังนั้นเราจึงประกาศก่อนว่าเราจะใช้ T ทั่วไป แล้วจึงบอกว่าเราจะคืนยาสามัญนี้ โดยธรรมชาติแล้วUtil.<Integer>getValue(element, String.class)มันจะล้มเหลวโดยมีข้อผิดincompatible types: Class<String> cannot be converted to 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);
		}
    }
}
มันจะทำงานได้ดี แต่ตราบใดที่คอมไพเลอร์เข้าใจว่าวิธีการที่เรียกว่ามีประเภทจำนวนเต็ม มาแทนที่เอาต์พุตคอนโซลด้วยบรรทัดต่อไปนี้: System.out.println(Util.getValue(element) + 1); และเราได้รับข้อผิดพลาด: ประเภทตัวถูกดำเนินการที่ไม่ถูกต้องสำหรับตัวดำเนินการไบนารี '+' ประเภทแรก: Object ประเภทที่สอง: int นั่นคือประเภทถูกลบไปแล้ว คอมไพเลอร์เห็นว่าไม่มีใครระบุประเภท ประเภทถูกระบุเป็นวัตถุ และการเรียกใช้โค้ดล้มเหลวโดยมีข้อผิดพลาด
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 8

ประเภททั่วไป

คุณสามารถพิมพ์ไม่เพียงแต่วิธีการเท่านั้น แต่ยังรวมถึงคลาสด้วย 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);
		}
	}
}
ทุกอย่างเรียบง่ายที่นี่ หากเราใช้คลาส ข้อมูลทั่วไปจะแสดงอยู่หลังชื่อคลาส ตอนนี้เรามาสร้างอินสแตนซ์ของคลาสนี้ด้วยวิธีการหลัก:
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 พิมพ์ Erasure อีกครั้ง เนื่องจากคลาสไม่มีแบบทั่วไปอีกต่อไป คอมไพลเลอร์จึงตัดสินใจว่าเนื่องจากเราส่ง List แล้ว วิธีการที่มี List<Integer> จึงเหมาะสมกว่า และเราก็ล้มลงด้วยความผิดพลาด ดังนั้น กฎ #2: หากมีการพิมพ์คลาส ให้ ระบุประเภทในรูปแบบทั่วไปเสมอ

ข้อ จำกัด

เราสามารถใช้ข้อจำกัดกับประเภทที่ระบุในข้อมูลทั่วไปได้ ตัวอย่างเช่น เราต้องการให้คอนเทนเนอร์ยอมรับเฉพาะ Number เป็นอินพุต คุณลักษณะนี้มีอธิบายไว้ในบทช่วยสอนของ 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");
    }
}
อย่างที่คุณเห็น เราได้จำกัดประเภททั่วไปให้เป็นคลาส/อินเทอร์เฟซ Number และรายการสืบทอด ที่น่าสนใจคือคุณสามารถระบุได้ไม่เพียงแต่คลาสเท่านั้น แต่ยังรวมถึงอินเทอร์เฟซด้วย ตัวอย่างเช่น: public static class NumberContainer<T extends Number & Comparable> { Generics ยังมีแนวคิดของ Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html ในทางกลับกัน จะแบ่งออกเป็นสามประเภท: หลักการ Get Put ที่เรียกว่า ใช้กับWildcards สามารถแสดงได้ในรูปแบบต่อไปนี้:
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 9
หลักการนี้เรียกอีกอย่างว่าหลักการ PECS (Producer Extendeds Consumer Super) คุณสามารถอ่านเพิ่มเติมเกี่ยวกับHabréได้ในบทความ “ การใช้ไวด์การ์ดทั่วไปเพื่อปรับปรุงการใช้งาน Java API ” รวมถึงในการสนทนาที่ยอดเยี่ยมเกี่ยวกับ stackoverflow: “ การใช้ไวด์การ์ดใน Generics Java ” นี่คือตัวอย่างเล็กๆ จากซอร์ส Java - เมธอด Collections.copy:
ทฤษฎียาชื่อสามัญใน Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 10
ตัวอย่างเล็กๆ น้อยๆ ว่ามันใช้งานไม่ได้:
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 ทุกอย่างจะเรียบร้อยดี เนื่องจากเราเติมรายการด้วยค่าก่อนส่งออก รายการจึงเป็นผู้บริโภคสำหรับเรา นั่นคือผู้บริโภค ดังนั้นเราจึงใช้ซุปเปอร์

มรดก

มีคุณสมบัติที่ผิดปกติอีกประการหนึ่งของยาชื่อสามัญนั่นคือการสืบทอด การสืบทอดของยาชื่อสามัญอธิบายไว้ในบทช่วยสอนของ Oracle ในส่วน " Generics, Inheritance และ Subtypes " สิ่งสำคัญคือการจำและตระหนักถึงสิ่งต่อไปนี้ เราไม่สามารถทำเช่นนี้ได้:
List<CharSequence> list1 = new ArrayList<String>();
เนื่องจากการสืบทอดทำงานแตกต่างกับยาชื่อสามัญ:
ทฤษฎียาชื่อสามัญในภาษา Java หรือวิธีใส่วงเล็บในทางปฏิบัติ - 11
และนี่คืออีกตัวอย่างที่ดีที่จะล้มเหลวโดยมีข้อผิดพลาด:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
ทุกอย่างก็เรียบง่ายที่นี่เช่นกัน List<String> ไม่ใช่ลูกหลานของ List<Object> แม้ว่า String จะเป็นผู้สืบทอดของ Object

สุดท้าย

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