코틀린 베이직 KB 06 숫자 타입 — Int 하나 뒤의 int 와 Integer
코틀린 베이직 · 6/26

KB 06 숫자 타입 — Int 하나 뒤의 int 와 Integer

gabury1고친 사람 github-actions[bot]

0. 1 은 Long 에 들어가는데 Int 는 안 들어간다

자바에서 long m = i; 는 생각 없이 쓰는 줄입니다. int 를 long 에 넣어도 값이 잘릴 일이 없으니까요. 코틀린으로 같은 걸 써 봤습니다.

Kotlin
fun main() {
    val l: Long = 1
    val i: Int = 1
    val m: Long = i
}
  • fun main() — 프로그램이 시작하는 함수입니다. 클래스 없이 파일 바로 아래에 둡니다
  • val l: Long = 1 — 다시 대입할 수 없는 변수 l 을 선언합니다. 코틀린은 이름 뒤에 콜론, 그 뒤에 타입을 씁니다
  • Int·Long — 코틀린의 정수 타입입니다. 자바의 int·long 에 해당하는데 첫 글자가 대문자입니다

First.kt 로 저장하고 코틀린 컴파일러 kotlinc 에 넣었습니다.

터미널
$ kotlinc First.kt -d out
First.kt:4:17: error: initializer type mismatch: expected 'Long', actual 'Int'.
    val m: Long = i
                ^

-d out 은 결과물을 out 폴더에 쓰라는 옵션입니다. 에러는 초깃값의 타입이 선언한 타입과 다르다는 뜻입니다.

에러는 4번째 줄 하나뿐입니다. 2번째 줄 val l: Long = 1 은 통과했습니다. 숫자 1 은 Long 에 들어가는데, 값이 1 인 Int 변수는 안 들어갑니다.

같은 내용을 자바로 옮겼습니다.

Java
public class First {
    public static void main(String[] args) {
        long l = 1;
        int i = 1;
        long m = i;
        System.out.println(m);
    }
}
터미널
$ javac -d jout First.java
$ java -cp jout First
1

자바는 경고 하나 없이 컴파일하고 1 을 찍었습니다.

수수께끼가 하나 더 있습니다. 자바에는 원시 타입 int 와 래퍼 클래스 Integer 가 따로 있는데, 코틀린에는 Int 하나뿐입니다. JVM 에서 도는 이상 결과물에는 둘 중 하나가 적혀야 합니다. 그럼 누가 어느 쪽을 고를까요.

이 편은 그 답부터 풉니다. 1절이 int 와 Integer 가 갈리는 규칙, 2절이 Int 를 Long 에 못 넣는 이유입니다. 3절부터는 리터럴(소스에 직접 적은 1·1.5 같은 값), 연산, Char 를 차례로 자바와 맞댑니다. 출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.

1. Int 하나 뒤에서 int 와 Integer 가 갈리는 규칙

코틀린 소스의 Int 가 결과물에서 int 가 될지 Integer 가 될지는 소스만 봐서는 안 보입니다. 이 절에서는 Int 를 다섯 가지 모양으로 받는 함수를 만들고, 클래스 파일에 적힌 선언 목록을 javap 로 열어 봅니다. JDK 에 딸린 도구이고, -p 는 private 멤버까지 보여 달라는 옵션입니다.

Kotlin
fun plain(x: Int) {}
fun nullable(x: Int?) {}
fun list(xs: List<Int>) {}
fun intArray(xs: IntArray) {}
fun boxedArray(xs: Array<Int>) {}
  • Int? — 타입 뒤의 ? 는 null 도 담는다는 표시입니다. 이런 타입을 널 허용 타입(nullable type)이라 부릅니다. 그냥 Int 에는 null 을 못 넣습니다
  • List<Int> — 자바의 List<Integer> 처럼, 제네릭 타입 List 에 타입 인자 Int 를 준 것입니다
  • IntArray — 정수만 담는 배열 전용 타입입니다
  • Array<Int> — 아무 타입이나 담는 배열 Array 에 타입 인자 Int 를 준 것입니다

함수 본문 {} 는 비워 뒀습니다. 볼 것은 매개변수 타입뿐입니다.

코틀린 컴파일러는 파일 바로 아래에 쓴 함수와 변수(톱레벨 선언)를 파일 이름 + Kt 라는 클래스의 static 멤버로 넣습니다. 그래서 Kinds.kt 의 함수는 KindsKt 클래스에서 찾습니다.

터미널
$ kotlinc Kinds.kt -d out
$ javap -p out/KindsKt.class
Compiled from "Kinds.kt"
public final class KindsKt {
  public static final void plain(int);
  public static final void nullable(java.lang.Integer);
  public static final void list(java.util.List<java.lang.Integer>);
  public static final void intArray(int[]);
  public static final void boxedArray(java.lang.Integer[]);
}

소스에는 전부 Int 라고 썼는데, 결과물에는 int 와 Integer 가 섞여 나왔습니다. 줄을 맞추면 이렇습니다.

코틀린 매개변수 클래스 파일의 타입 갈린 이유
Int int 기본은 원시 타입
Int? java.lang.Integer null 을 담아야 한다
List<Int> java.util.List<java.lang.Integer> 제네릭 타입 인자다
IntArray int[] 제네릭이 아닌 원시 타입 배열 전용 타입이다
Array<Int> java.lang.Integer[] Array 가 제네릭 클래스라 타입 인자다

이 다섯 줄에서 보이는 규칙은 둘입니다. null 을 담아야 하면 Integer 입니다. 제네릭 타입의 타입 인자로 들어가도 Integer 입니다. 둘 다 아니면 int 입니다. 코틀린 문서의 설명도 같습니다(공식 문서).

두 배열이 갈린 까닭도 이 규칙에서 나옵니다. Array<Int> 는 Array 가 제네릭 클래스라 Int 가 타입 인자로 들어가고, 그래서 Integer[] 입니다. 자바에 int[] 가 있는데도 Integer[] 가 된 것은 코틀린의 Array 가 제네릭이기 때문입니다.

IntArray 는 제네릭이 아닌 별도 타입이라 int[] 입니다. 원소를 감싸지 않는 배열이 필요할 때 Array<Int> 대신 씁니다.

제네릭에 Int 를 넣으면 왜 Integer 인가

제네릭 쪽 규칙은 코틀린이 처음 정한 게 아닙니다. 자바에서 static void list(List<int> xs) {} 를 컴파일하면 javac 가 error: unexpected type 과 함께 required: reference 를 냅니다.

required: reference 는 여기에 참조 타입, 곧 객체 타입이 와야 한다는 뜻입니다. 자바 제네릭은 컴파일 뒤 타입 인자를 Object 로 지웁니다. 이것을 타입 소거(type erasure)라 부릅니다. 지운 뒤에는 Object 로 다루니 객체만 받을 수 있습니다.

그래서 자바에서는 List<Integer> 라고 손으로 고쳐 씁니다. 코틀린에서는 컴파일러가 List<Int> 를 같은 모양으로 바꿔 줍니다.

감싸고 푸는 코드는 컴파일러가 넣는다

int 값을 Integer 타입 변수에 넣으려면 객체로 감싸야 합니다. 이 일을 박싱(boxing)이라 부르고, 반대로 꺼내는 일을 언박싱(unboxing)이라 부릅니다.

자바에서는 이 감싸고 푸는 코드를 컴파일러가 알아서 넣어 줍니다. 그것을 오토박싱(autoboxing)이라 부릅니다.

코틀린에서는 누가 언제 감싸는지, 클래스 파일을 자바 코드로 되돌리는 디컴파일러 CFR 로 봤습니다. 메서드 안에서 벌어지는 일은 선언 목록에 안 나오니 이쪽을 씁니다.

Kotlin
fun maybe(x: Int): Int? {
    return x
}

fun first(xs: List<Int>): Int {
    return xs[0]
}
  • ): Int? — 괄호 뒤 콜론 다음이 반환 타입입니다. maybe 는 Int 를 받아 Int? 로 돌려줍니다
  • xs[0] — 목록의 0번 원소입니다. 자바의 xs.get(0) 에 해당합니다
터미널
$ kotlinc Box.kt -d out
$ java -jar cfr.jar out/BoxKt.class --sugarboxing false
...
public final class BoxKt {
    @Nullable
    public static final Integer maybe(int x) {
        // ↓ 감싼다(박싱)
        return Integer.valueOf((int)x);
    }

    public static final int first(@NotNull List<Integer> xs) {
        Intrinsics.checkNotNullParameter(xs, (String)"xs");
        // ↓ 푼다(언박싱)
        return ((Number)xs.get((int)0)).intValue();
    }
}
  • 맨 위의 머리 주석, import, 한 줄이 아주 긴 @Metadata(...)(코틀린 컴파일러가 남기는 부가 정보)는 ... 로 줄였습니다. 디컴파일 결과는 소스를 되살린 것이 아니라, 클래스 파일에 적힌 내용을 자바 문법으로 다시 적은 것입니다
  • --sugarboxing false — 박싱 호출을 감추지 말라는 CFR 옵션입니다. 기본값이 true 라서, 옵션 없이 돌리면 return x; 처럼 감춘 모양으로 보여 줍니다
  • @Nullable·@NotNull — null 을 허용하는지를 컴파일러가 표시한 애노테이션입니다
  • Intrinsics.checkNotNullParameter — 인자가 null 이면 NullPointerException 을 던지는 검사입니다
  • (int)x·(int)0 — CFR 이 타입을 드러내려고 붙인 캐스트입니다. 소스에는 없습니다

maybe 는 Integer.valueOf(x) 로 감싸서 돌려줍니다. 반환 타입이 Int? 라 Integer 로 나가야 하기 때문입니다. first 는 목록에서 꺼낸 객체를 intValue() 로 풀어서 int 로 돌려줍니다.

같은 두 메서드를 자바로 쓰고, 같은 옵션으로 열었습니다.

Java
import java.util.List;

public class JBox {
    static Integer maybe(int x) { return x; }
    static int first(List<Integer> xs) { return xs.get(0); }
}
터미널
$ javac -d jout JBox.java
$ java -jar cfr.jar jout/JBox.class --sugarboxing false
...
public class JBox {
    static Integer maybe(int n) {
        // ↓ 코틀린과 같은 호출
        return Integer.valueOf((int)n);
    }

    static int first(List<Integer> list) {
        // ↓ (Number) 캐스트만 없다
        return list.get((int)0).intValue();
    }
}

매개변수 이름이 n·list 로 바뀐 것은 javac 가 기본 설정에서 지역 변수 이름을 남기지 않아 CFR 이 지어 붙였기 때문입니다.

자바 오토박싱과 같은 호출입니다. 다른 것은 코틀린 쪽의 (Number) 캐스트 하나입니다. 코틀린 컴파일러는 꺼낸 객체를 Number 로 받아 intValue() 를 부르는 모양으로 풀고, 자바는 Integer 에서 바로 intValue() 를 부릅니다.

Number 는 Integer 의 부모 클래스입니다. 어느 쪽으로 받든 실제로 불리는 것은 Integer 의 intValue() 입니다.

null 이나 제네릭이 끼지 않아도 감쌀 때가 있습니다. Any 는 자바의 Object 에 해당하는 코틀린 타입입니다. Int 를 이 타입으로 돌려주는 함수 fun f(x: Int): Any = x 를 하나 썼습니다.

등호 뒤에 돌려줄 값을 바로 적는 표기라 중괄호가 없습니다. 같은 옵션으로 열면 Object f(int x) 안에서 Integer.valueOf((int)x) 를 부릅니다. Int 를 객체 타입으로 넘길 때도 Integer 가 됩니다.

정리하면, 코틀린의 Int 는 평소 int 로 컴파일되고, null 이나 제네릭이 끼거나 객체 타입으로 넘어갈 때 Integer 가 됩니다. 그 사이의 박싱과 언박싱은 컴파일러가 선언 모양을 보고 넣습니다.

2. 자동 확장 변환이 없다

자바는 int 를 long 에 넣을 때 값을 알아서 넓혀 줍니다. 이렇게 좁은 타입의 값을 넓은 타입으로 바꾸는 변환을 확장 변환(widening conversion)이라 부릅니다(자바 언어 명세 5.1.2).

이 절에서는 0절의 에러가 대입 한 줄에서만 나는지 확인합니다. Int 를 Long 매개변수에 넘기는 호출과, Int 를 Double 에 넣는 대입을 한 파일에 담았습니다.

Kotlin
fun takeLong(x: Long) {}

fun main() {
    val i: Int = 1
    takeLong(i)
    val d: Double = i
}

kotlinc 는 5번째 줄과 6번째 줄에 에러를 하나씩 냈습니다.

터미널
$ kotlinc Widen.kt -d out
Widen.kt:5:14: error: argument type mismatch: actual type is 'Int', but 'Long' was expected.
    takeLong(i)
             ^
Widen.kt:6:19: error: initializer type mismatch: expected 'Double', actual 'Int'.
    val d: Double = i
                  ^

둘 다 막혔습니다. 함수 인자로 넘겨도, Double 에 넣어도 같은 종류의 에러입니다.

같은 두 줄을 자바로 옮기고, 반대 방향인 long → int 대입을 한 줄 더했습니다. Int 범위를 넘는 값을 담은 long big 을 int j 에 넣는 9번째 줄입니다.

Java
public class Widen {
    static void takeLong(long x) {}

    public static void main(String[] args) {
        int i = 1;
        takeLong(i);
        double d = i;
        long big = 3_000_000_000L;
        int j = big;
    }
}
터미널
$ javac -d jout Widen.java
Widen.java:9: error: incompatible types: possible lossy conversion from long to int
        int j = big;
                ^
1 error

에러는 9번째 줄 하나뿐입니다. takeLong(i); 와 double d = i; 는 통과했습니다.

자바는 int → long, int → double 처럼 넓은 쪽으로 가는 변환을 알아서 해 주고, 값이 잘릴 수 있는 좁은 쪽만 막습니다.

코틀린에는 이 확장 변환이 없습니다. 대입에서도 함수 인자에서도 숫자 타입을 저절로 바꾸지 않습니다(공식 문서). 넓은 쪽이든 좁은 쪽이든 타입이 다르면 에러입니다.

변환은 to타입() 함수를 손수 부른다

그러면 바꿀 때는 무엇을 쓰는지, 그렇게 바꾼 값이 어떻게 나오는지 봤습니다.

Kotlin
fun main() {
    val i: Int = 1
    val l: Long = i.toLong()
    val sum = i + 1L
    val big: Long = 3_000_000_000
    val cut: Int = big.toInt()
    println(l)      // 1
    println(sum)    // 2
    println(cut)    // -1294967296
}
  • i.toLong() — Int 를 Long 으로 바꾸는 함수입니다. toInt()·toDouble() 처럼 숫자 타입마다 to타입() 이 있습니다
  • 1L·3_000_000_000 — L 은 Long 리터럴 표시, 밑줄은 자릿수를 끊어 읽는 표시입니다. 3절에서 봅니다
  • val sum = i + 1L — 타입을 안 적으면 오른쪽 값에서 타입을 알아냅니다

실행은 kotlin 명령으로 했습니다. java 와 같은 일을 하되 코틀린 표준 라이브러리를 classpath 에 스스로 넣는 명령입니다.

터미널
$ kotlinc Convert.kt -d out
$ kotlin -cp out ConvertKt

다섯 줄 모두 에러 없이 컴파일됐습니다. 찍힌 값은 코드 오른쪽 주석에 붙여 뒀습니다.

눈여겨볼 줄은 big.toInt() 입니다. 30 억을 담은 Long 을 Int 로 줄였더니 -1294967296 이 찍혔습니다. 좁히는 쪽으로 가는 변환에서 코틀린은 값을 검사하지 않습니다. 넘치는 비트를 그냥 버립니다. 자바도 같은데, 자바는 이 자리에 (int) 캐스트를 손으로 붙이게 합니다 — 위의 int j = big; 이 possible lossy conversion 으로 막힌 것이 그 때문입니다. 코틀린은 캐스트 대신 toInt() 라는 함수 이름을 붙이게 하고, 붙이고 나면 자바 캐스트처럼 잘립니다.

남은 하나가 val sum = i + 1L 입니다. 대입은 막혔는데 덧셈은 에러 없이 2 를 찍었습니다. 그 이유는 연산자 쪽에 있습니다. 코틀린의 + 는 plus 함수 호출을 짧게 쓴 것입니다. Int 에는 Long 을 받아 Long 을 돌려주는 plus 가 따로 있습니다(공식 문서). 받는 타입마다 함수가 있으니 변환 없이 계산이 됩니다.

3. 리터럴 — 1 은 무엇의 1 인가

0절에서 val l: Long = 1 은 통과했습니다. 소스에 직접 적은 값 1 을 리터럴(literal)이라 부르는데, 이 리터럴의 타입이 어떻게 정해지는지 이 절에서 봅니다. 타입을 안 적은 톱레벨 변수 여덟 개와 Long 이라고 적은 하나를 javap -p 로 열었습니다.

Kotlin
val a = 1
val b = 3_000_000_000
val c = 1L
val d = 1_000_000
val e = 0xFF
val f = 0b1010
val g = 1.5
val h = 1.5f
val one: Long = 1

톱레벨 val 은 클래스 파일에서 private static final 필드와 게터로 바뀝니다. 그래서 필드 타입을 보면 컴파일러가 고른 타입이 보입니다.

터미널
$ kotlinc Literals.kt -d out
$ javap -p out/LiteralsKt.class
Compiled from "Literals.kt"
public final class LiteralsKt {
  private static final int a;
  private static final long b;
  private static final long c;
  private static final int d;
  private static final int e;
  private static final int f;
  private static final double g;
  private static final float h;
  private static final long one;
...

뒤쪽의 게터 아홉 개와 static {}(필드에 초깃값을 넣는 정적 초기화 블록)는 ... 로 줄였습니다.

리터럴 필드 타입 자바와 비교
1 int 같다
3_000_000_000 long 자바는 L 없이 쓰면 integer number too large 에러
1L long 같다
1_000_000 · 0xFF · 0b1010 int 밑줄·16진수·2진수 표기 모두 같다
1.5 double 같다. 자바도 float 에 넣으면 possible lossy conversion from double to float 에러
1.5f float 같다
val one: Long = 1 의 1 선언이 Long 이라 long 자바의 long one = 1; 은 int 값을 확장 변환

3_000_000_000 은 L 이 없는데 long 이 됐습니다. 타입을 안 적으면 Int 범위에 들어가는 값은 Int 로 정해집니다. 범위를 넘치는 값은 Long 으로 정해집니다.

val one: Long = 1 에서 중요한 것은 필드 타입이 아니라 에러 없이 컴파일됐다는 점입니다. 필드 타입은 선언한 Long 이 정하니, javap 만으로는 1 이 어떻게 들어갔는지 가를 수 없습니다.

근거는 둘입니다. 코틀린에는 확장 변환이 없으니 Int 값을 넣었다면 0절 4번째 줄처럼 에러가 났어야 합니다. 그런데 0절 2번째 줄 val l: Long = 1 은 통과했습니다. 공식 문서도 접미사 없는 정수 리터럴은 주변 문맥이 타입을 정해 줄 때까지 타입을 확정하지 않는다고 적습니다(공식 문서).

0절에서 1 은 되고 i 는 안 된 이유가 이것입니다. 리터럴의 타입은 들어갈 타입에 맞춰 정해집니다. 이미 Int 로 정해진 변수 i 는 바뀌지 않습니다.

자바에서 되던 리터럴이 막히는 곳

이번에는 틀린 리터럴을 모았습니다.

Kotlin
val big: Int = 3_000_000_000
val ratio: Float = 1.5
val lower = 1l
val octal = 010
val d = 1.5d

다섯 줄 모두 에러입니다. 앞 둘은 타입 에러, 뒤 셋은 표기 에러입니다. 5번째 줄은 에러를 셋 냈는데, 첫 에러만 옮기고 나머지는 ... 로 줄였습니다.

터미널
$ kotlinc BadLiterals.kt -d out
BadLiterals.kt:1:14: error: initializer type mismatch: expected 'Int', actual 'Long'.
val big: Int = 3_000_000_000
             ^
BadLiterals.kt:2:18: error: initializer type mismatch: expected 'Float', actual 'Double'.
val ratio: Float = 1.5
                 ^
BadLiterals.kt:3:14: error: use 'L' instead of 'l'.
val lower = 1l
             ^
BadLiterals.kt:4:13: error: leading zeros are not allowed in integer literals.
val octal = 010
            ^^^
BadLiterals.kt:5:12: error: unresolved reference 'd'.
val d = 1.5d
           ^
...

앞의 두 줄은 위 표에서 본 대로 자바도 막습니다.

갈리는 것은 뒤의 세 줄입니다. 셋 다 자바에서는 컴파일되는 표기입니다. 앞의 둘을 자바로 찍어 봤습니다.

Java
public class Octal {
    public static void main(String[] args) {
        System.out.println(010);   // 8
        System.out.println(1l);    // 1
    }
}
터미널
$ javac -d jout Octal.java
$ java -cp jout Octal

소문자 l 은 숫자 1 과 헷갈리는 표기라, 코틀린은 대문자 L 만 받습니다. 자바는 1l 을 받아 1 을 찍었습니다.

자바의 010 은 10 이 아니라 8진수로 읽혀 8 이 됐습니다. 앞에 0 을 붙여 자릿수를 맞췄다가 값이 바뀌는 사고가 날 수 있는 표기입니다. 코틀린은 8진수 리터럴을 지원하지 않고(공식 문서), 맨 앞의 0 자체를 에러로 막습니다.

자바의 1.5d 는 double 이라는 표시지만, 코틀린에는 D 접미사가 없습니다. 소수점 리터럴은 원래 Double 이라 붙일 필요가 없습니다. unresolved reference 'd' 는 1.5 뒤의 d 를 접미사가 아니라 d 라는 이름으로 읽었는데 그런 이름이 없다는 에러입니다.

자주 부딪히는 차이를 모으면 이렇습니다. L 없이 쓴 큰 수는 Long 이 됩니다. 소문자 l, 8진수 010, D 접미사는 에러입니다. (코틀린에만 있는 부호 없는 리터럴 1u 같은 표기는 다른 편에서 다룹니다.)

4. 연산 — 계산은 같고 비트 연산의 이름이 다르다

정수 나눗셈과 오버플로, 비트 연산을 한 파일에 모아 코틀린과 자바의 출력을 맞댑니다. 계산 결과가 자바와 같은지, 다른 것이 있다면 어디인지 가리려는 것입니다.

Kotlin
fun main() {
    val x = 7
    println(x / 2)             // 3
    println(x / 2.0)           // 3.5
    println(-x / 2)            // -3
    println(-x % 2)            // -1
    println(Int.MAX_VALUE + 1) // -2147483648
    println(x shl 2)           // 28
    println(x and 3)           // 3
    println(x or 8)            // 15
    println(x xor 1)           // 6
    println(x.inv())           // -8
    println(-x ushr 28)        // 15
}

오른쪽 주석이 그 줄이 찍은 값입니다.

  • Int.MAX_VALUE — Int 의 최댓값입니다. 자바의 Integer.MAX_VALUE 에 해당합니다
  • x shl 2·x and 3 — 비트 연산입니다. 점도 괄호도 없는 이 표기는 아래에서 풉니다
  • x.inv() — 모든 비트를 뒤집습니다. 자바의 ~x 입니다

같은 식을 자바 연산자로 옮긴 Ops.java 와 나란히 돌렸습니다.

터미널
$ kotlinc Ops.kt -d out
$ kotlin -cp out OpsKt
$ javac -d jout Ops.java
$ java -cp jout Ops

출력은 양쪽 다 11줄이고 한 글자도 다르지 않았습니다. 위 주석의 값이 자바에서도 그대로 나왔다는 뜻입니다.

앞의 다섯 줄이 말하는 것은 이렇습니다. 정수끼리 나누면 소수부를 버리고(3), 한쪽이 Double 이면 결과도 Double 입니다(3.5). 음수의 몫도 0 쪽으로 버리지 내림이 아니며(-3), 나머지의 부호는 왼쪽 피연산자를 따릅니다(-1). 넘치면 최솟값으로 돌아갑니다(-2147483648). 다섯 줄 다 자바 그대로입니다.

Kotlin 2.4.10 에서는 오버플로가 나는 식도 kotlinc 가 경고 없이 컴파일했습니다. 실행 중 예외도 나지 않았습니다. 코틀린이 오버플로를 잡아 주지는 않습니다.

shl 은 연산자가 아니라 함수다

다른 것은 비트 연산의 표기뿐입니다. 위 코드의 뒤쪽 여섯 줄을 자바와 맞대면 이렇습니다.

코틀린 자바 출력
x shl 2 x << 2 28
x and 3 x & 3 3
x or 8 x | 8 15
x xor 1 x ^ 1 6
x.inv() ~x -8
-x ushr 28 -x >>> 28 15

코틀린에는 << 가 없습니다. return x << 2 라고 쓴 Shift.kt 는 에러 셋을 냈는데, 첫 에러가 return type mismatch: expected 'Int', actual 'Boolean'. 입니다. << 를 모르니 앞의 < 를 비교 연산자로 읽은 것입니다.

코틀린의 shl·and 같은 이름은 중위 함수(infix function)입니다. 중위 함수는 x.shl(2) 를 점과 괄호 없이 x shl 2 로 쓰게 해 주는 함수 선언 방식입니다. 비트 연산 가운데 inv() 만 인자가 없어서 보통 함수 호출로 씁니다.

바뀐 것은 이름뿐이고 계산은 그대로입니다. 위 표의 출력 칸이 하나인 것은 여섯 줄 다 양쪽이 같은 값을 찍었다는 뜻입니다. 자바에서 연산자 자리였던 것이 코틀린에서 함수 이름 자리가 됐을 뿐입니다.

5. Char 는 숫자가 아니다

자바의 char 는 정수처럼 계산됩니다. 코틀린의 Char 도 그런지, 같은 네 식을 두 언어로 찍었습니다.

Kotlin
fun main() {
    val c = 'a'
    println(c + 1)       // b
    println('z' - c)     // 25
    println(c.code)      // 97
    println(98.toChar()) // b
}
  • 'a' — 작은따옴표는 Char 리터럴입니다. Char 는 자바의 char 에 해당합니다
  • c.code — 문자의 유니코드 번호를 Int 로 돌려주는 프로퍼티입니다. 프로퍼티는 필드와 게터를 한 이름으로 묶은 선언이라, 괄호 없이 읽습니다
  • 98.toChar() — Int 를 그 번호의 Char 로 바꿉니다

같은 식을 자바로도 옮겨 나란히 돌렸습니다.

Java
public class Chars {
    public static void main(String[] args) {
        char c = 'a';
        System.out.println(c + 1);      // 98  ← 코틀린은 b
        System.out.println('z' - c);    // 25
        System.out.println((int) c);    // 97
        System.out.println((char) 98);  // b
    }
}
터미널
$ kotlinc Chars.kt -d out
$ kotlin -cp out CharsKt
$ javac -d jout Chars.java
$ java -cp jout Chars

네 줄 가운데 첫 줄만 갈립니다. 나머지 셋은 표기만 다르고 찍히는 값이 같습니다 — c.code 가 자바의 (int) c, 98.toChar() 가 (char) 98 입니다.

'a' + 1 이 코틀린에서는 b, 자바에서는 98 입니다. 자바는 char 와 int 를 더하면 둘 다 int 로 올려서 계산합니다. 코틀린은 Char + Int 의 결과를 Char 로 정해 두어서, 문자를 한 칸 옮긴 b 가 나옵니다.

두 문자의 차 'z' - c 는 둘 다 25 입니다. 코틀린에서도 Char - Char 의 결과는 Int 입니다.

Int 변수에 Char 를 넣으면

자바에서 int n = c; 는 확장 변환으로 통과합니다(미확인 · 자바 언어 명세 5.1.2). 코틀린에서 val n: Int = c 를 컴파일하면 2절과 같은 initializer type mismatch: expected 'Int', actual 'Char'. 에러가 납니다.

옛 방식인 c.toInt() 는 컴파일되고 실행하면 97 을 찍습니다. 다만 kotlinc 는 'fun toInt(): Int' is deprecated 경고와 함께 Use Char.code property instead. 를 붙입니다. 폐기 예정(deprecated), 곧 앞으로 없앨 방식이니 Char.code 를 쓰라는 안내입니다.

Char 는 숫자 타입이 아닙니다(공식 문서). Char 하나에는 UTF-16 코드 단위 하나가 들어 있습니다. 그 번호가 필요하면 .code 로 꺼냅니다. 문자와 번호의 대응은 문자 인코딩의 이야기입니다.

6. 한 장 요약

코틀린의 Int 는 평소 int 로 컴파일된다. 매개변수·반환 타입에 쓴 Int 는 Int? 처럼 null 을 담거나, 제네릭 타입 인자로 들어가거나, Any 같은 객체 타입으로 넘어갈 때 Integer 가 된다. 박싱은 자바 오토박싱과 같은 호출을 컴파일러가 넣는다. 대신 자바가 해 주던 확장 변환은 없어서, Int 를 Long 에 넣으려면 toLong() 을 쓴다. 연산 결과도 자바와 같다. 다른 것은 비트 연산의 이름, Char 의 계산, 그리고 l·010·d 같은 몇몇 리터럴 표기다.

주제 자바 코틀린 확인한 방법
원시 타입과 래퍼 int · Integer 를 골라 쓴다 Int 하나. Int?·List<Int>·Array<Int>·Any 로 넘길 때는 Integer, 그냥 Int 와 IntArray 는 int javap -p · CFR
박싱 오토박싱 Integer.valueOf·intValue() 같은 호출을 컴파일러가 넣는다. 꺼낼 때 Number 캐스트가 붙는다 양쪽 CFR --sugarboxing false
int → long 대입·인자 확장 변환으로 통과 type mismatch 에러. toLong() 을 써야 한다 kotlinc · javac 에러
toLong() · toInt() 좁힐 때 (int) 캐스트를 손으로 붙인다 to타입() 함수를 손수 부른다. 좁힐 때 값 검사 없이 잘리는 것은 같다 실행 출력 · javac 에러
Int + Long long 으로 올려 계산 plus(Long) 이 있어서 된다 실행 출력 · API 문서
L 없는 큰 정수 integer number too large Long 으로 정해진다 javap -p
1l · 010 1 · 8진수 8 둘 다 에러 kotlinc 에러 · 자바 실행
1.5d double 접미사 에러. D 접미사가 없다 kotlinc 에러
정수 나눗셈·나머지·오버플로 — 결과가 자바와 같다. 오버플로 경고·예외 없음 실행 출력 대조
비트 연산 << & | ^ ~ >>> shl and or xor inv() ushr 라는 중위 함수. 결과는 같다 실행 출력 대조 · kotlinc 에러
'a' + 1 98 (int) b (Char) 실행 출력 대조
문자 → 번호 (int) c 또는 int n = c c.code. toInt() 는 폐기 예정 경고 kotlinc 경고 · 자바 언어 명세

관련 항목

코틀린 숫자 타입이 컴파일되는 JVM 타입

원시 타입 · 래퍼 클래스 · Integer · int · 원시 타입 배열 · 참조 타입

Int 가 Integer 로 바뀌는 조건과 그 과정

박싱 · 언박싱 · 오토박싱 · 널 허용 타입 · 제네릭 · 타입 인자

코틀린 숫자 타입의 종류

Int · Long · Short · Byte · Double · Float · Char · IntArray · Array

숫자 타입 사이를 오가는 변환

확장 변환 · 축소 변환 · 암묵적 형 변환 · 캐스트 · 이항 숫자 승격

숫자 리터럴의 표기

리터럴 · 정수 리터럴 · 부동소수점 리터럴 · 8진수 · 16진수 · 2진수 · 타입 추론

정수 연산에서 터지는 것

정수 나눗셈 · 정수 오버플로 · 나머지 연산 · 2의 보수

비트 연산을 이루는 연산과 표기

비트 연산 · 시프트 연산 · 부호 없는 오른쪽 시프트 · 중위 함수 · 연산자 오버로딩

Char 가 담는 문자 표현

유니코드 · UTF-16 · 코드 단위 · 문자 인코딩

컴파일 결과를 열어 보는 도구

kotlinc · javap · CFR · 디컴파일 · 클래스 파일 · 컴파일러