자바가 제공하는 다양한 연산자를 학습하세요.
+
, -
연산자 (부호 연산자)~
연산자+
-
*
/
)덧셈(+
), 뺄셈(-
), 곱셈(*
), 나눗셈(/
)
우선순위도 우리가 알고 있듯이 곱셈, 나눗셈이 덧셈, 뺄셈 연산자보다 높다.
public static void main(String args[]) {
int a = 10;
int b = 4;
System.out.printf("%d + %d = %d%n", a, b, a + b);
System.out.printf("%d - %d = %d%n", a, b, a - b);
System.out.printf("%d * %d = %d%n", a, b, a * b);
System.out.printf("%d / %d = %d%n", a, b, a / b);//---(1)
System.out.printf("%d / %f = %f%n", a, (float)b, a / (float)b); //---(2)
}
==========================================
10 + 4 = 14
10 - 4 = 6
10 * 4 = 40
10 / 4 = 2
10 / 4.000000 = 2.500000
(1) : 10 / 4
의 출력값으로 2.5
가 아닌 2
가 나오게 되었다. 이유는 자료형에 있다. 연산에 사용된 두 피연산자는 모두 int
타입이다. 고로 연산결과 역시 int
타입이기 때문에 실제 연산 결과가 2.5
일지라도 int타입의 경우 소수점을 저장하지 못하므로 정수만 남고 소수점 이하는 버려져 2
가 반환되게 된다.
그래서 올바른 연산결과를 얻기 위해서는 두 피연산자 중 어느 한 쪽을 실수형으로 형변환을 해야한다. 그래야만 다른 한 쪽도 같이 실수형으로 자동 형변환되어 결국 실수형의 값을 결과로 얻게 된다.
(2): 두 피연산자의 타입이 각각 int
타입과 float
타입으로 일치하지 않기 때문에 int
타입보다 범위가 넓은 float
타입으로 일치시킨 후에 연산을 수행한다.
피연산자가 정수형인 경우, 나누는 수로 0
을 사용하게 될 경우ArithmeticException
가 발생한다.
또한, 부동 소수점 값인 0.0f, 0.0d 로 나누는 것은 가능하지만 그 결과는 Infinity(무한대)이다.
byte a = 10;
byte b = 30;
byte c = (byte)(a*b);
System.out.println(c);
============================
44
큰 자료형의 값을 작은 자료형의 변수에 저장하려면 명시적 형변환을 해줘야 한다.
대신, 데이터의 손실이 발생하므로 값이 바뀔 수 있다.
public static void main(String[] args) {
int a = 1_000_000;
int b = 2_000_000;
long c = a * b; // a * b = 2,000,000,000,000 ?
System.out.println(c);
}
======================================================
-1454759936
c의 타입이 long
타입이기 때문에 2 x 10¹²
을 저장하기에 충분하므로 정상적으로 출력이 될 것 같지만, 결과는 전혀 다른 값이 출력된다. 그 이유는 int
타입과 int
타입의 연산결과는 int
타입이기 때문에 a*b
의 결과가 이미 오버플로우가 발생하여 long
타입으로 자동 형변환되어도 값은 변하지 않는다.
그렇기에 해당 연산이 진행되기 전 하나의 피연산자를 충분한 크기의 자료형으로 형변환해서 타입일치를 시켜 충분한 범위를 확보해야 한다.
public static void main(String[] args) {
int a = 1_000_000;
int b = 2_000_000;
long c = (long)a * b;
System.out.println(c);
}
======================================================
2000000000000
public static void main(String[] args) {
int start = 2_000_000_000;
int start = 2_100_000_000;
int mid = start + (end - start) / 2;
//int mid = (start + end) >>> 1; 도 가능하다.
//unsigned right shift -> 오른쪽으로 이동 후 남아 있는 빈 비트 공간을 0으로 채움. (음수 X)
System.out.println(mid);
}
(start+end) / 2
로 가운데 값을 구해버린 다면 오버플로우가 발생할 수 있으니 start
값에 (end - start) / 2
의 값을 더해주는 방향으로 하는 것이 더욱 안전하다.출처) https://whatisthenext.tistory.com/103
사칙연산의 피연산자로 숫자뿐만 아니라 문자도 가능하다. 문자는 실제로 해당 문자의 유니코드(부호없는 정수)로 바뀌어 저장되기 때문이다.
System.out.println('d' - 'a'); // 3
char c = 'a' + 1; //b
위 코드는 오류가 발생하지 않고 실행도 올바른 결과를 얻게 된다. int
보다 작은 타입의 피연산자를 int
타입으로 자동 형변환한다고 배웠는데 왜 문제가 없는걸까? 이는 리터럴 간의 연산이기 때문이다.
상수 또는 리터럴 간의 연산은 실행 과정동안 변하는 값이 아닌 컴파일 시에 컴파일러가 계싼해서 그 결과를 대체하기 때문에 컴파일 후에는 이미 char c = 'b';
가 되어있기 때문에 덧셈 연산이 수행되지 않는다.
하지만 수식에 변수가 들어가 있는 경우 컴파일러가 미리 계산을 할 수 없기 때문에 명시적 형변환을 해줘야 한다.
%
나머지 연산자는 왼쪽의 피연산자를 오른쪽 피연산자로 나누고 난 나머지 값을 결과로 반환하는 연산자이다.
나눗셈에서처럼 나누는 수(오른쪽 피연산자)로 0을 사용할 수 없다.
비트 연산자는 피 연산자를 비트단위로 연산하는데, 피 연산자를 이진수로 표현했을 때의 각 자리를 규칙에 따라 연산을 수행하며, 피연산자로 실수는 허용하지 않으며 정수만 허용된다.
int a = 3 & 1; // 0011 & 0001 = 0001(1)
int b = 2 | 1; // 0010 | 0001 = 0011(3)
int c = 3 ^ 1; // 0011 ^ 0001 = 1110(2)
// 2진수를 4자리로 표현하였지만 int타입간의 연산이라 32자리로 표현하는 것이 맞다.
&
(AND)|
(OR)^
(XOR)~
int d = ~10; //00001010(10) -> 11110101(-11)
// 2진수를 8자리로 표현하였지만 int타입간의 연산이라 32자리로 표현하는 것이 맞다.
~
(NOT)이러한 비트 전환연산자는 음수를 표현하기 위해 사용되는데 음수를 표현할 수 없는 컴퓨터의 제한적인 상황을 1의 보수를 통해 해결한 것이다.
>>
<<
int e = 8 >> 2; // 00001000(8) -> 00000010(2)
int f = 8 << 2; // 00001000(8) -> 00100000(32)
>>
(right SHIFT)<<
(left SHIFT)x << n은 x * 의 결과와 같다.
x >> n은 x / 의 결과와 같다.
주로 조건문과 반복문의 조건식에 사용되며, 연산결과는 오직 true
와 false
둘중 하나이다.(boolean
)
관계 연산자 역시 이항 연산자이므로 비교하는 피연산자의 타입이 서로 다를 경우에는 자료형의 범위가 큰 쪽으로 타입을 일치시킨 뒤 비교한다.
두 피연산자의 값의 크기를 비교하는 연산자로 참일 경우 true
, 아닐 경우 false를 반환한다.
기본형인 boolean
형을 제외하고 다 사용가능하지만 참조형에는 사용할 수 없다.
System.out.println(2 > 1); // true
System.out.println(7 < 4); // false
System.out.println(2 >= 2); // true
System.out.println(4 <= 3); // false
>
: 좌변 값이 크면 true 아니면 flase<
: 좌변 값이 작으면 true 아니면 flase>=
: 좌변 값이 크거나 같으면 true 아니면 flase<=
: 좌변 값이 작거나 같으면 true 아니면 flase두 피연산자의 값이 같은지 또는 다른지를 비교하는 연산자이다. 대소비교 연산자와는 다르게 참조형을 포함하여 모든 자료형에서 사용이 가능하다. 참조형의 경우에는 객체의 주소값을 저장하고 있기에 해당 주소값을 비교하여 값을 비교할 수 있다.
기본형과 참조형은 서로 형변환이 가능하지 않기 때문에 등가비교 연산자로 기본형과 참조형을 비교할 수는 없다.
class Animal{
...
}
Animal animal = new Animal();
Animal animalSecond = animal;
System.out.println(2 == 2); // true
System.out.println(4 != 4); // flase
System.out.println(animal == animalSecond); // true
=
: 두 값이 같으면 true 아니면 false!=
: 두 값이 다르면 true 아니면 falseSystem.out.println(10.0 == 10.0f); // true ---(1)
System.out.println(0.1 == 0.1f); // false ---(2)
(1) : 관계 연산자도 이항 연산자이므로 연산을 수행하기 전에 형변환을 통해 두 피연산자의 타입을 같게 맞춘 다음 피연산자를 비교한다. 10 == 10.0f
에서 더 범위가 넓은 자료형인 float으로 10을 변환한 뒤 비교한다.
(2) : (1)과 달리 결과가 false
가 나왔다. 왜 이런 결과를 얻는 걸까? 그것은 정수형과 달리 실수형은 근사값으로 저장되므로 오차가 발생할 수 있기 때문이다. 10.0f
는 오차없이 저장할 수 있는 값이라서 double
로 형변환해도 그대로 10.0
이 되지만, 0.1f
는 저장할 때 2진수로 변환하는 과정에서 오차가 발생한다. 물론 double
도 오차가 발생하지만 float
타입의 0.1f
보다 적은 오차로 저장된다.
float f = 0.1f; // 0.10000000149011612로 저장된다.
double d = 0.1; // 0.10000000000000001로 저장된다.
그렇다면 어떻게 실수형을 비교해야 할까?
double
타입의 값을 float
형변환한 다음 비교한다.x > 10 && x < 20 // x는 10보다 크고 20보다 작다.
i % 2 == 0 || i % 3 == 0 // i는 2의 배수 또는 3의 배수이다.
||
(OR 결합) : 피연산자 중 어느 한쪽만 true이면 true를 결과로 얻는다.&&
(AND 결합) : 피연산자 양쪽 모두 true이어야 true를 결과로 얻는다.ch < 'a' || ch > 'z' <-> !('a' <= ch && ch <= 'z')
!
: 피연산자가 true이면 false를, false면 true를 결과로 반환한다.참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용한다.
참조변수 instanceof 타입(클래스)
반환 타입은 Boolean
이며 반환 값이 true일 경우 참조변수가 검사한 타입으로 형변환이 가능하다는 의미가 된다.
public class Car {
...
}
public class FireEngine extends Car{
void water(){
System.out.println("물 뿌리기");
}
}
public class Ambulance extends Car{
void siren(){
System.out.println("웨옹 웨옹");
}
}
public class Test {
public static void main(String[] args) {
Car car = new FireEngine();
doShout(car);
}
static void doWork(Car car) {
if (car instanceof FireEngine) {
FireEngine fireEngine = (FireEngine)car;
fireEngine.water();
}
if (car instanceof Ambulance) {
Ambulance ambulance = (Ambulance)car;
ambulance.siren();
}
}
}
==================================================
물 뿌리기
위 코드에서 doWork
는 Car타입의 참조변수를 매개변수로 하는 메서드이다. 이 메서드가 호출될 때, 매개변수로 Car
클래스 또는 그 자식 클래스의 인스턴스를 넘겨받겠지만 메서드 내에서는 정확히 어떤 인스턴스인지 알 길 이 없다. 그래서 instanceof
연산자를 이용해서 참조변수가 가르키고 있는 인스턴스의 타입을 체크하고, 적절히 형변환한 다음에 작업을 해야한다.
이로써 조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있기 때문에, 참조변수의 타입과 인스턴스의 타입이 항상 일치하지 않는다는 것을 알 수 있다. 고로 instaceof
연산자를 통해 참조변수가 가르키고 있는 인스턴스의 타입을 확인 후 적절히 형변환할 수 있다는 것이다.
public class Test {
public static void main(String[] args) {
Car car = new FireEngine();
if (car instanceof FireEngine) {
System.out.println("this is a FireEngine instance.");
}
if (car instanceof Car) {
System.out.println("this is a Car instance.");
}
if (car instanceof Object) {
System.out.println("this is a Object instance.");
}
System.out.println(car.getClass().getName());
}
}
===============================================
this is a FireEngine instance.
this is a Car instance.
this is a Object instance.
FireEngine
null
은 어떤 것의 instance도 아님.
int x = 3;
// 좌측 피연산자 : x
// 우측 피연산자 : 3
대입연산자는 연산자를 기준으로 우측 피연산자의 값(식이라면 평가값)을 좌측 피연산자에 저장한다
만약 Reference Type
인 경우 주소값을 할당하는 것이다.
public class Number {
private final int value;
public Number(int value) {
this.value = value;
}
}
public class Test {
public static void main(String[] args) {
Number numberOne = new Number(1);
Number numberTwo = new Number(2); // ---(1)
numberOne = numberTwo; // ---(2)
}
}
int i = 3;
i = i + 2;
->
int i = 3;
i += 2;
대입 연산자는 다른 연산자(op)와 결합하여 op=
와 같은 방식으로 사용할 수 있다.
+=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
JAVA 8부터 람다 연산자(→)는 람다식을 도입하는 데 사용되는 연산자이다.
람다식은 간단히 말해서 메서드를 하나의 식으로 표현한 것이다. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 익명함수
이라고도 한다.
람다식을 설명하기 위해 먼저 함수형 인터페이스라는 개념을 알아보자.
추상메서드가 단 1개인 인터페이스로 하나 이상의 메서드를 가지게 된다면 함수형 인터페이스가 아니다.
static
메서드와 default
메서드의 개수에는 제약이 없다.@FunctionallInterface
애너테이션을 붙혀주면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는 지 확인해주니 꼭 붙이도록 하자!함수형 인터페이스를 람다식이 나오기 전에는 익명 내부 클래스를 만들어서 사용했다.
@FunctionalInterface
public interface Calculator {
public int add(int numOne, int numTwo);
}
public static void main(String[] args) {
Calculator calculator = new Calculator() {
@Override
public int add(int numOne, int numTwo) {
return numOne + numTwo;
}
};
}
하지만 람다식을 사용함으로써 간단히 처리할 수 있게 되었다.
람다식은 익명함수이기 때문에 기존 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통사이에 화살표 연산자를 넣어준다.
반환타입 메서드이름 (매개변수 선언) {
로직
}
------>
(매개변수 선언) -> {
로직
}
int max(int a, int b) {
return a > b ? a : b;
}
------>
(int a, int b) -> {
return a > b ? a : b;
}
반환값이 있는 메서드의 경우, return문 대신 식으로 대신할 수 있다. 식의 연산결과가 자동적으로 반환값이 되는데 이 때는 문장이 아닌 식이므로 끝에 ;
을 붙히지 않는다.
(int a, int b) -> { return a > b ? a : b; }
------>
(int a, int b) -> a > b ? a : b
public interface Calculator {
public int add(int numOne, int numTwo);
}
(인수타입 인수명) -> {로직}
Calculator cal = (int numOne, int numTwo) -> { return numOne + numTwo; };
(인수명) → {로직}
Calculator cal = (numOne, numTwo) -> { return numOne + numTwo; };
() -> {로직}
public interface Calculator {
public int add();
}
Calculator cal = () -> { return "addMethod"; };
(인수명) -> 로직
Calculator cal = (numOne, numTwo) -> numOne + numTwo;
인수명 -> 로직
public interface Calculator {
public int square(int num);
}
Calculator cal = num -> num * num;
자바에서 모든 메서드는 클래스내에 포함되어야 한다. 그렇다면 람다식은 어떤 클래스에 포함되어 있을 까?
람다식은 사실 익명 클래스의 객체와 동등하다.
(int a, int b) -> a > b ? a : b
new Object() {
int max(int a, int b) {
return a > b ? a : b;
}
}
그렇다면 이 익명개체의 메서드를 어떻게 호출할 수 있을까?
java.util.function 패키지에 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해놓았으니 활용하는 것이 좋다.
조건식 ? 식1(true인 경우) : 식2(false인 경우)
3항 연산자는 첫 번째 피연산자인 조건식의 평가결과에 따라 다른 결과를 반환한다.
조건식이 true
이면 2번째 피연산자인 식1
이, false
이면 식2
가 연산결과가 된다.
int x = 1;
int y = 2;
int result;
// 사용 전
if (x > y) {
result = x;
} else {
result = y;
}
//사용 후
result = x > y ? x : y;
출처) https://medium.com/@katekim720/연산자부터-조건-반복문까지-3d5cec6513d4
표에는 나와있지 않지만 괄호의 우선순위가 제일 높으며, 그 다음 산술 > 비교 > 논리 > 대입의 순서이다.
항은 단항 > 이항 > 삼항의 순서이며 연산자들의 진행방향은 좌측에서 우측으로 진행되며 예외적으로 단항 연산자와 대입 연산자의 경우에는 우측에서 좌측으로 진행된다.
기존에 존재하는 switch문에 표현식을 사용할 수 있게 확장되었다.
기존의 방법인 콜론 라벨(:
)을 통해 사용하는 것에서 추가적으로 화살표를 라벨(→
)로 사용함으로써 람다식을 사용할 수 있게 되었다. 그리고 fall through
가 없기 때문에 break
를 사용하지 않아도 된다.
:
대신 →
를 사용할 수 있다.static void howMany(int k) {
switch (k) {
case 1 -> System.out.println("one");
case 2 -> System.out.println("two");
default -> System.out.println("many");
}
}
howMany(1);
howMany(2);
howMany(3);
=======================
one
two
many
swtich가 확장되어 expression으로 사용할 수 있게 되었다.
static void howMany(int k) {
System.out.println(
switch (k) {
case 1 -> "one"
case 2 -> "two"
default -> "many"
}
);
}
public enum Day { SUNDAY, MONDAY, TUESDAY,
WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; }
Day day = Day.WEDNESDAY;
System.out.println(
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> throw new IllegalStateException("Invalid day: " + day);
}
);
즉, 다음과 같이 변수 할당을 할 수 있다.
T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};
yield 키워드가 추가되었다. 새로운 switch 표현식에는 full through
가 없고, break를 이용한 값 반환 방법이 없어지고, 그 대안으로 yield를 사용할 수 있게 되었다.
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> {
System.out.println(6);
yield 6;
}
case TUESDAY -> {
System.out.println(7);
yield 7;
}
case THURSDAY, SATURDAY -> {
System.out.println(8);
yield 8;
}
case WEDNESDAY -> {
System.out.println(9);
yield 9;
}
default -> {
throw new IllegalStateException("Invalid day: " + day);
}
};
추가적으로, 콜론 라벨을 사용한 switch 문에서도 사용이 가능하다.
int numLetters = switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
yield 6;
case TUESDAY:
System.out.println(7);
yield 7;
case THURSDAY:
case SATURDAY:
System.out.println(8);
yield 8;
case WEDNESDAY:
System.out.println(9);
yield 9;
default:
throw new IllegalStateException("Invalid day: " + day);
};