본문 바로가기

개발 노트/Kotlin

[kotlin] 문법 4주차 정리 - 객체지향 프로그래밍 심화

# 접근 제한자

변수나 메소드의 접근을 제한해요!!

 

- public, private, internal, protected로 접근을 제한 한다 

- 접근 권한을 통해 데이터의 무분별한 접근 막을 수 있다

- 클래스들간에 접근하면 안되는 상황을 구분하기 때문에, 향후에 유지보수하기 용이하다

 

 

- 용어정리

  • 프로젝트 : 최상단 개념 / 모듈,패키지,클래스를 포함

 

 

  • 모듈 : 프로젝트 아래의 개념 / 패키지,클래스를 포함

 

 

  • 패키지 : 모듈 아래의 개념 / 클래스를 포함

 

 

 

- 접근제한자의 종류

  • public : 명시하지 않으면 기본적으로 public (어디서나 접근 가능)
  • private : 동일한 클래스 내부에서만 접근
  • internal : 같은 모듈 내부에서만 접근
  • protected : 기본적으로 private이지만, 상속을 받은경우엔 타 모듈에서 접근 가능 

 

 

- 예시코드 

- public var처럼 변수앞에 접근제한자를 붙여서 사용가능하다

class AccessTestClass {
    public var a:Int = 1
    var b = 2
    private var c = 3
    internal var d = 4
    protected var e = 5
    
    public fun publicTest() {
        println("public 입니다")
    }

    fun publicTest2() {
        println("public 입니다")
    }

    private fun privateTest() {
        println("private 입니다")
    }

    internal fun internalTest() {
        println("internal 입니다")
    }

    protected fun protectedTest() {
        println("protected 입니다")
    }
}

 

-> public, internal만 외부에서 접근 가능하다

-> a, b,d 만 외부에서 사용할 수 있다

-> 나머지는 다 내부에서만 사용가능하다

 

 

class AccessTestChildClass: AccessTestClass() {

    fun protectedTest1() {
        println("e의 값은 ${e}")
    }
}

 

-> 이렇게하면 AccessTestClass를 상속받았으니깐, protected도 외부에서 접근이 가능하다

-> e를 사용할 수 있다

 

fun main() {
    var accessTestClass = AccessTestClass()
    var accessTestChildClass = AccessTestChildClass()

// . 하고 접근가능한 요소를 확인
//    accessTestClass. 
    accessTestChildClass.protectedTest1()
}

//결과
e의 값은 5

 

-> 이렇게 작성하고 실행해보자

 

 

 

# 예외처리의 활용 

프로그램 실행도중에 발생하는 예외를 적절하게 처리해요!!

 

- 프로그램을 실행하기 전에 알수있는 컴파일 에러는 "오류"라고 하고,

- 프로그램을 실행하는 도중에 발생하는 런타임 에러는 "예외"라고 한다

- 실행도중에 예외가 발생하면 프로그램이 비정상적으로 종료되는 문제가 발생한다

- 실행도중 프로그램이 종료되는건 심각한 문제이기 때문에, 미리 예외를 생각하고 코드를 작성해야한다

- 예외처리 방법에는 try-catchthrow가 있다

  • try-catch의 구조
fun method1() {
		try {
			예외가 발생할 가능성이 존재하는 코드
		} catch(예외종류) {
			예외가 발생했을때 처리할 코드
		}
}

 

  • throw의 구조
fun method1(num1:Int) {
		if(num1 > 10) {
			throw 예외종류
		}
}

 

 

 

- 예시코드

-> 숫자를 입력해야하는데 실수로 문자를 입력했을때의 예외처리하기!

  • 예외 시 발생한 오류화면

-> NumberFormatException이라는 오류가 발생하면서 프로그램 종료

 

  • 예외처리 코드 (try - catch)
	while(true) {
        try {
            var num1 = readLine()!!.toInt()
            println("내가 입력한 숫자는 ${num1}입니다")
            break
        } catch(e:java.lang.NumberFormatException) {
            println("숫자를 입력하세요")
        }
    }

 

-> while(true)을 사용해서 무한루프를 도는데, 입력한 숫자가 정수이면 반복문을 탈출해서 종료되고, 정수가 아닌 다른 타입이면 예외가 발생하기때문에( catch로 떨어지기 때문에 catch문을 실행하고) 다시 while문으로 돌아와서 또 숫자를 입력하게 되는것이다

-> 즉, 숫자를 제대로 입력할때까지 반복문을 도는 코드

 

  • 예외처리 코드2 (try - catch -finally)
	while(true) {
        try {
            var num1 = readLine()!!.toInt()
            println("내가 입력한 숫자는 ${num1}입니다")
            break
        } catch(e:java.lang.NumberFormatException) {
            println("숫자를 입력하세요")
        } finally {
            println("키보드와의 연결은 정상적입니다")
        }
    }

 

-> 예외 처리와 관계없이 항상 실행하는 코드finally에 작성

 

 

 

# 지연 초기화

변수나 상수의 값을 나중에 초기화해요!!

 

- 코틀린은 클래스를 설계할때 안정성을 위해 반드시 변수의 값을 초기화할 것을 권장하지만, 초기의값을 정의하기 난처할때 나중에 대입하기 위해서 필요한 문법이다 

- 지연초기화(늦은초기화)를 위해 lateinit, lazy 키워드를 활용한다

- 저사양으로 제한되어있는 환경에서는 메모리를 더욱 효율적으로 사용할 수 있다

 

더보기
Q. 왜 var 앞에 lateinit을 사용해줘야하나요?
코드를 짜다보면 변수 선언시 값 세팅을 하기에 애매해 null을 선언할때가 있습니다. 자바와 같은 언어는 변수 선언 후 초기화 되지 않으면 기본값으로 초기화 or null값을 갖게 됩니다.
코틀린에서는 변수 선언 시 초기화할 것을 권고하고 있고, null 초기화 사용을 추천하지 않습니다.
그렇다면 초기 선언 시 늦게 초기화가 필요할 때는 어떻게 할까요?
lateinit, lazy프로퍼티를 사용하시면 됩니다.

-lateinit
lateinit 사용 시 변수를 선언하고 필요 시 값을 할당하면 됩니다.
var 변수에만 사용 가능, Primitive type에서는 사용할 수 없습니다.

-lazy (lazy delegate properties)
val 변수에서 사용
람다함수 형태의 초기화 함수 선언
람다함수 안에 변수 초기화가 선언되어있어 코드 선언시 변수에 값을 초기화 하는것으로 보이지만
--> 실행시 val 변수 사용 시점에서 초기화 진행됩니다.

 

 

 

-예시 코드

 

1. 변수의 지연초기화

  • <기본사용>
fun main(){
    var s1 = Student()
    s1.name = "참새"
    s1.displayInfo()

    s1.age = 10
    s1.displayInfo()
}

class Student {
    lateinit var name:String
    var age:Int = 0

    fun displayInfo() {
        println("이름은: ${name} 입니다.")
        println("나이는: ${age} 입니다.")
    }
}

//결과
이름은: 참새 입니다.
나이는: 0 입니다.
이름은: 참새 입니다.
나이는: 10 입니다.

 

-> name변수값을 초기에 정의하기 어렵기 때문에, lateinit을 사용해서 초기화를 지연시켜준다

 

 

  • <고급사용>
fun main(){
    var s1 = Student()
    s1.name = "참새"
    s1.displayInfo()

    s1.age = 10
    s1.displayInfo()
}

class Student {
    lateinit var name:String
    var age:Int = 0

    fun displayInfo() {
		if(this::name.isInitialized) {
	        println("이름은: ${name} 입니다.")
	        println("나이는: ${age} 입니다.")
		} else {
		println("name변수를 초기화해주세요.")
				}
    }
}

//결과
이름은: 참새 입니다.
나이는: 0 입니다.
이름은: 참새 입니다.
나이는: 10 입니다.

 

-> 변수를 사용하기전에 초기화되었는지 확인해야 안정성을 높알 수 있다

-> isInitialized을 사용해서 값이 초기화되었는지 확인할 수 있다 (ture / false)

-> 사용할때는 값이아니라 참조형태로 사용해야하기 때문에 this:: 또는 ::를 붙인다

-> if(this::name.isInitialized) 을 사용해서 값이 초기화되었는지 미리 확인!

 

 

2. 상수의 지연초기화

fun main(){
    var s1 = Student()
    s1.name = "참새"
    s1.displayInfo()

    s1.age = 10
    s1.displayInfo()
}

class Student {
    lateinit var name:String
    var age:Int = 0
    val address: String by lazy {
        println("address 초기화")
        "seoul"
    }

    fun displayInfo() {
        println("이름은: ${name} 입니다.")
        println("나이는: ${age} 입니다.")
        println("주소는: ${address} 입니다.")
    }
}

//결과
이름은: 참새 입니다.
나이는: 0 입니다.
address 초기화
주소는: seoul 입니다.
이름은: 참새 입니다.
나이는: 10 입니다.
주소는: seoul 입니다.

 

-> 상수를 지연초기화할때는 lazy 키워드를 활용해서 사용한다

-> 상수를 사용하는 시점에 값을 대입하고 초기화가 수행된다

-> 여기서는 상수 10이 대입되는 시점에 lazy안에있는 값이 초기화되서 seoul이 호출된다

 

# 지연초기화 참고자료

https://velog.io/@haero_kim/Kotlin-lateinit-vs-lazy-%EC%A0%95%ED%99%95%ED%9E%88-%EC%95%84%EC%84%B8%EC%9A%94

 

 

 

# 널 세이프티

코틀린의 Null 안정성을 향상시켜줄 수 있다!!

 

- Null예외는 프로그램의 가용성을 저하시키는 치명적인 오류이다 (Null예외가 발생하면 프로그램이 꺼져버리기 때문)

- 코틀린은 Null예외로부터 안전한 설계를 위해 자료형에 Null 여부를 명시할 수 있다

- 코틀린은 ?, !!, ?., ?: 을 사용해서 Null 예외로부터 살아남으려고 한다

- 하지만 강제로 null이 아니라고하는 !! 는 최대한 사용을 지양하는 것이 좋다

 

 

- 예시코드

  • 주소를 저장하는 address변수는 null을 저장할 수 있다?을 사용해서 선언해준다
  • ?null을 저장할 수 있다고 선언 (빈값이 들어갈 수 있다고 선언) 하는 것
fun main(){
    var s = Student()
    s.name = "참새"
    s.address = "서울"
    s.displayInfo()
}

class Student {
    lateinit var name:String
    var address:String? = null
    
    fun displayInfo() {
        println("이름은: ${name} 입니다")
        println("주소는: ${address} 입니다")
    }
}

//결과
이름은: 참새 입니다
주소는: 서울 입니다

 

 

  • 메소드를 호출하고 전달받은 리턴값이 null이 아님을 !!키워드로 보증한다
  • !! 을 사용해서 null값이 아니다(빈값이 들어갈 수 없다) 라는것을 알려줌
fun main(){
//  var data = readLine()!!.toInt()

    var inputData = readLine()!!
    var data = inputData.toInt()
    println("Null아닌 값: ${data}")
}

 

-> data 변수에는 빈값을 입력할 수 없다고 !!를 사용해서 표현해줌

 

 

  • ?.키워드로 Null인지 확인하고, Null이 아닐때만 참조하는 메소드를 실행하도록 작성한다
  • 예제에서 주소를 저장하는 address는 초기값이 null이기 때문에, null 위협에 놓여있다
  • null이 아님을 보장해야하는데 강제로 !!를 사용하는건 바람직하지 않고... 그럼 어떤걸 사용해야될까?                        이때 ?. 를 사용한다 
  • ?.안전 호출연산자 (safe-calls) 라고 한다
fun main(){
    var s = Student()
    s.name = "참새"
    s.displayAddressLength()
    
    s.address = "서울"
    s.displayInfo()
}

class Student {
    lateinit var name:String
    var address:String? = null

    fun displayInfo() {
        println("이름은: ${name} 입니다")
        println("주소는: ${address} 입니다")
    }
    
    fun displayAddressLength() {
        println("주소의 길이는: ${address?.length} 입니다")
    }
}

//결과
주소의 길이는: null 입니다
이름은: 참새 입니다
주소는: 서울 입니다

 

-> address가 null이면 호출하지 않고(null이라고는 출력됨), null이 아닐때만 address의 길이를 호출하는 코드이다

-> name에 "참새"값만 들어있고 address값은 아직 들어가지 않았기때문에 displayAddressLength함수를 호출하면 address값은 null이 나오게된다. 그 이후에 address에 "서울"값을 넣었기 때문에 이제 "주소는 서울입니다" 가 출력된다

 

 

  • ?.는 null일때 null값이 출력되도록 했지만, 이제 여기서 더 나아가서 null이 출력되는것을 막고싶을수도있다
  • null대신에 다른 값을 출력하고 싶다면 ?: 키워드를 사용하면된다
  • ?:엘비스 연산자 라고 한다
fun main(){
    var s = Student()
    s.name = "참새"
    s.displayAddressLength()

    s.address = "서울"
    s.displayInfo()
}

class Student {
    lateinit var name:String
    var address:String? = null

    fun displayInfo() {
        println("이름은: ${name} 입니다")
        println("주소는: ${address} 입니다")
    }
    
    fun displayAddressLength() {
        println("주소의 길이는: ${address?.length ?: "초기화하세요"} 입니다")
    }
}

//결과
주소의 길이는: 초기화하세요 입니다
이름은: 참새 입니다
주소는: 서울 입니다

 

-> address의 길이가 null인지 확인하고, null이면 ?: 를 사용하여 "초기화하세요"를 출력하고,

null이 아니면 해당 address의길이를 출력하는 코드이다

-> 즉,  ?:을 사용해서 null일때도 원하는 문자열을 출력할 수 있다

 

 

 

# 배열 

변수에 순서를 매겨 활용할 수 있어요!!

 

- 일반적으로 변수를 선언하면 코틀린은 띄엄띄엄 랜덤으로 생성한다 

- 즉, 변수의 위치정보가 연속적이지 않기 때문에 순서가 없다

- 배열을 통해서 변수에 순서를 매겨서 연속적으로 활용할 수 있다!!

 

 

# 배열 사용법

- 배열을 사용하기 위해 arrayOf 메소드(키워드)를 제공한다

// arrayOf메소드를 호출하면 배열을 리턴해줍니다
// 1,2,3,4,5 각각을 저장한 변수 5개를 배열형태로 arr에 저장합니다
var arr = arrayOf(1,2,3,4,5)

// 배열요소를 모두 출력합니다
println(Arrays.toString(arr))

// 배열의 첫번째 요소에 저장된 값을 출력합니다
// var num1 = 1의 num1과 arr[0]은 동일합니다
// arr[0]은 하나의 변수로 취급할 수 있습니다
// arr은 0~4번방(인덱스)까지 접근할 수 있습니다
println(arr[0])

 

 

# 배열을 왜 사용할까??

- 변수들을 각각 선언하면 반복적으로 변수에 접근하는 행위를 할 수 없다

- 배열이 없다면 점수를 저장하기 위해 score1, score2, score3 이렇게 변수를 만들어야 한다

- 배열을 사용한다면 scores로 점수들을 묶어 활용할 수 있어, 효율적으로 코드를 작성할 수 있다

 

 

- 예시 코드

  • 국어점수를 배열로 묶어 모든 점수를 출력하는 코드
fun main() {
    var kors = arrayOf(90, 94, 96)
    for((idx, kor) in kors.withIndex()) {
        println("${idx}번째 국어 점수는 ${kor}입니다")
    }
}

//결과
0번째 국어 점수는 90입니다
1번째 국어 점수는 94입니다
2번째 국어 점수는 96입니다

 

-> withIndex()라는 메소드를 호출하면 인덱스값 실제값하나로 묶어서 리턴해준다

 

 

  • 기본적인 배열코드
fun main() {
    // 정수형 배열
    val numbers = arrayOf(1, 2, 3, 4, 5)
    println("정수형 배열: ${numbers.joinToString()}")

    // 문자열 배열
    val colors = arrayOf("Red", "Green", "Blue")
    println("문자열 배열: ${colors.joinToString()}")

    // 다차원 배열
    val matrix = Array(3) { Array(3) { 0 } }
    for (row in matrix) {
        for (element in row) {
            print("$element ")
        }
        println()
    }

    // 배열 요소 접근과 수정
    numbers[2] = 10
    println("수정된 정수형 배열: ${numbers.joinToString()}")

    // 배열 크기 확인
    println("정수형 배열의 크기: ${numbers.size}")

    // 배열 순회
    println("문자열 배열 순회:")
    for (color in colors) {
        println(color)
    }
}

//결과
정수형 배열: 1, 2, 3, 4, 5
문자열 배열: Red, Green, Blue
0 0 0 
0 0 0 
0 0 0 
수정된 정수형 배열: 1, 2, 10, 4, 5
정수형 배열의 크기: 5
문자열 배열 순회:
Red
Green
Blue

 

 

 

# 컬렉션

개발에 유용한 자료구조를 제공해요!!

 

- 코틀린에서는 리스트, 맵, 집합 자료구조를 지원한다

 

 

  • List 의 활용

- List읽기전용수정가능한 종류로 구분할 수 있다

- 배열(array)와 달리, 크기가 정해져있지 않아 동적으로 값을 추가할 수 있다

// 읽기전용 리스트입니다
// 0번, 1번, 2번 인덱스에 접근해서 값을 변경할 수 없습니다
var scores1 = listOf(값1, 값2, 값3)

// 수정가능 리스트입니다
// 0번, 1번, 2번 인덱스에 접근해서 값을 변경할 수 있습니다
var scores2 = mutableListOf(값1, 값2, 값3)
scores2.set(인덱스, 값)

// 수정가능 리스트입니다
// 0번, 1번, 2번 인덱스에 접근해서 값을 변경할 수 있습니다
// array로 데이터들을 저장하는 ArrayList도 mutableListOf와 동일하게 사용할 수 있어요
// 저장할 데이터의 자료형을 < > 안에 지정해야 사용할 수 있어요
var scores3 = ArrayList<자료형>(값1, 값2, 값3)
scores3.set(인덱스, 값)

 

 

  • Map의 활용

- Map은 키(변수명)으로 이루어진 자료형이다

- 읽기전용수정 가능한 종류로 구분할 수 있다

fun main() {
    // 읽기전용 맵입니다
    // 변수명[키]로 데이터에 접근할 수 있습니다
    var scoreInfo1 = mapOf("kor" to 94, "math" to 90, "eng" to 92)
    println(scoreInfo1["kor"])

    // 수정가능 맵입니다
    // 변수명[키]로 데이터에 접근할 수 있습니다
    var scoreInfo2 = mutableMapOf("kor" to 94, "math" to 90)
    scoreInfo2["eng"] = 92
    println(scoreInfo2["eng"])

    // 맵의 키와 값을 동시에 추출해서 사용할 수 있습니다
    for((k,v) in scoreInfo2) {
        println("${k}의 값은 ${v}입니다")
    }
}


//결과
94
92
kor의 값은 94입니다
math의 값은 90입니다
eng의 값은 92입니다

 

 

 

  • Set의 활용

# 기본활용

- Set순서가 존재하지 않고, 중복없이 데이터를 관리하는 집합 자료형이다

- 읽기전용수정가능한 종류로 구분할 수 있다

- 다른 컬렉션들은 요소를 찾는데에 집중하지만, Set은 요소가 존재하는지에 집중한다

//  읽기전용 Set입니다.
    var birdSet = setOf("닭", "참새", "비둘기")

//  수정가능 Set입니다.
//  var mutableBirdSet = mutableSetOf("닭", "참새", "비둘기")
//  mutableBirdSet.add("꿩")
//  mutableBirdSet.remove("꿩")
    println("집합의 크기는 ${birdSet.size} 입니다")

    var findBird = readLine()!!

    if(birdSet.contains(findBird)) {
        println("${findBird} 종류는 존재합니다.")
    } else {
        println("${findBird}는 존재하지 않습니다.")
    }
    
    
//결과
집합의 크기는 3 입니다
닭
닭 종류는 존재합니다.

 

 

# 고급 활용

- 교집합, 차집합, 합집합으로 간편하게 요소들을 추출할 수 있다

    // 귀여운 새의 집합
    var birdSet = setOf("닭", "참새", "비둘기", "물오리")

    // 날수있는 새의 집합
    var flyBirdSet = setOf("참새", "비둘기", "까치")

    // 모든 새의 집합 (합집합)
    var unionBirdSet = birdSet.union(flyBirdSet)

    // 귀엽고 날수있는 새의 집합 (교집합)
    var intersectBirdSet = birdSet.intersect(flyBirdSet)

    // 귀여운 새들중에서 날수없는 새의 조합 (차집합)
    var subtractBirdSet = birdSet.subtract(flyBirdSet)

    println("=====합집합=====")
    println("모든 새의 집합 : ${unionBirdSet}")

    println("=====교집합=====")
    println("귀엽고 날수있는 새의 집합 : ${intersectBirdSet}")

    println("=====차집합=====")
    println("귀엽고 날수없는 새의 집합 : ${subtractBirdSet}")
    
//결과
=====합집합=====
모든 새의 집합 : [닭, 참새, 비둘기, 물오리, 까치]
=====교집합=====
귀엽고 날수있는 새의 집합 : [참새, 비둘기]
=====차집합=====
귀엽고 날수없는 새의 집합 : [닭, 물오리]

 

 

+) 제네릭 (Generic)

- 코드를 작성하다보면 다양한 타입에 동일한 로직을 적용할 때가 많은데, 매번 Any타입으로 받는것은 타입 안정성을 저하시킬 수 있다

- 클래스 내부에서 사용할 자료형을 인스턴스를 생성할 때 고정한다

- 컴파일 시간에 자료형을 검색 해 적당한 자료형을 선택할 수 있게한다

- 객체 자료형의 안정성을 높이고, 형 변환의 번거로움을 줄여준다

fun <T> test(arr: Array<T>, data: T): Int {
    for(i in arr.indices) {
        if(arr[i] == data) return i
    }
    return -1
}

fun main() {
    val obj1: Array<String> = arrayOf("c", "java", "kotlin")

    val index = test<String>(obj1, "kotlin")
    println(index)
}

 

 

- 제네릭 참고용 블로그

https://sas-study.tistory.com/485

https://junyoung-developer.tistory.com/83

 

 

 

 

- 예시 코드

 

  • 컬렉션의 주요한 List, Map, Set의 간단한 예제
fun main() {
    // 리스트(List) 예시
    val numbersList = listOf(1, 2, 3, 4, 5)
    println("리스트: $numbersList")

    // 집합(Set) 예시
    val colorsSet = setOf("Red", "Green", "Blue", "Red")
    println("집합: $colorsSet")

    // 맵(Map) 예시
    val agesMap = mapOf("Alice" to 30, "Bob" to 25, "Charlie" to 35)
    println("맵: $agesMap")

    // 리스트 요소 접근
    println("첫 번째 요소: ${numbersList[0]}")
    println("마지막 요소: ${numbersList.last()}")

    // 집합 요소 추가
    val fruitsSet = mutableSetOf("Apple", "Banana")
    fruitsSet.add("Orange")
    println("수정된 집합: $fruitsSet")

    // 맵 요소 수정
    val agesMutableMap = mutableMapOf("Alice" to 30, "Bob" to 25, "Charlie" to 35)
    agesMutableMap["Alice"] = 31
    println("수정된 맵: $agesMutableMap")

    // 리스트 순회
    println("리스트 순회:")
    for (number in numbersList) {
        println(number)
    }
}


//결과
리스트: [1, 2, 3, 4, 5]
집합: [Red, Green, Blue]
맵: {Alice=30, Bob=25, Charlie=35}
첫 번째 요소: 1
마지막 요소: 5
수정된 집합: [Apple, Banana, Orange]
수정된 맵: {Alice=31, Bob=25, Charlie=35}
리스트 순회:
1
2
3
4
5

 

 

  • 교집합,합집합을 계산하는 예제
fun main() {
    // 두 개의 Set 정의
    val set1 = setOf(1, 2, 3, 4, 5)
    val set2 = setOf(4, 5, 6, 7, 8)

    // 교집합 구하기
    val intersection = set1.intersect(set2)
    println("교집합: $intersection")

    // 합집합 구하기
    val union = set1.union(set2)
    println("합집합: $union")
}

//결과
교집합: [4, 5]
합집합: [1, 2, 3, 4, 5, 6, 7, 8]

 

 

  • 학생 3명의 점수를 입력받아 평균점수를 출력하기위해 컬렉션을 활용하는 예제
fun main() {
    var students = mutableListOf<Student>()
    var averages = mutableMapOf<String, Int>()

    for(idx in 0..2) {
        println("학생의 이름을 입력하세요")
        var name = readLine()!!

        println("국어 점수를 입력하세요")
        var kor = readLine()!!.toInt()

        println("수학 점수를 입력하세요")
        var math = readLine()!!.toInt()

        println("영어 점수를 입력하세요")
        var eng = readLine()!!.toInt()

        var average = (kor + math + eng) / 3
        var tempStudent = Student(name, kor, math, eng)

        students.add(tempStudent)
        averages[name] = average
    }

    for(student in students) {
        var average = averages[student.name]
        student.displayInfo()
        println("평균점수는 ${average} 입니다")
    }
}

class Student(name:String, kor:Int, math:Int, eng:Int) {
    var name:String
    var kor:Int
    var math:Int
    var eng:Int
    
    init {
        this.name = name
        this.kor = kor
        this.math = math
        this.eng = eng
    }

    fun displayInfo() {
        println("이름: $name")
        println("국어: $kor")
        println("수학: $math")
        println("영어: $eng")
    }
}

//결과
학생의 이름을 입력하세요
다람쥐 
국어 점수를 입력하세요
95
수학 점수를 입력하세요
70
영어 점수를 입력하세요
100
학생의 이름을 입력하세요
토끼
국어 점수를 입력하세요
88
수학 점수를 입력하세요
75
영어 점수를 입력하세요
68
학생의 이름을 입력하세요
거북이
국어 점수를 입력하세요
75
수학 점수를 입력하세요
78
영어 점수를 입력하세요
62
이름: 다람쥐 
국어: 95
수학: 70
영어: 100
평균점수는 88 입니다
이름: 토끼
국어: 88
수학: 75
영어: 68
평균점수는 77 입니다
이름: 거북이
국어: 75
수학: 78
영어: 62
평균점수는 71 입니다

 

 

 

# Single-expression function

람다식을 이용해서 메소드를 간결하게 정의할 수 있다!!

 

- 하나의 메소드를 간결하게 표현할 수 있는 방법이다

 

- kotlin의 람다식 구조

{매개변수1, 매개변수2... -> 
		코드
}

 

 

 

- 예제 코드

  • 세 개의 숫자의 평균을 리턴해주는 함수를 람다식으로 정의
fun add(num1:Int, num2:Int, num3:Int) = (num1+num2+num3)/3

 

  • 메소드를 선언하지 않고 로직을 저장
var add = {num1: Int, num2: Int, num3: Int -> (num1+num2+num3) / 3}
println("평균값은 ${add(10,20,30)}입니다")

 

 

 

 

 

# 싱글턴

메모리 전역에서 유일한 객체임을 보장할 수 있다

 

- 보통 객체는 자원이 가능한 만큼 생성할 수 있다

- 각각의 객체는 상이한 위치정보를 가지고 있어서 저장하는 값도 객체마다 고유하다

- 싱글턴을 활용하면, 해당 객체는 메모리 전역에서 유일함을 보장하고 위치정보가 고정이다

- 프로그램이 실행되는 시점에 메모리에 바로 로드해서 위치를 잡는다

 

 

# Kotlin의 싱글턴 구현방법

- companion, object 키워드로 싱글턴을 구현할 수 있다 

 

 

 

# 싱글턴을 왜 사용할까??

- 싱글턴 객체는 전역적으로 활용할 수 있어서, 다른 클래스들에서 쉽게 접근할 수 있다

- 전역에서 공통적으로 사용하는 정보라면 메모리를 더욱 효율적으로 활용할 수 있다

- 객체 자원간의 충돌을 방지할 수 있다

- 프로그램에서 키보드 객체를 무한하게 제작한다면 입력순서가 꼬일 수 있기때문에, 키보드 객체는 싱글턴으로 제작해서 사용하고 싶을때 객체을 얻어오는 방법으로 사용한다

 

 

- 예제 코드

  • 변수를 선언하지 않아도 클래스를 바로 호출할 수 있음!
  • 객체를 생성하지 않고도 클래스 정보에 접근할 수 있다 (생성자 호출X)
fun main() {
    Bird.fly("참새")
}

object Bird {
    fun fly(name:String) {
        println("${name}가 날아요~")
    }
}

//결과
참새가 날아요~

 

 

 

  • 객체를 생성하지 않고도 클래스 정보에 접근할 수 있다 (생성자 호출O)
fun main() {
    // trash와 같이 생성자에 매개변수 전달 가능
    var singletonObject1 = MySingletonClass.getInstance(trash = 1)
    singletonObject1.setNum(5)
    println("num값은: ${singletonObject1.getNum()}")

    // singletonObject2에서 num을 10으로 대입
    var singletonObject2 = MySingletonClass.getInstance(trash = 1)
    singletonObject2.setNum(10)

    // singletonObject1의 num이 10으로 출력됨
    // singletonObject1과 singletonObject2는 같은 객체를 공유하기 때문
    println("num값은: ${singletonObject1.getNum()}")

}

class MySingletonClass private constructor() {
    private var num:Int = 0

    companion object {
        @Volatile private var instance: MySingletonClass? = null
        private var trash = 0

        fun getInstance(trash: Int): MySingletonClass {
            this.trash = trash
            // 외부에서 요청왔을때 instance가 null인지 검증
            if(instance == null) {
		            // synchronized로 외부 쓰레드의 접근을 막음
								// 쓰레드는 다음챕터에서 소개합니다!
		            // 쓰레드간의 객체상태 혼돈을 막기위해 사용한다고 이해해주세요
                synchronized(this) {
                    instance = MySingletonClass()
                }
            }
            return instance!!
            
//            엘비스연산자와 뒷장에서배울 scope function을 이용하면
//            아래와같이 더욱 직관적인 코드 작성이 가능합니다
//            return instance ?: synchronized(this) {
//                // also는 호출한 객체를 it으로 넘김
//                // instance가 null이라면 새로 생성하고 아니면 무시함
//                instance ?: MySigletonClass().also {
//                    instance = it
//                }
//            }
        }
    }
    fun setNum(num: Int) {
        this.num = num
    }

    fun getNum(): Int{
        return this.num
    }
}

//결과
num값은: 5
num값은: 10