여러개의 파일을 하나로 모아 압축한 것을 아카이브(ARchive) 라고 한다. JAR 는 복수의 클래스 파일과 그 밖의 파일을 하나로 모아 압축한 파일 형식이다. 파일들을 모아 압축을 통해 용량을 줄일 수 있기 때문에 데이터를 교환하기에 편리하다.
일반적으로 자바 프로그래밍을 하다보면 jar 파일을 심심찮게 볼 수 있는데, 압축파일이기 때문에 알집 같은 압축,해제 프로그램으로 풀 수 있다. 즉, 다른 의미로 해석하자면, 굳이 jar 커맨드를 이용하지 않아도, 일반 압축 프로그램에서 jar 파일을 생성할 수 있다는 의미이다. 알집의 경우, 압축은 가능한데, 압축을 해제하기 전에 미리보기 하는 기능은 되지 않았다.
○ JAR 커맨드(명령) JAR 파일은 명령 프롬프트에서 jar 커맨드(명령)를 사용하여 만들어 낼 수 있다. jar 커맨드는 'jar' 뒤에 다음과 같은 옵션을 붙여서 사용한다.
옵션 : 설명 c : 새 아카이브를 만든다. t : 아카이브의 내용을 표시한다. //일반적으로 내용을 볼때는 t 옵션만 사용하지 않고, tf 옵션으로 한다. x : 아카이브에서 파일을 추출한다. u : 기존의 아카이브를 업데이트 한다. v : 명령 프롬프트에상세정보를 표시한다. f : 아카이브 파일 이름을 지정한다. m : manifest 정보를 포함시킨다. o : 저장만 수행하며 ZIP 압축을 사용하지 않는다. M : manifest 파일을 만들지 않는다. i : 지정된 jar 파일에 대한 색인 정보를 생성한다. C : 지정된 디렉토리로 변경하고, 다음 파일을 포함시킨다.
jar 옵션에 대한 사용법을 다시 살펴보면 -------------------------------------------------- 사용법: jar {ctxu}[vfm0Mi] [jar-file] [manifest-file] [-C dir] files ... 옵션: -c 새 아카이브를 만듭니다. -t 아카이브에 대한 목차를 나열합니다. -x 아카이브에서 명명된 (또는 모든) 파일을 추출합니다. -u 기존의 아카이브를 업데이트합니다. -v 표준 출력에 대한 자세한 정보 출력을 생성합니다. -f 아카이브 파일 이름을 지정합니다. -m 지정된 manifest 파일에서 manifest 정보를 포함시킵니다. -0 저장만 수행하며 ZIP 압축을 사용하지 않습니다. -M 입력 항목에 대한 manifest 파일을 만들지 않습니다. -i 지정된 jar 파일에 대한 색인 정보를 생성합니다. -C 지정된 디렉토리로 변경하고 다음 파일을 포함시킵니다. 디렉토리인 파일이 하나ㅏ도 있으면 재귀적으로 처리됩니다. 'm' 및 'f' 플래그가 지정된 순서대로 manifest 파일 이름과 아카이브 파일 이름을 지정해야 합니다.
예 1: classes.jar 라는 아카이브 파일에 두 클래스 파일을 아카이브하려면 jar cvf classes.jar Foo.class Bar.class dP 2: 기존의 manifest 파일 'mymanifest' 를 사용하고 foo/ 디렉토리에 있는 모든 파일을 'classes.jar' 로 아카이브합니다. jar cvfm classes.jar mymanifest -C foo/ . --------------------------------------------------
책에서는 o 옵션이 애매하게 표시되어 있는데, 명령 프롬프트 상에서 설명해주는 명령어는 영어 o 소문자가 아니라 숫자 0 이다.
간단한 작업을 할때는 한가지 옵션만을 사용하겠지만, 복잡한 명령을 수행할때는 명령 옵션을 조합해서 사용하는데, 만약, 파일 A.class, B.class 두개의 파일을 신규 작성한 AB.jar 에 저장하려 한다면,
>jar cf AB.jar A.class B.class
라고 한다. cf 옵션이 두개의 옵션을 조합해서 사용하는 명령인데, 새로 만드는 옵션인 c 와 지정한 파일명을 사용하는 f 를 조합한 것이다. 그래서, AB.jar 라는 지정된 파일명으로 새로 만들고, 그 안에 A.class 와 B.class 두개를 포함시키는 것이다. JAR 파일에 들어갈 파일명은 공백으로 구분해준다.
만약, JAR 파일 AB.jar 에 저장되어 있는 파일의 내용을 표시하려면,
>jar tf AB.jar
라고 한다. 여기서 t 옵션은 내용을 표시하도록 하는 옵션이고, f 옵션은 지정된 파일명을 의미한다. 이 옵션을 이용하면, 윈도우 탐색기에서 폴더트리를 보여주듯이, jar 파일안에 어떠한 파일들이 들어있는지 경로와 파일명들을 보여준다.
○ 매니페스트(manifest) 파일 매니페스트 파일은 JAR 파일에 들어 있는 파일의 정보를 기술한 파일이다. 매니페스트 파일을 지정하지 않고 아카이브를 생성하게 되면, 표준 매니페스트 파일이 포함된다. 아래와 같은 내용의 매니페스트 파일을 작성하고, JAR 파일에 포함시켜두면, JAR 파일을 직접 실행할 수 가 있다.
main-class: A
실행하고 싶은 클래스명을 적어주는 것이다. 클래스명을 적을때는, 클래스명 앞에서는 스페이스가 반드시 있어야 하며, 뒤에는 개행문자가 필요하다.
○ JAR 파일의 실행 압축을 해제하지 않은 상태에서 바로 실행할 수 있는 JAR 파일을 작성하려면 다음과 같이 기술한다.
(Hello.java)---------------------- class Hello{ void hello(){ System.out.println("Hello"); } } ----------------------------------
(Bye.java)------------------------ class Bye{ void bye(){ System.out.println("Bye"); } } ----------------------------------
(a.java)-------------------------- class a{ public static void main(String [] args){ Hello h = new Hello(); Bye b = new Bye(); h.hello(); b.bye(); } } ----------------------------------
이렇게 소스를 작성한뒤 컴파일을 한다. 실행할 클래스명을 기술한 manifest.txt 라는 이름의 매니페스트 파일을 만든다.
즉, jar 파일로 압축하면서 매니페스트파일에 어떤 클래스를 시작클래스로 할것인지 지정하였기 때문에, a.class 가 시작되며, a.class 에서는 마치 패키지에서 다른 클래스들을 포함시켜서 사용하듯이, Hello.class 와 Bye.class 의 클래스들을 불러와써 쓸 수 있는 것이다.
요약: 많은 Java™ 개발자들이 JAR의 기본 기능만을 사용하고 있습니다. 단순히 JAR을 사용하여 클래스를 번들로 묶어서 프로덕션 서버로 전달하고 있을 뿐입니다. 하지만 JAR은 단순히 이름이 바뀐 ZIP 파일에 그치지 않고 그 이상의 기능을 제공합니다. Spring 종속성 및 구성 파일을 jar로 작성하는 방법에 대한 팁을 비롯하여 Java Archive 파일을 최대한으로 활용하는 방법에 대해 살펴봅니다.
대부분의 Java 개발자에게 JAR 파일과 그 특별한 사촌인 WAR 및 EAR은 단순히 장기적인 Ant 또는 Maven 프로세스의 최종 결과에 불과하다. 일반적으로 JAR을 서버(또는 드물게 사용자의 시스템)의 올바른 위치에 복사한 다음 잊어버린다.
실제로 JAR은 소스 코드를 저장하는 것 이상의 작업을 수행할 수 있지만 사용자가 가능한 작업과 작업 요청 방법을 알아야 한다.5가지 사항시리즈의 이 기사에서 제공하는 팁에서는 Java Archive 파일(및 WAR와 EAR)을 특히, 배치 시간에 최대한으로 활용하는 방법에 대해 설명한다.
많은 Java 개발자가 Spring을 사용하고 있고 Spring 프레임워크에서 일반적인 JAR 사용 방법이 문제가 되는 경우가 있기 때문에 일부 팁에서는 특별히 Spring 애플리케이션에서 JAR을 사용하는 방법에 대해 설명한다.
이 시리즈의 정보
Java 프로그래밍에 대해 알고 있다고 생각하는가? 하지만 실제로는 대부분의 개발자가 작업을 수행하기에 충분할 정도만 알고 있을 뿐 Java 플랫폼에 대해서는 자세히 알고 있지 않다. 이시리즈에서 Ted Neward는 Java 플랫폼의 핵심 기능에 대한 자세한 설명을 통해 까다로운 프로그래밍 과제를 해결하는 데 도움이 되는 알려져 있지 않은 사실을 밝힌다.
먼저 이후 팁에서 기본적으로 사용할 표준 Java Archive 파일 프로시저를 보여 주는 간단한 예제를 살펴보자.
일반적으로 코드 소스를 컴파일한 후jar명령행 유틸리티 또는 더 일반적으로 Antjar태스크를 통해 Java 코드(패키지별로 구분됨)를 단일 콜렉션으로 수집하여 JAR 파일을 빌드한다. 이 프로세스는 매우 간단하기 때문에 여기에서는 다루지 않겠지만 이 기사의 뒷부분에서 JAR의 생성 방법에 대한 주제를 다시 살펴볼 것이다. 지금은 단지 메시지를 콘솔에 인쇄하는 매우 유용한 태스크를 수행하는 독립형 콘솔 유틸리티인Hello를 아카이브해야 한다(Listing 1 참조).
.NET, C++ 등의 언어는 역사적으로 OS 친화적인 장점을 가지고 있다. 즉, 명령행에서 단순히 해당 이름을 참조하거나(helloWorld.exe) GUI 쉘에서 해당 아이콘을 두 번 클릭하여 애플리케이션을 실행할 수 있다. 하지만 Java 프로그래밍에서는 실행 애플리케이션인java가 JVM을 프로세스로 부트스트랩하고, 사용자가 실행할main()메소드가 있는 클래스를 나타내는 명령행 인수(com.tedneward.Hello)를 전달해야 한다.
이러한 추가 단계로 인해 Java에서는 사용자 친화적인 애플리케이션을 작성하기가 더 어렵다. 일반 사용자가 이러한 모든 요소를 명령행에 입력해야 한다. 하지만 이 작업은 많은 일반 사용자가 원하지 않는 것이다. 게다가 키보드를 잘못 눌러서 모호한 오류가 발생할 수도 있다.
이 문제를 해결하는 방법은 JAR 파일을 "실행 파일"로 만드는 것이다. 즉, Java 실행 프로그램이 JAR 파일을 실행할 때 시작할 클래스를 자동으로 인식할 수 있도록 만들면 된다. 이를 위해서는 다음과 같이 JAR 파일의 매니페스트(JAR의META-INF서브디렉토리에 있는MANIFEST.MF)에 항목을 추가하면 된다.
매니페스트는 이름/값 쌍으로 구성된 세트이다. 매니페스트에서는 캐리지 리턴과 공백으로 인해 문제가 발생할 수도 있기 때문에 JAR을 빌드할 때 Ant를 사용하여 매니페스트를 생성하는 방법이 가장 쉬운 방법이다. Listing 3에서는 Antjar태스크의manifest요소를 사용하여 매니페스트를 지정한다.
Hello유틸리티의 단어가 너무 많이 사용되고 있기 때문에 구현을 변경할 필요성이 제기되고 있다. Spring 또는 Guice와 같은 DI(Dependency Injection) 컨테이너가 많은 세부 사항을 자동으로 처리하기는 하지만 DI 컨테이너의 사용을 포함하도록 코드를 수정할 경우 Listing 4와 같은 결과가 발생할 수 있다는 문제점이 아직도 남아 있다.
package com.tedneward.jars;
import org.springframework.context.*;
import org.springframework.context.support.*;
public class Hello
{
public static void main(String[] args)
{
ApplicationContext appContext =
new FileSystemXmlApplicationContext("./app.xml");
ISpeak speaker = (ISpeak) appContext.getBean("speaker");
System.out.println(speaker.sayHello());
}
}
Spring에 대한 추가 정보
이 팁에서는 독자가 종속성 주입 및 Spring 프레임워크에 익숙하다고 간주하고 있다. 두 주제에 대해 자세히 알고 싶으면참고자료섹션을 참조하기 바란다.
실행 프로그램에 대한-jar옵션은-classpath명령행 옵션의 내용을 겹쳐쓰기 때문에 이 코드를 실행할 때 Spring이CLASSPATH및환경 변수에 있어야 한다. 다행스럽게도 JAR에서는 매니페스트에 표시할 다른 JAR 종속성을 선언할 수 있다. 이렇게 하면 사용자가 선언하지 않아도 CLASSPATH가 암묵적으로 작성된다(Listing 5 참조).
Class-Path속성에는 애플리케이션에서 사용하는 JAR 파일에 대한 상대 참조가 있다. 이 참조는 절대 참조로 작성하거나 접두부 없이 작성할 수 있으며, 이 경우 JAR 파일은 애플리케이션 JAR과 같은 디렉토리에 있는 것으로 간주된다.
아쉽게도 AntClass-Path속성에 대한value속성은 한 행으로 표시되어야 한다. 왜냐하면 JAR 매니페스트가 여러Class-Path속성을 처리하지 못하기 때문이다. 따라서 이러한 모든 종속성은 매니페스트 파일에 한 행으로 표시되어야 한다. 물론 보기에 좋지는 않지만java -jar outapp.jar을 사용할 수 있기에 그 가치는 충분하다고 할 것이다.
Spring 프레임워크를 사용하는 여러 다양한 명령행 유틸리티(또는 기타 애플리케이션)가 있다면 모든 유틸리티에서 참조할 수 있는 공통 위치에 Spring JAR 파일을 저장하여 효율을 높일 수 있다. 이렇게 하면 전체 파일 시스템 내에서 JAR 사본을 여러 개 유지하지 않아도 된다. Java 런타임에서 JAR에 대한 공통 위치로 사용하는 "확장 디렉토리"는 기본적으로 설치된 JRE 경로 아래의lib/ext서브디렉토리에 있다.
JRE는 사용자 정의할 수 있는 위치이지만 지정된 Java 환경 내에서 사용자 정의되는 경우가 거의 없기 때문에lib/ext가 JAR을 저장하기에 안전한 위치이고 Java 환경의CLASSPATH에서 JAR을 암묵적으로 사용할 수 있다고 생각해도 무방하다.
CLASSPATH환경 변수(몇 년 전에 Java 개발자가 남겨 두어야 했던)와 명령행-classpath매개변수가 많아지는 것을 피하기 위해 Java 6에서는클래스 경로 와일드카드라는 개념이 도입되었다. 클래스 경로 와일드카드를 사용하면 인수에 명시적으로 나열된 모든 JAR 파일을 하나하나 실행할 필요 없이lib/*와 해당 디렉토리에 나열된 모든 JAR 파일(재귀적이지 않음)을 클래스 경로에 지정할 수 있다.
아쉽게도 클래스 경로 와일드카드는 앞에서 설명한Class-Path속성 매니페스트 항목에 적용되지 않는다. 하지만 코드 생성 도구 또는 분석 도구와 같은 개발자 태스크를 위한 Java 애플리케이션(서버 포함)을 쉽게 실행할 수 있다.
Java 에코시스템의 많은 부분과 마찬가지로 Spring도 환경의 설정 방법을 설명하는 구성 파일을 사용한다. 즉, Spring에서는 JAR 파일과 같은 디렉토리에 있는 app.xml 파일을 사용한다. 하지만 대부분의 개발자는 구성 파일을 JAR 파일과 함께 복사하는 것을 잊어버린다.
일부 구성 파일은 시스템 관리자가 편집할 수 있지만 하이버네이트 맵핑과 같은 상당 수의 구성 파일은 시스템 관리자 도메인의 외부에 있기 때문에 배치 작업 중에 발생하는 문제의 원인이 되고 있다. 합리적인 해결 방법은 구성 파일과 코드를 함께 패키지하는 것이다. 이는 JAR이 기본적으로 외형 상 ZIP 형식으로 되어 있기 때문에 가능한 방법이다. JAR을 빌드할 때 구성 파일을 Ant 태스크나jar명령행에 포함시키면 된다.
JAR은 구성 파일뿐만 아니라 다른 유형의 파일도 포함할 수 있다. 예를 들어, 필자는 특성 파일에 액세스할 필요가 있는SpeakEnglish컴포넌트를 Listing 6과 같이 설정했다.
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
Random random = new Random();
public String sayHello()
{
// Pick a response at random
int which = random.nextInt(5);
return responses.getProperty("response." + which);
}
}
responses.properties를 JAR 파일에 넣는다는 것은 JAR 파일과 함께 배치할 파일이 하나 이상 있다는 것을 의미한다. 이 작업을 수행하려면 JAR 단계 동안 responses.properties 파일을 포함시켜야 한다.
특성을 JAR에 저장한 후 특성을 다시 가져오는 방법이 궁금할 수도 있을 것이다. 원하는 데이터가 동일한 JAR 파일 내에 함께 있는 경우에는 이전 예제에서 보았듯이 JAR 파일의 위치를 확인한 후JarFile오브젝트를 사용하여 JAR 파일을 열려고 시도하지 않아도 된다. 대신ClassLoader getResourceAsStream()메소드를 사용하여 클래스의ClassLoader를 통해 JAR 파일 내에서 특성을 "자원"으로 찾는다(Listing 7 참조).
이 기사에서는 적어도 역사와 필자의 경험을 바탕으로 대부분의 Java 개발자가 JAR에 대해 모르고 있던 5가지 주요 사항을 살펴보았다. 이러한 JAR 관련 팁은 모두 WAR에도 동일하게 적용된다. WAR의 경우에는 서블릿 환경이 디렉토리의 전체 내용을 선택하고 사전 정의된 시작점이 있기 때문에 일부 팁(특히,Class-Path및Main-Class속성)의 중요성이 낮을 수 있다. 하지만 전체적인 관점에서 이러한 팁은 "좋아, 이 디렉토리의 모든 항목을 복사하는 작업부터 시작하자..."라는 패러다임을 극복할 수 있도록 도와 준다. 결과적으로 Java 애플리케이션을 훨씬 더 간단하게 배치할 수도 있다.
이 시리즈의 다음 기사에서는 Java 애플리케이션의 성능 모니터링에 대해 모르고 있던 5가지 사항에 대해 살펴본다.
내부클래스의 마지막 !! 익명 클래스입니다. 말그대로 내부클래스의 이름이 존재하지 않는건데요.-ㅅ-; 다음과 같은 인터페이스가 있고, 그 인터페이스를 구현하는 클래스의 이용빈도수가 현저하게 적을 경우, 클래스를 새로 만드는 것은 낭비일 수 있습니다. 그밖에도 급하게 어떤 인터페이스의 인스턴스가 필요한데, 해당 인터페이스를 구현한 클래스도 만들어놓지 않은 경우 등등 익명클래스는 거의 일회성으로 많이 사용됩니다. 아래와 같은 인터페이스가 있고, 그 인터페이스를 구현하는 클래스를 즉석에서 만들면,
내부클래스란, 클래스 내에 선언된 클래스이다. 클래스에 다른 클래스 선언하는 이유는 간단하다. 두 클래스가 서로 긴밀한 관계에 있기 때문이다. . 한 클래스를 다른 클래스의 내부클래스로 선언하면 두 클래스의 멤버들간에 서로 쉽게 접근할 수 있다는 것과 외부에는 불필요한 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점을 얻을 수 있다.
.내부클래스의 장점 - 내부클래스에서 외부클래스의 멤버들을 쉽게 접근할 수 있다. - 코드의 복잡성을 줄일 수 있다.(캡슐화)
[참고]내부 클래스는 JDK1.1버젼 이후에 추가된 개념이다.
왼쪽의 A와 B 두 개의 독립적인 클래스를 오른쪽과 같이 바꾸면 B는 A의 내부클래스(inner class)가 되고 A는 B를 감싸고 있는 외부클래스(outer class)가 된다. 이 때 내부클래스인 B는 외부클래스인 A를 제외하고는 다른 클래스에서 사용되지 않아야한다.
내부클래스는 주로 AWT나 Swing과 같은 GUI어플리케이션의 이벤트처리 외에는 잘 사용하지 않을 정도로 사용빈도가 높지 않으므로 내부클래스의 기본 원리와 특징을 이해하는 정도까지만 학습해도 충분하다. 실제로는 발생하지 않을 경우까지 이론적으로 만들어 내서 고민하지말자.
내부클래스는 클래스 내에 선언된다는 점을 제외하고는 일반적인 클래스와 다르지 않다. 다만 앞으로 배우게 될 내부클래스의 몇 가지 특징만 잘 이해하면 실제로 활용하는데 어려움이 없을 것이다.
2. 내부클래스의 종류와 특징
내부클래스의 종류는 변수의 선언위치에 따른 종류와 같다. 내부클래스는 마치 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 변수의 선언위치에 따라 인스턴스변수, 클래스변수(static변수), 지역변수로 구분되는 것과 같이 내부클래스도 선언위치에 따라 다음과 같이 구분되어 진다. 내부클래스의 유효범위와 성질이 변수와 유사하므로 서로 비교해보면 이해하는데 많은 도움이 된다.
인스턴스클래스 (instance class)
외부클래스의 멤버변수 선언위치에 선언하며, 외부클래스의 인스턴스멤버처럼 다루어진다. 주로 외부클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다.
스태틱클래스 (static class)
외부클래스의 멤버변수 선언위치에 선언하며, 외부클래스의 static멤버처럼 다루어진다. 주로 외부클래스의 static멤버, 특히 static메서드에서 사용될 목적으로 선언된다.
지역클래스 (local class)
외부클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
익명클래스 (anonymous class)
클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)
[표10-1]내부클래스의 종류와 특징 [참고]초기화블럭 관련내용은 1권 p.167를 참고
3. 내부클래스의 선언
아래의 오른쪽 코드에는 외부클래스(Outer)에 3개의 서로 다른 종류의 내부클래스를 선언했다. 양쪽의 코드를 비교해 보면 내부클래스의 선언위치가 변수의 선언위치와 동일함을 알 수 있다. 변수가 선언된 위치에 따라 인스턴스변수, 스태틱변수(클래스변수), 지역변수로 나뉘듯이 내부클래스도 이와 마찬가지로 선언된 위치에 따라 나뉜다. 그리고, 각 내부클래스의 선언위치에 따라 같은 선언위치의 변수와 동일한 유효범위(scope)와 접근성(accessibility)을 갖는다.
4. 내부클래스의 제어자와 접근성
아래코드에서 인스턴스클래스(InstanceInner)와 스태틱클래스(StaticInner)는 외부클래스(Outer)의 멤버변수(인스턴스변수와 클래스변수)와 같은 위치에 선언되며, 또한 멤버변수와 같은 성질을 갖는다. 따라서 내부클래스가 외부클래스의 멤버와 같이 간주되고, 인스턴스멤버와 static멤버간의 규칙이 내부클래스에도 똑같이 적용된다.
내부클래스도 클래스이기 때문에 abstract나 final과 같은 제어자를 사용할 수 있을 뿐만 아니라, 멤버변수들처럼 private, protected과 접근제어자도 사용이 가능하다.
[예제10-1] InnerEx1.java
class InnerEx1 { class InstanceInner { int iv=100; // static int cv=100; // 에러! static변수를 선언할 수 없다. finalstaticint CONST = 100; // static final은 상수이므로 허용한다.
} staticclass StaticInner { int iv=200; staticint cv=200; // static클래스만 static멤버를 정의할 수 있다. }
void myMethod() { class LocalInner { int iv=300; // static int cv=300; // 에러! static변수를 선언할 수 없다. finalstaticint CONST = 300; // static final은 상수이므로 허용한다. } }
[참고]final이 붙은 변수는 상수(constant)이기 때문에 어떤 경우라도 static을 붙이는 것이 가능하다.
내부클래스 중에서 스태틱클래스(StaticInner)만 static멤버를 가질 수 있다. 드문 경우지만 내부클래스에 static변수를 선언해야한다면 스태틱클래스로 선언해야한다. 다만 final과 static이 동시에 붙은 변수는 상수이므로 모든 내부클래스에서 정의가 가능하다.
[예제10-2] InnerEx2.java
class InnerEx2 { class InstanceInner {} staticclass StaticInner {}
InstanceInner iv = new InstanceInner(); // 인스턴스 멤버간에는 서로 직접 접근이 가능하다. static StaticInner cv = new StaticInner(); // static 멤버간에는 서로 직접 접근이 가능하다.
staticvoid staticMethod() { // static멤버는 인스턴스 멤버에 직접 접근할 수 없다. // InstanceInner obj1 = new InstanceInner(); StaticInner obj2 = new StaticInner();
// 굳이 접근하려면 아래와 같이 객체를 생성해야한다. // 인스턴스 내부클래스는 외부클래스를 먼저 생성해야만 생성할 수 있다. InnerEx2 outer = new InnerEx2(); InstanceInner obj1 = outer.new InstanceInner(); }
void instanceMethod() { // 인스턴스 메서드에서는 인스턴스멤버와 static멤버 모두 접근 가능하다. InstanceInner obj1 = new InstanceInner(); StaticInner obj2 = new StaticInner(); // 메서드 내에 지역적으로 선언된 내부클래스는 외부에서 접근할 수 없다. // LocalInner lv = new LocalInner(); }
void myMethod() { class LocalInner {} LocalInner lv = new LocalInner(); } }
인스턴스멤버는 같은 클래스에 있는 인스턴스멤버와 static멤버 모두 직접 호출이 가능하지만, static멤버는 인스턴스멤버를 직접 호출할 수 없는 것처럼, 인스턴스클래스는 외부클래스의 인스턴스멤버를 객체 생성없이 바로 사용할 수 있지만, 스태틱클래스는 외부클래스의 인스턴스멤버를 객체 생성없이 사용할 수 없다.
마찬가지로 인스턴스클래스는 스태틱클래스의 멤버들을 객체생성없이 사용할 수 있지만, 스태틱클래스에서는 인스턴스클래스의 멤버들을 객체생성없이 사용할 수 없다.
class InstanceInner { int iiv = outerIv; // 외부클래스의 private멤버도 접근가능하다. int iiv2 = outerCv; }
staticclass StaticInner { // static클래스는 외부클래스의 인스턴스 멤버에 접근할 수 없다. // int siv = outerIv; staticint scv = outerCv; }
void myMethod() { int lv = 0; finalint LV = 0;
class LocalInner { int liv = outerIv; int liv2 = outerCv; // 외부클래스의 지역변수는 final이 붙은 변수(상수)만 접근가능하다. // int liv3 = lv; int liv4 = LV; } } }
내부클래스에서 외부클래스의 변수들에 대한 접근성을 보여 주는 예제이다. 내부클래스의 전체 내용 중에서 제일 중요한 부분이므로 잘봐두도록 하자.
인스턴스클래스(InstanceInner)는 외부클래스(InnerEx3)의 인스턴스멤버이기 때문에 인스턴스변수 outerIv와 static변수 outerCv를 모두 사용할 수 있다. 심지어는 outerIv의 접근제어자가 private일지라도 사용가능하다.
스태틱클래스(StaticInner)는 외부클래스(InnerEx3)의 static멤버이기 때문에 외부클래스의 인스턴스멤버인 outerIv와 InstanceInner를 사용할 수 없다. 단지 static멤버인 outerCv만을 사용할 수 있다.
지역클래스(LocalInner)는 외부클래스의 인스턴스멤버와 static멤버를 모두 사용할 수 있으며, 지역클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다. 단, final이 붙은 지역변수만 접근가능한데 그 이유는 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.
[예제10-4] InnerEx4.java
class Outer { class InstanceInner { int iv=100; } staticclass StaticInner { int iv=200; staticint cv=300; }
void myMethod() { class LocalInner { int iv=400; } } }
class InnerEx4 { publicstaticvoid main(String args[]) { // 인스턴스 내부클래스의 인스턴스를 생성하려면 // 외부클래스의 인스턴스를 먼저 생성해야한다. Outer oc = new Outer(); Outer.InstanceInner ii = oc.new InstanceInner();
컴파일 했을 때 생성되는 파일명은 '외부클래스명$내부클래스명.class'형식으로 되어 있다. 다만 서로 다른 메서드 내에서는 같은 이름의 지역변수를 사용하는 것이 가능한 것처럼, 지역내부클래스는 다른 메서드에 같은 이름의 내부클래스가 존재할 수 있기 때문에 내부클래스명 앞에 숫자가 붙는다.
class Inner { int value=20; // this.value void method1() { int value=30; System.out.println(" value :" + value); System.out.println(" this.value :" + this.value); System.out.println("Outer.this.value :" + Outer.this.value); } } // Inner클래스의 끝 } // Outer클래스의 끝
class InnerEx5 { publicstaticvoid main(String args[]) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.method1(); } } // InnerEx5 끝
[실행결과]
value :30 this.value :20 Outer.this.value :10
위의 예제는 내부클래스와 외부클래스에 선언된 변수의 이름이 같을 때 변수 앞에 'this' 또는 '외부클래스명.this'를 붙여서 서로 구별할 수 있다는 것을 보여준다.
5. 익명클래스(anonymous class)
이제 마지막으로 익명 클래스에 대해서 알아보도록 하자. 익명클래스는 특이하게도 다른 내부클래스들과는 달리 이름이 없다. 클래스의 선언과 객체의 생성을 동시에 하기 때문에 한 단번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.
이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 하나 이상의 인터페이스를 구현할 수 없다. 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.
익명클래스는 구문이 다소 생소하지만, 인스턴스클래스를 익명클래스로 바꾸는 연습을 몇 번만 해 보면 금새 익숙해 질 것이다.
[예제10-6] InnerEx6.java
class InnerEx6{ Object iv = newObject(){ void method(){} }; // 익명클래스 staticObject cv = newObject(){ void method(){} }; // 익명클래스
제너릭 타입(Generic Types)은 주로 자바 컬렉션에서 많이 사용되고 있다. 컬렉션은 자료구조이다. 컬렉션에는 어떤 자료를 담을지 알 수 없으므로 최상위 객체인 Object형태로 저장되고 관리되도록 설계되어 있다. 하지만, 의도하지 않은 자료형이 담기는 경우도 발생하게 된다. 이 때의 오류는 컴파일시에는 알 수가 없고 실행을 시켜보아야만 알 수 있다는 것이 문제점이었다. 제너릭 타입을 사용하면 프로그래머가 원하는 객체의 타입을 명시해서 의도하지 않은 객체는 저장될 수 없도록 컴파일시에 오류를 확인할 수있게 된다.
제너릭클래스 정의하기
제네릭 클래스를 정의하는 방법은 일반적인 클래스를 정의하는 것과 동일하다. 다만, 클래스명 뒤에 <제너릭타입, ...>이라고 덧붙여 준다.
public class Box<T> {
private T t; // T stands for "Type"
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
제너릭 클래스 선언 / 생성
일반적인 클래스와 동일하게 선언하고 생성할 수 있다. 다만, 클래스명 뒤에 <제너릭타입>을 덧붙여주면 된다.
Box<Integer> integerBox;
integerBox = new Box<Integer>();
제너릭 타입에 사용되는 파라미터
타입 매개변수는 하나의 대문자를 사용한다. 이들은 파일시스템에 실재로 존재하는 것은 아니다. 즉, T.java 라던지 T.class라는 파일은 없다. 타입매개변수를 여러개 사용할 수도 있지만 하나의 선언문에서 두 번 사용될 수는 없다. 즉, Box<T, U>는 가능하지만 Box<T, T>는 안된다.
E - Element (자바의 컬렉션에서 널리 사용되고 있다.)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types
제너릭 메서드 / 제너릭 생성자
타입 매개변수가 메서드의 선언 등에 사용될 수도 있다. 단, 매개변수의 범위가 메서드의 블록 이내로 한정된다.
이제, 객체지향의 '이다 (is a)'관계를 생각해 볼 때다. Integer는 Object에 할당할 수 있다. '이다'관계에 있기 때문이다. 마찬가지로 Number에 Integer를 할당할 수도 있고, Double을 할당할 수도 있다. 이러한 관계는 제너릭에서도 마찬가지이다.
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
그러나,
public void boxTest(Box<Number> n){
// method body omitted
}
이 경우에 Box<Integer>와 Box<Double>는 매개변수로 전달되지 않는다. 이것들은 Box<Number>의 하위타입이 아니기 때문이다. 꽤나 논리적인 내용 전개가 필요하지만 뭔말인지 헷갈리므로 그냥 패스~
와일드카드
?는 알 수 없는 타입을 뜻한다.
<?> - 모든 객체 자료형, 내부적으로는 Object로 인식한다.
<? super 객체자료형> - 명시된 객체자료형과 그 상위 객체, 내부적으로는 Object로 인식한다.
J2SE 5.0에서 가장 두드러진 특징 중의 하나는 제네릭(Generic) 프로그래밍을 지원한다는 것이다. 제네릭 프로그래밍이란 효율적인 알로그림의 추상적인 형태로 표현하기 위한 프로그래밍 기법이다(Generic Programming is a programming mehod that is based in finding the most abstract representations of efficient algorithms. - Alexander Stepanov 정의, WIKI 사이트 참조).
자바는 제네릭 프로그래밍을 위해서 제네릭 타입과 메소드를 제공한다. 자바 제네릭 프로그래밍은 기존 C++언어의 템플릿과 유사한 점도 있지만 차이점도 많이 갖고 있다. 이러한 차이점들은 C++ 템플릿에 비해 자바 제네릭 프로그래밍에 여러 가지 장점을 제공한다. 자바 제네릭 프로그래밍은 C++ 의 템플릿에 대해서 다음과 같은 장점을 갖는다.
컴파일 시 타입 체킹 가능 - 자바 제네릭 프로그래밍은 컴파일 시에 타입 체킹이 가능하기 때문에 실행 시에 형변환에서 발생할 수 있는 많은 에러를 방지할 수 있다.
하나의 컴파일 된 코드 생성 - C++의 템플릿은 실제로 사용되는 데이터에 따라 여러 개의 컴파일된 코드를 생성하는 데 비해서 자바는 하나의 컴파일된 코드를 생성한다.
소스 코드 불필요 - C++의 템플릿을 사용하는 경우에 템플릿을 사용하기 위해서는 템플릿 소스 코드가 필요하지만, 자바 제네릭 프로그래밍을 사용하는 경우에는 컴파일된 라이브러리만 존재하면 된다.
● 제네릭 클래스, 인터페이스
자바에서 제네릭 클래스, 인터페이스, 메소드는 '<' 과 '>' 문자를 이용해서 표현한다. 예를 들어, GList라는 제네릭 클래스는 다음과 같은 형태로 정의할 수 있다. 이 때 E는 타입을 표현하기 위해서 사용되며, 포멀 파라미터 타입(Formal parameter type)이라고 한다.
제네릭 Glist 클래스 정의
class GLisst<E> { void add(E x) { ... } ... }
정의된 제네릭 클래스는 생성해서 사용할 수 있다. 이 때 제네릭 클래스를 생성할 때 사용되는 타입( 예 : Integer )을 Actual Type Argument라고 한다. 또한 제네릭 타입 선언을 위해 호출하는 것( 예 : GList<Integer> )을 파라미터화된 타입이라고 한다.
● List 인터페이스 사용
GList<Integer> myList = new GList<Integer>();
파라미터화된 타입(parameterized type)은 클래스 혹은 인터페이스 이름 C와 파라미터 섹션에 해당되는 <T1, ... , Tn>으로 구성된다. 즉, C<T1, ... , Tn>으로 표현된다. 파라미터화된 타입은 다음과 같은 형태로 선언될 수 있다.
형태 : 파라미터화된 타이(Parameterized type)
Class Or Interface < ReferenceType [, ReferenceType ] >
다음 예는 파라미터화된 타입을 선언하는 것을 보여준다.
파라미터화된 타입
Vector<String>
Seq<Seq<A>>
Seq<String>.Zipper<Integer>
Collection<Integer>
Paint<String, String>
J2SE 5.0 에서 작성된 제네릭 프로그램은 컴파일된 후에 J2SE 1.4의 JVM에서도 실행될 수 있다. 이것은 제네릭 특성을 기존 JVM에서도 호환성 있도록 변환하기 떄문에 가능하다. 이처럼 제네릭 프로그램을 제네릭을 사용하지 않는 형태로 변환하는 것을 타입 제거(Type erasure)라고 한다.
java.util 패키지의 자바 컬렉션(Collection) 클래스들은 기본적으로 제네릭 프로그래밍을 지원하도록 만들어졌다. 예를 들어, java.util 패키지의 Vector 클래스도 제네릭 클래스 형태로 정의되어 있다. 따라서 우리는 Vector 클래스를 제네릭 프로그래밍 방법으로 사용할 수 있다. 다음의 StrinVector.java 예제는 Vector 를 이용해서 제네릭 프로그래밍을 사용하는 방법을 보여준다. 제네릭 프로그래밍을 사용하는 경우에 보다 편리하게 프로그래밍을 작성할 수 있다.
ex) StringVector.java
import java.util.*;
public class StringVector {
public static void main( String args[] ) {
Vector<String> v = new Vector<String>(); // 문자열을 원소로 갖는 백터 객체 v를 생성한다. v.addElement("Hello"); v.addElement("World!!"); //v.add(5); 컴파일시 에러 발생, 5는 String 타입이 아니다.
for ( String s : v ) { //for 문을 이용해서 백터에 포함된 원소들을 찾아서 출력한다. System.out.println( s ); } }
}
ex) NormalVector.java (제네릭 프로그래밍 방법을 사용하지 않았을 경우)
import java.util.*;
public class NormalVector {
public static void main( String args[] ) { Vector v = new Vector(); // 문자열을 원소로 갖는 백터 객체 v를 생성한다. v.addElement("Hello"); v.addElement("World!!"); //v.add(5); 컴파일시 에러 발생, 5는 String 타입이 아니다. int n = v.size(); for ( int i = 0 ; i < n ; i++ ) { //for 문을 이용해서 백터에 포함된 원소들을 찾아서 출력한다. String s = (String) v.elementAt( i ); System.out.println( s ) } }
}
제네릭 클래스를 사용할 때 타입 파라메터를 기술하지 않는 경우에 컴파일 시에 경고 메시지가 출력된다. -Xlint 옵션을 이용해서 컴파일 하면, 이 경고 메시지를 볼 수 있다. Note: VectorTest.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
ex) ValueWrapper.java ( 타입 파라메터 T를 갖고, T 타입의 멤버 필드 value와 value() 메소드를 갖는다.)
public class ValueWrapper<T> { // ValueWrapper 클래스는 타입 파라메터 T를 갖는 제네릭 클래스이다.
private T value; // value 멤버필드는 T타입이다.
public ValueWrapper(T value) { //ValueWrapper 의 생성자, this.value = value; }
public T value() { return value; // value() 메소드는 T 타입의 값을 리턴한다. }
public static void main(String[] args) { ValueWrapper<String> sf = new ValueWrapper<String>("Hello"); //<String>타입의 ValueWrapper의 객체 sf 생성 System.out.println( sf.value() );
ValueWrapper<Integer> si = new ValueWrapper<Integer>(new Integer(10)); // <Integer>타입은 <String>타입이 아니기 떄문에, new로 객체선언 해줘야한다. System.out.println( si.value() );
} }
실행결과( 객체 생성시 타입 파라메터 T를 <String>이나 <Integer>로 설정할 때 다른 값을 출력하는 것을 확인할 수 있다)
Hello 10
자바의 제네릭 프로그래밍은 JVM은 변경하지 않으면서 새로운 기능을 제공한다. 따라서 J2SE 5.0 이전에 작성된 프로그램들과도 호환성이 유지된다. 예를 들어, Vector 클래스는 제네릭 클래스로 정의되어 있지만, 타입을 갖기 않는 다음과 같은 형태로 사용할 수도 있다.
ex) 타입이 없는 경우
Vector v = new Vector();
이처럼 제네릭 클래스에서 타입 파라미터를 사용하지 않는 것을 로타입(Law Type)이라고 한다. 앞의 예에서 v는 로타입이다. 로타입을 사용할 경우에는 Object 클래스가 타입 파라메터로 사용된다. 파라미터화된 클래스 타입에서 로타입으로 할당은 가능하지만, 안전하지 않기 때문에 컴파일 시에 경고 메시지를 출력한다.
ex) Cell.java
class Cell<E> { private E value;
public Cell(E v) { value = v; }
public E get() { return value; }
public void set(E v) { value = v; }
public static void main(String[] args) { Cell<String> x = new Cell<String>( "abc" ); // <String>타입의 Cell 개체 x 생성 String value = x.get(); // 개체x가 생성되면서 데이터 "abc"를 반환하는 get() 이용하여 String value 에 넣는다. System.out.println( value ); x.set( "def" ); // 개체 x에 "def"를 대입한다.
Cell y = x; // String 타입을 갖는 Cell 객체 x는 로타입 형태인 y에 값을 할당할 수 있다. value = (String) y.get(); // Y 는 로타입이기 때문에 타입 파라미터로 Object가 사용된다. 따라서 형변환을 해야 value에 값을 할당할 수 있다. System.out.println( value ) ; y.set( "hello" );
} }
로 타입을 사용하는 경우에는 컴파일 할 때 경고 메시지가 출력된다.
Cell.java:26: warning: [unchecked] unchecked call to set(E) as a member of the raw type Cell y.set( "hello" ); ^ 1 warning
제네릭 프로그래밍은 타입을 매개 변수로 사용함으로써 프로그램의 일반성을 높이지만, 때로는 타입 파라미터의 범위를 제한해야 하는 경우도 존재한다. 이러한 필요성 때문에 타입 파라미터는 다음과 같은 형태로 범위를 제한할 수 있다.
ex) 타입 파라미터의 범위 제한
public class C1<T extends Number> { ... }
public class C2<T extends Person & Comparable> { ... }
C1 클래스를 사용하는 경우에 파라미터로는 Number 클래스의 서브클래스만 가능하다. C2 클래스의 경우에는 Person 클래스로부터 상속 받으며, Comparable 인터페이스를 구현한 클래스만 타입 파라미터로 사용될 수 있다. 타입 파라미터의 상위 타입을 지정하는 경우에는 부모 클래스를 처음에 오도록 하고, 인터페이스들은 & 를 이용해서 여러개 존재할 수 있다.
출처 - http://java.ihoney.pe.kr/16
Raw Types
Araw typeis the name of a generic class or interface without any type arguments. For example, given the generic Box class:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
To create a parameterized type of Box<T>, you supply an actual type argument for the formal type parameter T:
Box<Integer> intBox = new Box<>();
If the actual type argument is omitted, you create a raw type of Box<T>:
Box rawBox = new Box();
Therefore, Boxis the raw type of the generic typeBox<T>. However, a non-generic class or interface type isnota raw type.
Raw types show up in legacy code because lots of API classes (such as theCollectionsclasses) were not generic prior to JDK 5.0. When using raw types, you essentially get pre-generics behavior — a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
But if you assign a raw type to a parameterized type, you get a warning:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
You also get a warning if you use a raw type to invoke generic methods defined in the corresponding generic type:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, you should avoid using raw types.
The Type Erasure section has more information on how the Java compiler uses raw types.
Unchecked Error Messages
As mentioned previously, when mixing legacy code with generic code, you may encounter warning messages similar to the following:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
This can happen when using an older API that operates on raw types, as shown in the following example:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
The term "unchecked" means that the compiler does not have enough type information to perform all type checks necessary to ensure type safety. The "unchecked" warning is disabled, by default, though the compiler gives a hint. To see all "unchecked" warnings, recompile with -Xlint:unchecked.
Recompiling the previous example with -Xlint:unchecked reveals the following additional information:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
To completely disable unchecked warnings, use the-Xlint:-uncheckedflag. The@SuppressWarnings("unchecked")annotation suppresses unchecked warnings. If you are unfamiliar with the@SuppressWarningssyntax, seeAnnotations.
제네릭 메서드(generic method) : static List asList(E[] a)
자료형 토큰(type token) : String.class -
제네릭(Generic) : 형인자가 포함된 클래스나 인터페이스
List 인터페이스는 List<E> 이렇게 부르는게 맞다
List<String> : 스트링 인터페이스
무인자 자료형은 사용하면 안된다. 컴파일 타임에서 타입 안정성을 보장할수 없다. (ClassCastException 발생 함)
size 무인자 자료형은 아직도 지원하긴한다. 무인자 자료형을 인자로 받는 메서드에 형인자 자료형 객체를 전달할수 있어야 하고 반대도 가능해야 한다. 이진 호환성을 지원하기 위해 어쩔수 없이 존재한다.
형인자 자료형을 사용하면 엉뚱한 자료형의 객체를 넣는 코드를 컴파일 할때 무엇이 잘못인지 알수 있다. 컬랙션에서 원소를 꺼낼떄 형변환을 하지않아도 된다. (컴파일러가 알아서 해줌)
List vs List<Object>
List 는 완전히 형검사 절차를 생략한것이고 List<Object> 는 형검사를 진행한다
List 에는 String 을 넣을수 있지만 List<Object> 에는 넣을수 없다.
List 에는 메서드에 List<String> 을 전달 가능하지만 List<Object> 는 불가능하다
List<String> 은 List 의 하위자료형(subtype) 이지만 List<Object>의 하위 자료형은 아니다
List 와 같은 무인자 자료형을 사용하면 형 안전성을 잃게 되지만 List<Object> 와 같은 형인자 자료형은 형안전성이 있다.
실행 도중 오류를 일으키는 무인자 자료형(List)
1publicstaticvoidmain(String[]args){ 2List<Stringstrings=newArrayList<Object>(); 3unsafeAdd(strings,newIneger(42)); 4Strings=strings.get(0);// ClassCastException 발생!!!! 5} 6// 무인자 자료형에 인자로 보낼수는 있음 7privatestaticvoidunsafeAdd(Listlist,Objecto){ 8list.add(0);// 경고 발생 unchecked call to add(E) in raw type List 9}10// unsafeAdd(List<Object>... 로 바꾸어야 한다)
1//static int numElementsInCommon(Set s1, Set s2) { // 무인자 사용하면안된다 2staticintnumElementsInCommon(Set<?>s1,Set<?>s2){// 비한정적 와일드 카드 자료형 3intresult=0; 4for(Objecto1:s1){ 5if(s2.contains(01)){ 6result++; 7} 8} 9returnresultt;10}
와일드 카드 자료형은 안전, 무인자자료형은 안전하지 않다.
무인자 자료형에는 아무거나 넣을수 있어서 자료형 불변식이 쉽게 깨진다. Collection<?> 에는 null 이외에 어떤원소도 넣을수가 없다. 무언가 넣을려고 하면 컴파일 오류가 난다. 어떤 자료형 객체를 꺼낼수 있는지도 알수없다.
무인자 자료형을 그래도 써도 되는경우
클래스 리터럴(class literal)
자바 표준에서 클래스 리터럴에는 형인자 자료형을 쓸수 없다.(배열 자료형이나 기본 자료형은 쓸수있다)
List.class, String[].class int.class 는 가능하다
List<String>.class 나 List<?>.class 는 사용할수가 없다.
instanceOf 연산자 사용규칙
제네릭 자료형 정보는 프로그램이 실행될때는 지워지므로 instanceOf 연산자는 형인자 자료형에 적용할수가없다. 비한정적 와일드 카드 자료형은 가능하다. 하지만 코드만 지저분해질뿐 굳이 쓸이유가없다.
1if(oinstanceofSet){// 무인자 자료형2Set<?>m=(Set<?>)0;// 와일드 카드 자료형3}
ITEM24 : 무검검 경고(unchecked warning)를 제거하라
제네릭을 사용해서 프로그램을 작성하면 컴파일 경고 메세지를 많이 보게 됨
unchecked 캐스트 경고
unchecked 메소드 호출 경고
unchecked 제네릭 배열 생성 경고
unchecked 변환 경고
unchecked 예외를 무시하면 ClassCastException 가 생길수 있으므로 조심한다.
@SuppressWarnings(“unchecked”) 주석을 사용해서 경고 메세지를 안나타나게 억제할수 있다.
다양한 범위로 사용 가능하다. 가급적 제일 작은 범위로 사용하도록 해야한다.
가능한 주석으로 SuppressWarnings을 사용하는 이유를 남겨라!
1// ArrayList 의 toArray 함수 2public<T>T[]toArray(T[]a){ 3if(a.length<size){ 4// unchecked cast (Object[] , required: T[]) 5@SuppressWarnings("unchecked")T[]result=(T[])Arrays.CopyOf(elements,size,a.getClass()); 6returnresult; 7// return 문제 @SuppresseWarnings 를 사용할수 없다. 8//return (T[])Arrays.CopyOf(elements, size, a.getClass()); 9}10System.arraycopy(elements,0,a,0,size);11if(a.length>size)12a[size]=null;13returna;14}
ITEM25 : 배열 대신 리스트를 써라
배열(Array) vs 제네릭 타입
배열 공변(covariant)이다. Sub 이 Super 의 서브타입이라면 Sub[] 은 Super[]의 서브 타입이다.
제네릭은 불변(invariant)이다. Type1 Type2 List<Type1> List<Type2> 의 서브타입도 슈퍼타입도 아니다.
1// 런타임에서 에러 발생2Object[]objectArray=newLong[1];3objectArray[0]="I don't fit in";// ArrayStoreException 예외 발생4// 컴파일 에러! 컴파일 에러가 더 안전하고 좋은것!5List<Object>ol=newArrayList<Long>();// 호환이 안되는 타입이다!6ol.add("I don't fit in");
배열은 구체적(reified) : 자신의 요소타입을 런타임시에 알고 지키게 한다. String 객체를 Long 배열에 저장 하려고 하면 런타임에서 ArrayStoreException 예외 발생
제네릭은 소거자(Erasure) 에 의해 구현됨. 컴파일 시에만 자신의 타입 제약을 지키게 하고 런타임 시에는 자신의 요소타입 정보를 무시(소거) 한다.
new List<E>[], new List<Sting>[] , new E[] 와 같은 배열 생성식은 불가
제네릭 배열 생성은 불가
E, List<E>, List<String> 과 같은 타입들을 비구체화(nonreifiable) 타입 이라고한다.
비구체화 타입이란 컴파일 시보다 런타임 시에 더 적은 정보를 갖는
비구체화 타입은 배열 생성이 불가능
List<?>, Map<?,?> 와 같은 언바운드 와일드 카드 타입은 구체화 타입이다. 따라서 배열 생성 하는건 적법함
따라서 제네릭 타입을 가변인자를 갖는 메소드와 함께 사용 불가능,가변인자는 내부적으로 배열이 생성되는 구조이기 때문
제네릭 배열 생성 에러가 발생하면 E[] 보다는 List 를 사용하는것이 좋다
다중 스레드간에 동기화에 제네릭 배열이 좋다.
1// 제네릭을 사용하지 않으면서 동시성에도 결함이 없다. 2// synchronized(list) 로 전체를 묶는 방법이 있지만 동기화된 코드에서는 외계인(alien) 메소드(apply) 를 호출 하면안된다. 3// 따라서 lock 이걸로는 toArray() 를 사용해서 문제를 해결했다. 4staticObjectreduce(Listlist,Functonf,ObjectinitVal){ 5Object[]snapshot=list.toArray();// 내부적으로 List 에 lock 이 걸림 6Objectresult=initVal; 7for(Objecto:snapshot) 8result=f.apply(result,o); 9returnresult;10}1112static<E>Ereduce(List<E>list,Functon<E>f,EinitVal){13// 타입 안정성이 보장되지 않는다. 런타임시에 E 가 무슨 타입이 될지 컴파일러가 알지 못한다.14// ClassCastException 예외가 발생할수 있다.15// 컴파일 시에는 String[] Integer[] 등 아무거나 될수있지만 런타임에서는 Object[] 이므로 위험16// E[] 로 캐스팅 하는건 특별한 상황에서만 고려 되야함17E[]snapshot=(E[])list.toArray();18Eresult=initVal;19for(Eo:snapshot)20result=f.apply(result,o);21returnresult;22}232425static<E>Ereduce(List<E>list,Functon<E>f,EinitVal){26E[]snapshot;27synchronized(list){28// toArray 함수와는 다르게 락이 걸리지 않으므로 synchronized 함수로 변경해야한다.29snapshot=newArrayList<E>(list);// 배열보다는 ArrayList<E>30}31Eresult=initVal;32for(Eo:snapshot)33result=f.apply(result,o);34returnresult;35}
ITEM26 : 가능하면 제네릭 자료형으로 만들 것
1// Object 객체 기반의 컬랙션 2publicclassStack{ 3privateObject[]elements; 4privateintsize=0; 5privatestaticfinalintDEFAULT_INITIAL_CAPACITY=16; 6publicStack(){ 7elements=newObject[DEFAULT_INITIAL_CAPACITY]; 8} 9publicvoidpush(Objecte){...}10publicObjectpop(){11if(size==0)12thrownewEmptyStackException();13Objectresult=elements[--size];14elements[size]=null;// 쓸모없는 참조 제거15returnresult;16}17}1819// 제네릭 적용1 - 캐스팅 이용20publicclassStack<E>{21privateE[]elements;22privateintsize=0;23privatestaticfinalintDEFAULT_INITIAL_CAPACITY=16;2425// E[] 로 캐스팅 하므로 warning 이 발생한다.26// elements 는 private 로써 밖에서는 사용이 안되므로 해당 캐스팅만 문제없으면 외부적으로도 문제 없으므로, 아래처럼 캐스팅 하는것은 문제가 없다.27@SuppressWarnings("unchecked")28publicStack(){29// 제네릭 배열 생성, 컴파일 에러 발생! 비구체화 타입을 저장하는 배열은 생성할수 없다.30//elements = new E[DEFAULT_INITIAL_CAPACITY];31elements=(E[])newObject[DEFAULT_INITIAL_CAPACITY];32}33publicvoidpush(Objecte){...}34publicObjectpop(){35if(size==0)36thrownewEmptyStackException();37Objectresult=elements[--size];38elements[size]=null;// 쓸모없는 참조 제거39returnresult;40}41}4243// 제네릭 적용2 - elements 타입자체를 변경44publicclassStack<E>{45privateObject[]elements;46privateintsize=0;47privatestaticfinalintDEFAULT_INITIAL_CAPACITY=16;4849publicStack(){50elements=newObject[DEFAULT_INITIAL_CAPACITY];51}52publicvoidpush(Ee){...}53publicEpop(){54if(size==0)55thrownewEmptyStackException();5657@SuppressWarnings("unchecked")58Eresult=(E)elements[--size];59elements[size]=null;// 쓸모없는 참조 제거60returnresult;61}62}
배열 타입에 대한 unchecked 캐스트 경고를 억제하는게 더 위험하므로 적용2 가 더 좋은 방법 일 수 있다.
하지만 적용2는 elements 를 사용하는 모든 부분에 캐스팅을 해야하고 @SupressWarnings 를 적용해야 되므로 더 많은일을 해줘야한다.
Stack 클래스에서는 내부적으로 배열을 사용했다. 배열보다는 List 를 사용하라고 강조했지만 자바 언어 자체는 List 를 지원하지 않으므로 ArrayList 와 같은 일부 제네릭 타입은 내부적으로 배열을 사용한다. HashMap 은 성능 향상을 목적으로 배열을 사용하기도 한다.
제네릭 타입은 매개변수가 갖는 제약이 전혀없다. Stack<Object> Stack<int[]> Stack<List<String>> 등 여러 형태가 가능하다.
<E extends Delayed> 같은 바운드 타입 배개변수(bounded type parameter)는 허용가능한 값을 제한할수 있다.
하지만 Stack<int> Stack<double> 같은 기본형은 불가능하다. 박스형 기본형 Integer Double 클래스를 사용하는것이 좋다.
ITEM27 : 가능하면 제네릭 메서드로 만들 것
Collections 클래스의 모든 알고리즘 메소드들(binarySearch sort)는 제네릭화 되어있다.
1publicinterfaceUnaryFunction<T>{ 2Tapply(Targ); 3} 4// 불변적이지만 여러타입에대한 적합한 객체 생성 5privatestaticUnaryFunction<Object>IDENTIFY_FUNCTION=newUnaryFunction<Object>(){ 6publicObjectapply(Objectarg){returnarg;} 7} 8 9// 상태값이 없고 언바운드 타입 매개변수를 갖는다.10// 따라서 모든 타입에서 하나의 인스턴스를 공유해도 안전11@SuppressWarnings("unchecked")12publicstatic<T>UnaryFunction<T>identityFunction(){13return(UnaryFunction<T>)IDENTIFY_FUNCTION;14}15// 사용16UnaryFunction<String>sameString=identityFunction();17UnaryFunction<Number>sameNumber=identifyFunction();
재귀적 타입 바운드
Comparable 인터페이스와 가장 많이 사용
1publicstatic<TextendsComparable<T>>
자신과 비교될수 있는 모든 타입 T
캐스팅 없이 메소드를 사용할수 있다는것은 메소드를 제네릭 하게 만들었다는 의미
ITEM28 : 한정적 와일드카드를 써서 API 유연성을 높여라
매개변수화 타입은 불변 타입
불변(invariant)!! Type1 Type2 에 대해서 List<Tyep1> 은 List<Type2> 의 서브타입도 아니고 슈퍼 타입도 아님
List<Object> 에는 아무거나 저장 가능하지만 List<String> 에는 스트링만 저장 가능
스택 pushAll pop 메소드
1// 와일드 카드 타입을 사용하지 않는 pushAll - 불충분함! 2publicvoidpushAll(Iterable<E>src){ 3for(Ee:src) 4push(e); 5} 6Stack<Number>numberStack=newStack<Number>(); 7Iterable<Integer>integers=..; 8// 에러 메세지 pushAll(Iterable<Number>) in Stack<Number> 9// 불변형(상속관계아님) 이기 때문에 Integer iterable 은들어갈수없다.10numberStack.pushAll(integers);1112// 와일드 카드 사용하지 않은 popAll 메소드 - 불충분함!13publicvoidpopAll(Collection<E>dst){14while(!isEmpty())15dst.add(pop());16}17Stack<Number>numberStack=new..18Collection<Object>objects=...;19// 컴파일 에러! Collection<Object> 는 Collection<Number> 의 서브 타입이 아니다!20numberStack.popAll(objects);
바운드 와일드 카드 타입(bounded wildcard type)
pushAll 에는 E의 Iterable 이 아닌 E의 어떤 서브타입의 Iterable 이 되어야 한다.
1// 와일드 카드 타입 pushAll 은 E 타입을 생산하므로 extends2publicvoidpushAll(Iterable<?extendsE>src){...}
popAll 메소드의 인자 타입은 E타입을 저장하는 Collection 이 아닌 E의 어떤 수퍼 타입을 저장하는 Collection이 되어야 한다.
1// 와일드 카드타입 popAll 은 E 타입을 소비하므로 super2publicvoidpopAll(Collection<?superE>dst){...}
유연성을 극대화 하려면 메소드 인자에 와일드 카드 타입을 사용하자.
PECS : Producer->Extends, Consumer->Super
T가 생산자를 나타내면 <? extedns T>
T가 소비자를 나타내면 <? super T>
1static<E>Ereduce(List<E>list,Function<E>f,EinitVal)2// E가 생산자 역활을 하는 와일드 카드 타입 변수3statiac<E>Ereduce(List<?extendsE>list,Function<E>f,EinitVal);
와일드 카드를 쓰므로 인해 List 와 Function 를 사용해서 reduce 호출 가능하다!
반환 타입에는 와일드 카드 타입을 사용하지 말자
유연성 보다는 클라이언트 코드에서 와일드 카드 타입을 사용해야 하는 문제가 생긴다.
클라이언트 코드에서 와일드 카드 타입 때문에 고민해야 된다면 그 클래스의 API 가 잘못된거다.
명시적 타입 매개변수
1publicstatic<E>Set<E>union(Set<?extendsE>s1,SET<?extendsE>s2){...}2Set<Integer>integers=..3Set<Dobule>doubles=...4// 컴파일 에러! 타입 추론을 할수가없다.5Set<Number>numbers=union(integers,doubles);6Set<Number>numbers=Union.<Number>union(integers,doubles);
Comparable<T> 는 T 인스턴스를 소비한다. Comparable<? super T> 로 교체
1// 언바운드 타입 매개변수2publicstatic<E>voidswap(List<E>list,inti,intj);3// 언바운드 와일드 카드4publicstaticvoidswap(List<?>list,inti,intj);
public API 라면 언바운드 타입 매개변수를 사용하는것이 좋다.
메소드 선언부 타입 매개변수가 한번만 나타나면 그것을 와일드 카드로 바꾸면 된다
1publicstaticvoidswap(List<?>list,inti,intj){ 2// 컴파일 에러! List<?> 이므로 list 에는 null 제외한 어떤값도 추가할수 없다. 3list.set(i,list.set(j,list.get(i))); 4} 5 6publicstaticvoidswap(List<?>list,inti,intj){ 7swapHelper(list,i,j); 8} 9// private 지원 메소드10privatestatic<E>voidswapHelper(List<E>list,inti,intj){11list.set(i,list.set(j,list.get(i))));12}
ITEM29 : 형 안전 다형성 컨테이너를 쓰면 어떨지 따져보라
컨테이너에 제네릭을 사용하면 컨테이너 당 사용 가능한 타입 매개변수의 숫자가 제한된다. 컨테이너에 들어가는 타입이 결정되어있기 때문에.
컨테이너 자체보다는 요소의 키에 타입매개변수를 두면 그런 제약을 극복할수 있고 서로 다른 타입의 요소가 저장 될수 있다. 이런 컨테이너를 혼성 컨테이너라고 부른다.
제네릭은 Set Map 그리고 ThreadLocal AtomicReference 같은 단일요소 저장 컨테이너에도 쓰임
제네틱 타입 시스템을 키(Class<T>)로 사용해서 Map Set을 만듬, 그것을 혼성 컨테이너라고 부름!
클래스 리터럴 타입 : Class<T>
컴파일과 런타입 두 시점 모두의 타입정보를 전달될때 그것을 타입토큰(type token)
1// 타입 안전이 보장되는 혼성 컨테이너 패턴! 2publicclassFavorite{ 3privateMap<Class<?>,Object>favorites=newHashMap<Class<?>,Object>(); 4public<T>voidputFavorite(Class<T>type,Tinstance){ 5if(type==null) 6throw 7favorites.put(type,instance); 8} 9public<T>TgetFavorite(Class<T>type){10// Map 에는 Object 가 들어있지만 T 타입으로 리턴해야한다.11// 런타임시에 동적으로 cast 하는 cast 함수 Class<T> 정보가 있으므로 가능!12returntype.cast(favorites.get(type));13}14}
Map<Class<?>, Object> 에서 언바운드 와일드 카드 타입때문에 아무것도 Map 에 넣을수 없다고 생각할수 있지만 키값에 와일드 카드가 붙어 있다. 따라서 모든 키가 서로 다른 매개변수화 타입을 가질수 있다! 예를 들어 Class<String>, Class<Integer>
혼성 컨테이너 문제
Class 객체를 원천 타입의 형태로 사용하면 타입 안전이 보장되지 않을수 있다. 해결책은아래!
1// put 메소드, 동적 캐스트를 사용해서 런타임 시의 타입 안전을 획득!2public<T>voidputFavorite(Class<T>type,Tinstance){3favorites.put(type,type,cast(instance);4}
비구체화 타입에 사용 될 수 없다. Favorite 객체를 String 이나 String[] 에는 저장할수 있지만 List<String> 은 저장할수 없다.
List<String> 에 대한 Class 객체를 얻을수 없다. List<String>.class 구문 에러
그리고 아래 코드를 보자. 위의 코드는 StudentPerson과 EmployeeInfo가 사실상 같은 구조를 가지고 있다. 중복이 발생하고 있는 것이다. 중복을 제거해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
packageorg.opentutorials.javatutorials.generic;
classStudentInfo{
publicintgrade;
StudentInfo(intgrade){ this.grade = grade; }
}
classEmployeeInfo{
publicintrank;
EmployeeInfo(intrank){ this.rank = rank; }
}
classPerson{
publicObject info;
Person(Object info){ this.info = info; }
}
publicclassGenericDemo {
publicstaticvoidmain(String[] args) {
Person p1 = newPerson("부장");
EmployeeInfo ei = (EmployeeInfo)p1.info;
System.out.println(ei.rank);
}
}
위의 코드는 성공적으로 컴파일된다. 하지만 실행을 하면 아래와 같은 오류가 발생한다.
1
2
Exception in thread "main"java.lang.ClassCastException: java.lang.String cannot be cast to org.opentutorials.javatutorials.generic.EmployeeInfo
at org.opentutorials.javatutorials.generic.GenericDemo.main(GenericDemo.java:17)
아래 코드를 보자.
1
Person p1 = newPerson("부장");
클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다. 그렇기 때문에 위와 EmployeeInfo의 객체가 아니라 String이 와도 컴파일 에러가 발생하지 않는다. 대신 런타임 에러가 발생한다.컴파일 언어의 기본은 모든 에러는 컴파일에 발생할 수 있도록 유도해야 한다는 것이다. 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 있기 때문이다.
위와 같은 에러를 타입에 안전하지 않다고 한다. 즉 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한 할 수 없게 되는 것이다.
extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
packageorg.opentutorials.javatutorials.generic;
interfaceInfo{
intgetLevel();
}
classEmployeeInfo implementsInfo{
publicintrank;
EmployeeInfo(intrank){ this.rank = rank; }
publicintgetLevel(){
returnthis.rank;
}
}
classPerson<T extendsInfo>{
publicT info;
Person(T info){ this.info = info; }
}
publicclassGenericDemo {
publicstaticvoidmain(String[] args) {
Person p1 = newPerson(newEmployeeInfo(1));
Person<String> p2 = newPerson<String>("부장");
}
}
이상으로 제네릭의 기본적인 사용법을 알아봤다.
출처 - https://opentutorials.org/module/516/6237
4. 제네릭스(Generics)
제네릭스는, JDK1.5에서의 가장 큰 변화 중의 하나로, 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 더 많다. 그런데도 꺼낼 때 마다 타입체크를 하고 형변환을 하는 것은 아무래도 불편할 수밖에 없다.
▶ 제네릭스의 장점
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
[참고] 타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체를 저장하는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 형변환되어 발생할 수 있는 오류를 줄여준 다는 뜻이다.
간단히 얘기하면 다룰 객체의 타입을 미리 명시해줌으로써 형변환을 하지 않아도 되게 하는 것이다. 그게 전부이다. 너무 어렵게 생각하지 않길 바란다.
제네릭스에서는 참조형 타입(reference type), 간단히 말해서 ‘타입(type)’을 의미하는 기호로 ‘T'를 사용한다. ’T‘는 영단어 'type’의 첫 글자로, 어떠한 참조형 타입도 가능하다는 것을 의미한다.
'T'뿐 만아니라 때때로 요소(element)를 의미하는 ‘E', 키(key)를 의미하는 'K', 값(value)을 의미하는 ’V'도 사용된다. 이들은 기호의 종류만 다를 뿐 ‘임의의 참조형 타입’을 의미한다는 것은 모두 같다. 마치 수학식 ‘f(x, y) = x + y’가 ‘f(k, v) = k + v'와 다르지 않은 것처럼 말이다.
기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.
[참고] 타입을 지정하지 않으면 Object타입으로 간주된다.
제네릭스는 자바의 부가적인 기능 중에 하나일 뿐이다. 사용하면 편리하긴 하지만 사용하지 않아도 그만이다. 저자의 개인적인 생각으로는 프로그래밍을 처음 배우는 사람에게는 학습부담이 될 까 다소 우려스럽다. 다형성이나 형변환, 타입체크 등의 중요한 기본원칙에 대해서 완전히 이해하지 못한 상태에서 원칙에서 벗어나는 기능인 제네릭을 배운다면 혼란스러울 수 있기 때문이다.
일단 편안하게 읽어보고 이해가 잘 안 간다면, 부담없이 건너뛰고 다음 진도로 넘어가길 바란다. 자바가 좀 더 익숙해 진 후에 다시 보면, 보다 쉽게 이해가 갈 수 있는 부분이기 때문이다.
4.1 ArrayList<E>
대표적인 컬렉션 클래스인 ArrayList를 통해 제네릭스를 사용하는 방법을 설명해나갈 것이다. 대부분의 컬렉션 클래스들에 대해서도 동일한 방식으로 사용하면 된다.
위의 코드에 사용된 'E'는 요소를 뜻하는 'Element'의 약자로, ‘E' 대신 어떤 다른 문자를 사용해도 상관없지만 소스코드 내에서 같은 문자를 일관되게 사용해야한다.
이는 마치 메서드의 매개변수이름을 다르게 해도 메서드 내에서만 같은 이름을 사용하면 문제없는 것과 같다. 만일 타입을 지정하지 않으면 ‘E’는 Object타입으로 간주된다.
임의의 타입 ‘E'는 ArrayList타입의 참조변수를 선언하거나 ArrayList를 생성할 때 지정할 수 있으며, 만일 ArrayList에 Tv타입의 객체만을 저장하기 위해 ‘ArrayList<Tv> tvList = new ArrayList<Tv>();’와 같이한다면 ’E'는 ‘Tv'가 되는 것이다.
public class ArrayList<Tv> extends AbstractList<Tv>
implements List<Tv>, RandomAccess, Cloneable,
java.io.Serializable
{
private transient Tv[] elementData;
public boolean add(Tv o) { /* 내용생략 */ }
public Tv get(int index) { /* 내용생략 */ }
...
}
아래와 같이 컬렉션 클래스 이름 바로 뒤에 저장할 객체의 타입을 적어주면, 컬렉션에 저장할 수 있는 객체는 지정한 타입의 객체뿐이다.
컬렉션클래스<저장할 객체의 타입> 변수명 = new 컬렉션클래스<저장할 객체의 타입>();
ArrayList<Tv> tvList = new ArrayList<Tv>();
아래의 코드는 ArrayList에 Tv객체만 저장할 수 있도록 작성한 것이다. Tv타입이 아닌 객체를 저장하려하면 컴파일 시에 에러가 발생한다.
// Tv객체만 저장할 수 있는 ArrayList를 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
tvList.add(new Audio()); // 컴파일 에러 발생!!!
저장된 객체를 꺼낼 때는 형변환할 필요가 없다. 이미 어떤 타입의 객체들이 저장되어 있는지 알고 있기 때문이다. 제네릭스를 적용한 코드(오른쪽)와 그렇지 않은 코드(왼쪽)를 잘 비교해보자.
ArrayList tvList = new ArrayList();
tvList.add(new Tv());
Tv t = (Tv)tvList.get(0);
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
Tv t = tvList.get(0);
만일 다형성을 사용해야하는 경우에는 조상타입을 지정함으로써 여러 종류의 객체를 저장할 수 있다.
class Product{}
class Tv extends Product{}
class Audio extends Product{}
// Product클래스의 자손객체들을 저장할 수 있는 ArrayList를 생성
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); // 컴파일 에러가 발생하지 않는다.
list.add(new Audio()); // 컴파일 에러가 발생하지 않는다.
Product p = list.get(0); // 형변환이 필요없다.
Tv t = (Tv)list.get(1); // 형변환을 필요로 한다.
ArrayList가 Product타입의 객체를 저장하도록 지정하면, 이들의 자손인 Tv와 Audio타입의 객체도 저장할 수 있다. 다만 꺼내올 때 원래의 타입으로 형변환해야 한다.
제네릭스에서도 다형성을 적용해서 아래와 같이 할 수 있다.
List<Tv> tvList = new ArrayList<Tv>(); // 허용
그러나 Product클래스가 Tv클래스의 조상이라 할지라도 아래와 같이 할 수는 없다.
ArrayList<Product> list = new ArrayList<Tv>(); // 허용 안함~!!!
그래서 아래와 같이 메서드의 매개변수 타입이 ArrayList<Product>로 선언된 경우, 이 메서드의 매개변수로는 ArrayList<Product>타입의 객체만 사용할 수 있다. 그렇지 않으면 컴파일 에러가 발생한다.
public static void printAll(ArrayList<Product> list) {
for(Unit u : list) { // 향상된 for문, 부록 참고
System.out.println(u);
}
}
public static void main(String[] args) {
ArrayList<Product> productList = new ArrayList<Product>();
ArrayList<Tv> tvList = new ArrayList<Tv>();
printAll(productList);
printAll(tvList); // 컴파일 에러 발생~!!!
}
컬렉션에 저장될 객체에도 다형성이 필요할 때도 있을 것 같은데 다형성을 사용할 수는 없을까?
물론 방법이 있다. 와일드 카드‘?’를 사용하면 된다. 보통 제네릭에서는 단 하나의 타입을 지정하지만, 와일드 카드는 하나 이상의 타입을 지정하는 것을 가능하게 해준다. 아래와 같이 어떤 타입('?')이 있고 그 타입이 Product의 자손이라고 선언하면, Tv객체를 저장하는 ‘ArrayList<Tv>’ 또는 Audio객체를 저장하는 ‘ArrayList<Audio>’를 매개변수로 넘겨줄 수 있다. Tv와 Audio 모두 Product의 자손이기 때문이다.
// Product 또는 그 자손들이 담긴 ArrayList를 매개변수로 받는 메서드
public static void printAll(ArrayList<? extends Product> list) {
for(Unit u : list) {
System.out.println(u);
}
}
만일 아래와 같은 코드가 있다면 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.
public static void printAll(ArrayList<? extends Product> list,
ArrayList<? extends Product> list2) {
for(Unit u : list) {
System.out.println(u);
}
}
public static <T extends Product> void printAll(ArrayList<T> list,
ArrayList<T> list2) {
for(Unit u : list) {
System.out.println(u);
}
}
두 번째 코드는 ‘T’라는 타입이 Product의 자손타입이라는 것을 미리 정의해 놓고 사용한 것이다. 위의 두 코드는 서로 같은 의미의 코드이므로 잘 비교해보자.
[주의] 여기서 만일 Product가 클래스가 아닌 인터페이스라 할지라도 키워드로 'implements'를 사용하지 않고 클래스와 동일하게 'extends'를 사용한다는 것에 주의하자.
[예제11-80]/ch11/GenericsEx1.java
import java.util.*;
class Product {}
class Tv extends Product{}
class Audio extends Product{}
class GenericsEx1 {
public static void main(String[] args) {
ArrayList<Product> productList = new ArrayList<Product>();
ArrayList<Tv> tvList = new ArrayList<Tv>();
productList.add(new Tv());
productList.add(new Audio());
tvList.add(new Tv());
tvList.add(new Tv());
printAll(productList);
// printAll(tvList); // 컴파일 에러가 발생한다.
printAll2(productList); // ArrayList<Product>
printAll2(tvList); // ArrayList<Tv>
}
public static void printAll(ArrayList<Product> list) {
for(Product p : list) {
System.out.println(p);
}
}
// public static void printAll2(ArrayList<? extends Product> list) {
public static <T extends Product> void printAll2(ArrayList<T> list) {
for(Product p : list) {
System.out.println(p);
}
}
}
[실행결과]
Tv@9cab16
Audio@1a46e30
Tv@9cab16
Audio@1a46e30
Tv@3e25a5
Tv@19821f
이 예제의 결과는 별 의미가 없다. 예제를 변경해가면서 전에 설명한 내용을 직접 테스트 해보자.
한 가지 설명하고 넘어갈 것은 예제에 사용된 for문의 형태인데, 이 새로운 구문의 for문은 ‘향상된 for문’이라고 한다. list에 담긴 모든 요소를 반복할 때마다 하나씩 가져다 Product타입의 참조변수 p에 저장한다. 아래의 두 코드는 같은 것이니 잘 비교해 보자.
[참고] 향상된 for문에 대한 보다 자세한 내용은 책의 마지막에 있는 ‘부록’을 참고하라.
Iterator it = list.iterator();
for(;it.hasNext();) {
Product p = (Product)it.next();
System.out.println(p);
}
for(Product p : list) {
System.out.println(p);
}
4.2 Iterator<E>
다음은 Iterator의 실제 소스인데, 컬렉션 클래스 뿐 만아니라 Iterator에도 제네릭스가 적용되어 있는 것을 알 수 있다.
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
아래의 예제는 Iterator에 제네릭스를 적용한 예이다.
[예제11-81]/ch11/GenericsEx2.java
import java.util.*;
class GenericsEx2
{
public static void main(String[] args)
{
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("자바왕",1,1));
list.add(new Student("자바짱",1,2));
list.add(new Student("홍길동",2,1));
list.add(new Student("전우치",2,2));
Iterator<Student> it = list.iterator();
while(it.hasNext()) {
Student s = it.next();
System.out.println(s.name);
}
} // main
}
class Student {
String name = "";
int ban;
int no;
Student(String name, int ban, int no) {
this.name = name;
this.ban = ban;
this.no = no;
}
}
[실행결과]
자바왕
자바짱
홍길동
전우치
ArrayList에 Student객체를 저장할 것이라고 지정을 했어도 Iterator타입의 참조변수를 선언할 때 저장된 객체의 타입을 지정해주지 않으면, Iterator의 next()를 호출할 때 형변환을 해야 한다.
Iterator it = list.iterator();
while(it.hasNext()) {
Student s = (Student)it.next();
System.out.println(s.name);
}
Iterator<Student> it = list.iterator();
while(it.hasNext()) {
Student s = it.next();
System.out.println(s.name);
}
4.3 Comparable<T>과 Collections.sort()
클래스의 기본 정렬기준을 구현하는 Comparable인터페이스에도 제네릭스가 적용된다. 먼저 예제를 살펴보자. 이 예제는 Comparable를 사용해서 학생들의 총점을 기준으로 내림차순 정렬하여 출력한다.
[예제11-82]/ch11/GenericsEx3.java
import java.util.*;
class GenericsEx3
{
public static void main(String[] args)
{
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("자바왕",1,1,100,100,100));
list.add(new Student("자바짱",1,2,90,80,70));
list.add(new Student("홍길동",2,1,70,70,70));
list.add(new Student("전우치",2,2,90,90,90));
Collections.sort(list); // list를 정렬한다.
Iterator<Student> it = list.iterator();
while(it.hasNext()) {
Student s = it.next();
System.out.println(s);
}
}
}
class Student implements Comparable<Student> {
String name = "";
int ban = 0;
int no = 0;
int koreanScore = 0;
int mathScore = 0;
int englishScore = 0;
int total = 0;
Student(String name, int ban, int no, int koreanScore, int mathScore, int englishScore) {
this.name = name;
this.ban = ban;
this.no = no;
this.koreanScore = koreanScore;
this.mathScore = mathScore;
this.englishScore = englishScore;
total = koreanScore + mathScore + englishScore;
}
public String toString() {
return name + "\t"
+ ban + "\t"
+ no + "\t"
+ koreanScore + "\t"
+ mathScore + "\t"
+ englishScore + "\t"
+ total + "\t";
}
public int compareTo(Student o) {
return o.total - this.total;
}
} // end of class Student
[실행결과]
자바왕 1 1 100 100 100 300
전우치 2 2 90 90 90 270
자바짱 1 2 90 80 70 240
홍길동 2 1 70 70 70 210
학생들의 정보를 담은 Student인스턴스를 ArrayList에 담은 다음, Collection.sort()를 이용해서 Student클래스에 정의된 기본정렬(총점별로 내림차순)로 정렬해서 출력하는 간단한 예제이다.
먼저 제네릭스가 적용된 Comparable의 실제 소스를 보면 아래와 같다.
public interface Comparable<T> {
public int compareTo(T o); // 지정한 타입 T를 매개변수로 한다.
}
여기서 타입 ‘T’대신 타입 ‘Student’를 지정했기 때문에 아래와 같이 된다.
public interface Comparable<Student> {
public int compareTo(Student o); // 지정한 타입 T를 매개변수로 한다.
}
만일 이 예제에서 제네릭스를 사용하지 않았다면, 왼쪽의 코드 대신 오른쪽과 같은 코드를 사용해야 했을 것이다.
public int compareTo(Student o) {
return o.total - this.total;
}
public int compareTo(Object o) {
int result = -1;
if(o instanceof Student) {
Student tmp = (Student)o;
result = tmp.total-this.total;
}
return result;
}
한 눈에 봐도 왼쪽의 코드가 더 간결하다는 것을 알 수 있다. 타입이 미리 체크되어 있기 때문에 instanceof로 타입을 체크하거나 형변환할 필요가 없기 때문이다.
여기서 Collections.sort()에 적용된 제네릭스에 대해서 좀 더 자세히 살펴볼 필요가 있다.
다음은 Collections.sort()의 선언부인데 지금까지 보던 것과는 달리 좀 복잡하다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
②①
① ArrayList와 같이 List인터페이스를 구현한 컬렉션을 매개변수의 타입으로 정의하고 있다. 그리고 그 컬렉션에는 'T'라는 타입의 객체를 저장하도록 선언되어 있다.
② 'T'는 Comparable인터페이스를 구현한 클래스의 타입이어야 하며(<T extends Comparable>), 'T'또는 그 조상의 타입을 비교하는 Comparable이어야한다는 것(Comparable<? super T>)을 의미한다.
만일 Student클래스가 Person클래스의 자손이라면, <? super T>는 Student타입이나 Person타입이 가능하다.(물론 Object타입도 가능)
<? extends T> - T 또는 T의 자손 타입을 의미한다. <? super T> - T 또는 T의 조상 타입을 의미한다.
앞서 배운 <? extends T>와 반대로 <? super T>는 T 또는 T의 조상 타입을 의미한다.
[예제11-83]/ch11/GenericsEx4.java
import java.util.*;
class GenericsEx4
{
public static void main(String[] args)
{
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("자바왕",1,1,100,100,100));
list.add(new Student("자바짱",1,2,90,80,70));
list.add(new Student("홍길동",2,1,70,70,70));
list.add(new Student("전우치",2,2,90,90,90));
Collections.sort(list); // list를 정렬한다.
Iterator<Student> it = list.iterator();
while(it.hasNext()) {
Student s = it.next();
System.out.println(s);
}
} // main
}
// <T extends Comparable<? super T>>에서 'T'가 Student타입이므로
// <Student extends Comparable<Student>>와
// <Student extends Comparable<Person>>이 가능하다.
class Student extends Person implements Comparable<Person> {
String name = "";
int ban = 0;
int no = 0;
int koreanScore = 0;
int mathScore = 0;
int englishScore = 0;
int total = 0;
Student(String name, int ban, int no, int koreanScore, int mathScore, int englishScore) {
이전 예제의 Student클래스를 변경하여 Person클래스의 자손이 되도록 하고, Student의 조상인 Person을 Comparable의 타입으로 지정하였다.
Collections.sort()의 매개변수가 ‘<T extends Comparable<? super T>>’이기 때문에 예제에서는 'T'가 Student타입이므로 ‘<Student extends Comparable<Person>>' 또는 ‘<Student extends Comparable<Student>>'가 가능하며, 그 중에서 ‘<Student extends Comparable<Person>>'의 경우를 보여주고 있다.
// <T extends Comparable<? super T>>에서 'T'가 Student타입이므로
// <Student extends Comparable<Student>>와
// <Student extends Comparable<Person>>이 가능하다.
class Student extends Person implements Comparable<Person> {
...
Person클래스에 정의된 멤버변수 id는 Student클래스의 멤버변수 ban과 no를 문자열로 붙여서 만들었기 때문에, 실행결과를 보면 전과 달리 반과 번호를 기준으로 오름차순 정렬되어 있는 것을 알 수 있다.
4.4 HashMap<K,V>
HashMap처럼 데이터를 키(key)와 값(value)의 형태로 저장하는 컬렉션 클래스는 지정해 줘야할 타입이 두 개이다. 그래서 ‘<K,V>’와 같이 두 개의 타입을 콤마‘,’로 구분해서 적어줘야 한다. 여기서 ’K'와 ‘V'는 각각 'Key'의 ’Value'의 첫 글자에서 따온 것일 뿐, 'T'나 ‘E'와 마찬가지로 임의의 참조형 타입(reference type)을 의미한다.
다음은 HashMap의 실제 소스이다.
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
...
public V get(Object key) { /* 내용 생략 */ }
public V put(K key, V value) { /* 내용 생략 */ }
public V remove(Object key) { /* 내용 생략 */ }
...
}
만일 키의 타입이 String이고 저장할 값의 타입이 Student인 HashMap을 생성하려면 다음과 같이 한다.
HashMap<String, Student> map = new HashMap<String, Student>(); // 생성
map.put("자바왕", new Student("자바왕",1,1,100,100,100)); // 데이터 저장
위와 같이 HashMap을 생성하였다면, HashMap의 실제 소스는 'K'대신 String이, 'V'대신 Student가 사용되어 아래와 같이 바뀌는 셈이 된다.
이전 예제를 ArrayList대신 HashMap을 사용하도록 변경한 것일 뿐 별로 특별한 것은 없다. HashMap에서 값을 꺼내오는 get(Object key)를 사용할 때, 그리고 저장된 키와 값들을 꺼내오는 ketSet()과 values()를 사용할 때 형변환을 하지 않아도 된다.