JavaRush /Java Blog /Random-KO /아, 이 대사는...
articles
레벨 15

아, 이 대사는...

Random-KO 그룹에 게시되었습니다
java.lang.String 클래스는 아마도 Java에서 가장 많이 사용되는 클래스 중 하나일 것입니다. 그리고 매우 자주 문맹으로 사용되어 주로 성능과 관련하여 많은 문제를 야기합니다. 이 기사에서는 문자열, 문자열 사용의 복잡성, 문제의 원인 등에 대해 이야기하고 싶습니다.
아 이 대사는... - 1
우리가 이야기할 내용은 다음과 같습니다.
  • 라인 장치
  • 문자열 리터럴
  • 문자열 비교
  • 문자열 추가
  • 하위 문자열 가져오기 및 복사 생성자
  • 회선 변경
  • 기본부터 시작해 보겠습니다.

라인 장치

java.lang.String 클래스에는 세 가지 필드가 포함되어 있습니다.
/**
 * NOTE: This is just a partial API
 */
public final class String{

    private final char value[];
    private final int offset;
    private final int count;

}
실제로 여기에는 해시 코드와 같은 다른 필드도 포함되어 있지만 지금은 중요하지 않습니다. 주요 내용은 다음과 같습니다. 따라서 문자열의 기본은 문자 배열( char )입니다. 유니코드 UTF-16BE 인코딩은 문자를 메모리에 저장할 때 사용됩니다 . 자세한 내용은 여기에서 읽어보실 수 있습니다 . Java 5.0부터 2 이상의 유니코드 버전에 대한 지원이 도입되었으며 그에 따라 0xFFFF 보다 큰 코드를 가진 문자에 대한 지원이 도입되었습니다 . 이러한 문자의 경우 하나의 문자 가 아닌 두 개의 문자가 사용됩니다. 이러한 문자의 인코딩에 대한 자세한 내용은 동일한 문서에 나와 있습니다 . 이러한 기호에 대한 지원이 도입되었지만 문제는 표시되지 않는다는 것입니다. 나는 일련의 음악 기호( U1D100 )를 발견하고 어딘가(코드 1D120의 기호)에 높은음자리표를 표시하려고 했습니다. 예상대로 코드를 '\uD834' 및 '\uDD20'이라는 두 개의 문자 로 변환했습니다. 디코더는 이에 대해 불평하지 않고 정직하게 하나의 문자로 인식합니다. 그런데 이 기호가 존재하는 글꼴이 없습니다. 따라서 - 정사각형. 그리고 분명히 이것은 오랫동안 지속될 것입니다. 따라서 유니코드 4에 대한 지원 도입은 미래 기반의 프리즘을 통해서만 볼 수 있습니다. 더 나아가자. 두 번째와 세 번째 필드인 offsetcount 에 세심한 주의를 기울여 주시기 바랍니다 . 모든 문자가 사용되면 배열이 문자열을 완전히 정의하는 것처럼 보입니다 . 이러한 필드가 있는 경우 배열의 모든 문자를 사용할 수는 없습니다. 따라서 부분 문자열 선택 및 복사 생성자 섹션에서 이에 대해 이야기하겠습니다.

문자열 리터럴

What такое строковый литерал? Это строка, записаная в двойных кавычках, например, такая: "abc". Такие выражения используются в codeе сплошь и рядом. Строка эта может содержать escape-последовательности unicode, например, \u0410, что будет соответствовать русской букве 'А'. Однако, эта строка НЕ МОЖЕТ содержать последовательностей \u000A и \u000D, соответствующие символам LF и CR соответственно. Дело в том, что последовательности обрабатываются на самой ранней стадии компиляции, и символы эти будут заменены на реальные LF и CR (How если бы в редакторе просто нажали "Enter"). Для вставки в строку этих символов следует использовать последовательности \n и \r, соответственно. Строковые литералы сохраняются в пуле строк. Я упоминал о пуле в статье о сравнении на практике, но повторюсь. Виртуальная машина Java поддерживает пул строк. В него кладутся все строковые литералы, объявленные в codeе. При совпадении литералов (с точки зрения equals, см. тут) используется один и тот же an object, находящийся в пуле. Это позволяет сильно экономить память, а в некоторых случаях и повышать производительность. Дело в том, что строку в пул можно поместить принудительно, с помощью метода String.intern(). Этот метод возвращает из пула строку, равную той, у которой был вызван этот метод. Если же такой строки нет – в пул кладется та, у которой вызван метод, после чего возвращается link на нее же. Таким образом, при грамотном использовании пула появляется возможность сравнивать строки не по значению, через equals, а по ссылке, что значительно, на порядки, быстрее. Так реализован, например, класс java.util.Locale, который имеет дело с кучей маленьких, в основном двухсимвольных, строк – codeами стран, языков и т.п. См. также тут: Сравнение an objectов: практика – метод String.intern. Очень часто я вижу в различной литературе конструкции следующего вида:
public static final String SOME_STRING = new String("abc");
Если говорить еще точнее, нарекания у меня вызывает new String("abc"). Дело в том, что конструкция эта – безграмотна. В Java строковый литерал – "abc" – УЖЕ является an objectом класса String. А потому, использование еще и конструктора приводит к КОПИРОВАНИЮ строки. Поскольку строковый литерал уже хранится в пуле, и никуда из него не денется, то созданный НОВЫЙ an object – ничто иное How пустая трата памяти. Эту конструкцию с чистой совестью можно переписать вот так:
public static final String SOME_STRING = "abc";
С точки зрения codeа это будет абсолютно то же самое, но несколько эффективнее. Переходим к следующему вопросу –

Сравнение строк

Собственно, все об этом вопросе я уже писал в статье Сравнение an objectов: практика. И добавить больше нечего. Резюмируя сказаное там – строки надо сравнивать по значению, с использованием метода equals. По ссылке их можно сравнивать, но аккуратно, только если точно знаешь, что делаешь. В этом помогает метод String.intern. Единственный момент, который хотелось бы упомянуть – сравнение с литералами. Я часто вижу конструкции типа str.equals("abc"). И тут есть небольшие грабли – перед этим сравнением правильно бы было сравнить str с null, чтобы не получить NullPointerException. Т.е. правильной будет конструкция str != null && str.equals("abc"). Между тем – ее можно упростить. Достаточно написать всего лишь "abc".equals(str). Проверка на null в этом случае не нужна. На очереди у нас...

Сложение строк

Строки – единственный an object, для которого определена операция сложения ссылок. Во всяком случае, так было до версии Java 5.0, в которой появился autoboxing/unboxing, но речь сейчас не об этом. Общее описание принципа работы оператора конкатенации можно найти в статье о linkх, а именно – тут. Я же хочу затронуть более глубокий уровень. Представьте себе, представьте себе... Прямо How в песенке про кузнечика. :) Так вот, представьте себе, что нам надо сложить две строки, вернее, к одной прибавить другую:
String str1 = "abc";
str1 += "def";
Как происходит сложение? Поскольку an object класса строки неизменяем, то результатом сложения будет новый an object. Итак. Сначала выделяется память, достаточная для того, чтобы вместить туда содержимое обеих строк. В эту память копируется содержимое сначала первой строки, потом второй. Далее переменной str1 присваивается link на новую строку, а старая строка отбрасывается. Усложним задачу. Пусть у нас есть файл из четырех строк:
abc
def
ghi
jkl
Нам надо прочитать эти строки и собрать их в одну. Поступаем по той же схеме.
BufferedReader br = new BufferedReader(new FileReader("... filename ..."));
String result = "";
while(true){
    String line = br.readLine();
    if (line == null) break;
    result += line;
}
Вроде пока все хорошо и логично. Давайте разберем, что происходит на нижнем уровне. Первый проход цикла. result="", line="abc". Выделяется память на 3 символа, туда копируется содержимое line"abc". Переменной result присваивается link на новую строку, старая отбрасывается. Второй проход цикла. result="abc", line="def". Выделяется память на 6 символов, туда копируется содержимое result"abc", затем line"def". Переменной result присваивается link на новую строку, старая отбрасывается. Третий проход цикла. result="abcdef", line="ghi". Выделяется память на 9 символов, туда копируется содержимое result"abcdef", затем line"ghi". Переменной result присваивается link на новую строку, старая отбрасывается. Четвертый проход цикла. result="abcdefghi", line="jkl". Выделяется память на 12 символов, туда копируется содержимое result"abcdefghi", затем line"jkl". Переменной result присваивается link на новую строку, старая отбрасывается. Пятый проход цикла. result="abcdefghijkl", line=null. Цикл закончен. Итак. Три символа "abc" копировались в памяти 4 раза, "def" – 3 раза, "ghi" – 2 раза, "jkl" – один раз. Страшно? Не особо? А вот теперь представьте себе файл с длиной строки 80 символов, в котором где-то 1000 строк. Всего-навсего 80кб. Представor? What будет в этом случае? первая строка, How нетрудно подсчитать, будет скопирована в памяти 1000 раз, вторая – 999 и т.д. И при средней длине 80 символов через память пройдет ((1000 + 1) * 1000 / 2) * 80 = ... барабанная дробь... 40 040 000 символов, что составляет около 80 Мб (!!!) памяти. Каков же итог ТАКОГО цикла? Чтение 80-килоbyteного file вызвало выделение 80 Мб памяти. Ни много ни мало – в 1000 раз больше, чем полезный объем. Какой из этого следует сделать вывод? Очень простой. Никогда, запомните – НИКОГДА не используйте прямую конкатенацию строк, особенно в циклах. Даже в Howом-нибудь методе toString, если он вызывается достаточно часто, имеет смысл использовать StringBuffer instead of конкатенации. Собственно, компилятор при оптимизации чаще всего так и делает – прямые сложения он выполняет через StringBuffer. Однако в случаях, подобных тому, что привел я, оптимизацию компилятор сделать не в состоянии. What и приводит к весьма печальным последствиям, описаным чуть ниже. К сожалению, подобные конструкции встречаются слишком часто. Потому я и счел необходитмым заострить на этом внимание. Собственный опыт Не могу не вспомнить один эпизод из собственной практики. Один из программистов, работавших со мной, How-то пожаловался, что у него очень медленно работает его code. Он читал достаточно большой файл в HTML формате, после чего производил Howие-то манипуляции. И действительно, работало все с черепашьей speedю. Я взял посмотреть исходник, и обнаружил, что он... использует конкатенацию строк. У него было по 200-250 строк в каждом файле, и при чтении file около 200Кб через память проходило более 40Мб! В итоге я переписал немного code, заменив операции со строками на операции со StringBuffer-ом. Честно сказать, когда я запустил переписаный code, я подумал, что он просто где-то "упал". Обработка занимала доли секунды. Скорость выросла в 300-800 раз. После этого я коренным образом пересмотрел свое отношение к строковым операциям. Следующий акт марлезонского балета –

Выборка подстроки и копирующий конструктор

Представим, что у нас есть строка, из которой надо вырезать подстроку. Вопроса "How это сделать" не стоит – и так понятно. Вопрос в другом – что при этом происходит?
String str = "abcdefghijklmnopqrstuvwxyz";
str = str.substring(5,10);
Вроде тривиальный code. И первая мысль такая – выбирается подстрока "efghi", переменной str присваивается link на новую строку, а старый an object отбрасывается. Так? Почти. Дело в том, что для увеличения скорости при выборке подстроки используется ТОТ ЖЕ МАССИВ, что и в исходной строке. Иначе говоря, мы получим не an object, в котором массив value (cм. устройство строки) имеет длину 5 и содержит в себе символы 'e', 'f', 'g', 'h' и 'i', count=5 и offset=0. Нет, длина массива будет по-прежнему 26, count=5 и offset=5. И при отбрасывании старой строки массив НЕ ОТБРОСИТСЯ, а по-прежнему будет находиться в памяти, ибо на него есть link из новой строки. И существовать в памяти он будет до того момента, How будет отброшена уже новая строка. Это совсем неочевидный момент, который может привести к проблемам с памятью. Возникает вопрос – How этого избежать? Ответ – с помощью копирующего конструктора String(String). Дело в том, что в этом конструкторе в явном виде выделяется память под новую строку, и в эту память копируется содержимое исходной. Таким образом, если мы перепишем code так:
String str = "abcdefghijklmnopqrstuvwxyz";
str = new String(str.substring(5,10));
..., то длина массива value у an object str будет действительно 5, count=5 и offset=0. И это – единственный случай, где оправдано применение копирующего конструктора для строки. И How финальный аккорд –

Изменение строки

이것은 그 라인과 거의 관련이 없습니다. 저는 문자열이 어느 정도까지만 불변이라는 사실을 강조하고 싶습니다. 코드는 다음과 같습니다.
package tests;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * This application demonstrates how to modify java.lang.String object
 * through reflection API.
 *
 * @version 1.0
 * @author Eugene Matyushkin
 */
public class StringReverseTest {

    /**
     * final static string that should be modified.
     */
    public static final String testString = "abcde";

    public static void main(String[] args) {
        try{
            System.out.println("Initial static final string:  "+testString);
            Field[] fields = testString.getClass().getDeclaredFields();
            Field value = null;
            for(int i=0; i
여기서 무슨 일이 일어나고 있는 걸까요? 먼저 char[] 유형의 필드를 찾고 있습니다 . 이름으로도 검색이 가능했어요. 그러나 이름은 바뀔 수 있지만 유형이 매우 의심됩니다. 다음으로, 찾은 필드에서 setAccessible(true) 메소드를 호출합니다 . 이것이 핵심 사항입니다. 필드에 대한 액세스 수준 확인을 비활성화합니다(그렇지 않으면 필드가 비공개 이므로 값을 변경할 수 없습니다 ). 이 시점에서 ( checkPermission(new ReflectPermission("suppressAccessChecks")) 호출을 통해) 그러한 작업이 허용되는지 확인하는 보안 관리자로부터 큰 타격을 받을 수 있습니다 . 허용되는 경우(일반 애플리케이션의 기본값) 비공개 필드에 액세스할 수 있습니다. 그들이 말하는 나머지는 기술의 문제입니다. 결과적으로 다음과 같은 결과를 얻습니다.
Initial static final string:  abcde
Reversed static final string: edcba
Q.E.D. 따라서 실제 애플리케이션에서는 보안 정책 설정에 좀 더 신중하게 접근하는 것이 좋습니다. 그렇지 않으면 불변이라고 보장된 객체가 그렇지 않은 것으로 판명될 수도 있습니다. * * * 지금은 문자열에 관해 제가 말하고 싶은 전부인 것 같습니다. 관심을 가져주셔서 감사합니다! 원본 링크: 아, 이 대사는...
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION