프로퍼티의 접근
Person 클래스에 변수에 해당하는 name, age라는 필드를 가지고 있다고 가정하자.
공개하고 싶지않다면 가시성 지시자로 private을 지정할 수 있따.
그러면 내부의 코드가 아닌 곳에서는 접근할 수 없는데, 어떻게 설정해야 할까?
Getter Setter를 만들어여하고 getAge(), setAge()를 통해 설정할 수 있다.
아래 자바코드이다.
5주차와 마찬가지로 command + n을 누르면 getter, setter를 편하게 만들 수 있다.
package chap06.section1;
class Person{
// 멤버 필드
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Main{
public static void main(String[] args){
Person p1 = new Person("kildong", 30);
// p1.name = "Dooly" // 불가
p1.setName("Dooly");
System.out.println(p1.getName());
}
}
Kotlin에서는 아래와 같은 방법으로 게터세터를 지정한다.
var 프로퍼티 이름[: 프로퍼티 자료형][= 프로퍼티 초기화]
[get() { 게터 본문 }]
[set(value) {세터 본문}]
val 프로퍼티 이름[: 프로퍼티 자료형][= 프로퍼티 초기화]
[get() { 게터 본문 }]
package chap06.section1
class User(_id: Int, _name: String, _age: Int){
// 프로퍼티
val id: Int = _id
get() = field
var name: String = _name
get() = field
set(value){
field = value
}
var age: Int = _age
get() = field
set(value){
field = value
}
}
fun main(){
val user1 = User(1, "kildong", 30)
// user1.id = 2 // val프로퍼티는 값 변경 불가
user1.age = 35 // 세터
println("user1.age = ${user1.age}") // 게터
}
커스텀 게터와 세터의 사용
직접 게터와 세터를 정의하면서 새로운 내용을 작성하는 것을 커스텀 게터와 세터라고 한다.
단순히 값을 반환하거나 설정할 때는 굳이 게터와 세터를 따로 지정하지 않아도 된다.
그러나 입력 문자를 대문자로 바꾸는 등의 특정 연산을 수행해야 한다면 게터와 세터를 확장해 코드를 구성할 수 있어 아주 편리하다.
아래 예시에서는 "coco"가 value가 되는데, field의 함수로 대문자로 변경되어 출력되는것을 알 수 있다.
package chap06.section1.customgetset
class User(_id: Int, _name: String, _age: Int){
val id: Int = _id
var name: String = _name
set(value){
println("The name was changed")
field = value.toUpperCase() // 받은 인자를 대문자로 변경해 프로퍼티에 할당
}
var age: Int = _age
}
fun main(){
val user1 = User(1, "kildong", 35)
user1.name = "coco"
println("user3.name = ${user1.name}")
}
보조 프로퍼티의 사용
null이 되는 경우를 처리하기 위해 임시적으로 사용하는 프로퍼티이다. 이런 경우 보조 필드인 field를 사용하지 않고 추가로 내부의 프로퍼티를 임시로 선언해 사용할 수 있다.
아래 예시에서 보조 프로퍼티 값을 "abc"를 두었을 경우에는
null로 두었을 경우에는 NONAME이 출력되는것을 확인할 수 있다.
package chap06.section1.customproperty
class User(_id: Int, _name: String, _age: Int){
val id: Int = _id
private var tempName: String? = "abc"
// private var tempName: String? = null
var name: String = _name
get(){
if(tempName == null) tempName = "NONAME"
return tempName ?: throw AssertionError("Asserted by others")
}
var age: Int = _age
}
fun main(){
val user1 = User(1, "kildong", 35)
user1.name = ""
println("user3.name = ${user1.name}")
}
프로퍼티의 오버라이딩
프로퍼티는 기본적으로 오버라이딩 할 수 없는 final 형태로 선언된다. 만일 프로퍼티를 오버라이딩 가능하게 하려면 open키워드를 사용해 프로퍼티를 선언해야한다.
package chap06.section1
open class First{
open val x: Int = 0 // 오버라이딩 가능
get(){
println("First x")
return field
}
val y: Int = 0 // open 키워드가 없으면 final 프로퍼티
}
class Second : First(){
override val x: Int = 0 // 상위 클래스와 구현부가 다르게 오버라이딩됨
get(){
println("Second x")
return field + 3
}
// override val y: Int = 0 // 오버라이딩 불가
}
fun main(){
val second = Second()
println(second.x) // 오버라이딩된 두 번째 클래스 객체의 x
println(second.y) // 상위 클래스로부터 상속받은 값
}
위의 예시들을 종합해서 책에는 프로퍼티를 이용한 나이 속이기 예제가 있다.
18세 미만이면 18세로 고정
30세를 초과하면 원래 나이의 3살 깍아서 출력하는 코드이다.
package chap06.section1
fun main(){
val kim = FakeAge()
kim.age = 15
println("kim's real age = 15, pretended age = ${kim.age}")
}
class FakeAge{
var age: Int = 0
set(value){ // 나이에 따라 판별하는 세터
field = when{
value < 18 -> 18
value in 18..30 -> value
else -> value - 3
}
}
}
지연 초기화와 위임
프로퍼티 지연 초기화가 왜 필요할까?
객체의 정보가 나중에 나타나는 경우 객체 생성과 동시에 초기화하기 힘든 경우가 있다.
보통 클래스에서는 기본적으로 선언하는 프로퍼티 자료형들을 null을 가질 수 없기 때문에 생성자에서 초기화하거나 매개변수로부터 값을 초기화 해야하는 것이 규칙이다. lateinit와 lazy키워드를 통해 지연 초기화가 가능하다.
lateinit
예시를 살펴보자.
package chap06.section2
class Person{
lateinit var name: String // 지연 초기화를 위한 선언
fun test(){
if(!::name.isInitialized){ // 프로퍼티의 초기화 여부 판단
println("not initialized")
}
else{
println("initialized")
}
}
}
fun main() {
val kildong = Person()
kildong.test()
kildong.name = "Kildong" // 이 시점에서 초기화 됨(지연 초기화)
kildong.test()
println("name = ${kildong.name}")
}
kildong.name을 초기화 한 순간에 지연 초기화가 되는것을 확인할 수 있다.
lazy를 사용한 지연 초기화
lateinit을 통해서 프로퍼티나 객체를 선언할 때는 val은 허용하지 않고 var로 선언해야 했다.
하지만 var로 선언하면 객체나 프로퍼티의 경우 언제든 값이 변경될 수 있는 단점이 있다.
읽기 전용의 val로 선언한 객체나 프로퍼티를 나중에 초기화 하기 위해 lazy를 사용한다.
또한 lazy 인스턴스는 반환값을 가지는 함수이다.
아래 예시를 보자.
package chap06.section2
class LazyTest {
init{
println("init block") // 2
}
val subject by lazy{
println("lazy initialized") // 6
"Kotlin Programming"// 7 lazy 반환값
}
fun flow(){
println("not initialized") // 4
println("subject one: $subject") // 5 최초 초기화 시점!
println("subject two: $subject") // 8 이미 초기화된 값 사용
}
}
fun main() {
val test = LazyTest() // 1
test.flow() // 3
}
객체 지연 초기화하기
lazy를 사용해서 객체에 대한 지연을 할 수도 있다.
package chap06.section2.bylazyobj
class Person(val name: String, val age: Int)
fun main(){
var isPersonInstantiated: Boolean = false // 초기화 확인 용도
val person : Person by lazy { // lazy를 사용한 person 객체의 지연 초기화
isPersonInstantiated = true
Person("Kim", 23) // 이 부분이 Lazy 객체로 반환됨
}
val personDelegate = lazy{ Person("Hong", 40) } // 위임 변수를 사용한 초기화
println("person Init: $isPersonInstantiated")
println("personDelegate Init: ${personDelegate.isInitialized()}")
println("person.name = ${person.name}") // 이 시점에서 초기화
println("personDelegate.value.name = ${personDelegate.value.name}") // 이 시점에서 초기화
println("person Init: $isPersonInstantiated")
println("personDelegate Init: ${personDelegate.isInitialized()}")
}
lazy모드를 확인할 수 있는 방법도 있다.
위임
특정 클래스를 확장하거나 이용할 수 있도록 by를 통한 위임이 가능하다.
observable()함수와 vetoable()함수를 통한 위임도 가능하다.
observable()과 vetoable()의 비슷하지만, 반환값에 따라 프로퍼티 변경을 허용하거나 취소할 수 있다는 점이 다르다.
자세한것은 예시들로 살펴보자.
by의 사용방법
<var | var | class> 프로퍼티 혹은 클래스이름: 자료형 by 위임자
바로 예시로 알아보자.
package chap06.section2
interface Car{
fun go(): String
}
class VanImpl(val power: String): Car{
override fun go() = "은 짐을 적재하며 $power 을 가집니다."
}
class SportImpl(val power: String): Car{
override fun go() = "은 경주용에 사용되며 $power 을 가집니다."
}
class CarModel(val model: String, impl: Car): Car by impl{
fun carInfo(){
println("$model ${go()}") // 참조 없이 각 인터페이스 구현 클래스의 go()에 접근
}
}
fun main() {
val myDamas = CarModel("Damas 2010", VanImpl("100마력"))
val my350z = CarModel("350Z 2008", SportImpl("350마력"))
myDamas.carInfo() // carinfo에 대한 다형성을 나타냄
my350z.carInfo()
}
impl은 CarModel의 위임되어 각 구현 클래스인 VanImpl과 SportImpl의 go() 메서드를 생성된 위임자에 맞춰 호출할 수 있다.
객체 지연하기 코드에서 사용된 lazy 앞에 by가 붙어져있다. lazy는 사실 람다식이다.
따라서 사용된 프로퍼티는 람다식에 위임되어 사용가능하다. lazy의 동작 설명이다.
by lazy에 의한 지연 초기화는 스레드에 좀 더 안정적으로 프로퍼티를 사용할 수 있다.
왜냐하면 프로그램 시작 시 큰 객체가 있다면 시작 시간에 초기화해야하므로 느려질 수 밖에 없는데,
필요에 따라 해당 객체를 접근하는 시점에서 초기화하면 시작할 때마다 프로퍼티를 생성하느라 소비되는 시간을 줄일 수 있다.
observable() 함수의 사용방법
package chap06.section2
import kotlin.properties.Delegates
class User{
var name: String by Delegates.observable("NONAME"){ // 프로퍼티 위임
prop, old, new -> // 람다식 매개변수로 프로퍼티, 기존 값, 새로운 값 지정
println("$old -> $new") // 이 부분은 이벤트가 발생할 때만 실행
}
}
fun main() {
val user = User()
user.name = "Kildong" // 값이 변경되는 시점에서 첫 이벤트 발생
user.name = "Dooly" // 값이 변경되는 시점에서 두 번째 이벤트 발생
}
vetoable() 함수의 사용방법
package chap06.section2
import kotlin.properties.Delegates
fun main() {
var max: Int by Delegates.vetoable(0) { // 초기값은 0
prop, old, new ->
new > old // 조건에 맞지 않으면 거부권 행사
}
println(max) // 0
max = 10
println(max) // 10
// 여기서는 기존값이 새 값보다 크므로 false, 따라서 5를 재할당하지 않음
max = 5
println(max) // 10
}
정적 변수와 컴패니언 객체
모든 변수나 클래스의 객체는 꼭 동적으로 객체를 생성해서 사용하지 않아도 된다.
바로 정적변수와 컴패니언 객체를 사용하는것이다.
이것은 동적인 메모리에 할당 해제되는 것이 아닌 프로그램을 실행할 때 고정적으로 가지는 메모리로 객체 생성 없이 사용할 수 있다.
어떠한 객체라도 동일한 참조값을 가지고 있어 해당 클래스의 상태에 상관없이 접근할 수 있다. 따라서 모든 객체에 의해 공유되는 효과를 가진다.
컴패니언 객체의 예시를 보자.
package chap06.section3
class Person{
var id: Int = 0
var name: String = "Youngdeok"
companion object{
var language: String = "Korean"
fun work() {
println("working...")
}
}
}
fun main(){
println(Person.language) // 인스턴스를 생성하지 않고 기본값 사용
Person.language = "English" // 기본값 변경 가능
println(Person.language) // 변경된 내용 출력
Person.work() // 메서드 실행
// println(Person.name) // name은 컴패니언 객체가 아니므로 오류
}
Person 클래스의 language는 객체의 생성 없이도 접근할 수 있게 되었다.
하나의 메모리만 유지해 자원의 낭비를 줄일 수 있게 되었다.
디자인 패턴이라는 표현이 나왔다.
소프트웨어 설계에서 공통적인 문제에 대한 표준적인 패턴을 만들어 적용할 수 있게 한 기법이다.
패턴의 종류는 생성과 구조 행위로 나뉘며 싱글톤은 생성 패턴 중에 하나이다.
코틀린에서 자바의 static 멤버 사용하기
코틀린에는 컴패니언 객체를 사용하면 되지만, 자바와 연동해서 사용하려면 정적 변수나 메서드를 접근해야 하는 경우가 있을것이다.
먼저 자바의 Customer 클래스이다.
package chap06.section3;
public class Customer {
public static final String LEVEL = "BASIC"; // static 필드
public static void login() { // static 메서드
System.out.println("Login...");
}
}
코틀린에서 자바 클래스의 static메서드에 접근하는 코드이다.
package chap06.section3
fun main() {
println(Customer.LEVEL)
Customer.login()
}
그렇다면 자바에서 코틀린 컴패니언 객체를 사용할 수 있지 않을까?
그렇다 @JvmStatic 애노테이션 표기법을 알아야한다.
그러기 전에 애노테이션이란?
@JvmStatic, @override와 같이 @ 기호로 시작하는 애노테이션 표기는 사전적으로 '주석'이라는 뜻이다.
하지만 코드에서는 특수한 의미를 부여해 컴파일러가 목적에 맞추어 해석하도록 하거나 실행(런타임)할 때 특정 기능을 수행하게 할 수도 있다.
예시를 보자.
먼저 kotlin으로 구현한 클래스이다.
package chap06.section3
class KCustomer {
companion object{
const val LEVEL = "INTERMEDIATE"
@JvmStatic // 애노테이션 표기 사용
fun login() = println("Login...")
}
}
그리고 자바에서 코틀린 클래스를 접근하는 코드이다.
package chap06.section3;
public class KCustomerAccess {
public static void main(String[] args){
//코틀린 클래스의 컴패니언 객체에 접근
System.out.println(KCustomer.LEVEL);
KCustomer.login(); // 애노테이션을 사용할 때 접근 방법
KCustomer.Companion.login(); // 위와 동일한 결과로 애노테이션을 사용허지 않을 때 접근 방법
}
}
애노테이션을 사용하지 않을 때 방법도 알아보았다.
이런 방식으로 최상위 함수에도 접근할 수 있는데, 접근하는 클래스의 이름도 애노테이션으로 변경할 수 있다.
먼저 코틀린코드이다.
@file:JvmName("PKLevel")
package chap06.section3
fun packageLevelFunc() {
println("Package-Level Function")
}
fun main(){
packageLevelFunc()
}
자바에서 코틀린코드의 최상위 함수에 접근하려면 변경된이름인 PKLevel로 접근 가능하다.
package chap06.section3;
public class PackageLevelAccess {
public static void main(String[] args){
// PackageLevelFunction.packageLevelFunc();
PKLevel.packageLevelFunc(); // 변경된 이름으로 접근 가능
}
}
object 키워드로 선언하는 방법도 있다. 과연 컴패니언 객체와 다른점이 있을까?
아래 예시로 보자
package chap06.section3
// object 키워드를 사용한 방식
object OCustomer{
var name = "kildong"
fun greeting() = println("Hello World")
val HOBBY = Hobby("Basketball")
init{
println("Init!")
}
}
// 컴패니언 객체를 사용한 방식
class CCustomer{
companion object{
const val HELLO = "hello" // 상수 표현
var name = "Joosol"
@JvmField val HOBBY = Hobby("Football")
@JvmStatic fun greeting() = println("Hello World!")
}
}
class Hobby(val name: String)
fun main() {
OCustomer.greeting() // 객체의 접근 시점
OCustomer.name = "Dooly"
println("name = ${OCustomer.name}")
println(OCustomer.HOBBY.name)
CCustomer.greeting()
println("name = ${CCustomer.name}, HELLO = ${CCustomer.HELLO}")
println(CCustomer.HOBBY.name)
}
package chap06.section3;
public class OCustomerAccess {
public static void main(String[] args){
String name = OCustomer.INSTANCE.getName(); // 코틀린의 object 선언 객체의 메서드 접근
System.out.println(name);
}
}
바로 INSTANCE로 접근할 수 있다는 차이점이 있다.
object 표현식도 사용할 수 있다.
package chap06.section3
open class Superman() {
fun work() = println("Taking photos")
fun talk() = println("Talking with people")
open fun fly() = println("Flying in the air.")
}
fun main() {
val pretendedMan = object: Superman(){ // object 표현식으로 fly() 구현의 재정의
override fun fly() = println("I'm not a real superman. I can't fly!")
}
pretendedMan.work()
pretendedMan.talk()
pretendedMan.fly()
}
8주차 - 제네릭과 배열 (1) | 2024.01.11 |
---|---|
7주차 - 다양한 클래스와 인터페이스 (0) | 2024.01.09 |
5주차 - 클래스와 객체의 정의 (1) | 2024.01.06 |
4주차 - 프로그램의 흐름 제어 (0) | 2024.01.04 |
3주차 - 함수와 함수형 프로그래밍 (0) | 2024.01.02 |
댓글 영역