목차

JDK 1.5에 추가된 기능

StringBuilder

StringBuilder 가 새로 추가되었다. StringBuilder 는 StringBuffer 와 완전히 동일한 클래스이다. 다만 동기화(synchronization)처리를 하지 않기 때문에 멀티쓰레드 프로그래밍에서는 사용하면 안 되지만 멀티쓰레드 프로그래밍이 아닌 경우에는 StringBuffer 보다 바른 성능을 보장한다. 동기화의 여부를 제외하고 두 클래스가 기능상으로 완전히 동일하기 때문에 코드에서 StringBuffer 를 StringBuilder 로 바꾸기만 하면 된다. 즉, StringBuffer 타입의 참조변수를 선언한 부분과 StringBuffer 의 생성자만 바꾸면 된다는 말이다.

StringBuffer sb;
sb = new StringBuffer();
sb.append("abc");
StringBuilder sb;
sb = new StringBuilder();
sb.append("abc");

StringBuffer도 충분히 성능이 좋기 때문에 성능향상이 반드시 필요한 경우를 제외하고는 기존에 작성한 코드에서 StringBuffer를 StringBuilder로 굳이 바꿀 필요는 없을 것이다.

오토박싱(autoboxing)

컬렉션에는 객체로 저장해야하기 때문에 기본형 값을 저장하기 위해서는 Integer나 Long과 같은 Wrapper클래스를 사용해야했다. 그러나 이제부터는 기본형 값을 직접 컬렉션에 저장할 수 있다. 컴파일러에 의해서 자동적으로 Wrapper클래스로 변환되어 저장되는 데 이것을 오토박싱(autoboxing)이라고 한다. 뿐만아니라 저장된 값을 꺼낼 때도 변환과정을 거치지 않고도 기본형 형태의 값을 바로 얻을 수 있는 데 이것을 언박싱(unboxing)이라고 한다.

ArrayList list = new ArrayList();
list.add(new Integer(10));
list.add(new Integer(20));
list.add(new Integer(30));
 
Integer i = (Integer)list.get(0);
int value = i.intValue();

이전에는 위와 같은 코드를 사용했지만 이제는 아래와 같이 코드를 간략히 할 수 있다.

ArrayList<Integer> list = new ArrayList<Integer>();
list.add(10); // 오토박싱(autoboxing)
list.add(20); // 오토박싱(autoboxing)
list.add(30); // 오토박싱(autoboxing)
 
int value = list.get(0); // 언박싱(unboxing)

향상된 for 문

배열과 컬렉션에 저장된 요소에 접근할 때 기존보다 편리한 방법으로 처리할 수 있도록 for문의 새로운 문법이 추가되었다.

1. 배열
    for( 배열의 타입 변수명 : 배열 ) {
        // 반복할 문장
    }
 
2. 컬렉션
    for( 컬렉션에 저장된 요소의 타입 변수명 : 컬렉션 ) {
        // 반복할 문장
    }

먼저, 정수형 배열이 다음과 같이 선언되어 있다고 가정하자.

int[] arr = {10, 20, 30, 40, 50};

아래의 두 for 문은 서로 동일하다.

for(int i=0 ; i < arr.length ; i++) {
    System.out.println(arr[i]);
}
for(int i : arr) {
    // arr[i]가 아닌 i라는 것에 유의
    System.out.println(i);
}

이번엔 ArrayList를 생성하고 다음과 같이 세 개의 Integer객체를 저장하고 각 요소에 접근하기 위해 Iterator를 얻었다.

ArrayList<Integer> list = new ArrayList<Ingeger>();
list.add(new Ingerger(10));
list.add(new Ingerger(20));
list.add(new Ingerger(30));
 
Iterator it = list.iterator();
for(;it.hasNext();) {
    System.out.println(it.next());
}
for(Integer i : list) {
    System.out.println(i);
}

향상된 for 문은 배열보다는 컬렉션에 저장된 요소에 접근할 때 더욱 코드가 간결해진다는 것을 알 수 있다. 한 가지 주의해야할 점은 향상된 for 문내에서 삭제와 같이 컬렉션을 변경하는 동작을 해서는 안 된다는 것이다.

제네릭스(Generics)

제네릭스는 컬렉션 프레임워크와 관련된 기능으로 컬렉션에 저장하는 객체의 타입을 컴파일시에 체크하기 때문에 객체의 타입 안정성을 높이고 꺼낼 때는 자동으로 형변환해주기 때문에 편리하다.

컬렉션에 저장할 객체의 타입을 저장하기 위해 아래와 같이 한다. 이렇게 하면 컬렉션에 저장할 수 있는 객체는 지정한 타입의 객체뿐이다.

컬렉션클래스<저장할 객체의 타입> = new 컬렉션클래스<저장할 객체의 타입>;

아래의 코드는 ArrayList에 Tank객체만 저장할 수 있도록 작성한 것이다. Tank객체 이외의 객체를 저장하려고 하면 컴파일 시에 에러가 발생한다.

// Tank클래스의 객체만을 저장할 수 있는 ArrayList를 생성
ArrayList<Tank> list = new ArrayList<Tank>();
list.add(new Tank());
list.add(new Dropship()); // 컴파일 에러 발생 !!!

저장된 객체를 꺼낼 때는 형변환하지 않아도 자동형변환이 된다. 제네릭스를 적용한 코드와 그렇지 않은 코드를 잘 비교해보도록 하자.

ArrayList list = new ArrayList();
list.add(new Tank());
Tank t = (Tank)list.get(0);
ArrayList<Tank> list = new ArrayList<Tank>();
list.add(new Tank());
Tank t = list.get(0);

만일 다형성을 사용해야 하는 경우에는 조상타입을 지정함으로써 여러 종류의 객체를 저장할 수 있다.

class Unit{}
class Tank extends Unit {}
class Dropship extends Unit {}
 
// Unit클래스의 자손객체들을 저장할 수 있는 ArrayList를 생성
ArrayList<Unit> list = new ArrayList<Unit>();
list.add(new Tank());     // 컴파일 에러가 발생하지 않는다.
list.add(new Dropship()); // 컴파일 에러가 발생하지 않는다.
 
Unit u = list.get(0);       // 형변환이 필요없다.
Tank t = (Tank)list.get(0); // 형변환을 필요로 한다.

ArrayList가 Unit의 객체를 저장하도록 지정하면, 이들의 자손인 Tank와 Dropship의 객체도 저장할 수 있다. 다만 꺼내올 때 원래의 타입으로 형변환해야 한다.

다음은 ArrayList의 실제 소스의 일부이다.

public class ArrayList<E> extends AbstractList<E>
       implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private transient E[] elementData;
    public boolean add(E e) { /* 내용생략 */ }
    public E get(int index) { /* 내용생략 */ }
    ...
}

위의 코드에 사용된 'E'는 객체의 타입을 의미한다. 'Element'의 약자로 지정한 것인데 어떤 문자를 사용해도 상관없지만 소스코드 내에서 같은 문자를 일관되게 사용해야 한다. 이는 마치 메서드의 매개변수이름을 다르게 해도 메서드 내에서만 같은 이름을 사용하면 문제없는 것과 같다. 이를 이해하기 어렵다면 'E'를 Object타입으로 이해해도 좋다. 예를 들어 ArrayList를 ArrayList<Tank> list = new ArrayList<Tank>();와 같이 생성하는 경우는 실제소스코드가 아래와 같이 작성되어 있는 셈이다. 'E'가 'Tank'로 치환된 것과 같다고 보면 된다.

public class ArrayList<Tank> extends AbstractList<Tank>
       implements List<Tank>, RandomAccess, Cloneable, java.io.Serializable
{
    private transient Tank[] elementData;
    public boolean add(Tank e) { /* 내용생략 */ }
    public Tank get(int index) { /* 내용생략 */ }
    ...
}
public static void printAll(ArrayList<Unit> list) {
    for(Unit u : list) {
        System.out.println(u);
    }
}

위와 같이 선언된 메서드가 있을 때, 이 메서드의 매개변수로는 ArrayList<Unit>타입의 변수만 사용할 수 있다. 즉, ArrayList<Unit> list = new ArrayList<Unit>();으로 생성된 객체만 매개변수로 사용될 수 있다는 뜻이다.

public static void printAll(ArrayList<? extends Unit> list) {
    for(Unit u : list) {
        System.out.println(u);
    }
}

하지만 위와 같이 'T'라는 타입이 있고 그 타입이 Unit의 자손이라고 선언하면 이 메서드의 매게변수는 Unit의 자손인 Tank과 Dropship을 저장하는 ArrayList<Tank>타입 또는 ArrayList<Dropship>타입의 참조변수를 지정할 수 있다.

만일 아래와 같은 같은 코드가 있다면 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다. 아래의 두 코드는 동등한 의미의 코드이므로 잘 비교해보자.

public static void printAll(ArrayList<? extends Unit> list, ArrayList<? extends Unit> list2) {
    for(Unit u : list) {
        System.out.println(u);
    }
}
public static <T extends Unit> void printAll(ArrayList<T> list, ArrayList<T> list2) {
    for(Unit u : list) {
        System.out.println(u);
    }
}

[주의] 여기서 만일 Unit이 클래스가 아닌 인터페이스라 할지라도 키워드로 'implements'를 사용하지 않고 클래스와 동일하게 'extends'를 사용한다는 것에 주의하자.

타입에 안전한 열거형 (Typesafe enums)

이전까지 자바는 C언어와 달리 열거형이라는 것이 존재하지 않았으나 새로 추가되었다. 자바의 열거형은 C언어의 열거형보다 더 향상된 개념의 열거형으로 열거형이 갖는 값 뿐만아니라 타입까지 관리하기 때문에 보다 논리적인 오류를 줄일 수 있다.

class Card {
    static final int CLOVER  = 0;
    static final int HEART   = 1;
    static final int DIAMOND = 2;
    static final int SPADE   = 3;
 
    static final int TWO     = 0;
    static final int THREE   = 1;
    static final int FOUR    = 2;
 
    final int kind;
    final int num;
}
class Card {
    enum Kind { CLOVER, HEART, DIAMOND, SPADE }
    enum Value { TWO, THREE, FOUR }
 
    final Kind kind;    // 타입이 int가 아닌 Kind임에 유의하자.
    final Value value;
}

이전 방식으로는 타입이 달라도 값이 같으면 조건식결과가 true였으나, 새로 도입된 타입에 안전한 열거형에서는 실제 값이 같아도 타입이 다르면 조건식의 결과가 false가 된다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 안전한(typesafe) 열거형이라고 하는 것이다.

if(Card.CLOVER == Card.TWO)            // true지만 false이어야 의미상 맞음.
if(Card.Kind.CLOVER == Card.Value.TWO) // false

가변인수 (Varargs)

기존에는 메서드의 매개변수 개수를 고정적으로 지정해주어야만 했으나 JDK 1.5 부터는 동적으로 매개변수의 개수를 지정해 줄 수 있게 되었다.

public String concatenate(String s1, String s2) { /* 내용생략 */ }
public String concatenate(String s1, String s2, String s3) { /* 내용생략 */ }
public String concatenate(String s1, String s2, String s3, String s4) { /* 내용생략 */ }
...
public String concatenate(String[] strArr) { /* 내용생략 */ }

문자열을 결합하여 반환하는 concatenate메서드를 위와 같이 오버로딩하여 정의하는 대신 아래와 같이 가변인수를 정의하면 된다. 가변인수는 '타입… 변수명'와 같은 형식으로 정의한다.

public String concatenate(String... str) { /* 내용생략 */ }

가변인수의 대표적인 예는 PrintStream의 printf()와 MessageFormat의 format()이다.

public PrintStream printf(String format, Object... args) { /* 내용생략 */ }
public static String format(String pattern, Object... arguments) { /* 내용생략 */ }

가변인수를 사용할 때 한 가지 주의해야할 점은 오버로딩을 하는 경우인데, 먼저 예제를 살펴보고 그 다음에 자세히 설명하겠다.

concatenate 메서드는 매개변수로 입력된 문자열에 구분자를 사이에 포함시켜 결합해서 반환한다. 가변인수로 매개변수를 선언했기 때문에 문자열을 개수의 제약없이 매개변수로 지정할 수 있다. 위의 예제에서는 주석처리하였지만 concatenate 메서드의 또 다른 오버로딩된 메서드가 있다. 이 두 메서드는 별 문제가 없어 보이지만 위의 예제에서 주석을 풀고 컴파일을 하면 아래와 같이 컴파일에러가 발생한다.

VarArgsEx1.java:5: reference to concatenate is ambigous, both method
concatenate(java.lang.String,java.lang.String...) in VarArgsEx1 and
method concatenate(java.lang.String...) in VarArgsEx1 match
         System.out.println(concatenate("-", "100", "200", "300"));

1 error

에러의 내용을 살펴보면 두 오버로딩된 메서드를 구분되지 않아서 발생하는 것임을 알 수 있다. 가변인수를 선언한 메서드를 오버로딩할 때는 이와 같이 구별되지 못하는 경우가 발생하기 쉽기 때문에 주의해야 한다. 그래서 가능하면 가변인수를 사용한 메서드를 오버로딩하지 않는 것이 좋다.

static import

import 문을 사용하면 클래스의 패키지명을 생략할 수 있는 것과 같이 static import 문을 사용하면 static 멤버를 호출할 때 클래스명을 생략할 수 있다. 이러한 기능은 코딩을 편리하게 해주지만 남용하면 오히려 독이 될 수 있기 때문에 주의하도록 하자.

import static java.lang.System.out;
import static java.lang.Math.*;

만일 위와 같이 static import 문을 선언하였다면, 아래의 코드처럼 간략히 할 수 있다.

System.out.println(Math.random());
out.println(random());