Kế thừa và truy cập động vào các thành viên / thuộc tính và phương thức trong các ngôn ngữ giống như Java


7

Tôi có một câu hỏi về tính kế thừa trong các ngôn ngữ lập trình OO giống như Java. Nó xuất hiện trong lớp trình biên dịch của tôi, khi tôi giải thích cách biên dịch các phương thức và lời gọi của chúng. Tôi đã sử dụng Java làm ngôn ngữ nguồn mẫu để biên dịch.

Bây giờ hãy xem xét chương trình Java này.

class A {
  public int x = 0;
  void f () { System.out.println ( "A:f" ); } }

class B extends A {
  public int x = 1;
  void f () { System.out.println ( "B:f" ); } }

public class Main {
  public static void main ( String [] args ) {
    A a = new A ();
    B b = new B ();
    A ab = new B ();

    a.f();
    b.f();
    ab.f();
    System.out.println ( a.x );
    System.out.println ( b.x );
    System.out.println ( ab.x ); } }

Khi bạn chạy nó, bạn nhận được kết quả sau.

A:f
B:f
B:f
0
1 
0

Các trường hợp thú vị là những trường hợp xảy ra với đối tượng abcủa kiểu tĩnh A, là Bđộng. Như ab.f()in ra

B:f

nó tuân theo các yêu cầu phương thức không bị ảnh hưởng bởi kiểu thời gian biên dịch của đối tượng mà phương thức được gọi với. Nhưng System.out.println ( ab.x )in ra 0, vì vậy truy cập thành viên bị ảnh hưởng bởi các loại thời gian biên dịch.

Một sinh viên hỏi về sự khác biệt này: không nên truy cập của các thành viên và phương pháp phù hợp với nhau? Tôi không thể đưa ra một câu trả lời hay hơn "đó là ngữ nghĩa của Java".

Bạn có biết một lý do khái niệm rõ ràng tại sao các thành viên và phương pháp khác nhau theo nghĩa này không? Một cái gì đó tôi có thể cung cấp cho học sinh của tôi?

Chỉnh sửa : Sau khi điều tra thêm, đây dường như là một đặc điểm riêng của Java: C ++ và C # hoạt động khác nhau, xem ví dụ bình luận của Saeed Amiri bên dưới.

Chỉnh sửa 2 : Tôi vừa thử chương trình Scala tương ứng:

class A {
  val x = 0;
  def f () : Unit = { System.out.println ( "A:f" ); } }

class B extends A {
  override val x = 1;
  override def f () : Unit = { System.out.println ( "B:f" ); } }

object Main {
  def main ( args : Array [ String ] ) = {
    var a : A = new A ();
    var b : B = new B ();
    var ab : A = new B ();
    a.f();
    b.f();
    ab.f();
    System.out.println ( "a.x = " + a.x );
    System.out.println ( "b.x = " + b.x );
    System.out.println ( "ab.x = " + ab.x ); } }

Và tôi ngạc nhiên khi kết quả này trong

A:f
B:f
B:f
a.x = 0
b.x = 1
ab.x = 1

Lưu ý rằng các overrisesửa đổi là cần thiết. Điều này làm tôi ngạc nhiên vì Scala biên dịch thành JVM và hơn nữa, khi tôi biên dịch và thực thi chương trình Java ở đầu bằng trình biên dịch / thời gian chạy Scala, nó hoạt động giống như chương trình Java.


3
Điều này có liên quan đến ngôn ngữ lập trình của bạn, cách xử lý khởi tạo biến, nếu bạn làm như vậy trong C #: ideone.com/q6KpKk, bạn sẽ nhận được một kết quả khác, vấn đề của bạn là bạn đã ghi đè fx, không đề cập rõ ràng về điều này, phiên bản IMO C # sẽ tốt hơn ( dễ chấp nhận hơn trong quan điểm OO), thay vì thực hiện điều này, hãy xác định x, flà ảo và bạn sẽ nhận được kết quả nhất quán bằng cách ghi đè chúng vào B. Nhưng nếu bạn hỏi điều này trong Stackoverflow, bạn sẽ được chú ý nhiều hơn và tôi chắc chắn câu trả lời hay.

@SaeedAmiri cảm ơn vì C #, tôi mong nó sẽ giống như vậy ở đó. Nếu tôi không nhận được câu trả lời tốt ở đây, tôi sẽ chuyển sang Stackoverflow.
Martin Berger

Vui lòng qua một liên kết khi bạn vượt qua.
Nejc

1
@SaeedAmiri: Người ta không thể ghi đè các trường trong Java. Người ta chỉ có thể bóng các lĩnh vực.
Dave Clarke

Tôi có thể sai nhưng tôi đọc gần đây rằng Java chỉ cho phép "ghi đè phương thức" và "ghi đè trường" không được hỗ trợ. Vì vậy, khi bạn tạo một thể hiện của lớp A thì đối với trường, nó sẽ tìm định nghĩa gần nhất của thực thể x mà nó sẽ tìm thấy trong lớp A và từ đó có kết quả. Mọi người có thể sửa tôi nếu tôi sai ở đây.
Rajat Saxena

Câu trả lời:


6

Tôi nghi ngờ rằng điều này có liên quan mật thiết đến việc tạo bóng các trường trong Java.

Khi viết một lớp dẫn xuất, tôi có thể, như trong ví dụ của bạn, viết một trường xche mờ định nghĩa xtrong lớp cơ sở. Điều này có nghĩa là trong lớp dẫn xuất, định nghĩa ban đầu không còn có thể truy cập thông qua tên x. Lý do Java cho phép điều này là do các triển khai lớp dẫn xuất ít phụ thuộc vào các chi tiết triển khai của lớp cơ sở, do đó (chỉ một phần) tránh được vấn đề của lớp cơ sở dễ vỡ. Ví dụ, nếu lớp cơ sở ban đầu không có trường x, nhưng sau đó đã giới thiệu một lớp, ngữ nghĩa của Java sẽ tránh điều này phá vỡ việc triển khai lớp dẫn xuất.

Biến xtrong lớp dẫn xuất có thể có một loại hoàn toàn khác với biến trong lớp cơ sở - Tôi đã thử nghiệm điều này, nó hoạt động. Điều này trái ngược hoàn toàn với ghi đè phương thức, trong đó loại cần phải đủ tương thích (hay còn gọi là giống nhau). Vì vậy, khi tôi có một biến loại Atrong ví dụ của bạn, loại duy nhất tôi có thể lấy được cho nó là loại được chỉ định trong lớp A(hoặc ở trên). Vì điều này có thể khác với loại được khai báo trong lớp B, nó chỉ là loại an toàn để trả về giá trị của trường xtừ lớp A. (Và một cách tự nhiên, ngữ nghĩa phải giống nhau bất kể loại trường nào trong lớp B.)


Xin chào @DaveClarke giải thích của bạn tại sao ab.xnên trả về Athuộc tính của xcogent. Nhưng mà lý do rất tương tự cũng có giá trị cho các thành viên ở loại A, và thực sự C # và C ++ làm invoke Aphương pháp 's ftrong ab.f().
Martin Berger

2
Trong lịch sử, C ++ cho phép cả động (ảo) và công văn tĩnh. Các nhà thiết kế Java cho rằng điều này quá phức tạp và, theo các ngôn ngữ như Eiffel và Smalltalk, chỉ hỗ trợ ngữ nghĩa cho việc gửi phương thức. Một lần nữa, người ta có thể lập luận rằng điều này vừa đơn giản vừa tự nhiên hơn.
Dave Clarke

Xin chào @DaveClarke, tôi đồng ý rằng ảo hóa mọi phương pháp đều đơn giản hơn. Nhưng nó sẽ đồng đều hơn nữa nếu các thành viên cũng được ảo hóa (và sau đó không thể có các loại tùy ý). Rốt cuộc, người ta có thể thấy các phương thức là thành viên cấp cao hơn. Tôi không nói rằng đây là điều đúng đắn, nhưng tôi tự hỏi liệu có một lý do khái niệm nhỏ giọt nào để tạo ra sự khác biệt quyết liệt như vậy giữa các phương pháp và các thành viên.
Martin Berger

Cuối cùng các thành viên / lĩnh vực ảo hóa sẽ không thực sự có ý nghĩa gì. Sẽ có một trường xcho toàn bộ phân cấp lớp. Nó có nghĩa gì để ghi đè x? Đơn giản chỉ cần cho nó một giá trị mới. Vì vậy, vấn đề thực sự là, tại sao Java lại chọn tất cả các phương thức ảo và tạo bóng của các trường ?
Dave Clarke

Nhưng, @DaveClarke, ví dụ về Scala ở trên dường như cho tôi thấy rằng Scala thực sự ảo hóa các thành viên của nó (bạn phải cho overridehọ đạt được khả năng đánh máy. Vì vậy, một số ngôn ngữ đưa ra lựa chọn đó. trong không gian này?
Martin Berger

4

Trong Java, không có 'loại tĩnh của một đối tượng' - có loại đối tượng và có loại tham chiếu. Tất cả các phương thức Java là 'ảo' (để sử dụng thuật ngữ C ++), tức là chúng được giải quyết dựa trên loại đối tượng. Và btw. '@Override' không có tác dụng gì đối với mã được biên dịch - đó chỉ là một biện pháp bảo vệ tạo ra lỗi biên dịch nếu phương thức được chú thích không ghi đè lên phương thức siêu lớp '.

Mặt khác, quyền truy cập trường không bao giờ được giải quyết một cách linh hoạt - hay nói cách khác, Java không sử dụng đa hình khi truy cập các trường. Trong trường hợp (bệnh lý) rằng một lớp con có trường x có cùng tên với trường siêu lớp, lớp con có hai trường 'x'. Rõ ràng, có bóng tên - tên 'x' chỉ có thể được liên kết với một trong các trường này trong một bối cảnh nhất định.

Nhập cú pháp phân giải tên - khi bạn viết bx, trình biên dịch sẽ giải quyết trường này thành trường được khai báo trong các lớp con, nhưng khi bạn viết ab.x, trình biên dịch sẽ giải quyết trường đó được khai báo trong A - cả hai trường đều là thành viên của lớp B. có thể truy cập cả hai trường từ mã trong lớp B bằng super.x:

lớp B {... void g () {System.out.println ("cả hai:" + x + "và" + super.x);}}

Vì vậy, trong thời gian chạy, không có sự khác biệt nào cả cho dù trường 'lớp con được đặt tên là' x 'hay' y '- trong cả hai trường hợp, lớp con có hai trường phân biệt không có mối quan hệ giữa chúng.


Bạn đúng @Arno nhưng tại sao Java lại đưa ra những lựa chọn này và tại sao C # và Scala lại đưa ra những lựa chọn khác nhau? Có một lý do rõ ràng tốt cho việc này?
Martin Berger

1

Hãy nhớ rằng kế thừa là một hình thức nói B"là một" A, ngoại trừ chức năng của Acó thể được mở rộng, các biến thành viên được kế thừa có thể được coi là thuộc tính của đối tượng. Theo trực giác, cách chúng ta được dạy về kế thừa (được thừa nhận theo quan điểm của Java-ish), các biến thành viên giống như các thuộc tính và các thuộc tính chung không có nghĩa là được mở rộng, thay vào đó chỉ có chức năng.

Ví dụ: giả sử bạn có một Personlớp và người đó có một biến tên. Tên theo trực giác sẽ không được thay đổi, ngay cả khi chúng tôi mở rộng Personđến StudentProfessor. Tuy nhiên, các phương thức, ví dụ như Student.do_work()được mở rộng, để làm "bài tập về nhà", trong khi Professorlớp có thể chấm điểm kiểm tra Professor.do_work().

Tất nhiên bạn có thể phá vỡ trực giác này và về cơ bản bạn có thể tạo các biến thành viên tương đương với các thuộc tính hoặc sử dụng getters, có thể bị ghi đè, v.v., nhưng tôi nghĩ những gì tôi đã nói ở trên là trực giác làm theo cách này. Trong thực tế, tôi muốn nói rằng trong hầu hết các trường hợp, sẽ có một biến thành viên có cùng tên, trong một lớp dẫn xuất.

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.