달력

4

« 2024/4 »

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
2007. 1. 29. 15:43

Java 5의 새로운 기능 그거/Java2007. 1. 29. 15:43

관련글
http://kwon37xi.egloos.com/2510988
http://java.sun.com/j2se/1.5.0/docs/relnotes/features.html

by Jess Garms and Tim Hanson
2005/12/06

개요

Java 5.0은 이미 나와 있고, 많은 사용자들이 이번 릴리스의 JDK에 추가된 새로운 기능을 사용하기 시작할 것입니다. 이제 향상된 for loop 기능에서부터 generics와 같은 좀 더 복잡한 기능에 이르는 모든 기능이 코드 작성에 사용될 것입니다. 우리는 대규모의 Java 5.0 기반 작업을 방금 완료했으며, 이 문서에서는 이러한 여러 기능을 사용한 경험에 대해 이야기하고자 합니다. 이 문서에서는 단순한 소개를 넘어 기능에 대해 좀 더 깊이 있게 살펴보고 이 기능들이 실제 프로젝트에 미칠 영향과 이러한 기능들을 효과적으로 사용하기 위한 몇 가지 팁에 대해 알아보도록 하겠습니다.

소개

JDK 1.5의 베타 시험 기간 동안 우리는 BEA의 Java IDE용 Java 5 컴파일러를 사용해 보았습니다. 우리가 다양한 새 기능을 구현했던 것처럼 다른 사람들도 이 기능들을 새로운 방식으로 이용하기 시작했습니다. 일부는 훌륭하게 사용하였고 일부는 그렇지 못 했습니다. 컴파일러 자체가 새로운 기능을 사용하므로 우리는 이러한 기능을 사용하여 코드를 유지 관리하는 직접적인 경험을 얻을 수 있었습니다. 이 문서에서 이러한 여러 가지 기능과 이에 관련된 경험을 살펴보겠습니다.

이미 새로운 기능에 익숙하리라 생각되므로 각 기능에 대한 포괄적인 소개 대신, 흥미롭고 명확하게 표현되지 않은 함축적 내용 및 사용에 대해 살펴보겠습니다. 이러한 팁은 실행해 본 것 중에서 임의로 선택하여 언어 기능별로 대략적으로 분류한 것입니다.

가장 단순한 기능부터 시작해서 가장 고급 기능의 순서로 다룰 계획이며 Generics는 특히 이야기거리가 많은 주제이므로 이 문서의 반 정도를 할애할 것입니다.


Enhanced for Loop

향상된 새로운 for 루프는 컬렉션 및 배열에서 반복적으로 사용하기 위한 간단하면서도 일관된 구문을 제공합니다. 몇 가지 항목을 소개하면 다음과 같습니다.

Init 표현식

초기화 표현식은 루프 내에서 한 번만 값을 구하게 됩니다. 이것은 변수 선언을 삭제할 수 있음을 의미합니다. 이 예제에서 우리는 각 메서드가 루프를 통과할 때 해당 메서드의 값을 다시 구하지 않도록 하기 위해 Integer 배열을 만들어 computeNumbers()의 결과를 유지하도록 했습니다. 아래쪽 코드가 위쪽 코드에 비해 좀 더 명확하고 numbers 변수의 손실이 없습니다.

향상된 For가 없는 경우:

int sum = 0;
Integer[] numbers = computeNumbers();
for (int i=0; i < numbers.length ; i++)
    sum += numbers[i];

있는 경우:

int sum = 0;
for ( int number: computeNumbers() )
    sum += number;

제한

반복하는 동안 반복자 또는 인덱스에 액세스해야 하는 경우가 있습니다. 언뜻 보기에는 향상된 for 루프에서 이것을 허용되는 것처럼 보이지만 사실은 그렇지 않습니다. 다음 예제를 보겠습니다.

for (int i=0; i < numbers.length ; i++) {
    if (i != 0) System.out.print(",");
    System.out.print(numbers[i]);
}

배열에서 쉼표로 구분된 값 리스트을 출력하려고 합니다. 쉼표를 인쇄해야 할지 알아보려면 현재 첫 번째 항목에 있는지 여부를 알아야 합니다. 그러나 향상된 for를 사용하여 이런 정보를 얻을 방법이 없습니다. 인덱스를 유지하거나 첫 번째 항목을 이미 지나쳤는지 알려주는 boolean을 유지해야 합니다.

다음은 다른 예제입니다.

for (Iterator<integer> it = n.iterator() ; it.hasNext() ; )
    if (it.next() < 0)
        it.remove();

이 경우에는 Integers 컬렉션에서 음수 항목을 삭제하려고 합니다. 그렇게 하려면 반복자에서 메서드를 호출해야 하지만 향상된 for 루프를 사용할 경우 이러한 반복자는 숨겨집니다. 그러므로 Java 5 이전의 반복 메서드를 사용해야 합니다.

어쨌든 주의할 점은 Iterator는 Generic이므로 선언은 Iterator<Integer>이라는 것입니다. 많은 사람들은 이 사실을 놓치고 Iterator를 원시 형태로 사용합니다.


주석

주석 처리는 광범위한 주제입니다. 이 문서는 핵심적인 언어 기능에 대해 제한적으로 다루기 때문에 주석의 모든 가능성과 위험 요인에 대해서는 설명하지 않습니다.

그러나 내장된 주석(SuppressWarnings, Deprecated 및 Override) 및 일반적인 주석 처리의 제한 사항에 대해서 설명하도록 하겠습니다.

Suppress Warnings

이 주석은 클래스 또는 메서드 수준에서 컴파일러 경고 표시를 끕니다. 코드에서 더 이상 사용하지 않는(Deprecated) 메서드를 사용해야 하거나 정적으로는 typesafe인지 확인할 수 없지만 사실은 typesafe인 액션을 수행해야 한다는 것을 사용자가 컴파일러 보다 더 잘 알 수도 있습니다.

@SuppressWarnings("deprecation")
public static void selfDestruct() {
    Thread.currentThread().stop();
}

이것은 아마 가장 유용한 내장된 주석일 것입니다. 그러나 불행히도 javac는 1.5.0_04에서 이것을 지원하지 않습니다. 하지만 1.6에서는 지원되며, Sun은 이것을 1.5로 백포팅하는 중입니다.

이 주석은 Eclipse 3.1 및 다른 IDE에서도 지원됩니다. 이 주석을 사용하여 코드에 대한 경고가 발생하지 않도록 할 수 있습니다. 컴파일 시 경고가 표시된 경우, 이미 이 코드를 추가했으므로 다른 잘못된 코드가 있음을 알 수 있습니다. Generics를 추가하면 훨씬 더 좋습니다.

Deprecated

Deprecated는 아쉽게도 그다지 유용하지는 않습니다. 이것은 원래 @deprecated javadoc 태그를 대체하기 위한 것이지만 필드를 가지고 있지 않기 때문에 deprecated 클래스 또는 메서드 사용자에게 대안으로 사용하라고 알려줄 방법이 없습니다. 대부분 사용자는 javadoc 태그와 이 주석을 모두 필요로 합니다.

Override

Override는 이 주석이 달린 메서드가 슈퍼클래스에서 동일한 서명을 가진 메서드를 무시한다는 것을 나타냅니다.

@Override
public int hashCode() {
    ...
}

위의 경우를 예로 들면, hashCode에서 "C" 를 대문자로 쓰지 않은 경우 컴파일 시에는 오류가 표시되지 않지만 런타임 시에는 메서드가 예상했던 대로 호출되지 않습니다. Override 태그를 추가하면 실제로 override가 수행되지 않을 경우 컴파일러가 문제를 표시합니다.

또한 슈퍼클래스를 변경하는 경우 이것이 도움이 됩니다. 말하자면 이 메서드에 새 파라미터를 추가하고 메서드 자체의 이름을 바꾸면 하위 클래스가 슈퍼 클래스에서 더 이상 아무것도 무시하지 않기 때문에 컴파일에 실패하게 됩니다.

주석의 기타 정보

주석은 다른 상황에서 매우 유용할 수 있습니다. 주석은 동작을 직접 수정하지 않고 강화시킨 경우, 특히 보일러플레이트 코드를 추가하는 경우 EJB 및 웹 서비스 같은 프레임워크에서 가장 잘 작동합니다.

주석은 전처리기로 사용할 수 없습니다. 특히 Sun의 설계는 주석 때문에 클래스의 바이트 코드를 직접 수정하지 못하도록 했습니다. 이로써 언어의 결과를 제대로 이해할 수 있고 IDE같은 툴이 코드 심층 분석 및 refactoring 같은 기능을 수행할 수 있습니다.

주석은 완전한 해결책이 아닙니다. 처음 주석이 나타나면 사람들은 온갖 종류의 기술을 시도해 보려고 합니다. 다음을 살펴보겠습니다.

public class Foo {

    @Property
    private int bar;

}

여기서는 전용 필드 bar에 대해 getter 및 setter 메서드를 자동으로 생성하려고 합니다. 하지만 불행히도 이 생각은 다음과 같은 두 가지 이유에서 좋지 못합니다. 1) 이것은 작동하지 않으며, 2) 이 코드를 읽고 처리하기가 더욱 어려워집니다.

앞서 말한 바와 같이 Sun이 특별히 주석이 표시되는 클래스를 수정하지 못하도록 했기 때문에 이것은 실현될 수 없습니다.

가능하더라도 이 코드를 이해하기 더욱 어렵게 만들기 때문에 좋은 생각이 아닙니다. 이 코드를 처음 본 사람은 이 주석이 메서드를 생성한다는 생각을 하지 못할 것입니다. 또한 향후 이러한 메서드 중 하나에서 무언가를 해야 한다면 이 주석은 쓸모가 없습니다.

요약하자면 정규 코드로 할 수 있는 것에 주석을 사용하려고 하지 말라는 것입니다.


Enumerations

Enum은 수 년간 enum 값으로 사용되어온 public static final int 선언과 많이 유사합니다. int에서 가장 크고 확실하게 개선된 점은 type safe입니다. int와는 달리 enum 타입 중 한 가지를 다른 타입의 위치에 사용할 수 없습니다. 왜냐하면 컴파일러에게는 모든 것이 똑같아 보이기 때문입니다. 아주 드물게 예외가 있긴 하지만 이 경우에도 enum과 유사한 모든 int 구성을 enum 인스턴스로 교체해야 합니다.

Enum은 여러 가지 추가 기능을 제공합니다. 유틸리티 클래스인 EnumMap 및 EnumSet은 특히 enum에 최적화된 표준 컬렉션 구현입니다. 컬렉션에 enum만 포함되는 것을 알고 있다면 HashMap 또는 HashSet 대신 이러한 특정 컬렉션을 사용해야 합니다.

대부분의 경우 코드에서 모든 public static final ints를 enums으로 교체할 수 있습니다. enum은 비슷(Comparable)하고, 내부 클래스(또는 내부 enum)일지라도 이에 대한 참조가 똑같아 보이므로 정적으로 가져올 수 있습니다. enum을 비교할 때 선언되는 순서가 서수 값을 나타낸다는 사실에 유의하십시오.

"Hidden" 정적 메서드

작성한 모든 enum 선언에는 두 가지 정적 메서드가 표시됩니다. 이 두 가지 메서드는 Enum 자체가 아니라 enum 하위 클래스에 대한 정적 메서드이기 때문에 java.lang.Enum에 대한 javadoc에는 표시되지 않습니다.

첫 번째 values()는 enum에 가능한 모든 값의 배열을 리턴합니다.

두 번째 valueOf()는 제공된 문자열에 대한 enum을 리턴하는데, 원본 코드 선언과 똑같이 일치해야 합니다.

메서드

enums의 좋은 점 중의 하나는 메서드를 가질 수 있다는 것입니다. 과거에는 데이터베이스 타입을 JDBC URL로 번역하기 위해 public static final int에서 스위치를 수행한 코드가 필요했을 수 있습니다. 이제는 enum 자체에 직접 코드를 정리할 수 있는 메서드를 가질 수 있습니다. 다음은 DatabaseType enum에서 추상 메서드와 각 enum 인스턴스에 제공된 구현을 사용하여 이것을 어떻게 수행하는 지 보여주는 예제입니다.

public enum DatabaseType {
    ORACLE {
        public String getJdbcUrl() {...}
    },
    MYSQL {
        public String getJdbcUrl() {...}
    };
    public abstract String getJdbcUrl();
}

이제 enum에서 유틸리티 메서드를 직접 제공할 수 있습니다. 예를 들면 다음과 같습니다.

DatabaseType dbType = ...;
String jdbcURL = dbType.getJdbcUrl();

이전에는 URL을 얻기 위해 유틸리티 메서드가 있는 장소를 알려주어야 했습니다.


Varargs

varargs를 올바로 사용하면 정말로 거추장스러운 코드를 일부 정리할 수 있습니다. 다음과 같은 기본 예제는 String 인수를 다양하게 가진 로그 메서드입니다.

Log.log(String code)
Log.log(String code, String arg)
Log.log(String code, String arg1, String arg2)
Log.log(String code, String[] args)

varargs에 관한 설명에서 흥미로운 사실은 처음 4개의 예제를 새로운 vararged 예제로 교체하는 경우 얻어지는 호환성입니다.

Log.log(String code, String... args)

모든 varargs는 소스 호환적이어서 log() 메서드의 모든 호출자를 재컴파일하는 경우 4개의 모든 메서드를 바로 교체할 수 있습니다. 그러나 이전 버전과의 이진 호환성이 필요한 경우 처음 세 개는 그대로 두어야 합니다. 마지막 메서드에서만 Strings 배열을 가져오면 같은 값이 되므로 vararged 버전을 교체할 수 있습니다.

Casting

항목은 String이고 두 번째는 Exception일 것으로 예상할 수 있습니다.

Log.log(Object... objects) {
    String message = (String)objects[0];
    if (objects.length > 1) {
        Exception e = (Exception)objects[1];
        // Do something with the exception
    }
}

그 대신 메서드 서명은 vararg 파라미터에서 별도로 선언된 String 및 Exception 을 사용하여 다음과 같아야 합니다.

Log.log(String message, Exception e, Object... objects) {...}

지나치게 사용하려고 하지 마십시오. varargs를 사용하여 타입 시스템을 망가뜨릴 수 있습니다. 강력한 타입 지정(strong typing) 이 필요한 경우 사용하십시오. PrintStream.printf()은 이러한 규칙에 대한 예외입니다. 이것은 타입 정보를 나중에도 수용할 수 있도록 첫 번째 인수로 제공합니다.


Covariant Returns

공변(covariant) 리턴은 주로 구현의 리턴 타입이 API보다 일반적이지 않은 경우 캐스트를 방지하기 위해 사용됩니다. 다음 예제에서는 Animal 객체를 리턴하는 Zoo 인터페이스를 사용합니다. 이 구현은 AnimalImpl 객체를 리턴합니다. 그러나 JDK 1.5 이전에서는 Animal 객체를 리턴하도록 선언해야 했습니다.

public interface Zoo {
    public Animal getAnimal();
}

public class ZooImpl implements Zoo {
    public Animal getAnimal(){
        return new AnimalImpl();
    }
}

공변 리턴을 사용하면 다음 세 가지 anti-pattern을 바꿀 수 있습니다.

  1. 직접 필드에 액세스합니다. API 제한을 피하기 위해 일부 구현은 하위 클래스를 직접 필드로 제공합니다.
    ZooImpl._animal
  2. 추가 양식이 호출자에서 다운캐스트를 수행하므로 이 구현이 실제로 이러한 특정 하위 클래스임을 알 수 있습니다.
    ((AnimalImpl)ZooImpl.getAnimal()).implMethod();
  3. 마지막 양식은 완전히 다른 서명을 제안함으로써 이러한 문제를 피해가는 특수 메서드입니다.
    ZooImpl._getAnimal();

이러한 패턴은 모두 문제점과 제한이 있습니다. 보기에 안 좋거나 아니면 필요하지도 않은 구현 세부 정보를 제공합니다.

With covariance

공변(covariant) 리턴 패턴은 유지 관리가 더욱 간편하고 안전하고 편리합니다. 캐스트 또는 특수 메서드나 필드가 필요하지 않습니다.

public AnimalImpl getAnimal(){
    return new AnimalImpl();
}

결과 사용:

ZooImpl.getAnimal().implMethod();

Generics 사용

Generics 사용 및 Generics 구성이라는 두 가지 각도에서 Generics를 살펴보겠습니다. List, Set 및 Map의 사용에 관해서는 이야기하지 않겠습니다. 지금은 Generic 컬렉션에 대해서만 언급하도록 하겠습니다.

Generic 메서드 사용 및 컴파일러가 타입을 추론하는 방법에 대해 다룰 것입니다. 일반적으로는 이 정도의 내용만으로도 충분할 것입니다. 그러나 오류 메시지의 뜻을 이해할 수 있도록 문제의 해결 방법도 알아야 합니다.

Generic 메서드

generic 타입 이외에 Java 5에서는 generic 메서드를 도입했습니다. java.util.Collections의 예제에서는 싱글톤(singleton) 리스트을 구성합니다. 새로운 List의 엘리먼트 타입은 메서드에 전달된 객체의 타입에 기반하여 추론됩니다.

static <T> List<T> Collections.singletonList(T o)

이용 예제:

public List<Integer> getListOfOne() {
    return Collections.singletonList(1);
}

위 이용 예제에 int를 전달하면 메서드의 리턴 타입이 List가 됩니다. 컴파일러는 T에 대한 Integer를 추론합니다. 일반적으로 인수 타입를 명시적으로 지정해야 할 필요가 없기 때문에 이것은 Generic 타입과 다릅니다.

또한 이것은 autoboxing과 generics의 상호 작용도 보여줍니다. 인수 타입은 레퍼런스 타입이어야 합니다. 그것이 바로 List<Integer>가 아니라 List<int>를 얻게 되는 이유입니다.

파라미터가 없는 Generic 메서드

emptyList() 메서드는 java.util.Collections의 EMPTY_LIST 필드에 대한 type safe 대안으로서 Generics과 함께 도입되었습니다.

static <T> List<T> Collections.emptyList()

이용 예제:

public List<Integer> getNoIntegers() {
    return Collections.emptyList();
}

이전 예제와 달리 이것은 파라미터가 없으므로 T에 대한 타입을 컴파일러에서 어떻게 추론해야 할까요? 기본적으로 파라미터를 사용하여 한 번만 시도합니다. 실행된 것이 없을 경우 리턴 또는 할당 타입을 사용하여 다시 시도합니다. 이 경우 List<Integer>를 리턴하므로 T는 Integer로 추론됩니다.

리턴 문 또는 할당 문 이외의 위치에서 generic 메서드를 호출하면 어떻게 될까요? 그러면 컴파일러는 타입 추론의 두 번째 단계를 수행할 수 없습니다. 다음 예제에서 emptyList()가 조건부 연산자 내에서 호출됩니다.

public List<Integer> getNoIntegers() {
    return x ? Collections.emptyList() : null;
}

컴파일러는 리턴 컨텍스트를 볼 수 없고 T를 추론할 수 없으므로 포기하고 Object로 가정합니다. "cannot convert List<Object> to List<Integer>." 같은 오류 메시지가 표시됩니다.

이것을 수정하려면 메서드 호출에 인수 타입를 명시적으로 전달해야 합니다. 그러면 컴파일러는 인수 타입를 추론하려고 시도하지 않으므로 올바른 결과를 얻을 수 있습니다.

return x ? Collections.<Integer>emptyList() : null;

이런 상황이 자주 발생하는 또 다른 위치는 메서드 호출입니다. 메서드에 List을 사용하고 해당 파라미터에 대한 emptyList()를 전달하여 이것을 호출하려고 시도하는 경우에도 이 구문을 사용해야 합니다.

그 밖의 컬렉션

다음은 컬렉션이 아니라 새로운 방식으로 generics를 사용하는 세 가지 Generic 타입 예제입니다. 이 예제는 모두 표준 Java 라이브러리에 있는 것입니다.

  • Class<T>
    Class는 클래스의 타입에 파라미터화됩니다. 그러면 캐스팅 없이 newInstance를 구성할 수 있게 됩니다.

  • Comparable<T>
    Comparable 은 실제 비교 타입별로 파라미터화됩니다. 이것은 compareTo() 호출에 더욱 강력한 타입 지정을 제공합니다. 예를 들어, String은 Comparable<String>을 구현합니다. String 이외에서 compareTo()를 호출하면 컴파일 시 실패합니다.

  • Enum은 enum 타입별로 파라미터화됩니다. Color라는 enum은 Enum<Color>를 확장합니다. getDeclaringClass() 메서드는 enum 타입에 대한 클래스 객체를 리턴하는데, 이 경우에는 Color가 리턴됩니다. 이것은 익명의 클래스를 리턴할 수 있는 getClass()와는 다릅니다.

와일드카드

generics에서 가장 난해한 부분은 와일드카드를 이해하는 것입니다. 여기서는 세 가지 와일드카드의 타입 및 사용 목적에 대해 설명합니다.

먼저 배열 사용 방법을 살펴보겠습니다. Integer[]에서 Number[]를 할당할 수 있습니다. Float를 Number[]에 쓰려고 시도하면 컴파일은 되지만 런타임 시 ArrayStoreException에서 실패하게 됩니다.

Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5; // compiles, but fails at runtime

이 예제를 직접 generics으로 번역하려고 시도하면 할당이 허용되지 않기 때문에 컴파일 시 실패합니다.

List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList; // not allowed
nList.add(0.5);

Generics을 사용하는 경우 경고 없이 컴파일하는 코드가 있으면 런타임 ClassCastException을 절대로 얻을 수 없습니다.

상한선 와일드카드

배열와 달리, 여기서 필요한 것은 정확한 엘리먼트 타입이 알려지지 않은 리스트입니다.

List<Number>는 엘리먼트 타입이 명확한 Number인 리스트입니다.

List<? extends Number>는 정확한 엘리먼트 타입이 알려지지 않은 리스트입니다. 이것은 Number 또는 하위 타입입니다.

상한선

원래 예제를 업데이트하고 List<? extends Number>에 할당하면 이 할당은 성공합니다.

List<Integer> iList = new ArrayList<Integer>();
List<? extends Number> nList = iList;
Number n = nList.get(0);
nList.add(0.5); // Not allowed

리스트의 정확한 엘리먼트 타입(Float, Integer 또는 Number)에 상관없이 Number를 할당할 수 있기 때문에 이 리스트에서 Number를 얻을 수 있습니다.

floats를 리스트에 삽입할 수는 없습니다. 그럴 경우 안전하다는 것을 입증할 수 없기 때문에 컴파일 시 실패합니다. 리스트에 float를 추가하면 Integer만 저장하는 iList의 원래 type safety를 위반하게 됩니다.

와일드카드는 배열을 사용하는 것보다 강한 표현력을 제공합니다.

와일드카드 사용 이유

다음 예제에서 와일드카드는 API의 사용자에게 타입 정보를 숨기는 데 사용됩니다. 내부적으로 Set은 CustomerImpl로 저장됩니다. API 사용자가 알고있는 전부는 Customers를 읽을 수 있는 Set을 얻게 될 것이라는 사실입니다.

여기서는 Set<CustomerImpl>에서 Set<Customer>로 할당할 수 없기 때문에 와일드카드가 필요합니다.

public class CustomerFactory {

    private Set<CustomerImpl> _customers;

    public Set<? extends Customer> getCustomers() {
        return _customers;
    }

}

와일드카드 및 공변(covariant) 리턴

와일드카드를 일상적으로 사용하는 또 다른 경우는 공변(covariant) 리턴입니다. 동일한 규칙이 할당되어 공변 리턴에 적용됩니다. overridden 메서드에서 더 특정한 generic 타입을 리턴하도록 하려면 선언하는 메서드에 반드시 와일드카드를 사용해야 합니다.

public interface NumberGenerator {
    public List<? extends Number> generate();
}

public class FibonacciGenerator extends NumberGenerator {
    public List<Integer> generate() {
        ...
    }
}

여기서 배열을 사용하면 인터페이스는 Number[]를 리턴하고 구현은 Integer[]를 리턴할 수 있습니다.

하한선

상한선 와일드카드에 대해 이미 설명했습니다. 하한선 와일드카드도 있습니다. List<? super Number>는 정확한 "엘리먼트 타입"이 알려지지 않은 리스트이지만 이것은 MNumber이거나 또는 Number의 수퍼 타입입니다. 그러므로 이것은 List<Number> 또는 List<Object>일 수 있습니다.

하한선 와일드카드는 상한선 와일드카드 만큼 일반적이지 않지만 필요한 경우 반드시 사용되어야 합니다.

하한선과 상한선 비교

List<? extends Number> readList = new ArrayList<Integer>();
Number n = readList.get(0);

List<? super Number> writeList = new ArrayList<Object>();
writeList.add(new Integer(5));

첫 번째 리스트은 숫자를 읽을 수 있는 리스트입니다.

두 번째 리스트은 숫자를 쓸 수 있는 리스트입니다.

제한 없는 와일드카드

마지막으로, List<?>는 모든 것의 리스트이며 List와 거의 동일합니다. 항상 Objects를 읽을 수 있지만 리스트에 쓸 수는 없습니다.

공용 API의 와일드카드

요약하자면 와일드카드는 몇 단원 전에 살펴본 바와 같이 호출자에게 구현 세부 정보를 숨기는 데는 아주 유용합니다. 그러나 읽기 전용 액세스를 제공하기 위해 하한선 와일드카드가 표시되더라도 remove(int position)같은 비-generic 메서드로 인해 그렇게 하지 못합니다. 정말로 불변하는 컬렉션을 원한다면 unmodifiableList()처럼 java.util.Collections에서 이 메서드를 사용하십시오.

API를 작성할 경우 와일드카드를 알아야 합니다. 일반적으로 generic 타입을 전달할 때 와일드카드를 사용해야 합니다. 그러면 API가 더 광범위한 호출자에게 액세스할 수 있습니다.

다음 예제는 List<Number> 대신 List<? extends Number>를 사용하여 아래의 메서드가 여러 가지 다양한 타입의 Lists를 사용하여 호출되도록 합니다.

void removeNegatives(List<? extends Number> list);

Generic 타입 구성

이제 generic 타입 구성에 대해 살펴보겠습니다. generic 타입 구현 시 발생하는 일반적인 문제 뿐만 아니라 generic을 사용하여 type safety를 향상시킬 수 있는 예제를 보여줄 것입니다.

컬렉션과 유사한 함수

generic 클래스의 첫 번째 예제는 컬렉션과 유사한 예제입니다. Pair는 두 개의 파라미터를 가지며 필드는 타입의 인스턴스입니다.

public final class Pair<A,B> {
    public final A first;
    public final B second;

    public Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }
}

이것은 두 가지 타입으로 된 각각의 콤보에 대해 특수 목적의 클래스를 작성하지 않고 메서드에서 두 개의 항목을 리턴하도록 합니다. Object[]를 리턴하는 것도 할 수 있지만 그것은 타입이 안전하지 않거나 깔끔하지 않습니다.

아래 이용 예제는 메서드에서 File 및 Boolean을 리턴합니다. 메서드의 클라이언트는 캐스팅하지 않고 직접 이 필드를 사용할 수 있습니다.

public Pair<File,Boolean> getFileAndWriteStatus(String path){
    // create file and status
    return new Pair<File,Boolean>(file, status);
}

Pair<File,Boolean> result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;

그 밖의 컬렉션

다음 예제에서 generic은 추가 컴파일 시 안전을 위해 사용되었습니다. 생성되는 Peer 타입별로 DBFactory 클래스를 파라미터화하면 Factory 하위 클래스가 Peer의 특정 하위 타입을 리턴하도록 할 수 있습니다.

public abstract class DBFactory<T extends DBPeer> {
    protected abstract T createEmptyPeer();

    public List<T> get(String constraint) {
        List<T> peers = new ArrayList<T>();
        // database magic
        return peers;
    }
}

DBFactory<Customer>를 구현하면 CustomerFactory는 createEmptyPeer()에서 Customer를 리턴하게 됩니다.

public class CustomerFactory extends DBFactory<Customer>{

    public Customer createEmptyPeer() {
        return new Customer();
    }
}

Generic 메서드

파라미터 간에 또는 파라미터와 리턴 타입 간의 generic 타입에 대한 제한을 두기 위해 generic 메서드를 사용할 수 있습니다.

예를 들어, 제자리에서 역행하는 리버스 함수를 작성하면 generic 메서드가 필요 없습니다. 그러나 리버스 함수가 새로운 List를 리턴하도록 하려면 새로운 List 의 엘리먼트 타입이 전달된 List와 동일해야 합니다. 그럴 경우 generic 메서드가 필요합니다.

<T> List<T> reverse(List<T> list)

Reification

generic 클래스를 구현할 때 배열 T[]를 구성할 수 없습니다. generics은 데이터 삭제에 의해 구현되기 때문에 배열 T[]를 구성할 수 없습니다.

또한 Object[]를 T[]로 캐스트하려고 시도하지 마십시오. 이것은 안전하지 않습니다.

Reification 솔루션

generics 자습서에 따르면, 이 솔루션은 "Type 토큰"을 사용합니다. 생성자에 Class<T> 파라미터를 추가하면 클라이언트가 클래스의 타입 파라미터에 대해 올바른 클래스 객체를 제공하도록 할 수 있습니다.

public class ArrayExample<T> {
    private Class<T> clazz;

    public ArrayExample(Class<T> clazz) {
        this.clazz = clazz;
    }

    public T[] getArray(int size) {
        return (T[])Array.newInstance(clazz, size);
    }
}

ArrayExample<String>을 구성하려면, String.class의 타입이 Class<String>이기 때문에 클라이언트는 String.class를 생성자에게 전달해야 합니다.

클래스 객체가 있으면 올바른 엘리먼트 타입을 사용하는 배열을 구성할 수 있습니다.

결론

한마디로 요약하면 이 새로운 기능은 Java에 근본적 변화를 가져왔습니다. 이러한 기능을 언제 어떻게 사용하는지를 이해하면 더 나은 코드를 작성할 수 있을 것입니다.

추가자료

Jess Garms은 BEA Systems의 Javelin 컴파일러 팀의 리더입니다. 그 이전에 Jess는 BEA의 Java IDE, WebLogic Workshop에 관여했습니다. 또한 그는 암호화 관련 경험이 상당히 풍부하며 Wrox Press에서 출판한 "Professional Java Security"를 공동 저술하기도 했습니다.

Tim Hanson은 BEA Systems의 Javelin 컴파일러 설계자입니다. Tim은 BEA의 Java 컴파일러(가장 초기1.5 호환 구현 중 하나)를 상당 부분 개발했습니다. 이외에도 그는 CORBA/IDL 컴파일러(IBM 재직 시) 및 XQuery 컴파일러를 비롯한 수많은 컴파일러를 작성했습니다.


Return to dev2dev.

:
Posted by 뽀기