더 안전하고 깨끗한 코드를 위해 상수 유형 사용

이 튜토리얼에서는 Eric Armstrong의 "Create enumerated constants in Java"에 설명 된대로 열거 형 상수 에 대한 개념을 확장합니다 . 열거 형 상수와 관련된 개념에 익숙하다고 가정하고 Eric이 제시 한 예제 코드 중 일부를 확장 할 것이므로이 기사에 몰입하기 전에 해당 기사를 읽는 것이 좋습니다.

상수의 개념

열거 형 상수를 다룰 때이 기사의 끝에서 개념 의 열거 된 부분에 대해 논의 할 것 입니다. 지금은 지속적인 측면 에만 집중하겠습니다 . 상수는 기본적으로 값을 변경할 수없는 변수입니다. C / C ++에서 키워드 const는 이러한 상수 변수를 선언하는 데 사용됩니다. Java에서는 키워드를 사용합니다 final. 그러나 여기에 소개 된 도구는 단순한 기본 변수가 아닙니다. 실제 개체 인스턴스입니다. 개체 인스턴스는 변경 불가능하고 변경할 수 없습니다. 내부 상태는 수정할 수 없습니다. 이것은 클래스가 하나의 인스턴스 만 가질 수있는 싱글 톤 패턴과 유사합니다. 그러나이 경우 클래스에는 제한되고 미리 정의 된 인스턴스 집합 만있을 수 있습니다.

상수를 사용하는 주된 이유는 명확성과 안전성입니다. 예를 들어, 다음 코드는 자명하지 않습니다.

public void setColor (int x) {...} public void someMethod () {setColor (5); }

이 코드를 통해 색상이 설정되고 있는지 확인할 수 있습니다. 그러나 5는 어떤 색을 나타 냅니까? 이 코드가 자신의 작업에 대해 논평하는 드문 프로그래머 중 한 사람이 작성한 경우 파일 상단에서 답을 찾을 수 있습니다. 그러나 설명을 위해 오래된 디자인 문서 (존재하는 경우)를 찾아야 할 가능성이 더 큽니다.

더 명확한 해결책은 의미있는 이름을 가진 변수에 값 5를 할당하는 것입니다. 예를 들면 :

public static final int RED = 5; public void someMethod () {setColor (RED); }

이제 우리는 코드에서 무슨 일이 일어나고 있는지 즉시 알 수 있습니다. 색상이 빨간색으로 설정됩니다. 이것은 훨씬 깨끗하지만 더 안전합니까? 다른 코더가 혼란스러워서 다음과 같이 다른 값을 선언하면 어떻게 될까요?

public static final int RED = 3; public static final int GREEN = 5;

이제 두 가지 문제가 있습니다. 우선, RED더 이상 올바른 값으로 설정되지 않습니다. 둘째, 빨간색 값은라는 변수로 표시됩니다 GREEN. 아마도 가장 무서운 부분은이 코드가 잘 컴파일되고 제품이 배송 될 때까지 버그가 감지되지 않을 수 있다는 것입니다.

명확한 색상 클래스를 생성하여이 문제를 해결할 수 있습니다.

public class Color {public static final int RED = 5; public static final int GREEN = 7; }

그런 다음 문서화 및 코드 검토를 통해 프로그래머가 다음과 같이 사용하도록 권장합니다.

public void someMethod () {setColor (Color.RED); }

코드 목록의 디자인은 코더가 준수하도록 강요하지 않기 때문에 권장합니다. 모든 것이 순서가 맞지 않더라도 코드는 여전히 컴파일됩니다. 따라서 이것이 조금 더 안전하지만 완전히 안전하지는 않습니다. 프로그래머 Color 클래스를 사용해야 하지만 반드시 필요한 것은 아닙니다. 프로그래머는 다음 코드를 매우 쉽게 작성하고 컴파일 할 수 있습니다.

 setColor (3498910); 

setColor방법은이 큰 숫자를 색상으로 인식 합니까 ? 아마 아닐 것입니다. 그렇다면 이러한 불량 프로그래머로부터 어떻게 자신을 보호 할 수 있습니까? 그것이 상수 유형이 구출되는 곳입니다.

메서드의 서명을 재정의하는 것으로 시작합니다.

 public void setColor (Color x) {...} 

이제 프로그래머는 임의의 정수 값을 전달할 수 없습니다. 유효한 Color개체 를 제공해야 합니다. 이것의 구현 예는 다음과 같습니다.

public void someMethod () {setColor (new Color ( "Red")); }

우리는 여전히 깨끗하고 읽기 쉬운 코드로 작업하고 있으며 절대적인 안전을 달성하는 데 훨씬 더 가깝습니다. 그러나 우리는 아직 거기에 도달하지 않았습니다. 프로그래머는 여전히 혼란을 일으킬 여지가 있으며 임의로 다음과 같이 새로운 색상을 만들 수 있습니다.

public void someMethod () {setColor (new Color ( "안녕하세요, 제 이름은 테드입니다.")); }

Color클래스를 불변 으로 만들고 프로그래머로부터 인스턴스화를 숨겨서 이러한 상황을 방지합니다 . 우리는 각각 다른 유형의 색상 (빨간색, 녹색, 파란색)을 단일 색상으로 만듭니다. 이는 생성자를 비공개로 만든 다음 공개 핸들을 제한되고 잘 정의 된 인스턴스 목록에 노출하여 수행됩니다.

public class Color {private Color () {} public static final Color RED = new Color (); public static final Color GREEN = new Color (); 공개 정적 최종 색상 BLUE = new Color (); }

이 코드에서 우리는 마침내 절대적인 안전을 달성했습니다. 프로그래머는 가짜 색상을 조작 할 수 없습니다. 정의 된 색상 만 사용할 수 있습니다. 그렇지 않으면 프로그램이 컴파일되지 않습니다. 이것이 우리의 구현 모습입니다.

public void someMethod () {setColor (Color.RED); }

고집

자, 이제 우리는 상수 유형을 처리하는 깨끗하고 안전한 방법을 얻었습니다. 색상 속성을 가진 객체를 생성하고 색상 값이 항상 유효한지 확인할 수 있습니다. 하지만이 객체를 데이터베이스에 저장하거나 파일에 쓰려면 어떻게해야할까요? 색상 값을 어떻게 저장합니까? 이러한 유형을 값에 매핑해야합니다.

에서 JavaWorld의 위에서 언급 한 기사, 에릭 암스트롱은 문자열 값을 사용했다. 문자열을 사용하면 toString()메서드 에서 반환 할 의미있는 무언가를 제공하는 추가 보너스를 제공 하므로 디버깅 출력이 매우 명확 해집니다.

하지만 문자열은 저장 비용이 많이들 수 있습니다. 정수는 값을 저장하는 데 32 비트가 필요하고 문자열에는 문자 당 16 비트가 필요합니다 (유니 코드 지원으로 인해). 예를 들어, 숫자 49858712는 32 비트로 저장할 수 있지만 문자열 TURQUOISE에는 144 비트가 필요합니다. 색상 속성이있는 수천 개의 객체를 저장하는 경우 상대적으로 작은 비트 차이 (이 경우 32에서 144 사이)가 빠르게 합산 될 수 있습니다. 따라서 대신 정수 값을 사용하겠습니다. 이 문제에 대한 해결책은 무엇입니까? 문자열 값은 프레젠테이션에 중요하므로 유지하지만 저장하지는 않습니다.

1.1 이상의 Java 버전은 Serializable인터페이스 를 구현하는 한 자동으로 객체를 직렬화 할 수 있습니다. Java가 외부 데이터를 저장하지 않도록하려면 transient키워드를 사용하여 이러한 변수를 선언해야합니다 . 따라서 문자열 표현을 저장하지 않고 정수 값을 저장하기 위해 문자열 속성을 일시적으로 선언합니다. 다음은 정수 및 문자열 속성에 대한 접근 자와 함께 새 클래스입니다.

공용 클래스 Color는 java.io.Serializable을 구현합니다. {private int value; 개인 임시 문자열 이름; public static final Color RED = new Color (0, "Red"); public static final Color BLUE = new Color (1, "Blue"); public static final Color GREEN = new Color (2, "Green"); private Color (int value, String name) {this.value = value; this.name = 이름; } public int getValue () {반환 값; } public String toString () {반환 이름; }}

이제 상수 유형의 인스턴스를 효율적으로 저장할 수 있습니다 Color. 하지만 복원은 어떻습니까? 그것은 약간 까다로울 것입니다. 더 나아 가기 전에 앞서 언급 한 모든 함정을 처리 할 프레임 워크로 확장하여 유형을 정의하는 간단한 문제에 집중할 수 있도록하겠습니다.

상수 유형 프레임 워크

With our firm understanding of constant types, I can now jump into this month's tool. The tool is called Type and it is a simple abstract class. All you have to do is create a very simple subclass and you've got a full-featured constant type library. Here's what our Color class will look like now:

public class Color extends Type { protected Color( int value, String desc ) { super( value, desc ); } public static final Color RED = new Color( 0, "Red" ); public static final Color BLUE = new Color( 1, "Blue" ); public static final Color GREEN = new Color( 2, "Green" ); } 

The Color class consists of nothing but a constructor and a few publicly accessible instances. All of the logic discussed to this point will be defined and implemented in the superclass Type; we'll be adding more as we go along. Here's what Type looks like so far:

public class Type implements java.io.Serializable { private int value; private transient String name; protected Type( int value, String name ) { this.value = value; this.name = name; } public int getValue() { return value; } public String toString() { return name; } } 

Back to persistence

With our new framework in hand, we can continue where we left off in the discussion of persistence. Remember, we can save our types by storing their integer values, but now we want to restore them. This is going to require a lookup -- a reverse calculation to locate the object instance based on its value. In order to perform a lookup, we need a way to enumerate all of the possible types.

In Eric's article, he implemented his own enumeration by implementing the constants as nodes in a linked list. I'm going to forego this complexity and use a simple hashtable instead. The key for the hash will be the integer values of the type (wrapped in an Integer object), and the value of the hash will be a reference to the type instance. For example, the GREEN instance of Color would be stored like so:

 hashtable.put( new Integer( GREEN.getValue() ), GREEN ); 

Of course, we don't want to type this out for each possible type. There could be hundreds of different values, thus creating a typing nightmare and opening the doors to some nasty problems -- you might forget to put one of the values in the hashtable and then not be able to look it up later, for instance. So we'll declare a global hashtable within Type and modify the constructor to store the mapping upon creation:

 private static final Hashtable types = new Hashtable(); protected Type( int value, String desc ) { this.value = value; this.desc = desc; types.put( new Integer( value ), this ); } 

But this creates a problem. If we have a subclass called Color, which has a type (that is, Green) with a value of 5, and then we create another subclass called Shade, which also has a type (that is Dark) with a value of 5, only one of them will be stored in the hashtable -- the last one to be instantiated.

In order to avoid this, we have to store a handle to the type based on not only its value, but also its class. Let's create a new method to store the type references. We'll use a hashtable of hashtables. The inner hashtable will be a mapping of values to types for each specific subclass (Color, Shade, and so on). The outer hashtable will be a mapping of subclasses to inner tables.

This routine will first attempt to acquire the inner table from the outer table. If it receives a null, the inner table doesn't exist yet. So, we create a new inner table and put it into the outer table. Next, we add the value/type mapping to the inner table and we're done. Here's the code:

 private void storeType( Type type ) { String className = type.getClass().getName(); Hashtable values; synchronized( types ) // avoid race condition for creating inner table { values = (Hashtable) types.get( className ); if( values == null ) { values = new Hashtable(); types.put( className, values ); } } values.put( new Integer( type.getValue() ), type ); } 

And here's the new version of the constructor:

 protected Type( int value, String desc ) { this.value = value; this.desc = desc; storeType( this ); } 

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

해시 테이블 해시 테이블 구성 덕분에 Eric의 구현에서 제공하는 열거 기능을 노출하는 것은 매우 간단합니다. 유일한 경고는 Eric의 디자인이 제공하는 정렬이 보장되지 않는다는 것입니다. Java 2를 사용하는 경우 내부 해시 테이블을 정렬 된 맵으로 대체 할 수 있습니다. 그러나이 칼럼의 시작 부분에서 언급했듯이 지금은 JDK 1.1 버전에만 관심이 있습니다.

형식을 열거하는 데 필요한 유일한 논리는 내부 테이블을 검색하고 해당 요소 목록을 반환하는 것입니다. 내부 테이블이 없으면 단순히 null을 반환합니다. 전체 방법은 다음과 같습니다.