66
Эффективное программирование на Java Андрей Дмитриев [email protected] http://in4mix2006.narod.ru/ 2008 Copyright (C) 2000 - 2008 Sun Microsystems, Inc. All rights reserved.

Эффективное программирование на Java · Результат работы с таблицей Несмотря на то, что в таблицу была

  • Upload
    others

  • View
    23

  • Download
    0

Embed Size (px)

Citation preview

Эффективное программирование на Java

Андрей Дмитриев[email protected]://in4mix2006.narod.ru/2008

Copyright (C) 2000 - 2008 Sun Microsystems, Inc. All rights reserved.

Программа Методы генерации экземпляров. Вспомогательные классы. Устаревшие ссылки. Метод Object.finalize(). Переопределение Object.equals(). Переопределение Object.equals() и hashCode(). Проверка достоверности параметров метода. Неявное создание объектов. Тип объекта. Игнорирование исключений.

Часть 1. Методы генерации экземпляровСледует рассматривать возможность

созданий экземпляров класса с помощью статических методов генерации.

Пример использования class State { public State(){…}//конструктор //метод генерации public static State createDefaultState(){…} } … //использование State state = new State(); State newState = State.createDefaultState();

Пример использования (JDK)

public static Boolean valueOf(boolean b){ return (b?Boolean.TRUE:Boolean.FALSE); }

Недостатки Если конструктор спрятан, то создать

подкласс нельзя: классы, не имеющие открытых (или защищенных) конструкторов, не могут иметь подклассов.

Статический метод трудно отличим от других методов класса.Если использовать соглашения (valueOf(),

getInstance() и т.д.), то видимость улучшается.

Преимущества В отличие от конструктора, имеет

название, которое более точно отражает предназначение.

Не обязан при вызове создавать новый объект.

Имеет право вернуть не только сам объект данного класса, но и любой его подтип.

ВыводыИ конструкторы, и статические методы

генерации имеют свои области лучшего применения.

Часть 2. Вспомогательные классы Иногда приходится создавать классы,

целиком состоящие из статических методов.

Основное предназначение – предоставлять некоторую функциональность для других классов.

Пример использования class Util { public static int sign(int v){…} public static boolean translateValue

(boolean a, boolean b){…} … } … //использование int z = Util.sign(value); boolean b = Util.translateValue(value1, value2);

Альтернативное использование Данный конструктор называется конструктор по умолчанию. Он создается, если пользователем не был объявлен ни один

из конструкторов. Использование экземпляров данного класса нарушает

единство функциональности. Класс-наследник будет работать со вспомогательными методами

как с обычными методами экземпляра.

Util myUtil = new Util();

Абстрактный класс

Объявление класса Util абстрактным – неудачное решение:

Наследник Util может все равно быть создан.

Путаница ввиду того, что абстрактность предполагает использование класса в будущем.

Закрытый конструктор Объявление закрытого конструктора

позволяет предотвратить создание конструктора по умолчанию:

Кроме того, невозможно создать наследника класса Util ввиду того, что конструктор закрыт.

class Util { private Util(){} …}

ВыводыПредотвращение создания экземпляров

можно осуществить через перекрытие конструктора.

Часть 3. Устаревшие ссылки Автоматическая сборка мусора освобождает

от необходимости заботиться об удалении объектов.

Тем не менее, в некоторых случаях рекомендуется явно удалять объекты, необходимость в которых отпала.

Пример ситуацииВерхний элемент более использоваться стеком

не будет, однако ссылка на него осталась в массиве.

class Stack { public Object pop(){ if (size == 0) { throw …; } return elements[--size]; } … }

Реализация метода ensureCapacity()

private void ensureCapacity(){ if (elements.length == size){ Object [] old = elements; elements = new Object[2*elements.length]; System.arrayCopy(old, 0, elements, 0,

size); }

Удаление ненужных ссылок может происходить при увеличении размера стека:

Модификация метода pop()

public Object pop(){ if (size == 0) { throw …; } Object retValue = elements[--size]; elements[--size] = null; return retValue; }

Освобождение ссылки позволит сборщику мусора удалить объект при необходимости.

Выводы Обнуление ссылок позволяет экономнее

расходовать память. Побочный эффект: генерирование

исключения при попытке разыменовать обнуленную ссылку.

Часть 4. Метод Object.finalize()Следует избегать использования метода

Object.finalize().

Пример использования class DBConnector { protected void finalize(){ //некие завершающие действия } } … //рекомендация запустить сборку мусора

Runtime.getRuntime().gc(); //рекомендация запустить финализацию объектов

Runtime.getRuntime().runFinalization();

Предназначение В соответствии со спецификацией,

данный метод выполняется перед тем как объект будет удален из кучи (heap).

Спецификация не гарантирует его вызова немедленно и даже в течение жизни пользовательской программы.

Возможные ошибки Внутри метода finalize() можно восстановить

ссылку на данный объект.Неполностью инициализированные объекты.Нарушение целостности приложения.

При возникновении исключительной ситуации внутри метода finalize() возможно некорректное его исполнение.

Ошибка OutOfMemoryError вследствие того, что ресурсы ожидают освобождения внутри метода finalize().

Когда finalize() может быть нужен Для вызова завершающего метода, если

клиент забыл его вызвать. Для работы с ресурсами ОС через

С/С++.

Выводы

try { doConnect(url); } finally { doTermination(); }

• Рекомендуется создавать метод для завершения работы с объектом и заставлять клиентов его использовать всякий раз.

• Интерфейс Disposable.• Данный подход часто используется внутри блока

finally:

Выводы (cont.)

class DBConnector { protected void finalize(){ try { //…. } finally { super.finalize(); } }}

Если приходится использовать метод finalize(), то нужно связать его с методом предка:

Часть 5. Переопределение Object.equals() public boolean Object.equals(Object obj) возвращает

истину, если переданный объект равен исходному. Часто возникает необходимость переопределить

метод для того, чтобы гибко определять равенство объектов друг другу.

По умолчанию метод equals() возвращает истину только если ссылки равны.

Реализация по умолчанию Следует оставить реализацию без

изменений если:• Не предполагается сравнивать объекты;• Экземпляр класса внутренне уникален

(java.lang.Thread);• Метод уже унаследован от предка и этой

реализации достаточно;• Класс закрыт от внешнего использования и

никогда не будет вызван.

Когда переопределять метод Если есть понятие логической

эквивалентности. Обычно это касается классов-значений:

Integer, Date и т.д.

«Плюс» переопределенияВозможность использовать классы-

значения в качестве ключей.

Общие соглашения Рефлексивность: x.equals(x) == true; Симметричность: x.equals(y) == y.equals(x); Транзитивность: Если x.equals(y) == true и

y.equals(z) == true то x.equals(z) == true; Непротиворечивость: последовательные

вызовы equals всегда возвращают одно и то же значение, если объекты не меняются;

x.equals(null) всегда должно возвращать false.

Реализация метода при наследовании Не существует способа расширить класс,

порождающий экземпляры, и добавить к нему новый аспект, сохранив при этом соглашения для метода equals().

Композиция классов позволяет обойти данное ограничение.

Выводы Переопределение метода equals()

требует соблюдения нескольких правил. При сравнении объектов классов,

образующих иерархию, придется отказаться от метода equals().

Рекомендуется переопределять также метод hashCode().

Часть 6. Переопределение Object.equals() и hashCode() public boolean Object.equals(Object obj)

возвращает истину, если переданный объект равен исходному.

Часто возникает необходимость переопределить метод equals() для того, чтобы гибко определять равенство объектов друг другу.

По умолчанию метод equals() возвращает истину только если ссылки равны.

Метод Object.hashCode() Возвращает хеш-код (длинное целое

число) экземпляра класса. Возвращаемое значение используется

для организации коллекций объектов (HashMap, HashSet, HashTable).

Соглашения для метода hashCode() Возвращаемое значение hashCode() должно

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

Если два объекта равны (equals() возвратил true), то методы hashCode() двух объектов должны вернуть одно и то же значение.

Если два объекта не равны (equals() возвратил false), то методы hashCode() двух объектов могут вернуть одно и то же значение.

Устройство хеш-таблицы Коллекции данных распределяют

содержимое на основе их хеш-кода. Поиск вхождения в коллекцию данных

производится в том числе на основе хеш-кода.

Два класса: ключ и значениеclass CarId { int carId; CarId(int n) { carId = n; }}

class CarModel { String model; public CarModel(String model){ this.model = model; } public String toString(){return model;}}

Результат работы с таблицейНесмотря на то, что в таблицу была помещена

машина с номером три, поиск ее не находит. HashMap hm = new HashMap(); hm.put(new CarId(1), new CarModel("lada")); hm.put(new CarId(2), new CarModel("mazda")); hm.put(new CarId(3), new CarModel("volvo")); hm.put(new CarId(4), new CarModel("wv")); System.out.println("hm = " + hm + "\n"); CarId newCar = new CarId(3);

if(hm.containsKey(newCar)) System.out.println((CarModel)hm.get(newCar)); else { System.out.println("Key not found: " + newCar);} }

Модифицированный класс-ключПереопределение двух указанных

методов решает проблему.

class CarId2 { int carId; CarId2(int n) { carId = n; } public int hashCode() { return carId; } public boolean equals(Object o) { return (o instanceof CarId2) && (carId == ((CarId2)o).carId); }}

Рекомендации по реализации hashCode()

public int hashCode(){ int result = 17; result = 37 * result + field1; result = 37 * result + field2; return result;}

Цель: достичь уникальности значения для различных объектов:

Неизменяемый объект может запоминать свой хеш-код в поле.

Выводы Рекомендуется переопределять методы

equals() и hashCode() одновременно. Неудачный выбор хеш-функции может

повредить производительности работы со структурами данных.Различные объекты, имеющие одинаковые

хеш-коды, в коллекции будут организованы во вспомогательный список.

Часть 7. Проверка достоверности параметров метода Большинство методов и конструкторов имеют

ограничения на передаваемые параметры. Игнорирование некорректных параметров

откладывает обнаружение ошибки.

Пример: проверка параметра

/** … @return … @throws IOException, if … @throws IllegalArgumentException, if … */ public int readBytes(InputStream is){ if (is == null) { throw new IOException(…); }…

Важность проверокОсобенно важно проверять параметры,

используемые не во время исполнения метода, а позже.

Проверки Для публичных методов все проверки нужно

документировать. Выбрасывание исключения – подходящая

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

утверждения (assert). В некоторых случаях проверка параметров

недопустима по соображениям производительности. В таком случае проверку следует отложить.

Выводы Контроль передаваемых параметров

имеет важное значение для поддержания работоспособности

программы, простоты обнаружения проблемы. Излишних ограничений лучше избегать:

только действительно некорректные данныеметодам позволительно изменять свое

поведения в зависимости от параметров

Часть 8. Неявное создание объектов Строки – неизменяемые (Immutable)

объекты. Исполнение некоторых операций со

строками неявно порождает (промежуточные) строковые объекты.

Пример: создание новой строки Следующая запись создает два объекта

вместо одного:• String s = new String(“a string”);

Лучшее решение будет таким:• String s = “a string”;

Пример: объединение строк Объединение строк оператором «+» -

немасштабируемое решение:

Время, необходимое оператору «+» для объединения N строк, пропорционально N*N.

String s = “”;for (int i = 0; i < N; i++){

s+=“”+i;}

Интенсивная работа со строками Если строка предполагает

множественное изменение, то имеет смысл использовать изменяемые объекты:• java.lang.StringBuffer

• Организует синхронный доступ к строке.• java.lang.StringBuilder

• Эффективнее, но небезопасен для работы со многими потоками.

Другие неизменяемые объектыСледующие варианты использования конструкторов Boolean b = new Boolean(false); Integer i = new Integer(10); Float f = new Float(1.0);рекомендуется заменить на статические вызовы методов: public static Boolean valueOf(boolean b); public static Integer valueOf(int i); public static Float valueOf(float f);

Другие неизменяемые объекты (cont.) Например:

Long ln = new Long(1000); Лучше заменить на

Long ln = Long.valueOf(1000); Замечание: данного подхода следует

избегать в своих классах, если есть необходимость в новом экземпляре объекта.Ключ в таблице.

Autoboxing Для удобства написания можно

использовать автоматическую упаковку примитивных типов в классы-оболочки:ArrayList<Integer> list = new ArrayList<Integer>();list.add(0, 42);int total = list.get(0);

Выводы Если производительность имеет

решающее значение, то использовать операторы конкатенации строк не рекомендуется.

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

Часть 9. Тип объектаДля ссылки на объект рекомендуется использовать более общий интерфейс, а не конкретный класс.

Интерфейс как тип объекта Например,

Vector students = new Vector(); Лучше заменить на

List students = new Vector();

Возможности Программа приобретает дополнительную

гибкость. При изменении реализации программы

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

Выводы Имеет смысл предусмотреть

возможность изменения фактического типа объекта без необходимости изменять способ доступа к его данным.Возможная модернизация или развитие

кода.

Часть 10. Игнорирование исключений Декларирование исключения методом

указывает на необходимость предусмотреть его обработку.

Часто используется игнорирование исключения или игнорирование специфики исключения.

Пример игнорирования try { doConnect(url); } catch (ConnectException e) { //Пустой блок лишает смысла данное исключение.

}

Последствия Игнорирование может привести

программу в нежелательное состояние. При дальнейшей модификации кода

возникнет вопрос, почему данное исключение не учитывается.

Выводы Рекомендуется ставить в блок catch() нотификацию об

исключении: журнализация, вывод, файловый вывод. Если нотификация не нужна:

можно поместить комментарий, поясняющий причины отсутствия обработчика

пробросить исключение выше по стеку вызовов

try { doConnect(url); } catch (ConnectException e) { logger.info("message"); }

Ссылки

Effective Java, J. Bloch.

Q&A

Эффективное программирование на Java

Спасибо!

Андрей Дмитриев[email protected]://in4mix2006.narod.ru/2008