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

StackOverflow https://stackoverflow.com/questions/93091

  •  01-07-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

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

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

[...]

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

[...]

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

[...]

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

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

[...]

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

[...]

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

Другие советы

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

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

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

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

JVM внутренне поддерживает «Pool Bool».Чтобы достичь эффективности памяти, JVM будет направлять объект String из пула.Это не создаст новые строковые объекты.Итак, всякий раз, когда вы создаете новую строку буквальную литературу, JVM будет проверять пул, независимо от того, существует ли он или нет.Если уже присутствует в бассейне, просто дайте ссылку на тот же объект или создайте новый объект в пуле.Будет много ссылок на одни и те же строковые объекты, если кто -то изменит значение, это повлияет на все ссылки.Итак, Сан решил сделать его неизменным.

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

Безопасность головы:

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

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

Производительность:

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

Что еще более важно, неизменяемые строки позволяют им делиться своими внутренними данными.Для многих строковых операций это означает, что базовый массив символов не нужно копировать.Например, предположим, что вы хотите взять пять первых символов строки.В 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[].

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

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

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

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

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

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

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

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

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

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

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

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

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

Решение сделать строку изменяемой в C++ вызывает множество проблем. См. прекрасную статью Кельвина Хенни об этом. Коровье бешенство.

КОРОВА = Копировать при записи.

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

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

Почти для каждого правила есть исключение:

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top