Хотя в некоторых специфических случаях локальный дубликат, созданный с помощью метода clone()
, может обеспечивать желаемый результат, программист (автор метода) всё равно должен самостоятельно запрещать побочные эффекты от использования псевдонимов. В случае создания библиотеки, которая должна иметь общее назначение, но при этом не гарантирует корректность клонирования в правильном классе, что делать? Более вероятной ситуацией является то, когда мы хотим использовать псевдонимы положительно — чтобы запретить ненужное копирование объектов, — но при этом не хотим видеть возникающие из этого побочные эффекты.
Один подход заключается в создании "неизменяемых объектов", принадлежащих только читающим классам. Можно определить специальный класс, который не имеет никаких методов, способных изменять внутреннее состояние объекта. В таком классе использование псевдонимов не вызывает проблем, поскольку доступ осуществляется только для чтения. Когда несколько мест кода обращаются к одному и тому же объекту, побочных эффектов не происходит.Примером "неизменяемого объекта" служит Java стандартная библиотека, содержащая "обёртки" для всех примитивных типов данных. Возможно, вы уже заметили это: если требуется хранить значение типа int
в коллекции, такой как Vector
(которая использует ссылки типа Object
), можно обернуть это значение в класс Integer
стандартной библиотеки. Пример:```java
//: ImmutableInteger.java
// Класс Integer не может быть изменён
import java.util.*;
public class ImmutableInteger { public static void main(String[] args) { Vector v = new Vector(); for(int i = 0; i < 10; i++) v.addElement(new Integer(i)); // Но как изменить int внутри Integer? } } ///:~
Класс `Integer` (и все остальные "обёртки") реализуют простую форму "неизменяемости": они не предоставляют методов, позволяющих изменять объект.
Если действительно требуется объект, содержащий примитивный тип данных, и требуется возможность изменения этих данных, придётся создать его самостоятельно. К счастью, это довольно просто:
```java
//: MutableInteger.java
// Изменяемый класс обёртки
import java.util.*;
class IntValue {
int n;
IntValue(int x) { n = x; }
public String toString() {
return Integer.toString(n);
}
}
public class MutableInteger {
public static void main(String[] args) {
Vector v = new Vector();
for(int i = 0; i < 10; i++)
v.addElement(new IntValue(i));
System.out.println(v);
for(int i = 0; i < v.size(); i++)
((IntValue)v.elementAt(i)).n++;
System.out.println(v);
}
} ///:~
Обратите внимание, что n
здесь упрощает наш код.
Если по умолчанию инициализация нулем уже достаточно (так что нет необходимости в конструкторе), а также если не требуется вывод объекта (так что нет необходимости в методе toString
), то класс IntValue
может быть ещё более простым. Пример:
class IntValue { int n; }
Извлечение элементов и их преобразование выглядят несколько громоздко, но это проблема класса Vector
, а не класса IntValue
.
Можно создать свои собственные неизменяемые классы, вот пример:
```java
//: Immutable1.java
// Объекты, которые нельзя модифицировать,
// защищены от проблемы псевдонимизации.
public class Immutable1 {
private int data;
public Immutable1(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable1 quadruple() {
return new Immutable1(data * 4);
}
static void f(Immutable1 i1) {
Immutable1 quad = i1.quadruple();
System.out.println("i1 = " + i1.read());
System.out.println("quad = " + quad.read());
}
public static void main(String[] args) {
Immutable1 x = new Immutable1(47);
System.out.println("x = " + x.read());
f(x);
System.out.println("x = " + x.read());
}
} ///:~
Все данные объявлены как private
, и нет никаких публичных методов, которые бы изменяли эти данные. Действительно, единственный метод, который создаёт новый объект — это quadruple()
. Но этот метод просто создаёт новый объект Immutable1
, при этом исходный объект остаётся неизменным.
Метод f()
получает объект Immutable1
и выполняет различные действия над ним, а вывод программы показывает, что объект x
не был изменён. Таким образом, объект x
можно использовать многократно с псевдонимами, не опасаясь изменения его состояния, так как класс Immutable1
гарантирует, что объект не будет изменён.
String
) стоимость такого подхода слишком велика.Чтобы решить эту проблему, можно создать «союзника» класса, который позволяет выполнять изменения. При выполнении большого количества изменений можно временно переключиться на изменяемый союзник, а затем вернуться к неизменяемому классу. Поэтому пример можно изменить следующим образом://: Immutable2.java
// Вспомогательный класс для изменения
// неизменяемых объектов.
class Mutable {
private int data;
public Mutable(int initVal) {
data = initVal;
}
public Mutable add(int x) {
data += x;
return this;
}
public Mutable multiply(int x) {
data *= x;
return this;
}
public Immutable2 makeImmutable2() {
return new Immutable2(data);
}
}
public class Immutable2 {
private int data;
public Immutable2(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable2 add(int x) {
return new Immutable2(data + x);
}
public Immutable2 multiply(int x) {
return new Immutable2(data * x);
}
public Mutable makeMutable() {
return new Mutable(data);
}
public static Immutable2 modify1(Immutable2 y){
Immutable2 val = y.add(12);
val = val.multiply(3);
val = val.add(11);
val = val.multiply(2);
return val;
}
// Это даёт такой же результат:
public static Immutable2 modify2(Immutable2 y){
Mutable m = y.makeMutable();
m.add(12).multiply(3).add(11).multiply(2);
return m.makeImmutable2();
}
public static void main(String[] args) {
Immutable2 i2 = new Immutable2(47);
Immutable2 r1 = modify1(i2);
Immutable2 r2 = modify2(i2);
System.out.println("i2 = " + i2.read());
System.out.println("r1 = " + r1.read());
System.out.println("r2 = " + r2.read());
}
} ///:~
```Как обычно, методы в `Immutable2` сохраняют неизменяемость объекта, создавая новые объекты при каждом изменении. Эти операции выполняются с помощью методов `add()` и `multiply()`. Вспомогательный класс называется `Mutable`, он также имеет методы `add()` и `multiply()`, но они могут изменять объект типа `Mutable`, а не создавать новый. Кроме того, класс `Mutable` имеет метод, который создаёт объект типа `Immutable2` на основе данных этого объекта, и наоборот.Два статических метода `modify1()` и `modify2()` демонстрируют два разных подхода к достижению одного и того же результата. В методе `modify1()` все действия выполняются внутри класса `Immutable2`, и мы видим, что создаётся четыре новых объекта `Immutable2` (при каждом перезаписывается значение `val`, а старый объект становится мусором). В методе `modify2()` можно заметить, что его первым действием является получение объекта `Immutable2 y`, после которого создается объект типа `Mutable` (аналогично вызову `clone()`, но в этот раз создается объект другого типа). Затем выполняется множество операций модификации с использованием этого объекта `Mutable`, при этом не требуется создание большого количества новых объектов. В конце он переходит обратно к типу `Immutable2`. Здесь мы создаем всего два новых объекта (`Mutable` и конечный объект типа `Immutable2`), а не четыре.
Этот подход особенно полезен в следующих случаях:
(1) Когда требуется непрерывность объекта,
(2) Часто необходимы значительные изменения,
(3) Создание нового непрерывного объекта слишком дорого.
## 12.4.3 Непрерывные строки
Рассмотрите следующий код:
```java
//: Stringer.java
public class Stringer {
static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = new String("howdy");
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(qq); // HOWDY
System.out.println(q); // howdy
}
} ///:~
```Когда `q` передается в метод `upcase()`, это фактически является копией ссылки на `q`. Объект, которому эта ссылка указывает, существует в одном единственном физическом месте. При перемещении ссылки она просто копируется.
При рассмотрении определения метода `upcase()`, можно заметить, что переданная ссылка имеет имя `s`, и это имя существует только во время выполнения метода `upcase()`. После завершения работы метода локальная ссылка `s` исчезает, а метод возвращает результат — тот же самый объект, но со всеми символами приведенными к верхнему регистру. Конечно, он возвращает ссылку на этот объект, но это ссылка на новый объект, а сам `q` остаётся нетронутым. Как это происходит?
(1) Неявная константа
Если использовать следующее объявление:
```java
String s = "asdf";
String x = Stringer.upcase(s);
Необходимо ли нам, чтобы метод upcase()
изменял переданный параметр? Обычно это не желательно, так как параметры обычно предоставляются для чтения кодом, а не для модификации. Это важное условие гарантирует, что код будет легче писаться и пониматься.Для реализации этой гарантии в C++ используется специальное ключевое слово: const
. С его помощью программист может гарантировать, что ссылка (в C++ называется "указатель" или "ссылка") не будет использована для изменения исходного объекта. Однако для того чтобы использовать const
везде, где это необходимо, C++-программист должен постоянно помнить об этом. Это может быть запутывающим и трудно запомнить. Перегрузка +
и StringBuilder
Используя ранее упомянутую технологию, объекты класса String
были спроектированы как «неизменяемые». Проанализировав онлайн-документацию по классу String
(который мы рассмотрим подробнее позже в этой главе), можно заметить, что каждый метод, который может модифицировать String
, фактически создает и возвращает новый объект String
, содержащий изменённую информацию — сам же исходный String
остаётся нетронутым. Таким образом, в Java отсутствует аналогичная возможность C++, предоставляемая компилятором для поддержки неизменяемости объектов. Чтобы достичь этого свойства, следует самостоятельно реализовать его, как это сделано в классе String
.
Поскольку объекты типа String
являются неизменяемыми, они могут многократно использоваться с одним и тем же значением (так называемое «алиасирование»). Поскольку эти объекты доступны только для чтения, один указатель не может изменять данные таким образом, чтобы повлиять на другие указатели. Поэтому неизменяемые объекты хорошо решают проблему алиасирования.Модификация объекта путём создания нового объекта со всеми изменениями, как это делает String
, кажется подходящим решением для всех случаев модификации. Однако этот подход может быть низкозатратным при выполнении некоторых операций. Примером является перегруженный оператор +
для объектов типа String
. «Перегрузка» означает, что смысл оператора меняется в зависимости от контекста использования (в случае String
— это единственный оператор, который может быть перегружен в Java; программисты не имеют возможности перегружать другие операторы — примечание ④).Примечание ④: В C++ программист имеет полную свободу перегружать операторы. Однако эта возможность обычно требует сложной реализации (см. книгу «Thinking in C++», выпущенную Prentice-Hall в 1995 году), поэтому дизайнеры Java решили, что это плохая особенность, и отказались от её поддержки. Однако стоит отметить, что перегрузка операторов в Java проще, чем в C++.
Для объектов типа String
использование оператора +
позволяет объединять различные строки:
String s = "abc" + foo + "def" + Integer.toString(47);
```Можно представить себе, как это работает: строка `"abc"` может иметь метод `append()`, который создает новую строку, содержащую `"abc"` вместе с содержимым `foo`; затем эта новая строка создает ещё одну новую строку, добавляющую `"def"` и так далее. Эта идея может сработать, но она требует создания большого количества объектов типа строки. Хотя конечной целью является получение нового объединённой строки, промежуточный этап включает использование множества строковых объектов и постоянную сборку мусора. Я сомневаюсь, что дизайнеры Java попробовали этот подход раньше (это урок программирования — нельзя полностью понять систему, если не протестировать код и не запустить некоторые вещи). Также мне кажется, что они давно выяснили, что такая реализация не обеспечивает приемлемую производительность. Решение проблемы заключается в создании изменяемого объекта типа `StringBuilder`. Для работы со строками этот объект называется `StringBuffer`, компилятор автоматически создаёт `StringBuffer`, чтобы вычислить определённое выражение, особенно при применении перегруженных операторов `+` и `+=` к объектам типа `String`. В следующем примере показано решение этой задачи:```java
//: ImmutableStrings.java
// Демонстрация использования StringBuffer
public class ImmutableStrings {
public static void main(String[] args) {
String foo = "foo";
String s = "abc" + foo +
"def" + Integer.toString(47);
System.out.println(s);
// Эквивалентное использование StringBuffer:
StringBuffer sb =
new StringBuffer("abc"); // Создает объект String!
sb.append(foo);
sb.append("def"); // Создает объект String!
sb.append(Integer.toString(47));
System.out.println(sb);
}
} ///:~
При создании строки s
, компилятор выполняет работу, которая эквивалентна использованию sb
— создаёт объект StringBuffer
и использует метод append()
, чтобы добавлять новые символы непосредственно в объект StringBuffer
(а не каждый раз создавать новый объект). Хотя это более эффективно, но не стоит каждый раз создавать такие строки как "abc"
и "def"
, так как компилятор преобразует их в объекты типа String
. Поэтому, хотя StringBuffer
обеспечивает большую производительность, он также создаёт больше объектов, чем нам бы хотелось.
String
и StringBuffer
Вот сводка методов, применимых как к классу String
, так и к классу StringBuffer
, чтобы иметь представление о том, как они взаимодействуют друг с другом. Эти таблицы не включают все отдельные методы, а лишь те, которые имеют важное отношение к нашему обсуждению. Перегруженные методы представлены отдельной строкой.
Сначала рассмотрим различные методы класса String
:| Метод | Аргументы, перегрузка | Назначение |
| --- | --- | --- |
| Конструктор | Перегружен: по умолчанию, String
, StringBuilder
, массив char
, массив byte
| Создание объекта типа String
|
| length()
| Отсутствует | Количество символов в String
|
| charAt()
| int index
| char
в указанной позиции внутри String
|
| getChars()
, getBytes()
| Начальная и конечная точки копирования, целевой массив для копирования, индекс целевого массива | Копирование char
или byte
в внешний массив |
| toCharArray()
| Отсутствует | Возвращает массив char[]
, содержащий все символы из String
|
| equals()
, equalsIgnoreCase()
| Строка для сравнения | Проверка равенства содержимого двух строк |
| compareTo()
| Строка для сравнения | Результат может быть отрицательным, нулевым или положительным, в зависимости от лексикографического порядка String
и аргумента. Обратите внимание, что большие и маленькие буквы не считаются равными! |
| regionMatches()
| Положение смещения этого String
и другого String
, а также длина области для сравнения. Перегрузка добавляет возможность игнорировать регистр букв | Логический результат, указывающий на то, совпадает ли область для сравнения |
| startsWith()
| Возможная начальная строка. Перегрузка добавляет смещение как аргумент | Логический результат, указывающий начинается ли String
с данной строки || endsWith()
| Возможная окончающаяся строка | Логический результат, указывающий является ли данная строка окончанием |
| indexOf()
, lastIndexOf()
| Перегружены: char
, char
и начальный индекс, String
, String
и начальный индекс | Если аргумент не найден в этой String
, вернёт -1; иначе вернёт индекс начала аргумента. lastIndexOf()
начинает поиск с конца |
| substring()
| Перегружены: начальный индекс, начальный и конечный индексы | Возвращает новый объект String
, содержащий указанную подстроку |
| concat()
| Строка для объединения | Возвращает новый объект String
, содержащий все символы исходной строки и добавленные символы |
| replace()
| Старый символ для поиска, новый символ для замены | Возвращает новый объект String
, где произведена замена. Если ничего не найдено, используется старая строка |
| toLowerCase()
, toUpperCase()
| Отсутствует | Возвращает новый объект String
, где все символы приведены к нижнему или верхнему регистру соответственно. Если изменения не требуются, используются старые значения |
| trim()
| Отсутствует | Возвращает новый объект String
, где удалены пробелы в начале и конце. Если изменения не требуются, используются старые значения |
| valueOf()
| Переопределено: object
, char[]
, char[]
с смещением и количеством, boolean
, char
, int
, long
, float
, double
| Возвращает объект типа String
, содержащий символьное представление аргумента || Intern()
| Нет | Для каждого уникального набора символов создается один (и только один) объект типа String
. |
При этом изменения минимальны и сохранены структура и форматирование исходного текста.Примечание: Исходный текст был на китайском языке. Убедитесь, что каждый метод String
возвращает новый объект String
, если требуется изменение исходного содержимого. В противном случае, если нет необходимости в изменениях, метод просто возвращает ссылку на исходный объект String
. Это позволяет экономить место в памяти и системные затраты.
Ниже приведены методы класса StringBuilder
:| Метод | Аргументы/перегрузка | Описание |
| --- | --- | --- |
| Конструктор | Переопределён: по умолчанию, длина буфера, который создаётся на основе строки | Создание нового объекта StringBuilder
|
| toString()
| Нет | Создание строки на основе текущего StringBuilder
|
| length()
| Нет | Количество символов в StringBuilder
|
| capacity()
| Нет | Текущий размер выделенной памяти |
| ensureCapacity()
| Целое число, представляющее желаемую емкость | Увеличивает размер буфера до указанной величины |
| setLength()
| Целое число, указывающее новую длину строки в буфере | Обрезает или расширяет предыдущую строку. При увеличении заполняет пробелы значением null
|
| charAt()
| Целое число, представляющее положение элемента | Возвращает символ, находящийся в указанной позиции буфера |
| setCharAt()
| Целое число, представляющее положение элемента, и новый символ | Изменяет значение элемента в указанной позиции |
| getChars()
| Начальная и конечная точки копирования, целевой массив символов и индекс целевого массива | Копирует символы в внешний массив. В отличие от String
, здесь нет метода getBytes()
|
| append()
| Переопределён: Object
, String
, char[]
, char[]
с конкретным смещением и длиной, boolean
, char
, int
, long
, float
, double
| Преобразует аргумент в строку и добавляет её в конец текущего буфера. Если необходимо, увеличивает размер буфера || insert()
| Переопределён: первым аргументом является начальное положение вставки: Object
, String
, char[]
, boolean
, char
, int
, long
, float
, double
. | Преобразует второй аргумент в строку и вставляет её в текущий буфер. Положение вставки находится в начале области смещения. Если необходимо, увеличивает размер буфера. |
| reverse()
| Нет | Обращает порядок символов в буфере. |Наиболее часто используемый метод — это append()
. Этот метод используется компилятором при вычислении выражений со строками, содержащих операторы +
и +=
. Метод insert()
имеет аналогичную форму. Эти два метода позволяют выполнять важные операции над буфером без создания новых объектов.
Теперь вы уже знаете, что класс String
не является просто ещё одним классом, предоставленным Java. Внутри класса String
содержится множество специальных классов. С помощью компилятора и специального переопределения операторов приведения типа, таких как +
и +=
, можно преобразовать строковый литерал в объект типа String
. В этой главе мы рассмотрели последнее особое явление: "непрерывность" неизменяемости, достигаемую с использованием класса StringBuilder
, а также некоторые любопытные явления, возникающие при компиляции.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )