Tại sao Java không thể suy ra một siêu kiểu?


19

Chúng ta đều biết Long kéo dài Number . Vậy tại sao điều này không biên dịch?

Và làm thế nào để xác định phương thức withđể chương trình biên dịch mà không có bất kỳ diễn viên thủ công nào?

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;
  }

}
  • Không thể suy ra (các) đối số cho <F, R> with(F, R)
  • Loại getNumber () từ loại Builder.MyInterface là Number, loại này không tương thích với kiểu trả về của bộ mô tả: Dài

Đối với trường hợp sử dụng, xem: Tại sao loại trả về lambda không được kiểm tra tại thời điểm biên dịch


Bạn có thể đăng bài MyInterfacekhông?
Maurice Perry

nó đã ở trong lớp
jukzi

Hmm tôi đã thử <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)nhưng java.lang.Number cannot be converted to java.lang.Long)điều đó thật đáng ngạc nhiên vì tôi không thấy trình biên dịch lấy ý tưởng rằng giá trị trả về gettersẽ cần phải được chuyển đổi thành returnValue.
jingx

@jukzi OK. Xin lỗi tôi đã bỏ lỡ.
Maurice Perry

Đổi Number getNumber()sang<A extends Number> A getNumber() làm cho công cụ làm việc. Không biết liệu đây có phải là điều bạn muốn. Như những người khác đã nói vấn đề là đó MyInterface::getNumbercó thể là một hàm trả về Doublechẳng hạn và không Long. Tuyên bố của bạn không cho phép trình biên dịch thu hẹp loại trả về dựa trên thông tin khác hiện tại. Bằng cách sử dụng một kiểu trả về chung, bạn cho phép trình biên dịch làm như vậy, do đó nó hoạt động.
Giacomo Alzetta

Câu trả lời:


9

Biểu thức này:

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

có thể được viết lại thành:

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

Có tính đến chữ ký phương thức:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R sẽ được suy ra Long
  • F sẽ là Function<MyInterface, Long>

và bạn vượt qua một tham chiếu phương thức sẽ được suy ra vì Function<MyInterface, Number>đây là chìa khóa - làm thế nào trình biên dịch dự đoán rằng bạn thực sự muốn trả vềLong từ một hàm có chữ ký như vậy? Nó sẽ không làm giảm sức mạnh cho bạn.

Numbersiêu lớp của LongNumberkhông nhất thiết phải là một Long(đây là lý do tại sao nó không biên dịch) - bạn sẽ phải tự mình thực hiện một cách rõ ràng:

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

làm Fđược Function<MyIinterface, Long>hoặc vượt qua đối số chung một cách rõ ràng trong lời gọi phương thức như bạn đã làm:

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

và biết R sẽ được xem như Numbervà mã sẽ biên dịch.


Đó là một typecast thú vị. Nhưng tôi vẫn đang tìm kiếm một định nghĩa của phương thức "với" sẽ làm cho người gọi biên dịch mà không có bất kỳ diễn viên nào. Dù sao cũng cảm ơn vì ý tưởng đó.
jukzi

@jukzi Bạn không thể. Không quan trọng bạn withviết như thế nào . Bạn có MJyInterface::getNumberkiểu Function<MyInterface, Number>như vậy R=Numbervà sau đó bạn cũng có R=Longtừ đối số khác (hãy nhớ rằng chữ Java không phải là đa hình!). Tại thời điểm này, trình biên dịch dừng lại vì không phải lúc nào cũng có thể chuyển đổi a Numberthành a Long. Cách duy nhất để khắc phục điều này là thay đổi MyInterfaceđể sử dụng <A extends Number> Numberlàm kiểu trả về, điều này làm cho trình biên dịch có R=Avà sau đó R=Longvà vì A extends Numbernó có thể thay thếA=Long
Giacomo Alzetta

4

Chìa khóa cho lỗi của bạn nằm ở phần khai báo chung của loại F: F extends Function<T, R>. Tuyên bố không hoạt động là: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);Đầu tiên, bạn có một cái mới Builder<MyInterface>. Tuyên bố của lớp do đó ngụ ý T = MyInterface. Theo tuyên bố của bạn with, Fphải là một Function<T, R>, đó là một Function<MyInterface, R>trong tình huống này. Do đó, tham số getterphải lấy MyInterfacetham số dưới dạng (được thỏa mãn bởi các tham chiếu phương thức MyInterface::getNumberMyInterface::getLong) và trả về R, phải cùng loại với tham số thứ hai cho hàm with. Bây giờ, hãy xem nếu điều này giữ cho tất cả các trường hợp của bạn:

// 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);

Bạn có thể "khắc phục" sự cố này bằng các tùy chọn sau:

// 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);

Ngoài thời điểm này, chủ yếu là quyết định thiết kế cho tùy chọn nào làm giảm độ phức tạp mã cho ứng dụng cụ thể của bạn, vì vậy hãy chọn bất cứ thứ gì phù hợp với bạn nhất.

Lý do mà bạn không thể thực hiện việc này mà không truyền nằm ở phần sau, từ Đặc tả ngôn ngữ Java :

Chuyển đổi quyền anh coi các biểu thức của kiểu nguyên thủy là biểu thức của kiểu tham chiếu tương ứng. Cụ thể, chín chuyển đổi sau đây được gọi là chuyển đổi quyền anh :

  • Từ loại boolean đến loại Boolean
  • Từ kiểu byte đến kiểu Byte
  • Từ loại ngắn đến loại Ngắn
  • Từ kiểu char đến kiểu Nhân vật
  • Từ kiểu int đến kiểu Integer
  • Từ loại dài đến loại Dài
  • Từ kiểu float đến kiểu Float
  • Từ loại đôi đến loại Đôi
  • Từ loại null đến loại null

Như bạn có thể thấy rõ, không có chuyển đổi quyền anh ngầm từ dài thành Số và việc chuyển đổi mở rộng từ Dài thành Số chỉ có thể xảy ra khi trình biên dịch chắc chắn rằng nó yêu cầu Số chứ không phải Dài. Vì có mâu thuẫn giữa tham chiếu phương thức yêu cầu Số và 4L cung cấp Long, trình biên dịch (vì lý do nào đó ???) không thể thực hiện bước nhảy vọt logic mà Long là - Số và suy ra đó Flà a Function<MyInterface, Number>.

Thay vào đó, tôi quản lý để giải quyết vấn đề bằng cách chỉnh sửa một chút chữ ký hàm:

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

Sau khi thay đổi, điều sau đây xảy ra:

// 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);

Chỉnh sửa:
Sau khi dành nhiều thời gian hơn cho nó, thật khó khăn để thực thi an toàn loại dựa trên getter. Đây là một ví dụ hoạt động sử dụng các phương thức setter để thực thi loại an toàn của nhà xây dựng:

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");
  }
}

Được cung cấp khả năng an toàn kiểu để xây dựng một đối tượng, hy vọng tại một thời điểm nào đó trong tương lai chúng ta sẽ có thể trả về một đối tượng dữ liệu bất biến từ trình xây dựng (có thể bằng cách thêm một toRecord()phương thức vào giao diện và chỉ định trình xây dựng là a Builder<IntermediaryInterfaceType, RecordType>), vì vậy bạn thậm chí không phải lo lắng về kết quả đối tượng bị sửa đổi. Thành thật mà nói, thật đáng xấu hổ khi nó đòi hỏi rất nhiều nỗ lực để có được một trình xây dựng linh hoạt trường an toàn loại, nhưng có lẽ không thể nếu không có một số tính năng mới, tạo mã hoặc phản xạ khó chịu.


Cảm ơn tất cả công việc của bạn nhưng tôi không thấy bất kỳ cải thiện nào trong việc tránh việc đánh máy thủ công. Sự hiểu biết ngây thơ của tôi là một trình biên dịch sẽ có thể suy ra (tức là typecast) mọi thứ mà con người có thể.
jukzi

@jukzi Sự hiểu biết ngây thơ của tôi là như vậy, nhưng vì lý do gì nó không hoạt động theo cách đó. Tôi đã đưa ra một cách giải quyết mà khá nhiều đạt được hiệu quả mong muốn, tuy nhiên
Avi

Cảm ơn một lần nữa. Nhưng đề xuất mới của bạn quá rộng (xem stackoverflow.com/questions/58337639 ) vì nó cho phép biên dịch ".with (MyInterface :: getNumber," TÔI KHÔNG PHẢI LÀ SỐ ")";
jukzi

câu của bạn "Trình biên dịch không thể suy ra loại tham chiếu phương thức và 4L cùng một lúc" thật tuyệt. Nhưng tôi chỉ muốn nó theo cách khác arround. trình biên dịch nên thử Number dựa trên tham số đầu tiên và mở rộng thành Number của tham số thứ hai.
jukzi

1
WHAAAAAAAAAAAT? Tại sao BiConsumer hoạt động như dự định trong khi Chức năng thì không? Tôi không có ý tưởng. Tôi thừa nhận đây chính xác là loại an toàn tôi muốn nhưng không may là không hoạt động với getters. TẠI SAO TẠI SAO TẠI SAO.
jukzi

1

Có vẻ như trình biên dịch đã sử dụng giá trị 4L để quyết định rằng R là Long và getNumber () trả về một Số, không nhất thiết phải là Dài.

Nhưng tôi không chắc tại sao giá trị lại được ưu tiên hơn phương thức ...


0

Trình biên dịch Java nói chung không tốt trong việc suy ra nhiều kiểu chung hoặc các ký tự đại diện. Thường thì tôi không thể lấy thứ gì đó để biên dịch mà không sử dụng hàm trợ giúp để chụp hoặc suy ra một số loại.

Nhưng, bạn có thực sự cần phải nắm bắt chính xác loại Functionnhư F? Nếu không, có thể các tác phẩm sau đây, và như bạn có thể thấy, dường như cũng hoạt động với các kiểu con của 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");

    }
}

"với (MyInterface :: getNumber," KHÔNG PHẢI LÀ SỐ ")" không nên biên dịch
jukzi

0

Phần thú vị nhất nằm ở sự khác biệt giữa 2 dòng đó, tôi nghĩ:

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

Trong trường hợp đầu tiên, Trõ ràng Number, 4Lcũng là một Number, không có vấn đề. Trong trường hợp thứ hai, 4Llà một Long, vì vậy Tlà một Long, vì vậy chức năng của bạn không tương thích, và Java không thể biết nếu bạn có nghĩa là Numberhoặc Long.


0

Với chữ ký sau:

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

tất cả các ví dụ của bạn biên dịch, ngoại trừ thứ ba, yêu cầu rõ ràng phương thức này có hai biến loại.

Lý do phiên bản của bạn không hoạt động là vì các tham chiếu phương thức của Java không có một loại cụ thể. Thay vào đó, họ có loại được yêu cầu trong bối cảnh nhất định. Trong trường hợp của bạn, Rđược suy ra là Longdo 4L, nhưng getter không thể có kiểu Function<MyInterface,Long>vì trong Java, các kiểu chung là bất biến trong các đối số của chúng.


Mã của bạn sẽ biên dịch with( getNumber,"NO NUMBER")mà không mong muốn. Ngoài ra, sự thật là luôn luôn bất biến (xem stackoverflow.com/a/58378661/9549750 để chứng minh rằng các tướng của setters hành xử khác với các getters)
jukzi

@jukzi Ah, giải pháp của tôi đã được đề xuất bởi Avi. Quá tệ... :-). Nhân tiện, đúng là chúng ta có thể gán a Thing<Cat>cho một Thing<? extends Animal>biến, nhưng đối với hiệp phương sai thực sự tôi mong đợi rằng a Thing<Cat>có thể được gán cho a Thing<Animal>. Các ngôn ngữ khác, chẳng hạn như Kotlin, cho phép xác định các biến loại co-và contravariant.
Hoopje
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.