Java8: Tại sao lại cấm định nghĩa một phương thức mặc định cho một phương thức từ java.lang.Object


130

Các phương thức mặc định là một công cụ mới tuyệt vời trong hộp công cụ Java của chúng tôi. Tuy nhiên, tôi đã cố gắng viết một giao diện xác định defaultphiên bản của toStringphương thức. Java nói với tôi rằng điều này bị cấm, vì các phương thức được khai báo java.lang.Objectcó thể không được chỉnh sửa default. Tại sao điều này là trường hợp?

Tôi biết rằng có quy tắc "lớp cơ sở luôn thắng", do đó, theo mặc định (chơi chữ;), bất kỳ defaultviệc thực hiện Objectphương thức nào cũng sẽ bị ghi đè bởi phương thức Object. Tuy nhiên, tôi thấy không có lý do tại sao không nên có ngoại lệ cho các phương thức Objecttrong thông số kỹ thuật. Đặc biệt là toStringnó có thể rất hữu ích để có một triển khai mặc định.

Vậy, lý do tại sao các nhà thiết kế Java quyết định không cho phép defaultcác phương thức ghi đè các phương thức từ đó là Objectgì?


1
Tôi cảm thấy rất tốt về bản thân mình bây giờ, nâng cấp nó lên 100 lần và do đó là một huy hiệu vàng. câu hỏi hay!
Eugene

Câu trả lời:


186

Đây là một vấn đề thiết kế ngôn ngữ khác có vẻ như "rõ ràng là một ý tưởng tốt" cho đến khi bạn bắt đầu đào và bạn nhận ra rằng đó thực sự là một ý tưởng tồi.

Thư này có rất nhiều về chủ đề (và cả các chủ đề khác nữa.) Có một số lực lượng thiết kế đã hội tụ để đưa chúng ta đến thiết kế hiện tại:

  • Mong muốn giữ cho mô hình thừa kế đơn giản;
  • Thực tế là một khi bạn nhìn qua các ví dụ rõ ràng (ví dụ: chuyển AbstractListsang giao diện), bạn nhận ra rằng việc kế thừa bằng / hashCode / toString được liên kết chặt chẽ với kế thừa và trạng thái duy nhất, và các giao diện được nhân rộng và không trạng thái;
  • Rằng nó có khả năng mở ra cánh cửa cho một số hành vi đáng ngạc nhiên.

Bạn đã chạm vào mục tiêu "giữ cho nó đơn giản"; các quy tắc kế thừa và giải quyết xung đột được thiết kế rất đơn giản (các lớp thắng giao diện, giao diện dẫn xuất thắng trên siêu giao diện và bất kỳ xung đột nào khác được giải quyết bởi lớp triển khai.) Tất nhiên các quy tắc này có thể được điều chỉnh để tạo ngoại lệ, nhưng Tôi nghĩ bạn sẽ tìm thấy khi bạn bắt đầu kéo theo chuỗi đó, rằng độ phức tạp gia tăng không nhỏ như bạn nghĩ.

Tất nhiên, có một số mức độ lợi ích sẽ chứng minh sự phức tạp hơn, nhưng trong trường hợp này nó không có ở đó. Các phương thức mà chúng ta đang nói ở đây là bằng, hashCode và toString. Các phương thức này thực chất là về trạng thái đối tượng và đó là lớp sở hữu trạng thái chứ không phải giao diện, là người có vị trí tốt nhất để xác định ý nghĩa bình đẳng nào cho lớp đó (đặc biệt là hợp đồng cho sự bình đẳng khá mạnh; xem Hiệu quả Java cho một số hậu quả đáng ngạc nhiên); giao diện người viết chỉ là quá xa.

Thật dễ dàng để lấy ra AbstractListví dụ; Sẽ thật đáng yêu nếu chúng ta có thể thoát khỏi AbstractListvà đưa hành vi vào Listgiao diện. Nhưng một khi bạn vượt ra ngoài ví dụ rõ ràng này, sẽ không có nhiều ví dụ hay khác được tìm thấy. Tại root, AbstractListđược thiết kế cho thừa kế duy nhất. Nhưng giao diện phải được thiết kế cho nhiều kế thừa.

Hơn nữa, hãy tưởng tượng bạn đang viết lớp này:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

Người Fooviết nhìn vào các siêu kiểu, thấy không có sự thực thi nào bằng và kết luận rằng để có được đẳng thức tham chiếu, tất cả những gì anh ta cần làm là thừa kế bằng Object. Sau đó, vào tuần tới, người bảo trì thư viện cho Bar "một cách hữu ích" sẽ thêm một cài đặt mặc định equals. Ôi trời! Bây giờ các ngữ nghĩa Foođã bị phá vỡ bởi một giao diện trong một miền bảo trì khác "một cách hữu ích" thêm một mặc định cho một phương thức phổ biến.

Mặc định được coi là mặc định. Thêm một mặc định vào một giao diện không có (bất kỳ nơi nào trong hệ thống phân cấp) sẽ không ảnh hưởng đến ngữ nghĩa của các lớp triển khai cụ thể. Nhưng nếu mặc định có thể "ghi đè" các phương thức Object, điều đó sẽ không đúng.

Vì vậy, mặc dù có vẻ như là một tính năng vô hại, nhưng thực tế nó lại rất có hại: nó làm tăng thêm sự phức tạp cho một chút biểu cảm gia tăng, và nó làm cho nó trở nên quá dễ dàng đối với những thay đổi có chủ đích, vô hại đối với các giao diện được biên dịch riêng biệt để làm suy yếu ngữ nghĩa dự định của các lớp thực hiện.


13
Tôi vui vì bạn đã dành thời gian để giải thích điều này, và tôi đánh giá cao tất cả các yếu tố đã được xem xét. Tôi đồng ý rằng điều này sẽ nguy hiểm cho hashCodeequals, nhưng tôi nghĩ nó sẽ rất hữu ích cho toString. Ví dụ, một số Displayablegiao diện có thể xác định một String display()phương pháp, và nó sẽ tiết kiệm một tấn soạn sẵn để có thể xác định default String toString() { return display(); }trong Displayable, thay vì yêu cầu mỗi đơn Displayableđể thực hiện toString()hoặc mở rộng một DisplayableToStringlớp cơ sở.
Brandon

8
@Brandon Bạn đã đúng khi cho phép kế thừa toString () sẽ không nguy hiểm theo cách tương tự như đối với bằng () và hashCode (). Mặt khác, bây giờ tính năng này thậm chí còn bất thường hơn - và bạn vẫn phải chịu tất cả sự phức tạp bổ sung tương tự về quy tắc thừa kế, vì lợi ích của phương pháp này ... có vẻ tốt hơn để vẽ đường thẳng một cách rõ ràng nơi chúng tôi đã làm .
Brian Goetz

5
@gexinf Nếu toString()chỉ dựa trên các phương thức giao diện, bạn có thể chỉ cần thêm một cái gì đó giống như default String toStringImpl()vào giao diện và ghi đè toString()trong mỗi lớp con để gọi việc thực hiện giao diện - hơi xấu, nhưng hoạt động và tốt hơn là không có gì. :) Một cách khác để làm điều đó là tạo ra một cái gì đó như Objects.hash(), Arrays.deepEquals()Arrays.deepToString(). + 1'd cho câu trả lời của @ BrianGoetz!
Siu Ching Pong -Asuka Kenji-

3
Hành vi mặc định của todaring () của lambda thực sự đáng ghét. Tôi biết nhà máy lambda được thiết kế rất đơn giản và nhanh chóng, nhưng việc đưa ra một tên lớp bị bỏ rơi thực sự không hữu ích. Có một default toString()ghi đè trong giao diện chức năng sẽ cho phép chúng ta - ít nhất-- làm một cái gì đó như nhổ chữ ký của hàm và lớp cha của người thực hiện. Thậm chí tốt hơn, nếu chúng ta có thể mang lại một số chiến lược toString đệ quy, chúng ta có thể đi qua việc đóng cửa để có được một mô tả thực sự tốt về lambda, và do đó cải thiện đáng kể đường cong học tập lambda.
Groostav

Mọi thay đổi đối với toString trong bất kỳ lớp, lớp con, thành viên thể hiện nào đều có thể có tác dụng đó trong việc triển khai các lớp hoặc người dùng của một lớp. Hơn nữa, bất kỳ thay đổi nào đối với bất kỳ phương thức mặc định nào cũng có thể ảnh hưởng đến tất cả các lớp thực hiện. Vậy điều gì đặc biệt với toString, hashCode khi nói đến việc ai đó thay đổi hành vi của giao diện? Nếu một lớp mở rộng một lớp khác, họ có thể thay đổi quá. Hoặc nếu họ đang sử dụng mô hình đoàn. Ai đó sử dụng giao diện Java 8 sẽ phải làm như vậy bằng cách nâng cấp. Một cảnh báo / lỗi có thể được loại bỏ trên lớp con có thể đã được cung cấp.
mmm

30

Nghiêm cấm định nghĩa các phương thức mặc định trong giao diện cho các phương thức java.lang.Object, vì các phương thức mặc định sẽ không bao giờ có thể truy cập được.

Các phương thức giao diện mặc định có thể được ghi đè trong các lớp thực hiện giao diện và việc thực hiện lớp của phương thức có độ ưu tiên cao hơn so với thực hiện giao diện, ngay cả khi phương thức được triển khai trong lớp bậc trên. Vì tất cả các lớp kế thừa từ java.lang.Object, các phương thức trong java.lang.Objectsẽ được ưu tiên hơn phương thức mặc định trong giao diện và được gọi thay thế.

Brian Goetz từ Oracle cung cấp thêm một vài chi tiết về quyết định thiết kế trong bài đăng danh sách gửi thư này .


3

Tôi không nhìn vào đầu của các tác giả ngôn ngữ Java, vì vậy chúng tôi chỉ có thể đoán. Nhưng tôi thấy nhiều lý do và đồng ý với họ hoàn toàn trong vấn đề này.

Lý do chính để giới thiệu các phương thức mặc định là để có thể thêm các phương thức mới vào các giao diện mà không phá vỡ tính tương thích ngược của các triển khai cũ. Các phương thức mặc định cũng có thể được sử dụng để cung cấp các phương thức "tiện lợi" mà không cần phải định nghĩa chúng trong mỗi lớp thực hiện.

Không có cái nào trong số này áp dụng cho toString và các phương thức khác của Object. Nói một cách đơn giản, các phương thức mặc định được thiết kế để cung cấp hành vi mặc định khi không có định nghĩa nào khác. Không cung cấp các triển khai sẽ "cạnh tranh" với các triển khai hiện có khác.

Quy tắc "lớp cơ sở luôn thắng" cũng có lý do vững chắc. Người ta cho rằng các lớp định nghĩa các triển khai thực, trong khi các giao diện xác định các cài đặt mặc định , có phần yếu hơn.

Ngoài ra, giới thiệu BẤT K ex ngoại lệ nào cho các quy tắc chung gây ra sự phức tạp không cần thiết và đưa ra các câu hỏi khác. Đối tượng là (ít nhiều) một lớp như bất kỳ lớp nào khác, vậy tại sao nó phải có hành vi khác nhau?

Tất cả và tất cả, giải pháp bạn đề xuất có thể sẽ mang lại nhiều khuyết điểm hơn ưu điểm.


Tôi đã không chú ý đến đoạn thứ hai của câu trả lời của gexinf khi đăng bài của tôi. Nó chứa một liên kết giải thích vấn đề chi tiết hơn.
Marwin

1

Lý do rất đơn giản, đó là vì Object là lớp cơ sở cho tất cả các lớp Java. Vì vậy, ngay cả khi chúng ta có phương thức của Object được định nghĩa là phương thức mặc định trong một số giao diện, nó sẽ vô dụng vì phương thức của Object sẽ luôn được sử dụng. Đó là lý do tại sao để tránh nhầm lẫn, chúng ta không thể có các phương thức mặc định đang ghi đè các phương thức lớp Object.


1

Để đưa ra một câu trả lời rất khoa trương, chỉ cấm định nghĩa một defaultphương thức cho một phương thức công khaijava.lang.Object . Có 11 phương pháp để xem xét, có thể được phân loại theo ba cách để trả lời câu hỏi này.

  1. Sáu trong số các Objectphương pháp không thể có defaultphương pháp, vì họ là finalvà không thể ghi đè tại tất cả: getClass(), notify(), notifyAll(), wait(), wait(long), và wait(long, int).
  2. Ba trong số các Objectphương pháp không thể có defaultphương pháp vì những lý do đưa ra ở trên bởi Brian Goetz: equals(Object), hashCode(), và toString().
  3. Hai trong số các Objectphương thức có thểdefaultcác phương thức, mặc dù giá trị của các giá trị mặc định đó là đáng nghi ngờ nhất: clone()finalize().

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }

.Vấn đề ở đây là gì? Bạn vẫn chưa trả lời phần TẠI SAO - "Vậy, lý do tại sao các nhà thiết kế Java quyết định không cho phép các phương thức mặc định ghi đè các phương thức từ Object?"
pro_cheats
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.