Приклад створення об'єкта через Class.newInstance()

Уяви, що перед тобою постало завдання: потрібно створити об'єкт за допомогою рефлексії. Зробимо?

Почнемо з початкового класу, який ми хочемо отримати:


public class Employee {
    private String name;
    private String surname;
    private int age;

    {
        age = -1;
        name = "Ivan";
        surname = "Ivanov";
    }

    public Employee(String name, String surname, int age) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", surname='" + surname + '\'' +
                ", age=" + age +
                '}';
    }
}

Це буде наш клас, де є кілька полів, конструктор з параметрами, гетери та сеттери, метод toString() і блок ініціалізації. Тепер візьмемося за другу частину — створення об'єкта за допомогою рефлексії. Перший спосіб, який ми розберемо, буде реалізовано через Class.newInstance().


public class Main {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        Employee employee = Employee.class.newInstance();
        System.out.println("age is - " +  employee.getAge());
    }
}

Чудово, запускаємо наш код і чекаємо на виведення віку з ініціалізатора. Але отримуємо помилку про відсутність конструктора за замовчуванням. Виходить, із цим методом ми можемо отримати лише об'єкт, який створено за допомогою стандартного конструктора. Давай додамо конструктор за замовчуванням для нашого класу і протестуємо код ще раз.

Повідомлення про помилку:

Код нового конструктора:


public Employee() { }

Результат програми після додавання конструктора:

age is - -1

Чудово, ми розібралися, як працює цей метод. Тепер давай зазирнемо під капот. Відкриваємо документацію та бачимо, що наш метод уже deprecated (англ. — застарілий):

А ще може викинути винятки InstantiationException, IllegalAccessException. Тому в документації нам пропонують використовувати інший спосіб створення об'єкта, а саме Constructor.newInstance(). Давай розберемо роботу класу Constructor і поговоримо про нього детальніше.

Методи getConstructors і getDeclaredConstructors

Щоб працювати з класом Constructor, нам потрібно спочатку отримати його. Для цього в нас є два методи: getConstructors та getDeclaredConstructors.

Перший повертає список публічних конструкторів у вигляді масиву, а другий — список усіх конструкторів класу у вигляді масиву.

Давай додамо трохи приватності нашому класу — зробимо кілька приватних конструкторів для відображення роботи наших методів.

Додаємо кілька приватних конструкторів:


private Employee(String name, String surname) {
    this.name = name;
    this.surname = surname;
}

У структурі нашого класу добре видно, що один із конструкторів приватний:

Тестуємо роботу наших методів:


public class Main {
	  public static void main(String[] args) {
	      Class employeeClass = Employee.class;
	
	      System.out.println("getConstructors:");
	      printAllConstructors(employeeClass);
	
	      System.out.println("\n" +"getDeclaredConstructors:");
	      printDeclaredConstructors(employeeClass);
	  }
	
	  static void printDeclaredConstructors(Class<?> c){
	      for (Constructor<?> constructor : c.getDeclaredConstructors()   ) {
	          System.out.println(constructor);
	      }
	  }
	
	  static void printAllConstructors(Class<?> c){
	      for (Constructor<?> constructor : c.getConstructors()) {
	          System.out.println(constructor);
	      }
	  }
}

І отримуємо такий результат:

getConstructors:
public ru.javarush.Employee(java.lang.String,java.lang.String,int)
public.ru.javarush.Employee()

getDeclaredConstructors:
private ru.javarush.Employee(java.lang.String,java.lang.String)
public ru.javarush.Employee(java.lang.String,java.lang.String,int)
public ru.javarush.Employee()

Чудово, таким чином ми отримуємо доступ до класу Constructor. Тепер можемо поговорити про його можливості.

Клас java.lang.reflect.Constructor, його основні методи

Давай поглянемо на основні методи і те, яким чином вони працюють:

Метод Опис
getName() Повертає ім'я цього конструктора у вигляді рядка.
getModifiers() Повертає модифікатори мови Java у вигляді числа.
getExceptionTypes() Повертає масив об'єктів класу, які є типами винятків, оголошених конструктором.
getParameters() Повертає масив об'єктів Parameter, який представляють всі параметри. Повертає масив довжини 0, якщо конструктор не має параметрів.
getParameterTypes() Повертає масив об'єктів класу, які представляють формальні типи параметрів у порядку оголошення.
getGenericParameterTypes() Повертає масив об'єктів типу, які представляють формальні типи параметрів у порядку оголошення.

getName() & getModifiers()

Давай огорнемо наш масив у List, щоб було зручно працювати, і напишемо метод getName та getModifiers:


static List<Constructor<?>> getAllConstructors(Class<?> c) {
    return new ArrayList<>(Arrays.asList(c.getDeclaredConstructors()));
}

static List<String> getConstructorNames(List<Constructor<?>> constructors) {
    List<String> result = new ArrayList<>();
    for (Constructor<?> constructor : constructors) {
        result.add(constructor.toString());
    }
    return result;
}

static List<String> getConstructorModifiers(List<Constructor<?>> constructors) {
    List<String> result = new ArrayList<>();
    for (Constructor<?> constructor : constructors) {
        result.add(Modifier.toString(constructor.getModifiers()));
    }
    return result;
}

І наш метод main, де ми будемо все викликати:


public static void main(String[] args) {
    Class employeeClass = Employee.class;
    var constructors = getAllConstructors(employeeClass);
    var constructorNames = getConstructorNames(constructors);
    var constructorModifiers = getConstructorModifiers(constructors);

    System.out.println("Класс Employee:");
    System.out.println("Конструктори :");
    System.out.println(constructorNames);
    System.out.println("Модификатори :");
    System.out.println(constructorModifiers);
}

У результаті побачимо всю потрібну для нас інформацію:

Класс Employee:
Конструктори :
[private ru.javarush.Employee(java.lang.String), public
ru.javarush.Employee(java.lang.String,java.lang.String,int), public ru.javarush.Employee()]
Модификатори :
[private, public, public]

getExceptionTypes()

Цей метод дозволяє отримати масив винятків, які може викинути наш конструктор. Давай модифікуємо один із наших конструкторів і напишемо новий метод.

Тут ми трохи змінили наш поточний приватний конструктор:


private Employee(String name, String surname) throws Exception {
    this.name = name;
    this.surname = surname;
}

А тут у нас метод для отриманння типів винятків і додавання його до main:


static List<Class<?>> getConstructorExceptionTypes(Constructor<?> c) {
      return new ArrayList<>(Arrays.asList(c.getExceptionTypes()));
}


var constructorExceptionTypes = getConstructorExceptionTypes(constructors.get(0));
System.out.println("Типи винятків :");
System.out.println(constructorExceptionTypes);

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

І поглянемо на виведення після того, як додали throws Exception:

Типи винятків :
[class java.lang.Exception]

І до додавання винятку:

Типи винятків :
[]

Усе прекрасно, але як побачити, які параметри потрібні для наших конструкторів? Давай розберемо й цю частину.

getParameters() & getParameterTypes() & getGenericParameterTypes()

Почнемо ми знову з доопрацювання нашого приватного конструктора. Тепер він виглядатиме ось так:


private Employee(String name, String surname, List<String> list) {
    this.name = name;
    this.surname = surname;
}

У нас з'являються три додаткових методи: getParameters для отримання черговості параметрів та їхніх типів, getParameterTypes для отримання типів параметрів та getGenericParameterTypes отримання типів, огорнених у generics.


static List<Parameter> getConstructorParameters(Constructor<?> c) {
    return new ArrayList<>(Arrays.asList(c.getParameters()));
}

static List<Class<?>> getConstructorParameterTypes(Constructor<?> c) {
    return new ArrayList<>(Arrays.asList(c.getParameterTypes()));
}

static List<Type> getConstructorParametersGenerics(Constructor<?> c) {
    return new ArrayList<>(Arrays.asList(c.getGenericParameterTypes()));
}

І до нашого вже не такого маленького main додаємо ще трохи інформації:


var constructorParameterTypes = getConstructorParameterTypes(constructors.get(0));
var constructorParameters = getConstructorParameters(constructors.get(0));
var constructorParametersGenerics = getConstructorParametersGenerics(constructors.get(0));

System.out.println("Параметри конструкторов :");
System.out.println(constructorParameters);

System.out.println("Типи параметрів :");
System.out.println(constructorParameterTypes);

System.out.println("Типи параметрів конструкторів :");
System.out.println(constructorParametersGenerics);

Якщо ми поглянемо на наш результат, то отримаємо дуже докладні дані щодо параметрів наших конструкторів:

Параметри конструкторів :
[java.lang.String arg0, java.lang.String arg1, java.util.List<java.lang.String> arg2]
Типи параметрів :
[class java.lang.String, class java.lang.String, interface java.util.List]
Типи параметрів конструкторов :
[class java.lang.String, class java.lang.String, java.util.List<java.lang.String>]

Тут добре видно відмінність кожного зі способів. Ми бачимо, що можна окремо отримати дані про типи параметрів, дані про класи-обгортки і про все загалом. Супер! Ми закінчили знайомство з класом Constructor, і тепер можемо повернутися до основної теми нашої лекції – до створення об'єктів.

Створення об'єкта через Constructor.newInstance()

Другий спосіб створення об'єктів - виклик методу newInstance у конструкторі. Давай подивимося на прикладі роботи, як нам отримати певний конструктор.

Якщо перед тобою стоїть завдання отримати один конструктор, тобі необхідно використати метод getConstructor (не плутай з getConstructors, який повертає масив усіх конструкторів). Метод getConstructor повертає конструктор за замовчуванням.


public static void main(String[] args) throws NoSuchMethodException {
    Class employeeClass = Employee.class;
    Constructor<?> employeeConstructor = employeeClass.getConstructor();
    System.out.println(employeeConstructor);
}
public ru.javarush.Employee()

А якщо ми хочемо отримати певний конструктор, нам потрібно передавати до цього методу типи параметрів, які будуть у конструкторі.

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


Constructor<?> employeeConstructor2 = employeeClass.getDeclaredConstructor(String.class, String.class, List.class);
System.out.println(employeeConstructor2);

Таким чином ми можемо отримати певний конструктор. Тепер давай спробуємо створити об'єкт із приватного та публічного конструктора.

Публічний конструктор:


Class employeeClass = Employee.class;
Constructor<?> employeeConstructor = employeeClass.getConstructor(String.class, String.class, int.class);
System.out.println(employeeConstructor);

Employee newInstance = (Employee) employeeConstructor.newInstance("NeIvan", "NeIvanov", 10);
System.out.println(newInstance);

І в результаті в нас є об'єкт, з яким ми можемо працювати в подальшому:

public ru.javarush.Employee(java.lang.String,java.lang.String,int)
Employee{name=’NeIvan’ surname=’NeIvanov’, age=10}

Все чудово працює! Тепер спробуємо з приватним конструктором:


Constructor<?> declaredConstructor = employeeClass.getDeclaredConstructor(String.class, String.class, List.class);
System.out.println(declaredConstructor);

Employee newInstance2 = (Employee) declaredConstructor.newInstance("NeIvan", "NeIvanov", new ArrayList<>());
System.out.printf(newInstance2.toString());

В результаті ми отримаємо помилку щодо приватності нашого конструктора:

Java не змогла створити об'єкт за допомогою цього конструктора, але насправді є невелика чарівна штучка, яку ми вкажемо у методі main. Вона встановить доступність нашому конструктору й дозволить створювати об'єкти нашого класу:


declaredConstructor.setAccessible(true);

Результат створення об'єкта:

private ru.javarush.Employee(java.lang.String,java.lang.String,java.util.List)
Employee{name=’NeIvan’, surname=’NeIvanov’, age=-1}

Ми не встановлюємо вік у нашому конструкторі, і він залишається тим же, що й при ініціалізації.

Час підбивати підсумки!

Переваги створення через Constructor.newInstance()

За назвою обидва методи виглядають однаково, але між ними є відмінності:

Class.newInstance() Constructor.newInstance()
Може викликати лише конструктор no-arg. Може викликати будь-який конструктор незалежно від кількості параметрів.
Вимагає, щоб конструктор було видно. Також може викликати приватні конструктори за певних обставин.
Видає будь-який виняток (той, що перевіряється, або ні), який задекларовано конструктором. Завжди огортає виняток за допомогою InvocationTargetException.

Із цих причин Constructor.newInstance() краще за Class.newInstance(), і саме він використовується різними фреймворками та API, наприклад, Spring, Guava, Zookeeper, Jackson, Servlet тощо.