JavaRush /Blog Java /Random-VI /À, những dòng này...
articles
Mức độ

À, những dòng này...

Xuất bản trong nhóm
Lớp java.lang.String có lẽ là một trong những lớp được sử dụng nhiều nhất trong Java. Và rất thường xuyên nó được sử dụng một cách mù chữ, điều này gây ra nhiều vấn đề, chủ yếu là về hiệu suất. Trong bài viết này tôi muốn nói về các chuỗi, sự phức tạp của việc sử dụng chúng, nguồn gốc của các vấn đề, v.v.
À, những dòng này... - 1
Đây là những gì chúng ta sẽ nói về:
  • Thiết bị đường dây
  • Chuỗi ký tự
  • So sánh chuỗi
  • Phép cộng chuỗi
  • Hàm tạo tìm nạp và sao chép chuỗi con
  • Thay đổi một dòng
  • Hãy bắt đầu với những điều cơ bản.

Thiết bị đường dây

Lớp java.lang.String chứa ba trường:
/**
 * NOTE: This is just a partial API
 */
public final class String{

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

}
Trên thực tế, nó cũng chứa các trường khác, chẳng hạn như mã băm, nhưng hiện tại điều này không quan trọng. Những cái chính là những cái này. Vì vậy, cơ sở của một chuỗi là một mảng các ký tự ( char ). Mã hóa Unicode UTF-16BE được sử dụng khi lưu trữ các ký tự trong bộ nhớ . Bạn có thể đọc thêm về nó ở đây . Bắt đầu với Java 5.0, hỗ trợ cho các phiên bản Unicode trên 2 và theo đó, các ký tự có mã lớn hơn 0xFFFF đã được giới thiệu . Đối với những ký tự này, không phải một ký tự được sử dụng mà là hai ký tự; thông tin chi tiết hơn về cách mã hóa các ký tự này có trong cùng một bài viết . Mặc dù hỗ trợ cho những biểu tượng này đã được giới thiệu nhưng vấn đề là chúng sẽ không được hiển thị. Tôi đã tìm thấy một bộ ký hiệu âm nhạc ( U1D100 ) và cố gắng hiển thị khóa treble ở đâu đó (ký hiệu có mã 1D120). Tôi đã chuyển đổi mã thành hai ký tự , như mong đợi - '\uD834' và '\uDD20'. Bộ giải mã không phàn nàn về chúng; nó thành thật nhận ra chúng là một ký tự. Nhưng không có phông chữ nào tồn tại biểu tượng này. Và do đó - một hình vuông. Và rõ ràng, điều này sẽ kéo dài trong một thời gian dài. Vì vậy, việc giới thiệu hỗ trợ Unicode 4 chỉ có thể được xem qua lăng kính của nền tảng cho tương lai. Hãy đi xa hơn nữa. Tôi yêu cầu bạn chú ý đến trường thứ hai và thứ ba – offsetcount . Có vẻ như mảng xác định hoàn toàn chuỗi nếu TẤT CẢ các ký tự được sử dụng. Nếu các trường như vậy tồn tại thì không phải tất cả các ký tự trong mảng đều có thể được sử dụng. Đúng như vậy, chúng ta sẽ nói về điều này trong phần lựa chọn chuỗi con và sao chép hàm tạo.

Chuỗi ký tự

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 финальный аккорд –

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

Điều này ít liên quan đến dòng như vậy. Tôi chỉ muốn nhấn mạnh một thực tế là một chuỗi chỉ bất biến ở một mức độ nhất định. Vì vậy, đây là mã.
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
Những gì đang xảy ra ở đây? Đầu tiên tôi đang tìm trường loại char[] . Tôi cũng có thể tìm kiếm theo tên. Tuy nhiên, tên có thể thay đổi, nhưng tôi rất nghi ngờ về loại hình này. Tiếp theo, tôi gọi phương thức setAccessible(true) trên trường tìm thấy . Đây là điểm quan trọng - Tôi vô hiệu hóa việc kiểm tra cấp độ truy cập cho trường này (nếu không, đơn giản là tôi sẽ không thể thay đổi giá trị vì trường này là riêng tư ). Tại thời điểm này, tôi có thể bị người quản lý bảo mật đánh vào đầu, người kiểm tra xem hành động đó có được phép hay không (thông qua lệnh gọi tới checkPermission(new ReflectPermission("suppressAccessChecks")) ). Nếu được phép (và đây là mặc định cho các ứng dụng thông thường), tôi có thể truy cập vào trường riêng tư . Phần còn lại, như họ nói, là vấn đề kỹ thuật. Kết quả là tôi nhận được kết quả:
Initial static final string:  abcde
Reversed static final string: edcba
Q.E.D. Do đó, trong các ứng dụng thực tế, tôi khuyên bạn nên thực hiện cách tiếp cận cẩn thận hơn trong việc thiết lập các chính sách bảo mật. Nếu không, có thể những đối tượng mà bạn cho là được đảm bảo là bất biến lại không phải vậy. * * * Tôi đoán bây giờ đó là tất cả những gì tôi muốn nói về chuỗi. Cám ơn vì sự quan tâm của bạn! Liên kết đến nguồn gốc: À, những dòng này...
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION