(c)도경구 version 0.5 (2022/10/03)
version 0.51 (2022/10/04) 숙제 문제 보완
version 0.52 (2022/10/07) bouncingball 코드 개선

5. 제어 구조 II - 반복

반복 구조는 동일 코드블록의 반복 실행을 표현하는 구조

대표적인 유형 3가지

  • 반복 횟수 고정
  • 반복 횟수 사전 예측 불가
  • 무한 반복 (diverge)

while 루프

5-1. 반복 횟수 고정

패턴

int n = GOAL_VALUE;
int count = 0;
while (count < n) {
    // 코드
    count += 1;
}

사례 1. - 시험 점수 평균 구하기

import javax.swing.*;

public class CourseManagement {

    public double calculateAverage(int n) {
        double sum = 0.0;
        int count = 0;
        while (count < n) {
            String input = JOptionPane.showInputDialog("시험 점수?");
            int score = Integer.parseInt(input);
            sum += score;
            count += 1;
            // loop invariant : sum == count개 점수의 합
        }
        return sum / count; // n == count
    }

    public static void main(String[] args) {
        CourseManagement course_mgmt = new CourseManagement();
        String message = "평균 점수 = " + course_mgmt.calculateAverage(5);
        JOptionPane.showMessageDialog(null, message);
    }

}

사례 2. - 양궁 과녁 그리기

import javax.swing.*;
import java.awt.*;

public class Archery extends JPanel {

    private final int RINGS = 10; // 원의 개수
    private final int TARGET_DIAMETER;

    public Archery(int d) {
        TARGET_DIAMETER = d;
        JFrame f = new JFrame();
        f.getContentPane().add(this);
        f.setTitle("양궁 과녁");
        f.setSize(TARGET_DIAMETER, TARGET_DIAMETER + 28);
        f.setVisible(true);
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }

    public void paintComponent(Graphics g) {
        g.setColor(Color.green);
        g.fillRect(0,  0, TARGET_DIAMETER, TARGET_DIAMETER);
        final int OFFSET = TARGET_DIAMETER / RINGS;
        int number = 1;
        int diameter = TARGET_DIAMETER;
        int new_x_position = 0;
        int new_y_position = 0;
        while (number <= RINGS) {
            // loop invariant (루프 불변 성질): 지금까지 number-1 개의 링을 그렸음
            if (number <= 2) {
                g.setColor(Color.white);
                g.fillOval(new_x_position, new_y_position, diameter, diameter);
                g.setColor(Color.gray);
                g.drawOval(new_x_position, new_y_position, diameter, diameter);
            }
            else if (number <= 4) {
                g.setColor(Color.black);
                g.fillOval(new_x_position, new_y_position, diameter, diameter);
                if (number == 4) {
                    g.setColor(Color.white);
                    g.drawOval(new_x_position, new_y_position, diameter, diameter);
                }
            }
            else if (number <= 6) {
                g.setColor(Color.blue);
                g.fillOval(new_x_position, new_y_position, diameter, diameter);
                g.setColor(Color.white);
                g.drawOval(new_x_position, new_y_position, diameter, diameter);
            }
            else if (number <= 8) {
                g.setColor(Color.red);
                g.fillOval(new_x_position, new_y_position, diameter, diameter);
                g.setColor(Color.white);
                g.drawOval(new_x_position, new_y_position, diameter, diameter);
            }
            else if (number <= 10) {
                g.setColor(Color.yellow);
                g.fillOval(new_x_position, new_y_position, diameter, diameter);
                g.setColor(Color.red);
                g.drawOval(new_x_position, new_y_position, diameter, diameter);
            }
            new_x_position = OFFSET * number / 2;
            new_y_position = OFFSET * number / 2;
            number += 1;
            diameter = diameter - OFFSET;
        }
    }

    public static void main(String[] args) {
        new Archery(400);
    }

}

5-2. 반복 횟수 사전 예측 불가

입력 값에 종속 패턴

boolean processing = true;
while (processing) {
    // 입력
    if (/* 종료 신호 수신 */)
        processing = false;
    else {
        // 코드
    }
}

입력 값에 종속 사례 - 시험 점수 평균 구하기

import javax.swing.*;

public class CourseManagement {

    public double calculateAverage() {
        double sum = 0.0;
        int count = 0;
        boolean processing = true;
        while (processing) {
            // loop invariant : sum == count개 점수의 합
            String message = "다음 시험 점수? (입력 완료시 Cancel 버튼 누름)";
            String input = JOptionPane.showInputDialog(message);
            if (input == null) // Cancel 버튼을 눌렀음
                processing = false;
            else {
                int score = Integer.parseInt(input);
                sum += score;
                count += 1;
            }
        }
        if (count == 0) return 0;
        else return sum / count;
    }

    public static void main(String[] args) {
        CourseManagement course_mgmt = new CourseManagement();
        String message = "평균 점수 = " + course_mgmt.calculateAverage();
        JOptionPane.showMessageDialog(null, message);
    }

}

검색 결과에 종속 패턴

문자열 s에서 문자 c 찾기

int index = 0;
boolean found = false;
while (!found && index < s.length()) {
    if (s.charAt(index) == c)
        found = true;
    else
        index += 1;
}

문자열 검색 (while 루프 버전)

public class TextSearch {

    public int findChar(char c, String s) {
        boolean found = false;
        int index = 0;
        while (!found && index < s.length()) {
            // loop invariant:
            // (1) found == false : s[0], .., s[index-1]은 모두 c가 아님
            // (2) found == true : s.charAt(index) == c
            if (s.charAt(index) == c)
                found = true;
            else
                index = index + 1;
        }
        if (!found)
            index = -1;
        return index;
    }

    public static void main(String[] args) {
        TextSearch text_search = new TextSearch();
        System.out.println(text_search.findChar('a', "Hanyang"));
        System.out.println(text_search.findChar('e', "Hanyang"));
    }
}

while 루프로 for 루프 이해

아래 두 제어구조는 의미가 동일하다.

int count = 0;
while (count < n) {
    // 코드
    count += 1;
}
for (int count = 0; count < n; count += 1) {
    // 코드
}

문자열 검색 (for 루프 버전)

public class TextSearch {

    public int findChar(char c, String s) {
        int index;
        for (index = 0; index < s.length() && s.charAt(index) != c; index++)
            // loop invariant: s[0], .., s[index-1]은 모두 c가 아님
            ;
        if (index == s.length())
            index = -1;
        return index;
    }

    public static void main(String[] args) {
        TextSearch text_search = new TextSearch();
        System.out.println(text_search.findChar('a', "Hanyang"));
        System.out.println(text_search.findChar('e', "Hanyang"));
    }
}

5-3. 중첩 루프

사례 1 - 구구단 프린트 하기

public class TextSearch {

    public int findChar(char c, String s) {
        int index;
        for (index = 0; index < s.length() && s.charAt(index) != c; index++)
            // loop invariant: s[0], .., s[index-1]은 모두 c가 아님
            ;
        if (index == s.length())
            index = -1;
        return index;
    }

    public static void main(String[] args) {
        TextSearch text_search = new TextSearch();
        System.out.println(text_search.findChar('a', "Hanyang"));
        System.out.println(text_search.findChar('e', "Hanyang"));
    }
}

사례 2 - 체스 보드 그리기

import javax.swing.*;
import java.awt.*;

public class ChessBoardWriter extends JPanel {
    private int number_of_rows;
    private int square_size;
    private int panel_width;
    private int offset = 20;

    /* Constructor ChessBoardWriter - 패널을 만들고 프레임을 씌움
     * @param rows - 각 열별 칸의 갯수  
     * @param size - 한 칸의 길이  */
    public ChessBoardWriter(int rows, int size) {
        number_of_rows = rows;
        square_size = size;
        panel_width = number_of_rows * square_size + 2 * offset;

        JFrame f = new JFrame();
        f.getContentPane().add(this);
        f.setTitle("Chess Board");
        f.setSize(panel_width, panel_width + 28);
        f.setVisible(true);
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }

    /* paintComponent - 패널에 그림을 그림
     * @param g - 그래픽스 펜 */
    public void paintComponent(Graphics g) {
        g.setColor(Color.gray);
        g.fillRect(0,  0, panel_width, panel_width);
        paintBoard(offset, offset, number_of_rows, square_size, g);
    }

    /* paintBoard - 체스보드를 그림
     * @param start_x - 체스보드의 좌상단 구석의 x 좌표  
     * @param start_y - 체스보드의 좌상단 구석의 y 좌표
     * @param rows - 체스보드 열의 갯수
     * @param size - 체스보드 칸의 너비
     * @param g - 그래픽스 펜 */
    private void paintBoard(int start_x, int start_y,
                            int rows, int size, Graphics g) {
        for (int x = 0; x < rows; x = x + 1) {
            // loop invariant: x열까지 그렸음 (x 증가 전)
            int x_position = start_x + x * size;
            for (int y = 0; y < rows; y = y + 1) {
                // loop invariant: x열의 y칸까지 그렸음 (x 증가 후, y 증가 전)
                int y_position = start_y + y * size;
                if ((x + y) % 2 == 0) // 빨간색 칠할 차례
                    g.setColor(Color.black);
                else
                    g.setColor(Color.white);
                g.fillRect(x_position, y_position, size, size);
            }
        }
    }

    public static void main(String[] args) {
        new ChessBoardWriter(8, 40);
    }
}

5-4. 사례 학습 - 상자 속 공 굴리기 애니메이션

애니메이션 애플리케이션의 아키텍처 일반 패턴

모델

/** Box - 공이 돌아다니는 상자 */
public class Box {

    private int BOX_SIZE;  // 상자의 크기

    /** Constructor Box - 상자 생성
     * @param size - 상자의 크기 */
    public Box(int size) {
        BOX_SIZE = size;
    }

    /** inHorizontalContact - 공이 x축 방향으로 좌/우 벽에 도달 여부를 리턴
     * @param x_position - 공의 x 좌표
     * @return true, 공의 x 좌표가 좌우 벽의 x 좌표와 같거나 벗어났으면 true, 그렇지 않으면 false */
    public boolean inHorizontalContact(int x_position) {
        return (x_position <= 0 ) || (x_position >= BOX_SIZE);
    }

    /** inVerticalContact - 공이 y축 방향으로 아래/위 벽에 도달 여부를 리턴
     * @param y_position - 공의 y 좌표
     * @return true, 공의 y 좌표가 아래위 벽의 y 좌표와 같거나 벗어났으면 true, 그렇지 않으면 false */
    public boolean inVerticalContact(int y_position) {
        return (y_position <= 0 ) || (y_position >= BOX_SIZE);
    }

    /** size - 상자의 크기를 리턴 */
    public int size() {
        return BOX_SIZE;
    }
}

/** MovingBall - 2차원 상자에서 움직이는 공 */
public class MovingBall {
    private int x_pos; // 공의 중심 x 좌표
    private int y_pos; // 공의 중심 y 좌표
    private int radius; // 공의 반지름

    private int x_velocity = +5; // 속도 x축
    private int y_velocity = +2; // 속도 y축

    private Box container;

    /** Contructor MovingBall - 공 만들기
     * @param x_initial - 공의 중심 x 좌표
     * @param y_initial - 공의 중심 y 좌표
     * @param r - 공의 반지름
     * @param box - 공이 살고 있는 상자 */
    public MovingBall(int x_initial, int y_initial, int r, Box box) {
        x_pos = x_initial;
        x_pos = y_initial;
        radius = r;
        container = box;
    }

    /** x_pos - 공의 x축 위치 리턴 */
    public int x_pos() {
        return x_pos;
    }

    /** y_pos - 공의 y축 위치 리턴 */
    public int y_pos() {
        return y_pos;
    }

    /** radius - 공의 반지름 리턴 */
    public int radius() {
        return radius;
    }

    /** move - time_unit 만큼 공을 이동, 벽에 부딪치면 방향을 바꿈
     * @param time_unit - 프레임 사이의 시간 */
    public void move(int time_unit) {
        x_pos += x_velocity * time_unit;
        if (container.inHorizontalContact(x_pos))
            x_velocity = - x_velocity;
        y_pos += y_velocity * time_unit;
        if (container.inVerticalContact(y_pos))
            y_velocity = - y_velocity;
    }
}

모델 테스트

public class TestModel {

    public static void main(String[] args) {
        Box box = new Box(50);
        MovingBall ball = new MovingBall(25, 25, 10, box);
        for (int i = 0; i < 10; i++) {
            ball.move(1);
            System.out.print("x = " + ball.x_pos());
            System.out.println(", y = " + ball.y_pos());
        }
    }
}

import java.awt.*;

/** BoxWriter - 상자를 그림  */
public class BoxWriter {

    private Box box;   // 상자 객체

    /** Constructor BoxWriter
     * @param b - 상자 객체 */
    public BoxWriter(Box b) {
        box = b;
    }

    /** paint - 상자 그리기
     * @param g - 그래픽스 펜 */
    public void paintComponent(Graphics g) {
        int size = box.size();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, size, size);
        g.setColor(Color.BLACK);
        g.drawRect(0, 0, size, size);
    }
}
import java.awt.*;

/** BallWriter - 움직이는 공을 그림 */
public class BallWriter {

    private MovingBall ball; // 공 객체
    private Color color;     // 공의 색깔

    /** Constructor BallWriter
     * @param x - 공 객체
     * @param c - 공의 색깔 */
    public BallWriter(MovingBall x, Color c) {
        ball = x;
        color = c;
    }

    /** paint - 공 그리기
     * @param g - 그래픽스 펜  */
    public void paintComponent(Graphics g) {
        g.setColor(color);
        int radius = ball.radius();
        int diameter = radius * 2;
        g.fillOval(ball.x_pos() - radius, ball.y_pos() - radius, 
                   diameter, diameter);
    }
}
import java.awt.*;
import javax.swing.*;

/** AnimationWriter - 상자와 공의 애니메이션 디스플레이 */
public class AnimationWriter extends JPanel {
    private BoxWriter box_writer;    // 상자 그리는 객체
    private BallWriter ball_writer;  // 공 그리는 객체

    /** Constructor AnimationWriter - 상자와 공을 그리는 View 객체를 생성
     * @param b - 상자 그리는 객체
     * @param l - 공 그리는 객체
     * @param size - 프레임의 크기 */
    public AnimationWriter(BoxWriter b, BallWriter l, int size) {
        box_writer = b;
        ball_writer = l;
        JFrame f = new JFrame();
        f.getContentPane().add(this);
        f.setTitle("Bounce");
        f.setSize(size, size+22);
        f.setVisible(true);
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }

    /** paintComponent - 공과 상자 그리기
     * @param g - 그래픽스 펜 */
    public void paintComponent(Graphics g) {
        box_writer.paintComponent(g);
        ball_writer.paintComponent(g);
    }
}

컨트롤러 + 스타터

/** BounceController - 상자 안에서 움직이는 공 제어 */
public class BounceController {
    private MovingBall ball;  // 공 객체 (Model)
    private AnimationWriter writer; // 애니메이션 객체 (Output-View)

    /** Constructor BounceController 컨트롤러 초기화
     * @param b - 공 객체 (Model)
     * @param w - 애니메이션 객체 (Output-View)  */
    public BounceController(MovingBall b, AnimationWriter w) {
        ball = b;
        writer = w;
    }

    /** runAnimation - 내부 시계를 활용하여 애니메이션 구동 */
    public void runAnimation() {
        int time_unit = 1;    // 애니메이션 스텝의 시간 단위
        int painting_delay = 20;  // 다시 그리기 사이의 지연 시간 간격
        while (true) {
            delay(painting_delay);
            ball.move(time_unit);
            System.out.println(ball.x_pos() + ", " + ball.y_pos());
            writer.repaint();
        }
    }

    /** delay - how_long millisecond 동안 실행 정지  */
    private void delay(int how_long) {
        try { Thread.sleep(how_long); }
        catch (InterruptedException e) { }
    }
}
import java.awt.*;

/** BounceTheBall - 애니메이션 객체를 생성하고 공 운동 시작 */
public class BounceTheBall {
    public static void main(String[] args) {
        // 모델 객체 생성
        int box_size = 200;
        int ball_radius = 6;
        Box box = new Box(box_size);
        // 공을 상자의 적절한 위치에 둠
        MovingBall ball = new MovingBall((int)(box_size / 3.0),
                                         (int)(box_size / 5.0),
                                         ball_radius, box);
        BallWriter ball_writer = new BallWriter(ball, Color.RED);
        BoxWriter box_writer  = new BoxWriter(box);
        AnimationWriter writer = new AnimationWriter(box_writer, ball_writer, box_size);
        // 컨트롤러 객체를 생성하고 애니메이션 시작
        new BounceController(ball, writer).runAnimation();
    }
}

5-5. 실습 - 상자 속 공 굴리기 애니메이션 (확장)

실습#1. 파란 공을 하나 추가

  • 두 공은 서로 다른 장소에서 다른 방향으로 출발한다.
  • 두 공이 움직이는 속도는 같다.
  • 공이 충돌해도 그대로 통과한다.

실습#2. 츙돌시 진로 수정

  • 두 공이 충돌하면, 둘 다 진행 방향을 역방향으로 바꾼다.

실습#3. 장애물 설치

  • 중앙에 다음과 같은 모양의 적당한 길이의 장애물을 설치한다.

  • 공이 이 장애물 위면 또는 아래 면을 만나면 y축 진행 방향을 바꾼다.

숙제 - 데스크 탑 용 시계 완성 (제출 마감: 10월 13일 목요일 수업 시작 전)

4주차에 숙제로 완성했던 아날로그 시계를 이번에는 스스로 작동하도록 개선해보자.

  • 아날로그 시계의 분침과 시침은 그대로 쓰고, 초침은 동심원을 활용한다. 시계판의 지름을 60등분하여 1초에는 동심원의 지름이 1/60, 2초에는 2/60, …, 계속하여 0초가 되면 동심원이 시계판을 가득채우고, 다시 1초부터 동일하게 반복한다. 동심원은 1초 마다 커져야 하고, 분침은 1분에 한칸씩 움직이고, 시침은 분침이 움직일 때 비율에 맞추어 움직여야 한다.

  • TIME IS GOLD가 있었던 부분은 디지털 시계로 대치한다. 디지털 시계는 dd:dd:dd와 같은 형식으로 시,분,초를 각각 두 자리에 맞추어야 한다. 다시말해 10 미만의 경우 앞에 반드시 0을 붙여야 한다. 디지털 시계도 아날로그 시계와 마찬가지로 스스로 작동해야 한다.

  • 작동하는 시계를 캡쳐한 모습은 다음과 같다.

  • 시계 디자인은 위 그림과 같을 필요가 없고 각자 자유에 맡긴다.

  • 시계 작동 사례 동영상

  • 스타터 클래스 Clock과 컨트롤러 클래스 ClockController는 이미 아래와 같이 주어졌다. 지난 숙제로 완성한 ClockWriter 클래스를 위의 요구사항에 맞게 수정하여 애플리케이션을 완성한다. 모델의 역할은 너무 단순하여 굳이 클래스를 따로 두지 않는다.

public class ClockController {
    
    /** delay - how_long millisecond 동안 실행 정지  */
    private void delay(int how_long) { 
        try { Thread.sleep(how_long); }
        catch (InterruptedException e) { }
    }
    
    public void switchOn(ClockWriter clock) {
        while (true) { 
            delay(20);
            clock.repaint();
        }
    }

}
public class Clock {
    
    public static void main(String[] args) {
        ClockWriter clock = new ClockWriter(240);
        new ClockController().switchOn(clock);
    }

}