Глава 13 Thinking in Java 4th edition |
ИНФОРМАЦИЯ О ТИПАХ
Механизм RTTI (Runtime Type Information) предназначен для получения и использования информации о типах во время выполнения программы. RTTI освобождает разработчика от необходимости выполнять всю работу с типами на стадии компиляции и открывает немало замечательных возможностей.ИНФОРМАЦИЯ О ТИПАХ
Механизм RTTI (Runtime Type Information) предназначен для получения и использования информации о типах во время выполнения программы.
RTTI освобождает разработчика от необходимости выполнять всю работу с типами на стадии компиляции и открывает немало замечательных возможностей. Потребность в RTTI вскрывает целый ряд интересных (и зачастую сложных) аспектов объектно-ориентированного проектирования.
В этой главе рассматриваются способы получения информации об объектах и классах во время выполнения программы в Java. Существует два механизма получения такой информации: «традиционный» механизм RTTI, подразумевающий, что все типы доступны во время компиляции, а также механизм рефлексии (reflection), применяемый исключительно во время выполнения программы.
Необходимость в динамическом определении типов (RTTI)
Рассмотрим хорошо знакомый пример с геометрическими фигурами, основанный на полиморфизме. Обобщенным базовым классом является фигура Shape, а производными классами — окружность Circle, прямоугольник Square и треугольник Triangle.
Это обычная диаграмма наследования — базовый класс расположен вверху, производные классы присоединяются к нему снизу. Обычно при разработке объектно-ориентированных программ код по возможности манипулирует ссылками на базовый класс (в нашем случае это фигура — Shape). Если вдруг в программу будет добавлен новый класс (например, производный от фигуры Shape ромб — Rhomboid), то код менять не придется. В нашем случае метод draw() класса является динамически связываемым, поэтому программист-клиент может вызывать этот метод, пользуясь ссылкой базового типа Shape. Метод draw() переопределяется во всех производных классах, и по природе динамического связывания вызов его по ссылке на базовый класс все равно даст необходимый результат. Это и есть полиморфизм.
Таким образом, обычно вы создаете объект конкретного класса (Circle, Square или Triangle), проводите восходящее преобразование к фигуре Shape («забывая» точный тип объекта) и используете ссылку на обобщенную фигуру. Реализация иерархии Shape может выглядеть примерно так:
//: typeinfo/Shapes.java
import java.util.*;
abstract class Shape {
void draw() { System.out.println(this + ".draw()"); }
abstract public String toString();
}
class Circle extends Shape {
public String toString() { return "Circle"; }
}
class Square extends Shape {
public String toString() { return "Square"; }
}
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
public class Shapes {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(
new Circle(), new Square(), new Triangle()
);
for(Shape shape : shapeList)
shape.draw();
}
}
<spoiler text="Output:">
Circle.draw()
Square.draw()
Triangle.draw()
</spoiler>
Метод draw() базового класса Shape неявно использует метод toString() для вывода идентификатора класса, для чего ссылка this передается методу System.out.println() (обратите внимание: метод toString() объявлен абстрактным, чтобы производные классы были обязаны переопределить его и чтобы предотвратить создание экземпляров Shape). Когда этот объект встречается в выражении конкатенации строк, автоматически вызывается его метод toString() для получения соответствующего строкового представления. Каждый из производных классов переопределяет метод toString() (из базового класса Object), чтобы метод draw() выводил в каждом случае различную информацию.
В данном примере восходящее преобразование происходит во время помещения объекта-фигуры в контейнер List<Shape>. В процессе восходящего преобразования теряется конкретная информация, в том числе и точный тип фигур. Контейнеру все равно — он хранит просто объекты Shape.
Когда вы извлекаете из контейнера очередной элемент, контейнер, в котором все элементы хранятся в виде Object, автоматически преобразует результат обратно к Shape. Это наиболее основная форма RTTI, поскольку все подобные преобразования в языке Java проверяются на правильность на стадии исполнения. Именно для этого и служит RTTI: во время выполнения программы проверяется истинный тип объекта.
В нашем случае определение типа происходит частично: тип Object преобразуется к базовому типу фигур Shape, а не к конкретным типам Circle, Square или Triangle. Просто потому, что в данный момент нам известно только то, что контейнер List<Shape> заполнен фигурами Shape. Во время компиляции соблюдение этого требования обеспечивается контейнером и системой параметризованных типов Java, а при выполнении оно подтверждается успешным преобразованием типов.
Теперь в действие вступает полиморфизм — для каждой фигуры Shape вызывается свой метод draw(), в зависимости от того, окружность это (Circle), прямоугольник (Square) или треугольник (Triangle). И в основном именно так и должно быть; основная часть кода не должна зависеть от точного типа объекта, она оперирует с универсальным представлением целого семейства объектов (в нашем случае это фигура (Shape)). Такой подход упрощает написание программы, а впоследствии ее чтение и сопровождение. По этой причине полиморфизм часто используется при написании объектно-ориентированных программ.
Но что, если у вас имеется не совсем обычная задача, для успешного решения которой необходимо узнать точный тип объекта, располагая только ссылкой на базовый тип? Допустим, пользователи программы с фигурами хотят выделить определенные фигуры (скажем, все треугольники) на экране фиолетовым цветом. При помощи RTTI можно узнать точный тип объекта, на который указывает ссылка базового типа Shape.
Объект Class
Чтобы понять, как работает RTTI в Java, необходимо знать, каким образом хранится информация о типе во время выполнения программы. Для этой цели используется специальный объект типа Class, который и содержит описание класса. Объект Class используется при создании всех «обыкновенных» объектов любой программы.
Каждый класс, задействованный в программе, представлен своим объектом Class. Иначе говоря, при написании и последующей компиляции нового класса для него создается объект Class (который затем сохраняется в одноименном файле с расширением .class). Для создания объекта этого класса виртуальная машина Java JVM), исполняющая программу, использует подсистему, называемую загрузчиком классов.
Подсистема загрузчиков классов в действительности состоит из цепочки загрузчиков классов, но только основной загрузчик является частью реализации JVM. Основной загрузчик классов загружает так называемые доверенные классы, в том числе классы Java API, с локального диска. Обычно включать дополнительные загрузчики классов в цепочку не требуется, но в особых ситуациях (например, при загрузке классов для поддержки приложений веб-сервера или при загрузке классов по сети) существует возможность подключения дополнительных загрузчиков.
Все классы загружаются в JVM динамически, при первом использовании класса. Таким образом, программа на Java никогда не бывает полностью загружена до начала своего выполнения, и в этом отношении Java отличается от многих традиционных языков. Динамическая загрузка открывает возможности, недоступные или трудно реализуемые в языках со статической загрузкой вроде C++.
Сначала JVM проверяет, загружен ли объект Class для этого нового класса. При отрицательном результате JVM ищет подходящий файл .class и подгружает его (а дополнительный загрузчик, например, может подгружать байт-код из базы данных). Загруженный код класса проходит проверку на целостность и на отсутствие некорректного байт-кода Java (одна из «линий защиты» в Java).
После того как объект Class для определенного типа будет помещен в память, в дальнейшем он используется при создании всех объектов этого типа. Следующая программа проясняет сказанное:
//: typeinfo/SweetShop.java
// Проверка процесса загрузки классов
import static net.mindview.util.Print.*;
class Candy {
static { print("Загрузка класса Candy"); }
}
class Gum {
static { print("Загрузка класса Gum"); }
}
class Cookie {
static { print("Загрузка класса Cookie"); }
}
public class SweetShop {
public static void main(String[] args) {
print("в методе main()");
new Candy();
print("После создания объекта Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
print("Класс Gum не найден");
}
print("После вызова метода Class.forName(\"Gum\")");
new Cookie();
print("После создания объекта Cookie");
}
}
<spoiler text="Output:">
в методе main()
Загрузка класса Candy
После создания объекта Candy
Загрузка класса Gum
После вызова метода Class.forName("Gum")
Загрузка класса Cookie
После создания объекта Cookie
</spoiler>
В каждом из классов Candy, Gum и Cookie присутствует статический блок, который отрабатывает один раз, при первой загрузке класса. При выполнении этого блока выводится сообщение, говорящее о том, какой класс загружается. В методе main() создание объектов классов Candy, Gum и Cookie чередуется с выводом на экран вспомогательных сообщений, по которым можно оценить, в какой момент загружается тот или иной класс.
Из результата работы программы мы видим, что объект Class загружается только при непосредственной необходимости, а статическая инициализация производится при загрузке этого объекта.
Особенно интересно выглядит строка программы
Class.forName("Gum");
Все объекты Class принадлежат классу Class. Объект Class ничем принципиально не отличается от других объектов, поэтому вы можете выполнять операции со ссылкой на него (именно так и поступает загрузчик классов). Один из способов получения ссылки на объект Class заключается в вызове метода forName(), которому передается строка (String) с именем класса (следите за правильностью написания и регистром символов!). Метод возвращает ссылку на объект Class, которая в данном примере нам не нужна; метод Class.forName() вызывался ради побочного эффекта, то есть загрузки класса Gum, если он еще не в памяти. В процессе загрузки выполняется static-инициализатор класса Gum.
Если бы в рассмотренном примере метод Class.forName() сработал неудачно (не смог бы найти класс, который вы хотели загрузить), он возбудил бы исключение ClassNotFoundException. Здесь мы просто сообщаем о проблеме и двигаемся дальше, однако в более совершенной программе можно было бы попытаться исправить ошибку в обработчике исключения.
Чтобы использовать информацию RTTI на стадии исполнения, прежде всего необходимо получить ссылку на подходящий объект Class. Один из способов — вызов метода Class.forName() — удобен тем, что вам не потребуется уже существующий объект нужного типа. Впрочем, если такой объект уже существует, для получения ссылки на его объект Class можно вызвать метод getClass(), определенный в корневом классе Object. Метод возвращает объект Class, представляющий фактический тип объекта. Класс Class содержит немало интересных методов, продемонстрированных в следующем примере:
//: typeinfo/toys/ToyTest.java
// Testing class Class.
package typeinfo.toys;
import static net.mindview.util.Print.*;
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
// Закомментируйте следующий далее конструктор по
// умолчанию, тогда в строке с пометкой (*1*)
// возникнет ошибка NoSuchMethodError
Toy() {}
Toy(int i) {}
}
class FancyToy extends Toy
implements HasBatteries, Waterproof, Shoots {
FancyToy() { super(1); }
}
public class ToyTest {
static void printInfo(Class cc) {
print("Имя класса: " + cc.getName() +
" это интерфейс?[" + cc.isInterface() + "]");
print("Простое имя: " + cc.getSimpleName());
print("Каноническое имя: " + cc.getCanonicalName());
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("typeinfo.toys.FancyToy");
} catch(ClassNotFoundException e) {
print("He найден класс FancyToy");
System.exit(1);
}
printInfo(c);
for(Class face : c.getInterfaces())
printInfo(face);
Class up = c.getSuperclass();
Object obj = null;
try {
// Необходим конструктор по умолчанию:
obj = up.newInstance();
} catch(InstantiationException e) {
print("He удалось создать объект");
System.exit(1);
} catch(IllegalAccessException e) {
print("Нет доступа");
System.exit(1);
}
printInfo(obj.getClass());
}
}
<spoiler text="Output:">
Имя класса typeinfo toys FancyToy это интерфейс? [false]
Простое имя FancyToy
Каноническое имя- typeinfo.toys FancyToy
Имя класса typeinfo toys HasBatteries это интерфейс? [true]
Простое имя HasBatteries
Каноническое имя: typeinfo toys HasBatteries
Имя класса typeinfo toys Waterproof это интерфейс? [true]
Простое имя. Waterproof
Каноническое имя typeinfo toys Waterproof
Имя класса, typeinfo.toys Shoots это интерфейс? [true]
Простое имя: Shoots
Каноническое имя: typeinfo.toys Shoots
Имя класса: typeinfo.toys Toy это интерфейс? [false]
Простое имя: Toy
Каноническое имя typeinfo.toys.Toy
</spoiler>
Класс FancyToy, производный от Toy, реализует несколько интерфейсов: HasBatteries, Waterproof и Shoots. В методе main() создается ссылка на объект Class для класса FancyToy, для этого в подходящем блоке try вызывается метод forName(). Обратите внимание на необходимость использования полного имени (с именем пакета) в строке, передаваемой forName().
Метод printInfo() использует getName() для получения полного имени класса и методы getSimpleName() и getCanonicalName() (появившиеся в Java SE5), возвращающие имя без пакета и полное имя соответственно. Метод isInterface() проверяет, представляет ли объект Class интерфейс. Таким образом, по объекту Class можно узнать практически все, что может потребоваться узнать о типе.
Метод Class.getInterfaces() возвращает массив объектов Class, представляющих интерфейсы, реализованные объектом Class. Метод getSuperclass() возвращает непосредственный (то есть ближайший) базовый класс для объекта Class.
Метод newInstance() фактически реализует «виртуальный конструктор». Вы как бы говорите: «Я не знаю ваш точный тип, так что создайте себя самостоятельно». В рассмотренном примере ссылка up просто указывает на объект Class, больше никакой информации о типе у вас нет. Поэтому при создании нового экземпляра методом newInstance() вы получаете ссылку на обобщенный объект Object. Однако полученная ссылка на самом деле указывает на объект Toy. Следовательно, перед посылкой сообщений, характерных для класса Toy, придется провести нисходящее преобразование. Вдобавок объект, созданный с помощью метода newInstance(), обязан определить конструктор по умолчанию. Позднее в этой главе будет показано, как динамически создать объект класса любым конструктором с использованием механизма рефлексии Java.
Литералы class
В Java существует еще один способ получения ссылок на объект Class — посредством литерала class. В предыдущей программе получение ссылки выглядело бы так:
FancyToy.class:
Такой способ не только проще, но еще и безопасней, поскольку проверка осуществляется еще во время компиляции. К тому же он не требует вызова forName(), а значит, является более эффективным.
Литералы class работают со всеми обычными классами, так же как и с интерфейсами, массивами и даже с примитивами. Вдобавок во всех классах-обертках для примитивных типов имеется поле с именем TYPE. Это поле содержит ссылку на объект Class для ассоциированного с ним простейшего типа, как показано в табл. 13.1.
Таблица 13.1. Альтернативное обозначение объекта Class с помощью литералов
Литерал
Ссылка на объект Class
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE
Хотя эти версии эквивалентны, я предпочитаю использовать синтаксис .class, так как он лучше сочетается с обычными классами.
Интересно заметить, что создание ссылки на объект Class с использованием записи .class не приводит к автоматической инициализации объекта Class. Подготовка класса к использованию состоит из трех этапов:
- Загрузка — выполняется загрузчиком классов. Последний находит байт- код и создает на его основе объект Class.
- Компоновка — в фазе компоновки проверяется байт-код класса, выделяется память для статических полей, и при необходимости разрешаются все ссылки на классы, созданные этим классом.
- Инициализация — если у класса имеется суперкласс, происходит его инициализация, выполняются статические инициализаторы и блоки статической инициализации.
Инициализация откладывается до первой ссылки на статический метод (конструкторы являются статическими методами) или на неконстантное статическое поле:
//: typeinfo/ClassInitialization.java
import java.util.*;
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization:
System.out.println(Initable.staticFinal);
// Does trigger initialization:
System.out.println(Initable.staticFinal2);
// Does trigger initialization:
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
<spoiler text="Output:">
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
</spoiler>
По сути, инициализация откладывается настолько, насколько это возможно. Из результатов видно, что простое использование синтаксиса .class для получения ссылки на класс не приводит к выполнению инициализации. С другой стороны, вызов Class.forNames() немедленно инициализирует класс для получения ссылки на Class, как мы видим на примере initable3.
Параметризованные ссылки
Объект Class используется для создания экземпляров класса и содержит полный код методов этих экземпляров. Кроме того, в нем содержатся статические члены класса. Таким образом, ссылка на Class подразумевает точный тип того, на что она указывает — на объект класса Class.
Однако проектировщики Java SE5 решили предоставить возможность уточнения записи посредством ограничения типа объекта Class, на который может указывать ссылка; для этой цели применяется синтаксис параметризации. В следующем примере верны оба варианта синтаксиса:
//: typeinfo/GenericClassReferences.java
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // To же самое
intClass = double.class;
// genericIntClass = double.class; // Недопустимо
}
}
Если обычная ссылка на класс может быть связана с любым объектом Class, параметризованная ссылка может связываться только с объектами типа, указанного при ее объявлении. Синтаксис параметризации позволяет компилятору выполнить дополнительную проверку типов.
Новый синтаксис преобразования
В Java SE5 также появился новый синтаксис преобразования ссылок на Class, основанный на методе cast():
//: typeinfo/ClassCasts.java
class Building {}
class House extends Building {}
public class ClassCasts {
public static void main(String[] args) {
Building b = new House();
Class<House> houseType = House.class;
House h = houseType.cast(b);
h = (House)b; // ... or just do this.
}
}
Метод cast() получает объект-аргумент и преобразует его к типу ссылки на Class. Конечно, при взгляде на приведенный код может показаться, что он занимает слишком много места по сравнению с последней строкой main(), которая делает то же самое. Новый синтаксис преобразования полезен в тех ситуациях, когда обычное преобразование невозможно. Обычно это происходит тогда, когда при написании параметризованного кода (см. далее) ссылка на Class сохраняется для преобразования в будущем. Подобные ситуации встречаются крайне редко — во всей библиотеке Java SE5 cast() используется всего один раз (в com.sun.mirror.util.DeclarationFilter).
Другая новая возможность — Class.asSubclass() — вообще не встречается в библиотеке Java SE5. Этот метод позволяет преобразовать объект класса к более конкретному типу.
Проверка перед приведением типов
Итак, мы рассмотрели следующие формы RTTI:
- Классическое преобразование; аналог выражения «(Shape)», которое проверяет, «законно» ли приведение типов в данной ситуации, и в случае неверного преобразования возбуждает исключение ClassCastException.
- Объект Class, представляющий тип вашего объекта. К объекту Class можно обращаться для получения полезной информации во время выполнения программы.
В языке C++ классическая форма типа «(Shape)» вообще не задействует RTTI. Она просто сообщает компилятору, что необходимо обращаться с объектом как с новым типом. В языке Java, который при приведении проверяет соответствие типов, такое преобразование часто называют «безопасным нисходящим приведением типов». Слово «нисходящее» используется в силу традиций, сложившихся в практике составления диаграмм наследования. Если приведение окружности Circle к фигуре Shape является восходящим, то приведение фигуры Shape к окружности Circle является, соответственно, нисходящим. Поскольку компилятор знает, что Circle является частным случаем Shape, он позволяет использовать «восходящее» присваивание без явного преобразования типа. Тем не менее, получив некий объект Shape, компилятор не может быть уверен в том, что он получил: то ли действительно Shape, то ли один из производных типов (Circle, Square или Triangle). На стадии компиляции он видит только Shape и поэтому не позволит использовать «нисходящее» присваивание без явного преобразования типа.
Существует и третья форма RTTI в Java — ключевое слово instanceof, которое проверяет, является ли объект экземпляром заданного типа. Результат возвращается в логическом (boolean) формате, поэтому вы просто «задаете» вопрос в следующей форме:
іf(х instanceof Dog) ((Dog)x).bark().
Команда if сначала проверяет, принадлежит ли объект к классу Dog, и только после этого выполняет приведение объекта к типу Dog. Настоятельно рекомендуется использовать ключевое слово instanceof перед проведением нисходящего преобразования, особенно при недостатке информации о точном типе объекта; иначе возникает опасность исключения ClassCastException.
Обычно проводится поиск одного определенного типа (например, поиск треугольников среди прочих фигур), но с помощью ключевого слова instanceof легко можно идентифицировать все типы объекта. Предположим, что у нас есть иерархия классов для описания домашних животных Pet (и их владельцев — эта особенность пригодится нам в более позднем примере). Каждое существо (Individual) в этой иерархии обладает идентификатором id и необязательным именем.
//: typeinfo/pets/Individual.java
package typeinfo.pets;
public class Individual implements Comparable<Individual> {
private static long counter = 0;
private final long id = counter++;
private String name;
public Individual(String name) { this.name = name; }
// 'name' is optional:
public Individual() {}
public String toString() {
return getClass().getSimpleName() +
(name == null ? "" : " " + name);
}
public long id() { return id; }
public boolean equals(Object o) {
return o instanceof Individual &&
id == ((Individual)o).id;
}
public int hashCode() {
int result = 17;
if(name != null)
result = 37 * result + name.hashCode();
result = 37 * result + (int)id;
return result;
}
public int compareTo(Individual arg) {
// Compare by class name first:
String first = getClass().getSimpleName();
String argFirst = arg.getClass().getSimpleName();
int firstCompare = first.compareTo(argFirst);
if(firstCompare != 0)
return firstCompare;
if(name != null && arg.name != null) {
int secondCompare = name.compareTo(arg.name);
if(secondCompare != 0)
return secondCompare;
}
return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
}
}
В данный момент код Individual нас не интересует — достаточно знать, что объект можно создавать с именем или без, и у каждого объекта Individual имеется метод id(), возвращающий уникальный идентификатор. Также имеется метод toString(); если имя не указано, toString() выдает имя типа. Иерархия классов, производных от Individual:
//typeinfo/pets/Person.java
package typeinfo.pets;
public class Person extends Individual {
public Person(String name) { super(name); }
}
//typeinfo/pets/Pet.java
package typeinfo.pets;
public class Pet extends Individual {
public Pet(String name) { super(name); }
public Pet () { super(); }
}
//typeinfo/pets/Dog.java
package typeinfo.pets;
public class Dog extends Pet {
public Dog(String name) { super(name); }
public Dog() { super(); }
}
//typeinfo/pets/Cat.java
package typeinfo.pets;
public class Cat extends Pet {
public Cat(String name) { super(name); }
public Cat() { super();}
}
//typeinfo/pets/Rodent.java
package typeinfo.pets;
public class Rodent extends Pet {
public Rodent(String name) { super(name); }
public Rodent() { super(); }
}
//typeinfo/pets/Pug.java
package typeinfo.pets;
public class Pug extends Dog {
public Pug(String name) { super(name); }
public Pug() { super(); }
}
//typeinfo/pets/Mutt.java
package typeinfo.pets;
public class Mutt extends Dog {
public Mutt(String name) { super(name); }
public Mutt() { super(); }
}
//typeinfo/pets/EgyptianMau.java
package typeinfo.pets;
public class EgyptianMau extends Cat {
public EgyptianMau(String name) { super(name); }
public EgyptianMau() { super(); }
}
//typeinfo/pets/Manx.java
package typeinfo.pets;
public class Manx extends Cat {
public Manx(String name) { super(name); }
public Manx() { super(); }
}
//typeinfo/pets/Cymric.java
package typeinfo.pets;
public class Cymric extends Manx {
public Cymric(String name) { super(name); }
public Cymric() { super(); }
}
//typeinfo/pets/Rat.java
package typeinfo.pets;
public class Rat extends Rodent {
public Rat(String name) { super(name); }
public Rat() { super(); }
}
//typeinfo/pets/Mouse.java
package typeinfo.pets;
public class Mouse extends Rodent {
public Mouse(String name) { super(name); }
public Mouse() { super(); }
}
//typeinfo/pets/Hamster.java
package typeinfo.pets;
public class Hamster extends Rodent {
public Hamster(String name) { super(name); }
public Hamster() { super(); }
}
Затем нам понадобятся средства для создания случайных типов Pet, а для удобства — массивов и списков (List) с элементами Pet. Чтобы этот инструментарий мог «пережить» несколько разных реализаций, мы определим его в виде абстрактного класса:
// typeinfo/pets/PetCreator java
// Создание случайных последовательностей Pet
package typeinfo.pets;
import java.util.*;
public abstract class PetCreator {
//инициализация генератора случайных чисел
private Random rand = new Random(47);
//абстрактный метод возвращает список возможных типов(классов) животных
public abstract List<Class<? extends Pet>> getTypes();
public Pet randomPet(){ // Создание одного случайного объекта Pet
// генерация случайного номера в диапазоне списка классов животных
int n = rand.nextInt(getTypes().size());
try {//создание нового объекта для класса с данным номером в списке классов
return getTypes().get(n).newInstance(); }
catch( InstantiationException e) {throw new RuntimeException(e); }
catch (IllegalAccessException e) {throw new RuntimeException(e);}
}
public Pet[] createArray(int size) {//создание массива случайных Pet
Pet[] result = new Pet[size];
for (int i=0; i < size; i++)
result[i] = randomPet();
return result;
}
public ArrayList<Pet> arrayList(int size) {// создание списка случайных Pet
ArrayList<Pet> result = new ArrayList<Pet>();
Collections.addAll(result, createArray(size));
return result;
}
}
Абстрактный метод getTypes() поручает производному классу получение списка объектов Class. В качестве типа класса указан «любой производный от Pet», поэтому newInstance() создает Pet без необходимости преобразования типа. Метод randomPet() осуществляет случайную выборку из List и использует полученные объекты Class для создания нового экземпляра данного класса вызовом Class.newInstance(). Метод createArray() использует randomPet() для заполнения массива, a arrayList(), в свою очередь, использует createArray().
При вызове newInstance() возможны два вида исключений, обрабатываемые в секциях catch за блоком try. Имена исключений достаточно хорошо объясняют суть проблемы (IllegalAccessException — нарушение механизма безопасности Java, в данном случае если конструктор по умолчанию объявлен private).
Определяя субкласс PetCreator, достаточно предоставить список типов Pet, которые должны создаваться с использованием randomPet() и других методов. Метод getTypes() возвращает ссылку на статический объект List. Реализация с использованием forName() выглядит так:
//typeinfo/pets/ForNameCreator.java
package typeinfo.pets;
import java.util.*;
public class ForNameCreator extends PetCreator {
private static List<Class<? extends Pet>> types = new ArrayList<Class<? extends Pet>>();
private static String[] typeNames = {
"typeinfo.pets.Mutt",
"typeinfo.pets.Pug",
"typeinfo.pets.EgyptianMau",
"typeinfo.pets.Manx",
"typeinfo.pets.Cymric",
"typeinfo.pets.Rat",
"typeinfo.pets.Mouse",
"typeinfo.pets.Hamster"
};
@SuppressWarnings("unchecked")
// загрузчик списка классов
private static void loader() {
try { //при загрузке класс приводится к типу заданному типизациии контейнера
for(String name : typeNames)
types.add((Class<? extends Pet>)Class.forName(name));
}
catch(ClassNotFoundException e)
{throw new RuntimeException(e);}
}
// статический блок ОДНОКРТАНО загружающий список при инициализации данного класса
static { loader(); }
@Override
public List<Class<? extends Pet>> getTypes(){return types;}
}
Метод loader() создает список List объектов Class с использованием метода Class.forName(). При этом может произойти исключение ClassNotFoundException, что вполне понятно — ведь ему передается строка, содержимое которой невозможно проверить на стадии компиляции. При ссылке на эти классы необходимо указывать имя пакета, которому они принадлежат (typeinfo).
Для получения типизованного списка объектов Class требуется преобразование типа, что приводит к выдаче предупреждения на стадии компиляции. Метод loader() определяется отдельно и размещается в секции статической инициализации, потому что директива @SuppressWarnings не может располагаться прямо в секции статической инициализации.
Для подсчета объектов Pet нам понадобится механизм подсчета их разных видов. Для этой цели идеально подойдет карта (Мар), в которой ключами являются имена типов Pet, а значениями — переменные Integer с количеством Pet. Например, это позволит получать ответы на вопросы типа «сколько существует объектов Hamster?». При подсчете Pet будет использоваться ключевое слово instanceof:
//typeinfo/PetCount.java
// Использование instanceof
package typeinfo;
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
public class PetCount {
static class PetCounter extends HashMap<String,Integer> {
public void count(String type) {
Integer quantity = get(type);
if(quantity == null)
put(type, 1);
else
put(type, quantity +1);
}
}
public static void countPets(PetCreator creator) {
PetCounter counter= new PetCounter();
for(Pet pet : creator.createArray(20)) { // Подсчет всех объектов Pet:
printnb(pet.getClass().getSimpleName() + " ");
if(pet instanceof Pet) counter.count("Pet");
if(pet instanceof Dog) counter.count("Dog");
if(pet instanceof Mutt) counter.count("Mutt");
if(pet instanceof Pug) counter.count("Pug");
if(pet instanceof Cat) counter.count("Cat");
if(pet instanceof Manx) counter.count("EgyptіanMau");
if(pet instanceof Manx) counter.count("Manx");
if(pet instanceof Manx) counter.count("Cymric");
if(pet instanceof Rodent) counter.count("Rodent");
if(pet instanceof Rat) counter.count("Rat");
if(pet instanceof Mouse) counter.count("Mouse");
if(pet instanceof Hamster) counter.count("Hamster");
}
// Вывод результатов подсчета.
print();
print(counter);
}
public static void main(String[] args) {
//countPets(new ForNameCreator());
countPets(new LiteralPetCreator());
}
}
<spoiler text="Output:">
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt
Mutt Cymric Mouse Pug Mouse Cymric
{Pug=3. Cat=9, Hamster=l, Cymric=7, Mouse=2, Mutt=3, Rodent=5, Pet=20, Manx=7,
EgyptianMau=7, Dog=6, Rat=2}
</spoiler>
В countPets массив случайным образом заполняется объектами Pet с использованием PetCreator. Затем каждый объект Pet в массиве тестируется и подсчитывается при помощи instanceof.
У ключевого слова instanceof имеется одно серьезное ограничение: объект можно сравнивать только с именованным типом, но не с объектом Class. Возможно, вам показалось, что в предыдущем примере перебор всех выражений instanceof выглядит неудобно и громоздко, и вы правы. Тем не менее автоматизировать этот процесс невозможно — создать из объектов Class список ArrayList и сравнивать объекты по очереди с каждым его элементом не получится (к счастью, существует альтернативное решение, но это попозже). Впрочем, особенно горевать по этому поводу не стоит — если вам приходится записывать множество проверок instanceof, скорее всего, изъян кроется в архитектуре программы.
Использование литералов class
Если записать пример PetCreator.java с использованием литералов class, программа во многих отношениях становится более понятной:
//• typeinfo/pets/LiteralPetCreator.java
// Using class literals
package typeinfo.pets;
import java.util.*;
public class LiteralPetCreator extends PetCreator {
@SuppressWarnings("unchecked")
// список ВСЕХ возможных типов животных непосредственно инициализированный литералами классов
//сделан неизменяемым ни по размеру ни по значениям
public static final List<Class<? extends Pet>> allTypes =
Collections.unmodifiableList(//метод создает "read-only" коллекцию
Arrays.asList(
Pet.class, Dog.class, Cat.class, Rodent.class, Mutt.class,
Pug.class, EgyptianMau.class, Manx.class, Cymric.class,
Rat.class, Mouse.class, Hamster.class
)); // Типы для случайного создания:
// а это уже "рабочий список" типов животных ))
private static final List<Class<? extends Pet>> types =
allTypes.subList(allTypes.indexOf(Mutt.class),allTypes.size());
@Override
public List<Class<? extends Pet>> getTypes() {return types;}
public static void main(String[] args) {
System.out.println(types);
}
}
<spoiler text="Output:">
[class typeinfo pets.Mutt, class typeinfo.pets.Pug, class typeinfo.pets.EgyptianMau. class
typeinfo pets Manx, class typeinfo.pets.Cymric, class typeinfo.pets.Rat, class
typeinfo.pets.Mouse, class typeinfo.pets.Hamster]
</spoiler>
В будущем примере PetCount3.java контейнер Map заполняется всеми типами Pet (не только генерируемыми случайным образом), поэтому нам понадобился список allTypes. Список types представляет собой часть allTypes (создается вызовом List.subList()) со всеми типами Pet, поэтому он используется для случайного генерирования Pet.
На этот раз при создании types блок try не нужен, так как необходимые проверки типов проводятся еще во время компиляции и исключения не возбуждаются, в отличие от метода Class.forName().
Теперь библиотека typeinfo.pets содержит две реализации PetCreator. Чтобы вторая реализация использовалась по умолчанию, мы можем создать фасад (facade), использующий LiteralPetCreator:
//• typeinfo/pets/Pets.java
// Фасад для получения PetCreator по умолчанию
package typeinfo.pets;
import java.util.*;
public class Pets {
public static final PetCreator creator = new LiteralPetCreator();
public static Pet randomPet() {return creator.randomPet();};
public static Pet[] createArray(int size)
{return creator.createArray(size);}
public static ArrayList<Pet> arrayList(int size)
{return creator.arrayList(size);}
}
При этом также обеспечиваются косвенные вызовы randomPet(), createArray() и arrayList().
Поскольку PetCount.countPets() получает аргумент PetCreator, мы можем легко проверить работу LiteralPetCreator (через представленный фасад):
// typeinfo/PetCount2.java
package typeinfo;
import typeinfo.pets.*;
public class PetCount2 {
public static void main(String[] args) {
PetCount.countPets(Pets.creator);
}
}
/* (Выполните, чтобы увидеть результат) */
//:- Результат будет таким же, как у PetCount.java.
Динамический вызов instanceof
Метод Class.isInstance() позволяет выполнить динамическую проверку типа объекта. Благодаря ему в примере PetCount.java наконец-то можно будет избавиться от нагромождения instanceof:
// typeinfо/PetCount3.java
// Using isInstance()
package typeinfo;
import typeinfo.pets.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class PetCount3 {
static class PetCounter extends LinkedHashMap<Class<? extends Pet>,Integer> {
public PetCounter() {
super(MapData.map(LiteralPetCreator.allTypes, 0));
}
public void count(Pet pet) {
// Class.isInstance() избавляет от множественных instanceof:
for(Map.Entry<Class<? extends Pet>,Integer> pair: entrySet())
if(pair.getKey().isInstance(pet))
put(pair.getKey(), pair.getValue() + 1);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder("{");
for(Map.Entry<Class<? extends Pet>,Integer> pair : entrySet()) {
result.append(pair.getKey().getSimpleName());
result.append("=");
result.append(pair.getValue());
result.append(", ");
}
result.delete(result.length() -2, result.length());
result.append("J");
return result.toString();
}
}
public static void main(String[] args) {
PetCounter petCount = new PetCounter();
for(Pet pet : Pets.createArray(20)) {
printnb(pet.getClass().getSimpleName() + " "); petCount.count(pet);
}
print();
print(petCount);
}
}
<spoiler text="Output:">
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt
Mutt Cymric Mouse Pug Mouse Cymric
{Pet=20, Dog=6. Cat-9. Rodent=5, Mutt-3. Pug=3. EgyptianMau=2, Manx=7, Cymric=5, Rat-2.
Mouse=2, Hamster=l}
</spoiler>
Для подсчета всех разновидностей Pet контейнер PetCounter заполняется типами из LiteralPetCreator.allTypes. При этом используется класс net.mindview.util.MapData, который получает Iterable (allTypesList) и константу (0 в данном случае) и заполняет Map ключами из allTypes со значениями 0. Без предварительного заполнения Map будут подсчитаны только случайно сгенерированные типы, но не базовые типы (такие, как Pet и Cat).
Как видите, метод isInstance() избавил нас от необходимости нагромождать конструкции с instanceof. Вдобавок теперь в программу можно легко добавить новые типы Pet — для этого следует просто изменить массив LiteralPet Creator.types; остальная часть программы не потребует правки (которая была бы неизбежна с операторами instanceof).
Метод toString() был перегружен для получения удобочитаемого вывода.
Рекурсивный подсчет
Контейнер Мар в PetCount3.PetCounter был заполнен всеми классами Pet. Вместо предварительного заполнения карты мы также можем воспользоваться методом Class.isAssignableFrom() и создать обобщенный инструмент подсчета, не ограниченный подсчетом Pet:
//: net/mindview/util/TypeCounter.java
// Подсчет экземпляров в семействе типов
package net.mindview.util;
import java.util.*;
public class TypeCounter extends HashMap<Class<?>,Integer>{
private Class<?> baseType;
public TypeCounter(Class<?> baseType) {
this.baseType = baseType;
}
public void count(Object obj) {
Class<?> type = obj.getClass();
if(!baseType.isAssignableFrom(type))
throw new RuntimeException(obj + " incorrect type: "
+ type + ", should be type or subtype of "
+ baseType);
countClass(type);
}
private void countClass(Class<?> type) {
Integer quantity = get(type);
put(type, quantity == null ? 1 : quantity + 1);
Class<?> superClass = type.getSuperclass();
if(superClass != null &&
baseType.isAssignableFrom(superClass))
countClass(superClass);
}
public String toString() {
StringBuilder result = new StringBuilder("{");
for(Map.Entry<Class<?>,Integer> pair : entrySet()) {
result.append(pair.getKey().getSimpleName());
result.append("=");
result.append(pair.getValue());
result.append(", ");
}
result.delete(result.length()-2, result.length());
result.append("}");
return result.toString();
}
}
Метод count() получает Class для своего аргумента, а затем использует .isAssignableFrom() для проверки принадлежности объекта к интересующей вас иерархии. Метод countClass() сначала производит подсчет для точного типа класса, а затем, если baseType допускает присваивание из суперкласса, рекурсивно вызывает countClass() для суперкласса.
//: typeinfo/PetCount4.java
import typeinfo.pets.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class PetCount4 {
public static void main(String[] args) {
TypeCounter counter = new TypeCounter(Pet.class);
for(Pet pet : Pets.createArray(20)) {
printnb(pet.getClass().getSimpleName() + " ");
counter.count(pet);
}
print();
print(counter);
}
}
<spoiler text="Output:"> (Пример)
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt Mutt
Cymric Mouse Pug Mouse Cymric
{Mouse=2, Dog=6, Manx=7, EgyptianMau=2, Rodent=5, Pug=3, Mutt=3. Cymric=5, Cat=9. Hamster=l,
Pet=20, Rat=2}
</spoiler>
Как видно из результатов, подсчитываются как базовые, так и конкретные типы.
Регистрация фабрик
У построения объектов иерархии Pet есть один недостаток: каждый раз, когда в иерархию включается новый тип Pet, вы должны добавить его в LiteralPetCreator.java. В системах с регулярным добавлением новых классов это может создать проблемы.
Первое, что приходит в голову, — добавить в каждый класс статический инициализатор, который добавлял бы свой класс в некий список. К сожалению, статические инициализаторы вызываются только при первой загрузке класса, поэтому возникает «порочный круг»: класс отсутствует в списке генератора, поэтому генератор не может создать объект этого класса, соответственно, класс не загрузится и не будет помещен в список.
По сути, вы вынуждены создать список вручную (разве что вы напишете утилиту, которая будет анализировать исходный код, а затем создавать и компилировать список). Вероятно, лучшее, что можно сделать, — это разместить список в одном централизованном, очевидном месте. Вероятно, лучшим местом для него будет базовый класс иерархии.
В этом разделе мы также внесем другое изменение: создание объекта будет передано самому классу с использованием паттерна «метод-фабрика». Метод-фабрика может вызываться полиморфно и создает объект соответствующего типа. В следующей упрощенной версии методом-фабрикой является метод create() интерфейса Factory:
//: typeinfo/factory/Factory.java
package typeinfo.factory:
public interface Factory<T> { T create(); }
Обобщенный параметр T позволяет create() возвращать разные типы для разных реализаций Factory. Также при этом используется ковариантность возвращаемых типов.
В следующем примере базовый класс Part содержит список объектов-фабрик. Фабрики типов, которые должны создаваться методом createRandom(), «регистрируются» в базовом классе включением в список partFactories:
package typeinfo;
import typeinfo.factory.*;
import java.util.*;
class Part {
@Override
public String toString() {return getClass().getSimpleName(); }
static List<Factory<? extends Part>>partFactories
= new ArrayList<Factory<? extends Part>>();
static {
// При вызове Collections addAll() выдается предупреждение
// "unchecked generic array creation for varargs parameter"
partFactories.add(new FuelFilter.Factory());
partFactories.add(new AirFilter.Factory());
partFactories.add(new CabinAirFilter.Factory());
partFactories.add(new OilFilter.Factory());
partFactories.add(new FanBelt.Factory());
partFactories.add(new PowerSteeringBelt.Factory());
partFactories.add(new GeneratorBelt.Factory());
}
private static Random rand = new Random(47);
public static Part createRandom() {
int n = rand.nextInt(partFactories.size());
return partFactories.get(n).create();
}
}
class Filter extends Part {}
class FuelFilter extends Filter {
// Создание фабрики для каждого конкретного типа
public static class Factory implements typeinfo.factory.Factory<FuelFilter> {
@Override
public FuelFilter create() { return new FuelFilter ();}
}
}
class AirFilter extends Filter {
public static class Factory implements typeinfo.factory.Factory<AirFilter> {
@Override
public AirFilter create() { return new AirFilter(); }
}
}
class CabinAirFilter extends Filter {
public static class Factory implements typeinfo.factory.Factory<CabinAirFilter> {
@Override
public CabinAirFilter create() {return new CabinAirFilter();}
}
}
class OilFilter extends Filter {
public static class Factory implements typeinfo.factory.Factory<OilFilter> {
@Override
public OilFilter create() { return new OilFilter(); }
}
}
class Belt extends Part {}
class FanBelt extends Belt {
public static class Factory implements typeinfo.factory.Factory<FanBelt> {
@Override
public FanBelt create() { return new FanBelt(); }
}
}
class GeneratorBelt extends Belt {
public static class Factory implements typeinfo.factory.Factory<GeneratorBelt> {
@Override
public GeneratorBelt create() {return new GeneratorBelt();}
}
}
class PowerSteeringBelt extends Belt {
public static class Factory implements typeinfo.factory.Factory<PowerSteeringBelt> {
@Override
public PowerSteeringBelt create() {return new PowerSteeringBelt();}
}
}
public class RegisteredFactories {
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
System.out.println(Part.createRandom());
}
}
<spoiler text="Output:">
GeneratorBelt CabinAirFilter GeneratorBelt AirFilter PowerSteeringBelt CabinAirFilter Fuel
Filter PowerSteeringBelt PowerSteeringBelt Fuel Filter
</spoiler>
He все классы иерархии рассчитаны на создание экземпляров; в нашем примере классы Filter и Belt существуют исключительно в целях классификации. Экземпляры этих классов не создаются — только одного из их субклассов. Если класс должен создаваться посредством createRandom(), он содержит внутренний класс Factory.
Хотя для включения всех фабрик в список можно воспользоваться вызовом Collections.addAll(), компилятор выдает предупреждение, поэтому я вернулся к вызовам add(). Метод createRandom() случайным образом выбирает объект фабрики из partFactories и вызывает его метод create() для получения нового объекта Part.
instanceof и сравнение Class
При получении информации о типе объекта важно различать действие любой формы оператора instanceof (будь это сам оператор instanceof или метод isInstance() - они дают одинаковые результаты) и прямого сравнения объектов Class. Вот пример, который показывает, в чем их различия:
//: typeinfo/FamilyVsExactType.java
// The difference between instanceof and class
package typeinfo;
import static net.mindview.util.Print.*;
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof Base " + (x instanceof Base));
print("x instanceof Derived "+ (x instanceof Derived));
print("Base.isInstance(x) "+ Base.class.isInstance(x));
print("Derived.isInstance(x) " +
Derived.class.isInstance(x));
print("x.getClass() == Base.class " +
(x.getClass() == Base.class));
print("x.getClass() == Derived.class " +
(x.getClass() == Derived.class));
print("x.getClass().equals(Base.class)) "+
(x.getClass().equals(Base.class)));
print("x.getClass().equals(Derived.class)) " +
(x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
<spoiler text="Output:">
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
</spoiler>
Метод test() осуществляет проверку типов полученного объекта, используя для этого обе формы оператора instanceof. Затем он получает ссылку на объект Class и использует операцию сравнения ссылок == и метод equals(), чтобы проверить объекты Class на эквивалентность. Пример доказывает справедливость утверждения о том, что действие оператора instanceof и метода islnstance() одинаково. Совпадают и результаты работы операции сравнения == и метода equals(). Но сами тесты приводят к разным заключениям. В соответствии с концепцией типа instanceof дает ответ на вопрос: «Объект принадлежит этому классу или производному от него?» С другой стороны, сравнение объектов Class оператором == не затрагивает наследования — либо тип точно совпадает, либо нет.
Рефлексия: динамическая информация о классе
Если вы не знаете точный тип объекта, RTTI сообщит вам его. Однако в этом случае существуют ограничения: тип должен быть известен еще во время компиляции программы, иначе определить его с помощью RTTI и сделать с этой информацией что-то полезное будет невозможно. Другими словами, компилятор должен располагать информацией обо всех классах, к которым вы затем хотели бы применить динамическое определение типов (RTTI).
Сначала кажется, что это ограничение не столь существенно, но предположим, что у вас появилась ссылка на объект, который не находится в пространстве вашей программы. Более того, класс этого объекта недоступен во время ее компиляции. Например, вы получили последовательность байтов с диска или из сетевого соединения, и вам сказали, что эта последовательность представляет некоторый класс. Но компилятор ничего не знал об этом классе, когда обрабатывал вашу программу, как же его можно использовать?
В традиционных средах программирования такая задача показалась бы далекой от реальности. Однако границы мира программирования все больше расширяются и мы все чаще встречаемся с такими ситуациями. Во-первых, такие возможности требуются для компонентного программирования, которое служит основой для систем быстрой разработки приложений (Rapid Application Development, RAD). Это визуальный подход для создания программ (экран представлен в виде «формы»), где значки, представляющие визуальные компоненты, перетаскиваются на форму. Затем происходит настройка этих компонентов, они устанавливаются в некоторое состояние во время работы программы. Чтобы изменить состояние компонентов, необходимо некоторым образом создавать их экземпляры, просматривать их содержимое, считывать и записывать внутренние значения. Вдобавок компоненты с поддержкой событий графического интерфейса должны как-то рассказать о них, чтобы система быстрой разработки приложений помогла программисту реализовать поддержку этих событий. Механизм рефлексии предоставляет средства для получения информации о доступных методах и их именах. Такое компонентное программирование поддерживается и в Java, с помощью технологии JavaBeans.
Другая важная предпосылка поддержки динамической информации о классе — предоставление возможности создавать и использовать объекты на удаленных платформах. Этот механизм, называемый удаленным вызовом методов (Remote Method Invocation, RMI), позволяет программе на Java распределять свои объекты по нескольким машинам. Необходимость в удаленном вызове методов возникает по разным причинам: например, при выполнении задачи с интенсивными вычислениями можно сбалансировать нагрузку по доступным компьютерам. Иногда код, выполняющий определенные операции, размещается на одной машине, чтобы она стала общим хранилищем этих операций и любые изменения кода на такой машине автоматически распространялись на всех клиентов этого кода. (Интересный поворот — компьютер существует исключительно для того, чтобы упростить внесение изменений в программное обеспечение!) Ко всему прочему распределенное программирование также поддерживает удаленное специализированное оборудование, которое эффективно выполняет некоторые задачи — например, обращение матриц, — которые при решении их на локальной машине могут потребовать слишком много времени и ресурсов.
Класс Class (уже описанный в этой главе) поддерживает концепцию рефлексии (reflection), для которой существует дополнительная библиотека java.lang.reflect, состоящая из классов Field, Method и Constructor (каждый реализует интерфейс Member). Объекты этих классов создаются JVM, чтобы представлять соответствующие члены неизвестного класса. Объекты Constructor используются для создания новых объектов класса, методы get() и set() — для чтения и записи значений полей класса, представленных объектами Field, метод invoke() — для вызова метода, представленного объектом Method. Вдобавок в классе Class имеются удобные методы getFields(), getMethods() и getConstructors(), которые возвращают массивы таких объектов, как поля класса, его методы и конструкторы. (За подробной информацией обращайтесь к описанию класса Class в электронной документации JDK.) Таким образом, информация о неизвестном объекте становится доступной прямо во время выполнения программы, а потребность в ее получении ко времени компиляции программы отпадает.
Важно понимать, что в механизме рефлексии нет ничего сверхъестественного. Когда вы используете рефлексию для работы с объектом неизвестного типа, виртуальная машина JVM рассматривает его и видит, что он принадлежит определенному классу (это делает и обычное RTTI), но, перед тем как проводить с ним некоторые действия, необходимо загрузить соответствующий объект Class. Таким образом, файл .class для класса этого объекта должен быть доступен JVM либо в сети, либо в локальной системе. Таким образом, истинное различие между традиционным RTTI и рефлексией состоит в том, что при использовании RTTI файл .class открывается и анализируется компилятором. Другими словами, вы можете вызывать методы объекта «нормальным» способом. При использовании рефлексии файл .class во время компиляции недоступен; он открывается и обрабатывается системой выполнения.
Извлечение информации о методах класса
Рефлексия редко используется напрямую; она существует в языке в основном для поддержки других возможностей, таких как сериализация объектов и компоненты JavaBeans. Однако существуют ситуации, в которых динамическая информация о классе просто незаменима.
Для примера возьмем программу, выводящую на экран список методов некоторого класса. При просмотре исходного кода класса или его документации будут видны только те методы, которые были определены или переопределены именно в текущем классе. Но в классе может быть еще множество методов, доступных из его базовых классов. Искать их и сложно, и долго[28]. К счастью, рефлексия позволяет написать простой инструмент, выводящий полную информацию о полном интерфейсе класса. Вот как он работает:
//: typeinfo/ShowMethods.java
// Использование рефлексии для вывода полного списка методов
// класс, в том числе и определенных в базовом классе.
// {Args: ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
import static net.mindview.util.Print.*;
public class ShowMethods {
private static String usage = "usage:\n"
+ "ShowMethods qualified.class.name\n"
+ "To show all methods in class or:\n"
+ "ShowMethods qualified.class.name word\n"
+ "To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if(args.length < 1) {
print(usage);
System.exit(0);
}
int lines = 0;
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if(args.length == 1) {
for(Method method : methods)
print(p.matcher(method.toString()).replaceAll(""));
for(Constructor ctor : ctors)
print(p.matcher(ctor.toString()).replaceAll(""));
lines = methods.length + ctors.length;
} else {
for(Method method : methods)
if (method.toString().indexOf(args[1]) != -1) {
print(p.matcher(method.toString()).replaceAll(""));
lines++;
}
for(Constructor ctor : ctors)
if(ctor.toString().indexOf(args[1]) != -1) {
print(p.matcher(ctor.toString()).replaceAll(""));
lines++;
}
}
}catch(ClassNotFoundException e) {print("No such class: " + e);}
}
}
<spoiler text="Output:">
public static void main(String[]) public native int hashCodeO public final native Class getClass()
public final void wait(long.int) throws InterruptedException public final void wait() throws
InterruptedException public final native void wait(long) throws InterruptedException public
boolean equals(Object) public String toString() public final native void notifyО public final
native void notifyAll() public ShowMethods()
</spoiler>
Методы класса Class.getMethods() и getConstructors() возвращают массивы объектов Method и Constructor, которые представляют методы и конструкторы класса. В каждом из этих классов есть методы для получения и анализа имен, аргументов и возвращаемых значений представляемых методов и конструкторов. Впрочем, также можно использовать простой метод toString(), как и сделано здесь, чтобы получить строку с полным именем метода. Остальная часть кода разбирает командную строку и определяет, подходит ли определенное выражение образцу для поиска (с использованием indexOf()), а после выделяет описатели имен классов.
Результат, полученный от Class.forName(), не может быть известен во время компиляции, поэтому вся информация о сигнатуре методов становится доступной во время выполнения. Если вы тщательно изучите документацию по рефлексии из JDK, то увидите, что рефлексия позволяет установить необходимые аргументы и вызвать метод объекта, «абсолютно неизвестного» во время компиляции программы (чуть позже будут приведены соответствующие примеры). Скорее всего, вам эти возможности никогда не понадобятся, но сам факт их существования интересен.
Приведенный выше результат был получен из командной строки
java ShowMethods ShowMethods
На экран выводится открытый (public) конструктор по умолчанию, хотя в тексте программы такой конструктор не определяется. Тот конструктор, что имеется теперь в классе, автоматически сгенерирован компилятором. Если вы после этого сделаете класс ShowMethods не открытым (удалите из его определения спецификатор доступа public, то есть предоставите ему доступ в пределах пакета), сгенерированный компилятором конструктор исчезнет из списка методов. Сгенерированный конструктор имеет тот же уровень доступа, что и его класс.
Также интересно запустить программу в виде
java ShowMethods java.lang.String
с передачей дополнительного параметра char, int, String и т. п.
Эта программа сэкономит вам немало времени при программировании, когда вы будете мучительно вспоминать, есть ли у этого класса определенный метод, если вам потребуется узнать, имеются ли у некоторого класса методы, возвращающие объекты Color, и т. д.
Динамические посредники
«Посредник» (proxy) принадлежит к числу основных паттернов проектирования. Он представляет собой объект, который подставляется на место «настоящего» объекта для расширения или модификации его операций. Приведу тривиальный пример, показывающий структуру посредника:
//. typeinfo/SimpleProxyDemo.java
package typeinfo;
import static.net.mindview.util.Print.*;
interface Interface {
void doSomething();
void somethingElse(String arg);
}
class RealObject implements Interface {
@Override
public void doSomething() {print("doSomething"); }
@Override
public void somethingElse(String arg) { print("somethingElse " + arg);}
}
class SimpleProxy implements Interface {
private Interface proxied;
public SimpleProxy(Interface proxied) { this.proxied = proxied;}
@Override
public void doSomething() {
print("SimpleProxy doSomething");
proxied.doSomething();
}
@Override
public void somethingElse(String arg) {
print("SimpleProxy somethingElse " + arg);
proxied.somethingElse(arg);
}
}
class SimpleProxyDemo {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}
public static void main(String[] args) {
consumer(new RealObject());
consumer(new SimpleProxy(new RealObject()));
}
}
<spoiler text="Output:">
doSomething somethingElse bonobo SimpleProxy doSomething doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
</spoiler>
Поскольку consumer() получает Interface, он не знает, что ему передается — «настоящий» объект (RealObject) или посредник (Proxy), потому что оба типа реализуют Interface. Объект Proxy, находящийся между клиентом и «настоящим» объектом, выполняет операции, а затем вызывает идентичные методы RealObject.
Посредник пригодится в любой ситуации, когда требуется отделить дополнительные операции от «настоящего» объекта, и особенно когда нужно легко переключаться из режима использования дополнительных операций в режим отказа от них (и наоборот — главной целью паттернов является инкапсуляция изменений, поэтому для оправдания их применения что-то должно изменяться). Допустим, вы хотите отслеживать вызовы методов RealObject, измерять затраты на эти вызовы, и т. д. Такой код не должен встраиваться в приложение, а посредник позволит легко добавить или убрать его по мере необходимости.
Динамические посредники Java развивают концепцию посредника — и объект посредника создается динамически, и обработка вызовов опосредованных методов тоже осуществляется динамически. Все вызовы, обращенные к динамическому посреднику, перенаправляются одному обработчику, который определяет, что это за вызов и как с ним следует поступить. Вот как выглядит пример SimpleProxyDemo.java, переписанный для динамического посредника:
// typeinfo/SimpleDynamicProxy.java
package typeinfo;
import java.lang.reflect.*;
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println ("**** proxy. " + proxy.getClass()
+ ", method- " + method + ", args " + args);
if(args != null)
for(Object arg : args)
System.out.println(" " + arg);
return method.invoke(proxied, args);
}
}
class SimpleDynamicProxy {
public static void consumer(Interface іface) {
іface.doSomething();
іface.somethingElse("bonobo");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
// Вставляєм посредника и вызываем снова:
Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{Interface.class},
new DynamicProxyHandler(real));
consumer(proxy);
}
}
<spoiler text="Output:">
**** proxy: class SProxy(). method: public abstract void Interface.doSomething(), args: null
doSomething
**** proxy: class SProxy(). method: public abstract void
Interface.somethingElse(java.1ang.String),
args: [Ljava.1ang.Object.@42e816
bonobo
somethingElse bonobo
</spoiler>
Динамический посредник создается вызовом статического метода Proxy.newProxyInstance(), которому должен передаваться загрузчик класса, список интерфейсов, которые должны реализовываться посредником (а не классов или абстрактных классов!), а также реализация интерфейса InvocationHandler. Динамический посредник перенаправляет все вызовы обработчику, поэтому конструктор обработчика обычно получает ссылку на «настоящий» объект для перенаправления ему запросов.
Метод invoke() получает объект посредника на случай, если ему понадобится определить, откуда поступил запрос — впрочем, обычно это несущественно. Будьте внимательны при вызове методов посредника из invoke(), потому что вызовы через интерфейс перенаправляются через посредника.
В общем случае вы выполняете опосредованную операцию, а затем используете Method.invoke() для перенаправления запроса опосредованному объекту с передачей необходимых аргументов. При этом некоторые вызовы методов могут отфильтровываться, а другие — проходить:
//: typeinfo/SelectingMethods.java
// Looking for particular methods in a dynamic proxy.
import java.lang.reflect.*;
import static net.mindview.util.Print.*;
class MethodSelector implements InvocationHandler {
private Object proxied;
public MethodSelector(Object proxied) {
this.proxied = proxied;
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(method.getName().equals("interesting"))
print("Proxy detected the interesting method");
return method.invoke(proxied, args);
}
}
interface SomeMethods {
void boring1();
void boring2();
void interesting(String arg);
void boring3();
}
class Implementation implements SomeMethods {
public void boring1() { print("boring1"); }
public void boring2() { print("boring2"); }
public void interesting(String arg) {
print("interesting " + arg);
}
public void boring3() { print("boring3"); }
}
class SelectingMethods {
public static void main(String[] args) {
SomeMethods proxy= (SomeMethods)Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
new Class[]{ SomeMethods.class },
new MethodSelector(new Implementation()));
proxy.boring1();
proxy.boring2();
proxy.interesting("bonobo");
proxy.boring3();
}
}
<spoiler text="Output:">
boring1
boring2
Proxy detected the interesting method
interesting bonobo
boring3
</spoiler>
В данном случае мы просто проверяем имена методов, но с таким же успехом можно анализировать другие аспекты сигнатуры и даже значения аргументов.
Вряд ли вам придется каждый день пользоваться динамическими посредниками, но они хорошо подходят для решения многих разновидностей задач.
Объекты с неопределенным состоянием
Если использовать для обозначения неопределенного состояния (то есть отсутствия) объекта встроенное значение null, то при каждом использовании ссылки придется проверять, не равна ли она null. Это быстро утомляет, а код получается излишне громоздким. Проблема заключается в том, что null не имеет собственного поведения, кроме выдачи NullPointerException при попытке выполнения с ним какой-либо операции. Иногда бывает полезно ввести понятие объекта с неопределенным состоянием, который принимает сообщения, но возвращает значение, свидетельствующее об отсутствии «настоящего» объекта. Таким образом, вы можете считать, что все объекты действительны, и вам не придется тратить время на проверки null (и читать полученный код).
Было бы интересно представить себе язык программирования, автоматически создающий объекты с неопределенным состоянием, но на практике они применяются не так уж часто — иногда проверки null оказывается достаточно, иногда можно уверенно считать, что значение null вам не попадется, а иногда даже обработка аномальных ситуаций через NullPointerException является допустимой. Наибольшую пользу объекты с неопределенным состоянием приносят «вблизи от данных», представляя сущности в пространстве задачи. Простой пример: во многих системах имеется класс Person, а в коде возникают ситуации, когда объект не представляет конкретную личность (или, по крайней мере, информация о ней недоступна); при традиционном подходе вам следовало бы проверить ссылку null. Также можно воспользоваться объектом с неопределенным состоянием, но, даже несмотря на то, что такой объект будет отвечать на все сообщения, на которые отвечает «настоящий» объект, все равно потребуется способ проверки его на «определенность». Проще всего определить для этого специальный интерфейс:
//. net/mindview/uti1/Null.java
package net.mindview.util;
public interface Null {}
Это позволяет instanceof обнаруживать объекты с неопределенным состоянием и, что еще важнее, не требует включения метода isNull() во все классы (в конце концов, это фактически будет другим способом выполнения RTTI — так почему бы сразу не воспользоваться встроенными средствами?):
// typeinfo/Person.java
// Класс с неопределенным состоянием объекта
package typeinfo;
import net.mindview.util.*;
class Person {
public final String first;
public final String last;
public final String address; // И t д.
public static final Person NULL = new NullPerson();
public Person(String first, String last, String address){
this.first = first;
this.last = last;
this.address = address;
}
@Override
public String toString() {
return "Person: " + first + " " + last + " " + address;
}
public static class NullPerson extends Person implements Null {
private NullPerson() {
super("None", "None", "None");
}
@Override
public String toString() { return "NullPerson"; }
}
}
В общем случае объект с неопределенным состоянием является синглетным, поэтому он создается как экземпляр static final. Это возможно благодаря тому, что объект Person неизменяем — значения задаются в конструкторе, а затем читаются, но не могут изменяться (поскольку поля String по своей природе неизменяемы). Если вы захотите изменить NullPerson, его придется заменить новым объектом Person. Обратите внимание: для обнаружения обобщенной поддержки Null или более конкретного типа NullPerson можно использовать instanceof, но при синглетной архитектуре можно воспользоваться просто equals() или даже == для сравнения с Person.NULL.
Представьте, что вы собираетесь открыть новое предприятие, но, пока вакансии еще не заполнены, в каждой должности Position можно временно хранить «заполнитель» — объект Person с неопределенным состоянием:
//• typeinfo/Position.java
package typeinfo;
class Position {
private String title;
private Person person;
public Position(String jobTitle, Person employee) {
title = jobTitle;
person = employee;
if(person == null)
person = Person.NULL;
}
public Position(String jobTitle) {
title = jobTitle;
person = Person.NULL;
}
public String getTitle() { return title; }
public void setTitle(String newTitle) { title = newTitle;}
public Person getPerson() { return person; }
public void setPerson(Person newPerson) {
person = newPerson;
if(person == null)
person = Person.NULL;
}
@Override
public String toString() {
return "Position: " + title + " " + person;
}
}
Превращать Position в объект с неопределенным состоянием не обязательно, потому что существование Person.NULL подразумевает неопределенность Position (возможно, позднее выяснится, что явная поддержка неопределенного состояния для Position нужна, и вы добавите ее, но в соответствии с одним из канонов экстремального программирования в начальный проект следует включить «простейшее решение, которое будет работать», и включать новые функции лишь по мере возникновения реальной необходимости).
Теперь класс Staff может проверять объекты с неопределенным состоянием при заполнении вакансий:
//: typeinfo/Staff.java
import java.util.*;
public class Staff extends ArrayList<Position> {
public void add(String title, Person person) {
add(new Position(title, person));
}
public void add(String... titles) {
for(String title : titles)
add(new Position(title));
}
public Staff(String... titles) { add(titles); }
public boolean positionAvailable(String title) {
for(Position position : this)
if(position.getTitle().equals(title) &&
position.getPerson() == Person.NULL)
return true;
return false;
}
public void fillPosition(String title, Person hire) {
for(Position position : this)
if(position.getTitle().equals(title) &&
position.getPerson() == Person.NULL) {
position.setPerson(hire);
return;
}
throw new RuntimeException(
"Position " + title + " not available");
}
public static void main(String[] args) {
Staff staff = new Staff("President", "CTO",
"Marketing Manager", "Product Manager",
"Project Lead", "Software Engineer",
"Software Engineer", "Software Engineer",
"Software Engineer", "Test Engineer",
"Technical Writer");
staff.fillPosition("President",
new Person("Me", "Last", "The Top, Lonely At"));
staff.fillPosition("Project Lead",
new Person("Janet", "Planner", "The Burbs"));
if(staff.positionAvailable("Software Engineer"))
staff.fillPosition("Software Engineer",
new Person("Bob", "Coder", "Bright Light City"));
System.out.println(staff);
}
}
<spoiler text="Output:">
[Position: President Person: Me Last The Top. Lonely At, Position. СТО NullPerson, Position:
Marketing Manager NullPerson. Position: Product Manager NullPerson, Position. Project Lead
Person: Janet Planner The Burbs. Position: Software Engineer Person: Bob Coder Bright Light City,
Position: Software Engineer NullPerson, Position: Software Engineer NullPerson. Position-
Software Engineer NullPerson. Position. Test Engineer NullPerson. Position: Technical Writer
NullPerson]
</spoiler>
Обратите внимание: в некоторых местах нам по-прежнему приходится проверять объекты на определенное состояние, что принципиально не отличается от проверки null, но в других местах, скажем, при преобразованиях toString(), лишние проверки не нужны; мы просто считаем, что ссылка на объект действительна.
Если вместо конкретных классов используются интерфейсы, для автоматического создания объектов с неопределенным состоянием можно воспользоваться динамическим посредником. Допустим, имеется интерфейс Robot, определяющий имя и модель робота, а также список List<Operation>, определяющий, какие операции выполняет робот. Операция состоит из описания и команды:
//: typeinfo/Operation.java
public interface Operation {
String description();
void command();
}
Чтобы воспользоваться услугами робота, следует вызвать метод operations():
//: typeinfo/Robot.java
import java.util.*;
import net.mindview.util.*;
public interface Robot {
String name();
String model();
List<Operation> operations();
class Test {
public static void test(Robot r) {
if(r instanceof Null)
System.out.println("[Null Robot]");
System.out.println("Название: " + r.name());
System.out.println("Модель: " + r.model());
for(Operation operation : r.operations()) {
System.out.println(operation.description());
operation.command();
}
}
}
}
При этом используется вложенный класс, выполняющий проверку. Теперь мы можем создать робота для уборки снега:
//: typeinfo/SnowRemovalRobot.java
import java.util.*;
public class SnowRemovalRobot implements Robot {
private String name;
public SnowRemovalRobot(String name) {this.name = name;}
public String name() { return name; }
public String model() { return "SnowBot Series 11"; }
public List<Operation> operations() {
return Arrays.asList(
new Operation() {
public String description() {
return name + " может убирать снег";
}
public void command() {
System.out.println(name + " убирает снег");
}
},
new Operation() {
public String description() {
return name + " может колоть лед";
}
public void command() {
System.out.println(name + " колет лед");
}
},
new Operation() {
public String description() {
return name + " может чистить крышу";
}
public void command() {
System.out.println(name + " чистит крышу");
}
}
);
}
public static void main(String[] args) {
Robot.Test.test(new SnowRemovalRobot("Slusher"));
}
}
<spoiler text="Output:">
Название: Slusher
Модель: SnowBot Series 11
Slusher может убирать снег
Slusher убирает снег
Slusher может колоть лед
Slusher колет лед
Slusher может чистить крышу
Slusher чистит крышу
</spoiler>
Предполагается, что существуют разные типы роботов, и для каждого типа Robot объект с неопределенным состоянием должен делать что-то особенное — в нашем примере выдавать информацию о конкетном типе Robot, представленном объектом. Эта информация перехватывается динамическим посредником:
//: typeinfo/NullRobot.java
// Использование динамического посредника для создания
// объекта с неопределенным состоянием
import java.lang.reflect.*;
import java.util.*;
import net.mindview.util.*;
class NullRobotProxyHandler implements InvocationHandler {
private String nullName;
private Robot proxied = new NRobot();
NullRobotProxyHandler(Class<? extends Robot> type) {
nullName = type.getSimpleName() + " NullRobot";
}
private class NRobot implements Null, Robot {
public String name() { return nullName; }
public String model() { return nullName; }
public List<Operation> operations() {
return Collections.emptyList();
}
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return method.invoke(proxied, args);
}
}
public class NullRobot {
public static Robot
newNullRobot(Class<? extends Robot> type) {
return (Robot)Proxy.newProxyInstance(
NullRobot.class.getClassLoader(),
new Class[]{ Null.class, Robot.class },
new NullRobotProxyHandler(type));
}
public static void main(String[] args) {
Robot[] bots = {
new SnowRemovalRobot("SnowBee"),
newNullRobot(SnowRemovalRobot.class)
};
for(Robot bot : bots)
Robot.Test.test(bot);
}
}
<spoiler text="Output:">
Название: SnowBee Модель:
SnowBot Series 11
SnowBee может убирать снег
SnowBee убирает снег
SnowBee может колоть лед
SnowBee колет лед
SnowBee может чистить крышу
SnowBee чистит крышу
[Null Robot]
Название: SnowRemova1 Robot NullRobot
Модель: SnowRemovalRobot NullRobot
</spoiler>
Каждый раз, когда вам требуется объект Robot с неопределенным состоянием, вы вызываете newNullRobot() и передаете тип Robot, для которого создается
посредник. Посредник выполняет требования о поддержке интерфейсов Robot и Null, а также предоставляет имя опосредованного типа.
Интерфейсы и информация о типах
Одной из важных целей ключевого слова interface является изоляция компонентов и сокращение привязок. Использование интерфейсов вроде бы позволяет добиться этой цели, однако RTTI позволяет обойти ограничения — интерфейсы не обеспечивают стопроцентной изоляции. Начнем следующий пример с интерфейса:
//: typeinfo/interfacea/A.java
package typeinfo.interfaces;
public interface A {
void f();
}
Затем интерфейс реализуется, и выясняется, что можно «в обход» добраться до фактического типа реализации:
//: typeinfo/InterfaceViolation.java
// Интерфейс можно обойти
import typeinfo.interfacea.*;
class B implements A {
public void f() {}
public void g() {}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // // Ошибка компиляции
System.out.println(a.getClass().getName());
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
}
<spoiler text="Output:">
В
</spoiler>
Используя RTTI, мы выясняем, что объект а реализован в форме В. Преобразование к типу В позволяет вызвать метод, не входящий в интерфейс А.
Все это абсолютно законно и допустимо, но, скорее всего, вы предпочли бы оградить клиентских программистов от подобных выходок. Казалось бы, ключевое слово interaface должно защищать вас, но на самом деле этого не происходит, а факт использования В для реализации А становится известен любому желающему.
Одно из возможных решений: просто скажите программистам, что если они будут использовать фактический класс вместо интерфейса, то пускай сами разбираются со всеми возникающими проблемами. Вероятно, во многих случаях этого достаточно, но если «вероятно» вас не устраивает — можно применить более жесткие меры.
Проще всего установить для реализации пакетный уровень доступа, чтобы она оставалась невидимой для клиентов за пределами пакета:
//: typeinfo/packageaccess/HiddenC.java
package typeinfo.packageaccess;
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public class HiddenC {
public static A makeA() { return new C(); }
}
Единственная открытая (public) часть пакета, HiddenC, выдает интерфейс А при вызове. Интересно отметить, что, даже если makeA() будет возвращать С, за пределами пакета все равно удастся использовать только А, потому что имя С недоступно.
Попытка нисходящего преобразования к С тоже завершается неудачей:
//: typeinfo/HiddenImplementation.java
// Пакетный доступ тоже можно обойти
import typeinfo.interfacea.*;
import typeinfo.packageaccess.*;
import java.lang.reflect.*;
public class HiddenImplementation {
public static void main(String[] args) throws Exception {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
// Ошибка компиляции, символическое имя 'С' не найдено
/* if(a instanceof C) {
C c = (C)a;
c.g();
} */
// Однако рефлексия позволяет вызвать g():
callHiddenMethod(a, "g");
// ... И даже еще менее доступные методы!
callHiddenMethod(a, "u");
callHiddenMethod(a, "v");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object a, String methodName)
throws Exception {
Method g = a.getClass().getDeclaredMethod(methodName);
g.setAccessible(true);
g.invoke(a);
}
}
<spoiler text="Output:">
public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()
</spoiler>
Как видите, рефлексия позволяет вызвать все методы, даже приватные! Зная имя метода, можно вызвать setAccessible(true) для объекта Method, чтобы сделать возможным его вызов, как видно из реализации callHiddenMethod().
Можно подумать, что проблема решается распространением только откомпилированного кода, но и это не так. Достаточно запустить javap — декомпилятор, входящий в JDK. Командная строка выглядит так:
javap -private С
Флаг -private означает, что при выводе должны отображаться все члены, даже приватные. Таким образом, любой желающий сможет получить имена и сигнатуры приватных методов и вызвать их.
А если реализовать интерфейс в виде приватного внутреннего класса? Вот как это выглядит:
//: typeinfo/InnerImplementation.java
// Приватные внутренние классы не скрываются от рефлексии
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class InnerA {
private static class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public static A makeA() { return new C(); }
}
public class InnerImplementation {
public static void main(String[] args) throws Exception {
A a = InnerA.makeA();
a.f();
System.out.println(a.getClass().getName());
// Reflection still gets into the private class:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}
<spoiler text="Output:">
public C.f()
InnerA$C
public C.g()
package C.u()
protected C.v()
private C.w()
</spoiler>
He помогло. Как насчет анонимного класса?
//: typeinfo/AnonymousImplementation.java
// Анонимные внутренние классы тоже не скрыты от рефлексии
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class AnonymousA {
public static A makeA() {
return new A() {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
};
}
}
public class AnonymousImplementation {
public static void main(String[] args) throws Exception {
A a = AnonymousA.makeA();
a.f();
System.out.println(a.getClass().getName());
// Reflection still gets into the anonymous class:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}
<spoiler text="Output:">
public C.f()
AnonymousA$1
public C.g()
package C.u()
protected C.v()
private C.w()
</spoiler>
Похоже, не существует никакого способа предотвратить обращение и вызов методов с уровнем доступа, отличным от public, посредством рефлексии. Сказанное относится и к полям данных, даже к приватным:
//: typeinfo/ModifyingPrivateFields.java
import java.lang.reflect.*;
class WithPrivateFinalField {
private int i = 1;
private final String s = "I'm totally safe";
private String s2 = "Am I safe?";
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
}
}
public class ModifyingPrivateFields {
public static void main(String[] args) throws Exception {
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("i");
f.setAccessible(true);
System.out.println("f.getInt(pf): " + f.getInt(pf));
f.setInt(pf, 47);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
f = pf.getClass().getDeclaredField("s2");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
}
}
<spoiler text="Output:">
i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
</spoiler>
Впрочем, final-поля защищены от изменений. Система времени выполнения спокойно воспринимает любые попытки их изменения, но при этом ничего не происходит.
В действительности все эти нарушения уровня доступа не так уж страшны. Если кто-то захочет вызывать методы, которым вы назначили приватный или пакетный доступ (тем самым ясно показывая, что вызывать их не следует), вряд ли он станет жаловаться на то, что вы изменили некоторые аспекты этих методов. С другой стороны, «черный ход» к внутреннему устройству класса позволяет решить некоторые проблемы, нерешаемые другими средствами, и в общем случае преимущества рефлексии неоспоримы.
Резюме
Динамическое определение типов (RTTI) позволяет вам получить информацию о точном типе объекта тогда, когда у вас для него имеется лишь ссылка базового типа. Таким образом, оно открывает широкие возможности для злоупотреблений со стороны новичков, которые еще не поняли и не успели оценить всю мощь полиморфизма. У многих людей, ранее работавших с процедурными языками, возникает сильное желание разбить свою программу на множество конструкций switch при помощи RTTI. Однако при этом они лишаются всех преимуществ полиморфизма, относящихся к разработке программы в целом и ее дальнейшей поддержке. В Java рекомендуется использовать именно полиморфные методы, а к услугам RTTI следует прибегать только в крайнем случае.
Впрочем, при использовании полиморфных методов требуется полный контроль над базовым классом, поскольку в некоторой точке программы, после наследования очередного класса, вы можете обнаружить, что базовый класс не содержит нужного вам метода, и тогда RTTI вас выручит: при наследовании вы расширяете интерфейс класса, добавляя в него новые методы. Особенно верно это при использовании в качестве базовых классов библиотек, которые вы не можете изменить. Далее в своем коде в подходящий момент вы обнаруживаете новый тип и вызываете для него нужный метод. Такой подход не противоречит основам полиморфизма и расширяемости программы, так как добавление в программу нового типа не требует изменения бесчисленного множества конструкций switch. Но чтобы извлечь пользу из дополнительной функциональности нового класса, придется использовать RTTI.
Включение некоторого метода в базовый класс будет выгодно только одному производному классу, который действительно реализует его, но все остальные производные классы будут вынуждены использовать для этого метода какую-либо бесполезную «заглушку». Интерфейс базового класса «размывается» и раздражает тех, кому приходится переопределять ненужные абстрактные методы при наследовании от базового класса. Например, рассмотрим иерархию классов, представляющих музыкальные инструменты. Предположим, что вы хотите прочистить мундштуки духовых инструментов своего оркестра. Конечно, можно поместить в базовый класс Instrument (общее представление музыкального инструмента) еще один метод clearSpitValve() (прочистка мундштуков), но тогда получится, что и у синтезатора, и у барабана есть мундштук! С помощью RTTI можно получить гораздо более верное решение данной задачи, поскольку этот метод уместно поместить в более конкретный класс (например, в класс Wind, базовый для всех духовых инструментов). Однако еще более разумным стало бы включение в класс Instrument метода prepareInstrument() (подготовить инструмент к игре), который подошел бы всем инструментам без исключения. На первый взгляд можно было бы ошибочно решить, что в данном случае без RTTI не обойтись.
Наконец, иногда RTTI решает проблемы производительности. Если ваш код использует полиморфизм по всем правилам, но один из объектов чрезвычайно непродуктивно обрабатывается кодом, предназначенным для базового типа, то для этого объекта можно сделать исключение, определить его точный тип с помощью RTTI и работать с ним более производительно. Однако ни в коем случае не следует писать программы, ориентируясь только на их эффективность, как бы соблазнительно это ни было. Сначала надо получить работающую программу и только после этого решать, достаточно ли быстро она работает, и решать проблемы быстродействия, вооружившись инструментами для проверки скорости исполнения.
Мы также видели, что рефлексия открывает перед программистом множество новых возможностей и делает возможным более динамичный стиль программирования. Пожалуй, динамическая природа рефлексии кому-то покажется пугающей. Для тех, кто привык к безопасной статической проверке типов, сама возможность выполнения действий, правильность которых проверяется только на стадии выполнения, а для выдачи информации используются исключения, выглядит шагом в неверном направлении. Некоторые доходят до утверждений, будто сама возможность исключения на стадии выполнения свидетельствует о том, что такого кода лучше избегать. На мой взгляд, чувство безопасности весьма иллюзорно — неожиданности и исключения возможны всегда, даже если программа не содержит блоков try и спецификации исключений. Предпочитаю думать, что существование логически целостной модели выдачи информации об ошибках дает возможность писать динамический код с использованием рефлексии. Конечно, всегда желательно писать код со статической проверкой... когда это возможно. И все же динамический код является одной из важнейших особенностей, отделяющих Java от таких традиционных языков, как C++.
------------------------
ТРИО теплый пол отзыв
Заработок на сокращении ссылок
Earnings on reducing links
Код PHP на HTML сайты
Категория: Книги по Java
Комментарии |