intro : java 스코프, 형변환에 대한 개념을 알아보자.
스코프1 - 지역 변수와 스코프
변수는 선언한 위치에 따라 지역 변수, 멤버 변수(클래스 변수, 인스턴스 변수)와 같이 분류된다. 지역 변수는 이름 그대로 특정 지역에서만 사용할 수 있는 변수라는 뜻이다. 그 특정 지역을 벗어나면 사용할 수 없다. 여기서 말하는 지역이 바로 변수가 선언된 코드 블록 ({ }
)이다. 지역 변수는 자신이 선언된 코드 블록 안에서만 생존하고, 자신이 선언된 코드 블록을 벗어나면 제거된다. 따라서 이후에는 접근 할 수 없다.
public class Scope1 {
public static void main(String[] args) {
int m = 10; //m 생존 시작
if (true) {
int x = 20; //x 생존 시작
System.out.println("if m = " + m); //블록 내부에서 블록 외부는 접근 가능
System.out.println("if x = " + x);
} //x 생존 종료
//System.out.println("main x = " + x); //오류, 변수 x에 접근 불가
System.out.println("main m = " + m);
} //m 생존 종료
}
정리하자면 지역변수는 본인의 코드블록 안에서만 생존하고 자신의 코드블록을 벗어나면 제거되기 때문에 접근할 수 없다. 이렇게 변수의 접근 가능한 범위를 스코프(Scope) 라고 한다. 참고로 번역하면 범위라는 뜻이다. 아래 코드에서는 변수 m
은 main 메소드 안에서 접근 가능한 지역변수 이지만, 변수 i
는 for문 안에서만 접근 가능한 지역변수이다.
public class Scope2 {
public static void main(String[] args) {
int m = 10;
for (int i = 0; i < 2; i++) { //블록 내부, for문 내
System.out.println("for m = " + m); //블록 내부에서 외부는 접근 가능
System.out.println("for i = " + i);
} //i 생존 종료
//System.out.println("main i = " + i); //오류, i에 접근 불가
System.out.println("main m = " + m);
}
}
스코프2 - 스코프 존재 이유
변수를 선언한 시점부터 변수를 계속 사용할 수 있께 해도 되지 않을까? 왜 복잡하게 접근 범위라는 개념을 만들었을까?
public class Scope3_1 {
public static void main(String[] args) {
int m = 10;
int temp = 0;
if (m > 0) {
temp = m * 2;
System.out.println("temp = " + temp);
}
System.out.println("m = " + m);
}
}
위 코드를 보면 변수 m을 2배로 증가해서 출력하는 코드이다. 여기서 두배 증가한 값을 저장하기 위해 변수 temp를 사용하였는데, 해당 변수의 사용범위는 if 조건 안에서만 사용된다. 그런데 현재 temp 변수는 main 코드 블록안에 선언되어 있다. 이런경우 다음과 같은 문제가 발생한다.
비효율적 메모리 사용
temp는 if 코드 블록에서만 필요하지만 main 코드 블록이 종료될떄까지 메모리에 유지된다. 따라서 불필요한 메모리가 낭비된다. 만약 if 조건안에 temp를 선언했다면 더 효율적으로 메모리를 사용할 수 있다.
코드 복잡성 증가
좋은 코드는 군더더기가 없는 코드이다. temp변수는 if 조건 안에서만 필요하고 여기서만 사용하면 된다. 만약 if 조건안에서 temp 변수를 선언했다면 if 가 끝나고 temp를 전혀 생가하지 않아도 된다. 현재 위 코드는 간단한 코드이기에 복잡성이 증가한다는 말에 동의하기 어렵겠지만 실무환경에서는 하나라도 복잡성을 줄이는게 개발 생산성에 도움을 준다.
while문 vs for문 - 스코프 관점
아래 코드의 변수 스코프 관점에서 카운터 변수 i를 살펴보자. while문 예쩨에서는 i가 main 메소드 안에 선언되어 있고, for문 예제에서는 for문안에 선언되어 있다. 이런경우는 스코프의 제한이 for문이 더 제한적이기에 메모리 효올과 유지보수 관점에서 더 좋은점을 가지고 갈 수 있다.
// while문 예제
public class While2_3 {
public static void main(String[] args) {
int sum = 0;
int i = 1;
int endNum = 3;
while (i <= endNum) {
sum = sum + i;
System.out.println("i=" + i + " sum=" + sum);
i++;
}
//... 아래에 더 많은 코드들이 있다고 가정
}
}
// for문 예제
public class For2 {
public static void main(String[] args) {
int sum = 0;
int endNum = 3;
for (int i = 1; i <= endNum; i++) {
sum = sum + i;
System.out.println("i=" + i + " sum=" + sum);
}
//... 아래에 더 많은 코드들이 있다고 가정
}
}
형변환1 - 자동 형변환
작은 범위에서 큰 범위로는 당연히 값을 넣을 수 있다.(int
👉 long
👉 double
) 큰 범위에서 작은 범위는 다음과 같은 문제가 발생할 수 있다. (소수점버림, 오버플로우)
작음 범위에서 큰 범위로 대입은 허용한다.
자바에서 숫자를 표현할수 있는 범위는 다음과 같다. int
< long
< double
int
보다는 long
이, long
보다는 double
이 더 큰 범위를 표현할 수 있다. 작은 범위에서 큰 범위에 값을 대입하는 코드는 실행하면 특별한 문제없이 잘 수행된다. 자바는 기본적으로 같은 타입에 값을 대입할 수 있다. 그런데 다른 타입에 값을 대입하면 어떻게 될까? int
long
을 비교해보면 long
이 int
보다 더 큰 숫자 범위를 표현한다. 작은 범위 숫자 타입에서 큰 범위 숫자 타입에 대입을 하면 문제가 되지 않는다. 만약 이런 경우까지 오류가 발생한다면 개발이 너무 불편할 것이다. long
double
의 경우에도 double
은 부동 소수점을 사용하기 때문에 더 큰 숫자 범위를 표현한다. 따라서 대입할 수 있다. 정리하면 작은 범위에서 큰 범위로의 대입은 자바 언어에서 허용한다. 쉽게 이야기하면 큰 그릇은 작은 그릇에 담긴 내용물을 담을 수 있다.
public class Casting1 {
public static void main(String[] args) {
int intValue = 10;
long longValue;
double doubleValue;
longValue = intValue; // int -> long
System.out.println("longValue = " + longValue); //longValue = 10
doubleValue = intValue; // int -> double
System.out.println("doubleValue1 = " + doubleValue); //doubleValue1 = 10.0
doubleValue = 20L; // long -> double
System.out.println("doubleValue2 = " + doubleValue); //doubleValue2 = 20.0
}
}
자동형변환 (묵시적 형변환)
하지만 결국 대입하는 형(타입)을 맞추어야 하기 때문에 개념적으로는 다음과 같이 동작한다.
doubleValue = intValue
doubleValue = (double) intValue //형 맞추기
doubleValue = (double) 10 //변수 값 읽기
doubleValue = 10.0 //형변환
형변환2 - 명시적 형변환
큰 범위에서 작은 범위 대입은 명시적 형변환이 필요하다 만약 실수형 데이터를 정수형 타입에 담으면 어떻게 될까? double 타입의 데이터를 int에 담아보겠다. 명시걱으로 타입을 지정해서 형변환을 해주지 않으면 컴파일 오류가 발생한다.
public class Casting2 {
public static void main(String[] args) {
double doubleValue = 1.5;
int intValue = 0;
//intValue = doubleValue; //컴파일 오류 발생
intValue = (int) doubleValue; //형변환
System.out.println(intValue); //출력:1
}
}
형변환
하지만 만약 이런 위험을 개발자가 직접 감수하고도 값을 대입하고 싶다면 데이터 타입을 강제로 변경할 수 있다. 예를들어서 대략적인 결과를 보고싶은데 이때 소수점을 버리고 정수로만 보고 싶을 수 있다. 우리는 아래 코드와 같이 개발자가 직접 타입을 변환하는 것을 명시적 형변환 이라고 한다.
double doubleValue = 1.5;
intValue = (int) doubleValue; //형변환, intValue에 1이 할당 됨
형변환과 오버플로우
형변환을 할때 만약 작은 숫자가 표현할 수 있는 범위를 넘어서면 어떻게 될까? 아래코드에서 변수 maxIntOver 값을 intValue에 int 타입으로 변환하게되면 오버플로우가 발생한다. 정수형에서의 오버플로우는 int 타입으로 담을수 없는 큰 값이 들어오는경우 발생한다. 그렇기에 오버플로우가 발생하지 않도록 변수의 타입을 적절하게 잘 지정해 주어야 한다.
public class Casting3 {
public static void main(String[] args) {
long maxIntValue = 2147483647; //int 최고값
long maxIntOver = 2147483648L; //int 최고값 + 1(초과)
int intValue = 0;
intValue = (int) maxIntValue; //형변환
System.out.println("maxIntValue casting=" + intValue); //출력:2147483647
intValue = (int) maxIntOver; //형변환
System.out.println("maxIntOver casting=" + intValue); //출력:-2147483648
}
}
오버플로우, 언더플로우 조금만 더 알아보자!
정수 (시계열이라고 생각하자)
오버플로우 : 최댓값을 넘으면 최솟값으로 되돌아갑니다.
언더플로우 : 최솟값을 넘으면 최댓값으로 되돌아갑니다.
실수 (표현 못하는 숫자가 되니까 무한대 혹은 0으로 표현한다고 생각하자)
오버플로우 : 값이 너무 커지면 Inf(무한대)가 됩니다.
언더플로우 : 값이 너무 작아지면 서브노멀 값으로 표현되거나 0에 가까워집니다.
계산과 형변환
형변환은 대입 뿐만 아니라, 계산을 할 때도 발생한다. 알고리즘 문제 풀이시 오버플로우가 발생한다면 해당 개념이 부족해서 많이 발생하는거라고 볼 수 있다. (사실 내 얘기다…) 자바에서 계산은 다음과 같은 2가지를 기억하면 된다. 같은 타입끼리의 계산은 같은 타입의 결과를 낸다, 서로다른 타입의 계산은 큰 범위로 자동형변환이 일어난다.
public class Casting4 {
public static void main(String[] args) {
int div1 = 3 / 2;
System.out.println("div1 = " + div1); //1
double div2 = 3 / 2; // 정수끼리의 연산결과를 실수형 double에 담을 뿐이기에 결과는 1.0 이다.
System.out.println("div2 = " + div2); //1.0
double div3 = 3.0 / 2; // 실수형과, 정수형의 연산은 실수형의 결과로 반환된다.
System.out.println("div3 = " + div3); //1.5
double div4 = (double) 3 / 2;
System.out.println("div4 = " + div4); //1.5
int a = 3;
int b = 2;
double result = (double) a / b;
System.out.println("result = " + result); //1.5
}
}