Главная » Java » Класс StringBuffer

0

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

public static String guillemete(String quote) {

return ‘"’ + quote + ‘"';

}

Если бы компилятор ограничивался выражениями типа String, он действовал бы следующим образом:

quoted = String.valueOf(‘"’).concat(quote)

.concat(String.valueOf(‘"’));

При каждом вызове valueOf и concat создается новый объект String; следовательно, в результате подобной операции появятся четыре объекта String, из которых в дальнейшем будет использован только один. Что касается остальных, то их создание, присвоение начальных значений и удаление будет сопряжено с непроизводительными расходами.

Разумеется, компилятор действует значительно эффективнее. Для построения промежуточных строк используются объекты класса StringBuffer, а итоговые объекты String создаются лишь в случае необходимости. Объекты StringBuffer могут изменяться, поэтому для хранения промежуточных результатов не нужны новые объекты. При помощи StringBuffer приведенное выше выражение может быть представлено в следующем виде:

quoted = new StringBuffer().append(‘"’).

.append(quote).append(‘"’).toString();

В этом варианте создается всего один объект StringBuffer, который хранит строку, добавляет к ней новые фрагменты и пользуется методом toString для преобразования результатов своей работы в объект String.

Для построения и модификации строки можно воспользоваться  классом StringBuffer.

 содержит следующие конструкторы:

public StringBuffer()

Конструирует новый объект StringBuffer, начальное значение которого равно “”. public StringBuffer(String str)

Конструирует новый объект StringBuffer, исходное значение которого совпадает с str.

во многих отношениях напоминает класс String, а многие из содержащихся в нем методов имеют те же имена и контракты, что и методы String. Тем не менее StringBuffer не является расширением String, и наоборот. Оба этих класса являются независимыми расширениями класса Object.

8.8.1. Модификация буфера

Существует несколько возможностей изменить содержимое буфера объекта StringBuffer, в том числе добавить новые символы в его конец или вставить их в середину. Самый простой из таких методов называется setCharAt и служить для замены символа в конкретной позиции. Также имеется метод replace, который делает то же самое, что и

String.replace, однако работает с объектом StringBuffer. Метод replace не нуждается в создании нового объекта для хранения результата, поэтому несколько последовательных вызовов replace могут выполняться с одним буфером:

public static void

replace(StringBuffer str, char from, char to)

{

for (int i = 0; i << str.length(); i++)

if (str.charAt(i) == from)

str.setCharAt(i, to);

}

Метод setLength обрезает или расширяет строку, хранящуюся в буфере. Если передать setLength величину, меньшую длины текущей строки, то строка будет обрезана до указанного значения. Если же передаваемая длина превышает текущую, то строка расширяется, а новые позиции заполняются нуль-символами  (\u0000).

Кроме того, имеются методы append и insert, которые преобразуют данные любого типа в String, после чего присоединяют результат преобразования к концу строки либо вставляют его в середину. При выполнении метода insert существующие символы сдвигаются, чтобы освободить место для вставляемых новых символов. Методы append и insert преобразуют данные следующих типов:

Object

String

char[]

boolean

char int

int

long

float

double

Также существуют методы append и insert, которым в качестве аргумента передается часть массива char. Например, для создания объекта String Buffer с описанием квадратного корня из целого числа можно написать следующее:

String sqrtInt(int i) {

StringBuffer buf = new StringBuffer();

buf.append("sqrt(").append(i).append(‘)’); buf.append(" = ").append(Math.sqrt(i)); return buf.toString();

}

Методы append и insert возвращают исходный объект String, что в данном случае позволяет нам добавить новые символы к результату предыдущего действия.

Метод insert получает два параметра. Первый из них — позиция, начиная с которой в StringBuffer будут вставляться новые символы. Второй параметр — вставляемое значение, при необходимости преобразованное  в String. Приведем метод, который вставляет добавляет дату в начало буфера:

public static StringBuffer addDate(StringBuffer buf) { String now = new java.util.Date().toString(); buf.ensureCapacity(buf.length() + now.length() + 2); buf.insert(0, now).insert(now.length(), ": ");

return buf;

}

Все начинается с создания строки, содержащей текущую дату. Для этой цели используется класс java.util.Date; его конструктор по умолчанию создает объект, представляющий  текущее время на момент создания. Далее необходимо убедиться, что размеров буфера хватит для хранения всех добавляемых символов — увеличение буфера должно происходить только один раз, а не после каждого вызова insert. Затем в буфер вставляется строка с текущим временем, за которой следует простейшая строка- разделитель. Наконец, переданный буфер возвращается обратно, чтобы при вызове данного метода можно было осуществить что-нибудь наподобие той конкатенации, которая пригодилась при работе с собственными методами StringBuffer.

Метод reverse изменяет порядок следования символов в StringBuffer. Например, если буфер содержит строку “good”, то после выполнения reverse в нем окажется строка “doog”.

8.8.2. Извлечение данных

Чтобы создать объект String на основе объекта StringBuffer, следует вызвать метод

toString.

В StringBuffer не существует методов, которые бы удаляли часть содержимого буфера — вам придется преобразовать буфер в символьный массив, удалить то, что нужно, и построить новый буфер по массиву с оставшимися символами. Вероятно, для этого следует воспользоваться  методом getChars, который аналогичен методу String.getChars.

public void getChars(int srcBegin, int srcEnd, char[] dst,  int       dstBegin)

Копирует символы из заданной части буфера (определяемой позициями srcBegin и srcEnd) в массив dst начиная с dst[dstBegin]. Копирование ведется с позиции srcBegin до srcEnd (но не включает ее!). Позиция srcBegin должна представлять собой допустимый индекс

для данного буфера, а позиция srcEnd не может превышать длины текущей строки в

буфере (которая на единицу больше, чем последний индекс). Если какой-либо из индексов окажется недопустимым, возбуждается исключение Inde x OutOfBoundsException.

Приведем метод, в котором getChars используется для удаления части содержимого буфера:

public static StringBuffer

remove(StringBuffer buf, int pos, int cnt)

{

if (pos << 0 || cnt << 0 || pos + cnt >> buf.length())

throw new IndexOutOfBoundsException();

int leftover = buf.length() – (pos + cnt);

if (leftover == 0) {    // простое обрезание строки

buf.setlength(pos);

return buf;

}

char[] chrs = new char[leftover]; buf.getChars(pos + cnt, buf.Length(), chrs, 0); buf.setLength(pos);

buf.append(chrs);

return buf;

}

Прежде всего необходимо убедиться в том, что значения индексов массива не выходят за пределы допустимых. Обработать исключение можно и позднее, однако немедленная проверка позволяет лучше контролировать ситуацию. Затем мы вычисляем, сколько элементов находится после удаляемой части массива; если таких элементов нет, обрезаем буфер и возвращаемся. В противном случае, перед тем как возвращаться из метода, необходимо выделить эти символы методом getChars, затем обрезать буфер и приписать к нему выделенный ранее остаток.

8.8.3. Работа с емкостью буфера

Буфер объекта StringBuffer обладает определенной емкостью — так называется максимальная длина строки, которая может поместиться в нем перед тем, как придется

выделять дополнительное место. Буфер может увеличиваться автоматически по мере добавления новых символов, однако для повышения эффективности желательно установить его размер один раз.

Исходный размер буфера объекта StringBuffer может быть задан с помощью конструктора,

получающего всего один параметр типа int:

public StringBuffer(int capacity)

Конструирует новый объект StringBuffer с заданной исходной емкостью и начальным значением “”.

public synchronized void ensureCapacity(int minimum)

Позволяет убедиться в том, что буфер имеет емкость не менее заданного minimum. public int capacity()

Возвращает текущую емкость буфера.

С помощью этих методов можно избежать многократного увеличения буфера. Ниже приводится новая версия метода sqrtInt, которая обеспечивает не более чем однократное выделение нового пространства для буфера:

String sqrtIntFaster(int i) {

StringBuffer buf = new StringBuffer(50); buf.append("sqrt(").append(i).append(‘)’); buf.append(" = ").append(Math.sqrt(i)); return buf.toString();

}

Единственное изменение заключается в том, что на этот раз используется конструктор, который создает достаточно большой объект StringBuffer, способный вместить строку с результатом. Значение 50 несколько превышает максимально необходимое; следовательно, буфер никогда не придется увеличивать заново.

Упражнение 8.4

Напишите метод для преобразования строк с десятичными числами, при котором после каждой третьей цифры справа ставится запятая. Например, для исходной строки “1542729" метод должен возвращать строку ”1,542,729".

Упражнение 8.5

Измените метод из предыдущего упражнения так, чтобы при его вызове можно было указать символ-разделитель и количество цифр между разделителями.

Глава 9

ПОТОКИ

Как можно находиться в двух местах одновременно, если на самом деле вообще нигде не находишься? Firesign Theater

Большинство программистов привыкло писать программы, которые выполняются шаг за шагом, в определенной последовательности. На приведенной ниже иллюстрации показано, как извлекается банковский баланс, сумма на счету увеличивается и заносится обратно в запись о состоянии счета:

Аналогичные действия выполняются как живыми банковскими работниками, так и компьютерными программами. Подобная последовательность  действий, выполняемых по одному, называется потоком (th read) . В большинстве языков программистам приходится иметь дело с однопоточной моделью программирования.

Однако в настоящих банках подобные операции происходят одновременно. Несколько работников независимо друг от друга могут обновлять состояние банковских счетов:

Аналог подобной ситуации в компьютере называется многопоточностью  (multithreading). Поток (как и банковский работник) может работать независимо от других потоков. И подобно тому, как двое банковских служащих могут пользоваться одними и теми же картотеками, потоки также осуществляют совместный доступ к объектам.

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

Почти одновременно второй клиент приказывает другому работнику банка положить деньги на тот же самый счет. Оба работника идут в архив, чтобы найти информацию о счете (были же времена, когда в банках использовались бумажные картотеки!) и получают одинаковые данные. Затем они возвращаются к своим столам, заносят требуемую сумму на счет и идут обратно в архив, чтобы записать свои результаты, полученные независимо друг от друга. В таком случае на состоянии счета отразится лишь последняя из записанных транзакций; первая транзакция будет попросту потеряна.

В настоящих банках проблема решалась просто: работник оставлял в папке записку

“Занято; подождите завершения работы”. В компьютере происходит практически то же

самое: с объектом связывается понятие блокировка (lock), по которой можно определить,

используется объект или нет.

Многие реальные задачи программирования  лучше всего решаются с применением нескольких потоков. Например, интерактивные программы, предназначенные  для графического отображения данных, нередко разрешают пользователю изменять параметры отображения в реальном времени. Оптимальное динамическое поведение интерактивных программ достигается благодаря использованию потоков. В однопоточных системах иллюзия работы с несколькими потоками обычно достигается за счет использования прерываний или программных запросов (polling). Программные запросы служат для объединения частей приложения, управляющих отображением информации и вводом данных. Особенно тщательно должна быть написана программа отображения — запросы от нее должны поступать достаточно часто, чтобы реагировать на ввод информации пользователем в течение долей секунды. Эта программа либо должна позаботиться о том, чтобы операции графического вывода занимали минимальное время, либо прерывать свою собственную работу для выполнения запросов. Такое смешение двух разнородных аспектов программы приводит к появлению сложного, а порой и нежизнеспособного  кода.

С указанными проблемами проще всего справиться в многопоточной системе. Один поток обновляет изображение на основе текущих данных, а другой — обрабатывает ввод со стороны пользователя. Если ввод оказывается сложным (например, пользователь заполняет экранную форму), первый поток (вывод данных) может работать независимо, вплоть до получения новой информации. В модели с применением программных запросов приходится либо приостанавливать  обновление изображения, чтобы дождаться завершения нетривиального ввода, либо производить сложную синхронизацию, чтобы изображение могло обновляться во время заполнения формы пользователем. Модель с разделением процессов ввода и отображения может поддерживаться в многопоточной системе непосредственно, вместо того чтобы заново подгонять ее для реализации очередной задачи.

Источник: Арнольд К., Гослинг Д. – Язык программирования Java (1997)

По теме:

  • Комментарии