/* 본 글은 개인적으로 공부한 내용을 정리한 글이므로 오류가 있을 수 있습니다. */
참고 자료
[빡센] 코틀린 8. 입력과 출력 : https://bbaktaeho-95.tistory.com/11
알고리즘 테스트를 보면 값을 입력 받고 처리하는 식의 문제가 많다. 이번에는 코틀린으로 입력을 처리하는 방법에 대해 알아보자. 크게 입력을 받는 방법은 두개가 있다.
1. readLine() 함수 사용
fun main(args:Array<String>){
print("입력할 값 :")
val value = readLine()
println("입력 값 : $value")
}
readLine() 함수를 사용하게되면 반환 값을 String? 형태로 반환한다. 필요시 형변환을 통해 필요한 데이터 형태로 바꾸면 된다. (toInt, toDouble 같은 함수를 이용해 파싱)
여러 변수들을 받고 싶을 때는 어떻게 해결하면 좋을까?
fun main(args:Array<String>){
print("입력 크기 : ")
val size: Int = readLine()!!.toInt()
val arr = Array<Int>(size) { readLine()!!.toInt() }
var sum = 0
for (item in arr) {
sum+=item
}
println("Sum: $sum")
}
배열을 이용해서 해결 했다.
2. Scanner 사용
Java에서 사용하는 것처럼 Scanner 객체를 만들어 사용할 수 있다.
import java.util.Scanner
fun main(args:Array<String>){
//2. Scanner()
val sc = Scanner(System.`in`)
print("입력할 값: ")
val value3 = sc.nextInt()
val value4 = sc.nextInt()
println("입력 값: $value3, $value4")
}
next() 함수를 이용하면 문자 또는 문자열을 공백을 기준으로 입력받는다. 그렇기 때문에 1 2 3 4 로 입력했을 때 nextInt()를 이용해 파싱할 수 있다.
readLine() 과 마찬가지로 nextLine()은 한 줄 전체를 String 타입으로 입력 받는다.
추가 공부 ( 2021 / 09 / 24 )
입력에 관해 더 자세히 알아보고자 자바 입력 / 버퍼 입출력에 대해 추가적으로 공부했다.
[st-lab] Java - 입력 뜯어보기(Scanner, InputStream, BufferedReader) : https://st-lab.tistory.com/41
[Dev.Meoru for 안드로이드] Kotlin - 빠른 입출력(I/O) : https://meoru-tech.tistory.com/57
*앞으로 쓸 내용들은 위 참고 자료로 부터 나온 내용들을 개인적으로 요약한 자료입니다. 자세한 정보를 원한다면 위 링크로 들어가 읽어보길 바랍니다. (첫 번째 자료 강추)
1. Java 인코딩
*인코딩이 무엇인지 모른다면? 이 글([CS] 인코딩을 참고하자)
Java는 String 을 처리할 때 내부에서는 UTF-16BE 인코딩으로 문자열을 저장하고, 송수신에서는 직렬화가 필요한 경우 변형된 UTF-8 을 사용한다. 문자열 입/출력 할때에만 사용자가 지정한 인코딩 값과 또는 운영체제의 인코딩 값으로 문자열을 인코딩한다. UTF-8 기준 (editor file endoing 설정에 따라 달라진다.)
*입력(UTF-8) -> 송수신(modified UTF-8) -> 자바 메모리 (UTF-16) -> 송수신(modified UTF-8) -> 출력(UTF-8)
UTF-8 / UTF-16 차이
기본적으로 문자의 영역에 따라 사용하는 Byte 가 다르다. UTF-8 의 경우 영어는 1Byte 한글의 경우는 3Byte를 사용한다.
UTF-16은 거의 모든 문자가 2Byte 를 사용한다.
cf > 1 ~ 127 까지는 Ascii 코드 값과 유니코드(UTF-8, UTF-16 등..), MS계열 코드(CP949, MS949 등..) 의 값이 같다.( ms 랑 유니코드는 해당 범위에서 92 번만 다른데 이는 역슬래시로 윈도우에서는 대부분 ₩ 으로 표현되고 맥북, 리눅스 계열에서는 \ 으로 표현된다.)
val a = 'a'.toInt() // 97
val b = '가'.toInt() // 44032
char 'a' 를 Int 값으로 변환할 때 나온 97은 "아스키 코드 값" 이라고 이해하기 보다는 파일 인코딩 형식의 10진수 값이 나온다는 것이 정확하다.
2. Stream(스트림)
입/출력 을 완벽히 이해하기 위해서는 스트림을 알아야한다. 일단 'Stream' 의 뜻은 시내(n), 흐르다(v) 이다. 그렇다면 컴퓨터에서 Stream은 무엇을 의미할까?
쉽게 말하자면 "데이터 통로"이다.
*한 곳에서 다른 곳으로의 데이터 흐름을 스트림이라고 한다.
스트림은 단반향이다. 입/출력 말고도 파일 데이터, HTTP 응답 데이터 등도 스트림의 예시라고 할 수 있다.
InputStream
inputStream 은 자바에서 가장 기본이 되는 입력 스트림(통로)이다.
import java.util Scanner
fun main(){
val sc = Scanner(System.`in`)
}
여기서 Scanner에 넣어주는 System.`in` 은 InputStream 타입의 필드이다. System 클래스를 확인해보면 다음과 같다.
public final class System {
public static final InputStream in;
...
}
System class 에 있는 in 이라는 필드는 InputStream 의 정적 필드이다.
즉, Scanner 에 넣어주는 System.`in` 은 통로(inputStream)라고 생각하면 된다.
특징
- 입력받은 데이터는 int 형으로 저장되는데 이는 10진수의 UTF-16 값으로 저장된다.
- 1 byte 만 읽는다.
3. InputStreamReader
import java.util Scanner
fun main(){
val sc = Scanner(System.`in`)
}
위 코드에서 일어나는 과정을 설명해보자. 먼저 Scanner 클래스의 생성자로 주입해주는 System.`in` 은 다음 Scanner 클래스를 생성한다.
public Scanner(InputStream source) {
this((Readable)(new InputStreamReader(source)), (Pattern)WHITESPACE_PATTERN);
}
위 코드를 보면 Scanner(Readable source, Pattern pattern) 으로 넘겨진다.
여기서 InputStreamReader 가 나온다. InputStream의 특징에서 1byte 만 읽는다는 것을 알 수 있다. 이 때문에 InputStream은 문자를 온전하게 받아오는 것이 불가능한 경우가 생긴다. (출발점에서는 3byte 를 보냈는데 도착점에서 2byte를 스트림에 남기고 1byte만 받아가기 때문에 이를 변환하면 다른 문자로 변환됨.)
이러한 한계를 극복하고자 나온 것이 InputStreamReader 이다. 이는 문자를 온전히 받기 위해 InputStream 을 확장한 것이다. 문자를 온전히 받을 수 있는 InputStreamReader 를 문자 스트림이라고 부른다. 즉, 바이트 단위로 InputStream이 데이터를 받으면 InputStreamReader가 바이트를 묶어서 char 형태로 데이터를 처리해 변환한다.
특징
- 바이트 단위 데이터를 문자(character) 단위 데이터로 처리할 수 있도록 변환해준다.
- char 배열로 데이터를 받을 수 있다.
4. Scanner 메서드 원리
우리는 입력을 처리하기 위해 Scanner 의 next(), nextInt() , nextDouble() 등 메서드를 썼다. 그러면 이러한 메서드의 과정은 어떻게 될까?
과정은 다음과 같다. (자세한 과정은 첫 번째 참고자료에 아주 자세히 나와있다!)
- InputStream (바이트스트림) 을 통해 입력 받음
- 문자로 온전하게 받기 위해 중개자 역할을 하는 InputStreamReader(문자스트림) 을 통해 char 타입으로 데이터를 처리함
- 입력받은 문자는 입력 메소드( next(), nextInt() 등등.. ) 의 타입에 맞게 정규식을 검사함
- 정규식 문자열을 Pattern.compile() 이라는 메소드를 통해 Pattern 타입으로 변환함
- 반환된 Pattern 타입을 String으로 변환함
- String 은 입력 메소드의 타입에 맞게 반환함 ( nextInt() - Integer.parseInt() / nextDouble() - Double.parseDouble() 등등.. )
* 중간에 타입에 맞는 정규식을 검사하는 과정에서 많이 검사하기 때문에 Scanner의 속도가 느리다.
5. BufferedReader
Scanner 보다 좀 더 빨리 입력을 처리할 수는 방법은 없을까?
BufferedReader를 사용하면 된다. BufferReader는 버퍼를 통해 입력받은 문자를 쌓아둔 뒤 한 번에 문자열처럼 보내버린다. readLine() 메서드를 통해 String 형으로 데이터를 받을 수 있다.
기본적으로 bufferReader를 사용하는 코드는 다음과 같다.
val br = BufferedReader(InputStreamReader(System.`in`)
위 코드를 보면 다음과 같은 사실을 알 수 있다.
- 기본적으로 바이트 스트림인 InputStream을 통해 바이트 단위로 데이터를 입력 받는다.
- 입력 데이터를 char 형태로 처리하기 위해 중개자 역할인 문자스트림 InputStreamReader를 사용한다.
InputStream -> Byte Type -> InputSreamReader -> Char Type -> BufferReader -> String Type (char 의 직렬화)
byte 타입으로 읽어들이는 in을 char 타입으로 처리한 뒤 String, 즉 문자열로 저장할 수 있게 한다는 의미로 해석할 수 있다.
특징
- 버퍼가 있는 스트림이다.
- 정규식 검사를 따로 하지 않고 문자열(공백 포함)을 보내기 때문에 Scanner에 비해 성능이 좋다.
- 디폴트로 8192개의 문자를 저장할 수 있다. (변경가능)
- 기본적으로 개행이 입력되거나 flush()를 쓰거나 버퍼가 꽉 차게 되면 버퍼를 비우면서 프로그램으로 데이터를 보낸다.
하나하나 문자를 보내는 것이 아닌 한 번에 모아둔 다음 보내니 훨씬 속도가 빠르고 별다른 정규식을 검사하지 않으니 더더욱 속도는 빠를 수밖에 없다.
즉, 정리하자면 바이트 단위 [InputStream]로 문자를 입력받아 문자(character) [InputStreamReader]로 처리한 뒤 버퍼(buffer) [BufferedReader]에 담아두었다가 일정 조건이 되면 버퍼를 비우면서 데이터를 보내는 것이다.
Buffered Reader / Writer 사용 예시 (백준 15552번 - 빠른 A + B)
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.util.StringTokenizer
fun main() {
val br = BufferedReader(InputStreamReader(System.`in`))
val bw = BufferedWriter(OutputStreamWriter(System.out))
repeat(br.readLine().toInt()) {
val token = StringTokenizer(br.readLine())
val sum = (token.nextToken().toInt() + token.nextToken().toInt()).toString()
bw.write(sum + "\n")
}
bw.flush()
bw.close()
}
*StringTokenzer() 는 공백 기준으로 split 해줘서 token 이 담긴 배열을 반환.
주요 개념 : Stream , 인코딩
이제 알고리즘 풀 때 시간 단축할 수 있겠다.