StringTokenizer

StringTokenizer 클래스는 문자열을 우리가 지정한 구분자로 문자열을 쪼개주는 클래스입니다. 그렇게 쪼개어진 문자열을 우리는 토큰(token)이라고 부릅니다.

 

StringTokenizer를 사용하기 위해서는 java.util.StringTokenizer를 import해야합니다. 사용법은 굉장히 쉽습니다. 사용하는 메소드도 몇개 없는데요. 자주 사용하는 메소드 설명과 예제를 통해 이 클래스를 어떻게 사용하는지 살펴봅시다. 

 

생성자(Constructor)

생성자 설명
public StringTokenizer(String str);

절달된 매개변수 str을 기본(default) delim으로 분리합니다. 기본 delimiter는 공백 문자들인 " \t\n\r\t"입니다. 

public StringTokenizer(String str,String delim); 특정 delim으로 문자열을 분리합니다.

public StringTokenizer(String str,String delim,boolean returnDelims);

str을 특정 delim으로 분리시키는데 그 delim까지 token으로 포함할지를 결정합니다. 그 매개변수가 returnDelims로 true일시 포함, false일땐 포함하지 않습니다.

 

int countTokens() 

남아있는 token의 개수를 반환합니다. 전체 token의 갯수가 아닌 현재 남아있는 token 개수입니다.

 

boolean hasMoreElements(), boolean hasMoreTokens()

다음의 token을 반환합니다. StringTokenizer는 내부적으로 어떤 위치의 토큰을 사용하였는지 기억하고 있고 그 위치를 다음으로 옮깁니다. 두가지 메소드는 모두 같은 값을 반환합니다.

 

Object nextElement(), String nextToken()

이 두가지 메소드는 다음의 토큰을 반환합니다. 두가지 메소드는 같은 객체를 반환하는데 반환형은 다르네요. nextElement는 Object를, nextToken은 String을 반환하고 있습니다.

 

예제

이제 몇가지 예제를 통해서 더 자세히 알아보도록 합시다.

 

0) String 클래스에 있는 split 메소드 이용

public static void main(String[] ar){
	String str="this string includes default delims";
	System.out.println(str);
	System.out.println();
		
	System.out.println("==========using split method============");
	String []tokens=str.split(" ");
		
	for(int i=0;i<tokens.length;i++){
		System.out.println(tokens[i]);
	}
}

String클래스의 메소드인 split 메소드를 사용하여 StringTokenizer를 흉내낼 수 있습니다. split이 반환하는 값은 String 배열입니다.

this string includes default delims

==========using split method============
this
string
includes
default
delims

 

1) Default Delim을 이용

public static void main(String[] ar){
	String str="this string\tincludes\ndefault delims";
	StringTokenizer stk=new StringTokenizer(str);
	System.out.println(str);
	System.out.println();
		
	System.out.println("total tokens:"+stk.countTokens());
	System.out.println("================tokens==================");
	while(stk.hasMoreTokens()){
		System.out.println(stk.nextToken());
	}
	System.out.println("total tokens:"+stk.countTokens());
}

코드의 while문을 보면 토큰이 있는지 확인한 후 있다면 다음 토큰을 가져옵니다. 이렇게 하나씩 토큰을 소비한다고 보면되는데, 이런 패턴이 StringTokenizer를 사용하는 가장 일반적인 사용방법입니다.

실행결과

this string includes
default delims

total tokens:5
================tokens==================
this
string
includes
default
delims
total tokens:0

 

 

 

 

2) 특정 delim을 이용

public static void main(String[] ar){
	String str="this-=string-includes=delims";
	StringTokenizer stk=new StringTokenizer(str,"-=");
	System.out.println(str);
	System.out.println();
		
	System.out.println("total tokens:"+stk.countTokens());
	System.out.println("================tokens==================");
	while(stk.hasMoreTokens()){
		System.out.println(stk.nextToken());
	}
	System.out.println("total tokens:"+stk.countTokens());
}

특정 delim으로 문자열을 분리하는 예제입니다. 여기서는 "-"와 "="으로 분리를 했네요.

실행결과

this-=string-includes=delims

total tokens:4
================tokens==================
this
string
includes
delims
total tokens:0

2-1) String의 split과 비교

public static void main(String[] ar){
	String str="this-=string-includes=delims";
	System.out.println(str);
	System.out.println();
		
	String[] tokens=str.split("-=");
	System.out.println("total tokens:"+tokens.length);
	System.out.println("================tokens==================");
		
	for(int i=0;i<tokens.length;i++){
		System.out.println(tokens[i]);
	}
		
}

split을 이용하면 조금 다른 결과가 나옵니다. split은 정확히 "-="으로 문자를 쪼개기 때문에 "this-=string-includes=delims"에서 빨간 부분을 기준으로 쪼개는 겁니다. 결과를 확인해보세요.

실행결과

this-=string-includes=delims

total tokens:2
================tokens==================
this
string-includes=delims

3) delim까지 포함

public static void main(String[] ar){
	String str="this-string-includes=delims";
	StringTokenizer stk=new StringTokenizer(str,"-=",true);
	System.out.println(str);
	System.out.println();
		
	System.out.println("total tokens:"+stk.countTokens());
	System.out.println("================tokens==================");
	while(stk.hasMoreTokens()){
		System.out.println(stk.nextToken());
	}
	System.out.println("total tokens:"+stk.countTokens());
}

 위의 예제의 생성자에서 세번째 인자를 true로 전달했을때의 예제입니다. 이때 "-"와 "="를 토큰으로 포함하게 됩니다. 이 예제에서 true를 전달하지 않고 false로 전달한다면 위의 예제와 같은 결과가 나오게 됩니다.

실행결과

this-string-includes=delims

total tokens:7
================tokens==================
this
-
string
-
includes
=
delims
total tokens:0

 

이렇게 StringTokenizer의 사용방법을 알아보았습니다. 알아두면 문자열을 조금 더 유연하게 다룰 수 있겠네요.

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

동기화(Synchronization)

동기화는 특정 객체를 동시에 접근하여 변경하기를 막는 기법입니다. 가장 빈번하게 사용되는 예를 들어볼까요? 은행 입출금과 화장실의 예가 있는데 저는 화장실이 더 좋더군요.

 

우리가 고깃집에서 소주와 맥주를 거나하게 마셔서 화장실을 갈때 열쇠가 있지요? 여기서 화장실은 한개이고 열쇠 또한 한개라고 칩시다. 열쇠를 갖고 화장실에서 볼일을 보려고 문을 잠글겁니다. 문을 잠그고 나서는 각자 취향대로 볼일을 보고 물을 내리고 열쇠를 제자리에 걸어놓습니다.  화장실의 키를 갖고 열고 잠그는 것이 일종의 동기화입니다.

여러분들은 알게 모르게 동기화를 실천하고 있는 것이지요.

 

만약 이 과정에서 동기화가 빠지게 된다면 어떻게 될까요? 제가 화장실을 쓰고 있는데, 다른 사람이 와서 제 무릎에다가 지리는 현상이 발생하겠죠. 

 

이렇듯 멀티쓰레드(Multithread) 환경에서 어떤 쓰레드(사람)가 자원(화장실 열쇠)을 이용하는데 동시에 그 자원을 사용하면 안될때 동기화가 필요합니다.

 

자바에서 이러한 동기화 기법을 적용하는 방법은 두가지가 있습니다.

첫번째로는 특정 객체의 메소드에 synchronized 예약어를 사용하는 방법과 synchronized lock을 사용하는 방법입니다.

 

synchronized 사용하여 동기화

우선 동기화를 걸지 않은 코드를 먼저 봅시다. key라는 객체는 원래 HAS-A 관계가 적절하지만 이해를 돕기위해서 그런건 고려하지 않았습니다. 코드는 그렇게 어렵지 않습니다. 

쓰레드 3개를 생성하고 각각 알기쉽게 사람 이름을 붙여줍니다. 이 쓰레드는 이제 사람이라고 치고 key를 사용하여 볼일을 본다면 어떻게 될까요?

 

 

 

 

class Key {
	
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key(); //이 객체를 쓰레드 3개에서 사용
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

아래 결과처럼 철수가 싸는 중에 영희가 철수 무릎위에 볼일을 보고, 그 다음 영철이가 들어와서 또 한번 철수 무릎에 볼일을 봅니다. 그 다음 철수가 볼일을 보네요.

철수이(가) 화장실 문을 연다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.

 

철수를 위해 동기화를 할 필요가 있겠죠? 가장 간단한 방법은 동기화할 메소드에 synchronized 만 추가하면 됩니다. 

useKey메소드를 다음과 같이 변경합시다.

public synchronized void useKey(String name){
	open(name);
	defecate(name);
	close(name);
}

그리고 실행합시다.

철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

저희는 이렇게 철수를 도울 수 있습니다.

 

Synchronized Block으로 원하는 코드부분만 동기화

Synchronized로 동기화를 거는 건 무척 쉽네요. 하지만 그 메소드 전부를 동기화하기 때문에 불필요한 곳까지 동시에 실행할 수가 없습니다.

다음의 예를 보세요. 위의 코드에서 몇줄밖에 추가하지 않았습니다.

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		lookIntoAMirror(name);
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울을 보는것까지 동기화를 걸었네요. 우리는 그럴 필요없습니다. 거울은 누가 먼저보든 상관없으니까요. 볼일 보는 것만 정확히 동기화를 하려고 한다면 block을 이용해야합니다.

 

 

 

 

아래와 같이 원하는 부분만을 동기화할 수 있습니다. 

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public void useToilet(String name){
		lookIntoAMirror(name);
		synchronized(this){
			open(name);
			defecate(name);
			close(name);
		}
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울은 순서없이 먼저 들어온 사람이 봅니다.

철수이(가) 거울을 본다.
영희이(가) 거울을 본다.
철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
영철이(가) 거울을 본다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

 

synchronized block의 인자는 사실 잠글(lock 시킬) 객체를 말합니다. this이니까 바로 key객체 자신이겠죠.

lock 시킬 객체를 한번 바꿔서 실행하도록 하지요.

 

1) 아래는 this(Counter객체)를 통해서 lock을 시켰습니다. 

class Counter{
	public Integer cnt=0;
	
	public void increase(String threadName){
		synchronized(this){ 
			for(int i=0;i<10;i++)
				System.out.println(threadName+", cnt:"+(cnt++));
			System.out.println();
		}
	}
	
}
class MyThread extends Thread{
	private String threadName;
	private Counter counter;
	public MyThread(String threadName,Counter counter){
		this.threadName=threadName;
		this.counter=counter;
	}
	public void run(){
		counter.increase(threadName);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Counter counter=new Counter();
		MyThread thread1=new MyThread("thread1",counter);
		MyThread thread2=new MyThread("thread2",counter);
		
		thread1.start();
		thread2.start();
		
		thread1.join();
		thread2.join();
		System.out.println(counter.cnt);
	}
	
}

 

 

2) 다음의 코드는 cnt객체로 lock을 시켰습니다.

public void increase(String threadName){
  synchronized(cnt){
  	for(int i=0;i<10;i++)
  		System.out.println(threadName+", cnt:"+(cnt++));
  		System.out.println();
	}
}

 

3) 마지막으로 동기화를 걸지 않은 코드입니다.

public void increase(String threadName){
	
	for(int i=0;i<10;i++)
		System.out.println(threadName+", cnt:"+(cnt++));
	System.out.println();
	
}

 

1)의 결과는 thread1의 increase, thread2의 increase 둘 중 먼저 실행되는 것이 끝난 이후에 다른 쓰레드의 메소드가 실행되며 마지막 결과도 20으로 항상 동일합니다. this로 lock을 걸었기 때문에 다른 쓰레드의 increase가 끝난 이후에 다른 쓰레드의 increase가 실행됩니다. 

 

2)의 결과는 thread1의 increase, thread2의 increase 둘 중 무작위로 실행되며 실행이 끝난 이후 마지막 결과가 20으로 항상 동일합니다. 이 경우 cnt로 lock을 걸었기 때문에 cnt가 쓰이지 않는 순간(cnt++가 되지 않는 찰나의 순간) 다른 쓰레드가 cnt++을 실행할 수 있습니다.

 

3)의 결과는 동기화를 걸지 않았기 때문에 결과가 매 수행마다 다를 수 있습니다.

 

보통은 1)의 방법으로 동기화를 많이 구현하게 됩니다.

 

각각의 결과를 비교해보시기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

쓰레드(Thread)

쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레드를 여러개 사용하면 동시에 여러 작업을 할 수 있는 뜻이 되는 것이죠. 

우리가 익히 알고 있는 main 함수 역시 쓰레드입니다. 프로그램 실행 시 실행되는 첫번째 쓰레드이기 때문에 main함수를 우리는 메인 쓰레드라고도 합니다.

 

쓰레드를 사용해서 얻을 수 있는 이점이 있을 텐데 어떤 이점들이 있을까요?

 

  • 우선 가장 두드러진 장점은 바로 동시성입니다. 동시에 여러 일들을 할 수 있습니다. 그렇기 때문에 작업의 효율성을 높일 수 있습니다.
  • 쓰레드끼리 메모리를 공유합니다. 그렇기 때문에 메모리가 절약되는 효과를 볼 수 있습니다. 경제적인 것이죠.

그렇다고 해서 장점만 있는 것은 아닙니다. 아래와 같은 상황을 고려해야하죠.

 

  • 프로그램의 실행 단위가 많아지면 프로그램이 상당히 복잡해질 수 있습니다.
  • 예상치 못한 버그가 생길 수 있습니다. 이를 위해서 적절히 동기화를 걸어주어야 하지요.
  • 교착상태나 기아상태로 빠질 수 있습니다.

 

 

 

자바 쓰레드 사용

자바에서는 대표적인 두 가지 쓰레드를 생성하는 방법이 있는데요. 아래의 두 가지 방법을 활용하여 쓰레드를 생성해봅시다.

 

1. Thread 클래스를 상속받는 방법

2. Runnable 인터페이스를 구현하는 방법

 

1. Thread 클래스를 상속받는 방법

아래의 코드처럼 Thread 클래스를 상속합니다.

실행되는 각 쓰레드를 구별 짓기 위해서 쓰레드 이름을 저장하고 단지 for문에서 100까지 도는 아주 간단한 쓰레드입니다. 주의 해야할 것은 쓰레드의 실제 override하는 메소드는 run이지만, 쓰레드 호출 시 실행하는 메소드는 start라는 점을 주의하시기 바랍니다. 

class MyThread extends Thread{
	
	public MyThread(String threadName){
		super(threadName);
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(this.getName()+":"+i);
		}
		System.out.println();
	}
}
public class ThreadTest {

	public static void main(String[] ar){
		System.out.println("MainThread Start");
		for(int i=1;i<=3;i++){
			new MyThread("Thread"+i).start();
		}
		System.out.println("MainThread End");	
	}
	
}

 

위의 코드에서 프로그램을 예상할 수 있나요? 평소의 프로그램(싱글쓰레드)같았다면 우리는 이렇게 예상을 했을 겁니다. 

MainThread Start

Thread1:0

Thread1:1

Thread1:2

Thread1:3

...

Thread2:0

Thread2:1

Thread3:2

...

Thread3:98

Thread3:99

MainThread End

하지만 우리는 동시성을 원하니까 쓰레드를 돌렸지요. 그래서 위의 결과가 될 수도 있고(진짜 운이 드럽게 좋다거나 없다면) 아닐 수도 있습니다. 

대부분의 결과는 아래와 같이 제 얼굴처럼 뒤죽박죽된 결과가 발생합니다. 아래 결과는 실제 제 똥컴에서 돌린 결과입니다.

MainThread Start 
MainThread End 
Thread1:0 
Thread1:1 
Thread1:2 
Thread1:3

...

Thread1:59 
Thread1:60 
Thread2:0 
Thread2:1

...

Thread2:98 
Thread2:99 

Thread1:61 
Thread3:0 
Thread3:1

...

Thread1:97 
Thread1:98 
Thread1:99

 

쓰레드의 실행 순서는 이렇게 예측할 수 없습니다.

 

 

 

2. Runnable 인터페이스 구현

실제로 쓰레드를 생성할때 많이 사용하는 방법입니다. Runnable 인터페이스를 구현한 클래스를 Thread의 생성자로 주입하여 실행하는 방법이죠. 아래의 코드를 봅시다. 

 

달라진게 있다고 한다면 implements Runnable 쪽인것 같네요. 이렇게 구현한 MyThread라는 클래스를 객체화하여 main 메소드에서 실행시키고 있습니다.

Thread라는 클래스의 생성자로 Runnable 객체를 전달하고 있습니다.

Thread thread=new Thread(new MyThread("Thread"+i));
thread.start();

 

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar){
		System.out.println("MainThread Start");
		for(int i=1;i<=3;i++){
			Thread thread=new Thread(new MyThread("Thread"+i));
			thread.start();
		}
		System.out.println("MainThread End");	
	}
	
}

 

결과는 1번의 Thread 클래스를 상속받는 방법과 비슷하게 순서를 예측할 수 없이 실행됩니다.

MainThread Start 
MainThread End 
Thread2:0

...

Thread1:62 
Thread2:93 
Thread2:94

...

Thread3:97 
Thread3:98 
Thread3:99

 

저는 항상 Main 쓰레드가 먼저 끝나버리는군요. 

이점에 대해서 불만을 가지고 있는데요. 메인 쓰레드는 항상 다른 쓰레드를 기다렸다가 종료할 수 없을까요?? 자기 할일 끝났다고 먼저 가버리는 것은 조금 매너가 없으니까요. 

 

그래서 join이라는 쓰레드 메소드가 존재합니다.

 

join

join이라는 메소드를 통해서 분기를 어떤 지점에 합칠 수 있습니다. 그러니까 쓰레드를 생성한 쓰레드는 그 지점에서 기다려야합니다. 아래의 코드를 통해서 알아보도록 합시다.

 

알아보려면 코드를 조금 변경해야하는데, 우선 join을 쓰게 되면 InterruptedException이 발생합니다. 처리하기 귀찮으니 throws를 통해서 그냥 던져줍시다. 누군가 알아서 먹든가 하겠죠.

아래 코드처럼 변경해서 실행하면 메인쓰레드는 자신이 생성한 3개의 쓰레드를 끝날때까지 기다렸다가 자신도 종료하게 됩니다.

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar) throws InterruptedException{
		System.out.println("MainThread Start");
		Thread[] thread=new Thread[4];
		for(int i=1;i<=3;i++){
			thread[i]=new Thread(new MyThread("Thread"+i));
			thread[i].start();
		}
		
		for(int i=1;i<=3;i++)
			thread[i].join();
		
		System.out.println("MainThread End");	
	}
	
}

 아래의 실행결과처럼 메인쓰레드는 나머지 3개가 종료할때까지 기다립니다.

MainThread Start 
Thread1:0 
Thread1:1

...

Thread3:99

MainThread End

왜 join으로 이름을 정했을까요? 아래의 그림을 보면 이해하기가 쉬울 겁니다. 위 코드의 상황을 그림으로 옮겨놓았습니다.

main은 thread1, thread2, thread3을 수행시킵니다. 이때 thread1, thread2, thread3이 끝나면 다시 자신을 실행시키는 main와 합쳐지지요. 이해하기 쉽죠?

 

 

아 이것도 불만이다. 난 쓰레드를 썼지만 순차적으로 실행하겠다! thread1끝나면 thread2실행하고 thread2끝나면 thread3실행하겠다 하시는 분들은 thread 실행시키자 마자 바로 join거시면 됩니다. 

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar) throws InterruptedException{
		System.out.println("MainThread Start");
		Thread[] thread=new Thread[4];
		for(int i=1;i<=3;i++){
			thread[i]=new Thread(new MyThread("Thread"+i));
			thread[i].start();
			thread[i].join();
		}
		
		
		System.out.println("MainThread End");	
	}
	
}

 

아래의 그림에서 점선이 join하는 구간입니다.

 

실행결과는 뭐.. 순차적입니다.

MainThread Start 
Thread1:0 
Thread1:1

...

Thread3:99

MainThread End

 

 

근데 이렇게 구현할 거면 쓰레드 안쓰는 것이 낫죠. 단지 join을 어떻게 사용하는지 보여드린겁니다.

 

간단하게 자바에서 쓰레드를 사용하는 방법을 알아보았습니다. 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

HashSet

자바 Collection 중 Set의 대표적인 HashSet 클래스를 다루어 보도록 하겠습니다. HashSet은 Set의 파생클래스로 Set은 기본적으로 집합으로 중복된 원소를 허용하지 않습니다. HashSet은 순서 역시 고려가 되지 않습니다. 그렇다면 다음의 예제로 HashSet의 기본적인 동작을 살펴보도록 하겠습니다.

 

public static void main(String[] args){
	Set hashSet=new HashSet();
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* 위와 같은 데이터들을 다시 add */
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* HasSet의 "C"라는 원소 삭제 */
	hashSet.remove("C");
	
	/* HashSet 모든 원소 출력 */
	System.out.println("HashSet 원소 출력");
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		System.out.print(it.next()+" ");
	}
	
	/* HashSet의 모든 원소를 ArrayList로 전달 */
	List arrayList=new ArrayList();
	arrayList.addAll(hashSet);
	
	/* ArrayList의 모든 원소 출력 */
	System.out.println();
	System.out.println("ArrayList 원소 출력");
	for(int i=0;i<arrayList.size();i++){
		System.out.print(arrayList.get(i)+" ");
	}
}
 

 

우선 HashSet에 "F", "B", "D", "A", "C"라는 문자열을 차례대로 넣었습니다. 그리고 나서 다시 같은 데이터들을 넣게 되죠.  Set에는 중복을 허용하지 않는다고 하였으니 Set에는 현재 { "F", "B", "D", "A", "C" }가 존재합니다.

 

그 후에 "C"라는 문자열을 지우는 군요. 뭐 쉽습니다. "C"라는 원소를 삭제했으니 { "F", "B", "D", "A"}만이 남는것을 알 수 있겠군요. HashSet을 출력하는 부분을 보고 확인해보세요.

아, HashSet에는 순서가 없으므로 Iterator를 사용해서 집합안의 원소를 출력하고 있습니다. Iterator의 메소드는 3개가 다인데요.

hasNext() : 다음 원소가 남아있는지 여부를 알아냅니다. 다음 원소가 남았다면 true를 반환합니다.

next() : 다음 원소를 가져옵니다. 

remove() : 현재 반복자가 가리키고 있는 원소를 삭제합니다.

 

장난삼아서 ArrayList에 HashSet의 원소를 모두 전달해서 확인해봤습니다. 아래는 그 결과입니다.

HashSet 원소 출력
A B D F 
ArrayList 원소 출력
A B D F 

 

이제 우리는 단순히 String 타입의 자료형이 아닌 우리가 정의한 클래스의 객체를 HashSet의 원소에 넣고 싶습니다. 그렇다면 어떻게 중복된 원소인지 아닌지를 확인할 수 있을까요?

위에서 String 객체를 HashSet은 어떻게 중복된 원소라고 감지했을까요? HashSet이 알아서 판단해 줄까요?

 

우리가 다음과 같은 Person 클래스가 있다고 합시다.

 

class Person{
	String name; //이름
	int residentNumber; //주민번호
}

 

사람이 같은지 여부를 판별하는 것은 주민등록번호를 보고 알 수 있지요? 허나 Set에 단순히 이 Person 클래스를 넘겨준다면 중복 여부를 판별하지 못할 겁니다. Set이 Person 객체의 어떤 메소드를 호출해서 같은지 말지를 판변할 수 있으니까요. 그 메소드가 바로 equals와 hashCode입니다.

 

1. equals(Object o)

만약 두 객체(현재 이 객체와 인자로 넘어온 객체 o)가 같다는 것을 알려주려면 equals 메소드에서 true를 반환해주어야합니다.

 

2. hashCode()

만약 equals에서 두 객체가 같다라고 true를 반환했다면 hashCode는 두 객체에서 항상 같은 값을 반환해야합니다. 만일 equals에서 false를 반환하여 같지 않다고 반환했다면 hashCode 역시 다르게 반환하는 것이 좋습니다.

 

위의 조건을 고려해서 구현한 Person 클래스를 다시 봅시다.

class Person{
	String name; //이름
	int residentNumber; //주민번호
	
	public Person(String name,int residentNumber){
		this.name=name;
		this.residentNumber=residentNumber;
	}
	@Override
	public int hashCode(){
		/* Objects.hash 메소드로 residentNumber의 해쉬값 반환 */
		return Objects.hash(residentNumber);
	}
	
	@Override
	public boolean equals(Object o){
		/* 주민 번호가 같은 Person은 true 반환 */
		Person p=(Person)o;
		return p.residentNumber==this.residentNumber;
	}
}

 

equals와 hashCode를 Override한 코드를 확인해봅시다.

equals에서는 단순히 residentNumber만 비교해서 같다면 true, 다르다면 false입니다.

hashCode에서는 Objects.hash 메소드로 residentNumber의 해쉬값을 반환합니다. 이 hash는 residentNumber가 다르다면 항상 다른 값이 반환됩니다.

 

 

이제 Set에 원소를 넣고 확인해보도록 하지요.

 

public static void main(String[] args){
	
	Set hashSet=new HashSet();
	hashSet.add(new Person("reakwon",111111));
	hashSet.add(new Person("KDC",222222));
	hashSet.add(new Person("KSG",333333));
	hashSet.add(new Person("reakwon",111112));
	hashSet.add(new Person("MJW",111111));
	
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		Person p=it.next();
		System.out.println(p.name+"/"+p.residentNumber);
	}
}

 

동명이인이 있군요. 주민 번호 111111인 reakwon과 주민 번호 111112인 reakwon인 객체들이네요. 우리는 이름이 같은것은 같은 객체로 보지 않습니다. 주민 번호가 같은 객체만 중복된 원소로 보는 것이죠.

그래서 이 두 객체는 집합에 추가가 됩니다.

 

하지만 이름이 주민번호가 111111의 "MJW"라는 객체와 주민번호 111111의 "reakwon"라는 객체는 같습니다. 주민번호가 같기때문입니다. 그래서 "MJW"라는 Person객체는 집합이 추가하지 않습니다.

 

그 집합의 모든 원소를 출력한 결과는 아래와 같습니다.

KSG/333333 
reakwon/111112 
reakwon/111111 
KDC/222222

 

이제 우리가 만든 객체들도 HashSet에 추가할 수 있습니다.

 

HashSet을 알아보았으니 다음은 HashMap에 대해서 알아볼 차례겠군요. 아래의 링크로 들어가서 개념과 사용법을 익혀보도록 합시다.

reakwon.tistory.com/151

 

[자바] 해시맵(HashMap)의 개념과 사용 예, 기본 메소드와 같은 키인지 판별하는 방법

해시맵(HashMap) 해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서

reakwon.tistory.com

반응형
블로그 이미지

REAKWON

와나진짜

,

컬렉션(Collection)

이름에서도 알 수 있듯이 자료들을 효율적으로 모을 수 있는 자료구조를 의미합니다. Collection의 상속구조는 아래의 그림과 같습니다.

 

주황색 상자는 인터페이스, 파란색 상자는 클래스를 의미하며 파란색 화살표는 extends, 녹색 화살표는 implements의 관계를 나타냅니다.

 

Collection이라는 인터페이스는 Collection 계층의 최상위 인터페이스입니다. Iterable이라는 인터페이스를 상속했다는 것을 알 수 있고, Map은 Collection 인터페이스를 상속하지는 않지만 자바의 JCF(Java Collections Framework)에 포함됩니다. 

 

 

Collection 인터페이스의 메소드들을 간단히 살펴보도록 하겠습니다. 별로 어려운 메소드는 없을 것 같습니다.

boolean add(E e)

요소를 추가합니다.

boolean addAll(Collection<? extends E> c)

Collection 타입의 매개변수에 있는 모든 요소를 추가합니다.

void clear()

Collection의 모든 요소를 지웁니다.

boolean contains(Object o)

인자 o가 이 Collection에 속한다면 true를 반환하고 없다면 false를 반환합니다.

boolean containsAll(Collection<?> c)

Collection c의 원소들이 이 Collection에 모두 존재한다면 true, 그렇지 않으면 false를 반환합니다.

boolean equals(Object o)

o와 같은 객체인지 아닌지 확인합니다.

int hashCode()

이 Collection의 해쉬코드를 반환합니다.

boolean isEmpty()

이 Collection이 비어있는지 확인합니다.

Iterator<E> iterator()

이 Collection의 반복자(Iterator)를 반환합니다. Set같은 Collection은 순서를 고려하지 않기때문에 iterator로 원소를 순회합니다. Iterator의 대표적인 메소드는 hasNext와 next가 있습니다.

boolean remove(Object o)

o와 같은 원소가 이 Collection에 존재한다면 삭제합니다.

boolean removeAll(Collection<?> c)

이 Collection에 Collection c를 모두 제거합니다.

boolean retainAll(Collection<?> c)

매개변수 c의 요소들만을 남겨둡니다.

int size()

원소의 사이즈를 반환합니다.

Object[] toArray()

이 Collection의 모든 요소를 포함하는 배열로 반환합니다.

<T> T[] toArray(T[] a)

지정한 타입의 배열로 변환합니다.

 

 

이제 Collection을 상속받는 List, Set, Queue와 Map에 관한 간단한 설명을 시작하도록 하겠습니다. 한 번더 이야기하자면 아래의 Collection은 전부 인터페이스로 스스로 객체생성이 불가능하다는 것을 알아두세요.

1. List

순서가 중요한 Collection입니다. 순서가 있는 데이터의 집합으로 데이터의 중복을 허용합니다. 기존의 배열과는 다르게 크기가 동적으로 변합니다.

 

파생클래스

Vector, ArrayList : 두 클래스는 같이 설명하겠습니다. 우선 기본적인 동작은 원소를 추가, 삭제하는 것은 둘 다 비슷합니다. 하지만 Vector는 동기화 처리가되어있습니다. 즉, 하나의 스레드만이 Vector에 요소를 추가하거나 삭제가 가능하지요. 스레드에 대해서 안전하지만 느리다는 단점이 있습니다. 하지만 ArrayList는 동기화가 되어있지 않습니다. 그렇기 스레드에 대해 안전하지 않습니다. 하지만 추가, 삭제가 빠르다는 장점이 있습니다.

단일쓰레드 환경에서 개발할때에는 ArrayList를 쓰는것이 바람직하다고 할 수 있습니다.

LinkedList : 자료구조를 배웠다면 이해가 수월할 것인데, 간단히 말하면 각각의 요소는 다음 요소를 가리키고 있어 추가, 삭제 연산이 빠릅니다.

 

 

2. Set

순서가 없는 데이터의 집합으로 데이터의 중복을 허용하지 않습니다. 우리가 중딩시절에 배운 집합을 떠올리면 될텐데요. 참고로 저는 집합이 나오고 수학을 포기했습니다.

 

파생클래스

HashSet : 내부적으로 해싱을 이용해서 구현된 클래스입니다. Set 파생클래스에서 가장 성능이 우수합니다.

TreeSet : 내부적으로 레드-블랙 트리 방식으로 구현된 클래스입니다. 레드- 블랙 트리는 이진 탐색 트리의 일종으로 log(n)의 속도로 삽입, 삭제, 검색이 가능합니다. HashSet보다는 성능이 느립니다.

 

3. Queue

아마 너무나도 잘 알고 있을 Queue는 기본적으로 선입 선출(First-In First-Out) 형식의 자료구조입니다. 먼저 들어온 원소가 먼저 나간다는 Collection입니다.

 

파생클래스

PriorityQueue : 들어온 순서가 아니라 우선순위 별로 Queue에서 원소를 꺼내옵니다. 우선순위는 Comparable 인터페이스로 정할 수 있습니다.

ArrayDeque : 보통의 큐와는 다르게 큐의 양쪽에서 원소를 꺼내올 수 있는 Collection입니다.

 

4. Map

Map은 키-데이터(Key-Value) 쌍으로 자료를 보관하고 있습니다. Map에서 순서는 고려되지 않는 편이며 키는 중복을 허용할 수 없습니다.

 

파생클래스

HashMap : 키-값의 쌍으로 값을 가져올 수 있는 대표적인 Map의 Collection입니다. 동기화를 보장하지 않습니다. 키-값으로 쉽게 데이터를 검색할 수 있습니다. 단일 스레드에서 개발한다면 HashMap을 사용합시다.

또한 HashMap은 특이하게도 키 또는 값에 null을 저장할 수 있다는 점입니다.

Hashtable : HashMap과 사용법이 거의 동일한데, 다른점은 동기화를 보장한다는 것입니다. 그렇기 때문에 HashMap보다는 무겁겠죠?

또 HashMap과는 다르게 키 또는 값에 null을 사용할 수 없습니다.

SortedMap : 이름에서도 알 수 있듯이 정렬이 된 Map구조입니다. 

 

다음 포스팅에는 파생클래스를 사용하는 방법에 대해 알아보도록 하겠습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

String을 왜 인코딩하고 디코딩할까요? 인코딩과 디코딩을 해야하는 상황이 있습니다. 만일 DB가 한글을 지원하지 않는 경우 한글로 된 문자를 숫자로 encoding해서 DB에 저장하면 되고, 사용자에게 보여줄때는 다시 decoding해서 보여주면 됩니다.

또는 암호화할때 문자열을 encoding한 후에 암호화하게 됩니다.

이 밖에도 문자열을 encoding과 decoding을 해야할 상황이 있겠죠? 그러므로 문자열을 어떻게 encoding할지 decoding할지 알아보도록 하겠습니다.

 

byte[] getBytes()

byte[] getBytes(Charset charset)

byte[] getBytes(String charsetName)

문자열을 인코딩된 byte형태로 넘겨줍니다. 매개변수없이 그냥 getBytes()메소드를 사용하면 플랫폼에 따른 default charset을 사용합니다.

만일 특정 charset을 지정할 경우 인자를 받는 getBytes메소드를 사용하면 됩니다. ISO-8859-1, euc-kr, utf-8 등의 charset이 존재하는데 encoding과 decoding할때 이 chatset을 맞춰서 해야합니다. 그러지 않을 경우 문자가 깨지는 현상이 발생하게 됩니다. 

 

자, 그러면 이제 실제 프로그램을 짜면서 사용법을 알아보도록 합시다. 여기서는 가장 많이 사용하는 UTF-8로 charset을 지정했습니다.

public static void main(String[] args){
	String str="reakwon의 블로그";
	//default charset으로 인코딩된 바이트 배열	
	byte[] bytes=str.getBytes();
    //인코딩된 바이트 출력
	System.out.print("Default charset encoding: ");
	for(int i=0;i<bytes.length;i++)
		System.out.print(bytes[i]+" ");
	System.out.println();
	//default charset으로 디코딩된 문자열 출력
	String decoded=new String(bytes);
	System.out.println(decoded);
		
	System.out.println();
	try {
    	//UTF-8로 인코딩된 바이트 배열
		bytes=str.getBytes("UTF-8");
		System.out.print("UTF-8 charset encoding: ");
		for(int i=0;i<bytes.length;i++)
			System.out.print(bytes[i]+" ");
		System.out.println();
        //이 바이트 배열을 default charset으로 디코딩된 문자열 출력 : charset이 다르므로 한글이 깨짐.
		decoded=new String(bytes);
		System.out.println(decoded);
		//인코딩된 UTF-8로 디코딩되어 한글이 깨지지 않음.
		decoded=new String(bytes,"UTF-8");
		System.out.println(decoded);
			
	} catch (UnsupportedEncodingException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

 

문자열은 한글이 섞여있는 문자열입니다. 우선 default charset으로 바이트 배열을 얻어오고 출력해줍니다. 인코딩된 바이트 배열을 다시 디코딩하여 출력해줍니다. String으로 decoding하는 방법은 무척 간단합니다. 아래의 String 클래스의 생성자를 사용하면 됩니다.

 

 

String(byte[] bytes) : default charset으로 decoding한 문자열

String(byte[] bytes, String charsetName) : 특정 charset으로 decoding한 문자열

 

try...catch 안에서는 UTF-8로 인코딩하고 인코딩된 값을 출력해줍니다. 그 이후 String 인코딩 된 값을 출력해주는데요. 만약 default charset으로 디코딩한 후 문자열을 출력하면 어떻게 될까요? 예상하셨겠지만 영어를 제외한 한글을 깨져서 나옵니다.

그래서 인코딩된 방식과 같이 UTF-8로 디코딩해야합니다. 그래야만 한글이 깨지지 않지요. 그래서 바로 위의 String의 2번째 생성자를 사용해서 charset을 지정합니다. 그 후 출력하면 정상적으로 한글이 출력됩니다.

결과를 확인해보세요.

 

결과

Default charset encoding: 114 101 97 107 119 111 110 -64 -57 32 -70 -19 -73 -50 -79 -41 
reakwon의 블로그

UTF-8 charset encoding: 114 101 97 107 119 111 110 -20 -99 -104 32 -21 -72 -108 -21 -95 -100 -22 -73 -72 
reakwon?쓽 釉붾줈洹?
reakwon의 블로그

 

결과를 들여다보면 영어는 ASCII, 그리고 한글은 2바이트를 사용하는 것을 알 수 있네요.

반응형
블로그 이미지

REAKWON

와나진짜

,

자바에서 문자열을 다루기 위해서는 String 클래스의 메소드를 사용합니다. 이번 포스팅에서는 String 메소드를 설명하고 간단한 사용법울 알아보도록 하겠습니다.

 

int length()

문자열의 길이를 반환합니다. C언어로 치면 strlen과 같은 메소드겠네요. 사용법이 너무 간단하므로 예를 보이지는 않겠습니다.

 

String substring(int beginIndex)

String substring(int beginIndex, int endIndex)

substring 메소드는 beginIndex부터 문자열끝까지의 문자열을 반환합니다. 만약 끝을 가리키는 endIndex를 사용한다면 문자열의 특정 구간의 문자열을 반환합니다. 아래는 사용 예를 보여줍니다.

 

String str="AABBCCDD";
System.out.println(str.substring(2));
System.out.println(str.substring(2, 4));

"AABBCCDD"의 substring(2)는 처음 나오는 B의 자리를 인자로 전달합니다. 문자열의 첫번째 인덱스는 0부터 시작한다는 것을 잊지마세요. 그러니 처음 나오는 A의 인덱스는 0입니다. 결과는 B부터 문자열의 끝까지 새로운 문자열을 반환합니다.

또 substring(2,4)는 2번째 인덱스부터 4번째 인덱스 전까지 새로운 문자열을 반환합니다. 주의해야할 것은 4번 인덱스 앞까지라는 것입니다. 그러므로 4번 인덱스의 문자는 포함되지 않습니다. 결과는 "BB"라는 문자열이 반환되겠네요.

 

위의 결과는 아래와 같습니다.

 

결과

BBCCDD
BB

 

 

 

 

char charAt(int index)

문자열에서 char 값이 필요할때 사용하는 메소드입니다. 인자 index는 문자열에서 한 문자를 뽑아낼 위치를 말합니다.

String str="AABBCCDD";
System.out.println(str.charAt(4));
System.out.println(str.charAt(str.length()-1));

 

예를 들어 "AABBCCDD"라는 문자열에서 4번째 인덱스의 char 값을 얻고 싶다면 인자로 4를 넣어주면 됩니다. 그렇다면 처음 나오는 'C'가 될겁니다. 또는 마지막 문자열의 문자를 추출해내고 싶다면 length() 메소드의 반환값에서 1을 뺀 값을 전달하면 제일 마지막 문자를 가져올 수 있습니다.

 

결과

C
D

 

String concat(String str)

문자열을 합치는 메소드입니다. str을 현재 문자열 뒤에 추가합니다. 사실 저는 별로 이 메소드를 사용하지는 않는데요. 자바에서는 문자열을 합칠때 + 연산으로 합칠 수 있기 때문이지요.

String str="AABBCCDD";
System.out.println(str.concat("EE"));
System.out.println(str+"EE");

 

아래에서 똑같은 결과를 볼 수 있지요.

 

결과

AABBCCDDEE

AABBCCDDEE

 

boolean contains(CharSequence s)

문자열에서 특정 문자열이 포함되어있는지 여부를 확인하려면 이 메소드를 사용하면 됩니다. 매개변수에 포함됐는지의 문자열을 전달하고 문자열이 포함되어 있다면 true, 포함되지 않았다면 false를 반환합니다.

 

String str="AABBCCDD";
System.out.println(str.contains("BC"));
System.out.println(str.contains("AD"));

"BC"는 "AABBCCDD"에 포함되어 있으므로 true를 반환할 것이고, "AD"라는 문자열을 포함되어 있지 않으니 false를 반환합니다.

 

결과

true
false

 

 

 

 

static String copyValueOf(char []data)

static String copyValueOf(char []data, int offset, int count)

이 메소드는 static 메소드입니다. char형 배열을 문자열로 변환할때 사용하는 메소드인데요. 그냥 char 배열을 전달하면 그 배열 자체를 문자열로 반환합니다.

특정 위치에서 몇개의 문자까지 문자열로 받고 싶다면 offset에 특정 문자열을 얻고 싶은 위치를, count에 몇개의 문자를 받을 지를 전달하면 되겠습니다.

char []charArray={'A','B','C','D','E','F'};
System.out.println(String.copyValueOf(charArray));
System.out.println(String.copyValueOf(charArray,2,4));		

 

결과를 보면 직관적으로 이 메소드가 어떤 기능을 하는지 알 수 있을 겁니다.

 

결과

ABCDEF
CDEF

 

String[] split(String regex)

String[] split(String regex, int limit)

정규표현식을 기준으로 문자열을 쪼갭니다. 정규표현식을 몰라도 됩니다. 만약 limit에 인자를 전달하면 그 limit까지만 문자열을 쪼갭니다.

 

String str="AABB__CCDD__EEFF";
String []arr=str.split("__");
for(int i=0;i<arr.length;i++)
	System.out.println(arr[i]);

"AABB__CCDD__EEFF"를 "__"로 분리시켜보면 세개의 문자열이 나옵니다. "AABB", "CCDD", "EEFF"가 그것들이죠. 이것은 한가지 예이므로 쉼표로 분리하고 싶다면 쉼표로 분리할 수도 있습니다.

 

결과

AABB
CCDD
EEFF

 

String toLowerCase()

String toLowerCase(Locale locale)

String toUpperCase()

String toUpperCase(Locale locale)

문자열을 전부 소문자 또는 전부 대문자로 변환된 문자열을 얻고 싶을 때 위의 메소드를 사용하면 됩니다.

소문자로 변환된 문자열을 얻고 싶을때는 toLowerCase, 대문자로 변환된 문자열을 얻고 싶을때는 toUpperCase를 사용하면 되는 것이죠. 메소드 인자는 Locale은 사실 써본적은 없습니다. 인자없는 toLowerCase, toUpperCase로도 충분해 보입니다.

String str1="AABBCCDD";
String str2="ffgghhii";
System.out.println(str1.toLowerCase());
System.out.println(str2.toUpperCase());

 

"AABBCCDD"는 toLowerCase로 소문자로 출력하고, "ffgghhii"는 toUpperCase로 모두 대문자로 출력합니다.

 

결과

aabbccdd
FFGGHHII

 

 

int compareTo(String anotherString)

문자열을 사전순으로 비교합니다. 만약 anotherString이 사전순으로 앞에 등장할때는 양수를 반환하고 사전순으로 늦게 등장할때는 음수를 반환합니다. 만약 anotherString이 현재 문자열과 정확히 같다면 0을 반환하게 됩니다. 문자열을 비교할때는 equals를 주로 사용하는데요. 사전순으로 비교하고 싶을땐 compareTo를 사용하면 됩니다.

 

String str1="BCD";
String str2="ABC";
String str3="BCD";
String str4="CDE";
System.out.println(str1.compareTo(str2));
System.out.println(str1.compareTo(str3));
System.out.println(str1.compareTo(str4));

 

"ABC"는 "BCD" 기준으로 사전상 앞에 등장하니까 양수, "CDE"는 "BCD" 기준으로 사전상 뒤에 등장하니까 음수가 반환됩니다. str1과 str3은 문자열이 같으므로 0이 반환됩니다.

 

결과

1
0
-1

 

 

 

 

int compareToIgnoreCase(String str)

만약 소문자, 대분자를 무시하고 비교하고 싶을때는 이 메소드를 사용하면 됩니다. 원래 "ABC"와 "abc"를 compareTo 메소드로 비교하면 0이 반환되지 않습니다만 이 메소드를 사용해서 비교한다면 0이 반환됩니다.

String str1="abc";
String str2="ABC";
String str3="abc";
String str4="aaa";
System.out.println(str1.compareToIgnoreCase(str2));
System.out.println(str1.compareToIgnoreCase(str3));
System.out.println(str1.compareToIgnoreCase(str4));

 

str4와 비교하는 것을 제외하고는 0을 반환하겠군요.

 

결과

0
0
1

 

 

boolean equals(Object anObjec)

boolean equalsIgnoreCase(String anotherString)

사전순으로 비교할 필요없이 단순히 문자열이 같은지 다른지 비교할때는 이 메소드를 씁니다. 역시 대소문자를 구분하지 않는다면 equalsIgnoreCase를 사용하면 됩니다. 같다면 true, 다르면 false를 반환합니다.

 

 

static String format(String format, Object ...args)

특정 format으로된 문자열을 얻고 싶을때 사용하는 메소드입니다. C언어에서 이것과 비슷한 기능을 하는 sprintf가 되겠네요. 

String str=String.format("%d+%d=%d", 1,2,1+2);
System.out.println(str);

 

C언어를 사용했던 분들에게는 조금 익숙한 메소드겠네요.

 

결과

1+2=3

 

boolean startsWith(String prefix)

boolean startsWith(String prefix, int toffset)

boolean endsWith(String suffix)

startsWith는 문자열이 prefix로 시작하는지 확인하는 메소드입니다. prefix의 비교 위치를 지정하려면 startsWith의 두번째 메소드를 사용하면 됩니다.

endsWith는 문자열이 suffix의 문자열로 끝나는지 확인하는 메소드입니다. suffix로 끝난다면 true, 다르게 끝나면 false를 반환합니다.

		
String str="ABCDEF";
System.out.println(str.startsWith("AB"));
System.out.println(str.startsWith("BC",1));
System.out.println(str.endsWith("EF"));
System.out.println(str.endsWith("EE"));

결과

true
true
true
false

 

 

 

 

 

String replace(char oldChar, char newChar)

String replace(CharSequence target, CharSequence replacement)

String replaceAll(String regex, String replacement)

String replaceFirst(String regex, String replacement)

문자나 문자열을 다른 문자나 문자열로 바꾸고 싶을때 사용하는 메소드입니다. 메소드 이름이 직관적이기 때문에 사용법을 익히는데 그리 어렵지 않을 겁니다. 또는 아래의 예를 보고 이해하면 되겠지요.

String str="AB-----AB-----AB";
String str1=str.replaceFirst("AB", "ab");
String str2=str.replace("AB", "ab");
String str3=str.replaceAll("AB", "ab");
String str4=str.replace('A', 'a');
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);

 

결과

ab-----AB-----AB
ab-----ab-----ab
ab-----ab-----ab
aB-----aB-----aB

 

사실 이 중에서도 자주 쓰이는거 외에는 별로 쓸 일이 없을 겁니다. 자주 필요한 몇가지만 기억해두면 될 것 같네요. 다음 포스팅에서도 String 메소드에 대해서 더 알아보도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,

스트림

자바에서도 여러 입출력을 지원하지만 이번에 우리의 관심사는 바로 자바에서 제공하는 파일 입출력입니다.


그 전 우리는 스트림에 대한 이야기를 잠깐 간략하게 하고 넘어가겠습니다. 우선 파일에서 입력과 출력이라는 동작을 하려면 파일로 데이터를 전달하거나 파일로부터 전달 받는 길을 열어주어야합니다.


그러한 길을 스트림이라고 하지요.





파일로부터 입력을 받는 스트림을 입력스트림, 출력을 보내는 스트림을 출력스트림이라고 합니다.

그리고 바이너리 형태로 데이터를 입출력하는 스트림을 이진스트림, 문자형태로 입출력하는 스트림을 텍스트스트림이라고 합니다.




스트림을 알았으니 파일입출력을 알아보도록 합시다.


FileReader와 FileWriter


import java.io.*; public class Main { public static void main(String[] args) throws IOException{ File file=new File("test.txt"); if(!file.exists()) file.createNewFile(); FileWriter fw=new FileWriter(file); char []buf= {'m','e','s','s','a','g','e','\r','\n'}; for(int i=0;i<buf.length;i++) fw.write(buf[i]); fw.close(); FileReader fr=new FileReader(file); int EOF=-1; int c; while((c=fr.read())!=EOF) { System.out.print((char)c); } fr.close(); } }


이 코드는 test.txt파일에 "message"라는 문자열을 기록하고 읽어오는 프로그램이에요. 한줄 한줄씩 보도록 하지요.


우선 File 입출력시에는 IOException이 발생할 수 있고 처리가 귀찮으니 저 멀리 보내버리도록 합시다. 가버렷!


일단 file을 열어줘야겠지요? 절대 경로로 지정하지 않는다면 현재 프로젝트 디렉토리에 파일을 엽니다.


하지만 그 파일이 없다면 새로 생성합니다. 

이것을 다음의 라인이 나타냅니다.


File file=new File("test.txt"); if(!file.exists()) file.createNewFile();


그 후 파일에 "message"라는 문자열을 기록합니다. 그때 사용하는 클래스가 바로 FileWriter라는 클래스이지요.

FileWriter의 메소드 write를 통해서 char 배열의 문자열을 하나씩 기록한 후 스트림을 닫습니다.




닫아주어야 파일에 문자열이 입력이 됩니다! 파일을 닫지 않고 파일에 입력하려면 그렇지 않으면 flush함수를 사용하세요. 


FileWriter fw=new FileWriter(file);
char []buf= {'m','e','s','s','a','g','e','\r','\n'};
for(int i=0;i<buf.length;i++)
	fw.write(buf[i]);
fw.close();

이후 한문자씩 읽어오는데 그 역할을 수행하는 클래스가 바로 FileReader입니다. 파일의 끝은 int형의 -1입니다. 그래서 -1을 만날때까지 한문자 한문자 출력합니다.

int형태로 읽어왔으니 char로 바꿔줘야겠지요?


FileReader fr=new FileReader(file);
int EOF=-1;
int c;
while((c=fr.read())!=EOF) {
	System.out.print((char)c);
}
fr.close();


이제 수행을 해보도록 할게요. 어떤 변화가 있는지..


test.txt라는 파일이 생겼네요! 



이것을 까보면!




그안에 우리가 집어넣은 문자열이 존재하는 군요.


eclipse상의 결과 역시 "message"라는 문자열을 출력하는 군요.


파일에 기록한 문자열을 자바프로그램이 읽어온 것이랍니다.



한글자 한글자 읽어오는 것이 여간 불편한 것이 아니죠?

문자열을 사용해서 쓰는 것이 훨씬 더 편할텐데요.

그리고 읽어올때도 배열을 써서 읽어오는 것이 훨씬 편하구요.




그런 방법이 아래에 나와있습니다.




import java.io.*;


public class Main {
	public static void main(String[] args) throws IOException{
		File file=new File("test.txt");
		if(!file.exists())
			file.createNewFile();
		
		FileWriter fw=new FileWriter(file);
		fw.write("Hello, world!!\r\n");
		
	
		fw.close();
		
		FileReader fr=new FileReader(file);
		
		while(true) {
			char []buf=new char[4];
			int ret=fr.read(buf);
			if(ret==-1) break;
			System.out.print(String.valueOf(buf));
		}
		fr.close();
	}
}


FileWriter는 String을 받는 write메소드가 있어서 문자열로 그대로 파일에 기록할 수 있습니다.


중요한건 FileReader인데요. 우선 buf라는 char형 4개에 문자열을 계속 입력받는 거지요. 만약 파일에서 더 읽어올 것이 없다면 -1을 리턴하게 되니까 그 반환형이 -1이면 while루프를 탈출하면 됩니다.


이제 실행 후 파일을 확인해보고 이클립스에서도 확인해 봅시다.





파일에 제대로 적혀있고 이클립스 출력도 이 문자열이 나오는 것을 확인할 수 있죠?



Line 단위 입출력


아직도 불편하긴 합니다. /r/n을 통해서 개행하는 것도 별로구요. 보통 문자 입출력시에는 라인 단위로 입출력을 하기 때문에 라인별로 입력을 할 수 있었으면 좋겠습니다.


그래서 나온것이 버퍼단위의 입출력을 담당하고 있는 BufferedReader, BufferedWriter입니다.



import java.io.*;


public class Main {
	public static void main(String[] args) throws IOException{
		File file=new File("test.txt");
		if(!file.exists())
			file.createNewFile();
		
		BufferedWriter bw=new BufferedWriter(new FileWriter(file));
		
		bw.write("Hello, world!");
		bw.newLine();
		bw.write("Hello, world!!");
		bw.newLine();
		bw.write("Hello, world!!!");
		bw.newLine();
		bw.close();
		
		BufferedReader br=new BufferedReader(new FileReader(file));
		String line=null;
		while((line=br.readLine())!=null)
			System.out.println(line);
			
		br.close();
	}
}


BufferedWriter와 BufferedReader는 버퍼의 사이즈를 지정할 수도 있습니다. 그렇지 않으면 Default 사이즈로 버퍼에 담습니다.


BufferedWriter의 newLine메소드는 개행을 말합니다. 쉽죠? 별거 없어요.


여기서는 Hello, world!를 3라인에 걸쳐 출력합니다. (느낌표 갯수만 다르고요)


이제 BufferedReader를 통해서 읽어옵니다. 

readLine은 String 형태의 문자열을 반환하는데, 만약 더이상 출력할 문자열이 없으면 null을 반환하죠.


그래서 null을 만나게 되면 while루프를 탈출합니다.


이제 실행후 파일과 이클립스화면을 보면 둘의 결과는 같다는 것을 알 수 있습니다.


test.txt




이클립스 실행결과


Hello, world!

Hello, world!!

Hello, world!!!



어떻습니까?


BufferedReader와 BufferedWriter 정말 편리하죠?

이 두 클래스는 조금 빈번하게 쓰입니다. 기억해두세요.


아직 파일입출력에 대해서는 더 할 이야기가 나왔습니다. 나중에 더 이야기 해보도록 하지요.

반응형
블로그 이미지

REAKWON

와나진짜

,

안녕하세요. 이번 시간에는 클래스 중 조금 특별한 속성을 갖고 있는 추상 클래스(Abstract Class)와 자바에서 등장하는 인터페이스(Interface)에 대해서 알아보는 시간을 갖도록 하겠습니다.


추상클래스(Abstract Class)


름부터가 아주 비호감입니다. 저는 추상이라는 단어를 무척이나 싫어하거든요.


뭔가 있는 듯 없는 듯하면서도.. 만질수 있을 듯 없을 듯하면서도.. 볼수 있을듯 없을 듯한 그 거시기한 거.. 그런건데


아주아주 간단히 말해서 추상메소드가 적어도 0개 있는 클래스가 추상클래스라고 합니다.


장난하냐? 그러지 마시고 일단 추상 메소드가 무엇인지 말씀 드리고 시작하겠습니다.


추상클래스는 메소드의 선언만 되어있을 뿐 정의는 되어있지 않은 것을 말합니다. 몸통이 없다는 것이죠.


몸통이 없이 메소드의 선언만 존재하는 것이 추상 메소드이며 추상 메소드를 0개 이상 갖는다면 추상클래스이다 라고 말할 수 있겠네요. 

0개 이상이라는 말은 추상메소드를 갖지 않아도 추상 클래스가 될 수 있다는  것인데, 이렇게 되면 보통의 클래스를 의미하기 때문에 보통은 한개 이상 추상 메소드를 갖는다면 추상 클래스라고 합니다.




아래와 같이 사용합니다. 우선 추상클래스를 정의하려거든 class앞에 abstract라는 키워드를 붙여줍니다.


추상메소드 역시 마찬가지입니다. abstract 키워드를 붙이고 메소드 구현부만 없으면 됩니다. 

abstract class AbstractClass{
	int x;
	public void NormalMethod() {}
	public abstract void AbstractMethod(); 
}

보통의 클래스와의 차이점은 객체를 직접생성할 수 없다는 점입니다.


그래서 이 추상 클래스를 상속받아 그 상속받은 클래스의 객체로 생성해야합니다. 또는 다형성의 성질을 이용할 수 있지요.


사용법은 아래의 코드와 같습니다.


abstract class Animal{
	public void seeFood() {
		System.out.println("내가 음식을 봤을 때");
	}
	abstract public void cry();
}

class Dog extends Animal{

	@Override
	public void cry() {
		System.out.println("왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!");
	}
	
}

class Cat extends Animal{

	@Override
	public void cry() {
		System.out.println("야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~");
	}
	
}
public class Main {

	public static void main(String[] args) {
		Animal dog=new Dog();
		dog.seeFood();
		dog.cry();
		
		System.out.println();
		
		Animal cat=new Cat();
		cat.seeFood();
		cat.cry();
	}
}




Animal을 상속받은 클래스 Dog와 Cat은 무조건 abstract의 메소드를 오버라이딩해주어야 합니다. 그렇지 않으면 Animal클래스를 상속할 수 없습니다.

결과는 어떻게 될까 예상이 되시나요?


내가 음식을 봤을 때

왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!


내가 음식을 봤을 때

야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~



여기서 추상클래스와 비슷한 인터페이스(Interface)를 소개합니다.



인터페이스(Interface)


인터페이스는 명세라고도 불리는데요. 추상클래스의 가장 극단적인 형태라고 보면 됩니다. 

인터페이스는 전부가 추상메소드로 이루어져 있습니다.

자신의 변수도 쓸수 없고 오로지 메소드의 선언만이 있어야합니다.

아래처럼 말이죠.


interface Interface {
	public void method1();
	public void method2();
}

마치 추상클래스로 표현하자면 이렇게 표현할 수도 있겠네요.


abstract class Interface{
	public abstract void method1(); 
	public abstract void method2(); 
}


인터페이스의 사용 예제는 아래와 같습니다.


interface Animal {
	public void cry();
	public void sleep();
}

class Dog implements Animal{

	@Override
	public void cry() {
		System.out.println("왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!");
	}
	@Override
	public void sleep() {
		System.out.println("자신의 집에서 잠을 잡니다.");
	}
	
}

class Cat implements Animal{

	@Override
	public void cry() {
		System.out.println("야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~");
	}
	@Override
	public void sleep() {
		System.out.println("집사 얼굴 위에서 잠을 잡니다.");
	}
	
}
public class Main {

	public static void main(String[] args) {
		Animal dog=new Dog();
		dog.cry();
		dog.sleep();
		
		System.out.println();
		
		Animal cat=new Cat();
		cat.cry();
		cat.sleep();
	}
}

Dog와 Cat은 Animal 인터페이스를 implements하고 있습니다. 근데, 이제까지 봐왔던 extends가 아니군요. 그 이유는 아래에서 설명하도록 하겠습니다.


결과는 이렇게 됩니다.


왈!! 왈왈!! 왈왈~!!! 왈월워뤙왈!!! 멍멍!

자신의 집에서 잠을 잡니다.


야옹~~ 옹옹오오오오오옹~~~~~~ 야오우오우오우옹~~~~

집사 얼굴 위에서 잠을 잡니다.




추상클래스 VS 인터페이스


한가지 중요한건 이 둘의 차이에 대해서 알아야합니다.


추상클래스는 역시 클래스입니다. 단독으로 객체를 생성할 수만 없지 나머지는 보통의 클래스처럼 생성자, 변수, 메소드를 갖고 있을 수 있습니다. 그렇기 때문에 자식 클래스에서는 변수나 구현된 메소드를 물려받기 때문에 상속(extends)받을 수 있는 것이죠.


인터페이스 역시 직접 객체를 생성할 수 없습니다. 구현되어야 할 메소드만 명시해 놓고 인터페이스를 받는 클래스는 그 인터페이스에 맞는 메소드를 구현해야합니다. 그렇기 때문에 implements(구현하다)라는 키워드로 인터페이스를 전달받을 수 있지요.


클래스와는 다르게 여러개의 인터페이스를 클래스가 구현할 수 있습니다. 클래스로 따지자면 다중상속과 비슷한 개념입니다.

자바는 다중상속을 지원하지 않지만 다중 인터페이스를 통한 상속은 지원하지요.




아주 간단한 예를 들어보도록 하지요.

개발시에 동료에게 아래와 같은 Operator의 인터페이스를 구현하는 클래스를 만들기를 부탁했다고 한다면 여러분의 착한 동료는 그렇게 하겠죠? 


interface Calculator{
	public int sum(int a,int b);
	public int subtraction(int a,int b);
}



그래서 구현해야하는 sum과 subtraction을 호출하는 부분에 대해서만 신경써서 개발하고 sum과 subtraction 내부는 생각하지 않아도 됩니다


단, 동료에게 어떤 기능을 하는 메소드이니 이런 메소드 형식으로 만들어 달라라고 이야기 해주어야 겠죠?


여기서 여러분은 동료에게 이런 기능을 하는 멤버 메소드를 만들어라라고 명시한 겁니다.


여기 Calculator 명세를 너에게 줄건데 너는 꼭 int형 매개변수 2개를 받고, int 반환형을 갖는 sum을 구현하고

마찬가지로 int형 매개변수 2개를 받고, int 반환형을 갖는 subtraction이라는 메소드를 구현하라고 명세를 만들어 준 거에요.




이제 왜 인터페이스는 implements를 써서 받고, 추상클래스는 extends를 써어 받는 지 차이점을 알겠죠?



반응형
블로그 이미지

REAKWON

와나진짜

,

다형성(Polymorphism)


다형성이라는 개념은 OOP에서 아주 중요한 개념이므로 모르면 OOP에 대해서 제대로 안다고 할 수 없는 개념입니다.


각 요소들이 여러 가지 자료형으로 표현될 수 있다는 것을 말하게 되는데, 반댓말로는 단형성이 있습니다. 한가지의 요소는 한가지의 형태로만 매칭된다는 것을 의미합니다.


음... 일단 모르겠어요.. 다형성이 정확히 무엇인지.


암튼 뭐.. 앞에 '다'라는 의미는 '많은 다(多)' 자가 아니겠어요?? 뭔가 여러가지 (자료)형을 말하는 것 같은데 클래스를 만들어가면서 알아보도록 하지요.


여기 People이라는 클래스가 있습니다. 아주 간단하게 정의한 클래스죠. 그 안에는 printInfo라는 멤버메소드가 있군요.




class People{
	
	public void printInfo() {
		System.out.println("나는 사람입니다.");
	}
}



People 클래스에서 printInfo를 호출하게 되면 지가 사람이라는 군요.


그 밑에 Man과 Woman 클래스는 People클래스를 상속합니다.


class Man extends People{}
class Woman extends People{}


이후 메인에서는 이 두 클래스를 객체로 만들어 printInfo를 호출합니다. 


public class Test {

	public static void main(String[] args) {
		Man man=new Man();
		Woman woman=new Woman();
		
		man.printInfo();
		System.out.println();
		woman.printInfo();
	}
}


이후 실행을 하게 되면 아래의 결과가 나오게 되겠죠.


나는 사람입니다.


나는 사람입니다.


두 클래스 Man과 Woman은 People이라는 클래스를 상속받았으므로 printInfo 호출시 People의 printInfo를 호출할 수 있다는 것, 뭐 놀랍지 않군요.


이제 이것을 토대로 다형성을 세세하게 알아보도록 합시다.



Woman, Man은 People이다





UML 다이어그램으로 본다면 위의 그림과 같을 겁니다.

Man과 Woman은 People이라는 클래스를 상속하기 때문이에요.


우리는 이 다이어그램을 이런 관점으로 한번 바라볼 수 있을까요?


Man은 People이다. (남자는 사람이다.)

Woman은 People이다. (여자는 사람이다.)


현실 세계에서 이 다이어그램을 말로 풀어보아도 그 의미가 맞습니다.

그 반대는 어떨까요?


People은 Man이다. (사람은 남자이다.)

People은 Woman이다. (사람은 여자이다.)


사람은 남자인가요? 아니면 사람은 여자인가요?

그렇지 않습니다. 사람은 남자인지, 여자인지 알 수가 없습니다.


그렇기 때문에 반대로 표현하면 모호해진다는 것을 알 수 있지요.


여기서 중요한 점은 Man은 People로 표현할 수 있고, Woman도 People로 표현할 수 있다는 것입니다.


이것이 다형성의 개념이 나오게 됩니다. Man과 Woman은 People이기 때문에 People이라는 자료형으로 받을 수 있습니다.




한번 확인해볼까요?

public class Test {

	public static void main(String[] args) {
		People people=new Man();
		
		people.printInfo();
		System.out.println();
		
		people=new Woman();
		people.printInfo();
		
	}
}


실행을 시켜서 확인해보면 위의 결과와 동일한 것을 알 수 있습니다. 

Man과 Woman은 People(부모클래스)로 받을 수 있다는 점을 기억하세요!



다형성과 오버라이딩

여기서 Man과 Woman은 printInfo를 물려받았고 오버라이딩(Overriding)할 수 있다는 것을 알고 있습니다.


그래서 저는 Man과 Woman의 printInfo를 그 클래스에 맞도록 오버라이딩하고 싶습니다. Man과 Woman 클래스를 다음과 같이 수정해보도록 하지요.



class Man extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 남자입니다.");
	}
	
}
class Woman extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 여자입니다.");
	}
}

그리고 실행해본다면 


나는 사람입니다.

그리고 나는 남자입니다.


나는 사람입니다.

그리고 나는 여자입니다.


오버라이딩된 printInfo를 호출한다는 것을 알 수 있습니다. 오...

다형성에서 People은 자식클래스에서 재정의된 메소드를 호출할 수 있다는 것입니다.


그렇다면 Woman과 Man에서 단독으로 정의한 메소드는 어떻게 될까요?

Man과 Woman클래스에서 다음과 같이 메소드를 추가해보도록 합시다.



class Man extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 남자입니다.");
	}
	
	public void enlist() {
                System.out.println("내일 군대를 갑니다.");
		System.out.println("충성!");
	}
	
}
class Woman extends People{
	@Override
	public void printInfo() {
		super.printInfo();
		System.out.println("그리고 나는 여자입니다.");
	}
	
	public void makeUp() {
                System.out.println("예뻐질 거랍니다.");
		System.out.println("톡톡 촵촵!");
	}
}


그리고 people.enlist를 호출하려한다면 호출이 되지 않습니다. 왜냐하면 People 형이기 때문이죠. People 클래스는 enlist라는 메소드를 갖지 않기 때문에 호출할 수 없습니다.


이런 경우에는 데이터 형에 맞게 캐스팅해주어서 사용해야합니다.

바로 아래처럼요.


public class Test {

	public static void main(String[] args) {
		People people=new Man();
		people.printInfo();
		((Man)people).enlist();
		
		System.out.println();
		
		people=new Woman();
		people.printInfo();
		((Woman)people).makeUp();
		
	}
}


왜 그런걸까요?

People은 자신을 상속한 클래스 중에서 어떤 매소드를 만들지, 어떤 멤버 변수를 만들어낼지 미리 알아낼 수 없기 때문이죠.


때문에 그 메소드가 있는 객체로 직접 캐스팅해주어서 매소드를 사용해야합니다.


이해를 돕기 위한 그림이 아래에 있습니다. People이라는 자료형이 사용가능한 메소드는 printInfo밖에 없습니다. 그래서 Man의 printInfo메소드를 사용할 수 있습니다. 


또한 new가 동적 메모리를 할당하는 역할을 하므로 Man이 실제 메모리에 잡히게 됩니다. 따라서 형변환을 Man으로 하는 것이 가능한 것이죠.






이와 같은 다형성은 어디에서 쓰일까요?


대표적으로 메소드에서 매개변수로 People을 상속하는 클래스를 받을때 사용할 수 있습니다. 


        public static void func(People people) {
		people.printInfo();
	}
	public static void main(String[] args) {
		Man man=new Man();
		Woman woman=new Woman();
		func(man);
		
		System.out.println();
		func(woman);
	}

func의 매개변수 people은 People의 객체이기 때문에 그것을 상속하는 모든 클래스를 받아 낼 수 있어요.


그래서 Object 객체로 모든 객체를 받을 수 있는 것도 바로 이러한 다형성의 속성때문입니다. 




또한 필요에 의해서는 instanceof 연산자를 사용해서 캐스팅할 수 있습니다.




        public static void func(People people) {
		people.printInfo();
		if(people instanceof Man) 
			((Man)people).enlist();
		if(people instanceof Woman)
			((Woman)people).makeUp();
		
	}
	public static void main(String[] args) {
		Man man=new Man();
		Woman woman=new Woman();
		func(man);
		
		System.out.println();
		func(woman);
	}


OOP의 가장 중요한 특징 중 하나 다형성에 대해서 알아보았습니다. 도움이 되었스면 좋겠습니다. 화이팅!


반응형
블로그 이미지

REAKWON

와나진짜

,