- •Глава 1 введение в объекты 17
- •Глава 2 всё является объектом 44
- •Глава 3 операторы 65
- •Глава 4 управляющие конструкции 97
- •Глава 5 инициализация и завершение 112
- •Глава 6 управление доступом 146
- •Глава 7 повторное использование классов 161
- •Глава 8 полиморфизм 190
- •Глава 9 интерфейсы 212
- •Глава 10 внутренние классы 236
- •Глава 11 контейнеры и хранение объектов 269
- •Глава 12 обработка ошибок и исключений 302
- •Глава 13 информация о типах 342
- •Глава 14 параметризация 386
- •Глава 15 массивы 442
- •Глава 16 система ввода/вывода java 472
- •Глава 17 параллельное выполнение 546
- •Глава 1 введение в объекты
- •Глава 2 всё является объектом
- •Глава 3 операторы
- •Глава 4 управляющие конструкции
- •Глава 5 инициализация и завершение
- •1. Ваши объекты могут быть и не переданы сборщику мусора.
- •2. Сборка мусора не является удалением.
- •3. Процесс сборки мусора относится только к памяти.
- •Глава 6 управление доступом
- •Глава 7 повторное использование классов
- •Глава 8 полиморфизм
- •Глава 9 интерфейсы
- •InterfaceCollіsion.Java.23 f() в c не может реализовать f() в i1; попытка использовать несовместимые возвращаемые типы обнаружено: int требуется- void
- •InterfaceCollіsi on java:24- интерфейсы і1 и i1 несовместимы; оба определяют f(), но с различными возвращаемыми типами
- •Глава 10 внутренние классы
- •Глава 11 контейнеры и хранение объектов
- •Глава 12 обработка ошибок и исключений
- •Глава 13 информация о типах
- •Instanceof и сравнение Class
- •Глава 14 параметризация
- •Глава 15 массивы
- •Глава 16 система ввода/вывода java
- •Глава 17 параллельное выполнение
Instanceof и сравнение Class
При получении информации о типе объекта важно различать действие любой формы оператора instanceof (будь это сам оператор instanceof или метод isInstance() - они дают одинаковые результаты) и прямого сравнения объектов Class. Вот пример, который показывает, в чем их различия:
//: typeinfo/FamilyVsExactType.java
// The difference between instanceof and class
package typeinfo;
import static net.mindview.util.Print.*;
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof Base " + (x instanceof Base));
print("x instanceof Derived "+ (x instanceof Derived));
print("Base.isInstance(x) "+ Base.class.isInstance(x));
print("Derived.isInstance(x) " +
Derived.class.isInstance(x));
print("x.getClass() == Base.class " +
(x.getClass() == Base.class));
print("x.getClass() == Derived.class " +
(x.getClass() == Derived.class));
print("x.getClass().equals(Base.class)) "+
(x.getClass().equals(Base.class)));
print("x.getClass().equals(Derived.class)) " +
(x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
<spoiler text="Output:">
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
</spoiler> Метод test() осуществляет проверку типов полученного объекта, используя для этого обе формы оператора instanceof. Затем он получает ссылку на объект Class и использует операцию сравнения ссылок == и метод equals(), чтобы проверить объекты Class на эквивалентность. Пример доказывает справедливость утверждения о том, что действие оператора instanceof и метода islnstance() одинаково. Совпадают и результаты работы операции сравнения == и метода equals(). Но сами тесты приводят к разным заключениям. В соответствии с концепцией типа instanceof дает ответ на вопрос: «Объект принадлежит этому классу или производному от него?» С другой стороны, сравнение объектов Class оператором == не затрагивает наследования — либо тип точно совпадает, либо нет.
Рефлексия: динамическая информация о классе
Если вы не знаете точный тип объекта, RTTI сообщит вам его. Однако в этом случае существуют ограничения: тип должен быть известен еще во время компиляции программы, иначе определить его с помощью RTTI и сделать с этой информацией что-то полезное будет невозможно. Другими словами, компилятор должен располагать информацией обо всех классах, к которым вы затем хотели бы применить динамическое определение типов (RTTI). Сначала кажется, что это ограничение не столь существенно, но предположим, что у вас появилась ссылка на объект, который не находится в пространстве вашей программы. Более того, класс этого объекта недоступен во время ее компиляции. Например, вы получили последовательность байтов с диска или из сетевого соединения, и вам сказали, что эта последовательность представляет некоторый класс. Но компилятор ничего не знал об этом классе, когда обрабатывал вашу программу, как же его можно использовать? В традиционных средах программирования такая задача показалась бы далекой от реальности. Однако границы мира программирования все больше расширяются и мы все чаще встречаемся с такими ситуациями. Во-первых, такие возможности требуются для компонентного программирования, которое служит основой для систем быстрой разработки приложений (Rapid Application Development, RAD). Это визуальный подход для создания программ (экран представлен в виде «формы»), где значки, представляющие визуальные компоненты, перетаскиваются на форму. Затем происходит настройка этих компонентов, они устанавливаются в некоторое состояние во время работы программы. Чтобы изменить состояние компонентов, необходимо некоторым образом создавать их экземпляры, просматривать их содержимое, считывать и записывать внутренние значения. Вдобавок компоненты с поддержкой событий графического интерфейса должны как-то рассказать о них, чтобы система быстрой разработки приложений помогла программисту реализовать поддержку этих событий. Механизм рефлексии предоставляет средства для получения информации о доступных методах и их именах. Такое компонентное программирование поддерживается и в Java, с помощью технологии JavaBeans. Другая важная предпосылка поддержки динамической информации о классе — предоставление возможности создавать и использовать объекты на удаленных платформах. Этот механизм, называемый удаленным вызовом методов (Remote Method Invocation, RMI), позволяет программе на Java распределять свои объекты по нескольким машинам. Необходимость в удаленном вызове методов возникает по разным причинам: например, при выполнении задачи с интенсивными вычислениями можно сбалансировать нагрузку по доступным компьютерам. Иногда код, выполняющий определенные операции, размещается на одной машине, чтобы она стала общим хранилищем этих операций и любые изменения кода на такой машине автоматически распространялись на всех клиентов этого кода. (Интересный поворот — компьютер существует исключительно для того, чтобы упростить внесение изменений в программное обеспечение!) Ко всему прочему распределенное программирование также поддерживает удаленное специализированное оборудование, которое эффективно выполняет некоторые задачи — например, обращение матриц, — которые при решении их на локальной машине могут потребовать слишком много времени и ресурсов. Класс Class (уже описанный в этой главе) поддерживает концепцию рефлексии (reflection), для которой существует дополнительная библиотека java.lang.reflect, состоящая из классов Field, Method и Constructor (каждый реализует интерфейс Member). Объекты этих классов создаются JVM, чтобы представлять соответствующие члены неизвестного класса. Объекты Constructor используются для создания новых объектов класса, методы get() и set() — для чтения и записи значений полей класса, представленных объектами Field, метод invoke() — для вызова метода, представленного объектом Method. Вдобавок в классе Class имеются удобные методы getFields(), getMethods() и getConstructors(), которые возвращают массивы таких объектов, как поля класса, его методы и конструкторы. (За подробной информацией обращайтесь к описанию класса Class в электронной документации JDK.) Таким образом, информация о неизвестном объекте становится доступной прямо во время выполнения программы, а потребность в ее получении ко времени компиляции программы отпадает. Важно понимать, что в механизме рефлексии нет ничего сверхъестественного. Когда вы используете рефлексию для работы с объектом неизвестного типа, виртуальная машина JVM рассматривает его и видит, что он принадлежит определенному классу (это делает и обычное RTTI), но, перед тем как проводить с ним некоторые действия, необходимо загрузить соответствующий объект Class. Таким образом, файл .class для класса этого объекта должен быть доступен JVM либо в сети, либо в локальной системе. Таким образом, истинное различие между традиционным RTTI и рефлексией состоит в том, что при использовании RTTI файл .class открывается и анализируется компилятором. Другими словами, вы можете вызывать методы объекта «нормальным» способом. При использовании рефлексии файл .class во время компиляции недоступен; он открывается и обрабатывается системой выполнения.
Извлечение информации о методах класса
Рефлексия редко используется напрямую; она существует в языке в основном для поддержки других возможностей, таких как сериализация объектов и компоненты JavaBeans. Однако существуют ситуации, в которых динамическая информация о классе просто незаменима. Для примера возьмем программу, выводящую на экран список методов некоторого класса. При просмотре исходного кода класса или его документации будут видны только те методы, которые были определены или переопределены именно в текущем классе. Но в классе может быть еще множество методов, доступных из его базовых классов. Искать их и сложно, и долго[28]. К счастью, рефлексия позволяет написать простой инструмент, выводящий полную информацию о полном интерфейсе класса. Вот как он работает:
//: typeinfo/ShowMethods.java
// Использование рефлексии для вывода полного списка методов
// класс, в том числе и определенных в базовом классе.
// {Args: ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
import static net.mindview.util.Print.*;
public class ShowMethods {
private static String usage = "usage:\n"
+ "ShowMethods qualified.class.name\n"
+ "To show all methods in class or:\n"
+ "ShowMethods qualified.class.name word\n"
+ "To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if(args.length < 1) {
print(usage);
System.exit(0);
}
int lines = 0;
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if(args.length == 1) {
for(Method method : methods)
print(p.matcher(method.toString()).replaceAll(""));
for(Constructor ctor : ctors)
print(p.matcher(ctor.toString()).replaceAll(""));
lines = methods.length + ctors.length;
} else {
for(Method method : methods)
if (method.toString().indexOf(args[1]) != -1) {
print(p.matcher(method.toString()).replaceAll(""));
lines++;
}
for(Constructor ctor : ctors)
if(ctor.toString().indexOf(args[1]) != -1) {
print(p.matcher(ctor.toString()).replaceAll(""));
lines++;
}
}
}catch(ClassNotFoundException e) {print("No such class: " + e);}
}
}
<spoiler text="Output:">
public static void main(String[]) public native int hashCodeO public final native Class getClass()
public final void wait(long.int) throws InterruptedException public final void wait() throws
InterruptedException public final native void wait(long) throws InterruptedException public
boolean equals(Object) public String toString() public final native void notifyО public final
native void notifyAll() public ShowMethods()
</spoiler> Методы класса Class.getMethods() и getConstructors() возвращают массивы объектов Method и Constructor, которые представляют методы и конструкторы класса. В каждом из этих классов есть методы для получения и анализа имен, аргументов и возвращаемых значений представляемых методов и конструкторов. Впрочем, также можно использовать простой метод toString(), как и сделано здесь, чтобы получить строку с полным именем метода. Остальная часть кода разбирает командную строку и определяет, подходит ли определенное выражение образцу для поиска (с использованием indexOf()), а после выделяет описатели имен классов. Результат, полученный от Class.forName(), не может быть известен во время компиляции, поэтому вся информация о сигнатуре методов становится доступной во время выполнения. Если вы тщательно изучите документацию по рефлексии из JDK, то увидите, что рефлексия позволяет установить необходимые аргументы и вызвать метод объекта, «абсолютно неизвестного» во время компиляции программы (чуть позже будут приведены соответствующие примеры). Скорее всего, вам эти возможности никогда не понадобятся, но сам факт их существования интересен.
Приведенный выше результат был получен из командной строки
java ShowMethods ShowMethods
На экран выводится открытый (public) конструктор по умолчанию, хотя в тексте программы такой конструктор не определяется. Тот конструктор, что имеется теперь в классе, автоматически сгенерирован компилятором. Если вы после этого сделаете класс ShowMethods не открытым (удалите из его определения спецификатор доступа public, то есть предоставите ему доступ в пределах пакета), сгенерированный компилятором конструктор исчезнет из списка методов. Сгенерированный конструктор имеет тот же уровень доступа, что и его класс.
Также интересно запустить программу в виде
java ShowMethods java.lang.String
с передачей дополнительного параметра char, int, String и т. п. Эта программа сэкономит вам немало времени при программировании, когда вы будете мучительно вспоминать, есть ли у этого класса определенный метод, если вам потребуется узнать, имеются ли у некоторого класса методы, возвращающие объекты Color, и т. д.
Динамические посредники
«Посредник» (proxy) принадлежит к числу основных паттернов проектирования. Он представляет собой объект, который подставляется на место «настоящего» объекта для расширения или модификации его операций. Приведу тривиальный пример, показывающий структуру посредника:
//. typeinfo/SimpleProxyDemo.java
package typeinfo;
import static.net.mindview.util.Print.*;
interface Interface {
void doSomething();
void somethingElse(String arg);
}
class RealObject implements Interface {
@Override
public void doSomething() {print("doSomething"); }
@Override
public void somethingElse(String arg) { print("somethingElse " + arg);}
}
class SimpleProxy implements Interface {
private Interface proxied;
public SimpleProxy(Interface proxied) { this.proxied = proxied;}
@Override
public void doSomething() {
print("SimpleProxy doSomething");
proxied.doSomething();
}
@Override
public void somethingElse(String arg) {
print("SimpleProxy somethingElse " + arg);
proxied.somethingElse(arg);
}
}
class SimpleProxyDemo {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}
public static void main(String[] args) {
consumer(new RealObject());
consumer(new SimpleProxy(new RealObject()));
}
}
<spoiler text="Output:">
doSomething somethingElse bonobo SimpleProxy doSomething doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
</spoiler> Поскольку consumer() получает Interface, он не знает, что ему передается — «настоящий» объект (RealObject) или посредник (Proxy), потому что оба типа реализуют Interface. Объект Proxy, находящийся между клиентом и «настоящим» объектом, выполняет операции, а затем вызывает идентичные методы RealObject. Посредник пригодится в любой ситуации, когда требуется отделить дополнительные операции от «настоящего» объекта, и особенно когда нужно легко переключаться из режима использования дополнительных операций в режим отказа от них (и наоборот — главной целью паттернов является инкапсуляция изменений, поэтому для оправдания их применения что-то должно изменяться). Допустим, вы хотите отслеживать вызовы методов RealObject, измерять затраты на эти вызовы, и т. д. Такой код не должен встраиваться в приложение, а посредник позволит легко добавить или убрать его по мере необходимости. Динамические посредники Java развивают концепцию посредника — и объект посредника создается динамически, и обработка вызовов опосредованных методов тоже осуществляется динамически. Все вызовы, обращенные к динамическому посреднику, перенаправляются одному обработчику, который определяет, что это за вызов и как с ним следует поступить. Вот как выглядит примерSimpleProxyDemo.java, переписанный для динамического посредника:
// typeinfo/SimpleDynamicProxy.java
package typeinfo;
import java.lang.reflect.*;
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println ("**** proxy. " + proxy.getClass()
+ ", method- " + method + ", args " + args);
if(args != null)
for(Object arg : args)
System.out.println(" " + arg);
return method.invoke(proxied, args);
}
}
class SimpleDynamicProxy {
public static void consumer(Interface іface) {
іface.doSomething();
іface.somethingElse("bonobo");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
// Вставляєм посредника и вызываем снова:
Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{Interface.class},
new DynamicProxyHandler(real));
consumer(proxy);
}
}
<spoiler text="Output:">
**** proxy: class SProxy(). method: public abstract void Interface.doSomething(), args: null
doSomething
**** proxy: class SProxy(). method: public abstract void
Interface.somethingElse(java.1ang.String),
args: [Ljava.1ang.Object.@42e816
bonobo
somethingElse bonobo
</spoiler> Динамический посредник создается вызовом статического метода Proxy.newProxyInstance(), которому должен передаваться загрузчик класса, список интерфейсов, которые должны реализовываться посредником (а не классов или абстрактных классов!), а также реализация интерфейса InvocationHandler. Динамический посредник перенаправляет все вызовы обработчику, поэтому конструктор обработчика обычно получает ссылку на «настоящий» объект для перенаправления ему запросов. Метод invoke() получает объект посредника на случай, если ему понадобится определить, откуда поступил запрос — впрочем, обычно это несущественно. Будьте внимательны при вызове методов посредника из invoke(), потому что вызовы через интерфейс перенаправляются через посредника. В общем случае вы выполняете опосредованную операцию, а затем используете Method.invoke() для перенаправления запроса опосредованному объекту с передачей необходимых аргументов. При этом некоторые вызовы методов могут отфильтровываться, а другие — проходить:
//: typeinfo/SelectingMethods.java
// Looking for particular methods in a dynamic proxy.
import java.lang.reflect.*;
import static net.mindview.util.Print.*;
class MethodSelector implements InvocationHandler {
private Object proxied;
public MethodSelector(Object proxied) {
this.proxied = proxied;
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(method.getName().equals("interesting"))
print("Proxy detected the interesting method");
return method.invoke(proxied, args);
}
}
interface SomeMethods {
void boring1();
void boring2();
void interesting(String arg);
void boring3();
}
class Implementation implements SomeMethods {
public void boring1() { print("boring1"); }
public void boring2() { print("boring2"); }
public void interesting(String arg) {
print("interesting " + arg);
}
public void boring3() { print("boring3"); }
}
class SelectingMethods {
public static void main(String[] args) {
SomeMethods proxy= (SomeMethods)Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
new Class[]{ SomeMethods.class },
new MethodSelector(new Implementation()));
proxy.boring1();
proxy.boring2();
proxy.interesting("bonobo");
proxy.boring3();
}
}
<spoiler text="Output:">
boring1
boring2
Proxy detected the interesting method
interesting bonobo
boring3
</spoiler> В данном случае мы просто проверяем имена методов, но с таким же успехом можно анализировать другие аспекты сигнатуры и даже значения аргументов. Вряд ли вам придется каждый день пользоваться динамическими посредниками, но они хорошо подходят для решения многих разновидностей задач.
Объекты с неопределенным состоянием
Если использовать для обозначения неопределенного состояния (то есть отсутствия) объекта встроенное значение null, то при каждом использовании ссылки придется проверять, не равна ли она null. Это быстро утомляет, а код получается излишне громоздким. Проблема заключается в том, что null не имеет собственного поведения, кроме выдачи NullPointerException при попытке выполнения с ним какой-либо операции. Иногда бывает полезно ввести понятие объекта с неопределенным состоянием, который принимает сообщения, но возвращает значение, свидетельствующее об отсутствии «настоящего» объекта. Таким образом, вы можете считать, что все объекты действительны, и вам не придется тратить время на проверки null (и читать полученный код). Было бы интересно представить себе язык программирования, автоматически создающий объекты с неопределенным состоянием, но на практике они применяются не так уж часто — иногда проверки null оказывается достаточно, иногда можно уверенно считать, что значение null вам не попадется, а иногда даже обработка аномальных ситуаций через NullPointerException является допустимой. Наибольшую пользу объекты с неопределенным состоянием приносят «вблизи от данных», представляя сущности в пространстве задачи. Простой пример: во многих системах имеется классPerson, а в коде возникают ситуации, когда объект не представляет конкретную личность (или, по крайней мере, информация о ней недоступна); при традиционном подходе вам следовало бы проверить ссылку null. Также можно воспользоваться объектом с неопределенным состоянием, но, даже несмотря на то, что такой объект будет отвечать на все сообщения, на которые отвечает «настоящий» объект, все равно потребуется способ проверки его на «определенность». Проще всего определить для этого специальный интерфейс:
//. net/mindview/uti1/Null.java
package net.mindview.util;
public interface Null {}
Это позволяет instanceof обнаруживать объекты с неопределенным состоянием и, что еще важнее, не требует включения метода isNull() во все классы (в конце концов, это фактически будет другим способом выполнения RTTI — так почему бы сразу не воспользоваться встроенными средствами?):
// typeinfo/Person.java
// Класс с неопределенным состоянием объекта
package typeinfo;
import net.mindview.util.*;
class Person {
public final String first;
public final String last;
public final String address; // И t д.
public static final Person NULL = new NullPerson();
public Person(String first, String last, String address){
this.first = first;
this.last = last;
this.address = address;
}
@Override
public String toString() {
return "Person: " + first + " " + last + " " + address;
}
public static class NullPerson extends Person implements Null {
private NullPerson() {
super("None", "None", "None");
}
@Override
public String toString() { return "NullPerson"; }
}
}
В общем случае объект с неопределенным состоянием является синглетным, поэтому он создается как экземпляр static final. Это возможно благодаря тому, что объект Person неизменяем — значения задаются в конструкторе, а затем читаются, но не могут изменяться (поскольку поля String по своей природе неизменяемы). Если вы захотите изменить NullPerson, его придется заменить новым объектом Person. Обратите внимание: для обнаружения обобщенной поддержки Null или более конкретного типа NullPerson можно использоватьinstanceof, но при синглетной архитектуре можно воспользоваться просто equals() или даже == для сравнения с Person.NULL. Представьте, что вы собираетесь открыть новое предприятие, но, пока вакансии еще не заполнены, в каждой должности Position можно временно хранить «заполнитель» — объект Person с неопределенным состоянием:
//• typeinfo/Position.java
package typeinfo;
class Position {
private String title;
private Person person;
public Position(String jobTitle, Person employee) {
title = jobTitle;
person = employee;
if(person == null)
person = Person.NULL;
}
public Position(String jobTitle) {
title = jobTitle;
person = Person.NULL;
}
public String getTitle() { return title; }
public void setTitle(String newTitle) { title = newTitle;}
public Person getPerson() { return person; }
public void setPerson(Person newPerson) {
person = newPerson;
if(person == null)
person = Person.NULL;
}
@Override
public String toString() {
return "Position: " + title + " " + person;
}
}
Превращать Position в объект с неопределенным состоянием не обязательно, потому что существование Person.NULL подразумевает неопределенность Position (возможно, позднее выяснится, что явная поддержка неопределенного состояния для Position нужна, и вы добавите ее, но в соответствии с одним из канонов экстремального программирования в начальный проект следует включить «простейшее решение, которое будет работать», и включать новые функции лишь по мере возникновения реальной необходимости). Теперь классStaff может проверять объекты с неопределенным состоянием при заполнении вакансий:
//: typeinfo/Staff.java
import java.util.*;
public class Staff extends ArrayList<Position> {
public void add(String title, Person person) {
add(new Position(title, person));
}
public void add(String... titles) {
for(String title : titles)
add(new Position(title));
}
public Staff(String... titles) { add(titles); }
public boolean positionAvailable(String title) {
for(Position position : this)
if(position.getTitle().equals(title) &&
position.getPerson() == Person.NULL)
return true;
return false;
}
public void fillPosition(String title, Person hire) {
for(Position position : this)
if(position.getTitle().equals(title) &&
position.getPerson() == Person.NULL) {
position.setPerson(hire);
return;
}
throw new RuntimeException(
"Position " + title + " not available");
}
public static void main(String[] args) {
Staff staff = new Staff("President", "CTO",
"Marketing Manager", "Product Manager",
"Project Lead", "Software Engineer",
"Software Engineer", "Software Engineer",
"Software Engineer", "Test Engineer",
"Technical Writer");
staff.fillPosition("President",
new Person("Me", "Last", "The Top, Lonely At"));
staff.fillPosition("Project Lead",
new Person("Janet", "Planner", "The Burbs"));
if(staff.positionAvailable("Software Engineer"))
staff.fillPosition("Software Engineer",
new Person("Bob", "Coder", "Bright Light City"));
System.out.println(staff);
}
}
<spoiler text="Output:">
[Position: President Person: Me Last The Top. Lonely At, Position. СТО NullPerson, Position:
Marketing Manager NullPerson. Position: Product Manager NullPerson, Position. Project Lead
Person: Janet Planner The Burbs. Position: Software Engineer Person: Bob Coder Bright Light City,
Position: Software Engineer NullPerson, Position: Software Engineer NullPerson. Position-
Software Engineer NullPerson. Position. Test Engineer NullPerson. Position: Technical Writer
NullPerson]
</spoiler> Обратите внимание: в некоторых местах нам по-прежнему приходится проверять объекты на определенное состояние, что принципиально не отличается от проверки null, но в других местах, скажем, при преобразованиях toString(), лишние проверки не нужны; мы просто считаем, что ссылка на объект действительна.
Если вместо конкретных классов используются интерфейсы, для автоматического создания объектов с неопределенным состоянием можно воспользоваться динамическим посредником. Допустим, имеется интерфейс Robot, определяющий имя и модель робота, а также список List<Operation>, определяющий, какие операции выполняет робот. Операция состоит из описания и команды:
//: typeinfo/Operation.java
public interface Operation {
String description();
void command();
}
Чтобы воспользоваться услугами робота, следует вызвать метод operations():
//: typeinfo/Robot.java
import java.util.*;
import net.mindview.util.*;
public interface Robot {
String name();
String model();
List<Operation> operations();
class Test {
public static void test(Robot r) {
if(r instanceof Null)
System.out.println("[Null Robot]");
System.out.println("Название: " + r.name());
System.out.println("Модель: " + r.model());
for(Operation operation : r.operations()) {
System.out.println(operation.description());
operation.command();
}
}
}
}
При этом используется вложенный класс, выполняющий проверку. Теперь мы можем создать робота для уборки снега:
//: typeinfo/SnowRemovalRobot.java
import java.util.*;
public class SnowRemovalRobot implements Robot {
private String name;
public SnowRemovalRobot(String name) {this.name = name;}
public String name() { return name; }
public String model() { return "SnowBot Series 11"; }
public List<Operation> operations() {
return Arrays.asList(
new Operation() {
public String description() {
return name + " может убирать снег";
}
public void command() {
System.out.println(name + " убирает снег");
}
},
new Operation() {
public String description() {
return name + " может колоть лед";
}
public void command() {
System.out.println(name + " колет лед");
}
},
new Operation() {
public String description() {
return name + " может чистить крышу";
}
public void command() {
System.out.println(name + " чистит крышу");
}
}
);
}
public static void main(String[] args) {
Robot.Test.test(new SnowRemovalRobot("Slusher"));
}
}
<spoiler text="Output:">
Название: Slusher
Модель: SnowBot Series 11
Slusher может убирать снег
Slusher убирает снег
Slusher может колоть лед
Slusher колет лед
Slusher может чистить крышу
Slusher чистит крышу
</spoiler> Предполагается, что существуют разные типы роботов, и для каждого типа Robot объект с неопределенным состоянием должен делать что-то особенное — в нашем примере выдавать информацию о конкетном типе Robot, представленном объектом. Эта информация перехватывается динамическим посредником:
//: typeinfo/NullRobot.java
// Использование динамического посредника для создания
// объекта с неопределенным состоянием
import java.lang.reflect.*;
import java.util.*;
import net.mindview.util.*;
class NullRobotProxyHandler implements InvocationHandler {
private String nullName;
private Robot proxied = new NRobot();
NullRobotProxyHandler(Class<? extends Robot> type) {
nullName = type.getSimpleName() + " NullRobot";
}
private class NRobot implements Null, Robot {
public String name() { return nullName; }
public String model() { return nullName; }
public List<Operation> operations() {
return Collections.emptyList();
}
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return method.invoke(proxied, args);
}
}
public class NullRobot {
public static Robot
newNullRobot(Class<? extends Robot> type) {
return (Robot)Proxy.newProxyInstance(
NullRobot.class.getClassLoader(),
new Class[]{ Null.class, Robot.class },
new NullRobotProxyHandler(type));
}
public static void main(String[] args) {
Robot[] bots = {
new SnowRemovalRobot("SnowBee"),
newNullRobot(SnowRemovalRobot.class)
};
for(Robot bot : bots)
Robot.Test.test(bot);
}
}
<spoiler text="Output:">
Название: SnowBee Модель:
SnowBot Series 11
SnowBee может убирать снег
SnowBee убирает снег
SnowBee может колоть лед
SnowBee колет лед
SnowBee может чистить крышу
SnowBee чистит крышу
[Null Robot]
Название: SnowRemova1 Robot NullRobot
Модель: SnowRemovalRobot NullRobot
</spoiler> Каждый раз, когда вам требуется объект Robot с неопределенным состоянием, вы вызываете newNullRobot() и передаете тип Robot, для которого создается посредник. Посредник выполняет требования о поддержке интерфейсов Robot и Null, а также предоставляет имя опосредованного типа.
Интерфейсы и информация о типах
Одной из важных целей ключевого слова interface является изоляция компонентов и сокращение привязок. Использование интерфейсов вроде бы позволяет добиться этой цели, однако RTTI позволяет обойти ограничения — интерфейсы не обеспечивают стопроцентной изоляции. Начнем следующий пример с интерфейса:
//: typeinfo/interfacea/A.java
package typeinfo.interfaces;
public interface A {
void f();
}
Затем интерфейс реализуется, и выясняется, что можно «в обход» добраться до фактического типа реализации:
//: typeinfo/InterfaceViolation.java
// Интерфейс можно обойти
import typeinfo.interfacea.*;
class B implements A {
public void f() {}
public void g() {}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // // Ошибка компиляции
System.out.println(a.getClass().getName());
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
}
<spoiler text="Output:">
В
</spoiler> Используя RTTI, мы выясняем, что объект а реализован в форме В. Преобразование к типу В позволяет вызвать метод, не входящий в интерфейс А. Все это абсолютно законно и допустимо, но, скорее всего, вы предпочли бы оградить клиентских программистов от подобных выходок. Казалось бы, ключевое слово interaface должно защищать вас, но на самом деле этого не происходит, а факт использования В для реализации А становится известен любому желающему. Одно из возможных решений: просто скажите программистам, что если они будут использовать фактический класс вместо интерфейса, то пускай сами разбираются со всеми возникающими проблемами. Вероятно, во многих случаях этого достаточно, но если «вероятно» вас не устраивает — можно применить более жесткие меры. Проще всего установить для реализации пакетный уровень доступа, чтобы она оставалась невидимой для клиентов за пределами пакета:
//: typeinfo/packageaccess/HiddenC.java
package typeinfo.packageaccess;
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public class HiddenC {
public static A makeA() { return new C(); }
}
Единственная открытая (public) часть пакета, HiddenC, выдает интерфейс А при вызове. Интересно отметить, что, даже если makeA() будет возвращать С, за пределами пакета все равно удастся использовать только А, потому что имя С недоступно. Попытка нисходящего преобразования к С тоже завершается неудачей:
//: typeinfo/HiddenImplementation.java
// Пакетный доступ тоже можно обойти
import typeinfo.interfacea.*;
import typeinfo.packageaccess.*;
import java.lang.reflect.*;
public class HiddenImplementation {
public static void main(String[] args) throws Exception {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
// Ошибка компиляции, символическое имя 'С' не найдено
/* if(a instanceof C) {
C c = (C)a;
c.g();
} */
// Однако рефлексия позволяет вызвать g():
callHiddenMethod(a, "g");
// ... И даже еще менее доступные методы!
callHiddenMethod(a, "u");
callHiddenMethod(a, "v");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object a, String methodName)
throws Exception {
Method g = a.getClass().getDeclaredMethod(methodName);
g.setAccessible(true);
g.invoke(a);
}
}
<spoiler text="Output:">
public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()
</spoiler> Как видите, рефлексия позволяет вызвать все методы, даже приватные! Зная имя метода, можно вызвать setAccessible(true) для объекта Method, чтобы сделать возможным его вызов, как видно из реализации callHiddenMethod(). Можно подумать, что проблема решается распространением только откомпилированного кода, но и это не так. Достаточно запустить javap — декомпилятор, входящий в JDK. Командная строка выглядит так:
javap -private С
Флаг -private означает, что при выводе должны отображаться все члены, даже приватные. Таким образом, любой желающий сможет получить имена и сигнатуры приватных методов и вызвать их. А если реализовать интерфейс в виде приватного внутреннего класса? Вот как это выглядит:
//: typeinfo/InnerImplementation.java
// Приватные внутренние классы не скрываются от рефлексии
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class InnerA {
private static class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public static A makeA() { return new C(); }
}
public class InnerImplementation {
public static void main(String[] args) throws Exception {
A a = InnerA.makeA();
a.f();
System.out.println(a.getClass().getName());
// Reflection still gets into the private class:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}
<spoiler text="Output:">
public C.f()
InnerA$C
public C.g()
package C.u()
protected C.v()
private C.w()
</spoiler> He помогло. Как насчет анонимного класса?
//: typeinfo/AnonymousImplementation.java
// Анонимные внутренние классы тоже не скрыты от рефлексии
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class AnonymousA {
public static A makeA() {
return new A() {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
};
}
}
public class AnonymousImplementation {
public static void main(String[] args) throws Exception {
A a = AnonymousA.makeA();
a.f();
System.out.println(a.getClass().getName());
// Reflection still gets into the anonymous class:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}
<spoiler text="Output:">
public C.f()
AnonymousA$1
public C.g()
package C.u()
protected C.v()
private C.w()
</spoiler> Похоже, не существует никакого способа предотвратить обращение и вызов методов с уровнем доступа, отличным от public, посредством рефлексии. Сказанное относится и к полям данных, даже к приватным:
//: typeinfo/ModifyingPrivateFields.java
import java.lang.reflect.*;
class WithPrivateFinalField {
private int i = 1;
private final String s = "I'm totally safe";
private String s2 = "Am I safe?";
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
}
}
public class ModifyingPrivateFields {
public static void main(String[] args) throws Exception {
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("i");
f.setAccessible(true);
System.out.println("f.getInt(pf): " + f.getInt(pf));
f.setInt(pf, 47);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
f = pf.getClass().getDeclaredField("s2");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
}
}
<spoiler text="Output:">
i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
</spoiler> Впрочем, final-поля защищены от изменений. Система времени выполнения спокойно воспринимает любые попытки их изменения, но при этом ничего не происходит. В действительности все эти нарушения уровня доступа не так уж страшны. Если кто-то захочет вызывать методы, которым вы назначили приватный или пакетный доступ (тем самым ясно показывая, что вызывать их не следует), вряд ли он станет жаловаться на то, что вы изменили некоторые аспекты этих методов. С другой стороны, «черный ход» к внутреннему устройству класса позволяет решить некоторые проблемы, нерешаемые другими средствами, и в общем случае преимущества рефлексии неоспоримы.
Резюме
Динамическое определение типов (RTTI) позволяет вам получить информацию о точном типе объекта тогда, когда у вас для него имеется лишь ссылка базового типа. Таким образом, оно открывает широкие возможности для злоупотреблений со стороны новичков, которые еще не поняли и не успели оценить всю мощь полиморфизма. У многих людей, ранее работавших с процедурными языками, возникает сильное желание разбить свою программу на множество конструкций switch при помощи RTTI. Однако при этом они лишаются всех преимуществ полиморфизма, относящихся к разработке программы в целом и ее дальнейшей поддержке. В Java рекомендуется использовать именно полиморфные методы, а к услугам RTTI следует прибегать только в крайнем случае.
Впрочем, при использовании полиморфных методов требуется полный контроль над базовым классом, поскольку в некоторой точке программы, после наследования очередного класса, вы можете обнаружить, что базовый класс не содержит нужного вам метода, и тогда RTTIвас выручит: при наследовании вы расширяете интерфейс класса, добавляя в него новые методы. Особенно верно это при использовании в качестве базовых классов библиотек, которые вы не можете изменить. Далее в своем коде в подходящий момент вы обнаруживаете новый тип и вызываете для него нужный метод. Такой подход не противоречит основам полиморфизма и расширяемости программы, так как добавление в программу нового типа не требует изменения бесчисленного множества конструкций switch. Но чтобы извлечь пользу из дополнительной функциональности нового класса, придется использовать RTTI.
Включение некоторого метода в базовый класс будет выгодно только одному производному классу, который действительно реализует его, но все остальные производные классы будут вынуждены использовать для этого метода какую-либо бесполезную «заглушку». Интерфейс базового класса «размывается» и раздражает тех, кому приходится переопределять ненужные абстрактные методы при наследовании от базового класса. Например, рассмотрим иерархию классов, представляющих музыкальные инструменты. Предположим, что вы хотите прочистить мундштуки духовых инструментов своего оркестра. Конечно, можно поместить в базовый класс Instrument (общее представление музыкального инструмента) еще один метод clearSpitValve() (прочистка мундштуков), но тогда получится, что и у синтезатора, и у барабана есть мундштук! С помощью RTTI можно получить гораздо более верное решение данной задачи, поскольку этот метод уместно поместить в более конкретный класс (например, в класс Wind, базовый для всех духовых инструментов). Однако еще более разумным стало бы включение в класс Instrument метода prepareInstrument() (подготовить инструмент к игре), который подошел бы всем инструментам без исключения. На первый взгляд можно было бы ошибочно решить, что в данном случае без RTTI не обойтись.
Наконец, иногда RTTI решает проблемы производительности. Если ваш код использует полиморфизм по всем правилам, но один из объектов чрезвычайно непродуктивно обрабатывается кодом, предназначенным для базового типа, то для этого объекта можно сделать исключение, определить его точный тип с помощью RTTI и работать с ним более производительно. Однако ни в коем случае не следует писать программы, ориентируясь только на их эффективность, как бы соблазнительно это ни было. Сначала надо получить работающую программу и только после этого решать, достаточно ли быстро она работает, и решать проблемы быстродействия, вооружившись инструментами для проверки скорости исполнения.
Мы также видели, что рефлексия открывает перед программистом множество новых возможностей и делает возможным более динамичный стиль программирования. Пожалуй, динамическая природа рефлексии кому-то покажется пугающей. Для тех, кто привык к безопасной статической проверке типов, сама возможность выполнения действий, правильность которых проверяется только на стадии выполнения, а для выдачи информации используются исключения, выглядит шагом в неверном направлении. Некоторые доходят до утверждений, будто сама возможность исключения на стадии выполнения свидетельствует о том, что такого кода лучше избегать. На мой взгляд, чувство безопасности весьма иллюзорно — неожиданности и исключения возможны всегда, даже если программа не содержит блоков try и спецификации исключений. Предпочитаю думать, что существование логически целостной модели выдачи информации об ошибках дает возможность писать динамический код с использованием рефлексии. Конечно, всегда желательно писать код со статической проверкой... когда это возможно. И все же динамический код является одной из важнейших особенностей, отделяющих Java от таких традиционных языков, как C++.
