▩ 목 차 ▩
1. 기본 요소: 함수와 변수
1-1. Hello, World!
1-2. 함수
1-2-1. 식이 본문인 함수
1-3. 변수
1-3-1. 변경 가능한 변수와 변경 불가능한 변수
1-4. 더 쉽게 문자열 형식 지정: 문자열 템플릿
2. 클래스와 프로퍼티
2-1. 프로퍼티
2-2. 커스텀 접근자
2-3. 코틀린 소스코드 구조: 디렉터리와 패키지
3. 선택 표현과 처리: enum과 when
3-1. enum 클래스 정의
3-2. when으로 enum 클래스 다루기
3-3. when과 임의의 객체를 함께 사용
3-4. 인자가 없는 when 사용
3-5. 스마트 캐스트 : 타입 검사와 타입 캐스트를 조합
3-6. 리팩토링: if를 when으로 변경
3-7. if와 when의 분기에서 블록 사용
3-8. 대상을 이터레이션 : while과 for 루프
3-8-1. while 루프
3-8-2. 수에 대한 이터레이션: 범위와 수열
3-8-3. 맵에 대한 이터레이션
3-8-4. in으로 컬렉션이나 범위의 원소 검사
3-9. 코틀린의 예외 처리
3-9-1. try, catch, finally
3-9-2. try를 식으로 사용
4. 정리
■ 1. 기본 요소: 함수와 변수 ■
■ 1-1. Hello, World!
고전 예제인 'Hello, World!를 찍는 프로그램으로 시작해보자.
코틀린에서는 함수 하나로 그런 프로그램을 만들 수 있다.
fun main(args: Array<String>) {
println("Hello World!")
}
Hello World!
위의 코드에서 코틀린 문법이나 특성을 발견해보자.
- 함수를 선언할 때 fun 키워드를 사용한다.
- 파라미터 이름 뒤에 그 파라미터의 타입을 쓴다.
- 함수를 최상위 수준에 정의할 수 있다. 즉, 꼭 클래스 안에 함수를 넣어야 할 필요가 없다.
- 배열도 일반적인 클래스와 마찬가지다. 코틀린에는 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
- System.out.println 대신에 println이라고 쓴다. 코틀린 표준 라이브러리는 여러 가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싸 래퍼(wrapper)를 제공한다. println도 그런 함수 중 하나다.
- 최신 프로그래밍 언어 경향과 마찬가지로 줄 끝에 세미콜론(;)을 붙이지 않아도 된다.
■ 1-2. 함수
Hello Wolrd! 예제 경우 아무런 값도 반환하지 않는 함수이다. 하지만 의미 있는 결과를 반환하는 함수의 경우 반환 값의 타입을 어디에 지정해야 할까?
import kotlin.math.max
fun main(args: Array<String>) {
println( max(10, 20));
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
}
20
위의 fun(함수)를 봐보자.
함수 선언은 fun 키워드로 시작한다. fun 다음에는 함수 이름이 온다. 예제는 max라는 이름의 함수다. 함수 이름 뒤에는 괄호 안에 매개변수 목록이 온다. 함수의 반환타입은 매개변수 목록의 닫는 괄호 다음에 오는데, 괄호와 반환 타입 사이를 콜론(:)으로 구분해야 한다.
코틀린 if(값을 만들어내지 못하는)문장이 아니고 결과를 만드는 식(expression)이라는 점이 흥미롭다.
이 예제의 if식은 3항 연산자로 작성한 (a>b) ? a : b 식과 비슷하다.
*문(statement)과 식(expression)의 구분
코틀린에서 if는 식이지 문이 아니다. 식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면, 문은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있다. 자바에서는 모든 제어 구조가 문인 반면 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식이다. ==> 따라서 아주 간결하게 표현할 수 있다.
■ 1-2-1. 식이 본문인 함수
전에 살펴본 함수를 더 간결하게 표현할 수도 있다. 앞의 함수 본문은 if식 하나로만 이뤄져 있다. 이런 경우 다음과 같이 중괄호를 없애고 return을 제거하면서 등호(=)를 식 앞에 붙이면 더 간결하게 표현할 수 있다.
fun max(a:Int , b:Int) : Int = if(a>b) a else b
본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부르고, 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 부른다.
[ *인텔리J 아이디어 팁 : 인텔리J 아이디어는 이 두 방식의 함수를 서로 변환하는 메뉴가 있다. 각각은 '식 본문으로 변환'과 '블록 본문으로 변환' 이다 ]
코틀린에서는 식이 본문인 함수가 자주 쓰인다. 그런 함수의 본문 식에는 단순한 산술식이나 함수 호출 식뿐 아니라 if, when, try 등의 더 복잡한 식도 자주 쓰인다.
그리고 반환 타입을 생략하면 max 함수를 더 간략하게 만들 수 있다.
fun max(a:Int , b:Int) = if(a>b) a else b
여기서 반환 타입을 생략할 수 있는 이유는 무엇일까?
==> 코틀린은 정적 타입 지정 언어이므로 컴파일 시점에 모든 식의 타입을 지정해야 하지 않은가..[ 실제로 모든 변수나 모든 식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야 한다. 하지만 식이 본문인 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다.이렇게 컴파일러가 타입을 분석해 프로그래머 대신 프로그램 구성 요소의 타입을 정해주는 기능을 타입추론 이라 부른다.
여기서 유의할점이 식이 본문인 함수의 반환 타입만 생략 가능하다는 점이다. 블록이 본문인 함수가 값을 반환한다면 반드시 반환 타입을 지정하고 return문을 사용해 반환 값을 명시해야 한다. [ 이렇게 코틀린 언어를 설계한 의도는 반환타입을 명시하고 return을 쓴다면 함수가 어떤 타입의 값을 반환하고 어디서 그런 값을 반환하는지 더 쉽게 알아볼 수 있기 때문이다. ]
■ 1-3. 변수
자바에서는 변수를 선언할 때 타입이 맨 앞에 온다. 코틀린에서는 타입 지정을 생략하는 경우가 흔하다.
타입으로 번수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별할 수 없다.
==> 그런 이유로 코틀린에서는 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략하게 허용한다.
val question = "삶, 우주, 모든 것에 대한 궁극적인 질문"
val asnwer = 42
위는 타입 표기를 생략했다.
val answer: Int = 42
위는 타입을 명시했다.
어떻게 하면 타입 표기를 생략 할 수 있을까?
==> 식이 본문인 함수에서와 마찬가지로 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서 초기화 식의 타입을 변수 타입으로 지정한다. 즉, 초기화 식의 유무에 따라 타입을 명시를 해야하고 안해야 하는것을 결정한다. [ 왜냐하면 초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에 컴파일러가 타입을 추론할 수 없다. ]
■ 1-3-1. 변경 가능한 변수와 변경 불가능한 변수
변수 선언시 사용하는 키워드는 2가지가 있다.
- val(value에서 따옴) : 변경 불가능한(immutable)참조를 저장하는 변수다. val로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능하다. 자바로 말하자면 final 변수에 해당하낟.
- var(variable에서 따옴) : 변경 가능한(mutable)참조다. 이런 변수의 값은 바뀔 수 있다. 자바의 일반 변수에 해당한다.
기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하라.
변경 불가능한 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 코드가 함수형 코드에 가까워진다.
val 변수는 블록을 실행할 때 정확히 한번만 초기화 돼야 한다. 하지만 어떤 블록이 실행될 때 오직 초기화 문장만 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화 할 수도 있다. 아래 예시를 참고하자.
[EX] - 초기화가 안된 val변수를 블록안에서 여러값으로 초기화
val message: String
if (canPerformOperation()) {
message = "Success"
//... 연산을 수행한다.
}
else {
message = "Failed"
}
val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다. 아래 예시를 보자
val languages = arrayListOf("Java") // 불변 참조를 선언한다.
languages.add("Kotlin") // 참조가 가리키는 객체 내부를 변경한다.
var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다. 아래 예시를 보자.
var answer = 42
answer = "no answer" // "Error: type mismatch" 컴파일 오류 발생
위의 코드에서 컴파일 오류가 발생한 이유는 컴파일러가 기대하는 타입이 내가 입력한 타입과 다르기 때문이다.
[ 어떤 타입의 변수에 다른 타입의 값을 저장하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형변환 해야한다. 추후에 배움 ]
■ 1-4. 더 쉽게 문자열 형식 지정: 문자열 템플릿
아래 예제를 보자.
[EX] - 문자열 템플릿
fun main(args: Array<String>) {
val name = if (args.size >0) args[0] else "Kotlin"
println("Hello, $name!")
}
위의 코드는 "Bob"을 인자로 넘기면 "Hello, Bob!"을 출력하고 아무 인자도 없으면 "Hello, Kotlin!"을 출력한다.
위 예제는 문자열 템플릿 이라는 기능을 보여준다. 이 코드는 name이라는 변수를 선언하고 그 다음 줄에 있는 문자열 리터럴("") 안에서 그 변수를 사용했다. [ 여러 스크립트 언어와 비슷하게 코틀린에서도 변수를 문자열 안에 사용할 수 있다. ]
문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에 $를 추가해야 한다.
즉 이 문자열 템플릿은 자바의 문자열 접합 연산("Hello, " + name +"!")과 동일한 기능하을 하지만, 좀 더 간결하고 효율적이다.
[ 존재하지 않는 변수를 문자열 템플릿 안에서 사용하면 컴파일 오류가 발생한다. 또한 $ 문자를 문자열에 넣고 싶으면 \를 사용해 $를 이스케이프 시켜야 한다. ]
문자열 템플릿 안에 사용할 수 있는 대상은 간단히 변수 이름만으로 한정되지 않는다. 복잡한 식도 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣을 수 있다.
[EX] - 복잡한 식 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣기
fun main(args: Array<String>) {
if (args.size>0) {
println("Hello, ${args[0]}!")
}
}
또한 중괄호로 둘러싼 식 안에서 큰 따옴표를 사용할 수도 있다.
[EX] - 중괄호 식 안에서 큰따옴표 사용
fun main(args: Array<String>) {
println("Hello, ${if(args.size>0) args[0] else "someone"}!")
}
■ 2. 클래스와 프로퍼티 ■
클래스를 선언하는 기본 문법을 알아보자.
간단하게 자바 Kotlin을 사용하여 클래스 예제를 만들어 비교해보자.
[EX]- 자바를 이용하여 Person Class 생성
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
자바-코틀린 변환기는 자바 코드를 같은 일을 하는 코틀린 코드로 변환해준다. 변환기를 써서 위의 Person 클래스를 코틀린으로 변환해보자.
[EX] - 자바로 만든 Person 클래스를 자바-코틀린 변환기를 이용하여 코틀린으로 변환하기
class Person(val name: String)
이런 유형의 클래스를 값 객체(value Object)라 부르며, 다양한 언어가 값 객체를 간결하게 기술할 수 있는 구문을 제공한다.
자바를 코틀린으로 변환한 결과, public 접근제어자가 사라졌다.
==> 왜냐하면 코틀린의 기본 접근제어자는 public이므로 이런 경우 생략해도 된다.
■ 2-1. 프로퍼티
- 필드(field) : 클래스 내의 맴버변수
- 프로퍼티(property) : 필드와 게터 세터를 한데 묶어서 부르는 단어
==> 코틀린은 맴버변수라는 말 대신 프로퍼티라는 말을 사용합니다. 코틀린은 맴버변수에 대한 기본 접근자 메서드(default accessor method)를 자동으로 구현해주기 때문
클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다.
자바에서는 데이터를 필드에 저장하며, 멤버 필드의 접근제어자는 보통 private다. 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메서드를 제공한다. 보통은 필드를 읽기 위한 getter를 제공하고 필드를 변경하게 허용해야 할 경우 setter를 추가 제공 할 수 있다. 이런 예를 앞의 Person 클래스에서도 볼 수 있다.
자바에서는 필드와 접근자를 묶어 "프로퍼티" 라고 부르며, 프로퍼티라는 개념을 활용하는 프레임워크가 많다.
코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다. 클래스에서 프로퍼티를 성넌할 때는 앞에서 살펴본 변수를 선언하는 방법과 마찬가지로 val이나 var를 사용한다.
val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.
[EX] - Kotlin 클래스 안에서 변경 가능한 프로퍼티 선언하기
class Person(
val name: String, //읽기 전용 프로퍼티로, 코틀린은(비공개)필드와 필드를 읽는 단순한 (공개) 게터를 만들어낸다.
var isMarried:Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개)필드, (공개) 게터, (공개) 세터를 만들어낸다.
)
기본적으로 코틀린에서 프로퍼티를 선언하는 방식은
읽기 전용 프로퍼티의 경우 게터만 선언하며 변경할 수 있는 프로퍼티의 경우 게터와 세터를 모두 선언한다.
코틀린의 name 프로퍼티를 자바 쪽에서는 getName이라는 이름으로 볼 수 있다.
게터와 세터의 이름을 정하는 규칙에는 예외가 있다.
==> 이름이 is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고 원래 이름을 그대로 사용하며, 세터에는 is를 set으로 바꾼 이름을 사용한다. 따라서 자바에서 ismarried 프로퍼티의 게터를 호출하려면 isMarried()를 사용해야 한다.
아래 예제를 보며 자바와 코틀린에서 Person 클래스를 사용하는 것을 비교해보자.
[EX] - 자바에서 Person 클래스를 사용하는 방법(게터)
Person person = new Person("Bob", true);
System.out.println(person.getName());
System.out.println(person.isMarried());
[EX] - 코틀린에서 Person 클래스 사용하기(게터,세터)
val Person = Person("Bob",true) //new 키워드를 사용하지 않고 생성자를 호출한다.
println(Person.name) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
println(Person.isMarried) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
Person.isMarried=false; // 프로퍼티 이름을 직접 사용해 값을 넣어준다.(세터)
Person.name="setter" // 프로퍼티 이름을 직접 사용해 값을 넣어준다.(세터)
[ setName(값)과 getName이라는 접근자를 제공하는 자바 클래스에서 사용할때 코틀린에서는 name이라는 프로퍼티를 사용하여 똑같은 기능을 가질 수 있다. ( 객체.name(게터), 객체.name=값(세터) ) 자바 클래스가 isMarried(값)와 setMarried 메서드를 제공한다면 그에 상응하는 코틀린 프로퍼티의 이름은 isMarried다. ]
즉, 간단하게 말해서 클래스의 변수들은 게터 세터의 기능을 추가하지 않아도 가진다고 생각하면 편하다.
■ 2-2. 커스텀 접근자
이번에는 프로퍼티의 접근자를 직접 작성하는 방법을 알아보자. [위에서는 그냥 매개변수로 간단히 게터 세터만 구현 한 것, 커스텀 접근자는 직접 변수를 할당하고 get()메소드를 이용하여 return값을 얻어냄.]
직사각형 클래스인 Rectangle을 정의하면서 정사각형인지 알려주는 기능을 만들어본다고 가정을 하자. 직사각형이 정사각형인지를 별도의 필드에 저장할 필요가 없다. 즉, 사각형의 너비와 높이가 같은지 검사하면 정사각형 여부를 그때그때 알 수 있다.
[EX] - 커스텀 접근자
class Rectangle (val height:Int, val width: Int){
val isSquare:Boolean
get() {
return height == width
}
}
isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요 없다. 이 프로퍼티에는 자체 구현을 제공하는 게터만 존재한다. 클라이언트가 프로퍼티에 접근할 때마다 게터가 프로퍼티 값을 매번 다시 계산한다. 블록 본문을 이용하지 않으려면, get() = height == width라고 해도 된다. [ 쉽게 말해서 프로퍼티를 생성하고 get() 메소드를 프로퍼티 바로 뒤에 작성을 하면 객체.프로퍼티(커스텀 접근자)를 하게 되면 get안에 있는 retrun 값이 나오게 되는것이다. 즉, get()메소드를 이용해 프로퍼티의 원하는 값을 구성할 수 있다는 것이다. ]
앞의 커스텀 게터는 앞에서 살펴본 프로퍼티와 마찬가지 방식으로 사용 가능하다는 것이다.
여기서 get() 메소드를 간단히 get() = height == width 로 나타낼 수 있다.
fun main(args:Array<String>) {
val Rectangle = Rectangle(11,11)
println(Rectangle.isSquare)
}
true
파라미터가 없는 함수를 정의하는 방식과 커스텀 게터를 정의하는 방식은 모두 비슷하다. 단지 차이는 가독성 뿐이다.
■ 2-3. 코틀린 소스코드 구조: 디렉터리와 패키지
자바의 경우 모든 클래스를 패키지 단위로 관리한다.
코틀린에도 자바와 비슷한 개념의 패키지가 있다. 모든 코틀린 파일의 맨 앞에 package문을 넣을 수 있다.
==> 그러면 그 파일 안에 있는 모든 선언(클래스, 함수, 프로퍼티 등)이 해당 패키지에 들어간다.
같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.
반면, 다른 패키지에 정의한 선언을 사용하려면 임포트를 통해 선언을 불러와야 한다.
자바와 마찬가지로 임포트문은 파일의 맨 앞에 와야 하며, import 키워드를 사용한다. 예제를 보자.
[EX] - 클래스와 함수 선언을 패키지에 넣기
package shapes // 패키지 선언
import java.lang.Math.random // 표준 자바 라이브러리 클래스를 임포트
class Rectangle(var height: Int, val width: Int){ //패키지 선언
val isSquare:Boolean
get() = height == width
val num:Int
get() = height
}
fun createRandomRectangle() : Rectangle{
val random = random();
return Rectangle(random.toInt(),random.toInt())
}
코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 import 키워드로 가져올 수 있다.
[EX] - 다른 패키지에 있는 함수 임포트하기
package example
import shapes.createRandomRectangle //이름으로 함수 임포트하기
fun main(args:Array<String>){
println(createRandomRectangle().isSquare)
}
코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 import 키워드로 가져올 수 있다.
==> 패키지 이름 뒤에 .*를 추가하면 패키지 안의 모든 선언을 임포트 할 수 있다. 이런 * 임포트를 사용하면 패키지 안에 있는 모든 클래스뿐 아니라 최상위에 정의된 프로퍼티까지 모두 불러온다는 점에 유의해야 한다.
즉, 구체적인 임포트문 import sahpes.createRandomRectagle 대신에 import sahpes.*를 사용해도 컴파일에 아무 문제가 없다
자바의 경우 디렉터리 구조가 패키지 구조를 그대로 따라야 한다. 코틀린의 경우 패키지 구조와 디렉터리 구조가 맞아 떨어질 필요는 없다. 하지만 자바와 같이 구성하는 편이 좋다.[ 자바와 코틀린을 함께 사용하는 프로젝트에서 코틀린의 방식인 패키지 구조와 디렉터리 구조가 맞아 떨어지지 않으면 마이그레이션할 때 문제가 생길 수 있다. ]
또한 코틀린에서는 여러 클래스를 한 파일에 넣을 수 있고 파일의 이름도 마음대로 정할 수 있다.
==> 이것은 사용해도 무방하다. 왜냐하면 코틀린에서는 클래스 소스코드 크기가 작은 경우가 자주 있기 때문이다.
■ 3. 선택 표현과 처리: enum과 when ■
여기서는 when에 대해 설명한다. when은 자바의 switch를 대치하되 훨씬 더 강력하며, 앞으로 더 자주 사용할 프로그래밍 요소라고 생각할 수 있다. when에 대해 배우기 전에 코틀린에서 enum을 선언하는 방법과 스마트 캐스트에 대해서도 살펴본다.
■ 3-1. enum 클래스 정의
예제를 보자.
[EX] - 간단한 enum 클래스 정의하기
enum class list2_10_enum {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야 하는 흔치 않은 예이다.
코틀린에서는 enum class를 사용하지만 자바에서는 enum을 사용한다. 코틀린에서 enum은 소프트 키워드(soft keyword)라 부르는 존재다. enum은 class 앞에 있을 때는 특별한 의미를 지니지만 다른 곳에서는 이름에 사용할 수 있다. 반면 class는 키워드라 다른곳에서 사용할 수 없다. 따라서 class라는 이름을 사용할 수 없으므로 클래스를 표현하는 변수 등을 정의할 때는 clazz나 aClass와 같은 이름을 사용해야 한다.
자바와 마찬가지로 enum은 단순히 값만 열거하는 존재가 아니다. enum 클래스 안에도 프로퍼티나 메서드를 정의할 수 있다. 예제를 보자.
[EX]
enum class Color(
val r: Int, val g: Int, val b: Int // 상수의 프로퍼티를 정의한다.
) {
RED(255,0,0), ORANGE(255,165,0), // 각 상수를 생성할 때 그에 대한 프로퍼티 값을 지정한다.
YELLOW(255,255,0), GREEM(0,255,0), BLUE(0,0,255),
INDIGO(75,0,130), VIOLET(238,130,238); // 마지막 여기 세미콜론을 반드시 사용해야 한다.
fun rgb() = (r * 256 + g) * 256 + b // enum 클래스 안에서 메서드를 정의한다.
}
fun main(args:Array<String>){
println(Color.BLUE.rgb())
}
255
enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다. 각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야만 한다.
또한 이 예제에서는 코틀린에서 유일하게 세미콜론(;)이 필수인 부분을 볼 수 있다. enum 클래스 안에 메서드를 정의하는 경우 반드시 enum 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야 한다.
■ 3-2. when으로 enum 클래스 다루기
예를 들어, 무지개의 각 색에 대해 그와 상응하는 연상 단어를 짝지어주는 함수가 필요할때, 자바의 경우 swtich문을 사용할 것이다.
코틀린에서는 switch에 해당하는 구성요소는 when이다. if와 마찬가지로 when도 값을 만들어내는 식이다
==> 식이 본문인 함수에 when을 바로 사용할 수 있다.
[EX] - when을 사용해 올바른 enum 값 찾기
class list2_1_when {
}
fun getMneonic(color: Color) =
when (color){
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEM -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
fun main(args:Array<String>){
println(getMneonic(Color.BLUE))
}
Battle
위의 코드는 color로 전달된 값과 같은 분기를 찾는다. 자바와 달리 각 분기의 끝에 break를 넣지 않아도 된다.
성공적으로 매치되는 분기를 찾으면 switch는 그 분기를 실행한다.
한 분기 안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.[ 아래 예제 참고]
[EX] - 한 when 분기 안에 여러 값 사용하기
fun getWarmth(color: Color) =
when (color){
Color.RED, Color.ORANGE, Color.YELLOW -> "warn"
Color.GREEM -> "netural"
Color.BLUE,Color.INDIGO,Color.VIOLET->"cold"
}
fun main(args:Array<String>){
println(getWarmth(Color.BLUE))
}
cold
앞의 두 예제는 Color.YELLOW처럼 Color라는 enum 클래스 이름을 enum 상수 이름 앞에 붙인 전체 이름을 사용했다.
==> 상수 값을 임포트하면 이 코드를 더 간단하게 만들 수 있다.
package list2
import list2.Color // 다른 패키지에서 정의한 Color 클래스를 임포트한다.
import list2.Color.* //짧은 이름으로 사용하기 위해 enum 상수를 모두 임포트 한다.
class list2_14 {
}
fun getWarmth2(color: Color) =
when (color){
RED, ORANGE, YELLOW -> "warn"
GREEM -> "netural"
BLUE,INDIGO,VIOLET->"cold"
}
fun main(args:Array<String>){
println(getWarmth2(BLUE))
}
cold
■ 3-3. when과 임의의 객체를 함께 사용
코틀린에서 when은 자바의 switch보다 훨씬 더 강력하다. enum 상수나 숫자 리터럴만을 사용할 수 있는 자바 switch와 달리 코틀린 when 경우에는 임의의 객체를 허용한다.
두 색을 혼합했을 때 미리 정해진 팔레트에 들어있는 색이 될 수 있는 함수를 예시로 보자.
[EX] - when의 분기 조건에 여러 다른 객체 사용하기 (setOf())
package list2
import list2.Color.*
class list2_15_when {
}
fun mix(c1 : Color, c2 : Color) =
when (setOf(c1, c2)){ // when 식의 인자로 아무 객체나 사용할 수 있다. when은 이렇게 인자로 받은 객체가 각 분기 조건에 있는 객체와 같은지 테스트한다.
setOf(RED,YELLOW) -> ORANGE // 두 색을 혼합해서 다른 색을 만들 수 있는 경우를 열거한다.
setOf(YELLOW,BLUE) -> GREEM
setOf(BLUE,VIOLET) -> INDIGO
else -> throw Exception("Dirty color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
}
fun main(args:Array<String>){
println(mix(RED,YELLOW))
}
ORANGE
위의 코드를 보자.
c1과 c2가 RED와 YELLO라면(또는 YELLO와 RED라면) 그 둘을 혼합한 결과는 ORANGE다.
==> 이를 구현하기 위해 집합 비교를 사용한다.
코틀린 표준 라이브러리에는 인자로 전달받은 여러 객체를 그 객체들을 포함하는 집합인 Set 객체로 만드는 SetOf라는 함수가 있다. Set(집합)은 원소의 순서는 중요하지 않다.
==> 따라서 setOf(c1,c2)와 setOf(RED,YELLOW)가 같다는 말은 c1이 RED이고 c2가 YELLO거나, c1이 YELLOW이고 c2가 RED라는 말이다.
when식은 인자 값과 매치하는 조건 값을 찾을 때까지 각 분기를 검사한다. 여기서는 setOf(c1,c2)와 분기 조건에 있는 객체를 매치할 때 동등성을 사용한다.
==> 즉, 처음에는 setOf(1,2)와 setOf(RED, YELLOW)를 비교하고, 그 둘이 같지 않으면 계속 다음 분기의 조건 객체와 setOf(c1,c2)를 차례로 비교하는 식으로 작동한다. 모든 분기 식에서 만족하는 조건을 찾을 수 없다면 else 분기의 문장을 계산한다.
[ when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성 가능 ]
■ 3-4. 인자가 없는 when 사용
3-3의 예제가 비효율적임을 눈치 챘을 것이다. 이 함수는 호출될 때마다 함수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 Set 인스턴스를 생성한다. 보통은 이런 비효율성은 크게 문제가 되지 않는다.
==> 하지만 이 함수가 자주 호출된다면 불필요한 가비지 객체가 늘어날 것이다. 이것을 방지하기 위해 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다. [ 코드는 약간 읽기 어려워지지만 성능을 더 향상시키기 위해 이 정도 비용을 감수하자 ]
[EX] - 인자가 없는 when
package list2
import list2.Color.*
class list2_16_when {
}
fun mixOptimized(c1 : Color, c2 : Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEM
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty Color")
}
fun main(args:Array<String>){
println(mixOptimized(RED,YELLOW))
}
ORANGE
when에 아무 인자도 없으려면 각 분기의 조건이 Boolean 결과를 계산하는 식이어야 한다.
앞의 mix함수와 비교를 해본다면 추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다..
■ 3-5. 스마트 캐스트 : 타입 검사와 타입 캐스트를 조합
식을 인코딩하는 방법을 살펴보자. 아래 예제부터 보자.
[EX] - 식을 표현하는 클래스 계층
package list2
interface Expr
class Num(val value: Int) : Expr //value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현한다.
class Sum(val left : Expr, val right:Expr) : Expr //Expr 타입의 객체라면 어떤 것이나 Sum 연산의 인자가 될 수 있다. 따라서 Num이나 다른 Sum이 인자로 올 수 있다.
위 코드를 보자.
Sum은 Expr의 왼쪽과 오른쪽 인자에 대한 참조를 left와 right 프로퍼티로 저장한다.
이 예제에서 left와 right는 각각 Num이나 Sum일 수 있다.
예를들어, (1+2)+4 라는 식의 구조 객체를 표현한다면 , Sum(Sum(Num(1), Num(2)), Num(4))라는 구조 객체가 생긴다.
그림으로 표현하자면
Expr 인터페이스에는 두 가지 구현 클래스가 존재한다. 따라서 식을 평가하려면 두 가지 경우를 고려해야 한다.
- 어떤 식이 수라면 그 값을 반환한다.
- 어떤 식이 합계라면 좌항과 우항의 값을 계산한 다음에 그 두 값을 합한 값을 반환한다.
자바 스타일로 작성한 함수를 먼저 살펴본 다음 코틀린 스타일로 만든 함수를 살펴보자.
아래의 예제는 코틀린에서 if를 써서 자바 스타일로 함수 작성한 것이다.
[EX] - if 연쇄를 사용해 식을 계산하기(자바 스타일)
fun eval(e: Expr) : Int{
if(e is Num){
val n = e as Num // Num 타입으로 변환하는데, 이는 불필요한 중복이다.
return n.value
}
if(e is Sum){
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("Unknown expression")
}
fun main(args:Array<String>){
// println(Sum(Sum(Num(1),Num(2)),Num(4)))
println(eval(Sum(Sum(Num(1),Num(2)),Num(4))))
}
7
위의 코드를 보자.
코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다. 하지만 자바에서 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 한다.
==> 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 즉, 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 하지만 실제로는 컴파일러가 캐스팅을 수행해준다. 이를 스마트 캐스트라고 부른다.
eval 함수에서 e의 타입이 Num인지 검사한 다음 부분에서는 컴파일러는 e의 타입을 Num으로 해석한다. 그렇기 때문에 Num의 프로퍼티인 value를 명시적 캐스팅 없이 e.value로 사용할 수 있다.
sum의 프로퍼티인 right와 left도 마찬가지다. sum 타입인지 검사한 다음부터는 e.right와 e.left를 사용할 수 있다.
[ IDE를 사용하면 스마트 캐스트 부분의 배경색을 달리 표시해주므로 이런 변환이 자동으로 이뤄졌음을 쉽게 알 수 있다. ]
스마트 캐스트는 is로 변수에 든 값을 검사한 다음에 그 값이 바뀔수 없는 경우에만 작동한다. 예를 들어, 앞의 예제처럼 클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val 이어야 하며 커스텀 접근자를 사용한 것이어도 안된다.
즉, val이 아니거나 val이지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신 할 수 없기때문이다.
만약 원하는 타입으로 명시적으로 타입 캐스팅하려면 as 키워드를 사용한다.
val n = e as Num
■ 3-6. 리팩토링: if를 when으로 변경
코틀린의 if ( a > b) a else b는 자바의 a > b ? a : b처럼 작동한다.
==> 즉, 코틀린에서는 if가 값을 만들어내기 때문에 자바와 달리 3항 연산자가 따로 없다. 이런 특성을 사용하면 eval 함수에서 return문과 중괄호를 없애고 if식을 본문으로 사용해 더 간단하게 만들 수 있다. 아래 예제를 보자
[EX] - 값을 만들어내는 if 식
package list2
class list2_19_if {
}
fun eval2(e: Expr) : Int =
if (e is Num) {
e.value
} else if ( e is Sum) {
eval2(e.right) + eval2(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
fun main(args:Array<String>){
println(eval2(Sum(Num(1),Num(2))))
}
3
if의 분기에 식이 하나밖에 없다면 중괄호를 생략해도 된다 . if 분기에 블록을 사용하는 경우 그 블록의 마지막 식이 그 분기의 결과 값이다.
위의 코드를 when을 사용해 더 다듬자.
[EX] - if 중첩 대신 when 사용하기
package list2
class list2_20_when {
}
fun eval3(e: Expr) : Int =
when (e) {
is Num -> e.value
is Sum -> eval3(e.left) + eval3(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
fun main(args:Array<String>){
println(eval3(Sum(Num(3),Num(6))))
}
9
when 식을 앞에서 살펴본 값 동등성 검사가 아닌 다른 기능에도 쓸 수 있다. 이 예제는 받은 값의 타입을 검사하는 when 분기를 보여준다.
이 예제도 타입을 검사하고 나면 스마트 캐스트가 이뤄진다.
==> 따라서 Num이나 Sum의 멤버에 접근할 때 변수를 강제로 캐스팅 할 필요가 없다.
when과 if를 대신할 수 있는 경우가 언제인지 살펴보자.
■ 3-7. if와 when의 분기에서 블록 사용
if나 when 모두 분기에 블록을 사용할 수 있다. 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.
아래 예제를 보며 이해하자.
[EX] - 분기에 복잡한 동작이 들어가 있는 when 사용하기
package list2
import java.lang.IllegalArgumentException
class list2_21_when {
}
fun evalWithLogging(e: Expr) : Int =
when(e) {
is Num -> {
println("num : ${e.value}")
e.value // 이 식이 블록의 마지막 식이므로 e의 타입이 Num이면 e.value가 반환된다.
}
is Sum -> {
val left = evalWithLogging( e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right
}
else -> throw IllegalArgumentException("Unkown expression")
}
fun main(args:Array<String>){
println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))
}
num : 1
num : 2
sum: 1 + 2
num : 4
sum: 3 + 4
7
위의 코드를 보자. [ 여기서 위의 코드는 식이 본문인 함수여서 반환 타입을 생략할 수 있을 수 알았지만 컴파일 오류가 난다. 이유가 뭘까..?]
로그의 연산이 순서대로 이루어진 것을 알 수 있다.
또한 제일 중요한 것은 '블록의 마지막 식이 블록의 결과'라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립힌다.
■ 3-8. 대상을 이터레이션 : while과 for 루프
코틀린 특성 중 자바와 가장 비슷한 것이 이터레이션이다.
코틀린 while 루프는 자바와 동일하므로 간략하게 다룬다.
for는 자바의 for-each 루프에 해당하는 형태만 존재한다.
■ 3-8-1. while 루프
코틀린에는 while과 do-while 루프가 있다. 두 루프의 문법은 자바와 다르지 않다.
while (조건) { // 조건이 참인 동안 본문을 반복 실행한다.
/*...*/
}
do {
/*...*/
} while(조건) // 맨 처음에 무조건 본문을 한 번 실행한 다음, 조건이 참인 동안 본문을 반복 실행한다.
■ 3-8-2. 수에 대한 이터레이션: 범위와 수열
코틀린에는 자바의 for루프(어떤 변수를 초기화하고 그 변수를 루프를 한번 실행할 때마다 갱신하고 루프 조건이 거짓이 될 때 반복을 마치는 형태의 루프)에 해당하는 요소가 없다. 이런 루프의 가장 흔한 초깃값, 증가값, 최종값을 사용한 루프를 대신하기 위해 코틀린에서는 범위(range)를 사용한다.
[ 범위는 기본적으로 두 값으로 이뤄진 구간이다. 보통은 그 두 값은 정수 등의 숫자 타입의 값이며 .. 연산자로 시작 값과 끝 값을 연결해서 범위를 만든다. EX) val oneToTen = 1..10 ]
코틀린의 범위는 폐구간(닫힌 구간)또는 양끝을 포함하는 구간이다. 이는 두번째 같이 항상 범위에 포함된다는 뜻이다.
정수 범위로 수행할 수 있는 가장 단순한 작업은 범위에 속한 모든 값에 대한 이터레이션 이다.
순차적으로 수를 세면서 3으로 나눠떨어지는 수에 대해서는 피즈, 5로 나눠떨어지는 수에 대해서는 버즈라고 말해야 하고, 3과 5로 모두 나눠떨어지면 피즈버즈 라고 말을 하는것을 만들어보자.
[EX] - when을 사용해 피즈버즈 게임 구현하기
package list2
class list2_22_fizzBuzz {
}
fun fizzBuzz(i:Int) = when{
i % 15 == 0 -> "FizzBuzz "
i % 3 == 0 -> "Fizz "
i % 5 == 0 -> "Buzz"
else -> "$i"
}
fun main(args:Array<String>){
for(i in 1..100 ) {
println(fizzBuzz(i))
}
}
[ 위의 소스에서 when에다 i 값을 넣은 것은 왜 안되는지 의문이다. when(i) 공부해본 결과 when에 매개변수가 없어야지만 블록안에서 boolean 값을 다룰 수 있기 때문에 안된다는 것이다. 만약 when에 매개변수 없이 코딩을 하면 어떻게 될 것인가 ..?]
[EX] - 증가 값을 갖고 범위 이터레이션하기(100부터 거꾸로 세되 짝수만으로 게임을 진행)
for(i in 100 downTo 1 step 2 ) {
println(fizzBuzz(i))
}
여기서는 증가 값 step을 갖는 수열에 대해 이터레이션한다. 증가 값을 사용하면 수를 건널 뛸 수 있다.
증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다. 만드는 방법은 downTo를 이용하면 된다. 이 예제에서는 100 downTo 1로 역방향 수열로 만들었다. 그 뒤에 step 2 를 붙이면 증가 값의 절댓값이 2로 바뀐다. [ 이때 증가 값의 방향은 바뀌지 않음 ]
만약 끝 값을 포함하지 않는 반만 닫힌 범위에 대해 이터레이션하면 편할때가 자주 있다. 이런 범위를 만들고 싶다면 until 함수를 사용하면 된다. 예를들어, (x in 0 until size)라는 루프는 for(x in 0..size-1)과 같지만 좀 더 명확하게 개념을 표현한다.
■ 3-8-3. 맵에 대한 이터레이션
예제로 문자에 대한 2진 표현을 맵에 저장하고 출력해보자.
[EX] - 맵을 초기화하고 이터레이션하기
package list2
import java.util.*
val binaryReps = TreeMap<Char, String>() // 키에 해 정렬하기 위해 TreeMap을 사용한다.
fun mapex() {
for (c in 'A'..'F') { //A부터 F까지 문자의 범위를 사용해 이터레이션한다.
val binary = Integer.toBinaryString(c.toInt()) // 아스키 코드를 2진 표현으로 바꾼다.
binaryReps[c] = binary //c를 키로 c의 2진 표현을 맵에 넣는다.
}
for((letter,binary) in binaryReps) { // 맵에 대해 이터레이션한다. 맵의 키와 값을 두 변수에 각각 대입한다.
println("$letter = $binary")
}
}
fun main(args :Array<String>){
mapex()
}
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
.. 연산자를 숫자 타입의 값 뿐 아니라 문자 타입의 값에도 적용할 수 있다. 위 예제의 'A'..'F'는 A부터 F에 이르는 문자를 모두 포함하는 범위를 만든다.
위의 예제는 for 루프를 사용해 이터레이션하련는 컬렉션의 원소를 푸는 방법을 보여준다.[맵은 키/값 쌍을 원소로 하는 컬렉션이다.]
원소를 풀어서 letter아 binary라는 두 변수에 저장한다. letter에는 키가 들어가고, binary에는 2진 표현이 들어간다. get과 put을 사용하는 대신 map[key]=value를 사용해 값을 가져오고 설정할 수 있다.
즉, binaryReps[c] = binary 코드는 binaryReps.put(c, binary) 라는 자바 코드와 같다는 것이다.
위에서 사용했던 구조 분해 구문을 맵이 아닌 컬렉션에도 활용할 수 있다.
==> 구조 분해 구문을 사용하면 원소의 현재 인덱스를 유지하며서 컬렉션을 이터레이션 할 수 있다. [ 인덱스를 저장하기 위한 변수를 별도로 선언하고 루프에서 매번 그 변수를 증가시킬 필요가 없다. ] 아래 예제를 보자.
val list = arrayListOf("10","11","1001")
fun listindex() {
for((index, element) in list.withIndex()){
println("$index: $element")
}
}
0: 10
1: 11
2: 1001
[ withIndex의 정체에 대해서는 3장에서 살펴봄 ]
컬렉션이나 범위에 대해 in 키워드를 사용하고 어떤 값이 범위나 컬렉션에 들어있는지 알고 싶을 때도 in을 사용한다.
■ 3-8-4. in으로 컬렉션이나 범위의 원소 검사
in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
다음 예제를 보며 어떤 문자가 정해진 문자의 범위에 속하는지를 검사하는 방법을 보자.
[EX] - in을 사용해 값이 범위에 속하는지 검사하기
package list2
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
fun main(args :Array<String>){
println(isLetter('a'))
println(isNotDigit('3'))
}
true
false
위의 예제를 보니 어떤 문자가 글자인지 검사하는 방법은 간단해 보인다. 이러한 비교 로직은 표준 라이브러리의 범위 클래스 안에 깔끔하게 감춰져 있다. [ c in 'a'..'z' ] ==> [ 'a'<= c && c <= 'z' ] 로 변환된다.
또한 in과 !in 연산자를 when 식에서 사용해도 된다.
[EX] - when에서 in 사용하기
fun recognize(c:Char) = when(c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't Know..."
}
fun main(args :Array<String>){
// println(isLetter('a'))
// println(isNotDigit('3'))
println(recognize('3'))
}
It's a digit!
범위는 문자에만 국한되지 않는다. 비교가 가능한 클래스라면 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.
■ 3-9. 코틀린의 예외 처리
코틀린의 예외처리는 자바나 다른 언어의 예외 처리와 비슷하다. 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다. 함수를 호출하는 쪽에서는 그 예외를 잡아 처리할 수 있다. 발생한 예외를 함수 호출 단에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올때까지 예외를 다시 던진다.
코틀린의 기본 예외 처리 구문은 자바와 비슷하다. 예외를 던지는 방법은 전혀 놀랍지 않다.
if(percentage !in 0..100) {
throw IllegalArgumentException{
"A percentage value must be between 0 and 100: $percentage")
}
다른 클래스와 마찬가지로 예외 인스턴스를 만들때도 new를 붙일 필요가 없다.
자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.
var percentage =
if(number in 0..100)
number
else
throw IllegalArgumentException(
"A percentage value must be between 0 and 100: $number")
위의 예제는 if의 조건이 참이므로 프로그램이 정상 동작해서 percentage 변수가 number의 값으로 초기화 한다. 하지만 조건이 거짓이면 변수가 초기화되지 않는다.
■ 3-9-1. try, catch, finally
자바와 마찬가지로 예외를 처리하려면 try와 catch, finally절을 함께 사용한다. 파일에서 각 줄을 읽어 수로 변환하되 그 줄이 올바른 형태가 아니면 null을 반환하는 다음 예제를 보자.
fun readNumber(reader: BufferedReader) : Int ? { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) { // 예외타입을 :의 오른쪽에 쓴다.
return null
}
finally { // finally는 자바와 똑같이 작동한다.
reader.close()
}
}
fun main(args :Array<String>){
// println(isLetter('a'))
// println(isNotDigit('3'))
// println(recognize('3'))
// println("Kotlin" in setOf("Java","Scala"))
val reader = BufferedReader(StringReader("239"))
println(readNumber(reader))
}
239
자바 코드와 가장 큰 차이는 throws(이 경우 s가 붙어있다)절이 코드에 없다는 점이다.
자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOException을 붙여야 한다. 이유는 IOException이 체크 예외이기 때문이다.
자바에서는 체크 예외를 명시적으로 처리해야 한다. 어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 모두 catch로 처리해야 하며, 처리하지 않은 예외는 threow절에 명시해야 한다.
코틀린은 체크 예외와 언체크 예외를 구별하지 않는다. 코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다.
■ 3-9-2. try를 식으로 사용
자바와 코틀린의 중요한 차이를 하나 더 살펴보기 위해 방금 살펴본 예제를 고쳐보자.
finally 절을 없애고 파일에서 읽은 수를 출력하는 코드를 추가하자.
[EX] - try를 식으로 사용하기
package list2
import java.io.BufferedReader
import java.io.StringReader
import java.lang.NumberFormatException
fun readNumber2(reader: BufferedReader) { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
println(number)
}
fun main(args :Array<String>){
val reader = BufferedReader(StringReader("not a number"))
readNumber2(reader)
}
코틀린의 try 키워드는 if나 when과 마친가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다. if와 달리 try의 본문을 반드시 중괄호{}로 둘러싸야한다. 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.
위의 예제는 catch 블록안에서 return문을 사용한다.
==> 따라서, 예외가 발생한 경우 ctach 블록 다음의 코드는 실행되지 않는다.[현재는 return만을 입력해주었기 때문이다.] 하지만 계속 실행하고 싶다면 catch 블록도 값을 만들어야 한다. 역시 catch 블록도 그 안의 마지막 식이 블록 전체의 값이 된다. 예를 보고 이해하자.
[EX] - catch에서 값 반환하기
fun readNumber2(reader: BufferedReader) { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
// return
null
}
println(number)
}
fun main(args :Array<String>){
val reader = BufferedReader(StringReader("not a number"))
readNumber2(reader)
}
null
위 예제를 보면 try 코드 블록의 실행이 정상적으로 끝나면 그 블록의 마지막 식의 값이 결과다. 예외가 발생하고 잡히면 그 예외에 해당하는 catch 블록의 값이 결과다. 위의 예제에서 Integer가 아닌 String을 넣었기 때문에 NuberFormatException이 발생하였고 cach 블록에 값을 만들었기 때문에 아무런 값이 출력 되는 것이 아닌 함수의 결과 값이 null이 출력된다.
■ 4. 정리 ■
- 함수를 정의할 때 fun 키워드를 사용한다. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.
- 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다. 변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
- 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.
- 다른 언어에도 있는 if는 코틀린에서 식이며, 값을 만들어낸다.
- 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.
- 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.
- for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 하지만 코틀린의 for는 자바의 for보다 더 편리하다. 특히, 맵을 이터레이션하거나 이터레이션하면서 컬렉션의 원소와 인덱스를 함께 사용해야 하는 경우 코틀린의 for가 더 편리하다.
- 1..5와 같은 식은 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다. 어떤 값이 범위 안에 들어 있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.
- 코틀린 예외 처리는 자바와 비슷하다. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.
'Kotlin > Kotlin in action' 카테고리의 다른 글
6장 코틀린 타입 시스템 (0) | 2022.10.04 |
---|---|
5장 람다로 프로그래밍 (1) | 2022.09.29 |
4장 클래스, 객체, 인터페이스 (1) | 2022.09.24 |
3장 함수 정의와 호출 (1) | 2022.09.23 |
1장 코틀린이란 무엇이며, 왜 필요한가? (1) | 2022.09.21 |
▩ 목 차 ▩
1. 기본 요소: 함수와 변수
1-1. Hello, World!
1-2. 함수
1-2-1. 식이 본문인 함수
1-3. 변수
1-3-1. 변경 가능한 변수와 변경 불가능한 변수
1-4. 더 쉽게 문자열 형식 지정: 문자열 템플릿
2. 클래스와 프로퍼티
2-1. 프로퍼티
2-2. 커스텀 접근자
2-3. 코틀린 소스코드 구조: 디렉터리와 패키지
3. 선택 표현과 처리: enum과 when
3-1. enum 클래스 정의
3-2. when으로 enum 클래스 다루기
3-3. when과 임의의 객체를 함께 사용
3-4. 인자가 없는 when 사용
3-5. 스마트 캐스트 : 타입 검사와 타입 캐스트를 조합
3-6. 리팩토링: if를 when으로 변경
3-7. if와 when의 분기에서 블록 사용
3-8. 대상을 이터레이션 : while과 for 루프
3-8-1. while 루프
3-8-2. 수에 대한 이터레이션: 범위와 수열
3-8-3. 맵에 대한 이터레이션
3-8-4. in으로 컬렉션이나 범위의 원소 검사
3-9. 코틀린의 예외 처리
3-9-1. try, catch, finally
3-9-2. try를 식으로 사용
4. 정리
■ 1. 기본 요소: 함수와 변수 ■
■ 1-1. Hello, World!
고전 예제인 'Hello, World!를 찍는 프로그램으로 시작해보자.
코틀린에서는 함수 하나로 그런 프로그램을 만들 수 있다.
fun main(args: Array<String>) {
println("Hello World!")
}
Hello World!
위의 코드에서 코틀린 문법이나 특성을 발견해보자.
- 함수를 선언할 때 fun 키워드를 사용한다.
- 파라미터 이름 뒤에 그 파라미터의 타입을 쓴다.
- 함수를 최상위 수준에 정의할 수 있다. 즉, 꼭 클래스 안에 함수를 넣어야 할 필요가 없다.
- 배열도 일반적인 클래스와 마찬가지다. 코틀린에는 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
- System.out.println 대신에 println이라고 쓴다. 코틀린 표준 라이브러리는 여러 가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싸 래퍼(wrapper)를 제공한다. println도 그런 함수 중 하나다.
- 최신 프로그래밍 언어 경향과 마찬가지로 줄 끝에 세미콜론(;)을 붙이지 않아도 된다.
■ 1-2. 함수
Hello Wolrd! 예제 경우 아무런 값도 반환하지 않는 함수이다. 하지만 의미 있는 결과를 반환하는 함수의 경우 반환 값의 타입을 어디에 지정해야 할까?
import kotlin.math.max
fun main(args: Array<String>) {
println( max(10, 20));
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
}
20
위의 fun(함수)를 봐보자.
함수 선언은 fun 키워드로 시작한다. fun 다음에는 함수 이름이 온다. 예제는 max라는 이름의 함수다. 함수 이름 뒤에는 괄호 안에 매개변수 목록이 온다. 함수의 반환타입은 매개변수 목록의 닫는 괄호 다음에 오는데, 괄호와 반환 타입 사이를 콜론(:)으로 구분해야 한다.
코틀린 if(값을 만들어내지 못하는)문장이 아니고 결과를 만드는 식(expression)이라는 점이 흥미롭다.
이 예제의 if식은 3항 연산자로 작성한 (a>b) ? a : b 식과 비슷하다.
*문(statement)과 식(expression)의 구분
코틀린에서 if는 식이지 문이 아니다. 식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면, 문은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있다. 자바에서는 모든 제어 구조가 문인 반면 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식이다. ==> 따라서 아주 간결하게 표현할 수 있다.
■ 1-2-1. 식이 본문인 함수
전에 살펴본 함수를 더 간결하게 표현할 수도 있다. 앞의 함수 본문은 if식 하나로만 이뤄져 있다. 이런 경우 다음과 같이 중괄호를 없애고 return을 제거하면서 등호(=)를 식 앞에 붙이면 더 간결하게 표현할 수 있다.
fun max(a:Int , b:Int) : Int = if(a>b) a else b
본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부르고, 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 부른다.
[ *인텔리J 아이디어 팁 : 인텔리J 아이디어는 이 두 방식의 함수를 서로 변환하는 메뉴가 있다. 각각은 '식 본문으로 변환'과 '블록 본문으로 변환' 이다 ]
코틀린에서는 식이 본문인 함수가 자주 쓰인다. 그런 함수의 본문 식에는 단순한 산술식이나 함수 호출 식뿐 아니라 if, when, try 등의 더 복잡한 식도 자주 쓰인다.
그리고 반환 타입을 생략하면 max 함수를 더 간략하게 만들 수 있다.
fun max(a:Int , b:Int) = if(a>b) a else b
여기서 반환 타입을 생략할 수 있는 이유는 무엇일까?
==> 코틀린은 정적 타입 지정 언어이므로 컴파일 시점에 모든 식의 타입을 지정해야 하지 않은가..[ 실제로 모든 변수나 모든 식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야 한다. 하지만 식이 본문인 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다.이렇게 컴파일러가 타입을 분석해 프로그래머 대신 프로그램 구성 요소의 타입을 정해주는 기능을 타입추론 이라 부른다.
여기서 유의할점이 식이 본문인 함수의 반환 타입만 생략 가능하다는 점이다. 블록이 본문인 함수가 값을 반환한다면 반드시 반환 타입을 지정하고 return문을 사용해 반환 값을 명시해야 한다. [ 이렇게 코틀린 언어를 설계한 의도는 반환타입을 명시하고 return을 쓴다면 함수가 어떤 타입의 값을 반환하고 어디서 그런 값을 반환하는지 더 쉽게 알아볼 수 있기 때문이다. ]
■ 1-3. 변수
자바에서는 변수를 선언할 때 타입이 맨 앞에 온다. 코틀린에서는 타입 지정을 생략하는 경우가 흔하다.
타입으로 번수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별할 수 없다.
==> 그런 이유로 코틀린에서는 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략하게 허용한다.
val question = "삶, 우주, 모든 것에 대한 궁극적인 질문"
val asnwer = 42
위는 타입 표기를 생략했다.
val answer: Int = 42
위는 타입을 명시했다.
어떻게 하면 타입 표기를 생략 할 수 있을까?
==> 식이 본문인 함수에서와 마찬가지로 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서 초기화 식의 타입을 변수 타입으로 지정한다. 즉, 초기화 식의 유무에 따라 타입을 명시를 해야하고 안해야 하는것을 결정한다. [ 왜냐하면 초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에 컴파일러가 타입을 추론할 수 없다. ]
■ 1-3-1. 변경 가능한 변수와 변경 불가능한 변수
변수 선언시 사용하는 키워드는 2가지가 있다.
- val(value에서 따옴) : 변경 불가능한(immutable)참조를 저장하는 변수다. val로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능하다. 자바로 말하자면 final 변수에 해당하낟.
- var(variable에서 따옴) : 변경 가능한(mutable)참조다. 이런 변수의 값은 바뀔 수 있다. 자바의 일반 변수에 해당한다.
기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var로 변경하라.
변경 불가능한 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 코드가 함수형 코드에 가까워진다.
val 변수는 블록을 실행할 때 정확히 한번만 초기화 돼야 한다. 하지만 어떤 블록이 실행될 때 오직 초기화 문장만 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화 할 수도 있다. 아래 예시를 참고하자.
[EX] - 초기화가 안된 val변수를 블록안에서 여러값으로 초기화
val message: String
if (canPerformOperation()) {
message = "Success"
//... 연산을 수행한다.
}
else {
message = "Failed"
}
val 참조 자체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다. 아래 예시를 보자
val languages = arrayListOf("Java") // 불변 참조를 선언한다.
languages.add("Kotlin") // 참조가 가리키는 객체 내부를 변경한다.
var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다. 아래 예시를 보자.
var answer = 42
answer = "no answer" // "Error: type mismatch" 컴파일 오류 발생
위의 코드에서 컴파일 오류가 발생한 이유는 컴파일러가 기대하는 타입이 내가 입력한 타입과 다르기 때문이다.
[ 어떤 타입의 변수에 다른 타입의 값을 저장하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형변환 해야한다. 추후에 배움 ]
■ 1-4. 더 쉽게 문자열 형식 지정: 문자열 템플릿
아래 예제를 보자.
[EX] - 문자열 템플릿
fun main(args: Array<String>) {
val name = if (args.size >0) args[0] else "Kotlin"
println("Hello, $name!")
}
위의 코드는 "Bob"을 인자로 넘기면 "Hello, Bob!"을 출력하고 아무 인자도 없으면 "Hello, Kotlin!"을 출력한다.
위 예제는 문자열 템플릿 이라는 기능을 보여준다. 이 코드는 name이라는 변수를 선언하고 그 다음 줄에 있는 문자열 리터럴("") 안에서 그 변수를 사용했다. [ 여러 스크립트 언어와 비슷하게 코틀린에서도 변수를 문자열 안에 사용할 수 있다. ]
문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에 $를 추가해야 한다.
즉 이 문자열 템플릿은 자바의 문자열 접합 연산("Hello, " + name +"!")과 동일한 기능하을 하지만, 좀 더 간결하고 효율적이다.
[ 존재하지 않는 변수를 문자열 템플릿 안에서 사용하면 컴파일 오류가 발생한다. 또한 $ 문자를 문자열에 넣고 싶으면 \를 사용해 $를 이스케이프 시켜야 한다. ]
문자열 템플릿 안에 사용할 수 있는 대상은 간단히 변수 이름만으로 한정되지 않는다. 복잡한 식도 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣을 수 있다.
[EX] - 복잡한 식 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣기
fun main(args: Array<String>) {
if (args.size>0) {
println("Hello, ${args[0]}!")
}
}
또한 중괄호로 둘러싼 식 안에서 큰 따옴표를 사용할 수도 있다.
[EX] - 중괄호 식 안에서 큰따옴표 사용
fun main(args: Array<String>) {
println("Hello, ${if(args.size>0) args[0] else "someone"}!")
}
■ 2. 클래스와 프로퍼티 ■
클래스를 선언하는 기본 문법을 알아보자.
간단하게 자바 Kotlin을 사용하여 클래스 예제를 만들어 비교해보자.
[EX]- 자바를 이용하여 Person Class 생성
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
자바-코틀린 변환기는 자바 코드를 같은 일을 하는 코틀린 코드로 변환해준다. 변환기를 써서 위의 Person 클래스를 코틀린으로 변환해보자.
[EX] - 자바로 만든 Person 클래스를 자바-코틀린 변환기를 이용하여 코틀린으로 변환하기
class Person(val name: String)
이런 유형의 클래스를 값 객체(value Object)라 부르며, 다양한 언어가 값 객체를 간결하게 기술할 수 있는 구문을 제공한다.
자바를 코틀린으로 변환한 결과, public 접근제어자가 사라졌다.
==> 왜냐하면 코틀린의 기본 접근제어자는 public이므로 이런 경우 생략해도 된다.
■ 2-1. 프로퍼티
- 필드(field) : 클래스 내의 맴버변수
- 프로퍼티(property) : 필드와 게터 세터를 한데 묶어서 부르는 단어
==> 코틀린은 맴버변수라는 말 대신 프로퍼티라는 말을 사용합니다. 코틀린은 맴버변수에 대한 기본 접근자 메서드(default accessor method)를 자동으로 구현해주기 때문
클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다.
자바에서는 데이터를 필드에 저장하며, 멤버 필드의 접근제어자는 보통 private다. 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메서드를 제공한다. 보통은 필드를 읽기 위한 getter를 제공하고 필드를 변경하게 허용해야 할 경우 setter를 추가 제공 할 수 있다. 이런 예를 앞의 Person 클래스에서도 볼 수 있다.
자바에서는 필드와 접근자를 묶어 "프로퍼티" 라고 부르며, 프로퍼티라는 개념을 활용하는 프레임워크가 많다.
코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다. 클래스에서 프로퍼티를 성넌할 때는 앞에서 살펴본 변수를 선언하는 방법과 마찬가지로 val이나 var를 사용한다.
val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.
[EX] - Kotlin 클래스 안에서 변경 가능한 프로퍼티 선언하기
class Person(
val name: String, //읽기 전용 프로퍼티로, 코틀린은(비공개)필드와 필드를 읽는 단순한 (공개) 게터를 만들어낸다.
var isMarried:Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개)필드, (공개) 게터, (공개) 세터를 만들어낸다.
)
기본적으로 코틀린에서 프로퍼티를 선언하는 방식은
읽기 전용 프로퍼티의 경우 게터만 선언하며 변경할 수 있는 프로퍼티의 경우 게터와 세터를 모두 선언한다.
코틀린의 name 프로퍼티를 자바 쪽에서는 getName이라는 이름으로 볼 수 있다.
게터와 세터의 이름을 정하는 규칙에는 예외가 있다.
==> 이름이 is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고 원래 이름을 그대로 사용하며, 세터에는 is를 set으로 바꾼 이름을 사용한다. 따라서 자바에서 ismarried 프로퍼티의 게터를 호출하려면 isMarried()를 사용해야 한다.
아래 예제를 보며 자바와 코틀린에서 Person 클래스를 사용하는 것을 비교해보자.
[EX] - 자바에서 Person 클래스를 사용하는 방법(게터)
Person person = new Person("Bob", true);
System.out.println(person.getName());
System.out.println(person.isMarried());
[EX] - 코틀린에서 Person 클래스 사용하기(게터,세터)
val Person = Person("Bob",true) //new 키워드를 사용하지 않고 생성자를 호출한다.
println(Person.name) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
println(Person.isMarried) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
Person.isMarried=false; // 프로퍼티 이름을 직접 사용해 값을 넣어준다.(세터)
Person.name="setter" // 프로퍼티 이름을 직접 사용해 값을 넣어준다.(세터)
[ setName(값)과 getName이라는 접근자를 제공하는 자바 클래스에서 사용할때 코틀린에서는 name이라는 프로퍼티를 사용하여 똑같은 기능을 가질 수 있다. ( 객체.name(게터), 객체.name=값(세터) ) 자바 클래스가 isMarried(값)와 setMarried 메서드를 제공한다면 그에 상응하는 코틀린 프로퍼티의 이름은 isMarried다. ]
즉, 간단하게 말해서 클래스의 변수들은 게터 세터의 기능을 추가하지 않아도 가진다고 생각하면 편하다.
■ 2-2. 커스텀 접근자
이번에는 프로퍼티의 접근자를 직접 작성하는 방법을 알아보자. [위에서는 그냥 매개변수로 간단히 게터 세터만 구현 한 것, 커스텀 접근자는 직접 변수를 할당하고 get()메소드를 이용하여 return값을 얻어냄.]
직사각형 클래스인 Rectangle을 정의하면서 정사각형인지 알려주는 기능을 만들어본다고 가정을 하자. 직사각형이 정사각형인지를 별도의 필드에 저장할 필요가 없다. 즉, 사각형의 너비와 높이가 같은지 검사하면 정사각형 여부를 그때그때 알 수 있다.
[EX] - 커스텀 접근자
class Rectangle (val height:Int, val width: Int){
val isSquare:Boolean
get() {
return height == width
}
}
isSquare 프로퍼티에는 자체 값을 저장하는 필드가 필요 없다. 이 프로퍼티에는 자체 구현을 제공하는 게터만 존재한다. 클라이언트가 프로퍼티에 접근할 때마다 게터가 프로퍼티 값을 매번 다시 계산한다. 블록 본문을 이용하지 않으려면, get() = height == width라고 해도 된다. [ 쉽게 말해서 프로퍼티를 생성하고 get() 메소드를 프로퍼티 바로 뒤에 작성을 하면 객체.프로퍼티(커스텀 접근자)를 하게 되면 get안에 있는 retrun 값이 나오게 되는것이다. 즉, get()메소드를 이용해 프로퍼티의 원하는 값을 구성할 수 있다는 것이다. ]
앞의 커스텀 게터는 앞에서 살펴본 프로퍼티와 마찬가지 방식으로 사용 가능하다는 것이다.
여기서 get() 메소드를 간단히 get() = height == width 로 나타낼 수 있다.
fun main(args:Array<String>) {
val Rectangle = Rectangle(11,11)
println(Rectangle.isSquare)
}
true
파라미터가 없는 함수를 정의하는 방식과 커스텀 게터를 정의하는 방식은 모두 비슷하다. 단지 차이는 가독성 뿐이다.
■ 2-3. 코틀린 소스코드 구조: 디렉터리와 패키지
자바의 경우 모든 클래스를 패키지 단위로 관리한다.
코틀린에도 자바와 비슷한 개념의 패키지가 있다. 모든 코틀린 파일의 맨 앞에 package문을 넣을 수 있다.
==> 그러면 그 파일 안에 있는 모든 선언(클래스, 함수, 프로퍼티 등)이 해당 패키지에 들어간다.
같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.
반면, 다른 패키지에 정의한 선언을 사용하려면 임포트를 통해 선언을 불러와야 한다.
자바와 마찬가지로 임포트문은 파일의 맨 앞에 와야 하며, import 키워드를 사용한다. 예제를 보자.
[EX] - 클래스와 함수 선언을 패키지에 넣기
package shapes // 패키지 선언
import java.lang.Math.random // 표준 자바 라이브러리 클래스를 임포트
class Rectangle(var height: Int, val width: Int){ //패키지 선언
val isSquare:Boolean
get() = height == width
val num:Int
get() = height
}
fun createRandomRectangle() : Rectangle{
val random = random();
return Rectangle(random.toInt(),random.toInt())
}
코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 import 키워드로 가져올 수 있다.
[EX] - 다른 패키지에 있는 함수 임포트하기
package example
import shapes.createRandomRectangle //이름으로 함수 임포트하기
fun main(args:Array<String>){
println(createRandomRectangle().isSquare)
}
코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 import 키워드로 가져올 수 있다.
==> 패키지 이름 뒤에 .*를 추가하면 패키지 안의 모든 선언을 임포트 할 수 있다. 이런 * 임포트를 사용하면 패키지 안에 있는 모든 클래스뿐 아니라 최상위에 정의된 프로퍼티까지 모두 불러온다는 점에 유의해야 한다.
즉, 구체적인 임포트문 import sahpes.createRandomRectagle 대신에 import sahpes.*를 사용해도 컴파일에 아무 문제가 없다
자바의 경우 디렉터리 구조가 패키지 구조를 그대로 따라야 한다. 코틀린의 경우 패키지 구조와 디렉터리 구조가 맞아 떨어질 필요는 없다. 하지만 자바와 같이 구성하는 편이 좋다.[ 자바와 코틀린을 함께 사용하는 프로젝트에서 코틀린의 방식인 패키지 구조와 디렉터리 구조가 맞아 떨어지지 않으면 마이그레이션할 때 문제가 생길 수 있다. ]
또한 코틀린에서는 여러 클래스를 한 파일에 넣을 수 있고 파일의 이름도 마음대로 정할 수 있다.
==> 이것은 사용해도 무방하다. 왜냐하면 코틀린에서는 클래스 소스코드 크기가 작은 경우가 자주 있기 때문이다.
■ 3. 선택 표현과 처리: enum과 when ■
여기서는 when에 대해 설명한다. when은 자바의 switch를 대치하되 훨씬 더 강력하며, 앞으로 더 자주 사용할 프로그래밍 요소라고 생각할 수 있다. when에 대해 배우기 전에 코틀린에서 enum을 선언하는 방법과 스마트 캐스트에 대해서도 살펴본다.
■ 3-1. enum 클래스 정의
예제를 보자.
[EX] - 간단한 enum 클래스 정의하기
enum class list2_10_enum {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야 하는 흔치 않은 예이다.
코틀린에서는 enum class를 사용하지만 자바에서는 enum을 사용한다. 코틀린에서 enum은 소프트 키워드(soft keyword)라 부르는 존재다. enum은 class 앞에 있을 때는 특별한 의미를 지니지만 다른 곳에서는 이름에 사용할 수 있다. 반면 class는 키워드라 다른곳에서 사용할 수 없다. 따라서 class라는 이름을 사용할 수 없으므로 클래스를 표현하는 변수 등을 정의할 때는 clazz나 aClass와 같은 이름을 사용해야 한다.
자바와 마찬가지로 enum은 단순히 값만 열거하는 존재가 아니다. enum 클래스 안에도 프로퍼티나 메서드를 정의할 수 있다. 예제를 보자.
[EX]
enum class Color(
val r: Int, val g: Int, val b: Int // 상수의 프로퍼티를 정의한다.
) {
RED(255,0,0), ORANGE(255,165,0), // 각 상수를 생성할 때 그에 대한 프로퍼티 값을 지정한다.
YELLOW(255,255,0), GREEM(0,255,0), BLUE(0,0,255),
INDIGO(75,0,130), VIOLET(238,130,238); // 마지막 여기 세미콜론을 반드시 사용해야 한다.
fun rgb() = (r * 256 + g) * 256 + b // enum 클래스 안에서 메서드를 정의한다.
}
fun main(args:Array<String>){
println(Color.BLUE.rgb())
}
255
enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다. 각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야만 한다.
또한 이 예제에서는 코틀린에서 유일하게 세미콜론(;)이 필수인 부분을 볼 수 있다. enum 클래스 안에 메서드를 정의하는 경우 반드시 enum 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야 한다.
■ 3-2. when으로 enum 클래스 다루기
예를 들어, 무지개의 각 색에 대해 그와 상응하는 연상 단어를 짝지어주는 함수가 필요할때, 자바의 경우 swtich문을 사용할 것이다.
코틀린에서는 switch에 해당하는 구성요소는 when이다. if와 마찬가지로 when도 값을 만들어내는 식이다
==> 식이 본문인 함수에 when을 바로 사용할 수 있다.
[EX] - when을 사용해 올바른 enum 값 찾기
class list2_1_when {
}
fun getMneonic(color: Color) =
when (color){
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEM -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
fun main(args:Array<String>){
println(getMneonic(Color.BLUE))
}
Battle
위의 코드는 color로 전달된 값과 같은 분기를 찾는다. 자바와 달리 각 분기의 끝에 break를 넣지 않아도 된다.
성공적으로 매치되는 분기를 찾으면 switch는 그 분기를 실행한다.
한 분기 안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.[ 아래 예제 참고]
[EX] - 한 when 분기 안에 여러 값 사용하기
fun getWarmth(color: Color) =
when (color){
Color.RED, Color.ORANGE, Color.YELLOW -> "warn"
Color.GREEM -> "netural"
Color.BLUE,Color.INDIGO,Color.VIOLET->"cold"
}
fun main(args:Array<String>){
println(getWarmth(Color.BLUE))
}
cold
앞의 두 예제는 Color.YELLOW처럼 Color라는 enum 클래스 이름을 enum 상수 이름 앞에 붙인 전체 이름을 사용했다.
==> 상수 값을 임포트하면 이 코드를 더 간단하게 만들 수 있다.
package list2
import list2.Color // 다른 패키지에서 정의한 Color 클래스를 임포트한다.
import list2.Color.* //짧은 이름으로 사용하기 위해 enum 상수를 모두 임포트 한다.
class list2_14 {
}
fun getWarmth2(color: Color) =
when (color){
RED, ORANGE, YELLOW -> "warn"
GREEM -> "netural"
BLUE,INDIGO,VIOLET->"cold"
}
fun main(args:Array<String>){
println(getWarmth2(BLUE))
}
cold
■ 3-3. when과 임의의 객체를 함께 사용
코틀린에서 when은 자바의 switch보다 훨씬 더 강력하다. enum 상수나 숫자 리터럴만을 사용할 수 있는 자바 switch와 달리 코틀린 when 경우에는 임의의 객체를 허용한다.
두 색을 혼합했을 때 미리 정해진 팔레트에 들어있는 색이 될 수 있는 함수를 예시로 보자.
[EX] - when의 분기 조건에 여러 다른 객체 사용하기 (setOf())
package list2
import list2.Color.*
class list2_15_when {
}
fun mix(c1 : Color, c2 : Color) =
when (setOf(c1, c2)){ // when 식의 인자로 아무 객체나 사용할 수 있다. when은 이렇게 인자로 받은 객체가 각 분기 조건에 있는 객체와 같은지 테스트한다.
setOf(RED,YELLOW) -> ORANGE // 두 색을 혼합해서 다른 색을 만들 수 있는 경우를 열거한다.
setOf(YELLOW,BLUE) -> GREEM
setOf(BLUE,VIOLET) -> INDIGO
else -> throw Exception("Dirty color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
}
fun main(args:Array<String>){
println(mix(RED,YELLOW))
}
ORANGE
위의 코드를 보자.
c1과 c2가 RED와 YELLO라면(또는 YELLO와 RED라면) 그 둘을 혼합한 결과는 ORANGE다.
==> 이를 구현하기 위해 집합 비교를 사용한다.
코틀린 표준 라이브러리에는 인자로 전달받은 여러 객체를 그 객체들을 포함하는 집합인 Set 객체로 만드는 SetOf라는 함수가 있다. Set(집합)은 원소의 순서는 중요하지 않다.
==> 따라서 setOf(c1,c2)와 setOf(RED,YELLOW)가 같다는 말은 c1이 RED이고 c2가 YELLO거나, c1이 YELLOW이고 c2가 RED라는 말이다.
when식은 인자 값과 매치하는 조건 값을 찾을 때까지 각 분기를 검사한다. 여기서는 setOf(c1,c2)와 분기 조건에 있는 객체를 매치할 때 동등성을 사용한다.
==> 즉, 처음에는 setOf(1,2)와 setOf(RED, YELLOW)를 비교하고, 그 둘이 같지 않으면 계속 다음 분기의 조건 객체와 setOf(c1,c2)를 차례로 비교하는 식으로 작동한다. 모든 분기 식에서 만족하는 조건을 찾을 수 없다면 else 분기의 문장을 계산한다.
[ when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성 가능 ]
■ 3-4. 인자가 없는 when 사용
3-3의 예제가 비효율적임을 눈치 챘을 것이다. 이 함수는 호출될 때마다 함수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 Set 인스턴스를 생성한다. 보통은 이런 비효율성은 크게 문제가 되지 않는다.
==> 하지만 이 함수가 자주 호출된다면 불필요한 가비지 객체가 늘어날 것이다. 이것을 방지하기 위해 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다. [ 코드는 약간 읽기 어려워지지만 성능을 더 향상시키기 위해 이 정도 비용을 감수하자 ]
[EX] - 인자가 없는 when
package list2
import list2.Color.*
class list2_16_when {
}
fun mixOptimized(c1 : Color, c2 : Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEM
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty Color")
}
fun main(args:Array<String>){
println(mixOptimized(RED,YELLOW))
}
ORANGE
when에 아무 인자도 없으려면 각 분기의 조건이 Boolean 결과를 계산하는 식이어야 한다.
앞의 mix함수와 비교를 해본다면 추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다..
■ 3-5. 스마트 캐스트 : 타입 검사와 타입 캐스트를 조합
식을 인코딩하는 방법을 살펴보자. 아래 예제부터 보자.
[EX] - 식을 표현하는 클래스 계층
package list2
interface Expr
class Num(val value: Int) : Expr //value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현한다.
class Sum(val left : Expr, val right:Expr) : Expr //Expr 타입의 객체라면 어떤 것이나 Sum 연산의 인자가 될 수 있다. 따라서 Num이나 다른 Sum이 인자로 올 수 있다.
위 코드를 보자.
Sum은 Expr의 왼쪽과 오른쪽 인자에 대한 참조를 left와 right 프로퍼티로 저장한다.
이 예제에서 left와 right는 각각 Num이나 Sum일 수 있다.
예를들어, (1+2)+4 라는 식의 구조 객체를 표현한다면 , Sum(Sum(Num(1), Num(2)), Num(4))라는 구조 객체가 생긴다.
그림으로 표현하자면
Expr 인터페이스에는 두 가지 구현 클래스가 존재한다. 따라서 식을 평가하려면 두 가지 경우를 고려해야 한다.
- 어떤 식이 수라면 그 값을 반환한다.
- 어떤 식이 합계라면 좌항과 우항의 값을 계산한 다음에 그 두 값을 합한 값을 반환한다.
자바 스타일로 작성한 함수를 먼저 살펴본 다음 코틀린 스타일로 만든 함수를 살펴보자.
아래의 예제는 코틀린에서 if를 써서 자바 스타일로 함수 작성한 것이다.
[EX] - if 연쇄를 사용해 식을 계산하기(자바 스타일)
fun eval(e: Expr) : Int{
if(e is Num){
val n = e as Num // Num 타입으로 변환하는데, 이는 불필요한 중복이다.
return n.value
}
if(e is Sum){
return eval(e.right) + eval(e.left)
}
throw IllegalArgumentException("Unknown expression")
}
fun main(args:Array<String>){
// println(Sum(Sum(Num(1),Num(2)),Num(4)))
println(eval(Sum(Sum(Num(1),Num(2)),Num(4))))
}
7
위의 코드를 보자.
코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다. 하지만 자바에서 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 한다.
==> 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 즉, 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 하지만 실제로는 컴파일러가 캐스팅을 수행해준다. 이를 스마트 캐스트라고 부른다.
eval 함수에서 e의 타입이 Num인지 검사한 다음 부분에서는 컴파일러는 e의 타입을 Num으로 해석한다. 그렇기 때문에 Num의 프로퍼티인 value를 명시적 캐스팅 없이 e.value로 사용할 수 있다.
sum의 프로퍼티인 right와 left도 마찬가지다. sum 타입인지 검사한 다음부터는 e.right와 e.left를 사용할 수 있다.
[ IDE를 사용하면 스마트 캐스트 부분의 배경색을 달리 표시해주므로 이런 변환이 자동으로 이뤄졌음을 쉽게 알 수 있다. ]
스마트 캐스트는 is로 변수에 든 값을 검사한 다음에 그 값이 바뀔수 없는 경우에만 작동한다. 예를 들어, 앞의 예제처럼 클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val 이어야 하며 커스텀 접근자를 사용한 것이어도 안된다.
즉, val이 아니거나 val이지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신 할 수 없기때문이다.
만약 원하는 타입으로 명시적으로 타입 캐스팅하려면 as 키워드를 사용한다.
val n = e as Num
■ 3-6. 리팩토링: if를 when으로 변경
코틀린의 if ( a > b) a else b는 자바의 a > b ? a : b처럼 작동한다.
==> 즉, 코틀린에서는 if가 값을 만들어내기 때문에 자바와 달리 3항 연산자가 따로 없다. 이런 특성을 사용하면 eval 함수에서 return문과 중괄호를 없애고 if식을 본문으로 사용해 더 간단하게 만들 수 있다. 아래 예제를 보자
[EX] - 값을 만들어내는 if 식
package list2
class list2_19_if {
}
fun eval2(e: Expr) : Int =
if (e is Num) {
e.value
} else if ( e is Sum) {
eval2(e.right) + eval2(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
fun main(args:Array<String>){
println(eval2(Sum(Num(1),Num(2))))
}
3
if의 분기에 식이 하나밖에 없다면 중괄호를 생략해도 된다 . if 분기에 블록을 사용하는 경우 그 블록의 마지막 식이 그 분기의 결과 값이다.
위의 코드를 when을 사용해 더 다듬자.
[EX] - if 중첩 대신 when 사용하기
package list2
class list2_20_when {
}
fun eval3(e: Expr) : Int =
when (e) {
is Num -> e.value
is Sum -> eval3(e.left) + eval3(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
fun main(args:Array<String>){
println(eval3(Sum(Num(3),Num(6))))
}
9
when 식을 앞에서 살펴본 값 동등성 검사가 아닌 다른 기능에도 쓸 수 있다. 이 예제는 받은 값의 타입을 검사하는 when 분기를 보여준다.
이 예제도 타입을 검사하고 나면 스마트 캐스트가 이뤄진다.
==> 따라서 Num이나 Sum의 멤버에 접근할 때 변수를 강제로 캐스팅 할 필요가 없다.
when과 if를 대신할 수 있는 경우가 언제인지 살펴보자.
■ 3-7. if와 when의 분기에서 블록 사용
if나 when 모두 분기에 블록을 사용할 수 있다. 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.
아래 예제를 보며 이해하자.
[EX] - 분기에 복잡한 동작이 들어가 있는 when 사용하기
package list2
import java.lang.IllegalArgumentException
class list2_21_when {
}
fun evalWithLogging(e: Expr) : Int =
when(e) {
is Num -> {
println("num : ${e.value}")
e.value // 이 식이 블록의 마지막 식이므로 e의 타입이 Num이면 e.value가 반환된다.
}
is Sum -> {
val left = evalWithLogging( e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right
}
else -> throw IllegalArgumentException("Unkown expression")
}
fun main(args:Array<String>){
println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))
}
num : 1
num : 2
sum: 1 + 2
num : 4
sum: 3 + 4
7
위의 코드를 보자. [ 여기서 위의 코드는 식이 본문인 함수여서 반환 타입을 생략할 수 있을 수 알았지만 컴파일 오류가 난다. 이유가 뭘까..?]
로그의 연산이 순서대로 이루어진 것을 알 수 있다.
또한 제일 중요한 것은 '블록의 마지막 식이 블록의 결과'라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립힌다.
■ 3-8. 대상을 이터레이션 : while과 for 루프
코틀린 특성 중 자바와 가장 비슷한 것이 이터레이션이다.
코틀린 while 루프는 자바와 동일하므로 간략하게 다룬다.
for는 자바의 for-each 루프에 해당하는 형태만 존재한다.
■ 3-8-1. while 루프
코틀린에는 while과 do-while 루프가 있다. 두 루프의 문법은 자바와 다르지 않다.
while (조건) { // 조건이 참인 동안 본문을 반복 실행한다.
/*...*/
}
do {
/*...*/
} while(조건) // 맨 처음에 무조건 본문을 한 번 실행한 다음, 조건이 참인 동안 본문을 반복 실행한다.
■ 3-8-2. 수에 대한 이터레이션: 범위와 수열
코틀린에는 자바의 for루프(어떤 변수를 초기화하고 그 변수를 루프를 한번 실행할 때마다 갱신하고 루프 조건이 거짓이 될 때 반복을 마치는 형태의 루프)에 해당하는 요소가 없다. 이런 루프의 가장 흔한 초깃값, 증가값, 최종값을 사용한 루프를 대신하기 위해 코틀린에서는 범위(range)를 사용한다.
[ 범위는 기본적으로 두 값으로 이뤄진 구간이다. 보통은 그 두 값은 정수 등의 숫자 타입의 값이며 .. 연산자로 시작 값과 끝 값을 연결해서 범위를 만든다. EX) val oneToTen = 1..10 ]
코틀린의 범위는 폐구간(닫힌 구간)또는 양끝을 포함하는 구간이다. 이는 두번째 같이 항상 범위에 포함된다는 뜻이다.
정수 범위로 수행할 수 있는 가장 단순한 작업은 범위에 속한 모든 값에 대한 이터레이션 이다.
순차적으로 수를 세면서 3으로 나눠떨어지는 수에 대해서는 피즈, 5로 나눠떨어지는 수에 대해서는 버즈라고 말해야 하고, 3과 5로 모두 나눠떨어지면 피즈버즈 라고 말을 하는것을 만들어보자.
[EX] - when을 사용해 피즈버즈 게임 구현하기
package list2
class list2_22_fizzBuzz {
}
fun fizzBuzz(i:Int) = when{
i % 15 == 0 -> "FizzBuzz "
i % 3 == 0 -> "Fizz "
i % 5 == 0 -> "Buzz"
else -> "$i"
}
fun main(args:Array<String>){
for(i in 1..100 ) {
println(fizzBuzz(i))
}
}
[ 위의 소스에서 when에다 i 값을 넣은 것은 왜 안되는지 의문이다. when(i) 공부해본 결과 when에 매개변수가 없어야지만 블록안에서 boolean 값을 다룰 수 있기 때문에 안된다는 것이다. 만약 when에 매개변수 없이 코딩을 하면 어떻게 될 것인가 ..?]
[EX] - 증가 값을 갖고 범위 이터레이션하기(100부터 거꾸로 세되 짝수만으로 게임을 진행)
for(i in 100 downTo 1 step 2 ) {
println(fizzBuzz(i))
}
여기서는 증가 값 step을 갖는 수열에 대해 이터레이션한다. 증가 값을 사용하면 수를 건널 뛸 수 있다.
증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다. 만드는 방법은 downTo를 이용하면 된다. 이 예제에서는 100 downTo 1로 역방향 수열로 만들었다. 그 뒤에 step 2 를 붙이면 증가 값의 절댓값이 2로 바뀐다. [ 이때 증가 값의 방향은 바뀌지 않음 ]
만약 끝 값을 포함하지 않는 반만 닫힌 범위에 대해 이터레이션하면 편할때가 자주 있다. 이런 범위를 만들고 싶다면 until 함수를 사용하면 된다. 예를들어, (x in 0 until size)라는 루프는 for(x in 0..size-1)과 같지만 좀 더 명확하게 개념을 표현한다.
■ 3-8-3. 맵에 대한 이터레이션
예제로 문자에 대한 2진 표현을 맵에 저장하고 출력해보자.
[EX] - 맵을 초기화하고 이터레이션하기
package list2
import java.util.*
val binaryReps = TreeMap<Char, String>() // 키에 해 정렬하기 위해 TreeMap을 사용한다.
fun mapex() {
for (c in 'A'..'F') { //A부터 F까지 문자의 범위를 사용해 이터레이션한다.
val binary = Integer.toBinaryString(c.toInt()) // 아스키 코드를 2진 표현으로 바꾼다.
binaryReps[c] = binary //c를 키로 c의 2진 표현을 맵에 넣는다.
}
for((letter,binary) in binaryReps) { // 맵에 대해 이터레이션한다. 맵의 키와 값을 두 변수에 각각 대입한다.
println("$letter = $binary")
}
}
fun main(args :Array<String>){
mapex()
}
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
.. 연산자를 숫자 타입의 값 뿐 아니라 문자 타입의 값에도 적용할 수 있다. 위 예제의 'A'..'F'는 A부터 F에 이르는 문자를 모두 포함하는 범위를 만든다.
위의 예제는 for 루프를 사용해 이터레이션하련는 컬렉션의 원소를 푸는 방법을 보여준다.[맵은 키/값 쌍을 원소로 하는 컬렉션이다.]
원소를 풀어서 letter아 binary라는 두 변수에 저장한다. letter에는 키가 들어가고, binary에는 2진 표현이 들어간다. get과 put을 사용하는 대신 map[key]=value를 사용해 값을 가져오고 설정할 수 있다.
즉, binaryReps[c] = binary 코드는 binaryReps.put(c, binary) 라는 자바 코드와 같다는 것이다.
위에서 사용했던 구조 분해 구문을 맵이 아닌 컬렉션에도 활용할 수 있다.
==> 구조 분해 구문을 사용하면 원소의 현재 인덱스를 유지하며서 컬렉션을 이터레이션 할 수 있다. [ 인덱스를 저장하기 위한 변수를 별도로 선언하고 루프에서 매번 그 변수를 증가시킬 필요가 없다. ] 아래 예제를 보자.
val list = arrayListOf("10","11","1001")
fun listindex() {
for((index, element) in list.withIndex()){
println("$index: $element")
}
}
0: 10
1: 11
2: 1001
[ withIndex의 정체에 대해서는 3장에서 살펴봄 ]
컬렉션이나 범위에 대해 in 키워드를 사용하고 어떤 값이 범위나 컬렉션에 들어있는지 알고 싶을 때도 in을 사용한다.
■ 3-8-4. in으로 컬렉션이나 범위의 원소 검사
in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
다음 예제를 보며 어떤 문자가 정해진 문자의 범위에 속하는지를 검사하는 방법을 보자.
[EX] - in을 사용해 값이 범위에 속하는지 검사하기
package list2
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
fun main(args :Array<String>){
println(isLetter('a'))
println(isNotDigit('3'))
}
true
false
위의 예제를 보니 어떤 문자가 글자인지 검사하는 방법은 간단해 보인다. 이러한 비교 로직은 표준 라이브러리의 범위 클래스 안에 깔끔하게 감춰져 있다. [ c in 'a'..'z' ] ==> [ 'a'<= c && c <= 'z' ] 로 변환된다.
또한 in과 !in 연산자를 when 식에서 사용해도 된다.
[EX] - when에서 in 사용하기
fun recognize(c:Char) = when(c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't Know..."
}
fun main(args :Array<String>){
// println(isLetter('a'))
// println(isNotDigit('3'))
println(recognize('3'))
}
It's a digit!
범위는 문자에만 국한되지 않는다. 비교가 가능한 클래스라면 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.
■ 3-9. 코틀린의 예외 처리
코틀린의 예외처리는 자바나 다른 언어의 예외 처리와 비슷하다. 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다. 함수를 호출하는 쪽에서는 그 예외를 잡아 처리할 수 있다. 발생한 예외를 함수 호출 단에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올때까지 예외를 다시 던진다.
코틀린의 기본 예외 처리 구문은 자바와 비슷하다. 예외를 던지는 방법은 전혀 놀랍지 않다.
if(percentage !in 0..100) {
throw IllegalArgumentException{
"A percentage value must be between 0 and 100: $percentage")
}
다른 클래스와 마찬가지로 예외 인스턴스를 만들때도 new를 붙일 필요가 없다.
자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.
var percentage =
if(number in 0..100)
number
else
throw IllegalArgumentException(
"A percentage value must be between 0 and 100: $number")
위의 예제는 if의 조건이 참이므로 프로그램이 정상 동작해서 percentage 변수가 number의 값으로 초기화 한다. 하지만 조건이 거짓이면 변수가 초기화되지 않는다.
■ 3-9-1. try, catch, finally
자바와 마찬가지로 예외를 처리하려면 try와 catch, finally절을 함께 사용한다. 파일에서 각 줄을 읽어 수로 변환하되 그 줄이 올바른 형태가 아니면 null을 반환하는 다음 예제를 보자.
fun readNumber(reader: BufferedReader) : Int ? { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) { // 예외타입을 :의 오른쪽에 쓴다.
return null
}
finally { // finally는 자바와 똑같이 작동한다.
reader.close()
}
}
fun main(args :Array<String>){
// println(isLetter('a'))
// println(isNotDigit('3'))
// println(recognize('3'))
// println("Kotlin" in setOf("Java","Scala"))
val reader = BufferedReader(StringReader("239"))
println(readNumber(reader))
}
239
자바 코드와 가장 큰 차이는 throws(이 경우 s가 붙어있다)절이 코드에 없다는 점이다.
자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOException을 붙여야 한다. 이유는 IOException이 체크 예외이기 때문이다.
자바에서는 체크 예외를 명시적으로 처리해야 한다. 어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 모두 catch로 처리해야 하며, 처리하지 않은 예외는 threow절에 명시해야 한다.
코틀린은 체크 예외와 언체크 예외를 구별하지 않는다. 코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다.
■ 3-9-2. try를 식으로 사용
자바와 코틀린의 중요한 차이를 하나 더 살펴보기 위해 방금 살펴본 예제를 고쳐보자.
finally 절을 없애고 파일에서 읽은 수를 출력하는 코드를 추가하자.
[EX] - try를 식으로 사용하기
package list2
import java.io.BufferedReader
import java.io.StringReader
import java.lang.NumberFormatException
fun readNumber2(reader: BufferedReader) { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
println(number)
}
fun main(args :Array<String>){
val reader = BufferedReader(StringReader("not a number"))
readNumber2(reader)
}
코틀린의 try 키워드는 if나 when과 마친가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다. if와 달리 try의 본문을 반드시 중괄호{}로 둘러싸야한다. 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.
위의 예제는 catch 블록안에서 return문을 사용한다.
==> 따라서, 예외가 발생한 경우 ctach 블록 다음의 코드는 실행되지 않는다.[현재는 return만을 입력해주었기 때문이다.] 하지만 계속 실행하고 싶다면 catch 블록도 값을 만들어야 한다. 역시 catch 블록도 그 안의 마지막 식이 블록 전체의 값이 된다. 예를 보고 이해하자.
[EX] - catch에서 값 반환하기
fun readNumber2(reader: BufferedReader) { // 함수가 던질 수 있는 예외를 명시할 필요가 없다.
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
// return
null
}
println(number)
}
fun main(args :Array<String>){
val reader = BufferedReader(StringReader("not a number"))
readNumber2(reader)
}
null
위 예제를 보면 try 코드 블록의 실행이 정상적으로 끝나면 그 블록의 마지막 식의 값이 결과다. 예외가 발생하고 잡히면 그 예외에 해당하는 catch 블록의 값이 결과다. 위의 예제에서 Integer가 아닌 String을 넣었기 때문에 NuberFormatException이 발생하였고 cach 블록에 값을 만들었기 때문에 아무런 값이 출력 되는 것이 아닌 함수의 결과 값이 null이 출력된다.
■ 4. 정리 ■
- 함수를 정의할 때 fun 키워드를 사용한다. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.
- 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다. 변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
- 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.
- 다른 언어에도 있는 if는 코틀린에서 식이며, 값을 만들어낸다.
- 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.
- 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.
- for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 하지만 코틀린의 for는 자바의 for보다 더 편리하다. 특히, 맵을 이터레이션하거나 이터레이션하면서 컬렉션의 원소와 인덱스를 함께 사용해야 하는 경우 코틀린의 for가 더 편리하다.
- 1..5와 같은 식은 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다. 어떤 값이 범위 안에 들어 있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.
- 코틀린 예외 처리는 자바와 비슷하다. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.
'Kotlin > Kotlin in action' 카테고리의 다른 글
6장 코틀린 타입 시스템 (0) | 2022.10.04 |
---|---|
5장 람다로 프로그래밍 (1) | 2022.09.29 |
4장 클래스, 객체, 인터페이스 (1) | 2022.09.24 |
3장 함수 정의와 호출 (1) | 2022.09.23 |
1장 코틀린이란 무엇이며, 왜 필요한가? (1) | 2022.09.21 |