Java/Java

Java - 문자 기반 스트림(Reader, Writer)

Dlise 2023. 9. 14. 18:25

바이트 기반 스트림에 이어 문자 기반 스트림에 대해 알아보자.

https://dlise.tistory.com/80

 

Java - 바이트 기반 스트림(InputStream, OutputStream)

SWEA에서 문제를 풀면 사용 언어, 메모리 사용량, 실행시간, 코드 길이를 보여준다. 다른 사람들의 코드 및 결과도 확인이 가능한데 내가 푼 코드와 다른 사람들이 푼 코드의 메모리 사용량, 실행

dlise.tistory.com

 

문자 기반 스트림(Reader, Writer)

바이트 기반 스트림과 문자 기반 스트림은 데이터를 다루는 단위가 바이트인지 문자인지의 차이일 뿐 사용 방법은 거의 유사하다. 문자를 다루므로 스트림 처리 단위는 2byte이다.

 

바이트 기반 스트림에서 InputStream과 OutputStream이 다른 클래스들의 조상이었던 것처럼

문자 기반 스트림에선 Reader와 Writer가 조상이다.

- Reader

close() stream을 닫고 자원을 반환한다.
mark(int readlimit) 현재 위치를 표시한다.
markSupported() mark()와 reset()을 지원하는지에 대해 true, false를 반환한다.
read() input stream의 다음 문자를 반환한다. 반환할 값이 없으면 -1
read(char[] cbuf) 배열 cbuf만큼 읽어서 배열을 채우고 읽은 데이터의 수를 반환한다.
read(char[] cbuf, int off, int len) 최대 len개의 문자를 읽어서 배열 cbuf의 지정된 위치(off)부터 저장한다.
read(CharBuffer targer)  문자버퍼(target)에 저장한다.
ready()  데이터를 읽을 준비가 되었는지에 대해 true, false를 반환한다.
reset() mark로 표시한 위치로 되돌아간다.
skip(long n) stream에서 n만큼 건너뛴다.

 

- Writer

append(char c) 문자 c를 출력소스에 작성한다.
append(CharSequencn scq) 문자열 scq를 출력소스에 작성한다.
append(CharSequence csq, int start, int end) scq의 start부터 end까지를 출력소스에 작성한다.
close() stream을 닫고 자원을 반환한다.
flush() stream의 버퍼에 있는 모든 내용을 출력소스에 작성한다.
write(char[] cbuf) 배열 cbuf에 있는 내용을 출력소스에 작성한다.
write(char[] cbuf, int off, int len) cbuf의 off부터 len만큼 출력소스에 작성한다.
write(int c) c를 출력소스에 작성한다.
write(String str) str을 출력소스에 작성한다.
write(String str, int off, int len) str의 off부터 len만큼 출력소스에 작성한다.

Writer의 메서드를 보면 write와 append가 같은 역할을 하는 것을 알 수 있다.

하지만 서로 다른데, 가장 큰 차이점은 write는 String을, append는 CharSequence를 다룬다는 것이다.

CharSequence는 String는 물론 StringBuilder, StringBuffer도 포함한다. 즉, 범위가 다르다.

 

Java를 사용하다 보면 String 대신 StringBuilder 혹은 StringBuffer를 사용하는 경우가 많으므로 범용성이 좋은 append()를 사용하는 것이 좋아 보인다.

 

CharArrayReader, CharArrayWriter / StringReader, StringWriter

CharArray R/W, String R/W는 메모리 대상으로 입출력을 한다.

CharArraysWriterStringWriter에는 추가된 메서드가 있는데, 알아둘 것은 아래와 같다.

 

- CharArraysWriter

size(): CharArraysWriter에 출력한 데이터의 크기를 반환

toCharArray(): 출력한 문자들을 char형 배열로 반환

toString(): 출력한 문자들을 문자열로 반환

 

- StringWriter

getBuffer(): StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환

toString(): 출력한 문자열을 반환

 

먼저 CharArraysReader / Writer부터 사용해 보자.

cArr의 내용을 cw에 담고 wArr에 옮기는 작업을 한다.

import java.io.*;
import java.util.Arrays;

public class IO {
    public static void main(String[] args) {

        char[] cArr = {'h', 'e', 'l', 'l', 'o'};
        char[] wArr = new char[cArr.length];

        CharArrayReader cr = new CharArrayReader(cArr);
        CharArrayWriter cw = new CharArrayWriter();

        int data = 0;
        try {
            while((data = cr.read()) != -1) {
                cw.append((char)data);
            }
        } catch (IOException e) { e.printStackTrace(); }


        System.out.println("size: " + cw.size());
        System.out.println("toString:" + cw.toString());
        wArr = cw.toCharArray();
        System.out.println("wArr: " + Arrays.toString(wArr));
        
        cr.close();
        cw.close();
    }
}

 

다음으로 StringReader / Writer를 사용해 보자.

import java.io.*;

public class IO {
    public static void main(String[] args) {
        StringReader sr = new StringReader("Hello");
        StringWriter sw = new StringWriter();

        int data = 0;
        try {
            while((data = sr.read()) != -1) {
                sw.append((char)data);
            }
        } catch (IOException e) { e.printStackTrace(); }

        System.out.println(sw.toString().getClass() + ": " + sw.toString());
        System.out.println(sw.getBuffer().getClass() + ": " + sw.getBuffer());
    }
}

이름에서 알 수 있듯이 toString()은 String클래스, getBuffer()는 StringBuffer클래스를 반환한다.

 

 

FileReader, FileWriter

File을 다루는 문자 기반 스트림으로 사용법은 FileInputStream과 같다.

 

testFile.txt에는 문자열 "Test File"이 저장되어 있고 testFile2.txt는 비어있다.

아래 코드는 testFile의 내용을 testFile2에 한 문자씩 옮기는 작업을 한다.

import java.io.*;

public class IO {
    public static void main(String[] args) {
        File file = new File("./practice/testFile.txt");
        File file2 = new File("./practice/testFile2.txt");
        try {
            FileReader fr = new FileReader(file);
            FileWriter fw = new FileWriter(file2);

            int data = 0;
            while((data = fr.read()) != -1)
                fw.append((char)data);

            fr.close();
            fw.close();
        } 
        catch (FileNotFoundException e) { e.printStackTrace(); }
        catch (IOException e ) { e.printStackTrace(); }
        
    }
}

값이 올바르게 들아갔다.

 

PipedReader, PipedWriter

PipedReader, PipedWriterThread 간 데이터를 주고받을 때 사용한다.

해당 클래스는 다른 클래스와 달리 하나의 스트림으로 양방향 이동이 가능하다.

 

아래 코드는 2개의 Thread(readThread, wirteTread) 간 문자열 이동을 한다.

클래스를 하나씩 보자.

 

- main 클래스

2개의 Thread를 정의하고 서로를 연결한 후 실행한다.

public class IO {
    public static void main(String[] args) {
        ReadThread readThread = new ReadThread("readThread");
        WriteThread writeThread = new WriteThread("writeThread");

        readThread.connect(writeThread.getPipe());

        readThread.start();
        writeThread.start();
    }
}

 

- WriteThread 클래스

실행 시 "Hello"를 ReadThread로 보낸다. getPipe() 메서드는 연결을 위함이다.

class WriteThread extends Thread {
    PipedWriter pw = new PipedWriter();

    WriteThread(String s) {
        super(s);
    }

    public void run() {
        try {
            String msg = "Hello";
            System.out.println(getName() + ": " + msg);
            pw.write(msg);
            pw.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public PipedWriter getPipe() {
        return pw;
    }
}

 

- ReadThread 클래스

먼저 main에서 connect()로 PipedReader와 PipedWriter를 연결한다.

이후 문자를 하나씩 받아 StringBuilder에 더하며 모두 받으면 결과를 출력한다.

class ReadThread extends Thread {
    PipedReader pr = new PipedReader();
    StringBuilder sb = new StringBuilder();
    
    ReadThread(String s) {
        super(s);
    }

    public void run() {
        try {
            int data = 0;

            while((data = pr.read()) != -1) {
                sb.append((char)data);
            }

            pr.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(getName() + ": " + sb.toString());
        
    }

    public void connect(PipedWriter pw) {
        try {
            pr.connect(pw);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

결과:

 

문자 기반 보조 스트림

문자 기반 스트림도 바이트 기반 스트림과 마찬가지로 보조 스트림이 있다.

역시 가장 중요한 것은 버퍼를 이용하는 보조 스트림이다.

BufferedReader, BufferedWriter

버퍼를 이용해 효율을 높인다.

BufferedReader와 BufferedWriter는 주로 아래의 추가된 메서드를 활용한다.

 

- BufferedReader

readLine(): 다음 개행문자까지 데이터를 읽는다. 개행문자는 포함하지 않으며 데이터가 없으면 null을 반환한다.

 

 

- BufferedWriter

newLine(): 줄 바꿈

 

 

testFile.txt의 내용은 아래와 같다.

아래 코드는 testFile.txt를 한 문장씩 읽어 출력한다.

import java.io.*;

public class IO {

    public static void main(String[] args) {
        try {
            File file = new File("./practice/testFile.txt");
            BufferedReader br = new BufferedReader(new FileReader(file));

            String s;
            while((s = br.readLine()) != null) {
                System.out.println(s);
            }

            br.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

testFile.txt와 똑같이 출력되었다.

 

이번엔 testFile.txt의 내용을 testFile2.txt로 옮기는 코드이다.

import java.io.*;

public class IO {

    public static void main(String[] args) {
        try {
            File file = new File("./practice/testFile.txt");
            File file2 = new File("./practice/testFile2.txt");
            BufferedReader br = new BufferedReader(new FileReader(file));
            BufferedWriter bw = new BufferedWriter(new FileWriter(file2));

            String s;
            while((s = br.readLine()) != null)  {
                bw.append(s);
            }
            
            br.close();
            bw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

readLine() 메서드는 개행문자를 포함하지 않기 때문에 한 줄로 나왔다.

 

이를 해결하기 위해선 newLine()을 사용해야 한다.

import java.io.*;

public class IO {

    public static void main(String[] args) {
        try {
            File file = new File("./practice/testFile.txt");
            File file2 = new File("./practice/testFile2.txt");
            BufferedReader br = new BufferedReader(new FileReader(file));
            BufferedWriter bw = new BufferedWriter(new FileWriter(file2));

            String s;
            while((s = br.readLine()) != null)  {
                bw.append(s);
                bw.newLine(); //newLine() 추가
            }
            
            br.close();
            bw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

올바르게 출력되었다.

 

InputStreamReader, InputStreamWriter

바이트 기반 스트림을 문자 기반 스트림으로 연결시키는 역할을 하는데 인코딩을 지정할 수 있다.

즉, 파일의 내용을 깨지지 않도록 할 수 있다.

 

그래서 보통 아래와 같이 사용한다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));