들어가기 전에

1. 엘비스 연산자
data class Person(val name: String, val age: Int? =null
// 데이터 클래스를 선언하고 널이 될 수 있는 타입과 파라미터 디폴트 값
fun main(args: Array<String>){ //최상위함수
val persons= listOf(Person("영희"),
										Person("철수",age=29)) // 이름이 붙은 파라미터
val oldes = persons.maxBy{it.age?:0}//람다식과 엘비스 연산자 
	//객체를 받아 객체를 반환하는 람다식을 파라미터로 전달받는데 persons 객체를 전달받아 Int 값을 반환한
	println("나이가 가장 많은 사람: $oldest") //문자열 템플릿
}
}

Kotlin은 JAVA와 다르게 명령문 마지막의 세미콜론은 선택 사항이다.

한 줄에 여러 명령문을 작성하려면 세미콜론을 붙여야한다.

java의 this는 ‘객체, 자기 자신’을 나타낸다면 kotlin의 it은 람다식의 유일한 인자를 나타낸다.

엘비스 연산자를 사용하면 age가 null이면 0을 null이 아니면 age의 값을 반환한다.


코틀린의 경우 서버상의 코드, 안드로이드 디바이스에서 실행되는 모바일 애플리케이션, IOS, 데스크탑 애플리케이션 환경에서도 수행 할 수 있다.

광범위한 서비스를 제공하는 도메인에 적합한 언어라고 생각한다.


  1. 정적 타입 지정언어

    자바와 마찬가지로 코틀린도 정적 타입 지정 언어다.

    (정적 타입 지정 언어란? 프로그램 안에서 객체의 필드나 메서드를 사용할 때마다 컴파일러가 타입을 검증하는 언어- 구성요소의 타입을 컴파일 시점에 알 수 있어 컴파일 단계에서 해결 할 수 있다)

    자바와 달리 코틀린은 모든 변수의 타입을 프로그래머가 직접 명시할 필요는 없는데, 대부분의 경우 코틀린 컴파일러가 문맥으로부터 변수 타입을 자동으로 추론하기 때문에 타입 선언은 필수적인 요소가 아니다.

var x =1

변수를 정의하면서 정수 값으로 초기화하는데, 코틀린은 이 변수의 타입을 int로 추론한다

(이처럼 문맥을 통해 타입을 추론하는 과정을 ‘타입 추론’ 이라고 부른다)

  • 왜 사용하는가?
    • 성능

      실행 시점에 어떤 메소드를 호출할지 알아내는 과정이 필요 없다

    • 신뢰성

      컴파일러가 프로그램의 정확성을 검증한다

    • 유지 보수성

      코드에서 다루는 객체가 어떤 타입인지 알 수 있다.

    • 도구 지원

      정적 타입 지정으로 안전하게 리팩토링한다.

  • 자바와 다른 타입 시스템

    nullable type을 지원한다.

    컴파일 시점에 null pointer exception이 발생할 수 있는지 여부를 검사해준다

코틀린 기초

객체지향+함수형프로그래밍을 같이 사용

2.1 함수와 변수

본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부르고, 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 표현한다

→ 인텔리제이에서 이 두방식을 서로 변환하는 메뉴가 존재한다.

fun max(a:Int,b:Int)=if(a>b)a else b

코틀린은 정적 타입 지정 언어인데, 컴파일 시점에 모든 식의 타입을 지정해야 하는거 아닌가?

→ 실제로 모든 변수나 모든 식에는 타입이 존재하고, 모든 함수는 반환 타입이 정해져야 하는데, 식이 본문인 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해 식의 결과 타입을 추론한다.

함수를 선언할 떄 fun 키워드를 사용한다.

파라미터,변수 이름 뒤에 그 프라미터의 타입, 변수 타입을 쓴다,

함수를 최상위 수준에 정의 할 수 있다.( 클래스 안에 함수를 넣어야 할 필요가 없다)

자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.

표준 자바 라이브러리 함수를 간결하게 사용 할 수 있게 감싼 래퍼를 제공한다.

래퍼(데이터를 객체로 표현하기 위해 포장해주는 클래스

2.1.2 함수

의미 있는 결과를 반환하는 함수의 경우 반환 값의 타입은 파라미터 목록 뒤에 콜론(:)으로 구분하여 추가하자

문- 블록의 최상위 요소로 존재하며, 아무런 값을 만들어내지 않는다.

식- 값을 만들어 내며 다른 식의 하위요소로 계산에 참여할 수 있다.

실제로 모든 변수나 모든 식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야 한다.

→ 하지만 식이 본문인 함수의 경우 굳이 사용자가 반환타입을 적지 않아도 컴파일러가 추론을 통해 함수 반환 타입을 정해준다.

2.1.3변수

  • 변경 가능한 참조 (Variable :Var)
  • 변경 불가능한 참조 (Value :Val) := java의 final 변수에 해당
    • 참조 자체는 불변이지만, 그 참조가 가르키는 객체의 내부 값은 변경될 수 있다

문자열 리터럴에서 컴파일 오류가 발생하는데, 타입이 컴파일러가 기대하는 타입과 다르기 떄문에이다.

문자열 리터럴이 필요한 곳에 변수를 넣되 변수앞에 $를 추가하고 {}중괄호로 둘러싸서 사용하자

val languages= arrayListOf("Java")
languages.add("Kotlin")
//****//

var answer =42
answer ="No answer"
//컴파일 에러
fun main(args:Array<String>){
	val name = if(args.size>0) args[0] else "Kotlin"
	println("Hello, $name!")//"\"을 사용하면 $그대로 출력
}
-> $name 사용하는  보다 ${name} 사용하는 습관을 들이자.

2.2 클래스와 프로퍼티

프로퍼티(Property)

추상화

class Person(val name: String)

코드없이 데이터만 저장하는 클래스를 값 객체라 부르고, public 가시성 변경자가 사라지는데

코틀린의 기본 가시성은 public이므로 이런 경우 변경자를 생략한다.

→ 클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한주체 아래 가둔다

캡슐화

자바의 경우 데이터를 필드에 저장하고, 멤버 필드의 가시성은 보통 private로 사용하는데, 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메서드를 제공한다.

→ 필드를 읽기 위한 getter/setter를 추가로 제공한다.

class Person(
	val name:String,
	val isMarried: Boolean
)

읽기 전용 프로퍼티로, 코틀린은 비공개 필드와 필드를 읽는 단순한(공개) 게터를 만들어 내고, 코틀린 비공개필드, 공개 getter/setter를 만든다

코틀린은 프로퍼티를 언어 기본 기능으로 제공하고, 자바의 필드와 접근자 메서드를 완전히 대신한다.

기본적으로 코틀린에서 프로퍼티를 선언하는 방식은 프로퍼티와 관련 있는 접근자를 선언하는 것

(읽기 전용 프로퍼티는 getter만 변경 가능한 프로퍼티는 getter와 setter 모두 선언된다.)

코틀린의 name 프로퍼티를 자바 쪽에서는 getName으로 사용 할 수 있다.

Getter/Setter 의 이름 규칙

  • Setter - is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고, 원래 이름 그대로 사용하며, 세터에는 is를 Set으로 바꾼 이름을 사용한다 ismarred()

    but 자바에서 person.setMarried(false)로 표현하지만 코틀린은 person.isMarried =false로 사용한다

    만약 자바 클래스가 isMarried와 setMarried 메서드를 제공한다면 코틀린 프로퍼티의 이름은 isMarried로 자바에서 선언한 클래스에 코틀린 문법을 사용 할 수 있다.

2.2.2 커스텀 접근자(커스텀 Getter/Setter)

Class Rectangle(val height: Int,val width:Int){
	val isSquare:Boolean
		get(){
		return height==width
	}
}

2.2.3 코틀린 소스코드 구조: 디렉터리와 패키지

자바의 경우 모든 클래스를 패키지 단위로 관리하는데, 모든 코틀린 파일의 맨 앞에 package문을 넣을 수 있다.

같은 패지키에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용 가능하다

반면 다른 패키지의 경우 정의한 선언을 사용하려면 임포트를 통해 선언해야한다.

→ 코틀린의 디렉터리는 개발자 마음대로 할 수 있다, 파일의 이름도, 하위 폴더의 이름도

⇒ 대부분의 경우 패키지별로 디렉터리를 구성한다..


2.3 선택 표현과 처리: enun과 when

enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야 하는 흔치 않은 예,

코틀린에서는 enum을 class를 사용하지만 자바는 enum 을 사용하고. 소프트 키워드라 불리는 존재로, enum안에서 프로퍼티나 메서드를 정의 할 수 도 있다.

enum class Color(val r: Int,val g: Int val b: Int)
{
RED(255,0,0),ORANGE(255,165,0),
YELLOW(255,255,0),GREEN(0,255,0),BLUE(0,0,255),
INDIGO(75,0,139),VIOLET(238,130,238);
	fun rgb() = (r * 256 + g) * 256 + b
}
fun getMnemonic(color: Color)=
	when (color){
		Color.Red ->"Richard"
		Color.ORANGE -> "Of"
}

각 분기의 끝에 Break;를 넣지 않아도 되는데, 성공적으로 매치되는 분기를 찾으면 switch는 그 분기를 실행하고, 한 분기 안에서 여러 값을 매치 패턴으로 사용 할 수 도 있다.값은 ,(콤마)로 구분

fun getWarmth(color:Color)=when(color){
	Color.Red,Color.ORANGE,Color.YELLOW->"warm"
	Color.GREEN->"neutral"
	Color.BULE ->"cold"
}

enum 상수 값을 임포트해서 enum 클래스 수식자 없이 enun 사용도 가능하다

import Color
import Color.Color.*
	fun getWarmth(clor:Color) = when(color){
	RED,ORANGE->"warm"
	GREEN->"neutral"
}

분기 조건에 상수(숫자 리터럴)만 사용할 수 있는 자바와 달리 코틀린 when은 분지 조건을 임의의 객체로 허용하는데,

fun mix(c1:Color,c2:Color)=
when(setOf(c1,c2)){
		setOf(RED,YELLOW)->ORANGE
		setOf(YELLOW,BLUE)->GREEN
		setOf(BLUE,VIOLET)->INDIGO
		else->throw Exception("Dirty color")
}

setOf를 사용하여 Set객체로 만들고, 순서가 중요하지 않기 때문이다.

when 식은 인자 값과 매치하는 조건 값을 찾을 때까지 각 분기를 검사하는데, 분기 조건에 있는 조건들을 동등성에 맞게 비교한다.

인자가 없는 when식

fun mixOptimized(c1:Color,c2:Color)=
when {
		(c1==RED && c2==YELLOW)||
		(c1==YELLOW && c2 == RED) -> ORANGE
}

when에 아무런 인자도 없다면 각 분기의 조건이 불리언 결과를 계산하는 식이 된다.

2.3.5 스마트 캐스트

코틀린에서는 is를 사용해 변수 타입을 검사하는데, 자바의 instanceof와 비슷하다,

그러나그 타입에 속한 멤버에 접근하기 위해서 명시적으로 변수 타입을 캐스팅 하는데 이런 멤버 접근을 여러 번 수행한다면 변수에 따로 캐스팅한 결과를 저장한 후 사용한다.

프로그래머 대신 컴파일러가 캐스팅을하고, 어떤 변수가 원하는 타입인지 is로 검사혹 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 사용 할 수 있다.

이 과정을 스마트 캐스팅이라고 표현한다.

if(e is Sum){
		return eval(e.right)+eval(e.left)
}

스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.

클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 프로퍼티는 반드시 val이어야하고, 커스텀 접근자를 금지한다.

→ 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확실할 수 없기 떄문에

⇒ AS키워드를 이용해 해결한다

val n = e as Num

2.3.6 리팩토링

java와 달리 3항 연산자의 공백을 해결하기 위해 eval 함수에서 retrun문과 중괄호를 없애 if식을 본문으로 사용하여 해결한다

fun eval(e:Expr):Int =
	if(e is Num){
		e.value
	}else if (e is Sum){
		evel(e.right)+eval(e.left)
	}else{
		throw IllegalArgumentException("Unknown expression")
}
  • if의 문기에 식이 하나라면 중괄호를 생략한다, if 분기에 블록을 사용한다면 그 블록의 마지막 식이 그 분기의 결과 값이된다.
fun eval(e: Expr): Int=
	when(e){
		is Num->
			e.value
		is Sum ->
			eval(e.right)+eval(e.left)
		else ->
			throw IllegalArgumentException("Unknown expression")
	}

동등성 검사가 아닌 받은 값의 타입을 검사의 기능을 수행 할 수 도 있다

— java에서는 조건문을 수행하는 과정에서 타입 변환이나 강제 캐스팅이 필수였지만, 스마트 캐스팅으로 해결한다.

  • 블록을 사용하여 분기를 만드는경우

    if나 when 모두 분기에 블록을 사용 할 수 있는데, 블록의 마지막 문장이 블록 전체의 결과가 된다

fun evalWithLogging(e:Expr):Int =
	when(e){
		is Num->{
			println("num: ${e.value}")
				e.value
		}
		is Sum->{
			val left = evalWithLogging(e.left)
			val right = evalWithLogging(e.right)
			println("sum: $left + $right")
			left+right
		}
		else -> throw IllegalArgumentException("Unknown expression")
}

But!

‘블록의 마지막 식이 블록의 결과’라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립하는데, 함수에 대해서는 성립하지 않는다. 식이 본문인 함수는 블록을 본문으로 가질 수 없고블록이 본문인 함수는 내부에 return 문이 반드시 있어야 한다.

-? 블록이 어느 식에는 존재하고 어느 문에는 존재 하는지 이해하기 힘듬.

? 식안에 분기가 생성되면 블록이 필요하고 식안에 결과값이 필요하면 블록을 만들어 return하는지?

2.4 대상을 이터레이션:

→ 이터레이션(iteration)은 결과를 생성하기위한 프로세스의 반복

코틀린 특성 중 자바와 가장 비슷하며, 코틀린 while 루프는 자바와 동일하다.

2.4.2 수에 대한 이터레이션: 범위와 수열

  • 자바의 for 루프에 해당하는 요소가 존재하지 않는데(for-each 뺴고), 이런 루프의 가장 흔한 예로 초깃값, 증가 값, 최종 값을 사용한 루프를 대신하기 위해 코틀린에서는 range를 사용한다.

    i ) 범위는 기본적으로 두 값으로 이뤄진 구간이며, 그 두 값은 정수 등의 숫자 타입의 값이다.

    ii) .. 연산자로 시작 값과 끝 값을 연결하여 범위를 만든다

val ontToTen = 1..10

→ 코틀린의 범위는 폐구간 또는 양끝을 포함하는 구간이다.

사진

즉 두 번쨰 값이 항상 범위에 포함된다는 뜻이다.

step을 사용하여 for문의 증가값 부분을 대신하며, 증가 값을 자유롭게 설정 할 수 있다.

→ 만약 반폐구간에 대한 이터레이션을 할 경우에는 until 함수를 사용하자

for(x in 0 until size)
//비슷
for(x in 0..size-1) // 조금 더 명확한 개념 표현

2.4.3 맵에 대한 이터레이션

val binaryReps = TreeMap<Char, String>()
for(c in 'A'..'F'){
	val binary = Integer.toBinaryString(c.toInt())
	binaryReps[c] = binary
}
for((letter,bianry) in binaryReps){
	println("&letter = $binary")
}

get과 put을 사용하는 대신 map[key],map[key]=value를 사용해 값을 가져오고 설정할 수 있다.

binaryReps[c] = binary
//->
binaryReps.put(c,binary)
  • 컬랙션 응용
val list= arrayListOf("10","11","1101")
for ((index,element) in list.withIndex()){
	println("$index: $element")
}

→ 인덱스를 저장하기 위한 변수를 별도로 선언하지 않고, 루프에서 매번 면수를 증가시킬 필요가 없다.

2.4.4 in으로 컬랙션이나 범위의 원소 검사

in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다.

역산은 !in으로 사용한다

→ c in ‘a’..’z’ → ‘a’ ≤c && c≤ ‘z’로 변환된다.

2.5 코틀린의 예외 처리

자바와 다른 점은 다른 클래스와 마찬가지로 예외 인스턴스를 만들 때도 new를 붙일 필요가 없다.

자바와 달리 코틀린의 throw는 “식”이므로 다른 식에 포함될 수 있다.

→ try/catch/finally

try - 함수가 던질 수 있는 예외를 명시할 필요가 없다

catch - catch(e: NumberFromatException) 으로 사용한다. ( 예외타입을 :의 오른쪽에 작성)

but throws절이 코드에 없다!

자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOException을 붙이는데, IOE가 체크 예외

자바에서는 체크예외를 명시적으로 처리해야하며, 어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 catch로 처리, 처리하지 않는 예외는 thorws절에 명시한다.

코틀린은 체크 예외와 언체크 예외를 구별하지 않는다.

코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다.

2.5.2 try를 식으로 사용

trt키워드는 if 나 when과 마찬가지로 식으로 존재하는데, try 값을 변수에 대입할 수 있다, if와 달리 try의 본문은 반드시 중괄호{ } 로 둘러 싸야하며, 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이 된다.

함수 정의와 호출

함수 정의와 호출

3.1 코틀린에서 컬렉션 만들기

val set = hashSetOf(1,7,53)
val list =aarayListOf(1,7,53)
val map = hasMapOf(1 to "one",7 to"seven", 53 to "fifty-three")
println(set.javaClass)
//이때 set.javaclass는 getClsss()의 역활

결과적으로 코틀린은 코틀린 자체적인 컬렉션 기능을 제공하지 않고, 기존 자바 컬렉션을 제공한다.

표준 자바 컬렉션을 활용하면 자바 코드와 상호 작용하기 더 쉽다.

→ 자바 컬렉션과 코틀린 컬렉션을 서로 변환할 필요가 없기 때문에

//유용한 기능
val strings=listOf("first","second","fourteenth")
println(strings.last())//fourteenth
val numbers=setOF(1,14,2)
println(numbers.max())//14

3.2 함수를 호출하기 쉽게 만들기

자바 컬렉션에는 디폴트 tostring 구현되어 있는데, 출력 형식이 고정되어 있어 필요한 형식이 아닐 수도 있다.

예를들면

val list= listOf(1,2,3)
println(list)//[1,2,3]

형식을 1;2;3 처럼 원소 사이를 세미콜론으로 구분하고 괄호로 리스트를 둘러싸려면?

→ 구아바,아파치 커먼즈 같은 서드파티(제 3자, 즉 외부 라이브러리) 프로젝트를 추가하거나 직접 관련 로직을 구현해야한다.

fun <T> joinToString(
	collection: Collection<T>,
	separator: String,
	prefix: String,
	postfix:String): String{
	val result = StringBuilder(prefix)
		for((index,element) in collection.withIndex()){
				if(index>0) result.aapend(separator) // 0번쨰 인덱스에 구분자가 들어가면 ;1;2;3
				result.append(element)
		}
		result.append(postfix)
		return result.toString()
}

println(joinToString(list,";","(",")")) //(1;2;3)

3.2.1 이름 붙인 인자

위의 함수는 함수 호출 부분을 처음 접하는 사람에게는 읽히지 않거나 읽는데 시간을 소비해야 하는데 가독성 향상을 위해 이름을 붙여 인자를 사용한다.

인자로 전달한 각 문자열이 어떤 역활을 하는지 구분할 수 없고, 각 원소는 공백으로 구분되는지, 마침표로 구분되는지 , 함수의 시그니처를 살펴봐야하는 노력이 필요하다.

불리언 플래그 값을 전달해야 하는 경우 코드가 모호하기 떄문에 불리언 대신 enum타입 사용이 권장된다

(java)
jointToString(collection, /*separator */" ", /* prefix */"",/* postfix */".");
(kotlin)
jointTostring(collection, separator = " " , prefix =" " , postfix=".")

코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있으며, 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 인자는 모두 이름을 꼭 명시해야한다.

→ 자바로 작성된 코드는 이름 붙인 인자를 사용 할 수 없다!!

3.2.2 디폴트 파라미터 값

자바는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있다.

→ 하위 호환성을 유지하거나 API 사용자에게 편의를 더하는 등의 여러가지 이유로 추가된다.

→ 중복되는 오버로딩 함수가 늘면 추가되는 주석 또한 늘어나고 모호해지는 경우 발생

= 함수 선언에서 피라미터의 디폴트 값을 지정할 수 있다

  • 아무 접두사나 접미사 없이 콤마로 원소를 구분한다.
fun <T> joinToString(
	collection: Collection<T>,
	separator: String = ", ",
	prefix: String ="",
	postfix: String=""): String
//1)
JoinToString(list,",","","")//1, 2, 3
//2)
JoinToString(list)//1, 2, 3
//3)
JoinToString(list,";")//1; 2; 3
// separator를 ";"으로 지정하고 pre와 pos를 생략

→ 함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아닌 함수 선언 쪽에서 지정됨

= 어떤 클래스 안에 정의된 함수의 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 재 컴파일하면 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 디폴트 값을 적용받음

! 만약 자바 환경에서 코틀린 함수를 호출하는 경우? - 코틀린 함수가 디폴트 파라미터 값을 제공 하더라도 모든 인자를 명시해야 함, 자바 쪽에서 자주 사용 되는 호출 함수라면 “@JvmOverloads” 애노테이션을 활용하자. > 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로 부터 파라미터를 하나씩 생략한 오버로딩한 자바 메서드를 추가

3.2.2 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티

자바는 모든 코드를 클래스의 메서드로 작성해야 한다.

실전에서 어느 한 클래스에 포함시키기 어려운 코드가 생기는데, 일부 연산에는 비슷하게 중요한 역활을 하는 클래스가 둘 이상 있을 수도 있다.

? 코틀린에서는 이런 무의미한 클래스가 필요 없다, 대신 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스 밖에 위치 시킨다 ( 전역변수)

jvm이 클래스 안에 있는 코드만을 실행하는데 컴파일러는 이 파일을 컴파일할 때 새로운 클래스를 정의해준다.

  • 파일에 대응하는 클래스의 이름 변경하기

    코틀린 최상위 함수가 포함되는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 애노테이션을 추가한다, 애노테이션은 파일의 맨 앞, 패키지 이름 선언 이전에 위치

    ex) @file:JvmName(”StringFunctions”)

  • import strings.StringFunctions; → StringFunctions.joinToString

최상위 프로퍼티

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다.

var opCount =0//최상위 프로퍼티
fun performOperation(){
	opCount++
}
fun print(){
	println({$opCount}time)
}
/**/
const val UNIX_LINE_SEPARATOR ="/n"
//자바
public static final String UNIX_LINE_SEPARATOR="/n";

3.3 Method 다른 Class에 추가: 확장 함수와 확장 Property

기존 자바 API 재 작성 하지 않고 사용하는 “확장 함수”

확장 함수는 어떤 class 멤버 method 호출할 수 있지만 그 class밖에 선언된 함수다.

package strings
fun String.lastChar(): Char = this.get(this.length-1)
	//수신 객체 타입             //수신 객체(this)

println("Kotlin:.lastChar())//n

fun String.lastChar():Char= get(length-1)// 수신객체멤버에 this없이 접근도 된다.

확장하려는 class의 이름을 붙이기만 하면 확장 함수가 되며, class 이름을 수신 객체 타입, 호출되는 대상이 되는 값을 수신 객체라고 부른다.

하지만 확장 함수가 캡슐 화가 깨지는 않는데 Class 안에서 정의한 method와 달리 확장 함수 안에서는 Class 내부에서만 사용할 수 있는 private,protected멤버를 사용할 수 없다.

3.3.1 Import 와 확장 함수

확장 함수를 정의해도 자동으로 프로젝트 안의 모든 소스 코드에서 사용 할 수 있는 것은 아니다.

확장 함수를 정의하자마자 어디든 사용한다면, 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 발생 할 수도 있기 때문이다.

import strings.lastChar
val c = "Kotlin".lastChar()
//구분
import strings.*
//동일한 이름을 방지하기 위해 as를 사용
import strings.lastChar as last
val c= "Kotlin".last()
//코틀린 문법상 확장 함수는 반드시 짧은 이름을 사용

3.3.2 자바에서 확장 함수 호출

내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 method

  • 정적 method 호출하면서 첫 번째 인자로 수신 객체를 넘기면 된다.

다른 최상위 함수와 마찬가지로 확장 함수가 들어있는 자바 class 이름도 확장 함수가 들어있는 파일 이름에 따라 결정된다.

char c = StringUtilKt.lastChar("Java");

3.3.3 확장 함수로 유틸리티 함수 정의

fun <T> Collection<T>.joinToString(//Collection<T>에 대한 확장함수선언
	separator:String =", ",
	prefix: String ="",
	postifx: String =""):String {
	val result = StringBuilder(prefix)
	for((index,element) in this.withIndex()) //수신 객체 T타입의 원소로 이루어진 컬렉션
		if(index>0) result.append(separator)
		result.append(element)
	}

val list = arrayListOf(1,2,3)
println(list.joinToString(" "))//1 2 3

문법적인 편의, 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정 가능

fun Collection<String>.join(
	separator:String =", ",
	prefix: String ="",
	postifx: String ="")=joinToString(separator,prefix,postfix)
println(listOf("one","two","eight").join(" "))//one two eight
// but
listOf(1,2,8).join() //error

확장 함수가 정적 method 같은 특징을 가지므로, 확장 함수를 하위 class에서 Override 할 수 없다

3.3.4 확장 함수는 Override 할 수 없다.

View와 하위 class Button, button이 상위 class click함수를 override 할 때

Button이 View의 하위 타입이기 때문에 View 타입 변수를 선언해도 Button 타입 변수를 그 변수에 대입 가능하다.

View 타입 변수에 Click과 같은 일반 method 호출했는데, Click → button class override 했다면 실제로는 button이 override 한 click이 호출된다.

open class View{
	open fun click() = println("View Clicked")
}
class Button: View(){
	orverride fun click()= println("Button clicked")
}
val view: View= Button()
view.click()
//Button clicked

확장 함수는 class 일부가 아니며, 확장 함수는 class 밖에 선언된다. 이름과 파라미터가 완전히 같은 확장 함수를 기반 class와 하위 class에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.

fun View.showOff() = println("I`m a view!")
fun Button.showOff() = println("I`m a button!")
val view: view = Button()
view.showOff()
// i`m a view! *확장 함수는 정적으로 결정되기 떄문에!

//(자바)
View view = new Button();
ExtensionsKt.showOff(view);
//i`m a view! showOff함수를 kt파일에 정의했기 떄문에

view가 가리키는 객체의 실제 타입은 Button이지만, 위의 경우는 view 타입이 view이기 때문에 무조건 View의 확장 함수가 호출

3.3.5 확장 Property

확장 property 는, property 이름으로 불리지만 상태를 저장할 적절한 방법이 없기 때문에 실제로 확장 property는 아무 상태도 가질 수 없다.

property 문법으로 더 짧게 코드를 작성할 수 있다.

val String.lastChar: Char
	get() = get(length-1)

var StringBuilder.lastChar: Char
	get() = get(length-1) // 프로퍼티 게터
	set(value:Char){
		this.setCharAt(length-1,value) //프로퍼티 세터
}
//사용
println("Kotlin".lastChar)//n
	val sb =StringBuilder("Kotlin?")
	sb.lastChar = '!'
println(sb)//Kotlin!

뒷받침하는 필드가 없어, 기본 getter 구현을 제공할 수 없으므로 최소한 getter는 꼭 정의를 해야 한다.

초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드로 쓸 수 없다.

StringBuilder에 같은 property 정의한다면 StringBuilder의 맨 마지막 문자는 변경 가능하므로 propery를 var로 만들 수 있다.

3.4 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원

  1. vararg 키워드를 사용하여 호출 시 인자 개수가 달라질 수 있는 함수를 정의
  2. infix 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출
  3. destructring declration을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있음

3.4.1 자바 컬렉션 API확장

자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 어떻게 새로운 기능을 추가했는가, last와 max와 같은 함수들은 모두 확장 함수이다.

코틀린 표준 라이브러리는 수많은 확장 함수를 포함한다.

3.4.2 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

가변 길이 인자는 메서드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는데, 자바에서는 배열을 그냥 넘기지만 코틀린에서는 배열을 명시적으로 풀어 배열의 각 원소가 인자로 전달되게 해야한다.

→ spread 연산자를 사용 (실제로는 전달하려는 배열 앞에 * 를 붙여준다)

fun main(args: Array<String>){
	val list =listOf("args: ", *args)
	println(list)
}

3.4.3 값의 쌍 다루기: 중위 호출과 구조 분해 선언

to 라는 단언은 코틀린 키워드가 아닌데, 이 코드는 중위 호출이라는 특별한 방식으로 to라는 메서드를 호출한다.

중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣는다 ( 사이에 공백이 들어간다)

1. to("one") // to메서드를 일반적인 방식으로 호출
2. to "one" //to메서드를 중위 호출 방식으로 호출

함수를 중위 호출에 사용하게 허용하려면 infix 변경자를 함수 선언 앞에 추가하자.

infix fun Any.to(other: Any) =Pair(this, other)

이 함수는 Pair의 인스턴스를 반환하는데, pair는 코틀린 표준 라이브러리 클래스로, 두 원소로 이뤄진 순서 쌍을 표현, 실제로 to는 제너릭 함수

val (number, name) = 1 to "one"// 두변수를 즉시 초기화 할 수 있다.

이런 기능을 구조 분해 선언이라고 한다.

루프에서도 구조 분해 선언을 할 수 있는데, jointoString에서 본 withindex를 구조 분해 선언과 조합하여 컬랙션 원소의 인덱스와 값을 따로 변수에 담는다

for ((index,element) in collection.withIndex()){
	println("$index: $element")
}
//map
fun <K,V> mapOf(vararg values: Pair<K,V>: Map<K,V>
//mapOf의 경우 인자가 키와 값으로 이루어진 쌍이어야한다

3.5 문자열과 정규식 다루기

코틀린 문자열은 자바 문자열과 같고, 코틀린 코드가 만들어낸 문자열을 아무 자바 메서드에 넘겨도 되며, 자바 코드에서 받은 문자열을 아무 코틀린 표준 라이브러리 함수에 전달해도 무관하다.

자바 문자열을 감싸는 별도의 래퍼도 생기지 않는다.

3.5.1 문자열 나누기

  1. split 메서드

    자바 split 메서드는 점을 사용해 문자열을 분리할 수 없다

    “12.345-6.A”를 [12,345-6,A]로 배열이라고 생각하게 하는 실수를 유발한다.

    split의 구분 문자열은 실제로 정규식 이기 때문에 마침표는 문자를 나타내는 정규식으로 해석

println("12.345-6.A".split(".","-"))//여러 구분 문자열 지정
// 12, 345, 6 ,A 
"\\.|-".toRegex()//로 정규식을 명시적으로 만들 수 도 있다

3.5.2 정규식과 3중 따옴표로 묶은 문자열

fun paresePath(path:String){
	val directory = path.substringBeforeLast("/")
	val fullName = path.substringAfterLast("/")
	val fileName= fullName.substringBeforeLast(".")
	val extension = fullName.substringAfterLst(".")
	println("Dir: $directory,name: $fileName, ext: $setension")

//정규식
fun parsePath(path: String){
	val regex= """(.+)/(.+)\.(.+)""".toRegex()
	val matchResult = regex.metchEntire(path)
	if( matchResult!=null){
		val(directory,filename,extension)=matchResult.destructured
		println("Dir: $directory,name: $fileName, ext: $setension")
	}
}

3.5.3 여러 줄 3중 따옴표 문자열

3중 따옴표를 사용하면 줄 바꿈이 들어 있는 프로그램 텍스트를 쉽게 문자열로 만든다.

	val kotlinLo = """|   //
									 .| //
								   .|/  \"""

들여쓰기를 하되 들여쓰기의 끝부분을 특별한 문자열로 표시하고, trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.

3.6 코드 다듬기: 로컬 함수와 확장

코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다, 그렇게 하면 문법적인 부가 비용을 들이지 않고, 깔끔하게 코드를 조직할 수 있다.

java의 경우 DRY 원칙을 피하기 쉽지 않고, 클래스 안에 작은 메서드가 많아지고, 각 메서드 사이의 관계 파악이 어렵다. 그래서 로컬 함수를 사용 한다.

class User(val id:Int, val name: String. val address: String)

fun saveUser(user: User){
	if (user.name.isEmpty()){ //필드 검증이 중복된다.
		throw IllegalArgumentException(
			"Can`t save user ${user.id}: empty Name")
	}
	if (user.address.isEmpty()){
		throw IllegalArgumentException(
			"Can`t save user ${user.id}: empty Address")
}
//로컬 함수 사용
class User(val id:Int, val name: String, val address: String)
fun saveUser(user: User){
	fun validate(user:User,
							value:String,
							filedName: String){
		if(value.isEmpty(){
			throw IllegalArgumentException(
						"Can`t save user ${user.id}: empty $fieldName")
		}
}
validate(user,user.name,"Name")
validate(user,user.address,"Address")
//로컬 함수에서 바깥 함수의 파라미터 접근하기
class User(val id:Int, val name: String, val address: String)
fun saveUser(user: User){
	fun validate(value: String, fieldName: String){
		if(value.isEmpty(){
			throw IllegalArgumentException(
						"Can`t save user ${user.id}: " + "empty $fieldName")
		}
}
validate(user,user.name,"Name")
validate(user,user.address,"Address")
//검증 로직을 확장 함수로 추출하기

class User(val id:Int, val name: String, val address: String)
fun User.validateBeforeSave(){
	fun validate(value:String,
							filedName: String){
		if(value.isEmpty(){
			throw IllegalArgumentException(
						"Can`t save user ${user.id}: empty $fieldName")
		}
}
validate(user,user.name,"Name")
validate(user,user.address,"Address")
}
fun saveUser(user:User){
	user.validateBeforeSave()

한 객체만을 다루면서 객체의 비공개 데이터를 다룰 필요는 없는 함수는 확잠 함수로 만들어 객체.멤버 처럼 수신 객체를 지정하지 않고도 공개된 멤버 프로퍼티나 메서드에 접근할 수 있다.

User.validateBeforeSave를 saveUser내부에 로컬 함수로 넣을 수 있지만, 중첩된 함수의 깊이가 깊어지면 코드 가독성이 떨어져 일반적으로는 한 단계만 함수를 중첩시키라고 권장한다.