프로그래밍 언어/Java

[자바의 정석 스터디] Ch3. 연산자/ Ch4. 조건문과 반복문

hyuga_ 2024. 3. 25. 17:02

연산자와 피연산자

  • 연산자: 연산을 수행하는 기호 (+, -, *, /)
  • 피연산자: 연산자의 연산수행 대상

모든 연산자는 연산결과를 반환한다.

  • 즉, 연산결과를 반환하지 않으면 연산자가 아니다.
  • 예를 들어, () 괄호는 연산자가 아니다.

 

연산자의 종류

연산자는 총 5가지 종류가 있다.

 

  • 자바에서 not(!) 사용법: 느낌표로 묶어주면 된다.
    • ex) !(x > 3)

 

instanceof 객체지향에서 쓰는 연산자이다.

 

연산자의 우선순위, 결합법칙

두괄식 정리하면, 연산자의 우선순위와 결합법칙은 3가지만 기억하면 된다.

  1. 산술(+,-) > 비교(>,<) > 논리(&&,||) > 대입(=)
    • 대입 연산자는 제일 마지막에 수행된다
  2. 단항 (1) > 이항 (2) > 삼항 (3)
    • 단항 연산자의 우선순위가 이항 연산자보다 높다
    • (단항: 피연산자가 1개/ 이항: 피연산자가 2개 …)
  3. 단항 연산자, 대입 연산자를 제외한 모든 연산의 진행방향은 (→) 이다.

 

연산자의 우선순위

 

 

하나의 식에 연산자가 둘 이상 있을 때, 어떤 연산을 먼저 수행할지에 대한 규칙이다.

  • 괄호를 이용해서 우선순위를 바꿀 수 있다. 수학을 배웠다면 상식적인 개념
  • 거기서 Java에 맞게 조금만 업데이트하면 된다.

(연산자의 우선순위와 결합규칙)

 

 

연산자의 결합규칙

결합규칙: 우선순위가 같은 연산자가 있을 때, 어떤 것을 먼저 할건지에 대한 얘기이다.

3+4-5 라는 식이 있을 때, 덧셈 연산자(+)와 뺄셈 연산자(-)는 우선순위가 같다.

그럼 어떻게 하지? 마찬가지로 우린 자연스럽게 알고있다.

  • 왼쪽에서 오른쪽 (→) 으로 나아간다
  • 혹은 연산자는 자신의 오른쪽에 있는 피연산자에 붙는다 (결합된다)

위의 정리표를 보면 대입 연산자, 단항 연산자를 제외하면, 기본적으로 모두 왼쪽에서 오른쪽 (→) 방향이다.

 

 

형변환 연산자

형변환: 변수 또는 상수의 타입을 다른 타입으로 변환하는 것

바꾸는 방법은 쉽다: (타입) 피연산자

double d = 85.4;
int score = (int)d; 
System.out.println(score); // 85
  • 실수 → 정수일 때는 소숫점 아래를 자동으로 버린다.

자동 형변환

변수와 리터럴의 타입은 일치해야 하지만, 앞에서 배웠듯 Java는 유도리 있게 자동 형변환을 해준다.

float f = 1234; // 리터럴은 int이지만 float에 저장됨
  • float f = (float)1234; 으로 (float)이 자동으로 적용된 것이다.
  • 이는 컴파일러가 알아서 수행해준다.

그러나 더 큰 표현범위의 리터럴을 낮은 표현범위의 변수에 넣으려고 하면 에러가 발생한다.

int i = 3.14f; // 에러
int i = (int)3.14f; // ok 
  • 실수를 정수에 넣으면 소수점 아래를 버리는 값 손실이 발생하는데, 컴파일러가 이를 그냥 수행할 수는 없도록 설계된 것이다.
  • 그래서 사용자가 직접 ‘형변환 해!' 하고 형변환 연산자를 넣어준 경우에만 가능하다.

마찬가지로, byte → int 는 형변환 연산자를 생략 가능하다 (자동 형변환)

  • int → byte는 직접 형변환 연산자’(byte)’를 넣어줘야 한다.

 

자동 형변환 정리

형변환을 생략하면 컴파일러가 자동으로 형변환을 한다.

어떤 타입으로? → 기존의 값을 최대한 보존할 수 있는 타입으로 (큰 쪽으로)

 

 

왼쪽 타입의 리터럴을 오른쪽 타입의 변수에 담을 때는 모두 자동 형변환.

반대 방향은 값손실이 있을 수 있으므로 자동 형변환 되지 않고, 유저가 수동 형변환 해야한다.

 

 


사칙 연산자

뭔지 알겠지?: +, -, *, /

 

유의할 점:

(int) 10/ (int) 4 = 2.5가 아니라 (int) 2 가 나온다. 이러한 경우, 2.5를 얻으려면 피연산자 중 하나를 float로 바꿔줘야 한다.

그럼 나머지 하나도 float으로 취급되면서 (float) 10/ (float) 4= float 값인 2.5가 나온다.

 

왜지? → 산술변환 규칙 때문에!

 

나머지 연산자 (%)

피연산자를 나누고 남은 나머지를 반환한다.

int a = 11;
int b = 3;
System.out.println(a%b); // 2

% 연산자는 몇가지 특징을 지닌다.

  1. %의 양쪽 피연산자는 정수가 아니어도 된다. (수업에서는 정수만 허용된다고 하는데, 해보니까 되는데..?)
  2. 나누는 피연산자(오른쪽) 부호는 무시된다.
// %의 양쪽 피연산자는 정수가 아니어도 된다. 
double a = 11.0;
double b = 3.1;
System.out.println(a%b); // 1.6999999999999997

// 나누는 피연산자(오른쪽) 부호는 무시된다.
int a = 11;
int b = -3;
System.out.println(a%b); // 2

int c = -11;
int d = 3;
System.out.println(c%d); // -2

 

산술 변환⭐️

산술 변환: 연산 전에 피연산자들의 타입을 일치시키는 Java의 기능

  • 타입 다른 애들끼리 연산할 때, 사용자가 일일이 변수 타입 바꿔주긴 귀찮으니깐..

산순 변환에는 두 가지 중요한 규칙이 있다. (암기)

  1. 두 피연산자의 타입을 같게 일치시킨다. (둘 중에 더 큰 타입으로)
  2. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다.

하나씩 살펴보자.

 

1. 두 피연산자의 타입을 같게 일치시킨다. (값손실을 최소화하기 위해, 둘 중에 큰 타입으로 일치)

 

2. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다. (byte, char, short)

 

 

잘 이해해보자.

이를 이해하지 않으면 초보자때 실수를 종종 하게 된다.

int a = 1_000_000;
int b = 2_000_000;

long c = a * b; 
System.out.println(c); // -1454759936 (오버플로우)

‘결과값이 int 범위를 넘어가지만 long 타입 변수에 담았으니 됐겠지?’ 라고 생각했겠지만 오버플로우 된 다음에 long c 에 담기는 거라 상관없다.

이럴 때 a나 b 중에 하나를 (long)으로 바꿔주었다면, 자동으로 나머지도 산술 변환 처리되면서 long 타입의 결과값이 나왔을 것이다.

int a = 1_000_000;
int b = 2_000_000;

long c = (long)a * b; 
System.out.println(c); // 2,000,000,000,000
  • a가 (long)이니까, b도 자동으로 (int → long)이 된다.
  • long 간의 연산이 되어 정상적으로 연산이 처리된다.

 

Q. ‘2’ - ‘0’ 는 무엇이 될까?

A. (int) 2

둘 다 char 타입으로, 유니코드로 변환하면 50, 48이다.

char은 int보다 작은 타입이다. 따라서 자동으로 int로 바뀐다.

때문에 (int) 50 - (int) 48 = (int) 2가 된다.

반올림 - Math.round()

반올림은 round()라는 메소드를 통해 가능하다.

이 메소드는 Math라는 클래스에 소속되어 있다.

Math.round() 를 그냥 쓰면 소수점을 반올림한 정수를 반환한다.

long a = Math.round(4.5); // a에 5가 저장된다.

 

원하는 자리에서 반올림하기

살짝 구식으로 해야한다.

만약 소수점 셋째자리까지를 원한다면,

  • 1000을 곱해주고 → round() 먹인 뒤에 → 다시 1000을 나눠준다
double a = 4.52152;
double result = Math.round(a*1000); // 4521.52 기준으로 반올림 
System.out.println(result/1000); // 4.522

프린트할 때 int인 1000으로 나눠주었으나 result가 double임. 따라서 산술변환에 의해 double 값이 나온다.

double a = 4.52152;
System.out.println(Math.round(a*1000)/1000); 
// 4 -> (int)4522/(int)1000 이기 때문
System.out.println((double)Math.round(a*1000)/1000); 
// 4.522 -> (double)4522.0/(int)1000 이기 때문

따로 변수에 넣어줄 게 아니고 바로 print해야 하는 상황이라면, 이처럼 형변환 연산자를 활용해줘도 된다.

 

임의의 정수(난수) 만들기

랜덤한 수를 생성한다. 게임, 섞기 등을 구현할 때 자주 쓴다.

이 때 쓰이는 메서드는 Math 클래스의 random() 메서드이다.

Math.random()

  • 0.0과 1.0 사이의 임의의 double값을 반환한다
    • 0.0 ≤ Math.random() < 1.0
    • 0.0 ~ 0.9999 ..

원하는 정수를 얻는 법:

예를 들어 1~3 중에 random한 정수를 얻고싶다면?

  1. 각 변에 3을 곱한다.
0.0 * 3 <= Math.random() * 3 < 1.0 * 3
=  0.0 <= Math.random() * 3 < 3.0
  • 지금 얻을 수 있는 값의 범위는 0.0 ~ 2.999.. 이다
  • 우리는 1부터 얻고 싶다.
  1. 각 변을 int형으로 변환한다.
(int) 0.0 <= (int) Math.random() * 3 < (int) 1.0 * 3
=  0 <= (int) Math.random() * 3 < 3
  • 지금 얻을 수 있는 값의 범위는 0, 1, 2 이다.
  1. 각 변에 1을 더한다.
0 + 1 <= (int) Math.random() * 3 + 1 < 3 + 1
= 1 <= (int) Math.random() * 3 + 1 < 4
  • 지금 얻을 수 있는 값의 범위는 1, 2, 3 이다.

a ~ b 사이 정수를 얻는 방법을 일반화하면 다음과 같다.

int num = 0;
num = (int)Math.random()*(b-a+1) + a;
  1. 원하는 범위 내 값의 개수만큼 곱하기 (b-a+1)
  2. int로 형변환
  3. 최솟값만큼 더하기 (a)

예시1) 1~100 사이 정수 출력하기

Scanner scanner = new Scanner(System.in);

System.out.println("난수 생성 횟수를 입력하세요.");
int times = scanner.nextInt();
int num = 0;

// 난수 범위: 1~100, 정수 
for (int i=0; i < times ; i++) {
	num = (int)(Math.random()*100) + 1;
	System.out.println(num);
}

 

이름붙은 반복문

break는 하나의 반복문만 벗어날 수 있다.

그러나 중첩 반복문을 벗어나야 하는 상황도 있다.

  • 그런 경우 반복문에 이름을 붙여서 하나 이상의 반복문을 벗어날 수 있다

 

 

최상단 for문 앞에 Loop1 이라는 이름을 줬다.

  • continue Loop1 또는 break Loop1 와 같이 활용 가능하다.
  • break와 함께 쓰는 경우가 절대다수이다. (남궁성 강사님도 continue는 안써보심)

예제) 4반의 4번을 불러선 안 돼..

Loop: for (int i=1; i<=10; i++) {
	System.out.printf("%d반 - ",i);
	for (int j=1; j<=10; j++) {
		if (i == 4 && j ==4) {
			System.out.printf(" ");
			break Loop;
		}
		System.out.printf("%d ",j);
	}
	System.out.println();
}