[입출력]02. BufferedReader 활용
Scanner는 입력을 가장 편하게 처리할 수 있는 클래스이지만 BufferedReader에 비해 상대적으로 느리다. 이번 포스트에서는 BufferedReader에 대해 알아보자.
* 사실 많은 APS 사이트에서 Java의 표준 입력으로 Scanner를 지정하고 있기 때문에 Scanner로 대부분 문제를 풀 수 있다. 다만 약간의 시간을 더 줄여야할 필요가 있다면 BufferedReader를 썼을 때 확실히 성능의 향상을 이룰 수 있다.
BufferedReader
생성과 데이터 읽기
BufferedReader 역시 Scanner와 마찬가지로 키보드, 파일, 문자열을 통해서 데이터를 읽어올 수 있다.
// 키보드를 이용한 입력 처리
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 파일을 이용한 입력 처리
br = new BufferedReader(new FileReader("c:/Temp/sample_input.txt"));
// 문자열을 이용한 입력 처리
br = new BufferedReader(new StringReader(src));
BufferedReader를 통해서 데이터를 읽어들일 때 사용하는 메서드는 readLine()인데 이름대로 한 줄씩 데이터를 읽어들인다. readLine()를 이용해서 데이터를 읽다가 마지막 데이터를 읽으면 null이 리턴되는데 이 시점이 더 이상 읽을 데이터가 없는 시점이다.
다음은 BufferedReader를 이용한 데이터 읽기의 예이다.
private static void read0() throws IOException {
// 키보드를 이용한 입력 처리
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 파일을 이용한 입력 처리
br = new BufferedReader(new FileReader("c:/Temp/sample_input.txt"));
// 문자열을 이용한 입력 처리
br = new BufferedReader(new StringReader(src));
String line = null;
while( (line=br.readLine())!=null) {
System.out.println(line);
}
}
private static String src = "hello\r\n" +
"java\r\n" +
"world\r\n" +
"scanner is easy";
BufferedReader의 readLine()은 토큰 단위가 아닌 한 줄 단위로 데이터를 읽기 때문에 토큰 단위로 분리하기 위해서는 별도의 클래스가 필요하다. 이때 사용될 수 있는 것이 Stringtokenizer나 String의 split()이다.
문자열의 분리
Scanner의 nextXXX 계열 메서드들은 주어진 delimiter를 이용해 입력 값을 token으로 만들어서 처리할 수 있지만 자주 읽어야 하기 때문에 성능상 좋지는 않다. 반면 Scanner의 nextLine이나 BufferedReader의 readLine()의 경우는 한 줄씩 읽어들이기 때문에 특히 길게 나열된 데이터를 읽어들일 때 성능상 좋은 위치를 차지할 수 있다.
그럼 읽어들인 한 줄의 문자열을 어떻게 분리할 수 있을까?
구분자가 없는 문자열 처리
먼저 구분자 없이 한 문자씩 분리하는 경우가 있다. 이때는 문자열의 charAt 함수를 이용해서 한 문자씩 분리한다.
private static void perChar() {
String source = "Hello";
for(int i=0; i<source.length(); i++) {
System.out.printf("%d번째 문자: %c%n", i, source.charAt(i));
}
}
StringTokenizer 사용
StringTokenizer는 문자열을 구분자(delimiter)를 이용해서 분리해주는 클래스이다. 이때 기본 구분자로는 공백문자, \t, \r, \f, \n 이 사용된다. 특별한 구분자를 사용하려는 경우 기본 문자열과 함께 구분자를 넘겨주면 된다.
StringTokenizer st1 = new StringTokenizer("Hello Java World"); // 기본 구분자 활용
StringTokenizer st2 = new StringTokenizer("Hello Java World", " "); // 공백만을 활용
StringTokenizer에는 구분자에 의해 분리된 토큰들의 정보를 알수 있는 다양한 메서드들이 제공된다.
메서드 명 | 선언부와 설명 |
countTokens () | public int countTokens() |
소모하지 않고 남아있는 토큰의 개수를 리턴한다. 여기서 소모의 개념은 nextToken()을 호출한 것을 말한다. | |
hasMoreTokens () | public boolean hasMoreTokens() |
남아있는 토큰이 있는지 여부를 리턴한다. | |
nextToken () | public String nextToken() |
다음 토큰을 문자열로 리턴한다. 토큰이 반환되면 countTokens()의 개수가 1줄어든다. |
다음 예제를 통해 메서드들의 기본 사용법에 대해 알아보자.
private static void tokenizer1() {
String source = "10,030,042";
StringTokenizer tokens = new StringTokenizer(source, ",");
System.out.println("토큰의 개수: " + tokens.countTokens());
int total = 0;
while (tokens.hasMoreTokens()) {
total += Integer.parseInt(tokens.nextToken());
}
System.out.printf("총 데이터의 합은: %d%n", total);
}
토큰의 개수: 3
총 데이터의 합은: 82
위의 예제는 ','를 이용해서 source 문자열을 토큰화하며 StringTokenizer에서 토큰의 데이터 타입은 언제나 문자열이므로 Integer.parseInt()와 같은 메서드를 통해 원하는 타입으로 형변환 후 사용하는 모습을 보여준다.
StringTokenizer를 사용하면서는 2가지 주의사항이 있다.
첫 번째 주의사항은 토큰은 한번 읽어버리면 소비된다는 점이다. 다음의 예를 살펴보자.
private static void tokenizer2() {
String source = "10,030,042";
StringTokenizer tokens2 = new StringTokenizer(source, ",");
for (int i = 0; i < tokens2.countTokens(); i++) {
System.out.println(tokens2.nextToken());
}
}
tokenizer1() 과 동일하게 StringTokenizer를 구성하였고 countTokens()를 통해 토큰의 개수를 가져온 후 for 문을 이용해서 전체 토큰을 출력하려는 메서드이다.
하지만 실제 실행해보면 예상과는 다른 결과를 확인할 수 있다.
10
030
실제 출력된 토큰은 두개이며 맨 마지막 토큰인 042는 출력되지 못했다.
이유는 countToken()은 남아있는 토큰의 개수를 리턴하는데 nextToken()을 호출할 때마다 토큰이 하나씩 소모되기 때문에 countToken()의 값도 계속 줄어들게 된다. 결국 아래 그림처럼 3번째 토큰은 출력할 기회가 없어져버린다.
위와 같은 문제를 방지하기 위해서는 countTokens()의 값을 미리 변수에 할당해 놓거나 hasMoreTokens()를 조건에 사용하는 것이 좋다.
int cnt = tokens2.countTokens();
for(int i=0; i<cnt; i++) {
System.out.println(tokens2.nextToken());
}
for(; tokens2.hasMoreTokens();) {
System.out.println(tokens2.nextToken());
}
두 번째 주의 사항은 StringTokenizer를 만들면서 사용한 구분자는 문자열 단위가 아니라 개별 문자 단위로 적용된다는 점이다. 예를 들어 구분자에 '0,'라고 입력한다면 토큰이 나눠질때는 '0,'을 기준으로 나눠지는 것이 아니고 '0', 또는 ','을 이용해서 나눠진다.
다음과 같은 문자열에서 학생의 이름과 총점만을 출력하도록 처리해보자.
String source = "이름:홍길동,Java:100,HTML:80,Script:85";
문자열을 살펴보면 전체적으로 key:value 형태의 데이터가 ,를 이용해서 나뉘어있다. 따라서 구분자는 ':,'를 사용할 수 있다.
StringTokenizer tokens = new StringTokenizer(source, ": ,");
System.out.println("총 토큰의 개수: " + tokens.countTokens());
이렇게 분리하면 토큰의 개수는 총 8개가 될 것이다.
다음으로 위 토큰들은 key:value의 구조로 되어있기 때문에 언제나 두 개씩 빼내면 된다.
private static void tokenizer3() {
String source = "이름:홍길동,Java:100,HTML:80,Script:85";
StringTokenizer tokens = new StringTokenizer(source, ": ,");
System.out.println("총 토큰의 개수: " + tokens.countTokens());
String name = null;
int total = 0;
while (tokens.hasMoreTokens()) {
String key = tokens.nextToken();
if (key.equals("이름")) {
name = tokens.nextToken();
} else {
total += Integer.parseInt(tokens.nextToken());
}
}
System.out.printf("이름은 %s, 총점은 %d%n", name, total);
}
총 토큰의 개수: 8
이름은 홍길동, 총점은 265
String의 split() 사용
다음으로 String의 split() 을 이용해서 분리하는 방법에 대해 알아보자.
split()의 파라미터인 문자열은 단순구분자가 아닌 정규 표현식형태로 이 표현식에 의거해서 문자열을 분리한다.
정규표현식에 대한 설명은 정규표현식를 참조한다.
split() 메서드는 분리된 문자열들을 String [] 형태로 반환하기 때문에 사용법은 매우 쉽다.(정규표현식만 정복하면 된다. ㅜㅜ)
StringTokenizer의 처음 예제를 split() 형태로 바꿔보면 아래와 같다.
private static void split1() {
String source = "10,030,042";
String [] splited = source.split(",");
System.out.println("토큰의 개수: " + splited.length);
int total = 0;
for(String item: splited) {
total +=Integer.parseInt(item);
}
System.out.printf("총 데이터의 합은: %d%n", total);
}
토큰의 개수: 3
총 데이터의 합은: 82
이 예에서는 상관 없지만 만약 구분자가 '.' 인 경우는 이야기가 달라진다.
private static void split2() {
String source = "10.030.042";
String [] splited = source.split(".");
System.out.println("토큰의 개수: " + splited.length);
int total = 0;
for(String item: splited) {
total +=Integer.parseInt(item);
}
System.out.printf("총 데이터의 합은: %d%n", total);
}
토큰의 개수: 0
총 데이터의 합은: 0
정규표현식에서는 '.'의 의미가 어떤 한 문자를 나타낸다. 따라서 입력된 source의 문자 하나 하나가 구분자로 사용되기 때문에 데이터는 하나도 없는 것이다.
따라서 위와 같은 경우는 이스케이프 문자로 처리해줘야 한다.
private static void split2() {
String source = "10.030.042";
//String [] splited = source.split("."); // 정규표현식에서 .는 어떤 문자 하나
String [] splited = source.split("\\."); // 예외 문자 처리 필요
System.out.println("토큰의 개수: " + splited.length);
int total = 0;
for(String item: splited) {
total +=Integer.parseInt(item);
}
System.out.printf("총 데이터의 합은: %d%n", total);
}
마지막으로 tokenizer3()는 아래와 같이 변경가능하다. 정규 표현식에서 |는 or 조건을 나타낸다.
private static void split3() {
String source = "이름:홍길동,Java:100,HTML:80,Script:85";
String [] splited = source.split(":|,");// . 또는 ,
System.out.println("총 토큰의 개수: " + splited.length);
String name = null;
int total = 0;
for(int i=0; i<splited.length; i+=2) {
String key = splited[i];
if (key.equals("이름")) {
name = splited[i+1];
} else {
total += Integer.parseInt(splited[i+1]);
}
}
System.out.printf("이름은 %s, 총점은 %d%n", name, total);
}
이처럼 StringTokenizer는 새로운 클래스의 사용법을 익힐 필요가 있고 String의 split는 정규 표현식에 대한 지식이 필요하다. 일반적으로 StringTokenizer의 속도가 split 보다 빠른 편이다.
형변환
Scanner에서 데이터를 읽어들일 때는 nextInt(), nextBoolean() 과 같이 특정 데이터 타입으로 읽어들일 수 있었지만 StringTokenizer나 split()은 단지 문자열을 쪼개기만 하기 때문에 원하는 타입으로 사용하기 위해서는 Wrapper 클래스를 이용한 형변환이 필요하다.
Wrapper class
자바에서는 기본형 8개에 대응하는 8개의 Wrapper 클래스를 이용해서 기본형, 객체형, 문자열간의 형 변환을 지원한다.
기본형 | Wrapper 클래스 | 기본형 | Wrapper 클래스 |
byte | Byte | long | Long |
char | Character | float | Float |
short | Short | double | Double |
int | Integer | boolean | Boolean |
Wrapper class를 이용한 형변환
Wrapper 클래스의 종류는 다양하지만 모두 동일한 패턴의 메서드/방법으로 형 변환이 가능하다.
- 문자열 -> 기본형으로 변환을 위해서는 parseXX(String value) 계열의 메서드가 제공되며 반대로는 기본형에 빈 문자열을 더해서 그냥 결합시켜주면 된다.
- 문자열 -> Wrapper로 변환을 위해서는 valueOf 메서드를 사용하고 반대로는 toString() 메서드를 사용한다.
- 기본형 -> Wrapper로의 변환 시에도 문자열 처럼 valueOf 메서드를 사용하고 반대로는 xxValue() 계열의 메서드를 사용한다.
String strNum = "100";
int num = Integer.parseInt(strNum);
String strNum2 = num+"";
Integer wrapper1 = Integer.valueOf(strNum2);
int num2 = wrapper1.intValue();
Integer wrapper2 = Integer.valueOf(num2);
System.out.println(strNum2+" : "+num2+" : "+wrapper2);