이 글은 이것이 안드로이드다 with 코틀린(개정판)를 참고하여 작성하였습니다.
작성자 : 김태경
개발환경은 Windows, Android Studio입니다.
Kotlin 객체 지향 프로그래밍 기초
OOP의 5가지 기본 개념
- 변수와 타입
- 저장 공간에서 위치
- 저장 공간을 가리키기 위해 각 변수는 고유의 이름(identifier)을 부여받음.
- 흐름 제어
- 조건이 맞는 경우에만 코드를 실행
- 조건이 맞는 한 코드를 반복 실행
- 함수
- 코드를 분리
- 필요시 코드 블록을 실행
- 컬렉션
- 많은 요소를 한 군데에 저장
- 흐름 제어의 도움을 받아 여러 요소를 반복 실행
- (상속을 포함한)클래스와 객체
- 직접 데이터 타입을 생성
- 데이터 멤버와 메서드를 한 곳 생성
- 알아보기 쉽고 유지 가능한 코드를 쓸 수 있게 함.
- 팀으로 작업할 때 유용
클래스와 초기화
class Person constructor(firstName: String, lastName: String)
constructor 키워드를 사용하여 주 생성자를 선언한다. 객체 생성 시 값을 추가할 수 있게 해준다. 이때 constructor 키워드는 생략할 수 있다. {} 안에는 매개변수가 들어가게 된다.
fun main(){
var denis = Person("Denis","Panjuta")
}
다음과 같이 객체를 만들 수 있다.
class Person constructor(firstName: String, lastName: String){
init {
println("Person created")
}
}
클래스를 생성하면, init이라는 걸 사용할 수 있다. init은 객체가 생성되는 순간 호출된다. 즉, 객체를 준비하는 과정이다.
class Person constructor(firstName: String = "John", lastName: String = "Doe"){
init {
println("Person created")
}
}
다음과 같이 기본값을 정하는 것도 가능하다. var john = Person()이라는 새로운 객체를 만들면, {} 안에 인자가 없어도. 객체의 firstName에는 “John”, lastName에는 “Doe”라는 값으로 초기화가 된다.
lateinit
class Car(){
lateinit var owner : String
}
멤버 변수를 만들면 항상 초기화를 해야 한다. lateinit 코드를 사용하게 되면 나중에 초기화를 할 수 있게 해준다. 만약 값을 초기화하지 않고 호출한다면 ‘Uninitialized Property Access Exception’라는 에러 문구가 출력이된다. 따라서, 다음과 같이 init 안에서 초기화를 한다면 해결이 된다.
class Car(){
lateinit var owner : String
init {
this.owner = "Frank"
}
}
Custom Getter & Setter
Kotlin에서 Getter와 Setter는 자동으로 생성되어 사용할 수 있다. 그런데 Custom Getter 와 Setter를 이용하면 사용자가 원하는 값으로 Get과 Set을 할 수 있다.
class Car(){
val myBrand: String = "BMW"
get() {
return field.toLowerCase()
}
}
Custom Getter 예시다. get() 메서드 안에서 field를 통해 프로퍼티를 참조하여 “BMW”를 소문자형인 “bmw”로 Get을 하게 된다.
class Car(){
var max Speed: Int = 250
set(value){
field = if(value > 0 ) value else throw IllegalArgumentException("Maxspeed cannot be less than 0")
}
}
Custom Setter 예시다. Set 하려는 값이 만약 0보다 작으면 Set을 하지 않고 “IllegalArgumentException”이라는 에러 문구를 출력하게 된다.
private set
class Car(){
var myModel : String = "M5"
private set
}
private set 키워드를 사용하면 클래스 내부에서는 Set을 할 수 있지만, 클래스 외부에서는 Set을 할 수 없다.
fun main(){
var myCar = Car()
myCar.myModel = "M3"
}
class Car(){
var myModel : String = "M5"
private set
}
클래스 외부에서 Set을 시도하는 예시다. main() 함수는 클래스 외부에 해당하므로 “M3”로 Set이 되지 않고 에러가 발생하게 된다.
class Car(){
var myModel : String = "M5"
private set
init {
this.myModel = "M3"
}
}
클래스 내부에서 Set을 시도하는 예시다. init은 클래스 내부에 해당하기 때문에 에러가 발생하지 않고 정상적으로 “M3”로 Set을 하게 된다.
보조 생성자
class Person(var firstName: String, var lastName: String) {
var age: Int? = null
var hobby: String = "Watch Netflix"
var myFirstName = firstName
// 보조 생성자
constructor(firstName: String, lastName: String, age: Int): this(firstName, lastName) {
this.age = if(age > 0) age else throw IllegalArgumentException("Age must be greater than zero")
}
fun stateHobby(){
println("$firstname \\'s Hobby is: $hobby'" )
}
}
보조 생성자는 클래스의 바디 부분에서 constructor 키워드를 통해 선언한다. 위와 같이 주 생성자와 보조 생성자를 모두 선언했을 경우, 반드시 생성자끼리 연결해주어야 한다.
constructor(firstName: String, lastName: String, age: Int): this(firstName, lastName)
위와 같이, 주 생성자에 선언이 되었던 것처럼 타입까지 표시하고, this 뒤에 오는 () 안에 연결시킬 변수를 넣으면 된다.
데이터 클래스
data class User(val id: Long, var name: String)
fun main(){
val user1 = User(1, "Denis")
val name = user1.name
println(name)
user1.name = "Michael"
val user2 = User(1, "Michael")
println(user1.equals(user2))
}
class 앞에 data를 적으면 데이터 클래스가 만들어진다. 데이터 클래스는 기본적으로 getter와 setter를 까지 생성해주기 때문에 val user1 = User(1, "Denis")이 실행이 되면 User1이라는 객체의 id와 name이 각각 1과 “Denis”로 초기화가 된다. 또한 캐코니컬 메소드까지 알아서 생성해주기 때문에 eqauls() 메서드도 사용할 수 있다.
- println(user1.equals(user2)) → 출력 : 1
val updateUser = user1.copy()
단순히 바꾸고 싶은 변수를 빼고 내용을 복사해 넣고 싶을 때 다음과 같은 copy() 메서드를 사용할 수 있다.
val updateUser = user1.copy(name="Denis Panjuta")
복사는 하고 싶은데, 어떤 변수의 값을 다르게 초기화하고 싶을 때 다음과 같이 copy 뒤에 오는 () 안에 ‘다르게 초기화하고 싶은 변수명 = 원하는 값’을 넣으면 된다.
println(updatedUser.component1())
println(updatedUser.component2())
componentN()메서드는 N번째 원소를 리턴을 해준다.
- println(updatedUser.component1()) → 출력 : 1
- println(updatedUser.component2()) → 출력 : "Denis Panjuta"
무한하게 componentN을 선언할 수는 없고 맨 앞의 다섯 개 원소에 대해서만 componentN을 선언할 수 있다.
val (id, name) = updateUser
println("id=$id, name=$name") // 출력 : "id=1, name=Denis Panjuta"
updateUser의 id와 name 변수를 각각 따로 초기화하고 싶을 때 다음과 같이 구조 분해를 사용할 수 있다.
상속
상속은 프로퍼티나 메서드 같은 특성을 다른 클래스에서 사용할 수 있게 해준다.
구조는 다음과 같다.
- 슈퍼 클래스(부모 클래스, 베이스 클래스라고도 불린다.)
- 서브 클래스(자식 클래스, 파생 클래스라고도 불린다.)
open class Car{
// proper
// methods
}
class ElectricCar : Car(){
}
코들린의 모든 클래스는 자동으로 최종값이기 때문에 즉, 자동으로 상속할 수 없기 때문에 open 키워드를 사용해야 한다. 상속을 원하지 않는 경우, open 대신 sealed 키워드를 사용해야 한다. 그리고 class ElectricCar : Car()처럼 ‘서브 클래스명’ 뒤에 콜론(:)을 쓰고 ‘슈퍼 클래스명()’을 적어줘야 서브 클래스가 슈퍼 클래스로부터 상속받을 수 있다.
open class Car(val name: String, val brand: String){
// proper
// methods
}
class ElectricCar(name: String, brand: String, batteryLife: Double) : Car(name, brand){
}
- 슈퍼클래스의 매개변수 → 서브클래스의 매개변수에도 넘겨줘야 한다.
open class Car(val name: String, val brand: String){
var range: Double = 0.0
fun extendRange(amount: Double){
if(amount > 0)
range += amount
}
fun drive(distance: Double){
println("Drove for $distance KM")
}
}
class ElectricCar(name: String, brand: String, batteryLife: Double)
: Car(name, brand){
}
fun main(){
var myCar = Car("A3", "Audi")
var myEcar = ElectricCar("S-Model", "Tesla", 85.0)
myCar.drive(200.0) // 출력 : Drove for 200.0 KM
myEcar.drive(200.0) // 출력 : Drove for 200.0 KM
}
- 서브클래스에 메서드가 선언되지 않았더라도 슈퍼클래스의 메서드가 있으면 위와 같이 사용할 수 있다.
open class Car(val name: String, val brand: String){
open var range: Double = 0.0
fun extendRange(amount: Double){
if(amount > 0)
range += amount
}
open fun drive(distance: Double){
println("Drove for $distance KM")
}
}
class ElectricCar(name: String, brand: String, batteryLife: Double)
: Car(name, brand){
override var range = batteryLife * 6 // range 변수 오버라이딩
override fun drive(distance: Double){ // drive() 메서드 오버라이딩
println("Drove for $distance KM on electricity")
}
}
fun main(){
var myCar = Car("A3", "Audi")
var myEcar = ElectricCar("S-Model", "Tesla", 85.0)
myCar.drive(200.0) // 출력 : Drove for 200.0 KM
myEcar.drive(200.0) // 출력 : Drove for 200.0 KM on electricity
}
- 위와 같이 슈퍼클래스에 정의된 함수를 다시 정의하는 것을 오버라이딩이라고 한다. 오버라이딩을 하려면 재정의하고자 하는 함수 앞에 override 키워드를 붙인다.
인터페이스
인터페이스는 기본적으로 클래스가 서명할 수도 있는 계약서다. 서명하면, 클래스는 인터페이스의 프로퍼티와 함수 구현을 제공할 의무를 가지게 된다.
인터페이스는 클래스 기능을 확장하게 해준다. 단, 어떤 멤버도 가질 수 없다.
interface Drivable{
val maxSpeed: Double
fun drive(): String
fun brake(){
println("The drivable is braking")
}
}
open class Car(override val maxSpeed: Double, val name: String, val brand: String): Drivable{
open var range: Double = 0.0 // Drivable 인터페이스 프로퍼티 구현
fun extendRange(amount: Double){
if(amount > 0)
range += amount
}
override fun drive(): String {
return "driving the interface drive"
} // Drivable 인터페이스 함수 구현
open fun drive(distance: Double){
println("Drove for $distance KM")
}
}
- Car 클래스가 Drivable 인터페이스를 확장하기 때문에 그것의 변수와 함수를 구현해야 한다.
→ 프로퍼티는 주 생성자에서 구현했고, 함수는 바디에서 구현했다.
class ElectricCar(maxSpeed: Double, name: String, brand: String, batteryLife: Double)
: Car(maxSpeed, name, brand) {
override var range = batteryLife * 6
override fun drive(distance: Double) {
println("Drove for $distance KM on electricity")
}
override fun brake() {
super.brake()
}
}
- 인터페이스를 구현하는 클래스의 서브 클래스도 똑같이 인터페이스를 구현하게 된다. 인터페이스에 이미 구현된 함수 또한 오버라이딩할 수 있다. 그리고 super 키워드가 brake 함수를 인터페이스로부터 호출할 수 있게 해준다.
teslaS.brake() // 출력 : The drivable is braking
audiA3.brake() // 출력 : The drivable is braking
- Car, ElectricCar 클래스 모두 brake 함수를 구현한 적이 없지만 brake에서 자동으로 상속받기 때문에 위와 같이 출력이 된다.
인터페이스를 왜 사용해야 하는가
나중에 구현하고 싶은 특정 함수와 클래스 프로퍼티를 바로 구현하고 싶지 않을 때 유용하다. 또 구체적인 함수 바디를 아직 만들고 싶지 않을 때도 유용하다.
추상 클래스
- 추상 클래스의 객체는 만들 수 없다.
- 추상 클래스를 상속받은 클래스의 객체는 만들 수 있다.
abstract class Mammal(val name: String, val origin: String,
val weight: Double) { // 콘크리트 프로퍼티
// 추상 프로퍼티
abstract var maxSpeed: Double
// 추상 메서드
abstract fun run()
abstract fun breath()
// 콘크리트 메서드
fun displayDetails() {
println("Name: $name, Origin: $origin, Weight: $weight, " +
"Max Speed: $maxSpeed")
}
}
- 추상 프로퍼티, 메서드를 만들기 위해서 abstract 키워드를 사용한다.
class Human(name: String, origin: String, weight: Double,
override var maxSpeed: Double): Mammal(name, origin, weight) {
override fun run() {
println("Runs on two legs")
} // 추상 메서드 abstract fun run() 오버라이딩
override fun breath() {
println("Breath through mouth or nose")
} // 추상 메서드 abstract fun breath() 오버라이딩
}
class Elephant(name: String, origin: String, weight: Double,
override var maxSpeed: Double): Mammal(name, origin, weight) {
override fun run() {
println("Runs on four legs")
} // 추상 메서드 abstract fun run() 오버라이딩
override fun breath() {
println("Breath through the trunk")
} // 추상 메서드 abstract fun breath() 오버라이딩
}
- 추상 프로퍼티, 메서드는 서브 클래스가 꼭 오버라이딩 해야한다.
추상 클래스와 인터페이스의 차이점
- 추상 클래스는 단일 상속, 인터페이스는 다중 상속
- 추상 클래스는 필드와 생성자를 추가할 수 있으나, 인터페이스는 필드와 이들을 추가할 수 없다.
형 변환
스마트 캐스트
val obj1: Any = "I have a dream"
if(obj1 !is String) {
println("Not a String")
} else {
println("Found a String of length ${obj1.length}")
}
- 타입은 Any로 되어있지만 "I have a dream"이 String이기 때문에 else문 블록의 구문을 출력한다.
unsafe 캐스팅
val str1: String = obj1 as String
println(str1.length)
- obj1이 "I have a dream"이기 때문에 정상적으로 String으로 캐스팅이 되고 길이(14)가 출력이 된다. 값이 null일 경우 에러가 발생할 수 있기 때문에 unsafe 캐스팅이다.
safe 캐스팅
val obj3: Any = 1337
val str3: String? = obj3 as? String
println(str3)
- str3가 nullable이기 때문에 obj3가 String으로 캐스팅이 정상적으로 될 수 없을 때 null로 될 수 있도록 하여 오류를 방지할 수 있어 safe 캐스팅이다.
'GDSC HUFS 4기 > Kotlin Team #6' 카테고리의 다른 글
[6팀] 계산기 - XML 사용법과 UI 생성법 배우기 (0) | 2022.10.31 |
---|---|
[6팀] 코틀린으로 분 단위 계산기 만들기 (1) | 2022.10.31 |
[6팀] 코틀린 기초 더 배우기 (0) | 2022.10.09 |
[6팀] 코틀린 기초(2) (0) | 2022.10.03 |
[6팀] 코틀린 기초(1) (0) | 2022.10.02 |