자바에서 I/O 기능을 담당하는 대표적인 class가 있습니다.

 

위의 class들 중에 대표적으로 사용되는 것들을 알아보겠습니다.

 

1. byte 단위로 data를 교환하는 ByteStream

대표적인 ByteStream은 InputStream , OutputStream이 존재합니다.

 

Input ,Output 에서 유추할 수 있듯이

data가 나를 기준으로 들어오면 InputStream

data가 나를 기준으로 나가면 OutputStream 입니다.

2. 2byte 단위로 data를 읽어 문자로 변환해주는 Character Stream

Character Stream은 1byte 단위로 쓰일 수도 2byte단위로 쓰일 수도있습니다.

byte단위로 구성된 data를 사람이 볼 수 있도록 만들어주는 기능을 합니다.

 

영어만 쓰는 경우에는 byte단위로 읽어내도 문제가 없지만

한국어의 경우는 2byte씩 읽어야 하기 떄문에

 

문자 encoding 방식에 따라서 해석하는 형식이 달라집니다.


InputStream 의 종류

대표적으로 사용되는 ByteStream은 InputStream 입니다.

InpusStream의 SubType인 FileInputStream은 파일을 Byte단위로 읽어냅니다.

 

실제로 다음과 같이 declaration 돼있습니다.

class FileInputStream extends InputStream

하지만 앞서 말했듯이 한국어 같은 경우에는 2byte로 이루어져 있습니다.

 

한국어로 이루어진 txt파일의 경우 똑바로 읽어낸다고 해도 사람이 볼 수 없는 형태로 decoding 될 것 입니다.

이런 경우에 하나하나 체크해주면서 decoding을 해줘야 하는데 상당히 번거로운 작업입니다.

 

그래서 나온 것이 InputStreamReader 입니다.

public class InputStreamReader extends Reader 

byte단위로 읽어내는 것은 똑같지만 CharacterSet을 갖고 있어

2byte가 필요한 문자의 경우 자동으로 묶어서 decoding 해주는 기능을 갖고 있습니다.

 

상당히 좋아보이지만 , 극복하지 못한 단점이 존재합니다.

 

InputStreamReader는 한 글자씩 읽어내기 때문에 기능적으로 상당히 느립니다.

이러한 단점을 극복하기 위해 BufferedReader 가 존재합니다.

public class BufferedReader extends Reader 

똑같은 기능을 하지만 Buffer를 이용해 문자를 저장하여 문자열의 형태로 읽어냅니다.

따라서 상당히 효율적입니다.

OutputStream 의 종류

FileOutputStream은 InputStream과 마찬가지로 파일을 byte단위로 data를 내보냅니다.

 

대표적인 내장 메서드로 write()를 갖고있으며 Buffer 를 따로 갖고있지 않기 떄문에

별도의 동작없이 Write() 사용시 바로 data를 내보냅니다.

 

또한 byte단위로 내보내기 떄문에 한글파일을 보낼경우 깨지게 됩니다.

class FileOutputStream extends OutputStream

OutputStream에서 또한 InputStream과 비슷하게

OutputStreamWriter 를 가집니다.

public class OutputStreamWriter extends Writer 

이 또한 동일하게 1byte 혹은 2byte 단위로 data를 내보내게 됩니다.

마찬가지로 Buffer를 가지고 한꺼번에 작업을 처리하는 class들을 갖고있습니다.

 

대표적으로 PrintWriter , BufferWriter 이 존재합니다.

public class PrintWriter extends Writer 
public class BufferedWriter extends Writer 

두 class 모두 buffer를 사용하기 떄문에 버퍼에 임시로 보관된 데이터를 언제 내보낼지 알려주는 flush() 메서드가 존재합니다.

이 전까지는 buffer에 data를 계속해서 넣다가 flush()를 만나면 버퍼를 비우면서 내보내게 되는 것 입니다.

 

아래는 간단한 사용법 입니다.

BufferedWriter bw = new BufferedWriter(new FileWriter(new File("src/main/resources/newFile.txt")));

        String str = "새로 쓰는 문자열";
        bw.write(str); // buffer 에 문자열 담기 
        bw.newLine(); // 줄 바꿈 
        bw.flush(); // buffer 비우기 , 실질적으로 파일에 문자열 쓰여짐 
        bw.close(); // BufferedWriter 닫기

PrintWriter는 AutoFlush 옵션을 갖고 있습니다.

이 옵션을 true로 설정하면 매 print 마다 flush를 호출합니다.

 

이는 편리하지만 지나치게 빈번한 flush() 메서드를 호출하게 되어

모아서 출력하는 버퍼의 효과가 사라지며 상당히 많은 시간을 소요하게 됩니다.

 

따라서 자동으로 출력버퍼를 비우도록 하는 것은 바람직하지 않습니다.

PrintWriter pw = new PrintWriter(new FileWriter(new File("src/main/resources/newFile2.txt")),true);
        pw.println(sc.nextLine()); // AutoFlush Option이 true기 떄문에 자동으로 파일에 쓰여짐
        pw.print(sc.nextDouble()); // 문자열 뿐만 아니라 , double도 입력 가능 , BufferedWriter는 문자만 가능

주석에서도 표시 되지만

 

Bufferwriter는 Chracter , Chracter Array Type만 다룹니다.

PrintWriter는 어느 Type이든 메서드의 parameter로 받을 수 있습니다.

 

따라서 Chracter, Chracter Array 만 사용한다면 BufferReader 를 그렇지 않다면 PrintWriter를 사용하면 됩니다.

Example 파일 읽고 복사하여 쓰기

List<String> textName = new ArrayList<>();

        textName.add("src/main/resources/file1.txt");
        textName.add("src/main/resources/file2.txt");
        textName.add("src/main/resources/file3.txt");


        for (int i = 0; i < textName.size(); i++) {

            try (BufferedReader br = new BufferedReader(new FileReader(textName.get(i)))) {


                BufferedWriter bw = new BufferedWriter(new FileWriter(textName.get(i) + "_Copy_By_BufferWriter"));
                PrintWriter pw = new PrintWriter(new FileWriter(textName.get(i) + "_Copy_By_PrintWriter"),true);


                int count = 0;

                String str = null;

                while ((str = br.readLine()) != null) {
                    bw.write(str);
                    bw.newLine();
                    pw.println(str);
                    count++;
                }

                bw.flush();



                logger.info("{} 파일은 {}줄로 이루어져 있습니다.", textName.get(i), count);

            } catch (FileNotFoundException e) {
                logger.warn("파일을 찾지 못했습니다");
            } catch (IOException e) {
                logger.warn("I/O Stream에 문제가 있습니다");
            }


        }

다음은 BufferedReader를 이용해 .txt 파일을 읽어 해당 파일이 몇 줄로 이루어져 있는지 확인한 후

Copy_By 라는 이름으로 파일을 복사하는 코드입니다.

 

BufferedReader를 이용했기 때문에 .txt 파일을 한 줄 단위로 읽게 되고 , 더 이상 읽을것이 없는 경우 null 을 반환하게 되어

파일 읽기가 종료됩니다.

 

BufferedWriter는 계속해서 Buffer에 한 줄 씩 입력을 받은 후

파일 읽기가 끝난 후에 flush를 사용함으로써 한번에 Buffer안에 data를 내보냈고

PrintWriter의 경우 AutoFlush 옵션을 true로 설정해 별도의 flush 없이 파일에 data가 써집니다.

 

이해를 돕기 위해 try문 안에서 선언하여 사용했지만 try-with-resources 문을 사용해

 

코드를 간추리는 방식도 좋아보입니다.

Binary Tree Traversal

 

Traversal(순회)란 무엇일까요?

 

단순히 말하면 탐색하는 것을 의미합니다.

 

특정한 자료구조 안에 있는 원소들을 방문한다면 전부 Traversal이라 말 할수 있습니다.

for(int i=0;i<arr.length;i++){
    System.out.println(arr[i]);
}

위의 코드는 배열을 순회하는 대표적인 코드입니다.

 

배열같이 단순한 자료구조라면 다음과 같이 순회하면 되지만

 

Tree자료구조의 경우 어떨까요?

 

Tree에서 순회는 대표적으로 3가지가 존재합니다.

  1. preOrder (전위)
  2. inOrder (중위)
  3. postOrder (후위)



Tree안에 있는 노드들을 방문한다는 개념은 전부 같지만

순서의 차이가 존재합니다.

 

preOrder 는 이름에서 알 수 있듯이 구조가 다음과 같습니다.

노드 출력 -> left 방문-> right 방문

 

postOrder 또한 이름에서 알 수 있듯이

left 방문 -> right 방문 -> 노드 출력 의 구조를 가집니다.

 

그렇다면 inOrder는 다음과 같겠죠

left 방문 -> 노드 출력 -> right 방문

 

이런 구조를 가지고 자바 코드로 구현해 보겠습니다.

 public static void preOrder(BinaryTree bst) {


        if (bst == null) {
            return;
        }

        System.out.print(bst.getValue()+" ");
        printPreOrderNumber(bst.left);
        printPreOrderNumber(bst.right);

    }

현재 노드가 가지고 있는 value를 출력합니다.

 

또한 재귀적으로 해당 노드의 left , right를 방문합니다.

 

만약 노드가 null이라면 return이 되는 BaseCase를 가집니다.

 

inOrder와 postOrder는 위의 코드와 유사하게 순서만 바꿔주면 됩니다.

public static void inOrder(BinaryTree bst) {


        if (bst == null) {
            return;
        }

        preOrder(bst.getLeft());
        System.out.print(bst.getValue()+" ");
        preOrder(bst.getRight());

    }

    public static void postOrder(BinaryTree bst) {


        if (bst == null) {
            return;
        }

        postOrder(bst.getLeft());
        postOrder(bst.getRight());
        System.out.print(bst.getValue()+" ");

    }

이렇게 순회의 대표격인 pre,in,post Order를 알아 봤습니다.

 

순회에는 이 뿐만 아니라 더 다양한 방법이 존재하는데요

 

좀더 심화된 방법으로 BFS(Breadth-First Search) 를 알아 보겠습니다.

 

여태까지의 순회는 재귀적으로 구성됐지만 위의 순회는 재귀적이지 않습니다.

 

Level Order Traversal (BFS)는 Queue 자료구조를 쓰는 대표적인 알고리즘입니다.

 

구조는 다음과 같습니다.

  1. Tree의 Root 노드를 갖고있는 Queue 생성
  2. Queue를 pop하면서 노드 출력
  3. 노드의 left가 있다면 Queue에 추가
  4. 노드의 right가 있다면 Queue에 추가
  5. Queue의 size가 존재하지 않을떄까지 2~4번 반복

이것을 그대로 코드로 옮긴다면 다음과 같습니다.

public static void bfs(Queue<BinaryTree> q) {

        while (!q.isEmpty()) {

            BinaryTree temp = q.poll();

            System.out.print(temp.getValue()+" ");


            if (temp.getLeft() != null) {
                q.add(temp.getLeft());
            }

            if (temp.getRight() != null) {
                q.add(temp.getRight());
            }

        }

    }

만약 다음과 같은 Tree가 있다면 순회의 결과는 다음과 같습니다.

 

 

preOrder : F , B , A , D , C , E , G , I , H

inOrder : A , B , C , D , E , F , G , H , I

postOrder : A , C , E , D , B , H , I , G , F

levelOrder : F , B , G , A , D , I , C , E , H

+ Recent posts