Отражение

From AsIsWiki
Jump to: navigation, search
Форум

Назад | Оглавление | Дальше


Contents

Отражение

Reflection library (библиотека отражения) - набор инструментальных средств для динамической работы с Java-кодом. Широко применяется в JavaBeans при создании компонентов. Позволяет поддерживать инструментальные средства, подобные тем, что используются в Visual Basic. Например, если в процессе разработки или выполнения программы добавляется новый класс, то можно организовать опрос возможностей нового класса.

reflective - способность программы анализировать возможности классов.

Задачи, решаемые с помощью отражения:

  • Анализ возможностей классов в процессе выполнения программы.
  • Проверка объектов при выполнении программы (можно реализовать метод toString(), совместимый со всеми классами).
  • Реализация универсального массива для работы с кодом.
  • Применение объектов Method, подобных указателям на функции в C++.


Класс Class

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

Получить доступ к этой информации можно с помощью класса Class.
Метод getClass() класса Object возвращает экземпляр типа Class:

Employee e;
...
Class cl = e.getClass();  // объект cl описывает свойства класса Employee

Метод getName() класса Class возвращает имя класса:

System.out.println(e.getClass().getName() + " " + e.getName());

Результат:
- Employee Harry Hacker (если e относится к классу Employee)
- Manager Harry Hacker (если e относится к классу Manager)

Статический метод forName() так-же возвращает экземпляр типа Class:

String className = "java.util.Date";

Class cl = Class.forName(className);

Метод forName() генерирует контролируемое исключение (checked exception) если className не является именем класса или интерфейса.

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

  • убеждаемся, что класс, содержащий метод main(), не ссылается явно на другие классы;
  • начинаем выводить на экран данные и грузить остальные классы методом Class.forName()

Третий способ получения объекта Class:

Class cl1 = Date.class;  // Если импортирован java.util.*;

Class cl2 = int.class;

Class cl3 = Double[].class;

Отметим, что объект Class фактически описывает тип, который не обязательно является классом. Например, тип int - это не класс, однако, несмотря на это, int.class - это объект Class.

Начиная с JDK 5.0, класс Class допускает указание типа. Например, Employee.class соответствует типу Class<Employee>. Можно игнорировать параметр, определяющий тип, и работать с обычным классом Class. Дополнительную информацию о типизированных классах Class вы найдете в следующих разделах.

Исторически сложилось так, что метод getName() возвращает для массивов довольно странные имена:

Double[].class.getName() возвращает "[Ljava.lang.Double;"

int[].class.getName() возвращает "[I"

Виртуальная машина Java поддерживает уникальный объект Class для каждого типа. Следовательно, для сравнения объектов можно использовать оператор ==, как показано ниже.

if (e.getClass() == Employee.class) {
    ...
}

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

e.getClass().newInstance();  // создается новый экземпляр класса e

Для инициализации вновь созданного объекта, метод newInstance() использует конструктор по умолчанию (т.е. конструктор без параметров). Если в классе конструктор по умолчанию отсутствует, генерируется исключение.

Используя методы forName() и newInstance(), можно создавать экземпляры классов, имена которых хранятся в строках.

String s = "java.util.Date";

Object m = Class.forName(s).newInstance();

Если при создании объекта на основе имени класса нужно передать конструктору какие-либо параметры, использовать приведенное выше выражение нельзя. Вместо этого нужно применять метод newInstance() класса Constructor. Это один из нескольких классов, определенных в пакете java.lang.reflect.

Метод newInstance() является аналогом виртуального конструктора в C++. Хотя в C++ на самом деле нет виртуальных конструкторов в чистом виде, такое выражение употребляется для обозначения методов, реализуемых с помощью специализированных библиотек. Класс Class соответствует классу type_info в C++, а метод getClass() - оператору typeid. Класс Class в Java более гибок, чем его аналог в C++. Класс type_info может только извлекать строку с именем типа, но не способен создавать объекты этого типа.


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

Если во время выполнения программы возникает ошибка, программа генерирует исключение. Программист может создавать обработчики handlers исключений и программа будет продолжать работу.

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

Существуют два типа исключений:

  • неконтролируемые unchecked
  • контролируемые checked

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

Если остался фрагмент кода, при выполнении которого возможна генерация исключения и вы не предусмотрели его обработку, то компилятор будет настаивать на создании обработчика. Например, метод Class.forName() может сгенерировать контролируемое исключение.

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

try {
    String name = ... ;              // определение имени класса
    Class cl = Class.forName(name);  // может генерировать исключение
    ...                              // действия с классом cl
} catch(Exception e) {
    e.printStackTrace();
}

Если имя класса не существует, то оставшаяся часть блока try игнорируется и управление передается блоку catch. В данном случае предусмотрен вывод содержимого стека с помощью метода printStackTrace() класса Throwable. Этот класс является суперклассом класса Exception. Если в блоке try исключение не генерируется, то код обработчика в разделе catch не выполняется.

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


Использование отражения для анализа свойств классов

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

Три класса, Field, Method и Constructor, в пакете java.lang.reflect описывают соответственно поля, методы и конструкторы класса. Все три класса содержат метод getName(), возвращающий имя анализируемого класса. В состав класса Field входит метод getType(), возвращающий объект типа Class, описывающий тип поля. Классы Method и Constructor имеют методы, определяющие типы параметров, а класс Method может также определять тип возвращаемого значения. Все три класса содержат метод getModifiers(), возвращающий целое число, которое соответствует использованным модификаторам, например public или static. Для анализа этого числа применяются статические методы класса Modifiers из пакета java.lang.reflect. Например, в этом классе есть методы isPublic(), isPrivate() и isFinal(), оперделяющие, является тот или иной метод либо конструктор общедоступным, закрытым или терминальным. Все, что нужно сделать, - это применить соответствующий метод класса Modifier к целому числу, которое возвращается методом getModifiers(). Для вывода самих модификаторов используется метод Modifier.toString().

Методы getFields(), getMethods() и getConstructors() класса Class возвращают массивы общедоступных полей, методов и конструкторов, принадлежащих анализируемому классу. К ним относятся и общедоступные поля суперклассов. Методы getDeclaredFields(), getDeclaredMethods() и getDeclaredConstructors() класса Class возвращают массивы, состоящие из всех полей, методов и конструкторов, объявленных в классе. К ним относятся закрытые и защищенные элементы, но не элементы суперклассов.

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

import java.util.*;
import java.lang.reflect.*;

public class ReflectionTest {

    public static void main(String[] args) {
        // read class name from command line args or user input
        String name;
        if (args.length > 0) {
            name = args[0];
        } else {
            Scanner in = new Scanner(System.in);
            System.out.println("Enter class name (e.g. java.util.Date): ");
            name = in.next();
        }

        try {
            // print class name and superclass name (if != Object)
            Class cl = Class.forName(name);
            Class supercl = cl.getSuperclass();
            System.out.print("class " + name);
            if (supercl != null && supercl != Object.class) {
                System.out.print(" extends " + supercl.getName());
            }

            System.out.print("\n{\n");
            printConstructors(cl);
            System.out.println();
            printMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }

    /**
     * Prints all constructors of a class
     * @param cl a class
     */
    public static void printConstructors(Class cl) {
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Constructor c : constructors) {
            String name = c.getName();
            System.out.print("   " + Modifier.toString(c.getModifiers()));
            System.out.print(" " + name + "(");

            // print parameter types
            Class[] paramTypes = c.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * Prints all methods of a class
     * @param cl a class
     */
    public static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();

        for (Method m : methods) {
            Class retType = m.getReturnType();
            String name = m.getName();

            // print modifiers, return type and method name
            System.out.print("   " + Modifier.toString(m.getModifiers()));
            System.out.print(" " + retType.getName() + " " + name + "(");

            // print parameter types
            Class[] paramTypes = m.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * Prints all fields of a class
     * @param cl a class
     */
    public static void printFields(Class cl) {
        Field[] fields = cl.getDeclaredFields();

        for (Field f : fields) {
            Class type = f.getType();
            String name = f.getName();
            System.out.print("   " + Modifier.toString(f.getModifiers()));
            System.out.println(" " + type.getName() + " " + name + ";");
        }
    }
}

Если предложить для анализа класс Double:

java.lang.Double

то результат будет следующим:

class java.lang.Double extends java.lang.Number
{
   public java.lang.Double(double);
   public java.lang.Double(java.lang.String);

   public boolean equals(java.lang.Object);
   public static java.lang.String toString(double);
   public java.lang.String toString();
   public int hashCode();
   public static native long doubleToRawLongBits(double);
   public static long doubleToLongBits(double);
   public static native double longBitsToDouble(long);
   public volatile int compareTo(java.lang.Object);
   public int compareTo(java.lang.Double);
   public byte byteValue();
   public short shortValue();
   public int intValue();
   public long longValue();
   public float floatValue();
   public double doubleValue();
   public static java.lang.Double valueOf(java.lang.String);
   public static java.lang.Double valueOf(double);
   public static java.lang.String toHexString(double);
   public static int compare(double, double);
   public static boolean isNaN(double);
   public boolean isNaN();
   public boolean isInfinite();
   public static boolean isInfinite(double);
   public static double parseDouble(java.lang.String);

   public static final double POSITIVE_INFINITY;
   public static final double NEGATIVE_INFINITY;
   public static final double NaN;
   public static final double MAX_VALUE;
   public static final double MIN_NORMAL;
   public static final double MIN_VALUE;
   public static final int MAX_EXPONENT;
   public static final int MIN_EXPONENT;
   public static final int SIZE;
   public static final java.lang.Class TYPE;
   private final double value;
   private static final long serialVersionUID;
}

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


Применение отражения для анализа объектов в ходе выполнения программы

Для определения имени и типов полей любого объекта надо выполнить следующие действия:

  • получить соответствующий объект типа Class
  • вызвать метод getDeclaredFields()

В этом разделе мы попробуем определить содержимое поля. Разумеется, если имя и тип объекта известны при написании программы, это не составляет никакого труда. Однако отражение позволяет сделать это и для объектов, которые не известны на этапе компиляции.

Ключевым в этом процессе является метод get() класса Field. Если объект f имеет тип Field (например, если он получен в результате применения метода getDeclareFields()), а объект obj относится к тому же классу, что и объект f, то вызов f.get(obj) вернет объект, значение которого будет текущим значением поля в объекте obj. Рассмотрим конкретный пример:

Employee harry = new Employee("Harry Hacker", 3500, 10, 1, 1989);

Class cl = harry.getClass();  // объект типа Class, представляющий класс Employee

Field f = cl.getDeclaredField("name");  // имя поля для класса Employee

Object V = f.get(harry);  // значение поля name объекта harry, т.е. объект типа String "Harry Hacker"

Несмотря на то, что код кажется несложным, при его выполнении возникает проблема. Поскольку поле name объявлено как private, метод get() сгенерирует исключение IllegalAccessException. Метод get() можно применять только для общедоступных полей. Механизм безопасности Java позволяет определить, какие поля содержит объект, но не даст прочитать их содержимое, если эти поля недоступны.

По умолчанию,механизм отражения выполняет правила доступа, установленные в Java. Однако, если программа не контролируется диспетчером защиты, эти правила можно обойти. Чтобы сделать это, вызовите метод setAccessible() для объектов Field, Method или Constructor, например:

f.setAccessible(true);  // теперь можно вызывать f.get(harry);

Метод setAccessible() относится к классу AccessibleObject, общему суперклассу классов Field, Method и Constructor. Он нужен для обеспечения работы отладчиков, поддержки постоянных хранилищ и выполнения других подобных действий. Позднее мы применим его для создания настраиваемого метода toString().

С методом get() связана еще одна проблема. Поле name имеет тип String, поэтому можно вернуть его значение как объект Object. Допустим, однако, что нам нужно определить значение поля salary. Оно имеет тип double, а в Java нет объектов числового типа. Чтобы решить эту проблему, можно использовать либо метод getDouble() класса Field, либо метод get(), который с помощью механизма отражения поместит поле в соответствующий интерфейсный класс, в данном случае - Double.

Разумеется, значение поля можно не только определять, но и задавать. Вызов f.set(obj, value) задает новое значение поля, представленного переменной f в объекте obj.

В следующем листинге показано, как создать обобщенный метод toString(), работающий с любым классом. Сначала он использует метод getDeclaredFields() для получения всех полей. Затем применяется метод setAccessible(), делающий все эти поля доступными. Далее определяется имя и значение каждого поля. В листинге показан пример преобразования значений в строки путем рекурсивного вызова метода toString():

// Фрагмент кода

class ObjectAnalyzer {

    public String toString(Object obj) {

        Class cl = obj.getClass();
        ...
        String r = cl.getName();

        // inspect the fields of this class and all superclasses
        do {
            r += "[";
            Field[] fields = cl.getDeclaredFields();
            AccessibleObject.setAccessible(fields, true);

            // get the names and values of all fields
            for (Field f : fields) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    if (!r.endsWith("[")) {
                        r += ",";
                    }
                    r += f.getName() + "=";
                    try {
                        Object val = f.get(obj);
                        r += toString(val);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            r += "]";
            cl = cl.getSuperclass();

        } while (cl != Object.class);
        return r;
    }
    ...
}

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

Для просмотра содержимого любого объекта можно воспользоваться методом toString(). Рассмотрим следующий вызов:

ArrayList<Integer> squares = new ArrayList<Integer>();

for (int i = 1; i <= 5; i++) {
    squares.add(i * i);
}

System.out.println(new ObjectAnalyzer().toString(squares));

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

java.util.ArrayList[elementData=class
java.lang.Object[]{java.lang.Integer[value=1][][],
java.lang.Integer[value=4][][],java.lang.Integer[value=9][][],
java.lang.Integer[value=16][][],java.lang.Integer[value=25][][],
null,null,null,null,null},size=5][modCount=5][][]

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

public String toString() {
    return new ObjectAnalyzer().toString(this);
}

Такой метод полезен при отладке программы.

Файл ObjectAnalyzerTest.java:

import java.lang.reflect.*;
import java.util.*;

public class ObjectAnalyzerTest {

    public static void main(String[] args) {
        ArrayList<Integer> squares = new ArrayList<Integer>();
        for (int i = 1; i <= 5; i++) {
            squares.add(i * i);
        }
        System.out.println(new ObjectAnalyzer().toString(squares));
    }
}

class ObjectAnalyzer {

    /**
     * Converts an object to a string representation that lists
     * all fields.
     * @param obj an object
     * @return a string with the object's class name and all
     * field names and values
     */
    public String toString(Object obj) {
        if (obj == null) {
            return "null";
        }
        if (visited.contains(obj)) {
            return "...";
        }
        visited.add(obj);
        Class cl = obj.getClass();
        if (cl == String.class) {
            return (String) obj;
        }
        if (cl.isArray()) {
            String r = cl.getComponentType() + "[]{";
            for (int i = 0; i < Array.getLength(obj); i++) {
                if (i > 0) {
                    r += ",";
                }
                Object val = Array.get(obj, i);
                if (cl.getComponentType().isPrimitive()) {
                    r += val;
                } else {
                    r += toString(val);
                }
            }
            return r + "}";
        }

        String r = cl.getName();
        // inspect the fields of this class and all superclasses
        do {
            r += "[";
            Field[] fields = cl.getDeclaredFields();
            AccessibleObject.setAccessible(fields, true);
            
            // get the names and values of all fields
            for (Field f : fields) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    if (!r.endsWith("[")) {
                        r += ",";
                    }
                    r += f.getName() + "=";
                    try {
                        Class t = f.getType();
                        Object val = f.get(obj);
                        if (t.isPrimitive()) {
                            r += val;
                        } else {
                            r += toString(val);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            r += "]";
            cl = cl.getSuperclass();
        } while (cl != null);

        return r;
    }
    private ArrayList<Object> visited = new ArrayList<Object>();
}


Применение отражения для работы с универсальным массивом

Класс Array из пакета java.lang.reflect позволяет создавать массивы динамически. Например, используя метод arrayCopy(), о котором упоминалось ранее, можно увеличить массив, полностью сохранив его предыдущее содержимое.

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

Employee[] a = new Employee[100];
...
// массив полон
a = (Employee[])arrayGrow(a);

Как написать такой метод? Нам поможет тот факт, что массив Employee[] можно преобразовать в массив Object[]. Попробуем создать обобщенный метод. Мы просто увеличим массив на 10% + 10 элементов (поскольку одного увеличения на 10% для небольших массивов недостаточно).

static Object[] badArrayGrow(Object[] a) {  // бесполезно
    int newLength = a.length * 11 / 10 + 10;
    Object[] newArray = new Object[newLength];
    System.arraycopy(a, 0, newArray, 0, a.length);
    return newArray;
}

Однако, существует проблема, связанная с фактическим использованием полученного в результате массива. Этот массив имеет тип Object[], поскольку он создан с помощью выражения:

new Object[newLength];

Массив класса Object[] не может быть преобразован в массив класса Employee[]. При попытке сделать это возникает исключение ClassCast. Проблема в том, что в ходе выполнения программы система запоминает первоначальный тип элементов массива, т.е. тип, указанный в операторе new. Можно временно преобразовать массив типа Employee[] в массив класса Object[] и обратно, но массив, изначально созданный как массив класса Object[], преобразовать в массив класса Employee[] невозможно. Чтобы создать подходящий обобщенный метод, нужно научиться создавать новый массив, тип которого совпадал бы с типом исходного массива. Для этого нам нужны методы класса Array из пакета java.lang.reflect, особенно метод newInstance(), создающий новый массив. Тип элементов массива и требуемая длина должны передаваться в качестве параметров метода.

Object newArray = Array.newInstance(componentType, newLength);

Чтобы сделать это, нам нужно уметь определять длину и тип элементов нового массива.

Статический метод getLength() класса Array возвращает длину любого массива. Чтобы определить тип элементов нового массива, нужно выполнить следующие действия:

  • Определить, какому классу принадлежит объект a.
  • Убедиться, что он действительно является массивом.
  • Использовать метод getComponentType() класса Class (определенный лишь для объектов, представляющих собой массивы) и получить требуемый тип массива.

Почему метод getLength() принадлежит классу Array, а getComponentType() - классу Class? Вряд ли это известно кому-либо, кроме разработчиков этих классов. Приходится принимать существующее распределение методов по классам.

В итоге мы получим следующий код:

static Object goodArrayGrow(Object a) {  // правильный код
    Class cl = a.getClass();
    if (!cl.isArray()) {
        return null;
    }
    Class componentType = cl.getComponentType();
    int length = Array.getLength(a);
    int newLength = length * 11 / 10 + 10;
    Object newArray = Array.newInstance(componentType, newLength);
    System.arraycopy(a, 0, newArray, 0, length);
    return newArray;
}

Заметим, что метод arrayGrow можно применять для увеличения массива любого типа, а не только массива объектов.

int[] a = {1, 2, 3, 4};
a = (int[])goodArrayGrow(a);

Для этого параметр метода arrayGrow() объявляется как объект Object, а не как массив объектов Object (т.е. Object[]). Массив типа int[] можно преобразовать в объект типа Object, но не массив, состоящий из экземпляров класса Object!

В следующем листинге показано, как следует увеличивать размеры массивов разных типов. Отметим, что приведение типа значения, возвращаемого методом badArrayGrow(), вызовет исключение.

import java.lang.reflect.*;

public class ArrayGrowTest {

    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        a = (int[]) goodArrayGrow(a);
        arrayPrint(a);

        String[] b = {"Tom", "Dick", "Harry"};
        b = (String[]) goodArrayGrow(b);
        arrayPrint(b);

        System.out.println("The following call will generate an exception.");
        b = (String[]) badArrayGrow(b);
    }

    /**
     * This method attempts to grow an array by allocating a
     * new array and copying all elements.
     * @param a the array to grow
     * @return a larger array that contains all elements of a.
     * However, the returned array has type Object[], not
     * the same type as a
     */
    static Object[] badArrayGrow(Object[] a) {
        int newLength = a.length * 11 / 10 + 10;
        Object[] newArray = new Object[newLength];
        System.arraycopy(a, 0, newArray, 0, a.length);
        return newArray;
    }

    /**
     * This method grows an array by allocating a
     * new array of the same type and copying all elements.
     * @param a the array to grow. This can be an object array
     * or a fundamental type array
     * @return a larger array that contains all elements of a.
     */
    static Object goodArrayGrow(Object a) {
        Class cl = a.getClass();
        if (!cl.isArray()) {
            return null;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        int newLength = length * 11 / 10 + 10;

        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, length);
        return newArray;
    }

    /**
     * A convenience method to print all elements in an array
     * @param a the array to print. can be an object array
     * or a fundamental type array
     */
    static void arrayPrint(Object a) {
        Class cl = a.getClass();
        if (!cl.isArray()) {
            return;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        System.out.print(componentType.getName() + "[" + length + "] = { ");
        for (int i = 0; i < Array.getLength(a); i++) {
            System.out.print(Array.get(a, i) + " ");
        }
        System.out.println("}");
    }
}

Результат выполнения программы:

int[13] = { 1 2 3 0 0 0 0 0 0 0 0 0 0 }
java.lang.String[13] = { Tom Dick Harry null null null null null null null null null null }
The following call will generate an exception.
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
        at tests.ArrayGrowTest.main(ArrayGrowTest.java:17)
Java Result: 1


Указатели на методы

В Java не предусмотрены указатели на методы, т.е. нет способа передать одному методу адрес другого. Указатели на методы часто порождают ошибки, а действия, для которых применяются такие указатели, удобно выполнять с помощью интерфейсов. Однако, работая с JDK 1.1 и более поздними версиями, можно создавать указатели на методы. Такая возможность является побочным продуктом механизма отражения.

Среди нестандартных расширений Java, которые Microsoft внесла в язык J++ (и в появившийся затем C#), есть тип указателей на методы, отличающийся от класса Method, обсуждаемого в этом разделе. Эта технология называется делегированием. Однако внутренние классы, которые рассматриваются далее, - более полезный и универсальный механизм, чем делегирование.

Чтобы понять, как работают указатели на методы, вспомните, что поле объекта можно проверить с помощью метода get() класса Field. Аналогично класс Method содержит метод invoke(), позволяющий вызывать метод, содержащийся в текущем объекте этого класса.

Object invoke(Object obj, Object... args)

Первый параметр является неявным, а остальные объекты представляют собой явные параметры. (До появления JDK 5.0 приходилось передавать методу массив объектов, или, если явные параметры отсутствовали, задавать значение null.)

Если метод статический, первый параметр игнорируется; вместо него можно указать null.

Например, если m1 представляет метод getName() класса Employee, можно использовать следующее выражение:

String n = (String) m1.invoke(harry);

Как это уже было с методами get() и set() класса Field, если параметр или возвращаемое значение не являются объектами какого-либо класса, а представляет собой простой тип, возникает проблема. При работе с ранними версиями JDK, нужно было сначала преобразовать все базовые типы в соответствующие типы интерфейсных классов. С появлением JDK 5.0 можно полагаться на автоматическое преобразование простых типов в классы.

Если возвращаемое значение относится к простому типу, метод invoke() возвращает объект интерфейсного класса. Допустим, что m2 представляет метод raiseSalary() класса Employee. В результате выполнения метода будет возвращен объект Double, после чего вы сможете выполнить приведение типов. В JDK 5.0 преобразование произойдет автоматически.

double s = (Double) m2.invoke(harry);

Как получить объект Method? Можно, конечно, вызвать метод getDeclaredMethods() и выполнить поиск среди возвращаемого массива объектов типа Method. Кроме того, можно вызвать метод getMethod() класса Class. Его действие можно сравнить с методом getField(), получающим строку с указанием имени поля и возвращающим объект Field. Однако методов с одним и тем же именем может быть несколько, и следует тщательно выбирать требуемый. По этой причине нужно предусмотреть массив, содержащий правильные типы параметров. Сигнатура метода getMethod() выглядит следующим образом:

Method getMethod(String name, Class... parameterTypes)

Рассмотрим, например, как можно получить указатели на методы getName() и raiseSalary() класса Employee.

Method m1 = Employee.class.getMethod("getName");

Method m2 = Employee.class.getMethod("raiseSalary", double.class);

До появления JDK 5.0 приходилось размещать объекты Class в массиве, либо, при отсутствии параметров, указывать значение null.

Теперь, выяснив правила использования объектов Method, рассмотрим, как они работают. В следующем листинге приведена программа, которая выводит таблицу значений математической функции, например Math.sqrt или Math.sin. Результат работы программы выглядит следующим образом:

public static double java.lang.Math.sqrt(double)
    1,0000 |     1,0000
    2,0000 |     1,4142
    3,0000 |     1,7321
    4,0000 |     2,0000
    5,0000 |     2,2361
    6,0000 |     2,4495
    7,0000 |     2,6458
    8,0000 |     2,8284
    9,0000 |     3,0000
   10,0000 |     3,1623

Разумеется, код, осуществляющий вывод таблицы на экран, не зависит от конкретной функции.

double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx) {
    double y = (Double) f.invoke(null, x);
    System.out.printf("%10.4f | %10.4f%n" + y, x, y);
}

Здесь f - это объект Method. Поскольку мы вызываем статический метод, то в качестве первого параметра invoke() передается null.

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

Math.class.getMethod("sqrt", double.class);

При вызове getMethod() передается имя метода sqrt класса Math и параметр типа double.

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

import java.lang.reflect.*;

public class MethodPointerTest {

    public static void main(String[] args) throws Exception {
        
        // get method pointers to the square and sqrt methods
        Method square = MethodPointerTest.class.getMethod("square", double.class);
        Method sqrt = Math.class.getMethod("sqrt", double.class);

        // print tables of x- and y-values
        printTable(1, 10, 10, square);
        printTable(1, 10, 10, sqrt);
    }

    /**
     * Returns the square of a number
     * @param x a number
     * @return x squared
     */
    public static double square(double x) {
        return x * x;
    }

    /**
     * Prints a table with x- and y-values for a method
     * @param from the lower bound for the x-values
     * @param to the upper bound for the x-values
     * @param n the number of rows in the table
     * @param f a method with a double parameter and double return value
     */
    public static void printTable(double from, double to, int n, Method f) {
        
        // print out the method as table header
        System.out.println(f);

        // construct formatter to print with 4 digits precision
        double dx = (to - from) / (n - 1);

        for (double x = from; x <= to; x += dx) {
            try {
                double y = (Double) f.invoke(null, x);
                System.out.printf("%10.4f | %10.4f%n", x, y);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Таким образом, с помощью объектов Method можно делать то же, что и посредством указателей на функции в языке C++ (или делегирования в C#). Как и в C++, такой стиль програмирования обычно неудобен и часто приводит к ошибкам. Что случится, если мы вызовем метод с неправильными параметрами? Метод invoke() сгенерирует исключение.

Кроме того, и параметры, и возвращаемое значение метода invoke() обязательно должны иметь тип Object. Это значит, что нам придется осуществлять приведение типов, в результате компилятор не станет проверять программу. Следовательно, ошибки проявятся только при ее тестировании, когда исправить их будет значительно сложнее. Более того, программа, использующая механизм отражаения для получения указателей на методы,работает гораздо медленнее, чем программа, непосредственно вызывающая эти методы.

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


Справочник

java.lang.Class

static Class forName(String className)  // возвращает объект Class, представляющий класс с именем className

Object newInstance()  // возвращает новый экземпляр класса
Field[] getFields()  // возвращает массив общедоступных полей анализируемого класса или его суперкласса

Field[] getDeclaredFields()  // возвращает массив всех полей анализируемого класса
                             // возвращает массив нулевой длины, если таких полей нет
                             // или если объект Class представляет собой простой тип или массив
Method[] getMethods()  // возвращает массив общедоступных методов, включая наследуемые

Method[] getDeclaredMethods()  // возвращает массив всех методов, исключая унаследованные
Constructor[] getConstructors()  // возвращает массив общедоступных конструкторов

Constructor[] getDeclaredConstructors()  // возвращает массив всех конструкторов
Field getField(String name)  // возвращает общедоступное поле с указанным именем

Field[] getFields()  // возвращает массив всех полей
Field getDeclaredField(String name)  // возвращает поле с указанным именем, объявленное в классе

Field[] getDeclaredFields()  // возвращает массив всех полей, объявленных в классе


java.lang.reflect.Constructor

Object newInstance(Object[] args)  // создает новый экземпляр класса
                                   // args - параметры, передаваемые конструктору


java.lang.Throwable

void printStackTrace()  // выводит в стандартный поток ошибок объект Throwable и содержимое стека


java.lang.[Field, Method, Constructor]

Class getDeclaringClass()

Возвращает объект Class, соответствующий классу, в котором определен заданный конструктор, метод или поле.
Class[] getExceptionTypes()

Возвращет массив объектов типа Class, представляющих собой типы исключений, генерируемых заданным методом (Constructor или Method).
int getModifiers()

Возвращает целое число, соответствующее модификатору заданного конструктора, метода или поля.
Для анализа возвращаемого значения надо использовать методы класса Modifier.
String getName()  // возвращает строку, в которой содержится имя конструктора, метода или поля
Class[] getParameterTypes()  // возвращает массив объектов типа Class, представляющих типы параметров (Constructor или Method)
Class getReturnType()  // возвращает объект Class, соответствующий возвращаемому типу (в классе Method)


java.lang.reflect.Modifier

static String toString(int modifiers)  // возвращает строку с модификаторами, соответствующими битам в целом числе modifiers
static boolean isAbstract(int modifiers)

static boolean isFinal(int modifiers)

static boolean isInterface(int modifiers)

static boolean isNative(int modifiers)

static boolean isPrivate(int modifiers)

static boolean isProtected(int modifiers)

static boolean isPublic(int modifiers)

static boolean isStatic(int modifiers)

static boolean isStrict(int modifiers)

static boolean isSynchronized(int modifiers)

static boolean isVolatile(int modifiers)

Методы проверяют разряды числа modifiers, которые соответствуют модификаторам, определяемым именами методов.


java.lang.reflect.AccessibleObject

void setAccessible(boolean flag)  // устанавливает признак доступности заданного объекта отражения
                                  // true - проверка доступа к полю отменена и закрытые поля теперь доступны

boolean isAccessible()  // получает значение признака доступности заданного объекта отражения

static void setAccessible(AccessibleObject[] array, boolean flag)  // устанавливает признак доступности массива объектов


java.lang.reflect.Field

Object get(Object obj)  // возвращает значение поля объекта obj, описываемого данным объектом Field

void set(Object obj, Object newValue)  // устанавливает новое значение поля объекта obj, описываемого данным объектом Field


java.lang.reflect.Array

static Object get(Object array, int index)

static xxx getXxx(Object array, int index)

Методы возвращают элемент массива по указанному индексу.
xxx - простые типы boolean, byte, char, double, float, int, long, short
static void set(Object array, int index, Object newValue)

static setXxx(Object array, int index, xxx newValue)

Методы присваивают новое значение элементу массива по указанному индексу.
xxx - простые типы boolean, byte, char, double, float, int, long, short
static int getLength(Object array)  // Возвращает длину массива
static Object newInstance(Class componentType, int length)

static Object newInstance(Class componentType, int[] lengths)

Создает массив из компонентов componentType и заданной длинны length


java.lang.reflect.Method

public Object invoke(Object implicitParameter, Object[] explicit(parameters))  // вызов метода

implicitParameter - вызываемый метод
parameters - параметры метода

Для статических методов, в качестве неявного параметра передается ссылка null.
Простые типы следует передавать только в виде интерфейсных классов.
Значения простых типов должны извлекаться из интерфейсных классов.



Форум

Назад | Оглавление | Дальше

Personal tools
Namespaces

Variants
Actions
Navigation
Tools