Приступим к изучению методов совершенствования объекта Person, начиная со следующих:

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

Перегрузка методов

При создании двух методов с одинаковыми именами, но с разными списками аргументов (то есть разными по количеству или типу параметрами) получается перегруженный (overloaded) метод. Перегруженные методы всегда находятся в одном и том же классе. Во время выполнения программы среда исполнения Java (Java Runtime Environment - JRE, или Java Runtime) решает, какой вариант перегруженного метода вызвать, в зависимости от переданных ему аргументов.

Предположим, что объекту Person требуется несколько методов распечатки отчета о своем текущем состоянии. Назовем эти методы printAudit(). Вставьте такой перегруженный метод, показанный в листинге 1, в окно редактора Eclipse.

Листинг 1. printAudit(): перегруженный метод
public void printAudit(StringBuilder buffer) {
 buffer.append("Name="); buffer.append(getName());
 buffer.append(","); buffer.append("Age="); buffer.append(getAge());
 buffer.append(","); buffer.append("Height="); buffer.append(getHeight());
 buffer.append(","); buffer.append("Weight="); buffer.append(getWeight());
 buffer.append(","); buffer.append("EyeColor="); buffer.append(getEyeColor());
 buffer.append(","); buffer.append("Gender="); buffer.append(getGender());
}
public void printAudit(Logger l) {
 StringBuilder sb = new StringBuilder();
 printAudit(sb);
 l.info(sb.toString());
}

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

Два правила перегрузки методов

Запомните два важных правила работы с перегруженными методами:

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

При нарушении этих правил компилятор выдаст сообщение об ошибке.

Переопределение методов

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

Employee: подкласс класса Person

Как говорилось в Части 1 этого руководства, Employee может быть подклассом (или дочерним классом) класса Person, наделенным некоторыми дополнительными атрибутами:

  • код налогоплательщика,
  • табельный номер;
  • дата приема на работу,
  • заработная плата.

Чтобы объявить такой класс в файле с именем Employee.java, щелкните правой кнопкой мыши на пакете com.makotogroup.intro в Eclipse. Выберите New > Class...; откроется диалоговое окно создания нового класса Java, как показано на рисунке 1.

Рисунок 1. Диалоговое окно New Java Class

Диалог New Java Class в Project Explorer

Введите Employee в качестве имени класса и Person в качестве суперкласса, затем нажмите кнопку Finish. Вы увидите класс Employee в окне редактирования. Явно объявлять конструктор не нужно, просто двигайтесь дальше, и оба конструктора будут реализованы. Убедитесь, что фокус находится в окне редактирования класса Employee, и выберите Source > Generate Constructors from Superclass...; вы увидите диалоговое окно, показанное на рисунке 2.

Рисунок 2. Создание конструкторов в диалоговом окне Superclass

Путь для создания конструктора

Отметьте оба конструктора (как показано на рисунке 2) и нажмите кнопку OK. Eclipse сгенерирует конструкторы. Теперь класс Employee должен выглядеть, как в листинге 2.

Листинг 2. Новый, усовершенствованный класс Employee
package com.makotogroup.intro;

public class Employee extends Person {

 public Employee() {
 super();
 // Заглушка автоматически сгенерированного конструктора TODO 
 }

 public Employee(String name, int age, int height, int weight,
 String eyeColor, String gender) {
 super(name, age, height, weight, eyeColor, gender);
 // Заглушка автоматически сгенерированного конструктора TODO 
 }

}

Employee наследует свойства Person

Как видно в листинге 3, Employee наследует атрибуты и поведение своего родителя Person, а также имеет некоторые собственные свойства.

Листинг 3. Класс Employee с атрибутами Person
package com.makotogroup.intro;
import java.math.BigDecimal;

public class Employee extends Person {

 private String taxpayerIdentificationNumber;
 private String employeeNumber;
 private BigDecimal salary;

 public Employee() {
 super();
 }
 public String getTaxpayerIdentificationNumber() {
 return taxpayerIdentificationNumber;
 }
 public void setTaxpayerIdentificationNumber(String taxpayerIdentificationNumber) {
 this.taxpayerIdentificationNumber = taxpayerIdentificationNumber;
 }
 // Другие геттеры/сеттеры...
}

Переопределение метода: printAudit()

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

Решение заключается в том, чтобы переопределить перегрузку printAudit(), которая в качестве параметра принимает StringBuilder, и добавить код для печати атрибутов, специфичных для Employee.

Для этого в Eclipse IDE выберите Source > Override/Implement Methods..., и вы увидите диалоговое окно, как на рисунке 3.

Рисунок 3. Диалоговое окно Override/Implement Methods

Диалоговое окно Override/Implement Methods

Выберите перегрузку StringBuilder метода printAudit, как показано на рисунке 3, и нажмите кнопку OK. Eclipse сгенерирует заглушку метода, и вы сможете просто заполнить остальное, вот так:

@Override
public void printAudit(StringBuilder buffer) {
 // Сначала вызывается суперкласс этого метода для получения значений его атрибутов
 super.printAudit(buffer);
 // Теперь форматируются значения для этого экземпляра
 buffer.append("TaxpayerIdentificationNumber=");
 buffer.append(getTaxpayerIdentificationNumber());
 buffer.append(","); buffer.append("EmployeeNumber=");
 buffer.append(getEmployeeNumber());
 buffer.append(","); buffer.append("Salary=");
 buffer.append(getSalary().setScale(2).toPlainString());
}

Обратите внимание на вызов метода super.printAudit(). Мы просим суперкласс (Person) представить поведение своего метода printAudit(), а затем добавляем к нему поведение метода printAudit() для Employee.

Вызов super.printAudit() не обязательно должен быть первым, просто полезно распечатать сначала эти атрибуты. На самом деле, вообще не нужно вызывать super.printAudit(). Если его не вызывать, нужно либо отформатировать атрибуты из Person самостоятельно (в методе Employee.printAudit ()), либо исключить их совсем. Создание вызова super.printAudit() в данном случае еще проще.

Члены класса

Переменные и методы, полученные для Person и Employee, - это экземпляры переменных и методов. Чтобы ими воспользоваться, нужно либо создать необходимый экземпляр класса, либо иметь ссылку на такой экземпляр. Каждый экземпляр объекта имеет переменные и методы, и для каждого из них точное поведение (например, порожденное вызовом printAudit()) будет отличаться, потому что оно основано на состоянии экземпляра объекта.

Сами классы тоже могут иметь переменные и методы, которые называются членами класса. Члены класса декларируются с помощью ключевого слова static, о котором говорилось в Части 1 этого руководства. Различия между членами класса и членами экземпляров:

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

Добавление переменных и методов класса

Когда имеет смысл добавлять переменные и методы класса? Золотое правило: делать это редко, не злоупотребляя. Тем не менее, использовать переменные и методы класса бывает полезно:

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

Переменные класса

Чтобы создать переменную класса, при ее объявлении используется ключевое слово static:

accessSpecifier static variableName [= initialValue];

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

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

Например, мы определили атрибут Gender класса Person как переменную типа String, но не сделали для него никаких ограничений. В листинге 4 показан общий подход к использованию переменных класса.

Листинг 4. Использование переменных класса
public class Person {
//. . .
 public static final String GENDER_MALE = "MALE";
 public static final String GENDER_FEMALE = "FEMALE";
// . . .
 public static void main(String[] args) {
 Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", GENDER_MALE);
 // . . .
 }
//. . .
}

Объявление констант

Как правило, константы:

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

В листинге 4, чтобы использовать константу MALE при вызове конструктора Person, достаточно просто указать ее имя. Для использования константы вне класса нужно предварить ее именем класса, в котором она объявлена:

String genderValue = Person.GENDER_MALE;

Методы класса

Те, кто следит за примерами, начиная с Части 1, уже несколько раз вызывали статический метод Logger.getLogger() - всякий раз при получении экземпляра Logger для вывода каких-нибудь записей на консоль. Однако для этого не нужен экземпляр Logger; вместо него можно ссылаться на сам класс Logger. Это синтаксис вызова метода класса. Как и в случае переменных класса, ключевое слово static определяет Logger (в данном примере) как метод класса. По этой причине методы класса иногда еще называют статическими методами.

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

Теперь используем все, что вы узнали о статических переменных и методах, для создания статического метода Employee. Объявим переменную private static final для хранения Logger, который будут совместно использовать все экземпляры и который будет доступен по вызову getLogger() по отношению к классу Employee. В листинге 5 показано, как это делается.

Листинг 5. Создание метода класса (или статического метода)
public class Employee extends Person {
 private static final Logger logger = Logger.getLogger(Employee.class.getName());
//. . .
 public static Logger getLogger() {
 return logger;

 }

}

В листинге 5 происходят две важные вещи:

  • объявляется экземпляр Logger с доступом private, так что ни один класс вне Employee не может получить доступ напрямую;
  • Logger инициализируется при загрузке класса; это происходит потому, что для придания ему значения используется синтаксис инициализации Java.

Чтобы получить объект Logger класса Employee, нужно выполнить следующий вызов:

Logger employeeLogger = Employee.getLogger();

Сравнение объектов

Язык Java предоставляет два способа сравнения объектов:

  • оператор ==;
  • метод equals().

Сравнение объектов с помощью оператора ==

Оператор == сравнивает объекты на равенство, так что выражение a == b возвращает значение true, только если а и b имеют одно и то же значение. Для объектов это означает, что оба ссылаются на один и тот же экземпляр объекта. Для примитивов это означает, что значения идентичны. Рассмотрим в качестве примера Листинг 6.

Листинг 6. Сравнение объектов с помощью оператора ==
int int1 = 1;
int int2 = 1;
l.info("Q: int1 == int2? A: " + (int1 == int2));

Integer integer1 = Integer.valueOf(int1);
Integer integer2 = Integer.valueOf(int2);
l.info("Q: Integer1 == Integer2? A: " + (integer1 == integer2));

integer1 = new Integer(int1);
integer2 = new Integer(int2);
l.info("Q: Integer1 == Integer2? A: " + (integer1 == integer2));

Employee employee1 = new Employee();
Employee employee2 = new Employee();
l.info("Q: Employee1 == Employee2? A: " + (employee1 == employee2));

Если выполнить код листинга 6 внутри Eclipse, результат будет:

Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: int1 == int2? A: true
Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: Integer1 == Integer2? A: true
Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: Integer1 == Integer2? A: false
Apr 19, 2010 5:30:10 AM com.makotogroup.intro.Employee main
INFO: Q: Employee1 == Employee2? A: false

В первом случае в листинге 6 значения примитивов одни и те же, так что оператор == возвращает значение true. Во втором случае объекты Integer ссылаются на один и тот же экземпляр, так что == опять возвращает значение true. В третьем случае, хотя объекты Integer содержат одно и то же значение, == возвращает результат false, потому что integer1 и integer2 относятся к разным объектам. Отсюда должно быть ясно, почему employee1 == employee2 возвращает false.

Сравнение объектов с помощью equals()

equals() - это метод, который каждый объект языка Java получает "бесплатно", потому что он определяется как метод экземпляра java.lang.Object (от которого наследует каждый объект Java).

equals() вызывается точно так же, как любой другой метод:

a.equals(b);

Этот оператор вызывает метод equals() объекта а, передавая ему ссылку на объект b. По умолчанию Java-программа просто проверяет, что два объекта одинаковы, с помощью синтаксиса ==. Однако так как equals() - это метод, его можно переопределить. Рассмотрим пример из листинга 6, измененный в листинге 7 для сравнения двух объектов с помощью метода equals().

Листинг 7. Сравнение объектов с помощью метода equals()
Logger l = Logger.getLogger(Employee.class.getName());

Integer integer1 = Integer.valueOf(1);
Integer integer2 = Integer.valueOf(1);
l.info("Q: integer1 == integer2? A: " + (integer1 == integer2));
l.info("Q: integer1.equals(integer2)? A: " + integer1.equals(integer2));

integer1 = new Integer(integer1);
integer2 = new Integer(integer2);
l.info("Q: integer1 == integer2? A: " + (integer1 == integer2));
l.info("Q: integer1.equals(integer2)? A: " + integer1.equals(integer2));

Employee employee1 = new Employee();
Employee employee2 = new Employee();
l.info("Q: employee1 == employee2? A: " + (employee1 == employee2));
l.info("Q: employee1.equals(employee2)? A: " + integer1.equals(integer2));
Running this code produces:

Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1 == integer2? A: true
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1.equals(integer2)? A: true
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1 == integer2? A: false
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: integer1.equals(integer2)? A: true
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: employee1 == employee2? A: false
Apr 19, 2010 5:43:53 AM com.makotogroup.intro.Employee main
INFO: Q: employee1.equals(employee2)? A: false

Примечание о сравнении значений Integer

Нет ничего удивительного в том, что в листинге 7 метод equals() в применении к Integer возвращает true, если == возвращает true; но заметьте, что происходит во втором случае, когда создаются отдельные объекты, содержащие одно и то же значение 1: == возвращает false, потому что integer1 и integer2 относятся к разным объектам; но equals() возвращает true.

Создатели JDK решили, что для Integer значение equals() должно отличаться от стандартного (когда сравниваются ссылки на объект, чтобы определить, относятся ли они к одному и тому же объекту), возвращая значение true, когда внутренние значения int совпадают.

Для Employeeequals() не переопределяется, поэтому поведение по умолчанию (использование ==) возвращает то, что следует ожидать, учитывая, что employee1 и employee2 на самом деле относятся к разным объектам.

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

Переопределение equals()

Переопределив поведение метода Object.equals() по умолчанию, можно определить, что именно equals() будет означать для объектов вашего приложения. Опять же, для этого можно использовать Eclipse. Убедитесь, что в окне Eclipse IDE Source Employee находится в фокусе, и выберите Source > Override/Implement Methods. Появится диалоговое окно, изображенное на рисунке 4.

Рисунок 4. Диалоговое окно Override/Implement Methods

Диалоговое окно Override/Implement Methods

Мы уже использовали этот диалог, но в данном случае нам нужно реализовать метод суперкласса Object.equals(). Найдите в списке объект Object, отметьте метод equals(Object) и нажмите кнопку OK. Eclipse сгенерирует правильный код и разместит его в исходном файле.

Логично, что два объекта Employee, если состояние этих объектов одинаково. То есть, они равны, если их атрибуты - фамилия, имя, возраст – одни и те же.

Автоматическое создание метода equals()

Eclipse может генерировать метод equals() на основе переменных экземпляра (атрибутов), определиенных для класса. Так как Employee - это подкласс класса Person, сначала сгенерируем метод equals() для Person. В окне Eclipse Project Explorer щелкните правой кнопкой мыши на Person и выберите Generate hashCode() and equals(), чтобы вызвать диалоговое окно, показанное на рисунке 5.

Рисунок 5. Диалоговое окно для создания hashCode() и equals()

Диалоговое окно для автоматической генерации equals()

Выберите все атрибуты (как показано на рисунке 5) и нажмите кнопку OK. Eclipse сгенерирует метод equals(), который выглядит, как в листинге 8.

Листинг 8. Метод equals(), сгенерированный Eclipse
@Override
public boolean equals(Object obj) {
 if (this == obj)
 return true;
 if (obj == null)
 return false;
 if (getClass() != obj.getClass())
 return false;
 Person other = (Person) obj;
 if (age != other.age)
 return false;
 if (eyeColor == null) {
 if (other.eyeColor != null)
 return false;
 } else if (!eyeColor.equals(other.eyeColor))
 return false;
 if (gender == null) {
 if (other.gender != null)
 return false;
 } else if (!gender.equals(other.gender))
 return false;
 if (height != other.height)
 return false;
 if (name == null) {
 if (other.name != null)
 return false;
 } else if (!name.equals(other.name))
 return false;
 if (weight != other.weight)
 return false;
 return true;
}

О методе hashCode() пока не беспокойтесь - его можно сохранить или удалить. Метод equals(), сгенерированный Eclipse, кажется сложным, но делает он довольно простую вещь: если переданный объект – тот же, что в листинге 8, то equals() возвратит значение true. Если переданный объект – это null, он возвратит значение false.

Затем метод проверяет на совпадение объекты Class (что означает, что переданный объект должен быть объектом Person). Если они совпадают, то каждое значение атрибута переданного объекта проверяется на совпадение со значением экземпляра Person. Если атрибуты имеют значение null (то есть отсутствуют), equals() проверит все что есть, и если они совпадают, объекты будут считаться равными. Такое поведение подойдет не для каждой программы, но для большинства задач это работает.

Упражнение: создание метода equals() для объекта Employee

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

public static void main(String[] args) {
 Logger l = Logger.getLogger(Employee.class.getName());

 Employee employee1 = new Employee();
 employee1.setName("J Smith");
 Employee employee2 = new Employee();
 employee2.setName("J Smith");
 l.info("Q: employee1 == employee2? A: " + (employee1 == employee2));
 l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
}

Выполнив этот код, вы должны увидеть следующий результат:

Apr 19, 2010 5:26:50 PM com.makotogroup.intro.Employee main
INFO: Q: employee1 == employee2? A: false
Apr 19, 2010 5:26:50 PM com.makotogroup.intro.Employee main
INFO: Q: employee1.equals(employee2)? A: true

В данном случае одного только совпадения по Name достаточно, чтобы equals() посчитал объекты равными. Добавьте к этому другие атрибуты и посмотрите, что получится.

Упражнение: переопределение toString()

Помните метод printAudit(), о котором говорилось в самом начале этого раздела? Если вы подумали, что он сложноват, то были правы! Форматирование состояние объекта в String - столь распространенная операция, что разработчики Java встроили ее прямо в Object, в виде метода, называемого (что не удивительно) toString(). Реализация метода toString() по умолчанию не очень полезна, но она есть в каждом объекте. В этом упражнении мы сделаем метод toString() по умолчанию немного более полезным.

Если вы подозреваете, что Eclipse в состоянии сгенерировать для вас метод toString(), то вы правы. Вернитесь в Project Explorer и щелкните правой кнопкой мыши на классе Employee, затем выберите Source > Generate toString().... Вы увидите диалоговое окно, подобное показанному на рисунке 5. Выберите все атрибуты и нажмите кнопку OK. Код, сгенерированный Eclipse для Employee, приведен в листинге 9.

Листинг 9. Метод toString(), сгенерированный Eclipse
@Override
public String toString() {
 return "Employee [employeeNumber=" + employeeNumber + ", salary=" + salary
 + ", taxpayerIdentificationNumber=" + taxpayerIdentificationNumber
 + "]";
}

Код, созданный Eclipse для toString, не содержит метода toString() суперкласса (суперклассом для Employee является Person). Это можно исправить с помощью Eclipse, сделав следующее переопределение:

@Override
public String toString() {
 return super.toString() +
 "Employee [employeeNumber=" + employeeNumber + ", salary=" + salary
 + ", taxpayerIdentificationNumber=" + taxpayerIdentificationNumber
 + "]";
}

Добавление toString() значительно упрощает метод printAudit():

@Override
public void printAudit(StringBuilder buffer) {
 buffer.append(toString());
}

Теперь toString() выполняет всю тяжелую работу по форматированию текущего состояния объекта, и вы просто берете то, что он возвращает в StringBuilder.

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

Исключения

Ни одна программа никогда не работает правильно в 100% случаев, и разработчики языка Java учли это. Этот раздел посвящен встроенным в платформу Java механизмам обработки ситуаций, когда код работает не совсем так, как планировалось.

Основы обработки исключений

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

Чтобы приступить к обработке исключений, взгляните на код, приведенный в листинге 10.

Листинг 10. Видите ошибку?
// . . .
public class Employee extends Person {
// . . .
 private static Logger logger;// = Logger.getLogger(Employee.class.getName());

 public static void main(String[] args) {
 Employee employee1 = new Employee();
 employee1.setName("J Smith");
 Employee employee2 = new Employee();
 employee2.setName("J Smith");
 logger.info("Q: employee1 == employee2? A: " + (employee1 == employee2));
 logger.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));

 }

Обратите внимание, что инициализатор статической переменной, содержащей ссылку Logger, закомментирован. Выполните этот код, и вы получите следующий результат:

Exception in thread "main" java.lang.NullPointerException
 at com.makotogroup.intro.Employee.main(Employee.java:54)

Он говорит о том, что вы пытаетесь сослаться на несуществующий объект, что является довольно серьезной ошибкой программирования. К счастью, можно использовать блоки try и catch, чтобы попытаться "выловить" ее (с небольшой помощью со стороны блока finally).

Блоки try, catch и finally

В листинге 11 приведен код с ошибкой из листинга 10, очищенный с помощью стандартных блоков для обработки исключений try, catch и finally.

Листинг 11. Вылавливание исключений
// . . .
public class Employee extends Person {
// . . .
 private static Logger logger;// = Logger.getLogger(Employee.class.getName());

 public static void main(String[] args) {
 try {
 Employee employee1 = new Employee();
 employee1.setName("J Smith");
 Employee employee2 = new Employee();
 employee2.setName("J Smith");
 logger.info("Q: employee1 == employee2? A: " + (employee1 == employee2));
 logger.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
 } catch (NullPointerException npe) {
 // Обработка...
 System.out.println("Yuck! Outputting a message with System.out.println() " +
 "because the developer did something dumb!");
 } finally {
 // Всегда выполняется
 }
 }

Блоки try, catch и finally вместе образуют "сеть для ловли исключений". Сначала код, который может вызвать исключение, заключается в оператор try. Если это сработает, управление сразу передается блоку catch, или обработчику исключений. Когда оба оператора выполнены, управление передается блоку finally, независимо от того, имело ли место исключение. "Поймав" исключение, можно попытаться аккуратно обойти его или же выйти из программы (или метода).

В листинге 11 программа восстанавливается после ошибки и выводит сообщение о том, что произошло.

Иерархия исключений

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

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

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

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

Несколько блоков catch

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

try {
 // Код...
} catch (NullPointerException e) {
 // Обработка NPE...
} catch (Exception e) {
 // Обработка других общих исключений...
}

В этом примере NullPointerException - это дочерний класс (в конечном счете) исключения Exception, так что его следует поместить перед более общим блоком Exception catch.

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

Создание Java-приложений

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

Элементы Java-приложения

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

Начиная с Части 1, мы работаем с примером приложения для отдела кадров, которое включает в себя класс Person и его подклассы Employee. Теперь вы увидите, что происходит при добавлении к приложению нового класса.

Создание класса драйвера

Цель класса драйвера (как следует из названия) состоит в том, чтобы "вести" приложение. Обратите внимание, что этот простой драйвер для кадрового приложения содержит метод main():

package com.makotogroup.intro;
public class HumanResourcesApplication {
 public static void main(String[] args) {
 }
}

Создадим класса драйвера в Eclipse, используя ту же процедуру, которую мы применяли для создания классов Person и Employee. Назовем этот класс HumanResourcesApplication и не забудем выбрать добавление к классу метода main(). Eclipse сгенерирует класс.

Добавим немного кода в свой новый класс main(), чтобы он выглядел примерно так:

public static void main(String[] args) {
 Employee e = new Employee();
 e.setName("J Smith");
 e.setEmployeeNumber("0001");
 e.setTaxpayerIdentificationNumber("123-45-6789");
 e.printAudit(log);
}

Теперь запустим класс HumanResourcesApplication и последим за его выполнением. Вы должны увидеть следующий результат (обратная косая черта здесь указывает на продолжение строки):

Apr 29, 2010 6:45:17 AM com.makotogroup.intro.Person printAudit
INFO: Person [age=0, eyeColor=null, gender=null, height=0, name=J Smith,\
weight=0]Employee [employeeNumber=0001, salary=null,\
taxpayerIdentificationNumber=123-45-6789]

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

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

В данном руководстве мы уже несколько раз встречались с примерами наследования. В этом разделе повторяется кое-что из материала Части 1 о наследовании и более подробно объясняется, как работает наследование - включая иерархию наследования, конструкторы и наследование, а также абстракцию наследования.

Как работает наследование

Классы в Java-коде находятся в иерархиях. Классы, которые по иерархии находятся выше данного класса, называются суперклассами этого класса. Каждый конкретный класс является подклассом каждого класса, расположенного в иерархии выше его. Подкласс наследует свойства своего суперкласса. Класс java.lang.Object находится в вершине иерархии классов, то есть каждый класс Java является подклассом Object и наследует его свойства.

Например, предположим, что существует класс Person, который выглядит как в листинге 12.

Листинг 12. Public-класс Person
package com.makotogroup.intro;

// . . .
public class Person {
 public static final String GENDER_MALE = "MALE";
 public static final String GENDER_FEMALE = "FEMALE";
 public Person() {
 //Делать больше нечего......
 }
 private String name;
 private int age;
 private int height;
 private int weight;
 private String eyeColor;
 private String gender;
// . . .

}

Класс Person в листинге 12 неявно наследует свойства Object. Так как это предполагается для каждого класса, не нужно вводить фразу extends Object для каждого определяемого класса. Но что означает, что класс наследует свойства своего суперкласса? Лишь то, что Person имеет доступ к представленным переменным и методам своего суперкласса. В данном случае Person может "видеть" и использовать общедоступные методы и переменные объекта Object, а также его защищенные методы и переменные.

Определение иерархии классов

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

public class Employee extends Person {

 private String taxpayerIdentificationNumber;
 private String employeeNumber;
 private BigDecimal salary;
// . . .
}

Множественное и одиночное наследование

Такие языки, как C++, поддерживают концепцию множественного наследования: в любой точке иерархии класс может наследовать свойства одного или нескольких классов. Язык Java поддерживает только одиночное наследование, то есть ключевое слово extends можно использовать только с одним классом. Так что иерархия классов для любого заданного класса Java всегда состоит из прямой, охватывающей весь путь вплоть до java.lang.Object.

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

Граф наследования для Employee указывает на то, что Employee имеет доступ ко всем общедоступным и защищенным переменным и методам Person (потому что он непосредственно расширяет его), а также Object (потому что он фактически расширяет и этот класс, хотя и опосредованно). Однако поскольку Employee и Person находятся в одном и том же пакете, Employee имеет также доступ к переменным и методам package-private (их еще иногда называют дружественными (friendly)) класса Person.

Чтобы углубиться в иерархию классов еще на один шаг, можно создать третий класс, который расширяет Employee:

public class Manager extends Employee {
// . . .
}

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

Конструкторы и наследование

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

Основы конструкторов

Помните, что конструктор всегда имеет то же имя, что и класс, в котором он используется, и у него нет возвращаемого типа. Например:

public class Person {
 public Person() {
 }
}

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

public class Person {
}

Вызов конструктора суперкласса

Чтобы вызвать конструктор суперкласса, отличный от конструктора по умолчанию, нужно сделать это явно. Например, предположим, что у класса Person есть конструктор, который принимает имя создаваемого объекта Person. Конструктор Person можно вызвать из конструктора по умолчанию Employee, как показано в листинге 13.

Листинг 13. Инициализация нового объекта Employee
public class Person {
 private String name;
 public Person() {
 }
 public Person(String name) {
 this.name = name;
 }
}

// Между тем, в Employee.java
public class Employee extends Person {
 public Employee() {
 super("Elmer J Fudd");
 }
}

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

Листинг 14. Единообразный вызов суперкласса
public class Person {
 private String name;
 public Person(String name) {
 this.name = name;
 }
}
// Между тем, в Employee.java
public class Employee extends Person {
 public Employee(String name) {
 super(name);
 }
}

Объявление конструктора

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

public class Person {
 public Person() {
 }
}
// Между тем, в Employee.java
public class Employee extends Person {
 public Employee() {
 }
}

или:

public class Person {
 public Person() {
 }
}
// Между тем, в Employee.java
public class Employee extends Person {
 public Employee() {
 super();
 }
}

Конструкторы без аргументов

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

public class Person {
 private String name;
 public Person(String name) {
 this.name = name;
 }
}
// Между тем, в Employee.java
public class Employee extends Person {
 public Employee() {
 }
}

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

Как конструкторы вызывают конструкторы

Конструктор может быть вызван другим конструктором изнутри класса с использованием ключевого слова this и списка аргументов. Как и super(), вызов this() должен быть первой строкой конструктора. Например:

public class Person {
 private String name;
 public Person() {
 this("Some reasonable default?");
 }
 public Person(String name) {
 this.name = name;
 }
}
// Между тем, в Employee.java

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

Уровни доступа к конструкторам

Конструкторы могут иметь любой уровень доступа; при этом применяются определенные правила видимости. Правила доступа для конструкторов приведены в таблице 1.

Таблица 1. Правила доступа к конструкторам
Модификатор доступа к конструкторуОписание
public Конструктор может быть вызван любым классом.
protected Конструктор может быть вызван классом из того же пакета или любым подклассом.
Без модификатора (package-private) Конструктор может быть вызван любым классом из того же пакета.
private Конструктор может быть вызван только тем классом, в котором он определен.

Можно придумать случаи, когда конструкторы объявляются как protected или даже package-private, но зачем нужен конструктор private? Я использовал конструкторы private, когда не хотел допустить прямого создания объекта с помощью ключевого слова new при реализации, скажем, модели Factory (см. раздел Ресурсы). В этом случае для создания экземпляров класса может использоваться статический метод, и этот метод, будучи включенным в сам класс, может вызвать конструктор private.

Наследование и абстракция

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

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

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

Абстрактные классы и методы

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

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

Использование абстрагирования

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

public abstract class Employee extends Person {
// и т.д.
}

Если вы попытаетесь выполнить этот код, вы получите ошибку компиляции:

public void someMethodSomwhere() {
 Employee p = new Employee();// ошибка компиляции!!
}

Компилятор жалуется, что Employee - это абстрактный класс, и его экземпляр не может быть создан.

Возможности абстрагирования

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

public abstract class Employee extends Person {
 public abstract boolean validate();
}

Теперь каждый прямой подкласс Employee (такой как Manager) должен реализовать метод validate(). Однако как только подкласс реализовал метод validate(), ни одному из его подклассов не придется этого делать.

Например, предположим, что имеется объект Executive, который расширяет класс Manager. Такое определение было бы вполне допустимо:

public class Executive extends Manager {
 public Executive() {
 }
}

Когда (не) следует абстрагировать: два правила

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

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

Присвоения: классы

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

Manager m = new Manager();
Employee e = new Employee();
Person p = m; // нормально
p = e; // тоже нормально
Employee e2 = e; // да, так можно
e = m; // тоже нормально
e2 = p; // Неверно!

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

Manager m = new Manager();
Person p = m; // пока все хорошо
Employee e = m; // нормально
Employee e = p; // Неверно!

Employee (сотрудник) - это Person (человек), но это определенно не Manager (начальник), и компилятор следит за этим.

Интерфейсы

В этом разделе мы приступим к изучению интерфейсов и их использованию в коде Java.

Определение интерфейса

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

Определить интерфейс легко:

public interface interfaceName {
 returnType methodName( argumentList );
}

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

Методы, определенные в интерфейсе, не имеют тела метода. За предоставление тела метода отвечает реализатор интерфейса (как в случае с абстрактными методами).

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

public class Manager extends Employee implements BonusEligible, StockOptionRecipient {
// и т.д.
}

Интерфейсы-маркеры

Интерфейс может вообще не иметь никакого тела. На самом деле, вполне приемлемо следующее определение:

public interface BonusEligible {
}

Вообще говоря, такие интерфейсы называются интерфейсами-маркерами (marker interfaces), потому что они маркируют класс как реализацию этого интерфейса, но не предлагают особого явного поведения.

Зная все это, интерфейс определить очень легко:

public interface StockOptionRecipient {
 void processStockOptions(int numberOfOptions, BigDecimal price);
}

Реализация интерфейсов

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

public class className extends superclassName implements interfaceName {
// Тело класса
}

Предположим, что мы реализуем интерфейс StockOptionRecipient для класса Manager, как показано в листинге 15.

Листинг 15. Реализация интерфейса
public class Manager extends Employee implements StockOptionRecipient {
 public Manager() {
 }
 public void processStockOptions (int numberOfOptions, BigDecimal price) {
 log.info("I can't believe I got " + number + " options at $" + 
 price.toPlainString() + "!"); }
}

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

Создание интерфейсов в Eclipse

Если вы решите, что один из классов должен реализовать интерфейс, Eclipse легко сгенерирует правильную сигнатуру метода. Для реализации интерфейса достаточно изменить сигнатуру класса. Eclipse подчеркивает класс красной волнистой линией, помечая его как ошибочный, потому что класс не содержит метода (методов) интерфейса. Щелкните кнопкой мыши на имени класса, нажмите Ctrl + 1, и Eclipse предложит подсказки. Выберите из них Add Unimplemented Methods (Добавить нереализованные методы), и Eclipse сгенерирует методы, поместив их в нижнюю часть исходного файла.

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

Использование интерфейсов

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

Листинг 16. Присвоение нового экземпляра Manager ссылке StockOptionEligible
public static void main(String[] args) {
 StockOptionEligible soe = new Manager();// Вполне допустимо
 calculateAndAwardStockOptions(soe);
 calculateAndAwardStockOptions(new Manager());// Тоже работает
}
. . .
public static void calculateAndAwardStockOptions(StockOptionEligible soe) {
 BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
 int numberOfOptions = 10000;
 soe.processStockOptions(numberOfOptions, reallyCheapPrice);
}

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

Присвоения: классы

Из класса, реализующего интерфейс, можно присвоить ссылку переменной типа interface, но существуют определенные правила. В листинге 16 видно, что присвоение экземпляра Manager ссылке на переменную StockOptionEligible вполне допустимо. Причина в том, что класс Manager реализует этот интерфейс. Тем не менее, следующее присвоение будет недействительно:

 Manager m = new Manager();
 StockOptionEligible soe = m; // Нормально
 Employee e = soe; // Неверно!

Так как Employee - это супертип по отношению к Manager, на первый взгляд это может показаться правильным, но это не так. Потому что Manager является специализацией Employee, он *другой* и в данном конкретном случае реализует интерфейс, который не реализует Employee.

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

Вложенные классы

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

Где использовать вложенные классы

Как следует из названия, вложенный класс - это класс, определенный в другом классе. Вот вложенный класс:

public class EnclosingClass {
. . .
 public class NestedClass {
 . . .

 }
}

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

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

Область видимости вложенных классов

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

Предположим, что имеется следующее соотношение между менеджером Manager и вложенным классом DirectReports, который отражает группу сотрудников Employee, которые подчиняются начальнику Manager:

public class Manager extends Employee {
 private DirectReports directReports;
 public Manager() {
 this.directReports = new DirectReports();
 }
. . .
 private class DirectReports {
 . . .
 }
}

Так же как каждый объект Manager соответствует отдельному человеку, объект DirectReports соответствует группе реальных людей (работников), которые подчиняются начальнику. У разных начальников будут разные подчиненные DirectReports. В этом случае на вложенный класс DirectReports имеет смысл ссылаться только в контексте экземпляра содержащего его класса Manager, поэтому я сделал его классом private.

Вложенные классы public

Так как это класс private, экземпляр объекта DirectReports может создать только Manager. Но что, если нужно предоставить возможность создания экземпляров DirectReports внешнему объекту? Казалось бы, в этом случае можно присвоить классу DirectReports область видимости public, и тогда любой внешний код сможет создавать экземпляры DirectReports, как показано в листинге 17.

Listing 17. Creating DirectReports instances: First attempt
public class Manager extends Employee {
 public Manager() {
 }
. . .
 private class DirectReports {
 . . .
 }
}
//
public static void main(String[] args) {
 Manager.DirectReports dr = new Manager.DirectReports();// Это не будет работать!
}

Код, приведенный в листинге 17, не работает. Почему? Проблема (а также ее решение) связана со способом определения DirectReports в Manager и с правилами для данной области видимости.

Правила области видимости: повторение

Если есть переменная-член класса Manager, то можно ожидать, что компилятор потребует сначала сослаться на объект Manager, верно? То же относится и к классу DirectReports, по крайней мере, как мы определили его в листинге 17.

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

public class Manager extends Employee {
 public Manager() {
 }
. . .
 private class DirectReports {
 . . .
 }
}
// Между тем, где-нибудь в другом методе...
public static void main(String[] args) {
 Manager manager = new Manager();
 Manager.DirectReports dr = manager.new DirectReports();
}

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

Статические внутренние классы

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

public class Manager extends Employee {
. . .
 public static class ManagerComparator implements Comparator<Manager> {
 . . .
 }
}
// Между тем, где-нибудь в другом методе...
public static void main(String[] args) {
 Manager.ManagerComparator mc = new Manager.ManagerComparator();
 . . .
}

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

Анонимные внутренние классы

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

Листинг 18 опирается на пример из листинга 15 с добавлением метода по умолчанию для обработки типов Employee, отличных от StockOptionEligible:

Листинг 18. Обработка типов Employee , отличных от StockOptionEligible
public static void main(String[] args) {
 Employee employee = new Manager();// Вполне допустимо
 handleStockOptions(employee);
 employee = new Employee();// не StockOptionEligible
 handleStockOptions(employee);
}
. . .
private static void handleStockOptions(Employee e) {
 if (e instanceof StockOptionEligible) {
 calculateAndAwardStockOptions((StockOptionEligible)e);
 } else {
 calculateAndAwardStockOptions(new StockOptionEligible() {
 @Override
 public void awardStockOptions(int number, BigDecimal price) {
 log.info("Sorry, you're not StockOptionEligible!");
 }
 });
 }
}
. . .
private static void calculateAndAwardStockOptions(StockOptionEligible soe) {
 BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
 int numberOfOptions = 10000;
 soe.processStockOptions(numberOfOptions, reallyCheapPrice);

}

В этом примере создается реализация интерфейса StockOptionEligible с помощью анонимного внутреннего класса для экземпляров Employee, которые не реализуют этот интерфейс. Анонимные внутренние классы могут также использоваться для реализации методов обратного вызова и при обработке событий.

Регулярные выражения

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

API регулярных выражений

Следующий набор строк имеет нечто общее:

  • A string
  • A longer string
  • A much longer string

Заметьте, что каждая из этих строк начинается с буквы а и заканчивается словом string. API регулярных выражений Java (см. Ресурсы) помогает выявить такие элементы, увидеть в них общую модель и делать интересные вещи с использованием полученной информации.

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

  • Pattern описывает модель строки.
  • Matcher проверяет строку на соответствие модели.
  • PatternSyntaxException указывает на что-то неприемлемое в той модели, которую вы пытаетесь определить.

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

Синтаксис модели регулярных выражений

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

Таблица 2. Распространенные конструкции регулярных выражений
Конструкция RegexЧто считается совпадением
. Любой символ
? Ноль (0) или одно (1)повторение предшествующего
* Ноль (0) или более повторений предшествующего
+ Одно (1) или более повторений предшествующего
[] Диапазон символов или цифр
^ Отрицание последующего (то есть, "не что-то")
\d Любая цифра (иначе, [0-9])
\D Любой нецифровой символ (иначе, [^0-9])
\s Любой символ-разделитель (иначе, [\n\t\f\r])
\S Любой символ, отличный от разделителей (иначе, [^\n\t\f\r])
\w Любая буква или цифра (иначе, [A-Za-Z_0-9])
\W Любой знак, отличный от буквы или цифры (иначе, [^\w])

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

Поиск по модели

Вооружившись синтаксисом моделей из таблицы 2, можно выполнить простой пример, приведенный в листинге 19, используя классы API регулярных выражений Java.

Листинг 19. Поиск по модели регулярных выражений
Pattern pattern = Pattern.compile("a.*string");
Matcher matcher = pattern.matcher("a string");
boolean didMatch = matcher.matches();
Logger.getAnonymousLogger().info (didMatch);
int patternStartIndex = matcher.start();
Logger.getAnonymousLogger().info (patternStartIndex);
int patternEndIndex = matcher.end();
Logger.getAnonymousLogger().info (patternEndIndex);

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

Найти строку вида: буква а, за которой следуют ноль или более символов, а за ними - строка string.

Методы сравнения

Затем в листинге 19 вызывается метод matcher() объекта Pattern. Этот вызов создает экземпляр Matcher. Matcher проверяет переданную ему строку на соответствие модели, использованной при создании объекта Pattern.

Каждая строка на языке Java – это индексированная коллекция символов, начиная с 0-го и заканчивая символом с индексом "длина строки минус единица". Matcher анализирует строку, начиная с 0-го символа, в поисках совпадений с моделью. После завершения этого процесса Matcher содержит информацию о найденных (или не найденных) совпадениях в строке ввода. Доступ к этой информации можно получить, вызывая различные методы Matcher:

  • matches() сообщает, совпадает ли с моделью вся входная последовательность;
  • start() указывает значение индекса в строке, с которого начинается совпадение;
  • end() указывает значение индекса в строке, на котором совпадение заканчивается, плюс единица.

Код, приведенный в листинге 19, находит одно совпадение, с 0-й по 7-ю позицию. Следовательно, вызов matches() возвращает значение true, вызов start() возвращает значение 0, a вызов end() возвращает значение 8.

lookingAt() и matches()

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

Here is a string with more than just the pattern.

Если искать по модели a.*string и использовать lookingAt(), мы получим совпадение. Но метод matches() возвратил бы значение false, так как в строке есть не только то, что указано в модели.

Сложные модели регулярных выражений

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

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

MyWikiWord

Зная это, рассмотрим следующую строку:

Here is a WikiWord followed by AnotherWikiWord, then YetAnotherWikiWord.

В этой строке можно искать вики-слова с помощью модели регулярных выражений:

[A-Z][a-z]*([A-Z][a-z]*)+

И вот код для поиска wiki-слов:

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
 Logger.getAnonymousLogger().info("Found this wiki word: " + matcher.group());
}

Выполнив этот код, вы должны увидеть на консоли три wiki-слова.

Замена строк

Поиск совпадений полезен, но и после того как совпадение найдено со строками можно что-то делать. Одни строки можно заменять другими, как текст в текстовом редакторе. В Matcher есть несколько методов для замены элементов строк:

  • ReplaceAll() заменяет все совпадения указанной строкой;
  • replaceFirst() заменяет указанной строкой только первое совпадение.

Использовать методы замены Matcher легко:

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("replacement");
Logger.getAnonymousLogger().info("After: " + result);

Этот код находит wiki-слова, как и прежде. Обнаружив совпадение, Matcher заменяет wiki-слово другим. Выполнив этот код, вы должны увидеть на консоли следующее:

Before: Here is WikiWord followed by AnotherWikiWord, then SomeWikiWord.
After: Here is replacement followed by replacement, then replacement.

При использовании replaceFirst() вы бы увидели:

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
After: Here is a replacement followed by AnotherWikiWord, then SomeWikiWord.

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

При поиске совпадений с моделью регулярного выражения можно получить информацию о найденном. Мы видели, как это делается с помощью методов start() и end() объекта Matcher. Но на совпадения можно ссылаться и с помощью групп.

В каждой модели, как правило, создаются группы – для этого отдельные ее части заключаются в круглые скобки. Группы нумеруются слева направо, начиная с 1 (группа 0 соответствует всему совпадению). Код, приведенный в листинге 20, заменяет каждое вики-слово строкой, которая "обертывает" слово:

Листинг 20. Совпадение групп
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("blah$0blah");
Logger.getAnonymousLogger().info("After: " + result);

Выполнив этот код, вы получите следующий результат:

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
After: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.

Другой подход к совпадению групп

В листинге 20 делается ссылка на все совпадение путем включения в строку замены $0. Любая часть заменяющей строки вида $int относится к группе, указанной целым числом (так, $1 указывает на группу 1 и т.п.). Другими словами, запись $0 эквивалентна matcher.group(0);.

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

StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
 matcher.appendReplacement(buffer, "blah$0blah");
}
matcher.appendTail(buffer);
Logger.getAnonymousLogger().info("After: " + buffer.toString());

И вы получите тот же результат:

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
After: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.

Родовые типы

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

Что такое родовые типы?

С выпуском JDK 5 язык Java вдруг обрел странный и захватывающий новый синтаксис. Некоторые знакомые классы JDK были заменены на эквивалентные родовые типы.

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

Родовые типы в действии

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

В листинге 21 показано, как создается экземпляр java.util.ArrayList.

Листинг 21. Создание экземпляра ArrayList
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// Пока все хорошо

Как видите, ArrayList неоднороден: он содержит два типа String и один тип Integer. До JDK 5 в языке Java не было ничего, что ограничивало бы такое поведение, и это приводило к многочисленным ошибкам программирования. Например, в листинге 21 пока все выглядит хорошо. Но как насчет доступа к элементам ArrayList, который пытается получить код, приведенный в листинге 22?

Листинг 22. Попытка получить доступ к элементам в ArrayList
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// Пока все хорошо
*processArrayList(arrayList);
*// В некоторой более поздней части кода...
private void processArrayList(ArrayList theList) {
 for (int aa = 0; aa < theList.size(); aa++) {
 // В какой-то момент это не удастся...
 String s = (String)theList.get(aa);
 }
}

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

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

Listing 23. A second attempt, using generics
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("A String");
arrayList.add(new Integer(10));// ошибка компиляции!!
arrayList.add("Another String");
// Пока все хорошо
*processArrayList(arrayList);
*// В некоторой более поздней части кода...
private void processArrayList(ArrayList<String> theList) {
 for (int aa = 0; aa < theList.size(); aa++) {
 // Приведение не требуется...
 String s = theList.get(aa);
 }
}

Итерация с использованием родовых типов

Родовые типы дополнили язык Java специальным синтаксисом для работы с такими объектами, как списки, которые обычно перебирают элемент за элементом. Например, если нужно перебрать ArrayList, код из листинга 23 можно переписать следующим образом:

private void processArrayList(ArrayList<String> theList) {
 for (String s : theList) {
 String s = theList.get(aa);
 }
}

Этот синтаксис работает для объектов любого типа, которые итерабельны (то есть реализуют интерфейсIterable).

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

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

Если предполагается параметризация класса, нужно посмотреть, применимы ли следующие критерии:

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

Если оба эти критерия удовлетворены, то очевидно, что коллекция отвечает следующим требованиям:

  • "нечто" есть класс в составе коллекции;
  • операции (add, remove, size, clear и т.п.) в значительной степени одинаковы, независимо от объекта, составляющего коллекцию.

Параметризованный список

Код для создания списка в синтаксисе родовых типов выглядит следующим образом:

List<E> listReference = new concreteListClass<E>();

Буква E, которая означает Element, и есть то "нечто", о чем упоминалось выше. concreteListClass - это инициализированный класс JDK. JDK включает в себя несколько реализаций List<E>, но мы будем использовать ArrayList<E>. Другой способ увидеть обсуждаемый родовой класс – это Class<T>, где Т означает Type (тип). Как правило, E в коде Java означает ссылку на ту или иную коллекцию. А T - на параметризованный класс.

Таким образом, чтобы создать ArrayList , скажем, из элементов java.lang.Integer, нужно сделать следующее:

List<Integer> listOfIntegers = new ArrayList<Integer>();

SimpleList: параметризованный класс

Теперь предположим, что вам нужно создать свой собственный класс SimpleList с тремя методами:

  • add() добавляет элемент в конец списка SimpleList;
  • size() возвращает текущее количество элементов SimpleList;
  • clear() полностью очищает содержимое SimpleList.

В листинге 24 показан синтаксис для параметризации SimpleList.

Листинг 24. Параметризация SimpleList
package com.makotogroup.intro;
import java.util.ArrayList;
import java.util.List;
public class SimpleList<E> {
 private List<E> backingStore;
 public SimpleList() {
 backingStore = new ArrayList<E>();
 }
 public E add(E e) {
 if (backingStore.add(e))
 return e;
 else
 return null;
 }
 public int size() {
 return backingStore.size();
 }
 public void clear() {
 backingStore.clear();
 }
}

SimpleList можно параметризовать с помощью любого подкласса Object. Чтобы создать и использовать список SimpleList, скажем, объектов java.math.BigDecimal, нужно сделать следующее:

public static void main(String[] args) {
 SimpleList<BigDecimal> sl = new SimpleList<BigDecimal>();
 sl.add(BigDecimal.ONE);
 log.info("SimpleList size is : " + sl.size());
 sl.add(BigDecimal.ZERO);
 log.info("SimpleList size is : " + sl.size());
 sl.clear();
 log.info("SimpleList size is : " + sl.size());
}

Вы получите такой результат:

May 5, 2010 6:28:58 PM com.makotogroup.intro.Application main
INFO: SimpleList size is : 1
May 5, 2010 6:28:58 PM com.makotogroup.intro.Application main
INFO: SimpleList size is : 2
May 5, 2010 6:28:58 PM com.makotogroup.intro.Application main
INFO: SimpleList size is : 0

Перечисляемые типы

В JDK 5 добавился новый тип данных языка Java, enum. Не путайте его с java.util.Enumeration. Перечисляемый тип enum - это множество постоянных объектов, связанных с определенным понятием, каждый из которых представляет собой некоторое постоянное значение из этого множества. До ввода перечисляемых типов в язык Java нужно было определять множество постоянных значений для данного понятия (например, пола (Gender)) следующим образом:

public class Person {
 public static final String MALE = "male";
 public static final String FEMALE = "female";
}

Для ссылки на это постоянное значение нужно было написать некоторый код, например:

public void myMethod() {
 //. . .
 String genderMale = Person.MALE;
 //. . .
}

Определение констант типа enum

Использование типа enum делает процесс определения констант гораздо более формальным, а также более мощным. Вот определение enum для пола:

public enum Gender {
 MALE,
 FEMALE
}

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

package com.makotogroup.intro;

public enum Gender {
 MALE("male"),
 FEMALE("female");

 private String displayName;
 private Gender(String displayName) {
 this.displayName = displayName; 
 }

 public String getDisplayName() {
 return this.displayName;
 }
}

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

Перечисляемый тип реализует интерфейс

Предположим, мы определили интерфейс Displayable:

package com.makotogroup.intro;
public interface Displayable {
 public String getDisplayName();
}

Наш перечисляемый тип Gender может реализовать этот интерфейс (как и любой другой enum, необходимый для создания наглядного отображаемого имени), следующим образом:

package com.makotogroup.intro;

public enum Gender implements Displayable {
 MALE("male"),
 FEMALE("female");

 private String displayName;
 private Gender(String displayName) {
 this.displayName = displayName; 
 }
 @Override
 public String getDisplayName() {
 return this.displayName;
 }
}

Подробнее о родовых типах см. в разделе Ресурсы.

Операции ввода/вывода

В этом разделе представлен обзор пакета java.io. Вы научитесь использовать некоторые инструменты для сбора данных из различных источников и манипулирования ими.

Работа с внешними данными

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

Файлы

Из всех источников данных, доступных для Java-приложений, файлы ― наиболее распространенный и часто наиболее удобный. Если Java-приложение должно прочесть файл, нужно использовать потоки (streams), которые преобразуют входящие байты в типы языка Java.

java.io.File - это класс, который определяет ресурс файловой системы и представляет этот ресурс абстрактным образом. Создать объект File очень просто:

File f = new File("temp.txt");
File f2 = new File("/home/steve/testFile.txt");

Конструктор File принимает имя создаваемого файла. Первый вызов создает файл с именем temp.txt в данном каталоге. Второй - файл в определенном месте моей Linux-системы. Конструктору File можно передавать любую строку, если это допустимое имя файла для вашей ОС, независимо от того, существует ли файл, на который оно указывает.

Следующий код определяет, существует ли вновь созданный объект File.

File f2 = new File("/home/steve/testFile.txt");
if (f2.exists()) {
 // Файл существует. Обработать его...
} else {
 // Файл не существует. Создать его...
 f2.createNewFile();
}

java.io.File содержит и другие удобные методы, которые можно использовать для удаления файлов, создания каталогов (путем передачи имени каталога в качестве аргумента в конструктор File), определения, является ли ресурс файлом, каталогом или символической ссылкой, и т.п.

Реальные действия по вводу/выводу Java – это запись в источники и чтение из источников данных, и здесь-то в игру вступают потоки.

Использование потоков для операций ввода/вывода Java

К файлам файловой системы можно обращаться с помощью потоков. На самом низком уровне потоки позволяют программе принимать байты из источника или направлять вывод в место назначения. Некоторые потоки обрабатывают все виды 16-битных символов (типы Reader и Writer). Другие обрабатывают только 8-битные байты (типы InputStream и OutputStream). В рамках этих иерархий существует несколько разновидностей потоков, и все они находятся в пакете java.io. На самом высоком уровне абстракции находятся потоки символов и потоки байтов.

Потоки байтов считывают (InputStream с подклассами) и записывают (OutputStream с подклассами) 8-битные байты. Иными словами, поток байтов можно считать более грубым типом потока. Вот краткое описание двух общих потоков байтов и способов их использования:

  • FileInputStream/FileOutputStream: считывает байты из файла, записывает байты в файл;
  • ByteArrayInputStream/ByteArrayOutputStream: считывает байты из массива в памяти, записывает байты в массив в памяти.

Потоки символов

Потоки символов считывают (Reader с подклассами) и записывают (Writer с подклассами) 16-битные символы. Ниже приведен перечень избранных потоков символов и способы их использования:

  • StringReader/StringWriter: чтение и запись символов в строки и из строк в памяти;
  • InputStreamReader/InputStreamWriter (и подклассы FileReader/FileWriter): образуют мост между потоками байтов и потоками символов. Версии Reader считывают байты из потока и конвертируют их в символы. Версии Writer преобразовывают символы в байты и помещают их в потоки байтов.
  • BufferedReader/BufferedWriter: помещают данные в буфер, считывая или записывая другой поток, что делает операции чтения и записи более эффективными.

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

Чтение из файла

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

  1. создать объект InputStreamReader для файла, из которого нужно считывать.
  2. вызвать метод read() и считывать по одному символу за раз до конца файла.

Пример чтения из файла приведен в листинге 25.

Листинг 25. Пример чтения из файла.
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = new StringBuilder();
try {
 InputStream inputStream = new FileInputStream(new File("input.txt"));
 InputStreamReader reader = new InputStreamReader(inputStream);
 try {
 int c = reader.read();
 while (c != -1) {
 sb.append(c);
 }
 } finally {
 reader.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Запись в файл

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

  1. Создать объект FileOutputStream для файла, в который нужно записать данные.
  2. Вызвать метод write() для записи последовательности символов.

Пример записи файла приведен в листинге 26.

Листинг 26. Запись в файл
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = getStringToWriteSomehow();
try {
 OutputStream outputStream = new FileOutputStream(new File("output.txt"));
 OutputStreamWriter writer = new OutputStreamWriter(outputStream);
 try {
 writer.write(sb.toString());
 } finally {
 writer.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Потоки буферизации

Чтение и запись потоков символов по одному символу за раз не очень эффективны, поэтому в большинстве случаев вместо этого предпочитают использовать ввод/вывод с буферизацией. Код для чтения из файла с использованием буферизированного ввода/вывода выглядит так же, как в листинге 25, за исключением того, что InputStreamReader заключается в BufferedReader, как показано в листинге 27.

Листинг 27. Пример чтения из файла с буферизацией
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = new StringBuilder();
try {
 InputStream inputStream = new FileInputStream(new File("input.txt"));
 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
 try {
 String line = reader.readLine();
 while (line != null) {
 sb.append(line);
 line = reader.readLine();
 }
 } finally {
 reader.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

Запись в файл с использованием буфера ввода/вывода выполняется аналогично: OutputStreamWriter просто заключается в BufferedWriter, как показано в листинге 28.

Листинг 28. Пример записи в файл с буферизацией
Logger log = Logger.getAnonymousLogger();
StringBuilder sb = getStringToWriteSomehow();
try {
 OutputStream outputStream = new FileOutputStream(new File("output.txt"));
 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
 try {
 writer.write(sb.toString());
 } finally {
 writer.close();
 }
} catch (IOException e) {
 log.info("Caught exception while processing file: " + e.getMessage());
}

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

Сериализация Java

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

Что такое сериализация объектов?

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

Существует два основных способа сериализации объекта:

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

java.io.Serializable

Первым шагом к выполнению работы по сериализации является предоставление объектам возможности использовать этот механизм. Каждый объект, который нужно сериализовать, должен реализовать интерфейс java.io.Serializable:

import java.io.Serializable;
public class Person implements Serializable {
// и т.д.
}

Интерфейс Serializable помечает объекты класса Person для среды исполнения как сериализуемые. Каждый подкласс Person также помечается как сериализуемый.

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

Сериализация объекта

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

Предположим, что мы создали и заполнили объект Manager (напомним, что Manager находится в графе наследования Person, который является сериализуемым объектом), а теперь хотим сериализовать этот объект в выходной поток OutputStream, в данном случае, в файл. Этот процесс показан в листинге 29.

Листинг 29. Сериализация объекта
Manager m = new Manager();
m.setEmployeeNumber("0001");

m.setGender(Gender.FEMALE);
m.setAge(29);
m.setHeight(170);
m.setName("Mary D. Boss");
m.setTaxpayerIdentificationNumber("123-45-6789");
log.info("About to write object using serialization... object looks like:");
m.printAudit(log);
try {
 String filename = "Manager-" + m.hashCode() + ".ser";
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
 oos.writeObject(m);
 log.info("Wrote object...");
} catch (Exception e) {
 log.log(Level.SEVERE, "Caught Exception processing object", e);
}

Первым шагом является создание объекта и установка некоторых значений атрибутов. Далее, создаем выходной поток OutputStream, в данном случае это FileOutputStream, а затем вызываем writeObject() для этого потока. writeObject () - это метод, который использует Java-сериализацию для преобразования объекта в поток.

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

Десериализация объекта

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

Листинг 30. Десериализация объекта
Manager m = new Manager();
m.setEmployeeNumber("0001");
m.setGender(Gender.FEMALE);
m.setAge(29);
m.setHeight(170);
m.setName("Mary D. Boss");
m.setTaxpayerIdentificationNumber("123-45-6789");
log.info("About to write object using serialization... object looks like:");
m.printAudit(log);
try {
 String filename = "Manager-" + m.hashCode() + ".ser";
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
 oos.writeObject(m);
 log.info("Wrote object...");

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
 m = (Manager)ois.readObject();
 log.info("Read object using serialization... object looks like:");
 m.printAudit(log);
} catch (Exception e) {
 log.log(Level.SEVERE, "Caught Exception processing object", e);
}

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

Свойство serialVersionUID

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

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

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

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

Чтобы избежать этого, нужно взять в привычку явное объявление serialVersionUID:

import java.io.Serializable;
public class Person implements Serializable {
 private static final long serialVersionUID = 20100515;
// и т.д.
}

Я рекомендую использовать некую схему для номера версии serialVersionUID (в примере я использую текущую дату), причем нужно объявить его как private static final с типом long.

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

В качестве золотого правила, при каждом добавлении или удалении элементов класса (то есть атрибутов и методов) нужно изменять его serialVersionUID. Лучше получить исключение InvalidClassException на другом конце "провода", чем приложение с ошибкой из-за несовместимых изменений класса.

Заключение к Части 2

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

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

Ресурсы

Научиться

  • Оригинал статьи(EN)
  • Страница Java-технологии: официальный сайт Java содержит ссылки на все, что связано с платформой Java, включая спецификацию языка Java и документацию API Java .(EN)
  • Java 6: подробнее о JDK 6 и содержащихся в нем инструментах.
  • Страница Javadoc: о тонкостях использования Javadoc, в том числе о том, как применять инструмент командной строки и как написать свои собственные доклеты, которые позволяют создавать специальные форматы документации.
  • Новое в Java-технологии: сборник ресурсов developerWorks для начинающих Java-разработчиков.
  • 5 вещей, которые вы не знали о ...: серия статей developerWorks, которая знакомит с менее известными (но часто полезными) рекомендациями и сведениями по Java-программированию.
  • Try to catch me: Does exception handling impair performance?(Tony Sintes, JavaWorld, июль 2001 г.): статья, содержащая вводную информацию по обработке исключений в Java-программах.(EN)
  • Exception: Don't get thrown for a loss (Tony Sintes, JavaWorld, февраль 2002 г.): почувствуйте разницу между контролируемыми исключениями и исключениями времени выполнения
  • Regular expressions simplify pattern-matching code (Jeff Friesen, JavaWorld.com, февраль 2003 г.): расширенное введение в регулярные выражения.(EN)
  • Diagnosing Java code: Java generics without the pain, Part 1 (Eric Allen, DeveloperWorks, февраль 2003 г.): первая часть серии статей, посвященных введению в синтаксис родовых типов Java
  • Refactoring: Improving the Design of Existing Code (Martin Fowler et al., издательство Addison-Wesley, 1999 г.): превосходный учебник по написанию четкого, удобного в обслуживании кода.(EN)
  • Design patterns: Elements of reusable object-oriented software (Erich Gammaet др., издательство Addison-Wesley, 1994 г.): подробнее о модели Factory, одной из 23 моделей проектирования, которые определяют современный подход к разработке программного обеспечения.

Получить продукты и технологии

  • JDK 6: загрузите JDK 6 от Sun (Oracle).
  • Eclipse: загрузите IDE Eclipse для Java-разработчиков.
  • Комплекты разработчика IBM : IBM предоставляет ряд комплектов Java-разработчика для популярных платформ.(EN)

Вверх