High Level Programming Language/Reflection

Lesson: Members[Constructors]

헬로우월드 2024. 7. 6. 19:49

[튜토리얼]

 

Constructors

생성자는 클래스의 인스턴스인 객체를 생성할 때 사용됩니다. 일반적으로 메서드가 호출되거나 필드에 접근되기 전에 클래스 초기화에 필요한 작업을 수행합니다. 생성자는 절대 상속되지 않습니다.

메서드와 유사하게, 리플렉션은 클래스의 생성자를 발견하고 검색하며, 제어자, 파라미터, 애노테이션, 던지는 예외와 같은 선언 정보를 얻기 위한 API를 제공합니다. 또한 지정된 생성자를 사용하여 클래스의 새 인스턴스를 생성할 수도 있습니다. 생성자를 다룰 때 사용하는 주요 클래스는 Classjava.lang.reflect.Constructor 입니다. 생성자와 관련된 일반적인 작업은 다음 섹션에서 다룹니다:

 

Finding Constructors

생성자 선언에는 이름, 제어자, 파라미터 개수 및 throw 가능한 예외 목록이 포함됩니다. java.lang.reflect.Constructor 클래스는 이 정보를 얻는 방법을 제공합니다.

ConstructorSift 예제는 클래스의 선언된 생성자에서 주어진 타입의 파라미터가 있는 생성자를 검색하는 방법을 보여줍니다.

import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import static java.lang.System.out;

public class ConstructorSift {
    public static void main(String... args) {
	try {
	    Class<?> cArg = Class.forName(args[1]);

	    Class<?> c = Class.forName(args[0]);
	    Constructor[] allConstructors = c.getDeclaredConstructors();
	    for (Constructor ctor : allConstructors) {
		Class<?>[] pType  = ctor.getParameterTypes();
		for (int i = 0; i < pType.length; i++) {
		    if (pType[i].equals(cArg)) {
			out.format("%s%n", ctor.toGenericString());

			Type[] gpType = ctor.getGenericParameterTypes();
			for (int j = 0; j < gpType.length; j++) {
			    char ch = (pType[j].equals(cArg) ? '*' : ' ');
			    out.format("%7c%s[%d]: %s%n", ch,
				       "GenericParameterType", j, gpType[j]);
			}
			break;
		    }
		}
	    }

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

 

Method.getGenericParameterTypes()는 메서드 파라미터의 제네릭 타입 정보를 제공합니다.
이 정보는 *.class 파일( Java 5 이전에 컴파일된 파일이 아니라면)의 Signature 속성에 저장되며, 해당 속성이 없으면 제네릭 정보는 소거되고 getParameterTypes()로 대체됩니다.
유사하게 getGenericReturnType()이나 Field.getGenericType()도 같은 원리로 동작합니다.

Method.get*Types()의 리턴 값에 대한 구문은 Class.getName()에 설명되어 있습니다.

다음은 Locale 아규먼트가 있는 java.util.Formatter의 모든 생성자에 대한 출력입니다.

$ java ConstructorSift java.util.Formatter java.util.Locale
public
java.util.Formatter(java.io.OutputStream,java.lang.String,java.util.Locale)
throws java.io.UnsupportedEncodingException
       GenericParameterType[0]: class java.io.OutputStream
       GenericParameterType[1]: class java.lang.String
      *GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.String,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
       GenericParameterType[0]: class java.lang.String
       GenericParameterType[1]: class java.lang.String
      *GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.Appendable,java.util.Locale)
       GenericParameterType[0]: interface java.lang.Appendable
      *GenericParameterType[1]: class java.util.Locale
public java.util.Formatter(java.util.Locale)
      *GenericParameterType[0]: class java.util.Locale
public java.util.Formatter(java.io.File,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
       GenericParameterType[0]: class java.io.File
       GenericParameterType[1]: class java.lang.String
      *GenericParameterType[2]: class java.util.Locale

다음 예제 출력은 문자열에서 char[] 타입의 파라미터를 검색하는 방법을 보여줍니다.

$ java ConstructorSift java.lang.String "[C"
java.lang.String(int,int,char[])
       GenericParameterType[0]: int
       GenericParameterType[1]: int
      *GenericParameterType[2]: class [C
public java.lang.String(char[],int,int)
      *GenericParameterType[0]: class [C
       GenericParameterType[1]: int
       GenericParameterType[2]: int
public java.lang.String(char[])
      *GenericParameterType[0]: class [C

Class.forName()에서 허용되는 참조 및 기본 타입의 배열을 표현하는 구문은 Class.getName()에 설명([L, [[L)되어 있습니다. 첫 번째로 나열된 생성자는 public이 아니라 package-private입니다. 이 생성자는 예제 코드가 public 생성자만 리턴하는 Class.getConstructors()가 아니라 Class.getDeclaredConstructors()를 사용하기 때문에 리턴됩니다.

이 예에서는 가변적인 arity(아리티)의 아규먼트를 검색하려면 배열 구문을 사용해야 함을 보여줍니다.

arity란? 함수가 요구하는 파라미터의 수 (argument의 개수)

$ java ConstructorSift java.lang.ProcessBuilder "[Ljava.lang.String;"
public java.lang.ProcessBuilder(java.lang.String[])
      *GenericParameterType[0]: class [Ljava.lang.String;

 

 다음은 소스 코드에서 ProcessBuilder 생성자의 실제 선언입니다.

public ProcessBuilder(String... command)

 

파라미터는 java.lang.String 타입의 1차원 배열로 표현됩니다. 이는 Constructor.isVarArgs()를 호출하여 명시적으로 java.lang.String 배열인 파라미터와 구별할 수 있습니다.

마지막 예제에서는 제네릭 파라티머 타입으로 선언된 생성자에 대한 출력을 보고합니다.

$ java ConstructorSift java.util.HashMap java.util.Map
public java.util.HashMap(java.util.Map<? extends K, ? extends V>)
      *GenericParameterType[0]: java.util.Map<? extends K, ? extends V>

 

생성자에 대한 예외 타입은 메서드와 비슷한 방식으로 검색할 수 있습니다. 자세한 내용은 Obtaining Method Type Information 섹션에 설명된 MethodSpy 예제를 참조하세요.

 

Retrieving and Parsing Constructor Modifiers

자바 언어에서 생성자의 역할 때문에 메서드보다 의미 있는 제어자가 적습니다.

  • Access modifiers: public, protected, and private
  • Annotations

ConstructorAccess  예제는 지정된 액세스 제어자가 있는 주어진 클래스에서 생성자를 검색합니다. 또한 생성자가 합성(컴파일러 생성)인지 가변 arity인지도 표시합니다.

 

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import static java.lang.System.out;

public class ConstructorAccess {
    public static void main(String... args) {
	try {
	    Class<?> c = Class.forName(args[0]);
	    Constructor[] allConstructors = c.getDeclaredConstructors();
	    for (Constructor ctor : allConstructors) {
		int searchMod = modifierFromString(args[1]);
		int mods = accessModifiers(ctor.getModifiers());
		if (searchMod == mods) {
		    out.format("%s%n", ctor.toGenericString());
		    out.format("  [ synthetic=%-5b var_args=%-5b ]%n",
			       ctor.isSynthetic(), ctor.isVarArgs());
		}
	    }

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

    private static int accessModifiers(int m) {
	return m & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED);
    }

    private static int modifierFromString(String s) {
	if ("public".equals(s))               return Modifier.PUBLIC;
	else if ("protected".equals(s))       return Modifier.PROTECTED;
	else if ("private".equals(s))         return Modifier.PRIVATE;
	else if ("package-private".equals(s)) return 0;
	else return -1;
    }
}

 

 "package-private" 액세스에 해당하는 명시적인 제어자 상수는 없으므로, package-private 생성자를 식별하려면 세 가지 액세스 제어자가 모두 없는지 확인해야 합니다.

 

이 출력은 java.io.File 의  private 생성자를 보여줍니다.
$ java ConstructorAccess java.io.File private
private java.io.File(java.lang.String,int)
  [ synthetic=false var_args=false ]
private java.io.File(java.lang.String,java.io.File)
  [ synthetic=false var_args=false ]

 

합성 생성자는 드믑니다. 그러나 SyntheticConstructor 예제는 이러한 상황이 발생할 수 있는 일반적인 상황을 보여준다.
public class SyntheticConstructor {
    private SyntheticConstructor() {}
    class Inner {
	// Compiler will generate a synthetic constructor since
	// SyntheticConstructor() is private.
	Inner() { new SyntheticConstructor(); }
    }
}

 

$ java ConstructorAccess SyntheticConstructor package-private
SyntheticConstructor(SyntheticConstructor$1)
  [ synthetic=true  var_args=false ]

 

내부 클래스의 생성자는 외부 클래스의 private 생성자를 참조하므로 컴파일러는 package-private 생성자를 생성해야 합니다. 파라미터 타입 SyntheticConstructor$1은 임의적이며 컴파일러 구현에 따라 달라집니다. 합성 또는 non-public 클래스 멤버의 존재에 따라 달라지는 코드는 이식할 수 없습니다.

 

생성자는 java.lang.reflect.AnnotatedElement를 구현하는데, 이는 java.lang.annotation.RetentionPolicy.RUNTIME으로 런타임 어노테이션을 검색하는 메서드를 제공합니다. 어노테이션을 얻는 예는 클래스 제어자 및 타입 검사 섹션을 참조하세요.

 

Creating New Class Instances

클래스 인스턴스를 만드는 데는 두 가지 리플렉션 방법이 있습니다. java.lang.reflect.Constructor.newInstance()Class.newInstance()입니다. 전자가 더 선호되며 따라서 이러한 예에서 사용되는 이유는 다음과 같습니다.

때때로 객체의 생성 후에만 설정되는 내부 상태를 가져오는 것이 필요할 수 있습니다. 예를 들어, java.io.Console 에서 사용되는 내부 문자 집합을 얻어야 하는 시나리오를 고려해보세요. (Console의 문자 집합은 private 필드에 저장되며, 자바 가상 머신의 기본 문자 집합인 java.nio.charset.Charset.defaultCharset()이 리턴하는 값과 반드시 동일하지는 않습니다.) ConsoleCharset 예제는 이것을 어떻게 달성할 수 있는지를 보여줍니다.

Console 클래스의 private 디폴트 컨스트럭터를 사용할 수 없습니다.

(Class.setAccessible(true)를 호출하면, InaccessibleObjectException 이 발생합니다.

그러므로 아래 코드는 InaccessibleObjectException이 발생하면서 실행이 종료됩니다)

import java.io.Console;
import java.nio.charset.Charset;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;

public class ConsoleCharset {
    public static void main(String... args) {
	Constructor[] ctors = Console.class.getDeclaredConstructors();
	Constructor ctor = null;
	for (int i = 0; i < ctors.length; i++) {
	    ctor = ctors[i];
	    if (ctor.getGenericParameterTypes().length == 0)
		break;
	}

	try {
	    ctor.setAccessible(true);
 	    Console c = (Console)ctor.newInstance();
	    Field f = c.getClass().getDeclaredField("cs");
	    f.setAccessible(true);
	    out.format("Console charset         :  %s%n", f.get(c));
	    out.format("Charset.defaultCharset():  %s%n",
		       Charset.defaultCharset());

        // production code should handle these exceptions more gracefully
	} catch (InstantiationException x) {
	    x.printStackTrace();
 	} catch (InvocationTargetException x) {
 	    x.printStackTrace();
	} catch (IllegalAccessException x) {
	    x.printStackTrace();
	} catch (NoSuchFieldException x) {
	    x.printStackTrace();
	}
    }
}

다음 예시로 위 코드를 대체합니다.

public class Foo {

	private String name;
	
	private Foo() {
		name = "hello";
	}
	
	public Foo(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

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

 

import static java.lang.System.out;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.lang.reflect.InvocationTargetException;

public class FooName {
	
	public static void getFooField() throws NoSuchMethodException, Exception {
		
		Constructor<?>[] ctors = 
				 Foo.class.getDeclaredConstructors();
		Constructor<?> ctor = null;
		for (int i = 0; i < ctors.length; i++) {
		    ctor = ctors[i];
		    if (ctor.getGenericParameterTypes().length == 0)
		    	break;
		}
		
		try {
			boolean isSet = ctor.trySetAccessible();
			if (isSet) {
				ctor.setAccessible(true);
//				Foo foo = new Foo();
			    Foo c = (Foo)ctor.newInstance();
			    Field f = c.getClass().getDeclaredField("name");
			    if (isSet = f.trySetAccessible()) {
				    f.setAccessible(true);
				    
				    out.format("Foo name         :  %s%n", f.get(c));
			    }
			}   
	
	        // production code should handle these exceptions more gracefully
		} catch (InstantiationException x) {
		    x.printStackTrace();
	 	} catch (InvocationTargetException x) {
	 	    x.printStackTrace();
		} catch (IllegalAccessException x) {
		    x.printStackTrace();
		} catch (NoSuchFieldException x) {
		    x.printStackTrace();
		} catch (InaccessibleObjectException x) {
			x.printStackTrace();
		}

	}
	
    public static void main(String... args) throws Exception {

    	getFooField();

    }
}
 

 

Another common application of Constructor.newInstance() is to invoke constructors which take arguments. The RestoreAliases example finds a specific single-argument constructor and invokes it:
Constructor.newInstance()의 또 다른 일반적인 응용은 아규먼트를 취하는 생성자를 호출하는 것입니다. RestoreAliases 예제는 특정 단일 아규먼트 생성자를 찾아 호출합니다.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.lang.System.out;

class EmailAliases {
    private Set<String> aliases;
    private EmailAliases(HashMap<String, String> h) {
	aliases = h.keySet();
    }

    public void printKeys() {
	out.format("Mail keys:%n");
	for (String k : aliases)
	    out.format("  %s%n", k);
    }
}

public class RestoreAliases {

    private static Map<String, String> defaultAliases = new HashMap<String, String>();
    static {
	defaultAliases.put("Duke", "duke@i-love-java");
	defaultAliases.put("Fang", "fang@evil-jealous-twin");
    }

    public static void main(String... args) {
	try {
	    Constructor ctor = EmailAliases.class.getDeclaredConstructor(HashMap.class);
	    ctor.setAccessible(true);
	    EmailAliases email = (EmailAliases)ctor.newInstance(defaultAliases);
	    email.printKeys();

        // production code should handle these exceptions more gracefully
	} catch (InstantiationException x) {
	    x.printStackTrace();
	} catch (IllegalAccessException x) {
	    x.printStackTrace();
	} catch (InvocationTargetException x) {
	    x.printStackTrace();
	} catch (NoSuchMethodException x) {
	    x.printStackTrace();
	}
    }
}

 

This example uses Class.getDeclaredConstructor() to find the constructor with a single argument of type java.util.HashMap. Note that it is sufficient to pass HashMap.class since the parameter to any get*Constructor() method requires a class only for type purposes. Due to type erasure, the following expression evaluates to true:

이 예제에서는 Class.getDeclaredConstructor()를 사용하여 java.util.HashMap 타입의 단일 아규먼트가 있는 생성자를 찾습니다. get*Constructor() 메서드의 파라미터는 타입 목적으로만 클래스가 필요하므로 HashMap.class를 전달하는 것으로 충분합니다. type erasure로 인해 다음 expression은 true로 평가됩니다.

HashMap.class == defaultAliases.getClass()

 

위 예제는 그런 다음 Constructor.newInstance()를 사용하여 해당 생성자를 통해 클래스의 새 인스턴스를 생성합니다.

$ java RestoreAliases
Mail keys:
  Duke
  Fang