Журнал LinuxFormat - перейти на главную

LXF84:Java

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
м (восстановление кавычек в коде AWB)
 
(не показаны 6 промежуточных версий 4 участников)
Строка 1: Строка 1:
== Сигналы ==
+
{{Цикл/Java}}
''ЧАСТЬ 4: Сигналы традиционно считаются трудными в использовании, но
+
=== Сказка Java ===
при всем этом они незаменимы, когда речь заходит о настоящих приложе-
+
''ЧАСТЬ 1: От кофеварок до промышленных серверов и биллинговых систем - Java можно встретить буквально везде. Начните изучение этой технологии вместе с '''Антоном Черноусовым'''.''
ниях Unix. Разберитесь в них вместе с '''Андреем Боровским'''!''
+
  
Главное отличие сигналов от других средств взаимодействия между процессами заключается в том, что их обработка программой обычно происходит сразу же после поступления сигнала (или не происходит вообще), независимо от того, что программа делает в данный момент. Сигнал прерывает нормальный порядок выполнения инструкций в программе и передает управление специальной функции – обработчику сигнала. Если обработка сигнала не приводит к завершению процесса, то по выходе из функции-обработчика выполнение процесса возобновляется с той точки, в которой оно было прервано. У программ также есть возможность приостановить обработку поступающих сигналов временно, на период выполнения какой-либо важной операции. В традиционной терминологии приостановка получения определенных сигналов называется блокированием. Если для поступившего сигнала было установлено блокирование, сигнал будет передан программе, как только она разблокирует данный тип сигналов. Этим блокирование отличается от игнорирования сигнала, при котором сигналы соответствующего типа никогда не передаются программе. Следует помнить, что не все сигналы могут быть проигнорированы. Например, при получении программой сигнала принудительного завершения SIGKILL система ничего не сообщает программе, а просто прекращает ее работу. Таким образом, преимущество сигналов перед другими средствами межпроцессного взаимодействия заключается в том, что посылать программе сигналы можно в любой момент ее работы, не дожидаясь наступления каких-то особых условий. Источником сигналов может быть как сам операционная система, так и другие пользовательские программы. Если вам показалось, что сигналы похожи на прерывания, то вы совершенно правы. Для реализации сигналов действительно используются программные прерывания. Нужно ли обрабатывать сигналы в вашей программе? Большинство программ не делают этого. В случае программирования для графических оболочек многие функции сигналов берут на себя механизмы сообщений графической оболочки. Тем не менее, есть целый ряд программ (например, демоны и консольные многопоточные приложения), в которых обработка сигналов необходима.
+
Начиная ряд статей о программировании на объектно-ориентированном языке Java, хотел бы выразить свое мнение относительно корпоративного рынка разработки программного обеспечения, на котором в данный момент борются за лидерство два гиганта, две платформы: J2EE и .NET.
  
Большинству сигналов системы присвоена конкретная роль и, хотя у программиста существует возможность использовать сигналы для передачи произвольной информации, не соответствующей их стандартному назначению, делать этого не рекомендуется. Собственно говоря, с помощью сигналов можно передать не так уж и много информации – только номер сигнала (хотя на платформе x86, например, можно было бы организовать и передачу дополнительных параметров). Скудость данных, передаваемых сигналами, не удивительна, если учесть, что по умолчанию большинство сигналов просто завершают работу программы. При этом в некоторых случаях на диске сохраняется образ памяти выгруженной программы (знаменитый core dump). Соответственно и программа-источник сигнала обычно не ждет никакого ответа от программы-приемника. Номерам сигналов соответствуют константы, определенные в файле '''signal.h'''. Имена всех этих констант начинаются с префикса SIG, за которыми следует сокращенное название сигнала. Стандарт POSIX определяет две группы сигналов – «классические» сигналы Unix и сигналы реального времени. В отличие от классических сигналов сигналы реального времени всегда буферизуются, так что программа получит все посланные ей сигналы. В этой статье мы рассмотрим только классические сигналы Unix, каковых в Linux насчитывается 31. Этим сигналам назначены номера с 1 до 31 (номер 0, так называемый null-сигнал, имеет особый смысл). Полный список сигналов можно получить из заголовочного файла signal.h. Мы же рассмотрим несколько наиболее интересных сигналов.
+
В основе платформы J2EE лежит язык программирования Java – приложения, созданные с его помощью, являются кроссплатформенными, сам язык, средства разработки и технология активно поддерживаются многими корпорациями: Sun, Google, IBM, Oracle, BEA. Платформа пользуется большой популярностью и среди компаний, ориентированных на OpenSource, таких как Apache Foundation. В противовес J2EE, корпорация Microsoft предлагает свою технологию .NET, которая является колоссом, построенным на мощи программного обеспечения Microsoft.
*Сигнал SIGHUP (номер 1) изначально был предназначен для того, чтобы информировать программу о потере связи с управляющим терминалом (терминалы часто подключались к системе с помощью модемов, так что название сигнала происходит от hung up – повесить трубку). Кроме того, сигнал SIGHUP посылается приложению в том случае, если процесс-лидер сессии завершил свою работу. Многие программы-демоны, у которых нет лидера сессии, также обрабатывают этот сигнал. В ответ на получение SIGHUP демон обычно перезапускается (или просто повторно читает файл конфигурации). По умолчанию программа, получившая этот сигнал, завершается.
+
*Сигнал SIGINT (номер 2) обычно посылается процессу, если пользователь терминала дал команду прервать процесс (обычно эта команда – сочетание клавиш Ctrl-C) .
+
*Сигнал SIGABRT (номер 6) посылается программе в результате вызова функции abort(3). В результате программа завершается с сохранением на диске образа памяти.
+
*Сигнал SIGKILL (номер 9) завершает работу программы. Программа не может ни обработать, ни игнорировать этот сигнал.
+
*Сигнал SIGSEGV (номер 11) посылается процессу, который пытается обратиться к не принадлежащей ему области памяти. Если обработчик сигнала не установлен, программа завершается с сохранением на диске образа памяти.
+
*Сигнал SIGTERM (номер 15) вызывает «вежливое» завершение программы. Получив этот сигнал, программа может выполнить необходимые перед завершением операции (например, высвободить занятые ресурсы). Получение SIGTERM свидетельствует не об ошибке в программе, а о желании ОС или пользователя завершить ее.
+
*Сигнал SIGCHLD (номер 17) посылается процессу в том случае, если его дочерний процесс завершился или был приостановлен. Родительский процесс также получит этот сигнал, если он установил режим отслеживания сигналов дочернего процесса и дочерний процесс получил какой-либо сигнал. По умолчанию сигнал SIGCHLD игнорируется.
+
*Сигнал SIGCONT (номер 18) возобновляет выполнение процесса, остановленного сигналом SIGSTOP. Сигнал SIGSTOP (номер 19) приостанавливает выполнение процесса. Как и SIGKILL, этот сигнал не возможно перехватить или игнорировать.
+
*Сигнал SIGTSTP (номер 20) приостанавливает процесс по команде пользователя (обычно эта команда – сочетание клавиш Ctrl-Z).
+
*Сигнал SIGIO/SIGPOLL (в Linux обе константы обозначают один сигнал – номер 29) сообщает процессу, что на одном из дескрипторов, открытых асинхронно, появились данные. По умолчанию этот сигнал, как ни странно, завершает работу программы.
+
В стандартной системе Unix определены два сигнала, SIGUSR1 (в Linux – номер 10) и SIGUSR2 (номер 12), предназначенные для передачи произвольной информации, но использование этих сигналов не приветствуется. Одной из причин негативного отношения программистов Unix к пользовательским сигналам является то, что сигналы, вообще говоря, представляют собой ограниченный ресурс, совместное использование которого может вызвать конфликты (например, если программист задействовал эти сигналы в своей программе и при этом использует стороннюю библиотеку, в которой эти сигналы также задействованы).
+
  
Если вы не знали, то вам, возможно, будет интересно узнать, что обработка сигналов является частью стандарта языка Си и, как таковая, поддерживается даже на платформе Microsoft Windows. Однако, стандартный интерфейс сигналов Си, основанный на функции signal(), довольно неуклюж (недостатки интерфейса сигналов Си подробно описаны в книге [2]), так что мы воспользуемся более совершенным вариантом интерфейса, основанным на функции sigaction(2). Для демонстрации работы обработки сигналов мы напишем небольшую программу (файл '''sigdemo.c''' на компакт-диске).
+
Сегодня еще нельзя сказать, какая технология станет доминирующей, и, возможно, от вас зависит скорость распространения современной кроссплатформенной технологии создания приложений J2EE и языка программирования Java.
  
<code>
+
Предложенные вашему вниманию уроки позволят овладеть основными понятиями Java и создавать различные приложения с его помощью. Рассмотрев основы языка, мы сможем изучить типовые решения (паттерны) в программировании и приступить к изучению J2EE.
#include <stdio.h>
+
В первом уроке мы создадим свое первое приложение и разберемся с основными понятиями: инкапсуляция, наследование и полиморфизм. Для выполнения этого урока нам понадобится пакет Java SDK 1.5, текстовый редактор и немного терпения.
#include <stdlib.h>
+
=== Героический объект ===
  #include <signal.h>
+
Приступая к этой серии материалов, я вспомнил, что, как правило, изучение языков программирования начинается с написания приложения "Hello World!", но такой прием настолько изъезжен, что я намереваюсь предложить пример поинтереснее: мы напишем приложение, рассказывающее сказку. Основа приложения – два класса. Каждый класс содержит члены двух видов: атрибуты и методы. Атрибуты – это данные, принадлежащие самому классу. Совокупное значение атрибутов определяет состояние объекта или класса. Метод – это последовательность операторов, изменяющих значения атрибутов и, как следствие, состояние класса или объекта.
 +
 
 +
В каждой интересной сказке должен быть свой герой. Предлагаю взглянуть на класс, который реализует нашего героя. Итак, по порядку. Следующий код необходимо сохранить в файл '''FirstHero.java.'''
 +
<source lang = "java">
 +
public class FirstHero {
 +
  private String quest;
 +
  public FirstHero(String Quest) {
 +
    this.quest = Quest;
 +
  }
 +
  public void setQuest(String Quest){
 +
    this.quest = Quest;
 +
  };
 +
  public String getQuest(){
 +
    return quest;
 +
  };
 +
}
 +
</source>
 +
Расположение одного класса в одном файле считается хорошим тоном среди программистов на Java – так что приучайтесь к нему сразу. Имя файла должно совпадать с названием класса, расширение у файла – '''.java.'''
 +
 
 +
Первый созданный нами класс содержит один защищенный атрибут (private – модификатор, зарезервированное слово, которое обеспечивает возможность обращения к члену класса только внутри класса) и три открытых метода (public – модификатор, зарезервированное слово, которое обеспечивает возможность обращения к члену класса из любого другого класса.). Хорошо спроектированный класс не имеет открытых атрибутов, что позволяет быть уверенным в правильной работе с ними. Доступ к защищенным атрибутам осуществляется через открытые методы. Такая организация класса позволяет разработчику через методы реализовать различные проверки и исключить неправильное поведение объекта. Класс представляет собой объединение атрибутов и методов в единое целое – подобное объединение называется инкапсуляцией.
 +
 
 +
Вы можете заметить, что первый метод (третья строка представленного ранее листинга) имеет то же название, что и класс – FirstHero. Такой метод называется конструктором. Конструктор – это метод, который используется при создании экземпляра класса для задания внутреннего состояния объекта, например:
 +
<source lang = "java">
 +
FirstHero ourNewHero = new FirstHero("Спасти царевну");
 +
</source>
 +
Единственный атрибут нашего класса quest имеет тип строка (String). Для установки значения атрибута quest используется метод
 +
setQuest. Метод setQuest является public и не возвращает никакого значения. Для обозначения методов, не возвращающих значение, используется зарезервированное слово void. Для вызова метода setQuest необходим обязательный параметр: Quest типа String. Для получения значения атрибута quest используется метод getQuest, который вызывается без параметров, является открытым и возвращает значение типа String.
 +
 
 +
Вызов описанных выше методов для объекта ourNewHero осуществляется следующим образом:
 +
<source lang = "java">
 +
ourNewHero.getQuest();
 +
ourNewHero.setQuest("какая-то строка");
 +
</source>
 +
Парные методы, различающиеся приставками set и get, принято создавать для работы с закрытыми атрибутами. Это обязательное
 +
условие для реализации объекта в виде JavaBean. Что такое JavaBean, мы обсудим позже – сейчас запомните, что такое наименование методов позволяет избавиться от ряда проблем в больших системах.
 +
 
 +
Приступим к написанию самой сказки. Возьмите следующий код и поместите его в файл '''Story.java'''.
 +
 
 +
<source lang = "java">
 +
public class Story {
 +
  public static void main(String[] args) {
 +
    FirstHero ourNewHero = new FirstHero("Спасти царевну ");
 +
    System.out.println("Наш герой хочет отправиться в путь и " + ourNewHero.getQuest());
 +
  }
 +
}
 +
</source>
 +
 
 +
Это класс Story, он имеет всего один метод main, который является статичным (static-методы и переменные принято называть переменными класса и методами класса, т.к. они общие для всех объектов) и используется для работы с классом в целом. Метод main – это стартовая точка для запуска программы, с которой начинается интерпретация кода.
 +
 
 +
В ходе интерпретации метода main создается новый объект ourNewHero, а также вызывается метод println объекта out, который обеспечивает вывод в стандартный выходной поток строковойпеременной.
 +
 
 +
Откомпилируем код и увидим, что у нас получилось. Для компиляции переместитесь в каталог, где вы разместили свои файлы и выполните следующую команду:
 +
<source lang = "java">
 +
# javac *.java
 +
</source>
 +
При желании вы можете откомпилировать классы по отдельности, следующим образом:
 +
<source lang = "java">
 +
# javac Story.java
 +
# javac FirstHero.java
 +
</source>
 +
В итоге вы получите два файла '''Story.class''' и '''FirstHero.class'''. Теперь посмотрим на результат:
 +
<source lang = "java">
 +
# java Story
 +
Наш герой хочет отправиться в квест и Спасти царевну
 +
</source>
 +
 
 +
Первая программа на Java готова и работает! Вы создали два класса и научились выводить информацию в консоль. Но мы забыли о
 +
комментариях...
 +
 
 +
Комментарии бывают нескольких типов:
 +
*Строчные начинаются с символа // и длятся до окончания строки.
 +
*Многострочные комментарии заключаются между /* и */.
 +
*Комментарии JavaDoc – это многострочные комментарии, заключенные между /** и */, например:
 +
 
 +
<source lang = "java">
 +
/**
 +
  * Класс является прототипом объекта "герой"
 +
* @author Chernousov Anton
 +
* @version 0.1
 +
*/
 +
public class FirstHero {
 +
 +
/**
 +
  * Метод предназначен для установки внутренней переменной
 +
quest
 +
  * в значение Quest.
 +
  * У этого метода один параметр
 +
  * @param Quest это переменная означает задание для Героя
 +
  */
 +
  public void setQuest(String Quest)
 +
 +
</source>
 +
 
 +
Комментарии JavaDoc предназначены для формирования документации к проекту из исходных кодов. Если добавить в файл '''FirstHero.java''' только что приведенные строки, то с помощью команды '''javadoc -author -version *.java''' можно сформировать документацию к нашему проекту.
 +
 
 +
JavaDoc позволяет создавать подробную документацию, которая просто необходима для любого достаточно крупного проекта.
 +
=== Наследование, интерфейсы и их реализация ===
 +
Наша сказка написана, но что делать, если проект большой и над ним работает много людей? Как реализовать одновременное программирование разных классов так, чтобы результаты, полученные отдельными разработчиками, можно было без труда соединить в единое целое? Для решения этой задачи, а также задачи множественного наследования в Java существуют интерфейсы – interface. Но обо всем по порядку.
 +
 
 +
Наследование – это одна из парадигм программирования, которая связана с инкапсуляцией. Суть наследования заключается в следующем: если есть класс, инкапсулирующий в себе некоторые атрибуты и методы, то класс, наследующий его, автоматически наследует все его атрибуты и методы.
 +
<source lang = "java">
 +
public class SecondHero extends FirstHero{
 +
  public SecondHero(String Quest) {
 +
    super(Quest + " Несмеяну");
 +
  }
 +
}
 +
</source>
 +
Расположенный выше код объявляет класс SecondHero, который с помощью зарезервированного слова extends наследует (расширяет) класс FirstHero. В Java класс может наследовать (расширять)только один класс. При наследовании класса FirstHero, содержащего
 +
конструктор, обязательно требуется переопределить его (например, как это делается в методе SecondHero). При выполнении оператора super(Quest) будет вызван аналогичный метод из суперкласса (класс-родителя). Строкой super(Quest + "Несмеяну") мы переопределяем поведение конструктора. Естественно, класс SecondHero может содержать новые атрибуты и методы.
 +
 
 +
Для проверки механизма наследования сохраните приведенный текст класса SecondHero в файл '''SecondHero.java''' и в файле '''Story.java''' измените имя класса FirstHero на SecondHero. Откомпилируйте и запустите программу, в результате вы должны увидеть следующее:
 +
<source lang = "java">
 +
# java Story
 +
Наш герой хочет отправиться в путь и Спасти царевну Несмеяну
 +
</source>
 +
При разработке программы, перед тем, как приступить к программированию, сначала проводится процесс проектирования (например, через UML-нотации). В результате проектирования появляется документация к проекту, а также спецификации классов. Интерфейс – это явно указанная спецификация набора методов, которые должны быть представлены в классе, реализующий эту спецификацию. В серьезном проекте на момент начала программирования перед программистом оказываются интерфейсы, которые он должен реализовать в классах.
 +
 
 +
Интерфейс в Java – это специальный класс, в котором отсутствует реализация! Давайте напишем интерфейс для нашего класса FirstHero. Следует скопировать следующий далее код и поместить его в файл '''Hero.java''' – именно этот интерфейс мы будет использован далее. Как вы видите, в отличие от других классов, интерфейс объявляется с помощью зарезервированного слова interface. В нашем случае интерфейс представляет собой следующий набор методов:
 +
<source lang = "java">
 +
public interface Hero {
 +
  public void setQuest(String Quest);
 +
  public String getQuest();
 +
}
 +
</source>
 +
Чтобы класс FirstHero реализовывал интерфейс Hero, необходимо изменить первую строчку в файле '''FirstHero.java''' на public class FirstHero implements Hero {. Интерфейс может наследовать (расширять) множество других интерфейсов. Наример:
 +
<source lang = "java">
 +
public interface Lord {
 +
  public String sendToQuest();
 +
  public void setBodyguard(Hero hisHero);
 +
  public Hero getBodyguard();
 +
}
 
   
 
   
void term_handler(int i) {
+
public interface King extends Hero, Lord{
  printf ("Terminating\n");
+
  public void setWife (String PrincesName);
  exit(EXIT_SUCCESS);
+
  public String getWife();
}
+
}
+
</source>
int main(int argc, char ** argv) {
+
Класс может одновременно наследовать класс и реализовывать один или несколько интерфейсов. Например, как в классе LandLord,
  struct sigaction sa;
+
который наследует класс FirstHero и реализует интерфейс Lord:
  sigset_t newset;
+
<source lang = "java">
  sigemptyset(&newset);
+
public class LandLord extends FirstHero implements Lord{
  sigaddset(&newset, SIGHUP);
+
  private Hero bodyguard;
  sigprocmask(SIG_BLOCK, &newset, 0);  
+
  public LandLord(String Quest, Hero hisHero) {
  sa.sa_handler = term_handler;  
+
    super(Quest);
  sigaction(SIGTERM, &sa, 0);
+
    this.bodyguard = hisHero;
  printf("My pid is %i\n", getpid());  
+
  }
  printf("Waiting...\n");  
+
  public String sendToQuest() {
  while(1) sleep(1);
+
    bodyguard.setQuest(this.getQuest());
  return EXIT_FAILURE;
+
    return null;
}
+
  }
</code>
+
  public void setBodyguard(Hero hisHero) {
 +
    this.bodyguard = hisHero;
 +
  }
 +
  public Hero getBodyguard() {
 +
    return bodyguard;
 +
  }
 +
}
 +
</source>
 +
Чтобы окончательно закрыть тему наследования, необходимо поговорить про абстрактные классы – те, что занимают промежуточное
 +
место между интерфейсами и реальными классами. Объявление абстрактного класса или метода выполняется с помощью специального
 +
слова abstract:
 +
<source lang = "java">
 +
abstract public class Knight
 +
</source>
 +
Абстрактный класс представляет собой неполную реализацию всех объявленных методов, и его можно наследовать, как и любой из классов. Обязательным условием при наследовании является полная реализация абстрактных методов.
 +
 
 +
=== Полиморфизм ===
 +
Полиморфизм – это концепция, реализованная в Java, которая позволяет одному и тому же объекту выступать в разных формах. Например, экземпляр класс LandLord можно использовать как объект класса FirstHero. Это стало возможным благодаря тому, что класс LandLord наследует класса FirstHero.
  
Наша программа делает две вещи: обрабатывает сигнал SIGTERM (при получении этого сигнала программа выводит диагностическое сообщение и завершает свою работу) и блокирует сигнал SIGHUP, так что этот сигнал не может завершить ее работу. В тексте программы мы первым делом определяем функциюобработчик сигнала SIGTERM term_handler(). Функции-обработчики сигналов – это обычные функции Си, они имеют доступ ко всем глобально видимым переменным и функциям. Однако, поскольку мы не знаем, в какой момент выполнения программы будет вызвана функция-обработчик, мы должны проявлять особую осторожность при обращении к глобальным структурам данных из этой функции. Единственным параметром нашего варианта функции-обработчика сигнала (в Unix-системах существует и другой вариант) является переменная типа int, в которой передается номер сигнала, вызвавшего обработчик. Нам этот номер не нужен, поскольку мы знаем, что только один сигнал SIGTERM, может вызвать нашу функцию, однако, в принципе, ничто не мешает нам использовать одну функцию для обработки нескольких разных сигналов, и тогда параметр функции-обработчика будет иметь для нас смысл. Функция-обработчик не возвращает никакого значения, что вполне логично, так как она вызывается не нашей программой, а неким системным компонентом. Особый интерес представляет завершение программы из обработчика сигнала. Назначение обработчика сигналу SIGTERM означает, что «умолчательное» действие сигнала – завершение программы не будет выполняться автоматически, и нам необходимо (если, конечно, мы хотим, чтобы этот сигнал завершал программу) позаботиться обо всем явным образом. Если вы закомментируете вызов exit() в нашем примере, то увидите, что программа не будет завершаться по получении сигнала SIGTERM. В принципе, вы можете придать сигналу SIGTERM совершенно иной смысл, например, оповещать программу о наступлении времени вашей любимой телепередачи (или о выходе нового номера журнала Linux Format), однако назначать стандартным сигналам нестандартные действия категорически не рекомендуется. Обработчик SIGTERM предназначен для того, чтобы, по требованию системы или пользователя, программа могла быстро и элегантно закончить текущую задачу и завершить свое выполнение. Именно этим обработчик и должен заниматься. Перейдем теперь к тексту главной функции программы. Установка и удаление обработчиков сигналов осуществляются функцией sigaction(2). Первым параметром этой функции является номер сигнала, а в качестве второго и третьего параметров следует передать указатели на структуру sigaction. Эта структура содержит данные об операции, выполняемой над обработчиком сигнала. Второй параметр sigaction() служит для передачи новых значений для обработки сигнала, а третий – возвращает ранее установленные значения. В таблице 1 приводится краткое описание полей структуры sigaction. какого-либо сигнала. Иначе говоря, любой обрабатываемый сигнал прерывает выполнение sleep(). Впрочем, в нашем примере с бесконечным циклом это не помогло бы программе завершиться. Сигнал SIGTERM приведет к тому, что программа выдаст диагностическое сообщение и завершит работу, а сигналы SIGINT и SIGABRT – к тому, что программа завершится без всякого сообщения. Скомпилируйте и запустите программу в окне терминала. В другом окне скомандуйте
+
=== Перегруженные методы ===
<code>
+
Иногда при создании сложных (и не очень) систем есть необходимость реализовать два различных метода, названых одинаково – это возможно с помощью перегруженных (overloading) методов. Выбор реализации зависит от типа объектов, переданных в качестве параметров.
kill <PID>
+
<source lang = "java">
</code>
+
public class GreatKing extends LandLord implements King{
где PID – идентификатор процесса программы. Вы увидите, что перед тем как завершиться программа выдает диагностическое сообщение, тогда как при завершении с помощью Ctrl-C никакого сообщения не выводится. Рассмотрим теперь блокировку сигналов. Поскольку игнорирование сигнала устанавливается функцией sigaction(), можно было бы ожидать, что и блокировка устанавливается этой же функцией, но это не так. Поскольку нам, как правило, приходится блокировать несколько сигналов сразу, для блокировки существует специальная функция sigprocmask(2), которая оперирует наборами сигналов (signal sets). Разделение интерфейса между несколькими функциями вызвано еще и требованиями многопоточности. Параметры, устанавливаемые sigaction(), действительны для всей программы в целом, тогда как блокировку сигналов потоки осуществляют независимо друг от друга. Наборы сигналов хранятся в переменных специального типа – sigset_t, а операции над ними осуществляются с помощью специальных функций. Функция sigemptyset() инициализирует набор сигналов пустыми значениями, а функция sigfillset() устанавливает все возможные значения в наборе. Используемая нами функция sigaddset() добавляет значение сигнала в набор, а функция sigdelset() удаляет сигнал из набора. После того как набор сигналов сформирован, мы передаем его функции sigprocmask(), которая выполняет блокировку и разблокировку сигналов. Первым параметром этой функции должна быть одна из констант, определяющих операцию над заданными сигналами. Константа SIG_BLOCK указывает, что сигналы из нового набора должны быть добавлены к списку уже заблокированных сигналов. Константа SIG_SETMASK указывает, что новый набор блокируемых сигналов должен заменить уже существующий (при этом заблокированные ранее сигналы будут разблокированы, если они не заблокированы в новом наборе), а константа SIG_UNBLOCK указывает на необходимость разблокировать сигналы, переданные в наборе. В нашей программе мы блокируем сигнал SIGHUP и вы можете видеть, что программа не обрабатывает этот сигнал. Послать нашей программе сигнал SIGHUP вы можете с помощью консольной команды
+
  private String wife;
<code>
+
  private boolean theftedWife;
kill –s 1 <PID>
+
</code>
+
где PID – идентификатор процесса. Сигналы прерывают нормальный порядок выполнения программы и могут завершить работу программы, не способной завершиться иным образом. Но иногда бывает так, что программе просто нечего делать до тех пор, пока она не получит какой-либо сигнал. Иначе говоря, программу нужно заставить ждать появления сигнала, по возможности не нагружая процессор. Такая ситуация может возникнуть, например, в многопоточном приложении, когда нужно синхронизировать завершение нескольких потоков. Ожидание сигнала можно реализовать с помощью цикла, проверяющего значение флажка, который может сбросить обработчик сигнала. В некоторых случаях (таких как рассмотренный выше пример) можно реализовать ожидание и с помощью бесконечного цикла. Очевидно, однако, что эти методы не эффективны и не элегантны. В POSIX-системах существует специальная функция sigwait(3), которая «усыпляет» процесс до тех пор, пока процессу не будет передан один из заданного набора сигналов. Модифицируем нашу программу так, чтобы вместо бесконечного цикла она входила в цикл ожидания сигнала SIGHUP (файл swdemo.c на компакт-диске):
+
  
Поля структуры sigaction
+
  public GreatKing(String Quest, Hero hisHero) {
Поле sa_handler sa_mask sa_flags Значение Указатель на функцию обработчик сигнала или константа Маска сигналов, блокируемых на время вызова обработчика Дополнительные флаги
+
    super(Quest, hisHero);
 +
  }
  
Поле sa_handler должно содержать либо адрес функции-обработчика, либо специальную константу, указывающую, что нужно делать с сигналом. Константа SIG_IGN указывает, что сигнал следует игнорировать, а константа SIG_DFL – что нужно восстановить обработку сигнала, заданную системой по умолчанию. Поле sa_mask позволяет заблокировать некоторое множество сигналов на время выполнения обработчика данного сигнала. Делается это для того, чтобы обработка других сигналов не могла прервать обработку данного (это может быть необходимо, особенно, если один обработчик обрабатывает несколько разных сигналов). Параметр sa_flags позволяет задать ряд флагов для выполнения более тонкой настройки обработчика сигналов. Например, флаг SA_RESETHAND указывает, что после завершения обработки сигнала заданным обработчиком должен быть восстановлен обработчик, заданный по умолчанию, так что все последующие сигналы будут обрабатываться «умолчательным» обработчиком. В результате вызова функции sigaction() мы устанавливаем обработчик сигнала SIGTERM. Затем наша программа распечатывает значение PID (это значение понадобится нам для вызова команды kill) и входит в бесконечный цикл, из которого она может быть выведена одним из сигналов. Следует отметить, что функция sleep() прерывается (возобновляет выполнение программы раньше срока) если возвращает управление обработчик
+
  public void setWife(String PrincesName) {
<code>
+
    this.wife = PrincesName;
sigprocmask(SIG_BLOCK, &newset, 0);
+
    this.theftedWife = false;
while(!sigwait(&newset, &sig))
+
  }
  printf("SIGHUP recieved\n");
+
</code>
+
Первым параметром функции sigwait() является указатель на набор сигналов, получения которых будет ждать функция. Во втором параметре sigwait() вернет номер того сигнала, который возобновил работу программы (эта информация может быть полезна, если установлено несколько ожидаемых сигналов). Перед тем как вызывать sigwait(), набор ожидаемых сигналов следует заблокировать с помощью функции sigprocmask(), иначе, при получении сигнала, вместо выхода из sigwait() будет вызван соответствующий обработчик. Сигнал, который возобновил работу программы после вызова sigwait(), уже не может быть перехвачен назначенным ему обработчиком. В нашем примере мы «усыпляем» программу до тех пор, пока она не получит сигнал SIGHUP, распечатываем соответствующее сообщение и снова усыпляем (функция sigwait() возвращает 0, если ее вызов прошел успешно). В то время, когда программа приостановлена в ожидании некоторых сигналов, обработчики всех не заблокированных и не игнорируемых сигналов выполняются обычным образом. Функцию sigwait() можно использовать и для исследования сигналов. На компакт-диске вы найдете программку siglog.c, которая распечатывает информацию о каждом поступившем сигнале (естественно, исследуются только те сигналы, которые могут быть заблокированы). Рассмотрим здесь фрагмент этой программы:
+
<code>
+
sigset_t sset;
+
int sig;
+
...
+
sigfillset(&sset);
+
sigdelset(&sset, SIGTERM);
+
sigprocmask(SIG_SETMASK, &sset, 0);
+
while(!sigwait(&sset, &sig))
+
  printf("Signal %i - %s\n", sig, sys_siglist[sig]);
+
</code>
+
С помощью вызовов sigfillset() и sigdelset() мы создаем набор из всех сигналов, за исключением сигнала SIGTERM (этот сигнал понадобится нам для того, чтобы мы могли завершить работу программы). Далее мы блокируем сигналы набора sset и вызываем для них функцию sigwait(). Функция вернет управление при получении любого сигнала, кроме SIGTERM (для которого назначен отдельный обработчик). Получив новый сигнал, мы распечатываем информацию о нем. Массив char * sys_siglist[] определен в стандартной библиотеке glibc. Этот массив содержит наименования сигналов на «человеческом» языке (эти наименования можно использовать при выводе диагностических и отладочных сообщений). Наименования расположены так, чтобы их индексы в массиве соответствовали номерам сигналов. Те же данные возвращает и функция strsignal(), единственным параметром которой является номер сигнала. На протяжении всей этой статьи мы занимались обработкой сигналов, но не их генерацией. Поскольку основным источником сигналов является операционная система, нам и в «реальной жизни» чаще приходится заниматься именно обработкой. Однако, в заключение статьи следует рассмотреть и функции генерации сигналов. Для генерации сигналов в Unix предусмотрены две функции – kill(2) и raise(3). Первая функция предназначена для передачи сигналов любым процессам, к которым владелец данного процесса имеет доступ, а с помощью второй функции процесс может передать сигнал самому себе. Как это обычно принято в мире Unix, семантика вызова функции kill() совпадает с семантикой одноименной команды ОС. У функции kill() два аргумента – PID процесса-приемника и номер передаваемого сигнала. С помощью функции kill() как и с помощью одноименной команды можно передавать сообщения не только конкретному процессу, но и группе процессов. Таблица 2 демонстрирует поведение функции kill() в зависимости от значения PID:
+
  
Поля структуры sigaction
+
  public String getWife() {
Поле PID > 1 PID == 0 PID < 0 Значение Сигнал посылается процессу с соответствующим PID. Сигнал посылается всем процессам из той же группы, что и процесс-источник. Сигнал посылается всем процессам, чей идентификатор группы равен абсолютному значению PID. PID == 1 Сигнал посылается всем процессам системы.
+
    if (theftedWife){return null;}
 +
    else{ return wife;}
 +
  }
  
Вызов
+
  public String getWife(boolean flag) {
<code>
+
    if (flag){ this.theftedWife = true;}
raise(sig);
+
    return wife;
</code>
+
  }
эквивалентен вызову
+
}
<code>
+
</source>
kill(getpid(), sig);
+
В представленном классе GreatKing существуют две реализации перезагруженного метода getWife, которые можно вызвать следующим образом:
</code>
+
<source lang = "java">
Так же как и для других примитивов IPC, для сигналов действует система прав доступа, основанная на правах доступа владельцев процессов. Процесс-приемник получит сигнал только в том случае, если у процесса-источника есть соответствующие права. С помощью функции kill() можно проверить, существует ли в системе процесс с заданным PID, не посылая процессу никаких сигналов. Для этого предназначен псевдо-сигнал с номером 0. Если соответствующего процесса не существует, функция kill() вернет значение 1, соответствующее об ошибке. В любом случае, сигнал не будет отправлен. Читателей, полюбивших обработку сигналов, я могу обрадовать тем, что мы рассмотрели далеко не все функции, связанные с сигналами. При изучении документации вас ждет еще много полезного и приятного, мы же закончим на этом наше знакомство с сигналами. Мы можем столкнуться с необходимостью обработки сигналов при программировании, например, процессов-демонов. Ими мы и займемся в следующей статье. LXF
+
GreatKing ourKing = new GreatKing("Править мудро", (Hero) new FirstHero("Охранять господина"));
 +
String hisWife = ourKing.getWife();
 +
String theftedHisWife = ourKing.getWife(true);
 +
</source>
  
Литература:
+
=== Вместо заключения ===
#D. P. Bovet, M. Cesati, '''Understanding the Linux Kernel, 3rd Edition, O’Reilly''', 2005
+
Ну вот, мы создали всех героев. Разместите интерфейсы Lord, King, а также классы LandLord и GreatKing в соответствующих файлах.
#W. R. Stevens, S. A. Rago, '''Advanced Programming in the UNIX® Environment: Second Edition''', Addison Wesley Professional, 2005
+
Немного измените класс Story, чтобы завершить сказку:
 +
<source lang = "java">
 +
public class Story {
 +
  public static void main(String[] args) {
 +
    // создадим наших героев
 +
    Hero ourNewHero = (Hero) new FirstHero("Спасити царевну");
 +
    GreatKing ourKing = new GreatKing("Править мудро", (Hero) new FirstHero("Охранять господина"));
 +
    LandLord ourNewLandLord = new LandLord("Похитить царевну", (Hero) new FirstHero("Охранять господина"));
 +
    // Женим нашего короля
 +
    ourKing.setWife("Несмеяна");
 +
    // И начнем повествование
 +
    System.out.println("Жил был мудрый король, и должен был он " + ourKing.getQuest());
 +
    System.out.println("Была у него жена, и звали ее " + ourKing.getWife());
 +
    System.out.println("Был в королевстве коварный Визирь и хотел он " + ourNewLandLord.getQuest());
 +
    System.out.println("И сделал это");
 +
    String wife = ourKing.getWife(true);
 +
    //
 +
    System.out.println("А наш герой жаждал " + ourNewHero.getQuest() + " ..и добился этого");
 +
    ourKing.setWife(wife);
 +
    System.out.println("Король был счастлив.");
 +
    System.out.println("Ну а что случилось дальше вы можете придумать сами...");
 +
  }
 +
}
 +
</source>
 +
Откомпилируйте и насладитесь результатом выполнения урока.
  
'''Через месяц''' Мы породим монстра... то есть демона.
+
В [[LXF85:Java|следующей статье]] мы поговорим о простых числах и других типах, вычислениях, циклах, массивах и списках.

Текущая версия на 16:55, 27 апреля 2008

Содержание

[править] Сказка Java

ЧАСТЬ 1: От кофеварок до промышленных серверов и биллинговых систем - Java можно встретить буквально везде. Начните изучение этой технологии вместе с Антоном Черноусовым.

Начиная ряд статей о программировании на объектно-ориентированном языке Java, хотел бы выразить свое мнение относительно корпоративного рынка разработки программного обеспечения, на котором в данный момент борются за лидерство два гиганта, две платформы: J2EE и .NET.

В основе платформы J2EE лежит язык программирования Java – приложения, созданные с его помощью, являются кроссплатформенными, сам язык, средства разработки и технология активно поддерживаются многими корпорациями: Sun, Google, IBM, Oracle, BEA. Платформа пользуется большой популярностью и среди компаний, ориентированных на OpenSource, таких как Apache Foundation. В противовес J2EE, корпорация Microsoft предлагает свою технологию .NET, которая является колоссом, построенным на мощи программного обеспечения Microsoft.

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

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

[править] Героический объект

Приступая к этой серии материалов, я вспомнил, что, как правило, изучение языков программирования начинается с написания приложения "Hello World!", но такой прием настолько изъезжен, что я намереваюсь предложить пример поинтереснее: мы напишем приложение, рассказывающее сказку. Основа приложения – два класса. Каждый класс содержит члены двух видов: атрибуты и методы. Атрибуты – это данные, принадлежащие самому классу. Совокупное значение атрибутов определяет состояние объекта или класса. Метод – это последовательность операторов, изменяющих значения атрибутов и, как следствие, состояние класса или объекта.

В каждой интересной сказке должен быть свой герой. Предлагаю взглянуть на класс, который реализует нашего героя. Итак, по порядку. Следующий код необходимо сохранить в файл FirstHero.java.

public class FirstHero {
  private String quest;
  public FirstHero(String Quest) {
    this.quest = Quest;
  }
  public void setQuest(String Quest){
    this.quest = Quest;
  };
  public String getQuest(){
    return quest;
  };
}

Расположение одного класса в одном файле считается хорошим тоном среди программистов на Java – так что приучайтесь к нему сразу. Имя файла должно совпадать с названием класса, расширение у файла – .java.

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

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

FirstHero ourNewHero = new FirstHero("Спасти царевну");

Единственный атрибут нашего класса quest имеет тип строка (String). Для установки значения атрибута quest используется метод setQuest. Метод setQuest является public и не возвращает никакого значения. Для обозначения методов, не возвращающих значение, используется зарезервированное слово void. Для вызова метода setQuest необходим обязательный параметр: Quest типа String. Для получения значения атрибута quest используется метод getQuest, который вызывается без параметров, является открытым и возвращает значение типа String.

Вызов описанных выше методов для объекта ourNewHero осуществляется следующим образом:

ourNewHero.getQuest();
ourNewHero.setQuest("какая-то строка");

Парные методы, различающиеся приставками set и get, принято создавать для работы с закрытыми атрибутами. Это обязательное условие для реализации объекта в виде JavaBean. Что такое JavaBean, мы обсудим позже – сейчас запомните, что такое наименование методов позволяет избавиться от ряда проблем в больших системах.

Приступим к написанию самой сказки. Возьмите следующий код и поместите его в файл Story.java.

public class Story {
  public static void main(String[] args) {
    FirstHero ourNewHero = new FirstHero("Спасти царевну ");
    System.out.println("Наш герой хочет отправиться в путь и " + ourNewHero.getQuest());
  }
}

Это класс Story, он имеет всего один метод main, который является статичным (static-методы и переменные принято называть переменными класса и методами класса, т.к. они общие для всех объектов) и используется для работы с классом в целом. Метод main – это стартовая точка для запуска программы, с которой начинается интерпретация кода.

В ходе интерпретации метода main создается новый объект ourNewHero, а также вызывается метод println объекта out, который обеспечивает вывод в стандартный выходной поток строковойпеременной.

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

# javac *.java

При желании вы можете откомпилировать классы по отдельности, следующим образом:

# javac Story.java
# javac FirstHero.java

В итоге вы получите два файла Story.class и FirstHero.class. Теперь посмотрим на результат:

# java Story
Наш герой хочет отправиться в квест и Спасти царевну

Первая программа на Java готова и работает! Вы создали два класса и научились выводить информацию в консоль. Но мы забыли о комментариях...

Комментарии бывают нескольких типов:

  • Строчные начинаются с символа // и длятся до окончания строки.
  • Многострочные комментарии заключаются между /* и */.
  • Комментарии JavaDoc – это многострочные комментарии, заключенные между /** и */, например:
/**
 * Класс является прототипом объекта "герой"
 * @author Chernousov Anton
 * @version 0.1
 */
public class FirstHero {/**
   * Метод предназначен для установки внутренней переменной
quest
   * в значение Quest.
   * У этого метода один параметр
   * @param Quest это переменная означает задание для Героя
   */
  public void setQuest(String Quest)

Комментарии JavaDoc предназначены для формирования документации к проекту из исходных кодов. Если добавить в файл FirstHero.java только что приведенные строки, то с помощью команды javadoc -author -version *.java можно сформировать документацию к нашему проекту.

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

[править] Наследование, интерфейсы и их реализация

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

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

public class SecondHero extends FirstHero{
  public SecondHero(String Quest) {
    super(Quest + " Несмеяну");
  }
}

Расположенный выше код объявляет класс SecondHero, который с помощью зарезервированного слова extends наследует (расширяет) класс FirstHero. В Java класс может наследовать (расширять)только один класс. При наследовании класса FirstHero, содержащего конструктор, обязательно требуется переопределить его (например, как это делается в методе SecondHero). При выполнении оператора super(Quest) будет вызван аналогичный метод из суперкласса (класс-родителя). Строкой super(Quest + "Несмеяну") мы переопределяем поведение конструктора. Естественно, класс SecondHero может содержать новые атрибуты и методы.

Для проверки механизма наследования сохраните приведенный текст класса SecondHero в файл SecondHero.java и в файле Story.java измените имя класса FirstHero на SecondHero. Откомпилируйте и запустите программу, в результате вы должны увидеть следующее:

# java Story
Наш герой хочет отправиться в путь и Спасти царевну Несмеяну

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

Интерфейс в Java – это специальный класс, в котором отсутствует реализация! Давайте напишем интерфейс для нашего класса FirstHero. Следует скопировать следующий далее код и поместить его в файл Hero.java – именно этот интерфейс мы будет использован далее. Как вы видите, в отличие от других классов, интерфейс объявляется с помощью зарезервированного слова interface. В нашем случае интерфейс представляет собой следующий набор методов:

public interface Hero {
  public void setQuest(String Quest);
  public String getQuest();
}

Чтобы класс FirstHero реализовывал интерфейс Hero, необходимо изменить первую строчку в файле FirstHero.java на public class FirstHero implements Hero {. Интерфейс может наследовать (расширять) множество других интерфейсов. Наример:

public interface Lord {
  public String sendToQuest();
  public void setBodyguard(Hero hisHero);
  public Hero getBodyguard();
}
 
public interface King extends Hero, Lord{
  public void setWife (String PrincesName);
  public String getWife();
}

Класс может одновременно наследовать класс и реализовывать один или несколько интерфейсов. Например, как в классе LandLord, который наследует класс FirstHero и реализует интерфейс Lord:

public class LandLord extends FirstHero implements Lord{
  private Hero bodyguard;
  public LandLord(String Quest, Hero hisHero) {
    super(Quest);
    this.bodyguard = hisHero;
  }
  public String sendToQuest() {
    bodyguard.setQuest(this.getQuest());
    return null;
  }
  public void setBodyguard(Hero hisHero) {
    this.bodyguard = hisHero;
  }
  public Hero getBodyguard() {
    return bodyguard;
  }
}

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

abstract public class Knight

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

[править] Полиморфизм

Полиморфизм – это концепция, реализованная в Java, которая позволяет одному и тому же объекту выступать в разных формах. Например, экземпляр класс LandLord можно использовать как объект класса FirstHero. Это стало возможным благодаря тому, что класс LandLord наследует класса FirstHero.

[править] Перегруженные методы

Иногда при создании сложных (и не очень) систем есть необходимость реализовать два различных метода, названых одинаково – это возможно с помощью перегруженных (overloading) методов. Выбор реализации зависит от типа объектов, переданных в качестве параметров.

public class GreatKing extends LandLord implements King{
  private String wife;
  private boolean theftedWife;
 
  public GreatKing(String Quest, Hero hisHero) {
    super(Quest, hisHero);
  }
 
  public void setWife(String PrincesName) {
    this.wife = PrincesName;
    this.theftedWife = false;
  }
 
  public String getWife() {
    if (theftedWife){return null;}
    else{ return wife;}
  }
 
  public String getWife(boolean flag) {
    if (flag){ this.theftedWife = true;}
    return wife;
  }
}

В представленном классе GreatKing существуют две реализации перезагруженного метода getWife, которые можно вызвать следующим образом:

GreatKing ourKing = new GreatKing("Править мудро", (Hero) new FirstHero("Охранять господина"));
String hisWife = ourKing.getWife();
String theftedHisWife = ourKing.getWife(true);

[править] Вместо заключения

Ну вот, мы создали всех героев. Разместите интерфейсы Lord, King, а также классы LandLord и GreatKing в соответствующих файлах. Немного измените класс Story, чтобы завершить сказку:

public class Story {
  public static void main(String[] args) {
    // создадим наших героев
    Hero ourNewHero = (Hero) new FirstHero("Спасити царевну");
    GreatKing ourKing = new GreatKing("Править мудро", (Hero) new FirstHero("Охранять господина"));
    LandLord ourNewLandLord = new LandLord("Похитить царевну", (Hero) new FirstHero("Охранять господина"));
    // Женим нашего короля
    ourKing.setWife("Несмеяна");
    // И начнем повествование
    System.out.println("Жил был мудрый король, и должен был он " + ourKing.getQuest());
    System.out.println("Была у него жена, и звали ее " + ourKing.getWife());
    System.out.println("Был в королевстве коварный Визирь и хотел он " + ourNewLandLord.getQuest());
    System.out.println("И сделал это");
    String wife = ourKing.getWife(true);
    //
    System.out.println("А наш герой жаждал " + ourNewHero.getQuest() + " ..и добился этого");
    ourKing.setWife(wife);
    System.out.println("Король был счастлив.");
    System.out.println("Ну а что случилось дальше вы можете придумать сами...");
  }
}

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

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

Персональные инструменты
купить
подписаться
Яндекс.Метрика