벡터(Vector)

벡터는 ArrayList와 같이 선형적으로 자료를 담을 수 있는 List 컬렉션입니다. ArrayList와는 다르게 동기화 처리를 하기 때문에 ArrayList보다는 속도가 느리다는 단점이 있습니다. 하지만 여러 쓰레드에서 같은 List를 써야할 필요가 있다면 Vector를 사용하시는 것이 동기화 오류를 만들지 않는 방법이겠죠. 

동기화에 대한 예는 맨 아래에 설명하도록 하겠습니다. 지금부터 사용법에 대해서 알아보도록 합시다.

사용법

벡터는 Generic을 사용합니다. 꺽쇠 '<', '>' 에 자료 타입을 지정해주고 수행합니다. 꺽쇠 안에는 원소로 사용할 객체의 클래스를 명시해주면 됩니다. 아래는 Integer 형의 자료를 Vector에서 다룬다는 선언을 보여줍니다.

 

 

Vector<Integer> v=new Vector();

 

1 - 원소 추가와 삭제, 읽기(add, remove, get)

선형으로 자료를 담고 있습니다. 그래서 데이터를 넣으면 차례차례 뒤에 데이터들이 붙어서 쌓이게 됩니다. 특정 위치의 데이터도 삭제할 수 있습니다. 그리고 정수형 index로 원소를 가져올 수 있습니다. 아래가 그 예제를 보여줍니다. 

import java.util.Vector;	//클래스 import
public class Main {
	
	public static void main(String[] ar){
		Vector<Integer> v=new Vector();
		v.add(20);	//0
		v.add(50);	//1
		v.add(70);	//2
		v.add(100);	//3
		
		for(int i=0;i<v.size();i++) {
			System.out.println(i+"번째 원소:"+v.get(i));
		}
		
		System.out.println("\n index 1 원소 삭제");
		v.remove(1);
		
		for(int i=0;i<v.size();i++) {
			System.out.println(i+"번째 원소:"+v.get(i));
		}
	}
}

 

결과

0번째 원소:20
1번째 원소:50
2번째 원소:70
3번째 원소:100

 index 1 원소 삭제
0번째 원소:20
1번째 원소:70
2번째 원소:100

 

예제를 보면 차례차례 20, 50, 70, 100의 데이터를 넣어주고 있습니다. 그리고 for문을 돌면서 출력해주고 있습니다. 차례대로 출력이 되는 것을 확인할 수 있죠? 그런데 index가 1인 50을 삭제하게 되면 삭제된 뒤의 원소들이 전부 앞으로 당겨지게 됩니다.

 

 

remove

만약 size()보다 큰 index를 갖는 원소를 삭제하거나 get()으로 읽어온다면 ArrayIndexOutOfBoundsException이 발생하게 되므로 size() 체크를 항상 해주셔야합니다. 맨 아래 라인에 v.remove(10); 코드를 추가해서 확인해보도록 하세요.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 10
	at java.base/java.util.Vector.remove(Vector.java:844)
	at aa.Main.main(Main.java:23)

 

원소를 모두 삭제하고 싶다면 clear(), removeAllElements()를 사용하면 모두 지워집니다.

 

2. Vector를 반복할 수 있는 여러 방법

위의 예제는 단순 for문으로 i를 인덱스 삼아서 get()으로 원소를 읽어왔습니다. 이밖에도 여러 방법이 있는데, 그 방법을 소개합니다. 

2-1 forEach 메소드 사용

	Vector<String> v=new Vector();
	v.add("korea");
	v.add("england");
	v.add("rusia");
		
	v.forEach((item)->{
		System.out.println(item);
	});

람다식을 이용해서 순회할 수 있습니다. 함수 안 괄호에는 원소가 들어갑니다. 그 변수명이 item이지요. 그리고 중괄호('{}')에는 동작부를 구현하면 됩니다. 람다식을 몰라도 사용하는 데에는 문제가 없습니다.

2-2 Iterator객체로 순회

	Iterator<String> it=v.iterator();
	while(it.hasNext()) {
		String item=it.next();
		System.out.println(item);
	}

 

사이즈를 체크하지 않으면서 순회하고 싶다면 Iterator클래스로 객체를 만들어 사용하는 방법이 있습니다. Iterator클래스는 Java.util 패키지에 존재하니 import하여 사용해보세요. 순회에 필요한 메소드는 hasNext()와 원소를 가져오는 next()메소드만 알고 있으면 됩니다.

메소드 설명
hasNext() 이 다음에 원소가 있는지 확인합니다. 있으면 true, 없으면 false를 반환하지요. 대체로 while안의 조건문에 사용합니다.
next() 다음 원소를 가져옵니다. 반환되는 객체는 Generic으로 넘겨준 원소의 자료형입니다.

위 코드의 결과는 그 이전의 결과와 같습니다.

 

3. Vector와 Vector를 합치기

 

 

public static void main(String[] ar){
		Vector<String> v1=new Vector();
		v1.add("r");
		v1.add("e");
		v1.add("a");
		v1.add("k");
		
		Vector<String> v2=new Vector();
		v2.add("w");
		v2.add("o");
		v2.add("n");
		
		v1.addAll(v2);
		v1.forEach((item)->{
			System.out.print(item);
		});
		System.out.println();
	}

 

결과

reakwon

 

addAll() 메소드로 벡터와 벡터를 합칠 수 있습니다. 단, 두 벡터는 같은 Generic 형식을 사용해야하고 합칠 벡터 뒤에 그 벡터가 붙습니다. 또는 생성자를 이용해서 객체 생성시에 벡터를 합칠 수 있습니다. 아래와 같은 형식으로 사용할 수 있습니다. 아래 예는 v2에 v1을 객체 생성시에 합칩니다.

Vector<String> v2=new Vector(v1);

 

4. 원소가 존재하는 지 확인

	public static void main(String[] ar){
		Vector<String> v=new Vector();
		v.add("r");	v.add("e");
		v.add("a"); v.add("kwon");
		
		System.out.println(v.contains("r"));
		System.out.println(v.contains("z"));
	}

 

결과

true
false

 

contains() 메소드로 Vector에 원소가 있는지 확인할 수 있습니다. 있으면 true, 없으면 false를 반환합니다.

 

5. Vector 정렬

public static void main(String[] ar){
	Vector<String> strV=new Vector();
	strV.add("reakwon");
	strV.add("hello");
	strV.add("world");
		
	//알파벳 순으로 정렬
	Collections.sort(strV);
	strV.forEach((item)->{
		System.out.println(item);
	});
		
	System.out.println();
		
	Vector<Integer> intV=new Vector();
	intV.add(5);
	intV.add(1);
	intV.add(3);
		
	//오름차순 정렬
	Collections.sort(intV);
	intV.forEach((item)->{
		System.out.println(item);
	});
}

Collections.sort() 메소드를 이용해서 Vector의 데이터를 정렬할 수 있습니다. 기본적으로 숫자는 오름차순, 문자열은 사전순으로 정렬됩니다.

결과

hello
reakwon
world

1
3
5

혹은 내가 만든 객체를 정렬하려면 어떻게할까요? 사람 객체를 키순으로 정렬하고 싶다면, 또는 이름 순으로 정렬, 나이순으로 정렬하고 싶다면 어떻게할까요? 우리가 직접 비교해서 Collections에게 알려줘야합니다. 그것에 대한 설명은 아래의 링크를 참고해주세요. 사용법은 동일합니다.

reakwon.tistory.com/91

 

[자바/JAVA] Collections와 ArrayList를 이용한 객체 정렬

정렬 우리는 정렬에 관해서 배우기도 하였고 구현도 해보았습니다. 그래서 어떻게 정렬이 되는지 알고 있죠. 하지만 실제 프로그래밍하는 상황에서 이 정렬을 직접구현해서 프로그램을 만들지

reakwon.tistory.com

6. Thread-Safe 예

아까 이야기했던 동기화에 대한 부분을 확인해보도록 합시다. 우선 ArrayList로 아래의 코드를 짜면서 관찰해보죠. 

 

 

 

public class Main {
	
	static ArrayList<String> list=new ArrayList();
	public static void main(String[] ar){
		list.add("reakwon");
		list.add("hello");
		list.add("world");
		
		Thread thread=new Thread(new Runnable() {
			@Override
			public void run() {
				list.forEach((item)->{
					//1초마다 원소를 출력
					try {
					Thread.sleep(1000);
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(item);
				});
			}
		});
		
		thread.start();	//thread 시작
		
		//thread가 forEach문을 먼저 수행할 여유를 주기 위해 1초 기다림
		try {
			Thread.sleep(1000);
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
		//thread가 forEach() 하는 중에 원소추가
		list.add("thread-unsafe");
	}
}

 

결과

reakwon
Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1513)
	at aa.Main$1.run(Main.java:19)
	at java.base/java.lang.Thread.run(Thread.java:831)

 

위의 코드는 아주 간단한 코드입니다. thread라는 쓰레드와 메인 스레드는 list라는 ArrayList객체를 공유합니다. thread는 그 list를 forEach()로 1초마다 그 원소를 출력하는 역할을 하고, 메인 스레드는 스레드 생성하고 1초가 지난 다음 list에 원소를 추가하는 상황입니다. 이때 메인스레드가 thread가 forEach()를 수행하는 중에 list에 원소를 집어넣는 접근을 하게 되는데, 이때 두 스레드 동시에 list에 접근하게 됩니다. 그렇게 되면 위처럼 ConcurrentModificationException이 발생하게 되는 상황이 되죠. 어떻게 고칠까요?

ArrayList를 Vector로만 바꿔주면 이 문제는 해결됩니다.

static Vector<String> list=new Vector();

MainThread는 thread가 forEach()를 종료할때까지 Vector객체에 접근할 수 없고, 반대로 메인스레드의 add()가 끝날때까지 thread는 Vector객체에 접근할 수 없습니다.

Vector는 동작마다 동기화를 걸어줍니다. 이런 일은 속도롤 떨어지게 만드는 작업이지만 멀티 스레드 환경에서는 안전한 작업이죠. 그래서 여러 스레드가 있는 환경에서 개발한다면 Vector를 사용하고 단일 스레드 환경에서는 ArrayList를 활용하시면 되겠습니다.

이상으로 Vector에 대한 포스팅을 마칩니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

해시테이블(Hashtable)

해시테이블은 해시맵(HashMap)과 함께 Map을 구현한 키와 값 쌍을 데이터로 저장한 컬렉션(Collection)입니다. 여기서 키는 유일해야만 합니다. 해시테이블은 이 키를 가지고 키에 해당하는 값을 찾습니다.

Hashtable과 HashMap의 차이

Hashtable과 HashMap과의 차이점은 Thread-Safe인지 아닌지가 그 차이점인데요. Hashtable은 동기화가 걸려있어서 Thread-Safe하다고 할 수 있으며 HashMap은 동기화가 없어 unsafe하다고 할 수 있습니다. 그래서 안전성을 추구한다면 Hashtable을 쓰시면 되고, 데이터의 빠른 처리를 위해서라면 HashMap을 사용하시면 됩니다.

여기서 Hastable의 Thread-Safe가 무슨 의미를 하는지 맨 아래의 예제를 통해서 알아보도록 하세요.

해시맵의 사용법을 알고싶으신 분은 아래의 링크를 참고해주세요.

reakwon.tistory.com/151

 

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

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

reakwon.tistory.com

 

인터페이스인 Map을 구현한 만큼 HashMap과 사용법이 거의 비슷합니다. put으로 키와 값을 저장하고 get으로 키에 해당하는 값을 읽어옵니다. Hashtable에서 table은 소문자라는 점! 유의해주시길 바라요.

예 ) put, get 메소드 사용방법

import java.util.Hashtable;
import java.util.HashMap;
import java.util.Map;
public class Main {
	public static void main(String[] ar) {
		Map<String,Integer> ht=new Hashtable();
		ht.put("key1", 50);
		ht.put("key2",100);
		ht.put("key2",250);	//같은 키가 들어갈 경우에는?
		System.out.println(ht);
		System.out.println(ht.get("key1"));
		System.out.println(ht.get("key2"));
		System.out.println(ht.get("key3"));	//키가 없을 경우에는?
		
	}
}

 

결과

{key2=250, key1=50}
50
250
null

 

만약 동일한 키를 가지고 데이터를 집어넣게 된다면 가장 마지막의 키와 값 요소로 저장이 됩니다. 그리고 키가 존재하지 않으면 null을 반환합니다. 

예) 갖고 있는 모든 키를 순회(keySet)

만약 Hashtable이 갖고 있는 모든 키를 알고 싶다면 어떻게 할까요? 그럴때는 keySet()메소드로 모든 키를 가져올 수 있습니다. 이때 반환하는 자료형은 Set입니다. 아래의 코드같이 키와 값을 추출할 수 있습니다.

public static void main(String[] ar) {
	Map<String,Integer> ht=new Hashtable();
	ht.put("foo",100);
	ht.put("bar",250);
	for(String key:ht.keySet()) {
		System.out.println("{"+key+","+ht.get(key)+"}");
	}
}

 

결과

{bar,250}
{foo,100}

 

예) Hashtable에 Hashtable을 추가(putAll)

만약 두 Hashtable을 합치고 싶다면 putAll() 메소드를 사용하면 됩니다. 다만 putAll()의 인자로 전달한 Hashtable의 키, 값의 변화가 있다해도 합쳐진 Hastable에는 영향을 받지 않습니다.

public static void main(String[] ar) {
	Map<String,Integer> ht1=new Hashtable();
	ht1.put("key1",9999);
	ht1.put("key2",2020);
		
	Map<String,Integer> ht2=new Hashtable();
	ht2.put("key3",3030);
	ht2.put("key4",8888);
		
	ht2.putAll(ht1);
	System.out.println(ht2);
		
	ht1.put("key5",5555);	//ht1에 데이터 추가
	System.out.println(ht2);
}

 

결과

{key4=8888, key3=3030, key2=2020, key1=9999}
{key4=8888, key3=3030, key2=2020, key1=9999}

 

예) ForEach() 메소드로 키와 값 순회하기

Hashtable에는 ForEach메소드가 존재하는데, 이것을 가지고 키와 값으로 동작을 정의할 수 있습니다. 람다식을 알긴해야하는데, 사용법만 알아도 상관은 없습니다. 

public static void main(String[] ar) {
	Map<String,Integer> ht=new Hashtable();
	ht.put("application layer", 7);
	ht.put("presentation layer", 6);
	ht.put("session layer", 5);
	ht.put("tranport layer", 4);
	ht.put("network layer", 3);
	ht.put("datalink layer", 2);
	ht.put("physical layer", 1);
	ht.forEach((key,value)->
	{
		System.out.println("{"+key+","+value+"}");
		
	});
 }

 

결과

{session layer,5}
{physical layer,1}
{datalink layer,2}
{presentation layer,6}
{tranport layer,4}
{network layer,3}
{application layer,7}

 

 

Hashtable의 Thread-Safe

이제 여기서 Hashtable이 왜 Thread-Safe한지 설명하도록 하겠습니다. 우선 쓰레드에 대한 개념에 대해서 알 필요가 있습니다. 자바 쓰레드의 사용법은 아래의 링크를 참고해주세요.

reakwon.tistory.com/84

 

[JAVA/자바] 쓰레드(Thread) 다루기( Thread상속, Runnable 구현, join)

쓰레드(Thread) 쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레

reakwon.tistory.com

 

우선 Thread-Safe하지 않은 HashMap의 코드로 아래의 코드를 작성해보고 결과를 봅시다.

public class Main {
	static Map<String,Integer> hm=new HashMap();	//thread-unsafe
	public static void main(String[] ar) throws InterruptedException {
	
		Runnable runnable=new Thread() {
			@Override
			public void run(){
				hm.put("http",80);
				hm.put("ssh", 22);
				hm.put("dns", 53);
				hm.put("telnet",23);
				hm.put("ftp", 21);
				hm.forEach((key,value)->
				{
					try {
						Thread.sleep(1000);
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("{"+key+","+value+"}");
				
				});
			}
		};
		Thread thread=new Thread(runnable);
		thread.start();
		Thread.sleep(1000);
		hm.put("dhcp",67);
		System.out.println("MainThread end");

	}
}

 

보세요. 이 코드의 대한 저의 의도는 생성된 쓰레드(이하 thread라고 칭하도록 하겠습니다.)가 1초마다 run() 메소드의 저장된 키와 값을 순회하면서 출력하는 겁니다. 물론 예외없이 정상적으로요. 그러니 출력은 아래와 같아야하는 것이 이 코드의 의도입니다. 물론 MainThread가 끝나는 시점은 언제인지 예측 불가능하므로 어디서 나오든 상관없습니다. 중요한 것은 ftp~ssh까지만 출력되어야하는 것이 저의 의도입니다.

MainThread end
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}

 

하지만 우리의 예상과는 다르게 아래와 같이 출력됩니다. ConcurrentModificationException이라는 예외(Exception)가 발생하게 됩니다. 쓰레드들이 동시에 데이터를 변경할때 이 예외가 발생합니다.

MainThread end
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
{dhcp,67}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at aa.Main$1.run(Main.java:18)
	at java.base/java.lang.Thread.run(Thread.java:831)

 

왜 이런 예외가 발생할까요? 코드를 하나씩 해석해보도록 합시다.

자, HashMap은 두 스레드가 공용으로 사용할 수 있도록 static으로 지정해놓았고, 생성된 쓰레드는 해시맵에 http~ftp까지의 키와 값을 put하고 있습니다(여기서 value는 그 프로토콜의 포트번호를 의미합니다. 뭐 여기서는 중요한게 아니지요.) thread는 put으로 키,값을 저장하고 난 후에 forEach() 메소드를 이용해서 1초마다 갖고 있는 키와 값을 출력합니다. 

메인스레드에서는 thread가 forEach() 메소드를 수행할 시간을 벌어주기 위해서 1초간 잠재워줍니다. 그 후 thread는 자신의 루틴(routine) 중 forEach()메소드를 실행합니다. 

이때 thread가 forEach() 메소드가 수행하는 중에 메인 쓰레드에서 put으로 데이터를 집어넣고 있는 상황이죠. 즉, HashMap에 동시에 접근해서 변경이 발생하게 되는 그런 코드입니다. 

우리의 의도대로 동작하게 만드려면 어떻게 해야할까요? 이럴때 synchronized라는 키워드로 우리가 직접 동기화를 처리할 수도 있지만 더 간단한 방법으로는 바로 Hashtable을 사용하는 것입니다. 이 문제를 해결하려면 단 한줄만 바꾸면 됩니다.

static Map<String,Integer> hm=new Hashtable();	//thread-safe

 

물론 hm(HashMap)이라는 변수도 ht(Hashtable)로 바꾸면 더 일관성있겠죠. 그 후의 결과는 우리의 의도대로 아래와 같이 동작함을 알 수 있습니다.

{ftp,21}
{ssh,22}
{http,80}
{dns,53}
{telnet,23}
MainThread end

 

이처럼 스레드에 대해서 동시접근에 안전하려면 Hashtable을 쓰는 것이 좋고, 단일 쓰레드에서 사용한다면 HashMap을 쓰는 것이 좋습니다.

지금까지 Hashtable의 개념, 그리고 사용방법과 HashMap과의 차이점을 코드를 통해서 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

해시맵(HashMap)

해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서 키는 맵에 오직 유일하게 있어야합니다. 즉, 같은 맵에 두 개 이상의 키가 존재하면 안된다는 것입니다. 이름 그대로 열쇠이기 때문에 그 열쇠로 짝인 값(Value)를 찾아야하기 때문입니다. 값은 중복된 값이어도 상관이 없습니다. 

HashMap과 사용법이 거의 동일한 컬렉션(Collection)에는 Hashtable이 있습니다. 두 클래스의 차이점은 Thread 관점에서 안전하냐(Hashtable), 안전하지 않은 대신 속도가 빠르냐(HashMap)입니다. 여기서는 Thread-Safe하지 않은 HashMap사용법을 가장 아래에서 살펴보도록 하겠습니다. Hashtable의 개념과 사용법, Thread-Safe한 예제를 보려면 아래의 링크를 참고해주세요.

reakwon.tistory.com/152

 

자바 해시테이블(Hashtable) 개념과 사용법, HashMap과의 차이점을 코드로 이해할 수 있는 예제

해시테이블(Hashtable) 해시테이블은 해시맵(HashMap)과 함께 Map을 구현한 키와 값 쌍을 데이터로 저장한 컬렉션(Collection)입니다. 여기서 키는 유일해야만 합니다. 해시테이블은 이 키를 가지고 키에

reakwon.tistory.com

자신이 만든 객체도 키로 사용할 수 있는데, 그 사용법은 가장 아래쪽 부분에 설명되어있습니다.

 

 

map

 

Map 인터페이스를 구현한 HashMap은 키를 해싱하여 자료를 저장하고 꺼내오기 때문에 속도가 빠릅니다. 이번 포스팅에서는 사용방법 위주로 설명하도록 하겠습니다.

사용법

사용전에 HashMap과 Map은 java.util 안에 위치합니다. 두개를 import 하여 사용준비해주세요.

 

1. 키,값 저장(put), 읽기(get) 예

import java.util.HashMap;
import java.util.Map;
public class Main {
	public static void main(String[] ar) {
		Map<String,Integer> map=new HashMap();	//<키 자료형, 값 자료형>
		map.put("A", 100);
		map.put("B", 101);
		map.put("C", 102);
		map.put("C", 103); //중복된 key가 들어갈때는 이전 키,값을 지금의 것으로 업데이트
		System.out.println(map);
		System.out.println(map.get("A"));
		System.out.println(map.get("B"));
		System.out.println(map.get("C"));
	}
}

 

 

결과: 

{A=100, B=101, C=103}
100
101
103

map을 그냥 println으로 출력하게 되면 중괄호('{ }')로 묶여서 키와 값들이 출력됩니다. 기본적으로 map의 put과 get이 아주 많이 사용됩니다. map을 사용하려면 반드시 알아야하는 메소드입니다. put을 키와 값을 map에 저장하는 메소드이며 get은 입력받은 key와 대응되는 값을 돌려줍니다. 만약 해당하는 key가 없다면 null을 넘겨주게 됩니다.

 

2. containsKey 사용예 (이미 HashMap에 키가 있으면 값을 덮어쓰지 않는 예)

public static void main(String[] ar){
	Map<String,Integer> map=new HashMap();
	map.put("key1", 100);
	map.put("key2", 200);
	if(!map.containsKey("key2"))	//키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
		map.put("key2", 300); 
	System.out.println(map);
	System.out.println(map.get("key1"));
	System.out.println(map.get("key2"));
}

 

 

 

결과:

{key1=100, key2=200}
100
200

 

containsKey메소드로 키가 존재하는지 존재하지 않는지 알 수 있습니다. 이것과 비슷한 메소드로는 containsValue가 있죠. 이것은 반대로 값이 존재하는지 알아보는 메소드입니다. 존재시 true, 없을때는 false를 반환합니다. 위의 if문과 put메소드를 한꺼번에 처리할 수 있는 메소드가 존재합니다. 그래서 두 라인을 아래와 같이 바꿔써도 같은 동작을 합니다.

//if(!map.containsKey("key2"))	//키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
			//map.put("key2", 300); 
map.putIfAbsent("key2",300);

 

3. putAll 사용예 (Map에 다른 Map을 전부 포함)

public static void main(String[] ar) {
	Map<String,Integer> map1=new HashMap();
	Map<String,Integer> map2=new HashMap();
	//map1 put
	map1.put("map1-key1", 100);
	map1.put("map1-key2", 200);
		
	//map2 put
	map2.put("map2-key3", 300);
	map2.put("map2-key4", 400);
		
	System.out.println("map1:"+map1);
	System.out.println("map2:"+map2);
		
	//map2에 map1을 합침
	map2.putAll(map1);
	System.out.println("map2 includes map1:"+map2);
		
	//map1의 키, 값 변경
	map1.put("map1-key1", 1000);
	//map2에는 영향 없음.
	System.out.println("map2 includes map1:"+map2);
}

 

결과

map1:{map1-key1=100, map1-key2=200}
map2:{map2-key4=400, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}

 

아예 map을 통째로 인자로 넘겨주고 싶다면 putAll 메소드를 사용하면 됩니다. 주의해야할 점은 반드시 키와 값의 자료형이 같은 map이어야한다는 점입니다. 다른 자료형의 키, 값은 받을 수 없습니다.

 

putAll 대신 생성자를 이용해서 생성과 동시에 map의 데이터를 전부 넘겨줄 수도 있습니다.

Map<String,Integer> map2=new HashMap(map1);

 

 

 

4. keySet 사용예 (모든 키를 순회하는 코드)

list처럼 증가하는 index를 사용할 방법이 없지만 keySet메소드를 이용하여 키를 Set으로 넘겨주어 Map에 존재하는 키를 모두 순회할 수 있습니다. 

public static void main(String[] ar) {
	Map<String,Integer> map=new HashMap();
	map.put("key1",50);
	map.put("key2",100);
	map.put("key3",150);
	map.put("key4",200);
		
	System.out.println("All key-value pairs");
	for(String key:map.keySet()) {
		System.out.println("{"+key+","+map.get(key)+"}");
	}

}

 

결과

All key-value pairs
{key1,50}
{key2,100}
{key3,150}
{key4,200}

 

5. Foreach() 메소드로 순환하기

Foreach() 메소드를 사용하기전, 람다식을 이해하고 있어야합니다. 하지만 사용법만 알아도 유용하게 사용할 수 있습니다. 아래의 사용법을 보면서 익혀보세요.

public static void main(String[] ar) {
	Map<String,Integer> hm=new HashMap();
	hm.put("http",80);
	hm.put("ssh", 22);
	hm.put("dns", 53);
	hm.put("telnet",23);
	hm.put("ftp", 21);
	hm.forEach((key,value)->
	{
		System.out.println("{"+key+","+value+"}");
		
	});
	
}

 

위에 보이는 -> 가 있는 라인이 람다식입니다. key와 value를 사용하며 -> 이후 동작을 구현해주면 됩니다.

결과

{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}

 

6. 내가 만든 객체를 Key로 사용하기(나의 객체를 같은 키로 판단하는 방법)

public class Main {
	public static void main(String[] ar) {
		Person person1=new Person("reakwon","666666-7777777");
		Person person2=new Person("putty","123456-1234567");
		
		Person who=new Person("reakwon","666666-7777777");
		Map<Person,Integer> map=new HashMap();
		map.put(person1, 90);
		map.put(person2, 80);
		
		System.out.println("map includes "+who.getName()+"? "+map.containsKey(who));
	
		map.put(who, 70);
		System.out.println(map);
	}
}
class Person{
	private String name;
	private String id;
	public Person(String name,String id) {
		this.name=name;
		this.id=id;
	}
	public String getName() {
		return name;
	}
	
	@Override
	public String toString() {
		return this.name;
	}
}

 

결과

map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}

 

위의 코드에서 약간 불편한 점을 눈치 채셨나요? 저는 Person의 name과 id가 같으면 같은 키로 보고 중복처리를 하지 않을 생각이었습니다. 즉, 위의 map에는 putty와 reakwon만 있는 것이 저의 의도였고, who와 person1은 같은 키로 map이 인식해줬으면 좋겠습니다. 어떻게 구현할까요?

 

● equals() 메소드 Overriding

Object 클래스의 equals는 서로 같은 객체인지 아닌지 판별해주는 메소드입니다. String이 바로 이 메소드를 오버라이딩했지요. 그래서 문자열이 같은 경우에 equals의 결과는 true입니다. 이처럼 equals를 통해서 같은지 아닌지를 판별해주면 됩니다. 그래서 아래의 코드를 Person에 추가하면 됩니다.

	@Override
	public boolean equals(Object o) {
		if(o instanceof Person) {
			Person p=(Person)o;
			return this.id.equals(p.id) && this.name.equals(p.name);
		}
		return false;
	}

 

결과

 

 

map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}

 

분명 equals를 오버라이딩해서 같은 객체라고 명시했는대도 역시 false를 반환하고 있습니다. 어떻게 해결할 수 있을까요?

 

● hashCode() 메소드 Overriding

equals()와 쌍으로 기억해두셔야할 것은 바로 hashCode()입니다. hashCode는 각 객체가 갖는 유일한 값(Code)을 의미합니다. Object의 hashCode()는 원래 주소값에 의한 hashCode()로 각 객체가 전부 다른값을 가지고 있습니다. HashMap은 우선 hashCode를 비교하고 같을 때만 equals를 수행하여 정말 제대로 같은것인지 판별합니다. 그래서 HashMap은 애초에 hashCode()가 반환하는 값이 다르면 equals는 수행하지도 않습니다.

그래서 위 코드에서 같은 해시코드를 오버라이딩해서 name과 id에 따라 다른 hashCode를 갖게 만들어주어야합니다. 구현의 편의성을 위해서 String클래스를 사용합니다. String 클래스가 이미 그렇게 다 구현(문자열이 같으면 hashCode도 같고 equals도 true) 이 되어있습니다. 우리는 위의 예제에서 String을 키로 쓴적이 있었죠. String 클래스는 hashCode와 equals가 문자열이 같을때 같은 객체라고 판별할 수 있도록 두 메소드를 전부 재정의한 상태입니다.

그래서 우리는 아래의 코드를 위의 코드 대신 추가해주면 됩니다.

	@Override
	public int hashCode() {
		return name.hashCode()+id.hashCode();
	}	
	
	@Override
	public boolean equals(Object o) {
		return this.hashCode()==o.hashCode();
	}

 

 

 

 

결과

map includes reakwon? true
{reakwon=70, putty=80}

 

위의 코드는 대체적으로 잘 동작하지만 만약 운이 굉장히 좋지 않을 경우 다른 객체로 의도했던 것이 같은 객체로 인식될 수 있습니다. String 클래스의 hashCode의 구현 방식때문이지요. 하지만 이해를 돕기 위해서 더 완벽한 구현은 하지 않았습니다. 귀찮기도 하구요.

알아두셔야할 점은 은 객체를 판별하는 코드를 넣고자하여 equals를 재정의할때 반드시 hashCode도 다시 정의해주어야한다는 점을 기억하세요.

 

HashMap의 Thread-Unsafe 예

아래의 코드를 봅시다.

public class Main {
	static Map<String,String> hashmap=new HashMap();
	public static void main(String[] ar) throws InterruptedException {
	
		Runnable runnable=new Thread() {
			@Override
			public void run(){
				hashmap.put("seoul","02");
				hashmap.put("kyeongkido", "031");
				hashmap.put("busan", "051");
				hashmap.put("daejeon","042");
				hashmap.forEach((key,value)->
				{
					try {
						Thread.sleep(1000);
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("{"+key+","+value+"}");
				
				});
			}
		};
		Thread thread=new Thread(runnable);
		thread.start();
		Thread.sleep(1000);
		hashmap.put("jeju","064");
	}
}

 

우선 Thread의 개념부터 잡아야합니다. 자바 쓰레드의 개념과 사용방법을 알아보려면 아래의 링크로 가서 학습해봅시다.

reakwon.tistory.com/84

 

[JAVA/자바] 쓰레드(Thread) 다루기( Thread상속, Runnable 구현, join)

쓰레드(Thread) 쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레

reakwon.tistory.com

 

코드에 대한 결과는 예외를 던지며 프로그램이 종료됩니다. 아래와 같이 말이죠.

{seoul,02}
{daejeon,042}
{busan,051}
{jeju,064}
{kyeongkido,031}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at aa.Main$1.run(Main.java:17)
	at java.base/java.lang.Thread.run(Thread.java:831)

위 코드에 대한 의도는 thread라는 쓰레드에서 hashmap의 저장된 데이터 seoul ~ daejeon의 데이터를 모두 출력하는 의도입니다. 하지만 메인 쓰레드와 thread간의 HashMap에 동시에 접근했기 때문에 위 코드에 예외가 발생하게 된것입니다. 왜 동시에 접근했을까요? 

메인 스레드는 thread의 run() 메소드가 forEach가 수행할 수 있도록 잠시 1초간 기다려줍니다. 그리고 thread는 1초마다 저장했던 데이터들을 출력합니다. 그런데 그때 메인스레드에서 put() 메소드로 데이터를 저장하는 상황이 발생했습니다. 그래서 두 쓰레드가 동시에 HashMap에 접근하게 된것이죠.

이처럼 HashMap은 쓰레드에 대해서 안전하지 않은 컬렉션입니다. 오직 단일 쓰레드에서만 사용하면 안전한 컬렉션이죠. 이를 보완한 컬렉션이 바로 Hashtable입니다. 위의 링크로 들어가서 Hashtable로 이 문제를 해결한 코드를 확인해보세요.

이상으로 HashMap에 대한 기본적인 개념과 사용법 위주로 알아보았습니다. 이 정도만 알고 있어도 HashMap을 잘 사용할 수 있을 것으로 생각합니다. 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

하한(Lower bound), 상한(Upper bound)

하한, 상한은 배열이 이미 정렬된 상태에서 적용되어야합니다. 상한, 하한은 모두 이진 탐색(Binary Search)로 구현이 되어있기 때문이죠. 여기서 이진 탐색은 아래의 코드인 것을 알고 계시겠죠? 이진 탐색의 기본 코드는 아래와 같습니다. 이진 탐색의 탐색 속도는 O(lg 2)의 속도를 자랑하지요. 

 

 

int BinarySearch(int s, int e, int number)
{
    int left=s,right=e,mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] == number) return mid;
        if (arr[mid] > n) right = mid -1;
        else left = mid + 1;
    }
    return -1;
}

 

하한이란? (Lower Bound)

특정 값 K보다 같거나 큰 값이 처음 나오는 위치가 됩니다. 즉, K값 이상인 값이 맨 처음 나오는 위치여야합니다. 아래는 그 예를 나타내는 표입니다. 이때 맨 앞 요소의 index는 0으로 시작합니다.

배열
1 2 2 4 4 5 6 6 6 9 9
lower_bound
lower bound( 4 )  3
lower bound( 5 )  5
lower bound( 9 )  9

하한을 Binary Search방식으로 구현한 코드는 아래와 같은데요.  number값을 이상인 값이 mid 위치에서 발견되었다면  왼쪽으로 더 가다보면 아예 작은 수를 만나게 되고 left와 right의 역전 현상이 발생되어 멈춥니다. 멈추기 전에 우리는 가장 마지막에 arr[mid] >= number인 상황을 lower_bnd에 저장한 상황이죠. 

int Binary_Search_Lower_Bound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int lower_bnd = -1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] >= number)
        {
            lower_bnd = mid;
            right = mid - 1;
        }
        else left = mid + 1;
    }
    return lower_bnd;
}

범위를 좁혀가는 상황을 그림으로 표현했습니다.

 

- C++의 lower_bound 사용

C++에서는 라이브러리 함수 형태로 lower bound를 지원합니다. 아래와 같은 형식을 사용하게 되는데요. 사용법은 이렇습니다. 


lower_bound( 정렬한 배열 포인터, 정렬한 배열 포인터의 끝 주소, 값) - 정렬한 배열 포인터

여기서 정렬한 배열의 포인터는 알겠는데, 정렬한 배열 포인터의 끝 주소값은 어떻게 구할까요? 그냥 마지막 원소의 주소를 넣어주거나 배열의 주소 + 배열의 사이즈로 넣어주면 됩니다. 

lower_bound의 반환 값은 하한값의 주소를 돌려주기 때문에 만약 index를 얻으려면 정렬한 배열 포인터를 빼주면 됩니다. 이 함수의 사용법은 밑의 예제 코드에서 upper_bound의 함수와 같이 사용해보도록 하겠습니다.

상한이란? (Upper Bound)

특정 값 K보다 큰 값이 처음으로 나오는 위치가 됩니다. 즉, K값 초과인 값이 맨 처음으로 나오는 위치입니다. 몇 가지 예를 들어볼까요?

배열
1 2 2 4 4 5 6 6 6 9 9
upper_bound
upper bound( 4 ) 5
upper bound( 5 ) 6
upper bound( 9 ) -1 

이 역시 Binary Search로 구현되는데요. 구현한 모습은 아래와 같습니다. 중간의 비교문이 반대로 됐음을 알 수 있죠? 이것은 arr[mid]의 값이 number의 이하일때 number와 최대한 같은 값의 index를 찾기 위함입니다.

 

 

int Binary_Search_Upper_Bound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int upper_bnd=-1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] <= number)
        {
            upper_bnd = mid;
            left = mid + 1;
        }
        else right = mid - 1;
    }
    return upper_bnd+1 >= N ? -1: upper_bnd+1;
}

 

만약 이렇게 돼서 while루프가 멈추었다면 arr[upper_bnd]값은 number값 초과 직전의 값일 것입니다. 그렇다면 arr[upper_bnd+1]의 값은 number보다 큰 값일 테지요. 그래서 마지막 return에서 +1을 해주는데, 이것이 배열의 사이즈를 넘어갈 수 있으니까 사이즈 체크하여 return합니다. 

C++에서 당연히 lower_bound함수가 존재하듯 upper_bound의 함수 역시 존재하며 사용법은 동일합니다. 단, 아래와 같이 사용할때 상한 값을 못찾아줄때도 있는데, 이때는 배열의 인덱스를 넘어가는 값을 갖게됩니다.


  upper_bound(정렬한 배열 포인터, 정렬한 배열 포인터의 끝 주소, 값) - 정렬한 배열 포인터

 

이제까지 설명한 내용을 예제 코드를 통해서 확인해보세요. 직접 구현한 BinarySearch 형식의 upper_bound, lower_bound도 같이 구현되어있습니다.

#include <iostream>
#include <algorithm>

using namespace std;

int arr[100];
int N = 11;
int BinarySearchLowerBound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int lower_bnd = -1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] >= number)
        {
            lower_bnd = mid;
            right = mid - 1;
        }
        else left = mid + 1;
    }
    return lower_bnd;
}

int BinarySearchUpperBound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int upper_bnd=-1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] <= number)
        {
            upper_bnd = mid;
            left = mid + 1;
        }
        else right = mid - 1;
    }
    return upper_bnd+1 >= N ? -1: upper_bnd+1;
}


int main(void) {


    arr[0] = 4; arr[1] = 2; arr[2] = 4; arr[3] = 1; arr[4] = 9;
    arr[5] = 2; arr[6] = 6; arr[7] = 6; arr[8] = 6; arr[9] = 9, arr[10] = 5;
    
    //정렬
    sort(arr, arr + N);

    printf("정렬된 배열\n");
    for (int i = 0; i < N; i++) printf("%d ",arr[i]);
    printf("\n");
    
    printf("Lower Bound\n");

    printf("%d lower bound:%d\n", 4, BinarySearchLowerBound(0, N - 1, 4));
    printf("%d lower bound:%d\n", 5, BinarySearchLowerBound(0, N - 1, 5));
    printf("%d lower bound:%d\n", 2, BinarySearchLowerBound(0, N - 1, 2));
    printf("%d lower bound:%d\n", 1, BinarySearchLowerBound(0, N - 1, 1));
    printf("%d lower bound:%d\n", 9, BinarySearchLowerBound(0, N - 1, 9));

    printf("%d lower bound:%d\n", 4, lower_bound(arr, arr + N, 4) - arr);
    printf("%d lower bound:%d\n", 5, lower_bound(arr, arr + N, 5) - arr);
    printf("%d lower bound:%d\n", 2, lower_bound(arr, arr + N, 2) - arr);
    printf("%d lower bound:%d\n", 1, lower_bound(arr, arr + N, 1) - arr);
    printf("%d lower bound:%d\n", 9, lower_bound(arr, arr + N, 9) - arr);
  

    printf("Upper Bound\n");

    printf("%d upper bound:%d\n", 4, BinarySearchUpperBound(0, N - 1, 4));
    printf("%d upper bound:%d\n", 5, BinarySearchUpperBound(0, N - 1, 5));
    printf("%d upper bound:%d\n", 2, BinarySearchUpperBound(0, N - 1, 2));
    printf("%d upper bound:%d\n", 1, BinarySearchUpperBound(0, N - 1, 1));
    printf("%d upper bound:%d\n", 9, BinarySearchUpperBound(0, N - 1, 9));

    printf("%d upper bound:%d\n", 4, upper_bound(arr, arr + N, 4) - arr);
    printf("%d upper bound:%d\n", 5, upper_bound(arr, arr + N, 5) - arr);
    printf("%d upper bound:%d\n", 2, upper_bound(arr, arr + N, 2) - arr);
    printf("%d upper bound:%d\n", 1, upper_bound(arr, arr + N, 1) - arr);
    printf("%d upper bound:%d\n", 9, upper_bound(arr, arr + N, 9) - arr);   //배열의 인덱스 초과된 값이 나옴
  
}

 

 

그리고 위 코드의 결과입니다. 마지막 upper_bound의 값이 다름을 확인해보세요.

 

 

전체 코드의 결과

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

동적 메모리 할당,해제를 맡는 new, delete 키워드

C에서 메모리의 동적할당을 할때 malloc 함수를 사용하지요. 동적할당에 대해 모르신다면 저의 동적할당에 대한 포스팅을 보고 오시기 바랍니다.

reakwon.tistory.com/20

 

[C언어] 동적 메모리 할당의 세가지 방법 malloc, calloc, realloc

동적 메모리 할당 우리는 이제껏 메모리를 할당할때 정적으로 할당했습니다. 어떤 것이냐면 int arr[100]; 이렇게 할당을 했었죠. 뭐 문제없습니다. 실행도 잘 되구요. 하지만 이런 상황은 조금 불

reakwon.tistory.com

 

C언어에서 malloc을 사용할때 반환하는 void*를 형변환하여 사용하였습니다. 그리고 동적할당된 메모리를 해제할땐 free를 사용했습니다. C++에서는 이러한 동적할당에 관한 사용이 자주 이루어지기 때문에 아예 키워드로 지정해놓았습니다. malloc을 대체하는 키워드는 new, free를 대체하는 키워드는 delete입니다. 사용되는 방식은 다음과 같습니다.

MyClass* myclass = new MyClass();
delete(myclass);

여기서 malloc과 new는 동적할당하는 것은 같지만 malloc은 함수를 call하는 것이고 new는 C++에서 기본적으로 제공하는 키워드이기 때문에 별도의 라이브러리를 추가할 필요가 없습니다. 또한 new는 생성자를 자동호출하는 특징이 있지요. 이제 C++에서는 객체를 할당할때 malloc으로 하지 않고 new를 이용하여 할당합니다.

오버라이딩(Overriding)

Class의 상속에 대해서 배우셨나요? 그렇다면 이제 다형성을 배워볼 차례입니다. 다형성이라하면 여러 형태를 갖는 객체의 특징을 말하는데요. 아래의 코드를 가지고 다형성과 관련한 클래스의 특징들을 천천히 살펴보도록 합시다.

#include<iostream>
using namespace std;

class Animal {
private:
	int height;
	int weight;
public:
	Animal(int _height, int _weight) :height(_height), weight(_weight) {}
	void printInfo() {
		cout << "==============정보=============" << endl; ;
		cout << "키:" << height << "무게:" << weight << endl;
	}
	int getHeight() {
		return height;
	}
	int getWeight() {
		return weight;
	}
};

class Human :public Animal {
private:
	int race;
public:
	Human(int _height, int _weight, int _race) :Animal(_height, _weight) {
		race = _race;
	}
	void printInfo() {
		cout << "==============정보=============" << endl;
		cout << "키:" << getHeight() << "무게:" << getWeight() << endl;
		cout << "인종:";
		if (race == 0)
			cout << "황인" << endl;
		else if (race == 1)
			cout << "흑인" << endl;
		else if (race == 2)
			cout << "백인" << endl;
		else
			cout << "혼혈" << endl;
	}
};
int main() {
	
	Animal* animal = new Animal(50, 20);
	animal->printInfo();

	Human* human = new Human(150, 80, 3);
	human->printInfo();
	delete(animal);
	delete(human);
}

 

여기서 Animal클래스는 Human클래스의 부모 클래스입니다. main에서는 둘 다 new 키워드로 생성해서 정보를 출력하는 코드네요. Human 클래스에서 printInfo 메소드를 보면 cout 두줄이 정확히 Animal의 printInfo클래스의 메소드와 같은 것을 알 수 있습니다. 

어차피 Human클래스는 Animal클래스를 상속받고 있으니, Animal클래스의 printInfo를 재활용 할 순 없을까요? 그럴때 부모클래스::메소드명 을 호출하여 부모클래스의 메소드를 사용할 수 있습니다. Human 클래스의 메소드를 아래처럼 바꿔봅시다.

void printInfo() {
	Animal::printInfo();
	cout << "인종:";
	if (race == 0)
		cout << "황인" << endl;
	else if (race == 1)
		cout << "흑인" << endl;
	else if (race == 2)
		cout << "백인" << endl;
	else
		cout << "혼혈" << endl;
}

 

이처럼 부모클래스의 함수를 이용하여 자식 클래스에서 같은 메소드 이름으로 새로운 기능을 덧붙이는 방식을 오버라이딩이라고 합니다.

 

 

다형성(Polymorphism)

OOP(Object-Oriented Programming)은 현실 세계를 반영한다는 슬로건을 가지고 있습니다. 위의 코드를 현실세계와 연관지어 생각해봅시다. 우리 사람은 동물의 한 종이죠. 그래서 동물의 속성을 갖고 있습니다. 하지만 동물은 사람이 아니죠. 동물에는 사람외에도 개와 고양이 등 많이 있기 때문인데요. 정리하면 이렇게 되겠네요.

사람은 동물이다.(O)

동물은 사람이다.(X)

위에서 성립이 되는 상황(사람은 동물)을 C++에서는 아래와 같이 표현할 수 있습니다. main함수안의 내용을 아래와 같이 바꿔서 실행해보시기 바랍니다.

Animal* human = new Human(150, 80, 3);
human->printInfo();

 

이렇게 부모클래스를 통해 만들어진 객체가 자신을 상속받는 여러 클래스의 객체로 모양을 띄는 것을 다형성이라고 합니다. OOP에서 매우 중요한 개념입니다.

위의 코드를 수행한 결과에서 한가지 불편한 점이 있는데요. 저희는 human->printInfo()가 당연히 Human클래스의 printInfo를 출력한다는 믿음으로 프로그래밍을 했는데, 막상 결과는 Animal의 printInfo가 호출됩니다. 어떻게 해야지 우리가 원하는 동작을 할 수 있을까요?

virtual 키워드

만약 자신을 상속받는 자식 클래스의 객체가 자신의 메소드를 사용하는데, 이것을 오버라이딩했다면 자식 클래스의 객체 메소드를 호출하라고 지정하는 방법은 메소드 앞에 virtual 키워드를 사용하는 것입니다. 자신의 메소드는 가상으로 만들어져 있으니 자식의 메소드를 호출하라는 의미가 되겠죠. 그래서 우리의 목표를 달성하기 위해서는 Animal 클래스의 printInfo메소드 앞에 virtual 키워드를 추가하면 됩니다. 그렇게 되면 Human의 printInfo 메소드를 호출하게 됩니다.

class Animal {
private:
	int height;
	int weight;
public:
	Animal(int _height, int _weight) :height(_height), weight(_weight) {}
	virtual void printInfo() {
		cout << "==============정보=============" << endl; ;
		cout << "키:" << height << "무게:" << weight << endl;
	}
	int getHeight() {
		return height;
	}
	int getWeight() {
		return weight;
	}
};

 

virual키워드를 사용해야 자식 객체의 메소드를 사용한다고 했죠? 그렇다면 다음의 상황을 예측해봅시다.

class Human :public Animal {
private:
	int race;
public:
	//...생략...//
	void printInfo() {
		//...생략...//
	}
};

class Student :public Human {
private:
	char grade;
public:
	Student(int _height, int _weight, int _race,char _grade) :Human(_height, _weight, _race) {
		grade = _grade;
	}

	void printInfo() {
		Human::printInfo();
		cout << "성적:" << grade << endl;
	}
};

 

Human 클래스는 printInfo에 virtual키워드를 넣지 않았습니다. Student는 Human클래스를 상속받고 있는데, 이때 printInfo는 Human의 printInfo를 호출할까요? 메소드의 virtual이 지정되면 이후 자식은 자동으로 virtual 키워드가 적용이 됩니다. 그래서 결론은 Student의 printInfo가 호출이 되지요. 하지만 명시적으로 virtual을 지정해주는 것이 관례입니다. 코드의 가독성과 이해를 돕기 위해서지요.

자, 이런 다형성은 언제 필요할까요? 예를 들면 함수에서 해당 클래스를 상속하는 모든 객체를 받을 때가 그 예가 됩니다. 아래의 함수에서 변수로 animal를 가리키는 포인터를 받습니다. 이 함수의 목적은 animal인 객체의 printInfo를 출력하는 것인데, Animal클래스를 상속하는 모든 객체를 받을 수 있습니다. 

void printInfo(Animal *animal) {
	animal->printInfo();
}

 

 

this 포인터

객체가 생성될때 자기 자신을 가리키는 포인터가 this 포인터라고 합니다. 만일 아래와 같은 상황에서 this를 사용할 수 있습니다. 저는 생성자에서 객체의 grade를 전달받은 grade의 값으로 입력하고 싶어서 아래와 같은 코드를 사용했습니다.

class Student :public Human {
private:
	char grade;
	Student(int _height, int _weight, int _race,char grade) :Human(_height, _weight, _race) {
		grade = grade;
	}
//...생략...//
};

 

여기서 객체의 grade는 절대 변할 수 없습니다. 객체의 grade보다 매개 변수인 grade가 우선시 되기 때문에 매개변수 grade에 다시 매개변수의 grade의 값을 넣기 때문입니다. 이럴때 사용할 수 있는 것이 바로 this포인터를 사용하는 것이죠.

Student(int _height, int _weight, int _race,char grade) :Human(_height, _weight, _race) {
	this->grade = grade;
}

this포인터는 객체가 생성되고 난 이후에 그 효력을 발휘합니다. 그 객체의 주소를 나타내야하기 때문이죠. 아래와 같이 한번 주소를 찍어봅시다. 정확히 같은 곳을 가리키는 것을 알 수 있습니다.

class Student :public Human {
private:
	char grade;
public:
	Student(int _height, int _weight, int _race,char grade) :Human(_height, _weight, _race) {
		cout <<"this 포인터의 주소"<< this << endl;
		this->grade = grade;
	}
	//...생략...//
};

int main() {
	
	Animal* student = new Student(165, 55, 0, 'A');
	cout <<"student의 주소"<< student << endl;
	student->printInfo();
	delete(student);
}

 

이상 다형성과 관련된 포스팅을 마치도록 하겠습니다. 이해가 되지 않는 부분은 댓글로 남겨주세요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

C++ 상속(Inheritance)

다른 여타의 객체지향언어와 같이 C++ 역시 상속을 할 수 있습니다. 여러분은 현실 생활에서 상속이라는 개념을 알고있으신가요? 부모님으로부터 100억을 상속을 받으셨다면 이 포스팅을 볼 필요없으십니다. 저랑 친구해요.

객체지향 프로그래밍에서는 부모 클래스의 맴버 변수와 메소드를 자식 클래스가 재사용하는 개념으로 알고 계시면 됩니다. 이 맴버 변수나 메소드에 접근 제한자를 더하여 아무나 접근할 수 없도록 할 수 있습니다. C++에서 상속하는 방법은 ' : ' 콜론을 사용하여 상속을 받을 수 있습니다. JAVA에서 extends를 사용하여 상속을 하는 것과 같습니다. 형식은 아래와 같습니다.

class 클래스_이름 : 접근제한자 부모_클래스명{
	//.. 내용 ..//
}

 

접근 제한자

접근 제한자는 세가지가 있습니다. private, protected, public이라는 접근 제한자가 있지요. 

접근 제한자 설명
private 오직 자신의 클래스안에서만 접근 가능한 멤버 변수, 메소드.
protected 자신을 상속하는 클래스까지만 접근 가능
public 어떤 클래스에서나 접근 가능

접근 범위

 

생성자 (Constructor)

클래스가 객체를 생성할때 항상 호출하는 일종의 메소드가 있습니다. 이 메소드는 생성자라는 이름을 갖고 있습니다. 여러분이 객체를 생성할때 초기화하는 방법을 생성자에 기재합니다. 이를 테면, 각 맴버 변수들의 초기화 등이 있지요. 형식은 아래와 같습니다. 자신의 클래스명과 동일하지만 반환이 없는 메소드가 바로 생성자입니다.  생성자를 사용하려면 반드시 public으로 해야합니다. 모든 곳에서 접근 가능해야 초기화를 진행할 수 있기 때문이죠. 

여러분들이 굳이 생성자를 정의해놓지 않아도 C++은 알아서 기본 생성자(Default Constructor) 만들어줍니다. 비록 아무런 동작을 하지 않는 껍데기에 불과하지만요.

class 클래스명 {
public :
	클래스명(파라미터){
    	//..동작 ..//
    }
}

 

소멸자(Destructor)

소멸자는 객체가 소멸할때 마지막 최종작업을 기록하는 메소드를 말하며 아래 형식과 같습니다. 소멸자는 반환값, 전달받는 인자가 없고 ~클래스이름() 형태여야합니다. 주로 free, delete 등의 메모리 누수가 발생하지 않게 메모리 해제 작업등이 여기에 포함됩니다.

class A{
public:
	~A(){
    	//..메모리 해제등의 작업 ..//
    }
}

 

 

 

이제 여기서 간단한 예를 들어보도록 하겠습니다. 

#include <iostream>
using namespace std;

class A {
private:
	int a;
	int b;
public:
	A(int _a, int _b){
		a = _a;
		b = _b;
	}
	int add() {
		return a + b;
	}
};

class B : A {
public :
	B(int _a, int _b) : A(_a, _b) {}
	void printResult() {
		//A를 상속받았으니, 부모클래스의 add라는 메소드를 사용할 수 있음.
		printf("%d\n", add());
	}
};

int main() {
	B b(1, 2);
	b.printResult();
}

 

A는 자신만이 접근한 맴버 변수 a와 b를 가지고 있습니다. 그리고 생성자를 갖고 있는데, 여기서 a와 b를 초기화시켜줍니다. 그리고 누구나 접근 가능한(public) 메소드 add를 갖고 있습니다. 이 메소드는 단순 a와 b를 더한 값을 전달하지요. 맴버를 초기화할 수 있는 방법은 이런 방법도 있습니다. 

public :
	A(int _a, int _b):a(_a),b(_b) {
		
	}

B는 A 클래스를 부모 클래스(또는 Super Class라고 합니다.)로 상속을 받습니다. 이때 부모 클래스의 초기화를 하는 방법은 : 부모클래스명(매개변수명1, 매개변수명2 ...)으로 초기화 시킬 수 있습니다. 자, 여기서 눈여겨 보셔야할 점은 B는 A클래스를 상속받았기 때문에 a와 b는 갖고 있지만, 접근할 수 없습니다. 

printResult라는 메소드에서는 add()라는 메소드를 사용할 수 있는데 A의 클래스에서 정의하고 public으로 접근할 수 있게 만들었기 때문에 B클래스에서 사용할 수 있습니다.

접근 제한자를 통한 상속

우리는 위의 예제에서 접근 제한자는 따로 지정하지 않고 상속을 받았습니다. 상속받는 부분에서 접근 제한자를 지정하게 되면 부모 클래스의 맴버 변수, 메소드를 지정한 접근 제한자보다 범위가 넓은 맴버 변수, 메소드에 접근할 수가 없습니다. public을 명시적으로 지정해주어야만 다형성의 속성을 사용할 수 있습니다.

private 상속 

#include <iostream>
using namespace std;

class A {
private:
	int a;
protected:
	int b;
public:
	int c;
};

class B : private A { //b,c맴버 변수는 private 맴버로 접근 범위 졻혀짐

};


int main() {
	B b;
    //a = private, b = private, c = private
	b.a;
	b.b;
	b.c;
}

 

 

 

B는 A를 private로 상속받습니다. A의 맴버변수 b,c는 private보다 범위가 넓으므로 b,c는 private로 제한합니다. 따라서 a,b,c 모두 접근할 수 없습니다.

protected 상속

#include <iostream>
using namespace std;

class A {
private:
	int a;
protected:
	int b;
public:
	int c;
};

class B : protected A { //c맴버 변수는 protected 맴버로 접근 범위 졻혀짐

};


int main() {
	B b;

	//a = private, b = protected, c = protected
	b.a;
	b.b;
	b.c;
}

A의 맴버 변수 중에 protected보다 범위가 넓은 맴버는 protected로 접근이 제한됩니다.

public 상속

#include <iostream>
using namespace std;

class A {
private:
	int a;
protected:
	int b;
public:
	int c;
};

class B : public A { //맴버 변수의 접근 제한에 변화없음

};


int main() {
	B b;

	//a = private, b = protected, c = public
	b.a;
	b.b;
	b.c;
}

public보다 접근 제한이 넓은 맴버는 public으로 맞춰지는데, 의미없죠? public보다 큰 제한자는 없으니까요. 

 

캡슐화(Encapsulation)

캡슐화라는 것은 맴버 변수를 직접 변경할 수 없도록 캡슐처럼 껍데기를 둘러싸는 과정을 말합니다. 캡슐안에서 특정 로직에 따라 맴버 변수가 적절하게 변경되어야 프로그램이 안전할 수 있기 때문이죠. 아래의 코드가 그 예를 보여줍니다.

#include <iostream>
using namespace std;

class A {
private:
	int a;
	int b;
public:
	void setA(int _a) {
		if (_a > 50)
			_a = 50;
		a = _a;
	}
	void setB(int _b) {
		if (_b > 100)
			_b = 100;
		b = _b;
	}

	int getA() {
		return a;
	}
	int getB() {
		return b;
	}
};

class B : A {
public :
	void setAB(int a,int b) {
		setA(a);
		setB(b);
	}
	void printResult() {
		printf("%d + %d = %d\n", getA(), getB(), getA() + getB());
	}
};


int main() {
	B b;
	b.setAB(100, 200);
	b.printResult();
}

 

간단한 코드인데요. B는 A를 상속받습니다. 이때 a,b는 직접 설정할 수 없게 private로 접근을 제한했구요. 메인 함수에서는 b에서 setAB를 호출해서 a의 값을 100, b의 값을 200으로 바꾸려고 합니다. 하지만 A 클래스는 그것을 용납하지 않습니다. a의 최대값은 50, b의 최대값은 100으로 제한을 해놓았기 때문이죠. B가 a와 b를 변경할 수 있는 수단은 A 클래스의 setA와 setB를 호출하여 인자 전달을 하는 방법밖에 없습니다. 이렇게 맴버의 값을 함부로 변경할 수 없도록 맴버 함수로 껍질을 입히는 작업을 캡슐화라고 합니다. 이렇게 Get, Set 메소드를 Getter, Setter메소드라고 합니다.

객체지향 언어에서 반드시 필요한 속성임을 기억하세요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

 

SimpleDateFormat에는 문자열로 형식을 전달하는 생성자가 있는데 아래의 예시를 참고하면 될것.

 

Date and Time PatternResult

"yyyy.MM.dd G 'at' HH:mm:ss z" 2001.07.04 AD at 12:08:56 PDT
"EEE, MMM d, ''yy" Wed, Jul 4, '01
"h:mm a" 12:08 PM
"hh 'o''clock' a, zzzz" 12 o'clock PM, Pacific Daylight Time
"K:mm a, z" 0:08 PM, PDT
"yyyyy.MMMMM.dd GGG hh:mm aaa" 02001.July.04 AD 12:08 PM
"EEE, d MMM yyyy HH:mm:ss Z" Wed, 4 Jul 2001 12:08:56 -0700
"yyMMddHHmmssZ" 010704120856-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSZ" 2001-07-04T12:08:56.235-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX" 2001-07-04T12:08:56.235-07:00
"YYYY-'W'ww-u" 2001-W27-3

 

1) SimpleDateFormat와 Date로 현재 시간을 구하는 코드

SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd a hh:mm:ss");
Date date=new Date();
		
System.out.println("now  ("+format.format(date)+")");

 

2) System.currentTimeMillis 메소드를 이용하여 현재 시스템 시간을 구하는 코드

SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd a hh:mm:ss");
Date date=new Date(System.currentTimeMillis());
		
System.out.println("now  ("+format.format(date)+")");

 

3) 반대로 long형태의 millisecond로 반환하는 코드

String str = "2020-01-31 14:20:59";
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;

try{
	date = format.parse(str);
}catch(ParseException e){}

long timeMillis = date.getTime();
System.out.println("time millis : "+timeMillis);
		

 

위 코드는 2020년 1월 31일 오후 2시 20분 59초를 밀리세컨드로 반환하는데 어떤 시간을 기준으로 반환할까?

 

아래 코드를 실행해보면 답이 나온다.

String str = "1970-01-01 09:00:00";
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;

try{
	date = format.parse(str);
}catch(ParseException e){}

long timeMillis = date.getTime();
System.out.println("time millis : "+timeMillis);

 

아래와 같이 시간 차이(계산)도 쉽게 구할 수 있다. 

final long daySeconds = 86400L;
String str = "1991-09-21 03:00:00";
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date birthDay = null;

try{
	birthDay = format.parse(str);
}catch(ParseException e){}

long birthDayMillis = birthDay.getTime();
long now = System.currentTimeMillis();
long passed = ((now/1000L) - (birthDayMillis/1000L)) / daySeconds;
System.out.println("개 늙었네 : "+passed);

 

 

위의 소스에서 now나 birthdayMillis는 milliseconds단위이므로 1000을 곱하여 시간초로 바꾸고 서로 빼면 지금까지 흐른 초가 나오겠죠? 그걸 또 하루로 나눠버리면 , 아 참고로 하루는 86400초, 지금까지 흐른 일 수가 나오죠? 

 

4) 현재 SimpleDateFormat의 Pattern 알아보기

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("pattern : "+format.toPattern());

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

정렬

우리는 정렬에 관해서 배우기도 하였고 구현도 해보았습니다. 그래서 어떻게 정렬이 되는지 알고 있죠. 하지만 실제 프로그래밍하는 상황에서 이 정렬을 직접구현해서 프로그램을 만들지는 않을 거에요. 우리에겐 JAVA에서 제공하는 정렬 메소드가 있기 때문이죠. 저희보다 똑똑한 사람들이 만들어 놓은 것이니 우리는 그저 감사하며 사용하면됩니다. 그 전에 사용법을 알아야 잘 사용할 수 있겠죠? 이제부터 어떻게 사용하는지 알아보도록 합시다.

 

Collections.sort

Collections.sort의 메소드를 이용해서 정렬할 수 있습니다. 바로 기본 자료형의 wrapper 클래스의 객체들은 전부 이 Collections.sort를 통해서 알아서 정렬이 가능합니다. 

바로 예제를 통해 확인하도록 하지요.

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class Sort {

	public static void printList(String title,List list){
		System.out.println(title);
		System.out.println(list);
	}
	public static void main(String []ar){
		List<Integer> intList=new ArrayList<>();
		
		intList.add(4);
		intList.add(5);
		intList.add(1);
		intList.add(8);
		intList.add(3);
		
		printList("정수 정렬 전",intList);
		
		Collections.sort(intList);
		
		printList("정수 정렬 후",intList);
		
		System.out.println("==============================");
		
		List<String> strList=new ArrayList<>();
		
		strList.add("de");
		strList.add("dc");
		strList.add("ci");
		strList.add("ad");
		strList.add("aa");
		
		printList("문자열 정렬 전",strList);
		
		Collections.sort(strList);
		
		printList("문자열 정렬 후",strList);
		
		
	}
}

 정렬이 잘되는 것을 아래의 결과를 통해서 확인할 수 있네요. 

실행결과

정수 정렬 전
[4, 5, 1, 8, 3]
정수 정렬 후
[1, 3, 4, 5, 8]
==============================
문자열 정렬 전
[de, dc, ci, ad, aa]
문자열 정렬 후
[aa, ad, ci, dc, de]

 

 

Collections와 ArrayList를 이용한 객체 정렬

하지만 우리는 기본 자료형을 정렬하는 것이 아닌 우리가 만든 클래스의 객체를 정렬하려는 상황이 발생한다면 어떻게 하면 좋을까요?

아래와 같은 User라는 클래스가 있다고 칩시다.

class User{
	public String name;
	public int age;
	public User(String name,int age){
		this.name=name;
		this.age=age;
	}
	@Override
	public String toString(){
		return "(name:"+name+", age:"+age+")";
	}
}

그리고 이 클래스의 객체를 리스트에 5개를 담도록 하겠습니다. 

List<User> list=new ArrayList();

list.add(new User("A",40));
list.add(new User("B",25));
list.add(new User("C",10));
list.add(new User("D",30));
list.add(new User("E",26));

System.out.println(list);

그리고 출력한다면 뭐 뻔한 결과가 나올테죠.

실행결과

[(name:A, age:40), (name:B, age:25), (name:C, age:10), (name:D, age:30), (name:E, age:26)]

이 User의 객체들을 나이순으로 정렬하고 싶습니다(참고로 꼰대는 아니구요). 어떻게 정렬할 수 있을까요?

 

Comparator 구현

Collections.sort를 입력했을때 eclipse에 나오는 recommended method의 2번째 메소드를 보시기바랍니다. Comparator라는 객체를 받고 있는 것을 확인할 수 있습니다.

 

네, 답은 Comparator를 구현하면 이 상황을 해결할 수 있습니다. 

그러면 바로 구현해보도록 합시다. 구현할 것이 메소드 하나 compare밖에 없어요. 그 내부 코드양도 얼마되지 않습니다.

class UserComparator implements Comparator<User>{
	@Override
	public int compare(User a,User b){
		if(a.age>b.age) return 1;
		if(a.age<b.age) return -1;
		return 0;
	}
}

compare는 2개의 인자를 받는데 이 인자들은 우리가 앞서 구현했던 User객체입니다. 만약 a라는 객체가 b라는 객체보다 더 크다면 1, 작다면 -1, 같다면 0을 반환하도록 합니다. 이게 오름차순의 방법이고, 만약 내림차순으로 구현하고 싶다면 반대로 구현하면 되겠죠? (모르면 그냥 찍어서 return하고 원하는 결과와 반대다 싶으면 return을 반대로 해줍시다^^.)

오름차순

첫번째 인자가 더 큰 객체라면 1

두번째 인자가 더 큰 객체라면 -1

같다면 0

내림차순

첫번째 인자가 더 작은 객체라면 1

두번째 인자가 더 작은 객체라면 -1

같다면 0

 

 

이 클래스의 객체를 Collections.sort의 2번째 인자로 전달하기만 하면 됩니다. 이제 전체코드와 결과를 보겠습니다.

 

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Sort {

	public static void main(String[] ar){
		List<User> list=new ArrayList();
		
		list.add(new User("A",40));
		list.add(new User("B",25));
		list.add(new User("C",10));
		list.add(new User("D",30));
		list.add(new User("E",26));
		
		System.out.println("정렬 전 ->"+list);
		Collections.sort(list,new UserComparator());
		//list.sort(new UserComparator()); 이 방법 역시 정렬 방법
		System.out.println("정렬 후->"+list);
	}
}

class User{
	public String name;
	public int age;
	public User(String name,int age){
		this.name=name;
		this.age=age;
	}
	@Override
	public String toString(){
		return "(name:"+name+", age:"+age+")";
	}
}

class UserComparator implements Comparator<User>{
	@Override
	public int compare(User a,User b){
		if(a.age>b.age) return 1;
		if(a.age<b.age) return -1;
		return 0;
	}
}

 

실행결과

정렬 전 ->[(name:A, age:40), (name:B, age:25), (name:C, age:10), (name:D, age:30), (name:E, age:26)]
정렬 후->[(name:C, age:10), (name:B, age:25), (name:E, age:26), (name:D, age:30), (name:A, age:40)]

저희가 원하던 결과를 얻을 수 있습니다. 참고로 Collections.sort를 주석처리하고 list.sort를 실행해도 결과는 같습니다. List도 역시 Comparator를 받을 수 있으니까요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

와나진짜

,

 

 

C언어, C++에서는 메모리를 조금 더 쉽게 다루고자하는 함수가 몇가지 존재합니다. 그것들이 무엇이 있는지 설명과 예제를 통해서 알아보도록 하겠습니다. 메모리 관련 함수를 사용하기 위해서는 string.h를 include해야합니다.

 

0) string.h 헤더파일 추가

메모리 관련 함수를 사용하기 위해서 반드시 추가해주세요.

 

1) void* memset(void* source, int value, size_t n)

메모리 주소 source부터 시작해 n만큼 value로 메모리를 채웁니다. return 값은 메모리의 시작주소입니다.

간단하네요. 그러면 예제를 바로 보도록 하겠습니다.

#include <stdio.h>
#include <string.h>

int main() {

	int nums1[5];
	unsigned char nums2[5];
	int i;

	memset(nums1, 10, sizeof(nums1));
	memset(nums2, 10, sizeof(nums2));
	
	for (i = 0; i < 5; i++) {
		printf("nums1[%d] = %d \n", i, nums1[i]);
	}
	
	printf("\n");

	for (i = 0; i < 5; i++) {
		printf("nums2[%d] = %d \n", i, nums2[i]);
	}
}

 

nums1과 nums2를 5개의 배열로 잡는데 자료형이 다르군요. nums1는 int형(여기서는 4바이트), nums2는 unsigned char형(1바이트)입니다. 이후 둘의 메모리를 memset으로 10으로 초기화합니다.

어떤 결과가 나올까요? 두개의 for문에서 nums1과 nums2의 요소들이 전부 10으로 나올것 같은데 그럴까요?

실행결과

nums1[0] = 168430090
nums1[1] = 168430090
nums1[2] = 168430090
nums1[3] = 168430090
nums1[4] = 168430090

nums2[0] = 10
nums2[1] = 10
nums2[2] = 10
nums2[3] = 10
nums2[4] = 10

 

우리의 예상과는 조금은 다릅니다. memset내부에서 실제 10이란 값은 unsigned char로 변환되어 1바이트의 메모리에 그 값을 집어넣게 되는겁니다. 

그래서 4바이트인 int형은 이런식으로 메모리가 set이 됩니다.

00001010 00001010 00001010 00001010 -> 168430090

memset은 1바이트 단위의 메모리를 세팅합니다. 그래서 unsigned char 형의 nums2는 제대로 된 값을 읽을 수 있습니다.

 

2) void* memcpy(void* destination, const void* source, size_t num)

이 함수는 source의 메모리를 destination으로 num만큼 복사합니다. 이 함수에는 source나 destination이 num바이트 이상인지를 검사하지 않으므로 상당히 취약하며 이진데이터를 그대로 복사합니다. 그러니 중간에 NULL이 있는지 없는지 확인하지 않습니다. 아래의 예제를 봅시다.

#include <stdio.h>
#include <string.h>

int main() {

	unsigned char source[8];
	int destination[2];
	int i;

	memset(source, 10, sizeof(source));

	memcpy(destination, source, sizeof(source));

	for (i = 0; i < 2; i++) {
		printf("destination[%d] : %d\n", i, destination[i]);
	}
}

 

source는 8바이트이고 destination도 8바이트입니다. 우선 source를 10으로 전부 채운 후에 destination으로 메모리 복사를 하면 어떤 결과가 나올까요?

 

이전의 memset에서 보았듯 바이트 단위로 메모리가 복사되어 8바이트가 0000 1010으로 복사되는 것이지요.

00001010 00001010 00001010 00001010 -> 168430090

따라서 168430090의 값이 두 번 출력되게 됩니다.

실행 결과

destination[0] : 168430090
destination[1] : 168430090

 

 

3) int memcmp(const void* ptr1, const void* ptr2, size_t num)

메모리의 바이트를 비교합니다. ptr1과 ptr2가 num만큼 비교했을 때 같다면 0, 아니면 다른 값을 리턴합니다. strcmp와 비슷한 리턴 값을 보이는데, unsigned char으로 ptr1이 ptr2보다 크다면 양수, 작다면 음수를 리턴하게 됩니다.

 

#include <stdio.h>
#include <string.h>

int main() {

	unsigned char a[5] = { 0,1,2,3,4 };
	unsigned char b[5] = { 0,1,2,3,4 };

	printf("memcmp(a,b) = %d \n", memcmp(a, b,5));
	
	a[0] = 100;
	
	printf("memcmp(a,b) = %d \n", memcmp(a, b, 5));

	b[0] = 200;

	printf("memcmp(a,b) = %d \n", memcmp(a, b, 5));
}

 

처음 a,b는 정확히 같은 값을 갖고 있으므로 비교했을때 0이 리턴됩니다.

이후 a의 0번째 요소가 100으로 a가 b보다 더 크므로 비교했을때 양수가 리턴됩니다. 

그 다음 b의 0번째 요소가 200으로 a가 b보다 더 작으므로 음수가 리턴되지요.

실행 결과

memcmp(a,b) = 0
memcmp(a,b) = 1
memcmp(a,b) = -1

 

4) void* memchr(void* ptr, int value, size_t num)

memchr은 ptr에서 value를 찾을때 사용합니다. 즉 메모리에서 특정 값을 찾을 때 사용하는 함수입니다. 만약 값이 존재한다면 그 주소를 리턴하고 아니면 NULL을 반환합니다.

 

#include <stdio.h>
#include <string.h>

void printMemory(void *ptr) {
	if (ptr == NULL) {
		printf("메모리에 존재하지 않음\n");
	}
	else {
		printf("메모리에 %d가 존재. addr : %p \n", *((unsigned char*)ptr),ptr);
	}
}
int main() {

	unsigned char arr[5] = { 0,1,2,3,4 };
	unsigned char a = 4;
	unsigned char b = 5;
	int i;

	for (i = 0; i < 5; i++) 
		printf("arr[%d] : %d, %p\n", i, arr[i], &arr[i]);
	
	
	void* ptr=memchr(arr, a, 5);
	printMemory(ptr);

	ptr = memchr(arr, b, 5);
	printMemory(ptr);
}

 

현재 1바이트 배열 arr에는 0,1,2,3,4가 있습니다. 여기에서 a(4)와 b(5)를 찾을 겁니다. a는 존재하니까 ptr이 NULL이 아닌 a가 존재하는 그 주소를 반환하겠지요. b는 존재하지 않으므로 NULL이 반환됩니다.

 

실행결과

arr[0] : 0, 0093F75C
arr[1] : 1, 0093F75D
arr[2] : 2, 0093F75E
arr[3] : 3, 0093F75F
arr[4] : 4, 0093F760
메모리에 4가 존재. addr : 0093F760
메모리에 존재하지 않음

 

4가 있는 주소, 0093F760을 반환하는 것을 알 수 있네요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,