
Java는 오래된 언어답게 선행 타입이며, Java 10부터 도입된 var로 선언하면 타입 추론도 가능하다.
public class Main {
public static void main(String[] args) {
int v1 = 1;
var v2 = 2;
final int f1 = 1;
final int f2;
f1 = 2; // Cannot assign a value to final variable 'f1'
f2 = 2; // Ok
static final int s2 = 1; // Modifier 'static' not allowed here
}
static final int s1; // Variable 'a' might not have been initialized
static final int s2 = 1; // Ok
}
final로 선언된 변수는 선언/초기화를 분리할 수 있다.
하지만 초기화가 된 이후에 변경은 불가능하다.
static final은 다른 언어에서 const와 같다.
단, 함수 내부가 아닌, class 내에 선언되어야 한다.
이유는 초기화 시점이 다르기 때문이다.
instance는 생성될 때 Heap 영역에 로드되지만, class는 자바 런타임 환경인 JRE가 자바 컴파일러에 의해 생성된 자바 바이트 코드, .class 파일들과 java.lang 패키지 등을 프로그램 실행 시, JVM에 전달하고, JVM은 논리적으로 구분된 static 영역에 이들을 적재시킨다.
프로그램이 실행되면서 추후 사용할 자원들을 미리 배치하는 과정인데, class는 앞으로 찍어낼 instance가 생성될 때마다 계속해서 참조해서 사용될 데이터이기 때문에 미리 완성본을 static 영역에 저장하는 것이 효율적이기 때문이다.
이렇게 하면 instance를 수 억개 생성하더라도 결국 class는 static 영역에 원본 1개만 존재하면 된다.
static final로 생성된 변수는 이름 그대로 class와 함께 static 영역에 저장되는 변수이자, 변경될 수 없는 final이라는 특성을 띈다.
고로 타 언어의 const로 선언된 전역 변수와 매우 유사하다.
Java의 static final은 class 기반이라는 것이 차이이다.
그래서 class를 마치 namespace처럼 사용하는 모양새인데,
Person class의 numbers라는 static 변수가 있으면, Person.numbers와 같이 접근할 수 있는 형태여서 그렇다.
static이 모든 것을 객체로 설계하는 OOP와 다른 본질을 띄기 때문에 OOP 언어에서 배제되어야 한다는 이야기도 있었다고 하지만, Java, C#, C++ 등 현대의 대표적인 OOP 언어들은 모두 static 변수를
const c3 = 1;
var n3 = 1;
void main() {
var n1 = 1;
int n2 = 1;
final f1 = 1;
final f2;
f2 = 1;
const c1 = 1;
const c2; // Error: The const variable 'c2' must be initialized.
}
Java와 같은 선행 표기 방식이다.
const 키워드로 컴파일 타임 상수를 지정할 수 있다.
당연히 선언 + 초기화 동시에 되지 않으면 오류가 발생한다.
Java와 달리 class 외부에 전역 변수를 만들 수 있다.
final은 자바와 같다.
할당 역시 나중에 할 수 있다.
단, 할당이 되기 전에 사용하면 컴파일 오류가 뜬다.
Kotlin도 컴파일 뒤엔 자바와 같은 JVM 바이트코드가 되지만 모든 것을 class 내부에 선언해야 하는 자바와 달리, class 외부에 전역 변수를 작성할 수 있었다.
const c1 = 1; // Unresolved reference: a
const var c2 = 1; // Modifier 'const' is not applicable to 'vars'
const val c3 = 1; // Ok
var a2 = 1; // Ok
val b2 = 1; // Ok
fun main() {
const val c4 = 1; // Modifier 'const' is not applicable to 'local variable'
var a = 1;
val b = 2;
var c: Long = 3;
}
일단 기본적으로 var로 변수를 선언하며, 타입 추론(inference 혹은 deduce라고 부르더라)을 해주기 때문에 리터럴 값으로 초기화 된 값에 타입을 명시할 필요는 없다.
리터럴에 따른 기본 추론 타입이 아닌, Long 등의 특수한 타입으로 초기화가 필요한 경우에만 타입을 명시해주면 된다.
최신 언어답게 후행 타입이다.
TypeScript, Rust, Swift 등과 동일한 타입 어노테이션 문법이다.
const는 단독으로 사용될 수 없으며, 반드시 const val을 사용하여 선언 + 초기화 단계가 동시에 이루어지지 않아도 오류가 발생했다.
val은 재할당 불가능 키워드로 final을 좀 더 축약한 버전인데, 짧아진 것은 좋지만 var과 헷갈릴 여지가 조금 있어 보였다.
var, let으로 재할당 가능 여부를 나눴으면 더 좋았을지도 모르겠지만(그러면 Swift를 따라한다고 생각했을 수도..?) 훈수둘 짬도 안되고 무언가 사정이 있었겠거니 하고 넘어가자.
const를 단독으로 사용할 수 없는 이유가 static, static final을 자바에서 지원하기 때문에 이와 호환 시키기 위해서 const var, const val로 만든 것인가? 라는 생각이 들어 실험해 봤지만 const var은 오류가 발생한다.
무조건 const val만 된다.
왜 이렇게 만들었는지 궁금하지만 Java도 이제 막 제대로 보기 시작한 참이라 이유는 모르겠다.
전역 변수는 Dart와 마찬가지로 var, val로 선언된 변수 모두 사용이 가능했다.
특이한 점은, Primitive Types도 모두 객체라 Int, Long처럼 PascalCase였다.
그리고 Java와 달리 Unsigned Type이 존재한다.(UInt, UByte 등)
fun main(args: Array<String>) {
val a = 1; val b = 2
println(a) // 1
println(b) // 2
}
마지막으로 한 줄에 여러 변수를 선언하고 싶을 때, ;으로 구분해서 선언하면 Formatter가 줄바꿈을 하지 않고 그대로 냅둔다.
인텔리제이를 처음 만져봐서 이것저것 실험해보다가 다중 변수 선언을 하는데 오류 메세지로 ;을 쓰라고 알려줘서 알게 되었다.
Unexpected tokens (use ';' to separate expressions on the same line)
Rust 컴파일러한테 후두려 맞던 기억이 생각나지만 역시 컴파일러가 똑똑해야 맞으면서 하나라도 배워가는게 생기는 것 같다.