1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
12.4 只读类.md 33 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

12.4 Только читающие классы

Хотя в некоторых специфических случаях локальный дубликат, созданный с помощью метода 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 гарантирует, что объект не будет изменён.

12.4.2 Недостатки неизменяемостиНа первый взгляд, создание неизменяемого класса кажется хорошей идеей. Однако, когда действительно требуется изменённый экземпляр нового типа, придётся трудиться над созданием нового объекта, что может привести к увеличенной частоте сборки мусора. Для некоторых классов эта проблема не является значительной, но для других (например, для класса 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 обеспечивает большую производительность, он также создаёт больше объектов, чем нам бы хотелось.

12.4.4 Классы 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() имеет аналогичную форму. Эти два метода позволяют выполнять важные операции над буфером без создания новых объектов.

12.4.5 Особенности строки

Теперь вы уже знаете, что класс String не является просто ещё одним классом, предоставленным Java. Внутри класса String содержится множество специальных классов. С помощью компилятора и специального переопределения операторов приведения типа, таких как + и +=, можно преобразовать строковый литерал в объект типа String. В этой главе мы рассмотрели последнее особое явление: "непрерывность" неизменяемости, достигаемую с использованием класса StringBuilder, а также некоторые любопытные явления, возникающие при компиляции.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master