Điều gì thực sự sẽ xảy ra nếu java.lang.String không kết thúc?


16

Tôi là một nhà phát triển Java lâu năm và cuối cùng, sau khi học chuyên ngành, tôi có thời gian nghiên cứu về nó để thực hiện bài kiểm tra chứng chỉ ... Một điều luôn làm phiền tôi là String là "cuối cùng". Tôi hiểu điều đó khi đọc về các vấn đề bảo mật và những thứ liên quan ... Nhưng, nghiêm túc, có ai có một ví dụ thực sự về điều đó không?

Chẳng hạn, điều gì sẽ xảy ra nếu String không kết thúc? Giống như nó không có trong Ruby. Tôi chưa nghe thấy bất kỳ khiếu nại nào đến từ cộng đồng Ruby ... Và tôi biết về StringUtils và các lớp liên quan mà bạn phải tự thực hiện hoặc tìm kiếm trên web để thực hiện hành vi đó (4 dòng mã) cho bạn sẵn sàng.


9
Bạn có thể quan tâm đến câu hỏi này trên Stack Overflow: stackoverflow.com/questions/2068804/why-is-opes-final-in-java
Thomas Owens

Tương tự, điều gì sẽ xảy ra nếu java.io.Filekhông final. Ồ, không phải vậy. Bugger.
Tom Hawtin - tackline

1
Sự kết thúc của nền văn minh phương Tây như chúng ta biết.
Tulains Córdova

Câu trả lời:


22

Lý do chính là tốc độ: finalcác lớp không thể được mở rộng cho phép JIT thực hiện tất cả các loại tối ưu hóa khi xử lý chuỗi - không bao giờ cần phải kiểm tra các phương thức được ghi đè.

Một lý do khác là sự an toàn của luồng: Bất biến luôn luôn là luồng an toàn vì một luồng phải hoàn toàn xây dựng chúng trước khi chúng có thể được chuyển cho người khác - và sau khi xây dựng, chúng không thể thay đổi được nữa.

Ngoài ra, các nhà phát minh của thời gian chạy Java luôn muốn sai sót về mặt an toàn. Có thể mở rộng Chuỗi (điều tôi thường làm trong Groovy vì nó rất tiện lợi) có thể mở cả một hộp giun nếu bạn không biết bạn đang làm gì.


2
Đó là một câu trả lời không chính xác. Khác với xác minh theo yêu cầu của đặc tả JVM, HotSpot chỉ sử dụng finalnhư một kiểm tra nhanh rằng phương thức không bị ghi đè khi biên dịch mã.
Tom Hawtin - tackline

1
@ TomHawtin-tackline: Tài liệu tham khảo?
Aaron Digulla

1
Bạn dường như ngụ ý rằng sự bất biến và là cuối cùng có liên quan đến nhau. "Một lý do khác là an toàn luồng: Bất biến luôn là luồng an toàn b ...". Lý do một đối tượng là bất biến hay không không phải là vì chúng là hoặc không phải là cuối cùng.
Tulains Córdova

1
Ngoài ra, lớp String bất biến đối với finaltừ khóa trên lớp. Mảng ký tự mà nó chứa cuối cùng, làm cho nó bất biến. Làm cho chính lớp cuối cùng đóng vòng lặp như @able đề cập vì một lớp con có thể thay đổi không thể được giới thiệu.

1
@Snowman Nói chính xác, mảng ký tự là cuối cùng chỉ làm cho tham chiếu mảng không thay đổi, không phải nội dung của mảng.
Michał Kosmulski

10

Có một lý do khác tại sao Stringlớp của Java cần phải là cuối cùng: điều quan trọng là bảo mật trong một số tình huống. Nếu Stringlớp không phải là cuối cùng, đoạn mã sau sẽ dễ bị tấn công tinh vi bởi một người gọi độc hại:

 public String makeSafeLink(String url, String text) {
     if (!url.startsWith("http:") && !url.startsWith("https:"))
         throw SecurityException("only http/https URLs are allowed");
     return "<a href=\"" + escape(url) + "\">" + escape(text) + "</a>";
 }

Cuộc tấn công: một người gọi độc hại có thể tạo ra một lớp con của Chuỗi EvilString, trong đó EvilString.startsWith()luôn trả về đúng nhưng trong đó giá trị của EvilStringlà một cái gì đó xấu (ví dụ javascript:alert('xss'):). Do phân lớp, điều này sẽ trốn tránh kiểm tra bảo mật. Cuộc tấn công này được gọi là lỗ hổng thời gian kiểm tra thời gian sử dụng (TOCTTOU): giữa thời điểm kiểm tra được thực hiện (rằng url bắt đầu bằng http / https) và thời điểm sử dụng giá trị (để xây dựng đoạn mã html), giá trị hiệu quả có thể thay đổi. Nếu Stringkhông phải là cuối cùng, thì rủi ro TOCTTOU sẽ lan rộng.

Vì vậy, nếu Stringkhông phải là cuối cùng, việc viết mã bảo mật sẽ trở nên khó khăn nếu bạn không thể tin tưởng vào người gọi. Tất nhiên, đó chính xác là vị trí mà các thư viện Java đang ở: chúng có thể được gọi bởi các applet không đáng tin cậy, vì vậy chúng không dám tin tưởng vào người gọi của chúng. Điều này có nghĩa là sẽ rất khó để viết mã thư viện an toàn, nếu Stringkhông phải là cuối cùng.


Tất nhiên, vẫn có thể loại bỏ âm thanh TOCTTOU đó bằng cách sử dụng sự phản chiếu để sửa đổi char[]bên trong chuỗi, nhưng nó đòi hỏi một số may mắn trong thời gian.
Peter Taylor

2
@Peter Taylor, nó phức tạp hơn thế. Bạn chỉ có thể sử dụng sự phản chiếu để truy cập các biến riêng tư nếu SecurityManager cho phép bạn làm như vậy. Các ứng dụng và mã không tin cậy khác không được cấp quyền này và do đó không thể sử dụng sự phản chiếu để sửa đổi riêng tư char[]bên trong chuỗi; do đó, họ không thể khai thác lỗ hổng TOCTTOU.
DW

Tôi biết, nhưng điều đó vẫn để lại các ứng dụng và các applet đã ký - và có bao nhiêu người thực sự sợ hãi bởi hộp thoại cảnh báo cho một applet tự ký?
Peter Taylor

2
@Peter, đó không phải là vấn đề. Vấn đề là việc viết các thư viện Java đáng tin cậy sẽ khó khăn hơn rất nhiều và sẽ có nhiều lỗ hổng được báo cáo hơn trong các thư viện Java, nếu Stringkhông phải là cuối cùng. Miễn là một số thời gian mô hình mối đe dọa này có liên quan, thì nó trở nên quan trọng để bảo vệ chống lại. Miễn là nó quan trọng đối với các applet và mã không đáng tin cậy khác, điều đó đủ để biện minh cho việc đưa ra Stringcuối cùng, ngay cả khi nó không cần thiết cho các ứng dụng đáng tin cậy. (Trong mọi trường hợp, các ứng dụng đáng tin cậy có thể bỏ qua tất cả các biện pháp bảo mật, bất kể là cuối cùng.)
DW

"Cuộc tấn công: một người gọi độc hại có thể tạo một lớp con của String, EvilString, trong đó EvilString.startsWith () luôn trả về đúng ..." - không phải nếu startedWith () là cuối cùng.
Erik

4

Nếu không, các ứng dụng java đa luồng sẽ là một mớ hỗn độn (thậm chí còn tệ hơn cả thực tế).

IMHO Ưu điểm chủ yếu của các chuỗi cuối cùng (không thay đổi) là chúng vốn đã an toàn theo luồng: chúng không yêu cầu đồng bộ hóa (viết mã đó đôi khi khá tầm thường nhưng ít hơn rất nhiều so với điều đó). Nếu có thể thay đổi, đảm bảo những thứ như bộ công cụ windows đa luồng sẽ rất khó và những thứ đó là rất cần thiết.


3
finalcác lớp không có gì để làm với tính biến đổi của lớp.

Câu trả lời này không giải quyết câu hỏi. -1
FUZxxl

3
Jarrod Roberson: nếu lớp không phải là cuối cùng, bạn có thể tạo Chuỗi có thể thay đổi bằng cách sử dụng tính kế thừa.
ysdx

@JarrodRoberson cuối cùng cũng có người nhận ra lỗi. Câu trả lời được chấp nhận cũng ngụ ý cuối cùng bằng bất biến.
Tulains Córdova

1

Một lợi thế khác để Stringlà cuối cùng là nó đảm bảo kết quả có thể dự đoán được, giống như bất biến. String, mặc dù một lớp, có nghĩa là được xử lý trong một vài tình huống, giống như một loại giá trị (trình biên dịch thậm chí hỗ trợ nó thông qua String blah = "test"). Do đó, nếu bạn tạo một chuỗi có một giá trị nhất định, bạn hy vọng nó có hành vi nhất định được kế thừa cho bất kỳ chuỗi nào, tương tự như kỳ vọng bạn có với số nguyên.

Hãy suy nghĩ về điều này: Điều gì sẽ xảy ra nếu bạn phân lớp java.lang.Stringvà vượt quá các phương thức equalshoặc hashCodephương thức? Đột nhiên hai chuỗi "kiểm tra" không thể bằng nhau.

Hãy tưởng tượng sự hỗn loạn nếu tôi có thể làm điều này:

public class Password extends String {
    public Password(String text) {
        super(text);
    }

    public boolean equals(Object o) {
        return true;
    }
}

một số lớp khác:

public boolean isRightPassword(String suppliedString) {
    return suppliedString != null && suppliedString.equals("secret");
}

public void login(String username, String password) {
    return isRightUsername(username) && isRightPassword(password);
}

public void loginTest() {
    boolean success = login("testuser", new Password("wrongpassword"));
    // success is true, despite the password being wrong
}

1

Lý lịch

Có ba nơi mà trận chung kết có thể xuất hiện trong Java:

Làm cho một lớp cuối cùng ngăn chặn tất cả các lớp con của lớp. Tạo một phương thức cuối cùng ngăn các lớp con của phương thức ghi đè lên nó. Làm cho một trường cuối cùng ngăn chặn nó được thay đổi sau này.

Quan niệm sai lầm

Có những tối ưu hóa xảy ra xung quanh các phương pháp và trường cuối cùng.

Phương pháp cuối cùng giúp HotSpot dễ dàng tối ưu hóa hơn thông qua nội tuyến. Tuy nhiên, HotSpot thực hiện điều này ngay cả khi phương thức không phải là cuối cùng vì nó hoạt động với giả định rằng nó đã không bị ghi đè cho đến khi được chứng minh khác đi. Thông tin thêm về điều này trên SO

Một biến cuối cùng có thể được tối ưu hóa mạnh mẽ, và nhiều hơn về điều đó có thể được đọc trong phần JLS 17.5.3 .

Tuy nhiên, với sự hiểu biết đó, người ta nên biết rằng cả hai tối ưu hóa này đều không phải là về một lớp cuối cùng. Không có hiệu suất đạt được bằng cách làm cho một lớp cuối cùng.

Khía cạnh cuối cùng của một lớp cũng không liên quan gì đến tính bất biến. Người ta có thể có một lớp bất biến (như BigInteger ) không phải là cuối cùng, hoặc một lớp có thể thay đổi cuối cùng (như StringBuilder ). Quyết định về việc nếu một lớp học nên là cuối cùng là một câu hỏi về thiết kế.

Thiết kế cuối cùng

Chuỗi là một trong những loại dữ liệu được sử dụng nhiều nhất. Chúng được tìm thấy như là chìa khóa của bản đồ, chúng lưu trữ tên người dùng và mật khẩu, chúng là những gì bạn đọc từ bàn phím hoặc trường trên trang web. Dây ở khắp mọi nơi .

Bản đồ

Điều đầu tiên cần xem xét về những gì sẽ xảy ra nếu bạn có thể phân lớp Chuỗi là nhận ra rằng ai đó có thể xây dựng một lớp Chuỗi có thể thay đổi sẽ xuất hiện thành Chuỗi. Điều này sẽ làm rối bản đồ ở khắp mọi nơi.

Hãy xem xét mã giả thuyết này:

Map t = new TreeMap<String, Integer>();
Map h = new HashMap<String, Integer>();
MyString one = new MyString("one");
MyString two = new MyString("two");

t.put(one, 1); h.put(one, 1);
t.put(two, 2); h.put(two, 2);

one.prepend("z");

Đây là một vấn đề với việc sử dụng khóa có thể thay đổi nói chung với Bản đồ, nhưng điều mà tôi đang cố gắng đạt được đó là đột nhiên một số điều về Bản đồ bị phá vỡ. Entry không còn ở đúng vị trí trong bản đồ. Trong HashMap, giá trị băm đã (nên có) thay đổi và do đó, nó không còn ở mục bên phải. Trong TreeMap, cây hiện bị hỏng do một trong các nút nằm ở phía sai.

Vì việc sử dụng Chuỗi cho các khóa này rất phổ biến, nên ngăn chặn hành vi này bằng cách tạo Chuỗi cuối cùng.

Bạn có thể quan tâm đến việc đọc Tại sao String là bất biến trong Java? để biết thêm về bản chất bất biến của Chuỗi.

Chuỗi bất chính

Có một số tùy chọn bất chính khác nhau cho Chuỗi. Hãy xem xét nếu tôi tạo một Chuỗi luôn trả về đúng khi được gọi bằng ... và chuyển nó vào kiểm tra mật khẩu? Hoặc làm cho nó để các bài tập cho MyString sẽ gửi một bản sao của Chuỗi đến một số địa chỉ email?

Đây là những khả năng rất thực tế khi bạn có khả năng phân lớp Chuỗi.

Tối ưu hóa chuỗi Java.lang

Trong khi trước khi tôi đề cập rằng trận chung kết không làm cho String nhanh hơn. Tuy nhiên, lớp String (và các lớp khác trong java.lang) sử dụng thường xuyên việc bảo vệ các trường và phương thức mức gói để cho phép các java.langlớp khác có thể sửa đổi nội bộ thay vì luôn luôn thông qua API công khai cho Chuỗi. Các hàm như getChars không có kiểm tra phạm vi hoặc lastIndexOf được sử dụng bởi StringBuffer hoặc hàm tạo chia sẻ mảng bên dưới (lưu ý rằng đó là một điều Java 6 đã bị thay đổi do vấn đề bộ nhớ).

Nếu ai đó tạo một lớp con của Chuỗi, nó sẽ không thể chia sẻ những tối ưu hóa đó (trừ khi đó là một phần của java.langquá, nhưng đó là một gói kín ).

Thật khó để thiết kế một cái gì đó để mở rộng

Thiết kế một cái gì đó để có thể mở rộng là khó khăn . Điều đó có nghĩa là bạn phải tiết lộ các phần trong nội bộ của mình để lấy thứ khác để có thể sửa đổi.

Một chuỗi có thể mở rộng không thể bị rò rỉ bộ nhớ . Những phần của nó sẽ cần phải được tiếp xúc với các lớp con và thay đổi mã đó có nghĩa là các lớp con sẽ bị phá vỡ.

Java tự hào về khả năng tương thích ngược và bằng cách mở các lớp cốt lõi để mở rộng, người ta sẽ mất một số khả năng đó để sửa chữa mọi thứ trong khi vẫn duy trì khả năng tính toán với các lớp con bên thứ ba.

Checkstyle có một quy tắc mà nó thi hành (điều đó thực sự làm tôi thất vọng khi viết mã nội bộ) được gọi là "DesignForExtension", điều này thực thi rằng mọi lớp đều:

  • trừu tượng
  • Sau cùng
  • Thực hiện trống

Các lý do cho là:

Phong cách thiết kế API này bảo vệ các siêu lớp khỏi bị phá vỡ bởi các lớp con. Nhược điểm là các lớp con bị hạn chế về tính linh hoạt, đặc biệt là chúng không thể ngăn chặn việc thực thi mã trong siêu lớp, nhưng điều đó cũng có nghĩa là các lớp con không thể làm hỏng trạng thái của siêu lớp bằng cách quên gọi phương thức siêu.

Cho phép các lớp thực hiện được mở rộng có nghĩa là các lớp con có thể có thể làm hỏng trạng thái của lớp mà nó dựa trên và làm cho nó đảm bảo khác nhau mà siêu lớp đưa ra là không hợp lệ. Đối với một thứ phức tạp như String, gần như chắc chắn rằng việc thay đổi một phần của nó sẽ phá vỡ thứ gì đó.

Nhà phát triển

Đó là một phần của việc trở thành một nhà phát triển. Xem xét khả năng mỗi nhà phát triển sẽ tạo ra lớp con Chuỗi riêng của họ với bộ sưu tập của riêng mình utils trong đó. Nhưng bây giờ các lớp con này không thể được tự do gán lại cho nhau.

WleaoString foo = new WleaoString("foo");
MichaelTString bar = foo; // This doesn't work.

Cách này dẫn đến sự điên rồ. Truyền tới Chuỗi khắp mọi nơi và kiểm tra xem Chuỗi có phải là một thể hiện của lớp Chuỗi của bạn hay không, tạo một Chuỗi mới dựa trên chuỗi đó và ... chỉ, không. Đừng.

Tôi chắc rằng bạn có thể viết một lớp String tốt ... nhưng để lại văn bản triển khai nhiều chuỗi cho những người điên người viết C ++ và phải đối phó với std::stringchar*và một cái gì đó từ tăng và SString , và tất cả các phần còn lại .

Chuỗi ma thuật Java

Có một số điều kỳ diệu mà Java làm với String. Những điều này giúp lập trình viên dễ dàng xử lý hơn nhưng lại đưa ra một số điểm không nhất quán trong ngôn ngữ. Việc cho phép các lớp con trên String sẽ có một số suy nghĩ rất quan trọng về cách đối phó với những điều kỳ diệu này:

  • Chuỗi ký tự (JLS 3.10.5 )

    Có mã cho phép một người làm:

    String foo = "foo";

    Điều này không được nhầm lẫn với quyền anh của các loại số như Integer. Bạn không thể làm 1.toString(), nhưng bạn có thể làm "foo".concat(bar).

  • Các +nhà điều hành (JLS 15.18.1 )

    Không có loại tham chiếu nào khác trong Java cho phép một toán tử được sử dụng trên nó. Chuỗi là đặc biệt. Toán tử nối chuỗi cũng hoạt động ở cấp trình biên dịch để "foo" + "bar"trở thành "foobar"khi nó được biên dịch thay vì trong thời gian chạy.

  • Chuyển đổi chuỗi (JLS 5.1.11 )

    Tất cả các đối tượng có thể được chuyển đổi thành Chuỗi chỉ bằng cách sử dụng chúng trong ngữ cảnh Chuỗi.

  • Chuỗi thực tập ( JavaDoc )

    Lớp String có quyền truy cập vào nhóm Chuỗi cho phép nó có các biểu diễn chính tắc của đối tượng được điền ở kiểu biên dịch với chuỗi ký tự Chuỗi.

Việc cho phép một lớp con của Chuỗi có nghĩa là các bit này với Chuỗi giúp việc lập trình dễ dàng hơn sẽ trở nên rất khó hoặc không thể thực hiện khi các loại Chuỗi khác có thể.


0

Nếu Chuỗi không phải là cuối cùng, mọi rương công cụ lập trình sẽ chứa "Chuỗi chỉ với một vài phương thức trợ giúp đẹp". Mỗi cái này sẽ không tương thích với tất cả các rương công cụ khác.

Tôi coi đó là một hạn chế ngớ ngẩn. Hôm nay tôi tin chắc đây là một quyết định đúng đắn. Nó giữ cho Chuỗi là Chuỗi.


finaltrong Java không giống như finaltrong C #. Trong bối cảnh này, nó giống với C # readonly. Xem stackoverflow.com/questions/1327544/
Mạnh

9
@MainMa: Trên thực tế, trong bối cảnh này, có vẻ như nó giống như được niêm phong của C #, như trong public final class String.
cấu hình
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.