[Android/Flutter 교육] 9일차

MSU·2024년 1월 9일

Android-Flutter

목록 보기
9/85
post-thumbnail

DataClass

  • DataClass는 매개체의 데이터를 관리하는 용도로 사용하는 클래스

  • abstract, open, sealed, inner 클래스로 정의할 수 없다.

  • 반드시 주 생성자를 가지고 있어야 한다.

  • DataClass는 개발자의 개발 편리성을 위해 몇가지 메서드가 자동으로 구현된다

    1. equals : 객체가 가지고 있는 변수를 모두 비교(주생성자에 정의되어있는 변수 한정)

    2. hashCode : 객체를 구분하기 위한 고유한 정수값

    3. copy : 객체를 복제하는 메서드

    4. toStrihng : 객체가 가지고 있는 변수의 값을 출력

    5. componentN : 객체 분해

      fun main() {
        // 일반 클래스의 객체를 생성한다.
        val obj1 = TestClass1(100,200)
        val obj2 = TestClass1(100,200)
      
        // 데이터 클래스의 객체를 생성한다.
        val obj3 = TestClass2(100,200)
        val obj4 = TestClass2(100,200)
        
        // 일반 클래스를 통해 생성한 객체를 비교한다.
      
        // 코틀린에서 객체간에 == 연산자를 사용하면 equals 메서드를 호출하여 그 결과를 반환한다.
        // 만약 클래스에 equals 메서드를 구현하지 않으면 변수에 저장되어 있는 값을 비교한다.
        // 즉 변수가 가지고 있는 객체의 주소값이 같은지를 비교한다.
        if(obj1 == obj2){
            println("obj1과 obj2는 같은 객체입니다.")
        }else{
            println("obj1과 obj2는 다른 객체입니다.")
        }
      
        // 데이터 클래스를 통해 생성한 객체를 비교한다.
        // 데이터 클래스는 equals 메서드가 구현되어 있으며 객체가 가지고 있는 프로퍼티의 값이
        // 같은지를 비교해준다.
        if(obj3 == obj4){
            println("obj3과 obj4는 주 생성자에 정의된 프로퍼티의 값이 같습니다.")
        }else{
            println("obj3과 obj4는 주 생성자에 정의된 프로퍼티의 값이 다릅니다.")
        }
      
        // copy : 객체를 복제하여 새로운 객체를 생성한다.
        // 같은 클래스를 통해 객체를 생성하고 주 생성자에 정의되어 있는 프로퍼티의 값을 추출하여
        // 새롭게 생성된 객체에 저장해준다.
        val obj5 = obj3.copy()
        println("obj5 : ${obj5.a1}")
        println("obj5 : ${obj5.a2}")
        println("obj3 : ${obj3.a1}")
        println("obj3 : ${obj3.a2}")
      
        obj5.a1 = 1000
        println("obj5.a1 : ${obj5.a1}")
        println("obj3.a1 : ${obj3.a1}")
      
        // toString : 주생성자에 정의되어 있는 프로프티의 값을 확인할 수 있는 형태로 구현되어 있다.
        println("obj1 : $obj1")
        println("obj3 : $obj3")
      
        // componentN : 주 생성자에 정의되어 있는 프로퍼티의 개수 만큼 생성된다.
        // component1은 첫 번째 프로퍼티를 의미하고 component2는 두 번째 프로퍼티를 의미한다.
        // 각 프로퍼티가 가지고 있는 값을 반환받을 수 있다.
        val num1 = obj3.component1()
        val num2 = obj3.component2()
      
        println("num1 : $num1")
        println("num2 : $num2")
        
        // 첫 번째 변수인 num3 에는 component1을 호출하여 반환하는 값이 저장되고
        // 두 번째 변수인 num4 에는 component2을 호출하여 반환하는 값이 저장된다.
        val (num3, num4) = obj3
        println("num3 : $num3")
        println("num4 : $num4")
      }
      
      // 일반 클래스
      class TestClass1(var a1:Int, var a2:Int)
      
      // 데이터 클래스
      data class TestClass2(var a1:Int, var a2:Int)
      
      obj1과 obj2는 다른 객체입니다.
      obj3과 obj4는 주 생성자에 정의된 프로퍼티의 값이 같습니다.
      obj5 : 100
      obj5 : 200
      obj3 : 100
      obj3 : 200
      obj5.a1 : 1000
      obj3.a1 : 100
      obj1 : TestClass1@234c8c9b
      obj3 : TestClass2(a1=100, a2=200)
      num1 : 100
      num2 : 200

Generic

  • 클래스를 설계할 때 프로퍼티, 매개변수 등에 설정되는 자료형을 유동적으로 하고 싶을 때가 있을수도 있다.
  • 이 때 Generic 개념을 활용하면 클래스 작성시가 아닌 객체 생성시에 프로퍼티, 매개변수 등에 설정되는 자료형을 설정할 수 있다.
  • < > 안에 알파벳 문자를 넣어준다. 보통 대문자 한글자를 작성한다.
    이 알파벳은 아직 결정되지 않은 타입을 의미한다.
    어떠한 타입인지 결정되지 않았지만 이 타입의 변수들을 정의하여 클래스를 작성하고
    향후, 객체를 생성할 때 타입을 결정할 수 있다.
    제네릭 타입은 객체를 생성할 때 타입을 결정해줘야 한다.
class TestClass1<T>{
    fun testMethod1(a1:T){
        println("a1 : $a1")
    }
}

fun main() {
    // 제네릭 타입은 객체를 생성할 때 타입을 결정해줘야 한다.
    // T 타입으로 정의된 변수의 타입이 Int 로 결정된다.
    val t1 = TestClass1<Int>()
    t1.testMethod1(100)
    
    val t2 = TestClass1<String>()
    t2.testMethod1("안녕하세요")
}
a1 : 100
a1 : 안녕하세요
class TestClass2<A, B, C, D>(var a1:A, var a2:B){

    fun testMethod3(a3:C, a4:D){
        println("a1 : $a1")
        println("a2 : $a2")
        println("a3 : $a3")
        println("a4 : $a4")
    }
}

fun main() {
    val t3 = TestClass2<Int, Double, Boolean, String>(100,11.11)
    t3.testMethod3(true, "문자열1")

    val t4 = TestClass2<Double, String, Boolean, Int>(22.22,"문자열2")
    t4.testMethod3(false, 200)
}
a1 : 100
a2 : 11.11
a3 : true
a4 : 문자열1
a1 : 22.22
a2 : 문자열2
a3 : false
a4 : 200

가변성

불변성

  • 객체를 생성할 때 설정한 제네릭과 같은 변수에 담을 수 있다.
    클래스간의 상속 관계에 상관없이 제네릭에 설정한 클래스 타입이 다르면 오류가 발생한다.
fun main() {
    val t5:TestClass3<SubClass1> = TestClass3<SubClass1>()
    // val t6:TestClass3<SuperClass1> = TestClass3<SubClass1>() // 에러
    // val t6:TestClass3<SubClass2> = TestClass3<SubClass1>() // 에러
}

open class SuperClass1
open class SubClass1 : SuperClass1()
class SubClass2 : SubClass1()

// 불변성(제네릭에 키워드를 붙히지 않는다)
class TestClass3<T>

공변성

  • 변수에 설정한 제네릭이 객체를 생성했을 때 사용한 제네릭과 같거나 부모 클래스인 경우에 담을 수 있다.
fun main() {
	val t8:TestClass4<SubClass1> = TestClass4<SubClass1>()
    val t9:TestClass4<SuperClass1> = TestClass4<SubClass1>()
    // val t10:TestClass4<SubClass2> = TestClass4<SubClass1>() // 에러
}

open class SuperClass1
open class SubClass1 : SuperClass1()
class SubClass2 : SubClass1()

// 공변성
class TestClass4<out A>()

반 공변성

  • 변수에 설정한 제네릭이 객체를 생성했을 때 사용한 제네릭과 같거나 자식 클래스인 경우에 담을 수 있다.
fun main() {
    val t11:TestClass5<SubClass1> = TestClass5<SubClass1>()
    // val t12:TestClass5<SuperClass1> = TestClass5<SubClass1>() // 에러
    val t13:TestClass5<SubClass2> = TestClass5<SubClass1>()
}

open class SuperClass1
open class SubClass1 : SuperClass1()
class SubClass2 : SubClass1()

// 반 공변성
class TestClass5<in A>()

중첩클래스

  • 클래스 안에 만드는 클래스를 중첩클래스라고 부른다.
  • 클래스 안에 있다고 해서 클래스를 가지고 있는 클래스에서만 객체를 생성할 수 있는 것은 아니다. 그러나 외부에서 객체를 생성하는 것이 불편하다.
  • 안드로이드 개발에서는 중첩클래스를 많이 사용한다고 한다.

일반 중첩클래스

fun main() {
    // 내부 클래스의 객체를 생성하기 위해서는 외부 클래스의 객체가 필요하다.
    val obj1 = Outer1()
    val obj2 = obj1.Inner1()
}

// 일반 중첩 클래스
// 외부 클래스
class Outer1{

    // 내부 클래스
    inner class Inner1{

    }
}
  • 외부 클래스에서 내부 클래스의 프로퍼티나 메소드를 사용할 수 있을까? - X
    내부 클래스의 객체를 생성하려면 외부 클래스를 통해 만든 객체가 필요하다.
    따라서 내부 클래스를 가지고 만든 객체 입장에서는 외부 클래스의 객체가 생성되어 있다는 것을 보장받을 수 있다.
    이에 내부 클래스를 가지고 만든 객체는 외부 클래스에 정의된 모든 멤버를 사용할 수 있다.
fun main() {
    // 내부 클래스의 객체를 생성하기 위해서는 외부 클래스의 객체가 필요하다.
    val obj1 = Outer1()
	obj1.Inner1().innerValue1    
}
// 일반 중첩 클래스
// 외부 클래스
class Outer1{

    //프로퍼티
    var outerValue1 = 100

    // 메서드
    fun outerMethod1(){
        println("Outer1의 outerMethod1")
        
        // 내부 클래스가 가지고 있는 요소를 사용한다.
        // 외부 클래스의 객체를 생성했다고 해서 내부 클래스의 객체가 생성되는 것이 아니므로
        // 외부 클래스에서 내부 클래스의 요소는 사용할 수 없다.
        // println("innerValue : $innerValue1") // 에러
        // innerMethod1() // 에러
        
    }


    // 내부 클래스
    inner class Inner1{
        //프로퍼티
        var innerValue1 = 200

        // 메서드
        fun innerMethod1(){
            println("Inner1의 innerMethod1")

            // 내부 클래스의 객체를 생성하려면 외부 클래스를 통해 만든 객체가 필요하다.
            // 따라서 내부 클래스를 가지고 만든 객체 입장에서는 외부 클래스의 객체가 생성되어
            // 있다는 것을 보장받을 수 있다. 이에 외부 클래스에 정의된 모든 멤버를 사용할 수 있다.
            println("outerValue1 : $outerValue1")
            outerMethod1()
        }
    }
}

익명 중첩클래스

  • 인터페이스나 추상 클래스를 통해 객체를 생성할 수 없다.
    인터페이스를 구현하여 메서드를 구현한 클래스를 정의하거나 추상클래스를 상속받아 메서드를 구현한 클래스를 만들어야 객체를 생성할 수 있다.
    만약 이러한 클래스를 통해 생성하는 객체가 하나뿐이라면 익명 중첩클래스를 사용하면 편하게 작업할 수 있다.
// 인터페이스나 추상 클래스를 통해 객체를 생성할 수 없다.
// 인터페이스를 구현하여 메서드를 구현한 클래스를 정의하거나 추상클래스를 상속받아 메서드를 구현한
// 클래스를 만들어야 객체를 생성할 수 있다.
// 만약 이러한 클래스를 통해 생성하는 객체가 하나뿐이라면 익명 중첩클래스를 사용하면 편하게 작업할 수 있다.
interface Inter1{
    fun interMethod1()
    fun interMethod2()
}

class TestClass1:Inter1{
    override fun interMethod1() {
        println("TestClass1의 interMethod1")
    }

    override fun interMethod2() {
        println("TestClass2의 interMethod2")
    }
}

fun main() {
    // 다른 위치, 파일, 패키지, 모듈에 클래스를 작성했다면 그곳에서 클래스를 작성하고
    // 여기서 객체 생성
    val obj3 = TestClass1()
    obj3.interMethod1()
    obj3.interMethod2()

    // 만약 익명 중첩클래스를 사용한다면
    // 클래스 작성과 메서드의 오버라이딩과 객체 생성을 같은 곳에서 모두 작업한다.
    // 단, 이름이 없는 1회용 클래스이기 때문에 이 클래스를 더 이상 사용할 수 없다.
    // object : 인터페이스나 추상클래스
    val obj4 = object : Inter1{
        override fun interMethod1() {
            println("익명 중첩 클래스의 interMethod1")
        }

        override fun interMethod2() {
            println("익명 중첩 클래스의 interMethod2")
        }
    }

    obj4.interMethod1()
    obj4.interMethod2()
}
익명 중첩 클래스의 interMethod1
익명 중첩 클래스의 interMethod2

Null 처리

NullPointerException

  • Java 언어로 소프트웨어를 개발하다 보면 NullPointerException이라는 오류를 자주 만나게 된다.
  • 객체의 주소 값이 담겨져 있지 않는(null값이 들어있는) 참조 변수를 통해 객체 접근을 시도하면 발생되는 오류이다.

Null Safe

  • kotlin은 개발자가 null이 담겨 있는 참조변수를 통해 객체 접근을 시도할 때 오류가 발생되는 것을 방지하고자 다양한 방법을 제공하고 있다.
    이를 통해 null값에 대한 안정성을 확보(null safe)할 수 있다.
  • 변수에 null값이 들어있을 가능성이 있는 경우 이 변수에 객체 접근을 시도할 때 에러를 방지하고자 하는 것
  • 코틀린은 지연 초기화가 있기 때문에 변수에 null을 넣어주지 않아도 됨
    하지만 호출하는 메서드가 null을 반환하는 경우도 있기 때문에 null에 대한 대비가 필요
  • 변수에 null 값이 들어가지 못하도록 해준다.
  • 변수에 null 값이 들어가 있을 경우 객체의 접근을 막아준다.
  • 변수에 null 값이 들어가 있을 경우 메서드의 호출이나 프로퍼티 사용을 취소시킨다.
  • 변수에 null 값이 들어가 있을 경우 메서드를 호출하거나 프로퍼티 사용을 취소 시키고 지정된 기본값을 사용하게 한다.
fun main() {
    // null 을 허용하지 않는 프로퍼티
    // var a1:TestClass1 = null

    // null 을 허용하는 프로퍼티
    // null 을 허용하는 프로퍼티를 선언할 때는 타입을 반드시 작성해줘야 한다.
    var a2:TestClass1? = null
    
    // 변수에 null 값이 들어있는 상태에서 메서드를 호출
    // a2.test1Method() //에러
    // a2!!.test1Method() // NullPointerException 에러 발생
}

class TestClass1{

    fun test1Method(){
        println("TestClass1의 test1Method")
    }
    
    // 값을 반환하는 메서드
    fun test2Method():Int{
        return 100
    }
}

!!연산자

  • !! : null을 허용하는 변수에 저장되어 있는 값을 null을 허용하지 않는 형태로 변환한다.
    null을 허용하는 변수에 저장되어 있는 값을 null을 허용하지 않는 변수에 담을 때 사용한다.
    이 때, null값이 들어있다면 오류가 발생한다.
    이 연산자는 null의 안정성을 확보하는 것을 목적으로 사용하는 것이 아님
fun main() {
    // null을 허용하지 않는 타입 변수에 null을 허용하지 않는 타입의 변수의 값을 넣어준다.
    val t4:TestClass1 = t1
    // null을 허용하는 타입의 변수 뒤에 !!를 붙여서 null을 허용하지 않는 타입으로 변경
    // 이 때 null 값이 아닌 객체의 주소값이 들어 있으므로 정상 변환된다.
    val t5:TestClass1 = t2!!
    println("t4 : $t4")
    println("t5 : $t5")

    // null을 허용하는 타입의 변수 뒤에 !!를 붙여서 null을 허용하는 타입의 값으로 변경
    // 이 때 null 값이 들어있으므로 변환 때 오류가 발생한다.
    // 변수에 null아닌 객체의 주소값이 들어 있다는 것을 100% 보장할 수 있을 경우에만 사용한다.
    // val t6:TestClass1 = t3!! // 에러
    // println("t6 : $t6")
}

class TestClass1{

    fun test1Method(){
        println("TestClass1의 test1Method")
    }
}

?연산자

  • 변수 ? : 기본값 : null 값을 허용하는 변수를 사용할 때 null이 들어가 있을 경우 객체 대신에 기본값으로 설정되어 있는 것을 전달해 준다.
    변수에 null 값이 들어가 있을 경우 객체를 생성해서 반환해주는 작업을 할 때 사용한다.
fun main() {
    var t7:TestClass1? = null
    var t8:TestClass1? = TestClass1()

    var t9:TestClass1 = t7 ?: TestClass1()
    var t10:TestClass1 = t8 ?: TestClass1()

    println("t7 : ${t7}, t9 : $t9")
    println("t8 : ${t8}, t10 : $t10")

}

class TestClass1{

    var testValue1 = 100

    fun test1Method(){
        println("TestClass1의 test1Method")
    }

    // 값을 반환하는 메서드
    fun test2Method():Int{
        return 100
    }
}
t7 : null, t9 : TestClass1@e87f4b7c
t8 : TestClass1@cc814459, t10 : TestClass1@cc814459
  • ? : 객체가 가지고 있는 프로퍼티나 메서드를 사용할 때 사용하는 연산자
    변수에 null이 들어가 있으면 수행이 무시된다.
    변수에 객체의 주소값이 들어가 있다면 객체에 접근해 프로퍼티나 메서드를 사용한다.
fun main() {
    val t11:TestClass1? = null
    val t12:TestClass1? = TestClass1()

    // 프로퍼티
    // 객체를 가지고 있는 변수에 null이 들어가 있으면 프로퍼티 접근이 중단되고
    // null을 반환한다.
    println("t11.testValue1 : ${t11?.testValue1}")
    println("t12.testValue1 : ${t12?.testValue1}")

    // 메서드
    // null이 들어가 있으면 메서드 호출이 취소된다.
    t11?.test1Method()
    t12?.test1Method()

}

class TestClass1{

    var testValue1 = 100

    fun test1Method(){
        println("TestClass1의 test1Method")
    }

    // 값을 반환하는 메서드
    fun test2Method():Int{
        return 100
    }
}
t11.testValue1 : null
t12.testValue1 : 100
TestClass1의 test1Method

형변환

fun main() {
    // 객체 생성
    // 부모 클래스형 타입에 담는다.
    val obj1:SuperClass1 = SubClass1()
    // 구현한 인터페이스형 타입에 담는다.
    val obj2:Inter1 = SubClass2()
    
    // 객체의 주소값을 가지고 있는 변수의 타입이 부모클래스 타입이므로
    // 부모가 가지고 있는 메서드이거나 자식에서 overriding한 메서드만 호출할 수 있다.
    obj1.superMethod1()
    // obj1.subMethod1() // 에러
    // 객체의 주소값을 가지고 있는 변수의 타입이 구현한 인터페이스 타입이므로
    // overriding한 메서드만 호출할 수 있다.
    obj2.interMethod1()
    // obj2.subMethod2() // 에러
}

open class SuperClass1{
    fun superMethod1(){
        println("SuperClass1의 superMethod1 입니다.")
    }
}
interface Inter1{
    fun interMethod1()
}

class SubClass1 : SuperClass1(){
    fun subMethod1(){
        println("SubClass1의 subMethod1 입니다")
    }
}

class SubClass2 : Inter1{
    fun subMethod2(){
        println("SubClass2의 subMethod2 입니다")
    }

    override fun interMethod1() {
        println("SubClass2의 interMethod1 입니다")
    }
}

as 연산자

  • 객체의 주소값을 가지고 있는 변수의 타입을 다른 클래스 타입으로 변환한다.
fun main() {
    val obj1:SuperClass1 = SubClass1()
    val obj2:Inter1 = SubClass2()
    
    // as : 객체의 주소값을 가지고 있는 변수의 타입을 다른 클래스 타입으로 변환한다.
    val obj3 = obj1 as SubClass1
    obj1.superMethod1()
    obj1.subMethod1()

    obj3.superMethod1()
    obj3.subMethod1()
    
    // 위의 클래스는 obj1에 SubClass1을 가지고 만든 객체의 주소값이 담겨져 있기 때문에 가능한 것이다.
    // 만약 다른 클래스타입으로 변환을 할 경우 오류가 발생한다.
    // val obj4 = obj1 as String // 에러
}

open class SuperClass1{
    fun superMethod1(){
        println("SuperClass1의 superMethod1 입니다.")
    }
}
interface Inter1{
    fun interMethod1()
}

class SubClass1 : SuperClass1(){
    fun subMethod1(){
        println("SubClass1의 subMethod1 입니다")
    }
}

class SubClass2 : Inter1{
    fun subMethod2(){
        println("SubClass2의 subMethod2 입니다")
    }

    override fun interMethod1() {
        println("SubClass2의 interMethod1 입니다")
    }
}
SuperClass1의 superMethod1 입니다.
SubClass1의 subMethod1 입니다
SuperClass1의 superMethod1 입니다.
SubClass1의 subMethod1 입니다

is 연산자

  • 객체를 생성했을 때 사용했던 클래스가 무엇인지 확인할 수 있다.
  • 또한, 객체를 생성했을 때 사용했던 클래스의 부모클래스도 확인할 수 있다.
  • 의미는 객체에 지정한 클래스 부분이 있는지를 확인하는 것이다.
fun main() {
    // is : 객체를 생성했을 때 사용했던 클래스가 무엇인지 확인할 수 있다.
    // 또한, 객체를 생성했을 때 사용했던 클래스의 부모클래스도 확인할 수 있다.
    // 의미는 객체에 지정한 클래스 부분이 있는지를 확인하는 것이다.
    val obj5 = SuperClass1()
    var chk1 = obj5 is SubClass1
    var chk2 = obj5 is SuperClass1
    var chk3 = obj5 is Inter1

    println("chk1 : $chk1")
    println("chk2 : $chk2")
    println("chk3 : $chk3")
}

open class SuperClass1{
    fun superMethod1(){
        println("SuperClass1의 superMethod1 입니다.")
    }
}
interface Inter1{
    fun interMethod1()
}

class SubClass1 : SuperClass1(){
    fun subMethod1(){
        println("SubClass1의 subMethod1 입니다")
    }
}

class SubClass2 : Inter1{
    fun subMethod2(){
        println("SubClass2의 subMethod2 입니다")
    }

    override fun interMethod1() {
        println("SubClass2의 interMethod1 입니다")
    }
}
chk1 : false
chk2 : true
chk3 : false

스마트 캐스팅

  • 특정 조건을 만족하면 자동으로 형변환이 발생하는 개념
  • 형변환에 대해 크게 신경을 쓰지 않아도 됨
  • 형변환은 객체의 클래스 타입이 아닌 객체의 주소값을 가지고 있는 참조변수의 타입이 변경되는 것
fun main() {
	// 스마트 캐스팅 : 특정 조건을 만족하면 자동으로 형변환이 이루어지는 것을 의미한다.
    val obj6:SuperClass1 = SubClass1()
    // SubClass1부분이 있는지 검사한다.
    // 아래와 같이 객체에 특정 클래스 부분이 있는지를 검사하는 코드는
    // 해당 타입으로 형변환 한 다음에 메서드나 프로퍼티를 호출하려고 하는 경우가 많다.
    // 이제 코틀린은 검사 대상 타입으로 변환까지 해준다.

    // obj6.subMethod1() // 에러
    if(obj6 is SubClass1){
        obj6.subMethod1()
    }
    // 위의 if문이 끝나면 원래의 타입으로 돌아온다.
    // obj6.subMethod1() // 에러
}

open class SuperClass1{
    fun superMethod1(){
        println("SuperClass1의 superMethod1 입니다.")
    }
}
interface Inter1{
    fun interMethod1()
}

class SubClass1 : SuperClass1(){
    fun subMethod1(){
        println("SubClass1의 subMethod1 입니다")
    }
}

class SubClass2 : Inter1{
    fun subMethod2(){
        println("SubClass2의 subMethod2 입니다")
    }

    override fun interMethod1() {
        println("SubClass2의 interMethod1 입니다")
    }
}
SuperClass1의 superMethod1 입니다.
  • 매개변수가 Any인 함수 호출
fun main() {
    val obj1:SuperClass1 = SubClass1()
    val obj2:Inter1 = SubClass2()
    
    // 매개변수가 Any인 함수 호출
    // Any는 모든 클래스의 최상위 부모이고 코틀린에서 기본 자료형(Int, Float..)도 사실 클래스임
    anyFunction(obj1)
    anyFunction(obj2)
    anyFunction(100)
    anyFunction("안녕하세요")
}

open class SuperClass1{
    fun superMethod1(){
        println("SuperClass1의 superMethod1 입니다.")
    }
}
interface Inter1{
    fun interMethod1()
}

class SubClass1 : SuperClass1(){
    fun subMethod1(){
        println("SubClass1의 subMethod1 입니다")
    }
}

class SubClass2 : Inter1{
    fun subMethod2(){
        println("SubClass2의 subMethod2 입니다")
    }

    override fun interMethod1() {
        println("SubClass2의 interMethod1 입니다")
    }
}

// 매개변수로 Any 타입을 가지고 있는 함수
fun anyFunction(obj:Any){
    // is를 통해 클래스 타입을 확인하고
    // 스마트 캐스팅을 이용해 메서드를 호출할 수 있다.
    if(obj is SubClass1){
        obj.subMethod1()
    }
    if(obj is SubClass2){
        obj.subMethod2()
    }
    if(obj is Int){
        println("정수 입니다")
    }
    if(obj is String){
        println("문자열 입니다")
    }
}
SubClass1의 subMethod1 입니다
SubClass2의 subMethod2 입니다
정수 입니다
문자열 입니다

기본 자료형 형변환

  • 코틀린에서 사용하는 모든 기본 자료형들은 사실 클래스다.
    코틀린은 모든 값들 객체로 관리한다
fun main(){
    val str1 = "100"
    val number1 = str1.toInt()
    if(number1 is Int){
        println("정수로 변환되었습니다.")
    }

    val str2 = number1.toString()
    if(str2 is String){
        println("문자열로 변환되었습니다.")
    }
}
정수로 변환되었습니다.
문자열로 변환되었습니다.

Null 안정성을 위한 형변환

  • null을 허용하는 변수에 null이 아닌 객체의 주소값이 들어가 있음을 확신 시켜주면 스마트 캐스팅이 발생하여 null을 허용하지 않는 타입으로 변환해준다.
    강사님은 ?연산자 등 대신 이 방법을 권장함
// null을 허용하는 변수에 null이 아닌 객체의 주소값이 들어가 있음을 확신 시켜주면
// 스마트 캐스팅이 발생하여 null을 허용하지 않는 타입으로 변환해준다.
// 강사님은 ?연산자 등 대신 이 방법을 권장함
fun main() {
    val t1:TestClass1? = TestClass1()
    val t2:TestClass1? = null

    // !! : null을 허용하는 변수의 값을 null을 허용하지 않는 변수에 담을 때 사용한다
    // 만약 null이 들어가 있으면 오류가 발생한다.
    val t3 = t1!!
    // val t4 = t2!! // 에러

    // ?: : null일 경우 새로운 객체를 생성하고자 할 때 사용한다.
    val t5 = t1 ?: TestClass1()
    val t6 = t2 ?: TestClass1()

    // ? : 객체가 가지고 있는 프로퍼티나 메서드를 사용하고자 할 때 사용한다.
    // 프로퍼티인 경우 null이 반환되고 메서드인 경우 호출이 취소된다.
    t1?.testMethod1()
    t2?.testMethod1()

    // if문으로 null 여부를 검사해주면 null을 허용하지 않는 타입으로 스마트 캐스팅이 발생한다.
    if(t1 != null){
        t1.testMethod1()
    }

    if(t1 is TestClass1){
        t1.testMethod1()
    }
}

class TestClass1{
    fun testMethod1(){
        println("TestClass1의 testMethod1 입니다")
    }
}

열거형(Enum)

  • 프로그램 개발 시 특정 그룹안의 구성 요소를 정의하는 값이 필요하고자 할 때 사용한다.
  • 열거형은 정의한 단어 자체가 프로그램에서 사용하는 값이 된다.
  • 새로운 형태의 값을 정의하고자 할 때 열거형을 사용

정적 멤버를 사용할 때

fun main() {
    printDirection(Direction2.SOUTH)
}

// 정적 멤버
class Direction2{
    companion object{
        val NORTH = 0
        val SOUTH = 1
        val WEST = 2
        val EAST = 3
    }
}

// 매개변수로 들어오는 값으로 분기하여 출력하는 함수를 만든다
// 정적 멤버 사용
fun printDirection(dir:Int){
    when(dir){
        Direction2.WEST -> println("서쪽입니다")
        Direction2.SOUTH -> println("남쪽입니다")
        Direction2.NORTH -> println("북쪽입니다")
        Direction2.EAST -> println("동쪽입니다")
        // 정적 멤버로 정의된 변수들은 서로간에 어떠한 관계가 있는 것이 아니기 때문에
        // 몇가지 경우를 빼더라도 문제가 발생하지 않는다.
    }
}
남쪽입니다

열거형을 사용할 때

fun main() {
    printDirection2(Direction.EAST)
}

// 열거형 정의
enum class Direction{
    NORTH, SOUTH, WEST, EAST
}

// 매개변수로 들어오는 값으로 분기하여 출력하는 함수를 만든다
// 열거형 사용
fun printDirection2(dir:Direction){
    when(dir){
        Direction.EAST -> println("동쪽입니다")
        Direction.SOUTH -> println("남쪽입니다")
        Direction.NORTH -> println("북쪽입니다")
        Direction.WEST -> println("서쪽입니다")
        // 열거형에 정의된 값들은 하나의 열거형 안에 정의된 값이라는 관계를 갖기 때문에
        // 모든 경우를 다 적어주거나 빠진 경우가 있을 시 빠진 것에 대한 처리를 할 수 있도록 else 문을 넣어줘야 한다.
    }
}
동쪽입니다
  • when 구문 사용 시 else구문이 필수인데 정적 멤버 변수를 사용할 경우 각각이 독립적인 변수들이기 때문에 더 있을 수도 있다고 컴퓨터가 판단하기 때문에 반드시 else를 넣어줘야 한다.
// 매개변수로 들어오는 값에 따라 값을 반환하는 함수
// 정적멤버 사용
fun getValue1(a1:Int) = when(a1){
    Direction2.WEST -> "서쪽"
    Direction2.EAST -> "동쪽"
    Direction2.SOUTH -> "남쪽"
    Direction2.NORTH -> "북쪽"
    // 각각이 독립적인 변수들이기 때문에 더 있을 수도 있다고 컴퓨터가 판단하기 때문에
    // 반드시 else를 넣어줘야 한다.
    else -> "아무 방향"
}
fun main() {
    val r1 = getValue1(Direction2.SOUTH)
    println("r1 : $r1")
}
r1 : 남쪽
  • 이 때 열거형을 사용해주면 else는 작성하지 않아도 된다.
// 매개변수로 들어오는 값에 따라 값을 반환하는 함수
// 열거형 사용
// 하나의 열거형 안에 정의 되어 있는 모든 경우에 대해 작성해주면
// else는 작성하지 않아도 된다. 만약 빠진게 있다면 else를 넣어준다.
fun getValue2(a1:Direction) = when(a1){
    Direction.EAST -> "동쪽"
    Direction.WEST -> "서쪽"
    Direction.SOUTH -> "남쪽"
    Direction.NORTH -> "북쪽"
}
fun main() {
    val r2 = getValue2(Direction.EAST)
    println("r2 : $r2")
}
r2 : 동쪽

열거형 값 설정

  • 열거형을 정의할 때 값도 설정할 수 있다.
  • 열거형 클래스의 주 생성자는 열거형 하나를 만드는 양식을 의미한다.
  • 변수의 개수는 무제한이다.
enum class Number(val num:Int, val str:String){
    ONE(1, "일"),
    TWO(2, "이"),
    THREE(3, "삼")
}

fun printValue3(v1:Number){
    when(v1){
        Number.ONE -> println("1 입니다")
        Number.TWO -> println("2 입니다")
        Number.THREE -> println("3 입니다")
    }

    when(v1.num){
        1 -> println("1 입니다")
        2 -> println("2 입니다")
        3 -> println("3 입니다")
    }
    
    when(v1.str){
        "일" -> println("1 입니다")
        "이" -> println("2 입니다")
        "삼" -> println("3 입니다")
    }
}

fun main(){
   printValue3(Number.TWO)
}
2 입니다
2 입니다
2 입니다

Sealed 클래스

  • 특정 클래스를 상속받은 클래스들을 열거형 처럼 모아 관리하는 개념
  • Sealed 클래스가 부모클래스가 되고 Sealed 클래스가 관리하는 모든 클래스는 Sealed 클래스를 상속받은 자식 클래스에 해당한다.
  • 중첩클래스의 관계가 아님
// Sealed 클래스 : 특정 클래스를 상속받은 클래스들을 열거형 처럼 모아 관리하는 개념
// Sealed 클래스가 부모클래스가 되고 Sealed 클래스가 관리하는 모든 클래스는 Sealed 클래스를
// 상속받은 자식 클래스에 해당한다.

fun main() {
    // 객체를 생성한다.
    val obj1 = NumberClass.OneClass(100,200)
    val obj2 = NumberClass.TwoClass(300)
    val obj3 = NumberClass.ThreeClass(100,11.11)

    obj1.numberMethod1()
    obj2.numberMethod1()
    obj3.numberMethod1()
}

// 열거형
enum class Number1(var num:Int, var str:String){
    ONE(1,"일"), TWO(2,"이"), THREE(3,"삼")
}

// Sealed 클래스는 자기 자신을 상속받은 클래스들을 모아 관리하는 개념
sealed class NumberClass{

    open fun numberMethod1(){
        println("NumberClass의 numberMethod1")
    }

    // 관리할 클래스
    // 모든 클래스는 Sealed 클래스를 상속받는다.
    class OneClass(var a1:Int, var a2:Int) : NumberClass()
    class TwoClass(var a1:Int) : NumberClass(){
        override fun numberMethod1() {
            println("TwoClass의 numberMethod1")
        }
    }
    class ThreeClass(var a1:Int, var a2:Double) : NumberClass()

}
NumberClass의 numberMethod1
TwoClass의 numberMethod1
NumberClass의 numberMethod1
  • Sealed 클래스도 when절을 사용할 때 열거형enum처럼 모든 클래스를 적어주어야 에러가 안남
// 매개변수로 들어오는 객체의 클래스 타입에 따라 분기해 처리한다.
// 함수의 매개변수에 Sealed 클래스 타입을 넣어준다.
fun checkNumber(t1:NumberClass){
    // when으로 분기할 때 is를 통해 어떤 클래스를 통해 만든 객체인지 검사한다.
    // 이 때 스마트 캐스팅도 이루어진다.
    when(t1){
        is NumberClass.OneClass -> {
            println("OneClass입니다")
            println(t1.a1)
            println(t1.a2)
            t1.numberMethod1()
        }
        is NumberClass.TwoClass -> {
            println("TwoClass입니다")
            println(t1.a1)
            t1.numberMethod1()
        }
        is NumberClass.ThreeClass -> {
            println("ThreeClass입니다")
            println(t1.a1)
            println(t1.a2)
            t1.numberMethod1()
        } // ThreeClass까지 모두 작성해야 함
    }
}

fun main() {
    val obj1 = NumberClass.OneClass(100,200)
    val obj2 = NumberClass.TwoClass(300)
    val obj3 = NumberClass.ThreeClass(100,11.11)
    
    checkNumber(obj1)
    checkNumber(obj2)
    checkNumber(obj3)
}
OneClass입니다
100
200
NumberClass의 numberMethod1
TwoClass입니다
300
TwoClass의 numberMethod1
ThreeClass입니다
100
11.11
NumberClass의 numberMethod1

Reflection 리플렉션

  • Reflection : 프로그램 실행 중에 객체에 대한 다양한 정보를 파악할 수 있다.
import kotlin.reflect.KClass

// Reflection : 프로그램 실행 중에 객체에 대한 다양한 정보를 파악할 수 있다.


class TestClass

fun main() {
    // 클래스 타입
    // KClass<클래스타입> : 지정된 클래스의 타입을 파악한다(코틀린 클래스)
    val a1:KClass<String> = String::class
    println("String의 코틀린에서의 타입 : $a1")
    // Class<클래스타입> : 지정된 클래스의 타입을 파악한다(자바 클래스)
    val a2:Class<String> = String::class.java
    println("String의 자바에서의 타입 : $a2")
    
    // 변수를 통해 접근할 수 있는 객체의 클래스 타입을 파악한다.
    val str1 = "안녕하세요"
    val a3:KClass<out String> = str1::class
    println("str1을 통해 접근할 수 있는 객체의 클래스 타입(코틀린) : $a3")

    val a4:Class<out String> = str1::class.java
    println("str1을 통해 접근할 수 있는 객체의 클래스 타입(자바) : $a4")
    
    val test1 = TestClass()

    val a7 = test1::class
    val a8 = test1::class.java
    println("test1의 클래스 타입(코틀린) : $a7")
    println("test1의 클래스 타입(자바) : $a8")
    
    
    // 클래스 정보 분석
    println("추상 클래스 인가 : ${test1::class.isAbstract}")
    println("Companion 클래스 인가 : ${test1::class.isCompanion}")
    println("Data 클래스 인가 : ${test1::class.isData}")
    println("Final 클래스 인가 : ${test1::class.isFinal}")
    println("open 클래스 인가 : ${test1::class.isOpen}")
    println("중첩 클래스 인가 : ${test1::class.isInner}")
    println("Sealed 클래스 인가 : ${test1::class.isSealed}")
}
String의 코틀린에서의 타입 : class java.lang.String (Kotlin reflection is not available)
String의 자바에서의 타입 : class java.lang.String

str1을 통해 접근할 수 있는 객체의 클래스 타입(코틀린) : class java.lang.String (Kotlin reflection is not available)
str1을 통해 접근할 수 있는 객체의 클래스 타입(자바) : class java.lang.String

test1의 클래스 타입(코틀린) : class TestClass (Kotlin reflection is not available)
test1의 클래스 타입(자바) : class TestClass
  • 프로젝트 생성시 선택하는 Maven, Gradle -> 배포할 때
  • 안드로이드 기반이 Gradle이여서 Gradle을 사용
  • 코틀린에서 reflection 기능 라이브러리로 따로 빼고 있어서 이를 제대로 쓸 수 있게 지원하고 있음



※ 출처 : 멋쟁이사자 앱스쿨 2기, 소프트캠퍼스 
profile
안드로이드공부

0개의 댓글