JavaRush /Java Blog /Random-KO /Java의 리플렉션 - 사용 예

Java의 리플렉션 - 사용 예

Random-KO 그룹에 게시되었습니다
일상생활에서 '반성'이라는 개념을 접해본 적이 있을 것입니다. 보통 이 단어는 자기 자신을 연구하는 과정을 가리킨다. 프로그래밍에서는 비슷한 의미를 갖습니다. 프로그램에 대한 데이터를 검사하고 실행 중에 프로그램의 구조와 동작을 변경하는 메커니즘입니다. 여기서 중요한 점은 컴파일 타임이 아닌 런타임에 수행된다는 것입니다. 그런데 런타임에 코드를 검사하는 이유는 무엇입니까? 당신은 이미 그것을 보고 있습니다 :/ Reflection 사용 예 - 1성찰의 아이디어는 한 가지 이유로 즉시 명확하지 않을 수 있습니다: 이 순간까지 당신은 항상 당신이 작업하고 있는 클래스를 알고 있었습니다. 예를 들어 다음과 같은 클래스를 작성할 수 있습니다 Cat.
package learn.javarush;

public class Cat {

   private String name;
   private int age;

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

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

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

   public int getAge() {
       return age;
   }

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

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

}
당신은 그것에 대한 모든 것을 알고 있으며 어떤 필드와 방법이 있는지 볼 수 있습니다. Animal프로그램에 갑자기 다른 클래스의 동물이 필요한 경우에는 편의를 위해 공통 클래스를 사용하여 상속 시스템을 만들 수 있습니다 . 이전에는 부모 개체를 전달할 수 있는 수의과 진료소 클래스도 만들었고 Animal프로그램은 개가 개인지 고양이인지에 따라 동물을 치료했습니다. 이러한 작업은 매우 간단하지는 않지만 프로그램은 컴파일 타임에 클래스에 대해 필요한 모든 정보를 학습합니다. 따라서 메소드의 main()객체를 Cat동물병원 수업의 메소드에 전달하면 프로그램은 이미 이것이 개가 아니라 고양이라는 것을 알고 있습니다. 이제 우리가 또 다른 작업에 직면했다고 상상해 봅시다. 우리의 목표는 코드 분석기를 작성하는 것입니다. CodeAnalyzer단일 메서드( )를 사용하여 클래스를 만들어야 합니다 void analyzeClass(Object o). 이 방법은 다음을 수행해야 합니다.
  • 객체가 어떤 클래스에 전달되었는지 확인하고 콘솔에 클래스 이름을 표시합니다.
  • 비공개 필드를 포함하여 이 클래스의 모든 필드 이름을 결정하고 콘솔에 표시합니다.
  • 비공개 메서드를 포함하여 이 클래스의 모든 메서드 이름을 결정하고 콘솔에 표시합니다.
다음과 같이 보일 것입니다:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит an object o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
이제 이 문제와 이전에 해결했던 나머지 문제 사이의 차이점이 눈에 띕니다. 이 경우, 메소드에 정확히 무엇이 전달될지 사용자나 프로그램 모두 알지 못한다는 사실에 어려움이 있습니다 analyzeClass(). 당신이 프로그램을 작성하면, 다른 프로그래머가 그것을 사용하기 시작할 것이고, 그 프로그래머는 표준 Java 클래스나 자신이 작성한 클래스 등 무엇이든 이 메소드에 전달할 수 있습니다. 이 클래스에는 변수와 메서드가 얼마든지 있을 수 있습니다. 즉, 이 경우 우리(및 우리 프로그램)는 어떤 클래스를 사용하게 될지 전혀 모릅니다. 하지만 우리는 이 문제를 해결해야 합니다. 그리고 여기에 표준 Java 라이브러리인 Java Reflection API가 도움이 됩니다. Reflection API는 강력한 언어 기능입니다. 공식 Oracle 문서에는 자신이 수행하는 작업을 잘 이해하고 있는 숙련된 프로그래머만 이 메커니즘을 사용하도록 권장한다고 명시되어 있습니다. 왜 갑자기 그러한 경고가 미리 표시되는지 곧 이해하게 될 것입니다. :) 다음은 Reflection API를 사용하여 수행할 수 있는 작업 목록입니다.
  1. 객체의 클래스를 알아내거나 결정합니다.
  2. 클래스 수정자, 필드, 메서드, 상수, 생성자 및 슈퍼클래스에 대한 정보를 가져옵니다.
  3. 구현된 인터페이스/인터페이스에 어떤 메서드가 속하는지 알아보세요.
  4. 프로그램이 실행될 때까지 클래스 이름을 알 수 없는 경우 클래스의 인스턴스를 만듭니다.
  5. 이름별로 개체 필드의 값을 가져오고 설정합니다.
  6. 이름으로 개체의 메서드를 호출합니다.
인상적인 목록이죠? :) 주의하세요:리플렉션 메커니즘은 코드 분석기에 전달하는 클래스 개체에 관계없이 이 모든 작업을 "즉시" 수행할 수 있습니다! 예제를 통해 Reflection API의 기능을 살펴보겠습니다.

객체의 클래스를 찾고 결정하는 방법

기본부터 시작해 보겠습니다. Java의 리플렉션 메커니즘에 대한 진입점은 Class. 예, 정말 재미있어 보이지만 그게 리플렉션의 목적입니다 :) 클래스를 사용하여 Class먼저 메서드에 전달된 개체의 클래스를 결정합니다. 이것을 시도해 봅시다:
import learn.javarush.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
콘솔 출력:

class learn.javarush.Cat
두 가지에 주의하세요. 먼저 클래스를 의도적으로 Cat별도의 패키지에 넣었는데 이제 클래스의 전체 이름이 반환되는 learn.javarush;것을 볼 수 있습니다 . getClass()둘째, 변수 이름을 clazz. 조금 이상해 보입니다. 물론 "class"라고 불러야 하지만 "class"는 자바 언어에서 예약어이므로 컴파일러에서는 변수를 그런 식으로 호출하는 것을 허용하지 않습니다. 나는 그것에서 벗어나야 했다 :) 음, 나쁘지 않은 시작이었습니다! 가능성 목록에 또 무엇이 있었나요?

클래스 수정자, 필드, 메서드, 상수, 생성자 및 슈퍼클래스에 대한 정보를 얻는 방법

이것은 이미 더 흥미 롭습니다! 현재 클래스에는 상수나 상위 클래스가 없습니다. 완전성을 위해 추가해 보겠습니다. 가장 간단한 상위 클래스를 만들어 보겠습니다 Animal.
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
그리고 우리 클래스에 상속과 하나 Cat의 상수를 추가해 보겠습니다 .Animal
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
이제 우리는 완전한 세트를 갖게 되었습니다! 반사 가능성을 시험해 봅시다 :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Name класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
콘솔에서 얻는 내용은 다음과 같습니다.
Name класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
수업에 대한 자세한 정보를 많이 얻었습니다! 공개 부분뿐만 아니라 비공개 부분에 대해서도 마찬가지입니다. 주의하세요: private-변수도 목록에 표시됩니다. 실제로 이 시점에서 수업의 "분석"이 완료된 것으로 간주될 수 있습니다. 이제 이 방법을 사용하여 analyzeClass()가능한 모든 것을 배울 것입니다. 그러나 이것이 우리가 성찰 작업을 할 때 가질 수 있는 모든 가능성은 아닙니다. 단순한 관찰에 머물지 말고 적극적인 행동으로 나아가자! :)

프로그램이 실행되기 전에 클래스 이름을 알 수 없는 경우 클래스의 인스턴스를 만드는 방법

기본 생성자부터 시작하겠습니다. 아직 우리 수업에는 없으므로 Cat추가해 보겠습니다.
public Cat() {

}
Cat리플렉션(메서드)을 사용하여 객체를 생성하는 코드는 다음과 같습니다 createCat().
import learn.javarush.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
콘솔에 입력합니다:

learn.javarush.Cat
콘솔 출력:

Cat{name='null', age=0}
이것은 오류가 아닙니다. 값 name과 값은 클래스 메소드 age에서 출력을 프로그래밍했기 때문에 콘솔에 표시됩니다 . 여기서는 콘솔에서 객체를 생성할 클래스의 이름을 읽습니다. 실행 중인 프로그램은 자신이 생성할 객체가 있는 클래스의 이름을 학습합니다. 간결성을 위해 예제 자체보다 더 많은 공간을 차지하지 않도록 적절한 예외 처리를 위한 코드를 생략했습니다. 물론 실제 프로그램에서는 잘못된 이름이 입력되는 상황 등을 처리할 가치가 있습니다. 기본 생성자는 매우 간단하므로 이를 사용하여 클래스의 인스턴스를 만드는 것은 어렵지 않습니다. :) 그리고 메서드를 사용하여 이 클래스의 새 개체를 만듭니다. 클래스 생성자가 매개변수를 입력으로 사용하는지는 또 다른 문제입니다 . 클래스에서 기본 생성자를 제거하고 코드를 다시 실행해 보겠습니다. toString()CatReflection 사용 예 - 3newInstance()Cat

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
문제가 발생했습니다. 기본 생성자를 통해 객체를 생성하는 메서드를 호출했기 때문에 오류가 발생했습니다. 그런데 지금은 그런 디자이너가 없어요. 이는 메소드가 작동할 때 newInstance()리플렉션 메커니즘이 두 개의 매개변수와 함께 이전 생성자를 사용한다는 것을 의미합니다.
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
하지만 우리는 매개변수를 전혀 잊어버린 것처럼 매개변수에 대해 아무 작업도 수행하지 않았습니다. 리플렉션을 사용하여 생성자에 전달하려면 약간 조정해야 합니다.
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
콘솔 출력:

Cat{name='Barsik', age=6}
우리 프로그램에서 어떤 일이 일어나고 있는지 자세히 살펴보겠습니다. 우리는 객체 배열을 만들었습니다 Class.
Class[] catClassParams = {String.class, int.class};
String이는 생성자의 매개변수에 해당합니다(매개변수 및 만 있음 int). 이를 메소드에 전달 clazz.getConstructor()하고 필요한 생성자에 액세스합니다. 그 후에 남은 것은 newInstance()필요한 매개변수를 사용하여 메소드를 호출하고 객체를 필요한 클래스( )에 명시적으로 캐스팅하는 것을 잊지 않는 것 입니다 Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
결과적으로 우리의 객체가 성공적으로 생성될 것입니다! 콘솔 출력:

Cat{name='Barsik', age=6}
계속 진행합시다 :)

이름으로 개체 필드의 값을 가져오고 설정하는 방법

다른 프로그래머가 작성한 클래스를 사용하고 있다고 상상해 보십시오. 그러나 편집할 수 있는 기회는 없습니다. 예를 들어 JAR로 패키지된 기성 클래스 라이브러리가 있습니다. 수업 코드를 읽을 수는 있지만 변경할 수는 없습니다. 이 라이브러리에 클래스(이전 클래스라고 가정 Cat)를 만든 프로그래머는 최종 설계 전에 잠을 충분히 자지 못해 필드에 대한 getter 및 setter를 제거했습니다 age. 이제 이 수업이 여러분에게 왔습니다. 프로그램에 객체만 있으면 되기 때문에 귀하의 요구 사항을 완벽하게 충족합니다 Cat. 하지만 같은 분야에서는 그것들이 필요합니다 age! 이것은 문제입니다: 필드에 수정자가 있고 privategetter와 setter가 이 클래스의 개발자가 될 때 제거되었기 때문에 필드에 접근할 수 없습니다. / 음, 리플렉션은 이 상황에서도 우리에게 도움이 될 수 있습니다! Cat우리는 클래스 코드에 접근 할 수 있습니다. 최소한 클래스 코드에 어떤 필드가 있고 무엇이라고 불리는지 알아낼 수 있습니다. 이 정보를 바탕으로 문제를 해결합니다.
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           Field age = clazz.getDeclaredField("age");

           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
주석에 언급된 대로 name필드에서는 모든 것이 간단합니다. 클래스 개발자가 이에 대한 설정자를 제공했습니다. 또한 기본 생성자에서 객체를 생성하는 방법도 이미 알고 있습니다. 이에 대한 메서드가 있습니다 newInstance(). 하지만 두 번째 필드를 수정해야 합니다. 여기서 무슨 일이 일어나고 있는지 알아 봅시다 :)
Field age = clazz.getDeclaredField("age");
여기서는 객체를 사용하여 을 사용하여 Class clazz필드에 액세스합니다 . 이는 age 필드를 객체로 가져오는 기능을 제공합니다 . 그러나 필드에 단순히 값을 할당할 수 없기 때문에 이것만으로는 충분하지 않습니다 . 이렇게 하려면 다음 메소드를 사용하여 필드를 "사용 가능"하게 만들어야 합니다 . agegetDeclaredField()Field ageprivatesetAccessible()
age.setAccessible(true);
이 작업이 수행되는 필드에는 값을 할당할 수 있습니다.
age.set(cat, 6);
보시다시피, 우리는 일종의 setter를 뒤집어 놓았습니다. 필드에 Field age값을 할당하고 이 필드가 할당되어야 하는 객체에도 전달합니다. 메서드를 실행 main()하고 다음을 살펴보겠습니다.

Cat{name='Barsik', age=6}
좋아요, 우리가 다 해냈어요! :) 우리에게 어떤 다른 가능성이 있는지 살펴보겠습니다...

객체의 메소드를 이름으로 호출하는 방법

이전 예에서 상황을 약간 바꿔 보겠습니다. 클래스 개발자가 Cat필드에 실수를 했다고 가정해 보겠습니다. 둘 다 사용 가능하고 getter와 setter가 있으므로 모든 것이 정상입니다. 문제는 다릅니다: 그는 우리에게 꼭 필요한 메소드를 비공개로 만들었습니다.
private void sayMeow() {

   System.out.println("Meow!");
}
결과적으로 Cat프로그램에서 객체를 생성하지만 해당 메소드를 호출할 수는 없습니다 sayMeow(). 야옹거리지 않는 고양이가 있을까요? 꽤 이상해요 :/ 이 문제를 어떻게 고칠 수 있나요? 다시 한 번 Reflection API가 구출되었습니다! 우리는 필요한 메소드의 이름을 알고 있습니다. 나머지는 기술의 문제입니다.
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Barsik", 6);

           clazz = Class.forName(Cat.class.getName());

           Method sayMeow = clazz.getDeclaredMethod("sayMeow");

           sayMeow.setAccessible(true);

           sayMeow.invoke(cat);

       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
여기서 우리는 개인 필드에 액세스하는 상황과 거의 동일한 방식으로 행동합니다. 먼저 우리는 클래스 객체에 캡슐화된 필요한 메소드를 얻습니다 Method.
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
도움을 받으면 getDeclaredMethod()비공개 방법에 "연락"할 수 있습니다. 다음으로 메소드를 호출 가능하게 만듭니다.
sayMeow.setAccessible(true);
마지막으로 원하는 객체에 대해 메서드를 호출합니다.
sayMeow.invoke(cat);
메소드 호출은 "역방향 호출"과도 유사합니다. 점( )을 사용하여 객체에 필요한 메소드를 가리키는 데 익숙하며 cat.sayMeow()리플렉션 작업을 할 때 호출해야 하는 객체를 메소드에 전달합니다. . 콘솔에는 무엇이 있나요?

Meow!
모든 것이 해결되었습니다! :) 이제 Java의 리플렉션 메커니즘이 우리에게 제공하는 광범위한 가능성을 살펴보겠습니다. 어렵고 예상치 못한 상황(폐쇄된 도서관의 수업 예시처럼)에서는 정말 많은 도움이 될 수 있습니다. 그러나 여느 큰 힘과 마찬가지로 큰 책임도 따릅니다. 리플렉션의 단점은 Oracle 웹 사이트의 특별 섹션 에 설명되어 있습니다 . 세 가지 주요 단점이 있습니다.
  1. 생산성이 감소합니다. 리플렉션을 사용하여 호출되는 메서드는 일반적으로 호출되는 메서드보다 성능이 낮습니다.

  2. 안전 제한이 있습니다. 리플렉션 메커니즘을 사용하면 런타임 중에 프로그램의 동작을 변경할 수 있습니다. 그러나 실제 프로젝트의 작업 환경에서는 이를 허용하지 않는 제한 사항이 있을 수 있습니다.

  3. 내부 정보 공개 위험. 리플렉션을 사용하면 캡슐화 원칙을 직접적으로 위반한다는 점을 이해하는 것이 중요합니다. 이를 통해 비공개 필드, 메서드 등에 액세스할 수 있습니다. OOP 원칙에 대한 직접적이고 중대한 위반은 귀하가 통제할 수 없는 이유로 문제를 해결할 수 있는 다른 방법이 없는 가장 극단적인 경우에만 의존해야 한다고 설명할 필요가 없다고 생각합니다.

반사 메커니즘은 피할 수 없는 상황에서만 현명하게 사용하고 그 단점을 잊지 마십시오. 이것으로 강의를 마치겠습니다! 꽤 큰 것으로 판명되었지만 오늘은 많은 새로운 것을 배웠습니다 :)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION