imageДобрый день, Хаброжители!
Пополняем коллекцию O'Reilly.

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

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

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

В процессе изучения Java вы освоите эффективный и практичный подход к разработке программного обеспечения, началом которого станет глубокое понимание основ языка Java и его API.

На первый взгляд Java имеет много общего с языками C и C++, и если у вас есть опыт программирования на одном из них, то вам будет проще изучить Java. Но если у вас нет такого опыта, не огорчайтесь. Не надо уделять излишнее внимание синтаксическому сходству между Java и C или C++. Во многих отношениях Java ближе к более динамическим языкам, таким как Smalltalk и Lisp. Хорошо, если вы уже знаете другие объектно-ориентированные языки, но в этом случае вам придется, скорее всего, пересмотреть некоторые представления и изменить некоторые привычки. Считается, что язык Java намного проще таких языков, как C++ и Smalltalk. Если вы хорошо учитесь на коротких примерах и на собственном опыте, то эта книга должна вам понравиться.

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


Класс Thread и интерфейс Runnable


Выполняемый код в Java всегда связан с объектом Thread, начиная с главного метода, который запускается виртуальной машиной Java для старта вашего приложения. Новый поток порождается при создании экземпляра класса java.lang.Thread. Объект Thread представляет реальный поток в интерпретаторе Java и служит своего рода дескриптором для управления им и для координации его выполнения. С его помощью вы можете запустить поток, дождаться завершения потока, заставить его приостановиться на некоторое время или прервать его выполнение. Конструктор класса Thread получает информацию о том, где поток должен начать свое выполнение. На концептуальном уровне нам хотелось бы просто сообщить конструктору, какой метод должен выполняться. Это можно сделать несколькими способами; Java 8 поддерживает ссылки на методы, которые позволяют это сделать. Но сейчас мы выберем обходной путь и воспользуемся интерфейсом java.lang.Runnable для создания или пометки объекта, содержащего «исполняемый» метод. Интерфейс Runnable определяет один метод общего назначения run():

public interface Runnable {
     abstract public void run();
}

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

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

Создание и запуск потоков


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

Рассмотрим пример. В классе Animator реализуется метод run(), управляющий циклом прорисовки, который служит в нашей игре для обновления Field:

class Animator implements Runnable {
     boolean animating = true;

     public void run() {
          while ( animating ) {
               // Переместить яблоки на один "кадр"
               // Перерисовать поле
               // Сделать паузу
               ...
          }
     }
}

Чтобы его использовать, мы создаем объект Thread, передаем ему экземпляр Animator как целевой объект и вызываем его метод start(). Все эти действия можно выполнить явно:

Animator myAnimator = new Animator();
Thread myThread = new Thread(myAnimator);
myThread.start();

Здесь создается экземпляр класса Animator, который передается в аргументе конструктора в myThread. Как показано на рис. 9.1, при вызове метода start() объект myThread начинает выполнять метод run() класса Animator. Вперед!

image

Естественное создание потока

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

Тем не менее надо представить и другой способ создания потоков. В этом варианте целевой класс определяется как субкласс типа, который уже поддерживает Runnable. Оказывается, сам класс Thread для удобства реализует интерфейс Runnable; он содержит метод run(), который можно напрямую переопределить в соответствии с вашими целями:

class Animator extends Thread {
     boolean animating = true;

     public void run() {
          while ( animating ) {
               // Рисование в окне
               ...
          }
     }
}

Основа класса Animator выглядит примерно так же, как прежде, за исключением того, что наш субкласс теперь стал субклассом Thread. В соответствии с этой схемой конструктор по умолчанию класса Thread назначает самого себя целью, то есть по умолчанию Thread выполняет собственный метод run() при вызове метода start(), как показано на рис. 9.2. Теперь наш субкласс может просто переопределить метод run() из класса Thread. (Сам класс Thread просто определяет пустой метод run().)

image

Затем мы создаем экземпляр класса Animator и вызываем его метод start() (который также наследуется от Thread):

Animator bouncy = new Animator();
bouncy.start();

Также можно приказать объекту Animator запускать его поток при создании:

class Animator extends Thread {

     Animator () {
          start();
     }
     ...
}

Здесь объект Animator просто вызывает свой метод start() при создании экземпляра. (Вероятно, лучше запускать и останавливать объекты после их создания, вместо того чтобы полагаться на запуск потоков как на скрытый побочный эффект создания объекта, но для примера сойдет.)

Может показаться, что субклассирование Thread позволяет удобно упаковать воедино поток и его целевой метод run(). Тем не менее это решение не всегда оказывается лучшим с точки зрения проектирования. Если вы субклассируете Thread для реализации потока, вы тем самым заявляете, что вам нужен новый тип объекта, являющийся разновидностью Thread, который предоставляет весь открытый API класса Thread. Заманчиво выглядит идея взять объект, основной задачей которого является выполнение задачи, и превратить его в Thread, но реальные ситуации, в которых вы хотите создать субкласс Thread, встречаются не очень часто. Обычно бывает более естественно формировать структуру классов на основании требований вашей программы и использовать Runnable, чтобы связать выполнение с логикой вашей программы.

Управление потоками


Мы уже показали, как использовать метод start() для запуска выполнения нового потока. Существует ряд других методов экземпляров для явного управления выполнением потока.

  • Статический метод Thread.sleep() приостанавливает на заданный период времени (с некоторой точностью) поток, выполняемый в настоящий момент; при этом не расходуется (или почти не расходуется) процессорное время.
  • Методы wait() и join() координируют выполнение двух и более потоков. Они будут подробно рассмотрены при обсуждении синхронизации потоков далее в этой главе.
  • Метод interrupt() возобновляет поток, приостановленный операцией sleep() или wait() либо заблокированный иным способом в продолжительной операции ввода-вывода.

Устаревшие методы


Также необходимо упомянуть о трех устаревших методах управления потоками: stop(), suspend() и resume(). Метод stop() является парным по отношению к start(); он уничтожает поток. Методы start() и stop() могут вызываться только один раз за весь жизненный цикл потока. С другой стороны, устаревшие методы suspend() и resume() использовались для произвольной приостановки и последующего перезапуска потока.

И хотя эти устаревшие методы все еще поддерживаются в Java (и скорее всего, будут поддерживаться всегда), их не надо использовать при разработке нового кода. Проблема stop() и suspend() заключается в том, что эти методы перехватывают управление над выполнением потоков жестко и некоординированно. Это усложняет программирование; приложение не всегда может предвидеть возможное прерывание его выполнения в любой точке и корректно восстановиться после него. Более того, когда управление потоком перехватывается одним из этих способов, исполнительная система Java должна освободить все свои внутренние блокировки, использованные для синхронизации потоков. Это может привести к неожиданному поведению, а в случае вызова метода suspend(), который не освобождает эти блокировки, легко может привести к взаимной блокировке.

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

Метод sleep()


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

try {
     // Текущий поток
     Thread.sleep( 1000 );
} catch ( InterruptedException e ) {
     // Кто-то преждевременно нас разбудил
}

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

Метод join()


Наконец, если операция должна координироваться с другим потоком (а именно дожидаться, пока он завершит свою работу), вы можете использовать метод join(). Вызов метода join() заставляет вызывающую сторону заблокироваться до завершения целевого потока. Также можно вызвать join() с передачей продолжительности ожидания в миллисекундах. Это очень примитивная форма синхронизации потока. В Java есть более универсальные и мощные механизмы координации активности потоков, включая методы wait() и notify(), а также высокоуровневые API из пакета java.util.concurrent. Эти темы вам придется изучать в основном самостоятельно, но отметим, что язык Java упростил написание многопоточных приложений по сравнению со многими своими предшественниками.

Метод interrupt()


Метод interrupt() упоминался как средство возобновления работы потока, бездействующего из-за вызова sleep(), wait() или долгой операции ввода-вывода. Любой поток, не работающий постоянно (не находящийся в «жестком цикле»), должен периодически входить в одно из этих состояний, поэтому в какой-то точке кода поток должен получить приказ остановиться. При прерывании потока устанавливается его флаг статуса прерывания (interrupt status). Это может произойти в любой момент, независимо от того, бездействует поток или нет. Поток может проверить это состояние методом isInterrupted(). Другая форма метода — isInterrupted(boolean) — получает логическое значение, которое указывает, надо ли сбросить статус прерывания. Таким образом, поток может использовать статус прерывания как флаг и как сигнал.

Во всяком случае, предполагаемая функциональность метода именно такова. Но разработчикам Jаva с самого начала не удавалось добиться его корректной работы во всех возможных ситуациях. В первых виртуальных машинах Java (до версии 1.1) метод interrupt() вообще не работал. И даже в более новых версиях все еще есть проблемы с прерыванием вызовов ввода-вывода. Под «вызовом ввода-вывода» имеется в виду, что приложение блокируется в методе read() или write() на время перемещения байтов из источника (из файла, из сети) или в источник. В таком случае при выполнении interrupt() должно выдаваться исключение InterruptedIOException. Но этот механизм не всегда работает надежно. В свое время решения этой проблемы ожидали от нового фреймворка ввода-вывода java.nio, представленного в Java 1.4. При прерывании потока, связанного с операцией NIO, он «просыпается» и при этом автоматически закрывается поток ввода-вывода (I/O stream), называемый «каналом». (За дополнительной информацией о пакете NIO обращайтесь к главе 11.)

Снова об анимации


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

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

// Из класса Field...
     protected void paintComponent(Graphics g) {
          g.setColor(fieldColor);
          g.fillRect(0,0, getWidth(), getHeight());
          physicist.draw(g);
          for (Tree t : trees) {
               t.draw(g);
          }
          for (Apple a : apples) {
               a.draw(g);
          }
     }

// Из класса Apple...
     public void draw(Graphics g) {
          // Выбрать красный цвет для яблока, а затем нарисовать его!
          g.setColor(Color.RED);
          g.fillOval(x, y, scaledLength, scaledLength);
     }

Все просто. Сначала рисуется фоновое поле, потом физик, затем деревья и, наконец, яблоки. Такой порядок прорисовки гарантирует, что яблоки будут выводиться «поверх» всех остальных элементов. Класс Field переопределяет метод среднего уровня paintComponent(), доступный во всех графических элементах Swing для произвольной прорисовки, но эта тема будет рассматриваться в главе 10.

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

// Из класса Apple...

     public void toss(float angle, float velocity) {
          lastStep = System.currentTimeMillis();
          double radians = angle / 180 * Math.PI;
          velocityX = (float)(velocity * Math.cos(radians) / mass);
          // Начинаем с отрицательного значения velocity,
          // так как направлению вверх соответствуют меньшие значения y
          velocityY = (float)(-velocity * Math.sin(radians) / mass);
     }

     public void step() {
          // Проверяем, что перемещение вообще происходит; переменная
          // lastStep используется в качестве сторожевого значения
          if (lastStep > 0) {
               // Применяем силу гравитации
               long now = System.currentTimeMillis();
               float slice = (now - lastStep) / 1000.0f;
               velocityY = velocityY + (slice * Field.GRAVITY);
               int newX = (int)(centerX + velocityX);
               int newY = (int)(centerY + velocityY);
               setPosition(newX, newY);
          }
     }

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

public static final int STEP = 40; // Продолжительность кадра анимации
                                   // в миллисекундах
// ...

class Animator implements Runnable {
     public void run() {
          // "animating" - это глобальная переменная, которая позволяет
          // остановить анимацию и сэкономить ресурсы при отсутствии
          // активных яблок, которые нужно перемещать
          while (animating) {
               System.out.println("Stepping " + apples.size() + " apples");
               for (Apple a : apples) {
                    a.step();
                    detectCollisions(a);
               }
               Field.this.repaint();
               cullFallenApples();
               try {
                    Thread.sleep((int)(STEP * 1000));
               } catch (InterruptedException ie) {
                    System.err.println("Animation interrupted");
                    animating = false;
               }
          }
     }
}

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

Thread animationThread;

// ...

void startAnimation() {
     animationThread = new Thread(new Animator());
     animationThread.start();
}

При помощи событий пользовательского интерфейса, которые будут рассматриваться в разделе «События», с. 365, мы будем запускать яблоки по команде. Пока первое яблоко просто запускается в самом начале игры. Мы знаем, что скриншот на рис. 9.3 не очень впечатляет, но поверьте: в движении все выглядит просто потрясающе! :)

image


Смерть потоков


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

  • Он явно возвращает управление из своего целевого метода run().
  • Он сталкивается с неперехваченным исключением времени выполнения.
  • Вызывается нехороший и устаревший метод stop().

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

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

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

class Devil extends Thread {
     Devil() {
          setDaemon( true );
          start();
     }
     public void run() {
          // Разные демонические дела
     }
}

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

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

И последнее замечание по поводу корректного уничтожения потоков. Очень многие разработчики в своем первом приложении, использующем компонент Swing, сталкиваются с типичной проблемой: их приложение не завершается, а виртуальная машина Java продолжает «висеть», когда все уже завершено.

Java создает при работе с графикой GUI-поток (поток пользовательского интерфейса) для обработки событий ввода и прорисовки. GUI-поток не является демоном, поэтому он не завершается автоматически при завершении всех остальных потоков приложения и разработчик должен вызвать System.exit() явно. (И если подумать, это вполне логично, так как большинство GUI-приложений управляется событиями и просто ожидает ввода от пользователя, в ином случае они бы просто завершались после завершения их стартового кода.)

Об авторах
Марк Лой (Marc Loy) «подхватил вирус» Java в 1994 году после знакомства с бета-версией браузера HotJava, в которой крутилась анимация алгоритма сортировки. Разрабатывал и проводил обучающие курсы в Sun Microsystems, а впоследствии продолжал обучать намного более широкую аудиторию. Сейчас он занимается консультациями и написанием материалов по техническим и медийным темам. Также он исследует быстро растущий мир встраиваемой электроники и носимых устройств.

Патрик Нимайер (Patrick Niemeyer) начал участвовать в разработке языка Oak («предка» Java) во время работы в Southwestern Bell Technology Resources. В настоящее время является техническим директором Ikayzo, Inc., а также независимым консультантом и автором. Патрик создал BeanShell — популярный язык сценариев для Java. Он был участником нескольких экспертных групп JCP, которые руководили проектированием функциональности языка Java, и внес свой вклад во многие проекты с открытым кодом. В последнее время Патрик занимается разработкой аналитических программ для финансовой отрасли, а также современных мобильных приложений. Живет в Сент-Луисе с семьей и несколькими питомцами.

Дэн Лук (Dan Leuck) — исполнительный директор Ikayzo, Inc., компании интерактивного проектирования и разработки программного обеспечения с филиалами в Токио и Гонолулу (среди клиентов Ikayzo — Sony, Oracle, Nomura, PIMCO и федеральное правительство). Ранее работал вице-президентом по научным исследованиям и разработке в компании ValueCommerce (Токио), крупнейшей азиатской компании онлайн-маркетинга; руководителем по разработке в LastMinute.com (Лондон), крупнейшего европейского сайта B2C; президентом филиала DML в США. Дэн работал во многих консультативных советах и в комитетах компаний Macromedia и Sun Microsystems. Он является активным участником сообщества Java, работает над BeanShell, руководит проектом SDL и входит во многие экспертные группы Java Community Process.

Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Java

Комментарии (4)


  1. LeshaRB
    22.11.2022 20:42
    -4

    Идут обсуждения Loom в интернете., а тут отписывается Thread...


  1. ermadmi78
    23.11.2022 11:35
    +1

    ИМХО - Swing тут явно лишний. Я думаю в 2022 году информация про Swing неактуальна для 99% потенциальных читателей этой книги. Но Swing - это только 1 глава. А в остальном все выглядит весьма достойно.


    1. darkneeees
      25.11.2022 11:18

      Почему же лишний. Да JavaFX крутой, но не настолько лёгкий. Если мне нужна граф. приложение на каком нибудь микро пк, я сначала посмотрю, что должно уметь приложение и может уже сделаю выбор в пользу Swing. И это я ещё молчу про то, что без костылей некоторые элементы FX могут и не работать.


  1. TurboKaban
    25.11.2022 12:35

    Довелось почитать 4-е издание от ЭКСМО. Книга очень не понравилась по содержанию, переводу, форматированию кода (выравнивание по левому краю). Я не исключил версии, что причина во мне, поэтому поискал отзывы в интернете и они в основном совпали с моим. Надеюсь в этой редакции что-нибудь улучшили.