Java 5부터 등장한 제너릭을 이제서야 정리해보려고 한다. 사실 만들어져 있는 제너릭 API들은 많이 사용해왔지만 직접 제너릭 클래스를 작성하는 일은 아무래도 일반 클래스를 작성하는 것보다 빈도가 낮다보니, 제너릭에 관해 최근에서야 알게 된 것들이 있기도 하다.

나는 이미 자바의 버전이 6일 때 배우기 시작해서 제너릭 전과 후의 차이를 극적으로 느끼지는 못했지만 가끔 레거시 코드 중에 제너릭을 쓰지 않고 컬렉션을 사용하는 코드를 보면 조금 힘들긴 하다(물론 제너릭이 늘 옳은 것만은 아니라고 생각한다).

1. 제너릭을 왜 쓰는 걸까요?

2. 제너릭 타입

3. Parameterized Type

4. 제너릭 메서드

5. Bounded Type Parameter

- Class & Interfaces.

6. 제너릭 타입 추론

7. 제너릭으론 할 수 없는 것들

8. 그렇게 중요하진 않지만 흥미로운 사실 - Multiple Extended(Implemented) Generic Interface.

9. assignable - Foo<T>, Bar extends Foo<T>, Foo assignable Bar. @Autowired

10. 제너릭 리플렉션

  1. 익명 2017.05.03 19:03

    비밀댓글입니다

면접 질문 중에 가장 당황했던 질문이 아니었나 싶다.


"StackOverflow가 발생했을 때 어떻게 해결하실건가요?"


너무 OutOfMemoryError에 대해서만 대비를 했던 탓인지 카운터를 맞았다. 답변도 OutOfMemoryError에 대한 해결 방법으로 해버렸다. 그러자 면접관께서 "그건 Heap 메모리에서구요, StackOverflow는 어떻게 될까요?"하고 내가 잘못 이해했다고 생각을 하셨는지 다시금 바로 잡아주셨지만.. 머리가 하얘졌더랬다.


면접이 끝나고, 정신이 좀 돌아오니 그제야 StackOverflowError는 호출 스택의 깊이가 너무 깊어질 때 발생한다는 게 생각났다. 웬만해선 질문을 받아도 알면 대답하고, 모르면 모른다고 대답을 했을텐데, 말그대로 정말 당황해서 어버버 했었던 것 같다.


먼저 Java API 문서의 StackOverflowError 클래스의 설명에는 "Thrown when a stack overflow occurs because an application recurses too deeply"로 되어있다.  그럼 StackOverflowError가 발생했을 때 어떻게 해야할까? Heap 영역과 마찬가지로 Stack 영역의 메모리를 늘려주면 되는걸까? 복구는 가능할까?


일단 에러 상황을 만들기 위해 무한 재귀호출을 하도록 아래와 같이 작성했다.



그리고 JVM Option에서 -Xss10K를 추가해서 실행해보았다. 근데 아래와 같은 메시지를 출력하면서 실행되지 않는다.


Error: Could not create the Java Virtual Machine.

Error: A fatal exception has occurred. Program will exit.


The stack size specified is too small, Specify at least 160k


160K의 기준이 있나 싶어 1.8 버전의 JVM 명세를 찾아봤으나 딱히 160K에 대한 기준은 없은 것으로 봐서는 JVM 구현에서 결정하는 사항인 듯 하다. 검색을 하다보니 CASSANDRA 이슈에서 Stack 사이즈와 관련된 내용이 있었는데 Java 6의 경우는 -Xss128K도 되는데 Java 7부터는 160K의 하한이 생긴 모양이다. (https://issues.apache.org/jira/browse/CASSANDRA-4275)


다시 스택 크기를 일단 최소로 잡아서 160K로 조정해서 실행해보았다. 751까지 출력된 후 StackOverflowError를 발생시킨다. 일부러 재귀구조로 프로그램을 작성하지 않는 이상 이 정도의 깊이까지 호출하는 경우가 있을지는 의문이긴하다.


스택 크기를 320K로 조정했을 때는 2615까지, 480K일때는 4476, 640K일 때는 6345로 출력되었다. 메서드 구현에 따라 스택 메모리를 소모하는 정도는 달라질 수 있을 것이고, 또한 스택 크기를 지정할 때 스레드 스택 프레임을 위해 기본으로 할당되는 영역까지 고려되어야 할 것 같다. Stack 메모리는 스레드별로 할당되므로 요청을 스레드별로 처리하는 웹 애플리케이션의 경우에는 Heap과 Stack의 크기가 세심하게 설정되어야 할 것 같지만 아직은 경험이 없으니 이에 대한 방법은 더 공부를 해봐야겠다.



그럼 StackOverflowError가 발생했을 때 일반적인 Exception과 마찬가지로 예외 처리가 가능할까? 일단 Error도 try-catch로 Error를 잡을 수는 있지만, 이미 비정상적인 조건에서 발생하게 되므로, 에러가 발생하기 전까지, 혹은 그 후에 처리가 정상적으로 되었는지 확신하기 어렵다. 이 경우에는 Error를 잡는 것보다, Error가 발생하는 원인을 찾아 해결하는 쪽이 더 바람직하다고 본다.



※ 참고 자료

- https://docs.oracle.com/javase/7/docs/api/java/lang/StackOverflowError.html

- https://docs.oracle.com/javase/7/docs/api/java/lang/Error.html

- https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

https://issues.apache.org/jira/browse/CASSANDRA-4275

- http://stackoverflow.com/questions/20658264/when-does-stackoverflowerror-occur

현재 진행 중인 프로젝트에서 오픈 소스를 분석해서 설계서를 역으로 만들고 있는데(클래스 설계서),

 

약 400여개의 클래스를 손수 MS 워드 문서로 옮겨야 하니 노가다의 절정이라고 생각된다..

 

R&D 사업이니 설계 산출물 자체에 의미가 있다기보다는 예산에 대한 증빙 자료인 셈이다.

 

 

 

각론하고, 다행히 오픈 소스가 Java로 작성되어 있어 리플렉션으로 클래스 정보를 읽은 후 HTML 문서로 출력하고,

 

워드로 옮기면 되겠다. 라고 생각하고 작업을 했는데, 메서드의 파라미터 이름을 가지고 올 수 없었다.

 

 

 

이 문제를 해결하기 위한 방법을 두 가지 찾았는데, 하나는 Java 8의 리플렉션에 추가된 Method.getParamter()를 통해 얻은 파라미터 정보에서 getName()을 이용해 파라미터의 이름을 알아내는 것이고,

 

두 번째는 Java Parser를 이용하는 것이었다.

 

 

 

수작업을 줄이려고 시작한 건데 일이 커지기 시작했다.

 

거기에 더해 오픈소스를 분석한다는 의미는 사라지고 설계서를 만든다는 것만 남았다. (...)

 

 

 

 

 

Java Parser로 테스트를 해 보기 전에 잠시 Java 8에서 테스트를 해보았는데, 파라미터 이름이 arg0 과 같이 실제 이름이 아닌 컴파일러에서 치환된 이름으로 변환되어 나와서 더 찾아보지 않고 Java Parser로 테스트를 했다.

 

 

 

Java Parser를 이용할 때는 원하는 대로 파라미터를 출력할 수는 있었지만, 구조화된 타입이 아닌 파라미터 선언부의 문자열로만 값을 가져올 수 있었고, 한 가지 문제점으로는 public void test(/*Param1*/String param) 과 같은 메서드 선언에서 주석을 같이 읽어오게 되어 예외 케이스에 대한 처리가 복잡해질 것 같아서 다시 Java 8 리플렉션을 통해 파라미터 이름을 얻어올 수 있는 방법을 찾아보게 되었다.

 

 

 

검색을 하면서 하나 놓친 것이 있었는데, 자바 컴파일 옵션에서 -parameters 옵션을 주면 메서드 파라미터의 이름이 그대로 컴파일 된 바이트코드에서도 유지된다.

 

Eclipse에서도 아래와 같이

Store information about method parameters (usable via reflection) 체크박스에 체크하면 파라미터 옵션을 줄 수 있다. (Java 1.8로 컴파일러가 설정된 경우에 활성화된다)

 

 

 

 

 

 

 

 

파라미터 이름을 얻는 소스코드는 아래와 같다.

 

 

 

 

 

 

이 외에도 paranamer라는 라이브러리가 있었지만, 매뉴얼을 대강 읽어보니 Java8에 추가되었다고 노트되어있었다.

 

 

 

 

 

참고 자료

 

https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html

 

https://github.com/javaparser/javaparser

http://stackoverflow.com/questions/2237803/can-i-obtain-method-parameter-name-using-java-reflection

 

 

덧붙여,

이 작업을 하고 있는 둘은 한 명은 클래스 명세표 양식을 출력하는 웹 애플리케이션을 만들고 있고, 한 명은 클래스를 리플렉션해서 필요한 메타데이터를 뽑아내는 툴을 만들고 있다. 이쯤 되면 배보다 배꼽의 배꼽 수준인 듯 ㅋㅋ

  1. 오타감별사 2021.04.06 20:25

    자바 컴파일 옵션
    -parameter 이 아니라 -parameters 입니다.

다르다.

몰랐다.

그래서

한참을

돌았다.

그런데.

똑같네?

다른줄

알았다.


이게 무슨 소리지..

처음엔 내가 잘못 알고 있나? 싶었다. 


어떤 숫자가 있는지 없는지만 확인하면 되는 로직에 HashSet<Integer>를 사용했다.

거기에 0부터 24까지의 정수를 추가했다고 하면,

위와 같은 코드가 될 것이다.


그리고 저 아래와 같은 코드를 실행하면 출력 결과가 true인 것은 자명하다.


System.out.println(set.contains(1));


그렇다면 1 대신에 1l 또는 (long)1을 넣으면 true일 것이라고 예상했다.

헌데 결과는 false 이다.

내가 아는 바로는 int 의 표현 범위 내에서 int 와 long은 같은 hashCode를 가진다.

그래서 확인을 해봤다.


System.out.println(Integer.hashCode(1));

System.out.println(Long.hashCode(1));


실행 결과는 생각한대로 둘 다 1이다. int 범위 내의 어떤 값이든 그렇다.

그리고 정수 기본타입은 자신의 값이 곧 자신의 해시코드라고 알고 있었는데,

HashSet 이란 놈이 이렇게 배신을 할 줄이야.

이름만 보면 Set에 포함된 엘리먼트들의 유일성을 Hash 값으로 판단한단 말이 아닌가?


HashSet의 코드를 뜯어보니 HashSet은 내부적으로 엘리먼트의 저장을 HashMap을 사용한다.

왜인지는 모르겠다. 코드의 중복을 줄이기 위해서인지? 구현 편의인지.

HashMap은 Entry를 사용하니까 엘리먼트 하나를 저장하는 것보다 키, 밸류를 저장하는 게 손해아닌가..?

HashSet에서는 HashMap의 Entry의 value 값에 빈 Object객체를 넣는다.

빈 Object 객체는 static 변수로 선언되어 있다.

(private static final Object PRESENT = new Object();)


어쨌거나 어디서부터 잘못 이해하고 있는지 확인 하기 위해서 HashSet에서 contains 메서드가 호출하는 HashMap.containsKey 메서드를 살펴봤다.

containsKey에서는 map의 get메서드가 호출하는 getEntry(key)를 호출하고, key를 이용해서 Entry를 가져오지 못하면 null일 때 containsKey는 false가 되고 있으면 true가 된다.

그럼 getEntry에서는 무엇을 하느냐? key의 해시값을 이용해서 HashMap이 가지고 있는 hashSeed값과 적절한 연산을 통해 해시 값을 만들고, 이 해시를 이용해서 해시 테이블에 있는 엔트리를 찾는다.


결국 hash값으로 찾는게 맞단 소리고, HashMap의 hash 메서드의 인자를 long과 int 타입으로 테스트해보니 값이 같을 때 같은 값이 나온다.


여기까지의 결론으로는 결국 set.contains(1) 이나, set.contains((long)1) 이나 같은 결과를 보여야 하는게 정상인데..

왜 false라는 값이 나올까..?? map과 set을 좀 더 파헤쳐봐야겠다.



검색 중 Why you should not user HashSet/HashMap for Integer number 글 발견..

관련있는 글

인공지능 과제에서 인덱싱을 하면서 무심코 HashMap을 사용했는데,

다른 조의 시연을 보니 인덱싱에 상당한 시간이 소요되었다.


갑자기 궁금증이 생겨서 세 컬렉션의 속도를 비교하게 되었다.

테스트 엘리먼트는 Integer 인스턴스 5,000,000(5백만개)를 이용하여 수행하였다.

탐색과 삭제에서는 균등하게 떨어진 4990개의 데이터를 이용하였다.


테스트 결과 :

5000000개의 인스턴스 생성 시간 0.548829807초


HashMap Test

입력 소요 시간  2.415268645초

탐색 소요 시간 0.002399381초

삭제 소요 시간 0.002615092초


ArrayList

입력 소요 시간  0.381054002초

탐색 소요 시간 1.99475E-4초

삭제 소요 시간 137.231368119초


LinkedList

입력 소요 시간  1.503839756초

탐색 소요 시간 52.905209243초

삭제 소요 시간 52.587791295초



HashMap의 경우는 입력되는 시간을 제외하면 우수한 성능을 보였고,

(탐색과 삭제 시에 인덱스가 아닌 키값으로 하였음)


ArrayList 의 경우에는 내부적으로 배열을 쓰는 컬렉션 답게 탐색에서는 매우 우수한 속도를 보였지만

삭제 시에 배열의 구조가 변경되므로 매우 느린 속도를 보였다.


LinkedList는.. 탐색, 삭제 모두 순차 탐색을 하므로(실제로 이중 연결 링크드리스트로 되어있고, Head, Rear 포인터를 이용해서, 탐색하려고 하는 인덱스와 리스트 크기의 반과 비교해서 인덱스가 작은 경우 앞에서부터 탐색하고, 큰 경우 뒤에서부터 탐색하도록 되어있어 평균적으로 n / 2의 시간 복잡도를 가진다.) 많이 느렸다. -_-



따라서 HashMap은 Key, Value 쌍을 가지는 데이터를 관리할 때 용이하고,

ArrayList는 데이터가 입력 되고 삭제가 빈번하지 않은 경우에 사용하면 되고,

Linkedlist는 Queue와 같이 Head와 Read와 가까이에서 탐색, 삭제가 이뤄지는 경우에 쓰면 좋을 듯 하다.



테스트에 사용한 코드는 아래와 같습니다. 참고 하실 분은 아래의 소스를 사용하시면 됩니다.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

public class TestCode {
	static Integer[] testArray = new Integer[5000000];
	Integer[] values = new Integer[4990];

	public void hashMapTest() {
		long start = System.nanoTime();

		HashMap hashmap = new HashMap();
		for(Integer integer : testArray){
			hashmap.put(integer, integer);
		}
		
		long end = System.nanoTime();
		
		System.out.println("\nHashMap Test");
		System.out.println("\t입력 소요 시간  " + second(start, end) + "초");
		
		start = System.nanoTime();
		for(Integer value : values){
			hashmap.get(value);
		}		
		end = System.nanoTime();
		System.out.println("\t탐색 소요 시간 " + second(start, end) + "초");
		
		start = System.nanoTime();
		for(Integer value : values){
			hashmap.remove(value);
		}
		end = System.nanoTime();
		System.out.println("\t삭제 소요 시간 " + second(start, end) + "초");
	}
	
	public void arrayListTest(){
		long start = System.nanoTime();
		
		ArrayList arrayList = new ArrayList();
		for(Integer integer : testArray){
			arrayList.add(integer);
		}
		
		long end = System.nanoTime();
		
		System.out.println("\nArrayList");
		System.out.println("\t입력 소요 시간  " + second(start, end) + "초");
		
		start = System.nanoTime();
		for(Integer value : values){
			arrayList.get(value);
		}		
		end = System.nanoTime();
		System.out.println("\t탐색 소요 시간 " + second(start, end) + "초");
		
		start = System.nanoTime();
		for(Integer value : values){
			arrayList.remove(value);
		}
		end = System.nanoTime();
		System.out.println("\t삭제 소요 시간 " + second(start, end) + "초");
	}
	
	public void linkedListTest(){
		long start = System.nanoTime();
		
		List linkedList = new LinkedList();
		for(Integer integer : testArray){
			linkedList.add(integer);
		}
		
		long end = System.nanoTime();
		
		System.out.println("\nLinkedList");
		System.out.println("\t입력 소요 시간  " + second(start, end) + "초");
		start = System.nanoTime();
		for(int value : values){
			linkedList.get(value);
		}		
		end = System.nanoTime();
		System.out.println("\t탐색 소요 시간 " + second(start, end) + "초");
		
		start = System.nanoTime();
		for(int value : values){
			linkedList.remove(value);
		}
		end = System.nanoTime();
		System.out.println("\t삭제 소요 시간 " + second(start, end) + "초");
	}


	private void prepare() {
		long start = System.nanoTime();
		for (int i = 0; i < testArray.length; i++) {
			testArray[i] = i;
		}
		long end = System.nanoTime();
		
		ArrayList temp = new ArrayList(1000);
		for(int i = 0 ; i < 4990 ; i++){
			temp.add(i * 1000);
		}
		temp.toArray(values);
		
		
		System.out.println(testArray.length + "개의 인스턴스 생성 시간 " +
				second(start, end) + "초");
		
	}
	
	private double second(long start, long end){
		return (end - start) / Math.pow(10, 9);
	}

	public void start() {
		prepare();
		hashMapTest();
		arrayListTest();
		linkedListTest();
	}

	public static void main(String[] args) {
		TestCode test = new TestCode();
		test.start();
	}
}


  1. 잘봤습니다. 2013.12.15 01:25

    Hashmap은 중복된 값이 들어오면 이전의 값이 새로 들어온 값으로 바뀝니다.
    즉 입력하는 과정에서 계속 중복되므로 결국에 size는 1이 됩니다.
    이는 탐색속도를 측정할때 영향을 주게됩니다. (결국 size가 1인 map에서 탐색하니 속도가 빠르게 나올껍니다.)

    • 마음이 뛰다 2013.12.16 13:28 신고

      글의 제일 위쪽에 보시면
      테스트 엘리먼트는 Integer 인스턴스 5,000,000(5백만개)를 이용하여 수행하였다.
      탐색과 삭제에서는 균등하게 떨어진 4990개의 데이터를 이용하였다.
      라고 써두었습니다. 키를 같은 값으로 하지는 않았습니다.

  2. HELLO 2014.02.25 15:01

    ArrayList가 배열을 사용해서 검색에 빠르다고는 하나 아무래도 HashMap이 빠르겠죠. 잘보았습니다.

    • 마음이 뛰다 2014.04.17 15:55 신고

      둘다 O(1)이긴 하나, 연산 자체가 직접 접근과 해시 계산 후 접근이니,
      접근 자체는 ArrayList가 빠르지요. 삽입, 삭제, 검색 등의 연산이 각각 특성화된 것들이 있으니 무엇이 빠르다, 느리다 보다는 무엇을 어디에 적절하게 써야 하는지를 알아야 할 것 같습니다 :)

    • hhh 2017.10.31 13:47

      HashMap은 get 함수 자체로, key에 대한 value를 검색해서 가져오고, ArrayList는 get을 사용할 경우에는 index를 기준으로 가져오는 것입니다.
      ArrayList는 String 혹은 별도의 Object에 대하여 많이 사용하는데, index를 모를 경우에는 별도로 검색하는 부분을 구현해주어야하고 그러면 검색은 HashMap에 비해서는 많이 느려질겁니다.


[그림 1] AWT에서 한글이 깨져서 나오는 현상


AWT에서 한글 입력 시에 네모 글자로 나올 때가 있습니다. 정확한 이유는 모르겠지만 프로젝트의 인코딩과

컴파일시의 인코딩이 일치하지 않아서 발생하는 문제인 듯 합니다.


제 경우에는 프로젝트의 기본 인코딩이 UTF-8 인 경우였습니다.


보통은 Inherited Character Encoding 이라고 해서 디폴트 인코딩을 사용해서

문제 없이 사용할 수 있었는데, UTF-8로 지정하니 한글이 깨져버리네요.


이럴 때는 -Dfile.encoding=MS949 라는 VM Arguments를 추가해주면 됩니다.


상세한 설정 방법을 알아보도록 하겠습니다.


먼저 이클립스의 Package Explorer에서 프로젝트를 선택한 후 Alt + Enter 키 또는 마우스 오른쪽 버튼을 눌러

Properties for Project 대화상자를 엽니다. 제 경우의 프로젝트 이름은  GUI_MathML이군요.


[그림 2] Package Explorer에서 프로젝트 선택





[Properties for Project] 대화 상자가 열리면 Run/Debug Settings 탭으로 이동합니다.

그리고 Launch configurations for 'Project name': 항목을 보면 최근에 실행한 메인 클래스가 보이는데,

AWT의 메인이 되는 클래스를 클릭하고 [Edit...] 버튼을 누르시거나 더블클릭 해서 실행 설정 편집창을 엽니다.


※ 만약 아무런 클래스도 보이지 않는다면 [New...] 버튼을 눌러 Java Application을 선택하고 Main class를 선택하고, 이 후에 나오는 내용을 동일하게 적용하면 됩니다.


[그림 3] 프로젝트 속성 대화상자의 Run/Debug Settings 탭




[Edit configuration] 창이 열리면 Arguments 탭으로 이동하여 VM arguments: 란에

-Dfile.encoding=MS949 라고 입력해 준 후 [OK]버튼을 눌러 모든 설정을 완료하고 닫아줍니다.

[그림 4] Edit Configuration 대화상자의 Arguments 탭의 VM arguments 항목 수정





여기까지 설정을 마쳤으면 다시 프로그램을 실행해 봅니다.


[그림 5] 설정 완료 후  AWT 프로그램 재실행



이제 한글이 정상적으로 나옵니다. :D




힙을 구현하기는 귀찮아서 '혹시나' 싶어서 찾아보니 '역시나' 고맙게도 자바에 PriorityQueue가 있네요.


먼저 PriortyQueue는 java.util 패키지에 있고, 여러 형태의 생성자가 오버로딩되어 있는데,

주요한 두가지만 알아보면 아래와 같습니다.


  • PriortyQueue<E>()
  • PriorityQueue<E>(int initialCapacity, Comparator<? super E> comparator)


둘을 비교하자면 전자는 기본 생성자를 이용해서 심플하게 생성하는 방법이고,

후자는 초기 큐 내부의 배열 크기와 Comparator 객체를 전달하면서 객체를 생성하는 방법입니다.

(어째서인지 initialCapacity 없이 Comparator만 전달하는 생성자는 없더군요.)


※ initialCapacity?

큐 내부에서 사용하는 배열의 초기 크기를 결정하는 값입니다.

기본적으로 11의 크기를 사용하고 임의로 전달할 경우에는 그 크기를 사용하는데 1보다 작은 경우

IllegalArgumentException 예외가 발생합니다. 전달 인자가 잘못되었단 얘기죠.

배열의 크기는 initialCapacity로 고정되는 것이 아니라, 요소의 수가 배열의 크기에 도달하면

크기를 늘립니다. 재밌는 것은 배열의 크기가 커질 때 규칙을 보면 배열의 크기가 64 미만일 때는 2씩 커지고,

그 이후로는 이전 크기의 1/2 씩 증가하도록 되어있습니다.

왜 이런 룰이 적용되었는지는 잘 모르겠네요.

큐 내의 배열의 최대크기는 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

이렇게 정의되어 있으니, 값으로 보면 2147483639 까지입니다. 왠만한 경우엔 다 들어갈 수 있겠죠?

다만, 힙 메모리가 부족해서 최대 크기가 되기 전에 OutOfMemoryError가 먼저 발생하겠네요.



큐에서 중요한 두 메서드, 큐에 넣고(add), 큐에서 빼는(poll) 메서드에 대해 간단하게 설명하겠습니다.

덤으로 가장 우선순위가 높은 요소를 확인하는 peek 메서드도요.

  • boolean add(E e) : 큐에 요소를 추가합니다. 반환값은 성공 여부입니다.
  • E poll() : 큐에서 우선순위가 가장 높은 요소를 빼냅니다. 즉 반환 후에 큐에서 삭제됩니다.
  • E peek() : poll과 달리 큐에서 삭제하지 않고 가장 우선순위가 높은 요소를 얻습니다.



1. PriorityQueue()

기본 생성자를 이용하는 경우는 제너릭 타입 E의 compareTo 메서드를 이용하여 정렬에 사용합니다.

compareTo 메서드는 그냥 compareTo메서드를 만들어 쓰는 것이 아니라, 

Comparable 인터페이스를 구현해서 만듭니다.


만약 Comparable 인터페이스를 구현하지 않은 클래스를 큐의 엘리먼트로 사용하는 경우에는

java.lang.ClassCastException을 만나게 될 겁니다. PriorityQueue 내부에서 Comparable 타입으로 캐스팅해서

compareTo 메서드를 호출하기 때문입니다.


기본 생성자를 이용하여 큐의 객체를 생성하는 경우를 소스코드로 작성해볼까요.


먼저 요소로 사용할 Element 클래스를 작성해 보겠습니다.

class Element implements Comparable<Element>{
	private int num; // 정렬의 기준이 될 값
	
	public Element(int num){
		this.num = num;
	}
	
	public int getNum(){
		return num;
	}

	@Override // Comparable 인터페이스의 compareTo 메서드 구현
	public int compareTo(Element o) {
		return num <= o.num ? -1 : 1;
	}
}


compareTo 메서드의 기능은 현재 객체와 다른 Element 객체를 비교하여 우선순위를 판단하는 것입니다.

우선순위의 판단은 반환값으로 하는데 음수인 경우에는 현재 객체가 우선임을 의미하고,

양수인 경우에는 대상 객체(위의 소스코드에서는 Element o)가 우선임을 의미합니다.

간편하게 return num - o.num; 을 해도 됩니다.


다음으로 PriorityQueue를 사용해 볼 클래스 PriorityQueueTest 를 작성해 보겠습니다.

public class PriorityQueueTest {
	public static void main(String[] args) {
		PriorityQueue<Element> q =
				new PriorityQueue<Element>();
		
		Random random = new Random(System.nanoTime());
		
		// 0~49의 난수를 생성하여 큐에 넣습니다.
		for(int i = 0 ; i < 10 ; i++){
			q.add(new Element(random.nextInt(50)));
		}
		
		// 큐에서 값을 빼면서 정렬이 되었는지 출력해봅니다.
		int size = q.size();
		for(int i = 0 ; i<size ; i++){
			System.out.println(q.poll().getNum());
		}
	}
}

[출력결과]

4

14

19

19

30

33

42

42

46

49



2. PriorityQueue(int initialCapacity, Comparator<? super E> comparator)

복잡해 보이는 생성자지만 간단합니다. 초기 배열 크기와 Comparator를 구현한 객체를 전달해 주면 되는데,

Comparator<? super E> 가 조금 거슬리는 분도 있을 거라고 생각합니다.

Java 5 부터 제너릭 개념이 들어가면서 컬렉션 관련 API에는 제너릭이 들어가도록 모두 바뀌었습니다.

제너릭이라고 말하는게 꺽쇠 괄호 < > 를 이용해 타입을 지정해 주는 것을 말합니다.


제너릭에 관해서는 다른 글에서 얘기하기로 하고, 위의 <? super E>가 의미하는 것만 알아보면

Comparator 선언 시, 혹은 구현시에 Comparator<타입> 의 형태로 적어줘야 합니다.

이때 ? super E 가 의미하는 게 위에 들어가는 타입은 E를 상속받은 하위 클래스여야 한다는 의미가 됩니다.

풀어보면 어떤 타입(?)의 부모(super)가 E여야 한다. 라고 볼 수 있겠네요.


복잡하게 생각할 것 없이 여기서는 "타입"의 위치에 Element를 적어주도록 합시다.


Comparator 인터페이스를 구현하는 방법은 여러가지가 있겠지만 여기서는 그냥 익명 이너 클래스로 구현하여

전달하도록 하겠습니다. Element 클래스는 위의 클래스 그대로 사용합니다.


class PriorityQueueTest {
	public static void main(String[] args) {
		PriorityQueue<Element> q =
			new PriorityQueue<Element>(50, new Comparator<Element>() {
				@Override
				// Comparator 인터페이스의 compare 메서드를 구현합니다.
				// 여기서는 이전과 반대로 내림차순 정렬을 하도록 했습니다.
				public int compare(Element o1, Element o2) {
					return o2.getNum() - o1.getNum(); 
				}
			});
		
		Random random = new Random(System.nanoTime());
		
		// 0~49의 난수를 생성하여 큐에 넣습니다.
		for(int i = 0 ; i < 10 ; i++){
			q.add(new Element(random.nextInt(50)));
		}
		
		// 큐에서 값을 빼면서 정렬이 되었는지 출력해봅니다.
		int size = q.size();
		for(int i = 0 ; i<size ; i++){
			System.out.println(q.poll().getNum());
		}
	}
}

[출력 결과]

42

36

33

28

26

18

14

4

3

0


내림차순으로 정렬이 되었습니다. 여기서 조금 의아한 점을 찾으신 분도 있을 것 같습니다.

이미 Element는 Comparable 인터페이스를 구현해서 오름차순으로 정렬하도록 되어있는데,

내림차순으로 정렬되었기 때문입니다. 그 의문점의 결과는 위의 출력결과가 말해줍니다.


즉 compareTo 메서드보다 compare 메서드가 우선한다는 말이지요.

(그냥 내부적으로는 if(comparator != null){ ... } 이렇게 되어있어서 문장상에서 우선하게끔 되어있습니다.)



자바의 PriorityQueue 클래스를 사용해 봤습니다.

Comparator나 Comparable인터페이스에 대한 개념이 확실하게 잡힌다면 크게 무리 없이 사용할 수 있을 것 같습니다.

XMLStreamReader 클래스의 next 메서드는 이벤트를 반환하고, 반환된 이벤트에 따라 맞는 처리를 하게 된다.


StAX의 XMLEvent 클래스에는 XML의 이벤트가 상수로 정의되어 있는데, 아래와 같다.

  • XMLEvent.START_ELEMENT
  • XMLEvent.END_ELEMENT
  • XMLEvent.PROCESSING_INSTRUCTION
  • XMLEvent.CHARACTERS
  • XMLEvent.COMMENT
  • XMLEvent.SPACE
  • XMLEvent.START_DOCUMENT
  • XMLEvent.END_DOCUMENT
  • XMLEvent.ENTITY_REFERENCE
  • XMLEvent.ATTRIBUTE
  • XMLEvent.DTD
  • XMLEvent.CDATA
  • XMLEvent.NAMESPACE
  • XMLEvent.NOTATION_DECLARATION
  • XMLEvent.ENTITY_DECLARATION
참고로 이는 XMLStreamConstants에 동일하게 정의되어 있다.

또한 각각의 이벤트에 따라 사용할 수 있는 메서드가 정해져있는데,
이벤트에 맞지 않는 메서드를 사용하면 예외가 발생한다.

 이벤트

유효한 메서드 
 모든 이벤트에 적용가능

 getProperty(), hasNext(), require(), close(), getNamespaceURI(),
 isStartElement(), isEndElement(), isCharacters(),isWhiteSpace(), 

 getNamespaceContext(), getEventType(),getLocation(), hasText(), 

 hasName()

 START_ELEMENT

 next(), getName(), getLocalName(), hasName(), getPrefix(), 

 getAttributeXXX(), isAttributeSpecified(),getNamespaceXXX(), 

 getElementText(), nextTag()

 ATTRIBUTE  next(), nextTag() getAttributeXXX(), isAttributeSpecified()
 NAMESPACE  next(), nextTag() getNamespaceXXX()

 END_ELEMENT

 next(), getName(), getLocalName(), hasName(), getPrefix(), 

 getNamespaceXXX(), nextTag()

 CHARACTERS  next(), getTextXXX(), nextTag()
 CDATA  next(), getTextXXX(), nextTag()
 COMMENT  next(), getTextXXX(), nextTag()
 SPACE  next(), getTextXXX(), nextTag()
 START_DOCUMENT  next(), getEncoding(), getVersion(), isStandalone(), standaloneSet(),
 getCharacterEncodingScheme(), nextTag()

 END_DOCUMENT

 close()
 PROCESSING_INSTRUCTION  next(), getPITarget(), getPIData(), nextTag()
 ENTITY_REFERENCE  next(), getLocalName(), getText(), nextTag()
 DTD  next(), getText(), nextTag()



http://tutorials.jenkov.com/java-xml/stax-xmlstreamwriter.html에서 가져옴


The XMLStreamWriter class in the Java StAX API allows you to write XML events (elements, attributes etc.) either to a Writer, an OutputStream, or a Result (special JAXP object).

Here is a simple example that writes a series of events to disk, using a FileWriter:

XMLOutputFactory factory      = XMLOutputFactory.newInstance();

 try {
     XMLStreamWriter writer = factory.createXMLStreamWriter(
             new FileWriter("data\\output2.xml"));

     writer.writeStartDocument();
     writer.writeStartElement("document");
     writer.writeStartElement("data");
     writer.writeAttribute("name", "value");
     writer.writeEndElement();
     writer.writeEndElement();
     writer.writeEndDocument();

     writer.flush();
     writer.close();

 } catch (XMLStreamException e) {
     e.printStackTrace();
 } catch (IOException e) {
     e.printStackTrace();
 }

The result of executing this code is the following XML file (line breaks inserted for readability):

<?xml version='1.0' encoding='utf-8'?>
<document><data name="value"></data></document>

http://tutorials.jenkov.com/java-xml/stax-xmlstreamreader.html에서 가져옴


The XMLStreamReader class in Java StAX provides a Cursor style API for parsing XML. Like theIterator API it allows you to move from event to event in the XML, letting you control when to move to the next event. An "event" in this case is for instance the beginning of an element, the end of an element, a group of text etc. In other words, pretty much the same events you would get from a SAX parser.

To read more about the difference between the Iterator and Cursor style API's, read the introduction to StAX: Java StAX Parser

You create an XMLStreamReader via the javax.xml.stream.XMLInputFactory class. Here is how that looks:

XMLInputFactory factory = XMLInputFactory.newInstance();

//get Reader connected to XML input from somewhere..
Reader reader = getXmlReader();

try {

    XMLStreamReader streamReader =
        factory.createXMLStreamReader(reader);
    
} catch (XMLStreamException e) {
    e.printStackTrace();
}

Once created you can iterate through the XML input from the underlying Reader. Here is how that looks:

XMLStreamReader streamReader = factory.createXMLStreamReader(
    new FileReader("data\\test.xml"));

while(streamReader.hasNext()){
    streamReader.next();
    if(streamReader.getEventType() == XMLStreamReader.START_ELEMENT){
        System.out.println(streamReader.getLocalName());
    }
}

You obtain the event type by calling the XMLStreamReader.getEventType() method. When you know the event type, you can process the given event as you need.


XML Stream Events

Below is a list of the events you can encounter in an XML stream. There are constants for each of these events in the javax.xml.stream.XMLStreamConstants interface.

  • ATTRIBUTE
  • CDATA
  • CHARACTERS
  • COMMENT
  • DTD
  • END_DOCUMENT
  • END_ELEMENT
  • ENTITY_DECLARATION
  • ENTITY_REFERENCE
  • NAMESPACE
  • NOTATION_DECLARATION
  • PROCESSING_INSTRUCTION
  • SPACE
  • START_DOCUMENT
  • START_ELEMENT

XML Event Processing

From the XMLStreamReader you can get access to the corresponding XML data. You can also get information about where (line number + column number) in the XML stream the event was encountered.

+ Recent posts