이번 주차는 클래스와 객체이다.
클래스와 객체의 정의
객체지향 프로그래밍에서는 떼고 싶어도 뗄 수 없는 단어이다.
본 책에서는 단어를 객체에 관한 단어들을 정의해두었다.(나중에 다시 보자)
코틀린에서 사용하는 용어 | 다른 언어에서 사용하는 용어 |
클래스(Class) | 분류, 범주 |
프로퍼티(Property) | 속성(Attribute), 변수(Variable), 필드(Field), 데이터(Data) |
메서드(Method) | 함수(Function), 동작(Operation), 행동(Behavior) |
객체(Object) | 인스턴스(Instance) |
쉽게 생각하자, 어떤 클래스가 있다.
그 클래스에서 쓰는 변수를 프로퍼티, 함수를 메서드. 간단하다.
Bird라는 클래스를 만들어보자.
package chap05.section1
class Bird{ // 클래스 정의
// 프로퍼티(변수, 속성)
var name: String = "mybird"
var wing: Int = 2
var beak: String = "short"
var color: String = "blue"
// 메서드(함수)
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird() // 클래스의 생성자를 통한 객체의 생성
coco.color = "blue" // 객체의 프로퍼티에 값 할당
println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
coco.fly() // 객체의 멤버 메서드 사용
coco.sing(3)
}
바뀐 프로퍼티가 잘 출력된다.
생성자
생성자란 클래스를 통해 객체가 만들어질 때 기본적으로 호출되는 함수를 말한다.
클래스 안의 프로퍼티 값을 직접 입력하여 초기화해도 되지만 이렇게 하면 항상 같은 프로퍼티 값을 가지는 객체가 만들어진다.
객체를 생성할 때 필요한 값을 설정하여 객체를 만들면 훨씬 유연할 것이다.
생성자는 주 생성자와 부 생성자로 나뉘며 필요에 따라 주 생성자 또는 부 생성자를 사용할 수 있다.
부생성자
앞에서 나온 코드로 설명하겠다.
package chap05.section2
class Bird{ // 클래스 정의
// 프로퍼티 선언만 함
var name: String
var wing: Int
var beak: String
var color: String
// 부 생성자 - 매개변수를 통해 초기화할 프로퍼티에 지정
constructor(name: String, wing: Int, beak: String, color: String){
this.name = name // this.name은 선언된 현재 클래스의 프로퍼티를 나타냄
this.wing = wing
this.beak = beak
this.color = color
}
// 메서드(함수)
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird("mybird", 2, "short", "blue") // 생성자의 인자로 객체 생성과 동시에 초기화
coco.color = "yellow" // 객체의 프로퍼티에 값 할당
println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
coco.fly() // 객체의 멤버 메서드 사용
coco.sing(3)
}
프로퍼티로 타입만 지정하고, 부 생성자로 매개변수를 통해 프로퍼티에 지정.
그리고 메인함수에서 객체 생성과 동시에 초기화.
추상적이라고 말하지만 나한텐 너무 편하고 직관적이여서 마음에 들었다.
this 포인터를 사용하기 귀찮다면 언더바를 사용해도 괜찮다.
// 부 생성자 - 매개변수를 통해 초기화할 프로퍼티에 지정
constructor(_name: String, _wing: Int, _beak: String, _color: String){
name = _name // this.name은 선언된 현재 클래스의 프로퍼티를 나타냄
wing = _wing
beak = _beak
color = _color
}
주생성자
package chap05.section2.primary
class Bird(var name: String, var wing: Int, var beak: String, var color : String){ // 클래스 정의
// 프로퍼티는 매개변수 안에 var를 사용해 프로퍼티로서 선언되어 본문에서 생략됨
// 메서드(함수)
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird("mybird", 2, "short", "blue") // 생성자의 인자로 객체 생성과 동시에 초기화
coco.color = "yellow" // 객체의 프로퍼티에 값 할당
println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
coco.fly() // 객체의 멤버 메서드 사용
coco.sing(3)
}
코드가 더욱 간략하게 줄어든다.
초기화 블록을 가진 코드도 있다.
package chap05.section2.init
class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
// 초기화 블록
init {
println("----------초기화 블록 시작----------")
println("이름은 $name, 부리는 $beak")
this.sing(3)
println("---------- 초기화 블록 끝 ----------")
}
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird("mybird", 2, "short", "blue") // 생성자의 인자로 객체 생성과 동시에 초기화
coco.color = "yellow" // 객체의 프로퍼티에 값 할당
println("coco.color: ${coco.color}") // 객체의 멤버 프로퍼티 읽기
coco.fly() // 객체의 멤버 메서드 사용
}
상속과 다형성
상속에 대해서 먼저 알아보자.
클래스는 자식 클래스를 만들 때 상위 클래스(부모 클래스)의 속성과 기능을 물려받아 계승하는데 이것을 상속이라고 한다.
package chap05.section3.openclass
// 상속 가능한 클래스를 선언하기 위해 Open 사용
open class Bird(var name: String, var wing: Int, var beak: String, var color: String){
// 메서드
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
// 주 생성자를 사용하는 상속
class Lark(name: String, wing: Int, beak: String, color: String) : Bird(name, wing, beak, color){
fun singHitone() = println("Happy Song!") // 새로 추가한 메서드
}
class Parrot : Bird{
val language : String
constructor(name: String, wing: Int, beak: String, color: String, language: String):
super(name, wing, beak, color){
this.language = language // 새로 추가한 프로퍼티
}
fun speak() = println("Speak! $language")
}
fun main() {
val coco = Bird("mybird", 2, "short", "blue")
val lark = Lark("mylark", 2, "long", "brown")
val parrot = Parrot("myparrot", 2, "short", "multiple", "korea") // 프로퍼티 추가
println("Coco: ${coco.name}, ${coco.wing}, ${coco.beak}, ${coco.color}")
println("Lark: ${lark.name}, ${lark.wing}, ${lark.beak}, ${lark.color}")
println("Parrot: ${parrot.name}, ${parrot.wing}, ${parrot.beak}, ${parrot.color}, ${parrot.language}")
lark.singHitone() // 새로 추가한 메서드 사용 가능
parrot.speak()
lark.fly()
}
다형성
이름이 동일하지만 매개변수가 서로 다른 형태를 취하거나 실행 결과를 다르게 가질 수 있는 것을 다형성이라고 한다.
만약에 print()라는 함수가 있다면, 우리는 print("hello"), print(123) 등으로 쓴 경험이 있을것이다.
여기서 인자의 형식이 바꼈다는 차이가 있는데, 인자의 형식만 달라지는것을 오버로딩(Overloading)이라고 한다.
덧셈연산자의 오버로딩을 살펴보자.
add라는 함수는 많은데, 인자의 형식이 다른것을 확인할 수 있다.
package chap05.section3
fun main() {
val calc = Calc()
println(calc.add(3,2))
println(calc.add(3.2,1.3))
println(calc.add(3,2, 2))
println(calc.add("Hello","World"))
}
class Calc {
// 다양항 매개변수로 오버로딩된 메서드
fun add(x: Int, y: Int) : Int = x + y
fun add(x: Double, y: Double) : Double = x + y
fun add(x: Int, y: Int, z: Int) : Int = x + y + z
fun add(x: String, y: String) : String = x + y
}
상위와 하위 클래스에서 메서드나 프로퍼티의 이름은 같지만 기존의 동작을 다른 동작으로 재정의 하는것을 오버라이딩(Overriding)이라고 한다.
bird 예제로 다시 알아보자.
package chap05.section3
// 상속 가능한 클래스를 위헤 open 사용
open class Bird(var name: String, var wing: Int, var beak: String, var color: String){
// 메서드
fun fly() = println()
open fun sing(vol: Int) = println("Sing vol: $vol")
}
class Parrot(name: String, wing: Int = 2, beak: String, color: String, var language: String = "natural")
: Bird(name, wing, beak, color){
fun speak() = println("Speak! $language") // Parrot에 추가된 메서드
override fun sing(vol: Int){ // override된 메서드
println("I'm a parrot! The volume level is $vol")
speak() // 달라진 내
}
}
fun main() {
val parrot = Parrot(name = "myparrot", beak = "short", color = "multiple")
parrot.language = "English"
println("Parrot: ${parrot.name}, ${parrot.wing}, ${parrot.beak}, ${parrot.color}, ${parrot.language}")
parrot.sing(5) // 달라진 메서드 실행 가능
}
super와 this의 참조
this와 super를 통해 현재 객체를 사용하는 부 생성자이다.
package chap05.section4.personthis
open class Person{
constructor(firstName: String){
println("[Person] firstName: $firstName")
}
constructor(firstName: String, age: Int){
println("[Person] firstName: $firstName $age")
}
}
class Developer: Person{
constructor(firstName: String) : this(firstName, 10){
println("[Developer] $firstName")
}
constructor(firstName: String, age: Int) : super(firstName, age){
println("[Developer] $firstName, $age")
}
}
fun main() {
val sean = Developer("Sean")
}
주 생성자와 부 생성자를 함께 사용하는 경우도 있다.
package chap05.section4.prisecon
class Person(firstName: String, out: Unit = println("[Primary Constructor] Parameter")){ // 2. 주 생성자
val fName = println("[Property] Person fName: $firstName") // 3. 프로퍼티 할당
init {
println("[init] Person init block") // 4. 초기화 블럭
}
// 1. 부 생성자
constructor(firstName: String, age: Int, out: Unit = println("[Secondary Constructor] Parameter"))
: this(firstName){
println("[Secondary Constructor] Body: $firstName, $age") // 5. 부 생성자 본문
}
}
fun main() {
val p1 = Person("Kildong", 30) // 1 -> 2 호출, 3 -> 4 -> 5 실행
println()
val p2 = Person("Dooly") // 2 호출, 3 -> 4 실행
}
내부 클래스(inner class)에서 바깥 클래스로 접근하는 방법이 있다.
super키워드와 @기호를 사용해야한다.
package chap05.section4.innerref
open class Base {
open val x: Int = 1
open fun f() = println("Base Class f()")
}
class Child : Base() {
override val x: Int = super.x + 1
override fun f() = println("Child Class f()")
inner class Inside{
fun f() = println("Inside Class f()")
fun test(){
f() // 현재 이너 클래스의 f() 접근
Child().f() // 바로 바깥 클래스 f() 접근
super@Child.f() // Child의 상위 클래스인 Base클래스의 f() 접근
println("[Inside] super@Child.x: ${super@Child.x}") // Base의 x 접근
}
}
}
fun main() {
val c1 = Child()
c1.Inside().test() // 이너 클래스 Inside의 메서드 test() 실행
}
인터페이스에서 참조할 수도 있는데,
인터페이스란 일종의 구현 약속으로 인터페이스를 참조하는 클래스는 인터페이스가 가지고 있는 내용을 구현해야 하는 가이드를 제시한다.
따라서 인터페이스 자체로는 객체로 만들 수 없고 항상 인터페이스를 구현하는 클래스에서 생성해야한다.
코틀린은 자바처럼 한 번에 2개 이상의 클래스를 상속받는 다중 상속이 되지 않는다.
하지만 인터페이스로는 필요한 만큼 다수의 인터페이스를 지정해 구현할 수 있다.
package chap05.section4
open class A{
open fun f() = println("A Class f()")
fun a() = println("A Class a()")
}
interface B{
fun f() = println("B interface f()") // 인터페이스는 기본적으로 open임
fun b() = println("B interface b()")
}
class C : A(), B{ // 쉼표(,)를 사용해 클래스와 인터페이스를 지정
// 컴파일되려면 f()가 오버라이딩 되야함
override fun f() = println("C Class f()")
fun test(){
f() // 현재 클래스의 f()
b() // 인터페이스 B의 b()
super<A>.f() // A 클래스의 f()
super<B>.f() // B 클래스의 f()
}
}
fun main(){
val c = C()
c.test()
}
정보 은닉 캡슐화
클래스를 작성할 때 숨겨야 하는 속성이나 기능이 있는데, 이런 개념을 캡슐화라고 한다.
아래의 키워드 종류를 살펴보자.
종류 | 설명 |
private | 이 요소는 외부에서 접근할 수 없다. |
public | 이 요소는 어디서든 접근이 가능하다.(기본값) |
protected | 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능하다. |
internal | 같은 정의의 모듈 내부에서는 접근이 가능하다. |
각 예시를 살펴보자.
Private
package chap05.section5.privatetest
private class PrivateClass {
private var i = 1
private fun privateFunc(){
i += 1 // 접근 허용
}
fun access(){
privateFunc() // 접근 허용
}
}
class OtherClass{
val opc = PrivateClass() // 접근 불가 - 프로퍼티 opc는 private이 되야함
fun test(){
val pc = PrivateClass()
}
}
fun main() {
val pc = PrivateClass() // 생성 가능
pc.i // 접근 불가
pc.privateFunc() //접근 불가
}
fun TopFunction(){
val tpc = PrivateClass() // 객체 생성 가능
}
아래 코드인데 IntelliJ의 기능 덕분에 책의 내용을 스포당한기분이다.
Protected
아래 코드에서 main함수는 protect를 걸지 않는 access()함수만 접근 가능하다.
package chap05.section5.protectedtest
open class Base { // 최상위 클래스에는 protected를 사용할 수 없음
protected var i = 1
protected fun protectedFunc(){
i += 1 // 접근 허용
}
fun access(){
protectedFunc() // 접근 허용
}
protected class Nested // 내부 클래스에는 지시자 허용
}
class Derived : Base() {
fun test(base: Base): Int{
protectedFunc() // Base 클래스의 메서드 접근 가능
return i // Base 클래스의 프로퍼티 접근 가능
}
}
fun main() {
val base = Base() // 생성 가능
base.i // 접근 불가
base.protectedFunc() // 접근 불가
base.access() // 접근 가능
}
Internal
package chap05.section5.internal
internal class InternalClass {
internal var i = 1
internal fun icFunc() {
i += 1 // 접근 허용
}
fun access(){
icFunc() // 접근 허용
}
}
class Other{
internal val ic = InternalClass() // 프로퍼티를 지정할 때 internal로 맞춰야함
fun test(){
ic.i // 접근 허용
ic.icFunc() // 접근 허용
}
}
fun main(){
val mic = InternalClass() // 생성 가능
mic.i // 접근 허용
mic.icFunc() // 접근 허용
}
위 코드를 외부 파일에서도 적용해보겠다.
package chap05.section5.internal
fun main(){
val otheric = InternalClass()
println(otheric.i)
otheric.icFunc()
}
잘 접근이 되는것을 확인할 수 있다.
위에것들을 종합해서 자동차와 도둑의 예시로 코드를 정리했는데, 한번 실습해보자.
package chap05.section5.burglar
open class Car protected constructor(_year: Int, _model: String, _power: String, _wheel: String){
private var year: Int = _year
public var model: String = _model // public은 기본값이므로 생략 가능
protected open var power: String = _power
internal var wheel: String = _wheel
protected fun start(key: Boolean){
if(key)
println("Start the Engine!")
}
class Driver(_name: String, _license: String){
private var name: String = _name
var license: String = _license // public
internal fun driving() = println("[Driver] Driving() - $name")
}
}
class Tico(_year: Int, _model: String, _power: String, _wheel: String, var name: String, private var key: Boolean)
: Car(_year, _model, _power, _wheel){
override var power: String = "50hp"
val driver = Driver(name, "first class")
constructor(_name: String, _key: Boolean) : this(2014, "basic", "100hp", "normal", _name, _key){
name = _name
key = _key
}
fun access(password: String){
if(password == "gotico"){
println("----[Tico] access()----------")
// super.year // private는 접근 불가
println("super.model = ${super.model}") // public
println("super.power = ${super.power}") // protected
println("super.wheel = ${super.wheel}") // internal
super.start(key) // protected
// driver.name // private는 접근 불가
println("Driver().license = ${driver.license}") // public
driver.driving() // internal
}
else{
println("You're a burglar")
}
}
}
class Burglar() {
fun steal(anycar: Any){
if(anycar is Tico){ // 인자가 Tico의 객체일 때
println("----[Burglar] steal()---------")
// println(anycar.power) // protected 접근 불가
// println(anycar.year) // private는 접근 불가
println("anycar.name = ${anycar.name}") // public wjqrms
println("anycar.wheel = ${anycar.wheel}") // internal이 같은 모듈안에 있으므로 접근
println("anycar.model = ${anycar.model}") // public 접근
println(anycar.driver.license) // public 접근
anycar.driver.driving() // internal이 같은 모듈안에 있으므로 접근
// println(Car.start()) // protected 접근 불가
anycar.access("dontknow")
}
else{
println("Nothing to steal")
}
}
}
fun main(){
// val car = Car() // protected 생성 불가
val tico = Tico("kildong", true)
tico.access("gotico")
val burglar = Burglar()
burglar.steal(tico)
}
클래스와 클래스의 관계
본 책에서는 클래스 간의 관계를 총 4가지로 정의했다.
연관, 의존, 집합, 구성 각각의 관계를 이해해보자.
연관 관계
2개의 서로 분리된 클래스가 단방향 혹은 양방향으로 연결되는것을 말한다.
package chap05.section6.association
class Patient(val name: String){
fun doctorList(d: Doctor){ // 인자로 참조
println("Patient: $name, Doctor: ${d.name}")
}
}
class Doctor(val name: String){
fun patientList(p: Patient){ // 인자로 참조
println("Doctor: $name, Patient: ${p.name}")
}
}
fun main(){
val doc1 = Doctor("Kimsabu") // 객체가 따로 생성됨
val patient1 = Patient("Kildong")
doc1.patientList(patient1)
patient1.doctorList(doc1)
}
의존 관계
한 클래스가 다른 클래스에 의존되어 있어 영향을 주는 경우
package chap05.section6.dependency
class Patient(val name: String, var id: Int){
fun doctorList(d: Doctor){ // 인자로 참조
println("Patient: $name, Doctor: ${d.name}")
}
}
class Doctor(val name: String, val p: Patient){
val customerId: Int = p.id
fun patientList(){
println("Doctor: $name, Patient: ${p.name}")
println("Patient Id: $customerId")
}
}
fun main(){
val patient1 = Patient("Kildong", 1234)
val doc1 = Doctor("Kimsabu", patient1)
doc1.patientList()
}
main함수에서 봤을때, Patient의 객체의 변수를 Doctor의 객체를 생성할때 사용했다.
집합관계
연관 관계와 거의 동일하지만 특정 객체를 소유한다는 개념이 추가 된 것이다.
package chap05.section6
// 여러 마리의 오리를 위한 List 매개변수
class Pond(_name: String, _members: MutableList<Duck>){
val name: String = _name
val members: MutableList<Duck> = _members
constructor(_name: String) : this(_name, mutableListOf<Duck>())
}
class Duck(val name: String)
fun main() {
// 두 개체는 서로 생명주기에 영향을 주지 않음
val pond = Pond("myFavorite")
val duck1 = Duck("Duck1")
val duck2 = Duck("Duck2")
// 연못에 오리를 추가 - 연못에 오리가 집합
pond.members.add(duck1)
pond.members.add(duck2)
// 연못에 있는 오리들
for (duck in pond.members){
println(duck.name)
}
}
구성 관계
집합 관계와 거의 동일하지만 특정 클래스가 어느 한 클래스의 부분이 되는것이다.
package chap05.section6.composition
class Car(val name: String, val power: String){
private var engine = Engine(power) // Engine 클래스 객체는 Car에 의존적
fun startEngine() = engine.start()
fun stopEngine() = engine.stop()
}
class Engine(power: String){
fun start() = println("Engine has been started.")
fun stop() = println("Engine has been stopped.")
}
fun main() {
val car = Car("tico", "100hp")
car.startEngine()
car.stopEngine()
}
7주차 - 다양한 클래스와 인터페이스 (0) | 2024.01.09 |
---|---|
6주차 - 프로퍼티와 초기화 (3) | 2024.01.09 |
4주차 - 프로그램의 흐름 제어 (0) | 2024.01.04 |
3주차 - 함수와 함수형 프로그래밍 (0) | 2024.01.02 |
2일차 - 패키지, 변수와 자료형, 연산자 (1) | 2023.12.26 |
댓글 영역