Вопросы по Java на собеседовании (5)

1. JRE, JVM и JDK
2. Загрузчики классов, динамическая загрузка классов
3. Области памяти Heap и Stack
4. Сборщик мусора Garbage Collector
5. Рефлексия кода, reflection
6. Определение свойств класса в run-time
7. Определение интерфейсов и конструкторов класса в режиме run-time
8. Определение полей класса в режиме run-time
9. Определение методов класса в режиме run-time
10. Вызов метода класса в режиме run-time

Вопросы и ответы для собеседование по Java, Содержание.
Вопросы и ответы для собеседование по Java, часть 1.
Вопросы и ответы для собеседование по Java, часть 2.
Вопросы и ответы для собеседование по Java, часть 3.
Вопросы и ответы для собеседование по Java, часть 4.
Вопросы и ответы для собеседование по Java, часть 6.

1. JRE, JVM и JDK

JRE (Java Runtime Environment) представляет минимальную реализацию виртуальной машины, необходимую для исполнения Java-приложений. JRE состоит из виртуальной машины JVM (Java Virtual Machine) и библиотек Java-классов, но не включает компилятор и средства разработки Java приложений.

JDK (Java Development Kit) - это комплект разработчика приложений Java, включающий компилятор Java (javac), стандартные библиотеки классов Java, примеры, документацию, различные утилиты и исполнительную систему Java (JRE).

JVM (Java Virtual Machine) - это виртуальная машина Java, предназначенная для исполнения Java программ и являющаяся основной частью JRE. JVM интерпретирует байт-коды Java приложений, полученные в результате компиляции исходного кода Java-программы компилятором javac. Т.е. виртуальная Java машина содержит интерпретатор байт-кода. Для повышения производительности во многих JVM дополнительно применяется JIT-компиляция, предназначенная для преобразования часто исполняемых фрагментов байт-кода в машинный код.

2. Загрузчики классов, динамическая загрузка классов

Java класс может быть загружен в контекст программы каким-либо загрузчиком classloader. Виртуальные машины JVM включают хотя бы один загрузчик классов, так называемый базовый загрузчик, который загружает все основные классы из rt.jar. Базовый загрузчик никак не связан с программой. Следующий загрузчик - это загрузчик расширений, загружающий классы из $JAVA_HOME/lib/ext. Далее по иерархии идет системный загрузчик, который загружает определенные в classpath классы.

Процесс загрузки классов выполняется следующим образом. Сначала системный загрузчик проверяет наличие класса в своем кэше. Если класс найден, то он успешно загружается. В противном случае управление загрузкой передается загрузчику расширений, который также проверяет свой кэш загрузок и, в случае неудачи, передает задачу базовому загрузчику. Базовый загрузчик проверяет кэш и, в случае неудачи, пытается его загрузить. Если загрузка прошла успешно - то процесс закончен, если нет, то управление возвращается загрузчику расширений. Загрузчик расширений пытается загрузить класс и, в случае неудачи, передает задачу системному загрузчику. В завершение процесса, если системный загрузчик не сможет загрузить класс, то возбуждается исключение java.lang.ClassNotFoundException. Так в Java работает загрузка классов, так называемое делегирование загрузки.

Если в программе создаются пользовательские загрузчики, то они должны быть унаследованы от класса java.lang.ClassLoader.

Статическая и динамическая загрузка класса

Статическая загрузка класса происходит при использовании оператора "new". Динамическая загрузка происходит в режиме "run-time" с помощью статического метода класса Class.forName(className).

Подробное описание загрузчиков класса представлено здесь.

3. Области памяти Heap и Stack

Java Heap (куча) - это динамически распределяемая область оперативной памяти (RAM), создаваемая при старте JVM. Используется данная память для JRE классов и размещения объектов. Создание нового объекта в приложении происходит в куче. Количество выделенной памяти под объект зависит от его структуры. Так, например, для целочисленной переменной в куче будет выделено 32 бита. Любой объект, размещенный/созданный в куче, имеет глобальный доступ, и на него могут ссылаться из любой части приложения. Сборщик мусора периодически освобождает память путем удаления объектов, на которые нет каких-либо ссылок.

Java Stack (стек) - это также динамически распределяемая область оперативной памяти (RAM). Однако использование данной памяти имеет существенное отличие. При каждом вызове метода в программе в памяти стека создается новый блок, который содержит примитивы и ссылки на другие объекты в методе. Стековая память работает по принципу LIFO (last-in-first-out). Вместе с окончанием работы метода заканчивается использование блока, освобождая, таким образом, доступ следующему методу. Размер стековой памяти намного меньше объема памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи.

Размер памяти можно определить использованием соответствующих опций JVM. Так для определения начального и максимального размера памяти в куче необходимо использовать опции -Xms и -Xmx. Для определения стековой памяти следует использовать опцию -XX:MaxPermSize. Если память кучи исчерпана, то вызывается исключение java.lang.OutOfMemoryError: Java Heap Space. При отсутствии свободной памяти стека вызывается исключение java.lang.StackOverflowError.

4. Сборщик мусора Garbage Collector

Сборщик мусора имеет несколько алгоритмов очистки памяти. В первую очередь, он может быть вызван, когда объем свободной памяти в области Eden Space становится критичным. В этом случае, Garbage Collector просто переносит объекты, имеющие ссылки, из области Eden Space в область Survivor Space, а объекты без ссылок удаляет. Эта, так называемая minor'ная очистка, выполняется быстро.

Если при очистке мусора памяти в области Survivor Space недостаточно, то долгоживущие объекты переносятся в область Tenured Generation, где они могут хранится до конца работы приложения. Вся остальная куча очищается от мусора.

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

Более подробное описание распределения памяти в виртуальной машине JVM и о Garbage Collector'e представлено здесь.

5. Рефлексия кода, reflection

Java Reflection API - это механизм получения информации в режиме run-time приложения. Рефлексия позволяет получать информацию о конструкторах, методах и полях классов, и выполнять следующие операции над полями и методами класса :

  • определение класса объекта;
  • получение информации о полях, методах, конструкторах и суперклассах;
  • получение информации о модификаторах полей и методов;
  • создание экземпляра класса, имя которого неизвестно до момента выполнения программы;
  • определение и изменение значений свойств объекта;
  • вызов метода объекта.

Интерфейс Java Reflection API состоит из классов пакетов java.lang и java.lang.reflect.

6. Определение свойств класса в режиме run-time

В работающем приложении для получения класса используется метод forName (String className) :

// Без использования Reflection
Foo foo = new Foo();
 
// С использованием Reflection
Class foo = Class.forName("Foo");

Наименование класса, включающего пакет (package), извлекается методом getName() объекта Class :

Class aclass = foo.getClass(); 
System.out.println (aclass.getName());

Для получения значения модификатора класса используется метод getModifiers(). Класс java.lang.reflect.Modifier содержит статические методы, возвращающие логическое значения проверки модификатора класса :

Class cls = foo.getClass(); 
int mods = cls.getModifiers(); 
if (Modifier.isPublic  (mods))	{ System.out.println("public");  }
if (Modifier.isAbstract(mods))	{ System.out.println("abstract");}
if (Modifier.isFinal   (mods))	{ System.out.println("final");   }

Для получения суперкласса рефлексированного объекта (класса) необходимо использовать метод getSuperclass () :

Class cls      = foo.getClass(); 
Class superCls = cls.getSuperClass(); 

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

7. Определение интерфейсов и конструкторов класса в режиме run-time

Для получения в режиме run-time списка интерфейсов, реализующих классом, необходимо получить Class и использовать его метод getInterfaces(). Следующий пример демонстрирует получение списка интерфейсов класса ArrayList :

Class<?>   cls = ArrayList.class; 
Class<?>[] ifs = cls.getInterfaces(); 

System.out.println("List of interfaces\n"); 
for(Class<?> ifc : ifs) { 
    System.out.println (ifc.getName()); 
}

Чтобы IDE (Eclipse) не предупреждала о необходимости определения типа класса

Class is a raw type. References to generic type Class<T> should be parameterized

в коде используются generic'и. В консоль будут выведены следующие интерфейсы, реализуемые классом ArrayList :


List of interfaces

java.util.List
java.util.RandomAccess
java.lang.Cloneable
java.io.Serializable
 

Метод класса getConstructors () позволяет получить массив открытых конструкторов типа java.lang.reflect.Constructor. После этого, можно извлекать информацию о типах параметров конструктора и генерируемых исключениях. Пример :

Class<?> cls = obj.getClass(); 
Constructor[] constructors = cls.getConstructors(); 
for (Constructor constructor : constructors) { 
    Class<?>[] params = constructor.getParameterTypes(); 
    for (Class<?> param : params) { 
        System.out.println(param.getName()); 
    } 
} 

8. Определение полей класса в режиме run-time

Метод getFields() объекта Class возвращает массив открытых полей типа java.lang.reflect.Field. Эти поля могут быть определены не только в классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Field позволяет получить имя поля, тип и модификаторы.

Class<?> cls = obj.getClass(); 
Field[] fields = cls.getFields(); 
for (Field field : fields) { 
    Class<?> fld = field.getType(); 
    System.out.println("Class name : " + field.getName()); 
    System.out.println("Class type : " + fld.getName()); 
}

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

Class<?> cls = obj.getClass();
Field fld = cls.getField("fieldName"); 

Методы getField() и getFields() возвращают только открытые члены данных класса. Чтобы получить все поля класса необходимо использовать методы getDeclaredField() и getDeclaredFields(). Данные методы работают точно также, как и их аналоги getField() и getFields(), за исключением того, что они возвращают все поля, включая закрытые и защищенные.

Определение значение полей класса

Класс Field содержит специализированные методы для получения значений примитивных типов: getInt(), getFloat(), getByte() и др. Для установки значения поля, используется метод set(). Для примитивных типов имеются методы setInt(), setFloat(), setByte() и др.

Class<?> cls = obj.getClass();
Field field = cls.getField("fieldName"); 

String value = (String) field.get(obj);
field.set(obj, "New value");

9. Определение методов класса в режиме run-time

Метод getMethods() объекта Class возвращает массив открытых методов типа java.lang.reflect.Method. Эти методы могут быть определены не только в классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Method позволяет получить имя метода, тип возвращаемого им значения, типы параметров метода, модификаторы и генерируемые исключения.

Class<?> cls = obj.getClass(); 
Method[] methods = cls.getMethods(); 
for (Method method : methods) { 
    System.out.println("Method name : " + method.getName()); 
    System.out.println("Return type : " + 
                          method.getReturnType().getName());
 
    Class<?>[] params = method.getParameterTypes(); 
    System.out.print("Parameters : "); 
    for (Class<?> paramType : params) { 
        System.out.print(" " + paramType.getName()); 
    } 
    System.out.println(); 
} 

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

Class<?> cls = obj.getClass(); 
Class[] params = new Class[] {Integer.class, String.class};

Method method = cls.getMethod("methodName", params); 

10. Вызов метода класса в режиме run-time

Допустим, что имеется класс Reflect, включающий два закрытых поля id и name и несколько методов для определения значений этих полей. Нас будут интересовать только метод setData для определения значений и метод toString для печати значений.

class Reflect
{
    private String name;
    private int    id;

    ...
	
    public void setData(final int id, String name) {
        this.id   = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Reflect [ id : " + id + ", name : " + name + "]";
    }
}

Сначала получаем метод класса. Для этого формируем массив типов параметров метода и вызываем getMethod с наименованием метода класса и списком его параметров params. После этого формируем массив новых значение полей класса args и вызываем метод invoke объекта Method с указанием объекта класса и аргументами. В заключение получаем ссылку на метод toString и распечатываем значения.

Class<?> cls = reflect.getClass(); 

Class<?>[] params = new Class[] {int.class, String.class};
Method method = cls.getMethod("setData", params);

Object[] args = new Object[] {(int) 123, new String("New value")};
method.invoke(reflect, args);

System.out.println("invoke method toString()\n");

method = cls.getMethod("toString");
System.out.println(method.invoke(reflect));

Вопросы и ответы для собеседование по Java, Содержание.
Вопросы и ответы для собеседование по Java, часть 1.
Вопросы и ответы для собеседование по Java, часть 2.
Вопросы и ответы для собеседование по Java, часть 3.
Вопросы и ответы для собеседование по Java, часть 4.
Вопросы и ответы для собеседование по Java, часть 6.

  Рейтинг@Mail.ru