[Java] 4장_제어문

57973 단어 자바자바

4장_제어문

목표

자바가 제공하는 제어문(조건문, 반복문)을 학습한다.

목차

  1. 제어 구조
  2. if-else 문
  3. switch 문
  4. for 문
  5. while 문
  6. do-while 문
  7. break 문
  8. continue 문

Ref: WhiteShip 라이브 스터디 4주차 과제

본 과제에서는JUnt 5를 학습하거나 LinkedList, Stack, Queue를 구현하는 과제가 포함되어 있다.
이에 대한 내용은 추후 다른 문서에서 다룰 예정이다.

1. 제어 구조

프로그램은 가장 기본적인 의미에서 시작하자면 일련의 명령어 목록이라고 할 수 있다. 프로그램의 제어 구조는 이러한 명령어들이 통과하는 경로를 바꾸는 블록을 말한다.

제어 구조의 종류는 크게 세 가지가 있다.

  1. 둘 이상의 경로 중에서 하나를 선택 하는 경우 (조건부 분기 또는 조건문)

    관련: if / else / else if, 삼항 연산자, switch

  2. 여러 개의 값 또는 여러 개의 객체를 반복하는 경우 또는 특정 코드 블록을 반복적으로 실행 하는 경우 (루프 또는 반복문):

    관련: for, while , do while

  3. 루프 내부의 제어 흐름 을 변경하는 경우 (분기 문):

    관련: break, continue

다음 항목부터는 위에서 언급한 세 가지 구조대로 항목을 나누지 않고, 각 구조의 관련을 참고하여 항목을 구성하였다. 따라서 이하의 항목부터는 2. 조건문, 3. 반복문, 4. 분기 문 이렇게 구성되는 게 아니라, 2. if-else 문, 3. switch 문, 4. for 문 등으로 구성된다.

2. if-else 문

사용 예시

if / else 문은 가장 기본적인 제어 구조로, 의사 결정을 내릴 때 가장 기본이 되는 구문이라고 할 수 있다. if는 단독 사용이 가능하고, if / else 를 함께 사용하여 두 경로 중에서 하나를 선택하도록 할 수 있다.

if (num > 10) {
    System.out.println("num은 10보다 크다");
} else {
    System.out.println("num은 10보다 작거나 같다");
}

단, if / else 블록을 무한히 연결하거나, 내부에 중첩할 수 있지만, 코드 가독성을 크게 떨어뜨릴 수 있기 때문에 권장하지는 않는다.

삼항 연산자

위에 예시로 든 if / else 문을 아래 예시처럼 간단히 삼항 연산자로 정리할 수도 있다.

System.out.println(num > 10 ? "num은 10보다 크다" : "num은 10보다 작거나 같다");

삼항 연산자에 대한 구체적인 내용은 여기에 자세히 기술해두었다.

3. switch 문

사용 예시

선택할 수 있는 케이스가 여러 개인 경우, 아래와 같이 스위치 문을 작성할 수 있다.

public void printNameOfAnimal(String animal) {
    switch (animal) {
        case "DOG":
            System.out.println("DOG"); 
            break;
        case "CAT":
            System.out.println("CAT");
            break;
        case "TIGER":
            System.out.println("TIGER");
            break;
        default:
            System.out.println(animal + " is not in the given cases.");
            break;
    }
}

위의 예시에서는 switch의 인수 animal을 여러 케이스의 값와 비교한다. 인수 animal과 동일한 케이스 값이 없으면, default 레이블 아래의 블록이 실행된다.

break 문

한편, 중간 중간 보이는 breakswitch 전체 블록을 종료하는 역할을 한다. animal이 특정 케이스 값과 일치할 때, 해당 블록만 실행되고 나머지는 실행되지 않아야 하는데, 이를 위해 케이스마다 break문을 작성한다. 만약 break 를 깜빡하면, 그 아래에 있는 모든 블록이 실행된다(break가 없는 곳이라면). 아래 break를 빠뜨린 간단한 예시가 있다.

public class SwitchBreakDemo {
    public static void main(String[] args) {
        SwitchBreakDemo breakDemo = new SwitchBreakDemo();

        breakDemo.printNameOfAnimal("DOG");
    }   

    public void printNameOfAnimal(String animal) {
        switch (animal) {
            case "DOG":
                System.out.println("DOG");  // 바로 아래 break 문이 없음 
            case "CAT":
                System.out.println("CAT");  // 바로 아래 break 문이 없음
            case "TIGER":
                System.out.println("TIGER"); 
                break; // 여기까지 오면 switch 전체 블록이 종료됨
            default:
                System.out.println(animal + " is not in the given cases.");
                break;
        }
    }
}


// 출력 결과
// DOG
// CAT
// TIGER

분명 DOG가 인수로 전달되고 케이스 레이블 중에도 DOG가 분명히 있지만, 출력 결과는 CAT 그리고 TIGER까지 나타난다. 이는 DOGCAT에는 break가 없고, TIGER 에만 break가 있기 때문이다. case "DOG" 부터 시작해 아래로 타고 타고 흘러가다 TIGERbreak 에서 스위치 문이 종료되었다 default 레이블을 만나기 전에 스위치 문이 종료되었으니, default 레이블 블록은 당연히 실행되지 않았다. 한 가지 알아둘 점은, 마지막 블록은 break가 필요 없어 보이지만 break 를 추가하면 오류 발생 가능성이 줄어든다고 한다.

여러 케이스에 대해 동일한 코드를 실행하려는 경우에는, 아래 예시와 같이 블록(또는 구문)을 생략할 수도 있다.

public void printNameOfAnimal2(String animal) {
    switch (animal) {
        case "DOG":  // System.out.println("DOG"); 구문이 생략되었다
        case "CAT":
            System.out.println("DOG or CAT");
            break;
        case "TIGER":
            System.out.println("TIGER");
            break;
        default:
            System.out.println(animal + " is not in the given cases.");
            break;
    }
}

스위치 인수 및 케이스 값

스위치 인수(switch argument)나 케이스 값(case value)으로 모든 타입이 허용되지는 않는다. 이와 관련한 내용을 아래 살펴보기로 하자.

사용 가능한 타입

  • byte and Byte
  • short and Short
  • int and Integer
  • char and Character
  • enum (자바 5부터 사용 가능)
  • String (자바 7부터 사용 가능)

래퍼 클래스도 자바 5부터 사용이 가능해졌다.
당연한 얘기지만, switch 인수와 case 값은 동일 타입이어야 한다.

(주의!) null 값은 스위치 인수로 전달할 수 없다. 이렇게 했을 때 NullPointerException이 발생한다. 즉, 아래 코드를 실행하면 NullPointerException이 발생할 것으로 예측할 수 있다.

@Test(expected=NullPointerException.class) // 결과적으로 NullPointerException 발생이 예상된다
public void whenSwitchAgumentIsNull_thenNullPointerException() {
    String animal = null;
    Assert.assertEquals("domestic animal", s.exampleOfSwitch(animal));
}

public String exampleOfSwitch(String animal) {
    String result;
    switch (animal) {
        case "DOG":
        case "CAT":
            result = "domestic animal";
            break;
        case "TIGER":
            result = "wild animal";
            break;
        default:
            result = "unknown animal";
            break;
    }
    return result;
}

물론 switch 문의 case 레이블에도 null 값을 설정할 수 없다. 그렇게 하면 컴파일조차 되지 않는다.

상수 케이스 값

DOG 케이스 값을 dog 변수로 바꾸려고하면 반드시 변수의 타입 앞에 final을 붙여줘야 한다. 그렇지 않으면 컴파일되지 않는다.

final String dog = "DOG";  // final이 있는 변수
String cat = "CAT";  // final이 없는 변수

switch (animal) {
case dog: // 컴파일 된다
    result = "domestic animal";
case cat: // 컴파일 되지 않는다
    result = "feline"
}

특히 세 개 이상의 if / else 문은 읽기 어려울 수 있다. 이에 대한 대안으로 스위치 문을 사용할 수 있다.

문자열 비교

switch 문에서 == 연산자를 사용하여 문자열을 비교하면, new 연산자로 만든 문자열 인수를 문자열 case 값과 올바르게 비교할 수 없다.

참고로 == 연산자는, 피연산자가 원시 타입일 때는 값이 같은지 비교하고, 레퍼런트 타입일 때는 가리키는 주소가 같은지 비교한다.

문자열은 레퍼런스 타입이므로 switch 인수와 케이스 값은 서로 가리키는 주소를 비교할 것이다. 만약 인수가 new 연산자로 생성되었다면, 인수는 케이스의 값과 다른 주소를 가질 것이다. 그러므로 이 경우 문자열끼리 서로 동일한 내용을 가지고 있어도 주소가 다르기 때문에 올바른 케이스를 찾지 못하는 문제가 발생할 수 있다.

하지만 다행히, switch 연산자는 내부적으로 equals() 메소드를 사용한다. 특히 String 간의 equals() 메소드를 사용할 경우, 문자열의 내용만 비교하기 때문에 올바른 값을 비교를 할 수 있다(다른 Object는 이와 다름).

// exampleOfSwitch() 메소드는 윗윗 예시를 참고
@Test
public void whenCompareStrings_thenByEqual() {
    String animal = new String("DOG");
    assertEquals("domestic animal", s.exampleOfSwitch(animal));
}

자바 12부터 개선된(확장된) 스위치 문법을 공부하고 싶다면, 여기에 자세한 내용을 기술해두었으니 참고하도록 하자.

Ref: Java Switch Statement

4. for 문

루프

Loop, 반복문

for 문에 들어가기에 앞서, 루프에 대해 간단히 알아보자.

먼저 프로그래밍에서 루핑(Looping)이라 함은, boolean 표현식이 거짓(false)으로 판명날 때까지 명령를 실행하는 것 을 이야기한다. 그리고 루프(Loop)는 이러한 루핑 과정이 구현된 블록을 말한다.

자바에서는 아래와 같은 루프를 지원하고 있다.

  • 단순 for 루프
  • for-each 루프
  • while 루프
  • do-while 루프

단순 for 루프

이미 for 문에 대해 어느 정도 기초 지식을 가지고 있다고 생각하여, 몇몇 설명은 과감히 생략하였다.

가장 먼저 for 루프의 문법을 살펴보자. 자바에서 기본 문법은 아래와 같다.

for (초기화; boolean 표현식; 스텝)
    실행할 구문;

물론 문법만으로는 잘 이해되지 않으니 실제 구문도 작성해보았다.

public class ForDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println("단순 for 문: i = " + i);
        }
    }
}


// 출력 결과
// 단순 for 문: i = 0
// 단순 for 문: i = 1
// 단순 for 문: i = 2

for 문에서 초기화, boolean 표현식스텝 은 선택 사항이다. 다음처럼 작성하면 무한 for 루프를 만들 수 있다.

for ( ; ; ) {
    // 여기 오는 내용이 무한 반복됨
}

for 루프 레이블

for 루프에 레이블을 지정할 수도 있다. 만약 중첩된 for 루프를 사용하는 경우 레이블은 유용한 도구다.

public class ForDemo2 {
    public static void main(String[] args) {
        aa: for (int i = 1; i <= 3; i++) {  // aa 레이블 지정
            if (i == 1) {
                System.out.println("continue when i == " + i); 
                continue; // continue 문은 아래 "continue 문" 항목을 참고하자
            }
            for (int j = 1; j <= 3; j++) {
                if (i == 2 && j == 3) {
                    break aa;  // (i,j)가 (2,3)일 때 aa 루프 종료
                }
                System.out.println("(i,j): (" + i + "," + j + ")");
            }
        }
    }
}



// 출력 결과
// continue when i == 1
// (i,j): (2,1)
// (i,j): (2,2)

향상된 for 루프

자바 5부터 배열 또는 컬렉션의 모든 요소를 쉽게 반복할 수 있도록 for 문이 향상되었다. 향상된 for 루프의 구문은 아래와 같다.

for(Type item : items)
  statement;

이로써, items의 각 요소를 item 변수에 할당하고, 루프의 본문(statements)을 실행시킬 수 있게 되었다. 아래 구현된 코드를 살펴보자.

int[] intArr = { 0,1,2 }; 
for (int num : intArr) {
    System.out.println("향상된 for-each 문: i = " + num);
}

// 출력 결과
// 향상된 for-each 문: i = 0
// 향상된 for-each 문: i = 1
// 향상된 for-each 문: i = 2

이와 같이 향상된 for-each 문List<String> , Set<String>, Map<String,Integer>entrySet() 등 배열 또는 Iterable 인터페이스를 구현하는 구현체라면 보다 편리한 루핑 기능을 제공해준다.

Iterable.forEach 메소드

자바 8 이후로 약간 다른 방식으로 for-each 루프를 활용할 수 있게 되었다. Iterable 인터페이스의 forEach() 메소드와 람다식 인수를 활용하는 방법인데 forEach() 메소드의 내부는 아래와 같다.

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

// 참고로, default 키워드는 같은 패키지(폴더)에 있는 객체들만 사용을 허가하는 접근 제한자이다

forEach() 메소드를 사용하여 다음과 같은 코드를 작성할 수 있다.

List<String> names = new ArrayList<>();
names.add("Mark");
names.add("Eden");

// forEach 메소드의 인수로 람다식을 사용하였다
names.forEach(name -> System.out.println(name)); 

// 출력 결과
// Mark
// Eden

Ref: Java For Loop

5. while 문

while 루프 역시 자바의 가장 기본적인 루프 문이다. boolean 표현식이 참인 동안 명령문 또는 명령문 블록을 반복한다. 기본 문법은 아래와 같다.

while (boolean 표현식) 
    statement;

루프의 boolean 표현식은 루프의 첫 번째 반복이 시작되기 전에 실행된다. 즉, 조건식을 처음 실행했을 때 false로 판명되면 루프가 한 번도 실행되지 않는다. 아래는 while 문의 예시가 있다.

int i = 0;
while (i < 3) {
    System.out.println("while loop: i = " + i++);
}


// 출력 결과
// while 문: i = 0
// while 문: i = 1
// while 문: i = 2

Ref: Java While Loop

6. do-while 문

do-while 루프는 첫 번째 루프를 먼저 실행한 후에, 조건식을 계산한다. 이 점을 제외하면, while 루프와 동일하게 작동한다. 기본 문법은 다음과 같다.

do {
    statement;
} while (boolean 표현식);

실제 코드로는 다음과 같이 작성할 수 있다.

int i = 0;
do {
    System.out.println("do-while 문: i = " + i++);
} while (i < 3);


// 출력 결과
// do-while 문: i = 0
// do-while 문: i = 1
// do-while 문: i = 2

Ref: Java Do-While Loop

7. break 문

소개

break 문 사용시 루프 내부에서 제어 흐름이 분기되고 해당 블록의 코드 실행이 종료된다. 따라서 루프 완료 전 루프를 일찍 종료시키고 싶다면 break를 사용해야 한다. 아래 그림은 루프 내부에서 break 가 분기점을 만들고 루프를 일찍 종료시키는 것을 잘 보여주고 있다.

보통 그림을 참조할 때, 보고 직접 따라 그리는 편인데, 위의 그림은 내용이 명확하고 디자인도 예뻐서 그대로 참조하였다.
Ref: The Java continue and break Keywords

break레이블이 없는 것(unlabeled)과 있는 것(labeled), 두 가지 형식으로 나뉜다.

레이블 없는 break

레이블이 없는 break 문을 사용하여, for, while 또는 do-while 루프와 switch-case 블록을 종료할 수 있다. 아래 사용 예시를 살펴보자.

for (int i = 0; i < 5; i++) {
    if (i == 3) {  // i가 3일 때 출력되지 않고 for 루프 종료
        break;
    }
    System.out.println("i = " + i);
}

// 출력 결과
// i = 0
// i = 1
// i = 2

(주의!) 중첩 루프 의 경우, 레이블이 지정되지 않은 break 문은 자신이 속한 내부 루프만 종료한다. 반면, 외부 루프는 계속 실행된다. 아례 사용 예시를 살펴보자.

public static void main(String[] args) {
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++) {
            if (col == 1) {  // col이 1일 때 내부 루프(col)만 종료되고 외부 루프(row)는 계속 실행됨 
                break;
            }
            System.out.println("(row,col): " + "(" + row + "," + col + ")");
        }
    }
}


// 출력 결과
// (row,col): (0,0)
// (row,col): (1,0)
// (row,col): (2,0)

레이블 지정된 break

레이블이 지정된 break 문을 사용하여 for, while 또는 do-while 루프를 종료할 수도 있다. 레이블이 있는 break는 외부 루프를 종료한다. 아래 예시를 살펴보자.

public static void main(String[] args) {
        tempLabel:  // 레이블이 지정됨
        for (int row = 0; row < 3; row++) {
            for (int col = 0; col < 3; col++) {
                if (col == 1) {  // col이 1일 때 내부 루프와 외부 레이블의 루프(row) 모두 종료됨
                    break tempLabel;
                }
                System.out.println("(row,col): " + "(" + row + "," + col + ")");
            }
        }
    }
}


// 출력 결과
// (row,col): (0,0)

종료시 제어 흐름은 외부 루프가 종료된 직후의 명령문으로 넘어간다.

Ref: The Java continue and break Keywords

8. continue 문

간단히 말해, continue는 현재 진행 중인 루프에서 continue 문 직후의 나머지 부분을 모두 건너뛰는 것을 의미한다. 아래 그림은 루프 내부에서 continue 가 새로운 분기점을 만들고 루프를 바로 다음 조건식 검증으로 넘기는 것을 잘 보여준다.

break 문의 그림과 마찬가지로 내용이 명확하고 디자인도 예뻐서 그대로 참조하였다.
Ref: The Java continue and break Keywords

break처럼 continue 문은 레이블이 없는 것(unlabeled)과 있는 것(labeled), 두 가지 형식으로 나뉜다.

레이블 없는 continue

레이블이 없는 continue 문을 사용하여 for, while 또는 do-while 루프의 현재 반복에서 나머지 명령문의 실행을 우회할 수 있다. 즉, continue 문을 만났을 때 제어 호름은 내부 루프 끝으로 건너뛰고 외부 루프를 계속 실행한다. 아래 예시를 살펴보자.

public static void main(String[] args) {
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++) {
            if (col == 1) {  // col이 1일 때 내부 루프(col) 블록 끝으로 이동
                continue;  // 이때 continue 다음 구문들은 실행되지 않고 블록 끝으로 이동
            }
            System.out.println("(row,col): " + "(" + row + "," + col + ")");
        }
    }
}


// 출력 결과
// (row,col): (0,0)
// (row,col): (0,2)
// (row,col): (1,0)
// (row,col): (1,2)
// (row,col): (2,0)
// (row,col): (2,2)

레이블 지정된 continue

레이블이 지정된 continue 문은 외부 루프의 현재 단계를 건너 뛴다. 즉, 제어 흐름이 특정 조건에서 외부 루프를 건너 뛰기 때문에, 원하는 조건에서만 외부 루프를 실행할 수 있게 된다. 말이 조금 어려워졌다. 먼저 아래 예시를 살펴보고, 그 아래 설명도 참고하자.

public static void main(String[] args) {
    tempLabel:  // 레이블이 지정됨
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++) {
            if (col == 1) {  // col이 1일 때 외부 루프(row) 현재 단계를 건너 뜀 
                continue tempLabel;  // 현재 단계의 내부 루프(col)를 포함해서 건너 뜀
            }
            System.out.println("(row,col): " + "(" + row + "," + col + ")");
        }
    }
}


// 출력 결과
// (row,col): (0,0)
// (row,col): (1,0)
// (row,col): (2,0)

row0이고, col0 일 때, col == 1 의 결과는 false 이다. 따라서 continue 문이 실행되지 않고, 대신 그 아래 println 메소드가 실행된다((row,col): (0,0) 이 출력됨). 다음으로,

row은 그대로 0이고, col1 이 대입된다. 이때 col == 1 의 결과가 true 이므로 continue 문이 실행된다. 이 경우 레이블이 지정되어 있기 때문에, 외부 루프의 현재 단계(즉, row0인 단계)를 건너 뛰게 된다. row0인 단계가 모두 종료되었기 때문에 row에는 1이, col에는 다시 0이 대입된다. (col2 가 대입되는 게 아니다.) 이로써,

row1이 되고, col0이 되는데, col == 1의 결과가 false 이므로 continue 문이 실행되지 않는다. 대신 그 아래 println 메소드가 실행되어 이 단계에서는 (row,col): (1,0)이 출력된다. 이런 방식으로 계속 접근해보면 결국 위의 출력 결과 와 동일한 결과를 얻을 수 있다.

반복문에서 breakcontinue은 종종 return 문이나 다른 로직으로 대체하여 작성할 수 있다.

Ref: The Java continue and break Keywords

Ref

좋은 웹페이지 즐겨찾기