속성(property) 선언
코틀린에서 클래스는 var 키워드를 사용하여 변경가능하거나 val 키워드를 사용하여 읽기전용 프로퍼티를 가질 수 있다.
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}
또한 이름을 통해 프로퍼티를 참조할 수 있다.
fun copyAddress(address: Address): Address {
val result = Address() //코틀린에선 new 연산자가 필요없다.
result.name = address.name // 접근자가 호출됬다.
result.street = address.street
// ...
return result
}
Getters와 Setters
프로퍼티 선언의 전체 구문은 다음과 같다.
var <propertyName>[: <ProPertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
선언구분으로 알 수 있는 것은 코틀린은 자바처럼 별도의 Accessor 메서드를 선언할 필요없다.
프로퍼티가 Accessor메서드(getter & setter)를 가지고 있고 이를 통해서 이뤄지는 것을 알 수 있다.
initializer, getter, setter은 선택사항이다.
val i = 1 // 타입은 Int, 기본 getter, setter
val isEmpty: Boolean
get() = this.size == 0
var stringRepresentation: String
get() = this.toString()
set(value){
setDataFromString(value)
}
setter의 패러미터 이름을 value로 하는 건 관례이다.
만일 자바처럼 변수(var) 선언시 자동으로 null이나 0으로 초기화될 것이라 생각하고 Nullable타입으로 프로퍼티를 선언하면 빨간 줄을 볼 수 있을 것이다.
var allByDefault: Int? // 에러 : initializer필요 아니면 추상화 필요, 기본 getter와 setter 적용
읽기 전용 프로퍼티(val) 선언은 변경 가능 프로퍼티(var)와 두 가지 다르다.
var대신 val로 시작하고, setter를 허용하지 않는다.
val simple: Int? // Int타입, 기본 getter, 반드시 생성자에서 초기화 해야함
val inferredType = 1// Int타입, 기본 getter
코틀린 1.1부턴 getter에서 타입 추론할 수 있으면 프로퍼티 타입을 생략할 수 있다.
속성에 대한 사용자 지정 접근자를 정의할 수 있다. 사용자 지정 getter를 정의하면 속성이 액세스 될 때 마다 호출된다.
class Rectangle(val width: Int, val height: Int) {
val area: Int // getter 중 타입 추론이 되기 때문에 속성은 선택사항이다.
get() = this.width * this.height
}
저상태에서 area 가 변경가능 프로퍼티이고 대입연산자로 값이 할당돼있다면 어떨까?
fun main(args: Array<String>) {
val rect = Rectangle(10, 10)
println(rect.area)
rect.printArea()
}
class Rectangle(val width: Int, val height: Int) {
var area: Int = 10
get() = this.width * this.height
fun printArea(){
println(area)
}
}
100
100
똑같다. 액세스 될 때 마다 getter가 호출되기 때문에 할당된 값은 문제되지않는다.
만약 getter에서 속성을 유추할 수 있으면 속성을 생략해도 된다.
val area get() = this.width * this.height
이를 더 줄이면 평소 쓰던 문법을 볼 수 있다.
val area = this.width * this.height
사용자 지정 setter를 정의하면 초기화 될 때를 제외하고 속성에 값이 할당될 때 마다 호출된다.
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // parses the string and assigns values to other properties
}
뒤에 나오는 Backing Field를 사용하면 다음과 같은 사용법도 가능하다.
fun main(args: Array<String>) {
val rect = Rectangle()
rect.test()
}
class Rectangle {
private var testValue = "hihihihihi"
set(value) {
field = "$value hello world"
}
fun test() {
println("그냥 출력 : $testValue")
testValue = "hi im park"
println("setter 반영 출력 : $testValue")
}
}
그냥 출력 : hihihihihi
setter 반영 출력 : hi im park hello world
프로퍼티의 기본 구현을 바꾸지 않고 어노테이션을 붙이거나, 접근자의 가시성을 바꾸고 싶다면 몸체 없는 접근자를 정의하면 된다.
var sets: String = "abc"
private set //setter를 private로 하고 나머지는 기본구현 가짐
var setterWithAnnotation: Any? = null
@Inject set //setter에 @Inject 어노테이션 적용
지원(Backing) 필드
필드란 무엇일까?
자바에서 필드란 클래스와 메서드, 생성자 외의 빈 공간이라 볼 수 있다.

그렇다면 프로퍼티와 차이가 뭘까?
프로퍼티는 객체의 고유 속성을 칭하는 단어이고, 속성의 실체를 담은 공간이 필드라고 할 수 있다.
자바에서 필드에 접근은 2가지 경우로 나눌 수 있다.
- 같은 클래스일 경우 변수명으로 직접 참조
- 객체 생성 후 참조
만일 접근제어자를 private로 선언할 경우 getter(), setter()를 생성하여 클래스 외부에서 접근할 수 있다.
public class Test {
private String test;
public String getTest() { //getter
return test;
}
public String setTest(String test){ //setter
this.test = test;
}
}
public class Car {
String model;
String color;
int maxSpeed = 0;
public static void main() {
model = "2세대"; //1번 직접참조
Car sonata = new Car();
sonata.model = "sonata 1세대"; //2번 객체 생성후 참조
sonata.color = "마젠타";
if (sonata.maxSpeed == 0)
sonata.maxSpeed = 200;
Test test = new Test(); //접근
test.setTest("안녕하세요");
System.out.println(test.getTest());
}
}
여기서 Car 속성은 model, color, maxSpeed이고 필드는 null, null, 0이다
코틀린에서 필드는 값을 메모리에 저장하는 속성의 일부로만 사용된다. (자바의 필드와 차이점은 자동으로 get(), set()이 생성되는지 아닌지 이다.)
필드(field)는 직접 선언할 수 없다. 그러나 속성에 지원 필드가 필요한 경우 Kotlin이 이를 자동으로 제공한. 지원 필드는 field 식별자 를 사용하여 접근자에서 참조할 수 있다.
class Count{
var counter = 0
set(value){
if( value >= 0) field = value
}
}
여기서 질문 field식별자로 접근하지 말고 그냥 counter로 바로 접근하면 안 되나?
놀랍게도 가능하다
fun main(){
val a = Solution()
a.test = 1
print(a.test)
}
class Solution {
var test = 0
set(value) {
if(value > test) test = value
}
}
실행하면 에러 코드를 볼 수 있다.
Exception in thread "main" java.lang.StackOverflowError
다른 오류도 아니고 StackOverflow가 발생했다.
왜?
그 이유는 자바 코드로 디컴파일해보면 알 수 있다.
public final class Solution {
private int test;
public final int getTest() {
return this.test;
}
public final void setTest(int value) {
if (value > this.test) {
this.setTest(value); //재귀호출
}
}
}
보면 재귀 호출하고 있다.
파국이다
이와 반대로 field를 사용하면 정상적인 결과를 볼 수 있는데
public final class Solution {
private int test;
public final int getTest() {
return this.test;
}
public final void setTest(int value) {
if (value > this.test) {
this.test = value; //멤버변수 재할당 하고 있다.
}
}
}
아주 친절하게 멤버 변수에 재할당 해주고 있다. 결과적으로 가능은 한데 실행할 수 없다.
지원(Backing) 프로퍼티
암시적 지원 필드 체계(field를 통한 호출)에 맞지 않는 작업을 수행하려는 경우 항상 지원 속성을 갖는 것으로 대체할 수 있다.
private var _table: Map<String, Int>? = null // 지원 속성
public val table: Map<String, Int>
get(){
if(_table == null)
_table = HashMap()
return _table ?: throw AssertionError("Set to null by another thread")
}
JVM은 기본 getter, setter를 가진 private 프로퍼티에 접근하면 함수 호출에 따른 오버헤드가 발생하지 않도록 최적화한다.
나중에 안드로이드 프래그먼트에서 뷰 바인딩할 때 많이 쓴다.
컴파일 타임 상수
상수다. 컴파일 시점에 알아야 할 속성 값을 const 수식어를 이용해서 컴파일 타임 상수로 표시할 수 있다. 이러한 속성은 다음 조건을 충족해야 한다.
- 최상위 속성이거나, 오브젝트 선언 또는 컴패니언 객체의 멤버여야 한다.
- String이나 기본 타입 값으로 초기화
- 커스텀 getter가 아님
컴파일러는 상수의 사용을 인라인 하여 상수에 대한 참조를 살제 값으로 바꾼다. 그러나 필드는 제거되지 않으므로 리플렉션(reflection)을 사용하여 상호 작용을 할 수 있다.
이런 속성 어노테이션에서 사용할 수 있다.
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
초기화 지연(Late-Initialized) 속성 및 변수
일반적으로 null이 아닌 타입으로 선언한 속성는 생성자에서 초기화해야만 한다. 그러나 이 방식이 제약이 되는 경우가 종종 있다. 의존 주입이나 단위 테스트의 셋업 메서드에서 속성을 초기화한다고 하자. 이 경우 생성자에 null이 아닌 초기값을 제공할 순 없는데, 클래스 몸체에서 속성을 참조할 때 null 검사는 피하려고 한다.
이럴 때 lateinit 수식어를 붙일 수 있다.
public class MyTest{
lateinit var subject: TestSubject
@SetUp fun setup(){
subject = TestSubJect()
}
@Test fun test(){
subject.method()
}
}
lateinit는 클래스 몸체에 정의된 var프로퍼티가 getter, setter가 없는 경우 적용할 수 있다. 속성 타입은 not null 이여야 하고 기본 타입이면 안된다.
lateinit 속성이 초기화되기 전 접근하려 한다면 초기화 안되있다는 특별한 예외를 발생시킨다.
lateinit var가 초기화되었는지 확인
lateinit var가 이미 초기화 되었는지 확인하려면 해당 속성에서 .isInitialized 참조를 사용하면 된다.
fun main(){
var m = MyTest()
m.test("")
m.test("test")
}
class MyTest {
lateinit var subject: String
fun test(t: String){
if(t.isNotEmpty()) subject = t
if(this::subject.isInitialized){
println("초기화 됨")
}
else{
println("안됨")
}
}
}
안됨
초기화 됨
이 검사는 동일한 유형, 외부 유형 중 하나 또는 동일한 파일의 최상위 수준에서 선언될 때 액세스 가능한 속성에 대해서만 사용할 수 있다.
'코틀린' 카테고리의 다른 글
Functional (Single Abstract Method) Interface (1) | 2023.02.02 |
---|---|
인터페이스 (0) | 2023.02.01 |
클래스와 상속 (0) | 2023.01.30 |
흐름제어 (0) | 2023.01.30 |
타입 체크와 캐스트(is, as) (1) | 2023.01.30 |