3주차 - 연산자
산술 연산자
- 변수와 데이터에 대한 산술 연산을 수행하는데 사용됨
연산자 |
수행 연산 |
+ |
덧셈 |
- |
뺄셈 |
* |
곱셈 |
/ |
나눗셈 (몫만 취함) |
% |
Modulo 연산 (나누기 연산 후 나머지만 취함) |
Overflow 가능성
- 연산 결과가 피연산자 타입의 저장 가능 범위를 벗어나게 되면
오버플로우
가 발생, 의도치 않은 값이 변수에 저장됨
int a = 2_100_000_000;
int b = 2_000_000_000;
System.out.println(a + b); // -194967296
- 따라서 연산 시, 오버플로우 발생 가능성을 염두에 두어야 함
/
와 %
/
는 몫만 취하고, %
는 나머지만 취함
- 나눗셈 연산 시
/
의 반환 타입
a / b
에서 a
와 b
가 모두 정수이면 반환 값도 정수
a
와 b
중 하나라도 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일 경우
제네릭에선?
- 제네릭을 사용하는 객체 자체는 타입 체크 가능 (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항 연산자
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 소개
- 자바 개발자가 가장 많이 사용하는 테스팅 프레임워크
- 구성
- 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()
은 수행이 불가해도 예외가 터지지 않고, false
나 null
을 반환
add()
, remove()
, element()
는 수행 불가 시 예외가 터짐
- 메소드 구현 시 일관성에 대해 고민하기
참고문헌