
자바나 코틀린에서 제공해주는 입출력 함수는 아주 다양하다. 그런데 알고리즘을 풀이할 때 어떤 조합을 사용해야 입출력이 가장 빠를까? 그리고 왜 다른 것들에 비해 빠를까?
이전 포스팅 에서 split 과 stringTokenizer 에 대해서만 알아봐서 아쉬웠는데, 그 연장선으로 조금 더 자세하게 정리해보고 싶었다.
이번 글에서는 어떤 입출력 함수들이 있는지 알아보고, 위에서 말한 궁금증을 해결하기 위해 시도해본 것들을 정리해보려고 한다.
참고로 컴파일 에러는 import java.io.StreamTokenizer 등 import 를 까먹고 안해줘서 발생했으니 나처럼 까먹지 말자...
readln(), readLine(), print(), println()
System.'in'.bufferedReader()
StringBuilder()
System.out.bufferedWriter()
StringBuilder()
입력값을 받고 공백 등으로 구분할 경우
split() , StringTokenizer()
readln 과 readLine 은 같은 기능을 수행한다. (단, readln 은 null 이면 예외를 발생시킨다)
// readln
fun ex1() {
val name1 = readln().toInt()
val age1 = readln().toInt()
println("name1: $name1, age1: $age1")
}
// readLine
fun ex2() {
val br = BufferedReader(InputStreamReader(System.`in`))
val name2 = br.readLine().toInt()
val age2 = br.readLine().toInt()
println("name2: $name2, age2: $age2")
}
Scanner 는 사용자가 값을 입력할 때마다 전달하지만, BufferReader 는 입력을 마친 뒤 한번에 전달하기 때문에 더 빠르다. 참고로 br.read() 는 아스키코드 반환하는거라 readLine() 을 써줘야 한다.
또한 마지막에 flush 또는 close 꼭 해줘서 버퍼를 비워야한다. (단, 반복문 안에 선언하면 성능 저하되니 유의)
fun main() {
// bufferedReader 로 입력받고 bufferedWriter 로 출력
val br = System.`in`.bufferedReader()
val bw = System.out.bufferedWriter()
val userInput = br.readLine()
// 출력
bw.write(userInput)
// 또는 bw.close()
bw.flush()
}
생성된 후 내용이 변경 될 수 있는 문자열을 처리할 때 빠르다. 왜냐하면 String 은 불변객체다. 따라서 문자열을 수정해야 할 때, 기존 참조하고 있던 곳에 추가하는 것이 아니라 새로 변경할 값으로 String 객체를 생성하고 참조를 바꾼다. 그러나 StringBuilder 는 가변객체다. 기존에 참조하고 있는 값을 바꾸기 때문에 String 보다 빠르게 처리할 수 있는 것이다.
val sb = StringBuilder("Hello")
println(sb) // Hello
stringBuilder 메서드 (입출력)
sb.append("World")
println(sb) // "Hello World
sb.insert(6, "Kotlin")
println(sb) // Hello Kotlin World
sb.delete(6, 12)
println(sb) // Hello World
sb.reverse()
println(sb) // dlroW olleH
sb.replace(0, 5, "Hi")
println(sb)
val resultString = sb.toString()
sb.clear()
println(clear)
(bufferedReader 또는 StringBuilder 등으로) 문자열 입력을 받았을 때, 공백 혹은 컴마 같은 특정 문자를 기준으로 문자열을 나누고 싶을 때 사용한다.
split, chunked는 배열로 돌아오기 때문에 StringToKenizer 보다 살짝 느리다.
기본 split
// 일반
fun main() {
val text = "Hello World"
val split = text.split(" ")
println(split) // [Hello, World]
}
String 을 다른 형태로 바꿔줘야 할 때 map 을 사용한다.
// 형변환이 필요할 때
// readln() + split()
fun main(){
val (x,y) = readln().split(" ").map { it.toInt() }
println("$x $y")
}
// 형변환이 필요할 때
// bufferedReader() + split()
fun main() {
val br = System.`in`.bufferedReader()
val bw = System.`out`.bufferedWriter()
val (a, b) = br.readLine().split(" ").map { it.toInt() }
bw.write("${a + b}")
br.close()
bw.close()
}
bufferedReader 혹은 bufferedWriter 사용 시, close 로 메모리 관리하는 게 번거롭다면 아래처럼 use 를 써도 된다. use 블록을 벗어나면 스트림이 자동으로 닫히므로 close()를 명시적으로 호출할 필요가 없다.
fun main() {
System.`in`.bufferedReader().use { br ->
System.out.bufferedWriter().use { bw ->
val (a, b) = br.readLine().split(" ").map { it.toInt() }
bw.write("a + b = ${a + b}")
}
}
}
fun main() {
val text = "HelloWorld"
val chunked = text.chunked(2)
println(chunked) // [He, ll, ow, or, ld]
}
지정한 구분자로 문자열을 쪼개주는 클래스. 문자열을 토큰화 한다는 것은 하나의 문자열을 여러 개의 토큰으로 분리한다는 의미이다.
fun main() {
val str = "a b c d"
val st = StringTokenizer(str) // 문자열을 쪼갬, 미지정시 공백 기준
}
fun main() {
val tokenizer = StreamTokenizer(InputStreamReader(System.`in`))
// 첫 번째 숫자 읽기
tokenizer.nextToken()
val num1 = tokenizer.nval.toInt()
// 두 번째 숫자 읽기
tokenizer.nextToken()
val num2 = tokenizer.nval.toInt()
// 두 숫자 더하기
println(num1 + num2)
}
BufferedReader 와 StringTokenizer 를 함께 썼을 때 가장 빨랐다. 아래 소요시간별 코드 참고.
System.in.bufferedReader()
vs BufferedReader(InputStreamReader(System.in))
같은 BufferedReader 도 선언 방식에 따라 소모 시간이 다르다는 점이 흥미로웠다. 후자가 전자보다 빨랐다.
이유를 찾아보니 전자는 Kotlin에서 제공하는 확장 함수였기 때문인데, IDE 에서 System.in.bufferedReader() 코드를 타고 들어가면 아래와 같은 내용을 확인할 수 있다.

결국 System.in.bufferedReader()는 자바의 BufferedReader(InputStreamReader(System.in)) 를 좀 더 편리하게 사용할 수 있도록 한겹 감싸고 있던 코드였던 것이다.
또, 각각 다른 변수에 담아서 print로 출력하는 것과 변수에 담지 않고 바로 print 하는 것 사이에도 의외로 시간 차이가 났다. 사실 내 기준으로 5번 코드가 익숙하고 깔끔해서 좋은데 다른 코드가 더 빠른 것은 조금 아쉬운 결과였다.
(1) 80ms
fun main() = with(StreamTokenizer(System.`in`.bufferedReader())){
fun nextInt() : Int { nextToken(); return nval.toInt() }
val a = nextInt(); val b = nextInt()
print(a+b)
// print(nextInt()+nextInt()) 도 동일하게 80ms
}
(2) 80ms
fun main() {
val br = System.`in`.bufferedReader()
val st = StringTokenizer(br.readLine())
print(Integer.parseInt(st.nextToken()) + Integer.parseInt(st.nextToken()))
}
(3) 72ms
fun main() {
val br = BufferedReader(InputStreamReader(System.`in`))
val st = StringTokenizer(br.readLine())
print(Integer.parseInt(st.nextToken()) + Integer.parseInt(st.nextToken()))
}
(4) 88ms
fun main() {
val br = BufferedReader(InputStreamReader(System.`in`))
val st = StringTokenizer(br.readLine())
val a = Integer.parseInt(st.nextToken())
val b = Integer.parseInt(st.nextToken())
print(a+b)
}
(5) 112ms
fun main() {
val br = System.`in`.bufferedReader()
val bw = System.out.bufferedWriter()
val (a, b) = br.readLine().split(" ").map { it.toInt() }
bw.write("${a+b}")
br.close()
bw.close()
}
https://velog.io/@stdiodh/Kotlin-Fast-IO-빠른-입출력
https://velog.io/@alswp006/StringTokenizer와-StreamTokenizerfeat.StringTokenizer뜯어보기