Classes
Classes
- class 키워드로 선언한다.
- class의 구성
- header: 클래스 이름 다음부터 { 까지. constructor, 상속 등을 정의
- body: { } 로 정의된 코드 영역
- Java와 다른 점
- class의 access keyword - public 생략 가능
- 객체 생성자를 부를 때 new 를 적지 않는다.
- header 와 body 가 모두 선택사항이다.
Constructor(생성자)
- 하나의 Primary Constructor 와 여러 개의 Secondary Constructor로 구성할 수 있 다.
- Primary Constructor는 반드시 호출되어야 한다. (몇 단계를 거치든 무조건 호출되어야함)

- Primary Constructor는 header 영역에 정의된다

- Primary Constructor에 annotation도 없고 visibility 가 public 일 경우 다음과 같이 constructs 키워드를 생략하고 줄여쓸 수 있다

- Primary Constructor로 전달 된 파라미터는 클래스 Body 내의 변수 초기화에 사용할 수 있다.
// 클래스 Body 전체가 Primary 생성자를 초기화 할 수 있게 된다.
// constructor 는 생략 가능
class Hello constructor(userName : String) {
private val name:String = userName // 외부에서 접근 불가
}
fun hello(userName:String) {
val name = userName
}
- 초기화 코드가 필요할 경우 init { } 블록을 추가하여 작성 할 수 있다.
- init 블록은 Primary Constructor 일부 이다.
- init 블록이 여러 개 있으면 위에서 아래 순으로 실행된다.
-
class Hello(userName : String) { private val name:String = userName init { println("Hello! $name") } } fun main() { val h = Hello("user") }
- 코틀린에서는 class의 이름은 첫문자를 대문자, 함수는 소문자로 작성하여야 함. 그렇지 않으면 내부적으로 클래스를 함수로, 함수를 클래스로 인식할 수 도 있다.
실습
- 인자를 받지 않는 생성자를 가진 Test 클래스를 선언하고 객체를 생성한다.
class Test() {
init {
println("객체 생성됨")
}
}
fun main() {
val t = Test()
}
- 정수형 value property x, y를 가지는 Position class를 정의.
- Primary constructor 에서 정수형 숫자 2개를 받아서 property x, y를 초기화.
- 객체를 2개 이상 생성.
class Position(x:Int, y:Int) {
val x = x
val y = y
}
fun main() {
val p1 = Position(0,0)
val p2 = Position(2, 1)
}
- Primary Constructor에서 property 선언과 초기화를 한번에 할 수 있다.
- Class property도 할당 횟수에 따라 val, var로 구분하여 사용한다.

- 앞에서 작성한 Position 클래스에서 x, y 변수를 생성자에서 바로 선언하고 초기화 하도록 코드를 수정하세요.
-
// 이전 class Position(x: Int, y:Int) { val x = x val y = y } // 수정 후 class Position(val x:Int, val y:Int) { }
Secondary Constructor
- Secondary Constructor
- constructor 키워드를 이용해 class body에 선언한다.
- 직접적 또는 간접적으로 Primary constructor를 호출해야 한다.
class Hello (val name:String, val age:Int) { // 주 생성자
/*
(val name:String) 을 자바로 바꾸면
String name;
public Hello(String a) { name = a; }
*/
// 이름만 입력하는 경우
constructor(name:String):this(name,20) // 보조 생성자
// 나이만 입력하는 경우
constructor(age:Int):this("",age) // 보조 생성자
// 아무것도 입력하지 않는 경우 -> 이름만 입력한 경우의 생성자를 호출함
constructor():this("") // 보조 생성자
}
fun main() {
val h = Hello("user", 22)
println(h.name)
val h2 = Hello()
println(h2.name)
val h3 = Hello(22)
println(h3.name)
val h4 = Hello("lee")
println(h4.name)
}
- 코틀린에서는 파라미터가 많은걸 왠만하면 주 생성자로 사용한다.
→ 왜냐하면 주 생성자에 선언을 하면 val을 생략할 수 있기때문에
실습
- Position 클래스에 정수형 파라미터 하나만을 받아 x, y 모든 변수에 해당 값을 설정하는 Secondary Constructor를 추가하라.
package org.example.s240313_4_class
class Position(val x:Int, val y:Int) {
// 하나만 주면 같은 값으로 세팅하도록
constructor(value:Int):this(value,value)
}
/* 자바로 바꾼다면 2줄이 이만큼 커짐..
public class Position {
int x;
int y;
public Position(int value) {
x = value;
y = value;
}
public Postition(x, y) {
this.x = x;
this.y = y;
}
}
*/
fun main() {
val p1 = Position(0,0)
val p2 = Position(2, 1)
val p3 = Position(1)
}
Inheritance(상속)
- 모든 클래스의 최상위 부모는 Any: 부모가 명시되지 않은 경우 Any의 자식 클래스
- 상속을 하지 않으면 내부적으로는 Any를 상속받게 됨
- 클래스는 기본적으로 final 처리되어 상속 불가능
- 상속을 허용하려면 open 키워드를 사용해야 한다.
- 자식 클래스는 반드시 부모 클래스의 생성자를 사용하여야 한다.
- 부모 클래스를 지정하려면 header 뒤에 : 부모클래스() 와 같이 적는다.
- 부모 클래스의 Constructor를 호출하는 형태로 적어야 한다
- 부모의 생성자를 먼저 만들고 → 그 다음에 자식이 만들어지는 것

open class Parent {init {println("부모 생성")}}
class Child:Parent() {init {println("자식 생성")}}
fun main() {
var p = Parent()
var c = Child()
}

open class Parent() {
init {println("부모 생성")}
constructor(msg:String):this() { // 파라미터를 받는 보조 생성자가 넣는 순간 기본 생성자를 명시적으로 적어주어야함
println("부모 생성 - $msg")
}
}
class Child:Parent() {init {println("자식 생성")}}
fun main() {
var p = Parent()
var c = Child()
var p1 = Parent("MSGSMGMSGMSMGSMGSMGM")
}
- 만약 자식 클래스에 parmary constructor가 없다면 각 secondary constructor에서 super 키워드를 이용해 초기화 하거나 다른 secondary constructor를 이용해서 부모 클래스를 초기화한다.

- Position을 상속 가능하게 변경하고 이를 상속받은 TPosition 클래스를 정의한다.
- TPosition클래스는 정수형 z 변수를 추가로 가지며 Primary Constructor는 정수형 숫자 3개를 받는다.
- TPosition 클래스의 객체를 생성하라.

Overriding methods(재정의)
- 자식 클래스의 override를 허용하려면 부모 클래스의 함수에 open 키워드를 지정한다.
- 코틀린에서는 클래스 내의 함수도 final 처리가 되어져서 open 구문 필요
- 자식 클래스는 명시적으로 override 키워드를 추가하여 함수를 override 한다.
- override를 하려면 부모클래스에서는 함수에 open, 자식클래스에서는 함수에 override를 무조건 명시해줌

- override 키워드를 사용한 함수는 자동으로 open이 되며 만약 다른 자식의 override를 허용 하지 않으려면 final을 지정한다.

- 자바에서의 override는 필수가 아니고 만약 부모클래스의 메소드를 override를 할 때 자식클래스에서 오타가 나도 오류가 뜨지 않음!! 잘못된 사용이 발생할 수 있다.
TEST
- Position 클래스에 printValue 함수를 추가하라. 함수는 (x, y) 형태로 값을 출력한다.
- TPosition 클래스에서는 printValue 함수를 override 하여 (x, y, z)의 형태로 값이 출력되도록 한다.

Superclass methods
- 자식클래스에서 override 된 함수 말고 부모 클래스의 함수를 호출하려면 super 키워드를 사용한다.

Overriding properties
- val에도 open 및 override 키워드를 사용해야 한다.

- val을 var로 상속 받는 것은 가능하며 반대는 안된다.

초기화 순서
- 부모 클래스의 초기화가 먼저 이루어지고 자식 클래스가 초기화 된다.
- 부모 생성자의 할당 실행
- → 부모 생성자의 init 실행
- 부모의 나머지 초기화 코드 실행
- 자식 클래스의 초기화 실행
Abstract classes(추상클래스)
open class Animal() {
open fun say():String {return ""}
}
class Dog:Animal() {
override fun say():String {return "멍멍"}
}
class Cat:Animal() {
override fun say():String {return "야옹"}
}
class Pig:Animal() {}
// override를 빠뜨렸을때 부모의 함수를 사용한다.
fun main() {
val arr = mutableListOf<Animal>()
arr.add(Cat())
arr.add(Dog())
// arr.add(Animal())
arr.forEach{println(it.say())}
}
// 향후 발생할수 있는 문제점
// 만약 Animal() 클래스는 실 사용을 하려고 만든 클래스가 아닌
// override 를 위해 만든 클래스인데 메인 함수에서 Animal() 클래스를 생성한다면?
// -> 추상클래스로 해결
위의 문제를 추상클래스로 해결
- 일부 멤버 또는 전체 멤버가 abscract 일 때, 클래스가 abstract로 선언 된다.
- abstract 멤버는 open 키워드를 사용하지 않아도 된다.
- 추상 함수가 아닌 일반 함수를 포함할 수 있으며 일반함수는 필요에 따라 open 할 수 있다.
Properties
- Read-only, mutable한 경우에 맞게 val, var로 선언한다.
-
- Property들은 모두 constructor에서 할당 되어야 한다.
- 그래서 val의 경우 read-only라 생각해도 된다.
- 초기화를 통해 타입을 추론할 수 있게 하거나 타입을 정해줘야 한다.
-

- Getter/Setter : 각 Property는 getter와 setter를 가질 수 있다.
- 생략할 경우 기본 getter/setter가 제공된다.
- val은 getter만 제공된다.
- 코틀린에서 변수를 만들어주면 내부적으로 getter와 setter를 자동으로 만들어줌
- 밑의 사진은 코틀린 코드를 자바 코드로 변환된 코드이다.


- 자바 라이브러리의 호환성 등을 위해 getter/setter 함수를 사용하지 않으려면 property에 @JvmField 어노테이션을 추가한다.
class Hello1 {
var a = 0
get() {
println("getter")
return field
}
set(value) {
println("setter")
// a = value
field = value
}
}
fun main() {
val hello = Hello1()
hello.a = 1
println(hello.a)
}
- var의 경우 getter, setter를 , val은 getter를 별도로 구현할 수 있다.
- getter, setter에서 본래의 값을 부르는 변수는 field이다.
- 원래의 변수 이름을 사용하면 무한 반복에 걸려 Stack Overflow가 발생한다.
TEST
- 0 이상의 정수만 할당할 수 있는 count 변수를 가진 Test 클래스를 정의하라. 음수로 할당할 경우 무시하면 된다.

Properties
lateinit
- null을 허용하지 않는 변수를 생성자가 아닌 함수에서 초기화
- var에서만 사용 가능
- lateinit var 이름:type 형식으로 사용
- 초기화 되지 않은 변수에 접근할 경우 Exception 발생
- isInitialized 속성을 통해 초기화가 되었는지 확인 후 사용한다.

lazy : Delegation
- 특정 기능을 다른 객체에게 위임하고, 그에 따라 필요한 시점에서 메소드의 호출만 받는 패턴
- 람다식을 파라미터로 받아 Lazy<T> 객체를 반환. Delegation을 수행한다.
- 최초로 사용될 때( get() ) 지정된 lambda 코드가 실행되며 해당 값을 저장한다.
- 이후 사용될 때마다 저장된 값이 반환된다.
- 지정된 람다 코드는 동기식으로 동작한다.
- Delegation(위임) 설명
package org.example.s240320_4_class abstract class Parent { abstract fun hello() } class AChild:Parent() { override fun hello() {print("AA")} } class BChild(val c:Parent):Parent() { // 자식클래스가 되긴 되어야되는데 구현을 하기 귀찮아서 AChild에 구현이 된 // 걸 BChild에서 사용하는 것 override fun hello() {return c.hello()} } fun main() { val c1 = AChild() val c2 = BChild(c1) // 직접 구현을 하지 않고 AChild에서 구현된 걸 BChild에서도 쓰겠다. c2.hello() }
- lazy(지연 초기화) 설명
package org.example.s240320_4_class
class Hello2 {
val message by lazy {
1 + 1
}
fun test() {
print(message)
}
}
fun main() {
val h = Hello2()
h.test()
h.test() // 2번째 불렀을 때는 계산하지 않고 기존의 값을 그대로 사용함
// lazy는 바로 변수가 선언되지 않고 불렀을 때 그 때 선언이 됨
// 최초로 쓸 때 지연이 발생한다.
// lazy : 지연 초기화
// val만 사용 가능
}
- lazy : 코드에 작성은 되어있지만, 사용하지 않을 때는 message는 계산되어지지 않은 상태 + 메모리에 올라와있지 않은 상태임. 불렀을 때 계산이 되어지고, 메모리에 올라오게 됨.
lazy (지연 초기화)
Companion objects
- 클래스 안에서 선언된 객체로 하나의 instance만 생성되며 클래스 이름으로 access 할 수 있다.

- Static 과 비슷해 보이지만 static과 같지는 않다.
- 각 변수는 static 변수로 Outer class에 등록된다.
- companion object의 나머지 함수 등의 내용은 static class 코드로 변환되며 var 변수에 대한 getter, setter 함수가 추가된다.
- Outer class에서 해당 객체를 생성해 static 변수에 등록한다.
Interfaces
- 가상 함수 또는 함수를 가진 Interface를 선언 할 수 있다.
- 추상 클래스와 달리 데이터를 저장할 수는 없다
- 함수는 기본 body를 가질 수도 있다.
- 구현하기 : 클래스 이름 : Interface 이름을 적는다.
- Interface 구현이므로 부모 클래스 생성자와 달리 () 없이 이름만 적는다.


interface A {
fun test1()
fun test2() {println("test2")}
}
class Hello_interface1 : A {
override fun test1() { println("test1") }
}
class Hello_interface2: A {
override fun test1() { println("test1")}
override fun test2() { println("override test2")}
}
fun main() {
val one = Hello_interface1()
one.test1()
one.test2()
val two = Hello_interface2()
two.test1()
two.test2()
}
- properties는 abstract로 선언되거나 get() 함수를 제공해야 한다.
- interface의 properties는 상태를 저장하지 못한다.
- interface는 다른 Interface를 상속할 수 있다.
- 이 경우 클래스는 누락된 부분만 implement 하면 된다.
-
interface Parent2 { fun func() } interface Child:Parent2 { fun ChildFunc() } class A1:Child { override fun func() {println("func")} override fun ChildFunc() {println("childFunc")} } // 모든 함수를 오버라이드 해줘야함
- 여러 interface를 구현할 때
- 모든 interface에 default 구현이 있는 경우 명시적으로 override해 주어야 하며 새로운 코드를 작성하거나 super<Base>로 interface를 지정해 호출할 수 있다.
- 만약 함수 이름도 똑같고, 함수 타입도 똑같고 이럴 경우에 발생함.. 가능성 희박


실습

- 다음 interface를 구현한 TestImpl 클래스를 작성하라
package org.example.s240320_4_class
interface TEST10 {
fun message():String
fun value():Int
}
// TestImpl
class TestImpl:TEST10 {
override fun message():String {return "TestImpl message"}
override fun value():Int {return 1}
}
fun main() {
val ti = TestImpl()
print("${ti.message()} + ${ti.value()}")
}
Functional(SAM) Interfaces
- 하나의 추상 함수(Single Abstract Method)만을 가진 Interface
- Functional interface 또는 SAM interface라고 하며 다음과 같이 fun interface라고 선언한다.

fun interface SAM {
fun a(num:Int) // 추상함수가 하나밖에 없고, 그걸 object 형식으로 쓸 수 있게 끔 하는 것
}
fun main() {
// 축약형
val a1:SAM = SAM {print("a1")} // 람다식을 이용하여 SAM 인터페이스 구현
val a2:SAM = SAM {print("A2")} // 람다식은 fun a를 가리킴
// 비축약형
val a3:SAM = object:SAM{
override fun a(num:Int) {print("Basic")}
}
a1.a(2)
a2.a(1)
a3.a(10)
}
- implements 한 클래스 객체를 만들어 사용
- 인터페이스 타입의 객체를 만들어 사용
public interface A {
public void a();
}
public class B implements A {
public void a() {}
}
// 1번
A value = new B();
// A 타입은 a 함수가 구현이 되었는가? 만을 검사하기때문에 이렇게 가능하다.
// class B에서 추상메서드를 구현하였음
// 2번
A value2 = new A() {
public void a() {}
}
// A 타입이 요구하는 추상메서드를 만들어 전달하는 방법
// List<String> list = new ArrayList
interface A {
fun a()
}
class B:A {
override fun a() {}
}
// 1번
val value:A = B()
// 2번
val value2:A = object:A {
override fun a()
}
하나의 추상메서드만을 가진 인터페이스를 코틀린에서는 “SAM” 이라고 부르는데 이때 interface에 fun을 붙여서 사용할 수 있다.
fun interface A {
fun a(a:Int, b:Int)
}
val value3:A = { a, b -> ...// Body 구현
}
실습
다음과 같은 interface를 SAM conversion으로 구현하여 2개 이상의 객체를 생성하라.

fun interface Test {
fun message(): String
}
fun main() {
val t1 = Test{"hello"}
println(t1.message())
val t2 = Test{"world"}
println(t2.message())
}
- 만약 Test{} 안에 return을 적으면 가장 가까운 함수인 main 함수가 return이 되어버린다.
Visibility modifiers
- Class, object, interface, constructor, function, property, setter에 대하여 다음 네 가지의 visibility modifiers를 제공 (getter는 property를 따름)

- protected : 상속을 고려하지 않은것만 사용
var value = 0 // public 모든곳에서 사용가능
private var value = 0 // private 현재 파일에서만 사용 가능
class 안에 적지 않으면 Top-level 함수, 변수가 된다.
Extensions
- 상속 없이 함수를 추가하는 방법
- Receiver Type.함수 형태로 정의한다. 아래의 경우 MutableList<Int>가 Receiver
- this 는 Receiver object
var i = 3 //
var j = i.abs()
-3.abs() // 원래 타입에 없는 멤버함수를 추가하는 용도로 사용한다.

Password p = new String(); // 자바에서는 안됨 자식클래스에 부모클래스가 들어가려고 하기때문에
fun abs(num:Int):Int { } // 누구나 불러 쓸 수 있는 함수
abs(3)
fun Int.abs(num:Int):Int { // <- 확장 함수 , Int 객체를 사용하는 객체만 사용 가능
this}
3.abs()
- 확장을 사용하면 원래 Int 객체는 건드리지 않는다, 함수의 사용 범위를 해당 객체로 줄인다는 느낌
- 실제 클래스에 멤버 함수가 추가되는 것은 아니며 해당 타입에서 . 연산자를 통해 불러 쓸수 있는 함수를 추가할 뿐임.
- 같은 이름, 같은 파라미터의의 함수가 멤버로 이미 정의 된 경우 멤버 함수가 호출됨
- Overloading은 허용됨
- 기존 타입을 수정하지 않고 기능을 확장하는 효과가 있음

- Int 타입을 Receiver로 절대값을 반환하는 abs () 함수를 Extension으로 작성하라.
- 다음과 같이 불러쓸 수 있도록 작성하라.
fun Int.abs() = if (this>0) this else -this
fun main() {
val value = -3
println(value.abs())
println(25.abs())
}
실습
- String타입의 길이가 8이상이면 true, 아니면 false를 반환하는 isValid
fun String.isValid() = this.length >= 8
fun main() {
val pw = "123"
println(pw.isValid()) // false
println("12345678".isValid()) // true
}
Data Classes
- 데이터를 전달하기 위한 목적의 클래스. data 로 마크한다.
- Primary Constructor에는 하나 이상의 파라미터가 넘어가야 한다.
- Primary constructor의 파라미터에 대해 다음 함수가 자동으로 생성된다.
- equals(), hashCode(), toString(), componentN(), copy()
- Primary constructor의 파라미터에 대해 다음 함수가 자동으로 생성된다.
class User (
val userId:String,
val name:String,
val password:String
) {
override fun equals(other:Any?):Boolean {
val user = other as User
return this.userId == user.userId
}
}
data class Data_User ( // Data Classes를 사용하면 자동으로 equals와 toString 를 만들어줌
val userId:String,
val name:String
){
var password:String = ""
}
fun main() {
val user = User("abc","user","123123a")
val user2 = User("abc","user","123123a")
println(user == user2) // 오버라이드된 equals
println(user === user2) // false 임, 객체가 생성된 메모리 주소값이 다르기 때문에
// 코틀린에서는 == 값 비교 (equals)
// === 는 객체 비교 (메모리 주소값 비교)
val d_user = Data_User("abc","user")
d_user.password = "123"
val d2_user = Data_User("abc","user")
d2_user.password = "1234"
println(d_user == d2_user)
println(d_user === d2_user)
println(d_user)
}

- 위와 같이 data 클래스에서 password를 따로 분리를 했을 때는 대입을 따로 하여야 하고 비교 연산과 toString에서도 제외가 됨
- 다른 클래스를 상속 받을 수 있고 body를 가질 수 있다.
- body에서 별도로 변수를 추가할 수 있지만 이 경우 다음 함수들에 해당 property가 포함되지 않는다.
- equals(), hashCode(), toString(), componentN(), copy()
- Destructuring 방법

Sealed classes
- 상속을 같은 패키지로 제한하는 클래스, 인터페이스.
- 클래스: 모든 자식 클래스를 컴파일 타임에 알 수 있어야 한다.
- 인터페이스: 모든 구현체를 컴파일 타임에 알 수 있어야 한다.
- 서브 클래스, 인터페이스들은 같은 패키지에서 선언되어야 한다.

Generics
- Java에서와 같이 타입 파라미터를 받을 수 있다.

- 함수를 선언할 때는 다음의 문법을 따른다.
- fun <T> name( param:T ): T
- 특정 타입 및 그 자식 클래스만을 사용하도록 upper bound를 지정할 수도 있다.
- <T:UpperBound> 와 같이 표기한다.
Nested and inner classes
- Class, Interface는 Nest 될 수 있음
- Outer 객체 생성 여부와 상관 없이
- Nested 객체 생성 가능

- inner 마크가 붙은 Nested class는 outer 클래스의 멤버에 접근 가능
- Outer 객체가 생성된 이후 Inner 객체 생성 가능
- Outer 객체가 생성된 이후 Inner 객체 생성 가능
→ 그냥 class 안에 class를 만들어 사용했을 때는 Outer 클래스의 주소를 이용해서 그 안의 class에 접근을 하여 Outer 클래스의 생성 여부에 상관없이 접근이 가능하지만, 그 안의 클래스가 Outer 클래스에 접근을 하지 못한다
- inner 를 붙여주면 무조건적으로 Outer 클래스를 먼저 생성해주어야지만 사용 가능하다. 외부 클래스도 같이 생성이 되었기때문에 외부 클래스의 변수나 함수도 사용 가능
- 내부 클래스에서 외부 클래스의 부모 함수를 호출하려면 super@외부 클래스 를 사용한다

Enum classes
- 다음과 같이 enum 클래스를 정의 할 수 있다.
- 각 항목들은 Enum class의 instance 이므로 다음과 같이 초기화도 가능하다.
enum class Direction(val value:Int) {
NORTH(0), SOUTH(1), WEST(2), EAST(3)
}
fun move(dir:Direction) {
println("move to $dir , ${dir.value}")
}
fun main() {
val dir = Direction.EAST
move(dir)
}

Inline value classes
- 하나의 값을 가지는 Wrapper 역할을 할 수 있는 클래스
- value 키워드를 class 앞에 붙인다.
- primary constructor에는 단 하나의 val property만이 초기화 되어야 한다.
- secondary constructor를 가질 수 있으며 getter만을 가진 가상의 val 을 가질 수 있다
Object expressions
- 익명 클래스의 객체를 생성해주는 expression.
- 싱글톤 객체로 생성되며 한 번만 사용하는 경우 유용하다.
- Super type이 생략된 경우 Any를 상속한다.
open class Parent
interface interface1 {fun a()}
val obj = object {
val name = "ssString"
}
val obj2: Parent = object:Parent(), interface1 {
val name = "ssString"
override fun a() {println("익명 클래스 상속가능")}
}
Delegation
- Delegation Pattern - 상속 없이 코드를 재활용 하는 방법
- 클래스 단위에서 by 키워드로 Delegation을 지원한다.

일단 Interface를 모두 구현해놓은 “성실한 구현한 클래스” 를 사용하여 그 클래스에 구현해놓은걸 대신 불러오고, 거기에 내가 추가로 다시 override 해서 필요한 것만 재정의 할 수 있다.
Classes
Classes
- class 키워드로 선언한다.
- class의 구성
- header: 클래스 이름 다음부터 { 까지. constructor, 상속 등을 정의
- body: { } 로 정의된 코드 영역
- Java와 다른 점
- class의 access keyword - public 생략 가능
- 객체 생성자를 부를 때 new 를 적지 않는다.
- header 와 body 가 모두 선택사항이다.
Constructor(생성자)
- 하나의 Primary Constructor 와 여러 개의 Secondary Constructor로 구성할 수 있 다.
- Primary Constructor는 반드시 호출되어야 한다. (몇 단계를 거치든 무조건 호출되어야함)

- Primary Constructor는 header 영역에 정의된다

- Primary Constructor에 annotation도 없고 visibility 가 public 일 경우 다음과 같이 constructs 키워드를 생략하고 줄여쓸 수 있다

- Primary Constructor로 전달 된 파라미터는 클래스 Body 내의 변수 초기화에 사용할 수 있다.
// 클래스 Body 전체가 Primary 생성자를 초기화 할 수 있게 된다.
// constructor 는 생략 가능
class Hello constructor(userName : String) {
private val name:String = userName // 외부에서 접근 불가
}
fun hello(userName:String) {
val name = userName
}
- 초기화 코드가 필요할 경우 init { } 블록을 추가하여 작성 할 수 있다.
- init 블록은 Primary Constructor 일부 이다.
- init 블록이 여러 개 있으면 위에서 아래 순으로 실행된다.
-
class Hello(userName : String) { private val name:String = userName init { println("Hello! $name") } } fun main() { val h = Hello("user") }
- 코틀린에서는 class의 이름은 첫문자를 대문자, 함수는 소문자로 작성하여야 함. 그렇지 않으면 내부적으로 클래스를 함수로, 함수를 클래스로 인식할 수 도 있다.
실습
- 인자를 받지 않는 생성자를 가진 Test 클래스를 선언하고 객체를 생성한다.
class Test() {
init {
println("객체 생성됨")
}
}
fun main() {
val t = Test()
}
- 정수형 value property x, y를 가지는 Position class를 정의.
- Primary constructor 에서 정수형 숫자 2개를 받아서 property x, y를 초기화.
- 객체를 2개 이상 생성.
class Position(x:Int, y:Int) {
val x = x
val y = y
}
fun main() {
val p1 = Position(0,0)
val p2 = Position(2, 1)
}
- Primary Constructor에서 property 선언과 초기화를 한번에 할 수 있다.
- Class property도 할당 횟수에 따라 val, var로 구분하여 사용한다.

- 앞에서 작성한 Position 클래스에서 x, y 변수를 생성자에서 바로 선언하고 초기화 하도록 코드를 수정하세요.
-
// 이전 class Position(x: Int, y:Int) { val x = x val y = y } // 수정 후 class Position(val x:Int, val y:Int) { }
Secondary Constructor
- Secondary Constructor
- constructor 키워드를 이용해 class body에 선언한다.
- 직접적 또는 간접적으로 Primary constructor를 호출해야 한다.
class Hello (val name:String, val age:Int) { // 주 생성자
/*
(val name:String) 을 자바로 바꾸면
String name;
public Hello(String a) { name = a; }
*/
// 이름만 입력하는 경우
constructor(name:String):this(name,20) // 보조 생성자
// 나이만 입력하는 경우
constructor(age:Int):this("",age) // 보조 생성자
// 아무것도 입력하지 않는 경우 -> 이름만 입력한 경우의 생성자를 호출함
constructor():this("") // 보조 생성자
}
fun main() {
val h = Hello("user", 22)
println(h.name)
val h2 = Hello()
println(h2.name)
val h3 = Hello(22)
println(h3.name)
val h4 = Hello("lee")
println(h4.name)
}
- 코틀린에서는 파라미터가 많은걸 왠만하면 주 생성자로 사용한다.
→ 왜냐하면 주 생성자에 선언을 하면 val을 생략할 수 있기때문에
실습
- Position 클래스에 정수형 파라미터 하나만을 받아 x, y 모든 변수에 해당 값을 설정하는 Secondary Constructor를 추가하라.
package org.example.s240313_4_class
class Position(val x:Int, val y:Int) {
// 하나만 주면 같은 값으로 세팅하도록
constructor(value:Int):this(value,value)
}
/* 자바로 바꾼다면 2줄이 이만큼 커짐..
public class Position {
int x;
int y;
public Position(int value) {
x = value;
y = value;
}
public Postition(x, y) {
this.x = x;
this.y = y;
}
}
*/
fun main() {
val p1 = Position(0,0)
val p2 = Position(2, 1)
val p3 = Position(1)
}
Inheritance(상속)
- 모든 클래스의 최상위 부모는 Any: 부모가 명시되지 않은 경우 Any의 자식 클래스
- 상속을 하지 않으면 내부적으로는 Any를 상속받게 됨
- 클래스는 기본적으로 final 처리되어 상속 불가능
- 상속을 허용하려면 open 키워드를 사용해야 한다.
- 자식 클래스는 반드시 부모 클래스의 생성자를 사용하여야 한다.
- 부모 클래스를 지정하려면 header 뒤에 : 부모클래스() 와 같이 적는다.
- 부모 클래스의 Constructor를 호출하는 형태로 적어야 한다
- 부모의 생성자를 먼저 만들고 → 그 다음에 자식이 만들어지는 것

open class Parent {init {println("부모 생성")}}
class Child:Parent() {init {println("자식 생성")}}
fun main() {
var p = Parent()
var c = Child()
}

open class Parent() {
init {println("부모 생성")}
constructor(msg:String):this() { // 파라미터를 받는 보조 생성자가 넣는 순간 기본 생성자를 명시적으로 적어주어야함
println("부모 생성 - $msg")
}
}
class Child:Parent() {init {println("자식 생성")}}
fun main() {
var p = Parent()
var c = Child()
var p1 = Parent("MSGSMGMSGMSMGSMGSMGM")
}
- 만약 자식 클래스에 parmary constructor가 없다면 각 secondary constructor에서 super 키워드를 이용해 초기화 하거나 다른 secondary constructor를 이용해서 부모 클래스를 초기화한다.

- Position을 상속 가능하게 변경하고 이를 상속받은 TPosition 클래스를 정의한다.
- TPosition클래스는 정수형 z 변수를 추가로 가지며 Primary Constructor는 정수형 숫자 3개를 받는다.
- TPosition 클래스의 객체를 생성하라.

Overriding methods(재정의)
- 자식 클래스의 override를 허용하려면 부모 클래스의 함수에 open 키워드를 지정한다.
- 코틀린에서는 클래스 내의 함수도 final 처리가 되어져서 open 구문 필요
- 자식 클래스는 명시적으로 override 키워드를 추가하여 함수를 override 한다.
- override를 하려면 부모클래스에서는 함수에 open, 자식클래스에서는 함수에 override를 무조건 명시해줌

- override 키워드를 사용한 함수는 자동으로 open이 되며 만약 다른 자식의 override를 허용 하지 않으려면 final을 지정한다.

- 자바에서의 override는 필수가 아니고 만약 부모클래스의 메소드를 override를 할 때 자식클래스에서 오타가 나도 오류가 뜨지 않음!! 잘못된 사용이 발생할 수 있다.
TEST
- Position 클래스에 printValue 함수를 추가하라. 함수는 (x, y) 형태로 값을 출력한다.
- TPosition 클래스에서는 printValue 함수를 override 하여 (x, y, z)의 형태로 값이 출력되도록 한다.

Superclass methods
- 자식클래스에서 override 된 함수 말고 부모 클래스의 함수를 호출하려면 super 키워드를 사용한다.

Overriding properties
- val에도 open 및 override 키워드를 사용해야 한다.

- val을 var로 상속 받는 것은 가능하며 반대는 안된다.

초기화 순서
- 부모 클래스의 초기화가 먼저 이루어지고 자식 클래스가 초기화 된다.
- 부모 생성자의 할당 실행
- → 부모 생성자의 init 실행
- 부모의 나머지 초기화 코드 실행
- 자식 클래스의 초기화 실행
Abstract classes(추상클래스)
open class Animal() {
open fun say():String {return ""}
}
class Dog:Animal() {
override fun say():String {return "멍멍"}
}
class Cat:Animal() {
override fun say():String {return "야옹"}
}
class Pig:Animal() {}
// override를 빠뜨렸을때 부모의 함수를 사용한다.
fun main() {
val arr = mutableListOf<Animal>()
arr.add(Cat())
arr.add(Dog())
// arr.add(Animal())
arr.forEach{println(it.say())}
}
// 향후 발생할수 있는 문제점
// 만약 Animal() 클래스는 실 사용을 하려고 만든 클래스가 아닌
// override 를 위해 만든 클래스인데 메인 함수에서 Animal() 클래스를 생성한다면?
// -> 추상클래스로 해결
위의 문제를 추상클래스로 해결
- 일부 멤버 또는 전체 멤버가 abscract 일 때, 클래스가 abstract로 선언 된다.
- abstract 멤버는 open 키워드를 사용하지 않아도 된다.
- 추상 함수가 아닌 일반 함수를 포함할 수 있으며 일반함수는 필요에 따라 open 할 수 있다.
Properties
- Read-only, mutable한 경우에 맞게 val, var로 선언한다.
-
- Property들은 모두 constructor에서 할당 되어야 한다.
- 그래서 val의 경우 read-only라 생각해도 된다.
- 초기화를 통해 타입을 추론할 수 있게 하거나 타입을 정해줘야 한다.
-

- Getter/Setter : 각 Property는 getter와 setter를 가질 수 있다.
- 생략할 경우 기본 getter/setter가 제공된다.
- val은 getter만 제공된다.
- 코틀린에서 변수를 만들어주면 내부적으로 getter와 setter를 자동으로 만들어줌
- 밑의 사진은 코틀린 코드를 자바 코드로 변환된 코드이다.


- 자바 라이브러리의 호환성 등을 위해 getter/setter 함수를 사용하지 않으려면 property에 @JvmField 어노테이션을 추가한다.
class Hello1 {
var a = 0
get() {
println("getter")
return field
}
set(value) {
println("setter")
// a = value
field = value
}
}
fun main() {
val hello = Hello1()
hello.a = 1
println(hello.a)
}
- var의 경우 getter, setter를 , val은 getter를 별도로 구현할 수 있다.
- getter, setter에서 본래의 값을 부르는 변수는 field이다.
- 원래의 변수 이름을 사용하면 무한 반복에 걸려 Stack Overflow가 발생한다.
TEST
- 0 이상의 정수만 할당할 수 있는 count 변수를 가진 Test 클래스를 정의하라. 음수로 할당할 경우 무시하면 된다.

Properties
lateinit
- null을 허용하지 않는 변수를 생성자가 아닌 함수에서 초기화
- var에서만 사용 가능
- lateinit var 이름:type 형식으로 사용
- 초기화 되지 않은 변수에 접근할 경우 Exception 발생
- isInitialized 속성을 통해 초기화가 되었는지 확인 후 사용한다.

lazy : Delegation
- 특정 기능을 다른 객체에게 위임하고, 그에 따라 필요한 시점에서 메소드의 호출만 받는 패턴
- 람다식을 파라미터로 받아 Lazy<T> 객체를 반환. Delegation을 수행한다.
- 최초로 사용될 때( get() ) 지정된 lambda 코드가 실행되며 해당 값을 저장한다.
- 이후 사용될 때마다 저장된 값이 반환된다.
- 지정된 람다 코드는 동기식으로 동작한다.
- Delegation(위임) 설명
package org.example.s240320_4_class abstract class Parent { abstract fun hello() } class AChild:Parent() { override fun hello() {print("AA")} } class BChild(val c:Parent):Parent() { // 자식클래스가 되긴 되어야되는데 구현을 하기 귀찮아서 AChild에 구현이 된 // 걸 BChild에서 사용하는 것 override fun hello() {return c.hello()} } fun main() { val c1 = AChild() val c2 = BChild(c1) // 직접 구현을 하지 않고 AChild에서 구현된 걸 BChild에서도 쓰겠다. c2.hello() }
- lazy(지연 초기화) 설명
package org.example.s240320_4_class
class Hello2 {
val message by lazy {
1 + 1
}
fun test() {
print(message)
}
}
fun main() {
val h = Hello2()
h.test()
h.test() // 2번째 불렀을 때는 계산하지 않고 기존의 값을 그대로 사용함
// lazy는 바로 변수가 선언되지 않고 불렀을 때 그 때 선언이 됨
// 최초로 쓸 때 지연이 발생한다.
// lazy : 지연 초기화
// val만 사용 가능
}
- lazy : 코드에 작성은 되어있지만, 사용하지 않을 때는 message는 계산되어지지 않은 상태 + 메모리에 올라와있지 않은 상태임. 불렀을 때 계산이 되어지고, 메모리에 올라오게 됨.
lazy (지연 초기화)
Companion objects
- 클래스 안에서 선언된 객체로 하나의 instance만 생성되며 클래스 이름으로 access 할 수 있다.

- Static 과 비슷해 보이지만 static과 같지는 않다.
- 각 변수는 static 변수로 Outer class에 등록된다.
- companion object의 나머지 함수 등의 내용은 static class 코드로 변환되며 var 변수에 대한 getter, setter 함수가 추가된다.
- Outer class에서 해당 객체를 생성해 static 변수에 등록한다.
Interfaces
- 가상 함수 또는 함수를 가진 Interface를 선언 할 수 있다.
- 추상 클래스와 달리 데이터를 저장할 수는 없다
- 함수는 기본 body를 가질 수도 있다.
- 구현하기 : 클래스 이름 : Interface 이름을 적는다.
- Interface 구현이므로 부모 클래스 생성자와 달리 () 없이 이름만 적는다.


interface A {
fun test1()
fun test2() {println("test2")}
}
class Hello_interface1 : A {
override fun test1() { println("test1") }
}
class Hello_interface2: A {
override fun test1() { println("test1")}
override fun test2() { println("override test2")}
}
fun main() {
val one = Hello_interface1()
one.test1()
one.test2()
val two = Hello_interface2()
two.test1()
two.test2()
}
- properties는 abstract로 선언되거나 get() 함수를 제공해야 한다.
- interface의 properties는 상태를 저장하지 못한다.
- interface는 다른 Interface를 상속할 수 있다.
- 이 경우 클래스는 누락된 부분만 implement 하면 된다.
-
interface Parent2 { fun func() } interface Child:Parent2 { fun ChildFunc() } class A1:Child { override fun func() {println("func")} override fun ChildFunc() {println("childFunc")} } // 모든 함수를 오버라이드 해줘야함
- 여러 interface를 구현할 때
- 모든 interface에 default 구현이 있는 경우 명시적으로 override해 주어야 하며 새로운 코드를 작성하거나 super<Base>로 interface를 지정해 호출할 수 있다.
- 만약 함수 이름도 똑같고, 함수 타입도 똑같고 이럴 경우에 발생함.. 가능성 희박


실습

- 다음 interface를 구현한 TestImpl 클래스를 작성하라
package org.example.s240320_4_class
interface TEST10 {
fun message():String
fun value():Int
}
// TestImpl
class TestImpl:TEST10 {
override fun message():String {return "TestImpl message"}
override fun value():Int {return 1}
}
fun main() {
val ti = TestImpl()
print("${ti.message()} + ${ti.value()}")
}
Functional(SAM) Interfaces
- 하나의 추상 함수(Single Abstract Method)만을 가진 Interface
- Functional interface 또는 SAM interface라고 하며 다음과 같이 fun interface라고 선언한다.

fun interface SAM {
fun a(num:Int) // 추상함수가 하나밖에 없고, 그걸 object 형식으로 쓸 수 있게 끔 하는 것
}
fun main() {
// 축약형
val a1:SAM = SAM {print("a1")} // 람다식을 이용하여 SAM 인터페이스 구현
val a2:SAM = SAM {print("A2")} // 람다식은 fun a를 가리킴
// 비축약형
val a3:SAM = object:SAM{
override fun a(num:Int) {print("Basic")}
}
a1.a(2)
a2.a(1)
a3.a(10)
}
- implements 한 클래스 객체를 만들어 사용
- 인터페이스 타입의 객체를 만들어 사용
public interface A {
public void a();
}
public class B implements A {
public void a() {}
}
// 1번
A value = new B();
// A 타입은 a 함수가 구현이 되었는가? 만을 검사하기때문에 이렇게 가능하다.
// class B에서 추상메서드를 구현하였음
// 2번
A value2 = new A() {
public void a() {}
}
// A 타입이 요구하는 추상메서드를 만들어 전달하는 방법
// List<String> list = new ArrayList
interface A {
fun a()
}
class B:A {
override fun a() {}
}
// 1번
val value:A = B()
// 2번
val value2:A = object:A {
override fun a()
}
하나의 추상메서드만을 가진 인터페이스를 코틀린에서는 “SAM” 이라고 부르는데 이때 interface에 fun을 붙여서 사용할 수 있다.
fun interface A {
fun a(a:Int, b:Int)
}
val value3:A = { a, b -> ...// Body 구현
}
실습
다음과 같은 interface를 SAM conversion으로 구현하여 2개 이상의 객체를 생성하라.

fun interface Test {
fun message(): String
}
fun main() {
val t1 = Test{"hello"}
println(t1.message())
val t2 = Test{"world"}
println(t2.message())
}
- 만약 Test{} 안에 return을 적으면 가장 가까운 함수인 main 함수가 return이 되어버린다.
Visibility modifiers
- Class, object, interface, constructor, function, property, setter에 대하여 다음 네 가지의 visibility modifiers를 제공 (getter는 property를 따름)

- protected : 상속을 고려하지 않은것만 사용
var value = 0 // public 모든곳에서 사용가능
private var value = 0 // private 현재 파일에서만 사용 가능
class 안에 적지 않으면 Top-level 함수, 변수가 된다.
Extensions
- 상속 없이 함수를 추가하는 방법
- Receiver Type.함수 형태로 정의한다. 아래의 경우 MutableList<Int>가 Receiver
- this 는 Receiver object
var i = 3 //
var j = i.abs()
-3.abs() // 원래 타입에 없는 멤버함수를 추가하는 용도로 사용한다.

Password p = new String(); // 자바에서는 안됨 자식클래스에 부모클래스가 들어가려고 하기때문에
fun abs(num:Int):Int { } // 누구나 불러 쓸 수 있는 함수
abs(3)
fun Int.abs(num:Int):Int { // <- 확장 함수 , Int 객체를 사용하는 객체만 사용 가능
this}
3.abs()
- 확장을 사용하면 원래 Int 객체는 건드리지 않는다, 함수의 사용 범위를 해당 객체로 줄인다는 느낌
- 실제 클래스에 멤버 함수가 추가되는 것은 아니며 해당 타입에서 . 연산자를 통해 불러 쓸수 있는 함수를 추가할 뿐임.
- 같은 이름, 같은 파라미터의의 함수가 멤버로 이미 정의 된 경우 멤버 함수가 호출됨
- Overloading은 허용됨
- 기존 타입을 수정하지 않고 기능을 확장하는 효과가 있음

- Int 타입을 Receiver로 절대값을 반환하는 abs () 함수를 Extension으로 작성하라.
- 다음과 같이 불러쓸 수 있도록 작성하라.
fun Int.abs() = if (this>0) this else -this
fun main() {
val value = -3
println(value.abs())
println(25.abs())
}
실습
- String타입의 길이가 8이상이면 true, 아니면 false를 반환하는 isValid
fun String.isValid() = this.length >= 8
fun main() {
val pw = "123"
println(pw.isValid()) // false
println("12345678".isValid()) // true
}
Data Classes
- 데이터를 전달하기 위한 목적의 클래스. data 로 마크한다.
- Primary Constructor에는 하나 이상의 파라미터가 넘어가야 한다.
- Primary constructor의 파라미터에 대해 다음 함수가 자동으로 생성된다.
- equals(), hashCode(), toString(), componentN(), copy()
- Primary constructor의 파라미터에 대해 다음 함수가 자동으로 생성된다.
class User (
val userId:String,
val name:String,
val password:String
) {
override fun equals(other:Any?):Boolean {
val user = other as User
return this.userId == user.userId
}
}
data class Data_User ( // Data Classes를 사용하면 자동으로 equals와 toString 를 만들어줌
val userId:String,
val name:String
){
var password:String = ""
}
fun main() {
val user = User("abc","user","123123a")
val user2 = User("abc","user","123123a")
println(user == user2) // 오버라이드된 equals
println(user === user2) // false 임, 객체가 생성된 메모리 주소값이 다르기 때문에
// 코틀린에서는 == 값 비교 (equals)
// === 는 객체 비교 (메모리 주소값 비교)
val d_user = Data_User("abc","user")
d_user.password = "123"
val d2_user = Data_User("abc","user")
d2_user.password = "1234"
println(d_user == d2_user)
println(d_user === d2_user)
println(d_user)
}

- 위와 같이 data 클래스에서 password를 따로 분리를 했을 때는 대입을 따로 하여야 하고 비교 연산과 toString에서도 제외가 됨
- 다른 클래스를 상속 받을 수 있고 body를 가질 수 있다.
- body에서 별도로 변수를 추가할 수 있지만 이 경우 다음 함수들에 해당 property가 포함되지 않는다.
- equals(), hashCode(), toString(), componentN(), copy()
- Destructuring 방법

Sealed classes
- 상속을 같은 패키지로 제한하는 클래스, 인터페이스.
- 클래스: 모든 자식 클래스를 컴파일 타임에 알 수 있어야 한다.
- 인터페이스: 모든 구현체를 컴파일 타임에 알 수 있어야 한다.
- 서브 클래스, 인터페이스들은 같은 패키지에서 선언되어야 한다.

Generics
- Java에서와 같이 타입 파라미터를 받을 수 있다.

- 함수를 선언할 때는 다음의 문법을 따른다.
- fun <T> name( param:T ): T
- 특정 타입 및 그 자식 클래스만을 사용하도록 upper bound를 지정할 수도 있다.
- <T:UpperBound> 와 같이 표기한다.
Nested and inner classes
- Class, Interface는 Nest 될 수 있음
- Outer 객체 생성 여부와 상관 없이
- Nested 객체 생성 가능

- inner 마크가 붙은 Nested class는 outer 클래스의 멤버에 접근 가능
- Outer 객체가 생성된 이후 Inner 객체 생성 가능
- Outer 객체가 생성된 이후 Inner 객체 생성 가능
→ 그냥 class 안에 class를 만들어 사용했을 때는 Outer 클래스의 주소를 이용해서 그 안의 class에 접근을 하여 Outer 클래스의 생성 여부에 상관없이 접근이 가능하지만, 그 안의 클래스가 Outer 클래스에 접근을 하지 못한다
- inner 를 붙여주면 무조건적으로 Outer 클래스를 먼저 생성해주어야지만 사용 가능하다. 외부 클래스도 같이 생성이 되었기때문에 외부 클래스의 변수나 함수도 사용 가능
- 내부 클래스에서 외부 클래스의 부모 함수를 호출하려면 super@외부 클래스 를 사용한다

Enum classes
- 다음과 같이 enum 클래스를 정의 할 수 있다.
- 각 항목들은 Enum class의 instance 이므로 다음과 같이 초기화도 가능하다.
enum class Direction(val value:Int) {
NORTH(0), SOUTH(1), WEST(2), EAST(3)
}
fun move(dir:Direction) {
println("move to $dir , ${dir.value}")
}
fun main() {
val dir = Direction.EAST
move(dir)
}

Inline value classes
- 하나의 값을 가지는 Wrapper 역할을 할 수 있는 클래스
- value 키워드를 class 앞에 붙인다.
- primary constructor에는 단 하나의 val property만이 초기화 되어야 한다.
- secondary constructor를 가질 수 있으며 getter만을 가진 가상의 val 을 가질 수 있다
Object expressions
- 익명 클래스의 객체를 생성해주는 expression.
- 싱글톤 객체로 생성되며 한 번만 사용하는 경우 유용하다.
- Super type이 생략된 경우 Any를 상속한다.
open class Parent
interface interface1 {fun a()}
val obj = object {
val name = "ssString"
}
val obj2: Parent = object:Parent(), interface1 {
val name = "ssString"
override fun a() {println("익명 클래스 상속가능")}
}
Delegation
- Delegation Pattern - 상속 없이 코드를 재활용 하는 방법
- 클래스 단위에서 by 키워드로 Delegation을 지원한다.

일단 Interface를 모두 구현해놓은 “성실한 구현한 클래스” 를 사용하여 그 클래스에 구현해놓은걸 대신 불러오고, 거기에 내가 추가로 다시 override 해서 필요한 것만 재정의 할 수 있다.