java는 입력 스트림과 출력 스트림을 통해 데이터를 입출력한다. stream은 단방향으로 데이터가 흐르는 것을 말하는데, 다음과 같이 data는 출발지에서 나와 도착지로 흘러들어간다.
----출발지---- ----Program----- ---도착지----
|1. 키보드 | | | |1. 모니터 |
|2. 파일 | ---> | 도착지 출발지 |--> |2. 파일 |
|3. 프로그램 | ---------------- |3. 프로그램 |
------------- ------------
프로그램을 기준으로 데이터가 들어오면 입력 스트림, 데이터가 나가면 출력 스트림이 된다. 프로그램이 다른 프로그램과 데이터를 교환하려면 양쪽 모두 입력 스트림과 출력 스트림이 필요하다.
조심해야할 것은 입출력 기준이 항상 프로그램 기준이라는 것이다.
어떤 데이터를 입출력하느냐에 따라 stream은 다음 두 종류로 구분할 수 있다.
1. byte stream: 그림, 멀티미디어, 문자 등 모든 종류의 데이터를 입출력할 때 사용
2. 문자 stream: 문자만 입출력할 때 사용
아래는 byte stream의 클래스이다.
구분 | byte 입력 스트림 | byte 출력 스트림 |
---|---|---|
최상위 클래스 | InputStream | OutputStream |
하위 클래스 | XXXInputStream(FileInputStream) | XXXOutputStream(FileOutputStream) |
아래는 문자 stream의 클래스이다.
구분 | 문자 입력 스트림 | 문자 출력 스트림 |
---|---|---|
최상위 클래스 | Reader | Writer |
하위 클래스 | XXXReader(FileReader) | XXXWriter(FileWriter) |
OutputStream
|
---------------------------------------------------------
| | | |
FileOutPutStream PrintStream BufferedOutputStream DataOutputStream
OutputStream
class에는 모든 바이트 출력 스트림이 기본적으로 가져야할 메서드가 정의되어 있다. 다음은 OutputStream
class의 주요 메서드이다.
메서드 | 설명 |
---|---|
void write(int b) | 1byte를 출력 |
void write(byte[] b) | 매개값으로 주어진 배열 b의 모든 바이트를 출력 |
void write(byte[] b, int off, int len) | 매개값으로 주어진 배열 b[off]부터 len개의 바이트를 출력 |
void flush() | 출력 버퍼에 잔류하는 모든 바이트를 출력 |
void close() | 출력 스트림을 닫고 사용 메모리 해제 |
아래는 write
를 사용하여 ./test1.txt
파일에 데이터를 쓰는 것을 볼 수 있다.
public class Main {
public static void main(String[] args) {
try {
OutputStream os = new FileOutputStream("./test1.txt");
byte[] arr = {10, 20, 30};
os.write(arr);
os.flush();
os.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
OutputStream
에는 내부에 buffer가 있어서, buffer가 가득차면 그때야 출력을 한다. 즉, os.write
를 실행해도 내부 buffer에 데이터를 넣고만 있다는 것이다. 따라서, buffer를 비워주어 buffer내부의 데이터를 정해진 file로 출력시켜주기 위해서는 flush
를 사용해야한다. 또한, close
를 실행하여 반드시 명시적으로 파일 디스크립터에 대한 자원을 반환해주어야 한다.
InputStream
은 바이트 입력 스트림의 최상위 클래스로, 추상 클래스이다. 모든 바이트 입력 스트림은 InputStream
class를 상속받아 만들어진다.
InputStream
|
---------------------------------------------
| | |
FileInputStream BufferedInputStream DataInputStream
InputStream
클래스에는 바이트 입력 스트림이 기본적으로 가져야 할 메서드가 정의되어 있다. 다음은 InputStream
클래스의 주요 메서드이다.
메서드 | 설명 |
---|---|
int read() | 1byte를 읽은 후 바이트를 반환 |
int read(byte[] b) | 읽은 바이트를 매개값으로 주어진 배열에 저장 후 읽은 바이트 수를 리턴 |
void close() | 입력 스트림을 닫고 사용 메모리 해제 |
다음은 우리가 만든 test1.txt
파일을 InputStream
으로 읽어들이는 코드이다.
public class Main {
public static void main(String[] args) {
InputStream is = null;
try {
is = new FileInputStream("./test1.txt");
byte[] data = new byte[100];
while (true) {
int num = is.read(data);
if(num == -1) break;
for(int i =0; i< num;i ++) {
System.out.println(data[i]);
}
}
is.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
read
는 입력받은 byte
배열의 크기만큼만 데이터를 읽어 들일 수 있다. 따라서, 100 바이트씩만 읽어들이기 때문에 while
으로 반복해야하는 것이다. 이때 read
의 반환값이 -1
이라면 더이상 읽을 내용이 없다는 것이므로 순회를 종료한다.
바이트 입출력인 InputStream
과 OutputStream
에 대응하는 문자 입출력 스트림으로 Reader
과 Writer
이 있다. 입출력되는 단위가 문자인 것을 제외하고는 사용 방법이 동일하다.
Writer
는 출력 스트림의 최상위 클래스로 추상 클래스이다. 모든 문자 출력 스트림 클래스는 Writer
클래스를 상속받아서 만들어진다.
Writer
|
------------------------------------------------------
| | | |
FileWriter BufferedWriter PrintWriter OutputStreamWriter
Writer
클래스에는 모든 문자 출력 스트림이 기본적으로 가져야 할 메서드가 정의되어 있다. Writer
클래스의 주요 메서드는 다음과 같다.
메서드 | 설명 |
---|---|
void write(int c) | 매개값으로 주어진 한 문자를 출력 |
void write(char[] cbuf) | 매개값으로 주어진 배열의 모든 문자를 출력 |
void write(char[] cbuf, int off, int len) | 매개값을 주어진 배열에서 cbuf[off]부터 len개 까지의 문자를 출력 |
void write(String str) | 매개값으로 주어진 문자열을 출력 |
void write(String str, int off, int len) | 매개값으로 주어진 문자열에서 off 순번부터 len개 까지의 문자를 출력 |
void flush() | 버퍼에 잔류하는 모든 문자를 출력 |
void close() | 출력 스트림을 닫고 사용 메모리 해제 |
Writer
는 OutputStream
과 사용 방법은 동일하지만, 출력 단위가 문자이다. 그리고 문자열을 출력하는 메서드를 추가로 제공하는 것이다.
public class Main {
public static void main(String[] args) {
try {
Writer writer = new FileWriter("./test2.txt");
String data = "hello myworld!";
writer.write(data);
writer.flush();
writer.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
test2.txt
파일이 생기고 다음의 결과를 갖게 된다.
hello myworld!
Reader
는 문자 입력 스트림의 최상위 클래스로 추상 클래스이다. 모든 문자 입력 스트림 클래스는 Reader
클래스를 상속받아서 만들어진다.
Reader
|
-----------------------------------------
| | |
FileReader BufferedReader InputStreamReader
Reader
클래스에는 문자 입력 스트림이 기본적으로 가져야 할 메서드가 정의되어 있다. 다음은 Reader
클래스의 주요 메서드이다.
메서드 | 설명 |
---|---|
int read() | 1개의 문자를 읽고 반환 |
int read(char[] cbuf) | 읽은 문자들을 매개값으로 주어진 문자 배열에 저장하고 읽은 문자 수를 반환 |
void close() | 입력 스트림을 닫고, 사용 메모리 해제 |
Reader
는 InputStream
과 사용 방법은 동일하지만, 출력 단위가 문자(char
)이다.
public class Main {
public static void main(String[] args) {
try {
Reader reader = new FileReader("./test2.txt");
char[] buf = new char[3];
while (true) {
int len = reader.read(buf);
if(len == -1) break;
for(int i = 0; i< len; i++) {
System.out.print(buf[i]);
}
}
reader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileReader
에 읽어 들일 file의 path를 적어주고 read
메서드에 char[]
을 넣어주면 char[]
의 크기만큼 읽어들인다. 위의 경우 buf
의 사이즈가 3이므로 3개씩 읽어들이는 것이다.
보조 스트림은 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림을 말한다. 보조 스트림은 자체적으로 입출력을 수행할 수 없기 때문에, 입출력 source로부터 직접 생성된 입출력 스트림에 연결해서 사용해야 한다.
----------- ------------
|입력스트림| --보조 스트림---> Program --보조 스트림---> |츨력 스트림|
----------- ------------
입출력 스트림에 보조 스트림을 연결하려면 보조 스트림을 생성할 때 생성자 매개값으로 입출력 스트림을 제공하면 된다.
보조스트림 변수 = new 보조스트림(입출력스트림);
가령, 바이트 입력 스트림인 FileInputStream
에 InputStreamReader
보조 스트림을 연결하는 코드는 다음과 같다.
InputStream is = new FileInputStream("...");
InputStreamReader reader = new InputStreamReader(is);
보조스트림은 또 다른 보조스트림과 연결되어 stream 체인으로 구성할 수도 있다. 가령 문자 변환 보조 스트림인 InputStreamReader
에 BufferedReader
보조 스트림을 연결하는 코드는 다음과 같다.
InputStream is = new FileInputStream("...");
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader);
자주 사용되는 보조 스트림은 다음과 같다.
보조 스트림 | 기능 |
---|---|
InputStreamReader | 바이트 스트림을 문자 스트림으로 변환 |
BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter | 입출력 성능 향상 |
DataInputStream, DataOutputStream | 기본 타입 데이터 입출력 |
PrintStream, PrintWriter | 줄바꿈 처리 및 형식화된 문자열 출력 |
ObjectInputStream, ObjectOutputStream | 객체 입출력 |
이제부터 보조 스트림에 대해서 하나씩 알아보자.
바이트 스트림(InputStream, OutputStream)에서 입출력할 때 데이터가 문자라면 문자 스트림(Reader, Writer)로 변환해서 사용하는 것이 좋다. 그 이유는 문자로 바로 입출력하는 편리함이 있고, 문자셋의 종류를 지정할 수 있기 때문이다.
InputStream
을 Reader
로 변환하는 것은 InputStreamReader
보조 스트림을 연결하면 된다.
-----InputStreamReader---
byte ---> InputStream | ------> Reader -----> | --> program(문자)
-------------------------
다음은 InputStream
을 Reader
로 변환하는 코드이다.
InputStream is = new FileInputStream("./test2.txt");
Reader reader = new InputStreamReader(is);
OutputStream
을 Writer
로 변환하려면 OutputStreamWriter
보조 스트림을 연결하면 된다.
---OutputStreamWriter---
program(문자) --> | Writer -------> | --> OutputStream --> byte
------------------------
다음은 OutputStream
을 Writer
로 변환하는 코드를 보여준다.
OutputStream os = new FileOutputStream("./test.txt");
Writer writer = new OutputStreamWriter(os);
참고로 FileOutputStream
에 OutputStreamWriter
를 연결하지 않고, FileWriter
를 직접 생성할 수 있다. FileWriter
는 OutputStreamWriter
의 자식 클래스이다. 이것은 FileWriter
가 내부적으로 FileOutputStream
에 OutputStreamWriter
보조 스트림을 연결한 것이라고 볼 수 있다.
아래는 source stream은 byte 기반 FileOutputStream
과 FileInputStream
이지만, 문자 기반 스트림인 Writer
와 Reader
로 변환해서 사용한다.
public class Main {
public static void main(String[] args) {
try {
Main.write("bye world");
String data = Main.read();
System.out.println(data); // bye world
}catch (Exception e) {
e.printStackTrace();
}
}
public static void write(String str) throws Exception {
OutputStream os = new FileOutputStream("./test3.txt");
Writer writer = new OutputStreamWriter(os, "UTF-8");
writer.write(str);
writer.flush();
writer.close();
}
public static String read() throws Exception {
InputStream is = new FileInputStream("./test3.txt");
Reader reader = new InputStreamReader(is, "UTF-8");
char[] data = new char[100];
int num =reader.read(data);
reader.close();
String str = new String(data, 0, num);
return str;
}
}
write
는 byte stream인 FileOutputStream
으로 파일을 열었지만, 데이터를 출력할 때 OutputStreamWriter
를 사용하여 문자 stream인 Writer
로 처리한다.
read
는 byte stream인 FileIntputStream
으로 파일을 열었지만, 데이터를 입력할 때 InputStreamReader
를 사용하여 문자 stream인 Reader
로 처리한다.
하드 디스크의 성능이 너무 안좋으면, CPU와 Memory 성능과는 상관없이 하드디스크에 대한 병목 현상이 생길 수 밖에 없다. 이러한 문제에 대해서 memory buffer를 만들어 완충제 역할을 하면 실행 성능을 어느정도 향상 시킬 수 있다.
출력 스트림의 경우 직접 하드 디스크에 데이터를 보내지 않고, 메모리 버퍼에 데이터를 보냄으로써 출력 속도를 향상시킬 수 있다. 버퍼는 데이터가 쌓이기를 기다렸다가 꽉 차게되면 데이터를 한꺼번에 하드디스크로 보냄으로써 출력 회수를 줄여준다.
---Program--- ---메모리 버퍼---
| data |--고속전송-->| 메모리 | --buffer의 데이터를 모두 전송---> 하드디스크
------------- ----------------
입력 스트림에서도 buffer를 사용하면 읽기 성능이 좋아진다. 하드 디스크로부터 직접 읽는 것 보다는 메모리 버퍼로부터 읽는 것이 빠르다.
---Program--- ---메모리 버퍼---
| data |<--고속읽기--| 메모리 | --buffer에 데이터를 전송 <--- 하드디스크
------------- ----------------
위와 같이 메모리 버퍼를 제공하여 program의 실행 성능을 향상시키는 보조 스트림이 있다. 바이트 스트림에는 BufferedInputStream
, BufferedOutputStream
이 있고, 문자 스트림에는 BufferedReader
, BufferedWriter
이 있다. 보조 스트림을 연결하는 방법은 다음과 같다.
BufferedInputStream bis = new BufferedInputStream(바이트 입력 스트림);
BufferedOutputStream bos = new BufferedOutputStream(바이트 출력 스트림);
BufferedReader br = new BufferedReader(문자 입력 스트림);
BufferedWriter bw = new BufferedWriter(문자 출력 스트림);
참고로 Buffered~
stream도 close
를 해주어야 하는데, close
를 해주면 wrapping하고 있던 바이트 입출력 스트림, 문자 입출력 스트림 모두 close된다.
아래의 예제는 BufferedInputStream
, BufferedOutputStream
의 사용 방법이다.
public class Main {
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("./test4.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] data = {'h', 'e' , 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
bos.write(data);
bos.flush();
bos.close();
FileInputStream fis = new FileInputStream("./test4.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
byte[] buf = new byte[100];
while(true) {
int num = bis.read(buf);
if(num == -1) break;
for(int i = 0; i < num; i++) {
System.out.print((char) buf[i]); // hello world
}
}
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
hello world
를 BufferedOutputStream
으로 test4.txt
에 쓰고, BufferedInputStream
로 읽어내는 코드이다.
문자 스트림에도 성능을 위해 buffered를 사용할 수 있는데, 문자 입력 스트림 Reader
에 BufferedReader
를 연결하면 성능 향상 뿐만 아니라, 행 단위로 문자열을 읽는 매우 편리한 readLine
메서드를 제공한다는 것이다. 다음은 문자 파일을 행 단위로 읽는 코드를 보여준다.
BufferedReader br = new BufferedReader(new FileReader("..."));
while(true) {
String str = br.readLine(); // 파일에서 한 행씩 읽음
if(str == null) break; // 더 이상 읽을 행이 없을 경우 while문 종료
}
다음은 한 행씩읽으면서 행 단위 번호를 붙여주는 코드이다.
hello world
bye world
new world!
public class Main {
public static void main(String[] args) {
try {
BufferedReader br = new BufferedReader(new FileReader("./test4.txt"));
int lineNo = 1;
while(true) {
String str = br.readLine();
if (str == null) break;
System.out.println(lineNo + "\t" + str);
lineNo++;
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
결과는 아래와 같다.
1 hello world
2 bye world
3 new world!
BufferedReader
를 닫으면 FileReader
도 닫힌다.
바이트 스트림에 DataInputStream
과 DataOutputStream
보조 스트림을 연결하면 기본 타입인 boolean
, char
, short
, int
, long
, float
, double
값을 입출력할 수 있다.
byte --->InputStream ---> DataInputStream ---> program(기본 데이터 타입, int, double...) ---> DataOutputStream ---> OutputStream --> byte
다음은 DataInputStream
과 DataOutputStream
보조 스트림을 연결하는 코드이다.
DataInputStream dis = new DataInputStream("바이트 입력 스트림");
DataOutputStream dos = new DataOutputStream("바이트 출력 스트림");
DataInputStream
과 DataOutputStream
에서 각 primitive data type에 대해서 메서드를 사용하면 된다. readBoolean
, writeBoolean
, readInt
, writeInt
등이 있다.
DataInputStream
, DataOutputStream
을 사용할 때 한 가지 주의할 점이 있다. 데이터 타입의 크기가 모두 다르므로 DataOutputStream
으로 출력한 데이터를 다시 DataInputStream
으로 읽어 올 때에는 출력한 순서와 동일한 순서로 읽어야 한다는 것이다. 가령 출력할 때 순서가 int
-> boolean
-> double
이라면 읽을 때의 순서도 int
-> boolean
-> double
이어야 한다.
다음은 이름, 성적, 순위 순으로 파일에 출력하고 다시 파일로부터 읽는 방법을 보여준다.
public class Main {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream("./test4.txt");
DataOutputStream dos = new DataOutputStream(fos);
dos.writeUTF("Hong");
dos.writeDouble(95.5);
dos.writeInt(1);
dos.writeUTF("KIM");
dos.writeDouble(90.3);
dos.writeInt(2);
dos.flush();
dos.close();
FileInputStream fis = new FileInputStream("./test4.txt");
DataInputStream dis = new DataInputStream(fis);
for(int i = 0; i< 2; i++) {
String name = dis.readUTF();
double score = dis.readDouble();
int order = dis.readInt();
System.out.println(name + ": "+ score +": " + order);
}
dis.close();
}
}
출력 결과는 다음과 같다.
Hong: 95.5: 1
KIM: 90.3: 2
물론 이렇게 다양한 타입으로 쓰는 것을 추천하진 않는다. 그냥 writeUTF
, readUTF
만 하고 int
와 같은 숫자타입들은 미리 문자로 바꿔놓고, 쓰도록 하자. 왜냐면 output도 그렇고, input도 그렇고 어떤 순서가 명확하지 않은 채로 읽는 것은 분명 실수가 발생할 수 밖에 없다.
자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수 있다. 객체를 출력하려면 field값을 일렬로 늘어선 바이트로 변경해야하는데, 이를 직렬화(Serializable)이라고 한다. 반대로 직렬화된 바이트를 객체의 field값으로 복원하는 것을 역직렬화(Deserializable)이라고 한다.
ObjectInputStream
과 ObjectOutputStream
은 객체를 입출력할 수 있는 보조 스트림이다. ObjectOutputStream
은 바이트 출력 스트림과 연결되어 객체를 직렬화하고, ObjectInputStream
은 바이트 입력 스트림과 연결되어 객체로 복원하는 역직렬화를 한다.
byte ---> InputStream ---> ObjectInputStream(역직렬화) ---> 객체 (program 내부) ---> ObjectOutputStream(직렬화) ---> OutputStream ---> byte
다음은 ObjectInputStream
과 ObjectOutputStream
보조 스트림을 연결하는 코드이다.
ObjectInputStream ois = new ObjectInputStream(바이트 입력 스트림);
ObjectOutputStream oos = new ObjectOutputStream(바이트 출력 스트림);
ObjectOutputStream
으로 객체를 직렬화하기 위해서는 writeObject
메서드를 사용한다.
oos.writeObject(객체);
반대로 ObjectInputStream
의 readObject
메서드는 읽은 바이트를 역직렬화해서 객체로 생성한다.
객체 타입 변수 = (객체타입) ois.readObject();
단, 직렬화(Serialize), 역직렬화(Deserialize)를 객체에 적용하기 위해서는 해당 객체가 Serializable
인터페이스를 구현하고 있어야한다.
public class Something implements Serializable {
}
다음은 다양한 객체를 파일에 저장하고 다시 파일로부터 읽어 객체로 복원하는 예제이다. 복수의 객체를 저장할 경우, 출력된 객체 순서와 동일한 순서로 객체를 읽어야 한다.
public class Member implements Serializable {
private static final long serialVersionUID = -6221903048729L;
public String name;
public int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Member{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Product implements Serializable {
private static final long serialVersionUID = -6221903048730L;
public String name;
public int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
Member
와 Product
모두 Serializable
인터페이스를 implements
하고 있는 것을 볼 수 있다. 위의 예제에서 볼 수 있듯이 Serializable
인터페이스에는 별다른 추상 메서드가 없다. 그저 직렬화-역직렬화가 가능하다는 것만 알려주는 것이다.
public class Main {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream("./object.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Member kim = new Member("park",30);
Product notebook = new Product("notebook", 1000);
int[] arr1 = {1, 2,3};
oos.writeObject(kim);
oos.writeObject(notebook);
oos.writeObject(arr1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("./object.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
Member m2 = (Member) ois.readObject();
Product p2 = (Product) ois.readObject();
int[] arr2 = (int[]) ois.readObject();
ois.close();
System.out.println(m2); // Member{name='park', age=30}
System.out.println(p2); // Product{name='notebook', price=1000}
for(int num: arr2) {
System.out.print(num); // 123
}
}
}
ObjectOutputStream
을 사용하여 object.dat
에 Member
, Product
, int[]
객체를 직렬화하여 넣고, ObjectInputStream
으로 역직렬화하여 code에 객체로 가져오는 것을 볼 수 있다. 이때 readObject
의 반환 타입이 Object
이므로 원하는 객체의 타입으로 명시적 변환시켜주어야 한다.
참고로 객체가 직렬화될 때 instance의 모든 field값들은 직렬화 대상이다. 즉 public 뿐만 아니라 private도 직렬화된다. 단, instance의 static
field와 transient
field에 대해서는 직렬화되지 않는다.
public class XXX implements Serializable {
// 직렬화 대상
public int field1;
protected int field2;
int field3;
private int field4;
// 직렬화 제외
public static int field5; // 정적 필드는 직렬화 제외
transient int field6; // transient로 선언된 필드는 직렬화 제외
}
위의 예제를 보면 Member
와 Product
에 SerialVersionUID
field를 사용한 것을 볼 수 있다. 직렬화할 때 사용된 클래스와 역직렬화할 때 사용된 클래스는 기본적으로 동일한 클래스여야 한다. 만약 클래스의 이름이 같더라도 클래스의 내용이 다르면 역직렬화에 실패한다.
다음 코드를 보자, 첫번째 Member
클래스로 생성한 객체를 직렬화하면, 두번째 Member
클래스로 역직렬화가 불가능하다. 그 이유는 두번째 Member
클래스에는 field3
가 있기 때문이다.
public class Member implements Serializable {
int field1;
int field2;
}
...
public class Member implements Serializable {
int field1;
int field2;
int field3;
}
class 내용이 다르다 할지라도 직렬화된 field를 공통으로 포함하고 있다면 역직렬화할 수 있는 방법이 있다. 두 클래스가 동일한 serialVersionUID
상수 값을 가지고 있으면 된다.
public class Member implements Serializable {
private static final long serialVersionUID = -6221903048729L;
int field1;
int field2;
}
...
public class Member implements Serializable {
private static final long serialVersionUID = -6221903048729L;
int field1;
int field2;
int field3;
}
serialVersionUID
의 값은 개발자가 임의로 줄 수 있다. 단, unique함을 보장해야한다. 대부분의 ide에서 자동 생성을 제공하므로 사용하도록 하자.
java는 패키지 파일과 디렉터리 정보를 가지고 있는 File
과 Files
클래스를 제공한다. Files
는 File
을 개선한 클래스로 생각하면 된다.
File
클래스로부터 File
객체를 생성하는 방법은 다음과 같다.
File file = new File("...path");
경ㄹ 구분자는 운영체제마다 약간 다르다. 윈도우는 //
, /
이고, 리눅스, 맥은 /
을 사용한다.
File file = new File(".../path/file.txt");
File
객체를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니다. 또한, 경로에 실제 파일이나 디렉토리가 없더라도 Exception이 발생하지 않는다. 파일이나 디렉토리가 실제 있는지 확인하고 싶다면 File
객체를 새성하고 나서 exists()
메서드를 호출해보면 된다.
boolean isExist = file.exists(); // file이나 directory가 존재한다면 true를 반환
exists()
메서드가 false
를 반환할 경우, 다음 메서드로 file 또는 directory를 생성할 수 있다.
메서드 | 설명 |
---|---|
boolean createNewFile() | 새로운 파일 생성 |
boolean mkdir() | 새로운 디렉터리 생성 |
boolean mkdirs() | 경로 상에 없는 모든 디렉터리를 생성 |
exists()
메서드의 반환값이 true
라면 다음의 메서드를 사용할 수 있다.
메서드 | 설명 |
---|---|
boolean delete() | file 또는 directory 삭제 |
boolean canExecute() | 실행할 수 있는 file인지 여부 |
boolean canRead() | 읽을 수 있는 파일인지 여부 |
boolean canWrite() | 수정 및 저장할 수 있는 파일인지 여부 |
String getName() | 파일의 이름을 반환 |
String getParent() | 부모 디렉터리를 반환 |
File getParentFile() | 부모 디렉터리를 File 객체로 생성 후 반환 |
String getPath() | 전체 경로를 반환 |
boolean isDirectory() | 디렉터리인지 여부 |
boolean isFile() | 파일인지 여부 |
boolean isHidden() | 숨김 파일인지 여부 |
long lastModified() | 마지막 수정 날짜 및 시간을 반환 |
long length() | 파일의 크기 반환 |
String[] list() | 디렉터리에 포함된 파일 및 서브 디렉터리 목록 전부를 String 배열로 반환 |
String[] list(FilenameFilter filter) | 디렉터리에 포함된 파일 및 서브 디렉터리 목록 중에 FilenameFilter 에 맞는 것만 String 배열로 반환 |
File[] listFiles() | 디렉터리에 포함된 파일 및 서브 디렉터리 목록 전부를 File 배열로 반환 |
FIle[] listFiles(FilenameFilter filter) | 디렉터리에 포함된 파일 및 서브 디렉터리 목록 중에 FilenameFilter 에 맞는 것만 File 배열로 반환 |
다음은 temp
directory에 images
directory를 생성하고 file1.txt
, file2.txt
, file3.txt
파일을 생성하고, temp
directory에 있는 내용을 출력하는 예제이다.
public class Main {
public static void main(String[] args) throws Exception {
File dir = new File("./temp/images");
File file1 = new File("./temp/file1.txt");
File file2 = new File("./temp/file2.txt");
File file3 = new File("./temp/file3.txt");
if(dir.exists() == false) {
dir.mkdirs();
}
if(file1.exists() == false) {
file1.createNewFile();
}
if(file2.exists() == false) {
file2.createNewFile();
}
if(file3.exists() == false) {
file3.createNewFile();
}
File temp = new File("./temp");
File[] contents = temp.listFiles();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd a HH:mm");
for(File file: contents) {
System.out.printf("%-25s", sdf.format(new Date(file.lastModified())));
if(file.isDirectory()) {
System.out.printf("%-10s%-20s", "<DIR>", file.getName());
} else {
System.out.printf("%-10s%-20s", file.length(), file.getName());
}
System.out.println();
}
}
}
결과는 아래와 같다.
2025-02-17 PM 17:17 <DIR> images
2025-02-17 PM 17:17 0 file1.txt
2025-02-17 PM 17:17 0 file3.txt
2025-02-17 PM 17:17 0 file2.txt
파일 또는 directory의 정보를 얻기 위해 File
객체를 단독으로 사용할 수 잇찌만, 파일 입출력 스트림을 생성할 때 경로 제공 목적으로 사용되기도 한다.
File file = new File(".../path/image.txt");
FileInputStream fis = new FileInputStream(file);
File
객체를 넘겨주기만 해도 된다.
Files
클래스는 정적 메서드로 구성되어 있기 때문에 File
클래스처럼 객체로 만들 필요가 없다. Files
의 정적 메서드는 운영체제의 파일시스템에게 파일 작업을 수행하도록 위임한다.
Files
의 모든 정적 메서드들은 매개값으로 Path
객체를 받는다. Path
객체는 파일이나 디렉터리를 찾기 위한 경로 정보를 가지고 있는데, 정적 메서드인 get()
메서드로 다음과 같이 얻을 수 있다.
Path path = Paths.get(String first, String ... more);
get()
메서드의 매개값은 파일 경로인데, 전체 경로를 한꺼번에 지정해도 좋고, 상위 디렉터리와 하위 디렉터리를 나열해서 지정해도 좋다. 다음은 ./temp/dir/file.txt
경로를 이용해서 Path
객체를 얻는 방법을 보여준다.
Path path = Paths.get("./temp/dir/file.txt");
Path path = Paths.get("./temp/dir","file.txt");
Path path = Paths.get("./temp","dir","file.txt");
헷갈리니까 첫번째 방법만 기억하자.
아래 예제는 Files
클래스를 이용해서 ./temp
directory에 user.txt
파일을 생성하고 읽는 방법을 보여준다.
public class Main {
public static void main(String[] args) {
try {
String data = "" + "id: winter\n" + "email: winter@mycompany.com\n";
Path path = Paths.get("./temp/user.txt");
Files.writeString(path, data,Charset.forName("UTF-8"));
System.out.println("file type: " + Files.probeContentType(path));
System.out.println("file size: " + Files.size(path) + "bytes");
String contents = Files.readString(path, Charset.forName("UTF-8"));
System.out.println(contents);
}catch (IOException e) {
e.printStackTrace();
}
}
}
Files
의 모든 정적 메서드에서는 맨 앞에 Path
객체가 들어가는 것을 볼 수 있다.
출력 결과는 아래와 같다.
file type: text/plain
file size: 39bytes
id: winter
email: winter@mycompany.com
또한, project의 temp/user.txt
에 가면 user.txt
파일이 만들어진 것을 볼 수 있다.
id: winter
email: winter@mycompany.com