Почему они решили сделать String неизменным в Java и .NET (и некоторых других языках)? Почему они не сделали его изменяемым?
Спасибо, belugabob, но я не она, я он. Видимо, люди не принимают во внимание культурные различия.
Мои извинения - Крисси - это (как правило) девичье имя в Великобритании, что делает меня жертвой другого культурного различия :-)
Замечу, что в .NET String фактически внутренне изменяемый. StringBuilder в .NET 2.0 изменяет строку. Я просто оставлю это здесь.
На самом деле строки .NET находятся изменчивы. И это даже не хакерство.




Во многом это из соображений безопасности. Гораздо сложнее защитить систему, если вы не уверены, что ваши String защищены от взлома.
Не могли бы вы привести пример того, что вы подразумеваете под «защитой от несанкционированного доступа». Эти ответы кажутся действительно вырванными из контекста.
По вашей логике, никогда не должно быть изменчивости вообще, потому что «намного сложнее защитить систему, если вы не можете быть уверены, что ваши Object защищены от взлома»
Причин как минимум две.
Во-первых - безопасностьhttp://www.javafaq.nu/java-article1060.html
The main reason why String made immutable was security. Look at this example: We have a file open method with login check. We pass a String to this method to process authentication which is necessary before the call will be passed to OS. If String was mutable it was possible somehow to modify its content after the authentication check before OS gets request from program then it is possible to request any file. So if you have a right to open text file in user directory but then on the fly when somehow you manage to change the file name you can request to open "passwd" file or any other. Then a file can be modified and it will be possible to login directly to OS.
Второй - эффективность памятиhttp://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html
JVM internally maintains the "String Pool". To achive the memory efficiency, JVM will refer the String object from pool. It will not create the new String objects. So, whenever you create a new string literal, JVM will check in the pool whether it already exists or not. If already present in the pool, just give the reference to the same object or create the new object in the pool. There will be many references point to the same String objects, if someone changes the value, it will affect all the references. So, sun decided to make it immutable.
Это хороший аргумент в отношении повторного использования, особенно если вы используете String.intern (). Можно было бы использовать повторно, не делая все строки неизменяемыми, но в этот момент жизнь имеет тенденцию усложняться.
В наши дни ни одна из этих причин не кажется мне очень веской.
я не тронут соображениями безопасности. Процесс аутентификации может скопировать значение строки в новый внутренний объект перед проверкой.
Я не слишком убежден аргументом эффективности памяти (т.е. когда два или более объекта String используют одни и те же данные, и один изменяется, тогда оба изменяются). Объекты CString в MFC позволяют обойти это, используя подсчет ссылок.
безопасность на самом деле не является частью смысла существования неизменяемых строк - ваша ОС будет копировать строки в буферы режима ядра и выполнять там проверку доступа, чтобы избежать временных атак. Это действительно все о безопасности потоков и производительности :)
Должен сказать, аргумент безопасности меня совсем не убеждает.
Аргумент безопасности не имеет смысла. Строка неизменна только на этом конкретном языке (Java). Если бы строку «можно было как-то изменить», то это не было бы в Java, потому что я считаю Java очень безопасной - это был бы своего рода нативный эксплойт.
Аргумент эффективности памяти тоже не работает. В родном языке, таком как C, строковые константы - это просто указатели на данные в разделе инициализированных данных - они в любом случае доступны только для чтения / неизменяемы. «если кто-то изменит значение» - опять же, строки из пула в любом случае доступны только для чтения.
Это НЕ ЛУЧШИЙ ОТВЕТ для исходного вопроса, но вместо этого ответ ниже PRINCESS_FLUFF (который также получил больше "полезных" вариантов, чем этот) лучше подходит. По сути, это ответ напрямую от Effective Java, так что поддерживаю Джоша Блоха. Это скорее ответ, не зависящий от платформы, который лучше соответствует исходному заданному вопросу. Это также имеет больше смысла.
@snemarch Безопасных буферов ядра недостаточно. JVM реализует еще один уровень безопасности поверх ОС. Несколько лет назад запуск Java-апплетов в песочнице считался самой крутой особенностью Java. Помните? Я не знаю, повлияла ли безопасность на неизменность строк. Но если бы это было не так, я полагаю, что многим Java API также пришлось бы создавать дополнительные копии строк, прежде чем обрабатывать их, что усложняет их дизайн.
Безопасность потоков и производительность. Если строка не может быть изменена, можно быстро и безопасно передать ссылку между несколькими потоками. Если бы строки были изменяемыми, вам всегда приходилось бы копировать все байты строки в новый экземпляр или обеспечивать синхронизацию. Типичное приложение будет читать строку 100 раз каждый раз, когда эту строку необходимо изменить. См. Википедию на неизменность.
Одним из факторов является то, что, если бы String были изменяемыми, объекты, хранящие String, должны были бы хранить копии, чтобы их внутренние данные не изменились без уведомления. Учитывая, что String является довольно примитивным типом, например числами, приятно, когда с ними можно обращаться, как если бы они были переданы по значению, даже если они передаются по ссылке (что также помогает сэкономить память).
Строки - это не «довольно примитивные типы», как вы их представляете; примитивы можно дешево копировать, дешево сравнивать, дешево хранить, они не могут быть null и сильно оптимизированы компилятором. String - совсем другое дело.
Это компромисс. String входят в пул String, и когда вы создаете несколько идентичных String, они используют одну и ту же память. Разработчики посчитали, что этот метод экономии памяти будет хорошо работать в общем случае, поскольку программы часто перебирают одни и те же строки.
Обратной стороной является то, что конкатенации создают множество дополнительных String, которые являются лишь переходными и просто становятся мусором, фактически снижая производительность памяти. У вас есть StringBuffer и StringBuilder (в Java StringBuilder также есть в .NET), которые можно использовать для экономии памяти в этих случаях.
Имейте в виду, что «пул строк» не используется автоматически для строк ВСЕ, если вы явно не используете строки, указанные в «inter ()».
Согласно Эффективная Java, глава 4, стр. 73, 2-е издание:
"There are many good reasons for this: Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure.
[...]
"Immutable objects are simple. An immutable object can be in exactly one state, the state in which it was created. If you make sure that all constructors establish class invariants, then it is guaranteed that these invariants will remain true for all time, with no effort on your part.
[...]
Immutable objects are inherently thread-safe; they require no synchronization. They cannot be corrupted by multiple threads accessing them concurrently. This is far and away the easiest approach to achieving thread safety. In fact, no thread can ever observe any effect of another thread on an immutable object. Therefore, immutable objects can be shared freely
[...]
Другие небольшие моменты из той же главы:
Not only can you share immutable objects, but you can share their internals.
[...]
Immutable objects make great building blocks for other objects, whether mutable or immutable.
[...]
The only real disadvantage of immutable classes is that they require a separate object for each distinct value.
Прочтите второе предложение моего ответа: неизменяемые классы легче разрабатывать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны.
@PRINCESSFLUFF Я бы добавил, что совместное использование изменяемых строк опасно даже в одном потоке. Например, копирование отчета: report2.Text = report1.Text;. Затем в другом месте измените текст: report2.Text.Replace(someWord, someOtherWord);. Это изменит как первый отчет, так и второй.
@Sam он не спрашивал «почему они не могут быть изменяемыми», он спросил «почему они решили сделать неизменяемыми», на что это отлично отвечает.
@PRINCESSFLUFF Этот ответ конкретно не касается строк. Это был вопрос ОП. Это так расстраивает - это происходит постоянно в SO, а также с вопросами о неизменности String. Ответ здесь говорит об общих преимуществах неизменности. Так почему же не все типы неизменяемы? Не могли бы вы вернуться и обратиться к String?
@Howiecamp Я думаю, что ответ подразумевает, что строки могли быть изменяемыми (ничто не мешает существованию гипотетического изменяемого класса строк). Они просто решили не делать этого для простоты и потому, что это покрывает 99% случаев использования. Они по-прежнему предоставили StringBuilder для остальных 1% случаев.
Я до сих пор не вижу объяснения, почему бы по тем же причинам не сделать массивы неизменяемыми.
Неизменяемость - это хорошо. См. Эффективная Java. Если бы вам приходилось копировать строку каждый раз, когда вы ее передавали, это было бы большим количеством кода, подверженного ошибкам. Вы также не понимаете, какие изменения влияют на какие ссылки. Точно так же, как Integer должен быть неизменным, чтобы вести себя как int, строки должны вести себя как неизменяемые, чтобы действовать как примитивы. В C++ передача строк по значению выполняется без явного упоминания в исходном коде.
String не является примитивным типом, но обычно вы хотите использовать его с семантикой значения, то есть как значение.
Ценность - это то, чему вы можете доверять, и она не изменится за вашей спиной.
Если вы напишете: String str = someExpr();
Вы не хотите, чтобы это изменилось, если ВЫ не сделаете что-то с str.
String как Object имеет естественную семантику указателя, чтобы получить семантику значения, она также должна быть неизменной.
Решение сделать строку изменяемой в C++ вызывает множество проблем, см. Эту отличную статью Кельвина Хенни о Коровье бешенство.
COW = Копировать при записи.
Следует действительно спросить: «Почему X должен быть изменяемым?» Лучше по умолчанию использовать неизменяемость из-за преимуществ, уже упомянутых Принцесса пух. Если что-то изменяемое, должно быть исключением.
К сожалению, большинство текущих языков программирования по умолчанию используют изменяемость, но, надеюсь, в будущем по умолчанию больше используется неизменность (см. Список желаний для следующего основного языка программирования).
String в Java не являются по-настоящему неизменными, вы можете изменить их значение, используя отражение и / или загрузку класса. Вы не должны зависеть от этой собственности в целях безопасности.
Примеры см .: Волшебный трюк на Java
Я считаю, что вы сможете делать такие трюки только в том случае, если ваш код работает с полным доверием, поэтому нет потери безопасности. Вы также можете использовать JNI для записи непосредственно в то место памяти, где хранятся строки.
На самом деле я считаю, что вы можете изменить любой неизменяемый объект с помощью отражения.
На самом деле, причины, по которым строка неизменяема в java, не имеют большого отношения к безопасности. Две основные причины следующие:
Строки - чрезвычайно широко используемый тип объекта. Поэтому более или менее гарантированно его можно использовать в многопоточной среде. Строки неизменяемы, чтобы обеспечить безопасное совместное использование строк между потоками. Наличие неизменяемых строк гарантирует, что при передаче строк из потока A в другой поток B поток B не сможет неожиданно изменить строку потока A.
Это не только помогает упростить и без того довольно сложную задачу многопоточного программирования, но также помогает повысить производительность многопоточных приложений. Доступ к изменяемым объектам должен каким-то образом быть синхронизирован, когда к ним можно получить доступ из нескольких потоков, чтобы один поток не пытался прочитать значение вашего объекта, пока он изменяется другим потоком. Правильная синхронизация трудна для программиста и требует больших затрат во время выполнения. Неизменяемые объекты не могут быть изменены и поэтому не нуждаются в синхронизации.
Хотя упоминалось об интернировании строк, оно представляет собой лишь небольшой выигрыш в эффективности памяти для программ Java. Интернируются только строковые литералы. Это означает, что только те строки, которые одинаковы в вашем исходный код, будут использовать один и тот же String Object. Если ваша программа динамически создает одинаковые строки, они будут представлены в разных объектах.
Что еще более важно, неизменяемые строки позволяют им делиться своими внутренними данными. Для многих строковых операций это означает, что базовый массив символов не нужно копировать. Например, предположим, что вы хотите взять пять первых символов строки. В Java вы должны вызвать myString.substring (0,5). В этом случае метод substring () просто создает новый объект String, который разделяет базовый char [] myString, но кто знает, что он начинается с индекса 0 и заканчивается индексом 5 этого char []. Чтобы представить это в графической форме, вы получите следующее:
| myString |
v v
"The quick brown fox jumps over the lazy dog" <-- shared char[]
^ ^
| | myString.substring(0,5)
Это делает операции такого рода чрезвычайно дешевыми и O (1), поскольку операция не зависит ни от длины исходной строки, ни от длины подстроки, которую нам нужно извлечь. Такое поведение также имеет некоторые преимущества для памяти, поскольку многие строки могут совместно использовать свой базовый char [].
Реализация подстрок в виде ссылок, которые совместно используют char[], является довольно сомнительным дизайнерским решением. Если вы читаете весь файл в одну строку и поддерживаете ссылку только на подстроку из 1 символа, весь файл придется хранить в памяти.
Точно, я столкнулся с этой конкретной проблемой при создании краулера веб-сайта, которому нужно было всего лишь извлечь несколько слов со всей страницы. Весь HTML-код страницы находился в памяти, и из-за того, что подстрока разделяла char [], я сохранил весь HTML-код, хотя мне нужно было всего несколько байтов. Обходной путь для этого - использовать new String (original.substring (.., ..)), конструктор String (String) создает копию соответствующего диапазона базового массива.
Дополнение к описанию последующих изменений: Начиная с Jave 7, String.substring() выполняет полное копирование, чтобы предотвратить проблемы, упомянутые в комментариях выше. В Java 8 два поля, разрешающих совместное использование char[], а именно count и offset, удалены, тем самым уменьшая объем памяти, занимаемой экземплярами String.
Я согласен с частью Thead Safety, но сомневаюсь в использовании подстроки.
@LoveRight: Затем проверьте исходный код java.lang.String (grepcode.com/file/repository.grepcode.com/java/root/jdk/ope njdk /…), так было до Java 6 (которая была актуальной, когда был написан этот ответ). Я, по-видимому, изменился в Java 7.
Ух ты! Я не могу поверить в эту дезинформацию. Неизменяемость String не имеет ничего общего с безопасностью. Если у кого-то уже есть доступ к объектам в запущенном приложении (что следует предполагать, если вы пытаетесь защитить себя от «взлома» String в вашем приложении), у него наверняка будет множество других возможностей для взлома.
Совершенно новая идея заключается в том, что неизменность String решает проблемы многопоточности. Хммм ... У меня есть объект, который изменяется двумя разными потоками. Как мне решить эту проблему? синхронизировать доступ к объекту? Неаууууууу ... давайте вообще не позволяем никому изменять объект - это решит все наши беспорядочные проблемы с параллелизмом! Фактически, давайте сделаем все объекты неизменяемыми, а затем мы сможем удалить синхронизированную конструкцию из языка Java.
Настоящая причина (указанная другими выше) - это оптимизация памяти. В любом приложении один и тот же строковый литерал часто используется многократно. На самом деле это настолько распространено, что несколько десятилетий назад многие компиляторы оптимизировали хранение только одного экземпляра литерала String. Недостатком этой оптимизации является то, что код среды выполнения, изменяющий литерал String, создает проблему, поскольку он изменяет экземпляр для всего остального кода, который его разделяет. Например, для функции где-нибудь в приложении было бы нехорошо изменить буквальный String"dog" на "cat". printf("dog") приведет к записи "cat" в стандартный вывод. По этой причине необходим способ защиты от кода, который пытается изменить литералы String (т.е. сделать их неизменяемыми). Некоторые компиляторы (при поддержке ОС) могли бы добиться этого, поместив литерал String в специальный сегмент памяти, доступный только для чтения, что вызвало бы сбой памяти при попытке записи.
В Java это называется интернированием. Компилятор Java здесь просто следует стандартной оптимизации памяти, выполняемой компиляторами на протяжении десятилетий. И чтобы решить ту же проблему, что и эти литералы String, изменяемые во время выполнения, Java просто делает класс String неизменяемым (т. Е. Не дает вам никаких установщиков, которые позволили бы вам изменять содержимое String). String не должен был бы быть неизменным, если бы не происходило интернирование литералов String.
Я категорически не согласен с неизменяемостью и потоками комментариев, мне кажется, вы не совсем понимаете суть. И если Джош Блох, один из разработчиков Java, говорит, что это была одна из проблем проектирования, как это может быть дезинформацией?
Синхронизация стоит дорого. Ссылки на изменяемые объекты необходимо синхронизировать, а не на неизменяемые. Это причина сделать все объекты неизменяемыми, если только они не должны быть изменяемыми. Строки могут быть неизменяемыми, что делает их более эффективными при работе с несколькими потоками.
@Jim: Оптимизация памяти - это не причина, а причина. Поточная безопасность также является причиной «А», потому что неизменяемые объекты по своей сути потокобезопасны и не требуют дорогостоящей синхронизации, как сказал Дэвид. На самом деле безопасность потоков - это побочный эффект неизменности объекта. Вы можете думать о синхронизации как о способе сделать объект «временно» неизменяемым (ReaderWriterLock сделает его доступным только для чтения, а обычная блокировка сделает его вообще недоступным, что, конечно, также делает его неизменяемым).
@DavidThornley: Создание нескольких независимых путей ссылок на изменяемый держатель значения эффективно превращает его в сущность и значительно усложняет рассуждение даже помимо проблем с потоками. Как правило, изменяемые объекты более эффективны, чем неизменяемые, в тех случаях, когда для каждого будет существовать ровно один путь ссылки, но неизменяемые объекты позволяют эффективно совместно использовать содержимое объектов путем совместного использования ссылок. Наилучший образец представлен String и StringBuffer, но, к сожалению, этой модели следуют немногие другие типы.
Для большинства целей «строка» - это (используется / рассматривается как / считается / предполагается) значимое атомная единица,просто как число.
Вы должны знать почему. Просто подумай об этом.
Ненавижу это говорить, но, к сожалению, мы обсуждаем это, потому что наш язык - отстой, и мы пытаемся использовать одно слово, нить, для описания сложной, контекстно расположенной концепции или класса объекта.
Мы выполняем вычисления и сравнения со «строками» так же, как с числами. Если бы строки (или целые числа) были изменяемыми, нам пришлось бы написать специальный код, чтобы зафиксировать их значения в неизменяемых локальных формах, чтобы надежно выполнять любые вычисления. Поэтому лучше думать о строке как о числовом идентификаторе, но вместо того, чтобы иметь длину 16, 32 или 64 бита, она может быть длиной в сотни бит.
Когда кто-то говорит «струна», мы все думаем о разных вещах. Те, кто думают об этом просто как о наборе символов без какой-либо конкретной цели, конечно, будут потрясены тем, что кто-то просто решил, что они не могут манипулировать этими персонажами. Но «строковый» класс - это не просто массив символов. Это STRING, а не char[]. Есть несколько основных предположений относительно концепции, которую мы называем «строкой», и ее обычно можно описать как значимую атомарную единицу закодированных данных, таких как число. Когда люди говорят о «манипулировании строками», возможно, они действительно говорят о манипулировании символы для построения струны, и StringBuilder отлично подходит для этого. Подумайте немного о том, что на самом деле означает слово «строка».
Подумайте на мгновение, что было бы, если бы строки были изменяемыми. Следующая функция API может быть обманута для возврата информации для другого пользователя, если строка имени пользователя изменчивый намеренно или непреднамеренно изменена другим потоком, пока эта функция ее использует:
string GetPersonalInfo( string username, string password )
{
string stored_password = DBQuery.GetPasswordFor( username );
if (password == stored_password)
{
//another thread modifies the mutable 'username' string
return DBQuery.GetPersonalInfoFor( username );
}
}
Безопасность - это не только «контроль доступа», это также «безопасность» и «гарантия правильности». Если метод не может быть легко написан и надежно выполнять простые вычисления или сравнение, то вызывать его небезопасно, но было бы безопасно поставить под сомнение сам язык программирования.
В C# строка может изменяться с помощью указателя (используйте unsafe) или просто посредством отражения (вы можете легко получить базовое поле). Это делает недействительным пункт о безопасности, поскольку любой, кто намеренно хочет изменить строку, может сделать это довольно легко. Тем не менее, он обеспечивает безопасность программистов: если вы не сделаете что-то особенное, строка будет гарантированно неизменной (но не поточно-ориентированной!).
Да, вы можете изменить байты любого объекта данных (строка, int и т. д.) С помощью указателей. Однако мы говорим о том, почему строковый класс неизменяем в том смысле, что в него не встроены общедоступные методы для изменения его символов. Я говорил, что строка очень похожа на число в том смысле, что манипулирование отдельными символами имеет не больше смысла, чем манипулирование отдельными битами числа (когда вы обрабатываете строку как целый токен (а не как массив байтов), а число как числовое значение (не как битовое поле) .Мы говорим на уровне концептуального объекта, а не на уровне подобъекта.
И чтобы уточнить, указатели в объектно-ориентированном коде по своей сути небезопасны именно потому, что они обходят общедоступные интерфейсы, определенные для класса. Я говорил, что функцию можно было бы легко обмануть, если бы открытый интерфейс для строки позволял изменять ее другими потоками. Конечно, его всегда можно обмануть, обращаясь к данным напрямую с помощью указателей, но не так легко или непреднамеренно.
«указатели в объектно-ориентированном коде по своей сути небезопасны», если вы не назовете их Рекомендации. Ссылки в Java не отличаются от указателей в C++ (отключена только арифметика указателей). Другая концепция - это управление памятью, которым можно управлять или вручную, но это другое дело. У вас может быть ссылочная семантика (указатели без арифметики) без GC (обратное будет сложнее в том смысле, что семантику достижимости будет труднее сделать чистой, но не невыполнимой)
Другое дело, что если строки являются неизменяемыми почти, но не совсем так (я не знаю здесь достаточно CLI), это может быть очень плохо по соображениям безопасности. В некоторых более старых реализациях Java вы могли это сделать, и я нашел фрагмент кода, который использовал это для строк усвоить (попробуйте найти другую внутреннюю строку с тем же значением, поделитесь указателем, удалите старый блок памяти) и использовал бэкдор чтобы переписать содержимое строки, вызывая некорректное поведение в другом классе. (Попробуйте переписать «ВЫБРАТЬ *» на «УДАЛИТЬ»)
В C# можно атомарно изменить один бит целого числа [благодаря CompareExchange]. В Java это невозможно сделать, если целое число не инкапсулировано в объект AtomicInteger.
Неизменность не так тесно связана с безопасностью. Для этого, по крайней мере, в .NET, вы получаете класс SecureString.
Позднее редактирование: в Java вы найдете GuardedString, аналогичную реализацию.
Почти из каждого правила есть исключения:
using System;
using System.Runtime.InteropServices;
namespace Guess
{
class Program
{
static void Main(string[] args)
{
const string str = "ABC";
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
var handle = GCHandle.Alloc(str, GCHandleType.Pinned);
try
{
Marshal.WriteInt16(handle.AddrOfPinnedObject(), 4, 'Z');
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
}
finally
{
handle.Free();
}
}
}
}
Я знаю, что это шишка, но ... Они действительно неизменны? Обратите внимание на следующее.
public static unsafe void MutableReplaceIndex(string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
...
string s = "abc";
MutableReplaceIndex(s, '1', 0);
MutableReplaceIndex(s, '2', 1);
MutableReplaceIndex(s, '3', 2);
Console.WriteLine(s); // Prints 1 2 3
Вы даже можете сделать это методом расширения.
public static class Extensions
{
public static unsafe void MutableReplaceIndex(this string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
}
Что делает следующую работу
s.MutableReplaceIndex('1', 0);
s.MutableReplaceIndex('2', 1);
s.MutableReplaceIndex('3', 2);
Вывод: они находятся в неизменяемом состоянии, известном компилятору. Конечно, вышесказанное применимо только к строкам .NET, поскольку в Java нет указателей. Однако строка может быть полностью изменена с помощью указателей в C#. Указатели не предназначены для использования, имеют практическое применение или безопасное использование; однако это возможно, таким образом нарушая все «изменчивое» правило. Обычно вы не можете изменять индекс непосредственно строки, и это единственный способ. Есть способ предотвратить это, запретив экземпляры указателей строк или создав копию, когда указана строка, но ни то, ни другое не делается, что делает строки в C# не полностью неизменяемыми.
+1. Строки .NET на самом деле не являются неизменяемыми; фактически, это постоянно делается в классах String и StringBuilder по соображениям производительности.
У меня была такая же мысль, но я проверил расположение оригинальных плакатов и обнаружил, что они из Бельгии. Учитывая, что это означает, что они вряд ли будут носителями английского языка. В сочетании с тем фактом, что большинство туземцев плохо владеют языком, я решил немного расслабить ее.