JAVA (2) 제어문과 배열

통밀·2026년 7월 20일

JAVA

목록 보기
2/10

Java : 조건문과 반복문

들어가며

Java 프로그램은 기본적으로 main() 메서드의 시작 중괄호부터 끝 중괄호까지 위에서 아래로 한 줄씩 실행된다.

public class Main {

    public static void main(String[] args) {
        System.out.println("첫 번째 실행");
        System.out.println("두 번째 실행");
        System.out.println("세 번째 실행");
    }
}

하지만 실제 프로그램은 언제나 모든 코드를 같은 순서로 실행하지 않는다.

사용자의 나이에 따라 다른 문장을 출력하거나, 로그인 여부에 따라 접근을 허용해야 할 수 있다. 상품 목록을 모두 출력하려면 같은 작업을 여러 번 반복해야 한다.

이처럼 프로그램의 실행 순서를 조건에 따라 나누거나 특정 코드를 반복하도록 만드는 문법을 제어문이라고 한다.

Java의 제어문은 크게 다음과 같이 구분할 수 있다.

제어문
├─ 조건문
│  ├─ if
│  └─ switch
│
└─ 반복문
   ├─ for
   ├─ while
   └─ do-while

반복문이나 switch의 실행을 제어하기 위해 break와 continue도 함께 사용한다.


1. 조건문이란?

조건문은 조건식의 결과에 따라 서로 다른 코드를 실행한다.

Java의 조건식에는 반드시 true 또는 false를 결과로 만드는 표현식이나 boolean 타입 변수가 와야 한다.

int age = 20;

boolean isAdult = age >= 19;

if (isAdult) {
    System.out.println("성인입니다.");
}

Java에서는 숫자 0이나 1을 조건식으로 사용할 수 없다.

int number = 1;

// if (number) {
//     컴파일 오류
// }

C나 JavaScript와 달리 Java의 if 조건에는 반드시 boolean 결과가 필요하다.


2. if문

if문은 조건식이 true일 때 중괄호 내부의 실행문을 실행한다.

if (조건식) {
    조건식이 true일 때 실행할 코드
}

예를 들어 사용자의 점수가 60점 이상일 때 합격을 출력할 수 있다.

public class Main {

    public static void main(String[] args) {
        int score = 75;

        if (score >= 60) {
            System.out.println("합격입니다.");
        }

        System.out.println("프로그램을 종료합니다.");
    }
}

score >= 60이 true이므로 "합격입니다."가 출력된다.


중괄호는 생략하지 않는 것이 좋다

실행문이 한 줄뿐이라면 중괄호를 생략할 수 있다.

if (score >= 60)
    System.out.println("합격입니다.");

그러나 코드가 추가될 때 실수하기 쉽기 때문에 중괄호를 항상 작성하는 것이 안전하다.

if (score >= 60) {
    System.out.println("합격입니다.");
}

다음 코드는 들여쓰기만 보면 두 문장이 모두 조건에 포함된 것 같지만, 실제로는 첫 번째 문장만 if문의 영향을 받는다.

if (score >= 60)
    System.out.println("합격입니다.");

    System.out.println("축하합니다.");

"축하합니다."는 조건과 관계없이 항상 실행된다.

들여쓰기는 사람이 코드를 읽기 쉽게 만들 뿐, Java의 실행 범위를 결정하지 않는다. 실행 범위는 중괄호가 결정한다.


3. if-else문

조건식이 true인 경우와 false인 경우에 각각 다른 코드를 실행하려면 else를 사용한다.

if (조건식) {
    조건식이 true일 때 실행
} else {
    조건식이 false일 때 실행
}
public class Main {

    public static void main(String[] args) {
        int score = 55;

        if (score >= 60) {
            System.out.println("합격입니다.");
        } else {
            System.out.println("불합격입니다.");
        }
    }
}

두 블록 중 하나만 실행된다.


4. if-else if-else문

조건이 여러 개라면 else if를 사용한다.

if (조건식1) {
    실행문 A
} else if (조건식2) {
    실행문 B
} else {
    실행문 C
}

점수에 따라 등급을 출력하는 프로그램을 만들어보자.

public class Main {

    public static void main(String[] args) {
        int score = 85;
        char grade;

        if (score >= 90) {
            grade = 'A';
        } else if (score >= 80) {
            grade = 'B';
        } else if (score >= 70) {
            grade = 'C';
        } else if (score >= 60) {
            grade = 'D';
        } else {
            grade = 'F';
        }

        System.out.println("학점: " + grade);
    }
}

조건은 위에서부터 순서대로 검사하며, 처음으로 true가 된 블록만 실행한다.

따라서 조건의 순서가 매우 중요하다.


조건 순서를 잘못 작성한 경우

int score = 95;

if (score >= 60) {
    System.out.println("D 이상");
} else if (score >= 90) {
    System.out.println("A");
}

95는 첫 번째 조건인 score >= 60을 만족하므로 두 번째 조건은 검사하지 않는다.

범위를 판별하는 조건은 일반적으로 큰 값부터 작은 값 순서로 작성한다.

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
}

5. 중첩 if문

if문 내부에 또 다른 if문을 작성할 수 있다.

public class Main {

    public static void main(String[] args) {
        int age = 20;
        boolean hasTicket = true;

        if (age >= 19) {
            if (hasTicket) {
                System.out.println("입장할 수 있습니다.");
            } else {
                System.out.println("티켓이 필요합니다.");
            }
        } else {
            System.out.println("성인만 입장할 수 있습니다.");
        }
    }
}

다만 중첩이 너무 깊어지면 코드를 이해하기 어려워진다.

다음과 같이 논리 연산자를 활용해 조건을 합칠 수 있다.

if (age >= 19 && hasTicket) {
    System.out.println("입장할 수 있습니다.");
}

6. switch문

switch문은 하나의 변수나 표현식의 결과를 여러 값과 비교할 때 사용한다.

switch (변수 또는 표현식) {
    case 값1:
        실행문 A;
        break;

    case 값2:
        실행문 B;
        break;

    default:
        실행문 C;
}

요일 번호를 입력받아 요일을 출력하는 예제를 살펴보자.

public class Main {

    public static void main(String[] args) {
        int day = 3;

        switch (day) {
            case 1:
                System.out.println("월요일");
                break;

            case 2:
                System.out.println("화요일");
                break;

            case 3:
                System.out.println("수요일");
                break;

            case 4:
                System.out.println("목요일");
                break;

            case 5:
                System.out.println("금요일");
                break;

            default:
                System.out.println("주말입니다.");
        }
    }
}

switch문의 break

기존 형태의 switch문에서 break는 현재 switch문을 종료한다.

break를 작성하지 않으면 일치하는 case 이후의 코드까지 계속 실행된다. 이를 fall-through라고 한다.

public class Main {

    public static void main(String[] args) {
        int number = 1;

        switch (number) {
            case 1:
                System.out.println("첫 번째");

            case 2:
                System.out.println("두 번째");

            case 3:
                System.out.println("세 번째");
        }
    }
}

실행 결과는 다음과 같다.

첫 번째
두 번째
세 번째

의도적으로 여러 case를 함께 처리하는 경우가 아니라면 break를 빠뜨리지 않아야 한다.


여러 case를 함께 처리하기

기존 switch문에서는 여러 case를 이어 작성할 수 있다.

public class Main {

    public static void main(String[] args) {
        int month = 2;

        switch (month) {
            case 12:
            case 1:
            case 2:
                System.out.println("겨울");
                break;

            case 3:
            case 4:
            case 5:
                System.out.println("봄");
                break;

            case 6:
            case 7:
            case 8:
                System.out.println("여름");
                break;

            default:
                System.out.println("가을");
        }
    }
}

7. switch 표현식

최근 Java에서는 화살표 ->를 사용하는 switch 표현식을 사용할 수 있다.

switch 표현식은 다음과 같은 장점이 있다.

  • switch 자체가 값을 반환할 수 있다.
  • 자동으로 해당 case만 실행하므로 break가 필요 없다.
  • 여러 조건을 쉼표로 묶을 수 있다.
  • 코드가 간결해진다.
public class Main {

    public static void main(String[] args) {
        int score = 85;

        char grade = switch (score / 10) {
            case 10, 9 -> 'A';
            case 8 -> 'B';
            case 7 -> 'C';
            case 6 -> 'D';
            default -> 'F';
        };

        System.out.println("학점: " + grade);
    }
}

화살표 형태의 switch는 Java 14부터 정식 문법으로 사용할 수 있다.


switch 표현식에서 여러 문장 실행하기

한 case에서 여러 문장을 실행하고 값을 반환하려면 중괄호와 yield를 사용한다.

public class Main {

    public static void main(String[] args) {
        String membership = "GOLD";

        int discountRate = switch (membership) {
            case "VIP" -> {
                System.out.println("VIP 회원입니다.");
                yield 20;
            }

            case "GOLD" -> {
                System.out.println("GOLD 회원입니다.");
                yield 10;
            }

            default -> {
                System.out.println("일반 회원입니다.");
                yield 0;
            }
        };

        System.out.println("할인율: " + discountRate + "%");
    }
}

yield는 switch 표현식의 결과값을 반환한다.

메서드에서 사용하는 return과 비슷해 보이지만, yield는 현재 switch 표현식의 값을 결정한다.


8. if문과 switch문의 차이

구분if문switch문
주요 용도범위와 복합 조건 검사하나의 값에 따른 분기
조건 표현비교·논리 연산 가능일치하는 값 중심
범위 조건적합일반적으로 부적합
복합 조건적합상대적으로 제한적
가독성조건이 적으면 좋음경우의 수가 많을 때 좋음

다음과 같은 범위 조건에는 if가 적절하다.

if (score >= 90) {
    System.out.println("A");
}

다음과 같이 값이 명확히 나뉘는 경우에는 switch가 적절하다.

switch (menuNumber) {
    case 1:
        System.out.println("회원 조회");
        break;

    case 2:
        System.out.println("회원 등록");
        break;
}

9. 반복문이란?

반복문은 조건을 만족하는 동안 특정 코드를 반복 실행한다.

Java의 대표적인 반복문은 다음과 같다.

  • for
  • while
  • do-while

일반적으로 반복 횟수를 알고 있다면 for, 특정 조건이 만족될 때까지 반복한다면 while을 사용한다.


10. for문

for문은 초기화식, 조건식, 증감식으로 구성된다.

for (초기화식; 조건식; 증감식) {
    반복할 실행문
}

실행 순서는 다음과 같다.

1. 초기화식
2. 조건식 검사
3. 실행문 실행
4. 증감식 실행
5. 다시 조건식 검사

1부터 5까지 출력하는 예제를 살펴보자.

public class Main {

    public static void main(String[] args) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
        }
    }
}

int i = 1은 반복이 시작될 때 한 번만 실행된다.

i <= 5가 true인 동안 반복하며, 한 번 반복할 때마다 i++가 실행된다.


1부터 100까지 합 구하기

public class Main {

    public static void main(String[] args) {
        int sum = 0;

        for (int i = 1; i <= 100; i++) {
            sum += i;
        }

        System.out.println("합계: " + sum);
    }
}

for문의 변수 범위

for문의 초기화식에서 선언한 변수는 for문 내부에서만 사용할 수 있다.

for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

// System.out.println(i);
// 컴파일 오류

반복이 끝난 뒤에도 변수가 필요하다면 반복문 밖에서 선언해야 한다.

int i;

for (i = 0; i < 5; i++) {
    System.out.println(i);
}

System.out.println("반복 종료 후 i: " + i);

11. 중첩 for문

반복문 내부에 또 다른 반복문을 작성할 수 있다.

구구단을 출력하는 예제를 살펴보자.

public class Main {

    public static void main(String[] args) {
        for (int dan = 2; dan <= 9; dan++) {
            System.out.println("[" + dan + "단]");

            for (int number = 1; number <= 9; number++) {
                System.out.printf(
                        "%d × %d = %d%n",
                        dan,
                        number,
                        dan * number
                );
            }

            System.out.println();
        }
    }
}

바깥쪽 반복문이 한 번 실행될 때마다 안쪽 반복문은 처음부터 끝까지 실행된다.


12. while문

while문은 조건식이 true인 동안 반복한다.

while (조건식) {
    실행문
}
public class Main {

    public static void main(String[] args) {
        int number = 1;

        while (number <= 5) {
            System.out.println(number);
            number++;
        }
    }
}

while문에서는 증감식을 직접 작성해야 한다.

number++;

증감이나 조건을 변경하는 코드를 빠뜨리면 무한 반복이 발생할 수 있다.

int number = 1;

while (number <= 5) {
    System.out.println(number);

    // number가 변하지 않으므로 무한 반복
}

while문은 언제 사용할까?

반복 횟수를 정확히 모르고 특정 조건이 만족될 때까지 반복해야 할 때 유용하다.

사용자가 0을 입력할 때까지 숫자를 계속 더하는 예제다.

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int sum = 0;

        while (true) {
            System.out.print("숫자를 입력하세요. 종료는 0: ");
            int number = Integer.parseInt(scanner.nextLine());

            if (number == 0) {
                break;
            }

            sum += number;
        }

        System.out.println("입력한 숫자의 합: " + sum);

        scanner.close();
    }
}

13. do-while문

do-while문은 실행문을 먼저 한 번 실행한 뒤 조건을 검사한다.

do {
    실행문
} while (조건식);

조건이 처음부터 false여도 실행문이 최소 한 번 실행된다.

public class Main {

    public static void main(String[] args) {
        int number = 10;

        do {
            System.out.println(number);
            number++;
        } while (number < 5);
    }
}

조건 number < 5는 처음부터 false지만 10이 한 번 출력된다.


사용자 입력 검증 예제

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int menu;

        do {
            System.out.println("1. 조회");
            System.out.println("2. 등록");
            System.out.println("0. 종료");
            System.out.print("메뉴 선택: ");

            menu = Integer.parseInt(scanner.nextLine());

            if (menu < 0 || menu > 2) {
                System.out.println("올바른 메뉴를 입력하세요.");
            }
        } while (menu < 0 || menu > 2);

        System.out.println("선택한 메뉴: " + menu);

        scanner.close();
    }
}

입력을 반드시 한 번 받아야 하므로 do-while문이 잘 어울린다.


14. for, while, do-while 비교

반복문특징적합한 상황
for초기화·조건·증감이 한곳에 있음반복 횟수를 알고 있을 때
while조건을 먼저 검사종료 시점을 조건으로 판단할 때
do-while실행 후 조건 검사최소 한 번 실행해야 할 때

동일한 작업을 어떤 반복문으로도 구현할 수 있지만, 상황에 가장 자연스러운 문법을 선택하는 것이 좋다.


15. break문

break는 반복문이나 switch문의 실행을 즉시 종료한다.

public class Main {

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            if (i == 5) {
                break;
            }

            System.out.println(i);
        }
    }
}

실행 결과는 다음과 같다.

1
2
3
4

i가 5가 되는 순간 반복문이 종료된다.


16. continue문

continue는 현재 반복에서 남은 코드를 건너뛰고 다음 반복으로 이동한다.

public class Main {

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            if (i % 2 == 0) {
                continue;
            }

            System.out.println(i);
        }
    }
}

짝수일 때 출력을 건너뛰므로 홀수만 출력된다.

1
3
5
7
9

break와 continue 차이

break
→ 반복문 전체를 종료

continue
→ 현재 반복만 건너뛰고 다음 반복 진행
구분breakcontinue
현재 반복중단중단
다음 반복실행하지 않음계속 실행
반복문 전체종료유지

17. 라벨을 이용한 반복문 종료

반복문이 중첩된 상태에서 일반적인 break를 사용하면 가장 가까운 반복문 하나만 종료한다.

public class Main {

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                if (j == 2) {
                    break;
                }

                System.out.println("i = " + i + ", j = " + j);
            }
        }
    }
}

바깥쪽 반복문까지 한 번에 종료하려면 라벨을 사용할 수 있다.

public class Main {

    public static void main(String[] args) {
        outer:
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                if (i == 2 && j == 2) {
                    break outer;
                }

                System.out.println("i = " + i + ", j = " + j);
            }
        }

        System.out.println("반복 종료");
    }
}

라벨은 메모에 적힌 다음 형태처럼 작성한다.

outer:
for (...) {
    for (...) {
        break outer;
    }
}

라벨을 과도하게 사용하면 코드 흐름을 추적하기 어려워질 수 있다. 가능하면 메서드 분리나 조건 재구성을 먼저 고려하는 것이 좋다.


18. 종합 예제: 간단한 메뉴 프로그램

조건문과 반복문을 함께 활용해 간단한 회원 관리 메뉴를 만들어보자.

import java.util.Scanner;

public class MemberMenu {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println();
            System.out.println("===== 회원 관리 =====");
            System.out.println("1. 회원 조회");
            System.out.println("2. 회원 등록");
            System.out.println("3. 회원 삭제");
            System.out.println("0. 종료");
            System.out.print("메뉴를 선택하세요: ");

            int menu = Integer.parseInt(scanner.nextLine());

            switch (menu) {
                case 1:
                    System.out.println("회원 정보를 조회합니다.");
                    break;

                case 2:
                    System.out.println("새 회원을 등록합니다.");
                    break;

                case 3:
                    System.out.println("회원을 삭제합니다.");
                    break;

                case 0:
                    System.out.println("프로그램을 종료합니다.");
                    scanner.close();
                    return;

                default:
                    System.out.println("올바른 메뉴 번호를 입력하세요.");
            }
        }
    }
}

이 예제에서는 다음 개념이 사용되었다.

  • while (true)를 이용한 반복 메뉴
  • switch를 이용한 메뉴 분기
  • return을 이용한 프로그램 종료
  • Scanner를 이용한 입력
  • Integer.parseInt()를 이용한 문자열 변환

헷갈리기 쉬운 개념

if문의 조건은 boolean이어야 한다

int number = 1;

// if (number) {
//     사용할 수 없음
// }

if (number == 1) {
    System.out.println("1입니다.");
}

=과 ==는 다르다

int number = 10;        // 대입
boolean result = number == 10; // 비교

switch문의 break와 반복문의 break

두 경우 모두 현재 제어 구조를 종료하지만 종료 대상이 다르다.

  • switch 안의 break: 해당 switch 종료
  • 반복문 안의 break: 가장 가까운 반복문 종료

while과 do-while

while (조건식) {
    실행문
}

while은 조건을 먼저 검사한다.

do {
    실행문
} while (조건식);

do-while은 실행문을 먼저 실행하므로 최소 한 번 실행된다.


실무에서 알아두면 좋은 내용

조건문이 지나치게 중첩되면 읽기 어렵기 때문에, 실패 조건을 먼저 처리하고 메서드를 종료하는 가드 클로즈 방식을 활용할 수 있다.

public static void registerUser(String name, int age) {
    if (name == null || name.isBlank()) {
        System.out.println("이름이 올바르지 않습니다.");
        return;
    }

    if (age < 0) {
        System.out.println("나이가 올바르지 않습니다.");
        return;
    }

    System.out.println("회원을 등록합니다.");
}

다음과 같이 여러 단계로 중첩하는 것보다 읽기 쉽다.

if (name != null) {
    if (!name.isBlank()) {
        if (age >= 0) {
            System.out.println("회원을 등록합니다.");
        }
    }
}

반복문 내부에서 컬렉션을 순회할 때 요소를 직접 삭제하면 오류가 발생할 수 있으므로, 이후 컬렉션을 학습할 때 Iterator나 removeIf() 같은 방법도 함께 알아두는 것이 좋다.


오늘 배운 내용 정리

  • Java 프로그램은 기본적으로 위에서 아래로 실행된다.
  • 제어문을 사용하면 실행 흐름을 조건에 따라 나누거나 반복할 수 있다.
  • 조건문에는 if문과 switch문이 있다.
  • if문의 조건식은 반드시 boolean 결과를 만들어야 한다.
  • else if 조건은 위에서부터 순서대로 검사한다.
  • 하나의 값을 여러 경우와 비교할 때는 switch문이 유용하다.
  • 화살표를 사용하는 switch 표현식은 값을 반환하며 break가 필요 없다.
  • 여러 문장을 실행하는 switch 표현식에서는 yield로 값을 반환한다.
  • 반복 횟수를 알고 있다면 for문이 적합하다.
  • 조건에 따라 반복한다면 while문이 적합하다.
  • do-while문은 조건과 관계없이 최소 한 번 실행된다.
  • break는 반복문 전체를 종료한다.
  • continue는 현재 반복만 건너뛰고 다음 반복을 진행한다.
  • 라벨을 이용하면 중첩 반복문 바깥까지 종료할 수 있지만 남용하지 않는 것이 좋다.

배열

들어가며

학생 30명의 점수를 저장해야 한다고 가정해보자.

배열을 사용하지 않는다면 다음과 같이 변수를 여러 개 선언해야 한다.

int score1 = 90;
int score2 = 80;
int score3 = 100;

데이터가 30개, 100개, 1,000개로 늘어나면 각각의 변수를 직접 선언하고 처리하는 방식은 현실적이지 않다.

이 문제를 해결하기 위해 같은 타입의 데이터를 하나로 묶어 관리하는 자료구조가 배열이다.

int[] scores = {90, 80, 100};

하지만 배열을 제대로 이해하려면 먼저 Java의 기본 타입과 참조 타입, 그리고 Stack과 Heap 메모리의 관계를 알아야 한다.


1. 기본 타입과 참조 타입

Java의 데이터 타입은 기본 타입과 참조 타입으로 구분된다.

기본 타입

기본 타입 변수는 정수, 실수, 문자, 논리값을 직접 저장한다.

int age = 20;
double height = 175.5;
char grade = 'A';
boolean isStudent = true;

Java의 기본 타입은 다음 8개다.

byte
short
int
long
float
double
char
boolean

참조 타입

참조 타입 변수는 객체 자체가 아니라 객체를 참조하는 값을 저장한다.

대표적인 참조 타입은 다음과 같다.

  • 배열
  • 열거 타입
  • 클래스
  • 인터페이스
  • String
String name = "김자바";
int[] scores = {90, 80, 100};

개념적으로 다음과 같이 이해할 수 있다.

Stack 영역              Heap 영역

name     ───────────→   "김자바"

scores   ───────────→   [90][80][100]

참조 타입 변수에는 객체를 찾아갈 수 있는 참조값이 저장된다.

실제 JVM 구현에서 참조값이 반드시 사람이 볼 수 있는 메모리 주소 그 자체라고 단정할 수는 없다. 입문 단계에서는 객체의 위치를 찾아가기 위한 값이라고 이해하면 된다.


2. Java의 주요 메모리 영역

Java 프로그램이 실행되면 JVM은 목적에 따라 메모리를 여러 영역으로 나누어 사용한다.

입문 단계에서 중요한 영역은 다음 세 가지다.

JVM 메모리
├─ Method Area
├─ Heap
└─ JVM Stack

Method Area

메서드 영역에는 JVM이 읽은 클래스 관련 정보가 저장된다.

대표적으로 다음과 같은 정보가 포함된다.

  • 클래스의 바이트 코드
  • 메서드 코드
  • 필드 정보
  • 생성자 정보
  • 정적 필드
  • 런타임 상수 풀

정확한 내부 구현은 JVM 버전과 구현체에 따라 달라질 수 있지만, 클래스 단위 정보가 관리되는 영역이라고 이해할 수 있다.


Heap 영역

Heap에는 new 연산자 등을 통해 생성된 객체와 배열이 저장된다.

int[] scores = new int[3];
String name = new String("Java");

위 코드에서 배열과 String 객체는 Heap에 생성된다.

더 이상 어떤 변수에서도 참조하지 않는 객체는 Garbage Collector의 정리 대상이 된다.


JVM Stack 영역

메서드가 호출될 때마다 Stack에 프레임이 생성된다.

프레임에는 해당 메서드에서 사용하는 로컬 변수와 연산 중간값 등이 저장된다.

public static void main(String[] args) {
    int age = 20;
    int[] scores = new int[3];
}

개념적으로 다음과 같이 표현할 수 있다.

JVM Stack                        Heap

main() 프레임
┌────────────────┐
│ age = 20       │
│ scores = 참조값 │ ───────────→ [0][0][0]
└────────────────┘

메서드 실행이 끝나면 해당 프레임도 Stack에서 제거된다.


3. 참조 타입의 == 비교

기본 타입에 ==를 사용하면 값을 비교한다.

int first = 10;
int second = 10;

System.out.println(first == second);

참조 타입에 ==를 사용하면 두 변수가 같은 객체를 참조하는지 비교한다.

int[] first = {1, 2, 3};
int[] second = first;
int[] third = {1, 2, 3};

System.out.println(first == second);
System.out.println(first == third);

실행 결과는 다음과 같다.

true
false

first와 second는 같은 배열을 참조하지만, third는 내용이 같더라도 별도로 생성된 배열을 참조한다.

first  ──┐
         ├──→ [1][2][3]
second ──┘

third ──────→ [1][2][3]

4. null과 NullPointerException

참조 타입 변수는 객체를 참조하지 않는 상태를 나타내기 위해 null을 가질 수 있다.

String name = null;
int[] scores = null;

변수가 null인지 == 또는 !=로 확인할 수 있다.

if (name == null) {
    System.out.println("이름이 없습니다.");
}

NullPointerException

null 상태인 참조 변수를 통해 객체의 필드, 메서드 또는 배열 요소에 접근하면 NullPointerException이 발생한다.

public class Main {

    public static void main(String[] args) {
        String text = null;

        System.out.println(text.length());
    }
}

text가 참조하는 String 객체가 없기 때문에 length()를 호출할 수 없다.

배열도 마찬가지다.

int[] numbers = null;

numbers[0] = 10;

배열 객체가 생성되지 않았기 때문에 요소에 접근할 수 없다.

다음처럼 객체가 존재하는지 먼저 확인해야 한다.

if (text != null) {
    System.out.println(text.length());
}

5. String 객체와 문자열 비교

문자열 리터럴이 동일하면 Java는 문자열 상수 풀에 있는 같은 객체를 재사용할 수 있다.

String first = "Java";
String second = "Java";

System.out.println(first == second);

이 경우 true가 나올 가능성이 아니라, 동일한 문자열 리터럴은 일반적으로 같은 문자열 풀 객체를 참조하므로 true가 나온다.

반면 new 연산자로 생성하면 새로운 객체가 만들어진다.

String first = new String("Java");
String second = new String("Java");

System.out.println(first == second);

실행 결과는 false다.

문자열의 내용은 equals()로 비교해야 한다.

System.out.println(first.equals(second));

결과는 true다.

문자열 비교에 ==를 사용하면 생성 방식에 따라 결과가 달라질 수 있으므로 내용 비교에는 항상 equals()를 사용해야 한다.


6. 배열이란?

배열은 같은 타입의 여러 데이터를 하나의 연속된 구조로 관리하는 자료구조다.

각 데이터에는 0부터 시작하는 인덱스가 부여된다.

scores

index    0     1      2
       ┌────┬────┬─────┐
value  │ 90 │ 80 │ 100 │
       └────┴────┴─────┘

Java 배열의 특징은 다음과 같다.

  • 같은 타입의 값만 저장할 수 있다.
  • 인덱스는 0부터 시작한다.
  • 생성된 이후 길이를 변경할 수 없다.
  • 배열 자체는 참조 타입이다.
  • 배열의 요소는 기본 타입 값이나 객체 참조값을 저장한다.

7. 배열 변수 선언

배열 변수는 다음 두 방식으로 선언할 수 있다.

int[] scores;
int scores2[];

두 문법 모두 가능하지만 Java에서는 타입 뒤에 대괄호를 작성하는 첫 번째 방식을 권장한다.

int[] scores;

이 방식은 변수의 타입이 int 배열이라는 점을 더 분명히 보여준다.

int number;       // int 타입
int[] numbers;    // int[] 타입

배열 변수를 선언하는 것만으로 배열 객체가 생성되는 것은 아니다.

int[] numbers = null;

이 상태에서는 사용할 배열이 존재하지 않는다.


8. 값 목록으로 배열 생성하기

배열을 선언하면서 값 목록을 제공할 수 있다.

int[] scores = {90, 80, 100};
String[] names = {"김자바", "이자바", "박자바"};

배열 길이는 입력한 값의 개수에 따라 결정된다.

System.out.println(scores.length);

결과는 3이다.


선언과 값 목록을 분리할 수 없는 이유

다음 코드는 컴파일 오류가 발생한다.

int[] scores;

// scores = {90, 80, 100};
// 컴파일 오류

중괄호만 사용하는 값 목록 방식은 배열 변수를 선언하는 문장에서만 사용할 수 있다.

나중에 값을 결정해야 한다면 new를 명시해야 한다.

int[] scores;

scores = new int[]{90, 80, 100};

9. new 연산자로 배열 생성하기

배열의 길이만 알고 있고 값은 나중에 저장하려면 new 연산자를 사용한다.

int[] scores = new int[3];

기본 형식은 다음과 같다.

타입[] 변수 = new 타입[길이];

이미 변수를 선언했다면 생성문을 분리할 수 있다.

int[] scores;

scores = new int[3];

10. 배열의 기본값

new로 배열을 생성하면 각 요소는 타입에 맞는 기본값으로 자동 초기화된다.

요소 타입기본값
byte, short, int, long0
float, double0.0
char'\u0000'
booleanfalse
참조 타입null
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] numbers = new int[3];
        boolean[] results = new boolean[3];
        String[] names = new String[3];

        System.out.println(Arrays.toString(numbers));
        System.out.println(Arrays.toString(results));
        System.out.println(Arrays.toString(names));
    }
}

실행 결과는 다음과 같다.

[0, 0, 0]
[false, false, false]
[null, null, null]

11. 배열 요소에 접근하기

배열 요소는 인덱스를 이용해 읽거나 변경한다.

int[] scores = new int[3];

scores[0] = 90;
scores[1] = 80;
scores[2] = 100;

System.out.println(scores[0]);

배열의 첫 번째 요소는 인덱스 0이다.

첫 번째 요소 → 0
두 번째 요소 → 1
세 번째 요소 → 2

ArrayIndexOutOfBoundsException

배열의 범위를 벗어난 인덱스에 접근하면 ArrayIndexOutOfBoundsException이 발생한다.

int[] scores = new int[3];

scores[3] = 100;

길이가 3인 배열에서 사용할 수 있는 인덱스는 0, 1, 2다.

마지막 인덱스는 다음과 같이 구할 수 있다.

int lastIndex = scores.length - 1;

12. 배열의 length

배열의 길이는 length 필드로 확인한다.

int[] scores = {90, 80, 100};

System.out.println(scores.length);

String의 길이는 메서드이므로 괄호를 사용하지만, 배열의 length는 필드이므로 괄호가 없다.

String text = "Java";

System.out.println(text.length());
System.out.println(scores.length);
  • 문자열: length()
  • 배열: length

13. for문으로 배열 순회하기

배열의 모든 요소를 처리할 때는 for문과 length를 함께 사용한다.

public class Main {

    public static void main(String[] args) {
        int[] scores = {90, 80, 100};

        for (int i = 0; i < scores.length; i++) {
            System.out.printf(
                    "%d번째 점수: %d%n",
                    i,
                    scores[i]
            );
        }
    }
}

조건식에는 배열의 마지막 인덱스가 아니라 길이를 사용한다.

i < scores.length

<=를 사용하면 범위를 벗어난 인덱스에 접근하게 된다.

// i <= scores.length 사용 금지

배열의 합계와 평균

public class Main {

    public static void main(String[] args) {
        int[] scores = {90, 80, 100, 70, 85};

        int sum = 0;

        for (int i = 0; i < scores.length; i++) {
            sum += scores[i];
        }

        double average = (double) sum / scores.length;

        System.out.println("합계: " + sum);
        System.out.println("평균: " + average);
    }
}

배열 길이를 5처럼 직접 작성하지 않고 scores.length를 사용하면 배열 크기가 변경되어도 코드를 수정할 필요가 없다.


14. 향상된 for문

향상된 for문은 배열이나 컬렉션의 요소를 처음부터 끝까지 순서대로 읽을 때 사용한다.

for (요소타입 변수 : 배열) {
    실행문
}
public class Main {

    public static void main(String[] args) {
        int[] scores = {90, 80, 100};

        for (int score : scores) {
            System.out.println(score);
        }
    }
}

각 반복마다 배열 요소가 score 변수에 대입된다.


향상된 for문의 한계

향상된 for문에서는 현재 요소의 인덱스를 직접 알 수 없다.

for (int score : scores) {
    System.out.println(score);
}

인덱스가 필요하다면 일반 for문을 사용한다.

for (int i = 0; i < scores.length; i++) {
    System.out.println(i + ": " + scores[i]);
}

또한 기본 타입 배열을 향상된 for문으로 순회하며 반복 변수 값을 변경해도 원본 배열은 바뀌지 않는다.

public class Main {

    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        for (int number : numbers) {
            number = number * 10;
        }

        for (int number : numbers) {
            System.out.println(number);
        }
    }
}

출력은 여전히 1, 2, 3이다.

원본 요소를 변경하려면 인덱스로 접근해야 한다.

for (int i = 0; i < numbers.length; i++) {
    numbers[i] *= 10;
}

15. Arrays.toString()

배열 변수를 그대로 출력하면 사람이 읽기 어려운 문자열이 나온다.

int[] numbers = {1, 2, 3};

System.out.println(numbers);

배열 내용을 간단히 확인하려면 Arrays.toString()을 사용한다.

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        System.out.println(Arrays.toString(numbers));
    }
}

실행 결과는 다음과 같다.

[1, 2, 3]

Arrays 클래스를 사용하려면 import가 필요하다.

import java.util.Arrays;

16. 문자열을 char 배열과 int 배열로 변환하기

메모에 있는 "1234567890" 문자열 예제를 완성해보자.

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String original = "1234567890";

        char[] characters = new char[original.length()];
        int[] numbers = new int[original.length()];

        for (int i = 0; i < original.length(); i++) {
            characters[i] = original.charAt(i);
            numbers[i] = characters[i] - '0';
        }

        System.out.println(Arrays.toString(characters));
        System.out.println(Arrays.toString(numbers));
    }
}

실행 결과는 다음과 같다.

[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

characters의 요소는 문자이고 numbers의 요소는 숫자다.

numbers[i] = characters[i] - '0';

문자 '0'부터 '9'까지는 유니코드상 연속된 값을 가지므로 '0'을 빼면 실제 숫자를 얻을 수 있다.

'3' - '0'
→ 51 - 48
→ 3

문자열을 문자 배열로 바꾸는 작업은 API를 사용해 간단히 처리할 수도 있다.

char[] characters = original.toCharArray();

17. 배열의 길이는 변경할 수 없다

Java 배열은 한 번 생성되면 길이를 늘리거나 줄일 수 없다.

int[] numbers = new int[3];

각 요소의 값은 변경할 수 있다.

numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

그러나 네 번째 요소를 추가할 수는 없다.

// numbers[3] = 40;
// 범위 초과

더 큰 배열이 필요하다면 새로운 배열을 만들고 기존 값을 복사해야 한다.

int[] oldNumbers = {10, 20, 30};
int[] newNumbers = new int[5];

for (int i = 0; i < oldNumbers.length; i++) {
    newNumbers[i] = oldNumbers[i];
}

크기가 자주 변해야 한다면 배열보다 ArrayList가 더 적합하다.


18. for문을 이용한 배열 복사

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] original = {10, 20, 30};
        int[] copied = new int[5];

        for (int i = 0; i < original.length; i++) {
            copied[i] = original[i];
        }

        System.out.println(Arrays.toString(copied));
    }
}

실행 결과는 다음과 같다.

[10, 20, 30, 0, 0]

19. System.arraycopy()

Java가 제공하는 System.arraycopy()를 사용하면 배열의 일부 또는 전체를 복사할 수 있다.

System.arraycopy(
        원본배열,
        원본시작인덱스,
        대상배열,
        대상시작인덱스,
        복사할요소개수
);
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String[] original = {"Java", "Spring", "Vue"};
        String[] copied = new String[5];

        System.arraycopy(
                original,
                0,
                copied,
                0,
                original.length
        );

        System.out.println(Arrays.toString(copied));
    }
}

실행 결과는 다음과 같다.

[Java, Spring, Vue, null, null]

메서드 이름은 arrayCopy가 아니라 소문자 c를 사용한 다음 형태다.

System.arraycopy()

20. Arrays.copyOf()

배열 전체를 새로운 길이로 복사할 때는 Arrays.copyOf()가 간결하다.

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] original = {10, 20, 30};

        int[] copied = Arrays.copyOf(original, 5);

        System.out.println(Arrays.toString(copied));
    }
}

실행 결과는 다음과 같다.

[10, 20, 30, 0, 0]

두 번째 인수는 새 배열의 길이다.

Arrays.copyOf(원본배열, 새로운길이);

21. 얕은 복사와 참조값 복사

참조 타입 배열을 복사할 때는 배열 요소가 참조하는 객체를 새로 만드는 것이 아니라 참조값이 복사된다.

public class Main {

    public static void main(String[] args) {
        StringBuilder[] original = {
                new StringBuilder("Java")
        };

        StringBuilder[] copied = new StringBuilder[1];

        System.arraycopy(original, 0, copied, 0, 1);

        copied[0].append(" Programming");

        System.out.println(original[0]);
        System.out.println(copied[0]);
    }
}

실행 결과는 두 배열 모두 다음 문자열을 출력한다.

Java Programming

배열 객체는 서로 다르지만 내부 요소가 같은 StringBuilder 객체를 참조하기 때문이다.

original ──→ [참조값] ──┐
                         ├─→ StringBuilder 객체
copied   ──→ [참조값] ──┘

이와 같이 객체 자체가 아닌 참조값만 복사하는 방식을 얕은 복사라고 한다.


22. 다차원 배열

Java의 다차원 배열은 배열이 다른 배열을 참조하는 구조다.

2차원 배열은 다음과 같이 선언하고 생성한다.

int[][] scores = new int[2][3];

첫 번째 길이는 행, 두 번째 길이는 열로 이해할 수 있다.

scores
├─ scores[0] → [0][0][0]
└─ scores[1] → [0][0][0]

값을 저장할 때는 두 개의 인덱스를 사용한다.

scores[0][0] = 90;
scores[0][1] = 80;
scores[0][2] = 100;

scores[1][0] = 70;
scores[1][1] = 85;
scores[1][2] = 95;

2차원 배열 생성과 초기화

int[][] matrix = {
        {1, 2, 3},
        {4, 5, 6}
};

중첩 반복문으로 모든 요소를 출력할 수 있다.

public class Main {

    public static void main(String[] args) {
        int[][] matrix = {
                {1, 2, 3},
                {4, 5, 6}
        };

        for (int row = 0; row < matrix.length; row++) {
            for (int column = 0;
                 column < matrix[row].length;
                 column++) {

                System.out.print(matrix[row][column] + " ");
            }

            System.out.println();
        }
    }
}

matrix.length는 행 배열의 길이고, matrix[row].length는 해당 행의 열 길이다.


23. 계단식 배열

Java의 2차원 배열은 각 행의 길이가 서로 다를 수 있다.

int[][] scores = new int[2][];

scores[0] = new int[2];
scores[1] = new int[3];

메모리 구조는 다음과 같다.

scores
├─ scores[0] → [0][0]
└─ scores[1] → [0][0][0]

값 목록으로도 만들 수 있다.

int[][] numbers = {
        {1, 2},
        {3, 4, 5},
        {6}
};

이를 계단식 배열 또는 가변 길이 배열이라고 부르기도 한다.


24. 객체를 참조하는 배열

참조 타입 배열의 요소에는 객체의 참조값이 저장된다.

String[] languages = new String[3];

languages[0] = "Java";
languages[1] = "Java";
languages[2] = new String("Java");

관계는 다음과 같이 볼 수 있다.

languages[0] ──┐
               ├──→ 문자열 풀의 "Java"
languages[1] ──┘

languages[2] ─────→ new String("Java")

따라서 다음 결과가 나온다.

System.out.println(languages[0] == languages[1]);
System.out.println(languages[0] == languages[2]);
System.out.println(languages[0].equals(languages[2]));
true
false
true

25. 명령행 인수와 String[] args

main() 메서드의 매개변수는 문자열 배열이다.

public static void main(String[] args)

프로그램을 실행할 때 전달한 값들이 args 배열에 저장된다.

public class Main {

    public static void main(String[] args) {
        System.out.println("전달된 값의 개수: " + args.length);

        for (String argument : args) {
            System.out.println(argument);
        }
    }
}

예를 들어 다음과 같이 실행했다고 가정하자.

java Main Java Spring 100

args 배열은 다음 값을 가진다.

args[0] = "Java"
args[1] = "Spring"
args[2] = "100"

명령행 인수를 전달하지 않아도 일반적으로 args는 null이 아니라 길이가 0인 배열이다.


26. 열거 타입

데이터가 몇 가지 한정된 값만 가질 수 있다면 열거 타입을 사용할 수 있다.

요일은 다음 일곱 값 중 하나만 가진다.

MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY

문자열로 관리하면 오타가 발생할 수 있다.

String today = "SUNDEY";

열거 타입을 사용하면 허용된 값만 저장할 수 있다.


열거 타입 선언

Week.java 파일을 생성한다.

public enum Week {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

열거 상수는 관례적으로 모두 대문자와 언더바를 사용한다.


열거 타입 사용

public class Main {

    public static void main(String[] args) {
        Week today = Week.FRIDAY;

        if (today == Week.FRIDAY) {
            System.out.println("오늘은 금요일입니다.");
        }
    }
}

열거 상수는 정해진 하나의 객체이므로 ==를 사용해 안전하게 비교할 수 있다.

switch와도 잘 어울린다.

public class Main {

    public static void main(String[] args) {
        Week today = Week.SATURDAY;

        String message = switch (today) {
            case MONDAY, TUESDAY, WEDNESDAY,
                 THURSDAY, FRIDAY -> "평일입니다.";

            case SATURDAY, SUNDAY -> "주말입니다.";
        };

        System.out.println(message);
    }
}

27. 종합 예제: 학생 점수 관리

배열, 반복문, 조건문을 함께 활용해 점수 통계를 계산해보자.

import java.util.Arrays;

public class ScoreManager {

    public static void main(String[] args) {
        int[] scores = {85, 92, 78, 100, 66};

        int sum = 0;
        int maximum = scores[0];
        int minimum = scores[0];

        for (int score : scores) {
            sum += score;

            if (score > maximum) {
                maximum = score;
            }

            if (score < minimum) {
                minimum = score;
            }
        }

        double average = (double) sum / scores.length;

        System.out.println("점수: "
                + Arrays.toString(scores));

        System.out.println("합계: " + sum);
        System.out.printf("평균: %.2f%n", average);
        System.out.println("최고 점수: " + maximum);
        System.out.println("최저 점수: " + minimum);
    }
}

실행 결과는 다음과 같다.

점수: [85, 92, 78, 100, 66]
합계: 421
평균: 84.20
최고 점수: 100
최저 점수: 66

헷갈리기 쉬운 개념

배열 변수와 배열 객체

int[] numbers;

배열 변수만 선언한 상태다.

numbers = new int[3];

실제 배열 객체를 생성하고 참조값을 변수에 저장한 상태다.


배열 길이와 마지막 인덱스

길이가 3인 배열의 인덱스는 0, 1, 2다.

int length = numbers.length;
int lastIndex = numbers.length - 1;

배열의 length와 String의 length()

numbers.length
text.length()

배열은 필드, 문자열은 메서드다.


배열 변수 복사와 배열 요소 복사

int[] first = {1, 2, 3};
int[] second = first;

새로운 배열이 만들어진 것이 아니다. 두 변수가 같은 배열을 참조한다.

second[0] = 100;

System.out.println(first[0]);

결과도 100이다.

독립된 배열을 만들려면 실제 복사가 필요하다.

int[] second = Arrays.copyOf(first, first.length);

실무에서 알아두면 좋은 내용

배열은 크기가 고정되어 있고 같은 타입의 값을 빠르게 순차 처리하는 데 적합하다.

다음과 같은 경우 배열이 잘 어울린다.

  • 크기가 고정된 좌표값
  • 월별 데이터 12개
  • RGB 값
  • 알고리즘 문제의 입력 데이터
  • 행렬 계산

데이터의 추가와 삭제가 자주 발생한다면 ArrayList 같은 컬렉션이 더 적합하다.

List<String> names = new ArrayList<>();

배열의 내용을 비교할 때도 ==를 사용하면 안 된다.

int[] first = {1, 2, 3};
int[] second = {1, 2, 3};

System.out.println(first == second);

결과는 false다.

요소의 내용을 비교하려면 Arrays.equals()를 사용한다.

import java.util.Arrays;

System.out.println(Arrays.equals(first, second));

2차원 이상 배열의 내용을 비교할 때는 Arrays.deepEquals()를 사용한다.

Arrays.deepEquals(firstMatrix, secondMatrix);

2차원 배열을 출력할 때는 Arrays.deepToString()을 사용할 수 있다.

System.out.println(Arrays.deepToString(matrix));

오늘 배운 내용 정리

  • Java의 데이터 타입은 기본 타입과 참조 타입으로 구분된다.
  • 기본 타입 변수는 값을 직접 저장하고, 참조 타입 변수는 객체를 참조하는 값을 저장한다.
  • 배열은 참조 타입이며 배열 객체는 Heap 영역에 생성된다.
  • 메서드가 호출될 때 JVM Stack에 프레임이 생성되고, 로컬 변수가 프레임에서 관리된다.
  • 참조 타입에서 ==는 같은 객체를 참조하는지 비교한다.
  • 객체의 내용을 비교하려면 타입에 맞는 equals() 또는 배열 비교 API를 사용해야 한다.
  • 참조 타입 변수는 null을 가질 수 있다.
  • null인 변수로 객체를 사용하면 NullPointerException이 발생한다.
  • 배열은 같은 타입의 여러 데이터를 인덱스로 관리한다.
  • 배열 인덱스는 0부터 시작하며 마지막 인덱스는 length - 1이다.
  • 배열은 생성된 이후 길이를 변경할 수 없다.
  • new로 생성한 배열 요소는 타입별 기본값으로 초기화된다.
  • 일반 for문은 인덱스가 필요할 때 사용하고, 향상된 for문은 요소를 순서대로 읽을 때 편리하다.
  • Arrays.toString()으로 배열 내용을 쉽게 출력할 수 있다.
  • 배열은 반복문, System.arraycopy(), Arrays.copyOf() 등으로 복사할 수 있다.
  • 참조 타입 배열을 복사하면 기본적으로 요소가 가진 참조값이 복사된다.
  • Java의 2차원 배열은 각 행이 다른 배열을 참조하는 구조다.
  • 각 행의 길이가 다른 계단식 배열도 만들 수 있다.
  • String[] args에는 프로그램 실행 시 전달한 명령행 인수가 저장된다.
  • 한정된 값만 사용하는 데이터는 열거 타입으로 표현할 수 있다.
profile
넘어가 울타리 green green

0개의 댓글