이번 글에서는 안드로이드 앱 개발 과정에 매주 자주 사용되는 몇가지 문법들에 대해 간략하게 소개하고자 합니다.
1)이너클래스
2)인터페이스
3)익명클래스
4)static 의 기능인 companion object[동반객체]
5)늦은초기화 - lateinit, by lazy
1)~3) 까지는 자바에도 존재하는 문법이기에 코틀린 언어의 문법적 차이점을 위주로 살펴보시기 바랍니다.
4)~5) 는 자바에는 없던 새로운 문법이니 다소 생소하겠지만 앱 개발과정에 자주 사용되고 중요하니 주의깊게 보시길 바랍니다.
그럼, 위 5가지의 문법을 알아보도록 하겠습니다.
새로운 코틀린 파일 Test08OOP8.kt 를 만들고 문법을 알아보겠습니다.
코틀린 프로그램의 시작은 언제나 main() 함수이니 만들고 시작하겠습니다.
1. 이너 클래스 Inner class
클래스 안에 설계하는 클래스를 이너 클래스(inner classs)라고 부릅니다. 이너 클래스는 다음의 몇가지 특징을 가지고 있습니다.
- 이너클래스는 감싸고 있는 아웃터 클래스 밖에서는 인식되지 않는다. 인식하려면 아웃터클래스명.이너클래스명 으로 작성함.
- 이너클래스는 아웃터 클래스 밖에서 객체를 생성할 수 없음.
- 이너클래스는 아웃터 클래스의 멤버를 곧바로 사용할 수 있음.
코틀린의 이너클래스가 자바와 다른 점은 inner 키워드를 사용해야 한다는 겁니다. 안써도 에러는 아니지만 아웃터의 멤버를 사용할 수 없기에 이너클래스라고 볼 수 없는 클래스가 됩니다.
AAA 클래스 안에 BBB 라는 이름의 이너클래스를 만들어 보겠습니다.
//시작 함수
fun main(){
}//main 함수 종료..
//1. 클래스안에 inner 클래스만들기
class AAA{
var a:Int=0
fun show(){
println("AAA클래스이 show")
println()
}
//이너클래스 [ 자바와 다르게 inner키워드가 없으면 아웃터클래스의 멤버를 마음대로 사용할 수 없음 ] ////////////////
inner class BBB{
fun show(){
println("BBB 클래스의 show")
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
}
이너클래스의 특징을 하나씩 확인해 보겠습니다.
1.1) 이너클래스는 감싸고 있는 아웃터 클래스 밖에서는 인식되지 않는다. 인식하려면 아웃터클래스명.이너클래스명 으로 작성함.
당연하게도 이너 클래스는 아웃터 클래스 안에 숨어 있는 설계도면 같은 것 이기에 그냥은 인식될 수 없겠죠. 아웃터 클래스 안에 있는 멤버를 사용하듯이 . 연산자로 안에 있다는 것을 명시적으로 표시해야 합니다.
//시작 함수
fun main(){
//1. 이너클래스
val bbb: BBB //ERROR - BBB 이너클래스를 인식하지 못함
val bbb: AAA.BBB //OK - 아웃터 클래스명 AAA를 통해 인식 가능
}//main 함수 종료..
1.2) 이너클래스는 아웃터 클래스 밖에서 객체를 생성할 수 없음.
아웃터 클래스명을 통해 인식은 가능하더라도 아웃터 클래스 밖에서는 객체를 생성할 수 없습니다.
//시작 함수
fun main(){
//1. 이너클래스
//val bbb: BBB //ERROR - BBB 이너클래스를 인식하지 못함
//val bbb: AAA.BBB //OK - 아웃터 클래스명 AAA를 통해 인식 가능
val bbb: AAA.BBB = AAA.BBB() //ERROR - 아웃터 클래스 밖에서는 객체 생성 불가
}//main 함수 종료..
그럼 이너클래스는 어떻게 객체를 생성할 수 있을 까요? 이너 클래스는 아웃터 클래스가 존재할 때만 만들도록 강제하고자 하는 목적이 있기에 아웃터 클래스에서만 만들 수 있도록 하였습니다. 즉, 아웃터 클래스 안에서는 이너클래스를 생성할 수 있다는 것 입니다. 그래서 보통 아웃터 클래스 안에 이너 클래스 객체를 생성하여 리턴 해주는 기능 메소드를 정의하고 이를 호출하여 이너클래스 객체를 얻어옵니다. 앱 개발에 사용되는 APIs 중에도 이런 식으로 객체를 얻어오는 기능이 아주 많습니다.
AAA 클래스 안에 BBB 이너클래스 객체를 생성하여 리턴해주는 기능 메소드 getBBBInstance() 를 직접 정의해 호출해 보겠습니다.
//시작 함수
fun main(){
//1. 이너클래스
//val bbb: BBB //ERROR - BBB 이너클래스를 인식하지 못함
//val bbb: AAA.BBB //OK - 아웃터 클래스명 AAA를 통해 인식 가능
//val bbb: AAA.BBB = AAA.BBB() //ERROR - 아웃터 클래스 밖에서는 객체 생성 불가
val obj: AAA = AAA() // 아웃터 객체 생성
val obj2: AAA.BBB = obj.getBBBInstance() //이너클래스 객체를 생성하여 리턴해주는 메소드 이용
obj2.show()
}//main 함수 종료..
//1. 클래스안에 inner 클래스만들기
class AAA{
var a:Int=0
fun show(){
println("AAA클래스이 show")
println()
}
// BBB 이너클래스를 객체로 생성하여 리턴해주는 메소드
fun getBBBInstance() : BBB{
return BBB()
}
//이너클래스 [ 자바와 다르게 inner키워드가 없으면 아웃터클래스의 멤버를 마음대로 사용할 수 없음 ] ////////////////
inner class BBB{
fun show(){
println("BBB 클래스의 show")
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
}
//**출력**
BBB 클래스의 show
이 특징은 이너클래스를 아웃터 클래스의 종속관계로 함으로서 개발자가 실수로 아웃터 없이 이너객체를 생성하는 실수를 방지 해 줍니다.
예를 들어 다이얼로그 안에 있는 전용 버튼처럼 다이얼로그가 없는데 전용 버튼만 따로 만들어 지게 하지 않고 싶을 때 전용 버튼을 다이얼로그 이너 클래스로 만들어 사용합니다.
1.3) 이너클래스는 아웃터 클래스의 멤버를 곧바로 사용할 수 있음.
이너 클래스를 사용할 때 개발자에게 아주 매력적인 특징으로서 아웃터의 멤버를 내 것인양 사용할 수 있다는 것은 프로그래밍 과정에서 주입코드를 작성할 필요 없이 아웃터의 멤버를 제어할 수 있어 종속기능을 수행하기 용이합니다.
잘 생각해보면 이너클래스를 작성하는 위치가 아웃터 클래스 영역안에 있는 멤버변수, 멤버함수와 같은 위치에 있기에 멤버라고 볼 수 있습니다. 그렇기에 다른 멤버를 사용할 수 있는 것은 어찌보면 당연한 문법적 허용입니다.
또한, 아웃터 객체없이는 이너클래스 객체가 생성될 수 없는 1.2) 특징이 있었기에 이너 안에서 사용하는 멤버는 반드시 존재할 수 밖에 없기에 다른 클래스의 멤버지만 안전하게 사용이 가능합니다.
BBB 이너클래스의 show() 기능 메소드를 수정하여 아웃터의 멤버변수(프로퍼티)를 사용해 보겠습니다.
//시작 함수
fun main(){
//1. 이너클래스
//val bbb: BBB //ERROR - BBB 이너클래스를 인식하지 못함
//val bbb: AAA.BBB //OK - 아웃터 클래스명 AAA를 통해 인식 가능
//val bbb: AAA.BBB = AAA.BBB() //ERROR - 아웃터 클래스 밖에서는 객체 생성 불가
val obj: AAA = AAA() // 아웃터 객체 생성
val obj2: AAA.BBB = obj.getBBBInstance() //이너클래스 객체를 생성하여 리턴해주는 메소드 이용
obj2.show()
}//main 함수 종료..
//1. 클래스안에 inner 클래스만들기
class AAA{
var a:Int=0
fun show(){
println("AAA클래스이 show")
println()
}
// BBB 이너클래스를 객체로 생성하여 리턴해주는 메소드
fun getBBBInstance() : BBB{
return BBB()
}
//이너클래스 [ 자바와 다르게 inner키워드가 없으면 아웃터클래스의 멤버를 마음대로 사용할 수 없음 ] ////////////////
inner class BBB{
fun show(){
println( "부모클래스의 프로퍼티 a : $a ") //부모의 멤버를 마음대로
//아웃터 클래스의 this사용법이 Java와 다름!! [ this@부모클래스명 ]
//아웃터클래스의 show를 호출하고 싶다면..
this@AAA.show();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
}
//**출력**
부모클래스의 프로퍼티 a : 10
AAA클래스이 show
이너 클래스 안에서 아웃터 클래스와 같은 이름의 show()메소드가 존재하기에 이너 클래스 안에서 부모클래스를 명시적으로 구분하여 호출하고 싶다면 부모클래스.this 가 필요합니다. 다만, 코틀린에서는 이 부모 클래스의 this 참조변수를 this@부모클래스명 으로 Label 문법으로 작성하는 것이 차이가 있습니다.
앱 개발 할 때 버튼 이벤트 처리에 사용되는 익명클래스 안에서 Toast 같은 작업에 사용되었던 MainActivity.this 를 코틀린에서는 라벨 문법을 사용하여 this@MainActivity 를 쓰는 것도 이와 같습니다.
2. 인터페이스 interface
인터페이스는 자바와 다른 특징은 없습니다.
잘 알다시피 인터페이스는 규격을 만들기 위한 목적이기에 기능 코드가 작성되지 않은 이름만 있는 추상 메소드만 가지는 클래스 입니다.
// 인터페이스는 특별할 것 없음
interface Clickable{
//추상메소드
fun onClick()
}
코틀린이라고 다를 것 없이 인터페이스는 곧바로 객체를 생성할 수 없습니다.
//시작 함수
fun main(){
//2. 인터페이스
val c= Clickable() //ERROR :인터페이스는 곧바로 생성(new)할 수 없어서..
}//main 함수 종료..
//2. 인터페이스는 특별할 것 없음
interface Clickable{
//추상메소드
fun onClick()
}
인터페이스를 사용하려면 이름만 있는 추상메소드를 구현하는 class 를 만들어서 기능을 완성한 후 객체를 생성하여 사용합니다.
인터페이스를 사용함으로서 이 인터페이스를 구현한 클래스의 객체들은 반드시 추상메소드를 구현해야만 하기에 같은 이름의 기능메소드를 가질 수 밖에 없도록 강제합니다.
자바 앱 개발을 해보셨다면 이미 익숙하셨을 Button 의 클릭 이벤트 처리에 반응하는 리스너(Listener)가 인터페이스로 되어 있지요. 안드로이드에서 리스너를 인터페이스로 설계하여 제공하였기에 어떤 개발자든 버튼이 클릭되면 onClick() 메소드가 발동 하도록 코드를 작성할 수 밖에 없기에 유지관리가 용이합니다. 어떤 버튼은 onPress, 어떤 버튼은 onDown, 어떤 버튼은 onClick.. 등으로 매번 다르게 만들면 유지보수 하는 입장에서 해당 이벤트 처리 코드를 찾기 어려웠을 겁니다. 그렇기에 onClick()이라는 이름의 추상메소드를 가진 리스너 인터페이스를 구현하는 클래스를 만들고 onClick() 추상메소드의 기능을 해당 버튼에 맞게 만들면 규격화된 프로그래밍이 가능합니다.
그래픽적으로 화면을 구현하는 GUI 프로그래밍에 아주 많이 사용되는 문법입니다.
그럼. 위에서 안드로이드의 클릭 리스너와 비슷한 느낌으로 설계한 Clickable 이라는 인터페이스를 구현(implement)하는 Test 라는 이름의 클래스를 만들고 추상메소드 onClick()를 구현하고 사용해 보겠습니다.
자바와 문법적으로 다른 점
- 인터페이스를 구현하는 키워드 implement 대신에 코틀린의 상속처럼 콜론 : 키워드 사용.
- 상속때와 다르게 부모클래스의 위치에 있는 인터페이스 뒤에 생성자() 호출을 하지 않음. (인터페이스는 객체생성이 안되니 당연하겠죠.)
//시작 함수
fun main(){
//2. 인터페이스
//val c= Clickable() //ERROR :인터페이스는 곧바로 생성(new)할 수 없어서..
//clickable을 구현한 Test클래스 객체 생성
val t= Test()
t.onClick()
}//main 함수 종료..
//2. 인터페이스는 특별할 것 없음
interface Clickable{
//추상메소드
fun onClick()
}
//인터페이스를 구현하는 클래스 [ 클래스 상속과 문법적으로 표현법이 비슷해보이지만 주생성자 호출()문이 없음 : (다중 구현할때는 , 를 사용) ]
class Test : Clickable{
//추상메소드 구현 - 기능구현도 이미 Clickable에 있는 함수명과 같은 함수를 만드는 만큼 override 키워드 필요
override fun onClick() {
println("click!!!!")
println()
}
}
//**출력**
click!!!!
자바의 경우 상속할 때는 extends, 구현할 때는 implement 를 사용해야 해서 구별이 명확하지만 코드가 번거로운 반면에 코틀린의 경우에는 클래스 상속이나 인터페이스 구현이나 모두 콜론 : 키워드로 같기에 구별이 확 되지는 않지만 코드가 간결합니다. 그래서 코틀린의 경우 상속인지 구현인지를 코드 상으로 구별할 때 부모클래스 옆에 생성자 호출문이 있느냐 없느냐로 1차 구분합니다. 물론, 클래스 상속이어도 보조 생성자 사용으로 인해 생성자 호출문이 생략될 수도 있지만 대부분의 경우 구별이 되는 편입니다.
결국, 인터페이스를 사용하려면 추상메소드를 구현하는 클래스를 매번 설계하고 객체를 생성하여 사용해야 합니다.
근데. 이런식이면 만약 버튼이 10개 있다면 버튼 리스너 인터페이스를 구현하는 10개의 클래스를 설계해야 합니다. 물론, 그래도 되기는 하지만 클래스 이름 10가지를 만드는 것도 꽤 애를 먹게 되고 코드도 지저분해 집니다. 그래서 앱 개발에 아주 많이 필요한 문법이 이름이 없는 클래스를 만드는 익명클래스 입니다.
3. 익명클래스 Anonymous class
익명클래스는 객체를 생성할 때 설계하는 클래스를 말합니다. 그러다 보니 별도의 클래스명을 가지지 않습니다. 그래서 익명 클래스 라고 부릅니다.
코틀린에서 익명클래스를 만드는 문법은 자바와 많이 다릅니다. 객체를 영어로 표시한 object 키워드를 통해 객체를 생성합니다. 그 옆에 인터페이스 구현 기호인 콜론 : 을 통해 인터페이스를 구현합니다. 위에서 소개했던 Clickable 인터페이스를 구현하는 Test 클래스를 만들 때 하는 방법과 비슷하되 다른 점은 클래스명 대신에 object 키워드를 사용하는 겁니다.
정리하면 Clickable 인터페이스를 구현하는 클래스를 만들면서 객체까지 생성하는 것이 익명클래스입니다.
//시작 함수
fun main(){
//2. 인터페이스
//val c= Clickable() //ERROR :인터페이스는 곧바로 생성(new)할 수 없어서..
//3. 익명클래스 [ Java와 문법이 많이 다름 : object키워드를 사용해야만 함 ]
val a= object : Clickable{ //[ ()없는 것 주의!!!] -- Clickable 인터페이스 구현
override fun onClick() {
println("Anonymous class onClick!!!!!")
println()
}
}
a.onClick()
}//main 함수 종료..
//2. 인터페이스는 특별할 것 없음
interface Clickable{
//추상메소드
fun onClick()
}
//**출력**
Anonymous class onClick!!!!!
익명클래스는 안드로이드 앱 개발에서 매우 많이 사용하게 되니 연습은 앱 개발에서 충분히 해보도록 하고 이 글에서는 문법적 소개만 하고 넘어 가겠습니다.
4. 동반 객체 companion object
혹시 자바의 static 키워드를 기억하시나요? 미리 메모리에 만들어 놓기에 정적 멤버를 만들 때 사용하는 키워드 였습니다.
클래스의 멤버들은 객체를 생성하지 않으면 사용할 수 없습니다. 이 때 static 키워드가 붙은 정적 멤버는 객체 생성 없이 사용이 가능했습니다. 다만, 멤버이기 때문에 바로 인식은 안되고 클래스명을 통해 정적멤버를 인식하도록 하였습니다.
public class Hello{
public static void main(String[] args){
System.out.println("인스턴스 변수 a : " + a); //ERROR - a 변수 인식안됨, 또한 Sample 객체를 생성해야만 사용가능
System.out.println("static 변수 b : " + b); //ERROR - static 변수 b는 Sample 안에 있기에 인식안됨
//Sample 객체를 생성하지 않고 정적 멤버 사용하기
System.out.println("static 변수 b : " + Sample.b); //OK - 클래스명.정적멤버 로 사용가능
}
}
class Sample{
int a= 10; //일반 멤버변수 - 인스턴스 변수
static int b= 20; //정적 멤버변수
}
코틀린언어에는 static 키워드가 없습니다. 대신 등장한 문법이 동반객체 companion object 입니다. object(객체)가 클래스(설계도면)에 동반되었다는 의미에서 붙여진 이름입니다.
동반객체안에 선언한 변수와 함수(메소드)들은 객체를 생성하지 않고도 사용이 가능합니다.
//시작 함수
fun main(){
//4. 동반객체 [ companion object - java의 static 키워드와 유사한 기능 : 객체 생성없이 사용가능한 멤버들 ]
println( Sample.title )
// 동반객체의 멤버변수 값 변경
Sample.title="robin"
println( Sample.title)
//당연히 static method 같은 기능의 메소드도 만들수 있음
Sample.showTitle()
println()
}//main 함수 종료..
//4. companion object [ companion: 동반자, 동행 ] - JAVA의 static 키워드를 대신하는 문법
class Sample{
var a:Int=10
companion object{
var title:String="sam" //java static variable 같은 역할
fun showTitle():Unit{ //java static method 같은 역할
println("제목 : $title")
//println("Sample클래스 객체의 프로퍼티 a : $a ") //당연히 객체의 프로퍼티[자바에서는 인스턴스변수]는 사용할 수 없음
}
}
}
//**출력**
sam
robin
제목 : robin
자바의 static 과 비슷한 역할 이기에 객체생성 해야만 사용가능한 프로퍼티인 Int형 값 a 변수는 동반객체의 메소드안에서는 사용이 불가능합니다.
5. 늦은 초기화
코틀린의 클래스 문법에서는 프로퍼티(멤버변수)를 만들 때 반드시 초기화를 해야 만 합니다. 자바처럼 Default 초기화를 수행해 주지 않습니다. 하지만 프로그램을 구현하다 보면 클래스를 설계할 때 초기값을 주기에 적합하지 않은 경우도 존재합니다.
안드로이드 앱 개발과정에서의 대표적인 사례가 View 참조변수를 만들 때 입니다.
보통 액티비티안에 보여지는 뷰들을 제어하는 참조변수는 액티비티 전체에서 사용되는 경우가 많아 멤버변수로 만드는 경우가 많습니다. 하지만 실제 참조할 뷰는 별도의 xml 문서에 작성되어 객체 생성 후 자동 호출되는 onCreate() 메소드에서 객체로 생성되기에 이 때 찾아와 참조해야 합니다. 즉, 이미 참조변수를 선언할 때에는 아직 해당 뷰 객체가 만들어지기 전이어서 참조가 되지 않습니다. 그렇기에 뷰 참조변수를 만들 때 초기화를 하지 못하고 나중에 해야 할 필요가 있습니다. 자바에서는 null 값으로 자동 초기화 되지만 코틀린은 null 값을 저장하기 위해서 nullable 변수로 만들어야 하고 사용할 때마다 null 관련 연산자를 사용해야 하는 등의 추가 작업이 필요하여 불편해 집니다.
class MainActivity : AppCompatActivity() {
//뷰 참조변수
var tv: TextView //1. ERROR - 클래스의 프로퍼티는 반드시 초기화 해야 함.
var tv2: TextView= findViewById(R.id.tv2) //2. Exception - 문법에러는 아니지만 런타임(실행 중) 오류 발생함
//3. null값으로 초기화 하기 위해 TextView? nullable 변수로 선언
var tv3: TextView?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 화면에 보여줄 뷰를 설정
setContentView(R.layout.activity_main)
tv3= findViewById(R.id.tv3)
tv3?.text="aaa". // nullable 참조변수의 멤버를 사용할 때 마다 ?. null 안전 연산자를 사용하는 불편함이 있음.
}
}
위 코드처럼 뷰 참조용 프로퍼티는 초기화를 나중에 해야 할 필요가 있습니다. nullabal 변수를 사용하는 방법도 있지만 추후 코드가 불편해 질 수 있기에 더 좋은 방법을 권해드립니다. 이때 사용하는 문법이 늦은 초기화 문법 입니다.
코틀린의 늦은 초기화 문법에는 2가지 종류가 있습니다.
1) lateinit : var 변수에 사용하는 늦은 초기화
2) by lazy : val 변수에 사용하는 늦은 초기화
5.1) lateinit
class Hello{
var name:String //ERROR : 프로퍼티는 선언하면서 값을 초기화 하지 않으면 에러
}
앞서 설명했듯이 프로퍼티를 선언하면서 초기화를 하지 않으면 문법적 에러 입니다.
만약 나중에 초기화를 하고 싶다면 늦은 초기화를 해야 합니다. var 변수에 적용하는 lateinit( late:늦은 + initialize:초기화) 키워드를 적용해 보겠습니다. var 키워드 앞에 lateinit 을 추가하면 됩니다.
class Hello{
//var name:String //ERROR : 프로퍼티는 선언하면서 값을 초기화 하지 않으면 에러
lateinit var name:String //lateinit 키워드를 통해 나중에 초기화한다고 명시!
fun show(){
println("property name : $name") //프로퍼티이므로 메소드에서 사용가능
}
}
당연히 초기화가 되어 있지 않기에 초기화 없이 바로 name변수를 사용하면 Exception(런타임 중 에러)이 발생합니다.
//5.1) 프로퍼티(멤버변수)의 늦은 초기화문법 [ lateinit ]
val h:Hello= Hello()
// 초기화 없이 사용해 보기
println(h.name) //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생
h.show() //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생 - 늦은 초기화확인 문법을 통해 예외발생 막을 수 있음.
그런 name 변수값을 초기화 해주는 코드를 작성할 기능메소드를 추가해 보겠습니다. 이름은 onCreate()라고 해보겠습니다.
class Hello{
//var name:String //ERROR : 프로퍼티는 선언하면서 값을 초기화 하지 않으면 에러
lateinit var name:String //lateinit 키워드를 통해 나중에 초기화한다고 명시!
//초기화 코드를 가진 메소드!!
fun onCreate(){
name= "sam"
}
fun show(){
println("property name : $name") //프로퍼티이므로 다른 메소드에서 사용가능
}
이제 main()함수에서 초기화 후에 프로퍼티를 사용해보겠습니다.
fun main(){
val h:Hello= Hello()
//println(h.name) //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생
//h.show() //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생 - 늦은 초기화확인 문법을 통해 예외발생 막을 수 있음.
h.onCreate() //이 메소드에서 초기화
println(h.name)
h.show()
}
//**출력**
sam
property name : sam
♣ lateinit 키워드 사용할때 주의할 점.
- lateinit은 null초기화는 불가능[즉, String? 타입은 불가능함]
lateinit var title:String? //ERROR
- 기본형 자료형[primitive type : Int, Double, Boolean ..... ]들에도 사용불가
lateinit var age:Int //ERROR
- lateinit은 var 에만 사용가능 [ val 에는 사용불가 ]
lateinit val address:String //ERROR
- 늦은 초기화확인 문법이 등장함 [ 코틀린 버전 1.2부터 사용가능 ]
:: 을 붙인 lateinit 프로퍼티에 사용가능함 .isInitialized -- show()메소드에서 소개
fun show(){
//println("property name : $name") //프로퍼티이므로 다른 메소드에서 사용가능
//lateinit 프로퍼티의 초기화여부에 확인하여 안전하게 사용하기 [ 코틀린 버전 1.2부터 사용가능 ]
if( ::name.isInitialized ) println("property name : $name")
}
- lateinit 변수에 대한 setter/getter properties 정의가 불가능
- 클래스 주 생성자의 파라미터에서 사용 불가능
- 로컬 변수에서 사용 불가능
5.2) by lazy
class Hello{
//5.2) val의 늦은 초기화는 lazy [ by lazy ] - 작성 위치와 상관없이 사용될 때 lazy 블럭{}의 내용이 실행되고 마지막에 있는 값이 대입됨
val address:String by lazy { "seoul" }
}
코틀린에서 by 키워드는 특정 처리를 다른 객체에게 위임(Delegate) 할 때 사용합니다. by 에 대한 소개는 추후 소개하도록 하고 지금은 by 옆의 중괄호 { } 영역의 값을 대입해주는 정도로 인식하시면 됩니다. 단, 늦은 초기화를 위해 lazy(게으른) 키워드를 적용하여 중괄호 {} 영역의 내용이 작성 위치와는 상관없이 이 변수가 처음 사용되는 곳에서 대입되는 특징을 가집니다.
즉, lazy 옆의 {} 내용을 지금 당장 변수에 대입하지 않고 좀 게으르게 수행하라고 명령을 내렸다고 보시면 됩니다.
by lazy의 중요한 특징은 val 변수에만 적용할 수 있다는 것입니다.
lazy 블럭{}안에는 일반적인 실행문도 구현할 수 있습니다. 다만 title 변수는 String 타입을 값을 원하기에 중괄호 {} 의 마지막 실행문은 대입되는 문자열 값 이어야만 합니다.
class Hello{
//5.2) val의 늦은 초기화는 lazy [ by lazy ] - 작성 위치와 상관없이 사용될 때 lazy 블럭{}의 내용이 실행되고 마지막에 있는 값이 대입됨
val address:String by lazy { "seoul" }
// lazy 블럭{}안에 일반적인 실행문도 구현가능함
val title:String by lazy {
println("by lazy 초기화블럭")
"Hello title"
}
}
즉, title 에 나중에 대입 될 값은 "Hello title" 이라는 글씨입니다. {}영역의 실행이 나중에 된다는 점을 조금 더 확인해 보기 위해 객체가 생성될 때 실행되는 {초기화 블럭} init 를 추가하고 by lazy의 중괄호 {} 안의 println()실행문이 호출되는 순서를 살펴보겠습니다.
fun main(){
//5.2)lazy 초기화
val h:Hello= Hello() //이때 init{} 초기화 블럭 실행됨
println(h.address) //이때 address 변수가 사용되면서 lazy 블럭이 실행되며 초기화 됨 - "seoul"값이 대입됨
//by lazy{}블럭안에 실행문 넣고 실행되는 시점 확인
println(h.title) //이때 title이 사용되면서 lazy 블럭이 실행되며 초기화 됨 -- android에서 활용하기 좋음
}
class Hello {
//5.2) val의 늦은 초기화는 lazy [ by lazy ] - 작성 위치와 상관없이 사용될 때 lazy 블럭{}의 내용이 실행되고 마지막에 있는 값이 대입됨
val address:String by lazy { "seoul" }
// lazy 블럭{}안에 일반적인 실행문도 구현가능함 - 아래 만들어 본 초기화블럭인 init{} 과 다르게 이 프로퍼티를 사용하기 전에는 발동하지 않음.
val title:String by lazy {
println("by lazy 초기화블럭")
"Hello title"
}
//init 초기화블럭
init {
println("이 초기화 블럭은 객체생성시에 자동 호출됨")
}
}
//**출력**
이 초기화 블럭은 객체생성시에 자동 호출됨
seoul
by lazy 초기화블럭
Hello title
이 by lazy {} 의 초기화시점을 통해 안드로이드에서 아래와 같은 코드 작성이 가능합니다.
class MainActivity : AppCompatActivity() {
val tv: TextView = findViewById() //ERROR - onCreate()에서 setContentVeiw() 다은에 실행해야 하기에 에러
val tv:TextView by lazy { findViewById(R.id.tv) } <--이런식으로 사용할 수 있음. 그럼.. onCreate() 에서 findViewById()를 안해도 됨.
}
♣ by lazy 키워드를 사용할 때의 특징 ( 대부분 var 변수에 사용했던 lateinit 의 특징에 반대되는 특징들 입니다. )
- by lazy는 primitive 자료형도 가능함
val age:Int by lazy { 20 }
- by lazy는 nullable type에서도 사용가능함
val message:String? by lazy { "Hello by lazy....." }
val message2:String? by lazy { null }
- 이런식의 연산에 의한 초기화도 가능함
val message3:String? by lazy {
if(age<20) "미성년자"
else "성인"
//참고로 만약 lazy 초기화 하는 age가 이전에 사용된 적이 없다면 바로 이때 초기화됨
}
- by lazy는 var에는 사용할 수 없음
var sss:String by lazy { "Nice" } //ERROR
- 클래스 주 생성자에서는 사용 불가능
- 로컬 변수에서 사용 가능
- 당연히 사용자정의 클래스의 객체 늦은 초기화도 가능함
val nnn:MyKotlinClass by lazy { MyKotlinClass() }