Что бы действительно произошло, если бы java.lang.String не был финальным?


16

Я долгое время являюсь Java-разработчиком и, наконец, после специализации у меня есть время, чтобы прилично изучить его, чтобы сдать сертификационный экзамен ... Одна вещь, которая меня всегда беспокоила, это то, что String является "финальной". Я понимаю, когда читаю о проблемах безопасности и связанных с этим вещах ... Но, серьезно, у кого-нибудь есть настоящий пример этого?

Например, что произойдет, если строка не будет финальной? Как будто это не в Ruby. Я не слышал никаких жалоб от сообщества Ruby ... И я знаю о StringUtils и связанных с ним классах, которые вы должны либо реализовать самостоятельно, либо искать в Интернете, чтобы просто реализовать это поведение (4 строки кода), которое вы готовы.


9
Вы можете быть заинтересованы в этом вопросе о переполнении стека: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Томас Оуэнс

Точно так же, что случилось бы, если бы java.io.Fileне final. О, это не так. Мудак.
Том Хотин - tackline

1
Конец западной цивилизации, какой мы ее знаем.
Тулаинс Кордова

Ответы:


22

Основная причина - скорость: finalклассы не могут быть расширены, что позволило JIT выполнять все виды оптимизаций при обработке строк - никогда не нужно проверять переопределенные методы.

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

Кроме того, изобретатели среды выполнения Java всегда хотели ошибиться на стороне безопасности. Возможность расширения String (что я часто делаю в Groovy, потому что это очень удобно) может открыть целую банку червей, если вы не знаете, что делаете.


2
Это неверный ответ. Помимо проверки, требуемой спецификацией JVM, HotSpot использует только finalдля быстрой проверки того, что метод не переопределяется при компиляции кода.
Том Хотин - tackline

1
@ TomHawtin-tackline: ссылки?
Аарон Дигулла

1
Вы, кажется, подразумеваете, что неизменность и окончательность как-то связаны. «Другая причина - безопасность потоков: неизменяемые всегда безопасны для потоков ...». Причина, по которой объект является неизменным или нет, заключается не в том, что они являются или не являются окончательными.
Тулаинс Кордова

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()всегда возвращается true, но где значение EvilStringявляется чем-то злым (например, javascript:alert('xss')). Из-за подклассов это позволит избежать проверки безопасности. Эта атака известна как уязвимость, связанная с временем проверки (TOCTTOU): между временем, когда проверка выполнена (URL начинается с http / https), и временем, когда используется значение (для создания фрагмента HTML), эффективное значение может измениться. Если бы Stringне было окончательного, то риски TOCTTOU были бы повсеместными.

Поэтому, если Stringэто не окончательно, становится сложно написать безопасный код, если вы не можете доверять своему абоненту. Конечно, это именно то положение, в котором находятся библиотеки Java: они могут вызываться ненадежными апплетами, поэтому они не осмеливаются доверять своему вызывающему. Это означает, что было бы неразумно сложно писать безопасный код библиотеки, если бы он Stringне был окончательным.


Конечно, все еще возможно осуществить это TOCTTOU vuln, используя отражение для изменения char[]внутренней части строки, но это требует некоторой удачи во времени.
Питер Тейлор

2
@ Питер Тейлор, это сложнее, чем это. Вы можете использовать отражение только для доступа к закрытым переменным, если SecurityManager дает вам разрешение на это. Апплетам и другому ненадежному коду не дано это разрешение, и, следовательно, они не могут использовать отражение для изменения приватного char[]внутри строки; следовательно, они не могут использовать уязвимость TOCTTOU.
DW

Я знаю, но это все еще оставляет приложения и подписанные апплеты - и сколько людей фактически напугано диалоговым окном предупреждения для самоподписанного апплета?
Питер Тейлор

2
@ Питер, это не главное. Дело в том, что написание доверенных библиотек Java было бы намного сложнее, и было бы гораздо больше сообщений об уязвимостях в библиотеках Java, если бы они Stringне были окончательными. Пока в какой-то момент эта модель угрозы актуальна, ее становится важно защищать. Пока это важно для апплетов и другого ненадежного кода, этого вполне достаточно, чтобы оправдать Stringокончательный вывод, даже если он не нужен для доверенных приложений. (В любом случае доверенные приложения могут уже обойти все меры безопасности, независимо от того, является ли он окончательным.)
DW

«Атака: злонамеренный вызывающий объект может создать подкласс String, EvilString, где EvilString.startsWith () всегда возвращает true ...» - нет, если startWith () является окончательным.
Эрик

4

Если нет, то многопоточные Java-приложения были бы беспорядком (даже хуже, чем на самом деле).

ИМХО Главным преимуществом конечных строк (неизменяемых) является то, что они по своей природе поточно-ориентированы: они не требуют синхронизации (написание этого кода иногда довольно тривиально, но более чем менее чем далеко от этого). Если бы они были изменяемыми, гарантировать такие вещи, как многопоточные инструменты Windows, было бы очень сложно, и эти вещи строго необходимы.


3
finalклассы не имеют ничего общего с изменчивостью класса.

Этот ответ не касается вопроса. -1
FUZxxl

3
Джаррод Роберсон: если бы класс не был окончательным, вы могли бы создавать изменяемые строки, используя наследование.
ysdx

@JarrodRoberson наконец-то кто-то замечает ошибку. Принятый ответ также подразумевает окончательные равные неизменяемыми.
Тулаинс Кордова

1

Еще одним преимуществом Stringфинальной версии является то, что она обеспечивает предсказуемые результаты, как и неизменность. Stringхотя класс и предназначен для обработки в нескольких сценариях, например, тип значения (компилятор даже поддерживает его через String blah = "test"). Поэтому, если вы создаете строку с определенным значением, вы ожидаете, что она будет иметь определенное поведение, которое наследуется от любой строки, аналогично ожиданиям, которые вы имели бы с целыми числами.

Подумайте об этом: что делать, если вы подкласс java.lang.Stringи переусердствовали методы equalsили hashCode? Внезапно две строки «test» уже не могли сравниться друг с другом.

Вообразите хаос, если бы я мог сделать это:

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

Фон

Есть три места, где final может отображаться на Java:

Завершение класса предотвращает все подклассы класса. Создание метода final не позволяет подклассам метода переопределять его. Создание поля final предотвращает его изменение позже.

заблуждения

Существуют оптимизации, которые происходят вокруг окончательных методов и полей.

Последний метод облегчает оптимизацию HotSpot с помощью встраивания. Тем не менее, HotSpot делает это, даже если метод не является окончательным, поскольку он работает при условии, что он не был переопределен, пока не доказано обратное.Подробнее об этом на SO

Последняя переменная может быть агрессивно оптимизирована, и больше об этом можно прочитать в разделе 17.5.3 JLS .

Однако с этим пониманием следует помнить, что ни одна из этих оптимизаций не предназначена для того, чтобы сделать финал класса . Нет никакого выигрыша в производительности, сделав финал класса.

Последний аспект класса также не имеет ничего общего с неизменяемостью. Можно иметь неизменный класс (такой как 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? для больше о неизменной природе Струн.

Нечестивые строки

Есть ряд различных гнусных опций для строк. Подумайте, сделал ли я String, которая всегда возвращала true, когда вызывался равен ... и передал это в проверку пароля? Или сделал так, чтобы назначения MyString отправляли копию строки на какой-либо адрес электронной почты?

Это очень реальные возможности, когда у вас есть возможность подкласс String.

Java.lang Строковые оптимизации

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

Если бы кто-то создал подкласс String, он не смог бы поделиться этими оптимизациями (если только он не был частью этого java.lang, но это запечатанный пакет ).

Сложнее создать что-то для расширения

Создать что-то, что можно расширить, сложно . Это означает, что вы должны выставлять части своих внутренних элементов, чтобы что-то еще можно было модифицировать.

Расширяемая строка не могла исправить утечку памяти . Эти части должны были бы быть подвержены подклассам, и изменение этого кода означало бы, что подклассы сломались бы.

Java гордится обратной совместимостью и, открывая базовые классы для расширения, человек теряет часть этой способности исправлять вещи, сохраняя при этом совместимость с подклассами сторонних производителей.

У Checkstyle есть правило, которое оно применяет (которое действительно расстраивает меня при написании внутреннего кода), называемое «DesignForExtension», которое обеспечивает, чтобы каждый класс был:

  • абстрактный
  • окончательный
  • Пустая реализация

Рациональным для чего является:

Этот стиль разработки API защищает суперклассы от взлома подклассами. Недостатком является то, что подклассы ограничены в своей гибкости, в частности, они не могут помешать выполнению кода в суперклассе, но это также означает, что подклассы не могут испортить состояние суперкласса, забыв вызвать метод super.

Разрешение расширенных классов реализации означает, что подклассы могут повредить состояние класса, на котором он основан, и сделать так, чтобы различные гарантии, которые дает суперкласс, были недействительными. Для чего-то такого сложного, как String, почти наверняка, что изменение его части что- то сломает.

Разработчик хурбис

Это часть того, чтобы быть разработчиком. Рассмотрим вероятность того, что каждый разработчик создаст свой собственный подкласс 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 делает со строками. Это облегчает работу программиста, но вносит некоторые несоответствия в язык. Разрешение подклассов на 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 )

    Все объекты могут быть преобразованы в строки, просто используя их в контексте строки.

  • Строковый интернинг ( JavaDoc )

    Класс String имеет доступ к пулу Strings, что позволяет ему иметь канонические представления объекта, который заполняется в типе компиляции литералами String.

Разрешение подкласса String будет означать, что эти биты со String, которые облегчают программирование, станут очень трудными или невозможными, когда возможны другие типы String.


0

Если бы String не был окончательным, каждый сундук для программиста содержал бы свою собственную «String с несколькими хорошими вспомогательными методами». Каждый из них будет несовместим со всеми другими сундуками с инструментами.

Я посчитал это глупым ограничением. Сегодня я убежден, что это было очень разумное решение. Он хранит строки как строки.


finalв Java это не то же самое, что finalв C #. В этом контексте это то же самое, что и в C # readonly. Смотрите stackoverflow.com/questions/1327544/…
Арсений Мурзенко

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