Tại sao việc gọi một phương thức tĩnh không phải là một trường hợp là một lỗi đối với trình biên dịch Java?


76

Tôi chắc rằng tất cả các bạn đều biết hành vi mà tôi muốn nói - mã chẳng hạn như:

Thread thread = new Thread();
int activeCount = thread.activeCount();

kích hoạt cảnh báo trình biên dịch. Tại sao nó không phải là một lỗi?

BIÊN TẬP:

Nói rõ hơn: câu hỏi không liên quan gì đến Chủ đề. Tôi nhận thấy rằng các ví dụ về Chủ đề thường được đưa ra khi thảo luận về vấn đề này vì khả năng thực sự làm rối tung mọi thứ với chúng. Nhưng thực sự vấn đề là cách sử dụng như vậy luôn vô nghĩa và bạn không thể (thành thạo) viết một cuộc gọi như vậy và có nghĩa là nó. Bất kỳ ví dụ nào về kiểu gọi phương thức này sẽ là vô nghĩa. Đây là một cái khác:

String hello = "hello";
String number123AsString = hello.valueOf(123);

Điều này làm cho nó trông như thể mỗi cá thể Chuỗi đi kèm với một phương thức "String valueOf (int i)".


15
Để mở rộng quan điểm của bạn, trường hợp thừa thậm chí có thể là null: String hello = null; hello.valueOf(123);works!
Thilo

Câu trả lời:


79

Về cơ bản, tôi tin rằng các nhà thiết kế Java đã mắc lỗi khi họ thiết kế ngôn ngữ và đã quá muộn để sửa nó do các vấn đề tương thích liên quan. Có, nó có thể dẫn đến mã rất sai lệch. Có, bạn nên tránh nó. Có, bạn nên đảm bảo rằng IDE của bạn được định cấu hình để coi nó như một lỗi, IMO. Nếu bạn đã bao giờ tự thiết kế một ngôn ngữ, hãy ghi nhớ nó như một ví dụ về loại điều nên tránh :)

Chỉ để đáp lại quan điểm của DJClayworth, đây là những gì được phép trong C #:

public class Foo
{
    public static void Bar()
    {
    }
}

public class Abc
{
    public void Test()
    {
        // Static methods in the same class and base classes
        // (and outer classes) are available, with no
        // qualification
        Def();

        // Static methods in other classes are available via
        // the class name
        Foo.Bar();

        Abc abc = new Abc();

        // This would *not* be legal. It being legal has no benefit,
        // and just allows misleading code
        // abc.Def();
    }

    public static void Def()
    {
    }
}

Tại sao tôi nghĩ nó gây hiểu lầm? Bởi vì nếu tôi nhìn vào mã, someVariable.SomeMethod()tôi mong đợi nó sẽ sử dụng giá trị củasomeVariable . Nếu SomeMethod()là một phương thức tĩnh, kỳ vọng đó không hợp lệ; mã đang lừa tôi. Làm thế nào đó có thể là một điều tốt ?

Thật kỳ lạ, Java sẽ không cho phép bạn sử dụng một biến chưa được khởi tạo tiềm năng để gọi một phương thức tĩnh, mặc dù thực tế là thông tin duy nhất mà nó sẽ sử dụng là kiểu được khai báo của biến. Đó là một mớ hỗn độn không nhất quán và vô ích. Tại sao cho phép nó?

CHỈNH SỬA: Chỉnh sửa này là một phản hồi cho câu trả lời của Clayton, trong đó tuyên bố rằng nó cho phép kế thừa các phương thức tĩnh. Nó không. Các phương thức tĩnh không phải là đa hình. Đây là một chương trình ngắn nhưng đầy đủ để chứng minh rằng:

class Base
{
    static void foo()
    {
        System.out.println("Base.foo()");
    }
}

class Derived extends Base
{
    static void foo()
    {
        System.out.println("Derived.foo()");
    }
}

public class Test
{
    public static void main(String[] args)
    {
        Base b = new Derived();
        b.foo(); // Prints "Base.foo()"
        b = null;
        b.foo(); // Still prints "Base.foo()"
    }
}

Như bạn có thể thấy, giá trị thời gian thực thi của bhoàn toàn bị bỏ qua.


Tôi nghĩ rằng bạn có thể thiếu một cái gì đó. Xem bài viết của tôi.
DJClayworth

1
@DJClayworth: Không. Điều đó được phép trong C #, nhưng someInstanceOfMyClass.doComplexCalculation () thì không.
Jon Skeet

5
@Jon: Tôi đã chạy một thử nghiệm tương tự và xác nhận phát hiện của bạn. Dựa trên điều đó, tôi đồng ý rằng việc cho phép các phương thức tĩnh được gọi từ một var instance là sai đối với Java. Không có khả năng nào đạt được (như tôi nghĩ ban đầu), nhưng khả năng nhầm lẫn hoặc lỗi sẽ tăng lên.
Clayton

Giá trị thời gian thực thi của b hoàn toàn bị bỏ qua vì bạn đang gọi một phương thức tĩnh, phương thức này chỉ phụ thuộc vào lớp của b chứ không phải giá trị của nó. Tất cả các giá trị của kiểu Base có cùng một phương thức. Bạn đã khai báo Cơ sở b nên b là Cơ sở, không phải Bắt nguồn. Nếu bạn đã khai báo Derived b, nó sẽ in ra "Derived.foo ()".
Matthew

@Matthew: Vâng, tôi biết ... Tôi không chắc bạn đang cố gắng thực hiện điểm nào. (Cá nhân tôi sẽ không nói rằng "tất cả các giá trị của kiểu Base đều có cùng một phương thức" - một trường hợp không "có" một phương thức nào cả ... nhưng tôi nghĩ tôi hiểu ý bạn.)
Jon Skeet

15

Tại sao nó phải là một lỗi? Cá thể có quyền truy cập vào tất cả các phương thức tĩnh. Các phương thức tĩnh không thể thay đổi trạng thái của phiên bản (cố gắng một lỗi biên dịch).

Vấn đề với ví dụ nổi tiếng mà bạn đưa ra rất cụ thể đối với các luồng , không phải các cuộc gọi phương thức tĩnh. Có vẻ như bạn đang nhận được activeCount()chuỗi được gọi bởi thread, nhưng bạn đang thực sự nhận được số lượng cho chuỗi đang gọi. Đây là một lỗi logic mà bạn với tư cách là một lập trình viên đang mắc phải. Đưa ra một cảnh báo là điều thích hợp để trình biên dịch làm trong trường hợp này. Việc lưu ý đến cảnh báo và sửa mã của bạn là tùy thuộc vào bạn.

CHỈNH SỬA: Tôi nhận ra rằng cú pháp của ngôn ngữ là thứ cho phép bạn viết mã gây hiểu lầm, nhưng hãy nhớ rằng trình biên dịch và các cảnh báo của nó cũng là một phần của ngôn ngữ. Ngôn ngữ này cho phép bạn làm điều gì đó mà trình biên dịch cho là đáng ngờ, nhưng nó cung cấp cho bạn cảnh báo để đảm bảo rằng bạn biết rằng nó có thể gây ra sự cố.


9
Nó không liên quan đến trường hợp . Điều duy nhất là kiểu khai báo của biến. Đó là một sai lầm khủng khiếp, khủng khiếp và là sai lầm của các nhà thiết kế ngôn ngữ, IMO.
Jon Skeet

1
Có, nó không liên quan đến phiên bản, nhưng tôi không nghĩ rằng việc gọi một phương thức tĩnh từ một phiên bản phải là một lỗi chính thức. Nó chỉ là phong cách thực sự tồi tệ. IMHO, một lớp phải là tất cả các phương thức tĩnh hoặc các phương thức tĩnh phải là riêng tư, nhưng tôi không muốn trình biên dịch thực thi điều đó.
Bill the Lizard

1
@Bill: Tôi không nghĩ nó thậm chí còn đưa ra cảnh báo trong những ngày đầu. Cảnh báo đã được thêm vào vì nó đã gây hiểu lầm cho mọi người. Là gì lợi ích trong việc cho phép nó?
Jon Skeet

4
@Bill: Nói theo cách này - nếu Java không cho phép và ai đó gợi ý rằng nó nên cho phép, phản ứng của bạn sẽ như thế nào? Bạn có thực sự tranh luận ủng hộ nó không? Hay bạn sẽ tranh luận (như tôi) rằng đó là một tính năng vô nghĩa mà không cần thiết cho phép mã gây hiểu lầm?
Jon Skeet

2
Tôi không mua lý do "thời gian biên dịch" - rõ ràng là bạn đang tìm kiếm phương pháp thông qua tham chiếu; sau đó mất bao lâu để kiểm tra xem nó có tĩnh không? Tôi nghĩ sẽ dễ dàng hơn nếu chỉ coi đó là một lỗi thiết kế ngôn ngữ.
Jon Skeet

9

Họ không thể làm cho nó thành lỗi nữa, vì tất cả mã đã có sẵn ở đó.

Tôi với bạn rằng nó sẽ là một lỗi. Có lẽ nên có một tùy chọn / hồ sơ cho trình biên dịch để nâng cấp một số cảnh báo về lỗi.

Cập nhật: Khi họ giới thiệu từ khóa khẳng định trong 1.4, từ khóa này có các vấn đề tương thích tiềm ẩn tương tự với mã cũ, họ chỉ cung cấp từ khóa đó nếu bạn đặt rõ ràng chế độ nguồn thành "1.4" . Tôi cho rằng người ta có thể tạo ra lỗi trong chế độ nguồn mới "java 7". Nhưng tôi nghi ngờ họ sẽ làm điều đó, vì tất cả những rắc rối mà nó sẽ gây ra. Như những người khác đã chỉ ra, không nhất thiết phải ngăn bạn viết mã gây nhầm lẫn. Và các thay đổi ngôn ngữ đối với Java nên được giới hạn ở mức cần thiết nghiêm ngặt vào thời điểm này.


Có tùy chọn như trong Eclipse biên dịch (indirectStatic, help.eclipse.org/help33/index.jsp?topic=/... )
Peter Štibraný

1
Nhưng họ đã làm điều này khi giới thiệu từ khóa khẳng định trong Java 1.4. Tại sao họ không thể làm lại theo cách cũ?
Hosam Aly

Bạn đúng. Đối với Assert, bạn phải đặt cụ thể mức trình biên dịch là 1.4, nếu không nó vẫn biên dịch mà không cần xác nhận. Tôi cho rằng 1,5 chú thích và generic hoạt động giống nhau. Vì vậy, người ta có thể có mức biên dịch Java 7, điều đó sẽ khiến nó bị lỗi.
Thilo

6

Câu trả lời ngắn gọn - ngôn ngữ cho phép nó, vì vậy nó không phải là một lỗi.


2

Có khả năng cùng một lôgic khiến điều này không phải là lỗi:

public class X
{
    public static void foo()
    {
    }

    public void bar()
    {
        foo(); // no need to do X.foo();
    }
}

2

Điều thực sự quan trọng, từ quan điểm của trình biên dịch, là nó có thể phân giải các ký hiệu. Trong trường hợp của một phương thức tĩnh, nó cần biết lớp nào để tìm kiếm nó - vì nó không được liên kết với bất kỳ đối tượng cụ thể nào. Các nhà thiết kế của Java rõ ràng đã quyết định rằng vì họ có thể xác định lớp của một đối tượng, họ cũng có thể phân giải lớp của bất kỳ phương thức tĩnh nào cho đối tượng đó từ bất kỳ trường hợp nào của đối tượng. Họ chọn cho phép điều này - có lẽ là do sự quan sát của @ TofuBeer - để mang lại sự tiện lợi cho lập trình viên. Các nhà thiết kế ngôn ngữ khác đã có những lựa chọn khác nhau. Tôi có lẽ đã rơi vào trại thứ hai, nhưng nó không phải là vấn đề lớn đối với tôi. Tôi có thể sẽ cho phép sử dụng mà @TofuBeer đề cập,


2

Đó không phải là một lỗi vì nó là một phần của thông số kỹ thuật, nhưng rõ ràng là bạn đang hỏi về cơ sở lý luận, điều mà tất cả chúng ta đều có thể đoán được.

Tôi đoán rằng nguồn gốc của điều này thực sự là để cho phép một phương thức trong một lớp gọi một phương thức tĩnh trong cùng một lớp mà không gặp rắc rối. Vì việc gọi x () là hợp pháp (ngay cả khi không có tên tự lớp), nên việc gọi this.x () cũng phải hợp pháp và do đó việc gọi thông qua bất kỳ đối tượng nào cũng được coi là hợp pháp.

Điều này cũng giúp khuyến khích người dùng chuyển các chức năng riêng tư thành tĩnh nếu họ không thay đổi trạng thái.

Bên cạnh đó, các trình biên dịch thường cố gắng tránh khai báo lỗi khi không có cách nào dẫn đến lỗi trực tiếp. Vì một phương thức tĩnh không thay đổi trạng thái hoặc quan tâm đến đối tượng đang gọi, nó không gây ra lỗi thực tế (chỉ là nhầm lẫn) cho phép điều này. Một cảnh báo là đủ.


2
@Uri: Thật dễ dàng để cung cấp nó trong lớp hiện tại mà không cần chỉ định tên lớp. C # quản lý nó và các trình biên dịch Java quản lý để phân biệt sự khác biệt (vì bạn sẽ không nhận được cảnh báo trong trường hợp đó) vậy tại sao nó phải được chỉ định theo cách này?
Jon Skeet

2

Mục đích của tham chiếu biến cá thể là chỉ để cung cấp kiểu bao quanh tĩnh. Nếu bạn nhìn vào mã byte gọi một tĩnh thông qua instance.staticMethod hoặc EnclosingClass.staticMethod sẽ tạo ra cùng một mã lệnh gọi phương thức tĩnh. Không có tham chiếu đến cá thể xuất hiện.

Câu trả lời cũng như tại sao nó ở trong đó, nó chỉ là. Miễn là bạn sử dụng lớp học. và không thông qua một phiên bản, bạn sẽ giúp tránh nhầm lẫn trong tương lai.


1

Có thể bạn có thể thay đổi nó trong IDE của mình (trong Tùy chọn Eclipse -> Java -> Trình biên dịch -> Lỗi / Cảnh báo)


0

Không có tùy chọn cho nó. Trong java (giống như nhiều ngôn ngữ khác), bạn có thể có quyền truy cập vào tất cả các thành viên tĩnh của một lớp thông qua tên lớp của nó hoặc đối tượng cá thể của lớp đó. Điều đó sẽ tùy thuộc vào bạn và trường hợp của bạn và giải pháp phần mềm mà bạn nên sử dụng cái nào giúp bạn dễ đọc hơn.


0

Đó là chủ đề khá cũ nhưng vẫn được cập nhật và đáng ngạc nhiên là mang lại tác động cao hơn hiện nay. Như Jon đã đề cập, đó có thể chỉ là một sai lầm mà các nhà thiết kế của Java đã mắc phải ngay từ đầu. Nhưng tôi sẽ không tưởng tượng trước khi nó có thể ảnh hưởng đến an ninh.

Nhiều lập trình viên biết Apache Velocity, công cụ mẫu linh hoạt và mạnh mẽ. Nó mạnh đến mức cho phép cung cấp mẫu với một tập hợp các đối tượng được đặt tên - được coi là đối tượng từ ngôn ngữ lập trình (Java ban đầu). Các đối tượng đó có thể được truy cập từ bên trong khuôn mẫu giống như trong ngôn ngữ lập trình, ví dụ: cá thể Chuỗi của Java có thể được sử dụng với tất cả các trường, thuộc tính và phương thức chung của nó

$input.isEmpty()

trong đó đầu vào là một Chuỗi , chạy trực tiếp qua JVM và trả về true hoặc false cho đầu ra của trình phân tích cú pháp Velocity). Càng xa càng tốt.

Nhưng trong Java, tất cả các đối tượng đều kế thừa từ Object nên người dùng cuối của chúng tôi cũng có thể đưa nó vào mẫu

$input.getClass()

để lấy một thể hiện của Lớp chuỗi .

Và với tham chiếu này, họ cũng có thể gọi một phương thức tĩnh forName (String) trên

$input.getClass().forName("java.io.FileDescriptor")

sử dụng bất kỳ tên lớp nào và sử dụng nó cho bất kỳ tài khoản nào của máy chủ web có thể làm (deface, đánh cắp nội dung DB, kiểm tra tệp cấu hình, ...)

Khai thác này bằng cách nào đó (trong ngữ cảnh cụ thể) được mô tả ở đây: https://github.com/veracode-research/solr-injection#7-cve-2019-17558-rce-via-velocity-template-by-_s00py

Sẽ không thể thực hiện được nếu việc gọi các phương thức tĩnh từ tham chiếu đến thể hiện của lớp bị cấm.

Tôi không nói rằng một khung lập trình cụ thể tốt hơn một khung lập trình khác nhưng tôi chỉ muốn so sánh. Có một cổng Apache Velocity cho .NET. Trong C #, không thể gọi các phương thức tĩnh chỉ từ tham chiếu của instance, điều làm cho việc khai thác như thế này trở nên vô dụng:

$input.GetType().GetType("System.IO.FileStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

-9

Tôi chỉ xem xét điều này:

instanceVar.staticMethod();

viết tắt cho điều này:

instanceVar.getClass().staticMethod();

Nếu bạn luôn phải làm điều này:

SomeClass.staticMethod();

thì bạn sẽ không thể tận dụng tính kế thừa cho các phương thức tĩnh.

Tức là, bằng cách gọi phương thức static thông qua instance, bạn không cần biết instance đó là lớp cụ thể nào tại thời điểm biên dịch, chỉ biết rằng nó thực hiện staticMethod () ở đâu đó dọc theo chuỗi kế thừa.

CHỈNH SỬA: Câu trả lời này là sai. Xem bình luận để biết chi tiết.


1
Tốt, đó là một lý do khác tại sao nó không nên được phép - bởi vì bạn cũng đang bị lừa. Nó không hoạt động như bạn muốn. Nó sẽ chỉ sử dụng loại "instanceVar" đã khai báo trong ví dụ của bạn. Rất tiếc, giá trị của instanceVar thậm chí có thể là null.
Jon Skeet

1
@Clayton: Có, nhưng nó không gọi getClass (). Tôi sẽ chỉnh sửa câu trả lời của mình với một ví dụ.
Jon Skeet

3
(Và nếu nó đã làm getClass call (), nó có thể không sau đó gọi staticMethod (), bởi vì đó không phải là một phương thức trên lớp <T>.)
Jon Skeet

5
@Jon: Tôi sẽ để nó ở đây. Tôi học được nhiều điều từ những sai lầm như bất cứ điều gì khác, vì vậy nó có thể giúp người khác nhìn ra điều này. Cảm ơn bạn đã giúp đỡ.
Clayton

3
@Clayton: Bạn có thể muốn tạo wiki cộng đồng này để chúng tôi có thể từ chối nó mà không phạt bạn.
Bill the Lizard
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.