목표 : 자바가 제공하는 다양한 연산자를 학습하기
1. 연산의 기초
2. 단항 연산자
3. 산술 연산자
4. 관계 연산자
5. 논리 연산자
6. 비트 연산자
7. 비트 연산자
8. 3항 연산자
9. assignment(=) operator
10. instanceof
11. 화살표(->) 연산자
12. 연산자 우선 순위
13. Java 13, switch 연산자
연산에 대해서 공부하려면 연산의 정의를 알아야한다.
연산이란 프로그램에서 데이터를 처리하여 결과를 산출하는 것이다.
Java에서는 연산의 특징으로 피연산자간에 데이터가 일치해야 한다.
그 과정에서 캐스팅, 프로모션과 같은 자료형 변환이 발생하기도 한다.
자료형 변환 내용은 해당 링크를 통해서 알아보자
연산 용어
- 연산(operations) :
- 프로그램에서 데이터를 처리하여 결과를 산출하는 것
- 연산자(operator)
- 연산에서 사용되는 표시나 기호
+, — , *, ==….
- 연산에서 사용되는 표시나 기호
- 피연산자(operand)
- 연산되는 데이터
- 연산식(expression)
- 연산자와 피연산자를 이용하여 연산의 과정을 기술할 것
x, y, z, i, j
- 연산자와 피연산자를 이용하여 연산의 과정을 기술할 것
프리미티브 타입과 래퍼 타입의 숫자 유형
- 연산자는 숫자 피연산자가 필요하며 숫자 결과를 생성한다.
- 피연산자 유형은
프리미티브 타입의 숫자 유형
이거나래퍼 타입의 숫자 유형
일 수 있다. - 프리미티브 타입 :
byte
,short
,char
,int
,long
,float
,double
- 래퍼 타입 :
Byte
,Character
,Short
,Integer
,Long
,Float
,Double
- 궁금증 : JVM 에서 두 피연산자간의 데이터 타입은 같아야 한다 했는데?
- 정답 : 래퍼 클래스 타입은 연산시에 프리미티브 타입으로 언박싱되어 계산이 처리된다.
즉, 프리미티브 타입으로 연산이 되는 것이고 이 과정에서 프로모션 우선 순위를 따른다.
피연산자 1개로 사용되는 연산자.
연산자 | 연산자의 기능 | 결합 방향 |
---|---|---|
++(prefix) |
명령을 시작하기 전에 피연산자에 저장된 값을 증가val==++n; |
<- |
++(postfix) |
명령이 끝난 후에 피연산자에 저장된 값을 증가val==n++; |
<- |
--(prefix) , prefix |
명령이 시작하기 전에 피연산자에 저장된 값을 감소val==--n; |
<- |
--(postfix) , postfix |
명령이 끝난 후에 피연산자에 저장된 값을 감소val==n--; |
<- |
+ |
피연산자를 양수로 나타낸다. 대부분, 생략해서 사용한다. |
x |
- |
피연산자를 음수로 나타낸다. | x |
int a = 3;
System.out.println(++a); // 명령이 실행되기 전에 값이 4로 증가한다.
System.out.println(a); // 4출력
System.out.println(a++); // 명령이 끝나고 출력되므로 4가 출력되고 증가됨
System.out.println(a); // 5가 출력된다.
System.out.println(""); // 공백을 위한 빈칸.
a = 2;
int b = 4;
System.out.println(a++ == 2 && ++a == b);
// a는 현재 4이다.
System.out.println(a++ + ++a);
/* 실행 결과
4
4
4
5
true
10
*/
- 연산의 기준은
코드 줄 단위
가 아닌연산 단위
이다. a++ == 2
같은 경우는 연산이 종료후 값이 증가되므로 true가 된다.++a == b
같은 경우는이전 후위 증가
+전위 증가
로 값이 4가 되어 true가 된다.a++ + ++a
같은 경우는 전,후위가 같이 있으므로 둘다 5로 증가되어 계산되고 6으로 증가한다.
산술 연산자 : 사칙 연산자(
+
,-
,*
,/
)과 나머지(%
) 연산자를 의미
산술 연산자
연산자 | 연산자의 기능 | 결합 방향 |
---|---|---|
+ |
두 피연산자의 값을 더한다.val = 4 + 3; 값 : 7 |
-> |
- |
왼쪽의 피연산자 값에서 오른쪽의 피연산자 값을 뺀다val = 4 - 3; 값 : 1 |
-> |
* |
두 피연산자의 값을 곱한다.val = 4 * 3; 값 : 12 |
-> |
/ |
왼쪽의 피연사 값을 오른쪽의 피연산자 값으로 나눈다.val = 4 / 3; 값 : 1 |
-> |
% |
왼쪽의 피연산자 값을 오른쪽의 피연산자 값으로 나눴을 때 얻게 되는 나머지를 반환한다.val = 7 % 3; 값 : 1 |
-> |
int num1 = 8, num2 = 4;
System.out.println("+ 연산자에 의한 결과 : "+ (num1 + num2));
System.out.println("- 연산자에 의한 결과 : "+ (num1 - num2));
System.out.println("* 연산자에 의한 결과 : "+ (num1 * num2));
System.out.println("/ 연산자에 의한 결과 : "+ (num1 / num2));
System.out.println("% 연산자에 의한 결과 : "+ (num1 % num2));
/* 실행 결과
+ 연산자에 의한 결과 : 12
- 연산자에 의한 결과 : 4
* 연산자에 의한 결과 : 32
/ 연산자에 의한 결과 : 2
% 연산자에 의한 결과 : 0
*/
- 우리가 기존 아는 형태로 덧셈, 뺄셈, 나눗셈, 곱셈이 진행된다.
- 또한,
나머지 연산 (%)
이 존재하여 나머지 값을 얻을 수 있다. - 나머지 연산은 헷갈리기 쉬운데
7%2 == 1
처럼 몫을 구한 후의 나머지 값이다. - 이 외에도 사칙 연산을 진행할 때 오버플로우가 발생하는 것을 주의하면 좋다.
특수값
특수값 | 설명 |
---|---|
Infinity | "무한"또는 INF 값은 너무 큰 숫자를 나타냅니다. +INF 값은 너무 크고 양수 인 숫자를 나타냅니다. -INF 값은 너무 크고 음수 인 숫자를 나타냅니다. INF 값은 오버플로를 일으키는 부동 연산 또는 0으로 나누는 부동 소수점 연산에 의해 생성됩니다 |
NaN | "무기한"/ "숫자가 아님"또는 NaN은 의미없는 작업으로 인해 발생하는 값을 나타냅니다. NaN 값은 0을 0으로 나누거나 0을 나머지로 계산하여 생성됩니다. |
x | y | x / y | x % y |
---|---|---|---|
유한수 | +-0.0 | +-Infinity | NaN |
유한수 | +-Infinity | +-0.0 | x |
+-0.0 | +-0.0 | NaN | NaN |
+-Infinity | 유한수 | +-Infinity | NaN |
+-Infinity | +-Infinity | NaN | NaN |
System.out.println(3/0); // ArithmeticException Runtime Error!
System.out.println(3/0.0); // Infinity
System.out.println(3.14/0); // Infinity
System.out.println(0/0); // Nan
System.out.println(10%0); // Nan
정수
를정수 0
으로 나누면ArithmeticException
런타임 에러가 발생한다.정수
를실수 0.0
으로 나누거나실수
를정수 0
으로 나누면Infinity
가 나온다.정수 0
을0
으로 나누거나정수
를0
으로 나머지 계산을 하면NaN
이 나온다.
관계 연산자 : 두 피연산자 사이에서 크기 및 동등 관계를 따져주는 이항 연산자이다.
연산 결과값으로는 true/false를 나타내는 boolean 리터럴이 반환된다.
연산자 | 연산자의 기능 | 결합 방향 |
---|---|---|
< |
왼쪽의 피연산자가 오른쪽의 피연산자보다 초과인지 묻는다1 < 2; 결과 : true 2 < 1; 결과 : false |
-> |
> |
왼쪽의 피연산자가 오른쪽의 피연산자보다 초과인지 묻는다2 > 1; 결과 : true 1 > 2; 결과 : false |
-> |
<= |
왼쪽의 피연산자가 오른쪽의 피연산자보다 이하인지 묻는다1 <= 2; 결과 : true 1 <= 1; 결과 : true 2 <= 1; 결과 : false |
-> |
>= |
왼쪽의 피연산자가 오른쪽의 피연산자보다 이상인지 묻는다2 >= 1; 결과 : true 1 >= 1; 결과 : true 1 >= 2; 결과 : false |
-> |
== |
두 피연산자의 값이 같은지 묻는다. 변수일 경우 메모리에 저장된 값이 동등한지 묻는다. 1 == 1; 결과 : true 1 == 2; 결과 : false |
-> |
!= |
두 피연산자의 값이 다른지 묻는다. 변수일 경우 메모리에 저장된 값이 다른지 묻는다. 2 != 1; 결과 : true 1 != 1; 결과 : false |
-> |
System.out.println(1 < 2); // true
System.out.println(2 < 1); // false
System.out.println(2 > 1); // true
System.out.println(1 > 2); // false
System.out.println(1 <= 1); // true
System.out.println(1 <= 2); // true
System.out.println(1 >= 1); // true
System.out.println(2 >= 1); // true
System.out.println(1 == 1); // true
System.out.println(1 == 2); // false
System.out.println(1 != 1); // false
System.out.println(1 != 2); // true
float f = 0.1f;
double d = 0.1;
System.out.println(f == d); // false
System.out.println(0.01 == 0.01f); // false
System.out.println(0.1 == d); // true
- 두 피연산자의 데이터를 비교해주는 연산자이다.
- 주로 조건문(
if()
)이나 반복문(for()
)에서 사용되고 있다. - 실수의 비교 같은 경우는 조금 재미있는 상황이 발생한다.
- 이전 포스팅에서 언급했듯이 실수 리터럴은 완벽한 값이 아닌 근사값이다.
- 그렇기에
float
은 6자리까지,double
은 15자리까지만 정확성을 유지하고 나머지는 오차가 존재한다. - 관계 연산을 진행하면서
float
리터럴이double
형으로 프로모션 되지만,
정확성을 유지하는 15자리 범위는 같지만 나머지에서 오차가 발생하여false
가 된다. 0.1 == d
부분도 눈여겨볼 점이 있다.- 이전 포스팅에서 언급했듯이 리터럴이 처음 사용되면 메모리 어딘가에 계속 상주해있다.
- 이후 해당 리터럴값을 다시 호출하면 메모리에 상주된 리터럴이 재사용된다.
- 변수
d
같은 경우, 변수 선언시0.1
이라는 리터럴을 메모리에 상주시키고 복사를 했다. - 그리고
0.1 == d
시0.1
은 메모리에 상주된0.1 리터럴
을 불러와 사용을 한다. - 즉, 서로 비교하는 대상은 같은 값일 수밖에 없고 결과는 true가 된다.
논리 연산자 : 피연산자의 boolean 타입을 비교하는 연산자이다.
연산자 | 연산자의 기능 | 결합방향 |
---|---|---|
&& |
두 피연산자 모두 true일 경우 결과는 trueA && B |
-> |
₩₩ |
두 피연산자 중 하나라도 true일 경우 결과는 trueA ₩₩ B |
-> |
! |
피연산가 true면 결과는 false, 피연산자가 false면 결과는 true !A |
<- |
x |
y |
x && y |
x ₩₩ y |
---|---|---|---|
true | true | true | true |
true | false | false | true |
false | true | false | true |
false | flase | false | false |
x |
!x |
---|---|
true | false |
false | true |
System.out.println(true && true); // true
System.out.println(false && true); // false
System.out.println(true && false); // false
System.out.println(false && false); // false
System.out.println(true || true); // true
System.out.println(true || false); // true
System.out.println(false || true); // true
System.out.println(false || false); // false
System.out.println(!true); // false
System.out.println(!false); // true
&&
과||
는 양측의 피연산자의 boolean 리터럴을 비교한다.&&
는 양쪽이 모두 true면 true를 리턴한다.||
는 한쪽이라도 true면 true를 리턴한다.!
는 반대 개념의 boolean 리터럴을 반환한다.
int num1 = 0;
int num2 = 0;
boolean result;
result = ((num1 += 10) < 0) && ((num2 += 10) > 0);
System.out.println("result = " + result);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2 + "\n");
result = ((num1 += 10) > 0) || ((num2 += 10) > 0);
System.out.println("result = " + result);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2 + "\n");
/* 실행 결과
result = false
num1 = 10
num2 = 0
result = true
num1 = 20
num2 = 0
*/
Short-Circuit Evaluation(Lazy Evalutaion)
- 연산의 효율 및 속도를 높이기 위해서 불필요한 연산을 생략하는 행위다.
- 즉, 앞 연산 결과에 다음에 실행해애 할 연산의 처리 여부를 결정하는 것이다.
result = ((num1 += 10) < 0) && ((num2 += 10) > 0);
(num1 += 10) < 0)
는 false 이다.&&
앞 계산식에서 false가 나왔기에 뒤 연산은 진행하지 않고 넘어간다.- 결국
num1 +=10
만 진행 된 것이고num2 += 10
진행되지 않았다.
result = ((num1 += 10) > 0) || ((num2 += 10) > 0);
(num1 += 10) > 0)
는 true 이다.||
앞 계산식에서 true가 나왔기에 뒤 연산은 진행하지 않고 넘어간다.- 결국
num1 +=10
만 진행 된 것이고num2 += 10
진행되지 않았다.
논리 연산을 진행할 때 Short-Circuit Evaluation
를 주의하면서 프로그래밍 하자.
비트 연산자 : 정수형인 피연산자를 비트 단위로 논리 연산을 진행하는 것
비트 연산자
연산자 | 연산자의 기능 | 결합 방향 |
---|---|---|
& |
비트 단위로 AND 연산을 한다. 두 피연산의 비트값이 1일 경우 1리턴, 아닐 경우 0리턴 n1 & n2 |
-> |
₩ |
비트 단위로 OR 연산을 한다. 두 피연산의 비트값중 하나라도 1일 경우 1리턴, 아닐 경우 0리턴 원래는 수직선 기호이지만, 마크다운 에러가 발생해서 ₩ 으로 교체n1 ₩ n2 |
-> |
^ |
비트 단위로 XOR 연산을 한다. 두 피연산의 비트값이 하나라도 1일 경우 0리턴, 아닐 경우 1리턴 n1 ^ n2 |
-> |
~ |
피연산자의 모든 비트를 반전시켜서 얻은 결과를 반환하며 단항이다. 1은 0으로, 0은 1로 치환한다. ~n; |
<- |
x |
y |
x & y |
x ₩ y |
x ^ y |
---|---|---|---|---|
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
0 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 |
x |
~x |
---|---|
0 | 1 |
1 | 0 |
System.out.println(30 & 20);
System.out.println(30 | 20);
System.out.println(30 ^ 20);
System.out.println(~30);
System.out.println(~20);
System.out.println('A' | 'B');
System.out.println(~'A');
/* 실핼 결과
20
30
10
-31
67
-66
*/
- 비트는 0과 1로 이루어져있다.
- 우리가 사용하는 모든 코드들은 비트로 되어 있다.
- 정수 리터럴에 한하여 비트 연산을 진행할 수 있다.
- 비트 연산을 진행할 경우 피 연산자들의 비트값 0과 1을 비교하여 계산한다.
- 참고로, 문자 데이터 타입도 사실은 정수값이 들어가므로 비트 연산을 할 수 있다.
- 단, 문자열은 불가능하다.
- 각 칸은
1bit
를 의미하며 총8bit
, 즉1byte
로 구성되어 있다. - 편의를 위해
int
의4byte
메모리 전부를 표현하지 않고1byte
로 표현을 했다. - 30과 20의 비트를 기준으로 연산을 진행했다.
&
,₩
,^
은 우리의 예상대로 계산이 진행되었다.~30
의 결과값으로는-31
이 나왔다.~
는 0과1을 서로 치환하는 것이기에 원래는 값이 엄청 클 것이다.- 하지만,
~
를 사용할 경우-1*(피연산자값+1)
계산값이 나온다. - 이는 정수의 표현 방법과 보수로 인해 생기는 결과이며 올바른 계산법이다.
보수
- 컴퓨터가 정수를 표현하는 방식은 MSB에 따라 달라진다.
- MSB가 0이면 양수
- MSB가 1이면 음수
- 하지만, 음수같은 경우 양수와 같은 방식으로 만들 경우 문제가 생긴다.
- 음수와 양수가 같은 방식일 경우 :
- 1 :
00000001
- -1 :
10000001
- 1 + (-1) =
10000010
이라는 이상한 값이 나온다.
- 1 :
- 해결책 : 정수의 비트값 0과 1을 뒤집은 다음 +1을 더해준다.
1을 더해주는 이유는 치환된 비트끼리 값을 더할 경우11111111
이 되므로 1이 부족하다.- 1 :
00000001
- -1 :
11111110
->11111111
- 1 + (-1) =
00000000
이라는 정상적 계산 결과값이 나오게 되었다.
- 1 :
시프트 연산 : 피연산자를 이진수로 표현했을 때 비트를 좌/우로 이동시키는 연산자
연산자 | 연산자 기능 | 결합방향 |
---|---|---|
<< |
피연산자의 비트 열을 왼쪽으로 이동 이동에 따른 빈 공간은 0으로 채움 |
-> |
>> |
피연산자의 비트 열을 오른쪽으로 이동 이동에 따른 빈 공간은 양수일 경우 1, 음수일 경우 0으로 채움 |
-> |
>>> |
피연산자의 비트 열을 오른쪽으로 이동 이동에 따른 빈 공간은 0으로 채움 |
-> |
byte num;
num = 2;
System.out.println((byte)(num << 1)+ " ");
System.out.println((byte)(num << 2)+ " ");
System.out.println((byte)(num << 3)+ " " + '\n');
num = 8;
System.out.println((byte)(num >> 1)+ " ");
System.out.println((byte)(num >> 2)+ " ");
System.out.println((byte)(num >> 3)+ " " + '\n');
num = -8;
System.out.println((byte)(num >> 1)+ " ");
System.out.println((byte)(num >> 2)+ " ");
System.out.println((byte)(num >> 3)+ " " + '\n');
num = -8;
System.out.println((byte)(num >>> 1)+ " ");
System.out.println((byte)(num >>> 2)+ " ");
System.out.println((byte)(num >>> 3)+ " " + '\n');
/* 실행 결과
4 8 16
4 2 1
-4 -2 -1
-4 -2 -1
*/
- 피연산자를 비트로 표현하고 좌/우로 이동시키는 연산자이다.
>>>
같은 경우 부호 상관 없이 0의 값이 들어와야 하는데 문법이 바뀐 것 같다.숫자 << 1
: 곱하기 2숫자 >> 1
: 나누기 2
instanceof 연산자 : '참조하는 인스턴스'의 클래스나 상속하는 클래스를 묻는 연산자.
명시적 형 변환의 가능성을 판단해주는 연산자이다.
class Cake {
}
class CheeseCake extends Cake {
}
class StrawberryCheeseCake extends CheeseCake {
}
public void instanceOfPrint(Cake cake) {
if(cake instanceof Cake) {
System.out.println("케익 인스턴스 or");
System.out.println("케익 상속하는 인스턴스 \n");
}
if(cake instanceof CheeseCake) {
System.out.println("치즈케익 인스턴스 or");
System.out.println("치즈케익 상속하는 인스턴스 \n");
}
if(cake instanceof StrawberryCheeseCake) {
System.out.println("딸기치즈케익 인스턴스 or");
System.out.println("딸기치즈케익 상속하는 인스턴스 \n");
}
}
class YummyCakeOf {
public static void main(String[] args) {
System.out.println("//케이크//");
Cake cake = new Cake();
instanceOfPrint(cake);
System.out.println("//치즈케이크//");
cake = new CheeseCake();
instanceOfPrint(cake);
System.out.println("//딸기치즈케이크//");
cake = new StrawberryCheeseCake();
instanceOfPrint(cake);
}
}
/*
//케이크//
케익 인스턴스 or
케익 상속하는 인스턴스
//치즈케이크//
케익 인스턴스 or
케익 상속하는 인스턴스
치즈케익 인스턴스 or
치즈케익 상속하는 인스턴스
//딸기치즈케이크//
케익 인스턴스 or
케익 상속하는 인스턴스
치즈케익 인스턴스 or
치즈케익 상속하는 인스턴스
딸기치즈케익 인스턴스 or
딸기치즈케익 상속하는 인스턴스
*/
- 참조 변수가 기준이 아니다.
- 참조하는 인스턴스가 어떤 클래스인지, 또는 어떤 클래스를 상속 받았는지 알려준다.
- 즉, 클래스의 명시적 형 변환의 가능성을 판단해주는 연산자이다.
할당 연산자 : 변수와 같은 저장공간에 값 또는 수식의 연산결과를 저장하는데 사용된다.
연산자 | 연산 기능 | 결합 방향 |
---|---|---|
= |
연산자 오른쪽에 있는 값을 연산자 왼쪽에 있는 변수에 대입한다.val = 20; |
<- |
int a = 10;
a = 20;
System.out.println(a);
String str = "abdef";
str = "abcde";
System.out.println(str);
/* 실행 결과
20
abcde
*/
- 왼쪽 피연산자에 오른쪽 피연산자 값을 할당한다.
- 프리미티브 타입은 물론, 레퍼런스 타입도 참조 가능 대상이면 할당 가능하다.
깊은 복사와 얕은 복사
class Point {
int x=0;
int y=0;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "x :" + x + " y :" + y;
}
}
public class Main {
public static void main(String[] args) {
int a = 10;
int b = a; // 깊은 복사
a = 15;
System.out.println(a);
System.out.println(b);
Point point_1 = new Point(3, 5);
Point point_2 = point_1; // 얕은 복사
point_1.x = 7;
point_1.y = 2;
System.out.println(point_1);
System.out.println(point_2);
String str = "abcde";
String str2 = str; // 깊은 복사
str = "abcdef";
System.out.println(str);
System.out.println(str2);
}
}
/* 실행 결과
15
10
x :7 y :2
x :7 y :2
abcdef
abcde
*/
- 깊은 복사 : 값을 복사하기에 특정 변수의 값을 변경해도 다른 변수에 영향을 미치지 않는다.
- 얕은 복사 : 주소값을 복사하기에 특정 변수의 참조 대상의 값을 바꾸면 다른 변수에도 영향을 미친다.
- 일반적으로 프리미티브 타입은 깊은 복사, 레퍼런스 타입은 얕은 복사가 이루어진다.
- String 클래스 같은 경우는 레퍼런스 타입이지만 예외적으로 얕은 복사가 이루어지고 있다.
- 사실 String은
immutable 객체
여서 저장된 값이 바뀌는 것이 아닌 참조 대상을 바꾼 것이다. - 그렇다면 레퍼런스 타입을 깊은 복사하는 방법은 없을까?
- Object 최상위 클래스의
clone()
이라는 메서드를 사용하면 된다. - 레퍼런스 타입 깊은 복사 자세한 설명
- Object의
clone()
메서드를 살펴보니 native 키워드가 있어서 검색해보았다.
복합 대입 연산자
연산자 | 연산자의 기능 | 결합 방향 |
---|---|---|
+= |
기존값과 피연산자값을 더하여 기존값을 갱신한다.int val = 4; val += 3; 값 : 7 |
-> |
-= |
기존값에서 피연산자값을 빼서 기존값을 갱신한다.int val = 4; val -= 3; 값 : 1 |
-> |
*= |
기존값과 피연산자값을 곱해서 기존값을 갱신한다.int val = 4; val *= 3; 값 : 12 |
-> |
/= |
기존값을 피연산자값으로 나눈 몫으로 기존값을 갱신한다.int val = 4; val /= 3; 값 : 1 |
-> |
%= |
기존값을 피연산자값으로 나눈 나머지로 기존값을 갱신한다.int val = 7; val %= 3; 값 : 1 |
-> |
<<= |
기존값을 피연산자값만큼 왼쪽 시프트 후 기존값을 갱신한다.int val = 2; val <<= 2; 값 : 8 |
-> |
>>= |
기존값을 피연산자값만큼 오른쪽 시프트 후 기존값을 갱신한다.int val = 8; val >>= 2; 값 : 2 |
-> |
&= |
기존값과 피연산자값을 AND 비트 연산 후 기존값을 갱신한다.int val = 7; val &= 3; 값 : 3 |
-> |
₩= |
기존값과 피연산자값을 OR 비트 연산 후 기존값을 갱신한다.int val = 9; val ₩= 2; 값 : 11 |
-> |
^= |
기존값과 피연산자값을 XOR 비트 연산 후 기존값을 갱신한다.int val = 6; val ^= 3; 값 : 4 |
-> |
- 대입이 산술 연산자와 묶여서 정의된 형태의 연산자이다.
- 변수의 값을 하나의 피연산자로 사용하고, 연산된 결과값을 다시 기존 변수에 대입하여 갱신한다.
- 코드가 보다 간결해지는 장점이 있다.
람다 : 람다 함수는 함수형 프로그래밍 언어에서 사용되는 개념으로 익명 함수라고도 한다.
Java 8 부터 지원되며, 불필요한 코드를 줄이고 가독성을 향상시키는 것을 목적으로 두고있다.
람다 함수의 특징
- 메소드의 매개변수로 전달될 수 있고, 변수에 저장될 수 있다.
- 즉, 어떤 전달되는 매개변수에 따라서 행위가 결정될 수 있음을 의미한다.
- 컴파일러 타입 추론에 의지하고 추론이 가능한 코드는 모두 제거해 코드를 간결하게 한다.
람다식 표현
- 파라미터와 몸체로 구분된다.
- 파라미터와 몸체 사이에
->
구분을 추가하여 람다식을 완성시킨다. - 몸체 부분이 단일 행일 경우 중괄호와 return문을 생략할 수 있다.
( 파라미터 ) -> { 몸체 }
함수형 인터페이스의 추상 메서드를 위한 것으로
익명 클래스를 생성하고 인터페이스를 구현하고 추상 메서드를 정의했던 것과 달리
추상 메서드의 내용만 정의하여 바로 사용하는 방법이다.
예시
// @FunctionalInterface는 해당 인터페이스가 추상 메서드를 1개만 가지고 있는지 검증해준다.
// 람다식이 가능하진 검사 및 안정성 확보로 사용하는 용도로도 사용된다.
@FunctionalInterface
interface Printable{
void print(String s);
}
@FunctionalInterface
interface Printable2{
void print();
}
public class OneParamNoReturn {
public static void main(String[] args) {
Printable p;
Printable2 p2;
p = (String s) -> { System.out.println(s); };
p.print("Lambda exp one.");
p = (String s) -> System.out.println(s);
p.print("Lambda exp two.");
p = (s) -> System.out.println(s);
p.print("Lambda exp three.");
p = s -> System.out.println(s);
p.print("Lambda exp four.");
p2 = () -> System.out.println("NO ARGUMENT");
}
}
- 반환형이 void일 때 명령이 1개밖에 없을 경우
{}
를 생략할 수 있다. - 반환형이 void가 아니고 명령이 리턴하는 내용만 있을 경우
{}
를 생략할 수 있다. - 타입 추론을 통한 매개변수 자료형을 생략할 수 있으며 갯수에는 제한 없다.
- 매개변수가 1개일때
()
를 생략할 수 있다. - 매개변수가 없는 람다일 경우
()
만 사용해준다.
위 코드는 람다의 표현 예를 알려주기 위해서 간단히 만든 예제이다.
실제로는, 매개변수로 함수형 인터페이스(람다형 인터페이스)를 가진 메서드에 주로 사용된다.
실제 사용 용도
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
public class OneParamNoReturn {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
List<Integer> list2 = list.stream().filter( intValue -> intValue <= 3 ).collect(Collectors.toList()); // 람다 사용
List<Integer> list3 = list.stream().map( intValue -> intValue = 3 ).collect(Collectors.toList()); // 람다 사용
Iterator<Integer> it = list2.iterator();
while (it.hasNext()){
System.out.print(" list2 : " + it.next()+",");
}
System.out.println();
it = list3.iterator();
while (it.hasNext()){
System.out.print(" list3 : " + it.next());
}
}
}
filter()
와mao()
의 매개변수 데이터 타입은 함수형 인터페이스이다.- 즉, 람다를 사용하여 인수를 넘겨줄 수도 있다.
- 자주 사용되는 함수형 인터페이스에 대한 내용은 여기로
3항 연산자 : 조건식의 평가결과에 따라 다른 결과를 반환하는 연산자이다.
조건식 ? 식1 : 식2
// 평가 결과 -> // true 일 때 // false 일 때
- 피연산자의 조건식 평과결과에 따라 다른 결과를 반환한다.
- 조건식 평과결과가 true면
?
옆에 있는 식을 - 조건식 평과결과가 false면
:
옆에 있는 식을 사용한다.
// 같다.
if(x > y) {
result = x;
} else {
result = y;
}
result = (x > y) ? x : y;
result = (x > y) ? ((x == y ? x : y)) : ((x == y ? y : x));
if-else 문
을 대신하여 사용할 수 있고 코드가 간결해진다.- 단 여러번, 중첩되서 사용할 경우 오히려 가독성을 저해시킬 수 있다.
x = x + (mod < 0.5 ? 0 : 0.5);
// x = x + (mod < 0.5 ? 0.0 : 0.5);
- 두 리턴 결과값의 자료형이 다르다면, 프로모션 대상이된다.
- 즉, 자동 형변환으로 우선순위가 높은 자료형을 변환이된다.
자바에서 재공하는 모든 연산자를 하나의 표로 정리하면 아래와 같다.
연산기호 | 결합 방향 | 우선순위 |
---|---|---|
() , [] , . |
-> | 1(우선순위가 높다.) |
expr++ , expr-- |
<- | 2 |
++expr , --expr , +expr , -expr , ~ , ! , (type) |
<- | 3 |
* , / , % |
-> | 4 |
+ , - |
-> | 5 |
<< , >> , >>> |
-> | 6 |
< , > , <= , >= , instanceof |
-> | 7 |
== , != |
-> | 8 |
& |
-> | 9 |
^ |
-> | 10 |
₩ |
-> | 11 |
&& |
-> | 12 |
₩₩ |
-> | 13 |
? expr : expr |
<- | 14 |
= , += , -= , *= , /= , %= , &= , ^= , ₩= , <<= , >>= , >>>= |
<- | 15 |
switch : 주어진 조건 값의 결과에 따라 프로그램이 다른 명령을 수행하도록 하는 조건문이다.
if-else 와 달리, 더 많은 조건의 결과를 표현하기 위해 사용된다.
가독성이 더 좋고, 컴파일러가 최적화를 쉽게 할 수 있어 속도 또한 빠른 편이다.
기본 포맷
switch(조건식){
case 값1 :
실행문1;
break;
case 값2 :
실행문2;
break;
default :
어떤 case 절에도 해당하지 않을 때의 실행문;
break; // default에서는 break; 를 생략해도 되지만, 명시해주자.
// default; 하면 아무런 작업도 하지 않는 끝맺음
}
switch()
안에 리터럴이 들어올 수 있는 조건식을 넣어준다.- 변수를 넣어줘도 되며, 계산식도 들어올 수 있다.
- 각각의
case마다
조건식에 부합하는 알맞는 값을 넣어준다. - 마지막 케이스에는
default
를 넣어줄 수 있다. default;
하면 아무런 작업도 하지 않는 끝맺음을 줄 수 있다.
switch(조건식){
case 값1 :
실행문1;
case 값2 :
실행문2;
default :
어떤 case 절에도 해당하지 않을 때의 실행문;
break; // default에서는 break; 를 생략해도 되지만, 명시해주자.
}
switch(){}
는 위에서 아래로 흐름(flow)이 존재한다.- 그렇기 때문에
break;
를 명시하지 않을 경우 밑에 존재하는case 값:
도 실행한다. default :
가 마지막에 오는 것과default:
는break;
를 생략 가능한 이유이기도 하다.switch()
안에는 wrapper 허용하지만,case 값:
에는 wrapper 를 허용하지 않는다.- 부동 소수점은 근사값으로 비교가 정확하지 않을 수 있어 허용하지 않는다.
- 각 케이스는 고유해야하며 중복 케이스 값을 생성하면 컴파일 타임 오류가 발생한다.
JDK7 버전 이전
int value = 1;
switch(value){
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
case 3 :
System.out.println("3");
break;
default :
System.out.println("그 외의 숫자");
}
char ch = 'a';
switch (ch) {
case 'a':
System.out.println("해당 문자는 'A'입니다.");
break;
case 'e':
System.out.println("해당 문자는 'E'입니다.");
break;
case 'i':
System.out.println("해당 문자는 'I'입니다.");
break;
case 'o':
System.out.println("해당 문자는 'O'입니다.");
break;
case 'u':
System.out.println("해당 문자는 'U'입니다.");
break;
default:
System.out.println("해당 문자는 모음이 아닙니다.");
break;
}
- JDK 7 이전에는 정수 리터럴에 관한
switch(){}
만 가능했다. - 문자 같은 경우는 실제로 정수 리터럴을 사용하기에 사용 가능했다.
JDK 7 버전
String str = "A";
switch(str){
case "A":
System.out.println("1");
break;
case "B":
System.out.println("2");
break;
case "C" :
System.out.println("3");
break;
default :
System.out.println("그 외의 숫자");
break;
}
- JDK 7부터는
switch()
안에 문자열 타입의 변수도 올 수 있게 되었다. - 또한, enum 키워드를 사용한 열거체(enumeration type)도 사용할 수 있게 되었다.
JDK 12 버전
// 기본값
final int MONDAY = 1;
final int TUESDAY = 2;
final int WEDNESDAY = 3;
final int THURSDAY = 4;
final int FRIDAY = 5;
final int SATURDAY = 6;
final int SUNDAY = 7;
day = MONDAY;
// 기존 switch
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println("6");
break;
case TUESDAY:
System.out.println("7");
break;
case THURSDAY:
case SATURDAY:
System.out.println("8");
break;
case WEDNESDAY:
System.out.println("9");
break;
}
// break를 이용한 return 가능
String result = switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
break "6";
case TUESDAY:
break "7";
case THURSDAY:
case SATURDAY:
break "8";
case WEDNESDAY:
break "9";
}
// 화살표 연산자를 이용한 익명함수 처리 가능
// 여러 값을 동시에 처리할 수 있다.
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println("6");
case TUESDAY -> System.out.println("7");
default -> {
int result = day.toString().length();
System.out.println(result)
}
};
// 여러 명령어 리턴 버전
String result = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> "6";
case TUESDAY -> "7";
default -> {
int result = ;
break Integer.toString(day.toString().length());
}
};
// 명령이 1개이면 {}를 생략해도 된다.
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println("6");
case TUESDAY -> System.out.println("7");
case THURSDAY, SATURDAY -> System.out.println("8");
case WEDNESDAY -> System.out.println("9");
}
// 리턴값도 사용 가능
String result = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> "6";
case TUESDAY -> "7";
case THURSDAY, SATURDAY -> "8";
case WEDNESDAY -> "9";
};
break 리턴값;
을 통해 switch를 통해서도 값을 리턴할 수 있게 되었다.->
를 이용해 익명 함수를 만들 수 있게 되었고 다중 처리도 가능해졌다.->
처리시 명령이 1개면 각case
마다break;
를 생략해도 된다.->
처리시 명령이 1개면 각 명령 구문에서{}
를 생략해도 된다.->
처리시 명령이 2개 이상이면 각case
마다break;
를 무조건 사용해야한다.->
처리시 명령이 2개 이상이면 각 명령 구문에서{}
를 무조건 사용해야한다.->
처리시 리턴 대상이 있을시 명령이 1개면 리턴값만 기술하면 된다.->
처리시 리턴 대상이 있을시 명령이 2개 이상이면break 리턴값;
을 기술해야 한다.- switch JDK 12 변경사항
- switch JDK 12 변경사항 요약
- switch JDK 12 변경사항 공식 문서
JDK 13 버전
// 기본값
final int MONDAY = 1;
final int TUESDAY = 2;
final int WEDNESDAY = 3;
final int THURSDAY = 4;
final int FRIDAY = 5;
final int SATURDAY = 6;
final int SUNDAY = 7;
day = MONDAY;
// 기존 JDK 12버전의 break를 이용한 return 가능
String result = switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
break "6";
case TUESDAY:
break "7";
case THURSDAY:
case SATURDAY:
break "8";
case WEDNESDAY:
break "9";
}
// JDK 13버전의 yield를 이용한 return 가능
String result = switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
yield "6";
case TUESDAY:
yield "7";
case THURSDAY:
case SATURDAY:
yield "8";
case WEDNESDAY:
yield "9";
}
// 여러 명령어 리턴 버전
String result = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> "6";
case TUESDAY -> "7";
default -> {
int result = ;
yield Integer.toString(day.toString().length());
}
};
- 기존
break
리턴은 기존 문법과 헷갈리며 코드의 오인 여지를 준다. - 그래서 기존
break
리턴 대신에yield
키워드를 통해 리턴을 지원한다. - switch JDK 13 변경사항 요약
- switch JDK 13 변경사항 공식 문서