자바가 제공하는 다양한 연산자를 학습한다.
Operators
연산자는 하나, 둘 또는 세 개의 피연산자에 대해 특정 연산을 수행한 다음 결과를 반환하는 특수 기호 이다. 연산자를 탐색할 때 어떤 연산자가 가장 높은 우선 순위를 갖는지 미리 아는 것이 도움이 될 수 있다. 연산자 우선 순위 테이블에서는 연산자를 우선 순위에 따라 나열해두었다. 테이블 상단에 가까울수록 우선 순위가 높아지고, 같은 줄의 연산자는 같은 우선 순위를 갖는다. 할당 연산자를 제외한 모든 이항 연산자는 왼쪽에서 오른쪽으로 계산된다(할당 연산자는 오른쪽에서 왼쪽으로 계산된다.)
Assignment Operators,
=
오른쪽의 값을 왼쪽의 피연산자에 할당한다. 아래 예시를 참고하자.
int cadence = 0;
int speed = 0;
int gear = 1;
이 연산자는 아래와 같이 객체 레퍼런스를 할당하기 위해 사용할 수도 있다.
Point aPoint = new Point(23, 94);
Arithmetic Operators
더하기, 빼기, 곱하기, 나누기와 관련된 연산자들이다. 예시는 이미 많은 곳들에서 자세히 알려주고 있기 때문에 생략하였다(추후 시간이 되면 코드를 첨부해보려 한다).
| 연산자 | 설명 |
|---|---|
| + | 더하기 연산자 (문자열 연결에도 사용됨) |
| - | 빼기 연산자 |
| * | 곱하기 연산자 |
| / | 나누기 연산자 |
| % | 나머지 연산자 |
+ 연산자의 기능 중 하나인 문자열 연결은 아래 코드로 확인할 수 있다.
class ConcatDemo {
public static void main(String[] args){
String firstString = "This is";
String secondString = " a concatenated string.";
String thirdString = firstString + secondString;
System.out.println(thirdString);
}
}
// 출력 결과
// This is a concatenated string.
Relational Operators
관계 연산자는 한 피연산자가 다른 피연산자보다 큰지, 작은지, 같은지, 같지 않은지 여부 를 결정한다.
| 연산자 | 설명 |
|---|---|
| == | equal to |
| != | not equal to |
| > | greater than |
| >= | greater than or equal to |
| < | less than |
| <= | less than or equal to |
아래 코드는 관계 비교의 예시를 보여준다.
class ComparisonDemo {
public static void main(String[] args){
int value1 = 1;
int value2 = 2;
if(value1 == value2)
System.out.println("value1 == value2");
if(value1 != value2)
System.out.println("value1 != value2");
if(value1 > value2)
System.out.println("value1 > value2");
if(value1 < value2)
System.out.println("value1 < value2");
if(value1 <= value2)
System.out.println("value1 <= value2");
}
}
// 출력 결과
// value1 != value2
// value1 < value2
// value1 <= value2
Conditional Operators,
&&,||
공식 문서에서는 조건부 연산자 로 나와있지만, 많은 곳에서 논리 연산자라는 용어를 사용한다. 표현식의 참(true)과 거짓(false)을 판단하기 때문에 적절한 것 같다.
&& 및 || 연산자는 두 개의 boolean 표현식에 대해 조건부 AND (Conditional-AND) 및 조건부 OR (Conditional-OR) 연산을 수행한다. 첫 번째 피연산자는 반드시 계산되지만, 두 번째 피연산자는 필요한 경우에만 계산된다. 아래 활용 예시를 살펴보자.
class ConditionalDemo1 {
public static void main(String[] args){
int value1 = 1;
int value2 = 2;
if((value1 == 1) && (value2 == 2))
System.out.println("value1 is 1 AND value2 is 2");
if((value1 == 1) || (value2 == 1))
System.out.println("value1 is 1 OR value2 is 1");
}
}
// 출력 결과
// value1 is 1 AND value2 is 2
// value1 is 1 OR value2 is 1
다른 조건부 연산자로 ?: 를 사용할 수도 있다. 이를 삼항 연산자 라고 표현하는데, 다음 목차에서 자세히 살펴보자.
Ternary Operator,
?:, if-then-else문의 약어
이 연산자는 세 개의 피연산자를 사용하기 때문에 삼항 연산자 라고 표현한다. 활용법은 아래와 같다.
조건식 ? 값1 : 값2
조건식의 연산 결과가
true이면, 결과는값1이고, 조건식의 연산 결과가false이면 결과는값2이다.
다음 예시를 통해 someCondition이 true면 value1 값을 result 에 할당하고, 그렇지 않으면 value2 값을 result에 할당하는 걸 볼 수 있다.
class ConditionalDemo2 {
public static void main(String[] args){
String value1 = "value 1";
String value2 = "value 2";
String result;
boolean someCondition = true;
result = someCondition ? value1 : value2;
System.out.println("조건식이 true일 때: " + result);
someCondition = false;
result = someCondition ? value1 : value2;
System.out.println("조건식이 false일 때: " + result);
}
}
// 출력 결과
// 조건식이 true일 때: value 1
// 조건식이 false일 때: value 2
식이 간결하거나, true나 false의 결과 값 자리에 할당식을 사용하지 않을 때 삼항 연산자를 사용하여 가독성을 높일 수 있다.
Type Comparison Operator, 타입 비교 연산자
instanceof 연산자는 객체를 지정된 타입과 비교한다. 이를 사용하여 객체가 특정 클래스의 인스턴스인지, 하위 클래스의 인스턴스인지, 또는 특정 인터페이스를 구현하는 클래스의 인스턴스인지 확인할 수 있다.
아래 예시를 보기 전, 부모 클래스를 Parent로, 인터페이스를 MyInterface로, Parent를 상속하고 MyInterface를 구현하는 자식 클래스를 Child로 정의했다는 점을 알아두자.
class InstanceOfDemo {
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
Parent nullObject = null;
System.out.println("parent instanceof Parent: "
+ (parent instanceof Parent));
System.out.println("parent instanceof Child: "
+ (parent instanceof Child));
System.out.println("parent instanceof MyInterface: "
+ (parent instanceof MyInterface) + "\n");
System.out.println("child instanceof Parent: "
+ (child instanceof Parent));
System.out.println("child instanceof Child: "
+ (child instanceof Child));
System.out.println("child instanceof MyInterface: "
+ (child instanceof MyInterface) + "\n");
System.out.println("null instanceof Parent: "
+ (nullObject instanceof Parent));
System.out.println("null instanceof Child: "
+ (nullObject instanceof Child));
System.out.println("null instanceof MyInterface: "
+ (nullObject instanceof MyInterface));
}
}
class Parent {}
class Child extends Parent implements MyInterface {}
interface MyInterface {}
// 출력 결과
// parent instanceof Parent: true
// parent instanceof Child: false
// parent instanceof MyInterface: false
// child instanceof Parent: true
// child instanceof Child: true
// child instanceof MyInterface: true
// null instanceof Parent: false
// null instanceof Child: false
// null instanceof MyInterface: false
instanceof 연산자를 사용할 때 null 객체는 어떤 무엇의 인스턴스도 아니다.
Bitwise and Bit Shift Operators, 비트 단위
자바는 정수(Integer) 타입 에 대해 비트 및 비트 시프트 연산자를 제공한다. 다음 표에서는 각 연산자와 각 기능에 대해 설명하고 있다.
| 연산자 | 설명 |
|---|---|
| & | 대응되는 비트가 모두 1이면 1을 반환함. (비트 AND 연산) |
| | | 대응되는 비트 중에서 하나라도 1이면 1을 반환함. (비트 OR 연산) |
| ^ | 대응되는 비트가 서로 다르면 1을 반환함. (비트 XOR 연산) |
| ~ | 비트를 1이면 0으로, 0이면 1로 반전시킴. (비트 NOT 연산) |
| << | 명시된 수만큼 비트들을 전부 왼쪽으로 이동시킴. (left shift 연산) |
| >> | 부호를 유지하면서 지정한 수만큼 비트를 전부 오른쪽으로 이동시킴. (right shift 연산) |
| >>> | 지정한 수만큼 비트를 전부 오른쪽으로 이동시키며, 새로운 비트는 전부 0이 됨. |
단항 비트 보수 연산자 ~ 는 비트의 패턴을 반전시킨다. 따라서 모든 정수 타입에서 0을 1로 만들고 모든 1을 0으로 만든다. 예를 들어, 이 연산자를 비트 패턴이 00000000 인 값에 적용하면, 패턴이 11111111 로 변견된다.
부호가 있는(signed) 왼쪽 시프트 연산자 << 는 비트 패턴을 왼쪽으로 시프트하고, 오른쪽 시프트 연산자 >> 는 비트 패턴을 오른쪽으로 시프트한다. 시프트 연산자는 왼쪽 피연산자의 비트 패턴을 사용하고, 오른쪽 피연산자의 수만큼 위치를 이동시킨다.
어느 쪽으로 비트를 이동시킬 건지는 시프트의 방향(<< or >>)에 따라 다르다.
<<)으로 이동시키면, 이때 새로 생기는 오른쪽 비트들은 항상 0으로 채워진다. 따라서 모든 비트가 왼쪽으로 한 비트씩 이동할 때마다 그 값은 2배씩 증가한다.>>)으로 이동시키면, 이때 새로 생기는 왼쪽 비트들은 양수일 경우에는 0으로, 음수일 경우에는 모두 1로 채워진다. 따라서 부호는 변하지 않는다. 따라서 모든 비트가 오른쪽으로 한 비트씩 이동할 때마다 그 값은 2배씩 감소한다.>>> 연산자의 경우, 부호 비트까지 포함하여 모든 비트를 전부 오른쪽으로 이동시킨다. 이때 비트의 이동으로 새로 생기는 왼쪽 비트들은 항상 0으로 채워진다. 따라서 피연산자가 양수인 경우에는 부호 비트를 이동하지 않는 오른쪽 시프트 연산자(>>)와 동일한 결과를 반환하지만, 피연산자가 음수인 경우에는 부호 비트까지도 이동하므로, 전혀 다른 결과가 반환된다(이런 점에서 >>> 연산자는 10진수의 연산보다는 2진수의 연산에서만 주로 사용된다).class BitShiftDemo {
public static void main(String[] args) {
int positiveNum = 8;
int negativeNum = -8;
System.out.println("~ 연산자에 의한 positiveNum 결과 : "+ ~positiveNum);
System.out.println("<< 연산자에 의한 positiveNum 결과 : "+ (positiveNum << 2)); // 4를 곱한 것과 같음
System.out.println(">> 연산자에 의한 negativeNum 결과 : "+ (negativeNum >> 2)); // 4를 나눈 것과 같음
System.out.println(">>> 연산자에 의한 positiveNum 결과 : "+ (positiveNum >>> 2)); // 4를 나눈 것과 같음
System.out.println(">>> 연산자에 의한 negativeNum 결과 : "+ (negativeNum >>> 2)); // 전혀 다른 결과
System.out.println("~ 연산자에 의한 결과 : "+ ~num1);
System.out.println("<< 연산자에 의한 결과 : "+ (num1 << 2));
System.out.println(">> 연산자에 의한 결과 : "+ (num2 >> 2));
System.out.println(">>> 연산자에 의한 결과 : "+ (num1 >>> 2));
System.out.println(">>> 연산자에 의한 결과 : "+ (num2 >>> 2));
}
}
// 출력 결과
// ~ 연산자에 의한 positiveNum 결과 : -9
// << 연산자에 의한 positiveNum 결과 : 32
// >> 연산자에 의한 negativeNum 결과 : -2
// >>> 연산자에 의한 positiveNum 결과 : 2
// >>> 연산자에 의한 negativeNum 결과 : 1073741822
Refs:
- 테이블, 시프트 항목 설명 및 코드 예시: TCP SCHOOL - 자바 비트 연산자
- 기타 설명: Oracle Java docs - Bitwise and Bit Shift Operators
화살표 연산자(->)는 람다 표현식(Lambda Expression)에서 사용되기 때문에 람다 표현식에 대해 알아보려 한다.
Lambda Expression
람다 표현식이란 간단히 메소드를 하나의 식으로 표현한 것으로, 자바 8부터 지원되었다. 람다 표현식 문법은 아래와 같다.
( 매개 변수 목록 ) -> { 함수 본문 }
람다 표현식을 작성할 때 주목할 점들을 살펴보자면,
매개 변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있다.
매개 변수가 하나인 경우에는 괄호(( ))를 생략할 수 있다.
함수 본문이 하나의 명령문만으로 이루어진 경우에는 중괄호({ })를 생략할 수 있다. (이때 세미콜론(;)은 붙이지 않음)
함수 본문이 하나의 return 문으로만 이루어진 경우에는 중괄호({ })를 생략할 수 없다.
return 문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결과 값이 된다. (이때 세미콜론(;)은 붙이지 않음)
아래 첫 번째 코드가 일반적인 메소드 코드라면, 두 번째 코드는 람다 표현식 코드다.
int sumOfXY(int x, int y) {
return x + y;
}
(x, y) -> x + y;
메소드를 람다 표현식으로 표현하면, 위에서 언급한 화살표 연산자 -> 가 사용된다. 또한 람다 표현식은 클래스를 작성한 다음 객체를 생성하지 않아도 메소드를 사용할 수 있다.
클래스 이름을 가지지 않고, 일회성의 객체만을 가지는 익명 클래스의 특징을 생각해볼 때 람다 표현식은 익명 클래스와 동일하다고 할 수 있다.
아래 전통적인 방식으로 스레드 생성하는 코드와, 람다 표현식을 사용하여 스레드를 생성하는 코드를 살펴보며 내용을 이해해보자.
전통적인 방식의 스레드 생성 (익명 클래스를 활용)
new Thread(new Runnable() {
public void run() {
System.out.println("전통적인 방식의 일회용 스레드 생성");
}
}).start();
람다 표현식을 사용한 스레드 생성
new Thread(()->{
System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
}).start();
람다 표현식은 메소드의 매개 변수로 전달될 수 있고, 메소드의 결과 값으로 반환될 수도 있다. 따라서 람다 표현식을 사용하면, 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높여준다.
Ref: TCP SCHOOL- 람다 표현식
| 우선 순위 | 연산자 | 연산 방향 | 동작 |
|---|---|---|---|
| 1 | . | -> | 객체 멤버 접근 |
[, ] | -> | 배열 요소 접근 | |
(args) | -> | 메소드 호출 | |
data++, data-- | -> | 후위 증감 | |
| 2 | ++data, --data | <- | 전위 증감 |
+, - | <- | 단항 증감 | |
~, ! | <- | 비트 보수, 부정 연산 | |
| 3 | new | <- | 객체 생성 |
(type) | <- | 캐스팅 | |
| 4 | *, /, % | -> | 곱하기, 나누기, 나머지 |
| 5 | +, - | -> | 더하기, 빼기 |
+ | -> | 문자열 결합 | |
| 6 | <<, >>, >>> | -> | 왼쪽 시프트, 오른쪽 시프트, 부호없는 오른쪽 시프트 |
| 7 | <, <=, >, >= | -> | 작음, 작거나 같음, 큼, 크거나 같음 |
instanceof | -> | 타입 비교 | |
| 8 | ==, != | -> | 같음, 같지 않음 |
| 9 | & | -> | AND |
| 10 | ^ | -> | XOR |
| 11 | ` | ` | -> |
| 12 | && | -> | 조건부 AND |
| 13 | ` | ` | |
| 14 | ? : | <- | 3항 연산자 |
| 15 | =, *=, /=, %=, +=, -=, <<=, >>=, >>>=, &=, ^=, ` | =` | <- |
| 16 | -> | -> | 람다 표현식 |
switch 연산자의 기본적인 개념은 여기서 다루지 않는다. 만약 switch 연산자를 처음부터 공부하고 싶다면, 이곳에서 switch 문 항목을 참고하자.
자바 12, 13에서 switch와 관련해 Preview 기능으로 발표되었던 내용이 자바 14에서 공식화되었다. 이하 내용은 기존의 switch 구문을 비롯하여 자바 14에서 공식화된 부분들에 대해 소개한다.
기존 구문에서는 불필요하게 많은 break문이 필요했다. 가독성을 떨어뜨리고, 디버깅을 어렵게 만들었기 때문에 편한 코드 스타일은 아니었다. 또한, 개발자가 break 문을 언제든지 누락시킬 수 있다는 점도 성가신 부분이었다.
public class SwitchDemo {
public static void main(String[] args) {
SwitchDemo switchDemo = new SwitchDemo();
switchDemo.printDay(Day.TUE);
switchDemo.printDay(Day.FRI);
switchDemo.printDay(Day.SUN);
}
public void printDay(Day day) {
switch (day) {
case MON:
case TUE:
case WED:
case THUR:
case FRI:
System.out.println(day.name() + " is a week.");
break;
case SAT:
case SUN:
System.out.println(day.name() + " is a weekend.");
break;
}
}
enum Day {
MON, TUE, WED, THUR, FRI, SAT, SUN
}
}
// 출력 결과
// TUE is a week.
// FRI is a week.
// SUN is a weekend.
자바에서는 이와 같은 기존의 불편함을 제거하기 위해 새로운 switch 문법을 도입하였다.
아래는 새롭게 도입된 switch 문법을 설명한다.
기존의 스위치 블록 case L : 외에도 case L -> 와 같이 arrow case 를 허용하였다 (이 경우, break 문이 따로 필요하지 않다). 레이블이 일치하면 화살표 오른쪽에 있는 표현식, 블록, 또는 (편의상) throw 문만 실행되고 다음으로 넘어가지 않는다. 그리고 케이스마다 여러 개의 상수를 쉼표(,)로 구분 하는 것 역시 허용하였다. 아래에서는 이러한 내용을 적용시켜 기존 코드를 개선하였다.
.. 생략(직전 예시와 동일)
public void printDay(Day day) {
switch (day) {
case MON, TUE, WED, THUR, FRI ->
System.out.println(day.name() + " is a week.");
case SAT, SUN ->
System.out.println(day.name() + " is a weekend.");
}
}
.. 생략(직전 예시와 동일)
// 출력 결과(직전 예시와 동일)
// TUE is a week.
// FRI is a week.
// SUN is a weekend.
또한 자바에서는 switch 구문을 표현식 으로 사용할 수 있도록 문법을 확장하였다. 이전 예시와 아래 예시의 다른 부분은, 동일한 메소드 printDay() 안에서 switch 문이 =의 오른쪽 표현식으로 사용되었다는 점이다.
.. 생략(직전 예시와 동일)
public void printDay(Day day) {
String result = switch (day) {
case MON, TUE, WED, THUR, FRI -> day.name() + " is a week.";
case SAT, SUN -> day.name() + " is a weekend.";
};
System.out.println(result);
}
.. 생략(직전 예시와 동일)
// 출력 결과(이전 예시와 동일)
// TUE is a week.
// FRI is a week.
// SUN is a weekend.
물론 메소드의 리턴 타입을 현재 void 에서 String 으로 바꿀 수도 있다. 따라서 아래 예시도 문제없이 동작한다.
public class SwitchDemo {
public static void main(String[] args) {
SwitchDemo switchDemo = new SwitchDemo();
System.out.println(switchDemo.getDayInfo(Day.TUE));
System.out.println(switchDemo.getDayInfo(Day.FRI));
System.out.println(switchDemo.getDayInfo(Day.SUN));
}
public String getDayInfo(Day day) { // 메소드의 역할에 맞게 메소드 이름을 바꾸었다
String result = switch (day) {
case MON, TUE, WED, THUR, FRI -> day.name() + " is a week.";
case SAT, SUN -> day.name() + " is a weekend.";
};
return result;
}
.. 생략(직전 예시와 동일)
// 출력 결과(직전 예시와 동일)
// TUE is a week.
// FRI is a week.
// SUN is a weekend.
switch 구문이 표현식으로 사용될 수 있기 때문에 이런 일이 가능한 것이다.
이때, 메소드의 리턴 타입을 Object로 해도 컴파일 에러가 발생하지 않는다. 표현식마다 다른 타입을 가지고 있다 하더라도 타입과 상관없이 레이블에 일치하는 결과를 반환받을 수 있다.
.. 생략(직전 예시와 동일)
public Object getDayInfo(Day day) { // 메소드의 역할에 맞게 메소드 이름을 바꾸었다
return switch (day) {
case MON, TUE, WED, THUR, FRI -> day.name() + " is a week.";
case SAT, SUN -> 123456789;
};
}
.. 생략(직전 예시와 동일)
// 출력 결과
// TUE is a week. (타입: java.lang.String)
// FRI is a week. (타입: java.lang.String)
// 123456789 (타입: java.lang.Integer)
스위치 구문에서 블록이 필요한 경우도 있을 수 있다. 블록이 필요한 경우 에는, yield 문을 도입하여 블록의 값을 내보낼 수 있다.
.. 생략(직전 예시와 동일)
public int getDayInfo(Day day) { // 메소드의 역할에 맞게 메소드 이름을 바꾸었다
int result = switch (day) {
case MON -> 0;
case TUE -> 1;
default -> {
int length1 = day.toString().length(); // 3 (기능만 확인하는)
int length2 = day.toString().length(); // 3 (의미 없는 코드다)
yield length1 + length2; // 6
}
};
return result;
}
.. 생략(직전 예시와 동일)
// 출력 결과
// 0 (MON이 매개변수일 때)
// 1 (TUE가 매개변수일 때)
// 6 (SUN이 매개변수일 때)
스위치 표현식을 사용했다면, 값을 사용하여 정상적으로 표현식을 완료하거나, 예외를 발생시켜야 한다. 아래와 같은 경우에서는 컴파일 에러가 발생한다.
int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// ERROR! Block doesn't contain a yield statement
}
default -> 1;
};
i = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY:
yield 0;
default:
System.out.println("Second half of the week");
// ERROR! Group doesn't contain a yield statement
};
Refs: