ทำไมจาวาไม่สามารถอนุมานประเภทของ super?


19

Numberเราทุกคนรู้ยาวยื่นออกมา เหตุใดจึงไม่รวบรวม

และวิธีการกำหนดวิธีการwithเพื่อให้โปรแกรมคอมไพล์โดยไม่มีการส่งด้วยตนเองหรือไม่?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}
  • ไม่สามารถอนุมานประเภทอาร์กิวเมนต์สำหรับ <F, R> with(F, R)
  • ประเภทของ getNumber () จากประเภท Builder.MyInterface คือ Number สิ่งนี้ไม่เข้ากันกับประเภทการคืนของ descriptor: ยาว

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


คุณโพสต์ได้MyInterfaceไหม
Maurice Perry

มันอยู่ในชั้นเรียนแล้ว
jukzi

อืมผมพยายาม<F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)แต่ได้java.lang.Number cannot be converted to java.lang.Long)ซึ่งเป็นที่น่าแปลกใจเพราะผมไม่ได้ดูว่าคอมไพเลอร์ที่ได้รับความคิดที่ว่าค่าตอบแทนจากจะต้องมีการแปลงเป็นgetter returnValue
jingx

@jukzi ตกลง ขออภัยฉันพลาดไป
Maurice Perry

การเปลี่ยนNumber getNumber()เพื่อ<A extends Number> A getNumber()ทำให้สิ่งต่าง ๆ ทำงานได้ ไม่มีความคิดว่านี่คือสิ่งที่คุณต้องการ ขณะที่คนอื่น ๆ ได้กล่าวว่าปัญหาคือMyInterface::getNumberอาจจะมีฟังก์ชั่นที่ให้ผลตอบแทนเช่นไม่Double Longการประกาศของคุณไม่อนุญาตให้คอมไพเลอร์ จำกัด ประเภทการส่งคืนตามข้อมูลอื่นที่มีอยู่ ด้วยการใช้ชนิดส่งคืนทั่วไปคุณอนุญาตให้คอมไพเลอร์ทำเช่นนั้นได้
Giacomo Alzetta

คำตอบ:


9

การแสดงออกนี้:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

สามารถเขียนใหม่เป็น:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

คำนึงถึงลายเซ็นวิธีการบัญชี:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R จะอนุมานถึง Long
  • F จะ Function<MyInterface, Long>

และคุณผ่านการอ้างอิงเมธอดซึ่งจะอนุมานได้ว่าFunction<MyInterface, Number>นี่คือกุญแจ - คอมไพเลอร์ควรทำนายอย่างไรว่าคุณต้องการกลับมาLongจากฟังก์ชั่นที่มีลายเซ็นดังกล่าวจริงหรือไม่? มันจะไม่ทำ downcasting สำหรับคุณ

เนื่องจากNumberเป็น superclass ของLongและNumberไม่จำเป็นต้องมี a Long(นี่คือเหตุผลที่มันไม่ได้รวบรวม) - คุณจะต้องมีการแสดงออกอย่างชัดเจนด้วยตัวคุณเอง:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

ทำให้Fจะเป็นFunction<MyIinterface, Long>หรือส่งผ่านอาร์กิวเมนต์ทั่วไปอย่างชัดเจนในระหว่างการเรียกวิธีการตามที่คุณได้:

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

และรู้ว่าRจะถูกมองว่าเป็นNumberและรหัสจะรวบรวม


นั่นเป็น typecast ที่น่าสนใจ แต่ก็ยังฉันกำลังค้นหาคำจำกัดความของวิธีการ "กับ" ซึ่งจะทำให้ผู้โทรรวบรวมโดยไม่ต้องมีนักแสดง อย่างไรก็ตามขอขอบคุณสำหรับความคิดนั้น
jukzi

@jukzi คุณไม่สามารถ ไม่สำคัญว่าคุณwithเขียนอย่างไร คุณมีMJyInterface::getNumberประเภทFunction<MyInterface, Number>ดังนั้นR=Numberแล้วคุณยังมีR=Longจากอาร์กิวเมนต์อื่น ๆ (จำไว้ว่าตัวอักษร Java ไม่ polymorphic!) ณ จุดนี้คอมไพเลอร์จะหยุดเพราะมันเป็นไปไม่ได้เสมอในการแปลงไปNumber Longวิธีเดียวที่จะแก้ไขปัญหานี้ได้คือเปลี่ยนMyInterfaceเป็นใช้<A extends Number> Numberเป็นชนิดส่งคืนทำให้คอมไพเลอร์มีR=Aแล้วR=Longและเนื่องจากA extends Numberสามารถทดแทนได้A=Long
Giacomo Alzetta

4

กุญแจสู่ความผิดพลาดของคุณอยู่ในการประกาศทั่วไปประเภทของ:F F extends Function<T, R>คำสั่งที่ไม่ทำงานคือ: ครั้งแรกที่คุณมีใหม่new Builder<MyInterface>().with(MyInterface::getNumber, 4L); การประกาศของชั้นจึงหมายถึงBuilder<MyInterface> T = MyInterfaceตามประกาศของwithคุณFจะต้องเป็นFunction<T, R>ซึ่งเป็นFunction<MyInterface, R>ในสถานการณ์นี้ ดังนั้นพารามิเตอร์getterต้องใช้MyInterfaceเป็นพารามิเตอร์ (ความพึงพอใจโดยอ้างอิงวิธีการMyInterface::getNumberและMyInterface::getLong) และผลตอบแทนซึ่งจะต้องเป็นชนิดเดียวกันเป็นพารามิเตอร์ที่สองไปยังฟังก์ชันR withทีนี้มาดูกันว่านี่จะเป็นกรณีของคุณทั้งหมดหรือไม่:

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

คุณสามารถ "แก้ไข" ปัญหานี้ด้วยตัวเลือกต่อไปนี้:

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

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

เหตุผลที่คุณไม่สามารถทำสิ่งนี้ได้โดยไม่ต้องส่งคำสั่งนั้นอยู่ในข้อมูลต่อไปนี้จากข้อกำหนดภาษา Java :

การแปลงมวยถือว่าการแสดงออกของประเภทดั้งเดิมเป็นนิพจน์ของประเภทการอ้างอิงที่สอดคล้องกัน โดยเฉพาะการแปลงเก้ารายการต่อไปนี้เรียกว่าการแปลงมวย :

  • จากประเภทบูลีนถึงประเภทบูลีน
  • จาก type byte เป็น type Byte
  • จากประเภทสั้นถึงพิมพ์สั้น
  • จากประเภทถ่านเพื่อพิมพ์ตัวละคร
  • จาก type int เป็น Type Integer
  • จากพิมพ์ยาวเป็นพิมพ์ยาว
  • จากประเภทลอยไปลอยประเภท
  • จาก type double เป็น type Double
  • จากประเภท null เป็นประเภท null

อย่างที่คุณเห็นชัดเจนไม่มีการแปลงมวยโดยปริยายจากยาวเป็นจำนวนและการแปลงแบบขยายจาก Long เป็น Number สามารถเกิดขึ้นได้เมื่อคอมไพเลอร์แน่ใจว่ามันต้องการตัวเลขและไม่ใช่แบบยาว ในฐานะที่มีความขัดแย้งระหว่างอ้างอิงวิธีการที่ต้องใช้จำนวนและ 4L ที่ให้ลองคอมไพเลอร์ (ด้วยเหตุผลบางอย่าง ???) ไม่สามารถที่จะทำให้ก้าวกระโดดตรรกะที่ยาวเป็น-หมายเลขและได้ข้อสรุปว่าเป็นFFunction<MyInterface, Number>

แต่ฉันจัดการเพื่อแก้ไขปัญหาโดยการแก้ไขลายเซ็นฟังก์ชั่นเล็กน้อย:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

หลังจากการเปลี่ยนแปลงนี้เกิดขึ้นต่อไปนี้:

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

แก้ไข:
หลังจากใช้เวลากับมันนานขึ้นมันเป็นเรื่องยากที่จะบังคับใช้ความปลอดภัยของประเภทผู้ใช้ฐาน นี่คือตัวอย่างการทำงานที่ใช้วิธีการตั้งค่าเพื่อบังคับใช้ความปลอดภัยของผู้สร้าง:

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

ให้ความสามารถในการสร้างวัตถุที่ปลอดภัยชนิดหวังว่าในอนาคตเราจะสามารถส่งคืนวัตถุข้อมูลที่ไม่เปลี่ยนรูปจากผู้สร้าง (อาจจะเพิ่มtoRecord()วิธีการอินเทอร์เฟซและระบุผู้สร้างเป็นBuilder<IntermediaryInterfaceType, RecordType>) ดังนั้นคุณไม่ต้องกังวลเกี่ยวกับวัตถุที่เป็นผลลัพธ์ที่จะถูกแก้ไข จริงๆแล้วมันเป็นเรื่องน่าละอายอย่างยิ่งที่ต้องใช้ความพยายามอย่างมากในการสร้างตัวสร้างฟิลด์ที่ยืดหยุ่นได้ แต่มันอาจเป็นไปไม่ได้หากไม่มีคุณสมบัติใหม่การสร้างรหัสหรือการสะท้อนที่น่ารำคาญ


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

@ jukzi ความเข้าใจอันไร้เดียงสาของฉันเหมือนกัน แต่ไม่ว่าด้วยเหตุผลใดมันก็ไม่ทำงาน ฉันคิดวิธีแก้ปัญหาที่ได้ผลที่ต้องการค่อนข้างมาก
Avi

ขอบคุณอีกครั้ง. แต่ข้อเสนอใหม่ของคุณกว้างเกินไป (ดูstackoverflow.com/questions/58337639 ) เนื่องจากอนุญาตให้รวบรวม ".with (MyInterface :: getNumber" ฉันไม่ใช่หมายเลข ")";
jukzi

ประโยค "คอมไพเลอร์ไม่สามารถอนุมานประเภทวิธีการอ้างอิงและ 4L ในเวลาเดียวกัน" ของคุณเจ๋ง แต่ฉันแค่ต้องการให้มันเป็นอย่างอื่น arround คอมไพเลอร์ควรลอง Number ตามพารามิเตอร์แรกและทำการขยับขยายเป็น Number ของพารามิเตอร์ที่สอง
jukzi

1
WHAAAAAAAAAAAT? เหตุใด BiConsumer จึงทำงานได้ตามที่ตั้งใจไว้ในขณะที่ฟังก์ชั่นไม่ทำงาน ฉันไม่ได้รับความคิด ฉันยอมรับว่านี่เป็นสิ่งที่ฉันต้องการ แต่โชคไม่ดีที่ไม่ได้ทำงานกับผู้ได้รับ ทำไมถึงเป็นเช่นนั้น
jukzi

1

ดูเหมือนว่าคอมไพเลอร์ได้ใช้ค่า 4L เพื่อตัดสินใจว่า R คือ Long และ getNumber () จะคืนค่า Number ซึ่งไม่จำเป็นต้องเป็น Long

แต่ฉันไม่แน่ใจว่าทำไมค่าจึงมีความสำคัญมากกว่าวิธีการ ...


0

คอมไพเลอร์ Java โดยทั่วไปไม่ดีในการอนุมานประเภททั่วไป / ซ้อนหลายประเภทหรืออักขระตัวแทน บ่อยครั้งที่ฉันไม่สามารถรวบรวมบางอย่างได้โดยไม่ต้องใช้ฟังก์ชั่นตัวช่วยในการจับหรืออนุมานบางประเภท

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

import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
        return null;
    }

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}

"with (MyInterface :: getNumber," Not A Number ")" ไม่ควรคอมไพล์
jukzi

0

ส่วนที่น่าสนใจที่สุดคือความแตกต่างระหว่าง 2 บรรทัดนี้ฉันคิดว่า:

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

ในกรณีแรกTคือชัดเจนNumberดังนั้นจึง4Lเป็นNumberไม่มีปัญหา ในกรณีที่สอง4LคือLongเพื่อให้Tเป็นLongเพื่อให้การทำงานของคุณเข้ากันไม่ได้และ Java ไม่สามารถทราบว่าคุณหมายหรือNumberLong


0

ด้วยลายเซ็นต่อไปนี้:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

ตัวอย่างทั้งหมดของคุณรวบรวมยกเว้นตัวที่สามซึ่งต้องการวิธีการที่จะมีตัวแปรประเภทสองอย่างชัดเจน

สาเหตุที่รุ่นของคุณใช้งานไม่ได้เป็นเพราะการอ้างอิงวิธีของ Java ไม่มีประเภทที่เฉพาะเจาะจง แต่มีประเภทที่จำเป็นในบริบทที่กำหนดแทน ในกรณีของคุณRอนุมานได้ว่าเป็นLongเพราะ4Lแต่ผู้ทะเยอทะยานไม่สามารถมีประเภทFunction<MyInterface,Long>เพราะใน Java ประเภททั่วไปจะไม่เปลี่ยนแปลงในการขัดแย้งของพวกเขา


รหัสของคุณจะรวบรวมwith( getNumber,"NO NUMBER")ซึ่งไม่ต้องการ นอกจากนี้ก็ไม่เป็นความจริงว่ายาชื่อสามัญมักจะคงที่ (ดูstackoverflow.com/a/58378661/9549750สำหรับพิสูจน์ยาชื่อสามัญของ setters ประพฤติอื่น ๆ ที่แล้วบรรดา getters)
jukzi

@jukzi อ่าโซลูชันของฉันถูกเสนอโดย Avi แล้ว เลวร้ายเกินไป... :-). โดยวิธีการที่มันเป็นความจริงที่เราสามารถกำหนดThing<Cat>ให้เป็นThing<? extends Animal>ตัวแปร แต่สำหรับแปรปรวนจริงผมจะคาดหวังว่าจะได้รับมอบหมายให้เป็นThing<Cat> Thing<Animal>ภาษาอื่น ๆ เช่น Kotlin อนุญาตให้มีการกำหนดตัวแปรประเภท co-and contravariant
Hoopje
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.