เมื่อใดควรใช้วิธีการทั่วไปและเมื่อใดควรใช้ไวด์การ์ด


122

ฉันกำลังอ่านเกี่ยวกับวิธีการทั่วไปจากOracleDocGenericMethod ฉันค่อนข้างสับสนเกี่ยวกับการเปรียบเทียบเมื่อมีการบอกว่าเมื่อใดควรใช้ไวด์การ์ดและเมื่อใดควรใช้วิธีการทั่วไป อ้างจากเอกสาร

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

เราสามารถใช้วิธีการทั่วไปได้ที่นี่แทน:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

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

เราไม่คิดว่าไวด์การ์ด(Collection<? extends E> c);ก็สนับสนุนความหลากหลายของรูปแบบเช่นกัน? แล้วทำไมการใช้วิธีการทั่วไปจึงถือว่าไม่ดีในเรื่องนี้?

กล่าวต่อไปว่า

วิธีการทั่วไปอนุญาตให้ใช้พารามิเตอร์ชนิดเพื่อแสดงการอ้างอิงระหว่างประเภทของอาร์กิวเมนต์หนึ่งหรือหลายอาร์กิวเมนต์สำหรับวิธีการและ / หรือประเภทการส่งคืน หากไม่มีการพึ่งพาดังกล่าวไม่ควรใช้วิธีการทั่วไป

สิ่งนี้หมายความว่า?

พวกเขาได้นำเสนอตัวอย่าง

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

[ ... ]

เราสามารถเขียนลายเซ็นสำหรับวิธีนี้ได้อีกวิธีหนึ่งโดยไม่ต้องใช้สัญลักษณ์แทนเลย:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

เอกสารไม่สนับสนุนการประกาศครั้งที่สองและส่งเสริมการใช้ไวยากรณ์แรก? การประกาศครั้งแรกและครั้งที่สองแตกต่างกันอย่างไร ทั้งคู่ดูเหมือนจะทำสิ่งเดียวกัน?

มีใครช่วยให้แสงสว่างในพื้นที่นี้

คำตอบ:


173

มีสถานที่บางแห่งที่อักขระตัวแทนและพารามิเตอร์ประเภททำสิ่งเดียวกัน แต่ยังมีสถานที่บางแห่งที่คุณต้องใช้พารามิเตอร์ประเภท

  1. หากคุณต้องการบังคับใช้ความสัมพันธ์บางอย่างกับอาร์กิวเมนต์เมธอดประเภทต่างๆคุณไม่สามารถทำได้โดยใช้สัญลักษณ์แทนคุณต้องใช้พารามิเตอร์ชนิด

ใช้วิธีการของคุณเป็นตัวอย่างสมมติว่าคุณต้องการให้แน่ใจว่าsrcและdestรายการที่ส่งไปยังcopy()วิธีการควรเป็นประเภทพารามิเตอร์เดียวกันคุณสามารถทำได้ด้วยพารามิเตอร์ประเภทดังนี้:

public static <T extends Number> void copy(List<T> dest, List<T> src)

ที่นี่คุณจะมั่นใจได้ว่าทั้งสองdestและsrcมีประเภทพารามิเตอร์เดียวกันสำหรับList. ดังนั้นมันปลอดภัยที่จะคัดลอกองค์ประกอบจากไปsrcdest

แต่ถ้าคุณเปลี่ยนวิธีการใช้สัญลักษณ์แทน:

public static void copy(List<? extends Number> dest, List<? extends Number> src)

มันจะไม่ทำงานตามที่คาดไว้ ในกรณีที่ 2 คุณสามารถส่งList<Integer>และList<Float>เป็นและdest srcดังนั้นการย้ายองค์ประกอบจากsrcไปยังdestจะไม่ปลอดภัยอีกต่อไป หากคุณไม่ต้องการความสัมพันธ์แบบนั้นคุณก็มีอิสระที่จะไม่ใช้พารามิเตอร์ประเภทเลย

ความแตกต่างอื่น ๆ ระหว่างการใช้อักขระตัวแทนและพารามิเตอร์ประเภทคือ:

  • หากคุณมีอาร์กิวเมนต์ประเภทที่กำหนดพารามิเตอร์เพียงรายการเดียวคุณสามารถใช้สัญลักษณ์แทนได้แม้ว่าพารามิเตอร์ type จะใช้ได้เช่นกัน
  • พารามิเตอร์ประเภทรองรับหลายขอบเขตอักขระตัวแทนไม่ได้
  • สัญลักษณ์แทนรองรับทั้งขอบเขตบนและล่างพิมพ์พารามิเตอร์เพียงแค่รองรับขอบเขตบน ดังนั้นหากคุณต้องการกำหนดวิธีการที่ใช้ListประเภทIntegerหรือระดับซุปเปอร์คุณสามารถทำได้:

    public void print(List<? super Integer> list)  // OK

    แต่คุณไม่สามารถใช้พารามิเตอร์ type:

     public <T super Integer> void print(List<T> list)  // Won't compile

อ้างอิง:


1
นี่เป็นคำตอบที่แปลก มันไม่ได้อธิบายว่าทำไมคุณถึงต้องใช้?เลย คุณสามารถเขียนใหม่เป็น `public static <T1 ขยาย Number, T2 ขยาย Number> สำเนาเป็นโมฆะ (List <T1> dest, List <T2> src) และในกรณีนี้จะเห็นได้ชัดว่าเกิดอะไรขึ้น
kan

@kan นั่นคือปัญหาที่แท้จริง คุณสามารถใช้พารามิเตอร์ type เพื่อบังคับใช้ประเภทเดียวกัน แต่คุณไม่สามารถทำได้ด้วยสัญลักษณ์แทน การใช้พารามิเตอร์ประเภทสองประเภทที่แตกต่างกันเป็นสิ่งที่แตกต่างกัน
Rohit Jain

1
@benz คุณไม่สามารถกำหนดขอบเขตด้านล่างในListพารามิเตอร์ประเภทโดยใช้ List<T super Integer>ไม่ถูกต้องและจะไม่รวบรวม
Rohit Jain

2
@benz ยินดีต้อนรับค่ะ :) ขอแนะนำให้คุณอ่านลิงก์ที่โพสต์ไว้ตอนท้าย นั่นคือทรัพยากรที่ดีที่สุดใน Generics ที่คุณจะได้รับ
Rohit Jain

3
@ jorgen.ringen <T extends X & Y>-> หลายขอบเขต
Rohit Jain

12

ลองพิจารณาตัวอย่างต่อไปนี้จาก The Java Programming โดย James Gosling 4th edition ด้านล่างที่เราต้องการรวม 2 SinglyLinkQueue:

public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

ทั้งสองวิธีข้างต้นมีฟังก์ชันการทำงานเหมือนกัน แล้วแบบไหนดีกว่ากัน? คำตอบคือข้อที่ 2 ในคำพูดของผู้เขียนเอง:

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

หมายเหตุ: ในหนังสือจะกำหนดวิธีที่สองเท่านั้นและพิมพ์ชื่อพารามิเตอร์คือ S แทน "T" วิธีแรกไม่มีในหนังสือ


ฉันโหวตให้คำพูดของหนังสือมันตรงและกระชับ
คุราปิก้า

9

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

ตัวอย่างเช่น:

public <T> T giveMeMaximum(Collection<T> items);
public <T> Collection<T> applyFilter(Collection<T> items);

คุณกำลังแยก T บางส่วนตามเกณฑ์ที่กำหนด ถ้า T คือLongวิธีการของคุณจะกลับมาLongและCollection<Long>; ประเภทผลตอบแทนจริงขึ้นอยู่กับประเภทพารามิเตอร์ดังนั้นจึงมีประโยชน์และแนะนำให้ใช้ประเภททั่วไป

ในกรณีนี้คุณสามารถใช้ประเภทการ์ดเสริมได้:

public int count(Collection<?> items);
public boolean containsDuplicate(Collection<?> items);

ในการนี้ตัวอย่างเช่นสองสิ่งที่ประเภทของรายการในคอลเลกชันประเภทผลตอบแทนจะเป็นและintboolean

ในตัวอย่างของคุณ:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

ฟังก์ชันทั้งสองนี้จะส่งคืนบูลีนไม่ว่าจะเป็นประเภทของไอเท็มในคอลเลกชัน ในกรณีที่สองจะ จำกัด เฉพาะกรณีของคลาสย่อยของ E

คำถามที่สอง:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

รหัสแรกนี้อนุญาตให้คุณส่งผ่านList<? extends T> srcตัวแปรที่ต่างกัน รายการนี้สามารถมีหลายองค์ประกอบของคลาสต่างๆได้ตราบเท่าที่พวกเขาทั้งหมดขยายคลาสพื้นฐาน T

ถ้าคุณมี:

interface Fruit{}

และ

class Apple implements Fruit{}
class Pear implements Fruit{}
class Tomato implements Fruit{}

คุณสามารถทำได้

List<? extends Fruit> basket = new ArrayList<? extends Fruit>();
basket.add(new Apple());
basket.add(new Pear());
basket.add(new Tomato());
List<Fruit> fridge = new ArrayList<Fruit>(); 

Collections.copy(fridge, basket);// works 

ในทางกลับกัน

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

จำกัดList<S> srcให้เป็นคลาส S หนึ่งคลาสที่เป็นคลาสย่อยของ T รายการสามารถมีองค์ประกอบของคลาสเดียวเท่านั้น (ในอินสแตนซ์นี้ S) และไม่มีคลาสอื่นแม้ว่าจะใช้ T ด้วยก็ตาม คุณจะไม่สามารถใช้ตัวอย่างก่อนหน้านี้ของฉันได้ แต่คุณสามารถทำได้:

List<Apple> basket = new ArrayList<Apple>();
basket.add(new Apple());
basket.add(new Apple());
basket.add(new Apple());
List<Fruit> fridge = new ArrayList<Fruit>();

Collections.copy(fridge, basket); /* works since the basket is defined as a List of apples and not a list of some fruits. */

1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();ไม่ใช่ไวยากรณ์ที่ถูกต้อง คุณต้องสร้างอินสแตนซ์ ArrayList โดยไม่มีขอบเขต
Arnold Pistorius

ไม่สามารถเพิ่ม Apple ลงในตะกร้าในตัวอย่างด้านบนเนื่องจากตะกร้าอาจเป็นรายการของ Pears ' AFAIK ตัวอย่างที่ไม่ถูกต้อง และไม่คอมไพล์ด้วย.
คันนา 111

1
@ArnoldPistorius นั่นทำให้ฉันสับสน ฉันจะตรวจสอบเอกสาร API ของ ArrayList ArrayList(Collection<? extends E> c)และมันมีคอนสตรัคลงนาม คุณช่วยอธิบายได้ไหมว่าทำไมคุณถึงพูดแบบนั้น?
Kurapika

@ คุราปิก้าอาจเป็นเพราะฉันใช้ Java เวอร์ชันเก่าอยู่หรือเปล่า? ความคิดเห็นถูกโพสต์เมื่อเกือบ 3 ปีที่แล้ว
Arnold Pistorius

2

วิธีสัญลักษณ์แทนก็เป็นวิธีทั่วไปเช่นกันคุณสามารถเรียกใช้วิธีนี้ได้ในบางประเภท

<T>ไวยากรณ์กำหนดชื่อตัวแปรประเภท หากตัวแปรชนิดมีการใช้งานใด ๆ (เช่นในการใช้งานวิธีการหรือเป็นข้อ จำกัด สำหรับประเภทอื่น ๆ ) ก็เหมาะสมที่จะตั้งชื่อมิฉะนั้นคุณสามารถใช้?เป็นตัวแปรที่ไม่ระบุชื่อได้ เลยดูเหมือนสั้น ๆ

ยิ่งไปกว่านั้น?ไวยากรณ์ไม่สามารถหลีกเลี่ยงได้เมื่อคุณประกาศเขตข้อมูล:

class NumberContainer
{
 Set<? extends Number> numbers;
}

3
นี่คือไม่ควรแสดงความคิดเห็น?
Buhake Sindi

@BuhakeSindi ขออภัยสิ่งที่ไม่ชัดเจน? ทำไม -1? ผมคิดว่ามันตอบคำถาม
kan

2

ฉันจะพยายามตอบคำถามของคุณทีละข้อ

เราไม่คิดว่าไวด์การ์ด(Collection<? extends E> c);ก็สนับสนุนความหลากหลายของรูปแบบเช่นกัน?

ไม่เหตุผลก็คือไวด์การ์ดที่มีขอบเขตไม่มีประเภทพารามิเตอร์ที่กำหนดไว้ มันเป็นสิ่งที่ไม่รู้จัก สิ่งที่ "รู้" ทั้งหมดก็คือ "การกักกัน" นั้นเป็นประเภทE(อะไรก็ตามที่กำหนดไว้) ดังนั้นจึงไม่สามารถตรวจสอบและพิสูจน์ได้ว่าค่าที่ระบุตรงกับประเภทที่มีขอบเขตหรือไม่

ดังนั้นจึงไม่มีเหตุผลที่จะมีพฤติกรรมหลายรูปแบบบนสัญลักษณ์แทน

เอกสารไม่สนับสนุนการประกาศครั้งที่สองและส่งเสริมการใช้ไวยากรณ์แรก? การประกาศครั้งแรกและครั้งที่สองแตกต่างกันอย่างไร ทั้งคู่ดูเหมือนจะทำสิ่งเดียวกัน?

ตัวเลือกแรกจะดีกว่าในกรณีนี้เนื่องจากTมีขอบเขตเสมอและsourceจะมีค่า (ไม่ทราบ) ที่คลาสย่อยTแน่นอน

สมมติว่าคุณต้องการคัดลอกรายการตัวเลขทั้งหมดตัวเลือกแรกจะเป็น

Collections.copy(List<Number> dest, List<? extends Number> src);

srcยิ่งสามารถยอมรับList<Double>, List<Float>ฯลฯ destมีความผูกพันด้านบนเพื่อแปรชนิดที่พบใน

ตัวเลือกที่ 2 จะบังคับให้คุณผูกSทุกประเภทที่คุณต้องการคัดลอกเช่นนั้น

//For double 
Collections.copy(List<Number> dest, List<Double> src); //Double extends Number.

//For int
Collections.copy(List<Number> dest, List<Integer> src); //Integer extends Number.

เนื่องจากSเป็นประเภทพารามิเตอร์ที่ต้องการการเชื่อมโยง

ฉันหวังว่านี่จะช่วยได้.


คุณช่วยอธิบายได้ไหมในย่อหน้าสุดท้ายของคุณ
benz

สิ่งที่ระบุตัวเลือกที่ 2 จะบังคับให้คุณผูกข้อหนึ่ง ...... คุณช่วยอธิบายให้ละเอียดได้ไหม
benz

<S extends T>ระบุว่าSเป็นชนิดแปรที่เป็น subclass ของTจึงต้องใช้ชนิดแปร (ไม่มีสัญลักษณ์แทน) ที่เป็น subclass Tของ
Buhake Sindi

2

ความแตกต่างอีกประการหนึ่งซึ่งไม่ได้ระบุไว้ที่นี่

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // correct
    }
}

แต่สิ่งต่อไปนี้จะส่งผลให้เกิดข้อผิดพลาดเวลาคอมไพล์

static <T> void fromArrayToCollection(T[] a, Collection<?> c) {
    for (T o : a) {
        c.add(o); // compile time error
    }
}

0

เท่าที่ฉันเข้าใจมีเพียงกรณีการใช้งานเดียวเมื่อจำเป็นต้องใช้สัญลักษณ์แทนอย่างเคร่งครัด (เช่นสามารถแสดงบางสิ่งที่คุณไม่สามารถแสดงออกได้โดยใช้พารามิเตอร์ประเภทที่ชัดเจน) นี่คือเวลาที่คุณต้องระบุขอบเขตล่าง

นอกเหนือจากนั้นอักขระตัวแทนยังใช้ในการเขียนโค้ดที่กระชับมากขึ้นตามที่อธิบายไว้ในข้อความต่อไปนี้ในเอกสารที่คุณกล่าวถึง:

วิธีการทั่วไปอนุญาตให้ใช้พารามิเตอร์ชนิดเพื่อแสดงการอ้างอิงระหว่างประเภทของอาร์กิวเมนต์หนึ่งหรือหลายอาร์กิวเมนต์สำหรับวิธีการและ / หรือประเภทการส่งคืน หากไม่มีการพึ่งพาดังกล่าวไม่ควรใช้วิธีการทั่วไป

[ ... ]

การใช้สัญลักษณ์แทนมีความชัดเจนและรัดกุมกว่าการประกาศพารามิเตอร์ประเภทโจ่งแจ้งดังนั้นจึงควรเลือกใช้เมื่อทำได้

[ ... ]

สัญลักษณ์แทนยังมีข้อได้เปรียบที่สามารถใช้นอกเหนือจากลายเซ็นของวิธีการได้เช่นประเภทของฟิลด์ตัวแปรโลคัลและอาร์เรย์


0

ส่วนใหญ่ -> สัญลักษณ์แทนบังคับใช้ยาสามัญที่ระดับพารามิเตอร์ / อาร์กิวเมนต์ของวิธีการที่ไม่ใช่ทั่วไป บันทึก. นอกจากนี้ยังสามารถดำเนินการใน genericMethod ตามค่าเริ่มต้น แต่ที่นี่แทนที่จะเป็น? เราสามารถใช้ T เอง

ชื่อสามัญของแพ็คเกจ

public class DemoWildCard {


    public static void main(String[] args) {
        DemoWildCard obj = new DemoWildCard();

        obj.display(new Person<Integer>());
        obj.display(new Person<String>());

    }

    void display(Person<?> person) {
        //allows person of Integer,String or anything
        //This cannnot be done if we use T, because in that case we have to make this method itself generic
        System.out.println(person);
    }

}

class Person<T>{

}

SO wildcard มี usecases เฉพาะเช่นนี้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.