Що було б справді, якби java.lang.String не був остаточним?


16

Я тривалий час розробник Java, і нарешті, після закінчення спеціальності, встигаю гідно її вивчити, щоб скласти іспит на сертифікацію ... Одне, що мене завжди турбувало, - це те, що "Строка" є "остаточною". Я розумію це, коли читаю про проблеми безпеки та пов'язані з цим речі ... Але, серйозно, чи має хтось правдивий приклад цього?

Наприклад, що буде, якщо String не був остаточним? Наче його немає в Рубі. Я не чув жодних скарг, що надходять від спільноти Ruby ... І мені відомо про StringUtils та пов’язані з ними класи, які вам доведеться або реалізувати самостійно, або шукати в Інтернеті, щоб просто реалізувати таку поведінку (4 рядки коду) ви Ви готові.


9
Можливо, вас зацікавить це питання щодо переповнення стека: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens

Так само, що було б, якби java.io.Fileне final. О, це не так. Баггер.
Том Хотін - тайклін

1
Кінець західної цивілізації, як ми це знаємо.
Tulains Córdova

Відповіді:


22

Основна причина - швидкість: finalкласи не можна розширювати, що дозволило JIT робити всілякі оптимізації при обробці рядків - ніколи не потрібно перевіряти переохоплені методи.

Ще одна причина - безпека ниток: незмінні завжди безпечні для ниток, оскільки нитка повинна повністю створити їх, перш ніж вони можуть бути передані комусь іншому - і після побудови вони більше не можуть бути змінені.

Також винахідники Java часу завжди хотіли помилитися на стороні безпеки. Будучи в змозі розширити String (що я часто роблю в Groovy, тому що це так зручно), можна відкрити цілу банку глистів, якщо ти не знаєш, що ти робиш.


2
Це неправильна відповідь. Крім перевірки, що вимагається специфікацією JVM, HotSpot використовує лише finalяк швидку перевірку, чи метод не перекрито при компіляції коду.
Том Хотін - тайклін

1
@ TomHawtin-tackline: Посилання?
Аарон Дігулла

1
Ви, здається, натякаєте на те, що незмінність і остаточність є якимось чином пов'язаними. "Ще одна причина - безпека нитки: змінні елементи завжди захищені від потоку b ...". Причина, по якій об'єкт є незмінною чи ні, не є тим, що вони є або не є остаточними.
Tulains Córdova

1
Крім того, клас String є незмінним незалежно від finalключового слова в класі. Масив символів, який він містить, є остаточним, що робить його непорушним. Здійснення остаточного класу закриває цикл, як згадується @abl, оскільки змінений підклас неможливо ввести.

1
@Snowman Якщо точніше, остаточний масив символів лише робить посилання на масив незмінним, а не вміст масиву.
Міхал Космольський

10

Є ще одна причина, чому Stringклас Java повинен бути остаточним: він важливий для безпеки в деяких сценаріях. Якщо Stringклас не був остаточним, наступний код був би вразливим для тонких атак зловмисного абонента:

 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>";
 }

Атака: зловмисний абонент може створити підклас String, EvilStringде EvilString.startsWith()завжди повертається істина, але де значенням EvilStringє щось зло (наприклад, javascript:alert('xss')). Через підкласи це ухилиться від перевірки безпеки. Ця атака відома як вразливість часу перевірки до часу використання (TOCTTOU): між часом, коли перевірка виконується (що URL починається з http / https), і часом, коли значення використовується (для побудови фрагмента html) ефективне значення може змінюватися. Якщо Stringне було остаточним, то ризики TOCTTOU були б надзвичайно поширеними.

Тож якщо Stringце не остаточно, то складно написати захищений код, якщо ви не можете довіряти своєму абоненту. Звичайно, саме в такому положенні знаходяться бібліотеки Java: вони можуть викликати ненадійні аплети, тому вони не наважуються довіряти своєму абоненту. Це означає, що було б необґрунтовано складно написати захищений код бібліотеки, якби Stringне було остаточним.


Звичайно, все-таки можна зняти цю вулкан TOCTTOU, використовуючи відображення, щоб змінити char[]внутрішній рядок, але це вимагає певної удачі в часі.
Пітер Тейлор

2
@ Петер Тейлор, це складніше за це. Ви можете використовувати відображення лише для доступу до приватних змінних, якщо SecurityManager дає вам дозвіл на це. Аплетти та інший ненадійний код не дають цього дозволу, і тому не можуть використовувати відображення для зміни приватного char[]всередині рядка; отже, вони не можуть використовувати вразливість TOCTTOU.
DW

Я знаю, але це все ще залишає програми та підписані аплети - а скільки людей насправді лякається діалоговим діалоговим попередженням для самопідписаного аплету?
Пітер Тейлор

2
@ Петер, це не сенс. Справа в тому, що писати довірені бібліотеки Java було б набагато складніше, і, якби Stringне було остаточним, було б набагато більше повідомлень про вразливості в бібліотеках Java . Поки ця модель часу загрози є актуальною, тоді від неї стає важливо захищатися. Поки це важливо для аплетів та іншого ненадійного коду, цього, ймовірно, достатньо, щоб виправдати прийняття Stringостаточного, навіть якщо він не потрібен для надійних програм. (У будь-якому випадку, надійні програми вже можуть обійти всі заходи безпеки, незалежно від того, остаточні вони.)
DW

"Атака: зловмисний абонент може створити підклас String, EvilString, де EvilString.startsWith () завжди повертає істину ..." - не, якщо startWith () є остаточним.
Ерік

4

Якщо ні, багатопотокові програми Java будуть безладно (навіть гірше, ніж є насправді).

IMHO Основна перевага остаточних рядків (незмінних) полягає в тому, що вони по суті є безпечними для потоків: вони не потребують синхронізації (написання цього коду іноді досить тривіально, але більш ніж менш далеко від цього). Якби вони були незмінними, гарантувати такі речі, як багатопотокові набори інструментів Windows, було б дуже важко, і ці речі суворо необхідні.


3
finalкласи не мають нічого спільного з незмінністю класу.

Ця відповідь не стосується питання. -1
FUZxxl

3
Джеррод Роберсон: якщо клас не був остаточним, ви можете створити змінні Strings, використовуючи успадкування.
ysdx

@JarrodRoberson нарешті хтось помічає помилку. Прийнята відповідь також передбачає кінцеві рівні незмінні.
Tulains Córdova

1

Ще однією перевагою Stringбути остаточним є те, що він забезпечує передбачувані результати, як і незмінність. String, хоча клас призначений для обробки в кількох сценаріях, як тип значення (компілятор навіть підтримує його через String blah = "test"). Отже, якщо ви створюєте рядок з певним значенням, ви очікуєте, що він матиме певну поведінку, яка є спадковою для будь-якого рядка, подібно до очікувань, які ви мали б із цілими числами.

Подумайте над цим: що робити, якщо ви підкласуєте java.lang.Stringта переосмислюєте equalsабо hashCodeметоди? Раптом два "тесту" струни вже не могли дорівнювати одна одній.

Уявіть собі хаос, якби я міг це зробити:

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

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

якийсь інший клас:

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

Фон

Є три місця, де фінал може відображатися на Java:

Здійснення остаточного класу запобігає всім підкласам класу. Виконання методу остаточним запобігає його перекриття підкласів методу. Здійснення остаточного поля запобігає його подальшому зміні.

Помилкові уявлення

Існують оптимізації, які відбуваються навколо кінцевих методів і полів.

Остаточний метод спрощує HotSpot оптимізувати за допомогою вбудовування. Однак HotSpot робить це, навіть якщо метод не є остаточним, оскільки він працює за припущенням, що його не було відмінено, поки не доведено інше. Більше про це на SO

Кінцева змінна може бути агресивно оптимізована, і докладніше про це можна прочитати в розділі JLS 17.5.3 .

Однак, розуміючи це, слід пам'ятати, що жодна з цих оптимізацій не стосується того, щоб зробити клас остаточним. Немає підвищення продуктивності, зробивши фінал класу.

Заключний аспект класу також не має нічого спільного з незмінністю. Можна мати клас, що не змінюється (наприклад, BigInteger ), який не є остаточним, або клас, який може змінюватися і остаточно (наприклад, StringBuilder ). Рішення про те, чи повинен бути клас остаточним, - це питання дизайну.

Остаточний дизайн

Рядки є одним з найбільш використовуваних типів даних. Вони знаходяться як ключі до карт, вони зберігають імена користувачів та паролі, вони - те, що ви читаєте з клавіатури чи поля на веб-сторінці. Струни є скрізь .

Карти

Перше, що слід врахувати, що станеться, якщо ви зможете підкласировать String - це зрозуміти, що хтось може створити мутаційний клас String, який, здавалося б, інакше є String. Це зіпсує Карти всюди.

Розглянемо цей гіпотетичний код:

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");

Це проблема використання мутабельного ключа взагалі з картою, але те, що я намагаюся отримати там, - це раптом ряд речей про розрив карти. Запис більше не в потрібному місці на карті. У HashMap значення хеша змінилося (повинно було) змінитися, і, таким чином, його більше немає в потрібному введенні. У TreeMap дерево зараз зламано, оскільки один із вузлів знаходиться з виворітного боку.

Оскільки використання String для цих клавіш настільки поширене, цю поведінку слід запобігти, зробивши String остаточним.

Можливо, вам буде цікаво прочитати Чому String незмінний на Java? докладніше про непорушний характер Струн.

Підступні струни

Існує ряд різноманітних недобрих варіантів для рядків. Подумайте, чи я створив рядок, який завжди повертався справжнім, коли називались рівними ... і передав це в перевірку паролем? Або зробили так, що завдання на MyString надсилали копію String на якусь електронну адресу?

Це дуже реальні можливості, коли у вас є можливість підкласу String.

Оптимізація рядків Java.lang

Хоча раніше я згадував, що фінал не робить String швидшим. Однак клас String (та інші класи в java.lang) часто використовують захист полів та методів на рівні пакетів, щоб дозволити іншим java.langкласам мати можливість попрацювати з внутрішніми, а не постійно проходити через загальнодоступний API для String. Такі функції, як getChars без перевірки діапазону або lastIndexOf, який використовується StringBuffer, або конструктор, який розділяє базовий масив (зауважте, що це Java 6, яку було змінено через проблеми з пам'яттю).

Якщо хтось створив підклас String, він не зміг би поділитися цими оптимізаціями (якщо тільки це не було частиною java.lang, але це запечатаний пакет ).

Складніше розробити щось для розширення

Важко спроектувати щось, що можна розширити . Це означає, що вам доведеться викрити частини вашого внутрішнього простору для чогось іншого, щоб їх можна було змінити.

Розширювана струна не могла виправити витік пам'яті . Ці частини повинні були бути піддані підкласам, і зміна цього коду означало б, що підкласи будуть порушені.

Java пишається зворотною сумісністю і, відкриваючи основні класи до розширення, втрачає частину цієї здатності виправляти речі, зберігаючи при цьому обчислюваність з підкласами третіх сторін.

У Checkstyle є правило, яке застосовує (що мене дуже засмучує при написанні внутрішнього коду) під назвою "DesignForExtension", яке примушує будь-який клас:

  • Анотація
  • Фінал
  • Порожня реалізація

Раціональне для чого:

Цей стиль дизайну API захищає суперкласи від зламу підкласів. Мінусом є те, що підкласи обмежені у своїй гнучкості, зокрема, вони не можуть запобігти виконанню коду в надкласі, але це також означає, що підкласи не можуть пошкодити стан надкласу, забувши викликати супер-метод.

Дозволити розширення класів реалізації означає, що підкласи можуть пошкодити стан класу, на якому він заснований, і зробити так, що різні гарантії, які дає суперклас, недійсні. Для чого - то так складно , як струни, це майже впевненість , що зміна його частина буде що - небудь зламати.

Хурбі розробника

Його частина бути розробником. Розглянемо ймовірність того, що кожен розробник створить власний підклас String із власною колекцією утилітів . Але тепер ці підкласи не можна вільно присвоювати один одному.

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

Цей шлях призводить до божевілля. Передавайте String всюди і перевіряйте, чи є String - це екземпляр вашого класу String чи ні, створивши нову String на основі цього і ... просто, ні. Не варто.

Я впевнений , що ви можете написати хороший клас String , ... але залишити написання кілька реалізацій рядків для тих божевільних людей , які пишуть на C ++ і доводиться мати справу з std::stringі char*і що - то від підвищення і SString , і все інше .

Java String Magic

Є кілька магічних речей, які Java робить зі Strings. Це полегшує програмісту справу, але вводить деякі невідповідності в мові. Якщо дозволити підкласи на String, було б дуже важливо подумати, як боротися з цими магічними речами:

  • Строкові літерали (JLS 3.10.5 )

    Маючи код, який дозволяє робити:

    String foo = "foo";

    Це не слід плутати з боксами числових типів, таких як Integer. Ви не можете зробити 1.toString(), але можете зробити "foo".concat(bar).

  • +Оператор (ПСБ 15.18.1 )

    Жоден інший тип посилання на Java не дозволяє оператору використовувати його. Рядок особливий. Оператор конкатенації рядків також працює на рівні компілятора, так що "foo" + "bar"стає "foobar"коли він компілюється, а не під час виконання.

  • Перетворення рядків (JLS 5.1.11 )

    Усі об'єкти можна перетворити в рядки, просто використовуючи їх у String-контексті.

  • Інструмент для рядків ( JavaDoc )

    Клас String має доступ до пулу Strings, які дозволяють йому мати канонічні зображення об'єкта, який заповнюється на тип компіляції з літералами String.

Дозволити підклас String означатиме, що ці біти з String, які полегшують програмування, стали б дуже важкими або неможливими для виконання, коли можливі інші типи String.


0

Якби String не був остаточним, кожен скринька інструментів програміста містив би своє "String з кількома приємними допоміжними методами". Кожен із них був би несумісний з усіма іншими скринями інструментів.

Я вважав це дурним обмеженням. Сьогодні я переконаний, це було дуже обгрунтоване рішення. Він тримає струни як струнні.


finalв Java - це не те саме, що finalв C #. У цьому контексті це те саме, що і C # 's readonly. Див stackoverflow.com/questions/1327544 / ...
Arseni Mourzenko

9
@MainMa: Насправді в цьому контексті здається, що це те саме, що C # запечатано, як у public final class String.
конфігуратор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.