[Kotlin] 코틀린 객체지향 프로그래밍 기초
ref : Udemy [Android 12 및 Kotlin 개발 완전 정복] 섹션 4
OOP(객체지향 프로그래밍)
객체지향 언어로는 자바, c#, 코틀린 등등.. 이 있다.
OOP의 특징으로
- 변수
- 흐름제어
- 함수
- 컬렉션
- 클래스
가 있겠다
클래스와 객체 개념
차가 클래스라고 한다면
특정 속성이 있다고 해보자
속성의 예시로는 최고 속도, 바퀴 개수, 창문과 문의 개수, 색깔 등이 있을 수 있다.
이런 속성들은 변수라고 한다.
또한 차를 skill 도 가지고 있다.
drive, break 등 이 있을 것이다.
이런걸 메서드 라고 한다.
클래스
fun main(){
var dawon = Person("Dawon", "Seo")
var sangwon = Person()
var sangwonSeo = Person(lastName = "Seo")
}
class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
init {
println("initialized a new person object with" +
"firstName = $firstName and lastName = $lastName")
}
}
constructor는 보조 생성자 이다.
보조 생성자는 객체 생성 시 값을 추가하게 해준다.
private constructor 나 public constructor 이런식으로 정할 수도 있다.
하지만 보조생성자는 구체적으로 정하고 싶지 않다면 필수는 아니다(지워도 된다).
위는 Person 이라는 클래스를 만들어서 나만의 고유 타입을 만든 셈이다.
클래스를 생성하면 init을 작성할 수 있는데, 이를 이니셜라이저라고 한다.
init { } 안에 있는 코드는 자동으로 실행된다. 객체를 시작하기 위해 사용하는 거라고 볼 수 있다.
객체를 준비하는 과정인 것이다. 예시 코드에서는 성과 이름을 출력하도록 했다.
기본값을 설정하는 것이 가능하다.
firstName 과 lastName 으 디폴트 값을 설정해주었는데
이에 따라서 객체를 만들때 값을 설정해주기도, 기본값 그대로 만들기도 한다.
스코프
fun main(){
myFunction(5)
}
//여기 a 는 매개변수
fun myFunction(a:Int){
//a는 변수
var a = 4
println("a는 $a")
}
코틀린이 메서드에서 변수와 매개변수 이름을 왜 같게 정하는 걸까?
이런걸 섀도잉이라고 한다. 이는 현재 스코프에 이름이 속해있어서 그런 것이다.
fun main(){
myFunction(5)
}
//여기 a 는 매개변수
fun myFunction(a:Int){
//a는 변수
var a = a
println("a는 $a")
}
만약에 매개변수 a 자리에 5를 인자로 받았다면
이 a를 myFunction 내부에 존재하는 a라는 변수에 넣어주고
이 변수를 출력해준다.
그러면 5의 값을 확인할 수 있다.
//이 자리에 선언하는 b 변수는 모든 함수에 적용이 가능하다.
fun main(){
myFunction(5)
var b = 3 // myFunction에 있는 b와는 다르다.
}
//여기 a 는 매개변수
fun myFunction(a:Int){
//a는 변수
var b = a
println("b 는 $b")
}
변수를 어디에 생성하느냐에 따라서 사용할 수 있는 범위가 정해져있다.
같은 이름의 변수라고 하더라도 다른 함수에 있다면 다른 변수인 것이다.
하지만 모든 함수 밖에 위치하는 변수는
모든 함수에서 적용이 가능한 변수가 될 것이다.
클래스의 멤버
멤버 변수는 다르게 말하면 속성이다.
fun main(){
var dawon = Person("Dawon", "Seo")
var sangwon = Person()
var sangwonSeo = Person(lastName = "Seo")
dawon.stateHobby()
}
class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
//멤버변수 or 속성
var age : Int? = null
var hobby : String = "watch TV"
init {
println("initialized a new person object with" +
"firstName = $firstName and lastName = $lastName")
}
//멤버 함수 or 메서드
fun stateHobby(){
println("my hobby is $hobby ")
}
}
속성으로 age와 hobby를 만들었고
stateHobby 라고 하는 메서드를 만들었다. hobby 변수의 값을 출력해준다.
fun main(){
var dawon = Person("Dawon", "Seo")
var sangwon = Person()
var sangwonSeo = Person(lastName = "Seo")
dawon.hobby = "drawing"
dawon.stateHobby()
sangwon.hobby = "play game"
sangwon.stateHobby()
}
class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
//멤버변수 or 속성
var age : Int? = null
var hobby : String = "watch TV"
init {
println("initialized a new person object with" +
"firstName = $firstName and lastName = $lastName")
}
//멤버 함수 or 메서드
fun stateHobby(){
println("my hobby is $hobby ")
}
}
객체마다의 hobby 속성값을 따로 지정해줄 수 있다.
fun main(){
var dawon = Person("Dawon", "Seo", 23)
var sangwon = Person()
var sangwonSeo = Person(lastName = "Seo")
dawon.hobby = "drawing"
dawon.age = 25
println("dawon is ${dawon.age} years old")
dawon.stateHobby()
sangwon.hobby = "play game"
sangwon.stateHobby()
}
class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
//멤버변수 or 속성
var age : Int? = null
var hobby : String = "watch TV"
init {
println("initialized a new person object with" +
"firstName = $firstName and lastName = $lastName")
}
//두번째 생성자
constructor(firstName: String, lastName: String, age:Int) : this(firstName, lastName){
this.age = age //객체 생성시 생성자에 전달된 age 값이 this 클래스의 age 값으로 지정되어야
}
//멤버 함수 or 메서드
fun stateHobby(){
println("my hobby is $hobby ")
}
}
두번째 생성자를 만들었다. 이 생성자는 age 라고 하는 추가 정보를 가지고 있게 된다.
객체를 생성할 때에 전달된 age 의 값이 this 클래스의 age 값으로 지정되어야 하기에 this.age = age 라는 코드를 작성했다.
fun main(){
var dawon = Person("Dawon", "Seo", 23)
var sangwon = Person()
var sangwonSeo = Person(lastName = "Seo")
dawon.hobby = "drawing"
dawon.age = 25
println("dawon is ${dawon.age} years old")
dawon.stateHobby()
sangwon.hobby = "play game"
sangwon.stateHobby()
}
class Person constructor(firstName : String = "Sangwon", lastName : String = "Joe"){
//멤버변수 or 속성
var age : Int? = null
var hobby : String = "watch TV"
var firstName : String? = null
init {
this.firstName = firstName //인자로 받은 firstName을 변수에 저장
println("initialized a new person object with" +
"firstName = $firstName and lastName = $lastName")
}
//두번째 생성자
constructor(firstName: String, lastName: String, age:Int) : this(firstName, lastName){
this.age = age //객체 생성시 생성자에 전달된 age 값이 this 클래스의 age 값으로 지정되어야
}
//멤버 함수 or 메서드
fun stateHobby(){
println("$firstName hobby is $hobby ")
}
}
누구의 hobby 인지 출력하기 위해 Person 클래스 내에 firstName 이라는 멤버 변수를 만들어줬다.
객체를 생성할 때에 인자로 firstName 을 받아서 this.firstName 의 값으로 지정해줬다.
여기 까지하면 메서드를 작성할 때에 firstName 변수를 사용할 수있게 될 것이다.
lateinit(늦은 초기화), getter, setter
lateinit은 속성을 선언(초기화)할 때에 당장 초기화 하지않고
나중에 init 함수에서 초기화를 하겠다는 의미이다.
주의할 부분은 초기화 되지 않은 상태로 이 객체의 속성에 접근하려고 하면 안된다는 것이다(오류난다)
이건 비어있는 데이터에 접근하는 것과 같다.
fun main(){
var myCar = Car()
println("owner is ${myCar.owner}")
println("brand is ${myCar.myBrand}")
}
class Car(){
lateinit var owner : String
val myBrand : String = "BMW"
get() {
return field.toLowerCase()
}
init {
this.owner = "Dawon"
}
}
커스텀 getter 를 만들어 보았다.
myBrand 라는 속성을 get 할때에 소문자로 리턴해주는 함수 getter 이다.
fun main(){
var myCar = Car()
println("owner is ${myCar.owner}")
println("brand is ${myCar.myBrand}")
println("maxSpeed is ${myCar.maxSpeed}")
myCar.maxSpeed = 200
println("maxSpeed is ${myCar.maxSpeed}")
}
class Car(){
lateinit var owner : String
val myBrand : String = "BMW"
get() {
return field.toLowerCase()
}
var maxSpeed : Int = 250
/*
이 코드는 자동으로 생성되기 때문에 작성할 필요 없음.
get() = field
set(value) {
field = value
}
*/
init {
this.owner = "Dawon"
}
maxSpeed 라는 속성을 새로 만들어보았다.
getter 와 setter 는 자동으로 생성이 되기 때문에 따로 커스텀할 거 아니면 별도로 작성해줄 필요는 없다.
따로 작성하지 않아도 속성에 접근하고, 값을 새로 set 해주는 과정이 모두 가능하다.
import java.lang.IllegalArgumentException
fun main(){
var myCar = Car()
println("owner is ${myCar.owner}")
println("brand is ${myCar.myBrand}")
println("maxSpeed is ${myCar.maxSpeed}")
myCar.maxSpeed = -10
println("maxSpeed is ${myCar.maxSpeed}")
}
class Car(){
lateinit var owner : String
val myBrand : String = "BMW"
get() {
return field.toLowerCase()
}
var maxSpeed : Int = 250
/*
이 코드는 자동으로 생성되기 때문에 작성할 필요 없음.
get() = field
set(value) {
field = value
}
*/
set(value) {
field = if(value > 0) value else throw IllegalArgumentException("MaxSpeed cannot be less than 0")
}
init {
this.owner = "Dawon"
}
}
setter 를 커스텀해준 모습이다. 만약에 value가 0보다 크지 않을 경우 오류가 나도록 커스텀해줬다.
class Car(){
lateinit var owner : String
val myBrand : String = "BMW"
get() {
return field.toLowerCase()
}
var maxSpeed : Int = 250
/*
이 코드는 자동으로 생성되기 때문에 작성할 필요 없음.
get() = field
set(value) {
field = value
}
*/
set(value) {
field = if(value > 0) value else throw IllegalArgumentException("MaxSpeed cannot be less than 0")
}
var myModel : String = "m5"
private set //비공개 setter, car 클래스 내에서만 사용 가능.
//getter 는 private 이 아니기 때문에 main 함수에서 사용 가능
init {
this.owner = "Dawon"
}
}
예시 코드에서 private setter 는 다른 클래스에서는 사용이 불가능하다.
(getter 는 private 으로 선언한거 아니니 상관없이 가능하다)
private 으로 선언한 것은 해당 클래스 내에서만 사용 가능.
따라서 init 함수 안에서는 사용이 가능하다.
data class
data class 에 대해서 알아보자
data class User(val id:Long, var name:String) //매개변수는 val 이나 var 이어야
fun main(){
val user1 = User(1, "Dawon")
val name = user1.name
println(name)
user1.name = "Sangwon"
var user2 = User(1, "Sangwon")
println(user1 == user2) //true 라고 출력됨
println("User Details : $user1")
val updatedUser = user1.copy(name = "Dawon Seo")
println(user1)
println(updatedUser)
println(updatedUser.component1()) //print 1
println(updatedUser.component2()) //print Dawon Seo
}
User 라는 데이터 클래스를 만든다. 매개변수는 id 와 name 이 있다.
main 함수에서 id가 1이고 이름이 "Dawon" 인 객체를 만들어서 user1 에 넣었다.
객체 뒤에 . 을 적고 속성이름을 적어서 속성에 접근이 가능하다.
copy() 를 이용해서 객체의 복사본을 만들 수도 있다.
component를 통해 속성에 접근할 수도 있다.
(현재 위 예시코드에서는 component1 이 id를 의미하고, component2가 name 을 의미한다)
val (id, name) = updatedUser // 두 변수를 updated User 에 좌우되게 하도록 함
println("id=$id, name=$name")
id와 name 이라는 변수를 updatedUser 에 좌우되도록 지정할 수도 있다.
이러면 updatedUser 가 가지고있는 속성 id와 name 의 값이
변수 id와 name 에 들어갈 것이다.
상속
부모 클래스와 자식 클래스가 있다.
//super class, parent class, base class
open class Vehicle{ // 상속을 위해서는 open 으로 선언되어야 한다.
// 상속을 원하지 않으면 open 말고 sealed 를 넣으면 됨.
//속성
//메서드
}
//sub class, child class
open class Car : Vehicle(){
}
class ElectricCar: Car(){
}
예를 들어 Vehicle 이라는 부모클래스가 있고 그 안에 속성과 메서드들이 있다
이 Vehicle 클래스를 상속받는 Car 이라는 자식 클래스가 있고
ElectricCar 이라고 하는 자식클래스는 또 Car 이라고 하는 클래스를 상속받는 모습이다.
open class Car(val name:String, val brand:String) {
open var range : Double = 0.0 //자식클래스에서 오버라이드할거면 open으로 선언해야
fun extendRange(amount : Double){
if(amount > 0)
range += amount
}
open fun drive(distance : Double){ //override 할거면 open으로 선언해야
println("drove for $distance km")
}
}
//sub class, child class
class ElectricCar(name:String, brand:String, batteryLife:Double): Car(name, brand){
override var range = batteryLife * 6 //속성을 override
var chargerType = "type1"
override fun drive(distance: Double) { //부모클래스에서 override 한 함수
println("drove for $distance km")
}
fun drive(){ //상속에서 생성한 drive 함수
println("drove for $range km on electricity")
}
}
fun main(){
var myCar = Car("A3", "Audi")
var myECar = ElectricCar("s-model", "Tesla", 85.0)
myCar.drive(distance = 200.0)
myECar.drive(distance = 200.0) // 전기차에는 drive 함수가 없지만 부모클래스의 특성을 상속 받아서 가능
myECar.drive()
myECar.chargerType = "type2"
}
자식 클래스에서 부모클래스의 속성과 함수를 override 할 수 있다.
대신 override 하려면 상속해주는 부분(부모클래스 부분)에 open으로 선언되어 있어야 한다.
interface
interface Drivable{
val maxSpeed : Double
fun drive():String
fun brake(){
println("the drivable is braking")
}
}
interface 안에는 함수랑 속성이 있다.
만약 클래스가 이 interface 의 기능을 확장한다면 interface 에 만들어진 속성을 입력해야한다.
그리고 그 변수 오버라이딩을 위해서 override 키워드를 추가해야한다.
package org.techtown.kotlinbasics
import java.lang.IllegalArgumentException
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 //자식클래스에서 오버라이드할거면 open으로 선언해야
fun extendRange(amount : Double){
if(amount > 0)
range += amount
}
override fun drive() :String{ //이 drive 는 Drivable interface 에서 온 것
return "driving the interface drive"
}
open fun drive(distance : Double){ //override 할거면 open으로 선언해야
println("drove for $distance km")
}
}
//sub class, child class
class ElectricCar(maxSpeed: Double, name:String, brand:String, batteryLife:Double): Car(maxSpeed, name, brand){
override var range = batteryLife * 6 //속성을 override
var chargerType = "type1"
override fun drive(distance: Double) { //부모클래스에서 override 한 함수
println("drove for $distance km")
}
override fun drive(): String {
return "drove for $range km on electricity"
}
override fun brake() {
super.brake()
}
}
fun main(){
var myCar = Car(200.0,"A3", "Audi")
var myECar = ElectricCar(240.0,"s-model", "Tesla", 85.0)
myCar.drive(distance = 200.0)
myECar.drive(distance = 200.0) // 전기차에는 drive 함수가 없지만 부모클래스의 특성을 상속 받아서 가능
myECar.drive()
myECar.chargerType = "type2"
myECar.brake()
myCar.brake()
}
Car 과 ElectricCar 클래스에 Drivable 인터페이스를 적용한 모습이다.
인터페이스에 있던 함수를 override 하고 있다.
왜 인터페이스를 사용해야할까?
나중에 구현하고 싶은 특정 함수와 클래스 속성이 있다면 유용하다(바로 구현하고 싶지 않은 경우)
추상 클래스
추상 클래스는 인터페이스와 비슷한 점이 있다.
객체를 만들 수 없다는 점이다.
하지만 추상 클래스로부터 서브 클래스를 상속 받을 수 있다.
추상 클래스에서 property와 method 같은 멤버는 abstract 키워드를 사용하지 않는 한 추상적인 것이 아니다.
package eu.tutorials.interfacestest
// An abstract class cannot be instantiated
// (you cannot create objects of an abstract class).
// However, you can inherit subclasses from can them.
// The members (properties and methods) of an abstract class are non-abstract
// unless you explicitly use the abstract keyword to make them abstract.
abstract class Mammal(val name: String, val origin: String,
val weight: Double) { // Concrete (Non Abstract) Properties
// Abstract Property (Must be overridden by Subclasses)
abstract var maxSpeed: Double
// Abstract Methods (Must be implemented by Subclasses)
abstract fun run()
abstract fun breath()
// Concrete (Non Abstract) Method
fun displayDetails() {
println("Name: $name, Origin: $origin, Weight: $weight, " +
"Max Speed: $maxSpeed")
}
}
class Human(name: String, origin: String, weight: Double,
override var maxSpeed: Double): Mammal(name, origin, weight) {
override fun run() {
// Code to run
println("Runs on two legs")
}
override fun breath() {
// Code to breath
println("Breath through mouth or nose")
}
}
class Elephant(name: String, origin: String, weight: Double,
override var maxSpeed: Double): Mammal(name, origin, weight) {
override fun run() {
// Code to run
println("Runs on four legs")
}
override fun breath() {
// Code to breath
println("Breath through the trunk")
}
}
fun main() {
val human = Human("Denis", "Russia",
70.0, 28.0)
val elephant = Elephant("Rosy", "India",
5400.0, 25.0)
human.run()
elephant.run()
human.breath()
elephant.breath()
}
추상 클래스가 있다.(이 예시 코드에서는 Mammal 이다)
추상클래스에는 property와 method가 abstract 로 선언되어 있는데
이것은 꼭 서브클래스에서 overriding 해야한다.
추상클래스에서는 body 가 구현되어있지 않아도 된다.
추상클래스와 인터페이스. 인터페이스는 대략적인 설계 명세를 구현하고
인터페이스를 상속하는 하위 클래스에서 이를 구체화 하는 거는 동일하다.
그럼 추상클래스와 인터페이스의 차이점은 뭘까?
인터페이스에서는 property의 상태정보를 저장할 수 없다
다시 말하면 인터페이스에서는 property의 초기화가 불가능하다는것이다.
그리고 인터페이스는 다중 상속이 가능한데 추상클래스는 불가능하다
정리하자면..
- 추상클래스 : 형식만 선언하고 실제 구현은 서브클래스에 일임할 때
- 인터페이스 : 서로 다른 기능들을 여러개 물려주어야 할때
형 변환(타입 캐스팅)
리스트를 만들려면 ListOf 를 사용하면 된다.
리스트에 들어갈 요소들의 타입들을 지정해줄 수도 있는데, Any로 지정하면 여러 타입들의 요소를 넣을 수 있다.
val stringList: List<String> = listOf("Denis", "Frank", "Michael", "Garry")
val mixedTypeList: List<Any> = listOf("Denis", 31, 5, "BDay", 70.5, "weighs", "Kg")
for(value in mixedTypeList){
if (value is Int){
println("Integer: '$value'")
}else if(value is Double){
println("Double: '$value' with Floor value ${floor(value)}")
}else if(value is String){
println("String: '$value' of length ${
value.length} ")
}else{
println("Unknown Type")
}
}
그리고 요소들의 타입에 따라 조건을 달고 실행할 코드를 작성했다.
when 표현식으로 나타낼 수도 있겠다.
val obj3: Any = 1337
val str3: String? = obj3 as? String //works
println(str3) //prints null
위 코드처럼 키워드 as? 를 사용해서 explicit safe 캐스팅을 할 수 있다.