본문 바로가기
Java Tutorials

#29 The Reflection API:Classes

by xogns93 2024. 8. 13.


[튜토리얼]

 

Uses of Reflection

리플렉션은 JVM(Java Virtual Machine)에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정하는 기능이 필요한 프로그램에서 일반적으로 사용됩니다. 이는 비교적 고급 기능이므로 언어의 기본 사항을 잘 이해하고 있는 개발자만 사용해야 합니다. 이러한 주의 사항을 염두에 두고 리플렉션은 강력한 기술이며 응용 프로그램이 그렇지 않으면 불가능한 작업을 수행할 수 있도록 할 수 있습니다.

 

Extensibility Features[확장성 기능]

애플리케이션은 외부에서 정의된 사용자 클래스의 인스턴스를 완전한 이름을 사용하여 생성함으로써 확장성 객체를 활용할 수 있습니다.

 

Class Browsers and Visual Development Environments

클래스 브라우저는 클래스의 멤버를 나열할 수 있어야 합니다. 비주얼 개발 환경은 리플렉션을 통해 얻을 수 있는 타입 정보를 활용하여 개발자가 올바른 코드를 작성할 수 있도록 도울 수 있습니다.

 

Debuggers and Test Tools

디버거는 클래스의 private 멤버를 검사할 수 있어야 합니다. 테스트 하네스는 리플렉션을 사용하여 클래스에 정의된 API를 체계적으로 호출함으로써 테스트 스위트에서 높은 수준의 코드 커버리지를 보장할 수 있습니다.


Drawbacks of Reflection[리플렉션의 단점]

리플렉션은 강력하지만 무분별하게 사용해서는 안 됩니다. 리플렉션을 사용하지 않고 작업을 수행할 수 있다면, 이를 피하는 것이 바람직합니다. 리플렉션을 통해 코드를 접근할 때 다음과 같은 우려 사항을 염두에 두어야 합니다.

성능 오버헤드

리플렉션은 동적으로 해석되는 타입을 포함하기 때문에 특정 자바 가상 머신 최적화를 수행할 수 없습니다. 결과적으로, 리플렉션 작업은 비리플렉션 작업보다 성능이 느리며, 성능에 민감한 애플리케이션에서 자주 호출되는 코드 섹션에서는 피해야 합니다.

보안 제한

리플렉션은 런타임 권한을 필요로 하며, 이는 보안 관리자가 있는 환경에서는 제공되지 않을 수 있습니다. 이는 애플릿과 같이 제한된 보안 컨텍스트에서 실행해야 하는 코드에 대해 중요한 고려 사항입니다.

내부 노출

리플렉션은 private 필드 및 메서드 접근과 같이 비리플렉션 코드에서는 불법적인 작업을 수행할 수 있으므로, 예상치 못한 부작용을 초래할 수 있으며, 이는 코드의 기능을 저해하고 이식성을 파괴할 수 있습니다. 리플렉션 코드는 추상을 깨트리므로 플랫폼 업그레이드 시 동작이 변경될 수 있습니다.

 

Lesson: Classes

모든 타입은 참조 타입 또는 기본 타입 중 하나입니다. 클래스, 열거형, 배열(모두 java.lang.Object를 상속받음)뿐만 아니라 인터페이스도 참조 타입입니다. 참조 타입의 예로는 java.lang.String, 모든 기본 타입의 래퍼 클래스들(예: java.lang.Double), 인터페이스 java.io.Serializable, enum javax.swing.SortOrder 등이 있습니다. 기본 타입에는 boolean, byte, short, int, long, char, float, double이 있습니다.

모든 객체 타입에 대해, 자바 가상 머신은 해당 객체의 런타임 속성(멤버 및 타입 정보 포함)을 검사할 수 있는 메서드를 제공하는 java.lang.Class의 immutable 인스턴스를 인스턴스화합니다. Class는 새로운 클래스 및 객체를 생성할 수 있는 기능도 제공합니다. 가장 중요한 것은 모든 리플렉션 API의 진입점이라는 점입니다. 이 레슨에서는 클래스와 관련된 가장 일반적으로 사용되는 리플렉션 작업을 다룹니다:

  클래스 객체 가져오기: Class<T> 객체를 얻는 다양한 방법을 설명합니다.
  클래스 수정자 및 타입 검사: 클래스 선언 정보를 접근하는 방법을 보여줍니다.
  클래스 멤버 발견: 클래스 내의 생성자, 필드, 메서드 및 중첩 클래스를 나열하는 방법을 설명합니다.
  문제 해결: Class 사용 시 자주 발생하는 오류를 설명합니다.

 

Retrieving Class Objects

모든 리플렉션 작업의 진입점은 java.lang.Class입니다. java.lang.reflect.ReflectPermission을 제외하고, java.lang.reflect 패키지의 클래스들은 public 생성자를 가지고 있지 않습니다. 이러한 클래스에 접근하기 위해서는 Class의 적절한 메서드를 호출해야 합니다. 객체, 클래스 이름, 타입, 또는 기존의 Class에 접근할 수 있는지 여부에 따라 Class를 얻는 여러 가지 방법이 있습니다.

 

Object.getClass()

객체의 인스턴스가 존재하는 경우, 가장 간단하게 Class 객체를 얻는 방법은 Object.getClass() 메서드를 호출하는 것입니다. 물론, 이는 모든 참조 타입에 대해서만 동작하며, 모든 참조 타입은 Object로부터 상속받습니다. 다음은 몇 가지 예입니다.

Class c = "foo".getClass();
// String 클래스에 대한 Class 객체를 반환합니다.

 

Class c = System.console().getClass();
// 가상 머신과 연관된 고유한 콘솔이 있으며, 이는 정적 메서드 System.console()에 의해 반환됩니다.
// getClass()가 반환하는 값은 java.io.Console에 해당하는 Class입니다.

 

enum E { A, B }
Class c = A.getClass();
// A는 enum E의 인스턴스입니다. 따라서 getClass()는 열거형 타입 E에 해당하는 Class를 반환합니다.

 

byte[] bytes = new byte[1024];
Class c = bytes.getClass();
// 배열도 객체이므로 배열 인스턴스에 대해 getClass()를 호출할 수 있습니다.
// 반환된 Class는 byte 타입의 구성 요소를 갖는 배열에 해당합니다.

 

import java.util.HashSet;
import java.util.Set;

Set<String> s = new HashSet<String>();
Class c = s.getClass();
// 이 경우, java.util.Set은 java.util.HashSet 타입의 객체에 대한 인터페이스입니다.
// getClass()가 반환하는 값은 java.util.HashSet에 해당하는 클래스입니다.

 

The .class Syntax

타입이 사용 가능하지만 인스턴스가 없는 경우, 타입 이름에 ".class"를 붙여서 Class 객체를 얻을 수 있습니다. 이것은 원시 타입의 Class를 얻는 가장 쉬운 방법이기도 합니다.

boolean b;
Class c = b.getClass();   // 컴파일 타임 에러

Class c = boolean.class;  // 올바른 코드


boolean은 원시 타입이므로 boolean.getClass() 문장은 컴파일 타임 에러를 발생시킵니다. .class 구문은 boolean 타입에 해당하는 Class를 반환합니다.

Class c = java.io.PrintStream.class;


변수 c는 java.io.PrintStream 타입에 해당하는 Class가 됩니다.

Class c = int[][][].class;


.class 구문을 사용하여 주어진 타입의 다차원 배열에 해당하는 Class를 가져올 수 있습니다.

 

Class.forName()

클래스의 전체 이름(fully-qualified name)이 제공된 경우, 정적 메서드 Class.forName()을 사용하여 해당 클래스의 Class 객체를 얻을 수 있습니다. 이 방법은 원시 타입에는 사용할 수 없습니다. 배열 클래스의 이름 구문은 Class.getName()에 의해 설명됩니다. 이 구문은 참조 타입과 원시 타입에 모두 적용됩니다.

Class c = Class.forName("com.duke.MyLocaleServiceProvider");


이 문장은 주어진 전체 이름으로부터 클래스를 생성합니다.

 

여기서 말하는 전체 이름(fully-qualified name)은 패키지명까지 포함된 것을 말합니다. 예를 들어, 클래스 MyLocaleServiceProvider com.duke 패키지에 있다면, 이 클래스의 전체 이름은 com.duke.MyLocaleServiceProvider가 됩니다.
전체 이름을 사용하는 이유는 자바에서 동일한 이름을 가진 클래스가 여러 패키지에 있을 수 있기 때문에, 패키지명을 포함함으로써 정확하게 어떤 클래스를 참조하는지 명확히 하기 위함입니다.
다음은 예제 코드입니다:
Class c = Class.forName("com.duke.MyLocaleServiceProvider");
이 코드는 com.duke 패키지에 있는 MyLocaleServiceProvider 클래스를 로드합니다. 전체 이름을 통해 자바 런타임은 해당 클래스를 정확히 식별하고 로드할 수 있습니다.

 

Class cDoubleArray = Class.forName("[D");
Class cStringArray = Class.forName("[[Ljava.lang.String;");


변수 cDoubleArray는 기본 타입 double의 배열에 해당하는 Class를 포함합니다(즉, double[].class와 동일합니다). cStringArray 변수는 String의 2차원 배열에 해당하는 Class를 포함합니다(즉, String[][].class와 동일합니다).

Class.forName("[D")에서 [는 배열을 나타내는 특수한 표기법입니다. 이 표기법은 자바의 내부 클래스 이름 규칙을 따릅니다. 자바에서 배열의 타입을 표현할 때는 앞에 대괄호 [를 사용하여 배열임을 나타냅니다.
구체적으로:
⦁  [D는 원시 타입 double의 1차원 배열을 나타냅니다. 여기서 D double 타입을 의미합니다.
⦁  예를 들어 double[] 타입은 [D로 표현됩니다.
이와 같은 규칙은 다차원 배열에도 적용됩니다:
⦁  [[Ljava.lang.String;는 String 타입의 2차원 배열을 나타냅니다.
    ⦁ 첫 번째 [는 배열임을 나타내고, 두 번째 [는 다차원 배열임을 나타냅니다.   
    ⦁ L은 참조 타입을 나타내며, Ljava.lang.String;는 String 클래스를 의미합니다.
이 표기법을 사용하면 Class.forName 메서드를 통해 배열 타입의 Class 객체를 얻을 수 있습니다. 예를 들어:
Class cDoubleArray = Class.forName("[D"); // double[] 타입의 Class 객체
Class cStringArray = Class.forName("[[Ljava.lang.String;"); // String[][] 타입의 Class 객체 
위 코드는 각각 double 배열과 String 2차원 배열의 Class 객체를 반환합니다.

 

TYPE Field for Primitive Type Wrappers

.class 구문은 기본 타입의 Class 객체를 얻는 더 편리하고 선호되는 방법입니다. 그러나 기본 타입의 Class 객체를 얻는 또 다른 방법도 있습니다. 각 기본 타입과 void에는 자바의 java.lang 패키지에 해당하는 래퍼 클래스가 있으며, 이 클래스들은 원시 타입을 참조 타입으로 박싱하는 데 사용됩니다. 각 래퍼 클래스에는 해당 기본 타입의 Class와 동일한 TYPE이라는 필드가 포함되어 있습니다.

Class c = Double.TYPE;


java.lang.Double 클래스는 기본 타입 double을 객체가 필요한 경우 래핑하는 데 사용됩니다. Double.TYPE의 값은 double.class와 동일합니다.

Class c = Void.TYPE;


Void.TYPE void.class와 동일합니다.

 

Methods that Return Classes

리플렉션 API 중 몇 가지는 클래스를 반환하지만, 이러한 API는 직접적으로나 간접적으로 이미 클래스 객체를 얻은 경우에만 접근할 수 있습니다.

Class.getSuperclass()
주어진 클래스의 슈퍼 클래스를 반환합니다.

Class c = java.lang.String.class.getSuperclass();

 

java.lang.String의 슈퍼 클래스는 javax.lang.Object입니다.

Class.getClasses()
클래스의 모든 public 클래스, 인터페이스 및 enum을 상속된 멤버를 포함하여 반환합니다.

Class<?>[] cs = Character.class.getClasses();
for (Class clazz:cs)
    System.out.println(clazz.getTypeName());

 

java.lang.Character$UnicodeBlock
java.lang.Character$UnicodeScript
java.lang.Character$Subset


Character 클래스에는 두 개의 static nested 클래스 Character.Subset과 Character.UnicodeBlock 그리고 Character .UnicodeScript가 포함되어 있습니다.

Class.getDeclaredClasses()
이 클래스에 명시적으로 선언된 모든 클래스, 인터페이스 및 enum을 반환합니다.

Class<?>[] c = Character.class.getDeclaredClasses();


Character 클래스에는 두 개의 public 멤버 클래스 Character.Subset Character.UnicodeBlock` 및 하나의 private 클래스 Character.CharacterCache가 포함되어 있습니다.

Class.getDeclaringClass()


  java.lang.reflect.Field.getDeclaringClass()
  java.lang.reflect.Method.getDeclaringClass()
  java.lang.reflect.Constructor.getDeclaringClass()

이 멤버들이 선언된 클래스를 반환합니다. 익명 클래스 선언은 선언된 클래스가 없지만 포함된 클래스는 있을 수 있습니다.

import java.lang.reflect.Field;

Field f = System.class.getField("out");
Class c = f.getDeclaringClass();


out 필드는 System 클래스에 선언되어 있습니다.

public class MyClass {
    static Object o = new Object() {
        public void m() {} 
    };
    static Class<?> c = o.getClass().getEnclosingClass();
}


o에 의해 정의된 익명 클래스의 선언 클래스는 null입니다.

Class.getEnclosingClass()
클래스의 즉시 포함된 클래스를 반환합니다.

Class c = Thread.State.class.getEnclosingClass();


Thread.State enum의 포함된 클래스는 Thread입니다.

public class MyClass {
    static Object o = new Object() { 
        public void m() {} 
    };
    static Class<?> c = o.getClass().getEnclosingClass();
}


o에 의해 정의된 익명 클래스는 MyClass에 포함됩니다.

 

 

Examining Class Modifiers and Types

클래스는 런타임 동작에 영향을 미치는 하나 이상의 수정자로 선언될 수 있습니다:

  액세스 수정자: public, protected, private
  override[재정]를 요구하는 수정자: abstract
  하나의 인스턴스로 제한하는 수정자: static
  값 수정을 금지하는 수정자: final
  엄격한 부동 소수점 동작을 강제하는 수정자: strictfp
  Annotations

모든 액세스 수정자가 모든 클래스에 허용되는 것은 아닙니다. 예를 들어, 인터페이스는 final일 수 없고, 열거형(enum)은 abstract일 수 없습니다. java.lang.reflect.Modifier에는 모든 가능한 수정자에 대한 선언이 포함되어 있습니다. 또한 Class.getModifiers()가 반환하는 수정자 세트를 디코드할 수 있는 메서드도 포함되어 있습니다.

ClassDeclarationSpy 예제는 수정자, 제네릭 타입 파라미터, 구현된 인터페이스, 상속 경로를 포함하여 클래스 선언 구성 요소를 얻는 방법을 보여줍니다. Class java.lang.reflect.AnnotatedElement 인터페이스를 구현하기 때문에 런타임 어노테이션을 쿼리하는 것도 가능합니다.

package com.intheeast.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import static java.lang.System.out;

public class ClassDeclarationSpy {
	
    public static void main(String... args) {
    	
        try {
        	
            Class<?> c = Class.forName(args[0]);
            out.format("Class:%n  %s%n%n", c.getCanonicalName());
            out.format("Modifiers:%n  %s%n%n",
               Modifier.toString(c.getModifiers()));

            out.format("Type Parameters:%n");
            TypeVariable[] tv = c.getTypeParameters();
            if (tv.length != 0) {
        		out.format("  ");
        		for (TypeVariable t : tv)
            		out.format("%s ", t.getName());
        		out.format("%n%n");
            } else {
        		out.format("  -- No Type Parameters --%n%n");
            }

            out.format("Implemented Interfaces:%n");
            Type[] intfs = c.getGenericInterfaces();
            if (intfs.length != 0) {
        		for (Type intf : intfs)
            		out.format("  %s%n", intf.toString());
        		out.format("%n");
            } else {
        		out.format("  -- No Implemented Interfaces --%n%n");
            }

            out.format("Inheritance Path:%n");
            List<Class> l = new ArrayList<Class>();
            printAncestor(c, l);
            if (l.size() != 0) {
				for (Class<?> cl : l)
					out.format("  %s%n", cl.getCanonicalName());
				out.format("%n");
            } else {
				out.format("  -- No Super Classes --%n%n");
            }

            out.format("Annotations:%n");
            Annotation[] ann = c.getAnnotations();
            if (ann.length != 0) {
				for (Annotation a : ann)
					out.format("  %s%n", a.toString());
				out.format("%n");
            } else {
				out.format("  -- No Annotations --%n%n");
            }

        // production code should handle this exception more gracefully
        } catch (ClassNotFoundException x) {
            x.printStackTrace();
        }
    }

    private static void printAncestor(Class<?> c, List<Class> l) {
		Class<?> ancestor = c.getSuperclass();
		if (ancestor != null) {
			l.add(ancestor);
			printAncestor(ancestor, l);
		}
    }
}

 

Class 클래스의 오브젝트 메소드 getCanonicalName : 
기본 클래스의 정규 이름을 Java 언어 사양에서 정의한 대로 반환합니다. 기본 클래스에 정규 이름이 없는 경우 null을 반환합니다.

기본 클래스의 정규 이름(canonical name)이란, 클래스의 고유하고 표준화된 이름을 말합니다. 이는 패키지 이름을 포함한 완전한 이름으로, Java 언어 사양에 따라 정의됩니다. 예를 들어, java.util.ArrayList ArrayList 클래스의 정규 이름입니다. 정규 이름이 없는 클래스의 예는 다음과 같습니다:
1. 지역 클래스: 메서드 내부에 정의된 클래스입니다.
2. 익명 클래스: 이름이 없는 즉석에서 정의된 클래스입니다.
3. 배열 클래스: 배열 타입의 클래스입니다. 예를 들어, `String[]`은 정규 이름이 없습니다.
정규 이름은 클래스의 명확한 식별을 위해 사용되며, 리플렉션을 통해 클래스를 다룰 때 중요한 역할을 합니다.

 

Modifier Class
Modifier 클래스는 클래스 및 멤버 액세스 수정자를 디코드하기 위한 정적 메서드와 상수를 제공합니다. 수정자 세트는 서로 다른 수정자를 나타내는 고유한 비트 위치가 있는 정수로 표현됩니다. 수정자를 나타내는 상수 값은 Java 가상 머신 사양(The Java Virtual Machine Specification) 섹션 4.1, 4.4, 4.5, 4.7의 표에서 가져옵니다.

 

다음은 출력의 몇 가지 예입니다. 사용자 입력은 이탤릭체로 표시됩니다.

java ClassDeclarationSpy java.util.concurrent.ConcurrentNavigableMap

Class:
  java.util.concurrent.ConcurrentNavigableMap

Modifiers:
  public abstract interface

Type Parameters:
  K V

Implemented Interfaces:
  java.util.concurrent.ConcurrentMap<K, V>
  java.util.NavigableMap<K, V>

Inheritance Path:
  -- No Super Classes --

Annotations:
  -- No Annotations --


다음은 소스 코드에서 java.util.concurrent.ConcurrentNavigableMap의 실제 선언입니다:

public interface ConcurrentNavigableMap<K,V>
    extends ConcurrentMap<K,V>, NavigableMap<K,V>


ConcurrentNavigableMap은 인터페이스입니다, 인터페이스는 암묵적으로 abstract입니다. 컴파일러는 모든 인터페이스에 대해 이 수정자를 추가합니다. 또한 이 선언에는 두 개의 제네릭 타입 타입파라미터 K와 V가 포함되어 있습니다. 예제 코드는 이 파라미터의 이름만 출력하지만, java.lang.reflect.TypeVariable의 메서드를 사용하여 추가 정보를 검색할 수 있습니다. 인터페이스는 위와 같이 다른 인터페이스를 구현할 수도 있습니다.

위 콘솔의 출력중에,
Modifiers: public abstract interface
부분에 대한 설명으로 
"인터페이스는 암묵적으로 abstract입니다. 컴파일러는 모든 인터페이스에 대해 이 수정자를 추가합니다."
라고 언급하고 있습니다


$ java ClassDeclarationSpy "[Ljava.lang.String;"

Class:
  java.lang.String[]

Modifiers:
  public abstract final

Type Parameters:
  -- No Type Parameters --

Implemented Interfaces:
  interface java.lang.Cloneable
  interface java.io.Serializable

Inheritance Path:
  java.lang.Object

Annotations:
  -- No Annotations --


배열은 런타임 객체이므로 모든 타입 정보는 Java 가상 머신에 의해 정의됩니다. 특히, 배열은 Cloneable 및 java.io.Serializable을 구현하며, 배열의 직접적인 슈퍼클래스는 항상 Object입니다.

$ java ClassDeclarationSpy java.io.InterruptedIOException

Class:
  java.io.InterruptedIOException

Modifiers:
  public

Type Parameters:
  -- No Type Parameters --

Implemented Interfaces:
  -- No Implemented Interfaces --

Inheritance Path:
  java.io.IOException
  java.lang.Exception
  java.lang.Throwable
  java.lang.Object

Annotations:
  -- No Annotations --


상속 경로[Inheritance Path]에서 java.io.InterruptedIOException이 체크 예외임을 알 수 있습니다. 왜냐하면 RuntimeException이 없기 때문입니다.

$ java ClassDeclarationSpy java.security.Identity

Class:
  java.security.Identity

Modifiers:
  public abstract

Type Parameters:
  -- No Type Parameters --

Implemented Interfaces:
  interface java.security.Principal
  interface java.io.Serializable

Inheritance Path:
  java.lang.Object

Annotations:
  @java.lang.Deprecated()


이 출력은 java.security.Identity java.lang.Deprecated 어노테이션을 가지고 있음을 보여줍니다. 이는 리플렉션 코드를 통해 deprecated API를 감지하는 데 사용할 수 있습니다.

참고: 모든 어노테이션이 리플렉션을 통해 사용할 수 있는 것은 아닙니다. RUNTIME 보유 정책을 가진 어노테이션만 접근 가능합니다. 언어에 사전 정의된 세 가지 어노테이션 중 @Deprecated, @Override, @SuppressWarnings 중에서는 @Deprecated만 런타임에 사용할 수 있습니다.

 

Discovering Class Members

Class에서 필드, 메서드, 생성자에 접근하기 위한 두 가지 카테고리의 메서드가 있습니다: 이 멤버들을 열거하는 메서드 특정 멤버를 검색하는 메서드입니다. 또한, 클래스에 직접 선언된 멤버에 접근하는 메서드와 슈퍼인터페이스 및 슈퍼클래스에서 상속된 멤버를 검색하는 메서드는 구별됩니다. 다음 표는 모든 멤버 검색 메서드와 그 특성에 대한 요약을 제공합니다.

 

Class Methods for Locating Fields

Class API List of members? Inherited members? Private members?
getDeclaredField() no no yes
getField() no yes no
getDeclaredFields() yes no yes
getFields() yes yes no

 

Class Methods for Locating Methods

Class API List of members? Inherited members? Private members?
getDeclaredMethod() no no yes
getMethod() no yes no
getDeclaredMethods() yes no yes
getMethods() yes yes no

 

Class Methods for Locating Constructors

Class API List of members? Inherited members? Private members?
getDeclaredConstructor() no N/A(1) yes
getConstructor() no N/A(1) no
getDeclaredConstructors() yes N/A(1) yes
getConstructors() yes N/A(1) no

(1) Constructors are not inherited.

 

주어진 클래스 이름과 관심 있는 멤버의 표시를 바탕으로, ClassSpy 예제는 get*s() 메서드를 사용하여 상속된 멤버를 포함한 모든 공개 요소(public elements) 목록을 결정합니다.

package com.intheeast.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Member;
import static java.lang.System.out;

enum ClassMember { CONSTRUCTOR, FIELD, METHOD, CLASS, ALL }

public class ClassSpy {
	
    public static void main(String... args) {
		try {
			
		    Class<?> c = Class.forName(args[0]);
		    out.format("Class:%n  %s%n%n", c.getCanonicalName());
	
		    Package p = c.getPackage();
		    out.format("Package:%n  %s%n%n",
			       (p != null ? p.getName() : "-- No Package --"));
	
		    for (int i = 1; i < args.length; i++) {
			switch (ClassMember.valueOf(args[i])) {
			case CONSTRUCTOR:
			    printMembers(c.getConstructors(), "Constructor");
			    break;
			case FIELD:
			    printMembers(c.getFields(), "Fields");
			    break;
			case METHOD:
			    printMembers(c.getMethods(), "Methods");
			    break;
			case CLASS:
			    printClasses(c);
			    break;
			case ALL:
			    printMembers(c.getConstructors(), "Constuctors");
			    printMembers(c.getFields(), "Fields");
			    printMembers(c.getMethods(), "Methods");
			    printClasses(c);
			    break;
			default:
			    assert false;
			}
		    }
	
	        // production code should handle these exceptions more gracefully
		} catch (ClassNotFoundException x) {
		    x.printStackTrace();
		}
    }

    private static void printMembers(Member[] mbrs, String s) {
	out.format("%s:%n", s);
	for (Member mbr : mbrs) {
	    if (mbr instanceof Field)
		out.format("  %s%n", ((Field)mbr).toGenericString());
	    else if (mbr instanceof Constructor)
		out.format("  %s%n", ((Constructor)mbr).toGenericString());
	    else if (mbr instanceof Method)
		out.format("  %s%n", ((Method)mbr).toGenericString());
	}
	if (mbrs.length == 0)
	    out.format("  -- No %s --%n", s);
	out.format("%n");
    }

    private static void printClasses(Class<?> c) {
	out.format("Classes:%n");
	Class<?>[] clss = c.getClasses();
	for (Class<?> cls : clss)
	    out.format("  %s%n", cls.getCanonicalName());
	if (clss.length == 0)
	    out.format("  -- No member interfaces, classes, or enums --%n");
	out.format("%n");
    }
}

 

이 예제는 비교적 간결합니다. 그러나 printMembers() 메서드는 다소 불편한데, 이는 java.lang.reflect.Member 인터페이스가 가장 초기의 리플렉션 구현 이후로 존재했기 때문에 제네릭이 도입될 때 더 유용한 getGenericString() 메서드를 포함하도록 수정될 수 없었기 때문입니다. 가능한 대안은 다음과 같습니다: 예제에서 보이는 것처럼 테스트 및 캐스트를 수행하거나, 이 메서드를 printConstructors(), printFields(), printMethods()로 대체하거나, Member.getName()의 상대적으로 간결한 결과에 만족하는 것입니다.

출력 샘플과 해석은 다음과 같습니다. 사용자 입력은 이탤릭체로 표시됩니다.

 

$ java ClassSpy java.lang.ClassCastException CONSTRUCTOR

 

Class:
  java.lang.ClassCastException

Package:
  java.lang

Constructor:
  public java.lang.ClassCastException()
  public java.lang.ClassCastException(java.lang.String)

 

생성자는 상속되지 않기 때문에, 바로 상위 클래스인 RuntimeException 및 다른 상위 클래스에 정의된 예외 연결 메커니즘 생성자(즉, Throwable 파라미터를 가진 생성자)는 찾을 수 없습니다.


※자바에서 자식 클래스는 부모 클래스의 생성자를 자동으로 상속하지 않습니다. 부모 클래스의 생성자는 자식 클래스에서 명시적으로 호출해야 합니다. 이를 위해 자식 클래스의 생성자에서 super() 키워드를 사용하여 부모 클래스의 생성자를 호출할 수 있습니다.

다음은 간단한 예제입니다:

class Parent {
    public Parent() {
        System.out.println("Parent class no-arg constructor");
    }

    public Parent(String message) {
        System.out.println("Parent class constructor with message: " + message);
    }
}

class Child extends Parent {
    public Child() {
        // Explicitly calling the no-arg constructor of the parent class
        super();
        System.out.println("Child class no-arg constructor");
    }

    public Child(String message) {
        // Explicitly calling the parameterized constructor of the parent class
        super(message);
        System.out.println("Child class constructor with message: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child1 = new Child();
        Child child2 = new Child("Hello");
    }
}


출력:

Parent class no-arg constructor
Child class no-arg constructor
Parent class constructor with message: Hello
Child class constructor with message: Hello


이 예제에서 Child 클래스는 부모 클래스인 Parent 클래스의 생성자를 자동으로 상속받지 않습니다. 대신 super() 키워드를 사용하여 부모 클래스의 생성자를 명시적으로 호출하고 있습니다. `Child` 클래스의 각 생성자는 적절한 부모 클래스의 생성자를 호출하여 부모 클래스의 초기화 과정을 거칩니다.


 

$ java ClassSpy java.nio.channels.ReadableByteChannel METHOD

 

Class:
  java.nio.channels.ReadableByteChannel

Package:
  java.nio.channels

Methods:
  public abstract int java.nio.channels.ReadableByteChannel.read
    (java.nio.ByteBuffer) throws java.io.IOException
  public abstract void java.nio.channels.Channel.close() throws
    java.io.IOException
  public abstract boolean java.nio.channels.Channel.isOpen()

 

인터페이스 java.nio.channels.ReadableByteChannel은 read()를 정의합니다. 나머지 메서드는 슈퍼 인터페이스로부터 상속됩니다. 이 코드는 get*s()을 getDeclared*s()로 바꾸기만 하면 클래스에 실제로 선언된 메서드만 나열하도록 쉽게 수정될 수 있습니다.

$ java ClassSpy ClassMember FIELD METHOD
Class:
  ClassMember

Package:
  -- No Package --

Fields:
  public static final ClassMember ClassMember.CONSTRUCTOR
  public static final ClassMember ClassMember.FIELD
  public static final ClassMember ClassMember.METHOD
  public static final ClassMember ClassMember.CLASS
  public static final ClassMember ClassMember.ALL

Methods:
  public static ClassMember ClassMember.valueOf(java.lang.String)
  public static ClassMember[] ClassMember.values()
  public final int java.lang.Enum.hashCode()
  public final int java.lang.Enum.compareTo(E)
  public int java.lang.Enum.compareTo(java.lang.Object)
  public final java.lang.String java.lang.Enum.name()
  public final boolean java.lang.Enum.equals(java.lang.Object)
  public java.lang.String java.lang.Enum.toString()
  public static <T> T java.lang.Enum.valueOf
    (java.lang.Class<T>,java.lang.String)
  public final java.lang.Class<E> java.lang.Enum.getDeclaringClass()
  public final int java.lang.Enum.ordinal()
  public final native java.lang.Class<?> java.lang.Object.getClass()
  public final native void java.lang.Object.wait(long) throws
    java.lang.InterruptedException
  public final void java.lang.Object.wait(long,int) throws
    java.lang.InterruptedException
  public final void java.lang.Object.wait() hrows java.lang.InterruptedException
  public final native void java.lang.Object.notify()
  public final native void java.lang.Object.notifyAll()

 

위 결과의 필드 부분에는 열거형 상수가 나열됩니다. 이들은 기술적으로 필드이지만, 다른 필드와 구분하는 것이 유용할 수 있습니다. 이 예제는 java.lang.reflect.Field.isEnumConstant()를 사용하여 이러한 목적으로 수정될 수 있습니다. 이 트레일의 뒷부분에 있는 Enum을 조사하는 부분의 EnumSpy 예제에는 가능한 구현이 포함되어 있습니다.

위 출력의 메서드 부분에서는 메서드 이름에 선언 클래스의 이름이 포함

* ClassMember 클래스의 이름이 포함됨
ClassMember.valueOf(java.lang.String)

 

된다는 것을 알 수 있습니다. 따라서 toString() 메서드는 Object에서 상속된 것이 아니라 Enum에 의해 구현됩니다. 코드에 Field.getDeclaringClass()를 사용하여 이를 더 분명하게 만들도록 수정할 수 있습니다. 다음 단편은 잠재적인 해결책의 일부를 설명합니다.

if (mbr instanceof Field) {
    Field f = (Field)mbr;
    out.format("  %s%n", f.toGenericString());
    out.format("  -- declared in: %s%n", f.getDeclaringClass());
}