Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на Java.docx
Скачиваний:
2
Добавлен:
01.05.2025
Размер:
3.45 Mб
Скачать

Тема 3.12 Использование ключевого слова final

Какие бы богатые возможности ни представляли наследование и переопределение методов, в некоторых случаях бывает необходимо запретить их. Предположим, например, что вы создаете класс, в котором инкапсулированы средства управления некоторым устройством. Помимо прочего, в этом классе содержатся метод, инициализирующий данное устройство. Предположим также, что инициализационная процедура может быть составлена так, что пользователю будут доступны некоторые секретные данные, хранящиеся на устройстве. В этой ситуации вам потребуется запретить переопределение инициализационного метода. Язык Java предоставляет средства, позволяющие без труда запретить переопределение метода или наследование класса. Решает эту задачу ключевое слово final.

Тема 3.13 Предотвращение переопределения методов

Для того чтобы предотвратить переопределение метода, надо при его объявлении указать модификатор final. Методы, определенные таким образом, нельзя переопределять. Ниже приведен фрагмент кода, иллюстрирующий использование ключевого слова final.

Листинг 3.20

public class A {

public final void meth() {

System.out.println("This is a final method.");

}

}

public class B extends A {

public void meth() { // Ошибка! Переопределить метод невозможно

System.out.println("Illegal!");

}

}

Поскольку метод meth () объявлен как final, его нельзя переопределить в классе В. Если вы попытаетесь сделать это, то возникнет ошибка при компиляции программы.

Тема 3.14 Предотвращение наследования

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

Ниже приведен пример класса, подклассы которого создавать запрещено.

public final class А { . //

// Следующее определение класса недопустимо.

public class В extends А { // Ошибка! Подкласс класса А

// не может существовать.

// ...

Как видно из комментариев, недопустимо, чтобы класс В наследовал класс А определен как final.

Тема 3.15 Класс Object

В языке Java определен специальный класс Object. По умолчанию он считается суперклассом всех остальных классов. Другими словами, все классы являют подклассами Object. Это означает, что переменная типа Object может ссылаться на экземпляр любого класса. Более того, поскольку массивы реализованы в виде классов, переменная типа Object может также ссылаться на любой массив.

В классе Object определены перечисленные ниже методы, которые доступны в любом объекте.

Таблица 3.1 – Методы класса Object

Метод

Назначение

Object clone()

Создает новый объект, аналогичный объекту предназначенному для клонирования

boolean equals(Object объект)

Определяет, эквивалентны ли объекты

void finalize ()

Вызывается перед тем, как неиспользованный объект будет удален процедурой "сборки мусора"

Class<? Extends Object> getClass()

Позволяет определить класс объекта в процессе выполнения программы

int hashCode()

Возвращает хэш-код, связанный с вызывающим объектом

void notify()

Возобновляет работу потока, ожидающего оповещения от объекта

void notifyAll()

Возобновляет работу всех потоков, ожидающих оповещения от объекта

String toString()

Возвращает строку, описывающую объект

void wait()

Ожидает выполнения другого потока

void wait(long миллисекунды)

void wait(long миллисекунды, int наносекунды)

Методы getClass(), notify(), notifyAll(), и wait() объявлены final, остальные можно переопределять в подклассах.

Если при создании класса предполагается проверка логической эквива­лентности объектов, которая не выполнена в суперклассе, следует переоп­ределить два метода: equals(Object ob) и hashCode(). Кроме того, переопределение этих методов необходимо, если логика приложения предусматривает использование элементов в коллекциях. Метод equals() при сравнении двух объектов возвращает истину, если содержимое объектов эквивалентно, и ложь – в противном случае. При переопределении метода equals() должны выполняться соглашения, предусмотренные спецификацией языка Java, а именно:

  • рефлексивность – объект равен самому себе;

  • симметричность – если x.equals(y) возвращает значение true, то и y.equals(x) всегда возвращает значение true;

  • транзитивность – если метод equals() возвращает значение true при сравнении объектов x и y, а также y и z, то и при сравнении x и z будет возвращено значение true;

  • непротиворечивость – при многократном вызове метода для двух не подвергшихся изменению за это время объектов возвращаемое значение всегда должно быть одинаковым;

  • ненулевая ссылка при сравнении с литералом null всегда возвращает значение false.

При создании информационных классов также рекомендуется переопределять методы hashCode() и toString(), чтобы адаптировать их действия для создаваемого типа.

Метод hashCode() переопределен, как правило, в каждом классе и возвращает число, являющееся уникальным идентификатором объекта, завися­щим в большинстве случаев только от значения объекта. Его следует переопре­делять всегда, когда переопределен метод equals(). Метод hashCode() возвращает хэш-код объекта, вычисление которого управляется следующими соглашениями:

  • во время работы приложения значение хэш-кода объекта не изменяется, если объект не был изменен;

  • все одинаковые по содержанию объекты одного типа должны иметь одинаковые хэш-коды;

  • различные по содержанию объекты одного типа могут иметь различные хэш-коды.

Один из способов создания правильного метода hashCode(), гарантирующий выполнение соглашений, приведен ниже, в примере # 10.

Метод toString() следует переопределять таким образом, чтобы кроме стандартной информации о пакете (опционально), в котором находится класс, и самого имени класса (опционально), он возвращал значения полей объекта, вызвавшего этот метод (то есть всю полезную информацию объекта), вместо хэш-кода, как это делается в классе Object. Метод toString() класса Object возвращает строку с описанием объекта в виде:

getClass().getName() + '@' + Integer.toHexString(hashCode())

Метод вызывается автоматически, когда объект выводится методами println(), print() и некоторыми другими.

Объекты в методы передаются по ссылке, в результате чего в метод передается ссылка на объект, находящийся вне метода. Поэтому если в методе изменить значение поля объекта, то это изменение коснется исходного объекта. Во избежание такой ситуации для защиты внешнего объекта следует создать клон (копию) объекта в методе. Класс Object содержит protected-метод clone(), осуществляющий побитовое копирование объекта производного класса. Однако сначала необходимо переопределить метод clone() как public для обеспечения возможности вызова из другого пакета. В переопределенном методе следует вызвать базовую версию метода super.clone(), которая и выполняет собственно клонирование. Чтобы окончательно сделать объект клонируемым, класс должен реализовать интерфейс Cloneable. Интерфейс Cloneable не содержит методов относится к помеченным (tagged) интерфейсам, а его реализация гарантирует, что метод clone() класса Object возвратит точную копию вызвавшего его объекта с воспроизведением значений всех его полей. В противном случае метод генерирует исключение CloneNotSupportedException. Следует отметить, что при использовании этого механизма объект создается без вызова конструктора. В языке C++ аналогичный механизм реализован с помощью конструктора копирования.

Так как объекты создаются динамически с помощью операции new, а унич­тожаются автоматически, то желательно знать механизм ликвидации объектов и способ освобождения памяти. Автоматическое освобождение памяти, занимаемой объектом, выполняется с помощью механизма “сборки мусора”. Когда никаких ссылок на объект не существует, то есть все ссылки на него вышли из области видимости программы, предполагается, что объект больше не нужен, и память, занятая объектом, может быть освобождена. “Сборка мусора” происходит нерегулярно во время выполнения программы. Форсировать “сборку мусора” невозможно, можно лишь “рекомендовать” ее выполнить вызовом метода System.gc() или Runtime.getRuntime().gc(), но виртуальная машина выполнит очистку памяти тогда, когда сама посчитает это удобным. Вызов метода System.runFinalization() приведет к запуску метода finalize() для объектов утративших все ссылки.

Иногда объекту нужно выполнять некоторые действия перед освобождением памяти. Например, освободить внешние ресурсы. Для обработки таких ситуаций могут применяться два способа: конструкция try-finally и механизм finalization. Конструкция try-finally является предпочтительной, абсолютно надежной и будет рассмотрена в девятой главе. Запуск механизма finalization определяется алгоритмом сборки мусора и до его непосредственного исполнения может пройти сколь угодно много времени. Из-за всего этого поведение метода finalize()может повлиять на корректную работу программы, особенно при смене JVM. Если существует возможность освободить ресурсы или выполнить другие подобные действия без привлечения этого механизма, то лучше без него обойтись. Виртуальная машина вызывает этот метод всегда, когда она собирается уничтожить объект данного класса. Внутри метода finalize(), вызываемого непосредственно перед освобождением памяти, следует определить действия, которые должны быть выполнены до уничтожения объекта.

Метод finalize() имеет следующую сигнатуру:

protected void finalize(){

// код завершения

}

Ключевое слово protected запрещает доступ к finalize() коду, определенному вне этого класса. Метод finalize() вызывается только перед самой “сборкой мусора”, а не тогда, когда объект выходит из области видимости, то есть заранее невозможно определить, когда finalize() будет выполнен, и недоступный объект может занимать память довольно долго. В принципе этот метод может быть вообще не выполнен! Недопустимо в приложении доверять такому методу критические по времени действия по освобождению ресурсов.

В листинге 3.21 показан пример переопределения всех методов.

Листинг 3.21

public class Test implements Cloneable {

private int a;

private int b;

public Test() {

}

public Test(int a, int b) {

this.a = a;

this.b = b;

}

public int getA() {

return a;

}

public int getB() {

return b;

}

public void setA(int a) {

this.a = a;

}

public void setB(int b) {

this.b = b;

}

@Override

public String toString() {

return "Test{" + "a=" + a + ", b=" + b + '}';

}

@Override

public boolean equals(Object obj) {

if (obj == null) {

return false;

}

if (getClass() != obj.getClass()) {

return false;

}

final Test other = (Test) obj;

if (this.a != other.a) {

return false;

}

if (this.b != other.b) {

return false;

}

return true;

}

@Override

public int hashCode() {

int hash = 7;

hash = 97 * hash + this.a;

hash = 97 * hash + this.b;

return hash;

}

@Override

protected Object clone() {

try {

return super.clone();

} catch (CloneNotSupportedException ex) {

System.out.println("Ошибка при клонировании объекта");

}

return null;

}

@Override

protected void finalize() throws Throwable {

System.out.println("Объект будет удален: а=" + a + ", b=" + b);

}

}

public class Main {

public static void main(String[] args) {

Test ob = new Test();

ob.setA(1);

ob.setB(2);

Test ob2 = new Test(1, 2);

if (ob.equals(ob2)) {

System.out.println("объеты равны");

}

System.out.println(ob);

System.out.println(ob.hashCode());

System.out.println(ob2.hashCode());

Test ob3 = (Test) ob.clone();

System.out.println(ob3);

System.out.println(ob3.hashCode());

ob = null;

System.gc(); // просьба выполнить "сборку мусора"

}

}

В результате выполнения данной программы получим:

объеты равны

Test{a=1, b=2}

65962

65962

Test{a=1, b=2}

65962

Объект будет удален: а=1, b=2