리플렉션 API는 무엇을 위한 것입니까?

Java의 리플렉션 메커니즘을 통해 개발자는 이름을 몰라도 런타임에 클래스, 인터페이스, 필드 및 메서드에 대한 정보를 변경하고 얻을 수 있습니다.

Reflection API를 사용하면 새 개체를 만들고, 메서드를 호출하고, 필드 값을 가져오거나 설정할 수도 있습니다.

리플렉션을 사용하여 수행할 수 있는 모든 작업의 ​​목록을 작성해 보겠습니다.

  • 객체의 클래스 식별/결정
  • 클래스 수정자, 필드, 메소드, 상수, 생성자 및 수퍼클래스에 대한 정보 얻기
  • 구현된 인터페이스에 속하는 메서드 찾기
  • 프로그램이 실행될 때까지 클래스 이름을 알 수 없는 클래스의 인스턴스를 만듭니다.
  • 이름으로 인스턴스 필드의 값 가져오기 및 설정
  • 이름으로 인스턴스 메서드 호출

거의 모든 최신 Java 기술은 리플렉션을 사용합니다. 오늘날 대부분의 Java/Java EE 프레임워크 및 라이브러리의 기반이 됩니다. 예를 들면 다음과 같습니다.

  • 웹 애플리케이션 구축을 위한 Spring 프레임워크
  • JUnit 테스트 프레임 워크

이전에 이러한 메커니즘을 접해본 적이 없다면 왜 이 모든 것이 필요한지 묻고 있을 것입니다. 대답은 매우 간단하지만 매우 모호합니다. 리플렉션은 유연성과 응용 프로그램 및 코드를 사용자 정의하는 기능을 크게 향상시킵니다.

그러나 항상 장단점이 있습니다. 몇 가지 단점을 언급해 보겠습니다.

  • 애플리케이션 보안 위반. 리플렉션을 사용하면 해서는 안 되는 코드에 액세스할 수 있습니다(캡슐화 위반).
  • 보안 제한. 리플렉션에는 보안 관리자를 실행하는 시스템에서 사용할 수 없는 런타임 권한이 필요합니다.
  • 성능이 낮습니다. Java의 리플렉션은 로드할 클래스를 찾기 위해 클래스 경로를 스캔하여 동적으로 유형을 결정합니다 . 이로 인해 프로그램의 성능이 저하됩니다.
  • 유지하기 어렵다. 리플렉션을 사용하는 코드는 읽고 디버그하기 어렵습니다. 유연성이 떨어지고 유지 관리가 더 어렵습니다.

Reflection API를 사용하여 클래스 작업

모든 리플렉션 작업은 java.lang.Class 객체로 시작됩니다. 객체의 각 유형에 대해 java.lang.Class 의 불변 인스턴스가 생성됩니다. 개체 속성 가져오기, 새 개체 만들기 및 메서드 호출을 위한 메서드를 제공합니다.

java.lang.Class 작업을 위한 기본 메서드 목록을 살펴보겠습니다 .

방법 행동
문자열 getName(); 클래스의 이름을 반환
int getModifiers(); 액세스 수정자를 반환합니다.
패키지 getPackage(); 패키지에 대한 정보를 반환합니다.
클래스 getSuperclass(); 상위 클래스에 대한 정보를 반환합니다.
클래스[] getInterfaces(); 인터페이스 배열을 반환합니다.
생성자[] getConstructors(); 클래스 생성자에 대한 정보를 반환합니다.
Fields[] getFields(); 클래스의 필드를 반환합니다.
필드 getField(String 필드명); 클래스의 특정 필드를 이름으로 반환
메소드[] getMethods(); 메서드 배열을 반환합니다.

이들은 클래스, 인터페이스, 필드 및 메소드에 대한 데이터를 가져오는 가장 중요한 메소드입니다. 필드 값을 가져오거나 설정하고 비공개 필드 에 액세스할 수 있는 메서드도 있습니다 . 잠시 후에 살펴보겠습니다.

바로 지금 우리는 java.lang.Class 자체를 가져오는 것에 대해 이야기할 것입니다. 세 가지 방법이 있습니다.

1. Class.forName 사용

실행 중인 애플리케이션에서 클래스를 가져오려면 forName(String className) 메서드를 사용해야 합니다 .

이 코드는 리플렉션을 사용하여 클래스를 만드는 방법을 보여줍니다. 작업할 수 있는 Person 클래스를 만들어 봅시다 .


package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

예제의 두 번째 부분은 리플렉션을 사용하는 코드입니다.


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

이 접근 방식은 클래스의 전체 이름을 알고 있는 경우에 가능합니다. 그런 다음 정적 Class.forName() 메서드를 사용하여 해당 클래스를 가져올 수 있습니다. 이 방법은 기본 유형에 사용할 수 없습니다.

2. .class 사용하기

유형을 사용할 수 있지만 인스턴스가 없는 경우 유형 이름에 .class를 추가하여 클래스를 가져올 수 있습니다. 이것은 기본 유형의 클래스를 얻는 가장 쉬운 방법입니다.


Class aClass = Person.class;

3. .getClass() 사용

객체를 사용할 수 있는 경우 클래스를 가져오는 가장 쉬운 방법은 object.getClass() 를 호출하는 것입니다 .


Person person = new Person();
Class aClass = person.getClass();

마지막 두 접근 방식의 차이점은 무엇입니까?

코딩 시 관심 있는 클래스 개체를 알고 있는 경우 A.class를 사용하십시오 . 사용 가능한 인스턴스가 없으면 .class 를 사용해야 합니다 .

클래스의 메서드 가져오기

클래스의 메서드를 반환하는 메서드인 getDeclaredMethods()getMethods() 를 살펴보겠습니다 .

getDeclaredMethods()는 public, private, default 및 protected 메서드를 포함하지만 상속된 메서드는 포함하지 않고 Class 객체가 나타내는 클래스 또는 인터페이스의 모든 선언된 메서드에 대한 메서드 객체를 포함하는 배열을 반환 합니다 .

getMethods() 는 Class 객체가 나타내는 클래스 또는 인터페이스의 모든 공개 메서드(클래스 또는 인터페이스에 의해 선언된 메서드와 수퍼클래스 및 수퍼인터페이스에서 상속된 메서드)에 대한 Method 객체를 포함하는 배열을 반환 합니다 .

각각의 작동 방식을 살펴보겠습니다.

getDeclaredMethods() 부터 시작하겠습니다 . 다시 두 방법의 차이점을 이해하는 데 도움이 되도록 아래에서 추상 Numbers 클래스 로 작업합니다 . Method 배열을 List<String> 으로 변환하는 정적 메서드를 작성해 보겠습니다 .


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

이 코드를 실행한 결과는 다음과 같습니다.

byteValue
shortValue
intValue
longValue
float floatValue;
이중값

이들은 Number 클래스 내부에 선언된 메소드입니다 . getMethods()는 무엇을 반환합니까? 예제에서 두 줄을 변경해 보겠습니다.


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

이렇게 하면 다음과 같은 메서드 집합이 표시됩니다.

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
대기
대기
대기 toString hashCode getClass notify notifyAll
_




모든 클래스가 Object 를 상속하기 때문에 메서드는 Object 클래스의 공용 메서드도 반환합니다.

클래스의 필드 가져오기

getFields 및 getDeclaredFields 메서드 클래스의 필드를 가져오는 데 사용됩니다. 예를 들어 LocalDateTime 클래스를 살펴보겠습니다 . 코드를 다시 작성합니다.


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

이 코드를 실행한 결과 LocalDateTime 클래스에 포함된 필드 집합을 얻습니다.

MIN
MAX
serialVersionUID
날짜
시간

이전의 메서드 검사와 유사하게 코드를 약간 변경하면 어떤 일이 발생하는지 살펴보겠습니다.


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

산출:

최소
최대

이제 이러한 방법의 차이점을 알아 보겠습니다.

getDeclaredFields 메소드 는 이 메소드로 표시되는 클래스 또는 인터페이스에 의해 선언된 모든 필드에 대한 Field 객체 의 배열을 반환합니다.수업물체.

getFields 메소드는 클래스 또는 인터페이스의 모든 공용 필드에 대한 Field 객체 배열을 반환합니다 .수업물체.

이제 LocalDateTime 내부를 살펴보겠습니다 .

클래스의그리고MAX필드는 공용이므로 getFields 메서드를 통해 볼 수 있습니다. 대조적으로,날짜,시간,직렬 버전 UID메서드에는 private 수정자가 있습니다 . 즉, getFields 메서드를 통해 표시되지 않지만 getDeclaredFields를 사용하여 가져올 수 있습니다 . 이것이 비공개 필드 에 대한 필드 개체에 액세스하는 방법입니다 .

다른 방법에 대한 설명

이제 Class 클래스 의 몇 가지 메서드에 대해 이야기할 시간입니다 .

방법 행동
getModifiers 클래스에 대한 수정자 가져오기
getPackage 클래스가 포함된 패키지 가져오기
getSuperclass 상위 클래스 가져오기
getInterfaces 클래스에 의해 구현된 인터페이스 배열 가져오기
getName 정규화된 클래스 이름 얻기
getSimpleName 클래스 이름 얻기

getModifiers()

수정자는 다음을 사용하여 액세스할 수 있습니다.수업물체.

수정자는 public , static , interface 등과 같은 키워드입니다 . getModifiers() 메서드를 사용하여 수정자를 얻습니다 .


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

이 코드는정수비트 필드인 변수입니다. 각 액세스 수정자는 해당 비트를 설정하거나 지워서 켜거나 끌 수 있습니다. java.lang.reflect.Modifier 클래스 의 메서드를 사용하여 수정자를 확인할 수 있습니다 .


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Person 선언이 어떻게 생겼는지 기억해 보세요.


public class Person {
   …
}

다음과 같은 결과를 얻습니다.

클래스 수정자: 1
공개 여부: true
정적 여부: false
최종 여부: false
추상 여부: false
인터페이스 여부: false

클래스를 추상화하면 다음과 같습니다.


public abstract class Person { … }

이 출력:

클래스 수정자: 1025
공개 여부: true
정적 여부: false
최종 여부: false
추상 여부: true
인터페이스 여부: false

액세스 한정자를 변경했습니다. 즉, Modifier 클래스 의 정적 메서드를 통해 반환되는 데이터도 변경했습니다 .

getPackage()

클래스만 알면 해당 패키지에 대한 정보를 얻을 수 있습니다.


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

Class 객체가 있으면 부모 클래스에 액세스할 수 있습니다.


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

우리는 잘 알려진 Object 클래스를 얻습니다.


class java.lang.Object

그러나 클래스에 다른 상위 클래스가 있는 경우 대신 표시됩니다.


package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

여기에서 부모 클래스를 얻습니다.


class com.company.Human

get인터페이스()

클래스에 의해 구현된 인터페이스 목록을 얻는 방법은 다음과 같습니다.


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

Person 클래스 를 변경하는 것을 잊지 마십시오 .


public class Person implements Serializable { … }

산출:

[인터페이스 java.io.Serializable]

클래스는 많은 인터페이스를 구현할 수 있습니다. 그래서 우리는 배열을 얻습니다.수업사물. Java Reflection API에서 인터페이스는 다음으로도 표시됩니다.수업사물.

참고: 메서드는 부모 클래스가 아닌 지정된 클래스에 의해 구현된 인터페이스만 반환합니다. 클래스에 의해 구현된 전체 인터페이스 목록을 얻으려면 현재 클래스와 상속 체인의 모든 조상을 모두 참조해야 합니다.

getName() & getSimpleName() & getCanonicalName()

프리미티브, 중첩 클래스, 익명 클래스 및 String 클래스 와 관련된 예제를 작성해 보겠습니다 .


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

우리 프로그램의 결과:

int 클래스(기본):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class(일반 클래스):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class(중첩 클래스):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass()(익명 내부 클래스):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

이제 프로그램의 출력을 분석해 보겠습니다.

  • getName()은 엔터티의 이름을 반환합니다.

  • getCanonicalName()은 Java 언어 사양에 정의된 기본 클래스의 표준 이름을 반환합니다. 기본 클래스에 정식 이름이 없는 경우(즉, 로컬 또는 익명 클래스이거나 요소 유형에 정식 이름이 없는 배열인 경우) null을 반환합니다.

  • getSimpleName()은 소스 코드에 지정된 기본 클래스의 간단한 이름을 반환합니다. 기본 클래스가 익명인 경우 빈 문자열을 반환합니다.

  • getTypeName()은 이 유형의 이름에 대한 정보 문자열을 반환합니다.