자바 스터디 1회차 준비

18 minute read

5월 23일 부터 시작할 자바 스터디를 준비 하면서 참고 자료용으로 글을 작성하고자 한다.

먼저 남궁성 저자 - 자바의 정석을 기반으로 하는 스터디이며, 이후 스프링까지 이어질 예정이다.

  • 1주차 목차
    • 자바
      • 자바의 역사 및 특징
      • JVM
      • JDK, JRE 의 차이점
      • Compile, ByteCode
      • Compile Time, Runtime
    • 변수
      • 타입 (Primitive Type, Reference Type)
      • 변수 선언과 저장
      • 상수, 리터럴
      • swap
      • printf
      • Overflow
      • 형변환
    • 연산자
      • 연산자의 종류
      • 연산자의 우선순위
      • 증감 연산자
      • 논리 연산자, 논리 부정연산자
      • 조건 연산자, 삼항 연산자
      • 비트 연산자, 시프트 연산자
    • 제어문
      • 조건문
        • if문
        • if-else문
        • if-else if문
        • 중첩 if문
        • switch문
        • JDK 12 이후 개선된 switch문
      • 반복문
        • for문
        • for-each문
        • while문
        • do-while문
        • break문
        • 이름붙은 반복문
        • continue문

1. 자바

1-1) 자바의 역사 및 특징

자바란 1996년 James Gosling을 필두로 Sun MicroSystems 사에서 개발한 언어로, 객체지향 프로그래밍 언어이다.
현재는 Oracle사에서 인수해 Oracle의 제품이다.

자바의 특징으로는 앞서말한 객체지향이 있다.
또한 JVM이 존재하기 때문에 운영체제에 독립적이다.
이 말은 맥 환경에서 작성한 자바 코드를 윈도우 혹은 리눅스에서 실행하는데 전혀 문제가 없다는 의미이다.

Garbage Collection이 존재한다.
C와 같은 언어에선 객체가 생성되면 메모리위에 올라가고,
직접 해제를 하지 않는다면 프로그램 종료 시 까지 존재한다.
자바는 Garbage Collector에 의해 사용되지 않는 메모리를 자동으로 체크하고 반환한다.

멀티쓰레드를 지원하는 언어이다.
비교적 구현이 쉬운 편에 속한다.


1-2) JVM

JVM이란 Java Virtual Machine의 약자로,
한글로 직역하자면 자바 가상 컴퓨터이다.

error

위 그림은 일반적인 Application이다.
Application에서 OS를 거치고 바로 Computer로 전달된다.

error

반연 Java Application이다.
JVM을 거치면서 Compile 과정을 거치기 때문에 운영체제에 독립적일 수 있다.


1-3) JDK, JRE 차이점

JRE란 Java Application을 실행 시킬 수 있는 프로그램이다.
만약 자바로 작성된 프로그램을 실행 시키기 위해선 JRE가 필수적으로 설치되어 있어야 한다.

JDK란 개발을 하기 위해 필요한 개발도구를 포함 + JRE 이다.

대표적을 javac, javap, javadoc, jconsole 등이 있다.

JDK를 설치하면 JRE를 포함하게 된다.

JDK 11 이후 버전부터는 두 가지가 통합이 된다.


1-4) Compile, Byte Code

Compiler , Interpreter

컴파일을 공부하기 전 먼저 컴파일러와, 인터프리터에 대해 알아본다.

컴파일을 하기위한 도구(프로그램)으론 컴파일러와, 인터프리터 두가지가 있다.

  1. Compiler
    프로그램 Runtime 이전에 컴퓨터가 알아들을 수 있는 기계어로 번역을 한다.
    이때 자바는 일반적인 컴퓨터가 아닌 JVM이 해석할 수 있는 기계어인 ByteCode로 변환을 한다.
    실행 시 전체 번역을 하고 실행하기 때문에 번역속도가 상대적으로 느리지만, 실행 후 다시 번역을 하지 않기 때문에 실행속도는 빠르다.

  2. Interpreter
    소스코드를 한 번에 번역하는 Compiler와는 다르게 줄 단위로 번역을 한다.
    한 줄씩 번역을 하기 때문에 번역속도가 빠른 대신 실행속도가 상대적으로 느리다.
    대표적인 인터프리터 언어론 파이썬이 있는데,
    Jupiter NoteBook IDE를 사용해보면 줄 단위로 코드를 실행 할 수 있는데, 인터프리터 언어가 이러한 특징을 가지기 때문이다.

  3. Java 자바는 이 둘의 장점을 모두 가지기 위해 두 가지를 모두 사용한다.
    JVM이 해석 가능한 Byte Code를 만드는 Compiler와 이를 기계어로 번역하는 Interpreter가 존재한다.

IDE를 사용하기 전 Terminal을 사용해 Compile을 직접해본다.

step 1 java File 생성

error

error

vim을 사용해 java파일을 만든다.
이때 주의할 점은 파일명과 class명은 동일해야 한다. (대소문자 포함)

step 2 Compile

error

컴파일을 한다.
이때 사용하는 프로그램은 javac 이다. 주의할 점은 파일 확장자까지 입력을 해야된다.

컴파일 후 동명의 class파일이 생성된다.

step 3 실행

error

실행을 하기 위한 프로그램은 java이다.
컴파일과는 다르게 확장자를 제거한채로 실행을 한다.
이때 JDK11 버전 이상부터는 확장자를 포함해도 실행이 된다.

해당 명령어를 입력하면 Hello World! 라는 문구가 출력된다.

이때 실행되는 것은 java 파일이 아닌 class 파일이라는 점을 염두해 두자.

step 4 Byte Code

error

앞서 말했듯이 자바는 Compiler가 java 소스코드를 Byte Code로 변역을 한다.

이때 사용되는 프로그램은 javap이다. -c 옵션을 줘 바이트 코드를 볼 수 있다.

바이트코드를 뜯어볼일은 거의 없지만, 코드를 작성하다보면 상당히 내부적으로 행해지는 코드들이 많은데, 이럴 때 파악할때 도움이 될 수 있으니 참고하자.

먼저 클래스 생성 후 작성하지 않은 몇개의 코드가 생성된다.
public CompileTest(); 는 생성자이다.
자바는 기본적으로 클래스 당 하나 이상의 생성자가 필요한데, 작성을 하지 않으면 기본 생성자가 생성된다.

생성자에 대해선 객체지향 (클래스) 챕터에서 다시 공부한다.

또한 거의 마지막 줄에 java/io/PrintStream.println 메서드가 호출된 것을 알 수 있다.


1-5) Compile Time, Runtime

Compile Time은 소스코드가 바이트코드로 변환되어 실행가능한 시기를 의미한다.
이때 발생하는 에러는 Compile Error이며, IDE가 감지할 수 있다.

대표적으로 문법에러가 있다.

Runtime은 컴파일 과정을 거치고, 프로그램이 실행되는 시기를 의미한다.
이때 발생하는 에러는 Runtime Error이며, 의도치 않은 상황으로 발생되는 에러이다.

대표적으로 숫자를 0으로 나누려 한다거나, Null을 참조하려 할 때 발생한다.


2. 변수

변수란?

하나의 값을 저장 할 수 있는 저장공간이다.

2-1) 타입 (Primitive Type, Reference Type)

자바엔 타입이 존재한다.
Primitive Type (기본형) 8개와, Reference Type (참조형)이 있다.

먼저 Primitive Type이다.

  1 byte 2 byte 4 byte 8 byte
논리형 boolean      
문자형   char    
정수형 byte short int long
실수형     float double

정수형 중엔 int가 기본이며, 실수형 중엔 double이 기본이다.

자료형 기본 값 범위 bit byte
boolean false false, true 8 1
char \u0000 ‘\u0000’ ~ ‘\uffff’ 16 2
byte 0 -128 ~ 127 8 1
short 0 -32,768 ~ 32,767 16 2
int 0 -2,147,483,648 ~ 2,147,483,647 32 4
long 0L -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 64 8
float 0.0F 1.4E-45 ~ 3.4E38 32 4
double 0.0 4.0E=324 ~ 1.8E3-9 64 8

굳이 범위나, 비트, 바이트수를 외울 필요는 없고, 기본적으로 정수형은 20억 이상의 경우 long을 사용하고, 정밀한 소수점 계산은 double을 사용한다 정도만 알아도 무관한다.

가장 중요한 포인트는 변수에 실제 값을 저장한다는 점이다.

다음은 Reference Type이다.
참조형 타입은 기본형 타입 8개를 제외한 모든 타입을 말한다.
기본형 타입과 다르게 실제 값이 아닌 값이 저장된 주소를 저장한다.

문자를 표현하는 String이 대표적인 참조형 타입이다.
헷갈린다면 자바에서는 변수 뒤에 .을 붙여 확인할 수 있다.

error

error

참조형 타입은 클래스를 통해 만들어지기 때문에 클래스에 정의된 메서드나, 변수를 사용할 수 있다.

추가적으로 기본형은 타입명의 첫 글자가 소문자, 참조형은 대문자로 사용되는 Convention이 있다.

나중에 다시 공부하겠지만, Integer, Long, Float 등의 이름으로 기본형 타입을 감싸 참조형 타입으로 만든 Wrapper 클래스가 존재한다는 점을 참고하자.


2-2) 변수 선언과 저장

변수를 사용하기 위해선 선언과 저장이 필요하다.
선언은 저장공간을 만들고, 저장은 그 공간에 값을 저장한다.
아래 예시는 integer 타입의 변수 number에 5를 저장한 것이다.

주의 할 점은 변수는 단 하나의 값만 저장할 수 있으므로, 이미 값이 저장된 변수에 새로운 값을 저장하면 기존 값은 지워지고 새로운 값만 남는다.

// 선언
int number;

// 저장
number = 5;

// 한줄로 표현
int num = 10;

// 새로운 값 저장
num = 20;

2-3) 상수, 리터럴

일반적으로 상수는 변하지 않는 값을 의미한다.
그러나 프로그래밍에서의 상수는 변하지 않는 값을 저장하는 공간으로 정의된다.
자바에서는 final 키워드를 사용함으로써 상수를 사용할 수 있다.
참고사항으로 자바스크립트에서는 const를 사용한다.

리터럴은 변수 혹은 상수의 값을 의미한다.

error


2-4) swap

두개의 변수가 있다.
만약 두 변수의 값을 바꾸려면 어떻게 해야할까?

int x = 10;
int y = 20;

x에 y를 저장하고, y를 x에 저장해서는 두 변수의 값은 모두 10이 될 것 이다.

그렇기 때문에 임시로 사용할 하나의 변수를 만들어서 변경하도록 한다.

int x = 10;
int y = 20;
int temp;

temp = x;   // temp = 10
x = y;      // x    = 20
y = temp;   // y    = 10

2-5) printf

자바에서 출력은 일반적으로 println 메서드를 사용한다.
하지만 println은 변수의 값을 그대로 출력하므로, 다른 형태로 출력할 수 없다.

만약 값을 알지 못하는 실수형 변수를 사용해야 한다.
이때 소수점 자리수를 항상 두번째 자리 까지만 출력하고 싶다면 어떻게 할까?

public static void main(String[] args) {
        double d = 1.234;
        double d2 = 5.67890;
        System.out.printf("d = %3.2f%n", d);
        System.out.printf("d2 = %3.2f%n", d2);
}

// 1.23
// 5.68

println 메서드로는 당연히
d = 1.234 d2 = 5.67890이 나왔을 테지만
printf 메서드를 사용함으로써 전체 3자리 중 마지막 자리를 반올림해 소수점 2자리까지만을 출력하도록 했다.

2진수, 16진수 표현 등등.. 이보다 훨씬 다양한 경우에 사용할 수 있다.


2-6) Overflow

Overflow는 한글로 ‘넘치다’ 라는 뜻이다.
무엇이 넘칠까?

int 타입을 예시로 들어보겠다.
int는 -2147483648 ~ 2147483647 까지를 표현할 수 있는 타입이다.
만약 2147483647에 1을 더하면 2147483648이 될까?
만약 -2147483648에 1을 빼면 -2147483649가 될까?

표현할 수 있는 범위를 넘겼기 때문에 위의 예시는 전혀 엉뚱한 값이 나온다.

error

먼저 Integer.MAX_VALUE는 int의 최댓값, MIN_VALUE는 최솟값이다.
최댓값에서 1을 더하니 최솟값이 됬고, 최솟값에서 1을 빼니 최댓값이 되었다.

마치 시계의 시침이 23시에서 2시간이 지났다고 25시라고 하지 않는 것과 동일하다.

Overflow는 계산의 큰 영향을 미칠 수 있으니, 항상 필요한 타입을 잘 선택해서 사용해야 한다.

21억 이상의 큰 수를 다룰 경우라면 int보단 long을 사용해야 할 것이고,
사용할 일이 거의 없겠지만 long의 범위보다 큰 수를 사용할 땐, Big Integer를 사용한다.

마찬가지로 소수점 아래 9자리를 표현할 수 있는 float 보다 정확한 실수형은 소수점 아래 18자리까지 표현할 수 있는 double이다.
돈 계산과 같이 이보다 더 정확한 계산을 해야하는 경우 Big Decimal을 사용한다.

이 둘은 String을 사용하기 때문에 정확한 계산이 가능하다.

그러나 정확하다고 항상 사용한다면 메모리를 많이 차지하므로 비효율적인 코드가 될 수 있으니 잘 판단하고 사용하도록 한다.


2-7) 형변환

형변환은 두 가지가 종류가 있다.
Type Promotion (자동 형변환) , Casting (강제 형변환)

자동 형변환은 작은 단위의 타입을 큰 단위의 타입으로 변환하는 것을 말한다.

이 경우 Overflow와 같은 데이터 손실/변형이 일어나지 않으므로 자동으로 형변환을 할 수 있다.

public static void main(String[]args){
    byte a = 10;
    int b = a;
    System.out.println(b); // 10
}

강제 형변환은 반대로 큰 데이터 타입에서 작은 데이터 타입으로 변환하는 것을 말한다.

이 경우 데이터 손실/변형이 일어날 가능성이 매우 높으므로 강제로 형변환을 해줘야한다.

아래는 byte의 범위 안에서 강제 형변환을 한 경우이다.
이상없이 출력된다.

public static void main(String[]args){
    int a = 10;
    byte b = (byte)a;
    System.out.println(b);  // 10       
}

다음은 byte의 범위를 벗어난 채로 강제 형변환을 하는 경우이다.
Overflow가 발생한다.

public static void main(String[]args){
    int a = 128;
    byte b = (byte)a;
    System.out.println(b);  // -128       
}

또한 강제 형변환을 해야되는 경우 자동 형변환을 하려고 하면 컴파일 에러가 발생한다.

public static void main(String[]args){
    int a = 128;
    byte b = a;     // 이미 여기서부터 컴파일 에러
    System.out.println(b);       
}

문자 -> 숫자, 숫자 -> 문자, 문자열 -> 숫자, 숫자 -> 문자열 등의 형변환은 다음과 같다.

public static void main(String[] args) {
        
        int i = 3;
        char c = (char) (i + '0');
        System.out.println("int to char : c = " + c);

        char c2 = '5';
        int i2 = c2 - '0';
        System.out.println("char to int : i2 = " + i2);

        int i3 = 10;
        String s = i3 + "";
        System.out.println("int to String : s = " + s);

        String s2 = "100";
        int i4 = Integer.parseInt(s2);
        System.out.println("String to int : i4 = " + i4);

        String s3 = "2";
        char c3 = s3.charAt(0);
        System.out.println("String to char : c3 = " + c3);

        char c4 = '9';
        String s4 = c4 + "";
        System.out.println("char to String : s4 = " + s4);

}


        int to char : c = 3
        char to int : i2 = 5
        int to String : s = 10
        String to int : i4 = 100
        String to char : c3 = 2
        char to String : s4 = 9


3. 연산자

연산자란?
연산이란 수나 식을 일정한 규칙에 따라 계산하는 것이며, 연산을 수행하는 기호를 연산자라고 한다.

3-1) 연산자의 종류

연산자의 종류는 단항연산자 : 증감연산자, 부호연산자, 부정연산자, cast연산자
이항연산자 : 산술연산자, 비트연산자, 쉬프트연산자, 비교연산자, 논리연산자, 대입연산자

굉장히 종류가 많다.
굳이 외우지 않아도 자주 사용하는 것들은 익혀지니 걱정하지 말자.


3-2) 연산자의 우선순위

사칙연산에도 우선순위가 있듯이 프로그래밍 연산자에도 우선순위가 있다.

단항연산자 > 산술연산자 > 비교연산자 > 논리연산자 > 삼항연산자 > 대입연산자


3-3) 증감 연산자

증감 연산자는 ++와 –가 있다.
피연산자의 값을 1 증가 또는 감소 시키는 연산자이다.
증감 연산자는 피연산자의 왼쪽, 오른쪽 모두 사용가능하며,
왼쪽에 붙으면 전위형, 오른쪽에 위치하면 후위형이라고 한다.

전위형

public static void main(String[] args) {
    int i = 0;
    ++i;
    System.out.println("i = " + i); // i = 1
    --i;
    System.out.println("i = " + i); // i = 0
}

후위형

public static void main(String[] args) {
    int i = 0;
    i++;
    System.out.println("i = " + i); // i = 1
    i--;
    System.out.println("i = " + i); // i = 0
}

두 개의 차이점은 무엇일까?

전위형은 값이 참조되기 “전에” 증감을 하고, 후위형은 참조된 “후에” 증감을 한다.

public static void main(String[] args) {
    int i = 5, j = 0;
    j = i++;

    int k = 5, n = 0;
    n = ++k;
}

j는 6일까?
j는 5이다.
j = i++의 식을 풀어 쓰자면 다음과 같다.

public static void main(String[] args) {
    int i = 5, j = 0;
    j = i;
    i++;
}

그렇다면 n는 6일까? n = ++k를 풀어쓰면 다음과 같다.

public static void main(String[] args) {
    int k = 5, n = 0;
    ++k;
    n = k;
}

이것이 참조 전 후에 증감을 한다는 의미이다.


3-4) 논리 연산자, 논리 부정연산자

논리 연산자는 둘 이상의 조건이 결합된 경우에 사용된다.
&& : AND
|| : OR
반환 타입은 boolean이다.

public static void main(String[] args) {
    int number = 20;
    int number2 = 10;
    int number3 = 0;
    
    System.out.println(number > number2 && number > number3); // true

    System.out.println(number > number2 && number < number3); // false

    System.out.println(number > number2 || number < number3); // true

    System.out.println(number < number2 || number < number3); // false
}

AND 연산자는 모든 조건이 true일때 true반환이며,
OR 연산자는 조건 중 하나라도 true이면 true를 반환한다.

논리 부정연산자는 true를 false로, false를 true로 결과를 반대로 변경한다.
1 > 100 이라는 조건을 부정하기 때문에 괄호를 쳐야한다.

public static void main(String[] args) {
    System.out.println(1 > 100); // false
    System.out.println(!(1 > 100)); // true
}

3-5) 조건 연산자, 삼항 연산자

조건 연산자는 if-else문을 어느정도 대체 할 수 있다.
if-else가 너무 많으면 지저분한 코드가 될 수 있는데,
간단한 조건이라면 아래와 같이 사용할 수 있다.

public static void main(String[] args) {
    int num = 10;
    int num2 = 100;

    if (num > num2) {
        System.out.println(num);  
    } else {
        System.out.println(num2);
    }

    System.out.println(num > num2 ? num : num2);
}

삼항 연산자는 아래와 같이 사용도 가능하다.

public static void main(String[] args) {
        int num = 10;
        int num2 = 100;
        int num3 = 1000;

        System.out.println(num > num2 ? (num > num3 ? num : num3) : (num2 > num3 ? num2 : num3));

        // 1000
}

더 복잡하게도 사용 가능하지만, 보다시피 이 정도 만으로도 가독성이 매우 떨어진다.
이럴 경우 마음 편하게 if-else를 사용하자.


3-6) 비트 연산자, 시프트 연산자

이 부분은 과거 정리한 자료를 참고하자.

WhiteShip Live Study 3주차. 연산자


4. 제어문

코드의 실행 흐름은 항상 위에서 아래로 한 문장씩 진행된다.
조건에 따라 문장을 건너뛰거나, 반복 수행을 해야될 경우가 있다.
이럴 경우 사용되는 것이 제어문이다.

제어문은 조건문과, 반복문이 있다.

4-1) 조건문

조건문은 조건에 따라 다른 문장이 수행되도록 한다.


4-1-1) if문

조건문의 가장 기본적인 문법이다.
조건식과 괄호로 이루어져있다.
조건식이 true일 경우 괄호 안의 문장을 수행시킨다.
조건식의 결과가 boolean이 아닐 경우 컴파일 에러가 발생한다.

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

    if (score > 60) {
        System.out.println("합격입니다.");
    }
}
  1. 변수 score는 80이 저장되어있다.
  2. 조건문은 score가 60을 초과할 경우 “합격입니다.”를 출력한다.
  3. 조건문은 true가 되어 “합격입니다.”를 출력한다.

위와 같이 조건문 블럭안의 문장이 한 줄뿐이라면, 괄호를 생략할 수 있다.


4-1-2) if-else문

if-else문은 if문의 변형된 형태로, if문의 조건식이 true일때 실행되는 반면
else문은 if문의 조건식이 false일때 실행된다.

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

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

if문 예제에서 사용된 코드이다.
score를 30으로 내렸고, else문을 추가했다.
이 경우 if문의 조건식은 false가 되어 else문의 블럭으로 들어가 문장을 수행한다.
콘솔엔 “불합격입니다.” 가 출력된다.


4-1-3) if-else if문

if-else는 두 가지 상황밖에 연출하지 못한다.
true이거나, false이거나.

조건이 여러개일 경우 if-else if문을 사용할 수 있다.

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

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

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

점수에 따라 학점을 부여한다.
위 예제는 score가 70이니 1~2번 조건에 모두 false가 되고, 3번 조건에서 true가 되어 grade에는 C가 저장된다.
만약 score가 70보다 작은 수였다면 모든 조건에 false가 되어 else문의 블럭을 수행할 것이다.

if문의 else는 필수가 아니며,
else if의 경우 true를 만나면 블럭안의 문장을 수행하고, 종료된다.
if문 부터 else문까지 하나의 덩어리라고 생각하면 된다.


4-1-4) 중첩 if문

말그대로 if문을 중첩해서 사용할 수 있다.

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

    if (score >= 90) {
        if (score >= 95) {
            grade = "A+";
        } else {
            grade = "A-";
        }
    } else if (score >= 80) {
        if (score >= 85) {
            grade = "B+";
        } else {
            grade = "B-";
        }
    } else if (score >= 70) {
        if (score >= 75) {
            grade = "C+";
        } else {
            grade = "C-";
        }
    } else {
        grade = "F";
    }

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

조건이 true 혹은 false일때 한번 더 세부적으로 조건을 정할 수 있다.
위 예제의 +/- 학점과 같이 말이다.


4-1-5) switch문

조건이 많은 경우라면, else if문이 많이 사용될 것이고, 코드가 지저분해져 가독성이 떨어진다.
게다가 여러개의 조건식을 체크해야 하기 때문에 처리속도가 많이 걸린다.
이때 사용할 수 있는 문법이 switch문이다.
switch문은 단 하나의 조건식을 두고, 해당하는 case를 찾는다.

예시로 확인해보자.

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

    String grade = "";

    switch (score) {
        case 90:
            grade = "A";
            break;
        case 80:
            grade = "B";
            break;
        case 70:
            grade = "C";
            break;
        case 60:
            grade = "D";
            break;
        default:
            grade = "F";
    }

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

if문에 비해 확실히 간결해보인다.
여기서 주의할 점이 몇 가지 있다.

  1. 조건식의 결과는 정수 또는 문자열이어야한다.
  2. case문의 값은 상수, 문자열만 가능하며, 중복은 되지않는다. (변수, 실수는 불가능하다.)
  3. default문은 if문의 else와 같은 기능을 한다고 봐도 무방하다.
  4. case문 마지막에 break문이 선언되있다.
    이 부분이 굉장히 중요한데, break가 선언되어 있지 않다면, case문이 종료되지않고, 조건과 관계없이 다음 case문을 실행한다.
public static void main(String[] args) {
    int score = 80;

    String grade = "";

    switch (score) {
        case 90:
            grade = "A";
        case 80:
            grade = "B";
        case 70:
            grade = "C";
        case 60:
            grade = "D";
        default:
            grade = "F";
    }

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

score가 80이니 B 학점이 될것 같으나
F학점이 출력된다.
break가 없기 때문에 grade에는 B가 저장된 후, case 70이 수행되어 C가 저장되고, D가 저장되고, 마지막으로 default문이 실행되어 F가 저장되기 때문이다.

위 문제로써 switch문은 상당히 허술해보인다.


4-1-6) JDK 12 이후 개선된 switch문

이러한 문제를 JDK 14을 통해 개선한다.
(JDK 12, JDK 13에서 Preview로 공개했고, 실제 적용은 JDK 14 부터이다.)


먼저 break문이 제거되었다.

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

    String grade = "";

    switch (score) {
        case 90 -> grade = "A";
        case 80 -> grade = "B";
        case 70 -> grade = "C";
        case 60 -> grade = "D";
        default -> grade = "F";
    }

    System.out.println(grade);
}

break문을 제거하기 위해 화살표 문법을 쓴다.
사실상 이 문법은 case문의 문장이 한개이기 때문에 블럭이 생략되었지만,
이 괄호 블럭을 통해 case문의 영역을 지정해줬다고 볼 수 있다.

그리고 두 번째로 상당히 재밌는 문법이 추가된다.

public static void main(String[]args){
    System.out.println(switchYield(70));
}

public static String switchYield(int score) {
    String returnSwitch = switch (score) {
        case 90 -> "A";
        case 80 -> { yield "B"; }
        default -> { 
            System.out.println("80점 미만은 F학점!");
            yield "F"; 
        }
    };

    return returnSwitch;
}

바로 switch문에 리턴타입이 생겼다는 것이다.
이는 switch문을 변수에 담을 수 있다는 의미이기도 한다.

switchYield 메서드를 먼저 보자.
case는 총 3개있는데 조금씩 다른점이 보인다.
case 90은 화살표 문법을 사용해 returnSwitch 변수에 A를 저장한다.
case 80은 yield 라는 문법이 사용된다.
별 다른것없이 returnSwitch 변수에 B를 저장하는 것인데, 조금 다른점이 있다.

먼저 default를 보자.
블럭 안에 두개의 문장이 선언되어있다.

여기서 리턴을 한다는 것은, 해당 변수에 값이 저장이 되어야 하는데,
만약 System.out.println() 과 같은 void 메서드를 사용할 경우
변수에 값이 담을 수가 없다.
그렇기 때문에 컴파일 에러를 발생시켜 코드에 문제가 있음을 미리 알려주는데,
case 블럭에 한 줄이 넘는 코드가 있을 경우,
case 90 처럼 값을 저장하지 못한다.

이때 사용하는 예약어가 바로 yield이다.


4-2) 반복문

반복문은 어떤 작업이 반복적으로 수행되도록 한다.
반복문의 종류로는 for문, while문, while문의 변형 형태인 do-while문이 있다.
일반적으로 for문과 while문의 구조가 흡사해 어떤 상황에서도 서로 변환이 가능하다.


4-2-1) for문

error

for문의 기본 구조이다.
어떤 구조인지는 이 아래 내용을 보면 대충 감이 잡힐 것이다.

for (초기화; 조건문; 증감식) {}

먼저 변수를 초기화한다. 이때 변수명은 일반적으로 i를 사용한다.
그리고 몇번의 반복을 할 것인지 조건식을 작성한다.
위의 경우 i가 10 이상까지 라는 조건을 사용했다.
true인 동안 반복되며, false가 되면 종료된다.
그리고 마지막으로 증감식을 사용한다.
i가 1씩 증가하도록 함으로써 반복 후에 조건식을 false 상태로 만들 수 있게하기 위해서이다.

위 세 가지 요소는 생략 가능하며 세 가지 모두 생략한다면, 무한루프가 된다.

for (;;) {
    System.out.println("무한 루프!");
}

if문과 마찬가지로 중첩이 가능하다.


4-2-2) for-each문

자바에서 for-each라는 키워드는 없다.
그러나 문법은 존재하는데, 보통 다른 언어에서 for-each라고 하기때문에 자바에서도 for-each문이라고 한다.

일반적인 for문과는 다르게 Primitive Type은 사용불가하며, Reference Type으로 사용해야 한다.
이유는 for문은 증감식이 있지만, for-each는 증감식 대신 iterator를 사용해서 반복을 하기 때문이다.

문법은 아래와 같다.

for (사용  변수  : 레퍼런스) {}
public static void main(String[] args) {
    String[] arr = { "A", "B", "C", "D", "E" };
    
    for (String str : arr) {
        System.out.println(str);
    }
}	

맨 첫줄은 String형 배열을 선언 + 저장 한 것이다.

for문보다 성능이 좋지만, 아쉽게도 몇 가지 단점이 있다.

위 예제 기준으로, str은 arr 각각의 인덱스에 해당하는 값이다.

이 주소 값은 임시로 복사된 값이라, for-each문에서 아무리 수정을 해도 원본은 변하지 않는다.
즉 for-each문을 통해 원본수정은 불가하다.

또한 배열을 거꾸로 반복할 경우가 꽤 많은데, for-each문에선 불가하다.

그리고 시작하면 무조건 마지막 요소까지 반복하기 때문에 중간에 break문을 사용하려면 코드가 지저분해진다.

public static void main(String[] args) {
    String[] arr = { "A", "B", "C", "D", "E" };

    // 기존 for문이라면 조건식으로 조정
    for (int i = 0; i < 3; i++) {
        System.out.println(arr[i]);
    }

    System.out.println();

    // 별도로 index를 관리할 변수가 필요함    
    int idx = 0;
    
    for (String str : arr) {
        if (++idx > 3) {
            break;
        }
        
        System.out.println(str);
    }
}

둘의 결과는 동일하지만 이러한 상황에선 오히려 for-each의 가독성이 떨어진다.

성능이 좋은건 맞지만 가독성이 떨어지는 건 위험할 수 있다.
필요한 상황에 잘 맞춰 사용하도록 한다.


4-2-3) while문

int i = 1;

while (i <= 10) {
    System.out.println(i);
    i++;
}

while문의 문법이며, 맨 처음 for문 예제를 while문으로 변경한 경우이다.

while (조건식) 의 단순한 구조로 이루어져 있다.
마찬가지로 조건식이 true인 동안 반복이 되고 false면 종료된다.

while문으로 무한루프를 구현하려면, 조건식에 true를 사용하면 된다.
이는 false가 될 수 없기 때문이다.

while (true) {
    System.out.println("무한 루프입니다!");
}

4-2-4) do-while문

do-while문은 while문의 변형 형태로, 구조는 비슷하나, 동작 순서가 다르다.

블럭을 먼저 실행하고, 그 후 조건식을 체크하는데,
이는 블럭을 최소 한번은 수행한다는 것을 의미한다.

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int number = 0;

    do {
        System.out.print("숫자를 입력하세요 (입력값이 10 이하면 종료됩니다) : ");
        number = sc.nextInt();
    } while(number >= 10);
}

4-2-5) break문

switch문에서 잠깐 나왔던 break문에 대해 알아보자.
break문은 자신이 포함된 가장 가까운 반복문을 종료시킨다.

주로 if문을 사용해 특정 조건에 맞다면 반복문을 종료시키는 방법으로 사용된다.

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

    while (true) {
        if (i >= 100) {
            break;
        }

        sum += i++;
    }

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

위는 1~100까지의 수를 더해 sum에 저장하는 코드이다.
무한반복문이기 때문에 break문이 없다면, 메모리 릭과 같은 에러가 발생하지 않는다면 평생 종료되지 않는다.

무한반복문을 사용한다면, break문이 없어도 컴파일 에러가 아니기 때문에, 꼭 프로그램이 종료되지 않고, 계속 반복되어야 하는 프로그램이 아니라면 (ex)batch program) break문을 꼭 신경 써줘야 한다.


4-2-6) 이름붙은 반복문

break문은 가장 가까운 반복문을 종료시킨다.
그러나 간혹 중첩 반복문에서 전체를 종료시키고 싶을 때가 있을 텐데, 그럴때 반복문에 이름을 붙인 후, break, continue문에서 해당 이름으로 종료 또는 건너뛸 수 있다.


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

        InnerLoop : for (int j = 1; j < 9; j++) {
            if (i > 3) {
                break OutterLoop;
            }
            
            System.out.println("i x j = " + i * j);
        }
        System.out.println(); 
    }

}	

4-2-7) continue문

continue문은 반복문 내에서 반복문의 끝으로 이동시킨다.
만약 어떤 조건을 만나면 동작시키지 않아야 하는 코드가 있다면, continue문을 사용하자.

for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) {
        continue;
    }
    
    System.out.println(i);
}

위는 0~10까지 2의 배수를 제외하고 출력하는 예시이다.


Leave a comment