[KOSTA JAVA] #Day 11&12 (Thread, Generics)

0f1c04·2021년 3월 3일
0

KOSTA JAVA TIL

목록 보기
10/11
post-thumbnail

프로세스와 스레드

프로세스(Process)

  • 실행 중인 하나의 프로그램
  • 하나의 프로그램이 다중 프로세스를 만들기도 한다.

멀티 태스킹(multi tasking)

  • 두 가지 이상의 작업을 동시에 처리하는 것을 말한다.
  • 멀티 프로세스
    • 독립적으로 프로그램들을 실행하고 여러 가지 작업을 처리한다.
  • 멀티 스레드
    • 한 개의 프로그램을 실행하고 내부적으로 여러 가지 작업을 처리한다.

메인(main) 스레드

  • 모든 자바 프로그램은 메인 스레드 main() 메소드를 실행하면 시작된다.
  • main() 메소드의 첫 코드부터 아래로 순차적으로 실행된다.
  • 실행 종료 조건
    • 마지막 코드 실행
    • return 문을 만나면 종료
  • main 스레드는 작업 스레드들을 만들어 병렬로 코드들을 실행한다.
    • 멀티 스레드를 생성하여 멀티 태스킹을 수행한다.
  • 프로세스 종료
    • 싱글 스레드: 메인 스레드가 종료하면 프로세스도 종료
    • 멀티 스레드: 실행 중인 스레드가 하나라도 있다면, 프로세스 미종료

작업 스레드 생성과 실행

멀티 스레드로 실행하는 어플리케이션 개발

  • 몇 개의 작업을 병렬로 실행할지 결정하는 것이 선행되어야한다.

작업 스레드 생성 방법

  • Thread 클래스로부터 직접 생성

    • Runnable을 매개값으로 갖는 생성자 호출

    Thread thread = new Thread(Runnable target);

    • Runnable 구현 클래스 작성법

      class Task implements Runnable {
        public void run() {
          실행 코드;
        }
      }

      Runnable은 객체고 실제 스레드는 아니므로 위 같은 구현 객체를 생성 후, 이것을 매개값으로 Thread 생성자를 호출하면 작업 스레드가 생성된다.

      Runnable task = new Task();
      
      Thread thread = new Thread(task);

      Runnable 익명 객체를 매개값으로도 사용할 수 있다.

      Thread thread = new Thread(new Runnable() {
        public void run() {
          실행 코드;
        }
      })
  • Thread 하위 클래스로부터 생성

    • Thread 클래스 상속 후 run 메소드 재정의를 통해 스레드가 실핼할 코드 작성
    public class WorkerThread extends Thread {
      @Override
      public void run() {
        실행 코드;
      }
    }

    이렇게 생성된 작업 스레드 객체에서 start() 메소드를 호출하면 작업 스레드는 자신의 run() 메소드를 실행한다.

스레드의 이름

  • 메인 스레드 이름: main

  • 작업 스레드 이름 (자동 설정): Thread-n

    thread.getName();

  • 작업 스레드 이름 변경

    thread.setName("스레드 이름");

  • 코드 실행하는 현재 스레드 객체의 참조 얻기

    Thread thread = Thread.currentThread();

스레드 우선 순위

동시성과 병렬성

  • 동시성: 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아 가며 실행하는 성질을 말한다.
  • 병렬성: 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말한다.

스레드 스케줄링

  • 스레드의 개수가 코어의 수보다 많을 경우
    • 스레드 스케줄링: 스레드를 어떤 순서로 동시성을 실행할 것인가 결정
    • 스케줄링에 의해 스레드들은 번갈아 가며 run() 메소드를 조금씩 실행

자바의 스레드 스케줄링

  • 우선 순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용한다.
  • 우선 순위 방식(코드로 제어 가능)
    • 우선 순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링
    • 1~10까지 값을 가질 수 있으며 기본은 5이다.
  • 순환 할당 방식(코드로 제어할 수 없음)
    • 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행한다.

동기화 메소드와 동기화 블록

공유 객체를 사용할 때의 주의점

  • 멀티 스레드가 하나의 객체를 공유해서 오류가 생길 수 있다.

동기화 메소드 및 동기화 블록 - synchronized

  • 단 하나의 스레드만 실행할 수 있는 메소드 또는 블록

  • 다른 스레든는 메소드나 블록이 실행이 끝날 때까지 대기해야한다.

  • 동기화 메소드

    public synchronized void method() {
      임계 영역; //하나의 스레드만 실행
    }
  • 동기화 블록

    public void method() {
      //여러 스레드가 실행 가능한 영역
      ...
      synchronized(공유객체) {
        임계 영역 //하나의 스레드만 실행
      }
      //여러 스레드가 실행 가능한 영역
      ...
    }

스레드 상태

스레드의 일반적인 상태

  • 스레드 객체를 생성하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아닌 실행 대기상태가 된다.
    • 실행 대기상태란 아직 스케줄링이 되지 않아서 실행을 기다리는 상태
  • 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행상태.
  • 더 이상 실행할 코드가 없다면 스레드의 실행은 멈추게 되고, 이 상태를 종료 상태라 한다.

스레드에 일시 정지 상태가 도입한 경우

  • 스레드의 상태를 확인할 수 있는 방법: Thread.getState()

  • 스레드 상태의 상태 표

    상태열거 상수설명
    객체 생성NEW스레드 객체가 생성, start() 메소드가 호출되지 않은 상태
    실행 대기RUNNABLE실행 상태로 언제든지 갈 수 있는 상태
    일시 정지WAITING다른 스레드가 통지할 때까지 기다리는 상태
    TIMED_WAITING주어진 시간 동안 기다리는 상태
    BLOCKED사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
    종료TERMINATED실행을 마친 상태

스레드 상태 제어

상태 제어

  • 실행 중인 스레드의 상태를 변경하는 것을 말한다.

  • 상태 변화를 가져오는 메소드의 종류

    메소드설명
    interrupt()일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외 처리코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 제어
    notify()
    notifyAll()
    동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 제어
    resume()suspend() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 제어
    sleep()주어진 시간 동안 스레드를 일시 정지 상태로 제어, 주어진 시간이 지나면 자동 실행 대기 상태
    join()join() 메소드를 호출한 스레드는 일시 정지 상태, 실행 대기 상태로 가려면, join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 함
    wait()동기화 블록 내에서 스레드를 일시 정지 상태로 제어, 매개값으로 주어진 시간이 지나면 자동 실행 대기 상태, 시간이 주어지지 않으면 notify(), notifyAll()메소드에 의해 실행 대기 상태로 변화
    suspend()스레드를 일시 정지 상태 제어, resume()메소드를 호출하면 다시 실행 대기 상태 (현재는 사용하지 않는 메소드)
    yield()실행 중 우선순위가 동일한 다른 스레드에게 실행을 양보 후, 실행 대기 상태
    stop()스레드를 즉시 종료 (현재는 사용하지 않는 메소드)

Sleep()

  • 주어진 시간 동안 일시 정지

    try {
      Thread.sleep(1000);
    } catch(InterruptedException e) {
      //interrupt() 메소드 호출시 실행
    }
  • 얼마동안 일시 정지 상태로 있을 것인지 milli second 단위로 지정할 수 있다.

  • 일시 정지 상태에서 interrupt() 메소드를 호출하여 InterruptedException을 발생한다.

yield()

  • 다른 스레드에게 실행을 양보

    public void run() {
      while(true) {
        if(work) {
          System.out.println("Thread 작업 내용");
        } else {
          Thread.yield();
        }
      }
    }
  • yield()메소드를 호출한 스레드는 실행 대기 상태로 돌가고 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질 수 있도록 해준다.

join()

  • 다른 스레드의 종료를 기다림

  • 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때, 결과값을 받아 이용하는 경우에 많이 사용한다.

  • 예제: 1부터 100까지 합을 계산하는 스레드

    //SumThread.java
    public class SumThread extends Thread {
      private long sum;
      
      public long getSum() {
        return sum;
      }
      
      public void setSum(long sum) {
    		this.sum=sum;
    	}
    	
    	public void run() {
    		for(int i=1; i<=100; i++) {
    			sum+=i;
    		}
    	}
    }
    //JoinExample.java
    public class JoinExample {
    
    	public static void main(String[] args) {
    		SumThread sumThread = new SumThread();
    		sumThread.start();
    		
    		try {
    			sumThread.join(); // main스레드를 sumThread가 끝날 때까지 일시 정지시킴
    		} catch(InterruptedException e) {
    		}
    		
    		System.out.println("1~100 합: " + sumThread.getSum());
    	}
    }

스레드 간 협업(wait(), notify(), nofityAll())

두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방의 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다.

이 방식의 핵심은 공유 객체에 있다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait () 메소드를 호출하여 일시 정지 상태로 만든다. wait() 메소드에 시간을 지정하면 notify를 호출하지 않아도 자동적으로 실행 대기 상태가 된다.

  • nofity()는 wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll() 메소드는 wait()에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만든다.
  • 위의 메소드들은 Thread 클래스가 아닌 Object 클래스에 선언된 메소드이므로 모든 공유 객체에서 호출이 가능하다. 다만 이 메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다.
  • 예제: 데이터를 저장하는 스레드(생산자 스레드)가 데이터를 저장하면, 데이터를 소비하는 스레드(소비자 스레드)가 데이터를 읽고 처리하는 교대 작업을 구현
//두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class DataBox {
	private String data;
	
	public synchronized String getData() {
		//data 필드가 null이면 소비자 스레드를 일시 정지 상태로 만듬
		if(this.data ==null) {
			try {
				wait();
			} catch(InterruptedException e) {}
			
		}
		String returnValue =data;
		System.out.println("ConsummerThread가 읽은 데이터: "+ returnValue);
		
		//data필드가 null이면 소비자 스레드를 일시 정지 상태로 만듬
		data = null;
		notify();
		return returnValue;
	}
	
	public synchronized void setData(String data) {
		//data 필드가 null이 아니면 생산자 스레드를 일시 정지 상태로 만듬
		if(this.data !=null) {
			try {
				wait();
			} catch(InterruptedException e) {}
		}
		//data값을 저장하고 소비자 스레드를 실행 대기 상태로 만듬
		this.data = data;
		System.out.println("ProduceThread가  생성한 데이터: "+ data);
		notify();
		
	}
}
//데이터를 생산(저장)하는 스레드
public class ProducerThread extends Thread {
	private DataBox dataBox;
	
	public ProducerThread(DataBox dataBox) {
		this.dataBox = dataBox;
	}
	
	@Override
	public void run() {
		for(int i=1; i<=3; i++) {
			String data = "Data-" +i;
			dataBox.setData(data);
		}
	}
}
//데이터를 소비하는(읽는) 스레드
public class ConsumerThread extends Thread {
	private DataBox dataBox;
	
	public ConsumerThread(DataBox dataBox) {
		this.dataBox = dataBox;
	}
	
	@Override
	public void run() {
		for(int i=1; i<=3; i++) {
			String data = dataBox.getData();
		}
	}
}
//두 스레드를 생성하고 실행하는 메인 스레드
public class waitNotifyEx {

	public static void main(String[] args) {
		DataBox dataBox = new DataBox();
		
		ProducerThread producerThread = new ProducerThread(dataBox);
		ConsumerThread consumerThread = new ConsumerThread(dataBox);
		
		producerThread.start();
		consumerThread.start();

	}
}

스레드의 안전한 종료(stop 플래그, interrupt() )

스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료된다. 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다. 동영상을 다 보지 않고 닫기 버튼을 눌렀을 때처럼 말이다.

즉시 종료를 위하여 Thread는 stop() 메소드를 제공하고 있는데, 이 메소드를 쓸 경우 스레드가 사용중이던 자원들이 불안전한 상태로 남겨지게 되어 사용하지 않을 것을 권고하게 되었다.(deprecated)

  • 스레드의 안전한 종료를 위한 최선의 방법은 run()메소드가 정상적으로 종료되도록 유도하는 것이다. 2가지 방법이 있다.
  • 첫 방법은 stop 플래그를 이용하는 것이다. 다음과 같다
public class XXXThread extends Thread {
    private boolean stop; //stop 플래그 필드
    
    public void run(){
        while(!stop){
            스레드가 반복하는 코드;
        }
        //스레드가 필요한 자원 정리
    }
}
  • 예제: 1초후 출력 스레드를 중지시킴
//1초 후 출력 스레드를 중지시킴
public class StopFlagEx {
	public static void main(String[]args) {
		PrintThread1 printThread = new PrintThread1();
		printThread.start();
		
		try {Thread.sleep(100);} catch (InterruptedException e) {}
		
		//스레드 종료를 위해 stop 필드를 true로 변경
		printThread.setStop(true);
	}
}
//무한 출력 스레드
public class PrintThread1 extends Thread{
	private boolean stop;
	
	public void setStop(boolean stop) {
		this.stop=stop;
	}
	
	public void run() {
		while(!stop) {
			System.out.println("실행 중");
		}
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}
  • 스레드를 run() 종료로 유도하는 두번째 방법은 interrupt() 메소드를 사용하는 것이다.

  • interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시켜 run() 메소드를 정상 종료시킨다.

    주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면 예외가 발생한다는 것이다. 따라서 스레드가 일시정지 상태가 되지 않으면 해당 메소드를 통한 호출은 아무런 의미가 없다.

    일시정지를 하지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있다. interrupt() 메소드가 호출되었다면 스레드의 interrupted()와 isInterrupted() 메소드는 true를 리턴한다. 둘 다 현재 스레드가 interrupted되었는지 확인하는 메소드인데, interrupted() 메소드는 정적 메소드이고, isInterrupted() 메소드는 인스턴스 메소드라는 점이 다르다.

    boolean status = Thread.interrupted();
    boolean status = Object.isInterrupted();
  • 예제 : 1초 후 출력 스레드를 중지시킴

//1초 후 출력 스레드를 중지시킴
public class InterruptEx {
	public static void main(String[]args) {
		Thread thread1 = new PrintThread3();
		thread1.start();
		
		try {Thread.sleep(1000); } catch(InterruptedException e) {}
		
		//스레드를 종료시키기 위해 interruptException를 발생시킴
		thread1.interrupt();
		
	}
}
//무한 반복 출력 스레드이며, sleep()메소드 진행 중 interrupt를 받아 정지함
public class PrintThread2 extends Thread{
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1);
			}
		} catch(InterruptedException e) {}
		
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}
//무한 반복 출력 스레드이며, interrupted()메소드로 interrupt 현재 상태를 받아 정지함
public class PrintThread3 extends Thread{
	public void run() {
		while(true) {
			System.out.println("실행 중");
			if(Thread.interrupted()) {
				break;
			}
		}
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}

데몬 스레드

데몬(Daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 종료되는데, 그 이유는 주 스레드의 보조 역할을 수행하므로 주 스레드가 종료되면 데몬 스레드의 존재 의미가 없어지기 때문이다. 이 점을 제외하면 데몬 스레드는 일반 스레드와 큰 차이가 없다. ex) 워드의 자동저장 기능, JVM의 가비지 콜렉터

  • 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출해주면 된다. 주의할 점은 start() 메소드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하므로 start() 호출 전에 setDaemon(true)를 호출해야 한다.
  • 현재 실행 중인 스레드가 데몬 스레드인지 isDaemon() 메소드를 통해 구별할 수 있다. 맞으면 true를 리턴한다.

제네릭

제네릭 사용 이유

제네릭 타입을 이용하면 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있다. 제네릭은 클래스와 인터페이스, 그리고 메소드를 정의할 때 타입(Type)을 파라미터(Parameter)로 사용할 수 있도록 한다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 도와준다.

제네릭 사용 시 코드의 이점

  • 컴파일 시 강한 타입 체크를 할 수 있다. 실행 시 타입 에러가 나는 것보다 컴파일을 강하게 체크해서 에러를 사전에 방지한다.
  • 타입 변환(casting)을 제거한다. get함수를 통해 저장된 요소를 찾아올 때 타입 변환할 필요가 없다.

제네릭 타입

제네릭 타입이란

  • 타입을 파라미터로 가지는 클래스와 인터페이스
  • 선언 시 클래스 또는 인터페이스 이름 뒤에 <> 부호를 붙인다.
  • <> 사이에는 타입 파라미터가 위치한다.
  • 타입 파라미터
    • 일반적으로 대문자 알파벳 한 문자로 표현한다. ex) T, K, M, V
    • 개발 코드에서는 타입 파라미터 자리에 구체적인 타입을 지정해야 한다.

제네릭 타입 사용 여부에 따른 비교

  • 제네릭 타입을 사용하지 않은 경우

    • Object 타입을 사용할 경우 빈번한 타입 변환 발생하고 프로그램 성능이 저하된다.
    public class Box {
      private Object object;
      public void set(Object object) {this.object = object;}
      public Object get() {return object;}
    }
    Box box = new Box();
    box.set("hello");								//String 타입을 Object타입으로 자동 타입 변환해서 저장
    String str = (String)box.get(); //Object 타입을 String타입으로 강제 타입 변환해서 얻음
  • 제네릭 타입을 사용한 경우

    • 클래스 선언할 때 타입 파리미터를 사용한다.
    • 컴파일 시 타입 파라미터가 구체적인 클래스로 변경된다.
    public class Box<T> {
      private T t;
      public T get() {return t;}
      public void set(T t) {this.t = t;}
    }
    Box<String> box = new Box<String> ();
    ...
    public class Box<String> {
      private String t;
      public String get() {return t;}
      public void set(String t) {this.t = t;}
    }
    Box<Integer> box = new Box<Integer> ();
    ...
    public class Box<Integer> {
      private Integer t;
      public Integer get() {return t;}
      public void set(Integer t) {this.t = t;}
    }

멀티 타입 파라미터

제네릭 타입은 두 개 이상의 타입 파라미터도 사용 가능

  • 각 타입 파라미터는 콤마로 구분한다.

    class<K, V, ...> {...}

    interface<K, V, ...> {...}

  • 자바 7버젼부터는 다이아몬드 연산자를 사용하여 간단히 작성과 사용이 가능해졌다.

    Product<Tv, String> product = new Product<>();

제네릭 메소드

제네릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.

  • 제네릭 메소드 호출
리턴타입 변수 = <구체적 타입> 메소드명(매개값...);//명시적으로 구체적 타입 지정
//Box<Integer> box = <Integer>boxing(100);

리턴타입 변수 = 메소드명(매개값...);//매개값을 통해 구체적 타입 추측
//Box<Integer> box = boxing(100);
  • 제네릭 메소드는 정적 메소드로 사용가능하다(예제에는 거의 정적메소드로만 적혀 있다.)
  • 예제: 제네릭 메소드 호출
//제네릭 메소드가 있는 클래스
public class Util {
	public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2) {
		boolean keyCompare = p1.getKey().equals(p1.getKey());
		boolean valueCompare = p1.getValue().equals(p2.getValue());
		return keyCompare&&valueCompare;
	}
}
//제네릭 타입
public class Pair<K,V> {
	private K key;
	private V value;
	
	public Pair(K key, V value) {
		this.key = key;
		this.value= value;
	}
	
	public void setKey(K key) { this.key = key;}
	public void setValue(V value) { this.value = value;}
	
	public K getKey() { return key; }
	public V getValue() { return value;}
}
//제네릭 메소드 호출
public class Ex {

	public static void main(String[] args) {
		Pair<Integer, String> p1 = new Pair(100, "사과");
		Pair<Integer, String> p2 = new Pair(100, "사과");
		boolean answer = Util.<Integer, String>compare(p1, p2);
		//			   = Util.compare(p1, p2) 비명시적으로 가능
		if(answer) {
			System.out.println("논리 동등 객체");
		} else {
			System.out.println("논리 다른 객체");
		}
	}
}

제한된 타입 파라미터(<T extends 최상위타입>)

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 있을 때가 있다. 이 때 제한된 타입 파라미터(bounded type parameter)를 사용한다.

public <T extends 상위타입> 리턴타입 메소드(매개변수, ...){...} //상위타입은 클래스뿐아니라 인터페이스도 가능하다.
  • 타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능하다.
  • 주의할 점은 메소드의 중괄호 {} 안에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버(필드, 메소드)로 제한된다. 하위 타입에만 있는 필드와 메소드는 사용할 수 없다.
  • 예제: 숫자 변환 시험
//수 비교하는 메소드
public class UtilN {
	public static <T extends Number> int compare(T t1, T t2) {
		double v1 = t1.doubleValue();
		double v2 = t2.doubleValue();
		//Number 하위 Double 클래스로 변환되었으므로 다시 double 타입으로 변환함.
		return Double.compare(v1, v2);
	}
}
public class BoundedTypeParameterEx {

	public static void main(String[] args) {
		int result = UtilN.compare(10, 20);//Number타입만 가능함
		System.out.println(result);//int -> Integer(자동 Boxing, Number의 하위 클래스로 변환)
		
		int result1 = UtilN.compare(10.2,3.1);
		System.out.println(result1);//double => Double(자동 Boxing) 
	}
}

와일드카드 타입(<?>, <? extends ...>, <? super>)

코드에서 ?를 일반적으로 와일드카드(wildcard)라고 부른다. 제네릭 타입을 매개값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 와일드카드를 다음과 같이 세 가지 형태로 사용할 수 있다.

  • 제네릭타입<?> : Unbounded Wildcards(제한없음), 타입파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
  • 제네릭타입<? extends 상위타입> : Upper Bounded Wildcards(상위 클래스 제한), 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 하위 타입만 올 수 있다.(상위타입-> 하위 타입 가능)
  • 제네릭타입<? super 하위타입> : Lower Bounded Wildcards(하위 클래스 제한), 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 상위타입이 올 수 있다.(하위 타입 -> 상위 타입 가능)
  • 예제 : 수강신청
//Person, 최상위 클래스
public class Person {
	String name;
	public Person(String name) {
		this.name = name;
	};
	public String getName() {return name;}
	
	//이부분이 있어야 registerCourse의 출력값이 정상적으로 나옴. toString이 원래는 뭐였을까?
    //검색->
    //Object"클래스가 가진 메소드 중 "toString"메소드가 있습니다.
	//물론 "Object" 클래스의 모든 메소드는 모든 클래스가 사용이 가능합니다.
	//"toString" 메서드는 객체가 가지고 있는 정보나 값들을 문자열로 만들어 리턴하는 메소드 입니다.
    //그래서 이것을 의미 있는 값으로 재정의하여 사용한다!
	//출처: https://backback.tistory.com/68 [Back Ground]
	@Override
	public String toString() {
		return name;
	}
}
//Person 하위 클래스
public class Student extends Person{
	String student;
	public Student(String student) {
		super(student);
	};
}
//Student 하위 클래스
public class HighStudent extends Student{
	public HighStudent(String highStudent) {
		super(highStudent);
	}
}
//Person 하위 클래스
public class Worker extends Person{
	public Worker(String worker) {
		super(worker);
	};
}
//제네릭 타입
public class Course <T>{
	private String name;
	private T [] students;
	
	public Course(String name, int capacity) {
		this.name = name;
		//타입 파라미터로 배열 생성하기
		students = (T[]) (new Object[capacity]);
	}
	
	public String getName() {return name;}
    //T[] 리턴 타입 메소드
	public T[] getStudents() {return students;}
    
	public void add(T t) {//빈 배열 인덱스에 수강생 추가하는 메소드
		for(int i=0; i<students.length; i++) {
			if(students[i]==null) {
				students[i]=t;
				break;
			}
		}
	}
}
//수강생 등록
import java.util.Arrays;
public class WildCardEx {

	//double[] values = {1.0, 1.1, 1.2};
	//System.out.println(values.toString()); // 이렇게 하면 [D@46a49e6 같은 값이 나옵니다.
	//System.out.println(Arrays.toString(values)); // 이렇게 하면 [1.0, 1.1, 1.2] 이 출력됩니다.
	//출처: https://crmn.tistory.com/61 [크롬망간이 글 쓰는 공간]
    //
    public static void registerCourse( Course<?> course) {
		System.out.println(course.getName()+" 수강생: "+Arrays.toString(course.getStudents()));
	}
	public static void registerCourseStudent(Course<? extends Student> course) {
		System.out.println(course.getName()+ "수강생: "+ Arrays.toString(course.getStudents()));
	}
	public static void registerCourseWorker(Course<? super Worker> course) {
		System.out.println(course.getName()+ "수강생: "+ Arrays.toString(course.getStudents()));
	}
	
	public static void main(String[] args) {
        //수강별 등록
		Course<Person> personCourse = new Course<Person>("일반인 과정", 5);
			personCourse.add(new Person("일반인"));
			personCourse.add(new Worker("직장인"));
			personCourse.add(new Student("학생"));
			personCourse.add(new HighStudent("고등학생"));
		Course<Worker> workerCourse = new Course<Worker>("직장인 과정", 5);
			workerCourse.add(new Worker("직장인"));
		Course<Student> studentCourse = new Course<Student>("학생 과정", 5);
			studentCourse.add(new Student("학생"));
			studentCourse.add(new Student("고등학생"));
		Course<HighStudent> highStudentCourse = new Course<HighStudent>("고등학생 과정", 5);
			highStudentCourse.add(new HighStudent("고등학생"));
		//강의별 와일드카드 범위 따라 출력, 범위 안 맞으면 출력 안됨
		registerCourse(personCourse);
		registerCourse(studentCourse);
		registerCourse(highStudentCourse);
		registerCourse(workerCourse);
		System.out.println();
		
//		registerCourseStudent(personCourse); 
		registerCourseStudent(studentCourse);
		registerCourseStudent(highStudentCourse);
//		registerCourseStudent(workerCourse);
		System.out.println();
		
		registerCourseWorker(personCourse);
//		registerCourseWorker(studentCourse);
//		registerCourseWorker(highStudentCourse);
		registerCourseWorker(workerCourse);
		System.out.println();
		
		
	}

}

제네릭 타입의 상속과 구현

  • 제네릭도 상속이 가능하다. 또한 자식 제네렉 타입은 추가적으로 타입 파라미터를 가질 수 있다.
public class Child<T, M, C> extends Parent<T,M>{...}
  • 예제 : 제네릭 타입의 상속과 구현
//부모 제네릭 클래스
public class Product <T,M>{
	private T kind;
	private M model;
	
	public T getKind() { return kind;}
	public M getModel() { return model;}
	
	public void setKind(T kind) {this.kind =kind;}
	public void setModel(M model) {this.model=model;}
}
	
class Tv{}
//자식 제네릭 클래스
public class ChildProduct<T,M,C> extends Product<T,M>{
	private C company;
	
	public void setCompany(C company) {this.company=company;}
	public C getCompany() {return this.company;}
}
//제네릭 인터페이스
public interface Storage<T> {
	public void add(T item, int index);
	public T get(int index);
}
//제네릭 구현 클래스
public class StorageImpl<T> implements Storage<T>{
	private T[] array;
	
	public StorageImpl(int capacity) {
		this.array = (T[])(new Object[capacity]);
	}
	@Override
	public void add(T item, int index) {
		array[index] = item;
	}
	
	@Override
	public T get(int index) {
		return array[index];
	}
}
//제네릭 타입 사용 클래스
public class ChildEx {

	public static void main(String[] args) {
		ChildProduct<Tv,String,String> product = new ChildProduct<>();
		product.setKind(new Tv());
		product.setModel("Smart TV");
		product.setCompany("Samsung");
		
		Storage<Tv> storage = new StorageImpl<Tv>(100);
		storage.add(new Tv(), 0);
		Tv tv = storage.get(0);

	}

}

확인문제 풀이

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

public class ChildPair<K,V> extends Pair {
    public ChildPair(K key, V value) {
        super(key, value);
    }
}

public class OtherPair<K, V> {
    private K key;
    private V value;

    public OtherPair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

public class Util {
    public static <K,V extends Pair> Integer getValue(Pair<String, Integer> pair, String key) {
        if(pair.getKey().equals(key)) return pair.getValue();
        else return null;
    }
}

public class UtilExample {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("홍길동", 35);
        Integer age = Util.getValue(pair, "홍길동");
        System.out.println(age);

        ChildPair<String, Integer> childPair = new ChildPair<>("홍삼원", 20);
        Integer childAge = Util.getValue(childPair, "홍삼순");
        System.out.println(childAge);

        /*
        OtherPair<String, Integer> otherPair = new OtherPair<>("홍삼원", 20);
        //OtherPair는 Pair를 상속하지 않으므로 예외가 발생해야 한다.
        int otherAge = Util.getValue(otherPair, "홍삼원");
        System.out.println(otherAge);
         */
    }
}
profile
라면 먹고 싶다. 두 개 끓여서 혼자 먹고 싶다. - 임덕배 (1997. 06 ~ )

0개의 댓글