Тип String является нашим наиболее часто используемым типом данных, без исключения. Таким образом, повышение операционной эффективности String, несомненно, является лучшим способом повышения производительности программы.
В этой статье мы начнем с исходного кода String и шаг за шагом проведем вас к достижению небольшой цели оптимизации строк.Не только научит вас эффективно использовать струны, но и раскроет глубинные причины, стоящие за этим..
Очки знаний, задействованные в этой статье, показаны на следующем рисунке:
Прежде чем рассматривать, как оптимизировать String, давайте сначала разберемся с характеристиками String.
Строковые свойства
Если вы хотите понять характеристики String, вы должны начать с его исходного кода, а именно:
// 源码基于 JDK 1.8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// String 值的实际存储容器
private final char value[];
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// 忽略其他信息
}
Из его исходного кода мы видим, что класс String и егоvalue[]
свойстваfinal
модифицированный, которыйvalue[]
это окончательная структура, которая реализует хранилище строк, иfinal
Он сказал «финальный и последний».
Мы знаем, что поfinal
Модифицированные классы не могут быть унаследованы, что означает, что этот класс не может иметь подклассов, ноfinal
Модифицируемые переменные — это константы, значения которых нельзя изменить.Это означает, что после создания строки ее нельзя изменить..
Почему строка не может быть изменена?
Строковые классы и свойстваvalue[]
определяются какfinal
Что ж, в этом есть три преимущества:
- Безопасность: при вызове других методов, например перед вызовом некоторых операционных инструкций системного уровня, может выполняться ряд проверок. Если это изменяемый класс, его внутреннее значение может быть изменено после проверки. , что может привести к серьезным последствиям. происходит сбой системы, поэтому важной причиной принудительного проектирования String в качестве конечного класса являются соображения безопасности;
- Высокая производительность: уникальность хеш-значения гарантируется после того, как String станет неизменяемым, поэтому он более эффективен и больше подходит для кеша ключ-значение HashMap;
- Экономия памяти: неизменность String является основой для реализации пула строковых констант. Пул строковых констант относится к «пулу констант» при создании строки, сначала нужно проверить, существует ли такая «строка», если да , это не откроет новое пространство для создания строки, а напрямую вернет ссылку в пуле констант на этот объект, что может сэкономить больше места. Например, обычно существует два способа создания строки: способ прямого присваивания, например, String str="Java"; второй — создание новой формы, например, String str = new String("Java"). . Когда первый метод используется для создания строкового объекта в коде, JVM сначала проверяет, находится ли объект в пуле строковых констант, и если да, то возвращает ссылку на объект, иначе в пуле констант будет создана новая строка. Таким образом, вы можетеУменьшите повторное создание строковых объектов с одним и тем же значением и сэкономьте память. String str = new String("Java") Таким образом, сначала при компиляции файла класса константная строка "Java" будет помещена в константную структуру, а при загрузке класса "Java" будет в создать пул констант; во-вторых, при вызове new команда JVM вызовет конструктор String, сошлется на строку «Java» в пуле констант, создаст объект String в памяти кучи, и, наконец, str будет ссылаться на объект String.
1. Не вводите строку += напрямую
Из приведенного выше содержимого мы знаем, что класс String неизменяем, поэтому при использовании String мы не можем часто использовать += strings.
код перед оптимизацией:
public static String doAdd() {
String result = "";
for (int i = 0; i < 10000; i++) {
result += (" i:" + i);
}
return result;
}
Некоторые люди могут спросить, мои бизнес-требования таковы, как я могу их достичь?
Официальный предоставляет нам две схемы конкатенации строк:StringBuffer
иStringBuilder
,вStringBuilder
не является потокобезопасным, в то время какStringBuffer
является потокобезопасным,StringBuffer
Метод правописания использует ключевые словаsynchronized
Для обеспечения безопасности потоков исходный код выглядит следующим образом:
@Override
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
Также из-за использованияsynchronized
модифицировал, поэтомуStringBuffer
Суммарная производительность будет лучше, чемStringBuilder
Низкий.
Затем мы используемStringBuilder
Чтобы реализовать конкатенацию строк,оптимизированный код:
public static String doAppend() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(" i:" + i);
}
return sb.toString();
}
Давайте проверим разницу в производительности между двумя методами с помощью кода:
public class StringTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
// String
long st1 = System.currentTimeMillis(); // 开始时间
doAdd();
long et1 = System.currentTimeMillis(); // 开始时间
System.out.println("String 拼加,执行时间:" + (et1 - st1));
// StringBuilder
long st2 = System.currentTimeMillis(); // 开始时间
doAppend();
long et2 = System.currentTimeMillis(); // 开始时间
System.out.println("StringBuilder 拼加,执行时间:" + (et2 - st2));
System.out.println();
}
}
public static String doAdd() {
String result = "";
for (int i = 0; i < 10000; i++) {
result += ("Java中文社群:" + i);
}
return result;
}
public static String doAppend() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("Java中文社群:" + i);
}
return sb.toString();
}
}
Результат выполнения вышеуказанной программы выглядит следующим образом:
Конкатенация строк, время выполнения: 429
Правописание StringBuilder, время выполнения: 1
Конкатенация строк, время выполнения: 553
Правописание StringBuilder, время выполнения: 0
Конкатенация строк, время выполнения: 289
Правописание StringBuilder, время выполнения: 1
Конкатенация строк, время выполнения: 210
Правописание StringBuilder, время выполнения: 2
Конкатенация строк, время выполнения: 224
Правописание StringBuilder, время выполнения: 1
Из результатов видно, что производительность до и после оптимизации сильно отличается.
Примечание. Результаты этого теста производительности связаны с количеством циклов, то есть чем больше циклов, тем больше результат их деления по производительности.
Далее нам нужно подумать над вопросом:Почему метод StringBuilder.append() более эффективен, чем +=? И чем больше раз сварки, тем больше разрыв в производительности?
Когда мы открываем исходный код StringBuilder, мы можем найти «маленький секрет».Исходный код реализации родительского класса StringBuilder AbstractStringBuilder выглядит следующим образом:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
@Override
public AbstractStringBuilder append(CharSequence s, int start, int end) {
if (s == null)
s = "null";
if ((start < 0) || (start > end) || (end > s.length()))
throw new IndexOutOfBoundsException(
"start " + start + ", end " + end + ", s.length() "
+ s.length());
int len = end - start;
ensureCapacityInternal(count + len);
for (int i = start, j = count; i < end; i++, j++)
value[j] = s.charAt(i);
count += len;
return this;
}
// 忽略其他信息...
}
И StringBuilder использует предоставленный родительским классомchar[]
Как фактическая единица хранения собственного значения, она будет изменяться при каждом добавлении.char[]
Массив, StringBuildertoString()
Исходный код выглядит следующим образом:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
На основе приведенного выше исходного кода видно, что:StringBuilder используетchar[]
Как фактическую единицу хранения, ее нужно только модифицировать каждый раз, когда она добавляется.char[]
Массив может быть, просто вtoString()
Строка создается при создании строки; после создания строки ее нельзя изменить, поэтому новую строку необходимо воссоздавать каждый раз при ее добавлении, поэтому StringBuilder.append() Производительность будет лучше, чем += строки намного выше производительность.
2. Эффективно используйте стажерский метод
Правильное использование метода String.intern() может эффективно сэкономить память и повысить эффективность работы строк.Давайте сначала посмотрим.intern()
Определение и исходный код метода:
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
Как можно видеть intern()
является эффективным нативным методом, в его определении сказано, что при вызовеintern
метод, если пул строковых констант уже содержит эту строку, он будет напрямую возвращать ссылку на эту строку, если он не содержит эту строку, сначала добавить строку в пул констант, а затем вернуть ссылку на этот объект.
В каких случаях целесообразно использоватьintern()
метод?
Инженер Twitter однажды поделилсяString.intern()
Пример использования Твиттера: каждый раз, когда Твиттер публикует статус сообщения, он генерирует информацию об адресе.Исходя из предполагаемого масштаба пользователей Твиттера в то время, серверу требуется 32 ГБ памяти для хранения информации об адресе.
public class Location {
private String city;
private String region;
private String countryCode;
private double longitude;
private double latitude;
}
Учитывая, что многие пользователи имеют перекрывающуюся адресную информацию, такую как страны, провинции, города и т. д., эту часть информации можно выделить в отдельный класс, чтобы уменьшить дублирование.Код выглядит следующим образом:
public class SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
Благодаря оптимизации размер хранилища данных уменьшен примерно до 20 ГБ. Но памяти для хранения этих данных все равно очень много, что делать?
Инженеры Twitter используютString.intern()
Уменьшен размер хранилища очень повторяющейся адресной информации с 20 ГБ до сотен мегабайт, что оптимизирует хранение объектов String.
Реализованный основной код выглядит следующим образом:
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Начиная с JDK 1.7, пул констант был объединен с кучей, поэтому копия строки не будет скопирована, но в пул констант будет добавлена ссылка на первую встреченную строку. В это время будет оцениваться только то, существует ли уже строка в пуле констант, и если да, то будет возвращена ссылка на строку в пуле констант.
Это эквивалентно следующему коду:
String s1 = new String("Java中文社群").intern();
String s2 = new String("Java中文社群").intern();
System.out.println(s1 == s2);
Результат выполнения: true
Если кто-то спросит, почему бы не присвоить значение напрямую (используя String s1 = "Java Chinese Community"), это потому, что этот код создан для упрощения семантики приведенного выше бизнес-кода Twitter, и он использует объектный метод, а не прямой метод присвоения. больше оintern()
Контент можно посмотреть«Не спрашивайте меня, сколько объектов создает новая строка! Я докажу это вам! 》Эта статья.
3. Используйте метод разделения осторожно
Почему я советую вам соблюдать осторожностьSplit
метод, потому чтоSplit
В большинстве случаев метод использует регулярные выражения.С этим методом сегментации проблем нет, но поскольку производительность регулярных выражений очень нестабильна, неправильное использование вызовет проблемы с возвратом, что может привести к высокой загрузке ЦП.
Например, следующее регулярное выражение:
String badRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\\\/])+$";
String bugUrl = "http://www.apigo.com/dddp-web/pdf/download?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";
if (bugUrl.matches(badRegex)) {
System.out.println("match!!");
} else {
System.out.println("no match!!");
}
Эффект выполнения показан на следующем рисунке:
Реализация механизма, используемого регулярными выражениями Java, представляет собой автоматы NFA (недетерминированный конечный автомат, неопределенный конечный автомат).Этот механизм регулярных выражений будет выполнять откат во время сопоставления символов, и как только возврат произойдет, тогда время, которое он потребляет, станет очень большим, он может быть несколько минут, это может быть несколько часов, продолжительность времени зависит от количества и сложности возврата.
Чтобы лучше объяснить, что такое возврат, давайте воспользуемся следующим примером:
text = "abbc";
regex = "ab{1,3}c";
Цель приведенного выше примера относительно проста: сопоставить строки, начинающиеся с a, заканчивающиеся на c и содержащие 1–3 символа b между ними.
Процесс его разбора движком NFA выглядит так:
- Сначала прочитайте первое совпадение регулярного выражения
a
и первый символ строкиa
Сравните, сопоставьте, так что читайте второй символ регулярного выражения; - Прочитать второе совпадение регулярного выражения
b{1,3}
По сравнению со вторым символом b строки он совпадает. Но потому чтоb{1,3}
Показывает 1-3b
String и жадный характер автоматов NFA (то есть, чтобы соответствовать как можно большему количеству), поэтому в это время он не будет читать следующий сопоставитель регулярных выражений, но по-прежнему будет использоватьb{1,3}
и третий символ строкиb
Сравните и обнаружите, что он все еще совпадает, поэтому продолжайте использоватьb{1,3}
и четвертый символ строкиc
Сравните и обнаружите, что совпадений нет, после чего произойдет возврат; - После возврата мы прочитали четвертый символ строки
c
Будет выплевывается, указатель возвращается на позицию третьей строки, затем программа считывает следующий оператор регулярного выражения.c
, а затем прочитать следующий символ текущего указателяc
Сделайте сравнение и обнаружите, что есть совпадение, поэтому прочитайте следующий оператор, а затем обнаружите, что он закончился.
Это процесс обычного выполнения сопоставления и простого процесса выполнения с возвратом, и приведенный выше пример соответствует "com/dzfp-web/pdf/download?request=6e7JGm38jf..." из-за жадного сопоставления, поэтому программа всегда будет прочитайте следующую строку, чтобы найти соответствие, и, наконец, обнаружите, что точки нет, поэтому она будет отслеживаться одна за другой, что приведет к слишком высокой нагрузке ЦП.
Таким образом, мы должны использовать метод Split() с осторожностью, мы можем использовать метод String.indexOf() вместо метода Split(), чтобы завершить разделение строки. Если вы действительно не можете удовлетворить свои потребности, вы можете обратить внимание на проблему поиска с возвратом при использовании метода Split().
Суммировать
Благодаря анализу исходного кода String в этой статье были обнаружены неизменяемые характеристики String и три основных преимущества неизменяемых характеристик, а затем говорилось о трех методах оптимизации строк: не += строки напрямую, эффективно используйте метод intern(). метод и будьте осторожны Используйте метод Split(). И благодаря анализу исходного кода StringBuilder я узнал основные причины высокой производительности append(), а также проблему возврата, вызванную нестабильностью регулярных выражений, и вошел в анализ случаев высокой загрузки ЦП. Надеюсь, это могу помочь тебе.
последние слова
Оригинальность непроста, если вы считаете, что эта статья вам полезна,Пожалуйста, не стесняйтесь нажать «Нравится», это самая большая поддержка и поощрение для автора, спасибо.
Ссылки и благодарности
Для получения более интересного контента, пожалуйста, обратите внимание на публичный аккаунт WeChat «Java Chinese Community».