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

와나진짜

,

레이아웃 메니저(LayoutManager)

자바에서 컨테이너안에 버튼이나 체크박스 같은 컴포넌트를 보기좋게 배치하는 일은 쉬운 일이 아닙니다. 그럴때 레이아웃 메니저를 사용하면 됩니다. 레이아웃 메니저는 컨테이너가 컴포넌트를 어떻게 배치할 것인지를 도와주는 클래스들입니다. 여기서 소개서는 레이아웃 4개를 소개합니다. 그 4개가 BorderLayout, FlowLayout, GridLayout, CardLayout입니다. 이번 포스팅은 이 4개의 레이아웃 메니저에 대해 무슨 특징이 있고 어떻게 사용하면 되는 것인지 알아보도록 하겠습니다.

공통적으로 컨테이너에게 레이이웃 메니저를 적용시키려면 setLayout메소드로 사용할 LayoutManager를 지정해주시면 됩니다. 

1. BorderLayout

BorderLayout은 컴포넌트를 동, 서, 남, 북, 중앙으로 배치시킵니다. 아래의 코드를 통해서 화면배치가 어떻게 이루어지는 지 확인해봅시다.

public class Main {
	
	public static void main(String[] ar){
		Frame frame=new Frame("Reakwon");
		
		frame.setSize(500,300);
		frame.setLayout(new BorderLayout());
		Button eastButton=new Button("East");
		Button westButton=new Button("West");
		Button southButton=new Button("South");
		Button northButton=new Button("North");
		Button centerButton=new Button("Center");
		
		frame.add("East",eastButton);
		frame.add("West",westButton);
		frame.add("South",southButton);
		frame.add("North",northButton);
		frame.add("Center",centerButton);
		
		frame.setVisible(true);
	}
}

BorderLayout

 

Frame의 add()로 위치도 같이 지정해줍니다. add("위치", Component) 또는 add(Component, "위치")로 구분없이 사용할 수 있습니다. 이때 위치의 종류에는 "East", "West", "South", "North", "Center" 가 있는데, 각각은 동, 서, 남, 북, 가운데의 위치를 나타냅니다. 앞글자는 항상 대문자여야합니다.

Panel 사용하기 - 다른 Component를 더 배치

이렇게 버튼 5개만 추가하기보다는 가운데에 더 많은 Component를 결합하여 Frame을 구성하고 싶다면 어떻게하면 좋을까요? 이때 사용할 수 있는 것이 Panel입니다. Panel은 Frame과 같이 자신의 영역이 있어서 그 안에서 다른 Component를 구성할 수 있습니다. Panel은 두 종류가 있는데, 자신의 영역에서 담고 있는 컴포넌트들이 넘친다면 스크롤되지 않는 Panel과 스크롤될수 있는 ScrollPane이 있습니다. 여기서는 단순 Panel만을 사용하는 예를 보도록 하겠습니다.

public static void main(String[] ar){
	Frame frame=new Frame("Reakwon");
	frame.setSize(500,300);
	frame.setLayout(new BorderLayout());
	Button eastButton=new Button("East");
	Button westButton=new Button("West");
	Button southButton=new Button("South");
	Button northButton=new Button("North");
		
    //Panel의 Component 추가    
	Panel panel=new Panel(new BorderLayout());
	Button panelSouth=new Button("Panel_South");
	Button panelNorth=new Button("Panel_North");
	Button panelCenter=new Button("Panel_Center");
	panel.add("South",panelSouth);
	panel.add("North",panelNorth);
	panel.add("Center",panelCenter);
		
		
	frame.add("East",eastButton);
	frame.add("West",westButton);
	frame.add("South",southButton);
	frame.add("North",northButton);
    //Center에 panel 추가
	frame.add("Center",panel);
		
	frame.setVisible(true);
}

 

Frame과 별 차이가 있나요? Frame과 같이 add로 버튼을 추가할 수 있습니다. 그리고 Frame은 그런 Panel을 추가할 수 있죠. 왜냐면 Panel 역시 Component이니까요. 다음 보이는 배치가 위 코드의 결과입니다.

 

간격 넓히기

BorderLayout의 생성자를 주어서 상하 좌우 간격을 조절 할 수 있습니다. 아래처럼 생성자를 이용하는 방법이 있습니다. 처음 arguement는 Horizontal(좌우)의 간격, 두번째는 Vertical(상하)의 간격입니다.

frame.setLayout(new BorderLayout(10,2));

 

생성자를 사용하지 않고 이미 생성된 객체에서 setHgap과 setVgap을 통해서도 바꿀 수 있습니다. 위의 생성자를 통한 간격은 아래의 메소드를 통한 간격 조정과 동일하게 됩니다.

	BorderLayout layout=new BorderLayout();
	layout.setHgap(10);
	layout.setVgap(2);
	frame.setLayout(layout);

 

어떻게 변하는지 볼까요?

 

2. FlowLayout

이름부터 직관적으로 알 수 있듯이 기본적으로 물 흐르듯 먼저 더해진 Component들이 왼족에서 오른쪽으로 추가되고  모두 가운데로 정렬이 되며 자리가 모자라면 아랫줄에 Component들이 배치됩니다. 아래와 같이 FlowLayout을 사용하여 Layout 배치를 해보도록 하겠습니다.

public class Main {
	
	public static void main(String[] ar){
		Frame frame=new Frame("Reakwon");
		frame.setSize(500,300);
		frame.setLayout(new FlowLayout());
		
		for(int i=1;i<=10;i++) {
			frame.add(new Button("Button"+i));
		}
		
		frame.setVisible(true);
	}
}

 

setLayout으로 frame의 레이아웃을 FlowLayout으로 정하고 난 이후에 Frame에 버튼 10개를 추가해줍니다. 이런 Frame이 결과로 보여질겁니다.

Flowlayout1

 

왼쪽부터 차례대로 버튼이 추가되는 것을 알 수 있습니다. 이 프레임 사이즈를 줄여보세요. 자리가 모자르면 아래처럼 밑으로 버튼들이 내려가는 것을 확인할 수 있습니다.

Flowlayout2

 

정렬하는 방법에는 왼쪽정렬(FlowLayout.LEFT), 가운데 정렬(FlowLayout.CENTER), 오른쪽 정렬(FlowLayout.RIGHT)이 있고 간격도 역시 조정할 수 있습니다. 이 모든것은 생성자를 통해서도 가능하고 메소드를 통해서도 가능합니다. 간격 조정은 BorderLayout과 같고, 정렬하는 메소드는 setAlignment()를 이용하면 됩니다. 

위의 코드에서 생성자를 이렇게 고쳐봅시다.

frame.setLayout(new FlowLayout(FlowLayout.RIGHT,10,20));	//오른쪽 정렬, 좌우 간격, 상하 간격

 

그러면 정렬과 간격이 변한것을 볼 수 있습니다.

 

3. GridLayout

격자형태의 Component를 배치하고 싶다면 GridLayout을 사용하시면 됩니다. 기본적으로 생성자에 아무것도 없다면 한 행에 컴포넌트를 배치합니다. 그래서 행과 열의 수를 지정하여 사용합니다. 

GridLayout을 이용해서 Component를 배치해봅시다.

public static void main(String[] ar){
	Frame frame=new Frame("Reakwon");
	frame.setSize(500,300);
		
	frame.setLayout(new GridLayout(4, 3, 10, 3)); //행, 열 , 좌우 간격, 상하 간격
	for(int i=1;i<=9;i++) {
		frame.add(new Button(""+i));
	}
	frame.add(new Button("*"));
	frame.add(new Button("0"));
	frame.add(new Button("#"));
		
	frame.setVisible(true);
}

 

4행 3열의 격자 형태의 레이아웃을 사용했습니다. 그리고 좌우의 간격, 상하의 간격도 주어 Component를 떨어뜨렸지요. 보통 계산기 같은 버튼의 격자형 배치를 만들때 GridLayout이 많이 사용되며 사진의 격자형 배치고 GridLayout을 사용할 수 있습니다.

GridLayout

 

 

4. CardLayout

CardLayout은 카드를 겹쳐놓은듯이 배치하고 이전의 카드, 다음의 카드를 보여주듯 Component 또는 Container를 보여줍니다. 사용법은 아래와 같습니다.

public static void main(String[] ar){
	Frame frame=new Frame("Reakwon");
	frame.setSize(500,300);
		
	Panel card1=new Panel();
	card1.setBackground(Color.DARK_GRAY);
	card1.add(new Label("Card 1"));
		
	Panel card2=new Panel();
	card2.setBackground(Color.ORANGE);
	card2.add(new Label("Card 2"));
		
	CardLayout layout=new CardLayout();
	frame.setLayout(layout);
		
		//card1의 이름은 "card1" 
	frame.add(card1, "card1");
    	//card2의 이름은 "card2"
	frame.add(card2, "card2");
		
	layout.show(frame, "card1");
	frame.setVisible(true);
	
    //5초 대기
	try {
		Thread.sleep(5000);
	}catch(InterruptedException e) {
		e.printStackTrace();
	}
	//5초 후 frame에 추가한 다음 card를 보여줌
	layout.next(frame);
}

 

여기에는 Panel을 두개 사용하여 card1과 card2라는 변수로 이름을 정해줍니다. 이 둘을 구분하기 위해서 배경색과 이름을 보여주는 Label을 추가했습니다. Frame은 CardLayout으로 세팅하는데, 여기서 객체를 계속사용해야하니까 객체를 생성하여 setLayout의 인자로 전달해줍니다. 그리고 차례대로 card1, card2를  Frame에 추가하죠. 이제 가장 처음 보여야할 container를 정하는데 show()메소드를 이용합니다. 

그럼 현재 보이는 것은 card1입니다. 그리고 5초를 기다린 후 next() 메소드로 카드를 다음 것으로 바꿔줍니다. CardLayout은 메소드에 Container를 전달해줘야하는데, CardLayout이 설정된 Container를 전달하면 되는데 여기서는 frame이라는 개체가 그 Container입니다. 이 밖에도 처음, 마지막의 카드로 바꿔줄때는 아래와 같이 사용하면 됩니다.

layout.first(frame);
layout.last(frame);

 

우리가 구현한 위 코드의 결과는 아래처럼 처음 보이는 것은 card1입니다.

Card1

이후에 5초가 흐르면 card2 패널로 바뀌죠.

 

아직은 Event에 대해서 자세히 배우지 않았기 때문에 Thread를 잠시 sleep 시킨후 다음 카드로 변경했는데, Event를 배우면 버튼과 이것 저것을 합쳐서 Card를 바꿀 수 있습니다. 

 

이처럼 JAVA AWT에서 사용하는 LayoutManager에 대한 기본적인 개념과 사용법을 알아보았습니다. 어려운 개념이 있었나요? 우리는 LayoutManager를 사용하여 Component를 효율적으로 그리고 쉽게 배치할 수 있습니다. 이 LayoutManager들은 Swing에서도 그대로 사용됩니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

AWT(Abstract Window Toolkit)

AWT는 이름에서도 알 수 있듯이 Window프로그래밍을 하기 위한 GUI(Graphical User Interface)의 도구입니다. GUI가 무엇인지 모르시나요? 여러분이 Windows의 운영체제를 사용한다면 여기 보이는 화면들이 GUI라고 한답니다. 이전의 컴퓨터들은 명령어 방식의 UI를 썼다면 이제 그래픽이 추가되어 X버튼을 눌러 창을 닫고 하고 있지요. 그리고 JAVA는 AWT를 이용해서 윈도우의 GUI로 된 프로그램을 제작할 수 있습니다. 우리가 AWT를 클래스들을 이용하려면 java.awt.*과 동작에 대한 정의를 하려면 java.awt.event.*를 import하여 사용해야합니다.

이제 본격적으로 AWT에 대해서 알아보도록 하겠습니다. GUI에서 각각의 요소들을 우리는 컴포넌트라고 하며 클래스의 상속 계층도에서 메뉴(MenuComponent)를 제외한 모든 컴포넌트는 Component라는 클래스를 상속받고 있습니다. 이 컴포넌트 중에 조금 특별한 컴포넌트가 있는데 그 이름은 컨테이너(Container)입니다. 이름에서도 알 수 있듯이 무엇을 포함하는 클래스인것 같죠? 다른 컴포넌트를 포함하거나 컨테이너도 포함할 수 있습니다. 그래서 우리는 처음 시작할때 이 Container부터 배울 겁니다. 그 중 가장 일반적인 컨테이너는 Frame이라고 합니다.

아참, AWT보다 자바에서 진보적인 GUI는 Swing입니다. Swing이 여러면에서 더 성능이 좋긴한데, 사용법은 AWT와 거의 동일하므로 AWT 먼저 배우고 난 이후에 Swing 컴포넌트를 배워도 상관없습니다. 그리고 AWT 그래픽이 우리한테는 더 친숙할 겁니다. Swing컴포넌트들은 앞에 J가 붙습니다. 예를들어 Frame의 Swing컴포넌트 버전은 JFrame이죠.

이제는 코드를 짜면서 Frame이 무엇인지 알아보고 어떤 기능이 있는지 배워봅시다.

Frame 알아보기

우선 아래의 코드를 쓰고 실행해봅시다. 

import java.awt.*;
public class Main {
	
	public static void main(String[] ar){
		Frame frame=new Frame("Title!");
		frame.setSize(500,300);
		
	}
}

 

Frame을 생성하는 코드인데 생성자에는 Title명을 받는 생성자를 사용했습니다. 그리고 너비와 높이는 500, 300 pixel로 맞춰줍니다. 그리고 실행을 하면 아무것도 뜨지 않습니다. 왜냐면 프레임을 보이게 하려면 setVisible()을 true로 설정해야 보이기 때문이죠. 그래서 위 코드를 아래와 같이 수정해보면 이제 Frame이라는 것이 나올 겁니다.

public static void main(String[] ar){
	Frame frame=new Frame("Title!");
	frame.setSize(500,300);
	frame.setVisible(true);
}

 

자, Frame이 생성되었다는 것을 확인할 수 있습니다. 그리고 Title부분은 우리가 지정한 문자열과 같다는 것을 알 수 있습니다. 이제 구경했으니 닫아봅시다. 

Frame

 

닫히시나요? X버튼을 클릭해도 아무런 변화가 없다는 것을 알 수 있을 겁니다. 왜냐면 X버튼이 눌렸을때 JAVA가 이 프로그램을 닫아야하는지, 혹은 멈춰야하는지, 또는 확인 종료를 묻는 대화창을 띄어야하는지 우리가 정의를 해준적이 없죠. 그래서 X버튼을 눌렸을때를 우리는 Event, 즉 X버튼을 누르는 Event가 발생했다고 하고 동작을 처리해주는 것을 우리는 EventHandling이라고 합니다. 아직은 배우지 않았으니 Eclipse의 화면에 아래의 빨간색을 눌러 종료합시다.

 

X버튼 이벤트 발생시 Handling - WindowListener 추가하기

혹은 잠깐 맛보고 싶다면 아래의 코드를 추가하고 X버튼을 눌러보세요. 추가하기전에 import java.awt.event.*를 하여 import해주세요.

	frame.setVisible(true);
	frame.addWindowListener(new WindowListener() {

		@Override
		public void windowOpened(WindowEvent e) {}

		@Override
		public void windowClosing(WindowEvent e) {
			// TODO Auto-generated method stub
			e.getWindow().setVisible(false);	//보이지 않게	
			e.getWindow().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) {}
			
	});

 

setLocation() 으로 위치 조정

Frame이 자꾸 왼쪽 구석에만 뜨는데, 여간 불편한게 아니죠. 이때 위치를 조정할 수 있도록 setLocation() 메소드가 존재합니다. 위치는 Frame의 가장 왼쪽, 위 꼭지점을 위치로 잡습니다. 이것도 역시 pixel단위로 화면 왼쪽(x축)에서의 거리, 그리고 화면 위쪽에서의 거리(y축)를 지정할 수 있습니다. 화면 가운데에 Frame을 위치시키려면 이렇게 하면 됩니다. 

x축 위치 = (모니터의 화면 너비/2) - (프레임 너비/2)

y축 위치 = (모니터의 화면 높이/2) - (프레임 높이/2)

모니터의 화면 너비와 높이를 얻어오려면 Toolkit 클래스를 이용해서 아래의 코드처럼 구현하면 됩니다.

	frame.setSize(500,300);
	Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
	frame.setLocation((dim.width/2)-(frame.getWidth()/2), (dim.height/2)-(frame.getHeight()/2));
	frame.setVisible(true);

 

이 후 실행하게 되면 Frame이 정중앙에 보이는 것을 확인할 수 있습니다. 하지만 이것보다 더 간편한 방법이 존재하는데요. 위의 dim과 setLocation의 두줄이 아래 한줄로 표현될 수 있습니다. 바꿔서 실행해도 정중앙에 보이는것을 확인할 수 있습니다.

frame.setLocationRelativeTo(null);

 

배경색을 지정하기 - setBackground()

조금 심심할 수도 있으니 setBackground() 메소드로 배경색을 지정할 수도 있습니다. setBackground()에 전달할 Color의 객체를 사용한다면 원하는 색으로 바꿔줄 수 있습니다. 

frame.setBackground(new Color(180,211,211));

 

또는 Color의 static 멤버로도 가능하긴 합니다.

add 메소드로 다른 Component 추가해보기

Frame에서 다른 Component를 추가해보려면 add 메소드를 이용하면 됩니다. 여기서는 Button 컴포넌트를 사용해서 버튼을 추가해봤습니다. 

	Button button = new Button("My Button");
	frame.add(button);

 

아래처럼 버튼이 Frame을 꽉채운 것을 볼 수 있습니다. 그리고 아무리 눌러봤자 우리는 event를 handling하지 않았으므로 아무런 변화가 없습니다. 

 

이처럼 버튼 뿐만 아니라 다른 Component들도 add메소드로 추가가 가능합니다. 예를들어 체크박스 (Checkbox), 라디오 버튼(RadioButton) 등 말이죠. 한번 JAVA API doc을 참고하여 추가해보고 가지고 놀아보세요.

이상으로 Frame에 관한 간략한 사용법을 알아보았습니다. 다음 포스팅도 GUI의 포스팅을 이어나가도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

MessageFormat

MessageFormat클래스는 양식에 맞게 메시지를 형식화하는 편리한 클래스입니다. 주로 DB query에 이 MessageFormat이 쓰이는데, 한번 코드를 보면서 이해하도록 하겠습니다.

기본적인 클래스 사용법은 이렇습니다. 

MessageFormat format=new MessageFormat(
		"Name : {0}\n"+
		"Age : {1}\n"+
		"Name, Age : {0}, {1}");
String[] args= {"reakwon","7"};
System.out.println(format.format(args));

 

중괄호의 숫자가 Arguement들의 인덱스를 나타냅니다. Arguement들은 배열의 행태로 위의 args변수에 문자열 배열로 담겨있습니다. 그래서 {0}은 "reakwon", {1}은 "7"로 치환되게 됩니다. 그래서 그 결과는 아래와 같습니다. 

Name : reakwon
Age : 7
Name, Age : reakwon, 7

 

이런 MessageFormat의 사용처는 MySQL과 같은 query에 주로 사용합니다. SELECT * FROM my_table WHERE id = 'reakwon' 이런 쿼리들을 일반화할때 SELECT * FROM {0} WHERE {1} = '{2}' 식으로 사용할 수가 있죠.

MessageFormat format=new MessageFormat(
		"SELECT * FROM {0} WHERE {1} = {2}");
String[] args= {"my_table","id","'reakwon'"};
System.out.println(format.format(args));

 

결과

 

SELECT * FROM my_table WHERE id = 'reakwon'

 

static 메소드로 객체 생성하지 않고 사용할 수도 있습니다. 일회성으로 사용하려면 아래의 방법으로 간편하게 사용할 수도 있지요. MessageFormat의 static 메소드를 바로 사용하는 것을 알 수 있죠.

	String message="SELECT * FROM {0} WHERE {1} = {2}";
	String[] args= {"my_table","id","'reakwon'"};
	System.out.println(MessageFormat.format(message,args));

 

날짜 형식 

MessageFormat은 문자열로만 받는 것은 아닙니다. 날짜와 시간도 이런식으로 받아볼 수 있습니다. 아래의 예시처럼 출력할 수도 있습니다. 

	String message= "오늘 Date: {0,date}, Time :{0,time}";
	System.out.println(MessageFormat.format(message, new Date()));

 

결과

 

오늘 Date: 2021. 4. 6., Time :오전 12:04:34

 

쉼표와 date, time 등으로 날짜와 시간도 지정할 수 있습니다. new Date()로 시간과 날짜를 볼 수 있습니다. 이때 더 많은 정보를 보고 싶다면 아래처럼 사용하면 됩니다.

String message= "오늘 Date: {0,date,full}, Time :{0,time,full}";
System.out.println(MessageFormat.format(message, new Date()));

 

아래와 같이 요일까지 출력이되며 시간 같은 경우에는 나라의 정보도 확인할 수 있습니다.

 

오늘 Date: 2021년 4월 6일 화요일, Time :오전 12시 7분 11초 대한민국 표준시

 

반대로 간략한 정보를 원한다면 short로 바꿔서 입력해주면 됩니다. 더 많은 FormatType과 FormatStyle을 보시려면 지원하는 형식은 아래를 참고해주세요. 

java8 api document

 

기존의 형식 패턴을 applyPattern()으로 다시 지정해서 사용할 수도 있습니다. 이런 방식은 static 메소드가 아닌 객체를 만들어서 계속 사용할때 사용하는 방법입니다.

	String message= "오늘 Date: {0,date,short}, Time :{0,time,short}";
	MessageFormat format = new MessageFormat(message);
	Object[] args= {new Date()};
	System.out.println(format.format(args));
		
	String newMessage="자세히 \n"+
			"오늘 Date: {0,date,full}, Time :{0,time,full}";
	format.applyPattern(newMessage);
	System.out.println(format.format(args));
		

 

결과

 

오늘 Date: 21. 4. 6., Time :오전 12:14
자세히 
오늘 Date: 2021년 4월 6일 화요일, Time :오전 12시 14분 29초 대한민국 표준시

 

자, 여기까지 MessageFormat에 대한 소개와 사용 예제를 알아보았습니다. 이런 형식 클래스들은 유용한 경우가 많으니 알아두시면 좋을 것 같습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

DecimalFormat 클래스

우리의 일상에서 가장 많이 쓰이는 진수, 10진수를 형식화하는 역할을 하는 클래스가 JAVA에서 DecimalFormat이라는 클래스입니다. 이 클래스는 NumberFormat을 상속하고 있는 클래스이죠. 10진수를 다양한 형식에 맞게 출력해줄 수 있습니다. C언어에서는 이미 fprintf 등이 그 역할을 담당하고 있죠. 이제부터 어떻게 사용되는지 배워보도록 하겠습니다.

 

Decimal Pattern 적용

DecimalFormat은 java.text 패키지안에 존재하므로 따로 import를 해야합니다.  DecimalFormat은 문자 '#'과 '0'이 숫자를 나타내는데 쓰이며 쉼표(,), 마침표(.), 대시(-) 등으로 숫자 형식을 나타낼 수 있습니다. 많이 사용패턴은 아래의 표에 정리해놓았습니다.

 

 

Format 설명
0 10진수, 값이 없는 자리는 0으로 채움
# 10진수, 값이 없는 자리는 나타나지 않음
. 소수점을 이하 나타냄
- 음수 부호를 나타냄
, 단위 구분자를 나타냄
E 지수 기호를 나타냄, E 이후 0를 써서 표현(ex E0)
% 퍼센트 기호
' escape문자, 만약 #을 문자로 나타내고 싶다면 '#' 으로 표현
그외 문자 문자로 취급

 

 

DecimalFormat을 사용하는 방법은 생성자를 통해서 형식을 지정해주는 방법이 있습니다. 아래처럼 말이죠.

DecimalFormat format=new DecimalFormat("###,###.#######");

 

또는 applyPattern을 사용하여 패턴을 적용할 수 있습니다.

format.applyPattern("###,###.#######");

 

 

 

중요한것은 우리가 패턴을 적용하고 난 후 형식에 맞는 문자열을 뽑아와야합니다. 이때 format이라는 메소드를 사용하지요. 아래와 같이 사용합니다.

double n = 11223344.5678;
String formattedStr = format.format(n);

 

패턴과 사용예

DecimalFormat의 사용법은 어렵지 않습니다. 주로 사용하는 방식은 패턴을 지정하고 난 후 format으로 지정된 형식을 가진 숫자 형식 문자열을 가져오는 용도가 대부분입니다. 아래의 코드를 통해서 어떻게 출력되는지 확인해보세요.

public static void main(String[] ar){
		
	DecimalFormat format=new DecimalFormat();
		
	String patterns[]= {
			"0",
			"#",
			"0.0",
			"000.000",				//소수점
			"000,000,000.0",
			"000,000,000.000",
			"000,000,000.000000",	//숫자가 나타나지 않는 경우 나머지 빈자리를 0으로 채워줌
			"#,#,#,#.###",			//한글자씩 ,이 붙어서 나옴
			"###,###,###.#",
			"###,###,###.###",
			"###,###,###.######",
			"-###,###,###.######",	//숫자가 나타나지 않는 경우 출력하지 않음
			"###.##E0",				//지수 형식으로 출력
			"my number: ###.##%",	//my number라는 문자열이 합쳐짐
			"'#' ###,###.####",		//escape로 #을 문자화
			"'0' 000,000.00000000",	//escape로 0을 문자화
			"###,###.000000000"	//섞어서도 쓸 수 있음
	};
		
	double number=1234123123.1234;
		
	for(int i=0;i<patterns.length;i++) {
		format.applyPattern(patterns[i]);
		System.out.println("[pattern "+patterns[i]+"] "+format.format(number));
	}
		
}

 

결과

 

 

 

[pattern 0] 1234123123
[pattern #] 1234123123
[pattern 0.0] 1234123123.1
[pattern 000.000] 1234123123.123
[pattern 000,000,000.0] 1,234,123,123.1
[pattern 000,000,000.000] 1,234,123,123.123
[pattern 000,000,000.000000] 1,234,123,123.123400
[pattern #,#,#,#.###] 1,2,3,4,1,2,3,1,2,3.123
[pattern ###,###,###.#] 1,234,123,123.1
[pattern ###,###,###.###] 1,234,123,123.123
[pattern ###,###,###.######] 1,234,123,123.1234
[pattern -###,###,###.######] -1,234,123,123.1234
[pattern ###.##E0] 1.2341E9
[pattern my number: ###.##%] my number: 123412312312.34%
[pattern '#' ###,###.####] # 1,234,123,123.1234
[pattern '0' 000,000.00000000] 0 1,234,123,123.12340000
[pattern ###,###.000000000] 1,234,123,123.123400000

 

이상으로 간단하게 DecimalFormat클래스를 소개했고 사용법을 알아보았습니다. 워낙 어렵지 않은 형식 클래스이고 편리하게 십진수를 표현할 수 있으므로 적어도 #과 0의 차이와 format() 메소드만 알고 있으면 무난히 사용할 수 있겠습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

예외(Exception)

프로그램을 실행하다가 보면 어떤 원인때문에 비정상적인 동작을 일으키며 프로그램이 종료되는 상황을 보신적 있으실 겁니다. 이때 우리는 프로그램이 오류가 발생했다고 합니다. 에러의 종류는 우리가 컴파일할때 발생할 수 있는 컴파일 오류와 실행 중 발생되는 런타임 오류 두 종류가 있지요. 컴파일 오류는 우리가 잡기가 쉽지만, 런타임 오류는 잡기가 까다롭습니다. 자바에서는 런타임 오류를 두 종류로 보고 있습니다. 바로 에러(Error)예외(Exception)으로 말이죠. 

에러는 프로그램이 코드로 복구될 수 없는 오류를 의미하고 예외는 프로그래머가 직접 예측하여 막을 수 있는 처리가능한 오류라고 보시면 됩니다. 예를 들어 메모리가 부족한 경우 프로그래머가 직접 제어할 수 없으므로 이런 경우는 메모리 부족(OutOfMemoryError) 에러가 발생하고 함수 호출이 많아 스택이 쌓일 경우에는 StackOverFlowError가 발생할 수 있습니다.

그런데 아래의 코드처럼 어떤 수를 0으로 나눈다면 어떤 상황이 발생할까요?

int a,b;
a=10;
b=0;
		
int c=a/b;
System.out.println(c);

어떤 수를 0으로 나눌수는 없기 때문에 오류를 내보내게 됩니다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at aa.Main.main(Main.java:11)

 

하지만 조건문을 통해서 우리는 0으로 못나누게 할 수 있죠. 이처럼 우리가 예측가능한 상황에서 오류를 제어할 수 있는것이 예외입니다.

예외는 Compile시에 발견할 수 있는 예외와 프로그램 실행시에 발생하는 예외 두 종류가 있습니다. Compile시에 발생할 수 있는 예외는 아래의 사진과 같이 Eclipse와 같은 IDE를 쓰신다면 빨간줄로 예외를 처리하라고 욕합니다.

하지만 위에서의 예처럼 Compile시에 발견하지 못하는 에러를 Runtime에러라고 하는데, 이때는 프로그래머가 예측하여 처리해주어야합니다. 

그리고 그런 예외가 발생했을때 어떤 동작을 처리해야하는지를 우리는 예외 처리라고 합니다.

예외 처리

1. try, catch

예외가 발생했을때 우리는 try ... catch ... finally 라는 키워드로 예외를 처리할 수 있거나 메소드를 호출한 곳으로 던질 수 있습니다. 한 가지 중요한 점은 자바에서 모든 예외는 Exception이라는 클래스를 상속받습니다. Exception의 상속 트리를 아래에 간략하게 나타내었습니다.

예외 처리하는 방식은 이렇습니다.

try{
	//예외가 발생될만한 코드
}catch(FileNotFoundException e){	//FileNotFoundException이 발생했다면

}catch(IOException e){ //IOException이 발생했다면

}catch(Exception e){	//Exception이 발생했다면

}finally{	
	///어떤 예외가 발생하던 말던 무조건 실행
}

 

try 블록 : 이 블록에서 예외가 발생할만한 코드가 쓰여집니다. 

catch (예외 종류) 블록 : 이 부분에서 예외가 발생되었을때 처리하는 동작을 명시합니다. catch블록은 여러 개가 있을 수 있습니다. 맨 처음 catch 블록에서 잡히지 않는 예외라면 다음 catch의 예외를 검사합니다. 이때 상속관계에 있는 예외 중 부모가 위의 catch, 그리고 그 자식 예외 클래스가 아래의 catch로 놓일 순 없습니다. 예를 들어 아래와 같이말이죠.

try{
	//.. 중략 ..//
} catch (Exception e){
	//컴파일 오류 발생
} catch (IOException e){

}

Exception 클래스는 모든 예외의 부모이기 때문에 Exception을 IOException보다 위에서 처리할 수는 없다는 뜻입니다. 왜냐면 IOException의 catch블록은 도달할 수 없는 코드이기 때문이죠.

finally 블록 : 여기서는 예외가 발생하건 발생하지 않건 공통으로 수행되어야할 코드가 쓰여집니다.  임시 파일의 삭제 등 뒷정리 코드가 쓰입니다.

이것을 이용해서 우리는 위의 코드를 예외처리할 수 있습니다.

public static void main(String[] ar){
	int a,b;
	a=10;
	b=0;
	try {
		int c=a/b;
		System.out.println(c);	//예외발생으로 실행 불가한 코드
	}catch(ArithmeticException e) {
		System.out.println("ArithmeticException 발생");
		System.out.println("0으로 나눌 수는 없습니다");
		e.printStackTrace();
	}finally {
		System.out.println("finally 실행");
	}
}

 

printStackTrace()라는 메소드는 어느 부분에서 예외가 발생했는지 알려주는 추적로그를 보여줍니다. Exception이 발생했을때 기본 동작이죠. 결과는 아래와 같은 것을 알 수 있습니다.

ArithmeticException 발생
java.lang.ArithmeticException: / by zero
	at aa.Main.main(Main.java:11)		//Main.java에서 11번째 줄에서 발생했다는 printStackTrace
finally 실행

 

2. throws

아까전에 예외를 그냥 던질 수 있다고 했죠? 그 의미가 어떤 의미냐면 예외를 여기서 처리하지 않을테니 나를 불러다가 쓰는 녀석에게 에러 처리를 전가하겠다는 의미이며 코드를 짜는 사람이 이 선언부를 보고 어떤 예외가 발생할 수 있는지도 알게 해줍니다. 어떤 뜻인지 모르겠다구요? 아래의 코드를 통해서 알아보도록 합니다. 

public static void divide(int a,int b) throws ArithmeticException {
	if(b==0) throw new ArithmeticException("0으로 나눌 수는 없다니까?");
	int c=a/b;
	System.out.println(c);
}
public static void main(String[] ar){
	int a=10;
	int b=0;
		
	divide(a,b);
}

 

divide()메소드는 a와 b를 나눈 후에 출력하는 역할을 하는데, 이 나누기 부분에서 우리는 예외가 발생할 수 있음을 알았습니다. 그래서 try, catch로 예외 처리를 해야하지만, divide()를 호출하는 부분에서 처리하기를 원합니다. 왜냐면 divide()를 호출한 곳에서 예외가 발생한 다음의 처리를 divide() 메소드가 정하지 않기 때문입니다. 예를 들어 main메소드에서는 예외가 발생하면 다시 divide()를 호출하거나, 프로그램을 끝내거나, b의 값을 다시 입력받거나 해야하기 때문이고, divide()메소드가 그 결정을 할 수 없다는 의미입니다. 그래서 throws ArithmeticException을 divide를 호출한 main에다가 던지는 것(throw)입니다. 여기서 예외를 던지는 방법은 아래와 같습니다.

(아, 참고로 Exception 생성자 중에서 메시지를 받는 생성자가 있는데, 메시지를 보려면 getMessage()메소드를 이용할 수 있습니다. 아래에서 그 메소드를 사용합니다.)

throw 예외객체
ex) throw new Exception("예외 발생!")

 

예외를 발생시키는 키워드는 throw입니다. 이때 main은 그 예외를 처리하기 위해 try, catch블록을 쓰면 됩니다. 아래처럼 말이죠.

try {
	divide(a,b);
}catch(ArithmeticException e) {
	e.getMessage();
	e.printStackTrace();
}

 

throws 키워드로 처리되어야할 예외가 여러개가 존재한다면 쉼표로 끊어서 예외를 넘겨줄 수 있습니다. 그 결과는 아래와 같습니다.

java.lang.ArithmeticException: 0으로 나눌 수는 없다니까?
	at aa.Main.divide(Main.java:8)
	at aa.Main.main(Main.java:17)

 

이상으로 자바 Exception에 대한 기본 개념과 예외 처리하는 방법을 마치도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,