Thread & Runnable

Java/Threads 2013. 2. 7. 13:18

Thread 프로그램 생성 방식

 

Java에서는 두 가지 방식으로 thread를 생성할 수 있습니다.

1. Thread Class를 상속
2. Runnable Interface를 구현

Thread Runnable에 모두 선언되어 있는 void run() 메소드를 사용자는 오버라이드 하여 해당 작업을 기술하게 됩니다.

또한 run() 메쏘드는 Thread 클래스의 Start()메쏘드에 의하여 호출 되기 때문에 따로 호출하지 않아도 됩니다.

Thread Runnable의 차이는 Class Interface에서도 볼 수 있다시피 "상속"의 차이 입니다.
기본적으로 Java는 다중상속을 금지하고 있습니다.
따라서, A라는 클래스가 B 클래스를 상속받아야 하며, A가 쓰레드로 동작되어야 할 경우,
Runnable implement하면 됩니다.

Runnable은 오로지 run() 메쏘드만 정의되어 있으므로, 쓰레드에 대하여 다른 작업이 필요할 경우,
생성할 때
Thread t = new Thread(정의한 Runnable);
 형태로 생성하여 사용하여야 합니다.

아래는 위 설명에 대한 예제 code 입니다.

 

 

public class MyThread extends Thread {

    @Override

    public void run() {

        System.out.println("Hello, MyThread");

    }

}

 

 

public class MyRunnable implements Runnable {

    @Override

    public void run() {

        System.out.println("Hello, MyRunnable");

    }

}

 

 

public class MyTest {

   public static void main(String[] args) {

       Thread t1 = new Thread(new MyRunnable());

       t1.start();

                

       MyThread t2 = new MyThread();

       t2.start();

   }

}

 

 

출처 : http://truth4u.tistory.com/62

참조 : http://stevenjsmin.tistory.com/24 [ Thread Socket 프로그램 ]

 





다른 유용한 글

http://blog.naver.com/hyoun1202/90192727041



 Multi-Thread 구현하는 방법은 2가지가 있다.

 

1. java.lang.Thread 클래스를 상속받아서 구현하는 방법

    *   장점 : Thead 클래스의 메소드를 바로 사용 가능함 
    *  
단점 : 대상 클래스가 다른 클래스로부터 상속받아야하는 경우 사용할 없음  

2, java.lang.Runnable 인터페이스를 구현해서 개발하는 방법

    *   장점조상(또는 공통클래스로부터 상속받은 자손 클래스들도 Thread 사용 가능 
    *  
단점 : 스레드 클래스의 메소드 사용 불가

   Thead 클래스의 메소드들에 대한 정보는 http://ra2kstar.tistory.com/130 참조

 

다음은 1, 2 방법으로 각각 구현된 클래스들의 소스 코드와 이들을 실행하는 메인 함수의 소스 코드는 다음과 같다.



1. 메인 함수의 소스코드 

package sample.multithread;


public class MultiThreadExam {

/**

 * Multi-Thread Example<br>

 * [Multi-Thread 구현] 멀티 스레드를 구현하는 2가지 방법은 다음과 같다.<br>

 * 1. java.lang.Thread 클래스 이용 

         *         장점 : 스레드 클래스의 메소드를 바로 사용 가능함 

         *         단점 : 대상 클래스가 다른 클래스로부터 상속받아야하는 경우 사용할 수 없음<br>

 * 2. java.long.Runnable 인터페이스 이용 

         *         장점 : 다른 클래스로부터 상속받은 클래스도 스레드로 사용 가능 

         *         단점 : 스레드 클래스의 메소드 사용 불가

 

 * @author 황철연

 */

public static void main(String[] args) {


// 1번 방식으로 구현된 클래스 실행하기

ThreadExam threadExam = new ThreadExam();

threadExam.start();


// 2번 방식으로 구현된 클래스 실행하기

RunableExam runableExam = new RunableExam();

Thread runableExamTh = new Thread(runableExam);

runableExamTh.start();

}

}


 


2-1. 1 방법으로 구현된 클래스의 소스 코드 

package sample.multithread;

import java.awt.AWTException;

import java.awt.Robot;


public class ThreadExam extends Thread {


    // 핵심코드

    public void run() {

   mainJob();

    }


    public void mainJob() {

  for (int i = 0; i < 5; i++) {

System.out.println("ThreadExam.mainJob() 호출 : " + i);

setTimerOn(4); // 3초 Delay - 테스트용 함수

}

    }


    /**

     * 테스트용 함수 지정된 시간 동안 프로세스를 sleep시킨다.

     

     * @param timer 지연시간(단위:초)

     */

    protected boolean setTimerOn(int timer) {  


Robot tRobot;

int delayTime;

boolean result;

delayTime = timer * 1000; // mm초 단위에 맞도록 *1000을 한다.


try {

    tRobot = new Robot();

            // delay() 함수를 이용하여 delayTime 밀리초 동안 프로세스를 sleep 상태로 만든다.

    tRobot.delay(delayTime); 

    result = true;


} catch (AWTException e) {

    System.out.println(this.getClass().getName() + ",setTimerOn(" + timer + ")오류");

    result = false;

}


return result;

    }

}



 


2-2.  2 방법으로 구현된 클래스의 소스 코드 

package sample.multithread;

public class RunableExam extends AbstractBean implements Runnable {

    /**

     * Runnable 인터페이스 구현시 반드시 새로 정의해야( Overriding) 하는 함수 

     * @see java.lang.Runnable#run()

     */

     public void run() {

         mainJob();

     }

 

     public void mainJob(){

         for (int i = 0; i < 5; i++) {  

     System.out.println("RunableExam.mainJob() 호출 : " + i);  

     setTimerOn(3); //3초 Delay - 테스트용 함수

 }

     }

}



 


 RunableExam 클래스에서 상속받은 조상 클래스의 소스 코드


package sample.multithread;

import java.awt.AWTException;

import java.awt.Robot;


public class RunableExam extends AbstractBean implements Runnable {

    /**

     * 테스트용 함수 

     * 지정된 시간 동안 프로세스를 sleep시킨다.

     * @param timer 지연시간(단위:초)

     */ 

     protected boolean setTimerOn(int timer){ 

     

         Robot tRobot;

         int delayTime;

         boolean result;

         delayTime = timer * 1000; // mm초 단위에 맞도록 *1000을 한다.

     

         try {

             tRobot = new Robot();

             // delay() 함수를 이용하여 delayTime 밀리초 동안 프로세스를 sleep 상태로 만든다.

             tRobot.delay(delayTime);

             result = true;


         } catch (AWTException e) {

             System.out.println(this.getClass().getName()+ ", setTimerOn(" + timer + ") 오류");

             result = false;

         }

        

         return result;

     }

}

 

MultiThread 기존 Thread 관련없는 별도의 독립적인 Thread 실행시키므로 이들의 결과값을 리턴받는 방법이 까다롭다.

상기 예제의 경우 메인함수가 각각의 Thread 실행시킨 Thead 처리결과를 return 받을 없게된다. 왜냐면 각각의 Thread 메인함수가

실행중인 Thread와는 별개로 생성된 신규 Thread에서  각각 실행되기때문이다.

 

코드로 설명하면...

public static void main(String[] args) {
 

    String rtn1 = '0';

    String rtn2 = '0';     


    ThreadExam threadExam = new ThreadExam();
    threadExam.start();

    rtn1 = threadExam.getResult();
  
    RunableExam runableExam = new RunableExam();
    Thread runableExamTh = new Thread(runableExam);
    runableExamTh.start();

    rtn2 = runableExam.getResult();

   

    System.out.println(  rtn1 + "," + rtn2 );


 }

 

상기 예에서 ThreadExam  RunableExam 쓰래드가 정상적으로 실행 완료된 후의 결과값으로 각각(getResult() 반환값) '1' 반환 한다고

가정해보자. 이때 코드에서 System.out.println() 결과가 과연 '1,1' 찍힐까아니다. 결과는 '0,0'으로 찍힌다.

이유는 앞서 말했듯이 Thread main() 함수에 얽매이지 않고 독립적으로 실행되므로 main() 함수의 코드는 그냥 밑으로 흘러내려버린다.

그래서 '0,0' 찍히고 main() 함수의 실행은 종료된다. 그러나 ThreadExam RunableExam 쓰래드는 각각 독립적으로 실행되다가 종료될 것이다.

그럼 Thread 실행 결과를 Thread 실행한 객체에서 잡아내려면 어떻게 해야 할까? 이때 join() 함수를 사용하면 된다.

코드를 다음과 같이 변경해 보자.

public static void main(String[] args) {


    String rtn1 = '0';

    String rtn2 = '0';


    ThreadExam threadExam = new ThreadExam();
    threadExam.start();

 

    RunableExam runableExam = new RunableExam();
    Thread runableExamTh = new Thread(runableExam);
    runableExamTh.start();

 

    //Thread 상태 점검
    System.out.println( "ThreadExam isAlive  : " + threadExam.isAlive() );
    System.out.println( "RunableExam isAlive : " + runableExamTh.isAlive() );
  
    try {
         //join
Thread 끝날때까지 대기!
         threadExam.join();
         runableExamTh.join();


    } catch (InterruptedException e) {
         e.printStackTrace();
    }

     

    rtn1 = threadExam.getResult();

    rrn2 = runableExam.getResult();

    //Thread 최종결과반환

    System.out.println( rtn1 + "," + rtn2 );
      
    //Thread
상태 점검
    System.out.println( "ThreadExam isAlive  : " + threadExam.isAlive() );
    System.out.println( "RunableExam isAlive : " + runableExamTh.isAlive() );

}

 

코드의 핵심은 Thead.join() 함수이다. join 해주면 해당 쓰래드가 살행 완료될 때까지 main() 메소드가 기다리게된다. ThreadExam 

RunableExam  쓰래드는 이전과 같이 별개의 Thread 각각 독립적으로 실행되지만 각각의 쓰래드가 실행 완료 될때까지 main() 함수가 기다렸다가

이후 코드를 실행시킨다. 따라서 이때 main() 함수의 최종 결과값은 '1,1' 찍히게 된다.

추가로 상기 코드에서 새로 추가된 Thead.isAlive() 함수는 해당 쓰래드가 실행중인지 아닌지를 체크할 사용한다. 코드에서는 Thread 실행한

직후의  isAlive() 함수는 true 반환하고 실행 완료된 후에는 false 반환한다.

 

'Java > Threads' 카테고리의 다른 글

Thread Synchronization  (0) 2013.02.07
Controlling Thread Status  (0) 2013.02.07
Posted by Steven J.S Min
,

네트워크와 쓰레드(Thread)

 

    다음코드는 쓰래드 인터페이스를 구현한 클래스를 이용하여 서버와 클라이언트간 Socket통신을 통하여 메시지를 교환하는 코드이다.

    클라이언트는 서버로 메시지를 전송하면 서버는 하나의 클라이언트들로 부터 받은 메시지를 접속된 모든 클라이에게 다시 전송하는

    프로그램이다. 

 

서버 프로그램 : SocketServer.java

package server;

 

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.ArrayList;

import java.util.Iterator;

 

/**

 * 소켓 통신하기위한 기본 서버 프로그램.

 * @author JSMIN */

public class SocketServer implements Runnable {

         final static int SOCKET_SERVER_PORT = 5000;

         BufferedReader reader;

 

         // 클라이언가 접속할때마다 해당하는 출력스트림을 저장하기위한 배열.

         static ArrayList<PrintWriter> clientOutputStreams;

 

         // 클라이언트 요청에의해 생성된 소켓을이용하여 BufferedReader 객체를 생성한다.

         public SocketServer(Socket clientSocket) {

                  try {   InputStreamReader isReader = new InputStreamReader(

                                                              clientSocket.getInputStream());

                           reader = new BufferedReader(isReader);

 

                  } catch (IOException e) {

                           e.printStackTrace();

                  }

         }

 

         /* *

          * 별도의 실행스텍에서 실행되게 하기위한 메소드 구현 Runnable 인터페이스를 구현한 메소드.

          * @see java.lang.Runnable#run()  

          */

         @Override

         public void run() {

                  String message;

 

                  try {    while ((message = reader.readLine()) != null) {

                                   System.out.println("클라이언트메시지:" + message);

 

                                   // 접속한 모든 클라이언트들에게 메시지를 전송한다.

                                   send2AllClient(message);

                           }

 

                  } catch (Exception e) {

                           e.printStackTrace();

                  }

         }

 

         /**

          * 접속한 모든 클라이언트들에게 메시지를 전송한다.

          * @param message */

         public void send2AllClient(String message) {

 

                  // 클라이언트가 서버에 접속할때마다 clientOutputStreams PrintWriter객체들을 저장한다.

                  Iterator<PrintWriter> it = clientOutputStreams.iterator();

 

                  // 접속한 클라이언트 수 만큼 루프처리

                  while (it.hasNext()) {

                           try {  PrintWriter writer = (PrintWriter) it.next();

                                   writer.println(message);

                                   writer.flush();

 

                           } catch (Exception e) {

                                   e.printStackTrace();

                           }

                  }

         }

 

         /**

          * 소켓서버 메인메소드

          * @param args  */

         public static void main(String args[]) {

                  clientOutputStreams = new ArrayList<PrintWriter>();

 

                  try {   ServerSocket serverSocket = new ServerSocket(SOCKET_SERVER_PORT);

                           System.out.println("클라이언트 접속대기...");

                          

                           while (true) {

                                   // 클라이언트 연결요청 Listen 대기

                                   Socket clientSocket = serverSocket.accept();

 

                                   // 클라이언트로 부터 연결요청에의해 생성된 소켓을이용하여

                                   // 출력스트림 생성하여 출력객체를 만든다.

                                   PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());

                                   clientOutputStreams.add(writer);

 

                                                             // 쓰레드 실행: 별도의 프로그램 스택을 구성하여 서버프로그램 실행.

                                   Thread t = new Thread(new SocketServer(clientSocket));

                                   t.start();

 

                                   System.out.println("서버에 클라이언트 연결 생성...");

                           }

                  } catch (Exception ex) {

                           ex.printStackTrace();

                  }

 

         }

 

}

 

 

클라이언트 프로그램 : SocketClient.java

package client;

 

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.Socket;

 

/**

 * 서버와 통신하기위한 기본 클라이언트 프로그램.

 * @author JSMIN */

public class SocketClient {

         final static String SOCKET_SERVER_IP = "127.0.0.1";

         final static int   SOCKET_SERVER_PORT = 5000;

 

         BufferedReader reader;

         PrintWriter writer;

         Socket socket;

 

         /**

          * 소켓클라이언트 메인 메소드

          * @param args  */

         public static void main(String args[]) {

 

                  SocketClient client = new SocketClient();

 

                  try {   // 서버로의 연결을 구성한 후

                           // 반환된 소켓으로부터 주고 받을 객체(reader/writer)를 얻는다.

                           client.socket = new Socket(SOCKET_SERVER_IP, SOCKET_SERVER_PORT);

                           InputStreamReader streamReader = new InputStreamReader

                                                                   (client.socket.getInputStream());

                          

                           client.reader = new BufferedReader(streamReader);

                           client.writer = new PrintWriter(client.socket.getOutputStream());

 

                           System.out.println("서버로 연결이 형성됨!!");

 

                  } catch (IOException e) {

                           e.printStackTrace();

                  }

 

                  // Runnable 인터페이스를 구현한 클라이언트 프로그램(Job로직이 들어있는)을 실행한다.

                  Thread clientJobs = new Thread(new ClientJobs(client.reader));

                  clientJobs.start();

                 

                  client.sendMessage();

         }

 

         /**

          * 서버에게 메시지를 보내기위하여 위에서

          * 생성한 소켓으로부터 얻은 출력스트림과 연계된 writer를 이용한다*/

         private void sendMessage() {

                  BufferedReader bufferedReader;

                  String message;

 

                  try {    while (true) {

                                   bufferedReader = new BufferedReader(new InputStreamReader(System.in));

                                   message = bufferedReader.readLine();

 

                                   writer.println(message);

                                   writer.flush();

                           }

 

                  } catch (IOException e) {

                           e.printStackTrace();

                  }

 

         }

 

}

 

 

클라이언트 프로그램 : ClientJobs.java

package client;

 

import java.io.BufferedReader;

 

/**

 * 클라이언트에서 일기위한

 * @author JSMIN  */

public class ClientJobs implements Runnable {

 

         BufferedReader reader;

 

         public ClientJobs(BufferedReader reader) {

                  this.reader = reader;

         }

 

         @Override

         public void run() {

                  String message;

                  try {    while ((message = reader.readLine()) != null) {

                                   System.out.println("서버메시지: " + message);

                           }

 

                  } catch (Exception ex) {

                           ex.printStackTrace();

                  }

         }

}

 

 

 

SocketProgram.zip

'Java > Networking' 카테고리의 다른 글

기본 HTTP 웹서버 구현 – TinyHttpd Server  (0) 2013.02.12
서버와의 시간동기화 [ 소켓통신 ]  (0) 2013.02.11
RMI (Remote Method Invovation)  (0) 2013.02.01
Proxy Server 란  (1) 2013.02.01
Posted by Steven J.S Min
,

소개

Generics 타입에 대한 추상성을 제공한다.Collection에서 쓰이는 컨테이너 타입이 가장 일반적인 예가  것이다.예전에 작성하던 일반적인 코드는 다음과 같다.

List myIntList = new LinkedList(); // 1

myIntList.add(new Integer(0)); // 2

Integer x = (Integer) myIntList.iterator().next(); // 3

 

 

3 번째 줄에서 캐스팅을 하는 것은 다소 번거롭다. 대개의 경우 프로그래머는 어떤 리스트에 어떤 타입의 데이터가 들어갈 것인지 알고있지만, 캐스팅은 필수적인 작업이다. 컴파일러는 iterator 의해 Object 리턴될 것이라는 것까지 밖에 보장하지못한다. Integer 타입이란 것을 명확히 하기 위해서는 캐스팅을 밖에 없다. 물론, 캐스팅 작업은 프로그램을난잡하게할 뿐만 아니라, 런타임 에러의 가능성을 발생시킨다.


만약 프로그래머가 만들고자 하던 의도대로 리스트에 특정한 타입만 들어갈 있도록 강제할 있다면 얼마나 좋겠는가! 이것이 바로 generics 핵심 아이디어다. Generics 이용한 프로그램의 예는 다음과 같다.

List<Integer> myIntList = new LinkedList<Integer>(); // 1

myIntList.add(new Integer(0)); // 2

Integer x = myIntList.iterator().next(); // 3 

 

myIniList 변수에 타입에 대한 정의를 했다는 것이 중요하다. List<Integer>라고 명확하게 적어줌으로써 인스턴스가아무 타입이나 들어갈 있는 리스트가 아니라 Integer 리스트라는 것을 명확하게 한다. 이런 경우 List는타입인자(예제의 경우는 Integer) 받는 generic 인터페이스라한다. list 객체를 생성하는 시점에 타입인자를 명확히해준다.


한가지 주의할 점은 3'에서 보는 것과 같이 타입 캐스팅이 사라졌다는 것이다.

3 번째 줄에서 타입캐스팅을 하는 대신 1'에서 타입 파라미터로 Integer 설정해 줌으로 인해 프로그램이 한결 간결해졌다.

매우 중요한 차이는컴파일러가이제 타입을 체크할 있기 때문에 인자의 정확성을 컴파일 타임에 있게 되었다는 것이다. myIntListList<Integer> 정의되었을 경우 그게 언제어디서 쓰이던 타입 안정성에 대해 컴파일러로 부터 보장받을 수있다. , 타입이 명확하지 않으면, 컴파일이 되지 않는다. 또한 이렇게 코딩함으로 개발자는 인스턴스에 들어가는 타입을 더직관적으로 있다.

 

 

간단한 Generics 정의하기

다음은 java.util 패키지에 있는 List Iterator 선언부를 발췌한 것이다.

public interface List<E> {

    void add(E x);

 

    Iterator<E> iterator();

}

 

public interface Iterator<E> {

       E next();

 

       boolean hasNext();

}

 

<> 안에 들어간 요소를 제외하면, 위의 코드는 유사하다. 이게 List Iterator에서 타입인자를 정의하는 방법이다.타입인자는 정해진 타입을 사용하는 generic의 선언부를 통해 정의할 수 있다.(여기에는 몇 가지 제한 사항이 있다.)


List<Integer> 같은 List generic 방식의 선언을 이미 살펴보았다. Parameterized type 이라고 불리는 이런 방식의사용법은 일반적인 타입인자( 경우는 E라고 정의된 부분) 에서 모두 사용되며, 사용시에 특정한 타입( 경우에는Integer)으로 바뀔 있다.

다시 말해 List<Integer> 위의 코드에서 E라고 정의된 부분이 Integer 바뀐 것이라고 생각하면 된다.

public interface IntegerList {

       void add(Integer x);

 

       Iterator<Integer> iterator();

}


이렇게 쓰는 것은 매우 직관적이지만, 오해의 소지가 있다.

Parameterized type List<Integer> 이런 식의 확장의 경우 굉장히 명확해 보인다. 그러나, 이런 식으로 인터페이스가 선언되어 있다면, 확장을 수가없다. Generic type 선언은 한번만 컴파일 되면, 아무데서나 있다. , 다른 일반적인 클래스나 인터페이스와 같이 하나의 클래스 파일만 생기게 된다.

 입인자는 일반적인 메쏘드나 생성자에서 사용하는 보통 인자들과 비슷하다메쏘드에서 사용하는 일반적인 인자와 마찬가지로generic 선언부에 타입인자를 정의한다메쏘드가 호출되면실제값으로  인자가 치환되어 메쏘드의 내부가 실행되는 것과마찬가지로 generics 선언이 실행되면 실제 인자는타입 파라미터 값을 대체하게 된다.

이름을 짓는 데는 몇가지 약정이 있다. 타입인자는 어떤 의미를 내포하는(가능하면 글자로) 하기를 권고한다. ( Map 경우 key, value시스템이기 때문에<K, > 라고 표현하고 있다.)  

소문자를  타입인자로 사용하지 말아야 일반적인 클래스나인터페이스에서 쓰이는 인자와 명확하게 구분할 있다.  Element라는 의미로 많은 경우 E 사용하는 좋은 예이다.

 

 

Generics 하위 타입

아래 코드를 보자.

List<String> ls = new ArrayList<String>(); // 1

List<Object> lo = ls; // 2


 번째 줄은 당연히 맞다 번째 줄이 문제다다시 정리하면 다음과 같은 문제가 된다. String 리스트는 Object 리스트가   있는가보통은 "당근빠따다!"라고 생각할 거다.


그럼 추가적으로 다음과 같은 코드가 있을 때는?

lo.add(new Object()); // 3

String s = ls.get(0); // 4: Object String 대입하려고 한다!


Stirng 리스트인 ls Object 리스트인 lo 대입하고, lo Object 넣었다. 그래서 lo에는 이제 String만들어있는 아니다. 위와 같은 문제를 방지하기위해 컴파일러는 번째 줄에서 에러를 발생시킨다.

타입이고 G 어떤 Generic 타입일 경우일반적으로 Foo Bar 하위 타입이라고해서 G<Foo> G<Bar> 하위 타입이 되는 것은 아니다. Row Type 경우 상속의 Rule 적용되지만 파라미터 타입( 예제에서 String Object) 이런 룰이 적용되지 않는다, Parameterized type 경우에는 동일한 Type 이여야 한다.

 

 

Wildcards

Collection 요소들을 루프를 돌면서 찍는 코드를 생각해보자. 이전 버전에서는 다음과 같은 코드가 것이다.

void printCollection(Collection c) {

       Iterator i = c.iterator();

       for (int k = 0; k < c.size(); k++) {

              System.out.println(i.next());

       }

}

 

다음은 generics 이용한 새로운 방법이다.

 

void printCollection(Collection<Object> c) {

       for (Object e : c) {

              System.out.println(e);

       }

}

 

문제는 이런 새로운 방식이 이전 방식에 비해 별로 쓸모가 없다는 것이다. 예전 방식의 코드는 모든 종류의 Collection 쓸수 있었지만, 새로운 방식은Collection<Object> 있다. Collection<Object> 모든 Collection 상위 타입이 아니다.

다시 잠시 정리를 하고 넘어가면.


void foo(Collection<Object> arg) {

    ….

}


 같이 선언된 메쏘드에 대해
foo(Collection<String>타입의 머시기


 같은 방법으로 호출할  없다는 것이다.

그러면, 모든 타입의 Collection 대한 상위타입은 무엇일까?  Collection<?>라고 쓰면 된다. 이를 Wildcard 타입이라 부른다. Wildcard 타입을 이용한 메쏘드는 다음과 같이 쓰면 된다.

 

void printCollection(Collection<?> c) {

       for (Object e : c) {

              System.out.println(e);

       }

}


렇게하면 모든 타입의 collection 대해서 메쏘드를 사용할 있다. printCollection() 메쏘드 안을 잘보면 여전히 인자를 Object 타입으로 사용하는것을 있다. 이렇게 쓰는 것은 항상 안전하다.

 

그러나 다음과 같은 코드는 문제가 있다.

 

Collection<?> c = new ArrayList<String>();

c.add(new Object()); // 컴파일 에러!


c 가지는 요소의 타입이 무엇인지 불확실하기 때문에 Object 인스턴스를 추가할 수가 없다. add() 메쏘드는 인자로 E라는인자 타입을 가지는데, 경우? 정의되어 있기 때문에 여기서는 여전히 없는 타입이기 때문이다. 이런 경우add() 들어갈 있는 유일한 값은 null 뿐이다.

반대로 List<?>에서 get() 생각해보자. 리스트의 타입이 무엇인지 없지만, Object 하위 타입이란 알고 있다. 그래서 이런 경우 get() Object타입을 리턴한다.

제한된 wildcards

사각형과 원과 같은 어떤 모양을 그리는 간단한 애플리케이션을 생각해보자. 프로그램에서 이런 모양을 표현하기 위해서는 다음과 같은 상속관계가 정의될것이다.

public abstract class Shape {

       public abstract void draw(Canvas c);

}

 

public class Circle extends Shape {

       private int x, y, radius;

 

       public void draw(Canvas c) { ... }

}

 

public class Rectangle extends Shape {

       private int x, y, width, height;

 

       public void draw(Canvas c) { ... }

}

이들을 그리기 위한 캔버스는 다음과 같다.  

public class Canvas {

       public void draw(Shape s) {

              s.draw(this);

       }

}


Shape들을 한번에 그리기 위해서는 다음과 같은 메쏘드가 있을 법하다.

public void drawAll(List<Shape> shapes) {

    for (Shape s: shapes) {

        s.draw(this);

    }

}


drawAll() 정확히 Shape 리스트에 의해 호출된다. 따라서 List<Circle> 같은 것은 인자로 넣을 없다. , 인자로 들어가는 것은 Shape 하위객체까지 들어갈 있어야 한다. 이런 경우 메쏘드는 다음과 같이 정의되면 된다.

public void drawAll(List<? extends Shape> shapes) { ... }

List<Shape> List<? extends Shape> 바뀐 것이 포인트다. 이렇게 하면 List<Circle> 타입도 인자로 사용할 있다.

List<? extends Shape> 제한된 wildcard 예제다. ? 불분명한 타입을 표현하는 것인데, ? Shape의하위타입으로 제한을 하는 것이다. 이런 경우Shape Wildcard upper bound 부른다.

Wildcard 유연함을 사용하는 데는 대가가 따른다. 메쏘드 안에서 Shapes라는 인자에 추가하는 것은 된다. 예를 들면 다음과 같은 코드는 된다.

public void addRectangle(List<? extends Shape> shapes) {

    shapes.add(0, new Rectangle()); // compile-time error!

}


메쏘드를 호출하는 시점을 생각해보자. 인자로 List<Circle> 들어올 수도 있다. 그렇기 때문에List<? extends Shape> 선언된 변수에 Shape 하위클래스인 Rectangle 넣을 없다.

다음은 허용된다. (예제에서 Driver Person 하위타입이다.)

public class Census {

       public static void addRegistry(Map<String, ? extends Person> registry){...}

}

 

Map<String, Driver> allDrivers = ...;

Census.addRegistry(allDrivers);

 

Generic 메쏘드

클래스에만 generic 적용되는 것은 아니다클래스 내부의 메서드도 파라메터화를   있다  클래스는 제너릭이  수도 있고 안될 수도 있다.

제네릭 메소드를 정의할 때는 반환  앞에 제너릭 파라미터 목록을 추가하면 된다.

public class GenericMethods {

       public <T> void f(T x) {

       }

}



제너릭 클래스를 사용할 때는 객체 생성시 타입 파라미터를 반드시 지정해야 한다. 하지만 제너렉 메소드를 사용할 때는 파라미터 타입을 항상 지정할 필요는 없다. 컴파일러가 찾아줄 수 있기 때문이다. 이것을 타입 인자 추론(type argument inference)라고 한다.

어떤 array 모든 요소를 Collection 추가하는 메쏘드를 만들고자 한다첫번째 생각은 이러하다.

static void fromArrayToCollection(Object[] a, Collection<?> c) {

       for (Object o : a) {

              c.add(o); // 컴파일 에러!

       }

}



이제 초보자들이 흔히 하는 실수인 Collection<Object> 메쏘드에 인자로 쓰는 것과 같은 실수를 피하는법을 알아보자역시 Collection<?>  경우통하지 않는다이런 경우에 사용하는 것이 바로generic 메쏘드다타입 정의와 마찬가지로 메쏘드 정의에서도 generic 사용할  있다.

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {

       for (T o : a) {

              c.add(o); // 옳다

       }

}



이제 array 요소의 상위타입으로 구성된 collection 이용하여  메쏘드를   있다.

       Object[] oa = new Object[100];

       Collection<Object> co = new ArrayList<Object>();

       fromArrayToCollection(oa, co); // T Object

             

       String[] sa = new String[100];

       Collection<String> cs = new ArrayList<String>();

       fromArrayToCollection(sa, cs); // T String

       fromArrayToCollection(sa, co); // T Object

             

       Integer[] ia = new Integer[100];

       Float[] fa = new Float[100];

       Number[] na = new Number[100];

       Collection<Number> cn = new ArrayList<Number>();

       fromArrayToCollection(ia, cn); // T Number

       fromArrayToCollection(fa, cn); // T Number

       fromArrayToCollection(na, cn); // T Number

       fromArrayToCollection(na, co); // T Object

       fromArrayToCollection(na, cs); // 컴파일 에러

The method fromArrayToCollection(T[], Collection<T>) in the type Execution_1 is not applicable for the arguments (Number[], Collection<String>)

 

실제 인자가 어떤 타입인지 메쏘드를 호출할  넘겨주지 않는다는 것에 유의하자컴파일러가 넘어오는 인자에 따라 알아서  처리해준다

여기서  가지 의문점이 생긴다어떤  generic 메쏘드를 사용할 것이며어떤  wildcard 타입을 사용할 것인가다음의 Collection 라이브러리를 살펴보자.

interface Collection<E> {

       public boolean containsAll(Collection<?> c);

       public boolean addAll(Collection<? extends E> c);

}



이것을 generic 메쏘드를 사용하면 다음과 같이 된다.

interface Collection<E> {

       public <T> boolean containsAll(Collection<T> c);

 

       public <T extends E> boolean addAll(Collection<T> c);

       // 타입 변수도 정해져야 한다구!

}



그러나 conatainsAll addAll 모두 타입인자 T 한번만 사용된다리턴 타입은 타입인자에 무관하며메쏘드의인자와도 상관없다 여기서 인자의 타입은 다형성을 위해 사용된 것이다다시 정리하면, generic 메쏘드는   이상의인자 사이의 타입 연관성을 위해 정의된 것이다, Wildcard 인자의 하위유연성을 위해 만들어졌다.

물론, Generic 메쏘드와 wildcard를 동시에 사용하는 케이스도 있을 수 있다. 다음은 Collections.copy() 메쏘드 이다.

class Collections {

       public static <T> void copy(List<T> dest, List<? extends T> src) {...}

}



두개의 파라미터 간의 연관성을 살펴보자원본 array src로부터 복사된 객체 dest 반드시 T 할당 될  있어야 한다그러므로, src 타입은 반드시 T 하위 타입이어야 한다
위의 선언부를 wildcard 쓰지 않고 정의하는 방법도 있다.

class Collections {

       public static <T, S extends T> void copy(List<T> dest, List<S> src) {...}

}



첫 번째 타입인자 T는 첫 번째 메쏘드 인자의 선언부와 두 번째 타입인자 S를 정의하기 위한 부분에 쓰였다. S src의 타입을정의하기 위해 한 번만 쓰였다. S는 두 번째 타입인자를 정의하기 위한 곳 외에는 쓰이지 않았다. 다른 곳에 쓰이지 않았기때문에 S wildcard로 변환될 수 있다. wildcard를 쓰는것이 훨씬 더 명확하다. wildcard를 썼다는 것은 이것이 다른 용도로 사용되지 않는다는 것을 명확히 해주기 때문이다.

wildcard는 메쏘드 선언부 밖에서 멤버 변수나 array 등의 타입을 정의할 수 있다는 이점도 있다. Shape 그리는 문제로 돌아가보자그리기 전에 인자로 넘어온 Shape 리스트의 리스트를(List  겹이다.) static 멤버변수에 저장하는 상황을 가정하자.

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();

 

public void drawAll(List<? extends Shape> shapes) {

       history.addLast(shapes);

       for (Shape s : shapes) {

              s.draw(this);

       }

}



  경우 T 같이 쓰지 않고, wildcard 사용하는 것이 바람직하다. T 재사용할 필요가 없기 때문이다 위와 같이여러 개의 ?들끼리 굳이 구별을 할필요가 있다면 ?대신 T,S 같은 파라미터 변수를 사용하면 된다위와 같이 중첩된 경우도마찬가지다.

 

기존 코드와의 호환

지금까지는 generic 지원하는 버젼에 대해서만 알아보았다하지만 이미 기존에 만들어진 소스가 있을 것이다기존에 짜여진 모든 소스 코드를 수정하는것은 있을  없는 일이다.
10 장에서 기존코드를 generic 사용하는 새로운 코드로 바꾸는 것에 대해 알아보도록 하겠다이번 장에서는 훨씬 간단한 문제인기존 소스 코드와의 호환성에 대해서만 알아볼 것이다이것은  부분으로 나뉘어 진다기존 코드를 generic 사용된코드에서 사용하는 것과 generic 들어간 코드를 기존 코드에서 사용하는 것이다.

기존 코드를 generic 코드에서 사용하기.


generic 장점을 충분히 살리면서 기존 코드를 사용하는 법을 알아보겠다.
예제로 com.Fooblibar.widgets 패키지를 사용하고자 한다 치자.(Fooblibar  하는 건지는  필요 없으므로 생략!)

package com.Fooblibar.widgets;

 

public interface Part { … }

 

public class Inventory {

       /**

        * Adds a new Assembly to the inventory database.

        * The assembly is given the name name, and consists of a set parts

        * specified by parts. All elements of the collection parts must support the

        * Part interface.

        **/

       public static void addAssembly(String name, Collection parts) {...} 

       public static Assembly getAssembly(String name) {...}

}

 

public interface Assembly {

       Collection getParts(); // Returns a collection of Parts

}


  위에서 소개한 API 사용하는 코드를 짠다고 가정하자. addAssembly() 메쏘드를 호출할  적절한 인자로 호출하는게 바람직할 것이다그러니까Collection<Part>라고 정의된 generic type 명시적인 인자로호출을 하고자 한다.

package com.mycompany.inventory;

 

import com.Fooblibar.widgets.*;

 

public class Guillotine implements Part {

}

 

public class Main {

 

       public static void main(String[] args) {

              Collection<Part> c = new ArrayList<Part>();

              c.add(new Guillotine());

              c.add(new Blade());

              Inventory.addAssembly("thingee", c);

 

              Collection<Part> k = Inventory.getAssembly("thingee").getParts();

       }

}

addAssembly  호출할   번째 인자는 Collection 타입이다그리고 실제로 넘기는 인자는Collection<Part> 타입이다어떻게 작동할까컴파일러는Collection 어떤 타입으로 지정될지모른다. Collection 같은 generic type 타입인자 없이 호출  때는 row-type으로 호출된다.

이미 살펴봤던 것처럼 Collection Collection<?> 의미이지 Collection<Object> 의미하는  아니다라고 생각할 것이다. 그러나 위의 코드를 보면 별로 그런  같지도 않다

Collection<Part> k = Inventory.getAssembly("thingee").getParts();

가 정상적으로 작동하고 있기 때문이다. Collection<?> Collection<Part>로 명시적인 캐스팅없이 할당이 불가능하다. , Collection<?>로 호출이 되었다면에러가 났어야 마땅하다.

이러한것은 정상적으로 작동한다. 다만 "unchecked warning"(@로 시작하는 annotation 중 하나) 이 없다면, warning이 생긴다. 컴파일러는 정합성을 보장해 줄 수 없기 때문이다. getAssembly()라는 기존에 이미 존재하던메쏘드가 Collection<Part>를 리턴할 지를 체크할 방법이 없다. 위에서 사용한 코드에서는단지Collection이라고만 되어 있고, 그런 경우 모든 object를 사용할 수 있게 하는 방법뿐이다.

이론적으로만 봤을 때 이건 별로 합당치 못하지만, 기존에 쓰던 코드를 버리라고 할 수는 없다. 이건 개발자의 몫이다.

raw type이란 결국 wildcard와 매우 흡사하지만, raw type은 타입체크를 하지 않는 것이다. 이것은 기존 코드와의 호환성을 고려한 결정이다.

generic 코드에서 기존의 코드를 호출하는 것은 위험하다. 타입에 대한 안정성을 어떻게도 보장할 수 없기 때문이다.


Erasure  Translation

public String loophole(Integer x) {

       List<String> ys = new LinkedList<String>();

       List xs = ys;

       xs.add(x); // 컴파일 시점 unchecked warning

 

       return ys.iterator().next();

}

String 리스트를 옛날 방식의 리스트에다가 집어 넣으려 한다. 컴파일 시점에 warning이 뜬다. 이번에는 Integer를 리스트에 넣고 String으로 뽑아 쓰려는 경우를 보자. 물론, 잘못된 시도다. Warning을 무시하고 일단 실행시켜보면 잘못된 타입으로 뽑아 쓸 때 에러가 난다.

public String loophole(Integer x) {

       List ys = new LinkedList();

       List xs = ys;

       xs.add(x);

      

       return (String) ys.iterator().next(); // 타임 에러.

}

리스트로 부터 요소를 뽑아와 String으로 캐스팅을 하려고 하면 ClassCastException이 발생하게 된다.

컴파일러는 erasure라는 generics에 대한 첫 단계 변환을 실행한다. 소스 코드 차원에서의 변환이라고 생각하면 대략 맞다. 이는 generic을 제거하는 작업이라고 볼 수 있다.

erasure generic type 정보를 전부 제거해버리는 작업이다. List<String> List로 바꿔버리는 것처럼 <> 사이에 감싸진 모든 정보를 날려버린다.

기존 코드에서 generic 코드 사용하기

반대의 경우를 생각해보자. Fooblibar.com API generics를 사용하도록 바꿔보자. 그러나 그 API를 쓰는 코드는 아직 generics를 사용하지 않는다고 하면 아래와 같은 코드가 나올 것이다.

package com.Fooblibar.widgets;

 

public interface Part { … }

 

public class Inventory {

       /**

        * Adds a new Assembly to the inventory database.

        * The assembly is given the name name, and consists of a set parts

        * specified by parts. All elements of the collection parts must support the

        * Part interface.

        **/

    public static void addAssembly(String name, Collection<Part> parts) {...}

    public static Assembly getAssembly(String name) {...}

}

 

public interface Assembly {

       Collection<Part> getParts(); // Returns a collection of Parts

}


그걸 사용하는 코드는 다음과 같을 것이다.

위 의 코드는 generics가 나오기 전에 짜여졌으며, com.Fooblibar.widgets collection 라이브러리(둘다 generics를 쓴다) 를 사용하고 있다. 위의 코드에서generics가 사용되어야 할 부분은 전부 raw type으로정의되어 있다.

첫번째 줄에서 Collection<Part>를 사용해야하는데 raw typeCollection이 호출되므로 unchecked warning이 발생한다. 컴파일러 입장에서는 이CollectionCollection<Part>라고 확인할 방법이 없다.

 

Fine Print

Generic 클래스는  클래스의 모든 객체에 공유된다.
다음 코드는 무엇을 출력할까?

List<String> l1 = new ArrayList<String>();

List<Integer> l2 = new ArrayList<Integer>();

 

System.out.println(l1.getClass() == l2.getClass());



false가 출력될 거라 생각하겠지만 틀렸다. true를 출력한다. 모든 Generic 클래스의 인스턴스는 실제 타입이 무엇이건 간에 같은 Run-time 클래스를 가지게된다.

Generic이냐 아니냐를 결정하는 것은 타입 인자에 의한 것인데, 어떤 타입인자를 쓰건 그 클래스가 다른 것이 되진 않는다.

static 변수나 메쏘드는 그 클래스의 모든 인스턴스에 공유된다. 따라서 static 메쏘드나 static initializer에서 타입인자를 지정하는 것은 안 되며, static 변수의선언부나 initializer에 사용할 수도 없다. 다음 코드는 에러투성이다.

public class Test<E> {

       public static List<E> a = null;

       static {

              List<E> b = null;

       }

       public static List<E> c = null;

       public static List d = new ArrayList<E>();

}


 

Cast  instanceOf

특정 타입인자로 구성된 인스턴스가 타입인자를 포함한 generics 클래스의 인스턴스인지 체크하는 것은 허용되지 않는다.

Collection cs = new ArrayList<String>();

if (cs instanceof Collection<String>) { ...} // 안된다!



마찬가지로 다음 코드도 문제가 있다.

Collection<String> cstr = (Collection<String>) cs; // unchecked warning



여기서는 unchecked warning이 발생하는데, 이건 런타임 시스템에서 체크할 수 있는 게 아니기 때문이다.

다음 코드도 마찬가지다.

<T> T badCast(T t, Object o) {

       return (T) o; // unchecked warning

}


run time에 타입 변수라는 것은 없다. 이는 시간적으로나 공간적으로 과부하를 일으키지 않는다는 것이다. 이런 점은 바람직하지만, 믿을만한 캐스팅을 할수는 없다.

Arrays
Wildcard 타입을 제외하고는 generics array 만들  없다아래 코드를 보자.

List<String>[] lsa = new List<String>[10]; // 이건 되지!

Object o = lsa;

 

Object[] oa = (Object[]) o;

List<Integer> li = new ArrayList<Integer>();

li.add(new Integer(3));

oa[1] = li; // 말도 되지만, run time에는 무사통과

String s = lsa[1].get(0); // run-time error – ClassCastException



만 약에 parameterized type이 허용된다면 위와 같은 문제가 생길 수 있고, unchecked warning도 없이컴파일은 성공하고, run time에 에러가 발생할 것이다. generics를 사용하면 uncheck warning 없이컴파일이 잘 되었다는 것은 모든 타입이 다 정상적이라는 것을 보장한다.

그러나 여전히 wildcard는 쓸 수 있다. 위에서 사용한 코드를 조금 바꿔보자.

List<?>[] lsa = new List<?>[10]; // ok, wildcard 타입은 괜찮아.

Object o = lsa;

Object[] oa = (Object[]) o;

List<Integer> li = new ArrayList<Integer>();

li.add(new Integer(3));

oa[1] = li; // 옳타꾸나~

String s = (String) lsa[1].get(0); // run time error 그러나 waning 떠준다.

마찬가지로 타입변수로  element 가지는 array 만들려고 하면 컴파일 에러가 발생한다.

<T> T[] makeArray(T t) {

       return new T[100]; // error

}


타입변수로 된 것은 run time에는 존재하지 않기 때문에, 실제로 array의 요소의 타입이 뭔지 알 방법이 없다.

 

Class Literals as Run-time Type Tokens

JDK 1.5에서의 변화 중 하나는 java.lang.Class generic이 되었다는 것이다. 다른 클래스들이 generic을 사용한다는 것보다 훨씬 재미있는 예이다.

Class는 타입 변수 T를 가지고 있는데, 왜 있는 것일까? Class가 어떤 타입을 나타내는 지를 표현하는 것이다.

예 를 들어, String.class Class<String>이 되고, Serializable.classClass<Serialize>가 되는 것이다. 이렇게 하면, reflection을 이용한 코드에서 안정성을높일 수있다.

특히 Class<T>.newInstance() T를 리턴한다.

예를 하나 들어보자. db query를 수행하는 유틸리티 메쏘드를 작성하는데, 그 메쏘드는 Collection의 인스턴스를 반환한다고 가정하자.

Factory 인스턴스를 인자로 받는 코드는 대략 다음과 같을 것이다.

interface Factory<T> {

       T make();

}

 

public <T> Collection<T> select(Factory<T> factory, String statement) {

    Collection<T> result = new ArrayList<T>();

    /* run sql query using jdbc */

    for (/* iterate over jdbc results */ ) {

        T item = factory.make();

        /* use reflection and set all of item’s fields from sql results */

        result.add(item);

    }

    return result;

}



이는 다음과 같이 호출할  있다.

select(new Factory<EmpInfo>() {

                  public EmpInfo make() {

                         return new EmpInfo();

                  }

           } , "selection string");


 

또는 다음과 같이 Factory 인터페이스의 구현체인 EmpInfoFactory 클래스를 정의할 수도 있다.

class EmpInfoFactory implements Factory<EmpInfo> {

      ...

       public EmpInfo make() {

              return new EmpInfo();

       }

}



EmpInfoFactory를 정의했을 경우 호출하는 코드는 다음과 같다.

select(getMyEmpInfoFactory(), “selection string”);

아래의 두 가지 방법으로 정리가 된다.

§  호출하는 쪽에서 factory 클래스를 익명으로 만든다.

§  또는, 호출할 때 적합한 Factory 클래스들을 만들어 놓고, 각각의 인스턴스를 생성해서 호출한다. (매우 바람직하지 않다!)

Reflection을 쓰면, 이런 식으로 복잡하게 factory 클래스들을 만들어 내지 않을 수 있다.

Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");

       ...

 

public static Collection select(Class c, String sqlStatement) {

    Collection result = new ArrayList();

          

    /* jdbc 이용한 sql 수행 */

    for ( /* sql 수행 결과를 반복 */ ) {

        Object item = c.newInstance();

        /* 수행된 결과로 부터 만들어진 필드를 reflection 이용해서 세팅. */

        result.add(item);

    }

    return result;

}



그러나이런 식으로 하면 우리가 정말 원하는 타입의 collection 받아올  없다그래서 Class generic으로 선언된 것이며이를 이용하여 다음과 같이코드를 수정할  있다.

Collection<EmpInfo> emps = sqlUtility.select(EmpInfo.class, "select * from emps");

       ...

 

public static <T> Collection<T> select(Class<T>c, String sqlStatement) {

    Collection<T> result = new ArrayList<T>();

    /* jdbc 이용한 sql 수행 */

    for ( /* sql 수행 결과를 반복 */ ) {

        T item = c.newInstance();

        /* 수행된 결과로부터 만들어진 필드를 reflection 이용해서 세팅. */

        result.add(item);

    }

    return result;

}



이렇게 하면처음의 의도대로 원하는 타입의 collection 뽑아올  있다

 

Wildcards 심화학습

이번 장에서는 wildcard를 좀더 심도 있게 다뤄볼 생각이다. 읽기용으로 제한된 wildcard를 쓰는 것은 이미 몇 번 봐왔다. 이번에는 쓰기 전용을 생각해 보자.

Sink라는 예제 인터페이스를 생각해보자.

interface Sink<T> {

       flush(T t);

}


위 인터페이스를 사용하는 아래와 같은 코드를 생각해보자. writeAll 이라는 메쏘드는 coll이라는 변수로 정의된 Collection의 모든 원소를 Sink flush()에대입시키고, 마지막 원소를 반환하는 메쏘드다.

public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {

       T last;

       for (T t : coll) {

              last = t;

              snk.flush(last);

       }

       return last;

}

 

Sink<Object> s;

Collection<String> cs;

String str = writeAll(cs, s); // 잘못된 호출



위 에서 나타난 바와 같이 writeAll은 잘못 호출되고 있다. 왜냐하면 메쏘드 선언부에서는 T로 동일한 generic으로사용하였지만호출하는 부분에서는cs(Collection의 인스턴스) s(String의 인스턴스)가 각각 다른generic을 사용하기 때문이다.

writeAll() 메쏘드를 다음과 같이 고쳐보겠다.

public static <T> T writeAll(Collection<? extends T>, Sink<T>){...}

       ...

String str = writeAll(cs, s); // 호출은 되겠지만, return type 맞음.



호출까지는 잘 되겠지만 잘못된 코드다. 왜냐하면 return type Object로 넘어오기 때문이다. 위와 같이 호출할 경우 s Sink<Object> 이므로, T Object를 의미하게 된다.

이런 경우 해답은 lower bound. <? super T>으로 쓰면 된다.  ? extends T T의 하위객체라는 뜻이라면, ? super T T의 상위 객체라는 의미이다. 이를 사용한 코드는 아래와 같다.

public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk){...}

...

String str = writeAll(cs, s); // 아싸~~



이렇게 쓰면, 호출도 정상적으로 되고 return type도 우리가 원했던 대로 된다.

좀 더 현실적인 예제를 들어보자. java.util.TreeSet<E> E라는 원소 타입을 가지며 정렬된 Set을 만들수 있다. 이는 몇 개의 생성자를 가지는데, 그 중에Comparator를 인자로 받는 생성자가 있다. 이 인자는 정렬할 방법을정의해준다. 생성자를 정의하는 부분은 다음과 같이 생겼을 것 같다.

TreeSet(Comparator<E> c);

Comparator 인터페이스의 정의는 다음과 같다.

interface Comparator<T> {

    int compare(T fst, T snd);

}


TreeSet<String> 을 적절한 Comparator를 통해 호출하려고 하면 String 끼리 비교할 수 있는 Comparator를 만들어서 생성자에전달해주어야 한다. , Comparator<String>을 만들어 주어야 한다. 그러나Comparator<Object>도 그 역할을 해낼 수 있다. 그러나,TreeSet(Comparator<E> c) 와 같은 생성자로할 경우 E String을 의미하기 때문에Comparator<Object>는 받을 수가 없다. 이럴 경우 유연성을 위해 lower bound를 사용하면 된다., Comparator를인자로 받는 생성자는 다음과 같이 정의되면 된다.

TreeSet(Comparator<? super E> c)

이렇게 하면 이 생성자에 적용할 수 있는 모든 Comparator를 받을 수 있다.

lower bound의 마지막 예를 들어보자. Collections.max() 메쏘드를 살펴보자. 이 메쏘드는 인자로 받은 collection의 최대 값을 반환한다.

 최대값은 정렬 방법에 따라 달라질 수 있다. 때문에 인자로 받은 collection의 모든 원소들은 Comparable을 구현해야 한다.(Comparator가 아니다.) 게다가그 원소들끼리는 서로 비교가 가능해야 한다.

이를 코드로 정리해 보면 다음과 같이 될 것이다.

public static <T extends Comparable<T>>

T max(Collection<T> coll)



Collection<T> T를 원소로 가지는 어떤 Collection을 의미한다. 리턴 타입이 T라는 것은 인자로 받은 Collection의 원소의타입으로 리턴된다는 것이다. T Comparable<T>를 구현해야 서로 비교가 가능하다. 그러나 이것은 너무제약이 심하다. 다음의 코드를 보면 그 이유를 알 것이다.

class Foo implements Comparable<Object> {...}

...

Collection<Foo> cf = ...;

Collections.max(cf); // 돌아가면 좋겠는걸...



cf 의 모든 원소는 cf의 다른 원소들과 비교가 가능하다. cf의 원소는 Foo의 타입이고, Foo Comparable을 구현하고있기 때문이다. 그러나 위에서 정의한것처럼 Collections.max()가 정의되어 있다면, 위의 코드는 작동하지 않는다.Foo Comparable<Foo>를 구현한 것이 아니라 Comparable<Object>를 구현했기때문이다. 사실 정확히 Comparable<Foo>를 구현해야만 Foo의 인스턴스들이 서로 비교 가능한 것은 아니다.Foo의 상위 타입으로 비교를 해도 된다.그래서 Collections.max()를 다음과 같이 정의하면 된다.

public static <T extends Comparable<? super T>>

T max(Collection<T> coll)



만 약에 만들고자하는 API에서 타입 인자 T를 사용만 한다면, lower bound( ? super T) 가 나을 것이고,API T의 타입을 리턴한다면 유연성을 위해 upper bound( ? extends T) 를 사용하는 게 나을 것이다.

Wildcard Capture

/** Set s 어떤 element 추가해주는 메쏘드 */

public static <T> void addToSet(Set<T> s, T t) {...}


위와 같은 코드가 있을 때, 아래와 같은 코드로 호출한다고 치자.

Set<?> unknownSet = new HashSet<String>();

addToSet(unknownSet, “abc”); // 아니되옵니다~


unknownSet은 실제로는 HashSet<String>이지만 변수를 정의할 때는 generic 타입이 무엇인지 알 수 없기 때문에 이는 정상적으로 작동하지 않는다.

다음 코드를 살펴보자.

class Collections {

    ...

    <T> public static Set<T> unmodifiableSet(Set<T> set) { ... }

}

...

Set<?> s = Collections.unmodifiableSet(unknownSet); // 이건 정상적으로 작동한다. ??



제대로 작동하면 안 될 것 같다. 그러나 Set<T> Set<?>로 캐스팅이 가능하기 때문에 정상적으로작동한다. 결국 unmodifiableSet은 요소의 타입이 무엇이든 간에 상관없이 정상적으로 작동한다.

이 런 상황은상대적으로 많이 일어날 수 있기 때문에 안전하다는 게 보장된 코드를 받아들일 수 있도록 해야 하는 규칙이 있다. 이 규칙을wildcard capture라고 부르는데, 이는 컴파일러가 wildcard로 표현된 알 수 없는 타입을 타입 인자로 처리할 수있게 해준다.

 

기존 코드를 Generic 사용하도록 변경하기

지금까지 기존에 generic을 사용하지 않는 코드와 새로 짠 generic을 사용하는 코드 간의 상호작용에 대해서 알아보았다. 이제 기존 코드를 generic을 사용하도록 바꾸는 좀 더 어려운 작업에 들어가 보자.

generic을 사용하도록 코드를 바꾸기로 결정하면, API를 어떻게 바꿀 것인지 잘 생각해보아야 한다.

기존에 이 API를 사용하던 코드들도 여전히 정상적으로 작동해야 하므로, 지나치게 제약을 주면 안 된다. java.util.Collection를 다시 살펴보자. 아래는generic을 사용하기 전의 코드다.

interface Collection {

       public boolean containsAll(Collection c);

       public boolean addAll(Collection c);

}



이를 generic을 사용해서 대충 바꿔보면 아래와 같이 된다.

interface Collection<E> {

    public boolean containsAll(Collection<E> c);

    public boolean addAll(Collection<E> c);

}



타입이 안전하게 된 것 같아 보이지만, 사실 containsAll()은 어떤 타입의 Collection이든 다 받을 수 있어야한다. ("인자로 받을 수 있다" "결과가 false"는 다른 얘기다.) 이 메쏘드가 true를 리턴하기 위해서는 인자로받은 Collection E 타입의 값들을 가지고 있어야 할 것이다. 그러나 인자로 넘어오는Collection이 정확히 E의타입이란 보장은 없다. E의 서브 타입일 수도 있다.

addAll()에 있어서도 E의 서브 타입이 추가 가능하도록 만들어야 한다. section 5 에서 이 방법에 대해 다루었다.

이런 식으로 generic을 추가할 때는 기존에 이 API를 사용하는 코드들도 정상적으로 작동하도록 해야 한다. 그러니까, generic이 적용되지 않은 코드에서도정상적으로 작동하도록 해야 한다는 것이다.

이번에는 section 9에서 보았던 Collections.max()를 다시 살펴 보자. 마지막으로 우리가 생각했던 max()의 선언부는 다음과 같다.

public static <T extends Comparable<? super T>>

T max(Collection<T> coll)



괜찮아 보인다. 하지만 generic을 제거하고 생각해 보면 아래와 같이 된다.

public static Comparable max(Collection coll)



하지만 실제로는 아래와 같이 되어야 정상이다.

public static Object max(Collection coll)


, 리턴 타입은 Object가 되어야 한다.

이런 경우 타입 변수 T를 이용하여 다시 정의를 하면 다음과 같이 된다.

public static <T extends Object & Comparable<? super T>>

T max(Collection<T> coll)


이 것은 multiple bound라는 것의 예다. &를 이용하는 문법이다. T1 & T2 & ..& Tn과 같이 정의된 경우 T T1, T2...Tn 의 서브 타입이 되며, 첫번째 T1의타입으로 erasure가 작동한다.

마지막으로 고려해야 할 사항은 인자로 받는 Collection은 읽기가 가능해야 한다는 것이다. 그래서 JDK에서는 max()가 다음과 같이 정의되어 있다.

public static <T extends Object & Comparable<? super T>>

T max(Collection<? extends T> coll)



 

출처 : http://cafe.naver.com/sqler/830

참조: http://www.oracle.com/technetwork/java/javase/generics-tutorial-159168.pdf

 

 

generics-tutorial-159168.pdf

'Java > Generics' 카테고리의 다른 글

Java Generic과 Erasure  (0) 2013.10.28
Posted by Steven J.S Min
,

In this post, I will go over the basic constructs of dynamic proxy, followed by some notes and things to watch out when implementing it. First, the 5 elements of a dynamic proxy as implemented in this example:

TestImpl: the class behind the proxy, not to be directly invoked by the client. In some cases, this type may not exist.

TestIF: the proxy interface implemented by TestImpl, and the dynamic proxy. This is the interface type the client should reference. One or more interfaces are required to create dynamic proxy. If there is no such interface, you may need to dynamically generate them with tools like ASM.

TestInvocationHandler: the InvocationHandler that handles method invocation on the proxy from the client. It contains an instance of TestImpl to delegate method invocations to.

Test: the test main class.

$Proxy0: the dynamic proxy that implements TestIF, and the client-facing implementation of TestIF.

 

 

package proxy;

 

public interface TestIF {

       String hello(String name);

}

 

 

 

 

 

package proxy;

 

public class TestImpl implements TestIF {

 

       @Override

       public String hello(String name) {

              return String.format("Hello %s, this is %s", name, this);

       }

 

}

 

 

 

package proxy;

 

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

 

public class TestInvocationHandler implements InvocationHandler {

       private Object testImpl;

 

       public TestInvocationHandler(Object impl) {

              this.testImpl = impl;

       }

 

       @Override

       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

              if (Object.class == method.getDeclaringClass()) {

                     String name = method.getName();

                     if ("equals".equals(name)) {

                           return proxy == args[0];

                     } else if ("hashCode".equals(name)) {

                           return System.identityHashCode(proxy);

                     } else if ("toString".equals(name)) {

                           return proxy.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(proxy))

                                         + ", with InvocationHandler " + this;

                     } else {

                           throw new IllegalStateException(String.valueOf(method));

                     }

              }

              return method.invoke(testImpl, args);

 

       }

 

}

 

 

 

 package proxy;

 

import java.lang.reflect.Proxy;

 

public class Test {

 

       /**

        * @param args

        */

       public static void main(String[] args) {

              TestIF t = (TestIF) Proxy.newProxyInstance(TestIF.class.getClassLoader(), new Class<?>[] { TestIF.class },

                           new TestInvocationHandler(new TestImpl()));

              System.out.printf("t.hello(Duke): %s%n", t.hello("Duke"));

              System.out.printf("t.toString(): %s%n", t);

              System.out.printf("t.hashCode(): %H%n", t);

              System.out.printf("t.equals(t): %B%n", t.equals(t));

              System.out.printf("t.equals(new Object()): %B%n", t.equals(new Object()));

              System.out.printf("t.equals(null): %B%n", t.equals(null));

 

       }

 

}

 

 

 

Implementation notes:

When dispatching method invocations on proxy to delegate object, 3 methods from java.lang.Object need special handling: toString(), hashCode() and equals(Object). Since they are related to the proxy object identity, they should be serviced directly by the handler. One option is to base their return values on the proxy param, as in this example.

Other public methods from java.lang.Object are all final, and are not routed to the InvocationHandler by JVM. These methods are:

public final native Class<?> getClass();

public final native void notify();

public final native void notifyAll();

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException;

public final void wait() throws InterruptedException

The handler's invoke method should not invoke methods on the proxy object passed in as the first param (except the 6 final methods listed above), to avoid infinite loop and StackOverflowError. For example, this following debug line should not be used:

// AVOID THIS

System.out.printf("proxy=%s, method=%s, args=%s%n",

                  proxy, method, Arrays.toString(args));

If the handler already knows how to acquire the delegate TestImpl instance, through either direct instantiation or lookup, it can hide it completely from the client. So the only type of TestIF the client knows is the proxy.

The Object[] args passed into handler's invoke method is null, when the invoked method takes no param. This can come as a surprise to many. So a null check is needed before operating on args.

To run this example after compiling all java classes:

$ java Test

t.hello(Duke): Hello Duke, this is proxy.TestImpl@37975d46

t.toString(): $Proxy0@326cbb2d, with InvocationHandler proxy.TestInvocationHandler@74a138

t.hashCode(): 326CBB2D

t.equals(t): TRUE

t.equals(new Object()): FALSE

t.equals(null): FALSE

 

To save/keep/dump the generated proxy class file (with non-standard option):

 $java -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true Test

 

To view the structure of the proxy class:

$ $JAVA_HOME/bin/javap -v \$Proxy0

 

출처 : http://javahowto.blogspot.com.au/2011/12/java-dynamic-proxy-example.html

 

나중에 "http://b.pungjoo.com/17" 의 글도 꼭 읽어볼 것!

'Java > The Java Language' 카테고리의 다른 글

Exception chain  (0) 2013.10.17
Text Encoding  (0) 2013.10.12
The Class Class and Reflection  (0) 2013.02.07
What is Enum in Java  (1) 2013.02.06
Static and Nostatic Initializer blocks  (0) 2013.02.06
Posted by Steven J.S Min
,