들어가며
누군가가 나에게 주언어가 뭐에요? 라고 물어본다면 "자바가 제일 자신있습니다" 라고 답할 것이다.
그렇다. 이 글은 계속 자바와 비교하며 코틀린을 정리할 것이다. 내가 정리하려고 쓰는 것도 있지만 자바가 익숙한 개발자라면 재밌게 볼 수 있을 거라고 확신한다!
최근 도전적인 목표로 정한 코틀린! 공부해보니 자바와 매우 닮아있다. 굉장히 트렌디하고 혁신적인 언어라고 생각한다. 자 그럼 가보자🔥

🚨혹시나 틀린 내용이 있다면, 따끔한 댓글 부탁드립니다🚨
본문으로
1. 변수 선언 방식이 자바와 완전 다르다.
val variableName : String = “변수 선언 방법”
→ 자료형 추론 기능으로 String 생략 가능…wow
자세한 선언은 아래의 2번의 코드와 함께 보자!
2. 타입추론 방식을 적극 권장하고 실제로도 이렇게 사용한다.
val : 변수를 선언할 때 지정한 값에서 더이상 변경하지 않아야 하는 경우 사용(final) -> value
var : 변수의 값을 바꿀 수 있어야 하는 경우 사용 -> variable
문자열 내용에 내가 처음 봤을 때의 느낌을 적어봤다!
val name: String = "문자열임을 알아서 타입 추론하기에, String 생략 가능. IDE도 회색으로 칠하네!"
var hello = "생략하는 게 매우 어색하네.. 이래도 되는건가? 마우스를 올려놓으면 String이라고 IDE가 알려준다 다 타입 추론중!"
var age = 12
// name = "val은 재할당 불가! final 키워드가 내포되어 있구나"
age = 15 // var은 재할당 가능
3. 자바의 최상위 부모는 Object, 코틀린의 최상위 부모는 Any
4. primitive 타입을 사용하지 않고 다 Reference 타입(래퍼 클래스)를 이용한다.
// 애초에 int, long이 없음! 다 래퍼클래스로!
val number4: Long = 123
val number5: Any = 123
val number6 = 123.456
val number7: Double = 123.456
val number8: Float = 123.456F
컴파일시에 primitive타입으로 변환하여 로직이 돌아간다고 한다.
5. 코틀린에서는 "프로그램 시작 시점에 메모리 위로 올라가는 정적(static) 영역"이 없다.
대신 "런타임 시점에 하나의 객체로 초기화 되는 companion object"가 있다. (왼쪽이 자바, 오른쪽이 코틀린)
| 로드 시점 | 클래스 로딩 시 (JVM이 ClassLoader 통해 바로 메모리에 올림) | 실제로 처음 접근할 때 lazy하게 객체 생성 |
| 메모리 위치 | 메타데이터 영역 (Method Area) | Heap에 일반 객체처럼 올라감 |
| 형태 | “영역” (메서드, 필드만 존재) | “객체(instance)” (Companion 인스턴스가 실제로 존재) |
WOW 자바에서는 static 메서드에 접근하려면 같은 static만 가능했는데 코틀린은 그런거 없음!
- 클래스 인스턴스 메서드에서 companion 호출 가능
- companion에서 인스턴스 생성/참조 가능 (단, 순환 참조만 주의)
- companion이 인터페이스 구현, 상속 등 자유롭게 가능
예를 들어 팩토리 패턴도 깔끔하게 쓸 수 있음.
class User private constructor(val name: String) {
companion object {
fun from(name: String) = User(name)
}
}
val user = User.from("노을") // 팩토리 패턴 느낌, static 없이 구현
왜 companion(친구) 일까?
이는 코틀린의 철학이다. 코틀린은 정적 친구 대신 동반 객체라는 철학적 이름을 택한 것이다.
위 코드의 from()은 “그 사람 클래스와 붙어다니는 팩토리 친구”
여기서 잠깐! 위의 코드에서 companion 옆에 있는 object라는 키워드는 뭘까?
object 자체는 코틀린에서 “싱글톤”을 만들 때 쓰는 키워드이다.
아래 코드는 진짜 전역 싱글톤 객체고, companion object는 클래스 내부의 전용 싱글톤 친구.
object Database {
fun connect() { ... }
}
6. 특이한 연산자 소개합니다
a === b : a의 참조 주소와 b의 참조 주소가 같은지 비교 <=> 자바에서 ==이 참조 주소 비교,
a ! == b : a의 참조 주소와 b의 참조 주소가 다른지 비교
-> 자바에서는 ==가 참조 비교고, 그래서 값 비교하려고 .equals()를 쓰거나 equals() hashcode() 오버라이딩했었는데....! 다르구나~~
7. switch문은 when문!
fun main(args: Array<String>) {
val a = Random.nextInt(10)
val b = Random.nextInt(10)
when (a) {
1, 2 -> println("a is 1 or 2")
3 -> println("a is 3")
4 -> println("a is 4")
5 -> println("a is 5")
in 6..8 -> println("a is in 6,7,8")
else -> println("a is $a")
}
when {
a > b -> println("a > b")
else -> println("a <= b")
}
8. for문 표현법 완전 다르다..!(주석은 자바 표현인데 비교해보자!)
class LoopSample {
companion object {
@JvmStatic
fun main(args: Array<String>) {
// for(int i = 0; i < 5; i++)이 확연하게 줄긴하네~
for (i in 1..5) println(i)
println()
// --i 가 아니라 이렇게 downTo라는 표현을 직관적으로 쓰네. 진짜 나름 철학이 있네.
for (i in 5 downTo 1) println(i)
println()
//i+=2가 아니라 step이라는 키워드가 존재한다!
for (i in 1..5 step 2) println(i)
println()
}
}
}
9. 자바와 너무 다른 메서드 선언 방식 (코틀린에서는 모든 함수가 리턴값을 가진다! void는 아래 예시를 보자)
fun 함수명(인자:타입, 인자:타입) : return 타입 {
return 리턴값
}
// example
fun sum1(a:Int, b:Int) : return Int {
return a + b
}
// return을 생략한 표현식 가능
fun sum2(a: Int, b: Int) = a+b
// 만약 리턴할 값이 없다면 return 타입을 Unit으로 정의할 수 있으며 생략 가능함
// 코틀린에선 모든 함수가 리턴값을 다 가져야 하는데, 여태껏 Unit을 반환중이었고
// 그걸 생략했던 거구나
fun noReturnFuntion(a: String){
println("called!")
}
// 전달 받는 파라미터는 기본 값을 사용 할 수 있다. 아래처럼 쓰면 자연스럽게 오버로딩
fun defaultParameter(a: String="default value"){
println(a)
}
// 넘겨야 하는 값이 많을 경우 매개변수의 이름을 직접 명시하여 가독성을 높힐 수 있음
// 대박이다! 순서 보장을 할 필요 없구나
// 근데 순서 바꾸는 건 별로인 거 같고, 이름 명시해주는 것은 진짜 좋다. 가독성 올라갈듯
fun main() {
namedArgument(a = "around", c = "studio", b = "hub")
}
fun namedArgument(a: String, b: String, c: String){
println("$a $b $c")
}
10. 뭐? 멤버변수 선언과 생성자 표현을 한줄로 같이 한다고?!
class PersonWithConstructor constructor(private val name: String, private val age: Int) { // 주 생성자
//constructor키워드 생략 가능, 여기서 생성자와 함께 멤버변수 선언!
private var etc: String = "no value"
init {
println("Initializer block: $etc")
}
// 자바였다면 멤버변수 선언하고.. 생성자 만들고 하면 벌써 5줄은 넘겠네!
// public Person(String name) {
//this(name, 15);
//} 자바 였으면 원래 이렇게 해야할텐데, 코틀린은 바로 :로 짧게 표현 가능하네!
constructor(name: String) : this(name, 15) // 부 생성자
constructor(name: String, age: Int, etc: String) : this(name, age) { // 추가 생성자
this.etc = etc
println("additional constructor is called ${this.etc}")
}
fun sayHello() {
println("Hello!")
}
fun introduce() {
print("$name ")
print("$age ")
println(etc)
}
}
11. 기본 생성자에 로직을 못넣으니 init{}이 필요하다!
class User(val name: String) {
val upperName: String
init {
upperName = name.uppercase()
}
}
init{}은 객체 생성 시점 중에서도 “주생성자 호출 직후, 본문 실행 전에” 실행된다.
약간 주생성자의 일부분처럼 취급된다. → 주생성자와 함께 실행되는 초기화 블록
주생성자는 본문(body)가 없으니 생성시점에 로직을 넣고 싶으면 init 블록이 필요하다.
12. open이라는 키워드를 붙혀야 상속을 할 수 있다
open class Car(val name: String, val price: Double, val brand: String) {
fun introduce() {
println("this car is $name. this made by $brand")
}
fun howMuch() {
println("this car is $price dollars")
}
open fun myPurchaseDate() {
println("you don't buy yet")
}
}
class MyCar(name: String, price: Double, brand: String, private val purchaseDate: LocalDate) : Car(name, price, brand) {
override fun myPurchaseDate() {
println("you made a purchase on $purchaseDate")
}
}
자바에서 자동으로 extends가 열려 있던 걸 코틀린은 명시적으로 “닫아버렸다"
코틀린의 모든 클래스와 함수는 기본적으로 final
→ 상속하거나 오버라이드 불가능.
그래서 상속하고 싶으면 명시해야 한다.
→ 그게 open.
public이면 접근은 가능하지만 open을 써주지 않으면 상속은 불가능 하다!!
extends라는 키워드를 쓰지 않고 :로 바로 상속을 하는구나. 표현이 정말 직관적이다~~!
13. 코틀린의 데이터를 담는 객체 data class( 근데 자바보다 더 많은 기능을 지원하는)
// kotlin의 data class
data class User(val id: Int, val name: String)
// java의 record
public record User(int id, String name) {}
둘다 값을 담는 용도로 DTO 클래스로 많이 쓰이지만 약간의 차이가 있다.
일단 아래와 같은 공통점이 있다.
필드 정의 한 줄, 생성자 자동, toString(), equals(), hashCode(), 자동 getter() 지원, “값 담는 객체”라는 의도 표현
자바 record는 불변 필드(final)이라 한번 만든 값은 못바꾸지만, 코틀린 data class는 var 가능하다 -> 가변이다.
둘다 상속은 못받지만(final 느낌)
코틀린 data class는 인터페이스 구현 가능하고, copy()(데이터 복사, 값 그대로 복사가 된다)도 지원하고, default 파라미터도 되서 좀 더 유연하다!
// default parameter가 지원된다!
data class User(val id: Int, val name: String = "unknown")
// 자바 record는 이렇게 직접은 안된다. 오버로드를 따로 만들거나 compact constructor 써야한다.
14. 변수 선언 방식 때문에 Enum 표현이 매우 짧아진다!
enum class Color(val label: String, val code: String) {
RED("red", "#FE2E2E"),
YELLOW("yellow", "#F7FE2E"),
GREEN("green", "#40FF00"),
BLUE("blue", "#0000FF");
}
자바였으면 멤버변수 설정코드때문에 기본적으로 코드가 길어졌을 것이다.
15. 출력 형식에 $표시를 쓴다.
val name: String = "noeul"
var hello = "hello"
var age = 19
println("my name is $name") // noeul
println("hello is $hello") // hello
println("my age is $age") // 19
16. 결국 같은 .class 파일로 인식된다! -> JVM에선 똑같아서 java와 100% 호환된다

위의 사진을 보면 .java, .kt 파일 둘다 결국 .class로 컴파일 되어 같은 JVM에서 돌아간다. 그러므로 java와 kotlin은 100% 호환된다!
17. Null Safety하다!
코틀린이 Null Safety하다는 것은 Null을 다루는 방식이 명확하게 정해져 있기 때문이다.
아래 코드처럼 타입 뒤에 ?를 붙이면 그 변수는 null을 가질 수 있는(nullable) 변수가 된다.
그런데 name은 null일 수도 있는 타입(String?)인데 바로 name.length처럼 접근하고 있기 때문에, IDE가 . 부분부터 빨간 줄을 그어준다.
fun main() {
var name: String? = null
println(name.length) // 컴파일 오류 (nullable 타입에서 바로 접근 불가)
}
이건 “이 변수는 null일 수도 있으니까 그냥 쓰면 안 된다”고 컴파일러가 알려주는 것이고, 실제로는 컴파일 오류가 발생한다.
자바는 null에 대해 딱히 다른 조치를 취하지 않는다면 에러가 터져서 프로그램이 뻑나버린다.
public class Main {
public static void main(String[] args) {
String name = null;
System.out.println(name.length()); // 컴파일은 되지만 실행 시 NPE 발생
}
}
그래서 코틀린은 NULL 안정성을 제공한다 → 기본적으로 NULL을 허용하지 않으며, 명시적으로 NULL을 처리해야 사용할 수 있음
아래처럼 처리가 가능하다!
fun main() {
var name: String? = null
name?.let {
println(it.length) // name이 null이 아닐 때만 실행됨
}
}
(변수)?. -> 안전 호출 연산자란?
- 변수가 null이 아니면 뒤에 이어지는 코드를 실행하고,
- 변수가 null이면 그냥 아무 일도 안 하고 넘어간다.
name?.let { println(it.length) }
// 자바로 치면 아래와 같은 로직
if (name != null) {
System.out.println(name.length());
}
let {...} -> 스코프 함수란?
let은 "이 객체를 it으로 넘겨서 블록 안에서 쓸 수 있게 하는 함수"
그래서 name?.let { println(it.length)} 에서 it은 name의 실제 값이다.
it -> 암시적 파라미터 이름
람다식에서 매개변수가 하나뿐이면 굳이 이름 안써도 된다. 코틀린이 자동으로 it이란 이름으로 제공해줌.
결론적으로 지금은 name이 null이니까, 아무것도 출력되지 않는다!
18. 매우 직관적인 엘비스 연산자 -> ?:
// name 이 null이 아니면 name을 출력
// name 이 null이면 "비회원"을 출력
fun main() {
var name: String? = null
print(name ?: "비회원")
}
// 자바였으면 이랬다..
public class Main {
public static void main(String[] args) {
String name = null;
if (name == null) {
System.out.println("비회원");
} else {
System.out.println(name);
}
}
}
그리고 null이 들어올 수 있으면 kotlin은 컴파일 오류를 낸다! 자바는 나중에 런타임에 터진다~
마무리하며
이렇게 전적으로 자바와 비교하며 코틀린을 정리해보았다! 자바가 친숙한 개발자라면 재미있게 볼 수 있을 것이다! 공부하면서 느낀 것은 정말 트렌디하고 자바의 단점은 버리고 장점만 쟁취한 느낌이 든다. 다만 반면으로는 타입추론, 변수 선언 방식이 아직은 좀 낯설고 처음에는 별로인 느낌마저도 들었다 😂
그래도 확실히 자바 특유의 코드가 길어지는 부분을 Kotlin을 만든 JetBrain사에서 많이 고려해서 고친듯 하다! 다음은 열심히 배운 코틀린으로 이것저것 해볼 예정이다!😃 구글 안드로이드 스튜디오 공식언어여서 그런지 레퍼런스도 많고 해볼만한 것들도 많아보인다!
새로운 것을 배운다는 것은 언제나 설레이는 것 같다! 그럼 정진하자 🔥
'Java & Kotlin' 카테고리의 다른 글
| [JAVA] 자바가 제공하는 날짜 시간 총정리 (0) | 2026.01.31 |
|---|---|
| [JAVA] String은 바쁘다! 속도가 중요할 시 StringBuilder를 이용하자 (0) | 2026.01.30 |
| [JAVA] 함수형 인터페이스란?(Supplier<T>, Predicate<T>를 적용하게 된 이유를 중점으로) (0) | 2025.11.03 |
| [JAVA] Collection의 복사 방법에 대해 알아보자!(방어적 복사, 얕은 복사, 깊은 복사) feat. 내가 List.copyOf와 Arrays.asList를 쓴 이유 (0) | 2025.10.18 |
| [JAVA] split() 메서드 정복하기 (나는 왜 split(delimiter, -1)을 썻을까?) (1) | 2025.10.17 |