
연산자는 피연산자(Operand)의 개수에 따라 단항 연산자, 이항 연산자, 다항 연산자로 구분된다. 자바와 다르게 삼항 연산자는 없다. 다항 연산자를 삼항 연산자처럼 사용할 수 있는 방법도 있으나, 다르다.
| 연산자 | 내용 |
|---|---|
| + | 덧셈 연산자 |
| - | 뺄셈 연산자 |
| * | 곱셈 연산자 |
| / | 나눗셈 연산자 |
| % | 나머지 연산자 |
| ++ | 증가 연산자 |
| -- | 감소 연산자 |
덧셈 연산자
val a: Int = 10
val b: Int = 5
val sum: Int = a + b
println(sum) // 15
val hello: String = "Hello "
val world: String = "World!"
val hello_world: String = hello + world
println(hello_world) // Hello World!
나눗셈 연산자
val a: Int = 10
val b: Int = 3
val quotient: Int = a / b
println(quotient) // 3
val x: Double = 10.0
val y: Double = 3.0
val result: Double = x / y
println(result) // 3.333333333...
val whole:Double = 10.toDouble()/4
// val whole:Double = (Double)10/4 (x)
toDouble() 메서드를 사용하면 Double로 스마트 캐스팅이 된다. 증가 연산자
var a = 5
val b: Int
b = ++a * 3
println(b) // 18
println(a) // 6
var 변수를 써야 가능하다.감소 연산자
var a = 5
val b: Int
b = --a * 3
println(b) // 15
println(a) // 4
var 변수를 써야 가능하다.변수에 값을 할당하거나 변수의 값을 다른 값으로 업데이트하는 데 사용된다.
기본 대입 연산자
val integer: Int = 10
복합 대입 연산자
| 연산자 | 내용 |
|---|---|
| += | 왼쪽의 피연산자에 오른쪽 피연산자를 합한 후 왼쪽의 피연산자에 할당합니다. |
| -= | 왼쪽의 피연산자에 오른쪽 피연산자를 뺀후 왼쪽의 피연산자에 할당합니다. |
| *= | 왼쪽의 피연산자에 오른쪽 피연산자를 곱한 후 왼쪽의 피연산자에 할당합니다. |
| /= | 왼쪽의 피연산자를 오른쪽 피연산자로 나눈 후 왼쪽의 피연산자에 할당합니다. |
| %= | 왼쪽의 피연사자를 오른쪽 피연산자로 나눈 후 그 나머지를 왼쪽의 피연산자에 할당합니다. |
이항 연산자로서 두 피연산자 간의 관계를 비교하고, 그 결과로 논리 자료형 값을 반환하는 연산자이다.
| 연산자 | 내용 |
|---|---|
> | 크다 |
< | 작다 |
>= | 크거나 같다 |
<= | 작거나 같다 |
== | 같다 (동등성) |
=== | 같다 (동일성) |
!= | 같지 않다 (동등성) |
!== | 같지 않다 (동일성) |
동등성(Equality)은 비교하고자 하는 피연산자의 내용이나 값이 같은지 비교하는 것이고, 동일성(Identity)은 비교하고자 하는 피연산자가 실제로 메모리상 같은 객체인지, 즉 같은 참조를 가리키고 있는지 비교한다.
val a : Int = 128
val b : Int = 128
println(a==b) // true
val s1: String = "s1"
val s2: String = "s2"
println(s1 != s2) // true
위와 같이 두 객체의 값이 같은지 비교하고 싶으면 이중 등호(==)를 이용하여 비교 연산을 수행할 수 있다.
val a: Int? = 5
val b: Int? = null
val c: Int? = a
val d: Int = 5
println(a === b) // false
println(a === c) // true
println(a === d) // true
비교 대상들이 같은 객체를 참조하고 있는지 여부를 확인한다.
data class Box(val name:String)
val box1:Box = Box("A")
val box2:Box = Box("A")
println(box1 == box2) // true
println(box1 === box2) // false
box1 == box2 가 true가 나오는 이유는 data class는 내용 기준으로 equals 메서드도 오버라이딩 해주기 때문이다. (hashCode() 메서드도 오버라이딩한다.)new 키워드가 필요없다.&& | and |
|---|---|
| ` | |
! | not |
정수 자료형의 개별 비트(Bit)를 직접 다루는 연산자로서, 해당 자료의 비트에 대한 연산을 수행한다. Kotlin에서 비트 연산자는 연산자 자체로 연산이 가능하고, 메서드 형태로 호출하여 연산을 수행하는 것도 가능하다.
비트 AND 연산자, and (비트별 논리 곱)
val a = 5
val b = 3
val result1 = a and b // 1
val result2 = a.and(b) // . 이랑 ( )를 생략할 수 있다.
비트 OR 연산자 or (비트별 논리 합)
val a = 5
val b = 3
val result1 = a or b // 7
val result2 = a.or(b)
비트 XOR 연산자, xor (exclusive)
val a = 5 // 0101
val b = 3 // 0011
val result1 = a xor b // 6
val result2 = a.xor(b) // 6
비트 NOT 연산 메서드, inv()
val a = 5
val result = a.inv() // -6
inv()는 중위 함수가 아니기 때문에 .과 ()를 생략할 수 없다.비트 좌측이동 연산자
val a = 5 // 0101
val result1 = a shl 1 // 10 (shift-left)
val result2 = a.shl(1) // 1010
비트 우측이동 연산자
val a = 20 // 10100
val result1 = a shr 2 // 5 (shift-right)
val result2 = a.shr(2) // 0101
부호없는 비트 우측이동 연산자
val a = -5
val result1 = a ushr 1 // 2147483645

연산하고자 하는 피연산자 객체가 null이 아닐 때에만 속성이나 메서드에 접근할 수 있도록 해주는 연산자이다. 원래 null이 될 수 있는 변수에 대해 직접 접근하면 NPE이 발생하는데,
?.연산자를 사용하면 값이 존재할 때에는 연산을 수행하고 값이 존재하지 않을 때, 즉, null일 때에는 null이 반환된다.
val s1:String? = "Hello World!"
// val s1Length:Int = s1.length (x) s1이 nullable하기 때문에 이렇게 사용하지 못함.
// val s1Length:Int = s1?.length (x) ?.의 결과로 연산의 결과값이나 null이 반환될 수 있기 때문에 이렇게도 사용하지 못한다.
val s1Length: Int? = s1?.length
println("s1Length = $s1Length") // s1Length = 13
val s2:String? = null
val s2Length = s2?.length // 안전 호출 연산자를 사용했기 때문에 null이 반환된다. (NPE x)
println("s2Length = $s2Length") // s2Length = null
이항연산자로서 좌측 피연산자의 값이 null일 경우 우측 피연산자의 값을 기본값(Default Value)로서 반환하는 방식으로 동작한다.
val s3: String? = null
val s3Result:String = s3?:"Hello World!"
// orElseThrow()
val result = s3?: throw NoSuchElementException()
변수나 값이
null이 아님을 보장할 때 사용하는 연산자이다. 널 단정 연산자를 사용하면 컴파일러는 해당 내용에 대해 검사하지 않고 연산을 수행하게 된다. 정말null이 될 수 없는 조건을 확실히 알고 있거나 외부 API 호출 및 기타 상황에서 파악이 다소 불가피할 때 사용할 수 있다.
val s4:String? = "Hello World!"
val s4Result :Int= s4!!.length
val s5:String? = null
val s5Result:Int = s5!!.length // 널 단정 연산자를 사용하여 컴파일러가 확인을 하지 않기 때문에 NPE가 발생한다.
| 우선순위 | 연산자 | 설명 |
|---|---|---|
| 1 | () [] . ?. :: | 함수 호출, 인덱스 접근, 멤버 접근 등 |
| 2 | ++ -- + - ! inv() | 단항 연산자 (전위), not |
| 3 | * / % | 곱셈, 나눗셈, 나머지 |
| 4 | + - | 덧셈, 뺄셈 |
| 5 | .. | 범위 생성 연산자 |
| 6 | in !in is !is | 포함 여부, 타입 검사 |
| 7 | < > <= >= | 비교 연산자 |
| 8 | == != === !== | 동등성, 참조 비교 |
| 9 | and | 비트 AND |
| 10 | xor | 비트 XOR |
| 11 | && | 논리 AND |
| 12 | ?: | 엘비스 연산자 |
| 13 | = += -= *= … | 대입 및 복합 대입 연산자 |
// 스마트 캐스트
var value: Number = 10.0
println("value = ${value}")
println("type of value = ${value::class.simpleName}") // Double
value = 10
println("type of value = ${value::class.simpleName}") // Int
value = 10L
println("type of value = ${value::class.simpleName}") // Long
:: 리플랙션으로 변수의 타입을 얻을 수 있다.fun printLength(target: Any?) {
if(target is String) { // is로 타입추론을 하면서 스마트캐스트가 됨.
println("문자열의 길이는 ${target.length}입니다.")
} else {
println("문자열이 아닙니다!")
}
}
fun isNotInteger(target: Any?) {
if (target !is Int) {
println("정수가 아닙니다!")
return
}
// Int Type이면 is에서 스마트 캐스팅이 되고 아래에서는 Int형으로 이용할 수 있다.
println("정수입니다!")
println("target의 타입 = ${target.javaClass.simpleName}")
}
var target1:Any = "Hello World!"
val intTarget1 = target1 as Int
ClassCastException 예외가 발생한다.val target2: Any = 100
val strTarget2 = target2 as? String
as? : 스마트 캐스팅을 시도해보고 문제가 발생할 시 null을 할당해준다.val itemList: List<Any> = listOf("성찬", "s2", 100000, true)
printStr(itemList)
fun printStr(target: List<Any>) {
for(i in target) {
val iStr = i as? String
if( iStr != null) { // String이라면 !
print("$iStr ")
}
}
}
명령의 흐름을 제어하는 데 사용되는 문법으로, 제어문으로 통해 조건에 따라 코드의 실행 분기를 생성하거나 반복적인 작업을 수행할 수 있다.
Kotlin에서는 if 자체를 표현식으로 취급하기 때문에 결과를 값으로 활용할 수 있다. 즉, if를 포함한 다른 모든 내용들의 평가 결과가 바로 할당되거나 반환값으로 사용될 수 있다는 것을 의미한다.
다항 연산자를 지원하지 않는 Kotlin에서는 이 방법을 사용하여 다항 연산자와 같은 결과를 도출할 수 있다.
val score: Int = 85
val result = if ( score > 89 ) {
print("A")
'A'
} else if ( score > 79 ) {
print("B")
'B'
} else {
print("C")
'C'
}
println(result) // BB
조건에 따라 분기 처리를 깔끔하게 할 수 있도록 도와주는 제어문으로, 단순히 값 하나를 기준으로 비교하는 것이 아니라 여러 조건, 범위, 타입, 표현식 등을 조건으로 사용할 수 있기 때문에 유연하게 구성할 수 있는 것이 특징이다. when 역시 표현식으로 취급되기 때문에 결과를 값으로 활용할 수 있다.
val rank: Int = 1
when (rank) {
1 -> println("Gold")
else -> println("Default")
}
// Gold
인자 없는 when
인자 없이도 when을 사용할 수 있는데, 이 경우, 각 조건은 표현식으로 평가되어 참/거짓을 결정하며, 조건이 참인 경우 해당 블록 내의 실행문이 실행된다.
val rank: Int = 1
when {
rank === 1 -> println("Gold")
else -> println("Default")
}
Kotlin에서 반복 가능한 범위나 컬렉션을 순회하며 반복 작업을 수행할 수 있는 반복문이다. Kotlin에서는 in 키워드를 통해 범위 기반의 반복을 수행한다. for문은 인덱스 기반의 반복 작업뿐 아니라, 리스트나 배열 같은 컬렉션을 순회할 때도 매우 유용하다.
범위 생성 연산자, ..
.. 연산자는 시작값과 끝값을 포함하는 닫힌 범위(closed range)를 생성한다.
for ( i in 1..10 step 2) {
print("$i ")
}
// 1 3 5 7 9
역방향 범위 downTo
val range = 5 downTo 1 // 1~5 (infix 함수)
val range2 = 10 downTo 1 step 3
열린 범위 연산자
val range = 0 until 5 // 0~4
In 연산자
val s1 = 1..10 step 2
val t1 = 3
println("t1 in s1? = ${t1 in s1}") // true
println("t1 not in s1? = ${t1 !in s1}") // false
contains() 함수를 호출하는 방식으로 동작한다. 범위 포함 여부
val c1 = 1..5
val c2 = 2..4
println("c2 include c1? = ${c1.first in c2 && c1.last in c2}")
println("c1 include c2? = ${c2.first in c1 && c2.last in c1}")
fun IntRange.isWithin(outer: IntRange) : Boolean {
return this.first in outer && this.last in outer
}
라벨(Label) 활용
반복문이나 중첩문 혹은 익명 함수에서 흐름을 제어할 때 좀 더 정확하고 명시적으로 제어할 수 있도록 도와주는 기능이다. break에서 라벨을 사용할 경우 반복문이 중첩되었을 때 어떤 반복문을 종료하거나 건너뛸지 명확히 지정할 수 있게 된다.
outer@for (i in 1..3) {
for (j in 1..3) {
if (j == 2) break@outer
println("i = $i, j = $j")
}
}
// i = 1, j = 1
val board = listOf(
listOf(1,2,3),
listOf(4,5,6),
listOf(7,8,9),
)
val target = 5
search@for(i in board.indices) { // 배열의 인덱스를 범위로 반환해준다.
for ( j in board[i].indices) {
println("[${i}][${j}] = ${board[i][j]}")
if (board[i][j] == target) {
println("타겟 발견!")
break@search
}
}
}
indices는 컬렉션이나 배열의 인덱스 범위(Range)를 반환하는 프로퍼티이다. board.indices : 0..2board[i].indices : 0..2함수(Function)와 메서드(Method)는 모두 특정 작업을 수행하는 기능의 묶음을 의미한다. 하지만, 용도와 정의에 따라 차이가 있는데, 함수는 독립적으로 작동하는 코드 블록으로서, 주로 절차지향적 프로그래밍에서 사용된다.
함수
어떤 객체에도 속하지 않고, 그 자체로 호출되며 입력 값을 받아 처리한 후 결과를 반환한다.
메서드
객체 지향 프로그래밍에서 사용되는 개념으로서, 객체의 상태를 변경하거나 객체의 정보를 처리하는 기능의 묶음이다. 메서드는 특정 클래스나 객체에 소속되어 있으며, 객체의 인스턴스나 클래스 수준에서 호출된다.
정리
함수는 독립적이고 범용적인 코드 블록이며, 메서드는 객체 지향 프로그래밍의 일환으로 객체와 밀접하게 관련된 코드 블록이다. 함수는 전역적이거나 지역적으로 사용되며, 메서드는 클래스나 객체의 상태와 행동을 정의하고 관리한다.
fun 메서드이름([[매개변수:매개변수 자료형], ...])[:반환 타입] { // 정의부
명령문 // 본문
}
Unit을 사용한다. 만일 반환하는 값이 있다면 자료형을 반드시 기재해야 한다.함수이름(인자1, 인자2, ...);
Kotlin에서는 함수의 매개변수에 기본값을 지정할 수 있다. 기본값을 지정하게 되면 호출 시 값을 생략하더라도 매개변수에 기본 값을 할당하여 함수를 호출하도록 동작한다.
fun sayName(name: String = "성찬"): Unit {
println("Hello $name!")
}
sayName() // Hello 성찬!
sayName("앤톤") // Hello 앤톤!
이름 붙은 인자는 함수 호출 시 각 매개변수에 값을 전달할 때 매개변수 이름을 명시하는 기능이다. 이를 통해 인자의 순서에 얽매이지 않고 원하는 매개변수만 선택적으로 전달할 수 있다. 특히, 기본값 매개변수와 함께 사용할 때 일부만 직접 지정하고 나머지는 기본값을 사용하는 유연한 호출이 가능하다.
fun orderCoffee(name: String, size: String, isIce: Boolean ): Unit {
println("$size 사이즈 ${if (isIce) "아이스" else "핫"} $name(을)를 주문하였습니다!")
}
orderCoffee("아메리카노", size="Tall", isIce=true)
orderCoffee(name="아메리카노", "Tall", true) // 호출가능
// orderCoffee(size="Tall", isIce=false, "아메리카노") // 호출불가
단계적인 실행 흐름을 중심으로 프로그램을 작성하는 방식이다.
Q1. 아래의 코드에서 a와 d의 동일성 비교가 true가 나오는 이유가 뭘까? 이것도 상수풀 때문인가?
val a: Int? = 5 // 박싱된 Int (nullable이므로 객체로 존재)
val d: Int = 5 // Primitive Int
println(a === d) // true
A1. 상수풀 때문이 아니라, 박싱(autoboxing)의 최적화 때문이다.
여기서 핵심은 a === d에서 d는 Int이므로 a도 자동으로 언박싱/박싱 과정이 일어나 Int? → Int, 또는 반대로 Int → Int?가 되면서 비교가 이루어진다.
Kotlin/JVM에서는 -128 ~ 127 범위의 정수에 대해 캐싱된 박싱 객체를 사용합니다 (Java의 Integer.valueOf()와 동일한 방식).
따라서,
val a: Int? = 5 // 박싱된 5, 캐싱된 객체
val d: Int = 5 // 프리미티브 5 → 박싱 시에도 같은 캐싱된 객체 사용
이 경우 둘 다 같은 박싱된 객체를 참조하게 되고, a === d는 true가 된다.
Q2. 코틀린에는 참조형만 있고, 기본형은 없는 줄 알았는데 그건 아닌가보네?
A2. Kotlin은 언뜻 보기엔 전부 참조형(객체)만 있는 것처럼 보이지만, 실제로는 JVM 위에서 실행되기 때문에 기본형(primitive type)도 존재한다.
🔍 Kotlin에서 기본형 vs 참조형 정리
| 타입 선언 | JVM에서의 실제 타입 | 설명 |
|---|---|---|
| val x: Int | int 기본형. | 성능 최적화됨 |
| val x: Int? | Integer (박싱된 타입) 참조형. | null 허용 때문에 박싱 필요 |
✅ 중요한 포인트
Kotlin 자체는 기본형/참조형을 구분하지 않고, Int, Double, Boolean 등은 그냥 타입으로 다룸.
하지만 컴파일러가 최적화해서, nullable이 아니면 primitive로 변환함.
예: val x: Int = 5 → JVM의 int x = 5;
val x: Int? = 5 → JVM의 Integer x = Integer.valueOf(5);
정리하자면, Kotlin에는 문법적으로는 모두 객체처럼 보이지만, nullable 여부에 따라 JVM에서 primitive와 boxed 타입으로 나뉜다.
코틀린 재미있다. 근데 월요일은 쉽지 않다.. 오후에는 후끈후끈 덥기까지 했다! 아직 여름 시작도 안 했는데.. 찐찐 여름 오면 어쩌지?! 어쩌긴 어째... 선풍기 풀가동입니다.
내일은 커피 수혈해야겠다 ! 파이팅