Theme:

Java로 코딩 테스트를 보면 Python보다 코드가 길어지는데, 그래도 Java를 쓰는 이유가 있나요?

Java로 코딩 테스트를 보는 이유

Java는 Python보다 코드가 길지만, 몇 가지 장점이 있습니다.

  • **타입 안전성 **: 컴파일 시점에 타입 오류를 잡을 수 있습니다
  • ** 실행 속도 **: Python보다 10~50배 빠릅니다
  • ** 풍부한 자료구조 **: Collections Framework가 강력합니다
  • ** 백엔드 실무와 일치 **: Spring 기반 개발자라면 Java가 자연스럽습니다

단, 올바른 패턴을 알아야 Java의 장점을 살릴 수 있습니다.

빠른 입출력

BufferedReader + StringTokenizer

JAVA
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();

        // 한 줄 읽기
        int n = Integer.parseInt(br.readLine().trim());

        // 공백으로 구분된 여러 값 읽기
        StringTokenizer st = new StringTokenizer(br.readLine());
        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());

        // 배열 입력
        int[] arr = new int[n];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < n; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        // 출력 — StringBuilder에 모아서 한 번에
        for (int i = 0; i < n; i++) {
            sb.append(arr[i]).append('\n');
        }
        System.out.print(sb);
    }
}

Scanner vs BufferedReader 속도 비교

PLAINTEXT
입력 100만 줄:
Scanner:        약 2.5초
BufferedReader:  약 0.3초
→ 약 8배 차이!

코딩 테스트에서 시간 초과의 원인이 Scanner인 경우 가 생각보다 많습니다. 항상 BufferedReader를 사용하는 것을 습관화합니다.

입력 템플릿

JAVA
// 코딩 테스트 기본 템플릿
import java.io.*;
import java.util.*;

public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static StringBuilder sb = new StringBuilder();

    public static void main(String[] args) throws IOException {
        solve();
        System.out.print(sb);
    }

    static void solve() throws IOException {
        int n = nextInt();
        // 풀이 작성
    }

    static int nextInt() throws IOException {
        return Integer.parseInt(br.readLine().trim());
    }

    static int[] nextIntArray() throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = st.countTokens();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) arr[i] = Integer.parseInt(st.nextToken());
        return arr;
    }
}

자주 쓰는 자료구조 패턴

HashMap — 빈도 세기

JAVA
// 빈도 카운트
Map<Integer, Integer> freq = new HashMap<>();
for (int x : arr) {
    freq.merge(x, 1, Integer::sum);
    // 또는: freq.put(x, freq.getOrDefault(x, 0) + 1);
}

// 최빈값 찾기
int maxKey = Collections.max(freq.entrySet(),
    Map.Entry.comparingByValue()).getKey();

TreeMap — 정렬된 맵

JAVA
// 자동 정렬되는 맵
TreeMap<Integer, Integer> treeMap = new TreeMap<>();
treeMap.put(3, 30);
treeMap.put(1, 10);
treeMap.put(2, 20);

treeMap.firstKey();          // 1 (최솟값)
treeMap.lastKey();           // 3 (최댓값)
treeMap.floorKey(2);         // 2 (2 이하 최대)
treeMap.ceilingKey(2);       // 2 (2 이상 최소)
treeMap.subMap(1, true, 3, false); // {1=10, 2=20}

PriorityQueue — 힙

JAVA
// 최소 힙 (기본)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();

// 최대 힙
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

// 커스텀 정렬 — (비용, 노드) 쌍
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{10, 1}); // 비용 10, 노드 1
pq.offer(new int[]{5, 2});  // 비용 5, 노드 2
int[] top = pq.poll();      // [5, 2] — 비용이 작은 것 먼저

Deque — 양방향 큐

JAVA
Deque<Integer> deque = new ArrayDeque<>();
deque.offerFirst(1);  // 앞에 추가
deque.offerLast(2);   // 뒤에 추가
deque.pollFirst();    // 앞에서 제거
deque.pollLast();     // 뒤에서 제거

// 스택으로 사용
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);       // = offerFirst
stack.pop();         // = pollFirst

// 큐로 사용
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1);      // = offerLast
queue.poll();        // = pollFirst

Set — 중복 제거

JAVA
Set<Integer> set = new HashSet<>();
set.add(1);
set.contains(1);  // true

// 정렬된 Set
TreeSet<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.first();        // 1
sorted.floor(2);       // 1 (2 이하 최대)
sorted.ceiling(2);     // 3 (2 이상 최소)

정렬 패턴

JAVA
// 기본 정렬 (오름차순)
int[] arr = {3, 1, 2};
Arrays.sort(arr);

// 내림차순 — Integer[] 필요
Integer[] arr2 = {3, 1, 2};
Arrays.sort(arr2, Collections.reverseOrder());

// 2차원 배열 정렬 — 첫 번째 값 기준, 같으면 두 번째 값
int[][] intervals = {{3, 5}, {1, 3}, {1, 2}};
Arrays.sort(intervals, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
// → [[1,2], [1,3], [3,5]]

// List 정렬
List<int[]> list = new ArrayList<>();
list.sort(Comparator.comparingInt(a -> a[0]));

주의: int[]의 부분 정렬

JAVA
// 인덱스 2~5만 정렬
Arrays.sort(arr, 2, 6); // [fromIndex, toIndex)

자주 쓰는 유틸리티

배열 초기화

JAVA
int[] arr = new int[n];
Arrays.fill(arr, -1);

int[][] grid = new int[n][m];
for (int[] row : grid) Arrays.fill(row, Integer.MAX_VALUE);

최대/최소

JAVA
int max = Math.max(a, b);
int min = Math.min(a, b);

// 배열의 최대값
int arrMax = Arrays.stream(arr).max().orElse(0);
// 주의: 스트림은 느릴 수 있음. 루프가 더 빠름

String 처리

JAVA
// 문자열 → 문자 배열
char[] chars = s.toCharArray();

// 문자열 뒤집기
String reversed = new StringBuilder(s).reverse().toString();

// 부분 문자열
String sub = s.substring(2, 5); // 인덱스 2~4

// 문자열 비교
s1.equals(s2);        // 내용 비교 (== 쓰지 말 것!)
s1.compareTo(s2);     // 사전순 비교

시간/메모리 관리

int vs long

JAVA
// N이 10만이고 값이 10만이면, 합은 10^10 → int 범위 초과!
// int 범위: 약 ±2.1 × 10^9
// long 범위: 약 ±9.2 × 10^18

long sum = 0;
for (int x : arr) sum += x; // long으로 누적

재귀 깊이

JAVA
// Java의 기본 스택 크기로 깊이 약 5,000~10,000까지만 가능
// N이 10만이면 재귀 DFS가 스택 오버플로 발생

// 해결 1: 스레드 스택 크기 늘리기
public class Main {
    public static void main(String[] args) {
        new Thread(null, () -> {
            // 여기서 풀이 실행
        }, "main", 1 << 26).start(); // 64MB 스택
    }
}

// 해결 2: 재귀를 반복문으로 변환

메모리 절약

JAVA
// boolean 배열: 1바이트/원소 (BitSet은 1비트/원소)
BitSet visited = new BitSet(1_000_000);
visited.set(0);
visited.get(0); // true

// int[][] 대신 1차원 배열
int[] grid = new int[rows * cols];
// grid[r][c] → grid[r * cols + c]

유용한 코드 스니펫

그래프 인접 리스트

JAVA
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());

// 간선 추가 (양방향, 가중치)
graph.get(u).add(new int[]{v, weight});
graph.get(v).add(new int[]{u, weight});

Union-Find

JAVA
int[] parent = new int[n];
int[] rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;

int find(int x) {
    if (parent[x] != x) parent[x] = find(parent[x]);
    return parent[x];
}

void union(int a, int b) {
    a = find(a); b = find(b);
    if (a == b) return;
    if (rank[a] < rank[b]) { int t = a; a = b; b = t; }
    parent[b] = a;
    if (rank[a] == rank[b]) rank[a]++;
}

이분 탐색

JAVA
// lower_bound — target 이상인 첫 위치
int lowerBound(int[] arr, int target) {
    int lo = 0, hi = arr.length;
    while (lo < hi) {
        int mid = (lo + hi) / 2;
        if (arr[mid] < target) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}

// upper_bound — target 초과인 첫 위치
int upperBound(int[] arr, int target) {
    int lo = 0, hi = arr.length;
    while (lo < hi) {
        int mid = (lo + hi) / 2;
        if (arr[mid] <= target) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}

주의할 점

Scanner 대신 BufferedReader를 사용하지 않아 시간 초과

Scanner는 입력 파싱이 느립니다. 입력이 수만 줄 이상이면 BufferedReader + StringTokenizer 조합이 필수입니다. 출력도 StringBuilder에 모아서 한 번에 출력해야 합니다.

배열 크기를 넉넉하게 잡지 않아 ArrayIndexOutOfBoundsException

"1-indexed"로 문제를 풀 때 배열 크기를 n이 아닌 n+1로 잡아야 합니다. 이 1 차이로 런타임 에러가 빈번합니다.


정리

  • BufferedReader + StringTokenizer 는 Scanner보다 5~10배 빠릅니다. 항상 사용합니다
  • StringBuilder 에 출력을 모아서 한 번에 출력합니다
  • HashMap, TreeMap, PriorityQueue, Deque를 상황에 맞게 선택합니다
  • int 오버플로에 주의합니다. N × N이 21억을 넘으면 long을 씁니다
  • 재귀 깊이가 깊으면 스레드 스택 확장 또는 ** 반복문 변환 **을 고려합니다
  • lower_bound, Union-Find, 그래프 인접 리스트 등 ** 스니펫을 미리 준비 **해두면 실전에서 시간을 절약합니다
댓글 로딩 중...