[Java] 자바 입출력 정리 (백준)

FinDer178·2025년 5월 31일
0

코딩테스트

목록 보기
2/2

백준으로 코테 문제를 풀다보면 입출력 코드를 짜야 할 부분이 많다. 프로그래머스는 파라미터로 입력을 받고 answer로 리턴하는 형태로 입출력이 되는데 반해, 백준은 해당 입출력 받는 코드를 짜야 된다.

나 같은 경우는 원래 C++로 코테를 공부했을 때는, 입력은 cin을 출력은 그냥 cout이나 printf로 사용했기에 별 문제가 없었다. 하지만 Java로 코테 언어를 바꾸게 되면서 백준을 풀다가 입출력 부분이 계속 헷갈려 이번 기회에 블로그에 한꺼번에 정리할려고 한다.

1. Scanner

아마 자바에서 키보드로 데이터를 입력 받아 변수에 저장하는 가장 쉬운 방법은 Scanner를 사용하는 것이다. 사용 방법은 다음과 같다.

Scanner scanner = new Scanner(System.in); 

왼쪽 Scanner scanner는 Scanner 변수를 선언하는 부분이고, 오른쪽 new Scanner(System.in)은 Scanner 객체를 생성하는 부분이다. 이렇게 생성된 Scanner를 왼쪽 scanner 변수에 대입한다. 보통 백준에서는 Scanner를 다음과 같이 많이 쓴다. 먼저, Scanner를 사용하기 위해서는 다음과 같이 java.util.Scanner 클래스를 반드시 import 해야 한다. 아니면 import java.util.*을 사용해서 util 클래스 전체를 import 해도 된다.

import java.util.Scanner;

public class Main {
	public static void main(String[] args) {
    	
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt(); // 3을 입력 받았다고 가정
        int b = sc.nextInt(); // 5를 입력 받았다고 가정 
        
        System.out.println(a + b); // 8
   }
}

참 쉽다. 나는 그래서 주로 간단한 브론즈 문제나, 입력이 간단할 경우 Scanner를 주로 쓴다. 위에 nextInt()는 Scanner에서 자주 사용되는 메서드 중 하나이다. Scanner에서 자주 쓰는 메서드의 종류는 다음과 같다.

메서드명설명예시 입력 값
next()공백(스페이스, 탭 등) 단위로 문자열 한 단어 입력Hello WorldHello
nextLine()한 줄 전체 입력 (개행까지)Hello WorldHello World
nextInt()정수 입력42
nextLong()long형 정수 입력10000000000L
nextDouble()double형 실수 입력3.14
nextFloat()float형 실수 입력3.14f
nextBoolean()true 또는 false 입력 (boolean 값)true
hasNext()다음 입력 토큰 존재 여부 확인(값 확인 없이 true/false 반환)
hasNextLine()다음 줄 존재 여부 확인(값 확인 없이 true/false 반환)
hasNextInt()다음 토큰이 정수인지 확인(값 확인 없이 true/false 반환)
close()Scanner 객체를 닫고 자원을 해제(특정 입력 없음)

하지만 문제의 입출력이 많아지고 느려질 수록 이러한 Scanner 방식을 사용하면 입력 받을 때 시간초과가 발생할 수 있다. 또한, 문제마다 1, 2초 등의 시간 제한이 있기 때문에 각 테스트 케이스의 개수가 늘어날 수록 System.out.println()의 호출 횟수 또한 증가하게 된다. 그러므로 Scanner와 System.out.println 대신 BufferedReaderBufferedWriter를 사용할 수 있다.

2. BufferedReader / BufferedWriter

BufferedReader

우선 BufferedReader와 BufferedWriter를 설명하기 전에 간단하게 스트림(Stream)이라는 개념을 설명하고자 한다.

먼저, 데이터는 키보드를 통해 입력될 수도 있고, 파일 또는 프로그램으로부터 입력될 수도 있다. 반대로, 데이터는 모니터로 출력될 수도 있고, 파일에 저장되거나 다른 프로그램으로 전송될 수도 있다. 이것을 다 합쳐 데이터 입출력이라고 부른다.

자바는 입력 스트림과 출력 스트림을 통해 데이터를 입출력한다. 스트림은 단방향으로 데이터가 흐르는 것을 말하는데 아래 그림과 같이 데이터는 출발지에서 나와 도착지로 흘러들어간다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 선언
String str = br.readLine(); // 한줄 읽음 
int i = Integer.parseInt(br.readLine()); 

readLine() 메서드를 통해 한 줄 전체를 입력 받는다. 다만, 해당 메서드는 String으로 리턴 값이 고정되어 있기 때문에, 다른 타입으로 입력을 받고자 한다면 반드시 형변환이 필요하다. 그리고, 예외처리를 반드시 필요로 한다. 보통 main문에 throws IOException를 통한 예외처리를 많이 사용하는 편이다. (그리고 자바의 입출력과 관련한 java.io.* 클래스를 import 해야 한다.)
즉, BufferedReader는 Scanner와 달리 개행문자를 받아들이고 입력 받은 데이터가 String으로 고정되어 있다. 그러므로 따로 데이터를 가공해야 되는 경우가 많지만 Scanner보다 속도가 빠르다는 장점이 있다. 아래 사진을 보면 BufferedReader가 Scanner보다 빠른 것을 알 수 있다.

BufferedReader 클래스의 메서드 종류는 다음과 같다.

타입 + 메서드명설명
void close()입력 스트림을 닫고, 사용하던 자원을 해제
void mark(int, readAheadLimit)스트림의 현재 위치를 마킹
int read()한 글자만 읽어 정수형으로 반환 (e.g ,'3'을 읽어 정수형인 (int)'3' = 51로 반환)
String readLine()한 줄을 읽음
boolean ready()입력 스트림이 사용할 준비가 되었는지 확인 (1이 준비 완료)

StringTokenizer

BufferedReader를 통해 읽은 데이터는 개행문자 단위 (즉, Line 단위)로 나누어진다. 만약 이를 공백 단위로 데이터를 가공하고 싶으면 따로 작업을 해야 한다. 이럴 때 사용하는 것이 StringTokenizer나 String.split() 함수이다. StringTokenizer 클래스를 사용하기 위해서는 import java.util.StringTokenizer;를 import 해야 한다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 선언 
StringTokenizer st = new StringTokenizer(br.readLine());
int N = Integer.parseInt(st.nextToken()); // 문자열 -> 정수 변환
int M = Integer.parseInt(st.nextToken());

// String.split() 메서드
String[] arr = str.split(" ");

StringTokenizer의 nextToken() 함수를 사용하면 readLine()을 통해 입력 받은 토큰 값을 공백 단위로 구분하여 순서대로 호출한다.
String.split() 함수를 사용하면, 배열에 공백단위로 끊어 데이터를 저장하여 사용할 수 있다.

BufferedWriter

일반적으로 출력을 할 때 System.out.println(""); 을 사용하는 경우가 많다. 하지만 출력해야 하는 양이 많은 문제를 접하게 된 경우에 System.out.println("");을 사용하게 되면 성능상에 문제가 발생할 수 있다. 그래서 출력해야 될 양이 많은 경우에는, 입력과 동일하게 버퍼를 사용하는 것이 좋다.

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); // 선언
String str = "ABC";
bw.write(str); // 출력
bw.newLine(); // 줄바꿈
bw.flush(); // 버퍼에 남아있는 데이터 모두 출력
bw.close();  // 스트림 (연결된 자원) 닫음. 자원 해제 

BufferedWriter는 System.out.println("");처럼 출력과 개행을 동시해 할 수 없기 때문에, 개행을 위해서는 따로 newLine(); 또는 bw.write("\n");을 사용해야 한다. 그리고 BufferedWriter의 경우 버퍼를 점유하고 있기 때문에 반드시 사용 후, flush()를 통해 버퍼에 남아있는 데이터를 모두 출력하고, close()를 통해 버퍼를 해제해야 한다. close()를 사용하게 되면, 출력 스트림을 아예 닫아버리기 때문에 한번 출력 후, 다른 것도 출력하고자 한다면 flush()를 사용하면 된다.

타입 + 메서드명설명
void close()스트림을 닫음. 닫기 전 flush().
void flush()스트림을 비움
void newLine()개행 문자 역할
void write(char[] buf, int offset, int length)버퍼 offset 위치부터 length 크기 만큼 write
void write(int c)한 글자 쓰기
void (String s, int offset, int length)문자열에서 offset에서부터 일정 길이만큼 write

3. String, StringBuffer, StringBuilder

또, 자바로 알고리즘 문제를 풀다보면 StringBuilder를 사용하는 경우가 많다. 그러므로, String과 StrigBuffer, StringBuilder와의 차이점을 중심으로 이를 설명하고자 한다.

먼저, String 클래스 관련 메서드를 보여주고자 한다. 종류는 다음과 같다.

String str = "apple";

// 길이 반환
str.length(); // 5

// 빈 문자열 체크
str.isEmpty(); // false

// 문자 찾기
str.charAt(0); // 'a'
str.indexOf("a"); // 0
str.lastIndexOf("p"); // 2

// 문자 자르기
str.substring(1, 3); // "pp"
str.substring(3); // "le"

// 문자 치환 (바꾸기)
// replace([기존문자], [바꿀문자])
str.replace('p', 'e'); // "aeele"

// replaceAll([정규식], [바꿀문자])
str.replaceAll(".", "/"); // "/////"

// replaceFirst([기존문자], [바꿀문자])
str.replaceFirst("p", "e"); // "aeple"

// 문자 동일 여부 판단
str.equals("apple"); // true

// 문자 비교
// str과 "applp"가 다를 때 compareTo 결과
str.compareTo("applp"); // -1

// 문자 포함 여부 판단
str.contains("app"); // true

// 문자열 분리
str.split(" "); // {"apple"} (공백 없으므로 전체 문자열 반환)
str.split(""); // {"a", "p", "p", "l", "e"}

// 문자 앞뒤 공백 제거
str.trim(); // "apple" (앞뒤 공백 없으므로 그대로)

// 문자 <-> 숫자 변환
Integer.parseInt("100"); // 100
Integer.toString(100);  // "100"

자바에서 문자열이란 문자들을 배열의 형태로 구성한 이뮤터블(immutable) 객체이다. 이뮤터블 객체는 값을 변경할 수 없는 객체로 시간 복잡도 관점에서 주의해야 할 필요가 있다.

String string = "He";
string += "llo";
System.out.println(string); // "Hello"
  1. 문자열을 "He"로 초기화. 변수 string이 문자열 "He"를 참조
  2. 이어서 string이 참조하는 "He"와 "llo"를 합쳐 새로운 문자열을 만들고 string은 새로운 문자열인 "Hello"를 참조한다.

즉, 자바에서 String 객체는 값을 변경할 수 없으므로 문자열을 변경할 때마다 기존 문자열을 끊고 새 문자열을 참조해야 한다(새로운 값을 할당). 그러므로 시간 복잡도 관점에서 연산의 비용이 클 수 밖에 없다.

그러므로 이러한 문제를 해결하기 위해 나온 것이 StringBuffer 클래스와 StringBuilder 클래스이다. 이 두 클래스는 뮤터블하므로 값을 변경할 때 시간 복잡도 관점에서 더 효율적이다.

String의 값을 변경하는 연산이 많을 때 StringBuffer와 StringBuilder 클래스를 사용할 수 있고, 두 클래스의 차이는 멀티 스레드 환경에서 Thread-Safe 여부로 나뉜다. Thread-Safe가 없는 StringBuilder 클래스가 속도 측면에서 미세하지만 더 빠르므로 StringBuilder를 사용하면 된다.

즉, 코딩테스트에서 문자열의 값을 변경하는 연산이 많을 때 시간 초과가 발생하지 않도록 StringBuilder를 사용하면 좋다. StringBuilder 관련 메서드는 아래와 같다.

StringBuilder sb = new StringBuilder(); // StringBuilder 객체 생성

// 문자열 추가
sb.append("apple");

// 특정 인덱스에 문자 삽입
sb.insert(2, "oo");

// 문자열 삭제
sb.delete(0, 2); // "oople"

// 특정 인덱스의 문자 삭제
sb.deleteCharAt(2); // "oole"

// 특정 인덱스의 문자를 변경
sb.setCharAt(1, 'p');

// 문자열 뒤집기
sb.reverse();

// 문자열 절대길이 줄이기 
sb.setLength(2);

// 문자열 절대길이 늘이기
sb.setLength(4);

이렇게 백준에서 많이 쓰이는 자바 입출력 형식과 문자열 등을 알아봤다. 사실 대부분의 기업 코딩 테스트는 프로그래머스로 응시해서 입출력 같은 경우는 몰라도 된다. 하지만 백준과 같이 입출력이 많은 경우에는 반드시 이를 필수적으로 알아야 한다. 이전에는 정말 헷갈렸지만 이렇게 한번 제대로 정리하니깐 복습하기가 더 수월해진 것 같다!

참고

profile
낙관적 개발자

0개의 댓글