JavaRush /Java-Blog /Random-DE /Ah, diese Zeilen...
articles
Level 15

Ah, diese Zeilen...

Veröffentlicht in der Gruppe Random-DE
Die Klasse java.lang.String ist vielleicht eine der am häufigsten verwendeten in Java. Und sehr oft wird es analphabetisch verwendet, was zu vielen Problemen führt, vor allem mit der Leistung. In diesem Artikel möchte ich über Strings, die Feinheiten ihrer Verwendung, die Ursachen von Problemen usw. sprechen.
Ah, diese Zeilen... - 1
Hier erfahren Sie, worüber wir sprechen werden:
  • Line-Gerät
  • String-Literale
  • String-Vergleich
  • String-Addition
  • Konstruktor zum Abrufen und Kopieren von Teilzeichenfolgen
  • Eine Zeile ändern
  • Beginnen wir mit den Grundlagen.

Line-Gerät

Die Klasse java.lang.String enthält drei Felder:
/**
 * NOTE: This is just a partial API
 */
public final class String{

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

}
Tatsächlich enthält es auch andere Felder, zum Beispiel einen Hash-Code, aber das ist jetzt nicht wichtig. Die wichtigsten sind diese. Die Basis einer Zeichenfolge ist also ein Array von Zeichen ( char ). Beim Speichern von Zeichen im Speicher wird die Unicode -UTF-16BE- Codierung verwendet . Mehr darüber können Sie hier lesen . Ab Java 5.0 wurde die Unterstützung für Unicode-Versionen über 2 und dementsprechend für Zeichen mit Codes größer als 0xFFFF eingeführt . Für diese Zeichen wird nicht ein Zeichen verwendet , sondern zwei; weitere Einzelheiten zur Kodierung dieser Zeichen finden Sie im selben Artikel . Obwohl die Unterstützung für diese Symbole eingeführt wurde, besteht das Problem darin, dass sie nicht angezeigt werden. Ich habe einen Satz Musiksymbole ( U1D100 ) gefunden und versucht, den Violinschlüssel irgendwo anzuzeigen (Symbol mit Code 1D120). Ich habe den Code wie erwartet in zwei Zeichen umgewandelt – „\uD834“ und „\uDD20“. Der Decoder beschwert sich nicht über sie; er erkennt sie ehrlich als ein Zeichen. Es gibt jedoch keine Schriftart, in der dieses Symbol existiert. Und deshalb - ein Quadrat. Und das wird offenbar noch lange so bleiben. Daher kann die Einführung der Unterstützung für Unicode 4 nur unter dem Gesichtspunkt einer Grundlage für die Zukunft betrachtet werden. Gehen wir weiter. Ich bitte Sie, dem zweiten und dritten Feld – Offset und Count – besondere Aufmerksamkeit zu schenken . Es scheint, dass das Array die Zeichenfolge vollständig definiert, wenn ALLE Zeichen verwendet werden. Wenn solche Felder vorhanden sind, können nicht alle Zeichen im Array verwendet werden. Wir werden darüber im Abschnitt zur Teilstringauswahl und zum Kopierkonstruktor sprechen.

String-Literale

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

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

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

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

Строки – единственный ein Objekt, для которого определена операция сложения ссылок. Во всяком случае, так было до версии Java 5.0, в которой появился autoboxing/unboxing, но речь сейчас не об этом. Общее описание принципа работы оператора конкатенации можно найти в статье о Verknüpfungх, а именно – тут. Я же хочу затронуть более глубокий уровень. Представьте себе, представьте себе... Прямо Wie в песенке про кузнечика. :) Так вот, представьте себе, что нам надо сложить две строки, вернее, к одной прибавить другую:
String str1 = "abc";
str1 += "def";
Как происходит сложение? Поскольку ein Objekt класса строки неизменяем, то результатом сложения будет новый ein Objekt. Итак. Сначала выделяется память, достаточная для того, чтобы вместить туда содержимое обеих строк. В эту память копируется содержимое сначала первой строки, потом второй. Далее переменной str1 присваивается Verknüpfung на новую строку, а старая строка отбрасывается. Усложним задачу. Пусть у нас есть файл из четырех строк:
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 присваивается Verknüpfung на новую строку, старая отбрасывается. Второй проход цикла. result="abc", line="def". Выделяется память на 6 символов, туда копируется содержимое result"abc", затем line"def". Переменной result присваивается Verknüpfung на новую строку, старая отбрасывается. Третий проход цикла. result="abcdef", line="ghi". Выделяется память на 9 символов, туда копируется содержимое result"abcdef", затем line"ghi". Переменной result присваивается Verknüpfung на новую строку, старая отбрасывается. Четвертый проход цикла. result="abcdefghi", line="jkl". Выделяется память на 12 символов, туда копируется содержимое result"abcdefghi", затем line"jkl". Переменной result присваивается Verknüpfung на новую строку, старая отбрасывается. Пятый проход цикла. result="abcdefghijkl", line=null. Цикл закончен. Итак. Три символа "abc" копировались в памяти 4 раза, "def" – 3 раза, "ghi" – 2 раза, "jkl" – один раз. Страшно? Не особо? А вот теперь представьте себе файл с длиной строки 80 символов, в котором где-то 1000 строк. Всего-навсего 80кб. Представoder? Was будет в этом случае? первая строка, Wie нетрудно подсчитать, будет скопирована в памяти 1000 раз, вторая – 999 и т.д. И при средней длине 80 символов через память пройдет ((1000 + 1) * 1000 / 2) * 80 = ... барабанная дробь... 40 040 000 символов, что составляет около 80 Мб (!!!) памяти. Каков же итог ТАКОГО цикла? Чтение 80-килоByteного Datei вызвало выделение 80 Мб памяти. Ни много ни мало – в 1000 раз больше, чем полезный объем. Какой из этого следует сделать вывод? Очень простой. Никогда, запомните – НИКОГДА не используйте прямую конкатенацию строк, особенно в циклах. Даже в Wieом-нибудь методе toString, если он вызывается достаточно часто, имеет смысл использовать StringBuffer anstatt конкатенации. Собственно, компилятор при оптимизации чаще всего так и делает – прямые сложения он выполняет через StringBuffer. Однако в случаях, подобных тому, что привел я, оптимизацию компилятор сделать не в состоянии. Was и приводит к весьма печальным последствиям, описаным чуть ниже. К сожалению, подобные конструкции встречаются слишком часто. Потому я и счел необходитмым заострить на этом внимание. Собственный опыт Не могу не вспомнить один эпизод из собственной практики. Один из программистов, работавших со мной, Wie-то пожаловался, что у него очень медленно работает его Code. Он читал достаточно большой файл в HTML формате, после чего производил Wieие-то манипуляции. И действительно, работало все с черепашьей Geschwindigkeitю. Я взял посмотреть исходник, и обнаружил, что он... использует конкатенацию строк. У него было по 200-250 строк в каждом файле, и при чтении Datei около 200Кб через память проходило более 40Мб! В итоге я переписал немного Code, заменив операции со строками на операции со StringBuffer-ом. Честно сказать, когда я запустил переписаный Code, я подумал, что он просто где-то "упал". Обработка занимала доли секунды. Скорость выросла в 300-800 раз. После этого я коренным образом пересмотрел свое отношение к строковым операциям. Следующий акт марлезонского балета –

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

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

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

Это к строке Wie таковой относится слабо. Я лишь хочу показать тот факт, что строка является неизменяемой только до известной степени. Итак, Code.
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
Was тут происходит? Сначала я ищу поле типа char[]. Я мог бы искать и по имени. Однако Name может измениться, а вот тип – сильно сомневаюсь. Далее, я у найденого поля вызываю метод setAccessible(true). Это ключевой момент – я отключаю проверку уровня доступа к полю (иначе я просто не смогу изменить Bedeutung, ибо поле private). В этом месте я могу получить по голове от менеджера безопасности, который проверяет, разрешено ли такое действие (через вызов checkPermission(new ReflectPermission("suppressAccessChecks"))). Если разрешено (а по умолчанию для обычных приложений так и есть) – я могу получить доступ к private-полю. Остальное, Wie говорится, дело техники. В результате я получаю вывод:
Initial static final string:  abcde
Reversed static final string: edcba
Was и требовалось доказать. А потому – в реальных Anwendungenх я советую более тщательно подходить к настройке политики безопасности. Иначе может оказаться, что ein Objektы, которые вы считаете гарантированно неизменяемыми, таковыми не являются. * * * Наверное, это все, что я хочу рассказать о строках на данный момент. Спасибо за внимание! Ссылка на первоисточник: Ах, эти строки...
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION