Tại sao tham số kiểu mạnh hơn tham số phương thức


12

Tại sao lại là

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

nghiêm ngặt hơn rồi

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Đây là phần tiếp theo tại sao loại trả về lambda không được kiểm tra tại thời điểm biên dịch . Tôi tìm thấy bằng cách sử dụng phương pháp withX()như

.withX(MyInterface::getLength, "I am not a Long")

tạo ra lỗi thời gian biên dịch mong muốn:

Loại getLpm () từ loại BuilderExample.MyInterface dài, loại này không tương thích với kiểu trả về của bộ mô tả: Chuỗi

Trong khi sử dụng phương pháp with()thì không.

ví dụ đầy đủ:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

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

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

Ví dụ mở rộng

Ví dụ sau đây cho thấy hành vi khác nhau của phương thức và tham số loại được đưa vào Nhà cung cấp. Ngoài ra, nó cho thấy sự khác biệt đối với hành vi của Người tiêu dùng đối với tham số loại. Và nó cho thấy nó không tạo ra sự khác biệt khi nó là Người tiêu dùng hoặc Nhà cung cấp cho một tham số phương thức.

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}

1
Vì suy luận với cái sau. Mặc dù cả hai đều dựa trên trường hợp sử dụng, người ta cần phải thực hiện. Đối với bạn, trước đây có thể nghiêm ngặt và tốt. Để linh hoạt, người khác có thể thích cái sau.
Naman

Bạn đang cố gắng biên dịch cái này trong Eclipse? Tìm kiếm các chuỗi lỗi của định dạng bạn đã dán cho thấy đây là một lỗi cụ thể của Eclipse (ecj). Bạn có gặp vấn đề tương tự khi biên dịch với javaccông cụ thô hoặc công cụ xây dựng như Gradle hoặc Maven không?
dùng31601

@ user31601 tôi đã thêm một ví dụ đầy đủ với đầu ra javac. Các thông báo lỗi được tạo thành ít khác nhau nhưng vẫn nhật thực và javac giữ hành vi tương tự
jukzi

Câu trả lời:


12

Đây là một câu hỏi thực sự thú vị. Câu trả lời, tôi sợ, rất phức tạp.

tl; dr

Tìm ra sự khác biệt liên quan đến một số cách đọc khá sâu về đặc tả suy luận kiểu Java , nhưng về cơ bản làm rõ điều này:

  • Tất cả những thứ khác như nhau, trình biên dịch sẽ nhập vào loại cụ thể nhất có thể.
  • Tuy nhiên, nếu nó có thể tìm thấy sự thay thế cho một tham số loại thỏa mãn tất cả các yêu cầu, thì quá trình biên dịch sẽ thành công, tuy nhiên mơ hồ sự thay thế hóa ra là như vậy.
  • withcó một sự thay thế (thừa nhận mơ hồ) thỏa mãn tất cả các yêu cầu về R:Serializable
  • Đối với withX, việc giới thiệu tham số loại bổ sung Fbuộc trình biên dịch phải giải quyết Rtrước mà không xem xét ràng buộc F extends Function<T,R>. Rgiải quyết (cụ thể hơn nhiều) Stringmà sau đó có nghĩa là suy luận về Fthất bại.

Điểm đạn cuối cùng này là quan trọng nhất, nhưng cũng là gợn sóng nhất. Tôi không thể nghĩ ra một cách diễn đạt ngắn gọn hơn, vì vậy nếu bạn muốn biết thêm chi tiết, tôi khuyên bạn nên đọc phần giải thích đầy đủ bên dưới.

Đây có phải là hành vi dự định?

Tôi sẽ đi ra ngoài một chi ở đây, và nói không .

Tôi không cho rằng có một lỗi trong thông số kỹ thuật, hơn nữa (trong trường hợp withX), các nhà thiết kế ngôn ngữ đã giơ tay và nói "có một số tình huống mà suy luận kiểu quá khó, vì vậy chúng ta sẽ thất bại" . Mặc dù hành vi của nhà soạn nhạc liên quan đến withXdường như là điều bạn muốn, tôi sẽ coi đó là tác dụng phụ ngẫu nhiên của thông số kỹ thuật hiện tại, thay vì quyết định thiết kế có chủ đích tích cực.

Vấn đề này, bởi vì nó thông báo cho câu hỏi Tôi có nên dựa vào hành vi này trong thiết kế ứng dụng của mình không? Tôi cho rằng bạn không nên, vì bạn không thể đảm bảo rằng các phiên bản ngôn ngữ trong tương lai sẽ tiếp tục hành xử theo cách này.

Mặc dù các nhà thiết kế ngôn ngữ rất cố gắng không phá vỡ các ứng dụng hiện có khi họ cập nhật thông số kỹ thuật / thiết kế / trình biên dịch của họ, vấn đề là hành vi bạn muốn dựa vào là một trong đó trình biên dịch hiện không thành công (tức là không phải là ứng dụng hiện có ). Cập nhật Langauge biến mã không biên dịch thành mã biên dịch mọi lúc. Ví dụ, đoạn mã sau có thể được đảm bảo không biên dịch trong Java 7, nhưng sẽ biên dịch trong Java 8:

static Runnable x = () -> System.out.println();

Trường hợp sử dụng của bạn là không khác nhau.

Một lý do khác tôi nên thận trọng khi sử dụng withXphương pháp của bạn là Fchính tham số đó. Nói chung, một tham số loại chung trên một phương thức (không xuất hiện trong kiểu trả về) tồn tại để liên kết các loại nhiều phần của chữ ký với nhau. Nó đang nói:

Tôi không quan tâm nó Tlà gì , nhưng muốn chắc chắn rằng bất cứ nơi nào tôi sử dụng Tnó đều cùng loại.

Về mặt logic, sau đó, chúng tôi hy vọng mỗi tham số loại sẽ xuất hiện ít nhất hai lần trong một chữ ký phương thức, nếu không thì "nó không làm gì cả". Ftrong bạn withXchỉ xuất hiện một lần trong chữ ký, điều này gợi ý cho tôi việc sử dụng tham số loại không theo dòng với mục đích của tính năng này của ngôn ngữ.

Một triển khai thay thế

Một cách để thực hiện điều này theo cách "hành vi dự định" hơn một chút sẽ là chia withphương thức của bạn thành một chuỗi 2:

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

Điều này sau đó có thể được sử dụng như sau:

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

Điều này không bao gồm một tham số loại không giống như của bạn withX. Bằng cách chia nhỏ phương thức thành hai chữ ký, nó cũng thể hiện rõ hơn ý định của những gì bạn đang cố gắng thực hiện, từ quan điểm an toàn kiểu:

  • Phương thức đầu tiên thiết lập một lớp ( With) xác định kiểu dựa trên tham chiếu phương thức.
  • Phương thức scond ( of) ràng buộc kiểu valuetương thích với những gì bạn đã thiết lập trước đó.

Cách duy nhất một phiên bản tương lai của ngôn ngữ sẽ có thể biên dịch điều này là nếu việc gõ vịt hoàn chỉnh, có vẻ như không thể.

Một lưu ý cuối cùng để làm cho toàn bộ điều này không liên quan: Tôi nghĩ Mockito (và đặc biệt là chức năng khai thác của nó) về cơ bản có thể đã làm những gì bạn đang cố gắng đạt được với "công cụ xây dựng chung an toàn". Có lẽ bạn chỉ có thể sử dụng thay thế?

Giải thích đầy đủ (ish)

Tôi sẽ làm việc thông qua thủ tục suy luận kiểu cho cả hai withwithX. Điều này khá dài, vì vậy hãy từ từ. Mặc dù đã lâu, tôi vẫn để lại khá nhiều chi tiết. Bạn có thể muốn tham khảo thông số kỹ thuật để biết thêm chi tiết (theo liên kết) để thuyết phục bản thân rằng tôi đúng (tôi có thể đã phạm sai lầm).

Ngoài ra, để đơn giản hóa mọi thứ một chút, tôi sẽ sử dụng một mẫu mã tối thiểu hơn. Sự khác biệt chính là nó hoán đổi ra Functioncho Supplier, vì vậy có nhiều loại ít và các thông số trong vở kịch. Đây là một đoạn đầy đủ tái tạo hành vi bạn mô tả:

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

Chúng ta hãy lần lượt làm việc thông qua suy luận khả năng áp dụng và thủ tục suy luận kiểu cho từng lần gọi phương thức:

with

Chúng ta có:

with(TypeInference::getLong, "Not a long");

Tập ràng buộc ban đầu, B 0 , là:

  • R <: Object

Tất cả các biểu thức tham số phù hợp với khả năng ứng dụng .

Do đó, ràng buộc ban đầu được đặt ra cho suy luận khả năng ứng dụng , C , là:

  • TypeInference::getLong tương ứng với Supplier<R>
  • "Not a long" tương ứng với R

Điều này làm giảm tập B 2 bị ràng buộc của:

  • R <: Object(từ B 0 )
  • Long <: R (từ ràng buộc đầu tiên)
  • String <: R (từ ràng buộc thứ hai)

Vì đây không chứa các ràng buộc ' sai ', và (tôi giả sử) độ phân giải của Rthành công (cho Serializable) thì gọi là hiện hành.

Vì vậy, chúng tôi chuyển sang suy luận kiểu gọi .

Tập ràng buộc mới, C , với các biến đầu vàođầu ra liên quan , là:

  • TypeInference::getLong tương ứng với Supplier<R>
    • Biến đầu vào: không có
    • Biến đầu ra: R

Điều này không chứa sự phụ thuộc lẫn nhau giữa các biến đầu vàođầu ra , do đó có thể được giảm trong một bước duy nhất và tập ràng buộc cuối cùng, B 4 , giống như B 2 . Do đó, độ phân giải thành công như trước và trình biên dịch thở phào nhẹ nhõm!

withX

Chúng ta có:

withX(TypeInference::getLong, "Also not a long");

Tập ràng buộc ban đầu, B 0 , là:

  • R <: Object
  • F <: Supplier<R>

Chỉ có biểu thức tham số thứ hai là thích hợp cho khả năng ứng dụng . Cái đầu tiên ( TypeInference::getLong) thì không, bởi vì nó đáp ứng điều kiện sau:

Nếu mlà một phương thức chung và việc gọi phương thức không cung cấp các đối số kiểu rõ ràng, một biểu thức lambda được gõ rõ ràng hoặc một biểu thức tham chiếu phương thức chính xác mà loại mục tiêu tương ứng (như xuất phát từ chữ ký của m) là một tham số loại m.

Do đó, ràng buộc ban đầu được đặt ra cho suy luận khả năng ứng dụng , C , là:

  • "Also not a long" tương ứng với R

Điều này làm giảm tập B 2 bị ràng buộc của:

  • R <: Object(từ B 0 )
  • F <: Supplier<R>(từ B 0 )
  • String <: R (từ ràng buộc)

Một lần nữa, vì điều này không chứa 'ràng buộc sai sự thật ', và độ phân giải của Rthành công (cho String) thì gọi là hiện hành.

Kiểu suy luận một lần nữa ...

Lần này, tập ràng buộc mới, C , với các biến đầu vàođầu ra liên quan , là:

  • TypeInference::getLong tương ứng với F
    • Biến đầu vào: F
    • Biến đầu ra: không có

Một lần nữa, chúng ta không có sự phụ thuộc lẫn nhau giữa các biến đầu vàođầu ra . Tuy nhiên thời điểm này, một biến đầu vào ( F), vì vậy chúng ta phải giải quyết này trước khi thực hiện giảm . Vì vậy, chúng tôi bắt đầu với bộ B 2 ràng buộc của chúng tôi .

  1. Chúng tôi xác định một tập hợp con Vnhư sau:

    Đưa ra một tập hợp các biến suy luận để giải quyết, hãy Vlà tập hợp của tập hợp này và tất cả các biến mà độ phân giải của ít nhất một biến trong tập hợp này phụ thuộc vào.

    Bởi ràng buộc thứ hai trong B 2 , độ phân giải Fphụ thuộc vào R, vì vậy V := {F, R}.

  2. Chúng tôi chọn một tập hợp con Vtheo quy tắc:

    hãy { α1, ..., αn }là một tập hợp con không rỗng của các biến không có căn cứ Vsao cho i) cho tất cả i (1 ≤ i ≤ n), nếu αiphụ thuộc vào độ phân giải của một biến β, thì có thể βcó một khởi tạo hoặc có một số jsao cho β = αj; và ii) không tồn tại tập hợp con không trống của thuộc { α1, ..., αn }tính này.

    Tập hợp con duy nhất Vđáp ứng tính chất này là {R}.

  3. Sử dụng ràng buộc thứ ba ( String <: R), chúng tôi khởi tạo R = Stringvà kết hợp điều này vào tập ràng buộc của chúng tôi. Rbây giờ được giải quyết, và ràng buộc thứ hai có hiệu quả F <: Supplier<String>.

  4. Sử dụng ràng buộc thứ hai (sửa đổi), chúng tôi khởi tạo F = Supplier<String>. Fhiện đã được giải quyết.

Bây giờ đã Fđược giải quyết, chúng ta có thể tiến hành giảm , sử dụng ràng buộc mới:

  1. TypeInference::getLong tương ứng với Supplier<String>
  2. ... giảm đến Long tương thích với String
  3. ... làm giảm thành sai

... Và chúng tôi nhận được một lỗi biên dịch!


Ghi chú bổ sung về 'Ví dụ mở rộng'

Các ví dụ mở rộng trong vẻ câu hỏi tại một vài trường hợp thú vị mà không trực tiếp bao phủ bởi các hoạt động trên:

  • Trong đó kiểu giá trị là kiểu con của kiểu trả về phương thức ( Integer <: Number)
  • Trong đó giao diện chức năng là chống chỉ định trong loại suy ra (nghĩa Consumerlà hơn Supplier)

Cụ thể, 3 trong số các yêu cầu đã cho là có khả năng gợi ý hành vi trình biên dịch 'khác biệt' với hành vi được mô tả trong phần giải thích:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

Thứ hai trong số 3 sẽ trải qua quá trình suy luận chính xác như withXtrên (chỉ cần thay thế Longbằng NumberStringbằng Integer). Điều này minh họa một lý do khác tại sao bạn không nên dựa vào hành vi suy luận kiểu thất bại này cho thiết kế lớp của bạn, vì việc không biên dịch ở đây có thể không phải là một hành vi mong muốn.

Đối với 2 cái còn lại (và thực tế là bất kỳ yêu cầu nào khác liên quan đến Consumerbạn muốn thực hiện), hành vi sẽ rõ ràng nếu bạn thực hiện quy trình suy luận kiểu được đặt ra cho một trong các phương pháp trên (ví dụ: withđầu tiên, withXcho ngày thứ ba). Chỉ có một thay đổi nhỏ bạn cần lưu ý:

  • Ràng buộc về tham số đầu tiên ( t::setNumber tương thích với Consumer<R> ) sẽ giảm xuống R <: Numberthay vì Number <: Rnhư vậy Supplier<R>. Điều này được mô tả trong các tài liệu liên kết về giảm.

Tôi để nó như một bài tập để người đọc cẩn thận làm việc thông qua một trong các thủ tục trên, được trang bị kiến ​​thức bổ sung này, để chứng minh cho chính họ lý do tại sao một lời mời cụ thể không hoặc không biên dịch.


Rất sâu sắc, nghiên cứu và xây dựng tốt. Cảm ơn!
Zabuzard

@ user31601 Bạn có thể vui lòng chỉ ra sự khác biệt của Nhà cung cấp với Người tiêu dùng. Tôi đã thêm một ví dụ mở rộng trong câu hỏi ban đầu cho điều đó. Nó cho thấy hành vi covariant, contravariant và bất biến cho các phiên bản khác nhau của letBe (), letBeX () và let (). Be () tùy thuộc vào Nhà cung cấp / Người tiêu dùng.
jukzi

@jukzi Tôi đã thêm một vài ghi chú bổ sung, nhưng bạn nên có đủ thông tin để tự mình làm việc thông qua các ví dụ mới này.
dùng31601

Đó là sự xen kẽ: rất nhiều trường hợp đặc biệt trong 18.2.1. cho lambdas và tài liệu tham khảo phương pháp mà tôi không mong đợi bất kỳ trường hợp đặc biệt nào cho họ từ sự hiểu biết ngây thơ của tôi. Và có lẽ không có nhà phát triển bình thường nào mong đợi.
jukzi

Chà, tôi đoán lý do là với lambdas và các tham chiếu phương thức, trình biên dịch cần phải quyết định loại lambda nào nên thực hiện - nó phải đưa ra lựa chọn! Ví dụ, TypeInference::getLongcó thể imlement Supplier<Long>hoặc Supplier<Serializable>hoặc Supplier<Number>vv, nhưng điều quan trọng là nó chỉ có thể thực hiện một trong số họ (giống như bất kỳ lớp khác)! Điều này khác với tất cả các biểu thức khác, trong đó các kiểu đã triển khai đều được biết trước và trình biên dịch chỉ cần tìm ra liệu một trong số chúng có đáp ứng các yêu cầu ràng buộc hay không.
dùng31601
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.