국비/Java

2023.03.22 39일차 Java

춘핑이 2023. 3. 22. 18:38

39일차

18 데이터 입출력

입력받거나 출력을 해야하는지에 대한 문법이다.
듣고 나중에 활용된다. 기억해두자. 기본만 잘알아두면 사용하기 수월하다.
내용자체는 어렵다. 4개만 기억하면된다.

18.1 입력과 출력

어떻게 정의가 되나 ? 실행되는 프로그램을 기준으로 정의가 된다.
프로그램 기준으로 데이터가 들어오는게 입력
프로그램 기준으로 데이터가 나가는게 출력이다.

두 프로그램이 데이터를 주고받는다면 데이터를 주면 출력 데이터르 받으면 입력이다.

무조건 프로그램 기준으로 생각하자.

데이터는 한쪽으로 흘러간다. 그래서 스트림이라고 한다.
시냇물이 흘러가는 느낌.

데이터가 흘러가면 같은 스트림임에도 불구하고 입력 출력이 나뉜다.
17장에서도 스트림이라고 했다. 그래서 그냥 스트림이라면 앞뒤 문맥에 따라 구분하자.
정확하게 구분짓는다면 18장은 입출력스트림이다.
17장은 Stream API 18장은 Input,Output관련 I/O Stream이다.

어떤 데이터냐에 따라서 바이트 스트림과 문자 스트림으로 나뉜다.

우리가 작성하는 자바 코드는 문자이다. 이 파일을 주고받으려면 문자 스트림을 사용하면된다.
그림파일 같은 것을 주고받으려면 바이트 스트림을 사용해야한다.

그래서 4가지이다.
바이트 단위 입출력 2개 문자단위 입출력 2개

18.2 바이트 출력 스트림

OutputStream클래스 사용한다. 추상클래스이다.
그래서 실제 구현하는 상속받은 콘크리트 클래스를 사용해야한다.
FileOutputStream 등등이 있다.

FileOutputStream은 File객체를 파라미터로 받는다.
정의된이름을가지고 아웃풀 스트림을 가진다. 파라미터로 파일명을 사용한다.

FileNotFoundException이 발생할 수 있어서 예외 처리를 해줘야한다.

18.2.1 write

write(int b): 1바이트를 쓴다.
저장된 파일을 보면 크기가 1바이트 인것을 볼 수 있다.

public class WriterExample {
    public static void main(String[] args) {
        try {
            OutputStream os = new FileOutputStream("output/test1.db");

            byte a = 10;
            byte b = 20;
            byte c = 30;

            os.write(a);
            os.write(b);
            os.write(c);

            os.flush();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

18.2.3 flush()

flush()는 아웃풋스트림을 flush하는 것이다.
쌓여있던 내용물들이 한번에 나가는것이다.
추상 클래스의 flush()는 아무것도 하지 않고 콘크리트 클래스의 재정의된 메소드가 실행된다.
(다형성)
fileOutputStream의 flush도 아무일도 하지 않는다.
하지만 outputstream에 flush가 정의되어 있는 경우가 있다.
원래 flush() 하지않으면 목적지까지 도착하지 않는다.
아웃풋스트림에 놓여져있는 바이트들을 강제로 출력한다.

18.2.4 close()

인풋스트림이나 아웃풋스트림이 만들어지면 출력되거나 입력된다.
해당목적지에 스트림을 만들어 놓고나서 사용이 끝나면 꼭 닫아줘야한다.
그렇지 않으면 다른곳에서 이 목적지 소스를 사용하기 어려운 경우가 있다.
열었으면 닫아야한다.

꼭 닫아야하기 때문에 보통 finally블록에서 실행한다.
그런데 변수는 {}안에서만 사용하기 때문에
finally블럭에서도 사용할 수 있게 try블럭 밖에서 선언해줘야한다.

열지않아서 null일 수도 있으니 null체크를 해줘야한다.
IOException도 발생시켜서 예외처리도 해야한다.

public class C02Close {
    public static void main(String[] args) {
        // finally블럭에서도 사용할 수 있게 try블럭 밖에서 선언
        OutputStream os = null;
        try {
            // 스트림 열고
            os = new FileOutputStream("output/output.txt");
            // 어떤 작업
            os.write(1);
            os.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 꼭해야함
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

18.2.5 close() - 2

이 close한줄때문에 여러 코드가 생겨나는 문제가 생긴다.
그래서 try-catch를 할때 꼭 닫아줘야하는 객체가 있다면 간결하게 작성할 수 있는 방법이 생겼다.
try-with-resources방법이 생겨났다.
개발자가 직접작성하지 않아도 JVM이 알아서 닫도록 하는 방법이 생겻다.
try블록옆에 ()를 만들고 거기에 리소스를 활용하는 것을 넣으면 알아서 닫힌다.

자동으로 닫힐수 있는 타입은 AutoCloseable객체를 구현하고 있다면 자동으로 닫을 수 있다.

public class C03Close {
    public static void main(String[] args) {
        // try-with-resources
        // 자동으로 닫아줌
        try (OutputStream os = new FileOutputStream("output/output.txt");) {

            os.write(1);
            os.flush();

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

18.2.6 close() - 3

AutoCloseable가 구현되어 있다면 예외가 발생하더라도 닫힌다.
리소스 사용중에 예외로 프로그램이 종료된다면 리소스가 문제가 생길 수 있다.
그래서 finally에서 닫아줘야하지만 AutoCloseable를 사용하면 자동으로 닫을 수 있다.

public class MyResource implements AutoCloseable {
    private String name;

    public MyResource(String name) {
        this.name = name;
        System.out.println("[MyResource(" + name + ") 읽기");
    }

    public String read1() {
        System.out.println("[MyResource(" + name + ") 읽기");
        return "100";
    }

    public String read2() {
        System.out.println("[MyResource(" + name + ") 읽기");
        return "abc";
    }

    @Override
    public void close() throws Exception {
        System.out.println("[MyResource(" + name + ") 닫기");
    }
}

public class TryWithResourceExample {
    public static void main(String[] args) {
        try (MyResource res = new MyResource("A")) {
            String data = res.read1();
            int value = Integer.parseInt(data);
        } catch (Exception e) {
            System.out.println("예외 처리: " + e.getMessage());
        }

        System.out.println();

        try (MyResource res = new MyResource("A")) {
            String data = res.read2();
            // NumberFortmatException발생
            int value = Integer.parseInt(data);
        } catch (Exception e) {
            System.out.println("예외 처리: " + e.getMessage());
        }
    }
}

18.2.7 close() - 4

try-with resources에 사용되는 코드가 복잡할 경우 try밖으로 뺄 수 있다.
try()안에 변수만 나열해서 사용할 수 있다.
또한 복잡하다면 try-with-resources에 var도 사용가능하다.

public class C04Close {
    public static void main(String[] args) throws FileNotFoundException {

        OutputStream os = new FileOutputStream("");
        OutputStream os2 = new FileOutputStream("");
        OutputStream os3 = new FileOutputStream("");
        try (os; os2; os3) {
        } catch (Exception e) {
            e.getStackTrace();
        }

        try (var os = new FileOutputStream("")) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

18.2.8 write(byte[] b)

write(int b)는 4바이트인데 8비트만 쓰고 24비트는 무시한다.
아무리 큰 수를 넣어도 4바이트만 전달된다.
그래서 여러 바이트를 보내기 위해서는 배열에 담아 write(byte[] b)를 사용해야한다.

public class C06OutputStream {
    public static void main(String[] args) {
        try (OutputStream os = new FileOutputStream("output/output6.txt")) {
            // 한 바이트 쓰기
            os.write(298734); //1바이트
            os.write(92873492); //1바이트

            // 여러 바이트 쓰기
            byte[] data = { 3, 1, 0, 127, 64 };
            os.write(data); //5바이트
            os.write(data); //5바이트

            os.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

18.2.9 write(byte[] b, int off, int len)

write(바이트배열, 시작인덱스, 몇개)
지정된 바이트 배열에서 off부터 len까지를 출력한다.

public class C07OutputStream {
    public static void main(String[] args) {
        try (OutputStream os = new FileOutputStream("output/output7.txt")) {
            byte[] data = { 3, 3, 3, 3, 3, 3, 3, 3, 3 };
            os.write(data, 0, 3); // 3bytes
            os.write(data, 4, 5); // 5bytes
            os.write(data, 0, data.length); // 9bytes
            os.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

18.3 InputStream

InputStream은 바이트 단위로 입력을 받는 스트림
역시나 추상클래스여서 상속받은 콘크리트 클래스를 사용해야한다.
xxxInputStream이다. 프로그램 기준으로 들어오는것이다.
그래서 주요 메소드가 read()메소드이다.

18.3.1 read()

FileInputStream역시 file이나 String name(파일명)을 파라미터로 받는다.
read()하면 한바이트 읽게 된다.

3바이트파일을 4번째에 read()하면 에러가 날거 같지만 나지 않는다.
끝에 도달하면 -1을 리턴하기 때문이다.

public class C01InputStream {
    public static void main(String[] args) {
        String name = "output/test3.db";
        try (InputStream is = new FileInputStream(name)) {
            // read : 한바이트 읽기
            System.out.println(is.read()); // 20
            System.out.println(is.read()); // 30
            System.out.println(is.read()); // 40
            // 3바이트파일 4번째 에러 안남 끝에 도달
            System.out.println(is.read()); // -1
            System.out.println(is.read()); // -1
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("프로그램 종료");
    }
}

읽는 것이 여러번 반복되니 반복문을 사용하면된다.

public class C02InputStream {
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("output/test3.db")) {

            int data = 0;

            while ((data = is.read()) != -1) {
                System.out.println(data);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("프로그램 종료");
    }
}

18.3.2 read(byte[] b)

바이트배열로 읽어온다.
읽은값을 바이트배열에 저장하고 몇개 바이트를 읽엇는지를 리턴한다.
바이트를 읽어오고 남은 배열에는 원래의 값이 남아있는다.
초기값이 0이니 100개중 3바이트만 읽어오면 나머지 97은 0으로 채워져있다.

public class C04InputStream {
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("output/output6.txt")) {

            byte[] arr = new byte[5];

            int len = 0;
            while ((len = is.read(arr)) != -1) {
                System.out.println(Arrays.toString(arr));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("프로그램 종료");
    }
}

배열을 채우는 것이다.
읽어오지 못한 남은 공간은 이전값으로 남아있는다.
주의해서 사용해야한다.

public class ReadExample2 {
    public static void main(String[] args) {
        // 읽을 파일 크기 : 3bytes
        try (InputStream is = new FileInputStream("output/test2.db")) {

            byte[] data = new byte[5]; // {0, 0, 0, 0, 0}

            System.out.println(Arrays.toString(data)); //[0, 0, 0, 0, 0]
            is.read(data);
            System.out.println(Arrays.toString(data)); //[10, 20, 30, 0, 0]

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

18.3.3 파일 복사

입력을 받고 바로 출력해보자.

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

        String src = "output/image1.jpg";
        String des = "output/image_copy.jpg";

        try (InputStream is = new FileInputStream(src);
                OutputStream os = new FileOutputStream(des)) {

            int data = 0;

            while ((data = is.read()) != -1) {
                os.write(data);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("복사완료");
    }
}

18.3.3 파일 복사 -2

바이트 배열로 복사해보자.
os.wirte하는데 1024씩 복사하다보면 조금 남는부분이 있게 된다.
그래서 마지막은 남아서 읽은 부분만큼만 출력하도록 코드를 작성해줘야한다.

read(byte[] b) 메소드는 읽은 바이트 수를 리턴한다.
그래서 write를 0부터 len까지 하면 읽은 바이트 만큼만 출력하게 된다.

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

        String src = "output/image1.jpg";
        String des = "output/image_copy2.jpg";

        try (InputStream is = new FileInputStream(src);
                OutputStream os = new FileOutputStream(des)) {

            byte[] data = new byte[1024]; // 1kbyte

            while (true) {
                int len = is.read(data);
                if (len == -1) {
                    break;
                }
                os.write(data, 0, len);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("복사완료");
    }
}

18.3.4 파일 복사 -3

inputstream -> outputstream으로 파일을 복사하는 경우가 많이 일어난다.
그래서 InputStream에 OutputStream을 파라미터로 받는 transferTo()메소드가 생겼다.

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

        String src = "output/image1.jpg";
        String des = "output/image_copy3.jpg";

        try (InputStream is = new FileInputStream(src);
                OutputStream os = new FileOutputStream(des)) {

            is.transferTo(os);

        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("복사완료");
    }
}

18.4 Writer()

Writer()는 문자를 출력하는 클래스이다.
역시 추상클래스여서 상속받은 클래스를 사용해야한다.
주요메소드는 OutputStream에서본 write메소드이다.

18.4.1 write(int c)

한문자를 쓰면되는데 왜 int이냐면 char를 자동으로 int로 자동형변환 되기때문이다.
문자단위로 출력했기 때문에 그냥 열면 볼 수 있다.
저장된것을 보면 영어는 한글자당 1바이트 한글은 1글자당 3바이트인데 os나 파일형태에 따라 다르다.
int값을 받기때문에 우리가 알고있는 유니코드를 그대로 작성해도 된다.

public class C01Writer {
    public static void main(String[] args) {
        String fileName = "output/writer1.txt";
        try (Writer wr = new FileWriter(fileName)) {

            // write
            wr.write('a');
            wr.write('b');
            wr.write('가');
            wr.write('나');
            wr.write(9850); //U+267A 재활용모양 ♺

            wr.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("출력 완료");
    }
}

18.4.1 write(char[] c) / write(String)

문자배열을 받아서 출력하거나 문자열 자체를 출력할 수도 있다.

public class C02Writer {
    public static void main(String[] args) {
        String fileName = "output/writer2.txt";
        try (Writer wr = new FileWriter(fileName)) {

            char[] data = { '가', '나', '다', 'a', 'b' };

            wr.write(data);

            wr.write("hello world");
            wr.write("""
                    <h1>Lorem Ipsum</h1>
                    """);

            wr.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("출력 완료");
    }
}

18.5 Reader()

Reader()는 문자단위 입력스트림이다.
Reader의 주요메소드는 read()이다.

18.5.1 read()

read()는 읽은 문자를 int로 리턴한다.
int는 4바이트인데 char로 읽어와서 2바이트만 저장한다.
유니코드를 가져오는 것이다. 읽으려면 char타입으로 강제형변환해야한다.

public class C01Reader {
    public static void main(String[] args) {
        String name = "output/writer2.txt";
        try (Reader rd = new FileReader(name);) {
            int c1 = rd.read(); // '가'
            System.out.println((char) c1);

            int data = 0;
            while ((data = rd.read()) != -1) {
                System.out.println(data + ":" + (char) data);
            }

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

18.5.2 read(char[] c)

read(char[] c)는 여러 값을 읽어와서 배열에 저장한다.
읽은 유니코드의 값을 배열에 저장하는 것이다.
배열의 크기만큼 문자를 읽어낸다.

public class C03Reader {
    public static void main(String[] args) {
        String name = "output/writer2.txt";
        try (Reader rd = new FileReader(name);) {

            char[] data = new char[5];

            while (true) {
                int len = rd.read(data);
                if (len == -1) {
                    break;
                }
                System.out.println(new String(data, 0, len));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

18.6 BufferdOutputStream

기존에 사용한 InputStream OutputSteam은 버퍼가 없었다.
버퍼란 데이터를 한곳에서 다른 한 곳으로 전송하는 동안 일시적으로 데이터를 보관하는 메모리 영역이다.
버퍼링이란 버퍼를 채우는 동작을 의미한다.

기존 OutputStream을 사용하면 느린 네트워크를 쓰면 데이터를 보낼때마다
느린 네트워크를 이용해서 보내게 된다.

BufferdOutputStream을 사용하면 소스에서 목적지까지 가기전에 버퍼를 만들어둔다.
그리고 이 버퍼가 채워지길 기다렸다가 한번에 목적지로 보낸다.
이러면 보낼때마다 느린네트워크를 사용하는 것보다는 효율적이게 된다.

그래서 그냥 OutputStream의 flush()메소드는 버퍼를 비울게 없어서 아무일도 하지않는다.
BufferdOutputStream의 flush()메소드는 실제로 버퍼를 flush한다.

BufferedOutputStream의 생성자는 OutputStream을 파라미터로 받는다.

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

        String name = "output/buffered1.txt";
        try (OutputStream fis = new FileOutputStream(name);
                OutputStream os = new BufferedOutputStream(fis);) {

            long start = System.nanoTime();
            for (int i = 0; i < 1000000; i++) {
                os.write(1);
            }
            os.flush();
            long end = System.nanoTime();
            System.out.println((end - start) + "ns"); 

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

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

        String name = "output/buffered2.txt";
        try (OutputStream fis = new FileOutputStream(name);) {

            long start = System.nanoTime();
            for (int i = 0; i < 1000000; i++) {
                fis.write(1);
            }
            long end = System.nanoTime();
            System.out.println((end - start) + "ns");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Buffered : 23197800ns
기존 : 4546134600ns
성능에서 꽤나 차이가 난다.

2023.03.22 후기

의문점이 생겼다.
-1 리턴하면 그만 읽는데 -1을 값으로 넣으면 어떻게 되나?

-1 바이트 출력한것을 read로 읽으면
-1은 이진법으로 1111 1111 1111 1111 ...인데
read()는 1바이트(8비트)만 입력하기 때문에 1111 1111이 가서 255로 보인다..

그래서 배열로 읽어와야한다.
데이터는 8비트씩 오기때문에 read가 읽을게 없어서 -1을 리턴하는 것과 데이터의 -1과는 상관이없다.
바이트로 하나씩 읽어오기 때문이다.
문자를 읽어오면 문자를 읽어오지만 -1을 바이트로 쪼개서 읽어오는 것이다.