Kotlin 다시 보기 (2)
‘빅 너드 랜치의 코틀린 프로그래밍’ 을 읽고 정리
인터페이스와 추상 클래스
abstact 함수(추상 함수) : 함수의 헤더만 선언하고 몸체(Body)의 구현 코드가 없는 함수
객체간의 상속 관계가 없으면서, 주로 공통적인 속성이나 행동을 갖는 경우 : 인터페이스
객체간의 상속 관계가 있으면서, 주로 인스턴스 생성이 필요 없는 부모 클래스가 필요한 경우 : 추상 클래스
인터페이스 구현하기
인터페이스는 무엇을 해야 하는지 정의한 것이므로, 클래스로 구현(implement) 해야 한다.
해당 인터페이스를 구현하는 클래스를 정의(구현)한 후에 이 클래스에서 인터페이스에 명시된 속성과 함수의 구현 코드를 제공하게 한다.
인터페이스의 속성과 함수는 open 키워드를 지정하지 않아도 된다.
인터페이스는 how가 아닌 what을 정의한다.
- how는 인터페이스의 구현 클래스에서 정의
다중 상속 가능
속성 초기화, 값 지정 불가능
인터페이스 타입의 매개변수를 가지므로 이 인터페이스를 구현하는 어떤 클래스의 인스턴스도 인자로 받을 수 있다
인터페이스에 정의된 속성에 기본으로 구현된 getter와 함수에도 기본으로 구현된 몸체 코드를 제공할 수 있다.
damageRoll
의 속성의 값을 지정하지 않을 경우 기본으로 구현된 getter에서 값이 지정됨
|
|
SAM(single abstract method) 인터페이스
하나의 추상 메서드만 갖는 인터페이스 (코틀린이랑 상관없음)
인터페이스 임으로, 하나의 타입이 된다 (컴파일러가 자동 타입 추론 및 에러 검출)
함수형 인터페이스 라고도 불린다.
1 2 3
public interface OnClickListener { // 대표적 예, Java 코드 void onClick(View v); }
SAM 변환
- 자바 8 이전에는 함수형 인터페이스를 인자로 받는 Java 함수를 호출할 경우, 인터페이스를 구현한 익명 클래스 인스턴스를 만들어서 넘겨줘야 했다.
- 자바 8 이후와 코틀린에서 인터페이스를 인자로 받는 Java 메서드를 호출 할 경우, 객체(object) 대신 람다를 넘길 수 있다. 이를 SAM 변환이라고 한다.
SAM 주의 사항
- SAM 변환은 자바에서 작성한 인터페이스일 때만 동작
- 코틀린에서 하나의 추상 메소드만 있는 인터페이스를 생성하고, 그 인터페이스를 인자로 받는 함수에 람다를 넘기려고 하면 오류가 발생
- 이유는 코틀린에서는 함수를 파라미터로 사용할 수 있기 때문이다.
- 코틀린에서는 함수 타입이라는 특별한 타입이 존재하기 때문
- 만약 코틀린에서 SAM 변환을 사용하고 싶다면, 인터페이스 앞에 fun 키워드를 붙여주면 된다.
- 이유는 코틀린에서는 함수를 파라미터로 사용할 수 있기 때문이다.
|
|
추상 클래스
인터페이스와 비슷하게 추상 함수와 속성을 갖는다.
구현 코드가 있는 일반 함수도 가질 수 있다.
서브 클래스에서 추상 클래스의 속성과 함수를 상속받아 구현하도록 하는 것이 추상 클래스의 주 목적이다.
추상 클래스는 주로 부모 클래스를 정의할 때 사용된다. (상속)
인터페이스에 정의된 함수는 기본적으로 추상 함수지만, 추상 클래스에 정의되는 추상 함수에는
abstract 키워드
를 지정해야 한다.일반 클래스는 하나의 추상 클래스만 부모 클래스로 가질 수 있지만, 인터페이스는 여러 개 구현 가능하다.
제네릭
제네릭: 타입을 미리 확정하지 않고, 사용되는 시점에서 특정 타입을 지정할 수 있도록 해주는 기법
동일한 인터페이스 및 클래스, 함수의 정의를 재사용할 수 있어서 코드의 중복을 줄여준다.
컴파일 시점에서 사용 타입의 적합성을 확인할 수 있으므로, 타입 안전을 보장해 준다. (타입 추론 가능)
List 는 ‘제네릭 인터페이스’로 정의 되었다.
- 여기서 ‘List’ 는 raw 타입
- <> 안에 지정된 타입을 제네릭 타입(generic type) 이라고 한다.
List<out E>
제네릭 클래스의 인스턴스의 타입은 raw 타입과 제네릭 타입이 결합된 타입이 된다. ( 하나의
List<Int>
타입 )
제네릭 타입 정의하기
|
|
제네릭 타입 매개변수 : T / < >안에 지정됨
보통 return 타입 매개 변수는 ‘R’로 표현
(T) -> R
=> 함수 타입
제네릭 타입 제약
- 제네릭 타입
T
에: Loot
를 지정하면, Loot 클래스 및 이것의 서브 클래스만 LootBox 클래스의 매개변수 타입으로 사용될 수 있다.
|
|
vararg
|
|
vararg 키워드
를 추가하면 매개변수가 배열(Array) 로 처리되므로 여러 개의 아이템을 인자로 전달할 수 있다.- 코틀린에서 기본 타입이 아닌 Arrays 라는 참조 타입으로 배열을 지원한다. (코틀린은 모두 참조 타입)
- 여기서 배열은 컬렉션 타입이다.
- 코틀린에서 기본 타입이 아닌 Arrays 라는 참조 타입으로 배열을 지원한다. (코틀린은 모두 참조 타입)
|
|
기본 생성자에서의
vararg
- 코틀린의 기본 생성자에서
vararg
를 사용하면, 생성자에 가변적인 개수의 인자를 전달하여 객체를 초기화할 수 있다.
- 코틀린의 기본 생성자에서
vararg
사용 시 주의 사항:하나의 생성자에
vararg
매개변수는 최대 하나만 사용할 수 있다.vararg
매개변수는 항상 마지막 매개변수로 와야 한다.vararg
매개변수는 내부적으로 배열로 처리된다.
활용 예시:
다양한 개수의 인자를 받는 함수: 예를 들어, 평균을 계산하는 함수를 정의할 때
vararg
를 사용하면 여러 개의 숫자를 인자로 받아 처리할 수 있다.배열을 펼쳐서 전달: 스프레드 연산자(
*
)를 사용하여 배열을 펼쳐서vararg
매개변수에 전달할 수 있다.
in과 out
제네릭 클래스의 인스턴스는 raw 타입과 제네릭 타입이 결합된 것이 자신의 타입이 된다.
<>로 나타낸 제네릭 타입 간의 부모-자식 관계가 있더라도 컴파일러가 인식하지 못한다.
- 개발자 입장에서 불편함
이 불편을 해결 방법이 in과 out 이다.
out 키워드
를 지정하면, 두 제네릭 타입 간의 부모-자식 타입 관계를 ‘그대로’ 컴파일러가 고려해준다.List의 타입도
List<out E>
로 되어 있다.제네릭 타입 매개변수에
out 키워드
를 지정한 것을공변형(covariance)
in 키워드
를 지정하면, 제레릭 타입 매개변수를 포함한 클래스나 인터페이스의 관계가 제네릭 타입 매개변수의 부모-자식 관계와 ‘반대’ 로 된다.‘동물’ 타입이 ‘사자’ 타입의 부모인 경우
‘Pet<동물> 타입’이 ‘Pet<사자> 타입’의
자식 타입
으로 간주된다.제네릭 타입 매개변수
에 in 키워드를 지정한 것을반공변형(contravariance)
reified 키워드
컴파일된 JVM 바이트 코드에서는 제네릭 타입 매개변수의 정보가 수록되지 않고 소거된다.
List<Int>
,List<Double>
모두 JVM 바이트코드에는raw 타입 List
로 처리된다.- 실체화(reification): 코틀린은 이 문제를 해결하기 위해 제네릭 타입 매개변수를 컴파일러가 실제 타입으로 변경해주는 기능을 지원
- 제네릭 타입 매개변수의 실체화(reification)
제네릭 타입 매개변수가 지정된 클래스는 타입 검사에 사용될 수 없다.
if (l is List<String>)
⇒ 컴파일 에러
reified 키워드
를 사용하면 제네릭 매개변수로 전달된 인자의 타입과 인스턴스의 타입을 런타임 시에 검사할 수 있게 된다. (inline 키워드와 같이 사용)- 타입 매개변수의 실체화는 inline 키워드가 지정된 인라인 함수에서만 가능하다.
|
|
확장
기존 타입(클래스 or 인터페이스)의 정의를 직접 변경하지 않고 새로운 기능을 추가해준다.
JVM 바이트 코드는 코틀린 확장 함수가
static
메서드로 된다.각 타입에 대한 확장 함수나 속성을 포함하는 코틀린 표준 라이브러리 파일들은 타입 이름 끝에 s를 붙인 파일 이름을 갖는다.
Strings.kt도 그렇고 Sequences.kt, Ranges.kt, Maps.kt
코틀린은 확장 함수를 굉장히 많이 사용하여 핵심 API를 정의하고 있다.
표준 라이브러리의 크기가 작으면서도 많은 기능을 제공한다.
확장을 사용하면 함수나 속성을 하나만 정의하여 여러 타입에 같은 기능을 제공할 수 있으므로 메모리 효율 상승
확장 함수 정의하기
확장 함수를 추가할 타입을
수신자 타입
이라고 한다. 그리고 이것을 지정해야 한다.this
는 확장 함수가 호출된 ‘수신자 객체’를 뜻한다.클래스의 자식 클래스를 만들 수 없거나, 해당 클래스의 정의를 변경할 수 없을때(외부 라이브러리), 주로 확장을 사용한다.
제네릭 확장 함수
|
|
- 제네릭 확장 함수인 ‘easyPrint’는 어떤 타입에도 사용 가능하며 타입 정보도 유지된다.
확장 속성
- 확장 속성은 backing field를 갖지 않으므로 초기화할 수 없다.
- var 대신 val을 지정하고, 원하는 값을 반환하는
get
을 반드시 정의해야 한다. - 속성에서 반환될 값을 지정해줘야 한다.
- backing field에 데이터가 저장된다.
- var 대신 val을 지정하고, 원하는 값을 반환하는
|
|
infix 키워드
infix 키워드
는 하나의 인자를 갖는 확장 함수와 클래스 함수에 모두 사용할 수 있다.- infix(중위) 함수는 함수 호출 사이의
점(.) 과 괄호
를 생략하게 해준다. 일반 함수 호출 처럼 호출도 가능
|
|
함수형 프로그래밍
함수형 프로그래밍은 컬렉션을 사용하도록 설계된 고차 함수가 반환하는 데이터에 의존한다.
- 고차 함수의 연쇄적 호출
- 함수 타입의 역할 => 다른 함수를 값으로 정의해주거나, 인자로 받거나 반환하는 일급 함수을 지원
함수형 프로그램을 구성하는 함수의 유형에는 변환(transform), 필터(filter), 결합(combine) 이 있다.
코틀린은 다중 프로그래밍 방식으로 상황에 따라, 객체지향과 함수형 프로그래밍을 혼합하여 코드를 작성 가능
일급 함수 (First-Class Function)
- 정의: 일급 함수는 다른 변수와 동일하게 다룰 수 있는 함수를 의미
- 변수화, 매개변수화, 반환 값으로 사용가능
val func: (Int) -> Int = { it * 2 }
람다(익명 함수, 이름 없는 함수)
형태를 띔
- 정의: 일급 함수는 다른 변수와 동일하게 다룰 수 있는 함수를 의미
고차 함수 (Higher-Order Function)
- 정의: 고차 함수는 ‘일급 함수’를 인자로 받거나 반환하는 함수를 의미
val doubled = numbers.map { it * 2 } // map은 고차 함수
변환 (transform)
변환 함수는 입력 컬렉션에 저장된 모든 요소를 읽는다.
지정 변환 함수를 실행하여 컬렉션의 각 요소나 항목을 변환한 후, 변경된 ‘새로운 컬렉션’을 반환한다.
- 입력 컬렉션은 변경되지 않는다.
- 임시로 새로운 객체 생성 후, 다음 연쇄 호출된 함수로 넘김
map, flatMap
- 제네릭 타입 매개변수를 사용함으로 인자의 타입과 반환 타입을 다르게 처리할 수 있다.
<T, R> Iterable<T>.map(transform: (T) -> R): List<R>
flatMap
함수는 인자로 전달된 변환 함수의 결과로 산출된 모든 요소를 하나의 컬렉션으로 생성하여 반환한다.listOf( listOf(1,2,3), listOf(4,5,6) ).flatMap { it } // [1,2,3,4,5,6]
필터 (filter)
필터 함수는 컬렉션의 각 요소를 검사하고, true or false를 반환하는
술어 함수
를 인자로 받는다.filter 하나
true를 반환하는 요소만 결과 컬렉션에 해당 요소가 추가 된다.
결합 (combine)
서로 다른 컬렉션 인자로 받아서 모든 요소들이 합쳐진 새로운 컬렉션을 생성한다
zip, fold
fold : 최초 누적값을 인자로 받으며, 이 값은 각 요소에 대해 호출되는 익명 함수의 결과값으로 변경된다.
왜 함수형 프로그래밍일까?
처리 중간에 값을 축적하는 변수들이 내부적으로 정의되므로, 상태를 보존하는 변수들을 줄일 수 있다.
자동으로 축적 변수에 추가되므로, 코드 에러 발생 위험이 줄어든다.
- Side Effect 제거, 불변성 증가, 테스트 용이성 향상
새로운 연산이 필요하면, 함수의 연쇄 호출에 추가하면 된다.
시퀀스 (sequence)
컬렉션 타입들은 eager collection( 조기 컬렉션 ) 이라고 한다.
- 인스턴스가 생성될 때는 자신이 포함하는 요소나 항목이 추가되므로 바로 사용될 수 있기 때문이다.
지연 컬렉션(lazy collection)라는 내장된 타입 :
sequence
, 필요할 때만 값이 생성된다.시퀀스를 사용할 때는 새로운 값이 요청될 때마다 참조되는 반복자 함수를 정의한다.
함수형 프로그래밍에는 보통, 새로운 컬렉션을 자주 생성해야 한다.
그러나 시퀀스를 사용하면 대형 컬렉션에 사용할 수 있는 매커니즘을 제공한다.
- 시퀀스로 검사할 항목개수의 상한값을 정의할 필요가 없기 때문이다.
1 2 3
val oneThousandPrimes = generatesequence(3) { value -> value + 1 }.filter { it.isPrime() }.take(1000)
그러나 수십만 개의 요소를 가질 정도로 컬렉션이 커지면 컬렉션 타입을 변경하는 데따른 성능 향상이 중요해질 수 있다.
- 이 경우에는
asSequence 함수
를 사용해서 List를 시퀀스로 쉽게 변환할 수 있다.
- 이 경우에는