6장에서는 함수형 객체의 변경이 불가능한 상태 특징에 대해 설명한다. 이를 위해 책에서 분수(유리수)를 나타내는 클래스로 예를 든다.
따라서 피연산자가 되는 각 분수는 서로 다른 Rational 객체이다. 어떤 두 Rational 객체를 더하면, 그 합에 해당하는 새로운 Rational 객체가 생긴다.
이번 장을 마치면, Rational 클래스를 사용해 다음과 같은 일을 할 수 있다.
scala〉 val oneHalf = new Rational(l, 2)
oneHalf: Rational = 1/2
scala〉 val twoThirds = new Rational(2, 3)
twoThirds: Rational = 2/3
scala> (oneHalf / 7) + (1 - twoThirds)
resO: Rational = 17/42
Rational 객체는 변경 불가능한 객체라고 결정하였다.
변경 불가능한 객체는 인스턴스에 필요한 정보(분수 객체라면 분자,분모 값)를 생성 시점에 모두 제공하여 설계한다.
→ 생성한 다음에 값을 수정할 수 있게 되면 변경 가능한 객체가 된다.
class Rational(n: Int, d:Int) // n, d는 클래스 파라미터
◼️ 스칼라 컴파일러는 내부적으로 두 클래스 파라미터를 종합해서 클래스 파라미터와 같은 두 인자를 받는 주 생성자 (Primary Constructor)를 만든다.
◼️ Java에서는 클래스에 인자를 받는 생성자가 있지만, 스칼라에서는 클래스가 바로 인자를 받는다. 이러한 스칼라의 표기는 클래스 내부에서 파라미터를 바로 사용할 수 있어서 좀 더 간결하다.
필드를 정의하고 생성자의 인자를 필드로 복사하는 할당문 없어도 됨(자바와 다르게 따로 생성자를 만들지 않아도 된다. )
◼️ 스칼라 컴파일러는 클래스 내부에 있으면서 필드나 메소드 정의에 들어 있지 않은 코드를 주 생성자 내부로 밀어 넣는다.
예를 들면, 디버깅용 메시지를 출력하는 코드로 작성할 수 있다.
class Rational(n: Int, d:Int){
println("Created " + n + "/" + d)
}
====================
scala > new Rational(1, 2)
Created 1/2
res0: Rational = Rational02591e0c9
기본적으로 Class는 java.lang.Object 클래스에 있는 toString 메소드를 호출하게 되는데, toString은 클래스 이름,
@ 표시, 16진수 숫자를 출력한다. ex) Rational@2591e0c9
이러한 메소드들은 override 수식자를 통해 재정의 할 수 있다.
class Rational(n: Int, d: Int) {
override def toString = n + "/" + d
}
분수의 분모는 0일 수 없다.
따라서 객체를 생성할 때 정보가 유효한지 확인이 반드시 필요
선결 조건을 정의해서 해결 가능
require 문을 통해 선결 조건을 만들 수 있다.
class Rational(n: Int, d: Int) {
require(d != 0) // d == 0 이라면 IllegalArgumentException 예외 발생
override def toString = n + "/" + d
}
Rational이 변경 불가능한 객체이기 때문에 add 메소드가 객체 자체의 값을ㄹ 변경해서는 안된다.
따라서 더한 결과값을 담는 새로운 Rational 객체를 반환해야 한다.
// 아래 코드는 컴파일 에러가 발생한다.
class Rational(n: Int, d: Int) {
require(d != 0)
override def toString = n + "/" + d
def add(that: Rational): Rational =
new Rational( n * that.d + that.n * d, d * that.d )
}
따라서 변경 불가능한 객체의 클래스 파라미터에 접근하기 위해서는 필드를 따로 선언해줘야한다.
class Rational(n: Int, d: Int) {
require(d != 0)
val numer: Int = n
val denom: Int = d
override def toString = numer + "/" + denom
def add(that: Rational): Rational =
new Rational( numer * that.denom + that.numer * denom, denom * that.denom )
}
현재 실행중인 메소드의 호출 대상 인스턴스에 대한 참조를 자기 참조(self reference) 라고 한다.
생성
class Rational(n: Int, d: Int) {
require(d != 0)
val numer: Int = n // 분자
val denom: Int = d // 분모
override def toString = numer + "/" + denom
def add(that: Rational): Rational =
new Rational( numer * that.denom + that.numer * denom, denom * that.denom )
// 인자로 받은 Rational과 비교해 더 작은지 여부를 확인
// 자기 스스로를 참조한다.
def lessThan(that: Rational) =
this.numer * that.denom < that.numer * this.denom
def max(that: Rational) =
if (this.lessThan(that)) that else this
}
this.numer는 lessThan 메소드가 속한 객체의 분자를 나타낸다.
this를 생략할 수 없는 경우...
하나의 클래스에 여러 생성자가 필요한 경우 → 보조생성자 사용
ex ) 분모는 1로 미리 정해져 있고, 분자만을 인자로 받는 생성자
class Rational(n: Int, d: Int){
require(d!=0)
val numer : Int = n
val denom: Int = denom
def this(n: Int) = this(n, 1) // 보조 생성자
override def toString = numer + "/" + denom
def add(that: Rational) : Rational =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
}
scala> val y = new Rational(3)
y: Rational = 3/1
비공개 필드와 메소드가 있는 Rational 클래스
스칼라 컴파일러는 Rational 클래스의 필드 3개와 관련된 초기화 코드를 소스코드에 나온 순서대로 주 생성자에 위치 시킴 → g의 초기화 코드 gcd(n.abs, d,abs)는 다른 두 초기화 코드보다 먼저 수행됨
class Rational(n: Int, d: Int){
require(d!=0)
private val g = gcd(n.abs, d,abs) // 비공개 변수
val numer : Int = n / g
val denom: Int = denom / g
def this(n: Int) = this(n, 1) // 보조 생성자
override def toString = numer + "/" + denom
def add(that: Rational) : Rational =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
override def toString = numer + "/" + denom
private def gcd(a: Int, b:Int): Int = // 비공개 메소드
if(b == 0) a else gcd(b, a%b)
}
수학 기호로 기존의 add 메소드를 대체할 수 있다.
즉, 연산자를 재정의할 수 있다.
class Rational(n: Int, d: Int){
require(d!=0)
private val g = gcd(n.abs, d,abs) // 비공개 변수
val numer : Int = n / g
val denom: Int = denom / g
def this(n: Int) = this(n, 1) // 보조 생성자
override def toString = numer + "/" + denom
def + (that: Rational) : Rational =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
) // + 연산자 정의
def * (that: Rational) : Rational =
new Rational(numer * that.numer, denom * that.denom) // * 연산자 정의
override def toString = numer + "/" + denom
private def gcd(a: Int, b:Int): Int = // 비공개 메소드
if(b == 0) a else gcd(b, a%b)
}
scala> val x = new Rat onal(l, 2)
x: Rational = 1/2
scala> val y = new Rat onal(2, 3)
y: Rational = 2/3
scala> x + y
res7: Rational = 7/6
영숫자 식별자
- 문자나 밑줄(_)
로 시작, 두 번째 글자부터는 문자, 숫자, 밑줄 모두 사용 가능
- 특수문자 $
도 문자로 취급, 하지만 예약문자이기 떄문에 사용 X
- 자바의 관례에 따라 Camel-Case로 표기한다. ( ex) toString, HashSet )
- 필드, 메소드 인자, 지역 변수, 함수는 소문자로 시작
- 클래스는 대문자로 시작
- 상수는 첫 글자만 대문자로 표기 (자바의 관례와 다르다)
- 스칼라에서는 상수를 첫 글자만 대문자로 표기한다.
X_OFFSET
같은 자바 스타일의 상수 표기는 스칼라에서는 XOffset처럼 Camel-Case를 사용하는 것이 관례
연산자 식별자
- 하나 이상의 연산자 문자로 이루어짐
- 스칼라 컴파일러는 내부적으로 $
를 사용해 연산자 식별자를 해체하여 적합한 자바 식별자로 다시 만드는 작업을 수행
- 식별자 :->
는 내부적으로 $colon$minus$greater
로 바뀐다.
혼합 식별자
- 영문자와 숫자로 이뤄진 식별자의 뒤에 밑줄이 오고, 그 뒤에 연산자 식별자가 온다.
- unary_+
는 단항 연산자인 +를 정의하는 메소드의 이름
리터럴 식별자
- 역따옴표`...`
로 둘러싼 임의의 문자열
- 런타임이 인식할 수 있는 어떤 문자열이라도 역따옴표 사이에 넣을 수 있다.
- 스칼라 예약어조차도 리터럴 식별자로 정의할 수 있다.
ex ) yield는 스칼라의 예약어이므로 자바의 Thread 클래스에 있는 정적 메소드 yield에 접근하고 싶을 때, Thread.yield()와 같이 사용할 수 없다.
Thread.`yield()`
처럼 메소드 이름을 지정하면 사용할 수 있다.
오버로드한 여러 메소드를 추가한 Rational 클래스
메소드 호출 시점에 컴파일러는 오버로드한 메소드 중 인자의 타입이 일치하는 메소드를 선택 (자바와 유사)
class Rational(n: Int, d: Int){
require(d!=0)
private val g = gcd(n.abs, d,abs) // 비공개 변수
val numer : Int = n / g
val denom: Int = denom / g
def this(n: Int) = this(n, 1) // 보조 생성자
override def toString = numer + "/" + denom
def + (that: Rationa): Rational =
new Rational(numer * that.denom + that.numer * denom, denom * that.denom)
def + ( : Int): Rational =
new Rational(numer + i * denom, denom)
def - (that: Rational): Rational =
new Rational(numer * that.denom - that.numer * denom, denom * that.denom)
def - ( : Int): Rational =
new Rational(numer - i * denom, denom)
def * (that: Rational): Rational =
new Rational(numer * that.numer, denom * that.denom)
def * ( : Int): Rational =
new Rat onal(numer * i, denom)
def / (that: Rational): Rational =
new Rational(numer * that.denom, denom * that.numer)
def / (i: Int): Rational =
new Rational(numer, denom * i)
override def toString = numer + "/" + denom
private def gcd(a: Int, b:Int): Int = // 비공개 메소드
if(b == 0) a else gcd(b, a%b)
}
val x = new Rational(2, 3)
x * 2 // Int를 인자로 받는 * 메소드를 정의하였기 때문에 사용가능
2 * x // Error 발생
// int를 Rational로 변환하는 메소드 정의
// implicit 수식자는 컴파일러에서 몇몇 상황에 해당 메소드를 활용해 변환을 수행하라고 알려줌
implicit def inToRational(x: Int) = new Rational(x)
val r = new Rational(2,3)
println(2 * r)
===========================================
4/3
암시적 타입 변환이 동작하기 위해서는 해당 스코프 안네 변환 메소드가 존재해야함
만약, 암시적 타입 변환 메소드를 Rational 클래스 내부에 정의하였다면, 메소드를 호출한 스코프에는 존재하지 않으므로 변환이 이뤄지지 않음