Kotlin문법들 (Kotlin vs Java --> Kotlin의 장점) - NullSafe, ( apply, with, let, also, run ), Data Class, Lamda expression(람다함수), lateinit, lazy, setter와 getter, companion object

하이루·2021년 12월 13일
0

kotlin 공식 홈페이지 : https://kotlinlang.org/

NullSafe기능

--> a는 Integer 객체를 사용했으므로 Nullable
--> b는 Int?로 선언했으므로 Nullable
--> c는 Int로 선언했으므로 NotNull --> 기본값

-----------------> 해당 값들이 아래의 예시에 쓰임

기존의 java 코드

--> 위와 같이 null일 경우에 대비하여 일일이 코드상에서 처리해줘야함

Kotlin 코드

--> 위와 같이 NullSafe기능 덕분에 Null로 발생할 문제가 크게 줄어듬

위의 b와 같이 변수 옆에 ?를 해주면 이 변수가 null일 경우에 코드상에서 무시하고 진행됨

--> b는 nullable하므로 null이 들어갈 수 있지만 위와같이 b?로 사용하면 b의 값이 null일 경우 코드상에서 해당부분을 무시하고 진행한다.

null이 될 수 있는 type

--> kotlin은 null이 될 수 있는 타입을 명시적으로 표시할 수 있음

 val a: Int? = null

위의 코드에서 만약

if (s != null)

부분을 빼고 코딩했다면 nullSafe오류가 발생함 -> null에 대해 그만큼 철저함


null safe operator -> ?.

이후 메소드를 다룰때 null을 안전하게 처리하기 위해 코틀린은 ?.연산자를 지원함

 s?.length 
// s가 null일 경우 s 뒤에 있는 확장함수가 실행되지 않음 --> 바로 null을 반환함
// s가 값이 있을 경우 s 뒤에 있는 확장함수가 정상적으로 실행됨

위의 코드에서 s에 ? 가 없다면 nullsafe오류가 발생함


Elvis operator -> ?:

위에서 알아보았던 ?. 연산자는 좌항이 null이면 null을 반환함

그런데 코드를 작성하는 중에 해당 값이 null인 경우 default값을 주고 싶은 경우가 생김

 (위의 ?.의 경우ㅡ, null이면 바로 null을 반환함 )

이런 경우에 ?: 연산자를 사용하면 됨

예제 1)

해당 코드의 경우

  • s 의 값이 null 이면 "NoName"이 반환되고,
  • s 의 값이 null이 아니면 s의 값이 반환됨

예제 1-1) -> 예제 1)과 같은 의미를 가지고 있음

예제 2) --> ?:의 우항으로 return이나 throw를 넣어줄 수도 있음

위의 코드의 경우

  • 우항에 return이 들어갈 경우, s가 null이면 return이 실행됨 --> 리턴값 반환
    ( 위의 메소드는 리턴값이 없으므로 Function 메소드가 종료됨 )
  • 우항에 throw ~~ 부분이 들어갈 경우, s가 null이면 오류메세지를 출력함

Scope Function ( apply, with, let, also, run ) --> 범위함수

람다식을 사용하여 호출을 하면, 람다안에서 일시적으로 범위라는 것이 형성됨
--> 이 범위안에서 람다를 부른 객체나 수신 객체등에 자유롭게 접근할 수 있게 해줌

코드를 좀 더 읽기 쉽게 만들어주는 목적

Java와 Kotlin비교

Apply 함수 --> 보통 객체의 초기화에 사용되는 함수

--> apply함수 바로 앞에 있는 객체를 뒤에 블록으로 열어서 접근할 수 있음

기존의 Java 함수

--> 객체 생성 후, 객체의 변수값에 일일이 넣어줌

apply함수를 사용한 Kotlin함수

apply함수는 함수 안에서 this로 해당 객체를 부를 수 있음
--> 여기서 this는 생략 가능하므로
this.firstName 뿐만 아니라 firstName으로,
this.lastName 뿐만 아니라 lastName으로 해당 객체의 변수에 접근 가능한 것

또한 apply함수는 해당 객체 자신을 return해줌

     --> 즉, 위의 코드는 객체를 생성함과 동시에 객체의 변수에 값을 넣고ㅡ( 초기화 ) 
     이후에 이렇게 초기화한 객체 자신을 반환( return )하여 객체 변수에 할당하는 과정인 것
    

Also 함수 --> 주로 객체 유효성을 판단하거나, print함수를 이용한 디버깅에 사용하는 함수

--> 객체의 메소드에 대한 파라미터(return값)를 받아 람다함수를 만드는 함수

기존의 Java 함수

--> 객체의 메소드를 value변수에 넣고, 이를 실행하고 있음

also함수를 사용한 Kotlin함수

--> 객체의 메소드에 대한 return값를 람다함수에서 우리가 명시한 value라는 파라미터에 받아 람다 함수의 내용을 실행함

--> 위의 코드와 같이 파라미터를 받을 매개변수를 생략하면 "it" 이라는 명칭으로 파라미터에 접근할 수 있음

apply와 마찬가지로 객체가 반환되게 됨


Let 함수 --> Nullable하게 선언한 객체에 대해 NullSafe하게 람다함수를 사용하기 위해 사용 ( 자주 사용 )

--> null이 아닌 객체에 대해서 람다함수를 실행시키기 위한 함수

--> let함수의 경우, 위의 apply, also 함수들과 다르게 객체가 아닌 실행부에 있는 람다함수의 결과값을 반환한다.

기존의 Java 함수

--> NullSafe기능이 없기 때문에 number가 null이 아님을 확인 한 다음, sumNumberStr 변수에 값을 할당하고 있음

let 함수를 이용한 Kotlin함수

--> Nullable하게 선언한 Int자료형 number에 대해서, NullSafe하게 람다함수를 실행시키기 위해 let함수를 사용하고 있음

--> let 함수의 실행부에서 람다함수의 매개변수명을 따로 지정해주지 않았기 때문에 ( 매개변수를 생략했기 때문에 ) "it" 키워드를 통해 객체의 반환값에 접근하여 람다함수를 실행함

        --> 위의 경우 int형의 객체 number에 대해 number가 할당하고 있는 값이 반환값이다.

========> let 함수의 경우, 위의 코드와 같이

( 객체명?.함수() -----> 객체의 값이 null일 경우 함수를 실행시키지 않고 진행 )

객체명?.let { 람다함수실행부 }

의 형태로 자주 쓰이는데,
논리적으로
1. 객체가 null일 경우ㅡ, 뒤에 let 함수가 실행되지 않으며, ( 이 경우 문제 발생 여지가 있다. )

       --> 이 경우 null값에 해당하는 number객체 뒤의 let함수가 그냥 생략되는 판정이기 때문에
       sumNumberStr = number로 되어서 sumNumber = null이 된다. 
       
       --> 하지만 sumNumber는 NotNull 이므로 이대로 간다면 오류가 발생한다.
       
       --> 이것은 NullSafe에 대해서 문제를 발생시킬 수 있으며
       바로 아래 부분에 이에 대한 해결 방안이 있다. 
       
  1. 객체가 null이 아닌 경우, 그 값을 받아 let의 람다함수가 실행되게 된다.

NullSafe를 지키면서 let 함수 사용 --> 보통 이 방법으로 let함수를 사용하게 됨

--> NullSafe기능을 지키면서 let을 사용하는 방법이다.

기존의 Java 함수

--> number의 값이 null인지 아닌지 확인하며,
만약 number의 값이 null일 경우 sumNumberStr에 빈값을 넣어준다.

let 함수를 이용한 Kotlin함수

--> 맨 뒤 부분에 orEmpty()함수를 붙여주어,
만약 number의 값이 null이여서 let함수가 생략되었을 경우 orEmpty()함수를 통해 빈값을 리턴해준다.

즉, 위의 let 사용법에서는 number = null일 경우 sumNumberStr은 null이 할당되게 되지만,
여기서의 let 사용법에서는 number = null일 경우 sumNumberStr은 ""와 같은 빈값이 들어가게 된다.

With 함수 --> 객체에 있는 함수들을 실행할 때 사용

--> 아래의 run 함수와 비슷하지만, with 함수는 단일함수 형태로 사용

기존의 Java 함수

--> 기존의 java에선 먼저 객체를 생성한 후, 변수에 담음
이후 객체가 가진 변수나 메소드를 하나하나 참조하여 실행하고 있음

with 함수를 이용한 Kotlin함수

--> kotlin에서도 먼저 객체를 생성한 후 변수에 담음
이후 with 함수를 사용, with 함수의 파라미터로 생성한 객체를 주는 것으로,
객체가 가진 메소드나 변수에 this를 사용하여 접근할 수 있음 ( 위 코드에서는 this. 을 생략한 것 )

--> 또한 with 함수의 반환값은 실행부( {}부분 )에 해당하는 람다함수의 결과값이므로 값을 저장할수도 있다.


Run 함수 --> 객체의 구성( 초기화 )과 계산이 동시에 진행될 경우 사용

--> with 함수와 비슷하지만, run 함수는 확장함수 형태로 사용

기존의 Java 함수

--> 객체의 변수 초기화 해준 후, 객체의 메소드를 사용해서 값을 리턴

Run 함수를 이용한 Kotlin함수

--> 확장함수로 사용되어 람다함수에서 객체의 변수 초기화와 메소드 사용을 동시에 하고 있음
--> run함수는 this를 통해서 객체의 구성에 접근할 수 있으며, 리턴값은 람다함수의 결과값임
( this는 생략 가능 )

--> 여기서 람다함수의 반환값은 query()의 반환값임


Data Class --> Model_Class( 데이터를 담는 역할을 하는 Class )를 만들 때 손쉽게 만들게 해줌

--> 기존의 java에서 Model_Class( 데이터를 담는 역할을 하는 class )를 만들 때,
항상 만들었던 생성자, getter, setter,copy, toString, hashcode등등을 자동으로 만들어줌

   Kotlin의 경우, 객체의 변수에 직접 접근하는 방식을 사용하기 때문에 
   따로 getter, setter가 필요하지 않지만,
   kotlin코드를 java환경에서
   사용할 때를 위해서 Data Class는 getter, setter까지 만들어준다.

기존의 Java에서 Class 생성

--> String 타입 변수 1개를 객체에 담기 위해 많은 코드를 짜내야만 함

Kotlin에서의 Data Class를 이용한 Class 생성

--> Model_Class( 데이터를 담는 역할을 하는 class )를 만들 때, 어떤 변수들로 구성할지만 결정하면, 필요한 메소드를 자동으로 만들어 줌

Data Class를 통해 getter, setter가 만들어지는 조건

--> 위의 코드와 같이 변수의 타입이 val일 경우, 상수타입( 값이 변하지 않는 타입 )이므로 setter는 생성되지 않고, getter만 생성되게 된다.

--> 이와 다르게 변수의 타입이 var일 경우, 변수타입( 값을 변경할 수 있는 타입 )이므로 setter와 getter가 모두 생성된다.


Lamda expression --> 람다식을 쉽게 이용할 수 있도록 해주는 표현법

람다 함수란?

함수에 함수의 리턴값을 전달해서, 전달받은 함수에서 함수를 실행시키는 것

기존의 java에서의 메소드 사용법

--> setonClickListener함수를 통해서 View에 있는 onClickListener를 구현하게 되고, onClickListener 안에 있는 onClick이라는 메소드를 구현한 구현체를 button에 넘기게 된다.
이후, button에서 클릭이 일어났을 때 해당 onClick메소드를 실행하게 된다.

Kotlin에서 람다함수를 이용한 사용법

--> 인터페이스( interface )에 메소드가 1개밖에 없는 경우에는 위와 같이 간단하게 람다식으로 구현해줄 수 있음

 람다식의 파라미터가 1개일 경우 생략 가능하므로, 
 최종적으로는 위 코드의 "v ->" 부분까지 생략하여 "it"키워드로 접근 가능하게 된다.

lateinit, lazy init --> NotNull한 변수의 초기화 문제 해결

--> 변수자체는 NotNull이지만 코드상 나중에 값이 들어갈 경우 초기화되지 않는 문제해결

NullSafe한 코드를 사용하기 위해서 NotNull한 변수 선언

하지만 초기값이 없는 변수를 초기화해야하는 문제 발생
--> 초기값이 없으면 변수 선언 자체가 문제가 생김

lateinit

Nullable한 변수의 사용

NotNull한 변수의 선언에서, 나중에 초기화 하기 위해 lateinit 사용

--> NotNull한 변수의 선언시 앞에 lateinit 선언을 해주는 것으로
나중에 값을 할당할 것임을 코드에 알림

--> 나중에 반드시 값을 할당해줘야함 ( 값 할당 안하면 오류 발생 )

--> lateinit의 경우
지역변수의 선언엔 사용불가
전역변수의 선언에서만 사용할 수 있음

lazy

--> lazy 또한 lateinit과 같이 NotNull한 변수를 나중에 초기화 시키기 위해 사용함

--> lazy의 경우 해당 변수를 사용하기 전까진 할당되어있지 않다가
해당 변수가 사용되는 순간 할당해줌 -------------> ( 매우 중요한 특징 )

  lazy init의 경우, 사용되기 전까진 값이 할당되지 않기 때문에 
  나중에 View를 그린다거나 할 때ㅡ, lazy선언한 변수를 사용하지 않을 경우
  아직 값이 할당되어 있지 않아 View가 제대로 그려지지 않는다던가 하는 문제가 발생할 수 있다.
  
  --> 따라서 굳이 lazy init 할 필요 없을 경우는 그냥 초기화해서 사용하자
  

Setter 와 getter

참고 자료 : https://bbaktaeho-95.tistory.com/27

Setter와 Getter에 대한 기본 상식

kotlin 환경에서
최상위 변수( 메소드, 클래스의 외부에 정의된 변수 )
클래스의 멤버 변수로 선언하면
속성(attribute)으로 간주된다.

이때 클래스의 멤버변수는 모두 private제한자로 지정되게 되는데,
따라서 해당 클래스 내부의 getter와 setter를 통해서만 해당 클래스의 속성을 참조 할 수 있다.

val a : String = "최상위" //최상위 변수 --> val로 지정 --> getter만 생성

fun main() {
    val She = Human("백지연")
    val He = Human()
}

class Human
{
    var name: String  //클래스의 멤버 변수 --> var로 지정 --> setter, getter 생성
    var hobby: String  //클래스의 멤버 변수  --> var로 지정 --> setter, getter 생성
    var age: Int  //클래스의 멤버 변수  --> var로 지정 --> setter, getter 생성

    constructor(name: String = "김철수", hobby: String = "축구", age: Int = 25)
    {
        this.name = name
        this.hobby = hobby
        this.age = age
    }
}

위의 코드에서 클래스의 속성을 가져오는 기능을 getter라고 하고ㅡ,
클래스의 속성의 값을 변경하는 기능을 setter라고 한다.

kotlin의 경우, 편의성을 위해 getter와 setter가 자동생성되게 되며
아래와 같이 속성을 다루듯이 getter와 setter를 비 명시적으로 불러낼 수 있다.

getter --> get() {}

아래의 예시와 같이 기본적으로 getter는 비 명시적으로 자동 호출되어 값을 가져온다.

당연하게 생각했던 호출 과정이지만
사실 내부에선 get속성명() 으로 getter가 생성되어 동작한다.

따라서 getter 호출마다 추가적인 처리가 필요할 때는 getter를 변경해야 한다.
kotlin에서는 변수 바로 아래에 get(){} 을 사용하여 해당 변수의 getter의 내용을 정의해 줄 수 있다.

//////////////////////////////////////////////////////////////

get()을 이용하여 getter의 내용 정의하기

--> 아래와 같이 변수 아래에 get(){} 을 통해 getter를 설정해 놓으면,
해당 변수가 호출될 때 get(){}의 블록부분이 실행된다.

위의 코드를 보면 알 수 있는 것은
우리가 getter를 따로 명시하지 않았을 경우 호출되는 getter의 기본적인 내용은 다음과 같다고 볼 수 있다.

      get()
        {
            return field
        }
  • get()은 getter로써 해당 변수가 호출되면 실행된다.
    이때 get()의 리턴값이 해당 변수가 호출되었을 때 리턴되는 값이 된다.

  • 위에 getter의 내용을 정의하는 예시는 이런 기본 형태의 get()을 변형하여 활용한 것이다.

  • setter는 초기값에 대해서는 적용되지 않는다.

/////////////////////////////////////////////////////////////

여기서 field 키워드는 해당 get()의 내부에서 해당 getter에 대한 속성값을 가져오거나 변경할 수 있게 해준다.
예를 들어,

var name: String = "소지섭"
// 초기값에 대해서는 setter가 호출되지 않는다 -> setter는 값을 수정하는 경우에 실행됨

      get()
        {
            // field = "하정우" --> 이 경우 name에 "하정우"가 할당된다. ,, field키워드이므로 setter를 호출하지 않음
            return field
        }
  • 여기서 field 키워드는 변수 name의 값을 가져오며ㅡ, 위의 코드에선 "소지섭"을 가져올 것이다.

  • 동시에 field 키워드에 값을 할당( 위의 "하정우" 할당하는 부분처럼 )하면 그것은 name의 값이 된다.

여기서 field 키워드의 필요성에 대한 이야기를 하자면,

  • get() 내부에서 field 키워드로 변수에 접근할 경우ㅡ,
    아무 문제 없이 해당 변수의 값에 접근하고 또 변수의 값을 변경할 수 있다

  • get() 내부에서 변수명( 위의 코드에선 name )으로 직접 접근할 경우ㅡ,
    해당 변수는 호출시 getter을 호출하게 되며, get()의 내부에서 get()을 호출하는 재귀호출의 형태가 된다.
    이런 형태는 결과적으로 무한루프에 빠지게 되므로 이렇게 사용하지 않는 것이다.

따라서 get() 안에서 변수의 값에 접근할 때는 field 키워드를 사용하는 것이다.
( field 키워드를 통해 getter와 setter 내부에서 setter나 getter를 호출하지 않고 변수의 값을 수정하든지, 변수의 값을 가져오든지 할 수 있음 )

setter --> set() {}

아래의 예시와 같이 setter는 비 명시적으로 자동 호출되어 값을 수정한다.

당연하게 생각했던 호출 과정이지만
사실 내부에선 set속성명(파라미터명1) 으로 setter가 생성되어 동작한다.

따라서 setter 호출마다 추가적인 처리가 필요할 때는 setter를 변경해야 한다.
kotlin에서는 변수 바로 아래에 set(){} 을 사용하여 해당 변수의 setter의 내용을 정의해 줄 수 있다.

//////////////////////////////////////////////////////////////

set()을 이용하여 setter의 내용 정의하기

--> 아래와 같이 변수 아래에 set(){} 을 통해 setter를 설정해 놓으면,
해당 변수의 값이 수정될 때 set(){}의 블록부분이 실행된다.

위의 코드를 보면 알 수 있는 것은
우리가 setter를 따로 명시하지 않았을 경우 호출되는 setter의 기본적인 내용은 다음과 같다고 볼 수 있다.

      set(value)
        {
            field = value
        }
  • set()은 setter로써 해당 변수의 값이 수정될 때 실행된다.
    이때 해당 변수에 들어갈 값( name = "고길동" 에서 "고길동" 부분 )이 set()의 파라미터(value)로 들어오게 된다.

  • 이후 파라미터(value)로 들어온 값을 field에 넣는 것으로 해당 변수의 값이 수정된다.

  • 위에 setter의 내용을 정의하는 예시는 이런 기본 형태의 set()을 변형하여 활용한 것이다.

/////////////////////////////////////////////////////////////

여기서 field 키워드는 해당 set()의 내부에서 해당 setter에 대한 속성값을 가져오거나 변경할 수 있게 해준다.
예를 들어,

var name: String = "소지섭"
      set(value)
        {
            field = value 
            // 파라미터(value)로 "소지섭"이 들어와 name에 "소지섭"이 할당된다.
            // 이때 field키워드이므로 setter를 호출하지 않음
            
            //field = "장동건" --> 이 경우 name에 무엇을 넣든 name의 값은 "장동건"이 되어버린다.
            
        }
  • 여기서 field 키워드에 값을 할당하면 그것은 name의 값이 된다.

여기서 field 키워드의 필요성에 대한 이야기를 하자면,

  • set() 내부에서 field 키워드를 통해 값을 할당할 경우ㅡ,
    아무 문제 없이 해당 변수의 값에 접근하고 또 변수의 값을 변경할 수 있다

  • set() 내부에서 변수명( 위의 코드에선 name )에 직접 할당할 경우ㅡ,
    해당 변수는 호출시 getter을 호출하게 되며, 변수에 값을 할당하는 부분에서 setter를 호출하게 되어,
    set()의 내부에서 set()을 호출하는 재귀호출의 형태가 된다.
    이런 형태는 결과적으로 무한루프에 빠지게 되므로 이렇게 사용하지 않는 것이다.

따라서 set() 안에서 변수의 값에 접근할 때는 field 키워드를 사용하는 것이다.
( field 키워드를 통해 getter와 setter 내부에서 setter나 getter를 호출하지 않고 변수의 값을 수정하든지, 변수의 값을 가져오든지 할 수 있음 )


Companion object --> class내에 모든 인스턴스가 공유할 클래스

--> Java에서의 static과 비슷한 개념이다.

Class 내부에 companion object를 선언해놓으면, 해당 객체는 class의 모든 인스턴스에게 접근권한을 준다.

companion object의 이름은 생략될 수 있으며, 그럴 때엔 Companion이란 식별자를 사용한다.

이 때 중요한 것은 이렇게 정의된 객체는 싱글톤으로 class내에서 유일하다.

또한 Class 내에서 companion object는 한개만 정의될 수 있다.

Companion object의 예시


......
class a {

......


    companion object{

        private const val MAX_EMPLITUDE = Short.MAX_VALUE // 32767 --> 진폭 최대값 지정
        private const val DATA_PATH = "고정된 데이터 주소"
        private const val REQUEST_RECORD_AUDIO_PERMISSION = 201
    }
    
}

--> companion object를 통해 이름을 생략한 객체를 만들어준 모습이다.
--> 해당 객체를 통해 전역 변수들을 선언해 주었다.

profile
ㅎㅎ

0개의 댓글