John Squirrels
레벨 41
San Francisco

반사의 예

무작위의 그룹에 게시되었습니다
회원
아마도 당신은 일상 생활에서 "반사"라는 개념을 접했을 것입니다. 이 단어는 일반적으로 자신을 공부하는 과정을 의미합니다. 프로그래밍에서 이는 유사한 의미를 갖습니다. 즉, 프로그램이 실행되는 동안 프로그램에 대한 데이터를 분석하고 프로그램의 구조와 동작을 변경하는 메커니즘입니다. 반사의 예 - 1 여기서 중요한 것은 컴파일 타임이 아니라 런타임에 이 작업을 수행한다는 것입니다. 그런데 런타임에 코드를 검사하는 이유는 무엇입니까? 결국, 당신은 이미 코드를 읽을 수 있습니다. :/ 리플렉션의 아이디어가 즉시 명확하지 않을 수 있는 이유가 있습니다. 이 시점까지는 작업 중인 클래스를 항상 알고 있었습니다. 예를 들어 다음과 같이 클래스를 작성할 수 있습니다 Cat.
package learn.codegym;

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프로그램은 동물이 개인지 고양이인지에 따라 적절하게 처리했습니다. 이것이 가장 간단한 작업은 아니지만 프로그램은 컴파일 시간에 클래스에 대한 모든 필요한 정보를 학습할 수 있습니다. Cat이에 따라 동물병원 클래스의 메서드에 객체를 전달하면main()프로그램은 그것이 개가 아니라 고양이라는 것을 이미 알고 있습니다. 이제 우리가 다른 작업에 직면해 있다고 상상해 봅시다. 우리의 목표는 코드 분석기를 작성하는 것입니다. CodeAnalyzer단일 메소드로 클래스를 작성해야 합니다 . void analyzeObject(Object o). 이 방법은 다음을 수행해야 합니다.
  • 전달된 개체의 클래스를 결정하고 콘솔에 클래스 이름을 표시합니다.
  • 개인 필드를 포함하여 전달된 클래스의 모든 필드 이름을 결정하고 콘솔에 표시합니다.
  • 비공개를 포함하여 전달된 클래스의 모든 메서드 이름을 결정하고 콘솔에 표시합니다.
다음과 같이 표시됩니다.
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }

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

객체의 클래스를 식별/결정하는 방법

기본부터 시작하겠습니다. Java 리플렉션 엔진의 진입점은 클래스입니다 Class. 예, 정말 재미있어 보이지만 그것이 리플렉션입니다 :) Class클래스를 사용하여 먼저 메서드에 전달된 개체의 클래스를 결정합니다. 이렇게 해보자:
import learn.codegym.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("Fluffy", 6));
   }
}
콘솔 출력:
class learn.codegym.Cat
두 가지에 주의하십시오. 먼저 의도적으로 Cat클래스를 별도의 learn.codegym패키지에 넣습니다. getClass()이제 메서드가 클래스의 전체 이름을 반환하는 것을 볼 수 있습니다 . 둘째, 변수 이름을 clazz. 조금 이상해 보입니다. "클래스"라고 부르는 것이 합리적이지만 "클래스"는 Java에서 예약어입니다. 컴파일러는 변수를 호출하는 것을 허용하지 않습니다. 우리는 어떻게든 그 문제를 해결해야 했습니다 :) 처음에는 나쁘지 않았습니다! 그 기능 목록에 또 무엇이 있었습니까?

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

이제 상황이 더욱 흥미로워지고 있습니다! 현재 클래스에는 상수나 부모 클래스가 없습니다. 그것들을 추가하여 완전한 그림을 만들어 봅시다. 가장 간단한 Animal상위 클래스를 만듭니다.
package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
그리고 Cat클래스를 상속 Animal하고 하나의 상수를 추가합니다.
package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
이제 우리는 완전한 그림을 가지고 있습니다! 리플렉션이 무엇을 할 수 있는지 봅시다 :)
import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
다음은 콘솔에 표시되는 내용입니다.
Class name:  class learn.codegym.Cat
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age]
Parent class: class learn.codegym.Animal
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()]
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
우리가 얻을 수 있었던 자세한 수업 정보를 모두 살펴보십시오! 그리고 공개정보 뿐만 아니라 개인정보까지! 메모: private변수도 목록에 표시됩니다. 수업에 대한 우리의 "분석"은 본질적으로 완전한 것으로 간주될 수 있습니다. 우리는 analyzeObject()우리가 할 수 있는 모든 것을 배우기 위해 방법을 사용하고 있습니다. 그러나 이것이 우리가 성찰로 할 수 있는 전부는 아닙니다. 우리는 단순한 관찰에 그치지 않고 행동으로 옮길 것입니다! :)

프로그램이 실행될 때까지 클래스 이름을 알 수 없는 클래스의 인스턴스를 만드는 방법.

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

}
Cat다음은 리플렉션( createCat()메서드)을 사용하여 객체를 생성하는 코드입니다 .
import learn.codegym.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.codegym.Cat
콘솔 출력:
Cat{name='null', age=0}
이것은 오류가 아닙니다. name및 의 값은 클래스 의 메서드 age에서 출력하는 코드를 작성했기 때문에 콘솔에 표시됩니다 . 여기서 우리는 콘솔에서 생성할 개체의 클래스 이름을 읽습니다. 프로그램은 객체가 생성될 클래스의 이름을 인식합니다. 간결함을 위해 예제 자체보다 더 많은 공간을 차지하는 적절한 예외 처리 코드를 생략했습니다. 물론 실제 프로그램에서는 이름을 잘못 입력하는 등의 상황을 처리해야 합니다. 기본 생성자는 보시는 바와 같이 매우 간단하여 클래스의 인스턴스를 생성하는데 사용하기 쉽습니다 :) 메소드 이용 하기 , 이 클래스의 새 개체를 만듭니다. 다른 문제라면toString()Cat반사의 예 - 3newInstance()Cat생성자는 인수를 입력으로 사용합니다. 클래스의 기본 생성자를 제거하고 코드를 다시 실행해 보겠습니다.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
문제가 발생했습니다. 기본 생성자를 사용하여 객체를 생성하는 메서드를 호출했기 때문에 오류가 발생했습니다. 하지만 지금은 그런 생성자가 없습니다. 따라서 newInstance()메소드가 실행될 때 리플렉션 메커니즘은 두 개의 매개변수와 함께 이전 생성자를 사용합니다.
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
그러나 매개변수에 대해 완전히 잊은 것처럼 매개변수에 대해 아무 작업도 수행하지 않았습니다. 리플렉션을 사용하여 생성자에 인수를 전달하려면 약간의 "창의성"이 필요합니다.
import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 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='Fluffy', age=6}
프로그램에서 어떤 일이 일어나고 있는지 자세히 살펴보겠습니다. 객체 배열을 만들었습니다 Class.
Class[] catClassParams = {String.class, int.class};
생성자의 매개변수에 해당합니다( Stringint매개변수만 있음). 메서드 에 전달 clazz.getConstructor()하고 원하는 생성자에 대한 액세스 권한을 얻습니다. 그런 다음 필요한 newInstance()인수를 사용하여 메서드를 호출하고 객체를 원하는 유형으로 명시적으로 캐스팅하는 것을 잊지 마십시오 Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
이제 객체가 성공적으로 생성되었습니다! 콘솔 출력:
Cat{name='Fluffy', age=6}
바로 이동 :)

이름으로 인스턴스 필드의 값을 가져오고 설정하는 방법.

다른 프로그래머가 작성한 클래스를 사용하고 있다고 상상해 보십시오. 또한 편집할 수 없습니다. 예를 들어, JAR에 패키지된 기성품 클래스 라이브러리입니다. 클래스의 코드를 읽을 수는 있지만 변경할 수는 없습니다. 이 라이브러리에서 클래스 중 하나를 생성한 프로그래머(예전 클래스로 지정 Cat)가 설계가 완료되기 전날 밤에 충분한 수면을 취하지 못하고 필드의 getter 및 setter를 제거했다고 가정합니다 age. 이제 이 수업이 여러분을 찾아왔습니다. Cat프로그램에 개체 만 있으면 되므로 모든 요구 사항을 충족합니다 . 그러나 필드를 갖기 위해서는 그들이 필요합니다 age! 이것은 문제입니다. 필드에 도달할 수 없습니다.privatemodifier, getter 및 setter는 클래스를 생성한 수면 부족 개발자에 의해 삭제되었습니다. 클래스 에 대한 코드에 액세스할 수 있으므로 Cat적어도 클래스에 포함된 필드와 이름이 무엇인지 알아낼 수 있습니다. 이 정보로 무장하면 문제를 해결할 수 있습니다.
import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

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

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           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에 액세스합니다 . 연령 필드를 객체로 가져올 수 있습니다 . 그러나 단순히 필드에 값을 할당할 수 없기 때문에 이것만으로는 충분하지 않습니다 . 이렇게 하려면 다음 메서드를 사용하여 필드에 액세스할 수 있도록 해야 합니다. agegetDeclaredField()Field ageprivatesetAccessible()
age.setAccessible(true);
필드에 이 작업을 수행하면 값을 할당할 수 있습니다.
age.set(cat, 6);
보시다시피 Field age객체에는 int 값과 필드를 할당할 객체를 전달하는 일종의 뒤집힌 setter가 있습니다. 방법을 실행 main()하고 다음을 확인합니다.
Cat{name='Fluffy', age=6}
훌륭한! 우리는 해냈다! :) 우리가 무엇을 할 수 있는지 봅시다...

이름으로 인스턴스 메소드를 호출하는 방법.

이전 예에서 상황을 약간 변경해 보겠습니다. Cat클래스 개발자가 getter 및 setter에 실수를 하지 않았다고 가정해 보겠습니다 . 그런 점에서 모든 것이 괜찮습니다. 이제 문제는 다릅니다. 확실히 필요한 방법이 있지만 개발자가 비공개로 설정했습니다.
private void sayMeow() {

   System.out.println("Meow!");
}
즉, Cat프로그램에서 개체를 만들면 해당 메서드를 호출할 수 없습니다 sayMeow(). 우리는 야옹하지 않는 고양이를 가질거야? 이상하네요 :/ 어떻게 고칠까요? 다시 한 번 Reflection API가 도움이 됩니다! 필요한 메서드의 이름을 알고 있습니다. 다른 모든 것은 기술입니다.
import learn.codegym.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("Fluffy", 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();
   }
}
여기서 우리는 private 필드에 접근할 때 했던 것과 거의 같은 일을 합니다. 먼저 필요한 방법을 얻습니다. 개체 에 캡슐화됩니다 Method.
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
메서드 getDeclaredMethod()를 사용하면 비공개 메서드를 사용할 수 있습니다. 다음으로 메서드를 호출 가능하게 만듭니다.
sayMeow.setAccessible(true);
그리고 마지막으로 원하는 객체에서 메서드를 호출합니다.
sayMeow.invoke(cat);
여기에서 메서드 호출은 "콜백"처럼 보입니다. 원하는 메서드( )에서 개체를 가리키는 데 마침표를 사용하는 데 익숙 cat.sayMeow()하지만 리플렉션 작업을 할 때는 호출하려는 개체를 메서드에 전달합니다. 그 방법. 우리 콘솔에는 무엇이 있습니까?
Meow!
모든 것이 작동했습니다! :) 이제 Java의 리플렉션 메커니즘이 제공하는 방대한 가능성을 볼 수 있습니다. 어렵고 예상치 못한 상황(예: 폐쇄된 라이브러리의 클래스에 대한 예제)에서는 정말 많은 도움이 될 수 있습니다. 그러나 모든 위대한 힘과 마찬가지로 큰 책임을 수반합니다. 리플렉션의 단점은 Oracle 웹 사이트의 특별 섹션 에 설명되어 있습니다. 세 가지 주요 단점이 있습니다.
  1. 성능이 더 나쁩니다. 리플렉션을 사용하여 호출된 메서드는 일반적인 방식으로 호출된 메서드보다 성능이 떨어집니다.

  2. 보안 제한이 있습니다. 리플렉션 메커니즘을 사용하면 런타임 시 프로그램의 동작을 변경할 수 있습니다. 하지만 직장에서 실제 프로젝트를 진행하다 보면 이를 허용하지 않는 한계에 직면할 수 있습니다.

  3. 내부 정보 노출 위험. 리플렉션은 캡슐화 원칙에 대한 직접적인 위반임을 이해하는 것이 중요합니다. 리플렉션은 개인 필드, 메서드 등에 액세스할 수 있도록 합니다. OOP 원칙에 대한 직접적이고 노골적인 위반은 언급할 필요가 없다고 생각합니다. 귀하가 통제할 수 없는 이유로 문제를 해결할 다른 방법이 없는 가장 극단적인 경우에만.

반사를 피할 수 없는 상황에서만 현명하게 사용하고 단점을 잊지 마십시오. 이것으로 우리 수업은 끝났습니다. 꽤 길었지만 오늘 많이 배웠습니다 :)
코멘트
  • 인기
  • 신규
  • 이전
코멘트를 남기려면 로그인 해야 합니다
이 페이지에는 아직 코멘트가 없습니다