함수
규칙 : fun + 함수명 +(+ 파라미터명 + 파라미터 변수형+)+ : + 반환 변수형 = 비즈니스 로직
이때 파라미터 변수형과 반환 변수형이 같은 경우 생략이 가능함.
자바의 void 함수를 표현할 경우 코틀린에선 unit 으로 선언함.
fun main() {
println("Hello World!")
println(sum(1, 5))
println(sum1(1, 5))
sum2(1, 5)
}
//반환값을 정의한 함수
fun sum(a: Int, b: Int):Int = a+b
//파라미터와 반환값이 같은 경우 반환형 생략 가능
fun sum1(a: Int, b: Int) = a+b
//반환값이 없는 void 함수인 경우 unit으로 선언할 수 있음
fun sum2(a: Int, b: Int):Unit = println(a+b)
변수
val : 변경 불가능한 변수
var : 변경 가능한 변수
- 함수 와 마찬가지로 클래스 외부에 선언 가능
- 타입 추론 가능
- 변수명 먼저 선언 후 타입 선언
$변수명 사용하면 해당 변수를 문자열로 바로 삽입 가능
${수식(코틀린)} 사용하면 수식도 문자열로 바로 삽입 가능
fun main() {
val i: Int = 123
val j = 321
var age: Int = 20
age = 23
val number = 20
println("Please give me $number dollors")
println("I only have ${if (number <10 ) number else 10} dollor")
}
클래스
코틀린을 쓸 경우 자바에서 선언한 롬복을 자동으로 선언해줌
자바 클래스 : 필드를 갖는 생성자 선언, field+getter+setter 프로퍼티 생성 필요
@Getter
@Setter
@AllArgsConstructor
public class Person {
private String name;
private int age;
private boolean isMarred;
}
코틀린 클래스 : 필드를 모두 갖는 생성자 자동 선언, field+getter+setter 프로퍼티 자동 생성, 커스텀이 필요한 경우에만 getter, setter 선언
class Person(val name: String, val age: Int, val isMarried: Boolean)
Blooean 프로퍼티 getter, setter 선언 방식
Java : getter(isMarried()), setter(setMarried())
Kotlin : getter/setter(isMarried)
코틀린으로 생성한 클래스는 java로 디컴파일 한 경우 아래처럼 소스가 생성됨
public final class Person {
@NotNull
private final String name;
private final int age;
private final boolean isMarried;
public Person(@NotNull String name, int age, boolean isMarried) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
this.isMarried = isMarried;
}
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final boolean isMarried() {
return this.isMarried;
}
}
또한 코틀린에서는 getter를 아래처럼 커스텀해서 쓸 수 있음
class Rectangle (val height: Int, val width: Int) {
val isSquare:Boolean
get(){
return height == width
}
//자바에서는 별도 구분 함수를 통해 값을 검증하는 방식을 권장하지만
//코틀린에서는 프로퍼티 관련 검증 함수가 자동으로 생성됨으로 위 코드처럼 변수를 선언한 뒤 속성의 프로퍼티 사용하는 걸 권장함
fun isSquareV2() = height == width
}
root인 최상위 함수는 아래처럼 함수명으로 import 해서 사용할 수 있음
fun createRandomRectangle(): Rectangle {
val random = Random()
return Rectangle(random.nextInt(), random.nextInt())
}
fun main() {
println(createRandomRectangle().isSquare)
}
[디컴파일]
public final class MainKt {
@NotNull
public static final Rectangle createRandomRectangle() {
Random random = new Random();
return new Rectangle(random.nextInt(), random.nextInt());
}
public static final void main() {
Person person = new Person("James", 32, true);
String var1 = person.getName();
System.out.println(var1);
int var3 = person.getAge();
System.out.println(var3);
boolean var4 = person.isMarried();
System.out.println(var4);
Rectangle rectangle = new Rectangle(4, 4);
boolean var2 = rectangle.isSquare();
System.out.println(var2);
var2 = createRandomRectangle().isSquare();
System.out.println(var2);
}
// $FF: synthetic method
public static void main(String[] args) {
main();
}
}
디컴파일 할 경우 최상위 함수로 선언한 createRectangle 함수가 별도의 클래스의 public static 함수로 선언된 걸 확인할 수 있음
=> 자바랑 다르게 코틀린은 디렉토리 구조와 패키지 경로가 다를 수 있고 하나의 파일에 여러 클래스와 함수, 최상위 변수 모두 선언이 가능하다.
하나의 파일에 여러 클래스와 함수, 최상위 변수를 선언해서 사용할 경우 장점
- 코틀린에서 편리해서 자주 활용되는 부분임
- 결합도가 높은 클래스, 유사한 클래스, enum 클래스 등 한곳에 모아 놓으면 관리와 사용이 편리해짐
When 과 Enum
코틀린에서 When 을 사용한 경우 반드시 enum을 적용해야 되는데 이는 아래처럼 enum이 추가될 경우 when 절에서 해당 enum이 없음을 컴파일 시점에 알 수 있어 매우 유용함
enum class Color(val r: Int, val g: Int, val b: Int){
RED(255, 0, 0),
ORANGE(255, 166, 0),
YELLOW(255, 255, 0),
GREEN(0, 255, 0);// 아래 함수와 구분을 위해서 세미콜론 추가
fun rgb() = (r * 256 + g) *256 + b
}
fun getKoreanColor(color: Color): String =
when(color) {
RED -> "빨강"
ORANGE -> "오렌지"
YELLOW -> "노랑"
GREEN -> "녹색"
}
fun main(){
println(getKoreanColor(RED))
}
인자가 없는 When
fun mix(c1: Color, c2: Color) =
//인자가 없는 when의 경우 반드시 else을 정의해야 됨
when {
c1 == RED && c2 == YELLOW -> ORANGE
c1 == YELLOW && c2 == BLUE -> GREEN
else -> throw RuntimeException()
}
When으로 스마트캐스트(타입 검증 및 타입 캐스트 자동으로 가능)
- if 문에서도 가능하고 직접 형변환도 가능함 (as 명령어를 통해)
fun printObject(obj: Any): Unit = when(obj){
is String -> println(obj.lowercase())
is Duration -> println(obj.nano)
is LocalDateTime -> println(obj.month)
else -> println("Unknown type")
}
fun printObjectIf(obj: Any): Unit = if (obj is String) {
println(obj.lowercase())
} else {
println("nothing")
}
fun main(){
printObjectIf("OBNJECT")
printObject(Duration.ofNanos(3456))
printObject(LocalDateTime.now())
}
반복문
while 문 Java와 똑같고 for 반복문은 조금 다름
- 기본적으로 i in 1..100 이면 양쪽 끝 부분 포함, until은 마지막 값 미포함
fun evenOrOdd(n: Int):String = when {
n % 2 == 0 -> "even"
else -> "odd"
}
fun main(){
for (i in 1.. 10){
println(evenOrOdd(i))
}
// 마지막 10 미포함 -> until
for (i in 1 until 10){
println(evenOrOdd(i))
}
}
map과 이터레이션
mutableMapOf로 map 간단히 선언 가능
- listOf, mapOf, setOf도 있음
c in Object -> for-each랑 동일함
(key, value) in mapObject -> map과 같은 객체의 key, value를 자동으로 분해해서 값을 세팅하여 가져다 쓸 수 있음
fun main(){
// 값 변경이 가능한 map 객체
val students = mutableMapOf<Int, String>()
students[1] = "Jack"
students[2] = "Diana"
students[3] = "Frost"
for (student in students) {
println("번호:${student.key}, 이름:${student.value}")
}
//unmutable 변경 불가능한 map 객체 생성 (실무에서 다 많이 씀)
val st = mapOf(
1 to "Jock",
2 to "Diana",
3 to "Frost"
)
for ((key, value) in st){
println("번호:${key}, 이름:${value}")
}
}
in 키워드는 범위 검사할때도 사용할 수 있음
fun recognize(c: Char): String= when(c) {
in '0'..'9' -> "숫자"
in 'a'..'z', in 'A'..'Z' -> "알파벳"
else -> "숫자도 알파벳도 아님"
}
fun main(){
println("b는 ${recognize('b')}")
println("3는 ${recognize('3')}")
println("{는 ${recognize('}')}")
}
예외처리
try catch finally 구문도 if, when 처럼 값으로 취급할 수 있음
자바랑 다른 부분은 throw 할때 new로 Exception 클래스를 선언할 필요가 없고 checked Excpetion을 호출 함수에도 throw Exception을 별도로 표기할 필요가 없음
fun parse(numberStr: String): Int = try {
Integer.parseInt(numberStr)
throw IOException("일부로 발생시키는 checked exception") // java랑 다르게 new 없음
} catch (e: IOException) {
throw RuntimeException("UnChecked exception으로 변환") // 롤백될 수 있게 예외 변환
} finally {
println("무조건 실행되는 코드블록")
}
fun main(){
println(parse("1"))
}
Kotlin + Spring에서 @Transactional 을 활용할 때 주의점
CheckedException은 롤백되지 않음!!
- 자바에서는 checkedException은 컴파일 에러로 잡아주기 때문에 그전에 개발자가 직접 필요에 따라 RuntimeException으로 전환해서 롤백 처리를 하거나 안하거나 선택할 수 있음
- 코틀린에서는 별도로 checkedException을 컴파일에러로 잡지 않기 때문에 예외가 발생해도 롤백이 되지 않을 수 있음
그러므로 예외가 발생한 포인트에서는 반드시 try catch를 걸어주는게 좋음
코틀린 함수 활용
함수를 통해 이름으로 인자를 정의할 수 있으며 디폴트 파라미터 값 선언을 통해 인자 갯수에 따른 별도 메소드를 생성하는 오버로딩 작업을 하지 않아도 됨
fun getMyname(
firstName: String = "gildong",
lastName: String = "hong"
): String = "$firstName $lastName"
fun main(){
//파라미터 세팅 혼돈이 발생할 수 있음
println(getMyname("jinyoon", "yang"))
//파라미터 지정이 가능함
println(getMyname(firstName = "jinyoon", lastName = "yang"))
//디폴트 인자 세팅이 가능함 (장점 : 인자 값에 따른 오버로딩을 할 필요가 없음)
println(getMyname())
//특정 인자에만 값 세팅이 가능함
println(getMyname(lastName = "kim"))
}
최상위 함수와 프로퍼티 선언이 가능함 (장점 : 자바처럼 별도의 유틸 클래스를 생성해야 되는 불편함 해소)
//최상위 프로퍼티 선언 가능 (상수)
const val ONE = 1
const val TWO = 2
//최상위 함수 선언 가능 (유틸 클래스랑 유사)
fun sum(
num1: Int,
num2: Int
): Int = num1 + num2
-> 자바 코드로 작성할 경우
public final class NumberUtils {
public static final Integer ONE = 1;
public static final Integer TWO = 2;
private NumberUtils() {
}
public static Integer sum(Integer num1, Integer num2) {
return num1 + num2;
}
}
확장함수 & 확장 프로퍼티
//확장함수
fun String.double():String = this + this
//확장프로퍼티
val String.lastChar:Char
get() = get(this.length - 1)
//중위함수
infix fun String.add(postfix :String) = this + postfix
fun main() {
println("do".double())
println("dodo".lastChar)
println("do" add "do")
println(setOf(1, 4, 3, 6, 10).maxOrNull())
//구조분해 (자동으로 key, value의 값을 정의해서 호출할 수 있음)
mapOf(
"one" to 1,
"tow" to 2
).forEach {
(key, value) -> println("$key : $value")
}
}
확장함수로 자바와 다르게 상속을 받아 커스텀 함수를 정의할 필요가 없음 또한 컬렉션의 추가 메소드들은 모두 확장함수 형태로 선언되어 있어 호출해서 쓰기 편함
- 코틀린 확장 함수는 내부적으로 정적 메소로 생성됨으로 오버라이드 할 수 없음.
중위 호출함수는 확장함수이면서 파라미터가 한개인 함수에서만 활용이 가능하며 mapof 에 많이 활용됨. 확장함수 형태지만 .으로 호출할 필요없이 띄여쓰기로 함수를 호출할 수 있어 가독성이 향상됨
참고 : 코틀린 가변 인자 함수는 vararg로 선언하면 됨
인터페이스
- 자바와 동일함 (인터페이스를 만들고 안에 메소드를 정의함)
- 자바에서는 인터페이스를 구현할때 implements나 extends로 정의하지만 코틀린에서는 : 으로 정의하면 됨
- 인터페이스의 메서드 구현을 할 경우 반드시 override를 선언해야 됨.
interface Clickable {
fun click(): Unit
}
interface Focused {
fun focus(): Unit
// 기본 메소드 (구현체 정의되어 있음)
fun showOff(): Unit=println("showOff")
}
// 다중 상속
class Button : Clickable, Focused{
override fun click() {
println("clicked!!")
}
override fun focus() {
println("focused!!")
}
}
fun main() {
Button().click()
Button().focus()
Button().showOff()
}
상속
코틀린은 기본적으로 상속이 불가능한 final 로 생성됨 그러므로 반드시 open으로 선언해줘야 상속이 가능함
(이유 : 무분별한 상속을 방지하기 위함, 여러 개발자의 손을 타면서 원 작성자의 의도를 파악해서 잘 상속해서 클래스를 구현할 수 있는지 .. 이 문제를 막기 위함 - 취약한 기반 클래스 문제)
// 오픈 클래스 : 상속 가능
open class RichButton : Clickable {
// final method : 오버라이드 불가능
fun disable() {}
// open method : 오버라이드 가능
open fun animate() {}
// 오버라이드 메소드 : 오버라이드 가능
override fun click() {}
}
베이스 클래스 상속하는 방법
[버전 1]
open class Parent(val firstName:String)
class Chlid(val subName:String, firstName: String): Parent(firstName = firstName)
[버전 2]
//자바랑 비슷한 형태
class Child : Parent{
private val subName:String
constructor(subName:String, firstName: String):super(firstName){
this.subName = subName
}
}
부생성자 선언 방법
- 클래스 구현부에 construct를 통해 별도의 생성자를 추가로 선언할 수 있고 this - 다른 생성자, super - 상속 클래스 생성자를 호출함
class Child : Parent{
private val subName:String
constructor(subName: String):this(subName, "")
constructor(subName:String, firstName: String):super(firstName){
this.subName = subName
}
}
[간소화 버전]
class Child(val subName:String, firstName: String = "") : Parent(firstName)
backing Field - getter, setter 커스텀 할때 해당 필드를 의미하는 키워드
class Account{
var balance:Int = 0
set(value) {
if (value < 0) throw IllegalArgumentException("value is wrong")
else field = value
}
var accountName:String = ""
get() = "계좌이름 : "+field
}
fun main() {
val account = Account()
account.balance = 100
account.accountName = "급여계좌"
println("${account.accountName} , 잔액 : ${account.balance}")
}
public, private, internal, protected : 접근자
-자바랑 코틀린 차이점
자바는 기본적으로 package-private 인데 코틀린에선 사용하지 않음. 코틀린은 기본적으로 public임
internal : 모듈 안에서만 볼 수 있은 가시성 (멀티 모듈인 경우 제한을 위해 새로 생김)
private : 클래스 내에서 선언하면 자바처럼 해당 클래스에서만 사용할 수 있고 파일내에 선언하면 해당 파일 안에서만 사용할 수 있음
코틀린 클래스 생성 방식 변화
버전 1, 2를 거치면서 마지막 클래스 생성 방식으로 많이 간소화되었음
// version 1
/*
class User constructor(_username:String){
val username:String
init {
username = _username
}
}
*/
//version2
/*
class User (_username:String){
val userName:String = _username
}
*/
class User(val userName:String)
class User2(val userName:String, val level:Int=1)
fun main() {
println(User(userName = "Jenny").userName)
println(User2(userName = "Jenny").level)
println(User2(userName = "Jenny", level = 3).level)
}
sealed class : 봉인된 클래스
- 확장을 제한하여 편의성을 향상시키는 클래스 타입
sealed 클래스 내부에 해당 클래스의 구현체를 모두 나열한 뒤 when 절에서 해당 클래스를 처리할 경우 else 구문을 정의할 필요가 없으며 sealed 클래스에 구현체가 신규로 추가될 경우 컴파일 에러로 수정 위치를 찾기 수월해짐
[인터페이스 버전]
interface Error
class FileError(val filename:String) : Error
class DatabaseError(val database:Database) : Error
enum class Database{
ORACLE, MYSQL, MARIADB;
}
fun getMassage(error: Error)=when(error){
is FileError -> "error is fileError ${error.filename}"
is DatabaseError -> "error is databaseError ${error.database.name}"
else -> throw IllegalArgumentException("error is unknown")
}
fun main() {
println(getMassage(DatabaseError(Database.MYSQL)))
}
[Sealded 클래스 버전]
sealed class Error2{
class FileError(val filename:String) : Error2()
class DatabaseError(val database:Database) : Error2()
class RedisError(val host:String): Error2()
}
enum class Database2{
ORACLE, MYSQL, MARIADB;
}
fun getMassage2(error: Error2) =when(error){
is Error2.DatabaseError -> "error is databaseError ${error.database.name}"
is Error2.FileError -> "error is fileError ${error.filename}"
is Error2.RedisError -> "error is redisError ${error.host}"
}
fun main() {
println(getMassage2(Error2.RedisError("127.0.0.1")))
}
Sealed Class의 장점 : Error 클래스에 새로 구현체가 추가될 경우 When 절 코드에서 컴파일 에러로 잡히기 때문에 추가해야 되는 위치를 한번에 찾아 처리하기 편함
Private 처리를 통해 set, get 함수의 접근자를 변경할 수 있음.
class Account{
var balance:Int = 0
private set(value) {
if (value < 0) throw IllegalArgumentException("value is wrong")
else field = value
}
fun increaseBalance(amount : Int) {
this.balance += amount
}
fun decreaseBalance(amount: Int) {
this.balance -= amount
}
var accountName:String = ""
get() = "계좌이름 : "+field
}
fun main() {
val account = Account()
// account.balance = 100
account.increaseBalance(100)
account.accountName = "급여계좌"
println("${account.accountName} , 잔액 : ${account.balance}")
}
Data Class
데이터를 저장하는 클래스 타입을 처리할때 사용하며 Lombok 으로 @Data 선언하는거랑 유사함
- 데이터 처리에 필요한 기본 기능을 자동으로 제공해줌 ( + toString, equalsOf, hashCode)
클래스 앞에 Data 키워드만 추가하면 됨
data class User(val name: String, val age: Int)
Object Class
자바에서의 싱글톤 패턴 적용을 원하는 경우 Object 키워드를 추가해주면 됨
- 싱글톤 또는 Util 클래스 정의할때 Object 클래스로 생성하면 더 편리함
(익명 클래스에서도 Object 클래스 선언 가능함)
object NumberUtil {
fun sum(a:Int, b:Int) = a+b
fun multiply(a:Int, b:Int) = a*b
}
fun main() {
println(NumberUtil.multiply(10 ,2))
}
단, Util 클래스에서 Statict 메소드만 존재하고 별도 인스턴스 생성이 필요없는 경우에도 사용할 수 있지만 파일에 최상위 함수로 선언해서 사용하는 것이 더 좋음
Compainon Object : 자바의 static method를 대체함
- static으로 상수값을 저장하거나 팩토리 생성자를 만들때 사용하면 됨.
data class Child (val name:String, val age:Int){
companion object {
const val MAX_CHILDREN = 4
fun ofDefaultAge(subName:String):Child{
return Child(subName, 0)
}
}
}
fun main() {
println(Child.ofDefaultAge("Jinyoon"))
}
강좌 링크 :
'IT > 자바' 카테고리의 다른 글
4장 카프카 상세 개념 설명(2) (0) | 2024.02.15 |
---|---|
4장 카프카 상세 개념 설명 (0) | 2024.02.04 |
3장 카프카 기본 개념 설명 (0) | 2024.01.27 |
Test-DrivenDevelopment : 테스트 주도 개발 (0) | 2023.03.12 |
자바 플레이그라운드 with TDD, 클린코드 강좌 후기 Day 3 (0) | 2022.12.18 |