<![CDATA[Сайт свободных программ]]> ru <![CDATA[Отзыв о kwork.ru]]> Кворк пятый год.

Стабильный доход во фрилансе я начал получать за пять лет до регистрации на Кворк, но примерно с 2018-го года доходы с форумов упали, и я решил попробовать Кворк.
На форумах я занимался только ссылочными прогонами по базам сайтов и восстановлением сайтов и archive.org и переносом сайтов на CMS Wordpress. По прогонам, заказов было достаточно, но с каждым годом их было всё меньше и меньше, так как интерес к данному методу продвижения сайтов пропадает. По восстановлению сайтов из archive.org и по переносу сайтов на CMS Wordpress с форумов были заказы, но основная масса до 2019 года. Другие биржи фриланса, кроме одной похожей, я даже не стал пробовать, вот причины.

1. Нет карточек услуг, то есть чтобы Покупатель смог сделать заказ, он должен опросить сотни Исполнителей или разместить объявление и ждать. На Кворк же он может выбрать объявление, предоставить требуемую информацию и оплатить заказ, потому что Кворк работает как магазин, то есть Покупатели выбирают товары, в данном случае услуги, а не ждут когда кто-то откликнется, а Исполнители как продавцы данного магазина не имеет права отказываться предоставлять услугу без уважительных причин ! Если у Исполнителя нет времени, он может просто включить статус "занято", и его товары тут же будут убраны с полок магазина в его кабинет. Так как нет карточек услуг, Исполнитель, чтобы успевать получать побольше заказов, должен примерно каждые пол часа проверять сайт на наличие новых объявлений и стараться успеть отправить лучший отклик !
Есть у Кворк и обычная биржа фрилана, но мне обычные биржи фриланса не подходят.

2. На других биржах фриланса нет нужных мне рубрик, а конкретно доски объявлений и каталоги компаний. Ссылочные прогоны у них редкость, а чтоб успеть получить заказ, например по восстановлению сайтов из archive.org или по переносу сайтов на CMS Wordpress, необходимо примерно каждые пол часа смотреть новые объявления !


Мне, как Исполнителю, Кворк нравится по той причине, что нет необходимости для получения заказа каждые пол часа проверять сайт на наличие объявлений по своей теме. Покупатель приходит в магазин, покупает у меня товары, в данном случае услуги, и по желанию оставляет отзывы ! Есть одна похожая биржа фриланса, где я зарегистрировался два года назад, но там нет досок объявлений, нет каталогов компаний, а по восстановлению сайтов из вебархива и по переносу сайтов на CMS Wordpress у меня было всего 2-3 заказа за год !

Рейтинги. Чем больше у Исполнителя рейтинг, чем больше у него отзывов, тем выше его объявления отображаются в каталогах ! Это позволяет Исполнителям получать побольше заказов, а Покупателям видеть на первых страницах каталогов только лучшие предложения !

Комиссии. Я пересмотрел большинство бирж фриланса, как русскоязычных, так и англоязычных. Комиссия за гарантию сделок что у Кворка, что у других бирж примерно одинакова ! Считаю комиссю Кворка достойной платой за то, что нет необходимости примерно каждые пол часа проверять сайт на наличие новых объявлений и стараться успеть отправить лучший отклик, чтобы получить заказ !
Вывод заработка в банк. Мне нравиться, что Кворк выводит деньги как в недружественные страны, что крайне важно для меня!, так и в РФ ! С нетерпением жду возможность вывода заработка в криптовалюты !

Можно быть как Покупателем, так и Исполнителем. Если Вы зарегистрировались, например Покупателем, но хотите купить, Вам не нужно регистрироваться Исполнителем. Ваш баланс на Кворк всегда доступен как для вывода заработка, так и для покупок !

Какие выводи ? Без Кворк я бы не нашёл работу по перечисленным нишам во фрилансе !
Отдельное спасибо я хочу сказать разработчикам Кворк, которые составили уйму рекомендаций по работе с клиентами и ютуб-каналу "Бутик Идей" где по пунктам рассказано как работать с клиентами, чтобы получать побольше заказов и отзывов. Данные рекомендации мне помогают на работать и без гаранта !]]>
Разное https://linexp.ru?id=4757 Tue, 23 Apr 2024 18:48:59 GMT
<![CDATA[Отзыв о avito-shop.su]]> В ответ получил хамство и аккаунт проработавший всего один день ! Вначале я немного ошибся и оплатил не тот товар.
Пишу оператору просьбу вернуть мои 350 рублей либо продать другую услугу с доплатой от меня. Он ответил что услуги возврата денег нет, покупайте новый товар через скрипт на сайте (то есть оплатите полную стоимость нового товара).
После чего он оскорбил меня матом и тут же удалил переписку.
Мой ответ:
Я купил товар из рубрики "AVITO | login:pass"
По этому мною подразумевалось, что мною купленный товар это аккаунт с отзывом !
Вы зачем вводите в заблуждение и отказываетесь возвращать деньги ?

Ответили так:
Вот именно, Вами подразумевалось !
Вы ввели себя в заблуждение
Денег Вам никто не вернет. Купленный товар вы можете получить
Ждём ссылку на объявление
Снова хамство и мат ! См. скриншот.
Ждем ссылку на объявление
https//linexp.ru/uploads/files/images/avito-shop.su1.png

На следующий день я обратился к ним по другим контактам, и видать попал на хозяина.Я сказал ему, что я оплатил не ту услугу, и прошу или вернуть деньги, или продать мне другой товар. Он ответил, что оператор работает. А я ему: оператор наотрез отказывается продавать мне другой акк. — Сейчас оператор ответит.
Оператор ответил. без хамства. Договорились с ним за покупку AVITO.RU (Авито) ▶ Номер для AVITO на 14 дней за 450 рублей. Он продал мне номер на следующий день, я доплатил сто рублей и целый день пользовался аккаунтом авито.
Чтобы не потерять аккаунт я следовал всем его рекомендациям, то есть регистрация и вход только через московский прокси, даже не закрывал вкладку с авито, не выходил с авито и не закрывал браузер. IP не менялся, регистрация и пользование аккаунтом было непосредственно с самого VPS через RDP. Может быть я забыл настроить User agent на WIndows
За весь день разослал всего 8 вопросов разным продавцам и получил бан.
https//linexp.ru/uploads/files/images/avito-shop.su3.png
https//linexp.ru/uploads/files/images/avito-shop.su2.png
В итоге вместе с комиссией банка потеряно 550 рублей, а также время и нервы.

В отзывах я нашел их старый домен avitoshop.deer.su, который перенаправляет на новый. Наверно это потому, что старый домен забанили в какой-либо поисковой системе. А пожаловаться можно регистратору домена, хостингу, поисковым системам.

Пользоваться или не пользоваться их услугами, решайте сами. Я не рекомендую !]]>
Другое https://linexp.ru?id=4756 Wed, 21 Jun 2023 10:16:42 GMT
<![CDATA[Как установить и настроить Shadowsocks proxy с обфускацией (скрытием) вида трафика]]>
Shadowsocks стал неотъемлемым инструментом для доступа к ограниченному контенту в таких странах со строгой цензурой, как Китай.
Брандмауэр может определить, запрашиваете ли вы данные, используя другой маршрут, и заблокирует запрещенный веб-трафик. Shadowsocks использует SOCKS5, интернет-протокол с открытым исходным кодом, чтобы добавить слой шифрования между вашим локальным компьютером и прокси-сервером, к которому вы подключаетесь.

Shadowsocks использует TCP и HTTPS, которые не обладают таким же уровнем безопасности, что и VPN, но позволяют Shadowsocks обойти ограничения с лучшей скоростью соединения. Настраивать этот прокси сервер намного проще, чем любые другие прокси и впн.

Обфускация трафика — это дополнительное скрытие трафика, чтобы оборудование интернет-провайдера DPI (Deep Packet Inspection) не могло определить тип трафика.
Deep Packet Inspection (DPI) может обнаружить уникальную подпись Shadowsocks, поэтому его протокол не подходит, если провайдер решил фильтровать трафик. В России некоторые провайдеры, такие как yota, блокируют Shadowsocks без обфускации.

Устанавливаем Shadowsocks и плагин simple-obfs для скрытия трафика на сервер. Simple-obfs устарел и рекомендуют использовать плагин v2ray, но так как у меня старый линукс и нет возможности его обновить без потери некоторых драйверов, а пытаться собирать Shadowsocks и V2ray из исходников не хочется, я установил simple-obfs.

Настраиваем сервер.
apt install shadowsocks-libev simple-obfs
nano /etc/shadowsocks-libev/config.json
{
"server":["123.123.123.123"],
"mode":"tcp_and_udp",
"server_port":443,
"password":"7777",
"timeout":60,
"method":"chacha20-ietf-poly1305",
"plugin":"obfs-server",
"plugin_opts":"obfs=tls;--failover www.google.com"
}

Немного проясню за пункты:

"server":["123.123.123.123"] - сюда вы вписываете IP'шник вашего сервака;

"mode":"tcp_and_udp" - это протоколы соединения;

"server_port":443 - это порт TLS, через который будет идти трафик;

"password":"7777" - сюда вписываете любой пароль;

"method":"chacha20-ietf-poly1305" - это алгоритм шифрования за авторством Daniel J. Bernstein, разрабатываемый Гуглом для внутренних нужд. Хорош для устройств, не умеющих аппаратный AES, а это совсем дешёвые мобильники и прочие умные утюги; и для параноиков, боящихся закладок в процессоре;

"plugin":"obfs-server" - это плагин obfs-proxy, который и будет маскировать наш трафик под TLS.

failover=www.google.com - здесь адрес сервиса google.com, DPI будет видеть именно его безобидный трафик.

Затем нужно разрешить прослушивать серверу ss-server порты <1024 без запуска из-под root, нужно выполнить следующую команду:

setcap 'cap_net_bind_service=+ep' /usr/bin/ss-server

Если закрыт порт 443, тогда нужно его открыть:
iptables -I INPUT -p tcp --dport 443 -j ACCEPT

Перезапустим Shadowsocks:
systemctl restart shadowsocks-libev.service

Делаем системД юнит, в котором будет работать симпл-обфс

nano /etc/systemd/system/ss-obfs.service
[Unit]
Description=simple-obfuscation standalone server service
Documentation=man:shadowsocks-libev(8)
After=network.target
[Service]
Type=simple
User=nobody
Group=nogroup
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
LimitNOFILE=51200
ExecStart=/usr/bin/obfs-server -s 123.123.123.123 -p 80 --obfs http -r 127.0.0.1:1234 --failover www.google.com
[Install]
WantedBy=multi-user.target

в опцию “failover” вместо гугла можно написать что угодно — в эту сторону симпл-обфс будет перенаправлять трафик пришедший на 80ый порт от не-симпл-обфс клиентов, как-то активный пробинг провайдерами, фаерволами, майорами, etc. Желательно чтобы фейловер совпадал с опцией obfs-host клиента

3 сохраняем-выходим, рестартуем и включаем сервис
systemctl restart ss-obfs && systemctl enable ss-obfs

Настраиваем Shadowsocks и simple-obfs на компьютере.
На компьютере также устанавливаем shadowsocks-libev и simple-obfs. Создаём конфигурационный файл shsocks-obfs-tls.json:
nano ~/shsocks-obfs-tls.json
{
"server":"123.123.123.123",
"server_port":443,
"local_port":996,
"password":"7777",
"timeout":60,
"method":"chacha20-ietf-poly1305",
"plugin":"obfs-local",
"plugin_opts":"obfs=tls;obfs-host=www.google.com"
}

3 сохраняем-выходим, рестартуем и включаем сервис
systemctl restart ss-obfs && systemctl enable ss-obfs

ss-local -c ~/shsocks-obfs-tls.json

Если всё нормально, то у вас должно начаться соединение и появятся такие строки в терминале:
2019-10-24 10:35:55 INFO: plugin "obfs-local" enabled
2019-10-24 10:35:55 INFO: initializing ciphers... chacha20-ietf-poly1305
2019-10-24 10:35:55 INFO: listening at 127.0.0.1:996
2019-10-24 10:35:55 [simple-obfs] INFO: obfuscating enabled
2019-10-24 10:35:55 [simple-obfs] INFO: obfuscating hostname: www.google.com
2019-10-24 10:35:55 [simple-obfs] INFO: tcp port reuse enabled
2019-10-24 10:35:55 [simple-obfs] INFO: listening at 127.0.0.1:38393

Настраиваем Shadowsocks и Simple-obfs на Android.
Клиент Shadowsocks для Android позволяет пробросить через Shadowsocks любое приложение, которое вы выберите. Shadowsocks есть в Google Play и в F-Droid, а Simple-obfs уже нет в F-Droid, но он ещё есть в Google Play. В F-Droid есть V2ray. Тут те же настройки, разница только в том, что их можно ввести в окно, а не в текстовый файл.

https://f-gzhechko.medium.com/установка-ss-сервера-в-standalone-режиме-и-кастомизация-обфускаторов-1d173b3d5513

https://vk.com/@haccking1-ustanavlivaem-i-ispolzuem-shadowsocks-dlya-obhoda-dpi]]>
Компьютерная сеть, Компьютерные советы https://linexp.ru?id=4755 Mon, 12 Dec 2022 15:54:09 GMT
<![CDATA[Как удалить сайт или отдельные страницы из Вебархива (archive.org). Проверенный способ !]]> Вебархива. Раньше было достаточно добавить в robots.txt специальные директивы, но сейчас это не работает. Можно найти много устаревших и нерабочих примеров обращения в Вебархив с просьбой удаления всего сайта или конкретного периода. Я проверил эти устаревшие и нерабочие способы, убедился что они не работают.
Вот, какой ответ мне прислали из Вебархива.
Благодарим Вас за обращение к нам. Wayback Machine — это некоммерческий проект, Интернет-архив для сохранения сайтов Интернета в целях исследования и ради общего блага.
Чтобы мы могли лучше рассмотреть и помочь с вашим запросом относительно site1.ru и site2.ru, выполните следующие действия.

ШАГ 1: ПЕРЕЧИСЛИТЕ ВСЕ URL/URL-ПУТИ, КОТОРЫЕ ВЫ ХОТИТЕ ИСКЛЮЧИТЬ, ПЕРИОД ВАШЕГО ВЛАДЕНИЯ И ПЕРИОД, КОТОРЫЙ ВЫ ХОТИТЕ ИСКЛЮЧИТЬ (где это возможно, мы настроим исключение на запрошенный период)


ПРИМЕР 1 (несколько URL/путей из одного домена за один и тот же период времени):
URL/URL-путь для исключения: site1.ru/dir/file.html.
URL/URL-путь для исключения: site1.ru/images/
период владения доменом: 2020-02-25 по настоящее время (to present)
период времени для исключения: 2020-02-25 в будущее (to future)

ПРИМЕР 2 (полный домен и поддомены):
URL-адрес/URL-путь для исключения: site2.com (и все поддомены)
период владения доменом: с 1998-01-31 по 2001-08-30
период времени для исключения: с 1998-01-31 по 2001-08-30

ШАГ 2. Выберите и следуйте соответствующим разделам ниже для URL-адресов, которые вы хотите исключить из Wayback Machine.


ЕСЛИ ВЫ ЛИЧНО ВЛАДЕЕТЕ ВЕБ-САЙТАМИ данных URL-адресов, помогите нам подтвердить ваше право собственности на данные URL-адреса, выполнив одно из следующих действий:


(ОБРАТИТЕ ВНИМАНИЕ: если в whois для домена указано, что самая последняя регистрация была произведена позже периода, который вы хотите исключить, мы можем запросить подтверждение прошлого владения в дополнение к любой проверке текущего владения)

Добавьте текстовый файл с вашим запросом в корневой каталог сайта (например, domain.com/waybackverify.txt) или в записи DNS.
Если на вашем сайте указан основной адрес электронной почты, отправьте нам запрос с этого адреса (и укажите ссылку на место на сайте, где указан контакт). Примечание: если на сайте указаны контакты для обслуживания клиентов мы можем запросить дополнительную проверку.

Если адрес электронной почты владельца регистрации общедоступен в WHOIS, отправьте нам электронное письмо с этого адреса (и ссылку на список whois, где данный эмаил отображается).

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

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

ЕСЛИ ВЫ ПРЕДСТАВЛЯЕТЕ ЛИЦО, КОТОРОЕ ВЛАДЕЕТ ЛЮБЫМИ САЙТАМИ данных URL-адресов, помогите нам подтвердить ваше право собственности на данные URL-адреса, выполнив одно из следующих действий:


Добавьте текстовый файл с вашим запросом в корневой каталог сайта (например, domain.com/waybackverify.txt) или в записи DNS.
Если адрес электронной почты владельца регистрации общедоступен в списке поиска WHOIS, отправьте нам электронное письмо с этого адреса (и ссылку на список whois, где данный эмаил отображается).

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

Если вам не доступен ни один из применимых вариантов подтверждения, и вы считаете, что существует альтернативный способ четко и окончательно доказать право собственности, вы можете отправив нам соответствующую информацию в ответ на это письмо. Пожалуйста, поймите, что мы добросовестно проверим любой непосредственно относящийся к делу и управляемый, но не гарантируем какой-либо результата !

Кроме того, если вы считаете, что какие-либо архивы нарушают ваши авторские права, вы можете подать претензию в отношении авторских прав в соответствии с нашей Политикой в ​​отношении авторских прав, размещенной на нашем сайте по адресу http://www.archive.org/about/terms.php

Еще раз благодарим вас за то, что связались с нами и работали над нашим процессом.

---
Команда интернет-архива

Как я удалял сайты из Вебархива

.

Я отправил на ихнюю почту support@archivesupport.zendesk.com с почты, на которую зарегистрированы домены, следующее письмо:
Тема: DMCA Take Down Notice
Please, exclude _full domains_ & _subdomains_: site1.ru, site2.ru
Они пишут, что если в whois для домена указано, что самая последняя регистрация была произведена позже периода, который вы хотите исключить, мы можем запросить подтверждение прошлого владения в дополнение к любой проверке текущего владения.
Вебархив удаляет только период вашего владения доменом. То есть чтобы полностью удалить из Вебархива период своего владения доменами, а домены принадлежали не только мне, на сайте Вебархива я нашёл точный период своего владения данными доменами.
Time period of site1.ru: 2012-02-21 to 2022-06-15 and to forever and ever
Time period of site2.ru: 2016-01-10 to 2016-04-28 and to forever and ever
В корне сайта нужно разместить файл для подтверждения владения доменом.
http://site1.ru/waybackverify.txt
http://site2.ru/waybackverify.txt
И через два дня пришло сообщение, что указанные периоды владения доменами были добавлены к исключению из Wayback Machine, пожалуйста подождите 1 день, пока изменения вступят в силу. И через 1 день указанные периоды были удалены из archive.org.]]>
Компьютерные советы https://linexp.ru?id=4754 Wed, 13 Jul 2022 10:58:34 GMT
<![CDATA[Kodi]]> Благодаря красивому интерфейсу и мощному механизму создания скинов он доступен для Android, BSD, Linux, macOS, iOS и Windows.

Есть смарт тв? Вы еще ничего не видели! Kodi посрамляет ваш умный телевизор. Kodi работает на огромном количестве устройств и операционных систем. Ваша музыкальная коллекция еще никогда не выглядела так хорошо! Поддержка почти всех форматов, списков воспроизведения, миксов для вечеринок и многого другого. Это намного лучше, чем куча DVD на полке. Kodi оживляет вашу коллекцию фильмов, добавляя обложки, жанры актеров и многое другое. Идеально подходит для запойного просмотра или случайного просмотра вашего любимого шоу. Kodi держит весь ваш телевизор в порядке, как ничто другое. Kodi — лучший способ поделиться своими фотографиями на самом большом экране в доме или, может быть, просто украсить стену персональным слайд-шоу. Kodi — это программное обеспечение, созданное и запущенное сообществом для сообщества.
]]>
Фильмы и музыка, Домашние кинотеатры https://linexp.ru?id=4752 Sat, 02 Jul 2022 17:15:36 GMT
<![CDATA[Thinking_in_Java_4th_edition]]>

ВВЕДЕНИЕ В ОБЪЕКТЫ

Возникновением компьютерной революции мы обязаны машине. Поэтому наши языки программирования стараются быть ближе к этой машине.

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

Эта глава познакомит вас с основами ООП, включая рассмотрение основных методов разработки программ. Она, и книга вообще, подразумевает наличие у вас опыта программирования на процедурном языке, не обязательно C.

Настоящая глава содержит подготовительный и дополнительный материалы. Многие читатели предпочитают сначала представить себе общую картину, а уже потом разбираться в тонкостях ООП. Поэтому многие идеи в данной главе служат тому, чтобы дать вам цельное представление об ООП. Однако многие люди не воспринимают общей идеи до тех пор, пока не увидят конкретно, как все работает; такие люди нередко вязнут в общих словах, не имея перед собой примеров. Если вы принадлежите к последним и горите желанием приступить к основам языка, можете сразу перейти к следующей главе — пропуск этой не будет препятствием для написания программ или изучения языка. И все же чуть позже вам стоит вернуться к этой главе, чтобы расширить свой кругозор и понять, почему так важны объекты и какое место они занимают при проектировании программ.

Развитие абстракции

Все языки программирования построены на абстракции. Возможно, трудность решаемых задач напрямую зависит от типа и качества абстракции. Под словом «тип» я имею в виду: «Что конкретно мы абстрагируем?» Язык ассемблера есть небольшая абстракция от компьютера, на базе которого он работает. Многие так называемые «командные» языки, созданные вслед за ним (такие, как Fortran, BASIC и C), представляли собой абстракции следующего уровня. Эти языки обладали значительным преимуществом по сравнению с ассемблером, но их основная абстракция по-прежнему заставляет думать вас о структуре компьютера, а не о решаемой задаче. Программист должен установить связь между моделью машины (в «пространстве решения», которое представляет место, где реализуется решение, — например, компьютер) и моделью задачи, которую и нужно решать (в «пространстве задачи», которое является местом существования задачи — например, прикладной областью). Для установления связи требуются усилия, оторванные от собственно языка программирования; в результате появляются программы, которые трудно писать и тяжело поддерживать. Мало того, это еще создало целую отрасль «методологий программирования».

Альтернативой моделированию машины является моделирование решаемой задачи. Ранние языки, подобные LISP и APL, выбирали особый подход к моделированию окружающего мира («Все задачи решаются списками» или «Алгоритмы решают все» соответственно). PROLOG трактует все проблемы как цепочки решений. Были созданы языки для программирования, основанного на системе ограничений, и специальные языки, в которых программирование осуществлялось посредством манипуляций с графическими конструкциями (область применения последних оказалась слишком узкой). Каждый из этих подходов хорош в определенной области решаемых задач, но стоит выйти из этой сферы, как использовать их становится затруднительно.

Объектный подход делает шаг вперед, предоставляя программисту средства для представления задачи в ее пространстве. Такой подход имеет достаточно общий характер и не накладывает ограничений на тип решаемой проблемы. Элементы пространства задачи и их представления в пространстве решения называются «объектами». (Вероятно, вам понадобятся и другие объекты, не имеющие аналогов в пространстве задачи.) Идея состоит в том, что программа может адаптироваться к специфике задачи посредством создания новых типов объектов так, что во время чтения кода, решающего задачу, вы одновременно видите слова, ее описывающие. Это более гибкая и мощная абстракция, превосходящая по своим возможностям все, что существовало ранее<ref>Некоторые разработчики языков считают, что само по себе объектно-ориентированное программирование не является достаточным для решения всех задач программирования, и выступают за сочетание различных парадигм программирования в одном языке. Такие языки называют мультипарадигма?льными (multiparadigm). Смотрите книгу Тимоти Бадда Multiparadigm Programming in Leda (Addison-Wesley, 1995).</ref>. Таким образом, ООП позволяет описать задачу в контексте самой задачи, а не в контексте компьютера, на котором будет исполнено решение. Впрочем, связь с компьютером все же сохранилась. Каждый объект похож на маленький компьютер; у него есть состояние и операции, которые он позволяет проводить. Такая аналогия неплохо сочетается с внешним миром, который есть «реальность, данная нам в объектах», имеющих характеристики и поведение.

Алан Кей подвел итог и вывел пять основных черт языка Smalltalk — первого удачного объектно-ориентированного языка, одного из предшественников Java. Эти характеристики представляют «чистый», академический подход к объектно-ориентированному программированию:


  • Все является объектом. Представляйте себе объект как усовершенствованную переменную; он хранит данные, но вы можете «обращаться с запросами» к объекту, требуя у него выполнить операции над собой. Теоретически абсолютно любой компонент решаемой задачи (собака, здание, услуга и т. п.) может быть представлен в виде объекта.
  • Программа — это группа объектов, указывающих друг другу, что делать, посредством сообщений. Чтобы обратиться с запросом к объекту, вы «посылаете ему сообщение». Более наглядно можно представить сообщение как вызов метода, принадлежащего определенному объекту.
  • Каждый объект имеет собственную «память», состоящую из других объектов. Иными словами, вы создаете новый объект с помощью встраивания в него уже существующих объектов. Таким образом, можно сконструировать сколь угодно сложную программу, скрыв общую сложность за простотой отдельных объектов.

  • У каждого объекта есть тип. В других терминах, каждый объект является экземпляром класса, где «класс» является аналогом слова «тип». Важнейшее отличие классов друг от друга как раз и заключается в ответе на вопрос: «Какие сообщения можно посылать объекту?»
  • Все объекты определенного типа могут получать одинаковые сообщения. Как мы вскоре убедимся, это очень важное обстоятельство. Так как объект типа «круг» также является объектом типа «фигура», справедливо утверждение, что «круг» заведомо способен принимать сообщения для «фигуры». А это значит, что можно писать код для фигур и быть уверенным в том, что он подойдет для всего, что попадает под понятие фигуры. Взаимозаменяемость представляет одно из самых мощных понятий ООП.

Буч предложил еще более лаконичное описание объекта:

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

Суть сказанного в том, что объект может иметь в своем распоряжении внутренние данные (которые и есть состояние объекта), методы (которые определяют поведение), и каждый объект можно уникальным образом отличить от любого другого объекта — говоря более конкретно, каждый объект обладает уникальным адресом в памяти<ref>Это верно с некоторыми ограничениями, поскольку объекты могут реально существовать на других машинах и в различных адресных пространствах, и также могут храниться на диске. В этих случаях, индивидуальность объекта должна определяться чем-то иным, чем адресом памяти.</ref>.

Объект имеет интерфейс

Вероятно, Аристотель был первым, кто внимательно изучил понятие типа; он говорил о «классе рыб и классе птиц». Концепция, что все объекты, будучи уникальными, в то же время являются частью класса объектов со сходными ха­рактеристиками и поведением, была использована в первом объектно-ориентированном языке Simula-67, с введением фундаментального ключевого слова class, которое вводило новый тип в программу.

Язык Simula, как подразумевает его имя, был создан для развития и моделирования ситуаций, подобных классической задаче «банковский кассир». У вас есть группы кассиров, клиентов, счетов, платежей и денежных единиц — много «объектов». Объекты, идентичные во всем, кроме внутреннего состояния во время работы программы, группируются в «классы объектов». Отсюда и пришло ключевое слово class. Создание абстрактных типов данных есть фундаментальное понятие во всем объектно-ориентированном программировании. Абстрактные типы данных действуют почти так же, как и встроенные типы: вы можете создавать переменные типов (называемые объектами или экземплярами в терминах ООП) и манипулировать ими (что называется посылкой сообщений или запросом; вы производите запрос, и объект решает, что с ним делать). Члены (элементы) каждого класса обладают сходством: у каждого счета имеется баланс, каждый кассир принимает депозиты, и т. п. В то же время все члены отличаются внутренним состоянием: у каждого счета баланс индивидуален, каждый кассир имеет человеческое имя. Поэтому все кассиры, заказчики, счета, переводы и прочее могут быть представлены уникальными сущностями внутри компьютерной программы. Это и есть суть объекта, и каждый объект принадлежит к определенному классу, который определяет его характеристики и поведение.

Таким образом, хотя мы реально создаем в объектных языках новые типы данных, фактически все эти языки используют ключевое слово «класс». Когда видите слово «тип», думайте «класс», и наоборот<ref>Некоторые люди различают эти два понятия, указывая, что тип определяет интерфейс, а класс — это конкретная реализация интерфейса.</ref>.

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

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

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

Но как заставить объект выполнять нужные вам действия? Должен существовать механизм передачи запроса к объекту на выполнение некоторого действия — завершения транзакции, рисования на экране и т. д. Каждый объект умеет выполнять только определенный круг запросов. Запросы, которые вы можете посылать объекту, определяются его интерфейсом, причем интерфейс объекта определяется его типом. Простейшим примером может стать электрическая лампочка:

 Light lt = new Light();
lt.on();

Интерфейс определяет, какие запросы вы вправе делать к определенному объекту. Однако где-то должен существовать и код, выполняющий запросы. Этот код, наряду со скрытыми данными, составляет реализацию. С точки зрения процедурного программирования происходящее не так уж сложно. Тип содержит метод для каждого возможного запроса, и при получении определенного запроса вызывается нужный метод. Процесс обычно объединяется в одно целое: и «отправка сообщения» (передача запроса) объекту, и его обработка объектом (выполнение кода).

В данном примере существует тип (класс) с именем Light (лампа), конкретный объект типа Light с именем It, и класс поддерживает различные запросы к объекту Light: выключить лампочку, включить, сделать ярче или притушить. Вы создаете объект Light, определяя «ссылку» на него (It) и вызывая оператор new для создания нового экземпляра этого типа. Чтобы послать сообщение объекту, следует указать имя объекта и связать его с нужным запросом знаком точки. С точки зрения пользователя заранее определенного класса, этого вполне достаточно для того, чтобы оперировать его объектами.

Диаграмма, показанная выше, следует формату UML (Unified Modeling Language). Каждый класс представлен прямоугольником, все описываемые поля данных помещены в средней его части, а методы (функции объекта, которому вы посылаете сообщения) перечисляются в нижней части прямоугольника.

Часто на диаграммах UML показываются только имя класса и открытые методы, а средняя часть отсутствует. Если же вас интересует только имя класса, то можете пропустить и нижнюю часть.

Объект предоставляет услуги

В тот момент, когда вы пытаетесь разработать или понять структуру программы, часто бывает полезно представить объекты в качестве «поставщиков услуг». Ваша программа оказывает услуги пользователю, и делает она это посредством услуг, предоставляемых другими объектами. Ваша цель — произвести (а еще лучше отыскать в библиотеках классов) тот набор объектов, который будет оптимальным для решения вашей задачи.

Для начала спросите себя: «если бы я мог по волшебству вынимать объекты из шляпы, какие бы из них смогли решить мою задачу прямо сейчас?» Предположим, что вы разрабатываете бухгалтерскую программу. Можно представить себе набор объектов, предоставляющих стандартные окна для ввода бухгалтерской информации, еще один набор объектов, выполняющих бухгалтерские расчеты, объект, ведающий распечаткой чеков и счетов на всевозможных принтерах. Возможно, некоторые из таких объектов уже существуют, а для других объектов стоит выяснить, как они могли бы выглядеть. Какие услуги могли бы предоставлять те объекты, и какие объекты понадобились бы им для выполнения своей работы? Если вы будете продолжать в том же духе, то рано или поздно скажете: «Этот объект достаточно прост, так что можно сесть и записать его», или «Наверняка такой объект уже существует». Это разумный способ распределить решение задачи на отдельные объекты.

Представление объекта в качестве поставщика услуг обладает дополнительным преимуществом: оно помогает улучшить связуемостъ (cohesiveness) объекта. Хорошая связуемостъ — важнейшее качество программного продукта: она озна­чает, что различные аспекты программного компонента (такого как объект, хотя сказанное также может относиться к методу или к библиотеке объектов) хорошо «стыкуются» друг с другом. Одной из типичных ошибок, допускаемых при проектировании объекта, является перенасыщение его большим количеством свойств и возможностей. Например, при разработке модуля, ведающего распечаткой чеков, вы можете захотеть, чтобы он «знал» все о форматировании и печати.

Если подумать, скорее всего, вы придете к выводу, что для одного объекта этого слишком много, и перейдете к трем или более объектам. Один объект будет представлять собой каталог всех возможных форм чеков, и его можно будет запросить о том, как следует распечатать чек. Другой объект или набор объектов станут отвечать за обобщенный интерфейс печати, «знающий» все о различных типах принтеров (но ничего не «понимающий» в бухгалтерии — такой объект лучше купить, чем разрабатывать самому). Наконец, третий объект просто будет пользоваться услугами описанных объектов, для того чтобы выполнить задачу. Таким образом, каждый объект представляет собой связанный набор предлагаемых им услуг. В хорошо спланированном объектно-ориентированном проекте каждый объект хорошо справляется с одной конкретной задачей, не пытаясь при этом сделать больше нужного. Как было показано, это не только позволяет определить, какие объекты стоит приобрести (объект с интерфейсом печати), но также дает возможность получить в итоге объект, который затем можно использовать где-то еще (каталог чеков).

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

Скрытая реализация

Программистов полезно разбить на создателей классов (те, кто создает новые типы данных) и программистов-клиентов<ref>Признателен за этот термин другу Скотту Мейерсу.</ref> (потребители классов, использующие типы данных в своих приложениях). Цель вторых — собрать как можно больше классов, чтобы заниматься быстрой разработкой программ. Цель создателя класса — построить класс, открывающий только то, что необходимо программисту-клиенту, и прячущий все остальное. Почему? Программист-клиент не сможет получить доступ к скрытым частям, а значит, создатель классов оставляет за собой возможность произвольно их изменять, не опасаясь, что это кому-то повредит. «Потаенная» часть обычно и самая «хрупкая» часть объекта, которую легко может испортить неосторожный или несведущий программист-клиент, поэтому сокрытие реализации сокращает количество ошибок в программах.

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

Таким образом, первой причиной для ограничения доступа является необходимость уберечь «хрупкие» детали от программиста-клиента — части внутренней «кухни», не являющиеся составляющими интерфейса, при помощи которого пользователи решают свои задачи. На самом деле это полезно и пользователям — они сразу увидят, что для них важно, а что они могут игнорировать.

Вторая причина появления ограничения доступа — стремление позволить разработчику библиотеки изменить внутренние механизмы класса, не беспокоясь о том, как это отразится на программисте-клиенте. Например, вы можете реализовать определенный класс «на скорую руку», чтобы ускорить разработку программы, а затем переписать его, чтобы повысить скорость работы. Если вы правильно разделили и защитили интерфейс и реализацию, сделать это будет совсем несложно.

Java использует три явных ключевых слова, характеризующих уровень доступа: public, private и protected. Их предназначение и употребление очень просты. Эти спецификаторы доступа определяют, кто имеет право использовать следующие за ними определения. Слово public означает, что последующие определения доступны всем. Наоборот, слово private значит, что следующие за ним предложения доступны только создателю типа, внутри его методов. Термин private — «крепостная стена» между вами и программистом-клиентом. Если кто-то попытается использовать private-члены, он будет остановлен ошибкой компиляции. Спецификатор protected действует схоже с private, за одним исключением — производные классы имеют доступ к членам, помеченным protected, но не имеют доступа к private-членам (наследование мы вскоре рассмотрим).

В Java также есть доступ «по умолчанию», используемый при отсутствии какого-либо из перечисленных спецификаторов. Он также иногда называется доступом в пределах пакета (package access), поскольку классы могут использовать дружественные члены других классов из своего пакета, но за его пределами те же дружественные члены приобретают статус private.

Повторное использование реализации

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

Проще всего использовать класс повторно, непосредственно создавая его объект, но вы можете также поместить объект этого класса внутрь нового класса. Мы называем это внедрением объекта (создание объекта-члена). Новый класс может содержать любое ко­личество объектов других типов, в любом сочетании, которое необходимо для достижения необходимой функциональности. Так как мы составляем новый класс из уже существующих классов, этот способ называется композицией (если композиция выполняется динамически, она обычно именуется агрегацией). Композицию часто называют отношением типа «имеет» (has-a), как, например, в предложении «У автомобиля есть двигатель».

(На UML-диаграммах композиция обозначается закрашенным ромбом, показывающим, например, что существует только один автомобиль. Я обычно использую более общую форму отношений: только линии, без ромба, что означает ассоциацию (связь).<ref>Этого обычно достаточно для большинства диаграмм, где для вас не существенна разница между композицией или агрегацией.</ref>)

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

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

Наследование

Сама по себе идея объекта крайне удобна. Объект позволяет совмещать данные и функциональность на концептуальном уровне, то есть вы можете представлять нужное понятие из пространства-задачи вместо того, чтобы его конкретизировать используя диалект машины. Эти концепции и образуют фундаментальные единицы языка программирования, описываемые с помощью ключевого слова class.

Но согласитесь, было бы обидно создавать какой-то класс, а потом проделывать всю работу заново для похожего класса. Гораздо рациональнее взять готовый класс, «клонировать» его, а затем внести добавления и обновления в полученный клон. Это именно то, что вы получаете в результате наследования, с одним исключением — если изначальный класс (называемый также базовым* классом, суперклассом или родительским классом) изменяется, то все изменения отражаются и на его «клоне» (называемом производным классом, унаследованным классом, подклассом или дочерним классом).

P0025.png

(Стрелка (пустой треугольник) на UML-диаграмме направлена от производного класса к базовому классу. Как вы вскоре увидите, может быть и больше одного производного класса.)

Тип определяет не только свойства группы объектов; он также связан с другими типами. Два типа могут иметь общие черты и поведение, но различаться количеством характеристик, а также способностью обработать большее число сообщений (или обработать их по-другому). Для выражения этой общности типов при наследовании используется понятие базовых и производных типов. Базовый тип содержит все характеристики и действия, общие для всех типов, производных от него. Вы создаете базовый тип, чтобы представить основу своего представления о каких-то объектах в вашей системе. От базового типа порождаются другие типы, выражающие другие реализации этой сущности.

Например, машина по переработке мусора сортирует отходы. Базовым типом будет «мусор», и каждая частица мусора имеет вес, стоимость и т. п., и может быть раздроблена, расплавлена или разложена. Отталкиваясь от этого, наследуются более определенные виды мусора, имеющие дополнительные характеристики (бутылка имеет цвет) или черты поведения (алюминиевую банку можно смять, стальная банка притягивается магнитом). Вдобавок, некоторые черты поведения могут различаться (стоимость бумаги зависит от ее типа и состояния). Наследование позволяет составить иерархию типов, описывающую решаемую задачу в контексте ее типов.

Второй пример — классический пример с геометрическими фигурами. Базовым типом здесь является «фигура», и каждая фигура имеет размер, цвет, расположение и т. п. Каждую фигуру можно нарисовать, стереть, переместить, закрасить и т. д. Далее производятся (наследуются) конкретные разновидности фигур: окружность, квадрат, треугольник и т. п., каждая из которых имеет свои дополнительные характеристики и черты поведения. Например, для некоторых фигур поддерживается операция зеркального отображения. Отдельные черты поведения могут различаться, как в случае вычисления площади фигуры. Иерархия типов воплощает как схожие, так и различные свойства фигур.

P0026.png

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

Используя наследование от существующего типа, вы создаете новый тип. Этот новый тип не только содержит все члены существующего типа (хотя члены, помеченные как private, скрыты и недоступны), но и, что еще важнее, повторяет интерфейс базового класса. Значит, все сообщения, которые вы могли посылать базовому классу, вы также вправе посылать и производному классу. А так как мы различаем типы классов по совокупности сообщений, которые можем им посылать, это означает, что производный класс является частным случаем базового класса. В предыдущем примере «окружность есть фигура». Эквивалентность типов, достигаемая при наследовании, является одним из основополагающих условий понимания смысла объектно-ориентированного программирования.

Так как и базовый, и производный классы имеют одинаковый основной интерфейс, должна существовать и реализация для этого интерфейса. Другими словами, где-то должен быть код, выполняемый при получении объектом определенного сообщения. Если вы просто унаследовали класс и больше не предпринимали никаких действий, методы из интерфейса базового класса перейдут в производный класс без изменений. Это значит, что объекты производного класса не только однотипны, но и обладают одинаковым поведением, а при этом само наследование теряет смысл.

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

P0027.png

Хотя наследование иногда наводит на мысль, что интерфейс будет дополнен новыми методами (особенно в Java, где наследование обозначается ключевым словом extends, то есть «расширять»), это совсем не обязательно. Второй, более важный способ модификации класса заключается в изменении поведения уже существующих методов базового класса. Это называется переопределением (или замещением) метода.

P0028.png

Для замещения метода нужно просто создать новое определение этого метода в производном классе. Вы как бы говорите: «Я использую тот же метод интерфейса, но хочу, чтобы он выполнял другие действия для моего нового типа».

Отношение «является» в сравнении с «похоже»

При использовании наследования встает очевидный вопрос: следует ли при наследовании переопределять методы только базового класса (и не добавлять новые методы, не существующие в базовом классе)? Это означало бы, что производный класс будет точно такого же типа, как и базовый класс, так как они имеют одинаковый интерфейс. В результате вы можете свободно заменять объекты базового класса объектами производных классов. Можно говорить о полной замене, и это часто называется принципом замещения. В определенном смысле это способ наследования идеален. Подобный способ взаимосвязи базового и производного классов часто называют связью «является тем-то», поскольку можно сказать «круг есть фигура». Чтобы определить, насколько уместным будет наследование, достаточно проверить, существует ли отношение «является» между классами и насколько оно оправданно.

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

Предположим, что ваш дом снабжен всем необходимым оборудованием для контроля процесса охлаждения. Представим теперь, что кондиционер сломался и вы заменили его обогревателем, способным как нагревать, так и охлаждать. Обогреватель «похож на» кондиционер, но он способен и на большее. Так как система управления вашего дома способна контролировать только охлаждение, она ограничена в коммуникациях с охлаждающей частью нового объекта. Интерфейс нового объекта был расширен, а существующая система ничего не признает, кроме оригинального интерфейса.

P0029.png

Конечно, при виде этой иерархии становится ясно, что базовый класс «охлаждающая система» недостаточно гибок; его следует переименовать в «систему контроля температуры» так, чтобы он включал и нагрев, — и после этого заработает принцип замещения. Тем не менее эта диаграмма представляет пример того, что может произойти в реальности.

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

Взаимозаменяемые объекты и полиморфизм

При использовании иерархий типов часто приходится обращаться с объектом определенного типа как с базовым типом. Это позволяет писать код, не зависящий от конкретных типов. Так, в примере с фигурами методы манипулируют просто фигурами, не обращая внимания на то, являются ли они окружностями, прямоугольниками, треугольниками или некоторыми еще даже не определенными фигурами. Все фигуры могут быть нарисованы, стерты и перемещены, а методы просто посылают сообщения объекту «фигура»; им безразлично, как объект обойдется с этим сообщением.

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

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

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

В следующем примере объект BirdController (управление птицей) может работать только с обобщенными объектами Bird (птица), не зная точного их типа. С точки зрения BirdController это удобно, поскольку для него не придется писать специальный код проверки типа используемого объекта Bird, для обработки какого-то особого поведения. Как же все-таки происходит, что при вызове метода move(), без указания точного типа Bird, исполняется верное действие — объект Goose (гусь) бежит, летит или плывет, а объект Penguin (пингвин) бежит или плывет?

P0030.png

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

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

Для осуществления позднего связывания, Java вместо абсолютного вызова использует специальные фрагменты кода. Этот код вычисляет адрес тела метода на основе информации, хранящейся в объекте (процесс очень подробно описан в главе 7, посвящённой полиморфизму). Таким образом, каждый объект может вести себя различно, в зависимости от содержимого этого специального фрагмента кода. Когда вы посылаете сообщение, объект фактически сам решает, что же с ним делать.

В некоторых языках необходимо явно указать, что для метода должен использоваться гибкий механизм позднего связывания (в C++ для этого предусмотрено ключевое слово virtual). В этих языках методы по умолчанию компонуются не динамически. В Java позднее связывание производится по умолчанию, и вам не нужно помнить о необходимости добавления каких-либо ключевых слов для обеспечения полиморфизма.

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

Допустим, вы написали на Java следующий метод (вскоре вы узнаете, как это делать):

 void doSomething(Shape shape) {
shape.erase(); // стереть
//...
shape.draw(); // нарисовать
}

Метод работает с обобщенной фигурой (Shape), то есть не зависит от конкретного типа объекта, который рисуется или стирается. Теперь мы используем вызов метода doSomething() в другой части программы:

 Circle circle = new Circle();       // окружность 
Triangle triangle = new Triangle(); // треугольник
Line line = new Line(); // линия
doSomething(circle);
doSomething(triangle);
doSomething(line);

Вызовы метода doSomething() автоматически работают правильно, вне зависимости от фактического типа объекта.

На самом деле это довольно важный факт. Рассмотрим строку:

 doSomething(circle);

Здесь происходит следующее: методу, ожидающему объект Shape, передается объект «окружность» (Circle). Так как окружность (Circle) одновременно является фигурой (Shape), то метод doSomething() и обращается с ней, как с фигурой. Другими словами, любое сообщение, которое метод может послать Shape, также принимается и Circle. Это действие совершенно безопасно и настолько же логично.

Мы называем этот процесс обращения с производным типом как с базовым восходящим преобразованием типов. Слово преобразование означает, что объект трактуется как принадлежащий к другому типу, а восходящее оно потому, что на диаграммах наследования, базовые классы обычно располагаются вверху, а производные классы располагаются внизу «веером». Значит, преобразование к базовому типу — это движение по диаграмме вверх, и поэтому оно «восходящее».

P0032.png

Объектно-ориентированная программа почти всегда содержит восходящее преобразование, потому что именно так вы избавляетесь от необходимости знать точный тип объекта, с которым работаете. Посмотрите на тело метода doSomething():

 shape.erase();
// ...
shape.draw();

Заметьте, что здесь не сказано «если ты объект Circle, делай это, а если ты объект Square, делай то-то и то-то». Такой код с отдельными действиями для каждого возможного типа Shape будет путаным, и его придется менять каждый раз при добавлении нового подтипа Shape. А так, вы просто говорите: «Ты фигура, и я знаю, что ты способна нарисовать и стереть себя, ну так и делай это, а о деталях позаботься сама».

В коде метода doSomething() интересно то, что все само собой получается правильно. При вызове draw() для объекта Circle исполняется другой код, а не тот, что отрабатывает при вызове draw() для объектов Square или Line, а когда draw() применяется для неизвестной фигуры Shape, правильное поведение обеспечивается использованием реального типа Shape. Это в высшей степени интересно, потому что, как было замечено чуть ранее, когда компилятор генерирует код doSomething(), он не знает точно, с какими типами он работает. Соответственно, можно было бы ожидать вызова версий методов draw() и erase() из базового класса Shape, а не их вариантов из конкретных классов Circle, Square или Line. И тем не менее все работает правильно благодаря полиморфизму. Компилятор и система исполнения берут на себя все подробности; все, что вам нужно знать, — что это произойдёт... и, что еще важнее, как создавать программы, используя такой подход. Когда вы посылаете сообщение объекту, объект выберет правильный вариант поведения используя восходящее преобразование.

Однокорневая иерархия

Вскоре после появления, C++ стал активно обсуждаться вопрос из ООП — должны ли все классы обязательно быть унаследованы от единого базового класса? В Java (как практически во всех других ООП-языках, кроме C++) на этот вопрос был дан положительный ответ. В основе всей иерархии типов лежит единый базовый класс Object. Оказалось, что однокорневая иерархия имеет множество преимуществ.

Все объекты в однокорневой иерархии имеют некий общий интерфейс, так что по большому счету все они могут рассматриваться как один основополагающий тип. В C++ был выбран другой вариант — общего предка в этом языке не существует. С точки зрения совместимости со старым кодом эта модель лучше соответствует традициям C, и можно подумать, что она менее ограничена. Но как только возникнет необходимость в полноценном объектно-ориентированном программировании, вам придется создавать собственную иерархию классов, чтобы получить те же преимущества, что встроены в другие ООП-языки. Да и в любой новой библиотеке классов вам может встретиться какой-нибудь несовместимый интерфейс. Включение этих новых интерфейсов в архитектуру вашей программы потребует лишних усилий (и возможно, множественного наследования). Стоит ли дополнительная «гибкость» C++ подобных издержек? Если вам это нужно (например, при больших вложениях в разработку кода C), то в проигрыше вы не останетесь. Если же разработка начинается «с нуля», подход Java выглядит более продуктивным.

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

Однокорневая иерархия позволяет гораздо проще реализовать уборку мусора — одно из важнейших усовершенствований Java по сравнению с C++. Так как информация о типе во время исполнения гарантированно присутствует в любом из объектов, в системе никогда не появится объект, тип которого не удастся определить. Это особенно важно при выполнении системных операций, таких как обработка исключений, и для обеспечения большей гибкости программирования.

Контейнеры

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

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

К счастью, хороший ООП-язык поставляется с набором готовых контейнеров. В C++ это часть стандартной библиотеки C++, иногда называемая библиотекой стандартных шаблонов (Standard Template Library, STL). Smalltalk поставляется с очень широким набором контейнеров. Java также содержит контейнеры в своей стандартной библиотеке. Для некоторых библиотек считается, что достаточно иметь один единый контейнер для всех нужд, но в других (например, в Java) предусмотрены различные контейнеры на все случаи жизни: несколько различных типов списков List (для хранения последовательностей элементов), карты Map (известные также как ассоциативные массивы, позволяют связывать объекты с другими объектами), а также множества Set (обеспечивающие уникальность значений для каждого типа). Контейнерные библиотеки также могут содержать очереди, деревья, стеки и т. п.

С позиций проектирования, все, что вам действительно необходимо, — это контейнер, способный решить вашу задачу. Если один вид контейнера отвечает всем потребностям, нет основания использовать другие виды. Существует две причины, по которым вам приходится выбирать из имеющихся контейнеров. Во-первых, контейнеры предоставляют различные интерфейсы и возможности взаимодействия. Поведение и интерфейс стека отличаются от поведения и интерфейса очереди, которая ведет себя по-иному, чем множество или список. Один из этих контейнеров способен обеспечить более эффективное решение вашей задачи в сравнении с остальными. Во-вторых, разные контейнеры по-разному выполняют одинаковые операции. Лучший пример — это ArrayList и LinkedList. Оба представляют собой простые последовательности, которые могут иметь идентичные интерфейсы и черты поведения. Но некоторые операции значительно отличаются по времени исполнения. Скажем, время выборки произвольного элемента в ArrayList всегда остается неизменным вне зависимости от того, какой именно элемент выбирается. Однако в LinkedList невыгодно работать с произвольным доступом — чем дальше по списку находится элемент, тем большую задержку вызывает его поиск. С другой стороны, если потребуется вставить элемент в середину списка, LinkedList сделает это быстрее чем ArrayList. Эти и другие операции имеют разную эффективность, зависящую от внутренней структуры контейнера. На стадии планирования программы вы можете выбрать список LinkedList, а потом, в процессе оптимизации, переключиться на ArrayList. Благодаря абстрактному характеру интерфейса List такой переход потребует минимальных изменений в коде.

Параметризованные типы (generics)

До выхода Java SE5 в контейнерах могли храниться только данные Object — единственного универсального типа Java. Однокорневая иерархия означает, что любой объект может рассматриваться как Object, поэтому контейнер с элементами Object подойдет для хранения любых объектов<ref>Примитивные типы храниться в контейнерах не могут, но благодаря механизму автоматической упаковки (autoboxing) Java SE5 это ограничение несущественно. Далее в книге эта тема будет рассмотрена более подробно.</ref>.

При работе с таким контейнером вы просто помещаете в него ссылки на объекты, а позднее извлекаете их. Но если контейнер способен хранить только Object, то при помещении в него ссылки на объект другого типа происходит преоб­разование к Object, то есть утрата «индивидуальности» объекта. При выборке его обратно вы получаете ссылку на Object, а не ссылку на тип, который был помещен в контейнер. Как же преобразовать ее к конкретному типу объекта, помещенного в контейнер?

Задача решается тем же преобразованием типов, но на этот раз вы не используете восходящее преобразование (вверх по иерархии наследования к базовому типу). Теперь вы используете способ преобразования вниз по иерархии наследования (к дочернему типу). Данный способ называется нисходящим преобразованием. В случае восходящего преобразования известно, что окружность есть фигура, поэтому преобразование заведомо безопасно, но при обратном преобразовании (к дочернему типу), невозможно заранее сказать, представляет ли экземпляр Object объект Circle или Shape, поэтому нисходящее преобразование безопасно только в том случае, если вам точно известен тип объекта.

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

Нисходящее преобразование и проверки типа во время исполнения требуют дополнительного времени и лишних усилий от программиста. А может быть, можно каким-то образом создать контейнер, знающий тип хранимых объектов, и таким образом устраняющий необходимость преобразования типов и потенциальные ошибки? Решение называют механизмом параметризации типа. Параметризованные типы представляют собой классы, которые компилятор может автоматически адаптировать для работы с определенными типами. Например, компилятор может настроить параметризованный контейнер на хранение и извлечение только фигур (Shape).

Одним из важнейших изменений Java SE5 является поддержка параметризованных типов (generics). Параметризованные типы легко узнать по угловым скобкам, в которые заключаются имена типов-параметров; например, контейнер ArrayList, предназначенный для хранения объектов Shape, создается следующим образом:

 ArrayList<Shape> shapes = new ArrayList<Shape>();

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

Создание, использование объектов и время их жизни

Один из важнейших аспектов работы с объектами — организация их создания и уничтожения. Для существования каждого объекта требуются некоторые ресурсы, прежде всего память. Когда объект становится не нужен, он должен быть уничтожен, чтобы занимаемые им ресурсы стали доступны другим. В простых ситуациях задача не кажется сложной: вы создаете объект, используете его, пока требуется, а затем уничтожаете. Однако на практике часто встречаются и более сложные ситуации.

Допустим, например, что вы разрабатываете систему для управления движением авиатранспорта. (Эта же модель пригодна и для управления движением тары на складе, или для системы видеопроката, или в питомнике для бродячих животных.) Сначала все кажется просто: создается контейнер для самолетов, затем строится новый самолет, который помещается в контейнер определенной зоны регулировки воздушного движения. Что касается освобождения ресурсов, соответствующий объект просто уничтожается при выходе самолета из зоны слежения.

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

Теперь задача усложняется: как узнать, когда нужно удалять объекты? Даже если вы закончили работу с объектом, возможно, с ним продолжает взаимодействовать другая система. Этот же вопрос возникает и в ряде других ситуаций, и в программных системах, где необходимо явно удалять объекты после завершения работы с ними (например, в C++), он становится достаточно сложным.

Где хранятся данные объекта и как определяется время его жизни? В C++ на первое место ставится эффективность, поэтому программисту предоставляется выбор. Для достижения максимальной скорости исполнения место хранения и время жизни могут определяться во время написания программы. В этом случае объекты помещаются в стек (такие переменные называются автоматическими) или в область статического хранилища. Таким образом, основным фактором является скорость создания и уничтожения объектов, и это может быть неоценимо в некоторых ситуациях. Однако при этом приходится жертвовать гибкостью, так как количество объектов, время их жизни и типы должны быть точно известны на стадии разработки программы. При решении задач более широкого профиля — разработки систем автоматизированного проектирования
(CAD), складского учета или управления воздушным движением — этот подход может оказаться чересчур ограниченным.

Второй путь — динамическое создание объектов в области памяти, называемой «кучей» (heap). В таком случае количество объектов, их точные типы и время жизни остаются неизвестными до момента запуска программы. Все это определяется «на ходу» во время работы программы. Если вам понадобится новый объект, вы просто создаете его в «куче» тогда, когда потребуется. Так как управление кучей осуществляется динамически, во время исполнения программы на выделение памяти из кучи требуется гораздо больше времени, чем при выделении памяти в стеке. (Для выделения памяти в стеке достаточно всего одной машинной инструкции, сдвигающей указатель стека вниз, а освобождение осуществляется перемещением этого указателя вверх. Время, требуемое на выделение памяти в куче, зависит от структуры хранилища.)

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

В Java используется исключительно второй подход<ref>Примитивные типы, о которых речь пойдёт далее, являются особым случаем.</ref>. Каждый раз при создании объекта используется ключевое слово new для построения динамического экземпляра.

Впрочем, есть и другой фактор, а именно время жизни объекта. В языках, поддерживающих создание объектов в стеке, компилятор определяет, как долго используется объект, и может автоматически уничтожить его. Однако при создании объекта в куче компилятор не имеет представления о сроках жизни объекта. В языках, подобных C++, уничтожение объекта должно быть явно оформлено в программе; если этого не сделать, возникает утечка памяти (обычная проблема в программах C++). В Java существует механизм, называемый сборкой мусора; он автоматически определяет, когда объект перестает использоваться, и уничтожает его. Сборщик мусора очень удобен, потому что он избавляет программиста от лишних хлопот. Что еще важнее, сборщик мусора дает гораздо большую уверенность в том, что в вашу программу не закралась коварная проблема утечки памяти (которая «поставила на колени» не один проект на языке C++).

В Java сборщик мусора спроектирован так, чтобы он мог самостоятельно решать проблему освобождения памяти (это не касается других аспектов завершения жизни объекта). Сборщик мусора «знает», когда объект перестает ис­пользоваться, и применяет свои знания для автоматического освобождения памяти. Благодаря этому факту (вместе с тем, что все объекты наследуются от единого базового класса Object и создаются только в куче) программирование на Java гораздо проще, чем программирование на C++. Разработчику приходится принимать меньше решений и преодолевать меньше препятствий.

Обработка исключений: борьба с ошибками

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

Механизм обработки исключений встраивает обработку ошибок прямо в язык программирования или даже в операционную систему. Исключение представляет собой объект, генерируемый на месте возникновении ошибки, который затем может быть «перехвачен» подходящим обработчиком исключений, предназначенным для ошибок определенного типа. Обработка исключений словно определяет параллельный путь выполнения программы, вступающий в силу, когда что-то идет не по плану. И так как она определяет отдельный путь исполнения, код обработки ошибок не смешивается с обычным кодом. Это упрощает написание программ, поскольку вам не приходится постоянно проверять возможные ошибки. Вдобавок исключение не похоже на числовой код ошибки, возвращаемый методом, или на флаг, устанавливаемый в случае проблемной ситуации, — последние могут быть проигнорированы. Исключение не может быть проигнорировано, оно гарантировано будет где-то обработано. Наконец, исключения дают возможность восстановить нормальную работу программы после неверной операции. Вместо того, чтобы просто завершить программу, можно исправить ситуацию и продолжить ее выполнение; тем самым повышается надежность программы.

Механизм обработки исключений в Java выделяется среди остальных языков, потому что он был встроен в язык с самого начала, и разработчик обязан его использовать. Это единственно-приемлемый способ сообщения об ошибках. Если вы не напишете кода для подобающей обработки исключений, вы получите сообщение об ошибке во время компиляции. Подобный последовательный подход иногда заметно упрощает обработку ошибок.

Стоит отметить, что обработка исключений не является особенностью объектно-ориентированного языка, хотя в этих языках исключение обычно представлено объектом. Такой механизм существовал и до возникновения объектно-ориентированных языков.

Параллельное программирование

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

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

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

Все это создает впечатление, что потоки использовать очень легко. Но тут кроется подвох: совместно используемые ресурсы. Если несколько потоков пытаются одновременно получить доступ к одному ресурсу, возникают проблемы. Например, два процесса не могут одновременно посылать информацию на принтер. Для предотвращения конфликта совместные ресурсы (такие как принтер) должны блокироваться во время использования. Поток блокирует ресурс, завершает свою операцию, а затем снимает блокировку для того, чтобы кто-то еще смог получить доступ к ресурсу.

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

Java и Интернет

Если Java представляет собой очередной язык программирования, возникает вопрос: чем же он так важен и почему он преподносится как революционный шаг в разработке программ? С точки зрения традиционных задач программирования ответ очевиден не сразу. Хотя язык Java пригодится и при построении автономных приложений, самым важным его применением было и остается программирование для сети World Wide Web.

Что такое Веб?

На первый взгляд Веб выглядит довольно загадочно из-за обилия новомодных терминов вроде «серфинга», «присутствия» и «домашних страниц». Чтобы понять, что же это такое, полезно представить себе картину в целом — но сначала необходимо разобраться во взаимодействии клиент/серверных систем, которые представляют собой одну из самых сложных задач компьютерных вычислений.

Вычисления «клиент/сервер»

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

Таким образом, основная концепция клиент/серверных вычислений не так уж сложна. Проблемы возникают из-за того, что один сервер пытается обслуживать многих клиентов одновременно. Обычно для решения привлекается система управления базой данных, и разработчик пытается «оптимизировать» структуру данных, распределяя их по таблицам. Дополнительно система часто дает возможность клиенту добавлять новую информацию на сервер. А это значит, что новая информация клиента должна быть защищена от потери во время сохранения в базе данных, а также от возможности ее перезаписи данными другого клиента. (Это называется обработкой транзакций.) При изменении клиентского программного обеспечения необходимо не только скомпилировать и протестировать его, но и установить на клиентских машинах, что может обойтись гораздо сложнее и дороже, чем можно представить. Особенно сложно организовать поддержку множества различных операционных систем и компьютерных архитектур. Наконец, необходимо учитывать важнейший фактор производительности: к серверу одновременно могут поступать сотни запросов, и малейшая задержка грозит серьезными последствиями. Для уменьшения задержки программисты стараются распределить вычисления, зачастую даже проводя их на клиентской машине, а иногда и переводя на дополнительные серверные машины, используя так называемое связующее программное обеспечение (middleware). (Программы-посредники также упрощают сопровождение программ.)

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

Веб как гигантский сервер

Фактически веб представляет собой одну огромную систему «клиент/сервер». Впрочем, это еще не все: в единой сети одновременно сосуществуют все серверы и клиенты. Впрочем, этот факт вас не должен интересовать, поскольку обычно вы соединяетесь и взаимодействуете только с одним сервером (даже если его приходится разыскивать по всему миру).

На первых порах использовался простой однонаправленный обмен информацией. Вы делали запрос к серверу, он отсылал вам файл, который обрабатывала для вас ваша программа просмотра (то есть клиент). Но вскоре простого получения статических страниц с сервера стало недостаточно. Пользователи хотели использовать все возможности системы «клиент/сервер», отсылать информацию от клиента к серверу, чтобы, например, просматривать базу данных сервера, добавлять новую информацию на сервер или делать заказы (что требовало особых мер безопасности). Эти изменения мы постоянно наблюдаем в процессе развития веб.

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

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

Программирование на стороне клиента

Изначально система взаимодействия «сервер-браузер» разрабатывалась для интерактивного содержимого, но поддержка этой интерактивности была полностью возложена на сервер. Сервер генерировал статические страницы для браузера клиента, который их просто обрабатывал и показывал. Стандарт HTML поддерживает простейшие средства ввода данных: текстовые поля, переключатели, флажки, списки и раскрывающиеся списки, вместе с кнопками, которые могут выполнить только два действия: сброс данных формы и ее отправку серверу. Отправленная информация обрабатывается интерфейсом CGI (Common Gateway Interface), поддерживаемым всеми веб-серверами. Текст запроса указывает CGI, как именно следует поступить с данными. Чаще всего по запросу запускается программа из каталога cgi-bin на сервере. (В строке с адресом страницы в браузере, после отправки данных формы, иногда можно разглядеть в мешанине символов подстроку cgi-bin.) Такие программы можно написать почти на всех языках. Обычно используется Perl, так как он ориентирован на обработку текста, а также является интерпретируемым языком, соответственно, может быть использован на любом сервере, независимо от типа процессора или операционной системы. Впрочем, язык Python (мой любимый язык — зайдите на www.Python.org) постепенно отвоевывает у него «территорию» благодаря своей мощи и простоте.

Многие мощные веб-серверы сегодня функционируют целиком на основе CGI; в принципе, эта технология позволяет решать почти любые задачи. Однако веб-серверы, построенные на CGI-программах, тяжело обслуживать, и на них существуют проблемы со скоростью отклика. Время отклика CGI-программы зависит от количества посылаемой информации, а также от загрузки сервера и сети. (Из-за всего упомянутого запуск CGI-программы может занять продолжительное время). Первые проектировщики веб не предвидели, как быстро истощатся ресурсы системы при ее использовании в различных приложениях. Например, выводить графики в реальном времени в ней почти невозможно, так как при любом изменении ситуации необходимо построить новый GIF- файл и передать его клиенту. Без сомнения, у вас есть собственный горький опыт — например, полученный при простой посылке данных формы. Вы нажимаете кнопку для отправки информации; сервер запускает CGI-программу, которая обнаруживает ошибку, формирует HTML-страницу, сообщающую вам об этом, а затем отсылает эту страницу в вашу сторону; вам приходится набирать данные заново и повторять попытку. Это не только медленно, это попросту неэлегантно.

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

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

Модули расширения

Одним из самых важнейших направлений в клиентском программировании стала разработка модулей расширения (plug-ins). Этот подход позволяет программисту добавить к браузеру новые функции, загрузив небольшую программу, которая встраивается в браузер. Фактически с этого момента браузер обзаводится новой функциональностью. (Модуль расширения загружается только один раз.) Подключаемые модули позволили оснастить браузеры рядом быстрых и мощных нововведений, но написание такого модуля — совсем непростая задача, и вряд ли каждый раз при создании какого-то нового сайта вы захотите создавать расширения. Ценность модулей расширения для клиентского программирования состоит в том, что они позволяют опытному программисту дополнить браузер новыми возможностями, не спрашивая разрешения у его создателя. Таким образом, модули расширения предоставляют «черный ход» для интеграции новых языков программирования на стороне клиента (хотя и не все языки реализованы в таких модулях).

Языки сценариев

Разработка модулей расширения привела к появлению множества языков для написания сценариев. Используя язык сценария, вы встраиваете клиентскую программу прямо в HTML-страницу, а модуль, обрабатывающий данный язык, автоматически активизируется при ее просмотре. Языки сценария обычно довольно просты для изучения; в сущности, сценарный код представляет собой текст, входящий в состав HTML-страницы, поэтому он загружается очень быстро, как часть одного запроса к серверу во время получения страницы. Расплачиваться за это приходится тем, что любой в силах просмотреть (и украсть) ваш код. Впрочем, вряд ли вы будете писать что-либо заслуживающее подражания и утонченное на языках сценариев, поэтому проблема копирования кода не так уж страшна.

Языком сценариев, который поддерживается практически любым браузером без установки дополнительных модулей, является JavaScript (имеющий весьма мало общего с Java; имя было использовано в целях «урвать» кусочек успеха Java на рынке). К сожалению, исходные реализации JavaScript в разных браузерах довольно сильно отличались друг от друга и даже между разными версиями одного браузера. Стандартизация JavaScript в форме ECMAScript была полезна, но потребовалось время, чтобы ее поддержка появилась во всех браузерах (вдобавок компания Microsoft активно продвигала собственный язык VBScript, отдаленно напоминавший JavaScript). В общем случае разработчику приходится ограничиваться минимумом возможностей JavaScript, чтобы код гарантированно работал во всех браузерах. Что касается обработки ошибок и отладки кода JavaScript, то занятие это в лучшем случае непростое. Лишь недавно разработчикам удалось создать действительно сложную систему, написанную на JavaScript (компания Google, служба GMail), и это потребовало высочайшего энтузиазма и опыта.

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

Java

Если языки сценариев берут на себя 80 % задач клиентского программирования, кому же тогда «по зубам» остальные 20 %? Для них наиболее популярным решением сегодня является Java. Это не только мощный язык программирования, разработанный с учетом вопросов безопасности, платформенной совместимости и интернационализации, но также постоянно совершенствуемый инструмент, дополняемый новыми возможностями и библиотеками, которые элегантно вписываются в решение традиционно сложных задач программирования: многозадачности, доступа к базам данных, сетевого программирования и распределенных вычислений. Клиентское программирование на Java сводится к разработке апплетов, а также к использованию пакета Java Web Start.

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

Альтернативы

Честно говоря, апплеты Java не оправдали начальных восторгов. При первом появлении Java все относились к апплетам с большим энтузиазмом, потому что они делали возможным серьезное программирование на стороне клиента, повышали скорость отклика и снижали загрузку канала для Интернет-приложений. Апплетам предрекали большое будущее.

И действительно, в веб можно встретить ряд очень интересных апплетов. И все же массовый переход на апплеты так и не состоялся. Вероятно, главная проблема заключалась в том, что загрузка 10-мегабайтного пакета для установки среды Java Runtime Environment (JRE) слишком пугала рядового пользователя. Тот факт, что компания Microsoft не стала включать JRE в поставку Internet Explorer, окончательно решил судьбу апплетов. Как бы то ни было, апплеты Java так и не получили широкого применения.

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

.NET и С#

Некоторое время основным соперником Java-апплетов считались компоненты ActiveX от компании Microsoft, хотя они и требовали для своей работы наличия на машине клиента Windows. Теперь Microsoft противопоставила Java полноценных конкурентов: это платформа .NET и язык программирования С#. Платформа .NET представляет собой примерно то же самое, что и виртуальная машина Java (JVM) и библиотеки Java, а язык С# имеет явное сходство с языком Java. Вне всяких сомнений, это лучшее, что создала компания Microsoft в области языков и сред программирования. Конечно, разработчики из Microsoft имели некоторое преимущество; они видели, что в Java удалось, а что нет, и могли отталкиваться от этих фактов, но результат получился вполне достойным. Впервые с момента своего рождения у Java появился реальный соперник. Разработчикам из Sun пришлось как следует взглянуть на С#, выяснить, по каким причинам программисты могут захотеть перейти на этот язык, и приложить максимум усилий для серьезного улучшения Java в Java SE5.

В данный момент основные сомнения вызывает вопрос о том, разрешит ли Microsoft полностью переносить .NET на другие платформы. В Microsoft утверждают, что никакой проблемы в этом нет, и проект Mono (www.go-mono.com) предоставляет частичную реализацию .NET для Linux. Впрочем, раз реализация эта неполная, то, пока Microsoft не решит выкинуть из нее какую-либо часть, делать ставку на .NET как на межплатформенную технологию еще рано.

Интернет и интрасеть

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

Проблема безопасности подводит нас к одному из направлений, которое автоматически возникает в клиентском программировании. Если ваша программа исполняется в Интернете, то вы не знаете, на какой платформе ей предстоит ра­ботать. Приходится проявлять особую осторожность, чтобы избежать распространения некорректного кода. Здесь нужны межплатформенные и безопасные решения, наподобие Java или языка сценариев.

В интрасетях действуют другие ограничения. Довольно часто все машины сети работают на платформе Intel/Windows. В интрасети вы отвечаете за качество своего кода и можете устранять ошибки по мере их обнаружения. Вдобавок, у вас уже может накопиться коллекция решений, которые проверены на прочность в более традиционных клиент/серверных системах, в то время как новые программы придется вручную устанавливать на машину клиента при каждом обновлении. Время, затрачиваемое на обновления, является самым веским доводом в пользу браузерных технологий, где обновления осуществляются невидимо и автоматически (то же позволяет сделать Java Web Start). Если вы участвуете в обслуживании интрасети, благоразумнее всего использовать тот путь, который позволит привлечь уже имеющиеся наработки, не переписывая программы на новых языках.

Сталкиваясь с объемом задач клиентского программирования, способным поставить в тупик любого проектировщика, лучше всего оценить их с позиций соотношения «затраты/прибыли». Рассмотрите ограничения вашей задачи и попробуйте представить кратчайший способ ее решения. Так как клиентское программирование все же остается программированием, всегда актуальны технологии разработки, обещающие наиболее быстрое решение. Такая активная позиция даст вам возможность подготовиться к неизбежным проблемам разработки программ.

Программирование на стороне сервера

Наше обсуждение обошло стороной тему серверного программирования, которое, как считают многие, является самой сильной стороной Java. Что происходит, когда вы посылаете запрос серверу? Чаще всего запрос сводится к простому требованию «отправьте мне этот файл». Браузер затем обрабатывает файл подходящим образом: как HTML-страницу, как изображение, как Java-апплет, как сценарий и т. п.

Более сложный запрос к серверу обычно связан с обращением к базе данных. В самом распространном случае делается запрос на сложный поиск в базе данных, результаты которого сервер затем преобразует в HTML-страницу и посылает вам. (Конечно, если клиент способен производить какие-то действия с помощью Java или языка сценариев, данные могут быть обработаны и у него, что будет быстрее и снизит загрузку сервера.) А может быть, вам понадобится зарегистрироваться в базе данных при присоединении к какой-то группе, или оформить заказ, что потребует изменений в базе данных. Подобные запросы должны обрабатываться неким кодом на сервере; в целом это и называется серверным программированием. Традиционно программирование на сервере осуществлялось на Perl, Python, C++ или другом языке, позволяющем создавать программы CGI, но появляются и более интересные варианты. К их числу относятся и основанные на Java веб-серверы, позволяющие заниматься серверным программированием на Java с помощью так называемых сервлетов. Сервлеты и их детища, JSPs, составляют две основные причины для перехода компаний по разработке веб-содержимого на Java, в главном из-за того, что они решают проблемы несовместимости различных браузеров.

Несмотря на все разговоры о Java как языке Интернет-программирования, Java в действительности является полноценным языком программирования, способным решать практически все задачи, решаемые на других языках. Преимущества Java не ограничиваются хорошей переносимостью: это и пригодность к решению задач программирования, и устойчивость к ошибкам, и большая стандартная библиотека, и многочисленные разработки сторонних фирм — как существующие, так и постоянно появляющиеся.

Резюме

Вы знаете, как выглядит процедурная программа: определения данных и вызовы функций. Чтобы выяснить предназначение такой программы, необходимо приложить усилие, просматривая функции и создавая в уме общую картину. Именно из-за этого создание таких программ требует использования промежуточных средств — сами по себе они больше ориентированы на компьютер, а не на решаемую задачу.

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

Объектно-ориентированное программирование и язык Java подходят не для всех. Очень важно сначала выяснить свои потребности, чтобы решить, удовлетворит ли вас переход на Java или лучше остановить свой выбор на другой системе программирования (в том числе и на той, что вы сейчас используете). Если вы знаете, что в обозримом будущем вы столкнетесь с весьма специфическими потребностями или в вашей работе будут действовать ограничения, с которыми Java не справляется, лучше рассмотреть другие возможности (в особенности я рекомендую присмотреться к языку Python, https://www.Python.org). Выбирая Java, необходимо понимать, какие еще доступны варианты и почему вы выбрали именно этот путь.



https://linexp.ru/javabooks/Thinking_in_Java_4th_edition

]]>
Книги по Java https://linexp.ru?id=4751 Wed, 29 Jun 2022 14:31:55 GMT
<![CDATA[Глава 2 Thinking in Java 4th edition]]>

ВСЁ ЯВЛЯЕТСЯ ОБЪЕКТОМ

Хотя язык Java основан на C++, он является более «чистокровным» объектно-ориентированным языком. Как C++, так и Java относятся к семейству смешанных языков, но для создателей Java эта неоднородность была не так важна, если сравнивать с C++. Смешанный язык позволяет использовать несколько стилей программирования; причиной смешанной природы C++ стало желание сохранить совместимость с языком C. Так как язык C++ является надстройкой языка C, он включает в себя много нежелательных характеристик своего предшественника, что приводит к излишнему усложнению некоторых аспектов этого языка. Язык программирования Java подразумевает, что вы занимаетесь только объектно-ориентированным программированием. А это значит, что прежде, чем начать с ним работать, нужно «переключиться» на понятия объектно-ориентированного мира (если вы уже этого не сделали). Выгода от этого начального усилия — возможность программировать на языке, который по простоте изучения и использования превосходит все остальные языки ООП. В этой главе мы рассмотрим основные компоненты Java-программы и узнаем, что в Java (почти) все является объектом.

Для работы с объектами используются ссылки

Каждый язык программирования имеет свои средства манипуляции данными. Иногда программисту приходится быть постоянно в курсе, какая именно манипуляция производится в программе. Вы работаете с самим объектом или же с каким-то видом его косвенного представления (указатель в C или в C++), требующим особого синтаксиса? Все эти различия упрощены в Java. Вы обращаетесь со всем как с объектом, и поэтому повсюду используется единый последовательный синтаксис. Хотя вы обращаетесь со всем как с объектом, идентификатор, которым вы манипулируете, на самом деле представляет собой ссылку на объект. Представьте себе телевизор (объект) с пультом дистанционного управления (ссылка). Во время владения этой ссылкой у вас имеется связь с телевизором, но при переключении канала или уменьшении громкости вы распоряжаетесь ссылкой, которая, в свою очередь, манипулирует объектом. А если вам захочется перейти в другое место комнаты, все еще управляя телевизором, вы берете с собой «ссылку», а не сам телевизор. Также пульт может существовать сам по себе, без телевизора. Таким образом, сам факт наличия ссылки еще не означает наличия присоединенного к ней объекта. Например, для хранения слова или предложения создается ссылка String:

 String s;

Однако здесь определяется только ссылка, но не объект. Если вы решите послать сообщение s, произойдет ошибка, потому что ссылка s на самом деле ни к чему не присоединена (телевизора нет). Значит, безопаснее всегда инициализировать ссылку при ее создании:

 String s = "asdf";

В данном примере используется специальная возможность Java: инициализация строк текстом в кавычках. Обычно вы будете использовать более общий способ инициализации объектов.

Все объекты должны создаваться явно

Когда вы определяете ссылку, желательно присоединить ее к новому объекту. В основном это делается при помощи ключевого слова new. Фактически оно означает: «Создайте мне новый объект». В предыдущем примере можно написать:

 String s = new String("asdf"):

Это не только значит «предоставьте мне новый объект String», но также указывает, как создать строку посредством передачи начального набора символов. Конечно, кроме String, в Java имеется множество готовых типов. Важнее то, что вы можете создавать свои собственные типы. Вообще говоря, именно создание новых типов станет вашим основным занятием при программировании на Java, и именно его мы будем рассматривать в книге.

Где хранятся данные

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


  • Регистры. Это самое быстрое хранилище, потому что данные хранятся прямо внутри процессора. Однако количество регистров жестко ограничено, поэтому регистры используются компилятором по мере необходимости. У вас нет прямого доступа к регистрам, вы не сможете найти и малейших следов их поддержки в языке. (С другой стороны, языки C и C++ позволяют порекомендовать компилятору хранить данные в регистрах.)
  • Стек. Эта область хранения данных находится в общей оперативной памяти (RAM), но процессор предоставляет прямой доступ к ней с использованием указателя стека. Указатель стека перемещается вниз для выделения памяти или вверх для ее освобождения. Это чрезвычайно быстрый и эффективный способ размещения данных, по скорости уступающий только регистрам. Во время обработки программы компилятор Java должен знать жизненный цикл данных, размещаемых в стеке. Это ограничение уменьшает гибкость ваших программ, поэтому, хотя некоторые данные Java хранятся в стеке (особенно ссылки на объекты), сами объекты Java не помещаются в стек.

  • Куча. Пул памяти общего назначения (находится также в RAM), в котором размещаются все объекты Java. Преимущество кучи состоит в том, что компилятору не обязательно знать, как долго просуществуют находящиеся там объекты. Таким образом, работа с кучей дает значительное преимущество в гибкости. Когда вам нужно создать объект, вы пишете код с использованием new, и память выделяется из кучи во время выполнения программы. Конечно, за гибкость приходится расплачиваться: выделение памяти из кучи занимает больше времени, чем в стеке (даже если бы вы могли явно создавать объекты в стеке, как в C++).

  • Постоянная память. Значения констант часто встраиваются прямо в код программы, так как они неизменны. Иногда такие данные могут размещаться в постоянной памяти (ROM), если речь идет о «встроенных» системах.
  • Не-оперативная память. Если данные располагаются вне программы, они могут существовать и тогда, когда она не выполняется. Два основных примера: потоковые объекты (streamed objects), в которых объекты представлены в виде потока байтов, обычно используются для посылки на другие машины, и долгоживущие (persistent) объекты, которые запоминаются на диске и сохраняют свое состояние даже после окончания работы программы. Особенностью этих видов хранения данных является возможность перевода объектов в нечто, что может быть сохранено на другом носителе информации, а потом восстановлено в виде обычного объекта, хранящегося в оперативной памяти. В Java организована поддержка легковесного (lightweight) сохранения состояния, а такие механизмы, как JDBC и Hibernate, предоставляют более совершенную поддержку сохранения и выборки информации об объектах из баз данных.

Особый случай: примитивные типы

Одна из групп типов, часто применяемых при программировании, требует особого обращения. Их можно назвать «примитивными» типами (табл. 2.1). Причина для особого обращения состоит в том, что создание объекта с помощью new — особенно маленькой простой переменной — недостаточно эффективно, так как new помещает объекты в кучу. В таких случаях Java следует примеру языков C и C++. То есть вместо создания переменной с помощью new создается «автоматическая» переменная, не являющаяся ссылкой. Переменная напрямую хранит значение и располагается в стеке, так что операции с ней гораздо производительнее.

В Java размеры всех примитивных типов жестко фиксированы. Они не меняются с переходом на иную машинную архитектуру, как это происходит во многих других языках. Незыблемость размера — одна из причин улучшенной переносимости Java-npoгpaмм.

Таблица 2.1. Примитивные типы


Примитивный тип Размер, бит Минимум Максимум Тип упаковки
boolean (логические значения) Boolean
char (символьные значения) 16 Unicode 0 Unicode 2^16-1 Character
byte (байт) 8 -128 +127 Byte
short (короткое целое) 16 -2^15 +2^15-1 Short
int (целое) 32 -2^31 +2^31-1 Integer
long (длинное целое) 64 -2^63 +2^63-1 Long
float (число с плавающей запятой) 32 IEEE754 IEEE754 Float
double (число с повышенной точностью) 64 IEEE754 IEEE754 Double
void (пустое значение) Void

Все числовые значения являются знаковыми, так что не ищите слова unsigned.

Размер типа boolean явно не определяется; указывается лишь то, что этот тип может принимать значения true и false.

«Классы-обертки» позволяют создать в куче не-примитивный объект для представления примитивного типа. Например:

 char с = 'х';
Character ch = new Character(c),

Также можно использовать такой синтаксис:

 Character ch = new Character('x');

Механизм автоматической упаковки Java SE5 автоматически преобразует примитивный тип в объектную «обертку»:

 Character ch = 'х';

и обратно:

 char с = ch;

Причины создания подобных конструкций будут объяснены в последующих главах.

Числа повышенной точности

В Java существует два класса для проведения арифметических операций повышенной точности: BigInteger и BigDecimal. Хотя эти классы примерно подходят под определение «классов-оберток», ни один из них не имеет аналога среди примитивных типов.
Оба класса содержат методы, производящие операции, аналогичные тем, что проводятся над примитивными типами. Иначе говоря, с классами Biglnteger и BigDecimal можно делать то же, что с int или float, просто для этого используются вызовы методов, а не встроенные операции. Также из-за использования увеличенного объема данных операции занимают больше времени. Приходится жертвовать скоростью ради точности.

Класс BigInteger поддерживает целые числа произвольной точности. Это значит, что вы можете использовать целочисленные значения любой величины без потери данных во время операций.

Класс BigDecimal представляет числа с фиксированной запятой произвольной точности; например, они могут применяться для финансовых вычислений. За подробностями о конструкторах и методах этих классов обращайтесь к документации JDK.

Массивы в Java

Фактически все языки программирования поддерживают массивы. Использование массивов в C и C++ небезопасно, потому что массивы в этих языках представляют собой обычные блоки памяти. Если программа попытается получить доступ к массиву за пределами его блока памяти или использовать память без предварительной инициализации (типичные ошибки при программировании), последствия могут быть непредсказуемы.

Одной из основных целей Java является безопасность, поэтому многие проблемы, досаждавшие программистам на C и C++, не существуют в Java. Массив в Java гарантированно инициализируется, к нему невозможен доступ за пределами его границ. Проверка границ массива обходится относительно дорого, как и проверка индекса во время выполнения, но предполагается, что повышение безопасности и подъем производительности стоят того (к тому же Java иногда может оптимизировать эти операции). При объявлении массива объектов на самом деле создается массив ссылок, и каждая из этих ссылок автоматически инициализируется специальным значением, представленным ключевым словом null. Оно означает, что ссылка на самом деле не указывает на объект. Вам необходимо присоединять объект к каждой ссылке перед тем, как ее использовать, или при попытке обращения по ссылке null во время исполнения программы произойдет ошибка. Таким образом, типичные ошибки при работе с массивами в Java предотвращаются заблаговременно.

Также можно создавать массивы простейших типов. И снова компилятор гарантирует инициализацию — выделенная для нового массива память заполняется нулями. Массивы будут подробнее описаны в последующих главах.
В большинстве языков программирования концепция жизненного цикла переменной требует относительно заметных усилий со стороны программиста. Сколько «живет» переменная? Если ее необходимо удалить, когда это следует делать? Путаница со сроками существования переменных может привести ко многим ошибкам, и этот раздел показывает, насколько Java упрощает решение затронутого вопроса, выполняя всю работу по удалению за вас.

Ограничение области действия

В большинстве процедурных языков существует понятие области действия (scope). Область действия определяет как видимость, так и срок жизни имен, определенных внутри нее. В 'C, C++' и Java область действия устанавливается положением фигурных скобок { }. Например:

 {
int х = 12;
// доступно только х
{
int q = 96;
// доступны как х, так и q
}
// доступно ТОЛЬКО x
// q находится "за пределами видимости"
}

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

Учтите, что следующая конструкция не разрешена, хотя в C и C++ она возможна:

 { int х = 12; 
{
int х = 96; // неверно
}
}

Компилятор объявит, что переменная х уже была определена. Таким образом, возможность языков C и C++ «прятать» переменные во внешней области действия не поддерживается. Создатели Java посчитали, что она приводит к излишнему усложнению программ.

Область действия объектов

Объекты Java имеют другое время жизни в сравнении с примитивами. Объект, созданный оператором Java new, будет доступен вплоть до конца области действия. Если вы напишете:

 {String s = new String("строка"); } // конец области действия

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

Благодаря тому, что объекты, созданные new, существуют ровно столько, сколько вам нужно, в Java исчезает целый пласт проблем, присущих C++. В C++ приходится не только следить за тем, чтобы объекты продолжали существовать на протяжении своего жизненного цикла, но и удалять объекты после завершения работы с ними.

Возникает интересный вопрос. Если в Java объекты остаются в памяти, что же мешает им постепенно занять всю память и остановить выполнение программы? Именно это произошло бы в данном случае в C++. Однако в Java существует сборщик мусора (garbage collector), который наблюдает за объектами, созданными оператором new, и определяет, на какие из них больше нет ссылок. Тогда он освобождает память от этих объектов, которая становится доступной для дальнейшего использования. Таким образом, вам никогда не придется «очищать» память вручную. Вы просто создаете объекты, и как только надобность в них отпадет, эти объекты исчезают сами по себе. При таком подходе исчезает целый класс проблем программирования: так называемые «утечки памяти», когда программист забывает освобождать занятую память.

Создание новых типов данных

Если все является объектом, что определяет строение и поведение класса объектов? Другими словами, как устанавливается тип объекта? Наверное, для этой цели можно было бы использовать ключевое слово type («тип»); это было бы вполне разумно. Впрочем, с давних времен повелось, что большинство объектно-ориентированных языков использовали ключевое слово class в смысле «Я собираюсь описать новый тип объектов». За ключевым словом class следует имя нового типа. Например:

 class ATypeName { /* Тело класса */ }

Эта конструкция вводит новый тип, и поэтому вы можете теперь создавать объект этого типа ключевым словом new:

 ATypeName а = new ATypeName();

Впрочем, объекту нельзя «приказать» что-то сделать (то есть послать ему необходимые сообщения) до тех пор, пока для него не будут определены методы.

Поля и методы

При определении класса (строго говоря, вся ваша работа на Java сводится к определению классов, созданию объектов этих классов и посылке сообщений этим объектам) в него можно включить две разновидности элементов: поля (fields) (иногда называемые переменными класса) и методы (methods) (еще называемые функциями класса). Поле представляет собой объект любого типа, с которым можно работать по ссылке, или объект примитивного типа. Если используется ссылка, ее необходимо инициализировать, чтобы связать с реальным объектом (ключевым словом new, как было показано ранее).

Каждый объект использует собственный блок памяти для своих полей данных; совместное использование обычных полей разными объектами класса невозможно. Пример класса с полями:

  class DataOnly { int і; double d; boolean b;}

Такой класс ничего не делает, кроме хранения данных, но вы можете создать объект этого класса:

  DataOnly data = new DataOnly();

Полям класса можно присваивать значения, но для начала необходимо узнать, как обращаться к членам объекта. Для этого сначала указывается имя ссылки на объект, затем следует точка, а далее — имя члена, принадлежащего объекту:

ссылка.член 

Например:

  data.і = 47;
data.d = 1.1;
data.b = false;

Также ваш объект может содержать другие объекты, данные которых вы хотели бы изменить. Для этого просто продолжите «цепочку из точек». Например:

  myPlane.leftTank.capacity = 100;

Класс DataOnly не способен ни на что, кроме хранения данных, так как в нем отсутствуют методы. Чтобы понять, как они работают, необходимо разобраться, что такое аргументы и возвращаемые значения. Вскоре мы вернемся к этой теме.

Значения по умолчанию для полей примитивных типов

Если поле данных относится к примитивному типу, ему гарантированно присваивается значение по умолчанию, даже если оно не было инициализировано явно (табл. 2.2).

Таблица 2.2. Значения по умолчанию для полей примитивных типов

Примитивный тип   Значение по умолчанию
boolean false
char '\u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d

Значения по умолчанию гарантируются Java только в том случае, если переменная используется как член класса. Тем самым обеспечивается обязательная инициализация элементарных типов (что не делается в C++), которая уменьшает вероятность ошибок. Однако значение по умолчанию может быть неверным или даже недопустимым для вашей программы. Переменные всегда лучше инициализировать явно.

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

  int х;

Переменной х будет присвоено случайное значение (как в C и C++); она не будет автоматически инициализирована нулем. Вы отвечаете за присвоение правильного значения перед использованием х. Если же вы забудете это сделать, в Java существует очевидное преимущество в сравнении с C++: компилятор выдает ошибку, в которой указано, что переменная не была инициализирована. (Многие компиляторы C++ предупреждают о таких переменных, но в Java это считается ошибкой.)

Методы, аргументы и возвращаемые значения

Что такое - метод?

Во многих языках (таких как C и C++) для обозначения именованной подпрограммы употребляется термин функция. В Java чаще предпочитают термин метод, как бы подразумевающий «способ что-то сделать». Если вам хочется, вы можете продолжать пользоваться термином «функция». Разница только в написании, но в дальнейшем в книге будет употребляться преимущественно термин «метод».
Методы в Java определяют сообщения, принимаемые объектом. Основные части метода — имя, аргументы, возвращаемый тип и тело. Вот примерная форма:

возвращаемыйТип ИмяМетода( /* список аргументов */ ) { /* тело метода */}

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

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

имяОбъекта.имяМетода(арг1, арг2, арг3)

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

 int х = a.f();

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

Такое действие вызова метода часто называется посылкой сообщения объекту. В примере выше сообщением является вызов f(), а объектом — а. Объектно-ориентированное программирование нередко характеризуется обобщающей формулой «посылка сообщений объектам».

Список аргументов

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

Рассмотрим метод, получающий в качестве аргумента строку (String). Следующее определение должно размещаться внутри определения класса, для которого создается метод:

 int storage(String s) {
return s.length() * 2;
}

Метод указывает, сколько байтов потребуется для хранения данных определенной строки. (Строки состоят из символов char, размер которых — 16 бит, или 2 байта; это сделано для поддержки набора символов Unicode.) Аргумент имеет тип String и называется s. Получив объект s, метод может работать с ним точно так же, как и с любым другим объектом (то есть посылать ему сообщения). В данном случае вызывается метод length(), один из методов класса String, он возвращает количество символов в строке.

Также обратите внимание на ключевое слово return, выполняющее два действия. Во-первых, оно означает: «выйти из метода, все сделано». Во-вторых, если метод возвращает значение, это значение указывается сразу же за командой return. В нашем случае возвращаемое значение — это результат вычисления s.length() * 2.

Возвращаемое значение

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

 boolean flag() { return true; }
float naturalLogBaseO { return 2.718; }
void nothing() { return; }
void nothing2() {}

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

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

Создание программы на Java

Есть еще несколько вопросов, которые необходимо понять перед созданием первой программы на Java.

Видимость имен

Проблема управления именами присуща любому языку программирования. Если имя используется в одном из модулей программы и оно случайно совпало с именем в другом модуле у другого программиста, то как отличить одно имя от другого и предотвратить их конфликт? В C это определенно является проблемой, потому что программа с трудом поддается контролю в условиях «моря» имен. Классы C++ (на которых основаны классы Java) скрывают функции внутри классов, поэтому их имена не пересекаются с именами функций других классов. Однако в C++ дозволяется использование глобальных данных и глобальных функций, соответственно, конфликты полностью не исключены. Для решения означенной проблемы в C++ введены пространства имен (namespaces), которые используют дополнительные ключевые слова.

В языке Java для решения этой проблемы было использовано свежее решение. Для создания уникальных имен библиотек разработчики Java предлагают использовать доменное имя, записанное «наоборот», так как эти имена всегда уникальны. Мое доменное имя — MindView.net, и утилиты моей программной библиотеки могли бы называться net.mindview.utility.foibles. За перевернутым доменным именем следует перечень каталогов, разделенных точками.

В версиях Java 1.0 и 1.1 доменные суффиксы com, edu, org, net по умолчанию записывались заглавными буквами, таким образом, имя библиотеки выглядело так: NET.mindview.utility.foibles. В процессе разработки Java 2 было обнаружено, что принятый подход создает проблемы, и с тех пор имя пакета записывается строчными буквами.

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

Использование внешних компонентов

Когда вам понадобится использовать уже определенный класс в вашей программе, компилятор должен знать, как этот класс обнаружить. Конечно, класс может уже находиться в том же самом исходном файле, откуда он вызывается. В таком случае вы просто его используете — даже если определение класса следует где-то дальше в файле (В Java не существует проблемы «опережающих ссылок».)

Но что, если класс находится в каком-то внешнем файле? Казалось бы, компилятор должен запросто найти его, но здесь существует проблема. Представьте, что вам необходим класс с неким именем, для которого имеется более одного определения (вероятно, отличающихся друг от друга). Или, что еще хуже, представьте, что вы пишете программу и при ее создании в библиотеку добавляется новый класс, конфликтующий с именем уже существующего класса.

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

Большую часть времени вы будете работать с компонентами из стандартных библиотек Java, поставляющихся с компилятором. Для них не нужны длинные обращенные доменные имена; вы просто записываете:

import java.util.ArrayList;

чтобы сказать компилятору, что вы хотите использовать класс ArrayList. Впрочем, пакет util содержит множество классов, и вам могут понадобиться несколько из них. Чтобы избежать последовательного перечисления классов, используйте подстановочный символ * :

import java.util.*;

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

Ключевое слово static

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

Но есть две ситуации, в которых такой подход недостаточен. Первая — это когда некоторые данные должны храниться «в единственном числе» независимо от того, сколько было создано объектов класса. Вторая — когда вам потребуется метод, не привязанный ни к какому конкретному объекту класса (то есть метод, который можно вызвать даже при полном отсутствии объектов класса).

Такой эффект достигается использованием ключевого слова static, делающего элемент класса статическим. Когда вы объявляете что-либо как static, это означает, что данные или метод не привязаны к определенному экземпляру этого класса. Поэтому, даже если вы никогда не создавали объектов класса, вы можете вызвать статический метод или получить доступ к статическим данным. С обычным объектом вам необходимо сначала создать его и использовать для вызова метода или доступа к информации, так как нестатические данные и методы должны точно знать объект, с которым работают.

Некоторые объектно-ориентированные языки используют термины данные уровня класса и методы уровня класса, подразумевая, что данные и методы существуют только на уровне класса в целом, а не для отдельных объектов этого класса. Иногда эти термины встречаются в литературе по Java.

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

 class StaticTest {
static int і =47;
}

Теперь, даже при создании двух объектов StaticTest, для элемента StaticTest.i выделяется единственный блок памяти. Оба объекта совместно используют одно значение і. Пример:

 StaticTest stl = new StaticTest();
StaticTest st2 = new StaticTest();

В данном примере как st1.i, так и st2.i имеют одинаковые значения, равные 47, потому что расположены они в одном блоке памяти. Существует два способа обратиться к статической переменной. Как было видно выше, вы можете указать ее с помощью объекта, например st2.i. Также можно обратиться к ней прямо по имени класса (для нестатических членов класса такая возможность отсутствует):

 StaticTest.i++;

Оператор ++ увеличивает значение на единицу (инкремент). После выполнения этой команды значения st1.i и st2.i будут равны 48.

Синтаксис с именем класса является предпочтительным, потому что он не только подчеркивает, что переменная описана как static, но и в некоторых случаях предоставляет компилятору больше возможностей для оптимизации.

Та же логика верна и для статических методов. Вы можете обратиться к такому методу или через объект, как это делается для всех методов, или в специальном синтаксисе имяКласса.метод(). Статические методы определяются по аналогии со статическими данными:

  class Incrementable {
static void increment () { StaticTest.i++; }
}

Нетрудно заметить, что метод increment() класса Incrementable увеличивает значение статического поля і. Метод можно вызвать стандартно, через объект:

   Incrementable sf = new Incrementable();
sf.increment();

Или, поскольку increment() является статическим, можно вызвать его с прямым указанием класса:

  Incrementable.increment();

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

Наша первая программа на Java

Наконец, долгожданная программа. Она запускается, выводит на экран строку, а затем текущую дату, используя стандартный класс Date из стандартной библиотеки Java:

// НеlloDate.java
import java.util.*;
 
public class HelloDate {
 
public static void main(String[] args) {
System.out.println("Привет, сегодня: ");
System out println(new Date());
}
 
}

После запуска программы получим текст вида:

Привет, сегодня:
Wed Oct 05 14:39:36 MDT 2005

В начале каждого файла с программой должны находиться директивы import, в которых перечисляются все дополнительные классы, необходимые вашей программе. Обратите внимание на слово «дополнительные» — существует целая библиотека классов, присоединяющаяся автоматически к каждому файлу Java: java.lang. Запустите ваш браузер и просмотрите документацию фирмы Sun. (Если вы не загрузили документацию JDK с сайта https://java.sun.com или не получили ее иным способом, обязательно это сделайте.) Учтите, что документация не входит в комплект JDK, ее необходимо загрузить отдельно. Взглянув на список пакетов, вы найдете в нем различные библиотеки классов, поставляемые с Java.

Выберите java.lang. Здесь вы увидите список всех классов, составляющих эту библиотеку. Так как пакет java.lang. автоматически включается в каждую программу на Java, эти классы всегда доступны для использования. Класса Date в нем нет, а это значит, что для его использования придется импортировать другую библиотеку.

Если вы не знаете, в какой библиотеке находится нужный класс, или если вам понадобится увидеть все классы, выберите Tree (дерево классов) в документации. В нем можно обнаружить любой из доступных классов Java. Функция поиска текста в браузере поможет найти класс Date. Результат поиска показывает, что класс называется java.util.Date, то есть находится в библиотеке util, и для получения доступа к классу Date необходимо будет использовать директиву import для загрузки пакета java.util.*.

Если вы вернетесь к началу, выберете пакет java.lang, а затем класс System, то увидите, что он имеет несколько полей. При выборе поля out обнаруживается, что оно представляет собой статический объект PrintStream. Так как поле описано с ключевым словом static, вам не понадобится создавать объекты. Действия, которые можно выполнять с объектом out, определяются его типом: PrintStream.

Для удобства в описание этого типа включена гиперссылка, и, если щелкнуть на ней, вы обнаружите список всех доступных методов. Этих методов довольно много, и они будут позже рассмотрены в книге. Сейчас нас интересует только метод println(), вызов которого фактически означает: «вывести то, что передано методу, на консоль и перейти на новую строку». Таким образом, в любую программу на Java можно включить вызов вида System.out.println ("что-то"), чтобы вывести сообщение на консоль.

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

 public static void main(String[] args) {

Ключевое слово public обозначает, что метод доступен для внешнего мира (об этом подробно рассказывает глава 5). Аргументом метода main() является массив строк. В данной программе массив args не используется, но компилятор Java настаивает на его присутствии, так как массив содержит параметры, переданные программе в командной строке.

Строка, в которой распечатывается число, довольно интересна:

 System.out.println (new Date());

Аргумент представляет собой объект Date, который создается лишь затем, чтобы передать свое значение (автоматически преобразуемое в String) методу println(). Как только команда будет выполнена, объект Date становится ненужным, сборщик мусора заметит это, и в конце концов сам удалит его. Нам не нужно беспокоиться о его удалении самим.

Компиляция и выполнение

Чтобы скомпилировать и выполнить эту программу, а также все остальные программы в книге, вам понадобится среда разработки Java. Существует множество различных сред разработок от сторонних производителей, но в этой книге мы предполагаем, что вы избрали бесплатную среду JDK Java Developer's Kit) от фирмы Sun. Если же вы используете другие системы разработки программ, вам придется просмотреть их документацию, чтобы узнать, как компилировать и запускать программы.

Подключитесь к Интернету и посетите сайт https://java.sun.com. Там вы найдете информацию и необходимые ссылки, чтобы загрузить и установить JDK для вашей платформы.

Как только вы установите JDK и правильно установите пути запуска, в результате чего система сможет найти утилиты javac и java, загрузите и распакуйте исходные тексты программ для этой книги (их можно загрузить с сайта https://www.MindView.net). Там вы обнаружите каталоги (папки) для каждой главы книги. Перейдите в папку objects и выполните команду

javac HelloDate java

Команда не должна выводить каких-либо сообщений. Если вы получили сообщение об ошибке, значит, вы неверно установили JDK и вам нужно разобраться со своими проблемами.

И наоборот, если все прошло успешно, выполните следующую команду:

java НеlloDate

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

Эта последовательность действий позволяет откомпилировать и выполнить любую программу-пример из этой книги. Однако также вы увидите, что каждая папка содержит файл build.xml с командами для инструмента ant по автоматической сборке файлов для данной главы. После установки ant с сайта https://jakarta.apache.org/ant можно будет просто набрать команду ant в командной строке, чтобы скомпилировать и запустить программу из любого примера. Если ant на вашем компьютере еще не установлен, команды javac и java придется вводить вручную.

Комментарии и встроенная документация

В Java приняты два вида комментариев. Первый — традиционные комментарии в стиле C, также унаследованные языком C++. Такие комментарии начинаются с комбинации /* и распространяются иногда на множество строк, после чего за­канчиваются символами */. Заметьте, что многие программисты начинают каждую новую строку таких комментариев символом *, соответственно, часто можно увидеть следующее:

 /* Это комментарий,
* распространяющийся на
* несколько строк */

Впрочем, все символы между /* и */ игнорируются, и с таким же успехом можно использовать запись

 /* Это комментарий, 
распространяющийся на несколько строк */

Второй вид комментария пришел из языка C++. Однострочный комментарий начинается с комбинации // и продолжается до конца строки. Такой стиль очень удобен и прост, поэтому широко используется на практике. Вам не приходится искать на клавиатуре сначала символ /, а затем * (вместо этого вы дважды нажимаете одну и ту же клавишу), и не нужно закрывать комментарий. Поэтому часто можно увидеть такие примеры:

 // это комментарий в одну строку

Документация в комментариях

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

Инструмент для извлечения комментариев называется javadoc, он является частью пакета JDK. Некоторые возможности компилятора Java используются в нем для поиска пометок в комментариях, включенных в ваши программы. Он не только извлекает помеченную информацию, но также узнает имя класса или метода, к которому относится данный фрагмент документации. Таким образом, с минимумом затраченных усилий можно создать вполне приличную сопрово­дительную документацию для вашей программы.

Результатом работы программы javadoc является HTML-файл, который можно просмотреть в браузере. Таким образом, утилита javadoc позволяет создавать и поддерживать единый файл с исходным текстом и автоматически строить полезную документацию. В результае получается простой и практичный стандарт по созданию документации, поэтому мы можем ожидать (и даже требовать) наличия документации для всех библиотек Java.

Вдобавок, вы можете дополнить javadoc своими собственными расширениями, называемыми доклетами (doclets), в которых можно проводить специальные операции над обрабатываемыми данными (например, выводить их в другом формате).

Далее следует лишь краткое введение и обзор основных возможностей javadoc. Более подробное описание можно найти в документации JDK. Распаковав документацию, загляните в папку tooldocs (или перейдите по ссылке tooldocs).

Синтаксис

Все команды javadoc находятся только внутри комментариев /**. Комментарии, как обычно, завершаются последовательностью */. Существует два основных способа работы с javadoc: встраивание HTML-текста или использование разметки документации (тегов). Самостоятельные теги документации — это команды, которые начинаются символом @ и размещаются с новой строки комментария.

(Начальный символ * игнорируется.) Встроенные теги документации могут располагаться в любом месте комментария javadoc, также начинаются со знака @, но должны заключаться в фигурные скобки.

Существует три вида документации в комментариях для разных элементов кода: класса, переменной и метода. Комментарий к классу записывается прямо перед его определением; комментарий к переменной размещается непосредственно перед ее определением, а комментарий к методу тоже записывается прямо перед его определением. Простой пример:

//: object/Documentation1.java 
/** Комментарий к классу */
public class Documentation1 {
/** Комментарий к переменной */
public int і;
/** Комментарий к методу */
public void f() {}
} ///:~

Заметьте, что javadoc обрабатывает документацию в комментариях только для членов класса с уровнем доступа public и protected. Комментарии для членов private и членов с доступом в пределах пакета игнорируются, и документация по ним не строится. (Впрочем, флаг -private включает обработку и этих членов). Это вполне логично, поскольку только public- и protected-члены доступны вне файла, и именно они интересуют программиста-клиента.

Результатом работы программы является HTML-файл в том же формате, что и остальная документация для Java, так что пользователям будет привычно и удобно просматривать и вашу документацию. Попробуйте набрать текст предыдущего примера, «пропустите» его через javadoc и просмотрите полученный HTML-файл, чтобы увидеть результат.

Встроенный HTML

javadoc вставляет команды HTML в итоговый документ. Это позволяет полностью использовать все возможности HTML; впрочем, данная возможность прежде всего ориентирована на форматирование кода:

//: object/Documentation2.java
/**
* <pre>
* System.out.println(new Date());
* </pre>
*/

public class Documentation2 {}

Вы можете использовать HTML точно так же, как в обычных страницах, чтобы привести описание к нужному формату:

//: object/Documentation3.java
/**
* You can <em>even</em> insert a list:
* <ol>
* <li> Пункт первый
* <li> Пункт второй
* <li> Пункт третий
* </ol>
*/

public class Documentation3 {}

javadoc игнорирует звездочки в начале строк, а также начальные пробелы. Текст переформатируется таким образом, чтобы он отвечал виду стандартной документации. Не используйте заголовки вида <h1> или <hr> во встроенном HTML, потому что javadoc вставляет свои собственные заголовки и ваши могут с ними «пересечься».

Встроенный HTML-код поддерживается всеми типами документации в комментариях — для классов, переменных или методов.

Примеры тегов

Далее описаны некоторые из тегов javadoc, используемых при документировании программы. Прежде чем применять javadoc для каких-либо серьезных целей, просмотрите руководство по нему в документации пакета JDK, чтобы получить полную информацию о его использовании.

@see: ссылка на другие классы

Тег позволяет ссылаться на документацию к другим классам. javadoc там, где были записаны теги @see, создает HTML-ссылки на другие документы. Основные формы использования тега:

@see имя класса
@see полное-имя-класса
@see полное-имя-класса#имя-метода

Каждая из этих форм включает в генерируемую документацию замечание See Also («см. также») со ссылкой на указанные классы. javadoc не проверяет передаваемые ему гиперссылки.

{@link пакет.класс#член_класса метка}

Тег очень похож на @see, не считая того, что он может использоваться как встроенный, а вместо стандартного текста See Also в ссылке размещается текст, указанный в поле метка.

{@docRoot}

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

{@inheritDoc}

Наследует документацию базового класса, ближайшего к документируемому классу, в текущий файл с документацией.

@version

Имеет следующую форму:

@version информация-о-версии

Поле информации о версии содержит ту информацию, которую вы сочли нужным включить. Когда в командной строке javadoc указывается опция -version, в созданной документации специально отводится место, заполняемое информа­цией о версиях.

@author

Записывается в виде

@author информация-об-авторе

Предполагается, что поле информация-об-авторе представляет собой имя автора, хотя в него также можно включить адрес электронной почты и любую другую информацию. Когда в командной строке javadoc указывается опция -author, в созданной документации сохраняется информация об авторе.

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

@since

Тег позволяет задать версию кода, с которой началось использование некоторой возможности. В частности, он присутствует в HTML-документации по Java, где служит для указания версии JDK.

@param

Полезен при документировании методов. Форма использования:

@param имя-параметра описание

где имя-параметра — это идентификатор в списке параметров метода, а описание — текст описания, который можно продолжить на несколько строк. Описание считается завершенным, когда встретится новый тег. Можно записывать любое количество тегов @param, по одному для каждого параметра метода.

@return

Форма использования:

@return описание

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

@throws

Исключения будут рассматриваться в главе 9. В двух словах это объекты, которые можно «возбудить» (throw) в методе, если его выполнение потерпит неудачу. Хотя при вызове метода создается всегда один объект исключения, определенный метод может вырабатывать произвольное количество исключений, и все они требуют описания. Соответственно, форма тега исключения такова:

@throws полное-имя-класса описание

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

@deprecated

Тег используется для пометки устаревших возможностей, замещенных новыми и улучшенными. Он сообщает о том, что определенные средства программы не следует использовать, так как в будущем они, скорее всего, будут убраны. В Java SE5 тег @deprecated был заменен директивой @Deprecated (см. далее).

Пример документации

Вернемся к нашей первой программе на Java, но на этот раз добавим в нее комментарии со встроенной документацией:

//: object/HelloDate.java
import java.util.*;
 
/** Первая программа-пример книги.
* Выводит строку и текущее число.
* @author Брюс Эккель
* @author www.MindView.net
* @version 4.0
*/

public class HelloDate {
/** Точка входа в класс и приложение
* @param Массив строковых аргументов
* @throws exceptions Исключения не выдаются
*/

public static void main(String[] args) {
System.out.println("Привет, сегодня: ");
System.out.println(new Date());
}
}
/*
Output: (55% match)
Привет, сегодня:
Wed Oct 05 14:39:36 MDT 2005
*/
//:~

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

Тег /* Output: обозначает начало выходных данных, сгенерированных данным файлом. В этой форме их можно автоматически проверить на точность.

В данном случае значение (55% match) сообщает системе тестирования, что результаты будут заметно отличаться при разных запусках программы. В большинстве примеров книги результаты приводятся в комментариях такого вида, чтобы вы могли проверить их на правильность.

Стиль оформления программ

Согласно правилам стиля, описанным в руководстве Code Conventions for the Java Programming Language имена классов должны записываться с прописной буквы. Если имя состоит из нескольких слов, они объединяются (то есть символы подчеркивания не используются для разделения), и каждое слово в имени начинается с большой буквы:

class АllTheColorsOfTheRainbow { // ..

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

 class AllTheColorsOfTheRainbow {
 
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue) { /*.......*/ }
// ....
}

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

В исходных текстах Java, которые можно увидеть в библиотеках фирмы Sun, также используется схема размещения открывающих и закрывающих фигурных скобок, которая встречается в примерах данной книги.

Резюме

В этой главе я постарался привести информацию о программировании на Java, достаточную для написания самой простой программы. Также был представлен обзор языка и некоторых его основных свойств. Однако примеры до сих пор имели форму «сначала это, потом это, а после что-то еще». В следующих двух главах будут представлены основные операторы, используемые при программировании на Java, а также способы передачи управления в вашей программе.

]]>
Книги по Java https://linexp.ru?id=4750 Wed, 29 Jun 2022 14:31:21 GMT
<![CDATA[Глава 3 Thinking in Java 4th edition]]> ОПЕРАТОРЫНа нижнем уровне операции с данными в Java осуществляются посредством операторов. Язык Java создавался на основе C++, поэтому большинство этих операторов и конструкций знакомы программистам на C и C++. Также в Java были добавлены некоторые улучшения и упрощения. Если вы .знакомы с синтаксисом C или C++, бегло просмотрите эту и следующую главу, останавливаясь на тех местах, в которых Java отличается от этих языков. Если чтение дается вам с трудом, попробуйте обратиться к мультимедийному семинару Thinking in С, свободно загружаемому с сайта www.MindView.net. Он содержит аудиолекции, слайды, упражнения и решения, специально разработанные для быстрого ознакомления с синтаксисом C, необходимым для успешного овладения языком Java.

ОПЕРАТОРЫ

На нижнем уровне операции с данными в Java осуществляются посредством операторов.
Язык Java создавался на основе C++, поэтому большинство этих операторов и конструкций знакомы программистам на C и C++. Также в Java были добавлены некоторые улучшения и упрощения.
Если вы .знакомы с синтаксисом C или C++, бегло просмотрите эту и следующую главу, останавливаясь на тех местах, в которых Java отличается от этих языков. Если чтение дается вам с трудом, попробуйте обратиться к мультимедийному семинару Thinking in С, свободно загружаемому с сайта www.MindView.net. Он содержит аудиолекции, слайды, упражнения и решения, специально разработанные для быстрого ознакомления с синтаксисом C, необходимым для успешного овладения языком Java.

Простые команды печати

В предыдущей главе была представлена команда печати Java

 System.out.println("Какая длинная команда...");

Вероятно, вы заметили, что команда не только получается слишком длинной, но и плохо читается. Во многих языках до и после Java используется более простой подход к выполнению столь распространенной операции.
В главе 6 представлена концепция статического импорта, появившаяся в Java SE5, а также крошечная библиотека, упрощающая написание команд печати. Тем не менее для использования библиотеки не обязательно знать все подробности. Программу из предыдущей главы можно переписать в следующем виде:

//: operators/HelloDate.java
import java.util.*;
import static net.mindview.util.Print.*;
 
public class HelloDate {
public static void main(String[] args) {
print("Привет, сегодня: ");
print(new Date());
}
}

<spoiler text="Output:">

Привет, сегодня
Wed Oct 05 14-39 36 MDT 2005

</spoiler>
Результат смотрится гораздо приятнее. Обратите внимание на ключевое слово static во второй команде import.

Чтобы использовать эту библиотеку, необходимо загрузить архив с примерами кода. Распакуйте его и включите корневой каталог дерева в переменную окружения CLASSPATH вашего компьютера. Хотя использование net.mindview.util.Print упрощает программный код, оно оправданно не везде. Если программа содержит небольшое количество команд печати, я отказываюсь от import и записываю полный вызов System.out.println().

Операторы Java

Оператор получает один или несколько аргументов и создает на их основе новое значение. Форма передачи аргументов несколько иная, чем при вызове метода, но эффект тот же самый. Сложение (+), вычитание и унарный минус (-), умножение (*), деление (/) и присвоение (=) работают одинаково фактически во всех языках программирования.

Все операторы работают с операндами и выдают какой-то результат. Вдобавок некоторые операторы могут изменить значение операнда. Это называется побочным эффектом. Как правило, операторы, изменяющие значение своих операндов, используются именно ради побочного эффекта, но вы должны помнить, что полученное значение может быть использовано в программе и обычным образом, независимо от побочных эффектов.

Почти все операторы работают только с примитивами. Исключениями являются =, ==, !=, которые могут быть применены к объектам (и создают немало затруднений). Кроме того, класс String поддерживает операции + и +=.

Приоритет

Приоритет операций определяет порядок вычисления выражений с несколькими операторами. В Java существуют конкретные правила для определения очередности вычислений. Легче всего запомнить, что деление и умножение выполняются раньше сложения и вычитания. Программисты часто забывают правила предшествования, поэтому для явного задания порядка вычислений следует использовать круглые скобки. Например, взгляните на команды (1) и (2):

//: operators/Precedence.java
public class Precedence {
public static void main(String[] args) {
int x = 1, y = 2, z = 3;
int a = x + y - 2/2 + z; // (1)
int b = x + (y - 2)/(2 + z); // (2)
System.out.println("a = " + a + " b = " + b);
}
}

<spoiler text="Output:">

a = 5 b = 1

</spoiler>
Команды похожи друг на друга, но из результатов хорошо видно, что они имеют разный смысл в зависимости от присутствия круглых скобок.
Обратите внимание на оператор + в команде System.out.println. В данном контексте + означает конкатенацию строк, а не суммирование. Когда компилятор встречает объект String, за которым следует + и объект, отличный от String, он пытается преобразовать последний объект в String. Как видно из выходных данных, для а и b тип int был успешно преобразован в String.

Присвоение

Присвоение выполняется оператором =

Трактуется он так: «взять значение из правой части выражения (часто называемое просто значением) и скопировать его в левую часть (часто называемую именующим выражением)». Значением может быть любая константа, переменная или выражение, но в качестве именующего выражения обязательно должна использоваться именованная переменная (то есть для хранения значения должна выделяться физическая память). Например, вы можете присвоить постоянное значение переменной:

 а = 4;

но нельзя присвоить что-либо константе — она не может использоваться в качестве именующего выражения (например, запись 4 = а недопустима).

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

При присвоении объектов все меняется. При выполнении операций с объектом вы в действительности работаете со ссылкой, поэтому присвоение «одного объекта другому» на самом деле означает копирование ссылки из одного места в другое. Это значит, что при выполнении команды c = d для объектов в конечном итоге с и d указывают на один объект, которому изначально соответствовала только ссылка d. Сказанное демонстрирует следующий пример:

//: operators/Assignment.java
// Присвоение объектов имеет ряд хитростей
import static net.mindview.util.Print.*;
 
class Tank {
int level;
}
 
public class Assignment {
public static void main(String[] args) {
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 9;
t2.level = 47;
print("1: t1.level: " + t1.level +
", t2.level: " + t2.level);
t1 = t2;
print("2: t1.level: " + t1.level +
", t2.level: " + t2.level);
t1.level = 27;
print("3: t1.level: " + t1.level +
", t2.level: " + t2.level);
}
}

<spoiler text="Output:">

1: t1.level: 9, t2.level: 47
2: t1.level: 47, t2.level: 47
3: t1.level: 27, t2.level: 27

</spoiler>
Класс Tank предельно прост, и два его экземпляра (t1 и t2) создаются внутри метода main(). Переменной level для каждого экземпляра придаются различные значения, а затем ссылка t2 присваивается t1, в результате чего t1 изменяется.

Во многих языках программирования можно было ожидать, что t1 и t2 будут независимы все время, но из-за присвоения ссылок изменение объекта t1 отражается на объекте t2!
Это происходит из-за того, что t1 и t2 содержат одинаковые ссылки, указывающие на один объект. (Исходная ссылка, которая содержалась в t1 и указывала на объект со значением 9, была перезаписана во время присвоения и фактически потеряна; ее объект будет вскоре удален сборщиком мусора.)

Этот феномен совмещения имен часто называют синонимией (aliasing), и именно она является основным способом работы с объектами в Java. Но что делать, если совмещение имен нежелательно? Тогда можно пропустить присвоение и записать

 t1.level = t2.level;

При этом программа сохранит два разных объекта, а не «выбросит» один из них, «привязав» ссылки t1 и t2 к единственному объекту. Вскоре вы поймете, что прямая работа с полями данных внутри объектов противоречит принципам объектно-ориентированной разработки. Впрочем, это непростой вопрос, так что пока вам достаточно запомнить, что присвоение объектов может таить в себе немало сюрпризов.

Совмещение имен во время вызова методов

Совмещение имен также может происходить при передаче объекта методу:

//: operators/PassObject.java
// Передача объектов методам может работать
// не так. как вы привыкли.
import static net.mindview.util.Print.*;
 
class Letter {
char c;
}
 
public class PassObject {
static void f(Letter y) {
y.c = 'z';
}
public static void main(String[] args) {
Letter x = new Letter();
x.c = 'a';
print("1: x.c: " + x.c);
f(x);
print("2: x.c: " + x.c);
}
}

<spoiler text="Output:">

1: x.c: a
2: x.c: z

</spoiler>
Во многих языках программирования метод f() создал бы копию своего параметра Letter у внутри своей области действия. Но из-за передачи ссылки строка

 у.с = 'z';

на самом деле изменяет объект за пределами метода f().

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

Арифметические операторы

Основные математические операторы

Основные математические операторы остаются неизменными почти во всех языках программирования:

  • сложение ( + );
  • вычитание ( - );
  • деление ( / );
  • умножение ( * )
  • остаток от деления нацело ( % );

Деление нацело обрезает, а не округляет результат.

В Java также используется укороченная форма записи для того, чтобы одновременно произвести операцию и присвоение. Она обозначается оператором с последующим знаком равенства и работает одинаково для всех операторов языка (когда в этом есть смысл). Например, чтобы прибавить 4 к переменной х и присвоить результат х, используйте команду х += 4.

Следующий пример демонстрирует использование арифметических операций:

//: operators/MathOps.java
// Демонстрация математических операций.
import java.util.*;
import static net.mindview.util.Print.*;
 
public class MathOps {
public static void main(String[] args) {
// Создание и раскрутка генератора случайных чисел:
Random rand = new Random(47);
int i, j, k;
// Choose value from 1 to 100:
j = rand.nextInt(100) + 1;
print("j : " + j);
k = rand.nextInt(100) + 1;
print("k : " + k);
i = j + k;
print("j + k : " + i);
i = j - k;
print("j - k : " + i);
i = k / j;
print("k / j : " + i);
i = k * j;
print("k * j : " + i);
i = k % j;
print("k % j : " + i);
j %= k;
print("j %= k : " + j);
// Тесты для вещественных чисел:
float u, v, w; // также можно использовать double
v = rand.nextFloat();
print("v : " + v);
w = rand.nextFloat();
print("w : " + w);
u = v + w;
print("v + w : " + u);
u = v - w;
print("v - w : " + u);
u = v * w;
print("v * w : " + u);
u = v / w;
print("v / w : " + u);
// следующее также относится к типам
// char, byte, short, int. long и double:
u += v;
print("u += v : " + u);
u -= v;
print("u -= v : " + u);
u *= v;
print("u *= v : " + u);
u /= v;
print("u /= v : " + u);
}
}

<spoiler text="Output:">

j : 59
k : 56
j + k : 115
j - k : 3
k / j : 0
k * j : 3304
k % j : 56
j %= k : 3
v : 0.5309454
w : 0.0534122
v + w : 0.5843576
v - w : 0.47753322
v * w : 0.028358962
v / w : 9.940527
u += v : 10.471473
u -= v : 9.940527
u *= v : 5.2778773
u /= v : 9.940527

</spoiler>
Для получения случайных чисел создается объект Random. Если он создается без параметров, Java использует текущее время для раскрутки генератора, чтобы при каждом запуске программы выдавались разные числа.

Программа генерирует различные типы случайных чисел, вызывая соответствующие методы объекта Random: nextInt() и nextFloat() (также можно использовать nextLong() и nextDouble()). Аргумент nextInt() задает верхнюю границу ге­нерируемых чисел. Нижняя граница равна 0, но для предотвращения возможного деления на 0 результат смещается на 1.

Унарные операторы плюс и минус

Унарные минус (-) и плюс (+) внешне не отличаются от аналогичных бинарных операторов. Компилятор выбирает нужный оператор в соответствии с контекстом использования. Например, команда

 х = -а;

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

 х = а * -b;

но читающий код может запутаться, так что яснее будет написать так:

 х = а * (-b);

Унарный минус меняет знак числа на противоположный. Унарный плюс существует «для симметрии», хотя и не производит никаких действий.

Автоувеличение и автоуменьшение

В Java, как и в C, существует множество различных сокращений. Сокращения могут упростить написание кода, а также упростить или усложнить его чтение.

Два наиболее полезных сокращения — это операторы увеличения (инкремента) и уменьшения (декремента) (также часто называемые операторами автоматического приращения и уменьшения). Оператор декремента записывается в виде -- и означает «уменьшить на единицу».

Оператор инкремента обозначается символами ++ и позволяет «увеличить на единицу». Например, если переменная а является целым числом, то выражение ++а будет эквивалентно (а = а + 1). Операторы инкремента и декремента не только изменяют переменную, но и устанавливают ей в качестве результата новое значение.

Каждый из этих операторов существует в двух версиях: префиксной и постфиксной. Префиксный инкремент значит, что оператор ++ записывается перед переменной или выражением, а при постфиксном инкременте оператор следует после переменной или выражения. Аналогично, при префиксном декременте оператор -- указывается перед переменной или выражением, а при постфиксном - после переменной или выражения.

Для префиксного инкремента и декремента (то есть ++а и --а) сначала выполняется операция, а затем выдается результат. Для постфиксной записи (а++ и а--) сначала выдается значение, и лишь затем выполняется операция. Например:

//: operators/AutoInc.java
// Demonstrates the ++ and -- operators.
import static net.mindview.util.Print.*;
 
public class AutoInc {
public static void main(String[] args) {
int i = 1;
print("i : " + i);
print("++i : " + ++i); // Pre-increment
print("i++ : " + i++); // Post-increment
print("i : " + i);
print("--i : " + --i); // Pre-decrement
print("i-- : " + i--); // Post-decrement
print("i : " + i);
}
}

<spoiler text="Output:">

i : 1
++i : 2
i++ : 2
i : 3
--i : 2
i-- : 2
i : 1

</spoiler>
Вы видите, что при использовании префиксной формы результат получается после выполнения операции, тогда как с постфиксной формой он доступен до выполнения операции. Это единственные операторы (кроме операторов присваивания), которые имеют побочный эффект. (Иначе говоря, они изменяют свой операнд вместо простого использования его значения.)

Оператор инкремента объясняет происхождение названия языка C++; подразумевается «шаг вперед по сравнению с C». В одной из первых речей, посвященных Java, Билл Джой (один из его создателей) сказал, что «Java=C++--» («Си плюс плюс минус минус»). Он имел в виду, что Java — это C++, из которого убрано все, что затрудняет программирование, и поэтому язык стал гораздо проще. Продвигаясь вперед, вы увидите, что отдельные аспекты языка, конечно, проще, и все же Java не настолько проще C++.

Операторы сравнения и логические операторы

Операторы сравнения выдают логический (boolean) результат. Они проверяют, в каком отношении находятся значения их операндов. Если условие проверки истинно, оператор выдает true, а если ложно — false. К операторам сравнения относятся следующие: «меньше чем» (<), «больше чем» (>), «меньше чем или равно» (<=), «больше чем или равно» (>=), «равно» (==) и «не равно» (!=). «Равно» и «не равно» работают для всех примитивных типов данных, однако ос­тальные сравнения не применимы к типу boolean.

Проверка объектов на равенство

Операции отношений == и != также работают с любыми объектами, но их смысл нередко сбивает с толку начинающих программистов на Java. Пример:

//: operators/Equivalence.java
public class Equivalence {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2);
System.out.println(n1 != n2);
}
}

<spoiler text="Output:">

false
true

</spoiler>
Выражение System.out.println(n1 == n2) выведет результат логического сравнения, содержащегося в скобках. Казалось бы, в первом случае результат должен быть истинным (true), а во втором — ложным (false), так как оба объекта типа Integer имеют одинаковые значения. Но в то время как содержимое объектов одинаково, ссылки на них разные, а операторы != и == сравнивают именно ссылки. Поэтому результатом первого выражения будет false, а второго — true. Естественно, такие результаты поначалу ошеломляют.

А если понадобится сравнить действительное содержимое объектов? Придется использовать специальный метод equals(), поддерживаемый всеми объектами (но не примитивами, для которых более чем достаточно операторов == и !=). Вот как это делается:

//: operators/EqualsMethod.java
public class EqualsMethod {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1.equals(n2));
}
}

<spoiler text="Output:">

true

</spoiler>
На этот раз результат окажется «истиной» (true), как и предполагалось. Но все не так просто, как кажется. Если вы создадите свой собственный класс вроде такого:

//: operators/EqualsMethod2.java
// Метод equals() по умолчанию не сравнивает содержимое.
class Value {
int i;
}
 
public class EqualsMethod2 {
public static void main(String[] args) {
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2));
}
}

<spoiler text="Output:">

false

</spoiler>
мы вернемся к тому, с чего начали: результатом будет false. Дело в том, что метод equals() по умолчанию сравнивает ссылки. Следовательно, пока вы не переопределите этот метод в вашем новом классе, не получите желаемого результата. К сожалению, переопределение будет рассматриваться только в главе 8, а пока осторожность и общее понимание принципа работы equals() позволит избежать некоторых неприятностей.

Большинство классов библиотек Java реализуют метод equals() по-своему, сравнивая содержимое объектов, а не ссылки на них.

Логические операторы

Логические операторы И (&&), ИЛИ (||) и НЕ (!) производят логические значения true и false, основанные на логических отношениях своих аргументов. В следующем примере используются как операторы сравнения, так логические операторы:

//: operators/Bool.java
// Операторы сравнений и логические операторы.
import java.util.*;
import static net.mindview.util.Print.*;
 
public class Bool {
public static void main(String[] args) {
Random rand = new Random(47);
int i = rand.nextInt(100);
int j = rand.nextInt(100);
print("i = " + i);
print("j = " + j);
print("i > j is " + (i > j));
print("i < j is " + (i < j));
print("i >= j is " + (i >= j));
print("i <= j is " + (i <= j));
print("i == j is " + (i == j));
print("i != j is " + (i != j));
// Treating an int as a boolean is not legal Java:
//! print("i && j is " + (i && j));
//! print("i || j is " + (i || j));
//! print("!i is " + !i);
print("(i < 10) && (j < 10) is "
+ ((i < 10) && (j < 10)) );
print("(i < 10) || (j < 10) is "
+ ((i < 10) || (j < 10)) );
}
}

<spoiler text="Output:">

i = 58
j = 55
i > j is true
i < j is false
i >= j is true
i <= j is false
i == j is false
i != j is true
(i < 10) && (j < 10) is false
(i < 10) || (j < 10) is false

</spoiler>
Операции И, ИЛИ и НЕ применяются только к логическим (boolean) значениям. Нельзя использовать в логических выражениях не-boolean-типы в качестве булевых, как это разрешается в C и C++. Неудачные попытки такого рода видны в строках, помеченных особым комментарием //! (этот синтаксис позволяет автоматически удалять комментарии для удобства тестирования). Последующие выражения вырабатывают логические результаты, используя операторы сравнений, после чего к полученным значениям примененяются логические операции.

Заметьте, что значение boolean автоматически переделывается в подходящее строковое представление там, где предполагается использование строкового типа String.

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

Ускоренное вычисление

При работе с логическими операторами можно столкнуться с феноменом, называемым «ускоренным вычислением». Это значит, что выражение вычисляется только до тех пор, пока не станет очевидно, что оно принимает значение «истина» или «ложь». В результате, некоторые части логического выражения могут быть проигнорированы в процессе сравнения. Следующий пример демонстрирует ускоренное вычисление:

//: operators/ShortCircuit.java
// Демонстрация ускоренного вычисления
// при использовании логических операторов
import static net.mindview.util.Print.*;
 
public class ShortCircuit {
static boolean test1(int val) {
print("test1(" + val + ")");
print("result: " + (val < 1));
return val < 1;
}
static boolean test2(int val) {
print("test2(" + val + ")");
print("result: " + (val < 2));
return val < 2;
}
static boolean test3(int val) {
print("test3(" + val + ")");
print("result: " + (val < 3));
return val < 3;
}
public static void main(String[] args) {
boolean b = test1(0) && test2(2) && test3(2);
print("expression is " + b);
}
}

<spoiler text="Output:">

test1(0)
result: true
test2(2)
result: false
expression is false

</spoiler>
Каждый из методов test() проводит сравнение своего аргумента и возвращает либо true, либо false. Также они выводят информацию о факте своего вызова. Эти методы используются в выражении:

 testl(0) && test2(2) && test3(2)

Естественно было бы ожидать, что все три метода должны выполняться, но результат программы показывает другое. Первый метод возвращает результат true, поэтому вычисление выражения продолжается. Однако второй метод выдает результат false. Так как это автоматически означает, что все выражение будет равно false, зачем продолжать вычисления? Только лишняя трата времени. Именно это и стало причиной введения в язык ускоренного вычисления; отказ от лишних вычислений обеспечивает потенциальный выигрыш в производительности.

Литералы

Обычно, когда вы записываете в программе какое-либо значение, компилятор точно знает, к какому типу оно относится. Однако в некоторых ситуациях однозначно определить тип не удается. В таких случаях следует помочь компилятору определить точный тип, добавив дополнительную информацию в виде определенных символьных обозначений, связанных с типами данных. Эти обозначения используются в следующей программе:

//: operators/Literals.java
import static net.mindview.util.Print.*;
 
public class Literals {
public static void main(String[] args) {
int i1 = 0x2f; // Hexadecimal (lowercase)
print("i1: " + Integer.toBinaryString(i1));
int i2 = 0X2F; // Hexadecimal (uppercase)
print("i2: " + Integer.toBinaryString(i2));
int i3 = 0177; // Octal (leading zero)
print("i3: " + Integer.toBinaryString(i3));
char c = 0xffff; // max char hex value
print("c: " + Integer.toBinaryString(c));
byte b = 0x7f; // max byte hex value
print("b: " + Integer.toBinaryString(b));
short s = 0x7fff; // max short hex value
print("s: " + Integer.toBinaryString(s));
long n1 = 200L; // long suffix
long n2 = 200l; // long suffix (but can be confusing)
long n3 = 200;
float f1 = 1;
float f2 = 1F; // float suffix
float f3 = 1f; // float suffix
double d1 = 1d; // double suffix
double d2 = 1D; // double suffix
// (Hex and Octal also work with long)
}
}

<spoiler text="Output:">

i1: 101111
i2: 101111
i3: 1111111
c: 1111111111111111
b: 1111111
s: 111111111111111

</spoiler>
Последний символ обозначает тип записанного литерала. Прописная или строчная буква L определяет тип long (впрочем, строчная l может создать проблемы, потому что она похожа на цифру 1); прописная или строчная F соответствует типу float, а заглавная или строчная D подразумевает тип double.

Шестнадцатеричное представление (основание 16) работает со всеми встроенными типами данных и обозначается префиксом 0x или 0X с последующим числовым значением из цифр 0-9 и букв a-f, прописных или строчных. Если при определении переменной задается значение, превосходящее максимально для нее возможное (независимо от числовой формы), компилятор сообщит вам об ошибке. В программе указаны максимальные значения для типов char, byte и short. При выходе за эти границы компилятор автоматически сделает значение типом int и сообщит вам, что для присвоения понадобится сужающее приведение.

Восьмеричное представление (по основанию 8) обозначается начальным нулем в записи числа, состоящего из цифр 0 - 7. Для литеральной записи чисел в двоичном представлении в Java, C и C++ поддержки нет. Впрочем, при работе с шестнадцатеричныыми и восьмеричными числами часто требуется получить двоичное представление результата. Задача легко решается методами static toBinaryString() классов Integer и Long.

Экспоненциальная запись

Экспоненциальные значения записываются, по-моему, очень неудачно: 1.39e-47f. В науке и инженерном деле символом е обозначается основание натурального логарифма, равное примерно 2.718. (Более точное значение этой величины можно получить из свойства Math.E.)

Оно используется в экспоненциальных выражениях, таких как 1.39 * е exp47, что фактически значит 1.39 * 2.718 exp47. Однако во время изобретения языка FORTRAN было решено, что е будет обозначать «десять в степени», что достаточно странно, поскольку FORTRAN разрабатывался для науки и техники и можно было предположить, что его создатели обратят внимание на подобную неоднозначность.

Так или иначе, этот обычай был перенят в C, C++, а затем перешел в Java. Таким образом, если вы привыкли видеть в е основание натурального логарифма, вам придется каждый раз делать преобразование в уме: если вы увидели в Java выражение 1.39e-43f, на самом деле оно значит 1.39 * 10 exp43.

Если компилятор может определить тип автоматически, наличие завершающего суффикса типа не обязательно. В записи

 long n3 = 200;

не существует никаких неясностей, и поэтому использование символа L после значения 200 было бы излишним. Однако в записи

float f4 = 1e-43f; // десять в степени

компилятор обычно трактует экспоненциальные числа как double. Без завершающего символа f он сообщит вам об ошибке и необходимости использования приведения для преобразования double к типу float.

Поразрядные операторы

Поразрядные логические операторы

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

  • Поразрядный оператор И ( & ) заносит 1 в выходной бит, если оба входных бита были равны 1; в противном случае результат равен 0.
  • Поразрядный оператор ИЛИ ( | ) заносит 1 в выходной бит, если хотя бы один из битов операндов был равен 1; результат равен 0 только в том случае, если оба бита операндов были нулевыми.
  • Оператор ИСКЛЮЧАЮЩЕЕ ИЛИ, XOR, ( ^ ) имеет результатом единицу тогда, когда один из входных битов был единицей, но не оба вместе.
  • Поразрядный оператор НЕ ( ~ ), также называемый оператором двоичного дополнения, является унарным оператором, то есть имеет только один операнд. Поразрядное НЕ производит бит, «противоположный» исходному — если входящий бит является нулем, то в результирующем бите окажется единица, если входящий бит — единица, получится ноль.

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

Поразрядные операторы могут комбинироваться со знаком равенства =, чтобы совместить операцию и присвоение: &=, |= и ^= являются допустимыми сочетаниями. (Так как ~ является унарным оператором, он не может использоваться вместе со знаком "=".)

Тип boolean трактуется как однобитовый, поэтому операции с ним выглядят по-другому. Вы вправе выполнить поразрядные И, ИЛИ и ИСКЛЮЧАЮЩЕЕ ИЛИ, но НЕ использовать запрещено (видимо, чтобы предотвратить путаницу с логическим НЕ). Для типа boolean поразрядные операторы производят тот же эффект, что и логические, за одним исключением — они не поддерживают ускоренного вычисления.

Кроме того, в число поразрядных операторов для boolean входит оператор ИСКЛЮЧАЮЩЕЕ ИЛИ, отсутствующий в списке логических операторов. Для булевых типов не разрешается использование операторов сдвига, описанных в следующем разделе.

Операторы сдвига

Операторы сдвига также манипулируют битами и используются только с примитивными целочисленными типами. Оператор сдвига влево (<<) сдвигает влево операнд, находящийся слева от оператора, на количество битов, указанное после оператора. Оператор сдвига вправо (>>) сдвигает вправо операнд, находящийся слева от оператора, на количество битов, указанное после оператора. При сдвиге вправо используется заполнение знаком: при положительном значении новые биты заполняются нулями, а при отрицательном — единицами.

В Java также поддерживается беззнаковый сдвиг вправо (>>>), использующий заполнение нулями: независимо от знака старшие биты заполняются нулями. Такой оператор не имеет аналогов в C и C++.

Если сдвигаемое значение относится к типу char, byte или short, эти типы приводятся к int перед выполнением сдвига, и результат также получится int. При этом используется только пять младших битов с «правой» стороны. Таким образом, нельзя сдвинуть битов больше, чем вообще существует для целого числа int. Если вы проводите операции с числами long, то получите результаты типа long. При этом будет задействовано только шесть младших битов с «правой» стороны, что предотвращает использование излишнего числа битов.

Сдвиги можно совмещать со знаком равенства (<<=, или >>=, или >>>=). Именующее выражение заменяется им же, но с проведенными над ним операциями сдвига. Однако при этом возникает проблема с оператором беззнакового правого сдвига, совмещенного с присвоением. При использовании его с типом byte или short вы не получите правильных результатов. Вместо этого они сначала будут преобразованы к типу int и сдвинуты вправо, а затем обрезаны при возвращении к исходному типу, и результатом станет -1. Следующий пример демонстрирует это:

//: operators/URShift.java
// Проверка беззнакового сдвига вправо
import static net.mindview.util.Print.*;
 
public class URShift {
public static void main(String[] args) {
int i = -1;
print(Integer.toBinaryString(i));
i >>>= 10;
print(Integer.toBinaryString(i));
long l = -1;
print(Long.toBinaryString(l));
l >>>= 10;
print(Long.toBinaryString(l));
short s = -1;
print(Integer.toBinaryString(s));
s >>>= 10;
print(Integer.toBinaryString(s));
byte b = -1;
print(Integer.toBinaryString(b));
b >>>= 10;
print(Integer.toBinaryString(b));
b = -1;
print(Integer.toBinaryString(b));
print(Integer.toBinaryString(b>>>10));
}
}

<spoiler text="Output:">

11111111111111111111111111111111
1111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
1111111111111111111111

</spoiler>
В последней команде программы полученное значение не приводится обратно к b, поэтому получается верное действие.

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

//: operators/BitManipulation.java
// Использование поразрядных операторов.
import java.util.*;
import static net.mindview.util.Print.*;
 
public class BitManipulation {
public static void main(String[] args) {
Random rand = new Random(47);
int i = rand.nextInt();
int j = rand.nextInt();
printBinaryInt("-1", -1);
printBinaryInt("+1", +1);
int maxpos = 2147483647;
printBinaryInt("maxpos", maxpos);
int maxneg = -2147483648;
printBinaryInt("maxneg", maxneg);
printBinaryInt("i", i);
printBinaryInt("~i", ~i);
printBinaryInt("-i", -i);
printBinaryInt("j", j);
printBinaryInt("i & j", i & j);
printBinaryInt("i | j", i | j);
printBinaryInt("i ^ j", i ^ j);
printBinaryInt("i << 5", i << 5);
printBinaryInt("i >> 5", i >> 5);
printBinaryInt("(~i) >> 5", (~i) >> 5);
printBinaryInt("i >>> 5", i >>> 5);
printBinaryInt("(~i) >>> 5", (~i) >>> 5);
 
long l = rand.nextLong();
long m = rand.nextLong();
printBinaryLong("-1L", -1L);
printBinaryLong("+1L", +1L);
long ll = 922337207L;
printBinaryLong("maxpos", ll);
long lln = -922337208L;
printBinaryLong("maxneg", lln);
printBinaryLong("l", l);
printBinaryLong("~l", ~l);
printBinaryLong("-l", -l);
printBinaryLong("m", m);
printBinaryLong("l & m", l & m);
printBinaryLong("l | m", l | m);
printBinaryLong("l ^ m", l ^ m);
printBinaryLong("l << 5", l << 5);
printBinaryLong("l >> 5", l >> 5);
printBinaryLong("(~l) >> 5", (~l) >> 5);
printBinaryLong("l >>> 5", l >>> 5);
printBinaryLong("(~l) >>> 5", (~l) >>> 5);
}
static void printBinaryInt(String s, int i) {
print(s + ", int: " + i + ", binary:\n " +
Integer.toBinaryString(i));
}
static void printBinaryLong(String s, long l) {
print(s + ", long: " + l + ", binary:\n " +
Long.toBinaryString(l));
}
}

<spoiler text="Output:">

-1, int: -1, binary:
11111111111111111111111111111111
+1, int: 1, binary:
1
maxpos, int: 2147483647, binary:
1111111111111111111111111111111
maxneg, int: -2147483648, binary:
0
i, int: -1172028779, binary:
1
~i, int: 1172028778, binary:
101101010
-i, int: 1172028779, binary:
101101011
j, int: 1717241110, binary:
1100010110
i & j, int: 570425364, binary:
000010100
i | j, int: -25213033, binary:
1111110111
i ^ j, int: -595638397, binary:
1111000011
i << 5, int: 1149784736, binary:
010100000
i >> 5, int: -36625900, binary:
11111010100
(~i) >> 5, int: 36625899, binary:
1011
i >>> 5, int: 97591828, binary:
010100
(~i) >>> 5, int: 36625899, binary:
1011

</spoiler>
Два метода в конце, printBinaryInt() и printBinaryLong(), получают в качестве параметров, соответственно, числа int и long и выводят их в двоичном формате вместе с сопроводительным текстом. Вместе с демонстрацией поразрядных опе­раций для типов int и long этот пример также выводит минимальное и максимальное значение, +1 и -1 для этих типов, чтобы вы лучше понимали, как они выглядят в двоичном представлении. Заметьте, что старший бит обозначает знак: 0 соответствует положительному и 1 — отрицательному числам. Результат работы для типа int приведен в конце листинга

Тернарный оператор «если-иначе»

Тернарный оператор необычен тем, что он использует три операнда. И все же это действительно оператор, так как он производит значение, в отличие от обычной конструкции выбора if-else, описанной в следующем разделе. Выражение записывается в такой форме:

логическое-условие ? выражение0 : выражение1

Если логическое-условие истинно (true), то затем вычисляется выражение0, и именно его результат становится результатом выполнения всего оператора. Если же логическое-условие ложно (false), то вычисляется выражение1, и его значение становится результатом работы оператора.
Пример использования тернарного оператора:

 //: operators/TernaryIfElse.java
import static net.mindview.util.Print.*;
 
public class TernaryIfElse {
static int ternary(int i) {
return i < 10 ? i * 100 : i * 10;
}
static int standardIfElse(int i) {
if(i < 10)
return i * 100;
else
return i * 10;
}
public static void main(String[] args) {
print(ternary(9));
print(ternary(10));
print(standardIfElse(9));
print(standardIfElse(10));
}
}

<spoiler text="Output:">

900
100
900

</spoiler>
Конечно, здесь можно было бы использовать стандартную конструкцию if-else (описываемую чуть позже), но тернарный оператор гораздо компактнее. Хотя C (где этот оператор впервые появился) претендует на звание лаконичного языка, и тернарный оператор вводился отчасти для достижения этой цели, будьте благоразумны и не используйте его всюду и постоянно — он может ухудшить читаемость программы.

Операторы + и += для String

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

При создании C++ в язык была добавлена возможность перегрузки операторов, позволяющей программистам C++ изменять и расширять смысл почти любого оператора. К сожалению, перегрузка операторов, в сочетании с некоторыми ограничениями C++, создала немало проблем при проектировании классов. Хотя реализацию перегрузки операторов в Java можно было осуществить проще, чем в C++ (это доказывает язык C#, где существует простой механиз перегрузки), эту возможность все же посчитали излишне сложной, и поэтому программистам на Java не дано реализовать свои собственные перегруженные операторы, как это делают программисты на C++.

Использование + и += для строк (String) имеет интересные особенности. Если выражение начинается строкой, то все последующие операнды также будут предварительно преобразованы в строчное представление! (помните, что компилятор превращает символы в кавычках в объект String).

 int х = 0; у = 1; z = 2;
String s = "х, у, z";
System.out.println(s + x + у + z),

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

 System.out.println(x + s);

то и здесь Java преобразует x в строку.

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

Многие программисты склонны второпях записывать выражение без скобок, даже когда они не уверены в последовательности вычисления выражения. Это верно и для Java.

Еще одна распространенная ошибка в C и C++ выглядит следующим образом:

while(x = у) { // .
}

Программист хотел выполнить сравнение (==), а не присвоение. В C и C++ результат этого выражения всегда будет истинным, если только у не окажется нулем; вероятно, возникнет бесконечный цикл. В языке Java результат такого выражения не будет являться логическим типом (boolean), а компилятор ожидает в этом выражении именно boolean и не разрешает использовать целочисленный тип int, поэтому вовремя сообщит вам об ошибке времени компиляции, упредив проблему еще перед запуском программы. Поэтому подобная ошибка в Java никогда не происходит. (Программа откомпилируется только в одном случае: если х и у одновременно являются типами boolean, и тогда выражение х = у будет допустимо, что может привести к ошибке.)

Похожая проблема возникает в C и C++ при использовании поразрядных операторов И и ИЛИ вместо их логических аналогов. Поразрядные И и ИЛИ записываются одним символом (& и |), в то время как логические И и ИЛИ требуют в написании двух символов (&& и ||). Так же, как и в случае с операторами = и ==, легко ошибиться и набрать один символ вместо двух. В Java компилятор предотвращает такие ошибки, так как он не позволяет использовать тип данных в неподходящем контексте.

Операторы приведения

Слово приведение используется в смысле «приведение к другому типу». В определенных ситуациях Java самостоятельно преобразует данные к другим типам. Например, если вещественной переменной присваивается целое значение, ком­пилятор автоматически выполняет соответствующее преобразование (int преобразуется во float). Приведение позволяет сделать замену типа более очевидной или выполнить ее принудительно в случаях, где это не происходит в обычном порядке.

Чтобы выполнить приведение явно, запишите необходимый тип данных (включая все модификаторы) в круглых скобках слева от преобразуемого значения. Пример:

//: operators/Casting.java
public class Casting {
public static void main(String[] args) {
int i = 200;
long lng = (long)i;
lng = i; // "Расширение", явное преобразование не обязательно
long lng2 = (long)200;
lng2 = 200;
// "Сужающее" преобразование:
i = (int)lng2; // Преобразование необходимо
}
}

Как видите, приведение может выполняться и для чисел, и для переменных. Впрочем, в указанных примерах приведение является излишним, поскольку компилятор при необходимости автоматически преобразует целое int к типу long. Однако это не мешает вам выполнять необязательные приведения — например, чтобы подчеркнуть какое-то обстоятельство или просто для того, чтобы сделать программу более понятной. В других ситуациях приведение может быть необходимо для нормальной компиляции программы.

В C и C++ приведение могло стать источником ошибок и неоднозначности. В Java приведение безопасно, за одним исключением: при выполнении так называемого сужающего приведения (то есть от типа данных, способного хранить больше информации, к менее содержательному типу данных), то есть при опасности потери данных. В таком случае компилятор заставляет вас выполнить явное приведение; фактически он говорит: «это может быть опасно, но, если вы уверены в своей правоте, опишите действие явно». В случае с расширяющим приведением явное описание не понадобится, так как новый тип данных способен хранить больше информации, чем прежний, и поэтому потеря данных исключена.

В Java разрешается приводить любой простейший тип данных к любому другому простейшему типу, но это не относится к типу boolean, который вообще не подлежит приведению. Классы также не поддерживают произвольное приведение. Чтобы преобразовать один класс в другой, требуются специальные методы. (Как будет показано позднее, объекты можно преобразовывать в рамках семейства типов; объект Дуб можно преобразовать в Дерево и наоборот, но не к постороннему типу вроде Камня.)

Округление и усечение

При выполнении сужающих преобразований необходимо обращать внимание на усечение и округление данных. Например, как должен действовать компилятор Java при преобразовании вещественного числа в целое? Скажем, если значение 29.7 приводится к типу int, что получится — 29 или 30 ?

Ответ на этот вопрос может дать следующий пример:

//: operators/CastingNumbers.java
// Что происходит при приведении типов
// float или double к целочисленным значениям?
import static net.mindview.util.Print.*;
 
public class CastingNumbers {
public static void main(String[] args) {
double above = 0.7, below = 0.4;
float fabove = 0.7f, fbelow = 0.4f;
print("(int)above: " + (int)above);
print("(int)below: " + (int)below);
print("(int)fabove: " + (int)fabove);
print("(int)fbelow: " + (int)fbelow);
}
}

<spoiler text="Output:">

(int)above: 0
(int)below: 0
(int)fabove: 0
(int)fbelow: 0

</spoiler>
Отсюда и ответ на наш вопрос — приведение от типов с повышенной точностью double и float к целочисленным значениям всегда осуществляется с усечением целой части. Если вы предпочитаете, чтобы результат округлялся, используйте метод round() из java.lang.Math. Так как этот метод является частью java.lang, дополнительное импортирование не потребуется.

Повышение

Вы можете обнаружить, что при проведении любых математических и поразрядных операций примитивные типы данных, меньшие int (то есть char, byte и short), приводятся к типу int перед проведением операций, и получаемый результат имеет тип int. Поэтому, если вам снова понадобится присвоить его меньшему типу, придется использовать приведение. (И тогда возможна потеря информации.) В основном самый емкий тип данных, присутствующий в выражении, и определяет величину результата этого выражения; так, при перемножении float и double результатом станет double, а при сложении long и int вы получите в результате long.

В Java отсутствует sizeof()

В C и C++ оператор sizeof() выдает количество байтов, выделенных для хранения данных. Главная причина для использования sizeof() — переносимость программы. Различным типам данных может отводиться различное количество памяти на разных компьютерах, поэтому для программиста важно определить размер этих типов перед проведением операций, зависящих от этих величин. Например, один компьютер выделяет под целые числа 32 бита, а другой — всего лишь 16 бит. В результате на первой машине программа может хранить в целочисленном представлении числа из большего диапазона. Конечно, аппаратная совместимость создает немало хлопот для программистов на C и C++.
В Java оператор sizeof() не нужен, так как все типы данных имеют одинаковые размеры на всех машинах. Вам не нужно заботиться о переносимости на низком уровне — она встроена в язык.

Сводка операторов

Следующий пример показывает, какие примитивные типы данных используются с теми или иными операторами. Вообще-то это один и тот же пример, повторенный много раз, но для разных типов данных. Файл должен компилироваться без ошибок, поскольку все строки, содержащие неверные операции, предварены символами //! :

//: operators/AllOps.java
// Проверяет все операторы со всеми
// примитивными типами данных, чтобы показать,
// какие операции допускаются компилятором Java
 
public class AllOps {
// To accept the results of a boolean test:
void f(boolean b) {}
void boolTest(boolean x, boolean y) {
// Arithmetic operators:
//! x = x * y;
//! x = x / y;
//! x = x % y;
//! x = x + y;
//! x = x - y;
//! x++;
//! x--;
//! x = +y;
//! x = -y;
// Relational and logical:
//! f(x > y);
//! f(x >= y);
//! f(x < y);
//! f(x <= y);
f(x == y);
f(x != y);
f(!y);
x = x && y;
x = x || y;
// Bitwise operators:
//! x = ~y;
x = x & y;
x = x | y;
x = x ^ y;
//! x = x << 1;
//! x = x >> 1;
//! x = x >>> 1;
// Compound assignment:
//! x += y;
//! x -= y;
//! x *= y;
//! x /= y;
//! x %= y;
//! x <<= 1;
//! x >>= 1;
//! x >>>= 1;
x &= y;
x ^= y;
x |= y;
// Casting:
//! char c = (char)x;
//! byte b = (byte)x;
//! short s = (short)x;
//! int i = (int)x;
//! long l = (long)x;
//! float f = (float)x;
//! double d = (double)x;
}
void charTest(char x, char y) {
// Arithmetic operators:
x = (char)(x * y);
x = (char)(x / y);
x = (char)(x % y);
x = (char)(x + y);
x = (char)(x - y);
x++;
x--;
x = (char)+y;
x = (char)-y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
x= (char)~y;
x = (char)(x & y);
x = (char)(x | y);
x = (char)(x ^ y);
x = (char)(x << 1);
x = (char)(x >> 1);
x = (char)(x >>> 1);
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
x <<= 1;
x >>= 1;
x >>>= 1;
x &= y;
x ^= y;
x |= y;
// Casting:
//! boolean bl = (boolean)x;
byte b = (byte)x;
short s = (short)x;
int i = (int)x;
long l = (long)x;
float f = (float)x;
double d = (double)x;
}
void byteTest(byte x, byte y) {
// Arithmetic operators:
x = (byte)(x* y);
x = (byte)(x / y);
x = (byte)(x % y);
x = (byte)(x + y);
x = (byte)(x - y);
x++;
x--;
x = (byte)+ y;
x = (byte)- y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
x = (byte)~y;
x = (byte)(x & y);
x = (byte)(x | y);
x = (byte)(x ^ y);
x = (byte)(x << 1);
x = (byte)(x >> 1);
x = (byte)(x >>> 1);
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
x <<= 1;
x >>= 1;
x >>>= 1;
x &= y;
x ^= y;
x |= y;
// Casting:
//! boolean bl = (boolean)x;
char c = (char)x;
short s = (short)x;
int i = (int)x;
long l = (long)x;
float f = (float)x;
double d = (double)x;
}
void shortTest(short x, short y) {
// Arithmetic operators:
x = (short)(x * y);
x = (short)(x / y);
x = (short)(x % y);
x = (short)(x + y);
x = (short)(x - y);
x++;
x--;
x = (short)+y;
x = (short)-y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
x = (short)~y;
x = (short)(x & y);
x = (short)(x | y);
x = (short)(x ^ y);
x = (short)(x << 1);
x = (short)(x >> 1);
x = (short)(x >>> 1);
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
x <<= 1;
x >>= 1;
x >>>= 1;
x &= y;
x ^= y;
x |= y;
// Casting:
//! boolean bl = (boolean)x;
char c = (char)x;
byte b = (byte)x;
int i = (int)x;
long l = (long)x;
float f = (float)x;
double d = (double)x;
}
void intTest(int x, int y) {
// Arithmetic operators:
x = x * y;
x = x / y;
x = x % y;
x = x + y;
x = x - y;
x++;
x--;
x = +y;
x = -y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
x = ~y;
x = x & y;
x = x | y;
x = x ^ y;
x = x << 1;
x = x >> 1;
x = x >>> 1;
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
x <<= 1;
x >>= 1;
x >>>= 1;
x &= y;
x ^= y;
x |= y;
// Casting:
//! boolean bl = (boolean)x;
char c = (char)x;
byte b = (byte)x;
short s = (short)x;
long l = (long)x;
float f = (float)x;
double d = (double)x;
}
void longTest(long x, long y) {
// Arithmetic operators:
x = x * y;
x = x / y;
x = x % y;
x = x + y;
x = x - y;
x++;
x--;
x = +y;
x = -y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
x = ~y;
x = x & y;
x = x | y;
x = x ^ y;
x = x << 1;
x = x >> 1;
x = x >>> 1;
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
x <<= 1;
x >>= 1;
x >>>= 1;
x &= y;
x ^= y;
x |= y;
// Casting:
//! boolean bl = (boolean)x;
char c = (char)x;
byte b = (byte)x;
short s = (short)x;
int i = (int)x;
float f = (float)x;
double d = (double)x;
}
void floatTest(float x, float y) {
// Arithmetic operators:
x = x * y;
x = x / y;
x = x % y;
x = x + y;
x = x - y;
x++;
x--;
x = +y;
x = -y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
//! x = ~y;
//! x = x & y;
//! x = x | y;
//! x = x ^ y;
//! x = x << 1;
//! x = x >> 1;
//! x = x >>> 1;
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
//! x <<= 1;
//! x >>= 1;
//! x >>>= 1;
//! x &= y;
//! x ^= y;
//! x |= y;
// Casting:
//! boolean bl = (boolean)x;
char c = (char)x;
byte b = (byte)x;
short s = (short)x;
int i = (int)x;
long l = (long)x;
double d = (double)x;
}
void doubleTest(double x, double y) {
// Arithmetic operators:
x = x * y;
x = x / y;
x = x % y;
x = x + y;
x = x - y;
x++;
x--;
x = +y;
x = -y;
// Relational and logical:
f(x > y);
f(x >= y);
f(x < y);
f(x <= y);
f(x == y);
f(x != y);
//! f(!x);
//! f(x && y);
//! f(x || y);
// Bitwise operators:
//! x = ~y;
//! x = x & y;
//! x = x | y;
//! x = x ^ y;
//! x = x << 1;
//! x = x >> 1;
//! x = x >>> 1;
// Compound assignment:
x += y;
x -= y;
x *= y;
x /= y;
x %= y;
//! x <<= 1;
//! x >>= 1;
//! x >>>= 1;
//! x &= y;
//! x ^= y;
//! x |= y;
// Casting:
//! boolean bl = (boolean)x;
char c = (char)x;
byte b = (byte)x;
short s = (short)x;
int i = (int)x;
long l = (long)x;
float f = (float)x;
}
}

Заметьте, что действия с типом boolean довольно ограничены. Ему можно присвоить значение true или false, проверить на истинность или ложность, но нельзя добавить логические переменные к другим типам или произвести с ними любые иные операции.

В случае с типами char, byte и short можно заметить эффект повышения при использовании арифметических операторов. Любая арифметическая операция с этими типами дает результат типа int, который затем нужно явно приводить к изначальному типу (сужающее приведение, при котором возможна потеря информации). При использовании значений типа int приведение осуществлять не придется, потому что все значения уже имеют этот тип.

Однако не заблуждайтесь относительно безопасности происходящего. При перемножении двух достаточно больших целых чисел int произойдет переполнение. Следующий пример демонстрирует сказанное:

//: operators/Overflow.java
// Сюрприз! В Java можно получить переполнение.
public class Overflow {
public static void main(String[] args) {
int big = Integer.MAX_VALUE;
System.out.println("большое = " + big);
int bigger = big * 4;
System.out.println("еще больше = " + bigger);
}
}

<spoiler text="Output:">

большое = 2147483647
еще больше = -4

</spoiler>
Компилятор не выдает никаких ошибок или предупреждений, и во время исполнения не возникнет исключений. Язык Java хорош, но хорош не настолько.

Совмещенное присваивание не требует приведения для типов char, byte, short, хотя для них и производится повышение, как и в случае с арифметическими операциями. С другой стороны, отсутствие приведения в таких случаях, несомненно, упрощает программу.

Можно легко заметить, что за исключением типа boolean, любой примитивный тип может быть преобразован к другому примитиву. Как упоминалось ранее, необходимо остерегаться сужающего приведения при преобразованиях к меньшему типу, так как при этом возникает риск потери информации.

Резюме

Читатели с опытом работы на любом языке семейства C могли убедиться, что операторы Java почти нйчем не отличаются от классических. Если же материал этой главы показался трудным, обращайтесь к мультимедийной презентации «Thinking in C» (www.MindView.net).

]]>
Книги по Java https://linexp.ru?id=4749 Wed, 29 Jun 2022 14:30:47 GMT
<![CDATA[Глава 4 Thinking in Java 4th edition]]> УПРАВЛЯЮЩИЕ КОНСТРУКЦИИПодобно любому живому существу, программа должна управлять своим миром и принимать решения во время исполнения. В языке Java для принятия решений используются управляющие конструкции. В Java задействованы все управляющие конструкции языка C, поэтому читателям с опытом программирования на языке C или C++ основная часть материала будет знакома. Почти во всех процедурных языках поддерживаются стандартные команды управления, и во многих языках они совпадают. В Java к их числу относятся ключевые слова if-else, while, do-while, for, а также команда выбора switch. Однако в Java не поддерживается часто критикуемый оператор goto (который, впрочем, все же является самым компактным решением в некоторых ситуациях). Безусловные переходы «в стиле» goto возможны, но гораздо более ограничены по сравнению с классическими переходами goto.

УПРАВЛЯЮЩИЕ КОНСТРУКЦИИ

Подобно любому живому существу, программа должна управлять своим миром и принимать решения во время исполнения. В языке Java для принятия решений используются управляющие конструкции.
В Java задействованы все управляющие конструкции языка C, поэтому читателям с опытом программирования на языке C или C++ основная часть материала будет знакома. Почти во всех процедурных языках поддерживаются стандартные команды управления, и во многих языках они совпадают. В Java к их числу относятся ключевые слова if-else, while, do-while, for, а также команда выбора switch. Однако в Java не поддерживается часто критикуемый оператор goto (который, впрочем, все же является самым компактным решением в некоторых ситуациях). Безусловные переходы «в стиле» goto возможны, но гораздо более ограничены по сравнению с классическими переходами goto.

true и false

Все конструкции с условием вычисляют истинность или ложность условного выражения, чтобы определить способ выполнения. Пример условного выражения — А == В. Оператор сравнения == проверяет, равно ли значение А значению В. Результат проверки может быть истинным (true) или ложным (false). Любой из описанных в этой главе операторов сравнения может применяться в условном выражении. Заметьте, что Java не разрешает использовать числа в качестве логических значений, хотя это позволено в C и C++ (где не-ноль считается «истинным», а ноль — «ложным»). Если вам потребуется использовать числовой тип там, где требуется boolean (скажем, в условии if(a)), сначала придется его преобразовать к логическому типу оператором сравнения в условном выражении — например, if (а != 0).

if-else

Команда if-else является, наверное, наиболее распространенным способом передачи управления в программе. Присутствие ключевого слова else не обязательно, поэтому конструкция if существует в двух формах:

if(логическое выражение) команда

и

if(логическое выражение) 
команда
else
команда

Условие должно дать результат типа boolean. В секции команда располагается либо простая команда, завершенная точкой с запятой, либо составная конструкция из команд, заключенная в фигурные скобки.
В качестве примера применения if-else представлен метод test(), который выдает информацию об отношениях между двумя числами — «больше», «меньше» или «равно»:

//: control/IfElse.java
import static net.mindview.util.Print.*;
 
public class IfElse {
static int result = 0;
static void test(int testval, int target) {
if(testval > target)
result = +1;
else if(testval < target)
result = -1;
else
result = 0; // равные числа
}
 
public static void main(String[] args) {
test(10, 5);
print(result);
test(5, 10);
print(result);
test(5, 5);
print(result);
}
}

<spoiler text="Output:">

1
-1
0

</spoiler>
Внутри метода test() встречается конструкция else if; это не новое ключевое слово, a else, за которым следует начало другой команды — if.
Java, как и C с C++, относится к языкам со свободным форматом. Тем не менее в командах управления рекомендуется делать отступы, благодаря чему читателю программы будет легче понять, где начинается и заканчивается управляющая конструкция.

Циклы

while

Конструкции while, do-while и for управляют циклами и иногда называются циклическими командами. Команда повторяется до тех пор, пока управляющее логическое выражение не станет ложным. Форма цикла while следующая:

while(логическое выражение) команда

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

//: control/WhileTest.java
// Пример использования цикла while
public class WhileTest {
static boolean condition() {
boolean result = Math.random() < 0.99;
System.out.print(result + ", ");
return result;
}
public static void main(String[] args) {
while(condition())
System.out.println("Inside 'while'");
System.out.println("Exited 'while'");
}
} /* (Выполните, чтобы просмотреть результат) */

В примере используется статический метод random() из библиотеки Math, который генерирует значение double, находящееся между 0 и 1 (включая 0, но не 1). Условие while означает: «повторять, пока condition() возвращает true». При каждом запуске программы будет выводаться различное количество чисел.

do-while

Форма конструкции do-while такова:

do
команда
while(логическое выражение);

Единственное отличие цикла do-while от while состоит в том, что цикл do-while выполняется по крайней мере единожды, даже если условие изначально ложно. В цикле while, если условие изначально ложно, тело цикла никогда не от­рабатывает. На практике конструкция do-while употребляется реже, чем while.

for

Пожалуй, конструкции for составляют наиболее распространенную разновидность циклов. Цикл for проводит инициализацию перед первым шагом цикла. Затем выполняется проверка условия цикла, и в конце каждой итерации
осуществляется некое «приращение» (обычно изменение управляющей переменной). Цикл for записывается следующим образом:

for(инициализация; логическое выражение; шаг)
команда

Любое из трех выражений цикла (инициализация, логическое выражение или шаг) можно пропустить. Перед выполнением каждого шага цикла проверяется условие цикла; если оно окажется ложно, выполнение продолжается с инструкции, следующей за конструкцией for. В конце каждой итерации выполняется секция шаг.
Цикл for обычно используется для «счетных» задач:

//: control/ListCharacters.java
// Пример использования цикла "for": перебор
// всех ASCII-символов нижнего регистра
public class ListCharacters {
public static void main(String[] args) {
for(char c = 0; c < 128; c++)
if(Character.isLowerCase(c))
System.out.println("value: " + (int)c +
" character: " + c);
}
}

<spoiler text="Output:">

value: 97 character: a
value: 98 character: b
value: 99 character: c
value: 100 character: d
value: 101 character: e
value: 102 character: f
value: 103 character: g
value: 104 character: h
value: 105 character: i
value: 106 character: j

</spoiler>
Обратите внимание, что переменная c определяется в точке ее использования, в управляющем выражении цикла for, а не в начале блока, обозначенного фигурными скобками. Область действия для c — все выражения, принадлежащие циклу.
В программе также используется класс-«обертка» java.Lang.Character, который не только позволяет представить простейший тип char в виде объекта, но и содержит ряд дополнительных возможностей. В нашем примере используется статический метод этого класса isLowerCase(), который проверяет, является ли некоторая буква строчной.
Традиционные процедурные языки (такие, как C) требовали, чтобы все переменные определялись в начале блока цикла, чтобы компилятор при создании блока мог выделить память под эти переменные. В Java и C++ переменные разрешено объявлять в том месте блока цикла, где это необходимо. Это позволяет программировать в более удобном стиле и упрощает понимание кода.

Оператор-запятая

Ранее в этой главе уже упоминалось о том, что оператор «запятая» (но не запятая-разделитель, которая разграничивает определения и аргументы функций) может использоваться в Java только в управляющем выражении цикла for. И в секции инициализации цикла, и в его управляющем выражении можно записать несколько команд, разделенных запятыми; они будут обработаны последовательно.
Оператор «запятая» позволяет определить несколько переменных в цикле for, но все эти переменные должны принадлежать к одному типу:

//: control/CommaOperator.java
public class CommaOperator {
public static void main(String[] args) {
for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {
System.out.println("i = " + i + " j = " + j);
}
}
}

<spoiler text="Output:">

i = 1 j = 11
i = 2 j = 4
i = 3 j = 6
i = 4 j = 8

</spoiler>
Определение int в заголовке for относится как к і, так и к j. Инициализацонная часть может содержать любое количество определений переменных одного типа. Определение переменных в управляющих выражениях возможно только в цикле for. На другие команды выбора или циклов этот подход не распространяется.

Синтаксис foreach

В Java SE5 появилась новая, более компактная форма for для перебора элементов массивов и контейнеров (см. далее). Эта упрощенная форма, называемая синтаксисом foreach, не требует ручного изменения служебной переменной для перебора последовательности объектов — цикл автоматически представляет очередной элемент.
Следующая программа создает массив float, после чего перебирает все его элементы:

//: control/ForEachFloat.java
import java.util.*;
 
public class ForEachFloat {
public static void main(String[] args) {
Random rand = new Random(47);
float f[] = new float[10];
for(int i = 0; i < 10; i++)
f[i] = rand.nextFloat();
for(float x : f)
System.out.println(x);
}
}

<spoiler text="Output:">

0.72711575
0.39982635
0.5309454
0.0534122
0.16020656
0.57799757
0.18847865
0.4170137
0.51660204
0.73734957

</spoiler>
Массив заполняется уже знакомым циклом for, потому что для его заполнения должны использоваться индексы. Упрощенный синтаксис используется в следующей команде:

for(float x: f)

Эта конструкция определяет переменную х типа float, после чего последовательно присваивает ей элементы f.
Любой метод, возвращающий массив, может использоваться с данной разновидностью for. Например, класс String содержит метод toCharArray(), возвращающий массив char; следовательно, перебор символов строки может осуществляться так:

//: control/ForEachString.java
public class ForEachString {
public static void main(String[] args) {
for(char c : "An African Swallow".toCharArray() )
System.out.print(c + " ");
}
}

<spoiler text="Output:">

A n   A f r i c a n   S w a l l o w

</spoiler>
Как будет показано далее, «синтаксис foreach» также работает для любого объекта, поддерживающего интерфейс Iterable.
Многие команды for основаны на переборе серии целочисленных значений:

 for (int і = 0; і < 100; і++)

В таких случаях «синтаксис foreach» работать не будет, если только вы предварительно не создадите массив int. Для упрощения этой задачи я включил в библиотеку net.mindview.util.Range метод range(), который автоматически генерирует соответствующий массив:

//: control/ForEachInt.java
import static net.mindview.util.Range.*;
import static net.mindview.util.Print.*;
 
public class ForEachInt {
public static void main(String[] args) {
for(int i : range(10)) // 0..9
printnb(i + " ");
print();
for(int i : range(5, 10)) // 5..9
printnb(i + " ");
print();
for(int i : range(5, 20, 3)) // 5..20 step 3
printnb(i + " ");
print();
}
}

<spoiler text="Output:">

0 1 2 3 4 5 6 7 8 9
5 6 7 8 9
5 8 11 14 17

</spoiler>
Обратите внимание на использование printnb() вместо print(). Метод printnb() не выводит символ новой строки, что позволяет построить строку по фрагментам.

return

Следующая группа ключевых слов обеспечивает безусловный переход, то есть передачу управления без проверки каких-либо условий. К их числу относятся команды return, break и continue, а также конструкция перехода по метке, аналогичная goto в других языках.
У ключевого слова return имеется два предназначения: оно указывает, какое значение возвращается методом (если только он не возвращает тип void), а также используется для немедленного выхода из метода. Метод test() из предыдущего примера можно переписать так, чтобы он воспользовался новыми возможностями:

//: control/IfElse2.java
import static net.mindview.util.Print.*;
 
public class IfElse2 {
static int test(int testval, int target) {
if(testval > target)
return +1;
else if(testval < target)
return -1;
else
return 0; // Одинаковые значения
}
public static void main(String[] args) {
print(test(10, 5));
print(test(5, 10));
print(test(5, 5));
}
}

<spoiler text="Output:">

1
-1
0

</spoiler>
В данном случае секция else не нужна, поскольку работа метода не продолжается после выполнения инструкции return.

Если метод, возвращающий void, не содержит команды return, такая команда неявно выполняется в конце метода. Тем не менее, если метод возвращает любой тип, кроме void, проследите за тем, чтобы каждая логическая ветвь возвращала конкретное значение.

break и continue

В теле любого из циклов вы можете управлять потоком программы, используя специальные ключевые слова break и continue. Команда break завершает цикл, при этом оставшиеся операторы цикла не выполняются. Команда continue оста­навливает выполнение текущей итерации цикла и переходит к началу цикла, чтобы начать выполнение нового шага.
Следующая программа показывает пример использования команд break и continue внутри циклов for и while:

//: control/BreakAndContinue.java
// Применение ключевых слов break и continue.
import static net.mindview.util.Range.*;
 
public class BreakAndContinue {
public static void main(String[] args) {
for(int i = 0; i < 100; i++) {
if(i == 74) break; // Выход из цикла
if(i % 9 != 0) continue; // Следующая итерация
System.out.print(i + " ");
}
System.out.println();
// Использование foreach:
for(int i : range(100)) {
if(i == 74) break; // Выход из цикла
if(i % 9 != 0) continue; // Следующая итерация
System.out.print(i + " ");
}
System.out.println();
int i = 0;
// "Бесконечный цикл":
while(true) {
i++;
int j = i * 27;
if(j == 1269) break; // Выход из цикла
if(i % 10 != 0) continue; // Возврат в начало цикла
System.out.print(i + " ");
}
}
}

<spoiler text="Output:">

0 9 18 27 36 45 54 63 72
0 9 18 27 36 45 54 63 72
10 20 30 40

</spoiler>
В цикле for переменная і никогда не достигает значения 100 — команда break прерывает цикл, когда значение переменной становится равным 74. Обычно break используется только тогда, когда вы точно знаете, что условие выхода из цикла действительно достигнуто. Команда continue переводит исполнение в начало цикла (и таким образом увеличивает значение і), когда і не делится без остатка на 9. Если деление производится без остатка, значение выводится на экран.

Второй цикл for демонстрирует использование «синтаксиса foreach» с тем же результатом.

Последняя часть программы демонстрирует «бесконечный цикл», который теоретически должен исполняться вечно. Однако в теле цикла вызывается команда break, которая и завершает цикл. Команда continue переводит исполнение к началу цикла, и при этом остаток цикла не выполняется. (Таким образом, вывод на экран в последнем цикле происходит только в том случае, если значение і делится на 10 без остатка.) Значение 0 выводится, так как 0 % 9 дает в результате 0.

Вторая форма бесконечного цикла — for(;;). Компилятор реализует конструкции while(true) и for(;;) одинаково, так что выбор является делом вкуса.

Нехорошая команда goto

Ключевое слово goto появилось одновременно с языками программирования. Действительно, безусловный переход заложил основы принятия решений в языке ассемблера: «если условие А, перейти туда, а иначе перейти сюда». Если вам доводилось читать код на ассемблере, который генерируют фактически все компиляторы, наверняка вы замечали многочисленные переходы, управляющие выполнением программы (компилятор Java производит свой собственный «ассемблерный» код, но последний выполняется виртуальной-машиной Java, а не аппаратным процессором).
Команда goto реализует безусловный переход на уровне исходного текста программы, и именно это обстоятельство принесло ей дурную славу. Если программа постоянно «прыгает» из одного места в другое, нет ли способа реорганизовать ее код так, чтобы управление программой перестало быть таким «прыгучим»? Команда goto впала в настоящую немилость с опубликованием знаменитой статьи Эдгара Дейкстры «Команда GOTO вредна» (Goto considered harmful), их тех пор порицание команды goto стало чуть ли не спортом, а защитники репутации многострадального оператора разбежались по укромным углам.
Как всегда в ситуациях такого рода, существует «золотая середина». Проблема состоит не в использовании goto вообще, но в злоупотреблении — все же иногда именно оператор goto позволяет лучше всего организовать управление программой.
Хотя слово goto зарезервировано в языке Java, оно там не используется; Java не имеет команды goto. Однако существует механизм, чем-то похожий на безусловный переход и осуществляемый командами break и continue. Скорее, это способ прервать итерацию цикла, а не передать управление в другую точку программы. Причина его обсуждения вместе с goto состоит в том, что он использует тот же механизм — метки.

Метка

Метка представляет собой идентификатор с последующим двоеточием:

label1:

Единственное место, где в Java метка может оказаться полезной, — прямо перед телом цикла. Причем никаких дополнительных команд между меткой и телом цикла быть не должно. Причина помещения метки перед телом цикла может быть лишь одна — вложение внутри цикла другого цикла или конструкции выбора. Обычные версии break и continue прерывают только текущий цикл, в то время как их версии с метками способны досрочно завершать циклы и передавать выполнение в точку, адресуемую меткой:

label1:
внешний-цикл {
внутренний-цикл {
// ...
break; // 1
// ...
continue; // 2
//...
continue label1; // З
//...
break label1; // 4
}
}

В первом случае (1) команда break прерывает выполнение внутреннего цикла, и управление переходит к внешнему циклу. Во втором случае (2) оператор continue передает управление к началу внутреннего цикла. Но в третьем варианте (3) команда continue label1 влечет выход из внутреннего и внешнего циклов и возврат к метке label1. Далее выполнение цикла фактически продолжается, но с внешнего цикла. В четвертом случае (4) команда break label1 также вызывает переход к метке label1, но на этот раз повторный вход в итерацию не происходит. Это действие останавливает выполнение обоих циклов. Пример использования цикла for с метками:

//: control/LabeledFor.java
// Цикл for с метками
import static net.mindview.util.Print.*;
 
public class LabeledFor {
public static void main(String[] args) {
int i = 0;
outer: // Другие команды недопустимы
for(; true ;) { // infinite loop
inner: // Другие команды недопустимы
for(; i < 10; i++) {
print("i = " + i);
if(i == 2) {
print("continue");
continue;
}
if(i == 3) {
print("break");
i++; // В противном случае значение і
// не увеличивается.
break;
}
if(i == 7) {
print("continue outer");
i++; // В противном случае значение і
// не увеличивается.
continue outer;
}
if(i == 8) {
print("break outer");
break outer;
}
for(int k = 0; k < 5; k++) {
if(k == 3) {
print("continue inner");
continue inner;
}
}
}
}
// Использовать break или continue
// с метками, здесь не разрешается
}
}

<spoiler text="Output:">

i = 0
continue inner
i = 1
continue inner
i = 2
continue
i = 3
break
i = 4
continue inner
i = 5
continue inner
i = 6
continue inner
i = 7
continue outer
i = 8

</spoiler>
Заметьте, что оператор break завершает цикл for, вследствие этого выражение с инкрементом не выполняется до завершения очередного шага. Поэтому из-за пропуска операции инкремента в цикле переменная непосредственно уве­личивается на единицу, когда і == 3. При выполнении условия і == 7 команда continue outer переводит выполнение на начало цикла; инкремент опять пропускается, поэтому и в этом случае переменная увеличивается явно.

Без команды break outer программе не удалось бы покинуть внешний цикл из внутреннего цикла, так как команда break сама по себе завершает выполнение только текущего цикла (это справедливо и для continue).

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

Теперь рассмотрим пример, в котором используются команды break и continue с метками в цикле while:

//: control/LabeledWhile.java
// Цикл while с метками
import static net.mindview.util.Print.*;
 
public class LabeledWhile {
public static void main(String[] args) {
int i = 0;
outer:
while(true) {
print("Внешний цикл while");
while(true) {
i++;
print("i = " + i);
if(i == 1) {
print("continue");
continue;
}
if(i == 3) {
print("continue outer");
continue outer;
}
if(i == 5) {
print("break");
break;
}
if(i == 7) {
print("break outer");
break outer;
}
}
}
}
}

<spoiler text="Output:">

Внешний цикл while
i = 1
continue
i = 2
i = 3
continue outer
Внешний цикл while
i = 4
i = 5
break
Внешний цикл while
i = 6
i = 7
break outer

</spoiler>
Те же правила верны и для цикла while:


  • Обычная команда continue переводит исполнение к началу текущего внутреннего цикла, программа продолжает работу.
  • Команда continue с меткой вызывает переход к метке и повторный вход в цикл, следующий прямо за этой меткой.
  • Команда break завершает выполнение текущего цикла.
  • Команда break с меткой завершает выполнение внутреннего цикла и цикла, который находится после указанной метки.

Важно помнить, что единственная причина для существования меток в Java — наличие вложенных циклов и необходимость выхода по break и продолжения по continue не только для внутренних, но и для внешних циклов.

В статье Дейкстры особенно критикуются метки, а не сам оператор goto. Дейкстра отмечает, что, как правило, количество ошибок в программе растет с увеличением количества меток в этой программе. Метки затрудняют анализ программного кода. Заметьте, что метки Java не страдают этими пороками, потому что их место расположения ограничено и они не могут использоваться для беспорядочной передачи управления. В данном случае от ограничения возмож­ностей функциональность языка только выигрывает.

switch

Команду switch часто называют командой выбора. С помощью конструкции switch осуществляется выбор из нескольких альтернатив, в зависимости от значения целочисленного выражения. Форма команды выглядит так:

switch(целочисленное-выражение) {
case целое-значение1 : команда; break;
case целое-значение2 : команда; break;
case целое-значение3 : команда; break;
case целое-значение4 : команда; break;
case целое-значениеб : команда; break; // ..
default: оператор;
}

Целочисленное-выражение — выражение, в результате вычисления которого получается целое число. Команда switch сравнивает результат целочисленного-выражения с каждым последующим целым-значением. Если обнаруживается совпадение, исполняется соответствующая команда (простая или составная). Если же совпадения не находится, исполняется команда после ключевого слова default.

Нетрудно заметить, что каждая секция case заканчивается командой break, которая передает управление к концу команды switch. Такой синтаксис построения конструкции switch считается стандартным, но команда break не является строго обязательной.

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

Заметьте, что последняя секция default не содержит команды break; выполнение продолжается в конце конструкции switch, то есть там, где оно оказалось бы после вызова break. Впрочем, вы можете использовать break и в предложении default, без практической пользы, просто ради «единства стиля».

Команда switch обеспечивает компактный синтаксис реализации множественного выбора (то есть выбора из нескольких путей выполнения программы), но для нее необходимо управляющее выражение, результатом которого является целочисленное значение, такое как int или char. Если, например, критерием выбора является строка или вещественное число, то команда switch не подойдет. Придется использовать серию команд if-else.

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

//: control/VowelsAndConsonants.java
// Демонстрация конструкции switch
import java.util.*;
import static net.mindview.util.Print.*;
 
public class VowelsAndConsonants {
public static void main(String[] args) {
Random rand = new Random(47);
for(int i = 0; i < 100; i++) {
int c = rand.nextInt(26) + 'a';
printnb((char)c + ", " + c + ": ");
switch(c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u': print("vowel");
break;
case 'y':
case 'w': print("Sometimes a vowel");
break;
default: print("consonant");
}
}
}
}

<spoiler text="Output:">

y, 121: Sometimes a vowel
n, 110: consonant
z, 122: consonant
b, 98: consonant
r, 114: consonant
n, 110: consonant
y, 121: Sometimes a vowel
g, 103: consonant
c, 99: consonant

</spoiler>
Так как метод Random.nextInt(26) генерирует значение между 0 и 26, для получения символа нижнего регистра остается прибавить смещение 'а'. Символы в апострофах в секциях case также представляют собой целочисленные значения, используемые для сравнения.

Обратите внимание на «стопки» секций case, обеспечивающие возможность множественного сравнения для одной части кода. Будьте начеку и не забывайте добавлять команду break после каждой секции case, иначе программа просто пе­рейдет к выполнению следующей секции case. В команде

 int с = rand.nextInt(26) + 'а';

метод rand.nextInt() выдает случайное число int от 0 до 25, к которому затем прибавляется значение 'а'. Это означает, что символ а автоматически преобразуется к типу int для выполнения сложения.
Чтобы вывести с в символьном виде, его необходимо преобразовать к типу char; в противном случае значение будет выведено в числовом виде.

Резюме

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

]]>
Книги по Java https://linexp.ru?id=4748 Wed, 29 Jun 2022 14:30:13 GMT
<![CDATA[Глава 6 Thinking in Java 4th edition]]> Управление доступомВажнейшим фактором объектно-ориентированной разработки является отделение переменных составляющих от постоянных. Это особенно важно для библиотек. Пользователь {программист-клиент) библиотеки зависит от неизменности некоторого аспекта вашего кода. С другой стороны, создатель библиотеки должен обладать достаточной свободой для проведения изменений и улучшений, но при этом изменения не должны нарушить работоспособность клиентского кода.

УПРАВЛЕНИЕ ДОСТУПОМ

Важнейшим фактором объектно-ориентированной разработки является отделение переменных составляющих от постоянных.
Это особенно важно для библиотек. Пользователь {программист-клиент) библиотеки зависит от неизменности некоторого аспекта вашего кода. С другой стороны, создатель библиотеки должен обладать достаточной свободой для проведения изменений и улучшений, но при этом изменения не должны нарушить работоспособность клиентского кода.

Желанная цель может быть достигнута определенными договоренностями: Например, программист библиотеки соглашается не удалять уже существующие методы класса, потому что это может нарушить структуру кода программиста-клиента. В то же время обратная проблема гораздо острее. Например, как создатель библиотеки узнает, какие из полей данных используются программистом-клиентом? Это же относится и к методам, являющимся только частью реализации класса, то есть не предназначенным для прямого использования программистом-клиентом. А если создателю библиотеки понадобится удалить старую реализацию и заменить ее новой? Изменение любого из полей класса может нарушить работу кода программиста-клиента. Выходит, у создателя библиотеки «связаны руки», и он вообще ничего не вправе менять.

Для решения проблемы в Java определены спецификаторы доступа (access specifiers), при помощи которых создатель библиотеки указывает, что доступно программисту-клиенту, а что нет. Уровни доступа (от полного до минимального) задаются следующими ключевыми словами: public, protected, доступ в пределах пакета (не имеет ключевого слова) и private. Из предыдущего абзаца может возникнуть впечатление, что создателю библиотеки лучше всего хранить все как можно «секретнее», а открывать только те методы, которые, по вашему мнению, должен использовать программист-клиент. И это абсолютно верно, хотя и выглядит непривычно для людей, чьи программы на других языках (в особенности это касается C) «привыкли» к отсутствию ограничений. К концу этой главы вы наглядно убедитесь в полезности механизма контроля доступа в Java.

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

Пакет как библиотечный модуль

Пакет содержит группу классов, объединенных в одном пространстве имен.
Например, в стандартную поставку Java входит служебная библиотека, оформленная в виде пространства имен java.util. Один из классов java.util называется ArrayList. Чтобы использовать класс в программе, можно использовать его полное имя java.util.ArrayList. Впрочем, полные имена слишком громоздки, поэтому в программе удобнее использовать ключевое слово import. Если вы собираетесь использовать всего один класс, его можно указать прямо в директиве import:

//: access/SingleImport.java
import java.util.ArrayList;
 
public class SingleImport {
public static void main(String[] args) {
ArrayList list = new java.util.ArrayList();
}
}

Теперь к классу ArrayList можно обращаться без указания полного имени, но другие классы пакета java.util останутся недоступными. Чтобы импортировать все классы, укажите * вместо имени класса, как это делается почти во всех примерах книги:

 import java.util.*;

Механизм импортирования обеспечивает возможность управления пространствами имен. Имена членов классов изолируются друг от друга. Метод f() класса А не конфликтует с методом f() с таким же определением (списком аргументов) класса В. А как насчет имен классов? Предположим, что класс Stack создается на компьютере, где кем-то другим уже был определен класс с именем Stack. Потенциальные конфликты имен — основная причина, по которой так важны управление пространствами имен в Java и возможность создания уникальных идентификаторов для всех классов.

До этого момента большинство примеров книги записывались в отдельных файлах и предназначались для локального использования, поэтому на имена пакетов можно было не обращать внимания. (В таком случае имена классов размещаются в «пакете по умолчанию».) Конечно, это тоже решение, и такой подход будет применяться в книге, где только возможно. Но, если вы создаете библиотеку или программу, использующую другие программы Java на этой же машине, стоит подумать о предотвращении конфликтов имен.

Файл с исходным текстом на Java часто называют компилируемым модулем. Имя каждого компилируемого модуля должно завершаться суффиксом .java, а внутри него может находиться открытый (public) класс, имеющий то же имя, что и файл (с заглавной буквы, но без расширения .java). Любой компилируемый модуль может содержать не более одного открытого класса, иначе компилятор сообщит об ошибке. Остальные классы модуля, если они там есть, скрыты от окружающего мира — они не являются открытыми (public) и считаются «вспомогательными» по отношению к главному открытому классу.

В результате компиляции для каждого класса, определенного в файле .java, создается класс с тем же именем но с расширением .class. Таким образом, при компиляции нескольких файлов .java может появиться целый ряд файлов с рас­ширением .class. Если вы программировали на компилируемом языке, то, наверное, привыкли к тому, что компилятор генерирует промежуточные файлы (обычно с расширением OBJ), которые затем объединяются компоновщиком для получения исполняемого файла или библиотеки. Java работает не так. Рабочая программа представляет собой набор однородных файлов .class, которые объединяются в пакет и сжимаются в файл JAR (утилитой Java jar). Интерпретатор Java отвечает за поиск, загрузку и интерпретацию этих файлов.

Библиотека также является набором файлов с классами. В каждом файле имеется один рublіс-класс с любым количеством классов, не имеющих спецификатора public. Если вы хотите объявить, что все эти компоненты (хранящиеся в отдельных файлах .java и .class) связаны друг с другом, воспользуйтесь ключевым словом package.

Директива package должна находиться в первой незакомментированной строке файла. Так, команда.

 package access;

означает, что данный компилируемый модуль входит в библиотеку с именем access. Иначе говоря, вы указываете, что открытый класс в этом компилируемом модуле принадлежит имени mypackage и, если кто-то захочет использовать его, ему придется полностью записать или имя класса, или директиву import с access (конструкция, указанная выше). Заметьте, что по правилам Java имена пакетов записываются только строчными буквами.

Предположим, файл называется MyClass.java. Он может содержать один и только один открытый класс (public), причем последний должен называться MyClass (с учетом регистра символов):

//: access/mypackage/MyClass.java
package access.mypackage;
 
public class MyClass {
// ...
}

Если теперь кто-то захочет использовать MyClass или любые другие открытые классы из пакета access, ему придется использовать ключевое слово import, чтобы имена из access стали доступными. Возможен и другой вариант — записать полное имя класса:

//: access/QualifiedMyClass.java
 
public class QualifiedMyClass {
public static void main(String[] args) {
access.mypackage.MyClass m =
new access.mypackage.MyClass();
}
}

С ключевым словом import решение выглядит гораздо аккуратнее:

//: access/ImportedMyClass.java
import access.mypackage.*;
 
public class ImportedMyClass {
public static void main(String[] args) {
MyClass m = new MyClass();
}
}

Ключевые слова package и import позволяют разработчику библиотеки организовать логическое деление глобального пространства имен, предотвращающее конфликты имен независимо от того, сколько людей подключится к Интернету и начнет писать свои классы на Java.

Создание уникальных имен пакетов

Вы можете заметить, что, поскольку пакет на самом деле никогда не «упаковывается» в единый файл, он может состоять из множества файлов .class, что способно привести к беспорядку, может, даже хаосу. Для предотвращения проблемы логично было бы разместить все файлы .class конкретного пакета в одном каталоге, то есть воспользоваться иерархической структурой файловой системы. Это первый способ решения проблемы нагромождения файлов в Java; о втором вы узнаете при описании утилиты jar.

Размещение файлов пакета в отдельном каталоге решает две другие задачи: создание уникальных имен пакетов и обнаружение классов, потерянных в «дебрях» структуры каталогов. Как было упомянуто в главе 2, проблема решается «кодированием» пути файла в имени пакета. По общепринятой схеме первая часть имени пакета должна состоять из перевернутого доменного имени разработчика класса. Так как доменные имена Интернета уникальны, соблюдение этого правила обеспечит уникальность имен пакетов и предотвратит конфликты. (Только если ваше доменное имя не достанется кому-то другому, кто начнет писать программы на Java под тем же именем.) Конечно, если у вас нет собственного доменного имени, для создания уникальных имен пакетов придется придумать комбинацию с малой вероятностью повторения (скажем, имя и фамилия). Если же вы решите публиковать свои программы на Java, стоит немного потратиться на получение собственного доменного имени.

Вторая составляющая — преобразование имени пакета в каталог на диске компьютера. Если программе во время исполнения понадобится загрузить файл .class (что делается динамически, в точке, где программа создает объект определенного класса, или при запросе доступа к статическим членам класса), она может найти каталог, в котором располагается файл .class.

Интерпретатор Java действует по следующей схеме. Сначала он проверяет переменную окружения CLASSPATH (ее значение задается операционной системой, а иногда программой установки Java или инструментарием Java). CLASSPATH содержит список из одного или нескольких каталогов, используемых в качестве корневых при поиске файлов .class. Начиная с этих корневых каталогов, интерпретатор берет имя пакета и заменяет точки на слеши для получения полного пути (таким образом, директива package foo.bar.baz преобразуется в foo\bar\baz, foo/bar/baz или что-то еще в зависимости от вашей операционной системы). Затем полученное имя присоединяется к различным элементам CLASSPATH. В указанных местах ведется поиск файлов .class, имена которых совпадают с именем создаваемого программой класса. (Поиск также ведется в стандартных каталогах, определяемых местонахождением интерпретатора Java.)

Чтобы понять все сказанное, рассмотрим мое доменное имя: MindView.net. Обращая его, получаем уникальное глобальное имя для моих классов: net.mindview. (Расширения com, edu, org и другие в пакетах Java прежде записывались в верхнем регистре, но начиная с версии Java 2 имена пакетов записываются только строчными буквами.) Если потребуется создать библиотеку с именем simple, я получаю следующее имя пакета:

 package net.mindview.simple;

Теперь полученное имя пакета можно использовать в качестве объединяющего пространства имен для следующих двух файлов:

//: net/mindview/simple/Vector.java
// Создание пакета
package net.mindview.simple;
 
public class Vector {
public Vector() {
System.out.println("net.mindview.simple.Vector");
}
}

Как упоминалось ранее, директива package должна находиться в первой строке исходного кода. Второй файл выглядит почти так же:

//: net/mindview/simple/List.java
// Создание пакета
package net.mindview.simple;
 
public class List {
public List() {
System.out.println("net.mindview.simple.List");
}
}

В моей системе оба файла находятся в следующем подкаталоге:

С:\DOC\JavaT\net\mindview\simple

Если вы посмотрите на файлы, то увидите имя пакета net.mindview.simple, но что с первой частью пути? О ней позаботится переменная окружения CLASSPATH, которая на моей машине выглядит следующим образом:

CLASSPATH= D:\JAVA\LIBC\DOC\JavaT

Как видите, CLASSPATH может содержать несколько альтернативных путей для поиска.

Однако для файлов JAR используется другой подход. Вы должны записать имя файла JAR в переменной CLASSPATH, не ограничиваясь указанием пути к месту его расположения. Таким образом, для файла JAR с именем grape.jar пе­ременная окружения должна выглядеть так:

CLASSPATH= D:\JAVA\LIBС\flavors\grape.jar

После настройки CLASSPATH следующий файл можно разместить в любом каталоге:

//: access/LibTest.java
// Uses the library.
import net.mindview.simple.*;
 
public class LibTest {
public static void main(String[] args) {
Vector v = new Vector();
List l = new List();
}
}

<spoiler text="Output:">

net.mindview.simple.Vector
net.mindview.simple.List

</spoiler>
Когда компилятор встречает директиву import для библиотеки simple, он начинает поиск в каталогах, перечисленных в переменной CLASSPATH, найдет каталог net/mindview/simple, а затем переходит к поиску компилированных файлов с подходящими именами (Vector.class для класса Vector и List.class для класса List). Заметьте, что как классы, так и необходимые методы классов Vector и List должны быть объявлены со спецификатором public.

Конфликты имен

Что происходит при импортировании конструкцией * двух библиотек, имеющих в своем составе идентичные имена? Предположим, программа содержит следующие директивы:

 import net.mindview.simple.*; 
import java.util.*;

Так как пакет java.util.* тоже содержит класс с именем Vector, это может привести к потенциальному конфликту. Но, пока вы не начнете писать код, вызывающий конфликты, все будет в порядке — и это хорошо, поскольку иначе вам пришлось бы тратить лишние усилия на предотвращение конфликтов, которых на самом деле нет.
Конфликт действительно произойдет при попытке создать Vector:

 Vector v = new Vector();

К какому из классов Vector относится эта команда? Этого не знают ни компилятор, ни читатель программы. Поэтому компилятор выдаст сообщение об ошибке и заставит явно указать нужное имя. Например, если мне понадобится стандартный класс Java с именем Vector, я должен явно указать этот факт:

 java.util.Vector v = new java.util.Vector();

Данная команда (вместе с переменной окружения CLASSPATH) полностью описывает местоположение конкретного класса Vector, поэтому директива import java.util.* становится избыточной (по крайней мере, если вам не потребуются другие классы из этого пакета).

Пользовательские библиотеки

Полученные знания позволяют вам создавать собственные библиотеки, сокращающие или полностью исключающие дублирование кода. Для примера можно взять уже знакомый псевдоним для метода System.out.println(), сокращающий количество вводимых символов. Его можно включить в класс Print:

//: net/mindview/util/Print.java
// Методы-печати, которые могут использоваться
// без спецификаторов, благодаря конструкции
package net.mindview.util;
import java.io.*;
 
public class Print {
// Печать с переводом строки:
public static void print(Object obj) {
System.out.println(obj);
}
// Перевод строки:
public static void print() {
System.out.println();
}
// Печать без перевода строки:
public static void printnb(Object obj) {
System.out.print(obj);
}
// Новая конструкция Java SE5 printf() (from C):
public static PrintStream
printf(String format, Object... args) {
return System.out.printf(format, args);
}
}

Новые методы могут использоваться для вывода любых данных с новой строки (print()) или в текущей строке (printnb()).

Как нетрудно предположить, файл должен располагаться в одном из каталогов, указанных в переменной окружения CLASSPATH, по пути net/mindview. После компиляции методы static print() и printnb() могут использоваться где угодно, для чего в программу достаточно включить директиву import static:

//: access/PrintTest.java
// Использование статических методов печати из Print.java
import static net.mindview.util.Print.*;
 
public class PrintTest {
public static void main(String[] args) {
print("Available from now on!");
print(100);
print(100L);
print(3.14159);
}
}

<spoiler text="Output:">

Available from now on!
100
100
3.14159

</spoiler>
Теперь, когда бы вы ни придумали новый интересный инструмент, вы всегда можете добавить его в свою библиотеку.

Предостережение при работе с пакетами

Помните, что создание пакета всегда неявно сопряжено с определением структуры каталогов. Пакет обязан находиться в одноименном каталоге, который, в свою очередь, определяется содержимым переменной CLASSPATH. Первые эксперименты с ключевым словом package могут оказаться неудачными, пока вы твердо не усвоите правило «имя пакета — его каталог». Иначе компилятор будет выводить множество сообщений о загадочных ошибках выполнения, о не­возможности найти класс, который находится рядом в этом же каталоге. Если у вас возникают такие ошибки, попробуйте закомментировать директиву package; если все запустится, вы знаете, где искать причины.

Спецификаторы доступа Java

В Java спецификаторы доступа public, protected и private располагаются перед определением членов классов — как полей, так и методов. Каждый спецификатор доступа управляет только одним отдельным определением.

Если спецификатор доступа не указан, используется «пакетный» уровень доступа. Получается, что в любом случае действует та или иная категория доступа. В нескольких ближайших подразделах описаны разные уровни доступа.

Доступ в пределах пакета

Во всех рассмотренных ранее примерах спецификаторы доступа не указывались. Доступ по умолчанию не имеет ключевого слова, но часто его называют доступом в пределах пакета (package access, иногда «дружественным»). Это значит, что член класса доступен для всех остальных классов текущего пакета, но для классов за пределами пакета он воспринимается как приватный (private). Так как компилируемый модуль — файл — может принадлежать лишь одному пакету, все классы одного компилируемого модуля автоматически открыты друг для друга в границах пакета.

Доступ в пределах пакета позволяет группировать взаимосвязанные классы в одном пакете, чтобы они могли легко взаимодействовать друг с другом. Размещая классы в одном пакете, вы берете код пакета под полный контроль. Таким образом, только принадлежащий вам код будет обладать пакетным доступом к другому, принадлежащему вам же коду — и это вполне логично. Можно сказать, что доступ в пределах пакета и является основной причиной для группировки классов в пакетах. Во многих языках определения в классах организуются совершенно произвольным образом, но в Java придется привыкать к более жесткой логике структуры. Вдобавок классы, которые не должны иметь доступ к классам текущего пакета, следует просто исключить из этого пакета.

Класс сам определяет, кому разрешен доступ к его членам. Не существует волшебного способа «ворваться» внутрь него. Код из другого пакета не может запросто обратиться к пакету и рассчитывать, что ему вдруг станут доступны все члены: protected, private и доступные в пакете. Получить доступ можно лишь несколькими «законными» способами:


  • Объявить член класса открытым (public), то есть доступным для кого угодно и откуда угодно.
  • Сделать член класса доступным в пакете, не указывая другие спецификаторы доступа, и разместить другие классы в этом же пакете.
  • Как вы увидите в главе 7, где рассказывается о наследовании, производный класс может получить доступ к защищенным (protected) членам базового класса вместе с открытыми членами public (но не к приватным членам private). Такой класс может пользоваться доступом в пределах пакета только в том случае, если второй класс принадлежит тому же пакету (впрочем, пока на наследование и доступ protected можно не обращать внимания).
  • Предоставить «методы доступа», то есть методы для чтения и модификации значения. С точки зрения ООП этот подход является предпочтительным, и именно он используется в технологии JavaBeans.


public

При использовании ключевого слова public вы фактически объявляете, что следующее за ним объявление члена класса доступно для всех, и прежде всего для клиентских программистов, использующих библиотеку. Предположим, вы определили пакет dessert, содержащий следующий компилируемый модуль:

//: access/dessert/Cookie.java
// Создание библиотеки.
package access.dessert;
 
public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
void bite() { System.out.println("bite"); }
}

Помните, что файл Cookie.java должен располагаться в подкаталоге dessert каталога с именем access (соответствующем данной главе книги), а последний должен быть включен в переменную CLASSPATH. Не стоит полагать, будто Java всегда начинает поиск с текущего каталога. Если вы не укажете символ . (точка) в переменной окружения CLASSPATH в качестве одного из путей поиска, то Java и не заглянет в текущий каталог.
Если теперь написать программу, использующую класс Cookie:

//: access/Dinner.java
// Использование библиотеки.
import access.dessert.*;
 
public class Dinner {
public static void main(String[] args) {
Cookie x = new Cookie();
//! x.bite(); // Обращение невозможно
}
}

<spoiler text="Output:">

Cookie constructor

</spoiler>
то можно создать объект Cookie, поскольку конструктор этого класса объявлен открытым (public) и сам класс также объявлен как public. (Понятие открытого класса мы позднее рассмотрим чуть подробнее.) Тем не менее метод bite() этого класса недоступен в файле Dinner.java, поскольку доступ к нему предоставляется только в пакете dessert. Так компилятор предотвращает неправильное использование методов.

Пакет по умолчанию

С другой стороны, следующий код работает, хотя на первый взгляд он вроде бы нарушает правила:

//: access/Cake.java
// Обращение к классу из другого компилируемого модуля.
class Cake {
public static void main(String[] args) {
Pie x = new Pie();
x.f();
}
}

<spoiler text="Output:">

Pie.f()

</spoiler>
Второй файл в том же каталоге:

//: access/Pie.java
// Другой класс.
class Pie {
void f() { System.out.println("Pie.f()"); }
}

Вроде бы эти два файла не имеют ничего общего, и все же в классе Cake можно создать объект Pie и вызвать его метод f()! (Чтобы файлы компилировались, переменная CLASSPATH должна содержать символ точки.) Естественно было бы предположить, что класс Pie и метод f() имеют доступ в пределах пакета и поэтому закрыты для Cake. Они действительно обладают доступом в пределах пакета — здесь все верно. Однако их доступность в классе Cake.java объясняется тем, что они находятся в одном каталоге и не имеют явно заданного имени пакета. Java по умолчанию включает такие файлы в «пакет по умолчанию» для текущего каталога, поэтому они обладают доступом в пределах пакета к другим файлам в этом каталоге.

private

Ключевое слово private означает, что доступ к члену класса не предоставляется никому, кроме методов этого класса. Другие классы того же пакета также не могут обращаться к private-членам. На первый взгляд вы вроде бы изолируете класс даже от самого себя. С другой стороны, вполне вероятно, что пакет создается целой группой разработчиков; в этом случае private позволяет изменять члены класса, не опасаясь, что это отразится на другом классе данного пакета.

Предлагаемый по умолчанию доступ в пределах пакета часто оказывается достаточен для сокрытия данных; напомню, что такой член класса недоступен пользователю пакета. Это удобно, так как обычно используется именно такой уровень доступа (даже в том случае, когда вы просто забудете добавить спецификатор доступа). Таким образом, доступ public чаще всего используется тогда, когда вы хотите сделать какие-либо члены класса доступными для программиста-клиента. Может показаться, что спецификатор доступа private применяется редко и можно обойтись и без него. Однако разумное применение private очень важно, особенно в условиях многопоточного программирования (см. далее).

Пример использования private:

//: access/IceCream.java
// Демонстрация ключевого слова private.
 
class Sundae {
private Sundae() {}
static Sundae makeASundae() {
return new Sundae();
}
}
 
public class IceCream {
public static void main(String[] args) {
//! Sundae x = new Sundae();
Sundae x = Sundae.makeASundae();
}
}

Перед вами пример ситуации, в которой private может быть очень полезен: предположим, вы хотите контролировать процесс создания объекта, не разрешая посторонним вызывать конкретный конструктор (или любые конструкторы). В данном примере запрещается создавать объекты Sundae с помощью конструктора; вместо этого пользователь должен использовать метод makeASundae().

Все «вспомогательные» методы классов стоит объявить как private, чтобы предотвратить их случайные вызовы в пакете; тем самым вы фактически запрещаете изменение поведения метода или его удаление.

То же верно и к private-полям внутри класса. Если только вы не собираетесь предоставить доступ пользователям к внутренней реализации (а это происходит гораздо реже, чем можно себе представить), объявляйте все поля своих классов со спецификатором private.

protected

Чтобы понять смысл спецификатора доступа protected, необходимо немного забежать вперед. Сразу скажу, что понимание этого раздела не обязательно до знакомства с наследованием (глава 7). И все же для получения цельного представления здесь приводится описание protected и примеры его использования.

Ключевое слово protected тесно связано с понятием наследования, при котором к уже существующему классу (называемому базовым классом) добавляются новые члены, причем исходная реализация остается неизменной. Также можно изменять поведение уже существующих членов класса. Для создания нового класса на базе существующего используется ключевое слово extends:

 class Foo extends Bar {}

Остальная часть реализации выглядит как обычно.

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

Интерфейс и реализация

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

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

Это подводит нас непосредственно ко второй причине — разделению интерфейса и реализации. Если в программе использована определенная структура, но программисты-клиенты не могут получить доступ к ее членам, кроме отправки сообщений рubliс-интерфейсу, вы можете изменять все, что не объявлено как public (члены с доступом в пределах пакета, protected и private), не нарушая работоспособности изменений клиентского кода.

Для большей ясности при написании классов можно использовать такой стиль: сначала записываются открытые члены (public), затем следуют защищенные члены (protected), потом — с доступом в пределах пакета и наконец закрытые члены (private). Преимущество такой схемы состоит в том, что при чтении исходного текста пользователь сначала видит то, что ему важно (открытые члены, доступ к которым можно получить отовсюду), а затем останавливается при переходе к закрытым членам, являющимся частью внутренней реализации:

//: access/OrganizedByAccess.java
 
public class OrganizedByAccess {
public void pub1() { /* ... */ }
public void pub2() { /* ... */ }
public void pub3() { /* ... */ }
private void priv1() { /* ... */ }
private void priv2() { /* ... */ }
private void priv3() { /* ... */ }
private int i;
// ...
}

Такой подход лишь частично упрощает чтение кода, поскольку интерфейс и реализация все еще совмещены. Иначе говоря, вы все еще видите исходный код — реализацию — так, как он записан прямо в классе. Вдобавок документация в комментариях, создаваемая с помощью javadoc, снижает необходимость в чтении исходного текста программистом-клиентом.

Доступ к классам

В Java с помощью спецификаторов доступа можно также указать, какие из классов внутри библиотеки будут доступны для ее пользователей. Если вы хотите, чтобы класс был открыт программисту-клиенту, то добавляете ключевое слово public для класса в целом. При этом вы управляете даже самой возможностью создания объектов данного класса программистом-клиентом.

Для управления доступом к классу, спецификатор доступа записывается перед ключевым словом class:

 public class Widget {}

Если ваша библиотека называется, например, access, то любой программист-клиент сумеет обратиться извне к классу Widget:

 import access.Widget;

или

 import access *;

Впрочем, при этом действуют некоторые ограничения:


  • В каждом компилируемом модуле может существовать только один открытый (public) класс. Идея в том, что каждый компилируемый модуль содержит определенный открытый интерфейс и реализуется этим открытым классом. В модуле может содержаться произвольное количество вспомогательных классов с доступом в пределах пакета. Если в компилируемом модуле определяется более одного открытого класса, компилятор выдаст сообщение об ошибке.
  • Имя открытого класса должно в точности совпадать с именем файла, в котором содержится компилируемый модуль, включая регистр символов. Поэтому для класса Widget имя файла должно быть Widget.java, но никак не widget.java или WIDGET.java. В противном случае вы снова получите сообщение об ошибке.
  • Компилируемый модуль может вообще не содержать открытых классов (хотя это и не типично). В этом случае файлу можно присвоить любое имя по вашему усмотрению. С другой стороны, выбор произвольного имени создаст трудности у тех людей, которые будут читать и сопровождать ваш код.

Допустим, в пакете access имеется класс, который всего лишь выполняет некоторые служебные операции для класса Widget или для любого другого рublic-класса пакета. Конечно, вам не хочется возиться с созданием лишней документации для клиента; возможно, когда-нибудь вы просто измените структуру пакета, уберете этот вспомогательный класс и добавите новую реализацию. Но для этого нужно точно знать, что ни один программист-клиент не зависит от конкретной реализации библиотеки. Для этого вы просто опускаете ключевое слово public в определении класса; ведь в таком случае он ограничивается пакетным доступом, то есть может использоваться только в пределах своего пакета.

При создании класса с доступом в пределах пакета его поля все равно рекомендуется помечать как private (всегда нужно по максимуму перекрывать доступ к полям класса), но методам стоит давать тот же уровень доступа, что имеет и сам класс (в пределах пакета). Класс с пакетным доступом обычно используется только в своем пакете, и делать методы такого класса открытыми (public) стоит только при крайней необходимости — а о таких случаях вам сообщит компилятор.

Заметьте, что класс нельзя объявить как private (что сделает класс недоступным для окружающих, использовать он сможет только «сам себя») или protected. Поэтому у вас есть лишь такой выбор при задании доступа к классу: в пределах пакета или открытый (public). Если вы хотите перекрыть доступ к классу для всех, объявите все его конструкторы со спецификатором private, соответственно, запретив кому бы то ни было создание объектов этого класса. Только вы сами, в статическом методе своего класса, сможете создавать такие объекты. Пример:

// Demonstrates class access specifiers. Make a class
// Спецификаторы доступа для классов.
// Использование конструкторов, объявленных private,
// делает класс недоступным при создании объектов.
 
class Soup1 {
private Soup1() {}
// (1) Разрешаем создание объектов в статическом методе::
public static Soup1 makeSoup() {
return new Soup1();
}
}
 
class Soup2 {
private Soup2() {}
// (2) Создаем один статический объект и
// по требованию возвращаем ссылку на него):
private static Soup2 ps1 = new Soup2();
public static Soup2 access() {
return ps1;
}
public void f() {}
}
 
// В файле может быть определен только один public-класс:
public class Lunch {
void testPrivate() {
// Запрещено, т.к конструктор объявлен приватным::
//! Soup1 soup = new Soup1();
}
void testStatic() {
Soup1 soup = Soup1.makeSoup();
}
void testSingleton() {
Soup2.access().f();
}
}

До этого момента большинство методов возвращало или void, или один из примитивных типов, поэтому определение:

 public static Soupl makeSoup(){ return new Soup1()}

на первый взгляд смотрится немного странно. Слово Soup1 перед именем метода (makeSoup) показывает, что возвращается методом. В предшествующих примерах обычно использовалось обозначение void, которое подразумевает, что метод не имеет возвращаемого значения. Однако метод также может возвращать ссылку на объект; в данном случае возвращается ссылка на объект класса Soup1.

Классы Soup1 и Soup2 наглядно показывают, как предотвратить прямое создание объектов класса, объявив все его конструкторы со спецификатором private. Помните, что без явного определения хотя бы одного конструктора компилятор сгенерирует конструктор по умолчанию (конструктор без аргументов). Определяя конструктор по умолчанию в программе, вы запрещаете его автоматическое создание. Если конструктор объявлен со спецификатором private, никто не сможет создавать объекты данного класса.

Но как же тогда использовать этот класс? Рассмотренный пример демонстрирует два способа. В классе Soup1 определяется статический метод, который создает новый объект Soup1 и возвращает ссылку на него. Это бывает полезно в ситуациях, где вам необходимо провести некоторые операции над объектом перед возвратом ссылки на него, или при подсчете общего количества созданных объектов Soup1 (например, для ограничения их максимального количества).

В классе Soup2 использован другой подход — в программе всегда создается не более одного объекта этого класса. Объект Soup2 создается как статическая приватная переменная, пoтому он всегда существует только в одном экземпляре и его невозможно получить без вызова открытого метода access().

Резюме

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

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

В этой главе рассматривается процесс построения библиотек из классов; во-первых, механизм группировки классов внутри библиотеки и, во-вторых, механизм управления доступом к членам класса.

По оценкам проекты на языке C начинают «рассыпаться» примерно тогда, когда код достигает объема от 50 до 100 Кбайт, так как C имеет единое «пространство имен»; в системе возникают конфликты имен, создающие массу неудобств. В Java ключевое слово package, схема именования пакетов и ключевое слово import обеспечивают полный контроль над именами, так что конфликта имен можно легко избежать.

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

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

Открытый интерфейс класса — это то, что фактически видит его пользователь, поэтому очень важно «довести до ума» именно эту, самую важную, часть класса в процессе анализа и разработки. И даже при этом у вас остается относительная свобода действий. Даже если идеальный интерфейс не удалось построить с первого раза, вы можете добавить в него новые методы — без удаления уже существующих методов, которые могут использоваться программистами- клиентами.

]]>
Книги по Java https://linexp.ru?id=4747 Wed, 29 Jun 2022 14:29:39 GMT
<![CDATA[Глава 7 Thinking in Java 4th edition]]> ПОВТОРНОЕ ИСПОЛЬЗОВАНИЕ КЛАССОВВозможность повторного использования кода принадлежит к числу важнейших преимуществ Java. Впрочем, по-настоящему масштабные изменения отнюдь не сводятся к обычному копированию и правке кода.

ПОВТОРНОЕ ИСПОЛЬЗОВАНИЕ КЛАССОВ

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

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

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

Второй способ гораздо интереснее. Новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса. Этот механизм называется наследованием (inheritance), и большую часть работы в нем совершает компилятор. Наследование является одним из «краеугольных камней» объектно-ориентированного программирования; некоторые из его дополнительных применений описаны в главе 8.

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

Синтаксис композиции

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

//: reusing/SprinklerSystem.java
// Композиция для повторного использования кода.
 
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "Constructed";
}
public String toString() { return s; }
}
 
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
public String toString() {
return
"valve1 = " + valve1 + " " +
"valve2 = " + valve2 + " " +
"valve3 = " + valve3 + " " +
"valve4 = " + valve4 + "\n" +
"i = " + i + " " + "f = " + f + " " +
"source = " + source;
}
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
}
}

<spoiler text="Output:">
WaterSource()

valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed

</spoiler>
В обоих классах определяется особый метод toString(). Позже вы узнаете, что каждый не-примитивный объект имеет метод toString(), который вызывается в специальных случаях, когда компилятор располагает не объектом, а хочет получить его строковое представление в формате String. Поэтому в выражении из метода SрrinklerSystem.toString ():

"source = " + source;

компилятор видит, что к строке "source = " «прибавляется» объект класса WaterSource. Компилятор не может это сделать, поскольку к строке можно «добавить» только такую же строку, поэтому он преобразует объект source в String, вызывая метод toString(). После этого компилятор уже в состоянии соединить две строки и передать результат в метод System.out.println() (или статическим методам print() и printnb(), используемым в книге). Чтобы подобное поведение поддерживалось вашим классом, достаточно включить в него метод toString().

Примитивные типы, определенные в качестве полей класса, автоматически инициализируются нулевыми значениями, как упоминалось в главе 2. Однако ссылки на объекты заполняются значениями null, и при попытке вызова метода по такой ссылке произойдет исключение. К счастью, ссылку null можно вывести без выдачи исключения.

Компилятор не создает объекты для ссылок «по умолчанию», и это логично, потому что во многих случаях это привело бы к лишним затратам ресурсов. Если вам понадобится проинициализировать ссылку, сделайте это самостоятельно:


  • в точке определения объекта. Это значит, что объект всегда будет инициализироваться перед вызовом конструктора;
  • в конструкторе данного класса;
  • непосредственно перед использованием объекта. Этот способ часто называют отложенной инициализацией. Он может сэкономить вам ресурсы в ситуациях, где создавать объект каждый раз необязательно и накладно;
  • с использованием инициализации экземпляров.

В следующем примере продемонстрированы все четыре способа:

//: reusing/Bath.java
// Инициализация в конструкторе с композицией.
import static net.mindview.util.Print.*;
 
class Soap {
private String s;
Soap() {
print("Soap()");
s = "Constructed";
}
public String toString() { return s; }
}
 
public class Bath {
private String // Инициализация в точке определения :
s1 = "Happy",
s2 = "Happy",
s3, s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
print("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
// Инициализация экземпляра:
{ i = 47; }
public String toString() {
if(s4 == null) // Отложенная инициализация:
s4 = "Joy";
return
"s1 = " + s1 + "\n" +
"s2 = " + s2 + "\n" +
"s3 = " + s3 + "\n" +
"s4 = " + s4 + "\n" +
"i = " + i + "\n" +
"toy = " + toy + "\n" +
"castille = " + castille;
}
public static void main(String[] args) {
Bath b = new Bath();
print(b);
}
}

<spoiler text="Output:">

Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed

</spoiler>
Заметьте, что в конструкторе класса Bath команда выполняется до проведения какой-либо инициализации. Если инициализация в точке определения не выполняется, нет никаких гарантий того, что она будет выполнена перед отправкой сообщения по ссылке объекта — кроме неизбежных исключений времени выполнения.
При вызове метода toString() в нем присваивается значение ссылке s4, чтобы все поля были должным образом инициализированы к моменту их использования.

Синтаксис наследования

Наследование является неотъемлемой частью Java (и любого другого языка ООП). Фактически оно всегда используется при создании класса, потому что, даже если класс не объявляется производным от другого класса, он автоматически становится производным от корневого класса Java Object.

Синтаксис композиции очевиден, но для наследования существует совершенно другая форма записи. При использовании наследования вы фактически говорите: «Этот новый класс похож на тот старый класс». В программе этот факт выражается перед фигурной скобкой, открывающей тело класса: сначала записывается ключевое слово extends, а затем имя базового (base) класса. Тем самым вы автоматически получаете доступ ко всем полям и методам базового класса. Пример:

//: reusing/Detergent.java
// Синтаксис наследования и его свойства.
import static net.mindview.util.Print.*;
 
class Cleanser {
private String s = "Cleanser";
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
public String toString() { return s; }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
print(x);
}
}
 
public class Detergent extends Cleanser {
// Изменяем метод:
public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Вызываем метод базового класса
}
// Добавляем новые методы к интерфейсу :
public void foam() { append(" foam()"); }
// Проверяем новый класс:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
print(x);
print("Testing base class:");
Cleanser.main(args);
}
}

<spoiler text="Output:">

Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()

</spoiler>
Пример демонстрирует сразу несколько особенностей наследования. Во-первых, в методе класса Cleanser.append() новые строки присоединяются к строке s оператором +=, одним из операторов, специально «перегруженных» создателями Java для строк (String).

Во-вторых, как Cleanser, так и Detergent содержат метод main(). Вы можете определить метод main() в каждом из своих классов; это позволяет встраивать тестовый код прямо в класс. Метод main() даже не обязательно удалять после завершения тестирования, его вполне можно оставить на будущее.

Даже если у вас в программе имеется множество классов, из командной строки исполняется только один (так как метод main() всегда объявляется как public, то неважно, объявлен ли класс, в котором он описан, как public). В нашем примере команда java Detergent вызывает метод Detergent.main(). Однако вы также можете использовать команду java Cleanser для вызова метода Cleanser.main(), хотя класс Cleanser не объявлен открытым. Даже если класс обладает доступом в пределах класса, открытый метод main() остается доступным.

Здесь метод Detergent.main() вызывает Cleanser.main() явно, передавая ему собственный массив аргументов командной строки (впрочем, для этого годится любой массив строк).

Важно, что все методы класса Cleanser объявлены открытыми. Помните, что при отсутствии спецификатора доступа, член класса автоматически получает доступ «в пределах пакета», что позволяет обращаться к нему только из текущего пакета. Таким образом, в пределах данного пакета при отсутствии спецификатора доступа вызов этих методов разрешен кому угодно — например, это легко может сделать класс Detergent.

Но если бы какой-то класс из другого пакета был объявлен производным от класса Cleanser, то он получил бы доступ только к его public-членам. С учетом возможности наследования все поля обычно помечаются как private, а все методы — как public. (Производный класс также получает доступ к защищенным (protected) членам базового класса, но об этом позже.) Конечно, иногда вы будете отступать от этих правил, но в любом случае полезно их запомнить.

Класс Cleanser содержит ряд методов: append(), dilute(), apply(), scrub() и toString(). Так как класс Detergent произведен от класса Cleanser (с помощью ключевого слова extends), он автоматически получает все эти методы в своем интерфейсе, хотя они и не определяются явно в классе Detergent. Таким образом, наследование обеспечивает повторное использование класса.

Как показано на примере метода scrub(), разработчик может взять уже существующий метод базового класса и изменить его. Возможно, в этом случае потребуется вызвать метод базового класса из новой версии этого метода. Однако в методе scrub() вы не можете просто вызвать scrub() — это приведет к рекурсии, а нам нужно не это. Для решения проблемы в Java существует ключевое слово super, которое обозначает «суперкласс», то есть класс, производным от которого является текущий класс. Таким образом, выражение super.scrub() обращается к методу scrub() из базового класса.

При наследовании вы не ограничены использованием методов базового класса. В производный класс можно добавлять новые методы тем же способом, что и раньше, то есть просто определяя их. Метод foam() — наглядный пример такого подхода.

В методе Detergent.main() для объекта класса Detergent вызываются все методы, доступные как из класса Cleanser, так и из класса Detergent (имеется в виду метод foam()).

Инициализация базового класса

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

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

//: reusing/Cartoon.java
// Вызовы конструкторов при проведении наследования
import static net.mindview.util.Print.*;
 
class Art {
Art() { print("Art constructor"); }
}
 
class Drawing extends Art {
Drawing() { print("Drawing constructor"); }
}
 
public class Cartoon extends Drawing {
public Cartoon() { print("Cartoon constructor"); }
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}

<spoiler text="Output:">

Art constructor
Drawing constructor
Cartoon constructor

</spoiler>
Как видите, конструирование начинается с «самого внутреннего» базового класса, поэтому базовый класс инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса Cartoon не определен, компилятор сгенерирует конструктор по умолчанию, в котором также вызывается конструктор базового класса.

Конструкторы с аргументами

В предыдущем примере использовались конструкторы по умолчанию, то есть конструкторы без аргументов. У компилятора не возникает проблем с вызовом таких конструкторов, так как вопросов о передаче аргументов не возникает. Если класс не имеет конструктора по умолчанию или вам понадобится вызвать конструктор базового класса с аргументами, этот вызов придется оформить явно, с указанием ключевого слова super и передачей аргументов:

//: reusing/Chess.java
// Наследование, конструкторы и аргументы.
import static net.mindview.util.Print.*;
 
class Game {
Game(int i) {
print("Game constructor");
}
}
 
class BoardGame extends Game {
BoardGame(int i) {
super(i);
print("BoardGame constructor");
}
}
 
public class Chess extends BoardGame {
Chess() {
super(11);
print("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
}

<spoiler text="Output:">

Game constructor
BoardGame constructor
Chess constructor

</spoiler>
Если не вызвать конструктор базового класса в BoardGame(), то компилятор «пожалуется» на то, что не может обнаружить конструктор в форме Game(). Вдобавок вызов конструктора базового класса должен быть первой командой в конструкторе производного класса. (Если вы вдруг забудете об этом, компилятор вам тут же напомнит.)

Делегирование

Третий вид отношений, не поддерживаемый в Java напрямую, называется делегированием. Он занимает промежуточное положение между наследованием и композицией: экземпляр существующего класса включается в создаваемый класс (как при композиции), но в то же время все методы встроенного объекта становятся доступными в новом классе (как при наследовании). Например, класс SpaceShipControls имитирует модуль управления космическим кораблем:

//: reusing/SpaceShipControls.java
 
public class SpaceShipControls {
void up(int velocity#41; {}
void down(int velocity) {}
void left(int velocity) {}
void right(int velocity) {}
void forward(int velocity) {}
void back(int velocity) {}
void turboBoost() {}
}

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

//: reusing/SpaceShip.java
 
public class SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) { this.name = name; }
public String toString() { return name; }
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
}

Однако космический корабль не может рассматриваться как частный случай своего управляющего модуля — несмотря на то, что ему, к примеру, можно приказать двигаться вперед (forward()). Точнее сказать, что SpaceShip содержит SpaceShipControls, и в то же время все методы последнего предоставляются классом SpaceShip. Проблема решается при помощи делегирования:

//: reusing/SpaceShipDelegation.java
 
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls =
new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Делегированные методы:
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}

Как видите, вызовы методов переадресуются встроенному объекту controls, а интерфейс остается таким же, как и при наследовании. С другой стороны, делегирование позволяет лучше управлять происходящим, потому что вы можете ограничиться небольшим подмножеством методов встроенного объекта.
Хотя делегирование не поддерживается языком Java, его поддержка присутствует во многих средах разработки. Например, приведенный пример был автоматически сгенерирован в JetBrains Idea IDE.

Сочетание композиции и наследования

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

//: reusing/PlaceSetting.java
// Совмещение композиции и наследования.
import static net.mindview.util.Print.*;
 
class Plate {
Plate(int i) {
print("Plate constructor");
}
}
 
class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
print("DinnerPlate constructor");
}
}
 
class Utensil {
Utensil(int i) {
print("Utensil constructor");
}
}
 
class Spoon extends Utensil {
Spoon(int i) {
super(i);
print("Spoon constructor");
}
}
 
class Fork extends Utensil {
Fork(int i) {
super(i);
print("Fork constructor");
}
}
 
class Knife extends Utensil {
Knife(int i) {
super(i);
print("Knife constructor");
}
}
 
// A cultural way of doing something:
class Custom {
Custom(int i) {
print("Custom constructor");
}
}
 
public class PlaceSetting extends Custom {
private Spoon sp;
private Fork frk;
private Knife kn;
private DinnerPlate pl;
public PlaceSetting(int i) {
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
print("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
}

<spoiler text="Output:">

Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor

</spoiler>
Несмотря на то, что компилятор заставляет вас инициализировать базовые классы и требует, чтобы вы делали это прямо в начале конструктора, он не следит за инициализацией встроенный объектов, поэтому вы должны сами помнить об этом.

Обеспечение правильного завершения

В Java отсутствует понятие деструктора из C++ — метода, автоматически вызываемого при уничтожении объекта. В Java программисты просто «забывают» об объектах, не уничтожая их самостоятельно, так как функции очистки памяти возложены на сборщика мусора.

Во многих случаях эта модель работает, но иногда класс выполняет некоторые операции, требующие завершающих действий. Как упоминалось в главе 5, вы не знаете, когда будет вызван сборщик мусора и произойдет ли это вообще. Поэтому, если в классе должны выполняться действия по очистке, вам придется написать для этого особый метод и сделать так, чтобы программисты-клиенты знали о необходимости вызова этого метода. Более того, как описано в главе 10, вам придется предусмотреть возможные исключения и выполнить завершающие действия в секции finally.
Представим пример системы автоматизированного проектирования, которая рисует на экране изображения:

//: reusing/CADSystem.java
// Обеспечение необходимого завершения.
package reusing;
import static net.mindview.util.Print.*;
 
class Shape {
Shape(int i) { print("Shape constructor"); }
void dispose() { print("Shape dispose"); }
}
 
class Circle extends Shape {
Circle(int i) {
super(i);
print("Drawing Circle");
}
void dispose() {
print("Erasing Circle");
super.dispose();
}
}
 
class Triangle extends Shape {
Triangle(int i) {
super(i);
print("Drawing Triangle");
}
void dispose() {
print("Erasing Triangle");
super.dispose();
}
}
 
class Line extends Shape {
private int start, end;
Line(int start, int end) {
super(start);
this.start = start;
this.end = end;
print("Drawing Line: " + start + ", " + end);
}
void dispose() {
print("Erasing Line: " + start + ", " + end);
super.dispose();
}
}
 
public class CADSystem extends Shape {
private Circle c;
private Triangle t;
private Line[] lines = new Line[3];
public CADSystem(int i) {
super(i + 1);
for(int j = 0; j < lines.length; j++)
lines[j] = new Line(j, j*j);
c = new Circle(1);
t = new Triangle(1);
print("Combined constructor");
}
public void dispose() {
print("CADSystem.dispose()");
// Завершение осуществляется в порядке,
// обратном порядку инициализации
t.dispose();
c.dispose();
for(int i = lines.length - 1; i >= 0; i--)
lines[i].dispose();
super.dispose();
}
public static void main(String[] args) {
CADSystem x = new CADSystem(47);
try {
// Код и обработка исключений...
} finally {
x.dispose();
}
}
}

<spoiler text="Output:">

Shape constructor
Shape constructor
Drawing Line: 0, 0
Shape constructor
Drawing Line: 1, 1
Shape constructor
Drawing Line: 2, 4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem.dispose()
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line: 2, 4
Shape dispose
Erasing Line: 1, 1
Shape dispose
Erasing Line: 0, 0
Shape dispose
Shape dispose

</spoiler>
Все в этой системе является некоторой разновидностью класса Shape (который, в свою очередь, неявно наследует от корневого класса Object). Каждый класс переопределяет метод dispose() класса Shape, вызывая при этом версию метода из базового класса с помощью ключевого слова super.

Все конкретные классы, унаследованные от ShapeCircle, Triangle и Line, имеют конструкторы, которые просто выводят сообщение, хотя во время жизни объекта любой метод может сделать что-то, требующее очистки. В каждом классе есть свой собственный метод dispose(), который восстанавливает ресурсы, не связанные с памятью, к исходному состоянию до создания объекта.

В методе main() вы можете заметить два новых ключевых слова, которые будут подробно рассмотрены в главе 10: try и finally. Ключевое слово try показывает, что следующий за ним блок (ограниченный фигурными скобками) является защищенной секцией. Код в секции finally выполняется всегда, независимо от того, как прошло выполнение блока try. (При обработке исключений можно выйти из блока try некоторыми необычными способами.) В данном примере секция finally означает: «Что бы ни произошло, в конце всегда вызывать метод x.dispose()».

Также обратите особое внимание на порядок вызова завершающих методов для базового класса и объектов-членов в том случае, если они зависят друг от друга. В основном нужно следовать тому же принципу, что использует компилятор C++ при вызове деструкторов: сначала провести завершающие действия для вашего класса в последовательности, обратной порядку их создания. (Обычно для этого требуется, чтобы элементы базовых классов продолжали существовать.) Затем вызываются завершающие методы из базовых классов, как и показано в программе.

Во многих случаях завершающие действия не являются проблемой; достаточно дать сборщику мусора выполнить свою работу. Но уж если понадобилось провести их явно, сделайте это со всей возможной тщательностью и вниманием, так как в процессе сборки мусора трудно в чем-либо быть уверенным. Сборщик мусора вообще может не вызываться, а если он начнет работать, то объекты будут уничтожаться в произвольном порядке. Лучше не полагаться на сборщик мусора в ситуациях, где дело не касается освобождения памяти. Если вы хотите провести завершающие действия, создайте для этой цели свой собственный метод и не полагайтесь на метод finalize().

Сокрытие имен

Если какой-либо из методов базового класса Java был перегружен несколько раз, переопределение имени этого метода в производном классе не скроет ни одну из базовых версий (в отличие от C++). Поэтому перегрузка работает вне зависимости от того, где был определен метод — на текущем уровне или в базовом классе:

//: reusing/Hide.java
// Перегрузка имени метода из базового класса
// в производном классе не скроет базовую версию метода.
import static net.mindview.util.Print.*;
 
class Homer {
char doh(char c) {
print("doh(char)");
return 'd';
}
float doh(float f) {
print("doh(float)");
return 1.0f;
}
}
 
class Milhouse {}
 
class Bart extends Homer {
void doh(Milhouse m) {
print("doh(Milhouse)");
}
}
 
public class Hide {
public static void main(String[] args) {
Bart b = new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
}

<spoiler text="Output:">

doh(float)
doh(char)
doh(float)
doh(Milhouse)

</spoiler>
Мы видим, что все перегруженные методы класса Homer доступны классу Bart, хотя класс Bart и добавляет новый перегруженный метод (в C++ такое действие спрятало бы все методы базового класса). Как вы увидите в следующей главе, на практике при переопределении методов гораздо чаще используется точно такое же описание и список аргументов, как и в базовом классе. Иначе легко можно запутаться (и поэтому C++ запрещает это, чтобы предотвратить совершение возможной ошибки).

В Java SE5 появилась запись @Override; она не является ключевым словом, но может использоваться так, как если бы была им. Если вы собираетесь переопределить метод, используйте @Override, и компилятор выдаст сообщение об ошибке, если вместо переопределения будет случайно выполнена перегрузка:

//: reusing/Lisa.java
// {CompileTimeError} (Won't compile)
 
class Lisa extends Homer {
@Override
void doh(Milhouse m) {
System.out.println("doh(Milhouse)");
}
}

Композиция в сравнении с наследованием

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

Композиция в основном применяется, когда в новом классе необходимо использовать функциональность уже существующего класса, но не его интерфейс. То есть вы встраиваете объект, чтобы использовать его возможности в новом классе, а пользователь класса видит определенный вами интерфейс, но не замечает встроенных объектов. Для этого внедряемые объекты объявляются со спецификатором private.

Иногда требуется предоставить пользователю прямой доступ к композиции вашего класса, то есть сделать встроенный объект открытым (public). Встроенные объекты и сами используют сокрытие реализации, поэтому открытый доступ безопасен. Когда пользователь знает, что класс собирается из составных частей, ему значительно легче понять его интерфейс. Хорошим примером служит объект Саr (машина):

//: reusing/Car.java
// Композиция с использованием открытых объектов
 
class Engine {
public void start() {}
public void rev() {}
public void stop() {}
}
 
class Wheel {
public void inflate(int psi) {}
}
 
class Window {
public void rollup() {}
public void rolldown() {}
}
 
class Door {
public Window window = new Window();
public void open() {}
public void close() {}
}
 
public class Car {
public Engine engine = new Engine();
public Wheel[] wheel = new Wheel[4];
public Door
left = new Door(),
right = new Door(); // 2-door
public Car() {
for(int i = 0; i < 4; i++)
wheel[i] = new Wheel();
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
}

Так как композиция объекта является частью проведенного анализа задачи (а не просто частью реализации класса), объявление членов класса открытыми (public) помогает программисту-клиенту понять, как использовать класс, и облегчает создателю класса написание кода. Однако нужно все-таки помнить, что описанный случай является специфическим и в основном поля класса следует объявлять как private.

При использовании наследования вы берете уже существующий класс и создаете его специализированную версию. В основном это значит, что класс общего назначения адаптируется для конкретной задачи. Если чуть-чуть подумать, то вы поймете, что не имело бы смысла использовать композицию машины и средства передвижения — машина не содержит средства передвижения, она сама есть это средство. Взаимосвязь «является» выражается наследованием, а взаимосвязь «имеет» описывается композицией.

protected

После знакомства с наследованием ключевое слово protected наконец-то обрело смысл. В идеале закрытых членов private должно было быть достаточно. В реальности существуют ситуации, когда вам необходимо спрятать что-либо от ок­ружающего мира, тем не менее оставив доступ для производных классов.

Ключевое слово protected — дань прагматизму. Оно означает: «Член класса является закрытым (private) для пользователя класса, но для всех, кто наследует от класса, и для соседей по пакету он доступен». (В Java protected автоматически предоставляет доступ в пределах пакета.)

Лучше всего, конечно, объявлять поля класса как private — всегда стоит оставить за собою право изменять лежащую в основе реализацию. Управляемый доступ наследникам класса предоставляется через методы protected:

//: reusing/Orc.java
// Ключевое слово protected
import static net.mindview.util.Print.*;
 
class Villain {
private String name;
protected void set(String nm) { name = nm; }
public Villain(String name) { this.name = name; }
public String toString() {
return "I'm a Villain and my name is " + name;
}
}
 
public class Orc extends Villain {
private int orcNumber;
public Orc(String name, int orcNumber) {
super(name);
this.orcNumber = orcNumber;
}
public void change(String name, int orcNumber) {
set(name); // Доступно, так как объявлено protected
this.orcNumber = orcNumber;
}
public String toString() {
return "Orc " + orcNumber + ": " + super.toString();
}
public static void main(String[] args) {
Orc orc = new Orc("Limburger", 12);
print(orc);
orc.change("Bob", 19);
print(orc);
}
}

<spoiler text="Output:">

Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob

</spoiler>
Как видите, метод change() имеет доступ к методу set(), поскольку тот объявлен как protected. Также обратите внимание, что метод toString() класса Orс определяется с использованием версии этого метода из базового класса.

Восходящее преобразование типов

Самая важная особенность наследования заключается вовсе не в том, что оно предоставляет методы для нового класса, — наследование выражает отношения между новым и базовым классом. Ее можно выразить .следующим образом: «Новый класс имеет тип существующего класса».

Данная формулировка — не просто причудливый способ описания наследования, она напрямую поддерживается языком. В качестве примера рассмотрим базовый класс с именем Instrument для представления музыкальных инструментов и его производный класс Wind. Так как наследование означает, что все методы базового класса также доступны в производном классе, любое сообщение, которое вы в состоянии отправить базовому классу, можно отправить и производному классу.

Если в классе Instrument имеется метод play(), то он будет присутствовать и в классе Wind. Таким образом, мы можем со всей определенностью утверждать, что объекты Wind также имеют тип Instrument. Следующий пример показывает, как компилятор поддерживает такое понятие:

//: reusing/Wind.java
// Наследование и восходящее преобразование.
 
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
}
}
 
// Объекты Wind также являются объектами Instrument,
// поскольку они имеют тот же интерфейс:
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Восходящее преобразование
}
}

Наибольший интерес в этом примере представляет метод tune(), получающий ссылку на объект Instrument. Однако в методе Wind.main() методу tune() передается ссылка на объект Wind. С учетом всего, что говорилось о строгой проверке типов в Java, кажется странным, что метод с готовностью берет один тип вместо другого. Но стоит вспомнить, что объект Wind также является объектом Instrument, и не существует метода, который можно вызвать в методе tune() для объектов Instrument, но нельзя для объектов Wind. В методе tune() код работает для Instrument и любых объектов, производных от Instrument, а преобразование ссылки на объект Wind в ссылку на объект Instrument называется восходящим преобразованием типов (upcasting).

Почему «восходящее преобразование»?

Термин возник по историческим причинам: традиционно на диаграммах наследования корень иерархии изображался у верхнего края страницы, а диаграмма разрасталась к нижнему краю страницы. (Конечно, вы можете рисовать свои диаграммы так, как сочтете нужным.) Для файла Wind.java диаграмма наследования выглядит так:

Файл:P0187.png

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

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

Преобразование также может выполняться и в обратном направлении — так называемое нисходящее преобразование (downcasting). Но при этом возникает проблема, которая рассматривается в главе 1.1.

Снова о композиции с наследованием

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

Ключевое слово final

В Java смысл ключевого слова final зависит от контекста, но в основном оно означает: «Это нельзя изменить». Запрет на изменения может объясняться двумя причинами: архитектурой программы или эффективностью. Эти две причины основательно различаются, поэтому в программе возможно неверное употребление ключевого слова final.
В следующих разделах обсуждаются три возможных применения final: для данных, методов и классов.

Неизменные данные

Во многих языках программирования существует тот или иной способ сказать компилятору, что частица данных является «константой». Константы полезны в двух ситуациях:


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

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

Поле, одновременно объявленное с ключевыми словами static и final, существует в памяти в единственном экземпляре и не может быть изменено.

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

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

//: reusing/FinalData.java
// Действие ключевого слова final для полей.
import java.util.*;
import static net.mindview.util.Print.*;
 
class Value {
int i; // доступ в пределах пакета
public Value(int i) { this.i = i; }
}
 
public class FinalData {
private static Random rand = new Random(47);
private String id;
public FinalData(String id) { this.id = id; }
// Могут быть константами времени компиляции:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
// Типичная открытая константа:
public static final int VALUE_THREE = 39;
// He может быть константой времени компиляции:
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
// Массивы:
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // Ошибка значение нельзя изменить
fd1.v2.i++; // Объект не является неизменным!
fd1.v1 = new Value(9); // OK - не является неизменным
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Объект не является неизменным!
//! fd1.v2 = new Value(0); // Ошибка: ссылку
//! fd1.VAL_3 = new Value(1); // нельзя изменить
//! fd1.a = new int[3];
print(fd1);
print("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
print(fd1);
print(fd2);
}
}

<spoiler text="Output:">

fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18

</spoiler>
Так как valueOne и VALUE_TWO являются примитивными типами со значениями, заданными на стадии компиляции, они оба могут использоваться в качестве констант времени компиляции, и принципиальных различий между ними нет. Константа VALUE_THREE демонстрирует общепринятый способ определения подобных полей: спецификатор public открывает к ней доступ за пределами пакета; ключевое слово static указывает, что она существует в единственном числе, а ключевое слово final указывает, что ее значение остается неизменным. Заметьте, что примитивы final static с неизменными начальными значениями (то есть константы времени компиляции) записываются целиком заглавными буквами, а слова разделяются подчеркиванием (эта схема записи констант позаимствована из языка C).

Само по себе присутствие final еще не означает, что значение переменной известно уже на стадии компиляции. Данный факт продемонстрирован на примере инициализации І4 и INT_5 с использованием случайных чисел. Эта часть про­граммы также показывает разницу между статическими и нестатическими константами. Она проявляется только при инициализации во время исполнения, так как все величины времени компиляции обрабатываются компилятором одинаково (и обычно просто устраняются с целью оптимизации). Различие проявляется в результатах запуска программы. Заметьте, что значения поля І4 для объектов fdl и fd2 уникальны, но значение поля INT_5 не изменяется при создании второго объекта FinalData. Дело в том, что поле INT_5 объявлено как static, поэтому оно инициализируется только один раз во время загрузки класса.

Переменные от v1 до VAL_3 поясняют смысл объявления ссылок с ключевым словом final. Как видно из метода main(), объявление ссылки v2 как final еще не означает, что ее объект неизменен. Однако присоединить ссылку v2 к новому объекту не получится, как раз из-за того, что она была объявлена как final. Именно такой смысл имеет ключевое слово final по отношению к ссылкам. Вы также можете убедиться, что это верно и для массивов, которые являются просто другой разновидностью ссылки. Пожалуй, для ссылок ключевое слово final обладает меньшей практической ценностью, чем для примитивов.

Пустые константы

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

//: reusing/BlankFinal.java
// "Пустые" неизменные поля.
class Poppet {
private int i;
Poppet(int ii) { i = ii; }
}
 
public class BlankFinal {
private final int i = 0; // Инициализированная константа
private final int j; // Пустая константа
private final Poppet p; // Пустая константа-ссылка
// Пустые константы НЕОБХОДИМО инициализировать в конструкторе:
public BlankFinal() {
j = 1; // Инициализация пустой константы
p = new Poppet(1); // Инициализация пустой неизменной ссылки
}
public BlankFinal(int x) {
j = x; // Инициализация пустой константы
p = new Poppet(x); // Инициализация пустой неизменной ссылки
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(47);
}
}

Значения неизменных (final) переменных обязательно должны присваиваться или в выражении, записываемом в точке определения переменной, или в каждом из конструкторов класса. Тем самым гарантируется инициализация полей, объявленных как final, перед их использованием.

Неизменные аргументы

Java позволяет вам объявлять неизменными аргументы метода, объявляя их с ключевым словом final в списке аргументов. Это значит, что метод не может изменить значение, на которое указывает передаваемая ссылка:

//: reusing/FinalArguments.java
// Использование final с аргументами метода
 
class Gizmo {
public void spin() {}
}
 
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // запрещено -- g объявлено final
}
void without(Gizmo g) {
g = new Gizmo(); // Разрешено -- g не является final
g.spin();
}
// void f(final int i) { i++; } // Нельзя изменять
// неизменные примитивы доступны только для чтения:
int g(final int i) { return i + 1; }
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
}

Методы f() и g() показывают, что происходит при передаче методу примитивов с пометкой final: их значение можно прочитать, но изменить его не удастся.

Неизменные методы

Неизменные методы используются по двум причинам. Первая причина — «блокировка» метода, чтобы производные классы не могли изменить его содержание. Это делается по соображениям проектирования, когда вам точно надо знать, что поведение метода не изменится при наследовании.

Второй причиной в прошлом считалась эффективность. В более ранних реализациях Java объявление метода с ключевым словом final позволяло компилятору превращать все вызовы такого метода во встроенные (inline). Когда компилятор видит метод, объявленный как final, он может (на свое усмотрение) пропустить стандартный механизм вставки кода для проведения вызова метода (занести аргументы в стек, перейти к телу метода, исполнить находящийся там код, вернуть управление, удалить аргументы из стека и распорядиться возвращенным значением) и вместо этого подставить на место вызова копию реального кода, находящегося в теле метода. Таким образом устраняются издержки обычного вызова метода. Конечно, для больших методов подстановка приведет к «разбуханию» программы, и, скорее всего, никаких преимуществ от использования прямого встраивания не будет.

В последних версиях Java виртуальная машина выявляет подобные ситуации и устраняет лишние передачи управления при оптимизации, поэтому использовать final для методов уже не обязательно — и более того, нежелательно.

Cпецификаторы final и private

Любой закрытый (private) метод в классе косвенно является неизменным (final) методом. Так как вы не в силах получить доступ к закрытому методу, то не сможете и переопределить его. Ключевое слово final можно добавить к закрытому методу, но его присутствие ни на что не повлияет.

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

//: reusing/FinalOverridingIllusion.java
// Все выглядит так, будто закрытый (и неизменный) метод
// можно переопределить, но это заблуждение.
import static net.mindview.util.Print.*;
 
class WithFinals {
// To же, что и просто private:
private final void f() { print("WithFinals.f()"); }
// Также автоматически является final:
private void g() { print("WithFinals.g()"); }
}
 
class OverridingPrivate extends WithFinals {
private final void f() {
print("OverridingPrivate.f()");
}
private void g() {
print("OverridingPrivate.g()");
}
}
 
class OverridingPrivate2 extends OverridingPrivate {
public final void f() {
print("OverridingPrivate2.f()");
}
public void g() {
print("OverridingPrivate2.g()");
}
}
 
public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// Можно провести восходящее преобразование:
OverridingPrivate op = op2;
// Но методы при этом вызвать невозможно:
//! op.f();
//! op.g();
// И то же самое здесь:
WithFinals wf = op2;
//! wf.f();
//! wf.g();
}
}

<spoiler text="Output:">

OverridingPrivate2.f()
OverridingPrivate2.g()

</spoiler>
«Переопределение» применимо только к компонентам интерфейса базового класса. Иначе говоря, вы должны иметь возможность выполнить восходящее преобразование объекта к его базовому типу и вызвать тот же самый метод (это утверждение подробнее обсуждается в следующей главе). Если метод объявлен как private, он не является частью интерфейса базового класса; это просто некоторый код, скрытый внутри класса, у которого оказалось то же имя. Если вы создаете в производном классе одноименный метод со спецификатором public, protected или с доступом в пределах пакета, то он никак не связан с закрытым методом базового класса. Так как privat-метод недоступен и фактически невидим для окружающего мира, он не влияет ни на что, кроме внутренней организации кода в классе, где он был описан.

Неизменные классы

Объявляя класс неизменным (записывая в его определении ключевое слово final), вы показываете, что не собираетесь использовать этот класс в качестве базового при наследовании и запрещаете это делать другим. Другими словами, по какой-то причине структура вашего класса должна оставаться постоянной — или же появление субклассов нежелательно по соображениям безопасности.

//: reusing/Jurassic.java
// Объявление неизменным всего класса.
 
class SmallBrain {}
 
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
 
//! class Further extends Dinosaur {}
// Ошибка: Нельзя расширить неизменный класс Dinosaur
 
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
}

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

Предостережение

На первый взгляд идея объявления неизменных методов (final) во время разработки класса выглядит довольно заманчиво — никто не сможет переопределить ваши методы. Иногда это действительно так.
Но будьте осторожнее в своих допущениях. Трудно предусмотреть все возможности повторного использования класса, особенно для классов общего назначения. Определяя метод как final, вы блокируете возможность использования класса в проектах других программистов только потому, что сами не могли предвидеть такую возможность.

Хорошим примером служит стандартная библиотека Java. Класс Vector Java 1.0/1.1 часто использовался на практике и был бы еще полезнее, если бы по соображениям эффективности (в данном случае эфемерной) все его методы не были объявлены как final. Возможно, вам хотелось бы создать на основе Vector производный класс и переопределить некоторые методы, но разработчики почему-то посчитали это излишним.

Ситуация выглядит еще более парадоксальной по двум причинам. Во-первых, класс Stack унаследован от Vector, и это значит, что Stack есть Vector, а это неверно с точки зрения логики. Тем не менее мы видим пример ситуации, в которой сами проектировщики Java используют наследование от Vector.

Во-вторых, многие полезные методы класса Vector, такие как addElement() и elementAt(), объявлены с ключевым словом synchronized. Как вы увидите в главе 12, синхронизация сопряжена со значительными издержками во время выполнения, которые, вероятно, сводят к нулю все преимущества от объявления метода как final.

Все это лишь подтверждает теорию о том", что программисты не умеют правильно находить области для применения оптимизации. Очень плохо, что такой неуклюжий дизайн проник в стандартную библиотеку Java. (К счастью, современная библиотека контейнеров Java заменяет Vector классом ArrayList, который сделан гораздо более аккуратно и по общепринятым нормам. К сожалению, существует очень много готового кода, написанного с использованием старой библиотеки контейнеров.)

Инициализация и загрузка классов

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

В языке Java таких проблем не существует, поскольку в нем используется другой подход к загрузке. Вспомните, что скомпилированный код каждого класса хранится в отдельном файле. Этот файл не загружается, пока не возникнет такая необходимость. В сущности, код класса загружается только в точке его первого использования. Обычно это происходит при создании первого объекта класса, но загрузка также выполняется при обращениях к статическим полям или методам.

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

Инициализация с наследованием

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

//: reusing/Beetle.java
// Полный процесс инициализации.
import static net.mindview.util.Print.*;
 
class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized");
static int printInit(String s) {
print(s);
return 47;
}
}
 
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle() {
print("k = " + k);
print("j = " + j);
}
private static int x2 =
printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
print("Beetle constructor");
Beetle b = new Beetle();
}
}

<spoiler text="Output:">

static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39

</spoiler>
Запуск класса Beetle в Java начинается с выполнения метода Beetle.main() (статического), поэтому загрузчик пытается найти скомпилированный код класса Beetle (он должен находиться в файле Beetle.class). При этом загрузчик обнаруживает, что у класса имеется базовый класс (о чем говорит ключевое слово extends), который затем и загружается. Это происходит независимо от того, собираетесь вы создавать объект базового класса или нет. (Чтобы убедиться в этом, попробуйте закомментировать создание объекта.)

Если у базового класса имеется свой базовый класс, этот второй базовый класс будет загружен в свою очередь, и т. д. Затем проводится static-инициализация корневого базового класса (в данном случае это Insect), затем следующего за ним производного класса, и т. д. Это важно, так как производный класс и инициализация его static-объектов могут зависеть от инициализации членов базового класса.

В этой точке все необходимые классы уже загружены, и можно переходить к созданию объекта класса. Сначала всем примитивам данного объекта присваиваются значения по умолчанию, а ссылкам на объекты задается значение null — это делается за один проход посредством обнуления памяти. Затем вызывается конструктор базового класса. В нашем случае вызов происходит автоматически, но вы можете явно указать в программе вызов конструктора базового класса (записав его в первой строке описания конструктора Beetle()) с помощью ключевого слова super. Конструирование базового класса выполняется по тем же правилам и в том же порядке, что и для производного класса. После завершения работы конструктора базового класса инициализируются переменные, в порядке их определения. Наконец, выполняется оставшееся тело конструктора.

Резюме

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

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

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

]]>
Книги по Java https://linexp.ru?id=4746 Wed, 29 Jun 2022 14:29:07 GMT
<![CDATA[Глава 8 Thinking in Java 4th edition]]> ПОЛИМОРФИЗМПолиморфизм является третьей неотъемлемой чертой объектно-ориентированного языка, вместе с абстракцией данных и наследованием.

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

ПОЛИМОРФИЗМ

Полиморфизм является третьей неотъемлемой чертой объектно-ориентированного языка, вместе с абстракцией данных и наследованием.

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

Инкапсуляция создает новые типы данных за счет объединения характеристик и поведения. Сокрытие реализации отделяет интерфейс от реализации за счет изоляции технических подробностей в private-частях класса. Подобное механическое разделение понятно любому, кто имел опыт работы с процедурными языками.
Но полиморфизм имеет дело с логическим разделением в контексте типов. В предыдущей главе вы увидели, что наследование позволяет работать с объектом, используя как его собственный тип, так и его базовый тип.

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

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

Снова о восходящем преобразовании

Как было показано в главе 7, с объектом можно работать с использованием как его собственного типа, так и его базового типа. Интерпретация ссылки на объект как ссылки на базовый тип называется восходящим преобразованием.

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

//: polymorphism/music/Note.java
// Ноты для игры на музыкальных инструментах.
package polymorphism.music;
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}

Перечисления были представлены в главе 5. В следующем примере Wind является частным случаем инструмента (Instrument), поэтому класс Wind наследует от Instrument:

//: polymorphism/music/Instrument.java
package polymorphism.music;
import static net.mindview.util.Print.*;
 
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
 
//: polymorphism/music/Wind.java
package polymorphism.music;
 
// Объекты Wind также являются объектами Instrument
// поскольку имеют тот же интерфейс:
public class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
 
//: polymorphism/music/Music.java
// Наследование и восходящее преобразование
package polymorphism.music;
 
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Восходящее преобразование
}
}

<spoiler text="Output:">

Wind.play() MIDDLE_C

</spoiler>
Метод Music.tune() получает ссылку на Instrument, но последняя также может указывать на объект любого класса, производного от Instrument. В методе main() ссылка на объект Wind передается методу tune() без явных преобразований. Это нормально; интерфейс класса Instrument должен существовать и в классе Wind, поскольку последний был унаследован от Instrument. Восходящее преобразование от Wind к Instrument способно «сузить» этот интерфейс, но не сделает его «меньше», чем полный интерфейс класса Instrument.

Потеря типа объекта

Программа Music.java выглядит немного странно. Зачем умышленно игнорировать фактический тип объекта? Именно это мы наблюдаем при восходящем преобразовании, и казалось бы, программа стала яснее, если бы методу tune() передавалась ссылка на объект Wind. Но при этом мы сталкиваемся с очень важным обстоятельством: если поступить подобным образом, то потом придется писать новый метод tune() для каждого типа Instrument, присутствующего в системе. Предположим, что в систему были добавлены новые классы Stringed и Brass:

//: polymorphism/music/Music2.java
// Перегрузка вместо восходящего преобразования
package polymorphism.music;
import static net.mindview.util.Print.*;
 
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
 
class Brass extends Instrument {
public void play(Note n) {
print("Brass.play() " + n);
}
}
 
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // Без восходящего преобразования
tune(violin);
tune(frenchHorn);
}
}

<spoiler text="Output:">

Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C

</spoiler>
Программа работает, но у нее есть огромный недостаток: для каждого нового Instrument приходится писать новый, зависящий от конкретного типа метод tune(). Объем программного кода увеличивается, а при добавлении нового метода (такого, как tune()) или нового типа инструмента придется выполнить немало дополнительной работы. А если учесть, что компилятор не выводит сообщений об ошибках, если вы забудете перегрузить один из ваших методов, весь процесс работы с типами станет совершенно неуправляемым.

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

Именно это и позволяет делать полиморфизм. Однако большинство программистов с опытом работы на процедурных языках при работе с полиморфизмом испытывают некоторые затруднения.

Особенности

Сложности с программой Music.java обнаруживаются после ее запуска. Она выводит строку Wind.play(). Именно это и требуется, но не понятно, откуда берется такой результат. Взгляните на метод tune():

 public static void tune(Instrument i) {
//...
і.play(Note.MIDDLE_C),
}

Метод получает ссылку на объект Instrument. Как компилятор узнает, что ссылка на Instrument в данном случае указывает на объект Wind, а не на Brass или Stringed? Компилятор и не знает. Чтобы в полной мере разобраться в сути про­исходящего, необходимо рассмотреть понятие связывания (binding).

Связывание «метод-вызов»

Присоединение вызова метода к телу метода называется связыванием. Если связывание проводится перед запуском программы (компилятором и компоновщиком, если он есть), оно называется ранним связыванием (early binding). Возможно, ранее вам не приходилось слышать этот термин, потому что в процедурных языках никакого выбора связывания не было. Компиляторы C поддерживают только один тип вызова — раннее связывание.

Неоднозначность предыдущей программы кроется именно в раннем связывании: компилятор не может знать, какой метод нужно вызывать, когда у него есть только ссылка на объект Instrument.
Проблема решается благодаря позднему связыванию (late binding), то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называют динамическим (dynamic) или связыванием на стадии выполнения (runtime binding).

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

Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как final (приватные методы являются final по умолчанию). Следовательно, вам не придется принимать решений относительно использования позднего связывания — оно осуществляется автоматически.
Зачем объявлять метод как final? Как уже было замечено в предыдущей главе, это запрещает переопределение соответствующего метода.

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

Получение нужного результата

Теперь, когда вы знаете, что связывание всех методов в Java осуществляется полиморфно, через позднее связывание, вы можете писать код для базового класса, не сомневаясь в том, что для всех производных классов он также будет работать верно. Другими словами, вы «посылаете сообщение объекту и позволяете ему решить, что следует делать дальше».

Классическим примером полиморфизма в ООП является пример с геометрическими фигурами. Он часто используется благодаря своей наглядности, но, к сожалению, некоторые новички начинают думать, что ООП подразумевает графическое программирование — а это, конечно же, неверно.

В примере с фигурами имеется базовый класс с именем Shape (фигура) и различные производные типы: Circle (окружность), Square (прямоугольник), Triangle (треугольник) и т. п. Выражения типа «окружность есть фигура» очевидны и не представляют трудностей для понимания. Взаимосвязи показаны на следующей диаграмме наследования:

Файл:P0203.png


Восходящее преобразование имеет место даже в такой простой команде:

 Shape s = new Circle();

Здесь создается объект Circle, и полученная ссылка немедленно присваивается типу Shape. На первый взгляд это может показаться ошибкой (присвоение одного типа другому), но в действительности все правильно, потому что тип Circle (окружность) является типом Shape (фигура) посредством наследования. Компилятор принимает команду и не выдает сообщения об ошибке.

Предположим, вызывается один из методов базового класса (из тех, что были переопределены в производных классах):

 s.draw();

Опять можно подумать, что вызывается метод draw() из класса Shape, раз имеется ссылка на объект Shape — как компилятор может сделать что-то другое? И все же будет вызван правильный метод Circle.draw(), так как в программе используется позднее связывание (полиморфизм).
Следующий пример показывает несколько другой подход:

//: polymorphism/shape/Shape.java
package polymorphism.shape;
public class Shape {
public void draw() {}
public void erase() {}
}
 
//: polymorphism/shape/Circle.java
package polymorphism.shape;
import static net.mindview.util.Print.*;
public class Circle extends Shape {
public void draw() { print("Circle.draw()"); }
public void erase() { print("Circle.erase()"); }
}
 
//: polymorphism/shape/Triangle.java
package polymorphism.shape;
import static net.mindview.util.Print.*;
public class Triangle extends Shape {
public void draw() { print("Triangle.draw()"); }
public void erase() { print("Triangle.erase()"); }
}
 
//: polymorphism/shape/Square.java
package polymorphism.shape;
import static net.mindview.util.Print.*;
public class Square extends Shape {
public void draw() { print("Square.draw()"); }
public void erase() { print("Square.erase()"); }
}
 
//: polymorphism/shape/RandomShapeGenerator.java
// "Фабрика" случайных фигур.
package polymorphism.shape;
import java.util.*;
public class RandomShapeGenerator {
private Random rand = new Random(47);
public Shape next() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
 
 
//: polymorphism/Shapes.java
// Polymorphism in Java.
import polymorphism.shape.*;
 
public class Shapes {
private static RandomShapeGenerator gen =
new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = gen.next();
// Make polymorphic method calls:
for(Shape shp : s)
shp.draw();
}
}

<spoiler text="Output:">

Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()

</spoiler>
Базовый класс Shape устанавливает общий интерфейс для всех классов, производных от Shape — то есть любую фигуру можно нарисовать (draw()) и стереть (erase()). Производные классы переопределяют этот интерфейс, чтобы реализовать уникальное поведение для каждой конкретной фигуры.

Класс RandomShapeGenerator — своего рода «фабрика», при каждом вызове метода next() производящая ссылку на случайно выбираемый объект Shape. Заметьте, что восходящее преобразование выполняется в командах return, каждая из которых получает ссылку на объект Circle, Square или Triangle, а выдает ее за пределы next() в виде возвращаемого типа Shape. Таким образом, при вызове этого метода вы не сможете определить конкретный тип объекта, поскольку всегда получаете просто Shape.

Метод main() содержит массив ссылок на Shape, который заполняется последовательными вызовами RandomShapeGenerator.next(). К этому моменту вам известно, что имеются объекты Shape, но вы не знаете об этих объектах ничего конкретного (так же, как и компилятор). Но если перебрать содержимое массива и вызвать draw() для каждого его элемента, то, как по волшебству, произойдет верное, свойственное для определенного типа действие — в этом нетрудно убедиться, взглянув на результат работы программы.

Случайный выбор фигур в нашем примере всего лишь помогает понять, что компилятор во время компиляции кода не располагает информацией о том, какую реализацию следует вызывать. Все вызовы метода draw() проводятся с при­менением позднего связывания.

Расширяемость

Теперь вернемся к программе Music.java. Благодаря полиморфизму вы можете добавить в нее сколько угодно новых типов, не изменяя метод tune(). В хорошо спланированной ООП-программе большая часть ваших методов (или даже все методы) следуют модели метода tune(), оперируя только с интерфейсом базового класса.

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

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

Файл:P0206.png

Все новые классы правильно работают со старым, неизмененным методом tune(). Даже если метод tune() находится в другом файле, а к классу Instrument присоединяются новые методы, он все равно будет работать верно без повторной компиляции. Ниже приведена реализация рассмотренной диаграммы:

//: polymorphism/music3/Music3.java
// Расширяемая программа
package polymorphism.music3;
import polymorphism.music.Note;
import static net.mindview.util.Print.*;
 
class Instrument {
void play(Note n) { print("Instrument.play() " + n); }
String what() { return "Instrument"; }
void adjust() { print("Adjusting Instrument"); }
}
 
class Wind extends Instrument {
void play(Note n) { print("Wind.play() " + n); }
String what() { return "Wind"; }
void adjust() { print("Adjusting Wind"); }
}
 
class Percussion extends Instrument {
void play(Note n) { print("Percussion.play() " + n); }
String what() { return "Percussion"; }
void adjust() { print("Adjusting Percussion"); }
}
 
class Stringed extends Instrument {
void play(Note n) { print("Stringed.play() " + n); }
String what() { return "Stringed"; }
void adjust() { print("Adjusting Stringed"); }
}
 
class Brass extends Wind {
void play(Note n) { print("Brass.play() " + n); }
void adjust() { print("Adjusting Brass"); }
}
 
class Woodwind extends Wind {
void play(Note n) { print("Woodwind.play() " + n); }
String what() { return "Woodwind"; }
}
 
public class Music3 {
// Работа метода не зависит от фактического типа объекта,
// поэтому типы, добавленные в систему, будут работать правильно:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}

<spoiler text="Output:">

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

</spoiler>
Новый метод what() возвращает строку (String) с информацией о классе, а метод adjust() предназначен для настройки инструментов.

В методе main() сохранение любого объекта в массиве orchestra автоматически приводит к выполнению восходящего преобразования к типу Instrument.

Вы можете видеть, что метод tune() изолирован от окружающих изменений кода, но при этом все равно работает правильно. Для достижения такой функциональности и используется полиморфизм. Изменения в коде не затрагивают те части программы, которые не зависят от них. Другими словами, полиморфизм помогает отделить «изменяемое от неизменного».

Проблема: «переопределение» закрытых методов

Перед вами одна из ошибок, совершаемых по наивности:

//: polymorphism/PrivateOverride.java
// Попытка переопределения приватного метода
package polymorphism;
import static net.mindview.util.Print.*;
 
public class PrivateOverride {
private void f() { print("private f()"); }
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
 
class Derived extends PrivateOverride {
public void f() { print("public f()"); }
}

<spoiler text="Output:">

private f()

</spoiler>
Вполне естественно было бы ожидать, что программа выведет сообщение public f(), но закрытый (private) метод автоматически является неизменным (final), а заодно и скрытым от производного класса. Так что метод f() класса Derived в нашем случае является полностью новым — он даже не был перегружен, так как метод f() базового класса классу Derived недоступен.

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

Конструкторы и полиморфизм

Конструкторы отличаются от обычных методов, и эти отличия проявляются и при использовании полиморфизма. Хотя конструкторы сами по себе не полиморфны (фактически они представляют собой статические методы, только ключевое слово static опущено), вы должны хорошо понимать, как работают конструкторы в сложных полиморфных иерархиях. Такое понимание в дальнейшем поможет избежать некоторых затруднительных ситуаций.

Порядок вызова конструкторов

Порядок вызова конструкторов коротко обсуждался в главах 5 и 7, но в то время мы еще не рассматривали полиморфизм.
Конструктор базового класса всегда вызывается в процессе конструирования производного класса. Вызов автоматически проходит вверх по цепочке наследования, так что в конечном итоге вызываются конструкторы всех базовых классов по всей цепочке наследования. Это очень важно, поскольку конструктору отводится особая роль — обеспечивать правильное построение объектов.

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

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

Следующий пример показывает, как композиция, наследование и полиморфизм влияют на порядок конструирования:

//: polymorphism/Sandwich.java
// Порядок вызова конструкторов.
package polymorphism;
import static net.mindview.util.Print.*;
 
class Meal {
Meal() { print("Meal()"); }
}
 
class Bread {
Bread() { print("Bread()"); }
}
 
class Cheese {
Cheese() { print("Cheese()"); }
}
 
class Lettuce {
Lettuce() { print("Lettuce()"); }
}
 
class Lunch extends Meal {
Lunch() { print("Lunch()"); }
}
 
class PortableLunch extends Lunch {
PortableLunch() { print("PortableLunch()");}
}
 
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() { print("Sandwich()"); }
public static void main(String[] args) {
new Sandwich();
}
}

<spoiler text="Output:">

Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()

</spoiler>
В этом примере создается сложный класс, собранный из других классов, и в каждом классе имеется конструктор, который сообщает о своем выполнении. Самый важный класс — Sandwich, с тремя уровнями наследования (четырьмя, если считать неявное наследование от класса Object) и тремя встроенными объектами. Результат виден при создании объекта Sandwich в методе main(). Это значит, что конструкторы для сложного объекта вызываются в следующей по­следовательности:


  • Сначала вызывается конструктор базового класса. Этот шаг повторяется рекурсивно: сначала конструируется корень иерархии, затем следующий за ним класс, затем следующий за этим классом класс и т. д., пока не достигается «низший» производный класс.
  • Проводится инициализация членов класса в порядке их объявления.
  • Вызывается тело конструктора производного класса.

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

При вызове нормального метода известно, что конструирование уже было проведено, поэтому все части объекта инициализированы. Однако в конструкторе вы также должны быть уверены в том, что все используемые члены уже проинициализированы. Это можно гарантировать только одним способом — сначала вызывать конструктор базового класса.

В дальнейшем при выполнении конструктора производного класса можно быть уверенным в том, что все члены базового класса уже инициализированы. Гарантия действительности всех членов в конструкторе — важная причина, по которой все встроенные объекты (то есть объекты, помещенные в класс посредством композиции) инициализируются на месте их определения (как в рассмотренном примере сделано с объектами b, с и l). Если вы будете следовать этому правилу, это усилит уверенность в том, что все члены базового класса и объекты-члены были проинициализированы. К сожалению, это помогает не всегда, в чем вы убедитесь в следующем разделе.

Наследование и завершающие действия

Если при создании нового класса используется композиция и наследование, обычно вам не приходится беспокоиться о проведении завершающих действий — подобъекты уничтожаются сборщиком мусора. Но если вам необходимо провести завершающие действия, создайте в своем классе метод dispose() (в данном разделе я решил использовать такое имя; возможно, вы придумаете более удачное название). Переопределяя метод dispose() в производном классе, важно помнить о вызове версии этого метода из базового класса, поскольку иначе не будут выполнены завершающие действия базового класса. Следующий пример доказывает справедливость этого утверждения:

//: polymorphism/Frog.java
// Наследование и завершающие действия.
package polymorphism;
import static net.mindview.util.Print.*;
 
class Characteristic {
private String s;
Characteristic(String s) {
this.s = s;
print("Creating Characteristic " + s);
}
protected void dispose() {
print("disposing Characteristic " + s);
}
}
 
class Description {
private String s;
Description(String s) {
this.s = s;
print("Creating Description " + s);
}
protected void dispose() {
print("disposing Description " + s);
}
}
// живое существо
class LivingCreature {
private Characteristic p =
new Characteristic("is alive");
private Description t =
new Description("Basic Living Creature");
LivingCreature() {
print("LivingCreature()");
}
protected void dispose() {
print("LivingCreature dispose");
t.dispose();
p.dispose();
}
}
// животное
class Animal extends LivingCreature {
private Characteristic p =
new Characteristic("has heart");
private Description t =
new Description("Animal not Vegetable");
Animal() { print("Animal()"); }
protected void dispose() {
print("Animal dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
// земноводное
class Amphibian extends Animal {
private Characteristic p =
new Characteristic("can live in water");
private Description t =
new Description("Both water and land");
Amphibian() {
print("Amphibian()");
}
protected void dispose() {
print("Amphibian dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
// лягушка
public class Frog extends Amphibian {
private Characteristic p = new Characteristic("Croaks");
private Description t = new Description("Eats Bugs");
public Frog() { print("Frog()"); }
protected void dispose() {
print("Frog dispose");
t.dispose();
p.dispose();
super.dispose();
}
public static void main(String[] args) {
Frog frog = new Frog();
print("Bye!");
frog.dispose();
}
}

<spoiler text="Output:">

Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristic has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both water and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive

</spoiler>
Каждый класс в иерархии содержит объекты классов Characteristic и Description, которые также необходимо «завершать». Очередность завершения должна быть обратной порядку инициализации в том случае, если объекты зависят друг от друга. Для полей это означает порядок, обратный последовательности объявления полей в классе (инициализация соответствует порядку объявления).

В базовых классах сначала следует выполнять финализацию для производного класса, а затем — для базового класса. Это объясняется тем, что завершающий метод производного класса может вызывать некоторые методы базового класса, для которых необходимы действительные компоненты базового класса. Из результатов работы программы видно, что все части объекта Frog будут финализованы в порядке, противоположном очередности их создания.

Также обратите внимание на то, что в описанном примере объект Frog является «владельцем» встроенных объектов. Он создает их, определяет продолжительность их существования (до тех пор, пока существует Frog) и знает, когда вызывать dispose() для встроенных объектов.

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

//: polymorphism/ReferenceCounting.java
// Уничтожение совместно используемых встроенных объектов
import static net.mindview.util.Print.*;
 
class Shared {
private int refcount = 0;
private static long counter = 0;
private final long id = counter++;
public Shared() {
print("Creating " + this);
}
public void addRef() { refcount++; }
protected void dispose() {
if(--refcount == 0)
print("Disposing " + this);
}
public String toString() { return "Shared " + id; }
}
 
class Composing {
private Shared shared;
private static long counter = 0;
private final long id = counter++;
public Composing(Shared shared) {
print("Creating " + this);
this.shared = shared;
this.shared.addRef();
}
protected void dispose() {
print("disposing " + this);
shared.dispose();
}
public String toString() { return "Composing " + id; }
}
 
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new Shared();
Composing[] composing = { new Composing(shared),
new Composing(shared), new Composing(shared),
new Composing(shared), new Composing(shared) };
for(Composing c : composing)
c.dispose();
}
}

<spoiler text="Output:">

Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0

</spoiler>
В переменной static long counter хранится количество созданных экземпляров Shared. Для счетчика выбран тип long вместо int для того, чтобы предотвратить переполнение (это всего лишь хороший стиль программирования; в рас­сматриваемых примерах переполнение вряд ли возможно). Поле id объявлено со спецификатором final, поскольку его значение остается постоянным на протяжении жизненного цикла объекта.

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

Поведение полиморфных методов при вызове из конструкторов

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

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

По определению, задача конструктора — дать объекту жизнь (и это отнюдь не простая задача). Внутри любого конструктора объект может быть сформирован лишь частично — известно только то, что объекты базового класса были проини- циализированы. Если конструктор является лишь очередным шагом на пути построения объекта класса, производного от класса данного конструктора, «производные» части еще не были инициализированы на момент вызова текущего конструктора.

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

//: polymorphism/PolyConstructors.java
// Конструкторы и полиморфизм дают не тот
// результат, который можно было бы ожидать
import static net.mindview.util.Print.*;
 
class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
 
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
}
 
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}

<spoiler text="Output:">

Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

</spoiler>
Метод Glyph.draw() изначально предназначен для переопределения в производных классах, что и происходит в RoundGlyph. Но конструктор Glyph вызывает этот метод, и в результате это приводит к вызову метода RoundGlyph.draw(), что вроде бы и предполагалось.

Однако из результатов работы программы видно — когда конструктор класса Glyph вызывает метод draw(), переменной radius еще не присвоено даже значение по умолчанию 1. Переменная равна 0. В итоге класс может не выполнить свою задачу, а вам придется долго всматриваться в код программы, чтобы определить причину неверного результата.

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


  • Память, выделенная под новый объект, заполняется двоичными нулями.
  • Конструкторы базовых классов вызываются в описанном ранее порядке. В этот момент вызывается переопределенный метод draw() (да, перед вызовом конструктора класса RoundGlyph), где обнаруживается, что переменная radius равна нулю из-за первого этапа.
  • Вызываются инициализаторы членов класса в порядке их определения.
  • Исполняется тело конструктора производного класса.

У происходящего есть и положительная сторона — по крайней мере, данные инициализируются нулями (или тем, что понимается под нулевым значением для определенного типа данных), а не случайным «мусором» в памяти. Это относится и к ссылкам на объекты, внедренные в класс с помощью композиции. Они принимают особое значение null.

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

При написании конструктора руководствуйтесь следующим правилом: не пытайтесь сделать больше для того, чтобы привести объект в нужное состояние, и по возможности избегайте вызова каких-либо методов. Единственные методы, которые можно вызывать в конструкторе без опаски — неизменные (final) методы базового класса. (Сказанное относится и к закрытым (private) методам, поскольку они автоматически являются неизменными.) Такие методы невозможно переопределить, и поэтому они застрахованы от «сюрпризов».

Ковариантность возвращаемых типов

В Java SE5 появилась концепция ковариантности возвращаемых типов; этот термин означает, что переопределенный метод производного класса может вернуть тип, производный от типа, возвращаемого методом базового класса:

//: polymorphism/CovariantReturn.java
 
class Grain {
public String toString() { return "Grain"; }
}
 
class Wheat extends Grain {
public String toString() { return "Wheat"; }
}
 
class Mill {
Grain process() { return new Grain(); }
}
 
class WheatMill extends Mill {
Wheat process() { return new Wheat(); }
}
 
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}

<spoiler text="Output:">

Grain
Wheat

</spoiler>
Главное отличие Java SE5 от предыдущих версий Java заключается в том, что старые версии заставляли переопределение process() возвращать Grain вместо Wheat, хотя тип Wheat, производный от Grain, является допустимым возвращаемым типом. Ковариантность возвращаемых типов позволяет вернуть более специализированный тип Wheat.

Разработка с наследованием

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

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

//: polymorphism/Transmogrify.java
// Динамическое изменение поведения объекта
// с помощью композиции (шаблон проектирования «Состояние»)
import static net.mindview.util.Print.*;
 
class Actor {
public void act() {}
}
 
class HappyActor extends Actor {
public void act() { print("HappyActor"); }
}
 
class SadActor extends Actor {
public void act() { print("SadActor"); }
}
 
class Stage {
private Actor actor = new HappyActor();
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); }
}
 
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
}

<spoiler text="Output:">

HappyActor
SadActor

</spoiler>
Объект Stage содержит ссылку на объект Actor, которая инициализируется объектом HappyActor. Это значит, что метод performPlay() имеет определенное поведение. Но так как ссылку на объект можно заново присоединить к другому объекту во время выполнения программы, ссылке actor назначается объект SadActor, и после этого поведение метода performPlay() изменяется.

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

Нисходящее преобразование и динамическое определение типов

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

Но при использовании нисходящего преобразования вы не знаете достоверно, что фигура (например) в действительности является окружностью. С такой же вероятностью она может оказаться треугольником, прямоугольником или другим типом. Должен существовать какой-то механизм, гарантирующий правильность нисходящего преобразования; в противном случае вы можете случайно использовать неверный тип, послав ему сообщение, которое он не в состоянии принять. Это было бы небезопасно.

В некоторых языках (подобных C++) для проведения безопасного нисходящего преобразования типов необходимо провести специальную операцию, но в Java каждое преобразование контролируется! Поэтому, хотя внешне все выглядит как обычное приведение типов в круглых скобках, во время выполнения программы это преобразование проходит проверку на фактическое соответствие типу. Если типы не совпадают, происходит исключение ClassCastException. Процесс проверки типов во время выполнения программы называется динамическим определением типов (run-time type identification, RTTI). Следующий пример демонстрирует действие RTTI:

//: polymorphism/RTTI.java
// Нисходящее преобразование и динамическое определение типов (RTTI).
// {ThrowsException}
 
class Useful {
public void f() {}
public void g() {}
}
 
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
 
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Стадия компиляции- метод не найден в классе Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Нисх. преобразование /RTTI
((MoreUseful)x[0]).u(); //Происходит исключение
}
}

Класс MoreUseful расширяет интерфейс класса Useful. Но благодаря наследованию он также может быть преобразован к типу Useful. Вы видите, как это происходит, при инициализации массива х в методе main(). Так как оба объекта в массиве являются производными от Useful, вы можете послать сообщения (вызвать методы) f() и g() для обоих объектов, но при попытке вызова метода u() (который существует только в классе MoreUseful) вы получите сообщение об ошибке компиляции.

Чтобы получить доступ к расширенному интерфейсу объекта MoreUseful, используйте нисходящее преобразование. Если тип указан правильно, все пройдет успешно; иначе произойдет исключение ClassCastException. Вам не понадобится писать дополнительный код для этого исключения, поскольку оно указывает на общую ошибку, которая может произойти в любом месте программы.

Впрочем, RTTI не сводится к простой проверке преобразований. Например, можно узнать, с каким типом вы имеете дело, прежде чем проводить нисходящее преобразование. Глава 11 полностью посвящена изучению различных аспектов динамического определения типов Java.

Резюме

Полиморфизм означает «многообразие форм». В объектно-ориентированном программировании базовый класс предоставляет общий интерфейс, а различные версии динамически связываемых методов — разные формы использования интерфейса.

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

Чтобы эффективно использовать полиморфизм — а значит, все объектно- ориентированные приемы — в своих программах, необходимо расширить свои представления о программировании, чтобы они охватывали не только члены и сообщения отдельного класса, но и общие аспекты классов, их взаимоотношения. Хотя это потребует значительных усилий, результат стоит того. Наградой станет ускорение разработки программ, улучшение структуры кода, расширяемые программы и сокращение усилий по сопровождению кода.

]]>
Книги по Java https://linexp.ru?id=4745 Wed, 29 Jun 2022 14:28:33 GMT
<![CDATA[Глава 9 Thinking in Java 4th edition]]> ИНТЕРФЕЙСЫИнтерфейсы и абстрактные классы улучшают структуру кода и способствуют отделению интерфейса от реализации. В традиционных языках программирования такие механизмы не получили особого распространения. Например, в C++ существует лишь косвенная поддержка этих концепций. Сам факт их существования в Java показывает, что эти концепции были сочтены достаточно важными для прямой поддержки в языке.

ИНТЕРФЕЙСЫ

Интерфейсы и абстрактные классы улучшают структуру кода и способствуют отделению интерфейса от реализации.
В традиционных языках программирования такие механизмы не получили особого распространения. Например, в C++ существует лишь косвенная поддержка этих концепций. Сам факт их существования в Java показывает, что эти концепции были сочтены достаточно важными для прямой поддержки в языке.

Мы начнем с понятия абстрактного класса, который представляет собой своего рода промежуточную ступень между обычным классом и интерфейсом. Абстрактные классы — важный и необходимый инструмент для создания классов, содержащих нереализованные методы. Применение «чистых» интерфейсов возможно не всегда.


Абстрактные классы и методы

В примере с классами музыкальных инструментов из предыдущей главы методы базового класса Instrument всегда оставались «фиктивными». Попытка вызова такого метода означала, что в программе произошла какая-то ошибка. Это объяснялось тем, что класс Instrument создавался для определения общего интерфейса всех классов, производных от него.

В этих примерах общий интерфейс создавался для единственной цели— его разной реализации в каждом производном типе. Интерфейс определяет базовую форму, общность всех производных классов. Такие классы, как Instrument, также называют абстрактными базовыми классами или просто абстрактными классами

Если в программе определяется абстрактный класс вроде Instrument, создание объектов такого класса практически всегда бессмысленно. Абстрактный класс создается для работы с набором классов через общий интерфейс.

А если Instrument только выражает интерфейс, а создание объектов того класса не имеет смысла, вероятно, пользователю лучше запретить создавать такие объекты. Конечно, можно заставить все методы Instrument выдавать ошибки, но в этом случае получение информации откладывается до стадии выполнения. Ошибки такого рода лучше обнаруживать во время компиляции.

В языке Java для решения подобных задач применяются абстрактные методы. Абстрактный метод незавершен; он состоит только из объявления и не имеет тела. Синтаксис объявления абстрактных методов выглядит так:

 abstract void f();

Класс, содержащий абстрактные методы, называется абстрактным классом. Такие классы тоже должны помечаться ключевым словом abstract (в противном случае компилятор выдает сообщение об ошибке).

Если вы объявляете класс, производный от абстрактного класса, но хотите иметь возможность создания объектов нового типа, вам придется предоставить определения для всех абстрактных методов базового класса. Если этого не сделать, производный класс тоже останется абстрактным, и компилятор заставит пометить новый класс ключевым словом abstract.

Можно создавать класс с ключевым словом abstract даже тогда, когда в нем не имеется ни одного абстрактного метода. Это бывает полезно в ситуациях, где в классе абстрактные методы просто не нужны, но необходимо запретить создание экземпляров этого класса.

Класс Instrument очень легко можно сделать абстрактным. Только некоторые из его методов станут абстрактными, поскольку объявление класса как abstract не подразумевает, что все его методы должны быть абстрактными. Вот что по­лучится:



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

//: interfaces/music4/Music4.java
// Абстрактные классы и методы
package interfaces.music4;
import polymorphism.music.Note;
import static net.mindview.util.Print.*;
 
abstract class Instrument {
private int i; // Память выделяется для каждого объекта
public abstract void play(Note n);
public String what() { return "Instrument"; }
public abstract void adjust();
}
 
class Wind extends Instrument {
public void play(Note n) {
print("Wind.play() " + n);
}
public String what() { return "Wind"; }
public void adjust() {}
}
 
class Percussion extends Instrument {
public void play(Note n) {
print("Percussion.play() " + n);
}
public String what() { return "Percussion"; }
public void adjust() {}
}
 
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
public String what() { return "Stringed"; }
public void adjust() {}
}
 
class Brass extends Wind {
public void play(Note n) {
print("Brass.play() " + n);
}
public void adjust() { print("Brass.adjust()"); }
}
 
class Woodwind extends Wind {
public void play(Note n) {
print("Woodwind.play() " + n);
}
public String what() { return "Woodwind"; }
}
 
public class Music4 {
// Работа метода не зависит от фактического типа объекта.
// поэтому типы, добавленные в систему, будут работать правильно:
static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Восходящее преобразование при добавлении в массив
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}

<spoiler text="Output:">

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

</spoiler>
Как видите, объем изменений минимален.
Создавать абстрактные классы и методы полезно, так как они подчеркивают абстрактность класса, а также сообщают и пользователю класса, и компилятору, как следует с ним обходиться. Кроме того, абстрактные классы играют полезную роль при переработке программ, потому что они позволяют легко перемещать общие методы вверх по иерархии наследования.

Интерфейсы

Ключевое слово interface становится следующим шагом на пути к абстракции. Оно используется для создания полностью абстрактных классов, вообще не имеющих реализации. Создатель интерфейса определяет имена методов, списки аргументов и типы возвращаемых значений, но не тела методов.

Ключевое слово interface фактически означает: «Именно так должны выглядеть все классы, которые реализуют данный интерфейс». Таким образом, любой код, использующий конкретный интерфейс, знает только то, какие методы вызываются для этого интерфейса, но не более того. Интерфейс определяет своего рода «протокол взаимодействия» между классами. Однако интерфейс представляет собой нечто большее, чем абстрактный класс в своем крайнем проявлении, потому что он позволяет реализовать подобие «множественного наследования» C++: иначе говоря, создаваемый класс может быть преобразован к нескольким базовым типам.

Чтобы создать интерфейс, используйте ключевое слово interface вместо class. Как и в случае с классами, вы можете добавить перед словом interface спецификатор доступа public (но только если интерфейс определен в файле, имеющем то же имя) или оставить для него дружественный доступ, если он будет использоваться только в пределах своего пакета. Интерфейс также может содержать поля, но они автоматически являются статическими (static) и неизменными (final).

Для создания класса, реализующего определенный интерфейс (или группу интерфейсов), используется ключевое слово implements. Фактически оно означает: «Интерфейс лишь определяет форму, а сейчас будет показано, как это работает». В остальном происходящее выглядит как обычное наследование. Рассмотрим реализацию на примере иерархии классов Instrument:

Классы Woodwind и Brass свидетельствуют, что реализация интерфейса представляет собой обычный класс, от которого можно создавать производные классы.

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

Все сказанное можно увидеть в следующем примере с объектами Instrument. Заметьте, что каждый метод интерфейса ограничивается простым объявлением; ничего большего компилятор не" разрешит. Вдобавок ни один из методов интер­фейса Instrument не объявлен со спецификатором public, но все методы автоматически являются открытыми:

//: interfaces/music5/Music5.java
// Интерфейсы.
package interfaces.music5;
import polymorphism.music.Note;
import static net.mindview.util.Print.*;
 
interface Instrument {
// Константа времени компиляции:
int VALUE = 5; // static & final
// Определения методов недопустимы:
void play(Note n); // Автоматически объявлен как public
}
 
class Wind implements Instrument {
public void play(Note n) {
print(this + ".play() " + n);
}
public String toString() { return "Wind"; }
public void adjust() { print(this + ".adjust()"); }
}
 
class Percussion implements Instrument {
public void play(Note n) {
print(this + ".play() " + n);
}
public String toString() { return "Percussion"; }
public void adjust() { print(this + ".adjust()"); }
}
 
class Stringed implements Instrument {
public void play(Note n) {
print(this + ".play() " + n);
}
public String toString() { return "Stringed"; }
public void adjust() { print(this + ".adjust()"); }
}
 
class Brass extends Wind {
public String toString() { return "Brass"; }
}
 
class Woodwind extends Wind {
public String toString() { return "Woodwind"; }
}
 
public class Music5 {
// Работа метода не зависит от фактического типа объекта.
// поэтому типы, добавленные в систему, будут работать правильно:
static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Восходящее преобразование при добавлении в массив:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}

<spoiler text="Output:">

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

</spoiler>
В этой версии присутствует еще одно изменение: метод what() был заменен на toString(). Так как метод toString() входит в корневой класс Object, его присутствие в интерфейсе не обязательно.

Остальной код работает так же, как прежде. Неважно, проводите ли вы преобразование к «обычному» классу с именем Instrument, к абстрактному классу с именем Instrument или к интерфейсу с именем Instrument — действие будет одинаковым. В методе tune() ничто не указывает на то, является класс Instrument «обычным» или абстрактным, или это вообще не класс, а интерфейс.

Отделение интерфейса от реализации

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

Представьте, что у нас имеется класс Processor с методами name() и process(). Последний получает входные данные, изменяет их и выдает результат. Базовый класс расширяется для создания разных специализированных типов Processor. В следующем примере производные типы изменяют объекты String (обратите внимание: ковариантными могут быть возвращаемые значения, но не типы аргументов):

//: interfaces/classprocessor/Apply.java
package interfaces.classprocessor;
import java.util.*;
import static net.mindview.util.Print.*;
 
class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) { return input; }
}
 
class Upcase extends Processor {
String process(Object input) { // Ковариантный возвращаемый тип
return ((String)input).toUpperCase();
}
}
 
class Downcase extends Processor {
String process(Object input) {
return ((String)input).toLowerCase();
}
}
 
class Splitter extends Processor {
String process(Object input) {
// Аргумент split() используется для разбиения строки
return Arrays.toString(((String)input).split(" "));
}
}
 
public class Apply {
public static void process(Processor p, Object s) {
print("Using Processor " + p.name());
print(p.process(s));
}
public static String s =
"Disagreement with beliefs is by definition incorrect";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
process(new Splitter(), s);
}
}

<spoiler text="Output:">

Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement with beliefs is by definition incorrect
Using Processor Splitter
[Disagreement, with, beliefs, is, by, definition, incorrect]

</spoiler>
Метод Apply.process() получает любую разновидность Processor, применяет ее к Object, а затем выводит результат. Метод split() является частью класса String. Он получает объект String, разбивает его на несколько фрагментов по ограничи­телям, определяемым переданным аргументом, и возвращает String[ ]. Здесь он используется как более компактный способ создания массива String.
Теперь предположим, что вы обнаружили некое семейство электронных фильтров, которые тоже было бы уместно использовать с методом Apply. process():

//: interfaces/filters/Waveform.java
package interfaces.filters;
public class Waveform {
private static long counter;
private final long id = counter++;
public String toString() { return "Waveform " + id; }
}
 
//: interfaces/filters/Filter.java
package interfaces.filters;
public class Filter {
public String name() {
return getClass().getSimpleName();
}
public Waveform process(Waveform input) { return input; }
}
 
//: interfaces/filters/LowPass.java
package interfaces.filters;
public class LowPass extends Filter {
double cutoff;
public LowPass(double cutoff) { this.cutoff = cutoff; }
public Waveform process(Waveform input) {
return input; //Фиктивная обработка
}
}
 
//: interfaces/filters/HighPass.java
package interfaces.filters;
public class HighPass extends Filter {
double cutoff;
public HighPass(double cutoff) { this.cutoff = cutoff; }
public Waveform process(Waveform input) { return input; }
}
 
//: interfaces/filters/BandPass.java
package interfaces.filters;
public class BandPass extends Filter {
double lowCutoff, highCutoff;
public BandPass(double lowCut, double highCut) {
lowCutoff = lowCut;
highCutoff = highCut;
}
public Waveform process(Waveform input) { return input; }
}

Класс Filter содержит те же интерфейсные элементы, что и Processor, но, поскольку он не является производным от Processor (создатель класса Filter и не подозревал, что вы захотите использовать его как Processor), он не может исполь­зоваться с методом Apply.process(), хотя это выглядело бы вполне естественно.

Логическая привязка между Apply.process() и Processor оказывается более сильной, чем реально необходимо, и это обстоятельство препятствует повторному ис­пользованию кода Apply.process(). Также обратите внимание, что входные и выходные данные относятся к типу Waveform.
Но, если преобразовать класс Processor в интерфейс, ограничения ослабляются и появляется возможность повторного использования Apply.process(). Обновленные версии Processor и Apply выглядят так:

//: interfaces/interfaceprocessor/Processor.java
package interfaces.interfaceprocessor;
public interface Processor {
String name();
Object process(Object input);
}
 
//: interfaces/interfaceprocessor/Apply.java
package interfaces.interfaceprocessor;
import static net.mindview.util.Print.*;
public class Apply {
public static void process(Processor p, Object s) {
print("Using Processor " + p.name());
print(p.process(s));
}
}

В первом варианте повторного использования кода клиентские программисты пишут свои классы с поддержкой интерфейса:

//: interfaces/interfaceprocessor/StringProcessor.java
package interfaces.interfaceprocessor;
import java.util.*;
 
public abstract class StringProcessor implements Processor{
public String name() {
return getClass().getSimpleName();
}
public abstract String process(Object input);
public static String s =
"If she weighs the same as a duck, she's made of wood";
public static void main(String[] args) {
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
Apply.process(new Splitter(), s);
}
}
 
class Upcase extends StringProcessor {
public String process(Object input) { // Covariant return
return ((String)input).toUpperCase();
}
}
 
class Downcase extends StringProcessor {
public String process(Object input) {
return ((String)input).toLowerCase();
}
}
 
class Splitter extends StringProcessor {
public String process(Object input) {
return Arrays.toString(((String)input).split(" "));
}
}

<spoiler text="Output:">

Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]

</spoiler>
Впрочем, довольно часто модификация тех классов, которые вы собираетесь использовать, невозможна. Например, в примере с электронными фильтрами библиотека была получена из внешнего источника. В таких ситуациях применяется паттерн «адаптер»: вы пишете код, который получает имеющийся интерфейс, и создаете тот интерфейс, который вам нужен:

//: interfaces/interfaceprocessor/FilterProcessor.java
package interfaces.interfaceprocessor;
import interfaces.filters.*;
 
class FilterAdapter implements Processor {
Filter filter;
public FilterAdapter(Filter filter) {
this.filter = filter;
}
public String name() { return filter.name(); }
public Waveform process(Object input) {
return filter.process((Waveform)input);
}
}
 
public class FilterProcessor {
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)), w);
Apply.process(new FilterAdapter(new HighPass(2.0)), w);
Apply.process(
new FilterAdapter(new BandPass(3.0, 4.0)), w);
}
}

<spoiler text="Output:">

Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0

</spoiler>
Конструктор FilterAdapter получает исходный интерфейс (Filter) и создает объект с требуемым интерфейсом Processor. Также обратите внимание на применение делегирования в классе FilterAdapter.
Отделение интерфейса от реализации позволяет применять интерфейс к разным реализациям, а следовательно, расширяет возможности повторного использования кода.

«Множественное наследование» в Java

Так как интерфейс по определению не имеет реализации (то есть не обладает памятью для хранения данных), нет ничего, что могло бы помешать совмещению нескольких интерфейсов. Это очень полезная возможность, так как в некоторых ситуациях требуется выразить утверждение: «Икс является и А, и Б, и В одновременно». В C++ подобное совмещение интерфейсов нескольких классов называется множественным наследованием, и оно имеет ряд очень неприятных аспектов, поскольку каждый класс может иметь свою реализацию. В Java можно добиться аналогичного эффекта, но, поскольку реализацией обладает всего один класс, проблемы, возникающие при совмещении нескольких интерфейсов в C++, в Java принципиально невозможны:

При наследовании базовый класс вовсе не обязан быть абстрактным или «реальным» (без абстрактных методов). Если наследование действительно осуществляется не от интерфейса, то среди прямых «предков» класс может быть только один — все остальные должны быть интерфейсами. Имена интерфейсов перечисляются вслед за ключевым словом implements и разделяются запятыми. Интерфейсов может быть сколько угодно, причем к ним можно проводить восходящее преобразование. Следующий пример показывает, как создать новый класс на основе реального класса и нескольких интерфейсов:

//: interfaces/Adventure.java
// Использование нескольких интерфейсов.
 
interface CanFight {
void fight();
}
 
interface CanSwim {
void swim();
}
 
interface CanFly {
void fly();
}
 
class ActionCharacter {
public void fight() {}
}
 
class Hero extends ActionCharacter
implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
}
 
public class Adventure {
public static void t(CanFight x) { x.fight(); }
public static void u(CanSwim x) { x.swim(); }
public static void v(CanFly x) { x.fly(); }
public static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero h = new Hero();
t(h); // Используем объект в качестве типа CanFight
u(h); // Используем объект в качестве типа CanSwim
v(h); // Используем объект в качестве типа CanFly
w(h); // Используем объект в качестве ActionCharacter
 
}
}

Мы видим, что класс Неrо сочетает реальный класс ActionCharacter с интерфейсами CanFight, CanSwim и CanFly. При объединении реального класса с интерфейсами на первом месте должен стоять реальный класс, а за ним следуют ин­терфейсы (иначе компилятор выдаст ошибку).

Заметьте, что объявление метода fight() в интерфейсе CanFight совпадает с тем, что имеется в классе ActionCharacter, и поэтому в классе Неrо нет определения метода fight().

Интерфейсы можно расширять, но при этом получается другой интерфейс. Необходимым условием для создания объектов нового типа является наличие всех определений. Хотя класс Неrо не имеет явного определения метода fight(), это определение существует в классе ActionCharacter, что и делает возможным создание объектов класса Неrо.

Класс Adventure содержит четыре метода, которые принимают в качестве аргументов разнообразные интерфейсы и реальный класс. Созданный объект Неrо передается всем этим методам, а это значит, что выполняется восходящее пре­образование объекта к каждому интерфейсу по очереди. Система интерфейсов Java спроектирована так, что она нормально работает без особых усилий со стороны программиста.

Помните, что главная причина введения в язык интерфейсов представлена в приведенном примере: это возможность выполнять восходящее преобразование к нескольким базовым типам. Вторая причина для использования интерфейсов совпадает с предназначением абстрактных классов: запретить програм- мисту-клиенту создание объектов этого класса.

Возникает естественный вопрос: что лучше — интерфейс или абстрактный класс? Если можно создать базовый класс без определений методов и переменных-членов, выбирайте именно интерфейс, а не абстрактный класс. Вообще говоря, если известно, что нечто будет использоваться как базовый класс, первым делом постарайтесь сделать это «нечто» интерфейсом.

Расширение интерфейса через наследование

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

//: interfaces/HorrorShow.java
// Расширение интерфейса с помощью наследования
 
interface Monster {
void menace();
}
 
interface DangerousMonster extends Monster {
void destroy();
}
 
interface Lethal {
void kill();
}
 
class DragonZilla implements DangerousMonster {
public void menace() {}
public void destroy() {}
}
 
interface Vampire extends DangerousMonster, Lethal {
void drinkBlood();
}
 
class VeryBadVampire implements Vampire {
public void menace() {}
public void destroy() {}
public void kill() {}
public void drinkBlood() {}
}
 
public class HorrorShow {
static void u(Monster b) { b.menace(); }
static void v(DangerousMonster d) {
d.menace();
d.destroy();
}
static void w(Lethal l) { l.kill(); }
public static void main(String[] args) {
DangerousMonster barney = new DragonZilla();
u(barney);
v(barney);
Vampire vlad = new VeryBadVampire();
u(vlad);
v(vlad);
w(vlad);
}
}

DangerousMonster представляет собой простое расширение Monster, в результате которого образуется новый интерфейс. Он реализуется классом DragonZilla.

Синтаксис, использованный в интерфейсе Vampire, работает только при наследовании интерфейсов. Обычно ключевое слово extends может использоваться всего с одним классом, но, так как интерфейс можно составить из нескольких других интерфейсов, extends подходит для написания нескольких имен интерфейсов при создании нового интерфейса. Как нетрудно заметить, имена нескольких интерфейсов разделяются при этом запятыми.

Конфликты имен при совмещении интерфейсов

При реализации нескольких интерфейсов может возникнуть небольшая проблема. В только что рассмотренном примере интерфейс CanFight и класс ActionCharacter имеют идентичные методы void fight(). Хорошо, если методы полностью тождественны, но что, если они различаются по сигнатуре или типу возвращаемого значения? Рассмотрим такой пример:

//: interfaces/InterfaceCollision.java
package interfaces;
 
interface I1 { void f(); }
interface I2 { int f(int i); }
interface I3 { int f(); }
class C { public int f() { return 1; } }
 
class C2 implements I1, I2 {
public void f() {}
public int f(int i) { return 1; } // перегружен
}
 
class C3 extends C implements I2 {
public int f(int i) { return 1; } // перегружен
}
 
class C4 extends C implements I3 {
// Идентичны, все нормально:
public int f() { return 1; }
}
 
// Методы различаются только по типу возвращаемого значения:
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {}

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

InterfaceCollіsion.java.23 f() в C не может реализовать f() в I1; попытка использовать несовместимые возвращаемые типы обнаружено: int требуется- void
InterfaceCollіsi on java:24- интерфейсы І1 и I1 несовместимы; оба определяют f(), но с различными возвращаемыми типами

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

Интерфейсы как средство адаптации

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

Соответственно, интерфейсы часто применяются в архитектурном паттерне «Стратегия». Вы пишете метод, выполняющий несколько операций; при вызове метод получает интерфейс, который тоже указываете вы. Фактически вы говорите: «Мой метод может использоваться с любым объектом, удовлетворяющим моему интерфейсу». Метод становится более гибким и универсальным.

Например, конструктор класса Java SE5 Scanner получает интерфейс Readable. Анализ показывает, что Readable не является аргументом любого другого метода из стандартной библиотеки Java — этот интерфейс создавался исключительно для Scanner, чтобы его аргументы не ограничивались определенным классом. При таком подходе можно заставить Scanner работать с другими типами. Если вы хотите создать новый класс, который может использоваться со Scanner, реализуйте в нем интерфейс Readable:

//: interfaces/RandomWords.java
// Реализация интерфейса для выполнения требований метода
import java.nio.*;
import java.util.*;
 
public class RandomWords implements Readable {
private static Random rand = new Random(47);
private static final char[] capitals =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
private static final char[] lowers =
"abcdefghijklmnopqrstuvwxyz".toCharArray();
private static final char[] vowels =
"aeiou".toCharArray();
private int count;
public RandomWords(int count) { this.count = count; }
public int read(CharBuffer cb) {
if(count-- == 0)
return -1; // Признак конца входных данных
cb.append(capitals[rand.nextInt(capitals.length)]);
for(int i = 0; i < 4; i++) {
cb.append(vowels[rand.nextInt(vowels.length)]);
cb.append(lowers[rand.nextInt(lowers.length)]);
}
cb.append(" ");
return 10; // Количество присоединенных символов
}
public static void main(String[] args) {
Scanner s = new Scanner(new RandomWords(10));
while(s.hasNext())
System.out.println(s.next());
}
}

<spoiler text="Output:">

Yazeruyac
Fowenucor
Goeazimom
Raeuuacio
Nuoadesiw
Hageaikux
Ruqicibui
Numasetih
Kuuuuozog
Waqizeyoy

</spoiler>
Интерфейс Readable требует только присутствия метода read(). Метод read() либо добавляет данные в аргумент CharBuffer (это можно сделать несколькими способами; обращайтесь к документации CharBuffer), либо возвращает -1 при отсутствии входных данных.

Допустим, у нас имеется класс, не реализующий интерфейс Readable, — как заставить его работать с Scanner? Перед вами пример класса, генерирующего вещественные числа:

//: interfaces/RandomDoubles.java
import java.util.*;
 
public class RandomDoubles {
private static Random rand = new Random(47);
public double next() { return rand.nextDouble(); }
public static void main(String[] args) {
RandomDoubles rd = new RandomDoubles();
for(int i = 0; i < 7; i ++)
System.out.print(rd.next() + " ");
}
}

<spoiler text="Output:">

0.7271157860730044 0.5309454508634242
0.16020656493302599 0.18847866977771732 0.5166020801268457
0.2678662084200585 0.2613610344283964

</spoiler>
Мы снова можем воспользоваться схемой адаптера, но на этот раз адаптируемый класс создается наследованием и реализацией интерфейса Readable. Псевдомножественное наследование, обеспечиваемое ключевым словом interface, позволяет создать новый класс, который одновременно является и RandomDoubles, и Readable:

//: interfaces/AdaptedRandomDoubles.java
// Создание адаптера посредством наследования
import java.nio.*;
import java.util.*;
 
public class AdaptedRandomDoubles extends RandomDoubles
implements Readable {
private int count;
public AdaptedRandomDoubles(int count) {
this.count = count;
}
public int read(CharBuffer cb) {
if(count-- == 0)
return -1;
String result = Double.toString(next()) + " ";
cb.append(result);
return result.length();
}
public static void main(String[] args) {
Scanner s = new Scanner(new AdaptedRandomDoubles(7));
while(s.hasNextDouble())
System.out.print(s.nextDouble() + " ");
}
}

<spoiler text="Output:">

0.7271157860730044 0.5309454508634242 
0.16020656493302599 0.18847866977771732 0.5166020801268457
0.2678662084200585 0.2613610344283964

</spoiler>
Так как интерфейсы можно добавлять подобным образом только к существующим классам, это означает, что любой класс может быть адаптирован для метода, получающего интерфейс. В этом заключается преимущество интерфейсов перед классами.

Поля в интерфейсах

Объявление полей интерфейсов

Так как все поля, помещаемые в интерфейсе, автоматически являются статическими (static) и неизменными (final), объявление interface хорошо подходит для создания групп постоянных значений. До выхода Java SE5 только так можно было имитировать перечисляемый тип enum из языков C и C++:

//: interfaces/Months.java
// Использование интерфейсов для создания групп констант
package interfaces;
 
public interface Months {
int
JANUARY = 1, FEBRUARY = 2, MARCH = 3,
APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
NOVEMBER = 11, DECEMBER = 12;
}

Отметьте стиль Java — использование только заглавных букв (с разделением слов подчеркиванием) для полей со спецификаторами static и final, которым присваиваются фиксированные значения на месте описания. Поля интерфейса автоматически являются открытыми (public), поэтому нет необходимости явно указывать это.

В Java SE5 появилось гораздо более мощное и удобное ключевое слово enum, поэтому надобность в применении интерфейсов для определения констант отпала. Впрочем, эта старая идиома еще может встречаться в некоторых старых программах.

Инициализация полей интерфейсов

Поля, определяемые в интерфейсах, не могут быть «пустыми константами», но могут инициализироваться неконстантными выражениями. Например:

//: interfaces/RandVals.java
// Инициализация полей интерфейсов
// не-константными выражениями
import java.util.*;
 
public interface RandVals {
Random RAND = new Random(47);
int RANDOM_INT = RAND.nextInt(10);
long RANDOM_LONG = RAND.nextLong() * 10;
float RANDOM_FLOAT = RAND.nextLong() * 10;
double RANDOM_DOUBLE = RAND.nextDouble() * 10;
}

Так как поля являются статическими, они инициализируются при первой загрузке класса, которая происходит при первом обращении к любому из полей интерфейса. Простой тест:

//: interfaces/TestRandVals.java
import static net.mindview.util.Print.*;
 
public class TestRandVals {
public static void main(String[] args) {
print(RandVals.RANDOM_INT);
print(RandVals.RANDOM_LONG);
print(RandVals.RANDOM_FLOAT);
print(RandVals.RANDOM_DOUBLE);
}
}

<spoiler text="Output:">

8
-32032247016559954
-8.5939291E18
5.779976127815049

</spoiler>
Конечно, поля не являются частью интерфейса. Данные хранятся в статической области памяти, отведенной для данного интерфейса.

Вложенные интерфейсы

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

//: interfaces/nesting/NestingInterfaces.java
package interfaces.nesting;
 
class A {
interface B {
void f();
}
public class BImp implements B {
public void f() {}
}
private class BImp2 implements B {
public void f() {}
}
public interface C {
void f();
}
class CImp implements C {
public void f() {}
}
private class CImp2 implements C {
public void f() {}
}
private interface D {
void f();
}
private class DImp implements D {
public void f() {}
}
public class DImp2 implements D {
public void f() {}
}
public D getD() { return new DImp2(); }
private D dRef;
public void receiveD(D d) {
dRef = d;
dRef.f();
}
}
 
interface E {
interface G {
void f();
}
// Избыточное объявление public:
public interface H {
void f();
}
void g();
// He может быть private внутри интерфейса:
//! private interface I {}
}
 
public class NestingInterfaces {
public class BImp implements A.B {
public void f() {}
}
class CImp implements A.C {
public void f() {}
}
// Private-интерфейс не может быть реализован нигде,
// кроме как внутри класса, где он был определен:
//! class DImp implements A.D {
//! public void f() {}
//! }
class EImp implements E {
public void g() {}
}
class EGImp implements E.G {
public void f() {}
}
class EImp2 implements E {
public void g() {}
class EG implements E.G {
public void f() {}
}
}
public static void main(String[] args) {
A a = new A();
// Нет доступа к A.D:
//! A.D ad = a.getD();
// He возвращает ничего, кроме A.D:
//! A.DImp2 di2 = a.getD();
// Член интерфейса недоступен:
//! a.getD().f();
// Только другой объект класса А может использовать getD():
A a2 = new A();
a2.receiveD(a.getD());
}
}

Синтаксис вложения интерфейса в класс достаточно очевиден. Вложенные интерфейсы, как и обычные, могут иметь «пакетную» или открытую (public) видимость.

Любопытная подробность: интерфейсы могут быть объявлены закрытыми (private), как видно на примере A.D (используется тот же синтаксис описания, что и для вложенных классов). Для чего нужен закрытый вложенный интерфейс?

Может показаться, что такой интерфейс реализуется только в виде закрытого (private) вложенного класса, подобного DImp, но A.DImp2 показывает, что он также может иметь форму открытого (public) класса. Тем не менее класс A.DImp2 «замкнут» сам на себя. Факт реализации private-интерфейса не может упоминаться в программе, поэтому реализация такого интерфейса — просто способ принудительного определения методов этого интерфейса без добавления информации о дополнительном типе (то есть восходящее преобразование становится невозможным).

Метод getD() усугубляет сложности, связанные с private-интерфейсом, — это открытый (public) метод, возвращающий ссылку на закрытый (private) интерфейс. Что можно сделать с возвращаемым значением этого метода? В методе main() мы видим несколько попыток использовать это возвращаемое значение, и все они оказались неудачными. Заставить метод работать можно только одним способом — передать возвращаемое значение некоторому объекту, которому разрешено его использование (в нашем случае это еще один объект А, у которого имеется необходимый метод receiveD()).

Интерфейс Е показывает, что интерфейсы могут быть вложены друг в друга. Впрочем, правила для интерфейсов — в особенности то, что все элементы интерфейса должны быть открытыми (public), — здесь строго соблюдаются, поэтому интерфейс, вложенный внутрь другого интерфейса, автоматически объявляется открытым и его нельзя сделать закрытым (private).

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

Интерфейсы и фабрики

Предполагается, что интерфейс предоставляет своего рода «шлюз» к нескольким альтернативным реализациям. Типичным способом получения объектов, соответствующих интерфейсу, является паттерн «фабрика». Вместо того, чтобы вызывать конструктор напрямую, вы вызываете метод объекта-фабрики, который предоставляет реализацию интерфейса — в этом случае программный код теоретически отделяется от реализации интерфейса, благодаря чему становится возможной совершенно прозрачная замена реализации. Следующий пример демонстрирует типичную структуру фабрики:

//: interfaces/Factories.java
import static net.mindview.util.Print.*;
 
interface Service {
void method1();
void method2();
}
 
interface ServiceFactory {
Service getService();
}
 
class Implementation1 implements Service {
Implementation1() {} // Доступ в пределах пакета
public void method1() {print("Implementation1 method1");}
public void method2() {print("Implementation1 method2");}
}
 
class Implementation1Factory implements ServiceFactory {
public Service getService() {
return new Implementation1();
}
}
 
class Implementation2 implements Service {
Implementation2() {} // Доступ в пределах пакета
public void method1() {print("Implementation2 method1");}
public void method2() {print("Implementation2 method2");}
}
 
class Implementation2Factory implements ServiceFactory {
public Service getService() {
return new Implementation2();
}
}
 
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(new Implementation1Factory());
// Реализации полностью взаимозаменяемы:
serviceConsumer(new Implementation2Factory());
}
}

<spoiler text="Output:">

Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2

</spoiler>
Без применения фабрики вам пришлось бы где-то указать точный тип создаваемого объекта Service, чтобы он мог вызвать подходящий конструктор.

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

//: interfaces/Games.java
// Игровая библиотека с использованием фабрики
import static net.mindview.util.Print.*;
 
interface Game { boolean move(); }
interface GameFactory { Game getGame(); }
 
class Checkers implements Game {
private int moves = 0;
private static final int MOVES = 3;
public boolean move() {
print("Checkers move " + moves);
return ++moves != MOVES;
}
}
 
class CheckersFactory implements GameFactory {
public Game getGame() { return new Checkers(); }
}
 
class Chess implements Game {
private int moves = 0;
private static final int MOVES = 4;
public boolean move() {
print("Chess move " + moves);
return ++moves != MOVES;
}
}
 
class ChessFactory implements GameFactory {
public Game getGame() { return new Chess(); }
}
 
public class Games {
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while(s.move())
;
}
public static void main(String[] args) {
playGame(new CheckersFactory());
playGame(new ChessFactory());
}
}

<spoiler text="Output:">

Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
Chess move 3

</spoiler>
Если класс Games представляет сложный блок кода, такое решение позволит повторно использовать его для разных типов игр.
В следующей главе будет представлен более элегантный способ реализации фабрик на базе анонимных внутренних классов.

Резюме

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

Многие программисты поддались этому искушению. Тем не менее любая абстракция должна быть мотивирована реальной потребностью. Основное назначение интерфейсов — возможность переработки реализации в случае необходи­мости, а не введение лишнего уровня абстракции вместе с дополнительными сложностями. Дополнительные сложности могут оказаться довольно существенными. А представьте, что кто-то будет вынужден разбираться в вашем коде и в конечном итоге поймет, что интерфейсы были добавлены «на всякий случай», без веских причин — в таких ситуациях все проектирование, которое выполнялось данным разработчиком, начинает выглядеть довольно сомнительно.

В общем случае рекомендуется отдавать предпочтение классам перед интерфейсами. Начните с классов, а если необходимость интерфейсов станет очевидной — переработайте архитектуру своего проекта. Интерфейсы — замечательный инструмент, но ими нередко злоупотребляют.

]]>
Книги по Java https://linexp.ru?id=4744 Wed, 29 Jun 2022 14:28:00 GMT
<![CDATA[Глава 10 Thinking in Java 4th edition]]> ВНУТРЕННИЕ КЛАССЫОпределение класса может размещаться внутри определения другого класса. Такие классы называются внутренними (inner class). Внутренние классы весьма полезны, так как они позволяют группировать классы, логически принадлежащие друг другу, и управлять доступом к ним.

ВНУТРЕННИЕ КЛАССЫ

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

Создание внутренних классов

Внутренние классы создаются в точности так, как и следовало ожидать, — определение класса размещается внутри окружающего класса:

//: innerclasses/Parcel1.java
// Creating inner classes.
public class Parcel1 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
// Использование внутренних классов имеет много общего
// с использованием любых других классов в пределах Parcel1
public void ship(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship("Tasmania");
}
}

<spoiler text="Output:">

Tasmania

</spoiler>

The inner classes used inside ship( ) look just like ordinary classes. Here, the only practical
difference is that the names are nested within Parceli. You’ll see in a while that this isn’t the
only difference. More typically, an outer class will have a method that returns a reference to
an inner class, as you can see in the to( ) and contents( ) methods:

//: innerclasses/Parcel2.java 
// Returning a reference to an inner class.
 
public class Parcel2 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
public Destination to(String s) {
return new Destination(s);
}
public Contents contents() {
return new Contents();
}
public void ship(String dest) {
Contents c = contents();
Destination d = to(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p.ship("Tasmania");
Parcel2 q = new Parcel2();
// Defining references to inner classes:
Parcel2.Contents c = q.contents();
Parcel2.Destination d = q.to("Borneo");
}
}

<spoiler text="Output:">

Tasmania

</spoiler>

Если вам понадобится создать объект внутреннего класса где-либо, кроме как в не-статическом методе внешнего класса, тип этого объекта должен задаваться в формате

ИмяВнешнегоКласса.ИмяВнутреннегоКласса

что и делается в методе main().

Связь с внешним классом

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

//: innerclasses/Sequence.java
// Хранение последовательности объектов
interface Selector {
boolean end();
Object current();
void next();
}
 
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) { items = new Object[size]; }
public void add(Object x) {
if(next < items.length)
items[next++] = x;
}
private class SequenceSelector implements Selector {
private int i = 0;
public boolean end() { return i == items.length; }
public Object current() { return items[i]; }
public void next() { if(i < items.length) i++; }
}
public Selector selector() {
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(10);
for(int i = 0; i < 10; i++)
sequence.add(Integer.toString(i));
Selector selector = sequence.selector();
while(!selector.end()) {
System.out.print(selector.current() + " ");
selector.next();
}
}
}

<spoiler text="Output:">

0 1 2 3 4 5 6 7 8 9

</spoiler>

Класс Sequence — не более чем «оболочка» для массива с элементами Object, имеющего фиксированный размер. Для добавления новых объектов в конец последовательности (при наличии свободного места) используется метод add(). Для выборки каждого объекта в последовательности Sequence предусмотрен интерфейс с именем Selector. Он позволяет узнать, достигнут ли конец последовательности (метод end()), обратиться к текущему объекту (метод current()) и перейти к следующему объекту последовательности (метод next()). Так как Selector является интерфейсом, другие классы вправе реализовать его по-своему, а также другие методы могут использовать интерфейс как аргумент - всё это повышает универсальность кода.

Здесь SequenceSelector является закрытым (private) классом, предоставляющим функциональность интерфейса Selector. В методе main() вы можете наблюдать за процессом создания последовательности Sequence с последующим заполнением ее объектами String. Затем вызывается метод selector() для получения интерфейса Selector, который используется для перемещения по последовательности и выбора ее элементов.

На первый взгляд создание SequenceSelector напоминает создание обычного внутреннего класса. Но присмотритесь к нему повнимательнее. Заметьте, что в каждом из методов end(), current() и next() присутствует ссылка на items, а это не одно из полей класса SequenceSelector, а закрытое (private) поле объемлющего класса. Внутренний класс может обращаться ко всем полям и методам внешнего класса-оболочки, как будто они описаны в нем самом. Это весьма удобно, и вы могли в этом убедиться, изучая рассмотренный пример.

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

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

Конструкции .this и .new

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

//: innerclasses/DotThis.java
// Обращение к объекту внешнего класса.
public class DotThis {
void f() { System.out.println("DotThis.f()"); }
public class Inner {
public DotThis outer() {
return DotThis.this;
// A plain "this" would be Inner's "this"
}
}
public Inner inner() { return new Inner(); }
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
dti.outer().f();
}
}

<spoiler text="Output:">

DotThis.f()

</spoiler>

Иногда бывает нужно приказать другому объекту создать объект одного из его внутренних классов. Для этого перед .new указывается ссылка на другой объект внешнего класса:

//: innerclasses/DotNew.java
// Непосредственное создание внутреннего класса в синтаксисе .new
public class DotNew {
public class Inner {}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}

При создании объекта внутреннего класса указывается не имя внешнего класса DotNew, как можно было бы ожидать, а имя объекта внешнего класса. Это также решает проблему видимости имен для внутреннего класса, поэтому мы не используем (а вернее, не можем использовать) запись вида dn.new DotNew.Inner().
Невозможно создать объект внутреннего класса, не имея ссылки на внешний класс. Но если создать вложенный класс (статический внутренний класс), ссылка на объект внешнего класса не нужна.
Рассмотрим пример использования .new в примере Parcel:

//: innerclasses/Parcel3.java
// Использование new для создания экземпляров внутренних классов
public class Parcel3 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) { label = whereTo; }
String readLabel() { return label; }
}
public static void main(String[] args) {
Parcel3 p = new Parcel3();
// Для создания экземпляра внутреннего класса
// необходимо использовать экземпляр внешнего класса
Parcel3.Contents c = p.new Contents();
Parcel3.Destination d = p.new Destination("Tasmania");
}
}

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

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

//: innerclasses/Destination.java
public interface Destination {
String readLabel();
}
 
//: innerclasses/Contents.java
public interface Contents {
int value();
}

Теперь интерфейсы Contents и Destination доступны программисту-клиенту. (Помните, что в объявлении interface все члены класса автоматически являются открытыми (public).)
При получении из метода ссылки на базовый класс или интерфейс возможны ситуации, в которых вам не удастся определить ее точный тип, как здесь:

//: innerclasses/TestParcel.java
 
class Parcel4 {
private class PContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
}
 
public class TestParcel {
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
// Запрещено - нет доступа к private-классу:
//! Parcel4.PContents pc = p.new PContents();
}
}

В класс Parcel4 было добавлено кое-что новое: внутренний класс PContents является закрытым (private), поэтому он недоступен для всех, кроме внешнего класса Раrсе14.

Класс PDestination объявлен как protected, следовательно, доступ к нему имеют только класс Parcel4, классы из одного пакета с Раrсеl4 (так как спецификатор protected также дает доступ в пределах пакета) и наследники класса Раrсеl4.

Таким образом, программист-клиент обладает ограниченной информацией и доступом к этим членам класса. Более того, нельзя даже выполнить нисходящее преобразование к закрытому (private) внутреннему классу (или protected, кроме наследников), поскольку его имя недоступно, как показано в классе Test.

Таким образом, закрытый внутренний класс позволяет разработчику класса полностью запретить использование определенных типов и скрыть все детали реализации класса. Вдобавок, расширение интерфейса с точки зрения программиста-клиента не будет иметь смысла, поскольку он не сможет получить доступ к дополнительным методам, не принадлежащим к открытой части класса. Наконец, у компилятора Java появится возможность оптимизировать код.

Внутренние классы в методах и областях действия

Ранее мы рассмотрели ряд типичных применений внутренних классов. В основном ваш код будет содержать «простые» внутренние классы, смысл которых понять нетрудно. Однако синтаксис внутренних классов скрывает множество других, не столь тривиальных способов их использования: внутренние классы можно создавать внутри метода или даже в пределах произвольного блока. На то есть две причины:


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

В следующих примерах рассмотренная недавно программа будет изменена, благодаря чему у нас появятся:


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

Первый пример демонстрирует создание целого класса в контексте метода (вместо создания в контексте другого класса). Такие внутренние классы называются локальными:

//: innerclasses/Parcel5.java
// Вложение класса в тело метода.
public class Parcel5 {
public Destination destination(String s) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
Destination d = p.destination("Tasmania");
}
}

Теперь класс PDestination является частью метода destination(), а не частью класса Раrсеl5. Поэтому доступ к классу PDestination возможен только из метода destination().

Обратите внимание на восходящее преобразование, производимое в команде return, — из метода возвращается лишь ссылка на базовый класс Destination, и ничего больше. Конечно, тот факт, что имя класса PDestination находится внутри метода destination(), не означает, что объект PDestination после выхода из этого метода станет недоступным.

Идентификатор PDestination может использоваться для внутренних классов каждого отдельного класса в одном подкаталоге, без порождения конфликта имен.
Следующий пример демонстрирует, как можно вложить внутренний класс в произвольную область действия:

//: innerclasses/Parcel6.java
// Вложение класса в область действия
 
public class Parcel6 {
private void internalTracking(boolean b) {
if(b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
}
TrackingSlip ts = new TrackingSlip("ожидание");
String s = ts.getSlip();
}
// Здесь использовать класс нельзя!
// Вне области видимости
//! TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
}
}

Класс TrackingSlip вложен в область действия команды if. Это не значит, что класс создается в зависимости от условия — он компилируется вместе со всем остальным кодом. Однако при этом он недоступен вне контекста, в котором был определен. В остальном он выглядит точно так же, как и обычный класс.

Безымянные внутренние классы

Следующий пример выглядит немного странно:

//: innerclasses/Parcel7.java
// Метод возвращает экземпляр безымянного внутреннего класса
public class Parcel7 {
public Contents contents() {
return new Contents() {// Вставить определение класса
private int i = 11;
public int value() { return i; }
}; // В данной ситуации точка с запятой необходима
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}

Метод contents() совмещает создание возвращаемого значения с определением класса, который это возвращаемое значение и представляет! Вдобавок, этот класс является безымянным — у него отсутствует имя. Ситуация запутывается еще тем, что поначалу мы будто бы приступаем к созданию объекта Contents, а потом, остановившись перед точкой с запятой, говорим: «Стоп, а сюда я подкину определение класса».

Такая необычная форма записи значит буквально следующее: «Создать объект безымянного класса, который унаследован от Contents». Ссылка, которая возвращается при этом из выражения new, автоматически повышается до базового типа Contents. Синтаксис записи безымянного внутреннего класса является укороченной формой записи такой конструкции:

//: innerclasses/Parcel7b.java
// Расширенная версия Parcel7.java
public class Parcel7b {
class MyContents implements Contents {
private int i = 11;
public int value() { return i; }
}
public Contents contents() { return new MyContents(); }
public static void main(String[] args) {
Parcel7b p = new Parcel7b();
Contents c = p.contents();
}
}

В безымянном внутреннем классе базовый класс Contents создается с использованием конструктора по умолчанию. Следующая программа показывает, как следует поступать, если базовый класс требует вызова конструктора с аргументами:

//: innerclasses/Parcel8.java
// Вызов конструктора базового класса.
public class Parcel8 {
public Wrapping wrapping(int x) {
// Вызов конструктора базового класса:
return new Wrapping(x) { // // аргумент конструктора.
public int value() {
return super.value() * 47;
}
}; // // Требуется точка с запятой
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Wrapping w = p.wrapping(10);
}
}

Требуемый аргумент просто передается в конструктор базового класса, как в рассмотренном примере х в выражении new Wrapping(x). Хотя это обычный класс с реализацией, Wrapping также используется в качестве общего «интерфейса» для своих производных классов:

//: innerclasses/Wrapping.java
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
}

Класс Wrapping имеет конструктор с аргументом — просто для того, чтобы ситуация стала чуть более интересной.
Точка с запятой в конце безымянного внутреннего класса поставлена вовсе не для того, чтобы обозначить конец тела класса (как делается в C++). Вместо этого она указывает на конец выражения, в котором содержится внутренний класс. Таким образом, в данном случае ее использование ничем не отличается от обычного.
Инициализацию также можно провести в точке определения полей безымянного класса:

//: innerclasses/Parcel9.java
// Безымянный внутренний класс, выполняющий инициализацию.
// Более короткая версия программы Parcel5.java
 
public class Parcel9 {
// Для использования в безымянном внутреннем классе
// аргументы должны быть неизменны (final)
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.destination("Tasmania"&##41;;
}
}

Если вы определяете безымянный внутренний класс и хотите при этом использовать объекты, определенные вне этого внутреннего класса, компилятор требует, чтобы переданные на них ссылки объявлялись неизменными (final), как это сделано аргументе destination(). Без такого объявления вы получите сообщение об ошибке при компиляции программы.

Пока мы ограничиваемся простым присваиванием значений полям, указанный подход работает. А если понадобится выполнить некоторые действия, свойственные конструкторам? В безымянном классе именованный конструктор определить нельзя (раз у самого класса нет имени!), но инициализация экземпляра (instance initialization) фактически позволяет добиться желаемого эффекта:

//: innerclasses/AnonymousConstructor.java
// Создание конструктора для безымянного внутреннего класса.
import static net.mindview.util.Print.*;
 
abstract class Base {
public Base(int i) {
print("Base constructor, i = " + i);
}
public abstract void f();
}
 
public class AnonymousConstructor {
public static Base getBase(int i) {
return new Base(i) {
{ print("Inside instance initializer"); }
public void f() {
print("In anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
}

<spoiler text="Output:">

Base constructor, i = 47
Inside instance initializer
In anonymous f()

</spoiler>
В таком случае переменная і не обязана быть неизменной (final). И хотя і передается базовому конструктору безымянного класса, она никогда не используется напрямую внутри безымянного класса.
Вернемся к нашим объектам Parcel, на этот раз выполнив для них инициализацию экземпляра. Отметьте, что параметры метода destination() должны быть объявлены неизменными, так как они используются внутри безымянного класса:

//: innerclasses/Parcel10.java
// Демонстрация "инициализации экземпляра" для
// конструирования безымянного внутреннего класса
public class Parcel10 {
public Destination
destination(final String dest, final float price) {
return new Destination() {
private int cost;
// Инициализация экземпляра для каждого объекта:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel10 p = new Parcel10();
Destination d = p.destination("Tasmania", 101.395F);
}
}

<spoiler text="Output:">

Over budget!

</spoiler>
Внутри инициализатора экземпляра виден код, недоступный при инициализации полей (то есть команда if). Поэтому инициализатор экземпляра фактически является конструктором безымянного внутреннего класса. Конечно, возможности его ограничены; перегружать такой инициализатор нельзя, и поэтому он будет присутствовать в классе только в единственном числе.

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

Снова о методе-фабрике

Посмотрите, насколько приятнее выглядит пример interfaces/Factories.java при использовании безымянных внутренних классов:

//: innerclasses/Factories.java
import static net.mindview.util.Print.*;
 
interface Service {
void method1();
void method2();
}
 
interface ServiceFactory {
Service getService();
}
 
class Implementation1 implements Service {
private Implementation1() {}
public void method1() {print("Implementation1 method1");}
public void method2() {print("Implementation1 method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementation1();
}
};
}
 
class Implementation2 implements Service {
private Implementation2() {}
public void method1() {print("Implementation2 method1");}
public void method2() {print("Implementation2 method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementation2();
}
};
}
 
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
// Реализации полностью взаимозаменяемы;
serviceConsumer(Implementation2.factory);
}
}

<spoiler text="Output:">

Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2

</spoiler>
Теперь конструкторы Implementation1 и Implementation2 могут быть закрытыми, и фабрику необязательно оформлять в виде именованного класса. Кроме того, часто бывает достаточно одного фабричного объекта, поэтому в данном случае он создается как статическое поле в реализации Service. Наконец, итоговый синтаксис выглядит более осмысленно.
Пример interfaces/Games.java тоже можно усовершенствовать с помощью безымянных внутренних классов:

//: innerclasses/Games.java
// Использование анонимных внутренних классов в библиотеке Game
import static net.mindview.util.Print.*;
 
interface Game { boolean move(); }
interface GameFactory { Game getGame(); }
 
class Checkers implements Game {
private Checkers() {}
private int moves = 0;
private static final int MOVES = 3;
public boolean move() {
print("Checkers move " + moves);
return ++moves != MOVES;
}
public static GameFactory factory = new GameFactory() {
public Game getGame() { return new Checkers(); }
};
}
 
class Chess implements Game {
private Chess() {}
private int moves = 0;
private static final int MOVES = 4;
public boolean move() {
print("Chess move " + moves);
return ++moves != MOVES;
}
public static GameFactory factory = new GameFactory() {
public Game getGame() { return new Chess(); }
};
}
 
public class Games {
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while(s.move())
;
}
public static void main(String[] args) {
playGame(Checkers.factory);
playGame(Chess.factory);
}
}

<spoiler text="Output:">

Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2

</spoiler>
Вспомните совет, данный в конце предыдущей главы: отдавать предпочтение классам перед интерфейсами. Если архитектура системы требует применения интерфейса, вы это поймете. В остальных случаях не применяйте интерфейсы без крайней необходимости.

Вложенные классы

Если связь между объектом внутреннего класса и объектом внешнего класса не нужна, можно сделать внутренний класс статическим (объявить его как static). Часто такой класс называют вложенным (nested).

Чтобы понять смысл ключевого слова static в отношении внутренних классов, следует вспомнить, что в объекте обычного внутреннего класса тайно хранится ссылка на объект создавшего его объемлющего внешнего класса. При использовании статического внутреннего класса такой ссылки не существует. Применение статического внутреннего класса означает следующее:


  • для создания объекта статического внутреннего класса не нужен объект внешнего класса;
  • из объекта вложенного класса нельзя обращаться к нестатическим членам внешнего класса.

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

//: innerclasses/Parcel11.java
// Вложенные (статические внутренние) классы
 
public class Parcel11 {
private static class ParcelContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected static class ParcelDestination
implements Destination {
private String label;
private ParcelDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
// Вложенные классы могут содержать другие статические элементы;
public static void f() {}
static int x = 10;
static class AnotherLevel {
public static void f() {}
static int x = 10;
}
}
public static Destination destination(String s) {
return new ParcelDestination(s);
}
public static Contents contents() {
return new ParcelContents();
}
public static void main(String[] args) {
Contents c = contents();
Destination d = destination("Tasmania");
}
}

В методе main() не требуется объекта класса Parcel11; вместо этого для вызова методов, возвращающих ссылки на Contents и Destination, используется обычный синтаксис обращения к статическим членам класса.
Как было сказано ранее, в обычном (не-статическом) внутреннем классе для обращения к объекту внешнего класса используется специальная ссылка this. Во вложенном классе такая ссылка недействительна (по аналогии со статическими методами).

Классы внутри интерфейсов

Обычно интерфейс не может содержать программный код, но вложенный класс может стать его частью. Любой класс, размещенный внутри интерфейса, автоматически является public и static. Так как класс является статическим, он не нарушает правил обращения с интерфейсом — этот вложенный класс просто использует пространство имен интерфейса. Во внутреннем классе даже можно реализовать окружающий интерфейс:

//: innerclasses/ClassInInterface.java
// {main: ClassInInterface$Test}
 
public interface ClassInInterface {
void howdy();
class Test implements ClassInInterface {
public void howdy() {
System.out.println("Howdy!");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}

<spoiler text="Output:">

Howdy!

</spoiler>
Вложение классов в интерфейсы может пригодиться для создания обобщенного кода, используемого с разными реализациями этого интерфейса.
Ранее в книге я предлагал помещать в каждый класс метод main(), позволяющий при необходимости протестировать данный класс. Недостатком такого подхода является дополнительный скомпилированный код, увеличивающий размеры программы. Если для вас это нежелательно, используйте статический внутренний класс для хранения тестового кода:

//: innerclasses/TestBed.java
// Помещение тестового кода во вложенный класс.
// {main: TestBed$Tester}
 
public class TestBed {
public void f() { System.out.println("f()"); }
public static class Tester {
public static void main(String[] args) {
TestBed t = new TestBed();
t.f();
}
}
}

<spoiler text="Output:">

f()

</spoiler>
При компиляции этого файла создается отдельный класс с именем TestBed$Tester (для запуска тестового кода наберите команду java TestBed$Tester). Вы можете использовать этот класс для тестирования, но включать его в окончательную версию программы необязательно; файл TestBed$Tester.class можно просто удалить перед окончательной сборкой программы.

Доступ вовне из многократно вложенных классов

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

//: innerclasses/MultiNestingAccess.java
// Вложенные классы могут обращаться ко всем членам всех
// классов, в которых они находятся.
class MNA {
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
}
 
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}

Как видно из примера, в классе MNA.A.B методы f() и g() вызываются без дополнительных описаний (несмотря на то, что они объявлены как private). Этот пример также демонстрирует синтаксис, который следует использовать при создании объектов внутренних классов произвольного уровня вложенности из другого класса. Синтаксис .new обеспечивает правильную область действия, и вам не приходится уточнять имя класса при вызове конструктора.

Внутренние классы: зачем?

К настоящему моменту мы подробно рассмотрели синтаксис и семантику работы внутренних классов, но это не дало ответа на вопрос, зачем они вообще нужны.
Что же заставило создателей Java добавить в язык настолько фундаментальное свойство?
Обычно внутренний класс наследует от класса или реализует интерфейс, а код внутреннего класса манипулирует объектом внешнего класса, в котором он был создан. Значит, можно сказать, что внутренний класс — это нечто вроде «окна» во внешний класс.

Возникает резонный вопрос: «Если мне понадобится ссылка на интерфейс, почему бы внешнему классу не реализовать этот интерфейс?» Ответ: «Если это все, что вам нужно, — значит, так и следует поступить». Но что же отличает внутренний класс, реализующий интерфейс, от внешнего класса, реализующего тот же интерфейс? Далеко не всегда удается использовать удобство интерфейсов — иногда приходится работать и с реализацией. Поэтому наиболее веская причина для использования внутренних классов такова:
Каждый внутренний класс способен независимо наследовать определенную реализацию. Таким образом, внутренний класс не ограничен при наследовании в ситуациях, где внешний класс уже наследует реализацию.

Без возможности внутренних классов наследовать реализацию более чем одного реального или абстрактного класса некоторые задачи планирования и программирования становятся практически неразрешимыми. Поэтому внутренний класс выступает как «довесок» решения проблемы множественного наследования. Интерфейсы берут на себя часть этой задачи, тогда как внутренние классы фактически обеспечивают «множественное наследование реализации». Другими словами, внутренние классы позволяют наследовать от нескольких не-интерфейсов.

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

//: innerclasses/MultiInterfaces.java
// Два способа реализации нескольких интерфейсов.
package innerclasses;
 
interface A {}
interface B {}
 
class X implements A, B {}
 
class Y implements A {
B makeB() {
// Безымянный внутренний класс:
return new B() {};
}
}
 
public class MultiInterfaces {
static void takesA(A a) {}
static void takesB(B b) {}
public static void main(String[] args) {
X x = new X();
Y y = new Y();
takesA(x);
takesA(y);
takesB(x);
takesB(y.makeB());
}
}

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

//: innerclasses/MultiImplementation.java
// При использовании реальных или абстрактных классов
// "множественное наследование реализации" возможно
// только с применением внутренних классов
package innerclasses;
 
class D {}
abstract class E {}
 
class Z extends D {
E makeE() { return new E() {}; }
}
 
public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args) {
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}

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


  • У внутреннего класса может существовать произвольное количество экземпляров, каждый из которых обладает собственной информацией состояния, не зависящей от состояния объекта внешнего класса.
  • Один внешний класс может содержать несколько внутренних классов, по-разному реализующих один и тот же интерфейс или наследующих от единого базового класса. Вскоре мы рассмотрим пример такой конструкции.
  • Место создания объекта внутреннего класса не привязано к месту и времени создания объекта внешнего класса.
  • Внутренний класс не использует тип отношений классов «является тем-то», способных вызвать недоразумения; он представляет собой отдельную сущность.

Например, если бы в программе Sequence.java отсутствовали внутренние классы, пришлось бы заявить, что «класс Sequence есть класс Selector», и при этом ограничиться только одним объектом Selector для конкретного объекта Sequence. А вы можете с легкостью определить второй метод, reverseSelector(), создающий объект Selector для перебора элементов Sequence в обратном порядке. Такую гибкость обеспечивают только внутренние классы.

Замыкания и обратные вызовы

Замыканием (closure) называется вызываемый объект, который сохраняет информацию о контексте, он был создан. Из этого определения видно, что внутренний класс является объектно-ориентированным замыканием, поскольку он не только содержит информацию об объекте внешнего класса («место создания»), но к тому же располагает ссылкой на весь объект внешнего класса, с помощью которой он может манипулировать всеми членами этого объекта, в том числе и закрытыми (private).

При обсуждении того, стоит ли включать в Java некое подобие указателей, самым веским аргументом «за» была возможность обратных вызовов (callback). В механизме обратного вызова некоторому стороннему объекту передается ин­формация, позволяющая ему затем обратиться с вызовом к объекту, который произвел изначальный вызов.

Это очень мощная концепция программирования, к которой мы еще вернемся. С другой стороны, при реализации обратного вызова на основе указателей вся ответственность за его правильное использование возлагается на программиста. Как было показано ранее, язык Java ориентирован на безопасное программирование, поэтому указатели в него включены не были.
Замыкание, предоставляемое внутренним классом, — хорошее решение, гораздо более гибкое и безопасное, чем указатель. Рассмотрим пример:

//: innerclasses/Callbacks.java
// Использование внутренних классов
// для реализации обратных вызовов
package innerclasses;
import static net.mindview.util.Print.*;
 
interface Incrementable {
void increment();
}
 
// Простая реализация интерфейса:
class Callee1 implements Incrementable {
private int i = 0;
public void increment() {
i++;
print(i);
}
}
 
class MyIncrement {
public void increment() { print("Other operation"); }
static void f(MyIncrement mi) { mi.increment(); }
}
 
// Если класс должен вызывать метод increment()
// по-другому, необходимо использовать внутренний класс:
class Callee2 extends MyIncrement {
private int i = 0;
public void increment() {
super.increment();
i++;
print(i);
}
private class Closure implements Incrementable {
public void increment() {
// Указывается метод внешнего класса;
// в противном случае возникает бесконечная рекурсия.
Callee2.this.increment();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}
 
class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) { callbackReference = cbh; }
void go() { callbackReference.increment(); }
}
 
public class Callbacks {
public static void main(String[] args) {
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
}

<spoiler text="Output:">

Other operation
1
1
2
Other operation
2
Other operation
3

</spoiler>
Этот пример также демонстрирует различия между реализацией интерфейса внешним или внутренним классом. Класс Callee1 — наиболее очевидное решение задачи с точки зрения программирования.

Класс Callee2 наследует от класса MyIncrement, в котором уже есть метод increment), выполняющий действие, никак не связанное с тем, что ожидает от него интерфейс Incrementable. Когда класс MyIncrement наследуется в Callee2, метод increment() нельзя переопределить для использования в качестве метода интерфейса Incrementable, поэтому нам приходится предоставлять отдельную реализацию во внутреннем классе. Также отметьте, что создание внутреннего класса не затрагивает и не изменяет существующий интерфейс внешнего класса.

Все элементы, за исключением метода getCallbackReference(), в классе Callee2 являются закрытыми. Для любой связи с окружающим миром необходим интерфейс Incrementable. Здесь мы видим, как интерфейсы позволяют полностью отделить интерфейс от реализации.

Внутренний класс Closure просто реализует интерфейс Incrementable, предоставляя при этом связь с объектом Callee2 — но связь эта безопасна. Кто бы ни получил ссылку на Incrementable, он в состоянии вызвать только метод increment(), и других возможностей у него нет (в отличие от указателя, с которым программист может вытворять все, что угодно).

Класс Caller получает ссылку на Incrementable в своем конструкторе (хотя передача ссылки для обратного вызова может происходить в любое время), а после этого использует ссылку для «обратного вызова» объекта Callee.
Главным достоинством обратного вызова является его гибкость — вы можете динамически выбирать функции, выполняемые во время работы программы.

Внутренние классы и система управления

В качестве более реального пример использования внутренних классов мы рассмотрим то, что я буду называть здесь системой управления (control framework).

Каркас приложения (application framework) — это класс или набор классов, разработанных для решения определенного круга задач. При работе с каркасами приложений обычно используется наследование от одного или нескольких классов, с переопределением некоторых методов. Код переопределенных методов адаптирует типовое решение, предоставляемое каркасом приложения, к вашим конкретным потребностям.

Система управления представляет собой определенный тип каркаса приложения, основным движущим механизмом которого является обработка событий. Такие системы называются системами, управляемыми по событиям (event-driven system). Одной из самых типичных задач в прикладном программировании является создание графического интерфейса пользователя (GUI), всецело и полностью ориентированного на обработку событий.

Чтобы на наглядном примере увидеть, как с применением внутренних классов достигается простота создания и использования библиотек, мы рассмотрим систему, ориентированную на обработку событий по их «готовности». Хотя в практическом смысле под «готовностью» может пониматься все, что угодно, в нашем случае она будет определяться по показаниям счетчика времени. Далее приводится общее описание управляющей системы, никак не зависящей от того, чем именно она управляет. Нужная информация предоставляется посредством наследования, при реализации метода action().

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

//: innerclasses/controller/Event.java
// Общие для всякого управляющего события методы.
package innerclasses.controller;
 
public abstract class Event {
private long eventTime;
protected final long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
start();
}
public void start() { // Позволяет перезапуск
eventTime = System.nanoTime() + delayTime;
}
public boolean ready() {
return System.nanoTime() >= eventTime;
}
public abstract void action();
}

Конструктор просто запоминает время (от момента создания объекта), через которое должно выполняться событие Event, и после этого вызывает метод start(), который прибавляет к текущему времени интервал задержки, чтобы вычислить время возникновения события. Метод start() отделен от конструктора, благодаря чему становится возможным «перезапуск» события после того, как его время уже истекло; таким образом, объект Event можно использовать многократно. Скажем, если вам понадобится повторяющееся событие, достаточно добавить вызов start() в метод action().

Метод ready() сообщает, что пора действовать — вызывать метод action(). Конечно, метод ready() может быть переопределен любым производным классом, если событие Event активизируется не по времени, а по иному условию.

Следующий файл описывает саму систему управления, которая распоряжается событиями и инициирует их. Объекты Event содержатся в контейнере List<Event>. На данный момент достаточно знать, что метод add() присоединяет объект Event к концу контейнера с типом List, метод size() возвращает количество элементов в контейнере, синтаксис foreach() осуществляет последовательную выборку элементов List, а метод remove() удаляет заданный элемент из контейнера:

//: innerclasses/controller/Controller.java
// Обобщенная система управления
package innerclasses.controller;
import java.util.*;
 
public class Controller {
// Класс из пакета java.util для хранения событий Event::
private List<Event> eventList = new ArrayList<Event>();
public void addEvent(Event c) { eventList.add(c); }
public void run() {
while(eventList.size() > 0)
// Make a copy so you're not modifying the list
// while you're selecting the elements in it:
for(Event e : new ArrayList<Event>(eventList))
if(e.ready()) {
System.out.println(e);
e.action();
eventList.remove(e);
}
}
}

Метод run() в цикле перебирает копию eventList в поисках событий Event, готовых для выполнения. Для каждого найденного элемента он выводит информацию об объекте методом toString(), вызывает метод action(), а после этого удаляет событие из списка.
Заметьте, что в этой архитектуре совершенно неважно, что конкретно выполняет некое событие Event. В этом и состоит «изюминка» разработанной системы; она отделяет постоянную составляющую от изменяющейся. «Вектором изменения» являются различные действия разнообразных событий Event, выражаемые посредством создания разных субклассов Event.
На этом этапе в дело вступают внутренние классы. Они позволяют добиться двух целей:


  • Вся реализация системы управления создается в одном классе, с полной инкапсуляцией всей специфики данной реализации. Внутренние классы используются для представления различных разновидностей action(), необходимых для решения задачи.
  • Внутренние классы помогают избежать громоздкой, неудобной реализации, так как у них есть доступ к внешнему классу. Без этой возможности программный код очень быстро станет настолько неприятным, что вам захочется поискать другие альтернативы.

Рассмотрим конкретную реализацию системы управления, разработанную для управления функциями оранжереи. Все события — включение света, воды и нагревателей, звонок и перезапуск системы — абсолютно разнородны. Однако система управления разработана так, что различия в коде легко изолируются. Внутренние классы помогают унаследовать несколько производных версий одного базового класса Event в пределах одного класса. Для каждого типа события от Event наследуется новый внутренний класс, и в его реализации action() записывается управляющий код.
Как это обычно бывает при использовании каркасов приложений, класс GreenhouseControls наследует от класса Controller:

//: innerclasses/GreenhouseControls.java
// Пример конкретного приложения на основе системы
// управления, все находится в одном классе. Внутренние
// классы дают возможность инкапсулировать различную
// функциональность для каждого отдельного события
import innerclasses.controller.*;
 
public class GreenhouseControls extends Controller {
private boolean light = false;
public class LightOn extends Event {
public LightOn(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов
// физическое включение света
light = true;
}
public String toString() { return "Light is on"; }
}
public class LightOff extends Event {
public LightOff(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов
// физическое выключение света
light = false;
}
public String toString() { return "Light is off"; }
}
private boolean water = false;
public class WaterOn extends Event {
public WaterOn(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов.
// выключения системы полива
water = true;
}
public String toString() {
return "Greenhouse water is on";
}
}
public class WaterOff extends Event {
public WaterOff(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов.
// выключения системы полива
water = false;
}
public String toString() {
return "Greenhouse water is off";
}
}
private String thermostat = "Day";
public class ThermostatNight extends Event {
public ThermostatNight(long delayTime) {
super(delayTime);
}
public void action() {
// Сюда помещается аппаратный вызов.
// thermostat = "Ночь";
thermostat = "Night";
}
public String toString() {
return "Thermostat on night setting";
}
}
public class ThermostatDay extends Event {
public ThermostatDay(long delayTime) {
super(delayTime);
}
public void action() {
// Сюда помещается аппаратный вызов.
// thermostat = "День"
thermostat = "Day";
}
public String toString() {
return "Thermostat on day setting";
}
}
// Пример метода action(), вставляющего
// самого себя в список событий.
public class Bell extends Event {
public Bell(long delayTime) { super(delayTime); }
public void action() {
addEvent(new Bell(delayTime));
}
public String toString() { return "Bing!"; }
}
public class Restart extends Event {
private Event[] eventList;
public Restart(long delayTime, Event[] eventList) {
super(delayTime);
this.eventList = eventList;
for(Event e : eventList)
addEvent(e);
}
public void action() {
for(Event e : eventList) {
e.start(); // Перезапуск каждый раз
addEvent(e);
}
start(); // Возвращаем это событие Event
addEvent(this);
}
public String toString() {
return "Restarting system";
}
}
public static class Terminate extends Event {
public Terminate(long delayTime) { super(delayTime); }
public void action() { System.exit(0); }
public String toString() { return "Terminating"; }
}
}

Заметьте, что поля light, thermostat и ring принадлежат внешнему классу GreenhouseControls, и все же внутренние классы имеют возможность обращаться к ним, не используя особой записи и не запрашивая особых разрешений. Большинство методов action() требует управления оборудованием оранжереи, что, скорее всего, привлечет в программу сторонние низкоуровневые вызовы.

В основном классы Event похожи друг на друга, однако классы Bell и Restart представляют собой особые случаи. Bell выдает звуковой сигнал и добавляет себя в список событий, чтобы звонок позднее сработал снова. Заметьте, что внутренние классы действуют почти как множественное наследование: классы Bell и Restart имеют доступ ко всем методам класса Event, а также ко всем методам внешнего класса GreenhouseControls.

Классу Restart передается массив объектов Event, которые он добавляет в контроллер. Так как Restart также является объектом Event, вы можете добавить этот объект в список событий в методе Restart.action(), чтобы система регулярно перезапускалась.

Следующий класс настраивает систему, создавая объект GreenhouseControls и добавляя в него разнообразные типы объектов Event. Это пример шаблона проектирования «команда» — каждый объект в EventList представляет собой запрос, инкапсулированный в объекте:

//: innerclasses/GreenhouseController.java
// Configure and execute the greenhouse system.
// {Args: 5000}
import innerclasses.controller.*;
 
public class GreenhouseController {
public static void main(String[] args) {
GreenhouseControls gc = new GreenhouseControls();
// Вместо жесткого кодирования фиксированных данных
// можно было бы считать информацию для настройки
// из текстового файла:
gc.addEvent(gc.new Bell(900));
Event[] eventList = {
gc.new ThermostatNight(0),
gc.new LightOn(200),
gc.new LightOff(400),
gc.new WaterOn(600),
gc.new WaterOff(800),
gc.new ThermostatDay(1400)
};
gc.addEvent(gc.new Restart(2000, eventList));
if(args.length == 1)
gc.addEvent(
new GreenhouseControls.Terminate(
new Integer(args[0])));
gc.run();
}
}

<spoiler text="Output:">

Bing!
Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Greenhouse water is off
Thermostat on day setting
Restarting system
Terminating

</spoiler>
Класс инициализирует систему, включая в нее нужные события. Если передать программе параметр командной строки, она завершается по истечении заданного количества миллисекунд (используется при тестировании). Конечно, чтобы программа стала более гибкой, описания событий следовало бы не включать в программный код, а загружать из файла.
Этот пример поможет понять всю ценность механизма внутренних классов, особенно в случае с системами управления.

Наследование от внутренних классов

Так как конструктор внутреннего класса связывается со ссылкой на окружающий внешний объект, наследование от внутреннего класса получается чуть сложнее, чем обычное. Проблема состоит в том, что «скрытая» ссылка на объект объемлющего внешнего класса должна быть инициализирована, а в производном классе больше не существует объемлющего объекта по умолчанию. Для явного указания объемлющего внешнего объекта применяется специальный синтаксис:

//: innerclasses/InheritInner.java
// Наследование от внутреннего класса.
 
class WithInner {
class Inner {}
}
 
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // He компилируется
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}

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

ссылкаНаОбъемлющийКласс.super();

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

Можно ли переопределить внутренний класс?

Что происходит, если вы создаете внутренний класс, затем наследуете от его внешнего класса, а после этого заново описываете внутренний класс в производном классе? Другими словами, можно ли переопределить внутренний класс? Это было бы довольно интересно, но «переопределение» внутреннего класса, как если бы он был еще одним методом внешнего класса, фактически не имеет никакого эффекта:

//: innerclasses/BigEgg.java
// Внутренний класс нельзя переопределить
// подобно обычному методу,
import static net.mindview.util.Print.*;
 
class Egg {
private Yolk y;
protected class Yolk {
public Yolk() { print("Egg.Yolk()"); }
}
public Egg() {
print("New Egg()");
y = new Yolk();
}
}
 
public class BigEgg extends Egg {
public class Yolk {
public Yolk() { print("BigEgg.Yolk()"); }
}
public static void main(String[] args) {
new BigEgg();
}
}

<spoiler text="Output:">

New Egg()
Egg.Yolk()

</spoiler>
Конструктор по умолчанию автоматически синтезируется компилятором, а в нем вызывается конструктор по умолчанию из базового класса. Можно подумать, что при создании объекта BigEgg должен использоваться «переопределенный» класс Yolk, но это отнюдь не так, как видно из результата работы программы.

Этот пример просто показывает, что при наследовании от внешнего класса ничего особенного с внутренними классами не происходит. Два внутренних класса — совершенно отдельные составляющие, с независимыми пространствами имен. Впрочем, возможность явного наследования от внутреннего класса сохранилась:

//: innerclasses/BigEgg2.java
// Правильное наследование внутреннего класса,
import static net.mindview.util.Print.*;
 
class Egg2 {
protected class Yolk {
public Yolk() { print("Egg2.Yolk()"); }
public void f() { print("Egg2.Yolk.f()");}
}
private Yolk y = new Yolk();
public Egg2() { print("New Egg2()"); }
public void insertYolk(Yolk yy) { y = yy; }
public void g() { y.f(); }
}
 
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() { print("BigEgg2.Yolk()"); }
public void f() { print("BigEgg2.Yolk.f()"); }
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
}

<spoiler text="Output:">

Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()

</spoiler>
Теперь класс BigEgg2.Yolk явно расширяет класс Egg2.Yolk и переопределяет его методы. Метод insertYolk() позволяет классу BigEgg2 повысить один из своих объектов Yolk до ссылки у в классе Egg2, поэтому при вызове y.f() в методе g() используется переопределенная версия f(). Второй вызов Egg2.Yolk() — это вызов конструктора базового класса из конструктора класса BigEgg2.Yolk. Мы также видим, что при вызове метода g() используется «обновленная» версия метода.

Локальные внутренние классы

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

//: innerclasses/LocalInnerClass.java
// Хранит последовательность объектов
import static net.mindview.util.Print.*;
 
interface Counter {
int next();
}
 
public class LocalInnerClass {
private int count = 0;
Counter getCounter(final String name) {
// Локальный внутренний класс:
class LocalCounter implements Counter {
public LocalCounter() {
// У локального внутреннего класса
// может быть собственный конструктор:
print("LocalCounter()");
}
public int next() {
printnb(name); // Access local final
return count++;
}
}
return new LocalCounter();
}
// To же самое с безымянным внутренним классом:
Counter getCounter2(final String name) {
return new Counter() {
// У безымянного внутреннего класса не может быть
// именованного конструктора, «легальна» только
// инициализация экземпляром:
{
print("Counter()");
}
public int next() {
printnb(name); // final аргумент
return count++;
}
};
}
public static void main(String[] args) {
LocalInnerClass lic = new LocalInnerClass();
Counter
c1 = lic.getCounter("Local inner "),
c2 = lic.getCounter2("Anonymous inner ");
for(int i = 0; i < 5; i++)
print(c1.next());
for(int i = 0; i < 5; i++)
print(c2.next());
}
}

<spoiler text="Output:">

LocalCounter()
Counter()
Local inner 0
Local inner 1
Local inner 2
Local inner 3
Local inner 4
Anonymous inner 5
Anonymous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9

</spoiler>
Объект Counter возвращает следующее по порядку значение. Он реализован и как локальный класс, и как безымянный внутренний класс, с одинаковым поведением и характеристиками. Поскольку имя локального внутреннего класса недоступно за пределами метода, доводом для применения локального класса вместо безымянного внутреннего может быть необходимость в именованном
конструкторе и (или) перегруженных конструкторах; безымянные внутренние классы допускают только инициализацию экземпляром.
Другая причина для использования локального внутреннего класса вместо безымянного внутреннего — необходимость создания более чем одного объекта такого класса.

Идентификаторы внутренних классов

Так как каждый класс компилируется в файл с расширением .class, содержащий полную информацию о создании его экземпляров (эта информация помещается в «мета-класс», называемый объектом Class), напрашивается предположение, что внутренние классы также создают файлы .class для хранения информации о своих объектах Class. Имена этих файлов-классов строятся по жестко заданной схеме: имя объемлющего внешнего класса, затем символ $ и имя внутреннего класса. Например, для программы LocallnnerClass.java создаются следующие файлы с расширением .class:

Counter.class
LocalInnerClass$2.class
LocalInnerClass$lLocalCounter.class
LocalInnerClass.class

Если внутренние классы являются безымянными, компилятор использует в качестве их идентификаторов номера. Если внутренние классы вложены в другие внутренние классы, их имена просто присоединяются после символа $ и идентификаторов всех внешних классов.
Хотя такая схема построения внутренних имен проста и прямолинейна, она вполне надежна и работает практически в любых ситуациях. Так как она является стандартной для языка Java, все получаемые файлы автоматически становятся платформно-независимыми.

Резюме

Интерфейсы и внутренние классы — весьма нетривиальные концепции, и во многих других объектно-ориентированных языках вы их не найдете. Например, в C++ нет ничего похожего. Вместе они решают те задачи, которые C++ пытается решить с применением множественного наследования. Однако множественное наследование C++ создает массу проблем; по сравнению с ним интерфейсы и внутренние классы Java гораздо более доступны.

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

]]>
Книги по Java https://linexp.ru?id=4743 Wed, 29 Jun 2022 14:26:21 GMT
<![CDATA[Глава 11 Thinking in Java 4th edition]]> КОНТЕЙНЕРЫ И ХРАНЕНИЕ ОБЪЕКТОВОграниченное количество объектов с фиксированным временем жизни характерно разве что для относительно простых программ. В основном ваши программы будут создавать новые объекты на основании критериев, которые станут известны лишь во время их работы. До начала выполнения программы вы не знаете ни количества, ни даже типов нужных вам объектов.

КОНТЕЙНЕРЫ И ХРАНЕНИЕ ОБЪЕКТОВ

Ограниченное количество объектов с фиксированным временем жизни характерно разве что для относительно простых программ.
В основном ваши программы будут создавать новые объекты на основании критериев, которые станут известны лишь во время их работы. До начала выполнения программы вы не знаете ни количества, ни даже типов нужных вам объектов. Следовательно, использовать именованную ссылку для каждого из возможных объектов не удастся:

 МуТуре aReference;

так как заранее неизвестно, сколько таких ссылок реально потребуется.
В большинстве языков существуют некоторые пути решения этой крайне насущной задачи. В Java предусмотрено несколько способов хранения объектов (или, точнее, ссылок на объекты). Встроенным типом является массив, который мы уже рассмотрели. Библиотека утилит Java (java.util.*) также содержит достаточно полный набор классов контейнеров (также известных, как классы кол­лекций, но, поскольку имя Collection (коллекция) используется для обозначения определенного подмножества библиотеки Java, я буду употреблять общий термин «контейнер»). Контейнеры обладают весьма изощренными возможностями для хранения объектов и работы с ними, и с их помощью удается решить огромное количество задач.

Параметризованные и типизованные контейнеры

Одна из проблем, существовавших при работе с контейнерами до выхода Java SE5, заключалась в том, что компилятор позволял вставить в контейнер объект неверного типа. Для примера рассмотрим один из основных рабочих контейнеров
ArrayList, в котором мы собираемся хранить объекты Apple. Пока рассматривайте ArrayList как «автоматически расширяемый массив». Работать с ним несложно: создайте объект, вставляйте объекты методом add(), обращайтеcь к ним методом get(), используйте индексирование — так же, как для массивов, но без квадратных скобок. ArrayList также содержит метод size(), который возвращает текущее количество элементов в массиве.
В следующем примере в контейнере размещаются объекты Apple и Orange, которые затем извлекаются из него. Обычно компилятор Java выдает предупреждение, потому что в данном примере не используется параметризация, однако в Java SE5 существует специальная директива @SuppressWarnings для подавления предупреждений. Директивы начинаются со знака @ и могут получать аргументы; в данном случае аргумент означает, что подавляются только «непроверяемые» предупреждения:

//: holding/ApplesAndOrangesWithoutGenerics.java
// Простой пример работы с контейнером
// (компилятор выдает предупреждения).
import java.util.*;
 
class Apple {
private static long counter;
private final long id = counter++;
public long id() { return id; }
}
 
class Orange {}
 
public class ApplesAndOrangesWithoutGenerics {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ArrayList apples = new ArrayList();
for(int i = 0; i < 3; i++)
apples.add(new Apple());
// He препятствует добавлению объекта Orange:
apples.add(new Orange());
for(int i = 0; i < apples.size(); i++)
((Apple)apples.get(i)).id();
// Объект Orange обнаруживается только во время выполнения
}
}

Директивы Java SE5 будут рассмотрены позднее.
Apple и Orange — совершенно разные классы; они не имеют ничего общего, кроме происхождения от Object (напомню: если в программе явно не указан базовый класс, то в этом качестве используется Object). Так как в ArrayList хранятся объекты Object, метод add() может добавлять в контейнер не только объекты Apple, но и Orange, без ошибок компиляции или времени выполнения. Но при вызове метода get() класса ArrayList вы вместо объекта Apple получаете ссылку на Object, которую необходимо преобразовать в Apple. Все выражение должно быть заключено в круглые скобки, чтобы преобразование было выполнено перед вызовом метода id() класса Apple. Во время выполнения, при попытке преобразования объекта Orange в Apple, произойдет исключение.
В главе «параметризованные типы» вы узнаете, что создание классов, использующих механизм параметризации, может быть довольно сложной задачей. С другой стороны, с применением готовых параметризованных классов проблем обычно не бывает. Например, чтобы определить объект ArrayList, предназначенный для хранения объектов Apple, достаточно использовать вместо имени ArrayList запись АrrayList< Apple>. В угловых скобках перечисляются параметры типов (их может быть несколько), указывающие тип объектов, хранящихся в данном экземпляре контейнера.
Механизм параметризации предотвращает занесение объектов неверного типа в контейнер на стадии компиляции. Рассмотрим тот же пример, но с использованием параметризации:

//: holding/ApplesAndOrangesWithGenerics.java
import java.util.*;
 
public class ApplesAndOrangesWithGenerics {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<Apple>();
for(int i = 0; i < 3; i++)
apples.add(new Apple());
// Ошибка компиляции:
// apples.add(new Orange());
for(int i = 0; i < apples.size(); i++)
System.out.println(apples.get(i).id());
// Использование foreach:
for(Apple c : apples)
System.out.print(c.id() + " ");
}
}

<spoiler text="Output:">

0 1 2 0 1 2

</spoiler>
На этот раз компилятор не разрешит поместить объекты Orange в контейнер apples, поэтому вы получите ошибку на стадии компиляции (а не на стадии выполнения).
Также обратите внимание на то, что выборка данных из List не требует преобразования типов. Поскольку контейнер знает тип хранящихся в нем элементов, он автоматически выполняет преобразование при вызове get(). Таким образом, параметризация не только позволяет компилятору проверять тип объектов, помещаемых в контейнеры, но и упрощает синтаксис работы с объектами в контейнере. Пример также показывает, что, если индексы элементов вам не нужны, для перебора можно воспользоваться синтаксисом foreach.
Вы не обязаны точно соблюдать тип объекта, указанный в качестве параметра типа. Восходящее преобразование работает с параметризованными контейнерами точно так же, как и с другими типами:

//: holding/GenericsAndUpcasting.java
import java.util.*;
 
class GrannySmith extends Apple {}
class Gala extends Apple {}
class Fuji extends Apple {}
class Braeburn extends Apple {}
 
public class GenericsAndUpcasting {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<Apple>();
apples.add(new GrannySmith());
apples.add(new Gala());
apples.add(new Fuji());
apples.add(new Braeburn());
for(Apple c : apples)
System.out.println(c);
}
}

<spoiler text="Output:"> (Sample)

GrannySmith@7d772e
Gala@11b86e7
Fuji@35ce36
Braeburn@757aef

</spoiler>
Мы видим, что в контейнер, рассчитанный на хранение объектов Apple, можно помещать объекты типов, производных от Apple.
В результатах, полученных с использованием метода toString() объекта Object, выводится имя класса с беззнаковым шестнадцатеричным представлением хеш-кода объекта (сгенерированного методом hashCode()).

Основные концепции

В библиотеке контейнеров Java проблема хранения объектов делится на две концепции, выраженные в виде базовых интерфейсов библиотеки:


  • Коллекция: группа отдельных элементов, сформированная по некоторым правилам. Класс List (список) хранит элементы в порядке вставки, в классе Set (множество) нельзя хранить повторяющиеся элементы, а класс Queue (очередь) выдает элементы в порядке, определяемом спецификой очереди (обычно это порядок вставки элементов в очередь).
  • Карта: набор пар объектов «ключ-значение», с возможностью выборки по ключу. ArrayList позволяет искать объекты по порядковым номерам, поэтому в каком-то смысле он связывает числа с объектами. Класс Map (карта — также встречаются термины ассоциативный массив и словарь) позволяет искать объекты по другим объектам — например, получить объект значения по объекту ключа, по аналогии с поиском определения по слову.

Хотя на практике это не всегда возможно, в идеале весь программный код должен писаться в расчете на взаимодействие с этими интерфейсами, а точный тип указывается только в точке создания. Следовательно, объект List может быть создан так:

List<Apple> apples = new ArrayList<Apple>();

Обратите внимание на восходящее преобразование ArrayList к List, в отличие от предыдущих примеров. Если позднее вы решите изменить реализацию, достаточно сделать это в точке создания:

List<Apple> apples = new LinkedList<Apple>();

Итак, в типичной ситуации вы создаете объект реального класса, повышаете его до соответствующего интерфейса, а затем используете интерфейс во всем остальном коде.
Такой подход работает не всегда, потому что некоторые классы обладают дополнительной функциональностью. Например, LinkedList содержит дополнительные методы, не входящие в интерфейс List, а ТrееМар — методы, не входящие в Map. Если такие методы используются в программе, восходящее преобразование к обобщенному интерфейсу невозможно.
Интерфейс Collection представляет концепцию последовательности как способа хранения группы объектов. В следующем простом примере интерфейс Collection (представленный контейнером ArrayList) заполняется объектами Integer, с последующим выводом всех элементов полученного контейнера:

//: holding/SimpleCollection.java
import java.util.*;
 
public class SimpleCollection {
public static void main(String[] args) {
Collection<Integer> c = new ArrayList<Integer>();
for(int i = 0; i < 10; i++)
c.add(i); // Autoboxing
for(Integer i : c)
System.out.print(i + ", ");
}
}

<spoiler text="Output:">

0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

</spoiler>
Поскольку в этом примере используются только методы Collection, подойдет объект любого класса, производного от Collection, но ArrayList является самым простейшим типом последовательности.

Все коллекции поддерживают перебор в синтаксисе foreach, как в приведенном примере. Позднее в этой главе будет рассмотрена другая, более гибкая концепция итераторов.

Добавление групп элементов

Семейства Arrays и Collections в java.util содержат вспомогательные методы для включения групп элементов в коллекции. Метод Arrays.asList() получает либо массив, либо список элементов, разделенных запятыми, и преобразует его в объект List. Метод Collections.addAll() получает объект Collection и либо массив, либо список, разделенный запятыми, и добавляет элементы в Collection. Пример:

//: holding/AddingGroups.java
// Добавление групп элементов в объекты Collection
import java.util.*;
 
public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection =
new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5));
Integer[] moreInts = { 6, 7, 8, 9, 10 };
collection.addAll(Arrays.asList(moreInts));
// Работает намного быстрее, но таким способом
// невозможно сконструировать Collection:
Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
// Produces a list "backed by" an array:
List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
list.set(1, 99);// Можно - изменение элемента
// list.add(21); // Ошибка времени выполнения - нижележащий
// массив не должен изменяться в размерах
}
}

Конструктор Collection может получать другой объект Collection, используемый для его инициализации, поэтому для передачи исходных данных можно воспользоваться методом Arrays.asList(). Однако метод Collections.addAll() работает намного быстрее, и вы с таким же успехом можете сконструировать Collection без элементов, а затем вызвать Collections.addAll — этот способ считается предпочтительным.
Методу Collection.addAll() в аргументе может передаваться только другой объект Collection, поэтому он уступает в гибкости методам Arrays.asList() и Collections.addAll(), использующим переменные списки аргументов.
Также можно использовать вывод Arrays.asList() напрямую, в виде List, но в этом случае нижележащим представлением будет массив, не допускающий изменения размеров. Вызов add() или delete() для такого списка приведет к попытке изменения размера массива, а это приведет к ошибке во время выполнения.
Недостаток Arrays.asList() заключается в том, что он пытается «вычислить» итоговый тип List, не обращая внимания на то, что ему присваивается. Иногда это создает проблемы:

//: holding/AsListInference.java
// Arrays.asList() makes its best guess about type.
import java.util.*;
 
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
 
public class AsListInference {
public static void main(String[] args) {
List<Snow> snow1 = Arrays.asList(
new Crusty(), new Slush(), new Powder());
 
// He компилируется-
// List<Snow> snow2 = Arrays.asList(
// new Light(), new Heavy());
// Сообщение компилятора:
// found  : java.util.List<Powder>
// required: java.util.List<Snow>
 
// Collections.addAll() работает нормально:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy());
 
// Передача информации посредством уточнения
// типа аргумента
List<Snow> snow4 = Arrays.<Snow>asList(
new Light(), new Heavy());
}
}

При попытке создания snow2, Arrays.asList() создает List<Powder> вместо List <Snow>, тогда как Collections.addAll() работает нормально, потому что целевой тип определяется первым аргументом. Как видно из создания snow4, в вызов Arrays.asList() можно вставить «подсказку», которая сообщает компилятору фактический тип объекта List, производимого Arrays.asList().
С контейнерами Map дело обстоит сложнее, и стандартная библиотека Java не предоставляет средств их автоматической инициализации, кроме как по содержимому другого объекта Map.


Source(s): Java 4th edition

Вывод содержимого контейнеров

Для получения печатного представления массива необходимо использовать метод Arrays.toString, но контейнеры отлично выводятся и без посторонней помощи. Следующий пример демонстрирует использование основных типов контейнеров:

//: holding/PrintingContainers.java
// Вывод контейнеров по умолчанию
import java.util.*;
import static net.mindview.util.Print.*;
 
public class PrintingContainers {
static Collection fill(Collection<String> collection) {
collection.add("rat");
collection.add("cat");
collection.add("dog");
collection.add("dog");
return collection;
}
static Map fill(Map<String,String> map) {
map.put("rat", "Fuzzy");
map.put("cat", "Rags");
map.put("dog", "Bosco");
map.put("dog", "Spot");
return map;
}
public static void main(String[] args) {
print(fill(new ArrayList<String>()));
print(fill(new LinkedList<String>()));
print(fill(new HashSet<String>()));
print(fill(new TreeSet<String>()));
print(fill(new LinkedHashSet<String>()));
print(fill(new HashMap<String,String>()));
print(fill(new TreeMap<String,String>()));
print(fill(new LinkedHashMap<String,String>()));
}
}

<spoiler text="Output:">

[rat, cat, dog, dog]
[rat, cat, dog, dog]
[dog, cat, rat]
[cat, dog, rat]
[rat, cat, dog]
{dog=Spot, cat=Rags, rat=Fuzzy}
{cat=Rags, dog=Spot, rat=Fuzzy}
{rat=Fuzzy, cat=Rags, dog=Spot}

</spoiler>
Как уже было упомянуто, в библиотеке контейнеров Java существует две основные категории, различающиеся прежде всего тем, сколько в одной ячейке контейнера «помещается» элементов. Коллекции (Collection) содержат только один элемент в каждой ячейке. К этой категории относятся список (List), где в определенной последовательности хранится группа элементов, множество (Set), в которое можно добавлять только по одному элементу определенного типа, и очередь (Queue). В контейнерах Map (карта) хранятся два объекта: ключ и связанное с ним значение.
Из выходных данных программы видно, что вывод по умолчанию (обеспечиваемый методом toString() каждого контейнера) дает вполне приличные результаты. Содержимое Collection выводится в квадратных скобках, с разделением элементов запятыми. Содержимое Map заключается в фигурные скобки, ключи и значения разделяются знаком равенства (ключи слева, значения справа).
Контейнеры ArrayList и LinkedList принадлежат к семейству List, и из выходных данных видно, что элементы в них хранятся в порядке вставки. Они различаются не только скоростью выполнения тех или иных операций, но и тем, что LinkedList содержит больше операций, чем ArrayList.
HashSet, TreeSet и LinkedHashSet относятся к семейству Set. Из выходных данных видно, что в множествах Set каждый элемент хранится только в одном экземпляре, а разные реализации Set используют разный порядок хранения элементов. В HashSet порядок элементов определяется по довольно сложному алгоритму — пока достаточно знать, что этот алгоритм обеспечивает минимальное время выборки элементов, но порядок следования элементов на первый взгляд выглядит хаотично. Если порядок хранения для вас важен, используйте контейнер TreeSet, в котором объекты хранятся отсортированными по возрастанию в порядке сравнения, или LinkedHashSet с хранением элементов в порядке добавления.
Карта (Map) позволяет искать объекты по ключу, как несложная база данных. Объект, ассоциированный с ключом, называется значением. (Карты также называют ассоциативными массивами.)
В нашем примере используются три основные разновидности Map: HashMap, TreeMap и LinkedHashMap. Как и HashSet, HashMap обеспечивает максимальную скорость выборки, а порядок хранения его элементов не очевиден. TreeMap хра­нит ключи отсортированными по возрастанию, a LinkedHashMap хранит ключи в порядке вставки, но обеспечивает скорость поиска HashMap.

List

Контейнеры List гарантируют определенный порядок следования элементов. Интерфейс List дополняет Collection несколькими методами, обеспечивающими вставку и удаление элементов в середине списка. Существует две основные разновидности List:


  • Базовый контейнер ArrayList, оптимизированный для произвольного доступа к элементам, но с относительно медленнными операциями вставки (удаления) элементов в середине списка.
  • Контейнер LinkedList, оптимизированный для последовательного доступа, с быстрыми операциями вставки (удаления) в середине списка; Произвольный доступ к элементам LinkedList выполняется относительно медленно, но по широте возможностей он превосходит ArrayList.

В следующем примере используется библиотека typenfo.pets из главы «Информация о типе». Она содержит иерархию классов домашних животных Pet, а также ряд вспомогательных средств для случайного построения объектов Pet. Пока достаточно знать, что (1) библиотека содержит класс Pet и производные типы, и (2) статический метод Pets.arrayList() возвращает контейнер ArrayList, заполненный случайно выбранными объектами Pet.

//: holding/ListFeatures.java
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class ListFeatures {
public static void main(String[] args) {
Random rand = new Random(47);
List<Pet> pets = Pets.arrayList(7);
print("1: " + pets);
Hamster h = new Hamster();
pets.add(h); // Автоматическое изменение размера
print("2: " + pets);
print("3: " + pets.contains(h));
pets.remove(h); // Удаление объекта
Pet p = pets.get(2);
print("4: " + p + " " + pets.indexOf(p));
Pet cymric = new Cymric();
print("5: " + pets.indexOf(cymric));
print("6: " + pets.remove(cymric));
// Точно заданный объект:
print("7: " + pets.remove(p));
print("8: " + pets);
pets.add(3, new Mouse()); // Вставка no индексу
print("9: " + pets);
List<Pet> sub = pets.subList(1, 4);
print("subList: " + sub);
print("10: " + pets.containsAll(sub));
Collections.sort(sub); // Сортировка "на месте"
print("sorted subList: " + sub);
// Для containsAll() порядок неважен:
print("11: " + pets.containsAll(sub));
Collections.shuffle(sub, rand); // Случайная перестановка
print("shuffled subList: " + sub);
print("12: " + pets.containsAll(sub));
List<Pet> copy = new ArrayList<Pet>(pets);
sub = Arrays.asList(pets.get(1), pets.get(4));
print("sub: " + sub);
copy.retainAll(sub);
print("13: " + copy);
copy = new ArrayList<Pet>(pets); // Получение новой копии
copy.remove(2); // Удаление по индексу
print("14: " + copy);
copy.removeAll(sub); // Удаление заданных элементов print("15: " + copy);
print("15: " + copy);
copy.set(1, new Mouse()); // Замена элемента
print("16: " + copy);
copy.addAll(2, sub); // Вставка в середину списка
print("17: " + copy);
print("18: " + pets.isEmpty());
pets.clear(); // Удаление всех элементов
print("19: " + pets);
print("20: " + pets.isEmpty());
pets.addAll(Pets.arrayList(4));
print("21: " + pets);
Object[] o = pets.toArray();
print("22: " + o[3]);
Pet[] pa = pets.toArray(new Pet[0]);
print("23: " + pa[3].id());
}
}

<spoiler text="Output:">

1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
2: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Hamster]
3: true
4: Cymric 2
5: -1
6: false
7: true
8: [Rat, Manx, Mutt, Pug, Cymric, Pug]
9: [Rat, Manx, Mutt, Mouse, Pug, Cymric, Pug]
subList: [Manx, Mutt, Mouse]
10: true
sorted subList: [Manx, Mouse, Mutt]
11: true
shuffled subList: [Mouse, Manx, Mutt]
12: true
sub: [Mouse, Pug]
13: [Mouse, Pug]
14: [Rat, Mouse, Mutt, Pug, Cymric, Pug]
15: [Rat, Mutt, Cymric, Pug]
16: [Rat, Mouse, Cymric, Pug]
17: [Rat, Mouse, Mouse, Pug, Cymric, Pug]
18: false
19: []
20: true
21: [Manx, Cymric, Rat, EgyptianMau]
22: EgyptianMau
23: 14

</spoiler>
Строки вывода пронумерованы, чтобы вам было удобнее связывать результат с исходным кодом.
В первой строке выводится исходный контейнер List с объектами Pets. В отличие от массивов, List поддерживает добавление и удаление элементов с изменением размеров списка. Результат добавления Hamster виден в строке 2: объект появляется в конце списка.

Метод contains() проверяет, присутствует ли объект в списке. Чтобы удалить объект, передайте ссылку на него методу remove(). Кроме того, при наличии ссылки на объект можно узнать его индекс в списке при помощи метода indexOf(), как показано в строке 4.

При проверке вхождения элемента в List, проверке индекса элемента и удаления элемента из List по ссылке используется метод equals() (из корневого класса Object). Все объекты Pet считаются уникальными, поэтому несмотря на присутствие двух объектов Cymric в списке, если я создам новый объект Cymric и передам его indexOf(), результат будет равен -1 (элемент не найден), а вызов remove() вернет false. Для других классов метод equals() может быть определен иначе — например, объекты String считаются равными в случае совпадения содержимого.

В строках 7 и 8 из List успешно удаляется заданный объект. Строка 9 и предшествующий ей код демонстрируют вставку элемента в середину списка. Метод subList() позволяет легко создать «срез» из подмножества элементов списка; естественно, при передаче его методу containsAll() большего списка будет получен истинный результат. Вызовы Collections.sort() и Collections.shuffle() для sub не влияют на результат вызова containsAll().

Метод retainAll() фактически выполняет операцию «пересечения множеств», то есть определения всех элементов сору, которые также присутствуют в sub. И снова поведение метода зависит от реализации equals().

В строке 14 представлен результат удаления элемента по индексу — это проще, чем удаление по ссылке на объект, потому что вам не придется беспокоиться о поведении equals().
Работа метода removeAll() также зависит от equals(). Как подсказывает название, метод удаляет из List все объекты, входящие в List-аргумент.

Название метода set() выбрано неудачно, потому что оно совпадает с именем класса Set — возможно, лучше было бы назвать метод «replace», потому что он заменяет элемент с заданным индексом (первый аргумент) вторым аргументом.

В строке вывода 17 показано, что для List существует перегруженный метод addAll(), вставляющий новый список в середину исходного списка (вместо простого добавления в конец методом addAll(), унаследованным от Collection).

В строках 18-20 представлен результат вызова методов isEmpty() и clear(). Строки 22 и 23 демонстрируют, что любой объект Collection можно преобразовать в массив с использованием toArray().

Итераторы

У любого контейнера должен существовать механизм вставки и выборки элементов. В конце концов, контейнер предназначен именно для хранения объектов. При работе с List для вставки может использоваться метод add(), а для выборки — метод get() (впрочем, существуют и другие способы).

Если взглянуть на ситуацию с более высокого уровня, обнаруживается проблема: чтобы использовать контейнер в программе, необходимо знать его точный тип. Что, если вы начали использовать в программе контейнер List, а затем обнаружили, что в вашем случае будет удобнее применить тот же код к множеству (Set)? Или если вы хотите написать универсальный код, который не зависит от типа контейнера и может применяться к любому контейнеру?

С данной абстракцией хорошо согласуется концепция итератора (iterator). Итератор — это объект, обеспечивающий перемещение по последовательности объектов с выбором каждого объекта этой последовательности, при этом про­граммисту-клиенту не надо знать или заботиться о лежащей в ее основе структуре. Вдобавок, итератор обычно является так называемым «легковесным» (lightweight) объектом: его создание должно обходиться без заметных затрат ресурсов. Из-за этого итераторы часто имеют ограничения; например, Iterator в Java поддерживает перемещение только в одном направлении. Его возможности не так уж широки, но с его помощью можно сделать следующее:


  • Запросить у контейнера итератор вызовом метода iterator(). Полученный итератор готов вернуть начальный элемент последовательности при первом вызове своего метода next().
  • Получить следующий элемент последовательности вызовом метода next().
  • Проверить, остались ли еще объекты в последовательности (метод hasNext()).
  • Удалить из последовательности последний элемент, возвращенный итератором, методом remove().

Чтобы увидеть итератор в действии, мы снова воспользуемся иерархией Pets:

//: holding/SimpleIteration.java
import typeinfo.pets.*;
import java.util.*;
 
public class SimpleIteration {
public static void main(String[] args) {
List<Pet> pets = Pets.arrayList(12);
Iterator<Pet> it = pets.iterator();
while(it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
// Более простой способ (если подходит):
for(Pet p : pets)
System.out.print(p.id() + ":" + p + " ");
System.out.println();
// Итератор также способен удалять элементы:
it = pets.iterator();
for(int i = 0; i < 6; i++) {
it.next();
it.remove();
}
System.out.println(pets);
}
}

<spoiler text="Output:">

0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 8:Cymric 9:Rat 10:EgyptianMau 11:Hamster
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx 8:Cymric 9:Rat 10:EgyptianMau 11:Hamster
[Pug, Manx, Cymric, Rat, EgyptianMau, Hamster]

</spoiler>
Мы видим, что с Iterator можно не беспокоиться о количестве элементов в последовательности. Проверка осуществляется методами hasNext() и next().
Если вы просто перебираете элементы списка в одном направлении, не пытаясь модифицировать его содержимое, «синтаксис foreach» обеспечивает более компактную запись.
Iterator удаляет последний элемент, полученный при помощи next(), поэтому перед вызовом remove() необходимо вызвать next().
Теперь рассмотрим задачу создания метода display(), не зависящего от типа контейнера:

//: holding/CrossContainerIteration.java
import typeinfo.pets.*;
import java.util.*;
 
public class CrossContainerIteration {
public static void display(Iterator<Pet> it) {
while(it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void main(String[] args) {
ArrayList<Pet> pets = Pets.arrayList(8);
LinkedList<Pet> petsLL = new LinkedList<Pet>(pets);
HashSet<Pet> petsHS = new HashSet<Pet>(pets);
TreeSet<Pet> petsTS = new TreeSet<Pet>(pets);
display(pets.iterator());
display(petsLL.iterator());
display(petsHS.iterator());
display(petsTS.iterator());
}
}

<spoiler text="Output:">

0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
4:Pug 6:Pug 3:Mutt 1:Manx 5:Cymric 7:Manx 2:Cymric 0:Rat
5:Cymric 2:Cymric 7:Manx 1:Manx 3:Mutt 6:Pug 4:Pug 0:Rat

</spoiler>
В методе display() отсутствует информация о типе последовательности, и в этом проявляется истинная мощь итераторов: операция перемещения по последовательности отделяется от фактической структуры этой последовательности. Иногда говорят, что итераторы унифицируют доступ к контейнерам.

Listlterator

Listlterator — более мощная разновидность Iterator, поддерживаемая только классами List. Если Iterator поддерживает перемещение только вперед, ListIterator является двусторонним. Кроме того, он может выдавать индексы следующего и предыдущего элементов по отношению к текущей позиции итератора в списке и заменять последний посещенный элемент методом set(). Вызов listIterator() возвращает ListIterator, указывающий в начало List, а для создания итератора ListIterator, изначально установленного на элемент с индексом n, используется вызов listIterator(n). Все перечисленные возможности продемонстрированы в следующем примере:

//: holding/ListIteration.java
import typeinfo.pets.*;
import java.util.*;
 
public class ListIteration {
public static void main(String[] args) {
List<Pet> pets = Pets.arrayList(8);
ListIterator<Pet> it = pets.listIterator();
while(it.hasNext())
System.out.print(it.next() + ", " + it.nextIndex() +
", " + it.previousIndex() + "; ");
System.out.println();
// В обратном направлении
while(it.hasPrevious())
System.out.print(it.previous().id() + " ");
System.out.println();
System.out.println(pets);
it = pets.listIterator(3);
while(it.hasNext()) {
it.next();
it.set(Pets.randomPet());
}
System.out.println(pets);
}
}

<spoiler text="Output:">

Rat, 1, 0; Manx, 2, 1; Cymric, 3, 2; Mutt, 4, 3; Pug, 5, 4; Cymric, 6, 5; Pug, 7, 6; Manx, 8, 7;
7 6 5 4 3 2 1 0
[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx]
[Rat, Manx, Cymric, Cymric, Rat, EgyptianMau, Hamster, EgyptianMau]

</spoiler>
Метод Pets.randomPet() используется для замены всех объектов Pet в списке, начиная с позиции 3 и далее.

LinkedList

LinkedList тоже реализует базовый интерфейс List, как и ArrayList, но выполняет некоторые операции (например, вставку и удаление в середине списка) более эффективно, чем ArrayList. И наоборот, операции произвольного доступа выпол­няются им с меньшей эффективностью.
Класс LinkedList также содержит методы, позволяющие использовать его в качестве стека, очереди (Queue) или двусторонней очереди (дека).
Некоторые из этих методов являются псевдонимами или модификациями для получения имен, более знакомых в контексте некоторого использования. Например, методы getFirst() и element() идентичны — они возвращают начало (первый элемент) списка без его удаления и выдают исключение NoSuchElementException для пустого списка. Метод peek() представляет собой небольшую модификацию этих двух методов: он возвращает null для пустого списка.
Метод addFirst() вставляет элемент в начало списка. Метод offer() делает то же, что add() и addLast() — он добавляет элемент в конец списка. Метод removeLast() удаляет и возвращает последний элемент списка.
Следующий пример демонстрирует схожие и различающиеся аспекты этих методов:

//: holding/LinkedListFeatures.java
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class LinkedListFeatures {
public static void main(String[] args) {
LinkedList<Pet> pets =
new LinkedList<Pet>(Pets.arrayList(5));
print(pets);
// Идентично
print("pets.getFirst(): " + pets.getFirst());
print("pets.element(): " + pets.element());
// Различие проявляется только для пустых списков:
print("pets.peek(): " + pets.peek());
// Идентично, удаление и возврат первого элемента.
print("pets.remove(): " + pets.remove());
print("pets.removeFirst(): " + pets.removeFirst());
// Различие проявляется только для пустых списков:
print("pets.poll(): " + pets.poll());
print(pets);
pets.addFirst(new Rat());
print("After addFirst(): " + pets);
pets.offer(Pets.randomPet());
print("After offer(): " + pets);
pets.add(Pets.randomPet());
print("After add(): " + pets);
pets.addLast(new Hamster());
print("After addLast(): " + pets);
print("pets.removeLast(): " + pets.removeLast());
}
}

<spoiler text="Output:">

[Rat, Manx, Cymric, Mutt, Pug]
pets.getFirst(): Rat
pets.element(): Rat
pets.peek(): Rat
pets.remove(): Rat
pets.removeFirst(): Manx
pets.poll(): Cymric
[Mutt, Pug]
After addFirst(): [Rat, Mutt, Pug]
After offer(): [Rat, Mutt, Pug, Cymric]
After add(): [Rat, Mutt, Pug, Cymric, Pug]
After addLast(): [Rat, Mutt, Pug, Cymric, Pug, Hamster]
pets.removeLast(): Hamster

</spoiler>
Результат Pets.arrayList() передается конструктору LinkedList для заполнения. Присмотревшись к интерфейсу Queue, вы найдете в нем методы element(), offer(), peek(), poll() и remove(), добавленные в LinkedList для использования в реализации очереди (см. далее).

Стек

Стек часто называют контейнером, работающим по принципу «первым вошел, последним вышел» (LIFO). То есть элемент, последним занесенный в стек, будет первым, полученным при извлечении из стека.
В классе LinkedList имеются методы, напрямую реализующие функциональность стека, поэтому вы просто используете LinkedList, не создавая для стека новый класс. Впрочем, иногда отдельный класс для контейнера-стека лучше справляется с задачей:

//: net/mindview/util/Stack.java
// Making a stack from a LinkedList.
package net.mindview.util;
import java.util.LinkedList;
 
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void push(T v) { storage.addFirst(v); }
public T peek() { return storage.getFirst(); }
public T pop() { return storage.removeFirst(); }
public boolean empty() { return storage.isEmpty(); }
public String toString() { return storage.toString(); }
}

Это простейший пример определения класса с использованием параметризации. Суффикс <Т> после имени класса сообщает компилятору, что тип является параметризованным по типу Т — при использовании класса на место Т будет подставлен фактический тип. Фактически такое определение означает: «Мы определяем класс Stack для хранения объектов типа Т». Stack реализуется на базе LinkedList, также предназначенного для хранения типа Т. Обратите внимание: метод push() получает объект типа Т, а методы реек() и рор() возвращают объект типа Т. Метод реек() возвращает верхний элемент без извлечения из стека, а метод рор() удаляет и возвращает верхний элемент. Простой пример использования нового класса Stack:

//: holding/StackTest.java
import net.mindview.util.*;
 
public class StackTest {
public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
for(String s : "My dog has fleas".split(" "))
stack.push(s);
while(!stack.empty())
System.out.print(stack.pop() + " ");
}
}

<spoiler text="Output:">

fleas has dog My

</spoiler>
Если вы хотите использовать класс Stack в своем коде, вам придется либо полностью указать пакет, либо изменить имя класса при создании объекта; в противном случае, скорее всего, возникнет конфликт с классом Stack из пакета java.util. Пример использования имен пакетов при импортировании java.util.* в предыдущем примере:

//: holding/StackCollision.java
import net.mindview.util.*;
 
public class StackCollision {
public static void main(String[] args) {
net.mindview.util.Stack<String> stack =
new net.mindview.util.Stack<String>();
for(String s : "My dog has fleas".split(" "))
stack.push(s);
while(!stack.empty())
System.out.print(stack.pop() + " ");
System.out.println();
java.util.Stack<String> stack2 =
new java.util.Stack<String>();
for(String s : "My dog has fleas".split(" "))
stack2.push(s);
while(!stack2.empty())
System.out.print(stack2.pop() + " ");
}
}

<spoiler text="Output:">
fleas has dog My
fleas has dog My
</spoiler>
В java.util нет общего интерфейса Stack — вероятно, из-за того, что имя было задействовано в исходной, неудачно спроектированной версии java.util.Stack для Java 1.0. Хотя класс java.util.Stack существует, LinkedList обеспечивает более качественную реализацию стека, и решение net.mindview.util.Stack является предпочтительным.

Множество

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

Set имеет такой же интерфейс, что и Collection. В сущности, Set и является Collection, но обладает несколько иным поведением (кстати, идеальный пример использования наследования и полиморфизма: выражение разных концепций поведения). Пример использования HashSet с объектами Integer:

//: holding/SetOfInteger.java
import java.util.*;
 
public class SetOfInteger {
public static void main(String[] args) {
Random rand = new Random(47);
Set<Integer> intset = new HashSet<Integer>();
for(int i = 0; i < 10000; i++)
intset.add(rand.nextInt(30));
System.out.println(intset);
}
}
<spoiler text="Output:">
[15, 8, 23, 16, 7, 22, 9, 21, 6, 1, 29, 14, 24, 4, 19, 26, 11, 18, 3, 12, 27, 17, 2, 13,
28, 20, 25, 10, 5, 0]

</spoiler>
В множество включаются десять тысяч случайных чисел от 0 до 29; естественно, числа должны многократно повторяться. Но при этом мы видим, что в результатах каждое число присутствует только в одном экземпляре.

Также обратите внимание на непредсказуемый порядок следования чисел в выводе. Это объясняется тем, что HashSet использует хеширование для ускорения выборки. Порядок, поддерживаемый HashSet, отличается от порядка TreeSet или LinkedHashSet, поскольку каждая реализация упорядочивает элементы по-своему. Если вы хотите, чтобы результат был отсортирован, воспользуйтесь TreeSet вместо HashSet:

//: holding/SortedSetOfInteger.java
import java.util.*;
 
public class SortedSetOfInteger {
public static void main(String[] args) {
Random rand = new Random(47);
SortedSet<Integer> intset = new TreeSet<Integer>();
for(int i = 0; i < 10000; i++)
intset.add(rand.nextInt(30));
System.out.println(intset);
}
}

<spoiler text="Output:">

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 
23, 24, 25, 26, 27, 28, 29]

</spoiler>
Одной из наиболее распространенных операций со множествами является проверка принадлежности методом contains(), но существуют и другие операции, которые напомнят вам диаграммы Венна из школьного курса:

//: holding/SetOperations.java
import java.util.*;
import static net.mindview.util.Print.*;
 
public class SetOperations {
public static void main(String[] args) {
Set<String> set1 = new HashSet<String>();
Collections.addAll(set1,
"A B C D E F G H I J K L".split(" "));
set1.add("M");
print("H: " + set1.contains("H"));
print("N: " + set1.contains("N"));
Set<String> set2 = new HashSet<String>();
Collections.addAll(set2, "H I J K L".split(" "));
print("set2 in set1: " + set1.containsAll(set2));
set1.remove("H");
print("set1: " + set1);
print("set2 in set1: " + set1.containsAll(set2));
set1.removeAll(set2);
print("set2 removed from set1: " + set1);
Collections.addAll(set1, "X Y Z".split(" "));
print("'X Y Z' added to set1: " + set1);
}
}

<spoiler text="Output:">

H: true
N: false
set2 in set1: true
set1: [D, K, C, B, L, G, I, M, A, F, J, E]
set2 in set1: false
set2 removed from set1: [D, C, B, G, M, A, F, E]
'X Y Z' added to set1: [Z, D, C, B, G, M, A, F, Y, X, E]

</spoiler>
Имена методов говорят за себя. Информацию о других методах Set можно найти в документации JDK.

Карта

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

//: holding/Statistics.java
// Простой пример использования HashMap
import java.util.*;
 
public class Statistics {
public static void main(String[] args) {
Random rand = new Random(47);
Map<Integer,Integer> m =
new HashMap<Integer,Integer>();
for(int i = 0; i < 10000; i++) {
// Получение случайного числа от 0 до 20.
int r = rand.nextInt(20);
Integer freq = m.get(r);
m.put(r, freq == null ? 1 : freq + 1);
}
System.out.println(m);
}
}

<spoiler text="Output:">

{15=497. 4=481. 19=464. 8=468. 11=531. 16=533. 18=478. 3=508, 7=471. 12=521. 17=509. 2=489,  
13=506, 9=549, 6=519, 1=502, 14=477, 10=513, 5=503, 0=481}

</spoiler>
В main() механизм автоматической упаковки преобразует случайно сгенерированое целое число в ссылку на Integer, которая может использоваться с HashMap (контейнеры не могут использоваться для хранения примитивов).

Метод get() возвращает null, если элемент отсутствует в контейнере (то есть если число было сгенерировано впервые. В противном случае метод get() возвращает значение Integer, связанное с ключом, и последнее увеличивается на 1 (автоматическая упаковка снова упрощает вычисления, но в действительности при этом выполняются преобразования к Integer и обратно).

Следующий пример демонстрирует поиск объектов Pet по строковому описанию String. Он также показывает, как проверить присутствие некоторого ключа или значения в Map методами containsKey() и containsValue():

//: holding/PetMap.java
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class PetMap {
public static void main(String[] args) {
Map<String,Pet> petMap = new HashMap<String,Pet>();
petMap.put("My Cat", new Cat("Molly"));
petMap.put("My Dog", new Dog("Ginger"));
petMap.put("My Hamster", new Hamster("Bosco"));
print(petMap);
Pet dog = petMap.get("My Dog");
print(dog);
print(petMap.containsKey("My Dog"));
print(petMap.containsValue(dog));
}
}

<spoiler text="Output:">

{My Cat=Cat Molly, My Hamster=Hamster Bosco, My Dog=Dog Ginger}
Dog Ginger
true
true

</spoiler>
Map, по аналогии с массивами и Collection, легко расширяются до нескольких измерений; достаточно создать Map со значениями типа Map (причем значениями этих Map могут быть другие контейнеры, и даже другие Map). Контейнеры легко комбинируются друг с другом, что позволяет быстро создавать сложные структуры данных. Например, если нам потребуется сохранить информацию о владельцах сразу нескольких домашних животных, для этого будет достаточно создать контейнер Map<Person,List<Pet>>:

//: holding/MapOfList.java
package holding;
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class MapOfList {
public static Map<Person, List<? extends Pet>>
petPeople = new HashMap<Person, List<? extends Pet>>();
static {
petPeople.put(new Person("Dawn"),
Arrays.asList(new Cymric("Molly"),new Mutt("Spot")));
petPeople.put(new Person("Kate"),
Arrays.asList(new Cat("Shackleton"),
new Cat("Elsie May"), new Dog("Margrett")));
petPeople.put(new Person("Marilyn"),
Arrays.asList(
new Pug("Louie aka Louis Snorkelstein Dupree"),
new Cat("Stanford aka Stinky el Negro"),
new Cat("Pinkola")));
petPeople.put(new Person("Luke"),
Arrays.asList(new Rat("Fuzzy"), new Rat("Fizzy")));
petPeople.put(new Person("Isaac"),
Arrays.asList(new Rat("Freckly")));
}
public static void main(String[] args) {
print("People: " + petPeople.keySet());
print("Pets: " + petPeople.values());
for(Person person : petPeople.keySet()) {
print(person + " has:");
for(Pet pet : petPeople.get(person))
print(" " + pet);
}
}
}

<spoiler text="Output:">

People: [Person Luke, Person Marilyn, Person Isaac, Person Dawn, Person Kate]
Pets: [[Rat Fuzzy, Rat Fizzy], [Pug Louie aka Louis Snorkelstein Dupree, Cat Stanford aka Stinky
el Negro, Cat Pinkola], [Rat Freckly], [Cymric Molly, Mutt Spot], [Cat Shackleton, Cat Elsie May,
Dog Margrett]]
Person Luke has:
Rat Fuzzy
Rat Fizzy
Person Marilyn has:
Pug Louie aka Louis Snorkelstein Dupree
Cat Stanford aka Stinky el Negro
Cat Pinkola
Person Isaac has:
Rat Freckly
Person Dawn has:
Cymric Molly
Mutt Spot
Person Kate has:
Cat Shackleton
Cat Elsie May
Dog Margrett

</spoiler>
Map может вернуть множество (Set) своих ключей, коллекцию (Collection) значений или множество (Set) всех пар «ключ-значение». Метод keySet() создает множество всех ключей, которое затем используется в синтаксисе foreach для перебора Map.

Очередь

Очередь обычно представляет собой контейнер, работающий по принципу «первым вошел, первым вышел» (FIFO). Иначе говоря, элементы заносятся в очередь с одного «конца» и извлекаются с другого в порядке их поступления. Очереди часто применяются для реализации надежной передачи объектов между разными областями программы.
Класс LinkedList содержит методы, поддерживающие поведение очереди, и реализует интерфейс Queue, поэтому LinkedList может использоваться в качестве реализации Queue. В следующем примере LinkedList повышается восходящим преобразованием до Queue:

//: holding/QueueDemo.java
// Восходящее преобразование LinkedList в Queue
import java.util.*;
 
public class QueueDemo {
public static void printQ(Queue queue) {
while(queue.peek() != null)
System.out.print(queue.remove() + " ");
System.out.println();
}
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
Random rand = new Random(47);
for(int i = 0; i < 10; i++)
queue.offer(rand.nextInt(i + 10));
printQ(queue);
Queue<Character> qc = new LinkedList<Character>();
for(char c : "Brontosaurus".toCharArray())
qc.offer(c);
printQ(qc);
}
}

<spoiler text="Output:">

8 1 1 1 5 14 3 1 0 1
B r o n t o s a u r u s

</spoiler>
Метод offer(), один из методов Queue, вставляет элемент в конец очереди, а если вставка невозможна — возвращает false. Методы реек() и element() возвращают начальный элемент без его удаления из очереди, но реек() для пустой очереди возвращает null, a element() выдает исключение NoSuchElementException. Методы poll() и remove() удаляют и возвращают начальный элемент очереди, но poll() для пустой очереди возвращает null, a remove() выдает NoSuchElementException.
Автоматическая упаковка преобразует результат int вызова nextInt() в объект Integer, необходимый для queue, a char с — в объект Character, необходимый для qc. Интерфейс Queue сужает доступ к методам LinkedList так, что доступными остаются только соответствующие методы и у пользователя остается меньше возможностей для вызова методов LinkedList (конечно, queue можно преобразовать обратно в LinkedList, но это создает дополнительные затруднения).

PriorityQueue

Принцип FIFO описывает наиболее типичную организацию очереди. Именно организация очереди определяет, какой элемент будет следующим для заданного состояния очереди. Правило FIFO означает, что следующим элементом будет тот, который дольше всего находится в очереди.

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

При помещении объекта в PriorityQueue вызовом offer() объект сортируется в очереди. По умолчанию используется естественный порядок помещения объектов в очередь, однако вы можете изменить его, предоставив собственную реа­лизацию Comparator. PriorityQueue гарантирует, что при вызове peek(), poll() или remove() вы получите элемент с наивысшим приоритетом.

Создание приоритетной очереди для встроенных типов — Integer, String, Character и т. д. — является делом тривиальным. В следующем примере используются те же значения, что и в предыдущем, но PriorityQueue выдает их в другом порядке:

//: holding/PriorityQueueDemo.java
import java.util.*;
 
public class PriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<Integer>();
Random rand = new Random(47);
for(int i = 0; i < 10; i++)
priorityQueue.offer(rand.nextInt(i + 10));
QueueDemo.printQ(priorityQueue);
 
List<Integer> ints = Arrays.asList(25, 22, 20,
18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25);
priorityQueue = new PriorityQueue<Integer>(ints);
QueueDemo.printQ(priorityQueue);
priorityQueue = new PriorityQueue<Integer>(
ints.size(), Collections.reverseOrder());
priorityQueue.addAll(ints);
QueueDemo.printQ(priorityQueue);
 
String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION";
List<String> strings = Arrays.asList(fact.split(""));
PriorityQueue<String> stringPQ =
new PriorityQueue<String>(strings);
QueueDemo.printQ(stringPQ);
stringPQ = new PriorityQueue<String>(
strings.size(), Collections.reverseOrder());
stringPQ.addAll(strings);
QueueDemo.printQ(stringPQ);
 
Set<Character> charSet = new HashSet<Character>();
for(char c : fact.toCharArray())
charSet.add(c); // Autoboxing
PriorityQueue<Character> characterPQ =
new PriorityQueue<Character>(charSet);
QueueDemo.printQ(characterPQ);
}
}

<spoiler text="Output:">

0 1 1 1 1 1 3 5 8 14
1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25
25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1
A A B C C C D D E E E F H H I I L N N O O O O S S S T T U U U W
W U U U T T S S S O O O O N N L I I H H F E E E D D C C C B A A
A B C D E F H I L N O S T U W

</spoiler>
Мы видим, что дубликаты разрешены, а меньшие значения обладают более высокими приоритетами. Чтобы показать, как изменить порядок элементов посредством передачи собственного объекта Comparator, при третьем вызове кон­структора PriorityQueue<Integer> и втором — PriorityQueue<String> используется
Comparator с обратной сортировкой, полученный вызовом Collections.reverseOrder() (одно из новшеств Java SE5).
В последней части добавляется HashSet для уничтожения дубликатов Character — просто для того, чтобы пример был чуть более интересным.
Integer, String и Character изначально работают с PriorityQueue, потому что они обладают «встроенным» естественным упорядочением. Если вы хотите использовать собственный класс с PriorityQueue, включите дополнительную реализацию естественного упорядочения или предоставьте собственный объект Comparator.

Collection и Iterator

Collection — корневой интерфейс, описывающий общую функциональность всех последовательных контейнеров. Его можно рассматривать как «вторичный интерфейс», появившийся вследствие сходства между другими интерфейсами. Кроме того, класс java.util.AbstractCollection предоставляет реализацию Collection по умолчанию, поэтому вы можете создать новый подтип AbstractCollection без избыточного дублирования кода.

Один из доводов в пользу интерфейсов заключается в том, что они позволяют создавать более универсальный код. Код, написанный для интерфейса, а не для его реализации, может быть применен к более широкому кругу объектов. Таким образом, если я пишу метод, которому при вызове передается Collection, этот метод будет работать с любым типом, реализующим Collection, — следовательно, если новый класс реализует Collection, он будет совместим с моим методом. Однако интересно заметить, что стандартная библиотека C++ не имеет общего базового класса для своих контейнеров — вся общность контейнеров обеспечивается итераторами. Казалось бы, в Java будет логично последовать примеру C++ и выражать сходство между контейнерами при помощи итераторов, а не Collection. Тем не менее эти два подхода взаимосвязаны, потому что реализация Collection также означает поддержку метода iterator():

//: holding/InterfaceVsIterator.java
import typeinfo.pets.*;
import java.util.*;
 
public class InterfaceVsIterator {
public static void display(Iterator<Pet> it) {
while(it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void display(Collection<Pet> pets) {
for(Pet p : pets)
System.out.print(p.id() + ":" + p + " ");
System.out.println();
}
public static void main(String[] args) {
List<Pet> petList = Pets.arrayList(8);
Set<Pet> petSet = new HashSet<Pet>(petList);
Map<String,Pet> petMap =
new LinkedHashMap<String,Pet>();
String[] names = ("Ralph, Eric, Robin, Lacey, " +
"Britney, Sam, Spot, Fluffy").split(", ");
for(int i = 0; i < names.length; i++)
petMap.put(names[i], petList.get(i));
display(petList);
display(petSet);
display(petList.iterator());
display(petSet.iterator());
System.out.println(petMap);
System.out.println(petMap.keySet());
display(petMap.values());
display(petMap.values().iterator());
}
}

<spoiler text="Output:">

0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
4:Pug 6:Pug 3:Mutt 1:Manx 5:Cymric 7:Manx 2:Cymric 0:Rat
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
4:Pug 6:Pug 3:Mutt 1:Manx 5:Cymric 7:Manx 2:Cymric 0:Rat
{Ralph=Rat, Eric=Manx, Robin=Cymric, Lacey=Mutt, Britney=Pug, Sam=Cymric, Spot=Pug, Fluffy=Manx}
[Ralph, Eric, Robin, Lacey, Britney, Sam, Spot, Fluffy]
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx

</spoiler>
Обе версии display() работают как с объектами Map, так и с подтипами Collection; при этом как Collection, так и Iterator изолируют методы display() от знания конкретной реализации используемого контейнера.
В данном случае два решения примерно равноценны. Использование Iterator становится предпочтительным при реализации постороннего класса, для которого реализация интерфейса Collection затруднена или нежелательна. Например, если мы создаем реализацию Collection наследованием от класса, содержащего объекты Pet, нам придется реализовать все методы Collection, даже если они не будут использоваться в методе display(). Хотя проблема легко решается наследованием от AbstractCollection, вам все равно придется реализовать iterator() вместе с size(), чтобы предоставить методы, не реализованные AbstractCollection, но используемые другими методами AbstractCollection:

//: holding/CollectionSequence.java
import typeinfo.pets.*;
import java.util.*;
 
public class CollectionSequence
extends AbstractCollection<Pet> {
private Pet[] pets = Pets.createArray(8);
public int size() { return pets.length; }
public Iterator<Pet> iterator() {
return new Iterator<Pet>() {
private int index = 0;
public boolean hasNext() {
return index < pets.length;
}
public Pet next() { return pets[index++]; }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
CollectionSequence c = new CollectionSequence();
InterfaceVsIterator.display(c);
InterfaceVsIterator.display(c.iterator());
}
}

<spoiler text="Output:">

0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx
0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx

</spoiler>
Метод remove() является необязательной операцией. В нашем примере реализовывать его не нужно, и в случае вызова он выдает исключение.
Из приведенного примера видно, что при реализации Collection вы также реализуете iterator(), а простая отдельная реализация iterator() требует чуть меньших усилий, чем наследование от AbstractCollection. Но, если класс уже наследует от другого класса, наследование еще и от AbstractCollection невозможно. В этом случае для реализации Collection придется реализовать все методы интерфейса, и тогда гораздо проще ограничиться наследованием и добавить возможность создания итератора:

//: holding/NonCollectionSequence.java
import typeinfo.pets.*;
import java.util.*;
 
class PetSequence {
protected Pet[] pets = Pets.createArray(8);
}
 
public class NonCollectionSequence extends PetSequence {
public Iterator<Pet> iterator() {
return new Iterator<Pet>() {
private int index = 0;
public boolean hasNext() {
return index < pets.length;
}
public Pet next() { return pets[index++]; }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
NonCollectionSequence nc = new NonCollectionSequence();
InterfaceVsIterator.display(nc.iterator());
}
}

<spoiler text="Output:">

0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx

</spoiler>
Создание Iterator обеспечивает минимальную логическую привязку между последовательностью и методом, использующим эту последовательность, а также налагает гораздо меньше ограничений на класс последовательности, реализующий Collection.

Синтаксис foreach и итераторы

До настоящего момента «синтаксис foreach» использовался в основном с массивами, но он также будет работать с любым объектом Collection. Некоторые примеры уже встречались нам при работе с ArrayList, но можно привести и более об­щее подтверждение:

//: holding/ForEachCollections.java
// Синтаксис foreach работает с любыми коллекциями
import java.util.*;
 
public class ForEachCollections {
public static void main(String[] args) {
Collection<String> cs = new LinkedList<String>();
Collections.addAll(cs,
"Take the long way home".split(" "));
for(String s : cs)
System.out.print("'" + s + "' ");
}
}

<spoiler text="Output:">

'Take' 'the' 'long' 'way' 'home' 

</spoiler>
Поскольку cs является Collection, этот пример показывает, что поддержка foreach является характеристикой всех объектов Collection.
Работа этой конструкции объясняется тем, что в Java SE5 появился новый интерфейс Iterable, который содержит метод iterator() для создания Iterator, и именно интерфейс Iterable используется при переборе последовательности в синтаксисе foreach. Следовательно, создав любой класс, реализующий Iterable, вы сможете использовать его в синтаксисе foreach:

//: holding/IterableClass.java
// Любой Iterable работает с foreach.
import java.util.*;
 
public class IterableClass implements Iterable<String> {
protected String[] words = ("And that is how " +
"we know the Earth to be banana-shaped.").split(" ");
public Iterator<String> iterator() {
return new Iterator<String>() {
private int index = 0;
public boolean hasNext() {
return index < words.length;
}
public String next() { return words[index++]; }
public void remove() { // Не реализован
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(String s : new IterableClass())
System.out.print(s + " ");
}
}

<spoiler text="Output:">

And that is how we know the Earth to be banana-shaped.

</spoiler>
Метод iterator() возвращает экземпляр анонимной внутренней реализации Iterator<String>, последовательно доставляющей каждое слово в массиве. В main() мы видим, что IterableClass действительно работает в синтаксисе foreach.
В Java SE5 многие классы реализуют Iterable, прежде всего все классы Collection (но не Map). Например, следующий код выводит все переменные окружения (environment) операционной системы:

//: holding/EnvironmentVariables.java
import java.util.*;
 
public class EnvironmentVariables {
public static void main(String[] args) {
for(Map.Entry entry: System.getenv().entrySet()) {
System.out.println(entry.getKey() + ": " +
entry.getValue());
}
}
} /* (Execute to see output) */

System.getenv() возвращает Map, entrySet() создает Set с элементами Map.Entry, a Set поддерживает Iterable и поэтому может использоваться в цикле foreach.
Синтаксис foreach работает с массивами и всем, что поддерживает Iterable, но это не означает, что массив автоматически поддерживает Iterable:

//: holding/ArrayIsNotIterable.java
import java.util.*;
 
public class ArrayIsNotIterable {
static <T> void test(Iterable<T> ib) {
for(T t : ib)
System.out.print(t + " ");
}
public static void main(String[] args) {
test(Arrays.asList(1, 2, 3));
String[] strings = { "A", "B", "C" };
// Массив работает в foreach. но не является Iterable:
//! test(strings);
// его необходимо явно преобразовать к Iterable:
test(Arrays.asList(strings));
}
}

<spoiler text="Output:">

1 2 3 A B C

</spoiler>
Попытка передачи массива в аргументе Iterable завершается неудачей. Автоматическое преобразование в Iterable не производится; его необходимо выполнять вручную.

Идиома «метод-адаптер»

Что делать, если у вас имеется существующий класс, реализующий Iterable, и вы хотите добавить новые способы использования этого класса в синтаксисе foreach? Допустим, вы хотите иметь возможность выбора между перебором списка слов в прямом или обратном направлении. Если просто воспользоваться наследованием от класса и переопределить метод iterator, то существующий метод будет заменен и никакого выбора не будет.
Одно из решений этой проблемы основано на использовании идиомы, которую я называю «методом-адаптером». Термин «адаптер» происходит от одноименного паттерна: вы должны предоставить интерфейс, необходимый для работы синтаксиса foreach. Если у вас имеется один интерфейс, а нужен другой, проблема решается написанием адаптера. В данном случае требуется добавить к стандартному «прямому» итератору обратный, так что переопределение исключено. Вместо этого мы добавим метод, создающий объект Iterable, который может использоваться в синтаксисе foreach. Как будет показано далее, это позволит нам предоставить несколько вариантов использования foreach:

//: holding/AdapterMethodIdiom.java
// Идиома "метод-адаптер" позволяет использовать foreach
// с дополнительными разновидностями Iterable.
import java.util.*;
 
class ReversibleArrayList<T> extends ArrayList<T> {
public ReversibleArrayList(Collection<T> c) { super(c); }
public Iterable<T> reversed() {
return new Iterable<T>() {
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size() - 1;
public boolean hasNext() { return current > -1; }
public T next() { return get(current--); }
public void remove() { // // He реализован
throw new UnsupportedOperationException();
}
};
}
};
}
}
 
public class AdapterMethodIdiom {
public static void main(String[] args) {
ReversibleArrayList<String> ral =
new ReversibleArrayList<String>(
Arrays.asList("To be or not to be".split(" ")));
// Получаем обычный итератор, полученный при помощи iterator():
for(String s : ral)
System.out.print(s + " ");
System.out.println();
// Передаем выбранный нами Iterable
for(String s : ral.reversed())
System.out.print(s + " ");
}
}

<spoiler text="Output:">

To be or not to be 
be to not or be To

</spoiler>
Если просто поместить объект ral в синтаксис foreach, мы получим (стандартный) «прямой» итератор. Но если вызвать для объекта reversed(), поведение изменится.
Использовав этот прием, можно добавить в пример IterableClass.java два метода-адаптера:

//: holding/MultiIterableClass.java
// Adding several Adapter Methods.
import java.util.*;
 
public class MultiIterableClass extends IterableClass {
public Iterable<String> reversed() {
return new Iterable<String>() {
public Iterator<String> iterator() {
return new Iterator<String>() {
int current = words.length - 1;
public boolean hasNext() { return current > -1; }
public String next() { return words[current--]; }
public void remove() { He реализован
throw new UnsupportedOperationException();
}
};
}
};
}
public Iterable<String> randomized() {
return new Iterable<String>() {
public Iterator<String> iterator() {
List<String> shuffled =
new ArrayList<String>(Arrays.asList(words));
Collections.shuffle(shuffled, new Random(47));
return shuffled.iterator();
}
};
}
public static void main(String[] args) {
MultiIterableClass mic = new MultiIterableClass();
for(String s : mic.reversed())
System.out.print(s + " ");
System.out.println();
for(String s : mic.randomized())
System.out.print(s + " ");
System.out.println();
for(String s : mic)
System.out.print(s + " ");
}
}

<spoiler text="Output:">

banana-shaped. be to Earth the know we how is that And
is banana-shaped. Earth that how the be And we know to
And that is how we know the Earth to be banana-shaped.

</spoiler>
Из выходных данных видно, что метод Collections.shuffle не изменяет исходный массив, а только переставляет ссылки в shuffled. Так происходит только потому, что метод randomized() создает для результата Arrays.asList() «обертку» в виде ArrayList. Если бы операция выполнялась непосредственно с объектом List, полученным от Arrays.asList(), то это привело бы к изменению нижележащего массива:

//: holding/ModifyingArraysAsList.java
import java.util.*;
 
public class ModifyingArraysAsList {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] ia = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<Integer> list1 =
new ArrayList<Integer>(Arrays.asList(ia));
System.out.println("До перестановки : " + list1);
Collections.shuffle(list1, rand);
System.out.println("После перестановки : " + list1);
System.out.println("Массив: " + Arrays.toString(ia));
 
List<Integer> list2 = Arrays.asList(ia);
System.out.println("До перестановки : " + list2);
Collections.shuffle(list2, rand);
System.out.println("После перестановки : " + list2);
System.out.println("Массив: " + Arrays.toString(ia));
}
}

<spoiler text="Output:">

До перестановки : [1, 2, 3. 4, 5. 6. 7. 8, 9, 10] 
После перестановки : [4. 6, 3, 1. 8. 7, 2, 5. 10. 9]
Массив: [1, 2. 3. 4. 5. 6. 7, 8. 9. 10]
До перестановки : [1, 2. 3, 4, 5. 6. 7. 8, 9, 10]
После перестановки : [9. 1. 6. 3. 7, 2. 5, 10, 4. 8]
Массив: [9. 1, 6. 3. 7, 2, 5. 10. 4. 8]

</spoiler>
В первом случае вывод Arrays.asList() передается конструктору ArrayList(), а последний создает объект ArrayList, ссылающийся на элементы ia. Перестановка этих ссылок не изменяет массива. Но, если мы используем результат Arrays.asList(ia) напрямую, перестановка изменит порядок ia. Важно учитывать, что Arrays.asList() создает объект List, который использует нижележащий массив в качестве своей физической реализации. Если с этим объектом List выполняются какие-либо изменяющие операции, но вы не хотите изменения исходного массива, создайте копию в другом контейнере.

Резюме

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

  • В массивах объектам назначаются числовые индексы. Массив содержит объекты заранее известного типа, поэтому преобразование типа при выборке объекта не требуется. Массив может быть многомерным и может использоваться для хранения примитивных типов. Тем не менее изменить размер созданного массива невозможно.
  • В Collection хранятся отдельные элементы, а в Map — пары ассоциированных элементов. Механизм параметризации позволяет задать тип объектов, хранимых в контейнере, поэтому поместить в контейнер объект неверного типа невозможно, и элементы не нуждаются в преобразовании типа при выборке. И Collection, и Map автоматически изменяются в размерах при добавлении новых элементов. В контейнерах не могут храниться примитивы, но механизм автоматической упаковки автоматически создает объектные «обертки», сохраняемые в контейнере.
  • В контейнере List, как и в массиве, объектам назначаются числовые индексы — таким образом, массивы и List являются упорядоченными контейнерами.
  • Используйте ArrayList при частом использовании произвольного доступа к элементам или LinkedList при частом выполнении операций вставки и удаления в середине списка.
  • Поведение очередей и стеков обеспечивается контейнером LinkedList.
  • Контейнер Map связывает с объектом не целочисленный индекс, а другой объект. Контейнеры HashMap оптимизированы для быстрого доступа, а контейнер TreeMap хранит ключи в отсортированном порядке, но уступает по скорости HashMap. В контейнере LinkedHashMap элементы хранятся в порядке вставки, но хеширование обеспечивает быстрый доступ.
  • В контейнере Set каждый объект может храниться только в одном экземпляре. Контейнер HashSet обеспечивает максимальную скорость поиска, а в TreeSet элементы хранятся в отсортированном порядке. В контейнере LinkedHashSet элементы хранятся в порядке вставки.
  • Использовать старые классы Vector, Hashtable и Stack в новом коде не нужно.

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

]]>
Книги по Java https://linexp.ru?id=4742 Wed, 29 Jun 2022 14:25:49 GMT
<![CDATA[Глава 12 Thinking in Java 4th edition]]> ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЙОдин из основополагающих принципов философии Java состоит в том, что «плохо написанная программа не должна запускаться.

ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЙ

Один из основополагающих принципов философии Java состоит в том, что «плохо написанная программа не должна запускаться

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

Source(s): Java 4th edition

Основные исключения

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

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

При возбуждении исключения происходит сразу несколько вещей. Во-первых, создается объект, представляющий исключение, — точно так же, как и любой другой объект в Java (в куче, оператором new). Далее текущий поток исполнения (тот самый, где произошла ошибка) останавливается, и ссылка на объект, представляющий исключение, извлекается из текущего контекста. С этого момента включается механизм обработки исключений, который начинает поиск подходящего места программы для передачи исключения. Таким местом является обработчик исключений, который пытается решить возникшую проблему так, чтобы программа могла снова попытаться выполнить проблемную операцию или просто продолжила свое выполнение.

В качестве простого примера выдачи исключения представьте ссылку на объект t. Возможно, полученная вами ссылка не была инициализирована; стоит проверить это обстоятельство, прежде чем вызывать методы с использованием этой ссылки. Чтобы передать информацию об ошибке на более высокий уровень, создайте объект, представляющий передаваемую информацию, и «запустите» его из текущего контекста. Тем самым вы возбудите исключение. Вот как это выглядит:

 if(t == null) throw new NullPointerException( );

Вырабатывается исключение, которое позволяет вам — в текущем контексте — переложить с себя ответственность, не задумываясь о будущем. Ошибка, словно по волшебству, обрабатывается где-то в другом месте (вскоре мы узнаем, где именно).

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

Source(s): Java 4th edition

Аргументы исключения

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

 throw new NullPointerException("t = null");

Переданная строка потом может быть извлечена различными способами, о чем будет рассказано позже.Ключевое слово throw влечет за собой ряд довольно интересных действий. Как правило, сначала new используется для создания объекта, представляющего условие происшедшей ошибки. Ссылка на указанный объект передается команде throw. Фактически этот объект «возвращается» методом, несмотря на то что для возвращаемого объекта обычно предусмотрен совсем другой тип. Таким образом, упрощенно можно говорить об обработке исключений как об альтернативном механизме возврата из исполняемого метода (впрочем, с этой аналогией не стоит заходить слишком далеко). Возбуждение исключений также позволяет выходить из простых блоков видимости. В обоих случаях возвращается объект исключения и происходит выход из текущего метода или блока.
Но все сходство с обычным возвратом из метода на этом заканчивается, поскольку при возврате из исключения вы попадаете совсем не туда, куда попали бы при нормальном вызове метода. (Обработчик исключения может находиться очень «далеко» — на расстоянии нескольких уровней в стеке вызова — от метода, где возникла исключительная ситуация.)

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

Source(s): Java 4th edition

Перехват исключений

Чтобы увидеть, как перехватываются ошибки, сначала следует усвоить понятие защищенной секции — той части программы, в которой могут произойти исключения и за которой следует специальный блок, отвечающий за обработку этих исключений.

Блок try

Если вы «находитесь» внутри метода и инициируете исключение (или это делает другой вызванный метод), этот метод завершит работу при возникновении исключения. Но если вы не хотите, чтобы оператор throw завершил работу метода, разместите в методе специальный блок для перехвата исключения — так называемый блок try. Этот блок представляет собой простую область действия, которой предшествует ключевое слово

try:
try {
// Фрагмент, способный возбуждать исключения
}

Если бы не обработка исключений, для тщательной проверки ошибок вам пришлось бы добавить к вызову каждого метода дополнительный код для проверки ошибок — даже при многократном вызове одного метода. С обработкой исключений весь код размещается в блоке try, который и перехватывает все возможные исключения в одном месте. А это означает, что вашу программу становится значительно легче писать и читать, поскольку выполняемая задача не сме­шивается с обработкой ошибок.

Source(s): Java 4th edition

Обработчики исключений

Конечно, возбужденное исключение в конечном итоге должно быть где-то обработано. Этим местом является обработчик исключений, который создается для каждого исключения, которое вы хотите перехватить. Обработчики исключений размещаются прямо за блоком try и обозначаются ключевым словом catch:

 try {
// Часть программы, способная возбуждать исключения
} catch(Typel idl) {
// Обработка исключения Typel
} catch(Туре2 id2) {
// Обработка исключения Type2
} catch(ТуреЗ id3) {
// Обработка исключения ТуреЗ
} //И Т.д.

Каждое предложение catch (обработчик исключения) напоминает маленький метод, принимающий один и только один аргумент определенного типа. Идентификатор (idl, id2 и т. д.) может использоваться внутри обработчика точно так же, как и метод распоряжается своими аргументами. Иногда этот идентификатор остается невостребованным, так как тип исключения дает достаточно информации для его обработки, но тем не менее присутствует он всегда.
Обработчики всегда следуют прямо за блоком try. При возникновении исключения механизм обработки исключений ищет первый из обработчиков исключений, аргумент которого соответствует текущему типу исключения. После этого он передает управление в блок catch, и таким образом исключение считается обработанным. После выполнения предложения catch поиск обработчиков исключения прекращается. Выполняется только одна секция catch, соответствующая типу исключения; в этом отношении обработка исключений отличается от команды switch, где нужно дописывать break после каждого case, чтобы предотвратить исполнение всех прочих case.
Заметьте также, что внутри блока try могут вызываться различные методы, способные породить одинаковые типы исключения, но обработчик понадобится всего один.

Прерывание в сравнении с возобновлением

В теории обработки исключений имеется две основные модели. Модель прерывания (которое используется в Java и C++) предполагает, что ошибка настолько серьезна, что при возникновении исключения продолжить исполнение невозможно. Кто бы ни возбудил исключение, сам факт его выдачи означает, что исправить ситуацию «на месте» невозможно и возвращать управление обратно не нужно.
Альтернативная модель называется возобновлением. Она подразумевает, что обработчик ошибок сделает что-то для исправления ситуации, после чего предпринимается попытка повторить неудавшуюся операцию в надежде на успешный исход. В таком случае исключение больше напоминает вызов метода — чтобы применить модель возобновления в Java, вам придется пойти именно по этому пути (то есть не возбуждать исключение, а вызвать метод, способный решить проблему). Также можно создать блок try внутри цикла while, который станет снова и снова обращаться к этому блоку, пока не будет достигнут нужный результат.
Исторически сложилось, что программисты, использующие операционные системы с поддержкой возобновления, со временем переходили к модели прерывания, забывая другую модель. Хотя идея возобновления выглядит привлекательно, она не настолько полезна на практике. Основная причина кроется в обратной связи: обработчик ошибки часто должен знать, где произошло исключение и содержать специальный код для каждого отдельного места ошибки. А это усложняет написание и поддержку программ, особенно для больших систем, где исключения могут быть сгенерированы во многих различных местах.

Создание собственных исключений

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

//: exceptions/InheritingExceptions.java
// Создание собственного исключения
 
class SimpleException extends Exception {}
 
public class InheritingExceptions {
public void f() throws SimpleException {
System.out.println("Возбуждаем SimpleException из f()");
throw new SimpleException();
}
public static void main(String[] args) {
InheritingExceptions sed = new InheritingExceptions();
try {
sed.f();
} catch(SimpleException e) {
System.out.println("Перехвачено!");
}
}
}

<spoiler text="Output:">

Возбуждаем SimpleExceptі on из f() 
Перехвачено!

</spoiler>
Компилятор создает конструктор по умолчанию, который автоматически вызывает конструктор базового класса. Конечно, в этом случае вы лишаетесь конструктора вида SimpleException(String), но на практике он не слишком часто используется. Как вы еще увидите, наиболее важно в исключении именно имя класса, так что в основном исключений, похожих на созданное выше, будет достаточно.
В примере результаты работы выводятся на консоль. Впрочем, их также можно направить в стандартный поток ошибок, что достигается использованием класса System.err. Обычно это правильнее, чем выводить в поток System.out, который может быть перенаправлен. При выводе результатов с помощью System.err пользователь заметит их скорее, чем при выводе в System.out.
Также можно создать класс исключения с конструктором, получающим аргумент String:

//: exceptions/FullConstructors.java
 
class MyException extends Exception {
public MyException() {}
public MyException(String msg) { super(msg); }
}
 
public class FullConstructors {
public static void f() throws MyException {
System.out.println("Возбуждаем MyException изf()");
throw new MyException();
}
public static void g() throws MyException {
System.out.println("Возбуждаем MyException из g()");
throw new MyException("Создано в g()");
}
public static void main(String[] args) {
try {
f();
} catch(MyException e) {
e.printStackTrace(System.out);
}
try {
g();
} catch(MyException e) {
e.printStackTrace(System.out);
}
}
}

<spoiler text="Output:">

Возбуждаем MyException из f()                                                                            
MyException
at Ful1 Constructors.f(Ful1 Constructors.java:11)
at Full Constructors main(FullConstructors.java:19)
Возбуждаем MyException из g()
MyException: Создано в g()
at Full Constructors g(Ful1Constructors.java:15)
at FullConstructors.main(FullConstructors.java 24)

</spoiler>
Изменения незначительны — появилось два конструктора, определяющие способ создания объекта MyException. Во втором конструкторе используется конструктор родительского класса с аргументом String, вызываемый ключевым словом super.
В обработчике исключений вызывается метод printStackTrace() класса Throwable (базового для Exception). Этот метод выводит информацию о последовательности вызовов, которая привела к точке возникновения исключения. В нашем примере информация направляется в System.out, но вызов по умолчанию направляет информацию в стандартный поток ошибок:

 e.printStackTrace();

Регистрация исключений

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

//: exceptions/LoggingExceptions.java
// Регистрация исключений с использованием Logger
import java.util.logging.*;
import java.io.*;
 
class LoggingException extends Exception {
private static Logger logger =
Logger.getLogger("LoggingException");
public LoggingException() {
StringWriter trace = new StringWriter();
printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
}
 
public class LoggingExceptions {
public static void main(String[] args) {
try {
throw new LoggingException();
} catch(LoggingException e) {
System.err.println("Caught " + e);
}
try {
throw new LoggingException();
} catch(LoggingException e) {
System.err.println("Caught " + e);
}
}
}

<spoiler text="Output:"> (85% match)

Aug 30, 2005 4:02:31 PM LoggingException <init>
SEVERE: LoggingException
at LoggingExceptions.main(LoggingExceptions.java:19)

Caught LoggingException
Aug 30, 2005 4:02:31 PM LoggingException <init>
SEVERE: LoggingException
at LoggingExceptions.main(LoggingExceptions.java:24)

Caught LoggingException
</spoiler>
Статический метод Logger.getLogger() создает объект Logger, ассоциируемый с аргументом String (обычно имя пакета и класса, к которому относятся ошибки); объект передает свой вывод в System.err. Простейший способ записи ин­формации в Logger заключается в вызове метода, соответствующего уровню ошибки; в нашем примере используется метод severe(). Нам хотелось бы создать String для регистрируемого сообщения из результатов трассировки стека, но метод printStackTrace() по умолчанию не создает String. Для получения String необходимо использовать перегруженную версию printStackTrace() с аргументом java.io.PrintWriter (за подробными объяснениями обращайтесь к главе «Ввод/вывод»). Если передать конструктору PrintWriter объект java.io.StringWriter, для получения вывода в формате String достаточно вызвать toString().

Подход LoggingException чрезвычайно удобен (вся инфраструктура регистрации встроена в само исключение, и все работает автоматически без вмешательства со стороны клиента), однако на практике чаще применяется перехват и реги­страция «сторонних» исключений, поэтому сообщение должно генерироваться в обработчике исключения:

//: exceptions/LoggingExceptions2.java
// Регистрация перехваченных исключений
import java.util.logging.*;
import java.io.*;
 
public class LoggingExceptions2 {
private static Logger logger =
Logger.getLogger("LoggingExceptions2");
static void logException(Exception e) {
StringWriter trace = new StringWriter();
e.printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
public static void main(String[] args) {
try {
throw new NullPointerException();
} catch(NullPointerException e) {
logException(e);
}
}
}
<spoiler text="Output:"> (90% match)
Aug 30, 2005 4:07:54 PM LoggingExceptions2 logException
SEVERE: java.lang.NullPointerException
at LoggingExceptions2.main(LoggingExceptions2.java:16)

</spoiler>
На этом процесс создания собственных исключений не заканчивается — исключение можно снабдить дополнительными конструкторами и элементами:

//: exceptions/ExtraFeatures.java
// Дальнейшее расширение классов исключений
import static net.mindview.util.Print.*;
 
class MyException2 extends Exception {
private int x;
public MyException2() {}
public MyException2(String msg) { super(msg); }
public MyException2(String msg, int x) {
super(msg);
this.x = x;
}
public int val() { return x; }
public String getMessage() {
return "Detail Message: "+ x + " "+ super.getMessage();
}
}
 
public class ExtraFeatures {
public static void f() throws MyException2 {
print("Throwing MyException2 from f()");
throw new MyException2();
}
public static void g() throws MyException2 {
print("Throwing MyException2 from g()");
throw new MyException2("Originated in g()");
}
public static void h() throws MyException2 {
print("Throwing MyException2 from h()");
throw new MyException2("Originated in h()", 47);
}
public static void main(String[] args) {
try {
f();
} catch(MyException2 e) {
e.printStackTrace(System.out);
}
try {
g();
} catch(MyException2 e) {
e.printStackTrace(System.out);
}
try {
h();
} catch(MyException2 e) {
e.printStackTrace(System.out);
System.out.println("e.val() = " + e.val());
}
}
}

<spoiler text="Output:">

Throwing MyException2 from f()
MyException2: Detail Message: 0 null
at ExtraFeatures.f(ExtraFeatures.java:22)
at ExtraFeatures.main(ExtraFeatures.java:34)
Throwing MyException2 from g()
MyException2: Detail Message: 0 Originated in g()
at ExtraFeatures.g(ExtraFeatures.java:26)
at ExtraFeatures.main(ExtraFeatures.java:39)
Throwing MyException2 from h()
MyException2: Detail Message: 47 Originated in h()
at ExtraFeatures.h(ExtraFeatures.java:30)
at ExtraFeatures.main(ExtraFeatures.java:44)
e.val() = 47

</spoiler>
Было добавлено поле данных х вместе с методом, считывающим его значение, а также дополнительный конструктор для инициализации х. Переопределенный метод Throwable.getMessage() выводит более содержательную информацию об исключении. Метод getMessage() для классов исключений — аналог toString() в обычных классах.
Так как исключение является просто видом объекта, расширение возможностей классов исключений можно продолжить. Однако следует помнить, что все эти программисты, использующие ваши библиотеки, могут попросту проигно­рировать все «украшения» — нередко программисты ограничиваются проверкой типа исключения (как чаще всего бывает со стандартными исключениями Java).

Спецификации исключений

В языке Java желательно сообщать программисту, вызывающему ваш метод, об исключениях, которые данный метод способен возбуждать. Пользователь, вызывающий метод, сможет написать весь необходимый код для перехвата возможных исключений. Конечно, когда доступен исходный код, программист- клиент может пролистать его в поиске предложений throw, но библиотеки не всегда поставляются с исходными текстами. Чтобы библиотека не превращалась в «черный ящик», в Java добавили синтаксис (обязательный для использования), при помощи которого вы сообщаете клиенту об исключениях, возбуждаемых методом, чтобы он сумел правильно обработать их. Этот синтаксис называется спецификацией исключений (exception specification), входит в объявление метода и следует сразу за списком аргументов.

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

void f() throws TooBig, TooSmall. DivZero { //...

Однако запись

void f() { // ...

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

Обойти спецификацию исключений невозможно — если ваш метод возбуждает исключения и не обрабатывает их, компилятор предложит либо обработать исключение, либо включить его в спецификацию. Жестким контролем за соблюдением правил сверху донизу Java гарантирует правильность использования механизма исключений во время компиляции программы.

Впрочем, «обмануть» компилятор все же можно: вы вправе объявить о возбуждении исключения, которого на самом деле нет. Компилятор верит вам на слово и заставляет пользователей метода поступать так, как будто им и в самом деле необходимо перехватывать исключение. Таким образом можно «зарезервировать» исключение на будущее и уже потом возбуждать его, не изменяя описания готовой программы. Такая возможность может пригодиться и для создания абстрактных базовых классов и интерфейсов, в производных классах которых может возникнуть необходимость в возбуждении исключений.
Исключения, которые проверяются и навязываются еще на этапе компиляции программы, называют контролируемыми (checked).

Перехват произвольных исключений

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

 catch(Exception е) {System.out println("nepexвaчeно исключение");}

Подобная конструкция не упустит ни одного исключения, поэтому ее следует размещать в самом конце списка обработчиков, во избежание блокировки следующих за ней обработчиков исключений. Поскольку класс Exception является базовым для всех классов исключений, интересных программисту, сам он не предоставит никакой полезной информации об исключительной ситуации, но можно вызвать методы из его базового типа Throwable:


  • String getMessage(), String getLocalizedMessage() возвращают подробное сообщение об ошибке (или сообщение, локализованное для текущего контекста);
  • String toString() возвращает короткое описание объекта Throwable, включая подробное сообщение, если оно присутствует;
  • void printStackTrace(), void printStackTrace(PrintStream), void printStackTrace(java.io.PrintWriter) выводят информацию об объекте Throwable и трассировку стека вызовов для этого объекта. Трассировка стека вызовов показывает последовательность вызова методов, которая привела к точке возникновения исключения. Первый вариант отправляет информацию в стандартный поток ошибок, второй и третий — в поток по вашему выбору (в главе «Ввод/вывод» вы поймете, почему типов потоков два);
  • Throwable fillInStackTrace() записывает в объект Throwable информацию о текущем состоянии стека. Метод используется при повторном возбуждении ошибок или исключений.

Вдобавок в вашем распоряжении находятся методы типа Object, базового для Throwable (и для всех остальных классов). При использовании исключений может пригодиться метод getClass(), который возвращает информацию о классе объекта. Эта информация заключена в объекте типа Class. Например, вы можете узнать имя класса вместе с информацией о пакете методом getName() или получить только имя класса методом getSimpleName().

Рассмотрим пример с использованием основных методов класса Exception:

//: exceptions/ExceptionMethods.java
// Демонстрация методов класса Exception
import static net.mindview.util.Print.*;
 
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch(Exception e) {
print("Caught Exception");
print("getMessage():" + e.getMessage());
print("getLocalizedMessage():" +
e.getLocalizedMessage());
print("toString():" + e);
print("printStackTrace():");
e.printStackTrace(System.out);
}
}
}

<spoiler text="Output:">

Caught Exception
getMessage():My Exception
getLocalizedMessage():My Exception
toString():java.lang.Exception: My Exception
printStackTrace():
java.lang.Exception: My Exception
at ExceptionMethods.main(ExceptionMethods.java:8)

</spoiler>
Как видите, методы последовательно расширяют объем выдаваемой информации — всякий последующий фактически является продолжением предыдущего.

Трассировка стека

Информацию, предоставляемую методом printStackTrace(), также можно получить напрямую вызовом getStackTrace(). Метод возвращает массив элементов трассировки, каждый из которых представляет один кадр стека. Нулевой элемент представляет вершину стека, то есть последний вызванный метод последовательности (точка, в которой был создан и инициирован объект Throwable).

Соответственно, последний элемент массива представляет «низ» стека, то есть первый вызванный элемент последовательности. Рассмотрим простой пример:

//: exceptions/WhoCalled.java
// Программный доступ к данным трассировки стека
 
public class WhoCalled {
static void f() {
// Generate an exception to fill in the stack trace
try {
throw new Exception();
} catch (Exception e) {
for(StackTraceElement ste : e.getStackTrace())
System.out.println(ste.getMethodName());
}
}
static void g() { f(); }
static void h() { g(); }
public static void main(String[] args) {
f();
System.out.println("--------------------------------");
g();
System.out.println("--------------------------------");
h();
}
}

<spoiler text="Output:">

f
main
--------------------------------
f
g
main
--------------------------------
f
g
h
main

</spoiler>

Повторное возбуждение исключения

В некоторых ситуациях требуется заново возбудить уже перехваченное исключение; чаще всего это происходит при использовании Exception для перехвата всех исключений. Так как ссылка на текущее исключение уже имеется, вы попросту возбуждаете исключение по этой ссылке:

 catch(Exception е) {System.out.println("повторно возбуждено исключение"); throw e;}

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

Если вы просто заново возбуждаете исключение, информация о нем, выводимая методом printStackTrace(), будет по-прежнему относиться к месту возникновения исключения, но не к месту его повторного возбуждения. Если вам понадобится использовать новую трассировку стека, вызовите метод fillInStaсkTrасe(), который возвращает исключение (объект Throwable), созданное на базе старого с помещением туда текущей информации о стеке. Вот как это выглядит:

//: exceptions/Rethrowing.java
// // Демонстрация метода fillInStackTrace()
 
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}

<spoiler text="Output:">

originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
originating the exception in f()
Inside h(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.h(Rethrowing.java:20)
at Rethrowing.main(Rethrowing.java:35)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.h(Rethrowing.java:24)
at Rethrowing.main(Rethrowing.java:35)

</spoiler>
Строка с вызовом fillInStackTrace() становится новой точкой выдачи исключения.
Выдаваемое исключение также может отличаться от исходного. В этом случае эффект получается примерно таким же, как при использовании fillInStackTrace() — информация о месте зарождения исключения теряется, а остается информация, относящаяся к новой команде throw.

//: exceptions/RethrowNew.java
// Повторное возбуждение объекта,
// отличающегося от первоначального
class OneException extends Exception {
public OneException(String s) { super(s); }
}
 
class TwoException extends Exception {
public TwoException(String s) { super(s); }
}
 
public class RethrowNew {
public static void f() throws OneException {
System.out.println("originating the exception in f()");
throw new OneException("thrown from f()");
}
public static void main(String[] args) {
try {
try {
f();
} catch(OneException e) {
System.out.println(
"Caught in inner try, e.printStackTrace()");
e.printStackTrace(System.out);
throw new TwoException("from inner try");
}
} catch(TwoException e) {
System.out.println(
"Caught in outer try, e.printStackTrace()");
e.printStackTrace(System.out);
}
}
}

<spoiler text="Output:">

originating the exception in f()
Caught in inner try, e.printStackTrace()
OneException: thrown from f()
at RethrowNew.f(RethrowNew.java:15)
at RethrowNew.main(RethrowNew.java:20)
Caught in outer try, e.printStackTrace()
TwoException: from inner try
at RethrowNew.main(RethrowNew.java:25)

</spoiler>
О последнем исключении известно только то, что оно поступило из внутреннего блока try, но не из метода f().
Вам никогда не придется заботиться об удалении предыдущих исключений, и исключений вообще. Все они являются объектами, созданными в общей куче оператором new, и сборщик мусора уничтожает их автоматически.

Цепочки исключений

Зачастую необходимо перехватить одно исключение и возбудить следующее, не потеряв при этом информации о первом исключении — это называется цепочкой исключений (exception chaining). До выпуска пакета JDK 1.4 программистам приходилось самостоятельно писать код, сохраняющий информацию о предыдущем исключении, однако теперь конструкторам всех подклассов Throwable может передаваться объект-причина (cause). Предполагается, что причиной является изначальное исключение и передача ее в новый объект обеспечивает трассировку стека вплоть до самого его начала, хотя при этом создается и возбуждается новое исключение.
Интересно отметить, что единственными подклассами класса Throwable, принимающими объект-причину в качестве аргумента конструктора, являются три основополагающих класса исключений: Error (используется виртуальной машиной (JVM) для сообщений о системных ошибках), Exception и RuntimeException. Для организации цепочек из других типов исключений придется использовать метод initCause(), а не конструктор.
Следующий пример демонстрирует динамическое добавление полей в объект DynamicFields во время работы программы:

//: exceptions/DynamicFields.java
// Динамическое добавление полей в класс.
// Пример использования цепочки исключений
import static net.mindview.util.Print.*;
 
class DynamicFieldsException extends Exception {}
 
public class DynamicFields {
private Object[][] fields;
public DynamicFields(int initialSize) {
fields = new Object[initialSize][2];
for(int i = 0; i < initialSize; i++)
fields[i] = new Object[] { null, null };
}
public String toString() {
StringBuilder result = new StringBuilder();
for(Object[] obj : fields) {
result.append(obj[0]);
result.append(": ");
result.append(obj[1]);
result.append("\n");
}
return result.toString();
}
private int hasField(String id) {
for(int i = 0; i < fields.length; i++)
if(id.equals(fields[i][0]))
return i;
return -1;
}
private int
getFieldNumber(String id) throws NoSuchFieldException {
int fieldNum = hasField(id);
if(fieldNum == -1)
throw new NoSuchFieldException();
return fieldNum;
}
private int makeField(String id) {
for(int i = 0; i < fields.length; i++)
if(fields[i][0] == null) {
fields[i][0] = id;
return i;
}
// No empty fields. Add one:
Object[][] tmp = new Object[fields.length + 1][2];
for(int i = 0; i < fields.length; i++)
tmp[i] = fields[i];
for(int i = fields.length; i < tmp.length; i++)
tmp[i] = new Object[] { null, null };
fields = tmp;
// Recursive call with expanded fields:
return makeField(id);
}
public Object
getField(String id) throws NoSuchFieldException {
return fields[getFieldNumber(id)][1];
}
public Object setField(String id, Object value)
throws DynamicFieldsException {
if(value == null) {
// Most exceptions don't have a "cause" constructor.
// In these cases you must use initCause(),
// available in all Throwable subclasses.
DynamicFieldsException dfe =
new DynamicFieldsException();
dfe.initCause(new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if(fieldNumber == -1)
fieldNumber = makeField(id);
Object result = null;
try {
result = getField(id); // Get old value
} catch(NoSuchFieldException e) {
// Use constructor that takes "cause":
throw new RuntimeException(e);
}
fields[fieldNumber][1] = value;
return result;
}
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
print(df);
try {
df.setField("d", "A value for d");
df.setField("number", 47);
df.setField("number2", 48);
print(df);
df.setField("d", "A new value for d");
df.setField("number3", 11);
print("df: " + df);
print("df.getField(\"d\") : " + df.getField("d"));
Object field = df.setField("d", null); // Exception
} catch(NoSuchFieldException e) {
e.printStackTrace(System.out);
} catch(DynamicFieldsException e) {
e.printStackTrace(System.out);
}
}
}

<spoiler text="Output:">

null: null
null: null
null: null

d: A value for d
number: 47
number2: 48

df: d: A new value for d
number: 47
number2: 48
number3: 11

df.getField("d") : A new value for d
DynamicFieldsException
at DynamicFields.setField(DynamicFields.java:64)
at DynamicFields.main(DynamicFields.java:94)
Caused by: java.lang.NullPointerException
at DynamicFields.setField(DynamicFields.java:66)
... 1 more

</spoiler>
Каждый объект DynamicFields содержит массив пар Object-Object. Первый объект содержит идентификатор поля (String), а второй объект — значение поля, которое может быть любого типа, кроме неупакованных примитивов. При создании объекта необходимо оценить примерное количество полей. Метод setField() либо находит уже существующее поле с заданным именем, либо создает новое поле и сохраняет значение. Когда пространство для полей заканчивается, метод наращивает его, создавая массив размером на единицу больше и копируя в него старые элементы. При попытке размещения пустой ссылки null метод инициирует исключение DynamicFieldsException, создавая объект нужного типа и передавая методу initCause() в качестве причины исключение NullPointerException.
Для возвращаемого значения метод setField() использует старое значение поля, получая его методом getField(), который может возбудить исключение NoSuchFieldException. Если метод getField() вызывает программист-клиент, то он ответственен за обработку возможного исключения NoSuchFieldException, однако, если последнее возникает в методе setField(), это является ошибкой программы; соответственно, полученное исключение преобразуется в исключение RuntimeException с помощью конструктора, принимающего аргумент-причину.

Для создания результата toString() использует объект StringBuilder. Этот класс будет подробно рассмотрен при описании работы со строками.

Стандартные исключения Java

Класс Java Throwable описывает все объекты, которые могут возбуждаться как исключения. Существует две основные разновидности объектов Throwable (то есть ветви наследования). Тип Error представляет системные ошибки и ошибки времени компиляции, которые обычно не перехватываются (кроме нескольких особых случаев). Тип Exception может быть возбужден из любого метода стандартной библиотеки классов Java или пользовательского метода в случае неполадок при исполнении программы. Таким образом, для программистов интерес представляет прежде всего тип Exception.
Лучший способ получить представление об исключениях — просмотреть документацию JDK. Стоит сделать это хотя бы раз, чтобы получить представление о различных видах исключений, но вскоре вы убедитесь в том, что наиболее принципиальным различием между разными исключениями являются их имена. К тому же количество исключений в Java постоянно растет, и едва ли имеет смысл описывать их в книге. Любая программная библиотека от стороннего про­изводителя, скорее всего, также будет иметь собственный набор исключений. Здесь важнее понять принцип работы и поступать с исключениями сообразно.
Основной принцип заключается в том, что имя исключения относительно полно объясняет суть возникшей проблемы. Не все исключения определены в пакете java.lang, некоторые из них созданы для поддержки других библиотек, таких как util, net и io, как можно видеть из полных имен их классов или из базовых классов. Например, все исключения, связанные с вводом/выводом (I/O), унаследованы от java.io.IOException.

Особый случай: RuntimeException

Вспомним первый пример в этой главе:

 if(t == null) throw new NullPointerException();

Только представьте, как ужасно было бы проверять таким образом каждую ссылку, переданную вашему методу. К счастью, делать это не нужно — такая проверка автоматически выполняется во время исполнения Java-программы, и при попытке использования null-ссылок автоматически возбуждается NullPointerException. Таким образом, использованная в примере конструкция избыточна.

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

Что же происходит, когда подобные исключения не перехватываются? Так как компилятор не заставляет перечислять такие исключения в спецификациях, можно предположить, что исключение RuntimeException проникнет прямо в метод main(), и не будет перехвачено. Чтобы увидеть все в действии, испытайте следующий пример:

//: exceptions/NeverCaught.java
// Игнорирование RuntimeExceptions.
// {ThrowsException}
 
public class NeverCaught {
static void f() {
throw new RuntimeException("From f()");
}
static void g() {
f();
}
public static void main(String[] args) {
g();
}
}

Можно сразу заметить, что RuntimeException (и все от него унаследованное) является специальным случаем, так как компилятор не требует для него спецификации исключения. Выходные данные выводятся в System.err :

Exception in thread "main" java.lang.RuntimeException- Из f() at NeverCaught.f(NeverCaught.java:7) at NeverCaught.g(NeverCaught.java:10) at NeverCaught.main(NeverCaught.java-13)

Мы приходим к ответу на поставленный вопрос: если RuntimeException добирается до main() без перехвата, то работа программы завершается с вызовом метода printStackTrace().

Помните, что только исключения типа RuntimeException (и производных классов) могут игнорироваться во время написания текста программы, в то время как остальные действия компилятор осуществляет в обязательном порядке. Это объясняется тем, что RuntimeException является следствием ошибки программиста, например:


  • ошибки, которую невозможно предвидеть (к примеру, получение null- ссылки в вашем методе, переданной снаружи);
  • ошибки, которую вы как программист должны были проверить в вашей программе (подобной ArraylndexOutOfBoundsException, с проверкой размера массива). Ошибки первого вида часто становятся причиной ошибок второго вида.

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

Завершение с помощью finally

Часто встречается ситуация, когда некоторая часть программы должна выполняться независимо от того, было или нет возбуждено исключение внутри блока try. Обычно это имеет отношение к операциям, не связанным с освобождением памяти (так как это входит в обязанности сборщика мусора). Для достижения желаемой цели необходимо разместить блок finally после всех обработчиков исключений. Таким образом, полная конструкция обработки исключения выглядит так:

try {
// Защищенная секция: рискованные операции,
// которые могут породить исключения А, В. или С
} catch(A al) {
// Обработчик для ситуации А
} catch(B bl) {
// Обработчик для ситуации В
} catch(C cl) {
// Обработчик для ситуации С
} finally {
// Действия, производимые в любом случае
}

Чтобы продемонстрировать, что блок finally выполняется всегда, рассмотрим следующую программу:

//: exceptions/FinallyWorks.java
// Блок finally выполняется всегда
class ThreeException extends Exception {}
 
public class FinallyWorks {
static int count = 0;
public static void main(String[] args) {
while(true) {
try {
// Post-increment is zero first time:
if(count++ == 0)
throw new ThreeException();
System.out.println("No exception");
} catch(ThreeException e) {
System.out.println("ThreeException");
} finally {
System.out.println("In finally clause");
if(count == 2) break; // out of "while"
}
}
}
}

<spoiler text="Output:">

ThreeException
In finally clause
No exception
In finally clause

</spoiler>
Результат работы программы показывает, что вне зависимости от того, было ли возбуждено исключение, предложение finally выполняется всегда.
Данный пример также подсказывает, как справиться с тем фактом, что Java не позволяет вернуться к месту возникновения исключения, о чем говорилось ранее. Если расположить блок try в цикле, можно также определить условие, на основании которого будет решено, должна ли программа продолжаться. Также можно добавить статический счетчик или иной механизм для проверки нескольких разных решений, прежде чем отказаться от попыток восстановления. Это один из способов обеспечения повышенной отказоустойчивости программ.

Для чего нужен блок finally?

В языках без сборки мусора и без автоматических вызовов деструкторов блок finally гарантирует освобождение ресурсов и памяти независимо от того, что случилось в блоке try. В Java существует сборщик мусора, поэтому с освобожде­нием памяти проблем не бывает. Также нет необходимости вызывать деструкторы, их просто нет. Когда же нужно использовать finally в Java?

Блок finally необходим тогда, когда в исходное состояние вам необходимо вернуть что-то другое, а не память. Это может быть, например, открытый файл или сетевое подключение, часть изображения на экране или даже какой-то физический переключатель, вроде смоделированного в следующем примере:

//: exceptions/Switch.java
import static net.mindview.util.Print.*;
 
public class Switch {
private boolean state = false;
public boolean read() { return state; }
public void on() { state = true; print(this); }
public void off() { state = false; print(this); }
public String toString() { return state ? "on" : "off"; }
}
 
//: exceptions/OnOffException1.java
public class OnOffException1 extends Exception {}
 
//: exceptions/OnOffException2.java
public class OnOffException2 extends Exception {}
 
 
//: exceptions/OnOffSwitch.java
// Зачем использовать finally?
public class OnOffSwitch {
private static Switch sw = new Switch();
public static void f()
throws OnOffException1,OnOffException2 {}
public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
f();
sw.off();
} catch(OnOffException1 e) {
System.out.println("OnOffException1");
sw.off();
} catch(OnOffException2 e) {
System.out.println("OnOffException2");
sw.off();
}
}
}

<spoiler text="Output:">

on
off

</spoiler>
Наша цель — убедиться в том, что переключатель был выключен по завершении метода main(), поэтому в конце блока try и в конце каждого обработчика исключения помещается вызов sw.off(). Однако в программе может возникнуть неперехватываемое исключение, и тогда вызов sw.off() будет пропущен. Однако благодаря finally завершающий код можно поместить в одном определенном месте:

//: exceptions/WithFinally.java
// Finally гарантирует выполнение завершающего кода.
 
public class WithFinally {
static Switch sw = new Switch();
public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
OnOffSwitch.f();
} catch(OnOffException1 e) {
System.out.println("OnOffException1");
} catch(OnOffException2 e) {
System.out.println("OnOffException2");
} finally {
sw.off();
}
}
}

<spoiler text="Output:">

on
off

</spoiler>
Здесь вызов метода sw.off() просто перемещен в то место, где он гарантированно будет выполнен.

Даже если исключение не перехватывается в текущем наборе условий catch, блок finally отработает перед тем, как механизм обработки исключений продолжит поиск обработчика на более высоком уровне:

//: exceptions/AlwaysFinally.java
// Finally выполняется всегда
import static net.mindview.util.Print.*;
 
class FourException extends Exception {}
 
public class AlwaysFinally {
public static void main(String[] args) {
print("Entering first try block");
try {
print("Entering second try block");
try {
throw new FourException();
} finally {
print("finally in 2nd try block");
}
} catch(FourException e) {
System.out.println(
"Caught FourException in 1st try block");
} finally {
System.out.println("finally in 1st try block");
}
}
}

<spoiler text="Output:">

Entering first try block
Entering second try block
finally in 2nd try block
Caught FourException in 1st try block
finally in 1st try block

</spoiler>
Блок finally также исполняется при использовании команд break и continue. Заметьте, что комбинация finally в сочетании с break и continue с метками снимает в Java всякую необходимость в операторе goto.

Использование finally с return

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

//: exceptions/MultipleReturns.java
import static net.mindview.util.Print.*;
 
public class MultipleReturns {
public static void f(int i) {
print("Initialization that requires cleanup");
try {
print("Point 1");
if(i == 1) return;
print("Point 2");
if(i == 2) return;
print("Point 3");
if(i == 3) return;
print("End");
return;
} finally {
print("Performing cleanup");
}
}
public static void main(String[] args) {
for#40;int i = 1; i <= 4; i++)
f(i);
}
}

<spoiler text="Output:">

Initialization that requires cleanup
Point 1
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
End
Performing cleanup

</spoiler>
Из выходных данных видно, что выполнение finally не зависит от того, в какой точке защищенной секции была выполнена команда return.

Проблема потерянных исключений

К сожалению, реализация механизма исключений в Java не обошлась без изъяна. Хотя исключение сигнализирует об аварийной ситуации в программе и никогда не должно игнорироваться, оно может быть потеряно. Это происходит при использовании finally в конструкции определенного вида:

//: exceptions/LostMessage.java
// Как теряются исключения.
 
class VeryImportantException extends Exception {
public String toString() {
return "A very important exception!";
}
}
 
class HoHumException extends Exception {
public String toString() {
return "A trivial exception";
}
}
 
public class LostMessage {
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args) {
try {
LostMessage lm = new LostMessage();
try {
lm.f();
} finally {
lm.dispose();
}
} catch(Exception e) {
System.out.println(e);
}
}
}

<spoiler text="Output:">

A trivial exception

</spoiler>
В выводе нет никаких признаков VeryImportantException, оно было просто замещено исключением HoHumException в предложении finally. Это очень серьезный недочет, так как потеря исключения может произойти в гораздо более скрытой и трудно диагностируемой ситуации, в отличие от той, что показана в примере. Например, в C++ подобная ситуация (возбуждение второго исключения без обработки первого) рассматривается как грубая ошибка программиста. Возможно, в новых версиях Java эта проблема будет решена (впрочем, любой метод, способный возбуждать исключения — такой, как dispose() в приведенном примере — обычно заключается в конструкцию try-catch).

Еще проще потерять исключение простым возвратом из finally:

//: exceptions/ExceptionSilencer.java
 
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
// Команда 'return' в блоке finally
// прерывает обработку исключения
return;
}
}
}

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

Ограничения при использовании исключений

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

//: exceptions/StormyInning.java
// Переопределенные методы могут возбуждать только
// исключения, описанные в версии базового класса,
// или исключения, унаследованные от исключений
// базового класса.
 
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
 
abstract class Inning {
public Inning() throws BaseballException {}
public void event() throws BaseballException {
// Doesn't actually have to throw anything
}
public abstract void atBat() throws Strike, Foul;
public void walk() {} // Throws no checked exceptions
}
 
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
 
interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
 
public class StormyInning extends Inning implements Storm {
// OK to add new exceptions for constructors, but you
// must deal with the base constructor exceptions:
public StormyInning()
throws RainedOut, BaseballException {}
public StormyInning(String s)
throws Foul, BaseballException {}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
//! public void event() throws RainedOut {}
// If the method doesn't already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {}
// You can choose to not throw any exceptions,
// even if the base version does:
public void event() {}
// Overridden methods can throw inherited exceptions:
public void atBat() throws PopFoul {}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch(PopFoul e) {
System.out.println("Pop foul");
} catch(RainedOut e) {
System.out.println("Rained out");
} catch(BaseballException e) {
System.out.println("Generic baseball exception");
}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch(Strike e) {
System.out.println("Strike");
} catch(Foul e) {
System.out.println("Foul");
} catch(RainedOut e) {
System.out.println("Rained out");
} catch(BaseballException e) {
System.out.println("Generic baseball exception");
}
}
}

В классе Inning и конструктор, и метод event() объявляют, что будут возбуждать исключения, но в действительности этого не делают. Это допустимо, поскольку подобный подход заставляет пользователя перехватывать все виды исключений, которые потом могут быть добавлены в переопределенные версии метода event(). Данный принцип распространяется и на абстрактные методы, что и показано для метода atBat().
Интерфейс Storm интересен тем, что содержит один метод (event()), уже определенный в классе Inning, и один уникальный. Оба метода возбуждают новый тип исключения RainedOut. Когда класс StormyInning расширяет Inning и реализует интерфейс Storm, выясняется, что метод event() из Storm не способен изменить тип исключения для метода event() класса Inning. Опять-таки это вполне разумно, так как иначе вы бы никогда не знали, перехватываете ли нужное исключение в случае работы с базовым классом. Конечно, когда метод, описанный в интерфейсе, отсутствует в базовом классе (как rainHard()), никаких проблем с возбуждением исключений нет.
Метод StormyInning.walk() не компилируется из-за того, что он возбуждает исключение, тогда как Inning.walk() такого не делает. Если бы это позволялось, вы могли бы написать код, вызывающий метод Inning.walk() и не перехватывающий никаких исключений, а потом при подстановке объекта класса, производного от Inning, возникли бы исключения, нарушающие работу программы. Таким образом, принудительно обеспечивая соответствие спецификаций исключений в производных и базовых версиях методов, Java добивается взаимозаменяемости объектов.
Переопределенный метод event() показывает, что метод производного класса может вообще не возбуждать исключений, даже если это делается в базовой версии. Опять-таки это нормально, так как не влияет на уже написанный код — подразумевается, что метод базового класса возбуждает исключения. Аналогичная логика применима для метода atBat(), возбуждающего исключение PopFoul, производное от Foul, которое возбуждается базовой версией atBat(). Итак, если вы пишете код, работающий с Inning и вызывающий atBat(), то он должен перехватывать исключение Foul. Так как PopFoul наследует от Foul, обработчик исключения для Foul перехватит и PopFoul.
Последний интересный момент встречается в методе main(). Мы видим, что при работе именно с объектом StormyInning компилятор заставляет перехватывать только те исключения, которые характерны для этого класса, но при восходящем преобразовании к базовому типу компилятор заставляет перехватывать исключения из базового класса. Все эти ограничения значительно повышают ясность и надежность кода обработки исключений.
Хотя компилятор заставляет описывать исключения при наследовании, спецификация исключений не является частью объявления (сигнатуры) метода, которое состоит только из имени метода и типов аргументов. Соответственно, нельзя переопределять методы только по спецификациям исключений. Вдобавок, даже если спецификация исключения присутствует в методе базового класса, это вовсе не гарантирует его существования в методе производного класса. Данная практика сильно отличается от правил наследования, по которым метод базового класса обязательно присутствует и в производном классе. Другими словами, «интерфейс спецификации исключений» для определенного метода может сузиться в процессе наследования и переопределения, но никак не расшириться — и это прямая противоположность интерфейсу класса во время наследования.

Конструкторы

При программировании обработки исключений всегда спрашивайте себя: «Если произойдет исключение, будет ли все корректно завершено?» Чаще все идет более или менее безопасно, но с конструкторами возникает проблема. Конструктор приводит объект в определенное начальное состояние, но может начать выполнять какое-либо действие — такое как открытие файла — которое не будет правильно завершено, пока пользователь не освободит объект, вызвав специальный завершающий метод. Если исключение произойдет в конструкторе, эти финальные действия могут быть исполнены ошибочно. А это означает, что при написании конструкторов необходимо быть особенно внимательным.

Казалось бы, блок finally решает все проблемы. Но в действительности все сложнее — ведь finally выполняется всегда, и даже тогда, когда завершающий код не должен активизироваться до вызова какого-то метода. Если сбой в конструкторе произойдет где-то на середине, может оказаться, что часть объекта, освобождаемая в finally, еще не была создана.

В следующем примере создается класс, названный InputFile, который открывает файл и позволяет читать из него по одной строке. Он использует классы FileReader и BufferedReader из стандартной библиотеки ввода/вывода Java, которая будет изучена далее, но эти классы достаточно просты, и у вас не возникнет особых сложностей при работе с ними:

//: exceptions/InputFile.java
// Специфика исключений в конструкторах
import java.io.*;
 
public class InputFile {
private BufferedReader in;
public InputFile(String fname) throws Exception {
try {
in = new BufferedReader(new FileReader(fname));
// Other code that might throw exceptions
} catch(FileNotFoundException e) {
System.out.println("Could not open " + fname);
// Wasn't open, so don't close it
throw e;
} catch(Exception e) {
// All other exceptions must close it
try {
in.close();
} catch(IOException e2) {
System.out.println("in.close() unsuccessful");
}
throw e; // Rethrow
} finally {
// Don't close it here!!!
}
}
public String getLine() {
String s;
try {
s = in.readLine();
} catch(IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}
public void dispose() {
try {
in.close();
System.out.println("dispose() successful");
} catch(IOException e2) {
throw new RuntimeException("in.close() failed");
}
}
}

Конструктор InputFile получает в качестве аргумента строку (String) с именем открываемого файла. Внутри блока try он создает объект FileReader для этого файла. Класс FileReader не особенно полезен сам по себе, поэтому мы встраиваем его в созданный BufferedReader, с которым и работаем, — одно из преимуществ InputFile состоит в том, что он объединяет эти два действия.

Если при вызове конструктора FileReader произойдет сбой, возбуждается исключение FileNotFoundException. В этом случае закрывать файл не нужно, так как он и не открывался. Все остальные блоки catch обязаны закрыть файл, так как он уже был открыт во время входа в них. (Конечно, все было бы сложнее в случае, если бы несколько методов могли возбуждать FileNotFoundException. В таких ситуациях обычно требуется несколько блоков try.) Метод close() тоже может возбудить исключение, которое также проверяется и перехватывается — несмотря на то, что вызов находится в другом блоке catch — с точки зрения компилятора Java это всего лишь еще одна пара фигурных скобок. После выполнения всех необходимых локальных действий исключение возбуждается заново; ведь вызывающий метод не должен считать, что объект был благополучно создан.

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

Метод getLine() возвращает объект String со следующей строкой из файла. Он вызывает метод readLine(), способный возбуждать исключения, но они перехватываются; таким образом, сам getLine() исключений не возбуждает. При про­ектировании обработки исключений вы выбираете между полной обработкой исключения на определенном уровне, его частичной обработкой и передачей далее того же (или другого) исключения и, наконец, простой передачей далее. Там, где это возможно, передача исключения значительно упрощает программирование. В данной ситуации метод getLine() преобразует исключение в RuntimeException, чтобы указать на ошибку в программе.

Метод dispose() должен вызываться пользователем при завершении работы с объектом InputFile. Он освобождает системные ресурсы (такие, как открытые файлы), закрепленные за объектами BufferedReader и (или) FileReader. Делать это следует только тогда, когда работа с объектом InputFile действительно будет завершена. Казалось бы, подобные действия удобно разместить в методе finalize(), но, как упоминалось в главе 5, вызов этого метода не гарантирован (и даже если вы знаете, что он будет вызван, то неизвестно, когда). Это один из недостатков Java: все завершающие действия, кроме освобождения памяти, не производятся автоматически, так что вам придется информировать пользователя о том, что он ответственен за их выполнение.

Самый безопасный способ использования класса, который способен выдать исключение при конструировании и требует завершающих действий, основан на использовании вложенных блоков try:

//: exceptions/Cleanup.java
// Гарантированное освобождение ресурсов.
 
public class Cleanup {
public static void main(String[] args) {
try {
InputFile in = new InputFile("Cleanup.java");
try {
String s;
int i = 1;
while((s = in.getLine()) != null)
; // Perform line-by-line processing here...
} catch(Exception e) {
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
} finally {
in.dispose();
}
} catch(Exception e) {
System.out.println("InputFile construction failed");
}
}
}

<spoiler text="Output:">

dispose() successful

</spoiler>
Присмотритесь к логике происходящего: конструирование объекта InputFile фактически заключено в собственный блок try. Если попытка завершается неудачей, мы входим во внешнюю секцию catch и метод dispose() не вызывается. Но, если конструирование прошло успешно, мы хотим обеспечить гарантированное завершение, поэтому сразу же после конструирования создается новый блок try. Блок finally, выполняющий завершение, связывается с внутренним блоком try; таким образом, блок finally не выполняется при неудачном конструировании и всегда выполняется, если конструирование прошло удачно.
Эта универсальная идиома применяется и в тех ситуациях, когда конструктор не выдает исключений. Основной принцип: сразу же после создания объекта, требующего завершения, начинается конструкция try-finally:

//: exceptions/CleanupIdiom.java
// За каждым освобождаемым объектом следует try-finally
 
class NeedsCleanup { // Construction can't fail
private static long counter = 1;
private final long id = counter++;
public void dispose() {
System.out.println("NeedsCleanup " + id + " disposed");
}
}
 
class ConstructionException extends Exception {}
 
class NeedsCleanup2 extends NeedsCleanup {
// Construction can fail:
public NeedsCleanup2() throws ConstructionException {}
}
 
public class CleanupIdiom {
public static void main(String[] args) {
// Section 1:
NeedsCleanup nc1 = new NeedsCleanup();
try {
// ...
} finally {
nc1.dispose();
}
 
// Section 2:
// If construction cannot fail you can group objects:
NeedsCleanup nc2 = new NeedsCleanup();
NeedsCleanup nc3 = new NeedsCleanup();
try {
// ...
} finally {
nc3.dispose(); // Reverse order of construction
nc2.dispose();
}
 
// Section 3:
// If construction can fail you must guard each one:
try {
NeedsCleanup2 nc4 = new NeedsCleanup2();
try {
NeedsCleanup2 nc5 = new NeedsCleanup2();
try {
// ...
} finally {
nc5.dispose();
}
} catch(ConstructionException e) { // nc5 constructor
System.out.println(e);
} finally {
nc4.dispose();
}
} catch(ConstructionException e) { // nc4 constructor
System.out.println(e);
}
}

<spoiler text="Output:">

NeedsCleanup 1 disposed
NeedsCleanup 3 disposed
NeedsCleanup 2 disposed
NeedsCleanup 5 disposed
NeedsCleanup 4 disposed

</spoiler>
Секция 1 метода main() весьма прямолинейна: за созданием завершаемого объекта следует try-finally. Если конструирование не может завершиться неудачей, наличие catch не требуется. В секции 2 мы видим, что конструкторы, которые не могут завершиться неудачей, могут группироваться как для конструирования, так и для завершения.

Секция 3 показывает, как поступать с объектами, при конструировании которых возможны сбои и которые нуждаются в завершении. Здесь программа усложняется, потому что каждое конструирование должно заключаться в отдельную копию try-catch и за ним должна следовать конструкция try-finally, обеспечивающая завершение.

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

Идентификация исключений

Механизм обработки исключений ищет в списке «ближайший» подходящий обработчик в порядке их следования. Когда соответствие обнаруживается, исключение считается найденным и дальнейшего поиска не происходит.
Идентификация исключений не требует обязательного соответствия между исключением и обработчиком. Объект порожденного класса подойдет и для обработчика, изначально написанного для базового класса:

//: exceptions/Human.java
// Перехват иерархии исключений.
 
class Annoyance extends Exception {}
class Sneeze extends Annoyance {}
 
public class Human {
public static void main(String[] args) {
// Catch the exact type:
try {
throw new Sneeze();
} catch(Sneeze s) {
System.out.println("Caught Sneeze");
} catch(Annoyance a) {
System.out.println("Caught Annoyance");
}
// Catch the base type:
try {
throw new Sneeze();
} catch(Annoyance a) {
System.out.println("Caught Annoyance");
}
}
}

<spoiler text="Output:">

Caught Sneeze
Caught Annoyance

</spoiler>
Исключение Sneeze будет перехвачено в первом блоке catch, который ему соответствует — конечно, это будет первый блок. Но, если удалить первый блок catch, оставив только проверку Annoyance, программа все равно работает, потому что она перехватывает базовый класс Sneeze. Другими словами, блок catch (Annoyance а) поймает Annoyance или любой другой класс, унаследованный от него. Если вы добавите новые производные исключения в свой метод, программа пользователя этого метода не потребует изменений, так как клиент перехватывает исключения базового класса.
Если вы попытаетесь «замаскировать» исключения производного класса, поместив сначала блок catch базового класса:

 try { throw new Sneeze();
} catch(Annoyance a) {
// ..
} catch(Sneeze s) {
//...
}

компилятор выдаст сообщение об ошибке, так как он видит, что блок catch для исключения Sneeze никогда не выполнится.

Альтернативные решения

Система обработки исключений представляет собой «черный ход», позволяющий программе нарушить нормальную последовательность выполнения команд. «Черный ход» открывается при возникновении «исключительных ситуаций», когда обычная работа далее невозможна или нежелательна. Исключения представляют собой условия, с которыми текущий метод справиться не в состоянии. Причина, по которой возникают системы обработки исключений, кроется в том, что программисты не желали иметь дела с громоздкой проверкой всех возможных условий возникновения ошибок каждой функции. В результате ошибки ими просто игнорировались. Стоит отметить, что вопрос удобства программиста при обработке ошибок стоял на первом месте для разработчиков Java.

Основное правило при использовании исключений гласит: «Не обрабатывайте исключение, если вы не знаете, что с ним делать». По сути, отделение кода, ответственного за обработку ошибок, от места, где ошибка возникает, является одной из главных целей обработки исключений. Это позволяет вам сконцентрироваться на том, что вы хотите сделать в одном фрагменте кода, и на том, как вы собираетесь поступить с ошибками в совершенно другом месте программы. В результате основной код не перемежается с логикой обработки ошибок, что упрощает его сопровождение и понимание. Исключения также сокращают объем кода, так как один обработчик может обслуживать несколько потенциальных источников ошибок.

Контролируемые исключения немного усложняют ситуацию, поскольку они заставляют добавлять обрабатывающие исключения предложения там, где вы не всегда еще готовы справиться с ошибкой. В итоге возникает проблема «про­глоченных исключений»:

try {
// делает что-то полезное
} саtch (ОбязывающееИсключение е) {} // Проглотили!

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

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

Несмотря на кажущуюся простоту, проблема не только очень сложна, но и к тому же неоднозначна. Существуют твердые приверженцы обеих точек зрения, которые считают, что верный ответ (их) очевиден и просто бросается в глаза. Вероятно, одна из точек зрения основана на несомненных преимуществах перехода от слабо типизированного языка (например, C до выхода стандарта ANSI) к языку с строгой статической проверкой типов (то есть с проверкой во время компиляции), подобному C++ или Java. Преимущества такого перехода настолько очевидны, что строгая статическая проверка типов кажется панацеей от всех бед. Я надеюсь поставить под вопрос ту небольшую часть моей эволюции, отличающуюся абсолютной верой в строгую статическую проверку типов: без сомнения, большую часть времени она приносит пользу, но существует неформальная граница, за которой такая проверка становится препятствием на вашем пути (одна из моих любимых цитат такова: «Все модели неверны, но некоторые полезны»).

Предыстория

Обработка исключений зародилась в таких системах, как PL/1 и Mesa, а затем мигрировала в CLU, Smalltalk, Modula-3, Ada, Eiffel, С++, Python, Java и в появившиеся после Java языки Ruby и С#. Конструкции Java сходны с конструкциями C++, кроме тех аспектов, в которых решения C++ приводили к проблемам.

Обработка исключений была добавлена в C++ на довольно позднем этапе стандартизации. Модель исключений в C++ в основном была заимствована из CLU. Впрочем, в то время существовали и другие языки с поддержкой обработки исключений: Ada, Smalltalk (в обоих были исключения, но отсутствовали их спецификации) и Modula-З (в котором существовали и исключения, и их спецификации).

Следуя подходу CLU при разработке исключений C++, Страуструп считал, что основной целью является сокращение объема кода восстановления после ошибки. Вероятно, он видел немало программистов, которые не писали код обработки ошибок на C, поскольку объем этого кода был устрашающим, а размещение выглядело нелогично. В результате все происходило в стиле C: ошибки в коде игнорировались, а с проблемами справлялись при помощи отладчиков. Чтобы исключения реально заработали, C-программисты должны были писать «лишний» код, без которого они обычно обходились. Таким образом, объем нового кода не должен быть чрезмерным. Важно помнить об этих целях, говоря об эффективности контролируемых исключений в Java.

C++ добавил к идее CLU дополнительную возможность: спецификации исключений, то есть включение в сигнатуру метода информации об исключениях, возникающих при вызове. В действительности спецификация исключения несет двойной смысл. Она означает: «Я возбуждаю это исключение в коде, а вы его обрабатываете». Но она также может означать: «Я игнорирую исключение, которое может возникнуть в моем коде; обеспечьте его обработку». При освещении механизмов исключений мы концентрировались на «обеспечении обработки», но здесь мне хотелось бы поближе рассмотреть тот факт, что зачастую исключения игнорируются, и именно этот факт может быть отражен в спецификации исключения.

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

Интересно отметить, что из-за использования шаблонов (templates) спецификации исключений отсутствуют в стандартной библиотеке C++. В Java существуют ограничения на использование параметризованных типов со спецификациями исключений.

Перспективы

Во-первых, язык Java, по сути, стал первопроходцем в использовании контролируемых исключений (несомненно из-за спецификаций исключений C++ и того факта, что программисты на C++ не уделяли им слишком много внимания). Это был эксперимент, повторить который с тех пор пока не решился еще ни один язык.

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

Одним из важных достижений Java стала унификация модели передачи информации об ошибках, так как обо всех ошибках сообщается посредством исключений. В C++ этого не было, из-за обратной совместимости с C и возможности задействовать старую модель простого игнорирования ошибок. Когда Java изменил модель C++ так, что сообщать об ошибках стало возможно только посредством исключений, необходимость в дополнительных мерах принуждения в виде контролируемых исключений сократилась.

В прошлом я твердо считал, что для разработки надежных программ необходимы и контролируемые исключения, и строгая статическая проверка типов. Однако опыт, полученный лично и со стороны1, с языками, более динамичными, чем статичными, привел меня к мысли, что на самом деле главные преимущества обусловлены следующими аспектами:

  • Унификация модели сообщения об ошибках посредством исключений (независимо от того, заставляет ли компилятор программиста их обрабатывать).
  • Проверка типов, не привязанная к тому, когда она проводится — на стадии компиляции или во время работы программы.

Вдобавок снижение ограничений времени компиляции весьма положительно отражается на продуктивности программиста. С другой стороны, для компенсации чрезмерной жесткости статической проверки типов необходимы рефлексия и параметризация, как вы убедитесь в некоторых примерах книги.
Некоторые уверяли меня, что все сказанное является кощунством, безнадежно испортит мою репутацию, приведет к гибели цивилизации и провалу большой доли программных проектов. Вера в то, что выявление ошибок на стадии компиляции спасет ваш проект, весьма сильна, но гораздо важнее сознавать ограничения того, на что способен компьютер. Стоит помнить:
«Хороший язык программирования помогает программистам писать хорошие программы. Ни один из языков программирования не может запретить своим пользователям писать плохие программы».
В любом случае исчезновение когда-либо из Java контролируемых исключений весьма маловероятно. Это слишком радикальное изменение языка, и защитники их в Sun весьма сильны. История Sun неотделима от политики абсолютной обратной совместимости — фактически любое программное обеспечение Sun работает на любом оборудовании Sun, как бы старо оно ни было. Но, если вы чувствуете, что контролируемые исключения становятся для вас препятствием (особенно если вас заставляют обрабатывать исключение, а вы не знаете, как с ним поступить), существует несколько вариантов.

Передача исключений на консоль

В несложных программах, как во многих примерах данной книги, простейшим решением является передача исключения за пределы метода main(), на консоль. К примеру, при открытии файла для чтения (подробности вы вскоре узнаете) необходимо открыть и закрыть поток FilelnputStream, который возбуждает исключения. В небольшой программе можно поступить следующим образом (подобный подход характерен для многих примеров книги):

//: exceptions/MainException.java
import java.io.*;
 
public class MainException {
// Передаем все исключения на консоль:
public static void main(String[] args) throws Exception {
// Открываем файл :
FileInputStream file =
new FileInputStream("MainException.java");
// Используем файл ...
// Закрываем файл:
file.close();
}
}

Заметьте, что main() — такой же метод, как и все прочие; он тоже может иметь спецификацию исключений, и здесь типом исключения является Exception, базовый класс всех контролируемых исключений. Передавая его на консоль, вы освобождаетесь от необходимости написания предложений try-catch в теле метода main().

Преобразование контролируемых исключений в неконтролируемые

Рассмотренный выше подход хорош при написании метода main(), но в более общих ситуациях не слишком полезен. Подлинная проблема возникает при написании тела самого обычного метода, когда при вызове другого метода вы четко сознаете: «Понятия не имею, что делать с исключением дальше, но „съедать" мне его не хочется, так же как и печатать банальное сообщение». Проблема решается при помощи цепочек исключений. Управляемое исключение просто «заворачивается» в класс RuntimeException примерно так:

 try {
// ... делаем что-нибудь полезное
} саtch (НеЗнаюЧтоДелатьСЭтимКонтролируемымИсключением е) { throw new RuntimeException(e);
}

Решение идеально подходит для тех случаев, когда вы хотите «подавить» контролируемое исключение: вы не «съедаете» его, вам не приходится описывать его в своей спецификации исключений, и благодаря цепочке исключений вы не теряете информацию об исходном исключении.

Описанная методика позволяет игнорировать исключение и пустить его «всплывать» вверх по стеку вызова без необходимости писать блоки try-catch и (или) спецификации исключения. Впрочем, при этом вы все равно можете перехватить и обработать конкретное исключение, используя метод getCause(), как показано ниже:

//: exceptions/TurnOffChecking.java
// "Подавление" контролируемых исключений
import java.io.*;
import static net.mindview.util.Print.*;
 
class WrapCheckedException {
void throwRuntimeException(int type) {
try {
switch(type) {
case 0: throw new FileNotFoundException();
case 1: throw new IOException();
case 2: throw new RuntimeException("Where am I?");
default: return;
}
} catch(Exception e) { // Превращаем в неконтролируемое:
throw new RuntimeException(e);
}
}
}
 
class SomeOtherException extends Exception {}
 
public class TurnOffChecking {
public static void main(String[] args) {
WrapCheckedException wce = new WrapCheckedException();
// Можно вызвать throwRuntimeException() без блока try
// и позволить исключению RuntimeException покинуть метод:
wce.throwRuntimeException(3);
// Или перехватить исключение:
for(int i = 0; i < 4; i++)
try {
if(i < 3)
wce.throwRuntimeException(i);
else
throw new SomeOtherException();
} catch(SomeOtherException e) {
print("SomeOtherException: " + e);
} catch(RuntimeException re) {
try {
throw re.getCause();
} catch(FileNotFoundException e) {
print("FileNotFoundException: " + e);
} catch(IOException e) {
print("IOException: " + e);
} catch(Throwable e) {
print("Throwable: " + e);
}
}
}
}

<spoiler text="Output:">

FileNotFoundException: java.io.FileNotFoundException
IOException: java.io.IOException
Throwable: java.lang.RuntimeException: Where am I?
SomeOtherException: SomeOtherException

</spoiler>
Метод WrapCheckedException.throwRuntimeException() содержит код, генерирующий различные типы исключений. Они перехватываются и «заворачиваются» в объекты RuntimeException, становясь таким образом «причиной» этих ис­ключений.
При взгляде на класс TurnOffChecking нетрудно заметить, что вызвать метод throwRuntimeException() можно и без блока try, поскольку он не возбуждает никаких контролируемых исключений. Но когда вы будете готовы перехватить исключение, у вас будет возможность перехватить любое из них — достаточно поместить свой код в блок try. Начинаете вы с перехвата исключений, которые, как вы знаете, могут явно возникнуть в коде блока try, — в нашем случае первым делом перехватывается SomeOtherException. В конце вы перехватываете RuntimeException и заново возбуждаете исключение, являющееся его причиной (получая последнее методом getCause(), «завернутое» исключение). Так извлекаются из­начальные исключения, обрабатываемые в своих предложениях catch.
Методика «заворачивания» управляемых исключений в объекты RuntimeException встречается в некоторых примерах книги. Другое возможное решение — создание собственного класса, производного от RuntimeException. Перехватывать такое исключение не обязательно, но, если вы захотите, такая возможность существует.

Основные правила обработки исключений

Используйте исключения для того, чтобы:


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

Резюме

Исключения являются неотъемлемой частью программирования на Java; существует некий барьер, который невозможно преодолеть без умения работать с ними. По этой причине исключения были представлены именно в этой части книги — многими библиотеками (скажем, библиотекой ввода/вывода) просто невозможно нормально пользоваться без обработки исключений.
Одно из преимуществ обработки исключений состоит в том, что она позволяет сосредоточиться на решаемой проблеме, а затем обработать все ошибки в описанном коде в другом месте. Хотя исключения обычно описываются как средство передачи информации и восстановления после ошибок на стадии выполнения, я сильно сомневаюсь, что «восстановление» часто реализуется на практике. По моей оценке, это происходит не более чем в 10% случаев, и даже тогда в основном сводится к раскрутке стека к заведомо стабильному состоянию вместо реального выполнения действий по восстановлению. На мой взгляд, ценность исключений в основном обусловлена именно передачей информации. Java фактически настаивает, что программа должна сообщать обо всех ошибках в виде исключений, и именно это обстоятельство обеспечивает Java большое преимущество перед языками вроде C++, где программа может сообщать об ошибках разными способами (а то и не сообщать вовсе).

]]>
Книги по Java https://linexp.ru?id=4741 Wed, 29 Jun 2022 14:25:14 GMT
<![CDATA[Глава 13 Thinking in Java 4th edition]]> ИНФОРМАЦИЯ О ТИПАХМеханизм RTTI (Runtime Type Information) предназначен для получения и использования информации о типах во время выполнения программы. RTTI освобождает разработчика от необходимости выполнять всю работу с типами на стадии компиляции и открывает немало замечательных возможностей.

ИНФОРМАЦИЯ О ТИПАХ

Механизм RTTI (Runtime Type Information) предназначен для получения и использования информации о типах во время выполнения программы.
RTTI освобождает разработчика от необходимости выполнять всю работу с типами на стадии компиляции и открывает немало замечательных возможностей. Потребность в RTTI вскрывает целый ряд интересных (и зачастую сложных) аспектов объектно-ориентированного проектирования.

В этой главе рассматриваются способы получения информации об объектах и классах во время выполнения программы в Java. Существует два механизма получения такой информации: «традиционный» механизм RTTI, подразумевающий, что все типы доступны во время компиляции, а также механизм рефлексии (reflection), применяемый исключительно во время выполнения программы.

Необходимость в динамическом определении типов (RTTI)

Рассмотрим хорошо знакомый пример с геометрическими фигурами, основанный на полиморфизме. Обобщенным базовым классом является фигура Shape, а производными классами — окружность Circle, прямоугольник Square и треугольник Triangle.

13.1.png

Это обычная диаграмма наследования — базовый класс расположен вверху, производные классы присоединяются к нему снизу. Обычно при разработке объектно-ориентированных программ код по возможности манипулирует ссылками на базовый класс (в нашем случае это фигура — Shape). Если вдруг в программу будет добавлен новый класс (например, производный от фигуры Shape ромб — Rhomboid), то код менять не придется. В нашем случае метод draw() класса является динамически связываемым, поэтому программист-клиент может вызывать этот метод, пользуясь ссылкой базового типа Shape. Метод draw() переопределяется во всех производных классах, и по природе динамического связывания вызов его по ссылке на базовый класс все равно даст необходимый результат. Это и есть полиморфизм.
Таким образом, обычно вы создаете объект конкретного класса (Circle, Square или Triangle), проводите восходящее преобразование к фигуре Shape («забывая» точный тип объекта) и используете ссылку на обобщенную фигуру. Реализация иерархии Shape может выглядеть примерно так:

//: typeinfo/Shapes.java
import java.util.*;
 
abstract class Shape {
void draw() { System.out.println(this + ".draw()"); }
abstract public String toString();
}
 
class Circle extends Shape {
public String toString() { return "Circle"; }
}
 
class Square extends Shape {
public String toString() { return "Square"; }
}
 
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
 
public class Shapes {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(
new Circle(), new Square(), new Triangle()
);
for(Shape shape : shapeList)
shape.draw();
}
}

<spoiler text="Output:">

Circle.draw() 
Square.draw()
Triangle.draw()

</spoiler>
Метод draw() базового класса Shape неявно использует метод toString() для вывода идентификатора класса, для чего ссылка this передается методу System.out.println() (обратите внимание: метод toString() объявлен абстрактным, чтобы производные классы были обязаны переопределить его и чтобы предотвратить создание экземпляров Shape). Когда этот объект встречается в выражении конкатенации строк, автоматически вызывается его метод toString() для получения соответствующего строкового представления. Каждый из производных классов переопределяет метод toString() (из базового класса Object), чтобы метод draw() выводил в каждом случае различную информацию.
В данном примере восходящее преобразование происходит во время помещения объекта-фигуры в контейнер List<Shape>. В процессе восходящего преобразования теряется конкретная информация, в том числе и точный тип фигур. Контейнеру все равно — он хранит просто объекты Shape.
Когда вы извлекаете из контейнера очередной элемент, контейнер, в котором все элементы хранятся в виде Object, автоматически преобразует результат обратно к Shape. Это наиболее основная форма RTTI, поскольку все подобные преобразования в языке Java проверяются на правильность на стадии исполнения. Именно для этого и служит RTTI: во время выполнения программы проверяется истинный тип объекта.
В нашем случае определение типа происходит частично: тип Object преобразуется к базовому типу фигур Shape, а не к конкретным типам Circle, Square или Triangle. Просто потому, что в данный момент нам известно только то, что кон­тейнер List<Shape> заполнен фигурами Shape. Во время компиляции соблюдение этого требования обеспечивается контейнером и системой параметризованных типов Java, а при выполнении оно подтверждается успешным преобразованием типов.
Теперь в действие вступает полиморфизм — для каждой фигуры Shape вызывается свой метод draw(), в зависимости от того, окружность это (Circle), прямоугольник (Square) или треугольник (Triangle). И в основном именно так и должно быть; основная часть кода не должна зависеть от точного типа объекта, она оперирует с универсальным представлением целого семейства объектов (в нашем случае это фигура (Shape)). Такой подход упрощает написание программы, а впоследствии ее чтение и сопровождение. По этой причине полиморфизм часто используется при написании объектно-ориентированных программ.
Но что, если у вас имеется не совсем обычная задача, для успешного решения которой необходимо узнать точный тип объекта, располагая только ссылкой на базовый тип? Допустим, пользователи программы с фигурами хотят выделить определенные фигуры (скажем, все треугольники) на экране фиолетовым цветом. При помощи RTTI можно узнать точный тип объекта, на который указывает ссылка базового типа Shape.


Объект Class

Чтобы понять, как работает RTTI в Java, необходимо знать, каким образом хранится информация о типе во время выполнения программы. Для этой цели используется специальный объект типа Class, который и содержит описание класса. Объект Class используется при создании всех «обыкновенных» объектов любой программы.
Каждый класс, задействованный в программе, представлен своим объектом Class. Иначе говоря, при написании и последующей компиляции нового класса для него создается объект Class (который затем сохраняется в одноименном файле с расширением .class). Для создания объекта этого класса виртуальная машина Java JVM), исполняющая программу, использует подсистему, называемую загрузчиком классов.
Подсистема загрузчиков классов в действительности состоит из цепочки загрузчиков классов, но только основной загрузчик является частью реализации JVM. Основной загрузчик классов загружает так называемые доверенные классы, в том числе классы Java API, с локального диска. Обычно включать дополнительные загрузчики классов в цепочку не требуется, но в особых ситуациях (например, при загрузке классов для поддержки приложений веб-сервера или при загрузке классов по сети) существует возможность подключения дополнительных загрузчиков.
Все классы загружаются в JVM динамически, при первом использовании класса. Таким образом, программа на Java никогда не бывает полностью загружена до начала своего выполнения, и в этом отношении Java отличается от многих традиционных языков. Динамическая загрузка открывает возможности, недоступные или трудно реализуемые в языках со статической загрузкой вроде C++.
Сначала JVM проверяет, загружен ли объект Class для этого нового класса. При отрицательном результате JVM ищет подходящий файл .class и подгружает его (а дополнительный загрузчик, например, может подгружать байт-код из базы данных). Загруженный код класса проходит проверку на целостность и на отсутствие некорректного байт-кода Java (одна из «линий защиты» в Java).
После того как объект Class для определенного типа будет помещен в память, в дальнейшем он используется при создании всех объектов этого типа. Следующая программа проясняет сказанное:

//: typeinfo/SweetShop.java
// Проверка процесса загрузки классов
import static net.mindview.util.Print.*;
 
class Candy {
static { print("Загрузка класса Candy"); }
}
 
class Gum {
static { print("Загрузка класса Gum"); }
}
 
class Cookie {
static { print("Загрузка класса Cookie"); }
}
 
public class SweetShop {
public static void main(String[] args) {
print("в методе main()");
new Candy();
print("После создания объекта Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
print("Класс Gum не найден");
}
print("После вызова метода Class.forName(\"Gum\")");
new Cookie();
print("После создания объекта Cookie");
}
}

<spoiler text="Output:">

в методе main()
Загрузка класса Candy
После создания объекта Candy
Загрузка класса Gum
После вызова метода Class.forName("Gum")
Загрузка класса Cookie
После создания объекта Cookie

</spoiler>
В каждом из классов Candy, Gum и Cookie присутствует статический блок, который отрабатывает один раз, при первой загрузке класса. При выполнении этого блока выводится сообщение, говорящее о том, какой класс загружается. В методе main() создание объектов классов Candy, Gum и Cookie чередуется с выводом на экран вспомогательных сообщений, по которым можно оценить, в какой момент загружается тот или иной класс.
Из результата работы программы мы видим, что объект Class загружается только при непосредственной необходимости, а статическая инициализация производится при загрузке этого объекта.
Особенно интересно выглядит строка программы

Class.forName("Gum");

Все объекты Class принадлежат классу Class. Объект Class ничем принципиально не отличается от других объектов, поэтому вы можете выполнять операции со ссылкой на него (именно так и поступает загрузчик классов). Один из способов получения ссылки на объект Class заключается в вызове метода forName(), которому передается строка (String) с именем класса (следите за правильностью написания и регистром символов!). Метод возвращает ссылку на объект Class, которая в данном примере нам не нужна; метод Class.forName() вызывался ради побочного эффекта, то есть загрузки класса Gum, если он еще не в памяти. В процессе загрузки выполняется static-инициализатор класса Gum.
Если бы в рассмотренном примере метод Class.forName() сработал неудачно (не смог бы найти класс, который вы хотели загрузить), он возбудил бы исключение ClassNotFoundException. Здесь мы просто сообщаем о проблеме и двигаемся дальше, однако в более совершенной программе можно было бы попытаться исправить ошибку в обработчике исключения.
Чтобы использовать информацию RTTI на стадии исполнения, прежде всего необходимо получить ссылку на подходящий объект Class. Один из способов — вызов метода Class.forName() — удобен тем, что вам не потребуется уже сущест­вующий объект нужного типа. Впрочем, если такой объект уже существует, для получения ссылки на его объект Class можно вызвать метод getClass(), определенный в корневом классе Object. Метод возвращает объект Class, представляющий фактический тип объекта. Класс Class содержит немало интересных методов, продемонстрированных в следующем примере:

//: typeinfo/toys/ToyTest.java
// Testing class Class.
package typeinfo.toys;
import static net.mindview.util.Print.*;
 
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
 
class Toy {
// Закомментируйте следующий далее конструктор по
// умолчанию, тогда в строке с пометкой (*1*)
// возникнет ошибка NoSuchMethodError
Toy() {}
Toy(int i) {}
}
 
class FancyToy extends Toy
implements HasBatteries, Waterproof, Shoots {
FancyToy() { super(1); }
}
 
public class ToyTest {
static void printInfo(Class cc) {
print("Имя класса: " + cc.getName() +
" это интерфейс?[" + cc.isInterface() + "]");
print("Простое имя: " + cc.getSimpleName());
print("Каноническое имя: " + cc.getCanonicalName());
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("typeinfo.toys.FancyToy");
} catch(ClassNotFoundException e) {
print("He найден класс FancyToy");
System.exit(1);
}
printInfo(c);
for(Class face : c.getInterfaces())
printInfo(face);
Class up = c.getSuperclass();
Object obj = null;
try {
// Необходим конструктор по умолчанию:
obj = up.newInstance();
} catch(InstantiationException e) {
print("He удалось создать объект");
System.exit(1);
} catch(IllegalAccessException e) {
print("Нет доступа");
System.exit(1);
}
printInfo(obj.getClass());
}
}

<spoiler text="Output:">

Имя класса typeinfo toys FancyToy это интерфейс? [false]
Простое имя FancyToy
Каноническое имя- typeinfo.toys FancyToy
Имя класса typeinfo toys HasBatteries это интерфейс? [true]
Простое имя HasBatteries
Каноническое имя: typeinfo toys HasBatteries
Имя класса typeinfo toys Waterproof это интерфейс? [true]
Простое имя. Waterproof
Каноническое имя typeinfo toys Waterproof
Имя класса, typeinfo.toys Shoots это интерфейс? [true]
Простое имя: Shoots
Каноническое имя: typeinfo.toys Shoots
Имя класса: typeinfo.toys Toy это интерфейс? [false]
Простое имя: Toy
Каноническое имя typeinfo.toys.Toy

</spoiler>
Класс FancyToy, производный от Toy, реализует несколько интерфейсов: HasBatteries, Waterproof и Shoots. В методе main() создается ссылка на объект Class для класса FancyToy, для этого в подходящем блоке try вызывается метод forName(). Обратите внимание на необходимость использования полного имени (с именем пакета) в строке, передаваемой forName().
Метод printInfo() использует getName() для получения полного имени класса и методы getSimpleName() и getCanonicalName() (появившиеся в Java SE5), возвращающие имя без пакета и полное имя соответственно. Метод isInterface() проверяет, представляет ли объект Class интерфейс. Таким образом, по объекту Class можно узнать практически все, что может потребоваться узнать о типе.
Метод Class.getInterfaces() возвращает массив объектов Class, представляющих интерфейсы, реализованные объектом Class. Метод getSuperclass() возвращает непосредственный (то есть ближайший) базовый класс для объекта Class.
Метод newInstance() фактически реализует «виртуальный конструктор». Вы как бы говорите: «Я не знаю ваш точный тип, так что создайте себя самостоятельно». В рассмотренном примере ссылка up просто указывает на объект Class, больше никакой информации о типе у вас нет. Поэтому при создании нового экземпляра методом newInstance() вы получаете ссылку на обобщенный объект Object. Однако полученная ссылка на самом деле указывает на объект Toy. Следовательно, перед посылкой сообщений, характерных для класса Toy, придется провести нисходящее преобразование. Вдобавок объект, созданный с помощью метода newInstance(), обязан определить конструктор по умолчанию. Позднее в этой главе будет показано, как динамически создать объект класса любым конструктором с использованием механизма рефлексии Java.

Литералы class

В Java существует еще один способ получения ссылок на объект Class — посредством литерала class. В предыдущей программе получение ссылки выглядело бы так:

FancyToy.class:

Такой способ не только проще, но еще и безопасней, поскольку проверка осуществляется еще во время компиляции. К тому же он не требует вызова forName(), а значит, является более эффективным.
Литералы class работают со всеми обычными классами, так же как и с интерфейсами, массивами и даже с примитивами. Вдобавок во всех классах-обертках для примитивных типов имеется поле с именем TYPE. Это поле содержит ссылку на объект Class для ассоциированного с ним простейшего типа, как показано в табл. 13.1.
Таблица 13.1. Альтернативное обозначение объекта Class с помощью литералов
Литерал
Ссылка на объект Class

boolean.class	Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

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

Интересно заметить, что создание ссылки на объект Class с использованием записи .class не приводит к автоматической инициализации объекта Class. Подготовка класса к использованию состоит из трех этапов:


  • Загрузка — выполняется загрузчиком классов. Последний находит байт- код и создает на его основе объект Class.
  • Компоновка — в фазе компоновки проверяется байт-код класса, выделяется память для статических полей, и при необходимости разрешаются все ссылки на классы, созданные этим классом.
  • Инициализация — если у класса имеется суперкласс, происходит его инициализация, выполняются статические инициализаторы и блоки статической инициализации.

Инициализация откладывается до первой ссылки на статический метод (конструкторы являются статическими методами) или на неконстантное статическое поле:

//: typeinfo/ClassInitialization.java
import java.util.*;
 
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
 
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
 
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
 
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization:
System.out.println(Initable.staticFinal);
// Does trigger initialization:
System.out.println(Initable.staticFinal2);
// Does trigger initialization:
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}

<spoiler text="Output:">

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

</spoiler>
По сути, инициализация откладывается настолько, насколько это возможно. Из результатов видно, что простое использование синтаксиса .class для получения ссылки на класс не приводит к выполнению инициализации. С другой стороны, вызов Class.forNames() немедленно инициализирует класс для получения ссылки на Class, как мы видим на примере initable3.

Параметризованные ссылки

Объект Class используется для создания экземпляров класса и содержит полный код методов этих экземпляров. Кроме того, в нем содержатся статические члены класса. Таким образом, ссылка на Class подразумевает точный тип того, на что она указывает — на объект класса Class.
Однако проектировщики Java SE5 решили предоставить возможность уточнения записи посредством ограничения типа объекта Class, на который может указывать ссылка; для этой цели применяется синтаксис параметризации. В следующем примере верны оба варианта синтаксиса:

//: typeinfo/GenericClassReferences.java
 
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // To же самое
intClass = double.class;
// genericIntClass = double.class; // Недопустимо
}
}

Если обычная ссылка на класс может быть связана с любым объектом Class, параметризованная ссылка может связываться только с объектами типа, указанного при ее объявлении. Синтаксис параметризации позволяет компилятору выполнить дополнительную проверку типов.

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

В Java SE5 также появился новый синтаксис преобразования ссылок на Class, основанный на методе cast():

//: typeinfo/ClassCasts.java
class Building {}
class House extends Building {}
 
public class ClassCasts {
public static void main(String[] args) {
Building b = new House();
Class<House> houseType = House.class;
House h = houseType.cast(b);
h = (House)b; // ... or just do this.
}
}

Метод cast() получает объект-аргумент и преобразует его к типу ссылки на Class. Конечно, при взгляде на приведенный код может показаться, что он занимает слишком много места по сравнению с последней строкой main(), которая делает то же самое. Новый синтаксис преобразования полезен в тех ситуациях, когда обычное преобразование невозможно. Обычно это происходит тогда, когда при написании параметризованного кода (см. далее) ссылка на Class сохраняется для преобразования в будущем. Подобные ситуации встречаются крайне редко — во всей библиотеке Java SE5 cast() используется всего один раз (в com.sun.mirror.util.DeclarationFilter).
Другая новая возможность — Class.asSubclass() — вообще не встречается в библиотеке Java SE5. Этот метод позволяет преобразовать объект класса к более конкретному типу.

Проверка перед приведением типов

Итак, мы рассмотрели следующие формы RTTI:


  • Классическое преобразование; аналог выражения «(Shape)», которое проверяет, «законно» ли приведение типов в данной ситуации, и в случае неверного преобразования возбуждает исключение ClassCastException.
  • Объект Class, представляющий тип вашего объекта. К объекту Class можно обращаться для получения полезной информации во время выполнения программы.

В языке C++ классическая форма типа «(Shape)» вообще не задействует RTTI. Она просто сообщает компилятору, что необходимо обращаться с объектом как с новым типом. В языке Java, который при приведении проверяет соответствие типов, такое преобразование часто называют «безопасным нисходящим приведением типов». Слово «нисходящее» используется в силу традиций, сложившихся в практике составления диаграмм наследования. Если приведение окружности Circle к фигуре Shape является восходящим, то приведение фигуры Shape к окружности Circle является, соответственно, нисходящим. Поскольку компилятор знает, что Circle является частным случаем Shape, он позволяет использовать «восходящее» присваивание без явного преобразования типа. Тем не менее, получив некий объект Shape, компилятор не может быть уверен в том, что он получил: то ли действительно Shape, то ли один из производных типов (Circle, Square или Triangle). На стадии компиляции он видит только Shape и поэтому не позволит использовать «нисходящее» присваивание без явного преобразования типа.

Существует и третья форма RTTI в Java — ключевое слово instanceof, которое проверяет, является ли объект экземпляром заданного типа. Результат возвращается в логическом (boolean) формате, поэтому вы просто «задаете» вопрос в следующей форме:

іf(х instanceof Dog) ((Dog)x).bark().

Команда if сначала проверяет, принадлежит ли объект к классу Dog, и только после этого выполняет приведение объекта к типу Dog. Настоятельно рекомендуется использовать ключевое слово instanceof перед проведением нисходящего преобразования, особенно при недостатке информации о точном типе объекта; иначе возникает опасность исключения ClassCastException.
Обычно проводится поиск одного определенного типа (например, поиск треугольников среди прочих фигур), но с помощью ключевого слова instanceof легко можно идентифицировать все типы объекта. Предположим, что у нас есть иерархия классов для описания домашних животных Pet (и их владельцев — эта особенность пригодится нам в более позднем примере). Каждое существо (Individual) в этой иерархии обладает идентификатором id и необязательным именем.

//: typeinfo/pets/Individual.java
package typeinfo.pets;
 
public class Individual implements Comparable<Individual> {
private static long counter = 0;
private final long id = counter++;
private String name;
public Individual(String name) { this.name = name; }
// 'name' is optional:
public Individual() {}
public String toString() {
return getClass().getSimpleName() +
(name == null ? "" : " " + name);
}
public long id() { return id; }
public boolean equals(Object o) {
return o instanceof Individual &&
id == ((Individual)o).id;
}
public int hashCode() {
int result = 17;
if(name != null)
result = 37 * result + name.hashCode();
result = 37 * result + (int)id;
return result;
}
public int compareTo(Individual arg) {
// Compare by class name first:
String first = getClass().getSimpleName();
String argFirst = arg.getClass().getSimpleName();
int firstCompare = first.compareTo(argFirst);
if(firstCompare != 0)
return firstCompare;
if(name != null && arg.name != null) {
int secondCompare = name.compareTo(arg.name);
if(secondCompare != 0)
return secondCompare;
}
return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
}
}

В данный момент код Individual нас не интересует — достаточно знать, что объект можно создавать с именем или без, и у каждого объекта Individual имеется метод id(), возвращающий уникальный идентификатор. Также имеется метод toString(); если имя не указано, toString() выдает имя типа. Иерархия классов, производных от Individual:

//typeinfo/pets/Person.java 
package typeinfo.pets;
public class Person extends Individual {
public Person(String name) { super(name); }
}
//typeinfo/pets/Pet.java 
package typeinfo.pets;
public class Pet extends Individual {
public Pet(String name) { super(name); }
public Pet () { super(); }
}
//typeinfo/pets/Dog.java 
package typeinfo.pets;
public class Dog extends Pet {
public Dog(String name) { super(name); }
public Dog() { super(); }
}
//typeinfo/pets/Cat.java 
package typeinfo.pets;
public class Cat extends Pet {
public Cat(String name) { super(name); }
public Cat() { super();}
}
//typeinfo/pets/Rodent.java
package typeinfo.pets;
public class Rodent extends Pet {
public Rodent(String name) { super(name); }
public Rodent() { super(); }
}
//typeinfo/pets/Pug.java
package typeinfo.pets;
public class Pug extends Dog {
public Pug(String name) { super(name); }
public Pug() { super(); }
}
//typeinfo/pets/Mutt.java 
package typeinfo.pets;
public class Mutt extends Dog {
public Mutt(String name) { super(name); }
public Mutt() { super(); }
}
//typeinfo/pets/EgyptianMau.java
package typeinfo.pets;
public class EgyptianMau extends Cat {
public EgyptianMau(String name) { super(name); }
public EgyptianMau() { super(); }
}
//typeinfo/pets/Manx.java
package typeinfo.pets;
public class Manx extends Cat {
public Manx(String name) { super(name); }
public Manx() { super(); }
}
//typeinfo/pets/Cymric.java
package typeinfo.pets;
public class Cymric extends Manx {
public Cymric(String name) { super(name); }
public Cymric() { super(); }
}
//typeinfo/pets/Rat.java
package typeinfo.pets;
public class Rat extends Rodent {
public Rat(String name) { super(name); }
public Rat() { super(); }
}
//typeinfo/pets/Mouse.java
package typeinfo.pets;
public class Mouse extends Rodent {
public Mouse(String name) { super(name); }
public Mouse() { super(); }
}
//typeinfo/pets/Hamster.java
package typeinfo.pets;
public class Hamster extends Rodent {
public Hamster(String name) { super(name); }
public Hamster() { super(); }
}

Затем нам понадобятся средства для создания случайных типов Pet, а для удобства — массивов и списков (List) с элементами Pet. Чтобы этот инструментарий мог «пережить» несколько разных реализаций, мы определим его в виде абстрактного класса:

// typeinfo/pets/PetCreator java 
// Создание случайных последовательностей Pet
package typeinfo.pets;
import java.util.*;
 
public abstract class PetCreator {
//инициализация генератора случайных чисел
private Random rand = new Random(47);
//абстрактный метод возвращает список возможных типов(классов) животных
public abstract List<Class<? extends Pet>> getTypes();
 
 
public Pet randomPet(){ // Создание одного случайного объекта Pet
// генерация случайного номера в диапазоне списка классов животных
int n = rand.nextInt(getTypes().size());
try {//создание нового объекта для класса с данным номером в списке классов
return getTypes().get(n).newInstance(); }
catch( InstantiationException e) {throw new RuntimeException(e); }
catch (IllegalAccessException e) {throw new RuntimeException(e);}
}
 
 
public Pet[] createArray(int size) {//создание массива случайных Pet
Pet[] result = new Pet[size];
for (int i=0; i < size; i++)
result[i] = randomPet();
return result;
}
 
public ArrayList<Pet> arrayList(int size) {// создание списка случайных Pet
ArrayList<Pet> result = new ArrayList<Pet>();
Collections.addAll(result, createArray(size));
return result;
}
}

Абстрактный метод getTypes() поручает производному классу получение списка объектов Class. В качестве типа класса указан «любой производный от Pet», поэтому newInstance() создает Pet без необходимости преобразования типа. Метод randomPet() осуществляет случайную выборку из List и использует полученные объекты Class для создания нового экземпляра данного класса вызовом Class.newInstance(). Метод createArray() использует randomPet() для заполнения массива, a arrayList(), в свою очередь, использует createArray().
При вызове newInstance() возможны два вида исключений, обрабатываемые в секциях catch за блоком try. Имена исключений достаточно хорошо объясняют суть проблемы (IllegalAccessException — нарушение механизма безопасности Java, в данном случае если конструктор по умолчанию объявлен private).
Определяя субкласс PetCreator, достаточно предоставить список типов Pet, которые должны создаваться с использованием randomPet() и других методов. Метод getTypes() возвращает ссылку на статический объект List. Реализация с использованием forName() выглядит так:

//typeinfo/pets/ForNameCreator.java
package typeinfo.pets;
import java.util.*;
 
public class ForNameCreator extends PetCreator {
 
private static List<Class<? extends Pet>> types = new ArrayList<Class<? extends Pet>>();
private static String[] typeNames = {
"typeinfo.pets.Mutt",
"typeinfo.pets.Pug",
"typeinfo.pets.EgyptianMau",
"typeinfo.pets.Manx",
"typeinfo.pets.Cymric",
"typeinfo.pets.Rat",
"typeinfo.pets.Mouse",
"typeinfo.pets.Hamster"
};
 
@SuppressWarnings("unchecked")
// загрузчик списка классов
private static void loader() {
try { //при загрузке класс приводится к типу заданному типизациии контейнера
for(String name : typeNames)
types.add((Class<? extends Pet>)Class.forName(name));
}
catch(ClassNotFoundException e)
{throw new RuntimeException(e);}
}
// статический блок ОДНОКРТАНО загружающий список при инициализации данного класса
static { loader(); }
 
@Override
public List<Class<? extends Pet>> getTypes(){return types;}
}

Метод loader() создает список List объектов Class с использованием метода Class.forName(). При этом может произойти исключение ClassNotFoundException, что вполне понятно — ведь ему передается строка, содержимое которой невоз­можно проверить на стадии компиляции. При ссылке на эти классы необходимо указывать имя пакета, которому они принадлежат (typeinfo).
Для получения типизованного списка объектов Class требуется преобразование типа, что приводит к выдаче предупреждения на стадии компиляции. Метод loader() определяется отдельно и размещается в секции статической инициализации, потому что директива @SuppressWarnings не может располагаться прямо в секции статической инициализации.
Для подсчета объектов Pet нам понадобится механизм подсчета их разных видов. Для этой цели идеально подойдет карта (Мар), в которой ключами являются имена типов Pet, а значениями — переменные Integer с количеством Pet. Например, это позволит получать ответы на вопросы типа «сколько существует объектов Hamster?». При подсчете Pet будет использоваться ключевое слово instanceof:

//typeinfo/PetCount.java
// Использование instanceof
package typeinfo;
 
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
public class PetCount {
 
static class PetCounter extends HashMap<String,Integer> {
public void count(String type) {
Integer quantity = get(type);
if(quantity == null)
put(type, 1);
else
put(type, quantity +1);
}
}
 
public static void countPets(PetCreator creator) {
PetCounter counter= new PetCounter();
for(Pet pet : creator.createArray(20)) { // Подсчет всех объектов Pet:
printnb(pet.getClass().getSimpleName() + " ");
 
if(pet instanceof Pet) counter.count("Pet");
if(pet instanceof Dog) counter.count("Dog");
if(pet instanceof Mutt) counter.count("Mutt");
if(pet instanceof Pug) counter.count("Pug");
if(pet instanceof Cat) counter.count("Cat");
if(pet instanceof Manx) counter.count("EgyptіanMau");
if(pet instanceof Manx) counter.count("Manx");
if(pet instanceof Manx) counter.count("Cymric");
if(pet instanceof Rodent) counter.count("Rodent");
if(pet instanceof Rat) counter.count("Rat");
if(pet instanceof Mouse) counter.count("Mouse");
if(pet instanceof Hamster) counter.count("Hamster");
}
// Вывод результатов подсчета.
print();
print(counter);
}
public static void main(String[] args) {
//countPets(new ForNameCreator());
countPets(new LiteralPetCreator());
}
}

<spoiler text="Output:">

Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt
Mutt Cymric Mouse Pug Mouse Cymric
{Pug=3. Cat=9, Hamster=l, Cymric=7, Mouse=2, Mutt=3, Rodent=5, Pet=20, Manx=7,
EgyptianMau=7, Dog=6, Rat=2}

</spoiler>
В countPets массив случайным образом заполняется объектами Pet с использованием PetCreator. Затем каждый объект Pet в массиве тестируется и подсчитывается при помощи instanceof.
У ключевого слова instanceof имеется одно серьезное ограничение: объект можно сравнивать только с именованным типом, но не с объектом Class. Возможно, вам показалось, что в предыдущем примере перебор всех выражений instanceof выглядит неудобно и громоздко, и вы правы. Тем не менее автоматизировать этот процесс невозможно — создать из объектов Class список ArrayList и сравнивать объекты по очереди с каждым его элементом не получится (к счастью, сущест­вует альтернативное решение, но это попозже). Впрочем, особенно горевать по этому поводу не стоит — если вам приходится записывать множество проверок instanceof, скорее всего, изъян кроется в архитектуре программы.

Использование литералов class

Если записать пример PetCreator.java с использованием литералов class, программа во многих отношениях становится более понятной:

//• typeinfo/pets/LiteralPetCreator.java 
// Using class literals
package typeinfo.pets;
import java.util.*;
public class LiteralPetCreator extends PetCreator {
 
@SuppressWarnings("unchecked")
// список ВСЕХ возможных типов животных непосредственно инициализированный литералами классов
//сделан неизменяемым ни по размеру ни по значениям
public static final List<Class<? extends Pet>> allTypes =
Collections.unmodifiableList(//метод создает "read-only" коллекцию
Arrays.asList(
Pet.class, Dog.class, Cat.class, Rodent.class, Mutt.class,
Pug.class, EgyptianMau.class, Manx.class, Cymric.class,
Rat.class, Mouse.class, Hamster.class
)); // Типы для случайного создания:
// а это уже "рабочий список" типов животных ))
private static final List<Class<? extends Pet>> types =
allTypes.subList(allTypes.indexOf(Mutt.class),allTypes.size());
@Override
public List<Class<? extends Pet>> getTypes() {return types;}
 
public static void main(String[] args) {
System.out.println(types);
}
}

<spoiler text="Output:">

[class typeinfo pets.Mutt, class typeinfo.pets.Pug, class typeinfo.pets.EgyptianMau. class
typeinfo pets Manx, class typeinfo.pets.Cymric, class typeinfo.pets.Rat, class
typeinfo.pets.Mouse, class typeinfo.pets.Hamster]

</spoiler>
В будущем примере PetCount3.java контейнер Map заполняется всеми типами Pet (не только генерируемыми случайным образом), поэтому нам понадобился список allTypes. Список types представляет собой часть allTypes (создается вызо­вом List.subList()) со всеми типами Pet, поэтому он используется для случайного генерирования Pet.
На этот раз при создании types блок try не нужен, так как необходимые проверки типов проводятся еще во время компиляции и исключения не возбуждаются, в отличие от метода Class.forName().
Теперь библиотека typeinfo.pets содержит две реализации PetCreator. Чтобы вторая реализация использовалась по умолчанию, мы можем создать фасад (facade), использующий LiteralPetCreator:

//• typeinfo/pets/Pets.java
// Фасад для получения PetCreator по умолчанию
package typeinfo.pets;
import java.util.*;
 
public class Pets {
public static final PetCreator creator = new LiteralPetCreator();
public static Pet randomPet() {return creator.randomPet();};
 
public static Pet[] createArray(int size)
{return creator.createArray(size);}
public static ArrayList<Pet> arrayList(int size)
{return creator.arrayList(size);}
}

При этом также обеспечиваются косвенные вызовы randomPet(), createArray() и arrayList().
Поскольку PetCount.countPets() получает аргумент PetCreator, мы можем легко проверить работу LiteralPetCreator (через представленный фасад):

// typeinfo/PetCount2.java 
package typeinfo;
import typeinfo.pets.*;
 
public class PetCount2 {
 
public static void main(String[] args) {
PetCount.countPets(Pets.creator);
}
}
/* (Выполните, чтобы увидеть результат) */
//:- Результат будет таким же, как у PetCount.java.

Динамический вызов instanceof

Метод Class.isInstance() позволяет выполнить динамическую проверку типа объекта. Благодаря ему в примере PetCount.java наконец-то можно будет избавиться от нагромождения instanceof:

// typeinfо/PetCount3.java
// Using isInstance()
package typeinfo;
 
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class PetCount3 {
 
static class PetCounter extends LinkedHashMap<Class<? extends Pet>,Integer> {
public PetCounter() {
super(MapData.map(LiteralPetCreator.allTypes, 0));
}
 
public void count(Pet pet) {
// Class.isInstance() избавляет от множественных instanceof:
for(Map.Entry<Class<? extends Pet>,Integer> pair: entrySet())
if(pair.getKey().isInstance(pet))
put(pair.getKey(), pair.getValue() + 1);
}
 
@Override
public String toString() {
 
StringBuilder result = new StringBuilder("{");
for(Map.Entry<Class<? extends Pet>,Integer> pair : entrySet()) {
result.append(pair.getKey().getSimpleName());
result.append("=");
result.append(pair.getValue());
result.append(", ");
}
result.delete(result.length() -2, result.length());
result.append("J");
return result.toString();
}
}
 
public static void main(String[] args) {
PetCounter petCount = new PetCounter();
for(Pet pet : Pets.createArray(20)) {
printnb(pet.getClass().getSimpleName() + " "); petCount.count(pet);
}
print();
print(petCount);
 
}
}

<spoiler text="Output:">

Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt 
Mutt Cymric Mouse Pug Mouse Cymric
{Pet=20, Dog=6. Cat-9. Rodent=5, Mutt-3. Pug=3. EgyptianMau=2, Manx=7, Cymric=5, Rat-2.
Mouse=2, Hamster=l}

</spoiler>
Для подсчета всех разновидностей Pet контейнер PetCounter заполняется типами из LiteralPetCreator.allTypes. При этом используется класс net.mindview.util.MapData, который получает Iterable (allTypesList) и константу (0 в данном случае) и заполняет Map ключами из allTypes со значениями 0. Без предварительного заполнения Map будут подсчитаны только случайно сгенерированные типы, но не базовые типы (такие, как Pet и Cat).
Как видите, метод isInstance() избавил нас от необходимости нагромождать конструкции с instanceof. Вдобавок теперь в программу можно легко добавить новые типы Pet — для этого следует просто изменить массив LiteralPet Creator.types; остальная часть программы не потребует правки (которая была бы неизбежна с операторами instanceof).
Метод toString() был перегружен для получения удобочитаемого вывода.

Рекурсивный подсчет

Контейнер Мар в PetCount3.PetCounter был заполнен всеми классами Pet. Вместо предварительного заполнения карты мы также можем воспользоваться методом Class.isAssignableFrom() и создать обобщенный инструмент подсчета, не ограни­ченный подсчетом Pet:

//: net/mindview/util/TypeCounter.java
// Подсчет экземпляров в семействе типов
package net.mindview.util;
import java.util.*;
 
public class TypeCounter extends HashMap<Class<?>,Integer>{
private Class<?> baseType;
public TypeCounter(Class<?> baseType) {
this.baseType = baseType;
}
public void count(Object obj) {
Class<?> type = obj.getClass();
if(!baseType.isAssignableFrom(type))
throw new RuntimeException(obj + " incorrect type: "
+ type + ", should be type or subtype of "
+ baseType);
countClass(type);
}
private void countClass(Class<?> type) {
Integer quantity = get(type);
put(type, quantity == null ? 1 : quantity + 1);
Class<?> superClass = type.getSuperclass();
if(superClass != null &&
baseType.isAssignableFrom(superClass))
countClass(superClass);
}
public String toString() {
StringBuilder result = new StringBuilder("{");
for(Map.Entry<Class<?>,Integer> pair : entrySet()) {
result.append(pair.getKey().getSimpleName());
result.append("=");
result.append(pair.getValue());
result.append(", ");
}
result.delete(result.length()-2, result.length());
result.append("}");
return result.toString();
}
}

Метод count() получает Class для своего аргумента, а затем использует .isAssignableFrom() для проверки принадлежности объекта к интересующей вас иерархии. Метод countClass() сначала производит подсчет для точного типа класса, а затем, если baseType допускает присваивание из суперкласса, рекурсивно вызывает countClass() для суперкласса.

//: typeinfo/PetCount4.java
import typeinfo.pets.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class PetCount4 {
public static void main(String[] args) {
TypeCounter counter = new TypeCounter(Pet.class);
for(Pet pet : Pets.createArray(20)) {
printnb(pet.getClass().getSimpleName() + " ");
counter.count(pet);
}
print();
print(counter);
}
}

<spoiler text="Output:"> (Пример)

Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt Mutt 
Cymric Mouse Pug Mouse Cymric
{Mouse=2, Dog=6, Manx=7, EgyptianMau=2, Rodent=5, Pug=3, Mutt=3. Cymric=5, Cat=9. Hamster=l,
Pet=20, Rat=2}

</spoiler>
Как видно из результатов, подсчитываются как базовые, так и конкретные типы.

Регистрация фабрик

У построения объектов иерархии Pet есть один недостаток: каждый раз, когда в иерархию включается новый тип Pet, вы должны добавить его в LiteralPetCreator.java. В системах с регулярным добавлением новых классов это может создать проблемы.
Первое, что приходит в голову, — добавить в каждый класс статический инициализатор, который добавлял бы свой класс в некий список. К сожалению, статические инициализаторы вызываются только при первой загрузке класса, поэтому возникает «порочный круг»: класс отсутствует в списке генератора, поэтому генератор не может создать объект этого класса, соответственно, класс не загрузится и не будет помещен в список.
По сути, вы вынуждены создать список вручную (разве что вы напишете утилиту, которая будет анализировать исходный код, а затем создавать и компилировать список). Вероятно, лучшее, что можно сделать, — это разместить список в одном централизованном, очевидном месте. Вероятно, лучшим местом для него будет базовый класс иерархии.
В этом разделе мы также внесем другое изменение: создание объекта будет передано самому классу с использованием паттерна «метод-фабрика». Метод-фабрика может вызываться полиморфно и создает объект соответствующего типа. В следующей упрощенной версии методом-фабрикой является метод create() интерфейса Factory:

//: typeinfo/factory/Factory.java
package typeinfo.factory:
public interface Factory<T> { T create(); }

Обобщенный параметр T позволяет create() возвращать разные типы для разных реализаций Factory. Также при этом используется ковариантность возвращаемых типов.
В следующем примере базовый класс Part содержит список объектов-фабрик. Фабрики типов, которые должны создаваться методом createRandom(), «регистрируются» в базовом классе включением в список partFactories:

package typeinfo;
import typeinfo.factory.*;
import java.util.*;
 
class Part {
 
@Override
public String toString() {return getClass().getSimpleName(); }
 
static List<Factory<? extends Part>>partFactories
= new ArrayList<Factory<? extends Part>>();
 
static {
// При вызове Collections addAll() выдается предупреждение
// "unchecked generic array creation for varargs parameter"
partFactories.add(new FuelFilter.Factory());
partFactories.add(new AirFilter.Factory());
partFactories.add(new CabinAirFilter.Factory());
partFactories.add(new OilFilter.Factory());
partFactories.add(new FanBelt.Factory());
partFactories.add(new PowerSteeringBelt.Factory());
partFactories.add(new GeneratorBelt.Factory());
}
private static Random rand = new Random(47);
public static Part createRandom() {
int n = rand.nextInt(partFactories.size());
return partFactories.get(n).create();
}
}
class Filter extends Part {}
 
class FuelFilter extends Filter {
// Создание фабрики для каждого конкретного типа
public static class Factory implements typeinfo.factory.Factory<FuelFilter> {
@Override
public FuelFilter create() { return new FuelFilter ();}
}
}
 
class AirFilter extends Filter {
public static class Factory implements typeinfo.factory.Factory<AirFilter> {
@Override
public AirFilter create() { return new AirFilter(); }
}
}
 
class CabinAirFilter extends Filter {
public static class Factory implements typeinfo.factory.Factory<CabinAirFilter> {
@Override
public CabinAirFilter create() {return new CabinAirFilter();}
}
}
 
class OilFilter extends Filter {
public static class Factory implements typeinfo.factory.Factory<OilFilter> {
@Override
public OilFilter create() { return new OilFilter(); }
}
}
 
class Belt extends Part {}
 
class FanBelt extends Belt {
public static class Factory implements typeinfo.factory.Factory<FanBelt> {
@Override
public FanBelt create() { return new FanBelt(); }
}
}
 
class GeneratorBelt extends Belt {
public static class Factory implements typeinfo.factory.Factory<GeneratorBelt> {
@Override
public GeneratorBelt create() {return new GeneratorBelt();}
}
}
 
class PowerSteeringBelt extends Belt {
public static class Factory implements typeinfo.factory.Factory<PowerSteeringBelt> {
@Override
public PowerSteeringBelt create() {return new PowerSteeringBelt();}
}
}
 
public class RegisteredFactories {
 
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
System.out.println(Part.createRandom());
}
}

<spoiler text="Output:">

GeneratorBelt CabinAirFilter GeneratorBelt AirFilter PowerSteeringBelt CabinAirFilter Fuel 
Filter PowerSteeringBelt PowerSteeringBelt Fuel Filter

</spoiler>
He все классы иерархии рассчитаны на создание экземпляров; в нашем примере классы Filter и Belt существуют исключительно в целях классификации. Экземпляры этих классов не создаются — только одного из их субклассов. Если класс должен создаваться посредством createRandom(), он содержит внутренний класс Factory.
Хотя для включения всех фабрик в список можно воспользоваться вызовом Collections.addAll(), компилятор выдает предупреждение, поэтому я вернулся к вызовам add(). Метод createRandom() случайным образом выбирает объект фабрики из partFactories и вызывает его метод create() для получения нового объекта Part.

instanceof и сравнение Class

При получении информации о типе объекта важно различать действие любой формы оператора instanceof (будь это сам оператор instanceof или метод isInstance() - они дают одинаковые результаты) и прямого сравнения объектов Class. Вот пример, который показывает, в чем их различия:

//: typeinfo/FamilyVsExactType.java
// The difference between instanceof and class
package typeinfo;
import static net.mindview.util.Print.*;
 
class Base {}
class Derived extends Base {}
 
public class FamilyVsExactType {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof Base " + (x instanceof Base));
print("x instanceof Derived "+ (x instanceof Derived));
print("Base.isInstance(x) "+ Base.class.isInstance(x));
print("Derived.isInstance(x) " +
Derived.class.isInstance(x));
print("x.getClass() == Base.class " +
(x.getClass() == Base.class));
print("x.getClass() == Derived.class " +
(x.getClass() == Derived.class));
print("x.getClass().equals(Base.class)) "+
(x.getClass().equals(Base.class)));
print("x.getClass().equals(Derived.class)) " +
(x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}

<spoiler text="Output:">

Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true

</spoiler>
Метод test() осуществляет проверку типов полученного объекта, используя для этого обе формы оператора instanceof. Затем он получает ссылку на объект Class и использует операцию сравнения ссылок == и метод equals(), чтобы прове­рить объекты Class на эквивалентность. Пример доказывает справедливость утверждения о том, что действие оператора instanceof и метода islnstance() одинаково. Совпадают и результаты работы операции сравнения == и метода equals(). Но сами тесты приводят к разным заключениям. В соответствии с концепцией типа instanceof дает ответ на вопрос: «Объект принадлежит этому классу или производному от него?» С другой стороны, сравнение объектов Class оператором == не затрагивает наследования — либо тип точно совпадает, либо нет.

Рефлексия: динамическая информация о классе

Если вы не знаете точный тип объекта, RTTI сообщит вам его. Однако в этом случае существуют ограничения: тип должен быть известен еще во время компиляции программы, иначе определить его с помощью RTTI и сделать с этой информацией что-то полезное будет невозможно. Другими словами, компилятор должен располагать информацией обо всех классах, к которым вы затем хотели бы применить динамическое определение типов (RTTI).
Сначала кажется, что это ограничение не столь существенно, но предположим, что у вас появилась ссылка на объект, который не находится в пространстве вашей программы. Более того, класс этого объекта недоступен во время ее компиляции. Например, вы получили последовательность байтов с диска или из сетевого соединения, и вам сказали, что эта последовательность представляет некоторый класс. Но компилятор ничего не знал об этом классе, когда обрабатывал вашу программу, как же его можно использовать?
В традиционных средах программирования такая задача показалась бы далекой от реальности. Однако границы мира программирования все больше расширяются и мы все чаще встречаемся с такими ситуациями. Во-первых, такие возможности требуются для компонентного программирования, которое служит основой для систем быстрой разработки приложений (Rapid Application Development, RAD). Это визуальный подход для создания программ (экран представлен в виде «формы»), где значки, представляющие визуальные компоненты, перетаскиваются на форму. Затем происходит настройка этих компонентов, они устанавливаются в некоторое состояние во время работы программы. Чтобы изменить состояние компонентов, необходимо некоторым образом создавать их экземпляры, просматривать их содержимое, считывать и записывать внутренние значения. Вдобавок компоненты с поддержкой событий графического интерфейса должны как-то рассказать о них, чтобы система быстрой разработки приложений помогла программисту реализовать поддержку этих событий. Механизм рефлексии предоставляет средства для получения информации о доступных методах и их именах. Такое компонентное программирование поддерживается и в Java, с помощью технологии JavaBeans.
Другая важная предпосылка поддержки динамической информации о классе — предоставление возможности создавать и использовать объекты на удаленных платформах. Этот механизм, называемый удаленным вызовом методов (Remote Method Invocation, RMI), позволяет программе на Java распределять свои объекты по нескольким машинам. Необходимость в удаленном вызове методов возникает по разным причинам: например, при выполнении задачи с интенсивными вычислениями можно сбалансировать нагрузку по доступным компьютерам. Иногда код, выполняющий определенные операции, размещается на одной машине, чтобы она стала общим хранилищем этих операций и любые изменения кода на такой машине автоматически распространялись на всех клиентов этого кода. (Интересный поворот — компьютер существует исключительно для того, чтобы упростить внесение изменений в программное обеспечение!) Ко всему прочему распределенное программирование также поддерживает удаленное специализированное оборудование, которое эффективно выполняет некоторые задачи — например, обращение матриц, — которые при решении их на локальной машине могут потребовать слишком много времени и ресурсов.
Класс Class (уже описанный в этой главе) поддерживает концепцию рефлексии (reflection), для которой существует дополнительная библиотека java.lang.reflect, состоящая из классов Field, Method и Constructor (каждый реализует интерфейс Member). Объекты этих классов создаются JVM, чтобы представлять соответствующие члены неизвестного класса. Объекты Constructor используются для создания новых объектов класса, методы get() и set() — для чтения и записи значений полей класса, представленных объектами Field, метод invoke() — для вызова метода, представленного объектом Method. Вдобавок в классе Class имеются удобные методы getFields(), getMethods() и getConstructors(), которые возвращают массивы таких объектов, как поля класса, его методы и конструкторы. (За подробной информацией обращайтесь к описанию класса Class в электронной документации JDK.) Таким образом, информация о неизвестном объекте становится доступной прямо во время выполнения программы, а потребность в ее получении ко времени компиляции программы отпадает.
Важно понимать, что в механизме рефлексии нет ничего сверхъестественного. Когда вы используете рефлексию для работы с объектом неизвестного типа, виртуальная машина JVM рассматривает его и видит, что он принадлежит оп­ределенному классу (это делает и обычное RTTI), но, перед тем как проводить с ним некоторые действия, необходимо загрузить соответствующий объект Class. Таким образом, файл .class для класса этого объекта должен быть доступен JVM либо в сети, либо в локальной системе. Таким образом, истинное различие между традиционным RTTI и рефлексией состоит в том, что при использовании RTTI файл .class открывается и анализируется компилятором. Другими словами, вы можете вызывать методы объекта «нормальным» способом. При использовании рефлексии файл .class во время компиляции недоступен; он открывается и обрабатывается системой выполнения.

Извлечение информации о методах класса

Рефлексия редко используется напрямую; она существует в языке в основном для поддержки других возможностей, таких как сериализация объектов и компоненты JavaBeans. Однако существуют ситуации, в которых динамическая информация о классе просто незаменима.
Для примера возьмем программу, выводящую на экран список методов некоторого класса. При просмотре исходного кода класса или его документации будут видны только те методы, которые были определены или переопределены именно в текущем классе. Но в классе может быть еще множество методов, доступных из его базовых классов. Искать их и сложно, и долго[28]. К счастью, рефлексия позволяет написать простой инструмент, выводящий полную информацию о полном интерфейсе класса. Вот как он работает:

//: typeinfo/ShowMethods.java
// Использование рефлексии для вывода полного списка методов
// класс, в том числе и определенных в базовом классе.
// {Args: ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
import static net.mindview.util.Print.*;
 
public class ShowMethods {
 
private static String usage = "usage:\n"
+ "ShowMethods qualified.class.name\n"
+ "To show all methods in class or:\n"
+ "ShowMethods qualified.class.name word\n"
+ "To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\\w+\\.");
 
public static void main(String[] args) {
if(args.length < 1) {
print(usage);
System.exit(0);
}
int lines = 0;
try {
 
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if(args.length == 1) {
 
for(Method method : methods)
print(p.matcher(method.toString()).replaceAll(""));
 
for(Constructor ctor : ctors)
print(p.matcher(ctor.toString()).replaceAll(""));
 
lines = methods.length + ctors.length;
 
} else {
 
for(Method method : methods)
if (method.toString().indexOf(args[1]) != -1) {
print(p.matcher(method.toString()).replaceAll(""));
lines++;
}
 
for(Constructor ctor : ctors)
if(ctor.toString().indexOf(args[1]) != -1) {
print(p.matcher(ctor.toString()).replaceAll(""));
lines++;
}
}
}catch(ClassNotFoundException e) {print("No such class: " + e);}
}
}

<spoiler text="Output:">

public static void main(String[]) public native int hashCodeO public final native Class getClass()
public final void wait(long.int) throws InterruptedException public final void wait() throws
InterruptedException public final native void wait(long) throws InterruptedException public
boolean equals(Object) public String toString() public final native void notifyО public final
native void notifyAll() public ShowMethods()

</spoiler>
Методы класса Class.getMethods() и getConstructors() возвращают массивы объектов Method и Constructor, которые представляют методы и конструкторы класса. В каждом из этих классов есть методы для получения и анализа имен, аргументов и возвращаемых значений представляемых методов и конструкторов. Впрочем, также можно использовать простой метод toString(), как и сделано здесь, чтобы получить строку с полным именем метода. Остальная часть кода разбирает командную строку и определяет, подходит ли определенное выражение образцу для поиска (с использованием indexOf()), а после выделяет описатели имен классов.
Результат, полученный от Class.forName(), не может быть известен во время компиляции, поэтому вся информация о сигнатуре методов становится доступной во время выполнения. Если вы тщательно изучите документацию по рефлексии из JDK, то увидите, что рефлексия позволяет установить необходимые аргументы и вызвать метод объекта, «абсолютно неизвестного» во время компиляции программы (чуть позже будут приведены соответствующие примеры). Скорее всего, вам эти возможности никогда не понадобятся, но сам факт их существования интересен.

Приведенный выше результат был получен из командной строки

java ShowMethods ShowMethods

На экран выводится открытый (public) конструктор по умолчанию, хотя в тексте программы такой конструктор не определяется. Тот конструктор, что имеется теперь в классе, автоматически сгенерирован компилятором. Если вы после этого сделаете класс ShowMethods не открытым (удалите из его определения спецификатор доступа public, то есть предоставите ему доступ в пределах пакета), сгенерированный компилятором конструктор исчезнет из списка методов. Сгенерированный конструктор имеет тот же уровень доступа, что и его класс.

Также интересно запустить программу в виде

java ShowMethods java.lang.String

с передачей дополнительного параметра char, int, String и т. п.
Эта программа сэкономит вам немало времени при программировании, когда вы будете мучительно вспоминать, есть ли у этого класса определенный метод, если вам потребуется узнать, имеются ли у некоторого класса методы, воз­вращающие объекты Color, и т. д.

Динамические посредники

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

//. typeinfo/SimpleProxyDemo.java
package typeinfo;
import static.net.mindview.util.Print.*;
interface Interface {
void doSomething();
void somethingElse(String arg);
}
 
class RealObject implements Interface {
@Override
public void doSomething() {print("doSomething"); }
@Override
public void somethingElse(String arg) { print("somethingElse " + arg);}
}
 
class SimpleProxy implements Interface {
private Interface proxied;
public SimpleProxy(Interface proxied) { this.proxied = proxied;}
 
@Override
public void doSomething() {
print("SimpleProxy doSomething");
proxied.doSomething();
}
@Override
public void somethingElse(String arg) {
print("SimpleProxy somethingElse " + arg);
proxied.somethingElse(arg);
}
}
class SimpleProxyDemo {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}
public static void main(String[] args) {
consumer(new RealObject());
consumer(new SimpleProxy(new RealObject()));
}
}

<spoiler text="Output:">

doSomething somethingElse bonobo SimpleProxy doSomething doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo

</spoiler>
Поскольку consumer() получает Interface, он не знает, что ему передается — «настоящий» объект (RealObject) или посредник (Proxy), потому что оба типа реализуют Interface. Объект Proxy, находящийся между клиентом и «настоящим» объектом, выполняет операции, а затем вызывает идентичные методы RealObject.
Посредник пригодится в любой ситуации, когда требуется отделить дополнительные операции от «настоящего» объекта, и особенно когда нужно легко переключаться из режима использования дополнительных операций в режим отказа от них (и наоборот — главной целью паттернов является инкапсуляция изменений, поэтому для оправдания их применения что-то должно изменяться). Допустим, вы хотите отслеживать вызовы методов RealObject, измерять затраты на эти вызовы, и т. д. Такой код не должен встраиваться в приложение, а посредник позволит легко добавить или убрать его по мере необходимости.
Динамические посредники Java развивают концепцию посредника — и объект посредника создается динамически, и обработка вызовов опосредованных методов тоже осуществляется динамически. Все вызовы, обращенные к динамиче­скому посреднику, перенаправляются одному обработчику, который определяет, что это за вызов и как с ним следует поступить. Вот как выглядит пример SimpleProxyDemo.java, переписанный для динамического посредника:

// typeinfo/SimpleDynamicProxy.java
package typeinfo;
import java.lang.reflect.*;
 
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
 
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println ("**** proxy. " + proxy.getClass()
+ ", method- " + method + ", args " + args);
if(args != null)
for(Object arg : args)
System.out.println(" " + arg);
 
return method.invoke(proxied, args);
}
}
 
class SimpleDynamicProxy {
 
public static void consumer(Interface іface) {
іface.doSomething();
іface.somethingElse("bonobo");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
// Вставляєм посредника и вызываем снова:
Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{Interface.class},
new DynamicProxyHandler(real));
consumer(proxy);
}
}

<spoiler text="Output:">

**** proxy: class SProxy(). method: public abstract void Interface.doSomething(), args: null
doSomething
**** proxy: class SProxy(). method: public abstract void
Interface.somethingElse(java.1ang.String),
args: [Ljava.1ang.Object.@42e816
bonobo
somethingElse bonobo

</spoiler>
Динамический посредник создается вызовом статического метода Proxy.newProxyInstance(), которому должен передаваться загрузчик класса, список интерфейсов, которые должны реализовываться посредником (а не классов или абстрактных классов!), а также реализация интерфейса InvocationHandler. Динамический посредник перенаправляет все вызовы обработчику, поэтому конструктор обработчика обычно получает ссылку на «настоящий» объект для пере­направления ему запросов.
Метод invoke() получает объект посредника на случай, если ему понадобится определить, откуда поступил запрос — впрочем, обычно это несущественно. Будьте внимательны при вызове методов посредника из invoke(), потому что вызовы через интерфейс перенаправляются через посредника.
В общем случае вы выполняете опосредованную операцию, а затем используете Method.invoke() для перенаправления запроса опосредованному объекту с передачей необходимых аргументов. При этом некоторые вызовы методов могут отфильтровываться, а другие — проходить:

//: typeinfo/SelectingMethods.java
// Looking for particular methods in a dynamic proxy.
import java.lang.reflect.*;
import static net.mindview.util.Print.*;
 
class MethodSelector implements InvocationHandler {
private Object proxied;
public MethodSelector(Object proxied) {
this.proxied = proxied;
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(method.getName().equals("interesting"))
print("Proxy detected the interesting method");
return method.invoke(proxied, args);
}
}
 
interface SomeMethods {
void boring1();
void boring2();
void interesting(String arg);
void boring3();
}
 
class Implementation implements SomeMethods {
public void boring1() { print("boring1"); }
public void boring2() { print("boring2"); }
public void interesting(String arg) {
print("interesting " + arg);
}
public void boring3() { print("boring3"); }
}
 
class SelectingMethods {
public static void main(String[] args) {
SomeMethods proxy= (SomeMethods)Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
new Class[]{ SomeMethods.class },
new MethodSelector(new Implementation()));
proxy.boring1();
proxy.boring2();
proxy.interesting("bonobo");
proxy.boring3();
}
}

<spoiler text="Output:">

boring1
boring2
Proxy detected the interesting method
interesting bonobo
boring3

</spoiler>
В данном случае мы просто проверяем имена методов, но с таким же успехом можно анализировать другие аспекты сигнатуры и даже значения аргументов.
Вряд ли вам придется каждый день пользоваться динамическими посредниками, но они хорошо подходят для решения многих разновидностей задач.

Объекты с неопределенным состоянием

Если использовать для обозначения неопределенного состояния (то есть отсутствия) объекта встроенное значение null, то при каждом использовании ссылки придется проверять, не равна ли она null. Это быстро утомляет, а код получается излишне громоздким. Проблема заключается в том, что null не имеет собственного поведения, кроме выдачи NullPointerException при попытке выполнения с ним какой-либо операции. Иногда бывает полезно ввести понятие объекта с неопределенным состоянием, который принимает сообщения, но возвращает значение, свидетельствующее об отсутствии «настоящего» объекта. Таким образом, вы можете считать, что все объекты действительны, и вам не придется тратить время на проверки null (и читать полученный код).
Было бы интересно представить себе язык программирования, автоматически создающий объекты с неопределенным состоянием, но на практике они применяются не так уж часто — иногда проверки null оказывается достаточно, иногда можно уверенно считать, что значение null вам не попадется, а иногда даже обработка аномальных ситуаций через NullPointerException является допустимой. Наибольшую пользу объекты с неопределенным состоянием приносят «вблизи от данных», представляя сущности в пространстве задачи. Простой пример: во многих системах имеется класс Person, а в коде возникают ситуации, когда объект не представляет конкретную личность (или, по крайней мере, информация о ней недоступна); при традиционном подходе вам следовало бы проверить ссылку null. Также можно воспользоваться объектом с неопределенным состоянием, но, даже несмотря на то, что такой объект будет отвечать на все сообщения, на которые отвечает «настоящий» объект, все равно потребуется способ проверки его на «определенность». Проще всего определить для этого специальный интерфейс:

//. net/mindview/uti1/Null.java 
package net.mindview.util;
public interface Null {}

Это позволяет instanceof обнаруживать объекты с неопределенным состоянием и, что еще важнее, не требует включения метода isNull() во все классы (в конце концов, это фактически будет другим способом выполнения RTTI — так почему бы сразу не воспользоваться встроенными средствами?):

// typeinfo/Person.java
// Класс с неопределенным состоянием объекта
package typeinfo;
import net.mindview.util.*;
 
class Person {
 
public final String first;
public final String last;
public final String address; // И t д.
public static final Person NULL = new NullPerson();
 
public Person(String first, String last, String address){
this.first = first;
this.last = last;
this.address = address;
}
 
@Override
public String toString() {
return "Person: " + first + " " + last + " " + address;
}
 
public static class NullPerson extends Person implements Null {
private NullPerson() {
super("None", "None", "None");
}
@Override
public String toString() { return "NullPerson"; }
}
}

В общем случае объект с неопределенным состоянием является синглетным, поэтому он создается как экземпляр static final. Это возможно благодаря тому, что объект Person неизменяем — значения задаются в конструкторе, а затем чи­таются, но не могут изменяться (поскольку поля String по своей природе неизменяемы). Если вы захотите изменить NullPerson, его придется заменить новым объектом Person. Обратите внимание: для обнаружения обобщенной поддержки Null или более конкретного типа NullPerson можно использовать instanceof, но при синглетной архитектуре можно воспользоваться просто equals() или даже == для сравнения с Person.NULL.
Представьте, что вы собираетесь открыть новое предприятие, но, пока вакансии еще не заполнены, в каждой должности Position можно временно хранить «заполнитель» — объект Person с неопределенным состоянием:

//• typeinfo/Position.java
package typeinfo;
 
class Position {
private String title;
private Person person;
public Position(String jobTitle, Person employee) {
title = jobTitle;
person = employee;
if(person == null)
person = Person.NULL;
}
public Position(String jobTitle) {
title = jobTitle;
person = Person.NULL;
 
}
public String getTitle() { return title; }
public void setTitle(String newTitle) { title = newTitle;}
public Person getPerson() { return person; }
public void setPerson(Person newPerson) {
person = newPerson;
if(person == null)
person = Person.NULL;
}
@Override
public String toString() {
return "Position: " + title + " " + person;
}
}

Превращать Position в объект с неопределенным состоянием не обязательно, потому что существование Person.NULL подразумевает неопределенность Position (возможно, позднее выяснится, что явная поддержка неопределенного состояния для Position нужна, и вы добавите ее, но в соответствии с одним из канонов экстремального программирования в начальный проект следует включить «простейшее решение, которое будет работать», и включать новые функции лишь по мере возникновения реальной необходимости).
Теперь класс Staff может проверять объекты с неопределенным состоянием при заполнении вакансий:

//: typeinfo/Staff.java
import java.util.*;
 
public class Staff extends ArrayList<Position> {
public void add(String title, Person person) {
add(new Position(title, person));
}
public void add(String... titles) {
for(String title : titles)
add(new Position(title));
}
public Staff(String... titles) { add(titles); }
public boolean positionAvailable(String title) {
for(Position position : this)
if(position.getTitle().equals(title) &&
position.getPerson() == Person.NULL)
return true;
return false;
}
public void fillPosition(String title, Person hire) {
for(Position position : this)
if(position.getTitle().equals(title) &&
position.getPerson() == Person.NULL) {
position.setPerson(hire);
return;
}
throw new RuntimeException(
"Position " + title + " not available");
}
public static void main(String[] args) {
Staff staff = new Staff("President", "CTO",
"Marketing Manager", "Product Manager",
"Project Lead", "Software Engineer",
"Software Engineer", "Software Engineer",
"Software Engineer", "Test Engineer",
"Technical Writer");
staff.fillPosition("President",
new Person("Me", "Last", "The Top, Lonely At"));
staff.fillPosition("Project Lead",
new Person("Janet", "Planner", "The Burbs"));
if(staff.positionAvailable("Software Engineer"))
staff.fillPosition("Software Engineer",
new Person("Bob", "Coder", "Bright Light City"));
System.out.println(staff);
}
}

<spoiler text="Output:">

[Position: President Person: Me Last The Top. Lonely At, Position. СТО NullPerson, Position: 
Marketing Manager NullPerson. Position: Product Manager NullPerson, Position. Project Lead
Person: Janet Planner The Burbs. Position: Software Engineer Person: Bob Coder Bright Light City,
Position: Software Engineer NullPerson, Position: Software Engineer NullPerson. Position-
Software Engineer NullPerson. Position. Test Engineer NullPerson. Position: Technical Writer
NullPerson]

</spoiler>
Обратите внимание: в некоторых местах нам по-прежнему приходится проверять объекты на определенное состояние, что принципиально не отличается от проверки null, но в других местах, скажем, при преобразованиях toString(), лишние проверки не нужны; мы просто считаем, что ссылка на объект действительна.

Если вместо конкретных классов используются интерфейсы, для автоматического создания объектов с неопределенным состоянием можно воспользоваться динамическим посредником. Допустим, имеется интерфейс Robot, определяющий имя и модель робота, а также список List<Operation>, определяющий, какие операции выполняет робот. Операция состоит из описания и команды:

//: typeinfo/Operation.java
 
public interface Operation {
String description();
void command();
}

Чтобы воспользоваться услугами робота, следует вызвать метод operations():

//: typeinfo/Robot.java
import java.util.*;
import net.mindview.util.*;
 
public interface Robot {
String name();
String model();
List<Operation> operations();
class Test {
public static void test(Robot r) {
if(r instanceof Null)
System.out.println("[Null Robot]");
System.out.println("Название: " + r.name());
System.out.println("Модель: " + r.model());
for(Operation operation : r.operations()) {
System.out.println(operation.description());
operation.command();
}
}
}
}

При этом используется вложенный класс, выполняющий проверку. Теперь мы можем создать робота для уборки снега:

//: typeinfo/SnowRemovalRobot.java
import java.util.*;
 
public class SnowRemovalRobot implements Robot {
private String name;
public SnowRemovalRobot(String name) {this.name = name;}
public String name() { return name; }
public String model() { return "SnowBot Series 11"; }
public List<Operation> operations() {
return Arrays.asList(
new Operation() {
public String description() {
return name + " может убирать снег";
}
public void command() {
System.out.println(name + " убирает снег");
}
},
new Operation() {
public String description() {
return name + " может колоть лед";
}
public void command() {
System.out.println(name + " колет лед");
}
},
new Operation() {
public String description() {
return name + " может чистить крышу";
}
public void command() {
System.out.println(name + " чистит крышу");
}
}
);
}
public static void main(String[] args) {
Robot.Test.test(new SnowRemovalRobot("Slusher"));
}
}

<spoiler text="Output:">

Название: Slusher 
Модель: SnowBot Series 11
Slusher может убирать снег
Slusher убирает снег
Slusher может колоть лед
Slusher колет лед
Slusher может чистить крышу
Slusher чистит крышу

</spoiler>
Предполагается, что существуют разные типы роботов, и для каждого типа Robot объект с неопределенным состоянием должен делать что-то особенное — в нашем примере выдавать информацию о конкетном типе Robot, представленном объектом. Эта информация перехватывается динамическим посредником:

//: typeinfo/NullRobot.java
// Использование динамического посредника для создания
// объекта с неопределенным состоянием
import java.lang.reflect.*;
import java.util.*;
import net.mindview.util.*;
 
class NullRobotProxyHandler implements InvocationHandler {
private String nullName;
private Robot proxied = new NRobot();
NullRobotProxyHandler(Class<? extends Robot> type) {
nullName = type.getSimpleName() + " NullRobot";
}
private class NRobot implements Null, Robot {
public String name() { return nullName; }
public String model() { return nullName; }
public List<Operation> operations() {
return Collections.emptyList();
}
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return method.invoke(proxied, args);
}
}
 
public class NullRobot {
public static Robot
newNullRobot(Class<? extends Robot> type) {
return (Robot)Proxy.newProxyInstance(
NullRobot.class.getClassLoader(),
new Class[]{ Null.class, Robot.class },
new NullRobotProxyHandler(type));
}
public static void main(String[] args) {
Robot[] bots = {
new SnowRemovalRobot("SnowBee"),
newNullRobot(SnowRemovalRobot.class)
};
for(Robot bot : bots)
Robot.Test.test(bot);
}
}

<spoiler text="Output:">

Название: SnowBee Модель: 
SnowBot Series 11
SnowBee может убирать снег
SnowBee убирает снег
SnowBee может колоть лед
SnowBee колет лед
SnowBee может чистить крышу
SnowBee чистит крышу
[Null Robot]
Название: SnowRemova1 Robot NullRobot
Модель: SnowRemovalRobot NullRobot

</spoiler>
Каждый раз, когда вам требуется объект Robot с неопределенным состоянием, вы вызываете newNullRobot() и передаете тип Robot, для которого создается
посредник. Посредник выполняет требования о поддержке интерфейсов Robot и Null, а также предоставляет имя опосредованного типа.

Интерфейсы и информация о типах

Одной из важных целей ключевого слова interface является изоляция компонентов и сокращение привязок. Использование интерфейсов вроде бы позволяет добиться этой цели, однако RTTI позволяет обойти ограничения — интерфейсы не обеспечивают стопроцентной изоляции. Начнем следующий пример с интерфейса:

//: typeinfo/interfacea/A.java 
package typeinfo.interfaces;
public interface A {
void f();
}

Затем интерфейс реализуется, и выясняется, что можно «в обход» добраться до фактического типа реализации:

//: typeinfo/InterfaceViolation.java
// Интерфейс можно обойти
import typeinfo.interfacea.*;
 
class B implements A {
public void f() {}
public void g() {}
}
 
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // // Ошибка компиляции
System.out.println(a.getClass().getName());
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
}

<spoiler text="Output:">

В

</spoiler>
Используя RTTI, мы выясняем, что объект а реализован в форме В. Преобразование к типу В позволяет вызвать метод, не входящий в интерфейс А.
Все это абсолютно законно и допустимо, но, скорее всего, вы предпочли бы оградить клиентских программистов от подобных выходок. Казалось бы, ключевое слово interaface должно защищать вас, но на самом деле этого не происходит, а факт использования В для реализации А становится известен любому желающему.
Одно из возможных решений: просто скажите программистам, что если они будут использовать фактический класс вместо интерфейса, то пускай сами разбираются со всеми возникающими проблемами. Вероятно, во многих случаях этого достаточно, но если «вероятно» вас не устраивает — можно применить более жесткие меры.
Проще всего установить для реализации пакетный уровень доступа, чтобы она оставалась невидимой для клиентов за пределами пакета:

//: typeinfo/packageaccess/HiddenC.java
package typeinfo.packageaccess;
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
 
class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
 
public class HiddenC {
public static A makeA() { return new C(); }
}

Единственная открытая (public) часть пакета, HiddenC, выдает интерфейс А при вызове. Интересно отметить, что, даже если makeA() будет возвращать С, за пределами пакета все равно удастся использовать только А, потому что имя С недоступно.
Попытка нисходящего преобразования к С тоже завершается неудачей:

//: typeinfo/HiddenImplementation.java
// Пакетный доступ тоже можно обойти
import typeinfo.interfacea.*;
import typeinfo.packageaccess.*;
import java.lang.reflect.*;
 
public class HiddenImplementation {
public static void main(String[] args) throws Exception {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
// Ошибка компиляции, символическое имя 'С' не найдено
/* if(a instanceof C) {
C c = (C)a;
c.g();
} */

// Однако рефлексия позволяет вызвать g():
callHiddenMethod(a, "g");
// ... И даже еще менее доступные методы!
callHiddenMethod(a, "u");
callHiddenMethod(a, "v");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object a, String methodName)
throws Exception {
Method g = a.getClass().getDeclaredMethod(methodName);
g.setAccessible(true);
g.invoke(a);
}
}

<spoiler text="Output:">

public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()

</spoiler>
Как видите, рефлексия позволяет вызвать все методы, даже приватные! Зная имя метода, можно вызвать setAccessible(true) для объекта Method, чтобы сделать возможным его вызов, как видно из реализации callHiddenMethod().
Можно подумать, что проблема решается распространением только откомпилированного кода, но и это не так. Достаточно запустить javap — декомпилятор, входящий в JDK. Командная строка выглядит так:

javap -private С

Флаг -private означает, что при выводе должны отображаться все члены, даже приватные. Таким образом, любой желающий сможет получить имена и сигнатуры приватных методов и вызвать их.
А если реализовать интерфейс в виде приватного внутреннего класса? Вот как это выглядит:

//: typeinfo/InnerImplementation.java
// Приватные внутренние классы не скрываются от рефлексии
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
 
class InnerA {
private static class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public static A makeA() { return new C(); }
}
 
public class InnerImplementation {
public static void main(String[] args) throws Exception {
A a = InnerA.makeA();
a.f();
System.out.println(a.getClass().getName());
// Reflection still gets into the private class:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}

<spoiler text="Output:">

public C.f()
InnerA$C
public C.g()
package C.u()
protected C.v()
private C.w()

</spoiler>
He помогло. Как насчет анонимного класса?

//: typeinfo/AnonymousImplementation.java
// Анонимные внутренние классы тоже не скрыты от рефлексии
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
 
class AnonymousA {
public static A makeA() {
return new A() {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
};
}
}
 
public class AnonymousImplementation {
public static void main(String[] args) throws Exception {
A a = AnonymousA.makeA();
a.f();
System.out.println(a.getClass().getName());
// Reflection still gets into the anonymous class:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}

<spoiler text="Output:">

public C.f()
AnonymousA$1
public C.g()
package C.u()
protected C.v()
private C.w()

</spoiler>
Похоже, не существует никакого способа предотвратить обращение и вызов методов с уровнем доступа, отличным от public, посредством рефлексии. Сказанное относится и к полям данных, даже к приватным:

//: typeinfo/ModifyingPrivateFields.java
import java.lang.reflect.*;
 
class WithPrivateFinalField {
private int i = 1;
private final String s = "I'm totally safe";
private String s2 = "Am I safe?";
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
}
}
 
public class ModifyingPrivateFields {
public static void main(String[] args) throws Exception {
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("i");
f.setAccessible(true);
System.out.println("f.getInt(pf): " + f.getInt(pf));
f.setInt(pf, 47);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
f = pf.getClass().getDeclaredField("s2");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
}
}

<spoiler text="Output:">

i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!

</spoiler>
Впрочем, final-поля защищены от изменений. Система времени выполнения спокойно воспринимает любые попытки их изменения, но при этом ничего не происходит.
В действительности все эти нарушения уровня доступа не так уж страшны. Если кто-то захочет вызывать методы, которым вы назначили приватный или пакетный доступ (тем самым ясно показывая, что вызывать их не следует), вряд ли он станет жаловаться на то, что вы изменили некоторые аспекты этих методов. С другой стороны, «черный ход» к внутреннему устройству класса позволяет решить некоторые проблемы, нерешаемые другими средствами, и в общем случае преимущества рефлексии неоспоримы.

Резюме

Динамическое определение типов (RTTI) позволяет вам получить информацию о точном типе объекта тогда, когда у вас для него имеется лишь ссылка базового типа. Таким образом, оно открывает широкие возможности для злоупотреблений со стороны новичков, которые еще не поняли и не успели оценить всю мощь полиморфизма. У многих людей, ранее работавших с процедурными языками, возникает сильное желание разбить свою программу на множество конструкций switch при помощи RTTI. Однако при этом они лишаются всех преимуществ полиморфизма, относящихся к разработке программы в целом и ее дальнейшей поддержке. В Java рекомендуется использовать именно полиморфные методы, а к услугам RTTI следует прибегать только в крайнем случае.

Впрочем, при использовании полиморфных методов требуется полный контроль над базовым классом, поскольку в некоторой точке программы, после наследования очередного класса, вы можете обнаружить, что базовый класс не со­держит нужного вам метода, и тогда RTTI вас выручит: при наследовании вы расширяете интерфейс класса, добавляя в него новые методы. Особенно верно это при использовании в качестве базовых классов библиотек, которые вы не можете изменить. Далее в своем коде в подходящий момент вы обнаруживаете новый тип и вызываете для него нужный метод. Такой подход не противоречит основам полиморфизма и расширяемости программы, так как добавление в программу нового типа не требует изменения бесчисленного множества конструкций switch. Но чтобы извлечь пользу из дополнительной функциональности нового класса, придется использовать RTTI.

Включение некоторого метода в базовый класс будет выгодно только одному производному классу, который действительно реализует его, но все остальные производные классы будут вынуждены использовать для этого метода какую-либо бесполезную «заглушку». Интерфейс базового класса «размывается» и раздражает тех, кому приходится переопределять ненужные абстрактные методы при наследовании от базового класса. Например, рассмотрим иерархию классов, представляющих музыкальные инструменты. Предположим, что вы хотите прочистить мундштуки духовых инструментов своего оркестра. Конечно, можно поместить в базовый класс Instrument (общее представление музыкального инструмента) еще один метод clearSpitValve() (прочистка мундштуков), но тогда получится, что и у синтезатора, и у барабана есть мундштук! С помощью RTTI можно получить гораздо более верное решение данной задачи, поскольку этот метод уместно поместить в более конкретный класс (например, в класс Wind, базовый для всех духовых инструментов). Однако еще более разумным стало бы включение в класс Instrument метода prepareInstrument() (подготовить инструмент к игре), который подошел бы всем инструментам без исключения. На первый взгляд можно было бы ошибочно решить, что в данном случае без RTTI не обойтись.

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

Мы также видели, что рефлексия открывает перед программистом множество новых возможностей и делает возможным более динамичный стиль программирования. Пожалуй, динамическая природа рефлексии кому-то покажется пу­гающей. Для тех, кто привык к безопасной статической проверке типов, сама возможность выполнения действий, правильность которых проверяется только на стадии выполнения, а для выдачи информации используются исключения, выглядит шагом в неверном направлении. Некоторые доходят до утверждений, будто сама возможность исключения на стадии выполнения свидетельствует о том, что такого кода лучше избегать. На мой взгляд, чувство безопасности весьма иллюзорно — неожиданности и исключения возможны всегда, даже если программа не содержит блоков try и спецификации исключений. Предпочитаю думать, что существование логически целостной модели выдачи информации об ошибках дает возможность писать динамический код с использованием рефлексии. Конечно, всегда желательно писать код со статической проверкой... когда это возможно. И все же динамический код является одной из важнейших особенностей, отделяющих Java от таких традиционных языков, как C++.

]]>
Книги по Java https://linexp.ru?id=4740 Wed, 29 Jun 2022 14:24:38 GMT
<![CDATA[Глава 14 Thinking in Java 4th edition]]> ПАРАМЕТРИЗАЦИЯОбычные классы и методы работают с конкретными типами: либо, примитивами, либо с классами. Если ваш код должен работать с разными типами, такая жесткость может создавать проблемы.

Одним из механизмов обеспечения универсальности кода в объектно-ориентированных языках является полиморфизм. Например, вы можете написать метод, который получает в аргументе объект базового класса, а затем использует этот метод с любым классом, производным от него. Метод становится чуть более универсальным, а область его применения расширяется. Это относится и к классам — использование базового класса вместо производного обеспечивает дополнительную гибкость. Конечно, наследование возможно только для классов, не являющихся final.

ПАРАМЕТРИЗАЦИЯ

Обычные классы и методы работают с конкретными типами: либо, примитивами, либо с классами. Если ваш код должен работать с разными типами, такая жесткость может создавать проблемы.

Одним из механизмов обеспечения универсальности кода в объектно-ориентированных языках является полиморфизм. Например, вы можете написать метод, который получает в аргументе объект базового класса, а затем использует этот метод с любым классом, производным от него. Метод становится чуть более универсальным, а область его применения расширяется. Это относится и к классам — использование базового класса вместо производного обеспечивает дополнительную гибкость. Конечно, наследование возможно только для классов, не являющихся final.

Впрочем, иногда даже рамки одной иерархии оказываются слишком тесными. Если в аргументе метода передается интерфейс вместо класса, то ограничения ослабляются и в них включается все, что реализует данный интерфейс, — в том числе и классы, которые еще не были созданы. Это дает программисту-клиенту возможность реализовать интерфейс, чтобы соответствовать требованиям вашего класса или метода. Таким образом, интерфейсы позволяют выходить за рамки иерархий классов, если только у вас имеется возможность создать новый класс.
Но в некоторых случаях даже интерфейсы оказываются недостаточно гибкими. Интерфейс требует, чтобы ваш код работал в этом конкретном интерфейсе. Если бы было можно указать, что ваш код работает «с некоторым не заданным типом», а не с конкретным интерфейсом или классом, программа приобрела бы еще более общий характер.

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

Если использовать готовый параметризованный тип относительно несложно, при попытке написать собственный тип вас ждут сюрпризы. В частности, в этой главе я постараюсь объяснить, почему параметризованные типы Java получились именно такими.
Не стоит думать, что параметризованные типы Java бесполезны — во многих случаях они делают код более четким и элегантным. Но, если вы работали на другом языке, в котором они были реализованы более «чисто», вас могут ждать разочарования. В этой главе мы изучим как достоинства, так и недостатки параметризованных типов Java, чтобы вы могли использовать эту новую возможность более эффективно.

Простая параметризация

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

Рассмотрим класс для хранения одного объекта. Конечно, в этом классе можно указать точный тип объекта:

//: generics/Holder1.java
class Automobile {}
 
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
}

Однако такой «контейнер» получается не слишком универсальным — он не может использоваться только для одного типа. Конечно, было бы неудобно создавать новый класс для каждого типа, который нам встретится в программе.
До выхода Java SE5 можно было бы хранить в классе Object:

//: generics/Holder2.java
public class Holder2 {
private Object a;
public Holder2(Object a) { this.a = a; }
public void set(Object a) { this.a = a; }
public Object get() { return a; }
public static void main(String[] args) {
Holder2 h2 = new Holder2(new Automobile());
Automobile a = (Automobile)h2.get();
h2.set("Not an Automobile");
String s = (String)h2.get();
h2.set(1); // Автоматически упаковывается в Integer
Integer x = (Integer)h2.get();
}
}

Теперь класс Holder2 может хранить все, что угодно, — в приведенном примере один объект Holder2 используется для хранения трех разных типов данных.
В некоторых случаях бывает нужно, чтобы контейнер мог хранить объекты разных типов, но чаще контейнер предназначается для одного типа объектов. Одна из главных причин для применения параметризованных типов заключается именно в этом: вы можете указать, какой тип должен храниться в контейнере, и заданный тип будет поддерживаться комплиятором.
Итак, вместо Object в определении класса было бы удобнее использовать некий условный заменитель, чтобы отложить выбор до более позднего момента. Для этого после имени класса в угловых скобках указывается параметр типа, который при использовании заменяется фактическим типом. В нашем примере это будет выглядеть так (Т — параметр типа):

//: generics/Holder3.java
public class Holder3<T> {
private T a;
public Holder3(T a) { this.a = a; }
public void set(T a) { this.a = a; }
public T get() { return a; }
public static void main(String[] args) {
Holder3<Automobile> h3 =
new Holder3<Automobile>(new Automobile());
Automobile a = h3.get(); // Преобразование не требуется
// h3.set("Not an Automobile");// Ошибка
// h3.set(1); // Ошибка
}
}

При создании Holder3 необходимо указать тип объектов, хранящихся в контейнере, в угловых скобках, как в main(). В дальнейшем в контейнер можно будет помещать объекты только этого типа (или производного, так как принцип заменяемости работает и для параметризованных типов). А при извлечении вы автоматически получаете объект нужного типа.
В этом заключается основная идея параметризованных типов Java: вы указываете, какой тип должен использоваться, а механизм параметризации берет на себя все подробности.

Кортежи

При вызове метода часто требуется, чтобы метод возвращал несколько объектов. Команда return позволяет вернуть только один объект, поэтому проблема решается созданием объекта, содержащего несколько возвращаемых объектов. Конечно, можно создавать специальный класс каждый раз, когда возникает подобная ситуация, но параметризованные типы позволяют решить проблему один раз и избавиться от хлопот в будущем. Заодно решается проблема безопасности типов на стадии компиляции.
Концепция нескольких объектов, «упакованных» в один объект, называется кортежем (tuple). Получатель объекта может читать элементы, но не может добавлять их (эта концепция еще называется объектом передачи данных).
Обычно кортеж может иметь произвольную длину, а все объекты кортежа могут относиться к разным типам. Однако мы хотим задать тип каждого объекта и при этом гарантировать, что при чтении значения будет получен правильный тип. Для решения проблемы переменной длины мы создадим несколько разных кортежей. Вот один из них, рассчитанный на два объекта:

//: net/mindview/util/TwoTuple.java
package net.mindview.util;
 
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A a, B b) { first = a; second = b; }
public String toString() {
return "(" + first + ", " + second + ")";
}
}

Конструктор запоминает сохраняемый объект, а вспомогательная функция toString() выводит значения из списка. Обратите внимание: кортеж подразумевает упорядоченное хранение элементов.
При первом чтении может показаться, что такая архитектура нарушает общие принципы безопасности программирования на Java. Разве first и second не должны быть объявлены приватными, а обращения к ним осуществляться только из методов getFirst() и getSecond()? Подумайте, какая безопасность реализуется в этом случае: клиент может читать объекты и делать с прочитанными значениями все, что пожелает, но не может изменить first и second. Фактически объявление final делает то же самое, но короче и проще.
Кортежи большей длины создаются посредством наследования. Добавить новый параметр типа несложно:

//:------------ net/mindview/util/ThreeTuple.java
package net.mindview.util;
 
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A a, B b, C c) {
super(a, b);
third = c;
}
public String toString() {
return "(" + first + ", " + second + ", " + third +")";
}
}
 
//:-------------- net/mindview/util/FourTuple.java
package net.mindview.util;
 
public class FourTuple<A,B,C,D> extends ThreeTuple<A,B,C> {
public final D fourth;
public FourTuple(A a, B b, C c, D d) {
super(a, b, c);
fourth = d;
}
public String toString() {
return "(" + first + ", " + second + ", " +
third + ", " + fourth + ")";
}
}
 
//:--------------- net/mindview/util/FiveTuple.java
package net.mindview.util;
 
public class FiveTuple<A,B,C,D,E>
extends FourTuple<A,B,C,D> {
public final E fifth;
public FiveTuple(A a, B b, C c, D d, E e) {
super(a, b, c, d);
fifth = e;
}
public String toString() {
return "(" + first + ", " + second + ", " +
third + ", " + fourth + ", " + fifth + ")";
}
}

Чтобы воспользоваться этими классами, достаточно определить кортеж нужной длины как возвращаемое значение функции, а затем создать и вернуть его командой return:

//: generics/TupleTest.java
import net.mindview.util.*;
 
class Amphibian {}
class Vehicle {}
 
public class TupleTest {
static TwoTuple<String,Integer> f() {
// Autoboxing converts the int to Integer:
return new TwoTuple<String,Integer>("hi", 47);
}
static ThreeTuple<Amphibian,String,Integer> g() {
return new ThreeTuple<Amphibian, String, Integer>(
new Amphibian(), "hi", 47);
}
static
FourTuple<Vehicle,Amphibian,String,Integer> h() {
return
new FourTuple<Vehicle,Amphibian,String,Integer>(
new Vehicle(), new Amphibian(), "hi", 47);
}
static
FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return new
FiveTuple<Vehicle,Amphibian,String,Integer,Double>(
new Vehicle(), new Amphibian(), "hi", 47, 11.1);
}
public static void main(String[] args) {
TwoTuple<String,Integer> ttsi = f();
System.out.println(ttsi);
// ttsi.first = "there"; // Compile error: final
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}

<spoiler text="Output:">(80% match)

(hi, 47)
(Amphibian@1f6a7b9, hi, 47)
(Vehicle@35ce36, Amphibian@757aef, hi, 47)
(Vehicle@9cab16, Amphibian@1a46e30, hi, 47, 11.1)

</spoiler>
Спецификация final для public-полей предотвращает их изменение после конструирования (поэтому попытка выполнения команды ttsi.first="there" приводит к ошибке).
Конструкции new получаются немного громоздкими. Позднее в этой главе будет показано, как упростить их при помощи параметризованных методов.

Класс стека

Давайте рассмотрим менее тривиальный пример: реализацию традиционного стека. В главе 11 была приведена реализация стека на базе LinkedList. В этом примере класс LinkedList уже содержал все методы, необходимые для создания стека. Класс стека строился объединением одного параметризованного класса (Stack<T>) с другим параметризованным классом (LinkedList<T>). Этот пример показывает, что параметризованный тип — такой же тип, как и все остальные (за некоторыми исключениями, о которых речь пойдет позже):-
Вместо того, чтобы использовать LinkedList, мы также могли реализовать собственный механизм хранения связанного списка:

//: generics/LinkedStack.java
// Стек, реализованный на базе внутренней структуры
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
Node() { item = null; next = null; }
Node(U item, Node<U> next) {
this.item = item;
this.next = next;
}
boolean end() { return item == null && next == null; }
}
private Node<T> top = new Node<T>(); // Предохранитель
public void push(T item) {
top = new Node<T>(item, top);
}
public T pop() {
T result = top.item;
if(!top.end())
top = top.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> lss = new LinkedStack<String>();
for(String s : "Phasers on stun!".split(" "))
lss.push(s);
String s;
while((s = lss.pop()) != null)
System.out.println(s);
}
}

<spoiler text="Output:">

stun!
on
Phasers

</spoiler>
Внутренний класс Node тоже является параметризованным и имеет собственный параметр типа.
Для определения наличия элементов в стеке в этом примере используется предохранитель (end sentinel). Он создается при конструировании LinkedStack, а затем при каждом вызове push() новый объект Node<T> создается и связывается с предыдущим Node<T>. При вызове рор() всегда возвращается top.item, после чего текущий объект Node<T> уничтожается и происходит переход к следующему — если только текущим элементом не является предохранитель; в этом случае переход не выполняется. При повторных вызовах рор() клиент будет получать null, что свидетельствует об отсутствии элементов в стеке.

RandomList

Рассмотрим еще один пример контейнера: допустим, вам понадобилась особая разновидность списка, которая случайным образом выбирает один из своих элементов при вызове select(). Так как класс должен работать для любых объектов, мы воспользуемся параметризацией:

//: generics/RandomList.java
import java.util.*;
 
public class RandomList<T> {
private ArrayList<T> storage = new ArrayList<T>();
private Random rand = new Random(47);
public void add(T item) { storage.add(item); }
public T select() {
return storage.get(rand.nextInt(storage.size()));
}
public static void main(String[] args) {
RandomList<String> rs = new RandomList<String>();
for(String s: ("The quick brown fox jumped over " +
"the lazy brown dog").split(" "))
rs.add(s);
for(int i = 0; i < 11; i++)
System.out.print(rs.select() + " ");
}
}

<spoiler text="Output:">

brown over fox quick quick dog brown The brown lazy brown

</spoiler>

Параметризованные интерфейсы

Параметризация работает и с интерфейсами. Например, класс, создающий объекты, называется генератором. В сущности, генератор представляет собой специализированную версию паттерна «метод-фабрика», но при обращении к нему никакие аргументы не передаются, тогда как метод-фабрика обычно получает аргументы. Генератор умеет создавать объекты без дополнительной информации.

Обычно генератор определяет всего один метод — тот, который создает объекты. Назовем его next() и включим в стандартный инструментарий:

//: net/mindview/util/Generator.java
// Параметризованный интерфейс
package net.mindview.util;
public interface Generator<T> {
T next(); }

Возвращаемое значение метода next() параметризовано по типу Т. Как видите, механизм параметризации работает с интерфейсами почти так же, как с классами.

Чтобы продемонстрировать, как работает реализация Generator, мы воспользуемся иерархией классов, представляющих разные виды кофе:

//: generics/coffee/Coffee.java
package generics.coffee;
public class Coffee {
private static long counter = 0;
private final long id = counter++;
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
 
//: generics/coffee/Latte.java
package generics.coffee;
public class Latte extends Coffee {}
 
//: generics/coffee/Mocha.java
package generics.coffee;
public class Mocha extends Coffee {}
 
//: generics/coffee/Cappuccino.java
package generics.coffee;
public class Cappuccino extends Coffee {}
 
//: generics/coffee/Americano.java
package generics.coffee;
public class Americano extends Coffee {}
 
//: generics/coffee/Breve.java
package generics.coffee;
public class Breve extends Coffee {}

Теперь мы можем реализовать интерфейс Generator<Coffee>, который создает случайные типы объектов из иерархии Coffee:

//: generics/coffee/CoffeeGenerator.java
// Генератор случайных объектов из иерархии Coffee
package generics.coffee;
import java.util.*;
import net.mindview.util.*;
 
public class CoffeeGenerator
implements Generator<Coffee>, Iterable<Coffee> {
private Class[] types = { Latte.class, Mocha.class,
Cappuccino.class, Americano.class, Breve.class, };
private static Random rand = new Random(47);
public CoffeeGenerator() {}
//Для перебора
private int size = 0;
public CoffeeGenerator(int sz) { size = sz; }
public Coffee next() {
try {
return (Coffee)
types[rand.nextInt(types.length)].newInstance();
// Сообщение об ошибках во время выполнения:
} catch(Exception e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
public boolean hasNext() { return count > 0; }
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
public void remove() {// He реализован
throw new UnsupportedOperationException();
}
};
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for(int i = 0; i < 5; i++)
System.out.println(gen.next());
for(Coffee c : new CoffeeGenerator(5))
System.out.println(c);
}
}

<spoiler text="Output:">

Americano 0
Latte 1
Americano 2
Mocha 3
Mocha 4
Breve 5
Americano 6
Latte 7
Cappuccino 8
Cappuccino 9

</spoiler>
Параметризованный интерфейс Generator гарантирует, что next() вернет параметр типа. CoffeeGenerator также реализует интерфейс Iterable и поэтому может использоваться в синтаксисе foreach. Аргумент, по которому определяется момент прекращения перебора, передается при вызове второго конструктора.

А вот как выглядит другая реализация Generator<T>, предназначенная для получения чисел Фибоначчи:

//: generics/Fibonacci.java
// Построение чисел Фибоначчи
import net.mindview.util.*;
 
public class Fibonacci implements Generator<Integer> {
private int count = 0;
public Integer next() { return fib(count++); }
private int fib(int n) {
if(n < 2) return 1;
return fib(n-2) + fib(n-1);
}
public static void main(String[] args) {
Fibonacci gen = new Fibonacci();
for(int i = 0; i < 18; i++)
System.out.print(gen.next() + " ");
}
}

<spoiler text="Output:">

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 

</spoiler>
Хотя и внутри, и снаружи класса мы работаем с int, в параметре типа передается Integer. В этом проявляется одно из ограничений параметризации в языке Java: примитивные типы не могут использоваться в качестве параметров типа. Впрочем, в Java SE5 была добавлена удобная автоматическая упаковка (распаковка) для перехода от примитивных типов к объектным «оберткам», и наоборот.
Можно сделать следующий шаг вперед и создать генератор чисел Фибоначчи с реализацией Iterable. Конечно, можно изменить реализацию класса и добавить интерфейс Iterable, но исходные коды не всегда находятся в вашем распоряжении, и вообще там, где это возможно, лучше обойтись без их модификации. Вместо этого мы воспользуемся «адаптером» для получения нужного интерфейса (этот паттерн уже упоминался ранее в книге).
Существует несколько вариантов реализации адаптеров. Например, для получения адаптируемого класса можно воспользоваться наследованием:

//: generics/IterableFibonacci.java
// Adapt the Fibonacci class to make it Iterable.
import java.util.*;
 
public class IterableFibonacci
extends Fibonacci implements Iterable<Integer> {
private int n;
public IterableFibonacci(int count) { n = count; }
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
public boolean hasNext() { return n > 0; }
public Integer next() {
n--;
return IterableFibonacci.this.next();
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(int i : new IterableFibonacci(18))
System.out.print(i + " ");
}
}

<spoiler text="Output:">

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584

</spoiler>
Для использования IterableFibonacci в синтаксисе foreach мы передаем конструктору границу, чтобы метод hasNext() знал, когда следует возвращать false.

Параметризованные методы

До настоящего момента мы рассматривали параметризацию целых классов, однако параметризация может применяться и к отдельным методам классов. Сам класс при этом может быть параметризованным, а может и не быть — это не зависит от наличия параметризованных методов.
Параметризованный метод может изменяться независимо от класса. В общем случае параметризованные методы следует использовать «по мере возможности». Иначе говоря, если возможно параметризовать метод вместо целого класса, вероятно, стоит выбрать именно этот вариант. Кроме того, статические методы не имеют доступа к параметрам типа параметризованных классов; если такие методы должны использовать параметризацию, это должно происходить на уровне метода, а не на уровне класса.
Чтобы определить параметризованный метод, следует указать список параметров перед возвращаемым значением:

//: generics/GenericMethods.java
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}

<spoiler text="Output:">

java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods

</spoiler>
Класс GenericMethods не параметризован, хотя и класс, и его методы могут быть параметризованными одновременно. Но в данном случае только метод f() имеет параметр типа, обозначаемый списком параметров перед возвращаемым значением метода.
Учтите, что при использовании параметризованного класса параметры типов должны указываться при создании экземпляра. Но при использовании параметризованного метода указывать параметры типа не обязательно, потому что компилятор способен «вычислить» их за вас. Таким образом, вызов f() выглядит как обычный вызов метода; создается впечатление, что метод f() существует в бесконечном количестве перегруженных версий. При вызове ему даже может передаваться аргумент типа GenericMethods.
Для вызовов f(), использующих примитивные типы, в действие вступает механизм автоматической упаковки — примитивные типы автоматически преобразуются в соответствующие объекты. Это позволяет исключить некоторые фрагменты кода, которые были необходимы прежде из-за явного выполнения преобразований.

Вычисление типа аргумента

Параметризацию иногда упрекают в том, что она увеличивает объем кода. Для наглядности возьмем пример holding/MapOfList.java из главы 11. Создание контейнера Map с List выглядит так:

 Map<Person, List<? extends Pet>> petPeople = 
new HashMap<Person, List<? extends Pet>>():

(Ключевое слово extends и вопросительные знаки будут описаны позднее в этой главе.) Казалось бы, эта конструкция избыточна, а компилятор мог бы вычислить один из списков аргументов по-другому. В действительности сделать это он не может, но вычисление аргументов типов все же позволяет немного упростить код. Например, мы можем создать вспомогательную библиотеку с различными статическими методами, содержащими самые распространенные реализации различных контейнеров:

//: net/mindview/util/New.java
// Utilities to simplify generic container creation
// by using type argument inference.
package net.mindview.util;
import java.util.*;
 
public class New {
public static <K,V> Map<K,V> map() {
return new HashMap<K,V>();
}
public static <T> List<T> list() {
return new ArrayList<T>();
}
public static <T> LinkedList<T> lList() {
return new LinkedList<T>();
}
public static <T> Set<T> set() {
return new HashSet<T>();
}
public static <T> Queue<T> queue() {
return new LinkedList<T>();
}
// Примеры:
public static void main(String[] args) {
Map<String, List<String>> sls = New.map();
List<String> ls = New.list();
LinkedList<String> lls = New.lList();
Set<String> ss = New.set();
Queue<String> qs = New.queue();
}
}

Примеры использования представлены в main() — вычисление аргументов типов устраняет необходимость в повторении списков параметров. Этот прием можно использовать в holding/MapOfList.java:

//: generics/SimplerPets.java
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;
 
public class SimplerPets {
public static void main(String[] args) {
Map<Person, List<? extends Pet>> petPeople = New.map();
// Rest of the code is the same...
}
}

Пример интересный, однако трудно сказать, насколько он эффективен в действительности. Человеку, читающему код, придется просмотреть дополнительную библиотеку и разобраться в ее коде. Возможно, вместо этого стоит оставить исходное (пусть и избыточное) определение — как ни парадоксально, этот вариант проще. Хотя, если в стандартную библиотеку Java будет добавлено некое подобие New.java, им можно будет пользоваться.
Вычисление типов не работает ни в каких других ситуациях, кроме присваивания. Если передать результат вызова метода (скажем, New.map()) в аргументе другого метода, компилятор не пытается выполнить вычисление типа. Вместо этого вызов метода интерпретируется так, как если бы возвращаемое значение присваивалось переменной типа Object. Пример ошибки такого рода:

//: generics/LimitsOfInference.java
import typeinfo.pets.*;
import java.util.*;
 
public class LimitsOfInference {
static void
f(Map<Person, List<? extends Pet>> petPeople) {}
public static void main(String[] args) {
// f(New.map()); // Does not compile
}
}

Явное указание типа

При вызове параметризованного метода также можно явно задать тип, хотя на практике этот синтаксис используется редко. Тип указывается в угловых скобках за точкой, непосредственно перед именем метода. При вызове метода в пределах класса необходимо ставить this перед точкой, а при вызове статических методов перед точкой указывается имя класса. Проблема, продемонстрированная в LimitsOflnference.java, решается при помощи следующего синтаксиса:

//: generics/ExplicitTypeSpecification.java
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;
 
public class ExplicitTypeSpecification {
static void f(Map<Person, List<Pet>> petPeople) {}
public static void main(String[] args) {
f(New.<Person, List<Pet>>map());
}
}

Конечно, при этом теряются преимущества от использования класса New для уменьшения объема кода, но дополнительный синтаксис необходим только за пределами команд присваивания.
Параметризованные методы и переменные списки аргументов
Параметризованные методы нормально сосуществуют с переменными списками аргументов:

//: generics/GenericVarargs.java
import java.util.*;
 
public class GenericVarargs {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
for(T item : args)
result.add(item);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
System.out.println(ls);
}
}

<spoiler text="Output:">

[A]
[A, B, C]
[, A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]

</spoiler>
Метод makeList() предоставляет ту же функциональность, что и метод java.util.Arrays.asList() из стандартной библиотеки.

Использование параметризованных методов с Generator

Генераторы хорошо подходят для заполнения Collection, и для выполнения этой операции было бы удобно создать параметризованный метод:

//: generics/Generators.java
// A utility to use with Generators.
import generics.coffee.*;
import java.util.*;
import net.mindview.util.*;
 
public class Generators {
public static <T> Collection<T>
fill(Collection<T> coll, Generator<T> gen, int n) {
for(int i = 0; i < n; i++)
coll.add(gen.next());
return coll;
}
public static void main(String[] args) {
Collection<Coffee> coffee = fill(
new ArrayList<Coffee>(), new CoffeeGenerator(), 4);
for(Coffee c : coffee)
System.out.println(c);
Collection<Integer> fnumbers = fill(
new ArrayList<Integer>(), new Fibonacci(), 12);
for(int i : fnumbers)
System.out.print(i + ", ");
}
}

<spoiler text="Output:">

Americano 0
Latte 1
Americano 2
Mocha 3
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,

</spoiler>
Обратите внимание на то, как параметризованный метод fill() применяется к контейнерам и генераторам как для типа Coffee, так и для Integer.

Обобщенный генератор

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

//: net/mindview/util/BasicGenerator.java
// Автоматическое создание Generator для класса
// с конструктором по умолчанию (без аргументов)
package net.mindview.util;
 
public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type){ this.type = type; }
public T next() {
try {
// Assumes type is a public class:
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
// Получение генератора по умолчанию для заданного type:
public static <T> Generator<T> create(Class<T> type) {
return new BasicGenerator<T>(type);
}
}

Класс предоставляет базовую реализацию, создающую объекты класса, который (1) является открытым (так как BasicGenerator определяется в отдельном пакете, соответствующий класс должен иметь уровень доступа public, не ограничиваясь пакетным доступом), и (2) обладает конструктором по умолчанию (то есть конструктором без аргументов). Чтобы создать один из таких объектов BasicGenerator, следует вызвать метод create() и передать ему обозначение генерируемого типа, параметризованный метод create() позволяет использовать запись BasicGenerator.create(MyType.class) вместо более громоздкой конструкции new BasicGenerator<MyType>(MyType.class).
Для примера рассмотрим простой класс с конструктором по умолчанию:

//: generics/CountedObject.java
public class CountedObject {
private static long counter = 0;
private final long id = counter++;
public long id() { return id; }
public String toString() { return "CountedObject " + id;}
}

Класс CountedObject отслеживает количество созданных экземпляров и включает его в выходные данные toString().
При помощи BasicGenerator можно легко создать Generator для CountedObject:

//: generics/BasicGeneratorDemo.java
import net.mindview.util.*;
 
public class BasicGeneratorDemo {
public static void main(String[] args) {
Generator<CountedObject> gen =
BasicGenerator.create(CountedObject.class);
for(int i = 0; i < 5; i++)
System.out.println(gen.next());
}
}

<spoiler text="Output:">

CountedObject 0
CountedObject 1
CountedObject 2
CountedObject 3
CountedObject 4

</spoiler>
Как видите, применение параметризованного метода снижает объем кода, необходимого для получения объекта Generator. Раз уж механизм параметризации Java все равно заставляет вас передавать объект Class, его можно заодно ис­пользовать для вычисления типа в методе create().

Упрощение работы с кортежами

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

//: net/mindview/util/Tuple.java
// Библиотека для работы с кортежами
// с использованием вычисления аргументов типов
package net.mindview.util;
 
public class Tuple {
public static <A,B> TwoTuple<A,B> tuple(A a, B b) {
return new TwoTuple<A,B>(a, b);
}
public static <A,B,C> ThreeTuple<A,B,C>
tuple(A a, B b, C c) {
return new ThreeTuple<A,B,C>(a, b, c);
}
public static <A,B,C,D> FourTuple<A,B,C,D>
tuple(A a, B b, C c, D d) {
return new FourTuple<A,B,C,D>(a, b, c, d);
}
public static <A,B,C,D,E>
FiveTuple<A,B,C,D,E> tuple(A a, B b, C c, D d, E e) {
return new FiveTuple<A,B,C,D,E>(a, b, c, d, e);
}
}

А вот как выглядит обновленная версия TupleTest.java для тестирования Tuple.java:

//: generics/TupleTest2.java
import net.mindview.util.*;
 
class Amphibian {}
class Vehicle {}
 
public class TupleTest2 {
static TwoTuple<String,Integer> f() {
// Autoboxing converts the int to Integer:
return new TwoTuple<String,Integer>("hi", 47);
}
static TwoTuple f2() { return tuple("hi", 47); }
 
static ThreeTuple<Amphibian,String,Integer> g() {
return new ThreeTuple<Amphibian, String, Integer>(
new Amphibian(), "hi", 47);
}
static
FourTuple<Vehicle,Amphibian,String,Integer> h() {
return
new FourTuple<Vehicle,Amphibian,String,Integer>(
new Vehicle(), new Amphibian(), "hi", 47);
}
static
FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return new
FiveTuple<Vehicle,Amphibian,String,Integer,Double>(
new Vehicle(), new Amphibian(), "hi", 47, 11.1);
}
public static void main(String[] args) {
TwoTuple<String,Integer> ttsi = f();
System.out.println(ttsi);
// ttsi.first = "there"; // Compile error: final
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}

<spoiler text="Output:"> (80% match)

(hi, 47)
(Amphibian@1f6a7b9, hi, 47)
(Vehicle@35ce36, Amphibian@757aef, hi, 47)
(Vehicle@9cab16, Amphibian@1a46e30, hi, 47, 11.1)

</spoiler>
Обратите внимание: f() возвращает параметризованный объект TwoTuple, a f2() — непараметризованный объект TwoTuple. Компилятор в данном случае не выдает предупреждения о f2(), потому что возвращаемое значение не используется в «параметризованном» стиле: в каком-то смысле проводится «восходящее преобразование» его до непараметризованного TwoTuple. Но, если попытаться сохранить результат f2() в параметризованном объекте TwoTuple, компилятор выдаст предупреждение.

Вспомогательный класс Set

Рассмотрим еще один пример использования параметризованных методов: математические операции между множествами. Эти операции удобно определить в виде параметризованных методов, используемых с различными типами:

//: net/mindview/util/Sets.java
package net.mindview.util;
import java.util.*;
 
public class Sets {
public static <T> Set<T> union(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<T>(a);
result.addAll(b);
return result;
}
public static <T>
Set<T> intersection(Set<T> a, Set<T> b) {
Set<T> result = new HashSet<T>(a);
result.retainAll(b);
return result;
}
// Вычитание подмножества из надмножества
public static <T> Set<T>
difference(Set<T> superset, Set<T> subset) {
Set<T> result = new HashSet<T>(superset);
result.removeAll(subset);
return result;
}
// Дополнение -- все. что не входит в пересечение
public static <T> Set<T> complement(Set<T> a, Set<T> b) {
return difference(union(a, b), intersection(a, b));
}
}

Первые три метода дублируют первый аргумент, копируя его ссылки в новый объект HashSet, поэтому аргументы Set не изменяются напрямую. Таким образом, возвращаемое значение представляет собой новый объект Set.
Четыре метода представляют математические операции с множествами: union() возвращает объект Set, полученный объединением множеств-аргументов, intersection() возвращает объект Set с общими элементами аргументов, difference() вычисляет разность множеств, a complement() — объект Set со всеми элементами, не входящими в пересечение. Чтобы создать простой пример использования этих методов, мы воспользуемся перечислением, содержащим разные названия акварельных красок:

//: generics/watercolors/Watercolors.java
package generics.watercolors;
 
public enum Watercolors {
ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP_YELLOW, ORANGE,
BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET,
CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE,
COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE,
SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,
BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK
}

Для удобства (чтобы избежать уточнения всех имен) в следующем примере это перечисление импортируется статически. Мы используем EnumSet — новый инструмент Java SE5 для простого создания Set на базе перечисления. Статиче­скому методу EnumSet.range() передаются первый и последний элементы диапазона, по которому строится множество:

//: generics/WatercolorSets.java
import generics.watercolors.*;
import java.util.*;
import static net.mindview.util.Print.*;
import static net.mindview.util.Sets.*;
import static generics.watercolors.Watercolors.*;
 
public class WatercolorSets {
public static void main(String[] args) {
Set<Watercolors> set1 =
EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);
Set<Watercolors> set2 =
EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);
print("set1: " + set1);
print("set2: " + set2);
print("union(set1, set2): " + union(set1, set2));
Set<Watercolors> subset = intersection(set1, set2);
print("intersection(set1, set2): " + subset);
print("difference(set1, subset): " +
difference(set1, subset));
print("difference(set2, subset): " +
difference(set2, subset));
print("complement(set1, set2): " +
complement(set1, set2));
}
}

<spoiler text="Output:"> (Sample)

set1: [BRILLIANT_RED, CRIMSON, MAGENTA, ROSE_MADDER, VIOLET, CERULEAN_BLUE_HUE, PHTHALO_BLUE,
ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN, VIRIDIAN_HUE]
set2: [CERULEAN_BLUE_HUE, PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE, PERMANENT_GREEN,
VIRIDIAN_HUE, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER, BURNT_UMBER]
union(set1, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, PERMANENT_GREEN, BURNT_UMBER,
COBALT_BLUE_HUE, VIOLET, BRILLIANT_RED, RAW_UMBER, ULTRAMARINE, BURNT_SIENNA, CRIMSON,
CERULEAN_BLUE_HUE, PHTHALO_BLUE, MAGENTA, VIRIDIAN_HUE]
intersection(set1, set2): [ULTRAMARINE, PERMANENT_GREEN, COBALT_BLUE_HUE, PHTHALO_BLUE,
CERULEAN_BLUE_HUE, VIRIDIAN_HUE]
difference(set1, subset): [ROSE_MADDER, CRIMSON, VIOLET, MAGENTA, BRILLIANT_RED]
difference(set2, subset): [RAW_UMBER, SAP_GREEN, YELLOW_OCHRE, BURNT_SIENNA, BURNT_UMBER]
complement(set1, set2): [SAP_GREEN, ROSE_MADDER, YELLOW_OCHRE, BURNT_UMBER, VIOLET,
BRILLIANT_RED, RAW_UMBER, BURNT_SIENNA, CRIMSON, MAGENTA]

</spoiler>
В выходных данных показаны результаты выполнения каждой операции. В следующем примере представлены варианты вызова Sets.difference() для разных классов Collection и Map из java.util:

//: net/mindview/util/ContainerMethodDifferences.java
package net.mindview.util;
import java.lang.reflect.*;
import java.util.*;
 
public class ContainerMethodDifferences {
static Set<String> methodSet(Class<?> type) {
Set<String> result = new TreeSet<String>();
for(Method m : type.getMethods())
result.add(m.getName());
return result;
}
static void interfaces(Class<?> type) {
System.out.print("Interfaces in " +
type.getSimpleName() + ": ");
List<String> result = new ArrayList<String>();
for(Class<?> c : type.getInterfaces())
result.add(c.getSimpleName());
System.out.println(result);
}
static Set<String> object = methodSet(Object.class);
static { object.add("clone"); }
static void
difference(Class<?> superset, Class<?> subset) {
System.out.print(superset.getSimpleName() +
" extends " + subset.getSimpleName() + ", adds: ");
Set<String> comp = Sets.difference(
methodSet(superset), methodSet(subset));
comp.removeAll(object); // Don't show 'Object' methods
System.out.println(comp);
interfaces(superset);
}
public static void main(String[] args) {
System.out.println("Collection: " +
methodSet(Collection.class));
interfaces(Collection.class);
difference(Set.class, Collection.class);
difference(HashSet.class, Set.class);
difference(LinkedHashSet.class, HashSet.class);
difference(TreeSet.class, Set.class);
difference(List.class, Collection.class);
difference(ArrayList.class, List.class);
difference(LinkedList.class, List.class);
difference(Queue.class, Collection.class);
difference(PriorityQueue.class, Queue.class);
System.out.println("Map: " + methodSet(Map.class));
difference(HashMap.class, Map.class);
difference(LinkedHashMap.class, HashMap.class);
difference(SortedMap.class, Map.class);
difference(TreeMap.class, Map.class);
}
}

Анонимные внутренние классы

Параметризация также может применяться к внутренним классам и анонимным внутренним классам. Пример реализации интерфейса Generator с использованием анонимных внутренних классов:

//: generics/BankTeller.java
// Очень простая имитация банковского обслуживания.
import java.util.*;
import net.mindview.util.*;
 
class Customer {
private static long counter = 1;
private final long id = counter++;
private Customer() {}
public String toString() { return "Customer " + id; }
// A method to produce Generator objects:
public static Generator<Customer> generator() {
return new Generator<Customer>() {
public Customer next() { return new Customer(); }
};
}
}
 
class Teller {
private static long counter = 1;
private final long id = counter++;
private Teller() {}
public String toString() { return "Teller " + id; }
// Метод для получения объектов Generator:
public static Generator<Teller> generator =
new Generator<Teller>() {
public Teller next() { return new Teller(); }
};
}
 
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " обслуживает " + c);
}
public static void main(String[] args) {
Random rand = new Random(47);
Queue<Customer> line = new LinkedList<Customer>();
Generators.fill(line, Customer.generator(), 15);
List<Teller> tellers = new ArrayList<Teller>();
Generators.fill(tellers, Teller.generator, 4);
for(Customer c : line)
serve(tellers.get(rand.nextInt(tellers.size())), c);
}
}

<spoiler text="Output:">

Teller 3 обслуживает Customer 1
Teller 2 обслуживает Customer 2
Teller 3 обслуживает Customer 3
Teller 1 обслуживает Customer 4
Teller 1 обслуживает Customer 5
Teller 3 обслуживает Customer 6
Teller 1 обслуживает Customer 7
Teller 2 обслуживает Customer 8
Teller 3 обслуживает Customer 9
Teller 3 обслуживает Customer 10
Teller 2 обслуживает Customer 11
Teller 4 обслуживает Customer 12
Teller 2 обслуживает Customer 13
Teller 1 обслуживает Customer 14
Teller 1 обслуживает Customer 15

</spoiler>
И Customer, и Teller содержат приватные конструкторы, поэтому для создания их объектов пользователь вынужден использовать объекты Generator.

Customer содержит метод generator(), который при каждом вызове создает новый объект Generator<Customer>. На случай, если множественные объекты Generator вам не понадобятся, в Teller создается синглетный открытый объект generator. Оба подхода продемонстрированы в вызовах fill() внутри main().
Поскольку метод generator() в Customer и объект Generator в Teller являются статическими, они не могут быть частью интерфейса, поэтому «обобщить» эту конкретную идиому не удастся. Несмотря на это обстоятельство, она достаточно хорошо работает в методе fill().

Построение сложных моделей

К числу важных преимуществ параметризации относится простота и надежность создания сложных моделей. Например, можно легко создать список (List) с элементами-кортежами:

//: generics/TupleList.java
// Построение сложных параметризованных типов путем объединения
import java.util.*;
import net.mindview.util.*;
 
public class TupleList<A,B,C,D>
extends ArrayList<FourTuple<A,B,C,D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tl =
new TupleList<Vehicle, Amphibian, String, Integer>();
tl.add(TupleTest.h());
tl.add(TupleTest.h());
for(FourTuple<Vehicle,Amphibian,String,Integer> i: tl)
System.out.println(i);
}
}

<spoiler text="Output:"> (75% match)

(Vehicle@11b86e7, Amphibian@35ce36, hi, 47)
(Vehicle@757aef, Amphibian@d9f9c3, hi, 47)

</spoiler>
Запись получается довольно громоздкой (особенно при создании итератора), однако вы получаете довольно сложную структуру данных без излишков программного кода.
А вот другой пример, который показывает, как легко строить сложные модели на основе параметризованных типов. Хотя каждый класс представляет собой автономный «строительный блок», их совокупность имеет сложную структуру. В данном случае моделируется магазин с товарами, полками и стеллажами:

//: generics/Store.java
// Построение сложной модели на базе параметризованных контейнеров
import java.util.*;
import net.mindview.util.*;
 
class Product {
private final int id;
private String description;
private double price;
public Product(int IDnumber, String descr, double price){
id = IDnumber;
description = descr;
this.price = price;
System.out.println(toString());
}
public String toString() {
return id + ": " + description + ", price: $" + price;
}
public void priceChange(double change) {
price += change;
}
public static Generator<Product> generator =
new Generator<Product>() {
private Random rand = new Random(47);
public Product next() {
return new Product(rand.nextInt(1000), "Test",
Math.round(rand.nextDouble() * 1000.0) + 0.99);
}
};
}
 
class Shelf extends ArrayList<Product> {
public Shelf(int nProducts) {
Generators.fill(this, Product.generator, nProducts);
}
}
 
class Aisle extends ArrayList<Shelf> {
public Aisle(int nShelves, int nProducts) {
for(int i = 0; i < nShelves; i++)
add(new Shelf(nProducts));
}
}
 
class CheckoutStand {}
class Office {}
 
public class Store extends ArrayList<Aisle> {
private ArrayList<CheckoutStand> checkouts =
new ArrayList<CheckoutStand>();
private Office office = new Office();
public Store(int nAisles, int nShelves, int nProducts) {
for(int i = 0; i < nAisles; i++)
add(new Aisle(nShelves, nProducts));
}
public String toString() {
StringBuilder result = new StringBuilder();
for(Aisle a : this)
for(Shelf s : a)
for(Product p : s) {
result.append(p);
result.append("\n");
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(new Store(14, 5, 10));
}
}

<spoiler text="Output:">

258: Test, price: $400.99
861: Test, price: $160.99
868: Test, price: $417.99
207: Test, price: $268.99
551: Test, price: $114.99
278: Test, price: $804.99
520: Test, price: $554.99
140: Test, price: $530.99

</spoiler>
Как видно из Store.toString(), в результате мы получаем многоуровневую архитектуру контейнеров, не лишаясь преимуществ безопасности типов и управляемости. Впечатляет и то, что построение такой модели не потребует заметных умственных усилий.

Тайна стирания

Когда вы приступаете к более глубокому изучению контейнеров, некоторые обстоятельства на первых порах выглядят довольно странно. Например, запись ArrayList.class возможна, а запись ArrayList<Integer>.class — нет. Или возьмите следующий фрагмент:

//: generics/ErasedTypeEquivalence.java
import java.util.*;
 
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}

<spoiler text="Output:">

true

</spoiler>
Было бы логично считать, что ArrayList<String> и ArrayList<Integer> — разные типы, поэтому их поведение должно различаться, и при попытке поместить Integer в ArrayList<String> результат (неудача) должен отличаться от того, который будет получен при помещении Integer в ArrayList<Integer> (успех). Однако эта программа создает впечатление, что эти типы одинаковы. Следующий пример еще сильнее запутывает ситуацию:

//: generics/LostInformation.java
import java.util.*;
 
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION,MOMENTUM> {}
 
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<Frob>();
Map<Frob,Fnorkle> map = new HashMap<Frob,Fnorkle>();
Quark<Fnorkle> quark = new Quark<Fnorkle>();
Particle<Long,Double> p = new Particle<Long,Double>();
System.out.println(Arrays.toString(
list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
p.getClass().getTypeParameters()));
}
}

<spoiler text="Output:">

[E]
[K, V]
[Q]
[POSITION, MOMENTUM]

</spoiler>
Согласно документации JDK, Class.getTypeParameters() «возвращает массив объектов TypeVariable, представляющих переменные типов, указанные в параметризованном объявлении...» Казалось бы, по ним можно определить параметры типов — но, как видно из результатов, вы всего лишь узнаете, какие идентификаторы использовались в качестве заполнителей, а эта информация не представляет особого интереса.
Мы приходим к холодной, бездушной истине:
Информация о параметрах типов недоступна внутри параметризованного кода.
Таким образом, вы можете узнать идентификатор параметра типа и ограничение параметризованного типа, но фактические параметры типов, использованные для создания конкретного экземпляра, остаются неизвестными. Этот факт, особенно раздражающий программистов с опытом работы на C++, является основной проблемой, которую приходится решать при использовании параметризации в Java.

Параметризация в Java реализуется с применением стирания (erasure). Это означает, что при использовании параметризации вся конкретная информация о типе утрачивается. Внутри параметризованного кода вы знаете только то, что используется некий объект. Таким образом, List<String> и List<Integer> действительно являются одним типом во время выполнения; обе формы «стираются» до своего низкоуровневого типа List. Именно стирание и создаваемые им проблемы становятся главной преградой при изучении параметризации в Java; этой теме и будет посвящен настоящий раздел.

Подход C++

В следующем примере, написанном на C++, используются шаблоны. Синтаксис параметризованных типов выглядит знакомо, потому что многие идеи C++ были взяты за основу при разработке Java:

#include <iostream>
using namespace std;
 
template<class T> class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
 
class HasF {
public:
void f() { cout << "HasF::f()" << endl; }
};
 
int main() {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}

<spoiler text="Output:">

HasF-:f()

</spoiler>
Класс Manipulator хранит объект типа Т. Нас здесь интересует метод manipulate(), который вызывает метод f() для obj. Как он узнает, что у параметра типа Т существует метод f()? Компилятор C++ выполняет проверку при создании экземпляра шаблона, поэтому в точке создания Manipulator<HasF> он узнает о том, что HasF содержит метод f(). В противном случае компилятор выдает ошибку, а безопасность типов сохраняется.
Написать такой код на C++ несложно, потому что при создании экземпляра шаблона код шаблона знает тип своих параметров. С параметризацией Java дело обстоит иначе. Вот как выглядит версия HasF, переписанная на Java:

//: generics/HasF.java
public class HasF {
public void f() { System.out.println("HasF.f()"); }
}

Если мы возьмем остальной код примера и перепишем его на Java, он не будет компилироваться:

//: generics/Manipulation.java
// {CompileTimeError} (He компилируется)
 
class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
// Error: cannot find symbol: method f():
public void manipulate() { obj.f(); }
}
 
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator =
new Manipulator<HasF>(hf);
manipulator.manipulate();
}
}

Из-за стирания компилятор Java не может сопоставить требование о возможности вызова f() для obj из manipulate() с тем фактом, что HasF содержит метод f(). Чтобы вызвать f(), мы должны «помочь» параметризованному классу, и передать ему ограничение; компилятор принимает только те типы, которые соответствуют указанному ограничению. Для задания ограничения используется ключевое слово extends. При заданном ограничении следующий фрагмент компилируется нормально:

//: generics/Manipulator2.java
class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
}

Ограничение <T extends HasF> указывает на то, что параметр Т должен относиться к типу HasF или производному от него. Если это условие выполняется, то вызов f() для obj безопасен.
Можно сказать, что параметр типа стирается до первого ограничения (как будет показано позже, ограничений может быть несколько). Мы также рассмотрим понятие стирания параметра типа. Компилятор фактически заменяет параметр типа его «стертой» версией, так что в предыдущем случае Т стирается до HasF, а результат получается таким, как при замене Т на HasF в теле класса.
Справедливости ради нужно заметить, что в Manipulation2.java параметризация никакой реальной пользы не дает. С таким же успехом можно выполнить стирание самостоятельно, создав непараметризованный класс:

//: generics/Manipulator3.java
class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
}

Мы приходим к важному заключению: параметризация полезна только тогда, когда вы хотите использовать параметры типов, более «общие», нежели конкретный тип (и производные от него), то есть когда код должен работать для разных классов. В результате параметры типов и их применение в параметризованном коде сложнее простой замены классов. Впрочем, это не означает, что форма <Т extends HasF> чем-то ущербна. Например, если класс содержит метод, возвращающий Т, то параметризация будет полезной, потому что метод вернет точный тип:

//: generics/ReturnGenericType.java
class ReturnGenericType<T extends HasF> {
private T obj;
public ReturnGenericType(T x) { obj = x; }
public T get() { return obj; }
}

Просмотрите код и подумайте, достаточно ли он «сложен» для применения параметризации.
Ограничения будут более подробно рассмотрены далее в этой главе.

Миграционная совместимость

Чтобы избежать всех потенциальных недоразумений со стиранием, необходимо четко понимать, что этот механизм не является особенностью языка. Скорее это компромисс, использованный при реализации параметризации в Java, потому что параметризация не являлась частью языка в его исходном виде. Этот компромисс создает определенные неудобства, поэтому вы должны поскорее привыкнуть к нему и понять, почему он существует.
Если бы параметризация была частью Java 1.0, то для ее реализации стирание не потребовалось бы — параметры типов сохранили бы свой статус равноправных компонентов языка, и с ними можно было бы выполнять типизованные языковые и рефлексивные операции. Позднее в этой главе будет показано, что стирание снижает «обобщенность» параметризованных типов. Параметризация в Java все равно приносит пользу, но не такую, какую могла бы приносить, и причиной тому является стирание.
В реализации, основанной на стирании, параметризованные типы рассматриваются как второстепенные компоненты языка, которые не могут использоваться в некоторых важных контекстах. Параметризованные типы присутствуют только при статической проверке типов, после чего каждый параметризованный тип в программе заменяется параметризованным верхним ограничением. Например, обозначения типов вида List<T> стирается до List, а обычные переменные типа — до Object, если ограничение не задано.
Главная причина для применения стирания заключается в том, что оно позволяет параметризованным клиентам использовать ^параметризованные библиотеки, и наоборот. Эта концепция часто называется миграционной совместимостью. Наверное, в идеальном мире параметризация была бы внедрена везде и повсюду одновременно. На практике программисту, даже если он пишет только параметризованный код, приходится иметь дело с параметризованными библиотеками, написанными до Java SE5. Возможно, авторы этих библиотек вообще не намерены параметризовать свой код или собираются сделать это в будущем.
Из-за этого механизму параметризации Java приходится поддерживать не только обратную совместимость (существующий код и файлы классов остаются абсолютно законными и сохраняют свой прежний смысл), но и миграционную совместимость — чтобы библиотеки могли переводиться в параметризованную форму в собственном темпе, причем их параметризация не влияла бы на работу зависящего от него кода и приложений. Выбрав эту цель, проектировщики Java и различные группы, работавшие над проблемой, решили, что единственным приемлемым решением является стирание, позволяющее непараметризованному коду нормально сосуществовать с параметризованным.

Проблемы стирания

Итак, главным аргументом для применения стирания является процесс перехода с непараметризованного кода на параметризованный и интеграция параметризации в язык без нарушения работы существующих библиотек. Стирание позволяет использовать существующий ^параметризованный код без изменений, пока клиент не будет готов переписать свой код с использованием параметризации.
Однако за стирание приходится расплачиваться. Параметризованные типы не могут использоваться в операциях, в которых явно задействованы типы времени выполнения — преобразования типов, instanceof и выражения new. Вся ин­формация о типах параметров теряется, и при написании параметризованного кода вам придется постоянно напоминать себе об этом. Допустим, вы пишете фрагмент кода

 class Foo<T> { 
T var;
}

Может показаться, что при создании экземпляра Foo:

 Foo<Cat> f = new Foo<Cat>();

код class Foo должен знать, что он работает с Cat. Синтаксис создает впечатление, что тип Т подставляется повсюду внутри класса. Но на самом деле это не так, и при написании кода для класса вы должны постоянно напоминать себе: «Нет, это всего лишь Object».
Кроме того, стирание и миграционная совместимость означают, что контроль за использованием параметризации не настолько жесткий, как хотелось бы:

//: generics/ErasureAndInheritance.java
class GenericBase<T> {
private T element;
public void set(T arg) { arg = element; }
public T get() { return element; }
}
 
class Derived1<T> extends GenericBase<T> {}
 
class Derived2 extends GenericBase {} // Без предупреждений
 
// class Derived3 extends GenericBase<?> {}
// Странная ошибка.
// Обнаружен непредвиденный тип : ?
// требуется- класс или интерфейс без ограничений
 
public class ErasureAndInheritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here!
}
}

Derived2 наследует от GenericBase без параметризации, и компилятор не выдает при этом никаких предупреждений. Предупреждение выводится позже, при вызове set().
Для подавления этого предупреждения в Java существует директива, приведенная в листинге (до выхода Java SE5 она не поддерживалась):

@SuppressWarnings("unchecked")

Обратите внимание: директива применяется к методу, генерирующему предупреждение, а не ко всему классу. При подавлении предупреждений желательно действовать в самых узких рамках, чтобы случайно не скрыть настоящую проблему.
Ошибка, выдаваемая в Derived3, означает, что компилятор рассчитывает увидеть «обычный» базовый класс. Добавьте к этому дополнительные усилия на управление ограничениями, если вы не желаете интерпретировать параметр типа как простой Object, — и что мы получаем в остатке? Гораздо больше хлопот при гораздо меньше, пользе по сравнению с параметризованными типами в языках вроде C++, Ada или Eiffel. Конечно, это вовсе не означает, что эти языки в целом эффективнее Java в большинстве задач программирования, а говорит лишь о том, что их механизмы параметризации типов отличаются большей гибкостью и мощью, чем в Java.

Проблемы на границах

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

//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
 
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) { this.kind = kind; }
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[])Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMaker =
new ArrayMaker<String>(String.class);
String[] stringArray = stringMaker.create(9);
System.out.println(Arrays.toString(stringArray));
}
}

<spoiler text="Output:">

[null, null, null. null, null, null. null. null, null]

</spoiler>
Несмотря на то что объект kind хранится в виде Class<T>, стирание означает, что фактически он хранится в виде Class без параметра. Следовательно, при выполнении с ним каких-либо операций (например, при создании массива) Array.newInstance() не обладает информацией о типе, подразумеваемой kind. Метод не сможет выдать нужный результат, не требующий преобразования типа, а это приводит к выдаче предупреждения, с которым вам не удастся справиться.
Обратите внимание: для создания массивов в параметризованном коде рекомендуется использовать Array.newInstance().
Если вместо массива создается другой контейнер, ситуация меняется:

//: generics/ListMaker.java
import java.util.*;
 
public class ListMaker<T> {
List<T> create() { return new ArrayList<T>(); }
public static void main(String[] args) {
ListMaker<String> stringMaker= new ListMaker<String>();
List<String> stringList = stringMaker.create();
}
}

Компилятор не выдает предупреждений, хотя мы знаем, что <Т> в new ArrayList<T>() внутри create() удаляется — во время выполнения <Т> внутри класса нет, поэтому здесь его присутствие выглядит бессмысленным. Однако если вы попробуете применить эту идею на практике и преобразуете выражение в new ArrayList(), компилятор выдаст предупреждение.
Но действительно ли этот элемент не имеет смысла? Что произойдет, если мы поместим в список несколько объектов, прежде чем возватим его?

//: generics/FilledListMaker.java
import java.util.*;
 
public class FilledListMaker<T> {
List<T> create(T t, int n) {
List<T> result = new ArrayList<T>();
for(int i = 0; i < n; i++)
result.add(t);
return result;
}
public static void main(String[] args) {
FilledListMaker<String> stringMaker =
new FilledListMaker<String>();
List<String> list = stringMaker.create("Hello", 4);
System.out.println(list);
}
}

<spoiler text="Output:">

[Hello, Hello. Hello. Hello]

</spoiler>
Хотя компилятор ничего не может знать о Т в create(), он все равно способен проверить — на стадии компиляции — что заносимые в result объекты имеют тип Т и согласуются с ArrayList<T>. Таким образом, несмотря на то что стирание удаляет информацию о фактическом типе внутри метода или класса, компилятор все равно может проверить корректность использования типа в методе или классе.
Так как стирание удаляет информацию о типе внутри тела метода, на стадии выполнения особую роль приобретают границы — точки, в которых объект входит и выходит из метода. Именно в этих точках компилятор выполняет проверку типов и вставляет код преобразования. Рассмотрим следующий параметризованный пример:

//: generics/SimpleHolder.java
public class SimpleHolder {
private Object obj;
public void set(Object obj) { this.obj = obj; }
public Object get() { return obj; }
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String)holder.get();
}
}

Декомпилировав результат командой javap -с SimpleHolder, мы получим (после редактирования):
<spoiler text="Byte-code:">

public void set(java lang Object);

0: aload_0
1: aload 1
2: putfield #2; II Поле obj.Object;
5: return public java lang.Object get().
0: aload 0
1: getfield #2; II Поле obj-Object,
4: areturn public static void main(java lang.StringE]);
0: new #3, // Класс SimpleHolder
3: dup
4: invokespecial #4; // Метод "<init>".()V
7: astore_l
8: aload 1
9: ldc #5; II String Item
11: invokevirtual #6; // Метод set (Object;)V
14: aload_l
15: invokevirtual #7, // Метод get:()Object:
18: checkcast #8, //'Класс java/lang/String
21: astore_2
22: return

</spoiler>
Методы set() и get() просто записывают и читают значение, а преобразование проверяется в точке вызова get().
Теперь включим параметризацию в приведенный фрагмент:

//: generics/GenericHolder.java
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder =
new GenericHolder<String>();
holder.set("Item");
String s = holder.get();
}
}

Необходимость преобразования выходного значения get() отпала, но мы также знаем, что тип значения, передаваемого set(), проверяется во время компиляции. Соответствующий байт-код:
<spoiler text="Byte-code:">

public void set(java.lang.Object);

0:aload_0
1:aload_l
2:putfield #2: // Поле obj:0bject:
5:return public java.lang.Object get():
0:aload_0
1:getfield #2; // Поле obj:0bject:
4:areturn public static void main(java.lang.String[]);
0.new #3: // Класс GenericHolder
3:dup
4:invokespecial #4; // Метод "<init>"-()V
7:astore_l
8:aload_l
9:ldc #5; // String Item
11:invokevirtual #6; II Метод set:(Object:)V
14:aload_l
15:invokevirtual #7; // Метод get:()Object:
18:checkcast #8: // Класс java/lang/String
21:astore_2
22:return

</spoiler>
Как видите, байт-код идентичен. Дополнительная работа по проверке входного типа set() выполняется компилятором «бесплатно». Преобразование выходного значения get() по-прежнему сохранилось, но, по крайней мере, вам не приходится выполнять его самостоятельно — оно автоматически вставляется компилятором.

Компенсация за стирание

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

//: generics/Erased.java
// {CompileTimeError} (He компилируется)
 
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
if(arg instanceof T) {} // Error
T var = new T(); // Error
T[] array = new T[SIZE]; // Error
T[] array = (T)new Object[SIZE]; // Unchecked warning
}
}

Иногда такие проблемы удается обойти на программном уровне, но в отдельных случаях стирание приходится компенсировать посредством введения метки типа. Другими словами, вы явно передаете объект Class для своего типа.
Например, попытка использования instanceof в предыдущем примере завершилась неудачей из-за того, что информация о типе была стерта. При введении метки типа вместо instanceof можно использовать динамический метод isInstance():

//: generics/ClassTypeCapture.java
class Building {}
class House extends Building {}
 
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 =
new ClassTypeCapture<Building>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 =
new ClassTypeCapture<House>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}

<spoiler text="Output:">

true
true
false
true

</spoiler>
Компилятор следит за тем, чтобы метка типа соответствовала обобщенному аргументу.

Создание экземпляров типов

Попытка создания new T() в Erased.java не работает отчасти из-за стирания, а отчасти из-за того, что компилятор не может убедиться в наличии у Т конструктора по умолчанию (без аргументов). Но в C++ эта операция естественна, прямолинейна и безопасна (проверка выполняется во время компиляции):

//: generics/InstantiateGenericType.java
import static net.mindview.util.Print.*;
class ClassAsFactory<T> {
T x;
public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
 
class Employee {}
 
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe =
new ClassAsFactory<Employee>(Employee.class);
print("ClassAsFactory<Employee> успех");
try {
ClassAsFactory<Integer> fi =
new ClassAsFactory<Integer>(Integer.class);
} catch(Exception e) {
print("ClassAsFactory<Integer> неудача");
}
}
}

<spoiler text="Output:">

ClassAsFactory<Employee> успех 
ClassAsFactory<Integer> неудача

</spoiler>
Программа компилируется, но с ClassAsFactory<Integer> происходит сбой, так как Integer не имеет конструктора по умолчанию. Ошибка не обнаруживается во время компиляции, поэтому специалисты из Sun считают такие решения нежелательными. Вместо этого рекомендуется использовать явную фабрику и ограничивать тип, чтобы принимался только класс, реализующий эту фабрику:

//: generics/FactoryConstraint.java
interface FactoryI<T> {
T create();
}
 
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
// ...
}
 
class IntegerFactory implements FactoryI<Integer> {
public Integer create() {
return new Integer(0);
}
}
 
class Widget {
public static class Factory implements FactoryI<Widget> {
public Widget create() {
return new Widget();
}
}
}
 
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.Factory());
}
}

В сущности, это всего лишь разновидность передачи Class<T>. В обоих вариантах передаются объекты фабрик; просто в случае с Class<T> объект фабрики оказывается встроенным, а при предыдущем решении он создается явно. Тем не менее в обоих случаях реализуется проверка времени компиляции.
Другое решение основано на использовании паттерна «шаблонный метод». В следующем примере get() — шаблонный метод, a create() определяется в субклассе для получения объекта этого типа:

//: generics/CreatorGeneric.java
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() { element = create(); }
abstract T create();
}
 
class X {}
 
class Creator extends GenericWithCreate<X> {
X create() { return new X(); }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
 
public class CreatorGeneric {
public static void main(String[] args) {
Creator c = new Creator();
c.f();
}
}

<spoiler text="Output:">

X

</spoiler>

Массивы параметризованных типов

Как мы видели в Erased.java, создавать массивы параметризованных типов нельзя. Везде, где возникает необходимость в создании таких массивов, следует применять ArrayList:

//: generics/ListOfGenerics.java
import java.util.*;
 
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<T>();
public void add(T item) { array.add(item); }
public T get(int index) { return array.get(index); }
}

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

//: generics/ArrayOfGenericReference.java
class Generic<T> {}
 
public class ArrayOfGenericReference {
static Generic<Integer>[] gia;
}

Компилятор принимает эту запись без каких-либо предупреждений. С другой стороны, вы не сможете создать массив указанного типа (включая параметры типа), поэтому все это сбивает с толку. Поскольку все массивы обладают одинаковой структурой (размер каждого элемента и способ размещения в памяти) независимо от типа хранящихся данных, создается впечатление, что вы сможете создать массив Object и преобразовать его к нужному типу. Код откомпилируется, но работать не будет — он выдает исключение ClassCastException:

//: generics/ArrayOfGeneric.java
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// Компилируется, но приводит к ClassCastException:
//! gia = (Generic<Integer>[])new Object[SIZE];
// Тип времени выполнения является "стертым" type:
gia = (Generic<Integer>[])new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
//! gia[1] = new Object(); // Ошибка компиляции
// Обнаруживается несоответствие типов во время компиляции:
//! gia[2] = new Generic<Double>();
}
}

<spoiler text="Output:">

Generic[]

</spoiler>
Проблема в том, что массивы отслеживают свой фактический тип, который задается в точке создания массива. Таким образом, даже несмотря на то, что gia преобразуется в Generic<Integer>[], эта информация существует только на стадии компиляции (а без директивы @SuppressWarnings вы получите предупреждение). Во время выполнения мы по-прежнему имеем дело с массивом Object, и это создает проблемы. Успешно создать массив параметризованного типа можно только одним способом — создать новый массив «стертого» типа и выполнить преобразование.
Рассмотрим чуть более сложный пример. Допустим, имеется простая параметризованная «обертка» для массива:

//: generics/GenericArray.java
 
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[])new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
// Метод, предоставляющий доступ к базовому представлению:
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArray<Integer> gai =
new GenericArray<Integer>(10);
// Приводит к ClassCastException:
//! Integer[] ia = gai.rep();
// А так можно
Object[] oa = gai.rep();
}
}

Как и прежде, мы не можем использовать запись Т[] array = new T[sz], поэтому мы создаем массив объектов и преобразуем его.
Метод rер() возвращает Т[]; в методе main() для gai это должен быть тип Integer[], но при попытке вызова и сохранения результата по ссылке на Integer[] будет получено исключение ClassCastException — это снова происходит из-за того, что фактическим типом объекта времени выполнения является Object[]. Если мы немедленно проводим преобразование к Т[], то на стадии компиляции фактический тип массива теряется и компилятор может упустить некоторые потенциальные ошибки. Из-за этого лучше использовать в коллекции Object[], а затем добавить преобразование к Т при использовании элемента массива. Вот как это будет выглядеть в примере GenericArray.java:

//: generics/GenericArray2.java
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz) {
array = new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) { return (T)array[index]; }
@SuppressWarnings("unchecked")
public T[] rep() {
return (T[])array; // Предупреждение: непроверенное преобразование
}
public static void main(String[] args) {
GenericArray2<Integer> gai =
new GenericArray2<Integer>(10);
for(int i = 0; i < 10; i ++)
gai.put(i, i);
for(int i = 0; i < 10; i ++)
System.out.print(gai.get(i) + " ");
System.out.println();
try {
Integer[] ia = gai.rep();
} catch(Exception e) { System.out.println(e); }
}
}

<spoiler text="Output:"> (Sample)

0 12 3 4 5 6 7 8 9
java.lang.ClassCastException:
[Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

</spoiler>
На первый взгляд почти ничего не изменилось, разве что преобразование типа было перемещено. Без директив @SuppressWarnings вы по-прежнему будете получать предупреждения, но теперь во внутренней реализации используется Object[] вместо Т[]. При вызове get() объект преобразуется к Т; это правильный тип, поэтому преобразование безопасно. Но при вызове rер() снова делается попытка преобразования Object[] в Т[], которое остается неверным; в результате вы получите предупреждение во время компиляции и исключение во время выполнения. Не существует способа обойти тип базового массива, которым может быть только Object[]. У внутренней интерпретации array как Object[] вместо Т[] есть свои преимущества: например, вы с меньшей вероятностью забудете тип массива, что приведет к случайному появлению ошибок (впрочем, подавляющее большинство таких ошибок будет быстро выявлено на стадии выполнения).
В новом коде следует передавать метку типа. В обновленной версии GenericArray выглядит так:

//: generics/GenericArrayWithTypeToken.java
import java.lang.reflect.*;
 
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[])Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
// Expose the underlying representation:
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai =
new GenericArrayWithTypeToken<Integer>(
Integer.class, 10);
// This now works:
Integer[] ia = gai.rep();
}
}

Метка типа Class<T> передается конструктору для восстановления информации после стирания, чтобы мы могли создать фактический тип нужного массива (предупреждения при преобразовании по-прежнему приходится подавлять @SuppressWarnings). Получив фактический тип, мы возвращаем его для получения желаемых результатов, как видно из main().
К сожалению, просмотрев исходный код стандартных библиотек Java SE5, вы увидите, что преобразования массивов Object в параметризованные типы происходят повсеместно. Например, вот как выглядит копирующий конструктор для создания ArrayList из Collection после некоторой правки и упрощения:

 public ArrayList(Collection с) { 
size = c.size();
elementData = (E[])new Object[size];
с.toArray(elementData):
}

В ArrayList.java подобные преобразования встречаются неоднократно. И конечно, при их компиляции выдается множество предупреждений.

Ограничения

Ограничения, уже упоминавшиеся ранее в этой главе, сужают круг параметров типов, используемых при параметризации. Хотя это позволяет предъявлять требования к типам, к которым применяется ваш параметризованный код, у ог­раничений имеется и другой, потенциально более важный эффект: возможность вызова методов, определенных в ограничивающих типах.
Поскольку стирание уничтожает информацию о типе, при отсутствии ограничений для параметров типов могут вызываться только методы Object. Но, если ограничить параметр подмножеством типов, вы сможете вызвать методы из этого подмножества. Для установления ограничений в Java используется ключевое слово extends. Важно понимать, что в контексте параметризации extends имеет совершенно иной смысл, нежели в обычной ситуации. Следующий пример демонстрирует основы установления ограничений:

//: generics/BasicBounds.java
 
interface HasColor { java.awt.Color getColor(); }
 
class Colored<T extends HasColor> {
T item;
Colored(T item) { this.item = item; }
T getItem() { return item; }
// Ограничение позволяет вызвать метод:
java.awt.Color color() { return item.getColor(); }
}
 
class Dimension { public int x, y, z; }
 
// Не работает -- сначала класс, потом интерфейсы:
// class ColoredDimension<T extends HasColor & Dimension> {
 
// Несколько ограничений-
class ColoredDimension<T extends Dimension & HasColor> {
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; }
java.awt.Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
 
interface Weight { int weight(); }
 
// Как и при наследовании, конкретный класс может быть только один,
// а интерфейсов может быть несколько:
class Solid<T extends Dimension & HasColor & Weight> {
T item;
Solid(T item) { this.item = item; }
T getItem() { return item; }
java.awt.Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
int weight() { return item.weight(); }
}
 
class Bounded
extends Dimension implements HasColor, Weight {
public java.awt.Color getColor() { return null; }
public int weight() { return 0; }
}
 
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid =
new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}

Вероятно, вы заметили, что пример BasicBounds.java содержат некоторую избыточность, которая может быть устранена посредством наследования. С каждым уровнем наследования добавляются новые ограничения:

//: generics/InheritBounds.java
class HoldItem<T> {
T item;
HoldItem(T item) { this.item = item; }
T getItem() { return item; }
}
 
class Colored2<T extends HasColor> extends HoldItem<T> {
Colored2(T item) { super(item); }
java.awt.Color color() { return item.getColor(); }
}
 
class ColoredDimension2<T extends Dimension & HasColor>
extends Colored2<T> {
ColoredDimension2(T item) { super(item); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
 
class Solid2<T extends Dimension & HasColor & Weight>
extends ColoredDimension2<T> {
Solid2(T item) { super(item); }
int weight() { return item.weight(); }
}
 
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 =
new Solid2<Bounded>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
}
}

HoldItem просто хранит объект; это поведение наследуется классом Colored2, который также требует, чтобы его параметр реализовывал HasColor. ColoredDimension2 и Solid2 продолжают расширение иерархии и добавляют на каждом уровне новые ограничения. Теперь методы наследуются, и их не нужно повторять в каждом классе.
Пример с большим количеством уровней:

//: generics/EpicBattle.java
// Demonstrating bounds in Java generics.
import java.util.*;
 
interface SuperPower {}
interface XRayVision extends SuperPower {
void seeThroughWalls();
}
interface SuperHearing extends SuperPower {
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower {
void trackBySmell();
}
 
class SuperHero<POWER extends SuperPower> {
POWER power;
SuperHero(POWER power) { this.power = power; }
POWER getPower() { return power; }
}
 
class SuperSleuth<POWER extends XRayVision>
extends SuperHero<POWER> {
SuperSleuth(POWER power) { super(power); }
void see() { power.seeThroughWalls(); }
}
 
class CanineHero<POWER extends SuperHearing & SuperSmell>
extends SuperHero<POWER> {
CanineHero(POWER power) { super(power); }
void hear() { power.hearSubtleNoises(); }
void smell() { power.trackBySmell(); }
}
 
class SuperHearSmell implements SuperHearing, SuperSmell {
public void hearSubtleNoises() {}
public void trackBySmell() {}
}
 
class DogBoy extends CanineHero<SuperHearSmell> {
DogBoy() { super(new SuperHearSmell()); }
}
 
public class EpicBattle {
// Ограничения в параметризованных методах:
static <POWER extends SuperHearing>
void useSuperHearing(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
}
static <POWER extends SuperHearing & SuperSmell>
void superFind(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogBoy dogBoy = new DogBoy();
useSuperHearing(dogBoy);
superFind(dogBoy);
// Так можно:
List<? extends SuperHearing> audioBoys;
// А так нельзя:
// List<? extends SuperHearing & SuperSmell> dogBoys;
}
}

Метасимволы

Мы уже встречали простые примеры использования метасимволов — вопросительных знаков в выражениях аргументов параметризации — в главах 11 и 13. В этом разделе тема будет рассмотрена более подробно.
Начнем с примера, демонстрирующего одну особенность массивов: массив производного типа можно присвоить ссылке на массив базового типа:

//: generics/CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
 
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Тип времени выполнения - Apple[], а не Fruit[] или Orange[]:
try {
// Компилятор позволяет добавлять объекты Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Компилятор позволяет добавлять объекты Orange:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
}

<spoiler text="Output:">

java.1ang.ArrayStoreException: Fruit 
java.1ang.ArrayStoreException: Orange

</spoiler>
Первая строка main() создает массив Apple и присваивает его ссылке на массив Fruit. Выглядит логично — Apple является разновидностью Fruit, поэтому массив Apple также одновременно должен быть массивом Fruit.
С другой стороны, если фактическим типом массива является Аррlе[], в массиве можно разместить только Apple или субтип Apple, причем это правило должно соблюдаться как во время компиляции, так и во время выполнения. Но обратите внимание на то, что компилятор также позволит разместить в массиве ссылку на объект Fruit. Для компилятора это вполне логично, потому что он имеет дело со ссылкой Fruit[] — так почему бы не разрешить занести в массив объект Fruit или любого типа, производного от Fruit, — скажем, Orange? Во время компиляции это разрешено. Однако механизм времени выполнения знает, что он имеет дело с Apple[], и при попытке занесения постороннего типа происходит исключение.
Впрочем, для массивов это не создает особых проблем, потому что при вставке объекта неверного типа вы об этом очень быстро узнаете во время выполнения. Но одна из основных целей параметризации как раз и состоит в том, чтобы по возможности переместить выявление подобных ошибок на стадию выполнения. Итак, что же произойдет при использовании параметризованных контейнеров вместо массивов?

//: generics/NonCovariantGenerics.java
// {CompileTimeError} (Won't compile)
import java.util.*;
 
public class NonCovariantGenerics {
// Ошибка компиляции: несовместимые типы
List<Fruit> flist = new ArrayList<Apple>();
}

На первый взгляд это выглядит как утверждение «Контейнер с элементами Apple нельзя присвоить контейнеру с элементами Fruit», но следует вспомнить, что параметризация — это не только контейнеры. В действительности утверждение следует трактовать шире: «Параметризованный тип, в котором задействован тип Apple, нельзя присвоить параметризованному типу, в котором задействован тип Fruit». Если бы, как в случае с массивами, компилятор располагал достаточной информацией и мог понять, что речь идет о контейнерах, он мог бы проявить некоторую снисходительность. Но компилятор такой информацией не располагает, поэтому он отказывается выполнить «восходящее преобразование». Впрочем, это и не является восходящим преобразованием — List с элементами Apple не является «частным случаем» List с элементами Fruit. Первый может хранить Apple и подтипы Apple, а второй — любые разновидности Fruit... да, в том числе и Apple, но от этого он не становится List с элементами Apple, а по-прежнему остается List с элементами Fruit.
Проблема в том, что речь идет о типе контейнера, а не о типе элементов, которые в этом контейнере хранятся. В отличие от массивов, параметризованные типы не обладают встроенной ковариантностью. Это связано с тем, что массивы полностью определяются в языке и для них могут быть реализованы встроенные проверки как во время компиляции, так и во время выполнения, но с параметризованными типами компилятор и система времени выполнения не знают, что вы собираетесь делать с типами и какие правила при этом должны действовать.
Но иногда между двумя разновидностями параметризованных типов все же требуется установить некоторую связь, аналогичную восходящему преобразованию. Именно это и позволяют сделать метасимволы.

//: generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Метасимволы обеспечивают ковариантность:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Ошибка компиляции: добавление объекта
// произвольного типа невозможно
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Можно, но неинтересно
// Мы знаем, что возвращается по крайней мере Fruit:
Fruit f = flist.get(0);
}
}

Теперь flist относится к типу List<? extends Fruit>, что можно прочитать как «список с элементами любого типа, производного от Fruit». Однако в действительности это не означает, что List будет содержать именно типы из семейства Fruit. Метасимвол обозначает «некоторый конкретный тип, не указанный в ссылке flist». Таким образом, присваиваемый List должен содержать некий конкретный тип (например, Fruit или Apple), но для восходящего преобразования к flist этот тип несущественен.
Если единственное ограничение состоит в том, что List содержит Fruit или один из его подтипов, но вас не интересует, какой именно, что же с ним можно сделать? Если вы не знаете, какие типы хранятся в List, возможно ли безопасное добавление объекта? Нет, как и в случае с CovariantArrays.java, но на этот раз ошибка выявляется компилятором, а не системой времени выполнения.
Может показаться, что такой подход не совсем логичен — вам не удастся даже добавить Apple в List, в котором, как вы только что указали, должны храниться Apple. Да, конечно, но компилятор-то этого не знает! List<? extends Fruit> вполне может указывать на List<Orange>.
С другой стороны, вызов метода, возвращающего Fruit, безопасен; мы знаем, что все элементы List должны по меньшей мере относиться к Fruit, поэтому компилятор это позволит.

Насколько умен компилятор?

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

//: generics/CompilerIntelligence.java
import java.util.*;
 
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
}

Как видите, вызовы contains() и indexOf() с аргументами Apple воспринимаются нормально. Означает ли это, что компилятор действительно анализирует код, чтобы узнать, модифицирует ли некоторый метод свой объект?
Просмотр документации ArrayList показывает, что компилятор не настолько умен. Если add() получает аргумент параметризующего типа, contains() и indexOf() получают аргумент типа Object. Таким образом, когда вы указываете ArrayList<? extends Fruit>, аргумент add() превращается в <? extends Fruit>. По этому описанию компилятор не может определить, какой именно подтип Fruit требуется в данном случае, поэтому не принимает никакие типы Fruit. Даже если вы предварительно преобразуете Apple в Fruit, компилятор все равно откажется вызывать метод (например, add()), если в списке аргументов присутствует метасимвол.
У методов contains() и indexOf() аргументы относятся к типу Object, метасимволы в них отсутствуют, поэтому компилятор разрешает вызов. Это означает, что проектировщик параметризованного класса должен сам решить, какие вызовы «безопасны», и использовать типы Object для их аргументов. Чтобы сделать невозможным вызов при использовании типа с метасимволами, включите параметр типа в список аргументов.
В качестве примера рассмотрим очень простой класс Holder:

//: generics/GenericHolder.java
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder =
new GenericHolder<String>();
holder.set("Item");
String s = holder.get();
}
}
//---------------------------------------
//: generics/Holder.java
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Повышение невозможно
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Возвращает 'Object'
try {
Orange c = (Orange)fruit.get(); // Предупреждения нет
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Вызов set() невозможен
// fruit.set(new Fruit()); // Вызов set() невозможен
System.out.println(fruit.equals(d)); // OK
}
}

<spoiler text="Output:"> (Sample)

java.lang.ClassCastException. Apple cannot be cast to Orange
true

</spoiler>
Holder содержит метод set(), получающий T; метод get(), возвращающий Т; и метод equals(), получающий Object. Как вы уже видели, Holder<Apple> невозможно преобразовать в Holder<Fruit>, но зато можно в Holder<? extends Fruit>. При вызове get() будет возвращен только тип Fruit — то, что известно компилятору по ограничению «все, что расширяет Fruit». Если вы располагаете дополнительной информацией, то сможете выполнить преобразование к конкретному типу Fruit и обойтись без предупреждений, но с риском исключения ClassCastException. Метод set() не работает ни с Apple, ни с Fruit, потому что аргумент set() тоже содержит «? extends Fruit»; по сути, он может быть чем угодно, а компилятор не может проверить безопасность типов для «чего угодно».
Впрочем, метод equals() работает нормально, потому что он получает Object вместо Т. Таким образом, компилятор обращает внимание только на типы передаваемых и возвращаемых объектов. Он не анализирует код, проверяя, выпол­няются ли реальные операции чтения или записи.

Контравариантность

Также можно пойти другим путем и использовать метасимволы супертипов. В этом случае вы сообщаете, что метасимвол ограничивается базовым классом некоторого класса; при этом используется запись <? super MyClass>, и даже с па­раметром типа <? super Т>. Это позволяет безопасно передавать типизованный объект параметризованному типу. Таким образом, с использованием метасимволов супертипов становится возможной запись в коллекцию:

//: generics/SuperTypeWildcards.java
import java.util.*;
 
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}

Аргумент apples является контейнером List для некоторого типа, являющегося базовым для Apple; из этого следует, что Apple и производные от Apple типы могут безопасно включаться в контейнер. Но, поскольку нижним ограничением является Apple, мы не знаем, безопасно ли включать Fruit в такой List, так как это откроет List для добавления типов, отличных от Apple, с нарушением статической безопасности типов.
Ограничения супертипов расширяют возможности по передаче аргументов методу:

//: generics/GenericWriting.java
import java.util.*;
 
public class GenericWriting {
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static void f1() {
writeExact(apples, new Apple());
// writeExact(fruit, new Apple()); // Ошибка:
// Несовместимые типы: обнаружен Fruit, требуется Apple
}
static <T> void
writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) { f1(); f2(); }
}

Метод writeExact() использует параметр типа «как есть», без метасимволов. На примере f1() мы видим, что этот способ отлично работает — при условии, что в List<Apple> помещаются только объекты Apple. Однако writeExact() не позволяет поместить Apple в List<Fruit>, хотя мы знаем, что это должно быть возможно.
В writeWithWildcard() используется аргумент List<? super Т>, поэтому List содержит конкретный тип, производный от Т; следовательно, Т или производные от него типы могут безопасно передаваться в аргументе методов List. Пример встречается в f2: как и прежде, Apple можно поместить в List<Apple>, но, как и предполагалось, также стало можно поместить Apple в List<Fruit>.

Неограниченные метасимволы

Казалось бы, неограниченный метасимвол <?> должен означать «все, что угодно», а его использование эквивалентно использованию низкоуровневого типа. В самом деле, на первый взгляд компилятор подтверждает эту оценку:

//: generics/UnboundedWildcards1.java
import java.util.*;
 
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list) {
list1 = list;
list2 = list;
// list3 = list;// Предупреждение: непроверенное преобразование
// Обнаружен List, требуется List<? extends Object>
}
static void assign2(List<?> list) {
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list) {
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
// assign3(new ArrayList());// Предупреждение-
// Непроверенное преобразование. Обнаружен- ArrayList
// Требуется: List<? extends Object>
assign1(new ArrayList<String>());
assign2(new ArrayList<String>());
assign3(new ArrayList<String>());
// Приемлемы обе формы- List<?>
List<?> wildList = new ArrayList();
wildList = new ArrayList<String>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}

Во многих ситуациях, подобных рассмотренной, для компилятора совершенно не существенно, используется низкоуровневый тип или <?>. Конструкцию <?> можно считать обычным украшением; впрочем, она обладает некоторой практи­ческой ценностью, потому что фактически означает: «Код написан с учетом параметризации Java, и здесь эта конструкция означает не то, что я использую низкоуровневый тип, а то, что параметр параметризации может содержать про­извольный тип».
Второй пример демонстрирует важное практическое использование неограниченных метасимволов. Когда вы имеете дело с несколькими параметрами, иногда важно указать, что один параметр может относиться к произвольному типу, а другой ограничить определенным типом:

//: generics/UnboundedWildcards2.java
import java.util.*;
 
public class UnboundedWildcards2 {
static Map map1;
static Map<?,?> map2;
static Map<String,?> map3;
static void assign1(Map map) { map1 = map; }
static void assign2(Map<?,?> map) { map2 = map; }
static void assign3(Map<String,?> map) { map3 = map; }
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
// assign3(new HashMap()); // Предупреждение:
// Непроверенное преобразование. Обнаружен: HashMap
// Требуется: Map<String,?>
assign1(new HashMap<String,Integer>());
assign2(new HashMap<String,Integer>());
assign3(new HashMap<String,Integer>());
}
}

Когда в записи используются только неограниченные метасимволы, как в примере Мар<?,?>, компилятор не отличает такой тип от Map. Кроме того, пример UnboundedWildcardsl.java показывает, что компилятор по-разному интерпретирует List<?> и List<? extends Object>.
Ситуация осложняется тем, что компилятор не всегда интересуется различиями между List и List<?> (например), поэтому может показаться, что это одно и то же. В самом деле, поскольку параметризованный аргумент стирается до первого ограничения, List<?> кажется эквивалентным List<Object>, a List, по сути, тоже является List<Object> — однако ни одно из этих утверждений не является в полной мере истинным. List в действительности означает «низкоуровневый List, содержащий любой тип Object», тогда как List<?> означает «не-низкоуровневый List, содержащий какой-то конкретный тип, хотя мы не знаем, какой именно».
Когда же компилятор различает низкоуровневые типы и типы с неограниченными метасимволами? В следующем примере используется класс Holder<T>, определение которого приводилось ранее. Класс содержит методы, получающие аргумент Holder, но в разных формах: в виде низкоуровневого типа, с конкретным параметром типа, с неограниченным метасимволом:

//: generics/Wildcards.java
// Exploring the meaning of wildcards.
// Исследование значения метасимволов
 
public class Wildcards {
// Низкоуровневый аргумент:
static void rawArgs(Holder holder, Object arg) {
// holder.set(arg); // Предупреждение
// Непроверенный вызов set(T) как члена
// низкоуровневого типа Holder
// holder.set(new Wildcards());// To же предупреждение
// Невозможно: нет информации о 'Т'
// T t = holder.get();
 
// Допустимо, но информация типа теряется
Object obj = holder.get();
}
// По аналогии с rawArgs(), но ошибки вместо предупреждений:
static void unboundedArg(Holder<?> holder, Object arg) {
// holder.set(arg); // Ошибка:
// set(capture of ?) в Holder<capture of ?>
// не может применяться к (Object)
// holder.set(new Wildcards()); // Та же ошибка
 
// Невозможно; нет информации о 'T':
// T t = holder.get();
 
// Допустимо, но информация типа теряется:
Object obj = holder.get();
}
static <T> T exact1(Holder<T> holder) {
T t = holder.get();
return t;
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.set(arg);
T t = holder.get();
return t;
}
static <T>
T wildSubtype(Holder<? extends T> holder, T arg) {
// holder.set(arg); // Ошибка:
// set(capture of ? extends T) in
// Holder<capture of ? extends T>
// cannot be applied to (T)
T t = holder.get();
return t;
}
static <T>
void wildSupertype(Holder<? super T> holder, T arg) {
holder.set(arg);
// T t = holder.get(); // Ошибка:
// Несовместимые типы: обнаружен Object, требуется T
 
// Допустимо, но информация типа теряется:
Object obj = holder.get();
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
// Или:
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
 
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
 
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
 
// Object r1 = exact1(raw);// Предупреждение
// Непроверенное преобразование Holder в Holder<T>
// Непроверенный вызов метода: exactl(Holder<T>)
// применяется к (Holder)
Long r2 = exact1(qualified);
Object r3 = exact1(unbounded); // Должен возвращать Object
Long r4 = exact1(bounded);
 
// Long r5 = exact2(raw, lng); // Предупреждения-
// Непроверенное преобразование Holder в Holder<Long>
// Непроверенный вызов метода. exact2(Holder<T>,T)
// применяется к (Holder,Long)
Long r6 = exact2(qualified, lng);
// Long r7 = exact2(unbounded, lng);// Ошибка:
// exact2(Holder<T>.T) не может применяться к
// (Holder<capture of ?>.Long)
// Long r8 = exact2(bounded, lng); // Ошибка:
// exact2(Holder<T>.T) не может применяться
// к (Holder<capture of ? extends Long>,Long)
// Long r9 = wildSubtype(raw, lng); // Предупреждения
// Непроверенное преобразование Holder
// к Holder<? extends Long>
// Непроверенный вызов метода-
// wildSubtype(Holder<? extends T>,T)
// применяется к (Holder.Long)
Long r10 = wildSubtype(qualified, lng);
// Допустимо, но возвращать может только Object-
Object r11 = wildSubtype(unbounded, lng);
Long r12 = wildSubtype(bounded, lng);
// wildSupertype(raw, lng);// Предупреждения.
// Непроверенное преобразование Holder
// к Holder<? super Long>
// Непроверенный вызов метода:
// wildSupertype(Holder<? super T>,T)
// применяется к (Holder.Long)
wildSupertype(qualified, lng);
// wildSupertype(unbounded, lng); // Ошибка:
// wi1dSupertype(Hoider<? super T>,T) не может
// применяться к (Holder<capture of ?>,Long)
// wildSupertype(bounded, lng); // Ошибка:
// wildSupertype(Holder<? super T>,T) не может
// применяться к (Holder<capture of ? extends Long>.Long)
}
}

В методе rawArgs() компилятор знает, что Holder является параметризованным типом, поэтому несмотря на то, что здесь он выражен как низкоуровневый тип, компилятору известно, что передача Object методу set() небезопасна. Так как в данном случае используется низкоуровневый тип, методу set() можно передать объект произвольного типа, и он будет преобразован в Object. Таким образом, при использовании низкоуровневого типа вы лишаетесь проверки на стадии компиляции. Вызов get() демонстрирует ту же проблему: никакого Т нет, поэтому результатом может быть только Object.
Может создаться впечатление, что низкоуровневый Holder и Holder<?> — приблизительно одно и то же. Однако метод unboundedArgs() демонстрирует различия между ними — в нем выявляются те же проблемы, но информация о них выдается в виде ошибок, а не предупреждений, поскольку низкоуровневый Holder может содержать разнородные комбинации типов, тогда как Holder<?> содержит однородную коллекцию одного конкретного типа.
В exact1() и exact2() используются точные параметры типов (то есть без метасимволов). Мы видим, что exact2() обладает иными ограничениями, нежели exact1(), из-за дополнительного аргумента.
В wildSubtype() ограничения на тип Holder опускаются до Holder с элементами любого типа, удовлетворяющими условию extends Т. И снова это означает, что Т может быть типом Fruit, a holder сможет вполне законно стать Holder <Apple>. Чтобы предотвратить возможное размещение Orange в Holder<Apple>, вызовы set() (и любых других методов, получающих в аргументах параметр типа) запрещены. Однако мы знаем, что все объекты, полученные из Holder<? extends Fruit>, по меньшей мере, являются Fruit, поэтому вызов get() (или любого метода с возвращаемым значением параметра типа) допустим.

Реализация параметризованных интерфейсов

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

//: generics/MultipleInterfaceVariants.java
// {CompileTimeError} (He компилируется)
 
interface Payable<T> {}
 
class Employee implements Payable<Employee> {}
class Hourly extends Employee
implements Payable<Hourly> {}

Класс Hourly компилироваться не будет, потому что стирание сокращает Payable<Employee> и Payable<Hourly> до Payable, а в приведенном примере это означало бы двукратную реализацию одного интерфейса. Интересная подробность: если удалить параметризованные аргументы из обоих упоминаний Payable, как это делает компилятор при стирании, программа откомпилируется.

Преобразования типов и предупреждения

Преобразование типа или instanceof с параметром типа не приводит ни к какому эффекту. В следующем контейнере данные хранятся во внутреннем представлении в форме Object и преобразуются к Т при выборке:

//: generics/GenericCast.java
 
class FixedSizeStack<T> {
private int index = 0;
private Object[] storage;
public FixedSizeStack(int size) {
storage = new Object[size];
}
public void push(T item) { storage[index++] = item; }
@SuppressWarnings("unchecked")
public T pop() { return (T)storage[--index]; }
}
 
public class GenericCast {
public static final int SIZE = 10;
public static void main(String[] args) {
FixedSizeStack<String> strings =
new FixedSizeStack<String>(SIZE);
for(String s : "A B C D E F G H I J".split(" "))
strings.push(s);
for(int i = 0; i < SIZE; i++) {
String s = strings.pop();
System.out.print(s + " ");
}
}
}

<spoiler text="Output:">

J I H G F E D C B A

</spoiler>
Без директивы @SuppressWarnings компилятор выдает для рор() предупреждение о «непроверенном преобразовании». Вследствие стирания он не знает, безопасно преобразование или нет, поэтому метод рор() никакого преобразования не выполняет. Т стирается до первого ограничения, которым по умолчанию является Object, так что рор() на самом деле преобразует Object в Object.

Перегрузка

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

//: generics/UseList.java
// {CompileTimeError} (He компилируется)
import java.util.*;
 
public class UseList<W,T> {
void f(List<T> v) {}
void f(List<W> v) {}
}

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

//: generics/UseList2.java
import java.util.*;
 
public class UseList2<W,T> {
void f1(List<T> v) {}
void f2(List<W> v) {}
}

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

Резюме

Мне довелось работать с шаблонами C++ с момента их появления. Скорее всего, приведенный далее аргумент я выдвигал в спорах чаще, чем большинство моих единомышленников. Лишь недавно я задумался над тем, насколько в действительности справедлив этот аргумент, — сколько раз проблема, которую я сейчас опишу, проникала в рабочий код?

Аргумент такой: одним из самых логичных мест для использования механизма параметризации являются контейнерные классы: List, Set, Map и т. д. До выхода Java SE5 объект, помещаемый в контейнер, преобразовывался в Object, и информация типа терялась. Если же вы хотели снова извлечь объект из контейнера, его приходилось преобразовывать к нужному типу. Я пояснял происходящее на примере List с элементами Cat (разновидность этого примера с Apple и Orange приведена в начале главы 11). Без параметризованной версии контейнера из Java SE5 вы помещаете и извлекаете из контейнера Object, поэтому в List с элементами Cat легко поместить объект Dog.

Однако версии Java, существовавшие до появления параметризации, не допускали злоупотреблений объектами, помещаемыми в контейнер. Если вы помещали Dog в контейнер Cat, а затем пытались интерпретировать все элементы контейнера как Cat, то при извлечении ссылки на Dog и ее преобразовании к Cat происходило преобразование RuntimeException. Проблема обнаруживалась, пусть и на стадии выполнения, а не во время компиляции.

В предыдущих изданиях книги я писал:
«Это не просто мелкая неприятность, а потенциальный источник трудноуловимых ошибок. Если одна часть (или несколько частей) программы вставляет объекты в контейнер, а в другой части программы обнаруживается, что в контейнер был помещен недопустимый объект, вам придется искать, где именно была выполнена неверная операция вставки».

Но позже я задумался над этим аргументом, и у меня появились сомнения. Во-первых, насколько часто это происходит? Не помню, чтобы такая ошибка встретилась в моей программе. Когда я спрашивал людей на конференциях, мне тоже не удалось найти никого, с кем бы это случилось. В другой книге использовался пример списка с именем files, содержащего объекты String, — в этом примере казалось абсолютно логичным добавить в список объект типа File, так что объекту, вероятно, стоило присвоить имя fileNames. Какую бы проверку типов ни обеспечивал язык Java, программист все равно может написать малопонятную программу — а плохо написанная программа, даже если она компилируется, все равно остается плохо написанной. Вероятно, нормальный разработчик присвоит контейнеру понятное имя вроде cats, которое послужит предупреждением для программиста, пытающегося занести в контейнер другой объект, отличный от Cat. Но, даже если это и произойдет, как долго такая ошибка останется скрытой? Здравый смысл подсказывает, что исключение произойдет вскоре после начала тестирования с реальными данными.

Один автор даже предположил, что такая ошибка может «оставаться скрытой несколько лет». Но я что-то не помню потока сообщений от людей, у которых возникали проблемы с поиском ошибок «Dog в списке Cat», или хотя бы с их частым появлением. Так неужели такая заметная и довольно сложная возможность, как параметризация, была включена в Java из-за проблем такого рода?
Я считаю, что побудительной причиной для включения параметризации в язык (не обязательно конкретной реализации ее в Java!) является выразительность, а не создание типизованных контейнеров. Типизованные контейнеры — всего лишь побочный эффект возможности создания универсального кода. Таким образом, хотя аргумент «Dog в списке Cat» часто используется для оправдания параметризации, этот аргумент спорен.

Из-за того, что параметризация была «встроена» в Java (а не проектировалась как составная часть языка с самого начала), некоторые контейнеры получились не такими мощными, как хотелось бы. Для примера взгляните на Map, особенно на методы containsKey(Object key) и get(Object key). Если бы эти классы проектировались в расчете на параметризацию, в этих методах вместо Object использовались бы параметризованные типы; тем самым обеспечивались бы необходимые проверки стадии компиляции. Скажем, в аналогичных контейнерах C++ тип ключа всегда проверяется во время компиляции.

Бесспорно, введение любого механизма параметризации в более позднюю версию языка, получившего широкое распространение, — крайне хлопотная затея. В C++ шаблоны были включены в исходную ISO-версию языка, так что они фактически всегда являлись его составной частью. В Java параметризация была введена лишь спустя 10 лет после выхода первой версии. Этот факт породил немало проблем с миграцией кода, а также оказал значительное влияние на архитектуру. В результате программисты страдают из-за близорукости, проявленной проектировщиками языка при создании версии 1.0. Конечно, при создании исходной версии они знали о шаблонах C++ и даже рассматривали возможность включения их в язык, но по тем или иным причинам решили этого не делать (скорее всего, просто торопились). В результате пострадал как язык, так и работающие на нем программисты. Только время покажет, как подход к параметризации в Java отразится на самом языке.

]]>
Книги по Java https://linexp.ru?id=4739 Wed, 29 Jun 2022 14:12:26 GMT
<![CDATA[Глава 15 Thinking in Java 4th edition]]> МАССИВЫВ конце главы 5 было показано, как определить и инициализировать массив. Программист создает и инициализирует массивы, извлекает из них элементы по целочисленным индексам, а размер массива остается неизменным. Как правило, при работе с массивами этого вполне достаточно, но иногда приходится выполнять более сложные операции, а также оценивать эффективность массива по сравнению с другими контейнерами. В этой главе массивы рассматри­ваются на более глубоком уровне.

МАССИВЫ

В конце главы 5 было показано, как определить и инициализировать массив. Программист создает и инициализирует массивы, извлекает из них элементы по целочисленным индексам, а размер массива остается неизменным. Как правило, при работе с массивами этого вполне достаточно, но иногда приходится выполнять более сложные операции, а также оценивать эффективность массива по сравнению с другими контейнерами. В этой главе массивы рассматри­ваются на более глубоком уровне.

Особенности массивов

В Java существует немало разных способов хранения объектов, почему же массивы занимают особое место?

Массивы отличаются от других контейнеров по трем показателям: эффективность, типизация и возможность хранения примитивов. Массивы Java являются самым эффективным средством хранения и произвольного доступа к последовательности ссылок на объекты. Массив представляет собой простую линейную последовательность, благодаря чему обращения к элементам осуществляются чрезвычайно быстро. За скорость приходится расплачиваться тем, что размер объекта массива фиксируется и не может изменяться на протяжении его жизненного цикла. Как говорилось в главе 11, контейнер ArrayList способен автоматически выделять, дополнительное пространство, выделяя новый блок памяти и перемещая в него все ссылки из старого. Хотя обычно ArrayList отдается предпочтение перед массивами, за гибкость приходится расплачиваться, и ArrayList значительно уступает по эффективности обычному массиву.

И массивы, и контейнеры защищены от возможных злоупотреблений. При выходе за границу массива или контейнера происходит исключение RuntimeException, свидетельствующее об ошибке программиста.

До появления параметризации другим контейнерным классам приходилось работать с объектами так, как если бы они не имели определенного типа. Иначе говоря, объекты рассматривались как принадлежащие к типу Object, корневому для всей иерархии классов в Java. Массивы удобнее «старых» контейнеров тем, что массив создается для хранения конкретного типа. Проверка типов на стадии компиляции не позволит использовать неверный тип или неверно интерпретировать извлекаемый тип. Конечно, Java так или иначе запретит отправить неподходящее сообщение объекту на стадии компиляции или выполнения, так что ни один из способов не является более рискованным по сравнению с другим. Просто будет удобнее, если компилятор укажет на существующую проблему, и снижается вероятность того, что пользователь программы получит неожиданное исключение.

Массив может содержать примитивные типы, а «старые» контейнеры — нет. С другой стороны, параметризованные контейнеры могут проверять тип хранимых объектов, а благодаря автоматической упаковке они работают так, как если бы поддерживали хранение примитивов, поскольку преобразование выполняется автоматически. В следующем примере массивы сравниваются с параметризованными контейнерами:

//: arrays/ContainerComparison.java
import java.util.*;
import static net.mindview.util.Print.*;
 
class BerylliumSphere {
private static long counter;
private final long id = counter++;
public String toString() { return "Sphere " + id; }
}
 
public class ContainerComparison {
public static void main(String[] args) {
BerylliumSphere[] spheres = new BerylliumSphere[10];
for(int i = 0; i < 5; i++)
spheres[i] = new BerylliumSphere();
print(Arrays.toString(spheres));
print(spheres[4]);
 
List<BerylliumSphere> sphereList =
new ArrayList<BerylliumSphere>();
for(int i = 0; i < 5; i++)
sphereList.add(new BerylliumSphere());
print(sphereList);
print(sphereList.get(4));
 
int[] integers = { 0, 1, 2, 3, 4, 5 };
print(Arrays.toString(integers));
print(integers[4]);
 
List<Integer> intList = new ArrayList<Integer>(
Arrays.asList(0, 1, 2, 3, 4, 5));
intList.add(97);
print(intList);
print(intList.get(4));
}
}

<spoiler text="Output:">

[Sphere 0, Sphere 1, Sphere 2, Sphere 3, Sphere 4, null, null, null, null, null]
Sphere 4
[Sphere 5, Sphere 6, Sphere 7, Sphere 8, Sphere 9]
Sphere 9
[0, 1, 2, 3, 4, 5]
4
[0, 1, 2, 3, 4, 5, 97]

</spoiler>
Оба способа хранения объектов обеспечивают проверку типов, а единственное очевидное различие заключается в том, что массивы используют для обращения к элементам конструкцию [ ], a List — методы вроде add() или get(). Разра­ботчики языка намеренно сделали массивы и ArrayList настолько похожими, чтобы программисту было концептуально проще переключаться между ними. Но, как было показано в главе 11, контейнеры обладают более широкими воз­можностями, чем массивы.
С появлением механизма автоматической упаковки контейнеры по удобству работы с примитивами почти не уступают массивам. Единственным реальным преимуществом массивов остается их эффективность. Впрочем, при решении более общих проблем может оказаться, что возможности массивов слишком ограничены, и тогда приходится пользоваться контейнерными классами.

Массив как объект

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

//: arrays/ArrayOptions.java
// Инициализация и повторное присваивание массивов
import java.util.*;
import static net.mindview.util.Print.*;
 
public class ArrayOptions {
public static void main(String[] args) {
// Массивы объектов:
BerylliumSphere[] a; // Локальная неинициализированная переменная
BerylliumSphere[] b = new BerylliumSphere[5];
// Ссылки в массиве автоматически инициализируются null:
print("b: " + Arrays.toString(b));
BerylliumSphere[] c = new BerylliumSphere[4];
for(int i = 0; i < c.length; i++)
if(c[i] == null) // Проверка ссылки на действительность
c[i] = new BerylliumSphere();
// Агрегатная инициализация:
BerylliumSphere[] d = { new BerylliumSphere(),
new BerylliumSphere(), new BerylliumSphere()
};
// Динамическая агрегатная инициализация:
a = new BerylliumSphere[]{
new BerylliumSphere(), new BerylliumSphere(),
};
// (Завершающая запятая не обязательна в обоих случаях)
print("a.length = " + a.length);
print("b.length = " + b.length);
print("c.length = " + c.length);
print("d.length = " + d.length);
a = d;
print("a.length = " + a.length);
 
// Массивы примитивов:
int[] e; // Ссылка null
int[] f = new int[5];
// Примитивы в массиве автоматически инициализируются нулями:
print("f: " + Arrays.toString(f));
int[] g = new int[4];
for(int i = 0; i < g.length; i++)
g[i] = i*i;
int[] h = { 11, 47, 93 };
// Ошибка компиляции переменная e не инициализирована:
//!print("e.length = " + e.length);
print("f.length = " + f.length);
print("g.length = " + g.length);
print("h.length = " + h.length);
e = h;
print("e.length = " + e.length);
e = new int[]{ 1, 2 };
print("e.length = " + e.length);
}
}

<spoiler text="Output:">

b: [null, null, null, null, null]
a.length = 2
b.length = 5
c.length = 4
d.length = 3
a.length = 3
f: [0, 0, 0, 0, 0]
f.length = 5
g.length = 4
h.length = 3
e.length = 3
e.length = 2

</spoiler>
Массив а — неинициализированная локальная переменная, и компилятор не позволяет что-либо делать с этой ссылкой до тех пор, пока она не будет соответствующим образом инициализирована. Массив b инициализируется массивом ссылок на объекты BerylliumSpere, хотя ни один такой объект в массив не заносится. Несмотря на это, мы можем запросить размер массива, потому что b указывает на действительный объект. В этом проявляется некоторый недостаток массивов: поле length сообщает, сколько элементов может быть помещено в массив, то есть размер объекта массива, а не количество хранящихся в нем элементов. Тем не менее при создании объекта массива все ссылки автоматически инициализируются значением null, и, чтобы узнать, связан ли некоторый элемент массива с объектом, достаточно проверить ссылку на равенство null. Аналогично, массивы примитивных типов автоматически инициализируются нулями для числовых типов: (char)0 для char и false для boolean.
Массив с демонстрирует создание массива с последующим присваиванием объектов BerylliumSphere всем элементам массива. Массив d демонстрирует синтаксис «агрегатной инициализации», при котором объект массива создается (с ключевым словом new, как массив с) и инициализируется объектами BerylliumSphere, причем все это происходит в одной команде.
Следующую конструкцию инициализации массива можно назвать «динамической агрегатной инициализацией». Агрегатная инициализация, используемая d, должна использоваться в точке определения d, но при втором синтаксисе объект массива может создаваться и использоваться в любой точке. Предположим, методу hide() передается массив объектов BerylliumSphere. Его вызов может выглядеть так:

 hide(d);

однако массив, передаваемый в аргументе, также можно создать динамически:

 hide(new BerylliumSphere[]{ new BeryllіumSphere(), new BerylliumSphere() });

Во многих ситуациях такой синтаксис оказывается более удобным.

Выражение

 a=d;

показывает, как взять ссылку, связанную с одним объектом массива, и присвоить ее другому объекту массива, как это делается с любым другим типом ссылки на объект. В результате a и d указывают на один объект массива в куче. Вторая часть ArrayOptions.java показывает, что примитивные массивы работают точно так же, как массивы объектов, за исключением того, что примитивные значения сохраняются в них напрямую.

Возврат массива

Предположим, вы пишете метод, который должен возвращать не отдельное значение, а целый набор значений. В таких языках, как C и C++, это сделать нелегко, потому что возвращается из метода не массив, а только указатель на массив.
При этом возникают проблемы, поскольку сложности с управлением жизненным циклом массива могут привести к утечке памяти.
В Java вы просто возвращаете массив. Вам не нужно беспокоиться о нем — массив будет существовать до тех пор, пока он вам нужен, а когда надобность в нем отпадет, массив будет уничтожен уборщиком мусора. В качестве примера рассмотрим возвращение массива String:

//: arrays/IceCream.java
// Возвращение массивов из методов
import java.util.*;
 
public class IceCream {
private static Random rand = new Random(47);
static final String[] FLAVORS = {
"Chocolate", "Strawberry", "Vanilla Fudge Swirl",
"Mint Chip", "Mocha Almond Fudge", "Rum Raisin",
"Praline Cream", "Mud Pie"
};
public static String[] flavorSet(int n) {
if(n > FLAVORS.length)
throw new IllegalArgumentException("Set too big");
String[] results = new String[n];
boolean[] picked = new boolean[FLAVORS.length];
for(int i = 0; i < n; i++) {
int t;
do
t = rand.nextInt(FLAVORS.length);
while(picked[t]);
results[i] = FLAVORS[t];
picked[t] = true;
}
return results;
}
public static void main(String[] args) {
for(int i = 0; i < 7; i++)
System.out.println(Arrays.toString(flavorSet(3)));
}
}

<spoiler text="Output:">

[Rum Raisin, Mint Chip, Mocha Almond Fudge]
[Chocolate, Strawberry, Mocha Almond Fudge]
[Strawberry, Mint Chip, Mocha Almond Fudge]
[Rum Raisin, Vanilla Fudge Swirl, Mud Pie]
[Vanilla Fudge Swirl, Chocolate, Mocha Almond Fudge]
[Praline Cream, Strawberry, Mocha Almond Fudge]
[Mocha Almond Fudge, Strawberry, Mint Chip]

</spoiler>
Метод flavorSet() создает массив results с элементами String. Размер массива равен n; он определяется аргументом, передаваемым при вызове метода. Далее метод случайным образом выбирает элементы из массива FLAVORS и помещает их в массив results, возвращаемый методом. Массив возвращается точно так же, как любой другой объект, — по ссылке. При этом не важно, был ли массив создан методом flavorSet(), или он был создан в другом месте. Массив останется с вами все время, пока он будет нужен, а потом уборщик мусора позаботится о его уничтожении.
Из выходных данных видно, что метод flavorSet() действительно выбирает случайное подмножество элементов при каждом вызове.

Многомерные массивы

Создание многомерных массивов в Java не вызывает особых сложностей. Для многомерных массивов примитивных типов каждый вектор заключается в фигурные скобки:

//: arrays/MultidimensionalPrimitiveArray.java
// Creating multidimensional arrays.
import java.util.*;
 
public class MultidimensionalPrimitiveArray {
public static void main(String[] args) {
int[][] a = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
System.out.println(Arrays.deepToString(a));
}
}

<spoiler text="Output:">
[[1, 2, 3], [4, 5, 6]]
</spoiler>
Каждая вложенная пара фигурных скобок описывает новую размерность массива.
В этом примере используется метод Java SE5 Arrays.deepToString(). Как видно из выходных данных, он преобразует многомерные массивы в String.
Массив также может создаваться ключевым словом new. Пример создания трехмерного массива выражением new:

//: arrays/ThreeDWithNew.java
import java.util.*;
 
public class ThreeDWithNew {
public static void main(String[] args) {
// 3-D array with fixed length:
int[][][] a = new int[2][2][4];
System.out.println(Arrays.deepToString(a));
}
}

<spoiler text="Output:">

[[[0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]]

</spoiler>
Как видите, если массиву примитивных типов не заданы явные значения, он автоматически инициализируется значениями по умолчанию. Массивы объектов инициализируются ссылками null.
Векторы массивов, образующих матрицу, могут иметь разную длину (это называется ступенчатым массивом):

//: arrays/RaggedArray.java
import java.util.*;
 
public class RaggedArray {
public static void main(String[] args) {
Random rand = new Random(47);
// Трехмерный массив с векторами переменной длины:
int[][][] a = new int[rand.nextInt(7)][][];
for(int i = 0; i < a.length; i++) {
a[i] = new int[rand.nextInt(5)][];
for(int j = 0; j < a[i].length; j++)
a[i][j] = new int[rand.nextInt(5)];
}
System.out.println(Arrays.deepToString(a));
}
}

<spoiler text="Output:">

[[], [[0], [0], [0, 0, 0, 0]], [[], [0, 0], [0, 0]], 
[[0, 0, 0], [0], [0, 0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0], []], [[0], [], [0]]]

</spoiler>
Первая конструкция new создает массив, у которого первый элемент имеет случайную длину, а остальные остаются неопределенными. Вторая конструкция new в цикле for заполняет элементы, но оставляет третий индекс неопределенным вплоть до выполнения третьего new.
Массивы с не-примитивными элементами заполняются аналогичным образом. Пример объединения нескольких выражений new в фигурных скобках:

//: arrays/MultidimensionalObjectArrays.java
import java.util.*;
 
public class MultidimensionalObjectArrays {
public static void main(String[] args) {
BerylliumSphere[][] spheres = {
{ new BerylliumSphere(), new BerylliumSphere() },
{ new BerylliumSphere(), new BerylliumSphere(),
new BerylliumSphere(), new BerylliumSphere() },
{ new BerylliumSphere(), new BerylliumSphere(),
new BerylliumSphere(), new BerylliumSphere(),
new BerylliumSphere(), new BerylliumSphere(),
new BerylliumSphere(), new BerylliumSphere() },
};
System.out.println(Arrays.deepToString(spheres));
}
}

<spoiler text="Output:">

[[Sphere 0, Sphere 1], 
[Sphere 2, Sphere 3, Sphere 4, Sphere 5],
[Sphere 6, Sphere 7, Sphere 8, Sphere 9, Sphere 10, Sphere 11, Sphere 12, Sphere 13]]

</spoiler>
Массив spheres также является ступенчатым, то есть длины вложенных списков объектов различаются.
Механизм автоматической упаковки работает с инициализаторами массивов:

//: arrays/AutoboxingArrays.java
import java.util.*;
 
public class AutoboxingArrays {
public static void main(String[] args) {
Integer[][] a = { // Автоматическая упаковка:
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 },
{ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
{ 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 },
{ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 },
};
System.out.println(Arrays.deepToString(a));
}
}

<spoiler text="Output:">

[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [21, 22, 23, 24, 25, 26, 27, 28, 29, 30], 
[51, 52, 53, 54, 55, 56, 57, 58, 59, 60], [71, 72, 73, 74, 75, 76, 77, 78, 79, 80]]

</spoiler>
А вот как происходит поэлементное построение массива не-примитивных объектов:

//: arrays/AssemblingMultidimensionalArrays.java
// Создание многомерных массивов
import java.util.*;
 
public class AssemblingMultidimensionalArrays {
public static void main(String[] args) {
Integer[][] a;
a = new Integer[3][];
for(int i = 0; i < a.length; i++) {
a[i] = new Integer[3];
for(int j = 0; j < a[i].length; j++)
a[i][j] = i * j; // Автоматическая упаковка
}
System.out.println(Arrays.deepToString(a));
}
}

<spoiler text="Output:">

[[0, 0, 0], [0, 1, 2], [0, 2, 4]]

</spoiler>
Выражение i*j присутствует только для того, чтобы поместить менее тривиальное значение в Integer.
Метод Arrays.deepToString() работает как с массивами примитивных типов, так и с массивами объектов:

//: arrays/MultiDimWrapperArray.java
// Multidimensional arrays of "wrapper" objects.
import java.util.*;
 
public class MultiDimWrapperArray {
public static void main(String[] args) {
Integer[][] a1 = { // Автоматическая упаковка
{ 1, 2, 3, },
{ 4, 5, 6, },
};
Double[][][] a2 = { // Автоматическая упаковка
{ { 1.1, 2.2 }, { 3.3, 4.4 } },
{ { 5.5, 6.6 }, { 7.7, 8.8 } },
{ { 9.9, 1.2 }, { 2.3, 3.4 } },
};
String[][] a3 = {
{ "The", "Quick", "Sly", "Fox" },
{ "Jumped", "Over" },
{ "The", "Lazy", "Brown", "Dog", "and", "friend" },
};
System.out.println("a1: " + Arrays.deepToString(a1));
System.out.println("a2: " + Arrays.deepToString(a2));
System.out.println("a3: " + Arrays.deepToString(a3));
}
}

<spoiler text="Output:">

a1: [[1, 2, 3], [4, 5, 6]]
a2: [[[1.1, 2.2], [3.3, 4.4]], [[5.5, 6.6], [7.7, 8.8]], [[9.9, 1.2], [2.3, 3.4]]]
a3: [[The, Quick, Sly, Fox], [Jumped, Over], [The, Lazy, Brown, Dog, and, friend]]

</spoiler>
И снова в массивах Integer и Double механизм автоматической упаковки Java SE5 создает необходимые объекты-«обертки».

Массивы и параметризация

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

 Peel<Banana>[] peels = new Peel<Banana>[10]: // He разрешено

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

//: arrays/ParameterizedArrayType.java
class ClassParameter<T> {
public T[] f(T[] arg) { return arg; }
}
 
class MethodParameter {
public static <T> T[] f(T[] arg) { return arg; }
}
 
public class ParameterizedArrayType {
public static void main(String[] args) {
Integer[] ints = { 1, 2, 3, 4, 5 };
Double[] doubles = { 1.1, 2.2, 3.3, 4.4, 5.5 };
Integer[] ints2 =
new ClassParameter<Integer>().f(ints);
Double[] doubles2 =
new ClassParameter<Double>().f(doubles);
ints2 = MethodParameter.f(ints);
doubles2 = MethodParameter.f(doubles);
}
}

Обратите внимание, как удобно использовать параметризованный метод вместо параметризованного класса: вам не придется создавать очередную «версию» класса с параметром для каждого типа, к которому он применяется, и его можно сделать static. Конечно, параметризованный класс не всегда можно заменить параметризованным методом, но такое решение может оказаться предпочтительным.

Как выясняется, не совсем правильно говорить, что вы не можете создавать массивы параметризованных типов. Действительно, компилятор не позволит создать экземпляр массива параметризованного типа, но вы можете создать ссылку на такой массив. Пример:

 List<String>[] Is;

Такая конструкция проходит проверку без малейших возражений со стороны компилятора. И хотя вы не можете создать объект массива с параметризацией, можно создать объект непараметризованного типа и преобразовать его:

//: arrays/ArrayOfGenerics.java
// Возможность создания массивов параметризованных типов.
import java.util.*;
 
public class ArrayOfGenerics {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
List<String>[] ls;
List[] la = new List[10];
ls = (List<String>[])la; //Предупреждение о
// непроверенном преобразовании
ls[0] = new ArrayList<String>();
// Приводит к ошибке на стадии компиляции :
//! ls[1] = new ArrayList<Integer>();
 
// Проблема: List<String> является подтипом Object
Object[] objects = ls; // Поэтому присваивание возможно
// Компилируется и выполняется без ошибок и предупреждений :
objects[1] = new ArrayList<Integer>();
 
// Но если ваши потребности достаточно элементарны.
// создать массив параметризованных типов можно, хотя
// и с предупреждением о "непроверенном" преобразовании
List<BerylliumSphere>[] spheres =
(List<BerylliumSphere>[])new List[10];
for(int i = 0; i < spheres.length; i++)
spheres[i] = new ArrayList<BerylliumSphere>();
}
}

Мы видим, что при при получении ссылки на List<String>[] выполняется некоторая проверка на стадии компиляции. Проблема в том, что массивы ковариантны, поэтому List<String>[] также является Object[], поэтому вашему массиву можно присвоить ArrayList<Integer> без выдачи ошибок на стадии компиляции или выполнения.
Если вы уверены в том, что восходящее преобразование выполняться не будет, а ваши потребности относительно просты, можно создать массив параметризованных типов, обеспечивающий простейшую проверку типов на стадии компиляции. Тем не менее параметризованный контейнер практически всегда оказывается более удачным решением, чем массив параметризованных типов.

Создание тестовых данных

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

 Arrays.fill();

Класс Arrays из стандартной библиотеки Java содержит весьма тривиальный метод fill(): он всего лишь дублирует одно значение в каждом элементе массива, а в случае объектов копирует одну ссылку в каждый элемент. Пример:

//: arrays/FillingArrays.java
// Использование Arrays.fill()
import java.util.*;
import static net.mindview.util.Print.*;
 
public class FillingArrays {
public static void main(String[] args) {
int size = 6;
boolean[] a1 = new boolean[size];
byte[] a2 = new byte[size];
char[] a3 = new char[size];
short[] a4 = new short[size];
int[] a5 = new int[size];
long[] a6 = new long[size];
float[] a7 = new float[size];
double[] a8 = new double[size];
String[] a9 = new String[size];
Arrays.fill(a1, true);
print("a1 = " + Arrays.toString(a1));
Arrays.fill(a2, (byte)11);
print("a2 = " + Arrays.toString(a2));
Arrays.fill(a3, 'x');
print("a3 = " + Arrays.toString(a3));
Arrays.fill(a4, (short)17);
print("a4 = " + Arrays.toString(a4));
Arrays.fill(a5, 19);
print("a5 = " + Arrays.toString(a5));
Arrays.fill(a6, 23);
print("a6 = " + Arrays.toString(a6));
Arrays.fill(a7, 29);
print("a7 = " + Arrays.toString(a7));
Arrays.fill(a8, 47);
print("a8 = " + Arrays.toString(a8));
Arrays.fill(a9, "Hello");
print("a9 = " + Arrays.toString(a9));
// Интервальные операции:
Arrays.fill(a9, 3, 5, "World");
print("a9 = " + Arrays.toString(a9));
}
}

<spoiler text="Output:">

a1 = [true, true, true, true, true, true]
a2 = [11, 11, 11, 11, 11, 11]
a3 = [x, x, x, x, x, x]
a4 = [17, 17, 17, 17, 17, 17]
a5 = [19, 19, 19, 19, 19, 19]
a6 = [23, 23, 23, 23, 23, 23]
a7 = [29.0, 29.0, 29.0, 29.0, 29.0, 29.0]
a8 = [47.0, 47.0, 47.0, 47.0, 47.0, 47.0]
a9 = [Hello, Hello, Hello, Hello, Hello, Hello]
a9 = [Hello, Hello, Hello, World, World, Hello]

</spoiler>
Метод заполняет либо весь массив, либо, как показывают две последние команды, диапазон его элементов. Но, поскольку вызывать Arrays.fill() можно только для одного значения данных, полученные результаты не слишком полезны.

Генераторы данных

Чтобы создавать менее тривиальные массивы данных с более гибкими возможностями, мы воспользуемся концепцией генераторов, представленной в главе 14. Генератор способен выдавать любые данные по вашему выбору (напомню, что он является примером паттерна «стратегия» — разные генераторы представляют разные стратегии). В этом разделе будут представлены некоторые готовые генераторы, но вы также сможете легко определить собственный генератор для своих потребностей. Для начала рассмотрим простейший набор счетных генераторов для всех примитивных типов и String. Классы генераторов вложены в класс CountingGenerator, чтобы они могли обозначаться именами генерируемых объектов. Например, генератор, создающий объекты Integer, будет создаваться выражением new CountingGenerator.Integer():

//: net/mindview/util/CountingGenerator.java
// Simple generator implementations.
package net.mindview.util;
 
public class CountingGenerator {
public static class
Boolean implements Generator<java.lang.Boolean> {
private boolean value = false;
public java.lang.Boolean next() {
value = !value; // Поочередное переключение
return value;
}
}
public static class
Byte implements Generator<java.lang.Byte> {
private byte value = 0;
public java.lang.Byte next() { return value++; }
}
static char[] chars = ("abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
public static class
Character implements Generator<java.lang.Character> {
int index = -1;
public java.lang.Character next() {
index = (index + 1) % chars.length;
return chars[index];
}
}
public static class
String implements Generator<java.lang.String> {
private int length = 7;
Generator<java.lang.Character> cg = new Character();
public String() {}
public String(int length) { this.length = length; }
public java.lang.String next() {
char[] buf = new char[length];
for(int i = 0; i < length; i++)
buf[i] = cg.next();
return new java.lang.String(buf);
}
}
public static class
Short implements Generator<java.lang.Short> {
private short value = 0;
public java.lang.Short next() { return value++; }
}
public static class
Integer implements Generator<java.lang.Integer> {
private int value = 0;
public java.lang.Integer next() { return value++; }
}
public static class
Long implements Generator<java.lang.Long> {
private long value = 0;
public java.lang.Long next() { return value++; }
}
public static class
Float implements Generator<java.lang.Float> {
private float value = 0;
public java.lang.Float next() {
float result = value;
value += 1.0;
return result;
}
}
public static class
Double implements Generator<java.lang.Double> {
private double value = 0.0;
public java.lang.Double next() {
double result = value;
value += 1.0;
return result;
}
}
}

Каждый класс реализует некоторое понятие «счетности». В случае CountingGenerator.Character это повторение символов верхнего и нижнего регистра. Класс CountingGenerator.String использует CountingGenerator.Character для заполнения массива символов, который затем преобразуется в String. Размер массива определяется аргументом конструктора. Обратите внимание на то, что CountingGenerator.String использует базовую конструкцию Generator<java.lang.Character> вместо конкретной ссылки на CountingGenerator.Character.

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

//: arrays/GeneratorsTest.java
import net.mindview.util.*;
 
public class GeneratorsTest {
public static int size = 10;
public static void test(Class<?> surroundingClass) {
for(Class<?> type : surroundingClass.getClasses()) {
System.out.print(type.getSimpleName() + ": ");
try {
Generator<?> g = (Generator<lt;?>)type.newInstance();
for(int i = 0; i < size; i++)
System.out.printf(g.next() + " ");
System.out.println();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
test(CountingGenerator.class);
}
}

<spoiler text="Output:">

Double: 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0
Float: 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0
Long: 0 1 2 3 4 5 6 7 8 9
Integer: 0 1 2 3 4 5 6 7 8 9
Short: 0 1 2 3 4 5 6 7 8 9
String: abcdefg hijklmn opqrstu vwxyzAB CDEFGHI JKLMNOP QRSTUVW XYZabcd efghijk lmnopqr
Character: a b c d e f g h i j
Byte: 0 1 2 3 4 5 6 7 8 9
Boolean: true false true false true false true false true false

</spoiler>
Предполагается, что тестируемый класс содержит серию вложенных объектов Generator, каждый из которых имеет конструктор по умолчанию (то есть без аргументов). Рефлексионный метод getClasses() выдает информацию обо всех вложенных классах. Далее метод test() создает экземпляр каждого генератора и выводит результаты, полученные при десятикратном вызове next(). Следующий набор генераторов основан на случайных числах. Так как конструктор Random инициализируется константой, результаты будут повторяться при каждом запуске программы:

//: net/mindview/util/RandomGenerator.java
// Генераторы, выдающие случайные значения.
package net.mindview.util;
import java.util.*;
 
public class RandomGenerator {
private static Random r = new Random(47);
public static class
Boolean implements Generator<java.lang.Boolean> {
public java.lang.Boolean next() {
return r.nextBoolean();
}
}
public static class
Byte implements Generator<java.lang.Byte> {
public java.lang.Byte next() {
return (byte)r.nextInt();
}
}
public static class
Character implements Generator<java.lang.Character> {
public java.lang.Character next() {
return CountingGenerator.chars[
r.nextInt(CountingGenerator.chars.length)];
}
}
public static class
String extends CountingGenerator.String {
/ Подключение случайного генератора:
{ cg = new Character(); } // Инициализатор
public String() {}
public String(int length) { super(length); }
}
public static class
Short implements Generator<java.lang.Short> {
public java.lang.Short next() {
return (short)r.nextInt();
}
}
public static class
Integer implements Generator<java.lang.Integer> {
private int mod = 10000;
public Integer() {}
public Integer(int modulo) { mod = modulo; }
public java.lang.Integer next() {
return r.nextInt(mod);
}
}
public static class
Long implements Generator<java.lang.Long> {
private int mod = 10000;
public Long() {}
public Long(int modulo) { mod = modulo; }
public java.lang.Long next() {
return new java.lang.Long(r.nextInt(mod));
}
}
public static class
Float implements Generator<java.lang.Float> {
public java.lang.Float next() {
// Отсечение до двух разрядов в дробной части:
int trimmed = Math.round(r.nextFloat() * 100);
return ((float)trimmed) / 100;
}
}
public static class
Double implements Generator<java.lang.Double> {
public java.lang.Double next() {
long trimmed = Math.round(r.nextDouble() * 100);
return ((double)trimmed) / 100;
}
}
}

Как видите, RandomGenerator.String наследует от CountingGenerator.String, просто подключая новый генератор Character.
Чтобы генерируемые числа были не слишком велики, RandomGenerator.Integer по умолчанию берет остаток от деления на 10 000, но перегруженный конструктор позволяет выбрать меньшее значение. Аналогичный подход используется и для RandomGenerator.Long. Для генераторов Float и Double цифры в дробной части усекаются.
Для тестирования RandomGenerator можно воспользоваться уже готовым классом GeneratorsTest:

//: arrays/RandomGeneratorsTest.java
import net.mindview.util.*;
 
public class RandomGeneratorsTest {
public static void main(String[] args) {
GeneratorsTest.test(RandomGenerator.class);
}
}

<spoiler text="Output:">

Double: 0.73 0.53 0.16 0.19 0.52 0.27 0.26 0.05 0.8 0.76
Float: 0.53 0.16 0.53 0.4 0.49 0.25 0.8 0.11 0.02 0.8
Long: 7674 8804 8950 7826 4322 896 8033 2984 2344 5810
Integer: 8303 3141 7138 6012 9966 8689 7185 6992 5746 3976
Short: 3358 20592 284 26791 12834 -8092 13656 29324 -1423 5327
String: bkInaMe sbtWHkj UrUkZPg wsqPzDy CyRFJQA HxxHvHq XumcXZJ oogoYWM NvqeuTp nXsgqia
Character: x x E A J J m z M s
Byte: -60 -17 55 -14 -5 115 39 -37 79 115
Boolean: false true false false true true true true true true

</spoiler>
Чтобы изменить количество генерируемых значений, воспользуйтесь public-полем GeneratorsTest.size.

Создание массивов с использованием генераторов

Для создания массивов на основе Generator нам потребуются два вспомогательных класса. Первый использует произвольный Generator для получения массива типов, производных от Object. Для решения проблемы с примитивами второй класс получает произвольный массив с объектами-«обертками» и строит для него соответствующий массив примитивов.

public static class Double implements Generator<java.lang.Double> { 
public java.lang.Double next() {
long trimmed = Math.round(r.nextDouble() * 100);
return ((double)trimmed) / 100;
}
}

Первый вспомогательный класс может работать в двух режимах, представленных перегруженным статическим методом аrrау(). Первая версия метода получает существующий массив и заполняет его с использованием Generator; вторая версия получает объект Class.Generator и количество элементов и создает новый массив, который также заполняется с использованием Generator. Помните, что при этом создаются только массивы субтипов Object, но не массивы при­митивных типов:

//: net/mindview/util/Generated.java
package net.mindview.util;
import java.util.*;
 
public class Generated {
// Заполнение существующего массиваy:
public static <T> T[] array(T[] a, Generator<T> gen) {
return new CollectionData<T>(gen, a.length).toArray(a);
}
// Создание нового массива:
@SuppressWarnings("unchecked")
public static <T> T[] array(Class<T> type,
Generator<T> gen, int size) {
T[] a =
(T[])java.lang.reflect.Array.newInstance(type, size);
return new CollectionData<T>(gen, size).toArray(a);
}
}

Класс CollectionData создает объект Collection, заполненный элементами, которые были созданы генератором gen. Количество элементов определяется вторым аргументом конструктора. Все субтипы Collection содержат метод toArray(), заполняющий массив-аргумент элементами из Collection. Второй метод использует рефлексию для динамического создания нового массива соответствующего типа и размера. Затем созданный массив заполняется таким же способом, как в первом методе. Чтобы протестировать Generated, мы воспользуемся одним из классов CountingGenerator, описанных в предыдущем разделе:

//: arrays/TestGenerated.java
import java.util.*;
import net.mindview.util.*;
 
public class TestGenerated {
public static void main(String[] args) {
Integer[] a = { 9, 8, 7, 6 };
System.out.println(Arrays.toString(a));
a = Generated.array(a,new CountingGenerator.Integer());
System.out.println(Arrays.toString(a));
Integer[] b = Generated.array(Integer.class,
new CountingGenerator.Integer(), 15);
System.out.println(Arrays.toString(b));
}
}

<spoiler text="Output:">

[9, 8, 7, 6]
[0, 1, 2, 3]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

</spoiler>
Хотя массив а инициализируется, эти данные перезаписываются при вызове Generated.array(). Инициализация b показывает, как создать заполненный массив «с нуля».
Параметризация не работает с примитивами, поэтому для заполнения примитивных массивов будут использоваться генераторы. Для решения этой проблемы мы создадим преобразователь, который получает произвольный массив объектных «оберток» и преобразует его в массив соответствующих примитивных типов. Без него нам пришлось бы создавать специализированные генераторы для всех примитивов.

//: net/mindview/util/ConvertTo.java
package net.mindview.util;
 
public class ConvertTo {
public static boolean[] primitive(Boolean[] in) {
boolean[] result = new boolean[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i]; // Autounboxing
return result;
}
public static char[] primitive(Character[] in) {
char[] result = new char[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
public static byte[] primitive(Byte[] in) {
byte[] result = new byte[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
public static short[] primitive(Short[] in) {
short[] result = new short[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
public static int[] primitive(Integer[] in) {
int[] result = new int[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
public static long[] primitive(Long[] in) {
long[] result = new long[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
public static float[] primitive(Float[] in) {
float[] result = new float[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
public static double[] primitive(Double[] in) {
double[] result = new double[in.length];
for(int i = 0; i < in.length; i++)
result[i] = in[i];
return result;
}
}

Каждая версия primitive() создает примитивный массив правильной длины, а затем копирует элементы из массива in. Обратите внимание на выполнение автоматической распаковки в выражении

  result[i] = in[i];

Пример использования ConvertTo с обеими версиями Generated.array():

//: arrays/PrimitiveConversionDemonstration.java
import java.util.*;
import net.mindview.util.*;
 
public class PrimitiveConversionDemonstration {
public static void main(String[] args) {
Integer[] a = Generated.array(Integer.class,
new CountingGenerator.Integer(), 15);
int[] b = ConvertTo.primitive(a);
System.out.println(Arrays.toString(b));
boolean[] c = ConvertTo.primitive(
Generated.array(Boolean.class,
new CountingGenerator.Boolean(), 7));
System.out.println(Arrays.toString(c));
}
}

<spoiler text="Output:">

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
[true, false, true, false, true, false, true]

</spoiler>
Наконец, следующая программа тестирует инструментарий создания массивов с классами RandomGenerator:

//: arrays/TestArrayGeneration.java
// Test the tools that use generators to fill arrays.
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class TestArrayGeneration {
public static void main(String[] args) {
int size = 6;
boolean[] a1 = ConvertTo.primitive(Generated.array(
Boolean.class, new RandomGenerator.Boolean(), size));
print("a1 = " + Arrays.toString(a1));
byte[] a2 = ConvertTo.primitive(Generated.array(
Byte.class, new RandomGenerator.Byte(), size));
print("a2 = " + Arrays.toString(a2));
char[] a3 = ConvertTo.primitive(Generated.array(
Character.class,
new RandomGenerator.Character(), size));
print("a3 = " + Arrays.toString(a3));
short[] a4 = ConvertTo.primitive(Generated.array(
Short.class, new RandomGenerator.Short(), size));
print("a4 = " + Arrays.toString(a4));
int[] a5 = ConvertTo.primitive(Generated.array(
Integer.class, new RandomGenerator.Integer(), size));
print("a5 = " + Arrays.toString(a5));
long[] a6 = ConvertTo.primitive(Generated.array(
Long.class, new RandomGenerator.Long(), size));
print("a6 = " + Arrays.toString(a6));
float[] a7 = ConvertTo.primitive(Generated.array(
Float.class, new RandomGenerator.Float(), size));
print("a7 = " + Arrays.toString(a7));
double[] a8 = ConvertTo.primitive(Generated.array(
Double.class, new RandomGenerator.Double(), size));
print("a8 = " + Arrays.toString(a8));
}
}

<spoiler text="Output:">

a1 = [true, false, true, false, false, true]
a2 = [104, -79, -76, 126, 33, -64]
a3 = [Z, n, T, c, Q, r]
a4 = [-13408, 22612, 15401, 15161, -28466, -12603]
a5 = [7704, 7383, 7706, 575, 8410, 6342]
a6 = [7674, 8804, 8950, 7826, 4322, 896]
a7 = [0.01, 0.2, 0.4, 0.79, 0.27, 0.45]
a8 = [0.16, 0.87, 0.7, 0.66, 0.87, 0.59]

</spoiler>
Как видите, все версии ConvertTo.primitive() работают правильно.

Вспомогательный инструментарий Arrays

В библиотеку java.util включен класс Arrays, содержащий набор вспомогательных статических методов для работы с массивами. Основных методов шесть: equals() сравнивает два массива (также существует версия deepEquals() для многомерных массивов); fill() был описан ранее в этой главе; sort() сортирует массив; binarySearch( ищет элемент в отсортированном массиве; toString() создает представление массива в формате String, a hashCode() генерирует хеш-код массива. Все эти методы перегружены для всех примитивных типов и Object. Кроме того, метод Arrays.asList() преобразует любую последовательность или массив в контейнер List (см. главу 11).
Прежде чем обсуждать методы Arrays, следует рассмотреть еще один полезный метод, не входящий в Arrays.

Копирование массива

Стандартная библиотека Java содержит статический метод System.arraycopy(), который копирует массивы значительно быстрее, чем при ручном копировании в цикле for. Метод System.arraycopy() перегружен для работы со всеми типами. Пример для массивов int:

//: arrays/CopyingArrays.java
// Using System.arraycopy()
import java.util.*;
import static net.mindview.util.Print.*;
 
public class CopyingArrays {
public static void main(String[] args) {
int[] i = new int[7];
int[] j = new int[10];
Arrays.fill(i, 47);
Arrays.fill(j, 99);
print("i = " + Arrays.toString(i));
print("j = " + Arrays.toString(j));
System.arraycopy(i, 0, j, 0, i.length);
print("j = " + Arrays.toString(j));
int[] k = new int[5];
Arrays.fill(k, 103);
System.arraycopy(i, 0, k, 0, k.length);
print("k = " + Arrays.toString(k));
Arrays.fill(k, 103);
System.arraycopy(k, 0, i, 0, k.length);
print("i = " + Arrays.toString(i));
// Objects:
Integer[] u = new Integer[10];
Integer[] v = new Integer[5];
Arrays.fill(u, new Integer(47));
Arrays.fill(v, new Integer(99));
print("u = " + Arrays.toString(u));
print("v = " + Arrays.toString(v));
System.arraycopy(v, 0, u, u.length/2, v.length);
print("u = " + Arrays.toString(u));
}
}

<spoiler text="Output:">

i = [47, 47, 47, 47, 47, 47, 47]
j = [99, 99, 99, 99, 99, 99, 99, 99, 99, 99]
j = [47, 47, 47, 47, 47, 47, 47, 99, 99, 99]
k = [47, 47, 47, 47, 47]
i = [103, 103, 103, 103, 103, 47, 47]
u = [47, 47, 47, 47, 47, 47, 47, 47, 47, 47]
v = [99, 99, 99, 99, 99]
u = [47, 47, 47, 47, 47, 99, 99, 99, 99, 99]

</spoiler>
В аргументах arraycopy() передается исходный массив, начальная позиция копирования в исходном массиве, приемный массив, начальная позиция копирования в приемном массиве и количество копируемых элементов. Естественно, любое нарушение границ массива приведет к исключению.
Приведенный пример показывает, что копироваться могут как примитивные, так и объектные массивы. Однако при копировании объектных массивов копируются только ссылки, но не сами объекты. Такая процедура называется поверхностным копированием.System.arraycopy() не выполняет ни автоматической упаковки, ни автоматической распаковки — типы двух массивов должны полностью совпадать.

Сравнение массивов

Класс Arrays содержит метод equals() для проверки на равенство целых массивов. Метод перегружен для примитивов и Object. Чтобы два массива считались равными, они должны содержать одинаковое количество элементов, и каждый элемент должен быть эквивалентен соответствующему элементу другого массива (проверка осуществляется вызовом equals() для каждой пары; для примитивов используется метод equals() объектной «обертки» — например, Integer.equals() для int). Пример:

//: arrays/ComparingArrays.java
// Using Arrays.equals()
import java.util.*;
import static net.mindview.util.Print.*;
 
public class ComparingArrays {
public static void main(String[] args) {
int[] a1 = new int[10];
int[] a2 = new int[10];
Arrays.fill(a1, 47);
Arrays.fill(a2, 47);
print(Arrays.equals(a1, a2));
a2[3] = 11;
print(Arrays.equals(a1, a2));
String[] s1 = new String[4];
Arrays.fill(s1, "Hi");
String[] s2 = { new String("Hi"), new String("Hi"),
new String("Hi"), new String("Hi") };
print(Arrays.equals(s1, s2));
}
}

<spoiler text="Output:">

true
false
true

</spoiler>
Сначала массивы a1 и a2 полностью совпадают, поэтому результат сравнения равен true, но после изменения одного из элементов будет получен результат false. В последнем случае все элементы s1 указывают на один объект, тогда как s2 содержит пять разных объектов. Однако проверка равенства определяется содержимым (с вызовом Object.equals()), поэтому результат равен true.

Сравнение элементов массивов

Сравнения, выполняемые в ходе сортировки, зависят от фактического типа объектов. Конечно, можно написать разные методы сортировки для всех возможных типов, но такой код придется модифицировать при появлении новых типов.
Главной целью проектирования является «отделение того, что может измениться, от того, что остается неизменным». В данном случае неизменным остается общий алгоритм сортировки, а изменяется способ сравнения объектов. Вместо того, чтобы размещать код сравнения в разных функциях сортировки, мы воспользуемся паттерном проектирования «стратегия». В этом паттерне переменная часть кода инкапсулируется в отдельном классе. Объект стратегии передается коду, который остается неизменным, и последний использует стратегию для реализации своего алгоритма. При этом разные объекты выражают разные способы сравнения, но передаются универсальному коду сортировки.
В Java функциональность сравнения может выражаться двумя способами. Первый основан на «естественном» методе сравнения, который включается в класс при реализации java.lang.Comparable — очень простого интерфейса с единственным методом compareTo(). В аргументе метод получает другой объект того же типа. Он выдает отрицательное значение, если текущий объект меньше аргумента, нуль при равенстве и положительное значение, если текущий объект больше аргумента.

В следующем примере класс реализует Comparable, а для демонстрации совместимости используется метод стандартной библиотеки Java Arrays.sort():

//: arrays/CompType.java
// Реализация классом интерфейса Comparable.
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class CompType implements Comparable<CompType> {
int i;
int j;
private static int count = 1;
public CompType(int n1, int n2) {
i = n1;
j = n2;
}
public String toString() {
String result = "[i = " + i + ", j = " + j + "]";
if(count++ % 3 == 0)
result += "\n";
return result;
}
public int compareTo(CompType rv) {
return (i < rv.i ? -1 : (i == rv.i ? 0 : 1));
}
private static Random r = new Random(47);
public static Generator<CompType> generator() {
return new Generator<CompType>() {
public CompType next() {
return new CompType(r.nextInt(100),r.nextInt(100));
}
};
}
public static void main(String[] args) {
CompType[] a =
Generated.array(new CompType[12], generator());
print("before sorting:");
print(Arrays.toString(a));
Arrays.sort(a);
print("after sorting:");
print(Arrays.toString(a));
}
}

<spoiler text="Output:">

before sorting:
[[i = 58, j = 55], [i = 93, j = 61], [i = 61, j = 29]
, [i = 68, j = 0], [i = 22, j = 7], [i = 88, j = 28]
, [i = 51, j = 89], [i = 9, j = 78], [i = 98, j = 61]
, [i = 20, j = 58], [i = 16, j = 40], [i = 11, j = 22]
]
after sorting:
[[i = 9, j = 78], [i = 11, j = 22], [i = 16, j = 40]
, [i = 20, j = 58], [i = 22, j = 7], [i = 51, j = 89]
, [i = 58, j = 55], [i = 61, j = 29], [i = 68, j = 0]
, [i = 88, j = 28], [i = 93, j = 61], [i = 98, j = 61]
]

</spoiler>
Определяя метод сравнения, вы несете полную ответственность за принятие решения о его результатах. В приведенном примере в сравнении используются только значения і, а значения j игнорируются.
Метод generator() производит объект, реализующий интерфейс Generator, создавая анонимный внутренний класс. Объект строит объекты CompType, инициализируя их случайными значениями. В main() генератор заполняет массив CompType, который затем сортируется. Если интерфейс Comparable не реализован, то при попытке вызова sort() произойдет исключение ClassCastException. Это объясняется тем, что sort() преобразует свой аргумент к типу Comparable.
Теперь представьте, что вы получили класс, который не реализует интерфейс Comparable... а может быть, реализует, но вам не нравится, как он работает, и вы хотели бы задать для типа другой метод сравнения. Для решения проблемы создается отдельный класс, реализующий интерфейс Comparator. Он содержит два метода, compare() и equals(). Впрочем, вам практически никогда не придется реализовывать equals() — разве что при особых требованиях по быстродействию, потому что любой создаваемый класс неявно наследует от класса Object метод equals().

Класс Collections содержит метод reverseOrder(), который создает Comparator для порядка сортировки, обратного по отношению к естественному. Он может быть применен к CompType:

//: arrays/Reverse.java
// The Collections.reverseOrder() Comparator
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class Reverse {
public static void main(String[] args) {
CompType[] a = Generated.array(
new CompType[12], CompType.generator());
print("before sorting:");
print(Arrays.toString(a));
Arrays.sort(a, Collections.reverseOrder());
print("after sorting:");
print(Arrays.toString(a));
}
}

<spoiler text="Output:">

before sorting:
[[i = 58, j = 55], [i = 93, j = 61], [i = 61, j = 29]
, [i = 68, j = 0], [i = 22, j = 7], [i = 88, j = 28]
, [i = 51, j = 89], [i = 9, j = 78], [i = 98, j = 61]
, [i = 20, j = 58], [i = 16, j = 40], [i = 11, j = 22]
]
after sorting:
[[i = 98, j = 61], [i = 93, j = 61], [i = 88, j = 28]
, [i = 68, j = 0], [i = 61, j = 29], [i = 58, j = 55]
, [i = 51, j = 89], [i = 22, j = 7], [i = 20, j = 58]
, [i = 16, j = 40], [i = 11, j = 22], [i = 9, j = 78]
]

</spoiler>
Наконец, вы можете написать собственную реализацию Comparator. В следующем примере объекты CompType сравниваются по значениям j вместо і:

//: arrays/ComparatorTest.java
// Реализация Comparator.
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
class CompTypeComparator implements Comparator<CompType> {
public int compare(CompType o1, CompType o2) {
return (o1.j < o2.j ? -1 : (o1.j == o2.j ? 0 : 1));
}
}
 
public class ComparatorTest {
public static void main(String[] args) {
CompType[] a = Generated.array(
new CompType[12], CompType.generator());
print("before sorting:");
print(Arrays.toString(a));
Arrays.sort(a, new CompTypeComparator());
print("after sorting:");
print(Arrays.toString(a));
}
}

<spoiler text="Output:">

before sorting:
[[i = 58, j = 55], [i = 93, j = 61], [i = 61, j = 29]
, [i = 68, j = 0], [i = 22, j = 7], [i = 88, j = 28]
, [i = 51, j = 89], [i = 9, j = 78], [i = 98, j = 61]
, [i = 20, j = 58], [i = 16, j = 40], [i = 11, j = 22]
]
after sorting:
[[i = 68, j = 0], [i = 22, j = 7], [i = 11, j = 22]
, [i = 88, j = 28], [i = 61, j = 29], [i = 16, j = 40]
, [i = 58, j = 55], [i = 20, j = 58], [i = 93, j = 61]
, [i = 98, j = 61], [i = 9, j = 78], [i = 51, j = 89]
]

</spoiler>

Сортировка массива

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

//: arrays/StringSorting.java
// Sorting an array of Strings.
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class StringSorting {
public static void main(String[] args) {
String[] sa = Generated.array(new String[20],
new RandomGenerator.String(5));
print("Before sort: " + Arrays.toString(sa));
Arrays.sort(sa);
print("After sort: " + Arrays.toString(sa));
Arrays.sort(sa, Collections.reverseOrder());
print("Reverse sort: " + Arrays.toString(sa));
Arrays.sort(sa, String.CASE_INSENSITIVE_ORDER);
print("Case-insensitive sort: " + Arrays.toString(sa));
}
}

<spoiler text="Output:">

Before sort: [YNzbr, nyGcF, OWZnT, cQrGs, eGZMm, JMRoE, suEcU, OneOE, dLsmw, HLGEa, 
hKcxr, EqUCB, bkIna, Mesbt, WHkjU, rUkZP, gwsqP, zDyCy, RFJQA, HxxHv]
After sort: [EqUCB, HLGEa, HxxHv, JMRoE, Mesbt, OWZnT, OneOE, RFJQA, WHkjU, YNzbr,
bkIna, cQrGs, dLsmw, eGZMm, gwsqP, hKcxr, nyGcF, rUkZP, suEcU, zDyCy]
Reverse sort: [zDyCy, suEcU, rUkZP, nyGcF, hKcxr, gwsqP, eGZMm, dLsmw, cQrGs, bkIna,
YNzbr, WHkjU, RFJQA, OneOE, OWZnT, Mesbt, JMRoE, HxxHv, HLGEa, EqUCB]
Case-insensitive sort: [bkIna, cQrGs, dLsmw, eGZMm, EqUCB, gwsqP, hKcxr, HLGEa, HxxHv,
JMRoE, Mesbt, nyGcF, OneOE, OWZnT, RFJQA, rUkZP, suEcU, WHkjU, YNzbr, zDyCy]

</spoiler>
В выходных данных алгоритма сортировки String бросается в глаза то, что алгоритм является лексикографическим, то есть все слова, начинающиеся с прописных букв, предшествуют любым словам, начинающимся со строчных букв. Если вы хотите, чтобы слова группировались независимо от регистра символов, используйте режим String.CASE_INSENSITIVE_ORDER, как показано в последнем вызове sort() из приведенного примера.
Алгоритм сортировки, используемый стандартной библиотекой Java, спроектирован в расчете на оптимальность для сортируемого типа — быстрая сортировка для примитивов, надежная сортировка слиянием для объектов. Обычно вам не приходится беспокоиться о быстродействии, если только профайлер не укажет, что процесс сортировки тормозит работу программы.

Поиск в отсортированном массиве

После того, как массив будет отсортирован, вы сможете быстро найти нужный элемент методом Arrays.binarySearch(). Попытка вызова binarySearch() для несортированного массива приведет к непредсказуемым последствиям. В следующем примере генератор RandomGenerator.Integer заполняет массив, после чего тот же генератор используется для получения искомых значений:

//: arrays/ArraySearching.java
// Using Arrays.binarySearch().
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class ArraySearching {
public static void main(String[] args) {
Generator<Integer> gen =
new RandomGenerator.Integer(1000);
int[] a = ConvertTo.primitive(
Generated.array(new Integer[25], gen));
Arrays.sort(a);
print("Sorted array: " + Arrays.toString(a));
while(true) {
int r = gen.next();
int location = Arrays.binarySearch(a, r);
if(location >= 0) {
print("Location of " + r + " is " + location +
", a[" + location + "] = " + a[location]);
break; // Out of while loop
}
}
}
}

<spoiler text="Output:">

Sorted array: [128, 140, 200, 207, 258, 258, 278, 288, 322, 429, 511, 520, 522,
551, 555, 589, 693, 704, 809, 861, 861, 868, 916, 961, 998]
Location of 322 is 8, a[8] = 322

</spoiler>
Цикл while генерирует случайные значения как искомые до тех пор, пока одно из них не будет найдено в массиве.
Если искомое значение найдено, метод Arrays.binarySearch() возвращает неотрицательный результат. В противном случае возвращается отрицательное значение, представляющее позицию элемента при вставке (при сохранении сортировки массива). Если массив содержит повторяющиеся значения, алгоритм поиска не дает гарантий относительно того, какой именно из дубликатов будет обнаружен. Алгоритм проектировался не для поддержки дубликатов, а для того, чтобы переносить их присутствие. Если вам нужен отсортированный список без повторений элементов, используйте TreeSet (для сохранения порядка сортировки) или LinkedHashSet (для сохранения порядка вставки). Эти классы автоматически берут на себя все детали. Только в ситуациях, критичных по быстродействию, эти классы заменяются массивами с ручным выполнением операций.
При сортировке объектных массивов с использованием Comparator (примитивные массивы не позволяют выполнять сортировку с Comparator) необходимо включать тот же объект Comparator, что и при использовании binarySearch() (пе­регруженной версии). Например, программу StringSorting.java можно модифицировать для выполнения поиска:

//: arrays/AlphabeticSearch.java
// // Поиск с Comparator.
import java.util.*;
import net.mindview.util.*;
 
public class AlphabeticSearch {
public static void main(String[] args) {
String[] sa = Generated.array(new String[30],
new RandomGenerator.String(5));
Arrays.sort(sa, String.CASE_INSENSITIVE_ORDER);
System.out.println(Arrays.toString(sa));
int index = Arrays.binarySearch(sa, sa[10],
String.CASE_INSENSITIVE_ORDER);
System.out.println("Index: "+ index + "\n"+ sa[index]);
}
}

<spoiler text="Output:">

[bkIna, cQrGs, cXZJo, dLsmw, eGZMm, EqUCB, gwsqP, hKcxr, HLGEa, HqXum, 
HxxHv, JMRoE, JmzMs, Mesbt, MNvqe, nyGcF, ogoYW, OneOE, OWZnT, RFJQA,
rUkZP, sgqia, slJrL, suEcU, uTpnX, vpfFv, WHkjU, xxEAJ, YNzbr, zDyCy]
Index: 10
HxxHv

</spoiler>
Объект Comparator передается перегруженному методу binarySearch() в третьем аргументе. В приведенном примере успех поиска гарантирован, так как искомое значение выбирается из самого массива.

Резюме

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

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

Как было показано в этой главе, параметризация плохо сочетается с контейнерами. Даже если вам удастся тем или иным способом заставить их работать вместе, во время компиляции вы будете получать предупреждения.

Все эти факторы показывают, что при программировании для последних версий Java следует отдавать предпочтение контейнерам перед массивами. На массивы следует переходить только при критичных требованиях по быстродействию — и только в том случае, если переход принесет ощутимую пользу.

]]>
Книги по Java https://linexp.ru?id=4738 Wed, 29 Jun 2022 14:11:50 GMT
<![CDATA[Глава 16 Thinking in Java 4th edition]]> СИСТЕМА ВВОДА/ВЫВОДА JAVAСоздание хорошей системы ввода/вывода является одной из труднейших задач разработчика языка. Доказательством этого утверждения служит множество подходов, используемых при разработке систем ввода/вывода.

Содержание

СИСТЕМА ВВОДА/ВЫВОДА JAVA

Создание хорошей системы ввода/вывода является одной из труднейших задач разработчика языка. Доказательством этого утверждения служит множество подходов, используемых при разработке систем ввода/вывода.
Основная сложность состоит в том, что необходимо учесть все возможные ситуации. Это не только наличие множества источников и приемников данных, с которыми необходимо поддерживать связь (файлы, консоль, сетевые соединения), но и реализации различных форм этой связи (последовательный доступ, произвольный, буферизованный, двоичный, символьный, построчный, пословный и т. д.).

Разработчики библиотеки Java решили начать с создания огромного количества классов. Вообще говоря, в библиотеке ввода/вывода Java так много классов, что потеряться в них проще простого (парадоксально, но сама система ввода/вывода Java в действительности не нуждается в таком количестве классов). Потом, после выхода первой версии языка Java, в библиотеке ввода/вывода последовали значительные изменения: к ориентированным на посылку и прием байтов классам добавились основанные на Юникод классы, работающие с символами. В JDK-1.4 классы nіо (от сочетания «new I/O», «новый ввод/вывод») призваны улучшить производительность и функциональность.

В результате, чтобы понять общую картину ввода/вывода в Java и начать использовать ее, вам придется изучить порядочный ворох классов. Вдобавок не менее важно понять и изучить эволюцию библиотеки ввода/вывода, несмотря на вашу очевидную реакцию: «Избавьте меня от истории! Просто покажите, как работать с библиотекой!» Если не уяснить причины изменений, проведенных в библиотеке ввода/вывода, вскоре мы запутаемся в ней и не сможем твердо аргументировать сделанный нами выбор в пользу того или иного класса.
В этой главе мы познакомимся с различными классами, отвечающими за ввод/вывод в библиотеке Java, а также научимся использовать их.

Класс File

Перед тем как перейти к классам, которые осуществляют реальные запись и чтение данных, мы рассмотрим вспомогательные инструменты библиотеки, предназначенные для работы с файлами и каталогами.
Имя класса File весьма обманчиво: легко подумать, что оно всегда ссылается на файл, но это не так. Класс File может представлять как имя определенного файла, так, имена группы файлов, находящихся в каталоге. Если класс представляет каталог, его метод list() возвращает массив строк с именами всех файлов. Использовать в данной ситуации массив (а не более гибкий контейнер) очень удобно: количество файлов в каталоге фиксировано, как и размер массива, а если понадобится узнать имена файлов в другом каталоге, достаточно создать еще один объект File. Следующий раздел покажет, как использовать этот класс в совокупности с тесно связанным с ним интерфейсом FilenameFilter.

Список каталогов

Предположим, вы хотите получить содержимое каталога. Объект File позволяет получить этот список двумя способами. Если вызвать метод list() без аргументов, то результатом будет полный список файлов и каталогов (точнее, их названий), содержащихся в данном каталоге. Но, если вам нужен ограниченный список — например, список всех файлов с расширением .java, — используйте «фильтр», то есть класс, который описывает критерии отбора объектов File.
Рассмотрим пример. Заметьте, что полученный список без всяких дополнительных усилий сортируется (по алфавиту) с помощью метода java.util.Array.sort() и объекта String.CASE_INSENSITIVE_ORDER:

//: io/DirList.java
// Вывод списка каталогов с использованием регулярных выражений.
// {Args: "D.*\.java"}
import java.util.regex.*;
import java.io.*;
import java.util.*;
 
public class DirList {
public static void main(String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new DirFilter(args[0]));
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list)
System.out.println(dirItem);
}
}
 
class DirFilter implements FilenameFilter {
private Pattern pattern;
public DirFilter(String regex) {
pattern = Pattern.compile(regex);
}
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
}

<spoiler text="Output:">

DirectoryDemo.java
DirList.java
DirList2.java
DirList3.java

</spoiler>
Это показывает, что данный тип объекта должен поддерживать метод с именем accept(), который вызывается методом list() с целью определения того, какие имена файлов должны включаться в выходной список, а какие нет. Перед нами один из примеров паттерна «стратегия»: list() реализует базовую функциональность, a FilenameFilter предоставляет алгоритм, необходимый для работы list(). Так как метод list() принимает в качестве аргумента объект FilenameFilter, ему можно передать любой объект любого класса, лишь бы он реализовывал интерфейс FilenameFilter (даже во время выполнения). Таким образом легко изменять результат работы метода list(). Целью данного паттерна является обеспечение гибкости в поведении кода.

Метод accept() получает объект File, представляющий собой каталог, в котором был найден данный файл, и строку с именем файла. Помните, что метод list() вызывает accept() для каждого файла, обнаруженного в каталоге, чтобы определить, какие из них следует включить в выходной список — в зависимости от возвращаемого значения accept() (значение типа boolean).

Метод accept() использует объект регулярного выражения matcher, чтобы посмотреть, соответствует ли имя файла выражению regex. Метод list() возвращает массив.

Безымянные внутренние классы

Описанный пример идеально подходит для демонстрации преимуществ внутренних классов (описанных в главе 10). Для начала создадим метод filter(), который возвращает ссылку на объект FilenameFilter:

//: io/DirList2.java
// Использование безымянных внутренних классов.
// {Args: "D.*\.java"}
import java.util.regex.*;
import java.io.*;
import java.util.*;
 
public class DirList2 {
public static FilenameFilter filter(final String regex) {
// Creation of anonymous inner class:
return new FilenameFilter() {
private Pattern pattern = Pattern.compile(regex);
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
}; // End of anonymous inner class
}
public static void main(String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(filter(args[0]));
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list)
System.out.println(dirItem);
}
}

<spoiler text="Output:">

DirectoryDemo.java
DirList.java
DirList2.java
DirList3.java

</spoiler>
Заметьте, что аргумент метода filter() должен быть неизменным (final). Это необходимо для того, чтобы внутренний класс смог получить к нему доступ даже за пределами области определения аргумента.
Несомненно, структура программы улучшилась хотя бы потому, что объект FilenameFilter теперь неразрывно связан с внешним классом DirList2. Впрочем, можно сделать следующий шаг и определить безымянный внутренний класс как аргумент метода list(), в результате чего программа станет еще более компактной:

//: io/DirList3.java
// Создание безымянного внутреннего класса "на месте".
// {Args: "D.*\.java"}
import java.util.regex.*;
import java.io.*;
import java.util.*;
 
public class DirList3 {
public static void main(final String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0)
list = path.list();
else
list = path.list(new FilenameFilter() {
private Pattern pattern = Pattern.compile(args[0]);
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
});
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list)
System.out.println(dirItem);
}
}

<spoiler text="Output:">

DirectoryDemo.java
DirList.java
DirList2.java
DirList3.java

</spoiler>
На этот раз неизменным (final) объявлен аргумент метода main(), так как безымянный внутренний класс использует параметр командной строки (args[0]) напрямую.
Именно так безымянные внутренние классы позволяют быстро создать «одноразовый» класс, полезный только для решения одной конкретной задачи. Одно из преимуществ такого подхода состоит в том, что весь код, решающий некоторую задачу, находится в одном месте. С другой стороны, полученный код не слишком хорошо читается, поэтому при их использовании необходимо действовать осмотрительно.

Проверка существования и создание каталогов

Класс File не ограничивается представлением существующих файлов или каталогов, он способен на большее. Он также может использоваться для создания нового каталога или даже дерева каталогов, если последние не существуют. Можно также узнать свойства файлов (размер, дату последнего изменения, режим чтения (записи)), определить, файл или каталог представляет объект File, удалить файл. Следующая программа демонстрирует некоторые методы класса File (за полной информацией обращайтесь к документации JDК, доступной для загрузки с сайта java.sun.com):

//: io/MakeDirectories.java
// Использование класса File для создания
// каталогов и выполнения операций с файлами.
// {Параметры: MakeDirectoriesTest} import java io.-*;
import java.io.*;
 
public class MakeDirectories {
private static void usage() {
System.err.println(
"Usage:MakeDirectories path1 ...\n" +
"Creates each path\n" +
"Usage:MakeDirectories -d path1 ...\n" +
"Deletes each path\n" +
"Usage:MakeDirectories -r path1 path2\n" +
"Renames from path1 to path2");
System.exit(1);
}
private static void fileData(File f) {
System.out.println(
"Absolute path: " + f.getAbsolutePath() +
"\n Can read: " + f.canRead() +
"\n Can write: " + f.canWrite() +
"\n getName: " + f.getName() +
"\n getParent: " + f.getParent() +
"\n getPath: " + f.getPath() +
"\n length: " + f.length() +
"\n lastModified: " + f.lastModified());
if(f.isFile())
System.out.println("It's a file");
else if(f.isDirectory())
System.out.println("It's a directory");
}
public static void main(String[] args) {
if(args.length < 1) usage();
if(args[0].equals("-r")) {
if(args.length != 3) usage();
File
old = new File(args[1]),
rname = new File(args[2]);
old.renameTo(rname);
fileData(old);
fileData(rname);
return; // Exit main
}
int count = 0;
boolean del = false;
if(args[0].equals("-d")) {
count++;
del = true;
}
count--;
while(++count < args.length) {
File f = new File(args[count]);
if(f.exists()) {
System.out.println(f + " exists");
if(del) {
System.out.println("deleting..." + f);
f.delete();
}
}
else { // Doesn't exist
if(!del) {
f.mkdirs();
System.out.println("created " + f);
}
}
fileData(f);
}
}
}

<spoiler text="Output:"> (80% match)

created MakeDirectoriesTest
Absolute path: d:\aaa-TIJ4\code\io\MakeDirectoriesTest
Can read: true
Can write: true
getName: MakeDirectoriesTest
getParent: null
getPath: MakeDirectoriesTest
length: 0
lastModified: 1101690308831
It's a directory

</spoiler>
В методе fileData() продемонстрированы различные методы, предназначенные для получения информации о файлах и каталогах.
Сначала в методе main() вызывается метод renameTo(), который позволяет переименовывать (или перемещать) файлы, используя для этого второй аргумент — еще один объект File, который указывает на новое местоположение или имя.
Если вы поэкспериментируете с этой программой, то увидите, что создать пути произвольной сложности очень просто, поскольку всю работу за вас фактически делает метод mkdirs().

Ввод и вывод

В библиотеках ввода/вывода часто используется абстрактное понятие потока (stream) — произвольного источника или приемника данных, который способен производить или получать некоторую информацию. Поток скрывает детали низкоуровневых процессов, происходящих с данными непосредственно в устройствах ввода/вывода.

Классы библиотеки ввода/вывода Java разделены на две части — одни осуществляют ввод, другие вывод. В этом можно убедиться, просмотрев документацию JDK. Все классы, производные от базовых классов InputStream или Reader, имеют методы с именами read() для чтения одиночных байтов или массива байтов. Аналогично, все классы, производные от базовых классов OutputStream или Writer, имеют методы с именами write() для записи одиночных байтов или массива байтов. Впрочем, вы вряд ли станете использовать эти методы напрямую — они в основном предназначены для других классов, предоставляющих более полные возможности. Таким образом, заключение объекта-потока в один класс — занятие довольно неэффективное, обычно несколько объектов «наслаиваются» друг на друга для получения необходимой функциональности. Необходимость построения потока на основе нескольких объектов — главная причина трудностей в освоении библиотеки ввода/вывода Java.

Классы ввода/вывода удобно разделить по категориям, в зависимости от их функций. В Java 1.0 разработчики решили, что все, связанное с вводом данных, должно быть производным от базового класса InputStream, а все, имеющее отношение к выводу данных, — от класса OutputStream.

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

Типы InputStream

Назначение базового класса InputStream — представлять классы, которые получают данные из различных источников. Такими источниками могут быть:


  • массив байтов;
  • строка (String);
  • файл;
  • «канал» (pipe): данные помещаются с одного «конца» и извлекаются с другого;
  • последовательность различных потоков, которые можно объединить в одном потоке;
  • другие источники (например, подключение к Интернету).

С каждым из перечисленных источников связывается некоторый подкласс базового класса InputStream (табл. 16.1). Существует еще класс FilterInputStream, который также является производным классом InputStream и представляет собой основу для классов-«надстроек», наделяющих входные потоки полезными свойствами и интерфейсами. Его мы обсудим чуть позже.

P0490.png

Типы OutputStream

В данную категорию (табл. 16.2) попадают классы, определяющие, куда направляются ваши данные: в массив байтов (но не напрямую в String; предполагается, что вы сможете создать их из массива байтов), в файл или в канал. Вдобавок класс FilterOutputStream предоставляет базовый класс для классов-«надстроек», которые способны наделять существующие потоки новыми полезными атрибутами и интерфейсами. Подробности мы отложим на потом.
P0491.png

Добавление атрибутов и интерфейсов

«Наслаивание» дополнительных объектов для получения новых свойств и функций у определенных объектов называется надстройкой, или декораторомВ библиотеке ввода/вывода Java постоянно требуется совмещение нескольких свойств, поэтому в ней и используются надстройки. Существование

Все употребляемые далее термины: «настраивание», «наслаивание», «фильтрация» или «декорирование» — по сути, означают одно и то же — перегрузку всех методов InputStream для придания до­полнительной функциональности при работе с данными потоков. При этом класс FilterInputStream осуществляет перегрузку без посторонней помощи, а данные соответствующим образом трансфор­мируются. Подклассы FilterInputStream могут, в свою очередь, перегрузить эти же методы и добавить дополнительные методы и поля. — Примеч. ред.

в библиотеке ввода/вывода Java классов-фильтров объясняется тем, что абстрактный класс фильтра является базвовым классом для всех существующих надстроек. (Надстройка должна включать в себя интерфейс «декорируемого» объекта, но расширение этого интерфейса также не запрещено и практикуется некоторыми классами-«фильтрами».)
Однако у такого подхода есть свои недостатки. Надстройки предоставляют дополнительную гибкость при написании программы (можно легко совмещать различные атрибуты), но при этом код получается излишне сложным. Некоторое неудобство библиотеки ввода/вывода Java объясняется тем, что для получения желаемого объекта приходится создавать много дополнительных объектов — «ядро» ввода/вывода и несколько надстроек.
Интерфейс для надстроек предоставляют классы FilterInputStream (для входных потоков) и FilterOutputStream (для выходных потоков). Это абстрактные классы, производные от основных базовых классов библиотеки ввода/вывода InputStream и OutputStream. Наследование от этих классов — основное требование к классу-надстройке (поскольку таким образом обеспечивается общий интерфейс для «наслаиваемых» объектов).

Чтение из InputStream с использованием FilterInputStream

Классы, производные от FilterInputStream (табл. 16.3), выполняют две различные миссии. DataInputStream позволяет читать из потока различные типы простейших данных и строки. (Все методы этого класса начинаются с префикса read — например, readByte(), readFloat() и т. п.) Этот класс, вместе с «парным» классом DataOutputStream, предоставляет возможность направленной передачи простейших данных посредством потоков. Место доставки определяется классами, описанными в табл. 16.1.

Другие классы изменяют внутренние механизмы входного потока: применение буферизации, подсчет количества прочитанных строк (что позволяет запросить или задать номер строки), возможность возврата в поток только что прочитанных символов. Вероятно, последние два класса создавались в основном для построения компиляторов (то есть их добавили в библиотеку в процессе написания компилятора Java), и прока в повседневном программировании от них немного.

Ввод данных почти всегда осуществляется с буферизацией, вне зависимости от присоединенного устройства ввода/вывода, поэтому имеет смысл сделать отдельный класс (или просто специальный метод) не для буферизованного ввода данных, а, наоборот, для прямого.

Таблица 16.3. Разновидности надстроек FilterInputStream

P0493.png

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

Следит за количеством считанных из потока строк; вы можете узнать их число, вызвав метод getLineNumber(), а также перейти к определенной строке с помощью метода setLineNumber(int) Имеет односимвольный буфер, который позволяет вернуть в поток только что прочитанный символ

Входной поток InputStream, размер буфера (необязательно). По существу, никакого интерфейса этот класс не предоставляет, он просто присоединяет к потоку буфер. Добавьте дополнительно объект, предоставляющий интерфейс Входной поток InputStream. Этот класс просто считывает строки, поэтому к нему стоит добавить еще одну, более полезную, надстройку

Входной поток InputStream. Обычно используется при сканировании файлов для компилятора. Скорее всего, вам он не понадобится

Запись в OutputStream с помощью FilterOutputStream

У класса DataInputStream существует «парный» класс DataOutputStream (табл. 16.4), который позволяет форматировать и записывать в поток примитивы и строки таким образом, что на любой машине и на любой платформе их сможет прочитать и правильно обработать DataInputStream. Все его методы начинаются с префикса write (запись): writeByte(), writeFloat() и т. п.
Класс PrintStream изначально был предназначен для вывода значений в формате, понятном для человека. В данном аспекте он отличается от класса DataOutputStream, потому что цель последнего — разместить данные в потоке так, чтобы DataInputStream смог их правильно распознать.
Основные методы класса PrintStream — print() и println(), они перегружены для наиболее часто используемых типов. После вывода значения метод println() осуществляет перевод на новую строку, в то время как метод print() этого не делает.
На практике класс PrintStream довольно-таки неудобен, поскольку он перехватывает и скрывает все возбуждаемые исключения. (Приходится явно вызывать метод checkError(), который возвращает true, если во время исполнения какого-либо из методов класса произошла ошибка.) К тому же этот класс как следует не интернационализован и не выполняет перевод строки платформно-независимым способом (все эти проблемы решены в классе PrintWriter, описанном ниже).
Класс BufferedOutputStream модифицирует поток так, чтобы использовалась буферизация при записи данных, которая предотвращает слишком частые обращения к физическому устройству. Вероятно, именно его следует порекомендовать для вывода в поток каких-либо данных.

P0494.png

Используется в сочетании с классом DataInputStream, позволяет записывать в поток примитивы (целые, символы и т. д.) независимо от платформы При записи форматирует данные. Если класс DataOutputStream отвечает за хранение данных, то этот класс отвечает за отображение данных
Используется для буферизации вывода, то есть для предотвращения прямой записи в устройство каждый раз при записи данных. Для записи содержимого буфера в устройство используется метод flush()
Выходной поток OutputStream. Содержит все необходимое для записи простейших типов данных
Выходной поток OutputStream, а также необязательный логический (boolean) аргумент, показывающий, нужно ли очищать буфер записи при переходе на новую строку.
Должен быть последним в «наслоении» объектов для выходного потока OutputStream. Вероятно, вы будете часто его использовать Выходной поток OutputStream, а также размер буфера записи (необязательно). Не предоставляет интерфейса как такового, просто указывает, что при выводе необходимо использовать буферизацию. К классу следует присоединить более содержательный класс-«фильтр»

Классы Reader и Writer

При выпуске Java версии 1.1 в библиотеке ввода/вывода были сделаны значительные изменения. Когда в первый раз видишь новые классы, основанные на базовых классах Reader и Writer, возникает мысль, что они пришли на замену классам InputStream и OutputStream. Тем не менее это не так. Хотя некоторые аспекты изначальной библиотеки ввода/вывода, основанной на потоках, объявлены устаревшими (и при их использовании компилятор выдаст соответствующее предупреждение), классы InputStream и OutputStream все еще предоставляют достаточно полезные возможности для проведения байт-ориентированного ввода/ вывода. Одновременно с этим классы Reader и Writer позволяют проводить операции символьно ориентированного ввода/вывода, в кодировке Юникод. Дополнительно:


  • В Java 1.1 в иерархию потоков, основанную на классах InputStream и OutputStream, были добавлены новые классы. Очевидно, эта иерархия не должна была уйти в прошлое.
  • В некоторых ситуациях для решения задачи используются как «байтовые», так и «символьные» классы. Для этого в библиотеке появились классы-адаптеры: InputStreamReader конвертирует InputStream в Reader, a OutputStreamWriter трансформирует OutputStream в Writer.

Основной причиной появления иерархий классов Reader и Writer стала интернационализация. Старая библиотека ввода/вывода поддерживала только 8-битовые символы и зачастую неверно обращалась с 16-битовыми символами Юникода. Именно благодаря символам Юникода возможна интернационализация программ (простейший тип Java char (символ) также основан на Юникоде), поэтому новые классы отвечают за их правильное использование в операциях ввода/вывода. Вдобавок новые средства спроектированы так, что работают быстрее старых классов.

Источники и приемники данных

Практически у всех изначальных потоковых классов имеются соответствующие классы Reader и Writer со схожими функциями, однако работающие с символами Юникода. Впрочем, во многих ситуациях правильным (а зачастую и единствен­ным) выбором становятся классы, ориентированные на прием и посылку байтов; в особенности это относится к библиотекам сжатия данных java.util.zip. Поэтому лучше всего будет такая тактика: пытаться использовать классы Reader и Writer где только возможно. Обнаружить место, где эти классы неприменимы, будет нетрудно — компилятор выдаст вам сообщение об ошибке.

В табл. 16.5 показано соответствие между источниками и получателями информации двух иерархий библиотеки ввода/вывода Java.

P0495.png

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

Изменение поведения потока

Для потоков InputStream и OutputStream существуют классы-«декораторы» на основе классов FilterInputStream и FilterOutputStream. Они позволяют модифицировать изначальный поток ввода/вывода так, как это необходимо в данной ситуации. Иерархия на основе классов Reader и Writer также взяла на вооружение данный подход, но по-другому.

В табл. 16.6 соответствие классов уже не такое точное, как это было в предыдущей таблице. Причина — организация классов: в то время как BufferedOutputStream является подклассом FilterOutputStream, класс BufferedWriter не наследует от базового класса FilterWriter (от него вообще не происходит ни одного класса, хотя он и является абстрактным — видимо, его поместили в библиотеку просто для полноты картины). Впрочем, интерфейсы классов очень похожи.


P0496.png

Один совет очевиден: для чтения строк больше не следует употреблять класс DataInputStream (при такой попытке компилятор сообщит вам, что этот метод для чтения строк устарел), вместо него используйте класс BufferedReader. Во всех других ситуациях класс DataInputStream остается выбором «номер один» из всего многообразия библиотеки ввода/вывода.

Чтобы облегчить переход к классу PrintWriter, в него добавили конструктор, который принимает в качестве аргумента выходной поток OutputStream (обычный конструктор принимает класс Writer). Интерфейс форматирования PrintWriter практически идентичен интерфейсу PrintStream.

В Java SE5 были добавлены конструкторы PrintWriter, упрощающие создание файлов при выводе (см. далее).

Кроме того, в конструкторе класса PrintWriter можно указать дополнительный флаг, чтобы содержимое буфера каждый раз сбрасывалось при записи новой строки (методом println()).

Классы, оставленные без изменений

Некоторые классы избежали перемен и остались в версии Java 1.1 в том же виде, что и в версии 1.0:

• DataOutputStream;
• File;
• RandomAccessFile;
• SequenceInputStream.

Обращает на себя внимание тот факт, что изменения не коснулись класса DataOutputStream, используемого для пересылки данных независимым от платформы и машины способом, поэтому для передачи данных между компьютерами по-прежнему остаются актуальными иерархии InputStream и OutputStream.

RandomAccessFile: сам по себе

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

Поначалу с трудом верится, что класс RandomAccessFile не является полноценным представителем иерархии потоков ввода/вывода на основе классов InputStream и OutputStream. Но тем не менее никаких связей с этими классами и их иерархиями у него нет, разве что он реализует интерфейсы DataInput и DataOutput (также реализуемые классами DataInputStream и DataOutputStream). Он не использует функциональность существующих классов из иерархии InputStream и OutputStream — это полностью независимый класс, написанный «с чистого листа», со своими собственными методами. Причина кроется, скорее всего, в том, что класс RandomAccessFile позволяет свободно перемещаться по файлу как в прямом, так и в обратном направлении, что для других типов ввода/вывода невозможно. Так или иначе, он стоит особняком и напрямую наследует от корневого класса Object.

По сути, класс RandomAccessFile похож на пару совмещенных в одном классе потоков DataInputStream и DataOutputStream, к которым на всем «протяжении» применимы: метод getFilePointer(), показывающий, где вы «находитесь» в данный момент; метод seek(), позволяющий перемещаться на заданную позицию файла; и метод length(), определяющий максимальный размер файла. Вдобавок, конструктор этого класса требует второй аргумент (схоже с методом fopen() в С), устанавливающий режим использования файла: только для чтения (строка «r») или для чтения и для записи (строка «rw»). Поддержки файлов только для записи нет, поэтому разумно предположить, что класс RandomAccessFile можно было бы унаследовать от DataInputStream без потери функциональности.

Прямое позиционирование допустимо только для класса RandomAccessFile, и работает оно только в случае файлов. Класс BufferedInputStream позволяет вам пометить некоторую позицию потока методом mark(), а затем вернуться к ней методом reset(). Однако эта возможность ограничена (позиция запоминается в единственной внутренней переменной) и потому нечасто востребована.Большая часть (если не вся) функциональности класса RandomAccessFile в JDK-1.4 также реализуется отображаемыми в память файлами (memory-mapped files) из нового пакета nio. Мы обсудим их чуть позже.

Типичное использование потоков ввода/вывода

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

Буферизованное чтение из файла

Чтобы открыть файл для посимвольного чтения, используется класс FileInputReader; имя файла задается в виде строки (String) или объекта File. Ускорить процесс чтения помогает буферизация ввода, для этого полученная ссылка передается в конструктор класса BufferedReader. Так как в интерфейсе класса- имеется метод readLine(), все необходимое для чтения имеется в вашем распоряжении. При достижении конца файла метод readLine() возвращает ссылку null.

//: io/BufferedInputFile.java
import java.io.*;
 
public class BufferedInputFile {
// Throw exceptions to console:
public static String
read(String filename) throws IOException {
// Reading input by lines:
BufferedReader in = new BufferedReader(
new FileReader(filename));
String s;
StringBuilder sb = new StringBuilder();
while((s = in.readLine())!= null)
sb.append(s + "\n");
in.close();
return sb.toString();
}
public static void main(String[] args)
throws IOException {
System.out.print(read("BufferedInputFile.java"));
}
} /* (Execute to see output) *///:~

Объект StringBuilder sb служит для объединения всего прочитанного текста (включая переводы строк, поскольку метод readLine() их отбрасывает). В завершение файл закрывается методом close().

Чтение из памяти

В этой секции результат String файла BufferedInputFile.read() используется для создания StringReader. Затем символы последовательно читаются методом read(), и каждый следующий символ посылается на консоль.

//: io/MemoryInput.java
import java.io.*;
 
public class MemoryInput {
public static void main(String[] args)
throws IOException {
StringReader in = new StringReader(
BufferedInputFile.read("MemoryInput.java"));
int c;
while((c = in.read()) != -1)
System.out.print((char)c);
}
} /* (Execute to see output) *///:~

Обратите внимание: метод read() возвращает следующий символ в формате int, и для правильного вывода его необходимо предварительно преобразовать в char.

Форматированное чтение из памяти

Для чтения «форматированных» данных применяется класс DataInputStream, ориентированный на ввод/вывод байтов, а не символов. В данном случае необходимо использовать классы иерархии InputStream, а не их аналоги на основе класса Reader. Конечно, можно прочитать все, что угодно (например, файл), через InputStream, но здесь используется тип String.

//: io/FormattedMemoryInput.java
import java.io.*;
 
public class FormattedMemoryInput {
public static void main(String[] args)
throws IOException {
try {
DataInputStream in = new DataInputStream(
new ByteArrayInputStream(
BufferedInputFile.read(
"FormattedMemoryInput.java").getBytes()));
while(true)
System.out.print((char)in.readByte());
} catch(EOFException e) {
System.err.println("End of stream");
}
}
} /* (Execute to see output) *///:~

Для преобразования строки в массив байтов, пригодный для помещения в поток ByteArrayInputStream, в классе String предусмотрен метод getBytes(). Полученный ByteArrayInputStream представляет собой поток InputStream, подходящий для передачи DataInputStream.
При побайтовом чтении символов из форматированного потока DataInputStream методом readByte() любое полученное значение будет считаться действительным, поэтому возвращаемое значение неприменимо для идентификации конца потока. Вместо этого можно использовать метод available(), который сообщает, сколько еще осталось символов. В следующем примере показано, как читать файл побайтно:

//: io/TestEOF.java
// Проверка достижения конца файла одновременно
// с чтением из него по байту.
import java.io.*;
public class TestEOF {
public static void main(String[] args)
throws IOException {
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("TestEOF.java")));
while(in.available() != 0)
System.out.print((char)in.readByte());
}
} /* (Execute to see output) *///:~

Заметьте, что метод available() работает по-разному в зависимости от источника данных; дословно его функция описывается следующим образом: «количество байтов, которые можно прочитать без блокировки». При чтении из файла это означает весь файл, но для другого рода потоков это не обязательно верно, поэтому используйте этот метод разумно.

Определить конец входного потока можно и с помощью перехвата исключения. Впрочем, применение исключений в таких целях считается злоупотреблением.

Вывод в файл

Объект FileWriter записывает данные в файл. При вводе/выводе практически всегда применяется буферизация (попробуйте прочитать файл без нее, и вы увидите, насколько ее отсутствие влияет на производительность — скорость чтения уменьшится в несколько раз), поэтому мы присоединяем надстройку BufferedWriter. После этого подключается PrintWriter, чтобы выполнять форматированный вывод. Файл данных, созданный такой конфигурацией ввода/вывода, можно прочитать как обычный текстовый файл.

//: io/BasicFileOutput.java
import java.io.*;
 
public class BasicFileOutput {
static String file = "BasicFileOutput.out";
public static void main(String[] args)
throws IOException {
BufferedReader in = new BufferedReader(
new StringReader(
BufferedInputFile.read("BasicFileOutput.java")));
PrintWriter out = new PrintWriter(
new BufferedWriter(new FileWriter(file)));
int lineCount = 1;
String s;
while((s = in.readLine()) != null )
out.println(lineCount++ + ": " + s);
out.close();
// Вывод содержимого файла
System.out.println(BufferedInputFile.read(file));
}
} /* (Execute to see output) *///:~

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

Когда данные входного потока исчерпываются, метод readLine() возвращает null. Для потока out явно вызывается метод close(); если не вызвать его для всех выходных файловых потоков, в буферах могут остаться данные, и файл получится неполным.

Сокращенная форма вывода текстового файла

В Java SE5 у PrintWriter появился вспомогательный конструктор. Благодаря ему вам не придется вручную выполнять всю работу каждый раз, когда вам потребуется создать текстовый файл и записать в него данные. Вот как выглядит пример BasicFileOutput.java в обновленном виде:

//: io/FileOutputShortcut.java
import java.io.*;
public class FileOutputShortcut {
static String file = "FileOutputShortcut.out";
public static void main(String[] args)
throws IOException {
BufferedReader in = new BufferedReader(
new StringReader(
BufferedInputFile.read("FileOutputShortcut.java")));
// Сокращенная запись:
PrintWriter out = new PrintWriter(file);
int lineCount = 1;
String s;
while((s = in.readLine()) != null )
out.println(lineCount++ + ": " + s);
out.close();
// Вывод содержимого файла:
System.out.println(BufferedInputFile.read(file));
}
} /* (Execute to see output) *///:~

Буферизация по-прежнему обеспечена, но вам не приходится включать ее самостоятельно. К сожалению, для других распространенных операций сокращенной записи не предусмотрено, поэтому типичный код ввода/вывода по-прежнему содержит немало избыточного текста.

Сохранение и восстановление данных

PrintWriter форматирует данные так, чтобы их мог прочитать человек. Однако для вывода информации, предназначенной для другого потока, следует использовать классы DataOutputStream (для записи данных) и DataInputStream (для чтения данных). Конечно, природа этих потоков может быть любой, но в нашем случае открывается файл, буферизованный как для чтения, так и для записи. Надстройки DataOutputStream и DataInputStream ориентированы на посылку байтов, поэтому для них требуются потоки OutputStream и InputStream:

//: io/StoringAndRecoveringData.java
import java.io.*;
 
public class StoringAndRecoveringData {
public static void main(String[] args)
throws IOException {
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("Data.txt")));
out.writeDouble(3.14159);
out.writeUTF("That was pi");
out.writeDouble(1.41413);
out.writeUTF("Square root of 2");
out.close();
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("Data.txt")));
System.out.println(in.readDouble());
// Только readUTF() нормально читает
// строки в кодировке UTF для Java:
System.out.println(in.readUTF());
System.out.println(in.readDouble());
System.out.println(in.readUTF());
}
}

<spoiler text="Output:">

3.14159
That was pi
1.41413
Square root of 2

</spoiler>
Если данные записываются в выходной поток DataOutputStream, язык Java гарантирует, что эти данные в точно таком же виде будут восстановлены входным потоком DataInputStream — невзирая на платформу, на которой производится запись или чтение. Это чрезвычайно ценно, и это знает любой, так или иначе соприкасавшийся с вопросами переносимости программ. Если Java поддерживается на обеих платформах, проблема исчезает сама собой.

Единственным надежным способом записать в поток DataOutputStream строку (String) так, чтобы ее можно было потом правильно считать потоком DataInputStream, является кодирование UTF-8, реализуемое методами readUTF() и writeUTF(). UTF-8 — это разновидность кодировки Юникод, в которой каждый символ хранится в двух байтах. Если вы работаете только с кодировкой ASCII, «удвоение» данных в Юникоде приводит к неоправданным затратам дискового пространства и (или) нагрузке на сеть. Поэтому UTF-8 кодирует символы ASCII одним байтом, а символы из других кодировок записывает двумя или тремя байтами. Вдобавок в первых двух байтах строки хранится ее длина. Впрочем, методы readUTF() и writeUTF() используют специальную модификацию UTF-8 для Java (она описана в документации JDK), и для правильного считывания из другой программы (не на Java) строки, записанной методом writeUTF(), вам придется добавить в нее специальный код, позволяющий верно ее считать.

Методы readUTF() и writeUTF() позволяют смешивать строки и другие типы данных, записываемые потоком DataOutputStream, так как вы знаете, что строки будут правильно сохранены в Юникоде и их будет просто воспроизвести потоком DataInputStream.

Метод writeDouble() записывает число double в поток, а соответствующий ему метод readDouble() затем восстанавливает его (для других типов также существуют подобные методы). Но, чтобы правильно интерпретировать любые данные, вы должны точно знать их расположение в потоке; при наличии такой информации прочитать число double как какую-то последовательность байтов или символов не представляет сложности. Поэтому данные в файле должны иметь определенный формат, или вам придется использовать дополнительную информацию, показывающую, какие именно данные находятся в определенных местах. Заметьте, что сериализация объектов (описанная в этой главе чуть позже) часто предоставляет простейший способ записи и восстановления сложных структур данных.

Чтение/запись файлов с произвольным доступом

Как уже было замечено, работа с классом RandomAccessFile напоминает использование совмещенных в одном классе потоков DataInputStream и DataOutputStream (они реализуют те же интерфейсы DataInput и DataOutput). Кроме того, метод seek() позволяет переместиться к определенной позиции и изменить хранящееся там значение.

При использовании RandomAccessFile необходимо знать структуру файла, чтобы правильно работать с ним. Класс RandomAccessFile содержит методы для чтения и записи примитивов и строк UTF-8. Пример:

//: io/UsingRandomAccessFile.java
import java.io.*;
 
public class UsingRandomAccessFile {
static String file = "rtest.dat";
static void display() throws IOException {
RandomAccessFile rf = new RandomAccessFile(file, "r");
for(int i = 0; i < 7; i++)
System.out.println(
"Value " + i + ": " + rf.readDouble());
System.out.println(rf.readUTF());
rf.close();
}
public static void main(String[] args)
throws IOException {
RandomAccessFile rf = new RandomAccessFile(file, "rw");
for(int i = 0; i < 7; i++)
rf.writeDouble(i*1.414);
rf.writeUTF("The end of the file");
rf.close();
display();
rf = new RandomAccessFile(file, "rw");
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();
display();
}
}

<spoiler text="Output:">

Value 0: 0.0
Value 1: 1.414
Value 2: 2.828
Value 3: 4.242
Value 4: 5.656
Value 5: 7.069999999999999
Value 6: 8.484
The end of the file
Value 0: 0.0
Value 1: 1.414
Value 2: 2.828
Value 3: 4.242
Value 4: 5.656
Value 5: 47.0001
Value 6: 8.484
The end of the file

</spoiler>
Метод display() открывает файл и выводит семь значений в формате double. Метод main() создает файл, открывает и модифицирует его. Поскольку значение double всегда занимает 8 байт, для перехода к пятому числу методу seek() следует передать смещение 5*8.
Как упоминалось ранее, класс RandomAccessFile отделен от остальных классов иерархии ввода/вывода, если не считать того факта, что он реализует интерфейсы DataInput и DataOutput. Приходится предполагать, что для этого RandomAccessFile правильно организована буферизация, потому что включить ее в программе не удастся.
Некоторая свобода выбора предоставляется только со вторым аргументом конструктора: RandomAccessFile может открываться в режиме чтения ("r") или чтения/записи ("rw").
Также стоит рассмотреть возможность употребления вместо класса RandomAccessFile механизма отображаемых в память файлов.

Каналы

В этой главе были коротко упомянуты классы каналов PipedInputStream, PipedOutputStream, PipedReader и PipedWriter. Это не значит, что они редко используются или не слишком полезны, просто их смысл и действие нельзя донести до по­нимания до тех пор, пока не объяснена многозадачность: каналы предназначены для связи между отдельными потоками программы. Они будут описаны позднее.

Средства чтения и записи файлов

Очень часто в программировании производится такая цепочка действий: файл считывается в память, там он изменяется, а потом снова записывается на диск. Одна из проблем при работе с библиотекой ввода/вывода Java состоит в том, что для выполнения таких достаточно типичных операций вам придется написать некоторое количество кода — не существует вспомогательных функций, на которые можно переложить такую деятельность. Что еще хуже, с надстройками вообще трудно запомнить, как открываются файлы. Поэтому имеет смысл добавить в вашу библиотеку вспомогательные классы, которые легко сделают нужное за вас. В Java SE5 у PrintWriter появился вспомогательный конструктор, позволяющий легко открыть текстовый файл для чтения. Тем не менее существует много других типичных задач, часто выполняемых в повседневной работе, и было бы разумно избавиться от лишнего кода, связанного с их выполнением. Ниже показан такой класс TextFile с набором статических методов, построчно считывающих и записывающих текстовые файлы. Вдобавок можно создать экземпляр класса TextFile, который будет хранить содержимое файла в списке ArrayList (и функциональность списка ArrayList станет доступной при работе, с содержимым файла):

//: net/mindview/util/TextFile.java
// Статические функции для построчного считывания и записи
// текстовых файлов,- а также манипуляции файлом как списком ArrayList
package net.mindview.util;
import java.io.*;
import java.util.*;
 
public class TextFile extends ArrayList<String> {
// Read a file as a single string:
public static String read(String fileName) {
StringBuilder sb = new StringBuilder();
try {
BufferedReader in= new BufferedReader(new FileReader(
new File(fileName).getAbsoluteFile()));
try {
String s;
while((s = in.readLine()) != null) {
sb.append(s);
sb.append("\n");
}
} finally {
in.close();
}
} catch(IOException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
// Write a single file in one method call:
public static void write(String fileName, String text) {
try {
PrintWriter out = new PrintWriter(
new File(fileName).getAbsoluteFile());
try {
out.print(text);
} finally {
out.close();
}
} catch(IOException e) {
throw new RuntimeException(e);
}
}
// Read a file, split by any regular expression:
public TextFile(String fileName, String splitter) {
super(Arrays.asList(read(fileName).split(splitter)));
// Regular expression split() often leaves an empty
// String at the first position:
if(get(0).equals("")) remove(0);
}
// Normally read by lines:
public TextFile(String fileName) {
this(fileName, "\n");
}
public void write(String fileName) {
try {
PrintWriter out = new PrintWriter(
new File(fileName).getAbsoluteFile());
try {
for(String item : this)
out.println(item);
} finally {
out.close();
}
} catch(IOException e) {
throw new RuntimeException(e);
}
}
// Simple test:
public static void main(String[] args) {
String file = read("TextFile.java");
write("test.txt", file);
TextFile text = new TextFile("test.txt");
text.write("test2.txt");
// Break into unique sorted list of words:
TreeSet<String> words = new TreeSet<String>(
new TextFile("TextFile.java", "\\W+"));
// Display the capitalized words:
System.out.println(words.headSet("a"));
}
}

<spoiler text="Output:">

[0, ArrayList, Arrays, Break, BufferedReader, BufferedWriter, Clean, 
Display, File, FileReader, FileWriter, IOException, Normally, Output,
PrintWriter, Read, Regular, RuntimeException, Simple, Static, String,
StringBuilder, System, TextFile, Tools, TreeSet, W, Write]

</spoiler>
Метод read() присоединяет каждую строку к StringBuilder, а за ней присоединяется перевод строки, удаленный при чтении. Затем возвращается объект String, содержащий весь файл. Метод write() открывает файл и записывает в него текст.
Обратите внимание: к каждой операции открытия файла добавляется парный вызов close() в секции finally. Тем самым обеспечивается гарантированное закрытие файла после завершения работы.
Конструктор использует метод read() для превращения файла в String, после чего он вызывает метод String.split(), чтобы разбить результат на строки. В качестве разделителя используются символы новой строки (если вы будете часто использовать этот класс, то, возможно, захотите переписать этот конструктор, чтобы он работал эффективнее). К сожалению, аналогичного метода для соединения строк нет, так что для записи строк придется обойтись нестатическим методом write().
Так как класс должен упростить процесс чтения и записи файлов, все исключения IOException преобразуются в RuntimeException, чтобы пользователю не пришлось создавать блоки try/catch. Возможно, вы предпочтете создать другую версию, которая возвращает IOException вызывающей стороне.

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

Стандартный ввод/вывод

Термин «стандартный ввод/вывод» возник еще в эпоху UNIX (и в некоторой форме имеется и в Windows, и во многих других операционных системах). Он означает единственный поток информации, используемый программой. Вся информация программы приходит из стандартного ввода (standard input), все данные записываются в стандартный вывод (standard output), а все ошибки программы передаются в стандартный поток для ошибок (standard error). Значение стандартного ввода/вывода состоит в том, что программы легко соединять в цепочку, где стандартный вывод одной программы становится стандартным вводом другой программы. Это мощный инструмент.

Чтение из стандартного потока ввода

Следуя модели стандартного ввода/вывода, Java определяет необходимые потоки для стандартного ввода, вывода и ошибок: System.in, System.out и System.err. На многих страницах книги вы не раз могли наблюдать процесс записи в стандартный вывод System.out, для которого уже надстроен класс форматирования данных PrintStream. Поток для ошибок System.err схож со стандартным выводом, а стандартный ввод System.in представляет собой «низкоуровневый» поток InputStream без дополнительных надстроек. Это значит, что потоки System.out и System.err можно использовать напрямую, в то время как стандартный ввод System.in желательно надстраивать.

Обычно чтение осуществляется построчно, методом readLine(), поэтому имеет смысл буферизовать стандартный ввод System.in посредством BufferedReader. Чтобы сделать это, предварительно следует конвертировать поток System.in в считывающее устройство Reader посредством класса-преобразователя InputStreamReader. Следующий пример просто отображает на экране последнюю строку, введенную пользователем (эхо-вывод):

//: io/Echo.java
/ Чтение из стандартного ввода.
// {RunByHand}
import java.io.*;
 
public class Echo {
public static void main(String[] args)
throws IOException {
BufferedReader stdin = new BufferedReader(
new InputStreamReader(System.in));
String s;
while((s = stdin.readLine()) != null && s.length()!= 0)
System.out.println(s);
// An empty line or Ctrl-Z terminates the program
}
}

Присутствие спецификации исключений объясняется тем, что метод readLine() может возбуждать исключение IOException. Снова обратите внимание, что поток System.in обычно буферизуется, впрочем, как и большинство потоков.

Замена System.out на PrintWriter

Стандартный вывод System.out является объектом PrintStream, который, в свою очередь, наследует от базового класса OutputStream. В классе PrintWriter имеется конструктор, который принимает в качестве аргумента выходной поток OutputStream. Таким образом, вы можете преобразовать поток стандартного вывода System.out в символьно-ориентированный поток PrintWriter:

//: io/ChangeSystemOut.java
// Преобразование System out в символьный поток PrintWriter.
import java.io.*;
 
public class ChangeSystemOut {
public static void main(String[] args) {
PrintWriter out = new PrintWriter(System.out, true);
out.println("Hello, world");
}
}

<spoiler text="Output:">

Hello, world

</spoiler>
Важно использовать конструктор класса PrintWriter с двумя аргументами, и передать во втором аргументе true, чтобы обеспечить автоматический сброс буфера на печать, иначе можно вовсе не увидеть никакого вывода.

Перенаправление стандартного ввода/вывода

Класс System позволяет вам перенаправить стандартный ввод, вывод и поток ошибок. Для этого предусмотрены простые статические методы: setIn(InputStream); setOut(PrintStream); setErr(PrintStream).
Перенаправление стандартного вывода особенно полезно тогда, когда ваша программа выдает слишком много сообщений сразу и вы попросту не успеваете читать их, поскольку они заменяются новыми сообщениями. Перенаправление ввода удобно для программ, работающих с командной строкой, в которых необходимо поддержать некоторую последовательность введенных пользователем данных. Вот простой пример, показывающий, как использовать эти методы:

//: io/Redirecting.java
// Перенаправление стандартного ввода/вывода.
import java.io.*;
 
public class Redirecting {
public static void main(String[] args)
throws IOException {
PrintStream console = System.out;
BufferedInputStream in = new BufferedInputStream(
new FileInputStream("Redirecting.java"));
PrintStream out = new PrintStream(
new BufferedOutputStream(
new FileOutputStream("test.out")));
System.setIn(in);
System.setOut(out);
System.setErr(out);
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
String s;
while((s = br.readLine()) != null)
System.out.println(s);
out.close(); // Remember this!
System.setOut(console);
}
}

Программа присоединяет стандартный ввод к файлу и перенаправляет, стандартный ввод и поток для ошибок в другие файлы. Обратите внимание на сохранение ссылки на исходный объект System.out в начале программы и его восстановление в конце. Перенаправление основано на байтовом, а не на символьном вводе/выводе, поэтому в примере используются InputStream и OutputStream, а не их символьно-ориентированные эквиваленты Reader и Writer.

Новый ввод/вывод (nio)

При создании библиотеки «нового ввода/вывода» Java, появившейся в JDK-1.4 в пакетах java.nio.*, ставилась единственная цель: скорость. Более того, «старые» пакеты ввода/вывода были переписаны с учетом достижений nio, с намерением использовать преимущества повышенного быстродействия, поэтому улучшения вы получите, даже если не будете писать явный nіо-код. Подъем производительности просматривается как в файловом вводе/выводе, который мы здесь рассматриваем, так и в сетевом вводе/выводе.

Увеличения скорости удалось достичь с помощью структур, близких к средствам самой операционной системы: каналов[30] (channels) и буферов (buffers). Канал можно сравнить с угольной шахтой, вырытой на угольном пласте (данные), а буфер — с вагонеткой, которую вы посылаете в шахту. Тележка возвращается доверху наполненная углем, который вы из нее выгружаете. Таким образом, прямого взаимодействия с каналом у вас нет, вы работаете с буфером и «посылаете» его в канал. Канал либо извлекает данные из буфера, либо помещает их в него.

Напрямую взаимодействует с каналом только буфер ByteBuffer, то есть буфер, хранящий простые байты. Если вы просмотрите документацию JDK для класса java.nio.ByteBuffer, то увидите что он достаточно прост: вы создаете его, указывая, сколько места надо выделить под данные. Класс содержит набор методов для получения и помещения данных в виде последовательности байтов или в виде примитивов. Однако возможности записать в него объект или даже простую строку нет. Буфер работает на достаточно низком уровне, поскольку обеспечивается более эффективная совместимость с большинством операционных систем.

Три класса из «старой» библиотеки ввода/вывода были изменены так, чтобы они позволяли получить канал FileChannel: это FileInputStream, FileOutputStream и RandomAccessFile. Заметьте, что эти классы манипулируют байтами, что согласуется с низкоуровневой направленностью nio. Классы для символьных данных Reader и Writer не образуют каналов, однако вспомогательный класс java.nio.channels.Channels имеет набор методов, позволяющих получить объекты Reader и Writer для каналов.

Простой пример использования всех трех типов потоков. Создаваемые каналы поддерживают запись, чтение/запись и только чтение:

//: io/GetChannel.java
// Получение каналов из потоков
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
 
public class GetChannel {
private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
// Write a file:
FileChannel fc =
new FileOutputStream("data.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text ".getBytes()));
fc.close();
// Add to the end of the file:
fc =
new RandomAccessFile("data.txt", "rw").getChannel();
fc.position(fc.size()); // Move to the end
fc.write(ByteBuffer.wrap("Some more".getBytes()));
fc.close();
// Read the file:
fc = new FileInputStream("data.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
while(buff.hasRemaining())
System.out.print((char)buff.get());
}
}

<spoiler text="Output:">

Some text Some more

</spoiler>
Для любого из рассмотренных выше классов потоков метод getChannel() выдает канал FileChannel. Канал довольно прост: ему передается байтовый буфер ByteBuffer для чтения и записи, и вы можете заблокировать некоторые участки файла для монопольного доступа (этот процесс будет описан чуть позже).

Для помещения байтов в буфер ByteBuffer используется один из нескольких методов для записи данных (put); данные записываются в виде одного или нескольких байтов или значений примитивов. Впрочем, как было показано в примере, можно «заворачивать» уже существующий байтовый массив в буфер ByteBuffer, используя метод wrap(). Когда вы так делаете, байтовый массив не копируется, а используется как хранилище для полученного буфера ByteBuffer. В таких случаях говорят, что буфер ByteBuffer создается на базе массива.

Файл data.txt заново открывается с помощью класса RandomAccessFile. Заметьте, что канал FileChannel может перемещаться внутри файла; в нашем примере он сдвигается в конец файла так, чтобы дополнительные записи присоединялись за существующим содержимым.

Чтобы доступ к файлу ограничивался только чтением, следует явно получить байтовый буфер ByteBuffer статическим методом allocate(). Предназначение nio — быстрое перемещение большого количества данных, поэтому размер буфера имеет значение: на самом деле установленный в примере размер в 1 килобайт меньше, чем обычно требуется (поэкспериментируйте с работающим приложением, чтобы найти оптимальное решение).

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

После вызова метода read() буфера FileChannel для сохранения байтов в буфере ByteBuffer также необходимо вызвать для буфера метод flip(), позволяющий впоследствии извлечь из буфера его данные (да, все это выглядит немного неудобно, но помните, что расчет делался на высокое быстродействие, поэтому все делается на низком уровне). И если затем нам снова понадобится буфер для чтения, придется вызывать перед каждым методом read() метод clear(). В этом нетрудно убедиться на примере простой программы копирования файлов:

//: io/ChannelCopy.java
// Копирование файла с использованием каналов и буферов
// {Параметры Channel Copy java test txt}
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
 
public class ChannelCopy {
private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
if(args.length != 2) {
System.out.println("arguments: sourcefile destfile");
System.exit(1);
}
FileChannel
in = new FileInputStream(args[0]).getChannel(),
out = new FileOutputStream(args[1]).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
while(in.read(buffer) != -1) {
buffer.flip(); // Prepare for writing
out.write(buffer);
buffer.clear(); // Prepare for reading
}
}
}

В программе создаются два канала FileChannel: для чтения и для записи. Выделяется буфер ByteBuffer, а когда метод FileChannel.read() возвращает -1, это значит, что мы достигли конца входных данных (без сомнения, пережиток UNIX и С). После каждого вызова метода read(), помещающего данные в буфер, метод flip() подготавливает буфер так, чтобы информация из него могла быть извлечена методом write(). После вызова write() информация все еще хранится в буфере, поэтому метод clear() перемещает все его внутренние указатели, чтобы буфер снова был способен принимать данные в методе read(). Впрочем, рассмотренная программа не лучшим образом выполняет копирование файлов. Специальные методы, transferTo() и transferFrom(), позволяют напрямую присоединить один канал к другому:

//: io/TransferTo.java
// Использование метода transferToO для соединения каналов
// {Параметры TransferTo java TransferTo txt}
import java.nio.channels.*;
import java.io.*;
 
public class TransferTo {
public static void main(String[] args) throws Exception {
if(args.length != 2) {
System.out.println("arguments: sourcefile destfile");
System.exit(1);
}
FileChannel
in = new FileInputStream(args[0]).getChannel(),
out = new FileOutputStream(args[1]).getChannel();
in.transferTo(0, in.size(), out);
// Or:
// out.transferFrom(in, 0, in.size());
}
}

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

Преобразование данных

Если вы вспомните программу GetChannel.java, то увидите, что для вывода информации из файла нам приходилось считывать из буфера по одному байту и преобразовывать его от типа byte к типу char. Такой подход явно примитивен — если вы посмотрите на класс java.nio.CharBuffer, то увидите, что в нем есть метод toString(), который возвращает строку из символов, находящихся в данном буфере. Байтовый буфер ByteBuffer можно рассматривать как символьный буфер CharBuffer, как это делается в методе asCharBuffer(), почему бы так и не поступить? Как вы увидите уже из первого предложения expect(), это не сработает:

//: io/BufferToText.java
// Получение текста из буфера ByteBuffers и обратно
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.io.*;
 
public class BufferToText {
private static final int BSIZE = 1024;
public static void main(String[] args) throws Exception {
FileChannel fc =
new FileOutputStream("data2.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text".getBytes()));
fc.close();
fc = new FileInputStream("data2.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
// Doesn't work:
System.out.println(buff.asCharBuffer());
// Decode using this system's default Charset:
buff.rewind();
String encoding = System.getProperty("file.encoding");
System.out.println("Decoded using " + encoding + ": "
+ Charset.forName(encoding).decode(buff));
// Or, we could encode with something that will print:
fc = new FileOutputStream("data2.txt").getChannel();
fc.write(ByteBuffer.wrap(
"Some text".getBytes("UTF-16BE")));
fc.close();
// Now try reading again:
fc = new FileInputStream("data2.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
// Use a CharBuffer to write through:
fc = new FileOutputStream("data2.txt").getChannel();
buff = ByteBuffer.allocate(24); // More than needed
buff.asCharBuffer().put("Some text");
fc.write(buff);
fc.close();
// Read and display:
fc = new FileInputStream("data2.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
}
}

<spoiler text="Output:">

????
Decoded using Cp1252: Some text
Some text
Some text

</spoiler>
Буфер содержит обычные байты, следовательно, для превращения их в символы мы должны либо кодировать их по мере помещения в буфер, либо декодировать их при извлечении из буфера. Это можно сделать с помощью класса java.nio.charset.Charset, который предоставляет инструменты для преобразования многих различных типов в наборы символов:

//: io/AvailableCharSets.java
// Перечисление кодировок и их символических имен
import java.nio.charset.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class AvailableCharSets {
public static void main(String[] args) {
SortedMap<String,Charset> charSets =
Charset.availableCharsets();
Iterator<String> it = charSets.keySet().iterator();
while(it.hasNext()) {
String csName = it.next();
printnb(csName);
Iterator aliases =
charSets.get(csName).aliases().iterator();
if(aliases.hasNext())
printnb(": ");
while(aliases.hasNext()) {
printnb(aliases.next());
if(aliases.hasNext())
printnb(", ");
}
print();
}
}
}

<spoiler text="Output:">

Big5: csBig5
Big5-HKSCS: big5-hkscs, big5hk, big5-hkscs:unicode3.0, big5hkscs, Big5_HKSCS
EUC-JP: eucjis, x-eucjp, csEUCPkdFmtjapanese, eucjp,
Extended_UNIX_Code_Packed_Format_for_Japanese, x-euc-jp, euc_jp
EUC-KR: ksc5601, 5601, ksc5601_1987, ksc_5601, ksc5601-1987, euc_kr,
ks_c_5601-1987, euckr, csEUCKR
GB18030: gb18030-2000
GB2312: gb2312-1980, gb2312, EUC_CN, gb2312-80, euc-cn, euccn, x-EUC-CN
GBK: windows-936, CP936

</spoiler>
Вернемся к программе BufferToText.java. Если вы вызовете для буфера метод rewind() (чтобы вернуться к его началу), а затем используете кодировку по умолчанию в методе decode(), данные буфера CharBuffer будут правильно выведены на консоль. Чтобы узнать кодировку по умолчанию вызовите метод System.getProperty("fiLe.encoding"), который возвращает строку с названием кодировки. Передавая эту строку методу Charset.forName(), вы получите объект Charset, с помощью которого и декодируете строку.

Другой подход — кодировать данные методом encode() так, чтобы при чтении файла выводились данные, пригодные для вывода на печать (пример представлен в программе BufferToText.java). Здесь для записи текста в файл используется кодировка UTF-16BE, и при последующем чтении вам остается лишь преобразовать данные в буфер CharBuffer и вывести его содержимое. Наконец, мы видим, что происходит, когда вы записываете в буфер ByteBuffer через CharBuffer (мы узнаем об этом чуть позже). Заметьте, что для байтового буфера выделяется 24 байта. На каждый символ (char) отводится два байта, соответственно, буфер вместит 12 символов, а у нас в строке Some Text их только девять. Оставшиеся нулевые байты все равно отображаются в строке, образуемой методом toString() класса CharBuffer, что и показывают результаты.

Извлечение примитивов

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

//: io/GetData.java
//Получение различных данных из буфера ByteBuffer
import java.nio.*;
import static net.mindview.util.Print.*;
 
public class GetData {
private static final int BSIZE = 1024;
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
// Allocation automatically zeroes the ByteBuffer:
int i = 0;
while(i++ < bb.limit())
if(bb.get() != 0)
print("nonzero");
print("i = " + i);
bb.rewind();
// Store and read a char array:
bb.asCharBuffer().put("Howdy!");
char c;
while((c = bb.getChar()) != 0)
printnb(c + " ");
print();
bb.rewind();
// Store and read a short:
bb.asShortBuffer().put((short)471142);
print(bb.getShort());
bb.rewind();
// Store and read an int:
bb.asIntBuffer().put(99471142);
print(bb.getInt());
bb.rewind();
// Store and read a long:
bb.asLongBuffer().put(99471142);
print(bb.getLong());
bb.rewind();
// Store and read a float:
bb.asFloatBuffer().put(99471142);
print(bb.getFloat());
bb.rewind();
// Store and read a double:
bb.asDoubleBuffer().put(99471142);
print(bb.getDouble());
bb.rewind();
}
}

<spoiler text="Output:">

i = 1025
H o w d y !
12390
99471142
99471142
9.9471144E7
9.9471142E7

</spoiler>
После выделения байтового буфера мы убеждаемся в том, что его содержимое действительно заполнено нулями. Проверяются все 1024 значения, хранимые в буфере (вплоть до последнего, индекс которого (размер буфера) возвращается методом limit()), и все они оказываются нулями.

Простейший способ вставить примитив в ByteBuffer основан на получении подходящего «представления» этого буфера методами asCharBuffer(), asShortBuffer() и т. п., и последующем занесении в это представление значения методом put(). В примере мы так поступаем для каждого из простейших типов. Единственным исключением из этого ряда является использование буфера ShortBuffer, требующего приведения типов (которое усекает и изменяет результирующее значение). Все остальные представления не нуждаются в преобразовании типов.

Представления буферов

«Представления буферов» дают вам возможность взглянуть на соответствующий байтовый буфер «через призму» некоторого примитивного типа. Байтовый буфер все так же хранит действительные данные и одновременно поддерживает представление, поэтому все изменения, которые вы сделаете в представлении, отразятся на содержимом байтового буфера. Как было показано в предыдущем' примере, это удобно для вставки значений примитивов в байтовый буфер. Представления также позволяют читать значения примитивов из буфера, по одному (раз он «байтовый» буфер) или пакетами (в массивы). Следующий пример манипулирует целыми числами (int) в буфере ByteBuffer с помощью класса IntBuffer:

//: io/IntBufferDemo.java
// Работа с целыми числами в буфере ByteBuffer
// посредством буфера IntBuffer
import java.nio.*;
 
public class IntBufferDemo {
private static final int BSIZE = 1024;
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
IntBuffer ib = bb.asIntBuffer();
// Store an array of int:
ib.put(new int[]{ 11, 42, 47, 99, 143, 811, 1016 });
// Absolute location read and write:
System.out.println(ib.get(3));
ib.put(3, 1811);
// Setting a new limit before rewinding the buffer.
ib.flip();
while(ib.hasRemaining()) {
int i = ib.get();
System.out.println(i);
}
}
}

<spoiler text="Output:">

99
11
42
47
1811
143
811
1016

</spoiler>
Перегруженный метод put() первый раз вызывается для помещения в буфер массива целых чисел int. Последующие вызовы put() и get() обращаются к конкретному числу int из байтового буфера ByteBuffer. Заметьте, что такие обращения к простейшим типам по абсолютной позиции также можно осуществить напрямую через буфер ByteBuffer.

Как только байтовый буфер ByteBuffer будет заполнен целыми числами или другими примитивами через представление, его можно передать для непосредственной записи в канал. Настолько же просто считать данные из канала и исполь­зовать представление для преобразования данных к конкретному простейшему типу. Вот пример, который трактует одну и ту же последовательность байтов как числа short, int, float, long и double, создавая для одного байтового буфера ByteBuffer различные представления:

//: io/ViewBuffers.java
import java.nio.*;
import static net.mindview.util.Print.*;
 
public class ViewBuffers {
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.wrap(
new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' });
bb.rewind();
printnb("Byte Buffer ");
while(bb.hasRemaining())
printnb(bb.position()+ " -> " + bb.get() + ", ");
print();
CharBuffer cb =
((ByteBuffer)bb.rewind()).asCharBuffer();
printnb("Char Buffer ");
while(cb.hasRemaining())
printnb(cb.position() + " -> " + cb.get() + ", ");
print();
FloatBuffer fb =
((ByteBuffer)bb.rewind()).asFloatBuffer();
printnb("Float Buffer ");
while(fb.hasRemaining())
printnb(fb.position()+ " -> " + fb.get() + ", ");
print();
IntBuffer ib =
((ByteBuffer)bb.rewind()).asIntBuffer();
printnb("Int Buffer ");
while(ib.hasRemaining())
printnb(ib.position()+ " -> " + ib.get() + ", ");
print();
LongBuffer lb =
((ByteBuffer)bb.rewind()).asLongBuffer();
printnb("Long Buffer ");
while(lb.hasRemaining())
printnb(lb.position()+ " -> " + lb.get() + ", ");
print();
ShortBuffer sb =
((ByteBuffer)bb.rewind()).asShortBuffer();
printnb("Short Buffer ");
while(sb.hasRemaining())
printnb(sb.position()+ " -> " + sb.get() + ", ");
print();
DoubleBuffer db =
((ByteBuffer)bb.rewind()).asDoubleBuffer();
printnb("Double Buffer ");
while(db.hasRemaining())
printnb(db.position()+ " -> " + db.get() + ", ");
}
}

<spoiler text="Output:">

Byte Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 0, 4 -> 0, 5 -> 0, 6 -> 0, 7 -> 97,
Char Buffer 0 -> , 1 -> , 2 -> , 3 -> a,
Float Buffer 0 -> 0.0, 1 -> 1.36E-43,
Int Buffer 0 -> 0, 1 -> 97,
Long Buffer 0 -> 97,
Short Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 97,
Double Buffer 0 -> 4.8E-322,

</spoiler>
Байтовый буфер ByteBuffer создается как «обертка» для массива из восьми байтов, который затем и просматривается с помощью представлений для различных простейших типов.

О порядке байтов

Различные компьютеры могут хранить данные с различным порядком следования байтов. Прямой порядок big_endian располагает старший байт по младшему адресу памяти, а для обратного порядка little_endian старший байт помещается по высшему адресу памяти. При хранении значения, занимающего более одного байта, такого как число int, float и т. п., вам, возможно, придется учитывать различные варианты следования байтов в памяти. Буфер ByteBuffer укладывает данные в порядке big_endian, такой же способ всегда используется для данных, пересылаемых по сети. Порядок следования байтов в буфере можно изменить методом order(), передав ему аргумент ByteOrder.BIG_ENDIAN или ByteOrder. LITTLE_ENDIAN.
Рассмотрим двоичное представление байтового буфера, содержащего следующие два байта:

P0519.png

Если прочитать эти данные как тип short (ByteBuffer.asShortBuffer()), то получите число 97 (00000000 01100001), но при другом порядке следования байтов будет получено число 24 832 (01100001 00000000).

Следующий пример показывает, как порядок следования байтов отражается на символах в зависимости от настроек буфера:

//: io/Endians.java
// Endian differences and data storage.
import java.nio.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class Endians {
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.wrap(new byte[12]);
bb.asCharBuffer().put("abcdef");
print(Arrays.toString(bb.array()));
bb.rewind();
bb.order(ByteOrder.BIG_ENDIAN);
bb.asCharBuffer().put("abcdef");
print(Arrays.toString(bb.array()));
bb.rewind();
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.asCharBuffer().put("abcdef");
print(Arrays.toString(bb.array()));
}
}

<spoiler text="Output:">

[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0]

</spoiler>
В буфере ByteBuffer достаточно места для хранения всех байтов символьного массива, поэтому для вывода байтов подходит метод аrrау(). Метод аrrау() является необязательным, и вызывать его следует только для буфера, созданного на базе существующего массива; в противном случае произойдет исключение UnsupportedOperationException. Символьный массив помещается в буфер ByteBuffer посредством представления CharBuffer. При выводе содержащихся в буфере байтов мы видим, что настройка по умолчанию совпадает с режимом big_endian, в то время как атрибут little_endian переставляет байты в обратном порядке.

Буферы и операции с данными

Следующая диаграмма демонстрирует отношения между классами пакета nio; она поможет вам разобраться, как можно перемещать и преобразовывать данные. Например, если вы захотите записать в файл байтовый массив, то сначала вложите его в буфер методом ByteBuffer.wrap(), затем получите из потока FileOutputStream канал методом getChannel(), а потом запишите данные буфера ByteBuffer в полученный канал FileChannel.


P0521b.png

Отметьте, что перемещать данные каналов («из» и «в») допустимо только с помощью байтовых буферов ByteBuffer, а для остальных простейших типов можно либо создать отдельный буфер этого типа, либо получить такой буфер из байтового буфера посредством метода с префиксом as. Таким образом, буфер с примитивными данными нельзя преобразовать к байтовому буферу. Впрочем, вы можете помещать примитивы в байтовый буфер и извлекать их оттуда с помощью представлений, это не такое уж строгое ограничение.

Подробно о буфере

Буфер (Buffer) состоит из данных и четырех индексов, используемых для доступа к данным и эффективного манипулирования ими. К этим индексам относятся метка (mark), позиция (position), предельное значение (limit) и вместимость (capacity). Есть методы, предназначенные для установки и сброса значений этих индексов, также можно узнать их значение (табл. 16.7).

P0522.png

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

//: io/UsingBuffers.java
import java.nio.*;
import static net.mindview.util.Print.*;
 
public class UsingBuffers {
private static void symmetricScramble(CharBuffer buffer){
while(buffer.hasRemaining()) {
buffer.mark();
char c1 = buffer.get();
char c2 = buffer.get();
buffer.reset();
buffer.put(c2).put(c1);
}
}
public static void main(String[] args) {
char[] data = "UsingBuffers".toCharArray();
ByteBuffer bb = ByteBuffer.allocate(data.length * 2);
CharBuffer cb = bb.asCharBuffer();
cb.put(data);
print(cb.rewind());
symmetricScramble(cb);
print(cb.rewind());
symmetricScramble(cb);
print(cb.rewind());
}
}

<spoiler text="Output:">

UsingBuffers
sUniBgfuefsr
UsingBuffers

</spoiler>
Хотя получить буфер CharBuffer можно и напрямую, вызвав для символьного массива метод wrap(), здесь сначала выделяется служащий основой байтовый буфер ByteBuffer, а символьный буфер CharBuffer создается как представление байтового. Это подчеркивает, что в конечном счете все манипуляции производятся с байтовым буфером, поскольку именно он взаимодействует с каналом. На входе в метод symmetricScramble() буфер выглядит следующим образом:

P0523a.png

Позиция (pos) указывает на первый элемент буфера, вместительность (cap) и предельное значение (lim) — на последний. В методе symmetricScramble() цикл while выполняется до тех пор, пока позиция не станет равной предельному значению. Позиция буфера изменяется при вызове для него «относительных» методов put() или get(). Можно также использовать «абсолютные» версии методов put() и get(), которым передается аргумент-индекс, указывающий, с какого места начнет работу метод put() или метод get(). Эти методы не изменяют значение позиции буфера.
Когда управление переходит в цикл while, вызывается метод mark() для установки значения метки (mar). Состояние буфера в этот момент таково:

P0523b.png

Два вызова «относительных» методов get() сохраняют значение первых двух символов в переменных с1 и с2. После этих вызовов буфер выглядит так:


P0524a.png

Для смешивания символов нам нужно записать символ с2 в позицию 0, a c1 в позицию 1. Для этого можно обратиться за .«абсолютной» версией метода put(), но мы приравняем позицию метке, что и делает метод reset():

P0524b.png

Два вызова метода put() записывают с2, а затем c1:

P0524c.png

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

P0524d.png

Процесс продолжается до тех пор, пока не будет просмотрен весь буфер. В конце цикла while позиция находится в конце буфера. При выводе буфера на печать распечатываются только символы, находящиеся между позицией и предельным значением. Поэтому, если вы хотите распечатать буфер целиком, придется установить позицию на начало буфера, используя для этого метод rewind(). Вот в каком состоянии находится буфер после вызова метода rewind() (значение метки стало неопределенным):

P0524e.png

При следующем вызове symmetricScramble() процесс повторяется, и буфер CharBuffer возвращается к своему изначальному состоянию.

Отображаемые в память файлы

Механизм отображения файлов в память позволяет создавать и изменять файлы, размер которых слишком велик для прямого размещения в памяти. В таком случае вы считаете, что файл целиком находится в памяти, и работаете с ним как с очень большим массивом. Такой подход значительно упрощает код изменения файла. Небольшой пример:

//: io/LargeMappedFiles.java
// Создание очень большого файла, отображаемого в память.
// {RunByHand}
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import static net.mindview.util.Print.*;
 
public class LargeMappedFiles {
static int length = 0x8FFFFFF; // 128 MB
public static void main(String[] args) throws Exception {
MappedByteBuffer out =
new RandomAccessFile("test.dat", "rw").getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, length);
for(int i = 0; i < length; i++)
out.put((byte)'x');
print("Finished writing");
for(int i = length/2; i < length/2 + 6; i++)
printnb((char)out.get(i));
}
}

Чтобы одновременно выполнять чтение и запись, мы начинаем с создания объекта RandomAccessFile, получаем для этого файла канал, а затем вызываем метод mар(), чтобы получить буфер MappedByteBuffer, который представляет собой разновидность буфера прямого доступа. Заметьте, что необходимо указать начальную точку и длину участка, который будет проецироваться, то есть у вас есть возможность отображать маленькие участки больших файлов.
Класс MappedByteBuffer унаследован от буфера ByteBuffer, поэтому он содержит все методы последнего. Здесь представлены только простейшие вызовы методов put() и get(), но вы также можете использовать такие возможности, как метод asCharBuffer() и т. п.
Программа напрямую создает файл размером 128 Мбайт; скорее всего, это превышает ограничения вашей операционной системы на размер блока данных, находящегося в памяти. Однако создается впечатление, что весь файл доступен сразу, поскольку только часть его подгружается в память, в то время как остальные части выгружены. Таким образом можно работать с очень большими (размером до 2 Гбайт) файлами. Заметьте, что для достижения максимальной производительности используются низкоуровневые механизмы отображения файлов используемой операционной системы.

Производительность

Хотя быстродействие «старого» ввода/вывода было улучшено за счет переписывания его с учетом новых библиотек nio, техника отображения файлов качественно эффективнее. Следующая программа выполняет простое сравнение производительности:

//: io/MappedIO.java
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
 
public class MappedIO {
private static int numOfInts = 4000000;
private static int numOfUbuffInts = 200000;
private abstract static class Tester {
private String name;
public Tester(String name) { this.name = name; }
public void runTest() {
System.out.print(name + ": ");
try {
long start = System.nanoTime();
test();
double duration = System.nanoTime() - start;
System.out.format("%.2f\n", duration/1.0e9);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public abstract void test() throws IOException;
}
private static Tester[] tests = {
new Tester("Stream Write") {
public void test() throws IOException {
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(new File("temp.tmp"))));
for(int i = 0; i < numOfInts; i++)
dos.writeInt(i);
dos.close();
}
},
new Tester("Mapped Write") {
public void test() throws IOException {
FileChannel fc =
new RandomAccessFile("temp.tmp", "rw")
.getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_WRITE, 0, fc.size())
.asIntBuffer();
for(int i = 0; i < numOfInts; i++)
ib.put(i);
fc.close();
}
},
new Tester("Stream Read") {
public void test() throws IOException {
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("temp.tmp")));
for(int i = 0; i < numOfInts; i++)
dis.readInt();
dis.close();
}
},
new Tester("Mapped Read") {
public void test() throws IOException {
FileChannel fc = new FileInputStream(
new File("temp.tmp")).getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_ONLY, 0, fc.size())
.asIntBuffer();
while(ib.hasRemaining())
ib.get();
fc.close();
}
},
new Tester("Stream Read/Write") {
public void test() throws IOException {
RandomAccessFile raf = new RandomAccessFile(
new File("temp.tmp"), "rw");
raf.writeInt(1);
for(int i = 0; i < numOfUbuffInts; i++) {
raf.seek(raf.length() - 4);
raf.writeInt(raf.readInt());
}
raf.close();
}
},
new Tester("Mapped Read/Write") {
public void test() throws IOException {
FileChannel fc = new RandomAccessFile(
new File("temp.tmp"), "rw").getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_WRITE, 0, fc.size())
.asIntBuffer();
ib.put(0);
for(int i = 1; i < numOfUbuffInts; i++)
ib.put(ib.get(i - 1));
fc.close();
}
}
};
public static void main(String[] args) {
for(Tester test : tests)
test.runTest();
}
}

<spoiler text="Output:"> (90% match)

Stream Write: 0.56
Mapped Write: 0.12
Stream Read: 0.80
Mapped Read: 0.07
Stream Read/Write: 5.32
Mapped Read/Write: 0.02

</spoiler>
Как уже было видно из предыдущих примеров книги, runTest() — не что иное как метод шаблона, предоставляющий тестовую инфраструктуру для различных реализаций метода test(), определенного в безымянных внутренних подклассах. Каждый из этих подклассов выполняет свой вид теста, таким образом, методы test() также являются прототипами для выполнения различных действий, связанных с вводом/выводом.
Хотя кажется, что для отображаемой записи следует использовать поток FileOutputStream, на самом деле любые операции отображаемого вывода должны проходить через класс RandomAccessFile так же, как выполняется чтение/запись в рассмотренном примере.
Отметьте, что в методах test() также учитывается инициализация различных объектов для работы с вводом/выводом, и, несмотря на то что настройка отображаемых файлов может быть затратной, общее преимущество по сравнению с потоковым вводом/выводом все равно получается весьма значительным.

Блокировка файлов

Блокировка файлов позволяет синхронизировать доступ к файлу как к совместно используемому ресурсу. Впрочем, потоки, претендующие на один и тот же файл, могут принадлежать различным виртуальным машинам JVM, или один поток может быть Java-потоком, а другой представлять собой обычный поток операционной системы. Блокированные файлы видны другим процессам операционной системы, поскольку механизм блокировки Java напрямую связан со средствами операционной системы. Вот простой пример блокировки файла:

//: io/FileLocking.java
import java.nio.channels.*;
import java.util.concurrent.*;
import java.io.*;
 
public class FileLocking {
public static void main(String[] args) throws Exception {
FileOutputStream fos= new FileOutputStream("file.txt");
FileLock fl = fos.getChannel().tryLock();
if(fl != null) {
System.out.println("Locked File");
TimeUnit.MILLISECONDS.sleep(100);
fl.release();
System.out.println("Released Lock");
}
fos.close();
}
}

<spoiler text="Output:">

Locked File
Released Lock

</spoiler>
Блокировать файл целиком позволяет объект FileLock, который вы получаете, вызывая метод tryLock() или lock() класса FileChannel. (Сетевые каналы SocketChannel, DatagramChannel и ServerSocketChannel не нуждаются в блокировании, так как они доступны в пределах одного процесса. Вряд ли сокет будет использоваться двумя процессами совместно.) Метод tryLock() не приостанавливает программу. Он пытается овладеть объектом блокировки, но если ему это не удается (если другой процесс уже владеет этим объектом или файл не является разделяемым), то он просто возвращает управление. Метод lock() ждет до тех пор, пока не удастся получить объект блокировки, или поток, в котором этот метод был вызван, не будет прерван, или же пока не будет закрыт канал, для которого был вызван метод lock(). Блокировка снимается методом FileChannel.release().

Также можно заблокировать часть файла вызовом

 tryLock(long position, long size, boolean shared)

или

 lock (long position, long size, boolean shared)

Блокируется участок файла размером size от позиции position. Третий аргумент указывает, будет ли блокировка совместной.
Методы без аргументов приспосабливаются к изменению размеров файла, в то время как методы для блокировки участков не адаптируются к новому размеру файла. Если блокировка была наложена на область от позиции position до position + size, а затем файл увеличился и стал больше размера position + size, то часть файла за пределами position + size не блокируется. Методы без аргументов блокируют файл целиком, даже если он растет.
Поддержка блокировок с эксклюзивным или разделяемым доступом должна быть встроена в операционную систему. Если операционная система не поддерживает разделяемые блокировки и был сделан запрос на получение такой бло­кировки, используется эксклюзивный доступ. Тип блокировки (разделяемая или эксклюзивная) можно узнать при помощи метода FileLock.isShared().

Блокирование части отображаемого файла

Как уже было упомянуто, отображение файлов обычно используется для файлов очень больших размеров. Иногда при работе с таким большим файлом требуется заблокировать некоторые его части, в то время как доступные части будут изменяться другими процессами. В частности, такой подход характерен для баз данных, чтобы несколько пользователей могли работать с базой одновременно.
В следующем примере каждый их двух потоков блокирует свою собственную часть файла:

//: io/LockingMappedFiles.java
// Блокирование части отображаемого файла.
// {RunByHand}
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
 
public class LockingMappedFiles {
static final int LENGTH = 0x8FFFFFF; // 128 MB
static FileChannel fc;
public static void main(String[] args) throws Exception {
fc =
new RandomAccessFile("test.dat", "rw").getChannel();
MappedByteBuffer out =
fc.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH);
for(int i = 0; i < LENGTH; i++)
out.put((byte)'x');
new LockAndModify(out, 0, 0 + LENGTH/3);
new LockAndModify(out, LENGTH/2, LENGTH/2 + LENGTH/4);
}
private static class LockAndModify extends Thread {
private ByteBuffer buff;
private int start, end;
LockAndModify(ByteBuffer mbb, int start, int end) {
this.start = start;
this.end = end;
mbb.limit(end);
mbb.position(start);
buff = mbb.slice();
start();
}
public void run() {
try {
// Exclusive lock with no overlap:
// Монопольная блокировка без перекрытия:
FileLock fl = fc.lock(start, end, false);
System.out.println("Locked: "+ start +" to "+ end);
// Perform modification:
// Модификация:
while(buff.position() < buff.limit() - 1)
buff.put((byte)(buff.get() + 1));
fl.release();
System.out.println("Released: "+start+" to "+ end);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
}

Класс потока LockAndModify устанавливает область буфера и получает его для модификации методом slice(). В методе run() для файлового канала устанавливается блокировка (вы не вправе запросить блокировку для буфера, это позволено только для канала). Вызов lock() напоминает механизм синхронизации доступа потоков к объектам, у вас появляется некая «критическая секция» с монопольным доступом к данной части файла.
Блокировки автоматически снимаются при завершении работы JVM, закрытии канала, для которого они были получены, но можно также явно вызвать метод release() объекта FileLock, что здесь и показано.

Сжатие данных

Библиотека ввода/вывода Java содержит классы, поддерживающие ввод/вывод в сжатом формате (табл. 16.8). Они базируюся на уже существующих потоках ввода/вывода.

Эти классы не являются частью иерархии символьно-ориентированных потоков Reader и Writer, они надстроены над байт-ориентированными классами InputStream и OutputStream, так как библиотека сжатия работает не с символами, а с байтами. Впрочем, никто не запрещает смешивать потоки. (Помните, как легко преобразовать потоки из байтовых в символьные — достаточно использовать классы InputStreamReader и OutputStreamWriter.)

P0531.png

Хотя существует великое количество различных программ сжатия данных, форматы ZIP и GZIP используются, пожалуй, чаще всего. Таким образом, вы можете легко манипулировать своими сжатыми данными с помощью многочисленных программ, предназначенных для чтения и записи этих форматов.

Простое сжатие в формате GZIP

Интерфейс сжатия данных в формате GZIP является наиболее простым и идеально подходит для ситуаций, где имеется один поток данных, который необходимо уплотнить (а не разрозненные фрагменты данных). В следующем примере сжимается файл:

//: io/GZIPcompress.java
// {Args: GZIPcompress.java}
import java.util.zip.*;
import java.io.*;
 
public class GZIPcompress {
public static void main(String[] args)
throws IOException {
if(args.length == 0) {
System.out.println(
"Usage: \nGZIPcompress file\n" +
"\tUses GZIP compression to compress " +
"the file to test.gz");
System.exit(1);
}
BufferedReader in = new BufferedReader(
new FileReader(args[0]));
BufferedOutputStream out = new BufferedOutputStream(
new GZIPOutputStream(
new FileOutputStream("test.gz")));
System.out.println("Writing file");
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
out.close();
System.out.println("Reading file");
BufferedReader in2 = new BufferedReader(
new InputStreamReader(new GZIPInputStream(
new FileInputStream("test.gz"))));
String s;
while((s = in2.readLine()) != null)
System.out.println(s);
}
} /* (Execute to see output) *///:~

Работать с классами сжатия данных очень просто: вы просто надстраиваете их для своего потока данных (GZIPOutputStream или ZipOutputStream для сжатия, GZIPInputStream или ZipInputStream для распаковки данных). Дальнейшее сводится к элементарным операциям ввода/вывода. В примере продемонстрированы смешанные байтовые и символьные потоки: поток in основан на Reader, тогда как конструктор класса GZIPOutputStream использует только потоки на основе OutputStream, но не Writer. Поэтому при открытии файла поток GZIPInputStream преобразуется в символьный поток Reader.

Многофайловые архивы ZIP

Библиотека, поддерживающая формат сжатия данных ZIP, обладает гораздо более широкими возможностями. С ее помощью можно легко упаковывать произвольное количество файлов, а для чтения файлов в формате ZIP даже определен отдельный класс. В библиотеке поддержан стандартный ZIP-формат, поэтому сжатые ею данные будут восприниматься практически любым упаковщиком. Структура следующего примера совпадает со структурой предыдущего, но количество файлов, указываемых в командной строке, не ограничено. Вдобавок демонстрируется применение класса Checksum для получения и проверки контрольной суммы. Таких типов контрольных сумм в Java два: один представлен классом Adler32(этот алгоритм быстрее), а другой — классом CRC32 (медленнее, но точнее).

//: io/ZipCompress.java
// Использование формата ZIP для сжатия любого
// количества файлов, указанных в командной строке.
// {Параметры. ZipCompress java}
import java.util.zip.*;
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class ZipCompress {
public static void main(String[] args)
throws IOException {
FileOutputStream f = new FileOutputStream("test.zip");
CheckedOutputStream csum =
new CheckedOutputStream(f, new Adler32());
ZipOutputStream zos = new ZipOutputStream(csum);
BufferedOutputStream out =
new BufferedOutputStream(zos);
zos.setComment("A test of Java Zipping");
// No corresponding getComment(), though.
for(String arg : args) {
print("Writing file " + arg);
BufferedReader in =
new BufferedReader(new FileReader(arg));
zos.putNextEntry(new ZipEntry(arg));
int c;
while((c = in.read()) != -1)
out.write(c);
in.close();
out.flush();
}
out.close();
// Checksum valid only after the file has been closed!
print("Checksum: " + csum.getChecksum().getValue());
// Now extract the files:
print("Reading file");
FileInputStream fi = new FileInputStream("test.zip");
CheckedInputStream csumi =
new CheckedInputStream(fi, new Adler32());
ZipInputStream in2 = new ZipInputStream(csumi);
BufferedInputStream bis = new BufferedInputStream(in2);
ZipEntry ze;
while((ze = in2.getNextEntry()) != null) {
print("Reading file " + ze);
int x;
while((x = bis.read()) != -1)
System.out.write(x);
}
if(args.length == 1)
print("Checksum: " + csumi.getChecksum().getValue());
bis.close();
// Alternative way to open and read Zip files:
ZipFile zf = new ZipFile("test.zip");
Enumeration e = zf.entries();
while(e.hasMoreElements()) {
ZipEntry ze2 = (ZipEntry)e.nextElement();
print("File: " + ze2);
// ... and extract the data as before
}
/* if(args.length == 1) */
}
} /* (Execute to see output) *///:~

Для каждого файла, добавляемого в архив, необходимо вызвать метод putNextEntry() с соответствующим объектом ZipEntry. Класс ZipEntry содержит все необходимое для добавления к отдельной записи ZIP-файла дополнительной информации: имени файла, размера в сжатом и обычном виде, контрольной суммы CRC, дополнительных данных, комментариев, метода сжатия, признака каталога. В исходном формате ZIP также можно задать пароли, но библиотека Java не поддерживает эту возможность. Аналогичное ограничение встречается и при использовании контрольных сумм: потоки CheckedInputStream и CheckedOutputStream поддерживают оба вида контрольных сумм — и Adler32, и CRC32, однако в классе ZipEntry поддерживается только CRC. Это ограничение вынужденное, поскольку продиктовано требованиями формата ZIP, однако при этом быстрая контрольная сумма Adler32 оказывается в неравных условиях с CRC.

Для извлечения файлов в классе ZipInputStream предусмотрен метод getNextEntry(), который возвращает очередной элемент архива ZipEntry. Для получения более компактной записи можно использовать для архива объект ZipFile, чей ме­тод entries() возвращает итератор Enumeration, с помощью которого можно перемещаться по доступным элементам архивного файла.

Чтобы Иметь доступ к контрольной сумме, необходимо каким-либо образом хранить представляющий ее объект Checksum. В нашем случае сохраняются ссылки на потоки CheckedInputStream и CheckedOutputStream, хотя можно было бы просто сохранить ссылки на объекты Checksum.

Неясно, зачем в библиотеку сжатия ZIP был добавлен метод setComment(), вставляющий в архивный файл комментарий. Как указано в примере, добавить комментарий при получении архивного файла можно, но восстановить его при чтении архива потоком ZipInputStream нельзя. Полноценные комментарии поддерживаются только для отдельных вхождений ZIP-архива, объектов ZipEntry.

Конечно, уплотняемые данные не ограничены файлами; пользуясь библиотеками ZIP и GZIP, вы можете сжимать все, что угодно, даже данные сетевых потоков.

Архивы Java ARchives (файлы JAR)

Формат ZIP также применяется в файлах JAR (архивы Java ARchive), предназначенных для упаковки группы файлов в один сжатый файл. Как и все в языке Java, файлы JAR являются кросс-платформенными, поэтому не нужно заботиться о совместимости платформ. Наравне с файлами классов, в них могут содержаться также любые файлы — например, графические и мультимедийные.

Файлы JAR особенно полезны при работе с Интернетом. До их появления веб-браузерам приходилось выдавать отдельный запрос к серверу для каждого файла, необходимого для запуска апплета. Вдобавок все эти файлы не сжимались. Объединение всех нужных для запуска апплета файлов в одном сжатом файле сокращает время запроса к серверу, при этом уменьшается и загрузка сервера. Кроме того, каждый элемент архива JAR можно снабдить цифровой подписью.

Файл JAR представляет собой файл, в котором хранится набор сжатых файлов вместе с манифестом (manifest), который их описывает. (Вы можете создать манифест самостоятельно или же поручить эту работу программе jar.) За подробной информацией о манифестах JAR обращайтесь к документации JDK.

Инструмент jar, который поставляется вместе с пакетом разработки программ JDK, автоматически сжимает файлы по вашему выбору. Запускается эта программа из командной строки:

jar [параметры] место_назначения [манифест] список_файлов

Параметры запуска — просто набор букв (дефисы или другие служебные символы не нужны). Пользователи систем UNIX/Linux сразу заметят сходство с программой tar. Допустимы следующие параметры:

с    - Создание нового или пустого архива
t - Вывод содержимого архива
х - Извлечение всех файлов
х - файл Извлечение файла с заданным именем
f - Признак имени файла. Если не использовать этот параметр, jar решит, что входные
данные поступают из стандартного ввода, или при создании файла выходные данные будут
направляться в стандартный поток вывода
m - Означает, что первый аргумент содержит имя файла, содержащего манифест
v - Выводит краткое описание действий, выполняемых программой jar
о - Сохранение файлов без сжатия (для создания файлов JAR, которые можно указать
в переменной окружения CLASSPATH)
М - Отказ от автоматического создания манифеста

Если в списке файлов имеется каталог, то его содержимое вместе с подкаталогами и всеми файлами автоматически помещается в файл JAR. Информация о пути файлов также сохраняется.

Несколько примеров наиболее распространенных вариантов запуска программы jar:

jar cf myJarFile.jar *.class

Команда создает файл JAR с именем myJarFile.jar, в котором содержатся все файлы классов из текущего каталога, с автоматически созданным манифестом:

jar cmf myJarFile.jar myManifestFile.mf *.class

Почти идентична предыдущей команде, за одним исключением — в полученный файл JAR включается пользовательский манифест из файла myManifestFile.mf:

jar tf myJarFile.jar

Вывод содержимого (списка файлов) архива myJarFile.jar:

jar tvf myJarFile.jar

К предыдущей команде добавлен параметр v для получения более подробной информации о файлах, содержащихся в архиве myJarFile.jar:

jar cvf myApp.jar audio classes image


Предполагается, что audio, classes и image — это каталоги, содержимое которых включается в файл myApp.jar. Благодаря параметру v в процессе сжатия выводится дополнительная информация об упаковываемых файлах.
Инструмент jar не обладает возможностями архиватора zip. Например, он не позволяет добавлять или обновлять файлы в уже существующем архиве JAR. Также нельзя перемещать файлы и удалять их после перемещения. Но при этом созданный файл JAR всегда читается инструментом jar на другой платформе (архиваторы zip о такой совместимости могут только мечтать).

Сериализация объектов

Сериализация (serialization) объектов Java позволяет вам взять любой объект, реализующий интерфейс Serializable, и превратить его в последовательность байтов, из которой затем можно полностью восстановить исходный объект. Сказанное справедливо и для сетевых соединений, а это значит, что механизм сериализации автоматически компенсирует различия между операционными системами. То есть можно создать объект на машине с ОС Windows, превратить его в последовательность байтов, а затем послать их по сети на машину с ОС UNIX, где объект будет корректно воссоздан. Вам не надо думать о различных форматах данных, порядке следования байтов или других деталях.

Сама по себе сериализация объектов интересна потому, что с ее помощью можно осуществить легковесное долговременное хранение (lightweight persistence). Вспомните: это означает, что время жизни объекта определяется не только вре­менем выполнения программы — объект существует и между запусками программы. Можно взять объект и записать его на диск, а после, при другом запуске программы, восстановить его в первоначальном виде и таким образом получить эффект «живучести». Причина использования добавки «легковесное» такова: объект нельзя определить как «постоянный» при помощи некоторого ключевого слова, то есть долговременное хранение напрямую не поддерживается языком (хотя вероятно, такая возможность появится в будущем). Система выполнения не заботится о деталях сериализации — вам приходится собственноручно сериализовывать и восстанавливать объекты вашей программы. Если вам необходим более серьезный механизм сериализации, попробуйте библиотеку Java JDO или инструмент, подобный Hibernate (https://hibernate.sourceforge.net).

Механизм сериализации объектов был добавлен в язык для поддержки двух расширенных возможностей. Удаленный вызов методов Java (RMI) позволяет работать с объектами, находящимися на других компьютерах, точно так же, как и с теми, что существуют на вашей машине. При посылке сообщений удаленным объектам необходимо транспортировать аргументы и возвращаемые значения, а для этого используется сериализация объектов.

Сериализация объектов также необходима визуальным компонентам JavaBean. Информация о состоянии визуальных компонентов обычно изменяется во время разработки. Эту информацию о состоянии необходимо сохранить, а затем, при запуске программы, восстановить; данную задачу решает сериализация объектов.

Сериализовать объект достаточно просто, если он реализует интерфейс Serializable (это интерфейс для самоидентификации, в нем нет ни одного метода). Когда в язык был добавлен механизм сериализации, во многие классы стандартной библиотеки внесли изменения так, чтобы они были готовы к сериализации. К таким классам относятся все классы-оболочки для простейших типов, все классы контейнеров и многие другие. Даже объекты Class, представляющие классы, можно сериализовать.

Чтобы сериализовать объект, требуется создать выходной поток OutputStream, который нужно вложить в объект ObjectOutputStream. По сути, вызов метода writeObject() осуществляет сериализацию объекта, и далее вы пересылаете его в выходной поток данных OutputStream. Для восстановления объекта необходимо надстроить объект ObjectInputStream для входного потока InputStream, а затем вызвать метод readObject(). Как обычно, такой метод возвращает ссылку на обобщенный объект Object, поэтому после вызова метода следует провести нисходящее преобразование для получения объекта нужного типа.

Сериализация объектов проводится достаточно разумно и в отношении ссылок, имеющихся в объекте. Сохраняется не только сам образ объекта, но и все связанные с ним объекты, все объекты в связанных объектах, и т. д. Это часто называют «паутиной объектов», к которой можно присоединить одиночный объект, а также массив ссылок на объекты и объекты-члены. Если бы вы создавали свой собственный механизм сериализации, отслеживание всех присутствующих в объектах ссылок стало бы весьма нелегкой задачей. Однако в Java никаких трудностей со ссылками нет — судя по всему, в этот язык встроен достаточно эффективный алгоритм создания графов объектов. Следующий пример проверяет механизм сериализации: мы создаем цепочку связанных объектов, каждый из которых связан со следующим сегментом цепочки, а также имеет массив ссылок на объекты другого класса с именем Data:

//: io/Worm.java
// Тест сериализации объектов.
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
class Data implements Serializable {
private int n;
public Data(int n) { this.n = n; }
public String toString() { return Integer.toString(n); }
}
 
public class Worm implements Serializable {
private static Random rand = new Random(47);
private Data[] d = {
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10))
};
private Worm next;
private char c;
// Value of i == number of segments
public Worm(int i, char x) {
print("Worm constructor: " + i);
c = x;
if(--i > 0)
next = new Worm(i, (char)(x + 1));
}
public Worm() {
print("Default constructor");
}
public String toString() {
StringBuilder result = new StringBuilder(":");
result.append(c);
result.append("(");
for(Data dat : d)
result.append(dat);
result.append(")");
if(next != null)
result.append(next);
return result.toString();
}
public static void main(String[] args)
throws ClassNotFoundException, IOException {
Worm w = new Worm(6, 'a');
print("w = " + w);
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("worm.out"));
out.writeObject("Worm storage\n");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("worm.out"));
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
print(s + "w2 = " + w2);
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out2 = new ObjectOutputStream(bout);
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
ObjectInputStream in2 = new ObjectInputStream(
new ByteArrayInputStream(bout.toByteArray()));
s = (String)in2.readObject();
Worm w3 = (Worm)in2.readObject();
print(s + "w3 = " + w3);
}
}

<spoiler text="Output:">

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)

</spoiler>
Чтобы пример был интереснее, массив объектов Data в классе Worm инициализируется случайными числами. (Таким образом, нельзя заподозрить компилятор в том, что он использует дополнительную информацию для хранения объектов.) Каждый объект Worm помечается порядковым номером-символом (char), который автоматически генерируется в процессе рекурсивного формирования связанной цепочки объектов Worm. При создании цепочки ее размер указывается в конструкторе класса Worm. Для инициализации ссылки next рекурсивно вызывается конструктор класса Worm, однако с каждым разом размер цепочки уменьшается на единицу. В последнем сегменте цепочки ссылка next остается со значением null, что указывает на конец цепочки.

Все это делалось лишь по одной причине: для создания более или менее сложной структуры, которая не может быть сериализована тривиальным образом. Впрочем, сам акт сериализации проходит проще простого. После создания потока ObjectOutputStream (на основе другого выходного потока), метод writeObject() записывает в него объект. Заметьте, что в поток также записывается строка (String). В этот же поток можно поместить все примитивные типы, используя те же методы, что и в классе DataOutputStream (оба потока реализуют одинаковый интерфейс).

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

Из выходных данных видно, что восстановленный объект в самом деле содержит все ссылки, которые были в исходном объекте.

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

Обнаружение класса

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

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

//: io/Alien.java
// Сериализуемый класс.
import java.io.*;
public class Alien implements Serializable {}

Файл с программой, создающей и сериализующей объект Alien, находится в том же подкаталоге:

//: io/FreezeAlien.java
// Создание файла с данными сериализации.
import java.io.*;
 
public class FreezeAlien {
public static void main(String[] args) throws Exception {
ObjectOutput out = new ObjectOutputStream(
new FileOutputStream("X.file"));
Alien quellek = new Alien();
out.writeObject(quellek);
}
}

Вместо того чтобы перехватывать и обрабатывать исключения, программа идет по простому пути — исключения передаются за пределы main(), поэтому сообщения о них будут выдаваться на консоль.
После того как вы скомпилируете и запустите этот код, в каталоге с12 появится файл с именем X.file. Следующая программа скрыта от чужих глаз в «секретном» подкаталоге xfiles:

//: io/xfiles/ThawAlien.java
// Попытка восстановления сериализованного файла
// без сохранения класса объекта в зтом файле.
// {RunByHand}
import java.io.*;
 
public class ThawAlien {
public static void main(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(new File("..", "X.file")));
Object mystery = in.readObject();
System.out.println(mystery.getClass());
}
}

<spoiler text="Output:">

class Alien

</spoiler>
Даже открыв файл и прочитав из него данные для восстановления объекта mystery, виртуальная машина Java JVM) не сможет найти файл Alien.class; объект Class для объекта Alien будет в не досягаемости (в примере сознательно не рассматривается возможность обнаружения через переменную окружения CLASSPATH). Возникнет исключение ClassNotFoundException.

Управление сериализацией

Как вы могли убедиться, стандартный механизм сериализации достаточно прост в применении. Но что, если у вас возникли особые требования? Возможно, из соображений безопасности вы не хотите сохранять некоторые части вашего объекта, или сериализовать какой-либо объект, содержащийся в главном объекте, не имеет смысла, так как немного погодя его все равно потребуется создать заново.
Вы можете управлять процессом сериализации, реализуя в своем классе интерфейс Externalizable вместо интерфейса Serializable. Этот интерфейс расширяет оригинальный интерфейс Serializable и добавляет в него два метода, writeExternal() и readExternal(), которые автоматически вызываются в процессе сериализации и восстановления объектов, позволяя вам попутно выполнить специфические действия для конкретного объекта.

В следующем примере продемонстрирована простая реализация интерфейса Externalizable. Заметьте также, что классы Blip1 и Blip2 практически одинаковы, если не считать одного малозаметного отличия:

//: io/Blips.java
// Простая реализация интерфейса Externalizable... с проблемами.
import java.io.*;
import static net.mindview.util.Print.*;
 
class Blip1 implements Externalizable {
public Blip1() {
print("Blip1 Constructor");
}
public void writeExternal(ObjectOutput out)
throws IOException {
print("Blip1.writeExternal");
}
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
print("Blip1.readExternal");
}
}
 
class Blip2 implements Externalizable {
Blip2() {
print("Blip2 Constructor");
}
public void writeExternal(ObjectOutput out)
throws IOException {
print("Blip2.writeExternal");
}
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
print("Blip2.readExternal");
}
}
 
public class Blips {
public static void main(String[] args)
throws IOException, ClassNotFoundException {
print("Constructing objects:");
Blip1 b1 = new Blip1();
Blip2 b2 = new Blip2();
ObjectOutputStream o = new ObjectOutputStream(
new FileOutputStream("Blips.out"));
print("Saving objects:");
o.writeObject(b1);
o.writeObject(b2);
o.close();
// Now get them back:
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Blips.out"));
print("Recovering b1:");
b1 = (Blip1)in.readObject();
// OOPS! Throws an exception:
//! print("Recovering b2:");
//! b2 = (Blip2)in.readObject();
}
}

<spoiler text="Output:">

Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal

</spoiler>
Итак, объект BLip2 в программе не восстанавливается — попытка приводит к возникновению исключения. Заметили ли вы различие между классами Blip1 и Вlip2? Конструктор класса Blip1 объявлен открытым (public), в то время как конструктор класса Blip2 таковым не является, и именно это приводит к исключению в процессе восстановления. Попробуйте объявить конструктор класса Blip2 открытым и удалить комментарии //!, и вы увидите, что все работает, как и было запланировано.
При восстановлении объекта b1 вызывается конструктор по умолчанию класса Blip1. Это отличается от восстановления объекта, реализующего интерфейс Serializable, которое проводится на основе данных сериализации, без вызова конструкторов. В случае с объектом Externalizable происходит нормальный процесс конструирования (включая инициализацию в точке определения), и далее вызывается метод readExternal(). Вам следует иметь это в виду при реализации объектов Externalizable — в особенности обратите внимание на то, что вызывается конструктор по умолчанию.
Следующий пример показывает, что надо сделать для полноты операций сохранения и восстановления объекта Externalizable:

//: io/Blip3.java
// Восстановление объекта Externalizable.
import java.io.*;
import static net.mindview.util.Print.*;
 
public class Blip3 implements Externalizable {
private int i;
private String s; // No initialization
public Blip3() {
print("Blip3 Constructor");
// s, i not initialized
}
public Blip3(String x, int a) {
print("Blip3(String x, int a)");
s = x;
i = a;
// s & i initialized only in non-default constructor.
}
public String toString() { return s + i; }
public void writeExternal(ObjectOutput out)
throws IOException {
print("Blip3.writeExternal");
// You must do this:
out.writeObject(s);
out.writeInt(i);
}
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
print("Blip3.readExternal");
// You must do this:
s = (String)in.readObject();
i = in.readInt();
}
public static void main(String[] args)
throws IOException, ClassNotFoundException {
print("Constructing objects:");
Blip3 b3 = new Blip3("A String ", 47);
print(b3);
ObjectOutputStream o = new ObjectOutputStream(
new FileOutputStream("Blip3.out"));
print("Saving object:");
o.writeObject(b3);
o.close();
// Now get it back:
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Blip3.out"));
print("Recovering b3:");
b3 = (Blip3)in.readObject();
print(b3);
}
}

<spoiler text="Output:">

Constructing objects:
Blip3(String x, int a)
A String 47
Saving object:
Blip3.writeExternal
Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47

</spoiler>
Поля s и і инициализируются только во втором конструкторе, но не в конструкторе по умолчанию. Это значит, что, если переменные s и і не будут инициализированы в методе readExternal(), s останется ссылкой null, а і будет равно нулю (так как при создании объекта его память обнуляется). Если вы закомментируете две строки после фраз Необходимо действовать так и запустите программу, то обнаружите, что так оно и будет: в восстановленном объекте ссылка s имеет значение null, а целое і равно нулю.
Если вы наследуете от объекта, реализующего интерфейс Externalizable, то при выполнении сериализации следует вызывать методы базового класса writeExternal() и readExternal(), чтобы правильно сохранить и восстановить свой объект.
Итак, чтобы сериализация выполнялась правильно, нужно не просто записать всю значимую информацию в методе writeExternal() (для объектов Externalizable не существует автоматической записи объектов-членов), но и восстановить ее затем в методе readExternal(). Хотя сначала можно запутаться и подумать, что из-за вызова конструктора по умолчанию все необходимые действия по записи и восстановлению объектов Externalizable происходят сами по себе. Но это не так.

Ключевое слово transient

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

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

Однако работать с объектами Serializable удобнее, поскольку сериализация для них проходит полностью автоматически. Чтобы запретить запись некоторых полей объекта Serializable, воспользуйтесь ключевым словом transient. Фактически оно означает: «Это не нужно ни сохранять, ни восстанавливать — это мое дело».

Для примера возьмем объект Login, который содержит информацию о некотором сеансе входа в систему, с вводом пароля и имени пользователя. Предположим, что после проверки информации ее необходимо сохранить, выборочно, без пароля. Проще всего удовлетворить заявленным требованиям, реализуя интерфейс Serializable и объявляя поле с паролем password как transient. Вот как это будет выглядеть:

//: io/Logon.java
// Ключевое слово "transient".
import java.util.concurrent.*;
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class Logon implements Serializable {
private Date date = new Date();
private String username;
private transient String password;
public Logon(String name, String pwd) {
username = name;
password = pwd;
}
public String toString() {
return "logon info: \n username: " + username +
"\n date: " + date + "\n password: " + password;
}
public static void main(String[] args) throws Exception {
Logon a = new Logon("Hulk", "myLittlePony");
print("logon a = " + a);
ObjectOutputStream o = new ObjectOutputStream(
new FileOutputStream("Logon.out"));
o.writeObject(a);
o.close();
TimeUnit.SECONDS.sleep(1); // Delay
// Now get them back:
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Logon.out"));
print("Recovering object at " + new Date());
a = (Logon)in.readObject();
print("logon a = " + a);
}
}

<spoiler text="Output:"> (Sample)

logon a = logon info:
username: Hulk
date: Sat Nov 19 15:03:26 MST 2005
password: myLittlePony
Recovering object at Sat Nov 19 15:03:28 MST 2005
logon a = logon info:
username: Hulk
date: Sat Nov 19 15:03:26 MST 2005
password: null

</spoiler>
Поля date и username не имеют модификатора transient, поэтому сериализация для них проводится автоматически. Однако поле password описано как transient и поэтому не сохраняется на диске; механизм сериализации его также игнорирует. При восстановлении объекта поле password равно null. Заметьте, что при соединении строки (String) со ссылкой null перегруженным оператором + ссылка null автоматически преобразуется в строку null.
Также видно, что поле date сохраняется на диске и при восстановлении его значение не меняется.
Так как объекты Externalizable по умолчанию не сохраняют полей, ключевое слово transient для них не имеет смысла. Оно применяется только для объектов Serializable.

Альтернатива для Externalizable

Если вас по каким-либо причинам не прельщает реализация интерфейса Externalizable, существует и другой подход. Вы можете реализовать интерфейс Serializable и добавить (заметьте, что я сказал «добавить», а не «переопределить» или «реализовать») методы с именами writeObject() и readObject(). Они автоматически вызываются при сериализации и восстановлении объектов. Иначе говоря, эти два метода заменят собой сериализацию по умолчанию.
Эти методы должны иметь жестко фиксированную сигнатуру:

 private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException

С точки зрения проектирования программы этот подход вообще непонятен. Во-первых, можно подумать, что, раз уж эти методы не являются частью базового класса и не определены в интерфейсе Serializable, у них должен быть свой собственный интерфейс. Но так как методы объявлены закрытыми (private), вызываться они могут лишь членами их собственного класса. Однако члены обычных классов их не вызывают, вместо этого вызов исходит из методов writeObject() и readObject() классов ObjectInputStream и ObjectOutputStream. (Я не стану разражаться долгой тирадой о выборе тех же имен методов, а скажу лишь одно слово: неразумно.) Интересно, каким образом классы ObjectInputStream и ObjectOutputStream обращаются к закрытым членам другого класса? Можно лишь предположить, что это относится к таинству сериализации.

Так или иначе, все методы, описанные в интерфейсе, реализуются как открытые (public), поэтому, если методы writeObject() и readObject() должны быть закрытыми (private), они не могут быть частью какого-либо интерфейса. Однако вам необходимо строго следовать указанной нотации, и результат очень похож на реализацию интерфейса.

Судя по всему, при вызове метода ObjectOutputStream.writeObject() передаваемый ему объект Serializable тщательно анализируется (вне всяких сомнений, с использованием механизма рефлексии) в поисках его собственного метода writeObject(). Если такой метод существует, процесс стандартной сериализации пропускается, и вызывается метод объекта writeObject(). Аналогичные действия происходят и при восстановлении объекта.

Существует и еще одна хитрость. В вашем собственном методе writeObject() можно вызвать используемый в обычной сериализации метод writeObject(), для этого вызывается метод defaultWriteObject(). Аналогично, в методе readObject() можно вызвать метод стандартного восстановления defaultReadObject(). Следующий пример показывает, как производится пользовательское управление хранением и восстановлением объектов Serializable:

//: io/SerialCtl.java
// Управление сериализацией с определением собственных
// методов writeObject() и readObject()
import java.io.*;
 
public class SerialCtl implements Serializable {
private String a;
private transient String b;
public SerialCtl(String aa, String bb) {
a = "Not Transient: " + aa;
b = "Transient: " + bb;
}
public String toString() { return a + "\n" + b; }
private void writeObject(ObjectOutputStream stream)
throws IOException {
stream.defaultWriteObject();
stream.writeObject(b);
}
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
b = (String)stream.readObject();
}
public static void main(String[] args)
throws IOException, ClassNotFoundException {
SerialCtl sc = new SerialCtl("Test1", "Test2");
System.out.println("Before:\n" + sc);
ByteArrayOutputStream buf= new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(buf);
o.writeObject(sc);
// Now get it back:
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(buf.toByteArray()));
SerialCtl sc2 = (SerialCtl)in.readObject();
System.out.println("After:\n" + sc2);
}
}

<spoiler text="Output:">

Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2

</spoiler>
В данном примере одно из строковых полей класса объявлено с ключевым словом transient, чтобы продемонстрировать, что такие поля при вызове метода defaultWriteObject() не сохраняются. Строка сохраняется и восстанавливается программой явно. Поля класса инициализируются в конструкторе, а не в точке определения; это демонстрирует, что они не инициализируются каким-либо автоматическим механизмом в процессе восстановления. Если вы собираетесь использовать встроенный механизм сериализации для записи обычных (He-transient) составляющих объекта, нужно при записи объекта в первую очередь вызвать метод defaultWriteObject(), а при восстановлении объекта — метод defaultReadObject(). Это вообще загадочные методы. Например, если вызвать метод defaultWriteObject() для потока ObjectOutputStream без передачи аргументов, он все же как-то узнает, какой объект надо записать, где находится ссылка на него и как записать все его He-transient составляющие. Мистика.

Сохранение и восстановление transient-объектов выполняется относительно просто. В методе main() создается объект SerialCtl, который затем сериализуется потоком ObjectOutputStream. (При этом для вывода используется буфер, а не файл — для потока ObjectOutputStream это несущественно.) Непосредственно сериализация выполняется в строке

о.writeObject(sc);

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

Долговременное хранение

Было бы замечательно привлечь технологию сериализации, чтобы сохранить состояние вашей программы для его последующего восстановления. Но перед тем как это делать, необходимо ответить на несколько вопросов. Что произойдет при сохранении двух объектов, содержащих ссылку на некоторый общий третий объект? Когда вы восстановите эти объекты, сколько экземпляров третьего объекта появится в программе? А если вы сохраните объекты в отдельных файлах, а затем десериализуете их в разных частях программы? Следующий пример демонстрирует возможные проблемы:

//: io/MyWorld.java
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
class House implements Serializable {}
 
class Animal implements Serializable {
private String name;
private House preferredHouse;
Animal(String nm, House h) {
name = nm;
preferredHouse = h;
}
public String toString() {
return name + "[" + super.toString() +
"], " + preferredHouse + "\n";
}
}
 
public class MyWorld {
public static void main(String[] args)
throws IOException, ClassNotFoundException {
House house = new House();
List<Animal> animals = new ArrayList<Animal>();
animals.add(new Animal("Bosco the dog", house));
animals.add(new Animal("Ralph the hamster", house));
animals.add(new Animal("Molly the cat", house));
print("animals: " + animals);
ByteArrayOutputStream buf1 =
new ByteArrayOutputStream();
ObjectOutputStream o1 = new ObjectOutputStream(buf1);
o1.writeObject(animals);
o1.writeObject(animals); // Write a 2nd set
// Write to a different stream:
ByteArrayOutputStream buf2 =
new ByteArrayOutputStream();
ObjectOutputStream o2 = new ObjectOutputStream(buf2);
o2.writeObject(animals);
// Now get them back:
ObjectInputStream in1 = new ObjectInputStream(
new ByteArrayInputStream(buf1.toByteArray()));
ObjectInputStream in2 = new ObjectInputStream(
new ByteArrayInputStream(buf2.toByteArray()));
List
animals1 = (List)in1.readObject(),
animals2 = (List)in1.readObject(),
animals3 = (List)in2.readObject();
print("animals1: " + animals1);
print("animals2: " + animals2);
print("animals3: " + animals3);
}
}

<spoiler text="Output:"> (Sample)

animals: [Bosco the dog[Animal@addbf1], House@42e816
, Ralph the hamster[Animal@9304b1], House@42e816
, Molly the cat[Animal@190d11], House@42e816
]

</spoiler>
В этом примере стоит обратить внимание на использование механизма сериализации и байтового массива для «глубокого копирования» любого объекта с интерфейсом Serializable. (Глубокое копирование — создание дубликата всего графа объектов, а не просто основного объекта и его ссылок.)

Объекты Animal содержат поля типа House. В методе main() создается список ArrayList с несколькими объектами Animal, его дважды записывают в один поток и еще один раз — в отдельный поток. Когда эти списки восстанавливают и рас­печатывают, получается приведенный ранее результат (объекты при каждом запуске программы будут располагаться в различных областях памяти).

Конечно, нет ничего удивительного в том, что восстановленные объекты и их оригиналы будут иметь разные адреса. Но заметьте тот факт, что адреса в восстановленных объектах animals1 и animals2 совпадают, вплоть до повторения ссылок на объект House, общий для обоих списков. С другой стороны, при восстановлении списка animals3 система не имеет представления о том, что находящиеся в них объекты уже были восстановлены и имеются в программе, поэтому она создает совершенно иное семейство взаимосвязанных объектов.
Если вы будете проводить сериализацию с использованием единого выходного потока, сохраненная сеть объектов гарантированно восстановится в первоначальном виде, без излишних повторений объектов. Конечно, записать объекты можно тогда, когда они еще не приняли окончательного состояния, но это уже на вашей совести — сохраненные объекты останутся в том состоянии, в котором вы их записали (с теми связями, что у них были на момент сериализации).
Если уж необходимо зафиксировать состояние системы, безопаснее всего сделать это в рамках «атомарной» операции. Если вы сохраняете что-то, затем выполняете какие-то действия, снова сохраняете данные и т. д., у вас не получится безопасного хранилища состояния системы. Вместо этого следует поместить все объекты, являющиеся слагаемыми состояния системы в целом, в контейнер и сохранить этот контейнер единой операцией. Затем можно восстановить его вызовом одного метода.

Следующий пример — имитатор воображаемой системы автоматизированного проектирования (CAD), в котором используется такой подход. Вдобавок в нем продемонстрировано сохранение статических (static) нолей — если вы взглянете на документацию JDK, то увидите, что класс Class реализует интерфейс Serializable, поэтому для сохранения статических данных достаточно сохранить объект Class. Это достаточно разумное решение.

//: io/StoreCADState.java
// Сохранение состояния вымышленной системы CAD.
import java.io.*;
import java.util.*;
 
abstract class Shape implements Serializable {
public static final int RED = 1, BLUE = 2, GREEN = 3;
private int xPos, yPos, dimension;
private static Random rand = new Random(47);
private static int counter = 0;
public abstract void setColor(int newColor);
public abstract int getColor();
public Shape(int xVal, int yVal, int dim) {
xPos = xVal;
yPos = yVal;
dimension = dim;
}
public String toString() {
return getClass() +
"color[" + getColor() + "] xPos[" + xPos +
"] yPos[" + yPos + "] dim[" + dimension + "]\n";
}
public static Shape randomFactory() {
int xVal = rand.nextInt(100);
int yVal = rand.nextInt(100);
int dim = rand.nextInt(100);
switch(counter++ % 3) {
default:
case 0: return new Circle(xVal, yVal, dim);
case 1: return new Square(xVal, yVal, dim);
case 2: return new Line(xVal, yVal, dim);
}
}
}
 
class Circle extends Shape {
private static int color = RED;
public Circle(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) { color = newColor; }
public int getColor() { return color; }
}
 
class Square extends Shape {
private static int color;
public Square(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
color = RED;
}
public void setColor(int newColor) { color = newColor; }
public int getColor() { return color; }
}
 
class Line extends Shape {
private static int color = RED;
public static void
serializeStaticState(ObjectOutputStream os)
throws IOException { os.writeInt(color); }
public static void
deserializeStaticState(ObjectInputStream os)
throws IOException { color = os.readInt(); }
public Line(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) { color = newColor; }
public int getColor() { return color; }
}
 
public class StoreCADState {
public static void main(String[] args) throws Exception {
List<Class<? extends Shape>> shapeTypes =
new ArrayList<Class<? extends Shape>>();
// Add references to the class objects:
shapeTypes.add(Circle.class);
shapeTypes.add(Square.class);
shapeTypes.add(Line.class);
List<Shape> shapes = new ArrayList<Shape>();
// Make some shapes:
for(int i = 0; i < 10; i++)
shapes.add(Shape.randomFactory());
// Set all the static colors to GREEN:
for(int i = 0; i < 10; i++)
((Shape)shapes.get(i)).setColor(Shape.GREEN);
// Save the state vector:
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("CADState.out"));
out.writeObject(shapeTypes);
Line.serializeStaticState(out);
out.writeObject(shapes);
// Display the shapes:
System.out.println(shapes);
}
}

<spoiler text="Output:">

[class Circlecolor[3] xPos[58] yPos[55] dim[93]
, class Squarecolor[3] xPos[61] yPos[61] dim[29]
, class Linecolor[3] xPos[68] yPos[0] dim[22]
, class Circlecolor[3] xPos[7] yPos[88] dim[28]
, class Squarecolor[3] xPos[51] yPos[89] dim[9]
, class Linecolor[3] xPos[78] yPos[98] dim[61]
, class Circlecolor[3] xPos[20] yPos[58] dim[16]
, class Squarecolor[3] xPos[40] yPos[11] dim[22]
, class Linecolor[3] xPos[4] yPos[83] dim[6]
, class Circlecolor[3] xPos[75] yPos[10] dim[42]
]

</spoiler>
Класс Shape реализует интерфейс Serializable, поэтому все унаследованные от него классы по определению поддерживают сериализацию и восстановление. В каждой фигуре Shape содержатся некоторые данные, и в каждом унаследованном от Shape классе имеется статическое (static) поле, которое определяет цвет фигуры. (Если бы мы поместили статическое поле в базовый класс, то получили бы одно поле для всех фигур, поскольку статические поля в производных классах не копируются.) Для задания цвета некоторого типа фигур можно переопределить методы базового класса (статические методы не используют динамическое связывание). Метод randomFactory() создает при каждом вызове новую фигуру, используя для этого случайные значения Shape.
Классы Circle и Square — простые подклассы Shape, различающиеся только способом инициализации поля color: окружность (Circle) задает значение этого поля в месте определения, а прямоугольник (Square) инициализирует его в кон­структоре. Класс Line мы обсудим чуть позже.
В методе main() один список ArrayList используется для хранения объектов Class, а другой — для хранения фигур.
Восстановление объектов выполняется вполне тривиально:

//: io/RecoverCADState.java
// Восстановление состояния вымышленной системы CAD.
// {RunFirst: StoreCADState}
import java.io.*;
import java.util.*;
 
public class RecoverCADState {
@SuppressWarnings("unchecked")
public static void main(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("CADState.out"));
// Read in the same order they were written:
List<Class<? extends Shape>> shapeTypes =
(List<Class<? extends Shape>>)in.readObject();
Line.deserializeStaticState(in);
List<Shape> shapes = (List<Shape>)in.readObject();
System.out.println(shapes);
}
}

<spoiler text="Output:">

[class Circlecolor[1] xPos[58] yPos[55] dim[93]
, class Squarecolor[0] xPos[61] yPos[61] dim[29]
, class Linecolor[3] xPos[68] yPos[0] dim[22]
, class Circlecolor[1] xPos[7] yPos[88] dim[28]
, class Squarecolor[0] xPos[51] yPos[89] dim[9]
, class Linecolor[3] xPos[78] yPos[98] dim[61]
, class Circlecolor[1] xPos[20] yPos[58] dim[16]
, class Squarecolor[0] xPos[40] yPos[11] dim[22]
, class Linecolor[3] xPos[4] yPos[83] dim[6]
, class Circlecolor[1] xPos[75] yPos[10] dim[42]
]

</spoiler>
Мы видим, что значения переменных xPos, уPos и dim сохранились и были успешно восстановлены, однако при восстановлении статической информации произошло что-то странное. При записи все статические поля color имели значение 3, но восстановление дало другие результаты. В окружностях значением стала единица (то есть константа RED), а в прямоугольниках поля color вообще равны нулю (помните, в этих объектах инициализация проходит в конструкторе). Похоже, статические поля вообще не сериализовались! Да, это именно так — хотя класс Class и реализует интерфейс Serializable, происходит это не так, как нам хотелось бы. Отсюда, если вам понадобится сохранить статические значения, делайте это самостоятельно.

Именно для этой цели предназначены методы serializeStaticState() и deserializeStaticState() класса Line. Вы можете видеть, как они вызываются в процессе сохранения и восстановления системы. (Заметьте, порядок действий при сохранении информации должен соблюдаться и при ее десериализации.) Поэтому для правильного выполнения этих программ необходимо сделать следующее:


  • Добавьте методы sеrializeStaticState() и deserializeStaticState() во все фигуры Shape.
  • Уберите из программы список shapeTypes и весь связанный с ним код.
  • При сериализации и восстановлении вызывайте новые методы для сохранения статической информации.

Также стоит позаботиться о безопасности, ведь сериализация сохраняет и закрытые (private) поля. Если в вашем объекте имеется конфиденциальная информация, ее необходимо пометить как transient. Но в таком случае придется подумать о безопасном способе хранения такой информации, ведь при восстановлении объекта необходимо восстанавливать все его данные.

Предпочтения

Circlecolor[l] xPos[20] yPos[58] dim[16] Squarecolor[0] xPos[40] yPos[ll] dim[22] 
Linecolor[3] xPos[4] yPos[83] dim[6] Circlecolor[l] xPos[75] yPos[10] dim[42]

В пакете JDK-1.4 появился программный интерфейс АРІ для работы с предпочтениями (preferences). Предпочтения гораздо более тесно связаны с долговременным хранением, чем механизм сериализации объектов, поскольку они позволяют автоматически сохранять и восстанавливать вашу информацию. Однако они применимы лишь к небольшим, ограниченным наборам данных — хранить в них можно только примитивы и строки, и длина строки не должна превышать 8 Кбайт (не так уж мало, но вряд ли подойдет для решения серьезных задач). Как и предполагает название нового API, предпочтения предназначены для хранения и получения информации о предпочтениях пользователя и конфигурации программы.
Предпочтения представляют собой наборы пар «ключ-значение» (как в картах), образующих иерархию узлов. Хотя иерархия узлов и годится для построения сложных структур, чаще всего создают один узел, имя которого совпадает с именем класса, и хранят информацию в нем. Простой пример:

//: io/PreferencesDemo.java
import java.util.prefs.*;
import static net.mindview.util.Print.*;
 
public class PreferencesDemo {
public static void main(String[] args) throws Exception {
Preferences prefs = Preferences
.userNodeForPackage(PreferencesDemo.class);
prefs.put("Location", "Oz");
prefs.put("Footwear", "Ruby Slippers");
prefs.putInt("Companions", 4);
prefs.putBoolean("Are there witches?", true);
int usageCount = prefs.getInt("UsageCount", 0);
usageCount++;
prefs.putInt("UsageCount", usageCount);
for(String key : prefs.keys())
print(key + ": "+ prefs.get(key, null));
// You must always provide a default value:
print("How many companions does Dorothy have? " +
prefs.getInt("Companions", 0));
}
}

<spoiler text="Output:"> (Sample)

Location: Oz
Footwear: Ruby Slippers
Companions: 4
Are there witches?: true
UsageCount: 53
How many companions does Dorothy have? 4

</spoiler>
Здесь используется метод userNodeForPackage(), но с тем же успехом можно было бы заменить его методом systemNodeForPackage(), это дело вкуса. Предполагается, что префикс user используется для хранения индивидуальных пред­почтений пользователя, a system — для хранения информации общего плана о настройках установки. Так как метод main() статический, для идентификации узла применен класс PreferencesDemo.class, хотя в нестатических методах обычно вызывается метод getClass(). Использовать текущий класс для идентификации узла не обязательно, но чаще всего именно так и поступают.

Созданный узел используется для хранения или считывания информации. В данном примере в узел помещаются различные данные, после вызывается метод keys(). Последний возвращает массив строк String[], что может быть непривычно, если вы привыкли использовать метод keys() в коллекциях. Обратите внимание на второй аргумент метода get(). Это значение по умолчанию, которое будет возвращено, если для данного ключа не будет найдено значение. При переборе множества ключей мы знаем, что каждому из них сопоставлено значение, поэтому передача null по умолчанию безопасна, но обычно используется именованный ключ:

 prefs.getInt("Companions ". 0));

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

 int usageCount = prefs.getlntC"UsageCount", 0);
usageCount++;
prefs.putlnt("UsageCount". usageCount);

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

Запустив программу PreferencesDemo.java, вы увидите, что значение usageCount действительно увеличивается при каждом запуске программы, но где же хранятся данные? Никакие локальные файлы после запуска программы не создаются. Система предпочтений привлекает для хранения данных системные ресурсы, а конкретная реализация зависит от операционной системы. Например, в Windows используется реестр (поскольку он и так представляет собой иерархию узлов с набором пар «ключ-значение»). С точки зрения программиста, реализация — это несущественно: информация сохраняется «сама собой», и вам не приходится беспокоиться о том, как это работает на различных системах.

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

Резюме

Библиотека ввода/вывода Java удовлетворяет всем основным потребностям: она позволяет выполнять операции чтения и записи с консолью, файлами, буфером в памяти и даже сетевыми подключениями к Интернету. Наследование позволяет создавать новые типы объектов для ввода и вывода данных. Вы даже можете обеспечить расширяемость для объектов потока, использовав тот факт, что переопределенный метод toString() автоматически вызывается при передаче объекта методу, ожидающему String (ограниченное «автоматическое преобразование типов» Java).

Впрочем, ответы на некоторые вопросы не удается найти ни в документации, ни в архитектуре библиотеки ввода/вывода Java. Например, было бы замечательно, если бы при попытке перезаписи файла возбуждалось исключение — многие программные системы позволяют указать, что файл может быть открыт для вывода только тогда, когда файла с тем же именем еще не существует. В языке Java проверка существования файла возможна лишь с помощью объекта File, поскольку если вы откроете файл как FileOutputStream или FileWriter, старый файл всегда будет стерт.

При знакомстве с библиотекой ввода/вывода возникают смешанные чувства; с одной стороны, она берёт на себя значительный объем работы и к тому же обеспечивает переносимость. Но пока вы не вполне поняли суть работы шаблона декоратора, архитектура библиотеки кажется не совсем понятной, и от вас потребуются определенные усилия для ее изучения и освоения. Кроме того, библиотека не совсем полна: например, только отсутствие необходимых средств заставило меня писать инструменты, подобные TextFile (новый класс Java SE5 PrintWriter — шаг в правильном направлении, но это лишь частичное решение). К числу значительных усовершенствований Java SE5 следует отнести и то, что в нем появились возможности форматирования вывода, присутствующие практически в любом другом языке.

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

]]>
Книги по Java https://linexp.ru?id=4737 Wed, 29 Jun 2022 14:11:17 GMT
<![CDATA[Глава 17 Thinking in Java 4th edition]]> ПАРАЛЛЕЛЬНОЕ ВЫПОЛНЕНИЕДо настоящего момента мы имели дело исключительно с последовательным программированием. Все действия, выполняемые программой, выполнялись друг за другом, то есть последовательно.

Содержание

ПАРАЛЛЕЛЬНОЕ ВЫПОЛНЕНИЕ

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

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

Каждая из этих самостоятельных подзадач называется потоком (thread). Программа пишется так, словно каждый поток запускается сам по себе и использует процессор в монопольном режиме. На самом деле существует некоторый системный механизм, который обеспечивает совместное использованием процессора, но в основном думать об этом вам не придется. Модель потоков (и ее поддержка в языке Java) является программным механизмом, упрощающим одновременное выполнение нескольких операций в одной и той же программе.

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

Задачи

Программный поток представляет некоторую задачу или операцию, поэтому нам понадобятся средства для описания этой задачи. Их предоставляет интерфейс Runnable. Чтобы определить задачу, реализуйте Runnable и напишите метод run(), содержащий код выполнения нужных действий.

Например, задача LiftOff выводит обратный отсчет перед стартом:

//: concurrency/LiftOff.java
// Реализация интерфейса Runnable.
 
public class LiftOff implements Runnable {
protected int countDown = 10; // Значение по умолчанию
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" +
(countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
}

По идентификатору id различаются экземпляры задачи. Поле объявлено с ключевым словом final, поскольку оно не будет изменяться после инициализации.

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

Вызов статического метода Thread.yield() в run() обращен к планировщику потоков (часть потокового механизма Java, обеспечивающая переключение процессора между потоками). Фактически он означает, что очередная важная часть цикла была выполнена и теперь можно на время переключиться на другую задачу. Вызов yield() не обязателен, но в данном примере он обеспечивает более интересные результаты: вы с большей вероятностью увидите, что программный поток прерывает и возобновляет свою работу.

В следующем примере метод run() не выделяется в отдельный программный поток, а просто вызывается напрямую в main() (впрочем, поток все же используется — тот, который всегда создается для main()):

//: concurrency/MainThread.java
public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run();
}
}

<spoiler text="Output:">

#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!),

</spoiler>
Класс, реализующий Runnable, должен содержать метод run(), но ничего особенного в этом методе нет — он не обладает никакими особыми потоковыми возможностями. Чтобы использовать многопоточное выполнение, необходимо явно связать задачу с потоком.

Класс Thread

Традиционный способ преобразования объекта Runnable в задачу заключается в передаче его конструктору Thread. Следующий пример показывает, как организовать выполнение LiftOff с использованием Thread:

//: concurrency/BasicThreads.java
// Простейший вариант использования класса Thread..
public class BasicThreads {
public static void main(String[] args) {
Thread t = new Thread(new LiftOff());
t.start();
System.out.println("Waiting for LiftOff");
}
}

<spoiler text="Output:"> (90% match)

Waiting for LiftOff
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!),

</spoiler>
Конструктору Thread передается только объект Runnable. Метод start() выполняет необходимую инициализацию потока, после чего вызов метода run() интерфейса Runnable запускает задачу на выполнение в новом потоке.

Из выходных данных видно, что вызов start() быстро возвращает управление (сообщение «Waiting for LiftOff» появляется до завершения отсчета). В сущности, мы вызываем LiftOff.run(), а этот метод еще не завершил свое выполнение; но, поскольку LiftOff.run() выполняется в другом потоке, в потоке main() в это время можно выполнять другие операции. (Данная возможность не ограничивается потоком main() — любой поток может запустить другой поток.) Получается, что программа выполняет два метода сразу — main() и LiftOff.run().

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

//: concurrency/MoreBasicThreads.java
// Добавление новых потоков.
public class MoreBasicThreads {
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new Thread(new LiftOff()).start();
System.out.println("Waiting for LiftOff");
}
}

<spoiler text="Output:"> (Sample)

Waiting for LiftOff
#0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7),
#2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5), #2(5), #3(5),
#4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2),
#1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1), #4(1), #0(Liftoff!),
#1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!)

</spoiler>
Из выходных данных видно, что задачи выполняются одновременно друг с другом, с поочередной активизацией и выгрузкой потоков. Переключение осуществляется автоматически планировщиком потоков. Если на компьютере установлено несколько процессоров, планировщик потоков автоматически распределяет потоки между разными процессорами.

При разных запусках программы будут получены разные результаты, поскольку работа планировщика потоков недетерминирована. Более того, вы наверняка увидите значительные различия в результатах работы данной программы-примера, запуская ее на различных версиях пакета JDK. К примеру, предыдущие версии JVM не слишком часто выполняли квантование времени, соответственно, поток 1 мог первым закончить свой цикл, затем все свои итерации произвел бы поток 2, и т. д. Фактически то же самое получилось бы, если бы вызывалась процедура, выполняющая все циклы одновременно, за тем исключением, что запуск совокупности потоков требует больших издержек. Более поздние версии JDK обеспечивают более качественное квантование, и каждый поток регулярно получает свою долю внимания. Как правило, Sun не упоминает о подобных изменениях, так что рассчитывать на определенные «правила поведения» потоков не стоит. Лучше всего при написании кода с потоками занять максимально консервативную позицию.

Когда метод main() создает объекты-потоки Thread, он не сохраняет на них ссылки. Обычный объект, «забытый» таким образом, стал бы легкой добычей сборщика мусора, но только не объект-поток Thread. Каждый поток (Thread) са­мостоятельно «регистрирует» себя, то есть на самом деле ссылка на него где-то существует, и сборщик мусора не вправе удалить его объект.

Исполнители

Исполнители (executors), появившиеся в библиотеке java.util.concurrent в Java SE5, упрощают многозадачное программирование за счет автоматизации управления объектами Thread. Они создают дополнительную логическую прослойку между клиентом и выполнением задачи; задача выполняется не напрямую клиентом, а промежуточным объектом. Исполнители позволяют управлять выполнением асинхронных задач без явного управления жизненным циклом потоков. Именно такой способ запуска задач рекомендуется использовать в Java SE5/6.

Вместо явного создания объектов Thread в MoreBasicThreads.java мы можем воспользоваться исполнителем. Объект LiftOff умеет выполнять определенную операцию и предоставляет единственный метод для выполнения. Объект ExecutorService умеет создавать необходимый контекст для выполнения объектов Runnable. В следующем примере класс CachedThreadPool создает один поток для каждой задачи. Обратите внимание: объект ExecutorService создается статическим методом класса Executors, определяющим разновидность исполнителя:

//: concurrency/CachedThreadPool.java
import java.util.concurrent.*;
 
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}

<spoiler text="Output:"> (Sample)

#0(9), #0(8), #1(9), #2(9), #3(9), #4(9), #0(7), #1(8), #2(8), #3(8), #4(8),
#0(6), #1(7), #2(7), #3(7), #4(7), #0(5), #1(6), #2(6), #3(6), #4(6), #0(4),
#1(5), #2(5), #3(5), #4(5), #0(3), #1(4), #2(4), #3(4), #4(4), #0(2), #1(3),
#2(3), #3(3), #4(3), #0(1), #1(2), #2(2), #3(2), #4(2), #0(Liftoff!), #1(1),
#2(1), #3(1), #4(1), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),

</spoiler>
Очень часто для создания и управления всеми задачами в системе достаточно одного исполнителя.

Вызов shutdown() предотвращает передачу Executor новых задач. Текущий поток (в данном случае тот, в котором выполняется main()) продолжает выполняться со всеми задачами, переданными до вызова shutdown(). Работа программы прекращается после завершения всех задач в Executor.

CachedThreadPool в этом примере легко заменяется другим типом Executor. Например, в потоковом пуле фиксированного размера (FixedThreadPool) используется ограниченный набор потоков для выполнения переданных задач:

//: concurrency/FixedThreadPool.java
import java.util.concurrent.*;
 
public class FixedThreadPool {
public static void main(String[] args) {
// В аргументе конструктора передается количество потоков:
ExecutorService exec = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}

<spoiler text="Output:"> (Sample)

#0(9), #0(8), #1(9), #2(9), #3(9), #4(9), #0(7), #1(8), #2(8), #3(8), #4(8),
#0(6), #1(7), #2(7), #3(7), #4(7), #0(5), #1(6), #2(6), #3(6), #4(6), #0(4),
#1(5), #2(5), #3(5), #4(5), #0(3), #1(4), #2(4), #3(4), #4(4), #0(2), #1(3),
#2(3), #3(3), #4(3), #0(1), #1(2), #2(2), #3(2), #4(2), #0(Liftoff!), #1(1),
#2(1), #3(1), #4(1), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),

</spoiler>
С FixedThreadPool дорогостоящая операция создания потоков выполняется только один раз, в самом начале, поэтому количество потоков остается фиксированным. Это способствует экономии времени, поскольку вам не приходится нести затраты, связанные с созданием потока, для каждой отдельной задачи. В системах, управляемых событиями, обеспечивается максимальная скорость выполнения обработчиков событий, так как они могут просто получить поток из пула. Перерасход ресурсов в такой схеме исключен, так как FixedThreadPool использует ограниченное количество объектов Thread.

Исполнитель SingleThreadExecutor представляет собой аналог FixedThreadPool с одним потоком. Например, он может быть полезен для выполнения долгосрочных операций (скажем, прослушивания входящих подключений по сокету) и для коротких операций, выполняемых в отдельных потоках, — скажем, обновления локальных или удаленных журналов.

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

//: concurrency/SingleThreadExecutor.java
import java.util.concurrent.*;
 
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}

<spoiler text="Output:">

#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!),
#1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!),
#2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!),
#3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!),
#4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),

</spoiler>
Другой пример: допустим, имеется группа потоков, выполняющих операции с использованием файловой системы. Вы можете запустить эти задачи под управлением SingleThreadExecutor, чтобы в любой момент гарантированно вы­полнялось не более одной задачи. При таком подходе вам не придется возиться с синхронизацией доступа к общим ресурсам (без риска для целостности файловой системы). Возможно, в окончательной версии кода будет правильнее синхронизировать доступ к ресурсу (см. далее в этой главе), но SingleThreadExecutor позволит быстро организовать координацию доступа при построении рабочего прототипа.

Возврат значений из задач

Интерфейс Runnable представляет отдельную задачу, которая выполняет некоторую работу, но не возвращает значения. Если вы хотите, чтобы задача возвращала значение, реализуйте интерфейс Callable вместо интерфейса Runnable. Пара­метризованный интерфейс Callable, появившийся в Java SE5, имеет параметр типа, представляющий возвращаемое значение метода call() (вместо run()), а для его вызова должен использоваться метод ExecutorService.submit(). Простой пример:

//: concurrency/CallableDemo.java
import java.util.concurrent.*;
import java.util.*;
 
class TaskWithResult implements Callable<String> {
private int id;
public TaskWithResult(int id) {
this.id = id;
}
public String call() {
return "result of TaskWithResult " + id;
}
}
 
public class CallableDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> results =
new ArrayList<Future<String>>();
for(int i = 0; i < 10; i++)
results.add(exec.submit(new TaskWithResult(i)));
for(Future<String> fs : results)
try {
// Вызов get() блокируется до завершения;:
System.out.println(fs.get());
} catch(InterruptedException e) {
System.out.println(e);
return;
} catch(ExecutionException e) {
System.out.println(e);
} finally {
exec.shutdown();
}
}
}

<spoiler text="Output:">

result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
result of TaskWithResult 8
result of TaskWithResult 9

</spoiler>
Метод submit() создает объект Future, параметризованный по типу результата, возвращаемому Callable. Вы можете обратиться к Future с запросом isDone(), чтобы узнать, завершена ли операция. После завершения задачи и появления ре­зультата производится его выборка методом get(). Если get() вызывается без предварительной проверки isDone(), вызов блокируется до появления результата. Также можно вызвать get() с интервалом тайм-аута.

Перегруженный метод Executors.callable() получает Runnable и выдает Callable. ExecutorService содержит методы для выполнения коллекций объектов Callable.

Ожидание

Другим способом управления вашими потоками является вызов метода sleep(), который переводит поток в состояние ожидания на заданное количество миллисекунд. Если в классе LiftOff заменить вызов yield() на вызов метода sleep(), будет получен следующий результат:

//: concurrency/SleepingTask.java
// Вызов sleep() для приостановки потока.
import java.util.concurrent.*;
 
public class SleepingTask extends LiftOff {
public void run() {
try {
while(countDown-- > 0) {
System.out.print(status());
// Старый стиль.
// Thread.sleep(l00);
// Стиль Java SE5/6:
TimeUnit.MILLISECONDS.sleep(100);
}
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new SleepingTask());
exec.shutdown();
}
}

<spoiler text="Output:">

#0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7),
#1(7), #2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5),
#2(5), #3(5), #4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3),
#3(3), #4(3), #0(2), #1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1),
#4(1), #0(Liftoff!), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),

</spoiler>
Вызов метода sleep() может привести к исключению InterruptedException; перехват этого исключения продемонстрирован в run(). Поскольку исключения не распространяются по потокам обратно в main(), вы должны локально обработать любые исключения, возникающие внутри задачи.

В Java SE5 появилась новая версия sleep(), оформленная в виде метода класса TimeUnit; она продемонстрирована в приведенном примере. Она делает программу более наглядной, поскольку вы можете указать единицы измерения продолжительности задержки. Класс TimeUnit также может использоваться для выполнения преобразований, как будет показано далее в этой главе.

На некоторых платформах задачи выполняются в порядке «идеального распределения» — от 0 до 4, затем снова от 4 до 0. Это вполне логично, поскольку после каждой команды вывода задача переходит в состояние ожидания, что позволяет планировщику потоков переключиться на другой поток. Тем не менее такое поведение зависит от базовой реализации потокового механизма, поэтому полагаться на него нельзя. Если вам потребуется управлять порядком выполнения задач, используйте средства синхронизации (см. далее) или же вообще откажитесь от использования потоков и напишите собственные функции синхронизации, которые передают управление друг другу в нужном порядке.

Приоритет

Приоритет (priority) потока сообщает планировщику информацию об относительной важности потока. Хотя порядок обращения процессора к существующему набору потоков и не детерминирован, если существует несколько приос­тановленных потоков, одновременно ожидающих запуска, планировщик сначала запустит поток с большим приоритетом. Впрочем, это не значит, что потоки с младшими приоритетами не выполняются вовсе (то есть тупиковых ситуаций из-за приоритетов не возникает). Потоки с более низкими приоритетами просто запускаются чуть реже.

В подавляющем большинстве случаев все потоки должны выполняться со стандартным приоритетом. Любые попытки манипуляций с приоритетами обычно являются ошибкой.

Следующий пример демонстрирует использование приоритетов. Приоритет существующего потока читается методом getPriority() и задается методом setPriority():

//: concurrency/SimplePriorities.java
// Использование приоритетов потоков.
import java.util.concurrent.*;
 
public class SimplePriorities implements Runnable {
private int countDown = 5;
private volatile double d; // Без оптимизации
private int priority;
public SimplePriorities(int priority) {
this.priority = priority;
}
public String toString() {
return Thread.currentThread() + ": " + countDown;
}
public void run() {
Thread.currentThread().setPriority(priority);
while(true) {
// Высокозатратная, прерываемая операция:
for(int i = 1; i < 100000; i++) {
d += (Math.PI + Math.E) / (double)i;
if(i % 1000 == 0)
Thread.yield();
}
System.out.println(this);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(
new SimplePriorities(Thread.MIN_PRIORITY));
exec.execute(
new SimplePriorities(Thread.MAX_PRIORITY));
exec.shutdown();
}
}

<spoiler text="Output:"> (70% match)

Thread[pool-1-thread-6,10,main]: 5
Thread[pool-1-thread-6,10,main]: 4
Thread[pool-1-thread-6,10,main]: 3
Thread[pool-1-thread-6,10,main]: 2
Thread[pool-1-thread-6,10,main]: 1
Thread[pool-1-thread-3,1,main]: 5
Thread[pool-1-thread-2,1,main]: 5
Thread[pool-1-thread-1,1,main]: 5
Thread[pool-1-thread-5,1,main]: 5
Thread[pool-1-thread-4,1,main]: 5

</spoiler>
В этой версии метод toString() переопределяется и использует метод Thread. toString(), который выводит имя потока (его можно задать в конструкторе, но здесь имена автоматически генерируются в виде pool-1-thread-1, pool-1-thread-2 и т. д.), приоритет и группу, к которой принадлежит поток. Переопределенная версия toString() также выводит обратный отсчет, выполняемый задачей. Обратите внимание: для получения ссылки на объект Thread, управляющий задачей, внутри самой задачи, следует вызвать метод Thread.currentThread().

Мы видим, что приоритет последнего потока имеет наивысший уровень, а все остальные потоки находятся на низшем уровне. Учтите, что приоритет задается в начале выполнения run(); задавать его в конструкторе бессмысленно, потому что Executor в этот момент еще не начал выполнять задачу.

В метод run() были добавлены 100 000 достаточно затратных операций с плавающей запятой, включая суммирование и деление с числом двойной точности double. Переменная d была отмечена как volatile, чтобы компилятор не применял оптимизацию. Без этих вычислений вы не увидите эффекта установки различных приоритетов (попробуйте закомментировать цикл for с вычислениями). В процессе вычислений мы видим, что планировщик уделяет больше внимания потоку с приоритетом MAX_PRIORITY (по крайней мере, таково было поведение программы на машине под управлением Windows ХР). Несмотря даже на то, что вывод на консоль также является «дорогостоящей» операцией, с ним вы не увидите влияние уровней приоритетов, поскольку вывод на консоль не прерывается (иначе экран был бы заполнен несуразицей), в то время как математические вычисления, приведенные выше, прерывать допустимо. Вычисления выполняются достаточно долго, соответственно, механизм планирования потоков вмешивается в процесс и чередует потоки, проявляя при этом внимание к более приоритетным. Тем не менее для обеспечения переключения контекста в программе периодически выполняются команды yield().

В пакете JDK предусмотрено 10 уровней приоритетов, однако это не слишком хорошо согласуется с большинством операционных систем. К примеру, в Windows имеется 7 классов приоритетов, таким образом, их соотношение неочевидно (хотя в операционной системе Sun Solaris имеется 231 уровней). Переносимость обеспечивается только использованием универсальных констант МАХ_РRIORITY, NORM_PRIORITY и MIN_PRI0RITY.

Передача управления

Если вы знаете, что в текущей итерации run() сделано все необходимое, вы можете подсказать механизму планирования потоков, что процессором теперь может воспользоваться другой поток. Эта подсказка (не более чем рекомендация; нет никакой гарантии, что планировщик потоков «прислушается» к ней) воплощается в форме вызова метода yield(). Вызывая yield(), вы сообщаете системе, что в ней могут выполняться другие потоки того же приоритета.

В примере LiftOff метод yield() обеспечивает равномерное распределение вычислительных ресурсов между задачами LiftOff. Попробуйте закомментировать вызов Thread.yield() в Lift0ff.run() и проследите за различиями. И все же, в общем случае не стоит полагаться на yield() как на серьезное средство настройки вашего приложения.

Потоки-демоны

Демоном называется поток, предоставляющий некоторый сервис, работая в фоновом режиме во время выполнения программы, но при этом не является ее неотъемлемой частью. Таким образом, когда все потоки не-демоны заканчивают свою деятельность, программа завершается. И наоборот, если существуют работающие потоки не-демоны, программа продолжает выполнение. Существует, например, поток не-демон, выполняющий метод main().

//: concurrency/SimpleDaemons.java
// Потоки-демоны не препятствуют завершению работы программы.
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
 
public class SimpleDaemons implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
print(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public static void main(String[] args) throws Exception {
for(int i = 0; i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true); // Необходимо вызвать перед start()
daemon.start();
}
print("All daemons started");
TimeUnit.MILLISECONDS.sleep(175);
}
}

<spoiler text="Output:"> (Sample)

All daemons started
Thread[Thread-0,5,main] SimpleDaemons@530daa
Thread[Thread-1,5,main] SimpleDaemons@a62fc3
Thread[Thread-2,5,main] SimpleDaemons@89ae9e
Thread[Thread-3,5,main] SimpleDaemons@1270b73
Thread[Thread-4,5,main] SimpleDaemons@60aeb0
Thread[Thread-5,5,main] SimpleDaemons@16caf43
Thread[Thread-6,5,main] SimpleDaemons@66848c
Thread[Thread-7,5,main] SimpleDaemons@8813f2
Thread[Thread-8,5,main] SimpleDaemons@1d58aae
Thread[Thread-9,5,main] SimpleDaemons@83cc67
...

</spoiler>
Чтобы назначить поток демоном, следует перед его запуском вызвать метод setDaemon().

После того как main() завершит свою работу, ничто не препятствует завершению программы, поскольку в процессе не работают другие потоки, кроме демонов. Чтобы результаты запуска всех потоков-демонов были более наглядными, поток main() на некоторое время погружается в «сон». Без этого вы увидели бы только часть результатов при создании демонов. (Поэкспериментируйте с вызовом sleep() для интервалов разной продолжительности.)

В примере SimpleDaemons.java используется явное создание объектов Thread для установки их «демонского» флага. Вы также можете настроить атрибуты (демон, приоритет, имя) потоков, созданных исполнителем; для этого следует написать пользовательскую реализацию ThreadFactory:

//: net/mindview/util/DaemonThreadFactory.java
package net.mindview.util;
import java.util.concurrent.*;
 
public class DaemonThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}

Единственное отличие от обычной реализации ThreadFactory заключается в том, что в данном случае атрибут демона задается равным true. Теперь новый объект DaemonThreadFactory передается в аргументе Executors.newCachedThreadPool():

//: concurrency/DaemonFromFactory.java
// Использование ThreadFactory для создания демонов.
import java.util.concurrent.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
 
public class DaemonFromFactory implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
print(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
print("Interrupted");
}
}
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool(
new DaemonThreadFactory());
for(int i = 0; i < 10; i++)
exec.execute(new DaemonFromFactory());
print("All daemons started");
TimeUnit.MILLISECONDS.sleep(500); // Задержка
}
} /* (Execute to see output) *///:~

Каждый статический метод создания ExecutorService перегружается для получения объекта ThreadFactory, который будет использоваться для создания новых потоков.

Сделаем еще один шаг — создадим вспомогательный класс DaemonThreadPoolExecutor:

//: net/mindview/util/DaemonThreadPoolExecutor.java
package net.mindview.util;
import java.util.concurrent.*;
 
public class DaemonThreadPoolExecutor
extends ThreadPoolExecutor {
public DaemonThreadPoolExecutor() {
super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new DaemonThreadFactory());
}
}

Чтобы узнать, какие значения должны передаваться при вызове конструктора базового класса, я просто заглянул в исходный код Executors.java.

Чтобы узнать, является ли поток демоном, вызовите метод isDaemon(). Если поток является демоном, то все потоки, которые он производит, также будут демонами, что и демонстрируется следующим примером:

//: concurrency/Daemons.java
// Потоки, порождаемые демонами, также являются демонами
// Daemon threads spawn other daemon threads.
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
 
class Daemon implements Runnable {
private Thread[] t = new Thread[10];
public void run() {
for(int i = 0; i < t.length; i++) {
t[i] = new Thread(new DaemonSpawn());
t[i].start();
printnb("DaemonSpawn " + i + " started, ");
}
for(int i = 0; i < t.length; i++)
printnb("t[" + i + "].isDaemon() = " +
t[i].isDaemon() + ", ");
while(true)
Thread.yield();
}
}
 
class DaemonSpawn implements Runnable {
public void run() {
while(true)
Thread.yield();
}
}
 
public class Daemons {
public static void main(String[] args) throws Exception {
Thread d = new Thread(new Daemon());
d.setDaemon(true);
d.start();
printnb("d.isDaemon() = " + d.isDaemon() + ", ");
// Даем потокам-демонам завершить процесс запуска:
TimeUnit.SECONDS.sleep(1);
}
}

<spoiler text="Output:"> (Sample)

d.isDaemon() = true, DaemonSpawn 0 started, DaemonSpawn 1 started, DaemonSpawn 2 started,
DaemonSpawn 3 started, DaemonSpawn 4 started, DaemonSpawn 5 started, DaemonSpawn 6 started,
DaemonSpawn 7 started, DaemonSpawn 8 started, DaemonSpawn 9 started, t[0].isDaemon() = true,
t[1].isDaemon() = true, t[2].isDaemon() = true, t[3].isDaemon() = true, t[4].isDaemon() = true,
t[5].isDaemon() = true, t[6].isDaemon() = true, t[7].isDaemon() = true, t[8].isDaemon() = true,
t[9].isDaemon() = true,

</spoiler>
Поток Daemon переводится в режим демона, а затем порождает группу новых потоков, которые явно не назначаются демонами, но при этом все равно оказываются ими. Затем Daemon входит в бесконечный цикл, на каждом шаге которого вызывается метод yield(), передающий управление другими процессам.

Учтите, что потоки-демоны завершают свои методы run() без выполнения секций finally:

//: concurrency/DaemonsDontRunFinally.java
// Потоки-демоны не выполняют секцию finally.
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
 
class ADaemon implements Runnable {
public void run() {
try {
print("Starting ADaemon");
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException e) {
print("Exiting via InterruptedException");
} finally {
print("This should always run?");
}
}
}
 
public class DaemonsDontRunFinally {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new ADaemon());
t.setDaemon(true);
t.start();
}
}

<spoiler text="Output:">

Starting ADaemon

</spoiler>
Запуск программы наглядно показывает, что секция finally не выполняется. С другой стороны, если закомментировать вызов setDaemon(), вы увидите, что секция finally была выполнена.

Такое поведение верно, даже если из предыдущих описаний finally у вас сложилось обратное впечатление. Демоны завершаются «внезапно», при завершении последнего не-демона. Таким образом, сразу же при выходе из main() JVM немедленно прерывает работу всех демонов, не соблюдая никакие формальности. Невозможность корректного завершения демонов ограничивает возможности их применения. Обычно объекты Executor оказываются более удачным решением, потому что все задачи, находящиеся под управлением Executor, могут быть завершены одновременно.

Варианты кодирования

Во всех предшествующих примерах все классы задач реализовали интерфейс Runnable. В очень простых случаях можно использовать альтернативное решение с прямым наследованием от Thread:

//: concurrency/SimpleThread.java
// Прямое наследование от класса Thread..
 
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
// Store the thread name:
super(Integer.toString(++threadCount));
start();
}
public String toString() {
return "#" + getName() + "(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SimpleThread();
}
}

<spoiler text="Output:">

#1(5), #1(4), #1(3), #1(2), #1(1), #2(5), #2(4), #2(3), #2(2), #2(1), #3(5),
#3(4), #3(3), #3(2), #3(1), #4(5), #4(4), #4(3), #4(2), #4(1), #5(5), #5(4),
#5(3), #5(2), #5(1),

</spoiler>
Чтобы задать объектам Thread имена, вы вызываете соответствующий конструктор Thread. Имя читается в методе toString() при помощи getName().

Также иногда встречается идиома самоуправляемой реализации Runnable:

//: concurrency/SelfManaged.java
// Реализация Runnable. содержащая собственный объект Thread.
 
public class SelfManaged implements Runnable {
private int countDown = 5;
private Thread t = new Thread(this);
public SelfManaged() { t.start(); }
public String toString() {
return Thread.currentThread().getName() +
"(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SelfManaged();
}
}

<spoiler text="Output:">

Thread-0(5), Thread-0(4), Thread-0(3), Thread-0(2), Thread-0(1), Thread-1(5), Thread-1(4),
Thread-1(3), Thread-1(2), Thread-1(1), Thread-2(5), Thread-2(4), Thread-2(3), Thread-2(2),
Thread-2(1), Thread-3(5), Thread-3(4), Thread-3(3), Thread-3(2), Thread-3(1), Thread-4(5),
Thread-4(4), Thread-4(3), Thread-4(2), Thread-4(1),

</spoiler>
В целом происходящее не так уж сильно отличается от наследования от Thread, разве что синтаксис получается чуть более громоздким. Однако реализация интерфейса позволяет наследовать от другого класса, тогда как в варианте с Thread это невозможно.

Обратите внимание на вызов start() в конструкторе. Приведенный пример очень прост, поэтому, скорее всего, в нем такое решение безопасно, но вы должны знать, что запуск потоков в конструкторе может создать изрядные проблемы — до завершения конструктора может быть запущена на выполнение другая задача, которая обратится к объекту в нестабильном состоянии. Это еще одна причина, по которой использование Executor предпочтительнее явного создания объектов Thread.

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

//: concurrency/ThreadVariations.java
// Создание потоков с использованием внутренних классов..
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
 
// Using a named inner class:
class InnerThread1 {
private int countDown = 5;
private Inner inner;
private class Inner extends Thread {
Inner(String name) {
super(name);
start();
}
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
print("interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
}
public InnerThread1(String name) {
inner = new Inner(name);
}
}
 
// Используем безымянный внутренний класс::
class InnerThread2 {
private int countDown = 5;
private Thread t;
public InnerThread2(String name) {
t = new Thread(name) {
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
 
// Используем именованную реализацию Runnable:
class InnerRunnable1 {
private int countDown = 5;
private Inner inner;
private class Inner implements Runnable {
Thread t;
Inner(String name) {
t = new Thread(this, name);
t.start();
}
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public String toString() {
return t.getName() + ": " + countDown;
}
}
public InnerRunnable1(String name) {
inner = new Inner(name);
}
}
 
// Используем анонимную реализацию Runnable:
class InnerRunnable2 {
private int countDown = 5;
private Thread t;
public InnerRunnable2(String name) {
t = new Thread(new Runnable() {
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public String toString() {
return Thread.currentThread().getName() +
": " + countDown;
}
}, name);
t.start();
}
}
 
// Отдельный метод для выполнения кода в потоке:
class ThreadMethod {
private int countDown = 5;
private Thread t;
private String name;
public ThreadMethod(String name) { this.name = name; }
public void runTask() {
if(t == null) {
t = new Thread(name) {
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
}
 
public class ThreadVariations {
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerThread2("InnerThread2");
new InnerRunnable1("InnerRunnable1");
new InnerRunnable2("InnerRunnable2");
new ThreadMethod("ThreadMethod").runTask();
}
} /* (Execute to see output) *///:~

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

Третий и четвертый классы примера повторяют первые два, только вместо класса Thread они используют интерфейс Runnable.

Класс ThreadMethod демонстрирует создание потока в методе. Вы вызываете метод, когда программа готова к запуску потока, а метод возвращает управление после запуска потока. Если поток выполняет только вспомогательные операции и не является фундаментальной частью класса, то этот способ, вероятно, удобнее и практичнее запуска потока из конструктора класса.

Присоединение к потоку

Любой поток может вызвать метод join(), чтобы дождаться завершения другого потока перед своим продолжением. Если поток вызывает t.join() для другого потока t, то вызывающий поток приостанавливается до тех пор, пока целевой поток t не завершит свою работу (когда метод t.isAlive() вернет значение false).

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

Вызов join() может быть прерван вызовом метода interrupt() для потока-инициатора, поэтому потребуется блок try-catch.

Все эти операции продемонстрированы в следующем примере:

//: concurrency/Joining.java
// Демонстрация join().
import static net.mindview.util.Print.*;
 
class Sleeper extends Thread {
private int duration;
public Sleeper(String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}
public void run() {
try {
sleep(duration);
} catch(InterruptedException e) {
print(getName() + " was interrupted. " +
"isInterrupted(): " + isInterrupted());
return;
}
print(getName() + " has awakened");
}
}
 
class Joiner extends Thread {
private Sleeper sleeper;
public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
public void run() {
try {
sleeper.join();
} catch(InterruptedException e) {
print("Interrupted");
}
print(getName() + " join completed");
}
}
 
public class Joining {
public static void main(String[] args) {
Sleeper
sleepy = new Sleeper("Sleepy", 1500),
grumpy = new Sleeper("Grumpy", 1500);
Joiner
dopey = new Joiner("Dopey", sleepy),
doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
}
}

<spoiler text="Output:">

Grumpy was interrupted. isInterrupted(): false
Doc join completed
Sleepy has awakened
Dopey join completed

</spoiler>
Класс Sleeper — это тип потока, который приостанавливается на время, указанное в его конструкторе. В методе run() вызов метода sleep() может закончиться по истечении времени задержки, но может и прерваться. В секции catch выводится сообщение о прерывании, вместе со значением, возвращаемым методом isInterrupted(). Когда другой поток вызывает interrupt() для данного потока, устанавливается флаг, показывающий, что поток был прерван. Однако этот флаг сбрасывается при обработке исключения, поэтому внутри секции catch результатом всегда будет false. Флаг используется в других ситуациях, где поток может исследовать свое прерванное состояние в стороне от исключения.

Joiner — поток, который ожидает пробуждения потока Sleeper, вызывая для последнего метод join(). В методе main() каждому объекту Joiner сопоставляется Sleeper, и вы можете видеть в результатах работы программы, что, если Sleeper был прерван или завершился нормально, Joiner прекращает работу вместе с потоком Sleeper.

Совместное использование ресурсов

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

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

Некорректный доступ к ресурсам

Рассмотрим следующий пример, в котором одна задача генерирует четные числа, а другие задачи эти числа потребляют. Единственной задачей задач-потребителей является проверка четности этих чисел.

Начнем с определения EvenChecker, задачи-потребителя, поскольку она будет использоваться во всех последующих примерах. Чтобы отделить EvenChecker от различных генераторов, с которыми мы будем экспериментировать, мы опре­делим абстрактный класс IntGenerator, содержащий минимум необходимых методов для EvenChecker: метод для получения следующего значения next() и методы отмены. Класс не реализует интерфейс Generator, потому что он должен выдавать int, а параметризация не поддерживает примитивные параметры.

//: concurrency/IntGenerator.java
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
// Allow this to be canceled:
public void cancel() { canceled = true; }
public boolean isCanceled() { return canceled; }
}

IntGenerator содержит метод cancel(), изменяющий состояние флага canceled, и метод isCanceled(), проверяющий, был ли объект отменен. Поскольку флаг canceled относится к типу boolean, простые операции вроде присваивания и возврата значения выполняются атомарно, то есть без возможности прерывания, и вы не увидите поле в промежуточном состоянии между этими простыми операциями. Смысл ключевого слова volatile будет объяснен позже в этой главе.

Для тестирования IntGenerator можно воспользоваться следующим классом EvenChecker:

//: concurrency/EvenChecker.java
import java.util.concurrent.*;
 
public class EvenChecker implements Runnable {
private IntGenerator generator;
private final int id;
public EvenChecker(IntGenerator g, int ident) {
generator = g;
id = ident;
}
public void run() {
while(!generator.isCanceled()) {
int val = generator.next();
if(val % 2 != 0) {
System.out.println(val + " not even!");
generator.cancel(); // Отмена всех EvenChecker
}
}
}
// Тестирование произвольного типа IntGenerator:
public static void test(IntGenerator gp, int count) {
System.out.println("Press Control-C to exit");
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < count; i++)
exec.execute(new EvenChecker(gp, i));
exec.shutdown();
}
// Значение по умолчанию для count:
public static void test(IntGenerator gp) {
test(gp, 10);
}
}

Как видно из run(), все задачи EvenChecker, зависящие от объекта IntGenerator, проверяют, не были ли они отменены. При таком подходе задачи, совместно использующие общий ресурс (IntGenerator), наблюдают за ресурсом, ожидая от него сигнала завершения. Тем самым устраняется так называемая «ситуация гонки», когда две и более задачи торопятся отреагировать на некоторое условие; это приводит к возникновению конфликтов или получению других некорректных результатов. Будьте внимательны, постарайтесь продумать все возможные сбои в системах с параллельным выполнением и защититься от них. Например, задача не может зависеть от другой задачи, потому что порядок завершения задач не гарантирован. Зависимость задач от объекта, не являющегося задачей, устраняет потенциальную «ситуацию гонки».

Метод test() настраивает и тестирует произвольный тип IntGenerator, запуская несколько задач EvenChecker с общим IntGenerator. Если IntGenerator приводит к сбою, test() сообщает о происходящем и возвращает управление. В противном случае его следует завершить вручную клавишами Ctrl+C.

Задачи EvenChecker постоянно читают и проверяют значения, полученные от IntGenerator. Если generator.isCanceled() равен true, run() возвращает управление; это сообщает Executor в EvenChecker.test() о том, что задача завершена. Любая задача EvenChecker может вызвать cancel() для связанного с ней IntGenerator, в результате чего все остальные EvenChecker, использующие IntGenerator, будут корректно завершены. Как будет показано далее в этой главе, в Java существуют и более общие механизмы завершения потоков.

В первом варианте IntGenerator, который мы рассмотрим, next() выдает серию четных значений:

//: concurrency/EvenGenerator.java
// Конфликт потоков.
 
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public int next() {
++currentEvenValue; // Опасная точка!
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}

<spoiler text="Output:">
Нажмите Control-С, чтобы завершить программу

89476993 not even!
89476993 not even!

</spoiler>
Одна задача может вызвать next() после того, как другая задача выполнит первый инкремент currentEvenValue, но до второго инкремента (в позиции, помеченной комментарием «Опасная точка!»). При этом значение оказывается в «некорректном» состоянии. Чтобы доказать, что такое возможно, EvenChecker. test() создает группу объектов EvenChecker, которые непрерывно читают результаты EvenGenerator и проверяют их на четность. При обнаружении нечетного числа выводится сообщение об ошибке, и программа завершается.

Сбой рано или поздно произойдет, потому что задачи EvenChecker могут обратиться к информации EvenGenerator в «некорректном» состоянии. Впрочем, проблема может проявиться лишь после многих циклов отработки EvenGenerator; все зависит от особенностей операционной системы и других подробностей реализации. Чтобы ускорить наступление сбоя, попробуйте разместить вызов yield() между инкрементами. В этом и состоит одна из проблем многопоточного про­граммирования: программа, содержащая ошибку, на первый взгляд работает вполне нормально — а все потому, что вероятность сбоя очень мала.

Также стоит учитывать, что сама операция инкремента состоит из нескольких шагов и может быть прервана планировщиком потоков в ходе выполнения — другими словами, инкремент в Java не является атомарной операцией. Даже простое приращение переменной может оказаться небезопасным, если не организовать защиту задачи.

Разрешение конфликтов доступа

Предыдущий пример показательно иллюстрирует основную проблему потоков: вы никогда не знаете, когда поток будет выполняться. Вообразите, что вы сидите за столом с вилкой в руках, собираетесь съесть последний, самый лакомый кусочек, который лежит на тарелке прямо перед вами. Но, как только вы тянетесь к еде вилкой, она исчезает (как ваш поток был внезапно приостановлен, и другой поток не постеснялся «стянуть» у вас еду). Вот такую проблему нам приходится решать при написании выполняемых одновременно и использующих общие ресурсы программ. Чтобы многопоточность работала, необходим механизм, предотвращающий возможность состязания двух потоков за один ресурс (по крайней мере, во время критичных операций).

Предотвратить такое столкновение интересов несложно — надо блокировать ресурс для других потоков, пока он находится в ведении одного потока. Первый поток, получивший доступ к ресурсу, вешает на него «замок», и тогда все остальные потоки не смогут получить этот ресурс до тех пор, пока «замок» не будет снят, и только после этого другой поток овладеет ресурсом и заблокирует его, и т. д. Если переднее сиденье машины является для детей ограниченным ресурсом, то ребенок, первым крикнувший «Чур, я спереди!», отстоял свое право на «блокировку».

Для решения проблемы соперничества потоков фактически все многопоточные схемы синхронизируют доступ к разделяемым ресурсам. Это означает, что доступ к разделяемому ресурсу в один момент времени может получить только один поток. Чаще всего это выполняется помещением фрагмента кода в секцию блокировки так, что одновременно пройти по этому фрагменту кода может только один поток. Поскольку такое предложение блокировки дает эффект взаимного исключения, этот механизм часто называют мьютексом (MUTual Exclusion).

Вспомните свою ванную комнату — несколько людей (потоки) могут захотеть эксклюзивно владеть ей (разделяемым ресурсом). Чтобы получить доступ в ванную, человек стучится в дверь, желая проверить, не занята ли она. Если ванная свободна, он входит в нее и запирает дверь. Любой другой поток, желающий оказаться внутри, «блокируется» в этом действии, и ему приходится ждать у двери, пока ванная не освободится.

Аналогия немного нарушается, когда дверь в ванную комнату снова открывается, и приходит время передать доступ другому потоку. Как люди на самом деле не становятся в очередь, так и здесь мы точно не знаем, кто «зайдет в ванную» следующим, потому что поведение планировщика потоков недетерминировано. Существует гипотетическая группа блокированных потоков, толкущихся у двери, и, когда поток, который занимал «ванную», разблокирует ее и уйдет, тот поток, что окажется ближе всех к двери, «войдет» в нее. Как уже было замечено, планировщику можно давать подсказки методами yield() и setPriority(), но эти подсказки необязательно будут иметь эффект, в зависимости от вашей платформы и реализации виртуальной машины JVM.

В Java есть встроенная поддержка для предотвращения конфликтов в виде ключевого слова synchronized. Когда поток желает выполнить фрагмент кода, охраняемый словом synchronized, он проверяет, доступен ли семафор, получает доступ к семафору, выполняет код и освобождает семафор.

Разделяемый ресурс чаще всего является блоком памяти, представляющим объект, но это также может быть файл, порт ввода/вывода или устройство (скажем, принтер). Для управления доступом к разделяемому ресурсу вы сначала помещаете его внутрь объекта. После этого любой метод, получающий доступ к ресурсу, может быть объявлен как synchronized. Это означает, что, если задача выполняется внутри одного из объявленных как synchronized методов, все ос­тальные потоки не сумеют зайти ни в какой synchronized-метод до тех пор, пока первый поток не вернется из своего вызова.

Как известно, в окончательной версии кода поля класса обычно объявляются закрытыми (private), а доступ к их памяти осуществляется только посредством методов. Чтобы предотвратить конфликты, объявите такие методы синхро­низированными (с помощью ключевого слова synchronized):

 synchronized void f() { /* .. */ }
synchronized void g(){ /*.. */ }

Каждый объект содержит объект простой блокировки (также называемый монитором). При вызове любого синхронизированного (synchronized) метода объект переходит в состояние блокировки, и пока этот метод не закончит свою работу и не снимет блокировку, другие синхронизированные методы для объекта не могут быть вызваны. В только что рассмотренном примере, если для объекта вызывается метод f(), метод g() не будет вызван до тех пор, пока метод f() не завершит свою работу и не сбросит блокировку. Таким образом, монитор совместно используется всеми синхронизированными методами определенного объекта и предотвращает использование общей памяти несколькими потоками одновременно.

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

Также существует отдельный монитор для класса (часть объекта Class), который следит за тем, чтобы статические (static) синхронизированные (synchronized) методы не использовали одновременно общие статические данные класса.

Синхронизация для примера EvenGenerator

Включив в программу EvenGenerator.java поддержку synchronized, мы можем предотвратить нежелательный доступ со стороны потоков:

//: concurrency/SynchronizedEvenGenerator.java
// Упрощение работы с мьютексами с использованием
// ключевого слова synchronized.
// {RunByHand}
 
public class
SynchronizedEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public synchronized int next() {
++currentEvenValue;
Thread.yield(); // Ускоряем сбой
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new SynchronizedEvenGenerator());
}
}

Вызов Thread.yield() между двумя инкрементами повышает вероятность переключения контекста при нахождении currentEvenValue в нечетном состоянии. Так как мьютекс позволяет выполнять критическую секцию не более чем одной задаче, сбоев не будет.

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

Объекты Lock

Библиотека Java SE5 java.utiLconcurrent также содержит явный механизм управления мьютексами, определенный в java.util.concurrent.locks. Объект Lock можно явно создать в программе, установить или снять блокировку; правда, полученный код будет менее элегантным, чем при использовании встроенной формы. С другой стороны, он обладает большей гибкостью при решении некоторых типов задач. Вот как выглядит пример SynchronizedEvenGenerator.java с явным использо­ванием объектов Lock:

//: concurrency/MutexEvenGenerator.java
// Предотвращение потоковых конфликтов с использованием мьютексов.
// {RunByHand}
import java.util.concurrent.locks.*;
 
public class MutexEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
++currentEvenValue;
Thread.yield(); // Ускоряем сбой
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator());
}
}

MutexEvenGenerator добавляет мьютекс с именем lock и использует методы lock() и unlock() для создания критической секции в next(). При использовании объектов Lock следует применять идиому, показанную в примере: сразу же за вызовом lock() необходимо разместить конструкцию try-finally, при этом в секцию finally включается вызов unlock() — только так можно гарантировать снятие блокировки.

Хотя try-finally требует большего объема кода, чем ключевое слово synchronized, явное использование объектов Lock обладает своими преимуществами. При возникновении проблем с ключевым словом synchronized происходит ис­ключение, но вы не получите возможность выполнить завершающие действия, чтобы сохранить корректное состояние системы. При работе с объектами Lock можно сделать все необходимое в секции finally.

В общем случае использование synchronized уменьшает объем кода, а также радикально снижает вероятность ошибки со стороны программиста, поэтому явные операции с объектами Lock обычно выполняются только при решении особых задач. Например, с ключевым словом synchronized нельзя попытаться получить блокировку с неудачным исходом или попытаться получить блокировку в течение некоторого промежутка времени с последующим отказом — в подобных случаях приходится использовать библиотеку concurrent:

//: concurrency/AttemptLocking.java
// Объекты Lock из библиотеки concurrent делают возможными
// попытки установить блокировку в течение некоторого времени
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
 
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
} finally {
if(captured)
lock.unlock();
}
}
public void timed() {
boolean captured = false;
try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("tryLock(2, TimeUnit.SECONDS): " +
captured);
} finally {
if(captured)
lock.unlock();
}
}
public static void main(String[] args) {
final AttemptLocking al = new AttemptLocking();
al.untimed(); // True -- блокировка доступна
al.timed(); // True -- блокировка доступна
// Теперь создаем отдельную задачу для установления блокировки:
new Thread() {
{ setDaemon(true); }
public void run() {
al.lock.lock();
System.out.println("acquired");
}
}.start();
Thread.yield(); // Даем возможность 2-й задаче
al.untimed(); // False -- блокировка захвачена задачей
al.timed(); // False -- блокировка захвачена задачей
}
}

<spoiler text="Output:">

tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
acquired
tryLock(): false
tryLock(2, TimeUnit.SECONDS): false

</spoiler>
Класс ReentrantLock делает возможной попытку получения блокировки с последующим отказом от нее. Таким образом, если кто-то уже захватил блокировку, вы можете отказаться от своих намерений (вместо того, чтобы дожидаться ее освобождения). В методе timed() делается попытка установления блокировки, которая может завершиться неудачей через 2 секунды (обратите внимание на использование класса Java SE5 TimeUnit для определения единиц времени). В main() отдельный объект Thread создается в виде безымянного класса и устанавливает блокировку, чтобы методы untimed() и timed() могли с чем-то конкурировать.

Атомарные операции и ключевое слово volatile

В дискуссиях, посвященных механизму потоков в Java, часто можно услышать такое утверждение: «Атомарные операции не требуют синхронизации». Атомарная операция — это операция, которую не может прервать планировщик потоков — если она начинается, то продолжается до завершения, без возможности переключения контекста (переключения выполнения на другой поток). Не полагайтесь на атомарность, она ненадежна и опасна — используйте ее вместо синхронизации только в том случае, если вы являетесь экспертом в области синхронизации или, по крайней мере, можете получить помощь от такого эксперта.

Атомарные операции, упоминаемые в таких дискуссиях, включают в себя «простые операции» с примитивными типами, за исключением long и double.

Чтение и запись примитивных переменных гарантированно выполняются как атомарные (неделимые) операции. С другой стороны, JVM разрешается выполнять чтение и запись 64-разрядных величин (long и double) в виде двух раздельных 32-разрядных операций, с ненулевой вероятностью переключения контекста в ходе чтения или записи. Для достижения атомарности (при простом присваивании и возврате значений) можно определить типы long и double с модификатором volatile (учтите, что до выхода Java SE5 ключевое слово volatile не всегда работало корректно). Некоторые реализации JVM могут предоставлять более сильные гарантии, но вы не должны полагаться на платформенно-специфические возможности.

В многопроцессорных системах (которые в наши дни представлены многоядерными процессорами, то есть несколькими процесорами на одном чипе) видимость (visibility) играет гораздо более важную роль, чем в однопроцессорных системах. Изменения, вносимые одной задачей, — даже атомарные в смысле невозможности прерывания — могут оставаться невидимыми для других задач (например, если изменения временно хранятся в локальном кэше процессора). Таким образом, разные задачи будут по-разному воспринимать состояние приложения. Механизм синхронизации обеспечивает распространение видимости изменений, вносимых одной задачей в многопроцессорной системе, по всему приложению. Без синхронизации невозможно заранее предсказать, когда именно изменения станут видимыми.

Ключевое слово volatile обеспечивает видимость в рамках приложения. Если поле объявлено как volatile, это означает, что сразу же после записи в поле изменение будет отражено во всех последующих операциях чтения. Утверждение истинно даже при участии локальных кэшей — поля volatile немедленно записываются в основную память, и дальнейшее чтение происходит из основной памяти.

Если слепо следовать концепции атомарности, можно заметить, что метод getValue() в следующем примере вроде бы отвечает этому описанию:

//: concurrency/AtomicityTest.java
import java.util.concurrent.*;
 
public class AtomicityTest implements Runnable {
private int i = 0;
public int getValue() { return i; }
private synchronized void evenIncrement() { i++; i++; }
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while(true) {
int val = at.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}

<spoiler text="Output:"> (Sample)

191583767

</spoiler>
Однако программа находит нечетные значения и завершается. Хотя return і и является атомарной операцией, отсутствие synchronized позволит читать значение объекта, когда он находится в нестабильном промежуточном состоянии. Вдобавок переменная і не объявлена как volatile, а это приведет к проблемам с видимостью. Оба метода, getValue() и evenIncrement(), должны быть объявлены синхронизируемыми. Только эксперты в области параллельных вычислений могут пытаться применять оптимизацию в подобных случаях.

В качестве второго примера рассмотрим кое-что еще более простое: класс, производящий серийные номера. Каждый раз при вызове метода nextSerialNumber() он должен возвращать уникальное значение:

//: concurrency/SerialNumberGenerator.java
 
public class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static int nextSerialNumber() {
return serialNumber++; // Операция не является потоково-безопасной
}
}

Представить себе класс тривиальнее SerialNumberGenerator вряд ли можно, и если вы ранее работали с языком C++ или имеете другие низкоуровневые навыки, то, видимо, ожидаете, что операция инкремента будет атомарной, так как инкремент обычно реализуется в виде одной инструкции микропроцессора. Однако в виртуальной машине Java инкремент не является атомарным и состоит из чтения и записи, соответственно, ниша для проблем с потоками найдется даже в такой простой программе.

Поле serialNumber объявлено как volatile потому, что каждый поток обладает локальным стеком и поддерживает в нем копии некоторых локальных переменных. Если вы объявляете переменную как volatile, то тем самым указываете компилятору не проводить оптимизацию, а это приведет к удалению чтения и записи, удерживающих поле в соответствии с локальными данными потока. Операции чтения и записи осуществляются непосредственно с памятью без кэширования. Кроме того, volatile не позволяет компилятору изменять порядок обращений с целью оптимизации. И все же присутствие volatile не влияет на тот факт, что инкремент не является атомарной операцией.

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

с перезаписанными значениями минимальна. Методы add() и contains() объявлены как synchronized, чтобы избежать коллизий:

//: concurrency/SerialNumberChecker.java
// Кажущиеся безопасными операции с появлением потоков
// перестают быть таковыми...
// {Args: 4}
import java.util.concurrent.*;
 
// Reuses storage so we don't run out of memory:
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// Инициализируем значением, которое не производится
// классом SerialNumberGenerator:
for(int i = 0; i < size; i++)
array[i] = -1;
}
public synchronized void add(int i) {
array[index] = i;
// Возврат индекса к началу с записью поверх старых значений:
index = ++index % len;
}
public synchronized boolean contains(int val) {
for(int i = 0; i < len; i++)
if(array[i] == val) return true;
return false;
}
}
 
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials =
new CircularSet(1000);
private static ExecutorService exec =
Executors.newCachedThreadPool();
static class SerialChecker implements Runnable {
public void run() {
while(true) {
int serial =
SerialNumberGenerator.nextSerialNumber();
if(serials.contains(serial)) {
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);
}
}
}
public static void main(String[] args) throws Exception {
for(int i = 0; i < SIZE; i++)
exec.execute(new SerialChecker());
// Остановиться после n секунд при наличии аргумента:
if(args.length > 0) {
TimeUnit.SECONDS.sleep(new Integer(args[0]));
System.out.println("No duplicates detected");
System.exit(0);
}
}
}

<spoiler text="Output:"> (Sample)

Duplicate: 8468656

</spoiler>
В классе SerialNumberChecker содержится статическое поле CircuLarSet, хранящее все серийные номера, и вложенный поток Thread, который получает эти номера и удостоверяется в их уникальности. Создав несколько потоков, претендующих на серийные номера, вы обнаружите, что какой-нибудь из них довольно быстро получит уже имеющийся номер (заметьте, что на вашей машине программа может и не обнаружить конфликт, но на многопроцессорной системе она успешно их нашла). Для решения проблемы добавьте к методу nextSerialNumber() слово synchronized.

Предполагается, что безопасными атомарными операциями являются чтение и присвоение примитивов. Однако, как мы увидели в программе AtomicityTest.java, все так же просто использовать атомарную операцию для объекта, который находится в нестабильном промежуточном состоянии, так что ожидать, что какие-то предположения оправдаются, опасно и ненадежно.

Атомарные классы

В Java SE5 появились специальные классы для выполнения атомарных операций с переменными — Atomiclnteger, AtomicLong, AtomicReference и т. д. Эти классы содержат атомарную операцию условного обновления в форме

 boolean compareAndSer(expectedValue, updateValue);

Эти классы предназначены для оптимизации с целью использования атомарности на машинном уровне на некоторых
современных процессорах, поэтому в общем случае вам они не понадобятся. Иногда они применяются и в повседневном программировании, но только при оптимизации производительности. Например, версия AtomicityTest.java, переписанная для использования AtomicInteger, выглядит так:

//: concurrency/AtomicIntegerTest.java
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;
 
public class AtomicIntegerTest implements Runnable {
private AtomicInteger i = new AtomicInteger(0);
public int getValue() { return i.get(); }
private void evenIncrement() { i.addAndGet(2); }
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
public void run() {
System.err.println("Aborting");
System.exit(0);
}
}, 5000); // Завершение через 5 секунд
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest ait = new AtomicIntegerTest();
exec.execute(ait);
while(true) {
int val = ait.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}

Здесь вместо ключевого слова synchronized используется AtomicInteger. Так как сбой в программе не происходит, в программу включается таймер, автоматически завершающий ее через 5 секунд.

Вот как выглядит пример MutexEvenGenerator.java, переписанный для использования класса Atomiclnteger:

//: concurrency/AtomicEvenGenerator.java
// Атомарные классы иногда используются в обычном коде.
// {RunByHand}
import java.util.concurrent.atomic.*;
 
public class AtomicEvenGenerator extends IntGenerator {
private AtomicInteger currentEvenValue =
new AtomicInteger(0);
public int next() {
return currentEvenValue.addAndGet(2);
}
public static void main(String[] args) {
EvenChecker.test(new AtomicEvenGenerator());
}
}

Стоит еще раз подчеркнуть, что классы Atomic проектировались для построения классов из java.util.concurrent. Используйте их в своих программах только в особых случаях и только тогда, когда вы твердо уверены, что это не создаст новых проблем. В общем случае безопаснее использовать блокировки (с ключевым словом synchronized или явным созданием объектов Lock).

Критические секции

Иногда необходимо предотвратить доступ нескольких потоков только к части кода, а не к методу в целом. Фрагмент кода, который изолируется таким способом, называется критической секцией (critical section), для его создания также приме­няется ключевое слово synchronized. На этот раз слово synchronized определяет объект, блокировка которого должна использоваться для синхронизации последующего фрагмента кода:

 synchronized(синхронизируемыйОбьект) {
// К такому коду доступ может получить
// одновременно только один поток
}

Такая конструкция иначе называется синхронизированной блокировкой (synchronized block); перед входом в нее необходимо получить блокировку для syncObject. Если блокировка уже предоставлена другому потоку, вход в последующий фрагмент кода запрещается до тех пор, пока блокировка не будет снята.

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

//: concurrency/CriticalSection.java
// Синхронизация блоков вместо целых методов. Также демонстрирует защиту
// неприспособленного к многопоточности класса другим классом
// with a thread-safe one.
package concurrency;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;
 
class Pair { // Not thread-safe
private int x, y;
public Pair(int x, int y) {
this.x = x;
this.y = y;
}
public Pair() { this(0, 0); }
public int getX() { return x; }
public int getY() { return y; }
public void incrementX() { x++; }
public void incrementY() { y++; }
public String toString() {
return "x: " + x + ", y: " + y;
}
public class PairValuesNotEqualException
extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal: " + Pair.this);
}
}
// Произвольный инвариант - обе переменные должны быть:
public void checkState() {
if(x != y)
throw new PairValuesNotEqualException();
}
}
 
// Защита класса Pair внутри приспособленного к потокам класса:
abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage =
Collections.synchronizedList(new ArrayList<Pair>());
public synchronized Pair getPair() {
// Создаем копию, чтобы сохранить оригинал в безопасности:
return new Pair(p.getX(), p.getY());
}
// Предполагается, что операция занимает некоторое время
protected void store(Pair p) {
storage.add(p);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch(InterruptedException ignore) {}
}
public abstract void increment();
}
 
// Синхронизация всего метода:
class PairManager1 extends PairManager {
public synchronized void increment() {
p.incrementX();
p.incrementY();
store(getPair());
}
}
 
// Использование критической секции:
class PairManager2 extends PairManager {
public void increment() {
Pair temp;
synchronized(this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}
 
class PairManipulator implements Runnable {
private PairManager pm;
public PairManipulator(PairManager pm) {
this.pm = pm;
}
public void run() {
while(true)
pm.increment();
}
public String toString() {
return "Pair: " + pm.getPair() +
" checkCounter = " + pm.checkCounter.get();
}
}
 
class PairChecker implements Runnable {
private PairManager pm;
public PairChecker(PairManager pm) {
this.pm = pm;
}
public void run() {
while(true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
}
 
public class CriticalSection {
// Сравнение двух подходов:
static void
testApproaches(PairManager pman1, PairManager pman2) {
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator
pm1 = new PairManipulator(pman1),
pm2 = new PairManipulator(pman2);
PairChecker
pcheck1 = new PairChecker(pman1),
pcheck2 = new PairChecker(pman2);
exec.execute(pm1);
exec.execute(pm2);
exec.execute(pcheck1);
exec.execute(pcheck2);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch(InterruptedException e) {
System.out.println("Sleep interrupted");
}
System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
System.exit(0);
}
public static void main(String[] args) {
PairManager
pman1 = new PairManager1(),
pman2 = new PairManager2();
testApproaches(pman1, pman2);
}
}

<spoiler text="Output:"> (Sample)

pm1: Pair: x: 15, y: 15 checkCounter = 272565
pm2: Pair: x: 16, y: 16 checkCounter = 3956974

</spoiler>
Как было отмечено, класс Pair не приспособлен к работе с потоками, поскольку его инвариант (предположительно произвольный) требует равенства обоих переменных. Вдобавок, как мы уже видели в этой главе, операции инкремента небезопасны в отношении к потокам, и, так как ни один из методов не был объявлен как synchronized, мы не можем считать, что объект Pair останется неповрежденным в многопоточной программе.
Представьте, что вы получили готовый класс Pair, который должен работать в многопоточных условиях. Класс PairManager хранит объекты Pair и управляет любым доступом к ним. Заметьте, что единственными открытыми (public) методами являются getPair(), объявленный как synchronized, и абстрактный метод doTask(). Синхронизация этого метода будет осуществлена при его реализации.

Структура класса PairManager, в котором часть функциональности базового класса реализуется одним или несколькими абстрактными методами, определенными производными классами, называется на языке паттернов проектирования «шаблонным методом». Паттерны проектирования позволяют инкапсулировать изменения в коде — здесь изменяющаяся часть представлена методом increment(). В классе PairManager1 метод increment() полностью синхронизирован, в то время как в классе PairManager2 только часть его была синхронизирована посредством синхронизируемой блокировки. Обратите внимание еще раз, что ключевые слова synchronized не являются частью сигнатуры метода и могут быть добавлены во время переопределения.

Метод store() добавляет объект Pair в синхронизированный контейнер ArrayList, поэтому операция является потоково-безопасной. Следовательно, в защите он не нуждается, поэтому его вызов размещен за пределами синхронизируемого блока.

Класс PairManipulator создается для тестирования двух разновидностей PairManager: метод increment() вызывается в задаче в то время, как в другой задаче работает PairChecker. Метод main() создает два объекта PairManipulator и дает им поработать в течение некоторого времени, после чего выводятся результаты по каждому PairManipulator.

Для создания критических секций также можно воспользоваться явно созданными объектами Lock:

//: concurrency/ExplicitCriticalSection.java
// Использование объектов Lock для создания критических секций..
package concurrency;
import java.util.concurrent.locks.*;
 
// Синхронизация целого метода:
class ExplicitPairManager1 extends PairManager {
private Lock lock = new ReentrantLock();
public synchronized void increment() {
lock.lock();
try {
p.incrementX();
p.incrementY();
store(getPair());
} finally {
lock.unlock();
}
}
}
 
// Использование критической секции:
class ExplicitPairManager2 extends PairManager {
private Lock lock = new ReentrantLock();
public void increment() {
Pair temp;
lock.lock();
try {
p.incrementX();
p.incrementY();
temp = getPair();
} finally {
lock.unlock();
}
store(temp);
}
}
 
public class ExplicitCriticalSection {
public static void main(String[] args) throws Exception {
PairManager
pman1 = new ExplicitPairManager1(),
pman2 = new ExplicitPairManager2();
CriticalSection.testApproaches(pman1, pman2);
}
}

<spoiler text="Output:"> (Sample)

pm1: Pair: x: 15, y: 15 checkCounter = 174035
pm2: Pair: x: 16, y: 16 checkCounter = 2608588

</spoiler>
В программе создаются новые типы PairManager с явным использованием объектов Lock. ExplicitPairManager2 демонстрирует создание критической секции с использованием объекта Lock; вызов store() находится вне критической секции.

Синхронизация по другим объектам

Блоку synchronized необходимо передать объект, который будет использоваться для синхронизации. Чаще всего наиболее естественно передавать текущий объект, для которого был вызван метод synchronized(this), и именно такой подход применен в классе PairManager2. Таким образом, при входе в синхронизируемый блок другие синхронизированные методы объекта вызвать будет нельзя. Действие синхронизации по this фактически заключается в сужении области синхро­низации.

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

//: concurrency/SyncObject.java
// Синхронизация по другому объекту.
import static net.mindview.util.Print.*;
 
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
for(int i = 0; i < 5; i++) {
print("f()");
Thread.yield();
}
}
public void g() {
synchronized(syncObject) {
for(int i = 0; i < 5; i++) {
print("g()");
Thread.yield();
}
}
}
}
 
public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
}
}

<spoiler text="Output:"> (Sample)

g()
f()
g()
f()
g()
f()
g()
f()
g()
f()

</spoiler>
Метод f() класса DualSync синхронизируется по объекту this (синхронизируя метод целиком), а метод g() использует синхронизацию посредством объекта syncObject. Таким образом, два варианта синхронизации независимы. Демонст­рируется этот факт методом main(), в котором создается поток Threadс вызовом метода f(). Поток main() после этого вызывает метод g(). Из результата работы программы видно, что оба метода работают одновременно и ни один из них не блокируется соседом.

Локальная память потока

Второй механизм предотвращения конфликтов доступа к общим ресурсам основан на исключении их совместного использования. Локальная память потока представляет собой механизм автоматического выделения разных областей памяти для одной переменной во всех потоках, использующих объект. Следовательно, если пять потоков используют объект с переменной х, для х будет сгенерировано пять разных областей памяти. Фактически поток связывается с некоторым состоянием.

За выделение локальной памяти потоков и управление ею отвечает класс java.lang.ThreadLocal:

//: concurrency/ThreadLocalVariableHolder.java
// Автоматическое выделение собственной памяти каждому потоку.
import java.util.concurrent.*;
import java.util.*;
 
class Accessor implements Runnable {
private final int id;
public Accessor(int idn) { id = idn; }
public void run() {
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ": " +
ThreadLocalVariableHolder.get();
}
}
 
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value =
new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() { return value.get(); }
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new Accessor(i));
TimeUnit.SECONDS.sleep(3); // Небольшая задержка
exec.shutdownNow(); // Выход из всех объектов Accessor
}
}

<spoiler text="Output:"> (Sample)

#0: 9259
#1: 556
#2: 6694
#3: 1862
#4: 962
#0: 9260
#1: 557
#2: 6695
#3: 1863
#4: 963
...

</spoiler>
Объекты ThreadLocal обычно хранятся в статических полях. Если вы создаете объект ThreadLocal, для обращения к содержимому объекта можно использовать только методы get() и set(). Метод get() возвращает копию объекта, ассоцииро­ванного с потоком, a set() сохраняет свой аргумент в объекте потока, возвращая ранее хранившийся объект. Их использование продемонстрировано в методах increment() и get() класса ThreadLocalVariableHolder. Обратите внимание: методы increment() и get() не синхронизированы, потому что ThreadLocal не гарантирует отсутствия «ситуации гонки».

Взаимодействие между потоками

Итак, мы выяснили, что потоки способны конфликтовать друг с другом, и разобрались с тем, как предотвратить такие конфликты. Следующим шагом должно стать изучение возможностей взаимодействия между потоками. Ключевым моментом в этом процессе является подтверждение связи, безопасно реализуемое методами wait() и notify() класса Object. В многопоточной библиотеке Java SE5 также присутствуют объекты Condition с методами await() и signal(). Мы рассмотрим некоторые возникающие проблемы и их решения.

Методы wait() и notifyAII()

Метод wait() ожидает изменения некоторого условия, неподконтрольного для текущего метода. Довольно часто это условие изменяется в результате выполнения другой задачи. Активное ожидание, то есть проверка условия в цикле, не­желательно из-за неэффективного расходования вычислительных ресурсов. Таким образом, метод wait() обеспечивает механизм синхронизации действий между задачами.

Важно понять, что метод sleep() не освобождает объект блокировки. С другой стороны, метод wait() снимает блокировку с объекта, тем самым позволяя остальным потокам вызывать другие синхронизированные методы объекта во время выполнения wait(). Это очень важно, потому что обычно именно «другие» методы приводят к изменению условия и активизации приостановленной задачи.

Существует две формы метода wait(). У первой формы аргумент имеет такой же смысл, как и аргумент метода sleep(): это продолжительность интервала в миллисекундах, на который приостанавливается выполнение потока. Разница между методами состоит в следующем:


  • При выполнении метода wait() блокируемый объект освобождается.
  • Выйти из состояния ожидания, установленного wait(), можно двумя способами: с помощью уведомления notify() или notifyAll() либо по истечении срока ожидания.

Вторая, более распространенная форма вызывается без аргументов. Эта версия метода wait() заставит поток простаивать, пока не придет уведомление notify() или notifyAll().

Пожалуй, самое интересное в методах wait(), notify() и notifyAll() — их принадлежность к общему классу Object, а не к классу потоков Thread. Хотя это и кажется немного нелогичным — размещение чего-то, относящегося исключительно к механизму потоков, в общем базовом классе — на самом деле это решение совершенно оправдано, поскольку означенные методы манипулируют блокировками, которые являются частью любого объекта. В результате ожидание (wait()) может использоваться в любом синхронизированном методе, независимо от того, наследует ли класс от Thread или реализует Runnable. Вообще говоря, единственное место, где допустимо вызывать метод wait(), — это синхронизированный метод или блок (метод sleep() можно вызывать в любом месте, так как он не манипулирует блокировкой). Если вызвать метод wait() или notify() в обычном методе, программа скомпилируется, однако при ее выполнении возникнет исключение IllegalMonitorStateException с несколько туманным сообщением «текущий поток не является владельцем» («current thread not owner»). Это сообщение означает, что поток, востребовавший методы wait(), notify() или notifyAll(), должен быть «хозяином» блокируемого объекта (овладеть объектом блокировки) перед вызовом любого из данных методов.

Вы можете «попросить» объект провести операции с помощью его собственного объекта блокировки. Для этого необходимо сначала захватить блокировку для данного объекта. Например, если вы хотите вызвать notifyAll() для объекта х, то должны сделать это в синхронизируемом блоке, устанавливающем блокировку для х:

 synchronized(x) { х.notifyAll();}

Рассмотрим простой пример. В программе WaxOMatic.java задействованы два процесса: один наносит восковую пасту на автомашину (Саr), а другой полирует ее. Задача полировки не может приступить к работе до того, как задача нанесения пасты завершит свою операцию, а задача нанесения пасты должна ждать завершения полировки, чтобы наложить следующий слой пасты. Оба класса, WaxOn и WaxOff, используют объект Саr, который приостанавливает и возобновляет задачи в ожидании изменения условия:

//: concurrency/waxomatic/WaxOMatic.java
// Простейшее взаимодействие задач.
package concurrency.waxomatic;
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
 
class Car {
private boolean waxOn = false;
public synchronized void waxed() {
waxOn = true; // Готово к обработке
notifyAll();
}
public synchronized void buffed() {
waxOn = false; // Готово к нанесению очередного слоя
notifyAll();
}
public synchronized void waitForWaxing()
throws InterruptedException {
while(waxOn == false)
wait();
}
public synchronized void waitForBuffing()
throws InterruptedException {
while(waxOn == true)
wait();
}
}
 
class WaxOn implements Runnable {
private Car car;
public WaxOn(Car c) { car = c; }
public void run() {
try {
while(!Thread.interrupted()) {
printnb("Wax On! ");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed();
car.waitForBuffing();
}
} catch(InterruptedException e) {
print("Exiting via interrupt");
}
print("Ending Wax On task");
}
}
 
class WaxOff implements Runnable {
private Car car;
public WaxOff(Car c) { car = c; }
public void run() {
try {
while(!Thread.interrupted()) {
car.waitForWaxing();
printnb("Wax Off! ");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();
}
} catch(InterruptedException e) {
print("Exiting via interrupt");
}
print("Ending Wax Off task");
}
}
 
public class WaxOMatic {
public static void main(String[] args) throws Exception {
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));
exec.execute(new WaxOn(car));
TimeUnit.SECONDS.sleep(5); // Небольшая задержка...
exec.shutdownNow(); // Прерывание всех задач
}
}

<spoiler text="Output:"> (95% match)

Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On!
Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off!
Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Exiting via interrupt
Ending Wax On task
Exiting via interrupt
Ending Wax Off task

</spoiler>
Класс Саr содержит одну логическую переменную waxOn, которая описывает текущее состояние процесса полировки.
Метод waitForWaxing() проверяет флаг waxOn, и, если он равен false, вызывающая задача приостанавливается вызовом wait(). Очень важно, что это происходит в синхронизированном методе. При вызове wait() поток приостанавливается, а блокировка снимается. Последнее принципиально, потому что для безопасного изменения состояния объекта (например, для присваивания waxOn значения true, без чего приостановленная задача не сможет продолжить работу) блокировка должна быть доступна для другой задачи. В нашем примере при вызове другой задачей метода waxed(), указывающего, что пришло время что-то сделать, для задания истинного значения waxOn необходимо установить блокировку. Затем waxed() вызывает notifyAll(); задача, приостановленная вызовом wait(), активизируется. Для этого нужно сначала заново получить блокировку, освобожденную при входе в wait(). Задача не активизируется, пока блокировка не станет доступной.

Использование каналов для ввода/вывода между потоками

Часто бывает полезно организовать взаимодействие потоков посредством механизмов ввода/вывода. Библиотеки потоков могут предоставлять поддержку ввода/вывода между потоками в форме каналов (pipes). Последние представлены в стандартной библиотеке ввода/вывода Java классами PipedWriter (позволяет потоку записывать в канал) и PipedReader (предоставляет возможность другому потоку считывать из того же канала).

Простой пример взаимодействия двух потоков через канал:

//: concurrency/PipedIO.java
// Использование каналов для ввода/вывода между потоками
import java.util.concurrent.*;
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
class Sender implements Runnable {
private Random rand = new Random(47);
private PipedWriter out = new PipedWriter();
public PipedWriter getPipedWriter() { return out; }
public void run() {
try {
while(true)
for(char c = 'A'; c <= 'z'; c++) {
out.write(c);
TimeUnit.MILLISECONDS.sleep(rand.nextInt(500));
}
} catch(IOException e) {
print(e + " Sender write exception");
} catch(InterruptedException e) {
print(e + " Sender sleep interrupted");
}
}
}
 
class Receiver implements Runnable {
private PipedReader in;
public Receiver(Sender sender) throws IOException {
in = new PipedReader(sender.getPipedWriter());
}
public void run() {
try {
while(true) {
// Блокируется до появления следующего символа:
printnb("Read: " + (char)in.read() + ", ");
}
} catch(IOException e) {
print(e + " Receiver read exception");
}
}
}
 
public class PipedIO {
public static void main(String[] args) throws Exception {
Sender sender = new Sender();
Receiver receiver = new Receiver(sender);
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(sender);
exec.execute(receiver);
TimeUnit.SECONDS.sleep(4);
exec.shutdownNow();
}
}

<spoiler text="Output:"> (65% match)

Read: A, Read: B, Read: C, Read: D, Read: E, Read: F, Read: G, Read: H, Read: I,
Read: J, Read: K, Read: L, Read: M,
java.lang.InterruptedException: sleep interrupted Sender sleep interrupted
java.io.InterruptedIOException Receiver read exception

</spoiler>
Классы Sender и Receiver представляют задачи, которые должны взаимодействовать друг с другом. В классе Sender создается канал PipedWriter, существующий как автономный объект, однако при создании канала PipedReader в классе Receiver конструктору необходимо передать ссылку на PipedWriter. Sender записывает данные в канал Writer и бездействует в течение случайно выбранного промежутка времени. Класс Receiver не содержит вызовов sleep() или wait(), но при проведении чтения методом read() он автоматически блокируется при отсутствии данных.

Заметьте, что потоки sender и receiver запускаются из main() после того, как объекты были полностью сконструированы. Если запускать не полностью сконструированные объекты, каналы на различных платформах могут демонстрировать несогласованное поведение.

Взаимная блокировка

Итак, потоки способны перейти в блокированное состояние, а объекты могут обладать синхронизированными методами, которые запрещают использование объекта до тех пор, пока не будет снята блокировка. Возможна ситуация, в которой один поток ожидает другой поток, тот, в свою очередь, ждет освобождения еще одного потока и т. д., пока эта цепочка не замыкается на поток, который ожидает освобождения первого потока. Получается замкнутый круг потоков, которые дожидаются освобождения друг друга, и никто не может двинуться первым. Такая ситуация называется взаимной блокировкой (deadlock) (или «клинчем». — Примеч. ред.).

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

Классический пример взаимной блокировки, предложенный Эдгаром Дейкстрой — задача об обедающих философах. В стандартной формулировке говорится о пяти философах, но, как будет показано далее, допустимо любое количество. Часть времени философы проводят размышляя, часть времени проводят за едой. Когда они размышляют, то не нуждаются в общих ресурсах, но во время обеда они сидят за круглым столом с ограниченным количеством столовых приборов. В описании оригинальной задачи философы пользуются вилками, и, чтобы набрать спагетти из миски в центре стола, им требуются две вилки. Наверное, задача будет выглядеть более логично, если заменить вилки палочками для еды — очевидно, что каждому философу понадобятся две палочки.

Философы, как это часто бывает, очень бедны, и они смогли позволить себе приобрести лишь пять палочек (или в более общем виде — количество палочек совпадает с количеством философов). Последние разложены кругом по столу, между философами. Когда философу захочется поесть, ему придется взять палочку слева и справа. Если с какой-либо стороны желаемая палочка уже в руке другого философа, только что оторвавшемуся от размышлений придется подождать ее освобождения:

//: concurrency/Chopstick.java
// Палочки для обедающих философов.
 
public class Chopstick {
private boolean taken = false;
public synchronized
void take() throws InterruptedException {
while(taken)
wait();
taken = true;
}
public synchronized void drop() {
taken = false;
notifyAll();
}
}

Два философа (Philosopher) ни при каких условиях не смогут успешно взять (take()) одну и ту же палочку (Chopstick) одновременно. Если один философ уже взял палочку, другому философу придется подождать (wait()), пока она не будет освобождена текущим пользователем (drop()).

Когда задача Philosopher вызывает take(), она ожидает, пока флаг taken не перейдет в состояние false (то есть пока палочка не будет освобождена тем философом, который держит ее в данный момент). Далее задача устанавливает флаг taken равным true, показывая тем самым, что палочка занята. Завершив работу с Chopstick, Philosopher вызывает drop(), чтобы изменить флаг и оповестить (notifyAll()) всех остальных философов, ожидающих освобождения палочки:

//: concurrency/Philosopher.java
// Обедающий философ
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
public class Philosopher implements Runnable {
private Chopstick left;
private Chopstick right;
private final int id;
private final int ponderFactor;
private Random rand = new Random(47);
private void pause() throws InterruptedException {
if(ponderFactor == 0) return;
TimeUnit.MILLISECONDS.sleep(
rand.nextInt(ponderFactor * 250));
}
public Philosopher(Chopstick left, Chopstick right,
int ident, int ponder) {
this.left = left;
this.right = right;
id = ident;
ponderFactor = ponder;
}
public void run() {
try {
while(!Thread.interrupted()) {
print(this + " " + "thinking");
pause();
// Философ проголодался
print(this + " " + "grabbing right");
right.take();
print(this + " " + "grabbing left");
left.take();
print(this + " " + "eating");
pause();
right.drop();
left.drop();
}
} catch(InterruptedException e) {
print(this + " " + "exiting via interrupt");
}
}
public String toString() { return "Philosopher " + id; }
}

В методе Philosopher.run() все философы непрерывно переходят от размышлений к еде, и наоборот. Метод pause() делает паузу случайной продолжительности, если значение ponderFactor отлично от нуля. Итак, Philosopher думает в течение случайного промежутка времени, затем пытается захватить левую и правую палочки вызовами take(), ест в течение случайного промежутка времени, а затем все повторяется.

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

//: concurrency/DeadlockingDiningPhilosophers.java
// Демонстрация скрытой возможности взаимной блокировки.
// {Args: 0 5 timeout}
import java.util.concurrent.*;
 
public class DeadlockingDiningPhilosophers {
public static void main(String[] args) throws Exception {
int ponder = 5;
if(args.length > 0)
ponder = Integer.parseInt(args[0]);
int size = 5;
if(args.length > 1)
size = Integer.parseInt(args[1]);
ExecutorService exec = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for(int i = 0; i < size; i++)
sticks[i] = new Chopstick();
for(int i = 0; i < size; i++)
exec.execute(new Philosopher(
sticks[i], sticks[(i+1) % size], i, ponder));
if(args.length == 3 && args[2].equals("timeout"))
TimeUnit.SECONDS.sleep(5);
else {
System.out.println("Press 'Enter' to quit");
System.in.read();
}
exec.shutdownNow();
}
} /* (Execute to see output) *///:~

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

Первый аргумент командной строки изменяет значение ponder, влияющее на продолжительность размышлений. Если философов очень много или они проводят большую часть времени в размышлениях, взаимная блокировка может и не возникнуть, хотя ее теоретическая вероятность отлична от нуля. С нулевым аргументом взаимная блокировка наступает намного быстрее.

Объектам Chopstick не нужны внутренние идентификаторы; они идентифицируются по своей позиции в массиве sticks. Каждому конструктору Philosopher передаются ссылки на правую и левую палочки Chopstick. Последнему Philosopher в качестве правой палочки передается нулевой объект Chopstick; круг замыкается. Теперь может возникнуть ситуация, когда все философы одновременно попытаются есть, и каждый из них будет ожидать, пока сосед положит свою палочку. В программе наступает взаимная блокировка.

Если философы тратят на размышления больше времени, чем на еду, вероятность взаимной блокировки значительно снижается. Даже может возникнуть иллюзия, что программа свободна от блокировок (при ненулевом значении ponder или большом количестве объектов Philosopher), хотя на самом деле это не так. Именно этим и интересен настоящий пример: программа вроде бы ведет себя верно, тогда как на самом деле возможна взаимная блокировка.

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


  • Взаимное исключение: по крайней мере один ресурс, используемый потоками, не должен быть совместно используемым. В нашем случае одной палочкой для еды не могут одновременно есть два философа.
  • По крайней мере одна задача должна удерживать ресурс и ожидать выделения ресурса, в настоящее время удерживаемого другой задачей. То есть для возникновения тупика философ должен сохранять при себе одну палочку и ожидать другую.
  • Ресурс нельзя принудительно отбирать у задачи. Все процессы должны освобождать ресурсы естественным путем. Наши философы вежливы и не станут выхватывать палочки друг у друга.
  • Должно произойти круговое ожидание, когда процесс ожидает ресурс, занятый другим процессом, который в свою очередь ждет ресурс, удерживаемый еще одним процессом, и т. д., пока один из процессов не будет ожидать ресурса, занятого первым процессом, что и приведет к порочному кругу. В нашем примере круговое ожидание происходит потому, что каждый философ пытается сначала получить правую палочку, а потом левую.

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

//: concurrency/FixedDiningPhilosophers.java
// Обедающие философы без взаимной блокировки.
// {Args: 5 5 timeout}
import java.util.concurrent.*;
 
public class FixedDiningPhilosophers {
public static void main(String[] args) throws Exception {
int ponder = 5;
if(args.length > 0)
ponder = Integer.parseInt(args[0]);
int size = 5;
if(args.length > 1)
size = Integer.parseInt(args[1]);
ExecutorService exec = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for(int i = 0; i < size; i++)
sticks[i] = new Chopstick();
for(int i = 0; i < size; i++)
if(i < (size-1))
exec.execute(new Philosopher(
sticks[i], sticks[i+1], i, ponder));
else
exec.execute(new Philosopher(
sticks[0], sticks[i], i, ponder));
if(args.length == 3 && args[2].equals("timeout"))
TimeUnit.SECONDS.sleep(5);
else {
System.out.println("Press 'Enter' to quit");
System.in.read();
}
exec.shutdownNow();
}
} /* (Execute to see output) *///:~

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

В языке Java нет встроенных средств предупреждения взаимных блокировок; все зависит только от вас и аккуратности вашего кода. Вряд ли эти слова утешат того, кому придется отлаживать программу с взаимной блокировкой.

Новые библиотечные компоненты

В библиотеке java.util.concurrent из Java SE5 появился целый ряд новых классов, предназначенных для решения проблем многозадачности. Научившись пользоваться ими, вы сможете создавать более простые и надежные многозадачные программы.

В этом разделе приведено немало примеров использования различных компонентов. Другие, относительно редко встречающиеся компоненты, здесь не рассматриваются.

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

CountDownLatch

Класс синхронизирует задачи, заставляя их ожидать завершения группы операций, выполняемых другими задачами.

Объекту CountDownLatch присваивается начальное значение счетчика, а все задачи, вызвавшие await() для этого объекта, блокируются до момента обнуления счетчика. Другие задачи могут уменьшать счетчик, вызывая метод countDown() для объекта (обычно это делается тогда, когда задача завершает свою работу). Класс CountDownLatch рассчитан на «одноразовое» применение; счетчик не может возвращаться к прежнему состоянию. Если вам нужна версия с возможностью сброса счетчика, воспользуйтесь классом CyclicBarrier.

Задачи, вызывающие countDown(), не блокируются на время вызова. Только вызов await() блокируется до момента обнуления счетчика.

Типичный способ применения — разделение задачи на n независимых подзадач и создание объекта CountDownLatch с начальным значением n. При завершении каждая подзадача вызывает countDown() для объекта синхронизации. Потоки, ожидающие решения общей задачи, блокируются вызовом await(). Описанная методика продемонстрирована в следующем примере:

//: concurrency/CountDownLatchDemo.java
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
// Часть основной задачи.:
class TaskPortion implements Runnable {
private static int counter = 0;
private final int id = counter++;
private static Random rand = new Random(47);
private final CountDownLatch latch;
TaskPortion(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
doWork();
latch.countDown();
} catch(InterruptedException ex) {
// Приемлемый вариант выхода
}
}
public void doWork() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));
print(this + "completed");
}
public String toString() {
return String.format("%1$-3d ", id);
}
}
 
// Ожидание по объекту CountDownLatch:
class WaitingTask implements Runnable {
private static int counter = 0;
private final int id = counter++;
private final CountDownLatch latch;
WaitingTask(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
latch.await();
print("Latch barrier passed for " + this);
} catch(InterruptedException ex) {
print(this + " interrupted");
}
}
public String toString() {
return String.format("WaitingTask %1$-3d ", id);
}
}
 
public class CountDownLatchDemo {
static final int SIZE = 100;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
// Все подзадачи совместно используют один объект CountDownLatch:
CountDownLatch latch = new CountDownLatch(SIZE);
for(int i = 0; i < 10; i++)
exec.execute(new WaitingTask(latch));
for(int i = 0; i < SIZE; i++)
exec.execute(new TaskPortion(latch));
print("Launched all tasks");
exec.shutdown(); // Выход по завершению всех задач
}
} /* (Execute to see output) *///:~

TaskPortion некоторое время ожидает, имитируя выполнение части задачи, а класс WaitingTask представляет некую часть системы, которая обязана дождаться завершения всех подзадач. Все задачи используют один и тот же объект CountDownLatch, определяемый в main().

CyclicBarrier

Класс CyclicBarrier используется при создании группы параллельно выполняемых задач, завершения которых необходимо дождаться до перехода к следующей фазе. Все параллельные задачи «приостанавливаются» у барьера, чтобы сделать возможным их согласованное продвижение вперед. Класс очень похож на CountDownLatch, за одним важным исключением: CountDownLatch является «одноразовым», a CyclicBarrier может использоваться снова и снова.

Имитации привлекали меня с первых дней работы с компьютерами, и параллельные вычисления играют в них ключевую роль. Даже самая первая программа, которую я написал на BASIC, имитировала скачки на ипподроме. Вот как выглядит объектно-ориентированная, многопоточная версия этой программы с использованием CyclicBarrier:

//: concurrency/HorseRace.java
// Using CyclicBarriers.
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
class Horse implements Runnable {
private static int counter = 0;
private final int id = counter++;
private int strides = 0;
private static Random rand = new Random(47);
private static CyclicBarrier barrier;
public Horse(CyclicBarrier b) { barrier = b; }
public synchronized int getStrides() { return strides; }
public void run() {
try {
while(!Thread.interrupted()) {
synchronized(this) {
strides += rand.nextInt(3); // Produces 0, 1 or 2
}
barrier.await();
}
} catch(InterruptedException e) {
// Приемлемый вариант выхода
} catch(BrokenBarrierException e) {
// Исключение, которое нас интересует
throw new RuntimeException(e);
}
}
public String toString() { return "Horse " + id + " "; }
public String tracks() {
StringBuilder s = new StringBuilder();
for(int i = 0; i < getStrides(); i++)
s.append("*");
s.append(id);
return s.toString();
}
}
 
public class HorseRace {
static final int FINISH_LINE = 75;
private List<Horse> horses = new ArrayList<Horse>();
private ExecutorService exec =
Executors.newCachedThreadPool();
private CyclicBarrier barrier;
public HorseRace(int nHorses, final int pause) {
barrier = new CyclicBarrier(nHorses, new Runnable() {
public void run() {
StringBuilder s = new StringBuilder();
for(int i = 0; i < FINISH_LINE; i++)
s.append("="); // Забор на беговой дорожке
print(s);
for(Horse horse : horses)
print(horse.tracks());
for(Horse horse : horses)
if(horse.getStrides() >= FINISH_LINE) {
print(horse + "won!");
exec.shutdownNow();
return;
}
try {
TimeUnit.MILLISECONDS.sleep(pause);
} catch(InterruptedException e) {
print("barrier-action sleep interrupted");
}
}
});
for(int i = 0; i < nHorses; i++) {
Horse horse = new Horse(barrier);
horses.add(horse);
exec.execute(horse);
}
}
public static void main(String[] args) {
int nHorses = 7;
int pause = 200;
if(args.length > 0) { // Необязательный аргумент
int n = new Integer(args[0]);
nHorses = n > 0 ? n : nHorses;
}
if(args.length > 1) { // Необязательный аргумент
int p = new Integer(args[1]);
pause = p > -1 ? p : pause;
}
new HorseRace(nHorses, pause);
}
} /* (Execute to see output) *///:~

Для объекта CyclicBarrier можно задать «барьерное действие» — объект Runnable, автоматически запускаемый при обнулении счетчика (еще одно отличие CyclicBarrier от CountdownLatch). В нашем примере барьерное действие опре­деляется в виде безымянного класса, передаваемого конструктору CyclicBarrier.

Я попытался сделать так, чтобы каждый объект лошади отображал себя, но порядок отображения зависел от диспетчера задач. Благодаря CyclicBarrier каждая лошадь делает то, что ей необходимо для продвижения вперед, а затем ожидает у барьера перемещения всех остальных лошадей. Когда все лошади переместятся, CyclicBarrier автоматически вызывает «барьерную» задачу Runnable, чтобы отобразить всех лошадей по порядку вместе с барьером. Как только все задачи пройдут барьер, последний автоматически становится готовым для следующего захода.

DelayQueue

Класс представляет неограниченную блокирующую очередь объектов, реализующих интерфейс Delayed. Объект может быть извлечен из очереди только после истечения задержки. Очередь сортируется таким образом, что объект в начале очереди обладает наибольшим сроком истечения задержки. Если задержка ни у одного объекта не истекла, начального элемента нет, и вызов poll() возвращает null (из-за этого в очередь не могут помещаться элементы null).

В следующем примере объекты, реализующие Delayed, сами являются задачами, a DelayedTaskContainer берет задачу с наибольшей просроченной задержкой и запускает ее. Таким образом, DelayQueue является разновидностью при­оритетной очереди.

//: concurrency/DelayQueueDemo.java
import java.util.concurrent.*;
import java.util.*;
import static java.util.concurrent.TimeUnit.*;
import static net.mindview.util.Print.*;
 
class DelayedTask implements Runnable, Delayed {
private static int counter = 0;
private final int id = counter++;
private final int delta;
private final long trigger;
protected static List<DelayedTask> sequence =
new ArrayList<DelayedTask>();
public DelayedTask(int delayInMilliseconds) {
delta = delayInMilliseconds;
trigger = System.nanoTime() +
NANOSECONDS.convert(delta, MILLISECONDS);
sequence.add(this);
}
public long getDelay(TimeUnit unit) {
return unit.convert(
trigger - System.nanoTime(), NANOSECONDS);
}
public int compareTo(Delayed arg) {
DelayedTask that = (DelayedTask)arg;
if(trigger < that.trigger) return -1;
if(trigger > that.trigger) return 1;
return 0;
}
public void run() { printnb(this + " "); }
public String toString() {
return String.format("[%1$-4d]", delta) +
" Task " + id;
}
public String summary() {
return "(" + id + ":" + delta + ")";
}
public static class EndSentinel extends DelayedTask {
private ExecutorService exec;
public EndSentinel(int delay, ExecutorService e) {
super(delay);
exec = e;
}
public void run() {
for(DelayedTask pt : sequence) {
printnb(pt.summary() + " ");
}
print();
print(this + " Calling shutdownNow()");
exec.shutdownNow();
}
}
}
 
class DelayedTaskConsumer implements Runnable {
private DelayQueue<DelayedTask> q;
public DelayedTaskConsumer(DelayQueue<DelayedTask> q) {
this.q = q;
}
public void run() {
try {
while(!Thread.interrupted())
q.take().run(); // Выполнение задачи в текущем потоке
} catch(InterruptedException e) {
// Приемлемый вариант выхода
}
print("Finished DelayedTaskConsumer");
}
}
 
public class DelayQueueDemo {
public static void main(String[] args) {
Random rand = new Random(47);
ExecutorService exec = Executors.newCachedThreadPool();
DelayQueue<DelayedTask> queue =
new DelayQueue<DelayedTask>();
// Очередь заполняется задачами со случайной задержкой:
for(int i = 0; i < 20; i++)
queue.put(new DelayedTask(rand.nextInt(5000)));
// Назначение точки остановки
queue.add(new DelayedTask.EndSentinel(5000, exec));
exec.execute(new DelayedTaskConsumer(queue));
}
}

<spoiler text="Output:">

[128 ] Task 11 [200 ] Task 7 [429 ] Task 5 [520 ] Task 18 [555 ] Task 1 [961 ]
Task 4 [998 ] Task 16 [1207] Task 9 [1693] Task 2 [1809] Task 14 [1861]
Task 3 [2278] Task 15 [3288] Task 10 [3551] Task 12 [4258] Task 0 [4258]
Task 19 [4522] Task 8 [4589] Task 13 [4861] Task 17 [4868] Task 6 (0:4258)
(1:555) (2:1693) (3:1861) (4:961) (5:429) (6:4868) (7:200) (8:4522) (9:1207)
(10:3288) (11:128) (12:3551) (13:4589) (14:1809) (15:2278) (16:998) (17:4861)
(18:520) (19:4258) (20:5000)
[5000] Task 20 Calling shutdownNow()
Finished DelayedTaskConsumer

</spoiler>
DelayedTask содержит контейнер List<DelayedTask> с именем sequence, в котором сохраняется порядок создания задач, и мы видим, что сортировка действительно выполняется.

Интерфейс Delayed содержит единственный метод getDelay(), который сообщает, сколько времени осталось до истечения задержки или как давно задержка истекла. Метод заставляет нас использовать класс TimeUnit, потому что его аргумент относится именно к этому типу. Впрочем, этот класс очень удобен, поскольку он позволяет легко преобразовывать единицы без каких-либо вычислений. Например, значение delta хранится в миллисекундах, а метод Java SE5 System.nanoTime() выдает значение в наносекундах. Чтобы преобразовать значение delta, достаточно указать исходные и итоговые единицы:

 NANOSECONDS.convert(delta. MILL ISECONDS);

В getDelay() желаемые единицы передаются в аргументе unit. Аргумент используется для преобразования времени задержки во временные единицы, используемые вызывающей стороной.

Для выполнения сортировки интерфейс Delayed также наследует интерфейс Comparable, поэтому необходимо реализовать метод compareTo() для выполнения осмысленных сравнений. Методы toString() и summary() обеспечивают формати­рование вывода.

Из выходных данных видно, что порядок создания задач не влияет на порядок их выполнения — вместо этого задачи, как и предполагалось, выполняются в порядке следования задержек.

PriorityBlockingQueue

Фактически класс PriorityBlockingQueue представляет приоритетную очередь с блокирующими операциями выборки. В следующем примере объектами в приоритетной очереди являются задачи, покидающие очередь в порядке приоритетов. Для определения этого порядка в класс PrioritizedTask включается поле priority:

//: concurrency/PriorityBlockingQueueDemo.java
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
class PrioritizedTask implements
Runnable, Comparable<PrioritizedTask> {
private Random rand = new Random(47);
private static int counter = 0;
private final int id = counter++;
private final int priority;
protected static List<PrioritizedTask> sequence =
new ArrayList<PrioritizedTask>();
public PrioritizedTask(int priority) {
this.priority = priority;
sequence.add(this);
}
public int compareTo(PrioritizedTask arg) {
return priority < arg.priority ? 1 :
(priority > arg.priority ? -1 : 0);
}
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(rand.nextInt(250));
} catch(InterruptedException e) {
// Приемлемый вариант выхода
}
print(this);
}
public String toString() {
return String.format("[%1$-3d]", priority) +
" Task " + id;
}
public String summary() {
return "(" + id + ":" + priority + ")";
}
public static class EndSentinel extends PrioritizedTask {
private ExecutorService exec;
public EndSentinel(ExecutorService e) {
super(-1); // Минимальный приоритет в этой программе
exec = e;
}
public void run() {
int count = 0;
for(PrioritizedTask pt : sequence) {
printnb(pt.summary());
if(++count % 5 == 0)
print();
}
print();
print(this + " Calling shutdownNow()");
exec.shutdownNow();
}
}
}
 
class PrioritizedTaskProducer implements Runnable {
private Random rand = new Random(47);
private Queue<Runnable> queue;
private ExecutorService exec;
public PrioritizedTaskProducer(
Queue<Runnable> q, ExecutorService e) {
queue = q;
exec = e; // Используется для EndSentinel
}
public void run() {
// Неограниченная очередь без блокировки.
// Быстрое заполнение случайными приоритетами:
for(int i = 0; i < 20; i++) {
queue.add(new PrioritizedTask(rand.nextInt(10)));
Thread.yield();
}
// Добавление высокоприоритетных задач:
try {
for(int i = 0; i < 10; i++) {
TimeUnit.MILLISECONDS.sleep(250);
queue.add(new PrioritizedTask(10));
}
// Добавление заданий, начиная с наименьших приоритетов:
for(int i = 0; i < 10; i++)
queue.add(new PrioritizedTask(i));
// Предохранитель для остановки всех задач::
queue.add(new PrioritizedTask.EndSentinel(exec));
} catch(InterruptedException e) {
// Приемлемый вариант выхода
}
print("Finished PrioritizedTaskProducer");
}
}
 
class PrioritizedTaskConsumer implements Runnable {
private PriorityBlockingQueue<Runnable> q;
public PrioritizedTaskConsumer(
PriorityBlockingQueue<Runnable> q) {
this.q = q;
}
public void run() {
try {
while(!Thread.interrupted())
// Использование текущего потока для запуска задачи:
q.take().run();
} catch(InterruptedException e) {
// Приемлемый вариант выхода
}
print("Finished PrioritizedTaskConsumer");
}
}
 
public class PriorityBlockingQueueDemo {
public static void main(String[] args) throws Exception {
Random rand = new Random(47);
ExecutorService exec = Executors.newCachedThreadPool();
PriorityBlockingQueue<Runnable> queue =
new PriorityBlockingQueue<Runnable>();
exec.execute(new PrioritizedTaskProducer(queue, exec));
exec.execute(new PrioritizedTaskConsumer(queue));
}
} /* (Execute to see output) *///:~

Как и в предыдущем примере, последовательность создания объектов PrioritizedTask сохраняется в контейнере List sequence для сравнения с фактическим порядком выполнения. Метод run() делает небольшую паузу, а затем выводит информацию об объекте, а предохранитель EndSentinel выполняет те же функции, что и прежде.

PrioritizedTaskProducer и PrioritizedTaskConsumer связываются друг с другом через PriorityBlockingQueue. Так как сам блокирующий характер очереди обеспечивает всю необходимую синхронизацию, явная синхронизация не нужна — при чтении вам не нужно думать о том, содержит ли очередь элементы, потому что при отсутствии элементов очередь просто заблокирует читающую сторону.

Управление оранжереей на базе ScheduledExecutor

В главе 10 была представлена система управления гипотетической оранжереей, которая включала (отключала) различные устройства и регулировала их работу. Происходящее можно преобразовать в контекст многозадачности: каждое событие оранжереи представляет собой задачу, запускаемую в заранее заданное время. Класс ScheduledThreadPoolExecutor предоставляет именно тот сервис, который необходим для решения задачи. Используя методы schedule() (однократный запуск задачи) или scheduleAtFixedRate() (повторение задачи с постоянным промежутком), мы создаем объекты Runnable, которые должны запуститься в положенное время. Сравните это решение с тем, что приведено в главе 10, и посмотрите, насколько оно упрощается благодаря готовой функциональности ScheduledThreadPoolExecutor:

//: concurrency/GreenhouseScheduler.java
// Новая реализация іnnerclasses/GreenhouseController.java
// с использованием ScheduledThreadPoolExecutor.
// {Args: 5000}
import java.util.concurrent.*;
import java.util.*;
 
public class GreenhouseScheduler {
private volatile boolean light = false;
private volatile boolean water = false;
private String thermostat = "Day";
public synchronized String getThermostat() {
return thermostat;
}
public synchronized void setThermostat(String value) {
thermostat = value;
}
ScheduledThreadPoolExecutor scheduler =
new ScheduledThreadPoolExecutor(10);
public void schedule(Runnable event, long delay) {
scheduler.schedule(event,delay,TimeUnit.MILLISECONDS);
}
public void
repeat(Runnable event, long initialDelay, long period) {
scheduler.scheduleAtFixedRate(
event, initialDelay, period, TimeUnit.MILLISECONDS);
}
class LightOn implements Runnable {
public void run() {
// Put hardware control code here to
// physically turn on the light.
System.out.println("Turning on lights");
light = true;
}
}
class LightOff implements Runnable {
public void run() {
// Put hardware control code here to
// physically turn off the light.
System.out.println("Turning off lights");
light = false;
}
}
class WaterOn implements Runnable {
public void run() {
// Put hardware control code here.
System.out.println("Turning greenhouse water on");
water = true;
}
}
class WaterOff implements Runnable {
public void run() {
// Put hardware control code here.
System.out.println("Turning greenhouse water off");
water = false;
}
}
class ThermostatNight implements Runnable {
public void run() {
// Put hardware control code here.
System.out.println("Thermostat to night setting");
setThermostat("Night");
}
}
class ThermostatDay implements Runnable {
public void run() {
// Put hardware control code here.
System.out.println("Thermostat to day setting");
setThermostat("Day");
}
}
class Bell implements Runnable {
public void run() { System.out.println("Bing!"); }
}
class Terminate implements Runnable {
public void run() {
System.out.println("Terminating");
scheduler.shutdownNow();
// Must start a separate task to do this job,
// since the scheduler has been shut down:
new Thread() {
public void run() {
for(DataPoint d : data)
System.out.println(d);
}
}.start();
}
}
// New feature: data collection
static class DataPoint {
final Calendar time;
final float temperature;
final float humidity;
public DataPoint(Calendar d, float temp, float hum) {
time = d;
temperature = temp;
humidity = hum;
}
public String toString() {
return time.getTime() +
String.format(
" temperature: %1$.1f humidity: %2$.2f",
temperature, humidity);
}
}
private Calendar lastTime = Calendar.getInstance();
{ // Adjust date to the half hour
lastTime.set(Calendar.MINUTE, 30);
lastTime.set(Calendar.SECOND, 00);
}
private float lastTemp = 65.0f;
private int tempDirection = +1;
private float lastHumidity = 50.0f;
private int humidityDirection = +1;
private Random rand = new Random(47);
List<DataPoint> data = Collections.synchronizedList(
new ArrayList<DataPoint>());
class CollectData implements Runnable {
public void run() {
System.out.println("Collecting data");
synchronized(GreenhouseScheduler.this) {
// Pretend the interval is longer than it is:
lastTime.set(Calendar.MINUTE,
lastTime.get(Calendar.MINUTE) + 30);
// One in 5 chances of reversing the direction:
if(rand.nextInt(5) == 4)
tempDirection = -tempDirection;
// Store previous value:
lastTemp = lastTemp +
tempDirection * (1.0f + rand.nextFloat());
if(rand.nextInt(5) == 4)
humidityDirection = -humidityDirection;
lastHumidity = lastHumidity +
humidityDirection * rand.nextFloat();
// Calendar must be cloned, otherwise all
// DataPoints hold references to the same lastTime.
// For a basic object like Calendar, clone() is OK.
data.add(new DataPoint((Calendar)lastTime.clone(),
lastTemp, lastHumidity));
}
}
}
public static void main(String[] args) {
GreenhouseScheduler gh = new GreenhouseScheduler();
gh.schedule(gh.new Terminate(), 5000);
// Former "Restart" class not necessary:
gh.repeat(gh.new Bell(), 0, 1000);
gh.repeat(gh.new ThermostatNight(), 0, 2000);
gh.repeat(gh.new LightOn(), 0, 200);
gh.repeat(gh.new LightOff(), 0, 400);
gh.repeat(gh.new WaterOn(), 0, 600);
gh.repeat(gh.new WaterOff(), 0, 800);
gh.repeat(gh.new ThermostatDay(), 0, 1400);
gh.repeat(gh.new CollectData(), 500, 500);
}
} /* (Execute to see output) *///:~

В этой версии, помимо реорганизации кода, добавляется новая возможность: сбор данных о температуре и влажности в оранжерее. Объект DataPoint содержит и выводит одну точку данных, а запланированная задача CollectData генерирует данные имитации и включает их в List<DataPoint> при каждом запуске.

Обратите внимание на ключевые слова volatile и synchronized; благодаря им задачи не мешают работе друг друга. Все методы контейнера List с элементами DataPoint синхронизируются с использованием метода synchronizedList() библио­теки java.util.Соllectiоns при создании List.

Семафоры

При обычной блокировке доступ к ресурсу в любой момент времени разрешается только одной задаче. Семафор со счетчиком позволяет n задачам одновременно обращаться к ресурсу. Можно считать, что семафор «выдает разрешения» на использование ресурса, хотя никаких реальных объектов разрешений в этой схеме нет.
В качестве примера рассмотрим концепцию пула объектов: объекты, входящие в пул, «выдаются» для использования, а затем снова возвращаются в пул после того, как пользователь закончит работу с ними. Эта функциональность инкапсулируется в параметризованном классе:

//: concurrency/Pool.java
// Использование Semaphore в Pool ограничивает количество
// задач, которые могут использовать ресурс.
import java.util.concurrent.*;
import java.util.*;
 
public class Pool<T> {
private int size;
private List<T> items = new ArrayList<T>();
private volatile boolean[] checkedOut;
private Semaphore available;
public Pool(Class<T> classObject, int size) {
this.size = size;
checkedOut = new boolean[size];
available = new Semaphore(size, true);
// Заполнение пула объектами :
for(int i = 0; i < size; ++i)
try {
// Предполагается наличие конструктора по умолчанию:
items.add(classObject.newInstance());
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public T checkOut() throws InterruptedException {
available.acquire();
return getItem();
}
public void checkIn(T x) {
if(releaseItem(x))
available.release();
}
private synchronized T getItem() {
for(int i = 0; i < size; ++i)
if(!checkedOut[i]) {
checkedOut[i] = true;
return items.get(i);
}
return null; // Семафор предотвращает переход в зту точку
}
private synchronized boolean releaseItem(T item) {
int index = items.indexOf(item);
if(index == -1) return false; // Отсутствует в списке
if(checkedOut[index]) {
checkedOut[index] = false;
return true;
}
return false; // He был освобожден
}
}

В этой упрощенной форме конструктор использует newInstance() для заполнения пула объектами. Если вам понадобится новый объект, вызовите checkOut(); завершив работу с объектом, передайте его checkIn().

Логический массив checkedOut отслеживает выданные объекты. Для управления его содержимым используются методы getItem() и releaseItem(). В свою очередь, эти методы защищены семафором available, поэтому в checkOut() семафор available блокирует дальнейшее выполнение при отсутствии семафорных разрешений (то есть при отсутствии объектов в пуле). Метод checkIn() проверяет действительность возвращаемого объекта, и, если объект действителен, разрешение возвращается семафору.

Для примера мы воспользуемся классом Fat. Создание объектов этого класса является высокозатратной операцией, а на выполнение конструктора уходит много времени:

//: concurrency/Fat.java
// Объекты, создание которых занимает много времени.
 
public class Fat {
private volatile double d; // Предотвращает оптимизацию
private static int counter = 0;
private final int id = counter++;
public Fat() {
// Затратная, прервываемая операция:
for(int i = 1; i < 10000; i++) {
d += (Math.PI + Math.E) / (double)i;
}
}
public void operation() { System.out.println(this); }
public String toString() { return "Fat id: " + id; }
}

Мы создадим пул объектов Fat, чтобы свести к минимуму затраты на выполнение конструктора. Для тестирования класса Pool будет создана задача, которая забирает объекты Fat для использования, удерживает их в течение некоторого времени, а затем возвращает обратно:

//: concurrency/SemaphoreDemo.java
// Тестирование класса Pool
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
 
// Задача для получения ресурса из пула:
class CheckoutTask<T> implements Runnable {
private static int counter = 0;
private final int id = counter++;
private Pool<T> pool;
public CheckoutTask(Pool<T> pool) {
this.pool = pool;
}
public void run() {
try {
T item = pool.checkOut();
print(this + "checked out " + item);
TimeUnit.SECONDS.sleep(1);
print(this +"checking in " + item);
pool.checkIn(item);
} catch(InterruptedException e) {
// Приемлемый способ завершения
}
}
public String toString() {
return "CheckoutTask " + id + " ";
}
}
 
public class SemaphoreDemo {
final static int SIZE = 25;
public static void main(String[] args) throws Exception {
final Pool<Fat> pool =
new Pool<Fat>(Fat.class, SIZE);
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < SIZE; i++)
exec.execute(new CheckoutTask<Fat>(pool));
print("All CheckoutTasks created");
List<Fat> list = new ArrayList<Fat>();
for(int i = 0; i < SIZE; i++) {
Fat f = pool.checkOut();
printnb(i + ": main() thread checked out ");
f.operation();
list.add(f);
}
Future<?> blocked = exec.submit(new Runnable() {
public void run() {
try {
// Семафор предотвращает лишний вызов checkout.
// поэтому следующий вызов блокируется:
pool.checkOut();
} catch(InterruptedException e) {
print("checkOut() Interrupted");
}
}
});
TimeUnit.SECONDS.sleep(2);
blocked.cancel(true); // Выход из заблокированного вызова
print("Checking in objects in " + list);
for(Fat f : list)
pool.checkIn(f);
for(Fat f : list)
pool.checkIn(f); // Второй вызов checkIn игнорируется
exec.shutdown();
}
} /* (Execute to see output) *///:~

В коде main() создается объект Pool для хранения объектов Fat, после чего группа задач CheckoutTask начинает использовать Pool. Далее поток main() начинает выдавать объекты Fat, не возвращая их обратно. После того как все объекты пула будут выданы, семафор запрещает дальнейшие выдачи. Метод run() блокируется, и через две секунды вызывается метод cancel(). Лишние возвраты Pool игнорирует.

Exchanger

Класс Exchanger представляет собой «барьер», который меняет местами объекты двух задач. На подходе к барьеру задачи имеют один объект, а на выходе — объект, ранее удерживавшийся другой задачей. Объекты Exchanger обычно исполь­зуются в тех ситуациях, когда одна задача создает высокозатратные объекты, а другая задача эти объекты потребляет.

Чтобы опробовать на практике класс Exchanger, мы создадим задачу-поставщика и задачу-потребителя, которые благодаря параметризации и генераторам могут работать с объектами любого типа. Затем эти параметризованные задачи будут применены к классу Fat. ExchangerProducer и ExchangerConsumer меняют местами List<T>; при вызове метода Exchanger.exchange() вызов блокируется до тех пор, пока парная задача не вызовет свой метод exchange(), после чего оба метода exchange() завершаются, а контейнеры List<T> меняются местами:

//: concurrency/ExchangerDemo.java
import java.util.concurrent.*;
import java.util.*;
import net.mindview.util.*;
 
class ExchangerProducer<T> implements Runnable {
private Generator<T> generator;
private Exchanger<List<T>> exchanger;
private List<T> holder;
ExchangerProducer(Exchanger<List<T>> exchg,
Generator<T> gen, List<T> holder) {
exchanger = exchg;
generator = gen;
this.holder = holder;
}
public void run() {
try {
while(!Thread.interrupted()) {
for(int i = 0; i < ExchangerDemo.size; i++)
holder.add(generator.next());
// Заполненный контейнер заменяется пустым::
holder = exchanger.exchange(holder);
}
} catch(InterruptedException e) {
// Приемлемый способ завершения.
}
}
}
 
class ExchangerConsumer<T> implements Runnable {
private Exchanger<List<T>> exchanger;
private List<T> holder;
private volatile T value;
ExchangerConsumer(Exchanger<List<T>> ex, List<T> holder){
exchanger = ex;
this.holder = holder;
}
public void run() {
try {
while(!Thread.interrupted()) {
holder = exchanger.exchange(holder);
for(T x : holder) {
value = x; // Выборка значения
holder.remove(x); // Нормально для CopyOnWriteArrayList
}
}
} catch(InterruptedException e) {
// Приемлемый способ завершения.
}
System.out.println("Final value: " + value);
}
}
 
public class ExchangerDemo {
static int size = 10;
static int delay = 5; // Секунды
public static void main(String[] args) throws Exception {
if(args.length > 0)
size = new Integer(args[0]);
if(args.length > 1)
delay = new Integer(args[1]);
ExecutorService exec = Executors.newCachedThreadPool();
Exchanger<List<Fat>> xc = new Exchanger<List<Fat>>();
List<Fat>
producerList = new CopyOnWriteArrayList<Fat>(),
consumerList = new CopyOnWriteArrayList<Fat>();
exec.execute(new ExchangerProducer<Fat>(xc,
BasicGenerator.create(Fat.class), producerList));
exec.execute(
new ExchangerConsumer<Fat>(xc,consumerList));
TimeUnit.SECONDS.sleep(delay);
exec.shutdownNow();
}
}

<spoiler text="Output:"> (Sample)

Final value: Fat id: 29999

</spoiler>
В методе main() для обеих задач создается один объект Exchanger, а для перестановки создаются два контейнера CopyOnWriteArrayList. Эта разновидность List нормально переносит вызов метода remove() при перемещении по списку, не вы­давая исключения ConcurrentModificationException.

ExchangerProducer заполняет список, а затем меняет местами заполненный список с пустым, передаваемым от ExchangerConsumer. Благодаря Exchanger заполнение списка происходит одно­временно с использованием уже заполненного списка.

Моделирование

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

Примеры HorseRace.java и GreenhouseScheduler.java, приведенные ранее, тоже можно считать своего рода имитаторами.

Модель кассира

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

//: concurrency/BankTellerSimulation.java
// Пример использования очередей и многопоточного программирования..
// {Args: 5}
import java.util.concurrent.*;
import java.util.*;
 
// Объекты, доступные только для чтения, не требуют синхронизации:
class Customer {
private final int serviceTime;
public Customer(int tm) { serviceTime = tm; }
public int getServiceTime() { return serviceTime; }
public String toString() {
return "[" + serviceTime + "]";
}
}
 
// Очередь клиентов умеет выводить информацию о своем состоянии:
class CustomerLine extends ArrayBlockingQueue<Customer> {
public CustomerLine(int maxLineSize) {
super(maxLineSize);
}
public String toString() {
if(this.size() == 0)
return "[Empty]";
StringBuilder result = new StringBuilder();
for(Customer customer : this)
result.append(customer);
return result.toString();
}
}
 
// Случайное добавление клиентов в очередь:
class CustomerGenerator implements Runnable {
private CustomerLine customers;
private static Random rand = new Random(47);
public CustomerGenerator(CustomerLine cq) {
customers = cq;
}
public void run() {
try {
while(!Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(rand.nextInt(300));
customers.put(new Customer(rand.nextInt(1000)));
}
} catch(InterruptedException e) {
System.out.println("CustomerGenerator interrupted");
}
System.out.println("CustomerGenerator terminating");
}
}
 
class Teller implements Runnable, Comparable<Teller> {
private static int counter = 0;
private final int id = counter++;
// Счетчик клиентов, обслуженных за текущую смену:
private int customersServed = 0;
private CustomerLine customers;
private boolean servingCustomerLine = true;
public Teller(CustomerLine cq) { customers = cq; }
public void run() {
try {
while(!Thread.interrupted()) {
Customer customer = customers.take();
TimeUnit.MILLISECONDS.sleep(
customer.getServiceTime());
synchronized(this) {
customersServed++;
while(!servingCustomerLine)
wait();
}
}
} catch(InterruptedException e) {
System.out.println(this + "interrupted");
}
System.out.println(this + "terminating");
}
public synchronized void doSomethingElse() {
customersServed = 0;
servingCustomerLine = false;
}
public synchronized void serveCustomerLine() {
assert !servingCustomerLine:"quot;already serving: " + this;
servingCustomerLine = true;
notifyAll();
}
public String toString() { return "Teller " + id + " "; }
public String shortString() { return "T" + id; }
// Используется приоритетной очередью:
public synchronized int compareTo(Teller other) {
return customersServed < other.customersServed ? -1 :
(customersServed == other.customersServed ? 0 : 1);
}
}
 
class TellerManager implements Runnable {
private ExecutorService exec;
private CustomerLine customers;
private PriorityQueue<Teller> workingTellers =
new PriorityQueue<Teller>();
private Queue<Teller> tellersDoingOtherThings =
new LinkedList<Teller>();
private int adjustmentPeriod;
private static Random rand = new Random(47);
public TellerManager(ExecutorService e,
CustomerLine customers, int adjustmentPeriod) {
exec = e;
this.customers = customers;
this.adjustmentPeriod = adjustmentPeriod;
// Начинаем с одного кассира:
Teller teller = new Teller(customers);
exec.execute(teller);
workingTellers.add(teller);
}
public void adjustTellerNumber() {
// Фактически это система управления. Регулировка числовых
// параметров позволяет выявить проблемы стабильности
// в механизме управления.
// Если очередь слишком длинна, добавить другого кассира:
if(customers.size() / workingTellers.size() > 2) {
// Если кассиры отдыхают или заняты
// другими делами, вернуть одного из них:
if(tellersDoingOtherThings.size() > 0) {
Teller teller = tellersDoingOtherThings.remove();
teller.serveCustomerLine();
workingTellers.offer(teller);
return;
}
// Иначе создаем (нанимаем) нового кассира
Teller teller = new Teller(customers);
exec.execute(teller);
workingTellers.add(teller);
return;
}
// Если очередь достаточно коротка, освободить кассира:
if(workingTellers.size() > 1 &&
customers.size() / workingTellers.size() < 2)
reassignOneTeller();
// Если очереди нет. достаточно одного кассира:
if(customers.size() == 0)
while(workingTellers.size() > 1)
reassignOneTeller();
}
// Поручаем кассиру другую работу или отправляем его отдыхать:
private void reassignOneTeller() {
Teller teller = workingTellers.poll();
teller.doSomethingElse();
tellersDoingOtherThings.offer(teller);
}
public void run() {
try {
while(!Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(adjustmentPeriod);
adjustTellerNumber();
System.out.print(customers + " { ");
for(Teller teller : workingTellers)
System.out.print(teller.shortString() + " ");
System.out.println("}");
}
} catch(InterruptedException e) {
System.out.println(this + "interrupted");
}
System.out.println(this + "terminating");
}
public String toString() { return "TellerManager "; }
}
 
public class BankTellerSimulation {
static final int MAX_LINE_SIZE = 50;
static final int ADJUSTMENT_PERIOD = 1000;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
// Если очередь слишком длинна, клиенты уходят:
CustomerLine customers =
new CustomerLine(MAX_LINE_SIZE);
exec.execute(new CustomerGenerator(customers));
// TellerManager добавляет и убирает кассиров
// по мере необходимости:
exec.execute(new TellerManager(
exec, customers, ADJUSTMENT_PERIOD));
if(args.length > 0) // Необязательный аргумент
TimeUnit.SECONDS.sleep(new Integer(args[0]));
else {
System.out.println("Press 'Enter' to quit");
System.in.read();
}
exec.shutdownNow();
}
}

<spoiler text="Output:"> (Sample)

[429][200][207] { T0 T1 }
[861][258][140][322] { T0 T1 }
[575][342][804][826][896][984] { T0 T1 T2 }
[984][810][141][12][689][992][976][368][395][354] { T0 T1 T2 T3 }
Teller 2 interrupted
Teller 2 terminating
Teller 1 interrupted
Teller 1 terminating
TellerManager interrupted
TellerManager terminating
Teller 3 interrupted
Teller 3 terminating
Teller 0 interrupted
Teller 0 terminating
CustomerGenerator interrupted
CustomerGenerator terminating

</spoiler>
Объекты Customer очень просты; они содержат только поле данных final int. Так как эти объекты никогда не изменяют своего состояния, они являются объектами, доступными только для чтения, и поэтому требуют синхронизации или использования volatile. Вдобавок каждая задача Teller удаляет из очереди ввода только один объект Customer и работает с ним до завершения, поэтому задачи все равно будут работать с Customer последовательно.

Класс CustomerLine представляет собой общую очередь, в которой клиенты ожидают обслуживания. Он реализован в виде очереди ArrayBlockingQueue с методом toString(), который выводит результаты в желаемом формате. Генератор CustomerGenerator присоединяется к CustomerLine и ставит объекты Customer в очередь со случайными интервалами.

Teller извлекает клиентов Customer из CustomerLine и обрабатывает их последовательно, подсчитывая количество клиентов, обслуженных за текущую смену. Если клиентов не хватает, его можно перевести на другую работу (doSomethingElse()), а при появлении большого количества клиентов — снова вернуть на обслуживание очереди методом serveCustomerLine(). Чтобы приказать следующему кассиру вернуться к очереди, метод compareTo() проверяет количество обслуженных клиентов, чтобы приоритетная очередь автоматически ставила в начало кассира, работавшего меньше других.

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

Резюме

В этой главе я постарался изложить основы многопоточного программирования с использованием потоков Java. Прочитав ее, читатель должен понять следующее:


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

Очень важно понимать, когда рационально использовать параллельное выполнение, а когда этого делать не стоит. Основные причины для его использования:


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

Классический пример распределения ресурсов — использование процессора во время ожидания завершения операций ввода/вывода. Классический пример чуткого пользовательского интерфейса — отслеживание нажатий кнопки «Пре­рвать» во время продолжительного процесса загрузки.

Дополнительным преимуществом потоков является то, что они заменяют «тяжелое» переключение контекста процессов (порядка 1000 и более инструкций) «легким» переключением контекста выполнения (около 100 инструкций). Так как все потоки процесса разделяют одно и то же пространство памяти, легкое переключение затрагивает только выполнение программы и локальные переменные. С другой стороны, чередование процессов — тяжелое переключение контекста — требует обновления всего пространства памяти.

Основные недостатки многозадачности:


  • Замедление программы, связанное с ожиданием освобождения блокированных ресурсов.
  • Дополнительная нагрузка на процессор для управления потоками.
  • Совершенно ненужная сложность, являющаяся следствием неудачных решений при проектировании программы.
  • Аномальные ситуации: взаимные блокировки, конфликты доступа, гонки и т. д.
  • Непоследовательное поведение на различных платформах. Например, при разработке некоторых примеров для данной книги я обнаружил ситуации гонки, быстро проявлявшиеся на некоторых компьютерах, но незаметные на других.

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

Вдобавок многозадачное программирование сродни искусству. Язык Java существует для того, чтобы вы могли свободно создавать столько объектов, сколько вам нужно для решения вашей задачи — по крайней мере, в теории это так. (Например, создание миллионов объектов для проведения проекционно-разностного анализа вряд ли будет иметь смысл в Java.) Однако оказывается, что количество потоков упирается в определенный «потолок», так как после превышения этой-границы потоки становятся неподатливыми. Это критическое число трудно определить, зачастую оно зависит от операционной системы и виртуальной машины Java, значение может находиться где-то в районе сотни, а может исчисляться тысячами. Если для решения своей задачи вам требуется небольшая группа потоков, это ограничение не актуально, но при разработке больших программ оно может создать затруднения.

]]>
Книги по Java https://linexp.ru?id=4736 Wed, 29 Jun 2022 14:10:39 GMT
<![CDATA[Глава 5 Thinking in Java 4th edition]]> ИНИЦИАЛИЗАЦИЯ И ЗАВЕРШЕНИЕВ ходе компьютерной революции выяснилось, что основной причиной чрезмерных затрат в программировании является «небезопасное» программирование. Основные проблемы с безопасностью относятся к инициализации и завершению. Очень многие ошибки при программировании на языке C обусловлены неверной инициализацией переменных. Это особенно часто происходит при работе с библиотеками, когда пользователи не знают, как нужно инициализировать компонент библиотеки, или забывают это сделать.

ИНИЦИАЛИЗАЦИЯ И ЗАВЕРШЕНИЕ

В ходе компьютерной революции выяснилось, что основной причиной чрезмерных затрат в программировании является «небезопасное» программирование. Основные проблемы с безопасностью относятся к инициализации и завершению. Очень многие ошибки при программировании на языке C обусловлены неверной инициализацией переменных. Это особенно часто происходит при работе с библиотеками, когда пользователи не знают, как нужно инициализировать компонент библиотеки, или забывают это сделать. Завершение — очень акту­альная проблема; слишком легко забыть об элементе, когда вы закончили с ним работу и его дальнейшая судьба вас не волнует. В этом случае ресурсы, занимаемые элементом, не освобождаются, и в программе может возникнуть нехватка ресурсов (прежде всего памяти).

В C++ появилось понятие конструктора — специального метода, который вызывается при создании нового объекта. Конструкторы используются и в Java; к тому же в Java есть сборщик мусора, который автоматически освобождает ресурсы, когда объект перестает использоваться. В этой главе рассматриваются вопросы инициализации и завершения, а также их поддержка в Java.

Конструктор гарантирует инициализацию

Конечно, можно создать особый метод, назвать его initialize() и включить во все ваши классы. Имя метода подсказывает пользователю, что он должен вызвать этот метод, прежде чем работать с объектом. К сожалению, это означает, что пользователь должен постоянно помнить о необходимости вызова данного метода. В Java разработчик класса может в обязательном порядке выполнить инициализацию каждого объекта при помощи специального метода, называемого конструктором. Если у класса имеется конструктор, Java автоматически
вызывает его при создании объекта, перед тем как пользователи смогут обратиться к этому объекту. Таким образом, инициализация объекта гарантирована.

Как должен называться конструктор? Здесь есть две тонкости. Во-первых, любое имя, которое вы используете, может быть задействовано при определении членов класса; так возникает потенциальный конфликт имен. Во-вторых, за вызов конструктора отвечает компилятор, поэтому он всегда должен знать, какой именно метод следует вызвать. Реализация конструктора в C++ кажется наиболее простым и логичным решением, поэтому оно использовано и в Java: имя конструктора совпадает с именем класса. Смысл такого решения очевиден — именно такой метод способен автоматически вызываться при инициализации.
Рассмотрим определение простого класса с конструктором:

//: initialization/SimpleConstructor.java
// Demonstration of a simple constructor.
// Демонстрация простого конструктора
class Rock {
Rock() { // This is the constructor
System.out.print("Rock ");
}
}
 
public class SimpleConstructor {
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new Rock();
}
}

<spoiler text="Output:">

Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock

</spoiler>
Теперь при создании объекта:

 new Rock( );

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

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

//: initialization/SimpleConstructor2.java
// Constructors can have arguments.
// Конструкторы могут получать аргументы
class Rock2 {
Rock2(int i) {
System.out.print("Rock " + i + " ");
}
}
 
public class SimpleConstructor2 {
public static void main(String[] args) {
for(int i = 0; i < 8; i++)
new Rock2(i);
}
}

<spoiler text="Output:">

Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7

</spoiler>
В аргументах конструктора передаются параметры для инициализации объекта. Например, если у класса Tree (дерево) имеется конструктор, который получает в качестве аргумента целое число, обозначающее высоту дерева, то объекты Tree будут создаваться следующим образом:

 Tree t = new Тrее(12), // 12-метровое дерево

Если Tree(int initialHeight) является единственным конструктором класса, то компилятор не позволит создавать объекты Tree каким-либо другим способом.
Конструкторы устраняют большой пласт проблем и упрощают чтение кода. В предыдущем фрагменте кода не встречаются явные вызовы метода, подобного initialize(), который концептуально отделен от создания. В Java создание и инициализация являются неразделимыми понятиями — одно без другого невозможно.

Конструктор — не совсем обычный метод, так как у него отсутствует возвращаемое значение. Это ощутимо отличается даже от случая с возвратом значения void, когда метод ничего не возвращает, но при этом все же можно заставить его вернуть что-нибудь другое. Конструкторы не возвращают никогда и ничего (оператор new возвращает ссылку на вновь созданный объект, но сами конструкторы не имеют выходного значения). Если бы у них существовало возвращаемое значение и его можно было бы выбирать, то компилятору пришлось бы как-то объяснять, что же делать с этим значением.

Перегрузка методов

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

Проблемы возникают при перенесении нюансов человеческого языка в языки программирования. Часто одно и то же слово имеет несколько разных значений — оно перегружено. Это полезно, особенно в отношении простых различий. Вы говорите «вымыть посуду», «вымыть машину» и «вымыть собаку». Было бы глупо вместо этого говорить «посудоМыть посуду», «машиноМыть машину» и «собакоМыть собаку» только для того, чтобы слушатель не утруждал себя выявлением разницы между этими действиями. Большинство человеческих языков несет избыточность, и даже при пропуске некоторых слов определить смысл не так сложно. Уникальные имена не обязательны — сказанное можно понять из контекста.

Большинство языков программирования (и в особенности C) требовали использования уникальных имен для всех функций. Иначе говоря, программа не могла содержать функцию print() для распечатки целых чисел и одноименную функцию для вывода вещественных чисел — каждая функция должна была иметь уникальное имя.

В Java (и в C++) также существует другой фактор, который заставляет использовать перегрузку имен методов: наличие конструкторов. Так как имя конструктора предопределено именем класса, оно может быть только единственным. Но что, если вы захотите создавать объекты разными способами? Допустим, вы создаете класс с двумя вариантами инициализации: либо стандартно, либо на основании из некоторого файла. В этом случае необходимость двух конструкторов очевидна: один из них не имеет аргументов (конструктор по умолчанию, также называемый конструктором без аргументов (no-arg)), а другой получает в качестве аргумента строку с именем файла. Оба они являются полноценными конструкторами, и поэтому должны называться одинаково — именем класса.

Здесь перегрузка методов (overloading) однозначно необходима, чтобы мы могли использовать методы с одинаковыми именами, но с разными аргументами. И хотя перегрузка методов обязательна только для конструкторов, она удобна в принципе и может быть применена к любому методу.
Следующая программа показывает пример перегрузки как конструктора, так и обычного метода:

//: initialization/Overloading.java
// Demonstration of both constructor
// and ordinary method overloading.
// Демонстрация перегрузки конструкторов наряду
// с перегрузкой обычных методов
import static net.mindview.util.Print.*;
 
class Tree {
int height;
Tree() {
print("Planting a seedling");
height = 0;
}
Tree(int initialHeight) {
height = initialHeight;
print("Creating new Tree that is " +
height + " feet tall");
}
void info() {
print("Tree is " + height + " feet tall");
}
void info(String s) {
print(s + ": Tree is " + height + " feet tall");
}
}
 
public class Overloading {
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
Tree t = new Tree(i);
t.info();
t.info("overloaded method");
}
// Перегруженный конструктор:
new Tree();
}
}

<spoiler text="Output:">

Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling

</spoiler>
Объект Tree (дерево) может быть создан или в форме ростка (без аргументов), или в виде «взрослого растения» с некоторой высотой. Для этого в классе определяются два конструктора; один используется по умолчанию, а другой получает аргумент с высотой дерева.
Возможно, вы захотите вызывать метод info() несколькими способами. Например, вызов с аргументом-строкой info(String) используется при необходимости вывода дополнительной информации, а вызов без аргументов info() — когда дополнений к сообщению метода не требуется. Было бы странно давать два разных имени методам, когда их схожесть столь очевидна. К счастью, перегрузка методов позволяет использовать одно и то же имя для обоих методов.

Различение перегруженных методов

Если у методов одинаковые имена, как Java узнает, какой именно из них вызывается? Ответ прост: каждый перегруженный метод должен иметь уникальный список типов аргументов.

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

Даже разного порядка аргументов достаточно для того, чтобы методы считались разными (хотя описанный далее подход почти не используется, так как он усложняет сопровождение программного кода):

//: initialization/OverloadingOrder.java
// Overloading based on the order of the arguments.
// Перегрузка, основанная на порядке
// следования аргументов
import static net.mindview.util.Print.*;
 
public class OverloadingOrder {
static void f(String s, int i) {
print("String: " + s + ", int: " + i);
}
static void f(int i, String s) {
print("int: " + i + ", String: " + s);
}
public static void main(String[] args) {
f("String first", 11);
f(99, "Int first");
}
}

<spoiler text="Output:">

String: String first, int: 11
int: 99, String: Int first

</spoiler>
Два метода f() имеют одинаковые аргументы с разным порядком следования, и это различие позволяет идентифицировать метод.

Перегрузка с примитивами

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

//: initialization/PrimitiveOverloading.java
// Promotion of primitives and overloading.
// Повышение примитивных типов и перегрузка
import static net.mindview.util.Print.*;
 
public class PrimitiveOverloading {
void f1(char x) { printnb("f1(char) "); }
void f1(byte x) { printnb("f1(byte) "); }
void f1(short x) { printnb("f1(short) "); }
void f1(int x) { printnb("f1(int) "); }
void f1(long x) { printnb("f1(long) "); }
void f1(float x) { printnb("f1(float) "); }
void f1(double x) { printnb("f1(double) "); }
 
void f2(byte x) { printnb("f2(byte) "); }
void f2(short x) { printnb("f2(short) "); }
void f2(int x) { printnb("f2(int) "); }
void f2(long x) { printnb("f2(long) "); }
void f2(float x) { printnb("f2(float) "); }
void f2(double x) { printnb("f2(double) "); }
 
void f3(short x) { printnb("f3(short) "); }
void f3(int x) { printnb("f3(int) "); }
void f3(long x) { printnb("f3(long) "); }
void f3(float x) { printnb("f3(float) "); }
void f3(double x) { printnb("f3(double) "); }
 
void f4(int x) { printnb("f4(int) "); }
void f4(long x) { printnb("f4(long) "); }
void f4(float x) { printnb("f4(float) "); }
void f4(double x) { printnb("f4(double) "); }
 
void f5(long x) { printnb("f5(long) "); }
void f5(float x) { printnb("f5(float) "); }
void f5(double x) { printnb("f5(double) "); }
 
void f6(float x) { printnb("f6(float) "); }
void f6(double x) { printnb("f6(double) "); }
 
void f7(double x) { printnb("f7(double) "); }
 
void testConstVal() {
printnb("5: ");
f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); print();
}
void testChar() {
char x = 'x';
printnb("char: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
void testByte() {
byte x = 0;
printnb("byte: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
void testShort() {
short x = 0;
printnb("short: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
void testInt() {
int x = 0;
printnb("int: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
void testLong() {
long x = 0;
printnb("long: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
void testFloat() {
float x = 0;
printnb("float: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
void testDouble() {
double x = 0;
printnb("double: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
}
public static void main(String[] args) {
PrimitiveOverloading p =
new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
}

<spoiler text="Output:">

5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double)
short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)
int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)
float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)
double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)

</spoiler>
Если вы рассмотрите результат работы программы, то увидите, что константа 5 трактуется как int, поэтому если есть перегруженный метод, принимающий аргумент типа int, то он и используется. Во всех остальных случаях, если имеется тип данных, «меньший», чем требуется для существующего метода, то этот тип данных повышается соответственным образом. Только тип char ведет себя несколько иначе по той причине, что, если метода с параметром char нет, этот тип приводится сразу к типу int, а не к промежуточным типам byte или short.

Что же произойдет, если ваш аргумент «больше», чем аргумент, требующийся в перегруженном методе? Ответ можно найти в модификации рассмотренной программы:

//: initialization/Demotion.java
// Demotion of primitives and overloading.
// Понижение примитивов и перегрузка.
import static net.mindview.util.Print.*;
 
public class Demotion {
void f1(char x) { print("f1(char)"); }
void f1(byte x) { print("f1(byte)"); }
void f1(short x) { print("f1(short)"); }
void f1(int x) { print("f1(int)"); }
void f1(long x) { print("f1(long)"); }
void f1(float x) { print("f1(float)"); }
void f1(double x) { print("f1(double)"); }
 
void f2(char x) { print("f2(char)"); }
void f2(byte x) { print("f2(byte)"); }
void f2(short x) { print("f2(short)"); }
void f2(int x) { print("f2(int)"); }
void f2(long x) { print("f2(long)"); }
void f2(float x) { print("f2(float)"); }
 
void f3(char x) { print("f3(char)"); }
void f3(byte x) { print("f3(byte)"); }
void f3(short x) { print("f3(short)"); }
void f3(int x) { print("f3(int)"); }
void f3(long x) { print("f3(long)"); }
 
void f4(char x) { print("f4(char)"); }
void f4(byte x) { print("f4(byte)"); }
void f4(short x) { print("f4(short)"); }
void f4(int x) { print("f4(int)"); }
 
void f5(char x) { print("f5(char)"); }
void f5(byte x) { print("f5(byte)"); }
void f5(short x) { print("f5(short)"); }
 
void f6(char x) { print("f6(char)"); }
void f6(byte x) { print("f6(byte)"); }
 
void f7(char x) { print("f7(char)"); }
 
void testDouble() {
double x = 0;
print("double argument:");
f1(x);f2((float)x);f3((long)x);f4((int)x);
f5((short)x);f6((byte)x);f7((char)x);
}
public static void main(String[] args) {
Demotion p = new Demotion();
p.testDouble();
}
}

<spoiler text="Output:">

double argument:
f1(double)
f2(float)
f3(long)
f4(int)
f5(short)
f6(byte)
f7(char)

</spoiler>
Здесь методы требуют сужения типов данных. Если ваш аргумент «шире», необходимо явно привести его к нужному типу. В противном случае компилятор выведет сообщение об ошибке.

Перегрузка по возвращаемым значениям

Вполне логично спросить, почему при перегрузке используются только имена классов и списки аргументов? Почему не идентифицировать методы по их возвращаемым значениям? Следующие два метода имеют одинаковые имена и ар­гументы, но их легко отличить друг от друга:

 void f() {} 
int f() {}

Такой подход прекрасно сработает в ситуации, в которой компилятор может однозначно выбрать нужную версию метода, например: int х = f(). Однако возвращаемое значение при вызове метода может быть проигнорировано; это часто называется вызовом метода для получения побочного эффекта, так как метод вызывается не для естественного результата, а для каких-то других целей. Допустим, метод вызывается следующим способом:

 f();

Как здесь Java определит, какая из версий метода f() должна выполняться? И поймет ли читатель программы, что происходит при этом вызове? Именно из-за подобных проблем перегруженные методы не разрешается различать по возвращаемым значениям.

Конструкторы по умолчанию

Как упоминалось ранее, конструктором по умолчанию называется конструктор без аргументов, применяемый для создания «типового» объекта. Если созданный вами класс не имеет конструктора, компилятор автоматически добавит конструктор по умолчанию. Например:

//: initialization/DefaultConstructor.java
class Bird {}
 
public class DefaultConstructor {
public static void main(String[] args) {
Bird b = new Bird(); // Default!
}
}

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

//: initialization/NoSynthesis.java
class Bird2 {
Bird2(int i) {}
Bird2(double d) {}
}
 
public class NoSynthesis {
public static void main(String[] args) {
//! Bird2 b = new Bird2(); // No default
Bird2 b2 = new Bird2(1);
Bird2 b3 = new Bird2(1.0);
}
}

Теперь при попытке выполнения new Bird2() компилятор заявит, что не может найти конструктор, подходящий по описанию. Получается так: если определения конструкторов отсутствуют, компилятор скажет: «Хотя бы один конструктор необходим, позвольте создать его за вас». Если же вы записываете конструктор явно, компилятор говорит: «Вы написали конструктор, а следовательно, знаете, что вам нужно; и если вы создали конструктор по умолчанию, значит, он вам и не нужен».

Ключевое слово this

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

//: initialization/BananaPeel.java
class Banana { void peel(int i) { /* ... */ } }
 
public class BananaPeel {
public static void main(String[] args) {
Banana a = new Banana(),
b = new Banana();
a.peel(1);
b.peel(2);
}
}

Если существует только один метод с именем peel(), как этот метод узнает, для какого объекта он вызывается — а или b?

Чтобы программа могла записываться в объектно-ориентированном стиле, основанном на «отправке сообщений объектам», компилятор выполняет для вас некоторую тайную работу. При вызове метода peel() передается скрытый первый аргумент — не что иное, как ссылка на используемый объект. Таким образом, вызовы указанного метода на самом деле можно представить как:

 Banana.рееl(a,1);
Banana.peel(b,2);

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

Предположим, во время выполнения метода вы хотели бы получить ссылку на текущий объект. Так как эта ссылка передается компилятором скрытно, идентификатора для нее не существует. Но для решения этой задачи существует ключевое слово — this. Ключевое слово this может использоваться только внутри не-статического метода и предоставляет ссылку на объект, для которого был вызван метод. Обращаться с ней можно точно так же, как и с любой другой ссылкой на объект. Помните, что при вызове метода вашего класса из другого метода этого класса this вам не нужно; просто укажите имя метода. Текущая ссылка this будет автоматически использована в другом методе. Таким образом, продолжая сказанное:

//: initialization/Apricot.java
public class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ }
}

Внутри метода pit() можно использовать запись this.pick(), но в этом нет необходимости. Компилятор сделает это автоматически. Ключевое слово this употребляется только в особых случаях, когда вам необходимо явно сослаться на текущий объект. Например, оно часто применяется для возврата ссылки на текущий объект в команде return:

//: initialization/Leaf.java
// Simple use of the "this" keyword.
 
public class Leaf {
int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
}

<spoiler text="Output:">

i = 3

</spoiler>
Так как метод increment() возвращает ссылку на текущий объект посредством ключевого слова this, над одним и тем же объектом легко можно провести множество операций.
Ключевое слово this также может пригодиться для передачи текущего объекта другому методу:

//: initialization/PassingThis.java
 
class Person {
public void eat(Apple apple) {
Apple peeled = apple.getPeeled();
System.out.println("Yummy");
}
}
 
class Peeler {
static Apple peel(Apple apple) {
// ... remove peel
return apple; // Peeled
}
}
 
class Apple {
Apple getPeeled() { return Peeler.peel(this); }
}
 
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple());
}
}

<spoiler text="Output:">

Yummy

</spoiler>
Класс Apple вызывает Peeler.peel() — вспомогательный метод, который по какой-то причине должен быть оформлен как внешний по отношению к Apple (может быть, он должен обслуживать несколько разных классов, и вы хотите избежать дублирования кода). Для передачи текущего объекта внешнему методу используется ключевое слово this.

Вызов конструкторов из конструкторов

Если вы пишете для класса несколько конструкторов, иногда бывает удобно вызвать один конструктор из другого, чтобы избежать дублирования кода. Такая операция проводится с использованием ключевого слова this.
Обычно при употреблении this подразумевается «этот объект» или «текущий объект», и само слово является ссылкой на текущий объект. В конструкторе ключевое слово this имеет другой смысл: при использовании его со списком аргументов вызывается конструктор, соответствующий данному списку. Таким образом, появляется возможность прямого вызова других конструкторов:

//: initialization/Flower.java
// Calling constructors with "this"
import static net.mindview.util.Print.*;
 
public class Flower {
int petalCount = 0;
String s = "initial value";
Flower(int petals) {
petalCount = petals;
print("Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss) {
print("Constructor w/ String arg only, s = " + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//! this(s); // Can't call two!
this.s = s; // Another use of "this"
print("String & int args");
}
Flower() {
this("hi", 47);
print("default constructor (no args)");
}
void printPetalCount() {
//! this(11); // Not inside non-constructor!
print("petalCount = " + petalCount + " s = "+ s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.printPetalCount();
}
}

<spoiler text="Output:">

Constructor w/ int arg only, petalCount= 47
String & int args
default constructor (no args)
petalCount = 47 s = hi

</spoiler>
Конструктор Flower(String s, int petals) показывает, что при вызове одного конструктора через this вызывать второй запрещается. Вдобавок вызов другого конструктора должен быть первой выполняемой операцией, иначе компилятор выдаст сообщение об ошибке.

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

Метод printPetalCount() показывает, что компилятор не разрешает вызывать конструктор из обычного метода; это разрешено только в конструкторах.

Значение ключевого слова static

Ключевое слово this поможет лучше понять, что же фактически означает объявление статического (static) метода. У таких методов не существует ссылки this. Вы не в состоянии вызывать нестатические методы из статических (хотя обратное позволено), и статические методы можно вызывать для имени класса, без каких-либо объектов. Статические методы отчасти напоминают глобальные функции языка C, но с некоторыми исключениями: глобальные функции не разрешены в Java, и создание статического метода внутри класса дает ему право на доступ к другим статическим методам и полям.

Некоторые люди утверждают, что статические методы со своей семантикой глобальной функции противоречат объектно-ориентированной парадигме; в случае использования статического метода вы не посылаете сообщение объекту, поскольку отсутствует ссылка this. Возможно, что это справедливый упрек, и если вы обнаружите, что используете слишком много статических методов, то стоит пересмотреть вашу стратегию разработки программ. Однако ключевое слово static полезно на практике, и в некоторых ситуациях они определенно необходимы. Споры же о «чистоте ООП» лучше оставить теоретикам.

Очистка: финализация и сборка мусора

Программисты помнят и знают о важности инициализации, но часто забывают о значимости «приборки». Да и зачем, например, «прибирать» после использования обычной переменной int? Но при использовании программных библиотек «просто забыть» об объекте после завершения его работы не всегда безопасно. Конечно, в Java существует сборщик мусора, освобождающий память от ненужных объектов. Но представим себе необычную ситуацию. Предположим, что объект выделяет «специальную» память без использования оператора new. Сборщик мусора умеет освобождать память, выделенную new, но ему неизвестно, как следует очищать специфическую память объекта.

Для таких ситуаций в Java предусмотрен метод finalize(), который вы можете определить в вашем классе. Вот как он должен работать: когда сборщик мусора готов освободить память, использованную вашим объектом, он для начала вызывает метод finalize(). Hо только после этого освобождает занимаемую объектом память. Таким образом, метод finalize() позволяет выполнять завершающие действия во время работы сборщика мусора.

Все это может создать немало проблем для программистов, особенно для программистов на языке C++, так как они могут спутать метод finalize() с деструктором языка C++ — функцией, всегда вызываемой перед разрушением объекта. Но здесь очень важно понять разницу между Java и C++, поскольку в C++ объекты разрушаются всегда (в правильно написанной программе), в то время как в Java объекты удаляются сборщиком мусора не во всех случаях. Другими словами:

ВНИМАНИЕ----------------------------------------------------

  • 1. Ваши объекты могут быть и не переданы сборщику мусора.
  • 2. Сборка мусора не является удалением.

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

Для чего нужен метод finalize()?

Итак, если метод finalize() не стоит использовать для проведения стандартных операций завершения, то для чего же он нужен? Запомните третье правило:

ВНИМАНИЕ ------------------------------------------

  • 3. Процесс сборки мусора относится только к памяти.

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

Но значит ли это, что если ваш объект содержит другие объекты, то в finalize() они должны явно удаляться? Нет — сборщик мусора займется освобождением памяти и удалением объектов вне зависимости от способа их создания. Получается, что использование метода finalize() ограничено особыми случаями, в которых ваш объект размещается в памяти необычным способом, не связанным с прямым созданием экземпляра. Но, если в Java все является объектом, как же тогда такие особые случаи происходят?

Похоже, что поддержка метода finalize() была введена в язык, чтобы сделать возможными операции с памятью в стиле C, с привлечением нестандартных механизмов выделения памяти. Это может произойти в основном при использовании методов, предоставляющих способ вызова He-Java-кода из программы на Java. C и C++ пока являются единственными поддерживаемыми языками, но, так как для них таких ограничений нет, в действительности программа Java может вызвать любую процедуру или функцию на любом языке. Во внешнем коде можно выделить память вызовом функций C, относящихся к семейству malloc(). Если не воспользоваться затем функцией free(), произойдет «утечка» памяти. Конечно, функция free() тоже принадлежит к C и C++, поэтому придется в методе finalize() провести вызов еще одного «внешнего» метода.

После прочтения этого абзаца у вас, скорее всего, сложилось мнение, что метод finalize() используется нечасто. И правда, это не то место, где следует проводить рутинные операции очистки. Но где же тогда эти обычные операции будут уместны?

Очистка — ваш долг

Для очистки объекта его пользователю нужно вызвать соответствующий метод в той точке, где эти завершающие действия по откреплению и должны осуществляться. Звучит просто, но немного противоречит традиционным представлениям о деструкторах C++. В этом языке все объекты должны уничтожаться. Если объект C++ создается локально (то есть в стеке, что невозможно в Java), то удаление и вызов деструктора происходит у закрывающей фигурной скобки, ограничивающей область действия такого объекта. Если же объект создается оператором new (как в Java), то деструктор вызывается при выполнении программистом оператора C++ delete (не имеющего аналога в Java). А когда программист на C++ забывает вызвать оператор delete, деструктор не вызывается и происходит «утечка» памяти, к тому же остальные части объекта не проходят необходимой очистки. Такого рода ошибки очень сложно найти и устранить, и они являются веским доводом в пользу перехода с C++ на Java.
Java не позволяет создавать локальные объекты — все объекты должны быть результатом действия оператора new. Но в Java отсутствует аналог оператора delete, вызываемого для разрушения объекта, так как сборщик мусора и без того выполнит освобождение памяти. Значит, в несколько упрощенном изложении можно утверждать, что деструктор в Java отсутствует из-за присутствия сборщика мусора. Но в процессе чтения книги вы еще не раз убедитесь, что наличие сборщика мусора не устраняет необходимости в деструкторах или их аналогах. (И никогда не стоит вызывать метод finalize() непосредственно, так как этот подход не решает проблему.) Если же потребуется провести какие-то завершающие действия, отличные от освобождения памяти, все же придется явно вызвать подходящий метод, выполняющий функцию деструктора C++, но это уже не так удобно, как встроенный деструктор.
Помните, что ни сборка мусора, ни финализация не гарантированы. Если виртуальная машина Java (Java Virtual Machine, JVM) далека от критической точки расходования ресурсов, она не станет тратить время на освобождение памяти с использованием сборки мусора.

Условие «готовности»

В общем, вы не должны полагаться на вызов метода finalize() — создавайте отдельные «функции очистки» и вызывайте их явно. Скорее всего, finalize() пригодится только в особых ситуациях нестандартного освобождения памяти, с которыми большинство программистов никогда не сталкивается. Тем не менее существует очень интересное применение метода finalize(), не зависящее от того, вызывается ли он каждый раз или нет. Это проверка условия готовности объекта.

В той точке, где объект становится ненужным — там, где он готов к проведению очистки, — этот объект должен находиться в состоянии, когда освобождение закрепленной за ним памяти безопасно. Например, если объект представляет открытый файл, то он должен быть соответствующим образом закрыт, перед тем как его «приберет» сборщик мусора. Если какая-то часть объекта не будет готова к уничтожению, результатом станет ошибка в программе, которую затем очень сложно обнаружить. Ценность finalize() в том и состоит, что он позволяет вам обнаружить такие ошибки, даже если и не всегда вызывается. Единожды проведенная финализация явным образом укажет на ошибку, а это все, что вам нужно.
Простой пример использования данного подхода:

//: initialization/TerminationCondition.java
// Использование finalize() для выявления объекта,
// не осуществившего необходимой финализации
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
protected void finalize() {
if(checkedOut)
System.out.println("Error: checked out");
// Обычно это делается так-:
// super.finalize(); // Вызов версии базового класса
&##125;
}
 
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Правильная очистка-:
novel.checkIn();
// Теряем ссылку, забыли про очистку:
new Book(true);
// запрос JVM на сборку мусора и финализация :
System.gc();
}
}

<spoiler text="Output:">

Error: checked out

</spoiler>
«Условие готовности» состоит в том, что все объекты Book должны быть «сняты с учета» перед предоставлением их в распоряжение сборщика мусора, но в методе main() программист ошибся и не отметил один из объектов Book. Если бы в методе finalize() не было проверки на условие «готовности», такую оплошность было бы очень сложно обнаружить.

Заметьте, что для проведения принудительной финализации был использован метод System.gc(). Но даже если бы его не было, с высокой степенью вероятности можно сказать, что «утерянный» объект Book рано или поздно будет обнаружен в процессе исполнения программы (в этом случае предполагается, что программе будет выделено столько памяти, сколько нужно, чтобы сборщик мусора приступил к своим обязанностям).

Обычно следует считать, что версия finalize() базового класса делает что-то важное, и вызывать ее в синтаксисе super, как показано в Book.finalize(). В данном примере вызов закомментирован, потому что он требует обработки исключений, а эта тема нами еще не рассматривалась.

Как работает сборщик мусора

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

Представтьте кучу языка C++ в виде лужайки, где каждый объект «застолбил» свой собственный участок. Позднее площадка освобождается для повторного использования. В некоторых виртуальных машинах Java куча выглядит совсем иначе; она скорее похоже на ленту конвейера, которая передвигается вперед при создании нового объекта. А это значит, что скорость выделения хранилища для объекта оказывается весьма высокой. «Указатель кучи» просто передвигается вперед в «невозделанную» территорию, и по эффективности этот процесс близок к выделению памяти в стеке C++. (Конечно, учет выделенного пространства сопряжен с небольшими издержками, но их никоим образом нельзя сравнить с затратами, возникающими при поиске свободного блока в памяти.)

Конечно, использование кучи в режиме «ленты конвейера» не может продолжаться бесконечно, и рано или поздно память станет сильно фрагментирована (что заметно снижает производительность), а затем и вовсе исчерпается. Как раз здесь в действие вступает сборщик мусора; во время своей работы он компактно размещает объекты кучи, как бы смещая «указатель кучи» ближе к началу «ленты», тем самым предотвращая фрагментацию памяти. Сборщик мусора реструктуризует внутреннее расположение объектов в памяти и позволит получить высокоскоростную модель кучи для резервирования памяти.

Чтобы понять, как работает сборка мусора в Java, необходимо узнать, как устроены реализации сборщиков мусора (СМ) в других системах. Простой, но медленный механизм СМ называется подсчетом ссылок. С каждым объектом хранится счетчик ссылок на него, и всякий раз при присоединении новой ссылки к объекту этот счетчик увеличивается. Каждый раз при выходе ссылки из области действия или установке ее значения в null счетчик ссылок уменьшается. Таким образом, подсчет ссылок создает небольшие, но постоянные издержки во время работы вашей программы. Сборщик мусора перебирает объект за объектом списка; обнаружив объект с нулевым счетчиком, он освобождает ресурсы, занимаемые этим объектом. Но существует одна проблема — если объекты содержат циклические ссылки друг на друга, их счетчики ссылок не обнуляются, хотя на самом деле объекты уже являются «мусором». Обнаружение таких «циклических» групп является серьезной работой и отнимает у сборщика мусора достаточно времени. Подсчет ссылок часто используется для объяснения принципов процесса сборки мусора, но, судя по всему, он не используется ни в одной из виртуальных машин Java.

В более быстрых схемах сборка мусора не зависит от подсчета ссылок. Вместо этого она опирается на идею, что любой существующий объект прослеживается до ссылки, находящейся в стеке или в статической памяти. Цепочка проверки проходит через несколько уровней объектов. Таким образом, если начать со стека и статического хранилища, мы обязательно доберемся до всех используемых объектов. Для каждой найденной ссылки надо взять объект, на который она указывает, и отследить все ссылки этого объекта; при этом выявляются другие объекты, на которые они указывают, и так далее, пока не будет проверена вся инфраструктура ссылок, берущая начало в стеке и статической памяти. Каждый объект, обнаруженный в ходе поиска, все еще используется в системе. Заметьте, что проблемы циклических ссылок не существует — такие ссылки просто не обнаруживаются, и поэтому становятся добычей сборщика мусора автоматически.

В описанном здесь подходе работает адаптивный механизм сбора мусора, при котором JVM обращается с найденными используемыми объектами согласно определенному варианту действий. Один из таких вариантов называется ос- тановить-и-копировать. Смысл термина понятен: работа программы временно приостанавливается (эта схема не поддерживает сборку мусора в фоновом режиме). Затем все найденные «живые» (используемые) объекты копируются из одной кучи в другую, а «мусор» остается в первой. При копировании объектов в новую кучу они размещаются в виде компактной непрерывной цепочки, высвобождая пространство в куче {и позволяя удовлетворять заказ на новое хранилище простым перемещением указателя).

Конечно, когда объект перемещается из одного места в другое, все ссылки, указывающие на него, должны быть изменены. Ссылки в стеке или в статическом хранилище переопределяются сразу, но могут быть и другие ссылки на этот объект, которые исправляются позже, во время очередного «прохода». Исправление происходит по мере нахождения ссылок.

Существует два фактора, из-за которых «копирующие сборщики» обладают низкой эффективностью. Во-первых, в системе существует две кучи, и вы «перелопачиваете» память то туда, то сюда между двумя отдельными кучами, при этом половина памяти тратится впустую. Некоторые JVM пытаются решить эту проблему, выделяя память для кучи небольшими порциями по мере необходимости, а затем просто копируя одну порцию в другую.


Второй вопрос — копирование. Как только программа перейдет в фазу стабильной работы, она обычно либо становится «безотходной», либо производит совсем немного «мусора». Несмотря на это, копирующий сборщик все равно не перестанет копировать память из одного места в другое, что расточительно. Некоторые JVM определяют, что новых «отходов» не появляется, и переключаются на другую схему («адаптивная» часть). Эта схема называется пометить-и-убрать (удалить), и именно на ней работали ранние версии виртуальных машин фирмы Sun. Для повсеместного использования вариант «пометить-и-убрать» чересчур медлителен, но, когда известно, что нового «мусора» мало или вообще нет, он выполняется быстро.

Схема «пометить-и-убрать» использует ту же логику — проверка начинается со стека и статического хранилища, после чего постепенно обнаруживаются все ссылки на «живые» объекты. Однако каждый раз при нахождении объект поме­чается флагом, но еще продолжает существование. «Уборка» происходит только после завершения процесса проверки и пометки. Все «мертвые» объекты при этом удаляются. Но копирования не происходит, и если сборщик решит «упаковать» фрагментированную кучу, то делается это перемещением объектов внутри нее.

Идея «остановиться-и-копировать» несовместима с фоновым процессом сборки мусора; в начале уборки программа останавливается. В литературе фирмы Sun можно найти немало заявлений о том, что сборка мусора является фоновым процессом с низким приоритетом, но оказывается, что реализации в таком виде (по крайней мере в первых реализациях виртуальной машины Sun) в действительности не существует. Вместо этого сборщик мусора от Sun начинал выполнение только при нехватке памяти. Схема «пометить-и-убрать» также требует остановки программы.

Как упоминалось ранее, в описываемой здесь виртуальной машине память выделяется большими блоками. При создании большого объекта ему выделяется собственный блок. Строгая реализация схемы «остановиться-и-копировать» требует, чтобы каждый используемый объект из исходной кучи копировался в новую кучу перед освобождением памяти старой кучи, что сопряжено с большими перемещениями памяти. При работе с блоками памяти СМ использует незанятые блоки для копирования по мере их накопления. У каждого блока имеется счетчик поколений, следящий за использованием блока. В обычной ситуации «упаковываются» только те блоки, которые были созданы после последней сборки мусора; для всех остальных блоков значение счетчика увеличивается при создании внешних ссылок. Такой подход годится для стандартной ситуации — создания множества временных объектов с коротким сроком жизни. Периодически производится полная очистка — большие блоки не копируются (только наращиваются их счетчики), но блоки с маленькими объектами копируются и «упаковываются». Виртуальная машина постоянно следит за эффективностью сборки мусора и, если она становится неэффективной, потому что в программе остались только долгоживущие объекты, переключается на схему «пометить-и-убрать». Аналогично JVM следит за успешностью схемы «пометить-и-убрать», и, когда куча становится излишне фрагментированной, СМ переключается обратно к схеме «остановиться-и-копировать». Это и есть адаптивный механизм.

Существуют и другие способы ускорения работы в JVM. Наиболее важные — это действия загрузчика и то, что называется компиляцией «на лету» (Just-In-Time, JIT). Компилятор JIT частично или полностью конвертирует программу в «родной» машинный код, благодаря чему последний не нуждается в обработке виртуальной машиной и может выполняться гораздо быстрее. При загрузке класса (обычно это происходит при первом создании объекта этого класса) система находит файл .class, и байт-код из этого файла переносится в память. В этот момент можно просто провести компиляцию JIT для кода класса, но такой подход имеет два недостатка: во-первых, это займет чуть больше времени, что вместе с жизненным циклом программы может серьезно отразиться на производительности. Во-вторых, увеличивается размер исполняемого файла (байт-код занимает гораздо меньше места в сравнении с расширенным кодом JIT), что может привести к подкачке памяти, и это тоже замедлит программу. Альтернативная схема отложенного вычисления подразумевает, что код JIT компилируется только тогда, когда это станет необходимо. Иначе говоря, код, который никогда не исполняется, не компилируется JIT. Новая технология Java HotSpot, встроенная в последние версии JDK, делает это похожим образом с применением последовательной оптимизации кода при каждом его выполнении. Таким образом, чем чаще выполняется код, тем быстрее он работает.

Инициализация членов класса

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

 void f() { int і;
і ++, // Ошибка - переменная і не инициализирована
}

вы получите сообщение об ошибке, указывающее на то, что переменная і не была инициализирована. Конечно, компилятор мог бы присваивать таким переменным значения по умолчанию, но данная ситуация больше похожа на ошибку программиста, и подобный подход лишь скрыл бы ее. Заставить программиста присвоить переменной значение по умолчанию — значит предотвратить ошибку в программе.

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

//: initialization/InitialValues.java
// Вывод начальных значений, присваиваемых по умолчанию.
import static net.mindview.util.Print.*;
 
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues() {
print("Data type Initial value");
print("boolean " + t);
print("char [" + c + "]");
print("byte " + b);
print("short " + s);
print("int " + i);
print("long " + l);
print("float " + f);
print("double " + d);
print("reference " + reference);
}
public static void main(String[] args) {
InitialValues iv = new InitialValues();
iv.printInitialValues();
/* Тут возможен следующий вариант:
new InitialValues().printInitialValues();
*/

}
}

<spoiler text="Output:">

Data type      Initial value
boolean false
char [ ]
byte 0

</spoiler>
Присмотритесь — даже если значения явно не указываются, они автоматически инициализируются. (Символьной переменной char присваивается значение ноль, которое отображается в виде пробела.) По крайней мере, нет опасности случайного использования неинициализированной переменной.

Если ссылка на объект, определямая внутри класса, не связывается с новым объектом, то ей автоматически присваивается специальное значение null (ключевое слово Java).

Явная инициализация

Что делать, если вам понадобится придать переменной начальное значение? Проще всего сделать это прямым присваиванием этой переменной значения в точке ее объявления в классе. (Заметьте, что в C++ такое действие запрещено, хотя его постоянно пытаются выполнить новички.) В следующем примере полям уже знакомого класса InitialValues присвоены начальные значения:

//: initialization/InitialValues2.java
// Явное определение начальных значений переменных.
public class InitialValues2 {
boolean bool = true;
char ch = 'x';
byte b = 47;
short s = 0xff;
int i = 999;
long lng = 1;
float f = 3.14f;
double d = 3.14159;
}

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

//: initialization/Measurement.java
class Depth {}
 
public class Measurement {
Depth d = new Depth();
// ...
}

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

//: initialization/MethodInit.java
public class MethodInit {
int i = f();
int f() { return 11; }
}

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

//: initialization/MethodInit2.java
public class MethodInit2 {
int i = f();
int j = g(i);
int f() { return 11; }
int g(int n) { return n * 10; }
}

a так нет:

//: initialization/MethodInit3.java
public class MethodInit3 {
//! int j = g(i); // Illegal forward reference
int i = f();
int f() { return 11; }
int g(int n) { return n * 10; }
}

Это одно из мест, где компилятор на полном основании выражает недовольство преждевременной ссылкой, поскольку ошибка связана с порядком инициализации, а не с компиляцией программы.
Описанный подход инициализации очень прост и прямолинеен. У него есть ограничение — все объекты типа InitialValues получат одни и те же начальные значения. Иногда вам нужно именно это, но в других ситуациях необходима большая гибкость.

Инициализация конструктором

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

//: initialization/Counter.java
public class Counter {
int i;
Counter() { i = 7; }
// ...
}

переменной і сначала будет присвоено значение 0, а затем уже 7. Это верно для всех примитивных типов и ссылок на объекты, включая те, которым задаются явные значения в точке определения. По этим причинам компилятор не пытается заставить вас инициализировать элементы в конструкторе, или в ином определенном месте, или перед их использованием — инициализация и так гарантирована.

Порядок инициализации

Внутри класса очередность инициализации определяется порядком следования переменных, объявленных в этом классе. Определения переменных могут быть разбросаны по разным определениям методов, но в любом случае переменные инициализируются перед вызовом любого метода — даже конструктора. Например:

//: initialization/OrderOfInitialization.java
// Демонстрирует порядок инициализации.
import static net.mindview.util.Print.*;
 
// При вызове конструктора для создания объекта
// Window выводится сообщение:
class Window {
Window(int marker) { print("Window(" + marker + ")"); }
}
 
class House {
Window w1 = new Window(1); // Перед конструктором
House() {
// Показывает, что выполняется конструктор:
print("House()");
w3 = new Window(33); // Повторная инициализация w3
}
Window w2 = new Window(2); // После конструктора
void f() { print("f()"); }
Window w3 = new Window(3); // В конце
}
 
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f(); // Показывает, что объект сконструирован
}
}

<spoiler text="Output:">

Window(1)
Window(2)
Window(3)
House()
Window(33)
f()

</spoiler>
В классе House определения объектов Window намеренно разбросаны, чтобы доказать, что все они инициализируются перед выполнением конструктора или каким-то другим действием. Вдобавок ссылка w3 заново проходит инициализацию в конструкторе.

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

Инициализация статических данных

Данные статических полей всегда существуют в единственном экземпляре, независимо от количества созданных объектов. Ключевое слово static не может применяться к локальным-переменным, только к полям. Если статическое поле относится к примитивному типу, при отсутствии явной инициализации ему присваивается значение по умолчанию. Если это ссылка на объект, то ей присваивается значение null.
Если вы хотите провести инициализацию в месте определения, она выглядит точно так же, как и у нестатических членов класса.
Следующий пример помогает понять, когда инициализируется статическая память:

//: initialization/StaticInitialization.java
// Указание значений по умолчанию в определении класса.
import static net.mindview.util.Print.*;
 
class Bowl {
Bowl(int marker) {
print("Bowl(" + marker + ")");
}
void f1(int marker) {
print("f1(" + marker + ")");
}
}
 
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
print("Table()");
bowl2.f1(1);
}
void f2(int marker) {
print("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
 
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
print("Cupboard()");
bowl4.f1(2);
}
void f3(int marker) {
print("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
 
public class StaticInitialization {
public static void main(String[] args) {
print("Creating new Cupboard() in main");
new Cupboard();
print("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}

<spoiler text="Output:">

Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)

</spoiler>
Класс Bowl позволяет проследить за процессом создания классов; классы Table и Cupboard содержат определения статических объектов Bowl. Заметьте, что в классе Cupboard создается нестатическая переменная Bowl bowl3, хотя все ос­тальные определения — статические.

Из выходных данных программы видно, что статическая инициализация происходит только в случае необходимости. Если вы не создаете объектов Table и никогда не обращаетесь к Table.bowl1 или Table.bowl2, то, соответственно, не будет и объектов static Bowl bowl1 и static Bowl bowl2. Они инициализируются только при создании первого объекта Table (или при первом обращении к статическим данным). После этого статические объекты повторно не переопределяются.

Сначала инициализируются static-члены, если они еще не были проинициализированы, и только затем нестатические объекты. Доказательство справедливости этого утверждения легко найти в результате работы программы. Для выполнения main() (а это статический метод!) загружается класс Staticlnitialization; затем инициализируются статические поля table и cupboard, вследствие чего загружаются эти классы. И так как все они содержат статические объекты Bowl, загружается класс Bowl. Таким образом, все классы программы загружаются до начала main(). Впрочем, эта ситуация нетипична, поскольку в рядовой программе не все поля объявляются как статические, как в данном примере.

Неплохо теперь обобщить знания о процессе создания объекта. Для примера возьмем класс с именем Dog:


  • Хотя ключевое слово static и не используется явно, конструктор в действительности является статическим методом. При создании первого объекта типа Dog или при первом вызове статического метода-обращения к статическому полю класса Dog, интерпретатор Java должен найти класс Dog.class. Поиск осуществляется в стандартных каталогах, перечисленных в переменной окружения СLASSPATH.

  • После загрузки файла Dog.class (с созданием особого объекта Class, о котором мы узнаем позже) производится инициализация статических элементов. Таким образом, инициализация статических членов проводится только один раз, при первой загрузке объекта Class.
  • При создании нового объекта конструкцией new Dog() для начала выделяется блок памяти, достаточный для хранения объекта Dog в куче.
  • Выделенная память заполняется нулями, при этом все примитивные поля объекта Dog автоматически инициализируются значениями по умолчанию (ноль для чисел, его эквиваленты для типов boolean и char, null для ссылок).
  • Выполняются все действия по инициализации, происходящие в точке определения полей класса.
  • Выполняются конструкторы. Как вы узнаете из главы 7, на этом этапе выполняется довольно большая часть работы, особенно при использовании наследования.


Явная инициализация статических членов

Язык Java позволяет сгруппировать несколько действий по инициализации объектов static в специальной конструкции, называемой статическим блоком. Выглядит это примерно так:

//: initialization/Spoon.java
public class Spoon {
static int i;
static {
i = 47;
}
}

Похоже на определение метода, но на самом деле мы видим лишь ключевое слово static с последующим блоком кода. Этот код, как и остальная инициализация static, выполняется только один раз: при первом создании объекта этого класса или при первом обращении к статическим членам этого класса (даже если объект класса никогда не создается). Например:

//: initialization/ExplicitStatic.java
//Явная инициализация с использованием конструкции "static"
import static net.mindview.util.Print.*;
 
class Cup {
Cup(int marker) {
print("Cup(" + marker + ")");
}
void f(int marker) {
print("f(" + marker + ")");
}
}
 
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups() {
print("Cups()");
}
}
 
public class ExplicitStatic {
public static void main(String[] args) {
print("Inside main()");
Cups.cup1.f(99); // (1)
}
// static Cups cups1 = new Cups(); // (2)
// static Cups cups2 = new Cups(); // (2)
}

<spoiler text="Output:">

Inside main()
Cup(1)
Cup(2)
f(99)

</spoiler>
Статический инициализатор класса Cups выполняется либо при обращении к статическому объекту cup1 в строке с пометкой (1), либо если строка (1) закомментирована — в строках (2) после снятия комментариев. Если же и строка (1), и строки (2) закомментированы, static-инициализация класса Cups никогда не выполнится. Также неважно, будут ли исполнены одна или обе строки (2) программы — static-инициализация все равно выполняется только один раз.

Инициализация нестатических данных экземпляра

В Java имеется сходный синтаксис для инициализации нестатических переменных для каждого объекта. Вот пример: .

//: initialization/Mugs.java
// "Инициализация экземпляра"
import static net.mindview.util.Print.*;
 
class Mug {
Mug(int marker) {
print("Mug(" + marker + ")");
}
void f(int marker) {
print("f(" + marker + ")");
}
}
 
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1 & mug2 initialized");
}
Mugs() {
print("Mugs()");
}
Mugs(int i) {
print("quot;Mugs(int)");
}
public static void main(String[] args) {
print("Inside main()");
new Mugs();
print("new Mugs() completed");
new Mugs(1);
print("new Mugs(1) completed");
}
}

<spoiler text="Output:">

Inside main()
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed

</spoiler>
выглядит в точности так же, как и конструкция static-инициализации, разве что ключевое слово static отсутствует. Такой синтаксис необходим для поддержки инициализации анонимных внутренних классов (см. главу 9), но он также гарантирует, что некоторые операции будут выполнены независимо от того, какой именно конструктор был вызван в программе. Из результатов видно, что секция инициализации экземпляра выполняется раньше любых конструкторов.

Инициализация массивов

Массив представляет собой последовательность объектов или примитивов, относящихся к одному типу, обозначаемую одним идентификатором. Массивы определяются и используются с помощью оператора индексирования [ ]. Чтобы объявить массив, вы просто. указываете вслед за типом пустые квадратные скобки:

int[] al;

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

int al[];

Это соответствует ожиданиям программистов на C и C++, привыкших к такому синтаксису. Впрочем, первый стиль, пожалуй, выглядит более логично — он сразу дает понять, что имеется в виду «массив значений типа int». Он и будет использоваться в книге.

Компилятор не позволяет указать точный размер массива. Вспомните, что говорилось ранее о ссылках. Все, что у вас сейчас есть, — это ссылка на массив, для которого еще не было выделено памяти. Чтобы резервировать память для массива, необходимо записать некоторое выражение инициализации. Для массивов такое выражение может находиться в любом месте программы, но существует и особая разновидность выражений инициализации, используемая только в точке объявления массива. Эта специальная инициализация выглядит как набор значений в фигурных скобках. Выделение памяти (эквивалентное действию оператора new) в этом случае проводится компилятором. Например:

 int[] al = { 1, 2, 3, 4, 5 };

Но зачем тогда вообще нужно определять ссылку на массив без самого массива?

 int[] а2;

Во-первых, в Java можно присвоить один массив другому, записав следующее:

 а2 = al;

В данном случае вы на самом деле копируете ссылку, как показано в примере:

//: initialization/ArraysOfPrimitives.java
// Массивы простейших типов.
import static net.mindview.util.Print.*;
 
public class ArraysOfPrimitives {
public static void main(String[] args) {
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2;
a2 = a1;
for(int i = 0; i < a2.length; i++)
a2[i] = a2[i] + 1;
for(int i = 0; i < a1.length; i++)
print("a1[" + i + "] = " + a1[i]);
}
}

<spoiler text="Output:">

a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6

</spoiler>
Массив a1 инициализируется набором значений, в то время как массив а2 — нет; присваивание по ссылке а2 присваивается позже — в данном случае присваивается другой массив.

Все массивы (как массивы примитивов, так и массивы объектов) содержат поле> которое можно прочитать (но не изменить!) для получения количества элементов в массиве. Это поле называется length. Так как в массивах Java, C и C++ . Нумерация элементов начинается с нуля, последнему элементу массива соответствует индекс length—1. При выходе за границы массива C и C++ не препятствуют «прогулкам в памяти» программы, что часто приводит к печальным последствиям. Но Java защищает вас от таких проблем — при выходе за рамки массива происходит ошибка времени исполнения (исключение, тема главы 10).

А если во время написания программы вы не знаете, сколько элементов вам понадобится в новом массиве? Тогда просто используйте new для создания его элементов. В следующем примере new работает, хотя в программе создается массив примитивных типов (оператор new неприменим для создания примитивов вне массива):

//: initialization/ArrayNew.java
// Создание массивов оператором new.
import java.util.*;
import static net.mindview.util.Print.*;
 
public class ArrayNew {
public static void main(String[] args) {
int[] a;
Random rand = new Random(47);
a = new int[rand.nextInt(20)];
print("length of a = " + a.length);
print(Arrays.toString(a));
}
}

<spoiler text="Output:">

length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

</spoiler>
Размер массива выбирается случайным образом, с использованием метода Random.nextInt(), генерирующего число от нуля до переданного в качестве аргумента значения. Так как размер массива случаен, очевидно, что создание массива происходит во время исполнения программы. Вдобавок, результат работы программы позволяет убедиться в том, что элементы массивов простейших типов автоматически инициализируются «пустыми» значениями. (Для чисел и символов это ноль, а для логического типа boolean — false.)

Метод Arrays.toString(), входящий в стандартную библиотеку java.util, выдает печатную версию одномерного массива.

Конечно, в данном примере массив можно определить и инициализировать в одной строке:

 int[] а = new int[rand.nextInt(20)];

Если возможно, рекомендуется использовать именно такую форму записи. При создании массива непримитивных объектов вы фактически создаете массив ссылок. Для примера возьмем класс-обертку Integer, который является именно классом, а не примитивом:

//: initialization/ArrayClassObj.java
// Создание массива непримитивных объектов.
import java.util.*;
import static net.mindview.util.Print.*;
 
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
print("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
a[i] = rand.nextInt(500); // Автоматическая упаковка
print(Arrays.toString(a));
}
}

<spoiler text="Output:"> (Sample)

length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]

</spoiler>
Здесь даже после вызова new для создания массива

 Integer[] а = new Integer[rand nextlnt(20)];

мы имеем лишь массив из ссылок — до тех пор, пока каждая ссылка не будет инициализирована новым объектом Integer (в данном случае это делается посредством автоупаковки):

 a[i] = rand.nextlnt(500);

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

//: initialization/ArrayInit.java
// Инициализация массивов
import java.util.*;
 
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
new Integer(1),
new Integer(2),
3, // Autoboxing
};
Integer[] b = new Integer[]{
new Integer(1),
new Integer(2),
3, // Автоматическая упаковка
};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
}

<spoiler text="Output:">

[1, 2, 3]
[1, 2, 3]

</spoiler>
В обоих случаях завершающая запятая в списке инициализаторов не обязательна (она всего лишь упрощает ведение длинных списков).
Первая форма полезна, но она более ограничена, поскольку может использоваться только в точке определения массива. Вторая форма может использоваться везде, даже внутри вызова метода.

Списки аргументов переменной длины

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

//: initialization/VarArgs.java
// Использование синтаксиса массивов
// для получения переменного списка параметров.
class A {}
 
public class VarArgs {
static void printArray(Object[] args) {
for(Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[]{
new Integer(47), new Float(3.14), new Double(11.11)
});
printArray(new Object[]{"one", "two", "three" });
printArray(new Object[]{new A(#41;, new A(), new A()});
}
}

<spoiler text="Output:"> (Sample)

47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f

</spoiler>
Видно, что метод print() принимает массив объектов типа Object, перебирает его элементы и выводит их. Классы из стандартной библиотеки Java при печати выводят осмысленную информацию, однако объекты классов в данном примере выводят имя класса, затем символ @ и несколько шестнадцатеричных цифр. Таким образом, по умолчанию класс выводит имя и адрес объекта (если только вы не переопределите в классе метод toString() — см. далее).

До выхода Java SE5 переменные списки аргументов реализовывались именно так. В Java SE5 эта долгожданная возможность наконец-то была добавлена в язык — теперь для определения переменного списка аргументов может ис­пользоваться многоточие, как видно в определении метода printArray:

//: initialization/NewVarArgs.java
/ Создание списков аргументов переменной длины
// с использованием синтаксиса массивов.
public class NewVarArgs {
static void printArray(Object... args) {
for(Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
// Можно передать отдельные элементы:
printArray(new Integer(47), new Float(3.14),
new Double(11.11));
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
// Или массив:
printArray((Object[])new Integer[]{ 1, 2, 3, 4 });
printArray(); // Пустой список тоже возможен
}
}

<spoiler text="Output:"> (75% match)

47 3.14 11.11
47 3.14 11.11
one two three
A@1bab50a A@c3c749 A@150bd4d
1 2 3 4

</spoiler>

Резюме

Такой сложный механизм инициализации, как конструктор, показывает, насколько важное внимание в языке уделяется инициализации. Когда Бьерн Страуструп разрабатывал C++, в первую очередь он обратил внимание на то, что низкая продуктивность C связана с плохо продуманной инициализацией, которой была обусловлена значительная доля ошибок. Аналогичные проблемы возникают и при некорректной финализации. Так как конструкторы позволяют гарантировать соответствующие инициализацию и завершающие действия по очистке (компилятор не позволит создать объект без вызова конструктора), тем самым обеспечивается полная управляемость и защищенность программы.

В языке C++ уничтожение объектов играет очень важную роль, потому что объекты, созданные оператором new, должны быть соответствующим образом разрушены. В Java память автоматически освобождается сборщиком мусора, и аналоги деструкторов обычно не нужны. В таких случаях сборщик мусора Java значительно упрощает процесс программирования и к тому же добавляет так необходимую безопасность при освобождении ресурсов. Некоторые сборщики мусора могут проводить завершающие действия даже с такими ресурсами, как графические и файловые дескрипторы. Однако сборщики мусора добавляют издержки во время выполнения программы, которые пока трудно реально оценить из-за сложившейся исторически медлительности интерпретаторов Java. И хотя в последнее время язык Java намного улучшил свою производительность, проблема его «задумчивости» все-таки наложила свой отпечаток на возможность решения языком некоторого класса задач.

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

]]>
Книги по Java https://linexp.ru?id=4735 Wed, 29 Jun 2022 14:10:04 GMT
<![CDATA[Spring in Action]]> От автора WIKI-версииКогда то давно, Джимми Картер будучи президентом США решил выучить испанский язык. Свое оригинальное (для Президента) решение, Картер воплощал в жизнь не менее оригинальным способом - в качестве учебника испанского языка он выбрал ... обычную Библию(испано-язычной версии). И таки - получилось

От автора WIKI-версии

Когда то давно, Джимми Картер будучи президентом США решил выучить испанский язык. Свое оригинальное (для Президента) решение, Картер воплощал в жизнь не менее оригинальным способом - в качестве учебника испанского языка он выбрал ... обычную Библию(испано-язычной версии). И таки - получилось ...

Отчасти вдохновляясь этим примером, я однажды решил укрепить свои познания в английском, занявшись переводом на русский, не Библии,конечно, но много более прозаичной книги - "Spring in Action". И то, не всей, и не подряд, но лишь по мере сил и желания (в отличии от Картера, версии на родном языке у меня не было). В перевод попала несколько устаревшее второе издание книги, впрочем не утратившее ценности для тех кто любит "делать ЭТО" через XML файлы.

Книги из серии "..... in Action" (обычно в PDF формате и как правило на английском) заслуженно популярны в определенных кругах :) Среди них встречаются и емкие талмуды, типа "JSTL in Action" (глубокий, но легко читаемый и при умеренных познаниях в английском), и поделки много поскромнее (типа"Struts in Action") . Книга "Spring in Action" в этом списке все же из разряда "тяжеловесов", во всех смыслах данного слова. Читать ее без владения "fluent English", однако непросто. И дело не в сложности излагаемого материала(по большому счету он не сложен), а в том, она получилась - чрезмерно "английско-художественной", что ли.... Полный лирических отступлений, крылатых выражений, игры слов и прочего бла бла бла, язык авторов, быстро превращает чтение этого справочника (на языке оригинала), в весьма утомительный процесс. Но с другой стороны, это позволяет узнать, что слово "draw"(обычно -"рисовать") можно применять в значении "извлекать из"(букв.- "тянуть, тащить"). В итоге (с учетом общего стиля изложения, принятого в книге) понять точный смысл фраз, типа: "...Spring draw this data ..." , бывает одновременно - и весьма непросто, и крайне нужно. Потому читателям не переведенных мною глав, придется по ходу самим решать, чего же в подобных случаях желали авторы: поэтично выразиться о создании(записи) - файла, или игриво поведать о его чтении.

Впрочем, будем справедливы к написавшим, книгу ... Т.н. "носители языка"(и лица себя, к ним, приравнивающие) оценят ее по достоинству ( мол типа, не Шекспир, но в домашнем театре, ставить - можно ) Прочим же, остается вспоминать(с тоской) "классику" от Microsoft или Oracle, где руководствуясь принципом Оккама, умудрялись упаковывать свои глубокие мысли в куда меньший словарный запас.

''Эта книга была преобразована мною из PDF в викиучебник в качестве экспресс-справочника для личного пользования. Потому переводил я ее не тотально, а лишь в местах - на которые хватило энтузиазма. Остальные главы были просто приведены в удобный для быстрого поиска вид.   Публикуется, ВСЕ в виде - "как есть". Литературное качество русского текста было принесено в жертву смысловой точности перевода. Я все же программист, а не профессиональный переводчик. Возможно я кого-то разочарую тем, что некоторые места и главы книги я не переводил(и даже не планирую их переводить), но надо же было оставить в задел для будущих поколений

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

'With best regards, Vadim Yanik aka "Shual" '


]]>
Книги по Java https://linexp.ru?id=4734 Wed, 29 Jun 2022 14:09:24 GMT
<![CDATA[Глава 3 Spring in Action 2th edition]]>

Содержание

РАСШИРЕННОЕ СВЯЗЫВАНИЕ КОМПОНЕНТОВ

Эта глава охватывает


  • Создание бинов родитель/потомок
  • Пользовательские редакторы свойств
  • Пост-обработка бинов
  • Динамически скриптовые бины

Большинство людей имеют по крайней мере один ящик (а иногда даже целый шкаф или кабинет) в своем доме, где хранятся разные мелочи. Хотя его часто называют просто - "выдвижной ящик", но довольно много разных весьма полезных штуковин в конечном итоге, может оказаться, там. Такие вещи, как рулетка, зажимы, ручки, карандаши, чертежные кнопки, несколько запасных батарей, и т.д. как правило, всегда найдутся дома в подобном месте. Обычно, вы не используете эти предметы каждый день, но вы точно знаете что если отключат электричество, вы пороетесь в этом ящике и обязательно найдете батареи, которые вставите в ваш фонарь.

В главе 2 я показал Вам базовые механизмы связывания компонентов Spring, применяемые - ежедневно. Нет сомнения, что у Вас будет много возможностей использовать эти методы в Ваших приложениях основанных на Spring. Однако в данной главе мы немного пороемся в выдвижном ящике Spring контейнеров.

Хотя темы охватываемые в ней вполне практичны для ваших потребностей, они почти не найдут такого же применения, как те, что мы обсуждали в главе 2. Не часто вам будет необходимо изменить метод в компоненте или создать бин который знает свое имя. И не в каждом проекте необходимо вводить класс Ruby в Java приложение, основанное на Spring.

Так как эта глава касается ряда довольно необычных способностей Spring, вы можете захотеть пропустить ее и двигаться вперед к аспектно-ориентированным функциям Spring в главе 4. Но эта глава будет все еще здесь, ожидая , когда вы начнете нуждаться в ней. И если вы окажетесь тут, то увидите, как можно получить ваши компоненты разными экстремальными способами. А начнем мы ее с того, как создать и расширить абстрактные компоненты.


Source(s): Глава 3 Spring in Action 2th edition

Объявление родителей и потомков бинов.

Одна из основ объектно-ориентированного программирования - возможность создать класс, который расширяет другой класс. Новый класс наследует все свойства и методы родительского класса, но он в состоянии представить новые свойства и методы или даже переопределить свойства и методы родителя. Если вы читаете эту книгу, вы вероятно Java программист и подклассы для вас отнюдь не новость .

Но знаете ли вы, что ваши Spring компоненты подобным же образом могут "под-бинить" другие Spring бины? Объявление <bean> в Spring типично содержит атрибут class, чтобы указать тип бина, и некоторое количество(иногда- нулевое) <property>-элементов, чтобы внедрять их значения в свойства бина. И абсолютно все об этом бине объявлено в одном месте: в нем самом, в его декларации - <bean>.

Но создание множества отдельных объявлений бинов может сделать вашу конфигурацию Spring громоздкой и хрупкой. Иерархия классов в Java, создается — чтобы собрать общую функциональность и свойства в родительских классах, которые могут быть затем наследованы дочерними классами
Именно по тем же самым причинам, вы сочтете полезным создавать бины, которые бы расширяли и наследовали другие определения бинов.

И это - отличный способ сократить количество избыточного определения контекста Spring в конфигурационных XML файлах. Чтобы обеспечить функционал "бинов-потомков", элемент <bean> предоставляет два специальных атрибута:


  • parent - указывает на идентификатор бина, который будет предком бина с атрибутом parent. Атрибут parent указывает, что бин расширяет Java класс.
  • abstract — если установлено в true, указывает, что бин объявлен как абстрактный. То есть, он никогда не должен быть создан Spring.

Чтобы показать возможности под-компонентов Spring, вернемся к соревнованиям Spring Idol


Source(s): Глава 3 Spring in Action 2th edition

Абстрактный тип бинов

Как вы помните, в главе 2 Кенни был соперником тех, кто выступал в соревновании как Музыкант.
Определенно, специализация Кенни это - саксофон. Кенни был объявлен в Spring как бин используя следующий XML

<bean id="kenny" class="com.springinaction.springidol.Instrumentalist">
<property name="song" value="Jingle Bells" />
<property name="instrument" ref="saxophone" />
</bean>

За то время, что вы читали главу 2, новый соперник вступил в соревнования. Так совпало, что Давид тоже саксофонист. Более того, он должен играть тоже самое, что и Кенни. Бин Давида был объявлен в Spring следующим образом:

<bean id="david" class="com.springinaction.springidol.Instrumentalist">
<property name="song" value="Jingle Bells" />
<property name="instrument" ref="saxophone" />
</bean>
Файл:Fig31.png
Рисунок 3.1 Два компонента одного типа и свойствами, которые установлены одинаковые значения

Сейчас у нас есть два бина, объявленные в Spring фактически одинаковыми XML. Как показано на рисунке 3.1, Единственное различие между двумя этими компонентами - только их идентификаторы. Сейчас это может показаться не большой проблемой, но представьте, что может произойти, если у нас будет на конкурсе более 50 саксофонистов , которые все как один захотят исполнить одну и ту же мелодию. Как тут не сойти с ума объявляя их в конфигурации ?

Очевидным решением проблемы может быть запрет нескольким участникам далее вступать в состязание. Но мы уже создали прецедент, разрешив выступление Давида. Кроме того, нам нужен пример "бина-потомка". Так что мы вынуждены отложить начало состязания. Другое решение это создать бин, который будет родителем для всех наших соперничающих саксофонистов.
Компонент baseSaxophonist должен будет исполнть этот трюк:

<bean id="baseSaxophonist" 
class="com.springinaction.springidol.Instrumentalist" abstract="true">
<property name="instrument" ref="saxophone" />
<property name="song" value="Jingle Bells" />
</bean>

Бин baseSaxophonist почти не отличается от бинов kenny и david на первый взгляд. Но заметьте, что его атрибут abstract установлен в true.
Это сообщает Spring не пытаться инстанциировать этот компонент ... даже если будет явный запрос от контейнера.
Во многом это тот же абстрактный Java класс, который не может быть создан Хотя компонент baseSaxophonist невозможно инстанциировать, он все еще очень полезен, потому что он содержит общие свойства, которые присущи Кенни и Давиду. Поэтому сейчас мы можем объявить компоненты kenny и david следующим образом:

<bean id="kenny" parent="baseSaxophonist" />
<bean id="david" parent="baseSaxophonist" />
Файл:Fig32.png
Рисунок 3.2 Компоненты kenny и david разделяют некоторую общую конфигурацию. Создавая родительский компонент с общими свойствами и наследуя от него, избыточная конфигурация может быть исключена.

Атрибут parent показывает, что оба компонента kenny и david будут наследовать свое определение от компонента baseSaxophonist. Заметьте, атрибута class нет. Это потому, что компоненты kenny и david наследуют класс родительского компонента а также его свойства. Сейчас избыточные XML ушел и эти бины стали выглядеть более простыми и краткими.

Здесь уместно упомянуть, что родительские бины могут быть и не абстрактными. Разумеется, вполне возможно создать "бин-потомок" который расширяет конкретный бин. Но в нашем случае мы знаем, что у Spring нет никаких причин создавать экземпляр бина baseSaxophonist, поэтому мы объявили его как abstract.

Переопределение наследуемых свойств.

Предположим, что другой саксофонист вступил в состязание (я говорил вам, что это будет происходить) Но этот саксофонист будет исполнять “Mary Had a Little Lamb” вместо “Jingle Bells.” Означает ли это, что мы не можем переопределить baseSaxophonist когда будем объявлять нового участника?

Конечно, нет. Мы все еще можем переопределять baseSaxophonist. Но вместо того, чтобы применить все значения наследуемых свойств, мы должны переопределить свойство song.

Следующий XML объявляет нового саксофониста:

<bean id="frank" parent="baseSaxophonist">
<property name="song" value="Mary had a little lamb" />
</bean>
Файл:Fig33.png
Рисунок 3.3 Компонент frank наследует baseSaxophonistbean, но переопределяет свойство song.

Компонент frank будет по-прежнему наследовать класс и свойства компонента baseSaxophonist. Но, как видно на рисунке 3.3, frank переопределяет свойство song так, что он может исполнять песню о девочке и её мохнатом друге. Во многих отношениях объявления родительских и дочерних бинов отражают возможность родительских и дочерних классов, определенных в Java. Но затаите дыхание ... я покажу вам, что Spring наследование может делать нечто, чего невозможно сделать в Java.

Общие абстрактные свойства.

В конкурсе талантов Spring Idol может быть несколько соперников с музыкальными способностями. Как вы уже видели, у нас есть несколько музыкантов, которые исполняют песню на своих инструментах. Но они могут быть также исполнителями, которые используют свои голоса чтобы петь эти песни.

Предположим, что в конкурсе талантов Spring Idol есть два участника, первый - вокалист, а другой - гитарист, которые могут исполнять какую-то песню. Сконфигурированные как разные компоненты, эти участники могут быть похожи на такие конфигурационные файлы:

<bean id="taylor" class="com.springinaction.springidol.Vocalist">
<property name="song" value="Somewhere Over the Rainbow" />
</bean>
<bean id="stevie" class="com.springinaction.springidol.Instrumentalist">
<property name="instrument" ref="guitar" />
<property name="song" value="Somewhere Over the Rainbow" />
</bean>

Как и прежде, эти два компонента разделяют какую-то общую конфигурацию. Определенно, они оба имеют одно и тоже значение атрибута song. Но образцы в предыдущих секциях не похожи, два компонента не разделяют общий класс. Так что мы не можем создать общий родительский компонент для них.

Или можем? В Java, потомки наследуют общий базовый тип, который определен их родительским компонентом. В том смысле, что в Java нет способа для класса потомка расширить общий тип, наследуя свойства и/или методы но не наследуя класс общего родителя. Однако в Spring, под-компонент не обязан наследовать тип общего родителя. Два бина с совершенно разными значениями атрибута class все еще могут разделять общие настройки свойств, которые унаследованы от родительского компонента.

Рассмотрим объявление следующего родительского бина:

<bean id="basePerformer" abstract="true">
<property name="song" value="Somewhere Over the Rainbow" />
</bean>

Этот компонент basePerformer объявляет общее свойство song, которое разделят наши два исполнителя. Но заметьте, что он не имеет установленного атрибута class. Это нормально, потому-что каждый потомок будет определять свой тип в своем собственном атрибуте class. Вот новое определение taylor и stevie:

<bean id="taylor" 
class="com.springinaction.springidol.Vocalist" parent="basePerformer" />
 
<bean id="stevie"
class="com.springinaction.springidol.Instrumentalist" parent="basePerformer">
<property name="instrument" ref="guitar" />
</bean>
Файл:Fig34.png
Рисунок 3.4 Родительские компоненты не имеют определенного типа. Они могут использоваться только для хранения общих конфигураций, которые могут быть унаследованы компонентами других типов.

Отметьте, что оба эти компонента используют атрибуты class и parent вместе с другими. Такое разделение и наследование свойств общего базового бина возможно для двух и более компонентов

Как показано на рисунке 3.4, свойство song наследовано из компонента basePerformer, даже если каждый наследник имеет полностью различный и неродственный тип. Бин-наследование обычно используют для уменьшения объема XML, необходимого для объявления аспектов и транзакций. В главе 6 (раздел 6.4.2) вы увидите, как бин-наследование может значительно уменьшить количество избыточного XML, когда объявляются транзакции с TransactionProxyFactoryBean.

Но сейчас самое время продолжить рыться в ящике, чтобы посмотреть, какие другие полезные инструменты мы можем найти. Мы уже увидели setter injection (внедрение установкой свойств) и constructor injection (внедрение с использованием конструктора) в главе 2. Затем давайте посмотрим, как Spring обеспечивает другие виды внедрения, которые позволяют вам декларативно вводить методы в ваши компоненты, эффективно изменять функциональные возможности компонентов, без необходимости менять лежащий в их основе Java код

Внедрение методов

В главе 2 вы наблюдали две базовые формы внедрения зависимости (DI). Внедрение с использованием конструктора позволяет вам настраивать ваш бин внедряя значения в его конструкторы.

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

Но в этом разделе, я хотел бы показать вам необычную форму DI названную - "внедрением методов". Используя внедрение установкой свойств и внедрение конструктора, вы вводили значения в свойства компонента. Но как показано на рисунке 3.5, внедрение метода совершенно от этого отличается, потому-что позволяет вам вводить в компонент полностью определенный метод.

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

class String
def print_length
puts "This string is #{self.length} characters long"
end
end

Когда метод определен, вы можете вызвать его из любого созданного вами String.
Например:

message = "Hello"
message.print_length
Файл:Fig35.png
Рисунок 3.5 Method injection это тип внедрения, где методы класса заменяются альтернативной реализацией.

будет напечатано “This string is 5 characters long” в стандартном выводе.

Но это Ruby. Язык Java не такой гибкий. Пускай все и не столь элегантно, как в конструкциях для внедрения методов языка Ruby, но все-же это шаг в нужном направлении.

Spring поддерживает две формы внедрения методов:


  • Method replacement - возможность замены во время выполнения существующего метода (абстрактного или реального) на его новую произвольную реализацию,.
  • Getter injection - возможность замены во время выполнения существующего метода (абстрактного или реального) на его новую реализацию, которая ограничивается тем, что возвращает какой-либо компонент из контекста Spring.

Чтобы начать работать с внедрением методов, давайте посмотрим, как Spring поддерживает замещение методов общего назначения

Основы замещения методов.

Вам нравятся шоу иллюзионистов? Фокусники используют ловкость рук и отвлечение внимания чтобы прямо на наших глазах делать казалось бы невозможные вещи.

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

Только-что так случилось, что Гарри, обещанный нами фокусник, вступил в конкурс талантов Spring Idol и будет исполнять наш любимый фокус. Позвольте мне представить вам - Гарри, первого из класса Magician представленного вам в листинге 3.1

Листинг 3.1 Фокусник и его магическая коробка.

package com.springinaction.springidol;
 
public class Magician implements Performer {
public Magician() {}
 
public void perform() throws PerformanceException {
System.out.println(magicWords);
System.out.println("The magic box contains...");
System.out.println(magicBox.getContents());
}
 
// injected
private MagicBox magicBox;
 
public void setMagicBox(MagicBox magicBox) {
this.magicBox = magicBox;
}
 
private String magicWords;
 
public void setMagicWords(String magicWords) {
this.magicWords = magicWords;
}
}

Как вы можете видеть, класс Magician имеет два свойства, которые могут быть установлены используя Spring DI. Для Magician нужны какие-нибудь магические слова, чтобы делать иллюзию, так что хотелось бы установить свойство magicWords. Но что более важно, мы должны дать ему магический ящик используя свойство magicBox. Говоря о магическом ящике, вы можете найти его реализацию в листинге 3.2.

Листинг 3.2 Реализация магического ящика содержит великолепного ассистента...или нет?

package com.springinaction.springidol;
public class MagicBoxImpl implements MagicBox {
public MagicBoxImpl() {}
public String getContents() {
return "A beautiful assistant";
}
}

Ключевой момент в классе MagicBoxImpl, что вы должны обратить свое внимание на метод getContents(). Вы должны заметить, что он жестко закодирован всегда возвращать “A beautiful assistant”, но, как вы скоро увидите, все не так, как кажется. Но прежде чем я покажу вам трюк, вот как Гарри и его волшебный ящик связаны с контекстом Spring приложения.

<bean id="harry" class="com.springinaction.springidol.Magician">
<property name="magicBox" ref="magicBox" />
<property name="magicWords" value="Bippity boppity boo" />
</bean>
<bean id="magicBox" class="com.springinaction.springidol.MagicBoxImpl" />

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

ApplicationContext ctx = … //загружаем контекст Spring
Performer magician = (Performer) ctx.getBean("harry");
magician.perform();

Когда этот фрагмент кода начнет извлекать компонент harry из контекста приложения, и когда метод perform() будет вызван, вы сможете увидеть все так, как и должно быть:

"Bippity boppity boo"
The magic box contains..
A beautiful assistant

Такой вывод не должен стать сюрпризом для вас. В конце концов, MagicBoxImpl жестко закодирован возвращать “A beautiful assistant” когда вызывается getContents(). Но как я сказал, это только дразнилка. Сейчас Гарри не исполняет настоящий трюк. Но сейчас самое время начать представление, так что давайте настроим XML конфигурацию, чтобы она выглядела следующим образом:

<bean id="magicBox" class="com.springinaction.springidol.MagicBoxImpl">
<replaced-method name="getContents" replacer="tigerReplacer" />
</bean>
 
<bean id="tigerReplacer" class="com.springinaction.springidol.TigerReplacer" />
Файл:Fig36.png
Рисунок 3.6 Использование внедрения <replaced-method> в Spring, облегчает замещение метода getContents(), возвращающего “A beautiful assistant” иной реализацией, которая производит - Тигра.

Сейчас в компоненте magicBox есть элемент <replaced-method> (см. рисунок 3.6) Как следует из названия, этот элемент используется для замены метода - новой реализацией. В этом случае атрибут name указывает на метод getContents(), который будет заменен. И атрибут replacer указывает на вызов компонента tigerReplacer для замещения реализации.

Вот где происходит реальная ловкость рук. Компонент tigerReplacer это TigerReplacer (класс), который определен в листинге 3.3

Листинг 3.3 TigerReplacer, который заменяет реализацию метода

package com.springinaction.springidol;
import java.lang.reflect.Method;
import org.springframework.beans.factory.support.MethodReplacer;
 
public class TigerReplacer implements MethodReplacer {
public Object reimplement(Object target, Method method, Object[] args)
throws Throwable {
return "A ferocious tiger";
}
}

TigerReplacer реализует интерфейс SpringMethodReplacer. MethodReplacer требует только реализации метода reimplement(). Этот метод принимает три аргумента:


  • target - объект , в котором будет заменен метод
  • method - метод, который должен быть заменен
  • args - массив аргументов, принимаемых методом.

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

Тело метода reimplement() фактически становится новой реализацией метода getContents() магического ящика. В нашем примере, единственное, что мы хотим сделать, это вернуть "свирепый тигр". В действительности, содержимое ящика заменяется тигром как показано на рисунке 3.6 Сейчас, когда мы вызовем метод perform(), на консоль будет выведено следующее:

Bippity boppity boo
The magic box contains..
A ferocious tiger

Та-да! Великолепный ассистент был заменен свирепым тигром - без изменения существующего кода MagicBoxImpl. Волшебный трюк был успешно выполнен с помощью <replaced-method> Spring-а.

Стоит отметить, что хотя MagicBoxImpl имеет конкретную реализацию метода getContents(), было бы также возможно для getContents(), быть написанным как абстрактный метод.

В самом деле, внедрение метода это трюк, который полезен, когда фактическая реализация метода не известна до времени развертывания.
В это время класс метода замещения может быть представлен в JAR файле, помещенного в classpath приложения. Замена методов общего характера это конечно изящный трюк. Но есть более специфичная форма внедрения методов которая позволяет среде выполнения связать компоненты геттер методом. Давайте посмотрим как выполняется getter injection в компонентах Spring.

Использование getter injection

Getter injection это особый случай внедрения метода, когда метод (который обычно абстрактный) объявлен чтобы возвращать компонент определенного типа, но действительные возвращаемые компоненты настраиваются в контексте Spring.

Для примера, рассмотрим новую форму класса Instrumentalist с внедренными методами в листинге 3.4

Листинг 3.4:

package com.springinaction.springidol;
public abstract class Instrumentalist implements Performer {
public Instrumentalist() {}
 
public void perform() throws PerformanceException {
System.out.print("Playing " + song + " : ");
getInstrument().play();
}
 
private String song;
public void setSong(String song) {
this.song = song;
}
 
public abstract Instrument getInstrument();
}

В отличие от оригинального Instrumentalist, этот класс не получает инструмент путем setter injection. Вместо этого, есть абстактный метод getInstrument(), который будет возвращать инструмент исполнителя. Но если getInstrument() абстрактный, тогда большой вопрос как метод получит реализацию.

Один из возможных подходов это использовать замену метода общего назначения, как описано в последней части. Но это потребует написать класс, который реализует MethodReplacer, когда все, что мы должны сделать это переопределить метод getInstrument() чтобы возвращать определенный компонент. Для внедрений в стиле getter injection, Spring предлагает элемент конфигурации <lookup-method>. Так же как <replaced-method>, <lookup-method> заменяет метод новой реализацией во время выполнения.

Но <lookup-method> это сокращенный вариант getter-injection для <replaced-method>, где вы должны определять компонент в контексте Spring, который возвращает замененный метод. С <lookup-method> можно расслабиться. Теперь не надо писать класс MethodReplacer. Следующий XML демонстрирует как используется <lookup-method> для замены метода getInstrument() другим, который возвратит ссылку на бин guitar.

<bean id="stevie" class="com.springinaction.springidol.Instrumentalist">
<lookup-method name="getInstrument" bean="guitar" />
<property name="song" value="Greensleeves" />
</bean>

Как и в <replaced-method>, атрибут name в <lookup-method> указывает на метод, который будет замещен. Здесь мы заменяем метод getInstrument(). Атрибут bean указывает на на компонент, который будет возвращен при вызове метода getInstrument(). В нашем случае, это компонент с id=guitar.

В результате метод getInstrument() фактически будет переопределен таким образом:

public Instrument getInstrument() {
ApplicationContext ctx = …;
return (Instrument) ctx.getBean("guitar");
}

Сам по себе, getter injection это только видоизменение setter injection. Однако, это имеет значение, когда областью действия компонента является prototype :

<bean id="guitar" class="com.springinaction.springidol.Guitar" scope="prototype" />

Даже если область видимости prototype, метод guitar будет только однажды внедрен в свойство, если мы использовали setter injection. Однако, вводя его в метод getInstrument() через getter injection, мы гарантируем, что каждый вызов getInstrument() вернет различные гитары. Это может пригодиться, если гитарист порвет струну в середине исполнения и потребует свежий
струнный инструмент.

Вы должны знать, что несмотря ни на что, мы используем <lookup-method> чтобы выполнить getter injection в методе getInstrument(), нет никаких требований к <lookup-method> чтобы фактический замененный метод был методом установки свойств (getter, т. е. начинающийся с get). Любой не-void метод это кандидат на замещение в <lookup-method>.

Важно отметить, что даже если внедрение метода позволяет вам заменять реализацию метода, нет никакой возможности заменить сигнатуру метода. Параметры и возвращаемый тип должны оставаться такие, какие есть. Для <lookup-method> это означает, что атрибут bean должен указывать на компонент, тип которого может быть представлен типом, возвращаемым методом (Instrument в предыдущем случае).

Внедрение не-Spring компонентов.

Как вы узнали в главе 2, одна из главных задач Spring это настраивать бины. Но до сих пор всегда подразумевалось одно предостережение: контейнер Spring может настраивать только те компоненты, экземпляры которых он сам создает. Может показаться слишком странным на первый взгляд, но это представляет некоторые проблемы. Не все объекты в приложении созданы контейнером Spring. Рассмотрим следующие возможные сценарии:


  • Обычно JSP теги реализуются веб контейнером, в рамках которого выполняется приложение. Если JSP тег нуждается во взаимодействии с какими-то другими объектами, они должны создаваться явно.
  • Доменные объекты, которые типично создаются во время выполнения инструментами ORM (таким как Hibernate или iBATIS). В насыщенной доменной модели, доменные объекты имеют как состояние, так и поведение. Но если Вы не можете внедрить сервисные объекты в доменные объекты, то ваши доменные объекты должны получить свою собственную реализацию сервисных объектов, или логика поведения должна полностью содержаться в доменных объектах.

И тут может быть другая веская причина почему мы можем нуждаться в Spring чтобы настраивать объекты, которые не создаем. Например, предположим, что мы явно создаем экземпляр из примера Spring Idol :

Instrumentalist pianist = new Instrumentalist();
pianist.perform();

Так как Instrumentalist это - POJO, нет никаких причин по которым мы не можем прямо и явно создать его экземпляр. Но когда вызывается метод perform(), может быть брошено исключение NullPointerException. Это потому, что не смотря на то, что мы позволяем классу Instrumentalist создаваться, его свойство instrument может иметь значение - null. В самом деле, мы можем вручную настраивать свойства класса Instrumentalist. Например:

Piano piano = new Piano();
pianist.setInstrument(piano);
pianist.setSong("Chopsticks");

Даже если бы вручную введенные свойства работали, это не давало бы преимущества возможности Spring разделять конфигурацию и код.
Кроме того, если бы ORM создавали экземпляры Instrumentalist, мы бы никогда не смогли изменять конфигурацию их свойств.
К счастью, в Spring 2.0 включена возможность декларативно настраивать компоненты, экземпляры которых созданы вне Spring. Идея в том, что Spring настраивает компоненты, но не создает их экземпляры.

Рассмотрим компонент Instrumentalist который явно создан ранее. В идеале, мы хотели бы сконфигурировать pianist вне нашего кода и позволить Spring внедрить его со свойствами instrument и song. Следующий XML покажет, как мы можем сделать это.

<bean id="pianist" class="com.springinaction.springidol.Instrumentalist" abstract="true">
<property name="song" value="Chopsticks" />
<property name="instrument">
<bean class="com.springinaction.springidol.Piano" />
</property>
</bean>

По сути нет ничего необычного в этом объявлении бина . Компонент pianist объявляется как Instrumentalist.
И его свойства song и instrument связываются со своими значениями. Это просто ваш заурядный Spring бин, за исключением одной маленькой детали: его атрибут abstract установлен в true.

Но как Вы видели ранее, установка abstract в true сообщает Spring, что вы не хотите реализовывать экземпляр класса этого компонента. Это часто используется, чтобы объявить родительский компонент, который будет расширен дочерним компонентом. Но в этом случае мы просто указываем Spring, что компонент pianist не будет инстантиирован, это будет сделано вне Spring.

На самом деле, компонент pianist только служит шаблоном действий для Spring, когда он формирует Instrumentalist, который создан вне Spring. С этим определенным шаблоном нам нужен некий метод связывания его с классом Instrumentalist. Чтобы сделать это, мы будем аннотировать класс Instrumentalist аннотацией @Configurable:

package com.springinaction.springidol;
import org.springframework.beans.factory.annotation.Configurable;
 
@Configurable("pianist")
public class Instrumentalist implements Performer {
...
}

Аннотация @Configurable делает две вещи:


  • Во первых, это показывает, что экземпляр класса Instrumentalist может быть сконфигурирован Spring, даже если создан вне его.
  • Это также ассоциирует класс Instrumentalist с компонентом, id которого = pianist. Когда Spring пытается сконфигурировать экземпляр класса Instrumentalist,он будет рассматривать компонент pianist как шаблон.

Итак, как же Spring узнает, как настраивать компоненты с аннотацией @Configurable ?
За это отвечает, одна из последних вещей, добавленная в конфигурацию Spring:

<aop:spring-configured />

Конфигурационный элемент <aop:spring-configured> это один из многих новых конфигурационных элементов, введенных в Spring 2.0. Они представляют указатели для Spring, что это некие элементы, которые он будет конфигурировать, даже если они созданы
где-то еще.

<aop:spring-configured> устанавливает аспект AspectJ с точкой действия которая срабатывает, когда создается любой компонент аннотированный @Configurable. Когда компонент создается, вмешивается аспект и внедряет свойства в новый экземпляр, основанный на шаблоне <bean> в конфигурации Spring Т.к. сконфигурированный Spring аспект это аспект AspectJ, то ваше приложение нуждается, в запуске на JVM с включенной поддержкой AspectJ. Лучший способ для Java 5 JVM включить поддержку AspectJ, это запустить ее со следующими аргументами JVM:

-javaagent:/path/to/aspectjweaver.jar

В действительности, это сообщает JVM выполнять слияние во время загрузки с любыми аспектами AspectJ. Чтобы сделать это, ей необходимо знать, где расположены связанные AspectJ классы.

Вы должны будете заменить

/path/to/aspectjweaver.jar

действительным путем к фалу aspectjweaver.jar в вашей системе . В этой части мы явно создадим экземпляр Instrumentalist как простую демонстрацию возможности Spring конфигурировать компоненты, которые он не создает. Тем не менее, как я упоминал ранее, сконфигурированный компонент более вероятно был бы инстанцирован ORM или некоторой сторонней библиотекой, если бы это было реальным
приложением.

Сейчас смотрите, как редактор свойств Spring выполняет простую работу по внедрению сложных значений, основанных на String представлении.

Пользовательские редакторы свойств

По мере изучения этой книги, вы можете увидеть несколько примеров, где сложные свойства устанавливаются посредством простого строкового значения. Например, в главе 9, вы можете увидеть как в Spring связывается веб сервисы, используя JaxRpcPortProxyFactoryBean. Одно
из свойств JaxRpcPortProxyFactoryBean, которое вам нужно будет установить, это wsdlDocumentUrl. Это свойство типа java.net.URL. Но вместо того, чтобы создавать компонент java.net.URL и связывать его с этим свойством, вы можете сконфигурировать его используя String таким образом:

<property name="wsdlDocumentUrl" value="https://www.xmethods.net/sd/BabelFishService.wsdl" />

Spring автоматически, в скрытом режиме, преобразует значение String в URL объект. В действительности, магия этого трюка не в каких-то особых возможностях Spring, а скорее в малоизвестных возможности оригинального JavaBeans API. Интерфейс java.beans.PropertyEditor обеспечивает возможность модифицировать значения типа String, связывая их с не-String типами. Удобная реализация этого интерфейса - java.beans.PropertyEditorSupport - имеет два метода, интересующие нас.


  • getAsText ( ) возвращает String представление значений свойств.
  • setAsText (String value) устанавливает компоненту значение свойства, переданного в виде строки.

При попытке установить не String свойство в String значение, вызывается метод setAsText() для выполнения преобразования. Точно также, метод getAsText() вызывается, чтобы вернуть текстовое представление значения свойства.

Spring сопровождается несколькими общими редакторами, основанными на PropertyEditorSupport, включая org.springframework.beans.propertyeditors.URLEditor, который использует пользовательский редактор, чтобы конвертировать в/из String объекты java.net.URL. в таблице 3.1 представлен выбор для Spring пользовательских редакторов

Таблица 3.1 Spring работает со свойствами нескольких пользовательских редакторов, которые автоматически в свою очередь вводят строковые значения в более сложные типы.


Редактор свойств. Что делает.
ClassEditor Устанавливает свойство java.lang.Class для строки, значение которого содержит полное имя класса.
CustomDateEditor Устанавливает свойство java.util.Date для строки используя пользовательский объект java.text.DateFormat.
FileEditor Устанавливает свойство java.io.File для строкового значения, которое содержит полный путь к файлу.
LocaleEditorУстанавливает свойство java.util.Locale для строкового значения которое содержит текстовое представление локали (например, en_US).
StringArrayPropertyEditor. Преобразует разделенную запятыми строку в строковый массив свойств.
StringTrimmerEditor Автоматически отрезает пробелы у строк свойств с возможность конвертировать пустые строки в null.
URLEditor Устанавливает свойство java.net.URL для строкового значения содержащее спецификацию для URL.

В дополнение к обычным редакторам в таблице 3.1, вы можите написать свой собственный редактор, расширяя класс PropertyEditorSupport. Например, предположим, что в приложении есть объект Contact, который удобно хранит информацию о сотрудниках в вашей организации. Кроме того, в объекте Contact есть свойство phoneNumber, которое хранит контактный номер телефона:

public Contact {
private PhoneNumber phoneNumber;
 
public void setPhoneNumber(PhoneNumber phoneNumber) {
this.phoneNumber = phoneNumber;
}
}

Свойство phoneNumber является PhoneNumber типом и определяется следующим образом:

public PhoneNumber {
private String areaCode;
private String prefix;
private String number;
public PhoneNumber() {}
 
public PhoneNumber(String areaCode, String prefix, String number) {
this.areaCode = areaCode;
this.prefix = prefix;
this.number = number;
}
...
}

Используя базовую технологию изученную в главе 2, вы можите связать объект PhoneNumber со свойством phoneNumber в бине Contact следующим образом:

<beans>
<bean id="infoPhone" class="com.springinaction.chapter03.propeditor.PhoneNumber">
<constructor-arg value="888" />
<constructor-arg value="555" />
<constructor-arg value="1212" />
</bean>
 
<bean id="contact" class="com.springinaction.chapter03.propeditor.Contact">
<property name="phoneNumber" ref="infoPhone" />
</bean>
</beans>

Обратите внимание, что надо определить отдельный бин infoPhone, настроить объект PhoneNumber и затем связать свойство phoneNumber с описанием бина. Вместо этого, предположим что вы написали свой редактор PhoneEditor, например так:

public class PhoneEditor extends java.beans.PropertyEditorSupport {
 
public void setAsText(String textValue) {
String stripped = stripNonNumeric(textValue);
String areaCode = stripped.substring(0, 3);
String prefix = stripped.substring(3, 6);
String number = stripped.substring(6);
PhoneNumber phone = new PhoneNumber(areaCode, prefix, number);
setValue(phone);
}
 
private String stripNonNumeric(String original) {
StringBuffer allNumeric = new StringBuffer();
for(int i=0; i<original.length(); i++) {
char c = original.charAt(i);
if(Character.isDigit(c)) {
allNumeric.append(c);
}
}
return allNumeric.toString();
}
}

Теперь единственная оставшаяся вещь состоит в том, чтобы заставить Spring распознавать Ваш пользовательский редактор свойств
при связывании свойств бинов. Для этого Вы должны использовать Spring’s CustomEditor - Configurer.

CustomEditorConfigurer - BeanFactoryPostProcessor, который загружает пользовательский редактор в BeanFactory, вызывая метод registerCustomEditor(). (Произвольно, Вы можете вызвать метод registerCustomEditor() в Вашем собственном коде после того, как у Вас есть экземпляр класса фабрики бина). Добавляя следующий фрагмент XML к файлу конфигурации бина, Вы укажете Spring регистрировать PhoneEditor как пользовательский редактор:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="com.springinaction.chapter03.propeditor.PhoneNumber">
<bean id="phoneEditor"
class="com.springinaction.chapter03.propeditor.PhoneEditor">
</bean>
</entry>
</map>
</property>
</bean>

Теперь Вы будете в состоянии конфигурировать свойство phoneNumber объекта Contact, используя простое значение String и не создавая отдельный бин infoPhone:

<bean id="contact" class="com.springinaction.chapter03.propeditor.Contact">
<property name="phoneNumber" value="888-555-1212" />
</bean>

Отметьте, что многие из пользовательских редакторов, которые идут со Spring (такие как URLEditor и LocaleEditor) уже зарегистрированы в фабрике бинов после старта контейнера. Вы не должны регистрировать их явно, используя CustomEditorConfigurer. Редакторы свойств - только один из способов настроить, то как Spring создает и внедряет бины. Существуют и другие типы бинов, выполняющие в контейнере Spring особые функции. Давайте увидим, как создать несколько специальных бинов, которые позволяют Вам настраивать, то как контейнер Spring связывает бины.

Работа с особыми видами Spring бинов

Most beans configured in a Spring container are treated equally. Spring configures them, wires them together, and makes them available for use within an application. Nothing special.

But some beans have a higher purpose. By implementing certain interfaces, you can cause Spring to treat beans as special—as part of the Spring Framework itself. By taking advantage of these special beans, you can configure beans that


  • Become involved in the bean’s creation and the bean factory’s lifecycles by postprocessing bean configuration
  • Load configuration information from external property files
  • Load textual messages from property files, including internationalized messages
  • Listen for and respond to application events that are published by other beans and by the Spring container itself
  • Are aware of their identity within the Spring container

In some cases, these special beans already have useful implementations that come packaged with Spring. In other cases, you’ll probably want to implement the interfaces yourself.

Let’s start the exploration of Spring’s special beans by looking at Spring’s special beans that perform postprocessing of other beans after the beans have been wired together.

Пост-обработка бинов

In chapter 2, you learned how to define beans within the Spring container and how to wire them together. For the most part, you have no reason to expect beans to be wired in any way different than how you define them in the bean definition XML file. The XML file is perceived as the source of truth regarding how your application’s objects are configured.

But as you saw in figures 2.2 and 2.3, Spring offers two opportunities for you to cut into a bean’s lifecycle and review or alter its configuration. This is called postprocessing. From the name, you probably deduced that this processing is done after some event has occurred. The event this postprocessing follows is the instantiation and configuration of a bean. The BeanPostProcessor interface gives you two
opportunities to alter a bean after it has been created and wired:

public interface BeanPostProcessor {
 
Object postProcessBeforeInitialization(Object bean, String name)
throws BeansException;
 
Object postProcessAfterInitialization(Object bean, String name)
throws BeansException;
}

The postProcessBeforeInitialization() method is called immediately prior to bean initialization (the call to afterPropertiesSet() and the bean’s custom init-method). Likewise, the postProcessAfterInitialization() method is called immediately after initialization.

Writing a bean postprocessor

For example, suppose that you wanted to alter all String properties of your application beans to translate them into Elmer Fudd-speak. The Fuddifier class in listing 3.5 is a BeanPostProcessor that does just that. Listing 3.5 Listing 3.5 Fuddifying String properties using a BeanPostProcessor

public class Fuddifier implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String name)
throws BeansException {
Field[] fields = bean.getClass().getDeclaredFields();
try {
for(int i=0; i < fields.length; i++) {
if(fields[i].getType().equals(java.lang.String.class)) {
fields[i].setAccessible(true);
String original = (String) fields[i].get(bean);
fields[i].set(bean, fuddify(original));
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return bean;
}
 
private String fuddify(String orig) {
if(orig == null) return orig;
return orig.replaceAll("(r|l)", "w").replaceAll("(R|L)", "W");
}
 
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
return bean;
}
}

The postProcessAfterInitialization() method cycles through all of the bean’s properties, looking for those that are of type java.lang.String. For each String property, it passes it off to the fuddify() method, which translates the String into Fudd-speak. Finally, the property is changed to the “Fuddified” text. (You’ll also notice a call to each property’s setAccessible() method to get around the private visibility of a property. I realize that this breaks encapsulation, but how else could I pull this off?) The postProcessBeforeInitialization() method is left purposefully unexciting;

it simply returns the bean unaltered. Actually, the “Fuddification” process could have occurred just as well in this method. Now that we have a Fuddifying BeanPostProcessor, let’s look at how to tell the container to apply it to all beans.

Registering bean postprocessors

If your application is running within a bean factory, you’ll need to programmatically register each BeanPostProcessor using the factory’s addBeanPostProcessor() method:

BeanPostProcessor fuddifier = new Fuddifier();
factory.addBeanPostProcessor(fuddifier);

More likely, however, you’ll be using an application context. For an application
context, you’ll only need to register the postprocessor as a bean within the context:

<bean class="com.springinaction.chapter03.postprocessor.Fuddifier" />

The container will recognize the fuddifier bean as a BeanPostProcessor and call its postprocessing methods before and after each bean is initialized.
As a result of the fuddifier bean, all String properties of all beans will be Fuddified. For example, suppose you had the following bean defined in XML:

<bean id="bugs" class="com.springinaction.chapter03.postprocessor.Rabbit">
<property name="description" value="That rascally rabbit!" />
</bean>

When the fuddifier postprocessor is finished, the description property will
hold “That wascawwy wabbit!”

Spring’s own bean postprocessors

The Spring Framework itself uses several implementations of BeanPostProcessor under the covers. For example, ApplicationContextAwareProcessor is a BeanPostProcessor that sets the application context on beans that implement the ApplicationContextAware interface (see section 3.5.6). You do not need to register ApplicationContextAwareProcessor yourself. It is preregistered by the applicationcontext itself.
In the next chapter, you’ll learn of another implementation of BeanPostProcessor. You’ll also learn how to automatically apply aspects to application beans using DefaultAdvisorAutoProxyCreator, which is a BeanPostProcessor that creates AOP proxies based on all candidate advisors in the container.

Postprocessing the bean factory

Whereas a BeanPostProcessor performs postprocessing on a bean after it has been loaded, a BeanFactoryPostProcessor performs postprocessing on the entire Spring container. The BeanFactoryPostProcessor interface is defined as follows:

public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException;
}

The postProcessBeanFactory() method is called by the Spring container after all bean definitions have been loaded but before any beans are instantiated (including BeanPostProcessor beans). For example, the following BeanFactoryPostProcessor implementation gives
a completely new meaning to the term “bean counter”:

public class BeanCounter implements BeanFactoryPostProcessor {
private Logger LOGGER = Logger.getLogger(BeanCounter.class);
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory)
throws BeansException {
LOGGER.debug("BEAN COUNT: " + factory.getBeanDefinitionCount());
}
}

BeanCounter is a BeanFactoryPostProcessor that simply logs the number of bean definitions that have been loaded into the bean factory. If you’re using an application context container, registering a BeanFactoryPostProcessor is as simple as declaring it as a regular bean:

<bean id="beanCounter" 
class="com.springinaction.chapter03.postprocessor.BeanCounter" />

When the container sees that beanCounter is a BeanFactoryPostProcessor, it will automatically register it as a bean factory postprocessor. You cannot use BeanFactoryPostProcessors with basic bean factory containers—this feature is only available with application context containers.
BeanCounter is a naive use of BeanFactoryPostProcessor. To find more meaningful examples of BeanFactoryPostProcessor, we have to look no further than the Spring Framework itself. You’ve already seen CustomerEditorConfigurer (see section 3.4), which is an implementation of BeanFactoryPostProcessor used to register custom PropertyEditors in Spring.

Another very useful BeanFactoryPostProcessor implementation is PropertyPlaceholderConfigurer. PropertyPlaceholderConfigurer loads properties
from one or more external property files and uses those properties to fill in placeholder variables in the bean wiring XML file. Speaking of PropertyPlaceholderConfigurer, that’s what we’ll look at next.

Вынесение конфигурации в properties-файлы

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

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="jdbc:hsqldb:Training" />
<property name="driverClassName" value="org.hsqldb.jdbcDriver" />
<property name="username" value="appUser" />
<property name="password" value="password" />
</bean>

Однако такая настройка источника данных непосредственно в файле с остальными бинами может быть нецелесообразна или даже вредна. Спецификой баз данных являются детали развертывания приложения. Наоборот же, цель файла с бинами в основном ориентирована на определение того, какие компоненты есть в вашем приложении,чтобы их объединить.

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

К счастью, в Spring можно легко экстернализовать (от англ."externalize" т.е. "вынести - вовне" ) любые properties-пары свойств, если Вы используете ApplicationContext в качестве Вашего Spring контейнера. Вы можете использовать PropertyPlaceholderConfigurer Spring-а, чтобы сообщить Spring о необходимости загрузки определенной конфигурации из внешнего properties-файла. Чтобы включить эту функцию необходимо объявить следующую связь в файле с бинами:

<bean id="propertyConfigurer" 
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="jdbc.properties" />
</bean>

Свойство location указывает Spring где искать файл со свойствами. В данном случае, это файл jdbc.properties содержащий следующую информацию для JDBC:

database.url=jdbc:hsqldb:Training
database.driver=org.hsqldb.jdbcDriver
database.user=appUser
database.password=password

Свойство location позволяет работать с одним файлом свойств. Если вы захотите разделить вашу конфигурацию на несколько файлов со свойствами, используйте свойство locations в PropertyPlaceholderConfigurer для установки списка файлов со свойствами:

<bean id="propertyConfigurer" 
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>jdbc.properties</value>
<value>security.properties</value>
<value>application.properties</value>
</list>
</property>
</bean>

Теперь вы можите заменить жестко прописанную конфигурацию в файле с бинами - соответствующими переменными. Синтаксически переменные описываются в форме ${variable}, аналогично тому как описываются свойства Ant и языке выражений в JSP. После замены переменными новый бин с источником данных будет выглядеть так:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${database.url}" />
<property name="driverClassName" value="${database.driver}" />
<property name="username" value="${database.user}" />
<property name="password" value="${database.password}" />
</bean>
Файл:Fig37.png
Рисунок 3.7 PropertyPlaceholderConfigurer позволяет отделить значения конфигурации в отдельный файл со свойствами и затем загружает конкретные значения вместо переменных исходя из описанного Spring контекста.

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

Использование PropertyPlaceholderConfigurer полезно для выделения части настроек Spring в отдельный файл свойств. Но java использует propertiesфайлы не только для конфигурации; они (файлы свойств) используются для хранения текстовых сообщений и для интернационализации. Посмотрим как интерфейс источника сообщений Spring может использовать для вынесения текстовых сообщений в properties-файлы.

Вынесение текстовых сообщений.

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

Поддержка в Java параметризации и интернационализации сообщений позволяет определить один или несколько properties-файлов которые содержат текст, для отображения в вашем приложении. Там всегда должен быть файл, который используется по-умолчанию, вместе с конкретными properties-файлами, содержащими сообщения на других языках. Например, если пакет ваших сообщений для приложения называется “trainingtext”, то вы можете иметь следующий набор файлов свойств с сообщениями:

trainingtext.properties - сообщения по умолчанию, когда locale не определена или когда для конкретной locale отсутствует файл со свойствами
trainingtext_en_US.properties - сообщения для пользователей, использующих английский язык в США
trainingtext_es_MX.properties - сообщения для пользователей, использующих испанский язык в Мексике
trainingtext_de_DE.properties - сообщения для пользователей, использующих немецкий язык в Германии

Например, для сообщений по умолчанию и на английском языке, properties-файл может состоять из записей
таких, как

course=class
student=student
computer=computer

Тем временем, эти же сообщения но на испанском будут выглядеть так:

course=clase
student=estudiante
computer=computadora

ApplicationContext Spring-а поддерживает параметризацию сообщений, делая их доступными для контейнера через интерфейс MessageSource:

public interface MessageSource {
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
}

Spring работает на чтение-использование реализации MessageSource. ResourceBundleMessageSource просто использует имеющийся в Java java.util.ResourceBundle для извлечения сообщений. Для того, чтобы использовать ResourceBundleMessageSource надо добавить следующий XML в файл определения бинов:

<bean id="messageSource" 
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>trainingtext</value>
</property>
</bean>

Это очень важно, чтобы данный бин назывался именно - messageSource, потому что ApplicationContext будет искать его по имени, когда будет производить установки внутреннего источника сообщений. Зато, Вам никогда не надо внедрять бин messageSource в ваши бины приложения, явно. Вместо этого вы получите доступ к сообщениям ApplicationContext-а через метод getMessage( ). Например, чтобы получить сообщение с именем "computer" используйте этот код:

Locale locale = … ; //determine locale
String text = context.getMessage("computer", new Object[0], locale);

Вы возможно будете использовать пареметризованные сообщения из контекста в web-приложении, чтобы отобразить их на web-страничке. В этом случае вы захотите использовать специальный JSP тэг Spring-а (<spring:message>) для извлечения сообщений и тогда будет не нужен непосредственный доступ к ApplicationContext:

 <spring:message code="computer" />

Но если мне нужны бины, а не JSP, для извлечения сообщений, то как я могу получить доступ к ApplicationContext? Для этого надо немного подождать. Или вы можете сразу перейти к разделу, где я расскажу как сделать бины "осведомленными" о своих контейнерах.

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

Снижение связности с использованием событий

Файл:Fig38.png
Рисунок 3.8 Источник публикаций событие, как радиостанция, вещающая события приложения для своих слушателей. Издатель и его слушатели полностью отделены друг от друга.

Внедрение зависимостей является основным способом Spring ослабить связи между объектами приложения - но это не единственный путь (решение). Еще одиним способом для объектов взаимодействовать друг с другом является публикация и прослушивание событий приложения. Используя события, объект события (издатель) может взаимодействовать с другими объектами без знания о том, какие объекты прослушивают. Кроме того, тот объект, который принимает события может реагировать на события без знания о том, кто публикует эти события.

Это взаимодействие событиями является аналогом радиостанции и ее аудитории слушателей, как показано на рисунке 3.8. Радиостанция не подключается напрямую к радиостанции, и радиостанция не знает о настройках других радиостанций. Однако, разделенные станции по-прежнему могли общаться со своими слушателями. В Spring любой бин в контейнере может быть или слушателем или издателем или и тем и другим. Давайте посмотрим как создать бины, которые участвуют в событиях, начнем с бинов, которые публикуют события приложения.

Публикация событий.

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

Вначале определим собсвенное событие, например следующее CourseFullEvent:

public class CourseFullEvent extends ApplicationEvent {
private Course course;
public CourseFullEvent(Object source, Course course) {
super(source);
this.course = course;
}
 
public Course getCourse() {
return course;
}
}

Далее необходимо опубликовать сообщение. У интерфейса ApplicationContext есть метод publishEvent(), который позволяет публиковать ApplicationEvents. Любой ApplicationListener, который зарегистрирован в контексте приложения будет извлекать событие при вызове метода onApplicationEvent():

ApplicationContext context = …;
Course course = …;
context.publishEvent(new CourseFullEvent(this, course));

К сожалению, для того, чтобы публиковать события, ваши бины должны иметь доступ к ApplicationContext. Это означает, что ваши бины должны знать в каком контейнере они запущены. Вы увидети как сделать так, чтобы бины знали о том, в каком контейнере они работают в разделе 3.5.6.

Но в начале, если publisher публикует событие и никто его не прослушивает, событие действительно произошло? Я оставлю этот философский вопрос для вас, чтобы обдумать позже. Пока, давайте удостоверимся, что события не обращают внимания на создаваемые бины, которые прослушивают события.

Прослушивание событий

В дополнение к событиям, которые публикуются бинами, Spring сам публикует несколько событий в течение времени жизни приложения.
Все эти события - подклассы абстрактного класса org.springframework.context.ApplicationEvent.
Вот три таких события, приложения:


  • ContextClosedEvent - публикуется, когда контекст приложения закрыт.
  • ContextRefreshedEvent - публикуется, когда контекст приложения инициализирован или обновлен.
  • RequestHandledEvent - публикуется в контексте web-приложения, когда обрабатывается запрос.


Большинство бинов не знает зачем они были опубликованы. Но, что, если вы хотите получать уведомления о событиях приложения?
Если вы хотите бин реагировал на события приложения, будь они опубликованы другим бином или контейнером, все, что вам нужно это реализовать
интерфейс org.springframework.context.ApplicationListener. Для этого необходимо реализовать метод onApplicationEvent(), который отвечает за реагирование на события приложения:

public class RefreshListener implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent event) {

}
}

Только одну вещь вы должны сделать - сообщить Spring-у о прослушивателе сообщений приложения, для этого надо просто зарегистрировать его в контексте бина:

<bean id="refreshListener" class="com.springinaction.foo.RefreshListener"/>

Когда контейнер загружает бины в контекст приложения, он увидит, что реализован ApplicationListener и будет помнить, что надо вызвать его метод onApplicationEvent() при публикации события. Одну вещь надо иметь в виду, события приложения обрабатываются синхронно. Итак, вы должны позаботится о том, чтобы любые сообщения обрабатывались быстро. В противном случае, это окажет негативное влияние на производительность приложения. Как упоминалось ранее, бин должен знать о контейнере приложения, чтобы иметь возможность публиковать события. Даже если объект не заинтересован в публикации событий, вы можите разработать бин, который знает о контексте приложения, пока он (контекст) жив.
Далее, давайте посмотрим как создать бины, которые внедряются с ссылками на их контейнер.

Создание "осведомленных" бинов

Вы видели фильм "Матрица"? В этом фильме , люди были невольно порабощены машинами, жили своими каждодневными жизнями в виртуальном мире, в то время как в сущность их жизнь состояла в то, чтобы быть разводимыми, для питания машин энергией. Главному персонажу, Томасу Андерсону
был дан выбор между приемом "красной пилюли" открывающей правду его существования или приемом "синей пилюли" с продолжением прежней жизни и игнорированием правды... Он выбрал красную пилюлю и сделался оседомленным о своей реальной личности и правде о витруальном мире.

Большая часть бинов запущенных в контейнере Spring, подобны людям в фильме "Матрица" Для бинов незнание, есть - счастье. Они (бины) не знают (им даже не положено знать), свои имена, или о том, что они работают внутри контейнера Spring.

Как правило это хорошо, потому если бин знает о контейнере, то он становится связанным со Spring и не может существовать вне контейнера.
Но иногда бинам надо знать больше. Иногда им надо знать кто они есть, и где они запущены. Иногда они должны взять "красную пилюлю". (как в фильме "Матрица")

"Красная пилюля" в случае Spring бинов это интерфейсы NameAware, BeanFactoryAware и ApplicationContextAware. Реализуя эти три интерфейса бины будут знать свое имя, и свой BeanFactory или ApplicationContext соответственно.

Но имейте ввиду, что реализуя эти интерфейсы, бины становятся связанными со Spring. И в зависимости от того, как бин использует это знание, вы не можете использовать его вне Spring.

Знание, кто ты есть.

Spring контейнер сообщает бину о его имени через интерфейс BeanNameAware. Этот интерсейс имеет один метод setBeanName(), который указывает контейнеру Spring имя бина, которое устанавливается через его идентификатор или имя атрибута <bean> файле связывания-бинов:

public interface BeanNameAware {
void setBeanName(String name);
}

Для бина может быть полезно знать свое имя в "бухгалтерских" целях. Например, если бин может существовать более чем в одном экземпляре в контексте приложения, для данного бина может быть полезной возможность , самоидентифицировать себя самого по имени и типу, в случае
логгирования собственных действий

Внутри самого Spring Framework, интерфейс BeanNameAware используется несколько раз. Одно известное использование: с бинами выполняющими функции планирования. Например, CronTriggerBean, реализующий интерфейс BeanNameAware для установки имени его Quartz CronTrigger задания.

Следующий отрывок кода из CronTriggerBean иллюстрирует это:

package org.springframework.scheduling.quartz;
 
public class CronTriggerBean extends CronTrigger implements …, BeanNameAware, … {

private String beanName;

public void setBeanName(String beanName) {
this.beanName = beanName;
}

public void afterPropertiesSet(){
if (getName() == null) {
setBeanName(this.beanName);
}

}

}

Вы не должны сделать ничего специального для того чтобы контейнер Spring вызывал метод setBeanName() в классе реализующем BeanNameAware. Когда бин загружается, контейнер увидит что бин реализует BeanNameAware и автоматически вызовет метод setBeanName(), передавая имя бина объявленное как id (либо как name)атрибут элемента <bean> в XML-файле бин-соединений.

Здесь CronTriggerBean расширяет CronTrigger. После установки контекстом Spring всех свойства бина, имя бина передается в метод setBeanName() (объявленный в CronTriggerBean), который используется для установки имени планируемого действия.
Этот пример иллюстрирует, использование BeanNameAware для демонстрации встроенной поддержки планировщика в Spring. Подробнее о планировании будет рассказано в главе 12. Пока, давайте посмотрим, как наделить бин, знанием о Spring контейнере внутри которого он живет.

Знание, где ты живешь

Как Вы видели в этой секции, иногда полезно для бина быть в состоянии получить доступ к контексту приложения. Возможно, Ваш бин нуждается в доступе к параметризовавшему тексту сообщения в источнике сообщения. Или возможно это потребность быть в состоянии опубликовать
события для слушателя событий, отвечающего на них. Безотносительно причины, Ваш бин должен знать о контейнере, в котором он живет.
Spring интерфейсы ApplicationContextAware и BeanFactoryAware позволяют бину быть осведомленным о его контейнере. Эти интерфейсы объявляют методы setApplicationContext() и setBeanFactory(), соответственно.

Контейнер Spring обнаружит, реализует ли какой-либо из Ваших бинов любой из этих интерфейсов и обеспечит BeanFactory или ApplicationContext.
Имея доступ к ApplicationContext бин может активно взаимодействовать с контейнером. Это может быть полезно для програмного нахождения зависимостей от контейнера (когда они не были вставлены самим контейнером) или для публикации событий в приложении.

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

public class StudentServiceImpl implements StudentService, ApplicationContextAware {
private ApplicationContext context;
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
public void enrollStudentInCourse(Course course, Student student)
throws CourseException;

context.publishEvent(new CourseFullEvent(this, course));

}

}

Знание о контейнере приложение - одновременно благословение и проклятие для бина. С одной стороны доступ к контексту приложения предоставляет бину большую мощь С другой стороны знание о контейнере связывает бин со Spring, а это то чего в принципе желательно бы избегать по возможности.

До сих пор мы предполагали что все бины в контейнере Spring реализованы как Java классы. Это резонное предположение, но это не обязательный вариант. Давайте посмотрим как добавить динамическое поведение в приложение связыванием бинов реализованных с использованием скриптовых языков.

Скриптовые бины.

Файл:Fig39.png
Figure 3.9 Spring isn’t limited to only injecting POJOs into POJOs. You can also dynamically modify your application by injecting scripted beans into POJOs.

Когда Вы пишете Java код он превращается в бины в Вашего Spring приложениия и Вы в конечном итоге компилируете его в байт-код.
исполняемый JVM. Более того, Вы вероятно упакуете скомпилированный код в JAR, WAR, или EAR файл для дальнешего развертывания. Но что если после развертывания приложения Вы захотите изменить поведение Вашего кода.

Вы видите, что проблема со статически скомпилированным кодом состоит в том, что он... таки-да... действительно - статичен.
Будучи однажды скомпилированным в файл класса, и упакованным в архив развертывания, он трудно изменяем без перекомпиляции, повторной упаковки и развертывания всего приложения. Во многих обстоятельствах, это приемлемо (и возможно даже желаемо). Но даже в этом случае, статически скомпилированный код создает трудности, в реализации быстрых ответов на динамичные потребности бизнеса.

Например, предположим Вы разрабатываете приложение для Е-коммерции. В пределах приложения код рассчитывает промежуточные итоги и общую сумму покупки основанной на наборе выбранных позиций в заказе, подходящих тарифов доставки, и налогов с продажи. Но что, если Вы нуждаетесь в способности отказаться от налога с продаж в течение одного дня? Если налоговая часть вычисления Вашего приложения статически собрана с
остальным кодом, едиственно доступный дл Вас вариант - это развертывнаие "безналоговой" версии Вашего приложения в полночь, с последующи развертыванием базовой версии в следующую полночь.

You had better start brewing the coffee, because you’re going to be up late two nights in a row. With Spring, it’s possible to write scripted code in either Ruby, Groovy, or BeanShell and wire it into a Spring application context as if it were any other Javabased bean, as illustrated in figure 3.9. In the next few subsections, I’ll demonstrate how to wire Ruby, Groovy, and BeanShell scripts as Spring-managed beans.

If you’re fan of Calypso music then you’re in for a treat—I’m going to demonstrate scripted beans to the tune of one of the genre’s most famous songs. Please follow along as I demonstrate dynamically scripted beans by putting a scripted lime in a Java coconut.

Добавляем Лайм в Кокос

To illustrate how to script beans in Spring, let’s inject a scripted implementation of a Lime interface into a Java Coconut object. To start, let’s look at the Coconut class as defined in listing 3.6.

Listing 3.6 A Java in a coconut shell

package com.springinaction.scripting;
 
public class Coconut {
public Coconut() {}
public void drinkThemBothUp() {
System.out.println("You put the lime in the coconut...");
System.out.println("and drink 'em both up...");
System.out.println("You put the lime in the coconut...");
lime.drink();
}
 
// injected
private Lime lime;
public void setLime(Lime lime) {
this.lime = lime;
}
}

The Coconut class has one simple method, called drinkThemBothUp(). When this method is invoked, certain lyrics from Mr. Nilsson’s song are printed to the System.out. The last line of the method invokes a drink() method on an injected Lime instance to print the final lyric. The injected Lime is any object that implements the following interface:

package com.springinaction.scripting;
 
public interface Lime {
void drink();
}

When wired up in Spring, the Coconut class is injected with a reference to a Lime object using the following XML:

<bean id="coconut" class="com.springinaction.scripting.Coconut">
<property name="lime" ref="lime" />
</bean>

At this point, I’ve shown you nothing special about the Coconut class or the Lime interface that hints to scripted beans. For the most part, the code presented up to this point resembles the basic JavaBean and DI examples from chapter 2. The only thing missing is the exact implementation of the Lime interface and its declaration in the Spring context. The fact is that any Java-based implementation
of the Lime interface will do. But I promised you a scripted Lime and so a scripted Lime is what I’ll deliver next.

Пишем скриптовый бин

When scripting the Lime interface, we can choose to implement it as either a Ruby, Groovy, or BeanShell script. But regardless of which scripting language is chosen, we first need to do some setup in the Spring context definition file. Consider the following <beans> declaration to see what needs to be done:

<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="https://www.springframework.org/schema/lang"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
https://www.springframework.org/schema/lang
https://www.springframework.org/schema/lang/spring-lang-2.0.xsd">

...
</beans>

Spring 2.0 comes with several new configuration elements, each defined in an XML namespace and schema. We'll see a lot more of Spring’s custom configuration namespaces throughout this book. But for now, suffice it to say that the highlighted portions of this <beans> declaration tell Spring that we’re going to use certain configuration elements from the lang namespace. Now that the namespace has been declared in the Spring context file, we’re ready to begin scripting our Lime. Let’s start with a Ruby-colored Lime.

Пишем Лайм на Ruby

In recent years, Ruby has caught the attention of many Java developers, so it’d be no surprise if you’d like to write your scripted beans using this very popular scripting language. The following Ruby script implements the Lime interface and its

drink() method:
class Lime
def drink
puts "Called the doctor woke him up!"
end
end
Lime.new

A very important thing to note here is that the last line of the script instantiates a new Lime object. This line is crucial—without it, there will be no instance of the Lime created that can be wired into other Spring objects. Wiring the Ruby Lime in Spring is a simple matter of using the <lang:jruby> configuration element as follows:

<lang:jruby id="lime" 
script-source="classpath:com/springinaction/scripting/Lime.rb"
script-interfaces="com.springinaction.scripting.Lime" />
<lang:jruby>

requires two attributes to be set. The first, script-source, tells Spring where it can locate the script file. Here, the script file is Lime.rb and can be found in the classpath in the same package as the rest of the example code. Meanwhile, the script-interfaces attribute tells Spring what Java interface that the script will be implementing. Here it has been set to our Lime interface.

Пишем Groovy Лайм

Despite Ruby’s growing popularity, many developers will be targeting the Java platform for some time to come. Groovy is a language that mixes some of the best features of Ruby and other scripting languages into a familiar Java syntax—effectively giving a best-of-both-worlds option for Java developers.

For those of you who favor Groovy scripting, here’s an implementation of the Lime interface as a Groovy class:

class Lime implements com.springinaction.scripting.Lime {
void drink() {
print "Called the doctor woke him up!"
}
}

Wiring a Groovy script as a Spring bean is as simple as using the <lang:groovy> configuration element. The following <lang:groovy> configuration loads the Groovy implementation of the Lime interface:

<lang:groovy id="lime" 
script-source="classpath:com/springinaction/scripting/Lime.groovy" />

As with the <lang:jruby> element, the script-source attribute specifies the location of the Groovy script file. Again, we’re locating the script file in the classpath in the same package as the example code. Unlike the <lang:jruby> element, however, <lang:groovy> doesn’t require
(or even support) a script-interfaces attribute. That’s because there’s enough information in the Groovy script itself to indicate what interfaces the script implements. Notice that the Groovy Lime class explicitly implements com.springinaction.scripting.Lime.

Пишем Лайм в BeanShell

Another scripting language supported in Spring is BeanShell. Unlike Ruby and Groovy, which both provide their own syntax, BeanShell is a scripting language that mimics Java’s own syntax. This makes it an appealing option if you want to script portions of your application but do not want to have to learn another language. Completing our tour of scripting languages that can be wired in Spring, here’s a BeanShell implementation of the Lime interface:

void drink() {
System.out.println("Called the doctor woke him up!");
}

Probably the first thing you noticed about this BeanShell implementation of Lime is that there isn’t a class definition—only a drink() method is defined. In BeanShell scripts, you only define the methods required by the interface, but no class. Wiring the BeanShell lime is quite similar to wiring the Ruby lime, except that you use the <lang:bsh> element as follows:

<lang:bsh id="lime" script-source="classpath:com/springinaction/scripting/Lime.bsh" 
script-interfaces="com.springinaction.scripting.Lime" />

the interface being defined in the script. Now you’ve seen how to configure a scripted bean in Spring and how to wire it into a property of a POJO. But what if you want the injection to work the other way? Let’s see how to inject a POJO into a scripted bean.

Внедрение свойств скриптовых бинов

To illustrate how to inject properties of a scripted bean, let’s flip our lime-coconut example on its head. This time, the coconut will be a scripted bean and the lime will be a Java-based POJO. First up, here’s the Lime class in Java:

package com.springinaction.scripting;
public class LimeImpl implements Lime {
public LimeImpl() {}
public void drink() {
System.out.println("Called the doctor woke him up!");
}
}

LimeImpl is just a simple Java class that implements the Lime interface. And here it is configured as a bean in Spring:

<bean id="lime" class="com.springinaction.scripting.LimeImpl" />

Nothing special so far. Now let’s write the Coconut class as a script in Groovy:

class Coconut implements com.springinaction.scripting.ICoconut {
public void drinkThemBothUp() {
println "You put the lime in the coconut..."
println "and drink 'em both up..."
println "You put the lime in the coconut..."
lime.drink()
}
com.springinaction.scripting.Lime lime;
}

As with the Java version of Coconut, a few lyrics are printed and then the drink() method is called on the lime property to finish the verse. Here the lime property is defined as being some implementation of the Lime interface. Now all that’s left is to configure the scripted Coconut bean and inject it with the lime bean:

<lang:groovy id="coconut" 
script-source="classpath:com/springinaction/scripting/Coconut.groovy">
<lang:property name="lime" ref="lime" />
</lang:groovy>

Here the scripted Coconut has been declared similar to how we declared the scripted Lime in previous sections. But along with the <lang:groovy> element is a <lang:property> element to help us with dependency injection.

The <lang:property> element is available for use with all of the scripted bean elements. It is virtually identical in use to the <property> element that you learned about in chapter 2, except that its purpose is to inject values into the properties of scripted beans instead of into properties of POJO beans. In this case, the <lang:property> element is the lime property of the coconut bean with a reference to the lime bean—which in this case is a JavaBean. You may find it interesting, however, that you can also wire scripted beans into the properties
of other scripted beans. In fact, it’s quite possible to wire a BeanShell-scripted bean into a Groovy-scripted bean, which is then wired into a Ruby-scripted bean.

Following that thought to an extreme, it’s theoretically possible to develop an entire Spring application using scripted languages!

Обновление скриптовых бинов

One of the key benefits of scripting certain code instead of writing it in statically compiled Java is that it can be changed on the fly without a recompile or redeployment of the application. If the Lime implementation were to be written in Java and you decided to change the lyric that it prints, you’d have to recompile the implementation class and then redeploy the application. But with a scripting
language, you can change the implementation at any time and have the change applied almost immediately.

When I say “almost immediately,” that really depends on how often you’d like Spring to check for changes to the script. All of the scripting configuration elements have a refresh-check-delay attribute that allows you to define how often (in milliseconds) a script is refreshed by Spring.

By default, refresh-check-delay is set to –1, meaning that refreshing is disabled. But suppose that you’d like the Lime script refreshed every 5 seconds. The following <lang:jruby> configuration will do just that:

<lang:jruby id="lime" script-source="classpath:com/springinaction/scripting/Lime.rb" 
script-interfaces="com.springinaction.scripting.Lime" refresh-check-delay="5000" />

It should be pointed out that although this example is for <lang:jruby>, the refresh-check-delay attribute works equally well with <lang:groovy> and <lang:bsh>.

Скриптовые inline бины

Typically you’ll define your scripted beans in external scripting files and refer to them using the script-source attribute of the scripting configuration elements. However, in some cases, it may be more convenient to write the scripting code directly in the Spring configuration file.

To accommodate this, all of the scripting configuration elements support a <lang:inline-script> element as a child element. For example, the following XML defines a BeanShell-scripted Lime directly in the Spring configuration:

<lang:bsh id="lime" script-interfaces="com.springinaction.scripting.Lime">
<lang:inline-script>
<lt;![CDATA[
void drink() {
System.out.println("Called the doctor woke him up!");
}
]]>
</lang:inline-script>
</lang:bsh>

Instead of using script-source, this lime bean has the BeanShell code written as the content of a <lang:inline-script> element.
Take note of the use of <![CDATA[…]]> when writing the inline script. The script code may contain characters or text that may be misinterpreted as XML.

The <![CDATA[…]]> construct prevents scripted code from being parsed by the XML parser. In this example the script contains nothing that would confuse the XML parser. Nevertheless it’s a good idea to use <![CDATA[…]]> anyway, just in case the scripted code changes.
In this section, you were exposed to several Spring configuration elements that step beyond the <bean> and <property> elements that you were introduced to in chapter 2. These are just a few of the new configuration elements that were introduced in Spring 2.0. As you progress through this book, you’ll be introduced to even more configuration elements.

Резюме

As with all of the scripting elements, script-source indicates the location of the script file. And, as with the <lang:jruby> element, script-interfaces specifies Spring’s primary function is to wire application objects together. While chapter 2 showed how to do the basic day-to-day wiring, this chapter showed the more oddball wiring techniques.

To reduce the amount of repeated XML that defines similar beans, Spring offers the ability to declare abstract beans that describe common properties and then “sub-bean” the abstract bean to create the actual bean definitions. Perhaps one of the oddest features of Spring is the ability to alter a bean’s functionality through method injection. Using method injection, you can swap out a bean’s existing functionality for a replaced method definition. Alternatively,используя сеттер-инъекции, Вы можете заменять геттер-методы на другие объявленные с помощью Spring геттеры-методы возвращающие ссылки на бины. Не все объекты в приложении создаются или управляются Spring. Чтобы разрешить
DI для объектов, которые Spring не создает, Spring обеспечивает средства объявления объектов как "Spring-конфигурируемые". Spring-конфигурированные бины перехватываются контейнером Spring, после их создания, и конфигурируются на основе Spring бин шаблона.
Spring использует преимущества редакторов полей так что даже комплексные объекты такие как URL и массивы могут быть сконфигурированы с использованием значений типа String. В этой главе, Вы увидите как создавать собственные редакторы полей для упрощения конфигурирования комплексных полей объекта

Sometimes it is necessary for a bean to interact with the Spring container. For those circumstances, Spring provides several interfaces that enable a bean to be postprocessed, receive properties from an external configuration file, handle application events, and even know their own name. Finally, for the fans of dynamically scripted languages, Spring lets you write beans in scripted languages such as Ruby, Groovy, and BeanShell. This feature supports dynamic behavior in an application by making it possible to hot-swap bean definitions written in one of three different scripting languages.

Now you know how to wire together objects in the Spring container and have seen how DI helps to loosen the coupling between application objects. But DI is only one of the ways that Spring supports loose coupling. In the next chapter, I’ll look at how Spring’s AOP features help break out common functionality from the application objects that it affects.]]> Книги по Java https://linexp.ru?id=4733 Wed, 29 Jun 2022 14:07:40 GMT <![CDATA[Глава 1 Spring in Action 2th edition]]>

"Пружина" в действии

Эта глава охватывает


  • Исследование основных модулей Spring
  • Разъединение объектов приложений
  • Управление сквозными задачами с AOP

Все началось с компонента (bean) В 1996 язык программирования Java был еще молодой, захватывающей, перспективной платформой. Многие разработчики пришли в этот язык после того, как они увидели, как создавать богатые и динамические web-приложения используя апплеты.

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

Это было в декабре того года, когда Sun Microsystems обнародовал 1.00-A спецификацию JavaBeans. JavaBeans определили компонентную модель Java. Эта спецификация определила набор приемов программирования, который предоставлял возможность повторного использования и простого соединения Java-объектов в более сложные приложения. Несмотря на то, что JavaBeans были предназначены в качестве универсального средства определения повторно используемых компонентов, они преимущественно использовались в качестве шаблона для создания фрагментов пользовательского интерфейса.

Они оказались слишком простыми, чтобы быть способными к реальной работе. Enterprise-разработчики хотели большего. Сложные приложения часто требовали такие сервисы как поддержка транзакций, безопасность и распределенные вычисления, которые абсолютно не поддерживались спецификацией JavaBeans. Поэтому в марте 1998 Sun выпустил версию 1.0 спецификации Enterprise JavaBeans (EJB). Эта спецификация расширяла понятие компонентов Java на стороне сервера, предоставляя весьма недостающие enterprise сервисы, но была не в состоянии поддерживать простоту исходной спецификации JavaBeans.

На самом деле, за исключением названия, EJB имел весьма небольшое сходство с исходной спецификацией JavaBeans. Несмотря на тот факт, что многие удачные приложения созданы на базе EJB, к сожалению EJB никогда не достигала своей цели: упростить разработку корпоративных приложений. Это правда, что декларативное программирование в модели EJB упрощает многие инфраструктурные аспекты разработки, такие как транзакционность и безопасность. Однако, с другой стороны, EJB усложняют разработку, предписывая дескрипторы развертывания и отягощая код. Спустя время, многие разработчики разочаровались в EJB. В результате, его популярность в последние годы начала снижаться, заставляя многих разработчиков искать более простой путь.

Сегодня, разработка Java компонентов вернулась к своим истокам. Новые технологии программирования, включая аспектно-ориентированное программирование (aspect-oriented programming (AOP)) и внедрение зависимостей (dependency injection (DI)), дают JavaBeans большую часть возможностей, ранее заложенных в EJB. Эти технологии оснащают обычные Java объекты (plain-old Java objects (POJO)) моделью декларативного программирования, напоминающим EJB, но без всей сложности спецификации EJB. Больше не надо создавать громоздкий EJB компонент, когда будет достаточно простого компонента JavaBean.

По правде, даже EJB развились в соответствии с моделью программирования основанной на POJO. Используя идеи DI и AOP, последняя спецификация EJB гораздо проще своих предшественников. Для многих разработчиков, тем не менее, этот шаг очень мал и слишком запоздал. К тому времени, когда спецификация EJB 3 вышла на сцену, другие POJO-ориентированные фреймворки уже утвердились в Java сообществе в качестве стандартов де-факто.

Передовым средством простой разработки, основанной на POJO, является Spring Framework, который мы будем изучать на протяжении этой книги. В этой главе мы собираемся рассмотреть Spring Framework на верхнем уровне, давая вам представление о том, что такое Spring. Эта глава дает хорошее понимание круга задач, которые решает Spring и готовит почву для остальной части книги. Перво-наперво – давайте выясним что такое Spring.


Source(s): Spring in Action 2th edition

Что такое Spring?

Spring – это фреймворк с открытым исходным кодом, созданный Родом Джонсоном и описанный в его книге Expert One-on-One: J2EE Design and Development. Она была посвящена проблематике разработки корпоративных приложений. Spring делает возможным использование простых компонентов JavaBeans для достижения вещей, которые ранее были возможны только с EJB. Хотя полезность Spring не ограничивалась исключительно разработкой серверной стороны. Любое Java приложение может извлечь выгоду из Spring с точки зрения простоты, тестируемости и слабой связанности.

ЗАМЕЧАНИЕ

Чтобы избежать неоднозначности, я буду использовать слово "бин", обращаясь к обычному компоненту, соответствующему спецификации JavaBeans, и "EJB", обращаясь к компоненту, соответствующему спецификации EnterpriseJavaBeans. Я также время от времени буду использовать термин "POJO" (обычный объект Java - plain-old Java object) в качестве синонима слову "бин".

Spring делает многие вещи, но, если обнажить его до уровня базовых компонентов, Spring – это легковесный dependency injection (DI) и аспектно-ориентированный (AOP) контейнер и фреймворк. Это довольно сложно произносимо, но хорошо раскрывает основную цель Spring. Чтобы определение Spring имело больше смысла, давайте разберем это описание:


  • ЛегковесныйSpring является легковесной с точки зрения размера и накладных расходов. Весь Spring Framework можно разделить на простые JAR файлы, которые весят всего порядка 2.5 МВ. А издержки обработки при использовании Spring незначительны. К тому же Spring ненавязчив: в приложении объекты приложения, использующего Spring, зачастую не имеют зависимостей от специфических классов Spring.
  • Dependency Injection (инъекция зависимости) – Spring поддерживает слабое связывание посредством технологии, известной как dependency injection (DI). При использовании DI объекты пассивно отдают свои зависимости вместо того, чтобы создавать или искать зависимые объекты самостоятельно. Вы можете думать о DI как о перевернутом JNDI – вместо поиска объектом зависимостей в контейнере, контейнер предоставляет зависимости объекту на этапе создания не дожидаясь запроса.
  • Аспектно-ориентированный (AOP)Spring поставляется с богатой поддержкой аспектно-ориентированного программирования (AOP), что делает возможным взаимосвязанную разработку, отделяя бизнес-логику приложения от системных сервисов (таких как аудит и управление транзакциями). Объекты приложения делают то, что должны делать – выполняют прикладную логику – и ничего больше. Они не отвечают за другие системные вопросы (или даже не осведомлены о них), такие как регистрация событий (logging) или поддержка транзикций.
  • Контейнер – Spring является контейнером в том смысле, что он содержит в себе и управляет жизненным циклом и конфигурацией программных объектов. В Spring можно определить порядок создания программных объектов, их конфигурацию, и то, как они взаимодействуют друг с другом. FrameworkSpring позволяет конфигурировать и составлять сложные приложения из простых компонентов.

В Spring программные объекты соединены декларативно, обычно в XML файле. Кроме того Spring обеспечивает инфраструктурную функциональность (управление транзакций, взаимодействие с фреймворками хранения данных и пр.), оставляя разработку прикладной логики за вами. Итак: если обнажить Spring до уровня базовых компонентов, вы получите фреймворк, который помогает вам разрабатывать слабо связанный программный код. Даже если это все, что может Spring, преимущества слабого связывания (масштабируемость и простота тестирования) делают Spring стоящим фреймворком для создания приложений.

Но Spring - больше. Spring Framework поставляется с несколькими модулями, основанными на DI и AOP, что делает его многофункциональной платформой разработки приложений.

Модули Spring

Spring фреймворк состоит из нескольких структурированных модулей (рисунок 1.1). В совокупности данные модули предоставляют вам все необходимое для разработки приложений уровня предприятий. Но вы не обязаны основывать ваше приложение только на фреймворке Spring. Вы свободны в выборе модулей , которые необходимы в вашем приложении, и поиске альтернатив в том случае, если Spring не отвечает всем требованиям. Фактически Spring предлагает точки интеграции с некоторыми другими фреймворками и библиотеками, и вам не придется писать их самим.

Рисунок 1.1 Фреймворк Spring состоит из нескольких структурированых модулей, построенных на базе ядра контейнера. Данный модульный принцип позволяет использовать только необходимую часть фреймворка Spring в отдельном приложении.

Как можно видеть, все модули Spring построены на базе ядра контейнера. Контейнер, являясь существенной частью Spring, определяет, как компоненты создаются, конфигурируются и управляются. Вы будете неявно использовать эти классы, когда будете конфигурировать ваше приложение. Но как разработчика, вас, вероятно, более будут интересовать другие модули, которые используют сервисы, предоставляемые контейнером. Эти модули обеспечат фреймворки, с помощью которых вы будете строить ваше приложение, такими сервисами как AOP и persistence.

Давайте рассмотрим модули Spring на рисунке 1.1, каждый в отдельности, чтобы увидеть, как они вписываются в общую картину Spring.

Ядро контейнера

В самом основании рисунка 1.1, вы найдете ядро контейнера Spring. Ядро контейнера предоставляет основную функциональность фреймворка Spring. Этот модуль состоит из BeanFactory, который является основным контейнером Spring и базисом, на котором зиждиться Spring DI. Модуль ядра (центральная часть любого приложения Spring) будет обсуждаться на протяжении всей книги, начиная с главы 2, где мы исследуем связывание компонентов с использованием DI.

Модуль контекста приложения

Контекст приложения Spring строится на ядре контейнера. BeanFactory в модуле ядра делает Spring контейнером, в то время как модуль контекста делает его фреймворком. Данный модуль расширяет концепцию BeanFactory, добавляя поддержу интернационализации (I18N) сообщений, событий жизненного цикла приложения, и валидации. Кроме того, этот модуль предлагает многие enterprise сервисы, такие как email, доступ с помощью JNDI, EJB интеграцию, удаленный вызов и запланированный запуск. Также включена поддержка интеграции с фреймворками шаблонов, такими как Velocity и FreeMarker.

AOP модуль

Spring обеспечивает богатую поддержку аспектно-ориентированного программирования в своем AOP модуле. Данный модуль работает как базис при разработке ваших собственных аспектов в приложении, построенном с использованием Spring. Как и DI, AOP содействует слабой связанности объектов приложения. Однако с помощью AOP аспекты приложения (такие как транзакции и безопасность) отделяются от объектов, к которым они применяются. AOP module в Spring предоставляет несколько подходов в построении аспектов, включая создание аспектов, основанных на интерфейсах AOP Alliance (https://aopalliance.sf.net) и поддержку AspectJ. Поддержка AOP в Spring будет рассмотрена более подробно в главе 4.

Модуль абстракции JDBC и DAO

Работа с JDBC зачастую сводится к обширному использованию шаблонного кода, который получает соединение, создает SQL выражение, обрабатывает результирующее множество, а затем закрывает соединение. JDBC и модуль Объектов Доступа к Данным (DAO) в Spring абстрагируются от шаблонного кода, что позволит вам поддерживать код, касающийся доступа к базам данных, прозрачным и простым, а также предотвратит проблемы, появляющиеся в результате ошибки освобождения ресурсов базы данных. Этот модуль также формирует важный слой исключений, основаных на сообщениях об ошибках, выдаваемых некоторыми серверами баз данных. Так что больше никаких попыток расшифровать непонятные и проприетарные сообщения об ошибках SQL! К тому же данный модуль использует AOP модуль, чтобы предоставить сервисы управления транзакциями для объектов в приложении на Spring. Мы увидим, как основанная на шаблонах JDBC абстракция может сильно упростить JDBC код, когда будем рассматривать доступ к данным в Spring в главе 5.

Модуль интеграции объектно-реляционного отображения (ORM)

Для тех, кто предпочитает использовать инструментарий объектно-реляционного связывания (ORM) поверх JDBC, Spring предоставляет ORM модуль. Поддержка ORM в Spring строится на поддержке DAO, обеспечивающей удобный способ создания DAO объектов для некоторых ORM решений. Spring не пытается реализовать свое собственное ORM решение, но просто предоставляет рычаги управления некоторыми популярными ORM фреймворками, включая Hibernate, Java Persistence API, Java Data Objects, и iBATIS SQL Maps. Управление транзакциями в Spring поддерживает все данные ORM фреймворки так же, как и JDBC. В добавок к основанной на шаблонах JDBC абстракции, мы рассмотрим как Spring предоставляет похожие абстракции для ORM и persistence фреймворках в главе 5.

Расширения Управления Java (JMX)

Раскрытие внутренних деталей Java приложения для управления ими является важной частью завершения приложения для выпуска. Модуль JMX упрощает экспозицию ваших компонентов приложения в качестве JMX MBeans. Это делает возможным мониторинг и переконфигурацию исполняемого приложения. Поддержка JMX в Spring будет рассмотрена в главе 12.

Java EE Connector API (JCA)

Эскиз enterprise приложения изобилует смесью приложенией, исполняемых в наборе разнородных серверов и платформ. Интеграция всех этих приложений может быть непростым делом. Java EE Connection API (лучше известный как JCA) предоставляет стандартный способ интеграции Java приложений с разнообразными информационными системами уровня предприятий, включая мейнфреймы и базы данных. Во многих отношениях JCA очень похож на JDBC за исключением того, что JDBC сфокусирован на доступе к базам данных, а JCA является более универсальным API, используемым для соединения к унаследованным системам. Поддержка JCA в Spring похожа на поддержку JDBC в данном фреймворке, в том смысле, что шаблонный код JCA абстрагируется в шаблоны.

MVC фреймворк в Spring

Парадигма Модель/Вид/Контроллер (MVC) является общепринятым подходом к построению веб-приложений таким образом, чтобы пользовательский интерфейс был отделён от логики приложения. Java не испытывает недостатка в MVC фреймворках, таких как Apache Struts, JSF, WebWork, и Tapestry, которые являются наиболее популярным выбором среди MVC. Хотя Spring интегрируется с несколькими популярными MVC фреймворками, он также поставляется со своим собственным очень мощным MVC фреймворком, который использует слабо связные техники Spring в веб-слое приложения. Spring MVC будет рассмотрен в главах 13 и 14.

Portlet MVC в Spring

Многие веб-приложения построены по принципу страниц, который заключается в том, что каждый запрос к приложению дает на выходе отображение совершенно новой страницы. Каждая страница обычно представляет специфический набор информации или запрос новых данных от пользователя в определенной форме. В отличие от этого, приложения, основанные на портлетах, сосредотачивают несколько частей функциональности в одну веб-страницу. Это обеспечивает представление в нескольких приложениях одновременно. Если вы разрабатываете приложения с использованием портлетов, то вы определенно захотите взглянуть на Portlet MVC фреймворк в Spring. Portlet MVC основывается на Spring MVC и предоставляет набор контроллеров, которые поддерживают Java portlet API.

Веб модуль в Spring

Spring MVC и Spring Portlet MVC требуют особого подхода при загрузке контекста приложения Spring. Поэтому веб-модуль Spring предоставляет специальные классы для поддержки Spring MVC и Spring Portlet MVC. К тому же веб-модуль предлагает поддержку некоторых веб-ориентированых задач, таких как загрузку файлов несколькими частями и автоматическое связывание параметров запроса с вашими бизнес-объектами. Также он содержит поддержку интеграции с Apache Struts и JavaServer Faces (JSF).

Удаленный вызов

Не все приложения работают самостоятельно. Зачастую им необходимо задействовать функциональность других приложений, чтобы выполнить свою работу. При доступе по сети к другой программе для коммуникации используется некоторая форма удаленного вызова. Поддержка удаленного вызова в Spring делает доступной функциональность Java объектов в качестве удаленных объектов. Или, в случае, когда необходим удаленный доступ к объектам, модуль для удаленного вызова упрощает компоновку удаленных объектов в ваше приложение, как если бы они были обычными локальными объектами Java. Доступно несколько вариантов удаленного вызова, включая Remote Method Invocation (RMI), Hessian, Burlap, JAX-RPC и собственный HTTP Invoker. В главе 8 будут рассмотрены различные варианты реализации удаленного доступа поддерживаемые Spring.

Java Message Service (JMS)

Недостаток удаленного доступа в том, что он зависит от надежности сети и требует, чтобы оба конца связи были доступны. Связь, ориентированная на сообщения, с другой стороны, более надежна и гарантирует доставку сообщений, даже когда сеть и конечные узлы не надежны. Модуль Spring Java Message Service (JMS) помогает отправлять сообщения в JMS очередь сообщений и темы. В то же время, этот модуль также помогает создавать POJO объекты, управляемые сообщениями, способные принимать асинхронные сообщения. Мы увидим, как использовать Spring для отправки сообщений, в главе 10.

Хотя Spring охватывает многие вопросы, важно понимать, что Spring избегает повторное изобретение колеса, где это возможно. Spring опирается в большей степени на существующие API фреймворки. Например, как мы увидим дальше в главе 5, Spring не реализует собственный пакет доступа к данным, - вместо этого он взаимодействует с некоторыми платформами, способными на это, включая простой JDBC, iBATIS, Hibernate и JPA.

Теперь, когда вы увидели полную картину, давайте посмотрим, как работают DI и AOP функции Spring. Мы намочим ноги, внедряя наш первый компонент в контейнер Spring.

Быстрый старт

Dependency Injection - самая основная вещь Spring-а. Но на что похожа DI?
По великой традиции программирования, я начну показывать Вам как Spring работает на легком примере "Hello World". В отличие от оригинала программы Hello World, тем не менее, этот пример будет немного изменен, чтобы продемонстрировать основы Spring.

Первый класс “Springified” в примере Hello World нуждается в классе, который бы распечатал знакомое приветствие. Листинг 1.1 показывает интерфейс GreetingService, который определяет действие класса.

Листинг 1.1 Интерфейс для сервиса приветствия

package com.springinaction.chapter01.hello;
 
public interface GreetingService {
void sayGreeting()
}

GreetingServiceImpl (Листинг 1.2) имплементирует интерфейс GreetingService. Хотя и не обязательно скрывать информацию позади интерфейса, это
очень рекомендовано в качестве способа отделить реализацию от ее контракта.

Листинг 1.2 GreetingServiceImpl, который печатает приветствие

package com.springinaction.chapter01.hello;
 
public class GreetingServiceImpl implements GreetingService {
private String greeting;
public GreetingServiceImpl() {}
public GreetingServiceImpl(String greeting) {
this.greeting = greeting;
}
public void sayGreeting() {
System.out.println(greeting);
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
}

Класс GreetingServiceImpl имеет единственное свойство: приветствие. Это свойство является простой строкой, которая печатается, когда происходит вызов метода sayGreeting(). Вы, возможно, обратили внимание, что приветствие может быть установлено двумя разными способами:
При помощи конструктора, и при помощи сеттера.

Вот только не совсем очевидно, кто будет вызывать конструктор или метод setGreeting(), чтобы установить приветствие.
На самом деле, мы собираемся позволить установить свойство приветствия контейнеру Spring-а. Конфигурационный файл Spring
(hello.xml) в листинге 1.3 говорит контейнеру, как конфигурировать сервис приветствия.

Листинг 1.3 Конфигурирование Hello World в Spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="greetingService" class="com.springinaction.chapter01.hello.
<property name="greeting" value="Buenos Dias!" />

</bean>
</beans>

XML файл в листинге 1.3 объявляет экземпляр класса GreetingServiceImpl в контейнере Spring конфигурирует свойство приветствия значением “Buenos Dias!”. Позволим себе немного углубиться в подробности XML файла, чтобы понять, как это работает.

В корне этого простого XML файла находится элемент <beans>, который является корневым элементом любого конфигурационного файла Spring-а. Элемент <bean> используется для того, чтобы говорить контейнеру Spring-а о классе и как он должен быть сконфигурирован. Здесь аттрибут id задает имя бина greetingService, и аттрибут class используется для того, чтобы указать нужное имя класса.
В пределах элемента <bean>, элемент <property> используется для того, чтобы задать свойство, В нашем случае - приветствия. Как показано здесь, элемент <property> говорит контейнеру Spring вызывать setGreeting(), и отправить “Buenos Dias!” (немного таланта в испанском) описав бин.

Следующий фрагмент кода иллюстрирует примерно, что контейнер делаетпри создании экземпляра службы поздравлений, основанной на определении XML в листинге 1.3:

GreetingServiceImpl greetingService = new GreetingServiceImpl();
greetingService.setGreeting("Buenos Dias!");

По-другму, вы можете установить свойство приветствия через конструктор GreetingServiceImpl. Например:

<bean id="greetingService" 
class="com.springinaction.chapter01.hello.
<constructor-arg value="Buenos Dias!" />

</bean>

Следующий отрезок кода иллюстрирует, как контейнер инициализирует сервис приветствия,когда используется элемент <constructor-arg>:

GreetingServiceImpl greetingService = new GreetingServiceImpl("Buenos Dias");

Последний кусочек паззла, это класс, коорый загружает контейнер Spring, и использует его для извлечения приветствия. Листинг 1.4 Показывает этот класс.

Листинг 1.4 main класс программы

package com.springinaction.chapter01.hello;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
 
public class HelloApp {
public static void main(String[] args) throws Exception {
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("hello.xml"));
GreetingService greetingService = (GreetingService) factory.getBean("greetingService");
greetingService.sayGreeting();
}
}

Класс BeanFactory, использованный здесь, является контейнером Spring. После загрузки файла hello.xml в контейнер, метод main() обращается к методу getBean() в BeanFactory, чтобы установить ссылку на сервис приветствия.
При помощи этой ссылки, в конце вызывается метод sayGreeting().
Когда вы запустите приложение, оно напечатает (не удивительно)

Buenos Dias!

Это самое простое приложение, которое я мог придумать. Однако, не смотря на его простоту, оно иллюстрирует основы конфигурирования и
использования классов в Spring. К сожалению, это возможно слишком просто, потому что он только иллюстрирует как конфигурировать бин путем ввода строки в свойство. Реальная мощь Spring-а кроется в том, что бины могут быть встроены в другие бины, использую DI.

Понимание dependency injection (DI)

Хотя Spring делает много вещей, DI является сердцем Spring Framework.
Это может казаться немного запугивающим, вызывая в воображении сложной техники программирования
или дизайна паттернов. Но, как оказывается, DI не очень сложна, как кажется.
Фактически, применяя DI в вашем проекте, вы увидите, что ваш код
становится более легче, проще, и более легким в тестировании.
Но что означает "dependency injection" (DI)?

Внедрение зависимостей

На самом деле, dependency injection обычно применяется под другим именем: инверсия управления (inversion of control). Но в статье, написанной в 2004 году, Мартин Флаувер спросил какой аспект управления инвертируется. Он пришел к выводу, что это получение зависимостей, которые инвертируются. Основываясь на этом, он придумал фразу “dependency injection”, термин, лучше описывающий происходящее.

Другие нетривиальные приложения (в значительной степени более сложные, чем HelloWorld.java), составленные из двух или более классов, сотрудничающих друг с другом, реализуя бизнес логику. Традиционно, каждый объект ответственнен за получение его собственной ссылки на объекты, взаимодействующие с ним (их зависимости). Это может привести к чрезвычайно сложному и тяжело тестируемому коду.

Когда работает DI, объекты дают свои зависимости во время создания некоторому внешнему объекту, который координирует каждый объект в системе.
Другими словами, зависимости вводятся в объекты Итак, DI подразумевает инверсию ответственности относительно того, как объект получает
ссылки на зависимые объекты (см. рис. 1.2).

Рисунок 1.2 Внедрение зависимостей вызывает данные объекту зависимости в противоположность объекту, берущего ссылки внутри себя.

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

Например, если класс Foo на рисунке 1.2 знает о своей зависимости от Bar только через интерфейс, тогда для него совсем не важна фактическая реализация объекта Bar. Bar может быть как обычным локальным Java объектом, удаленным веб-сервисом, EJB, или реализацией-заглушкой для юнит тестирования - Foo не знает и не заботится об этом.

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

Dependency injection в действии

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

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

Листинг 1.5 Компонент Рыцаря Круглого Стола

package com.springinaction.chapter01.knight;
public class KnightOfTheRoundTable {
private String name;
private HolyGrailQuest quest;
 
public KnightOfTheRoundTable(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
 
public class KnightOfTheRoundTable implements Knight {
private String name;
private Quest quest;
 
public KnightOfTheRoundTable(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
 
public Object embarkOnQuest() throws QuestFailedException {
return quest.embark();
}
}

В листинге 1.5 рыцарь нарекается в соответствии с параметром своего конструктора. Этот конструктор устанавливает странствие рыцаря, приписывая ему HolyGrailQuest. Реализация странствия HolyGrailQuest довольно проста, как показано в листинге 1.6.

Листинг 1.6 Испытания в компоненте HolyGrail, которые выпадут на долю рыцаря

package com.springinaction.chapter01.knight;
 
public class HolyGrailQuest {
public HolyGrailQuest() {}
 
public HolyGrail embark() throws GrailNotFoundException {
HolyGrail grail = null;
// Искать Грааль
...
return grail;
}
}

Довольные своей работой, Вы гордо публикуете код в системе контроля версий. Вы хотите показать свое творение маркетинговому отделу, но глубоко внутри Вас что-то терзает. Вы уже почти списываете это на пельмени с кетчупом, которыми позавтракали, когда осознаете
в чем дело: Вы не написали юнит тесты.

Рыцарское тестирование

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

Листинг 1.7 Тестирование рыцаря

package com.springinaction.chapter01.knight;
import junit.framework.TestCase;
 
public class KnightOfTheRoundTableTest extends TestCase {
public void testEmbarkOnQuest() throws GrailNotFoundException {
KnightOfTheRoundTable knight = new KnightOfTheRoundTable("Bedivere");
HolyGrail grail = knight.embarkOnQuest();
assertNotNull(grail);
assertTrue(grail.isHoly());
}
}

После написания этого тесткейса, вы приступаете к написанию тесткейса для HolyGrailQuest. Но даже не начав, вы понимаете, что тесткейс для KnightOfTheRoundTableTest косвенно тестирует HolyGrailQuest. Также Вы беспокоитесь, что все тестирование охватывает все случаи.
Что случится если метод embark() класса HolyGrailQuest вернет null? Или что, если он возбудит исключение GrailNotFoundException?

Кто кого вызывает?

Рисунок 1.3 Рыцарь ответственен за получение своего странствия, через создание экземпляра или другим образом.

Основная проблема, связанная с KnightOfTheRoundTable, заключается в том, как он получает HolyGrailQuest. Создается ли новый экземпляр HolyGrail или через JNDI получается уже существующий, но каждый рыцарь ответственен за приобретение своего странствия (как показано на рисунке 1.3). Таким образом, у Вас нет способа протестировать класс рыцаря в отдельности. В данных условиях каждый раз, когда Вы тестируете KnightOfTheRoundTable, Вы будете также косвенно тестировать HolyGrailQuest.

И более того, Вы не можете сказать HolyGrailQuest, чтобы он вел себя по-разному (т.е., возвращал null или возбуждал исключение GrailNotFoundException) для разных тестов. Что могло бы помочь, так это, если бы Вы могли бы создать заглушку для HolyGrailQuest, которая позволяла бы решать Вам, как она должна себя вести. Но даже если вы создадите такую заглушку, KnightOfTheRoundTable все равно получает свой собственный HolyGrailQuest, что означает, что Вам придется вносить изменения в KnightOfTheRoundTable, чтобы получить заглушку для странствия, используемую в целях тестирования (и затем заменить ее обратно на стадии выпуска).

Уменьшение связанности с помощью интерфейсов

Одним словом проблема заключается в связанности. С этой точки зрения, KnightOfTheRoundTable статично связан с HolyGrailQuest. Они так скованы вместе, что Вы не можете получить KnightOfTheRoundTable без HolyGrailQuest.

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

Обычным методом, используемым для уменьшения связности, является скрытие деталей реализации за интерфейсами таким образом, чтобы класс с фактической реализацией мог быть заменен без влияния на класс клиента. К примеру предположим, что Вы создали
интерфейс Quest:

package com.springinaction.chapter01.knight;
public interface Quest {
abstract Object embark() throws QuestFailedException;
}

Затем Вы изменяете HolyGrailQuest, чтобы реализовать этот интерфейс. Также примите к сведению, что embark() теперь возвращает тип Object и возбуждает исключение QuestFailedException.

package com.springinaction.chapter01.knight;
 
public class HolyGrailQuest implements Quest {
public HolyGrailQuest() {}
public Object embark() throws QuestFailedException {
// Сделать все нужное, чтобы отправиться в странствие
return new HolyGrail();
}
}

К тому же следующий метод должен быть изменен в KnightOfTheRoundTable, чтобы он был совместим с этими типами Quest:

 private Quest quest;
...
public Object embarkOnQuest() throws QuestFailedException {
return quest.embark();
}

Также Вы можете сделать KnightOfTheRoundTable реализующим следующий интерфейс Knight:

 public interface Knight {
Object embarkOnQuest() throws QuestFailedException;
}

Скрытие реализации Вашего класса за интерфейсами определенно является шагом в нужном направлении. Но то, в чем многие разработчики терпят неудачу, это способ, которым они получают экзепляр Quest. Например, рассмотрим возможные изменения в KnightOfTheRoundTable:

Начало потерялось!!!
public HolyGrail embarkOnQuest() throws GrailNotFoundException {
return quest.embark();
}
}

В данном случае класс KnightOfTheRoundTable отправляется в странствие через интерфейс Quest. Но рыцарь все еще получает определенный тип странствия Quest (а именно HolyGrailQuest). Это немногим лучше прежней ситуации. KnightOfTheRoundTable может отправляться лишь в странствия за Святым Граалем и нет никаких других типов странствий.

Выдача и получение

Вопрос, который возникает на этом этапе, заключается в том, должен ли рыцарь быть ответственным за получение странствия, или рыцарю выдается странствие, в которое он должен отправится?
Рассмотрим следующие изменения в KnightOfTheRoundTable:

public class KnightOfTheRoundTable implements Knight {
private String name;
private Quest quest;
 
public KnightOfTheRoundTable(String name) {
this.name = name;
}
 
public Object embarkOnQuest() throws QuestFailedException {
return quest.embark();
}
 
public void setQuest(Quest quest) {
this.quest = quest;
}
}
Рисунок 1.4 Рыцарь более не ответственнен за получение своего странствия. Вместо этого ему выдается (внедряется в него) странствие с помощью его метода setQuest().

Заметили разницу? Сравните рисунок 1.4 с рисунком 1.3, чтобы найти отличия в том, как рыцарь получает странствие. Теперь рыцарю выдается странствие вместо того, чтобы он производил его сам. KnightOfTheRoundTable больше не ответственен за производство своих странствий.
И от того, что он знает о странствии только через интерфейс Quest, Вы можете поручить рыцарю любую реализацию Quest, какую только пожелаете.

В одном случае, Вы можете выдать ему HolyGrailQuest (СтранствиеЗаСвятымГраалем). В другом, иная реализация Quest, как например RescueDamselQuest (СтранствиеЗаПрекраснойДамой), может быть поручена рыцарю. Подобным образом, в тесткейсе Вы можете задавать реализацию-заглушку Quest.

Вкратце, в этом и заключается DI: ответственность за координацию взаимодействия между зависимыми объектами выносится из самих объектов.

Присвоение странствия рыцарю

Теперь, когда Вы написали Ваш класс KnightOfTheRoundTable, которому может быть дан произвольный объект Quest,
как Вы можете конкретизировать, какой Quest должен быть передан?

Процесс создания связей между компонентами приложения называется связыванием. В Spring существует множество способов связать компоненты вместе, но наиболее общим подходом является использование XML. В листинге 1.8 показано содержимое простого конфигурационного файла Spring, knight.xml, который передает странствие (а именно, HolyGrailQuest) рыцарю KnightOfTheRoundTable.

Листинг 1.8 Встраивание странствия в рыцаря в конфигурационном XML файле Spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

 
<bean id="quest"
class="com.springinaction.chapter01.knight.HolyGrailQuest" />
 
<bean id="knight"
class="com.springinaction.chapter01.knight.KnightOfTheRoundTable">
<constructor-arg value="Bedivere" />
<property name="quest" ref="quest" />
</bean>
</beans>

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

Теперь, когда мы объявили отношения между рыцарем и странствием, нам нужно загрузить XML file и стартануть приложение.

Увидим как это работает

В приложении, построенном с использованием Spring, BeanFactory загружает определения компонентов и связывает компоненты вместе. Из-за того, что компоненты в примере для рыцаря объявлены в файле XML, подходящей фабрикой для этого примера является XmlBeanFactory. Метод main() в листинге 1.9 использует XmlBeanFactory, чтобы загрузить knight.xml и получить ссылку на объект Knight.

Листинг 1.9 Запуск примера с рыцарем

package com.springinaction.chapter01.knight;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
 
public class KnightApp {
public static void main(String[] args) throws Exception {
BeanFactory factory =
new XmlBeanFactory(new FileSystemResource("knight.xml"));
 
Knight knight = (Knight) factory.getBean("knight");
 
knight.embarkOnQuest();
}
}

Как только у приложения появляется ссылка на объект Knight, оно просто вызывает метод embarkOnQuest(), чтобы начать приключение рыцаря. Необходимо иметь в виду, что этот класс ничего не знает о странствии, в которое предстоит отправится рыцарю. Опять таки, единственная сущность, которой известен тип поручаемого рыцарю странствия, это файл knight.xml.

Хоть это и весело - посылать рыцарей в странствия с использованием внедрения зависимостей, но давайте все-таки рассмотрим использование DI в жизненных enterprise приложениях.

Внедрение зависимостей в enterprise приложениях

Рисунок 1.5 Традиционные подходы к получению сервиса могли бы привести к тесному связыванию между объектом подсчета суммы и сервисом заказа.

Допустим Вам поручили написание online-магазина. Частью веб-приложения является компонент обслуживания заказов, который обрабатывает все функции, связанные с размещением заказов. На рисунке 1.5 показано несколько способов, которым компонент Checkout веб-слоя (возможно, представленнный действием WebWork или страницей Tapestry) может производить доступ к сервису заказов.


(1) Простой, но безыскусный, подход мог бы состоять в том, чтобы создавать экземпляр сервиса заказа тогда, когда это необходимо. Вместо прямого связывания веб-слоя с определенным классом сервиса такой подход в итоге приведет к расточительному созданию экземпляров класса OrderServiceImpl, когда достаточно разделяемого, не сохраняющего своего состояния синглтона.
(2) Если сервис для заказов реализован в виде EJB версии 2.x, то для доступа к сервису Вам сначало надо будет получить базовый интерфейс посредством JNDI , который затем будет использован для доступа к реализации интерфеса сервиса для EJB. В таком случае, веб-слой больше привязан не к определенному интерфейсу, а к JNDI и модели програмирования EJB 2.x.
(3) В форме компонента EJB 3 сервис для заказов может быть получен напрямую из JNDI (без доступа через базовый интерфейс). Опять таки, в этом случае не будет привязки к определенному классу реализации, но будет зависимость от JNDI.
(4) При использовании EJB или без, Вам возможно захочется спрятать детали получения через JNDI за локатором сервиса. Это устранит видимые проблемы сцепки с другими подходами, но теперь веб-слой связан с Service Locator.


Рисунок 1.6 Внедряя OrderService в компонент Checkout, Checkout освобождается от знания, как сервис реализован и где он находится.

Ключевой вопрос со всеми из этих подходов является то, что компонент веб-слой слишком активно участвует в получении собственной зависимости. Он слишком много знает о том, откуда происходит заказ сервиса и как это осуществляется.

Если слишком сильное знание о вашей зависимости приводит к тесно связанному коду, то само собой разумеется, что зная как можно меньше о зависимости приведет к слабо связанному коду. Рассмотрим рисунок 1.6, который показывает, как компонент Checkout может быть предоставлен OrderService вместо того, чтобы запрашивать его. Теперь давайте посмотрим, каким образом это будет реализовано с помощью DI:

 private OrderService orderService;
 
public void doRequest(HttpServletRequest request) {
Order order = createOrder(request);
orderService.createOrder(order);
}
 
public void setOrderService(OrderService orderService) {
this.orderService = orderService;
}

Не ищите код! Ссылка на OrderService (который является интерфейсом) предоставляется классом с помощью метода setOrderService(). Веб компонент
не знает или не заботится о том, откуда берется OrderService. Он может быть внедрен Spring-ом или он может быть внедрен вручную явным вызовом setOrderService(). Он также не имеет понятия о том, как реализуется OrderService — он знает только о нем через интерфейс OrderService. С DI ваши объекты приложения освобождаются от бремени извлечение их собственных зависимостей и способны сосредоточить внимание на своих задач, полагая, что их зависимости будут доступны, когда это необходимо.

Внедрение зависимостей является благом для слабосвязанных кода, сделав возможным держать ваши объекты приложения на расстоянии друг от друга.Однако мы лишь слегка дотронулись до контейнера Spring и DI. В главах 2 и 3 вы увидите больше способов связывания объектов в контейнере Spring.

Внедрение зависимости это только одна техника, которую Spring предлагает обьектам POJO в поддержку слабого связывания. Аспект-риентированное программирование обеспечивает различные виды разделения функциональности приложения (такие как безопасность и транзакции).......Давайте взглянем на то, как Spring поддерживает AOP.

Применение аспект-ориентированного программирования

Хотя DI делает возможным связать программные компоненты друг с другом свободно, аспекториентированное
программирование позволяет вам привлекать функциональные возможности многократно используемых компонентов повсюду в вашем приложениии

Введение в АОП.

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

Рисунок 1.7 Вызов общесистемных проблем (задач) таких как регистрация и безопасность часто разбросаны по модулям, где эти проблемы (задачи) не являются основными.

Однако часто эти компоненты также несут дополнительную ответственность сверх своей коренной функциональности. Системные сервисы, такие как логгирование, транзакции, управление, и безопасность, часто находят свой путь в компонентах, основная ответственность которых - что-нибудь еще. Такие системные сервисы обычно упоминают как cross-cutting проблемами потому что они имеют тенденцию идти вразрез с различными компонентами системы

С распространением этих проблем в разных компонентах, вы вводите два уровня сложности в ваш код:


  • Код который реализует общесистемные проблемы дублируется в различных компонентах. Это означает, что если вы нуждаетесь в замене того, как проблема обрабатывается, вы будете вынуждены просматривать различные компоненты. Даже если вы абстрагируете проблему в отдельный модуль таким образом, чтобы воздействовать на ваши компоненты простым вызовом метода, этот простой вызов метода будет продублирован в разных местах.
  • Ваши компоненты будут замусорены кодом, который не сочетается с их основной функциональностью. Метод добавления записи в адресную книгу должен быть озабочен тем, как добавить адрес а не тем, безопасно ли это или это использует транзакцию.


Рисунок 1.8 Использование АОП, Общесистемные задачи охватывают компоненты, которые воздействуют на них. Это позволяет компонентам приложения фокусироваться на их специфичных бизнес функциях.

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


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

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

Чтобы продемонстрировать как аспекты могут применяться в Spring, вернемся к примеру с рыцарем, добавим базовый аспект регистрации.

АОП в действии.

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

Хммм...менестрель который поет о рыцаре? Это не должно звучать слишком тяжело. Начнем, создайте класс Minstrel, как показано в листинге 1.10

Листинг 1.10 Класс Minstrel, включающий музыкальное логгирование.

package com.springinaction.chapter01.knight;
import org.apache.log4j.Logger;
 
public class Minstrel {
private static final Logger SONG = Logger.getLogger(Minstrel.class);
 
public void singBefore(Knight knight) {
SONG.info("Fa la la; Sir " + knight.getName() + " is so brave!");
}
 
public void singAfter(Knight knight) {
SONG.info("Tee-hee-he; Sir " + knight.getName() + " did embark on a quest!");
}
}

Придерживаясь линии мышления Внедрения Зависимости, вы меняете KnightOfTheRoundTable предоставляющего экземпляр Minstrel:

Рисунок 1.9 Без АОП рыцарь должен дать указание менестрелю спеть песню. Это мешает первичной деятельности рыцаря убить дракона и спасти девицу.
 public class KnightOfTheRoundTable implements Knight {
...
private Minstrel minstrel;
 
public void setMinstrel(Minstrel minstrel) {
this.minstrel = minstrel;
}
...
public HolyGrail embarkOnQuest()
throws QuestFailedException {
minstrel.singBefore(this);
HolyGrail grail = quest.embark();
minstrel.singAfter(this);
return grail;
}
}
Рисунок 1.10 Аспектно-ориентированный менестрель охватывает рыцаря, ведет хронику его деятельности без того чтобы рыцарь знал о менестреле.

Это должно сработать! Ох подождите...тут одна маленькая проблема. Как это, каждый рыцарь должен остановиться и сказать менестрелю спеть песню прежде чем рыцарь сможет продолжить свое путешествие (как на рисунке 1.9). Затем после путешествия, рыцарь должен не забыть напомнить менестрелю продолжить пение о своих делах. Необходимость помнить остановиться и сказать менестрелю что надо сделать, определенно препятсвует
выполнению квеста рыцарем.

В идеале, менестрель может взять больше инициативы и автоматически петь песни без явного указания делать это. Рыцарь не должен знать (или действительно даже заботится) что его дела будут записаны в песню. В конце-концов, у вас не может быть рыцаря опоздавшего на странствие из-за ленивого менестреля.

Короче говоря контроль за услугами менестреля выходят за рамки обязанностей рыцаря. Другой способ сказать об этом что услуги менестреля (записывать песни) ортогональны обязанностям рыцаря (выполнять задание). Поэтому есть смысл "завернуть" менестреля в аспект который добавит его услугу по записи песен рыцарю. Тогда услуги менестреля будут охватывать всю деятельность рыцаря, рыцарь даже не будет знать, что менестрель здесь, как
показано на рисунке 1.10

Как выясняется, это довольно легко превратить класса Minstrel в листинге 1.10 в аспект, используя поддержку Spring-ом АОП. Давайте посмотрим, каким образом.

Встраивание аспектов.

Есть несколько путей реализации аспектов в Spring, и мы будем ковыряться в каждом из них в главе 4. Но ради примера мы будем использовать новое пространство имен АОП введенное в Spring 2.0. Начиная работу вы должны убедиться, что вы объявили пространство имен в контексте определения XML:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
''https://www.springframework.org/schema/aop ''
''https://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
''
...
</beans>

С объявлением пространства имен, мы готовы создать аспект. Часть XML в листинге 1.11 объявляет minstrel как bean в контексте Spring и затем создает аспект который уведомляет knight бин.

Листинг 1.11 Встраивание MinstrelAdvice в knight

<bean id="minstrel"
class="com.springinaction.chapter01.knight.Minstrel" />
 
<aop:config>
<aop:aspect ref="minstrel">
 
<aop:pointcut
id="questPointcut"
expression="execution(* *.embarkOnQuest(..) and target(bean)" />
 
<aop:before
method="singBefore"
pointcut-ref="questPointcut"
arg-names="bean" />
 
<aop:after-returning
method="singAfter"
pointcut-ref="questPointcut"
arg-names="bean" />
</aop:aspect>
</aop:config>

Там много чего есть в листинге 1.11, так что давайте разберем его по частям:


  • Первое что мы найдем это объявление <bean>, создание бина minstrel в Spring. Это класс Minstrel из листинга 1.10. Minstrel не имеет никаких зависимостей, так что нет необходимости внедрять их куда либо.
  • Следующий элемент <aop:config>. Этот елемент указывает, что мы собираемся делать какие-то вещи АОП. Большинство конфигурационных элементов АОП Spring должны содержаться в <aop:config>. Внутри <aop:config> есть элемент <aop:aspect>. Этот элемент показывает что мы объявляем аспект. Функциональные возможности аспекта определены в бине, на который указывает атрибут ref. Таким образом, бин minstrel, который Minstrel (имеется ввиду класс), будет обеспечивать функциональные возможности аспекта
  • Аспект состоит из pointcuts - точек внедрения (места, где будут применяться функциональные возможности аспекта) и описание (как применяются функциональные возможности). Элемент <aop:pointcut> определяет pointcut которые вызываются при выполнении метода embarkOnQuest(). (Если вы знакомы с AspectJ, вы можете узнать pointcut как выражения AspectJ синтаксиса.)
  • Наконец, есть две части описания AOP. Элемент <aop:before> объявляет что метод singBefore() класса Minstrel должен быть вызван перед pointcut, а элемент <aop:after> объявляет, что метод singAfter()должен быть вызван после pointcut. Pointcut в обоих случаях это ссылка на questPointcut (id="questPointcut"), который является выполнением embarkOnQuest().

Вот и все, что нужно сделать! Мы только что перевели Minstrel в аспект Spring. Не беспокойтесь, если здесь вы чего-то не поняли - вы увидите намного больше примеров по Spring АОП в главе 4 что должно помочь вам понять это. Сейчас есть два важных момента для извлечения из этого примера.

Во-первых, Minstrel по-прежнему POJO — нет ничего о Minstrel, указывающее на то, что он должен использоваться как аспект. Вместо этого Minstrel был превращен в аспект декларативно в контексте Spring.

Во вторых, и более важно, knight больше не нужно напоминать minstrel петь о его подвигах. Как аспект minstrel позаботиться об этом автоматически. На самом деле рыцарю даже не нужно знать о существовании менестреля. Следовательно классу KnightOfTheRoundTable можно вернуться к более простой форме как прежде:

public class KnightOfTheRoundTable implements Knight {
private String name;
private Quest quest;
 
public KnightOfTheRoundTable(String name) {
this.name = name;
}
public HolyGrail embarkOnQuest() throws QuestFailedException {
return quest.embark()
}
public void setQuest(Quest quest) {
this.quest = quest;
}
}

Используя АОП вести хронику рыцарской деятельности стало веселее. Но АОП Spring-а может использоваться для еще более практичных вещей, чем сочинение нестареющих сонетов о рыцарях. Как вы увидите позже, Spring использует АОП для обеспечения сервисов предприятия
таких как описание транзакций (глава 6) и безопасность (глава 7).

Резюме

Теперь вы должны иметь достаточно хорошее представление о том, что Spring приносит на стол. Spring
стремится сделать разработку enterprise Java проще и поддерживает слабосвязанный код.
Важным для этого является внедрение зависимостей и АОП.
В этой главе мы сделаем маленький тест внедрения зависимости в Spring.
DI это метод связывания объектов приложения так, что объекты не должны знать
откуда их зависимости появляются или как они реализуются. Вместо того, чтобы
получать зависимости от них самих, зависимые объекты получают объекты
которые от них зависят. Так как зависимые объекты знают о своих
внедренных объектах часто только через интерфейсы, связь сохраняется очень слабо.
В дополнение внедрения зависимости мы также видим блестящую поддержку АОП Spring-ом.
АОП позволяет вам сконцентрировать логику, которая обычно бывает разбросана
по всему приложению, в одном месте - аспекте. Когда Spring связывает ваши
бины друг с другом, такие аспекты могут быть вплетены во время выполнения, фактически предоставляя
бинам новое поведение.
Внедрение зависимости и АОП это центр всего в Spring. Таким образом вы
должны понимать, как использовать эти основные функции Spring чтобы иметь возможность использовать
оставшуюся часть платформы. В этой главе мы только прикоснулись к
особенностям DI и AOP Spring-а. В следующих нескольких главах мы углубимся в DI и АОП.
Без лишних слов, давайте перейдем к главе 2, чтобы узнать, как связываются
вместе объекты в Spring используя внедрение зависимости.

]]>
Книги по Java https://linexp.ru?id=4732 Wed, 29 Jun 2022 14:06:57 GMT
<![CDATA[Предисловие Thinking in Java 4th edition]]> ПРЕДИСЛОВИЕПоначалу я рассматривал Java как «очередной язык программирования»... которым он и является во многих отношениях.


Поначалу я рассматривал Java как «очередной язык программирования»... которым он и является во многих отношениях.

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

Все программирование в той или иной степени связано с управлением сложными задачами: сложность решаемой проблемы накладывается на сложность системы, в которой она решается. Именно из-за этих трудностей большинство программных проектов завершаются неудачей. И до сих пор ни один из языков, которые я знаю, не был смоделирован и создан в расчете на преодоление сложности разработки и поддержки программ. Конечно, многие решения при создании языков принимались в расчете на разрешение сложностей, но при этом всегда находилось еще что-то, считаемое достаточно важным, чтобы учитывать это при планировании языка. Все это неизбежно заставляло программистов «биться головой о стену» при столкновении с проблемами. Например, С++ создавался в расчете на эффективность и совместимость с С (чтобы легко переходить с этого языка на С++). Оба решения, несомненно, полезны и стали одними из причин успеха С++, но также привели к дополнительным трудностям, что не позволило успешно воплотить в жизнь некоторые проекты (конечно, можно винить программистов и руководителей проектов, но, если язык в силах помочь в устранении ошибок, почему этим не воспользоваться?). Или другой пример: Visual Basic (VB) изначально был привязан к BASIC, в который не была заложена возможность расширения, из-за чего все дополнения, созданные для VB, имеют ужасный и сложно поддерживаемый синтаксис. С другой стороны, С++, VB, Perl и другие языки, подобные Smalltalk, частично фокусировались на преодолении трудностей и, как результат, стали успешными в решении определенных типов задач.

Больше всего удивило меня при ознакомлении с Java то, что его создатели из Sun, похоже, наряду с другими целями хотели по возможности облегчить работу программиста. Они словно говорили: «Мы хотим, чтобы вы могли быстрее и проще написать надежный код». Раньше такое намерение приводило к тому, что быстродействие программ оставляло желать лучшего (хотя со временем ситуация улучшилась). И все же такой подход изумительно повлиял на сроки разработки программ; для разработки эквивалентной программы на С++ требуется вдвое или еще больше человеко-часов. Уже одно это приводит к экономии колоссальных денег и уймы времени, но Java не «застывает» в упоении достигнутым. Творцы языка идут дальше, встраивая поддержку технологии, ставших важными в последнее время (многозадачность, сетевое программирование), в сам язык или его библиотеки, что значительно упрощает решение этих задач. Наконец, Java энергично берется за действительно сложные проблемы: платформно-независимые программы, динамическое изменение кода и даже безопасность. Каждая из этих проблем способна задержать сроки сдачи вашего проекта, а может легко стать непреодолимым препятствием. Таким образом, несмотря на прошлые проблемы с производительностью, перспективы Java потрясают: он значительно повышает продуктивность нашей работы.

Во всех случаях — при создании программ, командной разработке проектов, конструировании пользовательских интерфейсов для общения программы с потребителем, запуске программ на разных типах компьютеров, простом написании программ, использующих Интернет, — Java расширяет «полосу пропускания» информации при коммуникациях между людьми. Я полагаю, что перегонка туда-сюда большого объема битов не есть главный результат информационной революции; нас ожидает истинный переворот, когда мы сможем с легкостью общаться друг с другом: один на один, в группах и, наконец, всепланетно. Я слышал предположение, что следующей революцией будет появление единого разума, образованного из критической массы людей и взаимосвязей между ними. Java может быть катализатором этой революции, а может и не быть, но, по крайней мере, вероятность такого влияния заставляет меня чувствовать, что я делаю что-то значимое, пытаясь обучать этому языку.


Java SE5 и SE6

В этом издании книги были учтены изменения, внесенные в Java в версии, которую фирма Sun изначально назвала JDK 1.5, затем переименовала в JDK5 или J2SE5 и наконец в Java SE5. Многие изменения Java SE5 создавались для удобства программиста. Как вы увидите, создатели Java не полностью преуспели на этом пути, но в общем сделали большие шаги в правильном направлении.

В этом издании я постарался полностью интегрировать усовершенствования Java SE5/6, включить и использовать их во всей книге. Таким образом, это издание «предназначено только для Java SE5/6», и большинство примеров не будет компилироваться в других версиях. Шаг довольно рискованный, и все же я полагаю, что преимущества того стоили.

Благодарности

Прежде всего выражаю благодарность моим друзьям и помощникам, которые работали со мной на семинарах и помогали создавать учебные проекты: это Дэйв Бартлетт (Dave Bartlett), Билл Веннерс (Bill Venners), Чак Аллисон (Chuck Allison), Джереми Майер (Jeremy Meyer) и Джейми Кинг (Jamie King). Я благодарен им за терпение, с которым они относятся к моим попыткам построить наилучшую модель совместного существования нашего коллектива.

За последнее время (несомненно, из-за широкого распространения Интернета) ко мне обращалось множество людей, которые мне помогали, — особенно те, кто работает на дому. В прошлом мне пришлось бы нанимать огромный офис, чтобы вместить всех этих людей, но благодаря Сети, курьерской почте и телефону я мог пользоваться их содействием без дополнительных затрат. Пола Стойер (Paula Steuer) оказала неоценимую помощь: она взяла под свой контроль мой сомнительный трудовой график и привела его к нормальному виду (спасибо за то, что подталкивала меня, когда я чего-то не хотел делать, Пола). Джонатан Уилкокс (Jonathan Wilcox) просеял всю мою корпоративную структуру и отыскал все скрытые ловушки, которые могли помешать нормальному ходу дел (с юридической точки зрения). Спасибо за внимание и настойчивость. Шарлин Кобо (Sharlynn Cobaugh), эксперт по обработке звука, играла важную роль при создании мультимедийных семинаров, а также решении других проблем. Спасибо за скрупулезность, с которой она подходила к решению совершенно необъяснимых компьютерных проблем. Группа Amaio из Праги помогла мне в работе над некоторыми проектами. Дэниел Уилл-Харрис (Daniel Will-Harris) предложил изначальную идею «работы по Интернету»; и конечно, именно ему принадлежит решающее слово во всех решениях из области графического дизайна.

За прошедшие годы Джеральд Уайнберг (Gerald Weinberg) стал моим «неофициальным» учителем, и за это я благодарен ему.

Эрвин Варга (Ervin Varga) очень помог мне с технической правкой 4-го издания — хотя разные люди помогали мне с другими главами и примерами, Эрвин был главным техническим рецензентом книги. Он нашел многие ошибки и внес дополнения, значительно улучшившие мой текст. Его педантичность и внимание к деталям просто потрясают; бесспорно, это лучший технический редактор, с которым мне доводилось работать.

Мой блог на сайте Билла Веннерса www.Artima.com помог мне организовать обратную связь с читателями. Спасибо всем, кто помог мне прояснить некоторые концепции, — Джеймс Уотсон (James Watson), Говард Ловатт (Howard Lovatt), Майкл Баркер (Michael Barker) и многие другие... особенно те, кто помогал мне с проработкой темы параметризации.

Большое спасибо Марку Уэлшу (Mark Welsh) за его постоянную помощь.

Эван Кофски (Evan Cofsky), знаток всех тонкостей установки и сопровождения веб-серверов на базе Linux, помогает мне организовать нормальную работу сервера MindView.

Кафетерий Camp4 Coffee в Крестед-Бьют, штат Колорадо, стал стандартным местом проведения досуга посетителей семинаров MindView, а во время перерывов они обеспечивают отличное выездное обслуживание. Спасибо моему другу Элу Смиту (А1 Smith) за то, что создал это заведение и сделал его таким отличным местом. Я также благодарен всем баристам Сатр4, таким приветливым и дружелюбным.

Я благодарен всем сотрудникам Prentice Hall, которые снабжали меня всем необходимым и мирились с моими особыми запросами.

Некоторые программы оказались воистину бесценными в ходе работы над примерами, и я очень благодарен их создателям. Cygwin (www.cygwin.com) решает бесчисленные проблемы, которые Windows решить не может (не хочет), и я с каждым днем привязываюсь к этому пакету все сильнее. IBM Eclipse (www. eclipse.org) — совершенно замечательное творение для сообщества разработчиков. JetBrains IntelliJ Idea продолжает прокладывать новые творческие пути в области инструментариев разработчика.

Я начал использовать Enterprise Architect от Sparxsystems во время работы над этой книгой, и программа быстро стала моим основным UML-инструментом. Форматер кода Jalopy, созданный Марко Ханзикером (Marco Hunsicker) (www.triemax.com), часто оказывался очень полезным, а Марко помог настроить его под мои специфические потребности. Я также обнаружил, что J Edit с плагинами Славы Пестова (Slava Pestov) (www.jedit.org) оказывается весьма полезным в некоторых случаях; это вполне достойный редактор для начинающих Java-программистов.

И конечно, я постоянно использую в своей повседневной работе Python (www.Python.org), творение моего друга Гидо Ван Россума (Guido Van Rossum) и группы безумных гениев, с которыми я провел множество замечательных дней. Спасибо всему сообществу Python, объединившему таких выдающихся людей!

Хочу отдельно поблагодарить всех своих учителей и учеников (которые на самом деле тоже являются учителями).

Кошка Молли часто сидела у меня на коленях, пока я работал над книгой. Так она вносила свой теплый, пушистый вклад в мою работу.

Напоследок перечислю лишь некоторых из моих друзей и помощников: Пэтти Гаст (Patty Gast) — ас в области массажа, Эндрю Бинсток (Andrew Binstock), Стив Синоски (Steve Sinoski), Джей Ди Хильдебрандт (JD Hildebrandt), Том Кеффер (Tom Keffer), Брайан Макэлхинни (Brian McElhinney), Бринкли Барр (Brinkley Barr), Билл Гейтс из «Midnight Engineering Magazine», Ларри Константин (Larry Constantine) и Люси Локвуд (Lucy Lockwood), Джин Ванг (Gene Wang), Дэйв Мейер (Dave Mayer), Дэвид Интерсимон (David Intersimone), Крис и Лора Стрэнд (Chris and Laura Strand), Элмквисты (Almquists), Брэд Джербик (Brad Jerbic), Мэрилин Цвитанич (Marylin Cvitanic), Марк Мабри (Mark Mabry), семья Роббинс (Robbins), семьи Мелтер (Moelter) и Макмиллан (McMillan), Майкл Вилк (Michael Wilk), Дэйв Стонер (Dave Stoner), Крэнстоны (Cranstons), Ларри Фогг (Larry Fogg), Майк Секейра (Mike Sequeira), Гэри Энтсмингер (Gary Entsminger), Кевин и Сонда Донован (Kevin and Sonda Donovan), Джо Лорди (Joe Lordi), Дэйв и Бренда Бартлетт (Dave and Brenda Bartlett), Блейк, Эннет и Джейд (Blacke, Annette & Jade), Рентшлеры (Rentschlers), Судеки (Sudeks), Дик (Dick), Патти (Patty) и Ли Экель (Lee Eckel), Линн и Тодд (Lynn and Todd) и их семьи. Ну и, конечно, мама с папой.

]]>
Книги по Java https://linexp.ru?id=4731 Wed, 29 Jun 2022 14:06:15 GMT
<![CDATA[Глава 4 Spring in Action 2th edition]]>

Advising beans

This chapter covers


  • Basics of aspect-oriented programming
  • Creating aspects from POJOs
  • Automatically proxying beans
  • Using @AspectJ annotations
  • Injecting dependencies into AspectJ aspects

As I’m writing this chapter, Texas (where I reside) is going through several days of record-high temperatures. It’s really hot. In weather like this, air-conditioning is a must. But the downside of air-conditioning is that it uses electricity and electricity costs money. And there’s very little we can do to avoid paying for a cool and comfortable home. That’s because every home has a meter that measures every single kilowatt, and once a month someone comes by to read that meter so that the electric company accurately knows how much to bill us.

Now imagine what would happen if the meter went away and nobody came by to measure our electricity usage. Suppose that it were up to each homeowner to contact the electric company and report their electricity usage. Although it’s possible that some obsessive homeowners would keep careful record of their lights, televisions, and air-conditioning, most wouldn’t bother. Most would estimate their usage and others wouldn’t bother reporting it at all. It’s too much trouble to monitor electrical usage and the temptation to not pay is too great.

Electricity on the honor system might be great for consumers, but it would be less than ideal for the electric companies. That’s why we all have electric meters on our homes and why a meter-reader drops by once per month to report the consumption to the electric company.

Some functions of software systems are like the electric meters on our homes. The functions need to be applied at multiple points within the application, but it’s undesirable to explicitly call them at every point.

Monitoring electricity consumption is an important function, but it isn’t foremost in most homeowners’ minds. Mowing the lawn, vacuuming the carpet, and cleaning the bathroom are the kinds of things that homeowners are actively involved in. Monitoring the amount of electricity used by their house is a passive event from the homeowner’s point of view.

In software, several activities are common to most applications. Logging, security, and transaction management are important things to do, but should they be activities that your application objects are actively participating in? Or would it be better for your application objects to focus on the business domain problems they’re designed for and leave certain aspects to be handled by someone else?

In software development, functions that span multiple points of an application are called cross-cutting concerns. Typically, these cross-cutting concerns are conceptually separate from (but often embedded directly within) the application’s business logic. Separating these cross-cutting concerns from the business logic is where aspect-oriented programming (AOP) goes to work.

In chapter 2, you learned how to use dependency injection (DI) to manage and configure your application objects. Whereas DI helps you decouple your application objects from each other, AOP helps you decouple cross-cutting concerns from the objects that they affect.

Logging is a common example of the application of aspects. But it isn’t the only thing aspects are good for. Throughout this book, you’ll see several practical applications of aspects, including declarative transactions, security, and caching.

This chapter explores Spring’s support for aspects, including the exciting new AOP features added in Spring 2.0. In addition, you’ll see how AspectJ—another popular AOP implementation—can complement Spring’s AOP framework. But first, before we get too carried away with transactions, security, and caching, let’s see how aspects are implemented in Spring, starting with a primer on a few of AOP’s fundamentals.

Introducing AOP

As stated earlier, aspects help to modularize cross-cutting concerns. In short, a cross-cutting concern can be described as any functionality that affects multiple points of an application. Security, for example, is a cross-cutting concern in that many methods in an application can have security rules applied to them. Figure 4.1 gives a visual depiction of cross-cutting concerns.

Figure 4.1 represents a typical application that is broken down into modules. Each module’s main concern is to provide services for its particular domain. However, each of these modules also requires similar ancillary functionalities, such as security and transaction management.

A common object-oriented technique for reusing common functionality is to apply inheritance or delegation. But inheritance can lead to a brittle object hierarchy if the same base class is used throughout an application, and delegation can be cumbersome because complicated calls to the delegate object may be required.

Файл:Fig41.png
Figure 4.1 Aspects modularize crosscutting concerns, applying logic that spans multiple application objects.

Aspects offer an alternative to inheritance and delegation that can be cleaner in many circumstances. With AOP, you still define the common functionality in one place, but you can declaratively define how and where this functionality is applied without having to modify the class to which you are applying the new feature. Cross-cutting concerns can now be modularized into special objects called aspects. This has two benefits. First, the logic for each concern is now in one place, as opposed to being scattered all over the code base. Second, our service modules are now cleaner since they only contain code for their primary concern (or core functionality) and secondary concerns have been moved to aspects.

Defining AOP terminology

Like most technologies, AOP has formed its own jargon. Aspects are often described in terms of advice, pointcuts, and joinpoints. Figure 4.2 illustrates how these concepts are tied together.

Unfortunately, many of the terms used to describe AOP features are not intuitive. Nevertheless, they are now part of the AOP idiom, and in order to understand AOP, you must know these terms. In other words, before you walk the walk, you have to learn to talk the talk.

Advice

When a meter-reader shows up at your house, their purpose is to report the number of kilowatt-hours back to the electric company. Sure, they have a list of houses that they must visit and the information that they report is important. But the actual act of recording electricity usage is the meter-reader’s main job.

Likewise, aspects have a purpose—a job that they are meant to do. In AOP terms, the job of an aspect is called advice.

Advice defines both the what and the when of an aspect. In addition to describing the job that an aspect will perform, advice addresses the question of when to perform the job. Should it be applied before a method is invoked? After the method is invoked? Both before and after method invocation? Or should it only be applied if a method throws an exception?

Файл:Fig42.png
Figure 4.2 An aspect’s functionality (advice) is woven into a program’s execution at one or more joinpoints.

Joinpoint

An electric company services several houses, perhaps even an entire city. Each house will have an electric meter that needs to be read and thus each house is a potential target for the meter-reader. The meter-reader could potentially read all kinds of devices, but to do his job, he needs to target electric meters that are attached to houses.

In the same way, your application may have thousands of opportunities for advice to be applied. These opportunities are known as joinpoints. A joinpoint is a point in the execution of the application where an aspect can be plugged in. This point could be a method being called, an exception being thrown, or even a field being modified. These are the points where your aspect’s code can be inserted into the normal flow of your application to add new behavior.

Pointcut

It’s not possible for any one meter-reader to visit all houses serviced by the electric company. Instead, they are assigned a subset of all of the houses to visit. Likewise, an aspect doesn’t necessarily advise all joinpoints in an application. Pointcuts help narrow down the joinpoints advised by an aspect.

If advice defines the what and when of aspects then pointcuts define the where. A pointcut definition matches one or more joinpoints at which advice should be woven. Often you specify these pointcuts using explicit class and method names or through regular expressions that define matching class and method name patterns. Some AOP frameworks allow you to create dynamic pointcuts that determine whether to apply advice based on runtime decisions, such as the value of method parameters.

Aspect

When a meter-reader starts his day, he knows both what he is supposed to do (report electricity usage) and which houses to collect that information from. Thus he knows everything he needs to know to get his job done.

An aspect is the merger of advice and pointcuts. Taken together, advice and pointcuts define everything there is to know about an aspect—what it does and where and when it does it.

Introduction

An introduction allows you to add new methods or attributes to existing classes (kind of mind-blowing, huh?). For example, you could create an Auditable advice class that keeps the state of when an object was last modified. This could be as simple as having one method, setLastModified(Date), and an instance variable to hold this state. The new method and instance variable can then be introduced to existing classes without having to change them, giving them new behavior and state.

Target

A target is the object that is being advised. This can be either an object you write or a third-party object to which you want to add custom behavior. Without AOP, this object would have to contain its primary logic plus the logic for any cross-cutting concerns. With AOP, the target object is free to focus on its primary concern, oblivious to any advice being applied.

Proxy

A proxy is the object created after applying advice to the target object. As far as the client objects are concerned, the target object (pre-AOP) and the proxy object (post-AOP) are the same—as they should be. That is, the rest of your application will not have to change to support the proxy object.

Weaving

Weaving is the process of applying aspects to a target object to create a new, proxied object. The aspects are woven into the target object at the specified joinpoints. The weaving can take place at several points in the target object’s lifetime:


  • Compile time—Aspects are woven in when the target class is compiled. This requires a special compiler. AspectJ’s weaving compiler weaves aspects this way.
  • Classload time—Aspects are woven in when the target class is loaded into the JVM. This requires a special ClassLoader that enhances that target class’s bytecode before the class is introduced into the application. AspectJ 5’s load-time weaving (LTW) support weaves aspects in this way.
  • Runtime—Aspects are woven in sometime during the execution of the application. Typically, an AOP container will dynamically generate a proxy object that will delegate to the target object while weaving in the aspects. This is how Spring AOP aspects are woven.

That’s a lot of new terms to get to know. Revisiting figure 4.2, you can now understand that advice contains the cross-cutting behavior that needs to be applied to an application’s objects. The joinpoints are all the points within the execution flow of the application that are candidates to have advice applied. The pointcut defines where (at what joinpoints) that advice is applied. The key concept you should take from this? Pointcuts define which joinpoints get advised.

Now that you’re familiar with some basic AOP terminology, let’s see how these core AOP concepts are implemented in Spring.

Spring’s AOP support

Not all AOP frameworks are created equal. They may differ in how rich of a joinpoint model they offer. Some allow you to apply advice at the field modification level, while others only expose the joinpoints related to method invocations. They may also differ in how and when they weave the aspects. Whatever the case, the ability to create pointcuts that define the joinpoints at which aspects should be woven is what makes it an AOP framework.

Much has changed in the AOP framework landscape in the past few years. There has been some housecleaning among the AOP frameworks, resulting in some frameworks merging and others going extinct. In 2005, the AspectWerkz project merged with AspectJ, marking the last significant activity in the AOP world and leaving us with three dominant AOP frameworks:



Since this is a Spring book, we will, of course, focus on Spring AOP. Even so, there’s a lot of synergy between the Spring and AspectJ projects, and the AOP support in Spring 2.0 borrows a lot from the AspectJ project. In fact, the <aop:spring-configured /> configuration element described in chapter 3 (see section 3.3) takes advantage of AspectJ’s support for constructor pointcuts and load-time weaving.

Spring’s support for AOP comes in four flavors:


  • Classic Spring proxy-based AOP (available in all versions of Spring)
  • @AspectJ annotation-driven aspects (only available in Spring 2.0)
  • Pure-POJO aspects (only available in Spring 2.0)
  • Injected AspectJ aspects (available in all versions of Spring)

The first three items are all variations on Spring’s proxy-based AOP. Consequently, Spring’s AOP support is limited to method interception. If your AOP needs exceed simple method interception (constructor or property interception, for example), you’ll want to consider implementing aspects in AspectJ, perhaps taking advantage of Spring DI to inject Spring beans into AspectJ aspects.

I’ll talk about AspectJ and how it fits into Spring a little later in this chapter (in sections 4.3.2 and 4.5). Because Spring’s AOP support is proxy based, that will be the focus of most of this chapter. But before we get started, it’s important to understand a few key points of Spring’s AOP framework.

Spring advice is written in Java

All of the advice you create within Spring will be written in a standard Java class. That way, you will get the benefit of developing your aspects in the same integrated development environment (IDE) you would use for your normal Java development. What’s more, the pointcuts that define where advice should be applied are typically written in XML in your Spring configuration file. This means both the aspect’s code and configuration syntax will be familiar to Java developers.

Contrast this with AspectJ, which is implemented as a language extension to Java. There are benefits and drawbacks to this approach. By having an AOPspecific language, you get more power and fine-grained control, as well as a richer AOP toolset. However, you are required to learn a new tool and syntax to accomplish this.

Spring advises objects at runtime

In Spring, aspects are woven into Spring-managed beans at runtime by wrapping them with a proxy class. As illustrated in figure 4.3, the proxy class poses as the target bean, intercepting advised method calls and forwarding those calls to the target bean. Between the time that the proxy intercepts the method call and the time it invokes the target bean’s method, the proxy performs the aspect logic.

Файл:Fig43.png
Figure 4.3 Spring aspects are implemented as proxies that wrap the target object. The proxy handles method calls, performs additional aspect logic,and then invokes the target method.

Spring does not create a proxied object until that proxied bean is needed by the application. If you are using an ApplicationContext, the proxied objects will be created when it loads all of the beans from the BeanFactory. Because Spring creates proxies at runtime, you do not need a special compiler to weave aspects in Spring’s AOP.

Spring generates proxied classes in two ways. If your target object implements an interface(s) that exposes the required methods, Spring will use the JDK’s java.lang.reflect.Proxy class. This class allows Spring to dynamically generate a new class that implements the necessary interfaces, weave in any advice, and proxy any calls to these interfaces to your target class.

If your target class does not implement an interface, Spring uses the CGLIB library to generate a subclass to your target class. When creating this subclass, Spring weaves in advice and delegates calls to the subclass to your target class. There are two important things to take note of when using this approach:


  • Creating a proxy with interfaces is favored over proxying classes, since this leads to a more loosely coupled application. The ability to proxy classes is provided so that legacy or third-party classes that do not implement interfaces can still be advised. This approach should be taken as the exception, not the rule.
  • Methods marked as final cannot be advised. Remember, Spring generates a subclass to your target class. Any method that needs to be advised is overridden and advice is woven in. This is not possible with final methods.

Spring only supports method joinpoints

As mentioned earlier, multiple joinpoint models are available through various AOP implementations. Because it is based on dynamic proxies, Spring only supports method joinpoints. This is in contrast to some other AOP frameworks, such as AspectJ and JBoss, which provide field and constructor joinpoints in addition to method pointcuts. Spring’s lack of field pointcuts prevents you from creating very fine-grained advice, such as intercepting updates to an object’s field. And without constructor pointcuts, there’s no way to apply advice when a bean is instantiated.

However, as Spring focuses on providing a framework for implementing J2EE services, method interception should suit most, if not all, of your needs. If you find yourself in need of more than method interception, you’ll want to complement Spring AOP with AspectJ. Now you have a general idea of what AOP does and how it is supported by Spring. It’s time to get our hands dirty creating aspects in Spring.

Creating classic Spring aspects

In chapter 2, we demonstrated dependency injection by putting on a talent show called Spring Idol. In that example, we wired up several performers as <bean>s to show their stuff. It was all greatly amusing. But a show like that needs an audience or else there’s little point in it.

Therefore, we’re now going to provide an audience for the talent show. The Audience class in listing 4.1 defines the functions of an audience.

Listing 4.1 Defining an audience for the Spring Idol competition

package com.springinaction.springidol;
 
public class Audience {
public Audience() {}
public void takeSeats() {
System.out.println("The audience is taking their seats.");
}
//Executes before performance
public void turnOffCellPhones() {
System.out.println("The audience is turning off " +
"their cellphones");
}
//Executes after performance
public void applaud() {
System.out.println("CLAP CLAP CLAP CLAP CLAP");
}
//Executes after bad performance
public void demandRefund() {
System.out.println("Boo! We want our money back!");
}
}

There’s nothing particularly special about the Audience class. In fact, it’s just a simple POJO. Nonetheless, this class will provide the basis for many of the examples in this chapter. It can be wired as Spring <bean> with the following XML:

 
<bean id="audience"
class="com.springinaction.springidol.Audience" />

Taking a closer look at the Audience class, you can see that it defines four different things that an Audience can do:* They can take their seats.


  • They can courteously turn off their cell phones.
  • They can give a round of applause.
  • They can demand a refund.

Although these functions clearly define an Audience’s behavior, it’s not clear when each method will be called. What we’d like is for the Audience to take their seats and turn off their cell phones prior to the performance, to applaud when the performance is good, and to demand a refund when the performance goes bad.

One option would be for us to inject an Audience into each performer and to change the perform() method to call the Audience’s methods. For example, consider an updated version of Instrumentalist in listing 4.2.

Listing 4.2 An instrumentalist that tells its audience how to respond

package com.springinaction.springidol;
 
public class Instrumentalist implements Performer {
public Instrumentalist() {}
 
public void perform() throws PerformanceException {
audience.takeSeats(); // Manipulates Audience
audience.turnOffCellPhones(); // Manipulates Audience
 
try {
System.out.print("Playing " + song + " : ");
instrument.play();
audience.applaud(); // Manipulates Audience
} catch (Throwable e) {
audience.demandRefund(); // Manipulates Audience
}
}
 
private String song;
public void setSong(String song) {
this.song = song;
}
 
private Instrument instrument;
public void setInstrument(Instrument instrument) {
this.instrument = instrument;
}
// Injects Audience
private Audience audience;
public void setAudience(Audience audience) {
this.audience = audience;
}
}

This would certainly work, but doesn’t it seem odd that the Instrumentalist has to tell its Audience what to do? The performer’s job is to give a performance, not to prompt its audience to respond to the performance. The audience’s job is to respond to that performance on its own, without being prompted to do so.

Another caveat to this approach is that every performer will need to be injected with an Audience and will need to call the Audience’s methods. As a result, the various implementations of Performer are dependent and coupled to the Audience class. This would mean that a performer couldn’t perform without an audience. So much for singing in the shower!

The most notable thing about injecting an audience into a performer is that the performer is completely responsible for asking the audience to applaud. In real life, this would be like a performer holding up an “Applaud!” sign. But when you think about it, the performer should focus on performing and not concern himself with whether or not the audience applauds. The audience should react to the performer’s performance… not be prodded by the performer.

In other words, the audience is a cross-cutting concern relative to the performer. Since cross-cutting concerns are the province of aspects, perhaps we should define the audience as an aspect. And that’s precisely what we’re going to do. Let’s start by defining some advice.

Creating advice

As mentioned earlier in this chapter, advice defines what an aspect does and when it does it. In Spring AOP, there are five types of advice, each defined by an interface (see table 4.1).

Table 4.1 Spring AOP advice comes in five forms that let you choose when advice is executed relative to a joinpoint.

Advice type
Interface
Before org.springframework.aop.MethodBeforeAdvice
After-returning org.springframework.aop.AfterReturningAdvice
After-throwing org.springframework.aop.ThrowsAdvice
Around org.aopalliance.intercept.MethodInterceptor
Introduction org.springframework.aop.IntroductionInterceptor

Notice that all of these interfaces are part of the Spring Framework, except for MethodInterceptor. When defining around advice, Spring takes advantage of a suitable interface that is already provided by the AOP Alliance, an open source project whose goal is to facilitate and standardize AOP. You can read more about the AOP Alliance on their website at https://aopalliance.sourceforge.net.

When you think about what an audience is expected to do and match it up against the advice types in table 4.1, it seems clear that taking their seats and turning off their cell phones is best performed as before advice. Likewise, applause is an after-returning advice. And after-throwing is an appropriate advice for demanding a refund.

AudienceAdvice (listing 4.3) is a class that implements three of the five advice interfaces from table 4.1 to define the advice applied by an audience. (We’ll talk about the other advice types a little later in this chapter.)

Listing 4.3 Advice that defines how an audience’s functionality is applied

package com.springinaction.springidol;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
 
public class AudienceAdvice implements
MethodBeforeAdvice, // Implements
AfterReturningAdvice, // three types
ThrowsAdvice { // of advice
 
public AudienceAdvice() {}
 
public void before(Method method, Object[] args, Object target)
throws Throwable {
audience.takeSeats(); // Invokes before method
audience.turnOffCellPhones(); // Invokes before method
}
 
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
audience.applaud(); // Executes after successful return
}
 
public void afterThrowing(Throwable throwable) {
audience.demandRefund(); //Executes after exception thrown
}
 
private Audience audience;
public void setAudience(Audience audience) {
this.audience = audience;
}
}

There’s a lot going on in listing 4.3, but one thing to take notice of is that AudienceAdvice has an Audience as a dependency. Therefore, we’ll need to declare the AudienceAdvice in Spring as follows:

<bean id="audienceAdvice"
class="com.springinaction.springidol.AudienceAdvice">
<property name="audience" ref="audience" />
</bean>

AudienceAdvice is a single class that implements three different types of AOP advice. Let’s break it down one advice type at a time, starting with before advice.

Before advice

We want our audience to take their seats and turn off their cell phones prior to a performance. Therefore, AudienceAdvice provides before advice by implementing the MethodBeforeAdvice interface. This interface requires that a before() method be implemented:

public void before(Method method, Object[] args, Object target)
throws Throwable {
audience.takeSeats();
audience.turnOffCellPhones();
}

The before() method takes three parameters. The first parameter is a java.lang.reflect.Method object that represents the method to which the advice is being applied. The second parameter is an array of Objects that are the arguments that were passed to the method when the method was called. The final parameter is the target of the method invocation (i.e., the object on which the method was called).

If you’re familiar with Java’s dynamic proxy support that was introduced in Java 1.3, these parameters may seem familiar. They are nearly the same parameters that are given to the invoke() method of java.lang.reflect.InvocationHandler.

These parameters are available to you if your advice needs them. In this case, however, they’re ignored, as their values have no bearing on the functionality of the audience.

After returning advice

If a performance goes well (i.e., if no exceptions are thrown), we’d like the audience to graciously applaud. We know that a performance is successful if the perform() method returns. Therefore, AudienceAdvice implements After-

public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
audience.applaud();
}

You’ll notice that the parameters to the afterReturning() method aren’t much different than the parameters to the before() method of MethodBeforeAdvice. The only difference is that an additional parameter has been added as the first parameter. This parameter holds the value that was returned from the invoked method.

Again, as with before(), the parameters are irrelevant to the audience and are thus ignored.

After throwing advice

People paid good money to be in the audience to see these performances. If anything goes wrong, they’re going to want their money back. Therefore, if the perform() method fails for any reason—that is, if the method throws an exception— the audience will demand their money back. To accommodate after throwing advice, the AudienceAdvice class implements the ThrowsAdvice interface.

Unlike MethodBeforeAdvice and AfterReturningAdvice, however, ThrowsAdvice doesn’t require that any method be implemented. ThrowsAdvice is only a marker interface that tells Spring that the advice may wish to handle a thrown exception.

An implementation of ThrowsAdvice may implement one or more afterThrowing() methods whose signatures take the following form:

public void afterThrowing([method], [args], [target], throwable);


All of the parameters of afterThrowing() are optional except for the one that is a Throwable type. It is this parameter that tells Spring which exceptions should be handled by the advice. For example, suppose we want to write a log entry every time that a NullPointerException is thrown. The following afterThrowing() method handles that task:

public void afterThrowing(Method method, Object[] args,
Object target, NullPointerException e) {
LOGGER.error("NPE thrown from " + method.getName());
}


In the case of AudienceAdvice, only one afterThrowing() method is defined:

public void afterThrowing(Throwable throwable) {
audience.demandRefund();
}

This indicates that we want the audience to demand a refund if any Exception is thrown from the perform() method. Furthermore, since the invocation target, method, and arguments are unimportant to the audience, the parameters are left out of the method signature.

With the audience advice class defined, we’re ready to associate the advice with a pointcut to create a complete aspect. But first, let’s look at how the same advice could have been implemented as an around advice.

Around advice

Around advice is effectively before, after-returning, and after-throwing advice all rolled into one. In Spring, around advice is defined by the AOP Alliance’s MethodInterceptor interface. In our example, the AudienceAdvice class could be rewritten as around advice, as shown in listing 4.4. This new AudienceAroundAdvice class is equivalent to AudienceAdvice, but is implemented as around advice.


Listing 4.4 Defining audience advice as around advice

package com.springinaction.springidol;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
 
public class AudienceAroundAdvice
implements MethodInterceptor {
// Implements MethodInterceptor
public Object invoke(MethodInvocation invocation)
throws Throwable {
try {
 
audience.takeSeats(); // Executes before
audience.turnOffCellPhones(); // method call
 
// Calls target method
Object returnValue = invocation.proceed();
 
audience.applaud(); // Executes after successful return
 
return returnValue;
} catch (PerformanceException throwable) {
 
audience.demandRefund(); // Executes after exception thrown
 
throw throwable;
}
}
// injected
private Audience audience;
public void setAudience(Audience audience) {
this.audience = audience;
}
}

The MethodInterceptor interface requires that only an invoke() method be implemented. In the case of AudienceAroundAdvice, the invoke() method instructs the audience to take their seats and turn off their cell phones. Next it calls proceed() on the method invocation to cause the advised method to be invoked. If a PerformanceException is caught from calling invocation.proceed(), the audience will demand a refund. Otherwise, the audience will applaud.

The nice thing about writing around advice is that you can succinctly define before and after advice in one method. If you have advice that will be applied both before and after a method, you may find around advice preferable to implementing the individual interface for each advice type. It’s less beneficial, however, if you only need before advice or after advice (but not both).

Around advice also offers you the opportunity to inspect and alter the value returned from the advised method. This makes it possible to write advice that performs some postprocessing on a method’s return value before returning the value to the caller. AfterReturningAdvice only allows you to inspect the returned value—you can’t change it.

But around advice has one minor gotcha: you must remember to call proceed(). Failure to call proceed() will result in the advice being applied but the target method never being executed. But then again, that may be what you want. Perhaps you’d like to prevent execution of a method under certain conditions. Compare that to MethodBeforeAdvice, where you can inspect the method and its parameters prior to invocation but you can’t stop the method from being invoked (short of throwing an exception, breaking the execution chain).

At this point, we’ve seen several ways to create advice that defines both the what and the when of aspects. But if you take a close look at either AudienceAdvice or AudienceAroundAdvice, you won’t find any clues as to what methods those advices will be applied to. That brings us to the topic of pointcuts to define the where characteristic of aspects.

Файл:Fig44.png
Figure 4.4 Pointcuts select one or more joinpoints where advice should be applied by an aspect. In this case, all methods that perform a transfer operation are singled out by the pointcut.

Defining pointcuts and advisors

So far we have only discussed how to create AOP advice. This is not very useful if we cannot expressively define where this advice should be applied in our application.

This is where pointcuts come in. Recall that joinpoints are the points within application code where aspect advice could be woven in. Pointcuts are a way of selecting a subset of all possible joinpoints where advice should be woven, as illustrated in figure 4.4.

Spring comes with several different types of pointcuts to choose from. Two of the most useful pointcuts are regular expression pointcuts and AspectJ expression pointcuts. Let’s look at regular expression pointcuts first.

Declaring a regular expression pointcut

The main purpose of a pointcut is to choose which method(s) that advice will be applied to, usually by matching a method signature against some pattern. If you’re a fan of regular expressions, you may want to use a regular expression pointcut to match the method signature.

Spring comes with two classes that implement regular expression pointcuts:


  • org.springframework.aop.support.Perl5RegexpMethodPointcut—Useful when an application will be running in a pre-Java 1.4 environment. Requires Jakarta ORO.
  • org.springframework.aop.support.JdkRegexpMethodPointcut—Best choice when running in Java 1.4 or higher. Does not require Jakarta ORO.

Since we’ll be targeting a Java 1.5 runtime, we’re going to define the pointcut using JdkRegexpMethodPointcut as follows:

<bean id="performancePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*perform" />
</bean>

The pattern property is used to specify the pointcut pattern used in method matching. Here the pattern property has been set to a regular expression that should match any method called perform() on any class.

Once you have defined a pointcut, you’ll need to associate it with advice. The following <bean> associates the regular expression pointcut we just defined with the audience advice defined in the previous section:

<bean id="audienceAdvisor"
class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="audienceAdvice" />
<property name="pointcut" ref="performancePointcut" />
</bean>

DefaultPointcutAdvisor is an advisor class that simply associates advice with a pointcut. Here the advice property has been set to reference the audienceAdvice bean from the previous section. Meanwhile, the pointcut property references the performancePointcut bean, which is our pointcut that matches the perform() method.

Combining a pointcut with an advisor

Although the audienceAdvisor bean completely defines an aspect by associating a pointcut with advice, there’s a slightly terser way to define an advisor with a regular expression pointcut.

RegexpMethodPointcutAdvisor is a special advisor class that lets you define both a pointcut and an advisor in a single bean. To illustrate, consider the following <bean> declaration:

<bean id="audienceAdvisor"
class="org.springframework.aop.support. RegexpMethodPointcutAdvisor">
<property name="advice" ref="audienceAdvice" />
<property name="pattern" value=".*perform" />
</bean>

This single <bean> does the work of two beans. It is effectively equivalent to both the performancePointcut bean and the previously defined audienceAdvisor bean.

Defining AspectJ pointcuts

Although regular expressions work fine as a pointcut definition language, their purpose is for general-purpose text parsing—they weren’t created with pointcuts in mind. Contrast them with how pointcuts are defined in AspectJ and you’ll find that AspectJ’s pointcut language is a true pointcut expression language.

If you’d prefer to use AspectJ-style expressions when defining your Spring pointcuts, you’ll want to use AspectJExpressionPointcut instead of JdkRegexpMethodPointcut. The following <bean> declares the performance pointcut using an AspectJ pointcut expression:

<bean id="performancePointcut"
class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
<property name="expression" value="execution(* Performer+.perform(..))" />
</bean>
Файл:Fig45.png
Figure 4.5 Spring AOP uses AspectJ-style pointcuts to select places to apply advice. This pointcut specifies that advice should be applied on any method named “perform” on any class with any number of arguments.

The pointcut expression is defined as a value of the expression property. In this case, we’re indicating that the pointcut should trigger advice when any perform() method taking any arguments is executed on a Performer, returning any type. Figure 4.5 summarizes the AspectJ expression used.

To associate the AspectJ expression pointcut with the audience advice, you could use DefaultPointcutAdvisor, just as with regular expression pointcut. But just as with regular expression pointcuts, you can also simplify how pointcuts and advice are tied together by using a special advisor that lets you define the pointcut expression as a property of the advisor. For AspectJ pointcut expressions, the advisor class to use is AspectJExpressionPointcutAdvisor. The following <bean> applies the audience advice to the perform() method using an AspectJ pointcut expression:

<bean id="audienceAdvisor"
class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
<property name="advice" ref="audienceAdvice" />
<property name="expression" value="execution(* *.perform(..))" />
</bean>

The advice property references the advice being applied—here it’s the audienceAdvice bean from earlier. The expression property is where the AspectJ pointcut expression is set.

In Spring AOP, advisors completely define an aspect by associating advice with a pointcut. But aspects in Spring are proxied. Whether you use regular expression pointcuts or AspectJ pointcuts, you’ll still need to proxy your target beans for the advisors to take effect. For that, you’ll need to declare one or more ProxyFactoryBeans.

Using ProxyFactoryBean

As you may recall from chapter 2, one of the performers in the Spring Idol competition is the juggling poet named Duke. As a quick reminder, here’s how Duke is declared as a <bean> in Spring:

<bean id="dukeTarget"
class="com.springinaction.springidol.PoeticJuggler"
autowire="constructor">
<constructor-arg ref="sonnet29" />
</bean>

If you’re paying close attention, you’ve probably noticed one small change that was made to this <bean> declaration. The id attribute has changed from duke to dukeTarget. We’ll explain why this has been done in a moment. But for now we wanted to draw your attention to this new id.

For a bean to be advised by an advisor, it must be proxied. Spring’s ProxyFactoryBean is a factory bean that produces a proxy that applies one or more interceptors (and advisors) to a bean. The following <bean> definition creates a proxy for the duke bean:

<bean id="duke"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="dukeTarget" />
<property name="interceptorNames" value="audienceAdvisor" />
<property name="proxyInterfaces"
value="com.springinaction.springidol.Performer" />
</bean>
Файл:Fig46.png
Figure 4.6 When the perform() method is called on the PoeticJuggler, the call is intercepted by the proxy and execution is given to the audienceAdvisor bean before the actual perform() method is executed.

The most notable thing about this bean is that its id is duke. But hold on—won’t that mean that when the Spring container is asked for a bean named duke it will be the proxy and not the PoeticJuggler that is returned? That’s absolutely right. In fact, that’s elemental to how Spring AOP works. As depicted in figure 4.6, when you invoke a method on an advised bean, you are actually invoking a method on the proxy. The proxy will use the pointcut to decide whether advice should be applied (or not), and then it invokes the advised bean itself.

Because the ProxyFactoryBean has been given the id of the advised bean (duke), the advised bean will need to be given a new id. That’s why we renamed the actual PoeticJuggler bean as dukeTarget. And it’s the dukeTarget bean that is referenced by the target property of ProxyFactoryBean. Put simply, this property tells ProxyFactoryBean which bean it will be proxying.

The interceptorNames property tells ProxyFactoryBean which advisors to apply to the proxied bean. This property takes an array of Strings, of which each member is the name of an interceptor/advisor bean in the Spring context. In our case, we only want to apply a single advisor, so we provide a single value of audienceAdvisor (don’t worry; Spring will automatically turn that value into a single member array). However, we could have just as easily set that property explicitly as an array using the following XML:

<property name="interceptorNames">
<list>
<value>audienceAdvisor</value>
</list>
</property>

The final property set on ProxyFactoryBean is proxyInterfaces. ProxyFactoryBean produces a Java dynamic proxy that advises the target bean, but you’ll still need a way to invoke methods on that proxy. The proxyInterfaces property tellsProxyFactoryBean which interface(s) the proxy should implement. As with the interceptorNames property, this property is an array property—actually, an array of java.lang.Class. But we specified the value as a single String value. Fortunately, Spring is smart enough (using the ClassEditor property editor from table 3.1) to translate that single String value into a single-member Class array.

Abstracting ProxyFactoryBean

So far we’ve only proxied Duke. This means that the audience will only attend Duke’s performance. If we want the audience to take their seats, turn off their cell phones, and applaud for our other performers, then we’ll need to proxy the other performers as well.

To that end, here’s Stevie, proxied with the audience advisor:

<bean id="stevie"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="stevieTarget" />
<property name="proxyInterfaces"
value="com.springinaction.springidol.Performer" />
<property name="interceptorNames" value="audienceAdvisor" />
</bean>

Now the audience will watch Stevie’s performance as well as Duke’s. But wait a minute—do we have to write all of this XML for each and every bean that we want to proxy? The proxyInterfaces property will be the same for all performers. And the interceptorNames property will be the same. It seems a bit too much to have to repeat this information for all of our performers when the only thing that will be different will be the target property.

It’s often the case that an aspect will be applied to multiple beans in your application. In fact, that’s why aspects are said to handle cross-cutting concerns— because an aspect’s concern cuts across multiple objects. Although you could write the same ProxyFactoryBean declaration for all of the beans to be advised, there’s a better way that cuts down on the amount of redundant XML.

The trick is to declare a single ProxyFactoryBean as an abstract bean, then reuse that declaration as a parent for each of the advised beans. For example, audienceProxyBase declares an abstract bean with the common proxyInterfaces and interceptorNames properties set:

<bean id="audienceProxyBase"
class="org.springframework.aop.framework.ProxyFactoryBean"
abstract="true">
<property name="proxyInterfaces"
value="com.springinaction.springidol.Performer" />
<property name="interceptorNames" value="audienceAdvisor" />
</bean>

The audienceProxyBase bean has its abstract attribute set to true, indicating that it is an abstract bean and that Spring shouldn’t try to instantiate it directly. Instead, this bean will serve as the basis for the other performer beans. Here are the new, terser, declarations of stevie and duke, which use the parent attribute to extend the audienceProxyBase bean:

<bean id="stevie" parent="audienceProxyBase">
<property name="target" ref="stevieTarget" />
</bean>
 
<bean id="duke" parent="audienceProxyBase">
<property name="target" ref="dukeTarget" />
</bean>

That’s much more succinct, isn’t it? In this form, only the variant target property is declared. The common properties are inherited from the parent bean.

Using abstract beans to define a parent for all of your advised beans is a great way to cut down on the amount of XML in your Spring configuration. However, there’s still more that you can do to reduce the amount of XML required to proxy beans with advisors. Coming up next, you’ll learn how to eliminate the need for ProxyFactoryBean and have Spring automatically proxy beans to be advised.

Autoproxying

One thing that may have struck you as odd from the previous section is that we had to rename our bean to dukeTarget and then give the ProxyFactoryBean an id of duke. This left us with a strange arrangement of beans: the bean that actually represents Duke is named dukeTarget, while the bean named duke is really a ProxyFactoryBean with the purpose of proxying the real Duke with an audience.

If you found that unclear, don’t feel too bad. It’s a confusing concept that baffles most programmers who are just getting their feet wet with Spring AOP.

In addition to confusion, ProxyFactoryBean also lends to the verbosity in the Spring configuration file. Even if you define an abstract ProxyFactoryBean, you will still need declare two beans for each bean that is advised: the target bean and the proxy bean. It would be so much nicer if we could simply declare the advisor once and let Spring automatically create proxies for beans whose methods match the advisor’s pointcut.

Good news! Spring provides support for automatic proxying of beans. Autoproxying provides a more complete AOP implementation by letting an aspect’s pointcut definition decide which beans need to be proxied, rather than requiring you to explicitly create proxies for specific beans.

Actually, there are two ways to autoproxy beans:


  • Basic autoproxying of beans based on advisor beans declared in the Spring context— The advisor’s pointcut expression is used to determine which beans and which methods will be proxied.
  • Autoproxying based on @AspectJ annotation-driven aspects—The pointcut specified on the advice contained within the aspect will be used to choose which beans and methods will be proxied.

Using either of these autoproxying strategies can eliminate ProxyFactoryBean from your Spring context XML file. The former approach to autoproxying uses the advisors we’ve already created up to this point in Spring. Let’s start by looking at this basic autoproxy mechanism.

Creating autoproxies for Spring aspects

If you take a look at the audienceAdvisor bean declared in section 4.2.2, you’ll see that it has all of the information needed to advise our performer beans:

<bean id="audienceAdvisor"
class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
<property name="advice" ref="audienceAdvice" />
<property name="expression" value="execution(* *.perform(..))" />
</bean>

The advice property tells it what advice to apply and the expression property tells it where to apply that advice. Despite that wealth of information, we still have to explicitly declare a ProxyFactoryBean for Spring to proxy our performers.

However, Spring comes with a handy implementation of BeanPostProcessor (see chapter 3) called DefaultAdvisorAutoProxyCreator, which automatically checks to see whether an advisor’s pointcut matches a bean’s methods and replaces that bean’s definition with a proxy that applies the advice. In a nutshell, it automatically proxies beans with matching advisors.

To use DefaultAdvisorAutoProxyCreator, all you have to do is declare the following <bean> in your Spring context:

<bean class="org.springframework.aop.framework.autoproxy.
DefaultAdvisorAutoProxyCreator" />

Notice that this bean doesn’t have an id. That’s because we’ll never refer to it directly. Instead, the Spring container will recognize it as a BeanPostProcessor and put it to work creating proxies.


With DefaultAdvisorAutoProxyCreator declared, we no longer need to declare ProxyFactoryBeans in the Spring context. What’s more, we no longer have to give our beans weird names that end with target. We can now give them appropriate names like steve or duke:

<bean id="duke"
class="com.springinaction.springidol.PoeticJuggler"
autowire="constructor">
<constructor-arg ref="sonnet29" />
</bean>

In this way, we are able to keep both bean declarations and bean code free from the aspect-related details.

Spring’s basic autoproxy facility is fine for working with simple advice or when in a pre–Java 5 environment. But if you’re targeting Java 5, you may want to consider Spring’s support for AspectJ’s annotation-based aspects. Let’s see how to create aspects in Spring that are annotation based.

Autoproxying @AspectJ aspects

A major new feature of AspectJ 5 is the ability to annotate POJO classes to be aspects. This new feature is commonly referred to as @AspectJ Prior to AspectJ 5, writing AspectJ aspects involved learning a Java language extension. But AspectJ’s new aspect annotations make it simple to turn any class into an aspect just by sprinkling a few annotations around.

Looking back at our Audience class, we see that Audience contained all of the functionality needed for an audience, but none of the details to make it an aspect. That left us having to create advice, pointcuts, and advisors—AOP plumbing—to define an audience aspect.

But with @AspectJ annotations, we can revisit our Audience class and turn it into an aspect without the need for any additional classes or bean declarations. Listing 4.5 shows the new Audience class, now annotated to be an aspect.


Listing 4.5 Annotating Audience to be an aspect

package com.springinaction.springidol;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Audience
public Audience() {}
@Pointcut("execution(* *.perform(..))")
public void performance() {}
@Before("performance()")
public void takeSeats() {
System.out.println("The audience is taking their seats.");
}
@Before("performance()")
public void turnOffCellPhones() {
System.out.println("The audience is turning off their cellphones");
}
@AfterReturning("performance()")
public void applaud() {
System.out.println("CLAP CLAP CLAP CLAP CLAP");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("Boo! We want our money back!");
}
}

The new Audience class is now annotated with @Aspect This annotation indicates that Audience is not just any old POJO but that it is an aspect.

The @Pointcut annotation is used to define a reusable pointcut within an [@AspectJ aspect. The value given to the @Pointcut annotation is an AspectJ pointcut expression—here indicating that the pointcut should match the perform() method of any class. The name of the pointcut is derived from the name of the method to which the annotation is applied. Therefore, the name of this pointcut is performance(). The actual body of the performance() method is irrelevant and, in fact, should be empty. The method itself is just a marker, giving the @Pointcutannotation something to attach itself to.

Each of the audience’s methods has been annotated with advice annotations. The @Before annotation has been applied to both takeSeats() and turnOffCellPhones() to indicate that these two methods are before advice. The @AfterReturning annotation indicates that the applaud() method is an after-returning advice method. And the @AfterReturning annotation is placed on demandRefund() so that it will be called if any exceptions are thrown during the performance.

The name of the performance() pointcut is given as the value parameter to all of the advice annotations. This tells each advice method where it should be applied.Notice that aside from the annotations and the no-op performance() method, the Audience class is functionally unchanged. This means that it’s still a simple Java object and can be used as such. It can also still be wired in Spring as follows:

<bean id="audience"
class="com.springinaction.springidol.Audience" />

Because the Audience class contains everything that’s needed to define its own pointcuts and advice, there’s no more need for a class that explicitly implements one of Spring’s advice interfaces. There’s also no further need to declare an advisor bean in Spring. Everything needed to use Audience as advice is now contained in the Audience class itself.

There’s just one last thing to do to make Spring apply Audience as an aspect.

You must declare an autoproxy bean in the Spring context that knows how to turn @AspectJ-annotated beans into proxy advice.

For that purpose, Spring comes with an autoproxy creator class called AnnotationAwareAspectJAutoProxyCreator. You could register an AnnotationAwareAspectJAutoProxyCreator as a <bean> in the Spring context, but that would require a lot of typing (believe me… I’ve typed it twice so far). Instead, to simplify that rather long name, Spring also provides a custom configuration element in the aop namespace that’s much easier to remember:

<aop:aspectj-autoproxy />

<aop:aspectj-autoproxy/> will create an AnnotationAwareAspectJAutoProxyCreator in the Spring context and will automatically proxy beans whose methods match the pointcuts defined with @Pointcutannotations in @Aspect-annotated beans.

To use the <aop:aspectj-autoproxy> configuration element, you’ll need to remember to include the aop namespace in your Spring configuration file:

<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
https://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop-2.0.xsd">


</beans>


You should be aware that AnnotationAwareAspectJAutoProxyCreator only uses @AspectJ’s annotations as a guide for creating proxy-based aspects. Under the covers, it’s still Spring-style aspects. This is significant because it means that although you are using @AspectJ’ annotations, you are still limited to proxying method invocations.

You may also be interested to know that AnnotationAwareAspectJAutoProxyCreator also creates proxies based on classic Spring advisors. That is, it also does the same job that DefaultAdvisorAutoProxyCreator does. So, if you have any advisor beans declared in your Spring context, those will also automatically be used to advise proxied beans.

Annotating around advice

Just as with classic Spring advice, you are not limited to before and after advice types when using @AspectJ annotations. You may also choose to create around advice. For that, you must use the @Around annotation, as in the following example:

@Around("performance()")
public void watchPerformance (ProceedingJoinPoint joinpoint) {
System.out.println("The audience is taking their seats.");
System.out.println("The audience is turning off " +
"their cellphones");
try {
joinpoint.proceed();
System.out.println("CLAP CLAP CLAP CLAP CLAP");
} catch (PerformanceException throwable) {
System.out.println("Boo! We want our money back!");
}
}

Here the @Around annotation indicates that the watchPerformance() method is to be applied as around advice to the performance() pointcut.

As you may recall from section 4.2.1, around advice methods must remember to explicitly invoke proceed() so that the proxied method will be invoked. But simply annotating a method with @Around isn’t enough to provide a proceed() method to call. Therefore, methods that are to be around advice must take a ProceedingJoinPoint object as an argument and then call the proceed() method on that object.

Autoproxying of aspects sure makes configuring Spring aspects a lot simpler and makes the application of aspects transparent. But in its transparency, autoproxying obscures many details of the aspects. With autoproxying it is less apparent as to which beans are aspects and which beans are being proxied. In the next section, we’ll see how some new features in Spring 2.0 achieve a middle ground where aspects are explicitly defined but without all of the XML verbosity of using ProxyFactoryBean.

Declaring pure-POJO aspects

The Spring development team recognized that using ProxyFactoryBean is somewhat clumsy. So, they set out to provide a better way of declaring aspects in Spring. The outcome of this effort is found in the new XML configuration elements that come with Spring 2.0.

You’ve already seen one of the new elements in the aop namespace <aop:aspectj-autoproxy>. But Spring 2.0 comes with several more configuration elements in the aop namespace that make it simple to turn any class into an aspect. The new AOP configuration elements are summarized in table 4.2.

Revisiting our audience example one last time, you’ll recall that the Audience class has all of the methods that define an audience’s functionality. We only need to turn that Audience class into an aspect with pointcuts that tell it when to perform each of its actions. In the previous section we did that with @AspectJ annotations, but this time we’ll do it using Spring’s AOP configuration elements.

The great thing about Spring’s AOP configuration elements is that they can be used to turn any class into an aspect. The original Audience class from listing 4.1, for instance, is just a plain Java class—no special interfaces or annotations. Using Spring’s AOP configuration elements, as shown in listing 4.6, we can turn the audience bean into an aspect.

Table 4.2 Spring 2.0’s AOP configuration elements simplify declaration of POJO-based aspects.

AOP configuration element
Purpose
<aop:advisor> Defines an AOP advisor.
<aop:after> Defines an AOP after advice (regardless of whether the advised method returns successfully).
<aop:after-returning> Defines an AOP after-returning advice.
<aop:after-throwing> Defines an AOP after-throwing advice.
<aop:around> Defines an AOP around advice.
<aop:aspect> Defines an aspect.
<aop:before> Defines an AOP before advice.
<aop:config> The top-level AOP element. Most <aop:*> elements must be contained within <aop:config>.
<aop:pointcut> Defines a pointcut.

Listing 4.6 Defining an audience aspect using Spring’s AOP configuration elements

<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
https://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="audience"
class="com.springinaction.springidol.Audience" />
<!--References audience bean as aspect -->
<aop:config>
<aop:aspect ref="audience">
<!--Executes before performance -->
<aop:before
method="takeSeats"
pointcut="execution(* *.perform(..))" />
<!--Executes before performance -->
<aop:before
method="turnOffCellPhones"
pointcut="execution(* *.perform(..))" />
<!--Executes after performance -->
<aop:after-returning
method="applaud"
pointcut="execution(* *.perform(..))" />
<!--Executes after bad performance -->
<aop:after-throwing
method="demandRefund"
pointcut="execution(* *.perform(..))" />
</aop:aspect>
</aop:config>
</beans>
Файл:Fig47.png
Figure 4.7 The Audience aspect includes four bits of advice that weave advice logic around methods that match the aspect’s pointcut.

The first thing to notice about the Spring AOP configuration elements is that most of them must be used within the context of the <aop:config> element. There are a few exceptions to this rule, but none of those exceptions appear in this section. When we encounter such an exception elsewhere in this book, I’ll be sure to point it out.

Within <aop:config> you may declare one or more advisors, aspects, or pointcuts. In listing 4.6, we’ve declared a single aspect using the <aop:aspect> element. The ref attribute references the POJO bean that will be used to supply the functionality of the aspect—in this case, Audience. The bean that isreferenced by the ref attribute will supply the methods called by any advice in the aspect.

The aspect has four different bits of advice. The two <aop:before> elements define method before advice that will call the takeSeats() and turnOffCellPhones() methods (declared by the method attribute) of the Audience bean before any methods matching the pointcut are executed. The <aop:afterreturning> element defines an after-returning advice to call the applaud() method after the pointcut. Meanwhile, the <aop:after-throwing> element defines an after-throwing advice to call the demandRefund() method if any exceptions are thrown. Figure 4.7 shows how the advice logic is woven into the business logic.

In all advice elements, the pointcut attribute defines the pointcut where the advice will be applied. The value given to the pointcut attribute is a pointcut defined in AspectJ’s pointcut expression syntax.

You’ll notice that the value of the pointcut attribute is the same for all of the advice elements. That’s because all of the advice is being applied to the same pointcut. This, however, presents a DRY (don’t repeat yourself) principle violation. If you decide later to change the pointcut, you must change it in four different places.

To avoid duplication of the pointcut definition, you may choose to define a named pointcut using the <aop:pointcut> element. The XML in listing 4.7 shows how the <aop:pointcut> element is used within the <aop:aspect> element to define a named pointcut that can be used by all of the advice elements.

Listing 4.7 Defining a named pointcut to eliminate redundant pointcut definitions

<aop:config>
<aop:aspect ref="audience">
<!--Defines performance pointcut-->
<aop:pointcut
id="performance"
expression="execution(* *.perform(..))" />
 
<aop:before
method="takeSeats"
pointcut-ref="performance" /> <!--References pointcut-->
 
<aop:before
method="turnOffCellPhones"
pointcut-ref="performance" /> <!--References pointcut-->
 
<aop:after-returning
method="applaud"
pointcut-ref="performance" /> <!--References pointcut-->
 
<aop:after-throwing
method="demandRefund"
pointcut-ref="performance" /> <!--References pointcut-->
</aop:aspect>
</aop:config>

Now the pointcut is defined in a single location and is referenced across multiple advice elements. The <aop:pointcut> element defines the pointcut to have an id of performance. Meanwhile, all of the advice elements have been changed to reference the named pointcut with the pointcut-ref attribute.

As used in listing 4.7, the <aop:pointcut> element defines a pointcut that can be referenced by all advices within the same <aop:aspect> element. But you can also define pointcuts that can be used across multiple aspects by placing the <aop:pointcut> elements within the scope of the <aop:config> element.

It’s worth mentioning at this point that both the <aop:aspect> element and the @AspectJ annotations are effective ways to turn a POJO into an aspect. But <aop:aspect> has one distinct advantage over @AspectJ in that you do not need the source code of the class that is to provide the aspect’s functionality. With @AspectJ you must annotate the class and methods, which requires having the source code. But <aop:aspect> can reference any bean.Spring AOP enables separation of cross-cutting concerns from an application’s business logic. But as we’ve mentioned, Spring aspects are still proxy based and are limited to advising method invocations. If you need more than just method proxy support, you’ll want to consider using AspectJ. In the next section, you’ll see how AspectJ aspects can be used within a Spring application.

Injecting AspectJ aspects

Although Spring AOP is sufficient for many applications of aspects, it is a weak AOP solution when contrasted with AspectJ. AspectJ offers many types of pointcuts that are simply not possible with Spring AOP.

Constructor pointcuts, for example, are convenient when you need to apply advice upon the creation of an object. Unlike constructors in some other object-oriented languages, Java constructors are different from normal methods. This makes Spring’s proxy-based AOP woefully inadequate for advising creation of an object.

For the most part, AspectJ aspects are independent of Spring. Although they can certainly be woven into any Java-based application, including Spring applications, there’s little involvement on Spring’s part in applying AspectJ aspects.

However, any well-designed and meaningful aspect will likely depend on other classes to assist in its work. If an aspect depends on one or more classes when executing its advice, you can instantiate those collaborating objects with the aspect itself. Or, better yet, you can use Spring’s dependency injection to inject beans into AspectJ aspects.

To illustrate, let’s create a new aspect for the Spring Idol competition. A talent competition needs a judge. So, let’s create a judge aspect in AspectJ. JudgeAspect (listing 4.8) is such an aspect.

Listing 4.8 An AspectJ implementation of a talent competition judge

package com.springinaction.springidol;
 
public aspect JudgeAspect {
public JudgeAspect() {}
 
pointcut performance() : execution(* perform(..));
 
after() returning() : performance() {
System.out.println(criticismEngine.getCriticism());
}
 
// injected
private CriticismEngine criticismEngine;
public void setCriticismEngine(CriticismEngine criticismEngine) {
this.criticismEngine = criticismEngine;
}
}
Файл:Fig48.png
Figure 4.8 Aspects need injection, too. Spring can inject AspectJ aspects with dependencies just as if they were another bean.

The chief responsibility for JudgeAspect is to make commentary on a performance after the performance has completed. The performance() pointcut in listing 4.8 matches the perform() method. When it’s married with the after() returning() advice, you get an aspect that reacts to the completion of a performance.

What makes listing 4.8 interesting is that the judge doesn’t make simple commentary on its own. Instead, JudgeAspect collaborates with a CriticismEngine object, calling its getCriticism() method, to produce critical commentary after a performance. To avoid unnecessary coupling between JudgeAspect and the CriticismEngine, the JudgeAspect is given a reference to a CriticismEngine through setter injection. This relationship is illustrated in figure 4.8.

CriticismEngine itself is an interface that declares a simple getCriticism() method. An implementation of CriticismEngine is found in listing 4.9.

Listing 4.9 An implementation of the CriticismEngine used by JudgeAspect

package com.springinaction.springidol;
 
public class CriticismEngineImpl implements CriticismEngine {
public CriticismEngineImpl() {}
 
public String getCriticism() {
int i = (int) (Math.random() * criticismPool.length);
 
return criticismPool[i];
}
 
// injected
 
private String[] criticismPool;
public void setCriticismPool(String[] criticismPool) {
this.criticismPool = criticismPool;
}
}

CriticismEngineImpl implements the CriticismEngine interface by randomly choosing a critical comment from a pool of injected criticisms. This class can be declared as a Spring <bean> using the following XML:

<bean id="criticismEngine"
class="com.springinaction.springidol.CriticismEngineImpl">
<property name="criticisms">
<list>
<value>I'm not being rude, but that was appalling.</value>
<value>You may be the least talented
person in this show.</value>
<value>Do everyone a favor and keep your day job.</value>
</list>
</property>
</bean>

So far, so good. We now have a CriticismEngine implementation to give to JudgeAspect. All that’s left is to wire CriticismEngineImpl into JudgeAspect.

Before we show you how to do the injection, you should know that AspectJ aspects can be woven into your application without involving Spring at all. But if you want to use Spring’s dependency injection to inject collaborators into an AspectJ aspect, you’ll need to declare the aspect as a <bean> in Spring’s configuration. The following <bean> declaration injects the criticismEngine bean into JudgeAspect:

<bean class="com.springinaction.springidol.JudgeAspect"
factory-method="aspectOf">
<property name="criticismEngine" ref="criticismEngine" />
</bean>

For the most part, this <bean> declaration isn’t much different from any other <bean>you may find in Spring. But the big difference is the use of the factorymethod attribute. Normally Spring beans are instantiated by the Spring container—but AspectJ aspects are created by the AspectJ runtime. By the time Spring gets a chance to inject the CriticismEngine into JudgeAspect, JudgeAspect has already been instantiated.

Since Spring isn’t responsible for the creation of JudgeAspect, it isn’t possible to simply declare JudgeAspect as a bean in Spring. Instead, we need a way for Spring to get a handle to the JudgeAspect instance that has already been created by AspectJ so that we can inject it with a CriticismEngine. Conveniently, all AspectJ aspects provide a static aspectOf() method that returns the singleton instance of the aspect. So to get an instance of the aspect, you must use factory-method to invoke the aspectOf() method instead of trying to call JudgeAspect’s constructor.

In short, Spring doesn’t use the <bean> declaration from earlier to create an instance of the JudgeAspect—it has already been created by the AspectJ runtime. Instead, Spring retrieves a reference to the aspect through the aspectOf() factory method and then performs dependency injection on it as prescribed by the
<bean> element.

Summary

AOP is a powerful complement to object-oriented programming. With aspects, you can now group application behavior that was once spread throughout your applications into reusable modules. You can then declaratively or programmatically define exactly where and how this behavior is applied. This reduces code duplication and lets your classes focus on their main functionality.

Spring provides an AOP framework that lets you insert aspects around method executions. You have learned how you can weave advice before, after, and around a method invocation, as well as add custom behavior for handling exceptions.

You have several choices in how you can use aspects in your Spring applications. Wiring advice and pointcuts in Spring is much easier in Spring 2.0 with the addition of @AspectJ annotation support and a simplified configuration schema.

Finally, there are times when Spring AOP isn’t powerful enough and you must turn to AspectJ for more powerful aspects. For those situations, we looked at how to use Spring to inject dependencies into AspectJ aspects.

At this point, we’ve covered the basics of the Spring Framework. You’ve seen how to configure the Spring container and how to apply aspects to Springmanaged objects. These core Spring techniques will be foundational throughout the rest of the book. In the coming chapters, we’ll begin applying what we’ve learned as we develop enterprise capabilities into our applications. We’ll start in the next chapter by looking at how to persist and retrieve data using Spring’s JDBC and ORM abstractions.

]]> Книги по Java https://linexp.ru?id=4730 Wed, 29 Jun 2022 14:05:38 GMT <![CDATA[Глава 5 Spring in Action 2th edition]]>

Содержание

Введение

Теперь пришло время включить ваши знания об основах Spring в реальное приложение. Лучше всего начать с такого требования любого корпоративного приложения, как сохранение данных. Каждый, возможно, уже имел дело с базами данных в прошлом. При этом вы знаете, что доступ к данным имеет много "подводных камней". У нас есть framework для инициализации нашего доступа к данным, открытия соединений, обработки различных исключений и закрытия соединений. Если бы у нас этого небыло, то мы могли бы испортить или удалить важные данные. Даже, в случае если Вы не испытывли последствий неправильно доступа к данным, то поверьте на слово, это - плохая штука.

Так как мы стремимся к "хорошим штукам", мы обращаемся к Spring. У Spring есть набор модулей доступа к данным, которые интегрируются с разными популярными технологиями хранения. Если вы для сохранения данных используете напрямую JDBC, iBATIS или ORM (напрмер Hibernate), Spring избавит от написания множества рутинного кода обычно требуемого при доступе к данным. Вместо возни с низкоуровневым доступом к данным можно положиться на Spring, который выполнит эту работу за Вас, а основное свое внимание уделить управлению данными в самом приложении.

В этой главе мы собираемся построить слой persistence для приложения RoadRantz (см. рисунок 5.1). В этом слое мы сталкиваемся с некоторым выбором. Мы могли бы использовать JDBC, Hibernate, the Java Persistence API (JPA), iBATIS или любой другой из числа persistence frameworks. К счастью для нас, Spring поддерживает все эти persistence механизмы.

По мере токо, как мы строим persistence слой приложения RoadRantz, мы увидим, как Spring использует абстрактные функции доступа к данным, что упрощает сохранение кода. Вы увидите, как Spring позволяет работать с JDBC, Hibernate, JPA, и iBATIS еще проще. И прежде чем мы закончим наше обсуждение доступа к данным, мы поговорим о том, как использовать Spring поддержку декларативного кэширования чтобы увеличить производительность приложения.

Файл:Fig51.png

[Рисунок 5.1 Как и большинство приложений, RoadRantz сохраняет и восстанавливает данные из реляционных баз данных. Это persistence слой приложения, где происходит весь доступ к данным]

Вне зависимости от persistence технологии, которую вы выбрали, простую JDBC или сложную JPA, есть много точек соприкосновения между всеми frameworks доступа к данным в Spring. Поэтому, прежде чем мы перейдем к поддержке доступа к данным в Spring, давайте поговорим об основах DAO поддержки в Spring.


Source(s): Глава 5 Spring in Action 2th edition

Философия доступа к данным в Spring

Из предыдущих глав Вы знаете, что одна из целей Spring - позволить Вам разрабатывать приложения, соответствующие объектно-ориентированным (ОО) принципам программировани, основанные на интерфейсах. Поддержка доступа к данным в Spring не является исключением.
DAO обозначает "объект доступа к данным", что прекрасно описывает роль DAO в приложении. DAO существуют, чтобы обеспечить средства для чтения и записи данных в базу данных. Они должны предоставлять эту функциональность через интерфейс, который доступен остальной части приложения. Рисунок 5.2 показывает правильный подход к проектированию вашего уровня доступа к данным.

Файл:Fig52.png
Рисунок 5.2 Сервисы объектов сами не обрабатывают доступ к данным. Вместо этого они делегируют DAO доступ к данным. Интерфейс DAO делает его слабо связанными с сервисом объекта.

Как Вы можете видеть, сервисные объекты получают доступ к DAOs через интерфейсы. Это имеет несколько преимуществ. Во-первых, это делает Ваши сервисные объекты легко тестируемыми так как они не связаны с конкретной реализации доступа к данным. На самом деле, можно создать макет реализаций этих интерфейсов доступа к данным. Это позволит вам проверить свои сервисные объекты без необходимости подключения к базе данных, что значительно ускорит Ваш модуль тестов и исключит вероятность ошибки теста из-за противоречивых данных.

Кроме того, слой приложения реализуюший доступ к данным превращает persistence в "черный ящик" . То есть, подход DAO состоит в "изоляции" выбранного persistence , когда только ключевые методы доступа к данным предоставляются через интерфейс. Это делается для получения гибкого дизайна системы и позволяет заменить используемый persistence фреймворк на другой с минимальными воздействием на остальной код приложения. Если детали реализации уровня доступа к данным будут просачиваться в другие части приложения, то все приложение станет связанным с уровнем доступа к данным, что приведет к жесткой конструкции системы.


ПРИМЕЧАНИЕ.

Если после прочтения двух последних разделов, вы чувствуете, что у меня сильный уклон в сторону сокрытия слоя persistence за интерфейсами, то я счастлив, что смог сформировать у Вас эту точку зрения. Дело в том, я считаю, что интерфейсы являются ключевыми к написанию слабосвязанного кода и что они должны быть использованы во всех слоях приложения, а не только на уровне доступа к данным. Тем не менее, это также важно отметить, что в то время как Spring поощряет использование интерфейсов, Spring не требует этого - вы можете использовать Spring для соединения beans (DAO или иным образом) непосредственно в свойства другого bean без интерфейса между них.

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


Source(s): Глава 5 Spring in Action 2th edition

Знакомство с иерархией исключений доступа к данным в Spring .

Если вы когда нибудь писали код JDBC (без использования Spring), то вы вероятно хорошо знаете что в JDBC и шагу нельзя ступить без необходимости отлова java.sql.SQLException. Вот некоторые общие проблемы, которые могут вызывать SQLException:


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

Большинство SQLExceptions, которые выброшены означают фатальную ошибку. Если приложение не может подключиться к базе данных, poundtopockett.co.uk это обычно означает что приложение не может продолжать работу. Например, если ошибка в запросе, трудно что-то сделать с этим во время выполнения.
Если нет ничего, что можно сделать, чтобы избавиться от SQLException, почему мы должны перехватывать их?

Даже если у вас есть схема для решения некоторых SQLExceptions, вам придется перехватить SQLException и разбираться в его свойства для получения дополнительной информации о характере проблемы. Это потому, что SQLException рассматривается как "одна форма подходит всем" исключениям для проблем, связанных с доступом к данным. Вместо того, чтобы иметь разные типы исключений для каждой возможной проблемы, SQLException является исключением "на все случаи жизни" охватывающее большинство проблем доступа к данным.

Некоторые persistence frameworks предлагают более разнообразные иерархии исключений. Hibernate, например, предлагает около двух десятков различных исключений, каждое исключение ориентированно на конкретную проблему доступа к данным. Это делает возможным написать блоки перехвата конкретных исключений, с которые Вы хотите обрабатывать.

Но следует учесть, что Hibernate-исключения являются Hibernate-специфичными. А как было сказано выше, мы хотели бы изолировать специфику механизма сохранения в слое доступа к данным. Если бросаются конкретные Hibernate исключения, то факт того, что мы имеем дело именно с Hibernate будет просачиваться в остальные части приложения.

Таким образом, или с этим придется мириться, или Вы будете вынуждены перехватывая исключения используемого persistence провайдера, преобразовывать их в некие новые "универсальные", выбрасывая их повторно, чтобы получить желаемый эффект "черного ящика".

С одной стороны, иерархия исключений JDBC является слишком общей, для удобного анализ конкретных причин , возникающих проблем. С другой стороны, иерархия исключений в Hibernate - проприоретарна и Hibernate-специфична. Мы же нуждаемся в иерархиии исключений доступа к данным являющаейся информативной, но не связанная непосредственно с конкретным типом persistence провайдера.


Source(s): Глава 5 Spring in Action 2th edition

"Универсальная" иерархия исключений

Платформа Spring обеспечивает собственную иерархию исключений доступа к данным, позволяющую решить обе озвученные проблемы. В отличие от JDBC, Spring предоставляет несколько исключений доступа к данным, каждое из которых описывает причины того, почему они возникли. Таблица 5.1 показывает сравнение некоторых исключений доступа к данным Spring-а против исключений предлагаемых JDBC.

Как Вы можете видеть, исключения Spring описывают практически все, что может произойти во время чтения или записи данных в базу. А список исключений доступа к данным в Spring больше, нежели показано в таблице 5.1. Он не приводится целиком только потому, чтобы не вызвать, ощущение абсолютной неполноценности JDBC.

Таблица 5.1 Иерархия исключений JDBCв сравнении с исключениями доступа к данным в Spring.

Файл:Tab51b.png

Хотя иерархия исключений в Spring гораздо богаче, чем простые SQLException JDBC, она не связана с каким-либо частным решением persistence. Это означает, что Вы можете рассчитывать на то, что Spring бросает согласованный набор исключений, независимо от выбранного вами провайдера persistence. Это помогает держать Ваш выбор persistence в ограниченном слоем доступа к данным.


Source(s): Глава 5 Spring in Action 2th edition

Смотрите! Нет блоков перехвата!

Из таблицы 5.1, не очевидно что все эти исключения наследуются от DataAccessException. Но "особенным" DataAccessException делает тот факт, что оно является непроверяемым(uncheked) исключением. Другими словами, Вы не обязаны ловить каждое возникшее исключение доступа к данным используя Spring (хотя вы можете это сделать, если хотите).

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

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


Source(s): Глава 5 Spring in Action 2th edition

Шаблоны доступа к данным

Возможно, Вы когда-либо раньше путешествовали на самолете. Коль так, Вы наверняка согласитесь с тем, что одной из самых важных частей путешествия является перемещение вашего багажа из точки А в точку Б. В этом процессе довольно много шагов. Когда вы приходите к терминалу, то ваша первая остановка будет у стойки для проверки багажа. Далее, служба безопасности проверит его для обеспечения безопасности полета. Затем он принимается на "поезд для багажа" и движется к нужному самолету. Если вы используете пересадку с рейса на рейс, то и ваш багаж должен быть перемещен вслед за вами. Когда вы прибудете в пункт назначения, то багаж должен быть извлечен из самолета и размещен на транспортере. Наконец, вы спуститесь в зону получения багажа и заберете его.

Хотя существует множество шагов, выполнения этого процесса, вы активно задействованы лишь в паре из них. А перевозчик берет на себя ответственность за выполнение всех остальных необходимых действий. Лично, Вы участвуете лишь тогда, когда вам действительно нужно что-то сделать, об остальном же просто - "заботятся". Это отражает сущность мощного шаблона дизайна: Template Method.

Шаблонный метод определяет скелет процесса. В нашем примере, процесс осуществляется над багажом, при отправлении из одного города и прибытии в другой. Сам процесс - фиксирован, он никогда не меняется. Общая последовательность событий для обработки багажа происходит каждый раз одинаково: багаж принят, багаж погружен на самолет, и так далее. Некоторые этапы процесса являются фиксированными, то есть, какие-то шаги случаются тоже каждый раз. Когда самолет прибывает в пункт назначения, каждый предмет багажа выгружается и размещается на транспортере для выдачи багажа.

Однако, в определенные моменты, процесс делегирует свою работу подклассу, который заполняет некую реализацию конкретными деталями. Это переменная часть процесса. Например, обработка багажа начинается с проверки пассажирских вещей. Эта часть процесса всегда должно произойти в начале, так что его последовательность в этом процессе является фиксированной. Однако регистрация багажа для каждого пассажира - отличается, и реализация этой части процесса определяется пассажиром. В программном обеспечении условный, шаблонный метод - "делегатов" реализует интерфейс определенной части процесса. Различные реализации этого интерфейса определяют конкретные реализации этой части процесса.

Файл:Fig53.png
Рисунок 5.3 классы шаблонов DAO в Spring несут ответственность за общие задачи доступа к данным. Для реализации специфических задач, они могут могут посылать сообщения пользовательским DAO объектам обратного вызова.

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

Spring разделяет фиксированные и переменные части процесса доступа к данным на два различных класса: шаблоны и обратные вызовов. Шаблоны управляют фиксированной частью процесса, а пользовательский код доступа к данным обрабатывается в обратных вызовах. Рисунок 5.3 показывает обязанности обоих этих классов.

Как вы можете видеть на рисунке 5.3, классы Spring шаблонов управляют фиксированной частью доступа к данным - это управление транзакциями, управление ресурсами и обработку исключений. Между тем, специфика доступа к данным, относящаяся к вашему приложению - создание отчетности, связывание параметров и маршалинг набора результатов - обрабатываются в реализации обратного вызова. На практике это делается элегантным framework, поскольку вам не придется беспокоиться о вашей логики доступа к данным.

У Spring есть несколько шаблонов на выбор, в зависимости от вашего выбора persistence провайдера. Если вы используете напрямую JDBC, то вы будете использовать JdbcTemplate. Но если вы предпочитаете один из ORM провайдеров , то тогда возможно HibernateTemplate или JpaTemplate это более подходящий выбор. В таблице 5.2 перечислены все шаблоны Spring для доступа к данным и их назначение.

Table 5.2 Spring comes with several data access templates, each suitable for a different persistence
mechanism.


Класс шаблона (org.springframework.*) Назначение шаблона…
jca.cci.core.CciTemplate JCA CCI connections
jdbc.core.JdbcTemplate JDBC connections
jdbc.core.namedparam.NamedParameterJdbcTemplate JDBC connections with support for named parameters
jdbc.core.simple.SimpleJdbcTemplate JDBC connections, simplified with Java 5 constructs
orm.hibernate.HibernateTemplate Hibernate 2.x sessions
orm.hibernate3.HibernateTemplate Hibernate 3.x sessions
orm.ibatis.SqlMapClientTemplate iBATIS SqlMap clients
orm.jdo.JdoTemplate Java Data Object implementations
orm.jpa.JpaTemplate Java Persistence API entity managers
orm.toplink.TopLinkTemplate Oracle’s TopLink

Как вы увидите, для использования шаблона доступа к данным достаточно просто включить его в файл с настройками, как обычный бин в контексте Spring, а затем связать его с DAO в вашем приложении. Или же вы можете воспользоваться преимуществом поддержки DAO классов в Spring для дальнейшего упрощения конфигурации DAO в вашем приложении. Прямое связывание шаблонов это хорошо, но Spring также предоставляет набор удобных базовых классов DAO, которые могут управлять шаблоном для Вас. Давайте посмотрим, как эти основанные на шаблонах классы DAO работают.

Использование классов поддержки DAO

Файл:Fig54.png
Рисунок 5.4 Соотношения между DAO приложением, классами подержки DAO и классами шаблонов доступа в Spring.

Собственно сами шаблоны доступа это только часть модуля доступа к данным в Spring. Каждый шаблон также предоставляет удобные методы, которые упрощают доступ к данным без необходимости создания явной реализации обратного вызова. Кроме того, на вершине дизайна "шаблон-обратный вызов" , Spring предоставляет "классы поддержки" DAO, которые предназначены быть подклассами Ваших собственных классов DAO. Рисунок 5.4 иллюстрирует связь между классом шаблона , "классом поддержки" DAO и Вашей собственной реализацией DAO класса.

Позже, когда мы исследуем варианты индивидуальной поддержки доступа к данным в Spring, мы увидим, как классы поддержки DAO обеспечивают удобный доступ к шаблону класса, который они поддерживают. При написании в приложении собственной реализации DAO, Вы можете включить подклассом класса поддержки DAO, и вызвать метод поиска шаблона, чтобы иметь прямой доступ к лежащему в основе шаблону доступа. Например, если в Ваше DAO приложение включен подклассом, класс поддержки JdbcDaoSupport, то Вам нужно всего лишь вызвать метод getJdbcTemplate() для получения шаблона доступа JdbcTemplate и работать с ним.

Плюс, если вам нужен доступ непосредственно к базовой persistence платформе, каждый из классов поддержки DAO обеспечивает доступ к любому классу, который он использует, чтобы общаться с базой данных. Например, класс JdbcDaoSupport содержит метод getConnection() для работы непосредственно с JDBC подключением.

Подобно тому, как Spring предоставляет несколько реализаций классов шаблонов доступа к данным, он также обеспечивает нескольким классов поддержки DAO - по одному для каждого шаблона. Таблица 5.3 перечисляет классы поддержки DAO, которые идут вместе со Spring.

Таблица 5.3 Классы поддержки DAO Spring обеспечивают удобный доступ к своим соответствующим шаблонам доступа к данным.


DAO support class (org.springframework.*) Provides DAO support for…
jca.cci.support.CciDaoSupport JCA CCI соединения
jdbc.core.support.JdbcDaoSupport JDBC соединения
jdbc.core.namedparam.NamedParameterJdbcDaoSupport JDBC соединения с поддержкой именованных параметров
jdbc.core.simple.SimpleJdbcDaoSupport JDBC соединения с поддержкой Java 5 новшеств
orm.hibernate.support.HibernateDaoSupport Hibernate 2.x сессии
orm.hibernate3.support.HibernateDaoSupport Hibernate 3.x сессии
orm.ibatis.support.SqlMapClientDaoSupport iBATIS SqlMap клиенты
orm.jdo.support.JdoDaoSupport Java Data Object реализации
orm.jpa.support.JpaDaoSupport Java Persistence API менеджеры сущностей
orm.toplink.support.TopLinkDaoSupport Oracle’s TopLink

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

Мы начнем с основ JDBC доступа , так как это основной способ чтения и записи баз данных. Далее мы рассмотрим работу с Hibernate и с JPA , двумя самыми популярными ORM решения на базе POJO. В конце, я буду копаться в поддержке Spring для iBATIS , который является persistence провайдером, совмещающим поддержку отображения в стиле ORM с полным контролем запроса JDBC.

Но обо всем по порядку - большинство из вариантов в Spring persistence поддержки будут зависеть от источника данных. Итак, прежде чем мы сможем начать работу по созданию объектов шаблонов и DAO классов, нам предварительно нужно настроить сам Spring для работы с источником данных, чтобы обеспечить доступ DAO объектов к базе данных

Настройка источников данных

Независимо от того, какую из форм поддержки DAO в Spring Вы используете, Вам необходимо настроить ссылку на источник данных. Spring предлагает несколько вариантов настройки beans источников данных в Spring приложении, в том числе:


  • Источники данных, которые определены с помощью драйвера JDBC
  • Источники данных, найденные через JNDI
  • Источники данных, из пулов соединений с БД

Для готовых к production приложений я рекомендую использовать источник данных который берет соединения из пула соединений. Когда это возможно, я предпочитаю получить объединенный источник данных из сервера приложений через JNDI. Исходя из этого предпочтения, давайте начнем изучение с того как настроить Spring для извлечения данных из источника JNDI.

Использование JNDI источников данных

Приложения созданные в Spring будут довольно часто разворачиваться для запуска в сервере JEE приложений, таких как WebSphere, JBoss, или даже веб-контейнер Tomcat. Эти серверы позволяют настраивать источники данных для получения их через JNDI. Преимущество настройки источников данных таким образом состоит в том, что ими можно полностью управлять вне приложения, оставив приложению только запросить доступ к источнику данных, когда это потребуется будет. Более того, источники данных, управляются сервером приложений и часто кластеризованы для более высокой производительности и могут заменяться системными администраторами.

С помощью Spring Вы можете настроить ссылку на источник данных так, чтобы он хранился в JNDI и вставлять ее в классы при необходимости, так словно это еще один обычный bean Spring. JndiObjectFactoryBean позволяет получить из JNDI любой объект (включая источники данных) и сделать его доступным как bean в Spring.

Мы рассмотрим подробнее JndiObjectFactoryBean когда перейдем к главе 11. Пока же будем просто использовать JndiObjectFactoryBean, извлекая данные из источника JNDI:

<bean id="dataSource" 
class="org.springframework.jndi.JndiObjectFactoryBean"
scope="singleton">
<property name="jndiName" value="/jdbc/RantzDatasource" />
<property name="resourceRef" value="true" />
</bean>

Атрибут jndiName используется, чтобы определить имя ресурса в JNDI. Если только свойстов jndiName установлено, то источник данных будет найден в том виде как он есть. Но если приложение работает в сервере приложений Java, то Вы вероятно захотите установить свойство resourceRef в true.

Когда resourceRef равно true, то к значению jndiName будет добавляться java:comp/env/ для получения источника данных в качестве ресурса Java из JNDI каталога сервера приложений. Следовательно, фактическое имя будет java:comp/env/jdbc/RantzDatasource.

Проблемы доступа к JNDI

Использованиие JNDI источников данных имеет одну особенность, потенциально - проблемную. Она связана с тем что доступ к JDBC источникам в JNDI происходит через контекст сервера(не следует путать контекст JEE сервера ,c контекстом Spring, это - два совершенно разных понятия!). Легче всего проблема проявит себя, при попытках обратится к JNDI источнику данных, без развертывания приложения в сервере(например при unit-тестировании компонентов приложения вне сервера). Суть-же проблемы заключается в том, что хотя контекст сервера автоматически доступен любому компоненту развернутого в нем приложения, однако без принятия специальных мер, контекст будет недоступен этому же компоненту, но работающему вне - сервера. Таким образом доступ к JNDI источнику данных, извне сервера, становится - невозможным.

Наиболее простой и очевидный путь решения проблемы (в нашем случае), это явно инициализировать контекст JEE сервера, в XML-файле конфигурации Spring. Это делается путем установки значений(зависящих от конкретного JEE сервера) для свойства jndiEnvironment в бине JndiObjectFactoryBean. Свойство jndiEnvironment реализовано в виде карты типа java.util.Properties. В ней, properties-парами задаются параметры инициализации контекста для данного сервера. Простейший пример того как это делается для сервера Oracle WebLogic приведен ниже:

<bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/mySQL1" />
<property name="resourceRef" value="true" />
<property name="jndiEnvironment">
<props>
 
<!-- подключает реализицию фабрики контекста (для сервера
WebLogic, это - обязательный параметр инициализации:-->
<prop key="java.naming.factory.initial">
weblogic.jndi.WLInitialContextFactory
</prop>
 
<!--параметры JNDI провайдера (для сервера WebLogic
это - дополнительный параметр и обычно не требуется:
<prop key="java.naming.provider.url">
t3://weblogic:7001
</prop> -->
 
</props>
</property>
</bean>

В общем случае, перечень параметров, необходимых для инициализации серверного контекста, (как и их конкретные значения), различны - для различных JEE серверов, и их настроек. Информацию об этом следует искать в технической документации сервера, предварительно ознакомившись с общими принципами использования контекста JEE сервера в главе 11. К сожалению, вероятно не все сервера, позволяют Spring настроить доступ извне, к их контексту , столь простым образом. В частности документация сервера GlassFish утверждает что в нем JEE-компонеты , вообще недоступны для объектов вне сервера, при поиске их через путь java:comp/env в JNDI, со всеми вытекающими отсюда последствиями.

Источники данных JNDI в Spring 2.0(и выше)

Если вы используете Spring 2.0, XML файл необходимый для извлечения данных из источника JNDI, значительно упрощается с использованием пространства имен jee. Вы можете использовать конфигурацию элементов из пространства имен jee, описав ваш элемент как <beans> следующим образом:

<beans xmlns="https://www.springframework.org/schema/beans" 
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="https://www.springframework.org/schema/jee"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
https://www.springframework.org/schema/jee
https://www.springframework.org/schema/jee/spring-jee-2.0.xsd">

Пространство имен jee предлагает элемент <jee:jndi-lookup> для извлечения объектов из JNDI. Следующий XML файл эквивалентен точному указанию JndiObjectFactoryBean как было показано ранее:

<jee:jndi-lookup id="dataSource" 
jndi-name="/jdbc/RantzDatasource"
resource-ref="true" />

jndi-name и resource-ref это атрибуты являющиеся ссылками прямо на jndiName и resourceRef свойства в JndiObjectFactoryBean.

Использование пулов соединений

Если вам не удается получить источник данных из JNDI, следующим наилучшим выходом является настройка пулов соединений с БД непосредственно в Spring. Хотя Spring не предоставляет собственной реализации пула , есть подходящий кандидат в проекте Jakarta Commons Database Connection Pools (DBCP)(https://jakarta.apache.org/commons/dbcp). Чтобы добавить DBCP к вашему приложению, надо либо скачать его и поместить JAR файл в classpath сборщика приложения Ant или добавить следующую <dependency> в Project Object Model (POM) сборщика приложений Maven 2:

<dependency> 
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.1</version>
</dependency>

DBCP включает несколько источников данных, которые предоставляющих пулы соединений, но BasicDataSource часто используется потому, что он довольно прост в настройке в Spring и потому, что он напоминает Spring собственный DriverManagerDataSource (о котором мы будем говорить далее).

В нашем RoadRantz приложении, мы настроим bean для BasicDataSource следующим образом:

<bean id="dataSource" 
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"
value="org.hsqldb.jdbcDriver" />
<property name="url"
value="jdbc:hsqldb:hsql://localhost/roadrantz/roadrantz" />
<property name="username" value="sa" />
<property name="password" value="" />
<property name="initialSize" value="5" />
<property name="maxActive" value="10" />
</bean>

Первые четыре свойства это начальные настройки BasicDataSource. Свойство driverClassName указывает на полное имя класса драйвера JDBC. Здесь мы настроили его для использования драйвера JDBC базы данных Hypersonic. C помощью cвойства url мы установили полный JDBC URL нашей базы данных. Наконец, свойства username и password используются для аутентификации при подключении к базе данных.

Эти четыре основных свойства определяют сведения о подключении для BasicDataSource. Кроме того, некоторые свойства могут быть использованы для настройки пула данных самого источника. В таблице 5.4 перечислены некоторые из наиболее полезных свойств конфигурации пула для BasicDataSource.

Для наших целей, мы настроили пул, с начальной емкостью в пять соединений. Если будет необходимо больше одновременных соединений, BasicDataSource будет добавлять их, вплоть до установленного максимума в 10 активных соединений.

Таблица 5.4 Свойства конфигурации пула BasicDataSource.


Свойство конфигурации Назначение
initialSize Число соединений создаваемых при старте пула .
maxActive Максимальное число одновременно допустимы активных соединений. Если 0 то не ограничено.
maxIdle Максимально допустимое число простаивающих соединений которые не будут завершены . Если 0 то не ограничено.
maxOpenPreparedStatements Максимальное количество скомпилированных запросов которые могут быть выделены из пула запросов одновременно. Если 0 то не ограничено.
maxWait Время ожидания пулом возврата активного соединения в пул (при отсутствии свободных соединений) до выброса исключения. Если –1, ожидать бесконечно indefinitely.
minEvictableIdleTimeMillis Сколько времени соединение может оставаться простаивающих в пуле прежде, чем оно может быть удалено из пула.
minIdle Минимальное число простаивающих соединений, которые могут оставаться в пуле, без создания новых соединений
poolPreparedStatements Разрешен или нет пул скомпилированных запросов (boolean).

Иcпользование JDBC драйверов

Простейшие источники данных которыев Вы можете настроить в Spring, это те, что определены через драйвер JDBC. Spring предлагает на выбор два класса таких источников данных (оба в пакете org.springframework.jdbc.datasource):


  • DriverManagerDataSource - возвращает новое соединение каждый раз, когда запрашивается соединение. В отличие от DBCP в BasicDataSource, соединения предоставляемые DriverManagerDataSource каждый раз создаются заново (что сопряжено с потерями производительности)
  • SingleConnectionDataSource - возвращает одно и то же соединение каждый раз, когда соединение запрошивается. Хотя SingleConnectionDataSource и не является пулом, Вы можете думать о нем, как об источнике данных с пулом из одного соединения.

Настройка любого из этих источников данных, подобна тому, как мы настраивали в DBCP BasicDataSource:

<bean id="dataSource" 
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName"
value="org.hsqldb.jdbcDriver" />
<property name="url"
value="jdbc:hsqldb:hsql://localhost/roadrantz/roadrantz" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>

Разница лишь в том, что поскольку ни DriverManagerDataSource ни SingleConnectionDataSource не обеспечивают пулом соединений, в них нет свойств настроек конфигурации пула.

Хотя SingleConnectionDataSource и DriverManagerDataSource прекрасно подходят для небольших приложений и для процесса разработки приложений, Вы должны серьезно рассмотреть последствия использования любого из них в реальном production- приложении. Поскольку SingleConnectionDataSource имеет одно и только одно соединение для работы с базой данных, и это не очень хорошо работает в многопоточном приложении. В то же время, несмотря на то, что DriverManagerDataSource способен поддерживать несколько потоков, он каждый раз при запросе на соединение несет потери производительности на его создание . Из-за этих ограничений, я настоятельно рекомендую использовать пулы соединений в качестве источников данных.

Теперь, когда мы установили соединение с базой данных через источник данных, мы готовы для фактического доступа к базе данных. Самый элементарный способ получения доступа к базе данных , это использование JDBC. Итак, давайте начнем наше исследование абстракции доступа к данным в Spring, и посмотрим на то, как Spring упростит работу с обычным JDBC .

Использование JDBC совместно со Spring

Существует много технологий сохранения данных. Hibernate, iBATIS и JPA лишь некоторые из них. Несмотря на такое изобилие вариантов, брать и записывать Java-объекты прямо в базу данных это уже немного старомодный путь для заработка. Нет, стоп, а как же люди теперь деньги то делают ?! А, проверенным дедовским методом - сохраняя данные, старым... добрым... JDBC...

А почему собственно, нет ?! JDBC не требует знания языка запросов другого фреймворка. Он построен на вершине SQL, который является языком доступа к данным. Плюс, когда Вы используете JDBC, Вы можете куда более тонко настроить производительность доступа к данным, в сравнении с любой иной альтернативной технологией. И JDBC позволяет Вам воспользоваться всеми преимуществами фирменных особенностей Вашей базы данных, в то время когда другие провайдеры persistence могут этому препятствовать, или даже изо всех сил это запрещать.

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

Но не все так безоблачно в мире JDBC. К его мощности, гибкости, и простоте, "в нагрузку" прилагаются куда менее изящные вещи...

Борьба с безудержным JDBC кодом

Хотя JDBC предоставляет API, который работает в тесном контакте с базой данных, Вы несете ответственность за управление всем, что касается доступа к базе данных. Это включает в себя управление ресурсами базы данных и обработку исключений.

Если Вы когда-либо писали программы для JDBC, которые вставляют данные в базу данных, листинг 5.1 не должн быть слишком чужд Вам

Листинг 5.1 Использование JDBC для вставки строки в базу данных

private static final String MOTORIST_INSERT = 
"insert into motorist (id, email, password, " +
"firstName, lastName) " +
"values (null, ?,?,?,?)";
 
public void saveMotorist(Motorist motorist) {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
//Opens connection
stmt = conn.prepareStatement(MOTORIST_INSERT);
//Creates statement
stmt.setString(1, motorist.getEmail());
//Binds
stmt.setString(2, motorist.getPassword());
//parameters
stmt.setString(3, motorist.getFirstName());
//Binds
stmt.setString(4, motorist.getLastName());
//parameters
//Executes statement
stmt.execute();
} catch (SQLException e) {
//Handles exceptions—
 

//somehow
} finally {
try {
if(stmt != null) { stmt.close(); }Cleans up
if(conn != null) { conn.close(); }resources
} catch (SQLException e) {}
}
}

О Святой Безудержный Код! Более 20 строк кода, чтобы вставить простой объект в базу данных. При этом, сами выполняемые операции JDBC, из разряда: "проще, просто не бывает". Так неужели надо обязательно написать так много строчек, чтобы сделать что-то столь простое? Строго говоря, это не совсем так. Лишь несколько строчек, на самом деле, делают вставку. Но JDBC требует, чтобы Вы корректно работали с соединениями и SQL-выражениям, и как-то еще обрабатывалось SQLException, которое может быть брошено.

Теперь давайте посмотрим листинг 5.2, где мы используем напрямую JDBC для обновления строки в таблице о автолюбителях в базе данных.

Листинг 5.2 Использование JDBC для обновления строки в базе данных

private static final String MOTORIST_UPDATE = 
"update motorist " +
"set email=?, password=?, firstName=?, lastName=? " +
"where id=?";
public void updateMotorist(Motorist motorist) {
Connection conn = null;
PreparedStatement stmt = null;
//Opens connection
try {
conn = dataSource.getConnection();
//Creates statement
stmt = conn.prepareStatement(MOTORIST_UPDATE);
stmt.setString(1, motorist.getEmail());
//Binds parameters
stmt.setString(2, motorist.getPassword());
stmt.setString(3, motorist.getFirstName());
//Binds
stmt.setString(4, motorist.getLastName());
//parameters
stmt.setInt(5, motorist.getId());
//Executes statement
stmt.execute();
} catch (SQLException e) {
//Handles exceptions—
 

 
//somehow
} finally {
try {
if(stmt != null) { stmt.close(); }Cleans up
if(conn != null) { conn.close(); }resources
} catch (SQLException e) {}
}
}

На первый взгляд, листинг 5.2 может оказаться идентичным листингу 5.1. На самом деле, не считая SQL-строку и строку, в которой создается сам запрос к БД, они идентичны. Опять же, это довольно большое количество кода, чтобы сделать что-то столь простое как обновить одну строку в базе данных. Более того, здесь много повторяющегося кода. В идеале, мы бы должны были написать строчки, которые являются специфическими для конкретной задачи. В конце концов, те строчки, которые отличают листинга 5.2 от листинга 5.1. В остальном это просто шаблонный код.
Чтобы закончить наш обзор прямого использования JDBC, давайте посмотрим, как мы могли бы получить данные из базы данных. Как вы можете видеть в листинге 5.3, это тоже не слишком красиво.

Listing 5.3 Using JDBC to query a row from a database

private static final String MOTORIST_QUERY = 
"select id, email, password, firstName, lastName " +
" from motorist where id=?";
public Motorist getMotoristById(Integer id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
//Opens connection
try {
conn = dataSource.getConnection();
//Creates statement
stmt = conn.prepareStatement(MOTORIST_QUERY);
stmt.setInt(1, id);
//Binds parameters
rs = stmt.executeQuery();
//Executes query
Motorist motorist = null;
if(rs.next()) {
motorist = new Motorist();
motorist.setId(rs.getInt("id"));
//Processes
motorist.setEmail(rs.getString("email"));
//results
motorist.setPassword(rs.getString("password"));
motorist.setFirstName(rs.getString("firstName"));
motorist.setLastName(rs.getString("lastName"));
}
return motorist;
} catch (SQLException e) {
//Handles exceptions—
 

 
//somehow
 
} finally {
try {
//??
}
if(rs != null) { rs.close(); }
if(stmt != null) { stmt.close(); }
if(conn != null) { conn.close(); }
} catch (SQLException e) {}
//Cleans up resources
}
return null;

Данный пример столь же многословен, как и предыдущие примеры вставки-обновления - а то возможно и больше. Это словно принцип Парето перевернутый с ног на голову: получается что на самом деле лишь 20 процентов кода необходимо, чтобы написать строки с запросами, в то время как оставшиеся 80 процентов - просто шаблонный код.

К настоящему времени Вы уже были должны увидеть, что многое из JDBC-кода, это стандартный код для создания соединений, SQL-запросов и обработки исключений. Начиная с этого места, мы закончим наконец эту пытку и не заставим Вас дальше смотреть, на этот ужасный, противный код.

Хотя в действительности, эта "простыня" шаблонного кода имеет важное значение. Очистка ресурсов и обработки ошибок делает доступ к данным надежнее. Без этого, ошибки остаются незамеченными и ресурсы могут быть оставлены открытыми, что приводит к непредсказуемому выполнению кода и утечке ресурсов. Так что мало того, что мы нуждаемся в этом коде, мы также должны убедиться, что весь он - корректный. Это еще одна причина, чтобы позволить фремворку иметь дел с этой "простыней", так как мы знаем, что это пишется один раз и уже написано за нас и правильно.

Работа с шаблонами JDBC

JDBC модуль в Spring освободит Ваш JDBC код, от необходимости управлению ресурсами и обработке исключений. Это даст Вам свободу писать только тот код, который необходим для перемещения данных в БД и обратно.

Как мы объясняли ранее, Spring скрывает весь вспомогательный код доступа к данным за классом шаблона доступа. Для JDBC, Spring поставляет три класса шаблонов, на выбор:


  • JdbcTemplate - Самый основной шаблон JDBC в Spring, этот класс предоставляет простой доступ к базе данных через JDBC и простые индексно-параметризированные запросы.
  • NamedParameterJdbcTemplate - Этот класс JDBC шаблона позволяет выполнять запросы, где значения параметров должны быть связаны с именоваными параметрами в SQL, вместо индексированных параметров.
  • SimpleJdbcTemplate - Эта версия шаблона JDBC использует, такие новые возможности Java 5 как autoboxing, generics и varargs для упрощения работы с шаблоном.

Какой из этих шаблонов Вы выбираете, в значительной мере дело вкуса. Но в старых версииях JRE, SimpleJdbcTemplate не будет доступен, так как этот шаблон зависит от функций Java 5.

Чтобы помочь вам решить, какой из этих шаблонов JDBC будет лучше для Вас, давайте посмотрим на их один за другим, начиная с JdbcTemplate.

Использование JdbcTemplate

Все что JdbcTemplate должны делать, это выполнять свою работу реализуя интерфейс DataSource. Это делает достаточно легким в настройке бин JdbcTemplate в Spring, используя следующий XML фрагмент:

<bean id="jdbcTemplate" 
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>

Фактически DataSource, на который ссылается свойство dataSource может быть любой реализацией javax.sql.DataSource, включая те, которые мы создавали в разделе посвященном источникам данных.

Теперь мы можем внедрить JdbcTemplate в наш DAO и использовать его для доступа к базе данных. Например, предположим, что RoadRantz DAO базируется на JdbcTemplate:

public class JdbcRantDao implements RantDao { 

private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

Тогда, мы должны связать свойство jdbcTemplate бина JdbcRantDao следующим образом:

<bean id="rantDao" 
class="com.roadrantz.dao.jdbc.JdbcRantDao">
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>

С JdbcTemplate в распоряжении нашего DAO мы можем очень упростить метод saveMotorist () из Листинга 5.1. Новый метод saveMotorist() базирующийся на JdbcTemplate, приведен в Листинге 5.4.

Листинг 5.4 Основанный на JdbcTemplate метод сохранения данных saveMotorist()

private static final String MOTORIST_INSERT = 
"insert into motorist (id, email, password, " +
"firstName, lastName) " +
"values (null, ?,?,?,?)";
public void saveMotorist(Motorist motorist) {
jdbcTemplate.update(MOTORIST_INSERT,Inserts
new Object[] { motorist.getEmail(),motorist.getPassword(),motorist
motorist.getFirstName(), motorist.getLastName() }); data
}

Мы думаем, Вы согласитесь с тем, что эта версия метода saveMotorist() является значительно проще. Тут больше нет кода для создания соединений и SQL-запросов, а также нет обработки исключений. Нет ничего кроме чистого кода вставки данных.

На самом деле там присутствует весь шаблонный код. Если вы его не видите, это не значит что его там нет. Он умно спрятан внутри JdbcTemplate. Когда вызывается метод update(), JdbcTemplate получает соединение, создает SQL-запрос и выполняет его.

Так же вы не видите как обрабатываются SQLException. Внутри себя JdbcTemplate "ловит" любые выброшенные исключения. Далее он преобразует обобщенное исключение SQLException в одно из более конкретных исключений доступа к данным из таблицы 5.1 и затем сгенерирует его выброс. Так как в Spring все исключения доступа к данным наследуются от RuntimeException, то мы не обязаны их ловить в методе saveMotorist().

Читать данные также проще используя JdbcTemplate. Листинг 5.5 показывает новую версию getMotoristById() в которой используются обратные вызовы JdbcTemplate чтобы отображать возвращаемый result set в объекты предметной области.

Листинг 5.5 Запрос на выборку к motorist используя JdbcTemplate

private static final String MOTORIST_SELECT = 
"select id, email, password, firstName, lastName from motorist";
private static final String MOTORIST_BY_ID_SELECT =
MOTORIST_SELECT + " where id=?";
//Queries for
public Motorist getMotoristById(long id) {
//motorist
List matches = jdbcTemplate.query(MOTORIST_BY_ID_SELECT,
new Object[] { Long.valueOf(id) },
//Binds query
new RowMapper() {
//parameter
public Object mapRow(ResultSet rs, int rowNum)
throws SQLException, DataAccessException {
Motorist motorist = new Motorist();
motorist.setId(rs.getInt(1));
motorist.setEmail(rs.getString(2));
motorist.setPassword(rs.getString(3));
motorist.setFirstName(rs.getString(4));
motorist.setLastName(rs.getString(5));
return motorist;
}
});
//Maps query results to Motorist object
return matches.size() > 0 ? (Motorist) matches.get(0) : null;
}

This getMotoristById() method uses JdbcTemplate’s query() method to query a Motorist from the database. The query() method takes three parameters:


  • Объект String, содержащий строку SQL-запроса, который будет использоваться, чтобы выбрать данные из базы данных
  • Массив объектов Object, содержащий значения, которые будут связаны с индексируемыми параметрами запроса
  • Объект класса RowMapper, извлекающий значения из ResultSet и создающий объекты предметной области (в этом случае Motorist)

Реальное волшебство происходит в объекте RowMapper. Для каждой строки, из возвращаемых запросом, JdbcTemplate будет вызывать mapRow() метод в RowMapper. Внутри RowMapper, мы написали код, который создает объект Motorist и инициализирует его значениями полученными из ResultSet.

getMotoristById() немного более длинен чем метод saveMotorist(). Даже если так, мы все равно сосредоточилось на том, чтобы восстанавливать объект Motorist из базы данных. В отличие от традиционного JDBC, нет никакого кода управления ресурсами или обработки исключений.

Использование именованных параметров

Метод saveMotorist() в Листинге 5.4 использовал индексированные параметры. Это означало то, что мы должны были обратить внимание на порядок следования параметров в запросе, и передать их значения в таком же порядке вызывая метод update() . Если нам когда-либо потребуется изменить SQL, таким образом, изменив порядок следования параметров в тексте запроса, мы также должны привести ему в соответствие список значений передаваемых в метод.

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

private static final String MOTORIST_INSERT = 
"insert into motorist (id, email, password, " +
"firstName, lastName) " +
"values (null, :email, :password, :firstName, :lastName)";

С именованными параметрами, порядок их следования для связывания значений не важен. Мы можем связать каждое значение по его имени. При изменении в SQL запросе порядка следования параметров, Java код их подстановки изменений не потребует.

К сожалению, шаблон JdbcTemplate не поддерживает именованные параметры. Вместо этого мы должны будем использовать специальный JDBC шаблон названный - NamedParameterJdbcTemplate. Настройка его конфигурации в Spring подобна настройке JdbcTemplate:

<bean id="jdbcTemplate" 
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>

Поскольку NamedParameterJdbcTemplate - специальный шаблон JDBC и потому что он не является потомком JdbcTemplate, мы должны будем изменить свойство jdbcTemplate в нашем DAO, чтобы соответствовать новому классу шаблона:

public class JdbcRantDao implements RantDao { 

private NamedParameterJdbcTemplate jdbcTemplate;
public void setJdbcTemplate(
NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

Теперь мы готовы обновить наш метод saveMotorist() , чтобы использовать именованные параметры. Листинг 5.6 показывает новую версию saveMotorist(), использующую именованные параметры.

Листинг 5.6 Использование именованных параметров в Spring JDBC шаблонах.

 public void saveMotorist(Motorist motorist) { 
Map parameters = new HashMap();
parameters.put("email", motorist.getEmail());
parameters.put("password", motorist.getPassword());
parameters.put("firstName", motorist.getFirstName());
parameters.put("lastName", motorist.getLastName());
//Binds parameter values
jdbcTemplate.update(MOTORIST_INSERT, parameters);
//Performs
}
//insert

Первая вещь, которую Вы заметите, состоит в том, что эта версия saveMotorist() немного более длинна чем предыдущая. Это потому что именованные параметры связаны через java.util.Map. Несмотря на это, каждая строка фокусируется на главной цели - вставке объектов Motorist в базу данных. Снова отсутствует код управления ресурсами или обработки исключений, загромождающий главную цель метода.

Упрощение работы с JDBC в Java 5

Spring обеспечивает еще один специализированный шаблон JDBC, который Вы возможно захотите использовать. Если Вы бросите повторный взгляд на Листинг 5.4, то Вы увидите что параметры переданные в метод update() передавались как
массив объектов класса Object. Это - типичная стратегия
используемая, чтобы передать списки параметров с переменной длиной в метод. С Java 5 новые языковые конструкции (известных как varargs), позволяют передать списки параметров переменной длины, без необходимости создания массива Object.

Использование преимуществ Java 5 varargs означает что метод saveMotorist() может быть далее упрощен следующим образом:

public void saveMotorist(Motorist motorist) {
jdbcTemplate.update(MOTORIST_INSERT,
motorist.getEmail(), motorist.getPassword(),
motorist.getFirstName(), motorist.getLastName());
}

jdbcTemplate в этом новом методе saveMotorist() не является стандартным объектом JdbcTemplate. Вместо этого, используется специальный шаблон JDBC - SimpleJdbcTemplate, реализующий преимущества особенностей синтаксиса Java 5. Мы конфигурируем SimpleJdbcTemplate bean в Spring почти как обычный JdbcTemplate:

<bean id="jdbcTemplate" 
class="org.springframework.jdbc.core.simple.SimpleJdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>

Кроме того, мы должны будем изменить тип свойства jdbcTemplate на SimpleJdbcTemplate(вместо JdbcTemplate):

public class JdbcRantDao implements RantDao { 

private SimpleJdbcTemplate jdbcTemplate;
public void setJdbcTemplate(SimpleJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

Поскольку мощь SimpleJdbcTemplate проистекает из использования особенностей Java 5 , это все будет работать только под управлением соответствующего JRE (Java 5 или выше).

SimpleJdbcTemplate умеет больше чем просто оказывать varargs поддержку для, связывания параметров со значениями. Он также использует возможности Java 5 по поддержке autoboxing(автоупаковки типов) когда производится отображение возвращаемого запросом resultset в объект.

Повторно взгляните на метод getMotoristById() в Листинге 5.5. Две вещи Вы должны принять во внимание:


  • параметр id предварительно должен был быть преобразован в класс-обертку(java.lang.Long), чтобы быть помещенным в массив Object.
  • Типом, возвращаемым методом mapRow() объекта RowMapper, является java.lang.Object.

Это потому, что RowMapper предназначен, быть в достаточной мере абстрагированным и обобщенным, чтобы поддерживать любые типы объектов. Следствием является то, что возвращаемый результат, также должен быть самого общего типа из возможных: т.е. - Object. Теперь рассмотрим новую версию getMotoristById() в Листинге 5.7, которая использует для работы SimpleJdbcTemplate.

Листинг 5.7 Использование SimpleJdbcTemplate для извлечения объекта Motorist из базы данных.

public Motorist getMotoristById(long id) { 
List<Motorist> matches = getSimpleJdbcTemplate().query(
MOTORIST_BY_ID_SELECT,
new ParameterizedRowMapper<Motorist>() {
//Returns Motorist
 
public Motorist mapRow(ResultSet rs, int rowNum)
throws SQLException {
Motorist motorist = new Motorist();
motorist.setId(rs.getInt(1));
motorist.setEmail(rs.getString(2));
motorist.setPassword(rs.getString(3));
motorist.setFirstName(rs.getString(4));
motorist.setLastName(rs.getString(5));
return motorist;
}
},
//Shows id isn’t wrapped
id
);
return matches.size() > 0 ? matches.get(0) : null;
}

Различия между Листингом 5.5 и Листингом 5.7 довольно тонкие. Первым различием отметьте то, что мы используем ParameterizedRowMapper, чтобы превратить результаты запроса в объект. ParameterizedRowMapper использует новшество Java5 возвращаемые ковариантные типы, для возможности устанавливать конкретный тип возвращаемый методом mapRow(). Другими словами, "новый RowMapper" знает, что он имеет дело с Motorist и не просто с Object.

Другое различие в том, что параметр id больше не нужно обязательно, оборачивать в объект Long , чтобы затем помещать в массив Object . Благодаря тому, что SimpleJdbcTemplate использует новые возможности Java 5 - autoboxing и varargs , теперь происходит автоматическое преобразования параметра id в класс-обертку.

Еще одно незначительное различие - то, что порядки следования параметров в методе query() несколько отличны для шаблонов JdbcTemplate и SimpleJdbcTemplate . Поскольку теперь метод query(), способен принимать varargs параметры, то varargs-набор значений (используемых для подстановки в SQL-запрос) пришлось поместить последним в списке параметров данного метода, во избежании неоднозначностей.
Иначе, методу query() было бы "непонятно" , когда-же список varargs заканчивается, а следующие после него прочие параметры метода - начинаются (это является общим синтаксическим ограничением использования varargs в Java5).

Как Вы уже увидели к настоящему времени, процесс сочинения собственного DAO-класса на JDBC-основе, включает конфигурирование bean-a подходящего JDBC шаблона , соединение его с Вашим классом DAO, ну и затем наконец, использование этого шаблона для доступ к базе данных. В целом, этот процесс подразумевает конфигурирование по крайней мере трех bean-ов в конфигурационном XML файле Spring: источника данных, класса шаблона, и собственно Вашего DAO. Давайте теперь посмотрим как же можно не включать в XML конфигурацию один из этих bean-ов если использовать классы поддержки DAO в Spring.

Классы поддержки DAO для JDBC.

Файл:Fig55.png
Рисунок 5.5 JdbcDaoSupport в Spring определен "хранителем"(placeholder) для объекта JdbcTemplate так, чтобы его наследники не должны были бы управлять своими собственными JdbcTemplate.

Для всех основанных на JDBC DAO классов приложения, мы должны будем убедиться, что добавили свойство JdbcTemplate и его метод сеттера. И мы должны будем убедиться, что связали бин JdbcTemplate со свойством JdbcTemplate каждого DAO. Это не большое дело, если в приложении всего один DAO. Но если у Вас много объектов DAO, то потребуется много повторющегося кода.

Одним из решений проблемы было бы, создание общего родительскиого класса для всех Ваших DAO где и разместить JdbcTemplate. Тогда все Ваши DAO классы расширили бы этот класс и использовали бы JdbcTemplate родительского класса для доступа к данным. Рисунок 5.5 показывает предложенные отношения между DAO-приложением и базовым классом DAO.

Идея создания базового суперкласса DAO, который включал бы в себя JdbcTemplate - весьма хорошая идея, потому Spring уже содержит такой базовый класс "прямо из коробки". JdbcDaoSupport в Spring и есть суперкласс для того, чтобы наследовать от него, классы DAO для JDBC. Чтобы использовать JdbcDaoSupport просто напишите свой класс DAO, расшириряющий его. Например, в приложении RoadRantz класс DAO для JDBC мог бы быть написан таким образом:

public class JdbcRantDao extends JdbcDaoSupport
implements RantDao {

}

JdbcDaoSupport обеспечит удобный доступ к JdbcTemplate через метод getJdbcTemplate(). И тогда, наш saveMotorist() метод может быть переписан например вот так:

public void saveMotorist(Motorist motorist) { 
getJdbcTemplate().update(MOTORIST_INSERT,
new Object[] { motorist.getEmail(), motorist.getPassword(),
motorist.getFirstName(), motorist.getLastName() });
}

Конфигурируя Ваш класс DAO в Spring, Вы могли бы непосредственно связать бин JdbcTemplate со свойством jdbcTemplate следующим образом:

<bean id="rantDao" class="com.roadrantz.dao.jdbc.JdbcRantDao"> 
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>

Это конечно будет работать, хотя и не особо отличается от того, как мы уже конфигурировали DAO, и здесь не расширен класс JdbcDaoSupport. Альтернативно, мы можем пропустить бин-посредник и связать хранилище данных непосредственно со свойством dataSource, которое JdbcRantDao наследует от JdbcDaoSupport:

<bean id="rantDao" class="com.roadrantz.dao.jdbc.JdbcRantDao"> 
<property name="dataSource" ref="dataSource" />
</bean>

Когда у JdbcRantDao конфигурируется свойство dataSource, это неявно создает экземпляр JdbcTemplate, избавляя Вас от необходимости явно объявлять бин JdbcTemplate в конфигурации Spring.

Поддержка DAO для именованных параметров

Ранее, мы уже показали Вам некоторые вариации на тему JdbcTemplate. Одна из них - NamedParameterJdbcTemplate, предлагает возможность использовать в запросах именованные параметры вместо индексируемых . Если Вы предпочитаете применять именованные параметры в запросах, Вы можете использовать класс NamedParameterJdbcDaoSupport как родительский для Вашего DAOs.

Например, если мы хотим использовать запросы с именованными параметрами в приложении RoadRantz, мы могли написать класс JdbcRantDao, наследующий NamedParameterJdbcDaoSupport следующим образом:

public class JdbcRantDao extends NamedParameterJdbcDaoSupport
implements RantDao {

}

Так же, как в случае с JdbcDaoSupport, класс NamedParameterJdbcDaoSupport обеспечивает удобный доступ к шаблону. Однако, вместо вызова getJdbcTemplate(), мы должны будем вызвать getNamedParameterJdbcTemplate(), чтобы получить требуемый нам шаблон JDBC. Вот так бы мог выглядеть метод saveMotorist() если использовать запросы с именованными параметрами:

public void saveMotorist(Motorist motorist) { 
Map parameters = new HashMap();
parameters.put("email", motorist.getEmail());
parameters.put("password", motorist.getPassword());
parameters.put("firstName", motorist.getFirstName());
parameters.put("lastName", motorist.getLastName());
getNamedParameterJdbcTemplate().update(
MOTORIST_INSERT, parameters);
}

getNamedParameterJdbcTemplate() возвращает объект NamedParameterJdbcDaoSupport , что мы используем, чтобы выполнить обновление. Так же, когда мы использовали NamedParameterJdbcDaoSupport, значения параметров передаваемые в запрос помещаются в карту java.util.Map.

Упрощение поддержки DAO для JDBC в Java 5

Также, ранее, мы показывали Вам, как использовать JDBC шаблон работающий в стиле Java 5, и называемый - SimpleJdbcTemplate. Если Вы хотели бы использовать такие преимущества Java 5 как varargs и autoboxing в Ваших DAO, тогда Вы возможно решите сделать свой класс DAO наследуемым от SimpleJdbcDaoSupport:

public class JdbcRantDao extends SimpleJdbcDaoSupport
implements RantDao {

}

Получить доступ к SimpleJdbcTemplate, который содержится в внутри SimpleJdbcDaoSupport, можно просто вызвав его метод getSimpleJdbcTemplate(). Вот версия saveMotorist(), обновленная, чтобы использовать SimpleJdbcTemplate:

public void saveMotorist(Motorist motorist) {
getSimpleJdbcTemplate().update(MOTORIST_INSERT,
motorist.getEmail(), motorist.getPassword(),
motorist.getFirstName(), motorist.getLastName());
}

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

Даже при том, что Spring снимает большую часть головной боли при работе с JDBC, с ростом размеров приложения этот путь все равно может стать неоправданно сложным. Чтобы разрешить проблемы доступа к большим объемам данных, Вы
возможно захотите изучить "настоящий" persistence фреймворк, такой как Hibernate.

Интеграция Hibernate и Spring.

Когда мы были детьми, езда на велосипеде была забавой, не так ли? Мы поехали бы в школу с утра. Когда уроки окончились, мы отправились бы в дом нашего лучшего друга. Когда же становилось поздно, пока наши родители вопили на нас за то, что мы шляемся дотемна, мы занимались всякой ерундой катаясь по темноте. Ну и дела, были забавой, в те дни ...

Когда мы выросли, нам потребовалось нечто побольше чем велосипед. Иногда у нас довольно приличное расстояние от дома до работы. Бакалея должна быть доставлена, а нашим детям нужно на тренировки по футболу. А если Вы еще и живете в Техасе, то без кондиционера - это уже не жизнь... Короче, наши потребности просто переросли наш велосипед.

JDBC - это велосипед в мире технологий хранения данных. Он чудесен для то чего придуман, и с некоторыми видами работы справляется просто великолепно. Но поскольку наши приложения становятся более сложными, давайте сформулируем наши требования к persistence. Итак, мы должны быть в состоянии отображать свойства объекта столбцы базы данных, неплохо было бы иметь наши SQL-выражения и запросы, созданными за нас, освобождая себя от печатания бесконечных строк из вопросительных знаков. Мы также нуждаемся в некоторых "фичах", которые являются более сложными:


  • Lazy loading("ленивая загрузка")—Поскольку наши графы объектов становятся более сложными, мы иногда не хотим извлекать все дерево соотношений немедленно. Чтобы использовать типичный пример, предположим, что мы выбираем коллекцию объектов PurchaseOrder, и каждый из этих объектов в свою очередь содержит коллекцию объектов LineItem. Если мы интересуемся только признаками PurchaseOrder, не имеет никакого смысла захватывать данные из LineItem. Это весьма накладно. Lazy loading позволяет захватывать только те данные, которые действительно необходимы.
  • Eager fetching("энергичная загрузка") - понятие противоположное lazy loading. Eager fetching позволяет Вам захватить весь граф объектов в рамках одного запроса. В случаях, где мы точно знаем, что мы нуждаемся и в объектах PurchaseOrder и в связанных с ними объектах LineItems, eager fetching позволяет нам получать их базы данных за одну операцию, избавляя от накладных повторных обращений к БД.
  • Cascading("каскадирование") - Иногда изменения таблицы базы данных, также должны привести к изменениям в других связанных таблицах. Возвращаясь к нашему примеру заказа на поставку: когда объект заказа удален, мы также обычно хотим удалить связанные с ним объекты LineItems из базы данных.

Некоторые фреймворки способны предоставить подобную функциональность. Общее название для такой функциональности - object-relational mapping (ORM). Использование инструмента ORM для Вашего persistence-слоя может буквально спасти Вас, от тысяч строк лишнего кода и множества часов потраченных на его разработку. Это позволит Вам сместить фокус разработки с написания обильного и чреватого ошибками SQL-кода, на требования реализации самого приложения.

Spring оказывает поддержку для нескольких провайдеров ORM, включая Hibernateт, iBATIS, Apache OJB, Java Data Objects (JDO), Oracle TopLink, и Java Persistence API (JPA).

Как и в случае с поддержкой JDBC , Spring обеспечивает поддержку ORM-провайдеров предоставляя им точки интеграции для фреймворков, а также некоторые дополнительные сервисы:


  • Интегрированную поддержку Spring для декларативных транзакций.
  • Прозрачную обработку исключений.
  • Потокобезопасные легковесные классы шаблонов
  • Классы поддержки DAO
  • Управление ресурсами

У нас просто нет достаточного количества места в этой главе, чтобы рассмотреть все ORM-провайдеры , которые поддержаны в Spring. Это даже хорошо, потому что поддержка в Spring различных вариантов реализации ORM довольно похожа. Как только Вы приобретаете навык использования
любого из ORM в Spring , Вы сможете легко переключиться на другой.

Давайте начнем изучение, глядя на то, как Spring интегрируется с тем, что, возможно является, самым популярным в использовании вариантом ORM — Hibernate . Позже в этой главе, мы также увидим , как Spring интегрируется с JPA и с iBATIS.

Hibernate это open source ORM-фреймворк, который получил существенную популярность в сообществе разработчиков. Он обеспечивает не только базовые возможности ORM, но также и все прочие "фичи" которые принято ожидать от полнофункционального инструмента ORM, а именно: lazy loading, еager fetching, кэширование и даже
распределенное кэширование.

Выбор версии Hibernate

На момент написания книги, последней доступной версией была - Hibernate 3.2. Но не так давно, последней доступной версией была Hibernate 2.x, и Вы возможно все еще сталкиваетесь с некоторыми приложениями, которые пока не используют Hibernate 3. Выбор между Hibernate 2 и Hibernate 3, является существенным потому что Hibernate API весьма различается между этими двумя версиями.

В то время как много функциональных усовершенствований и особенностей были введены в Hibernate 3, одно тонкое изменение усложнило его Spring интеграцию. Версия 2 Hibernate API имела структур пакета net.sf.hibernate , но в версии 3, его структура была изменена на org.hibernate. Перед командой разработчиков Spring это изменение поставило дилемму. Поскольку классы интеграции Spring-Hibernate, должны были импортировать классы или из net.sf.hibernate или из org.hibernate, пришлось выбирать между:


а) Прекратить поддержку для Hibernate 2, и поддерживать в дальнейшем только Hibernate 3
б) Разделить код Hibernate поддержки в Spring на две ветви — одну для Hibernate 2, другую для Hibernate 3

Признавая значение обратной совместимости, команда Spring решила выбрать второй вариант. Поддержка Hibernate 2 находится в дистрибутиве Spring в пакете org.springframework.orm.hibernate, а поддежка Hibernate 3 в пакете org.springframework.orm.hibernate3.

По большей части, классы в пакете для Hibernate 3 зеркально отражают соответствующие классы из Hibernate 2 пакета. Для Hibernate 3 в Spring также включена поддержка связывания полей БД через аннотации.

Всегда, когда это возможно старайтесь использовать Hibernate 3. Примеры в данной главе отражают этот выбор. Но если Ваши обстоятельство не предоставляют Вам, роскошь Hibernate 3, Вы обнаружите , что поддержка Hibernate 2 в Spring мало чем отличается от поддержки Hibernate 3, кроме имени пакета и невозможности использования в Hibernate 2 аннотаций при связывании полей БД.

Независимо от выбранной версии Hibernate , первый шаг которой Вам придется сделать - это сконфигурировать бин фабрики Hibernate-сессий в Spring.

Использование Hibernate шаблонов

Главный интерфейс для того, чтобы взаимодействовать с Hibernate - org.hibernate.Session. Интерфейс Session обеспечивает базовую функциональность доступа к данным, позволяя сохранять, обновлять, удалять, и загрузжать объекты в/из базы данных. Это через Hibernate интерфейс Session , объекты DAO в приложения будут удовлетворять все свои потребности в услугах persistence.

Стандартный способ получить ссылку на Hibernate объект Session - через реализацию SessionFactory интерфейса в Hibernate. Среди прочих вещей, SessionFactory ответственен за открытие, закрытие, и управление Hibernate Session.

Почти как JdbcTemplate-абстракция, прогнавшая от нас скуку работы с JDBC, Spring шаблон HibernateTemplate обеспечивает абстрактный слой над Hibernate Session. Главная ответственность HibernateTemplate состоит в том, чтобы упростить работу открытия, и закрытие Hibernate Session и преобразовывать собственные исключения Hibernate к одному из универсальных непроверяемых исключений Spring, перечисленных в таблице 5.1.

Следующий XML показывает, как конфигурировать HibernateTemplate в Spring:

<bean id="hibernateTemplate" 
class="org.springframework.orm.hibernate3.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

Свойству sessionFactory присваивается ссылка на используемую реализацию org.hibernate.SessionFactory. Здесь у Вас есть несколько вариантов, в зависимости от как Вы используете, Hibernate, чтобы отображать Ваши объекты в таблицы базы данных.

Использование классических маппинг-файлов Hibernate.

Файл:Fig56.png
Рисунок 5.6 LocalSessionFactoryBean является фабричным бином Spring, загружающим один или несколько маппинг-файлов XML конфигурации Hibernate , чтобы произвести Hibernate SessionFactory.

Если Вы будете использовать маппинг-файл классической XML-конфигурации Hibernate для связывания полей БД со свойствами объетов, то Вы захотите использовать Spring LocalSessionFactoryBean. Как показано в рисунке 5.6, LocalSessionFactoryBean это фабричный Spring бин , производящий локальный экземпляр Hibernate SessionFactory, который вытягивает свои метаданные о маппинге из одного или более XML маппинг-файлов.


Возможно класс LocalSessionFactoryBean методологически правильнее было бы назвать LocalSessionFactoryFactoryBean. Но для избежания тавтологии, вспомогательная "Фабрика" была опущена.


Следующий фрагмент XML показывает, как конфигурировать LocalSessionFactoryBean, который загружает маппинг-файлы файлы для RoadRantz доменных объектов:

<bean id="sessionFactory" 
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingResources">
<list>
<value>com/roadrantz/domain/Rant.hbm.xml</value>
<value>com/roadrantz/domain/Motorist.hbm.xml</value>
<value>com/roadrantz/domain/Vehicle.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
</props>
</property>
</bean>

Свойство dataSource ссылается на любую реализацию интерфейса javax.sql.DataSource, включая уже нами рассмотренные. Свойство mappingResources принимает список одного или более путей к маппинг-файлам, как ресурсы в classpath. Здесь мы определили три маппинг-файла, которые описывают persistence из приложения RoadRantz. Наконец, свойство hibernateProperties позволяет нам обеспечить любую дополнительную конфигурацию, подходящую для Hibernate сессии. Как минимум, мы должны определить Hibernate диалект (то есть указать Hibernate , как нужно формировать SQL-запросы к конкретной используемой базе данных). Здесь мы оставили решение диалекта как placeholder переменную, которая будет заменена PropertyPlaceholderConfigurer (см. подробное описание в Главе 3).

Работа с аннотированными доменными объектами.

Файл:Fig57.png
Рисунок 5.7 AnnotationSessionFactoryBean Spring производит Hibernate SessionFactory, читая аннотации в одном или нескольких доменных классах.

Ориентируясь на Java 5+, Вы можете выбрать использование аннотаций для связывания доменных объектов с persistence метаданными. Hibernate 3 поддерживает как JPA-аннотации, так и Hibernate-аннотации , для описания объектов подлежащих сохранению. При использовании аннотаций в Hibernate, AnnotationSessionFactoryBean в Spring работает почти так же, как LocalSessionFactoryBean, за исключением того, что он создает SessionFactory на основе аннотаций в одном или нескольких доменных классах (как показано на рисунке 5.7). Код XML, необходимый для конфигурирования AnnotationSessionFactoryBean Spring похож на XML для LocalSessionFactoryBean:

<bean id="sessionFactory" 
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="annotatedClasses">
<list>
<value>com.roadrantz.domain.Rant</value>
<value>com.roadrantz.domain.Motorist</value>
<value>com.roadrantz.domain.Vehicle</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
</props>
</property>
</bean>

Свойства dataSource и hibernateProperties служат одной цели как в случае с AnnotationSessionFactoryBean так и с LocalSessionFactoryBean. Однако, вместо настройки одного или нескольких маппинг-файлов мы должны будем настроить AnnotationSessionFactoryBean с одним или более классами, аннотированными для persistence в Hibernate. Здесь мы привели список доменных объектов в приложении RoadRantz.

Доступ к данным через Hibernate-шаблоны

Имея бин HibernateTemplate объявленым и связанным(wire) с фабрикой сессий, мы готовы начать использовать его для сохранения и извлечения объектов в базе данных. Листинг 5.8 показывает, часть HibernateRantDao, который вводится(inject) в HibernateTemplate.

Listing 5.8 Creating a HibernateTemplate-based DAO

public class HibernateRantDao implements RantDao { 
public HibernateRantDao() {}

private HibernateTemplate hibernateTemplate;
public void setHibernateTemplate(HibernateTemplate template) {
this.hibernateTemplate = template;
} //Injects HibernateTemplate
}

HibernateRantDao принимает ссылку HibernateTemplate через setter injection("инъекцию сеттера") , поэтому нам нужно настроить его в Spring следующим образом:

<bean id="rantDao" class="com.roadrantz.dao.hibernate.HibernateRantDao"> 
<property name="hibernateTemplate" ref="hibernateTemplate" />
</bean>

Так как мы конкретизируем методы в HibernateRantDao, мы можем использовать введенный HibernateTemplate для доступа к объектам, хранящимся в базе данных. Например, вот saveVehicle() метод, используемый для сохранения объекта Vehicle в базу данных:

public void saveVehicle(Vehicle vehicle) { 
hibernateTemplate.saveOrUpdate(vehicle);
}

Здесь мы используем метод saveOrUpdate() класса HibernateTemplate для сохранения Vehicle. SaveOrUpdate() проверяет объект, чтобы определить, является ли его поле ID - пустым(null). Если - null, то значит это - новый объект и, следовательно, он вставляется в базу данных. Если - не null, то предполагается, что это уже существующий объект и его данные обновляются.

Вот метод findVehiclesByPlate() , который использует метод find() класса HibernateTemplate для получения Vehicle путем запроса по названию штата и номерному знаку автомобиля:

public Vehicle findVehicleByPlate(String state, 
String plateNumber) {
List results = hibernateTemplate.find("from " + VEHICLE +
" where state = ? and plateNumber = ?",
new Object[] {state, plateNumber});
return results.size() > 0 ? (Vehicle) results.get(0) : null;
}

А вот как Вы можете использовать метод load() в HibernateTemplate для загрузки конкретного экземпляра Motorist по значению его ID поля:

public Motorist getMotoristById(Integer id) { 
return (Motorist) hibernateTemplate.load(Motorist.class, id);
}
Файл:Fig58.png
Рисунок 5.8 HibernateDaoSupport является удобным суперклассом для DAO основанных на Hibernate , который обеспечивает своих потомков шаблоном HibernateTemplate созданным из веденного(injected) SessionFactory.

Это примеры только трех из возможных способов, использования HibernateTemplate. Шаблон HibernateTemplate предлагает несколько десятков методов , которые помогут вам запрашивать и сохранять объекты через Hibernate. Если Вы уже знакомы с persistence методами предоставляемыми через интерфейс Session в Hibernate, Вы будете рады найти большинство из этих методов, доступными в HibernateTemplate.

В Листинге 5,8 мы явно вводим(inject) HibernateTemplate в HibernateRantDao. Это хорошо для ряда случаев, но Spring также предлагает и класс DAO-поддержки для Hibernate, который обеспечивает Вас шаблоном HibernateTemplate без его явного связывания. Давайте переделаем HibernateRantDao чтобы воспользоваться DAO-поддержкой Spring для Hibernate.

Создание DAO, поддерживаемых Hibernate.

Пока конфигурация HibernateRantDao включает в себя четыре бина. Источник данных привязан(wire) к бину фабрики сессий (LocalSessionFactoryBean или AnnotationSessionFactoryBean). Бин фабрики сессий привязан(wire) к HibernateTemplate. Наконец, HibernateTemplate привязан(wire) к HibernateRantDao, где он используется для доступа к базе данных.

Чтобы упростить все немного, Spring предлагает - HibernateDaoSupport. Это удобный класса поддержки DAO , который позволяет Вам привязать(wire) бин фабрики сессий непосредственно к классу DAO. Внутри себя, HibernateDaoSupport создает HibernateTemplate, который наш DAO можно использовать, как показано в UML на рисунке 5,8 .

Давайте переработаем HibernateRantDao для использования HibernateDaoSupport. Первый шаг изменений - сделать класс HibernateRantDao наследуемым от HibernateDaoSupport:

public class HibernateRantDao extends HibernateDaoSupport implements RantDao { 

}

HibernateRantDao больше не нуждается в свойстве HibernateTemplate как это было в листинге 5.8. Вместо этого, Вы можете использовать метод getHibernateTemplate() , чтобы получить HibernateTemplate, который HibernateDaoSupport создает для Вас. Итак, следующий шаг заключается в изменении всех методов доступа к данным в HibernateRantDao, чтобы использовать getHibernateTemplate(). Например, вот метод saveMotorist() обновленый для варианта HibernateRantDao основанного на HibernateDaoSupport :

public void saveMotorist(Motorist motorist) {
getHibernateTemplate().saveOrUpdate(motorist);
}

Последнее, что осталось сделать, это перемонтировать HibernateRantDao в файле конфигурации Spring. С тех пор как HibernateRantDao больше не нуждается в ссылке на HibernateTemplate , мы должны будем удалить hibernateTemplate бин. Вместо этого HibernateTemplate, новый родитель нашего HibernateRantDao - HibernateDaoSupport, нуждается в Hibernate SessionFactory, чтобы он мог производить HibernateTemplate внутри себя. Таким образом, мы связываем(wire) sessionFactory бин со свойством sessionFactory в HibernateRantDao:

<bean id="rantDao" class="com.roadrantz.dao.hibernate.HibernateRantDao">
<property name="sessionFactory" ref="sessionFactory" />
</bean>


На данный момент, мы создали DAO на Hibernate основе, для приложения RoadRantz, и связали его в конфигурацию Spring. Исключая транзакции (о которых мы поговорим в следующей главе), Вы теперь знаете почти все, что нужно знать об использовании Hibernate со Spring.

Файл:Fig59.png
Рисунок 5.9 Используя контекстные сессии Hibernate 3, мы можем связывать(wire) SessionFactory (произведенную бином фабрики сессий) непосредственно с нашим DAO, таким образом, развязывая(decoupling) класс DAO со Spring API.

Но обратите внимание, что HibernateRantDao расширяет Spring-специфический класс. Это может быть и не проблемою, лично, для - Вас, но найдутся в мире люди, которые будут рассматривать это, как вторжение Spring, в код их приложения. По этой причине, давайте взглянем как воспользоваться поддержкой контекстных сессий в Hibernate 3, чтобы удалить Spring-специфические зависимости из кода Вашего DAO.

Использование контекстных сессий Hibernate 3

Одной из обязанностей HibernateTemplate является управление Hibernate объектами Session. Это включает в себя открытие и закрытие сессий, а также обеспечения одной сессии за одну транзакцию. Без HibernateTemplate, Вам бы ничего не оставалось, как загромождать ваши DAO, множеством шаблонным кодом управления сессиями.

Оборотная сторона HibernateTemplate в том, что он несколько навязчив. Когда мы используем HibernateTemplate в нашей DAO (напрямую или через HibernateDaoSupport), класс HibernateRantDao связан со Spring API. Хотя это может не вызывать особого беспокойство у одних разработчиков, другие могут найти вторжение Spring в свой код DAO нежелательным.

Однако, существует еще один вариант... Контекстные сессии, введенные в Hibernate 3, то это новый механизм, с помощью которого Hibernate сам управляет одним объектом Session за одну транзакцию. При этом нет необходимости в HibernateTemplate для обеспечения такого поведения. Таким образом, вместо связывания(wire) HibernateTemplate в ваши DAO, вы можете связать(wire) Hibernate SessionFactory вместо этого, как показано на рисунке 5.9.

Для иллюстрации рассмотрим эту новую "без-Spring" версию HibernateRantDao:

 public class HibernateRantDao implements RantDao { 

private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}

В этой новой HibernateRantDao, ссылка SessionFactory внедряется(inject) в свойство sessionFactory. После того, как SessionFactory происходит из Hibernate API, HibernateRantDao больше не зависит от Spring Framework. Вместо того чтобы использовать HibernateTemplate для выполнения persistence операций , вы сейчас запросите SessionFactory для текущей сессии.

Вот, например saveRant() метод обновленный для использования в контекстных сессиях Hibernate 3

public void saveRant(Rant rant) { 
sessionFactory.getCurrentSession().saveOrUpdate(rant);
}

Когда дело доходит до конфигурирования HibernateRantDao в Spring, это ничем не отличается от того, как мы настроили его для HibernateDaoSupport-версии HibernateRantDao. Оба HibernateDaoSupport (в том числе наша новая "чисто-Hibernate" версия HibernateRantDao) требуют Hibernate SessionFactory для связывания(wire) его со своим свойством sessionFactory. В любом случае, SessionFactory бин (являющийся SessionFactory-производителем как LocalSessionFactoryBean или AnnotationSessionFactoryBean) подходит:

<bean id="rantDao" class="com.roadrantz.dao.hibernate.HibernateRantDao">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

Теперь у нас есть два варианта создания DAO на основе Hibernate, в нашем Spring приложении: HibernateTemplate или контекстная сессия. Какой мы выбираем? Следующие вещи, нужно учесть при принятии решения о выборе:


  • Конечно, если вы используете Hibernate 2, то у вас нет другого выбора, кроме как использовать HibernateTemplate.
  • Главным преимуществом контекстных сессий Hibernate является снижение связности(decouple) ваших DAO-реализаций и Spring.
  • Основным недостатком контекстных сессий является то, что они бросают Hibernate-специфичные исключения. Хотя HibernateException и являются RuntimeException (что не обязвывает нас их обрабатывать и упрощает код) их иерархия специфична для Hibernate. Это может затруднить переход на другое решение ORM.

Несмотря на несколько попыток, придумать стандартный persistence framework, включая EJB-компоненты управления данными(EJB entity beans) и Java Data Objects (JDO), Hibernate занял положение persistence стандарта де-факто в сообществе Java. Даже с беспрецедентной популярностью, Hibernate, история может показатьто, что это в конечном счете лишь стадия для истинного стандарта persistence: Java Persistence API (JPA).

Хорошей новостью является то, что ORM абстракции Spring API, не ограничиваются Hibernate. Spring также обеспечивает API-абстракцию для JPA, похожим с Hibernate образом. Наш обзор интеграции Spring с persistence-провайдерами продолжается в следующем разделе с обсуждения того, как использовать в наших целях Java Persistence API

Интеграция Spring с Java Persistence API

Файл:Fig510.png
Figure 5.10 Spring’s JpaTemplate templates JPA data access, ensuring that EntityManagers are opened and closed as necessary, handling exceptions, and involving EntityManagers in Spring transactions.

From its beginning, the EJB specification has included the concept of entity
beans. In EJB, entity beans are a type of EJB that describes business objects that are
persisted in a relational database. Entity beans have undergone several tweaks
over the years, including bean-managed persistence (BMP) entity beans and container-
managed persistence (CMP) entity beans.

Entity beans both enjoyed the rise and suffered the fall of EJB’s popularity. In
recent years, developers have traded in their heavyweight EJBs for simpler POJObased
development. This presented a challenge to the Java Community Process to
shape the new EJB specification around POJOs. The result is JSR-220—also known
as EJB 3.

The portion of the EJB 3 specification that replaces entity beans is known as
the Java Persistence API (JPA). JPA is a POJO-based persistence mechanism that
draws ideas from both Hibernate and Java Data Objects (JDO) and mixes Java 5
annotations in for good measure.

With the Spring 2.0 release came the premiere of Spring integration with JPA.
The irony is that many blame (or credit) Spring with the demise of EJB. But now
that Spring provides support for JPA, many developers are recommending JPA for
persistence in Spring-based applications. In fact, some say that Spring-JPA is the
dream team for POJO development.

Spring’s JPA support mirrors the template-based support Spring provides for

the other persistence frameworks. Therefore, let’s get started with Spring and JPA

by looking at Spring’s JpaTemplate.

5.5.1 Using JPA templates

Keeping consistent with Spring’s support for other persistence solutions, the central
element of Spring-JPA integration is a template class. JpaTemplate, specifically,
is a template class that wraps a JPA EntityManager. The following XML
configures a JPA template in Spring:

<bean id="jpaTemplate" 
class="org.springframework.orm.jpa.JpaTemplate">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>

The entityManagerFactory property of JpaTemplate must be wired with an implementation of JPA’s javax.persistence.EntityManagerFactory interface, as shown in figure 5.10. JpaTemplate will use the EntityManagerFactory to produce EntityManagers as needed. I’ll show you where the entityManagerFactory bean comes from in section 5.5.2.

Similar to Spring’s other persistence templates, JpaTemplate exposes many of the same data access methods provided by a native JPA EntityManager. But unlike a plain JPA, JpaTemplate ensures that EntityManagers are opened and closed as necessary, involves the EntityManagers in transactions, and handles exceptions.

To write a JpaTemplate-based DAO, add a JpaTemplate property to the DAO and provide a setter for injection. Here’s an excerpt from JpaRantDao showing the JpaTemplate property:

public class JpaRantDao implements RantDao { 
public JpaRantDao() {}

// injected
private JpaTemplate jpaTemplate;
public void setJpaTemplate(JpaTemplate jpaTemplate) {
this.jpaTemplate = jpaTemplate;
}
}

When configuring JpaRantDao in Spring, we simply wire the JpaTemplate into
the jpaTemplate property:

<bean id="rantDao" 
class="com.roadrantz.dao.jpa.JpaRantDao">
<property name="jpaTemplate" ref="jpaTemplate" />
</bean>

With the JpaTemplate injected into the DAO, we’re now ready to use the template
to access persisted objects.

Accessing data through the JPA template

As we mentioned, JpaTemplate provides many of the same persistence methods that are provided by JPA’s EntityManager. This should make working with JpaTemplate second nature if you’re already familiar with JPA. For example, the following implementation of the saveMotorist() method uses JpaTemplate’s persist() method to save a Motorist object to the database:

public void saveMotorist(Motorist motorist) { 
jpaTemplate.persist(motorist);
}

In addition to the standard set of methods provided by EntityManager, JpaTemplate also provides some convenience methods for data access. For example, consider the following getRantsForDay() method that uses a native JPA EntityManager to find all of the Rant objects that were entered on a given day:

public List<Rant> getRantsForDay(Date day) { 
Query query = entityManager.createQuery(
"select r from Rant r where r.date=?1");
query.setParameter(1, day);
return query.getResultList();
}

The first thing getRantsForDay() has to do is create a Query object. Then it sets the query parameters. In this case, there’s only one query parameter, but you can imagine that a much more interesting example would involve one call to setParameter() for each parameter. Finally, the query is executed to retrieve the results. Contrast that method with the following implementation of getRantsForDay():

public List<Rant> getRantsForDay(Date day) { 
return jpaTemplate.find(
"select r from Rant r where r.date=?1", day);
}

In this version, getRantsForDay() takes advantage of a convenient find() method offered by JpaTemplate. EntityManager doesn’t have such a simple find() method that takes a query and one or more parameters. Under the covers, JpaTemplate’s find() method creates and executes the Query for you, saving you a couple of lines of code.

The one unanswered question is where we get the entityManagerFactorybean that we wired into the JpaTemplate. Before we see what else Spring has to offer with regard to JPA integration, let’s configure the entityManagerFactory bean.

5.5.2 Configuring an entity manager factory

In a nutshell, JPA-based applications use an implementation of EntityManagerFactory to get an instance of an EntityManager. The JPA specification defines two kinds of entity managers:


  • Application-managed—entity managers are created when an application directly requests an entity manager from an entity manager factory. With application-managed entity managers, the application is responsible for opening or closing entity managers and involving the entity manager in transactions. This type of entity manager is most appropriate for use in stand-alone applications that do not run within a Java EE container.
  • Container-managed—entity managers are created and managed by a Java EE container. The application does not interact with the entity manager factory at all. Instead, entity managers are obtained directly through injection or from JNDI. The container is responsible for configuring the entity manager factories. This type of entity manager is most appropriate for use by a Java EE container that wants to maintain some control over JPA configuration beyond what is specified in persistence.xml. Both kinds of entity manager implement the same EntityManager interface. The key difference is not in the EntityManager itself, but rather in how the EntityManager is created and managed. Application-managed EntityManagers are created by an EntityManagerFactory obtained by calling the createEntityManagerFactory() method of the PersistenceProvider. Meanwhile, container-managed EntityManagerFactorys are obtained through PeristenceProvider’s createContainerEntityManagerFactory() method.

So what does this all mean for Spring developers wanting to use JPA? Actually,not much. Regardless of which variety of EntityManagerFactory you want to use, Spring will take responsibility for managing EntityManagers for you. If using an application-managed entity manager, Spring plays the role of an application and transparently deals with the EntityManager on your behalf. In the container-managed scenario, Spring plays the role of the container. Each flavor of entity manager factory is produced by a corresponding Spring
factory bean:


  • LocalEntityManagerFactoryBean produces an application-managed EntityManagerFactory.
  • LocalContainerEntityManagerFactoryBean produces a container-managed EntityManagerFactory.

It’s important to point out that the choice made between an application-managed EntityManagerFactory and a container-managed EntityManagerFactory is completely transparent to a Spring-based application. Spring’s JpaTemplate hides the intricate details of dealing with either form of EntityManagerFactory, leaving your data access code to focus on its true purpose: data access.

The only real difference between application-managed and container-managed entity manager factories, as far as Spring is concerned, is how each is configured within the Spring application context. Let’s start by looking at how to configure the application-managed LocalEntityManagerFactoryBean in Spring. Then we’ll see how to configure a container-managed LocalContainerEntityManagerFactoryBean.

Configuring application-managed JPA

Application-managed entity manager factories derive most of their configuration information from a configuration file called ersistence.xml. This file must appear in the META-INF directory within the classpath.

The purpose of the persistence.xml file is to define one or more persistence units. A persistence unit is a grouping of one or more persistent classes that correspond to a single data source. In simple terms, persistence.xml enumerates one or more persistent classes along with any additional configuration such as data sources and XML-based mapping files. Here’s a typical example of a persistence.xml file as it pertains to the RoadRantz application:

<persistence xmlns="https://java.sun.com/xml/ns/persistence" 
version="1.0">
<persistence-unit name="rantzPU">
<class>com.roadrantz.domain.Motorist</class>
<class>com.roadrantz.domain.Rant</class>
<class>com.roadrantz.domain.Vehicle</class>
<properties>
<property name="toplink.jdbc.driver"
value="org.hsqldb.jdbcDriver" />
<property name="toplink.jdbc.url"
value="jdbc:hsqldb:hsql://localhost/roadrantz/roadrantz" />
<property name="toplink.jdbc.user"
value="sa" />
<property name="toplink.jdbc.password"
value="" />
</properties>
</persistence-unit>
</persistence>

Because so much configuration goes into a persistence.xml file, there’s very little configuration that’s required (or even possible) in Spring. The <bean> in listing 5.9 declares a LocalEntityManagerFactoryBean in Spring.

Listing 5.9 Configuring an application-managed EntityManagerFactory factory bean

<bean id="entityManagerFactory" 
class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<!--Selects persistence unit -->
<property name="persistenceUnitName" value="rantzPU" />
</bean>

The value given to the persistenceUnitName property refers to the persistence unit name as it appears in persistence.xml.

The reason why much of what goes into creating an application-managed EntityManagerFactory is contained in persistence.xml has everything to do with what it means to be application managed. In the application-managed scenario (not involving Spring), an application is entirely responsible for obtaining an EntityManagerFactory through the JPA implementation’s PersistenceProvider. The application code would become incredibly bloated if it had to define the persistence unit every time it requested an EntityManagerFactory. By specifying it in persistence.xml, JPA can look in this well-known location for persistence unit definitions.

But with Spring’s support for JPA, the JpaTemplate will be the one that interacts with the PersistenceProvider—not our application code. Therefore, it seems a bit silly to extract configuration information into persistence.xml. In fact, it prevents us from configuring the EntityManagerFactory in Spring (so that, for example, we can provide a Spring-configured data source). For that reason, we should turn our attention to container-managed JPA.

Configuring container-managed JPA

Container-managed JPA takes a slightly different approach. When running within a container, an EntityManagerFactory can be produced using information provided by the container. This form of JPA is intended for use in JEE application servers (such as WebLogic or JBoss) where data source information will be configured through the application server’s configuration.

Nevertheless, container-managed JPA is also possible with Spring. Instead of configuring data source details in persistence.xml, you can configure this information in the Spring application context. For example, listing 5.10 shows how to configure container-managed JPA in Spring using LocalContainerEntityManagerFactoryBean.

Listing 5.10 Configuring a container-managed EntityManagerFactory ???
factory bean

<bean id="entityManagerFactory" 
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
<property name="database" value="HSQL"/>
</bean>
</property>
<property name="loadTimeWeaver">
<!--OpenConfigures data sources connection Configures JPA vendor-specifics -->
<bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver" />
</property>
<!--Specifies load-time weaver-->
</bean>


Here we’ve configured the dataSource property with a Spring-configured data source. Any implementation of javax.sql.DataSource is appropriate, such as those that we configured in section 5.2. Although a data source may still be configured in persistence.xml, the data source specified through this property takes precedence.

The jpaVendorAdapter property can be used to provide specifics about the particular JPA implementation to use. In this case, we’re using TopLink Essentials, so we’ve configured it with a TopLinkJpaVendorAdapter. Several properties are set on the vendor adapter, but the most important one is the database property, where we’ve specified the Hypersonic database as the database we’ll be using. Other values supported for this property include those listed in table 5.5.

Certain dynamic persistence features require that the class of persistent objects be modified with instrumentation to support the feature. Objects whose properties are lazily loaded (that is, they will not be retrieved from the database until they are actually accessed) must have their class instrumented with code that knows to retrieve unloaded data upon access. Some frameworks use dynamic proxies to implement lazy loading. Others, such as JDO, perform class instrumentation at compile time.

JPA allows for load-time instrumentation of persistent classes so that a class is modified with dynamic persistence features as the class is loaded. The loadTimeWeaver property of LocalContainerEntityManagerFactoryBean lets us specify how the dynamic persistence features are woven into the persistent class. In this case, we’ve chosen Spring’s SimpleLoadTimeWeaver.

Which entity manager factory bean you choose will depend primarily on how you will use it. For simple applications, LocalEntityManagerFactoryBean may be

Файл:Fig511.png
Figure 5.11 JpaDaoSupport is a convenient superclass for JPA-backed DAO classes. It is wired with an EntityManagerFactory(produced by an EntityManagerfactory bean) and make a JpaTemplate available for data access.

Table 5.5 The TopLink vendor adapter supports several databases. You can specify which database to use by setting its database property.


Database platform Value for database property
IBM DB2 DB2
Hypersonic HSQL
Informix INFORMIX
MySQL MYSQL
Oracle ORACLE
PostgresQL POSTGRESQL
Microsoft SQL Server SQLSERVER
Sybase SYBASE

sufficient. But because LocalContainerEntityManagerFactoryBean enables us to configure more of JPA in Spring, it is an attractive choice and likely the one that you’ll choose for production use.

5.5.3 Building a JPA-backed DAO

Previously, we wired a reference to an entity manager factory bean into a JpaTemplate and then wired the JpaTemplate into our DAO. But Spring’s JpaDaoSupport simplifies things a bit further by making it possible to wire the entity manager factory bean directly into our DAO class.

JpaDaoSupport provides the same convenience for JPA-backed DAOs as JdbcDaoSupport and HibernateDaoSupport provided for JDBC-backed and Hibernate-backed DAOs, respectively. As shown in figure 5.11, a JPA-backed DAO class extends JpaDaoSupport and is injected with an EntityManagerFactory (which may be produced by an EntityManagerFactoryBean). Under the covers, JpaDaoSupport creates a JpaTemplate and makes it available to the DAO for data access. To take advantage of Spring’s JPA DAO support, we will write JpaRantDao to subclass JpaDaoSupport:

 public class JpaRantDao extends JpaDaoSupport  implements RantDao { 

}

Now, instead of wiring JpaRantDao with a JpaTemplate reference, we’ll wire it directly with the entityManagerFactory bean:

<bean id="rantDao" class="com.roadrantz.dao.jpa.JpaRantDao"> 
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>

Internally, JpaDaoSupport will use the entity manager factory wired into the entityManagerFactory property to create a JpaTemplate. As we flesh out the implementation of JpaRantDao, we can use the JpaTemplate by calling getJpaTemplate(). For example, the following reimplementation of saveMotorist() uses JpaDaoSupport’s getJpaTemplate() method to access the JpaTemplate and to persist a Motorist object:

public void saveMotorist(Motorist motorist) { 
getJpaTemplate().persist(motorist);
}

Both Hibernate and JPA are great solutions for object-relational mapping. Through ORM, the gory details of data access—SQL statements, database connections, and result sets—are hidden and we can deal with data persistence at the object level. However, although ORM hides data access specifics, it also hinders (or even prevents) fine-grained control of how persistence is handled.

At the other end of the spectrum is JDBC. With JDBC, you have complete control over data access. But with this control comes complete responsibility for the tedium of connection management and mapping result sets to objects. Next up, let’s have a look at how Spring integrates with iBATIS, a persistence framework that strikes a balance between the absolute control of JDBC and the transparent mapping of ORM.

Интеграция Spring с iBATIS

Somewhere in between pure JDBC and ORM is where iBATIS resides. iBATIS is
often classified among ORM solutions such as Hibernate and JPA, but I prefer to
refer to it as an object-query mapping (OQM) solution. Although the iBATIS feature
set overlaps that of ORM in many ways, iBATIS puts you in full control of the
actual SQL being performed. iBATIS will still take responsibility for mapping
query results to domain objects, but you are free to author the queries in any manner
that suits you best.

Spring offers integration with iBATIS that mirrors that of its integration with JDBC and ORM frameworks. As with the other persistence frameworks described in this chapter, we’re going to keep our focus on how Spring integrates with iBATIS. If you’d like to learn more about iBATIS, I recommend you check out iBATIS in Action (Manning, 2007).

5.6.1 Configuring an iBATIS client template

At the center of the iBATIS API is the com.ibatis.sqlmap.client.SqlMapClient interface. SqlMapClient is roughly equivalent to Hibernate’s Session or JPA’s EntityManager. It is through this interface that all data access operations are performed.

Unfortunately, iBATIS shares many of the same problems as JDBC, Hibernate (pre-3.0), and JPA. Specifically, applications that use iBATIS for persistence are required to manage sessions. This session management code is typically nothing more than boilerplate code and distracts from the real goal of persisting objects to a database.

Furthermore, the persistence methods of SqlMapClient are written to throw java.sql.SQLException if there are any problems. As we’ve already discussed, SQLException is both a checked exception and too generic to react to in any useful way.

SqlMapClientTemplate is Spring’s answer to the iBATIS session management and exception-handling problems. Much like the other templates that we’ve covered in this chapter, SqlMapClientTemplate wraps an SqlMapClient to transparently open and close sessions. It also will catch any SQLExceptions that are thrown and rethrow them as one of Spring’s unchecked persistence exceptions in table 5.1.

Configuring an SqlMapClientTemplate

SqlMapClientTemplate can be configured in the Spring application context as
follows:

<bean id="sqlMapClientTemplate" 
class="org.springframework.orm.ibatis.SqlMapClientTemplate">
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>

The sqlMapClient property must be wired with a reference to an iBATIS SqlMap-
Client. In Spring, the best way to get an SqlMapClient is through SqlMapClient-
FactoryBean:

<bean id="sqlMapClient" 
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="sql-map-config.xml" />
</bean>

SqlMapClientFactoryBean is a Spring factory bean that produces an SqlMapClient. The dataSource property is wired with a reference to a javax.sql.DataSource. Any of the data sources described in section 5.2 will do.

Defining iBATIS SQL maps

As for the configLocation property, it should be configured with the path to an XML file that enumerates the locations of the iBATIS SQL maps. For the RoadRantz application, we’ve defined one SQL map file per domain object. Therefore, the sql-map-config.xml file will look like this:

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//
DTD SQL Map Config 2.0//EN"
"https://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<sqlMap resource="com/roadrantz/domain/rant-sql.xml" />
<sqlMap resource="com/roadrantz/domain/motorist-sql.xml" />
<sqlMap resource="com/roadrantz/domain/vehicle-sql.xml" />
</sqlMapConfig>

The three SQL map files are loaded as resources from the classpath under the same package as the domain objects themselves. As an example of iBATIS SQL mapping, listing 5.11 shows an excerpt from rant-sql.xml.

Listing 5.11 An example of mapping SQL queries to Rant objects

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"https://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMap> namespace="Rant"

<resultMap id="rantResult"
class="com.roadrantz.domain.Rant">
 
<!--Defines result mpping-->
<result property="id" column="id" />
<result property="rantText" column="rant_text" />
<result property="postedDate" column="posted_date" />
<result property="vehicle" column="vehicle_id"
select="getVehicleById" />
</resultMap>
 

 
<select id="getRantsForDay"
resultMap="rantResult"
parameterClass="int">
 
<![CDATA[
Declares
select id, posted_date, rant_text, vehicle_id
getRantsForDay
from rant
query
where posted_date = #VALUE#Declares
]]>getRantsForDay
</select> query

</sqlMap>

In listing 5.11, we’ve defined a query that loads a list of Rant objects based on data
passed in as a parameter when the getRantsForDay query is performed. The
query is associated with a <resultMap> entry that tells iBATIS to convert each row
returned from the query into a Rant object. By the time our DAO sees the results,
they will be in the form of a List of Rant objects.

Using the template in a DAO

Before we can use the SqlMapClientTemplate to perform data access operations,
we must wire it into our DAO. The following excerpt from IBatisRantDao shows
an implementation of RantDao that is injected with an SqlMapClientTemplate:

public class IBatisRantDao implements RantDao { 

// injected
private SqlMapClientTemplate sqlMapClientTemplate;
public void setSqlMapClientTemplate(
SqlMapClientTemplate sqlMapClientTemplate) {
this.sqlMapClientTemplate = sqlMapClientTemplate;
}
}

Since IBatisRantDao depends on an SqlMapClientTemplate, we’ll need to configure it as follows in the Spring configuration:

<bean id="rantDao" 
class="com.roadrantz.dao.ibatis.IBatisRantDao">
<property name="sqlMapClientTemplate"
ref="sqlMapClientTemplate" />
</bean>

With the SqlMapClientTemplate injected into IBatisRantDao, we can now start fleshing out the persistence methods needed by the RoadRantz application. Here’s what the getRantsForDay() method looks like when written to use the injected SqlMapClientTemplate:

public List<Rant> getRantsForDay(Date day) { 
return sqlMapClientTemplate.queryForList(
"getRantsForDay", day);
}

As with the other persistence mechanisms, Spring also provides DAO support for iBATIS. Before we end our exploration of Spring-iBATIS integration, let’s see how we can build the RoadRantz data access layer using iBATIS DAO support.

5.6.2 Building an iBATIS-backed DAO

The SqlMapClientDaoSupport class is a DAO support class for iBATIS. Much like the other DAO support classes offered by Spring, SqlMapClientDaoSupport is intended to be subclassed by a DAO implementation. As depicted in figure 5.12, SqlMapClientDaoSupport is a convenient superclass for iBATIS-backed DAOs that exposes an SqlMapClientTemplate object that can be used to execute
iBATIS queries. Rewriting the IBatisRantDAO class to use SqlMapClientDaoSupport, we have the following class definition.

public class IBatisRantDAO extends SqlMapClientDaoSupport
implements RantDao {

}

SqlMapClientDaoSupport provides an SqlMapClientTemplate for your DAO to use through its getSqlMapClientTemplate() method. As an example of how to use getSqlMapClientTemplate(), here’s the new getRantsForDay() method:


Файл:Fig512.png

Figure 5.12 SqlMapClientDaoSupport is a convenient way to create iBATISbacked
DAO classes. SqlMapClientDaoSupport is injected with an
SqlMapClient that it wraps with an SqlMapClientTemplate to hide iBATIS
boilerplate code.

public List<Rant> getRantsForDay(Date day) { 
return getSqlMapClientTemplate().queryForList(
"getRantsForDay", day);
}

The big difference between wiring an SqlMapClientTemplate directly into a DAO and subclassing SqlMapClientDaoSupport is that you can eliminate one of the beans in the Spring configuration. When a DAO subclasses SqlMapClientDaoSupport, you can bypass the SqlMapClientTemplate bean and wire an SqlMapClient (or an SqlMapClientFactoryBean that produces an SqlMapClient) directly into
the DAO:

<bean id="rantDao" class="com.roadrantz.dao.ibatis.IBatisRantDao"> 
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>

As with the other persistence frameworks that integrate with Spring, the decision to either use a DAO support class or wire a template directly into your DAO is mostly a matter of taste. Although SqlMapClientDaoSupport does slightly simplify configuration of an iBATIS-backed DAO, you may prefer to inject an SqlMapClientTemplate into an application’s DAO—especially if your DAO class already subclasses
another base class.

Thus far, you’ve seen several ways of reading and writing data to a database, and we’ve built the persistence layer of the RoadRantz application. Now that you know how to read data from a database, let’s see how to avoid unnecessary database reads using Spring’s support for data caching.

5.7 Caching

In many applications, data is read more frequently than it is written. In the RoadRantz application, for instance, more people will visit the site to view the rants for a particular day or vehicle than those who post rants. Although the list of rants will grow over time, it will not grow as often as it is viewed.

Moreover, the data presented by the RoadRantz application is not considered time sensitive. If a user were to browse the site and see a slightly outdated list of rants, it probably would not have any negative impact on them. Eventually, they could return to the site to see a newer list of rants and no harm would be done.

Nevertheless, every time that a list of rants is requested, the DAO will go back to the database and ask for the latest data (which, more often than not, is the same data as the last time it asked).

Database operations are often the number-one performance bottleneck in an application. Even the simplest queries against highly optimized data stores can add up to performance problems in a high-use application.

When you consider the infrequency of data changes along with the performance costs of querying a database, it seems silly to always query the database for the latest data. Instead, it seems to make sense to cache frequently accessed (but not frequently updated) data.

On the surface, caching sounds quite simple: after retrieving some information, store it away in a local (and more easily accessible) location so that it’s handy the next time you need it. But implementing a caching solution by hand can be tricky. For example, have a look at HibernateRantDao’s getRantsForDay() method:

public List<Rant> getRantsForDay(Date day) { 
return getHibernateTemplate().find("from " + RANT +
" where postedDate = ?", day);
}

The getRantsForDay() method is a perfect candidate for caching. There’s no way to go back in time and add a rant for a day in the past. Unless the day being queried for is today, the list of rants returned for any given day will never change. Therefore, there’s no point in always going back to the database for the list of rants that were posted last Tuesday. The database only needs to be queried once,
and then we can remember it in case we’re ever asked for it again. Now let’s modify getRantsForDay() to use some form of homegrown cache:

public List<Rant> getRantsForDay(Date day) { 
List<Rant> cachedResult =
rantCache.lookup("getRantsForDay", day);
if(cachedResult != null) {
return cachedResult;
}
cachedResult = getHibernateTemplate().find("from " + RANT +
" where postedDate = ?", day);
rantCache.store("getRantsForDay", day, cachedResult);
return cachedResult
}

This version of getRantsForDay() is much more awkward. The real purpose of getRantsForDay() is to look up the rants for a given day. But the bulk of the method is dealing with caching. Furthermore, it doesn’t directly deal with some of the complexities of caching, such as cache expiration, flushing, or overflow.


Файл:Fig513.png

Figure 5.13 The Spring Modules caching module intercepts calls to a bean’s methods, looking up data from a cache for quick data access and thus avoiding unnecessary slow queries to the database.


Fortunately, a more elegant caching solution is available for Spring applications. The Spring Modules project (https://springmodules.dev.java.net) provides caching via aspects. Rather than explicitly instrument methods to be cached, Spring
Modules caching aspects apply advice to bean methods to transparently cache their results.

As illustrated in figure 5.13, Spring Modules support for caching involves a proxy that intercepts calls to one or more methods of Spring-managed beans. When a proxied method is called, Spring Modules Cache first consults a cache to see whether the method has already been called previously with the same arguments. If so, it will return the value in the cache and the actual method will not be
invoked. Otherwise, the method is called and its return value is stored in the cache for the next time that the method is called.

In this section, we’re going to cache-enable the DAO layer of the RoadRantz application using Spring Modules Cache. This will make the application perform better and give our hard-working database a well-earned break.

5.7.1 Configuring a caching solution

Although Spring Modules provides a proxy for intercepting methods and storing
the results in a cache, it does not provide an actual caching solution. Instead, it
relies on a third-party cache solution. Several caching solutions are supported,
including:

  • EHCache
  • GigaSpaces
  • JBoss Cache
  • JCS
  • OpenSymphony’s OSCache
  • Tangosol’s Coherence

For our RoadRantz application, I’ve chosen EHCache. This decision was based primarily on my previous experience with EHCache and the fact that it is readily available in the Maven repository at www.ibiblio.org. However, regardless of which caching solution you choose, the configuration for Spring Modules Cache is quite similar for all caching solutions.

The first thing we’ll need to do is create a new Spring configuration file to declare caching in. While we could have worked the Spring Modules Cache configuration into any of the Spring context configuration files loaded in the RoadRantz application, it’s better to keep thing separate. So we’ll create roadrantz-cache.xml to hold our caching configuration.

As with any Spring context configuration file, roadrantz-cache.xml is rooted with the <beans> element. However, to take advantage of Spring Modules’ support for EHCache, we’ll need to declare the <beans> element to recognize the ehcache namespace:

<beans xmlns="https://www.springframework.org/schema/beans" 
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:ehcache="https://www.springmodules.org/schema/ehcache"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
https://www.springmodules.org/schema/ehcache
https://www.springmodules.org/schema/cache/springmodules-ehcache.xsd">


</beans>

We’re using EHCache for the RoadRantz application, but if you’d like to use one of the other supported caching providers, you’ll need to swap out the namespace and schema declaration with the Spring Modules namespace and schema declaration appropriate for your choice. Table 5.6 lists each namespace along with its URI and schema URI.

Regardless of which caching provider you choose, you’ll be given several Spring configuration elements for configuring declarative caching in Spring. Table 5.7 catalogs these elements.


Table 5.6 The namespaces and schemas for the various caching providers supported by
Spring Modules.

Namespace Namespace URI Schema URI 
ehcache https://www.springmodules.org/
schema/ehcache
https://www.springmodules.org/
schema/cache/springmodulesehcache.
xsd
gigaspaces https://www.springmodules.org/
schema/gigaspaces
https://www.springmodules.org/
schema/cache/springmodulesgigaspaces.
xsd /
jboss https://www.springmodules.org/
schema/jboss
https://www.springmodules.org/
schema/cache/springmodulesjboss.
xsd
jcs https://www.springmodules.org/
schema/jcs
https://www.springmodules.org/
schema/cache/springmodules-jcs.xsd
oscache https://www.springmodules.org/
schema/oscache
https://www.springmodules.org/
schema/cache/springmodulesoscache.
xsd
tangosol https://www.springmodules.org/
schema/tangosol
https://www.springmodules.org/
schema/cache/springmodulestangosol.
xsd

Table 5.7 Spring Modules’ configuration elements.

Configuration element What it’s for
<namespace:annotations> Declaring cached methods by tagging them with Java
5 annotations
<namespace:commons-attributes> Declaring cached methods by tagging them with
Jakarta Commons Attributes metadata
<namespace:config> Configuring the EHCache cache provider in Spring XML
<namespace:proxy> Declaring cached methods by declaring a proxy in
Spring XML

Since we’re using EHCache as the caching provider, we’ll need to tell Spring where to find the EHCache configuration file.4 That’s what the <ehcache:config>
element is for:

<ehcache:config 
configLocation="classpath:ehcache.xml" />

Here we’re setting the configLocation attribute to tell Spring to load EHCache’s configuration from the root of the application’s classpath.

Configuring EHCache

As for the ehcache.xml file itself, we’ve configured it as shown in listing 5.12.

Listing 5.12 Configuring EHCache in ehcache.xml

<ehcache> 
<defaultCache
maxElementsInMemory="500"
eternal="true"
overflowToDisk="false"
memoryStoreEvictionPolicy="LFU" />
 
 
<cache name="rantzCache"
maxElementsInMemory="500"
eternal="true"
overflowToDisk="false"
memoryStoreEvictionPolicy="LFU" />
 
 
</ehcache>

Configures
default cache

Configures
rantzCache

To summarize the code, we’ve configured two caches for EHCache to manage. The <defaultCache> element is mandatory and describes the cache that will be used if no other suitable cache is found. The <cache> element defines other caches and may appear zero or more times in ehcache.xml (once for each cache it defines). Here we’ve defined rantzCache as the only nondefault cache. The attributes specified on <defaultCache> and <cache> describe the behavior of the cache. Table 5.8 lists the attributes available when configuring a cache
in EHCache.

4
At the time of this writing, the EHCache configuration (and the specific configuration for the other
caching providers) is still specified in a provider-specific file external to Spring. But future versions may
expose provider-specific configuration through the <namespace:config> element so that the external
file is no longer necessary.


Table 5.8 Cache configuration attributes for EHCache.

Attribute Used to specify…
diskExpiryThreadIntervalSeconds How often (in seconds) the disk expiry thread is run—
that is, how often the disk-persisted cache is
cleansed of expired items. (Default: 120 seconds.)
diskPersistent Whether or not the disk store persists between
restarts of the VM. (Default: false.)
eternal Whether or not elements are eternal. If they are eternal,
the element never expires. (Required.)
maxElementsInMemory The maximum number of elements that will be cached
in memory. (Required.)
memoryStoreEvictionPolicy How eviction will be enforced when
maxElementsInMemory is reached. By default,
the least recently used (LRU) policy is applied. Other
options are first-in/first-out (FIFO) and less frequently
used (LFU). (Default: LRU.)
name The name of the cache. (Required for <cache>.)
overflowToDisk Whether or not the cache is allowed to overflow to
disk when the in-memory cache has reached the
maxElementsInMemory limit. (Required.)
timeToIdleSeconds The time (in seconds) between accesses before an
element expires. A value of 0 indicates that the element
can be idle forever. (Default: 0.)
timeToLiveSeconds The time (in seconds) that an element is allowed to
live in cache before it expires. A value of 0 indicates
that the element can live in cache forever without
expiring. (Default: 0.)

For the RoadRantz application, we’ve configured one default cache (because EHCache says that we have to) and another cache called rantzCache that will be the primary cache. We’ve configured both caches to allow for up to 500 elements to be kept in cache (with no expiration) and the least frequently used elements will be evicted. In addition, no disk overflow will be allowed.

With EHCache configured in the Spring application context, we are now ready to declare which beans and methods should have their results cached. Let’s start

by declaring a proxy that will cache the values returned from the methods of the
RoadRantz DAO layer.

5.7.2 Proxying beans for caching

We’ve already identified the getRantsForDay() method of HibernateRantDao as a candidate for caching. Back in the Spring context definition, we’ll use the <ehcache:proxy> element to wrap the HibernateRantDao with a proxy that will cache everything returned from getRantsForDay():

<ehcache:proxy id="rantDao" 
refId="rantDaoTarget">
<ehcache:caching
methodName="getRantsForDay"
cacheName="rantzCache" />
</ehcache:proxy>

The <ehcache:caching> element declares which method(s) will be intercepted and which cache their return values will be cached in. For our purposes, method-Name has been set to intercept the getRantsForDay() method and to use the rantzCache cache.

You may declare as many <ehcache:caching> elements within <ehcache:proxy> as you need to describe caching for a bean’s methods. You
could use one <ehcache:caching> element for each cached method. Or you can also use wildcards to specify multiple methods with only one <ehcache:caching> element. The following <ehcache:caching> element, for example, will proxy all methods whose name starts with get to be cached:

<ehcache:caching 
methodName="get*"
cacheName="rantzCache" />

Putting items into a cache is only half of the problem. After a while the cache will become littered with lots of data, some of which may no longer be relevant. Eventually, it may be desirable to clear out the cache (call it “Spring cleaning”) and start over. Let’s see how to flush the cache upon a method call.


Flushing the cache

Where the <ehcache:caching> element declares methods that populate the cache, <ehcache:flushing> declares methods that empty the cache. For example, let’s suppose that you’d like to clear out the rantzCache cache whenever the saveRant() method is called. The following <ehcache:flushing> element will handle that for you:

<ehcache:flushing 
methodName="saveRant"
cacheName="rantzCache" />

By default, the cache specified in the cacheName attribute will be flushed after the method specified with methodName is invoked. But you can change the timing of the flush by using the when attribute:

<ehcache:flushing 
methodName="saveRant"
cacheName="rantzCache"
when="before" />

By setting when to before we are asking for the cache to be flushed before the saveRant() method is invoked.


Declaring a proxied inner bean

Take note of <ehcache:proxy>’s id and refId attributes. The proxy produced by <ehcache:proxy> will be given an id of rantDao. However, that’s the id of the real HibernateRantDao bean. Therefore, we’ll need to rename the real bean to rantDaoTarget, which is referred to by the refId attribute. (This is consistent with how classic Spring AOP proxies and their targets are named. See section 4.2.3 for a reminder of how that works.)

If the id/refId arrangement seems awkward, then you also have the option of declaring the target bean as an inner bean of <ehcache:proxy>. For example, here’s <ehcache:proxy> reconfigured with HibernateRantDao as an inner bean:

<ehcache:proxy id="rantDao">
<bean class="com.roadrantz.dao.HibernateRantDao">
<property name="sessionFactory"
ref="sessionFactory" />
</bean>
<ehcache:caching
methodName="getRantsForDay"
cacheName="rantzCache" />
</ehcache:proxy>

Even using inner beans, you’ll still need to declare one <ehcache:proxy> element for each bean to be proxied and one or more <ehcache:caching> element for the methods. For simple applications, this may be okay. But as the number of cache-proxied beans and methods goes up, it will mean more and more XML in your Spring configuration.

If the inner-bean approach still seems clumsy or if you will be proxying several beans to be cached, you may want to consider using Spring Modules’ support for declarative caching by annotation. Let’s kiss <ehcache:proxy> goodbye and see how Spring Modules supports annotation-driven caching.

5.7.3 Annotation-driven caching

In addition to the XML-based caching configuration described in the previous section, Spring Modules supports declarative caching using code-level metadata. This support comes in two varieties:


  • Java 5 annotations—This is the ideal solution if you’re targeting the Java 5 platform.
  • Jakarta Commons Attributes—If you’re targeting pre–Java 5, you may choose Jakarta Commons Attributes.

For RoadRantz, we’re targeting Java 5. Therefore, we’ll be using Java 5 annotations to declare caching in the DAO layer. Spring Modules provides two annotations with regard to caching:


  • @Cacheable—Declares that a method’s return value should be cached
  • @CacheFlush—Declares a method to be a trigger for flushing a cache

Using the @Cacheable annotation, we can declare the getRantsForDay() method
to be cached like so:

@Cacheable(modelId="rantzCacheModel") 
public List<Rant> getRantsForDay(Date day) {
return getHibernateTemplate().find("from " + RANT +
" where postedDate = ?", day);
}

The modelId attribute specifies a caching model that will be used to cache the values returned from getRantsForDay(). We’ll talk more about how the caching model is defined in a moment. But first, let’s use @CacheFlush to specify a flush action when the saveRant() method is called:

@CacheFlush(modelId="rantzFlushModel") 
public void saveRant(Rant rant) {
getHibernateTemplate().saveOrUpdate(rant);
}

The modelId attribute refers to the flushing model that will be cleared when the saveRant() method is invoked. Speaking of caching and flushing models, you probably would like to know where those come from. The <ehcache:annotations> element is used to enable Spring Modules’ support for annotations. We’ll configure it in roadrantzcache.
xml as follows:

<ehcache:annotations> 
<ehcache:caching id="rantzCacheModel"
cacheName="rantzCache" />
</ehcache:annotations>

Within the <ehcache:annotations> element, we must configure at least one <ehcache:caching> element. <ehcache:caching> defines a caching model. In simple terms, a caching model is little more than a reference to a named cache configured in ehcache.xml. Here we’ve associated the name rantzCacheModel with a cache named rantzCache. Consequently, any @Cacheable whose modelId is rantzCacheModel will target the cache named rantzCache.

A flushing model is quite similar to a caching model, except that it refers to the cache that will be flushed. We’ll configure a flushing model called rantzFlushModel alongside the rantzCacheModel using the <ehcache:flushing> element:

<ehcache:annotations> 
<ehcache:caching id="rantzCacheModel"
cacheName="rantzCache" />
<ehcache:flushing id="rantzFlushModel"
cacheName="rantzCache" />
</ehcache:annotations>

The one thing that sets cache models apart from flushing models is that a flushing
model not only decides which cache to flush, but also when to flush it. By default,
the cache is flushed after @CacheFlush-annotated methods are called. But you can
change that by specifying a value for the when attribute of <ehcache:flushing>:

<ehcache:annotations> 
<ehcache:caching id="rantzCacheModel"
cacheName="rantzCache" />
<ehcache:flushing id="rantzFlushModel"
cacheName="rantzCache"
when="before" />
</ehcache:annotations>

By setting the when attribute to before, the cache will be flushed before a @CacheFlush-annotated method is invoked.

5.8 Summary

Data is the lifeblood of an application. Some of the data-centric among us may even contend that data is the application. With such significance being placed on data, it’s important that we develop the data access portion of our applications in a way that is robust, simple, and clear, Spring’s support for JDBC and ORM frameworks takes the drudgery out of data access by handling common boilerplate code that exists in all persistence mechanisms, leaving you to focus on the specifics of data access as they pertain to your application.

One way that Spring simplifies data access is by managing the lifecycle of database connections and ORM framework sessions, ensuring that they are opened and closed as necessary. In this way, management of persistence mechanisms is virtually transparent to your application code.

Also, Spring is able to catch framework-specific exceptions (some of which are checked exceptions) and convert them to one of a hierarchy of unchecked exceptions that are consistent among all persistence frameworks supported by Spring. This includes converting nebulous SQLExceptions thrown by JDBC and iBATIS into meaningful exceptions that describe the actual problem that led to the exception being thrown.

We’ve also seen how an add-on module from the Spring Modules project can provide declarative caching support for your data access layer, increasing performance when often-requested, but scarcely updated, data is retrieved from a database.

Transaction management is another aspect of data access that Spring can make simple and transparent. In the next chapter, we’ll explore how to use Spring AOP for declarative transaction management.

Wikijava.org.ua-1px.png

]]>
Книги по Java https://linexp.ru?id=4729 Wed, 29 Jun 2022 14:05:04 GMT
<![CDATA[Глава 2 Spring in Action 2th edition]]> Книги по Java https://linexp.ru?id=4728 Wed, 29 Jun 2022 14:04:23 GMT <![CDATA[Глава 6 Spring in Action 2th edition]]> ticket typically involves the following actions:

Understanding transactions

To illustrate transactions, consider the purchase of a movie ticket. Purchasing a
ticket typically involves the following actions:


  • The number of available seats will be examined to verify that there are enough seats available for your purchase.
  • The number of available seats is decremented by one for each ticket purchased.
  • You provide payment for the ticket.
  • The ticket is issued to you.

If everything goes well, you’ll be enjoying a blockbuster movie and the theater will be a few dollars richer. But what if something goes wrong? For instance, what if you paid with a credit card that had reached its limit? Certainly, you would not receive a ticket and the theater wouldn’t receive payment. But if the number of seats isn’t reset to its value before the purchase, the movie may artificially run out of seats (and thus lose sales). Or consider what would happen if everything else works fine but the ticket issue fails. You’d be short a few dollars and be stuck at home watching reruns on cable TV.

To ensure that neither you nor the theater loses out, these actions should be wrapped in a transaction. As a transaction, they’re all treated as a single action, guaranteeing that they’ll either all fully succeed or they’ll all be rolled back as if these steps never happened. Figure 6.1 illustrates how this transaction plays out.

Transaction Committed

1. Verify Seats
2. Reserve Seat
3. Receive Payment
4. Issue Ticket
Purchase Ticket
Everything Goes Well
Something Goes Wrong
Transaction Rolled Back
Figure 6.1 The steps involved when purchasing a movie ticket should be all or nothing. If every step is successful then the entire transaction is successful. Otherwise, the steps should be rolled back—as if they never happened.


Transactions play an important role in software, ensuring that data and resources are never left in an inconsistent state. Without them, there is potential for data to be corrupted or inconsistent with the business rules of the application. Before we get too carried away with Spring’s transaction support, it’s important to understand the key ingredients of a transaction. Let’s take a quick look at the
four factors that guide transactions and how they work.

Explaining transactions in only four words

In the grand tradition of software development, an acronym has been created to
describe transactions: ACID. In short, ACID stands for:


  • Atomic—Transactions are made up of one or more activities bundled together as a single unit of work. Atomicity ensures that all the operations in the transaction happen or that none of them happen. If all the activities succeed, the transaction is a success. If any of the activities fail, the entire transaction fails and is rolled back.
  • Consistent—Once a transaction ends (whether successful or not), the system is left in a state that is consistent with the business that it models. The data should not be corrupted with respect to reality.
  • Isolated—Transactions should allow multiple users to work with the same data, without each user’s work getting tangled up with the others. Therefore, transactions should be isolated from each other, preventing concurrent reads and writes to the same data from occurring. (Note that isolation typically involves locking rows and/or tables in a database.)
  • Durable—Once the transaction has completed, the results of the transaction should be made permanent so that they will survive any sort of system crash. This typically involves storing the results in a database or some other form of persistent storage.

In the movie ticket example, a transaction could ensure atomicity by undoing the result of all the steps if any step fails. Atomicity supports consistency by ensuring that the system’s data is never left in an inconsistent, partially done state. Isolation
also supports consistency by preventing another concurrent transaction from stealing seats out from under you while you are still in the process of purchasing them.

Finally, the effects are durable because they will have been committed to some persistent storage. In the event of a system crash or other catastrophic event, you shouldn’t have to worry about results of the transaction being lost. For a more detailed explanation of transactions, we suggest that you read Martin Fowler’s Patterns of Enterprise Application Architecture (Addison-Wesley Professional, 2002). Specifically, chapter 5 discusses concurrency and transactions.Now that you know the makings of a transaction, let’s see the transaction capabilities available to a Spring application.

Understanding Spring’s transaction management support

Spring, like EJB, provides support for both programmatic and declarative transaction
management support. But Spring’s transaction management capabilities
exceed those of EJB.

Spring’s support for programmatic transaction management differs greatly
from that of EJB. Unlike EJB, which is coupled with a Java Transaction API (JTA)
implementation, Spring employs a callback mechanism that abstracts away the
actual transaction implementation from the transactional code. In fact, Spring’s
transaction management support doesn’t even require a JTA implementation. If
your application uses only a single persistent resource, Spring can use the transactional
support offered by the persistence mechanism. This includes JDBC, Hibernate,
Java Data Objects (JDO), and Apache’s Object Relational Bridge (OJB).
However, if your application has transaction requirements that span multiple
resources, Spring can support distributed (XA) transactions using a third-party
JTA implementation. We’ll discuss Spring’s support for programmatic transactions
in section 6.3.

While programmatic transaction management affords you flexibility in precisely
defining transaction boundaries in your code, declarative transactions help
you decouple an operation from its transaction rules. Spring’s support for declarative
transactions is reminiscent of EJB’s container-managed transactions (CMTs).
Both allow you to define transaction boundaries declaratively. But Spring’s declarative
transactions go beyond CMTs by allowing you to declare additional attributes
such as isolation level and timeouts. We’ll begin working with Spring’s declarative
transaction support in section 6.4.

Choosing between programmatic and declarative transaction management is
largely a decision of fine-grained control versus convenience. When you program
transactions into your code, you gain precise control over transaction boundaries,
beginning and ending them precisely where you want. Typically, you will not
require the fine-grained control offered by programmatic transactions and will
choose to declare your transactions in the context definition file.

Regardless of whether you choose to program transactions into your beans or
to declare them as aspects, you’ll be using a Spring transaction manager to


interface with a platform-specific transaction implementation. Let’s see how
Spring’s transaction managers free you from dealing directly with platformspecific
transaction implementations.

Choosing a transaction manager

Spring does not directly manage transactions. Instead, it comes with a selection of
transaction managers that delegate responsibility for transaction management to
a platform-specific transaction implementation provided by either JTA or the persistence
mechanism. Spring’s transaction managers are listed in table 6.1.

Table 6.1 Spring has transaction managers for every occasion.

Transaction manager (org.springframework.*) Use it when…
jca.cci.connection.
CciLocalTransactionManager
Using Spring’s support for J2EE Connector
Architecture (JCA) and the Common Client
Interface (CCI).
jdbc.datasource.
DataSourceTransactionManager
Working with Spring’s JDBC abstraction support.
Also useful when using iBATIS for persistence.
jms.connection.JmsTransactionManager Using JMS 1.1+.
jms.connection.
JmsTransactionManager102
Using JMS 1.0.2.
orm.hibernate.
HibernateTransactionManager
Using Hibernate 2 for persistence.
orm.hibernate3.
HibernateTransactionManager
Using Hibernate 3 for persistence.
orm.jdo.JdoTransactionManager Using JDO for persistence.
orm.jpa.JpaTransactionManager Using the Java Persistence API (JPA) for persistence.
orm.toplink.TopLinkTransactionManager Using Oracle’s TopLink for persistence.
transaction.jta.JtaTransactionManager You need distributed transactions or when
no other transaction manager fits the need.
transaction.jta.
OC4JJtaTransactionManager
Using Oracle's OC4J JEE container.
transaction.jta.
WebLogicJtaTransactionManager
You need distributed transactions and your
application is running within WebLogic.

Jpa
Transaction
Manager
Platform-Specific Transaction Implementations
Figure 6.2 Spring’s transaction managers delegate transaction-management responsibility
to platform-specific transaction implementations.
JTA JPA
Jpa
Transaction
Manager
Platform-Specific Transaction Implementations
Figure 6.2 Spring’s transaction managers delegate transaction-management responsibility
to platform-specific transaction implementations.

Each of these transaction managers acts as a facade to a platform-specific transaction
implementation. (Figure 6.2 illustrates the relationship between transaction
managers and the underlying platform implementations for a few of the transaction
managers.) This makes it possible for you to work with a transaction in
Spring with little regard to what the actual transaction implementation is.

To use a transaction manager, you’ll need to declare it in your application context.
In this section, you’ll learn how to configure a few of Spring’s most commonly
used transaction managers, starting with DataSourceTransactionManager,
which provides transaction support for plain JDBC and iBATIS.

JDBC transactions

If you’re using straight JDBC for your application’s persistence, DataSourceTransactionManager
will handle transactional boundaries for you. To use DataSource-
TransactionManager, wire it into your application’s context definition using the
following XML:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 
<property name="dataSource" ref="dataSource"/>
</bean>

Spring's Transaction Managers

Platform 
Transaction
Manager
JDBC Hibernate JDO
DataSource
Transaction
Manager
Hibernate
Transaction
Manager
Jdo
Transaction
Manager
Jta
Transaction
Manager

Notice that the dataSource property is set with a reference to a bean named
dataSource. Presumably, the dataSource bean is a javax.sql.DataSource bean
defined elsewhere in your context definition file.

Behind the scenes, DataSourceTransactionManager manages transactions by
making calls on the java.sql.Connection object retrieved from the DataSource.
For instance, a successful transaction is committed by calling the commit()
method on the connection. Likewise, a failed transaction is rolled back by calling
the rollback() method.

Hibernate transactions

If your application’s persistence is handled by Hibernate then you’ll want to use HibernateTransactionManager. For Hibernate 2.x, it is a bean declared with the following XML:

<bean id="transactionManager" 
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

On the other hand, if you’re using Hibernate 3.x, you’ll need to declare this version of the HibernateTransactionManage bean (pay careful attention to the package name):

<bean id="transactionManager" 
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

The sessionFactory property should be wired with a Hibernate SessionFactory, here cleverly named sessionFactory. See chapter 5 for details on setting up a Hibernate session factory.

HibernateTransactionManager delegates responsibility for transaction management to an org.hibernate.Transaction object that it retrieves from the Hibernate session. When a transaction successfully completes, HibernateTransactionManager will call the commit() method on the Transaction object. Similarly, when a transaction fails, the rollback() method will be called on the Transaction object.

Java Persistence API transactions

Hibernate has been Java’s de facto persistence standard for a few years, but now the Java Persistence API (JPA) has entered the scene as the true standard for Java persistence. If you’re ready to move up to JPA then you’ll want to use Spring’s JpaTransactionManager to coordinate transactions. Here’s how you might configure JpaTransactionManager in Spring:

<bean id="transactionManager" 
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>

JpaTransactionManager only needs to be wired with a JPA entity manager factory (any implementation of javax.persistence.EntityManagerFactory). JpaTransactionManager will collaborate with the JPA EntityManager produced by the factory to conduct transactions.

In addition to applying transactions to JPA operations, JpaTransactionManageralso supports transactions on simple JDBC operations on the same DataSourceused by EntityManagerFactory. For this to work, JpaTransactionManager must also be wired with an implementation of JpaDialect. For example, suppose that you’ve configured TopLinkJpaDialect as follows:

<bean id="jpaDialect" 
class="org.springframework.orm.jpa.vendor.TopLinkJpaDialect" />

Then you must wire the jpaDialect bean into the JpaTransactionManager like this:

<bean id="transactionManager" 
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
<property name="jpaDialect"
ref="jpaDialect" />
</bean>

It’s important to note that the JpaDialect implementation must support mixed JPA/JDBC access for this to work. All of Spring’s vendor-specific implementations of JpaDialect (HibernateJpaDialect, OpenJpaDialect, and TopLinkJpaDialect) provide support for mixing JPA with JDBC. DefaultJpaDialect, however, does not.

Java Data Objects transactions

Perhaps JDBC and Hibernate aren’t your style and you’re not quite ready to move up to JPA. Suppose that instead you’ve decided to implement your application’s persistence layer using Java Data Objects (JDOs). In that case, the transaction manager of choice will be JdoTransactionManager. It can be declared into your application’s context like this:

<bean id="transactionManager" 
class="org.springframework.orm.jdo.JdoTransactionManager">
<property name="persistenceManagerFactory"
ref="persistenceManagerFactory"/>
</bean>

With JdoTransactionManager, you need to wire in a javax.jdo.PersistenceManagerFactory instance to the persistenceManagerFactory property.

Under the covers, JdoTransactionManager works with the transaction object retrieved from the JDO persistence manager, calling commit() at the end of a successful transaction and rollback() if the transaction fails.

Java Transaction API transactions

If none of the aforementioned transaction managers meet your needs or if your
transactions span multiple transaction sources (e.g., two or more different databases),
you’ll need to use JtaTransactionManager:

<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName"
value="java:/TransactionManager" />
</bean>

JtaTransactionManager delegates transaction management responsibility to a JTA implementation. JTA specifies a standard API to coordinate transactions between an application and one or more data sources. The transactionManagerName property specifies a JTA transaction manager to be looked up via JNDI.

JtaTransactionManager works with javax.transaction.UserTransaction and javax.transaction.TransactionManager objects, delegating responsibility for transaction management to those objects. A successful transaction will be committed with a call to the UserTransaction.commit() method. Likewise, if the transaction fails, the UserTransaction’s rollback() method will be called.

By now, I hope you’ve found a Spring transaction manager suitable for your application’s needs and have wired it into your Spring configuration file. Now it’s time to put that transaction manager to work. We’ll start by employing the transaction manager to program transactions manually.

Programming transactions in Spring

There are two kinds of people: those who are control freaks and those who aren’t. Control freaks like complete control over everything that happens and don’t take anything for granted. If you’re a developer and a control freak, you’re probably the kind of person who prefers the command line and would rather write your own getter and setter methods than to delegate that work to an IDE.

Control freaks also like to know exactly what is going on in their code. When it comes to transactions, they want full control over where a transaction starts, where it commits, and where it ends. Declarative transactions aren’t precise enough for them.

This isn’t a bad thing, though. The control freaks are at least partially right. As you’ll see later in this chapter, you are limited to declaring transactions at the method level. If you need more fine-grained control over transactional boundaries, programmatic transactions are the only way to go.

We don’t have to look hard to find a need for transactions in the RoadRantz
application. Consider the addRant() method of RantServiceImpl (listing 6.1) as an example of a transactional method.

Listing 6.1 addRant(), which adds a Rantand associates the Rantwith a Vehicle

public void addRant(Rant rant) { 
rant.setPostedDate(new Date());
 
Vehicle rantVehicle = rant.getVehicle(); // Check for
Vehicle existingVehicle = // existing
rantDao.findVehicleByPlate(rantVehicle.getState(), // vehicle
rantVehicle.getPlateNumber());
 
if(existingVehicle != null) { //Associates vehicle to rant
rant.setVehicle(existingVehicle);
} else {
rantDao.saveVehicle(rantVehicle);//Saves new vehicle
}
rantDao.saveRant(rant);// Saves rant
}

There’s a lot more going on in addRant() than just simply saving a Rant object:

  • First, it’s possible that the rant’s vehicle already exists and, if so, the rant should be associated with the existing vehicle.
  • If the rant’s vehicle doesn’t already exist, the vehicle needs to be saved.
  • Finally, the rant itself must be saved.

If any of these actions go sour, all actions should be rolled back as if nothing happened.
Otherwise, the database will be left in an inconsistent state. A vehicle
could be added to the database without any associated rants. In other words,
addRant() should be transactional.

One approach to adding transactions is to programmatically add transactional boundaries directly within the addRant() method using Spring’s TransactionTemplate. Like other template classes in Spring (such as JdbcTemplate, discussed in chapter 5), TransactionTemplate utilizes a callback mechanism. I’ve updated the addRant() method in listing 6.2 to show how to add a transactional context using a TransactionTemplate.

Listing 6.2 Programmatically adding transactions to addRant()

public void addRant(Rant rant) {
transactionTemplate.execute(
 
new TransactionCallback() {
public Object doInTransaction(TransactionStatus ts) {
// Performs within transaction
try {
rant.setPostedDate(new Date());
 
Vehicle rantVehicle = rant.getVehicle();
Vehicle existingVehicle =
rantDao.findVehicleByPlate(rantVehicle.getState(),
rantVehicle.getPlateNumber());
 
if(existingVehicle != null) {
rant.setVehicle(existingVehicle);
} else {
rantDao.saveVehicle(rantVehicle);
}
 
rantDao.saveRant(rant);
} catch (Exception e) {
ts.setRollbackOnly(); //Rolls back on exceptions
}
return null;
}
}
}

To use the TransactionTemplate, you start by implementing the TransactionCallback interface. Because TransactionCallback has only one method to implement, it is often easiest to implement it as an anonymous inner class, as shown in listing 6.2. As for the code that needs to be transactional, place it within the doInTransaction() method.

Calling the execute() method on the TransactionTemplate instance will execute the code contained within the TransactionCallback instance. If your code encounters a problem, calling setRollbackOnly() on the TransactionStatus object will roll back the transaction. Otherwise, if the doInTransaction() method returns successfully, the transaction will be committed. Where does the TransactionTemplate instance come from? Good question. It should be injected into RantServiceImpl, as follows:

<bean id="rantService" 
class="com.roadrantz.service.RantServiceImpl">

<property name="transactionTemplate ">
<bean class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager"
ref="transactionManager" />
</bean>
</property>
</bean>

Notice that the TransactionTemplate is injected with a transactionManager. Under the hood, TransactionTemplate uses an implementation of PlatformTransactionManager to handle the platform-specific details of transaction management. Here we’ve wired in a reference to a bean named transactionManager, which could be any of the transaction managers listed in table 6.1.

Programmatic transactions are good when you want complete control over transactional boundaries. But, as you can see from the code in listing 6.2, they are a bit intrusive. You had to alter the implementation of addRant()—using Springspecific classes—to employ Spring’s programmatic transaction support.

Usually your transactional needs won’t require such precise control over transactional boundaries. That’s why you’ll typically choose to declare your transactions outside your application code (in the Spring configuration file, for instance). The rest of this chapter will cover Spring’s declarative transaction management.

Declaring transactions

At one time not too long ago, declarative transaction management was a capability only available in EJB containers. But now Spring offers support for declarative transactions to POJOs. This is a significant feature of Spring because you now have an alternative to EJB for declaring atomic operations.

Spring’s support for declarative transaction management is implemented through Spring’s AOP framework. This is a natural fit because transactions are a system-level service above an application’s primary functionality. You can think of a Spring transaction as an aspect that “wraps” a method with transactional boundaries.

Spring provides three ways to declare transactional boundaries in the Spring configuration. Historically, Spring has always supported declarative transactions by proxying beans using Spring AOP. But Spring 2.0 adds two new flavors of declarative transactions: simple XML-declared transactions and annotation-driven transactions.

We’ll look at all of these approaches to declaring transactions later in this section, but first let’s examine the attributes that define transactions.

Defining transaction attributes

In Spring, declarative transactions are defined with transaction attributes. A transaction attribute is a description of how transaction policies should be applied to a method. There are five facets of a transaction attribute, as illustrated in figure 6.3.

Although Spring provides several mechanisms for declaring transactions, all of them rely on these five parameters to govern how transactions policies are administered. Therefore, it’s essential to understand these parameters in order to declare transaction policies in Spring.

Regardless of which declarative transaction mechanism you use, you’ll have the opportunity to define
these attributes. Let’s examine each attribute to understand how it shapes a transaction.

Propagation behavior

The first facet of a transaction is propagation behavior. Propagation behavior
defines the boundaries of the transaction with respect to the client and to the
method being called. Spring defines seven distinct propagation behaviors, as
described in table 6.2.

NOTE
The propagation behaviors described in table 6.2 are defined as
constants in the org.springframework.transaction.TransactionDefinition
interface.

Propagation
Isolation
Read-Only?
T imeout
Rollback RulesDeclarative
T ransaction
Figure 6.3 Declarative
transactions are defined in
terms of propagation behavior,
isolation level, read-only hints,
timeout, and rollback rules.

Table 6.2 Propagation rules define when a transaction is created or when an existing transaction can
be used. Spring provides several propagation rules to choose from.

Propagation behavior What it means
PROPAGATION_MANDATORY Indicates that the method must run within a transaction. If
no existing transaction is in progress, an exception will be
thrown.
PROPAGATION_NESTED Indicates that the method should be run within a nested
transaction if an existing transaction is in progress. The
nested transaction can be committed and rolled back individually
from the enclosing transaction. If no enclosing
transaction exists, behaves like PROPAGATION_
REQUIRED. Vendor support for this propagation behavior is
spotty at best. Consult the documentation for your resource
manager to determine if nested transactions are supported.
PROPAGATION_NEVER Indicates that the current method should not run within a
transactional context. If there is an existing transaction in
progress, an exception will be thrown.
PROPAGATION_NOT_SUPPORTED Indicates that the method should not run within a transaction.
If an existing transaction is in progress, it will be suspended
for the duration of the method. If using
JTATransactionManager, access to
TransactionManager is required.
PROPAGATION_REQUIRED Indicates that the current method must run within a transaction.
If an existing transaction is in progress, the method will
run within that transaction. Otherwise, a new transaction will
be started.
PROPAGATION_REQUIRES_NEW Indicates that the current method must run within its own
transaction. A new transaction is started and if an existing
transaction is in progress, it will be suspended for the duration
of the method. If using JTATransactionManager,
access to TransactionManager is required.
PROPAGATION_SUPPORTS Indicates that the current method does not require a transactional
context, but may run within a transaction if one is
already in progress.

The propagation behaviors in table 6.2 may look familiar. That’s because they mirror the propagation rules available in EJB’s container-managed transactions (CMTs). For instance, Spring’s PROPAGATION_REQUIRES_NEW is equivalent to CMT’s RequiresNew. Spring adds an additional propagation behavior not available in CMT, PROPAGATION_NESTED, to support nested transactions.

Propagation rules answer the question of whether a new transaction should be started or suspended, or if a method should even be executed within a transactional context at all.

For example, if a method is declared to be transactional with PROPAGATION_REQUIRES_NEW behavior, it means that the transactional boundaries are the same as the method’s own boundaries: a new transaction is started when the method begins and the transaction ends with the method returns or throws an exception.If the method has PROPAGATION_REQUIRED behavior, the transactional boundaries
depend on whether a transaction is already under way.

Isolation levels

The second dimension of a declared transaction is the isolation level. An isolation
level defines how much a transaction may be impacted by the activities of other
concurrent transactions. Another way to look at a transaction’s isolation level is to
think of it as how selfish the transaction is with the transactional data.

In a typical application, multiple transactions run concurrently, often working
with the same data to get their job done. Concurrency, while necessary, can lead
to the following problems:

  • Dirty read—Dirty reads occur when one transaction reads data that has been written but not yet committed by another transaction. If the changes are later rolled back, the data obtained by the first transaction will be invalid.
  • Nonrepeatable read—Nonrepeatable reads happen when a transaction performs the same query two or more times and each time the data is different. This is usually due to another concurrent transaction updating the data between the queries.
  • Phantom reads—Phantom reads are similar to nonrepeatable reads. These occur when a transaction (T1) reads several rows, and then a concurrent transaction (T2) inserts rows. Upon subsequent queries, the first transaction (T1) finds additional rows that were not there before.

In an ideal situation, transactions would be completely isolated from each other,thus avoiding these problems. However, perfect isolation can affect performance because it often involves locking rows (and sometimes complete tables) in the data store. Aggressive locking can hinder concurrency, requiring transactions to wait on each other to do their work.

Realizing that perfect isolation can impact performance and because not all applications will require perfect isolation, sometimes it is desirable to be flexible


Table 6.3 Isolation levels determine to what degree a transaction may be impacted by other
transactions being performed in parallel.

Isolation level What it means
ISOLATION_DEFAULT Use the default isolation level of the underlying data
store.
ISOLATION_READ_UNCOMMITTED Allows you to read changes that have not yet been committed.
May result in dirty reads, phantom reads, and
nonrepeatable reads.
ISOLATION_READ_COMMITTED Allows reads from concurrent transactions that have
been committed. Dirty reads are prevented, but phantom
and nonrepeatable reads may still occur.
ISOLATION_REPEATABLE_READ Multiple reads of the same field will yield the same
results, unless changed by the transaction itself. Dirty
reads and nonrepeatable reads are prevented, but phantom
reads may still occur.
ISOLATION_SERIALIZABLE This fully ACID-compliant isolation level ensures that dirty
reads, nonrepeatable reads, and phantom reads are all
prevented. This is the slowest of all isolation levels
because it is typically accomplished by doing full table
locks on the tables involved in the transaction.

with regard to transaction isolation. Therefore, several levels of isolation are possible,
as described in table 6.3.

NOTE
The isolation levels described in table 6.3 are defined as constants in the
org.springframework.transaction.TransactionDefinition interface.

ISOLATION_READ_UNCOMMITTED is the most efficient isolation level, but isolates the transaction the least, leaving the transaction open to dirty, nonrepeatable, and phantom reads. At the other extreme, ISOLATION_SERIALIZABLE prevents all forms of isolation problems but is the least efficient.

Be aware that not all data sources support all the isolation levels listed in table 6.3. Consult the documentation for your resource manager to determine what isolation levels are available.

Read-only

The third characteristic of a declared transaction is whether it is a read-only transaction. If a transaction performs only read operations against the underlying data store, the data store may be able to apply certain optimizations that take advantage of the read-only nature of the transaction. By declaring a transaction as read-only, you give the underlying data store the opportunity to apply those optimizations
as it sees fit.

Because read-only optimizations are applied by the underlying data store when a transaction begins, it only makes sense to declare a transaction as read-only on methods with propagation behaviors that may start a new transaction (PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, and PROPAGATION_ NESTED).

Furthermore, if you are using Hibernate as your persistence mechanism, declaring a transaction as read-only will result in Hibernate’s flush mode being set to FLUSH_NEVER. This tells Hibernate to avoid unnecessary synchronization of objects with the database, thus delaying all updates until the end of the transaction.

Transaction timeout

For an application to perform well, its transactions can’t carry on for a long time. Therefore, the next trait of a declared transaction is its timeout.

Suppose that your transaction becomes unexpectedly long-running. Because transactions may involve locks on the underlying data store, long-running transactions can tie up database resources unnecessarily. Instead of waiting it out, you can declare a transaction to automatically roll back after a certain number of seconds.

Because the timeout clock begins ticking when a transaction starts, it only makes sense to declare a transaction timeout on methods with propagation behaviors that may start a new transaction (PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, and PROPAGATION_NESTED).

Rollback rules

The final facet of the transaction pentagon is a set of rules that define what exceptions prompt a rollback and which ones do not. By default, transactions are rolled back only on runtime exceptions and not on checked exceptions. (This behavior is consistent with rollback behavior in EJBs.)

However, you can declare that a transaction be rolled back on specific checked exceptions as well as runtime exceptions. Likewise, you can declare that a transaction not roll back on specified exceptions, even if those exceptions are runtime exceptions.

Now that you’ve got an overview of how transaction attributes shape the behavior of a transaction, let’s see how to use these attributes when declaring transactions
in Spring.

Proxying transactions

In pre-2.0 versions of Spring, declarative transaction management was accomplished by proxying your POJOs with Spring’s TransactionProxyFactoryBean. TransactionProxyFactoryBean is a specialization of ProxyFactoryBean that knows how to proxy a POJO’s methods by wrapping them with transactional boundaries. Listing 6.3 shows how you can declare a TransactionProxyFactoryBean that wraps the RantServiceImpl class.

Listing 6.3 Proxying the rant service for transactions

<bean id="rantService" 
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="target"
ref="rantServiceTarget" /> <!--Wires transaction target -->
 
<property name="proxyInterfaces"
value="com.roadrantz.service.RantService" /> <!--Specifies proxy interface-->
 
<property name="transactionManager"
ref="transactionManager" /> <!--Wires in transaction manager -->
 
<property name="transactionAttributes">
<props> <!-- Configures transaction rules, boundaries -->
<prop key="add*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_SUPPORTS,readOnly</prop>
</props>
</property>
</bean>

Notice that the bean’s id is rantService. But wait—doesn’t that conflict with the RantServiceImpl bean that we’ve already declared? As a matter of fact, it does, and here’s why: the rant service has no idea that its methods are being called within the context of a transaction. If any object makes calls directly to the rant service, those calls will not be transactional. Instead, collaborating objects should
invoke methods on the proxy that is produced by TransactionProxyFactoryBean (as shown in figure 6.4). The proxy will ensure that transactional rules are applied and then proxy the call to the real rant service. Therefore, rather than inject the rant service directly into those objects that use it, we’ll inject the rant service proxy into those objects.

This means that the proxy produced by TransactionProxyFactoryBean must pretend to be a rant service. That’s the purpose of the proxyInterfaces property.


Transaction
Proxy
(produced by
Transaction
Proxy
FactoryBean)
RantServiceImplClient
addRant()
RantServiceaddRant()
Platform
Transaction
Manager
Transaction
Attributes
Figure 6.4
TransactionProxyFactoryBean produces
a transaction-aware proxy that receives calls on
behalf of the transaction target, wrapping the
calls in a transaction.
Here we’re telling TransactionProxyFactoryBean to produce a proxy that implements
the RantService interface.

So what becomes of the original rantService bean that we declared? Quite simply, it is renamed to rantServiceTarget and injected into the TransactionProxyFactoryBean to be proxied.

The transactionManager property supplies the appropriate transaction manager bean. This can be any of the transaction managers discussed in section 6.2. TransactionProxyFactoryBean will use the transaction manager to start, suspect, commit, and roll back transactions based on the transaction attributes defined in the transactionAttributes property of TransactionProxyFactoryBean.

Speaking of the transactionAttributes property, this property declares which methods are to be run within a transaction and what the transaction attributes are to be. This property is given a <props> collection where the key of each <prop> is a method name pattern and the value defines the transaction attributes for the method(s) selected.

The value of each <prop> given to the transactionAttributes property is a comma-separated value that takes the form shown in figure 6.5.

In the case of the rant service, we’re declaring that all methods whose name starts with add (including addRant()) should be run within a transaction. All other methods support transactions (but do not necessarily require a transaction) and are read-only.


Is the transaction

read only?

(optional)

Propagation Behavior
PROPAGATION, ISOLATION, readOnly, -Exception, +Exception


Isolation Level Rollback Rules
(optional) (optional)

Figure 6.5 A transaction attribute definition is made up of a propagation
behavior, an isolation level, a read-only flag, and rollback rules. The propagation
behavior is the only required element.

Creating a transaction proxy template

It’s one thing to proxy a single service bean using TransactionProxyFactoryBean. But what if you have multiple service beans in your application and they all must be transactional? The XML required to proxy a single service bean is verbose enough, but it gets really messy if you have to repeat it for more than one service bean.

Fortunately, you don’t have to. Using Spring’s ability to create abstract beans and then “sub-bean,” you can define your transaction policies in one place and then apply them repeatedly to all of your service beans. First, you must create an abstract declaration of TransactionProxyFactoryBean. The following declaration of txProxyTemplate does the trick:

<bean id="txProxyTemplate" 
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
abstract="true">
<property name="transactionManager"
ref="transactionManager" />
<property name="transactionAttributes">
<props>
<prop key="add*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_SUPPORTS,readOnly</prop>
</props>
</property>
</bean>

You’ll notice that this abstract declaration is virtually identical to the concrete declaration in listing 6.2. Missing, however, are the specifics of the bean that will be proxied. From this single abstract TransactionProxyFactoryBean declaration, we can now make any number of beans transactional by using txProxyTemplate as the parent declaration of the bean. For example, the following XML extends
txProxyTemplate for the rant service:

<bean id="rantService" parent="txProxyTemplate"> 
<property name="target" ref="rantServiceTarget" />
<property name="proxyInterfaces"
value="com.roadrantz.service.RantService" />
</bean>

This XML is much simpler and only specifies the target bean that is to be proxied with transactions and its interface. Proxying another bean with the same transactional policies involves creating another bean declaration whose parent is txProxyTemplate and targets the other bean.

Although TransactionProxyFactoryBean has been the workhorse of Spring’s declarative transaction support since the very beginning, it is somewhat cumbersome to use. Recognizing that awkwardness of TransactionProxyFactoryBean, Spring 2.0 adds simplified support for declarative transaction. Let’s switch gears and see what Spring 2.0 has to offer with regard to declarative transactions.

Declaring transactions in Spring 2.0

The problem with TransactionProxyFactoryBean is that using it results in extremely verbose Spring configuration files (transaction proxy templates notwithstanding). What’s more, the practice of naming the target bean with a target suffix is somewhat peculiar and can be confusing.

The good news is that Spring 2.0 provides some new configuration elements especially for declaring transactions. These elements are in the tx namespace and can be used by adding the spring-tx-2.0.xsd schema to your Spring configuration
XML file:

<beans xmlns="https://www.springframework.org/schema/beans" 
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xmlns:tx="https://www.springframework.org/schema/tx"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
https://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop-2.0.xsd
https://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

Note that the aop namespace should also be included. This is important, because the new declarative transaction configuration elements rely on a few of Spring’s new AOP configuration elements (as discussed in chapter 4).


The tx namespace provides a handful of new XML configuration elements, most notably the <tx:advice> element. The following XML snippet shows how <tx:advice> can be used to declare transactional policies similar to those we defined for the rant service in listing 6.3:

<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="*" propagation="SUPPORTS"
read-only="true"/>
</tx:attributes>
</tx:advice>

With <tx:advice>, the transaction attributes are defined in a <tx:attributes> element, which contains one or more <tx:method> elements. The <tx:method> element defines the transaction attributes for a given method (or methods) as defined by the name attribute (using wildcards).

<tx:method> has several attributes that help define the transaction policies for the method(s), as defined in table 6.4.

As defined in the txAdvice transaction advice, the transactional methods configured are divided into two categories: Those whose names begin with add and everything else. The addRant() method falls into the first category and is declared to require a transaction. The other methods are declared with propagation ="supports"—they’ll run in a transaction if one already exists, but they don’t need to run within a transaction.

Table 6.4 The six facets of the transaction pentagon (see figure 6.3) are specified in the attributes of
the <tx:method> element.

Attribute Purpose
isolation Specifies the transaction isolation level
no-rollback-for Specifies exceptions for which the transaction should continue and not be
rolled back
propagation Defines the transaction’s propagation rule
read-only Specifies that a transaction be read-only
rollback-for Specifies checked exceptions for which a transaction should be rolled back
and not committed
timeout Defines a timeout for a long-running transaction

When declaring a transaction using <tx:advice>, you’ll still need a transaction manager just like you did when using TransactionProxyFactoryBean. Choosing convention over configuration, <tx:advice> assumes that the transaction
manager will be declared as a bean whose id is transactionManager. If you happen to give your transaction manager a different id (txManager, for instance), you’ll need to specify the id of the transaction manager in the transactionmanager
attribute:

<tx:advice id="txAdvice"
transaction-manager="txManager">

</tx:advice>

On its own, <tx:advice> only defines an AOP advice for advising methods with transaction boundaries. But this is only transaction advice, not a complete transactional aspect. Nowhere in <tx:advice> did we indicate which beans should be advised—we need a pointcut for that. To completely define the transaction aspect, we must define an advisor. This is where the aop namespace gets involved. The following XML defines an advisor that uses the txAdvice advice to advise any beans that implement the RantService interface:

<aop:config>
<aop:advisor
pointcut="execution(* *..RantService.*(..))"
advice-ref="txAdvice"/>
</aop:config>

The pointcut attribute uses an AspectJ pointcut expression to indicate that this advisor should advise all methods of the RantService interface. Which methods are actually run within a transaction and what the transactional attributes are for those methods is defined by the transaction advice, which is referenced with the advice-ref attribute to be the advice named txAdvice.

Although the <tx:advice> element goes a long way toward making declarative transactions more palatable for Spring developers, there’s one more new feature of Spring 2.0 that makes it even nicer for those working in a Java 5 environment. Let’s have a look at how Spring transactions can be annotation driven.

Defining annotation-driven transactions

The <tx:advice> configuration element greatly simplifies the XML required for declarative transactions in Spring. What if I told you that it could be simplified even further? What if I told you that, in fact, you only need to add a single line of XML to your Spring context in order to declare transactions?

In addition to the <tx:advice> element, the tx namespace provides the <tx:annotation-driven> element. Using <tx:annotation-driven> is often as simple as the following line of XML:

<tx:annotation-driven />

That’s it! If you were expecting more, I apologize. I could make it slightly more interesting by specifying a specific transaction manager bean with the transaction-manager attribute (which defaults to transactionManager):

<tx:annotation-driven transaction-manager="txManager" />

Otherwise, there’s not much more to it than that. That single line of XML packs a powerful punch that lets you define transaction rules where they make the most sense: on the methods that are to be transactional.

Annotations are one of the biggest and most debated new features of Java 5. Annotations let you define metadata directly in your code rather than in external configuration files. Although there’s much discussion on the proper use of annotations, I think that annotations are a perfect fit for declaring transactions.

The <tx:annotation-driven> configuration element tells Spring to examine all beans in the application context and to look for beans that are annotated with @Transactional, either at the class level or at the method level. For every bean that is @Transactional, <tx:annotation-driven> will automatically advise it with transaction advice. The transaction attributes of the advice will be defined by parameters of the @Transactional annotation. For example, listing 6.4 shows RantServiceImpl, updated to include the @Transactional annotations.

Listing 6.4 Annotating the rant service to be transactional

@Transactional(propagation=Propagation.SUPPORTS, readOnly=true) 
public class RantServiceImpl implements RantService {

@Transactional(propagation=Propagation.REQUIRED, readOnly=false)
public void addRant(Rant rant) {

}

}

At the class level, RantServiceImpl has been annotated with a @Transactional annotation that says that all methods will support transaction and be read-only. At the method level, the addRant() method has been annotated to indicate that this method requires a transactional context.

It may be interesting to note that the @Transactional annotation may also be applied to an interface. For example, listing 6.5 shows the RantService interface annotated with @Transactional.

Listing 6.5 Annotating the rant service to be transactional at the interface level

@Transactional(propagation=Propagation.SUPPORTS, readOnly=true) 
public interface RantService {

@Transactional(propagation=Propagation.REQUIRED, readOnly=false)
void addRant(Rant rant);

}

By annotating RantService instead of RantServiceImpl, we’re indicating that all implementations of RantService should be transactional.

Summary

Transactions are an important part of enterprise application development that leads to more robust software. They ensure an all-or-nothing behavior, preventing data from being inconsistent should the unexpected occur. They also support concurrency by preventing concurrent application threads from getting in each other’s way as they work with the same data.

Spring supports both programmatic and declarative transaction management. In either case, Spring shields you from having to work directly with a specific transaction management implementation by abstracting the transaction management platform behind a common API.

Spring employs its own AOP framework to support declarative transaction management. Spring’s declarative transaction support rivals that of EJB’s CMT, enabling you to declare more than just propagation behavior on POJOs, including isolation levels, read-only optimizations, and rollback rules for specific exceptions.


This chapter showed you how to bring declarative transactions into the Java 5 programming model using annotations. With the introduction of Java 5 annotations, making a method transactional is simply a matter of tagging it with the appropriate transaction annotation.

As you’ve seen, Spring bestows the power of declarative transactions to POJOs. This is an exciting development—declarative transactions were previously only available to EJBs. But declarative transactions are only the beginning of what Spring has to offer to POJOs. In the next chapter, you’ll see how Spring extends declarative security to POJOs.

Wikijava.org.ua-1px.png

]]>
Книги по Java https://linexp.ru?id=4727 Wed, 29 Jun 2022 14:03:54 GMT
<![CDATA[Глава 7 Spring in Action 2th edition]]> Seinfeld, Kramer frequently let himself into Jerry’s apartment to help himself to the goodies in Jerry’s refrigerator. On Friends, the various characters often entered one another’s apartments without warning or hesitation. Even once, while in London, Ross burst into Chandler’s hotel room, narrowly missing Chandler in a compromising situation with Ross’s sister.

Securing Spring

This chapter covers


  • Introducing Spring Security
  • Securing web applications using servlet filters
  • Authentication against databases and LDAP
  • Transparently securing method invocations

Have you ever noticed that most people in television sitcoms don’t lock their doors? It happens all the time. On Seinfeld, Kramer frequently let himself into Jerry’s apartment to help himself to the goodies in Jerry’s refrigerator. On Friends, the various characters often entered one another’s apartments without warning or hesitation. Even once, while in London, Ross burst into Chandler’s hotel room, narrowly missing Chandler in a compromising situation with Ross’s sister.

In the days of Leave it to Beaver, it wasn’t so unusual for people to leave their doors unlocked. But it seems crazy that in a day when we’re concerned with privacy and security we see television characters enabling unhindered access to their apartments and homes.

It’s a sad reality that there are villainous individuals roaming around seeking to steal our money, riches, cars, and other valuables. And it should be no surprise that as information is probably the most valuable item we have, crooks are looking for ways to steal our data and identity by sneaking into unsecured applications.

As software developers, we must take steps to protect the information that resides in our applications. Whether it’s an email account protected with a username/password pair or a brokerage account protected with a trading PIN, security is a crucial aspect of most applications.

It is no accident that I chose to describe application security with the word “aspect.” Security is a concern that transcends an application’s functionality. For the most part, an application should play no part in securing itself. Although you could write security functionality directly into your application’s code (and that’s not uncommon), it is better to keep security concerns separate from application concerns.

If you’re thinking that it is starting to sound as if security is accomplished using aspect-oriented techniques, you’re right. In this chapter we’re going to explore ways to secure your applications with aspects. But we won’t have to develop those aspects ourselves—we’re going to look at Spring Security, a security framework based on Spring AOP and servlet filters.1


I’m probably going to get a lot of emails about this, but I have to say it anyway: servlet filters are a primitive form of AOP, with URL patterns as a kind of pointcut expression language. There… I’ve said it… I feel better now.

Introducing Spring Security

Spring Security is a security framework that provides declarative security for your Spring-based applications. Spring Security provides a comprehensive security solution, handling authentication and authorization, at both the web request level and at the method invocation level. Based on the Spring Framework, Spring Security takes full advantage of dependency injection (DI) and aspectoriented techniques.

What’s in a name?

Historically, Spring Security is also known as Acegi Security (or simply Acegi). Acegi has long been a subproject of Spring. But as I write this, plans are afoot to bring Acegi even closer under the Spring umbrella of projects. Part of that move involves dropping the Acegi name in favor of “Spring Security.” This change is scheduled to take place in the 1.1.0 version of Acegi/Spring Security. Knowing that the change is imminent, I’ve decided to go ahead and start referring to it as Spring Security, although you’ll still see the Acegi name thrown about a bit in this chapter.

When securing web applications, Spring Security uses servlet filters that intercept servlet requests to perform authentication and enforce security. And, as you’ll find in section 7.4.1, Spring Security employs a unique mechanism for declaring servlet filters that enables you to inject them with their dependencies using Spring DI.

Spring Security can also enforce security at a lower level by securing method invocations. When securing methods, Spring Security uses Spring AOP to proxy objects, applying aspects that ensure that the user has proper authority to invoke the secured methods.

In any case, whether you only need security at the web request level or if you require lower-level method security, Spring Security employs five core components to enforce security, as shown in figure 7.1.

Before we get into the nitty-gritty of Spring Security, let’s take a high-level view of Spring Security and the part that each of these components plays in securing applications.

Файл:Fig71.png
Figure 7.1 The fundamental elements of Spring Security.Security interceptors

When you arrive home after a long day at work, you’ll need to unlock the door to your home. To open the door, you must insert a key into the lock that trips the tumblers properly and releases the latch. If the cut of the key is incorrect, the tumblers won’t be tripped and the latch won’t be released. But if you have the right key, all of the tumblers will accept the key and the latch will be released, allowing you to open the door.

In Spring Security, the security interceptor can be thought of as a latch that prevents you from accessing a secured resource in your application. To flip the latch and get past the security interceptor, you must enter your “key” (typically a username and password) into the system. The key will then try to trip the security interceptor’s “tumblers” in an attempt to grant you access to the secured resource.

The actual implementation of a security interceptor will depend on what resource is being secured. If you’re securing a URL in a web application, the security interceptor will be implemented as a servlet filter. But if you’re securing a method invocation, aspects will be used to enforce security. You’ll see both forms of security interceptor later in this chapter.

A security interceptor does little more than intercept access to resources to enforce security. It does not actually apply security rules. Instead, it delegates that responsibility to the various managers that are pictured at the bottom of figure 7.1. Let’s have a look at each of these managers, starting with the authentication manager.

Authentication managers

The first of the security interceptor’s tumblers to be tripped is the authentication manager. The authentication manager is responsible for determining who you are. It does this by considering your principal (typically a username) and your credentials (typically a password).

Your principal defines who you are and your credentials are evidence that corroborates your identity. If your credentials are good enough to convince the authentication manager that your principal identifies you then Spring Security will know whom it is dealing with.

As with the rest of Spring Security (and Spring itself), the authentication manager is a pluggable interface-based component. This makes it possible to use Spring Security with virtually any authentication mechanism you can imagine. As you’ll see later in this chapter, Spring Security comes with a handful of flexible authentication managers that cover the most common authentication strategies.

Access decisions managers

Once Spring Security has determined who you are, it must decide whether you are authorized to access the secured resource. An access decision manager is the second tumbler of the Spring Security lock to be tripped. The access decision manager performs authorization, deciding whether to let you in by considering your authentication information and the security attributes that have been associated with the secured resource.

For example, the security rules may dictate that only supervisors should be allowed access to a secured resource. If you have been granted supervisor privileges then the second and final tumbler, the access decision manager, will have been tripped and the security interceptor will move out of your way and let you gain access to the secured resource.

Just as with the authentication manager, the access decision manager is pluggable. Later in this chapter, we’ll take a closer look at the access decision managers that come with Spring Security.

Run-as managers

If you’ve gotten past the authentication manager and the access decision manager then the security interceptor will be unlocked and the door is ready to open. But before you twist the knob and go in, there’s one more thing that the security interceptor might do.

Even though you’ve passed authentication and been granted access to a resource, there may be more security restrictions behind the door. For example, you may be granted the rights to view a web page, but the objects that are used to create that page may have different security requirements than the web page. A run-as manager can be used to replace your authentication with an authentication that allows you access to the secured objects that are deeper in your application.

Note that not all applications have a need for identity substitution. Therefore, run-as managers are an optional security component and are not necessary in many applications secured by Spring Security.

After-invocation managers

Spring Security’s after-invocation manager is a little different from the other security manager components. Whereas the other security manager components perform some form of security enforcement before a secured resource is accessed, the after-invocation manager enforces security after the secured resource is accessed.

After-invocation managers are kind of like the person who waits to examine a receipt at the exit of some discount and home electronics stores. They check to ensure that you have proper authority to remove the valuable items from the store. Instead of making sure that you are allowed to remove a big-screen television from a store, however, after-invocation managers make sure that you’re allowed to view the data that is being returned from a secured resource.

If an after-invocation manager advises a service layer bean, it will be given the opportunity to review the value returned from the advised method. It can then make a decision as to whether the user is allowed to view the returned object. The after-invocation manager also has the option of altering the returned value to ensure that the user is only able to access certain properties of the returned object.

Like run-as managers, not all applications call for an after-invocation manager. You’ll only need an after-invocation manager if your application’s security scheme requires that access be restricted at the domain level on a per-instance basis.

Now that you’ve seen the big picture of Spring Security, we’re ready to configure Spring Security for the RoadRantz application. For our purposes, we won’t need a run-as manager or an after-invocation manager, so we’ll defer those as advanced Spring Security topics. Meanwhile, let’s get started by configuring an authentication manager.

Authenticating users

When applying security to an application, the first thing you need to do, before deciding whether to allow access, is figure out who the user is. In most applications, this means presenting a login screen to the user and asking them for the username and password.

How the user is prompted for their username and password will vary from application to application. For now, we’ll assume that the user’s login details have already been provided and we need Spring Security to authenticate the user. We’ll look at different ways to prompt the user for their username and password a little later in this chapter.

In Spring Security, the authentication manager assumes the job of establishing a user’s identity. An authentication manager is defined by the org.acegisecurity.AuthenticationManager interface:

public interface AuthenticationManager {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException;
Файл:Fig72.png
Figure 7.2 A ProviderManager delegates authentication responsibility to one or more authentication providers.

Third time’s a charm

A-ha! There’s the word “acegi” in the package name for AuthenticationManager. As mentioned earlier in this chapter, Spring Security has historically been known as Acegi Security. When Acegi is formally renamed to Spring Security, the packaging of its classes will also change. Actually, this will be the third base package name that Acegi/Spring Security has had. Acegi was originally packaged under net.sf.acegisecurity… then it was changed to org.acegisecurity. When version 1.1.0 is released, it will likely be repackaged under org.springframework.security. Nevertheless, as those changes haven’t happened yet, the examples in this chapter show the org.acegisecurity packaging.


The authenticate() method will attempt to authenticate the user using the org.acegisecurity.Authentication object (which carries the principal and credentials). If successful, the authenticate() method returns a complete Authentication object, including information about the user’s granted authorities (which will be considered by the authorization manager). If authentication fails, an AuthenticationException will be thrown.

As you can see, the AuthenticationManager interface is quite simple and you

could easily implement your own AuthenticationManager. But Spring Security comes with ProviderManager, an implementation of AuthenticationManager that is suitable for most situations. So instead of rolling our own authentication manager, let’s take a look at how to use ProviderManager.

Configuring a provider manager

ProviderManager is an authentication manager implementation that delegates responsibility for authentication to one or more authentication providers, as shown in figure 7.2.

The purpose of ProviderManager is to enable you to authenticate users against multiple identity management sources. Rather than relying on itself to perform authentication, ProviderManager steps one by one through a collection of authentication providers, until one of them successfully authenticates the user (or until it runs out of providers). This makes it possible for Spring Security to support multiple authentication mechanisms for a single application.

The following chunk of XML shows a typical configuration of ProviderManager in the Spring configuration file:

<bean id="authenticationManager"
class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
<ref bean="ldapAuthenticationProvider"/>
</list>
</property>
</bean>

ProviderManager is given its list of authentication providers through its providers property. Typically, you’ll only need one authentication provider, but in some cases, it may be useful to supply a list of several providers so that if authentication fails against one provider, another provider will be tried. Spring comes with several authentication providers, as listed in table 7.1.


Table 7.1 Spring Security comes with authentication providers for every occasion.

Authentication provider ('''org.acegisecurity.*''')
Purpose
adapters.AuthByAdapterProvider Authentication using container adapters. This makes it possible to authenticate against users created within the web container (e.g., Tomcat, JBoss, Jetty, Resin, etc.).
providers.anonymous. AnonymousAuthenticationProvider Authenticates a user as an anonymous user. Useful when a user token is needed, even when the user hasn’t logged in yet.
providers.cas.CasAuthenticationProvider Authentication against the JA-SIG Central Authentication Service (CAS). Useful when you need single sign-on capabilities.
providers.dao.DaoAuthenticationProvider Retrieving user information, including username and password from a database.
providers.dao. LdapAuthenticationProvider Authentication against a Lightweight Directory Access Protocol (LDAP) server.
providers.jaas. JaasAuthenticationProvider Retrieving user information from a JAAS login configuration.
providers.rememberme. RememberMeAuthenticationProvider Authenticates a user that was previously authenticated and remembered. Makes it possible to automatically log in a user without prompting for username and password.
providers.rcp. RemoteAuthenticationProvider Authentication against a remote service.
providers.TestingAuthenticationProvider Unit testing. Automatically considers a TestingAuthenticationToken as valid. Not for production use.
providers.x509. X509AuthenticationProvider Authentication using an X.509 certificate. Useful for authenticating users that are, in fact, other applications (such as a web-service client).
runas.RunAsImplAuthenticationProvider Authenticating a user who has had their identity substituted by a run-as manager.

As you can see in table 7.1, Spring Security provides an authentication provider to meet almost any need. But if you can’t find an authentication provider that suits your application’s security needs, you can always create your own authentication provider by implementing the org.acegisecurity.providers.AuthenticationProvider interface:

public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class authentication);
}

Файл:Fig73.png
Figure 7.3 A DaoAuthenticationManager authenticates users on behalf of the authentication manager by pulling user information from a database.

You may have noticed that the AuthenticationProvider interface isn’t much different from the AuthenticationManager interface shown a few pages back. They both share an authenticate() method that handles the authentication. In fact, you can think of authentication providers as subordinate authentication managers.

Space constraints do not allow me to go into the details of all 11 of Spring Security’s authentication providers. However, I will focus on a couple of the most commonly used authentication providers, starting with DaoAuthenticationProvider, which supports simple database-oriented authentication.

Authenticating against a database

Many applications store user information, including the username and password, in a relational database. If that’s how your application keeps user information, Spring Security’s DaoAuthenticationProvider may be a good choice for your application.

A DaoAuthenticationProvider is a simple authentication provider that uses a Data Access Object (DAO) to retrieve user information (including the user’s password) from a relational database.

With the username and password in hand, DaoAuthenticationProvider performs authentication by comparing the username and password retrieved from the database with the principal and credentials passed in an Authentication object from the authentication manager (see figure 7.3). If the username and password match up with the principal and credentials, the user will be authenticated and a fully populated Authentication object will be returned to the authentication manager. Otherwise, an AuthenticationException will be thrown and authentication will have failed.

Configuring a DaoAuthenticationProvider couldn’t be simpler. The following XML excerpt shows how to declare a DaoAuthenticationProvider bean and wire it with a reference to its DAO:

<bean id="authenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService"
ref="userDetailsService"/>
</bean>

The userDetailsService property is used to identify the bean that will be used to retrieve user information from the database. This property expects an instance of org.acegisecurity.userdetails.UserDetailsService. The question that remains is how the userDetailsService bean is configured.

The UserDetailsService interface requires that only one method be implemented:

public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException;
}

This method is fairly self-explanatory and you may already be thinking of several ways that you can implement this interface. But before you start writing your own implementation of UserDetailsService, you may be interested to know that Spring Security comes with two ready-made implementations of AuthenticationDao to choose from: InMemoryDaoImpl and JdbcDaoImpl. Let’s see how these two classes work to look up user details, starting with InMemoryDaoImpl.

Using an in-memory DAO

Although it may seem natural to assume that an AuthenticationDao object will always query a relational database for user information, that doesn’t necessarily have to be the case. If your application’s authentication needs are trivial or for development-time convenience, it may be simpler to configure your user information directly in the Spring configuration file.

For that purpose, Spring Security comes with InMemoryDaoImpl, an implementation of UserDetailsService that draws its user information from its Spring configuration. Here’s an example of how you may configure an InMemoryDaoImpl in the Spring configuration file:

<bean id="authenticationDao"
class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userMap">
<value>
palmerd=4moreyears,disabled,ROLE_PRESIDENT
bauerj=ineedsleep,ROLE_FIELD_OPS
obrianc=nosmile,ROLE_SR_ANALYST,ROLE_OPS
myersn=traitor,disabled,ROLE_CENTRAL_OPS
</value>
</property>
</bean>

The userMap property is configured with an org.acegisecurity.userdetails.memory.UserMap object that defines a set of usernames, passwords, and privileges. Fortunately, you needn’t concern yourself with constructing a UserMap instance when wiring InMemoryDaoImpl because there’s a property editor that handles the conversion of a String to a UserMap object for you.

On each line of the userMap, String is a name-value pair where the name is the username and the value is a comma-separated list that starts with the user’s password and is followed by one or more names that are the authorities to be granted to the user. Figure 7.4 breaks down the format of an entry in the user map.

In the declaration of the authenticationDao bean earlier, four users are defined: parlmerd, bauerj, obrianc, and myersn. Respectively, their passwords are 4moreyears, ineedsleep, nosmile, and traitor. The authorities are granted as follows:


  • ROLE_PRESIDENT authority has been given to the user whose username is palmerd.
  • ROLE_FIELD_OPS has been given to bauerj.
  • ROLE_CENTRAL_OPS has been given to myersn.
  • The obrianc user has been granted two authorities: ROLE_SR_ANALYST and ROLE_OPS.


Файл:Fig74.png
Figure 7.4 A Spring Security user map maps a username to a password, grantedusername enabled privileges, and optionally their status.

Take special note of the palmerd and myersn users. A special disabled flag immediately follows their passwords, indicating that they have been disabled (and thus can’t authenticate).

Although InMemoryDaoImpl is convenient and simple, it has some obvious limitations. Primarily, security administration requires that you edit the Spring configuration file and redeploy your application. While this is acceptable (and maybe even helpful) in a development environment, it is probably too cumbersome for production use. Therefore, I strongly advise against using InMemoryDaoImpl in a production setting. Instead, you should consider using JdbcDaoImpl, which we’ll look at next.

Declaring a JDBC DAO

JdbcDaoImpl is a simple, yet flexible, authentication DAO that retrieves user information from a relational database. In its simplest form, all it needs is a reference to a javax.sql.DataSource, and it can be declared in the Spring configuration file as follows:

<bean id="authenticationDao"
class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

As configured here, JdbcDaoImpl makes some basic assumptions about how user information is stored in the database. Specifically, it assumes a Users table and an Authorities table, as illustrated in figure 7.5.

When JdbcDaoImpl looks up user information, it will query with the following SQL:

SELECT username, password, enabled
FROM users
WHERE username = ?

Likewise, when looking up a user’s granted authorities, JdbcDaoImpl will use the following SQL:

SELECT username, authority
FROM authorities
WHERE username = ?
Файл:Fig75.png
Figure 7.5 The database tables assumed by JdbcDaoImpl.

While the table structures assumed by JdbcDaoImpl are straightforward, they probably do not match the tables you have set up for your own application’s security. For instance, in the RoadRantz application, the Motorist table holds registered users’ usernames (in the email column) and password. Does this mean that we can’t use JdbcDaoImpl to authenticate motorists in the RoadRantz application?

Not at all. But if we are to use JdbcDaoImpl, we must help it out a bit by telling it how to find the user information by setting the usersByUsernameQuery property. The following adjustment to the authenticationDao bean sets it up to query users from RoadRantz’s Motorist table:

<bean id="authenticationDao"
class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref bean="dataSource" />
<property name="usersByUsernameQuery">
<value>
SELECT email as username, password, enabled
FROM Motorist
WHERE email=?
</value>
</property>
</bean>

Now JdbcDaoImpl knows to look in the Motorist table for authentication information. But we must also tell JdbcDaoImpl how to query the database for a user’s granted authorities. For that we’ll set the authoritiesByUsernameQuery property:

<bean id="authenticationDao"
class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource" />
...
<property name="authoritiesByUsernameQuery">
<value>
SELECT email as username, privilege as authority
FROM Motorist_Privileges mp, Motorist m
WHERE mp.motorist_id = m.id
AND m.email=?
</value>
</property>
</bean>

Here we’ve configured JdbcDaoImpl to retrieve the motorist’s granted authorities from the Motorist_Privileges table. The query joins in the Motorist table because the Motorist_Privileges table only knows about a Motorist through a foreign key and JdbcDaoImpl expects the query to retrieve the authorities by username.

Working with encrypted passwords

When DaoAuthenticationProvider compares the user-provided password (at authentication) with the one retrieved from the database, it assumes that the password has been stored unencrypted. To beef up security, you may want to encrypt the password before storing it in the database. But if the password is stored encrypted in the database, the user-provided password must also be encrypted before the two passwords can be compared.

To accommodate encrypted passwords, DaoAuthenticationProvider can be wired with a password encoder. Spring Security comes with several password encoders to choose from, as described in table 7.2.

Table 7.2 Spring Security’s password encoders.

Password encoder ('''org.acegisecurity.providers.*''')
Purpose
encoding.Md5PasswordEncoder Performs Message Digest (MD5) encoding on the password
encoding.PlaintextPasswordEncoder Performs no encoding on the password, returning it unaltered
encoding.ShaPasswordEncoder Performs Secure Hash Algorithm (SHA) encoding on the password
ldap.authenticator.LdapShaPasswordEncoder Encodes the password using LDAP SHA and salted-SHA (SSHA) encodings

By default DaoAuthenticationProvider uses the PlaintextPasswordEncoder, which means that the password is left unencoded. But we can specify a different encoding by wiring DaoAuthenticationProvider’s passwordEncoder property. For example, here’s how to wire DaoAuthenticationProvider to use MD5 encoding:

<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="authenticationDao" />
<property name="passwordEncoder">
<bean class="org.acegisecurity.providers.encoding.Md5PasswordEncoder" />
</property>
</bean>

You’ll also need to set a salt source for the encoder. A salt source provides the salt, or encryption key, for the encoding. Spring Security provides two salt sources:


  • SystemWideSaltSource—Provides the same salt for all users
  • ReflectionSaltSource—Uses reflection on a specified property of the user’s User object to generate the salt

ReflectionSaltSource is the more secure of the two salt sources because each user’s password will likely be encoded using a different salt value. Even if a hacker were to figure out the salt used to encode one user’s password, it’s unlikely that they’ll be able to use the same salt to crack another user’s password. To use a ReflectionSaltSource, wire it into DaoAuthenticationProvider’s saltSource property like this:

<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="authenticationDao" />
<property name="passwordEncoder">
<bean class="org.acegisecurity.providers.encoding.Md5PasswordEncoder" />
</property>
<property name="saltSource">
<bean class="org.acegisecurity.providers.dao.salt.ReflectionSaltSource">
<property name="userPropertyToUse" value="userName" />
</bean>
</property>
</bean>

Here the user’s userName property is used as the salt to encode the user’s password. It’s important that the salt be static and never change. Otherwise, it will be impossible to authenticate the user (unless the password is re-encoded after the change using the new salt).

Although ReflectionSaltSource is certainly more secure, SystemWideSaltSource is much simpler and is sufficient for most circumstances. SystemWideSaltSource uses a single salt value for encoding all users’ passwords. To use a SystemWideSaltSource, wire the saltSource property like this:

<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="authenticationDao" />
<property name="passwordEncoder">
<bean class="org.acegisecurity.providers.encoding.Md5PasswordEncoder" />
</property>
<property name="saltSource">
<bean class="org.acegisecurity.providers.dao.salt.SystemWideSaltSource">
<property name="systemWideSalt" value="ABC123XYZ789" />
</bean>
</property>
</bean>

In this case, the same salt value, ABC123XYZ789, is used for encoding all passwords.

Caching user information

Every time that a request is made to a secured resource, the authentication manager is asked to retrieve the user’s security information. But if retrieving the user’s information involves performing a database query, querying for the same data every time may hinder application performance. Recognizing that a user’s information will not frequently change, it may be better to cache the user data upon the first query and retrieve it from cache with every subsequent request.

To enable caching of user information, we must provide DaoAuthenticationProviderwith an implementationof the org.acegisecurity.providers.dao.UserCache interface. This interface mandates the implementation of three methods:

public UserDetails getUserFromCache(String username);
public void putUserInCache(UserDetails user);
public void removeUserFromCache(String username);

The methods in the UserCache are self-explanatory, providing the ability to put, retrieve, or remove user details from the cache. It would be simple enough for you to write your own implementation of UserCache. However, Spring Security provides two convenient implementations that you should consider before developing your own:


  • org.acegisecurity.providers.dao.cache.NullUserCache
  • org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache

NullUserCache does not actually perform any caching at all. Instead, it always returns null from its getUserFromCache() method, forcing DaoAuthenticationProvider to query for the user information. This is the default UserCache used by DaoAuthenticationProvider.

EhCacheBasedUserCache is a more useful cache implementation. As its name implies, it is based on EHCache. Using EHCache with DaoAuthenticationProvider is simple. Simply wire an EhCacheBasedUserCache bean into DaoAuthenticationProvider’s userCache property:

<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="authenticationDao" />
...
 
<property name="userCache">
<bean class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
<property name="cache" ref="ehcache" />
</bean>
</property>
</bean>

The cache property refers to an ehcache bean, which should be an EHCache Cache object. One way to get such a Cache object is to use the Spring Modules’ cache module. For example, the following XML uses Spring Modules to configure EHCache:

<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager" ref="cacheManager" />
<property name="cacheName" value="userCache" />
</bean>
 
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
</bean>

As you may recall from chapter 5, Spring Modules’ EhCacheFactoryBean is a Spring factory bean that produces an EHCache Cache object. The actual caching configuration is found in the ehcache.xml file, which will be retrieved from the classpath.

DaoAuthenticationProvider is great when your application’s security information is kept in a relational database. Often, however, an application’s security is architected to authenticate against an LDAP server. Let’s see how to use Spring Security’s LdapAuthenticationProvider, which is a more suitable choice when authentication must happen via LDAP.

Authenticating against an LDAP repository

Spring Security supports authentication against LDAP through LdapAuthenticationProvider, an authentication provider that knows how to check user credentials against an LDAP repository. The following <bean> illustrates a typical configuration for LdapAuthenticationProvider:

<bean id="ldapAuthProvider"
class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">
<constructor-arg ref="authenticator" />
<constructor-arg ref="populator" />
</bean>

As you can see, there’s not much exciting about the LdapAuthenticationProvider. There are no details on how to find the LDAP server or about the repository’s initial context. Instead, LdapAuthenticationProvider is wired with an authenticator and a populator through constructor injection. What are those beans and what are they used for?

In fact, although LdapAuthenticationProvider claims to know how to talk to an LDAP repository, it actually relies on two strategy objects to do the real work:


  • The authenticator strategy handles the actual authentication (e.g., verification of user credentials) against the LDAP repository. The authenticator strategy can be any object that implements org.acegisecurity.providers.ldap.LdapAuthenticator.
  • The populator strategy is responsible for retrieving a user’s set of granted authorities from the LDAP repository. The populator strategy is any object that implements org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator.

Because the authentication and authorities responsibilities are defined as strategies, separate from LdapAuthenticationProvider, you are able to wire in the strategy implementations that best fit your application’s security needs.

So just how are the authenticator and populator beans defined? Let’s start by looking at the authenticator bean, which defines the authentication strategy for LdapAuthenticationProvider.

Authenticating with LDAP binding

When it comes to authenticating against LDAP, two approaches are commonly taken:


  • Binding to the LDAP server using the username and password of an LDAP user
  • Retrieving a user’s entry in LDAP and comparing the supplied password with a password attribute in the LDAP record

For bind authentication, Spring Security comes with an LdapAuthenticator implementation called BindAuthenticator. BindAuthenticator uses an LDAP bind operator to bind as a user to the LDAP server. This approach relies on the LDAP server to authenticate the user’s credentials.

The following <bean> declares a BindAuthenticator in Spring:

<bean id="authenticator"
class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg ref="initialDirContextFactory" />
<property name="userDnPatterns">
<list>
<value>uid={0},ou=motorists</value>
</list>
</property>
</bean>

Here I’ve declared the BindAuthenticator to be injected through a constructor argument and through the userDnPatterns property. We’ll come back to the constructor argument in a moment. First, let’s consider the userDnPatterns property.

The userDnPatterns property is used to tell BindAuthenticator how to find a user in LDAP. It takes a list of one or more patterns that BindAuthenticator will use as the distinguished name (DN) to identify the user. In this case, we’re only using a single DN pattern, as described in figure 7.6.

The {0} in the DN pattern is a pattern argument that serves as a placeholder for the username. For example, if the username is cwagon, the DN used to bind to LDAP will be uid=cwagon,ou=motorists.

Now back to the constructor argument. The main thing that a BindAuthenticator needs to know to be able to do its job is how to access the LDAP repository. Thus, it is constructed with a constructor argument wired to initialDirContextFactory, which is declared as follows:

<bean id="initialDirContextFactory"
class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg
value="ldap://ldap.roadrantz.com:389/dc=roadrantz,dc=com"/>
</bean>
Файл:Fig76.png
Figure 7.6 For our purposes, a user’s distinguished name (DN) is broken into the user’s ID (UID) and organizational unit (OU).

DefaultInitialDirContextFactory captures all the information needed to connect to an LDAP server and produces a JNDI DirContext object. If you don’t know much about JNDI or DirContext, don’t worry about these details. Just keep in mind that BindAuthenticator uses DefaultInitialDirContextFactory to know how to get to the LDAP repository.

The constructor argument used to create DefaultInitialDirContextFactory is wired with the URL of the LDAP provider. In this case, I’ve wired it with a reference to the RoadRantz LDAP server2 and established the initial context at dc=roadrantz,dc=com. The DN used to look up the user information will be relative to this initial context.

Authenticating by comparing passwords

As an alternative to bind authentication, Spring Security also supports authentication by password comparison with PasswordComparisonAuthenticator. PasswordComparisonAuthenticator works by comparing the supplied password with a password attribute (userPassword, by default) in the user record. Here’s how it might be configured in Spring:

<bean id="authenticator"
class="org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator">
<constructor-arg ref="initialDirContextFactory" />
<property name="userDnPatterns">
<list>
<value>uid={0},ou=motorists</value>
</list>
</property>
</bean>

Notice that with the exception of the class name, this PasswordComparisonAuthenticator declaration is identical to the BindAuthenticator declaration. That’s because in their simplest forms, both are fundamentally the same. Both need an initial context factory to know how to get to the LDAP repository, and both need one or more DN patterns for locating user records.

But there are a few more properties you use to customize PasswordComparisonAuthenticator. For example, if the default userPassword attribute doesn’t suit your needs, you can override it by wiring in a new value to the passwordAttributeName property. For example, declare PasswordComparisonAuthenticator as follows to compare the password against an attribute named userCredentials:

<bean id="authenticator"
class="org.acegisecurity.providers.ldap. authenticator.PasswordComparisonAuthenticator">
<constructor-arg ref="initialDirContextFactory" />
<property name="userDnPatterns">
<list>
<value>uid={0},ou=motorists</value>
</list>
</property>
<property name="passwordAttributeName" value="userCredentials" />
</bean>

In case you’re already thinking about it, don’t try accessing the LDAP server at ldap.roadrantz.com. That address is just an example… there’s no LDAP provider there.


Another customization you may choose is how the password is encoded in LDAP. By default, PasswordComparisonAuthenticator uses Spring Security’s LdapShaPasswordEncoder to encode the password before comparison. LdapShaPasswordEncoder supports LDAP Secure Hash Algorithm (SHA) and SSHA (salted-SHA) encodings. But if these don’t suit your needs, any implementation of org.acegisecurity.providers.encoding.PasswordEncoder, including those in table 7.2, can be wired into the passwordEncoder property.

For example, should you store the password in LDAP in plain text (not advised, but possible), declare PasswordComparisonAuthenticator like this:

<bean id="authenticator"
class="org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator">
<constructor-arg ref="initialDirContextFactory" />
<property name="userDnPatterns">
<list>
<value>uid={0},ou=motorists</value>
</list>
</property>
<property name="passwordEncoder">
<bean class="org.acegisecurity.providers.encoding.? PlaintextPasswordEncoder" />
</property>
</bean>

Before we move on to the populator strategy bean, let’s make one last tweak to the initialDirContextFactory bean.

Unlike BindAuthenticator, PasswordComparisonAuthenticator doesn’t bind to LDAP using the user’s DN. Some LDAP providers allow anonymous binding, in which case the initialDirContextFactory will work as is. However, for security reasons, most LDAP providers do not allow anonymous binding, so we’ll need to provide a manager DN and password for DefaultInitialDirContextFactory to bind with:

<bean id="initialDirContextFactory"
class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg
value="ldap://ldap.roadrantz.com:389/dc=roadrantz,dc=com"/>
<property name="managerDn"
value="cn=manager,dc=roadrantz,dc=com" />
<property name="managerPassword" value="letmein" />
</bean>

When DefaultInitialDirContextFactory accesses LDAP, it will bind as the manager and act on behalf of the user when comparing the user’s password.

Now let’s configure the populator strategy bean to complete the LDAP authentication picture.

Declaring the populator strategy bean

Authenticating the user is only the first step performed by LdapAuthenticationProvider. Once the user’s identity is confirmed, LdapAuthenticationProvider must retrieve a list of the user’s granted authorities to determine what rights the user has within the application.

As with authentication, LdapAuthenticatorProvider uses a strategy object to find a user’s granted authorities from LDAP. Spring Security comes with one implementation of the LdapAuthoritiesPopulator interface: DefaultLdapAuthoritiesPopulator. Here’s how DefaultLdapAuthoritiesPopulator is configured in Spring:

<bean id="populator"
class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="initialDirContextFactory" />
<constructor-arg value="ou=groups" />
<property name="groupRoleAttribute" value="ou" />
</bean>

The first thing you’ll notice is that DefaultLdapAuthoritiesPopulator is constructed with two constructor arguments, the first of which is a reference to our old friend, initialDirContextFactory. Just like the authenticator strategy bean, the populator strategy bean needs to know how to get to the LDAP repository to retrieve the user’s granted authorities.

The second constructor argument helps DefaultLdapAuthoritiesPopulator find groups within the LDAP repository. Since an LDAP repository is hierarchical in nature, security groups could be found anywhere. This constructor argument specifies a base DN from which to search for groups. This base DN is relative to the initial context. Therefore, with the group base DN as ou=groups, we’ll be searching for groups in ou=groups,dc=roadrantz,dc=com.

Finally, the groupRoleAttribute property specifies the name of the attribute that will contain role information (which effectively translates to a user’s granted authorities). It defaults to cn, but for our example, we’ve set it to ou.

Configured this way, DefaultLdapAuthoritiesPopulator will retrieve all groups that the user is a member of—that is, all groups that have a member attribute with the user’s DN.

For example, suppose that you have an LDAP repository populated with the following LDIF:3

dn: ou=groups,dc=roadrantz,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups
dn: cn=motorists,ou=groups,dc=roadrantz,dc=com
objectClass: groupOfNames
objectClass: top
cn: motorists
description: Acegi Security Motorists
member: uid=craig,ou=people,dc=roadrantz,dc=com
member: uid=raymie,ou=people,dc=roadrantz,dc=com
ou: motorist
dn: cn=vips,ou=groups,dc=roadrantz,dc=com
objectClass: groupOfNames
objectClass: top
cn: vips
description: Acegi Security Motorists
member: uid=craig,ou=people,dc=roadrantz,dc=com
ou: vip

LDAP aficionados know LDIF to be the LDAP Data Interchange Format. It’s the standard way of representing LDAP directory content.


When the user named craig is authenticated, his granted authorities will include ROLE_MOTORIST and ROLE_VIP. But when raymie is authenticated, her granted authorities will only include ROLE_MOTORIST, because the vips group does not have her DN as a member attribute.

Note that the group name (which is in the ou attribute) is converted to uppercase and then prefixed with ROLE_. The case normalization is just a convenience that helps find a user’s authorities regardless of whether it’s lower or uppercase. You can turn off this behavior by setting the convertToUpperCase property to false.

The ROLE_ prefix is provided for the sake of RoleVoter, which we’ll talk about in section 7.3.2. If you would rather use a different role prefix, you can configure DefaultLdapAuthoritiesPopulator’s rolePrefix property however you’d like.

For example, to turn off uppercase normalization and change the role prefix to GROUP_, configure DefaultLdapAuthoritiesPopulator like this:

<bean id="populator"
class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="initialDirContextFactory" />
<constructor-arg value="ou=groups" />
<property name="groupRoleAttribute" value="ou" />
<property name="convertToUpperCase" value="false" />
<property name="rolePrefix" value="GROUP_" />
</bean>

One more tweak that you may want to make to DefaultLdapAuthoritiesPopulator is to change how it looks for members. Normally, it looks for groups whose member attribute has the user’s DN. That’s fine if your LDAP is set up to use the member attribute that way. But let’s say that instead of member, your LDAP repository uses an associate attribute to track membership. In that case, you’ll want to set the groupSearchFilter property like this:

<bean id="populator"
class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="initialDirContextFactory" />
<constructor-arg value="ou=groups" />
<property name="groupRoleAttribute" value="ou" />
<property name="convertToUpperCase" value="false" />
<property name="rolePrefix" value="GROUP_" />
<property name="groupSearchFilter" value="(associate={0})" />
</bean>

Notice that the groupSearchFilter property uses the {0} pattern argument to represent the user’s DN.

Now we’ve wired in Spring Security’s authentication processing beans to identify the user. Next let’s see how Spring Security determines whether an authenticated user has the proper authority to access the secured resource.

Controlling access

Authentication is only the first step in Spring Security. Once Spring Security has figured out who the user is, it must decide whether to grant access to the resources that it secures. We’ve configured the authentication manager from figure 7.1. Now it’s time to configure the access decision manager. An access decision manager is responsible for deciding whether the user has the proper privileges to access secured resources. Access decision managers are defined by the org.acegisecurity.AccessDecisionManager interface:

public interface AccessDecisionManager {
public void decide(Authentication authentication, Object object,
ConfigAttributeDefinition config)
throws AccessDeniedException,
InsufficientAuthenticationException;
public boolean supports(ConfigAttribute attribute);
public boolean supports(Class clazz);
}

The supports() methods consider the secured resource’s class type and its configuration attributes (the access requirements of the secured resource) to determine whether the access decision manager is capable of making access decisions for the resource. The decide() method is where the ultimate decision is made. If it returns without throwing an AccessDeniedException or InsufficientAuthenticationException, access to the secured resource is granted. Otherwise, access is denied.

Voting access decisions

Spring Security’s access decision managers are ultimately responsible for determining the access rights for an authenticated user. However, they do not arrive at their decision on their own. Instead, they poll one or more objects that vote on whether or not a user is granted access to a secured resource. Once all votes are in, the decision manager tallies the votes and arrives at its final decision.

Spring Security comes with three implementations of AccessDecisionManager, as listed in table 7.3. Each takes a different approach to tallying votes.

All of the access decision managers are configured the same in the Spring configuration file. For example, the following XML excerpt configures a UnanimousBased access decision manager:

Table 7.3 Spring Security’s access decision managers help decide whether a user is granted access by tallying votes on whether to let the user in.

Access decision manager How it decides to grant/deny access
org.acegisecurity.vote.AffirmativeBased Allows access if at least one voter votes to grant access
org.acegisecurity.vote.ConsensusBased Allows access if a consensus of voters vote to grant access
org.acegisecurity.vote.UnanimousBased Allows access if all voters vote to grant access
<bean id="accessDecisionManager"
class="org.acegisecurity.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<ref bean="roleVoter"/>
</list>
</property>
</bean>

The decisionVoters property is where you provide the access decision manager with its list of voters. In this case, there’s only one voter, which is a reference to a bean named roleVoter. Let’s see how the roleVoter is configured.

Casting an access decision vote

Although access decision voters don’t have the final say on whether access is granted to a secured resource (that job belongs to the access decision manager), they play an important part in the access decision process. An access decision voter’s job is to consider the user’s granted authorities alongside the authorities required by the configuration attributes of the secured resource. Based on this information, the access decision voter casts its vote for the access decision manager to use in making its decision.

An access decision voter is any object that implements the org.acegisecurity.vote.AccessDecisionVoter interface:

public interface AccessDecisionVoter {
public static final int ACCESS_GRANTED = 1;
public static final int ACCESS_ABSTAIN = 0;
public static final int ACCESS_DENIED = -1;
public boolean supports(ConfigAttribute attribute);
public boolean supports(Class clazz);
public int vote(Authentication authentication, Object object,
ConfigAttributeDefinition config);
}

As you can see, the AccessDecisionVoter interface is similar to that of AccessDecisionManager. The big difference is that instead of a decide() method that returns void, there is a vote() method that returns int. That’s because an access decision voter doesn’t decide whether to allow access—it only returns its vote as to whether or not to grant access.

When faced with the opportunity to place a vote, an access decision voter can vote one of three ways:


  • ACCESS_GRANTED—The voter wishes to allow access to the secured resource.
  • ACCESS_DENIED—The voter wishes to deny access to the secured resource.
  • ACCESS_ABSTAIN—The voter is indifferent.

As with most Spring Security components, you are free to write your own implementation of AccessDecisionVoter. However, Spring Security comes with RoleVoter, a useful implementation that votes when the secured resources configuration attributes represent a role. More specifically, RoleVoter participates in a vote when the secured resource has a configuration attribute whose name starts with ROLE_.

The way that RoleVoter decides on its vote is by simply comparing all of the configuration attributes of the secured resource (that are prefixed with ROLE_) with all of the authorities granted to the authenticated user. If RoleVoter finds a match, it will cast an ACCESS_GRANTED vote. Otherwise, it will cast an ACCESS_ DENIED vote.

The RoleVoter will only abstain from voting when the authorities required for access are not prefixed with ROLE_. For example, if the secured resource only requires non-role authorities (such as CREATE_USER), the RoleVoter will abstain from voting.

You can configure a RoleVoter with the following XML in the Spring configuration file:

<bean id="roleVoter"
class="org.acegisecurity.vote.RoleVoter"/>

As stated earlier, RoleVoter only votes when the secured resource has configuration attributes that are prefixed with ROLE_. However, the ROLE_ prefix is only a default. You may choose to override the default prefix by setting the rolePrefix property:

<bean id="roleVoter"
class="org.acegisecurity.vote.RoleVoter">
<property name="rolePrefix" value="GROUP_" />
</bean>

Here, the default prefix has been overridden to be GROUP_. Thus the RoleVoter will now only cast authorization votes on privileges that begin with GROUP_.

Handling voter abstinence

Knowing that any voter can vote to grant or deny access or abstain from voting, you may be wondering what would happen if all voters abstained from voting. Will the user be granted or denied access?

By default, all the access decision managers deny access to a resource if all the voters abstain. However, you can override this default behavior by setting the allowIfAllAbstain property on the access decision manager to true:

<bean id="accessDecisionManager"
class="org.acegisecurity.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<ref bean="roleVoter"/>
</list>
</property>
<property name="allowIfAllAbstain" value="true" />
</bean>

By setting allowIfAllAbstain to true, you are establishing a policy of “silence is consent.” In other words, if all voters abstain from voting, access is granted as if they had voted to grant access.

Now that you’ve seen how Spring Security’s authentication and access control managers work, let’s put them to work. In the next section you’ll learn how to use Spring Security’s collection of servlet filters to secure a web application. Later, in section 7.6, we’ll dig deep into an application and see how to use Spring AOP to apply security at the method-invocation level.

Securing web applications

Spring Security’s support for web security is heavily based on servlet filters. These filters intercept an incoming request and apply some security processing before the request is handled by your application. Spring Security comes with a handful of filters that intercept servlet requests and pass them on to the authentication and access decision managers to enforce security. Depending on your needs, you may use several of the filters listed in table 7.4 to secure your application.

Even though table 7.4 lists 17 filters provided by Spring Security, most applications will suffice with only a handful of them. Specifically, when a request is submitted to a Spring-secured web application, it will pass through at least the following four filters (as illustrated in figure 7.7):

Table 7.4 Spring Security controls access to web applications through several servlet filters.

Filter ('''org.acegisecurity.*''')
Purpose
adapters.HttpRequestIntegrationFilter Populates the security context using information from the user principal provided by the web container.
captcha. CaptchaValidationProcessingFilter Helps to identify a user as a human (as opposed to an automated process) using Captcha techniques. Captcha is a technique used to distinguish human users from automated/computer-driven users by challenging the user to identify something (typically an image) that is easily identified by a human, but difficult for a computer to make out.
concurrent.ConcurrentSessionFilter Ensures that a user is not simultaneously logged in more than a set number of times.
context. HttpSessionContextIntegrationFilter Populates the security context using information obtained from the HttpSession.
intercept.web. FilterSecurityInterceptor Plays the role of security interceptor, deciding whether or not to allow access to a secured resource.
providers.anonymous. AnonymousProcessingFilter Used to identify an unauthenticated user as an anonymous user.
securechannel. ChannelProcessingFilter Ensures that a request is being sent over HTTP or HTTPS (as the need dictates).
ui.basicauth.BasicProcessingFilter Attempts to authenticate a user by processing an HTTP Basic authentication.
ui.cas.CasProcessingFilter Authenticates a user by processing a CAS (Central Authentication Service) ticket.
ui.digestauth.DigestProcessingFilter Attempts to authenticate a user by processing an HTTP Digest authentication.
ui.ExceptionTranslationFilter Handles any AccessDeniedException or AuthenticationException thrown by any of the other filters in the filter chain.
ui.logout.LogoutFilter Used to log a user out of the application.
ui.rememberme. RememberMeProcessingFilter Automatically authenticates a user who has asked to be “remembered” by the application.
ui.switchuser. SwitchUserProcessingFilter Used to switch out a user. Provides functionality similar to Unix’s su.
ui.webapp. AuthenticationProcessingFilter Accepts the user’s principal and credentials and attempts to authenticate the user.
ui.webapp.SiteminderAuthenticationProcessingFilter Authenticates a users by processing CA/ Netegrity SiteMinder headers.
ui.x509.X509ProcessingFilter Authenticates a user by processing an X.509 certificate submitted by a client web browser.
wrapper.SecurityContextHolderAwareRequestFilter Populates the servlet request with a request wrapper.

Файл:Fig77.png
Figure 7.7 The flow of a request through Spring Security’s core filters.

(1) Due to the stateless nature of HTTP, Spring Security needs a way to preserve a user’s authentication between web requests. An integration filter is responsible for retrieving a previously stored authentication (most likely stored in the HTTP session) at the beginning of a request so that it will be ready for Spring Security’s other filters to process.

(2)' Next, one of the authentication-processing filters will determine if the request is an authentication request. If so, the pertinent user information (typically a username/ password pair) is retrieved from the request and passed on to the authentication manager to determine the user’s identity. If this is not an authentication request, the filter performs no processing and the request flows on down the filter chain.

(3) The next filter in line is the exception translation filter. The exception translation filter’s sole purpose in life is to translate AccessDeniedExceptions and AuthenticationExceptions that may have been thrown into appropriate HTTP responses. If an AuthenticationException is detected, the request will be sent to an authentication entry point (e.g., login screen). If an AccessDeniedException is thrown, the default behavior will be to return an HTTP 403 error to the browser.

(4) The last of the required filters is the filter security interceptor. This filter plays the part of the security interceptor (see section 7.1.1) for web applications. Its job is to examine the request and determine whether the user has the necessary privileges to access the secured resource. It doesn’t work alone, though. It leans heavily on the authentication manager and the access decision manager to help it grant or restrict access to the resource.


If the user makes it past the filter security interceptor, the user will be granted access to the secured web resource. Otherwise, an AccessDeniedException will be thrown and the exception translation filter will handle it appropriately.

We’ll explore each of these filters individually in more detail. But before you can start using them, you need to understand how Spring Security places a Springflavored twist on servlet filters.

Proxying Spring Security’s filters

If you’ve ever used servlet filters, you know that for them to take effect, you must configure them in the web application’s web.xml file, using the <filter> and <filter-mapping> elements. While this works, it doesn’t lend itself to configuration using dependency injection.

For example, suppose you have the following filter declared in your web.xml file:

<filter>
<filter-name>Foo</filter-name>
<filter-class>FooFilter</filter-class>
</filter>

Now suppose that FooFilter needs a reference to a Bar bean to do its job. How can you inject an instance of Bar into FooFilter?

The short answer is that you can’t. The web.xml file has no notion of dependency injection, nor is there a straightforward way of retrieving beans from the Spring application context and wiring them into a servlet filter. The only option you have is to use Spring’s WebApplicationContextUtils to retrieve the bar bean from the Spring context. For example, you might place the following in the filter’s code:

ApplicationContext ctx = WebApplicationContextUtils.
getWebApplicationContext(servletContext);
Bar bar = (Bar) ctx.getBean("bar");

The problem with this approach, however, is that you must code Spring-specific code into your servlet filter. Furthermore, you end up hard-coding a reference to the name of the bar bean.

Fortunately, Spring Security provides a better way through FilterToBeanProxy.

Proxying servlet filters

Файл:Fig78.png
Figure 7.8 FilterToBeanProxy proxies filter handling to a delegate filter bean in the Spring application context.

FilterToBeanProxy is a special servlet filter that, by itself, doesn’t do much. Instead, it delegates its work to a bean in the Spring application context, as illustrated in figure 7.8. The delegate bean implements the javax.servlet.Filter interface just like any other servlet filter, but is configured in the Spring configuration file instead of web.xml.

By using FilterToBeanProxy, you are able to configure the actual filter in Spring, taking full advantage of Spring’s support for dependency injection. The web.xml file only contains the <filter> declaration for FilterToBeanProxy. The actual FooFilter is configured in the Spring configuration file and uses setter injection to set the bar property with a reference to a Bar bean.

To use FilterToBeanProxy, you must set up a <filter> entry in the web application’s web.xml file. For example, if you are configuring a FooFilter using FilterToBeanProxy, you’d use the following code:

<filter>
<filter-name>Foo</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>
com.roadrantz.FooFilter
</param-value>
</init-param>
</filter>

Here the targetClass initialization parameter is set to the fully qualified class name of the delegate filter bean. When this FilterToBeanProxy is initialized, it will look for a bean in the Spring context whose type is FooFilter. FilterToBeanProxy will delegate its filtering to the FooFilter bean found in the Spring context:

<bean id="fooFilter"  class="com.roadrantz.FooFilter">
<property name="bar" ref="bar" />
</bean>

If a FooFilter bean isn’t found, an exception will be thrown. If more than one matching bean is found, the first one found will be used.

Optionally, you can set the targetBean initialization parameter instead of targetClass to pick out a specific bean from the Spring context. For example, you might pick out the fooFilter bean by name by setting targetBean as follows:

<filter>
<filter-name>Foo</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetBean</param-name>
<param-value>fooFilter</param-value>
</init-param>
</filter>

The targetBean initialization parameter enables you to be more specific about which bean to delegate filtering to, but requires that you match the delegate’s name exactly between web.xml and the Spring configuration file. This creates extra work for you if you decide to rename the bean. For this reason, it’s probably better to use targetClass instead of targetBean.


NOTE

It may be interesting to know that there’s nothing about FilterToBeanProxy that is specific to Spring Security or to securing web applications. You may find that FilterToBeanProxy is useful when configuring your own servlet filters. In fact, because it’s so useful, a similar filter named org.springframework.web.filter.DelegatingFilterProxy was added to Spring in version 1.2.

Finally, you’ll need to associate the filter to a URL pattern. The following <filter-mapping> ties the Foo instance of FilterToBeanProxy to a URL pattern of /* so that all requests are processed:

<filter-mapping>
<filter-name>Foo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Regardless of whether you choose targetClass or targetBean, FilterToBeanProxy must be able to access the Spring application context. This means that the Spring context has to be loaded using Spring’s ContextLoaderListener or ContextLoaderServlet (see chapter 13).

So now that you know how FilterToBeanProxy works, the burning question is: what does all of this have to do with Spring Security? I’m glad you asked.

As I mentioned earlier, Spring Security uses servlet filters in enforcing web security. Each of these filters must be injected with other beans from the Spring application context to do their job. For example, the FilterSecurityInterceptor needs to be injected with the AuthenticationManager and the AccessDecisionManager so that it can enforce security. Unfortunately, the servlet specification doesn’t make it easy to do dependency injection on servlet filters. FilterToBeanProxy solves this problem by being the “front-man” for the real filters that are configured as beans in the Spring application context.

Proxying multiple filters

Now you’re probably wondering, if FilterToBeanProxy handles requests by proxying to a Spring-configured bean, what is on the receiving end (on the Spring side)? That’s an excellent question.

Файл:Fig79.png
Figure 7.9 FilterChainProxy chains multiple filters together on behalf of FilterToBeanProxy.

Actually, the bean that FilterToBeanProxy proxies to can be any implementation of javax.servlet.Filter. This could be any of Spring Security’s filters, or it could be a filter of your own creation. But as I’ve already mentioned, Spring Security requires at least four and possibly a dozen or more filters to be configured. Does this mean that you have to configure a FilterToBeanProxy for each of Spring Security’s filters?

Absolutely not. While it’s certainly possible to add several FilterToBeanProxys to web.xml (one for each of Spring Security’s filters), that’d be way too much XML to write. To make life easier, Spring Security offers FilterToBeanProxy’s cohort, FilterChainProxy.

FilterChainProxy is an implementation of javax.servlet.Filter that can be configured to chain together several filters at once, as illustrated in figure 7.9.

FilterBeanProxy intercepts the request from the client and sends it to FilterChainProxy for handling. FilterChainProxy then passes the request through one or more filters that are configured in the Spring application context. FilterChainProxy is configured like this in the Spring application context:

<bean id="filterChainProxy"
class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=filter1,filter2,filter3
</value>
</property>
</bean>

The filterInvocationDefinitionSource property takes a String that is parsed into a scheme that FilterChainProxy will use to chain filters together. In this example, the first line tells FilterChainProxy to normalize URL patterns to lowercase before comparing them. The next line says that Apache Ant–style paths are to be used when declaring URL patterns.

Finally, one or more URL-to-filter-chain mappings are provided. Here, the /** pattern (in Ant, this means all URLs will match) is mapped to three filters. The filter configured as the filter1 bean will be the outermost filter and will receive the request first. The filter2 bean is next. And the filter3 bean will be the innermost bean and will be the last filter to receive the request before the actual secured resource is processed. When a response is returned, it flows in reverse order, from filter3 to filter1.

Configuring proxies for Spring Security

Up to now, we’ve kept the configuration of the filter proxies mostly generic. But it’s time to configure them for use in Spring Security. First up, let’s configure a FilterToBeanProxy in web.xml:

<filter>
<filter-name>Spring Security Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>

Here we’ve configured FilterToBeanProxy to proxy to any bean in the Spring context whose type is FilterChainProxy. This is perfect because, as you may have guessed, we’re going to configure a FilterChainProxy in the Spring context.

But before we leave the web.xml file, we need to configure a filter mapping for the FilterToBeanProxy:

<filter-mapping>
<filter-name>Spring Security Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

As with any servlet <filter-mapping>, the <filter-name> value must match the <filter-name> of the <filter> that is being mapped. As for the <url-pattern>, we recommend that you use /* so that all requests are piped through Spring Security and are potentially secured. Even if it isn’t necessary to secure the entire application, filtering all requests through /* will keep the web.xml configuration simple. Later, when we configure FilterSecurityInterceptor, we can choose which parts of the application should be secured and which should not.

And that’s all that’s needed in the web.xml file! Even though Spring Security uses several filters to secure a web application, we only have to configure the one FilterToBeanProxy filter in web.xml. From here on out, we’ll configure Spring Security in the Spring application context.

Speaking of the Spring application context, we’re going to need a FilterChainProxy bean to handle the requests delegated from FilterToBeanProxy. For the RoadRantz application, let’s start with the minimal Spring Security configuration by configuring a FilterChainProxy in the Spring application context (in roadrantz-security.xml) with the <bean> declaration shown in listing 7.1.

Listing 7.1 Configuring a FilterChainProxy for Spring Security

<bean id="filterChainProxy"
class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionIntegrationFilter,
authenticationProcessingFilter,
exceptionTranslationFilter,
filterSecurityInterceptor
</value>
</property>
</bean>

Here, we’ve configured FilterChainProxy to chain together four of Spring Security’s filters. We’ll explore each of these filters in more detail in a moment. First, however, it’s important to point out that if it weren’t for FilterChainProxy, we’d have to configure four different <filter> entries and eight different <filtermapping> entries in web.xml. But with FilterChainProxy, the web.xml configuration is simplified to a single <filter> and <filter-mapping> pair.

Файл:Fig710.png
Figure 7.10 Spring Security’s filters layer each other to apply security tasks.

As the request comes in from the client and makes its way toward the secured resource, it passes through each of the filters. You can think of Spring Security’s filters as different layers in the skin of an onion. Figure 7.10 shows how a request flows through each of Spring Security’s filters on its way to a secured resource.

Aside from looking a bit like the opening segment of a Looney Tunes cartoon, figure 7.10 illustrates something very important about Spring Security. Although not all of Spring Security’s filters are required for every application, it’s crucial that the ones that are used be configured in a specific order in the filterInvocationDefinitionSource property. That’s because some of the filters make assumptions about what security tasks have been performed before them. If the filters are configured out of order, they conflict with one another.

Each of the filters configured in the filterInvocationDefinitionSource property of FilterChainProxy refers to a <bean> configured in the Spring application context. Let’s follow the path of the request through each of the filters, starting with the integration filter.

Handling the security context

Have you seen the movie Finding Nemo? If so, you’ll most certainly remember that one of the main characters was a blue tang (that’s a fish, in case you didn’t know) named Dory. Many of the funniest moments in the movie were a result of Dory’s struggle with short-term memory loss. Throughout much of the movie, she would forget little things, such as where they were going or the name of Marlin, the clownfish who was looking for his son Nemo.

As it turns out, HTTP and Dory have a lot in common. You see, HTTP is a stateless protocol. That means that, like Dory, HTTP lives in the here-and-now and tends to forget things between requests. This poses a small problem for secure applications that are served over HTTP. Without something to help HTTP remember who you are, you’d have to log into an application with each request.

Fortunately, several solutions have been devised to help HTTP with its shortterm memory loss. With Java-based web applications, sessions can be used to store data between requests. With each request, stored user information can be retrieved from the session, used to process the request, and then placed back into the session so that it’s available for the next request.


The first Spring Security filter that a request must pass through is HttpSessionContextIntegrationFilter. This filter’s main job is to try to remember an authenticated user between requests. It is configured in the Spring application context like this:

<bean id="httpSessionIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>

When a request first comes in, HttpSessionContextIntegrationFilter checks to see if it can find the user’s authentication information in the session (stored there from a previous request). If so then HttpSessionContextIntegrationFilter makes the user information available for Spring Security to use in the course of the current request. At the end of the request, HttpSessionContextIntegrationFilter will deposit the user’s authentication information back into the session so that it will be available for the next request.

If HttpSessionContextIntegrationFilter finds a user’s authentication information in the session, there’s no need for the user to log in again. But if the user’s authentication can’t be found, it probably means that they haven’t logged in yet. To handle user login, we’ll need to configure an authentication-processing filter, which is the next filter configured in FilterChainProxy and the next filter we’ll discuss.

Prompting the user to log in

Файл:Fig711.png
Figure 7.11 Authentication entry points and authenticationprocessing filters work together to authenticate a web user.

When securing web applications with Spring Security, authentication is performed using a tag-team made up of an authentication entry point and an authentication-processing filter. As illustrated in figure 7.11, an authentication entry point prompts the user for login information, which is then processed by the authentication-processing filter.

An authentication entry point starts the login process by prompting the user with a chance to provide their credentials. After the user submits the requested information, an authentication-processing filter attempts to authenticate the user.

Spring Security comes with five matched pairs of authentication entry points and authentication-processing filters, as described in table 7.5.

Table 7.5 Spring Security’s authentication entry points prompt the user to log in. An authentication-processing filter processes the login request once the credentials are submitted.

Authentication entry point Authentication-processing filter
Purpose
BasicProcessingFilterEntryPoint BasicProcessingFilter Prompts the user to log in via a browser dialog using HTTP Basic authentication
AuthenticationProcessingFilterEntryPoint AuthenticationProcessingFilter Redirects the user to an HTML form-based login page
CasProcessingFilterEntryPoint CasProcessingFilter Redirects the user to login page provided by JA-SIG’s CAS single sign-on solution
DigestProcessingFilterEntryPoint DigestProcessingFilter Prompts the user to log in via a browser dialog using HTTP Digest authentication
X509ProcessingFilterEntryPoint X509ProcessingFilter Processes authentication using X.509 certificates
Файл:Fig712.png
Figure 7.12 HTTP Basic authentication uses a browser-produced login dialog box to prompt a user for their credentials. This dialog box is from the Mac OS X version of Mozilla Firefox.

Let’s take a closer look at how the authentication entry point and authenticationprocessing filter work together to authenticate a user. We’ll examine a few of Spring Security’s authentication options, starting with Spring Security’s support for HTTP Basic authentication.

Basic authentication

The simplest form of web-based authentication is known as Basic authentication. Basic authentication works by sending an HTTP 401 (Unauthorized) response to the web browser. When the browser sees this response, it realizes that the server needs the user to log in. In response, the browser pops up a dialog box to prompt the user for a username and password (see figure 7.12).

When the user submits the login, the browser sends it back to the server to perform the authentication. If authentication is successful, the user is sent to the desired target URL. Otherwise, the server may send back another HTTP 401 response and the browser will prompt the user again to log in.

Using Basic authentication with Spring Security starts with configuring a BasicProcessingFilterEntryPoint bean:

<bean id="authenticationEntryPoint"
class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
<property name="realmName" value="RoadRantz" />
</bean>

BasicProcessingFilterEntryPoint has only one property to be configured. The realmName property specifies an arbitrary String that is displayed in the login dialog box to give users some indication of what it is that they’re being asked to log into. For example, the dialog box shown in figure 7.12 asks the user to enter a username and password for the RoadRantz realm.

After the user clicks the OK button in the login dialog box, the username and password are submitted via the HTTP header back to the server. At that point, BasicProcessingFilter picks it up and processes it:

<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
<property name="authenticationManager"
ref="authenticationManager"/>
<property name="authenticationEntryPoint"
ref="authenticationEntryPoint"/>
</bean>

BasicProcessingFilter pulls the username and password from the HTTP header and sends them on to the authentication manager, which is wired in through the authenticationManager property. If authentication is successful, an Authentication object is placed into the session for future reference. Otherwise, if authentication fails, control is passed on to the authentication entry point (the BasicProcessingFilterEntryPoint wired in through the authenticationEntryPoint property) to give the user another chance.

Although Basic authentication is fine for simple applications, it has some limitations. Primarily, the login dialog box presented by the browser is neither userfriendly nor aesthetically appealing. Basic authentication doesn’t fit the bill when you want a more professional-looking login.

For the RoadRantz application, we want an eye-appealing web page that shares the same look and feel as the rest of the application. Therefore, Spring Security’s AuthenticationProcessingFilterEntryPoint is more appropriate for our needs. Let’s see how it works.

Form-based authentication

AuthenticationProcessingFilterEntryPoint is an authentication entry point that prompts the user with an HTML-based login form. For the RoadRantz application, we’ll configure it in roadrantz-security.xml as follows:

<bean id="authenticationEntryPoint"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.htm" />
<property name="forceHttps" value="true" />
</bean>

AuthenticationProcessingFilterEntryPoint is configured here with two properties. The loginFormUrl property is set to a URL (relative to the web application’s context) that will display the login page. The forceHttps property is set to true to force the login page to be displayed securely over HTTPS, even if the original request was made over HTTP.

Here we’ve set loginFormUrl to /login.htm. loginFormUrl can be configured with any URL that takes the user to an HTML form for login. In the case of the RoadRantz application, /login.htm is ultimately associated with a Spring MVC UrlFilenameViewController that displays the login page. Figure 7.13 shows what the RoadRantz login page might look like.

Regardless of how the login page is displayed, it’s important that it contain an HTML form that resembles the following:

<form method="POST" action="j_acegi_security_check">
<b>Username: </b><input type="text" name="j_username"><br>
<b>Password: </b><input type="password" name="j_password"><br>
<input type="submit" value="Login">
</form>

The login form must have two fields named j_username and j_password in which the user will enter the username and password, respectively. That’s because those are the field names expected by AuthenticationProcessingFilter. As for the form’s action attribute, it has been set to j_acegi_security_check, which will be intercepted by AuthenticationProcessingFilter.

AuthenticationProcessingFilter is a filter that processes authentication based on the username and password information given to it in the j_username and j_password parameters. It is configured in roadrantz-security.xml as follows:

<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="filterProcessesUrl"
value="/j_acegi_security_check" />
<property name="authenticationFailureUrl"
value="/login.htm?login_error=1" />
<property name="defaultTargetUrl" value="/" />
<property name="authenticationManager"
ref="authenticationManager"/>
</bean>

Файл:Fig713.png
Figure 7.13 The RoadRantz login page is found at /login.htm, which is ultimately handled by Spring MVC’s UrlFilenameViewController.

The filterProcessesUrl property tells AuthenticationProcessingFilter which URL it should intercept. This is the same URL that is in the login form’s action attribute. It defaults to /j_acegi_security_check, but I’ve explicitly defined it here to illustrate that you can change it if you’d like.

The authenticationFailureUrl property indicates where the user will be sent should authentication fail. In this case, we’re sending them back to the login page, passing a parameter to indicate that authentication failed (so that an error message may be displayed).

Under normal circumstances, when authentication is successful, AuthenticationProcessingFilter will place an Authentication object in the session and redirect the user to their desired target page. The defaultTargetUrl property defines what will happen in the unusual circumstance where the target URL isn’t known. This could happen if the user navigates directly to the login screen without first attempting to access a secured resource.

Finally, the authenticationManager property is wired with a reference to an authenticationManager bean. Just like all other authentication-processing filters, the form-based AuthenticationProcessingFilter relies on an authentication manager to help establish the user’s identity.

Now we have an authentication processing filter and authentication entry point defined in the Spring configuration, ready for users to log in. But there’s one loose end left to tie up. The authentication-processing filter is wired into the FilterChainProxy, but you’re probably wondering what is supposed to be done with the authentication entry point. What part of Spring Security uses the authentication entry point to prompt the user for login?

I’ll answer that question for you soon. But first, we’ll need to look at the exception translation filter, the next filter in line to handle a secured request.

Handling security exceptions

In the course of providing security, any of Spring Security’s filters may throw some variation of AuthenticationException or AccessDeniedException. AuthenticationException, for example, will be thrown if, for any reason, the user cannot be authenticated. This could be because the user provided an invalid username/ password pair. Or it could even mean that the user hasn’t even attempted to log in yet. Even if the user is successfully authenticated, they may not be granted the authority necessary to visit certain secured pages. In that case, AccessDeniedException will be thrown.

Without anything to handle Spring Security’s AuthenticationException or AccessDeniedException, they’d flow up to the servlet container and be displayed in the browser as a really ugly stack trace. It goes without saying that this is less than ideal. We’d prefer to handle such exceptions in a more graceful manner.

That’s where ExceptionTranslationFilter comes in. ExceptionTranslationFilter is configured at a level just outside of FilterSecurityInterceptor so that it will have a chance to catch the exceptions that may be thrown by FilterSecurityInterceptor. ExceptionTranslationFilter is configured in Spring as follows:

<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint"
ref="authenticationEntryPoint" />
</bean>

ExceptionTranslationFilter catches the exceptions thrown from FilterSecurityInterceptor… but what does it do with them?

Notice that ExceptionTranslationFilter is injected with a reference to the authentication entry point. If ExceptionTranslationFilter catches an AuthenticationException, it means that the user hasn’t been successfully authenticated. In that case, the user is sent to the authentication entry point configured in the authenticationEntryPoint property to try to log in.

Handling authorization exceptions

An AccessDeniedException indicates that the user has been authenticated but has not been granted sufficient authority to access the resource that has been requested. In that case, an HTTP 403 error is returned to the browser. The HTTP

403 error means “forbidden” and indicates that the user isn’t allowed to access a requested resource.

By default, ExceptionTranslationFilter uses an AccessDeniedHandlerImpl to deal with AccessDeniedExceptions. Unless otherwise configured, AccessDeniedHandlerImpl only sends an HTTP 403 error to the browser. Unfortunately, an HTTP 403 error is usually displayed in a user-unfriendly way in the browser.

But we can configure our own AccessDeniedHandlerImpl that will forward the user to a nicer-looking error page when AccessDeniedException is caught. The following XML configures an AccessDeniedHandlerImpl that sends the user to an error page at the URL /error.htm:

<bean id="accessDeniedHandler"
class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage" value="/error.htm" />
</bean>

All that’s left to do is to wire this accessDeniedHandler into the ExceptionTranslationFilter:

<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint"
ref="authenticationEntryPoint" />
<property name="accessDeniedHandler"
ref="accessDeniedHandler" />
</bean>

We’ve now declared three out of the four security filters required by Spring Security. The three filters configured thus far are the tumblers in Spring Security’s lock. Now it’s time to configure FilterSecurityInterceptor, the latch that decides whether or not to allow access to a web resource.

Enforcing web security

Whenever a user requests a page within a web application, that page may or may not be a page that needs to be secure. In Spring Security, a filter security interceptor handles the interception of requests, determining whether a request is secure and giving the authentication and access decision managers a chance to verify the user’s identity and privileges. It is declared in the Spring configuration file as follows:

<bean id="filterSecurityInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"
ref="authenticationManager" />
<property name="accessDecisionManager"
ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/editProfile.htm=ROLE_MOTORIST
</value>
</property>
</bean>

FilterSecurityInterceptor plays the part of the security interceptor (as described in section 7.1.1) for web applications. When a request comes in for a resource (likely a web page or Spring MVC controller), FilterSecurityInterceptor will perform several checks to see whether the user is allowed to access the resource:


  • Has the user been authenticated? If not, FilterSecurityInterceptor will throw an AuthenticationException (which will be handled by the exception translation filter, which will be handled by the exception translation filter).
  • Is the requested resource secured? The objectDefinitionSource property defines which resources are to be secured and what privileges are required to access them. If the request’s URL matches one of the URL patterns in objectDefinitionSource then the resource is secure.
  • Has the user been granted privileges that are sufficient for accessing the resource? FilterSecurityInterceptor will compare the user’s granted privileges with those declared as being required for the resource. If the user’s privileges are sufficient, the request will be granted. If not, FilterSecurityInterceptor will throw an AccessDeniedException (that will be handled by the exception translation filter).

FilterSecurityInterceptor doesn’t work alone when making these decisions. That’s why it’s wired with a reference to an authentication manager and a reference to an access decision manager.

As for the objectDefinitionSource property, this is how we declare which resources are secured and what privileges are required to access them. The first line indicates that we want all URL patterns to be normalized to lowercase before comparison (otherwise, the URL patterns will be case sensitive). The next line indicates that we’ll be using Ant-style paths for declaring the URL patterns.

From the third line on, we can declare one or more URL patterns and what privilege is required to access each. In this case, we have one URL pattern that ensures that only authenticated users with the ROLE_MOTORIST role are allowed to visit the editProfile.htm page.

If the user has been authenticated and has appropriate privileges, FilterSecurityInterceptor will let the request continue. If, however, FilterSecurityInterceptor determines that the user doesn’t have adequate privileges, either an AuthenticationException or an AccessDeniedException will be thrown.

At this point, we’ve configured the basic filters required to secure the RoadRantz application with Spring Security. But there’s one more filter that, although not required, comes in handy for guaranteeing that secure information be transmitted securely in web requests. Next, I’ll show you how to ensure that secure requests are carried over HTTPS using Spring Security’s ChannelProcessingFilter.

Ensuring a secure channel

The letter “s” is the most important letter on the Internet. Anyone who has spent more than five minutes surfing the Web knows that most web pages are associated with URLs that start with “https://”. That’s because most web pages are requested and sent using the HTTP protocol.

Файл:Fig714.png
Figure 7.14 ChannelProcessingFilterredirects HTTP requests as HTTPS (and vice versa), ensuring the proper security for each request.

HTTP is perfect for most pages, but is woefully insufficient when confidential information is passed around on the Internet. Information sent over HTTP can be easily intercepted and read by nefarious hackers who will use it for their illpurposed plans.

When information must be sent confidentially, the letter “s” goes to work. For those pages, you’ll find that the URL begins with “https://” instead of simply “https://”. With HTTPS, information is still sent using HTTP, but is sent on a different port and is encrypted so that if it is intercepted, it can’t be read by anyone for whom it isn’t meant.

Unfortunately, the problem with HTTPS is that the burden of ensuring that a page be transferred over HTTPS belongs to whoever writes the link to the secure page. In other words, for a page to be secured with encrypted HTTPS, it must be linked to with a URL that starts with “https://”. Without that one little “s” in there, the page will be sent unencrypted over HTTP.

Because it’s too easy to omit the all-important “s,” Spring Security offers a foolproof way to ensure that certain pages be transferred using HTTPS, regardless of which URL was used to link to them. As illustrated in figure 7.14, ChannelProcessingFilter is a Spring Security filter that intercepts a request, checks to see if it needs to be secure and, if so, calls “s” to work by redirecting the request to an HTTPS form of the original request URL.

We’ve configured a ChannelProcessingFilter for the RoadRantz application in roadrantz-security.xml as follows:

<bean id="channelProcessingFilter"
class="org.acegisecurity.securechannel.ChannelProcessingFilter">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/login.htm=REQUIRES_SECURE_CHANNEL
/j_acegi_security_check*=REQUIRES_SECURE_CHANNEL
/**=REQUIRES_INSECURE_CHANNEL
</value>
</property>
<property name="channelDecisionManager"
ref="channelDecisionManager" />
</bean>

The filterInvocationDefinitionSource property is configured to tell ChannelProcessingFilter which pages should be secured with HTTPS and which should not. It is configured with one or more URL patterns that are mapped to be either secure or not secure.

But before the URL patterns appear, we must set a few ground rules for how the URLs will be handled. The first line contains CONVERT_URL_TO_LOWERCASE_ BEFORE_COMPARISON to tell Spring Security to normalize all URLs before comparing them to the URL patterns that will follow. The second line contains PATTERN_TYPE_APACHE_ANT, which indicates that the URL patterns will be presented using Apache Ant–style paths.

Each line that follows maps a URL pattern to its security requirements. In the RoadRantz application, the login page must be secure (so that nobody can intercept a user’s password). Therefore,/login.htmismappedto REQUIRES_SECURE_CHANNEL, indicating that it should be sent over HTTPS. Likewise, information sent to the URL that processes logins must also be encrypted. As you’ll see soon, Spring Security’s AuthenticationProcessingFilter responds to /j_acegi_security_check, so this URL pattern is also set to REQUIRES_SECURE_CHANNEL.

None of the other pages in the RoadRantz application require encryption. So the /** URL pattern (which, in Ant path syntax indicates all URLs) is set to REQUIRES_INSECURE_CHANNEL, specifying that all other pages must be sent over plain, unsecured HTTP. Notice that these pages require an insecure channel. That means that if these pages are accessed over HTTPS, ChannelProcessingFilter will redirect them to be sent over HTTP.

Managing channel decisions

While ChannelProcessingFilter handles the task of redirecting the HTTP requests to HTTPS (and vice versa), it doesn’t necessarily need to redirect every request. Thus, it depends on a ChannelDecisionManagerImpl (wired into the channelDecisionManager property) to weigh the decision as to whether or not a request should be redirected. The ChannelDecisionManagerImpl is configured as follows:

<bean id="channelDecisionManager"
class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<bean class="org.acegisecurity.securechannel.SecureChannelProcessor"/>
<bean class="org.acegisecurity.securechannel.InsecureChannelProcessor"/>
</list>
</property>
</bean>

Here we’ve configured ChannelDecisionManagerImpl with two channel processors—one for secure channel (HTTPS) processing and one for insecure (HTTP) channel processing.

Before we move past Spring Security’s support for web-based security, let’s see how to use Spring Security’s tag library to enforce security rules within a page in the web application.

View-layer security

In most applications, there are certain elements that should only be displayed to a certain class of users. As you’ve already seen, Spring Security’s filters prevent certain pages from being presented to users who are not granted a specific set of authorities.

But filters provide a coarse-grained security, limiting access at the request level. In some cases, you may want more fine-grained control over what the user is allowed to see. Maybe all users of an application will be allowed to see a certain page, but only users who are granted special authority may see certain elements on that page.

To provide fine-grained security in web applications, Spring Security comes with a small, but powerful, JSP tag library. This tag library provides only three tags, as listed in table 7.6.

Table 7.6 Spring Security’s JSP tags for view-layer security.

Tag name
What it does
<authz:acl> Conditionally renders the tag body if the user has been granted one of a set of specific permissions to a domain object
<authz:authentication> Renders information about the user
<authz:authorize> Conditionally renders the tag body if the user has been (or has not been) granted certain authorities

To use these tags in a JSP page, the tag library must be imported using the JSP

<%@ taglib prefix="authz" uri="https://acegisecurity.org/authz" %>

Let’s have a look at how to apply these tags, starting with <authz:authorize>.

Conditionally rendering content

The most useful of Spring Security’s JSP tags is the <authz:authorize> tag. This tag effectively performs an if statement, evaluating whether or not the current user has been granted proper authority to view certain content. If so, the body of the tag will be rendered. Otherwise, the tag’s content will be ignored.

To illustrate, let’s add a welcome message and a link to logoff from the RoadRantz application. It doesn’t make much sense to welcome a user who isn’t authenticated and even less sense to offer them a logoff link. Therefore, we want to be certain that the user has been granted certain privileges before they’re presented with that information. Using the ifAllGranted attribute of the <authz: authorize> tag, we might add the content to the view using this JSP snippet:

<authz:authorize ifAllGranted="ROLE_MOTORIST,ROLE_VIP">
Welcome Motorist!<br/>
<a href="j_acegi_logout">Logoff</a>
</authz:authorize>

Because the ifAllGranted attribute was used, the content contained in the body of the tag will only be rendered if the motorist has been granted both ROLE_MOTORIST ''and ''ROLE_VIP privileges. However, that is too restrictive, because while all users are granted ROLE_MOTORIST privileges, only a select few are granted ROLE_VIP privileges. So maybe the ifAnyGranted attribute would be more appropriate:

<authz:authorize ifAnyGranted="ROLE_MOTORIST,ROLE_VIP">
Welcome Motorist!<br/>
<a href="j_acegi_logout">Logoff</a>
</authz:authorize>

In this case, the user must be granted either ROLE_MOTORIST ''or ''ROLE_VIP privileges for the welcome message and logoff link to be displayed.

Although it may seem obvious, it is worth pointing out that if you are only checking for a single privilege, the choice between ifAllGranted and ifAnyGranted is moot. Either attribute will work equally well when only one privilege is listed in the attribute value.

The final attribute option you have is ifNotGranted, which only renders the tag’s content if the user has not been granted any of the authorities listed. For example, we’d use this to prevent content from being rendered to anonymous users:

<authz:authorize ifNotGranted="ROLE_ANONYMOUS">
<p>This is super-secret content that anonymous users aren't
allowed to see.</p>
</authz:authorize>

These three attributes cover a lot of ground by themselves. But some real security magic is conjured up when they’re used together. When used together, these attributes are evaluated by logically AND’ing them together. For instance, consider the following:

<authz:authorize ifAllGranted="ROLE_MOTORIST"
ifAnyGranted="ROLE_VIP,ROLE_FAST_LANE"
ifNotGranted="ROLE_ADMIN">
<p>Only special users see this content.</p>
</authz:authorize>

Used together this way, the tag’s content will only be rendered if the user has been granted ROLE_MOTORIST privileges and either ROLE_VIP or ROLE_FAST_LANE privileges, and is not granted ROLE_ADMIN privileges. Even though this is a contrived example, you can imagine how powerful the <authz:authorize> tag can be by combining its three attributes.

Controlling what the user can see is only one facet of Spring Security’s JSP tag library. Now let’s see how to use Spring Security tags to display information about an authenticated user.

Displaying user authentication information

In the previous section, we added a welcome message to the RoadRantz application for authorized users. For simplicity’s sake, the message was “Welcome Motorist!” That’s a good start, but we’d like to make the application more personal by displaying the user’s login name instead of “Motorist.”

Fortunately, the user’s login is typically carried around in the object that is returned from the user’s Authentication.getPrincipal() method. All we need is a convenient way to access the principal object in the JSP. That’s what the <authz:authentication> tag is for.

The <authz:authentication> tag renders properties of the object that is returned from Authentication.getPrincipal() to JSP output. Authentication.getPrincipal() typically returns an implementation of Spring Security’s org.acegisecurity.userdetails.UserDetails interface, which includes a getUsername() method. Therefore, all we need to do to display the username property of the UserDetails object is to add the following <authz:authentication> tag:

<authz:authorize ifAnyGranted="ROLE_MOTORIST,ROLE_VIP">
Welcome <authz:authentication operation="username"/>
</authz:authorize>

The operation attribute is a bit misleading, seeming to indicate that its purpose is to invoke a method. It’s true that it invokes a method, but more specifically, it invokes the getter method of the property whose name is specified in the operation attribute.

By default, the first letter of the operation value is capitalized and the result is

prepended with get to produce the name of the method that will be called. In this case, the getUsername() method is called and its return value is rendered to the JSP output.

Now you’ve seen how to secure web applications using Spring Security’s filters. Before we are done with Spring Security, however, let’s have a quick look at how to secure method invocations using Spring Security and AOP.

Securing method invocations

Whereas Spring Security used servlet filters to secure web requests, Spring Security takes advantage of Spring’s AOP support to provide declarative method-level security. This means that instead of setting up a SecurityEnforcementFilter to enforce security, you’ll set up a Spring AOP proxy that intercepts method invocations and passes control to a security interceptor.

Creating a security aspect

Probably the easiest way to set up an AOP proxy is to use Spring’s BeanNameAutoProxyCreator and simply list the beans that you’ll want secured.5 For instance, suppose that you’d like to secure the courseService and billingService beans:

<bean id="autoProxyCreator" class=
"org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>securityInterceptor</value>
</list>
</property>
<property name="beanNames">
<list>
<value>courseService</value>
<value>billingService</value>
</list>
</property>
</bean>

Here the autoproxy creator has been instructed to proxy its beans with a single interceptor, a bean named securityInterceptor. The securityInterceptor bean is configured as follows:

<bean id="securityInterceptor"
class="org.acegisecurity.intercept.method.MethodSecurityInterceptor">
<property name="authenticationManager">
<ref bean="authenticationManager"/>
</property>
<property name="accessDecisionManager">
<ref bean="accessDecisionManager"/>
</property>
<property name="objectDefinitionSource">
<value>
com.springinaction.springtraining.service.
CourseService.createCourse=ROLE_ADMIN
com.springinaction.springtraining.service.
CourseService.enroll*=ROLE_ADMIN,ROLE_REGISTRAR
</value>
</property>
</bean>

This is only a suggestion. If you prefer one of the other mechanisms for proxying beans (as discussed in chapter 4), such as ProxyFactoryBean or DefaultAdvisorAutoProxyCreator, you are welcome to use those here instead.


MethodSecurityInterceptor does for method invocations what FilterSecurityInterceptor does for servlet requests. That is, it intercepts the invocation and coordinates the efforts of the authentication manager and the access decision manager to ensure that method requirements are met.

Notice that the authenticationManager and accessDecisionManager properties are the same as for FilterSecurityInterceptor. In fact, you may wire the same beans into these properties as you did for FilterSecurityInterceptor.

MethodSecurityInterceptor also has an objectDefinitionSource property just as FilterSecurityInterceptor does. But, although it serves the same purpose here as with FilterSecurityInterceptor, it is configured slightly differently. Instead of associating URL patterns with privileges, this property associates method patterns with privileges that are required to invoke the method.

Файл:Fig715.png
Figure 7.15 Method security rules are defined by mapping a fully qualified class name and method to the privileges required to execute that method. Wildcards may be used when specifying the method.

A method pattern (see figure 7.15) includes the fully qualified class name and the method name of the method(s) to be secured. Note that you may use wildcards at either the beginning or the end of a method pattern to match multiple methods.

When a secured method is called, MethodSecurityInterceptor will determine whether the user has been authenticated and has been granted the appropriate authorities to call the method. If so, the call will proceed to the target method. If not, an AcegiSecurityException will be thrown. More specifically, an AuthenticationException will be thrown if the user cannot be authenticated. Or, if the user hasn’t been granted authority to make the call, an AccessDeniedException will be thrown.

In keeping with Spring’s exception philosophy, AcegiSecurityException is an unchecked exception. The calling code can either catch or ignore the exception.

Writing method security attributes in the Spring configuration file is only one way to declare method-level security. Now let’s look at how to use Jakarta Commons Attributes to declare security attributes.

Securing methods using metadata

As with transactions and handler mappings, the first thing you must do is declare a metadata implementation to tell Spring how to load metadata. If you haven’t already added a CommonsAttributes bean to your application context, add one now:

<bean id="attributes"
class="org.springframework.metadata.commons.CommonsAttributes"/>

Next, you must declare an object definition source. In section 7.6.1, you defined an object definition source by setting the objectDefinitionSource property with a String that mapped security attributes to methods. But this time you’re going to declare security attributes directly in the secured object’s source code. Spring Security’s MethodDefinitionAttributes is an object definition source that retrieves its security attributes from the secured object’s metadata:

<bean id="objectDefinitionSource" 
class="org.acegisecurity.intercept.method.MethodDefinitionAttributes">
<property name="attributes"><ref bean="attributes"/></property>
</bean>

The attributes property of MethodDefinitionAttributes is wired with a reference to the attributes bean so that it will know to pull security attributes using Jakarta Commons Attributes.6

Now that the objectDefinitionSource is configured, wire it into the objectDefinitionSource property of MethodSecurityInterceptor (replacing the String definition from section 7.6.1):

<bean id="securityInterceptor"
class="org.acegisecurity.intercept.method. MethodSecurityInterceptor">

<property name="objectDefinitionSource">
<ref bean="objectDefinitionSource"/>
</property>
</bean>

When Spring supports JSR-175 annotations, you will wire the attributes property with a different metadata implementation.


Now you’re ready to start tagging your code with security attributes. The only security attribute you need to know is SecurityConfig, which associates a privilege with a method. For example, the following snippet of code shows how to tag the enrollStudentInCourse() method from CourseService to require either ROLE_ADMIN or ROLE_REGISTRAR privileges:

/**
* @@org.acegisecurity.SecurityConfig("ROLE_ADMIN")
* @@org.acegisecurity.SecurityConfig("ROLE_REGISTRAR")
*/

public void enrollStudentInCourse(Course course,
Student student) throws CourseException;

Declaring these security attributes on enrollStudentInCourse() is equivalent to the declaration of the objectDefinitionSource as defined in section 7.6.1.

Summary

Security is a very important aspect of many applications. Spring Security provides a mechanism for securing your applications that is based on Spring’s philosophy of loose coupling, dependency injection, and aspect-oriented programming.

You may have noticed that this chapter presented very little Java code. We hope you weren’t disappointed. The lack of Java code illustrates a key strength of Spring Security—loose coupling between an application and its security. Security is an aspect that transcends an application’s core concerns. Using Spring Security, you are able to secure your applications without writing any security code directly into your application code.

Another thing you may have noticed is that much of the configuration required to secure an application with Spring Security is ignorant of the application that it is securing. The only Spring Security component that really needs to know any specifics about the secured application is the object definition source where you associate a secured resource with the authorities required to access the resource. Loose coupling runs both ways between Spring Security and its applications.

Wikijava.org.ua-1px.png

]]>
Книги по Java https://linexp.ru?id=4726 Wed, 29 Jun 2022 14:03:24 GMT
<![CDATA[Глава 9 Spring in Action 2th edition]]>

Building contract-first web services in Spring

This chapter covers


  • Defining XML service contracts
  • Creating document-centric web services
  • Marshaling and unmarshaling XML messages
  • Building template-based web service clients

Imagine that it’s the weekend and you’ve got a trip planned. Before you hit the road, you stop by your bank to deposit your paycheck and to pick up some spending cash.

This is not an unusual scenario, but what makes it interesting is that you bank at an unusual bank. When you walk in the door, there are no tellers to help you. Instead, you have full access to handle the transaction yourself. You have direct access to the ledger and to the vault, allowing you to handle all of the minute details of the transaction on your own. So, you perform the following tasks:


1 - You place your signed paycheck in a box designated for deposited checks.
2 - You edit your account’s ledger, incrementing the balance by the amount on the check.
3 - You take $200 from the vault and place it in your pocket.
4 - You edit your account’s ledger, decrementing the balance by $200.
5 - As a thank-you for all of the hard work you did, you pay yourself a service fee by pocketing another $50 bill on the way out the door.

Whoa! Steps 1–4 seem to be on the up and up. But isn’t step 5 a bit odd?

The problem (if that’s what you want to call it) with this bank is that they trust their customers with too much direct access to the internal workings of the bank. Instead of providing an appropriate interface to the inner workings of the bank (commonly known as a “teller”), they give you full access to the inner workings to do as you please. Consequently, you are able to perform an unrecorded and questionable withdrawal.

As nice as this is for the customer, most banks don’t work that way (if your bank really does allow you this kind of access, please email me—I’d really like to start banking there!). Most banks have tellers, ATM machines, and websites to allow you to manipulate your account. These interfaces to the bank are customer-facing abstractions to the vault and the ledger. While they may provide service with a smile, they only allow you to perform activities that fit within the bank’s business model.

Likewise, most applications do not allow direct access to the internal objects that make up the application. Take web applications, for instance. In a Spring MVC-based web application (which we’ll look at when we get to chapter 13), users interact with the application through controllers. Behind the scenes, there may be dozens or even hundreds of objects that perform the core tasks of the application. But the user is only allowed to interact with the controllers, which, in turn, interact with the back-end objects.


In the previous chapter, we saw that XFire is a quick and easy way to develop web services using remote exporters. But when we export an application bean as a web service, we’re exposing the application’s internal API, which carries with it some consequences: as I alluded to in the banking scenario, you must be careful not to accidentally expose too much of your application’s internal API. Doing so may give your web service clients more access to the inner workings of your application than they need.

In this chapter, you’ll learn an alternative way of building web services using the Spring-WS framework. We’ll separate the service’s external contract from the application’s internal API, and we’ll focus on sending messages between clients and the service, not on invoking remote methods.

I won’t deceive you: building web services with Spring-WS is not as simple as exporting them with XFire. However, I think you’ll find that it isn’t that much more difficult and that the architectural advantages that Spring-WS affords make it well worth considering.

Introducing Spring-WS

Spring Web Services (or Spring-WS, for short) is an exciting new subproject of Spring that is focused on building contract-first web services. What are contract-first web services? It might be easier to answer that question by first talking about their antithesis: contact-last web services.

In chapter 8 (see section 8.5.1), we used XFire to export bean functionality as a remote web service. We started by writing some Java code (the service implementation). Then we configured it as a <bean> in Spring. Finally, we used XFire’s XFireExporter to turn it into a web service. We never had to explicitly define the service’s contract (WSDL and XSD). Instead, XFire automatically generated the contract after the service was deployed. In short, the contract was the last thing defined, thus the designation of “contract-last.”

Contract-last web services are a popular approach to web service development for one basic reason: they’re easy. Most developers don’t have the intestinal fortitude required to understand WSDL, SOAP, and XML Schema (XSD). In the contract-last approach, there’s no need to manipulate complex WSDL and XSD files. You simply write a service class in Java and ask the web service framework to “SOAP-ify” it. If a web services platform such as XFire is willing to cope with the web services acronyms then why should we worry ourselves with it?

But there’s one small gotcha: when a web service is developed contract last, its contract ends up being a reflection of the application’s internal API. Odds are that your application’s internal API is far more volatile than you (or your service’s clients) would like the external API to be. Changes to the internal API will mean changes to your service’s contract, which will ultimately require changes in the clients that are consuming your service. A clever refactoring today may result in a new service contract tomorrow.

This leads to the classic web services versioning problem. It’s much easier to change a web service’s contract than to change the clients that consume that service. If your web service has 1,000 clients and you change your service’s contract then 1,000 clients will be broken until they are changed to adhere to the new contract. A common solution to this problem is to maintain multiple versions of a service until all clients have upgraded. This, however, would multiply maintenance and support costs, as you would have to support multiple versions of the same service.

A better solution is to avoid changing the service’s contract. And when the contract must be changed, the changes shouldn’t break compatibility with previous versions. But this can be difficult to do when the service’s contract is automatically generated.

In short, the problem with contract-last web services is that the service’s most important artifact, the contract, is treated as an afterthought. The focus of a contract-last web service is on how the service should be implemented and not on what it should do.

The solution to contract-last’s problems is to flip it on its head—create the contract first and then decide how it should be implemented. When you do, you end up with contract-first web services. The contract is written with little regard for what the underlying application will look like. This is a pragmatic approach, because it emphasizes what is expected of the service and not how it will be implemented.

You’re probably getting an uneasy feeling about now. It could be that unusually large burrito that you had for lunch... or it could be that you’re terrified that we’re going to have to create a WSDL file by hand.

Don’t worry. It’s not going to be as bad as you think. Along the way, I’ll show you several tricks that make it easy to create the service contract. (If that doesn’t make you feel better, I suggest you take an antacid and cut back on the spicy food at lunch.)

The basic recipe for developing a contract-first web service with Spring-WS appears in table 9.1.

Table 9.1 The steps for developing a contract-first web service.

Step
Action
What we’ll do
1 Define the service contract. This involves designing sample XML messages that will be processed by our web service. We’ll use these sample messages to create XML Schema that will later be used to create WSDL.
2 Write a service endpoint. We’ll create classes that will receive and process the messages sent to the web service.
3 Configure the endpoint and Spring-WS infrastructure. We’ll wire up our service endpoint along with a handful of Spring-WS beans that will tie everything together.

To demonstrate Spring-based web services, we’re going to build a poker hand evaluation service. Figure 9.1 illustrates the requirements for this web service: given five cards, identify the poker hand in question.

Since we’re creating a contract-first web service, it’s only logical that the first thing we should do is define the service contract. Let’s get started.

Файл:Fig91.png
Figure 9.1 We’ll build a poker hand evaluation web service. Given a poker hand made up of five cards, the web service will determine what kind of poker hand was dealt.

Defining the contract (first!)

The single most important activity in developing a contract-first web service is defining the contract itself. When defining the contract, we’ll define the messages that are sent to and received from the service, with no regard for how the service is implemented or how the messages will be handled.

Even though the topic of this chapter is Spring-WS, you’ll find that this section is remarkably Spring free. That’s because the contract of a web service should be defined independent of the implementation of the service. The focus is on what needs to be said, not how it needs to be done. We’ll tie this all into Spring-WS starting in section 9.3. But for now, the techniques described in this section are applicable to contract-first services in general, regardless of the underlying framework.

A contract-first view of web services places emphasis on the messages that are sent to and received from services. Therefore, the first step in defining a service’s contract is determining what the messages will look like. We’ll start by creating sample XML messages for our web services that we’ll use to define the service contract.

Creating sample XML messages

In simple terms, our poker hand evaluation service takes a poker hand made up of five cards as input and produces a poker hand designation (e.g., Full House, Flush, etc.) as output. Writing a sample input message for the service as XML might look a little like this:

<EvaluateHandRequest
xmlns="https://www.springinaction.com/poker/schemas">
<card>
<suit>HEARTS</suit>
<face>TEN</face>
</card>
<card>
<suit>SPADES</suit>
<face>KING</face>
</card>
<card>
<suit>HEARTS</suit>
<face>KING</face>
</card>
<card>
<suit>DIAMONDS</suit>
<face>TEN</face>
</card>
<card>
<suit>CLUBS</suit>
<face>TEN</face>
</card>
</EvaluateHandRequest>

That’s fairly straightforward, isn’t it? There are five <card> elements, each with a <suit> and a <face>. That pretty much describes a poker hand. All of the <card> elements are contained within an <EvaluateHandRequest> element, which is the message we’ll be sending to the service.

As simple as the input message was, the output message is even simpler:

<EvaluateHandResponse
xmlns="https://www.springinaction.com/poker/schemas">
<handName>Full House</handName>
</EvaluateHandResponse>

The <EvaluateHandResponse> message simply contains a single <handName> element that holds the designation of the poker hand.

These sample messages will serve as the basis for our service’s contract. And, although this may bring about some disbelief on your part, you should know that by defining these sample messages, we’ve already finished the hardest part of designing the service contract. No kidding.

Forging the data contract

Now we’re ready to create the service contract. Before we do that, however, let’s conceptually break the contact into two parts:


  • The data contract will define the messages going in and out of the service. In our example, this will include the schema definition of the <EvaluateHandRequest> and <EvaluateHandResponse> messages.
  • The operational contract will define the operations that our service will perform. Note that a SOAP operation does not necessarily correspond to a method in the service’s API.

Both of these contract parts are typically (but not necessarily) defined in a single WSDL file. The WSDL file usually contains an embedded XML Schema that defines the data contract. The rest of the WSDL file defines the operational contract, includingone or more <wsdl:operation>elementswithin the <wsdl:binding> element.

Файл:Fig92.png
Figure 9.2 Trang is an XSD inference tool that makes simple work of producing XML Schema from sample XML files.

Don’t worry yourself too much with the details of that last paragraph. I promised that creating the contract would be easy, so there’s no need for you to know the details of what goes into a WSDL file. The key point is that there are two distinct parts of the contract.

The data contract is defined using XML Schema (XSD). XSD allows us to precisely define what should go into a message. Not only can we define what elements are in the message, but we can also specify the types of those messages and place constraints on what data goes into the message.

Although it’s not terribly difficult to write an XSD file by hand, it’s more work than I care to do. So, I’m going to cheat a little by using an XSD inference tool. An XSD inference tool examines one or more XML files and, based on their contents, produces an XML schema that the XML files can be validated against.

Several XSD inference tools are available, but one that I like is called Trang. Trang is a command-line tool (available from www.thaiopensource.com/relaxng/ trang.html) that takes XML as input and produces an XSD file as output (see figure 9.2). Trang is Java based and thus can be used anywhere there’s a JVM. As the URL implies, Trang is useful for generating RELAX NG schemas (an alternative schema style), but is also useful for creating XML Schema files. For Spring-WS, we’ll be using Trang to generate XML Schema.

Once you’ve downloaded and unzipped Trang, you’ll find trang.jar in the distribution. This is an executable JAR file, so running Trang is simple from the command line:

% java -jar trang.jar EvaluateHandRequest.xml EvaluateHandResponse.xml PokerTypes.xsd

When running Trang, I’ve specified three command-line arguments. The first two are the sample message XML files that we created earlier. Because we’ve specified both message files, Trang is able to produce an XSD file that can validate the messages in both files. The last argument is the name of the file we want Trang to write the XSD to.

When run with these arguments, Trang will generate the data contract for our service in PokerTypes.xsd (listing 9.1).

Listing 9.1 PokerTypes.xsd, which defines the data contract for the web service

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="https://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace=
"https://www.springinaction.com/poker/schemas"
xmlns:schemas=
"https://www.springinaction.com/poker/schemas">
<xs:element name="EvaluateHandRequest">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded"
ref="schemas:card"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="card">
<xs:complexType>
<xs:sequence>
<xs:element ref="schemas:suit"/>
<xs:element ref="schemas:face"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="suit" type="xs:NCName"/>
<xs:element name="face" type="xs:NCName"/>
<xs:element name="EvaluateHandResponse">
<xs:complexType>
<xs:sequence>
<xs:element ref="schemas:handName"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="handName" type="xs:string"/>
</xs:schema>

Trang saved us a lot of trouble by inferring the XSD for our messages. We’re not completely off the hook, though. XSD isn’t perfect. As it infers the XSD, Trang makes some assumptions about what kind of data will be in your XML. Most of the time, those assumptions are okay. But often, we’ll need to fine-tune the generated XSD to be more precise.

For example, Trang assumed that the values of the <suit> and <face> elements should be defined as noncolonized1 names ( xs:NCName). What we actually want is for those elements to be simple strings (xs:string). So, let’s tweak the definitions of <suit> and <face> to be strings:

<xs:element name="suit" type="xs:string"/>
<xs:element name="face" type="xs:string"/>

A noncolonized name is a name that isn’t qualified with a namespace related prefix. Therefore, it does not have a colon (:)—it isn’t “colonized.”


We also know that there are only four possible values for the <suit> element, so we could constrain the message a bit further:

<xs:element name="suit" type="schemas:Suit" />
<xs:simpleType name="Suit">
<xsd:restriction base="xs:string">
<xsd:enumeration value="SPADES" />
<xsd:enumeration value="CLUBS" />
<xsd:enumeration value="HEARTS" />
<xsd:enumeration value="DIAMONDS" />
</xsd:restriction>
</xs:simpleType>

Likewise, there are only 13 legal values for the <face> element, so let’s define those limits in XSD:

<xs:element name="face" type="schemas:Face" />
<xs:simpleType name="Face">
<xsd:restriction base="xs:string">
<xsd:enumeration value="ACE" />
<xsd:enumeration value="TWO" />
<xsd:enumeration value="THREE" />
<xsd:enumeration value="FOUR" />
<xsd:enumeration value="FIVE" />
<xsd:enumeration value="SIX" />
<xsd:enumeration value="SEVEN" />
<xsd:enumeration value="EIGHT" />
<xsd:enumeration value="NINE" />
<xsd:enumeration value="TEN" />
<xsd:enumeration value="JACK" />
<xsd:enumeration value="QUEEN" />
<xsd:enumeration value="KING" />
</xsd:restriction>
</xs:simpleType>

Also, notice that Trang incorrectly assumes that the <EvaluateHandRequest> may contain an unlimited number of <card> elements (maxOccurs="unbounded"). But a poker hand contains exactly five cards. Therefore, we’ll need to adjust the definition of <EvaluateHandRequest> accordingly:

<xs:element name="EvaluateHandRequest">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="5" maxOccurs="5"
ref="schemas:card"/>
</xs:sequence>
</xs:complexType>
</xs:element>

As for <EvaluateHandResponse>, it’s fine as is. We could constrain the possible values returned in the <handName> element, but it’s not necessary. So, we’ll leave it unchanged.

Now we have the data contract for the poker hand evaluation service, but what about the operational contract? Aren’t we going to need some WSDL to completely define the web service?

Yes, we’ll absolutely need WSDL—after all, WSDL is the standard for defining web services. We could write the WSDL by hand, but that’s no fun. And, again, I promised you that this would be easy. But I’m going to have to ask you to wait awhile to see where the operational contract comes into play. I’ll show you how the WSDL gets created in section 9.4.6 when we wire a WSDL definition bean in Spring.

But first, we need to create a service endpoint. The contract only defines the messages sent to and from the service, not how they’re handled. Let’s see how to create message endpoints in Spring-WS that will process messages from a web service client.

Handling messages with service endpoints

As you’ll recall from the opening of this chapter, a well-designed application doesn’t allow direct access to the internal objects that do the fine-grained tasks of a system. In Spring MVC, for example, a user interacts with the application through controllers, which in turn translate the user’s requests into calls to internal objects.

Файл:Fig93.png
Figure 9.3 Message endpoints are the implementation of a web service in Spring-WS. Taking a message-centric approach, message endpoints process incoming XML messages and produce XML responses.

It may be helpful to know that Spring MVC and Spring-WS are a lot alike. Whereas a user interacts with a Spring MVC application through one of several controllers, a web service client interacts with a Spring-WS application through one of several message endpoints.

Figure 9.3 illustrates how message endpoints interact with their client. A message endpoint is a class that receives an XML message from the client and, based on the content of the message, makes calls to internal application objects to perform the actual work. For the poker hand evaluation service, the message endpoint will process <EvaluateHandRequest> messages.

Once the endpoint has completed processing, it will return its response in yet another XML message. In the case of the poker hand evaluation service, the response XML is an <EvaluateHandResponse> document.

Spring-WS defines several abstract classes from which message endpoints can be created, as listed in table 9.2.

For the most part, all of the abstract endpoint classes in table 9.2 are similar. Which one you choose is mostly a matter of taste and which XML parsing technology you prefer (e.g., SAX versus DOM versus StAX, etc.). But AbstractMarshallingPayloadEndpoint is a bit different from the rest of the pack in that it supports automatic marshaling and unmarshaling of XML messages to and from Java objects.

Table 9.2 The message endpoint options available with Spring-WS.

Abstract endpoint class in package org.springframework.ws.server.endpoint
Description
AbstractDom4jPayloadEndpoint Endpoint that handles message payloads as dom4j Elements
AbstractDomPayloadEndpoint Endpoint that handles message payloads as DOM Elements
AbstractJDomPayloadEndpoint Endpoint that handles message payloads as JDOM Elements
AbstractMarshallingPayloadEndpoint Endpoint that unmarshals the request payload into an object and marshals the response object into XML
AbstractSaxPayloadEndpoint Endpoint that handles message payloads through a SAX ContentHandler implementation
AbstractStaxEventPayloadEndpoint Endpoint that handles message payloads using event-based StAX
AbstractStaxStreamPayloadEndpoint Endpoint that handles message payloads using streaming StAX
AbstractXomPayloadEndpoint Endpoint that handles message payloads as XOM Elements

We’ll have a look at AbstractMarshallingPayloadEndpoint a little later in this chapter (in section 9.3.2). First, though, let’s see how to build an endpoint that processes XML messages directly.

Building a JDOM-based message endpoint

Our poker hand evaluation web service takes an <EvaluateHandRequest> message as input and produces an <EvaluateHandResponse> as output. Therefore, we’ll need to create a service endpoint that processes an <EvaluateHandRequest> element and produces an <EvaluateHandResponse> element.

Any of the abstract endpoint classes in table 9.2 will do, but we’ve chosen to base our endpoint on AbstractJDomPayloadEndpoint. This choice was mostly arbitrary, but I also like JDOM’s XPath support, which is a simple way to extract information out of a JDOM Element. (For more information on JDOM, visit the JDOM homepage at https://www.jdom.org.)

EvaluateHandJDomEndpoint (listing 9.2) extends AbstractJDomPayloadEndpoint to provide the functionality required to process the <EvaluateHandRequest> message.

Listing 9.2 An endpoint that will process the <EvaluateHandRequest>message

package com.springinaction.poker.webservice;
import java.util.Iterator;
import java.util.List;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.xpath.XPath;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.ws.server.endpoint.AbstractJDomPayloadEndpoint;
import com.springinaction.poker.Card;
import com.springinaction.poker.Face;
import com.springinaction.poker.PokerHand;
import com.springinaction.poker.PokerHandEvaluator;
import com.springinaction.poker.PokerHandType;
import com.springinaction.poker.Suit;
public class EvaluateHandJDomEndpoint
extends AbstractJDomPayloadEndpoint
implements InitializingBean {
private Namespace namespace;
private XPath cardsXPath;
private XPath suitXPath;
private XPath faceXPath;
 
protected Element invokeInternal(Element element)
throws Exception {
Card cards[] = extractCardsFromRequest(element);
PokerHand pokerHand = new PokerHand();
pokerHand.setCards(cards);
PokerHandType handType =
pokerHandEvaluator.evaluateHand(pokerHand);
return createResponse(handType);
}
private Element createResponse(PokerHandType handType) {
Element responseElement =
new Element("EvaluateHandResponse", namespace);
responseElement.addContent(
new Element("handName", namespace).setText(
handType.toString()));
return responseElement;
}
private Card[] extractCardsFromRequest(Element element)
throws JDOMException {
Card[] cards = new Card[5];
List cardElements = cardsXPath.selectNodes(element);
for(int i=0; i < cardElements.size(); i++) {
Element cardElement = (Element) cardElements.get(i);
Suit suit = Suit.valueOf(
suitXPath.valueOf(cardElement));
Face face = Face.valueOf(
faceXPath.valueOf(cardElement));
cards[i] = new Card();
cards[i].setFace(face);
cards[i].setSuit(suit);
}
return cards;
}
public void afterPropertiesSet() throws Exception {
namespace = Namespace.getNamespace("poker",
"https://www.springinaction.com/poker/schemas");
cardsXPath =
XPath.newInstance("/poker:EvaluateHandRequest/poker.card");
cardsXPath.addNamespace(namespace);
faceXPath = XPath.newInstance("poker:face");
faceXPath.addNamespace(namespace);
suitXPath = XPath.newInstance("poker:suit");
suitXPath.addNamespace(namespace);
}
// injected
 
private PokerHandEvaluator pokerHandEvaluator;
public void setPokerHandEvaluator(
PokerHandEvaluator pokerHandEvaluator) {
this.pokerHandEvaluator = pokerHandEvaluator;
}
}private PokerHandEvaluator pokerHandEvaluator;
public void setPokerHandEvaluator(
PokerHandEvaluator pokerHandEvaluator) {
this.pokerHandEvaluator = pokerHandEvaluator;
}
}

The invokeInternal() method is the entry point into this endpoint. When called, it is passed a JDOM Element object that contains the incoming message—in this case, an <EvaluateHandRequest>. invokeInternal() hands off the Element to the extractCardsFromRequest() method, which uses JDOM XPath objects to pull card information out of the <EvaluateHandRequest> element.

After an array of Card objects is returned, invokeInternal() then does the right thing and passes those Cards to an injected PokerHandEvaluator to evaluate the poker hand. PokerHandEvaluator is defined by the following interface:

package com.springinaction.poker;
public interface PokerHandEvaluator {
PokerHandType evaluateHand(PokerHand hand);
}

The actual implementation of PokerHandEvaluator isn’t relevant to the discussion of building web services with Spring-WS, so I’ll leave it out (but you can find it in the downloadable examples).

The fact that the endpoint calls PokerHandEvaluator’s evaluateHand() method is significant. A properly written Spring-WS endpoint shouldn’t perform any business logic of its own. It should only mediate between the client and the internal API. The actual business logic is performed in the PokerHandEvaluator implementation. Later, in chapter 13, we’ll see a similar pattern applied to Spring MVC controllers where a controller merely sits between a web user and a serverside object.

Once the PokerHandEvaluator has determined the type of poker hand it was given, invokeInternal() passes the PokerHandType object off to createResponse() to produce an <EvaluateHandResponse> element using JDOM. The resulting JDOM Element is returned and EvaluateHandJDomEndpoint’s job is done.

Файл:Fig94.png
Figure 9.4 Marshaling endpoints leverage a marshaler/unmarshaler to handle XML messages so that the endpoint only has to deal with POJOs.

EvaluateHandJDomEndpoint is a fine example of how to implement a SpringWS endpoint. But there are an awful lot of XML specifics in there. Although the messages handled by Spring-WS endpoints are XML, there’s usually no reason why your endpoint needs to be written to know that. Let’s see how a marshaling endpoint can help us eliminate all of that XML parsing code.

Marshaling message payloads

As we mentioned before, AbstractMarshallingPayloadEndpoint is a little different from all of the other Spring-WS abstract endpoint classes. Instead of being given an XML Element to pull apart for information, AbstractMarshallingPayloadEndpoint is given an object to process.

Actually, as illustrated in figure 9.4, a marshaling endpoint works with an unmarshaler that converts an incoming XML message into a POJO. Once the endpoint is finished, it simply returns a POJO and a marshaler converts it into an XML message to be returned to the client. This greatly simplifies the endpoint implementation, as it no longer has to include any XML-processing code.

For example, consider listing 9.3, which shows EvaluateHandMarshallingEndpoint, a new implementation of our poker hand evaluation endpoint that extends AbstractMarshallingPayloadEndpoint.

Listing 9.3 The endpoint that will process the '''<EvaluateHandRequest> '''message

package com.springinaction.poker.webservice;
import org.springframework.ws.server.endpoint.
? AbstractMarshallingPayloadEndpoint;
import com.springinaction.poker.PokerHand;
import com.springinaction.poker.PokerHandEvaluator;
import com.springinaction.poker.PokerHandType;
public class EvaluateHandMarshallingEndpoint
extends AbstractMarshallingPayloadEndpoint {
protected Object invokeInternal(Object object)
throws Exception {
EvaluateHandRequest request =
(EvaluateHandRequest) object;
PokerHand pokerHand = new PokerHand();
pokerHand.setCards(request.getHand());
PokerHandType pokerHandType =
pokerHandEvaluator.evaluateHand(pokerHand);
return new EvaluateHandResponse(pokerHandType);
}
// injected
private PokerHandEvaluator pokerHandEvaluator;
public void setPokerHandEvaluator(
PokerHandEvaluator pokerHandEvaluator) {
this.pokerHandEvaluator = pokerHandEvaluator;
}
}

The first thing that you probably noticed about EvaluateHandMarshallingEndpoint is that it is much shorter than EvaluateHandJDomEndpoint. That’s because EvaluateHandMarshallingEndpoint doesn’t have any of the XML parsing code that was necessary in EvaluateHandJDomEndpoint.

Instead, the invokeInternal() method is given an Object to process. In this case, the Object is an EvaluateHandRequest:

package com.springinaction.poker.webservice;
import com.springinaction.poker.Card;
public class EvaluateHandRequest {
private Card[] hand;
public EvaluateHandRequest() {}
public Card[] getHand() {
return hand;
}
public void setHand(Card[] cards) {
this.hand = cards;
}
}

On the other end of invokeInternal(), an EvaluateHandResponse object is returned. EvaluateHandResponse looks like this:

package com.springinaction.poker.webservice;
import com.springinaction.poker.PokerHandType;
public class EvaluateHandResponse {
private PokerHandType pokerHand;
public EvaluateHandResponse() {
this(PokerHandType.NONE);
}
public EvaluateHandResponse(PokerHandType pokerHand) {
this.pokerHand = pokerHand;
}
public PokerHandType getPokerHand() {
return this.pokerHand;
}
public void setPokerHand(PokerHandType pokerHand) {
this.pokerHand = pokerHand;
}
}

So how is an incoming <EvaluateHandRequest> XML message transformed into an EvaluateHandRequest object? And, while we’re on the subject, how does an EvaluateHandResponse object end up being an <EvaluateHandResponse> message that gets sent to the client?

What you don’t see here is that AbstractMarshallingPayloadEndpoint has a reference to an XML marshaler. When it receives an XML message, it uses the marshaler to turn the XML message into an object before calling invokeInternal(). Then, when invokeInternal() is finished, the marshaler turns the object returned into an XML message.

A large part of Spring-WS is an object-XML mapping (OXM) abstraction. Spring-WS’s OXM comes with support for several OXM implementations, including:


  • JAXB (versions 1 and 2)
  • Castor XML
  • JiBX
  • XMLBeans
  • XStream

You may be wondering which OXM I chose for EvaluateHandMarshallingEndpoint. I’ll tell you, but not yet. The important thing to note here is that EvaluateHandMarshallingEndpoint has no idea where the Object that is passed to invokeInternal() came from. In fact, there’s no reason why the Object even has to have been created from unmarshaled XML.

This highlights a key benefit of using a marshaling endpoint. Because it takes a simple object as a parameter, EvaluateHandMarshallingEndpoint can be unittested just like any other POJO. The test case can simply pass in an EvaluateHandRequest object and make assertions on the returned EvaluateHandResponse.

Now that we’ve written our service endpoint, we’re ready to wire it up in Spring.

Wiring it all together

We’re finally down to the final stage of developing a Spring-WS service. We need to configure the Spring application context with our endpoint bean and a handful of infrastructure beans required by Spring-WS.

Spring-WS is based on Spring MVC (which we’ll see more of in chapter 13). In Spring MVC, all requests are handled by DispatcherServlet, a special servlet that dispatches requests to controller classes that process the requests. Similarly, Spring-WS can be fronted by MessageDispatcherServlet, a subclass of DispatcherServlet that knows how to dispatch SOAP requests to Spring-WS endpoints.


The “web” in web services seems to imply that all web services are served over HTTP. But that’s not necessarily true. Spring-WS has support for JMS, email, and raw TCP/IP-based web services. Nevertheless, since most web services are, in fact, served over HTTP, that’s the configuration I’ll talk about here.

MessageDispatcherServlet is a fairly simple servlet and can be configured in a web application’s web.xml with the following <servlet> and <servletmapping> elements:

<servlet>
<servlet-name>poker</servlet-name>
<servlet-class>org.springframework.ws.transport.http.
MessageDispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>poker</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>

We’ll tweak this MessageDispatcherServlet’s configuration a little later, but this will get us started for now.

MessageDispatcherServlet is only the front end of Spring-WS. There are a handful of beans that we’ll need to wire in the Spring application context. Let’s see what those beans are and what they do.

Spring-WS: The big picture

Файл:Fig95.png
Figure 9.5 Spring-WS service configuration consists of several beans, including mappings, endpoints, marshalers, and other utility beans.

Over the next several pages, we’re going to configure several beans in the Spring context. Before we get too deep in the XML, it is probably worthwhile to have a look at the big picture to see what we’re about to do. Figure 9.5 shows the beans we’ll define and how they relate to one another.

Figure 9.5 shows the beans that we’ll configure for the poker hand evaluation service and how they relate to one another. But what are these beans and what do they do? To summarize these six beans:


  • payloadMapping—Maps incoming XML messages to an appropriate endpoint. In this case, we’ll use a mapping that looks up endpoints using the incoming XML’s root element (by its qualified name).
  • evaluateHandEndpoint—This is the endpoint that will process the incoming XML message for the poker hand evaluation service.
  • marshaller—The evaluateHandEndpoint could be written to process the incoming XML as a DOM or JDOM element, or even as a SAX event handler. Instead, the marshaller bean will automatically convert XML to and from Java objects.
  • pokerHandEvaluator—This is a POJO that performs the actual poker hand processing. evaluateHandEndpoint will use this bean to do its work.
  • endpointExceptionResolver—This is a Spring-WS bean that will automatically convert any Java exceptions thrown while processing a request into appropriate SOAP faults.
  • poker—Although it’s not obvious from its name, this bean will serve the WSDL for the poker hand web service to the client. Either it can serve handcreated WSDL or it can be wired to automatically generate WSDL from the message’s XML Schema.

Now that we have a roadmap of where we’re going, let’s dive right into configuring Spring-WS, starting with the message handler adapter.

Mapping messages to endpoints

When a client sends a message, how does MessageDispatcherServlet know which endpoint should process it? Even though we’re only building one endpoint in this chapter’s example (the evaluate hand endpoint), it’s quite possible that MessageDispatcherServlet could be configured with several endpoints. We need a way to map incoming messages to the endpoints that process them.

In chapter 13, we’ll see how Spring MVC’s DispatcherServlet maps browser requests to Spring MVC controllers using handler mappings. In a similar way, MessageDispatcherServlet uses an endpoint mapping to decide which endpoint should receive an incoming XML message.

For the poker hand evaluation service, we’ll use Spring-WS’s PayloadRootQNameEndpointMapping, which is configured in Spring like this:

<bean id="payloadMapping"
class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
<property name="endpointMap">
<map>
<entry key=
"{https://www.springinaction.com/poker/schemas}EvaluateHandRequest"
value-ref="evaluateHandEndpoint" />
</map>
</property>
</bean>

PayloadRootQNameEndpointMapping maps incoming SOAP messages to endpoints by examining the qualified name (QName) of the message’s payload and looking up the endpoint from its list of mappings (configured through the endpointMap property).

In our example, the root element of the message is <EvaluateHandRequest> with a namespace URI of https://www.springinaction.com/poker/schemas. This makes the QName of the message {https://www.springinaction.com/poker/schemas}EvaluateHandRequest. We’ve mapped this QName to a bean named evaluateHandEndpoint, which is our endpoint implementation that we created in section 9.3.2.

Wiring the service endpoint

Now we’re finally down to wiring the endpoint that will process our message. If you chose to use the JDOM-based endpoint then it is configured in Spring like this:

<bean id="evaluateHandEndpoint"
class="com.springinaction.poker.webservice.EvaluateHandJDomEndpoint">
<property name="pokerHandEvaluator"
ref="pokerHandEvaluator" />
</bean>

The only property that must be injected is the pokerHandEvaluator property. Remember that EvaluateHandJDomEndpoint doesn’t actually evaluate the poker hand, but delegates to an implementation of PokerHandEvaluator to do the dirty work. Thus, the pokerHandEvaluator bean should be configured like this:

<bean id="pokerHandEvaluator"
class="com.springinaction.poker.PokerHandEvaluatorImpl"/>

If the JDOM-based endpoint didn’t suit you and instead you chose to use EvaluateHandMarshallingEndpoint, a bit of extra configuration is involved:

<bean id="evaluateHandEndpoint"
class="com.springinaction.poker.webservice.EvaluateHandMarshallingEndpoint">
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
<property name="pokerHandEvaluator"
ref="pokerHandEvaluator" />
</bean>

Again, the pokerHandEvaluator property is injected with a reference to a PokerHandEvaluatorImpl. But the marshaling endpoint must have its marshaller and unmarshaller properties set as well. Here we’ve wired them with references to the same marshaller bean, which we’ll configure next.

Configuring a message marshaler

The key to translating objects to and from XML messages is object-XML mapping (OXM). Spring-OXM is a subproject of Spring-WS that provides an abstraction layer over several popular OXM solutions, including JAXB and Castor XML.

The central elements of Spring-OXM are its Marshaller and Unmarshaller interfaces. Implementations of Marshaller are expected to generate XML elements from Java objects. Conversely, Unmarshaller implementations are used to construct Java objects from XML elements.

AbstractMarshallingPayloadEndpoint takes advantage of the Spring-OXM marshalers and unmarshalers when processing messages. When AbstractMarshallingPayloadEndpoint receives a message, it hands it off to an Unmarshaller to unmarshal the XML message into an object that is passed to invokeInternal(). Then, when invokeInternal() is finished, the object returned is given to a Marshaller to marshal the object into XML that will be returned to the client.

Fortunately, you won’t have to create your own implementations of Marshaller and Unmarshaller. Spring-OXM comes with several implementations, as listed in table 9.3.

Table 9.3 Marshalers transform objects to and from XML. Spring-OXM provides several marshaling options that can be used with Spring-WS.

OXM solution
Spring-OXM marshaler
Castor XML org.springframework.oxm.castor.CastorMarshaller
JAXB v1 org.springframework.oxm.jaxb.Jaxb1Marshaller
JAXB v2 org.springframework.oxm.jaxb.Jaxb2Marshaller
JiBX org.springframework.oxm.jibx.JibxMarshaller
XMLBeans org.springframework.oxm.xmlbeans.XmlBeansMarshaller
XStream org.springframework.oxm.xstream.XStreamMarshaller

As you can see, table 9.3 only lists marshaler classes. That’s not an oversight, though. Conveniently, all of the marshaler classes in table 9.3 implement both the Marshaller and the Unmarshaller interfaces to provide one-stop solutions for OXM marshaling.

The choice of OXM solution is largely a matter of taste. Each of the OXM options offered by Spring-WS has its good and bad points. XStream, however, has limited support for XML namespaces, which are necessary in defining the types for web services. Therefore, while Spring-OXM’s XStream may prove useful for general-purpose XML serialization, it should be disregarded for use with web services.

For the poker hand evaluator service, we chose to use Castor XML. Therefore, we’ll need to configure a CastorMarshaller in Spring:

<bean id="marshaller"
class="org.springframework.oxm.castor.CastorMarshaller">
<property name="mappingLocation"
value="classpath:mapping.xml" />
</bean>

Castor XML can do some basic XML marshaling without any additional configuration. But our OXM needs are a bit more complex than what default Castor XML can handle. Consequently, we’ll need to configure CastorMarshaller to use a Castor XML mapping file. The mappingLocation property specifies the location of a Castor XML mapping file. Here we’ve configured mappingLocation to look for a mapping file with the name mapping.xml in the root of the application’s classpath.

As for the mapping.xml file itself, it is shown in listing 9.4.

Listing 9.4 Castor XML mapping file for poker hand service types

<?xml version="1.0"?>
<!DOCTYPE mapping PUBLIC
"-//EXOLAB/Castor Object Mapping DTD Version 1.0//EN"
"https://castor.exolab.org/mapping.dtd">
<mapping xmlns="https://castor.exolab.org/">
<class name="com.springinaction.poker.webservice.EvaluateHandRequest">
<map-to xml="EvaluateHandRequest" />
<field name="hand"
collection="array"
type="com.springinaction.poker.Card"
required="true">
<bind-xml name="card" node="element" />
</field>
</class>
<class name="com.springinaction.poker.Card">
<map-to xml="card" />
<field name="suit"
type="com.springinaction.poker.Suit"
required="true">
<bind-xml name="suit" node="element" />
</field>
<field name="face"
type="com.springinaction.poker.Face"
required="true">
<bind-xml name="face" node="element" />
</field>
</class>
<class name="com.springinaction.poker.webservice.EvaluateHandResponse">
<map-to xml="EvaluateHandResponse"
ns-uri=
"https://www.springinaction.com/poker/schemas"
ns-prefix="tns" />
<field name="pokerHand"
type="com.springinaction.poker.PokerHandType"
required="true">
<bind-xml name="tns:handName" node="element"
QName-prefix="tns"
xmlns:tns=
"https://www.springinaction.com/poker/schemas"/>
</field>
</class>
</mapping>

Now we have configured an endpoint mapping bean, the endpoint implementation bean, and an XML marshaling bean. At this point the poker hand evaluator web service is mostly done. We could deploy it and stop for the day. But there are still a couple of beans left that will make the web service more complete. Let’s see how to make our web service more robust by declaring a bean that maps Java exceptions to SOAP faults.

Handling endpoint exceptions

Файл:Fig96.png
Figure 9.6 SoapFaultMappingExceptionResolver maps any Java exceptions thrown from a message endpoint into a SOAP fault to be returned to the client.

Things don’t always work out as expected. What will happen if a message can’t be marshaled to a Java object? What if the message isn’t even valid XML? Maybe the service endpoint or one of its dependencies throws an exception—then what should we do?

If an exception is thrown in the course of processing a message, a SOAP fault will need to be sent back to the client. Unfortunately, SOAP doesn’t know or care anything about Java exceptions. SOAP-based web services communicate failure using SOAP faults. We need a way to convert any Java exceptions thrown by our web service or by Spring-WS into SOAP faults.

For that purpose, Spring-WS provides SoapFaultMappingExceptionResolver. As shown in figure 9.6, SoapFaultMappingExceptionResolver will handle any uncaught exceptions that occur in the course of handling a message and produce an appropriate SOAP fault that will be sent back to the client.

For our service, we’ve configured a SoapFaultMappingExceptionResolver in Spring that looks like this:

<bean id="endpointExceptionResolver"
class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.springframework.oxm.UnmarshallingFailureException">
SENDER,Invalid message received</prop>
<prop key="org.springframework.oxm.ValidationFailureException">
SENDER,Invalid message received</prop>
</props>
</property>
<property name="defaultFault"
value="RECEIVER,Server error" />
</bean>

The exceptionMappings property is configured with one or more SOAP fault definitions mapped to Java exceptions. The key of each <prop> is a Java exception that needs to be translated to a SOAP fault. The value of the <prop> is a two-part value where the first part is the type of fault that is to be created and the second part is a string that describes the fault.

SOAP faults come in two types: sender and receiver faults. Sender faults typically indicate that the problem is on the client (e.g., the sender) side. Receiver faults indicate that the web service (e.g., the receiver) received a message from the client but is having some problem processing the message.

For example, if a service receives an XML message that can’t be unmarshaled, the marshaler will throw an org.springframework.oxm.UnmarshallingFailureException. Because the sender created the useless XML, this is a sender fault. As for the message, it is simply set to “Invalid message received” to indicate the nature of the problem.

Файл:Fig97.png
Figure 9.7 DynamicWsdl11Definitionautomatically produces WSDL for a web service based on the XML Schema that validates the service’s messages.

An org.springframework.oxm.ValidationFailureException is handled the same way.Any exceptions not explicitly mapped in the exceptionMappings property will be handled by the fault definition in the defaultFault property. In this case, we’re assuming that if the exception thrown doesn’t match any of the mapped exceptions, it must be a problem on the receiving side. Thus, it is a receiver fault and the message simply states “Server error.”

Serving WSDL files

Finally, I’m going to make good on my promise to show you where the WSDL file for the poker hand evaluation web service comes from. As you recall from section 9.2.1, we’ve already created the data portion of the contract as XML Schema in PokerTypes.xsd. Before we go any further, you may want to turn back to listing 9.1 to review the details of the data service.

Pay particular attention to the names I chose for the XML elements that make up our web service messages: EvaluateHandRequest and EvaluateHandResponse. These names weren’t chosen arbitrarily. I chose them purposefully to take advantage of a convention-over-configuration feature in Spring-WS that will automatically create WSDL for the poker hand evaluation service.

To make this work, we’ll need to configure Spring-WS’s DynamicWsdl11Definition. DynamicWsdl11Definition is a special bean that MessageDispatcherServlet works with to generate WSDL from XML Schema. This will come in handy, as we already have some XML Schema defined for the data portion of the contract. Here’s how I’ve configured DynamicWsdl11Definition in Spring:

<bean id="poker"
class="org.springframework.ws.wsdl.wsdl11.
DynamicWsdl11Definition">

<property name="builder">
<bean class="org.springframework.ws.wsdl.wsdl11.builder.
XsdBasedSoap11Wsdl4jDefinitionBuilder">

<property name="schema" value="/PokerTypes.xsd"/>
<property name="portTypeName" value="Poker"/>
<property name="locationUri"
value="https://localhost:8080/Poker-WS/services"/>
</bean>
</property>
</bean>

DynamicWsdl11Definition works by reading an XML Schema definition, specified here as PokerTypes.xsd by the schema property. It looks through the schema file for any element definitions whose names end with Request and Response. It assumes that those suffixes indicate a message that is to be sent to or from a web service operation and creates a corresponding <wsdl:operation> element in the WSDL it produces, as shown in figure 9.7.

For example, when DynamicWsdl11Definition processes the PokerTypes.xsd file, it assumes that the EvaluateHandRequest and EvaluateHandResponse elements are input and output messages for an operation called EvaluateHand. Consequently, the following definition is placed in the generated WSDL:

<wsdl:portType name="Poker">
<wsdl:operation name="EvaluateHand">
<wsdl:input message="schema:EvaluateHandRequest"
name="EvaluateHandRequest">
</wsdl:input>
<wsdl:output message="schema:EvaluateHandResponse"
name="EvaluateHandResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>
Файл:Fig98.png
Figure 9.8 The URL configured in the locationUri property.

Notice that DynamicWsdl11Definition placed the EvaluateHand <wsdl:operation> within a <wsdl:portType> with the name Poker. It named the <wsdl:portType> using the value wired into its portTypeName property.

The last of DynamicWsdl11Definition’s properties that we’ve configured is locationUri. This property tells the client where the service can be found. The diagram in figure 9.8 breaks down the URL configured in the locationUri property.

In this case, I’m assuming that it will be running on the local machine, but you’ll want to change the URL if you’ll be running it on a different machine. Notice that the URL ends with /services, which matches the <servlet-mapping> that we created for MessageDispatcherServlet.

Speaking of <servlet-mapping>s, we’ll also need to add a new <servlet-mapping> '''''to web.xml so that '''''MessageDispatcherServlet '''''will serve WSDL definitions. The following '''''<nowiki><servlet-mapping> definition should do the trick:

<servlet-mapping>
<servlet-name>poker</servlet-name>
<url-pattern>*.wsdl</url-pattern>
</servlet-mapping>
Файл:Fig99.png
Figure 9.9 SimpleWsdl11Definition simply serves a predefined WSDL file through the MessageDispatcherServlet. It can optionally be configured to transform the service’s address location to match the location of MessageDispatcherServlet.

Now MessageDispatcherServlet has been configured (through DynamicWsdl11Definition) to automatically produce WSDL for the poker hand evaluation service. The only question left unanswered at this point is where to find the generated WSDL.

The generated WSDL can be found at https://localhost:8080/Poker-WS/poker.wsdl. How did I know that? I know that MessageDispatcherServlet is mapped to *.wsdl, so it will attempt to create WSDL for any request that matches that pattern. But how did it know to produce WSDL for our poker service at poker.wsdl?

The answer to that question lies in one last bit of convention followed by MessageDispatcherServlet. Notice that I declared the DynamicWsdl11Definition bean to have an ID of poker. When MessageDispatcherServlet receives a request for /poker.wsdl, it will look in the Spring context for a WSDL definition bean named poker. In this case, it will find the DynamicWsdl11Definition bean that I configured.

Using predefined WSDL

DynamicWsdl11Definition is perfect for most situations, as it keeps you from having to write the WSDL by hand. But you may have special circumstances that require you to have more control over what goes into the service’s WSDL definition. In that case you’ll need to create the WSDL yourself and then wire it into Spring using SimpleWsdl11Definition:

<bean id="poker"
class="org.springframework.ws.
wsdl.wsdl11.SimpleWsdl11Definition">

<property name="wsdl"
value="/PokerService.wsdl"/>
</bean>

SimpleWsdl11Definition doesn’t generate WSDL (see figure 9.9); it just serves WSDL that you’ve provided through the wsdl property.

The only problem with predefined WSDL (aside from the trouble that it takes to create it) is that it is statically defined. This creates a problem for the part of the WSDL that specifies the service’s location. For example, consider the following (statically defined) chunk of WSDL:

<wsdl:service name="PokerService">
<wsdl:port binding="tns:PokerBinding" name="PokerPort">
<wsdlsoap:address
location="https://localhost:8080/Poker-WS/services"/>
</wsdl:port>
</wsdl:service>

Here the service is defined as being available at [1] WS/services. That is probably okay for development purposes, but it will need to be changed when you deploy it to another server. You could manually change the WSDL file every time you deploy it to another server, but that’s cumbersome and is susceptible to mistakes.

But MessageDispatcherServlet knows where it’s deployed and it knows the URL of requests used to access it. So, instead of tweaking the WSDL every time you deploy it to another server, why not let MessageDispatcherServlet rewrite it for you?

All you need to do is to set an <init-param> named transformWsdlLocations to true and MessageDispatcherServlet will take it from there:

<servlet>
<servlet-name>poker</servlet-name>
<servlet-class>org.springframework.ws.transport.http.
MessageDispatcherServlet</servlet-class>
<init-param>
<param-name>transformWsdlLocations</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
When transformWsdlLocations

When transformWsdlLocations is set to true, MessageDispatcherServlet will rewrite the WSDL served by SimpleWsdl11Definition to match the request’s URL.

Deploying the service

We’ve defined our contract, the endpoint has been written, and all of the SpringWS beans are in place. At this point, we’re ready to package up the web service application and deploy it. Since I chose to use Maven 2 for this project, creating a deployable WAR file is as simple as typing the following at the command line:

% mvn package deploy

Once Maven’s finished, there will be a Poker-WS.war file in the target directory, suitable for deployment in most web application servers.

Using Spring-WS to build a web service only demonstrates half of its capabilities. Spring-WS also comes with a client API based on the same message-centric paradigm that Spring-WS promotes on the service side. Let’s see how to build a client to consume the poker hand evaluation service using Spring-WS client templates.

Consuming Spring-WS web services

In chapter 8, you saw how to use JaxRpcPortProxyFactoryBean and XFireClientFactoryBean to build clients that communicate with remote web services. But both of those take a remote object view of web services, treating web services as remote objects whose methods can be invoked locally. Throughout this chapter, we’ve been talking about a message-centric approach to web services where clients send XML messages to a web service and receive XML messages back in response. A different paradigm on the service side demands a different paradigm on the client side as well. That’s where Spring-WS’s WebServiceTemplate comes in.

WebServiceTemplate is the centerpiece of Spring-WS’s client API. As shown in

figure 9.10, it employs the Template design pattern to provide the ability to send and receive XML messages from message-centric web services. We’ve already seen how Spring uses the Template pattern for its data access abstractions in chapter 5.

Файл:Fig910.png
Figure 9.10 WebServiceTemplateis the central class in Spring-WS’s client API. It sends and receives XML messages to and from web services on behalf of a client.

As we look at Spring-WS’s client API, you’ll find that it resembles the data access API in many ways.

To demonstrate WebServiceTemplate, we’ll create several different implementations of the PokerClient interface, which is defined as follows:

package com.springinaction.ws.client;
import java.io.IOException;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
public interface PokerClient {
PokerHandType evaluateHand(Card[] cards)
throws IOException;
}

Each implementation will show a different way of using WebServiceTemplate to send messages to the poker hand evaluation web service.

But first things first… Let’s configure WebServiceTemplate as a bean in Spring.

Working with web service templates

As I’ve already mentioned, WebServiceTemplate is the central class in the SpringWS client API. Sending messages to a web service involves producing SOAP envelopes and communications boilerplate code that is pretty much the same for every web service client. When sending messages to a Spring-WS client, you’ll certainly want to rely on WebServiceTemplate to handle the grunt work so that you can focus your efforts on the business logic surrounding your client.

Configuring WebServiceTemplate in Spring is rather straightforward, as shown in this typical <bean> declaration:

<bean id="webServiceTemplate"
class="org.springframework.ws.client.core.WebServiceTemplate">
<property name="messageFactory">
<bean class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
</property>
<property name="messageSender" ref="messageSender"/>
</bean>

WebServiceTemplate needs to know how to construct the message that will be sent to the service and how to send the message. The object wired into the messageFactory property handles the task of constructing the message. It should be wired with an implementation of Spring-WS’s WebServiceMessageFactory interface. Fortunately, you won’t have to worry about implementing WebServiceMessageFactory, as Spring-WS comes with three suitable choices (shown in table 9.4).

Table 9.4 WebServiceTemplate relies on a message factory to construct the message sent to a web service. Spring-WS provides three message factory implementations to choose from.

Message factory
What it does
AxiomSoapMessageFactory Produces SOAP messages using the AXIs Object Model (AXIOM). Based on the StAX streaming XML API. Useful when working with large messages and performance is a problem.
DomPoxMessageFactory Produces Plain Old XML (POX) messages using a DOM. Use this message factory when neither the client nor the service cares to deal with SOAP.
SaajSoapMessageFactory Produces SOAP messages using the SOAP with Attachments API for Java (SAAJ). Because SAAJ uses a DOM, large messages could consume a lot of memory. If performance becomes an issue, consider using AxiomSoapMessageFactory instead.

Since the messages sent to and from the poker hand evaluation service are rather simple, I’ve chosen to wire a SaajSoapMessageFactory into WebServiceTemplate’s messageFactory property. (This is also the default message factory used by MessageDispatcherServlet.) If I were to decide later that performance is an issue, switching to AXIOM-based messages would be a simple matter of rewiring the messageFactory property.


It’s not all SOAP

Figure 9.10 is a bit misleading. It implies that Spring-WS only deals with SOAPbased web services. In fact, Spring-WS only uses SOAP if it is wired with an AxiomSoapMessageFactory or SaajSoapMessageFactory. The DomPoxMessageFactory supports POX messages that aren’t sent in a SOAP envelope. If you have an aversion to using SOAP, maybe DomPoxMessageFactory is for you. You may be interested in knowing that the upcoming Spring-WS 1.1 release will also include support for REST.

The messageSender property should be wired with a reference to an implementation of a WebServiceMessageSender. Again, Spring-WS provides a couple of appropriate implementations, as listed in table 9.5.

Table 9.5 Message senders send the messages to a web service. Spring-WS comes with two message senders.

Message sender
What it does
CommonsHttpMessageSender Sends the message using Jakarta Commons HTTP Client. Supports a preconfigured HTTP client, allowing advanced features such as HTTP authentication and HTTP connection pooling.
HttpUrlConnectionMessageSender Sends the message using Java’s basic facilities for

HTTP connections. Provides limited functionality.

The choice between CommonsHttpMessageSender and HttpUrlConnectionMessageSender boils down to a trade-off between functionality and another JAR dependency. If you won’t be needing the advanced features supported by CommonsHttpMessageSender (such as HTTP authentication), HttpUrlConnectionMessageSenderwillsuffice. But ifyou willneed those features then CommonsHttpMessageSender is a must—but you’ll have to be sure to include Jakarta Commons HTTP in your client’s classpath.

As the advanced features aren’t an issue for the poker hand evaluation web service, I’ve chosen HttpUrlConnectionMessageSender, which is configured like this in Spring:

<bean id="messageSender"
class="org.springframework.ws.transport.http.
HttpUrlConnectionMessageSender">

<property name="url"
value="https://localhost:8080/Poker-WS/services"/>
</bean>

The url property specifies the location of the service. Notice that it matches the URL in the service’s WSDL definition.

If I decide later that I’ll need to authenticate to use the poker hand evaluation web service, switching to CommonsHttpMessageSender is a simple matter of changing the messageSender bean’s class specification.

Sending a message

Once the WebServiceTemplate has been configured, it’s ready to use to send and receive XML to and from the poker hand evaluation service. WebServiceTemplate provides several methods for sending and receiving messages. This one, however, stands out as the most basic and easiest to understand:

public boolean sendAndReceive(Source requestPayload,
Result responseResult)
throws IOException

The sendAndReceive() method takes a java.xml.transform.Source and a java.xml.transform.Result as parameters. The Source object represents the message payload to send to the web service, while the Result object is to be populated with the message payload returned from the service.

Listing 9.5 shows TemplateBasedPokerClient, an implementation of the PokerClient interface that uses WebServiceTemplate’s sendAndReceive() method to communicate with the poker hand evaluation service.

Listing 9.5 Client that uses an injected WebServiceTemplate to send and receive XML messages from the poker hand evaluation service

package com.springinaction.ws.client;
import java.io.IOException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.transform.JDOMResult;
import org.jdom.transform.JDOMSource;
import org.springframework.ws.client.core.WebServiceTemplate;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
public class TemplateBasedPokerClient
implements PokerClient {
public PokerHandType evaluateHand(Card[] cards)
throws IOException {
Element requestElement =
new Element("EvaluateHandRequest");
Namespace ns = Namespace.getNamespace(
"https://www.springinaction.com/poker/schemas");
requestElement.setNamespace(ns);
Document doc = new Document(requestElement);
for(int i=0; i<cards.length; i++) {
Element cardElement = new Element("card");
Element suitElement = new Element("suit");
 
suitElement.setText(cards[i].getSuit().toString());
Element faceElement = new Element("face");
faceElement.setText(cards[i].getFace().toString());
cardElement.addContent(suitElement);
cardElement.addContent(faceElement);
doc.getRootElement().addContent(cardElement);
}
JDOMSource requestSource = new JDOMSource(doc);
JDOMResult result = new JDOMResult();
webServiceTemplate.sendAndReceive(requestSource, result);
Document resultDocument = result.getDocument();
Element responseElement = resultDocument.getRootElement();
Element handNameElement =
responseElement.getChild("handName", ns);
return PokerHandType.valueOf(handNameElement.getText());
}
private WebServiceTemplate webServiceTemplate;
public void setWebServiceTemplate(
WebServiceTemplate webServiceTemplate) {
this.webServiceTemplate = webServiceTemplate;
}
}

Both Source and Result are interfaces that are a standard part of Java’s XML API and are available in the Java SDK. There are countless implementations of these interfaces to choose from, but as you can see in listing 9.5, I chose to use the JDOM implementations. This choice was mostly arbitrary but influenced by the fact that I am familiar with JDOM and know how to use it to construct XML messages.

TemplateBasedPokerClient’s evaluateHand() method starts by using JDOM to construct an <EvaluateHandRequest> message from the array of Card elements passed in. Once it has the request message, it calls sendAndReceive() on the WebServiceTemplate. It then uses JDOM to parse the result and find the PokerHandType that should be returned.

Notice that the WebServiceTemplate is injected through a setter method. Therefore, TemplateBasedPokerClient must be configured in Spring as follows:

<bean id="templateBasedClient"
class="com.springinaction.ws.client.TemplateBasedPokerClient">
<property name="webServiceTemplate" ref="webServiceTemplate" />
</bean>

The webServiceTemplate property is wired with a reference to the webServiceTemplate bean that we configured earlier.While reading through listing 9.5, you may have noticed that the bulk of the evaluateHand() method involves creating and parsing XML. In fact, only one line deals specifically with sending a message. Manually creating and parsing XML messages may be okay when the messages are very simple, but you can probably imagine the amount of code that would be required to construct complex message payloads. Even with the poker hand evaluation service, where the message payload is far from complex, the amount of XML processing code is staggering.

Fortunately, you don’t have to deal with all of that XML on your own. In section 9.4.4 you saw how an endpoint can use a marshaler to transform objects to and from XML. Now I’ll show you how WebServiceTemplate can also take advantage of marshalers to eliminate the need for XML processing code on the client side.

Using marshalers on the client side

In addition to the simple sendAndReceive() method we used in listing 9.5, WebServiceTemplate also provides marshalSendAndReceive(), a method for sending and receiving XML messages that are marshaled to and from Java objects.

Using marshalSendAndReceive() is a simple matter of passing in a request object as a parameter and receiving a response object as the returned value. In the case of the poker hand evaluation service, these objects are EvaluateHandRequest and EvaluateHandResponse, respectively.

Listing 9.6 shows MarshallingPokerClient, an implementation of PokerClient that uses marshalSendAndReceive() to communicate with the poker hand evaluation service.

Listing 9.6 MarshallingPokerClient, which takes advantage of a marshaler to convert objects to and from XML

package com.springinaction.ws.client;
import java.io.IOException;
import org.springframework.ws.client.core.WebServiceTemplate;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
import com.springinaction.poker.webservice.EvaluateHandRequest;
import com.springinaction.poker.webservice.EvaluateHandResponse;
public class MarshallingPokerClient
implements PokerClient {
public PokerHandType evaluateHand(Card[] cards)
throws IOException {
EvaluateHandRequest request = new EvaluateHandRequest();
request.setHand(cards);
EvaluateHandResponse response = (EvaluateHandResponse)
webServiceTemplate.marshalSendAndReceive(request);
return response.getPokerHand();
}
private WebServiceTemplate webServiceTemplate;
public void setWebServiceTemplate(
WebServiceTemplate webServiceTemplate) {
this.webServiceTemplate = webServiceTemplate;
}
}

Wow! MarshallingPokerClient’s evaluateHand() method is much simpler and no longer involves any XML processing. Instead, it constructs an EvaluateHandRequest object and populates it with the Card array that was passed in. After calling marshalSendAndReceive(), passing in the EvaluateHandRequest object, evaluateHand() receives an EvaluateHandResponse, which it uses to retrieve the PokerHandType that it returns.

So, how does WebServiceTemplate know how to marshal/unmarshal EvaluateHandRequest and EvaluateHandResponse objects? Is it really that smart?

Well, no… not really. Actually, it doesn’t know anything about marshaling and unmarshaling those objects. However, as shown in figure 9.11, it can be wired with a marshaler and an unmarshaler that know how to handle the marshaling:

<bean id="webServiceTemplate"
class="org.springframework.ws.client.core.WebServiceTemplate">
<property name="messageFactory">
<bean class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
</property>
<property name="messageSender" ref="urlMessageSender"/>
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>
Файл:Fig911.png
Figure 9.11 When wired with a marshaler and unmarshaler, a client can send and receive Java objects from WebServiceTemplate. WebServiceTemplate will use the marshaler and unmarshaler to transform the Java objects to and from XML.

Here I’ve wired both the marshaller and unmarshaller properties with a reference to a marshaller bean, which is the same CastorMarshaller configured in section 9.4.4. But it could just as easily have been any of the marshalers listed in table 9.3.

MarshallingPokerClient is much cleaner than TemplateBasedPokerClient. But there’s still a little bit more we can do to trim the fat. Let’s see how to use Spring-WS’s WebServiceGatewaySupport class to eliminate the need to explicitly wire in a WebServiceTemplate.

Using web service gateway support

As you’ll recall from chapter 5 (see sections 5.3.3, 5.4.3, 5.5.3, and 5.6.2), Spring’s data access API includes convenient support classes that provide templates so that the templates themselves do not need to be configured. In a similar way, SpringWS provides WebServiceGatewaySupport, a convenient support class that automatically provides a WebServiceTemplate to client classes that subclass it.

Listing 9.7 shows one final implementation of PokerClient, PokerServiceGateway, that extends WebServiceGatewaySupport.

Listing 9.7 ''WebServiceGatewaySupport, which provides a WebServiceTemplate through getWebServiceTemplate()

package com.springinaction.ws.client;
import java.io.IOException;
import org.springframework.ws.client.core.support.
WebServiceGatewaySupport;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
import com.springinaction.poker.webservice.EvaluateHandRequest;
import com.springinaction.poker.webservice.EvaluateHandResponse;
public class PokerServiceGateway
extends WebServiceGatewaySupport
implements PokerClient {
public PokerHandType evaluateHand(Card[] cards)
throws IOException {
EvaluateHandRequest request = new EvaluateHandRequest();
request.setHand(cards);
EvaluateHandResponse response = (EvaluateHandResponse)
getWebServiceTemplate().marshalSendAndReceive(request);
return response.getPokerHand();
}
}

As you can see, PokerServiceGateway isn’t much different from MarshallingPokerClient. The key difference is that PokerServiceGateway isn’t injected with a WebServiceTemplate. Instead, it gets its WebServiceTemplate by calling getWebServiceTemplate(). Under the covers, WebServiceGatewaySupport will create a WebServiceTemplate object without one being explicitly defined in Spring.

Even though WebServiceTemplate no longer needs to be defined in Spring, the details of how to create a WebServiceTemplate must still be configured through WebServiceGatewaySupport’s properties. For PokerServiceGateway, this means configuring the messageFactory, messageSender, marshaller, and unmarshaller properties:

<bean id="pokerClientGateway"
class="com.springinaction.ws.client.PokerServiceGateway">
<property name="messageFactory">
<bean class="org.springframework.ws.soap.saaj.
SaajSoapMessageFactory"/>

</property>
<property name="messageSender" ref="messageSender"/>
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>

Notice that the properties are configured exactly as they were with WebServiceTemplate.

Summary

Traditionally, web services have been viewed as just another remoting option. In fact, some developers lovingly refer to SOAP as “CORBA with XML.”The problem with the web services as remoting view is that it leads to tight coupling between a service and its clients. When treated as remoting, a client is bound to the service’s internal API. The contract with the client is a side effect of this binding. Changes to the service could break the contract with the client, requiring the client to change or requiring the service to be versioned.

In this chapter, we’ve looked at web services from a different angle, taking a message-centric view. This approach is known as contract-first web services, as it elevates the contract to be a first-class citizen of the service. Rather than simply being remote objects, contract-first web services are implemented as message endpoints that process messages sent by the client and defined by the contract. Consequently, the service and its API can be changed without impacting the contract.

Spring-WS is an exciting new web service framework that encourages contractfirst web services. Based on Spring MVC, Spring-WS endpoints handle XML messages sent from the client, producing responses that are also XML messages.

If you’re like me, you’re probably a bit skeptical about all of the work that went into configuring a web service in Spring-WS. I won’t deny that contract-first web services require a bit more work than using XFire to SOAP-ify a bean in contractlast style. In fact, when I first looked at Spring-WS, I initially dismissed it as too much work and no benefit… crazy talk.

But after some more thought, I realized that the benefits of decoupling the service’s contract from the application’s internal API far outweigh the extra effort required by Spring-WS. And that work will pay dividends in the long run as we are able to revise and refactor our application’s internal API without worrying about breaking the service’s contract with its clients.

Web services, especially those that are contract first, are a great way for applications to communicate with each other in a loosely coupled way. Another approach is to send messages using the Java Message Service (JMS). In the next chapter, we’ll explore Spring’s support for asynchronous messaging with JMS.

Wikijava.org.ua-1px.png

]]>
Книги по Java https://linexp.ru?id=4725 Wed, 29 Jun 2022 13:59:59 GMT
<![CDATA[Глава 8 Spring in Action 2th edition]]> Книги по Java https://linexp.ru?id=4724 Wed, 29 Jun 2022 13:59:23 GMT <![CDATA[Глава 9 Spring in Action 2th edition_1]]>

Building contract-first web services in Spring

This chapter covers


  • Defining XML service contracts
  • Creating document-centric web services
  • Marshaling and unmarshaling XML messages
  • Building template-based web service clients

Imagine that it’s the weekend and you’ve got a trip planned. Before you hit the road, you stop by your bank to deposit your paycheck and to pick up some spending cash.

This is not an unusual scenario, but what makes it interesting is that you bank at an unusual bank. When you walk in the door, there are no tellers to help you. Instead, you have full access to handle the transaction yourself. You have direct access to the ledger and to the vault, allowing you to handle all of the minute details of the transaction on your own. So, you perform the following tasks:


1 - You place your signed paycheck in a box designated for deposited checks.
2 - You edit your account’s ledger, incrementing the balance by the amount on the check.
3 - You take $200 from the vault and place it in your pocket.
4 - You edit your account’s ledger, decrementing the balance by $200.
5 - As a thank-you for all of the hard work you did, you pay yourself a service fee by pocketing another $50 bill on the way out the door.

Whoa! Steps 1–4 seem to be on the up and up. But isn’t step 5 a bit odd?

The problem (if that’s what you want to call it) with this bank is that they trust their customers with too much direct access to the internal workings of the bank. Instead of providing an appropriate interface to the inner workings of the bank (commonly known as a “teller”), they give you full access to the inner workings to do as you please. Consequently, you are able to perform an unrecorded and questionable withdrawal.

As nice as this is for the customer, most banks don’t work that way (if your bank really does allow you this kind of access, please email me—I’d really like to start banking there!). Most banks have tellers, ATM machines, and websites to allow you to manipulate your account. These interfaces to the bank are customer-facing abstractions to the vault and the ledger. While they may provide service with a smile, they only allow you to perform activities that fit within the bank’s business model.

Likewise, most applications do not allow direct access to the internal objects that make up the application. Take web applications, for instance. In a Spring MVC-based web application (which we’ll look at when we get to chapter 13), users interact with the application through controllers. Behind the scenes, there may be dozens or even hundreds of objects that perform the core tasks of the application. But the user is only allowed to interact with the controllers, which, in turn, interact with the back-end objects.


In the previous chapter, we saw that XFire is a quick and easy way to develop web services using remote exporters. But when we export an application bean as a web service, we’re exposing the application’s internal API, which carries with it some consequences: as I alluded to in the banking scenario, you must be careful not to accidentally expose too much of your application’s internal API. Doing so may give your web service clients more access to the inner workings of your application than they need.

In this chapter, you’ll learn an alternative way of building web services using the Spring-WS framework. We’ll separate the service’s external contract from the application’s internal API, and we’ll focus on sending messages between clients and the service, not on invoking remote methods.

I won’t deceive you: building web services with Spring-WS is not as simple as exporting them with XFire. However, I think you’ll find that it isn’t that much more difficult and that the architectural advantages that Spring-WS affords make it well worth considering.

Introducing Spring-WS

Spring Web Services (or Spring-WS, for short) is an exciting new subproject of Spring that is focused on building contract-first web services. What are contract-first web services? It might be easier to answer that question by first talking about their antithesis: contact-last web services.

In chapter 8 (see section 8.5.1), we used XFire to export bean functionality as a remote web service. We started by writing some Java code (the service implementation). Then we configured it as a <bean> in Spring. Finally, we used XFire’s XFireExporter to turn it into a web service. We never had to explicitly define the service’s contract (WSDL and XSD). Instead, XFire automatically generated the contract after the service was deployed. In short, the contract was the last thing defined, thus the designation of “contract-last.”

Contract-last web services are a popular approach to web service development for one basic reason: they’re easy. Most developers don’t have the intestinal fortitude required to understand WSDL, SOAP, and XML Schema (XSD). In the contract-last approach, there’s no need to manipulate complex WSDL and XSD files. You simply write a service class in Java and ask the web service framework to “SOAP-ify” it. If a web services platform such as XFire is willing to cope with the web services acronyms then why should we worry ourselves with it?

But there’s one small gotcha: when a web service is developed contract last, its contract ends up being a reflection of the application’s internal API. Odds are that your application’s internal API is far more volatile than you (or your service’s clients) would like the external API to be. Changes to the internal API will mean changes to your service’s contract, which will ultimately require changes in the clients that are consuming your service. A clever refactoring today may result in a new service contract tomorrow.

This leads to the classic web services versioning problem. It’s much easier to change a web service’s contract than to change the clients that consume that service. If your web service has 1,000 clients and you change your service’s contract then 1,000 clients will be broken until they are changed to adhere to the new contract. A common solution to this problem is to maintain multiple versions of a service until all clients have upgraded. This, however, would multiply maintenance and support costs, as you would have to support multiple versions of the same service.

A better solution is to avoid changing the service’s contract. And when the contract must be changed, the changes shouldn’t break compatibility with previous versions. But this can be difficult to do when the service’s contract is automatically generated.

In short, the problem with contract-last web services is that the service’s most important artifact, the contract, is treated as an afterthought. The focus of a contract-last web service is on how the service should be implemented and not on what it should do.

The solution to contract-last’s problems is to flip it on its head—create the contract first and then decide how it should be implemented. When you do, you end up with contract-first web services. The contract is written with little regard for what the underlying application will look like. This is a pragmatic approach, because it emphasizes what is expected of the service and not how it will be implemented.

You’re probably getting an uneasy feeling about now. It could be that unusually large burrito that you had for lunch... or it could be that you’re terrified that we’re going to have to create a WSDL file by hand.

Don’t worry. It’s not going to be as bad as you think. Along the way, I’ll show you several tricks that make it easy to create the service contract. (If that doesn’t make you feel better, I suggest you take an antacid and cut back on the spicy food at lunch.)

The basic recipe for developing a contract-first web service with Spring-WS appears in table 9.1.

Table 9.1 The steps for developing a contract-first web service.

Step
Action
What we’ll do
1 Define the service contract. This involves designing sample XML messages that will be processed by our web service. We’ll use these sample messages to create XML Schema that will later be used to create WSDL.
2 Write a service endpoint. We’ll create classes that will receive and process the messages sent to the web service.
3 Configure the endpoint and Spring-WS infrastructure. We’ll wire up our service endpoint along with a handful of Spring-WS beans that will tie everything together.

To demonstrate Spring-based web services, we’re going to build a poker hand evaluation service. Figure 9.1 illustrates the requirements for this web service: given five cards, identify the poker hand in question.

Since we’re creating a contract-first web service, it’s only logical that the first thing we should do is define the service contract. Let’s get started.

Файл:Fig91.png
Figure 9.1 We’ll build a poker hand evaluation web service. Given a poker hand made up of five cards, the web service will determine what kind of poker hand was dealt.

Defining the contract (first!)

The single most important activity in developing a contract-first web service is defining the contract itself. When defining the contract, we’ll define the messages that are sent to and received from the service, with no regard for how the service is implemented or how the messages will be handled.

Even though the topic of this chapter is Spring-WS, you’ll find that this section is remarkably Spring free. That’s because the contract of a web service should be defined independent of the implementation of the service. The focus is on what needs to be said, not how it needs to be done. We’ll tie this all into Spring-WS starting in section 9.3. But for now, the techniques described in this section are applicable to contract-first services in general, regardless of the underlying framework.

A contract-first view of web services places emphasis on the messages that are sent to and received from services. Therefore, the first step in defining a service’s contract is determining what the messages will look like. We’ll start by creating sample XML messages for our web services that we’ll use to define the service contract.

Creating sample XML messages

In simple terms, our poker hand evaluation service takes a poker hand made up of five cards as input and produces a poker hand designation (e.g., Full House, Flush, etc.) as output. Writing a sample input message for the service as XML might look a little like this:

<EvaluateHandRequest
xmlns="https://www.springinaction.com/poker/schemas">
<card>
<suit>HEARTS</suit>
<face>TEN</face>
</card>
<card>
<suit>SPADES</suit>
<face>KING</face>
</card>
<card>
<suit>HEARTS</suit>
<face>KING</face>
</card>
<card>
<suit>DIAMONDS</suit>
<face>TEN</face>
</card>
<card>
<suit>CLUBS</suit>
<face>TEN</face>
</card>
</EvaluateHandRequest>

That’s fairly straightforward, isn’t it? There are five <card> elements, each with a <suit> and a <face>. That pretty much describes a poker hand. All of the <card> elements are contained within an <EvaluateHandRequest> element, which is the message we’ll be sending to the service.

As simple as the input message was, the output message is even simpler:

<EvaluateHandResponse
xmlns="https://www.springinaction.com/poker/schemas">
<handName>Full House</handName>
</EvaluateHandResponse>

The <EvaluateHandResponse> message simply contains a single <handName> element that holds the designation of the poker hand.

These sample messages will serve as the basis for our service’s contract. And, although this may bring about some disbelief on your part, you should know that by defining these sample messages, we’ve already finished the hardest part of designing the service contract. No kidding.

Forging the data contract

Now we’re ready to create the service contract. Before we do that, however, let’s conceptually break the contact into two parts:


  • The data contract will define the messages going in and out of the service. In our example, this will include the schema definition of the <EvaluateHandRequest> and <EvaluateHandResponse> messages.
  • The operational contract will define the operations that our service will perform. Note that a SOAP operation does not necessarily correspond to a method in the service’s API.

Both of these contract parts are typically (but not necessarily) defined in a single WSDL file. The WSDL file usually contains an embedded XML Schema that defines the data contract. The rest of the WSDL file defines the operational contract, includingone or more <wsdl:operation>elementswithin the <wsdl:binding> element.

Файл:Fig92.png
Figure 9.2 Trang is an XSD inference tool that makes simple work of producing XML Schema from sample XML files.

Don’t worry yourself too much with the details of that last paragraph. I promised that creating the contract would be easy, so there’s no need for you to know the details of what goes into a WSDL file. The key point is that there are two distinct parts of the contract.

The data contract is defined using XML Schema (XSD). XSD allows us to precisely define what should go into a message. Not only can we define what elements are in the message, but we can also specify the types of those messages and place constraints on what data goes into the message.

Although it’s not terribly difficult to write an XSD file by hand, it’s more work than I care to do. So, I’m going to cheat a little by using an XSD inference tool. An XSD inference tool examines one or more XML files and, based on their contents, produces an XML schema that the XML files can be validated against.

Several XSD inference tools are available, but one that I like is called Trang. Trang is a command-line tool (available from www.thaiopensource.com/relaxng/ trang.html) that takes XML as input and produces an XSD file as output (see figure 9.2). Trang is Java based and thus can be used anywhere there’s a JVM. As the URL implies, Trang is useful for generating RELAX NG schemas (an alternative schema style), but is also useful for creating XML Schema files. For Spring-WS, we’ll be using Trang to generate XML Schema.

Once you’ve downloaded and unzipped Trang, you’ll find trang.jar in the distribution. This is an executable JAR file, so running Trang is simple from the command line:

% java -jar trang.jar EvaluateHandRequest.xml EvaluateHandResponse.xml PokerTypes.xsd

When running Trang, I’ve specified three command-line arguments. The first two are the sample message XML files that we created earlier. Because we’ve specified both message files, Trang is able to produce an XSD file that can validate the messages in both files. The last argument is the name of the file we want Trang to write the XSD to.

When run with these arguments, Trang will generate the data contract for our service in PokerTypes.xsd (listing 9.1).

Listing 9.1 PokerTypes.xsd, which defines the data contract for the web service

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="https://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace=
"https://www.springinaction.com/poker/schemas"
xmlns:schemas=
"https://www.springinaction.com/poker/schemas">
<xs:element name="EvaluateHandRequest">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded"
ref="schemas:card"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="card">
<xs:complexType>
<xs:sequence>
<xs:element ref="schemas:suit"/>
<xs:element ref="schemas:face"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="suit" type="xs:NCName"/>
<xs:element name="face" type="xs:NCName"/>
<xs:element name="EvaluateHandResponse">
<xs:complexType>
<xs:sequence>
<xs:element ref="schemas:handName"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="handName" type="xs:string"/>
</xs:schema>

Trang saved us a lot of trouble by inferring the XSD for our messages. We’re not completely off the hook, though. XSD isn’t perfect. As it infers the XSD, Trang makes some assumptions about what kind of data will be in your XML. Most of the time, those assumptions are okay. But often, we’ll need to fine-tune the generated XSD to be more precise.

For example, Trang assumed that the values of the <suit> and <face> elements should be defined as noncolonized1 names ( xs:NCName). What we actually want is for those elements to be simple strings (xs:string). So, let’s tweak the definitions of <suit> and <face> to be strings:

<xs:element name="suit" type="xs:string"/>
<xs:element name="face" type="xs:string"/>

A noncolonized name is a name that isn’t qualified with a namespace related prefix. Therefore, it does not have a colon (:)—it isn’t “colonized.”


We also know that there are only four possible values for the <suit> element, so we could constrain the message a bit further:

<xs:element name="suit" type="schemas:Suit" />
<xs:simpleType name="Suit">
<xsd:restriction base="xs:string">
<xsd:enumeration value="SPADES" />
<xsd:enumeration value="CLUBS" />
<xsd:enumeration value="HEARTS" />
<xsd:enumeration value="DIAMONDS" />
</xsd:restriction>
</xs:simpleType>

Likewise, there are only 13 legal values for the <face> element, so let’s define those limits in XSD:

<xs:element name="face" type="schemas:Face" />
<xs:simpleType name="Face">
<xsd:restriction base="xs:string">
<xsd:enumeration value="ACE" />
<xsd:enumeration value="TWO" />
<xsd:enumeration value="THREE" />
<xsd:enumeration value="FOUR" />
<xsd:enumeration value="FIVE" />
<xsd:enumeration value="SIX" />
<xsd:enumeration value="SEVEN" />
<xsd:enumeration value="EIGHT" />
<xsd:enumeration value="NINE" />
<xsd:enumeration value="TEN" />
<xsd:enumeration value="JACK" />
<xsd:enumeration value="QUEEN" />
<xsd:enumeration value="KING" />
</xsd:restriction>
</xs:simpleType>

Also, notice that Trang incorrectly assumes that the <EvaluateHandRequest> may contain an unlimited number of <card> elements (maxOccurs="unbounded"). But a poker hand contains exactly five cards. Therefore, we’ll need to adjust the definition of <EvaluateHandRequest> accordingly:

<xs:element name="EvaluateHandRequest">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="5" maxOccurs="5"
ref="schemas:card"/>
</xs:sequence>
</xs:complexType>
</xs:element>

As for <EvaluateHandResponse>, it’s fine as is. We could constrain the possible values returned in the <handName> element, but it’s not necessary. So, we’ll leave it unchanged.

Now we have the data contract for the poker hand evaluation service, but what about the operational contract? Aren’t we going to need some WSDL to completely define the web service?

Yes, we’ll absolutely need WSDL—after all, WSDL is the standard for defining web services. We could write the WSDL by hand, but that’s no fun. And, again, I promised you that this would be easy. But I’m going to have to ask you to wait awhile to see where the operational contract comes into play. I’ll show you how the WSDL gets created in section 9.4.6 when we wire a WSDL definition bean in Spring.

But first, we need to create a service endpoint. The contract only defines the messages sent to and from the service, not how they’re handled. Let’s see how to create message endpoints in Spring-WS that will process messages from a web service client.

Handling messages with service endpoints

As you’ll recall from the opening of this chapter, a well-designed application doesn’t allow direct access to the internal objects that do the fine-grained tasks of a system. In Spring MVC, for example, a user interacts with the application through controllers, which in turn translate the user’s requests into calls to internal objects.

Файл:Fig93.png
Figure 9.3 Message endpoints are the implementation of a web service in Spring-WS. Taking a message-centric approach, message endpoints process incoming XML messages and produce XML responses.

It may be helpful to know that Spring MVC and Spring-WS are a lot alike. Whereas a user interacts with a Spring MVC application through one of several controllers, a web service client interacts with a Spring-WS application through one of several message endpoints.

Figure 9.3 illustrates how message endpoints interact with their client. A message endpoint is a class that receives an XML message from the client and, based on the content of the message, makes calls to internal application objects to perform the actual work. For the poker hand evaluation service, the message endpoint will process <EvaluateHandRequest> messages.

Once the endpoint has completed processing, it will return its response in yet another XML message. In the case of the poker hand evaluation service, the response XML is an <EvaluateHandResponse> document.

Spring-WS defines several abstract classes from which message endpoints can be created, as listed in table 9.2.

For the most part, all of the abstract endpoint classes in table 9.2 are similar. Which one you choose is mostly a matter of taste and which XML parsing technology you prefer (e.g., SAX versus DOM versus StAX, etc.). But AbstractMarshallingPayloadEndpoint is a bit different from the rest of the pack in that it supports automatic marshaling and unmarshaling of XML messages to and from Java objects.

Table 9.2 The message endpoint options available with Spring-WS.

Abstract endpoint class in package org.springframework.ws.server.endpoint
Description
AbstractDom4jPayloadEndpoint Endpoint that handles message payloads as dom4j Elements
AbstractDomPayloadEndpoint Endpoint that handles message payloads as DOM Elements
AbstractJDomPayloadEndpoint Endpoint that handles message payloads as JDOM Elements
AbstractMarshallingPayloadEndpoint Endpoint that unmarshals the request payload into an object and marshals the response object into XML
AbstractSaxPayloadEndpoint Endpoint that handles message payloads through a SAX ContentHandler implementation
AbstractStaxEventPayloadEndpoint Endpoint that handles message payloads using event-based StAX
AbstractStaxStreamPayloadEndpoint Endpoint that handles message payloads using streaming StAX
AbstractXomPayloadEndpoint Endpoint that handles message payloads as XOM Elements

We’ll have a look at AbstractMarshallingPayloadEndpoint a little later in this chapter (in section 9.3.2). First, though, let’s see how to build an endpoint that processes XML messages directly.

Building a JDOM-based message endpoint

Our poker hand evaluation web service takes an <EvaluateHandRequest> message as input and produces an <EvaluateHandResponse> as output. Therefore, we’ll need to create a service endpoint that processes an <EvaluateHandRequest> element and produces an <EvaluateHandResponse> element.

Any of the abstract endpoint classes in table 9.2 will do, but we’ve chosen to base our endpoint on AbstractJDomPayloadEndpoint. This choice was mostly arbitrary, but I also like JDOM’s XPath support, which is a simple way to extract information out of a JDOM Element. (For more information on JDOM, visit the JDOM homepage at https://www.jdom.org.)

EvaluateHandJDomEndpoint (listing 9.2) extends AbstractJDomPayloadEndpoint to provide the functionality required to process the <EvaluateHandRequest> message.

Listing 9.2 An endpoint that will process the <EvaluateHandRequest>message

package com.springinaction.poker.webservice;
import java.util.Iterator;
import java.util.List;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.xpath.XPath;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.ws.server.endpoint.AbstractJDomPayloadEndpoint;
import com.springinaction.poker.Card;
import com.springinaction.poker.Face;
import com.springinaction.poker.PokerHand;
import com.springinaction.poker.PokerHandEvaluator;
import com.springinaction.poker.PokerHandType;
import com.springinaction.poker.Suit;
public class EvaluateHandJDomEndpoint
extends AbstractJDomPayloadEndpoint
implements InitializingBean {
private Namespace namespace;
private XPath cardsXPath;
private XPath suitXPath;
private XPath faceXPath;
 
protected Element invokeInternal(Element element)
throws Exception {
Card cards[] = extractCardsFromRequest(element);
PokerHand pokerHand = new PokerHand();
pokerHand.setCards(cards);
PokerHandType handType =
pokerHandEvaluator.evaluateHand(pokerHand);
return createResponse(handType);
}
private Element createResponse(PokerHandType handType) {
Element responseElement =
new Element("EvaluateHandResponse", namespace);
responseElement.addContent(
new Element("handName", namespace).setText(
handType.toString()));
return responseElement;
}
private Card[] extractCardsFromRequest(Element element)
throws JDOMException {
Card[] cards = new Card[5];
List cardElements = cardsXPath.selectNodes(element);
for(int i=0; i < cardElements.size(); i++) {
Element cardElement = (Element) cardElements.get(i);
Suit suit = Suit.valueOf(
suitXPath.valueOf(cardElement));
Face face = Face.valueOf(
faceXPath.valueOf(cardElement));
cards[i] = new Card();
cards[i].setFace(face);
cards[i].setSuit(suit);
}
return cards;
}
public void afterPropertiesSet() throws Exception {
namespace = Namespace.getNamespace("poker",
"https://www.springinaction.com/poker/schemas");
cardsXPath =
XPath.newInstance("/poker:EvaluateHandRequest/poker.card");
cardsXPath.addNamespace(namespace);
faceXPath = XPath.newInstance("poker:face");
faceXPath.addNamespace(namespace);
suitXPath = XPath.newInstance("poker:suit");
suitXPath.addNamespace(namespace);
}
// injected
 
private PokerHandEvaluator pokerHandEvaluator;
public void setPokerHandEvaluator(
PokerHandEvaluator pokerHandEvaluator) {
this.pokerHandEvaluator = pokerHandEvaluator;
}
}private PokerHandEvaluator pokerHandEvaluator;
public void setPokerHandEvaluator(
PokerHandEvaluator pokerHandEvaluator) {
this.pokerHandEvaluator = pokerHandEvaluator;
}
}

The invokeInternal() method is the entry point into this endpoint. When called, it is passed a JDOM Element object that contains the incoming message—in this case, an <EvaluateHandRequest>. invokeInternal() hands off the Element to the extractCardsFromRequest() method, which uses JDOM XPath objects to pull card information out of the <EvaluateHandRequest> element.

After an array of Card objects is returned, invokeInternal() then does the right thing and passes those Cards to an injected PokerHandEvaluator to evaluate the poker hand. PokerHandEvaluator is defined by the following interface:

package com.springinaction.poker;
public interface PokerHandEvaluator {
PokerHandType evaluateHand(PokerHand hand);
}

The actual implementation of PokerHandEvaluator isn’t relevant to the discussion of building web services with Spring-WS, so I’ll leave it out (but you can find it in the downloadable examples).

The fact that the endpoint calls PokerHandEvaluator’s evaluateHand() method is significant. A properly written Spring-WS endpoint shouldn’t perform any business logic of its own. It should only mediate between the client and the internal API. The actual business logic is performed in the PokerHandEvaluator implementation. Later, in chapter 13, we’ll see a similar pattern applied to Spring MVC controllers where a controller merely sits between a web user and a serverside object.

Once the PokerHandEvaluator has determined the type of poker hand it was given, invokeInternal() passes the PokerHandType object off to createResponse() to produce an <EvaluateHandResponse> element using JDOM. The resulting JDOM Element is returned and EvaluateHandJDomEndpoint’s job is done.

Файл:Fig94.png
Figure 9.4 Marshaling endpoints leverage a marshaler/unmarshaler to handle XML messages so that the endpoint only has to deal with POJOs.

EvaluateHandJDomEndpoint is a fine example of how to implement a SpringWS endpoint. But there are an awful lot of XML specifics in there. Although the messages handled by Spring-WS endpoints are XML, there’s usually no reason why your endpoint needs to be written to know that. Let’s see how a marshaling endpoint can help us eliminate all of that XML parsing code.

Marshaling message payloads

As we mentioned before, AbstractMarshallingPayloadEndpoint is a little different from all of the other Spring-WS abstract endpoint classes. Instead of being given an XML Element to pull apart for information, AbstractMarshallingPayloadEndpoint is given an object to process.

Actually, as illustrated in figure 9.4, a marshaling endpoint works with an unmarshaler that converts an incoming XML message into a POJO. Once the endpoint is finished, it simply returns a POJO and a marshaler converts it into an XML message to be returned to the client. This greatly simplifies the endpoint implementation, as it no longer has to include any XML-processing code.

For example, consider listing 9.3, which shows EvaluateHandMarshallingEndpoint, a new implementation of our poker hand evaluation endpoint that extends AbstractMarshallingPayloadEndpoint.

Listing 9.3 The endpoint that will process the '''<EvaluateHandRequest> '''message

package com.springinaction.poker.webservice;
import org.springframework.ws.server.endpoint.
? AbstractMarshallingPayloadEndpoint;
import com.springinaction.poker.PokerHand;
import com.springinaction.poker.PokerHandEvaluator;
import com.springinaction.poker.PokerHandType;
public class EvaluateHandMarshallingEndpoint
extends AbstractMarshallingPayloadEndpoint {
protected Object invokeInternal(Object object)
throws Exception {
EvaluateHandRequest request =
(EvaluateHandRequest) object;
PokerHand pokerHand = new PokerHand();
pokerHand.setCards(request.getHand());
PokerHandType pokerHandType =
pokerHandEvaluator.evaluateHand(pokerHand);
return new EvaluateHandResponse(pokerHandType);
}
// injected
private PokerHandEvaluator pokerHandEvaluator;
public void setPokerHandEvaluator(
PokerHandEvaluator pokerHandEvaluator) {
this.pokerHandEvaluator = pokerHandEvaluator;
}
}

The first thing that you probably noticed about EvaluateHandMarshallingEndpoint is that it is much shorter than EvaluateHandJDomEndpoint. That’s because EvaluateHandMarshallingEndpoint doesn’t have any of the XML parsing code that was necessary in EvaluateHandJDomEndpoint.

Instead, the invokeInternal() method is given an Object to process. In this case, the Object is an EvaluateHandRequest:

package com.springinaction.poker.webservice;
import com.springinaction.poker.Card;
public class EvaluateHandRequest {
private Card[] hand;
public EvaluateHandRequest() {}
public Card[] getHand() {
return hand;
}
public void setHand(Card[] cards) {
this.hand = cards;
}
}

On the other end of invokeInternal(), an EvaluateHandResponse object is returned. EvaluateHandResponse looks like this:

package com.springinaction.poker.webservice;
import com.springinaction.poker.PokerHandType;
public class EvaluateHandResponse {
private PokerHandType pokerHand;
public EvaluateHandResponse() {
this(PokerHandType.NONE);
}
public EvaluateHandResponse(PokerHandType pokerHand) {
this.pokerHand = pokerHand;
}
public PokerHandType getPokerHand() {
return this.pokerHand;
}
public void setPokerHand(PokerHandType pokerHand) {
this.pokerHand = pokerHand;
}
}

So how is an incoming <EvaluateHandRequest> XML message transformed into an EvaluateHandRequest object? And, while we’re on the subject, how does an EvaluateHandResponse object end up being an <EvaluateHandResponse> message that gets sent to the client?

What you don’t see here is that AbstractMarshallingPayloadEndpoint has a reference to an XML marshaler. When it receives an XML message, it uses the marshaler to turn the XML message into an object before calling invokeInternal(). Then, when invokeInternal() is finished, the marshaler turns the object returned into an XML message.

A large part of Spring-WS is an object-XML mapping (OXM) abstraction. Spring-WS’s OXM comes with support for several OXM implementations, including:


  • JAXB (versions 1 and 2)
  • Castor XML
  • JiBX
  • XMLBeans
  • XStream

You may be wondering which OXM I chose for EvaluateHandMarshallingEndpoint. I’ll tell you, but not yet. The important thing to note here is that EvaluateHandMarshallingEndpoint has no idea where the Object that is passed to invokeInternal() came from. In fact, there’s no reason why the Object even has to have been created from unmarshaled XML.

This highlights a key benefit of using a marshaling endpoint. Because it takes a simple object as a parameter, EvaluateHandMarshallingEndpoint can be unittested just like any other POJO. The test case can simply pass in an EvaluateHandRequest object and make assertions on the returned EvaluateHandResponse.

Now that we’ve written our service endpoint, we’re ready to wire it up in Spring.

Wiring it all together

We’re finally down to the final stage of developing a Spring-WS service. We need to configure the Spring application context with our endpoint bean and a handful of infrastructure beans required by Spring-WS.

Spring-WS is based on Spring MVC (which we’ll see more of in chapter 13). In Spring MVC, all requests are handled by DispatcherServlet, a special servlet that dispatches requests to controller classes that process the requests. Similarly, Spring-WS can be fronted by MessageDispatcherServlet, a subclass of DispatcherServlet that knows how to dispatch SOAP requests to Spring-WS endpoints.


The “web” in web services seems to imply that all web services are served over HTTP. But that’s not necessarily true. Spring-WS has support for JMS, email, and raw TCP/IP-based web services. Nevertheless, since most web services are, in fact, served over HTTP, that’s the configuration I’ll talk about here.

MessageDispatcherServlet is a fairly simple servlet and can be configured in a web application’s web.xml with the following <servlet> and <servletmapping> elements:

<servlet>
<servlet-name>poker</servlet-name>
<servlet-class>org.springframework.ws.transport.http.
MessageDispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>poker</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>

We’ll tweak this MessageDispatcherServlet’s configuration a little later, but this will get us started for now.

MessageDispatcherServlet is only the front end of Spring-WS. There are a handful of beans that we’ll need to wire in the Spring application context. Let’s see what those beans are and what they do.

Spring-WS: The big picture

Файл:Fig95.png
Figure 9.5 Spring-WS service configuration consists of several beans, including mappings, endpoints, marshalers, and other utility beans.

Over the next several pages, we’re going to configure several beans in the Spring context. Before we get too deep in the XML, it is probably worthwhile to have a look at the big picture to see what we’re about to do. Figure 9.5 shows the beans we’ll define and how they relate to one another.

Figure 9.5 shows the beans that we’ll configure for the poker hand evaluation service and how they relate to one another. But what are these beans and what do they do? To summarize these six beans:


  • payloadMapping—Maps incoming XML messages to an appropriate endpoint. In this case, we’ll use a mapping that looks up endpoints using the incoming XML’s root element (by its qualified name).
  • evaluateHandEndpoint—This is the endpoint that will process the incoming XML message for the poker hand evaluation service.
  • marshaller—The evaluateHandEndpoint could be written to process the incoming XML as a DOM or JDOM element, or even as a SAX event handler. Instead, the marshaller bean will automatically convert XML to and from Java objects.
  • pokerHandEvaluator—This is a POJO that performs the actual poker hand processing. evaluateHandEndpoint will use this bean to do its work.
  • endpointExceptionResolver—This is a Spring-WS bean that will automatically convert any Java exceptions thrown while processing a request into appropriate SOAP faults.
  • poker—Although it’s not obvious from its name, this bean will serve the WSDL for the poker hand web service to the client. Either it can serve handcreated WSDL or it can be wired to automatically generate WSDL from the message’s XML Schema.

Now that we have a roadmap of where we’re going, let’s dive right into configuring Spring-WS, starting with the message handler adapter.

Mapping messages to endpoints

When a client sends a message, how does MessageDispatcherServlet know which endpoint should process it? Even though we’re only building one endpoint in this chapter’s example (the evaluate hand endpoint), it’s quite possible that MessageDispatcherServlet could be configured with several endpoints. We need a way to map incoming messages to the endpoints that process them.

In chapter 13, we’ll see how Spring MVC’s DispatcherServlet maps browser requests to Spring MVC controllers using handler mappings. In a similar way, MessageDispatcherServlet uses an endpoint mapping to decide which endpoint should receive an incoming XML message.

For the poker hand evaluation service, we’ll use Spring-WS’s PayloadRootQNameEndpointMapping, which is configured in Spring like this:

<bean id="payloadMapping"
class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
<property name="endpointMap">
<map>
<entry key=
"{https://www.springinaction.com/poker/schemas}EvaluateHandRequest"
value-ref="evaluateHandEndpoint" />
</map>
</property>
</bean>

PayloadRootQNameEndpointMapping maps incoming SOAP messages to endpoints by examining the qualified name (QName) of the message’s payload and looking up the endpoint from its list of mappings (configured through the endpointMap property).

In our example, the root element of the message is <EvaluateHandRequest> with a namespace URI of https://www.springinaction.com/poker/schemas. This makes the QName of the message {https://www.springinaction.com/poker/schemas}EvaluateHandRequest. We’ve mapped this QName to a bean named evaluateHandEndpoint, which is our endpoint implementation that we created in section 9.3.2.

Wiring the service endpoint

Now we’re finally down to wiring the endpoint that will process our message. If you chose to use the JDOM-based endpoint then it is configured in Spring like this:

<bean id="evaluateHandEndpoint"
class="com.springinaction.poker.webservice.EvaluateHandJDomEndpoint">
<property name="pokerHandEvaluator"
ref="pokerHandEvaluator" />
</bean>

The only property that must be injected is the pokerHandEvaluator property. Remember that EvaluateHandJDomEndpoint doesn’t actually evaluate the poker hand, but delegates to an implementation of PokerHandEvaluator to do the dirty work. Thus, the pokerHandEvaluator bean should be configured like this:

<bean id="pokerHandEvaluator"
class="com.springinaction.poker.PokerHandEvaluatorImpl"/>

If the JDOM-based endpoint didn’t suit you and instead you chose to use EvaluateHandMarshallingEndpoint, a bit of extra configuration is involved:

<bean id="evaluateHandEndpoint"
class="com.springinaction.poker.webservice.EvaluateHandMarshallingEndpoint">
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
<property name="pokerHandEvaluator"
ref="pokerHandEvaluator" />
</bean>

Again, the pokerHandEvaluator property is injected with a reference to a PokerHandEvaluatorImpl. But the marshaling endpoint must have its marshaller and unmarshaller properties set as well. Here we’ve wired them with references to the same marshaller bean, which we’ll configure next.

Configuring a message marshaler

The key to translating objects to and from XML messages is object-XML mapping (OXM). Spring-OXM is a subproject of Spring-WS that provides an abstraction layer over several popular OXM solutions, including JAXB and Castor XML.

The central elements of Spring-OXM are its Marshaller and Unmarshaller interfaces. Implementations of Marshaller are expected to generate XML elements from Java objects. Conversely, Unmarshaller implementations are used to construct Java objects from XML elements.

AbstractMarshallingPayloadEndpoint takes advantage of the Spring-OXM marshalers and unmarshalers when processing messages. When AbstractMarshallingPayloadEndpoint receives a message, it hands it off to an Unmarshaller to unmarshal the XML message into an object that is passed to invokeInternal(). Then, when invokeInternal() is finished, the object returned is given to a Marshaller to marshal the object into XML that will be returned to the client.

Fortunately, you won’t have to create your own implementations of Marshaller and Unmarshaller. Spring-OXM comes with several implementations, as listed in table 9.3.

Table 9.3 Marshalers transform objects to and from XML. Spring-OXM provides several marshaling options that can be used with Spring-WS.

OXM solution
Spring-OXM marshaler
Castor XML org.springframework.oxm.castor.CastorMarshaller
JAXB v1 org.springframework.oxm.jaxb.Jaxb1Marshaller
JAXB v2 org.springframework.oxm.jaxb.Jaxb2Marshaller
JiBX org.springframework.oxm.jibx.JibxMarshaller
XMLBeans org.springframework.oxm.xmlbeans.XmlBeansMarshaller
XStream org.springframework.oxm.xstream.XStreamMarshaller

As you can see, table 9.3 only lists marshaler classes. That’s not an oversight, though. Conveniently, all of the marshaler classes in table 9.3 implement both the Marshaller and the Unmarshaller interfaces to provide one-stop solutions for OXM marshaling.

The choice of OXM solution is largely a matter of taste. Each of the OXM options offered by Spring-WS has its good and bad points. XStream, however, has limited support for XML namespaces, which are necessary in defining the types for web services. Therefore, while Spring-OXM’s XStream may prove useful for general-purpose XML serialization, it should be disregarded for use with web services.

For the poker hand evaluator service, we chose to use Castor XML. Therefore, we’ll need to configure a CastorMarshaller in Spring:

<bean id="marshaller"
class="org.springframework.oxm.castor.CastorMarshaller">
<property name="mappingLocation"
value="classpath:mapping.xml" />
</bean>

Castor XML can do some basic XML marshaling without any additional configuration. But our OXM needs are a bit more complex than what default Castor XML can handle. Consequently, we’ll need to configure CastorMarshaller to use a Castor XML mapping file. The mappingLocation property specifies the location of a Castor XML mapping file. Here we’ve configured mappingLocation to look for a mapping file with the name mapping.xml in the root of the application’s classpath.

As for the mapping.xml file itself, it is shown in listing 9.4.

Listing 9.4 Castor XML mapping file for poker hand service types

<?xml version="1.0"?>
<!DOCTYPE mapping PUBLIC
"-//EXOLAB/Castor Object Mapping DTD Version 1.0//EN"
"https://castor.exolab.org/mapping.dtd">
<mapping xmlns="https://castor.exolab.org/">
<class name="com.springinaction.poker.webservice.EvaluateHandRequest">
<map-to xml="EvaluateHandRequest" />
<field name="hand"
collection="array"
type="com.springinaction.poker.Card"
required="true">
<bind-xml name="card" node="element" />
</field>
</class>
<class name="com.springinaction.poker.Card">
<map-to xml="card" />
<field name="suit"
type="com.springinaction.poker.Suit"
required="true">
<bind-xml name="suit" node="element" />
</field>
<field name="face"
type="com.springinaction.poker.Face"
required="true">
<bind-xml name="face" node="element" />
</field>
</class>
<class name="com.springinaction.poker.webservice.EvaluateHandResponse">
<map-to xml="EvaluateHandResponse"
ns-uri=
"https://www.springinaction.com/poker/schemas"
ns-prefix="tns" />
<field name="pokerHand"
type="com.springinaction.poker.PokerHandType"
required="true">
<bind-xml name="tns:handName" node="element"
QName-prefix="tns"
xmlns:tns=
"https://www.springinaction.com/poker/schemas"/>
</field>
</class>
</mapping>

Now we have configured an endpoint mapping bean, the endpoint implementation bean, and an XML marshaling bean. At this point the poker hand evaluator web service is mostly done. We could deploy it and stop for the day. But there are still a couple of beans left that will make the web service more complete. Let’s see how to make our web service more robust by declaring a bean that maps Java exceptions to SOAP faults.

Handling endpoint exceptions

Файл:Fig96.png
Figure 9.6 SoapFaultMappingExceptionResolver maps any Java exceptions thrown from a message endpoint into a SOAP fault to be returned to the client.

Things don’t always work out as expected. What will happen if a message can’t be marshaled to a Java object? What if the message isn’t even valid XML? Maybe the service endpoint or one of its dependencies throws an exception—then what should we do?

If an exception is thrown in the course of processing a message, a SOAP fault will need to be sent back to the client. Unfortunately, SOAP doesn’t know or care anything about Java exceptions. SOAP-based web services communicate failure using SOAP faults. We need a way to convert any Java exceptions thrown by our web service or by Spring-WS into SOAP faults.

For that purpose, Spring-WS provides SoapFaultMappingExceptionResolver. As shown in figure 9.6, SoapFaultMappingExceptionResolver will handle any uncaught exceptions that occur in the course of handling a message and produce an appropriate SOAP fault that will be sent back to the client.

For our service, we’ve configured a SoapFaultMappingExceptionResolver in Spring that looks like this:

<bean id="endpointExceptionResolver"
class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.springframework.oxm.UnmarshallingFailureException">
SENDER,Invalid message received</prop>
<prop key="org.springframework.oxm.ValidationFailureException">
SENDER,Invalid message received</prop>
</props>
</property>
<property name="defaultFault"
value="RECEIVER,Server error" />
</bean>

The exceptionMappings property is configured with one or more SOAP fault definitions mapped to Java exceptions. The key of each <prop> is a Java exception that needs to be translated to a SOAP fault. The value of the <prop> is a two-part value where the first part is the type of fault that is to be created and the second part is a string that describes the fault.

SOAP faults come in two types: sender and receiver faults. Sender faults typically indicate that the problem is on the client (e.g., the sender) side. Receiver faults indicate that the web service (e.g., the receiver) received a message from the client but is having some problem processing the message.

For example, if a service receives an XML message that can’t be unmarshaled, the marshaler will throw an org.springframework.oxm.UnmarshallingFailureException. Because the sender created the useless XML, this is a sender fault. As for the message, it is simply set to “Invalid message received” to indicate the nature of the problem.

Файл:Fig97.png
Figure 9.7 DynamicWsdl11Definitionautomatically produces WSDL for a web service based on the XML Schema that validates the service’s messages.

An org.springframework.oxm.ValidationFailureException is handled the same way.Any exceptions not explicitly mapped in the exceptionMappings property will be handled by the fault definition in the defaultFault property. In this case, we’re assuming that if the exception thrown doesn’t match any of the mapped exceptions, it must be a problem on the receiving side. Thus, it is a receiver fault and the message simply states “Server error.”

Serving WSDL files

Finally, I’m going to make good on my promise to show you where the WSDL file for the poker hand evaluation web service comes from. As you recall from section 9.2.1, we’ve already created the data portion of the contract as XML Schema in PokerTypes.xsd. Before we go any further, you may want to turn back to listing 9.1 to review the details of the data service.

Pay particular attention to the names I chose for the XML elements that make up our web service messages: EvaluateHandRequest and EvaluateHandResponse. These names weren’t chosen arbitrarily. I chose them purposefully to take advantage of a convention-over-configuration feature in Spring-WS that will automatically create WSDL for the poker hand evaluation service.

To make this work, we’ll need to configure Spring-WS’s DynamicWsdl11Definition. DynamicWsdl11Definition is a special bean that MessageDispatcherServlet works with to generate WSDL from XML Schema. This will come in handy, as we already have some XML Schema defined for the data portion of the contract. Here’s how I’ve configured DynamicWsdl11Definition in Spring:

<bean id="poker"
class="org.springframework.ws.wsdl.wsdl11.
DynamicWsdl11Definition">

<property name="builder">
<bean class="org.springframework.ws.wsdl.wsdl11.builder.
XsdBasedSoap11Wsdl4jDefinitionBuilder">

<property name="schema" value="/PokerTypes.xsd"/>
<property name="portTypeName" value="Poker"/>
<property name="locationUri"
value="https://localhost:8080/Poker-WS/services"/>
</bean>
</property>
</bean>

DynamicWsdl11Definition works by reading an XML Schema definition, specified here as PokerTypes.xsd by the schema property. It looks through the schema file for any element definitions whose names end with Request and Response. It assumes that those suffixes indicate a message that is to be sent to or from a web service operation and creates a corresponding <wsdl:operation> element in the WSDL it produces, as shown in figure 9.7.

For example, when DynamicWsdl11Definition processes the PokerTypes.xsd file, it assumes that the EvaluateHandRequest and EvaluateHandResponse elements are input and output messages for an operation called EvaluateHand. Consequently, the following definition is placed in the generated WSDL:

<wsdl:portType name="Poker">
<wsdl:operation name="EvaluateHand">
<wsdl:input message="schema:EvaluateHandRequest"
name="EvaluateHandRequest">
</wsdl:input>
<wsdl:output message="schema:EvaluateHandResponse"
name="EvaluateHandResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>
Файл:Fig98.png
Figure 9.8 The URL configured in the locationUri property.

Notice that DynamicWsdl11Definition placed the EvaluateHand <wsdl:operation> within a <wsdl:portType> with the name Poker. It named the <wsdl:portType> using the value wired into its portTypeName property.

The last of DynamicWsdl11Definition’s properties that we’ve configured is locationUri. This property tells the client where the service can be found. The diagram in figure 9.8 breaks down the URL configured in the locationUri property.

In this case, I’m assuming that it will be running on the local machine, but you’ll want to change the URL if you’ll be running it on a different machine. Notice that the URL ends with /services, which matches the <servlet-mapping> that we created for MessageDispatcherServlet.

Speaking of <servlet-mapping>s, we’ll also need to add a new <servlet-mapping> '''''to web.xml so that '''''MessageDispatcherServlet '''''will serve WSDL definitions. The following '''''<nowiki><servlet-mapping> definition should do the trick:

<servlet-mapping>
<servlet-name>poker</servlet-name>
<url-pattern>*.wsdl</url-pattern>
</servlet-mapping>
Файл:Fig99.png
Figure 9.9 SimpleWsdl11Definition simply serves a predefined WSDL file through the MessageDispatcherServlet. It can optionally be configured to transform the service’s address location to match the location of MessageDispatcherServlet.

Now MessageDispatcherServlet has been configured (through DynamicWsdl11Definition) to automatically produce WSDL for the poker hand evaluation service. The only question left unanswered at this point is where to find the generated WSDL.

The generated WSDL can be found at https://localhost:8080/Poker-WS/poker.wsdl. How did I know that? I know that MessageDispatcherServlet is mapped to *.wsdl, so it will attempt to create WSDL for any request that matches that pattern. But how did it know to produce WSDL for our poker service at poker.wsdl?

The answer to that question lies in one last bit of convention followed by MessageDispatcherServlet. Notice that I declared the DynamicWsdl11Definition bean to have an ID of poker. When MessageDispatcherServlet receives a request for /poker.wsdl, it will look in the Spring context for a WSDL definition bean named poker. In this case, it will find the DynamicWsdl11Definition bean that I configured.

Using predefined WSDL

DynamicWsdl11Definition is perfect for most situations, as it keeps you from having to write the WSDL by hand. But you may have special circumstances that require you to have more control over what goes into the service’s WSDL definition. In that case you’ll need to create the WSDL yourself and then wire it into Spring using SimpleWsdl11Definition:

<bean id="poker"
class="org.springframework.ws.
wsdl.wsdl11.SimpleWsdl11Definition">

<property name="wsdl"
value="/PokerService.wsdl"/>
</bean>

SimpleWsdl11Definition doesn’t generate WSDL (see figure 9.9); it just serves WSDL that you’ve provided through the wsdl property.

The only problem with predefined WSDL (aside from the trouble that it takes to create it) is that it is statically defined. This creates a problem for the part of the WSDL that specifies the service’s location. For example, consider the following (statically defined) chunk of WSDL:

<wsdl:service name="PokerService">
<wsdl:port binding="tns:PokerBinding" name="PokerPort">
<wsdlsoap:address
location="https://localhost:8080/Poker-WS/services"/>
</wsdl:port>
</wsdl:service>

Here the service is defined as being available at [1] WS/services. That is probably okay for development purposes, but it will need to be changed when you deploy it to another server. You could manually change the WSDL file every time you deploy it to another server, but that’s cumbersome and is susceptible to mistakes.

But MessageDispatcherServlet knows where it’s deployed and it knows the URL of requests used to access it. So, instead of tweaking the WSDL every time you deploy it to another server, why not let MessageDispatcherServlet rewrite it for you?

All you need to do is to set an <init-param> named transformWsdlLocations to true and MessageDispatcherServlet will take it from there:

<servlet>
<servlet-name>poker</servlet-name>
<servlet-class>org.springframework.ws.transport.http.
MessageDispatcherServlet</servlet-class>
<init-param>
<param-name>transformWsdlLocations</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
When transformWsdlLocations

When transformWsdlLocations is set to true, MessageDispatcherServlet will rewrite the WSDL served by SimpleWsdl11Definition to match the request’s URL.

Deploying the service

We’ve defined our contract, the endpoint has been written, and all of the SpringWS beans are in place. At this point, we’re ready to package up the web service application and deploy it. Since I chose to use Maven 2 for this project, creating a deployable WAR file is as simple as typing the following at the command line:

% mvn package deploy

Once Maven’s finished, there will be a Poker-WS.war file in the target directory, suitable for deployment in most web application servers.

Using Spring-WS to build a web service only demonstrates half of its capabilities. Spring-WS also comes with a client API based on the same message-centric paradigm that Spring-WS promotes on the service side. Let’s see how to build a client to consume the poker hand evaluation service using Spring-WS client templates.

Consuming Spring-WS web services

In chapter 8, you saw how to use JaxRpcPortProxyFactoryBean and XFireClientFactoryBean to build clients that communicate with remote web services. But both of those take a remote object view of web services, treating web services as remote objects whose methods can be invoked locally. Throughout this chapter, we’ve been talking about a message-centric approach to web services where clients send XML messages to a web service and receive XML messages back in response. A different paradigm on the service side demands a different paradigm on the client side as well. That’s where Spring-WS’s WebServiceTemplate comes in.

WebServiceTemplate is the centerpiece of Spring-WS’s client API. As shown in

figure 9.10, it employs the Template design pattern to provide the ability to send and receive XML messages from message-centric web services. We’ve already seen how Spring uses the Template pattern for its data access abstractions in chapter 5.

Файл:Fig910.png
Figure 9.10 WebServiceTemplateis the central class in Spring-WS’s client API. It sends and receives XML messages to and from web services on behalf of a client.

As we look at Spring-WS’s client API, you’ll find that it resembles the data access API in many ways.

To demonstrate WebServiceTemplate, we’ll create several different implementations of the PokerClient interface, which is defined as follows:

package com.springinaction.ws.client;
import java.io.IOException;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
public interface PokerClient {
PokerHandType evaluateHand(Card[] cards)
throws IOException;
}

Each implementation will show a different way of using WebServiceTemplate to send messages to the poker hand evaluation web service.

But first things first… Let’s configure WebServiceTemplate as a bean in Spring.

Working with web service templates

As I’ve already mentioned, WebServiceTemplate is the central class in the SpringWS client API. Sending messages to a web service involves producing SOAP envelopes and communications boilerplate code that is pretty much the same for every web service client. When sending messages to a Spring-WS client, you’ll certainly want to rely on WebServiceTemplate to handle the grunt work so that you can focus your efforts on the business logic surrounding your client.

Configuring WebServiceTemplate in Spring is rather straightforward, as shown in this typical <bean> declaration:

<bean id="webServiceTemplate"
class="org.springframework.ws.client.core.WebServiceTemplate">
<property name="messageFactory">
<bean class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
</property>
<property name="messageSender" ref="messageSender"/>
</bean>

WebServiceTemplate needs to know how to construct the message that will be sent to the service and how to send the message. The object wired into the messageFactory property handles the task of constructing the message. It should be wired with an implementation of Spring-WS’s WebServiceMessageFactory interface. Fortunately, you won’t have to worry about implementing WebServiceMessageFactory, as Spring-WS comes with three suitable choices (shown in table 9.4).

Table 9.4 WebServiceTemplate relies on a message factory to construct the message sent to a web service. Spring-WS provides three message factory implementations to choose from.

Message factory
What it does
AxiomSoapMessageFactory Produces SOAP messages using the AXIs Object Model (AXIOM). Based on the StAX streaming XML API. Useful when working with large messages and performance is a problem.
DomPoxMessageFactory Produces Plain Old XML (POX) messages using a DOM. Use this message factory when neither the client nor the service cares to deal with SOAP.
SaajSoapMessageFactory Produces SOAP messages using the SOAP with Attachments API for Java (SAAJ). Because SAAJ uses a DOM, large messages could consume a lot of memory. If performance becomes an issue, consider using AxiomSoapMessageFactory instead.

Since the messages sent to and from the poker hand evaluation service are rather simple, I’ve chosen to wire a SaajSoapMessageFactory into WebServiceTemplate’s messageFactory property. (This is also the default message factory used by MessageDispatcherServlet.) If I were to decide later that performance is an issue, switching to AXIOM-based messages would be a simple matter of rewiring the messageFactory property.


It’s not all SOAP

Figure 9.10 is a bit misleading. It implies that Spring-WS only deals with SOAPbased web services. In fact, Spring-WS only uses SOAP if it is wired with an AxiomSoapMessageFactory or SaajSoapMessageFactory. The DomPoxMessageFactory supports POX messages that aren’t sent in a SOAP envelope. If you have an aversion to using SOAP, maybe DomPoxMessageFactory is for you. You may be interested in knowing that the upcoming Spring-WS 1.1 release will also include support for REST.

The messageSender property should be wired with a reference to an implementation of a WebServiceMessageSender. Again, Spring-WS provides a couple of appropriate implementations, as listed in table 9.5.

Table 9.5 Message senders send the messages to a web service. Spring-WS comes with two message senders.

Message sender
What it does
CommonsHttpMessageSender Sends the message using Jakarta Commons HTTP Client. Supports a preconfigured HTTP client, allowing advanced features such as HTTP authentication and HTTP connection pooling.
HttpUrlConnectionMessageSender Sends the message using Java’s basic facilities for

HTTP connections. Provides limited functionality.

The choice between CommonsHttpMessageSender and HttpUrlConnectionMessageSender boils down to a trade-off between functionality and another JAR dependency. If you won’t be needing the advanced features supported by CommonsHttpMessageSender (such as HTTP authentication), HttpUrlConnectionMessageSenderwillsuffice. But ifyou willneed those features then CommonsHttpMessageSender is a must—but you’ll have to be sure to include Jakarta Commons HTTP in your client’s classpath.

As the advanced features aren’t an issue for the poker hand evaluation web service, I’ve chosen HttpUrlConnectionMessageSender, which is configured like this in Spring:

<bean id="messageSender"
class="org.springframework.ws.transport.http.
HttpUrlConnectionMessageSender">

<property name="url"
value="https://localhost:8080/Poker-WS/services"/>
</bean>

The url property specifies the location of the service. Notice that it matches the URL in the service’s WSDL definition.

If I decide later that I’ll need to authenticate to use the poker hand evaluation web service, switching to CommonsHttpMessageSender is a simple matter of changing the messageSender bean’s class specification.

Sending a message

Once the WebServiceTemplate has been configured, it’s ready to use to send and receive XML to and from the poker hand evaluation service. WebServiceTemplate provides several methods for sending and receiving messages. This one, however, stands out as the most basic and easiest to understand:

public boolean sendAndReceive(Source requestPayload,
Result responseResult)
throws IOException

The sendAndReceive() method takes a java.xml.transform.Source and a java.xml.transform.Result as parameters. The Source object represents the message payload to send to the web service, while the Result object is to be populated with the message payload returned from the service.

Listing 9.5 shows TemplateBasedPokerClient, an implementation of the PokerClient interface that uses WebServiceTemplate’s sendAndReceive() method to communicate with the poker hand evaluation service.

Listing 9.5 Client that uses an injected WebServiceTemplate to send and receive XML messages from the poker hand evaluation service

package com.springinaction.ws.client;
import java.io.IOException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.transform.JDOMResult;
import org.jdom.transform.JDOMSource;
import org.springframework.ws.client.core.WebServiceTemplate;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
public class TemplateBasedPokerClient
implements PokerClient {
public PokerHandType evaluateHand(Card[] cards)
throws IOException {
Element requestElement =
new Element("EvaluateHandRequest");
Namespace ns = Namespace.getNamespace(
"https://www.springinaction.com/poker/schemas");
requestElement.setNamespace(ns);
Document doc = new Document(requestElement);
for(int i=0; i<cards.length; i++) {
Element cardElement = new Element("card");
Element suitElement = new Element("suit");
 
suitElement.setText(cards[i].getSuit().toString());
Element faceElement = new Element("face");
faceElement.setText(cards[i].getFace().toString());
cardElement.addContent(suitElement);
cardElement.addContent(faceElement);
doc.getRootElement().addContent(cardElement);
}
JDOMSource requestSource = new JDOMSource(doc);
JDOMResult result = new JDOMResult();
webServiceTemplate.sendAndReceive(requestSource, result);
Document resultDocument = result.getDocument();
Element responseElement = resultDocument.getRootElement();
Element handNameElement =
responseElement.getChild("handName", ns);
return PokerHandType.valueOf(handNameElement.getText());
}
private WebServiceTemplate webServiceTemplate;
public void setWebServiceTemplate(
WebServiceTemplate webServiceTemplate) {
this.webServiceTemplate = webServiceTemplate;
}
}

Both Source and Result are interfaces that are a standard part of Java’s XML API and are available in the Java SDK. There are countless implementations of these interfaces to choose from, but as you can see in listing 9.5, I chose to use the JDOM implementations. This choice was mostly arbitrary but influenced by the fact that I am familiar with JDOM and know how to use it to construct XML messages.

TemplateBasedPokerClient’s evaluateHand() method starts by using JDOM to construct an <EvaluateHandRequest> message from the array of Card elements passed in. Once it has the request message, it calls sendAndReceive() on the WebServiceTemplate. It then uses JDOM to parse the result and find the PokerHandType that should be returned.

Notice that the WebServiceTemplate is injected through a setter method. Therefore, TemplateBasedPokerClient must be configured in Spring as follows:

<bean id="templateBasedClient"
class="com.springinaction.ws.client.TemplateBasedPokerClient">
<property name="webServiceTemplate" ref="webServiceTemplate" />
</bean>

The webServiceTemplate property is wired with a reference to the webServiceTemplate bean that we configured earlier.While reading through listing 9.5, you may have noticed that the bulk of the evaluateHand() method involves creating and parsing XML. In fact, only one line deals specifically with sending a message. Manually creating and parsing XML messages may be okay when the messages are very simple, but you can probably imagine the amount of code that would be required to construct complex message payloads. Even with the poker hand evaluation service, where the message payload is far from complex, the amount of XML processing code is staggering.

Fortunately, you don’t have to deal with all of that XML on your own. In section 9.4.4 you saw how an endpoint can use a marshaler to transform objects to and from XML. Now I’ll show you how WebServiceTemplate can also take advantage of marshalers to eliminate the need for XML processing code on the client side.

Using marshalers on the client side

In addition to the simple sendAndReceive() method we used in listing 9.5, WebServiceTemplate also provides marshalSendAndReceive(), a method for sending and receiving XML messages that are marshaled to and from Java objects.

Using marshalSendAndReceive() is a simple matter of passing in a request object as a parameter and receiving a response object as the returned value. In the case of the poker hand evaluation service, these objects are EvaluateHandRequest and EvaluateHandResponse, respectively.

Listing 9.6 shows MarshallingPokerClient, an implementation of PokerClient that uses marshalSendAndReceive() to communicate with the poker hand evaluation service.

Listing 9.6 MarshallingPokerClient, which takes advantage of a marshaler to convert objects to and from XML

package com.springinaction.ws.client;
import java.io.IOException;
import org.springframework.ws.client.core.WebServiceTemplate;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
import com.springinaction.poker.webservice.EvaluateHandRequest;
import com.springinaction.poker.webservice.EvaluateHandResponse;
public class MarshallingPokerClient
implements PokerClient {
public PokerHandType evaluateHand(Card[] cards)
throws IOException {
EvaluateHandRequest request = new EvaluateHandRequest();
request.setHand(cards);
EvaluateHandResponse response = (EvaluateHandResponse)
webServiceTemplate.marshalSendAndReceive(request);
return response.getPokerHand();
}
private WebServiceTemplate webServiceTemplate;
public void setWebServiceTemplate(
WebServiceTemplate webServiceTemplate) {
this.webServiceTemplate = webServiceTemplate;
}
}

Wow! MarshallingPokerClient’s evaluateHand() method is much simpler and no longer involves any XML processing. Instead, it constructs an EvaluateHandRequest object and populates it with the Card array that was passed in. After calling marshalSendAndReceive(), passing in the EvaluateHandRequest object, evaluateHand() receives an EvaluateHandResponse, which it uses to retrieve the PokerHandType that it returns.

So, how does WebServiceTemplate know how to marshal/unmarshal EvaluateHandRequest and EvaluateHandResponse objects? Is it really that smart?

Well, no… not really. Actually, it doesn’t know anything about marshaling and unmarshaling those objects. However, as shown in figure 9.11, it can be wired with a marshaler and an unmarshaler that know how to handle the marshaling:

<bean id="webServiceTemplate"
class="org.springframework.ws.client.core.WebServiceTemplate">
<property name="messageFactory">
<bean class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
</property>
<property name="messageSender" ref="urlMessageSender"/>
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>
Файл:Fig911.png
Figure 9.11 When wired with a marshaler and unmarshaler, a client can send and receive Java objects from WebServiceTemplate. WebServiceTemplate will use the marshaler and unmarshaler to transform the Java objects to and from XML.

Here I’ve wired both the marshaller and unmarshaller properties with a reference to a marshaller bean, which is the same CastorMarshaller configured in section 9.4.4. But it could just as easily have been any of the marshalers listed in table 9.3.

MarshallingPokerClient is much cleaner than TemplateBasedPokerClient. But there’s still a little bit more we can do to trim the fat. Let’s see how to use Spring-WS’s WebServiceGatewaySupport class to eliminate the need to explicitly wire in a WebServiceTemplate.

Using web service gateway support

As you’ll recall from chapter 5 (see sections 5.3.3, 5.4.3, 5.5.3, and 5.6.2), Spring’s data access API includes convenient support classes that provide templates so that the templates themselves do not need to be configured. In a similar way, SpringWS provides WebServiceGatewaySupport, a convenient support class that automatically provides a WebServiceTemplate to client classes that subclass it.

Listing 9.7 shows one final implementation of PokerClient, PokerServiceGateway, that extends WebServiceGatewaySupport.

Listing 9.7 ''WebServiceGatewaySupport, which provides a WebServiceTemplate through getWebServiceTemplate()

package com.springinaction.ws.client;
import java.io.IOException;
import org.springframework.ws.client.core.support.
WebServiceGatewaySupport;
import com.springinaction.poker.Card;
import com.springinaction.poker.PokerHandType;
import com.springinaction.poker.webservice.EvaluateHandRequest;
import com.springinaction.poker.webservice.EvaluateHandResponse;
public class PokerServiceGateway
extends WebServiceGatewaySupport
implements PokerClient {
public PokerHandType evaluateHand(Card[] cards)
throws IOException {
EvaluateHandRequest request = new EvaluateHandRequest();
request.setHand(cards);
EvaluateHandResponse response = (EvaluateHandResponse)
getWebServiceTemplate().marshalSendAndReceive(request);
return response.getPokerHand();
}
}

As you can see, PokerServiceGateway isn’t much different from MarshallingPokerClient. The key difference is that PokerServiceGateway isn’t injected with a WebServiceTemplate. Instead, it gets its WebServiceTemplate by calling getWebServiceTemplate(). Under the covers, WebServiceGatewaySupport will create a WebServiceTemplate object without one being explicitly defined in Spring.

Even though WebServiceTemplate no longer needs to be defined in Spring, the details of how to create a WebServiceTemplate must still be configured through WebServiceGatewaySupport’s properties. For PokerServiceGateway, this means configuring the messageFactory, messageSender, marshaller, and unmarshaller properties:

<bean id="pokerClientGateway"
class="com.springinaction.ws.client.PokerServiceGateway">
<property name="messageFactory">
<bean class="org.springframework.ws.soap.saaj.
SaajSoapMessageFactory"/>

</property>
<property name="messageSender" ref="messageSender"/>
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>

Notice that the properties are configured exactly as they were with WebServiceTemplate.

Summary

Traditionally, web services have been viewed as just another remoting option. In fact, some developers lovingly refer to SOAP as “CORBA with XML.”The problem with the web services as remoting view is that it leads to tight coupling between a service and its clients. When treated as remoting, a client is bound to the service’s internal API. The contract with the client is a side effect of this binding. Changes to the service could break the contract with the client, requiring the client to change or requiring the service to be versioned.

In this chapter, we’ve looked at web services from a different angle, taking a message-centric view. This approach is known as contract-first web services, as it elevates the contract to be a first-class citizen of the service. Rather than simply being remote objects, contract-first web services are implemented as message endpoints that process messages sent by the client and defined by the contract. Consequently, the service and its API can be changed without impacting the contract.

Spring-WS is an exciting new web service framework that encourages contractfirst web services. Based on Spring MVC, Spring-WS endpoints handle XML messages sent from the client, producing responses that are also XML messages.

If you’re like me, you’re probably a bit skeptical about all of the work that went into configuring a web service in Spring-WS. I won’t deny that contract-first web services require a bit more work than using XFire to SOAP-ify a bean in contractlast style. In fact, when I first looked at Spring-WS, I initially dismissed it as too much work and no benefit… crazy talk.

But after some more thought, I realized that the benefits of decoupling the service’s contract from the application’s internal API far outweigh the extra effort required by Spring-WS. And that work will pay dividends in the long run as we are able to revise and refactor our application’s internal API without worrying about breaking the service’s contract with its clients.

Web services, especially those that are contract first, are a great way for applications to communicate with each other in a loosely coupled way. Another approach is to send messages using the Java Message Service (JMS). In the next chapter, we’ll explore Spring’s support for asynchronous messaging with JMS.

Wikijava.org.ua-1px.png

]]>
Книги по Java https://linexp.ru?id=4723 Wed, 29 Jun 2022 13:58:50 GMT
<![CDATA[Глава 13 Spring in Action 2th edition]]> Spring разработан, чтобы помочь вам в решении всех этих проблем. На основе шаблона Model-View-Controller (MVC) , Spring MVC поможет вам построить web-приложения, которые будут являться столь же гибкими и слабо связанными, как сам Spring Framework.

Содержание

Обработка веб-запросов

В данной главе рассматриваются


  • Отображение запросов в Контроллеры Spring
  • Явное связывание параметров форм
  • Валидация заполняемых форм
  • Связывания исключений с Видами

Как у разработчика JEE, у Вас более чем вероятно, есть опыт разработки web-приложения. На самом деле, для многих разработчиков Java ЕЕ, создание web-приложений является их основным занятием. Если у вас подобный опыт, таки есть, Вы уже наверное хорошо осведомлены о тех довольно специфических проблемах, которые приходится решать в подобных системах. И все они лишь усугубляются независимой природой протокола HTTP.

Веб-фреймворк Spring разработан, чтобы помочь вам в решении всех этих проблем. На основе шаблона Model-View-Controller (MVC) , Spring MVC поможет вам построить web-приложения, которые будут являться столь же гибкими и слабо связанными, как сам Spring Framework.

В этой главе, и следующих главах мы будем изучать веб-фреймворк Spring MVC . В этой главе мы сосредоточимся на той части Spring MVC, который обрабатывает запросы. Вы увидите, как расширить богатый исходный набор Spring классов Контроллера, чтобы справится практически с любыми web-функциями, требуемыми вашему приложению. Вы также увидите, как классы "обработчиков-маппинга" в Spring делают легкой работу связывая URL шаблонов с конкретными реализациями Контроллеров. Главе 14 будут будет посвящена тому, как использовать Spring MVC для получения Видов формирующих и отправляющих ответ сервера обратно пользователю.

Однако прежде чем углубляться в особенности реализации Контроллеров и обработчиков-мапинга, давайте окинем беглым взглядом Spring MVC, построив нашу первую, пусть небольшую но полную web-функциональность.


Source(s): Глава 13 Spring in Action 2th edition

Обзор Spring MVC

Видели ли вы когда нибудь детскую игру "Мышеловка"? Это довольно сумасшедшая игра. Цель ее - отправить небольшой стальной шарик через ряд дурацких приспособлений для того, чтобы вызвать срабатывание мышеловки. Путь шарика проходит по разным видами сложных конструкций, начиная от скатывания извилистой рампой, чтобы затем с игрушечных качелей запрыгнуть на миниатюрное колесо обозрения, а потом быть выброшенным из крошечного ведерка ударом резинового башмачка. Он проходит через все это, просто чтобы защелкнуть пружиной ловушки, бедных, ничего не подозревающих пластиковых мышей.

На первый взгляд, можно подумать, что MVC Spring Framework во многом похож на эту "Мышеловку". Только вместо того чтобы перемещать шарик через различные пандусы, детские качели-шаталки, и колеса, Spring запутанными кругами перемещает запросы, между Диспетчер-Сервлетом, Обработчиками Маппинга, Контроллерами, Арбитами Видов.

Но не стоит слишком отождествлять Spring MVC с игрой Мышеловка в стиле Руби Голдберга. Каждый из компонентов в Spring MVC выполняет вполне конкретную цель. Начнем изучение Spring MVC, изучив жизненный цикл типичного запроса.


Source(s): Глава 13 Spring in Action 2th edition

Один день из жизни Запроса

Каждый раз, когда пользователь нажимает на ссылку или отправляет форму в веб-браузере, Запрос идет на работу. Суть работы Запроса сводится к тому, что он - курьер. Подобно, почтовому перевозчику или человеку из Федеральной экспресс-доставки , Запрос живет, перенося информацию из одного места в другое.

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

Когда Запрос покидает браузер, он несет в себе информацию о том, что пользователь - попросил. По крайней мере, Запрос непременно будет нести запрошенный URL. Но оно может также иметь дополнительные данные, например такие как информация, представленная в форме которую заполнил пользователь.


(1) Первой остановкой в разъездах Запроса, является DispatcherServlet Spring. Как и большинство Java реализаций MVC фреймворков, Spring MVC пропускает все входящие запросы через один сервлет фронт-контроллера. Фронт-контроллер является общим шаблоном веб-приложений , в котором одному сервлету делегирована ответственность за все запросы к остальным компонентам приложения (выполняющим фактическую обработку). В случае с Spring MVC, фронт-контроллером является DispatcherServlet;

(2) Работа DispatcherServlet в том, чтобы отправить запрос - Контроллер-у(Controller). В Spring MVC, Контроллер это Spring-компонент, который обрабатывает запрос. Но типичное приложение может иметь несколько Контроллеров и DispatcherServlet-у требуется помощь в принятии решения, в какой-же Контроллер отправить запрос. Таким образом, DispatcherServlet консультирует c одинм или несколькими - Обработчиками-маппинга(handler mappings), чтобы выяснить, где будет следующая остановка Запроса . Обработчик-маппинга при принятии своего решения будет руководствоваться в первую очередь URL переданным в пришедшем запросе;


Файл:Fig131.png
Рисунок 13.1 запрос диспетчеризуется сервлетом DispatcherServlet на контроллер (который выбирается через обработчика-маппинга). Как только контроллер закончит обработку, запрос передается в один из видов (которая выбирается через ViewResolver) для отображения выходной.
(3) Как только соответствующий Контроллер был определен, DispatcherServlet посылает Запрос в счастливый путь к выбранному Контроллеру. В Контроллере, Запрос отдаст часть своей полезной нагрузки (информацию, заполненной пользователем формы) и будет терпеливо ждать пока Контроллер обработает эту информацию. (На самом деле, хорошо разработанный Контроллер, почти или практически не занимается обработкой этой информации, а вместо себя делегирует ответственность за бизнес-логику одному или нескольким служебным объектам);

(4) Логика работы Контроллера часто приводит к тому, что некоторая информация, должна быть передана назад пользователю и отображаться в его браузере. Эта информация называется - Моделью данных, или просто Моделью(Model). Но отправки обратно "сырой" информации из Модели, пользователю - недостаточно, перед отправкой информация должен быть отформатирована и представлена в удобном для пользователя формате, как правило, в HTML. Для этого информация должна быть передана в один из - Видов(View), которым обычно являются JSP. Итак, последнее, что Контроллер должен сделать, это упаковать Модель данных и имя Вида в объект ModelAndView. Затем он отсылает наш Запрос, вместе с новой "посылкой" - объектом ModelAndView, назад в DispatcherServlet. Как следует из названия, объектом ModelAndView содержит Модель данных, а также некий намек на то, какой из Видов будет использован для отображения результата.

(5) Так, чтобы Контроллер не был жестко связан с с каким либо конкретным Видом, объект ModelAndView не несет ссылку на фактический JSP. Вместо этого он несет только "логическое имя" Вида, используемое затем для поиска фактического Вида. Последний и будет производить в результате своей работы, необходимый нашему браузеру HTML. После того как ModelAndView доставлен в DispatcherServlet, последний использует помощь - Арбитра Вида(view resolver), чтобы найти заветные JSP фактического Вида .

(6) Теперь, когда DispatcherServlet знает, какой фактический Вид будет отображать результаты, работа Запроса подходит к концу. Его конечная остановка - у реализации Вида (вероятно, JSP), куда он доставляет Модель данных. С момента получением фактическим Видом - Модели данных работа нашего трудолюбивого Запроса завершена.... .

Вид, будет использовать полученную Модель данных для генерации отображения страницы, которую затем будет доставлять обратно в браузере, другой (уже не столь трудолюбивый) курьер - объект Ответ (Response).

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

Настраиваем DispatcherServlet

В основе Spring MVC является DispatcherServlet, сервлет, который функционирует в качестве фронт-контроллер в Spring MVC. Как и любой сервлет, DispatcherServlet должен быть настроен в файле web.xml ваших веб-приложений. Поместите следующее объявление <servlet> в web.xml файл приложения:

<servlet>
<servlet-name>roadrantz</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-name> данное сервлету является важным. По умолчанию, когда DispatcherServlet загружен, он будет загружать контекст Spring приложения из файла XML, чье имя основано на имени сервлета. В этом случае, поскольку сервлет называется roadrantz, то и DispatcherServlet будет пытаться загрузить контекст приложения из файла с именем roadrantz-servlet.xml.

Далее вы должны указать, какие адреса будут обработаны DispatcherServlet. Добавьте следующие <servlet-mapping> в web.xml, чтобы DispatcherServlet обрабатывал все URL-адреса, которые заканчиваются на .htm :

<servlet-mapping>
<servlet-name>roadrantz</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>

Таким образом, вам, наверное любопытно, почему мы выбрали именно этот шаблон URL-адреса. Это может быть потому, что весь контент производимый нашим приложение - HTML. А может быть потому что мы хотим, обмануть наших друзей, заставляя их думать что все наше приложение состоит из статических HTML-файлов. А может быть еще и потому, что .do - это какое-то глупое расширения.

Но правда заключается в том, что URL шаблон в принципе произволен и мы могли бы выбрали любой URL шаблон для DispatcherServlet. Наша основная причина выбора *.htm в том то, что этот шаблон, используемый в соответствии с соглашением в большинстве приложений Spring MVC, которые производят HTML-контент. Обоснование этой конвенции является то, что если контент производится в HTML и т.д. URL должен отражать сей факт.

Теперь, когда DispatcherServlet настраивается в web.xml и ему задан URL-маппинг, вы вероятно готовы, чтобы начать писать веб-слой приложения. Однако, есть еще одна вещь, которую мы рекомендуем вам добавить в web.xml.

Разделение контекста приложения

Файл:Fig132.png
Рисунок 13.2 Разделение приложения на отдельные ярусы помогает чисто разделить ответственность. Код Security-уровня обеспечивает защиту приложений, код веб-уровня ориентирован на взаимодействие с пользователем, код сервис-уровня ориентирован на бизнес-логику и код persistence-слоя - соглашения доступа к базе данных.

Как мы уже упоминали ранее, DispatcherServlet будет загружать контекст Spring приложение из одного файла XML, имя которого основывается на его <servlet-name>. Но это не значит, что вы не можете разделить ваш контекст приложения на несколько файлов XML. На самом деле, мы рекомендуем вам разделить ваш контекст приложения с применением слоев, как показано на рисунке 13.2.

Настроенный, DispatcherServlet уже загружает roadrantz-servlet.xml. Следовательно, вы можете поместить все <bean> приложения определений в roadrantz-servlet.xml, но в итоге этот файл станет довольно громоздким. Разделение его на логические части с применением слоев может упростить поддержку приложения, сохраняя каждый из конфигурационных файлов Spring ответственным только за один слой приложения. Это также позволяет легко заменять слой конфигурации, не затрагивая другие слои (например замена roadrantz-data.xml файла, использующего Hibernate , одноименным файлом для использования iBATIS).

Поскольку конфигурационным файл DispatcherServlet является roadrantz-servlet.xml, имеет смысл, чтобы этот файл содержал <bean>-определения, относящихся к контроллерам и другим компонентам Spring MVC. Что же касается сервисных бинов и бинов слоя данных, разумно было бы поместить их определения в roadrantz-service.xml и roadrantz-data.xml, соответственно.

Настройка загрузчика контекста

Чтобы убедиться, что все эти конфигурационные файлы загружены, вам необходимо настроить загрузчик контекста в файле web.xml. Загрузчик контекста загружает конфигурационные файлы контекста в дополнение к тому, что загружает сам DispatcherServlet. Наиболее часто используемый загрузчик контекста - сервлет-слушатель называемый ContextLoaderListener который настраивается в web.xml следующим образом:

<listener> 
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

ПРИМЕЧАНИЕ

Некоторые веб-контейнеры, не инициализируют слушателей до сервлетов - что очень важно при загрузке определения контекста Spring! Если ваше приложение будет развернуто к старом веб-контейнер, который придерживается спецификации Servlet 2.2 или 2.3, которые не инициализируют слушателей перед сервлетами, вы должны использовать ContextLoaderServlet вместо ContextLoaderListener.

При настройке ContextLoaderListener , вы должны указать ему, местоположение файлов конфигурации Spring для загрузки. Если не указано иное, контекст загрузчик будет искать файл конфигурации Spring в /WEB-INF/applicationContext.xml. Но это расположение не поддается разделению контекста приложения с применение слоев, так что вы, вероятно, захотите изменить это поведение. Вы можете указать один или несколько конфигурационные файлы контекста Spring для загрузки, установив параметра contextConfigLocation в контексте сервлета:

<context-param> 
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/roadrantz-service.xml
/WEB-INF/roadrantz-data.xml
/WEB-INF/roadrantz-security.xml
</param-value>
</context-param>

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

DispatcherServlet теперь настроен и готов к отправке запросов в веб-слой приложения. Но веб-слой ведь еще не был построен! Не волнуйтесь. Мы будем строить много веб-слоев в этой главе. Давайте начнем обзор с того, как все части Spring MVC вместе собираются производить веб-функциональность.

Spring MVC в двух словах

Каждое веб-приложение имеет домашнюю страницу. Это необходимо чтобы иметь отправную точку в приложении. Это дает пользователям место для старта и знакомое место, чтобы туда вернуться, если заблудишься. В противном случае, они будут крутиться вокруг да около, кликать по ссылкам, разочаруются и, вероятно, в конечном итоге уйдут на другой веб-сайт.

Приложение RoadRantz не является исключением из "правила домашней страницы" . Потому, нет лучшего места, чтобы приступить к разработке веб-слоя нашего приложения, чем его домашняя страницы. При ее создании , мы быстро ознакомимся с азами Spring MVC.

Как вы помните одно из требований к RoadRantz: домашняя страница должна отображать список самых последних из введенных тирад. Следующий список шагов определяет минимум, того что вы должны сделать, чтобы построить домашнюю страницу в Spring MVC:


  • 1) Написать класс Контроллера, который реализует логику работы страницы. Логика заключается в использовании RantService чтобы получить список последних введенных тирад.
  • 2) Настройка Контроллера в файле конфигурации контекста DispatcherServlet (roadrantz-servlet.xml).
  • 3) Настройте Арбитр-Вида(View resolver), чтобы связать контроллер с JSP.
  • 4) Написать JSP, который будет отображать домашнюю страницу пользователям.

Первым шагом является создание объекта контроллера, который будет обрабатывать запросы к домашней странице . Так, что без дальнейших проволочек, давайте напишем наш первую Spring MVC контроллер.

Создание Контроллера

Файл:Fig133.png
Рисунок 13.3 Контроллер обрабатывает веб-запросы от имени DispatcherServlet. Хорошо спроектированный контроллер не делает всю работу сам, он делегирует ее к объекту сервисного уровня для исполнения бизнес-логики

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

Точно так же в Spring MVC, контроллер класса, это ваш интерфейс к функциональности приложения. Как показано на рисунке 13.3, контроллер получает запрос, передает его для обработки в сервисные классы (реализующие логику приложения), и в конечном итоге собирает результаты в страницу, которая возвращается к вам в вашем веб-браузере. В связи с этим, контроллер не сильно отличается от обычного HttpServlet или от Action в Struts.

Домашняя страница контроллер приложения RoadRantz относительно проста. Он не принимает никаких параметров запроса и просто выдает список недавно введенных тирады для отображения его на главной странице. Листинг показывает 13.1 HomePageController, этоSpring MVC контроллер, который реализует функциональность домашней страницы.

Листинг 13.1 HomePageController, который формирует список недавних тирад для показа на домашней странице

package com.roadrantz.mvc; 
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import com.roadrantz.service.RantService;
 
public class HomePageController extends AbstractController {
public HomePageController() {}
 
protected ModelAndView handleRequestInternal(
HttpServletRequest request, HttpServletResponse response)
throws Exception {
//Retrieves list of rants
List recentRants = rantService.getRecentRants();
 
return new ModelAndView("home", //Goes to “home” view
"rants", recentRants); //Returns rants in model
}
 
private RantService rantService;
 
public void setRantService(RantService rantService) {
//Injects RantService
this.rantService = rantService;
}
}

Чем контроллер Spring MVC отличается от сервлета или от Struts Action так это - тем, что он настроен просто как обычный JavaBean в контексте Spring приложения . Это означает, что вы можете использовать все преимущества внедрения зависимостей (DI) и Spring AOP с классом контроллера точно так же, как и с любым другим бином.

DI в случае HomePageController, используется для связывания в него RantService. Таким образом HomePageController делегирует ответственность за получения списка последних тирад в связанный RantService.

Введение в ModelAndView

После того как шеф-повар приготовил еду, официант/официантка заберет ее чтобы принести к вашему столу. На выходе, последнее, что они могут сделать, это добавить несколько заключительных штрихов, может быть, веточку петрушки.

Как только бизнес-логика была выполнена сервисными объектами, пришло время для Контроллера, чтобы отправить результаты обратно в браузер. Последнее, что делает handleRequestInternal(), это возвращение объекта - ModelAndView. Класс ModelAndView представляет важное понятие в Spring MVC. На самом деле, каждый метод выполняемый Контроллером должен вернуть ModelAndView. Так что, найдите время, чтобы понять, как этот важный класс работает.

Объект ModelAndView, как следует из названия, полностью инкапсулирует как Вид так и Модель данных, которая будет отображаться этим Видом. В случае HomePageController, объект ModelAndView строится следующим образом:

 new ModelAndView("home", "rants", recentRants);

Первый параметр этого конструктора ModelAndView - логическое имя компоненты - Вида , который будет использоваться для отображения информации из этого Контроллера. Здесь, логическое имя Вида - home. Арбитр Вида(view resolver) будет использовать это имя для поиска реального объекта View (вы узнаете больше о Видах и Арбитрах Вида позже в главе 14).

Следующие два параметра представляют собой объект Модели, которая будет передана Виду. Эти два параметра выступают в качестве пары имя-значение. Вторым параметром является имя для объекта Модели, сам объект - Модель дан в качестве третьего параметра. В этом случае Модель содержащая список тирад в переменной recentRants и названная - rants, будет передана в Вид с логическим именем home.

Конфигурирование бина Контроллера

Теперь, когда HomePageController был написан, пришло время настроить его в конфигурационном файле контекста DispatcherServlet (который для RoadRantz называется - roadrantz-servlet.xml). Следующий фрагмент XML объявляет HomePageController:

<bean name="/home.htm" 
class="com.roadrantz.mvc.HomePageController">
<property name="rantService" ref="rantService" />
</bean>

Как упоминалось ранее, свойство rantService должен быть введено с реализацией интерфейса RantService. В этой декларации <bean>, мы связываем свойство rantService с ссылкой на другой бин по имени rantService. Сам бин rantService объявлен в другом месте (в roadrantz-service.xml, если быть точными).

Одной вещью, которая, возможно, поразила вас своей странностью является то, что вместо указания идентификатора(id) бина HomePageController, мы указали - имя(name) этого бина . И, чтобы сделать эту вещь еще страннее, мы вместо того чтобы задать "настоящее" имя, задали его как URL-шаблон /home.htm. Здесь имя атрибута выполняет двойные обязанности: служит именем бина и одновременно URL-шаблоном для запросов, которые должны обрабатываться данным Контроллером. Поскольку URL-шаблон имеет специальные символы, которые недопустимы в XML id-атрибута (в частности - слэш (/) символ), то вместо name-атрибута должен быть использован id-атрибут.

Когда на DispatcherServlet поступает запрос с URL, который заканчивается на /home.htm, DispatcherServlet направит запрос в HomePageController для его обработки. Однако следует отметить, что единственной причиной того, что name-атрибут бина используется как его URL-шаблон, является то, что мы пока не настроили бин handlermapping. А Обработчиком-маппинга "по умолчанию" для DispatcherServlet-а, является BeanNameUrlHandlerMapping, который (как мы помним) и использует имя бина, в качестве URL-шаблона. Позже, Вы - увидите, как использовать в Spring и некоторые другие Обработчики-маппинга , позволящие Вам отделить имя бина Контроллера от его URL-шаблона.

Объявление Арбитра Вида

На обратном пути в веб-браузер, результаты веб-операции должны быть представлены в "человеческом" формате. Как официант может положить веточку петрушки на тарелку, чтобы сделать ее вид более презентабельным, так и полученный список тирад должен быть немного "приодет", прежде чем представлять его клиенту. Для этого мы будем использовать JSP-страницу, которая сможет отображать результаты в удобном для пользователя формате.

Но как же Spring узнает, какую именно JSP использовать для визуализации результатов? Как вы помните, одним из значений, возвращаемых в объекте ModelAndView является логическое имя Вида. Хотя логическое имя Вида напрямую не ссылается на конкретные JSP, оно может быть использовано чтобы косвенным путем вывести нас к нужной JSP.

Чтобы помочь Spring MVC выяснить, какую JSP использовать, вам придется объявить в roadrantz-servlet.xml еще один или несколько дополнительных бинов : Арбитры Видов.

Суть работы Арбитра Видов состоит в том, чтобы принять логическое имя Вида возвращаемое в ModelAndView и провести его сопоставление с фактическим Видом. В случае HomePageController, нам необходимо чтобы Арбитр Видов преобразовал имя - home (логическое имя Вида) в имя JSP-файла, который отобразит домашнюю страницу.

Как будет показано - позже , Spring MVC поставляется с несколькими арбитрами Вида из которых можно выбирать. Но для Видов, которые основываются на JSP, нет ничего проще, чем InternalResourceViewResolver:

<bean id="viewResolver" 
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
 
<property name="prefix">
<value>/WEB-INF/jsp/</value>
</property>
 
<property name="suffix">
<value>.jsp</value>
</property>
</bean>

InternalResourceViewResolver предваряет имя Вида, возвращаемое в ModelAndView значением своего свойства prefix и завершает его значением своего свойства - suffix. Так как HomePageController возвращает имя Вида - home в ModelAndView, InternalResourceViewResolver найдет фактический Вид в /WEBINF/JSP/home.jsp

Создание JSP-Вида

Мы написали Контроллер, который будет обрабатывать запросы к домашней странице и сконфигурировали его в контексте приложения Spring. Он будет консультироваться с бином RantService для поиска последних добавленных тирад. И когда это будет сделано, Контроллер будет посылать результаты в JSP Вид. Так что теперь нам осталось только создать JSP, которая отображает нужную веб-страницу. JSP в листинге 13.2 перебирает список тирад и отображает их на домашней странице.

Листинг 13,2 home.jsp, которая отображает список последних тирад

<%@ page contentType="text/html" %> 
<%@ taglib prefix="c" uri="https://java.sun.com/jstl/core" %>
<html>
<head><title>Rantz</title></head>
<body>
<h2>Welcome to RoadRantz!</h2>
<h3>Recent rantz:</h3>
<ul>
<c:forEach items="${rants}" var="rant">
<li><c:out value="${rant.vehicle.state}"/> /
<c:out value="${rant.vehicle.plateNumber}"/> --
Iterates over
<c:out value="${rant.rantText}"/>
list of rants
</li>
</c:forEach>
</ul>
</body>
</html>

Хотя мы не оставили каких-либо эстетических элементов в home.jsp для краткости, она по-прежнему может служить для иллюстрации того, как Модель данных, возвращаемая в ModelAndView может быть использована в работе Вида. В HomePageController, мы поместили список тирад в свойство "Модели" именуемое - rants. Когда home.jsp генерирует домашнюю страницу, код JSP ссылается на список тирад как на ${rants}.

Убедитесь, что имя этой JSP страницы - home.jsp и поместите ее в папку /WEB-INF/jsp веб-приложения. Это то место где InternalResourceViewResolver постараемся ее найти.

Собираем все - вместе

Файл:Fig134.png
Рисунок 13.4 запрос к домашней странице посылается DispatcherServlet-ом к Контроллеру HomePageController (в соответствии с указаниями обработчика-маппинга BeanNameUrlHandlerMapping). По окончанию, Арбитр Вида InternalResourceViewResolver направляет запрос к home.jsp для отображения страницы.

Домашняя страница завершена. Для обработки запросов к этой странице, Вы написали Контроллер. Затем настроили его, так чтобы полагаясь на обработчик-маппинга BeanNameUrlHandlerMapping иметь доступ к Контроллеру через URL шаблон /home.htm. Далее написали простую JSP, которая генерирует домашнюю страницу, и настроили Арбитр Вида для поиска этой JSP. А теперь, как это живет все вместе? Рисунок 13.4 показывает шаги, того, как запрос к странице /home.htm будет выполняться.

Напомним этапы этого процесса:


(1) DispatcherServlet получает запросы, в которых URL шаблон - /home.htm.

(2) DispatcherServlet консультируется с BeanNameUrlHandlerMapping чтобы найти Контроллер с name-атрибутом бина - /home.htm; он находит бин HomePageController

(3) DispatcherServlet отправляет запрос в HomePageController для обработки.

(4) HomePageController возвращает объект ModelAndView с логическим именем Вида - home и "Моделью данных" в виде списка тирад, в свойстве называемом - rants.

(5) DispatcherServlet консультируется с Арбитром Вида (сконфигурирован как InternalResourceViewResolver), чтобы найти Вид с логическим именем - home. Арбитр Вида InternalResourceViewResolver возвращает путь к /WEB-INF/jsp/home.jsp.

(6) DispatcherServlet перенаправляет запрос на JSP с полным именем /WEB-INF/jsp/home.jsp чтобы сгенерировать домашнюю страницу и отобразить ее пользователю.

Теперь, когда вы увидели "большую картину" Spring MVC в целом , давайте замедлим немного ход и более пристально рассмотрим каждую из движущихся частей, участвующих в обслуживании запроса. И начнем мы с обработчика-маппинга..

Сопоставление запросов и Контроллеров

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

Таким же образом, когда запрос поступает в DispatcherServlet, там должно быть какой-нибудь каталог, чтобы помочь выяснить, куда запрос должен быть отправлен далее. Обработчик-маппинга помогает DispatcherServlet-у выяснить, в какой Контроллер запрос должен быть направлен. Обработчики-маппинга как правило устанавливают соответствие между URL шаблонами и бинам определенных Контроллеров . Это подобно тому, как URL-адреса отображаются в сервлеты с использованием <servlet-mapping>-элемента в файле web.xml веб-приложения. Или же как Actions в Struts, сопоставляются с URL, используя path-атрибут для <action> в struts-config.xml.

В предыдущем разделе, мы опирались на тот факт, что DispatcherServlet по умолчанию использовал обработчик-маппинга BeanNameUrlHandlerMapping. Этот обработчик было хорош, чтобы начать работу, но он не может подходить для всех случаев жизни. К счастью, Spring MVC предлагает несколько реализаций обработчиков-маппинга на ваш выбор.

Все обработчики-маппинга в Spring MVC это реализации интерфейса org.springframework.web.servlet.HandlerMapping . Spring уже изначально содержит четыре полезные реализации HandlerMapping, как показано в таблице 13.1.

Таблица 13.1 обработчики-маппинга помогают DispatcherServlet-у найти нужный Контроллер для обработки запроса.


Обработчик-маппинга Как он отображает запросы к Контроллерам
BeanNameUrlHandlerMapping Сопоставляет Контроллеры с URL-адресами, используя свойство name бина Контроллера.
SimpleUrlHandlerMapping Сопоставляет Контроллеры с URL-адресами, используя карту java.util.Properties определенную в контексте приложения Spring.
ControllerClassNameHandlerMapping Сопоставляет Контроллеры с URL-адресами, используя имя класса контроллера в качестве основы для URL-адреса.
CommonsPathMapHandlerMapping Сопоставляет Контроллеры для запросов с использованием на уровне исходных метаданных размещены в коде контроллера. Метаданных определяется с помощью Jakarta Commons Attributes( https://jakarta.apache.org/commons/attributes).

Вы уже видели пример того, как BeanNameUrlHandlerMapping в роли обработчик-маппинга по умолчанию, использовался совместно с DispatcherServlet. Давайте посмотрим, как использовать остальные обработчики, начиная с SimpleUrlHandlerMapping.

Использование SimpleUrlHandlerMapping

SimpleUrlHandlerMapping, вероятно, один из самых простых обработчик-маппинга в Spring. Он позволяет сопоставить URL шаблоны непосредственно с Контроллерами без того, чтобы именовать ваши бины каким либо особым образом.
Для примера, рассмотрим следующее объявление SimpleUrlHandlerMapping, ассоциирующее несколько Контроллеров в приложениии RoadRantz с их шаблоны URL:

<bean id="simpleUrlMapping" 
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/home.htm">homePageController</prop>
<prop key="/rantsForVehicle.htm">
rantsForVehicleController
</prop>
<prop key="/rantsForVehicle.rss">
rantsForVehicleControllerRss
</prop>
<prop key="/rantsForDay.htm">rantsForDayController</prop>
<prop key="/login.htm">loginController</prop>
<prop key="/register.htm">registerMotoristController</prop>
<prop key="/addRant.htm">addRantController</prop>
</props>
</property>
</bean>

Свойство mappings в SimpleUrlHandlerMapping связывает в себя карту java.util.Properties (используя <props>). key-атрибут каждого <prop>-элемента, это - URL-шаблон. Так же, как в случае с BeanNameUrlHandlerMapping, все URL-шаблоны - относительные (т.е. относительно URL-адреса <servlet-mapping> в DispatcherServlet) . Значением каждого <prop>-элемента является имя бина Контроллера, который будет обрабатывать запросы к данному URL-шаблону.

В случае, если вам интересно, откуда все те другие контроллеры появились, просто потерпите. К тому времени, эта глава будет пройдена, мы сможем увидеть, большинство из них. Но сначала давайте рассмотрим еще один способ объявить сопоставление Контроллера с URL - использование имени класса контроллеров.

Используем ControllerClassNameHandlerMapping

Часто вы сможете придумать сопоставление ваших Контроллеров , с такими URL шаблонами, которые бы во многом были схожи с именами классов контроллеров. Например, в приложении RoadRantz, это сопоставление rantsForVehicle.htm с RantsForVehicleController , а rantsForDay.htm с RantsForDayController.

Вы обратили внимание на закономерность? Во всех этих случаях, URL-шаблон такой же, как имя класса Контроллера, если убрать из его имени - "Controller" и добавить в конце - .htm Кажется, что URL-шаблоны подобные этим, можно было бы автоматически получать из имен классов соответствующих Контроллеров , просто руководствуясь некими заданными общими правилами, без необходимости явного описания сопоставления.

На самом деле, это и есть примерно то, что делает ControllerClassNameHandlerMapping:

 <bean id="urlMapping" 
class="org.springframework.web.servlet.mvc.ControllerClassNameHandlerMapping"/>
Файл:Fig135.png
Рисунок 13.5 ControllerClassNameHandlerMapping сопоставляет запрос - Контроллеру, убирая Controller-часть с конца названия класса и нормализуя его целиком к строчным буквам.

При настройке ControllerClassNameHandlerMapping, вы говорите DispatcherServletSpring сопоставлять URL-шаблоны для Контроллеров следуя простой конвенции. Вместо явного сопоставления каждого Контроллера и URL-шаблона, Spring будет автоматически сопоставлять Контроллеры с URL-шаблонами, основанными на именах классов этих Контроллера. Рисунок 13.5 иллюстрирует, как для RantsForVehicleController будет сформирован URL-щаблон.

Проще говоря, для получения URL-шаблона, Controller-часть имени класса Контроллера удаляется (если она существует), оставшийся текст переводится в нижний регистр, слэш (/) будет добавлен в начале, и ".htm" добавляется в конце. Следовательно, бин Контроллера, чей класс - RantsForVehicleController будет соответствовать - /rantsforvehicle.htm. Обратите внимание, что весь URL-шаблон преобразуется к строчными буквам, что немного отличается от соглашений, которым мы следовали в случае с SimpleUrlHandlerMapping.

Отображение на основе метаданных

Последним обработчиком-маппинга который мы рассмотрим это CommonsPathMapHandlerMapping. Этот обработчик-маппинга считывает метаданные размещеные в исходном коде Контроллера, чтобы определить сопоставляемый URL. В частности, метаданные, как ожидается, будут в виде org.springframework.web.servlet.handler.commonsattributes.PathMap атрибута скомпилированного в Контроллер с помощью Jakarta Commons Attributes compiler.

Для использования CommonsPathMapHandlerMapping, просто объявить его как <bean> в вашем конфигурационном файле контекста следующим образом:

<bean id="urlMapping" class="org.springframework.web. 
servlet.handler.metadata.CommonsPathMapHandlerMapping"/>

Затем пометьте каждый из ваших Контроллеров атрибутом PathMap чтобы объявить URL-шаблон для контроллера. Например, для сопоставления HomePageController с /home.htm, пометьте HomePageController следующим образом:

/**
* @@org.springframework.web.servlet.handler.
commonsattributes.PathMap("/home.htm")
*/

public class HomePageController
extends AbstractController {

}

Наконец, вам нужно настроить процесс сборки приложения, включив в него использование Commons Attributes compiler так, чтобы атрибуты были скомпилированы в код вашего приложения. Мы адресуем Вас к домашней странице Commons Attributes compiler ( https://jakarta.apache.org/commons/attributes ) для выяснения подробностей того, как настроить этот компилятор совместно с Ant или Maven.

Совмещение обработчиков-маппинга

Как вы уже увидели, Spring содержит набор полезных обработчиков-маппинга. Но что, если вы не можете однозначно решить, какой-же из них, конкретно, вам - нужен? Например, предположим, что ваше приложение было простым и вы решили использовали BeanNameUrlHandlerMapping. Но оно начинает расти и вы хотите, дополнительно, применить SimpleUrlHandlerMapping. Но можете ли вы смешивать разные обработчики-маппинга в одном приложении? Как выясняется, все классы обработчиков-маппинга реализуют в Spring интерфейс - Ordered . Это означает, что вы можете объявить несколько обработчиков-маппинга в приложении, и задавая в них значение свойства - order, управлять тем самым взаимным приоритетом их выбора (т.е. порядком их использования по отношению друг к другу).

Предположим, вы хотите использовать как BeanNameUrlHandlerMapping так и SimpleUrlHandlerMapping, рядом друг с другом в одном приложении. Тогда, вы должны были бы объявить бины обработчиков-маппинга следующим образом:

<bean id="beanNameUrlMapping" 
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="order"><value>1</value></property>
</bean>
 
<bean id="simpleUrlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="order"><value>0</value></property>
<property name="mappings">

</property>
</bean>

Обратите внимание, что чем НИЖЕ значения свойства - order, тем ВЫШЕ - приоритет. В данном случае, значение order у SimpleUrlHandlerMapping оказался меньше, чем у BeanNameUrlHandlerMapping. Это означает, что при попытке сопоставить URL с Контроллером , DispatcherServlet сначала проконсультируется с SimpleUrlHandlerMapping. Консультироваться с BeanNameUrlHandlerMapping он станет, только если обращение к SimpleUrlHandlerMapping не дало никаких результатов.

Обработчики-маппинга в Spring помогают DispatcherServlet-у узнать, какому Контроллеру запрос должен быть направлен. После того, как DispatcherServlet выяснил, куда отослать запрос, это уже дело выбранного Контроллера, его - обработать. Потому, давайте посмотрим, как создавать контроллеры в Spring MVC.

Обработка запросов Контроллерами

Если DispatcherServlet является сердцем Spring MVC, то Контроллеры, это его - мозги. При реализации поведение приложения Spring MVC, вы расширяете один из классов Spring Контроллера. Контроллер получает запросы от DispatcherServlet и выполняет некоторые бизнес-функции от имени пользователя.

Если вы знакомы с другими веб-фреймворками, такими как Struts или WebWork, вы можете признать Контроллеры в Spring примерно эквивалентными по целевому назначению - "действиям"(Actions) в Struts или WebWork. Одним большим отличием между Spring Контроллерами и Struts / WebWork "действиями", однако, является то, что Spring предлагает богатую иерархию Контроллеров (как показано на рисунке 13.6), в сравнении с довольно плоской иерархией "действий" в Struts или WebWork.

На первый взгляд, Рисунок 13.6 может показаться несколько пугающим. Конечно, по сравнению с другими MVC-фреймворками, таких как Jakarta Struts или WebWork, требуется намного больше, чтобы проглотить иерархию Контроллеров Spring. В действительности, однако, это лишь кажущаяся сложность и на самом деле все довольно просто и гибко.

В верхней части иерархии Контроллеров находится интерфейс - Controller. Любой класс, реализующий этот интерфейс может быть использован для обработки запросов через Spring MVC Framework .

Чтобы создать свой собственный Контроллер, все что вам нужно сделать, это написать класс, который реализует этот интерфейс.

Хотя вы конечно могли бы написать класс, который непосредственно реализует интерфейс Controller, гораздо больше шансов что вы расширите один из классов ниже в иерархии. В то время как интерфейс Controller определяет основной контракт между Контроллером и Spring MVC, различные классы Контроллера обеспечивают дополнительную функциональность за рамками этих основ.

Широкий выбор классов Контроллеров это одновременно благословение и проклятие. В отличие от других веб-фреймворков, которые заставят вас работать с единственным типом объекта контроллера (например, Action класс в Struts), Spring позволяет вам выбрать Контроллер, который наиболее подходит для ваших нужд. Тем не менее, с таким обилием классов Контроллера , многие разработчики оказываются перегружены и не знаю, как решить что же и выбрать .

Чтобы помочь вам решить, какой класс Контроллера наследовать в Контроллерах вашего приложения, рассмотрим таблицу 13.2. Как вы можете видеть, классы Spring Контроллеров могут быть сгруппированы в шесть категорий, которые обеспечивают все больше функциональности (и представляют больше сложности) по мере продвижения вниз по таблице. Вы можете также заметить из Рисунка 13.5, что (за исключением ThrowawayController) по мере продвижения вниз Контроллер-иерархии, каждый нижележащий Контроллер основан на функциональности Контроллеров , что над ним.

Файл:Fig136.png
Рисунок 13.6 Иерархия Контроллеров Spring MVC включает в себя Контроллеры для любого случая - от простейших запросов запросов, до сложных комплексных видов обработки.

Таблица 13.2 Spring MVC в выборе класса контроллера.


Тип Классы Полезно, когда ...
View ParameterizableViewController UrlFilenameViewController Ваш Контроллер нужен только для отображения статических Видов - обработки или поиска данных не требуется.
Simple Controller (интерфейс) AbstractController Ваш Контроллер предельно прост и требует немного больше функциональных возможностей, чем предоставляется базисными Java сервлетами .
Разовый ThrowawayController Вы хотите простой способ обработки запросов, как команд (по аналогии с Actions в WebWork).
MultiAction MultiActionController Ваше приложение имеет несколько действий, которые выполняют аналогичные или связанные логики.
Command BaseCommandController AbstractCommandController Ваш Контроллер будет принимать один или несколько параметров из запроса и связывать их в объект. Он также, будет способные выполнять проверки параметров.
Form AbstractFormController SimpleFormController Вы должны отобразить форму ввода для пользователя, а также обрабатывать данные, введенные в форму.
Wizard AbstractWizardFormController Вы хотите, по-шагово проводить пользователя через сложные, многостраничные формы ввода, которые в конечном итоге должны обрабатываться как единая форма.

Вы уже видели пример простого контроллера, который расширяет AbstractController. В листинге 13.1, HomePageController расширяет AbstractController и получает список последних тирады для отображения на главной странице. AbstractController является идеальным выбором, потому что домашняя страница весьма проста и не принимает ввода от пользователя.
Основывать ваш Контроллер на AbstractController это прекрасно, если вам не нужно особой мощности. Большинство Контроллеров , однако, будут более интересным, принимая параметры и требования проверки этих параметров. В следующих разделах, мы собираемся построить несколько Контроллеров , которые определяют веб-слой приложений RoadRantz за счет расширения других реализаций классов Контроллеров (представленных на рисунке 13.6), начиная с Command-контроллеров.

Обработка Команд

Файл:Fig137.png
Рисунок 13.7 command-контроллер избавит вас от хлопот контакта с параметрами запроса, напрямую. Он превращает параметры запроса в объект Команды, с который вы и будете обрабатывать вместо запроса.

Это довольно обычно для веб-запроса, содержать один или несколько параметров, которые используются сервером для формирования определенных результатов. Например, одним из требований для приложения RoadRantz является способность, отображать список тирад для какого либо одного конкретного транспортного средства.

Конечно, вы можете расширить AbstractController и извлекать параметры нужные вашему Контроллеру непосредственно из HttpServletRequest. Но тогда Вы должны были бы также написать логику, которая превращает данные из запроса в бизнес-объекты, и вам также придется поместить логику проверки этих данных в код Контроллера. Хотя, на самом деле, всей этой логике в Контроллере не место.

В случае, когда вам необходим Контроллер для выполнения действий основанных на параметрах, ваш класс Контроллера должен наследовать класс command-контроллера, такой как - AbstractCommandController

Как показано на рисунке 13.7, command-контроллер автоматически превращает параметры запроса в объект - Команда. Сommand-контроллеры также могут быть связаны с подключаемыми валидаторами для проверки того, являются ли параметры запроса - действительными.

Листинг 13.3 показывает класс command-контроллера RantsForVehicleController, который используется для отображения списка тирад, введенных для конкретного транспортного средства.

Листинг 13.3 RantsForVehicleController, перечисляет все тирады для конкретного транспортного средства

package com.roadrantz.mvc; 
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;
import com.roadrantz.domain.Vehicle;
import com.roadrantz.service.RantService;
 
public class RantsForVehicleController
extends AbstractCommandController {
 
public RantsForVehicleController() {
// Sets command class, name
setCommandClass(Vehicle.class);
setCommandName("vehicle");
}
 
protected ModelAndView handle(HttpServletRequest request,
HttpServletResponse response, Object command,
BindException errors) throws Exception {
 
Vehicle vehicle = (Vehicle) command; //Casts command
//object to Vehicle
 
List vehicleRants = //Uses RantService to retrieve list
rantService.getRantsForVehicle(vehicle)); // of rants
 
 
Map model = errors.getModel(); //Creates the
model.put("rants",4 //model
rantService.getRantsForVehicle(vehicle));
model.put("vehicle", vehicle);
 
//Returns model
return new ModelAndView("vehicleRants", model);
}
 
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
}

Метод handle() класса RantsForVehicleController является основным используемым методом для AbstractCommandController. Этот метод немного более интересен, чем handleRequestInternal() из AbstractController. В дополнение к HttpServletRequest и HttpServletResponse, handle() принимает Object, который и есть - Команд для Контроллера.

Объект Команды это бин, который предназначенный для хранения параметров запроса легкодоступной форме. Если вы знакомы с Jakarta Struts, вы можете распознать объект Команды в Spring как аналогичный с ActionForm в Struts. Ключевая разница состоит в том, что в отличие от Struts-"команды", которая должна расширять ActionForm, объект Команды в Spring это обычный POJO, которому не нужно наследовать никаких Spring-специфичных классов.

В этом случае объект Команды является экземпляром Vehicle(автомобиля), как это указано в конструкторе Контроллера. Вы также можете опознать Vehicle в качестве доменного класс, который описывает автомобиль в главе 5. Хотя экземпляры Команд и не обязаны быть экземплярами доменных классов, однако это довольно удобно, если конечно - реализуемо. Действительно, Vehicle уже определяет все данные необходимые в RantsForVehicleController. Удобно, и что именно этот же самый тип, необходим методу getRantsForVehicle() класса RantService. Это делает Vehicle практически идеальным выбором для класса Команды.

Перед вызовом метода handle(), Spring будет пытаться выявить соответствие любых параметров, переданных в запросе, со свойствами объекта Команды. Vehicle имеет два свойства: state и plateNumber. Если запрос содержит параметры с такими же именами, их значения будут автоматически привязаны к одноименным свойствам Vehicle.

Как и HomePageController, вы также должны зарегистрировать RantsForVehicleController в roadrantz-servlet.xml:

<bean id="rantsForVehicleController" 
class="com.roadrantz.mvc.RantsForVehicleController">
<property name="rantService" ref="rantService" />
</bean>

Сommand-контроллеры позволяют легко обрабатывать запросы с параметрами путем автоматического связывания параметров запроса с полями командных объектов. Параметры запроса могут быть заданы как в виде URL-параметров (что весьма вероятно в случае с RantsForVehicleController) так в виде полей из веб-формы. Хотя сommand-контроллер и может обрабатывать ввод из формы, однако Spring предоставляет другой тип контроллера с лучшей поддержкой для обработки форм. Давайте рассмотрим form-контроллеры Spring следующим шагом.

Обработка содержания форм

Файл:Fig138.png
Рисунок 13.8 На HTTP GET запрос, form-контроллер выводит форму для получения пользовательского ввода. После отправки заполненной формы с HTTP POST, form-контроллер обрабатывает входные и возвращает страницу подтверждения ввода.

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

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

При реализации процесса отправки тирады, вы могли бы прельститься идеей расширить AbstractController для отображения формы и расширить AbstractCommandController для обработки формы. Это, безусловно, может работать, но будет в конечном итоге более трудным, путем, чем это необходимо. Вы должны были бы поддерживать два разных Контроллера, которые работают в тандеме, чтобы обрабатывать отправку тирад. Не проще было бы иметь всего один контроллер, чтобы обрабатывать как отображение формы так и прием ее ввода?

Тогда, т.н. form-контроллер это именно то, что вам нужно. Form-контроллеры продвинули концепцию command-контроллеров на шаг вперед, как показано на рисунке 13.8, добавив функциональность для отображения формы (когда получен HTTP GET запрос), и обработки ввода формы (когда получен HTTP POST запрос). Кроме того, если возникнут ошибки при обработке формы, Контроллер будет помнить, о потребности в повторном отображении формы, так что пользователь может исправить ошибки и отправить форму еще раз.

Чтобы проиллюстрировать, как работают form-контроллеры , рассмотрим AddRantFormController в листинге 13.4.

Листинг 13.4 контроллер для добавления новых тирады

package com.roadrantz.mvc; 
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import com.roadrantz.domain.Rant;
import com.roadrantz.domain.Vehicle;
import com.roadrantz.service.RantService;
 
public class AddRantFormController extends SimpleFormController {
private static final String[] ALL_STATES = {
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FL",
"GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME",
"MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH",
"NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI",
"SC", "SD", "TN", "TX", "UT", "VA", "VT", "WA", "WV", "WI",
"WY"
};
 
public AddRantFormController() {
setCommandClass(Rant.class); //Sets command
setCommandName("rant"); //class, name
}
 
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
Rant rantForm = (Rant) super.formBackingObject(request);
rantForm.setVehicle(new Vehicle()); //Sets up Rant command
return rantForm; //with blank Vehicle
 
}
 
protected Map referenceData(HttpServletRequest request)
throws Exception {
Map referenceData = new HashMap();
referenceData.put("states", ALL_STATES);//Provides list of
return referenceData; //states for form
}
 
protected ModelAndView onSubmit(Object command,
BindException bindException) throws Exception {
 
Rant rant = (Rant) command;
 
rantService.addRant(rant); // <---Adds new rant
 
return new ModelAndView(getSuccessView());
}
 
private RantService rantService;
public void setRantService(RantService rantService) {
// Handling web requests
this.rantService = rantService;
}
}

Хотя это и не очевидно из листинга, но AddRantFormController несет ответственность как за отображения формы для ввода тирад, так и за обработку результатов заполнения этой формы. Когда Контроллер получает HTTP GET запрос, он направит этот запрос в Вид формы. А когда он получает HTTP POST запрос, метод onSubmit() будет обрабатывать содержание формы.

Метод referenceData() не является обязательным, но он очень удобен, когда нужно предоставить любую дополнительную информацию для отображения в форме. В данном случае, нашей форме понадобится перечень Штатов, который будет отображаться (предположительно в выпадающем списке выбора). Таким образом, метод referenceData() класса AddRantFormController добавляет массив строк, содержащий все 50 штатов США, а также округ Колумбия.

При нормальных обстоятельствах, объект Команды, который поддерживает форму, является просто экземпляром класса Команды. В случае AddRantFormController, однако, простого экземпляра Rant не будет. Форма собирается использовать вложенное внутри Rant свойство Vehicle, как часть объекта поддержки для формы . Поэтому было необходимо переопределить formBackingObject() для установки свойства vehicle. В противном случае, NullPointerException будет брошен, при попытке Контроллера связывать свойства state и plateNumber с параметрами запроса.

Метод onSubmit() обрабатывает содержание формы(запрос HTTP POST), передав объект Команды (являющийся экземпляром Rant), в метод addRant() из внедренной ссылки на RantService .

Что не понятно из листинга 13.4, так это то - как этот Контроллер узнает, что следует показывать форму ввода тирад Также не ясно, куда попадет пользователь после успешного добавления тирады. Единственный намек - это то, что результатом вызова для getSuccessView() дается - ModelAndView. Но откуда же берется Вид подтверждения успешности ввода?

SimpleFormController разработан таким образом, чтобы детали Вида, держать как можно подальше от Java-кода Контроллера. Вместо жесткого кодирования объектов ModelAndView, вы настраиваете form-контроллер в файле конфигурации контекста, следующим образом:

<bean id="addRantController" 
class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
</bean>

Так же как и другие Контроллеры, бин addRantController связывается с любым сервисом, с который ему возможно, потребуется (например, rantService). Но здесь вы также устанавливаете свойства formView и successView. Свойство formView - это логическое имя Вида отображаемого, в случае когда Контроллер получает запрос HTTP GET (или при любом возникновении ошибок). Аналогично, successView - это логическое имя Вида , служащего для подтверждения, успешной отправки формы. Арбитр Вида , будет использовать эти логические имена, чтобы найти требуемый по ситуации фактический объект Вида (View), для генерации отображаемой страницы.

Проверка формы ввода

Когда AddRantFormController вызывает addRant(), важно убедиться в полноте и правильности всех данных помещаемых, в объект Rant. Вы же не хотите, чтобы пользователи могли ввести только имя Штата не введя при этом номерных знаков (или наоборот). Кроме того, какой смысл в том чтобы правильно ввести и название Штата и номерной знак машины, но при этом не набрать никакого текста тирады? И важно, чтобы пользователь не мог ввести номерной знак, являющийся - недопустимым.

Валидацию в Spring MVC обеспечивает интерфейс org.springframework.validation.Validator. Он объявлен следующим образом:

public interface Validator {
void validate(Object obj, Errors errors);
boolean supports(Class clazz);
}

Реализации этого интерфейса должны изучить поля объекта, переданного в метод validate() и отвергнуть любые недопустимые значения посредством объекта Errors. Метод supports() используется, чтобы помочь Spring определить может ли валидатор использоваться для данного класса.

RantValidator (листинг 13.5) является реализацией Validator используемой для проверки объекта-команды Rant.

Листинг 13.5 Проверка занесения Rant

package com.roadrantz.mvc; 
import org.apache.oro.text.perl.Perl5Util;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.roadrantz.domain.Rant;
 
public class RantValidator implements Validator {
public boolean supports(Class clazz) {
return clazz.equals(Rant.class);
}
 
public void validate(Object command, Errors errors) {
Rant rant = (Rant) command;
 
ValidationUtils.rejectIfEmpty(
errors, "vehicle.state", "required.state",
"State is required.");
 
ValidationUtils.rejectIfEmpty(
errors, "vehicle.plateNumber", "required.plateNumber",
"The license plate number is required.");
 
ValidationUtils.rejectIfEmptyOrWhitespace( // Validates
errors, "rantText", "required.rantText", // required
"You must enter some rant text."); // fields
 
validatePlateNumber( // Validates plate
rant.getVehicle().getPlateNumber(), errors); // numbers
 
}
 
private static final String PLATE_REGEXP =
"/[a-z0-9]{2,6}/i";
 
private void validatePlateNumber(
String plateNumber, Errors errors) {
 
Perl5Util perl5Util = new Perl5Util();
 
if(!perl5Util.match(PLATE_REGEXP, plateNumber)) {
errors.reject("invalid.plateNumber",
"Invalid license plate number.");
}
}
}

Единственное, что нужно сделать, это настроить AddRantFormController чтобы использовать RantValidator. Вы можете сделать это, связывая RantValidator (показан здесь как внутренний бин) в бин AddRantFormController-а:

<bean id="addRantController" 
class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
<property name="validator">
<bean class="com.roadrantz.mvc.RantValidator" />
</property>
</bean>

Когда напыщенная речь введена, если все требуемые свойства установлены, и если номерной знак проходит проверку на корректность, будет вызван метод onSubmit() в AddRantFormController и тирада будет добавлена. Однако, если RantValidator отвергнет любое из полей, пользователю будет возвращен Вид формы, чтобы исправить ошибки.

За счет реализации интерфейса Validator, вы можете программно получить полный контроль над проверкой объектов-Команд в приложении. Это может быть замечательно, если ваши потребности проверки являются сложными и требуют специальной логики.

Тем не менее, в простых случаях, таких как обеспечение обязательных полей и основное форматирование, писать собственную реализацию интерфейса Validator , это как то уж, слишком.... Было бы неплохо, если бы мы могли написать правила проверки - декларативно, место того, чтобы писать их в коде Java. Давайте посмотрим, как использовать декларативные проверки в Spring MVC

Проверка с Commons Validator

Можно услышать жалобу, в адрес Spring MVC, что валидацию посредством интерфейса Validator, даже близко не сравнить, с возможностями проверки доступными в Jakarta Struts. Мы не можем спорить с этим фактом. Jakarta Struts действительно имеет очень хорошие возможности для объявления правил проверки за пределами кода Java. Однако, хорошей новостью, является - то, что мы можем производить подобные декларативные проверки и в Spring MVC.

Но прежде чем вы отправитесь копаться в JavaDoc Spring в поисках декларативной реализации Validator, вы должны знать, что у Spring - НЕТ такого валидатора. На самом деле, Spring вообще не содержит какой-либо реализаций интерфейса Validator, предлагая вам "с нуля" написать свою собственную.

Тем не менее, вам не придется отправляься очень далеко, чтобы найти готовые реализации Validator, поддерживающую декларативную проверку. Проект Spring Modules ( https://springmodules.dev.java.net ) является дочерним проектом Spring, который предоставляет ряд расширений Spring, область действия которых превышает основной проект. Одним из таких расширений является модуль валидации, использующий для обеспечения декларативной проверки Jakarta Commons Validator ( https://jakarta.apache.org/commons/validator ).

Для использования модуля проверки в вашем приложении, вы начнете с того, чтобы сделать файл springmodules-validator.jar доступным в classpath приложения. Если вы используете для сборки Ant, вам необходимо скачать дистрибутив Spring Modules (лично я использую версию 0.6) и найти файл spring-modules-0.6.jar в папке dist. Затем добавьте этот JAR, в <lib>-секцию для <war>-задачи, чтобы убедиться, что файл будет помещен в WEB-INF/lib папку WAR-файла приложения.

Если вы используете Maven 2, чтобы сделать вашу сборку (как это делаю - я), вам нужно добавить следующие <dependency> в pom.xml:

<dependency>
<groupId>org.springmodules</groupId>
<artifactId>springmodules-validation</artifactId>
<version>0.6</version>
<scope>compile</scope>
</dependency>

Вы также должны добавить Jakarta Commons Validator JAR в classpath вашего приложения. В Maven 2, это будет выглядеть следующим образом:

<dependency> 
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.1.4</version>
<scope>compile</scope>
</dependency>

Spring Modules обеспечивает реализацию Validator, называемую - DefaultBeanValidator. Этот класс настраивается в roadrantz-servlet.xml следующим образом:

<bean id="beanValidator" 
class= "org.springmodules.commons.validator.DefaultBeanValidator">
<property name="validatorFactory" ref="validatorFactory" />
</bean>

Класс DefaultBeanValidator не делает каких-либо фактических работ по проверке. Вместо этого, он делегируте Commons Validator-у проверки значений полей. Как вы можете видеть, DefaultBeanValidator имеет свойство validatorFactory связанное со ссылкой на бин validatorFactory. Этот бин, объявляется, используя следующий XML:

<bean id="validatorFactory" 
class="org.springmodules.commons.validator.DefaultValidatorFactory">
<property name="validationConfigLocations">
<list>
<value>WEB-INF/validator-rules.xml</value>
<value>WEB-INF/validation.xml</value>
</list>
</property>
</bean>

DefaultValidatorFactory это класс, который загружает конфигурацию Commons Validator от имени DefaultBeanValidator. Свойство validationConfigLocations принимает список из одного или нескольких файлов конфигураций проверки. Здесь мы использовали его для загрузки двух конфигураций: validator-rules.xml и validation.xml.

Файл validator-rules.xml содержит набор предопределенных правил проверок для общих целей, таких например как проверка электронной почты и номеров кредитных карт. Этот файл поставляется с дистрибутивом Commons Validator, так что вам не придется писать его самостоятельно - просто добавьте данный файл в WEB-INF каталог вашего приложения. Таблица 13.3 перечисляет все правила проверки, имеющиеся в validator-rules.xml.

Таблица 13.3 правил проверки Commons Validator, доступные в файле - validator-rules.xml.


Правило проверки !! Что оно проверяет
byte Это поле содержит значение, которое может быть отнесено к byte
сreditCard Это поле содержит строку, которая проходит LUHN-проверку и, таким образом, это допустимый номер для кредитной карты номер
date Это поле содержит значение, которое соответствует формату Date
double Это поле содержит значение, которое может быть отнесено к double
email Это поле содержит строку, которая, может являться, адресом электронной почты
float Это поле содержит значение, которое может быть отнесено к float
floatRange Это поле содержит значение, которое находится в пределах диапазона значений float
??intRange Это поле содержит значение, которое попадает в диапазон значений int
integer Это поле содержит значение, которое может быть отнесено к int
long Это поле содержит значение, которое может быть отнесено к long
mask Это поле содержит строковое значение, которое соответствует заданной маске
maxlength Это поле содержит не более чем заданное число символов
minlength Это поле имеет по крайней мере определенное количество символов
required Это поле не пустое
requiredif Это поле не пустое, но только если нет другого критерия
short Это поле содержит значение, которое может быть отнесено к short

Другой файл - validation.xml, определяет правила проверки специфичные для конкретного приложения, и применяются непосредственно в приложении RoadRantz. Листинг 13.6 показывает содержимое validation.xml применительно к RoadRantz.

Листинг 13.6 Объявление проверки в RoadRantz

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE form-validation PUBLIC
"-//Apache Software Foundation//DTD
Commons Validator Rules Configuration 1.1//EN"
"https://jakarta.apache.org/commons/dtds/validator_1_1.dtd">
 
<form-validation>
<formset>
<form name="rant">
<!--Requires rant text-->
<field property="rantText" depends="required">
<arg0 key="required.rantText" />
</field>
<!--Requires vehicle state -->
<field property="vehicle.state" depends="required">
<arg0 key="required.state" />
</field>
<!--Requires and masks plate number-->
<field property="vehicle.plateNumber"
depends="required,mask">
<arg0 key="invalid.plateNumber" />
<var>
<var-name>mask</var-name>
<var-value>^[0-9A-Za-z]{2,6}$</var-value>
</var>
</field>
</form>
</formset>
</form-validation>

Если содержимое validation.xml выглядит для вас странно знакомым, это вероятно, потому Struts использует те же файлы проверки XML. За кулисами, чтобы делать свою проверки, Struts использует - Commons Validator . А сейчас Spring Modules привнесли декларативную проверку в Spring.

Последнее, что необходимо сделать, это изменить объявление Контроллера, чтобы связать в него новую декларативную реализации Validator-a:

<bean id="addRantController" 
class="com.roadrantz.mvc.AddRantFormController">
<property name="formView" value="addRant" />
<property name="successView" value="rantAdded" />
<property name="rantService" ref="rantService" />
<property name="validator" ref="beanValidator" />
</bean>

Основное предположение в работе с SimpleFormController состоит в том, что форма - это одна страница. Это может быть хорошо, когда вы делаете что-то простое, например добавление всего одной тирады. Но что, если ваши формы являются сложными, требуя от пользователя ответить на несколько вопросов? В этом случае, возможно, имеет смысл разбить форму на несколько подразделов и водить пользователей через нее с помощью мастера. Давайте посмотрим, как Spring MVC может помочь вам построить мастера форм.

Обработка сложных форм с помощью мастеров

Файл:Fig139.png
Рисунок 13.9 wizard-контроллер это специальный form-контроллер, который помогает разбивать длинные и сложные формы на несколько страниц.

Еще одна из задуманных возможностей приложения RoadRantz состоит в том, что любой может зарегистрироваться в нем в качестве пользователя (как motorist (автомобилист)в терминах RoadRantz) и затем получать уведомления, если какие-либо тирады прозвучат об их автомобилях. Мы разработали уведомления о тирадах по электронной почте в главе 12. Значит мы также должны предоставить пользователям возможность зарегистрировать себя и свои транспортные средства. Мы могли бы поместить всю форму регистрации автомобилист в единую JSP и расширив SimpleFormController, обрабатывать и сохранять данные. Однако, мы не знаем, сколько автомобилей каждый из пользователей захочет зарегистрировать, и нам придется излишне изощряться, чтобы запрашивать у пользователя неизвестное количество данных о транспортных средствах в рамках одной формы.

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


  • Общая информация пользователей, такие как фамилия, имя, пароль и адрес электронной почты
  • Информация об автомобиле (название Штата и номерной знак)
  • Подтверждение (возможность для пользователя проконтролировать введенную информацию перед отправкой формы)

К счастью, Spring MVC предлагает AbstractWizardFormController, чтобы помочь нам. AbstractWizardFormController является самым мощным из Контроллеров , которые идут со Spring. Как показано на рисунке 13.9, wizard-контроллер представляет собой особый тип form-контроллера, который собирает данные формы, состоящей из нескольких страниц в один общий объект-команду для последующей ее обработки. Давайте посмотрим, как построить многостраничную форму регистрации с использованием AbstractWizardFormController.

Создание основного wizard-контроллера

Для построения wizard-контроллера, необходимо расширить класс AbstractWizardFormController . Класс MotoristRegistrationController (листинг 13.7) показывает простейший wizard-контроллер, который будет использоваться для регистрации пользователей в RoadRantz.

Листинг 13.7 Регистрация автомобилистов с помощью мастера

package com.roadrantz.mvc; 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractWizardFormController;
import com.roadrantz.domain.Motorist;
import com.roadrantz.domain.Vehicle;
import com.roadrantz.service.RantService;
 
public class MotoristRegistrationController
extends AbstractWizardFormController {
public MotoristRegistrationController() {
setCommandClass(Motorist.class); // Sets command
setCommandName("motorist"); // class, name
}
 
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
Motorist formMotorist = new Motorist();
List<Vehicle> vehicles = new ArrayList<Vehicle>(); // Creates form
vehicles.add(new Vehicle()); // backing object
formMotorist.setVehicles(vehicles);
return formMotorist;
}
 
protected Map referenceData(HttpServletRequest request,
Object command, Errors errors, int page) throws Exception {
 
Motorist motorist = (motorist) command;
Map refData = new HashMap();
 
if(page == 1 && request.getParameter("_target1") != null) {
refData.put("nextVehicle", // Increments next
motorist.getVehicles().size() - 1); //vehicle pointer
}
return refData;
}
 
protected void postProcessPage(HttpServletRequest request,
Object command, Errors errors, int page) throws Exception {
 
Motorist motorist = (Motorist) command;
 
if(page == 1 && request.getParameter("_target1") != null) {
motorist.getVehicles().add(new Vehicle()); //Adds new
} //blank vehicle
}
 
protected ModelAndView processFinish(HttpServletRequest request,
HttpServletResponse response, Object command,
BindException errors)
throws Exception {
 
Motorist motorist = (motorist) command;
 
// the last vehicle is always blank...remove it
motorist.getVehicles().remove(
motorist.getVehicles().size() - 1);
 
rantService.addMotorist(motorist); //Adds motorist
 
return new ModelAndView(getSuccessView(),
"motorist", motorist);
}
// injected
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
// returns the last page as the success view
private String getSuccessView() {
return getPages()[getPages().length-1];
}
}

Как и с любым command-контроллером, вы должны установить класс Команды, при использовании - wizard-контроллера. Здесь MotoristRegistrationController был настроен, на использование Motorist в качестве класса Команды. Из-за того что автомобилист может регистрировать не только одно, но и нескольких транспортных средств, метод formBackingObject() переопределяется для установки свойства vehicles в список объектов Vehicle. Список начинается с чистого объекта Vehicle, чтобы затем его поля заполнять - в форме.

Поскольку пользователь может зарегистрировать любое количество транспортных средств, и список автомобилей будет расти с каждым новым добавленным, Вид формы нуждается в способе знать: который из элементов списка будет являться следующим обрабатываемым элементом. Чтобы сделать индекс следующего автомобиля доступны для формы, метод referenceData() - переопределяется.

Единственным обязательным методом в AbstractWizardFormController является processFinish(). Этот метод вызывается чтобы финализировать форму, когда пользователь закончил ее заполнения (по-видимому, нажав на кнопку "Submit"). В MotoristRegistrationController, метод processFinish() посылает данные заключенные в объекте Motorist методу addMotorist() во внедренном объекте RantService .

Обратите внимание, что в MotoristRegistrationController, нет ничего указывающего на то, что страницы? составляют - форму. Или на порядок появления страниц. Это потому, что AbstractWizardFormController выполняет большую часть работы, связанной с управлением рабочим процессом мастера как бы "под одеялом". Но как все-жеAbstractWizardFormController узнает, что страниц составляют - форму?

Часть из этого может стать более очевидной, если вы увидите - то, как MotoristRegistrationController объявлен в roadrantz-servlet.xml:

<bean id="registerMotoristController"
class="com.roadrantz.mvc.MotoristRegistrationController">
<property name="rantService" ref="rantService" />
<property name="pages">
<list>
<value>motoristDetailForm</value>
<value>motoristVehicleForm</value>
<value>motoristConfirmation</value>
<value>redirect:home.htm</value>
</list>
</property>
</bean>

Вот как мастер узнает, какие страницы составляют форму: список логических имен Видов? присваивается свойству - pages. Эти имена в конечном итоге будет разрешены в объекты Вида - Арбитром Видов. Но пока, мы лишь отметим для себя, что эти логические имена будут преобразованы в имена файлов JSP.

Хотя это это и проясняет как MotoristRegistrationController узнает, какие собственно страницы следует показывать, но это все еще не говорит нам о том, откуда же берется порядок их показа.

Пошагово сквозь страницы формы

Первой страницей отображаемой в любом wizard-контроллере, будет первая страница в списке, который задается свойством - pages . В нашем конкретном случае мастера регистрации "автомобилиста" , первой будет отображена страница - motoristDetailForm.

В вопросе выбора, к какой же следующей странице нужно переходить, объект AbstractWizardFormController консультирует один из его методов - getTargetPage(). Этот метод возвращает целое число, которое является индексом (от нуля) в списке задающем значения свойства - pages .

Реализация по умолчанию getTargetPage() определяет, следующую страницу для перехода, на основе параметра в запросе называемого - name значение которого должен начинается с _target и заканчивается числом. Метод getTargetPage() удаляет _target-префикс параметра и использует оставшееся число как индекс в списке страниц. Например, если запрос имеет параметр, с именем _target2, пользователь получит страницу генерируемую Видом motoristConfirmation.

Знание того, как getTargetPage() - работает, поможет вам настроить кнопки "Вперед" и "Назад" в коде HTML-страниц вашего мастера. Например, предположим, что пользователь находится на странице motoristVehicleForm (индекс = 1). Чтобы создать кнопки "Вперед" и "Назад" на этой странице, достаточно добавить в ее HTML код две submit-кнопки, с соответствующими именами (включающими _target префикс):

<form method="POST" action="feedback.htm"> 

<input type="submit" value="Back" name="_target0">
<input type="submit" value="Next" name="_target2">
</form>

При нажатии кнопки "Назад", параметр с name равным _target0, помещается в запрос отсылаемый обратно в MotoristRegistrationController. Метод getTargetPage() обработает этот параметр и отправить пользователя на страницу motoristDetailForm (индекс = 0). Аналогичным образом, если нажата кнопка "Вперед" , getTargetPage() будет обрабатывать параметр с name равным _target2 и решит отправить пользователю страницу motoristConfirmation (индекс = 2).

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

Завершение мастера

Мы выяснили, как сделать шаг назад или шаг вперед через мастер формы. Но как вы можете сказать Контроллеру, что вы закончили, и что processFinish() метод должен быть вызван?

Для этого есть другой специальный параметр запроса, с name равным - _finish, который указывает, AbstractWizardFormController-у что пользователь закончил заполнение формы и хочет представить информацию для обработки. Так же, как параметры _targetX, _finish может быть использован для создания кнопку "Готово" на странице:

<form method="POST" action="feedback.htm"> 

<input type="submit" value="Finish" name="_finish">
</form>

Когда AbstractWizardFormController увидит _finish-параметр в запросе, он передаст управление методу processFinish() для окончательной обработки формы.

В отличие от других form-контроллеров, AbstractWizardFormController не обеспечивает средств настройки Вида для страницы подтверждения ввода формы. И реализация этой функциональности ложится на нас. Потому, мы добавили метод getSuccessView() в MotoristRegistrationController чтобы вернуть последнюю страницу в списке pages. Так что, после окончания ввода формы, метод processFinish() возвратит ModelAndView с последним по списку Видом из pages, в качестве Вида подтверждения ввода формы.

Отмена мастера

А что делать, если пользователь уже частично пройдя этапы регистрации вдруг решает, что он не хочет, завершать ее в данный момент? Как он может отказаться от сохранения введенных в форме данных?

Помимо очевидного ответа - "он может закрыть свой браузер", вы также можете добавить в форму кнопку "Отмена":

<form method="POST" action="feedback.htm"> 

<input type="submit" value="Cancel" name="_cancel">
</form>

Как вы видите, кнопка "Отмена" должна иметь _cancel в качестве name, чтобы, при ее нажатии, браузер помещал в запрос параметр именуемый - _cancel. Когда AbstractWizardFormController получит этот параметр, он передаст управление в метод processCancel().

По умолчанию, processCancel() бросает исключение о том, что операции отмены не поддерживается. Таким образом, вам необходимо переопределить этот метод, чтобы он (как минимум) отправлял пользователя к любой странице (на ваше усмотрение) если тот нажмет кнопку "Отмена". Данная реализация processCancel() отправляет пользователя в Вид подтверждения ввода формы:

protected ModelAndView processCancel(HttpServletRequest request,
HttpServletResponse response, Object command,
BindException bindException) throws Exception {
 
return new ModelAndView(getSucessView());
}

Если есть потребность в какой-либо работе по очистке и освобождению ресурсов, выполняемых в случае отмены, вы просто можете разместить этот код в методе processCancel() перед возвращением объекта ModelAndView.

Постраничная проверка в мастерах формы

Как и для любого command-контроллера, данные в объект Команды, wizard-контроллера могут быть проверены с помощью объекта Validator. Однако, есть одно небольшое - но.

С другими command-контроллерами, объект Команды полностью формируется - сразу. Но с wizard-контроллерами, объект Команды заполняется поэтапно, по мере того как пользователь делает шаги по страницам мастера. Используя мастера формы, не имеет смысла выполнять проверку - одномоментно (т.е. "ВСЁ - сразу"), потому что, если проверку провести слишком рано, вы вероятно обнаружите при проверке проблемы, вызванные тем, что пользователь не завершил работу мастера. И наоборот, после нажатии кнопки "Готово", будет уже слишком поздно для проверки, потому что любые найденные ошибки могут охватить несколько страницы (и как далеко пользователю теперь нужно будет вернуться?).

Вместо одномоментной проверки объекта Команды , wizard-контроллеры проверяют объект Команду - постранично. Это делается каждый раз, когда происходит меж-страничный переход, путем вызова метода validatePage(). Реализация по умолчанию для метода validatePage() - "пустая" (то есть, нет проверки), но вы можете переопределить его, предложив "свою цену".

К примеру, на странице motoristDetailForm вы спросите у пользователя его адрес электронной почты. Это поле не является обязательным, но если оно будет заполнено, то его содержимое должно быть в допустимом формате для адреса электронной почты. Следующий пример метода validatePage() показывает, как проверить адрес электронной почты, когда пользователь переходы со страницы motoristDetailForm:

protected void validatePage(Object command, Errors errors, 
int page) {
 
Motorist motorist = (Motorist) command;
MotoristValidator validator =
(MotoristValidator) getValidator();
 
if(page == 0) {
validator.validateEmail(motorist.getEmail(), errors);
}
}

Когда пользователь переходит со страницы motoristDetailForm (индекс = 0), будет вызван метод validatePage() с 0, передаваемым в page аргумент. Первая делом validatePage() получают ссылку на объект Команду - Motorist и ссылку на объект MotoristValidator. Поскольку не нужно делать проверку электронной почты из любой другой страницы, validatePage() проверяет, что пользователь переходит со страницы 0.

На данный момент, вы можете выполнять проверку электронной почты непосредственно в методе validatePage(). Тем не менее, типичный мастер будет иметь несколько полей, которые должны быть проверены. Таким образом, validatePage() может стать довольно громоздким. Мы рекомендуем вам делегировать ответственность за филигранную проверку уровня отдельных полей в отдельные методы объекта Validator вашего Контроллера , как мы уже сделали здесь с вызовом validateEmail() метода в MotoristValidator.

Все это означает, что вы должны будете установить свойство validator при настройке Контроллера:

<bean id="registerMotoristController"
class="com.roadrantz.mvc.MotoristRegistrationController">
 
<property name="rantService" ref="rantService" />
<property name="pages">
<list>
<value>motoristDetailForm</value>
<value>motoristVehicleForm</value>
<value>motoristConfirmation</value>
<value>redirect:home.htm</value>
</list>
</property>
 
<property name="validator">
<bean class="com.roadrantz.mvc.MotoristValidator" />
</property>
</bean>

Важно знать, что в отличие от других command-контроллеров , wizard-контроллеры никогда не вызывают стандартной метод validate() их объекта Validator. Это потому, что метод validate() одномоментно проверяет весь объект Команды, и потому понятно, что по причинам изложенным выше для мастера это - бессмысленно.

Контроллеры, которые вы видели до сих пор являются частью одной иерархии, уходящей своими корнями к интерфейсу Controller . Хотя Контроллеры постепенно становятся все более сложными (и более мощными) по мере продвижения вниз по иерархии, все Контроллеры, реализующие интерфейс Controller чем-то напоминают друг-друга. Но прежде чем мы закончим наше обсуждение Контроллеров , давайте еще посмотрим на иной Контроллер, который очень отличается от прежних - "разовый"-контроллер.

Работа с "разовыми"-контроллерами

Последним Контроллером, который вы можете найти для себя полезным является - т.н "разовый"-контроллер. Несмотря на сомнительное название, "разовые"-контроллеры могут быть весьма полезны и просты в использовании. "Разовые"-контроллеры значительно проще, чем другие Контроллеры, о чем свидетельствует интерфейс ThrowawayController:

public interface ThrowawayController { 
ModelAndView execute() throws Exception;
}

Для создания собственного "разового"-контроллера, все, что вы должны сделать, это реализовать этот интерфейс и поместить в логику программы в метод execute() . Довольно просто, не так ли?

Но, стойте. Как параметры, передаются в Контроллер? Выполняемым методам других Контроллеров даются HttpServletRequest и объекты Команд , из которых можно вытянуть все параметры запроса. Если-же метод execute() не принимает аргументов, то как может ваш Контроллер обрабатывать ввод данных пользователем?

Вы могли заметить на рисунке 13.5, что интерфейс ThrowawayController даже вне иерархии интерфейса Controller. Это потому, что "разовые"-контроллеры сильно отличаются от других Контроллеров . Вместо того, чтобы получать параметры через HttpServletRequest или объект Команды, "разовые"-контроллеры сами выступают в качестве своих собственных объектов Команды. Если вы когда-либо работали с WebWork, это может показаться вполне естественным, поскольку WebWork-"действия" ведут себя аналогичным образом.

Из требований к RoadRantz мы знаем, что нам нужно отобразить список тирад за определенный месяц, день и год. Мы могли бы реализовать это с помощью command-контроллера, как мы это делали с RantsForVehicleController (листинг 13.3). К сожалению, у нас не существует готового доменного объекта, который бы уже содержал месяц, день и год. Это означает, что мы должны были бы создать специальный класс Команд для хранения этих данных. Было бы совсем не трудно создать такой POJO, но возможно, есть путь и получше.

Вместо реализации RantsForDayController как command-контроллера, давайте выполним его его в виде ThrowawayController-а, как показано в листинге 13.8.

Листинг 13.8 "разовый"-контроллер, который выдает список тирад на данный день:

package com.roadrantz.mvc; 
import java.util.Date;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.throwaway.ThrowawayController;
import com.roadrantz.service.RantService;
 
public class RantsForDayController implements ThrowawayController {
private Date day;
 
public ModelAndView execute() throws Exception {
List<Rant> dayRants = rantService.getRantsForDay(day); //Gets list
//of rants
return new ModelAndView("dayRants", "rants", dayRants);//for day
}
 
public void setDay(Date day) { //Binds day to
this.day = day; //request
}
 
private RantService rantService;
public void setRantService(RantService rantService) {
this.rantService = rantService;
}
}

Перед тем как RantsForDayController обрабатывает запрос, Spring будет вызывать метод setDay(), передавая в него значение параметра day из запроса. Затем в методе execute() , RantsForDayController просто передаст day, в rantService.getRantsForDay(), чтобы получить список тирад в этот день. Одна вещь, которая остается такой же, как и других Контроллерах то, что метод execute() должен возвращать объект ModelAndView, после своего завершения.

Так же, как с любым другим Контроллером , вы должны объявить "разовый"-контроллер в файле конфигурации контекста DispatcherServlet. Есть лишь одна небольшая разница, которую вы можете увидеть в этой конфигурации RantsForDayController:

<bean id="rantsForDayController"
class="com.roadrantz.mvc.RantsForDayController"
scope="prototype">
<property name="rantService" ref="rantService" />
</bean>

Обратите внимание, что scope-атрибут был установлен на prototype. Это то, за что "разовые"-контроллеры и получили свое название. По умолчанию все бины являются - singleton , и поэтому, пока вы не установите scope=" prototype", бин RantsForDayController будет повторно использоваться между запросами. Это значит, его свойства (которые должны отражать значения параметра запроса) также могут быть использован повторно. Настройка scope=" prototype" говорит Spring выбросить Контроллер после того, как он был использован, и создавать "свежий" экземпляр его, для каждого нового запроса.

Есть еще одна вещь, которую следует сделать прежде, чем мы сможем использовать наши "разовые"-контроллеры. DispatcherServlet знает, как направить запросы к Контроллерам благодаря использованию адаптера-обработчика. Концепция адаптеров-обработчиков это нечто, о чем вы обычно не должны волноваться, потому что DispatcherServlet использует адаптер-обработчика по умолчанию , который отправляет запросы к Контроллерам в иерархии интерфейса Controller.

Но из-за того что ThrowawayController находится вне иерархии Controller, то DispatcherServlet не знает, как ему общаться с ThrowawayController. Чтобы заставить его работать, вы должны сообщить DispatcherServlet-у о необходимости использовать другой адаптер-обработчика. В частности, необходимо настроить ThrowawayControllerHandlerAdapter следующим образом:

<bean id="throwawayHandler" 
class="org.springframework.web.servlet.mvc.
throwaway.ThrowawayControllerHandlerAdapter"/>

Объявляя этот бин - так, вы говорите DispatcherServlet, заменить его адаптер-обработчика по умолчанию на ThrowawayControllerHandlerAdapter.

Это нормально, если ваше приложение не содержит ничего, кроме "разовых"-контроллеров. Но приложение RoadRantz будет использовать как "разовые" так и обычные ("регулярные") контроллеры рядом друг с другом в одном приложении.

Следовательно, Вы все еще нуждаетесь, чтобы DispatcherServlet мог использовать также и "регулярный" адаптер-обработчика - SimpleControllerHandlerAdapter. Потому, вам необходимо объявить в вашей конфигурации и SimpleControllerHandlerAdapter, сделав это - следующим образом:

<bean id="simpleHandler" 
class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>

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

Независимо от того, какая функциональность вашими Контроллерами выполняется, в конечном итоге, им всем придется вернуть некоторые результаты - пользователю. Страницы результатов генерируются и отображаются - Видами, которые будут выбраны по их логическое именам, задаваемым при создании объектов ModelAndView. Однако должен существовать механизм для отображения логических имен в фактические Виды , которые и будут показывать - ответ. Мы увидим, его в главе 14, когда мы обратим наше внимание на арбитров-видов Spring.

Но для начала, обратили ли вы внимание, что все Контроллеры Spring MVC объявлены, генерирующими - исключения? Вполне ведь возможно, что все может пойти наперекосяк, когда Контроллер обрабатывает запрос. Если исключение выброшено из Контроллера, то что пользователь видит? Давайте выясним, как контролировать поведение ошибочных контроллеров с арбитром исключения.

Обработка исключений

Наклейка для бампера - гласит: "Ошибка это не опция. Она входит в обязательную поставку с программным обеспечением" Если отбросить юмор то это формулировка универсальной истины. Не все всегда идет как задумано, и даже - в программном обеспечении. Когда ошибка происходит (а это неизбежно произойдет), вы ведь наверное хотели бы, чтобы пользователи вашего приложения, увидел не загадочную трассировку стека, а - понятное дружественным сообщение? Как же можно изящно сообщать об ошибках в программе вашим пользователям?

Когда исключение генерируется - Контроллером, то Арбитр Исключений SimpleMappingExceptionResolver приходит к нам на помощь. Используйте следующее определение <bean> , чтобы настроить SimpleMappingExceptionResolver для элегантной обработки любых java.lang.Exceptions выброшенных из Контроллеров Spring MVC:

<bean id="exceptionResolver" 
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.Exception">friendlyError</prop>
</props>
</property>
</bean>

Свойство ExceptionMappings принимает карту java.util.Properties, которая содержит отображение полных имен классов исключений - в логические имена Видов. В приведенном случае базовый класс исключений, отображается на Вид с логическим именм friendlyError. Так, что в случае возникновения какой-либо ошибки, пользователям не придется видеть загадочные трассировки стека в своем браузере.

Когда Контроллер генерирует исключение, Арбитр Исключений SimpleMappingExceptionResolver преобразует его в логический friendlyError, который, в свою очередь, будет преобразован в фактически Вид с использованием соответственно настроенных Арбитров Вида. Если InternalResourceViewResolver будет настроен, тогда возможно, пользователю будет отправлена страница, заданная в /WEB-INF/jsp/friendlyError.jsp.

Резюме

Spring Framework поставляется с мощным и гибким веб-фреймворком, который сам построен на основе принципов Spring слабой связи, внедрения зависимостей и расширяемости.

Spring предлагает множество обработчиков-маппинга, которые помогают выбрать соответствующий Контроллер для обработки поступившего запроса. У вас также есть возможность выбора, среди нескольких методов определения соответствия URL-адресов - Контроллерам Эти методы основываются на:


  • имени бина Контроллера
  • имени класса Контроллера
  • простом отображения URL-к-Контроллеру
  • метаданных в исходном коде.

Для обработки запроса, Spring предоставляет широкий выбор классов Контроллеров разной сложности, начиная от очень простого интерфейса Controller, и заканчивая очень мощными wizard-контроллерами. Наличие нескольких слоев между ними, позволяет Вам всегда выбрать Контроллер с необходимым количеством функций(и не более сложный, чем требуется). Это позиционирует Spring отдельно от других веб-платформ MVC, таких как Struts или WebWork, где Ваш выбор ограничен только одним или двумя Action-классами.

В общем случае, Spring MVC поддерживает слабую связь между тем, как выбирается Контроллер чтобы обработать запрос и как выбирается Вид чтобы отобразить результаты запроса. Это мощная концепция для создания веб-слоя наиболее подходящего для Вашего приложения.

В этой главе Вы приняли участие в беглом туре, посвященном тому как Spring MVC обрабатывает запросы. По пути Вы также увидели, как устроена большая часть веб-слоя приложений RoadRantz.

Независимо от того, какая функциональность обеспечивается Контроллером, Вас в конечном счете более всего будет интересовать то, что должен получить и увидеть пользователь. Итак, в следующей главе, мы опираясь на Spring MVC, будем создавать слой Вида для приложения RoadRantz. В дополнение к JSP, Вы узнаете, как использовать в качестве альтернативы - языки шаблонов, такие как Velocity и FreeMarker. И Вы также узнаете, как динамически генерировать не-HTML-контент, в частности: Excel таблицы, PDF документы, и RSS-каналы.

Wikijava.org.ua-1px.png

]]>
Книги по Java https://linexp.ru?id=4722 Wed, 29 Jun 2022 13:58:15 GMT
<![CDATA[Главы 10-12 Spring in Action 2th edition]]> Книги по Java https://linexp.ru?id=4721 Wed, 29 Jun 2022 13:57:39 GMT <![CDATA[Core Java 2 Том I]]> ОсновыКнига адресована прежде всего программистам-профессионалам и представляет собой исчерпывающий справочник и методическое пособие по основам программирования на языке Java. Однако это не просто учебник по синтаксису языка.

ОСНОВЫ

Оглавление

Книга адресована прежде всего программистам-профессионалам и представляет собой исчерпывающий справочник и методическое пособие по основам программирования на языке Java. Однако это не просто учебник по синтаксису языка. Назначение книги — Обучить методам объектно-ориентированного программирования и научить справляться с основными проблемами в этой области. Работа с книгой не требует опыта программирования на языке С++ и применения методов ООП. Любой программист, работавший с такими языками, как VisuaJ Basic, С, Cobol или Pascal, не будет испытывать затруднений при работе с ней. Книга содержит многочисленные примеры и советы по программированию. Авторы уделили большое внимание возможностям, которые стали доступны программистам с появлением JDK 5.0. Новые языковые и библиотечные средства нашли свое отражение в кодах примеров, приведенных в книге.

Введение

Глава 1

Введение в язык Java

  • Программная платформа Java
  • Характерные особенности языка Java
  • Простота
  • Поддержка объектов
  • Поддержка распределенных вычислений
  • Надежность
  • Безопасность
  • Независимость от архитектуры компьютера
  • Переносимость
  • Использование интерпретатора
  • Производительность
  • Поддержка потоков
  • Динамичность
  • Java и Internet
  • История языка Java
  • Распространенные заблуждения относительно языка Java

Глава 2

Среда программирования на языке Java

  • Инсталляция Java Development Kit
  • Выбор среды разработки программ
  • Использование инструментов, вызываемых из командной строки
  • Использование интегрированной среды разработки программ
  • Компиляция и запуск программ из текстового редактора
  • Выполнение графического приложения
  • Создание и запуск аплетов

Глава 3

Основные конструкции языка Java

  • Простая программа на языке Java
  • Комментарии
  • Типы данных
  • Переменные
  • Операторы
  • Строки
  • Ввод и вывод
  • Поток управления
  • Работа с большими числами
  • Массивы

Глава 4

Объекты и классы

  • Введение в объектно-ориентированное програм мирование
  • Использование готовых классов
  • Определение собственных классов
  • Статические поля и методы
  • Параметры методов
  • Формирование объектов
  • Пакеты
  • Комментарии и документирование
  • Рекомендации по разработке классов

Глава 5

Глава 6

Глава 7

Глава 8

Глава 9

Глава 10

Глава 11

Глава 12

Глава 13

Приложение А

Приложение Б

Шаблон:Sam

]]>
Книги по Java https://linexp.ru?id=4720 Wed, 29 Jun 2022 13:57:04 GMT
<![CDATA[Core Java 2 Том I Глава 2]]> Среда программирования на языке JavaВ этой главе вы научитесь инсталлировать Java Development Kit (JDK), а также компилировать и запускать на выполнение программы различных типов: консольные программы, графические приложения и аплеты.
В этой главе...
  • Инсталляция Java Development Kit
  • Выбор среды разработки программ
  • Использование инструментов, вызываемых из командной строки
  • Использование интегрированной среды разработки программ
  • Компиляция и запуск программ из текстового редактора
  • Выполнение графического приложения
  • Создание и запуск аплетов

В этой главе вы научитесь инсталлировать Java Development Kit (JDK), а также компилировать и запускать на выполнение программы различных типов: консольные программы, графические приложения и аплеты. Мы будем использовать средства па­кета JDK, набирая команды в окне, предназначенном для работы с оболочкой. Однако многие программисты предпочитают интегрированные среды разработки. Вы узнаете, как использовать широко распространенную интегрированную среду разработки программ для компиляции и выполнения программ, написанных на языке Java. Изучать интегрированные среды разработки программ и пользоваться ими до­вольно легко, однако они долго загружаются и предъявляют высокие требования к ресурсам компьютера, так что применять их для разработки небольших программ не имеет смысла. В качестве компромисса можно использовать текстовый редактор, из которого вызываются компилятор и интерпретатор языка Java. Овладев приемами, рассмотренными в этой главе, и выбрав инструмент для разработки программ, вы сможете перейти к главе 3, с которой и начинается изучение языка Java.

Инсталляция Java Development Kit

Наиболее полные и современные версии Java 2 Standart Edition (J2SE) реализова­ны для операционных систем Solaris, Linux и Windows. Версии для Macintosh и мно­гих других платформ лицензированы и поставляются производителями соответст­вующих систем.

Где найти JDK

Если вам необходимо скопировать на свой компьютер Java Development Kit, вам надо обратиться на Web-страницу Sun и, приложив немалые усилия, чтобы разобрать­ся в терминах, найти требуемое программное обеспечение.

Аббревиатура JDK вам уже знакома. Она, как нетрудно догадаться, означает Java Development Kit. Некоторые трудности может вызвать тот факт, что в версиях 1.2-1.4 пакет называется Java SDK (Software Development Kit). Кое-где вы найдете и привыч­ный термин JDK.
Далее вам надо найти последовательность символов J2SE, которая означает Java 2 Standard Edition. Существуют также J2EE (Java 2 Enterprise Edition) и J2ME (Java 2 Micro Edition).

Термин "Java 2" был введен в 1998 году, когда специалисты компании Sun по мар­кетингу поняли, что очередной номер реализации никак не отражает глобальных от­личий между JDK 1.2 и предыдущими версиями. Но, поскольку такое мнение сформи­ровалось уже после выхода в свет JDK, было решено, что номер версии 1.2 останется за инструментальным пакетом. Последующие реализации JDK имеют номера 1.3, 1.4 и 5.0. Платформа же была переименована из Java в Java 2. Таким образом, последний инструментальный пакет называется Java 2 Standard Edition Development Kit version 5.0, илиJ2SE 5.0.

Для нас, инженеров, эти манипуляции с именами версий выглядят, по меньшей мере, странно. Именно поэтому мы никогда не сможем достичь успеха, в маркетинге.

Если вы используете операционную систему Solaris, Linux или Windows, можете за­грузить Java Development Kit, обратившись по адресу http://link.linexp.ru/6011992/https://java.sun.com/j2se. Выберите версию 5.0 или более позднюю реализацию и укажите требуемую платформу.

Время от времени компания Sun выпускает наборы, содержащие, помимо Java Development Kit, интегрированную среду разработки. Это интегрированное окруже­ние в разные периоды времени называлось Forte, Sun ONE Studio, Sun Java Studio и Netbeans. Трудно предугадать, какое имя оно будет носить в тот момент, когда вы обратитесь на Web-узел компании Sun. На данном этапе нам нужен только Java Development Kit. Если впоследствии вы решите поработать с интегрированной сре­дой разработки, то сможете скопировать ее с сервера http://link.linexp.ru/6011992/https://netbeans.org

После того как JDK окажется на вашем компьютере, следуйте инструкциям по ин­сталляции для вашей платформы. На момент написания данной книги инструкции были представлены в документе http://link.linexp.ru/6011992/https://java.sun.com/j2se/5.0/install.html

От конкретной системы зависит только процесс инсталляции и компиляции. После того как вы убедились в работоспособности Java, вы сможете непосредственно выполнять рекомендации, приведенные в данной книге. Независимость от операци­онной системы является одним из главных преимуществ Java перед многими другими языками.

Document-check.png Процедура инсталляции предлагает по умолчанию имя каталога для установки JDK. В имя каталога входит номер версии, например jdks.o. Это может показаться не столь важным, но данный подход упрощает работу с новыми версиями JDK, уста­новленными для проверки.
Если вы работаете в системе Windows, мы рекомендуем не принимать предлагаемый каталог. Обычно это c:\Program Files\jdk5.0. По крайней мере, откажитесь от Program Files и разместите пакет в другой позиции файловой системы.
В данной книге мы используем для ссылки на каталог, содержащий Java Development Kit, имя jdk. Например, если в тексте указано имя jdk/Ып, значит, речь идет о катало­ге /usr/local/jdk5.0/bin или с:\jdk5.0\bin.

Установка пути к исполняемым файлам

После инсталляции JDK вам нужно сделать еще один шаг: добавить имя каталога jdk/bin в список путей, по которым операционная система ищет исполняемые фай­лы. В различных системах это действие выполняется по-разному.

  • В системе Unix (включая Solaris и Linux) процедура редактирования путей к выполняемым файлам зависит от используемой оболочки. В случае С shell {эта оболочка принимается на платформе Solaris по умолчанию) добавьте в ко­нец файла ~/.bashrc следующую строку:

. set path=(/usr/local/jdk/bin $path)

  • Если вы работаете с оболочкой Bourne Again (она принимается по умолчанию на платформе Linux), добавьте в конец файла ~/.bashrc или ~/.bash_profile строку, приведенную ниже.

export PATH=/usr/local/jdk/bin:$РАТН

  • Для других оболочек системы Unix нужно найти способ выполнить аналогич­ную процедуру.
  • На платформе Windows 95/98/Ме добавьте строку, приведенную ниже, в файл AUTOEXEC.ВАТ:

SET PATH=C:\jdk\bin;%PATH%

  • Заметьте, что вокруг знака равенства нет пробелов. Чтобы эффект от включе­ния этой строки проявился, нужно перезагрузить компьютер.
  • На платформе Windows NT/2000 откройте панель управления, выберите пик­тограмму System, а затем вкладку Environment. Прокрутите содержимое окна User Variables, пока не найдете переменную PATH. Добавьте имя каталога jdk/bin в начало списка; новый элемент списка отделяется от уже существую­щих точкой с запятой, например:

c:\jdk\bin; остальное

  • Сохраните выполненные установки. В любом новом консольном окне заданный путь будет использоваться для поиска выполняемых файлов.

Правильность установок можно проверить следующим образом.

Откройте окно оболочки. Как вы это сделаете, зависит от операционной системы. Введите следующую строку:

java -version

Нажмите клавишу <Enter>. На экране должно появиться следующее сообщение: java version "5.0"

Java (TM) 2 Runtime Environment, Standard Edition Java HotSpot (TM) Client vm

Если вместо этого сообщения появится строка наподобие "java:command not found", "Bad command or file name" или "The name specified is nor recognized as an internal or external command, operable program or batch file", нужно еще раз проверить, правильно ли выполнена инсталляция и установлен путь.

Инсталляция библиотек и документации

Библиотечные файлы поставляются в пакете JDK в виде архива src. zip. Вам следует распаковать этот файл, чтобы получить доступ к исходным текстам программ. Мы настоятельно рекомендуем сделать это. Выполните перечисленные ниже, действия.

1. Убедитесь, что пакетJDKустановлен, а имя каталога jdk/bin находится в списке путей к выполняемым файлам.

2. Откройте окно для ввода команд оболочки.

3. Перейдите в каталог^ {т.е. в каталог /usr/local/ jdk5 . О или С: / jdk5.0).

4. Создайте подкаталог s г с.

mkdir src cd src

5. Выполните следующую команду:

jar xvf ../src.jar (или, если вы работаете в системе Windows, jar xvf . Л src . zip).

Document-check.png Файл src.zip содержит исходные тексты всех общедоступных библиотек. Чтобы получить дополнительные исходные тексты (компилятора, виртуальной машины, плат-форменно-ориентированных методов и вспомогательных классов), посетите Web-страницу https: //www.sun.com/software/communitysource/java2
Документация содержится в отдельном архиве, который может иметь различные расширения (.zip, .gz и .z). Ее можно скопировать с Web-страницы http://link.linexp.ru/6011992/https:// java.sun.com/docs. Выберите тот формат архива, который вам подходит. Если сомневаетесь, остановитесь на формате .zip, поскольку такой файл можно распаковать с помощью программы j аг, входящей в пакет JDK. Если вы решили использовать программу jar, выполните следующие действия.

1. Убедитесь, что пакет JDK установлен правильно, а имя каталога jdk/bin находится в списке путей к выполняемым файлам.

2. Скопируйте файл с расширением .zip, содержащий документацию, на свой компьютер и поместите его в каталог jdk. Файл с документацией называется j2sdk<версия>-doc . zip, где вместо слова версия указан конкретный номер реализации, например 5_0.

3. Откройте окно для ввода команд.

4. Перейдите в каталогjrffc.

5. Выполните следующую команду:

jar xvf j2sdk<версия>-doc. zip Здесь, как и ранее, версия означает номер реализации.

Установка примеров программ

В процессе работы вам, вероятно, понадобятся примеры программ, приведенные в книге. Вы можете найти их на Web-странице http://link.linexp.ru/6011992/https://www.phptr.com/corejava. Программы хранятся в архиве corejava. zip, который нужно распаковать в отдельный каталог, — мы рекомендуем назвать его CoreJavaBook. Для распаковки можно применять любую утилиту, позволяющую работать с zip-файлами, например программу WinZip (которую можно найти по адресу http://link.linexp.ru/6011992/https://www.winzip.com), либо использовать утилиту jаг, входящую в состав JDK. Если вы решили использовать программу jаг, выполните следующие действия.

1. Убедитесь, что пакет JDK установлен правильно, а каталог jdk/Ып находится в списке путей к выполняемым файлам.

2. Создайте каталог CoreJavaBook.

3. Скопируйте файл core j ava. zip в этот каталог.

4. Откройте окно для ввода команд.

5. Перейдите в каталог CoreJavaBook.

6. Выполните следующую команду:

jar xvf corejava.zip

Навигация по каталогам

Изучая язык Java, вы, вероятно, захотите просмотреть исходные тексты программ. Кроме того, вам, без сомнения, придется интенсивно работать с документацией. В табл. 2.1 показано дерево каталогов, относящихся к языку Java. Схема этих каталогов может измениться, если вы работаете в интегрированной среде разработки программ, а имя корневого каталога зависит от номера инсталлированной версии.

Таблица 2.1. Дерево каталогов JDK

jdk (Имя может быть другим, например jdk5 .0)


Структура каталогов Описание
bin Компилятор и другие инструментальные средства
demo Демонстрационные программы
docs Библиотечная документация в формате HTML (после распаковки архива j2sdk<версия>-doc.zip)
include Файлы для создания платформенно-ориентированных методов (см. том 2)
jre Файлы для организации среды выполнения Java-программ
lib Библиотеки
src Исходные тексты библиотек (после распаковки файла src.jar)

Для начинающих разработчиков, только приступающих к изучению языка Java, наиболее важными среди каталогов, приведенных в табл. 2.1, являются docs и src. Каталог docs содержит документацию о библиотеках Java в формате HTML. Их мож­но просматривать с помощью Web-браузера, например Netscape Navigator.

Document-check.png Установите в вашем браузере закладку на локальную версию файла does/api/index.html. При изучении языка Java вам придется не раз обращаться к этой странице.

Каталог src содержит исходные тексты программ, находящихся в общедоступных библиотеках Java. Освоившись с языком, вы можете оказаться в ситуации, когда ни ин­формация из глобальной сети, нй эта книга не помогут вам решить вашу проблему. В этом случае воспользуйтесь исходными кодами. По исходным текстам вы всегда сможете по­нять, что именно делает та или иная библиотечная функция. Например, если вы интере­суетесь, как работает класс System, загляните в файл src/java/lang/System.java.

Выбор среды разработки программ

Если раньше вы программировали на языке Visual Basic или Visual С++, значит, вы уже знакомы со средой разработки, содержащей встроенный текстовый редактор, меню для компиляции и запуска программ, а также отладчик. Пакет JDK не имеет ни­каких средств, даже отдаленно напоминающих интегрированную среду разработки программ. Все команды выполняются в командной строке. Мы рассказываем, как ин­сталлировать и использовать пакет JDK, поскольку считаем, что развитая среда раз­работки далеко не обязательно облегчает процесс изучения языка; она может скры­вать некоторые важные детали, представляющие интерес для программиста.

Интегрированные среды разработки программ не совсем удобны для написания простых программ, поскольку они работают медленно, потребляют большой объем ресурсов и, работая с ними, приходится устанавливать переменные окружения для каждой вновь создаваемой программы. Интегрированные среды полезны, если вы разрабатываете большой проект, состоящий из многих файлов. Кроме того, в состав этих сред входит отладчик, крайне необходимый при разработке серьезных программ (отладчик, работающий в режиме командной строки, поставляемый в составе JDK, крайне неудобно использовать). Мы покажем, как работать с программой Eclipse, представляющей собой свободно распространяемую среду разработки, которая сама написана на языке Java. Разумеется, если у вас уже есть среда разработки программ, например NetBeans или JBuilder, поддерживающая текущую версию языка Java, вы можете смело использовать ее при рассмотрении примеров из этой книги.
Для простых программ удобно использовать нечто среднее между работой в ре­жиме командной строки и интегрированной средой разработки, а именно: текстовый редактор, интегрированный с пакетом JDK. На платформе Linux для этой цели мож­но использовать текстовый редактор Emacs. Под управлением операционной системы Windows удобна программа TextPad — прекрасный, свободно распространяемый редактор программ для среды Windows, хорошо интегрируемый со средствами Java. Тем же, кого интересуют кроссплатформенные инструменты, можно посоветовать JEdit. Используя текстовый редактор, интегрированный с JDK, можно легко и быстро создавать программы на языке Java. Большинство программ, приведенных в этой кни­ге, разработано именно так. Поскольку компилировать и выполнять программы можно, не выходя из текстового редактора, то он фактически формирует простую интег­рированную среду.

Итак, у вас есть три варианта дальнейшей работы.

  • Использовать набор инструментальных средств JDK вместе со своим любимым текстовым редактором. Компиляция и запуск программы на выполнение в этом случае производится из командной строки.
  • Использовать интегрированную среду разработки программ, например Eclipse, либо другой свободно распространяемый или коммерческий продукт.
  • Использовать JDK и текстовый редактор, интегрированный вместе с этим пакетом. Такую возможность предоставляют Emacs, TextPad, JEdit и многие другие подобные приложения. В этом случае компиляция и запуск на выполне­ние программ производятся внутри редактора.

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

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

Откройте окно для ввода команд и перейдите в каталог CoreJavaBook/vlch2 / Welcome. (Если вы помните, каталог CoreJavaBook был специально создан для хра­нения кодов примеров из данной книги.)

Введите следующие команды:

javac Welcome.java java Welcome

На экране должно появиться сообщение, показанное на рис.2.1.

Файл:Рис.1.2.png

Рис. 2.1. Компиляция и выполнение программы Welcome .java

Document-check.png В системе Windows режим командной строки реализуется в окне DOS. Для того чтобы открыть это окно, надо выбрать в главном меню системы пункт Run. Если вы работае­те с Windows NT/2000/XP, введите cmd; в противном случае вам надо задать command. Нажмите клавишу <Enter>, и требуемое окно появится на экране. Если вы никогда не работали в таком режиме, мы рекомендовали бы вам обратиться к специальному руководству. Подобные руководства публикуют в Web факультеты компьютерных наук многих учебных заведений. В частности, вы можете воспользоваться документом http://link.linexp.ru/6011992/https://www.es.sjsu.edu/faculty/horstman/CS46A/windows/tutorial.html.

Примите наши поздравления! Вы только что в первый раз скомпилировали и выполнили программу на языке Java.

Что же произошло? Программа javaс — это компилятор языка Java. Она скомпилировала файл Welcome, java и преобразовала его в файл Welcome.class. Программа java — это интерпретатор языка Java. Он интерпретирует байтовые коды, которые компилятор записал в файл с расширением .class.

Document-check.png Если ВЬ| получили сообщение об ошибке, указывающее на приведенную ниже строку, то, вероятнее всего, используете старую версию компилятора Java. В JDK 5.0 язык был дополнен новыми возможностями, и мы воспользуемся их преимуществами.
for (String g : greeting)
Если вы все же хотите продолжать работу со старой версией Java, вам надо перепи­сать цикл следующим образом:
for (int i = 0; i < greeting.length; System.out.printIn(greeting[i]);
В данной книге мы всегда будем использовать средства из JDK 5.0. Их можно без труда преобразовать в выражения для более старых версий. Необходимая для этого информация приведена в приложении Б.

Программа Welcome чрезвычайно проста и лишь выводит сообщение на экран. Текст этой программы представлен в листинге 2.1. Подробнее мы рассмотрим эту программу в следующей главе.

Листинг 2.1. Содержимое файла welcome. Java

 
/**
@version 1.11 2000-04-22
@author Cay Horstmann
*/

 
public class Welcome
{
public static void main(String[] args)
{
String[] greeting = new String[3];
greeting[0] = "Welcome to Core Java";
greeting[1] = "by Cay Horstmann";
greeting[2] = "and Gary Cornell";
 
for (int i = 0; i < greeting.length; i++)
System.out.println(greeting[i]);
}
}

Возможные ошибки

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

  • Если вы вводите программу вручную, внимательно следите за употреблением прописных и строчных букв. В частности, в рассмотренном выше примере имя класса — Welcome, а не welcome или WELCOME.
  • Компилятор требует указывать имя файла, в данном случае Welcome .java. Интерпретатору требуется имя класса, в данном случае это Welcome, без расширения .javaили .class.
  • Если вы получили сообщение "Bad command or file name" или "javac: command not found", проверьте, правильно ли выполнена инсталляция, в частности, верно ли указаны пути к выполняемым файлам.
  • Если компилятор javac выдал сообщение "cannot read: Welcome. java", нужно проверить, имеется ли файл в соответствующем каталоге.

При работе под управлением операционной системы Unix проверьте, правильно ли использованы прописные буквы в имени файла Welcome .java. При работе в среде Windows просматривайте содержимое каталогов с помощью команды dir. Некоторые текстовые редакторы (в частности, Notepad) сохраняют текст в файлах с расширением . txt. Если вы используете эту программу для редактирования файла Welcome. j ava, она сохранит его под именем Welcome, java.txt. По умолчанию в среде Windows программа Explorer скрывает расширение - txt, поскольку оно предполагается по умолчанию. В этом случае нужно переименовать файл, пользуясь командой г en, либо повторно сохранить его, указав имя в кавычках, например "Welcome .java".

  • Если компилятор java выдал сообщение об ошибке java.lang'. NoClassDef FoundError, проверьте, правильно ли вы указали имя файла.
  • Если компилятору не подходит имя welcome (начинающееся со строчной буквы), повторно выполните команду java Welcome, написав имя с прописной буквы W. В языке Java учитывается регистр символов.
  • Если интерпретатор выдает сообщение об ошибке, в котором фигурирует имя Welcome/ java, значит, вы случайно ввели команду java Welcome. java. Задайте java Welcome.
  • Если вы задали имя Welcome, а виртуальная машина не может найти класс с этим именем, значит, у вас неверно установлены пути к классам. Для простых программ лучше всего отменить установки переменной окружения CLASSPATH. Сделать это можно с помощью приведенной ниже команды.

set CLASSPATH=

Данная программа работает в Windows, а также в Unix/Linux с оболочкой С. Если в Unix/Linux применяется оболочка Bourne/bash, надо использовать следующее выражение:

export CLASSPATH=

Подробно об установке путей к классам речь пойдет в главе 4.

  • Если сообщение об ошибках касается новых языковых конструкций, это значит, что ваш компилятор не поддерживает JDK 5.0. Если вы по каким-то причинам не можете использовать JDK 5.0 или более поздние версии, следует изменить исходный код. Необходимая для этого информация приведена в приложении Б.
  • Если в вашей программе слишком много ошибок, то все сообщения о них мелькают на экране очень быстро. Компилятор j ava выводит подобные сообщения в стандартный поток ошибок, и если информация занимает больше одного экрана, то ее желательно записать в файл.

При работе на платформе Unix или Windows NT/2000/XP это не представляет большой сложности. Для. этого можно использовать конструкцию, подобную приведенной ниже.

javac MyProg.java 2> errors.txt

При работе под управлением операционной системы Windows 95, находясь в режиме командной строки, невозможно перенаправить стандартный поток ошибок. В этом случае вы можете загрузить программу err out с Web-страницы https: //www. horstmaim. с от/с ore java/faq.html и выполнить команду errout j avac Myprog.j ava > errors.txt

Document-check.png Превосходное учебное пособие приведено на Web-странице http://link.linexp.ru/6011992/https://java.sun.com.docs/books/tutorial/getStarted/cupojava В нем подробно описываются проблемы, с которыми сталкиваются начинающие программисты.

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

В этом разделе мы покажем, как скомпилировать программу в интегрированной среде Eclipse. Данный продукт распространяется свободно; вы можете скопировать его с Web-страницы http://link.linexp.ru/6011992/https://eclipse.org. Программа Eclipse написана на языке Java, но вследствие применения нестандартных библиотек для работы с окнами переносимость ее ограничена. Тем не менее существуют версии Eclipse для операционных систем Linux, Mac OS X, Soilaris и Windows.

После запуска Eclipse активизируйте пункт меню File^New Project и выберите Java Project в диалоговом окне инструмента типа "мастер" (рис. 2.2). На данном рисунке показан продукт Eclipse 3.0М8. В вашей версии Eclipse окно может несколько отличаться.

Щелкните на кнопке Next Укажите имя проекта Welcome и введите полный путь к каталогу, содержащему файл Welcome.java (рис. 2.3). Отмените опцию Create project in workspace и щелкните на кнопке Finish.

В результате выполненных действий вы создали новый проект. Щелкните на треугольном маркере, соответствующем окну проекта, а затем на маркере рядом с пунктом Default package. Дважды щелкните на Welcome.java. На экране отобразится окно с кодом программы, показанное на рис. 2.4.

Щелкните правой кнопкой мыши в левой панели на имени проекта (Welcome). В появившемся контекстном меню выберите пункт Build Project. В результате ваша программа будет скомпилирована. Если при компиляции не возникло ошибок, выберите пункт меню Run -- Run As -- Java Application. В нижней части окна появится панель для отображения выходных данных, на которой будут выведены результаты выполнения программы (рис. 2.5).

Файл:Рис.2.2.png

Рис. 2.2. Диалоговое окно Eclipse, предназначенное для создания проекта

Файл:Рис.2.3.png

Рис. 2.3. Настройка проекта Eclipse

Файл:Рис.2.4.png

Рис. 2.4. Редактирование исходного кода при работе с Eclipse

Файл:Рис.2.5.png

Рис. 2.5. Выполнение программы в среде Eclipse

Сообщения об ошибках компиляции

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

public static void main(string[] args)

Снова попытайтесь скомпилировать программу. Вы получите сообщение об ошиб­ке, которое информирует вас о том, что вы использовали неизвестный тип string (рис. 2.6). Щелкните на сообщении. Курсор перейдет на соответствующую строку в окне редактирования, и вы сможете быстро исправить ошибку.

Файл:Рис.2.6.png

Рис. 2.6. Сообщение об ошибке, отображаемое в среде Eclipse

Таким образом, на простом примере вы получили представление о работе в среде Eclipse. Отладчик Eclipse будет рассмотрен в главе 11.

Комииляция и запуск программ из текстового редактора

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

Emacs — это превосходный текстовый редактор, способный работать с Unix, Linux, Windows и Mac OS X. Однако многие программисты, использующие Windows, считают, что изучать его довольно сложно. Таким программистам мы можем порекомендовать текстовый редактор TextPad. В отличие от Emacs, TextPad соответствует стандартам системы Windows. Информацию об этом редакторе и саму программу можно найти на Web-странице http://link.linexp.ru/6011992/https://www.textpad.com. Заметим, что редактор TextPad относится к условно бесплатным программам. По истечении оговоренного срока вам придется заплатить за него. (Будучи пользователями этого продукта, мы не имеем никакого отношения к его производителям.)

Помимо упомянутых выше редакторов, мы можем предложить jEdit — свободно распространяемую программу, написанную на Java. Этот редактор можно скопировать, обратившись по адресу https: / /jedit.org. Независимо от того, выберете ли вы Emacs, TextPad, JEdit или другой редактор, основные принципы работы остаются неизменными. Закончив редактировать исходный код, можно вызывать компилятор. Редактор корректно запустит программу компиляции и предоставит вам сообщения об ошибках. Устранив ошибки, повторите компиляцию, после чего вы сможете запустить программу на выполнение.

На рис. 2.7 показан принцип компиляции Java-программы с помощью редактора Emacs. Для запуска компилятора надо выбрать пункт меню JDE - Compile.

Файл:Рис.2.7.png

Рис. 2.7. Вызов компилятора из редактора Emacs

Document-check.png Текстовый редактор Emacs, созданный в рамках проекта GNU, можно загрузить с Web-страницы http://link.linexp.ru/6011992/https://www.gnu.org/software/emacs/. Версию этого редактора для операционной системы Windows можно найти по адресу http://link.linexp.ru/6011992/https://www.gnu.org/software/emacs/windows/ntemacs.html. Используя текстовый редактор Emacs, убедитесь, что на вашем компьютере инсталлирован продукт JDEE. Его можно скопи­ровать с Web-страницы http://link.linexp.ru/6011992/https://jdee.sunsite.dk. Для работы со средствами JDK 5.0 вам понадобится JDEE2.4.3beta1.

Сообщения об ошибках отображаются в нижней части окна. Если вы поместите курсор на сообщение и нажмете клавишу <Enter>, курсор переместится на соответствующую строку исходного кода.

После устранения ошибок вы можете запустить программу, выбрав пункт меню JDE^Run Арр. Данные, генерируемые в процессе выполнения программы, отображаются в окне редактора (рис. 2.8).

Файл:Рис.2.8.png

Рис. 2.8. Запуск программы из редактора Emacs

Выполнение графического приложения

Программа Welcome не поражает воображение. Перейдем теперь к графическому приложению. Это приложение представляет собой очень простую программу, которая выводит на экран изображение из файла. Как и прежде, скомпилируем и выполним это приложение в режиме командной строки.

1. Откройте окно для ввода команд.

2. Перейдите в катадог CoreJavaBock/vlch2 / ImageViewer.

3. Введите следующие команды:

j avac ImageVi ewer.j ava java ImageViewer

На экране появится новое окно приложения ImageViewer (рис. 2.9).

Выберите пункт меню FileOOpen и найдите файл с расширением . gif, который вы хотите открыть. (Мы поместили в каталог, в котором находится программа, несколько графических файлов.)

Файл:Рис.2.9.png

Рис. 2.9. Окно выполняющегося приложения ImageViewer

Чтобы завершить выполнение программы, щелкните на кнопке Close в строке заголовка или откройте системное меню и закройте программу. (Чтобы скомпилировать и выполнить эту программу в текстовом редакторе или интегрированной среде, нужно сделать то же, что и прежде. Например, при работе с текстовым редактором Emacs выберите пункт меню JDE - Compile, а затем JDE - Run Арр.)

Мы надеемся, что эта программа будет для вас интересной и полезной. Рассмотрим ее исходный текст. Эта программа существенно длиннее, чем первая, однако она не слишком сложна, особенно если представить себе, сколько строк кода на языке С или С++ нужно было бы написать, чтобы достичь того же эффекта. Конечно, на языке Visual Basic такую программу написать совсем легко, для этого достаточно перетащить несколько компонентов с помощью мыши и добавить несколько строк, чтобы код стал функциональным. В пакет JDK не входит инструмент для создания интерфейса, поэтому для всех элементов нужно писать соответствующий код, как показано в листинге 2.2. Написанию подобных программ посвящены главы 7-9.

Листинг 2.2. Содержимое файла ImageViewer .java

/**
@version 1.21 2002-06-19
@author Cay Horstmann
*/

 
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
 
/**
A program for viewing images.
*/

public class ImageViewer
{
public static void main(String[] args)
{
JFrame frame = new ImageViewerFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
}
 
/**
A frame with a label to show an image.
*/

class ImageViewerFrame extends JFrame
{
public ImageViewerFrame()
{
setTitle("ImageViewer");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 
// use a label to display the images
label = new JLabel();
Container contentPane = getContentPane();
contentPane.add(label);
 
// set up the file chooser
chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
 
// set up the menu bar
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
 
JMenu menu = new JMenu("File");
menuBar.add(menu);
 
JMenuItem openItem = new JMenuItem("Open");
menu.add(openItem);
openItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent evt)
{
// show file chooser dialog
int r = chooser.showOpenDialog(null);
 
// if file selected, set it as icon of the label
if(r == JFileChooser.APPROVE_OPTION)
{
String name
= chooser.getSelectedFile().getPath();
label.setIcon(new ImageIcon(name));
}
}
});
 
JMenuItem exitItem = new JMenuItem("Exit");
menu.add(exitItem);
exitItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.exit(0);
}
});
}
 
private JLabel label;
private JFileChooser chooser;
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 400;
}

Создание и запуск аплетов

Первые два примера, представленные в этой книге, — это приложения, написанные на языке Java, т.е независимые программы, аналогичные обычным платформенно-ориентированным продуктам. С другой стороны, как упоминалось в предыдущей гла­ве, всеобщий интерес к языку Java был вызван в основном его возможностями выпол­нять аплеты под управлением Web-браузеров. В данном разделе мы рассмотрим созда­ние и выполнение аплета в режиме командной строки. Затем мы загрузим этот аплет в специальную программу просмотра (appletviewer), входящую в состав пакета JDK. В завершение мы отобразим его в окне Web-браузера.
Перейдите в каталог CoreJavaBook/vlch2/WelcomeApplet, а затем введите следующие команды:

javac WelcomeApplet.java appletviewer WelcomeApplet.html

Внешний вид окна программы для просмотра аплетов показан на рис. 2.10.

Первая команда нам уже знакома — она вызывает компилятор языка Java. В процессе компиляции исходный текст аплета, содержащийся в файле WelcomeApplet.java, преобразуется в байтовый код, который помещается в файл WelcomeApplet. class.

Однако на этот раз мы не запускаем программу java. Вместо этого мы пользуемся appletviewer. Эта программа специально предназначена для быстрого тестирования аплетов и входит в пакет JDK. Исходными данными для appletviewer является файл в формате HTML. Содержимое файла WelcomeApplet. html показано в листинге 2.3.

Файл:Рис.2.10.png

Рис. 2.10. Аплет WelcomeApplet, отображающийся в окне appletviewer

Листинг 2.3. Содержимое файла WelcomeApplet .html

<html>
<title>WelcomeApplet</title>
<body>
<hr>
<p>
This applet is from the book
<a href="http://link.linexp.ru/6011992/https://www.horstmann.com/corejava.html">
Core Java</a> by <em>Cay Horstmann</em> and
<em>Gary Cornell</em>,published by Sun Microsystems Press.
</p>
<applet code="WelcomeApplet.class" width="400" height="200">
<param name="greeting" value ="Welcome to Core Java!"/>
</applet>
<hr>
<p><a href="WelcomeApplet.java">The source.</a></p>
</body>
</html>

Если вы знаете язык HTML, то заметите некоторые стандартные конструкции и дескриптор <applet>, который указывает программе просмотра, что необходимо загрузить аплет, код которого содержится в файле WelcomeApplet .class. Программа appletviewer игнорирует все выражения HTML, за исключением дескриптора <applet>.

Другие дескрипторы языка HTML обрабатываются в том случае, если вы просматриваете документ с помощью браузера, поддерживающего Java 2. Однако при работе с браузерами не исключены проблемы.

• Mozilla (а также Netscape 6 и более поздние версии) поддерживают Java 2 в системах Windows, Linux и Mac OS X. Если вы хотите поэкспериментировать с ап-летами, вам надо выбрать последнюю версию браузера и убедиться, что она поддерживает Java.

• Некоторые версии браузера Internet Explorer вообще не поддерживают язык Java. Другие версии поддерживают лишь очень старую виртуальную машину Java компании Microsoft. Если вы работаете с браузером Internet Explorer под управлением операционной системы Windows, обратитесь по адресу http://link.linexp.ru/6011992/https://java.com и установите Java Plug-In.

• На платформе Macintosh под управлением OS X возможности языка Java уже интегрированы в браузер Internet Explorer. В частности, на момент написания книги этот браузер обеспечивает возможности J2SE версии 1.4. Система OS 9 поддерживает лишь устаревшую версию 1.1.

Убедившись, что ваш браузер поддерживает Java 2, попробуйте загрузить в него аплет. Для этого выполните следующее.

1. Запустите браузер.

2. Выберите пункт меню File^Open File (или откройте файл другим способом).

3. Перейдите в каталог CoreJavaBook/vlch2 /WelcomeApplet.

В диалоговом окне выберите файл WelcomeApplet .html. Браузер загрузит аплет, а также текст документа. Web-страница будет выглядеть приблизительно так, как показано на рис. 2.11.

Нетрудно заметить, что это приложение действительно интерактивно и взаимодействует с Internet. Щелкните на кнопке Cay Horstmann, и на экране отобразится страница Кея Хорстманна. Щелкните на кнопке Gary Cornell. Аплет откроет окно для ввода электронной почты, в котором уже указан адрес Гари Корнелла.

Файл:Рис.2.11.png

Рис. 2.11. Просмотр аплета WelcomeApplet с помощью браузера

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

Document-check.png Аплет можно запустить из текстового редактора или интегрированной среды разра­ботки. При работе с редактором Emacs выберите пункт меню JDE-Run Applet. Рабо­тая с Eclipse, выберите пункт Run-Run as-Java Applet.

Код аплета Welcome показан в листинге 2.4. В данный момент его нужно лишь бегло просмотреть. К созданию аплетов мы еще вернемся в главе 10.

Итак, мы рассмотрели механизмы компиляции и запуска программ, написанных на языке Java. Теперь мы готовы перейти к главе 3, в которой приступим к изучению языка Java.

Листинг 2.4. Содержимое файла WelcomeApplet.java

/**
@version 1.21 2002-06-19
@author Cay Horstmann
*/

 
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
 
public class WelcomeApplet extends JApplet
{
public void init()
{
Container contentPane = getContentPane();
contentPane.setLayout(new BorderLayout());
 
JLabel label = new JLabel(getParameter("greeting"),
SwingConstants.CENTER);
label.setFont(new Font("Serif", Font.BOLD, 18));
contentPane.add(label, BorderLayout.CENTER);
 
JPanel panel = new JPanel();
 
JButton cayButton = new JButton("Cay Horstmann");
cayButton.addActionListener(makeURLActionListener(
"http://link.linexp.ru/6011992/https://www.horstmann.com"));
panel.add(cayButton);
 
JButton garyButton = new JButton("Gary Cornell");
garyButton.addActionListener(makeURLActionListener(
"mailto:gary@thecornells.com"));
panel.add(garyButton);
 
contentPane.add(panel, BorderLayout.SOUTH);
}
 
private ActionListener makeURLActionListener(final String u)
{
return new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
try
{
getAppletContext().showDocument(new URL(u));
}
catch(MalformedURLException e)
{
e.printStackTrace();
}
}
};
}
}

]]> Книги по Java https://linexp.ru?id=4719 Wed, 29 Jun 2022 13:56:25 GMT <![CDATA[Core Java 2 Том I Глава 1]]>

Оглавление


Введение в язык Java

В этой главе...
  • Программная платформа Java
  • Характерные особенности языка Java
  • Java и Internet
  • История языка Java
  • Распространенные заблуждения относительно языка Java

 На появление первой версии Java в 1996 году откликнулись не только специа­лизированные компьютерные газеты и журналы, но даже такие издания, как The New York Times, The Washington Post и Business Week. Java — единственный язык про­граммирования, удостоившийся десятиминутного репортажа на National Public Radio. Это было удивительное время. Тем дням и последующей истории языка Java посвящена данная глава.

Программная платформа Java

В первой редакции этой книги говорилось следующее.

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

 По поводу этого абзаца наш редактор долго спорил с одним из руководителей ком­пании Sun. Но и сейчас, по прошествии долгого времени, такая оценка кажется нам правильной. Действительно, Java обладает целым рядом преимуществ, о которых мы поговорим чуть позже. Однако более поздние дополнения далеко не так элегантны, как исходный вариант языка, и виной тому пресловутые требования совместимости.

 Как уже говорилось в первой редакции, Java — не только язык. Хорошие языки — не редкость; появление некоторых из них вызвало в своё время сенсацию в компью­терном мире. В отличие от них, Java — это программная платформа, включающая мощную библиотеку, большой объем кода, вполне пригодного для повторного использования, и среду для выполнения программ, которая обеспечивает безопас­ность, независимость от операционной системы и автоматическую "сборку мусора".

 Программистам нужны языки с четкими синтаксическими правилами и понятной семантикой. Такому требованию, помимо Java, отвечают десятки языков. Некоторые из них даже обеспечивают переносимость и "сборку мусора", однако их библиотеки оставляют желать лучшего. В результате программисты вынуждены самостоятельно реализовывать графические операции, действия с базой данных и другие часто встречающиеся процедуры. Java объединяет в себе прекрасный язык, высококачест­венную среду выполнения программ и обширную библиотеку. В результате многие программисты остановили свой выбор именно на Java.

Характерные особенности языка Java

 Авторы Java написали руководство, в котором объяснялись цели и достоинства но­вого языка. В этом документе приведено одиннадцать характерных особенностей Java. Этот язык:

  • простой;
  • переносимый;
  • объектно-ориентированный;
  • интерпретируемый;
  • распределенный;
  • высокопроизводительный;
  • надежный;
  • многопотоковый;
  • безопасный;
  • динамичный,
  • не зависит от архитектуры компьютера;

 В данном разделе мы

  • приведем цитаты из руководства по языку Java, раскрывающие особенности языка;
  • поделимся с читателями соображениями по поводу отдельных свойств языка, основываясь на собственном опыте работы с его последней версией.

 Руководство, о котором идет речь, можно найти по адресу https://java.sun.com/docs/white/langenv/. Характерные особенности Java описаны в документе, который находится по адресу ftp://ftp.javasoft.com/docs/papers/java-overview.ps.

Простота

"Мы хотели создать систему, которая позволяла бы легко писать программы, не требо­вала дополнительного обучения и учитывала сложившуюся практику и стандарты про граммирования. Мы считали С++ не совсем подходящим для этих целей, однако, чтобы сделать систему более доступной, язык Java был разработан максимально похожим на не­го. Исключили мы лишь редко используемые, малопоштньїе и невразумительные средства С++, которые, по нашему мнению, приносят больше вреда, чем пользы ".

 Синтаксис языка Java, по существу, представляет собой упрощенный вариант син­таксиса языка С++. В этом языке нет файлов заголовков, указателей структур, объеди­нений, перегрузки операторов, виртуальных базовых классов и т.п. (Различия между Java и С++ упоминаются на протяжении всей книги.) Однако разработчики не стреми­лись исправить все недостатки языка С++. Например, синтаксис оператора switch в языке Java остался неизменным. Зная С++, несложно перейти к языку Java.

 Если вы привыкли к визуальной среде программирования (например, Visual Basic), язык Java может показаться сложным. Его синтаксис некоторые считают странным (хотя понять смысл выражения не составляет труда). И что самое главное, при работе на языке Java приходится намного больше программировать. Начинающие программи­сты нередко предпочитают Visual Basic, так как его визуальная среда программирования позволяет почти автоматически создавать инфраструктуру приложения. Чтобы достичь того же результата с помощью языка Java, необходимо вручную написать достаточно большой объем кода, но программы, получающиеся при этом, более компактны.

"Другой аспект простоты - краткость. Одна из целей языка Java - обеспечить раз­работку независимых программ, способных выполняться на машинах с ограничен­ным объемом ресурсов. Размер основного интерпретатора и средств поддержки клас­сов составляет окало 40 Кбайт; стандартные библиотеки и средства поддержки потоков, в том числе автономное микроядро (self-contained microkernel), занимают еще 175 Кбайт".

 Это огромный успех. Заметим, однако, что библиотеки, предназначенные для поддержки графического пользовательского интерфейса, имеют значительно боль­ший объем.

Поддержка объектов

"По сути, объектно-ориентированное программирование - это способ разработки, в центре внимания которого находятся данные (т.е. объекты) и средства доступа к ним. Проводя аналогию со столярным делом, можно сказать, что "объектно-ориентированный "мастер в основном сосредоточен на стуле, который он изготавли­вает, и лишь во вторую очередь его интересуют инструменты, необходимые для это­го; в то же время "не объектно-ориентированный " столяр думает лишь о своих инст­рументах. Средства языков Java и С++, предназначенные для работы с объектами, по существу совпадают".

 За прошедшие 30 лет объектный подход уже доказал свое право на жизнь, и без не­го невозможно представить себе современный язык программирования. Действи­тельно, особенности Java, связанные с объектами, сравнимы с языком С++. Основное различие между ними заключается в механизме множественного наследования, кото­рый в языке Java заменен механизмом интерфейсов. Механизмы отражения (см. гла­ву 5) и сериализации (см. главу 12) позволяют реализовать постоянные (persistent) объекты и средства для создания графически^ пользовательских интерфейсов на ос­нове готовых компонентов.

Document-check.png Если вы никогда не использовали объектно-ориентированные языки, внимательно изучите главы 4-6. В них излагаются основы объектного подхода и демонстрируются его преимущества над такими традиционными, процедурно-ориентированными язы­ками, как С или Вasic, проявляющиеся при разработке сложных проектов.

Поддержка распределенных вычислений

"Язык Java предоставляет разработчику обширную библиотеку программ дм передачи данных на базе протоколов TCP/IP (Transmission Control Protocol/Internet Protocol -протокол, управления передачей/Internet-протокол), HTTP (Hypertext Transfer Protocol -протокол передачи гипертекстовой информации) и FTP (File Transfer Protocol - прото­кол передачи файлов). Приложения, написанные на языке Java, могут открывать объ­екты и получать к ним доступ по сети; при этом для адресации используются URL (Uniform Resource Location - унифицированный локатор ресурсов) ".

 Язык Java предоставляет мощные и удобные средства для работы в сети. Каждый, кто когда-либо пытался писать программы для работы в глобальной сети, будет при­ятно удивлен тем, как легко решаются на Java самые трудные задачи, например от­крытие гнезд (socket). (Работа в сети будет описана во втором томе данной книги.) Связь между распределенными объектами в языке Java обеспечивается механизмом вызова удаленных методов (эта тема также затрагивается во втором томе).

 В настоящее время сложные распределенные приложения обычно реализуются на базе Java 2 Enterprise Edition (J2EE).

Надежность

"Язык Java предназначен для написания программ, которые должны надежно рабо­тать в любых ситуациях. Основное внимание в данном языке уделяется раннему об­наружению возможных ошибок контролю в процессе выполнения программы, а так­же устранению ситуаций, которые могут вызвать ошибки... Единственное сущест­венное отличие языка Java от языка С++ заключается в модели указателей, принятой в Java, которая исключает возможность записи в произвольно выбранную область памяти и повреждения данных".


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

 Если раньше вы программировали на Visual Basic либо на другом языке, в котором указатели явным образом не используются, возможно, вам будет непонятно, почему этот вопрос настолько важен. Программистам, использующим язык С, нужны указатели для доступа к строкам, массивам, объектам и даже файлам. При программировании на Visual Basic ничего этого не требуется, и разработчик может не беспокоиться о распределении памяти для хранения объектов. С другой стороны, многие структуры данных в языке, не имеющем указателей, реализовать очень трудно. Для простых конструкций, вроде строк и массивов, указатели не нужны. Возможности указателей в полной мере проявляются лишь там, где без них нельзя обойтись, например, при создании связных списков. Про­граммист на языке Java навсегда избавлен от неверных указателей, опасности непра­вильного распределения памяти и ее некорректного использования.

Безопасность

"Язык Java предназначен для использования в сетевой или распределенной среде. По этой причине большое внимание было уделено безопасности. Java позволяет созда­вать системы, защищенные от вирусов и несанкционированного доступа."


 В первом издании мы написали: "Никогда не говори никогда", — и оказались правы. Вскоре после выхода первой версии Java Development Kit группа экспертов по вопросам безопасности из Принстонского университета обнаружила первые ошибки в системе за­щиты Java 1.0. Компания Sun Microsystems развернула исследования в области безопас­ности Java-программ. В частности, она обнародовала спецификацию и код виртуаль­ной машины и библиотек, ответственных за защиту. Это ускорило выявление и устра­нение ошибок. Следует заметить, что количество таких ошибок было относительно невелико и чтобы воспользоваться ими, злоумышленник должен был бы обладать доволь­но глубокими знаниями.

 Перечислим некоторые ситуации, возникновение которых предотвращает систе­ма безопасности языка Java.

  • Намеренное переполнение стека выполняемой программы — один из распро­страненных способов атаки.
  • Повреждение участков памяти, находящихся за пределами пространства, выде­ленного процессу.
  • Несанкционированное чтение файлов и их модификация.

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

Document-check.png Альтернативный механизм доставки кода, предложенный компанией Microsoft, опира­ется на технологию ActiveX и для безопасности использует только цифровые подписи. Очевидно, что этого недостаточно — любой пользователь программного обеспечения компании Microsoft может подтвердить, что даже программы широко известных произ­водителей часто завершаются аварийно, создавая тем самым опасность повреждения данных. Система безопасности в языке Java намного надежнее, чем технология ActiveX, поскольку она контролирует приложение в процессе его работы.

Независимость от архитектуры компьютера


"Компилятор генерирует объектный файл, формат которого не зависит от архитек­туры компьютера. Скомпилированная программа может выполняться на любых про­цессорах; для ее работы необходима лишь исполняющая система Java. Код, генерируемый компилятором Java, называется байтовым кодом. Он разработан таким образом, , чтобы на любой машине его можно было легко интерпретировать либо в процессе рабо­ты перевести в набор команд, ориентированных на конкретный процессор ".

Эта идея не нова. Более двадцати лет назад она была предложена Никлаусом Виртом (Niclaus Wirth) для языка Pascal; эта же технология была реализована в системе UCSD Pascal.
Очевидно, что код, интерпретируемый с помощью виртуальной машины, всегда будет работать медленнее, чем машинные инструкции, поэтому целесообразность та­кого подхода у многих вызывает сомнение. Однако эффективность байтового кода можно существенно повысить за счет компиляции, осуществляемой в процессе вы­полнения программы. Этот механизм доказал свою эффективность и даже использо­вался при создании платформы .NET.

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

Переносимость


"В отличие от языков С и С++, ни один из элементов спецификации Java не зависит от реализации. И размер основных типов данных, и арифметические операции над ними строго определены ".

 Например, тип int в языке Java всегда означает 32-разрядное целое число. В языках С и С++ тип int может означать как 16-разрядное, так и 32-разрядное це­лое число. Единственное ограничение заключается в том, что размер типа int не может быть меньше размера типа short int и больше размера типа long int. Фиксированный размер числовых типов позволяет избежать многих неприятно­стей, связанных с выполнением программ на разных компьютерах. Бинарные дан­ные хранятся и передаются в неизменном формате, что также позволяет избежать недоразумений, связанных с разным порядком следования байтов на разных плат­формах. Строки сохраняются в стандартном формате Unicode.


"Библиотеки, представляющие собой часть системы, предоставляют интерфейс, обеспечивающий переносимость программ. Например, в языке предусмотрен абст­рактный класс Window и его реализации для операционных систем Unix, Windows и Macintosh".

Каждый, кто когда-либо пытался написать программу, которая одинаково хорошо работала бы под управлением операционных систем Windows, Macintosh и десятка разновидностей системы Unix, знает, что это очень трудная задача. Разработчики версии Java 1.0 предприняли героическую попытку решить эту проблему, предоставив простой инструментальный набор, адаптирующий обычные элементы пользователь­ского интерфейса к различным платформам. К сожалению, библиотека, на которую было затрачено немало труда, не позволила достичь приемлемых результатов в раз­ных системах. (В результате для каждой системы в графических программах проявля­лись свои характерные ошибки.) Однако это было лишь началом. Во многих прило­жениях машинная независимость оказалась намного важнее изысканности графиче­ского пользовательского интерфейса. Именно эти приложения выиграли от появления версии Java 1.0. Однако теперь инструментальный набор для создания графического пользовательского интерфейса полностью переработан и больше не зависит от интерфейсных средств, используемых на конкретном компьютере. Новая версия более осмысленна и, по нашему мнению, более привлекательна для пользователя, чем предыдущие.

Использование интерпретатора

"Java-программа может выполняться на любой машине, для которой реализован ин­терпретатор. Поскольку редактирование связей - относительно легкий процесс, раз­работка программ может стать намного быстрее и эффективнее ".


Инкрементное связывание имеет свои преимущества, однако в данном случае оценка несомненно завышена. В любом случае средства разработки Java работают до­вольно медленно. Если вы, например, привыкли к скорости, которую обеспечивает среда Microsoft Visual С++, то, перейдя на Java, вы поначалу будете разочарованы. (Последняя версия Visual Studio существенно проигрывает по сравнению с классиче­ской средой разработки. Независимо от того, каким языком вы пользуетесь, вам явно придется обратиться к руководству с просьбой выделить более быстродействующий компьютер.)

Производительность

"Хотя обычно интерпретируемые байтовые коды имеют достаточную производи­тельность, бывают ситуации, когда требуется еще более высокая эффективность. Байтовые коды можно транслировать во время выполнения программы в машинные команды для процессора, на котором выполняется данное приложение".


Если для выполнения байтовых кодов применяется интерпретатор, не следует употреблять словосочетание "высокая производительность". Однако на многих плат­формах возможен специальный тип компиляции, обеспечиваемый так называемыми синхронными компиляторами (just-in-time Compilers — JIT). Они транслируют байто­вые коды в машинориентированные команды, сохраняют результат в памяти, а затем по мере необходимости вызывают его! Поскольку при этом интерпретация выполня­ется только один раз, данный подход во много раз увеличивает скорость работы. Не­смотря на то что синхронные компиляторы не обеспечивают такой производитель­ности, как компиляторы, ориентированные на конкретные типы процессоров, они работают намного быстрее интерпретаторов, обеспечивая для некоторых программ 10- и даже 20-кратное ускорение. Эта технология постоянно совершенствуется, и не исключено, что со временем она обеспечит скорость, характерную для кода, генери­руемого традиционными компиляторами. Например, синхронный компилятор может определить, какой фрагмент кода выполняется чаще, и оптимизировать его по скоро­сти выполнения.

Поддержка потоков

"Наличие нескольких потоков обеспечивает лучшую интерактивностъ и возмож­ность выполнения программ в реальном масштабе времени ".


Если вы когда-либо пытались организовать многопотоковые вычисления на каком-нибудь из языков программирования, вы будете приятно удивлены тем, как это легко сделать на Java. Потоки в языке Java могут использовать преимущества многопроцес­сорных систем, если операционная система позволяет это сделать. К сожалению, реали­зации потоков на большинстве систем сильно отличаются друг от друга, а разработчики языка Java не предпринимают должных усилий, чтобы достичь независимости от плат­формы и в этом вопросе. Только код для вызова потоков остается одинаковым для всех машин, а ответственность за их поддержку возлагается на операционную систему или библиотеку потоков. (Потоки описываются во втором томе.) Несмотря на это, именно простота организации многопотоковых вычислений делает язык Java таким привлека­тельным для разработки программного обеспечения серверов.

Динамичность

"Во многих отношениях язык Java является более динамичным, чем языки С и С++. Он был разработан так, чтобы легко адаптироваться к постоянно изменяющейся среде. В библиотеки можно свободно включать новые методы и объекты, никак не затрагивая этим приложения, пользующиеся библиотеками. Язык Java позволяет легко получать информацию о ходе выполнения программы ".



Document-check.png Это очень важно в тех случаях, когда требуется добавить код в уже выполняемую про­грамму. Ярким примером этого является код, который загружается из глобальной сети для выполнения браузером. В версии Java 1.0 получить информацию о работающей программе было непросто, однако последняя версия языка Java раскрывает перед программистом как структуру, так и поведение объектов. Это весьма ценно для систем, которые должны ана­лизировать объекты в ходе выполнения программы. К таким системам относятся средства создания графического пользовательского интерфейса, интеллектуальные отладчики, включаемые компоненты и базы данных.
Компания Microsoft выпустила продукт под названием J++, который очень похож на Java. Синтаксис языка практически совпадает с Java. Подобно Java, J++ использует для выполнения байтовых кодов виртуальную машину, однако интерфейс с внешним кодом существенно отличается. Кроме того, дополнительные языковые конструкции, реали­зованные Microsoft, вряд ли пригодны для чего-то, кроме взаимодействия с Windows API. Основные библиотеки Java и J++ (используемые для работы со строками, с сете­выми соединениями, для поддержки потоков, выполнения математических вычислений и т.д.) совершенно идентичны. В то же время библиотеки для работы с графикой, соз­дания пользовательских интерфейсов и доступа к удаленным объектам существенно отличаются друг от друга. В настоящее время Microsoft прекратила поддержку J++ и со­средоточила свои усилия на другом языке, который называется С# и несколько напо­минает Java, но использует другую виртуальную машину. Создан даже язык J#, предна­значенный для переноса приложений J++ на виртуальную машину, используемую С#. Языки J++, С# и J# в данной книге рассматриваться не будут.

Java и Internet

При работе по сети Java-программы используются просто — пользователи загружают байтовые коды языка Java по сети и выполняют их на своих машинах. Программы Java, ра­ботающие под управлением Web-браузеров, называются аплетами. Для использования аплета нужен Web-браузер, поддерживающий язык Java и способный интерпретировать бай­товые коды. Лицензия на исходные коды языка Java принадлежит компании Sun, настаи­вающей на неизменности как самого языка, так и структуры его основных библиотек. К сожалению, в реальности все не так. Разные версии браузеров Netscape и Internet Explorer поддерживают разные версии языка Java, причем некоторые из этих версий зна­чительно устарели. Эта неприятная ситуация создает все больше препятствий при разра­ботке аплетов, которые позволяли бы использовать преимущества последней версии язы­ка Java. Чтобы решить данную проблему, компания Sun разработала программу Java Plug-in, позволяющую формировать наиболее современную среду для запуска Java-программ в браузерах Netscape и Internet Explorer (подробнее об этом — в главе 10).

Загрузка аплета напоминает вставку рисунка на Web-страницу. Аллет становится частью страницы, а текст обтекает занимаемое им пространство. Изображение, реа­лизуемое аплетом становится динамичным. Оно реагирует на команды пользователя, изменяет в зависимости от них свой внешний вид и выполняет обмен данными между компьютером, на котором выполняется аплет, и компьютером, где этот аплет посто­янно хранится.

На рис. 1.1 показан хороший пример динамической Web-страницы, выполняющей сложные вычисления и применяющей аплет для отображения моделей молекул. Чтобы лучше понять структуру молекулы, можно вращать ее либо изменять масштаб изображения, используя мышь. Подобные эффекты нельзя реализовать на статических Web-страницах, однако аплеты делают это возможным. (Этот аплет можно найти по адресу https://jmol.sourceforge.net .)

Файл:Рис.1.1.png

Рис. 1.1. Аплет Jmol

С помощью аплетов на Web-страницу можно добавлять новые кнопки и текстовые поля.. Однако, если соединение производится по телефонной линии, такие аплеты медленно загружаются. Почти то лее самое можно сделать с помощью языка Dynamic HTML, форм языка HTML (Hypertext Markup Language — язык разметки гипертекстовых данных) или языка сценариев, например JavaScript. Первые аплеты предназначались для анимации: вращающиеся глобусы, танцующие персонажи мультфильмов, изменяющиеся тексты и т.п. Однако для решения этой задачи требовалась лишь малая часть возможностей аплетов: большинство из перечисленного могут делать и анимированные GIF-файлы. Поэтому аплеты целесообразно применять не столько для оформления Web-страниц, сколько для реализации сложных взаимодействий с пользователем.

Несовместимость браузеров и медленная загрузка аплетов по телефонным соединениям привели к тому, что аплеты не стали широко распространенным элементом оформления Web-страниц. В сетях intranet ситуация совершенно иная. В них обычно не возникают проблемы, связанные с пропускной способностью канала, поэтому время загрузки аплетов оказывается очень небольшим. Пользователи intranet обычно применяют одинаковые браузеры, а при необходимости можно применять Java Plug-In. Сотрудники не могут неверно установить программу, реализованную в виде аплета, а системному администратору не нужно обходить все клиентские машины и обновлять на них программное обеспечение. На предприятиях многие программы, предназначенные для учета товаров, планирования отпусков, возмещения транспортных расходов и т.п., часто используются именно в виде аплетов, выполняющихся под управлением браузеров.

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

История языка Java

В данном разделе кратко описывается история языка Java. В основу раздела положены различные опубликованные материалы (в частности, интервью с создателями языка Java в июльском выпуске электронного журнала SunWorldaa 1995 год).

История Java восходит к 1991 году, когда группа инженеров из компании Sun под руководством Патрика Нотона (Patrick Naughton) и члена Совета директоров (и разностороннего специалиста) Джеймса Гослинга (James Gosling) занялась разработкой языка, который можно было бы использовать для программирования бытовых устройств, например, контроллеров для переключения каналов кабельного телевидения. Так как подобные устройства не потребляют много энергии и имеют малый памяти, язык должен был быть маленьким и генерировать очень компактные программы. Кроме того, поскольку разные производители могут выбирать разные процессоры, было важно не привязаться к конкретной архитектуре. Проект получил кодовое название "Green".

Стремясь реализовать компактный и независимый от платформы код, разработчики возродили модель, использованную при реализации первых версий языка Pascal на заре эры персональных компьютеров. Никлаус Вирт, создатель языка Pascal, в свое время разработал переносимый язык, генерирующий промежуточный код для некоей гипотетической машины. Такие гипотетические машины называются виртуальными, отсюда термин — виртуальная машина Java (Java virtual machine —JVM). Этот промежуточный код можно выполнять на любой машине, имеющей соответствующий интерпретатор, Инженеры, работавшие над проектом "Green", также использовали виртуальную машину, что решило их основную проблему.

Большинство сотрудников компании Sun имели опыт работы с операционной системой Unix, поэтому в основу разрабатываемого ими языка был положен язык С++, а не Pascal. Они сделали язык объектно-, а не процедурно-ориентированным. Как сказал Гос-линг в своем интервью: "Язык — это всегда средство, а не цель". Сначала Гослинг решил назвать его Oak (Дуб). (Возможно потому, что он любил смотреть на дуб, растущий прямо под окнами его офиса в компании Sun.) Потом сотрудники компании узнали, что слово Oak уже используется в качестве имени ранее созданного языка программирования, и изменили название на Java.

В 1992 году в рамках проекта Green был выпущен первый продукт, названный *7. Это было устройство интеллектуального дистанционного управления. (Помещаясь в объеме 6x4x4 дюйма, оно имело мощность рабочей станции SPARK.) К сожалению, ни одна из компаний — производителей электронной техники не заинтересовалась этой разработкой. Затем группа стала заниматься созданием устройства для кабельно-го телевидения, которое могло бы осуществлять новые виды услуг, например включать видеосистему по требованию. И снова они не получили ни одного контракта. Примечательно, что одной из компаний, отказавшихся подписать с ними контракт, руководил Джим Кларк (Jim Clark) — основатель компании Netscape, впоследствии сделавшей очень много для развития языка Java.

Весь 1993 год и половину 1994 года продолжались безрезультатные поиски покупателей продукции, разработанной в рамках проекта "Green" (группа, работающая над проектом, выделилась под названием "First Person, Inc."}. Патрик Нотон, один из основателей группы, в основном занимавшийся маркетингом, налетал в общей сложности более 300 тысяч миль, пытаясь продать разработанную технологию. Работа группы "First Person, Inc." была прекращена в 1994 году.

Тем временем в рамках глобальной сети развивалась система World Wide Web. Ключевым элементом этой системы является браузер, превращающий гипертекстовые данные в изображение на экране. В 1994 году большинство пользователей применяли некоммерческий WetH>pay3ep Mosaic, разработанный в суперкомпьютерном центре Университета штата Иллинойс в 1993 году. Частично этот браузер был написан Марком Андреессеном (Mark Andreessen). В то время Марк заканчивал университет, и браузер был его дипломной работой. (Затем он стал одним из основателей и ведущим специалистом компании Netscape, и к нему пришли слава и богатство.)

В своем интервью журналу Sun World Гослинг сказал, что в середине 1994 года разработчики языка поняли: "Нам нужно создать высококачественный браузер. Такой браузер должен представлять собой приложение, соответствующее технологии кли-ентчзервер, в которой жизненно важным является именно то, что мы сделали: архитектурная независимость, выполнение в реальном времени, надежность, безопасность — вопросы, которые не были чрезвычайно важны для рабочих станций. И мы создали такой браузер".

Реальный браузер был разработан Патриком Нотоном и Джонатаном Пэйном (Johnatan Payne). Позднее он был доработан и получил имя Hotjava. Чтобы продемонстрировать все возможности Java, браузер был написан на этом языке. Однако разработчики не забывали о таких средствах, которые теперь называются аплетами, наделив свой продукт способностью выполнять код внутри Web-страниц. Продукт, созданный в рамках новой технологии, был представлен на выставке Sun World '95 23 мая 1995 года и вызвал всеобщий интерес к Java.

Компания Sun выпустила первую версию Java в начале 1996 года. Через несколько месяцев после нее появилась версия Java 1.02. Пользователи быстро поняли, что версия Java 1.02 не подходит для разработки серьезных приложений. Конечно, эту версию можно применять для реализации визуальных эффектов на Web-страницах, например располагать произвольным образом элементы текста, однако версия Java 1.02 была еще сырой. В ней даже отсутствовали средства вывода на принтер. В следующей версии, Java 1.1, были устранены наиболее очевидные недостатки, намного улучшены средства отражения и реализована новая модель событий для программирования графического пользовательского интерфейса. Несмотря на это, ее возможности все еще были ограничены.

Выпуск версии Java 1.2 стал основной новостью конференции JavaOne в 1998 году. В новой версии слабые средства для создания графического пользовательского интерфейса и графических приложений были заменены мощным инструментарием. Это был шаг вперед, к реализации лозунга "Write Once, Run Anywhere" ("Однажды реализовано— везде выполняется"), выдвинутого при разработке предыдущих версий. В декабре 1998 года через три дня (!) после выхода в свет название новой версии было изменено на громоздкое словосочетание Java 2 Standard, Edition Software Development Kit Version 1.2 (Стандартная редакция пакета инструментальных средств для разработки программного обеспечения на языке Java 2, версия 1.2).

Кроме Standard Edition, были предложены еще два варианта: Micro Edition ("микроредакция") для портативных устройств, например для мобильных телефонов, и Enterprise Edition (редакция для корпоративных приложений). В нашей книге внимание в основном уделено Standard Edition.

Версии 1.3 и 1.4 пакета инструментальных средств Standard Edition намного совершеннее первоначальной реализации Java 2. Они обладают новыми возможностями, производительность их повышена, и, разумеется, они содержат намного меньше ошибок. В процессе развития Java многие взгляды на аплеты и серверные программы были пересмотрены. В частности, оказалось, что на базе Java можно разрабатывать высококачественные серверные приложения.

В версии 5.0 язык Java подвергся наиболее существенной модификации с момента реализации версии 1.1. (Первоначально версия 5.0 имела номер 1.5, но на конференции JavaOne в 2004 г. была принята новая нумерация версий.) После многолетних исследований были добавлены новые базовые конструкции, хотя при этом не были выдвинуты требования модификации виртуальной машины. Ряд элементов, например циклы "for each", автоматическое представление простых типов в виде объектов и метаданные были явно "навеяны" языком С#. Изменение и дополнение базовых языковых конструкций — болезненный процесс, но многие из средств версии 5.0 настолько удобны, что разработчики наверняка примут их с энтузиазмом.

В табл. 1.1 приведена информация об этапах развития языка Java.

В табл. 1.2 показан рост объема библиотеки АРІ по мере появления новых версий пакета JDK.

Таблица 1.1. Этапы развития языка Java


Версия Новые языковые средства
1.0 Реализация языка
1.1 Внутренние классы
1.2 Отсутствуют
1.3 Отсутствуют
1.4 Директивы
5.0 Универсальные классы, цикл "for each", представление простых типов в виде объектов, метаданные, нумерация, статическое импортирование

Таблица 1.2. Рост объема библиотеки АРІ для Java Standart Edition


Версия Количество классов и интерфейсов
1.0 211
1.1 477
1.2 1524
1.3 1840
1.4 2723
5.0 3270

Распространенные заблуждения относительно языка Java

В этом разделе перечислены и прокомментированы некоторые из распространенных заблуждений, касающихся языка Java.

ЯзыкJava - это расширение языка HTML.

Java — это язык программирования, а язык HTML — это способ описания структуры Web-страниц. Между ними нет ничего общего, за исключением HTML-дескрипторов, позволяющих размещать на Web-страницах аплеты, написанные на языке Java.

Я использую XML, поэтому мне не нужен язык Java.

Java — это язык программирования, a XML — средство описания данных. Данные, описанные с помощью языка XML, можно обрабатывать программами, написанными на любом языке программирования, но лишь библиотека API языка Java содержит превосходные средства для обработки таких данных. Кроме того, на языке Java реализованы многие программы независимых производителей, предназначенные для работы с XML-документами. Более подробную информацию по этому вопросу можно найти во втором томе данной книги.

Язык Java легко выучить.

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

Язык Java со временем станет универсальным языком программирования для всех платформ.

Теоретически это возможно. Именно об этом мечтают все производители программного обеспечения, кроме компанйи Microsoft. Однако есть много приложений, прекрасно работающих на персональных компьютерах, которые не будут так же хорошо работать на других устройствах или под управлением браузера. Кроме того, эти приложения написаны так, чтобы максимально использовать возможности процессоров и платформенноориентированных библиотек. В любом случае они уже перенесены на все важные платформы. К таким приложениям относятся текстовые и графические редакторы, а также Web-браузеры. Обычно эти приложения пишутся на языке С или С++, и пользователь ничего не выиграет, если переписать их на языке Java. Помимо всего прочего» после реализации на языке Java эти программы станут, по крайней мере, в ближайшем будущем, менее эффективными.

Язык Java - это не больше чем еще один язык программирования.

Java — прекрасный язык программирования. Многие программисты отдают предпочтение именно ему, а не языку С или С++. Однако в мире существуют сотни великолепных языков, так и не получивших широкого распространения, в то время как языки с очевидными недостатками, такие как С++ и Visual Basic, достигли ошеломительных успехов.

Почему так происходит? Успех любого языка программирования в основном определяется его системой поддержки, а не элегантностью его синтаксиса. Существуют ли стандартные библиотеки, позволяющие сделать именно то, что необходимо программисту? Разработана ли удобная среда для создания и отладки программ? Интегрирован ли язык и его инструментарий в инфраструктуру компьютера? Язык Java достиг успехов в области серверных приложений, поскольку его библиотеки классов позволяют легко сделать то, что раньше было трудно реализовать, например, поддерживать работу в сети и обеспечивать выполнение нескольких потоков. Тот факт, что язык Java уменьшил количество ошибок, связанных с указателями, также говорит в его пользу. Благодаря этому производительность труда программистов повысилась. Однако не в этом кроется причина его успеха.

С появлением языка С# Java устарел.

Разработчики С# использовали многие удачные технические решения, применявшиеся в языке Java, такие как полностью определенный язык, использование виртуальной машины и систему "сборки мусора". Однако по ряду характеристик С# уступает Java, это вопросы безопасности и переносимости программ. Самое очевидное преимущество С# — это прекрасная среда разработки. Если вы работаете в системе Windows, может быть, вам стоит использовать С#. Если же оценить реальную ситуацию, то на сегодняшний день Java остается основным инструментом очень многих программистов.

Java разработан и поддерживается компанией Sun, поэтому пользоваться им не следует.

Мнения на этот счет расходятся. Когда недостатки данного языка были очевидны, хотелось, чтобы их поскорее исправили в рамках открытой системы. Однако на сегодняшний день ситуация не так проста.

Несмотря на то что Sun сохраняет полный контроль над Java, в работу над данным языком и его библиотеками уже оказались вовлечены многие другие компании. Исходных код виртуальной машины и библиотек доступен всем желающим, но его можно использовать лишь для ознакомления, вносить в него изменения запрещено.

Если оценить известные на данный момент открытые языки, ясно видно, что они никак не превосходят Java. Наиболее популярными считаются "три буквы Р" в "LAMP" (Linux, Apache, MySQL и Perl/PHP/Python). Эти языки имеют ряд положительных качеств, но в то же время в них не согласованы изменения, вносимые в разные версии, возможности библиотек ограничены, а возможности инструментов разработки явно недостаточны.

Отдельного рассмотрения заслуживают С++ и С#, выработкой стандартов для которых занимались независимые комитеты. Нельзя не согласиться, что их рекомендации более "прозрачны" по сравнению с деятельностью Java Community. Но на практике оказывается, что применимость выработанных спецификаций ограничена. Недостаточно стандартизовать язык и большинство основных библиотек. В реальных проектах очень скоро приходится выходить за рамки действий по обработке строк, наборов данных и файлов. Поэтому, если проанализировать С#, оказывается, что для основной массы библиотек стандартов попросту не существует.

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

Программы на языке Java работают под управлением интерпретатора, значит, серьезные приложения будут выполняться слишком медленно.

В начале своего развития Java-программы действительно интерпретировались. Теперь в состав всех виртуальных машин, за исключением "микроредакции", входит синхронный компилятор. Основные элементы кода будут выполняться не медленнее, чем если бы они были написаны на С++.

Для Java характерны дополнительные накладные расходы по сравнению с С++, которые, впрочем, не имеют никакого отношения к использованию виртуальной машины. Они связаны с процедурой "сборки мусора". Автоматическая "сборка мусора", конечно же, происходит несколько медленнее, чем управление памятью вручную, и для Java-nporpaMM требуется немного больше памяти, чем для программ, выполняющих аналогичные функции, но написанных на С++. Запуск программы — достаточно длительная процедура, в особенности для очень больших приложений. Пользовательский интерфейс имеет меньшее быстродействие, так как его элементы являются независимыми от конкретной платформы.

Годами пользователи жалуются на низкое быстродействие Java по сравнению с С++. Однако за это время мощность компьютеров многократно возросла, и не исключено, что на новых машинах Java-приложения будут выполняться быстрее, чем работали на старом оборудовании программы, написанные на С++. Теперь, когда нет оснований жаловаться на низкое быстродействие, некоторые противники Java утверждают, что качество интерфейсных элементов, реализуемые средствами данного языка, очень низкое.

Все программы на языке Java выполняются под управлением Web-браузеров.

Все аплеты, написанные на языке Java, действительно выполняются под управлением Web-браузеров. Собственно, это и есть определение аплета— программа, выполняемая в среде, реализуемой Web-браузером. Однако вполне возможно и уместно создавать независимые Java-npoграммы, которые никак не связаны с Web-браузером. Эти программы (обычно называемые приложениями) являются полностью переносимыми. Их можно без всяких изменений выполнять на другой машине! Поскольку язык Java более удобен и менее подвержен ошибкам, чем язык С++, многие разработчики выбирают именно его. А если учесть наличие средств доступа к базам данных, например Java Database Connectivity (см. том 2), то трудно предпочесть Java какой-то другой язык. Очень удобен Java для начинающих, выбравших его в качестве первого языка программирования.

Большинство программ, рассматриваемых в этой книге, являются независимыми приложениями. Конечно, аплеты очень забавны. Однако Java-приложения более важны и полезны.

Javoranaembt представляют большую опасность для системы защиты.

В свое время было опубликовано несколько отчетов об ошибках в системе защиты языка Java. Большинство из них касалось реализаций языка Java в конкретных браузерах. Исследователи восприняли это как вызов и принялись искать глобальные недостатки в системе защиты Java, которые делали бы бессмысленной модель безопасности аплетов. Их усилия не увенчались успехом. Обнаруженные ошибки в конкретных реализациях вскоре были исправлены, и, после этого, насколько мы знаем, ни одна реальная система не была взломана посредством аплета. Чтобы оценить значение этого факта, вспомните о миллионах вирусных атак на выполняемые файлы операционной системы Windows и макросы редактора Word, действительно вызвавшие немало бед. При этом критика недостатков платформы была удивительно беззуба. Кроме того, механизм ActiveX в браузере Internet Explorer мог бы вызвать много нареканий, однако способы его взлома настолько очевидны, что лишь немногие специалисты потрудились опубликовать результаты своих исследований.

Некоторые системные администраторы даже стали выключать системы защиты языка Java в своих браузерах, чтобы пользователи могли, как и прежде, загружать исполняемые файлы, элементы управления ActiveX и документы, созданные с помощью текстового процессора Word. Можно утверждать, что в настоящее время вероятность преодоления системы защиты аплетов на языке Java сравнима с вероятностью погибнуть в авиационной катастрофе, в то время как риск заразить компьютер вирусом, открыв документ, созданный текстовым процессором Word, куда больше, чем риск погибнуть под колесами автомобиля, перебегая дорогу в час пик.

ЯзыкJavaScript - упрощенная версия языка Java.

Язык JavaScript — это язык сценариев, который можно использовать в составе Web-страницы. Он был разработан компанией Netscape и сначала назывался LiveScript. Синтаксис языка JavaScript напоминает синтаксис языка Java, однако на этом их сходство заканчивается (за исключением имени, конечно). Подмножество JavaScript было стандартизовано как ЕСМА-262, однако его расширения, необходимые для реальной работы, стандартизованы не были. В результате сложный JavaScript-сценарий, который можно было бы выполнять как под управлением браузеров компании Netscape, так и с помощью браузера Internet Explorer, остался несбыточной мечтой.

Пользуясь языком Java, можно заменить компьютер Internet-устройством стоимостью 500 долларов.

Некоторые специалисты уверены, что это непременно случится. Нам кажется абсурдным думать, что пользователи, работающие дома, решат заменить мощные и удобные персональные компьютеры устройствами без жестких дисков с ограниченными возможностями. Однако сетевой компьютер, оснащенный средствами языка Java, предоставляет вполне реальную возможность реализовать "нулевую администраторскую инициативу", направленную на уменьшение стоимости компьютеров, занятых в бизнесе.

Мы рассматриваем Internet-устройство как дополнение к персональному компьютеру. Если цена позволяет, почему бы не приобрести специальную аппаратуру для просмотра на экране электронной почты и новостей? Ядро языка Java настолько мало, что он является очевидным выбором для телефонов и других подобных аппаратов.

Ответы на часто задаваемые вопросы о языке Java можно найти в документе по адресу https://docs.oracle.com/javase/tutorial/index.html



Реклама






]]>
Книги по Java https://linexp.ru?id=4718 Wed, 29 Jun 2022 13:55:46 GMT
<![CDATA[Core Java 2 Том I Глава 3]]>

Содержание

Оглавление

В этой главе...

  • Простая программа на языке Java
  • Комментарии
  • Типы данных
  • Переменные
  • Операторы
  • Строки
  • Ввод и вывод
  • Поток управления
  • Работа с большими числами
  • Массивы

Будем считать, что вы успешно инсталлировали JDK. и выполнили простые про­граммы, приведенные в главе 2. Настало время приступать непосредственно к програм­мированию. В этой главе вы узнаете, как в языке Java реализуются основные концепции программирования, например типы данных, ветви и циклы.

К сожалению, на языке Java нелегко написать программу с графическим интерфей­сом, — Для этого нужно изучить множество вопросов, связанных с окнами, полями вво­да, кнопками и т.п. Поскольку описание таких технических подробностей увело бы нас далеко в сторону от нашей основной цели — анализа основных языковых конструкций, в этой главе мы рассмотрим лишь простые программы, иллюстрирующие то или иное понятие. Все эти программы используют для ввода и вывода данных консоль.
В заключение заметим, что программисты, имеющие опыт работы на языке С/С++- могут просто бегло просмотреть эту главу. Разработчикам, использующим другие языки, например Visual Basic, многие понятия также окажутся знакомыми, хотя синтаксис выражений будет существенно отличаться. Таким читателям мы сове­туем тщательно изучить эту главу.

Простая программа на языке Java

Рассмотрим самую простую программу, какую только можно себе предста­вить. В процессе работы она лишь выводит сообщение в консольное окно.

/**
@version 1.01 1997-03-22
@author Gary Cornell
*/

 
/*
This is the first sample program in Core Java Chapter 3
Copyright (C) 1997 Cay Horstmann and Gary Cornell
*/

 
public class FirstSample
{
public static void main(String[] args)
{
System.out.println("We will not use 'Hello, World!'");
}
}

Этому примеру стоит посвятить столько времени, сколько потребуется, чтобы привыкнуть к особенностям языка; и подробно рассмотреть характерные особенно­сти Java-программ, которые будут встречаться во всех приложениях. Первое, на что надо обратить внимание, — в языке Java учитывается регистр символов. Если вы перепу­таете их (например, введете Main вместо main), программа выполняться не будет.

Теперь просмотрим исходный код построчно. Ключевое слово public называется модификатором доступа (access modifier); такие модификаторы управляют обращением к коду из других частей программы. Более подробно мы рассмотрим этот вопрос в главе 5. Ключевое слово class напоминает нам, что все элементы Java-программ находятся в со­ставе классов. Классы будут детально рассматриваться в следующей главе, а пока мы будем считать их некими "контейнерами", в которых' реализована логика программы, опреде­ляющая работу приложения. Как указывалось в главе 1, классы — это "строительные бло­ки", из которых состоят все приложения и аплеты, написанные на языке Java.

За ключевым словом class следует имя класса. Правила формирования имен классов не слишком строги. Имя должно начинаться с буквы, а остальная его часть может пред­ставлять собой произвольное сочетание букв и цифр. Длина имени не ограничена. В каче­стве имени класса нельзя использовать зарезервированные слова языка Java (например, public или class). {Список зарезервированных слов приведен в приложении А.)

Согласно соглашениям об именовании, имя класса должно начинаться с пропис­ной буквы (именно так сформировано имя FirstSample). Если имя состоит из не­скольких слов, каждое из них должно начинаться с прописной буквы. (Правила, по которым в середине слова может стоять символ верхнего регистра, иногда называют "camel case", или, в соответствии с этими же правилами, CamelCase.)

Файл, содержащий исходный текст, должен называться так же, как и общедоступный (public) класс, и иметь расширение .java. Таким образом, код рассматриваемого здесь класса мы должны поместить в файл FirstSample. java. (Как и следует ожидать, регистр символов учитывается, поэтому имя firstsample .java не подходит.)

Если вы правильно назвали файл и не допустили ошибок в исходном тексте про­граммы, то в результате компиляции получите файл, содержащий байтовые коды данного класса. Компилятор языка Java автоматически назовет этот файл FirstSample. class и сохранит его в том же каталоге, в котором содержится исход­ный файл. Осталось выполнить байтовые коды с помощью интерпретатора языка Java, набрав команду "*~java FirstSample

(Расширение .class не указывается!) Выполняясь, программа выведет на экран строку "We will not use 'Hello, World'!".

Когда для запуска скомпилированной программы используется команда java имя_класса
интерпретатор языка Java всегда начинает свою работу с выполнения метода main ( ) указанного класса. Следовательно, чтобы программа могла выполняться, в классе должен присутствовать метод main ( ). Разумеется, в класс можно добавить и другие методы. (Мы покажем, как создавать такие методы, в следующей главе.)


ВАЖНОЕ ЗАМЕЧАНИЕ !
В соответствии со спецификацией языка Java метод main ( ) должен быть объявлен как public. (Спецификация языка Java является официальным документом. Его можно скопировать, обратившись по адресу http : / / j ava. sun. com/docs /books/j Is.) Однако некоторые версии интерпретатора Java допускали выполнение программ, даже когда метод main () не имел модификатора public. Эта ошибка была помещена в спи­сок замеченных недостатков, представленный на сайте https://developer.java. sun.com/developer/bugParade, и получила номер 4252539. Однако она была поме­чена как "исправлению не подлежит". Разработчики компании Sun выступили с разъяс­нениями, что спецификация виртуальной машины языка Java не требует, чтобы метод main( ) был общедоступным (см. Web-страницу https: / /java.sun.com/docs/books/ vmspec), а попытка исправить эту ошибку "может вызвать проблемы". К счастью, здра­вый смысл в итоге восторжествовал. Интерпретатор языка Java в пакете JDK1.4 требу­ет, чтобы метод main ( ) был общедоступным.

Эта история не оставляет равнодушных разработчиков. С одной стороны, становится как-то неуютно от того, что инженеры, призванные гарантировать высокое качество программ, не всегда оказываются квалифицированными специалистами и позволяют себе "отмахиваться" от замеченных ошибок. С другой стороны, стоит отметить тот факт, что компания Sun разместила список ошибок и способы их исправления на Web-сайте, открыв его для всеобщего доступа. Эта информация весьма полезна для программи­стов. Вы даже можете проголосовать за вашу "любимую" ошибку. Ошибки, набравшие наибольшее число голосов, будут исправлены в следующих выпусках пакета JDK.

Обратите внимание на фигурные скобки в исходном тексте программы. В языке Java, так же, как и в языке C/C++, фигурные скобки используются для выделения блоков про­граммы, В языке java код любого метода должен начинаться с открывающей фигурной скобки ({) и завершаться закрывающей фигурной скобкой (} ).
Расстановка фигурных скобок всегда вызвала споры. Обычно мы стараемся распо­лагать скобки одну под другой, выравнивая их с помощью пробелов. В то же время компилятор языка Java игнорирует пробелы, поэтому, фигурные скобки можно рас­полагать где угодно. Изучая различные операторы цикла, мы поговорим о скобках более подробно.
Пока мы не будем обращать внимание на ключевые слова static void, считая их просто необходимой частью программы на языке Java. В конце главы 4 мы полностью раскроем смысл этих слов. Сейчас важно помнить, что каждое приложение на языке Java должно иметь метод main ( ), заголовок которого приведен ниже.

public class имя_класса {
public static void main(String[] args) { команды }
}

{{Note|text=
Если вы программируете на языке С++, то, конечно же, знаете, что такое класс. Клас­сы в языке Java похожи на классы в языке C/C++, однако между ними есть сущест­венные различия. Например* в языке Java все методы принадлежат тому или иному классу. (Термин метод в языке Java соответствует термину функция-член в С++.) Следовательно, в языке Java должен существовать класс, которому принадлежит ме­тод main (). Вероятно, вы знакомы с понятием статических фучкций-членов в языке С++. Это функции-члены, определенные внутри класса и не принадлежащие ни од­ному объекту. Метод main () в языке Java всегда является статическим. В заключе­ние, как и в языке C/C++, ключевое слово void означает, что метод не возвращает никакого значения. В отличие от языка C/C++, метод main {) не передает операцион­ной системе код завершения. Если данный метод корректно завершает свою работу, код завершения равен 0. Чтобы изменить код завершения, надо использовать метод

System.exit ()

Теперь обратите внимание на следующий фрагмент кода:

{
System.out.println("We will not use 'Hello, World!");
}

Фигурные скобки отмечают начало и конец тела метода, содержащего лишь одну строку кода. Как и в большинстве языков программирования, операторы языка Java можно считать предложениями. В языке Java каждый оператор должен заканчиваться точкой с запятой. В частности, символ конца строки не означает конец оператора, поэтому оператор может занимать столько строк, сколько потребуется.
В нашем случае при выполнении метода main () на консоль выводится одна строка текста.

В данном примере мы используем объект System.out и вызываем его метод println (). Заметьте, что метод отделяется от объекта точкой. В общем случае вызов метода имеет следующий вид:

объект*. метод (параметры)

В нашем примере мы вызываем метод println (), передавая ему в.качестве пара­метра текстовую строку. Метод выводит строку текста на консоль, дополняя ее симво­лом перевода строки. В языке Java, как и в языке С/С++, строковый литерал помещается в двойные кавычки. (Далее в этой главе мы рассмотрим работу со строками подробнее.)

Методам в языке Java, как и функциям в любом другом языке программирования, может передаваться один или несколько параметров; метод также может вызываться без параметров. (В некоторых языках параметры принято называть аргументами). Даже если метод не имеет параметров, после его имени надо ставить скобки. Напри­мер, при вызове метода println () без параметров на экран выводится пустая строка. Такой вызов выглядит следующим образом:
System.out.println();


ВАЖНОЕ ЗАМЕЧАНИЕ !
В объекте System. out есть метод print (), который выводит строку текста, не до­бавляя к ней символ перехода на новую строку. Например, выражение System, out.print ("Hello") выводит текст "Hello" и оставляет курсор в конце строки. Следующие данные, выводимые на экран, появятся сразу за буквой "о".

Комментарии

Комментарии в языке Java, как и в большинстве языков программирования, игно­рируются при выполнении программы. Таким образом, в программу можно добавлять столько комментариев, сколько потребуется, не опасаясь увеличить ее объем. В языке Java есть три способа выделения комментариев в тексте. Чаще всего используются две косые черты (//), при этом комментарий начинается сразу за символами //и про­должается до конца строки.

 System.out.println("We will not use 'Hello, World!'"); // Остроумно, не правда ли?

Если нужны комментарии, состоящие из нескольких строк, можно каждую строку начинать символами //. Кроме того, для создания больших блоков комментариев можно использовать разделители / * и * /, как показано в листинге 3.1.

/**
@version 1.01 1997-03-22
@author Gary Cornell
*/

 
/*
This is the first sample program in Core Java Chapter 3
Copyright (C) 1997 Cay Horstmann and Gary Cornell
*/

 
public class FirstSample
{
public static void main(String[] args)
{
System.out.println("We will not use 'Hello, World!'");
}
}

В заключение отметим, что в языке Java есть и третья разновидность комментари­ев, которую можно использовать для автоматической генерации документации. Эти комментарии начинаются символами / * * и заканчиваются символами * /. Более под­робную информацию об этом виде комментариев и автоматической генерации доку­ментации можно найти в главе 4.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Комментарии, выделяемые символами /* и */. в языке Java не могут быть вложенны­ми. Это значит, что фрагмент кода нельзя отключить, просто окружив его парами символов /* и */, поскольку в составе этого кода в свою очередь могут содержаться разделители /* и */.

Типы данных

Язык Java является строю типизированным. Это значит, что тип каждой переменной должен быть объявлен. В языке Java есть восемь основных, или простых типов (primitive types) данных. Четыре из них представляют целые числа, два — действительные числа с плавающей точкой, один — символы в формате Unicode и последний — логические значения.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке Java предусмотрен пакет для выполнения арифметических действий с про­извольной точностью. Однако так называемые "большие числа" в языке Java являют­ся объектами. Позднее в этой главе мы покажем, как с ними работать.

Целые числа

Целочисленные типы используются для представления как положительных, так и отрицательных чисел, не имеющих дробной части. В языке Java есть четыре целочис­ленных типа. Они представлены в табл. 3.1.

Таблица 3.1. Целочисленные типы в языке Java


Тип Требуемый объем памяти (байты) Диапазон (включительно)
int 4 от -2147483648 до 2147483647 (больше 2 миллиардов)
short 2 от -32768 до 32767
long 8 от -9223372036854775808 до -9223372036854775807
byte 1 от-128 до 127

В большинстве случаев тип int наиболее удобен. Если нужно задать количество жителей в самой густонаселенной стране, нет никакой необходимости прибегать к типу long. Типы byte и short в основном используются в специальных приложе­ниях, например, при низкоуровневой обработке файлов или с целью экономии памя­ти при формировании больших массивов.

В языке Java диапазоны целочисленных типов не зависят от машины, на которой выполняется программа. Это существенно упрощает перенос программного обеспече­ния с одной платформы на другую. Сравните данный подход с языками С и С++, где для каждого конкретного процессора используется тип, наиболее эффективный именно на нем. В результате программа на языке С, которая отлично работает на 32-разрядном процессоре, может привести к целочисленному переполнению в 16-разрядной системе.

Длинные целые числа имеют суффикс L (например, 4000000000L). Шестнадцате-ричные числа имеют префикс Ох (например, OxCAFE). Восьмеричные числа имеют префикс 0. Например, 010 — это число 8. Такая запись иногда приводит к недоразу­мениям, поэтому мы не рекомендуем применять восьмеричные числа.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языках С и С++ int означает целочисленный тип, зависящий от машины, для кото­рой предназначена программа. На 16-разрядном процессоре, например процессоре 8086, целые числа занимают 2 байта. На 32-разрядном процессоре, например про­цессоре Sun SPARK, они занимают 4 байта. На процессоре Intel Pentium размер цело­го типа в языках С и С++ зависит от операционной системы: в DOS и Windows 3.1 це­лые числа занимают 2 байта. При использовании 32-разрядного режима работы в системе Windows целые числа занимают 4 байта. В языке Java размеры всех число­вых типов не зависят от платформы, на которой выполняется программа,

Заметим, что в языке Java нет беззнаковых типов unsigned.

Числа с плавающей точкой

Типы с плавающей точкой представляют значения, имеющие дробную часть. В языке Java есть два типа для чисел с плавающей точкой, они приведены в табл. 3.2.

Таблица 3.2. Числа с плавающей точкой в языке Java


Тип Требуемый объем памяти (байты) Диапазон
float Приблизительно ±3,40282347E+38F (6-7 значащих десятичных цифр)
double Приблизительно ±1,7976931348623157E+308F (15 значащих десятичных цифр)

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

Числа типа float имеют суффикс F, например 3 .402F. Числа с плавающей точ­кой, не имеющие суффикса F (например, 3 . 402), всегда рассматриваются как числа типа double. Для их представления можно (но не обязательно) использовать суф­фикс D, например 3 . 4 02D.
В JDK 5.0 допустимо задавать числа с плавающей точкой в шестнадцатеричном формате. Например, 0,125— то же, что Oxl.Op-З. В шестнадцатеричной записи для указания степени вместо е используется р.

Все вычисления, производящиеся над числами с плавающей точкой, следуют стандарту IEEE 754. В частности, в языке Java есть три специальных значения с плавающей точкой:

  • положительная бесконечность;
  • отрицательная бесконечность;
  • NaN (не число).

Они используются для обозначения переполнения и ошибок. Например, результат деления положительного числа на 0 равен положительной бесконечности. Вычисле­ние 0/0 или извлечение квадратного корня из отрицательного числа равно NaN.

В языке Java существуют константы Double. positive_infinity, Double. negative_ infinity и Double.NaN (а также соответствующие константы типа float). Однако на практике они редко используются. В частности, для того, чтобы убедиться, что некий результат равен константе Double. NaN, нельзя выполнить проверку

if (х == Double.NAN) // Так нельзя проверить,
// является ли результат числом.

Все величины, "не являющиеся числами", считаются разными. Однако можно вызы­вать метод Double. isNaN ():

if (Double.isNaN(x)) // Такая проверка допустима.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Числа с плавающей точкой нельзя использовать в финансовых вычислениях, где ошибки округления недопустимы. Например, в результате выполнения команды system.out.println(2.0 - 1.1) будет выведено не 0,9, как логично ожидать, а 0,8999999999999999. Подобные ошибки связаны с внутренним двоичным пред­ставлением чисел. Подобно тому, как в десятичной системе счисления нельзя точно представить результат деления 1/3, так и в двоичной системе невозможно точно представить 1/10. Если вам надо исключить ошибки округления, следует использо­вать класс BigDecimal, который будет рассмотрен далее в этой главе.

Символы

Для того чтобы правильно использовать тип char, надо иметь представление о принципах кодировки Unicode*. До появления Unicode существовало несколько раз­личных стандартов: ASCII, ISO 8859-1, KOI-8, GB18030, BIG-5 и т.д. При этом возникали две проблемы. Во-первых, один и тот же код в разных кодировках соответствовал раз­личным символам. Во-вторых, в языках с большим набором символов использовался код различной длины: часто употребляющиеся символы представлялись одним байтом, дру­гие знаки — двумя, тремя и большим количеством байтов.
Для решения этих проблем была разработана кодировка Unicode. В результате ис­следований, направленных на унификацию кодов символов, выяснилось, что двухбай­тового кода более чем достаточно для представления всех символов, использующихся во всех языках; при этом оставался достаточный резерв для любых мыслимых расшире­ний. В 1991 г. была выпущена спецификация Unicode 1.0, в которой было использовано меньше половины из возможных 65536 кодов. В Java изначально были приняты 16-би­товые символы Unicode, что стало еще одним преимуществом перед другими языками.

Однако впоследствии случилось непредвиденное: количество символов превысило допустимые 65536. Причиной тому стали чрезвычайно большие наборы иероглифов китайского, японского и корейского языков. Поэтому в настоящее'время 16-битового типа char недостаточно для описания всех символов Unicode.

Чтобы понять, как эта проблема решается в Java, начиная с JDK 5.0, надо ввести не­сколько терминов. Назовем кодовой точкой (code point) значение, связанное с символом в схеме кодирования. Согласно стандарту Unicode, кодовые точки записываются в шест-наддатеричном формате и предваряются символами U+. Например, для буквы А кодовая точка равна U+0041. В Unicode кодовые точки объединяются в 17 кодовых плоскостей (code plane). Первая кодовая плоскость, называемая основной многоязыковой плоскостью (basic multilingual plane), состоит из "классических" символов Unicode с кодовыми точками от U+0000 до U+FFFF. Шестнадцать дополнительных плоскостей с кодовыми точками от U+10000 до U+10FFFF содержат дополнительные символы (supplementary character).

Кодировка UTF-16 — это способ представления всех кодов Unicode последовательно­стью переменной длины. Символы из основной многоязыковой плоскости представля­ются 1бйбитовыми значениями, называемыми кодовыми единицами (code unit). Дополни­тельные символы обозначаются последовательными парами кодовых единиц. Каждое из значений пары попадает на используемую 2048-байтовую область основной много­языковой плоскости, называемой областью подстановки (surrogates area); от U+D800 до U+DBFF для первой кодовой единицы и от U+DC00 до U+DFFF для второй кодовой еди­ницы. Такой подход позволяет сразу определить, соответствует ли значение коду кон­кретного символа или является ли частью кода дополнительного символа. Например, математическому коду символов, обозначающему множество целых чисел, соответствует кодовая точка U+1D56B и две кодовых единицы, U+D835 и U+DD6B (описание алгоритма кодирования можно найти по адресу https://en.wikipedia.org/wiki/UTF-16).

В Java тип char описывает кодовую единицу TJTF-16.

Начинающим программистам мы советуем использовать коды UTF-16 лишь в случае крайней необходимости. Если это возможно, используйте строки.

В некоторых случаях применение типа char вполне оправдано. Речь идет о работе с символьными константами. Например, символьной константой является 'А', которой соответствует значение 65. Не следует путать символ ' А' со строкой "А", состоящей из одного символа. Кодовые единицы Unicode можно выражать в виде шестнадцатерич-ных чисел в диапазоне от \u0000 до \uFFFF. Например, значение \u2122 соответст­вует символу торговой марки (™), а \u03C0 — греческой букве тс.

Кроме префикса \и, который предваряет кодовую единицу Unicode, существует также несколько специальных символьных последовательностей, показанных в табл. 3.3. Эти последовательности можно применять в составе символьных констант или строк, например ' \u2122 ' или "Неllо\n". Последовательности, начинающиеся с \u (и никакие другие), можно даже указывать за пределами символьных констант или строк. Приведенный ниже пример корректен, так как последовательности \u005В и \u005D соответствуют символам [ и ].

public static void main(String\u005B\u005D args)

Таблица 3.3. Специальные символы


Специальный символ Описание Значение Unicode
\b Возврат на одну позицию \u0008
\t Табуляция \u0009
\n Переход на новую строку \u000a
\r Возврат каретки \u000d
\ Двойная кавычка \u0022
\' Одинарная кавычка \u0027
\\ Обратная косая черта \u005c


ВАЖНОЕ ЗАМЕЧАНИЕ !
Теоретически в приложении или аплете на языке Java можно использовать любой символ в формате Unicode, однако будет ли он отображаться на экране дисплея, зависит от вашего браузера (для аплетов) и от операционной системы.

Логические значения

Для типа boolean предусмотрены два значения: false и true. Они соответствуют результатам вычисления логических выражений. Преобразование логических пере­менных в целочисленные и наоборот невозможно.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ вместо логических значений можно использовать числа и даже указате­ли. Значение 0 эквивалентно логическому значению false, а ненулевые величины — значению true. В языке Java представлять логические значения посредством других типов невозможно. Следовательно, программист на языке Java защищен от недора­зумений, подобных следующему:
if (х = 0) // Вместо проверки х==0 выполнили присваивание!

В языке С++ эта строка компилируется и выполняется проверка, причем выражение всегда равно false. В языке Java наличие такой строки приведет к ошибке на этапе компиляции, поскольку целочисленное выражение х = о нельзя преобразовать а ло­гическое значение.

Переменные


В языке Java каждая переменная имеет тип. При объявлении переменной сначала указывается ее тип, а затем ее имя. Ниже приведено несколько примеров объявления переменных.

double salary; 
int vacationDays;
long earthPopulation;
char yesChar;
boolean done;

Обратите внимание на точку с запятой в конце каждого выражения. Она необхо­дима, поскольку объявление в языке Java считается оператором.
Имя переменной должно начинаться с буквы и представлять собой сочетание букв и цифр. Термины "буквы'* и "цифры" в Java имеют более широкое значение, чем в большинстве других языков программирования. Буквами считаются символы ' А' —' Z', 'a'-'z', '_' и любой другой символ в кодировке Unicode, соответствующий букве. Например, немецкие пользователи в именах переменных могут использовать символ 1 а1, а греческие пользователи могут воспользоваться буквой л. Аналогично цифрами считаются как обычные десятичные цифры, ' 0 ' - ' 9 ', так и любые символы в кодировке Unicode, использующиеся для обозначения цифры в каком-либо языке. Символы наподобие ' +' или ' ©', а также пробел нельзя использовать в именах переменных. Все символы в имени переменной важны, причем регистр также учиты­вается. Длина имени переменной не ограничена.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если вы действительно хотите знать, какие символы в формате Unicode считаются "буквами" в языке Java, воспользуйтесь методами isJavaldentifierStart () и isJavaldentifierPart() класса Character.

В качестве имен переменных нельзя использовать зарезервированные слова, (Список зарезервированных слов приведен в приложении А.)

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

 int i,j; // Обе переменные — целочисленные.

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


ВАЖНОЕ ЗАМЕЧАНИЕ !
Как уже было сказано, в языке Java различаются прописные и строчные буквы, например имена hireday и hireDay считаются разными. Иногда для переменной трудно подобрать подходящее имя. Многие программисты в этих случаях дают пере­менной имя, совпадающее с именем типа, но отличающееся регистром символов. Например:

Box box;
Где -- Box — это тип, a box — имя переменной. На наш взгляд, намного лучше использовать в имени переменной префикс а:
Box аВох;

Инициализация переменных

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

int vacationDays;
System.out.printin(vacationDays); // Ошибка!
// Переменная не инициализирована.

Для присвоения ранее объявленной переменной какого-либо значения нужно ука­зать слева ее имя, поставить знак равенства (=), а справа записать некоторое выраже­ние на языке Java, задающее требуемое значение.

int vacationDays; vacationDays = 12;

При желании вы можете одновременно объявить и инициализировать перемен­ную. Например:

int vacationDays = 12;

В языке Java объявление переменной можно размещать в любом месте кода; например, приведенный ниже фрагмент вполне допустим.

double salary = 65000.0; System.out.println(salary);
int vacationDays =12; // Здесь можно объявить переменную.

При создании программ на Java рекомендуется объявлять переменную как можно ближе к той точке кода, где она будет использована.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языках С и С++ различаются объявление и определение переменной. Ниже приве­ден пример определения переменной.

int i = 10;

Объявление переменной выглядит следующим образом:

extern int i;

В языке Java объявление и определение переменных не различаются.

Константы

В языке Java для обозначения констант используется ключевое слово final, например:

public class Constants {
public static void main(String[] args) I
final double CM_PER_IHCH = 2.54; double paperWidth = 8.5; double PaperHeight = 11;
System.out.printIn("Размер страницы в сантиметрах: " + paperWidth * CM_PER_INCH + "на" + paperheight * CM_PER_INCH);
}
}

Ключевое слово final означает, что присвоить какое-либо значение данной пе­ременной можно лишь один раз и изменять его нельзя. Использовать в именах кон­стант только прописные буквы необязательно.

В языке Java часто возникает необходимость в константах, доступных нескольким методам внутри одного класса. Обычно они называются константами класса (class con­stant). Константы класса объявляются с помощью ключевых слов static final. Ниже приведен пример использования константы класса.

public class Constants2 {
public static void main(String[] args) {
double paperWidth = * 8.5; double PaperHeight = 11;
System.out.println{"Размер страницы в сантиметрах: " + paperWidth * CM_PER_INCH + "на" + paperHeight * CM_PER_INCH);
}
public static final double CM_PER_INCH =2.54;
}

Константа класса задается вне метода main (), поэтому ее можно использовать в дру­гих методах того же класса. Более того, если (как в данном примере) константа объяв­лена как public, методы из других классов также могут получить к ней доступ. В нашем примере это можно сделать с помощью выражения Constants2 . CM_PER_INCH.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке Java слово const является зарезервированным, однако сейчас оно уже не употребляется. Для объявления констант следует использовать ключевое слово final.

Операторы

Для обозначения операций сложения, вычитания, умножения и деления в языке Java используются обычные арифметические операторы + - * /. Оператор / обо­значает целочисленное деление, если оба его аргумента являются целыми числами. В противном случае этот оператор обозначает деление чисел с плавающей точкой. Остаток от деления целых чисел обозначается символом %. Например, 15/2 равно 7; 15%2 равно 1, а 15.0/2 = 7.5.

Заметим, что целочисленное деление на 0 генерирует исключение, в то время как результатом деления на 0 чисел с плавающей точкой является бесконечность или NaN.
В языке Java предусмотрена сокращенная запись бинарных арифметических опе­раторов (т.е. операторов, предполагающих два операнда). Например, выражение
х += 4; эквивалентно х = х + 4,- (В сокращенной записи символ арифметической операции, например * или %, разме­щается перед знаком равенства, например *= или %=.)

Одной из заявленных целей языка Java является машинная независимость. Вычисле­ния должны приводить к одинаковому результату, независимо от того, какая вирту­альная машина их выполняет. Для арифметических вычислений над числами с пла­вающей точкой это неожиданно оказалось трудной задачей. Тип double использует для хранения числовых значений 64 бита, однако некоторые процессоры применяют 80-разрядные регистры с плавающей точкой. Эти регистры обеспечивают дополни­тельную точность на промежуточных этапах вычисления. Рассмотрим в качестве при­мера следующее выражение:

double w = х * у / z;

Многие процессоры компании Intel вычисляют выражение х * у и сохраняют этот про­межуточный результат в 80-разрядном регистре, затем делят его на значение перемен­ной z и в самом конце округляют ответ до 64 бит. Так можно повысить точность вычис­лений, избежав переполнения. Однако этот результат может оказаться иным, если в процессе всех вычислений используется 64-разрядный процессор. По этой причине в первоначальном описании виртуальной машины Java указывалось, что все промежуточ­ные вычисления должны округляться. Это вызвало протест многих специалистов. Округление не только может привести к переполнению. Вычисления при этом происхо­дят медленнее, поскольку операции округления занимают определенное время. В ре­зультате разработчики языка Java изменили свое мнение, стремясь разрешить кон­фликт между оптимальной производительностью и воспроизводимостью результатов. По умолчанию в виртуальной машине 8 промежуточных вычислениях может использо­ваться повышенная точность. Однако методы, помеченные ключевым словом strictfp, должны использовать операции над числами с плавающей точкой, гаранти­рующие воспроизводимость результатов. Например, метод main О можно записать так, как показано ниже.

public static strictfp void main(String[] args)

В этом случае все команды внутри метода main {) будут выполнять точные операции над числами с плавающей точкой.

Детали выполнения этих операций тесно связаны с особенностями работы процес­соров Intel. По умолчанию промежуточные результаты могут использовать расширен­ный показатель степени, но не расширенную мантиссу. (Микросхемы компании Intel поддерживают округление мантиссы без потери производительности.) Следователь­но, единственное различие между вычислениями по умолчанию и точными вычисле­ниями состоит в возможности переполнения.

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

Операторы инкрементирования и декрементирования

Программисты, конечно, знают, что одной из наиболее распространенных опера­ций с числовыми переменными является добавление или вычитание единицы. В язы­ке Java, как и в языках С и С++, есть операторы инкрементирования и декрементиро­вания: в результате вычисления выражения х++ к текущему значению переменной х прибавляется единица, а х—уменьшает х на единицу. Например, в результате обработ­ки следующего фрагмента значение переменной n становится равным 13.

int n = 12; n++;

Поскольку эти операторы ++ и -- изменяют значение переменной, их нельзя приме­нять к самим числам. Например, выражение 4++ является недопустимым.

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

int m = 7; int.n = 7;
int а = 2 * // Теперь значение а равно 16, am равно 8.
int b = 2 * n++; // Теперь значение b равно 14, an равно 8.

Мы не рекомендуем использовать операторы инкрементирования и декременти­рования внутри выражений, поскольку это зачастую запутывает код и приводит к до­садным ошибкам.

(Поскольку именно оператор ++ дал имя языку С++, это послужило поводом к пер­вой шутке о нем. Недоброжелатели отмечают, что даже имя этого языка содержит в себе ошибку: "Язык следовало бы назвать ++С, потому что мы хотим использовать его только после улучшения".)

Операторы отношения и логические операторы

Язык Java содержит полный комплекг операторов отношения. Чтобы проверить ра­венство, следует использовать символы ==. Например, значение выражения 3 == 7 равно false.

Для проверки неравенства используются символы ! =. Так, значение выражения 3 ! = 7 равно true.

Кроме того, в языке Java есть обычные операторы < (меньше), > (больше), <= (меньше или равно) и => (больше или равно).

В языке Java, как и в С++, используются символы && для обозначения логического оператора "и", а также символы ] j для обозначения логического оператора "или". Как обычно, знак восклицания означает логический оператор отрицания. Операторы && и | | задают вычисление по сокращенной схеме, согласно которой, если первый эле­мент определяет значение всего выражения, то остальные элементы не вычисляются. Рассмотрим два выражения, объединенных оператором &&.
выражение_1 && выражение_2

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

х!=0 && 1/х > х+у // Не делить на 0.

Таким образом, деление на нуль не происходит.

Аналогично, значение выражение_1 || выражение_2 истинно, если истинным является значение первого выражения. В этом случае вычислять второе выражение нет необходи­мости.

В языке Java есть также тернарный оператор ? :, который иногда оказывается по­лезным.

условие ? выражение_1 : выражение_2

Если условие истинно, то вычисляется первое выражение, а если ложно — второе выражение. Например, х < у ? х : у возвращает меньшее из чисел х и у.

Побитовые операции

Работая с любыми целочисленными типами, можно применять операторы, непо­средственно обрабатывающие биты, из которых состоят целые числа. Это значит, что для определения состояния отдельных битов числа можно использовать маски. В языке Java есть следующие .побитовые операторы: & ("и") , | ("или"), ^ ("исклю­чающее или"), ~ ("не"). Например, если n — это целое число, то приведенное ниже вы­ражение равно единице только в том случае, если четвертый бит в двоичном пред­ставлении числа равен единице.

int fourthBitFromRight = (n & 8) / 8;

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

В применении к логическим переменным операторы & и | дают логические значения. 
Эти операторы аналогичны операторам && и | |, за исключением того, что вычисле­ние производится
по полной схеме, т.е. обрабатываются все элементы выражения.

В языке Java есть также операторы >> и <<, сдвигающие битовое представление числа вправо или влево. Эти операторы часто оказываются удобными, если нужно по­строить битовое представление на основе маски:

int fourthBitFromRight = (n & . {1 << 3)) >> 3;

В языке есть даже оператор >>>, заполняющий старшие разряды нулями, в то время как оператор >> восстанавливает в старших разрядах знаковый бит. Оператора <<< в языке Java нет.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Значение, присутствующее в правой части операторов побитового сдвига, сокраща­ется по модулю 32 (если левая часть является числом типа long, правая часть со­кращается по модулю 64). Например, значение выражения 1<<35 эквивалентно вы­ражению 1<<3, или 8.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языках C/C++ не определено, какой сдвиг выполняет оператор >> : арифметический (при котором знаковый бит восстанавливается) или логический (при котором старшие разряды заполняются нулями). Разработчики средств реализации языка могут выбрать тот вариант, который покажется им более эффективным. Это значит, что результат вы­полнения операции сдвига вправо в языке C/C++ определен лишь для неотрицательных чисел. В языке Java данная проблема разрешена путем ввода оператора >>>.

Математические функции и константы

Класс Math содержит набор математических функций, которые часто оказываются необходимыми при решении практических задач.

Чтобы извлечь квадратный корень из числа, применяют метод sqrt ().

double х = 4;
double у = Math.sqrt{x);;
System.out.println(y); // Выводит число 2.0.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Между методами println () и sqrt () есть небольшая разница. Метод println () принадлежит объекту system.out. (Напомним, что out — это объект, определенный в классе system и представляющий стандартное устройство вывода.) Метод sqrt () принадлежит классу Math, а не объекту. Такие методы называются статическими. Они будут рассматриваться в главе 4.

В языке Java нет оператора возведения в степень: для этого нужно использовать метод pow () класса Math. В результате выполнения следующей строки кода пере­менной у присваивается значение переменной х, возведенное в степень а .

double у = Math.pow(x,а);

Оба параметра метода pow (), а также возвращаемое им значение имеют тип double.

Класс Math содержит методы для вычисления обычных тригонометрических функций:

Math.sin()

Math.cos()

Math.tan()

Math.atan()

Math.atan2()

Кроме того, в него включены экспоненциальная и обратная к ней логарифмиче­ская функции (натуральный логарифм):

Math.ехр

Math.log

В данном классе также определены две константы — приближенное представление чисел п и e.

Math.PI

Math.E


ВАЖНОЕ ЗАМЕЧАНИЕ !
Начиная с JDK 5.0, при вызове методов для математических вычислений класс Math можно не указывать, включив вместо этого в начало файла с исходным кодом следую­щее выражение:

import static java.lang.Math.* ;

Например, при компиляции приведенной ниже строки кода ошибка не возникает.

System.out.printin("The square root of \u03C0 is " + sqrt(PI));

Подробно вопросы статического импортирования мы рассмотрим в главе 4.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Для повышения своей производительности функции в классе Math используют про­граммы из аппаратного модуля, предназначенного для вычислений с плавающей точ­кой. Если для вас не очень существенна скорость работы, а важнее получить пред­сказуемые результаты, используйте класс strictMath. Он реализует алгоритмы из библиотеки fdlibm, гарантирующей идентичность результатов на всех платформах. Исходные тексты программ, реализующих эти алгоритмы, можно найти на Web-странице https://www.netlib.org/fdlibm/index.html. (Поскольку в библиотеке fdlibm каждая функция определена неоднократно, в классе strictMath в соответ­ствии с IEEE 754 имена функций начинаются с буквы "е".)

Преобразование числовых типов

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

Файл:Cj2I.3.1.png

Рис. 3.1. Допустимые преобразования числовых типов

Шесть сплошных линий со стрелками обозначают преобразования, которые вы­полняются без потери информации. Три штриховые линии, также со стрелками, означают преобразования, при которых может произойти потеря точности. Напри­мер, количество цифр в длинном целом числе 123456789 превышает количество цифр, которое может быть представлено типом float. Число, преобразованное в тип float, имеет тот же порядок, но несколько меньшую точность.

int n = 123456789;
float f = n; // Содержимое f равно 1.234567892Е8.

Если два значения объединяются бинарным оператором (например, n+f, где n — целое число, a f — число с плавающей точкой), то перед выполнением операции оба операнда преобразовываются в числа одинакового типа.

  • Если хотя бы один из операндов имеет тип double, то второй тоже преобразо­вывается в число типа double.
  • В противном случае, если хотя бы один из операндов имеет тип float, то вто­рой тоже преобразовывается в тип float.
  • В противном случае, если хотя бы один из операндов имеет тип long, то вто­рой тоже преобразовывается в число типа long.
  • В противном случае оба операнда преобразовываются в числа типа int.

Приведение числовых типов

Как уже было сказано, при необходимости значения типа int автоматически пре­образовываются в значения типа double. С другой стороны, в ряде ситуаций число типа double должно рассматриваться как целое. Преобразования чисел в языке Java возможны, однако, разумеется, при этом может происходить потеря информации. Такие преобразования называются приведением типов (cast). Синтаксически приведе­ние типа задается парой скобок, внутри которых указывается желательный тип, а за­тем имя переменной. Например:

double х = 9.997; 
int nx = (int)x;

Теперь в результате приведения значения с плавающей точкой к целому типу пере­менная nх равна 9, поскольку при этом дробная часть числа отбрасывается.

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

double х = 9.997;
int nx = (int)Math.round(x);

Теперь переменная nx равна 10. При вызове метода round () по-прежнему нужно выполнять приведение (int), поскольку возвращаемое им значение имеет тип long.


ВАЖНОЕ ЗАМЕЧАНИЕ !
При попытке приведений типов результат может выйти за пределы допустимого диа­пазона. В этом случае произойдет усечение. Например, при вычислении выражения (byte) 300 будет получено значение 44.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Приведение логических значений к целым и наоборот невозможно. Это предотвра­щает появление ошибок. В редких случаях, когда действительно необходимо пред­ставить логическое значение в виде целого, можно использовать условное выраже­ние b ? 1 : 0.

Иерархия операторов

В табл. 3.4 приведена информация о приоритете операторов. Если скобки не ис­пользуются, сначала выполняются более приоритетные операции. Операторы, нахо­дящиеся на одном уровне иерархии, выполняются слева направо, за исключением операторов, имеющих правую ассоциативность, как показано в таблице. Например, поскольку оператор && приоритетнее | | выражение а && b | | с эквивалентно (а && b) || с. Так как оператор+= ассоциируется справа налево, выражение а +-b += с означает а += (b += с). В данном случае значение b += с (значение b после прибавления к нему значения с) присваивается переменной а.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В отличие от языков С и С++, язык Java не имеет оператора "запятая". Однако в опе­раторе for в качестве первого и третьего операторов можно использовать список выражений, разделенных запятыми.

Файл:Cj2I.3.4.png

Нумерованные типы

В некоторых случаях переменной должны присваиваться лишь значения из огра­ниченного набора. Предположим, например, что вы продаете пиццу четырех разме­ров: малого, среднего, большого и очень большого. Конечно, вы можете представить размеры целыми числами, например 1, 2, 3 и 4, или буквами S, М, L и X. Однако такой подход чреват ошибками. В процессе составления программы можно присвоить переменой недопустимое значение, например 0 или m.

В JDK 5.0 появилась возможность использовать в подобных ситуациях нумерован­ный тип. Например, вы можете включить в программу приведенное ниже выражение.

enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };

После этого можно определять переменные данного типа. Size S = Size.MEDIUM;

Переменная типа Size может содержать только предопределенные значения. Допустимо также значение null, указывающее на то, что данной переменной не при­своена никакая величина.

Подробно нумерованные типы будут рассмотрены в главе 5.

Строки

Строка Java— это последовательность символов Unicode. Например, строка "Java\u2122 " состоит из пяти символов: J, a, v, а и ™. В языке Java нет встроенного типа для строк. Вместо этого стандартная библиотека языка содержит класс String. Каждая строка, помещенная в кавычки, представляет собой экземпляр класса String.

String е = ""; // Пустая строка. 
String greeting = "Hello";

Кодовые точки и кодовые единицы

В языке Java строки реализованы как последовательности значений типа char. Как было сказано ранее, тип char позволяет задавать кодовые единицы, представ­ляющие кодовые точки Unicode в кодировке UTF-16. Наиболее часто используемые символы Unicode представляются одной кодовой единицей. Дополнительные симво­лы задаются парами кодовых единиц.

Метод length () возвращает число кодовых единиц для данной строки в кодиров­ке UTF-16. Ниже приведен пример использования данного метода.

String greeting = "Hello";
int n = greeting.length(); // Значение n равно 5.

Чтобы определить реальную длину, представляющую собой число кодовых точек, надо использовать следующий вызов:

int cpCount = greeting.codePointCount(0, greeting.length());

Метод s . char At (n) возвращает кодовую единицу в позиции п, где п находится в интервале от 0 до s. length () - 1. Ниже приведены примеры вызова данного метода.

char first = greeting.charAt(0); // Первый символ - 'Н' 
char last = greeting.charAt(4); // Последний символ - 'о'

Для получения i-й кодовой точки надо использовать приведенные ниже выражения.

int index = greeting.offsetByCodePoints(0, i); 
int cp = greeting.codePointAt(index);

Использование кодовых единиц может привести к недоразумениям. Рассмотрим приведенную ниже строку.

Z is the set of integers

Для представления символа Z используются две кодовые единицы UTF-16. Приве­денный ниже вызов метода даст не код пробела, а второй код символа Z. Чтобы избе­жать возникновения данной проблемы, не следует использовать тип char, так как он представляет символы на слишком низком уровне,

char ch = sentence.charAt(i);

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

int cp = sentence.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i += 2;
else i++;

Метод codePointAt () определяет, является ли кодовая единица первой или вто­рой частью дополнительного символа, и всегда возвращает корректный результат. Поэтому вы можете организовать просмотр строки и в обратном направлении.

i --
int cp = sentence.codePointAt(i);
if (Character. is Supplement aryCodePoint (cp)) i --;

Подстроки

С помощью метода substring () класса String можно выделить подстроку дан­ной строки. Например, в результате выполнения приведенного ниже кода формиру­ется строка "Hel".

String greeting = "Hello";
String s = greeting.substring(0, 3);

Второй параметр метода substring ( ) — это первая кодовая единица, которую не следует включать в состав подстроки. В данном примере мы хотим скопировать симво­лы из позиций 0,1 и 2, поэтому задаем при вызове метода substring ( ) значения 0 и 3.
Описанный способ вызова метода substring ( ) имеет положительную особен­ность: подсчет кодовых единиц в подстроке осуществляется исключительно просто. Строка s.substring {а, b) всегда содержит b - а кодовых единиц. Например, в сформированной выше строке "Hel" содержится 3 - 0 = 3 кодовых единиц.

Изменение строк

В классе String отсутствуют методы, которые позволяли бы изменять символы в существующих строках. Если, например, вы хотите изменить строку greeting с "Hello" на "Help ! ", то заменить требуемые два символа невозможно. Для разработ­чиков, использующих язык С, это звучит, по крайней мере, странно. "Как же модифи­цировать строку?" — спросят они. В Java внести необходимые изменения можно, выполнив конкатенацию подстроки greeting и символов "р! "

greeting = greeting.substring(0, 3) + "р!";

В результате текущим значением переменной greeting становится строка " Help! ".

Поскольку, программируя на языке Java, вы не можете изменять отдельные симво­лы в строке, в документации для описания объектов String используется термин неизменяемые, или немодифицируемие (immutable). Как число 3 всегда равно 3, так и строка "Hello " всегда содержит символы ' H ', 'е \ '1', '1' и 'о'. Изменить эти значения невозможно. Однако, как мы только что убедились, можно изменить содержимое строковой переменной greeting и заставить ее ссылаться на другую строку так же, как числовой переменной, в которой хранится число 3, можно присвоить число 4.
Не снижается ли при этом эффективность? Кажется, было бы намного проще из­менять символы, чем создавать новую строку заново. Возможно, это и так. Действи­тельно, неэффективно создавать новую строку посредством конкатенации строк "Hel" и "р! ". Однако неизменяемые строки имеют одно большое преимущество: компилятор может делать строки совместно используемыми.

Чтобы понять этот принцип, представьте, что в совместно используемом пуле нахо­дятся разные строки. Строковые переменные указывают объекты в этом пуле. При копи­ровании строки переменной и оригинал, и копия содержат одну и ту же последователь­ность символов. Логично не прибегать к дублированию строк, а поместить в переменные ссылки одну и ту же область памяти. Одним словом, разработчики языка Java решили, что эффективность совместного использования памяти перевешивает неэффективность ре­дактирования строк путем выделения подстрок и конкатенации.

Посмотрите на свою программу; мы подозреваем, что большую часть времени вы проводите, не изменяя строки, а сравнивая их. Разумеется, бывают случаи, когда непо­средственные манипуляции со строками более эффективны. (Одна из таких ситуаций возникает, когда нужно образовать строку из отдельных символов, поступающих из файла или клавиатуры.) Для этих ситуаций в языке Java предусмотрен отдельный класс StringBuffer, который будет описан в [Core Java 2 Том I Глава 12 | главе 12]. Если же эффективность обработки строк для вас не важна (как это часто случается во многих приложениях на языке Java), вы можете не применять класс StringBuffer, а использовать лишь класс String.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Когда программисты, работающие на языке С, видят строки в Java-программе, они обычно попадают в тупик, поскольку привыкли рассматривать строки как массивы символов:

char greeting[] = "Hello";

Это не совсем подходящая аналогия: строка в языке Java больше напоминает указа­тель *сhаг:

char* greeting = "Hello";

При замене содержимого greeting другой строкой Java-программа выполняет при­мерно следующее:

char* temp = malloc(6);
strncpy (temp, greeting, 4);
strncpy (temp + 4, "!", 2);
greeting = temp;

Разумеется, теперь переменная greeting указывает на строку "Help!". И даже самые убежденные поклонники языка С должны признать, что синтаксис Java более элегантен, чем последовательность вызовов функции strncpy (). А что будет, если мы присвоим строке greeting еще одно значение?

greeting = "Howdy";

Не останется ли занятой ранее выделенная память? К счастью, в языке Java есть ме­ханизм автоматической "сборки мусора". Если память больше не нужна, она вскоре будет освобождена.

Если вы программируете на языке С++ и применяете класс string, определенный в стандарте ANSI С++, вам будет намного легче работать с объектами string в языке Java. Объекты класса string в языке С++ также обеспечивают автоматическое выде­ление и освобождение памяти. Однако строки в языке С++ могут изменяться — отдельные символы в строке можно модифицировать.

Конкатенация

Язык Java, как и большинство языков программирования, дает возможность ис­пользовать знак + для объединения (конкатенации) двух строк.

String expletive = "Вставка";
String PG13 = "удаленная";
String message = expletive + PG13;

Код, приведенный выше, присваивает переменной message строку "Вставкаудаленная". (Обратите внимание на отсутствие пробела между словами: знак + объединяет в точностите строки, которые были заданы.)

При конкатенации строки со значением, отличным от строкового, это значение пре­образовывается в строку. Например, в результате выполнения приведенного ниже кода переменной rating присваивается строка " PG13 ".

int age = 13;
String rating = "PG" + age;

Это свойство широко используется в операторах вывода; например, приведенное ниже выражение выводит результат выполненных ранее вычислений (обратите вни­мание на пробел после слова Ответ).

System.out.println("Ответ " + answer);

Проверка совпадения строк

Чтобы проверить, совпадают ли две строки, следует использовать метод equals (). Приведенное ниже выражение возвращает значение true, если строки s и t равны ме­жду собой, в противном случае возвращается значение false.

s.equals(t)

Заметим, что в качестве s и t могут быть использованы как переменные, так и кон­станты. Например, следующее выражение вполне допустимо:

"Hellol".ecruals(greeting);

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

"Hello".equalsIgnoreCase("hello");

Для проверки строк на равенство нельзя применять оператор ==! Он лишь опреде­ляет, хранятся ли обе строки в одной и той же области памяти.

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

String greeting = "Hello"; // Инициализирует переменную greeting строкой.
if (greeting = "Hello") ...
// Возможно, это условие истинно,
if (greeting.substring(0, 3) == "Hel") ...
// Возможно, это условие ложно.

Если виртуальная машина всегда обеспечивает совместное использование одина­ковых строк, то для проверки их равенства можно применять оператор ==. Однако совместно использовать можно лишь константы, а не строки, являющиеся результа­тами таких операций, как + или substring. Следовательно, либо нужно навсегда отка­заться от проверок строк на равенство с помощью оператора ==, либо вы получите программу, содержащую наихудшую из возможных ошибок, проявляющуюся лишь время от времени, — ошибку, возникновение которой невозможно предсказать.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если вы привыкли использовать класс string в языке С++, будьте особенно внима­тельны при проверке совпадения строк. В классе string оператор == перегружен и позволяет проверять идентичность содержимого строк. Возможно, в языке Java раз­работчики напрасно отказались от возможности работать со строками как с число­выми значениями, однако это позволило сделать строки похожими на указатели. Разработчики могли переопределить оператор == для строк так, как они это сделали с оператором +. Что ж, каждый язык имеет свои недостатки.

Программисты, создающие продукты на языке С, никогда не используют для провер­ки строк на равенство оператор ==, вместо этого они вызывают функцию strcmp (). Метод compareTo() в языке Java представляет собой точный аналог функции strcmp (). Можно, конечно, использовать выражения вида

if (greeting.compareTo("Help") ==0) ...

Однако нам кажется, что применение метода equals () делает программу более удобочитаемой.

Класс String в языке Java содержит более 50 методов. Многие из них оказались очень полезными и используются довольно часто. Приведенный ниже фрагмент опи­сания API содержит наиболее полезные из них.

Время от времени мы будем включать в текст книги фрагменты описания прикладно­го программного интерфейса Java (Application Programming Interface — API). Каждый такой фрагмент начинается с имени класса, например java.lang.string. В данном случае java.lang — это пакет; механизм пакетов будет подробно рассмотрен в гла­ве 4. После имени класса следуют имена конкретных методов и их описание.

Мы также приводим номер версии, в которой был реализован описываемый класс. Если тот или иной метод был добавлен позже, это указывается отдельно.

Обычно в тексте книги не перечисляются все методы отдельного класса; мы ограничи­ваемся лишь наиболее часто используемыми методами.

Полный список методов можно найти в справочной системе.

API java.lang.string 1.0

  • char charAt(int index)

Возвращает символ, расположенный в указанной позиции. Вызывать этот ме­тод следует только в том случае, если вас интересуют низкоуровневые кодовые единицы.

  • int codePointAt(int index) 5.0

Возвращает кодовую точку, начало или конец которой находится в указанной позиции.

  • int offsetByCodePoints(int startlndex, int cpCount) 5.0

Возвращает индекс кодовой точки, которая определяется cpCount относи­тельно startlndex.

  • int compareTo(String other)

Возвращает отрицательное значение, если данная строка лексикографически предшествует строке other, положительное значение — если строка other предшествует данной строке, и 0 — если строки идентичны.

  • boolean endsWith(String suffix) Возвращает значение true, если строка заканчивается подстрокой suffix.
  • boolean equals(Object other) Возвращает значение true, если данная строка совпадает со строкой other.
  • boolean equalsIgnoreCase(String other)

Возвращает значение true, если данная строка совпадает со строкой other без учета регистра символов.

  • int indexOf(String str)
  • int indexOf(String str, int fromlndex)
  • int indexOf(int cp)
  • int indexOf{int cp, int fromlndex)

Возвращает индекс начала первой подстроки, совпадающей со строкой str, либо индекс указанной кодовой точки. Отсчет начинается с позиции 0 или formlndex.

Если указанная подстрока в составе строки отсутствует, возвращается значение, равное-1.

  • int lastlndexOf(String str)
  • int lastlndexOf(String str, int fromlndex)
  • int lastlndexOf(int cp)
  • int lastlndexOf(int cp, int fromlndex)

Возвращает начало последней подстроки, равной строке str, либо индекс ука­занной кодовой точки. Отсчет начинается с позиции 0 или formlndex. Если указанная подстрока в составе строки отсутствует, возвращается значение, равное-1.

  • int length() Возвращает длину строки.
  • int codePointCount(int startlndex, int endlndex) 5.0

Возвращает число кодовых точек между startlndex и endlndex -1. Половина па­ры, обозначающей дополнительный индекс, считается как полноправная кодо­вая точка.

  • String replace(CharSequence oldString, CharSecruence newString)

Возвращает новую строку, которая получается путем замены всех подстрок, соответствующих oldString, на строку newString. В качестве параметров CharSequence могут выступать объекты String или StringBuilder.

  • boolean startWith(String prefix)

Возвращает значение true, если строка начинается подстрокой prefix.

  • String substring(int beginlndex)
  • String substring(int beginlndex, int endlndex)

Возвращает новую строку, состоящую из всех кодовых единиц, начиная с пози­ции Beginlndex и заканчивая концом строки или позицией endlndex -1.

  • String toLowerCase()

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

  • String toupperCase()

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

  • String trim ()

Возвращает новую строку, из которой исключены все предшествующие и за­вершающие пробелы.

Интерактивная документация по API

Как вы уже знаете, класс String имеет много методов. Более того, в стандартной библиотеке существует несколько сотен классов, содержащих огромное количество методов. Совершенно невозможно запомнить всю информацию, которая может по­надобиться при написании программ. Следовательно, очень важно уметь пользовать­ся интерактивной справочной системой, содержащей документацию об API, и нахо­дить нужные классы и методы. Документация является составной частью набора ин­струментальных средств JDK. Она представлена в формате HTML. Обратитесь спомощью вашего браузера к документу docs/api/index.html в каталоге, в кото­ром установлен JDK. Вы увидите информацию, показанную на рис. 3.2.

Файл:Cj2I.pic3.2.png

Окно браузера разделено на три фрейма. В небольшом фрейме в верхнем левом углу отображаются имена всех доступных пакетов. Под ним во фрейме побольше перечис­лены все классы. Щелкните мытью на любом из имен классов, и информация об этом классе будет показана в большом фрейме, расположенном справа (рис. 3.3). Например, чтобы получить дополнительные сведения о методах класса String, прокрутите содер­жимое левого нижнего фрейма, пока не увидите ссылку String, и щелкните на ней.
Затем прокрутите содержимое правого фрейма до тех пор, пока не увидите крат­кое описание всех методов. Методы расположены в алфавитном порядке (рис. 3.4). Щелкните на имени интересующего вас метода, чтобы получить его детальное описа­ние (рис. 3.5). Например, если вы щелкнете на ссылке compareToIgnoreCase, то по­лучите описание метода с этим именем.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Сразу сделайте закладку в браузере, указывающую на документ

docs/api/index.html.

Файл:Cj2I.pic3.3.png

Файл:CJ2I.pic3.4.png

Файл:CJ2I.pic3.5.png

Ввод и вывод

Для того чтобы немного "оживить" программы, рассматриваемые в качестве при­меров, организуем ввод информации и форматирование выходных данных. В совре­менных приложениях для ввода используются средства графического пользователь­ского интерфейса, однако в данный момент вы еще не обладаете знаниями, достаточ­ными для формирования интерфейсных элементов. Поскольку наша текущая цель — лучше узнать языковые средства Java, мы ограничимся вводом и выводом посредством консоли. Вопросы программирования графического интерфейса будут подробно рас­смотрены в главах 7 - 9.

Чтение входных данных

Вы уже знаете, что информацию можно вывести на стандартное устройство вывода (т.е. в консольное окно), вызвав метод System.out.println ().

Однако до появления JDK 5.0 отсутствовали удобные средства чтения информации со стандартного устройства ввода (т.е. клавиатуры). К счастью, в настоящее время проблемы, возникающие при этом, практически разрешены.

Для того чтобы организовать чтение информации с консоли, вам надо создать объект Scanner и связать его со стандартным входным потоком

System. in.
Scanner in = new Scanner(System.in);

Сделав это, вы получите в свое распоряжение многочисленные методы класса Scanner, предназначенные для чтения входных данных. Например, метод nextLine () обеспечивает прием строки текста.

System.out.print("Как вас зовут? "); 
String name = in.nextLine();

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

String firstName = in.next();

Для чтения целочисленного значения предназначен метод nextlnt ( ).

System.out.print("Сколько вам лет? "); 
int age = in.nextlnt();

Как нетрудно догадаться, метод nextDouble ( ) читает очередное число в формате с плавающей точкой.

Программа, код которой представлен в листинге 3.2, запрашивает имя пользова­теля и его возраст, а затем выводит сообщение типа

Сау, в следующем году вам будет 46

В первой строке содержится выражение

import java.util.*;

Класс Scanner принадлежит пакету java.util package. Если вы собираетесь использовать в программе класс, не содержащийся в базовом пакете java. lang, вам надо включить в состав кода директиву import. Подробно пакеты и директива import будут рассмотрены в главе 4.

Листинг 3.2. Содержимое файла InputTest.java

/**
@version 1.10 2004-02-10
@author Cay Horstmann
*/

 
import java.util.*;
 
public class InputTest
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
 
// get first input
System.out.print("What is your name? ");
String name = in.nextLine();
 
// get second input
System.out.print("How old are you? ");
int age = in.nextInt();
 
// display output on console
System.out.println("Hello, " + name + ". Next year, you'll be " + (age + 1));
}
}


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если вы не имеете возможности работать с JDK 5.0 или более поздними версиями, то организовать ввод данных будет несколько труднее. Вам придется использовать диалоговое окно, показанное на рис. 3.6.
String inut = JOptionPane.showInputDialog(promptString);

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

Например, следующая строка кода позволяет запросить имя пользователя вашей программы:

String name = JOptionPane.showInputDialog("Как вас зовут?");

Чтобы считать число, нужно выполнить более сложную работу. Метод JOptionPane. showInputDialog {) возвращает строку, а не число. Для преобразо­вания этой строки в число нужно использовать метод integer .parseint () или Double. parseDouble (). Пример такого преобразования приведен ниже.

String input = JOptionPane.showInputDialog("Сколько вам лет?"); int age = Integer.parseint(input);

Если пользователь введет число 45, то строковой переменной input будет присвое­на строка "45". Метод integer. parseint () преобразовывает строку в соответст­вующее число, т.е. 45.

Класс JOptionPane принадлежит пакету javax. swing, поэтому вам надо включить в состав исходного файла следующее выражение:

import j avax.swing.*;

И наконец, если ваша программа вызывает метод JOptionPane. showInputDialog (), вам надо завершить ее вызовом system, exit (0). Дело в том, что при создании диало­гового окна формируется новый управляющий поток. Когда выполнение метода main () оканчивается, автоматического завершения потока не происходит. Для того чтобы пре­кратить работу всех потоков, надо вызвать метод system.exit (). Дополнительную информацию о потоках можно найти в главе 1 второго тома. Следующая программа обеспечивает те же возможности, что и программа, представленная в листинге 3.2, но не использует средства JDK 5.0.

import javax.swing.*; 
public class inputTest {
 
public static void main(String[] args) {
 
String name = JOptionPane.showInputDialog(
 
"Как вас зовут?");
 
String input = JOptionPane.showInputDialog(
 
"Сколько вам лет?");
 
int age = Integer.parseint(input); System.out.printlnfname +
 
", в следующем году вам будет " + (age + 1));
 
System.exit (0) ;
 
}
 
}

Файл:Cj2I.pic3.6.png

Шаблон:API

Шаблон:API

Шаблон:API

Форматирование выходных данных

Число х можно вывести на консоль с помощью выражения System, out .printin (х). В результате на экране отобразится число с максимальным количеством значащих цифр, допустимых для данного типа. Например, в результате выполнения приведенного ниже фрагмента кода на экран будет выведено число 3333.3333333333335.

double х = 10000.0 /3.0; System.out.print(x);

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

До появления JDK 5.0 процесс форматирования чисел был сопряжен с определен­ными трудностями. В JDK 5.0 был реализован метод printf (), привычный всем про­граммистам, использующим язык С. Например, с помощью приведенного ниже выра­жения мы можем вывести значение х в виде числа, размер поля которого составляет 8 цифр, а дробная часть равна двум цифрам. (Число цифр дробной части называют также точностью.)

System.out.printf("%8.2f", x);

В результате на экран будет выведено, не считая ведущих пробелов, семь символов:

3333.33

Метод printf () позволяет задавать произвольное число параметров. Пример вы­зова с несколькими параметрами приведен ниже.

System.out.printf("%s, в следующем году вам будет %d", name, age);

Каждый спецификатор формата, начинающийся с символа %, заменяется соответ­ствующим параметром. Символ преобразования, которым завершается спецификатор формата, задает тип форматируемого значения: f — число с плавающей точкой; s — строка; d — десятичное число.

Символы преобразования описаны в табл. 3.5.

Файл:Cj2I.tab3.5.png

В составе спецификатора формата могут присутствовать флаги, управляющие форматом выходных данных. Назначение всех флагов описано в табл. 3.6. Например, запятая, используемая в качестве флага, формирует разделитель групп. Так, в резуль­тате выполнения приведенного ниже выражения на экран будет выведена строка 3,333.33.

System.out.printf ("%, .2f", 10000.0 / 3.0);

В одном спецификаторе формата можно использовать несколько флагов, напри­мер последовательность символов "%, {.2Г указывает на то, что при выводе будут использованы разделители групп, а отрицательные числа будут помещены в скобки).


ВАЖНОЕ ЗАМЕЧАНИЕ !
Преобразование s можно использовать для форматирования любого объекта. Если этот объект реализует интерфейс Formattable, вызывается метод formatTo(). В против­ном случае для преобразования объекта в строку применяется метод toString(). Метод toString() будет обсуждаться в главе 5, а интерфейсы — в главе 6.

Для создания форматированной строки без вывода ее можно использовать стати­ческий метод String.format ().

String message = String.format("%s, в следующем году вам будет %d", name, age);

Файл:Cj2I.tab3.6.png

Несмотря на то, что тип Date будет, подробно рассмотрен лишь в главе 4, нам, чтобы закончить разговор о методе printf (), желательно подробно рассмотреть средства форматирования даты и времени. Для этой цели используется последова­тельность из двух символов, начинающаяся с буквы t, за которой следует одна из букв, приведенных в табл. 3.7. Пример такого форматирования приведен ниже.

System.out.printf("%tc", new Date());

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

Mon Feb 09 18:05:19 PST 2004

Как видно в табл. 3.7, некоторые форматы предполагают отображение лишь от­дельных компонентов даты — дня или месяца. Было бы неразумно многократно зада­вать дату лишь для того, чтобы отформатировать различные ее элементы. По этой причине в строке, определяющей формат, может задаваться индекс форматируемого параметра. Индекс должен следовать непосредственно за символом % и завершаться знаком Пример использования индекса приведен ниже.

System.out.printf("%1$s %2$tB %2$te, %2$tY", "Дата:", newDate());

Файл:Cj2I.tab3.7.png
Файл:Cj2I.tab3.7 2.png

В результате обработки данного выражения будет выведена следующая строка:

Дата: February 9, 2004

Вы также можете использовать флаг <, который означает, что форматированию подлежит тот же параметр, который был сформатирован последним. Так, приведен­ное ниже выражение дает тот же результат, что и рассмотренное ранее.

System.out.printf("%s %tB %<te, %<tY", "Дата:", new Date());


ВАЖНОЕ ЗАМЕЧАНИЕ !
Индексы начинаются с единицы. Так, выражение %1$ задает форматирование перво­го параметра. Это сделано для того, чтобы избежать конфликтов с флагом 0.

В данном разделе были рассмотрены не все особенности метода printf (). На рис. 3.7 показана диаграмма, которая строго задает синтаксис спецификатора формата.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Ряд правил форматирования зависит от специфических условий конкретной страны или региона. Так, например, в Германии разделителем групп является не запятая, а точка, а вместо Monday выводится имя Montag. Вопросы интернационализации приложений будут рассмотрены во втором томе.

Файл:Cj2I.pic3.7.png


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если вы работаете с одной из версий, предшествующих JDK 5.0, вместо метода printf () вам придется использовать классы NumberFormat и DateFormat.

Поток управления

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


ВАЖНОЕ ЗАМЕЧАНИЕ !
Поток управления в языке Java построен точно так же, как и в языках С и С++, за ис­ключением двух особенностей. В нем нет оператора goto, однако существует версия оператора break с метками, который можно использовать для выхода из вложенного цикла (в языке С для этого пришлось бы применять оператор goto). Кроме того, в JDK 5.0 реализован вариант оператора for, который не имеет аналогов в С или С++. Его можно сравнить с оператором foreach в С#.

Блоки

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

Блок, или составной оператор, — это произвольное количество простых операто­ров языка Java, заключенных в фигурные скобки. Блоки определяют область видимо­сти переменных. Блоки могут быть вложенными один в другой. Ниже приведен пример блока, вложенного в другой блок в методе main ().

public static void main(String[] args) {
int n;
...
{
int k;
...
} // Переменная к определена только в этом блоке.
}

В языке Java невозможно объявить переменные с одним именем в двух вложенных блоках. Например, приведенный ниже фрагмент кода содержит ошибку и не будет скомпилирован.

public static void raain(String[] args) {
int n; {
int k;
int n; // Ошибка — невозможно переопределить переменную // n во внутреннем блоке.
}
}


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ переменные во вложенных блоках можно переопределять. Внутреннее определение маскирует внешнее. Это может привести к ошибкам, поэтому в языке Java подобный подход не реализован.

Условные выражения

Условный оператор в языке Java имеет следующий вид: if (условие) оператор

Условие должно указываться в скобках.

В языке Java, как и в большинстве других языков программирования, часто необ­ходимо выполнять много операторов в зависимости от выполнения одного условия. В этом случае используется блок, формируемый следующим образом:

{
Выражение_1; Выражение_2;
}

Например,

if (yourSales >= target) {
performance = "Удовлетворительно"; bonus = 100;
}

В этом фрагменте все операторы, помещенные в фигурные скобки, будут выполнены, если значение переменной yourSales больше значения переменной target или равно ему (рис. 3.8).


ВАЖНОЕ ЗАМЕЧАНИЕ !
Блок (иногда называемый составным оператором) позволяет включать несколько (простых) операторов в любую структуру языка Java, которая в ином случае состояла бы лишь из одного (простого) оператора.

Файл:Cj2I.pic3.8.png

Условное выражение в языке Java может также иметь приведенную ниже форму. Его действие показано на рис. 3.9.

if {условие) оператор_1 else оператор_2

Например:

if (yourSales >= target) {
performance = "Удовлетворительно");
bonus = 100 + 0.01 * (yourSales - target);
}
else
{
performance = "Неудовлетворительно"; bonus = 0;
}

Часть else не является обязательной. Она объединяется с ближайшим операто­ром if. Таким образом, в следующем фрагменте кода оператор else соответствует второму оператору if:

if (х <= 0) if (х == 0) sign = 0; else sign = -1;

В программах часто встречаются также операторы типа if ... else (рис. З.9). Пример такой языковой конструкции приведен ниже.

if {yourSales >= 2 * target)
performance = "Великолепно"; bonus = 1000;
else if (yourSales >= 1.5 * target)
performance = "Хорошо"; bonus = 500;
else if (yourSales >= target)
performance = "Удовлетворительно"; bonus = 100;
else
System.out.println("Bы уволены");

Файл:Cj2I.pic3.9.png
Файл:Cj2I.pic3.10.png

Неопределенные циклы

Цикл while обеспечивает выполнение выражения (или группы операторов, составляющих блок) до тех пор, пока условие равно true. Данный цикл записывается в следующем виде:

while (условие) оператор

Тело цикла while не будет выполнено ни разу, если его условие изначально равно false (рис. 3.11).

В листинге 3.3 показана программа, подсчитывающая, сколько лет надо вносить деньги на счет, чтобы накопить заданную сумму. Считается, что каждый год вносится одна и та же сумма и процентная ставка не изменяется.

[[Файл:Cj2I.pic3.11.png

Условие цикла while проверяется в самом начале. Следовательно, возможна ситуация, при которой код, содержащийся в блоке, не будет выполнен никогда. Если вы хотите, чтобы блок выполнялся хотя бы один раз, проверку условия нужно пере­нести в конец. Это можно сделать с помощью цикла do/while, который записывается следующим образом:

do оператор while (условие) ;

Условие проверяется лишь после выполнения тела цикла. Затем тело цикла повторяет­ся, вновь проверяет условие и т.д. Например, код в листинге 3.4 вычисляет новый баланс вашего счета, а затем спрашивает вас, не собираетесь ли вы на заслуженный отдых.

do
{
balance += payment;
double interest = balance * interestRate / 100; balance += interest; year++;
// Вывести текущий баланс
// Спросить, не собирается ли пользователь на пенсию, // и получить ответ.
}
while (input.equals("N"));

Если пользователь отвечает N", цикл повторяется (рис. 3.12). Эта программа явля­ется хорошим примером применения циклов, которые нужно выполнить хотя бы один раз.

Листинг 3.3. Содержимое файла Retirement. java

/**
@version 1.20 2004-02-10
@author Cay Horstmann
*/

 
import java.util.*;
 
public class Retirement
{
public static void main(String[] args)
{
// read inputs
Scanner in = new Scanner(System.in);
 
System.out.print("How much money do you need to retire? ");
double goal = in.nextDouble();
 
System.out.print("How much money will you contribute every year? ");
double payment = in.nextDouble();
 
System.out.print("Interest rate in %: ");
double interestRate = in.nextDouble();
 
double balance = 0;
int years = 0;
 
// update account balance while goal isn't reached
while (balance < goal)
{
// add this year's payment and interest
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
years++;
}
 
System.out.println("You can retire in " + years + " years.");
}
}

Файл:Cj2I.pic3.12.png

Листинг 3.4. Содержимое файла Retirement2. java

/**
@version 1.20 2004-02-10
@author Cay Horstmann
*/

 
import java.util.*;
 
public class Retirement2
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
 
System.out.print("How much money will you contribute every year? ");
double payment = in.nextDouble();
 
System.out.print("Interest rate in %: ");
double interestRate = in.nextDouble();
 
double balance = 0;
int year = 0;
 
String input;
 
// update account balance while user isn't ready to retire
do
{
// add this year's payment and interest
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
 
year++;
 
// print current balance
System.out.printf("After year %d, your balance is %,.2f%n", year, balance);
 
// ask if ready to retire and get input
System.out.print("Ready to retire? (Y/N) ");
input = in.next();
}
while (input.equals("N"));
}
}

Определенные циклы

Цикл for — очень распространенная языковая конструкция. В ней число повторе­ний контролируется переменной, выполняющей роль счетчика и обновляемой на ка­ждой итерации. Приведенный ниже цикл выводит на экран числа от 1 до 10. Его вы­полнение показано на рис. 3.13.

for (int i = 1; i <= 10; i++) System.out.println(i);

Файл:Cj2I.pic3.13.png

Первый элемент оператора for обычно выполняет инициализацию счетчика, второй формулирует условие выполнения тела цикла, а третий определяет способ обновления счетчика.

Хотя в языке Java, как и в языке С++, элементами оператора цикла for могут быть практически любые операторы, существуют неписанные правила, согласно которым все три элемента оператора for должны только инициализировать, проверять и об­новлять один и тот же счетчик. Если не придерживаться этих правил, полученный код станет совершенно непригоден для чтения.

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

for (int і = 10; і > 0; -і)
System.out.prinln("Обратный отсчет System.out.println("Старт!);
+ і);


ВАЖНОЕ ЗАМЕЧАНИЕ !
Будьте осторожны, проверяя в цикле равенство двух чисел с плавающей точкой. Представленный ниже цикл может никогда не завершиться.

for (double X = 0; X != 10; х += 0.1) ...
Из-за ошибок округления окончательный результат никогда не будет достигнут. Например, в приведенном выше цикле переменная х изменит свое значение с 9.999999999999998 на 10.099999999999998. Так произойдет потому, что для чис­ла 0.1 не существует точного двоичного представления.

При объявлении переменной в первой части оператора for ее область видимости простирается до конца тела цикла.

for (int i = 1; i <= 10; i++) {
}
// Здесь переменная i больше не определена.

Если переменная определена в операторе for, ее нельзя использовать вне этого цикла. Следовательно, если вы хотите использовать конечное значение счетчика вне цикла for, эту переменную нужно объявлять до начала цикла!

int i;
for (i = 1; i <= 10; f++) {
} // Здесь переменная i по-прежнему доступна.

С другой стороны, можно объявить переменные, имеющие одинаковое имя в раз­ных циклах for.

for (int i = 1; i <= 10; i++) {
}
for (int i = 11; i <= 20; i++) // Переопределение переменной i. {
}

Действия, выполняемые посредством цикла for, можно реализовать с помощью цикла whilе. Приведенные ниже два фрагмента кода эквивалентны.

for (int i = 10; i > 0; i--)
System.out.printin("Обратный отсчет . . . " + i);

и

int i = 10; while (i > 0) {
System, out.printin ("Обратный отсчет . . . " + i) ;
i--;
}

В листинге 3.5 показан типичный пример использования цикла for.

Эта программа вычисляет вероятность выигрыша в лотерее. Например, если нужно угадать 6 номеров из 50, количество возможных вариантов равно (50x49x48x47x46x45)/ (1x2x3x4x5x6), так что шанс выиграть равен 1 из 15890700.Удачи!

В общем случае, если нужно угадать k номеров из n, количество возможных вари­антов равно следующему выражению:
(nx(n-1)x(n-2)x...х (n-k+1) / (1Х2ХЗХ...xk)

Оно вычисляется с помощью следующего цикла for:

int lotteryOdds =1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n-i+1) / i;


ВАЖНОЕ ЗАМЕЧАНИЕ !
В конце данной главы будет рассмотрен "обобщенный цикл for", называемый также циклом "for each". Эта языковая конструкция была введена в JDK 5.0.

Листинг 3.5. Содержимое файла LotteryOdds.java

/**
@version 1.20 2004-02-10
@author Cay Horstmann
*/

 
import java.util.*;
 
public class LotteryOdds
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
 
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
 
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
 
/*
compute binomial coefficient
n * (n - 1) * (n - 2) * . . . * (n - k + 1)
-------------------------------------------
1 * 2 * 3 * . . . * k
*/

 
int lotteryOdds = 1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n - i + 1) / i;
 
System.out.println("Your odds are 1 in " + lotteryOdds + ". Good luck!");
}
}

Многовариантное ветвление — оператор switch

Конструкция if /else может оказаться неудобной, если вам необходимо реализо­вать выбор из многих вариантов. В языке Java есть оператор switch, эквивалентный одноименному оператору, используемому в языках С и С++.
Например, программируя выбор из четырех альтернативных вариантов (рис. 3.14), можно использовать следующий код:

Scanner in = new Scanner(System.in);
System.out.print("Select an option (1, 2, 3, 4) ");
int choice = in.nextlnt();
switch (choice)
{
case 1:
break; case 2:
break;
case 3: '
break; case 4:
break; default:
// Неверный выбор.
break;
}

Выполнение начинается с метки case, соответствующей значению переменной choice, и продолжается до очередного оператора break или конца оператора switch. Если ни одна метка не совпадает со значением переменной, выполняется раздел default (если он предусмотрен).
Заметим, что метка case должна быть целочисленной. Нельзя проверять строки. Например, в следующем фрагменте кода сделана ошибка:

String input = ... ; switch (input) // ОШИБКА {
case "A": // ОШИБКА break;
}


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если вы забудете добавить в конце раздела case оператор break, возможно после­довательное выполнение нескольких разделов case. Совершенно очевидно, что эта ситуация чревата ошибками, поэтому мы никогда не используем оператор switch в своих программах.

Прерывание потока управления

Несмотря на то что разработчики языка Java сохранили зарезервированное слово goto, они решили не включать его в язык. В принципе применение операторов goto считается признаком плохого стиля программирования. Некоторые программисты считают, что борьба с использованием оператора goto ведется недостаточно активно (см., например, известную статью Дональда Кнута (Donald Knuth) "Structured Programming with goto statements"). Они считают, что применение операторов goto может приводить к ошибкам, однако в некоторых случаях нужно выполнять прежде­временный выход из цикла. Создатели языка Java согласились с их аргументами и даже добавили в язык новый оператор для поддержки такого стиля программирования — оператор break с меткой.

Рассмотрим обычный оператор break, в котором не применяется метка. Для вы­хода из цикла можно применять тот же оператор, который использовался для выхода из тела оператора switch. Пример использования оператора break приведен ниже.

while (years <= 100) {
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
if(balance >= goal) break;
years++;
}

Теперь выход из цикла осуществляется при выполнении одного из двух условий: years > 100 — в начале цикла либо balance >= goal — внутри цикла. Разумеется, те же действия можно было бы вычислить и без применения оператора break.

while (years <= 100 && balance < goal) {
balance += payment;
double interest = balance * interestRate / 100; balance += interest;
if(balance < goal)
years++;
}

Заметим, однако, что проверка условия balance < goal в данном варианте про­граммы повторяется дважды. Оператор break позволяет избежать этого.

В отличие от языка С++, язык Java содержит также оператор break сметкой, обес­печивающий выход из вложенных циклов. Если во вложенном цикле выполняется некоторое условие, возможно, потребуется выйти из всех вложенных циклов. Про­граммировать дополнительные условия для проверки каждого вложенного цикла попросту неудобно.

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

Scanner in = new Scanner(System.in);
int n;
read_data:
while (. . .) // Этот цикл помечен
{
Поток управления 109
for (. . .) // Этот цикл не помечен
{
System.out.print("Введите число >= 0: ");
n = in.nextlnt();
if (n < 0) // Если это не произойдет, цикл продолжится break read„data;
// Прерывание цикла
}
// Данное выражение выполняется сразу же после оператора break с меткой if (п < 0) // Проверка на „наличие недопустимой ситуации
// Обработка недопустимой ситуации
else
{
// Действия в результате нормального выполнения программы
}

Если в программу было введено неверное число, оператор break с меткой выпол­нит переход в конец помеченного блока. В этом случае необходимо проверить, нор­мально ли осуществлен выход из цикла, или он произошел в результате выполнения оператора break.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Это может показаться странным, но метку можно связать с любым оператором, даже оператором if или блоком. Соответствующий пример приведен ниже.
метка: {
if (условие) break label; // Выход из блока.
}
// При выполнении оператора break управление передается в эту точку.

Итак, если вам крайне нужен оператор goto, вы можете поместить блок, из которого нужно выйти, и применить оператор break! Естественно, мы не рекомендуем исполь­зовать этот способ. Заметим также, что подобным образом можно выйти из блока, но невозможно войти в него.

Итак, если вам крайне нужен оператор goto, вы можете поместить блок, из которого нужно выйти, и применить оператор break! Естественно, мы не рекомендуем исполь­зовать этот способ. Заметим также, что подобным образом можно выйти из блока, но невозможно войти в него.

Существует также оператор continue, который, подобно break, прерывает нор­мальное выполнение программы. Оператор continue передает управление в начало текущего вложенного цикла. Пример использования данного оператора приведен ниже.

while (sum < goal) {
String input = JOptionPane.showInputDialog("Введите число"); n = integer.Parselnt(input) ,* if (n < 0) continua;
sum += n; //He выполняется, если n < 0.
}

Если n < 0, то оператор continue выполняет переход в начало цикла, пропуская оставшуюся часть текущей итерации.

Если оператор continue используется в цикле for, он передает управление оператору увеличения счетчика цикла.

for (count = 0; count < 100; count ++) {
String input = JOptionPane.showInputDialog ("Введите число");
n = Integer.parselnt(input);
if (n < 0) continue;
sum += n; // He выполняется, если n < 0.
}

Если n < 0, то оператор continue выполнит переход к оператору count++.

В языке Java есть также оператор continue с меткой, передающий управление заголовку оператора цикла, помеченного соответствующей меткой.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Многие программисты считают, что операторы break и continue неоправданно усложняют текст программы. Применять эти операторы вовсе не обязательно — те же действия можно реализовать, не прибегая к ним. В этой книге мы нигде не ис­пользуем ни break, ни continue.

Работа с большими числами

Если для решения задачи недостаточно точности встроенных целочисленных типов и чисел с плавающей точкой, можно обратиться к классам Biglnteger HBigDecimal, принадлежащим пакету java.matJi. Эти классы предназначены для выполнения действий с числами, состоящими из произвольного количества цифр. Классы Biglnteger и BigDecimal реализуют арифметические операции произволь­ной точности соответственно для целых и действительных чисел.

Для преобразования обычного числа в число с произвольной точностью (называемое также большим числом) используется статический метод valueOf ():

Biglnteger а = Biglnteger.valueof(100);

К сожалению, к большим числам нельзя применять обычные математические опера­торы, например + или *. Вместо этого надо использовать методы add () и multiply () из соответствующих классов.

Biglnteger с = a.add(b); // с = а + b
Biglnteger d = с.multiply(b.add(Biglnteger.value.Of(2))); // d = с * (b + 2)


ВАЖНОЕ ЗАМЕЧАНИЕ !
В отличие от языка С++, язык Java не поддерживает перегрузку операторов. Поэтому разработчики класса Biglnteger были лишены возможности переопределить опе­раторы + и * для методов add () и multiply () в классе Biglnteger.

В листинге 3.6 показана модифицированная программа для подсчета шансов выиграть в лотерее (ее исходный вариант см. в листинге 3.5). Теперь эта программа может работать с большими числами. Например, если вам предложили сыграть в лотерее, в которой нуж­но угадать 60 чисел из 490 возможных, то эта программа сообщит вам, что шанс выиграть равен 1 из 7163958434619955574151162225400929334117176127892634934933510134594811 04668848. Удачи!

Программа, представленная в листинге 3.5, вычисляла следующее выражение:

lotteryOdds = lottery * (n - i + 1) / i;

При работе с большими числами соответствующая строка кода выглядит так:

lotteryOdds = lotteryOdds.multiply(Biglnteger.valueOf(n-i+1)
.divide(Biglnteger.valueOf(i));

Листинг 3.6. Содержимое файла BiglntegerTest .Java

/**
@version 1.20 2004-02-10
@author Cay Horstmann
*/

 
import java.math.*;
import java.util.*;
 
public class BigIntegerTest
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
 
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
 
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
 
/*
compute binomial coefficient
n * (n - 1) * (n - 2) * . . . * (n - k + 1)
-------------------------------------------
1 * 2 * 3 * . . . * k
*/

 
BigInteger lotteryOdds = BigInteger.valueOf(1);
 
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds
.multiply(BigInteger.valueOf(n - i + 1))
.divide(BigInteger.valueOf(i));
 
System.out.println("Your odds are 1 in " + lotteryOdds + ". Good luck!");
}
}

Шаблон:API

Шаблон:API

Массивы

Массив — это структура данных, в которой хранятся величины одинакового типа. Доступ к отдельному элементу массива осуществляется с помощью целочисленного индекса. Например, если а — массив целых чисел, то значение выражения а [ i ] равно i-му целому числу в массиве.

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

int [] а;

Однако этот оператор лишь объявляет переменную а, не инициализируя ее. Чтобы создать массив, нужно применить оператор new.

int[] а = new int[100];

Этот оператор создает массив, состоящий из 100 целых чисел.

{{


ВАЖНОЕ ЗАМЕЧАНИЕ !
Объявить массив можно двумя способами:
int[] а;
или
int а [ ] ;

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

Элементы сформированного выше массива нумеруются от 0 до 99 (а не от 1 до 100). После создания массив можно заполнять конкретными значениями, в частности, это можно делать в цикле.

int [] а = new int [100] ;
for (int i = 0; i < 100; i++)
a [i] = i; // Заполняет массив числами от 0 до 99.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если, создав массив, состоящий из 100 элементов, вы попытаетесь обратиться к а [100] (или любому другому элементу, индекс которого выходит за пределы диапазона от 0 до 99), программа прекратит работу, поскольку будет сгенерировано исключение, соответствующее выходу индекса массива за пределы допустимого диапазона.

Чтобы подсчитать количество элементов в массиве, используйте выражение имя_массива. length. Например:

for (int i = 0; i < a.length; i++) System,out.println(a[i]);

После создания массива изменить его размер невозможно (хотя можно, конечно, изменять его отдельные элементы). Если'в ходе выполнения программы необходимо часто изменять размер массива, лучше использовать другую структуру данных, назы­ваемую списком.

Цикл "for each "

В JDK 5.0 был реализован новый цикл, позволяющий перебирать все элементы массива (а также любого другого набора данных), не применяя счетчик.
Новый вариант цикла for записывается следующим образом:
for (переменная : набор_данных) выражение

При обработке цикла переменной последовательно присваивается каждый эле­мент набора данных, после чего выполняется выражение (или блок). В качестве набора данных может использоваться массив либо экземпляр класса, реализующего интерфейс Iterable, например ArrayList. Списки будут обсуждаться в главе 5, а интерфейс Iterable — в главе 2 второго тома.

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

for (int element : а)
System.out.println(element);

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

Действия данного цикла можно кратко описать как "обработка каждого элемента из а". Разработчики языка Java рассматривали возможность применения в качестве идентификатора данного цикла ключевых слов f oreach и in. Однако данный тип цикла появился намного позже основных языковых средств Java, и введение нового ключевого слова привело бы к необходимости изменять исходный код некоторых готовых приложений, содержащих переменные или методы с такими именами.

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

for (int i = 0; i < a.length; i++) System.out.println(a[i]) ;

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


ВАЖНОЕ ЗАМЕЧАНИЕ !
Переменная цикла в выражении "for each" перебирает не значения индекса, а эле­менты массива.

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

Инициализация массивов и анонимные массивы

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

int[] smallPrimes = { 2, 3, 5, 7, 11, .13};

Заметьте, что в этом случае оператор new не нужен.

Кроме того, можно даже инициализировать массив, не имеющий имени, или анонимный массив.

new int[] {16, 19, 23, 29, 31, 37};

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

Например, выражение

smallPrimes = new int{ 17, 19, 23, 29, 31, 37 };

представляет собой сокращенную запись выражения

int[] anonymous = { 17, 19, 23, 29, 31, 37 }; 
smallPrimes = anonymous;


ВАЖНОЕ ЗАМЕЧАНИЕ !
При необходимости можно создать массив нулевого размера. Такой массив может оказаться полезным при написании метода, возвращающего массив, который в неко­торых случаях оказывается пустым. Массив нулевой длины.объявляется следующим образом:

new тип_элементов [ 0 ]
Заметим, что такой массив не эквивалентен объекту null. (Подробно этот вопрос будет обсуждаться в главе 4.)

Копирование массивов

По мере необходимости одну переменную массива можно скопировать в другую, но при этом обе переменные будут ссылаться на один и тот же массив.

int[] luckyNumbers = smallPrimes;
luckyNuimbers[5] =12; // Теперь элемент smallPrimes[5]также равен 12.

Результат копирования переменных массивов показан на рис. 3.15. Если необхо­димо скопировать все элементы одного массива в другой, следует использовать метод arrayсору () из класса System. Его вызов выглядит следующим образом:

System.arraycopy(from, fromlndex, to, tolndex, count);

Массив to должен иметь достаточный размер, чтобы в нем поместились все копи­руемые элементы.

Файл:Cj2I.pic3.15.png

Ниже приведен фрагмент кода, результаты выполнения которого показаны на рис. 3.16. Вначале создаются два массива, а затем последние четыре элемента первого массива копируются во второй. Копирование исходного массива начинается с элемента с номером 2, а в целевой массив копируемые данные помещаются, начиная с элемен­та с номером 3.

int[] smallPrimes = {2, 3, 5, 7, 11, 13};
int[] luckyNumbers = {1001, 1002, 1003, 1004, 1005, 1006, 1007};
System.arraycopy(smallPrimes, 2, luckyNumbers, 3, 4);
for { int i = 0; i < luckyNumbers.length; i++)
System.println(i + ": " + luckyNumbers[i]);

Выполнение данного фрагмента приводит к следующему результату:

1001
1002
1003
5
7
11
13

Файл:Cj2I.pic3.16.png


ВАЖНОЕ ЗАМЕЧАНИЕ !
Массив в языке Java значительно отличается от массива в языке С++. Однако пере­менную массива можно условно сравнить с указателем на динамически созданный массив. Таким образом, выражение Java
int[] а = new int[100]; //Java

можно сравнить с выражением c++

int* а = new int[100]; // С++

но оно существенно отличается от следующего:

int а[100]; // С++

В языке Java оператор [ ] по умолчанию проверяет диапазон изменения индексов. Кроме того, в языке Java не поддерживается арифметика указателей, — нельзя увеличить указа­тель а, чтобы обратиться к следующему элементу массива.

Параметры командной строки

В каждой из рассмотренных ранее программ на языке Java присутствовал метод main () с параметром String [] args. Этот параметр означает, что метод main () получает массив, элементами которого являются параметры, указанные в командной строке.

Рассмотрим следующую программу:

public class Message {
public static void main(String[] args) {
if (args[0].equals("-h");
System.out.print{"Hello, ");
else if (args[0].equals("-g"))
System.out.print("Goodbye, "); // Вывод остальных параметров командной строки.
for (int i = 1; i < args.length; i++)
System.out.print (" " + args[i]);
System.out.print("!");
}
}

При следующем вызове программы

java Message -g cruel world

массив args будет состоять из таких элементов:

args[0]: "-g" 
args[1]: "cruel"
args[2]: "world"

Программа выведет сообщение

Goodbye, cruel world!


ВАЖНОЕ ЗАМЕЧАНИЕ !
При запуске программы на языке Java ее имя не помещается в массиве args. Напри­мер, после запуска из командной строки программы Message с помощью команды
java Message -h world

элемент args [0 ] будет равен "-h" , а не "Message" или "java".

Сортировка массива

Если нужно упорядочить массив чисел, можно применить метод sort () из класса Arrays.

int[] а = new int[10000]; 
Arrays.sort(a);

Этот метод использует усовершенствованную версию алгоритма быстрой сорти­ровки, которая считается наиболее эффективной для большинства множеств. Класс Arrays содержит несколько удобных методов, предназначенных для работы с мас­сивами. Эти методы приведены в конце данного раздела.

Программа, код которой представлен в листинге 3.7, создает массив и генерирует случайную комбинацию чисел для лотереи. Например, если нужно выиграть "6 из 49", программа может вывести следующее сообщение:
Попробуйте такую комбинацию, и станете богатым!

4
7
8
19
30
44

Чтобы выбрать случайные числа, массив numbers сначала заполняется числами 1,2, ...,n.

int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;

Второй массив предназначен для хранения сгенерированных чисел.

int[] result = new int[k];

Теперь- сгенерируем к чисел. Метод Math. random () возвращает случайное чис­ло с плавающей точкой, лежащее в интервале от 0 (включительно) до 1 (это значение не принадлежит интервалу). Умножая результат на число п, получим случайное число, лежащее между 0 и n-1.

int г = (int)(Math.random() * n) ;

Присвоим i-e число i-му элементу массива. Сначала там будет помещено само чис­ло г, однако, как будет показано ниже, содержимое массива number будет изменяться после генерации каждого нового числа.

result[i] = numbers[r];

Теперь мы должны убедиться, что ни одно число не повторится — все номера должны быть разными. Следовательно, нужно поместить в элемент number [г] последнее число, содержащееся в массиве, и уменьшить n на единицу.

numbers[r] = numbers[n -1];
n--;

Обратите внимание на то, что при каждой генерации мы получаем индекс, а не са­мо число. Этот индекс относится к массиву, содержащему числа, которые еще не были выбраны.

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

Arrays.sort(result);
for (int i = 0; i < result.length; i++)
System.out.println(result[i]);

Листинг 3.7. Содержимое файла LotteryDrawing.Java

/**
@version 1.20 2004-02-10
@author Cay Horstmann
*/

 
import java.util.*;
 
public class LotteryDrawing
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
 
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
 
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
 
// fill an array with numbers 1 2 3 . . . n
int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;
 
// draw k numbers and put them into a second array
int[] result = new int[k];
for (int i = 0; i < result.length; i++)
{
// make a random index between 0 and n - 1
int r = (int) (Math.random() * n);
 
// pick the element at the random location
result[i] = numbers[r];
 
// move the last element into the random location
numbers[r] = numbers[n - 1];
n--;
}
 
// print the sorted array
Arrays.sort(result);
System.out.println("Bet the following combination. It'll make you rich!");
for (int r : result)
System.out.println(r);
}
}

Шаблон:API

Шаблон:API

Многомерные массивы

Для доступа к элементам многомерного массива применяется несколько индексов. Такие массивы используются для хранения таблиц и более сложных упорядоченных структур данных. Если многомерные массивы в вашей работе не нужны, можете смело пропустить этот раздел.

Допустим, что вам нужно создать таблицу чисел, показывающих, как возрастет первоначальная инвестиция объемом 10000 долларов при разных процентных став­ках, если прибыль ежегодно выплачивается и реинвестируется. Необходимые данные показаны в табл. 3.8.

Файл:Cj2I.tab3.8.png

Очевидно, что эту информацию лучше всего хранить в двухмерном массиве (или матрице), который мы назовем balance.

Объявить двухмерный массив в языке Java довольно просто. Например, это можно сделать следующим образом:

double[[]] balances;

Как обычно, мы не можем использовать массив, пока он не инициализирован с помощью оператора new.

balances = new double[NYEARS][NRATES];

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

int[][] magicSguare = 
{
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
) ;

После инициализации массива к его отдельным элементам можно обращаться с помощью двух пар квадратных скобок, например balances [ i ] [ j ].

Программа, приведенная в качестве примера, сохраняет процентные ставки в од­номерном массиве interest, а результаты подсчета баланса для каждого года и каж­дой процентной ставки— в двухмерном массиве balance. Первая строка массива инициализируется исходной суммой.

for (int j = 0; j < balances[0].length; j++) 
balances[0][j] = 10000;

Затем мы подсчитываем содержимое остальных строк.

for {int i = 1; i < balances.length; i++ ,
{
for (int j = 0; j < balances[i].length; j++)
{
double oldBalance = balances[i - 1][j];
double interest = ... ;
balances[i][j] = oldBalance + interest;
}
}

В листинге 3.8 код программы показан полностью.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Цикл "for each" не обеспечивает автоматического перебора элементов двухмерного массива. Он лишь перебирает строки, которые, в свою очередь, являются одномерны­ми массивами. Для обработки всех элементов двухмерного массива нужно два цикла.
for (double[] row : balances) 
for (double b : row).
Обработка b

Листинг 3.8. Содержимое файла CompoundInterest.java

/**
@version 1.40 2004-02-10
@author Cay Horstmann
*/

 
public class CompoundInterest
{
public static void main(String[] args)
{
final double STARTRATE = 10;
final int NRATES = 6;
final int NYEARS = 10;
 
// set interest rates to 10 . . . 15%
double[] interestRate = new double[NRATES];
for (int j = 0; j < interestRate.length; j++)
interestRate[j] = (STARTRATE + j) / 100.0;
 
double[][] balances = new double[NYEARS][NRATES];
 
// set initial balances to 10000
for (int j = 0; j < balances[0].length; j++)
balances[0][j] = 10000;
 
// compute interest for future years
for (int i = 1; i < balances.length; i++)
{
for (int j = 0; j < balances[i].length; j++)
{
// get last year's balances from previous row
double oldBalance = balances[i - 1][j];
 
// compute interest
double interest = oldBalance * interestRate[j];
 
// compute this year's balances
balances[i][j] = oldBalance + interest;
}
}
 
// print one row of interest rates
for (int j = 0; j < interestRate.length; j++)
System.out.printf("%9.0f%%", 100 * interestRate[j]);
 
System.out.println();
 
// print balance table
for (double[] row : balances)
{
// print table row
for (double b : row)
System.out.printf("%10.2f", b);
 
System.out.println();
}
}
}

"Неровные" массивы

Все языковые конструкции, которые мы до сих пор рассматривали, мало отлича­лись от других языков программирования. Однако механизм массивов в Java имеет особенность, предоставляющую совершенно новые возможности. В данном языке вообще нет многомерных массивов, только одномерные. Многомерные массивы — это "массивы массивов".

Например, массив balances в предыдущем примере фактически представляет собой массив, состоящий из 10 элементов, каждый из которых является массивом из шести элементов, представляющих, собой числа с плавающей точкой (рис 3.17).

Файл:Cj2I.pic3.17.png

Выражение balance[i] определяет i-й подмассив, т.е i-ю строку таблицы. Эта строка сама представляет собой массив, и выражение balance [ i ] [ j ] относится к его j -му элементу.

Поскольку строки массива доступны из программы, их можно легко перес­тавлять!

double[] temp = balances[i]; 
balances[i] = balances[i+1];
balances[i=1] = temp;

Кроме того, в языке Java легко создавать "неровные" массивы, т.е массивы, у кото­рых разные строки имеют разную длину. Приведем стандартный пример. Создадим массив, в котором элемент, стоящий на пересечении i-й строки и j-го столбца, равен количеству возможностей выбрать j чисел из i.

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 1
1 6 15 20 15 6 1

Поскольку число j не может превышать i, получается треугольная матрица. В i-й строке этой матрицы находится i+1 элементов. (Мы можем выбрать и 0 элементов; сделать это можно лишь одним-единственным способом.) Чтобы создать неровный массив, сначала разместим в памяти массив, хранящий его строки.

int[] [] odds = new int[NMAX+l] [];
Затем поместим туда сами строки.
for (int n=0; n<=NMAX; n++)
odds[n] = new int[n+1];

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

for (int n=0; n<odds.length; n++)
for (int k=0; k<odds[n].length; k++)
{
// Вычисление вариантов.
odds[n][k] = lotteryOdds;
}

В листинге 3.9 приведен полный текст данной программы.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке Java объявление
double [] [] balance = new double [ 10 [ 6 ] ; // Java

не эквивалентно объявлению

double balance[10][6]; // С++

и даже не соответствует

double (*balance)[6] = new double[10][6]; // С++

Вместо этого в памяти размещается массив, состоящий из десяти указателей. Сред­ствами С++ это можно выразить следующим образом:

double** balance = new double*[10]; // С++

Затем каждый элемент в массиве указателей заполняется массивом, состоящим из 6 чисел.

for (i = 0; i < 10; i++)
balanced] = new double [6];

К счастью, этот цикл выполняется автоматически при объявлении массива с помо­щью оператора new double[10] [6].
Если вам нужны "неровные" массивы, разме­щайте массивы строк отдельно.

Листинг 3.9. Содержимое файла LottaryArray.java

/**
@version 1.20 2004-02-10
@author Cay Horstmann
*/

 
public class LotteryArray
{
public static void main(String[] args)
{
final int NMAX = 10;
 
// allocate triangular array
int[][] odds = new int[NMAX + 1][];
for (int n = 0; n <= NMAX; n++)
odds[n] = new int[n + 1];
 
// fill triangular array
for (int n = 0; n < odds.length; n++)
for (int k = 0; k < odds[n].length; k++)
{
/*
compute binomial coefficient
n * (n - 1) * (n - 2) * . . . * (n - k + 1)
-------------------------------------------
1 * 2 * 3 * . . . * k
*/

int lotteryOdds = 1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n - i + 1) / i;
 
odds[n][k] = lotteryOdds;
}
 
// print triangular array
for (int[] row : odds)
{
for (int odd : row)
System.out.printf("%4d", odd);
System.out.println();
}
}
}



Реклама






]]>
Книги по Java https://linexp.ru?id=4717 Wed, 29 Jun 2022 13:55:12 GMT
<![CDATA[Core Java 2 Том I Глава 4]]>

Содержание

Оглавление

В этой главе...

  • Введение в объектно-ориентированное программирование
  • Использование готовых классов
  • Определение собственных классов
  • Статические поля и методы
  • Параметры методов
  • Формирование объектов
  • Пакеты
  • Комментарии и документирование
  • Рекомендации по разработке классов

В этой главе рассматриваются следующие вопросы.

  • Введение в объектно-ориентированное программирование.
  • Создание объектов, соответствующих классам из стандартной библиотеки Java.
  • Создание собственных классов.

Если вы недостаточно хорошо ориентируетесь в вопросах объектно-ориентирован­ного программирования, внимательно прочитайте данную главу. Для создания объектов требуется совершенно иной способ мышления по сравнению подходом, типичным для процедурно-ориентированных языков. Освоить новые принципы создания программ не всегда просто, но сделать это необходимо; для овладения языком Java надо хорошо знать основные понятия объектно-ориентированного программирования.

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

Введение в объектно-ориентированное программирование

Объектно-ориентированное программирование (ООП) в настоящее время стало «доминирующей парадигмой программирования, вытеснив "структурные", процедур­но-ориентированные подходы, разработанные в начале 1970-х годов. Java представляет собой полностью объектный язык, и на нем невозможно создавать привычные процедурные программы. Мы надеемся, что, прочитав эту главу и проанализировав примеры, приведенные в тексте, вы сможете продуктивно работать на языке Java.

Начнем с вопроса, который на первый взгляд не относится к программированию: каким образом такие компании, как Compaq, Dell, Gateway и другие производители персональных компьютеров, так быстро стали крупными фирмами? Возможно, боль­шинство читателей ответят, что они делали хорошее оборудование и продавали его по очень низким ценам, к тому же в то время спрос на компьютеры был очень высо­ким. Поставим вопрос несколько иначе: как они смогли в короткий срок произвести так много моделей, реагируя на постоянно изменяющиеся требования?

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

Производители компьютеров, приобретая изделия, относились к ним как к "черным ящикам". Например, покупая блоки питания, они приобретали нечто, обладающее оп­ределенными свойствами (размером, формой и т.д.) и способное выполнять опреде­ленные функции (сглаживать выходные сигналы, обеспечивать необходимую мощность и т.д.). Достаточно рассмотреть деятельность компании Compaq, чтобы убедиться в вы­сокой эффективности такого подхода. Перейдя от собственного производства компо­нентов компьютера к их покупке и последующей сборке, фирма резко улучшила свое финансовое положение.

В основе ООП лежит та же идея. Любая программа состоит из объектов, обладаю­щих определенными свойствами, в частности способными выполнять определенные операции. Создавать ли объект самостоятельно или покупать его у других программи­стов — решение зависит лишь от наличия денег и времени. Если объекты удовлетворяют вашим требованиям, неважно, как именно они сделаны. В ООП нужно заботиться лишь о том, что представляет собой объект. Производители компьютеров не вникают в детали внутреннего устройства блоков питания, пока эти блоки работают корректно. Точно так же большинство программистов, работающих на языке Java, не интересуются, как именно реализован тот или иной объект; главное, чтобы он удовлетворял их требованиям.

Традиционное структурное программирование заключается в разработке набора процедур (или алгоритмов) для решения поставленной задачи. Определив эти проце­дуры, программист должен задать подходящий способ хранения данных. Вот почему создатель языка Pascal Никлаус Вирт (Niklaus Wirth) назвал свою знаменитую книгу по программированию Алгоритмы + Структуры данных = Программы. Заметьте, что в на­звании этой книги алгоритмы стоят на первом месте, а структуры данных — на вто­ром. Это отражает образ мышления программистов того времени. Во-первых, они решали, как манипулировать данными; затем решали, какую структуру применить для организации этих данных, чтобы работать с ними было легче.

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

Основной принцип, обеспечивший высокую эффективность ООП, гласит: каждый объект предназначен для выполнения определенных задач. Если перед объектом сто­ит задача, для решения которой он не предназначен, у него должен быть доступ к дру­гому объекту, который может решить эту задачу. Другими словами, первый объект просит второй решить поставленную задачу. Это — обобщенный вариант обращений, применяемых в процедурном программировании. (Напомним, что в языке Java это обычно называется вызовом метода.)
Объект никогда не должен непосредственно манипулировать внутренними дан­ными другого объекта, а также предоставлять другим объектам непосредственный доступ к своим данным. Все связи между объектами обеспечиваются с помощью вызо­вов методов. Инкапсуляция (encapsulation) данных объекта максимально повышает возможность его повторного использования, уменьшает взаимозависимость объектов и минимизирует время отладки программы.

Разумеется, от объектов не следует ожидать слишком многого. Этим они схожи с модулями, применяемыми в процедурно-ориентированном программировании. И разработка, и отладка программы, состоящей из небольших объектов, выполняю­щих частные задачи, намного проще по сравнению с программой, созданной из гро­мадных объектов с крайне сложными внутренними данными и сотнями функций для манипулирования ими.

Основные термины объектно-ориентированного программирования

Для дальнейшей работы вам нужно освоить терминологию ООП. Наиболее важ­ным термином является класс, который мы уже видели в примерах программ из главы 3. Класс — это шаблон, или проект, по которому будет сделан объект. Обычно класс сравнивают с формой для выпечки печенья. Объект — это само печенье. Конст­руирование объекта на основе некоторого класса Называется созданием экземпляра (instance) этого класса.

Как мы уже видели, все коды, которые создаются в языке Java, находятся внутри классов. Стандартная библиотека языка Java содержит несколько тысяч классов, предназначенных для решения разных задач, например, для создания пользователь­ского интерфейса, календарей, установления сетевых соединений и т.д. Несмотря на это, программисты продолжают создавать свои собственные классы на языке Java, чтобы формировать объекты, характерные для разрабатываемого приложения, а также адаптировать классы из стандартной библиотеки для своих нужд.

Инкапсуляция (иногда называемая сокрытием данных) — это ключевое понятие при работе с объектами. Формально инкапсуляцией считается обычное объединение дан­ных и операций над ними в одном пакете и сокрытие данных от других объектов. Данные в объекте называются полями экземпляра (instance fields), а функции и проце­дуры, выполняющие операции над данными, — его методами (methods). В конкретном объекте, т.е. экземпляре класса, поля экземпляра имеют определенные значения. Множество этих значений называется текущим состоянием (state) объекта. Примене­ние любого метода к какому-нибудь объекту может изменить его состояние.

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

Еще один принцип ООП облегчает разработку собственных классов в языке Java: класс можно сконструировать на основе других классов. В этом случае говорят, что вновь созданный класс расширяет класс, на основе которого он создан. Язык Java, по су­ществу, создан на основе "глобального суперкласса, называемого Object. Все осталь­ные объекты расширяют его. В следующей главе мы рассмотрим этот вопрос подробнее.

Если вы разрабатываете класс на основе существующего, то новый класс содержит все свойства и методы расширяемого класса, кроме того, в нем добавляются новые методы и поля данных. Расширение класса и получение на его основе нового называ­ется наследованием (inheritance). Детально принцип наследования будет описан в сле­дующей главе.

Объекты

В объектно-ориентированном программировании определены следующие ключевые свойства объектов.

  • Поведение (behavior) объекта — что с ним можно делать и какие методы к нему можно применять.
  • Состояние объекта — как этот объект реагирует на применение методов.
  • Сущность (identity) объекта — чем данный объект отличается от других, харак­теризующихся таким же поведением и состоянием.

Все объекты, являющиеся экземплярами одного и того же класса, ведут себя оди­наково. Поведение объекта определяется методами, которые можно вызвать.

Далее, каждый объект сохраняет информацию о своем состоянии. Со временем состоя­ние объекта может измениться, однако спонтанно это произойти не может. Состояние объекта может изменяться только в результате вызовов методов. (Если состояние объекта изменилось вследствие иных причин, значит, принцип инкапсуляции не соблюден.)

Состояние объекта не полностью описывает его, поскольку каждый объект имеет свою собственную сущность. Например, в системе обработки заказов два заказа могут отличаться друг от друга, даже если они относятся к одним и тем же товарам. Заме­тим, что индивидуальные объекты, представляющие собой экземпляры класса, всегда отличаются своей сущностью и обычно отличаются своим состоянием.

Эти основные характеристики могут влиять друг на друга. Например, состояние объекта может влиять на его поведение. (Если заказ "выполнен" или "оплачен", объ­ект может отказаться выполнить вызов метода, требующего добавить или удалить из­делие. И наоборот, если заказ "пуст", т.е. ни одна единица товара не была заказана, он не может быть выполнен.)

В традиционной процедурной программе выполнение начинается сверху, с функ­ции main. При разработке объектно-ориентированной системы понятие "верха" не существует, и новички в ООП часто интересуются, с чего начинать. Ответ таков: "Сначала найдите классы и добавьте методы в каждый класс".


ВАЖНОЕ ЗАМЕЧАНИЕ !
Классы представляют собой аналоги имен существительных в описании решаемой задачи, а методы — используемых при этом глаголов.

Например, при описании системы обработки заказов используются следующие имена существительные:

  • изделие;
  • заказ;
  • адрес доставки;
  • оплата;
  • счет.

Этим именам соответствуют классы Item, Order и т.д.

Рассмотрим теперь глаголы. Предметы заказываются. Заказы выполняются или отменяются. Оплата заказа осуществляется. Используя эти глаголы, можно определить объект, выполняющий такие действия. Например, если поступил новый заказ, то от­ветственность за его обработку должен нести объект Order, поскольку именно в нем содержится информация о способе хранения и видах заказываемых предметов. Сле­довательно, в классе Order должен существовать метод add, получающий объект Item в качестве параметра.

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

Отношения между классами

Между классами существуют три обычных отношения.

  • Зависимость ("использует").
  • Агрегирование ("содержит").
  • Наследование ("является").

Отношение зависимости (dependence — "uses-a") наиболее очевидное и распростра­ненное. Предположим, например, что в системе поддержки заказов используются клас­сы Order и Account. Класс Order, вероятнее всего, использует класс Account, по­скольку объекты Order должны иметь доступ к объектам Account, чтобы проверить кредитоспособность заказчика. Однако класс Item не зависит от класса Account, так как объекты Item никогда не интересуются состоянием счета заказчика. Следовательно, класс зависит от другого класса, если его методы выполняют какие-либо действия с эк­земплярами этого класса.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Старайтесь минимизировать количество взаимозависимых классов. Если класс а не знает о существовании класса в, он тем более ничего не знает о любых его изме­нениях! (Это значит, что любые изменения класса в не повлияют на поведение объектов класса а.)

Отношение агрегирования (aggregation — "has-a") понять довольно просто. Оно озна­чает, что класс А содержит экземпляры класса В. Например, объект Order может со­держать экземпляры класса Item.

Наследование (inheritance— "is-a") выражает отношение между более конкретным и более общим классом. Например, класс RushOrder является производным от класса Order. Класс RushOrder имеет специальные методы для обработки приоритетов и разные методы для вычисления стоимости доставки, в то время как другие его мето­ды, например для заказа предмета и выписывания счета, унаследованы от класса Order. В частности, если класс А расширяет класс В, то говорят, что класс А наследует методы класса В и, кроме них, имеет дополнительные возможности. (Более подробно наследование описано в следующей главе.)
Многие программисты используют средства UML (Unified Modeling Language) для изображения диаграмм классов (class diagrams), описывающих отношения между клас­сами. Пример такой диаграммы, приведен на рис. 4.1. Здесь классы изображены с по­мощью прямоугольников, а отношения между ними — различными стрелками. В табл. 4.1 показаны основные обозначения, принятые в языке TJML.

Таблица 4.1. Обозначение отношений между классами, принятые в языке UML Отношение Обозначение в языке UML
Файл:Cj2I.tab.4.1.png

Файл:Cj2I.pic.4.1.png


ВАЖНОЕ ЗАМЕЧАНИЕ !
Некоторые специалисты не признают понятие агрегирования и предпочитают использовать отношение "связи" ("association"). С точки зрения моделирования это разумно. Однако для программистов отношение, при котором один объект содержит другой, гораздо удобнее. Мы предпочитаем использовать понятие агрегирования еще по одной причине: его обозначение проще для восприятия, чем обозначение отношения связи (табл. 4.1).

ВАЖНОЕ ЗАМЕЧАНИЕ !
Для создания диаграмм на языке UML разработано много инструментов. Некоторые поставщики предлагают очень мощные (и очень дорогие) программы, призванные стать основным средством проектирования. Среди них можно отметить продукты Rational Rose (https://www.ibm.com/software/awdtools/developer/modeler) и Together (https://www.boriand.com/together). В качестве альтернативы можно назвать программу ArgoUML, созданную на принципах открытого кода (https://argouml.tigris.org/). Коммерческая версия программы поддержки URL доступна по адресу https://gentleware.com. Если вам нужно лишь нарисовать диа­грамму, приложив минимум усилий, попробуйте воспользоваться программой Violet (https://horstmann.com/violet).

Различия между ООП и традиционным процедурным программированием

Завершим наше краткое введение в объектно-ориентированное программирова­ние анализом различий между ним и процедурной моделью. В процедурном програм­мировании сначала формулируется задача, а затем выполняются следующие действия.

  • Путем пошагового уточнения исходная задача разбивается на все более мелкие подзадачи, пока они не станут настолько простыми, что их можно будет реали­зовать непосредственно (программирование "сверху вниз").
  • Создаются процедуры, предназначенные для решения простых задач; затем эти процедуры объединяются в более сложные, пока программа не станет делать именно то, что требуется (программирование "снизу вверх").

Большинство программистов, конечно, используют смешанные стратегии "сверху вниз" и "снизу вверх". При разработке процедур нужно придерживаться такого же правила, как и при создании методов в объектно-ориентированном программирова­нии: ищите глаголы, или действия, в описании задачи. Существенное отличие заклю­чается в том, что при объектно-ориентированном программировании в проекте сна­чала выделяются классы и лишь затем определяются их методы. Между традицион­ными процедурами и ООП есть еще одна существенная разница: каждый метод связан с классом и класс отвечает за его выполнение.

Для решения небольших задач процедурный подход вполне оправдан. Однако при разработке больших проектов классы и методы имеют существенные преимущества. Клас­сы предоставляют удобный механизм кластеризации методов. Даже несложный web-браузер для своей реализации может потребовать около 2000 функций, либо 100 клас­сов, содержащих в среднем по 20 методов. Программистам намного легче оперировать с набором классов. Кроме того, в этом случае проще распределить задачи между участника­ми группы разработчиков. Немаловажную роль в организации совместной работы играет инкапсуляция: классы скрывают детали представления данных от всех остальных классов. Как показано на рис. 4.2, если при разработке допущена ошибка, которая приводит к ис­кажению данных, причину легче найти среди 20 классов, чем среди 2000 процедур.

Файл:Cj2I.pic.4.2.png

Рис. 4.2. Процедурный и объектный подход

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

Существует более серьезная проблема: в то время как на основе класса можно соз­дать несколько объектов с одинаковым поведением, в процедурно-ориентированных языках невозможно получить несколько копий одного модуля. Допустим, мы достигли модульной инкапсуляции, объединив совокупность заказов вместе с прекрасным сба­лансированным бинарным деревом, обеспечивающим быстрый доступ. И вдруг выяс­няется, что нужно иметь две такие совокупности: одну для текущих заказов, а другую — для выполненных. Нельзя просто дважды вызвать модуль, реализующий дерево зака­зов. Для этого нужно скопировать и переименовать все процедуры! Классы не имеют таких ограничений. Единожды определив класс, легко создать любое количество его экземпляров (в то время как модуль существует только в одном экземпляре).
В данном разделе мы лишь слегка затронули очень большую тему. В конце этой главы помещен короткий раздел "Советы по разработке классов", однако для более глубокого понимания процесса объектно-ориентированного проектирования нужно изучать книги, посвященные ООП и UML. В частности, мы можем порекомендовать книгу Гради Буча (Grady Booch), Айвара Джекобсона (Ivar Jacobson) и Джеймса Рамбо (James Rumbaugh) Unified Modelling Language User Guid (Addison-Wesley, 1999).

Использование готовых классов

Поскольку на языке Java ничего нельзя сделать без классов, мы вкратце обсудили в предыдущих разделах, как работают некоторые из них. К сожалению, в Java есть классы, к которым не совсем подходят приведенные выше рассуждения. Ярким при­мером является класс Math. Как вы уже видели, методы класса Math, например метод Math.random, можно вызывать, ничего не зная о деталях их реализации. Для обра­щения к методу достаточно знать его имя и параметры (если они предусмотрены). К сожалению, в классе Math нет данных, а следовательно, нет необходимости в их сокрытии. Таким образом, программист может не заботиться о создании объектов и инициализации их полей, — в классе Math ничего подобного нет!

В следующем разделе мы рассмотрим класс Date. Мы увидим, как создаются экзем­пляры этого класса и вызываются методы.

Объекты и объектные переменные

Чтобы работать с объектами, их нужно сначала создать и задать их исходное состояние. Затем к этим объектам можно применять методы.

В языке Java для создания новых экземпляров используются конструкторы. Конструк­тор — это специальный метод, предназначенный для создания и инициализации экземпляра класса, В качестве примера можно привести класс Date, содержащийся в стан­дартной библиотеке Java. С помощью объектов этого класса можно описать текущий или любой другой момент времени, например "December 31,1999, 23:59:59 GMP".


ВАЖНОЕ ЗАМЕЧАНИЕ !
Может возникнуть вопрос: почему для представления даты и времени применяются классы, а не встроенные типы (как в некоторых языках)? Такой подход применяется, например, в Visual Basic; при этом дата задается в виде #6/1/1995#. На первый взгляд, это удобно — программист может использовать встроенный тип и не заботиться о клас­сах. Однако не является ли удобство кажущимся? В некоторых странах даты записыва­ются в виде месяц/день/год, а в других— год/месяц/число. Могут ли разработчики языка предусмотреть все возможные варианты? Даже если это удастся сделать, соот­ветствующие средства будут слишком сложны, причем программисты будут вынуждены применять их. Использование классов позволяет переложить ответственность за реше­ние этих проблем с разработчиков языка на создателей библиотек. Если системный класс плох, то разработчики всегда могут написать свой собственный.

Имя конструктора всегда совпадает с именем класса. Следовательно, конструктор класса Date называется Date. Чтобы создать объект Date, конструктор нужно объе­динить с оператором new, как показано в следующем примере:

new Date ()

Это выражение создает новый объект, который инициализируется текущими датой и временем.
При желании объект можно передать методу.

System.out.println (new Date());

И наоборот, можно вызвать метод вновь созданного объекта. Среди методов клас­са Date есть метод toString (), позволяющий представить дату в виде строки. Его можно вызвать следующим образом:

String = new Date().toString();

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

Date birthday = new Date () ;

На рис. 4.3 условно показана переменная birthday, ссылающаяся на вновь соз­данный объект.

Файл:Cj2I.pic.4.3.png

Рис. 4.3. Создание нового объекта

Между объектами и объектными переменными есть существенная разница. Например, приведенное ниже выражение определяет объектную переменную deadline, которая может ссылаться на объекты типа Date.

Date deadline; // Переменная deadline не ссыпается ни на один объект

Важно понимать, что на данном этапе сама переменная deadline объектом не является и даже не ссылается ни на один объект. Поэтому ни один метод класса Date с помощью этой переменной вызывать пока нельзя. Попытка сделать это приведет к появ­лению сообщения об ошибке.

s = deadline.toString(); // Вызывать метод еще рано

Сначала переменную deadline нужно инициализировать. У программиста есть две возможности. Разумеется, переменную можно инициализировать вновь создан­ным объектом:

deadline = new Date{);

Кроме того, можно заставить переменную ссылаться на существующий объект:

deadline = birthday;

Теперь переменные deadline и birthday ссылаются на один и тот же объект (рис. 4.4).

Файл:Cj2I.pic.4.4.png

Рис. 4.4. Объектные неременные, ссылающиеся на один и тот же объект

Важно помнить, что объектная переменная фактически не содержит никакого объекта. Она лишь ссылается на него.

В языке Java значение любой объектной переменной представляет собой ссылку на объект, размещенный в другом месте. Оператор new также возвращает ссылку.

Например, приведенная ниже строка кода состоит из двух частей.

Date deadline = new Date() ;

Выражение new Date () создает объект типа Date, а значение переменной представ­ляет собой ссылку на вновь созданный объект.

Объектной переменной можно явно присвоить ссылку null, чтобы отметить тот факт, что она пока не ссылается ни на один объект.

deadline = null;
if(deadline != null)
System.out.println(deadline);

Если вы попытаетесь вызывать метод объекта посредством переменной, значение которой равно null, то при выполнении программы возникнет ошибка.

birthday = null;
String s = birthday.toStringf); // Ошибка!

Локальные объектные переменные не инициализируются автоматически. Прог­раммист должен сам инициализировать переменную, либо вызвав оператор new, либо присвоив значение null.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Многие ошибочно полагают, что объектные переменные в языке Java похожи на пе­ременные языка С++, типы которых задаются именами классов. Однако в языке С++ такая переменная не может иметь значение null. Кроме того, им ничего нельзя при­сваивать. Объектные переменные в языке Java следует считать аналогами указателей на объекты. Например, выражение
Date birthday; // Java почти эквивалентно приведенной ниже строке кода.
Date* birthday; // С++

Такая аналогия все расставляет по своим местам. Разумеется, указатель Date* не инициализируется, пока не выполнен оператор new. Синтаксис выражений в языках С++ и Java почти совпадает.

Date* birthday = new Date О; // С++

При копировании одной переменной в другую в обеих переменных появляется ссылка на один и тот же объект. Эквивалентом ссылки null в языке С++ является указатель null.

Все объекты в языке Java располагаются в динамической памяти. Если объект содержит другую объектную переменную, она представляет собой всего лишь указатель на дру­гой объект, расположенный в области памяти под названием "куча".

В языке С++ указатели доставляют много хлопот, так как из-за них часто возникают ошибки. Очень легко создать неверный указатель или потерять управление памятью. В языке Java эти проблемы попросту не существуют. Если вы используете неинициа­лизированный указатель, то система поддержки выполнения программ обязательно сообщит об ошибке и не продолжит выполнение некорректной программы, выдавая случайные результаты. Программист может не заботиться об управлении памятью, поскольку механизм "сборки мусора" делает это за него автоматически.

В языке С++ большое внимание уделено автоматическому копированию объектов. Например, копией связного списка является новый связный список, который, имея старое содержимое, содержит совершенно другие связи. Другими словами, копиро­вание объектов осуществляется так же, как и копирование встроенных типов. В языке Java для получения полной копии объекта используется метод clone.

Класс GregorianCalendar из библиотеки Java

В предыдущих примерах мы использовали класс Date, являющийся частью стан­дартной библиотеки Java. Экземпляр класса Date находится в состоянии, которое от­ражает конкретный момент времени.

Хотя при использовании класса Date нам необязательно знать о формате даты, отметим, что время представляется количеством миллисекунд (положительным или отрицательным), отсчитанным от фиксированного момента времени, так называемо­го начала эпохи, т.е. от 00:00:00 UTC, 1 января 1970 года. Аббревиатура UTC означает Universal Coordinated Time — научный стандарт времени. UTC применяется наряду с более известным GMT (Greenwich Mean Time).

Однако класс Date не очень удобен для работы с датами. Разработчики библиоте­ки Java считали, что представление даты, например "December 31, 1999, 23:59:59", является совершенно произвольным и должно зависеть от календаря. Данное кон­кретное представление подчиняется григорианскому календарю, самому распростра­ненному календарю в мире. Однако тот же самый момент времени совершенно иначе представляется в китайском или еврейском лунном календаре, не говоря уже о кален­даре, которым будут пользоваться потенциальные заказчики с Марса.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Вся история развития человечества сопровождалась созданием календарей — сис­тем именования различных моментов времени. Как правило, основой для календарей был солнечный или лунный цикл. Если вас интересуют подобные вопросы, обрати­тесь к книге Наума Дершовица (Nachum Dershowitz) и Эдварда М. Рейнголда (Edward М. Retngold) Calendrical Calculations {Cambridge University Press, 2nd ed., 2001). Там вы найдете сведения о календаре французской революции, календаре Майя и других эк­зотических системах.

Разработчики библиотеки решили отделить вопросы, связанные с отслеживанием моментов времени, от вопросов их представления. Таким образом, стандартная биб­лиотека Java содержит два отдельных класса: класс Date, представляющий момент времени, и класс GregorianCalendar, расширяющий более общий класс Calendar, описывающий свойства календаря в целом. Теоретически можно расширить класс Calendar и реализовать китайский лунный или марсианский календари. Однако в стандартной библиотеке, кроме григорианского календаря, пока нет никакой дру­гой реализации.

Отделение измерения времени от календарей представляет собой хорошее решение, вполне соответствующее принципу объектно-ориентированного программирования.
Класс Date содержит лишь небольшое количество методов, позволяющих сравни­вать два момента времени. Например, методы before и after определяют, предше­ствует один момент времени другому или следует за ним.

if (today.before (birthday))
System.out.println("Еще есть время купить подарок.");

Класс GregorianCalendar содержит гораздо больше методов, чем класс Date. В частности, в нем есть несколько полезных конструкторов. Так, приведенное ниже выражение формирует новый объект, представляющий дату и момент времени, когда он был создан.

new GregorianCalendar()


ВАЖНОЕ ЗАМЕЧАНИЕ !
На самом деле класс Date имеет такие методы, как getDay(), getMonth() и getYear (), но использовать их без крайней необходимости не рекомендуется.

Эти методы были частью класса Date еще до того, как разработчики библиотеки по­няли, что классы, реализующие разные календари, разумнее было бы отделить друг от друга. Сделав это, они пометили методы класса Date как не рекомендованные к применению (deprecated). Вы можете продолжать использовать их в своих програм­мах, получая при этом предупреждения от компилятора, а еще лучше их не применять совсем, поскольку в будущем они могут быть удалены из библиотеки.

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

new GregorianCalendar(1999, 11, 31)

Довольно странно, что количество месяцев отсчитывается от нуля. Так, число 11 означает декабрь. Для большей ясности можно использовать константу Calendar.December.

new GregorianCalendar(1999, Calendar.December, 31)

Кроме того, посредством конструктора GregorianCalendar можно задать время,

new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)

Чаще всего ссылка на созданный объект присваивается переменной.

GregorianCalendar deadline = new GregorianCalendar(...);

В классе GregorianCalendar инкапсулированы поля экземпляра, в которых записа­на указанная дата. Не имея доступа к исходному тексту, невозможно определить, какое представление даты и времени использует этот класс. Разумеется, для програм­миста, применяющего готовый класс, это совершенно неважно. Важно лишь то, какие методы доступны ему.

Модифицирующие методы и методы доступа

После того как вы познакомились с классом GregorianCalendar, у вас, возмож­но, возникли вопросы: как получить текущий день, месяц или год на базе даты, инкап­сулированной в объекте данного класса? Каким образом можно изменить эти значе­ния? Ответы на эти вопросы вы найдете в данном разделе, а дополнительную инфор­мацию — в документации по API. Рассмотрим наиболее важные методы.
Календарь должен вычислять атрибуты, соответствующие указанному моменту времени, например, дату, день недели, месяц или год. Получить одно из этих значе­ний можно, используя метод get () класса GregorianCalendar. Чтобы выбрать же­лаемый атрибут, нужно передать методу константу, определенную в классе Calendar, например Calendar .MONTH или Calendar.DAY_OF_WEEK:

GregorianCalendar now = new GregorianCalendar () ;
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);

Состояние объекта можно изменить с помощью метода set ().

deadline.set(Calendar.YEAR, 2001);
deadline(Calendar.MONTH, Calendar.APRIL);
deadline.set(Calendar.DAY, 15);

Существует способ установить год, месяц и день с помощью одного вызова.

deadline.set(2001. Calendar.APRIL, 15);

При желании вы можете добавить к заданной дате определенное количество дней, недель, месяцев и т.д.

deadline.add(Calendar.MONTH, 3); // Сдвинуть момент deadline на 3 месяца.

Добавив отрицательное число, вы сдвинете момент времени, описываемый объек­том, назад.

Между методом get (), с одной стороны, и методами set () и add (), с другой, есть принципиальная разница. Метод get () только просматривает состояние объекта и сообщает о нем, в то время как методы set () и add () модифицируют состояние объ­екта. Методы, которые могут изменять поля экземпляра, называются модифицирующи­ми (mutator), а методы, которые могут лишь просматривать поля экземпляра, не из­меняя их, называются методами доступа (accessor).

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


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ для формирования метода доступа используется суффикс const. Метод, не объявленный с помощью ключевого слова const, считается модифицирующим. Однако в языке Java нет специальных синтаксических средств, позволяющих различать модифицирующие методы и методы доступа.

В именах методов доступа принято использовать префикс get (), а в именах модифицирующих методов — префикс set (). Например, класс GregorianCalendar обеспечивает получение и установку момента времени, представленного объектом, посредством методов getTime () и setTime ().

Date time = calendar.getTime(); 
calendar.setTime(time);

Эти методы очень полезны для преобразований объектов GregorianCalendar в объекты Date и наоборот. Предположим, что нам известны год, месяц и день и мы хотим создать объект Date, поля которого были бы заполнены этими значениями. Поскольку класс Date ничего не знает о календарях, создадим сначала объект GregorianCalendar, а затем вызовем метод getTime () для получения даты.

GregorianCalendar calendar = new GregorianCalendar(year, month, day); 
Date hireDay = calendar.getTime();

И наоборот, если нужно определить год, месяц или день, записанные в объекте Date, создадим объект GregorianCalendar, установим время, а затем вызовем метод get ().

GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);

В конце этого раздела приведена программа, иллюстрирующая работу класса GregorianCalendar. Программа выводит на экран календарь текущего месяца в сле­дующем формате:

Файл:CJ2I.calendar.png

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

Рассмотрим ключевые вопросы, связанные с работой этой программы. Сначала создадим объект GregorianCalendar, инициализированный текущими датой и вре­менем. (На самом деле в данном приложении время нас не интересует.)

GregorianCalendar d = new GregorianCalendar();

Затем определим текущий день и месяц, дважды вызвав метод get ().

int today = d.get(Calendar.DAY_OF_MONTH); 
int month = d.get{Calendar.MONTH);

После этого передадим методу set () объекта d параметр, задающий первое число месяца, и получим день недели, соответствующий этой дате.

d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);

Переменная weekday будет содержать значение 1 (или Calendar. SUNDAY), если пер­вый день месяца — воскресенье, 2 (или Calendar. MONDAY) — если понедельник, и т.д.

Затем на экран выводится заголовок и пробелы, выравнивающие первую строку календаря.

Для каждого дня выведем на экран пробел, если число меньше 10, затем— само число, а потом звездочку, если число соответствует сегодняшнему дню. Каждое вос­кресенье выводится с новой строки.

Затем установим объект d на следующий день.

d.add(Calendar.DAY_OF_MONTH, 1);

Когда нам следует остановиться? Нам неизвестно, сколько в месяце дней: 31, 30, 29 или 28. Мы продолжаем итерации, пока объект d остается в пределах текущего месяца.

do {
...
}
while (d.get (Calendar .MONTH) == month) ;

Как только объект d перейдет на следующий месяц, программа завершит свою работу. Полный текст программы приведен в листинге 4.1.

Как видим, класс GregorianCalendar позволяет легко создавать программы для работы с календарем, выполняя такие сложные действия, как отслеживание дней не­дели и учет продолжительности месяцев. Программист не обязан ничего знать о том, как именно класс GregorianCalendar вычисляет месяцы и дни недели. Нужно просто использовать интерфейс класса — методы get (), set () и add ().

Основное назначение этой программы — показать, как можно использовать ин­терфейс класса для выполнения действительно сложных задач, не вникая в детали реализации.

Листинг 4.1. Содержимое файла CalendarTest.java

/**
@version 1.31 2004-02-19
@author Cay Horstmann
*/

 
import java.util.*;
 
public class CalendarTest
{
public static void main(String[] args)
{
// construct d as current date
GregorianCalendar d = new GregorianCalendar();
 
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);
 
// set d to start date of the month
d.set(Calendar.DAY_OF_MONTH, 1);
 
int weekday = d.get(Calendar.DAY_OF_WEEK);
 
// get first day of week (Sunday in the U.S.)
int firstDayOfWeek = d.getFirstDayOfWeek();
 
// indent first line of calendar
for (int i = firstDayOfWeek; i < weekday; i++ )
System.out.print(" ");
 
do
{
// print day
int day = d.get(Calendar.DAY_OF_MONTH);
System.out.printf("%3d", day);
 
// mark current day with *
if (day == today)
System.out.print("*");
else
System.out.print(" ");
 
// advance d to the next day
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);
 
// start a new line at the start of the week
if (weekday == firstDayOfWeek)
System.out.println();
}
while (d.get(Calendar.MONTH) == month);
// the loop exits when d is day 1 of the next month
 
// print final end of line if necessary
if (weekday != firstDayOfWeek)
System.out.println();
}
}

Чтобы не усложнять программу, имена выводятся на английском языке, а первым днем недели считается воскресенье. Если вы хотите выводить календарь по согла­шениям другой страны, ознакомьтесь с классом DateFormatSymbols. Метод Calendar.getFirstDayOfWeek() возвращает первый день недели. Для США — это воскресенье, а для Германии — понедельник.

Шаблон:API

Определение собственных классов

В главе 3 мы уже попробовали создавать простые классы. Однако все они состояли из одного-единственного метода main (). Теперь пришло время научиться создавать "рабочие" классы для более сложных приложений. Обычно в этих классах нет метода main(). Вместо этого у них есть другие методы и поля. Чтобы написать полностью законченную программу, нужно объединить классы, один из которых содержит метод main().

Класс Employee

Простейшее определение класса в языке Java имеет следующий вид:

class ИмяКласса {
конструктор_1 конструктор__2
...
метод_1 метод_2
...
поле__1 поле_2
}

ВАЖНОЕ ЗАМЕЧАНИЕ !
Мы придерживаемся стиля, согласно которому сначала указываются методы класса, а в конце — его поля. Это сделано для того, чтобы привлечь основное внимание к ин­терфейсу, а не к реализации класса.

Рассмотрим следующую, весьма упрощенную версию класса Employee, который можно использовать для создания платежной ведомости.

class Employee {
// Конструктор
public Employee (String n, double s, int year, int month, int day)
{
name = n; salary = s;
GregorianCalendar calendar = new GregorianCalendar (year, month - 1, day);
hireDay = calendar.getTime();
 
// Метод 1
public String getName() {
return name;
}
// Другие методы
// Поля экземпляра
private String name; private double salary; private Date hireDay;
}

Детали реализации этого класса мы проанализируем в следующих разделах. А сей­час рассмотрим код, представленный в листинге 4.2 и иллюстрирующий работу класса
Employee.

В данной программе создается массив Employee, в который заносятся три объекта

Employee.
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", . . .);
staff[1] = new Employee("Harry Hacker", . . .);
staff[2] = new Employee("Tony Tester", . . .);

Затем, для того чтобы поднять зарплату каждого сотрудника на 5%, вызывается метод класса Employee.

for (Employee е : staff) 
е.raiseSalary(5);

В заключение с помощью методов getName ( ), getSalary () и getHireDay () выводится информация о каждом сотруднике.

for (Employee е : staff) {
System.out.println("name=" + getName() + ", зарплата=" + e.getSalary() + ", дата найма на работу=" + е.getHireDay());
}

Заметим, что этот пример программы состоит из двух классов: Employee и EmployeeTest, объявленного как public. Метод main ( ) с выражениями, которые мы только что описали, содержится в классе EmployeeTest.

Исходный текст программы содержится в файле EmployeeTest.java, поскольку его имя должно совпадать с именем общедоступного класса. В исходном файле может быть только один класс, объявленный как public, и любое количество классов, в объявлении которых данное ключевое слово отсутствует.

Далее при компиляции исходного кода компилятор создает два файла классов: EmployeeTest.class и Employee.class.

Выполнение программы начинается, когда интерпретатор получает имя класса, содержащего метод main ( ).

java EmployeeTest

При этом интерпретатор начинает обрабатывать метод main ( ) из класса EmployeeTest. В результате выполнения кода создаются три новых объекта Employee и отображается их состояние.

Листинг 4.2. Содержимое файла EmployeeTest.java

/**
@version 1.11 2004-02-19
@author Cay Horstmann
*/

 
import java.util.*;
 
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
 
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
 
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
 
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary()
+ ",hireDay=" + e.getHireDay());
}
}
 
class Employee
{
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
// GregorianCalendar uses 0 for January
hireDay = calendar.getTime();
}
 
public String getName()
{
return name;
}
 
public double getSalary()
{
return salary;
}
 
public Date getHireDay()
{
return hireDay;
}
 
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
 
private String name;
private double salary;
private Date hireDay;
}

Использование нескольких исходных файлов

Программа, приведенная в листинге 4.2, содержит два класса в одном исходном файле. Многие программисты предпочитают помещать каждый класс в отдельный файл. Например, класс Employee можно разместить в файле Employee.java, а класс EmployeeTest — в файле EmployeeTest. java.

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

javac Employee*.java

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

javac EmployeeTest.java

Как ни странно, файл Employee. java также будет скомпилирован. Обнаружив, что в файле EmployeeTest. java используется класс Employee, компилятор языка Java станет искать файл Employee.class. Если он его не найдет, то автоматически будет скомпилирован файл Employee. java. Более того, если файл Employee.java создан позже, чем существующий файл Employee. class, компилятор языка Java также выполнит повторную компиляцию и создаст файл класса.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Если вы знакомы с утилитой make, имеющейся в Unix и других операционных систе­мах, то поведение компилятора не вызовет вопросов. Дело в том, что в компиляторе языка Java реализованы возможности этой утилиты.

Анализ класса Employee

Проанализируем класс Employee. Начнем с методов этого класса. Изучая исход­ный текст программы, легко видеть, что в классе Employee реализованы один конст­руктор и четыре метода.

public Employee(String n, double s, int year, int month, int day) 
public String getName()
public double getSalary()
public Date getHireDay()
public void raiseSalary (double byPercent)

Все методы этого класса объявлены как public, т.е. обращение к этим методам может осуществляться из любого класса. (Существуют четыре возможных уровня дос­тупа; они рассматриваются в этой и следующей главах.)

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

private String name; 
private double salary;
private Date hireDay;

Ключевое слово private означает, что к данным полям имеют доступ только методы самого класса Employee. Ни один внешний метод не может читать эти поля или изменять их.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Поля экземпляра могут быть объявлены как public, однако делать этого не следует. Любые компоненты программы могут обращаться к общедоступным полям и моди­фицировать их содержимое. Это противоречит принципу инкапсуляции. Мы настоя­тельно рекомендуем всегда закрывать доступ к полям экземпляра с помощью ключе­вого слова private.

В заключение отметим, что два из трех полей экземпляра представляют собой объекты: поля name и hireDay являются ссылками на экземпляры классов String и Date. Это довольно распространенное явление: классы часто содержат экземпляры других классов.

Конструкторы

Рассмотрим конструктор класса Employee.

public Employee(String n, double s, int year, int month, int day) 
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}

Имя конструктора совпадает с именем класса. Этот конструктор выполняется при создании объекта Employee, заполняя поля экземпляра заданными значениями. Например, при создании экземпляра класса Employee с помощью выражения

new Employee("James Bond", 100000, 1950, 1, 1); поля экземпляра будут заполнены такими значениями:
name = "James Bond";
salary = 100000;
hireDay = January 1, 1950;

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

james.Employee("James Bond", 250000, 1950, 1, 1); // ОШИБКА

при компиляции приведет к ошибке.

Позднее в этой главе мы вернемся к конструкторам. Пока запомните следующее.

  • Имя конструктора совпадает с именем класса.
  • Класс может иметь несколько конструкторов.
  • Конструктор может иметь один или несколько параметров либо не иметь их вовсе.
  • Конструктор не возвращает никакого значения.
  • Конструктор всегда вызывается совместно с оператором new.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Конструкторы в языках Java и С++ работают одинаково. Однако учтите, что все объек­ты в языке Java размещаются в динамической памяти и конструкторы вызываются только вместе с оператором new. Программисты, имеющие опыт работы на языке С++, часто допускают такую ошибку:
Employee number007("James Bond", 10000, 1950, 1, 1); // С++, а не Java

Это выражение в С++ работает, а в Java — нет.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Будьте осторожны и не называйте локальные переменные так же, как и поля экземп­ляра. Например, приведенный ниже конструктор не сможет установить зарплату сотрудника.
public Employee (String n, double s, ...) 
{
String name = n; // ОШИБКА
double salary = s; // ОШИБКА
}

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

Явные и неявные параметры

Методы объекта имеют доступ ко всем его полям. Рассмотрим следующий метод:

public void raiseSalary(double byPercent) 
{
double raise = salary * byPercent / 100;
salary += raise;
}
 
Он устанавливает новое значение поля salary. (Данный метод не возвращает ни­какого значения.) Например, вызов метода
<syntaxhighlight lang='java'>
number007.raiseSalary(5);

увеличивает значение поля number007.salary в объекте number007 на 5%. Строго говоря, вызов данного метода приводит к выполнению двух выражений:

double raise = number007.salary * 5 /100; 
number007.salary += raise;

Метод raiseSalary имеет два параметра. Первый параметр, называемый неявным, представляет собой объект типа Employee, который указывается перед именем метода. Второй параметр, число, указанное в скобках после имени метода, называется явным.
Легко видеть, что явные параметры перечисляются в объявлении метода, напри­мер double byPercent. Неявный параметр в объявлении метода не приводится.
В каждом методе ключевое слово this ссылается на неявный параметр. При же­лании можно переписать метод raiseSalary следующим образом:

public void raiseSalary(double byPercent) 
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}

Некоторые программисты предпочитают такой стиль, поскольку в нем более чет­ко различаются поля экземпляра и локальные переменные.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ методы обычно определяются вне класса. Например:
void Employee::raiseSalary(double byPercent) // С++, а не Java
{
...
}

Если определить метод внутри класса, он автоматически станет подставляемым (inline).

class Employee {
...
int getName() { return name; } // Подставляемая функция в С++.
}

В языке Java все методы определяются внутри класса. Это не делает их подставляе­мыми. Виртуальная машина Java анализирует, как часто производится обращение к методу, и принимает решение о том, должен ли метод быть подставляемым.

Преимущества инкапсуляции

Рассмотрим довольно простые методы getName (), getSalary () и getHireDay ().

public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay ()
{
return hireDay;
}

Они представляют собой яркий пример методов доступа (accessor). Поскольку они лишь возвращают значения полей экземпляра, иногда их называют методами дос­тупа к полю (field accessor).

Но не проще ли было сделать поля name, salary и hireDay открытыми (т.е. объ­явить их как publiс) и не создавать отдельные методы доступа к ним?

Дело в том, что поле name доступно лишь для чтения. После того как значение этого поля будет установлено конструктором, ни один метод не сможет его изменить. Следовательно, у нас есть гарантия, что информация, хранящаяся в этом поле, не бу­дет искажена.

Поле salary допускает запись, однако изменить его значение может только метод raiseSalary(). В частности, если окажется, что поле содержит неверное значение, нужно будет отладить только один метод. Если бы поле salary было открытым, при­чина ошибки могла бы находиться где угодно.

Иногда нужно иметь возможность читать и модифицировать содержимое поля. Для этого необходимо реализовать в составе класса следующие три элемента.

  • Закрытое поле данных (ключевое слово private).
  • Общедоступный метод доступа (ключевое слово public).
  • Общедоступный модифицирующий метод (ключевое слово public).

Это намного утомительнее, чем просто сделать общедоступным одно-единственное поле данных, однако при этом программист получает существенные преимущества.

1. Внутреннюю реализацию класса можно изменять совершенно независимо от других классов.

Предположим, например, что имя и фамилия сотрудника хранятся отдельно.

String firstName; 
String lastName;

Тогда метод getName должен формировать возвращаемое значение следующим образом:

firstName + " " + lastName

Остальные части программы остаются неизменными. Разумеется, и методы доступа, и модифицирующие методы должны быть переработаны, чтобы учесть новое представление данных.

2. Модифицирующие методы могут выполнять проверку ошибок, в то время как при присвоении полю некоторого значения ошибки не выявляются.
Например, метод setSalary() может проверить, не стала ли зарплата отри­цательной величиной.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Будьте осторожны при создании методов доступа, возвращающих ссылки на изменяе­мый объект. Создавая класс Emloyee, мы нарушили это правило: метод getHireDay () возвращает объект Date.
class Employee 
{
...
public Date getHire();
{
return hireDay;
}
...
private Date hireDay;
}

Это не соответствует принципу инкапсуляции! Рассмотрим пример неверного кода:

Employee harry = ...;
Date d = harry.getHireDay();
double tenYearslnMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long)tenYearslnMilliSeconds);
// Значение объекта изменено.

Причину ошибки трудно уловить. Обе ссылки d и harry. hireDay относятся к одному и тому же объекту (рис. 4.5). Применение модифицирующего метода к объекту d ав­томатически изменяет состояние объекта, содержащегося в классе Employee!

Чтобы вернуть ссылку на изменяемый объект, его нужно сначала клонировать. Клон — это точная копия объекта, расположенная в другом месте памяти. Детали клонирования рассмотрены в главе 6. Ниже приведен исправленный код.

class Employee 
{
...
public Date getHireDay()
{
return (Date)hireDay.clone();
}
}

Используйте метод clone (), если вам нужно скопировать изменяемое поле данных.

Файл:Cj2I.pic.4.5.png

Доступ к данным из различных экземпляров класса

Как вы уже знаете, метод имеет доступ к любым данным объекта, которому он принадлежит. Но он также может обращаться к закрытым данным всех экземпляров сво­его классах Рассмотрим метод equals, сравнивающий между собой два экземпляра класса Employee.

class Employee {
boolean equals(Employee other) {
return name.equals(other.name);
}
}

Вызов этого метода выглядит следующим образом:

if (harry.equals(boss)) ...

Этот метод имеет доступ к закрытым полям объекта harry, что вовсе не удивительно. Но он также имеет доступ к полям объекта boss. Это вполне объяснимо, поскольку boss — объект Employee, а методы, принадлежащие классу Employee, могут обра­щаться к закрытым полям любого объекта этого типа.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ действует такое же правило. Метод имеет доступ к переменным и функ­циям любого объекта своего класса.

Закрытые методы

При реализации класса мы сделали все поля данных закрытыми, поскольку предостав­лять к ним доступ из других классов весьма рискованно. А как поступать с методами? Для взаимодействия с другими объектами необходимы открытые методы. Однако в ряде случаев для вычислений необходимы вспомогательные методы. Как правило, эти вспомо­гательные методы не являются частью интерфейса, поэтому указывать при их объявлении ключевое слово public нет необходимости; чаще всего они объявляются как private.

Чтобы сделать метод закрытым, измените ключевое слово public на private.

Сделав метод закрытым, совершенно' не обязательно сообщать пользователям о его наличии. Использовать его будет только разработчик класса. Необходимо пре­доставлять информацию об открытых методах, поскольку другие части программы могут к ним обращаться.

Неизменяемые поля экземпляра

Поля экземпляра можно объявить с помощью ключевого слова final. Такое поле должно инициализироваться при созданий объекта, после этого его значение изме­нить уже нельзя.

Например, поле name класса Employee можно объявить неизменяе­мым, поскольку после создания объекта оно никогда не изменяется — метода setName не существует.

class Employee {
private final String name;
}

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

private final Date hiredate;

Оно означает, что переменная hiredate не изменяется после создания объекта. Однако это не означает, что состояние объекта, на который ссылается переменная, остается неизменным. В любой момент можно вызвать метод setTime ().

Статические поля и методы

Во всех программах, которые мы до сих пор обсуждали, при объявлении метода main () использовался модификатор static. Рассмотрим действие этого модификатора.

Статические поля

Поле, имеющее модификатор static, существует в одном экземпляре. В то же время, если поле не статическое, то каждый объект содержит его копию. Предполо­жим, что нам требуется присвоить уникальный идентификационный номер каждому сотруднику. Добавим в класс Employee поле id и статическое поле nextId.

class Employee {
private int id;
private static int nextld = 1;
}

Теперь каждый объект Employee имеет свое поле id, кроме того, есть поле nextld, которое одновременно принадлежит всем экземплярам класса.

Попробуем объяснить это несколько иначе. Если существует тысяча объектов Employee, то в них есть тысяча полей id, по одному в каждом объекте. В то же время существует только один экземпляр статического поля nextld. Даже если не создан ни один объект Employee, статическое поле nextld существует. Оно принадлежит клас­су, а не конкретному объекту.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В большинстве объектно-ориентированных языков статические поля называются по­лями класса (class field). Термин "статический" унаследован от языка С++.

Реализуем простой метод.

public void setId() {
id = nextld;
nextld++;
}

Допустим, нам нужно задать идентификационный номер объекта harry, harry.setld() ;

Теперь значение поля id объекта harry задано, а значение статического поля nextld увеличено на единицу.

harry.id = ...; 
Employee.nextld++;

Константы

Статические переменные используются довольно редко. В то же время статиче­ские константы используются гораздо чаще. Например, класс Math имеет статиче­скую константу.

public class Math {
public static final double PI = 3.14159265358979323846;
}

Обратиться к этой константе в своей программе можно с помощью выражения Math.PI.

Если бы ключевое слово static было пропущено, константа PI была бы обычным полем экземпляра класса Math. Это значит, что для доступа к такой константе нужно было бы создать объект Math, причем каждый подобной объект имел бы свою копию константы PI.

Еще одна часто используемая статическая переменная — System, out. Она объяв­лена в классе System.

public class System {
public static final PrintStream out = ...;
}

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

out = new PrintStream(...); // ОШИБКА — поле out изменить нельзя.

ВАЖНОЕ ЗАМЕЧАНИЕ !
Просмотрев код класса System, вы увидите метод setout (), позволяющий присво­ить полю System, out другой поток. Как же этот метод может изменить переменную, описанную как final? Дело в том, что метод setout — платформенно-ориентированный, он реализован средствами, отличными от Java. Платформенно-ориентированные методы могут обходить механизмы контроля, предусмотренные в языке Java. Это очень специфическое решение, которое ни в коем случае не следует повторять в своих программах.

Статические методы

Статические методы могут выполняться, даже если экземпляр класса не существу­ет. Например, метод pow() из класса Math — статический. Выражение Math. pow(x, у) вычисляет х*. При выполнении своей задачи этот метод не использует ни одного эк­земпляра класса Math. Иными словами, он не имеет неявного параметра. Это значит, что статические методы — это методы, в которых не используется объект this.

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

public static int getNextld() {
return nextld; // Возвращает статическое поле
}

Чтобы вызвать этот метод, нужно указать имя класса, int n = Employee.getNextld() ;

Можно ли пропустить ключевое слово static при описании этого метода? Да, но при этом для его вызова потребуется ссылка на объект типа Employee.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Для вызова статического метода можно использовать и объекты. Например, если harry — это объект Employee, то можно вместо Employee.getNextld () использо­вать вызов harry.getNextld(). Однако такое обозначение усложняет восприятие программы, поскольку для вычисления результата метод getNextld () не обращает­ся к объекту harry. Мы рекомендуем для вызова статических методов использовать имена классов, а не объекты.

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

  • Когда методу не нужен доступ к информации о состоянии объекта, поскольку все необходимые параметры задаются явно (например, в методе Math. pow).
  • Когда методу нужен доступ лишь к статическим полям класса (в качестве при­мера можно привести метод Employee. getNextld ()).

ВАЖНОЕ ЗАМЕЧАНИЕ !
Статические поля и методы в языках Java и С++, по существу, отличаются только син­таксически. В языке С++ для доступа к статическому полю или методу, находящемуся вне области видимости, можно использовать оператор ::, например Math: : PI.

Термин "статический" — исторический курьез. Сначала ключевое слово static было введено в языке С для обозначения локальных переменных, которые не уничтожались при выходе из блока. В этом контексте слово "статический" имеет смысл: переменная продолжает существовать после выхода из блока, а также при повторном входе в него. Затем слово "статический" в языке С приобрело второе значение — глобальные пере­менные и функции, к которым нельзя получить доступ из других файлов. Ключевое сло­во static было просто использовано повторно, чтобы не вводить новое. В итоге в язы­ке С++ это ключевое слово было применено в третий раз, получив совершенно новую интерпретацию. Оно обозначает переменные и функции, принадлежащие классу, но не принадлежащие ни одному объекту этого класса. Именно этот смысл имеет ключевое слово static вязыке Java.

Порождающие методы

Рассмотрим еще одно применение статических методов. Класс Number Format использует порождающие методы для создания объектов, соответствующих различ­ным стилям форматирования.

NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentlnstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // Выводит $0.10
System.out.println{percentFormatter.format(x)); // Выводит 10%

Почему же не использовать для этой цели конструктор? Тому есть две причины.

  • Конструктору нельзя присвоить произвольное имя. Его имя всегда должно сов­падать с именем класса. В примере с классом NumberFormat имеет смысл при­менять разные имена для разных типов форматирования,
  • При использовании конструктора тип объекта фиксирован. Если же применя­ются порождающие методы, они возвращают объект DecimalFormat, который наследует свойства NumberFormat. (Подробно вопросы наследования будут обсуждаться в главе 5.)

Метод main ()

Заметим, что статические методы можно вызывать, даже если соответствующий объект еще не создан. Например, для того чтобы вызвать метод Math.pow(), объек­ты Math не нужны. По той же причине метод main () объявляется как статический.

public class Application {
public static void main(String[] args) {
// Здесь создаются объекты.
}
}


ВАЖНОЕ ЗАМЕЧАНИЕ !
Каждый класс может содержать метод main (). Используя его, можно тестировать классы независимо друг от друга. Например, метод main () можно добавить в класс

Employee.

class Employee {
public Employee(String n, double s, int year, int month, int day)
{
name - n; salary = s;
GregorianCalendar calendar =
new GregorianCalendar(year, month - 1, day) ;
hireDay = calendar.getTime();
 
public static void main(String[] args) // Отладочный модуль.
{
Employee е = new Employee ("Romeo", 50000, 2003, 3, 31);
e.raiseSalary(10);
System.out.princln(e.getName() + " " + e.getSalary());
}
}

Если вы хотите протестировать только класс Employee, выполните следующую команду:

java Employee

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

java Application

и метод main класса Employee никогда не будет выполнен.

Программа, показанная в листинге 4.3, содержит версию класса Employee со стати­ческим полем nextld и статическим методом gecNextld ( ). Массив заполняется тремя объектами Employee, а затем выводится информация о сотрудниках. В заключение на экране отображается очередной доступный идентификационный номер.

Заметим, что класс Employee имеет статический метод main () для модульного тестирования. Попробуйте выполнить метод main ( ) с помощью приведенных ниже команд.

java Employee и

java StaticTest

/**
@version 1.01 2004-02-19
@author Cay Horstmann
*/

 
public class StaticTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
 
staff[0] = new Employee("Tom", 40000);
staff[1] = new Employee("Dick", 60000);
staff[2] = new Employee("Harry", 65000);
 
// print out information about all Employee objects
for (Employee e : staff)
{
e.setId();
System.out.println("name=" + e.getName()
+ ",id=" + e.getId()
+ ",salary=" + e.getSalary());
}
 
int n = Employee.getNextId(); // calls static method
System.out.println("Next available id=" + n);
}
}
 
class Employee
{
public Employee(String n, double s)
{
name = n;
salary = s;
id = 0;
}
 
public String getName()
{
return name;
}
 
public double getSalary()
{
return salary;
}
 
public int getId()
{
return id;
}
 
public void setId()
{
id = nextId; // set id to next available id
nextId++;
}
 
public static int getNextId()
{
return nextId; // returns static field
}
 
public static void main(String[] args) // unit test
{
Employee e = new Employee("Harry", 50000);
System.out.println(e.getName() + " " + e.getSalary());
}
 
private String name;
private double salary;
private int id;
private static int nextId = 1;
}

Параметры методов

Рассмотрим термины, которые используются для описания способа передачи пара­метров методам (или функциям). Термин вызов по значению (call by value) означает, что метод получает значение, переданное ему вызывающим модулем. В противоположность этому вызов по ссылке (call by reference) означает, что метод получает от вызывающего модуля адрес (location) переменной. Таким образом, метод может модифицировать значе­ние, хранящееся в переменной, переданной по ссылке, но не может изменять значение переменной, переданной по значению. Термин "вызов по..." относится к стандартной компьютерной терминологии, описывающей способ передачи параметров в различных языках программирования, а не только в языке Java. (На самом деле, существует еще и третий способ передачи параметров — вызов по имени (call by name), представляющий в основном исторический интерес, поскольку он был применен в языке Algol, одном из первых языков программирования высокого уровня.)

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

Рассмотрим следующий вызов:

double percent = 10; 
harry.raiseSalary(percent);

He имеет значения, как именно реализован метод, мы знаем, что после его вызова значение переменной percent останется равным 10.
Изучим эту ситуацию подробнее. Допустим, что метод пытается утроить значение параметра.

public static void tripleValue(double x); // He работает. {
x = 3 * x;
}

Вызовем этот метод.

double percent = 10; 
tripleValue(percent);

Однако такой способ не работает. После вызова метода значение переменной percent остается равным 10. Происходит следующее.
1. Переменная х инициализируется копией значения параметра percent (т.е. числом 10). ,
2. Значение переменной х утраивается — теперь оно равно 30. Однако значение переменной percent остается равным 10 (рис. 4.6).
3. Метод завершает свою работу, и параметр х больше не используется.

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

  • Простые типы (числа, логические переменные и т.д.).
  • Ссылки на объекты.

Файл:Cj2I.pic.4.6.png

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

public static void tripleSalary(Employee x) // Работает {
x.raiseSalary(200);
}

При обработке следующего фрагмента кода будут выполнены перечисленные ниже действия.

harry = new Employee(...); 
tripleSalary (harry) ;

1. Переменная x инициализируется копией значения переменной harry, т.е. ссылкой на объект.

2. Метод raiseSalary () применяется к этой ссылке на объект. Метод объекта Employee, на который указывают ссылки х и harry, увеличивает зарплату сотрудников на 200%.

3. Метод завершает свою работу, и параметр х больше не используется. Разумеет­ся, переменная harry продолжает ссылаться на объект, в котором зарплата увеличена втрое (рис. 4.7).

Как видите, реализовать метод, изменяющий состояние объекта, передаваемого как параметр, довольно легко. Фактически такие изменения вносятся очень часто. Причина этого проста — метод получает копию ссылки на объект, поэтому и копия, и оригинал ссылки указывают на один и тот же объект.

Файл:Cj2I.pic.4.7.png

Во многих языках программирования (в частности, в языках С++ и Pascal) есть два способа передачи параметров: вызов по значению и вызов по ссылке. Некоторые программисты (и, к сожалению, даже авторы некоторых книг) утверждают, что язык Java использует при передаче объектов только вызов по ссылке. Однако это не так. Попробуем написать метод, выполняющий обмен двух объектов Employee.

public static void swap(Employee x, Employee у) //He работает. {
Employee temp = x;
x = y;
у = temp;
}

Если бы в языке Java для передачи объектов в качестве параметров использовался вызов по ссылке, этот метод работал бы так:

Employee а = new Employee("Alice", . . .); 
Employee b = new Employee("Bob", . . .);
swap (a, b) ;
// Ссылается ли теперь а на Bob, a b — на Alice?

Однако на самом деле этот метод не меняет местами объектные ссылки, храня­щиеся в переменных а и Ь. Параметры х и у метода swap () инициализируются копиями этих ссылок. Затем метод меняет местами эти копии.

// Ссылка х соответствует Alice, а ссылка у — Bob. 
Employee temp = х;
х = у;
у = temp;
// Теперь х ссылается на Bob, а у — на Alice.

В конце концов, следует признать, что все было напрасно. По завершении работы метода переменные х и у уничтожаются, а исходные переменные а и b продолжают ссылаться на прежние объекты (рис. 4.8).

Файл:Cj2I.pic.4.8.png

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

Ниже перечисляется, что может делать метод со своими параметрами, а что — нет.

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

Программа, приведенная в листинге 4.4, иллюстрирует эти утверждения. Сначала программа безуспешно пытается утроить значение числового параметра.

Проверка метода CripleValue: До: percent=10.0 В конце метода: х=30.0 После: percent=10.0

Затем программа успешно утраивает зарплату сотрудника.

Проверка метода tripleSalary: До: salary=50000.0 В конце метода: salary=150000.0 После: salary=15000.0

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

В заключение программа, демонстрирует безрезультатную работу метода swap ().

Проверка метода swap: До: a=Alice До: b=Bob

В конце метода: x=Bob В конце метода: y=Alice После: a=Alice После: b=Bob

Как видим, переменные х и у меняются местами, однако переменные а и Ь оста­ются прежними.


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ применяется как вызов по значению, так и вызов по ссылке. Например, легко можно реализовать методы void triplevalue (doubles х) или void swap(Employее& x, Emloyee& у). Эти методы модифицируют свои параметры, являющиеся ссылками
/**
@version 1.00 2000-01-27
@author Cay Horstmann
*/

 
public class ParamTest
{
public static void main(String[] args)
{
/*
Test 1: Methods can't modify numeric parameters
*/

System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);
 
/*
Test 2: Methods can change the state of object
parameters
*/

System.out.println("\nTesting tripleSalary:");
Employee harry = new Employee("Harry", 50000);
System.out.println("Before: salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());
 
/*
Test 3: Methods can't attach new objects to
object parameters
*/

System.out.println("\nTesting swap:");
Employee a = new Employee("Alice", 70000);
Employee b = new Employee("Bob", 60000);
System.out.println("Before: a=" + a.getName());
System.out.println("Before: b=" + b.getName());
swap(a, b);
System.out.println("After: a=" + a.getName());
System.out.println("After: b=" + b.getName());
}
 
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}
 
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
System.out.println("End of method: salary="
+ x.getSalary());
}
 
public static void swap(Employee x, Employee y)
{
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}
 
class Employee // simplified Employee class
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
 
public String getName()
{
return name;
}
 
public double getSalary()
{
return salary;
}
 
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
 
private String name;
private double salary;
}

Формирование объектов

Мы уже знаем, как можно писать простые конструкторы, определяющие началь­ные состояния объектов. Однако создание объектов — очень важная задача, поэтому в языке Java предусмотрено много разнообразных механизмов для создания конструк­торов. Эти механизмы рассматриваются ниже.

Перегрузка

Как вы помните, в классе GregorianCalendar предусмотрено несколько конст­рукторов. Создать объект этого класса позволяет выражение

GregorianCalendar today = new GregorianCalendar();

To же можно сделать с помощью следующей строки кода:

GregorianCalendar deadline = new GregorianCalendar(2099, Calendar, DECEMBER, 31);

Такая возможность называется перегрузкой (overloading). О перегрузке говорят в том случае, если несколько методов (в нашем случае несколько конструкторов) имеют одина­ковое имя, но разные параметры. Компилятор должен сам определить, какой метод вы­звать, сравнивая типы параметров, описанных в заголовках методов, с типами значений, указанных в вызове. Если ни один метод не соответствует вызову либо если одному вызову одновременно соответствует несколько вариантов, возникает ошибка компиляции. (Этот процесс называется разрешением перегрузки (overloading resolution).)


ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке Java можно перегрузить любой метод, а не только конструкторы. Таким обра­зом, чтобы полностью описать метод, нужно указать его имя и типы параметров. Подобное написание называется сигнатурой метода (signature). Например, в классе String есть четыре общедоступных метода indexOf ( ). Они имеют следующие сигнатуры:
indexOf(int) 
indexOf(int, int)
indexOf(String)
indexOf{String, int)

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

Инициализация полей по умолчанию

Если значение поля в конструкторе явно не задано, ему автоматически присваива­ется значение по умолчанию: числам — нули, логическим переменным — значения false, а ссылкам на объект — null. Однако полагаться на действия по умолчанию считается плохим стилем. Если поля инициализируются неявно, программа становит­ся менее понятной.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Между полями и локальными переменными есть существенная разница. Локальные переменные всегда должны явно инициализироваться в методе. Однако, если поле в составе класса не инициализируется явно, ему автоматически присваивается значе­ние, задаваемое по умолчанию (0, значение false или ссылка null).

Рассмотрим в качестве примера класс Employee. Допустим, что в конструкторе значения некоторых полей не заданы. По умолчанию поле salary должно инициали­зироваться нулем, а поля name и hireDay — ссылкой null. Однако при вызове мето­дов getName ( ) или getHireDay ( ) ссылка null может оказаться совершенно неже­лательной.

Date h = harry.getHireDay();
calendar.setTime(h); // Если h = null, генерируется исключение.

Конструктор по умолчанию

Конструктор по умолчанию (default constructor) не имеет параметров. Например, конструктор по умолчанию класса Employee имеет следующий вид:

public Employee() {
name = "";
salary = 0;
hireDay = new Date();
}

Если в классе вовсе не определены конструкторы, вызывается конструктор по умол­чанию. Этот конструктор присваивает всем полям экземпляра их значения, предусмот­ренные по умолчанию. Так, все числовые значения, содержащиеся в полях экземпляра, окажутся равными нулю, логические значения — false, а ссылки на объекты —null.

Если в классе есть хотя бы один конструктор и явно не определен конструктор без параметров, то при создании объектов необходимо задавать параметры конструкто­ра. Например, класс Employee в листинге 4.2 имеет один конструктор:

Employee(String name, double salary, int у, int m, int d)

В этом случае нельзя создать объект, поля которого принимали бы значения по умолчанию. Другими словами, представленный ниже вызов приведет к возникнове­нию ошибки.
е = new Employee ();


ВАЖНОЕ ЗАМЕЧАНИЕ !
Учтите, что конструктор по умолчанию вызывается, только если в классе не опреде­лены другие конструкторы. Если есть хотя бы один конструктор с параметрами и не­обходимо создавать экземпляр класса с помощью приведенного ниже выражения, следует явно определить конструктор без параметров.
new ИмяКласса ()

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

public ИмяКласса ()
{
}

Явная инициализация полей

Поскольку конструкторы в классе можно перегружать, есть несколько способов задать начальное состояние полей его экземпляров. Всегда следует быть уверенным, что, незави­симо от вызова конструктора, все поля экземпляра имеют некие разумные значения.
Есть возможность присвоить каждому полю соответствующее значение в опреде­лении класса. Например:

class Employee 
{
...
private String name = "";
}

Это присваивание выполняется до вызова конструктора. Такой подход особенно полезен тогда, когда надо, чтобы, независимо от вызова конструктора класса, поле имело конкретное значение.

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

class Employee {
static int assignld()
int r = nextld;
nextld ++;
return r;
}
private int id = assignld();
}

В языке С++ нельзя инициализировать поля экземпляра непосредственно в описании класса. Значения всех полей должны задаваться в конструкторе. Однако в языке С++ есть синтаксическая конструкция, называемая списком инициализации (initializer list).

Employee:: Employee(String n, double s, int y, int m, int d) // С++ 
: name(n),
salary(s),
hireDay(y, m, d)
{
}

В языке С++ эта синтаксическая конструкция используется для вызова конструкторов полей экземпляра. В языке Java поступать так нет никакой необходимости, поскольку объекты не могут содержать другие объекты; разрешается иметь только ссылки на них.

Имена параметров

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

public Employee(String n, double s) {
name = n; salary = s;
}

Однако недостаток такого подхода заключается в том, что, читая программу, невозможно понять, что означают параметры n и s.

Некоторые программисты добавляют к осмысленным именам параметров пре­фикс "а".

public Employee(String aName, double aSalary) {
name = aName; salary = aSalary
}

Такая программа вполне понятна. Любой читатель может сразу определить, в чем заключается смысл параметра. Есть еще один широко распространенный прием. Чтобы использовать его, надо знать, что параметры маскируют (shadow) поля экземпляра с одинаковыми именами. Например, если вызвать метод с параметром salary, то эта переменная будет ссылаться на параметр, а не на поле экземпляра. Доступ к полю экземпляра осуществляется с помощью выражения this.salary. Напомним, что клю­чевое слово this означает неявный параметр, т.е. создаваемый объект. Следующий пример иллюстрирует данный подход:

public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}

ВАЖНОЕ ЗАМЕЧАНИЕ !
В языке С++ к именам полей экземпляра обычно добавляют префиксы, представ­ляющие собой либо символ подчеркивания, либо букву (нередко для этой цели ис­пользуется m или х). Например, поле, в котором записан размер зарплаты, может называться _salary, mSalary или xSalary. Разработчики, создающие программы на языке Java, как правило, так не поступают.

Вызов одного конструктора из другого

Ключевое слово this ссылается на неявный параметр метода. Однако у этого сло­ва есть еще одно значение.
Если первый оператор конструктора имеет вид this (...), то вызывается.другой конструктор этого же класса. Ниже приведен типичный пример.

public Employee(double s) {
// Вызывает конструктор Employee{String, double).
this("Employee " + nextld, s);
nextld++;
}

Если вы используете выражение new Employee (60000), то конструктор Employee (double) вызывает конструктор Employee (String, double).
Применять ключевое слово this для вызова другого конструктора очень полез­но — при этом достаточно лишь один раз написать общий код, создающий объект.


ВАЖНОЕ ЗАМЕЧАНИЕ !
Объект this в языке Java идентичен указателю this в языке С++. Однако в языке С++ невозможно вызвать один конструктор с помощью другого. Для того чтобы реа­лизовать общий код инициализации объекта в языке С++, нужно создать отдельный метод.

Инициализационные блоки

На самом деле в языке Java существует и третий механизм — использование инициализационного блока (initialization block). Такой блок выполняется каждый раз, когда создается объект данного класса. Рассмотрим следующий пример кода:

class Employee {
public Employee(String n, double s) {
name = n;
salary = s;
}
public Employee() {
name = "";
salary = 0;
}
...
private static int nextld;
private int id;
private String name;
private double salary;
// Инициализационный блок {
id = nextId nextId ++;
}
}

В этом примере начальное значение поля id задается в инициализационном блоке объекта, причем неважно, какой именно конструктор используется для создания экземпляра класса. Инициализационный блок выполняется первым, а тело конструк­тора—после него.

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

Уничтожение объекта и метод finalize ()

Пакеты

Импортирование классов

Импортирование статических методов и полей

Добавление классов в пакеты

Как виртуальная машина определяет, где находятся классы

Область видимости пакета

Комментарии и документирование

Включение комментариев

Комментарии к классу

Комментарии к методам

Комментарии к полям

Комментарии общего характера

Комментарии к пакетам и обзоры

Извлечение комментариев

Рекомендации по разработке классов

Шаблон:Sam

]]>
Книги по Java https://linexp.ru?id=4716 Wed, 29 Jun 2022 13:54:27 GMT
<![CDATA[Безколекторні двигуни постійного струму 2210N 1000Kv]]> Вітаю читачів!

Ми отримали посилкою із Сінгапуру безколекторні двигуни постійного струму 2210N 1000Kv

Безколекторні двигуни постійного струму 2210N 1000Kv
Характеристики:
Вага: 45г
Діаметр валу: 3мм
Kv: 1000 об за хвилину / вольт
Максимальний струм: 11.2А
Максимальна напруга: 11В
Довжина: 28мм
Діаметр: 28мм
Видима частина валу: 18мм
Повна довжина: 46мм
Безколекторний двигун постійного струму 2210N 1000Kv
 
Можливі конфігурації:
Конфігурація 1:
Пропелери: GWS9050
Акумулятор: @3S
Максимальний струм на вході: 11.2А
Кутова швидкість: 8700об/хв
Тяга: 740г
Конфігурація 2:
Пропелери: APC9x3.8
Акумулятор: @2S
Максимальний струм на вході: 6.5А
Кутова швидкість: 6260об/хв
Тяга: 405г

Ми уже встигли розібрати і зібрати один із них, тому скоро буде викладений фото-звіт :)




Дата публікації: 2014-10-08 10:10


Автор статті: Ярослав
]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4715 Wed, 29 Jun 2022 13:52:16 GMT
<![CDATA[Коротка історія розвитку вертолітної техніки]]> Коротка історія розвитку вертолітної техніки

В бакалаврській роботі я описував історію розвитку вертолітної техніки. Інформацію збирав на цих ресурсах:

  1. Helicopter — Wikipedia, the free encyclopedia [Електронний ресурс] : (проект) / Фонд «Вікімедіа» (Wikimedia Foundation Inc.). — Електорон. дані (1 файл). — 2015. — Режим доступу: https://en.wikipedia.org/wiki/Helicopter. — Назва з екрану. — Дата звернення: 10.05.2015.
  2. The History of Helicopters [Електронний ресурс] — 2006. — Режим доступу: https://www.aviastar.org/history/
  3. Rise of the Multicopter | Model Aviation [Електронний ресурс] : видання Академії моделювання повітряних суден / Пол Джентайл — 2012. — Режим доступу: https://modelaviation.com/riseofmulticopter

Першим літальним апаратом, що здійснював вертикальний політ, прийнято вважати китайську дзиґу, також відому як бамбуковий вертоліт або бамбукову бабку. Вона являла собою іграшковий пропелер та ручку, що виступала в ролі валу. Пропелер спочатку насаджувався на ручку, і під час інтенсивного обертання ручки, пропелер здіймався в повітря. Ця іграшка існувала в Китаї приблизно в 400 році до нашої ери, в період Чжаньґо. Достеменно не відомо хто придумав цю іграшку і який вона мала вигляд, коли її вперше сконструювали, однак натураліст Лонуа із допомогою механіка Б’явеню в 1784 р. сконструювали невеличкий пристрій, який за принципом дії найбільш нагадував цю іграшку.

Гелікоптер Лонуа та Б’явеню 

Гелікоптер Лонуа та Б’явеню

В середині XIX століття в архівах Міланської бібліотеки були знайдені малюнки знаменитого італійського художника і вченого Леонардо да Вінчі (1452-1519 рр.) із зображенням літального апарата з великим гвинтом, який приводили в рух люди, розміщені на спеціальній платформі.

 Гелікоптер Леонардо да Вінчі

Гелікоптер Леонардо да Вінчі

Реальною спробою втілення ідеї вертольоту варто вважати проект видатного російського вченого М.В. Ломоносова, який побудував в 1754 р. модель літального апарата, яку назвав "Аэродромической машинкой".

Із початку XIX століття різні дослідники працювали над ідеєю гелікоптера. В 1818 р. Ламбертґі розробив модель пристрою, призначеного для вертикального польоту. Пізніше, в 1828 р. флорентійський швець Вітторіо Сарті розробив проект під назвою “Aereo Veliero” (літаючий вітрильник). Сер Джордж Кейлі, батько британської авіації, в 1843 р. розробив проект під назвою “Aerial Carriage” (літаюча карета). Два роки по тому Косус розробив проект літального апарата із трьома несучими гвинтами. Завдяки напрацюванням цих дослідників до середини XIX століття сформувалось хороше підґрунтя для подальших розробок гелікоптера.

Патент на проект гелікоптера вперше був виданий Генрі Брайту. Він розробив цей літальний апарат в 1861 р. Підіймальну силу утворюють два співвісні дволопатеві гвинти, що обертаються в різні сторони та закріплені на вертикально розміщеному валу. Принцип роботи гелікоптера Генрі Брайта був використаний в розробках багатьох винахідників XX століття: Берлінера, вон Кармана, Асбоза, Д’Асканіо, а також декількох сучасних компаній: Bendix, Breguet, Gyrodyne, Камов, Lualdi-Tassotti та інших.

 Гелікоптер Генрі Брайта

Гелікоптер Генрі Брайта

Термін «гелікоптер» придумав Ґустав де Понтон Д’Амкур. Винахідник використав його в своїй патентній заяві, яку подав в Англії 3 серпня 1861 р. Термін складається із двох грецьких слів ἕλιξ (гелікс) — гвинтова лінія та πτερόν (птерон) — крило.

В 1863 р. Д’Амкурт, очолюючи групу ентузіастів, побудував малу алюмінієву модель гелікоптера. Несучі гвинти приводились в рух паровим двигуном. Винахідник отримав патент на свою розробку в Англії та у Франції.

Французький розробник Альфонс Пенауд сконструював ряд моделей гелікоптерів, що базувались на принципі китайської дзиґи в 1870 р. В якості лопатей використовувались закручені гумові смужки. Деякі з його моделей підіймались на висоту в 15,5 м. Також Пенауд займався розробкою інших літальних апаратів: орнітоптерів та моделей планерних літаків, що запускались в політ за допомогою стартової катапульти. Ці моделі також здійснювали успішні польоти.

Аченбах в 1874 р. вперше використав хвостовий гвинт, щоби протидіяти обертальному моменту головного несучого гвинта. На сьогодні в галузі вертольотобудування одногвинтова схема із хвостовим рульовим гвинтом є найпоширенішою.

 Гелікоптер Аченбаха

Гелікоптер Аченбаха

В 1877 р. Енріко Форланіні, італійський професор, побудував гелікоптер, що важив 3.5 кг. Літальний апарат мав два несучих гвинта, які обертались за допомогою парового двигуна. Вертоліт був офіційно представлений в Ла Скала, Мілан. Літальний апарат злетів та піднявся на висоту 13 м.

Цього ж року Кастел побудував гелікоптер, який здійснив вдалий зліт.

До початку XX століття винахідники продовжували розроблювати проекти гелікоптерів. В 1879 р. Меліков розробив модель гелікоптера, що за своєю конструкцією нагадував гвинт, а Джон Грінус розробив та запатентував проект гелікоптера за поперечною схемою із великим крилом, Ватсон Квінбі отримав патент на свій проект гелікоптера, в якому роль лопатей підіймальних поверхонь відіграють невеликі вітрила. Едвард Джонстон із Алабами розробив проект гелікоптера із 6 пропелерами, для кожного із яких був передбачений окремий двигун, автор розробки отримав патент на свій винахід в 1888 р.

Нова сторінка в історії гелікоптерної техніки відкрилась із проведенням вдалого випробування винаходу Луї та Жака Бреге, над яким вони працювали спільно із професором Чарльзом Ріше в 1907 р. Gyroplane No.1 (гіроплан №1) — таке ім’я розробники надали своєму літальному апарату. Гелікоптер здійснив зліт із пілотом на борту і піднявся на висоту 60 см. Політ був дуже не стабільний, тому корпус винаходу під час випробування утримували асистенти. Варто зауважити, що Gyroplane No.1 — це перший відомий квадрокоптер.

Гелікоптер Бреге-Ріше 

Gyroplane No.1 Бреге-Ріше

Поль Корню в 1907 р. здійснив керований політ на своєму гелікоптері, побудованому за поздовжньою схемою. Вертоліт піднявся на висоту близько 2 м і провів в повітрі приблизно 20 с. Однак, літальний апарат не потребував стабілізації на землі.

 Гелікоптер Поля Корню

Гелікоптер Поля Корню

Якоб Еллєхамер в 1911 р. побудував гелікоптер за співвісною схемою, який здійснив декілька вдалих злетів, успішні випробовування цього вертольота проводились до вересня 1916 р. В 1922 р. Гелікоптер Ботезата та Жерома також здійснив успішний злет та піднявся в повітря на 1.8 м. В цьому ж році Рауль Патерас Пескара продемонстрував гелікоптер, побудований за співвісною схемою, він зміг провести в повітрі до 10 хв. Етьєн Омішен розробив та побудував шість різних гелікоптерів, найвидатнішим із яких був Oemichen No.2, 14 квітня 1924 р. Міжнародна федерація повітроплавання зафіксувала рекорд — гелікоптер Етьєна Омішена No.2 подолав дистанцію в 365 м. Три дні по тому результат вдалось збільшити до 525 м. 4 травня літальний апарат пробув в польоті 14 хвилин і вперше подолав відстань в 1 км.

В 1923 р. де ла Сірва, іспанський інженер, побудував літальний апарат, який може здійснювати безпечний політ на невеликих повітряних швидкостях. Розробник назвав свій винахід “Autogiro” (автожир). Літальний апарат мав фюзеляж як у літака, в носовій частині був встановлений пропелер та двигун, на верхній частині фюзеляжу був встановлений головний ротор, а на хвостовій — горизонтальний та вертикальний стабілізатори.

Автожир де ла Сірва 

Автожир де ла Сірва

Борис Н. Юрієв разом з Олексієм М. Черьомухіним в 1932 р. побудували гелікоптер ЦАГИ 1-ЭА14 за одногвинтовою схемою із двома пропелерами на кінцях рами для компенсування реактивного моменту. В серпні цього ж року гелікоптер Черьомухіна піднявся на висоту в 605 метрів.

В квітні 1933 р. Ніколас Флорін, використовуючи поздовжню схему, побудував перший тандемний гелікоптер. Особливість цього літального апарату була в тому, що передній та задній гвинти обертались в одному напрямку, а реактивний момент компенсувався за рахунок нахилу їхніх осей обертання в різні боки відносно гелікоптера.


 

Масове виробництво гелікоптерів бере свій початок із середини 30-х років XX століття. Генріх Фокке в 1933 р. отримав ліцензію на виробництво автожиру Cierva C.30. Пізніше розробник сконструював гелікоптер Focke-Wulf Fw 61 на основі поперечної схеми, який здійснив свій перший політ в 1936 р. Цей вертоліт побив усі встановлені гелікоптерами світові рекорди на 1937 р.

Під час Другої світової війни нацистська Німеччина використовувала гелікоптери для огляду, транспортування та евакуації потерпілих.

В Сполучених Штатах Америки Ігор Сікорський та Лоренc Лепаж займались розробкою гелікоптера для збройних сил Сполучених Штатів Америки. Лепаж побудував гелікоптер за поперечною схемою Platt-LePage XR-1. Сікорський надав перевагу одногвинтовій схемі із хвостовим рульовим гвинтом і побудував вертоліт Vought-Sikorsky VS-300, який став першим літальним апаратом, що знайшов практичне застосування.

Першим масово випущеним гелікоптером був R-4, розроблений в 1942 р. Сікорським на основі VS-300. Партія складала 100 вертольотів. Всього до закінчення Другої світової війни Ігор Сікорський виготовив 400 гелікоптерів.

Компанія Bell Aircraft спільно із компанією Arthur M. Young в 1946 р. виготовили гелікоптер Bell 47 за одногвинтовою схемою із заднім рульовим гвинтом. Цей вертоліт був популярним впродовж наступних 30 років.

 Гелікоптер Bell 47

Гелікоптер Bell 47

В 1951 р. Чарльз Каман модифікував свій синхроптер K-225, оснастивши його турбінним двигуном. В 1954 р. розробник встановив такий двигун на гелікоптер Navy HTK-1.

Наступного року вдалий політ здійснив Alouette II. Він був оснащений турбінним двигуном Turbomeca Artouste. Цей гелікоптер був поставлений на масове виробництво і випускався з 1956 р впродовж 19 років.

Турбінні двигуни здійснили революцію в авіаційній промисловості. Поява більш потужних газотурбінних двигунів вела до зменшення співвідношення між вагою двигуна і вертольота. Газотурбінні двигуни також більш надійні, ніж поршневі, особливо для підтримки достатньо високих рівнів потужності, необхідних для вертольотів. Газотурбінні двигуни змогли зменшити так, що всі вертольоти, окрім найменших моделей, можуть працювати на цих двигунах. Наприклад, сучасні гелікоптери Sikorsky CH-53E Super Stallion та Mil Mi-26 оснащені турбінними двигунами.

На сьогодні дослідники ставлять перед собою завдання виключати пілота із процесу керування гелікоптера за необхідних умов, наприклад поганої видимості. Досягнути цієї мети можна за допомогою системи автоматичного керування.




Дата публікації: 2015-06-27 12:12
]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4714 Wed, 29 Jun 2022 13:51:29 GMT
<![CDATA[Огляд відладочної плати Silicon Laboratories на базі мікроконтролера C8051F310-TB]]> Дата публікації: 2014-04-24 20:08

Автор статті: Ярослав

  Вітання всім користувачам! Врешті-решт, довівши сайт до пристойного вигляду, можна потрохи перебиратись до суті.

  Невід’ємним існтрументом для нашої роботи буде відладочна плата. Її ми плануємо в першу чергу використовувати для розроблення і зневадження програми керування кутовими швидкостями двигунів.

  Ми маємо доступ до ось такого комплекту розробника від SiLabs

Комплект розробки для плати SiLabs на базі мікроконтроллера C8051F310-TB

  Звісно, найважливіший інструмент серед представлених - це відладочна плата, давайте глянемо, який вигляд вона має!

Розглянемо:

  1. Загальний вигляд плати.
  2. Структурну схему плати.
  3. Конектори на платі.
  4. Підключення плати до комп'ютера.

Загальний вигляд плати

Відладочна плата SiLabs C8051F310

Відладочна плата SiLabs C8051F310

  Як ви бачите на фотографії, плата має дві ділянки: безпосередньо сама плата, що забезпечує роботу мікроконтролера і ділянка, виконана у вигляді макетної плати для підключення пристроїв і приладів користувача. Конектори, світлодіоди і кнопочки розглядаються далі в статті. Сама по собі плата невеличка, її габаритні розміри 101 x 85 x 14 мм. Плата виготовлена якісно, елементи, які до неї підпаяні - стандартні, їх можна знайти, наприклад, на столичному радіоринку ;) Також плату можна розширювати кристалами користувача.

  На сайті виробника представлена необхідна документація і програмне забезпечення. Загалом вичерпна інформація по самому комплекту для розробки міститься в документі C8051F31x-DK.pdf, а усе, що буде необхідно для написання програм в Data-Sheet C8051F31x.pdf.

  В цій статті представлений переклад деяких пунктів мануалу C8051F31x-DK.pdf

Вгору

Структурна схема платиC8051F310

 Схематичне зображення відладочної плати SiLabs C8051F310

  На борту у неї є 10 конекторів:

P1 - Конектор для живлення (вхід: напруга від 7В до 15В постійного струму, що подається від нерегульованого адаптеру живлення)

J1 - 34-піновий розширений конектор для вводу/виводу

J3 - конектор для налаштування режимів роботи порту вводу/виводу

J4 - DEBUG конектор для Debug адаптера

J5 - DB-9 конектор для UART0 RS232 інтерфейсу

J6 - Блок вводів/виводів для аналогових сигналів

J7 - конектор для низькочастотного фільтру

J8 - конектор для USB Debug адаптера

J9, J10 - конектори для підключення зовнішніх кристалів

  Також на платі розміщено дві кнопки і два світлодіода:

S1 (RESET) - встановлює лічильник команд на нульову команду

S2 (P0.7) - встановлює на пін P0.7 34-пінового конектора верхній рівень сигналу

D2 - зелений світлодіод, сигналізує стан порту P3.3

D3 - червоний світлодіод, сигналізує про те, що на плату подається напруга живлення

Вгору

Опис конекторів

  34-піновий конектор для вводу/виводу забезпечує доступ до всіх сигнальних пінів C8051F310 контролеру. Також доступні піни для напруги +3 V, цифрового заземлення і виводу вбудованого низькочастотного фільтру. Також на платі є ділянка макетної плати Prototype Area (див. схематичне зображення). Всі вхідні/вихідні сигнали, що надходять на J1 конектор, також надходять на контакти Prototyping Area I/O Connection Points (див. схематичне зображення). Кожен із цих контактів має відповідне маркування, що відповідає маркуванню пінів конектора J1.

  Табличка, в якій описані піни J1 конектора: 

Pin #

Опис

Pin #

Опис

Pin #

Опис

Pin #

Опис

1+3 VD (+3.3 VDC)11P1.021P2.231P3.4
2PWM Output12P1.122P2.332/RST (Reset)
3P0.013P1.223P2.433GND (Ground)
4P0.114P1.324P2.534GND (Ground)
5P0.215P1.425P2.6  
6P0.316P1.526P2.7  
7P0.417P1.627P3.0  
8P0.518P1.728P3.1  
9P0.619P2.029P3.2  
10P0.720P2.130P3.3  

  DEBUG інтерфейс (J4) надає доступ до DEBUG (C2) пінів C8051F310. Його використовують для підключення Serial адаптеру або USB Debug адаптеру до відладочної плати для безпосереднього (in-circuit) зневадження і запису програм в пам'ять контролера.

  Табличка, в якій описані піни J1 конектора:

Pin #

Опис

1+3 VD (+3.3 VDC)
2, 3, 9GND (Ground)
4C2D
5/RST (Reset)
6P3.0
7C2CK
8Not Connected
10USB Power

  Послідовний інтерфейс (J5)

  Електричне коло RS232 передавача і DB-9 (J5) конектор, що наявні на платі, призначені для зручності підключення до UART0 (універсальний асинхронний ресивер-трансміттер) мікроконтролера. Сигнали TX, RX, RTS і CTS ресивера-трансміттера можуть подаватись на DB-9 конектор і на передавач, якщо встановити перемички на відповідні контакти конектора J3:

J3[5-6] - Встановіть перемичку сюди, щоб з'єднати UART0 TX (P0.4) із передатчиком (transceiver)

J3[7-8] - Встановіть перемичку сюди, щоб з'єднати UART0 RX (P0.5) із передатчиком (transceiver)

J3[9-10] - Встановіть перемичку сюди, щоб з'єднати UART0 RTS (P3.1) із передатчиком (transceiver)

J3[11-12] - Встановіть перемичку сюди, щоб з'єднати UART0 CTS (P3.2) із передатчиком (transceiver)

  Аналогові вводи/виводи (J6)

  Деякі піни мікроконтролера C8051F310 підключені до блоку J6. В наступній таблиці показано, які саме піни мікроконтролера підключені до блоку аналогових вводів/виводів:

Pin #

Опис

1P2.5 / AIN2.5
2AIN2.4
3GND (Ground)
4P0.0 / Vref (Voltage Reference)

  Встановіть перемичку на J7[2-3] щоб з'єднати AIN2.4 ввід із P2.4 піном мікроконтролера.

  USB Debug адаптер (J8) також має контакти для подачі живлення на плату. Вони розведені на J4[10] і J8[1]. Встановіть перемичку на J8[2-3], щоби подати живлення на плату напряму від блоку живлення (ac/dc адаптеру живлення). Або ж встановіть перемичку на J8[1-2], щоб подати живити плату від USB Debug адаптеру. Зауважте, що другий варіант конфігурації не підтримується ані EC1, ані EC2 Serial адаптерами.

  Низькочастотний фільтр (J7), що розташований на відладочній платі може бути підключеним до піну P2.4 (на порті). Для цього встановіть перемичку на J7[1-2], щоб підключити пін P2.4 мікроконтролера до низькочастотного фільтру. Вивід низьчкочастотного фільтру можна знімати як PWM сигнал на J1[2]. Мікроконтролер C8051F310 може бути запрограмований на генерування сигналу широтно-імпульсної модуляції (Pulse Width Modulated waveform) який потім подається на вхід низькочастотного фільтру, щоб реалізувати цифро-аналоговий перетворювач, що керується користувачем. Перегляньте додаток до документації: "AN107:  Implementing 16-Bit PWM Using the PCA" (Реалізація 16-бітового сигналу широтно-імпульсної модуляції із використанням масиву програмованих лічильників (Programmable Counter Array)) в теці "documentation" на диску для того, щоб дізнатись як згенерувати напругу прямого струму значення, заданого в програмі за допомогою широтно-імпульсної модуляції і низькочастотного фільтру.

Вгору

Підключення до комп'ютера

  Підключення здійснюється за наступною схемою:

Підключення відладочної плати SiLabs до комп'ютера

  При цьому важливо запустити IDE від SiLabs, і натиснути сполучення кнопок ALT + C або в головному меню вибрати Debug - Connect, тобто підключитись до плати, тільки після цього USB Debug адаптер почне працювати.

Подяка

  Дякую тобі, гостю, що прочитав цю статтю! Нам приємно, що наша праця стала тобі в пригоді. Якщо десь в цій статті ти помітив неточності або ж у тебе є запитання і ти хочеш отримати на них відповіді - сміливо залишай коментарі або пиши нам на пошту! Щасти!

]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4713 Wed, 29 Jun 2022 13:50:54 GMT
<![CDATA[Плани на найближчий час]]> Дата публікації: 2014-05-10 15:03

Автор статті: Ярослав

Бажаємо доброгу часу доби нашим читачам!

Ось список справ, який ми плануємо зробити найближчим часом.

1. Новина по основному завданню, яке ми робили останнім часом - коробочка для відладочної плати. Там ми розкажемо про різні нюанси через які все не було так легко.

2. Вибір і придбання двигунів для квадрокоптера. Звісно основним елементом БПЛА є те, що його піднімає в повітря. Це будуть двигуни із лопатями. на разі точно відомо, що це будуть безколекторні двигуни постійного струму. По цій темі спробуємо відзняти відео, може зробити іще якийсь додатковий матеріал. Точно буде новина, в якій розпишемо процес придбання двигуна. Побутує думка, що такі специфічні запчастини ліпше купувати закордоном. Спробуємо щось знайти у нас в Києві.

Двигун із зарубіжного сайту

І останнє з приводу сайту. Якщо у вас є якісь побажання або зауваження, то залишайте коментарі або пишіть на пошту і ми глянемо що можна із цим зробити.

Дякую за увагу Вам, читачі! Удачі Вам.

:)

]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4712 Wed, 29 Jun 2022 13:50:09 GMT
<![CDATA[Плата Arduino Uno В робочому арсеналі]]> Вітаю читачів нашого сайту!

Після виконання бартеру, ми отримали в користування плату Arduino Uno.

Arduino Uno

 Очікуйте в майбутньому:

  • нові мануали
  • деякі оновлення сайту

Дякую за увагу!

:)

]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4711 Wed, 29 Jun 2022 13:49:32 GMT
<![CDATA[Плата розподілення живлення]]> Привіт користувачам!

Плата розподілення живлення

Для нашого квадрокоптера ми виготовили плату розподілення живлення.

Деатальніше про це ви можете дізнатись в повній версії статті.

1) Спочатку ми зняли розміри платформи на яку буде встановленя плата розподілення живлення (ПРЖ).

Перший ярус рами квадрокоптера

Перший ярус рами квадрокоптера

2) Із текстолітової плати вирізали прямокутник та зрізали на ньому кути, які закривали отвори для гвинтів, а також висвердлили отвори для закріплення ПРЖ на першому ярусі квадрокоптера.

Текстолітова плата

 Текстолітова плата

3) На текстолітовій платі канцелярським ножен вирізали доріжки для проводів.

Вирізання доріжок плати розподілення живлення

Вирізання доріжок плати розподілення живлення

4) Пролудили доріжки.

Нанесення припою на доріжки

Нанесення припою на доріжки

Так виглядає результат:

Залуджені доріжки на платі розподілення живлення

Залуджені доріжки на платі розподілення живлення

5) Зайвий припой прибрали канцелярським ножем.

Прибирання зайвого припою

Прибирання зайвого припою

6) Продзвонили доріжки, щоб впевнитись, що вони не контактіють між собою.

Перевірка опору між доріжками

Перевірка опору між доріжками

7) Припаяли попередньо підготовлений конектор XT60.

Припаювання конектора XT60

Припаювання конектора XT60

8) Припаяли проводи до 3.5 мм bullet-конекторів.

Для цього залудили провід.

Лудіння проводу

Лудіння проводу

В 3.5 мм конектор кинули шматочок каніфолі.

Кидаємо шматочок каніфолі всередину 3.5 мм bullet конектора

Кидаємо шматочок каніфолі всередину 3.5 мм bullet конектора

Розігріли конектор.

Розігрівання 3.5 мм bullet конектора

Розігрівання 3.5 мм bullet конектора

Наплавили в конектор припой.

Лудіння 3.5 мм bullet конектора

Лудіння 3.5 мм bullet конектора

Припаяли провід до конектора.

Припаювання проводу до 3.5 мм bullet конектора

Припаювання проводу до 3.5 мм bullet конектора

На цьому кроці проводи із конекторами виглядають так:

Проводи припаяні до 3.5 мм bullet конекторів

Проводи припаяні до 3.5 мм bullet конекторів

Заізолювали конектори за допомогою термозбіжної трубки.

Ізолювання проводу за допомогою термозбіжної трубки

Ізолювання проводу за допомогою термозбіжної трубки

Результат роботи:

Проводи заізольовані термозбіжною трубкою

Проводи заізольовані термозбіжною трубкою

9) Припаяли підготовлені проводи до ПРЖ.

Припаювання проводів із 3.5 мм bullet конекторами до плати

Припаювання проводів із 3.5 мм bullet конекторами до плати

Так виглядає результат:

Плата розподілення живлення із припаяними XT60 і 3.5 мм bullet конекторами

Плата розподілення живлення із припаяними XT60 і 3.5 мм bullet конекторами

10) Нарешті, припаяли до ПРЖ універсальний ланцюг перетворення напруги (UBEC або стабілізатор).

Плата розподілення живлення

Плата розподілення живлення

Якщо у Вас виникли запитання по цій статті - залишайте їх в коментарях або пишіть нам на пошту.

Дякуємо за увагу!




Дата публікації: 2015-03-23 21:09
]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4710 Wed, 29 Jun 2022 13:48:59 GMT
<![CDATA[Поточний етап]]> Привіт, ми працюємо.

Зараз на 2-ій знизу стадії:

undefined ]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4709 Wed, 29 Jun 2022 13:48:18 GMT
<![CDATA[Поточний стан проекту]]>     Вітаю відвідувачів нашого сайту!

    Радий Вам повідомити, що робота по конструюванню квадрокоптера іде у високому темпі. На сьогодні ми уже замовили майже всі необхідні комплектуючі. Також на руках у нас уже є контролер безпілотного літального апарата, GPS-модуль, двигуни та деякі інші деталі.

    Однак, в попередніх дописах є певні недоліки, які пов’язані із тим, що ми описували іще не зібраний літальний апарат. Виходячи із цього, краще буде спочатку повністю зібрати квадрокоптер і запустити його, а потім створювати дописи, які будуть ґрунтуватись на основі отриманого досвіду.

    Тому найближчих півтора місяці не будуть з’являтись пости, що напряму пов’язані зі збиранням квадрокоптера.

    Просимо із розумінням поставитись до цього наших читачів.

    Зичимо Вам вдячність за цікавість до нашого проекту!

    Команда HighWay-UA.

:)

Дата публікації: 2014-11-29 15:03


Автор статті: Ярослав
]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4708 Wed, 29 Jun 2022 13:47:42 GMT
<![CDATA[Поздоровлення із Днем Незалежності України]]>     Хочемо привітати Вас, шановні співвітчизники, із величним святом Незалежності України! В цей непростий час для нашого народу і для нашої держави, варто пам’ятати про наші вікові традиціїі і про наших предків, про віру й волю всіх, хто виборював та відстоював нашу землю, про мужність і звитягу наших воїнів, про всіх трударів, що невтомно працюють на благо нашої країни. Всі ми краплинки одного цілого - нашої прекрасної країни, нашої неповторної неньки України!

Козаки верхи

    Бажаємо сили і витримки, твердої віри у майбуття, затишку в серцях і безмежного кохання до рідної землі! Слава Україні!

:)

Дата публікації: 2014-08-24 14:02


Автор статті: Ярослав
]]>
Проект розроблення безпілотного летального апарата https://linexp.ru?id=4707 Wed, 29 Jun 2022 13:46:58 GMT