스터디/자바

[자바 스터디] 3, 4주차 : 연산자, 제어문

gmelon 2023. 1. 22. 00:16

3주차 - 연산자

산술 연산자

  • 변수와 데이터에 대한 산술 연산을 수행하는데 사용됨
연산자 수행 연산
+ 덧셈
- 뺄셈
* 곱셈
/ 나눗셈 (몫만 취함)
% Modulo 연산 (나누기 연산 후 나머지만 취함)

Overflow 가능성

  • 연산 결과가 피연산자 타입의 저장 가능 범위를 벗어나게 되면 오버플로우가 발생, 의도치 않은 값이 변수에 저장됨
    int a = 2_100_000_000;
    int b = 2_000_000_000;

    System.out.println(a + b); // -194967296
  • 따라서 연산 시, 오버플로우 발생 가능성을 염두에 두어야 함

/%

  • /는 몫만 취하고, %는 나머지만 취함
    • 즉, 6 / 4 = 1, 6 % 4 = 2.
  • 나눗셈 연산 시

/의 반환 타입

  • a / b 에서 ab가 모두 정수이면 반환 값도 정수
  • ab 중 하나라도 floating-point number 이면 반환 값도 floating-point number
  • 예제
    9 / 2 == 4
    9 / 2.0 == 4.5
    9.0 / 2 == 4.5
    9.0 / 2.0 == 4.5

분모가 0인 나눗셈

  • 정수 연산의 경우 ArthmenticException 발생
  • 실수 연산의 경우
    • / -> Infinity (무한대)
    • % -> NaN(Not a Number)
    • 각각 Double.isInfinite(), Double.isNan()으로 확인해주어야 함
  • 실수 계산 시 분모가 0임을 확인하는 방법 (실수 비교)
    • 일정 임계치를 범위로 두고 0에 가까운 값인지 확인 (참고자료)

비트 연산자

연산자 수행 연산
| Bitwise OR
& Bitwise AND
^ Bitwise exclusive OR (XOR)
~ Bitwise Complement
<< Left Shift
>> (Signed) Right Shift
>>> Unsigned Right Shift
  • 예시
    // Bitwise OR
    0 | 0 == 0
    0 | 1 == 1
    1 | 0 == 1
    1 | 1 == 1

    // Bitwise AND
    0 | 0 == 10
    0 | 1 == 0
    1 | 0 == 0
    1 | 1 == 1

    // Bitwise XOR
    0 | 0 == 1
    0 | 1 == 0
    1 | 0 == 0
    1 | 1 == 1

    // Bitwise Complement
    ~0 == 1
    ~1 == 0
    ~1001 == 0110

    // Left Shift
    // 빈 공간은 0으로 채워짐
    // 좌측으로 밀린 비트는 타입에 따라 공간이 없다면 버려짐
    2 << 2 == 8 // 0010(2) << 2 == 1000(8)

    // Signed Right Shift
    // 우측으로 비트를 미는데, 부호 비트 (+ == 0, - == 1)을 좌측 빈 공간에 채운다
    int number1 = 8;
    int number2 = -8;

    number1 >> 2 == 2 // 좌측 빈 비트가 0으로 채워잠
    number2 >> 2 == -2 // 좌측 빈 비트가 1로 채워짐

    // Unsigned Right Shift
    // 무조건 좌측 빈 비트가 0으로 채워짐

    int number1 = 8;
    int number2 = -8;

    number1 >>> 2 == 2
    number2 >>> 2 == 1073741822

관계(비교) 연산자

연산자 수행 연산
== Is Equal To
!= Not Equal To
> Greater Than
< Less Than
>= Greater Than or Equal To
<= Less Than of Equal To

논리 연산자

연산자 수행 연산
&& Logical AND
! Logical NOT (Bitwise Complement과 다름)

Short Circuit Logical Operators

  • 논리 연산 수행 시 두번째 logical expression을 수행하기 전에 결과가 확정되면, 두번째는 수행하지 않는 것을 말함

Logical AND

  • 첫번째 논리식이 true면 두번째 논리식까지 수행해봐야 결과를 알 수 있으므로 모두 수행한다.
  • 첫번째 논리식이 false면 무조건 전체 결과가 false이므로 두번째 논리식을 아예 수행하지 않는다

Logical OR

  • 첫번째 논리식이 false면 두번째 논리식까지 수행해봐야 결과를 알 수 있으므로 모두 수행한다.
  • 첫번째 논리식이 true면 무조건 전체 결과가 true이므로 두번째 논리식을 아예 수행하지 않는다

활용방안 / 주의점

  • 예를 들어, true인지 평가하고 싶은 대상이 null일 수 있을 때 첫번째 논리식에 대상 != null를 넣고 logical AND 연산을 수행하면 대상이 null이 아닐 경우에만 두번째 논리식을 수행하게 할 수 있음
  • 하지만, 두번째 논리식에 중요한 비지니스 로직이 포함된 경우 위와 같은 예시에 의해 로직이 아예 실행되지 않을 수 있으므로 주의해야 함

instanceof

  • 객체가 어떤 클래스인지, 어떤 클래스를 상속받았는지 확인하는데 사용

문법

  • object instanceOf type
  • object가 1.type 이거나 2.type을 상속받는 클래스라면 true를 리턴
    • type을 적을 땐 .class 없이 사용 가능

예시

    new ArrayList() instanceOf List // true
    new ArrayList() instanceOf Set // false

object가 null일 경우

  • 항상 false 리턴

제네릭에선?

  • 제네릭을 사용하는 객체 자체는 타입 체크 가능 (ArrayList, List 와 같이)
  • 다만, 제네릭(ex. T) 자체는 컴파일 시 타입이 결정되므로 아래와 같이 사용할 수 없음 (컴파일 에러 발생)
    public <T> boolean sample(List<T> list) {
        if (T instanceOf Integer) return true;
        return false;
    }

assignment(=, 대입) operator

연산자 수행 연산
= (오른쪽의 피연산자를) 왼쪽의 피연산자에 대입
+= 왼쪽의 피연산자에 오른쪽 피연산자를 더한 후, 그 결과값을 〃
-= 왼쪽의 피연산자에서 오른쪽의 피연산자를 뺀 후, 그 결과값을 〃
*= 왼쪽의 피연산자에 오른쪽 피연산자를 곱한 후, 그 결과값을 〃
/= 왼쪽의 피연산자를 오른쪽의 피연산자로 나눈 후, 그 결과값을 〃
%= 왼쪽의 피연산자를 오른쪽의 피연산자로 나눈 후, 그 나머지를 〃
&= 왼쪽의 피연산자를 오른쪽의 피연산자와 비트 AND 연산 후, 그 결과값을 〃
|= 왼쪽의 피연산자를 오른쪽의 피연산자와 비트 OR 연산 후, 그 결과값을 〃
^= 왼쪽의 피연산자를 오른쪽의 피연산자와 비트 XOR 연산 후, 그 결과값을 〃
<<= 왼쪽의 피연산자를 오른쪽의 피연산자만큼 왼쪽 시프트 한 후, 그 결과값을 〃
>>= 왼쪽의 피연산자를 오른쪽의 피연산자만큼 signed 오른쪽 시프트 한 후, 그 결과값을 〃
>>>= 왼쪽의 피연산자를 오른쪽의 피연산자만큼 unsigned 오른쪽 시프트 한 후, 그 결과값을 〃
  • -==-로 입력할 경우, 음수를 왼쪽 피연산자에 단순 대입 하는 연산자로 동작하니 주의하기

화살표(->) 연산자

  • 자바 8에서 람다 표현식이 등장하며 도입된 문법
    • 익명 함수를 만들기 위해 사용
    • FuntionalInterface의 메소드를 구현할 때 주로 사용 (Consumer, Supplier, …)
    • 15주차 - 람다식이 있어 여기서는 간단히 문법에 대해서만 다룸
    // 기본 형태
    (parameters) -> { statements; }

    // 파라미터는 0개 이상
    () -> { statements; }

    // 파라미터가 1개면 ( ) 생략 가능
    parameter -> { statements; }

    // statements가 하나면 { } 생략 가능, 이때 ; 도 생략 가능
    (parameters) -> statement

    // 활용 예시
    List<State> states = ... ;

    states.forEach(state -> state.doSomething());
    states.stream()
        .map(state -> state.mapToSomething())
        .collect(...);

3항 연산자

  • if-else 문을 한 번에 작성 가능
    int a = 10;
    int b = 20;

    // 기존 if-else 코드
    if (a < b) {
        return a;
    } else {
        return b;
    }

    // 위와 동일하게 동작하는 3항 연산자 코드
    return a < b ? a : b;
  • 대부분의 경우 3항 연산자를 쓰는 것보다 if-else 그대로 사용하는게 가독성 및 디버깅에 유리함
    • else도 쓰지 않는게..!
    • 컴파일러의 구현에 따라 번역되는 바이트코드는 달라질 수 있음

단항 연산자

  • 오직 하나의 피연산자만을 받아 값을 계산하는 연산자
연산자 수행 연산
+ 양수 표현 (쓰지 않아도 기본값)
- 음수 표현, 피연산자를 음수 취급
++ 피연산자의 값을 1 증가시킴
-- 피연산자의 값을 1 감소시킴
! 피연산자의 논리값을 반전시킴

Prefix, Postfix

  • 단항 연산자를 prefix로 사용하면, 단항 연산 수행 후 주어진 연산(메소드 호출, 대입 등)을 수행
  • 단항 연산자를 postfix로 사용하면, 주어진 연산(메소드 호출, 대입 등)을 먼저 수행하고 단항 연산을 수행
  • 예시
    int var = 5;

    method(++var); // method에 6 전달
    System.out.println(var); // 6 출력

    var = 5;
    method(var++); // method에 5 전달
    System.out.println(var); // 6 출력

연산자 우선 순위

  • 할당 연산을 제외한 이항 연산자는 연산자의 왼쪽 -> 오른쪽 순으로 평가
    • 할당 연산은 연산자의 오른쪽 -> 왼쪽 순으로 평가

우선 순위 표

  • 동일한 행의 연산자들은 동일한 우선순위를 가짐 (left -> right 규칙 적용)
연산자 우선 순위
expr++ expr-- 1
++expr --expr +expr -expr ~ ! 2
* / % 3
+ - 4
<< >> >>> 5
< > <= >= instanceof 6
== != 7
& 8
^ 9
| 10
&& 11
|| 12
?: 13
= += -= *= /= %= &= ^= |= <<= >>= >>>= 14

피드백

  • 중간 값을 구할 때 int + int 는 오버플로우를 일으킬 수 있음
    • 따라서, 작은 값 + (큰 값 - 작은 값) / 2 로 구하거나,
    • (작은 값 + 큰 값) >>> 1 (비트 연산) 로도 구할 수 있음 (양수만 가능)
  • XOR 연산
    • 5(101) ^ 0(000) == 5(101), 5(101) ^ 5(101) == 0(000)
    • XOR 연산은 순서에 관계가 없으므로 오직 1번만 등장하는 숫자 찾기 등에 활용 가능하다

4주차 - 제어문

선택문 (조건문)

if-else

  • if 조건이 참인 경우에 if문 블럭 내의 내용을 수행
    if (true) {
        // 본문 내용 수행
        return value;
    }
  • if 조건이 거짓인 경우 else문 블럭 내의 내용을 수행
    • else if 문이 있을 경우 먼저 else if 조건이 참인지 평가하고 참이면 else if 블럭 내용을 수행함
    if (false) {
        // 실행 X
        return a;
    } else if (true) {
        // 실행 O
        return b;
    } else {
        // 실행 X
        return c;
    }

    if (false) {
        // 실행 X
        return a;
    } else if (false) {
        // 실행 X
        return b;
    } else {
        // 실행 O
        return c;
    }
  • if 문 블럭 내부에 또 다시 if문을 넣을 수도 있음 (중첩 if문)
    if (true) {
        if (false) {
            // 실행 X
        } else {
            // 실행
            return value;
        }
    }

switch / case

  • 조건으로 주어진 하나의 값이 어떤 값인지에 따라 많은 양의 다른 로직을 수행하고자 할 때 유용
    • 다중 if문에 비해서는 가독성이 높음
    • 각 case마다 로직 종료 후 break를 걸지 않으면 계속해서 다음 case의 로직을 수행하기 때문에 조심해야 함
    int a = getValue();
    switch(a) {
        case 0:
            doSomeThingA();
            break;
        case 1:
            doSomeThingB();
            break;
        case 2:
            doSomeThingC();
            break;
        default:
            doSomethingDefault();
        }

반복문

for문 (기본)

  • for(초기화 ; 조건문 ; 증감식) {} 의 구조를 사용함
    • 조건문이 참인 동안 내부 로직 수행 -> "증감식" 수행 을 반복함
    • 최초 동작 시 (조건문이 참일 경우) 초기화 -> 조건문 확인 -> 내부 로직 수행 -> 증감식 수행 -> 조건문 확인 -> … 순으로 동작하므로 최초 선언된 조건문이 참이면 최소 1회는 본문 내용이 수행됨
    int iterationCount = getIterationCount();
    for (int i = 0 ; i < iterationCount ; i++) {
        doSomething(i);
    }

while문

  • while (조건) {} 의 구조를 사용
    • 조건이 참일 동안 본문 로직을 반복 수행
  • 최초 조건이 거짓이면 1회도 본문 로직이 수행되지 않을 수 있음
    • 반면, 조건이 변화하지 않으면 무한 루프에 빠질 수 있으므로 주의 (루프 내에서 통해 조건이 변화해야 함)
    int i = 0;
    while (i <= 1) {
        // i == 0, i == 1인 경우 총 2회 수행
        i++;
    }

do-while문

  • while과 달리 최소 1회는 본문 로직이 수행
    • 즉, 먼저 구문을 실행한 후 마지막에 조건을 확인
    int i = 0;
    do {
        // i == 0, i == 1인 경우 총 2회 수행
        System.out.println(i); // 0, 1
        i++;
    } while (i <= 1); // 조건이 참이면 다시 위로 올라가 본문을 수행

for-each문 (향상된 for문)

  • 컬렉션의 원소를 하나씩 가져와 for문처럼 반복을 수행할 수 있음
    List<String> names = getNames();
    for (String name : names) {
        // names의 원소를 하나씩 꺼내서 해당 원소를 name에 대입하여 아래 로직을 수행
        doSomething(name);
    }

컬렉션에서의 forEach(Consumer)

  • 컬렉션에서 제공하는 메소드 forEach()에 인자로 익명 함수 (함수형 인터페이스 Consumer의 메소드를 구현) 전달하면 간단히 컬렉션 원소에 특정 로직을 반복시킬 수 있음
    List<String> names = getNames();
    names.forEach(name -> name.doSomething()); // (name::doSomething) 으로도 가능

break과 continue

  • break
    • 자신이 속한 가장 안쪽의 반복문을 중단
  • continue
    • 자신이 속한 가장 안쪽의 반복문에서 다음 반복으로 넘어감

Collection의 Iterator()

  • Iterator를 사용하는 모든 콜렉션은 iterator() 메소드를 구현
    • 이 메서드는 Iterator 인터페이스를 리턴

Iterator 인터페이스

  • Iterator는 hasNext(), next() 메소드를 필수로 갖고, remove() 메소드를 선택적으로 지원
    public interface Iterator<E> {

    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    ...
  }

hasNext(), next()

  • hasNext() - 다음번 엘리멘트가 있다면 true를 반환, 없다면 false를 반환
  • next() - 다음번 엘리멘트를 반환
    • 최초 호출 시, 첫번째 엘리멘트를 반환함

for문에서의 활용

    list = 아무아무 컬렉션;
    for (Iterator<Integer> itr = list.iterator() ; itr.hasNext() ; ) {
        // itr.next() 수행으로 인해 itr.hasNext()의 값이 바뀌게 되므로 for문의 증감식은 필요하지 않음
        doSomething(itr.next());
    }

⭐️ remove()

  • 오라클 자바 공식 문서에서는 아래와 같이 Iterator.remove가 반복 도중 원소를 삭제하는 유일하게 안전한 방법이라고 소개하고 있음

Note that Iterator.remove is the only safe way to modify a collection during iteration; the behavior is unspecified if the underlying collection is modified in any other way while the iteration is in progress.

  • 향상된 for문에서 remove를 시도할 경우
    • ConcurrentModificationException 발생
    List<String> names = getNames();
    for (String name : names) {
        if (name.equals("현상혁")) {
            names.remove(name);  // ConcurrentModificationException 💥
        }
    }
  • 기존 인덱스 기반으로 remove를 시도할 경우
    • 이 경우 삭제는 되지만, 삭제 시마다 인덱스가 조정되기 때문에 따로 인덱스를 보정해주지 않는다면 의도한대로 모든 원소를 순회할 수 없음
    • 아래 예시에서는 3을 삭제하면서 인덱스가 4를 스킵하고 바로 5를 가리키게 되므로 삭제가 정상적으로 수행되지 못함
    List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
    for (int i = 0; i < numbers.size(); i++) {
        Integer number = numbers.get(i);
            if (number.equals(3) || number.equals(4)) {
                numbers.remove(number);
            }
        }

        System.out.println(numbers); // [1, 2, 4, 5]
    }
  • Iterator.remove() 사용 시
    • 내부적으로 콜렉션이 아닌 복제본을 순회 (추가 메모리 사용) 하기 때문에 가능한 일
        List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        for (Iterator<Integer> itr = numbers.iterator(); itr.hasNext();) {
            Integer number = itr.next();
            if (number.equals(3) || number.equals(4)) {
                itr.remove(); // 컬렉션이 아닌 itr에게 remove()를 해야 함
            }
        }

        System.out.println(numbers); // [1, 2, 5]
  • Iterator.remove()의 더 간편한 사용 방법
    • 컬렉션.removeIf(Predicate)으로 순회를 직접 작성하지 않아도 원하는 원소를 삭제할 수 있다
    List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
    numbers.removeIf(number -> number.equals(3) || number.equals(4));

    System.out.println(numbers); // [1, 2, 5]

JUnit 필수 개념 정리

JUnit5 소개

  • 자바 개발자가 가장 많이 사용하는 테스팅 프레임워크
    • 자바 8 이상을 필요로 함
  • 구성
    • JUnit Platform - 테스트를 실행해주는 런쳐 제공. TestEngine API 제공.
    • Jupiter - TestEngine API 구현체, Junit 5 제공
    • Vintage - TestEngine API 구현체, Junit 3, 4 제공

의존성 추가

  • 2.2+ 버전의 스프링 부트 프로젝트 생성 시 자동으로 JUnit 5 의존성이 같이 받아짐
  • 스프링 부트 미사용 시
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>원하는_버전_작성</version>
    <scope>test</scope>
    </dependency>

테스트 이름 표시

  • @DisplayNameGeneration()
    • 클래스 어노테이션으로 작성하면 미리 구현된 혹은 직접 구현한 테스트 이름 표시 규칙을 적용해줌
    • 기본 구현체ReplaceUnderscores 제공 -> 테스트 메소드의 _를 공백으로 바꿔줌
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class FramesTest {

    public static final int GAME_FINISHING_BOWL_TRIES = 12;

    @Test
    void 게임종료여부_종료되지않은_상태() {
        assertThat(framesProvider(1).gameFinished()).isFalse();
    }

  • @DisplayName()
    • @DisplayNameGeneration()보다 우선순위가 높음
    • 어떤 테스트 인지 메소드명 말고 별도로 테스트 이름을 작성할 수 있는 기능

Assertj 라이브러리를 사용한 대표적인 Assertions

  • Assertj
    • JUnit과 호환되는 3rd-party 라이브러리
    • 더욱 편리하고 가독성 좋게 테스트 코드를 작성할 수 있음
    • JUnit 팀에서도 정식 추천하는 라이브러리
    • 메서드 체이닝을 사용해 편하게 검증을 수행할 수 있음
  • 대표적인 Assertions
assertj 코드 검증 내용
isEqualTo(Object o) 실제 값이 주어진 값과 같은지 확인 ( equals() )
isSameAs(Object o) 실제 값이 주어진 값과 같은지 확인 ( == )
isInstanceOf(Class type) 실제 값이 주어진 유형의 인스턴스인지 확인
isExactlyInstanceOf(Class type) 실제 값이 정확히 주어진 유형의 인스턴스인지 확인
isTrue() / isFalse() 주어진 값이 참 / 거짓인지 확인
contains(Object o) 컬렉션이 주어진 값을 포함하는지 확인
containsOnly(Object o) 컬렉션이 주어진 값 포함하는지 확인
containsExactly(Object o) 컬렉션이 정확히 순서까지 고려하여 주어진 값만 포함하는지 확인
startsWith(Object o) 컬렉션이 주어진 값으로 시작하는지 확인
doesNotContainNull() null을 포함하지 않는지 확인

테스트 반복

  • RepeatedTest
    • 반복 횟수 / 반복 테스트 이름을 설정할 수 있음

ParameterizedTest

  • 여러 다른 매개변수를 대입해가며 테스트를 반복 실행 가능
  • 인자 값을 받는 방법들
    • @ValueSource
    • @NullSoure, @EmptySource, @NullAndEmptySource
    • @MethodSource
    • @CsvSource

과제 0

// TODO 과제 작성

피드백

  • Queue의 동작
    • 각각 삽입, 삭제, 확인 기능
    • offer(), poll(), peek() 은 수행이 불가해도 예외가 터지지 않고, falsenull을 반환
    • add(), remove(), element()는 수행 불가 시 예외가 터짐
    • 메소드 구현 시 일관성에 대해 고민하기

참고문헌