Đâ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.
- Vì
with
có 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 F
buộc trình biên dịch phải giải quyết R
trước mà không xem xét ràng buộc F extends Function<T,R>
. R
giải quyết (cụ thể hơn nhiều) String
mà sau đó có nghĩa là suy luận về F
thấ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 withX
dườ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 withX
phương pháp của bạn là F
chí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ó T
là gì , nhưng muốn chắc chắn rằng bất cứ nơi nào tôi sử dụng T
nó đề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ả". F
trong bạn withX
chỉ 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 with
phươ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 value
tươ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 with
và withX
. Đ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 Function
cho 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à:
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 R
thà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 và đầ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 và đầ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 m
là 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 R
thà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 và đầ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 và đầu ra . Tuy nhiên thời điểm này, là 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 .
Chúng tôi xác định một tập hợp con V
như sau:
Đưa ra một tập hợp các biến suy luận để giải quyết, hãy V
là 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 F
phụ thuộc vào R
, vì vậy V := {F, R}
.
Chúng tôi chọn một tập hợp con V
theo 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ứ V
sao cho i) cho tất cả i (1 ≤ i ≤ n)
, nếu αi
phụ 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ố j
sao 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}
.
Sử dụng ràng buộc thứ ba ( String <: R
), chúng tôi khởi tạo R = String
và kết hợp điều này vào tập ràng buộc của chúng tôi. R
bây giờ được giải quyết, và ràng buộc thứ hai có hiệu quả F <: Supplier<String>
.
Sử dụng ràng buộc thứ hai (sửa đổi), chúng tôi khởi tạo F = Supplier<String>
. F
hiệ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:
TypeInference::getLong
tương ứng với Supplier<String>
- ... giảm đến
Long
tương thích với String
- ... 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
Consumer
là 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ư withX
trên (chỉ cần thay thế Long
bằng Number
và String
bằ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 Consumer
bạ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, withX
cho 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 <: Number
thay vì Number <: R
như 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.