Calendar 클래스

Calendar 클래스는 날짜와 시간을 다루기 위해서 Date와 함께 많이 쓰이는 클래스 중 하나입니다. Calendar를 사용하기 위해서는 java.util.Calendar를 import 시켜야합니다. 

가장 기본적으로 현재 날짜와 시간을 가져올 수도 있고, 특정 시간으로 시간을 되돌리거나 뒤로갈 수도 있습니다. 이는 Calendar에서 제공하는 상수들을, 예를 들면 YEAR, MONTH, DAY_OF_MONTH 등을 이용하여 설정할 수 있습니다. 어떤 필드가 있는지 볼까요? 아래의 필드를 이용해서 get, set 메소드를 통해 값을 얻어오거나 설정할 수 있습니다.

Field(static int) 설명
YEAR 년도를 나타냅니다.
MONTH 월을 나타내는데, 이때 1월을 상수 0으로 대응이 됩니다. 그래서 실제 월을 구할때는 +1을 해주어야합니다.
DATE, DAY_OF_MONTH 월의 날짜를 의미합니다.
DAY_OF_WEEK 일주일에 해당되는 요일을 의미합니다. 일요일부터 시작이며 일요일은 1입니다. 수요일은 4의 값을 갖습니다.
HOUR 시간을 표시하는데 12시간 단위의 시간을 의미합니다.
HOUR_OF_DAY 시간을 표시하는데 24시간 단위의 시간을 의미합니다.
MINUTE 분을 의미하는 필드입니다.
SECOND 초를 의마하는 필드입니다.
MILLISECOND 밀리 세건드 단위를 의미하는 필드입니다.

 

아래의 필드는 get, set으로 얻지는 않고 비교할때 사용할 수 있습니다.

Field(static int) 설명
JANUARY 1월을 나타냅니다. 0의 값을 갖고 있습니다.
월을 나타내는 필드는 전부 대문자입니다. 2월을 FEBURARY, 3월은 MARCH입니다. 각 숫자는 월-1에 값을 갖습니다.
SUNDAY 일요일에 해당하는 값이며 1을 가집니다. 요일을 나타내는 상수도 마찬가지로 전부 대문자로 표시할 수 있으며 SUNDAY의 1부터 SATURDAY의 7까지 나타낼 수 있습니다.

 

이번 포스팅에서는 Calendar클래스를 어떻게 사용하고 다루는지 예를 통해서 설명하도록 하겠습니다.

1. 현재 시간의 정보를 표시하는 예제

    public static void main(String[] args){
    	Calendar cal=Calendar.getInstance();	//getInstance()로 객체 생성
    	System.out.println("현재 날짜:"+cal.get(Calendar.YEAR)+"-"+(cal.get(Calendar.MONTH)+1)+"-"+cal.get(Calendar.DAY_OF_MONTH));
    	System.out.println("일주일 중 오늘은 "+cal.get(Calendar.DAY_OF_WEEK)+"번째 요일 (1은 일요일)");
    	System.out.println("일년 중 오늘은 "+cal.get(Calendar.DAY_OF_YEAR)+"번째 날");
    	System.out.println("현재 시간 "+cal.get(Calendar.HOUR_OF_DAY)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND)+":"+cal.get(Calendar.MILLISECOND));
    	System.out.println("현재 시간 "+cal.get(Calendar.AM_PM)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND));
    	System.out.println("이번 주는 일년 중 "+cal.get(Calendar.WEEK_OF_YEAR)+"번째 주");
    }

 

Calendar객체는 new 키워드로 객체를 생성할 수 없고 getInstance() 메소드로 객체를 생성할 수 있습니다. 위 예제는 현재 날짜와 시간을 나타내며 위에 설명한 표의 데이터가 어떤 값을 나타내는 지 확인하는 예제입니다.

현재 날짜:2021-10-20
일주일 중 오늘은 4번째 요일 (1은 일요일)
일년 중 오늘은 293번째 날
현재 시간 19:36:39:959
현재 시간 1:36:39
이번 주는 일년 중 43번째 주

 

2. set 메소드로 날짜 설정

    public static void main(String[] args){
    	Calendar cal=Calendar.getInstance();	//getInstance()로 객체 생성. 기본 현재 날짜
    	cal.set(Calendar.MONDAY,Calendar.DECEMBER); 	//12월로 설정
    	cal.set(Calendar.HOUR_OF_DAY,14);	//오후 2시로 Calendar 객체 설정
    	System.out.println("설정된 날짜 - "+(cal.get(Calendar.MONTH)+1)+"월 "+cal.get(Calendar.DATE)+"일");
    	System.out.println("설정된 시간 - "+cal.get(Calendar.HOUR_OF_DAY)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND));
    	
    }

 

이번에는 set 메소드로 month와 hour를 설정해보았습니다. 아래처럼 원래의 date와 minute, second는 현재의 시간과 동일하며 month와 hour만 바뀐 것을 알 수 있네요.

설정된 날짜 - 12월 20일
설정된 시간 - 14:43:55

 

이렇게 개별적으로 바꿀 수도 있습니다만, 만약 한꺼번에 바꾸고 싶다고 하면 오버로딩된 set메소드를 활용하시면 됩니다.

    public static void main(String[] args){
    	Calendar newYear=Calendar.getInstance();
    	newYear.set(2022,Calendar.JANUARY,1);	//년, 월, 일 설정
    	System.out.println(newYear.get(Calendar.YEAR)+"년 "+(newYear.get(Calendar.MONTH)+1)+"월 "+newYear.get(Calendar.DATE)+"일");
    	
    	newYear.set(2022,Calendar.JANUARY,1,0,0);	//년, 월, 일, 시, 분 설정
    	System.out.println(newYear.get(Calendar.YEAR)+"년 "+(newYear.get(Calendar.MONTH)+1)+"월 "+newYear.get(Calendar.DATE)+"일");
    	System.out.println(newYear.get(Calendar.HOUR_OF_DAY)+"시 "+newYear.get(Calendar.MINUTE));
    }
2022년 1월 1
2022년 1월 1
0시 0

 

3. 밀리초로 1970년 1월 1일 00시 00분부터 흐른 시간 구하기

    public static void main(String[] args){
    	Calendar today=Calendar.getInstance();	//getInstance()로 객체 생성. 기본 현재 날짜
    	System.out.println("1970년 00시 00분부터 흐른 초 :"+today.getTimeInMillis()/1000);
    	
    	SimpleDateFormat format=new SimpleDateFormat("a hh:mm:ss");	
    	System.out.println("현재시간 "+format.format(today.getTimeInMillis()));	//SimpleDateFormat으로 출력
    	
    	
    	Calendar newYear=Calendar.getInstance();	//현지 시간으로 설정
    	newYear.set(Calendar.YEAR, 2021);
    	newYear.set(Calendar.MONTH, Calendar.OCTOBER);
    	newYear.set(Calendar.DAY_OF_MONTH, 21);
    	
    	long diff=newYear.getTimeInMillis()-today.getTimeInMillis();
    	Calendar dDay=Calendar.getInstance();
    	dDay.setTimeInMillis(diff);	//1년 이내로만 이 코드를 쓸 수 있음
    	System.out.println("남은 날 수 :"+(dDay.get(Calendar.DAY_OF_YEAR)-1));	//오늘이 포함되므로 -1
    	
    	diff=diff/(60*60*24*1000);	//60(분) * 60(1분) * 24(시간) * 1(초) = 하루 
    	System.out.println("남은 날 수 :"+diff);
    }

 

getTimeInMillis를 통해서 밀리초단위로 구할 수 있습니다. 밀리초 단위로 SimpleDateFormat에 설정하여 더 쉽게 볼 수도 있고, 두 시간 사이의 계산도 가능합니다. 

1970년 00시 00분부터 흐른 초 :1634727499
현재시간 오후 07:58:19
남은 날 수 :2
남은 날 수 :1

 

4. 특정 날짜가 현재 날짜보다 전날인지, 이후인지 확인

    public static void main(String[] args){
    	Calendar yesterday=Calendar.getInstance();
    	yesterday.set(Calendar.DATE, yesterday.get(Calendar.DATE)-1);	//현재 날짜 -1로 설정
    	Calendar today=Calendar.getInstance();
    	
    	Calendar tomorrow=Calendar.getInstance();
    	tomorrow.set(Calendar.DATE, tomorrow.get(Calendar.DATE)+1);	//현재 날짜 +1
    	
    	System.out.println("오늘이 어제보다 이전인가? "+today.before(yesterday));
    	System.out.println("오늘이 내일보다 이전인가? "+today.before(tomorrow));
    	
    	System.out.println("오늘이 어제보다 이후인가? "+today.after(yesterday));
    	System.out.println("오늘이 내일보다 이후인가? "+today.after(tomorrow));
    }

위 코드는 어제와 내일로 일단 날짜를 지정한 Calendar객체로 오늘이 앞날인지, 뒷날인지 비교하는 코드입니다. 

오늘이 어제보다 이전인가? false
오늘이 내일보다 이전인가? true
오늘이 어제보다 이후인가? true
오늘이 내일보다 이후인가? false

 

또한 위의 코드는 아래와 같이 add 메소드를 활용하는 방식으로 바꿀 수도 있습니다.

    	Calendar yesterday=Calendar.getInstance();
    	yesterday.set(Calendar.DATE, -1);
    	Calendar today=Calendar.getInstance();
    	
    	Calendar tomorrow=Calendar.getInstance();
    	tomorrow.add(Calendar.DATE,1);

 

이상으로 Calendar 클래스의 활용과 예제를 살펴보았습니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

정적(static)

정적 멤버(혹은 변수)와 메소드라는 것은 무엇일까요? 우선 아래와 같은 상황이 있다고 가정해봅시다. Counter라는 클래스가 있으며 Counter는 멤버로 cnt를 갖고 있습니다. 그리고 count라는 메소드가 있는데, 이 count 메소드는 단순히 cnt를 하나 증가하는 역할을 합니다.

그리고 메인에서는 Counter의 두 객체 counter1, counter2를 생성하고 둘은 counter() 메소드를 통해서 cnt를 증가시키고 있습니다. 여기서 저의 의도는 두 객체가 cnt를 같이 증가시키고 싶습니다.

class Counter{
	public int cnt;
	public void count() {
		cnt++;
	}
	
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter1=new Counter();
    	Counter counter2=new Counter();
    	counter1.count();
    	counter2.count();
    	System.out.println("counter1:"+counter1.cnt+", counter2:"+counter2.cnt);
    	
    }
}

 

만약 프로그램이 저의 의도대로 동작한다면 counter2까지 왔을때 cnt는 2가 되어야합니다. (물론 그렇게 동작하지 않습니다.) 결과를 보면 아래와 같네요.

counter1:1, counter2:1

 

정적 변수(Static variable)

여기서 저의 희망은 cnt라는 맴버를 공통으로 사용하는 것입니다. 이때 static하나만 써주면 저의 목적이 달성됩니다. Counter 클래스에서 cnt를 선언할때 static만 붙여봅시다.

class Counter{
	public static int cnt;
	public void count() {
		cnt++;
	}
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter1=new Counter();
    	Counter counter2=new Counter();
    	counter1.count();
    	counter2.count();
    	System.out.println("counter1:"+counter1.cnt+", counter2:"+counter2.cnt);
    	
    }
}

그리고 결과를 봐야겠죠.

counter1:2, counter2:2

 

제가 말한대로 동작하는 것을 알 수 있습니다.

어떻게 이런것이 가능할까요? static 변수나 메소드는 static 메모리 구역에 따로 고정적(정적)으로 할당되어 관리됩니다. 그래서 이 static 변수와 메소드는 모든 Counter 클래스가 공통적으로 사용할 수 있습니다. 이러한 특징 때문에 클래스 변수, 메소드라고 합니다.

 

static에 대해서 가볍게 아시려면 "공용 변수, 메소드" 라고 쉽게 기억하시면 됩니다.

 

그렇지만 static에 대해서 좀 더 자세히 알려면 여러분은 우선 프로그램이 실행될때의 메모리 구조에 대해서 아셔야합니다. 우리가 항상쓰던 new로 객체를 생성하는 것은 heap 영역에 저장되어 프로그램이 실행하는 중간에 메모리를 할당합니다. 그래서 메소드가 끝나서 쓸일이 없어질때 자바의 Garbage Collector에 의해서 메모리에서 수거가 됩니다.

하지만 static은 프로그램이 실행 전 먼저 메모리에 잡히게 됩니다. 이때 실행 전이라고 하는 것은 프로그램이 메모리에 적재되고 명령어를 수행하기 전을 말합니다. 그리고 프로그램이 종료될때 해제가 되지요. 즉, heap의 영역보다 나중에 정리된다는 뜻입니다.

메모리 구조

 

가장 먼저 메모리에 적재되어 정적(static)으로 존재하기 때문에 static 멤버나 메소드는 클래스의 객체 생성없이 클래스의 이름만가지고도 사용할 수 있습니다.

public static void main(String[] args){
	Counter.cnt++;
    Counter.cnt++;
    System.out.println("cnt:"+Counter.cnt);
}
cnt:2

 

정적 메소드(static method)

이제 정적 변수는 알았는데, 정적 메소드는 그렇다면 무엇일까요? 역시 변수와 개념은 다르지 않습니다. 메모리에 메소드하나를 고정적으로 두어서 사용이 가능합니다. Counter 클래스를 변형시켜서 보도록 합시다. 

class Counter{
	public static int count(int cnt) {
		return cnt+1;
	}
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter=new Counter();
    	System.out.println("count 1:"+Counter.count(1));
    	System.out.println("count 2:"+Counter.count(2));
    	System.out.println("counter's count 5:"+counter.count(5));
    }
}

 

그러면 메모리에는 이런식으로 잡히게 됩니다. 마치 counter라는 메소드를 공용으로 사용하듯 말이죠.

 

실행을 시켜보면 아래와 같은 결과를 갖게 됩니다.

count 1:2
count 2:3
counter's count 5:6

 

이제 메모리에 정적변수나 메소드가 다른 동적으로 메모리를 할당(new 키워드로 할당하거나 일반 메소드)하는 시점보다 먼저 메모리에 잡히게 되었다는 것을 알았다면 다음과 같은 규칙을 이해하게 될 것입니다.

 - 정적 메소드에서는 정적 메소드나 정적 변수만 사용할 수 있다. 멤버 변수는 사용할 수 없다.

이러한 이유는 정적 메소드에서는 자신보다 나중에 메모리가 할당되는 멤버 변수나 메소드는 언제 할당되는지 알수가 없습니다. 만약 아래의 코드처럼 count가 먼저 호출되는 상황에서 cnt가 메모리에 할당되지 않았다면요. 그래서 이처럼 빨간줄이 쳐지면서 static 만이 올수가 있다고 메시지를 띄워줍니다.

 

main 메소드에서 static 메소드만 호출 가능했던 이유

main 메소드 역시 static 메소드이기 때문에 여러분들이 클래스를 배우기 전 메소드를 배울때 묻지도 따지지도 않고 우선 static을 붙여서 메소드를 구현해보셨을 것입니다. main 메소드는 프로그램 실행 전 먼저 메모리에 할당되어야하기 때문이고, 메인이 static이기 때문에 호출되는 메소드 역시 static이어야하기 때문입니다.

 

이상으로 자바에서 static 개념에 대한 설명이었습니다. 무엇보다 메모리가 할당되는 시점만 이해한다면 왜 이런 결과가 나오는지 이해하기가 쉬울 것입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

람다 표현식(Lambda Expression)

람다 표현식은 JAVA SE 8부터 추가된 기능입니다. 람다 표현식은 함수를 하나의 식으로 표현한 것인데요. 메소드의 이름을 갖다버린 익명 함수(Anonymous Function)로 볼 수 있습니다.

아주 간단히 이야기하자면 귀찮은 익명 메소드(자바에서는 익명 클래스, Anonymous Class)를 아주 간단하게 줄인 표현식이라고 생각하면 됩니다.

 

기존 방식

예를 들어 안드로이드에서 클릭 이벤트 발생시에 아래와 같이 View.OnClickListener를 정의해주고 onClick 메소드를 채워줘야 클릭 이벤트를 받을 수 있습니다.

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG,"클릭 발생");
            }
        });

 

구현해야할 메소드는 onClick 메소드 하나 뿐인것을 알 수 있습니다. onClick 메소드는 너무나 간단하게 로그를 찍는 코드밖에 없습니다. 하지만 꽤나 복잡한 과정을 거치게 되죠. 분명 로그만 찍는데 부가적으로 기록해야하는 코드가 길다는 것이죠. 

혹은 우리가 자주 사용하는 Thread를 Runnable을 통해서 객체 생성과 동시에 동작시키려면 이렇게 코드를 짜야합니다.

new Thread(new Runnable() {
    		@Override
    		public void run() {
    			System.out.println("Thread run");
    		}
    	}).start();

 

 

람다를 사용한 방식

만약 위의 코드들에 람다표현식을 적용한다면 이렇게 바뀝니다.

button.setOnClickListener(v->Log.e(TAG,"클릭 발생"));

 

그리고 Thread를 동작시키는 코드는 아래와 같게됩니다.

new Thread(()->System.out.println("Thread run")).start();

 

몇줄이 단지 한줄로 바뀌게 되지요. 이렇게 람다를 적용하면 이렇게 간단한 코드로 바뀌게 됩니다. 즉, 가독성이 정말 높아지게 됩니다. 여기서 우리는 단지 하나의 익명 클래스를 구현했지만, 그것이 여러개가 된다면 가독성이 매우 떨어지게 됩니다.

 

반면 익명 함수이기 때문에 재사용이 불가능하고, 디버깅이 어려운 단점이 있습니다. 

 

람다 표현식 문법

이제는 람다를 사용하는 방법에는 어떤 방법이 있는지 알아보도록 합시다. 이때 람다를 사용할 수 있는 경우는 Overriding해야할 메소드가 하나여야합니다. 두개 이상의 메소드를 구현해야할때에는 사용할 수가 없습니다.

1. 구현부가 한줄인 경우

() -> System.out.println(x)

 

2. 인자가 있는 경우

() -> {System.out.println("lambda")};

 

3. 인자가 있는 경우

인자가 2개 이상인 경우에는 괄호로 묶어야합니다. 그리고 타입을 명시적으로 지정해줄 수도 있습니다.

x -> {System.out.println(x)};
(x) -> {System.out.println(x)};
(x,y) -> {System.out.println(x+","+y)};
(int x, int y) -> {System.out.println(x+","+y)};

 

4. 반환값이 있을 경우

(int x) -> { return x*x };

 

 

함수형 프로그래밍

람다를 사용하는 가장 주된 이유는 함수형 프로그래밍을 해줄 수 있게 만들어주기 때문입니다. 여기서 함수형 프로그래밍이라는 것은 순수 함수, 메소드로 문제를 해결하는 프로그래밍 페러다임입니다. 자바 8 이전에는 어떻게(how) 문제를 해결하는 것에 초점을 두지만, 8 이후에는 무엇(what)을 할 것인가에 대해서도 지원하게 됩니다.

예를 들어 for을 우리가 구현하지 않고 미리 정의된 forEach를 사용하는 방식입니다. 이렇게 되면 코드를 짧게 유지하여 유지 보수하기가 매우 쉬워집니다. 

 

함수형 프로그래밍을 도와주는 몇몇 기법에는 지금 소개한 람다 표현식과 StreamAPI 등이 있습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

자바 소켓 프로그래밍(Network Socket Programming)

네트워크에서는 크게 두가지 프로토콜이 있습니다. TCP와 UDP가 그것들이죠. TCP는 연결형에 신뢰성을 바탕을 둔 전송 프로토콜이고, UDP는 비연결성의 비신뢰성의 프로토콜입니다. 여기서 왜 TCP가 신뢰성이 있냐면 패킷 유실시에 다시 재전송을 하기 때문이죠. UDP는 그런게 없이 잃어버리든 말든 재전송따위는 없이 보냅니다. 

여기서는 자바를 통해 TCP를 이용해 서버와 통신하는 프로그램을 구현해보도록 하겠습니다. TCP를 통해서 프로그래밍을 할땐 Socket에 관한 이해가 필요한데, 아래의 링크를 통해서 개념을 잡고 오세요. 설명은 리눅스 C 소켓 프로그래밍을 설명하지만 기본 개념은 같습니다.

reakwon.tistory.com/81

 

[리눅스] 소켓(socket) 개념과 예제(connect, bind, listen, accept,send,recv 사용)

소켓(socket) 네트워크 통신을 하는 표준 방법으로 프로세스간 연결의 종점이라고 볼 수 있습니다. 기본적인 개념은 아래의 그림과 같습니다. 위의 그림은 TCP/IP에서의 인터넷 통신을 보여줍니다.

reakwon.tistory.com

 

서버-클라이언트 모델을 사용하기 때문에 서버용 프로그램과 클라이언트용 프로그램 두가지 메인 프로그램이 필요합니다.

Client는 GUI로 화면에 출력하고 서버는 Console 출력화면을 사용하도록 하겠습니다. 

 

서버 구현

Socket에 대한 개념을 충분히 이해하셨다면 이제 코드만 이해하면 됩니다. 아니, 방법만 알면 됩니다. 서버쪽에는 ServerSocket이라는 클래스를 이용하여 이것으로 클라이언트 연결이 올때까지 대기합니다. accept() 메소드가 바로 클라이언트 연결을 대기하는 메소드이고 연결이 성립될때까지 대기(컴퓨터 과학에서는 Block 된다고합니다.)합니다. accept는 연결이 되면 실제 통신하기 위해 Socket 객체를 넘겨주며 넘겨받은 Socket으로 실제 통신을 한다고 생각하시면 됩니다. 

그러니까 정리하자면 ServerSocket(개인적으로 부모 Socket이라 부릅니다.)은 Client를 받기 위한 소켓이고, 실제 데이터 송수신하는 소켓은 ServerSocket이 accept()하고 넘겨준 Socket(개인적으로 세끼 Socket이라고 합니다.)이라는 것이죠. 

아래의 코드는 포트번호 9999를 사용하며, 연결이 오면 소켓의 정보를 출력해주고 클라이언트에게 "Hello!! From Server"라는 메시지를 보낸 후 통신을 끊는 아주 간단한 서버의 코드입니다.

 

- TCPServer.java

public class TCPServer {
	
	public final static int SERVER_PORT=9999;
	public static void main(String[] ar) {
		ServerSocket ss=null;
		try {
			ss=new ServerSocket(SERVER_PORT);
			
		}catch(IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				System.out.println("Waiting connection...");
				Socket s=ss.accept();		//새끼 Socket 넘겨줌
				System.out.println("[ Connection Info ]");
				System.out.println("client address:"+s.getInetAddress());	//클라이언트 IP주소
				System.out.println("client port:"+s.getPort());			//클라이언트 포트 번호
				System.out.println("my port:"+s.getLocalPort());		//나(Server, Local)의 포트
				
				PrintWriter pw=new PrintWriter(new OutputStreamWriter(s.getOutputStream()));
				pw.println("Hello!! From server");
			
				pw.close();
				s.close();
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
	}
}

 

클라이언트 구현

GUI 설정때문에 코드가 길어보이는 것뿐이니 사실 별거없습니다. 간단하게 클라이언트는 Swing 컴포넌트로 JTextArea에 Socket의 정보와 서버로부터 온 메시지를 출력해줍니다.  .정말 별거없죠? 

public class TCPClient extends JFrame{
	public final static int SERVER_PORT=9999;
	
	private Socket s;
	private JTextField messageField;
	private JTextArea messages;
	
	public TCPClient() {
		super(" TCP Client");
		messageField=new JTextField(40);
		messages=new JTextArea(10,50);
		
		messages.setBackground(Color.black);		//배경 검은색
		JScrollPane scrolledMessages=new JScrollPane(messages);	//스크롤 가능하도록
		
		this.setLayout(new BorderLayout(10,10));
		this.add("North",messageField);
		this.add("Center",scrolledMessages);
		
		messages.setEnabled(false);		//TextArea disactive
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(620,400);
		this.setLocationRelativeTo(null);	//창 가운데 위치
		this.setVisible(true);
		
		
		connectServer();
	}
	
	public void connectServer() {
		String serverIP="127.0.0.1";
		
		try {
			Socket s=new Socket(serverIP,SERVER_PORT);
			messages.append("server port:"+s.getPort()+", my port:"+s.getLocalPort()+"\n");
			BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream()));
			messages.append(br.readLine());
			
			br.close();
			s.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public static void main(String []ar) {
		new TCPClient();	
	}
}

 

위의 IP주소는 127.0.0.1인 이유는 저의 주소를 그대로 사용하게 만들도록 하기 위함입니다. 클라이언트는 포트번호 9999로 서버 소켓에 연결합니다. 아래는 서버와 클라이언트를 실행한 화면입니다. 

서버부터 실행하고 다음 클라이언트를 실행해야합니다. 서버와 클라이언트의 소켓정보가 일치됨을 확인할 수 있고 클라이언트는 서버로부터의 메시지를 잘 받아왔네요.

 

 

에코 서버-클라이언트 구현

클라이언트의 메시지를 그대로 돌려주는 서버를 우리는 에코 서버(Echo Server)라고 합니다. 그것을 구현해볼껀데요. 여기서 서버는 ServerSocket으로 새로운 통신을 확립시키면 새끼 Socket을 새로운 통신 쓰레드에 넘겨주며 작업을 처리하게 할것이고 다음 연결을 대기하게 만듭니다. 쓰레드와 소켓이 보통 같이 구현됩니다.

서버 구현


public class TCPServer {
	
	public final static int SERVER_PORT=9999;
	public static void main(String[] ar) {
		ServerSocket ss=null;
		try {
			ss=new ServerSocket(SERVER_PORT);
			
		}catch(IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				System.out.println("Waiting connection...");
				Socket s=ss.accept();		//새끼 Socket 넘겨줌
				
				new ServerThread(s).start();
				
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
	}
}

class ServerThread extends Thread{
	
	private Socket s;
	private BufferedReader br;
	private PrintWriter pw;
	public ServerThread(Socket s) {
		this.s=s;
		try {
			br=new BufferedReader(new InputStreamReader(s.getInputStream()));	//Socket으로 Read용 Stream
			pw=new PrintWriter(new OutputStreamWriter(s.getOutputStream()));	//Socket으로 Write용 Stream
		}catch(IOException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void run() {
		
		while(true) {
			String received;
			try {
				received = br.readLine();	//1. 받고
				System.out.println("server received :"+received);
				if(received==null||received.equals("quit")) {	//quit 또는 q가 오면 종료
					if(br!=null) br.close();
					if(pw!=null) pw.close();
					if(s!=null) s.close();
					return;
				}
				
				pw.println("Server Got Your Message : "+received);	//2. 보냄
				pw.flush();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

 

ServerThread의 생성자에서는 소켓을 넘겨받고 사용할 인풋,아웃풋 스트림을 생성합니다. 그리고 run() 메소드에서 이런작업을 진행하지요. 

 

1. 우선 클라이언트로부터 메시지를 읽어옵니다. 이때 BufferedReader를 사용하죠.

2. 메시지를 검사하고 다시 클라이언트로 서버의 메시지를 첨가해 다시 넘겨줍니다. 이때 PrintWriter를 사용합니다. 여기서 한가지 하는 실수가 뭐냐면 PrintWriter는 내부적으로 버퍼를 사용하기 때문에 버퍼가 다 차기전까지 내용을 출력하지 않습니다. 그래서 강제로 비워줘야하는데, 그 메소드가 flush()입니다. 이전의 맨 처음 코드에서 flush()를 하지 않은 이유는 PrintWriter의 close()메소드가 버퍼를 비우고 스트림을 닫기 때문에 굳이 flush()를 호출할 필요가 없었죠. flush()는 변기물을 내리는 것처럼 버퍼를 흘려보내서 비워준다는 뜻입니다.

 

혹은 이렇게 이렇게 꼭 flush()를 하기 귀찮고 자동으로 flush()해주기를 원한다면 아래의 생성자를 사용하세요.

pw=new PrintWriter(new OutputStreamWriter(s.getOutputStream()),true);
PrintWriter(OutputStream out, boolean autoFlush)
Creates a new PrintWriter from an existing OutputStream.

두번째 인자는 auto flush를 하느냐 마냐를 정해줍니다. true면 auto flush활성화하는것입니다.

 

클라이언트는 이와 반대의 순서로 메시지를 보내고, 읽어야겠죠? 

클라리언트 구현

public class TCPClient extends JFrame implements KeyListener{
	//...생략...//
	@Override
	public void keyTyped(KeyEvent e) {}

	@Override
	public void keyPressed(KeyEvent e) {}

	@Override
	public void keyReleased(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if(keyCode==KeyEvent.VK_ENTER) {
			try {
				if(br==null||pw==null) return;
				String msgText=messageField.getText();
				if(msgText==null||msgText.length()==0) return;
				
				if(msgText.equals("quit")) {	//종료
					if(br!=null) br.close();
					if(pw!=null) pw.close();
					if(s!=null) s.close();
					return;
				}
				
				
				pw.println(msgText);	//1. 보내고
				pw.flush();
				messages.append(br.readLine()+"\n");	//2. 받음
				
				//뒷정리
				messageField.setText("");
				messageField.requestFocus();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}	
	}

 

나머지 불필요한 코드는 넣지 않았습니다. 사실 별로 어려운 소스코드는 아닙니다. 단지 소켓으로 인풋, 아웃풋 스트림을 생성한 후에 println()과 flush()로 문자열을 전송한 후에 다시 에코된 메시지를 readLine으로 읽는 겁니다.

 

이제 결과를 보시면 아래와 같이 잘 echo됨을 확인할 수 있습니다.

 

 

여기까지 Socket을 생성하여 연결하는 방법과 네트워크 전송에 사용하는 reader인 BufferedReader, writer인 PrintWriter에 대한 짤막한 사용법도 알아보았습니다.

 

이런 개념을 바탕으로 여러분들이 응용하기 나름이겠지만 채팅 어플리케이션도 만들 수 있고, 심지어 패인트 객체의 정보를 네트워크상으로 주고 받음으로써 캐치마인드까지도 구현해볼 수도 있습니다. 컴퓨터 프로그램에서 네트워크는 절대 빠질수 없는 개념이니 자바로 꼭 익혀두시길 바랍니다.

 

뭐, 시간되면 채팅 어플리케이션 만드는 포스팅도 진행해보도록 할게요. 그럴 시간이 나오려나...

반응형
블로그 이미지

REAKWON

와나진짜

,

JTable

엑셀과 같이 행과 열이 존재하여 표의 형태로 자료를 편집하고 관리할 수 있는 Swing 컴포넌트가 JTable입니다. 행과 열이 만나는 곳이 셀이라고 하며 각 행들은 보통 레코드라고 부릅니다. JTable도 JList와 같이 단일 선택 모드와 다중 선택 모드가 있는데 여기서는 단일 선택 모드를 설명하도록 하겠습니다.

다중 선택모드는 JList와 매우 흡사하므로 아래의 JList를 다루는 포스팅을 참고하시면 될겁니다.

reakwon.tistory.com/166

 

[자바/GUI] Swing JList(다중 선택, 단일 선택) - 동적으로 아이템 추가, 삭제

JList - 단일 선택 모드(SingleSelectionMode) (다중 선택 모드는 아래쪽에서 설명합니다.) Swing 컴포넌트 중에서 자료를 추가하고 삭제할 수 있도록 만든 컴포넌트가 있습니다. 그게 JList인데요. 여러분

reakwon.tistory.com

 

그래서 아래의 프로그램을 구현해보면서 JTable이 어떻게 동작하는지 확인해보도록 합시다.

JTable Example

 

위의 프로그램은 아래와 같이 동작합니다.

1. 추가 - 이름, 나이, 성별, 3개의 점수를 JTextField로 입력받아서 Table에 추가가 됩니다.

2. 삭제 - 선택된 레코드가 있다면 그 레코드를 JTable에서 삭제하며, 없으면 가장 위쪽(가장 먼저 추가된)의 레코드를 삭제합니다. 

3. 읽기 - 셀이 선택되면 시스템 Output에 단순 출력합니다.

 

구현

1. 기본 설정

우선 JFrame을 상속받아 기본 세팅을 진행해보도록 합시다. 여기서는 레이아웃의 배치와 JTable을 초기화, 각 버튼, 텍스트 필드의 이벤트 설정등을 생성자에서 구현하게 됩니다.

public class JTableTest extends JFrame implements MouseListener,KeyListener {
	
	private final String[] labels= {"Name","Age","Sex","Korean","English","Math"};
	private JTextField []fields=new JTextField[6];
	
	private JScrollPane scrolledTable;
	private JTable table;
	
	private JButton addBtn;
	private JButton delBtn;
	
	public JTableTest(String title) {
		
		
		this.setLayout(new BorderLayout(10,10));
		
		JPanel topPanel=new JPanel(new GridLayout(6,4,10,5));
		
		for(int i=0;i<6;i++) {
			topPanel.add(new JLabel(labels[i]));
			fields[i]=new JTextField(30);
			topPanel.add(fields[i]);
		}
		topPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
		this.add("North",topPanel);				//가장 위쪽 Panel 설정
		
		String header[]= {"Name","Age","Sex","Korean","English","Math"};
		DefaultTableModel model=new DefaultTableModel(header,0);	//header추가, 행은 0개 지정
		
		table=new JTable(model);
		table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
		scrolledTable=new JScrollPane(table);	//스크롤 될 수 있도록 JScrollPane 적용
		scrolledTable.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));	//너무 붙어있어서 가장자리 띄움(padding)
		this.add("Center",scrolledTable);	//가운데에 JTable 추가
		
		
		
		JPanel rightPanel=new JPanel(new GridLayout(5,1,10,10));
		
		addBtn=new JButton("레코드 추가");
		delBtn=new JButton("레코드 삭제");			
		
		
		rightPanel.add(addBtn);
		rightPanel.add(delBtn);
		rightPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
		
		this.add("East",rightPanel);		//오른쪽에 버튼들 추가
		
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(620,400);
		this.setLocationRelativeTo(null);	//창 가운데 위치
		this.setVisible(true);
		
		
		//이벤트 추가
		addBtn.addMouseListener(this);	//추가 처리
		delBtn.addMouseListener(this);	//삭제 처리
		for(int i=0;i<6;i++)
			fields[i].addKeyListener(this);	//엔터 처리
		table.addMouseListener(this);	//셀 읽기 처리
	}


	@Override
	public void mousePressed(MouseEvent e) {}
	@Override
	public void mouseReleased(MouseEvent e) {}
	@Override
	public void mouseEntered(MouseEvent e) {}
	@Override
	public void mouseExited(MouseEvent e) {}

	//KeyListener Overrides
	@Override
	public void keyTyped(KeyEvent e) {}
	@Override
	public void keyPressed(KeyEvent e) {}
	@Override
	public void keyReleased(KeyEvent e) {
	
	}

}

 

JTable은 DefaultTableModel으로 테이블의 데이터를 추가, 삭제할 수 있습니다. 그래서 DefaultTableModel을 생성한 다음 JTable 생성자로 넘겨줍니다. DefaultTableModel의 생성자는 여러가지가 있는데, 여기서는 header만 있고 맨 처음 상태에서는 아무런 레코드가 없는 상태이기 때문에 row count=0으로 지정하여 생성합니다.

그리고 JTable이 Record가 넘쳐날 경우 잘리지 않게 JScrollPane으로 스크롤 가능하게 만들어준 점도 주목해주세요.

2. 추가 구현 - addRecord()

조금 전 DefaultTableModel을 통해서 레코드를 추가하는 것이 가능합니다. 우선 JTable에 아까 전달한 DefaultTableModel을 얻어온 다음 addRow() 메소드를 통해 추가가 가능합니다. addRow()는 Object 배열을 받을 수 있도록 구현이 되어있는데, 우리는 입력받는 TextField의 텍스트를 String 배열로 넘겨줄 것입니다.

public void addRecord() {
	DefaultTableModel model=(DefaultTableModel)table.getModel();
	String []record=new String[6];
	for(int i=0;i<6;i++) {
		if(isInvalidInput(fields[i].getText())) {
			System.out.println("Invalid Input");
			return;
		}
		record[i]=fields[i].getText();
	}
	model.addRow(record);
}

 

여기서 유요한 입력인지 아닌지를 판별해주는 것이 isInvalidInput()인데, 간단하게 텍스트 필드가 비어있는지 확인하는 메소드입니다.

	private boolean isInvalidInput(String input) {
		return input==null||input.length()==0;
	}

 

그리고 다음 입력을 편하게 받을 수 있도록 필드를 전부 비워주고 가장 맨 처음인 이름 필드에 focus를 하도록 하지요. model.addRow(record) 밑에 아래의 라인을 추가 구현하면 됩니다.

	//모든 TextField 비우기
	for(int i=0;i<6;i++)
		fields[i].setText("");
		
	fields[0].requestFocus();

 

이제 addRecords()라는 레코드 추가 메소드는 구현이 되었습니다. 이 메소드는 추가버튼이나 텍스트 필드에서 엔터가 쳐지는 순간 호출하면 됩니다.

	@Override
	public void mouseClicked(MouseEvent e) {
		Object src=e.getSource();
		if(src==addBtn) {
			addRecord();
		}

	}

 

	@Override
	public void keyReleased(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if(keyCode==KeyEvent.VK_ENTER) {
			addRecord();
		}
	}

 

3. 삭제 구현 - removeRecord()

우리는 삭제할때 선택된 레코드가 있으면 그 레코드를 삭제하고 아니면 가장 처음 추가된 레코드를 삭제한다고 했죠. 우리가 구현한 removeRecord()는 선택된 index를 넘겨받는데 만약 선택된 index가 없다면 index는 -1의 값을 가진다고 합시다.

	public void removeRecord(int index) {
		DefaultTableModel model=(DefaultTableModel)table.getModel();
		if(index<0) {
			if(table.getRowCount()==0)//비어있는 테이블이면
				return;
			index=0;
		}
		model.removeRow(index);
	}

 

index가 -1인 경우에는 아예 Table에 레코드가 없거나, 레코드는 있는데 선택하지 않는 경우입니다. 레코드의 수를 확인하려면 JTable의 getRowCount() 메소드를 통해서 확인하면 됩니다. 

getRowCount() == 0 : 이미 비어있으니 아무 동작하지 않고 종료합니다.

getRowCount() > 0 : 레코드가 존재하니까 가장 상위 레코드인 0을 index의 값으로 저장합니다.

이제 DefaultTableMode의 removeRow()에 그 index를 넘겨주면 해당 index의 행이 삭제됩니다. 이 removeRecords() 메소드는 삭제버튼이 눌리게 되면 호출하면 됩니다.

	@Override
	public void mouseClicked(MouseEvent e) {
		Object src=e.getSource();
		//...생략 ...//
		if(src==delBtn) {
			int selected=table.getSelectedRow();
			removeRecord(selected);
		}
	}

 

JTable의 getSelectedRow() 메소드는 선택된 레코드가 없다면 -1을 넘겨줍니다. 그래서 우리는 그대로 removeRecords()에 그 값을 넘겨주면 됩니다.

 

4. 읽기 - printCell()

읽으려면 cell의 위치를 알아야합니다. 그래서 선택된 행의 위치, 선택된 열의 위치를 얻어와서 DefaultTableModel의 getValueAt()에 전달해주면 셀을 읽어올 수 있습니다. 내용을 출력해주는 메소드를 printCell()이라고 하여 정의하면 이렇습니다.

	public void printCell(int row,int col) {
		DefaultTableModel model=(DefaultTableModel)table.getModel();
		System.out.println(model.getValueAt(row, col));
	}

 

이 메소드는 JTable이 클릭할때 발생시켜주면 되는데 이때 클릭된 셀의 행과 열 위치를 JList의 getSelectedRow()메소드와 getSelectedColumn() 메소드를 통해서 알아오면 됩니다.

	@Override
	public void mouseClicked(MouseEvent e) {
		Object src=e.getSource();
		
		//... 생략 ...//
		if(src==table) {
			int row=table.getSelectedRow();	
			int col=table.getSelectedColumn();
			
			printCell(row,col);
		}
		
	}

 

전반적인 소스 구현은 끝이 났습니다.

 

전체 코드

전체 코드는 아래와 같습니다.

 

- JTableTest.java


public class JTableTest extends JFrame implements MouseListener,KeyListener {
	
	private final String[] labels= {"Name","Age","Sex","Korean","English","Math"};
	private JTextField []fields=new JTextField[6];
	
	private JScrollPane scrolledTable;
	private JTable table;
	
	private JButton addBtn;
	private JButton delBtn;
	
	public JTableTest(String title) {
		
		
		this.setLayout(new BorderLayout(10,10));
		
		JPanel topPanel=new JPanel(new GridLayout(6,4,10,5));
		
		for(int i=0;i<6;i++) {
			topPanel.add(new JLabel(labels[i]));
			fields[i]=new JTextField(30);
			topPanel.add(fields[i]);
		}
		topPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
		this.add("North",topPanel);				//가장 위쪽 Panel 설정
		
		String header[]= {"Name","Age","Sex","Korean","English","Math"};
		DefaultTableModel model=new DefaultTableModel(header,0);	//header추가, 행은 0개 지정
		
		table=new JTable(model);
		scrolledTable=new JScrollPane(table);	//스크롤 될 수 있도록 JScrollPane 적용
		scrolledTable.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));	//너무 붙어있어서 가장자리 띄움(padding)
		this.add("Center",scrolledTable);	//가운데에 JTable 추가
		
		
		
		JPanel rightPanel=new JPanel(new GridLayout(5,1,10,10));
		
		addBtn=new JButton("레코드 추가");
		delBtn=new JButton("레코드 삭제");			
		
		
		rightPanel.add(addBtn);
		rightPanel.add(delBtn);
		rightPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
		
		this.add("East",rightPanel);		//오른쪽에 버튼들 추가
		
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(620,400);
		this.setLocationRelativeTo(null);	//창 가운데 위치
		this.setVisible(true);
		
		
		//이벤트 추가
		addBtn.addMouseListener(this);	//추가 처리
		delBtn.addMouseListener(this);	//삭제 처리
		for(int i=0;i<6;i++)	//엔터 처리
			fields[i].addKeyListener(this);
		table.addMouseListener(this);	//셀 읽기 처리
		
	}

	private boolean isInvalidInput(String input) {
		return input==null||input.length()==0;
	}

	public void removeRecord(int index) {
		DefaultTableModel model=(DefaultTableModel)table.getModel();
		if(index<0) {
			if(table.getRowCount()==0)//비어있는 테이블이면
				return;
			index=0;
		}
		model.removeRow(index);
	}
	public void addRecord() {
		DefaultTableModel model=(DefaultTableModel)table.getModel();
		String []record=new String[6];
		for(int i=0;i<6;i++) {
			if(isInvalidInput(fields[i].getText())) {
				System.out.println("Invalid Input");
				return;
			}
			record[i]=fields[i].getText();
		}
		model.addRow(record);
		
		//모든 TextField 비우기
		for(int i=0;i<6;i++)
			fields[i].setText("");
		
		fields[0].requestFocus();
	}
	
	public void printCell(int row,int col) {
		DefaultTableModel model=(DefaultTableModel)table.getModel();
		System.out.println(model.getValueAt(row, col));
	}

	//MouseListener Overrides
	@Override
	public void mouseClicked(MouseEvent e) {
		Object src=e.getSource();
		if(src==addBtn) 
			addRecord();
		
		if(src==delBtn) {
			int selected=table.getSelectedRow();
			removeRecord(selected);
		}
		
		if(src==table) {
			int row=table.getSelectedRow();	
			int col=table.getSelectedColumn();
			printCell(row,col);
		}
		
	}
	
	@Override
	public void mousePressed(MouseEvent e) {}
	@Override
	public void mouseReleased(MouseEvent e) {}
	@Override
	public void mouseEntered(MouseEvent e) {}
	@Override
	public void mouseExited(MouseEvent e) {}

	//KeyListener Overrides
	@Override
	public void keyTyped(KeyEvent e) {}
	@Override
	public void keyPressed(KeyEvent e) {}
	@Override
	public void keyReleased(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if(keyCode==KeyEvent.VK_ENTER) {
			addRecord();
		}
	}
}

 

이 클래스를 main 함수에서 호출하면 됩니다.

 

 

- Main.java

public class Main {
	public static void main(String[] ar) {
		JTableTest test=new JTableTest("reakwon");
	}
}

JTable read value

여기까지 JTable에 대한 기본적인 행 추가, 삭제에 대해서 알아보았습니다. 기본적인 사용법만 읽힌다면 어렵지 않게 사용할 수 있을 것 같습니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

JList - 단일 선택 모드(SingleSelectionMode)

(다중 선택 모드는 아래쪽에서 설명합니다.)

Swing 컴포넌트 중에서 자료를 추가하고 삭제할 수 있도록 만든 컴포넌트가 있습니다. 그게 JList인데요. 여러분들도 흔하게 접한 GUI이기도 할겁니다. 글로 설명하는 것보다는 아래의 캡처 화면으로 보시면 "아, 이거구나." 하실겁니다. JList는 하나의 항목을 선택할 수도 있고 다중 선택으로 여러 개의 항목을 선택할 수도 있습니다. 

 

그냥 JList의 메소드를 주구 장창 이야기하고 설명하는 것보다 코드를 구현하면서 그때 그때 필요한 메소드와 사용법을 설명하는 것이 재밌을 것 같습니다. 이제 아래의 프로그램을 Code를 일일히 해석해가면서 구현해볼까요?

 

 

JList Test Program

 

위의 프로그램은 이렇게 동작합니다.

1. 추가 -  JTextField를 통해서 문자열을 입력받아서 추가 또는 엔터를 누르면 JList에 추가가 됩니다. TextField가 비었으면 동작하지 않습니다.

2. 삭제 - 삭제를 누르게 되면 가장 위쪽의 항목이 삭제됩니다. 만약 항목을 선택한 상태에서 삭제 버튼을 누르면 선택된 항목이 삭제됩니다.

3. 선택 - 선택을 누르면 별 다른 것은 없고 컴퓨터 화면에 출력해줍니다. 

 

1. 초기 설정

public class JListTest extends JFrame implements MouseListener,KeyListener,ListSelectionListener{
	
	
	private JList list;				//리스트
	private JTextField inputField;	//테스트 입력 Field
	private JButton addBtn;		//추가 버튼
	private JButton delBtn;		//삭제 버튼
	
	private DefaultListModel model;	//JList에 보이는 실제 데이터
	private JScrollPane scrolled;
	
	public JListTest(String title) {
		super(title);
		init();
	}

	public void init() {
		model=new DefaultListModel();
		list=new JList(model);
		inputField=new JTextField(35);
		addBtn=new JButton("추가");
		delBtn=new JButton("삭제");
		
		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);	//하나만 선택 될 수 있도록
		
		inputField.addKeyListener(this);	//엔터 처리
		addBtn.addMouseListener(this);		//아이템 추가
		delBtn.addMouseListener(this);		//아이템 삭제
		list.addListSelectionListener(this);	//항목 선택시
		
		this.setLayout(new BorderLayout());
		
		
		JPanel topPanel=new JPanel(new FlowLayout(10,10,FlowLayout.LEFT));
		topPanel.add(inputField);
		topPanel.add(addBtn);
		topPanel.add(delBtn);		//위쪽 패널 [textfield]  [add] [del]
		topPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));	//상, 좌, 하, 우 공백(Padding)
		
		scrolled=new JScrollPane(list);
		scrolled.setBorder(BorderFactory.createEmptyBorder(0,10,10,10)); 
		
		this.add(topPanel,"North");
		this.add(scrolled,"Center");	//가운데 list
		
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(620,400);
		this.setLocationRelativeTo(null);	//창 가운데 위치
		this.setVisible(true);
		
	}
}

 

우선 JFrame을 상속받고 Component들을 배치시켜줍니다. 그리고 필요한 EventListener를 지정해주죠. 여기서 필요한 Listener는 전부 JListTest에 구현하도록 하겠습니다.

JList를 사용할때는 단순 문자열같은 아이템을 추가하고자 한다면 DefaultListModel을 사용합니다. 이것을 JList의 생성자로 전달하면 JList는 DefaultListModel을 읽어서 리스트에 보여줍니다. 그리고 항목이 JList의 크기를 넘어갈때 마우스 스크롤이 되어야겠지요? 그래서 JScrollPane을 사용하여 스크롤되게 해줍니다. 이때 JList에 setPreferredSize를 사용하여 사이즈를 지정해버리면 JScrollPane이 정상동작하지 않습니다.

 

2. 이벤트 핸들링

 

 

public class JListTest extends JFrame implements MouseListener,KeyListener,ListSelectionListener{
	//... 생략...//
	public void init() {
		//...생략...//
	}

	//MouseListener
	@Override
	public void mouseClicked(MouseEvent e) {
		
	}
	@Override
	public void mousePressed(MouseEvent e) {}
	@Override
	public void mouseReleased(MouseEvent e) {}
	@Override
	public void mouseEntered(MouseEvent e) {}
	@Override
	public void mouseExited(MouseEvent e) {}

	//KeyListener
	@Override
	public void keyTyped(KeyEvent e) {}
	@Override
	public void keyPressed(KeyEvent e) {}
	@Override
	public void keyReleased(KeyEvent e) {
		
	}

	
    //ListSelectionListener
	@Override
	public void valueChanged(ListSelectionEvent e) {
	}
	
}

 

우리가 구현할 것은 버튼이 클릭됐을때 추가, 삭제 기능이 되어야하기 때문에 mouseClicked 메소드를 구현하고, 키보드 엔터 처리가 되어야하기 때문에 keyReleased() 메소드, 아이템이 선택됐을때 처리를 하기 위해 valueChanged()를 구현해야합니다. 

2-1. 추가

버튼 클릭시에는 텍스트가 입력이 되었는지 안되었는지부터 확인하고 텍스트가 입력되었다면 list항목에 추가해주면 됩니다. 아까 실 데이터는 DefaultListModel 객체에 존재한다고 했었죠. DefaultListModel의 addElement 메소드를 사용하면 추가됩니다. 추가는 addItem()이라는 메소드로 처리하도록 합시다.

	public void addItem() {
		String inputText=inputField.getText();
		if(inputText==null||inputText.length()==0) return;
		model.addElement(inputText);
		inputField.setText("");		//내용 지우기
		inputField.requestFocus();	//다음 입력을 편하게 받기 위해서 TextField에 포커스 요청
		//가장 마지막으로 list 위치 이동
		scrolled.getVerticalScrollBar().setValue(scrolled.getVerticalScrollBar().getMaximum());
	}

 

나머지는 편하게 항목을 추가하기 위한 작업인데요. 내용을 지운 후 다음 입력을 바로 받기 위해서 포커싱을 요청합니다. 그리고 맨 아랫줄의 scrolled 메소드는 가장 마지막 항목으로 이동하기 위해서 추가했습니다.

이 메소드는 addBtn이 클릭했을때 호출하면 되겠네요.

@Override
	public void mouseClicked(MouseEvent e) {
		if(e.getSource() == addBtn) {
			addItem();
		}
		
	}

 

그리고 엔터가 눌렸을 경우도 역시 addItem()을 호출하면 됩니다.  엔터 처리는 별것 없죠?

 

 

	@Override
	public void keyReleased(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if(keyCode==KeyEvent.VK_ENTER) {
			addItem();
		}
	}

 

2-2. 삭제

이제 삭제를 구현해볼까요? 삭제하는 메소드는 removeItem(int index) 이라고 정의하도록 합시다. 만약 index가 0보다 작은 경우는 선택된 항목이 없다는 것으로 가정하겠습니다. 이때 선택된 항목이 없는 경우는 두가지가 존재하겠죠? 아예 추가된 항목이 없는 경우(1), 그리고 항목은 존재하지만 선택하지 않은 경우(2)입니다.

첫번째 경우(1)는 list의 항목들이 있는지 봐야하는데 이때 실데이터가 있는 DefaultListModel의 size() 메소드를 보면 됩니다. 만약 size()가 0이라면 추가된 항목이 없는것이니까 아무것도 하지 않고 빠져나옵니다. 

size가 0보다 큰 경우는 두번째 경우(2)이므로 가장 맨 위쪽의 index인 0을 지정하여 삭제합니다. 삭제하는 메소드는 DefaultListModel의 remove() 메소드를 사용하면 됩니다.

결국 removeItem() 메소드는 아래와 같이 구현됩니다.

 

	public void removeItem(int index) {
		if(index<0) {
			if(model.size()==0) return;	//아무것도 저장되어 있지 않으면 return
			index=0;	//그 이상이면 가장 상위 list index
		}
		
		model.remove(index);
	}

 

삭제버튼이 눌렸을때 처리해야하는데, 선택된 아이템의 index를 가져오는 메소드는 JList의 getSelectedIndex()메소드를 사용하면 됩니다. 만약 선택된 index가 없다면 이 메소드는 -1을 반환하게 됩니다. 이 반환값을 그대로 removeItem()에 전달하면 되겠네요.

그래서 삭제 버튼이 눌렸을때는 아래와 같이 구현됩니다.

	@Override
	public void mouseClicked(MouseEvent e) {
		if(e.getSource() == addBtn) {
			addItem();
		}
		if(e.getSource() == delBtn) {
			int selected=list.getSelectedIndex();
			removeItem(selected);
		}
	}

 

만약 -1의 값을 처리하지 않을 경우 아래와 같이 ArrayIndexOutOfBoundsException 예외가 발생합니다.

Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 10
...

 

2-3. 항목이 눌렸을때

항목이 새롭게 선택되면 ListSelectionListener의 valueChanged()메소드가 호출됩니다. 그래서 여기서 처리를 해주면 되는데, 우리는 단순히 시스템 output stream(화면)에 출력하기로 했죠. 

 

 

	@Override
	public void valueChanged(ListSelectionEvent e) {
		if(!e.getValueIsAdjusting()) {	//이거 없으면 mouse 눌릴때, 뗄때 각각 한번씩 호출되서 총 두번 호출
			System.out.println("selected :"+list.getSelectedValue());
		}
	}

 

 

e.getValueIsAdjusting()이라는 메소드를 통해서 값 조정 중이 아닐때에만 출력하도록 구현했습니다. vaueChanged는 여러번 호출되는데, 그렇기 때문에 만약 if 조건문이 없다면 여러번 출력됩니다.

 

3. 전체 구현 코드

이제 전부 구현했습니다. JListTest 전체 코드는 아래에 존재합니다. import는 코드의 간결함을 위해 모두 포함했습니다.

 

- JListTest.java

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.*;

public class JListTest extends JFrame implements MouseListener,KeyListener,ListSelectionListener{
	
	
	private JList list;				//리스트
	private JTextField inputField;	//테스트 입력 Field
	private JButton addBtn;		//추가 버튼
	private JButton delBtn;		//삭제 버튼
	
	private DefaultListModel model;	//JList에 보이는 실제 데이터
	private JScrollPane scrolled;
	
	public JListTest(String title) {
		super(title);
		init();
	}

	public void init() {
		model=new DefaultListModel();
		list=new JList(model);
		inputField=new JTextField(35);
		addBtn=new JButton("추가");
		delBtn=new JButton("삭제");
		
		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);	//하나만 선택 될 수 있도록
		
		inputField.addKeyListener(this);	//엔터 처리
		addBtn.addMouseListener(this);		//아이템 추가
		delBtn.addMouseListener(this);		//아이템 삭제
		list.addListSelectionListener(this);	//항목 선택시
		
		this.setLayout(new BorderLayout());
		
		
		JPanel topPanel=new JPanel(new FlowLayout(10,10,FlowLayout.LEFT));
		topPanel.add(inputField);
		topPanel.add(addBtn);
		topPanel.add(delBtn);		//위쪽 패널 [textfield]  [add] [del]
		topPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));	//상, 좌, 하, 우 공백(Padding)
		
		scrolled=new JScrollPane(list);
		scrolled.setBorder(BorderFactory.createEmptyBorder(0,10,10,10)); 
		
		this.add(topPanel,"North");
		this.add(scrolled,"Center");	//가운데 list
		
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(620,400);
		this.setLocationRelativeTo(null);	//창 가운데 위치
		this.setVisible(true);
		
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		if(e.getSource() == addBtn) {
			addItem();
		}
		if(e.getSource() == delBtn) {
			int selected=list.getSelectedIndex();
			removeItem(selected);
		}
	}
	
	public void removeItem(int index) {
		if(index<0) {
			if(model.size()==0) return;	//아무것도 저장되어 있지 않으면 return
			index=0;	//그 이상이면 가장 상위 list index
		}
		
		model.remove(index);
	}

	public void addItem() {
		String inputText=inputField.getText();
		if(inputText==null||inputText.length()==0) return;
		model.addElement(inputText);
		inputField.setText("");		//내용 지우기
		inputField.requestFocus();	//다음 입력을 편하게 받기 위해서 TextField에 포커스 요청
		//가장 마지막으로 list 위치 이동
		scrolled.getVerticalScrollBar().setValue(scrolled.getVerticalScrollBar().getMaximum());
	}
	//MouseListener 
	@Override
	public void mousePressed(MouseEvent e) {}
	@Override
	public void mouseReleased(MouseEvent e) {}
	@Override
	public void mouseEntered(MouseEvent e) {}
	@Override
	public void mouseExited(MouseEvent e) {}

	//KeyListener
	@Override
	public void keyTyped(KeyEvent e) {}
	@Override
	public void keyPressed(KeyEvent e) {}

	@Override
	public void keyReleased(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if(keyCode==KeyEvent.VK_ENTER) {
			addItem();
		}
	}

	
	//ListSelectionListener
	@Override
	public void valueChanged(ListSelectionEvent e) {
		if(!e.getValueIsAdjusting()) {	//이거 없으면 mouse 눌릴때, 뗄때 각각 한번씩 호출되서 총 두번 호출
			System.out.println("selected :"+list.getSelectedValue());
		}
	}
	
}

 

JListTest를 Main함수에서 생성하면 됩니다. 어렵지 않죠.

 

 

- Main.java

public class Main {
	
	public static void main(String[] ar) {
		JListTest frame=new JListTest("JList Test");
	}
}

 

 

JList - 다중 선택 모드(Multiple Selection Mode)

다중 선택을 할수도 있습니다. 아래와 같이 JList에 setSelectionMode를 MULTIPLE_INTERVAL_SELECTION으로 변경하면 가능하죠. 

list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);	//여러개 선택 될 수 있도록

 

1. 선택된 항목들 읽어오기 

만약 여러개 선택된 값을 확인하고 싶다면 JList의 getSelectedValuesList() 메소드를 통해 List Collection으로 선택된 항목들을 가져올 수 있습니다. 그래서 다중 선택의 valueChanged() 버전은 아래와 같이 구현됩니다.

	//ListSelectionListener
	@Override
	public void valueChanged(ListSelectionEvent e) {
		if(!e.getValueIsAdjusting()) {	
			List<String> ls=list.getSelectedValuesList();
			for(String value:ls) {
				System.out.println("selected : "+value);
			}
		}
	}

 

2. 선택된 항목들 삭제하기

삭제하는 것은 조금 생각해봐야하는데요. getSelectedIndices() 메소드로 선택된 인덱스들을 정수형 배열로 가져올 수 있습니다. 이 배열은 오름차순으로 정렬된 배열입니다. 그 배열은 removeItem() 전달해주도록 하겠습니다.

	if(e.getSource() == delBtn) {
		int []selected=list.getSelectedIndices();
		removeItem(selected);
	}

 

만약 선택되지 않았다면 배열의 길이는 0이 되니까 선택안된 경우에는 size()와 비교해서 처리하면 되구요. 그것보다는 여러개가 분산적으로 처리됐을때 삭제하는 것이 문제인데, 이때 삭제하는 방법은 거꾸로 인덱스를 돌면서 삭제해야 이런 예외가 발생하지 않습니다.

Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 11 >= 10
...

 

왜냐면 remove하면 길이가 하나씩 줄어드니, for문으로 계속 remove를 하면 사이즈가 계속 줄어들기 때문에 발생하게 됩니다. 이렇게 되면 index가 size()를 넘어설 수가 있기 때문에 size()가 줄어드는 것은 허용하되 index를 넘어가지 않게 거꾸로(내림차순) 돌아주는 겁니다. 

	public void removeItem(int []indices) {
		if(indices.length==0) {
			if(model.size()==0) return;	//아무것도 저장되어 있지 않으면 return
			model.remove(0);	//그 이상이면 가장 상위 list index
			return;
		}
		for(int i=indices.length-1;i>=0;i--) {
			model.removeElementAt(indices[i]);
		}
	}

 

완성된 다중선택 모드의 실행화면입니다.

multiple selected
multiple removed

 

너무 길었네요. 이상으로 JList에 대한 포스팅을 마치도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

RandomeAccessFile

보통의 자바에서는 입력과 출력이 별도로 분리되어있습니다. 그러니까 InputStream이 있고, OutputStream으로 입력받는 스트림, 그리고 출력하는 스트림이 따로 존재하잖아요? 하지만 RandomAccessFile은 예외적으로 RandomAccessFile이라는 객체 하나로 쓰기와 읽기 둘 다 할 수 있습니다. 사실 RandomAccessFile 클래스는 DataInput, 그리고 DataOutput을 둘다 implements 했기 때문에 읽기, 쓰기가 가능합니다. DataStream 중에 DataInputStream클래스가 DataInput, DataOutputStream클래스가 DataOutput을 구현했습니다. 

RandomAccessFile

 

RandomAccess 파일은 다음 데이터를 읽을지 쓸지 가리키는 Pointer가 존재합니다. 맨 처음 파일이 생성되면 가장 처음 위치인 0의 위치를 가리키고 있다가 쓰거나 읽으면 이 포인터가 다음의 위치로 옮겨가게 됩니다.

1. 데이터 쓰기 - write ~()

아래의 코드는 RandomAccessFile에 파일을 기록하는 프로그램을 구현한 건데요.

public static void main(String[] ar) throws IOException{
	RandomAccessFile raf=new RandomAccessFile("raf_file.dat","rw");
	System.out.println("position:" + raf.getFilePointer());	
		
	raf.writeInt(10);
	System.out.println("position:" + raf.getFilePointer());	//4bytes
		
	raf.writeChar('C');
	System.out.println("position:" + raf.getFilePointer());	//2bytes
		
	raf.writeLong(1010L);
	System.out.println("position:" + raf.getFilePointer());	//8bytes
		
	raf.writeByte(8);
	System.out.println("position:" + raf.getFilePointer());	//1byte
	
	raf.close();
		
}

 

출력을 보면 각각 기록하는 자료형에 따라서 그 포인터의 값이 증가함을 알 수 있습니다. 

position:0
position:4
position:6
position:14
position:15

 

아래의 그림처럼 데이터가 자료형에 따라서 포인터의 다음 자료를 저장할 위치를 가리킵니다. 처음은 아무것도 기록되어 있지 않으니 0의 위치에 있고, 다음 정수형 자료를 기록했으니 다음 입력받을 위치는 4의 위치에 기록하면 됩니다. 

 

문자열을 쓸때는 writeUTF() 메소드를 사용합니다. 여기서는 간단하게 세글자를 써보도록 하겠습니다. 

 
	   //write
	raf.writeUTF("RAF");
	System.out.println(raf.getFilePointer());
		

그렇다면 결과는 3이 되어야하지 않을까요? 하지만 결과는 5가 나오게 됩니다. 그 이유는 파일에 문자열을 쓰는 것은 문제가 없는데 읽을때가 문제가 됩니다. 어디까지 읽어야하는지 알수 없게 되거든요. 그래서 앞 2바이트는 문자열의 길이를 기록합니다. 그래서 총 5바이트가 되는 것이죠.

 

2. Data 읽기 - read~ ()

기록을 해봤으니 이제 데이터를 읽어와볼까요? 읽어오는 메소드도 write와 같은 형태의 메소드명으로 만들어져있습니다. 만약 Int형의 자료를 읽으려면 readInt() 메소드를 사용하면 되겠죠? 아래의 코드가 읽는 코드를 구현한 것입니다.

public static void main(String[] ar) throws IOException{
	RandomAccessFile raf=new RandomAccessFile("raf_file.dat","rw");
    
    //write
	raf.writeInt(10);	
	raf.writeChar('C');	
	raf.writeLong(1010L);	
	raf.writeByte(8);
    
    //read
	System.out.println(raf.readInt());
	System.out.println(raf.readChar());
	System.out.println(raf.readLong());
	System.out.println(raf.readByte());
	
	raf.close();
		
}

 

결과는 EOFException이 발생하여 프로그램이 종료됩니다. EOF(End Of File)는 파일의 마지막이라는 뜻으로 이미 끝인 파일에 read메소드를 수행해서 발생합니다.

Exception in thread "main" java.io.EOFException
	at java.base/java.io.RandomAccessFile.readInt(RandomAccessFile.java:841)
	at reakwon.Main.main(Main.java:17)

 

3. 파일 포인터(위치) 변경 - seek()

이쯤에서 위의 포인터를 그린 그림을 살짝 보면 우리는 이미 포인터가 15의 위치로 이 위치가 EOF였습니다. 그러다가 read로 그 지점부터 읽다가 보니 바로 EOF 예외가 발생하게 되었죠. 따라서 파일 포인터 위치를 맨 처음으로 되돌려야합니다. 그럴때 사용할 수 있는 메소드가 seek 메소드입니다. 여기서 0이 파일의 맨 처음을 의미합니다. 읽기 전에 seek() 메소드로 되돌려보면 정상적으로 읽히고 있음을 확인할 수 있습니다.

		//... 중략 ...
		raf.writeByte(8);
	    
		raf.seek(0);
	    //read
		System.out.println(raf.readInt());
        //... 중략    ...    
}
10
C
1010
8

 

4. 파일의 크기 - length()

파일의 크기를 구하고 싶다면 length() 의 함수를 쓰면 파일의 크기를 구할 수 있습니다. 사실 파일의 크기이기도 하지만  다음 파일이 기록될 위치이기도 하며 EOF입니다. 이때 byte단위로 파일의 크기가 return 됩니다.

이상으로 RandomAccessFile에 대해서 알아보았습니다. 파일을 읽기, 쓰기 스트림 따로 두지 않고 하나의 객체를 통해서 읽기, 쓰기를 한다는 개념입니다. 이때 읽기, 쓰기에 따라 파일의 pointer가 달라진다는 점을 기억하시면 무엇인가 에러가 발생했을때 가장 처음 의심해볼 수 있습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

AWT Event

Java나 다른 언어에서 Event라는 것은 특정 사건의 발생함을 의미합니다. 사용자가 행동하는 모든 것이 프로그램에서 이벤트로 동작이 될 수 있습니다. 버튼을 마우스로 클릭하거나 키보드로 입력을 받거나, 창을 이동하거나 마우스를 단순히 이동하는 것 등이 Event라고 합니다. 그리고 그 Event가 발생했을때 어떻게 프로그램을 동작시킬지 처리하는 것을 이벤트 핸들링(Event Handling)이라고 합니다. 자바에서 Event가 발생했을때 처리가능하도록 Interface를 정의해두었는데, 그 인터페이스 이름들은 뒤에 ~Listener가 붙게 됩니다. 예를 들어 Mouse에 관한 이벤트 처리 인터페이스는 MouseListener, 그리고 윈도우 창에 관한 이벤트를 처리할 수 있는 인터페이스는 WindowListener입니다. 우리는 Event가 발생했을때 어떻게 다뤄질 수 있는지 WindowListener를 통해서 알아보도록 하겠습니다.

 

EventHandling - 이벤트 처리 방법

이벤트 핸들링 위해 ~Listener라는 인터페이스가 존재한다고 했었죠. 이것을 implements로 상속받아 어떤 이벤트를 처리할지 구현하면 됩니다. 그래서 상속받은 인터페이스가 구현이 되면 그 이벤트에 영향을 받을 Component에 추가해주면 됩니다. 대부분읜 여기서는 WindowListener를 통해서 어떻게 처리하는지 알아본다고 했었죠. 그래서 WindowListener를 implements해보고 내부를 채워보도록 합시다.

WindowHandler 클래스를 만들고 WindowListener를 implements해봅시다.

class WindowHandler implements WindowListener{
}

 

이렇게 되면 WindowHandler에 밑줄이 쫙 그어지는데, 마우스를 가져다대면 아래에 Add unimplemented methods가 생십니다. 이것을 클릭해서 모든 메소드를 구현해줍니다.

 

아래처럼 아직은 메소드가 구현이 되어있지 않은 빈 클래스입니다. 

class WindowHandler implements WindowListener{

	@Override
	public void windowOpened(WindowEvent e) {}
	@Override
	public void windowClosing(WindowEvent e) {}
	@Override
	public void windowClosed(WindowEvent e) {}
	@Override
	public void windowIconified(WindowEvent e) {}
	@Override
	public void windowDeiconified(WindowEvent e) {}
	@Override
	public void windowActivated(WindowEvent e) {}
	@Override
	public void windowDeactivated(WindowEvent e) {}
}

 

AWT의 Frame에 관해서 배우셨다면 Frame의 X버튼을 클릭해도 닫아지지 않습니다. 이유는 우리가 X버튼을 눌렸을때 발생하는 이벤트에 대해서 처리해주지 않았기 때문입니다. 이 이벤트는 이벤트에 관한 정보를 WindowEvent에 담아서  WindowListener의 windowClosing() 메소드를 호출합니다. 아래의 코드를 구현하면 프로그램이 종료됩니다.

	@Override
	public void windowClosing(WindowEvent e) {
		Window w=e.getWindow();	
		w.setVisible(false);	//frame을 invisible	
		w.dispose();			//메모리 제거
		System.exit(0);					//시스템 종료
	}

 

WindowEvent에는 이벤트가 발생한 Component를 알 수 있습니다. 여기서는 단일 Frame을 사용하기 때문에 Frame을 구분할 필요가 없지만 만약 여러 프레임을 사용해서 같은 WindowHandler를 사용한다면 getComponent()를 통해서 Frame을 구분할 수 있습니다. 이벤트 클래스는 ComponentEvent를 상속받기 때문에 WindowEvent객체 뿐만 아니라 ComponentEvent를 상속받은 모든 Event클래스는 getComponent()를 통해서 객체를 구분할 수 있습니다.

해당하는 Window객체를 getWindow() 메소드로 가져올 수 있습니다. 

이벤트 리스너가 구현이 되었다면 이제 컴포넌트에 연결을 지어야겠죠? 해당 컴포넌트의 add~Listener()  메소드가 그 역할을 합니다. 여기서는 addWindowListener() 메소드입니다.

	public static void main(String[] ar) {
		Frame frame=new Frame("reakwon");
		frame.setSize(500,300);		//크기 설정
		frame.setLocationRelativeTo(null);	//중앙위치
		
		frame.addWindowListener(new WindowHandler());	//이벤트 설정
		
		frame.setVisible(true);	//Frame visible
	}

 

그리고 난 후에 프로그램을 실행시켜보고 X버튼을 누르면 프로그램이 종료되는 것을 확인할 수 있습니다. 

 

단순히 종료시키는 것이 아니라 "정말 종료하시겠습니까?" 대화창을 열어 확인해볼 수도 있겠죠. 여러분이 응용하기 나름입니다.

이벤트 리스너를 연결하는 방법

1. Component가 Listener를 implements하여 자신을 add하는 방법

이벤트를 연결하는 것중에는 아예 이벤트 리스너를 해당 Component가 상속받는 형태로 구현할 수도 있습니다. 아래의 방식처럼 말이죠.


class MyFrame extends Frame implements WindowListener{

	public MyFrame(String title) {
		super(title);
		this.setSize(500,300);
		this.setLocationRelativeTo(null);
		this.addWindowListener(this);	//WindowListener를 이곳에서 구현
		this.setVisible(true);
	}
	@Override
	public void windowOpened(WindowEvent e) {}
	@Override
	public void windowClosing(WindowEvent e) {	
		this.setVisible(false);	//frame을 invisible	
		this.dispose();			//메모리 제거
		System.exit(0);					//시스템 종료
	}
	@Override
	public void windowClosed(WindowEvent e) {}
	@Override
	public void windowIconified(WindowEvent e) {}
	@Override
	public void windowDeiconified(WindowEvent e) {}
	@Override
	public void windowActivated(WindowEvent e) {}
	@Override
	public void windowDeactivated(WindowEvent e) {}
}




 

 

2. 익명 클래스(Anonymouse class) 방식으로 연결

또는 익명 클래스로 바로 리스너를 구현해서 집어넣는 방법으로도 이벤트 리스너 연결이 가능합니다.

class MyFrame extends Frame{

	public MyFrame(String title) {
		super(title);
		this.setSize(500,300);
		this.setLocationRelativeTo(null);
		this.addWindowListener(new WindowListener() {
			@Override
			public void windowOpened(WindowEvent e) {}
			@Override
			public void windowClosing(WindowEvent e) {
				
				MyFrame.this.setVisible(false);	//frame을 invisible	
				MyFrame.this.dispose();			//메모리 제거
				System.exit(0);					//시스템 종료
			}
			@Override
			public void windowClosed(WindowEvent e) {}
			@Override
			public void windowIconified(WindowEvent e) {}
			@Override
			public void windowDeiconified(WindowEvent e) {}
			@Override
			public void windowActivated(WindowEvent e) {}
			@Override
			public void windowDeactivated(WindowEvent e) {}
		});	
		this.setVisible(true);
	}
	
}

 

2.1익명 클래스시 자주하는 실수

1. 익명 클래스 방식으로 구현할때 주의해야할 점은 this가 WindowListener 자체를 가리키는 객체를 말합니다. 그래서 Component를 가리키게 하려면 앞에 클래스명을 명시적으로 알려줘야합니다.

2. 아래의 코드가 에러가 발생하는 이유는 무엇일까요? 

	public MyFrame(String title) {
		//...생략...//
		int count=0;
		this.addWindowListener(new WindowListener() {
		
			@Override
			public void windowClosing(WindowEvent e) {
				count++;
                //...생략...//
			}
			
		});	
	}

 

저는 분명 count가 하나 증가하기를 기대하고 코드에 넣었는데, 빨간 밑줄인 에러가 발생합니다. 마우스를 가져다 대보니 final을 쓰라고 하네요.

 

왜 일까요? 여러분은 두가지를 간과하고 계실 겁니다. 

첫번째는 코드가 순차적으로 실행될 것이라는 믿음, 그리고 두번째 지역변수는 함수가 끝나면 정리된다는 것을 까먹은 것이죠. 위 이벤트 리스너의 실행은 이벤트가 발생했을때의 수행하는 것이지, 순차적 실행이 아닙니다. 그리고 생성자가 종료된 후에는 count라는 지역변수는 메모리에 없습니다. 그래서 증가시킬 수가 없죠. 

해결방법은 지역변수의 count를 final로 선언하여 값을 변경하지 않는것이고, 값을 변경하길 원한다면 아예 클래스(여기서는 MyFrame)의 멤버 변수로 선언하는 방법입니다.

지금까지 이벤트의 개념과 이벤트 핸들링을 하는 방법을 설명했는데, 이것을 일일히 설명하는 것보다는 아예 프로그램을 만들면서 그때그때 설명하는 것이 더 기억에 많이 남고 재밌을 것 같습니다. 그래서 시간이 된다면 프로그램 자체를 만들면서 설명해보도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

스캐너 (Scanner)

스캐너는 여러분들이 키보드로 입력받거나, 파일로 입력받거나 문자열로 입력을 받는데 도움을 주는 아주 유용한 클래스로 JDK1.5 버전부터 추가된 클래스입니다. 입력에 아주 유용한 클래스이기 때문에 반드시 알아야할 필요가 있습니다. 

Scanner클래스는 키보드, 파일, 문자열 등으로 입력받기 위해 다양한 생성자를 제공하며 다양한 자료형을 입력받을 수 있지요. Scanner에서 입력을 받으려면 next[자료형]() 메소드를 이용하면 되는데, 여기서 next~() 메소드를 표로 정리하였습니다. 이 메소드들과 나머지 유용한 Scanner 메소드의 사용법은 아래의 예를 통해서 사용하며 익히게 될 겁니다. Scanner의 생성자는 다양한데 항상 두번째 argument는 인코딩 방식의 Charset의 이름입니다. 예를 들어 UTF-8인지 ANSI인지 지정할 수 있죠. 지정하지 않는다면 OS에서 지정된 default charset으로 지정됩니다. 보통 쓸일은 없습니다.

 

Scanner의 next~() 메소드


입력받는 자료형과 다른 자료형의 데이터가 읽혀지면 InputMismatchException이 발생하니 상황에 맞춰 예외처리를 하시기 바랍니다. 모든 nextLine()을 제외한 next~() 메소드는 구분자가 공백입니다.

메소드명 설명
nextBoolean() boolean의 자료형을 입력받습니다. 자료형은 두개인 true,false로 대소문자를 구분하지 않습니다.  
nextByte() byte의 자료형을 입력으로 받습니다. 입력 범위(127~ -128) 밖이면 InputMismatchException이 발생합니다.
nextShort() short의 자료형을 입력받습니다.
nextInt() int형의 자료형을 입력받습니다. 
nextLong() long형의 자료형을 입력받습니다.
nextDouble() double형의 자료형을 입력받습니다.
nextFloat() float형의 자료형을 입력받습니다.
next() String형의 문자열을 입력받습니다. 이때 공백 문자까지 입력받습니다.  
nextLine() 문자열을 입력받는데 다른 next~() 메소드와 다른 점은 줄단위로 입력받는다는 점입니다. 

 

 

지금 보여드리는 사용법은 Scanner의 아주 기초적인 키보드에서 입력을 받는 예입니다.

키보드 입력

Scanner는 InputStream을 생성자로 받을 수 있는데, 여기서 키보드 입력을 받으려면 System.in의 InputStream을 넘겨주면 됩니다. 

Scanner in=new Scanner(System.in);

 

아래의 예제는 nextInt()로 문자열의 입력받을 갯수를 입력받아 for 루프로 단어를 입력받는 예제입니다.

public static void main(String[] ar) {
	Scanner in=new Scanner(System.in);
	int N=in.nextInt();
	String []strs=new String[N];
	for(int i=0;i<N;i++) 
		strs[i]=in.next();
		
	for(int i=0;i<N;i++)
		System.out.println("String["+i+"] : "+strs[i]);
		
}

 

정수 입력을 3, 그리고 문자열을 공백으로 주어 입력받은 결과입니다.

3
My Name reakwon
String[0] : My
String[1] : Name
String[2] : reakwon

 

문자열에서 입력

Scanner는 문자열로도 입력을 받을 수 있습니다. 

	String str="2 boys and 2 girls";
	Scanner in=new Scanner(str);
		
	int num1 = in.nextInt();
	String word1 = in.next();
	String word2 = in.next();
	int num2 = in.nextInt();
	String word3 = in.next();
	System.out.println(num1+" "+word1+" "+word2+" "+num2+" "+word3);

 

위의 str의 문자열은 입력된 형식을 정확히 안다는 가정하에 코딩한 것입니다. 2는 int형, 그리고 boys는 문자열 이렇게 Scanner의 메소드로 입력을 받는 예제입니다. 여기서 모든 입력은 공백으로 구분되어 집니다. 아래는 그 결과입니다.

2 boys and 2 girls

 

파일에서의 입력

이번에는 파일을 입력 삼아서 읽어볼까요? 아래의 파일이 프로젝트와 같은 디렉토리에 존재한다고 해봅시다. 아래에 input.txt를 만들어주세요.

위 파일은 모두 실수이죠?  그래서 nextDouble()메소드를 사용할 것인데 이때 double의 자료형이 있는지 검사하며 자료형이 존재하면 내용을 읽어서 출력하고 싶습니다. 이때 사용할 수 있는 메소드가 hasNext[자료형] 입니다.

public static void main(String[] ar) throws IOException {
		
	Scanner in=new Scanner(new File("input.txt"));
	System.out.println("파일로부터 실수 입력");
	while(in.hasNextDouble()) {
		System.out.println(in.nextDouble());
	}
}

 

파일에서 입력을 받을때는 IOException이 발생할 수 있으므로 throws 키워드를 사용해서 IOException을 처리해줍니다. while문에 보이는 hasNextDouble()이 보이시나요? 이 메소드가 다음에 double의 자료형을 갖는 데이터가 있냐고 물어보는 메소드로 있다면 true, 아니면 false를 반환합니다. 나머지의 int, float, boolean 역시 저 메소드 명의 형식을 따라가니까 사용해보시면 됩니다. 여기서는 굳이 정리하지 않겠습니다.

구분자 정하기(Delemiter)

구분자는 기본적으로 공백인데 필요에 따라서는 우리가 정해야될 필요가 있습니다. 그럴때 사용할 수 있는 메소드가 useDelimiter() 메소드입니다. useDelimiter()는 두가지로 오버로딩이 되어있는 메소드인데, 단순 문자열로만 구분할 수 있는 메소드와 정규표현식을 사용하여 쓸 수 있는 구분자가 사용됩니다. 여기서 한번 구분자를 우리가 정해보도록 합시다. 아까 사용했던 input.txt파일을 이렇게 바꿔봅시다.

 

라인 단위로 -로 실수가 구분되어있습니다. 이것을 방금 전에 결과와 같게 만든다면 어떻게 구현하면 좋을까요? 우선 line단위로 입력을 받는다면 그 line은 "실수1-실수2-실수3..." 으로 구분되어있겠죠. 그리고 "-"를 구분자로 삼아서 nextDouble() 메소드로 읽어주면 됩니다. 그래서 Scanner가 2개가 필요합니다. 아래의 코드가 그 구현을 보여줍니다.

public static void main(String[] ar) throws IOException {
		
	Scanner in=new Scanner(new File("input.txt"));
	System.out.println("파일로부터 실수 입력");
		
	while(in.hasNextLine()) {
		Scanner line=new Scanner(in.nextLine());	//Line단위의 입력을 Scanner의 전달
		line.useDelimiter("-");						//"-"을 구분자로 사용
			
		while(line.hasNextDouble()) {				//다음 Double 자료형이 있는가?
			System.out.println(line.nextDouble());	//있다면 Double 출력
		}
	}
}

 

코드의 결과는 동일합니다.

파일로부터 실수 입력
1.1
2.2
3.3
4.4
5.5
11.1
12.2
13.3
14.4
15.5

 

이렇게 Scanner에 대한 사용법에 대해서 알아보았습니다. Scanner는 Stream과 정규 표현식을 자세히 알면 유용한 클래스입니다. 이상으로 Scanner에 대한 코드와 설명이었습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

직렬화(Serialization)

우리는 파일에 텍스트를 기록하고, 이진 데이터를 기록하는 방법은 많이들 알고 계시겠습니다. 그런데 만약 이런 종류의 데이터들이 아니라 객체를 파일로 저장하거나 읽어올 수 있을까요? 네, 물론 있습니다. 여러분이 직렬화만 배운다면 할 수 있습니다. 

직렬화에 대한 아주 직관적인 그림은 아래에 나타내어져있는데요. Account라는 클래스는 email, 이름, 주소, 폰 번호, 등록 일자를 멤버로 갖는 클래스입니다. 이것을 객체화하여 파일이나 네트워크로 write할때는 직렬화를 거쳐서 전달됩니다. 반대로 읽어올때는 역직렬화(Deserialization)를 거쳐서 가져오게 되지요. 

 

직렬화에 메소드는 포함하지 않습니다. 메소드는 각 클래스가 같은 동작을 수행하기 때문에 직렬화해서 저장할 필요는 없습니다. 단지 멤버들마다 다른 값을 가지고 있는 필드들이 직렬화가 됩니다. 

이제 본격적으로 직렬화는 어떻게 수행해야할까요? 우리는 두가지만 알고 있으면 됩니다. 

1. 직렬화가 가능한 클래스를 구현하기

2. 직렬화가 된 클래스의 객체를 쓰고 읽는 Stream 준비하기

 

1. 직렬화가 가능한 클래스 구현하기

이건 간단합니다. 직렬화를 하려면 우선 Serializable을 인터페이스를 implements를 해야합니다. 그래서 우리가 Account클래스를 정의하려면 아래와 같이 정의가 되어야합니다.

class Account implements Serializable{
	private String email;
	private String name;
	private String address;
	private String phone;
	private Date reg_date;
	public Account(String email,String name,String address,String phone) {
		this.email=email;
		this.name=name;
		this.address=address;
		this.phone=phone;
		reg_date=new Date();
	}
	
	public String getEmail() {
		return email;
	}
	public String getName() {
		return name;
	}
	public String getAddress() {
		return address;
	}
	public String getPhone() {
		return phone;
	}
	public String getRegDate() {
		SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd");
		return format.format(reg_date);
	}
	@Override
	public String toString() {
		return "Information:"+getEmail()+"/"+getName()+"/"+getAddress()+"/"+getPhone()+"/"+getRegDate();
		
	}
}

 

Serializable을 implements하고 있는 것을 볼 수 있네요. 

직렬화 제외하기 - transient

하지만 우리가 직렬화로 파일에다가 기록할때 민감한 데이터이기 때문에 직렬화에 제외하는 방법은 transient라는 키워드를 사용하면 됩니다. 그러면 직렬화에 빠지게 되어 파일에 저장되지 않습니다. 예를 들면 아래와 같이 사용할 수 있겠습니다.

class Account implements Serializable{
	//... 생략 ...
	private Date reg_date;
	private transient String password; 
	public Account(String email,String name,String address,String phone) {
		//...생략...
		reg_date=new Date();
		password="12341234";
	}
	//...생략...
}

 

2. 객체 쓰기 읽기

2-1. 객체 쓰기(ObjectOutputStream)

객체를 쓰려면 Stream을 열어야합니다. 우리는 당장 파일에 그 객체를 쓸 것이기 때문에 FileOutputStream을 사용할 것이고 이 파일 스트림에 객체를 쓸 것이기 때문에 ObjectOutputStream을 사용할 겁니다. 사용법은 아주 간단한데요. 아래의 차례대로 진행하면 됩니다. 우선 FileOutputStream으로 파일의 Stream을 열고 이것을 ObjectOutputStream으로 전달해주면 됩니다.

FileOutputStream fos=new FileOutputStream("user.acc");
ObjectOutputStream oos=new ObjectOutputStream(fos);

 

여기서 user.acc는 우리가 객체를 저장할 파일 이름입니다. 파일을 열때 FileNotFoundException이라는 IOException이 발생할 수 있습니다. 이것을 직접 처리하기가 귀찮으니 우리는 메인 함수에서 throws로 처리하도록 하겠습니다.

public static void main(String[] ar) throws IOException

 

그리고 파일에 써줄 객체를 정의해주고 ObjectOutputStream의 객체 쓰기 메소드인 writeObject에 전달해주면 됩니다. 그리하여 객체를 쓰는 전체코드는 아래와 같습니다. 

public static void main(String[] ar) throws IOException, ClassNotFoundException{
	Account wuser=new Account("reakwon@aaa.ccc","reakwon","seoul","010-1234-1010");
		
	FileOutputStream fos=new FileOutputStream("user.acc");
	ObjectOutputStream oos=new ObjectOutputStream(fos);
	oos.writeObject(wuser);
	oos.close();
}

 

이제 파일이 저장이 되었을텐데 현재 프로젝트 working directory에 있을 겁니다.

 

그리고 메모장으로 그 내용을 까보면 이렇습니다. 중간에 짤리고 알아볼 수 없는 문자들도 있지만 대충 우리가 알 수 있는 것은 멤버의 필드들만 보이는 것을 확인할 수 있고 메소드의 구현부는 확인할 수가 없지요. 물론 우리가 직렬화하지 않길 원하는 멤버 필드(transient로 선언한 멤버 필드) 역시 이곳에 기록되어 있지 않습니다.

 

2-2. 객체 읽기(ObjectInputStream)

이제는 이 파일에 쓰여진 객체를 읽어볼까요? 객체를 읽는데는 파일 읽기 스트림(FileInputStream)을 열고 ObjectInputStream을 통해서 읽어오면 됩니다. 이것 역시 간단하게 ObjectInputStream에 FileInputStream을 전달하고 readObject를 이용해서 객체를 얻어오면 됩니다. 이때 Object객체로 반환하기 때문에 적절히 형변환을 해야합니다. 그래서 객체를 읽는 과정은 아래와 같습니다.

Account ruser=null;
		
FileInputStream fis=new FileInputStream("user.acc");
ObjectInputStream ois=new ObjectInputStream(fis);
ruser=(Account)ois.readObject();

ois.close();

 

혹시나 객체의 클래스를 찾을 수가 없는 예외가 발생할 수 있습니다. 그때 발생할 수 있는 예외가 ClassNotFoundException입니다. 이것도 메인 메소드에서 던져주도록 합시다.

public static void main(String[] ar) throws IOException, ClassNotFoundException{

 

객체를 쓰고 읽는 전체 코드는 이렇습니다. 

public class Main {
	
	public static void main(String[] ar) throws IOException, ClassNotFoundException{
		Account wuser=new Account("reakwon@aaa.ccc","reakwon","seoul","010-1234-1010");
		
		Account ruser=null;
		
		FileOutputStream fos=new FileOutputStream("user.acc");
		ObjectOutputStream oos=new ObjectOutputStream(fos);
		oos.writeObject(wuser);
		
		FileInputStream fis=new FileInputStream("user.acc");
		ObjectInputStream ois=new ObjectInputStream(fis);
		ruser=(Account)ois.readObject();
		
		System.out.println(ruser);
		
		oos.close();
		ois.close();
		
	}
}

 

그래서 읽어보면 제대로 읽어오는 것을 확인할 수가 있네요.

Information:reakwon@aaa.ccc/reakwon/seoul/010-1234-1010/2021-04-07

 

만약 transient로 직렬화에 포함되지 않은 데이터는 null로 읽힙니다.

 

직렬화 클래스 버전 관리

객체의 직렬화나 역직렬화에서 클래스는 완전히 동일한 클래스를 통해서 쓰고 읽혀야합니다. 그렇지 않은 경우는 아래의 에러를 맛 볼 수 있습니다. 여기서 쓸때는 기존의 Account 클래스, 그리고 읽을때는 필드하나를 추가해 변경된 Account클래스로 읽어 보았습니다.

 

Exception in thread "main" java.io.InvalidClassException: aa.Account; local class incompatible: stream classdesc serialVersionUID = 2399026023152107267, local class serialVersionUID = 9178730303496146785
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:722)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2022)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1872)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2179)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1689)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:495)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:453)
	at aa.Main.main(Main.java:15)

 

이렇게 InvalidClassException이라는 예외가 발생하므로 우리는 직렬화 클래스의 버전을 관리해주어야합니다. 자바는 serialVersionUID를 통해서 버전이 같은 클래스인지 아닌지 판단할 수 있습니다. 그래서 이 버전이 같은지 같지 않은지를 통해서 조치를 취해야합니다. 그래서 우리도 위의 serialVersionUID를 사용하여 클래스 버전을 관리할 수 있습니다.

class Account implements Serializable{
	static final long serialVersionUID=1919191919191919L;
    //...생략...
}

 

이처럼 객체를 저장하고 읽을때는 객체가 직렬화가 되어야하고 Object를 쓰는 ObjectOutputStream, Object를 읽는 ObjectInputStream을 사용하여 읽을 수 있습니다. 객체를 파일로 읽고 쓰는 경우는 어떤 경우에 유용할까요? 엑셀 파일을 저장하거나 한글 문서파일을 저장, 그리고 그림판의 파일을 저장하고 불러오는 등 객체를 쓰고 읽어야할 경우는 매우 많으니 알아두시는 게 매우 편합니다.

 

여기서 우리는 File에만 Object를 쓰고 읽었지만 네트워크에서도 가능합니다. 이상으로 객체 직렬화에 대한 포스팅을 마치도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,