Почему строки не могут быть изменяемыми в Java и.NET?

Почему они решили сделать строку неизменной в Java и.NET (и некоторых других языках)? Почему они не сделали его изменчивым?

17 ответов

Решение

Согласно Эффективной Java, глава 4, стр. 73, 2-е издание:

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

[...]

" Неизменяемые объекты просты. Неизменяемый объект может находиться точно в одном состоянии, в том состоянии, в котором он был создан. Если вы убедитесь, что все конструкторы устанавливают инварианты классов, то гарантируется, что эти инварианты будут оставаться верными в течение всего времени, с никаких усилий с вашей стороны.

[...]

Неизменяемые объекты по своей природе поточно-ориентированы; они не требуют синхронизации. Они не могут быть повреждены несколькими потоками, обращающимися к ним одновременно. Это, безусловно, самый простой подход к обеспечению безопасности потоков. Фактически, ни один поток не может наблюдать какое-либо влияние другого потока на неизменный объект. Следовательно, неизменяемые объекты могут свободно использоваться

[...]

Другие маленькие пункты из той же главы:

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

[...]

Неизменяемые объекты создают отличные строительные блоки для других объектов, как изменяемых, так и неизменяемых.

[...]

Единственным реальным недостатком неизменяемых классов является то, что им требуется отдельный объект для каждого отдельного значения.

Есть как минимум две причины.

Первый - безопасность http://www.javafaq.nu/java-article1060.html

Основной причиной, по которой Стринг стал неизменным, была безопасность. Посмотрите на этот пример: у нас есть метод открытия файла с проверкой входа. Мы передаем String этому методу для обработки аутентификации, которая необходима перед передачей вызова в ОС. Если строка была изменяемой, то можно было каким-то образом изменить ее содержимое после проверки подлинности до того, как ОС получит запрос от программы, тогда можно запросить любой файл. Поэтому, если у вас есть право открывать текстовый файл в пользовательском каталоге, но на лету, когда вам как-то удастся изменить имя файла, вы можете запросить открыть файл "passwd" или любой другой. Затем файл может быть изменен, и можно будет войти непосредственно в ОС.

Второе - эффективность памяти http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html

JVM поддерживает "пул строк". Для достижения эффективности использования памяти JVM будет ссылаться на объект String из пула. Он не будет создавать новые объекты String. Таким образом, всякий раз, когда вы создаете новый строковый литерал, JVM проверит в пуле, существует ли он уже или нет. Если он уже присутствует в пуле, просто укажите ссылку на тот же объект или создайте новый объект в пуле. Будет много ссылок, указывающих на одни и те же объекты String, если кто-то изменит значение, это повлияет на все ссылки. Итак, солнце решило сделать его неизменным.

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

Теад Безопасность:

Строки - чрезвычайно широко используемый тип объекта. Поэтому более или менее гарантировано использование в многопоточной среде. Строки являются неизменяемыми, чтобы быть уверенными в том, что безопасно разделять строки между потоками. Наличие неизменяемых строк гарантирует, что при передаче строк из потока A в другой поток B поток B не может неожиданно изменить строку потока A.

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

Спектакль:

Хотя было упомянуто интернирование String, оно представляет лишь небольшой выигрыш в эффективности памяти для программ Java. Только строковые литералы интернированы. Это означает, что только строки, одинаковые в вашем исходном коде, будут использовать один и тот же объект String. Если ваша программа динамически создает одинаковые строки, они будут представлены в разных объектах.

Что еще более важно, неизменные строки позволяют им обмениваться своими внутренними данными. Для многих строковых операций это означает, что базовый массив символов не нужно копировать. Например, скажем, вы хотите взять пять первых символов строки. В Java вы бы вызвали myString.substring(0,5). В этом случае метод substring() просто создает новый объект String, который разделяет лежащий в основе myString символ char[], но кто знает, что он начинается с индекса 0 и заканчивается индексом 5 этого char[]. Чтобы поместить это в графическую форму, вы должны получить следующее:

 |               myString                  |
 v                                         v
"The quick brown fox jumps over the lazy dog"   <-- shared char[]
 ^   ^
 |   |  myString.substring(0,5)

Это делает этот вид операций чрезвычайно дешевым, и O(1), поскольку операция не зависит ни от длины исходной строки, ни от длины подстроки, которую мы должны извлечь. Такое поведение также имеет некоторые преимущества памяти, так как многие строки могут использовать свой основной символ [].

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

Нужно спросить: "Почему Х должен быть изменчивым?" Лучше по умолчанию использовать неизменяемость из-за преимуществ, уже упомянутых Принцессой Пухом. Должно быть исключение, что что-то изменчивое.

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

Вот Это Да! Я не могу поверить дезинформации здесь. Строки, являющиеся неизменяемыми, не имеют ничего общего с безопасностью. Если у кого-то уже есть доступ к объектам в работающем приложении (что можно предположить, если вы пытаетесь защититься от того, что кто-то "взломал" строку в вашем приложении), у него наверняка будет множество других возможностей для взлома.

Это довольно новая идея, что неизменность String решает проблемы с многопоточностью. Хм... У меня есть объект, который изменяется двумя разными потоками. Как мне решить это? синхронизировать доступ к объекту? Naawww ... давайте не будем никого менять объект - это решит все наши проблемы с параллельным выполнением! Фактически, давайте сделаем все объекты неизменяемыми, а затем мы сможем удалить синхронизированный конструкт из языка Java.

Настоящая причина (указанная выше) - это оптимизация памяти. В любом приложении достаточно часто использовать один и тот же строковый литерал. Фактически, это настолько распространено, что десятилетия назад многие компиляторы оптимизировали хранение только одного экземпляра строкового литерала. Недостаток этой оптимизации заключается в том, что код времени выполнения, который модифицирует строковый литерал, создает проблему, поскольку он изменяет экземпляр для всего другого кода, который разделяет его. Например, было бы нехорошо для функции где-то в приложении изменять строковый литерал "собака" на "кошка". Printf("собака") приведет к тому, что "кошка" будет записана на стандартный вывод. По этой причине должен быть способ защиты от кода, который пытается изменить строковые литералы (то есть сделать их неизменяемыми). Некоторые компиляторы (с поддержкой со стороны ОС) могли бы выполнить это, поместив строковый литерал в специальный сегмент памяти только для чтения, который мог вызвать ошибку памяти, если была предпринята попытка записи.

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

String не является примитивным типом, но обычно вы хотите использовать его с семантикой значения, то есть как значение.

Ценность, которой вы можете доверять, не изменится за вашей спиной. Если вы напишите: String str = someExpr();Вы не хотите, чтобы это изменилось, если вы не делаете что-то с ул.

Строка как объект имеет естественную семантику указателя, чтобы получить семантику значения, она также должна быть неизменной.

Одним из факторов является то, что если бы строки были изменяемыми, объекты, хранящие строки, должны были бы соблюдать осторожность при хранении копий, чтобы их внутренние данные не изменились без уведомления. Учитывая, что строки являются довольно примитивным типом, подобным числам, хорошо, когда можно обращаться с ними так, как будто они были переданы по значению, даже если они передаются по ссылке (что также помогает экономить память).

Я знаю, что это удар, но... Они действительно неизменны? Учтите следующее.

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# не полностью неизменяемыми.

Для большинства целей "строка" - это (используется / рассматривается как / считается / считается) значимая атомная единица, как и число.

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

Ты должен знать почему. Просто подумай об этом.

Мне неприятно это говорить, но, к сожалению, мы обсуждаем это, потому что наш язык отстой, и мы пытаемся использовать одно слово, строку, чтобы описать сложную, контекстуально расположенную концепцию или класс объекта.

Мы выполняем вычисления и сравнения с "строками", аналогично тому, как мы делаем с числами. Если бы строки (или целые числа) были изменяемыми, мы должны были бы написать специальный код, чтобы зафиксировать их значения в неизменяемых локальных формах, чтобы надежно выполнять любые вычисления. Поэтому лучше всего рассматривать строку как числовой идентификатор, но вместо длины в 16, 32 или 64 бита она может быть длиной в сотни бит.

Когда кто-то говорит "струна", мы все думаем о разных вещах. Те, кто думает об этом просто как о наборе персонажей, не имея особой цели, конечно же, будут потрясены тем, что кто-то просто решил, что он не сможет манипулировать этими персонажами. Но класс "string" - это не просто массив символов. Это 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 );
    }
}

Безопасность - это не только "контроль доступа", но и "безопасность" и "обеспечение правильности". Если метод не может быть легко написан и зависит от надежного выполнения простого вычисления или сравнения, тогда его небезопасно вызывать, но было бы безопасно подвергать сомнению сам язык программирования.

Неизменность не так тесно связана с безопасностью. Для этого, по крайней мере, в.NET, вы получаете класс SecureString.

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

Недостатком является то, что конкатенации создают много дополнительных строк, которые являются только переходными и просто становятся мусором, что фактически ухудшает производительность памяти. У вас есть StringBuffer и StringBuilder (в Java StringBuilder также находится в.NET) для использования для сохранения памяти в этих случаях.

Строки в Java не являются действительно неизменяемыми, вы можете изменить их значение, используя отражение и / или загрузку классов. Вы не должны зависеть от этого свойства в целях безопасности. Примеры см.: Волшебный трюк в Java

Решение иметь изменяемую строку в C++ вызывает много проблем, см. Эту превосходную статью Келвина Хенни о Mad COW Disease.

COW = Копировать при записи.

Неизменность это хорошо. Смотрите Эффективная Java. Если бы вам приходилось копировать строку каждый раз, когда вы ее передавали, то это было бы очень подверженным ошибкам кодом. У вас также есть путаница относительно того, какие модификации влияют на какие ссылки. Точно так же, как Integer должен быть неизменным, чтобы вести себя как int, строки должны вести себя как неизменные, чтобы действовать как примитивы. В C++ передача строк по значению делает это без явного упоминания в исходном коде.

Строка является неизменным (однажды созданный объект не может быть изменен). Объект, созданный в виде строки, сохраняется в пуле константных строк. Каждый неизменяемый объект в Java является поточно-ориентированным, что означает, что String также является поточно-ориентированным. Строка не может использоваться двумя потоками одновременно. Строка, однажды назначенная, не может быть изменена.

String  demo = " hello " ;

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

demo="Bye" ;  

// новая строка "Bye" создается в пуле констант и на нее ссылается демо-переменная

// строка "hello" все еще существует в пуле строковых констант, и ее значение не переопределяется, но мы потеряли ссылку на строку "hello"

Существует исключение почти для каждого правила:

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();
            }
        }
    }
}

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

Другие вопросы по тегам