신규 블로그를 만들었습니다!
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
자바 타입의 범위와 스칼라 타입의 범위가 동일하기 때문에, 스칼라 컴파일러는 Int
나 Double
같은 인스턴스를 바이트 코드로 컴파일 할때 자유롭게 자바의 원시 타입으로 변환할 수 있다.
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
스칼라 컴파일러는 'ident
를 Symbol("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
동작 순서
- s 인터폴레이터는 내장된 표현식(${} 내부 식)을 평가한다.
- 결과에 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
인터폴레이터 말고도 raw
와 f
도 있다.
raw
를 사용하면 이스케이프 시퀀스를 인식하지 못한다. (그대로 출력)
println(raw"Hello\nWorld!")
Hello\nWorld!
f
는 printf
스타일의 형식 지정을 사용할 수 있게 한다.
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))
이렇게 점과 괄호를 생략하여 사용하는 방법은 + 연산자만 가능한것이 아니다. 예를들어 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_-)
주의 할 점은 전위 연산자로 사용할 수 있는 식별자는 +, -, !, ~ 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 |
---|
최근댓글