신규 블로그를 만들었습니다!

2020년 이후부터는 아래 블로그에서 활동합니다.

댓글로 질문 주셔도 확인하기 어려울 수 있습니다.

>> https://bluemiv.tistory.com/

5. 기본 타입과 연산

살펴볼 내용

  • String, Int, Long, Short, Byte, Float, Double, Char, Boolean
  • 연산자 우선순위
  • 암시적 변환

5.1 기본타입

스칼라 타입

  • java.lang.String
  • scala.Int, scala.Long, scala.Short ...
// String만 java.lang 패키지에 있다.
val str: java.lang.String = "Hello, Scala!"
val int: scala.Int = 3

 

만약 scala 패키지에서 String 객체를 찾게된다면

// scala 패키지에는 String이 없다.
val str: scala.String = "Hello, Scala!"

아래와 같은 에러 구문을 볼 수 있다.

type String is not a member of package scala
    val str: scala.String = "Hello, Scala!"

 

java.lang과 scala 패키지는 자동 임포트를 하기 때문에 언제 어디서나 간단한 이름으로(Boolean, Char, Int, String 등) 사용하면 된다.

val str: String = "Hello, Scala!"
val int: Int = 3

자바 타입의 범위와 스칼라 타입의 범위가 동일하기 때문에, 스칼라 컴파일러는 IntDouble같은 인스턴스를 바이트 코드로 컴파일 할때 자유롭게 자바의 원시 타입으로 변환할 수 있다.

type java scala
Byte(byte) 1byte(8bits) 1byte(8bits)
Short(short) 2bytes(16bits) 2bytes(16bits)
Int(int) 4bytes(32bits) 4bytes(32bits)
Long(long) 8bytes(64bits) 8bytes(64bits)


5.2. 리터럴

리터럴은 상수 값을 코드에 적는 방법을 말한다.

5.2.1. 정수 리터럴

정수 리터럴: Int, Long, Short, Byte

  • Decimal (10진수)
  • Hexadecimal (16진수)

시작 부분에 따라 진법이 달라진다.

  • 0x 또는 0X 로 시작하면 16진수를 뜻한다.
  • 0x와 같이 추가 장식이 없다면 10진수이다.
val hex = 0x13
println(s"16 * 1 + 1 * 3 = $hex")

val hex2 = 0xAB
println(s"16 * 10 + 1 * 11 = $hex2")
16 * 1 + 1 * 3 = 19
16 * 10 + 1 * 11 = 171

정수 리터럴 끝에 L 또는 l을 붙이면 Long 리터럴이 된다. 아무것도 없다면 Int 리터럴이다.

val myInt = 12345
val myLong = 12345L

println(s"myInt type: ${myInt.getClass.getTypeName}")
println(s"myLong type: ${myLong.getClass.getTypeName}") 
myInt type: int
myLong type: long

Short 또는 Byte 변수에 Int 리터럴을 할당하면 스칼라는 리터럴의 값을 해당 타입(Short 또는 Byte)로 취급한다.

val myByte: Byte = 123
val myShort: Short = 123

println(s"myByte type: ${myByte.getClass.getTypeName}")
println(s"myShort type: ${myShort.getClass.getTypeName}")
myByte type: byte
myShort type: short

5.2.2. 부동소수점 리터럴

  • Float: 뒤에 F 또는 f를 붙인다.
  • Double: 아무것도 붙이지 않거나 명시적으로 D 또는 d를 붙일 수도 있다.
val myFloat = 1.2345678901234567890F
val myDouble = 1.2345678901234567890

println(s"myFloat: $myFloat (Type: ${myFloat.getClass.getTypeName})")
println(s"myDouble: $myDouble (Type: ${myDouble.getClass.getTypeName})")
myFloat: 1.2345679 (Type: float)
myDouble: 1.2345678901234567 (Type: double)

e 또는 E는 10의 지수부를 뜻한다.

val myDouble = 1.234e2
println(s"1.234 * 10^2 = $myDouble")
1.234 * 10^2 = 123.4

5.2.3. 문자 리터럴

유니코드를 이용하여 문자 리터럴을 지정할 수도 있다.

val myChar = 'A'
println(myChar)

val myUnicode = '\u0041'
println(myUnicode)
A
A

스칼라 쉘에서는 유니코드를 이용하여 식별자 이름을 지정할 수 있다.

scala〉 val B\u0041\u0044 = 1
BAD: Int = 1

이스케이프 문자

5.2.4. 문자열 리터럴

문자열 리터럴은 큰따옴표(")로 둘러싼 문자들로 이뤄진다.

val hello = "Hello, Scala!"
val escape = "\\\"\'"

이스케이프 시퀀스가 많거나 여러 줄인 경우에는 문자열로 표시할때 가독성이 떨어진다.

큰따옴표 연속 3개(""")를 이용해서 raw한 문자열을 표현할 수 있다.

println("""Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type
and scrambled it to make a type specimen book.""")

또는

println(
    """|Lorem Ipsum is simply dummy text of the printing and typesetting industry.
        |Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
        |when an unknown printer took a galley of type
        |and scrambled it to make a type specimen book.
    """.stripMargin)

5.2.5. 심볼 리터럴

심볼(Symbol)은 싱글 따옴표 + 문자열로 표현한다.

val mySymbol = 'ident
println(mySymbol)
println(s"Type: ${mySymbol.getClass.getTypeName}")
println(s"name: ${mySymbol.name}")
println(s"toString: ${mySymbol.toString()}"
'ident
Type: scala.Symbol
name: ident
toString: 'ident

스칼라 컴파일러는 'identSymbol("ident") 이라는 팩토리 메소드로 호출한다.

val mySymbol = 'ident
val mySymbol = Symbol("ident")
def updateRecordByName(r: Symbol, value: Any) = {
    // 코드
}

동적으로 필드의 이름을 지정하는 상황에서 심볼을 사용하면 편리하다.

Symbol을 사용하지 않는 경우

scala> updateRecordByName(favoriteAlbum, "OK Computer")
<console>:6: error: not found: value favoriteAlbum

Symbol 사용하는 경우

Symbol을 사용하면 오류 없이 넘길 수 있다.

scala> updateRecordByName('favoriteAlbum, "OK Computer")

같은 Symbol 리터럴은 2번 이상 사용하면 사용된 Symbol은 서로 완전히 동일한 Symbol 객체를 참조한다. (인턴 intern 한다)

왜 심볼을 사용하는것인가..???

  • lisp부터 내려오는 함수언어 전통 때문
  • 성능 (Symbol 비교가 String 비교보다 조금 더 빠르다)
  • 객체 크기 (Symbol이 String보다 사용하는 메모리 비용이 싸다)
  • 문자열과 심볼 리터럴이 다르니까 소스코드에서 구분이 쉽다. (의미상 소스코드에서만 구분이 필요하고 실제 문자열 자체가 프로그램 내에서 그리 중요하지는 않은 경우)

예를들어,

// String 사용
RunServer( Map("port" -> 8080, "ssh" -> true ) )

보다는

// Symbol 사용
RunServer( Map('port -> 8080, 'ssh -> true ) )

Symbol을 사용하는 것이 더 경제적이다.

5.2.6. 불리언 리터럴

Boolean 타입의 리터럴에는 true와 false가 있다.

val bool = true
val fool = false

5.3. 문자열 인터폴레이션

문자열 인터폴레이션(interpolation)은 간결하고 읽기 쉬운 코드를 작성할 수 있게 해준다.

s 인터폴레이터와 $ 달러 기호를 이용하여 사용한다. 예를들어, s"$이름" 또는 s"${이름}"과 같이 사용

예제1

val name = "Taehong"
println(s"Hello, $name!")
Hello, Taehong!

예제2

println(s"The answer is ${6 * 7}")
The answer is 42

동작 순서

  1. s 인터폴레이터는 내장된 표현식(${} 내부 식)을 평가한다.
  2. 결과에 toString()을 호출하여 ${} 내장된 표현식을 toString의 결과로 대치해준다.
class Test {

  override def toString: String = {
    println("실행")
    this.getClass.getName
  }
}
val test: Test = new Test()
println(s"Class name is $test")
실행
Class name is Test

raw와 f

s 인터폴레이터 말고도 rawf도 있다.

raw를 사용하면 이스케이프 시퀀스를 인식하지 못한다. (그대로 출력)

println(raw"Hello\nWorld!")
Hello\nWorld!

fprintf 스타일의 형식 지정을 사용할 수 있게 한다.

println(f"pi: ${1.23456789}%.5f")
pi: 1.23457

정리하면, 문자열 인터폴레이션은 컴파일 시점에 코드를 재작성하는 형태로 구현되어 있다.

5.4. 연산자는 메소드다

스칼라에서는 연산자도 메소드다. (3장에서 살펴봤듯이 매개변수가 1개인경우 점(.)과 괄호()를 생략할 수 있다)

1 + 2
1.+(2)

+() 함수를 호출할 수 있는 것은 Int 클래스에 + 메소드가 정의가 되어 있기 때문이다.

그리고 오버로드(overload)를 통해 파라미터 타입을 다르게 한 메소드가 존재한다.

val num: Int = 3
val longNum: Long = 500000000L
println(num + longNum)
println(num.+(longNum))

실제 Int 클래스 내부 구현

이렇게 점과 괄호를 생략하여 사용하는 방법은 + 연산자만 가능한것이 아니다. 예를들어 String의 indexOf 메소드를 보면,

val myStr = "Hello, Scala!"
println(myStr.indexOf('H'))
println(myStr indexOf 'H')
0
0

만약 매개변수가 여러개인 경우, 매개변수 부분에 괄호를 묶어주면 이와 같이 사용이 가능하다.

val myStr = "Hello, Scala!"
println(myStr.indexOf("Scala", 3))
println(myStr indexOf ("Scala", 3))
7
7

프로그래머가 메소드를 사용하는 방법에 따라 달라질 뿐, 모든 메소드는 연산자가 될 수 있다!

위와 같은 방식으로 사용하는 표기법을 중위 infix 연산자 표기법 이라고 한다. 스칼라에는 중위 연산자 말고도 전위 prefix, 후위 postfix 연산자도 있다.

  • prefix: -7 에서 - 부분
  • infix: 7 + 2 에서 + 부분
  • postfix: 7 toLong 에서 toLong 부분

여기서 전위와 후위는 중위와 다르게 피연산자가 하나이고, 이는 단항 unary 연산자를 뜻한다.

사실 전위와 후위도 실제로는 메소드 호출을 간략하게 표현한 것이다.

println(-7)
println(7.unary_-)

실제 Int 클래스 내부 구현

주의 할 점은 전위 연산자로 사용할 수 있는 식별자는 +, -, !, ~ 4가지 뿐이다. unary_* 를 정의한다고 해도 *를 전위 연산자로 사용할 수 없다.

만약 *p 와 같이 사용하는 코드가 있다면 그건 전위연산자가 아닌 *.p와 같다.

*의 경우 컴파일 에러 발생

기본 타입에 대한 여러가지 연산자가 이미 구현이 되어 있기 때문에 스칼라 API 문서를 살펴볼 필요가 있다

5.5. 산술 연산

정수 연산시 +, -, /, %, * 등이 있다. 피연산자가 정수일때, / 연산시 소수 이하부분을 제외한 정수 부분만 돌려준다.

scala〉 11 / 4
reslO: Int = 2
scala> 11.Of / 4.Of
resl2: Float = 2.75

스칼라에서는 나머지 연산 %로 구하는 부동소수점 나머지는 IEEE754 표준과 다르다.

IEEE 754는 전기 전자 기술자 협회(IEEE)에서 개발한 컴퓨터에서 부동소수점을 표현하는 가장 널리 쓰이는 표준

만약 IEEE754 표준 나머지 연산이 필요하다면 scala.math.IEEEremainder()를 사용해야 한다.

println(11.0 % 4.0)
println(math.IEEEremainder(11.0, 4.0))
3
-1.0

5.6. 관계 연산과 논리 연산

수 타입을 크다(〉),작다(<),크거나 같다(>=),작거나 같다(이라는 관계 연산자를 사용 해 비교 가능하다.

! 연산자 (unary_! 메소드)를 사용해 Boolean 값을 반전시킬 수 있다.

println(!true) // false
println(true.unary_!) // false

||, && 계산은 쇼트 서킷(short circuit) 연산으로 이루어진다. (자바와 동일)

def main(args: Array[String]): Unit = {
  left() && right()
}

def left() = {
  println("left")
  false
}

def right() = {
  println("right")
  true
}
left

이미 앞에서 false라는 결과가 도출되었기 때문에 뒤에 right() 메소드는 호출되지 않는다.

스칼라에는 메소드의 계산을 미루는 기능이 있다. 이런 기능을 이름에 의한 호출(call by name) 이라고 한다.

call by name 에 대한 간략한 예제

def main(args: Array[String]): Unit = {
  callByValue(something())
  callByName(something())
}

def something() = {
  println("something")
  1
}

def callByValue(num: Int) = {
  println("Call By Value")
  println(num)
  println(num)
}

def callByName(num: => Int) = {
  println("Call By Name")
  println(num)
  println(num)
}
something
Call By Value
1
1
Call By Name
something
1
something
1

즉, call by name은 메소드에서 해당 인자를 사용할 때 연산한다.

5.7. 비트 연산

scala> 1 & 2
res1: Int = 0

scala> 1 | 3
res2: Int = 3

scala> 1 ^ 3
res3: Int = 2

scala> ~1
res4: Int = -2

scala> -1 >> 31
res5: Int = -1

scala> -1 >>> 31 // 부호없는 쉬프트
res6: Int = 1

scala> 1 << 2
res7: Int = 4

5.8. 객체 동일성

객체 비교

scala> 1 == 2
res0: Boolean = false

scala> 1 != 2
res1: Boolean = true

scala> 2 == 2
res2: Boolean = true

scala> List(1,2,3) == List(1,2,3)
res3: Boolean = true

scala> List(1,2,3) == List(5,6,7)
res4: Boolean = false

다른 타입 비교

scala> 1 == 1.0
res5: Boolean = true

scala> List(1,2,3) == "Hello, Scala!"
res6: Boolean = false

null 과 비교

scala> List(1,2,3) == null
res7: Boolean = false

scala> null == List(1,2,3)
res8: Boolean = false

스칼라에서는 좌항이 null이 아닌경우 equals 메소드를 호출한다.

스칼라의 == 와 자바의 ==

  • 자바에서는 ==를 사용해 객체는 참조값을 비교, primitive는 값을 비교한다. (즉, 자바는 객체일때는 JVM의 힙에서 같은 객체를 가리키는지 비교를 한다)
  • 스칼라는 ==로 값을 비교. 만약 스칼라에서 참조값을 비교하고 싶다면 eq 또는 ne를 사용한다.

5.9. 연산자 우선순위와 결합 법칙

스칼라에서는 메소드의 첫 글자를 보고 우선순위를 정한다. (스칼라는 연산자라는 개념이 없기 때문에)

  • 메소드 이름이 *로시작한다면,이 메소드는 +로 시작하는 이름의 메소드보다 우선순위가 더 높다.
2 + 2 * 7 ====> 2 + ( 2 * 7 )

다른 예로, 임의의 메소드 +++(), ***() 생성한 뒤, 아래식을 수행하면

a +++ b *** c ===> a +++ (b *** c)

우선순위 (표의 맨위가 제일 높다)

할당 연산자

할당연산자로 끝나는 메소드는 (비교연산자(==, !=, >=, <=)를 제외한) 모든 연산자 보다 우선순위가 낮다.

x *= y + 1

*= 메소드의 이름은 *로 시작하지만 할당 연산자에 의해 우선순위가 가장 낮다. 즉, 아래 식과 같다.

// (x *= y) + 1 // 틀림
x *= (y + 1) // 맞음

: 으로 끝나는 메소드

메소드 이름이 : 으로 끝나면 오른쪽부터 왼쪽으로 짝을 지어 나간다.

a ::: b ::: c 는 a ::: (b ::: c)
a * b * c 는 (a * b) * c

결국은 애매하면... 괄호를 사용하면 됨 (명확하게 표시하기에도 좋다)

5.10. 풍부한 래퍼

래퍼 연산
래퍼 클래스

이러한 기능은 암시적 변환 implicit conversion 이라는 기법에 의해 가능한 것이다. (21장 참고)

Reference

Programming in Scala 3E

'Language > Scala' 카테고리의 다른 글

스칼라(Scala) 단언문과 테스트  (0) 2019.07.24
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기