Tại sao các phương thức tĩnh và mặc định được thêm vào các giao diện trong Java 8 khi chúng ta đã có các lớp trừu tượng?


99

Trong Java 8, các giao diện có thể chứa các phương thức được triển khai, các phương thức tĩnh và các phương thức được gọi là "mặc định" (mà các lớp triển khai không cần ghi đè).

Theo quan điểm của tôi (có lẽ là ngây thơ), không cần phải vi phạm các giao diện như thế này. Giao diện luôn là một hợp đồng bạn phải thực hiện, và đây là một khái niệm rất đơn giản và thuần túy. Bây giờ nó là một kết hợp của một số điều. Theo ý kiến ​​của tôi:

  1. phương thức tĩnh không thuộc về giao diện. Chúng thuộc về các lớp tiện ích.
  2. Các phương thức "mặc định" không nên được cho phép trong các giao diện. Bạn luôn có thể sử dụng một lớp trừu tượng cho mục đích này.

Nói ngắn gọn:

Trước Java 8:

  • Bạn có thể sử dụng các lớp trừu tượng và thông thường để cung cấp các phương thức tĩnh và mặc định. Vai trò của giao diện rất rõ ràng.
  • Tất cả các phương thức trong một giao diện nên được ghi đè bằng cách thực hiện các lớp.
  • Bạn không thể thêm một phương thức mới trong một giao diện mà không sửa đổi tất cả các cài đặt, nhưng đây thực sự là một điều tốt.

Sau Java 8:

  • Hầu như không có sự khác biệt giữa một giao diện và một lớp trừu tượng (ngoài nhiều kế thừa). Trong thực tế, bạn có thể mô phỏng một lớp học thông thường với một giao diện.
  • Khi lập trình các triển khai, lập trình viên có thể quên ghi đè các phương thức mặc định.
  • Có lỗi biên dịch nếu một lớp cố gắng thực hiện hai hoặc nhiều giao diện có một phương thức mặc định có cùng chữ ký.
  • Bằng cách thêm một phương thức mặc định vào một giao diện, mọi lớp thực hiện sẽ tự động kế thừa hành vi này. Một số trong các lớp này có thể chưa được thiết kế với chức năng mới đó và điều này có thể gây ra vấn đề. Chẳng hạn, nếu ai đó thêm một phương thức mặc định mới default void foo()vào một giao diện Ix, thì lớp Cxtriển khai Ixvà có một foophương thức riêng có cùng chữ ký sẽ không biên dịch.

Các lý do chính cho những thay đổi lớn như vậy, và những lợi ích mới (nếu có) mà họ thêm vào là gì?


30
Câu hỏi thưởng: Tại sao họ không giới thiệu nhiều kế thừa cho các lớp thay thế?

2
phương thức tĩnh không thuộc về giao diện. Chúng thuộc về các lớp tiện ích. Không có họ thuộc @Deprecatedthể loại! các phương thức tĩnh là một trong những cấu trúc bị lạm dụng nhất trong Java, vì sự thiếu hiểu biết và lười biếng. Rất nhiều phương pháp tĩnh thường có nghĩa là lập trình viên không đủ năng lực, tăng khả năng ghép nối theo một số bậc độ lớn và là cơn ác mộng đối với bài kiểm tra đơn vị và cấu trúc lại khi bạn nhận ra tại sao chúng là một ý tưởng tồi!

11
@JarrodRoberson Bạn có thể cung cấp thêm một số gợi ý (liên kết sẽ rất tuyệt) về "là một cơn ác mộng đối với bài kiểm tra đơn vị và cấu trúc lại khi bạn nhận ra tại sao chúng là một ý tưởng tồi!"? Tôi chưa bao giờ nghĩ điều đó và tôi muốn biết thêm về nó.
chảy nước

11
@Chris Nhiều kế thừa trạng thái gây ra một loạt các vấn đề, đặc biệt là phân bổ bộ nhớ ( vấn đề kim cương cổ điển ). Tuy nhiên, nhiều kế thừa hành vi chỉ phụ thuộc vào việc thực hiện đáp ứng hợp đồng đã được thiết lập bởi giao diện (giao diện có thể gọi các phương thức khác mà nó tuyên bố và yêu cầu). Một sự khác biệt tinh tế, nhưng rất thú vị.
ssube

1
bạn muốn thêm phương thức vào giao diện hiện có, cồng kềnh hoặc gần như không thể trước java8 bây giờ bạn chỉ có thể thêm nó làm phương thức mặc định.
VdeX

Câu trả lời:


59

Một ví dụ tạo động lực tốt cho các phương thức mặc định là trong thư viện chuẩn Java, nơi bạn hiện có

list.sort(ordering);

thay vì

Collections.sort(list, ordering);

Tôi không nghĩ rằng họ có thể làm điều đó nếu không có nhiều hơn một triển khai giống hệt nhau List.sort.


19
C # khắc phục vấn đề này với Phương thức mở rộng.
Robert Harvey

5
và nó cho phép một danh sách được liên kết sử dụng một không gian thêm O (1) và hợp nhất thời gian O (n log n), bởi vì các danh sách được liên kết có thể được hợp nhất tại chỗ, trong java 7, nó chuyển sang một mảng bên ngoài và sau đó sắp xếp nó
ratchet freak

5
Tôi đã tìm thấy bài báo này , nơi Goetz giải thích vấn đề. Vì vậy, bây giờ tôi sẽ đánh dấu câu trả lời này là giải pháp.
Smith

1
@RobertHarvey: Tạo hai danh sách hàng trăm triệu <Byte>, sử dụng IEnumerable<Byte>.Appendđể tham gia với họ và sau đó gọi Count, sau đó cho tôi biết cách các phương thức mở rộng giải quyết vấn đề. Nếu CountIsKnownCountlà thành viên của IEnumerable<T>, lợi nhuận từ Appendcó thể quảng cáo CountIsKnownnếu các bộ sưu tập cấu thành đã làm, nhưng không có phương pháp như vậy thì không thể.
supercat

6
@supercat: Tôi không biết bạn đang nói về cái gì.
Robert Harvey

48

Câu trả lời đúng trong thực tế được tìm thấy trong Tài liệu Java , trong đó nêu rõ:

[d] phương thức efault cho phép bạn thêm chức năng mới vào giao diện của thư viện và đảm bảo khả năng tương thích nhị phân với mã được viết cho các phiên bản cũ hơn của các giao diện đó.

Đây là một nguồn đau đớn lâu dài trong Java, bởi vì các giao diện có xu hướng không thể phát triển một khi chúng được công khai. (Nội dung trong tài liệu có liên quan đến bài báo mà bạn đã liên kết trong một nhận xét: Tiến hóa giao diện thông qua các phương thức mở rộng ảo .) Ngoài ra, việc áp dụng nhanh chóng các tính năng mới (ví dụ: lambdas và API luồng mới) chỉ có thể được thực hiện bằng cách mở rộng giao diện bộ sưu tập hiện có và cung cấp triển khai mặc định. Phá vỡ tính tương thích nhị phân hoặc giới thiệu các API mới có nghĩa là vài năm nữa sẽ trôi qua trước khi các tính năng quan trọng nhất của Java 8 được sử dụng phổ biến.

Lý do cho phép các phương thức tĩnh trong các giao diện một lần nữa được tiết lộ bởi tài liệu: [t] của anh ấy giúp bạn dễ dàng tổ chức các phương thức trợ giúp trong thư viện của mình; bạn có thể giữ các phương thức tĩnh cụ thể cho một giao diện trong cùng một giao diện thay vì trong một lớp riêng biệt. Nói cách khác, các lớp tiện ích tĩnh như java.util.Collectionsbây giờ (cuối cùng) có thể được coi là một mô hình chống, nói chung (tất nhiên không phải luôn luôn ). Tôi đoán là việc thêm hỗ trợ cho hành vi này là không đáng kể một khi các phương thức mở rộng ảo được thực hiện, nếu không thì có lẽ nó đã không được thực hiện.

Trên một lưu ý tương tự, một ví dụ về cách các tính năng mới này có thể có ích là xem xét một lớp gần đây đã làm tôi khó chịu , java.util.UUID. Nó không thực sự cung cấp hỗ trợ cho các loại UUID 1, 2 hoặc 5 và không thể sửa đổi để làm điều đó. Nó cũng bị mắc kẹt với một trình tạo ngẫu nhiên được xác định trước không thể bị ghi đè. Việc triển khai mã cho các loại UUID không được hỗ trợ đòi hỏi phải phụ thuộc trực tiếp vào API của bên thứ ba thay vì giao diện, hoặc nếu không thì việc duy trì mã chuyển đổi và chi phí cho việc thu gom rác bổ sung đi kèm với nó. UUIDThay vào đó, với các phương thức tĩnh, có thể được định nghĩa là một giao diện, cho phép thực hiện bên thứ ba thực sự của các phần còn thiếu. (Nếu UUIDban đầu được định nghĩa là một giao diện, có lẽ chúng ta sẽ có một số loại lộn xộnUuidUtil lớp với các phương thức tĩnh, điều này cũng sẽ rất tệ.) Rất nhiều API cốt lõi của Java bị xuống cấp do không dựa trên các giao diện, nhưng kể từ Java 8, số lượng lý do cho hành vi xấu này đã bị giảm đáng kể.

Thật không đúng khi nói rằng [t] ở đây hầu như không có sự khác biệt giữa một giao diện và một lớp trừu tượng , bởi vì các lớp trừu tượng có thể có trạng thái (nghĩa là khai báo các trường) trong khi các giao diện không thể. Do đó, nó không tương đương với nhiều kế thừa hoặc thậm chí thừa kế kiểu mixin. Các mixin thích hợp (như các đặc điểm của Groovy 2.3 ) có quyền truy cập vào trạng thái. (Groovy cũng hỗ trợ các phương thức mở rộng tĩnh.)

Theo ý kiến ​​của tôi, theo ý kiến ​​của Doval cũng không phải là một ý kiến ​​hay. Một giao diện được cho là để xác định hợp đồng, nhưng nó không có nghĩa vụ phải thi hành hợp đồng. (Dù sao cũng không phải bằng Java.) Xác minh đúng việc triển khai là trách nhiệm của bộ thử nghiệm hoặc công cụ khác. Xác định hợp đồng có thể được thực hiện với các chú thích và OVal là một ví dụ điển hình, nhưng tôi không biết liệu nó có hỗ trợ các ràng buộc được xác định trên các giao diện hay không. Một hệ thống như vậy là khả thi, ngay cả khi hiện tại không tồn tại. (Chiến lược bao gồm tùy chỉnh thời gian biên dịch javacthông qua bộ xử lý chú thíchAPI và tạo mã byte thời gian chạy.) Lý tưởng nhất là các hợp đồng sẽ được thực thi vào thời gian biên dịch và trường hợp xấu nhất khi sử dụng bộ kiểm tra, nhưng tôi hiểu rằng việc thực thi thời gian chạy được chấp nhận. Một công cụ thú vị khác có thể hỗ trợ lập trình hợp đồng trong Java là Khung kiểm tra .


1
Để biết thêm theo dõi trên đoạn cuối cùng của tôi (tức là không thực hiện hợp đồng trong giao diện ), nó có giá trị chỉ ra rằng defaultphương pháp này không thể ghi đè lên equals, hashCodetoString. Một phân tích chi phí / lợi ích rất nhiều thông tin về lý do tại sao điều này không được phép có thể được tìm thấy ở đây: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/iêu
ngreen 6/10/2016

Thật tệ khi Java chỉ có một phương thức ảo equalsvà một hashCodephương thức duy nhất vì có hai loại công bằng khác nhau mà các bộ sưu tập có thể cần kiểm tra và các mục sẽ triển khai nhiều giao diện có thể bị mắc kẹt với các yêu cầu hợp đồng mâu thuẫn. Có thể sử dụng các danh sách sẽ không thay đổi vì hashMapcác khóa là hữu ích, nhưng cũng có những lúc sẽ hữu ích để lưu trữ các bộ sưu tập hashMapphù hợp với những thứ dựa trên trạng thái tương đương thay vì trạng thái hiện tại [tương đương ngụ ý trạng thái phù hợp và bất biến ] .
supercat

Vâng, loại Java có một cách giải quyết với các giao diện so sánh và so sánh. Nhưng đó là những thứ xấu xí, tôi nghĩ vậy.
ngreen

Các giao diện đó chỉ được hỗ trợ cho một số loại bộ sưu tập nhất định và chúng đặt ra vấn đề của riêng chúng: bộ so sánh có thể tự đóng gói trạng thái (ví dụ: bộ so sánh chuỗi chuyên dụng có thể bỏ qua số lượng ký tự có thể định cấu hình ở đầu mỗi chuỗi, trong trường hợp đó là số các ký tự bỏ qua sẽ là một phần của trạng thái của bộ so sánh) sẽ trở thành một phần của trạng thái của bất kỳ bộ sưu tập nào được sắp xếp theo nó, nhưng không có cơ chế xác định nào để hỏi hai bộ so sánh nếu chúng tương đương nhau.
supercat

Ồ vâng, tôi nhận được nỗi đau của so sánh. Tôi đang làm việc trên một cấu trúc cây nên đơn giản nhưng không phải vì rất khó để có được bộ so sánh đúng. Có lẽ tôi sẽ viết một lớp cây tùy chỉnh chỉ để làm cho vấn đề biến mất.
ngreen

44

Bởi vì bạn chỉ có thể kế thừa một lớp. Nếu bạn có hai giao diện có triển khai đủ phức tạp để bạn cần một lớp cơ sở trừu tượng, thì hai giao diện đó là loại trừ lẫn nhau trong thực tế.

Cách khác là chuyển đổi các lớp cơ sở trừu tượng đó thành một tập hợp các phương thức tĩnh và biến tất cả các trường thành đối số. Điều đó sẽ cho phép bất kỳ người triển khai giao diện nào gọi các phương thức tĩnh và có được chức năng, nhưng đó là rất nhiều bản tóm tắt trong một ngôn ngữ đã quá dài dòng.


Là một ví dụ động lực về lý do tại sao có thể cung cấp triển khai trong các giao diện có thể hữu ích, hãy xem xét giao diện Stack này:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Không có cách nào để đảm bảo rằng khi ai đó thực hiện giao diện, popsẽ đưa ra một ngoại lệ nếu ngăn xếp trống. Chúng ta có thể thực thi quy tắc này bằng cách tách popthành hai phương thức: một public finalphương thức thực thi hợp đồng và một protected abstractphương thức thực hiện popping thực tế.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

Chúng tôi không chỉ đảm bảo tất cả các triển khai tôn trọng hợp đồng, chúng tôi còn giải phóng họ khỏi việc phải kiểm tra xem ngăn xếp có trống không và ném ngoại lệ. Đó là một chiến thắng lớn! ... ngoại trừ việc chúng ta phải thay đổi giao diện thành một lớp trừu tượng. Trong một ngôn ngữ có sự kế thừa duy nhất, đó là một sự mất linh hoạt lớn. Nó làm cho giao diện của bạn sẽ loại trừ lẫn nhau. Có thể cung cấp các triển khai chỉ dựa vào chính các phương thức giao diện sẽ giải quyết được vấn đề.

Tôi không chắc cách tiếp cận của Java 8 trong việc thêm phương thức vào giao diện có cho phép thêm phương thức cuối cùng hoặc phương thức trừu tượng được bảo vệ hay không, nhưng tôi biết ngôn ngữ D cho phép và cung cấp hỗ trợ riêng cho Thiết kế theo Hợp đồng . Không có nguy hiểm trong kỹ thuật này vì poplà cuối cùng, vì vậy không có lớp thực hiện nào có thể ghi đè lên nó.

Đối với việc triển khai mặc định các phương thức có thể ghi đè, tôi giả sử mọi triển khai mặc định được thêm vào API Java chỉ dựa vào hợp đồng của giao diện mà chúng được thêm vào, do đó, bất kỳ lớp nào thực hiện chính xác giao diện cũng sẽ hoạt động chính xác với các triển khai mặc định.

Hơn thế nữa,

Hầu như không có sự khác biệt giữa một giao diện và một lớp trừu tượng (ngoài nhiều kế thừa). Trong thực tế, bạn có thể mô phỏng một lớp học thông thường với một giao diện.

Điều này không hoàn toàn đúng vì bạn không thể khai báo các trường trong giao diện. Bất kỳ phương pháp nào bạn viết trong giao diện đều không thể dựa vào bất kỳ chi tiết triển khai nào.


Như một ví dụ có lợi cho các phương thức tĩnh trong các giao diện, hãy xem xét các lớp tiện ích như Bộ sưu tập trong API Java. Lớp đó chỉ tồn tại bởi vì các phương thức tĩnh đó không thể được khai báo trong các giao diện tương ứng của chúng. Collections.unmodifiableListcũng có thể được khai báo trong Listgiao diện và nó sẽ dễ tìm thấy hơn.


4
Đối số phản đối: vì các phương thức tĩnh, nếu được viết đúng, khép kín, chúng có ý nghĩa hơn trong một lớp tĩnh riêng biệt nơi chúng có thể được thu thập và phân loại theo tên lớp và ít ý nghĩa hơn trong một giao diện, về cơ bản chúng là một tiện ích mời các hành vi lạm dụng như giữ trạng thái tĩnh trong đối tượng hoặc gây ra tác dụng phụ, làm cho các phương thức tĩnh không thể kiểm chứng được.
Robert Harvey

3
@RobertHarvey Điều gì ngăn bạn làm những điều ngớ ngẩn không kém nếu phương thức tĩnh của bạn nằm trong một lớp? Ngoài ra, phương thức trong giao diện có thể không yêu cầu bất kỳ trạng thái nào cả. Bạn có thể chỉ đơn giản là đang cố gắng thực thi một hợp đồng. Giả sử bạn đã có một Stackgiao diện và bạn muốn đảm bảo rằng khi popđược gọi với một ngăn xếp trống, một ngoại lệ sẽ được đưa ra. Đưa ra các phương pháp trừu tượng boolean isEmpty()protected T pop_impl(), bạn có thể thực hiện final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Điều này thực thi hợp đồng trên TẤT CẢ những người thực hiện.
Doval

Đợi đã, cái gì? Phương pháp đẩy và Pop trên ngăn xếp sẽ không được static.
Robert Harvey

@RobertHarvey Tôi sẽ rõ ràng hơn nếu không giới hạn ký tự cho các bình luận, nhưng tôi đã tạo ra trường hợp cho việc triển khai mặc định trong một giao diện, không phải các phương thức tĩnh.
Doval

8
Tôi nghĩ rằng các phương thức giao diện mặc định là một bản hack đã được giới thiệu để có thể mở rộng thư viện chuẩn mà không cần phải điều chỉnh mã hiện có dựa trên nó.
Giorgio

2

Có lẽ mục đích là cung cấp khả năng tạo các lớp mixin bằng cách thay thế nhu cầu tiêm thông tin tĩnh hoặc chức năng thông qua một phụ thuộc.

Ý tưởng này dường như liên quan đến cách bạn có thể sử dụng các phương thức mở rộng trong C # để thêm chức năng đã triển khai vào giao diện.


1
Các phương thức mở rộng không thêm chức năng cho các giao diện. Các phương thức mở rộng chỉ là đường cú pháp để gọi các phương thức tĩnh trên một lớp bằng cách sử dụng list.sort(ordering);biểu mẫu thuận tiện .
Robert Harvey

Nếu bạn nhìn vào IEnumerablegiao diện trong C #, bạn có thể thấy cách triển khai các phương thức mở rộng cho giao diện đó (giống như LINQ to Objects) thêm chức năng cho mọi lớp thực hiện IEnumerable. Đó là những gì tôi muốn nói bằng cách thêm chức năng.
rae1

2
Đó là điều tuyệt vời về phương pháp mở rộng; họ ảo tưởng rằng bạn đang tăng cường chức năng cho một lớp hoặc giao diện. Đừng nhầm lẫn rằng với việc thêm các phương thức thực tế vào một lớp; các phương thức lớp có quyền truy cập vào các thành viên riêng của một đối tượng, các phương thức mở rộng không (vì chúng thực sự chỉ là một cách khác để gọi các phương thức tĩnh).
Robert Harvey

2
Chính xác, và đó là lý do tại sao tôi thấy một số mối quan hệ về việc có các phương thức tĩnh hoặc mặc định trong một giao diện trong Java; việc thực hiện dựa trên những gì có sẵn cho giao diện chứ không phải chính lớp đó.
rae1

1

Hai mục đích chính tôi thấy trong defaultcác phương thức (một số trường hợp sử dụng phục vụ cả hai mục đích):

  1. Cú pháp đường. Một lớp tiện ích có thể phục vụ mục đích đó, nhưng các phương thức cá thể đẹp hơn.
  2. Mở rộng giao diện hiện có. Việc thực hiện là chung chung nhưng đôi khi không hiệu quả.

Nếu đó chỉ là mục đích thứ hai, bạn sẽ không thấy điều đó trong một giao diện hoàn toàn mới Predicate. Tất cả các @FunctionalInterfacegiao diện chú thích được yêu cầu phải có chính xác một phương thức trừu tượng để lambda có thể thực hiện nó. Bổ sung defaultcác phương pháp như and, or, negatechỉ là tiện ích, và bạn đang không được phép ghi đè lên chúng. Tuy nhiên, đôi khi các phương thức tĩnh sẽ làm tốt hơn .

Đối với việc mở rộng các giao diện hiện có - ngay cả ở đó, một số phương thức mới chỉ là cú pháp đường. Các phương pháp Collectionnhư stream, forEach, removeIf- về cơ bản, nó chỉ là tiện ích bạn không cần phải ghi đè. Và sau đó có những phương pháp như thế spliterator. Việc triển khai mặc định là tối ưu, nhưng ít nhất, mã biên dịch. Chỉ dùng đến điều này nếu giao diện của bạn đã được xuất bản và sử dụng rộng rãi.


Đối với các staticphương thức, tôi đoán các phương thức khác bao quát nó khá tốt: Nó cho phép giao diện là lớp tiện ích của riêng nó. Có lẽ chúng ta có thể thoát khỏi Collectionstương lai của Java? Set.empty()sẽ đá.

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.