코틀린-안드로이드

16일차)알고리즘 문제(최댓값과 최솟값), 코드카타 리뷰, 코틀린 문법 강의 5주차(자료형 변환, 여러 인스턴스 리턴, scope function, 확장함수, 비동기 프로그래밍, 쓰레드와 코루틴)

songyooho 2024. 6. 7. 20:16

>알고리즘 문제

 

1. 문제

문자열 s에는 공백으로 구분된 숫자들이 저장되어 있습니다. str에 나타나는 숫자 중 최소값과 최대값을 찾아 이를 "(최소값) (최대값)"형태의 문자열을 반환하는 함수, solution을 완성하세요.
예를들어 s가 "1 2 3 4"라면 "1 4"를 리턴하고, "-1 -2 -3 -4"라면 "-4 -1"을 리턴하면 됩니다.

제한 조건

  • s에는 둘 이상의 정수가 공백으로 구분되어 있습니다.

2. 솔루션

class Solution {
    fun solution(s: String): String {
        var list=s.split(" ")
        var arr=arrayOf<Int>()
        list.forEach{arr+=it.toInt()}
        return "${arr.minOrNull()} ${arr.maxOrNull()}"
    }
}

-maxOrNUll, minOrNull : min,max가 deprecated되었기에 이걸사용. 최대 최소나 null을 반환해줌

@sorted: 리스트에 정렬 사용시 이걸 사용

 

 

>코드카타 리뷰

1. iterator

-사용법: iterator를 선언후 while문에서 hasNext()로 다음 원소가 있는 한 순회를 하며 next()를 이요

val iterator = 컬렉션.iterator()

while(iterator.hasNext()){
	val item=iterator.next()
    .
    .
}

2. map

-원본과 원소개수는 같지만 람다식에 의해 원소가 변형된 새로운 컬렉션을 만드는 함수

- 원본컬렉션.map{ it으로 구성된 함수 }

 

 

>코틀린 문법 강의 5주차

 

1. 자료형 변환

1)일반 자료형간의 변환: to자료형()

2)객체 자료형간의 변환

<1>상속 관계 자료형간만이 가능

<2>종류

-업캐스팅: 자식클래스를 부모클래스로 변환

=>예시

fun main() {
    var birds = mutableListOf<Bird>()
    birds.add(Sparrow(name) as Bird) //부모클래스 컬렉션에 자식클래스 객체 추가. as Bird는 생략가능
}

open class Bird(name: String) {
    var name: String
    init {
        this.name = name
    }
}

class Sparrow(name: String): Bird(name) {
}

@주생성자와 프로퍼티의 이름이 같을시 위와같이 init에서 초기화하면 됨

 

-다운캐스팅: 업캐스팅된 자료형을 원래대로 변환시키는 것. 마찬가지로 자식클래스를 자료형으로 가지는 변수나 컬렉션 같은곳에 다시 넣으면 된다.

@업캐스팅을 되돌리는 것만 가능하고 원래부터 부모클래스였던 인스턴스는 다운캐스팅 불가!

=>업캐스팅한 객체를 다운캐스팅 가능한 이유: 업캐스팅해도 메모리상에 업캐스팅전 원본 정보가 남아있기때문이다.

 

 

 

2. 여러 인스턴스 리턴: 메소드가 여러 데이터 리턴하게 해주는 방법

1)다루는 법: 결과값을 toList()로 리스트화 시켜서 다루면 된다.

2)종류

<1>Pair

-메소드 반환 자료형:Pair<자료형1, 자료형2>

-사용:Pair(결과1, 결과2)

 

<2>Triple

-메소드 반환 자료형:Triple<자료형1, 자료형2, 자료형3>

-사용:Triple(결과1, 결과2, 결과3)

 

 

 

3. Scope Function

1)사용: 자기자신의 객체를 전달하여 효율적으로 처리 가능

2)종류

<1>let

-사용: .?let{ 앞의 객체를  it으로 받음. 수행코드 }

-결과: 블록 내 수행결과를 반환. 객체가 null인 경우 null반환

-예시

var result = strNum?.let {
	Integer.parseInt(it)
}

 

<2>with

-사용: with(객체){ 앞의 객체를 this로 받고 생략 가능. 수행코드 }

-결과: 블록 내 코드 수행. 객체는 반드시 null이 아니어야 함

@세이프콜(.?)을 지원하지 않기에 let과 병합해서 쓰기도 함. =>?.let{ with(it){ ~ } }

-예시

with(alphabets) {
	//var result = this.subSequence(0,2)
	var result = subSequence(0,2)
	println(result)
}

 

<3>also

-사용: .?also{ 앞의 객체를 it으로 받음. 수행코드}

-결과: 블록내 수행결과와 상관없이 앞에서 받았던 객체를 반환=>즉, 주로 객체를 수정할때 사용

-예시

 var result = student?.also {
 	it.age = 50
}

 

<4>apply

-사용: ?.apply{ 앞의 객체를 this로 받고 생략 가능. 수행코드 }

-결과: 블록내 수행결과와 상관없이 앞에서 받았던 객체를 반환=>즉, 주로 객체를 수정할때 사용

-예시

var result = student?.apply {
	age = 50
}

 

<5>run

-사용:

=>객체에서 호출하는 경우: ?.run{ 앞의 객체를 this로 받고 생략 가능. 수행코드 }

=>객체에서 호출하지 않는 경우: run{ 수행코드 }

-결과: 블록 내 수행 결과 반환     

@null체크가 가능하므로 with보다 안전하게 사용가능

 

3)유사성과 차이점

<1>run 과 let: run은 주로 반환값을 얻기위해 사용되며 let은 주로 어떤 작업을 하기 위해 사용됨

-예시

val result = "hello".run {
    println("The length of this string is $length")
    length // 반환 값
}

str?.let {
    println("The length of the string is ${it.length}")
}

 

<2>also와 apply: 둘 다 수신객체를 반환하는 함수이나 also는 수신객체로 it을 받고 apply는 수신객체로 this를 받음

<3>run과 with: 둘다 비슷하게 사용되나 run이 null체크가 가능하므로 더 안전

@표

  Scope에서 접근방식 this Scope에서 접근방식 it
블록 수행 결과를 반환 run, with let
객체 자신을 반환 apply also

 

4)Scope function이 중복되어 사용되는 경우:it이 여러번 나타나므로 람다식을 {c->it대신 c사용} 꼴로 바꾸어 줌

-예시

Person().also {
	it.name = "한석봉"
	it.age = 40
  val child = Person().also { c ->
	  c.name = "홍길동"
    c.age = 10
  }
  it.child = child
}

@this가 사용되는 것은 람다식으로 바꾸기 불가능

 

 

 

4. 확장함수

1)사용: 클래스에 메소드를 외부에서 추가하고 싶을때 사용

2)주의점:

-확장함수는 클래스의 public 멤버에만 접근할 수 있음

-하위 클래스에서 확장함수는 상속 불가능=>즉, 오버라이드도 불가능함.

3)예시

fun main() {
	//확장함수
    fun Student.getGrade() = println("학생의 등급은 ${this.grade} 입니다")
    var student = Student("참새", 10, "A+")
    student.getGrade()
}

class Student(name: String, age: Int, grade: String) {
    var name: String
    var age: Int
	var grade: String

    init {
        this.name = name
        this.age = age
		this.grade = grade
    }
 }

 

 

 

5. 비동기 프로그래밍

1) 의미

-동기 프로그래밍: 순차적으로 하나씩 작업을 수행. 요청을 보내고 결과값 반환까지 작업을 멈춤

-비동기 프로그래밍: 여러 일을 한번에 수행. 요청을 보내고 결과값을 받는 동안 멈추지 않고 또 다른 일을 수행

=>context switching이 중요!: 한 cpu에서 여러 작업을 수행하기 위해 스케줄링 하는 것

 

 

 

6. 쓰레드와 코루틴

1)사전작업: 외부 종속성 추가 필요

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC")
}

 

2)쓰레드

<1>의미

-프로세스: 프로그램이 메모리에 올라가서 실행되면 이걸 프로세스라고 함.

-쓰레드: 프로세스보다 작은 단위. 쓰레드는 독립된 메모리 영역을 가지고 이 영역은 스택형태로 되어있음.

=>쓰레드 생성시 스택메모리 일정 영역 차지

<2>사용: thread(start = true){}로 새 쓰레드 생성하여 사용 가능

<3>예시

fun main() {
    thread(start = true) {
        for(i in 1..10) {
            println("Thread1: 현재 숫자는 ${i}")
            runBlocking {
                launch {
                    delay(1000)
                }
            }
        }
    }
}

-delay(num): num에 해당하는 ms(마이크로 세컨드. 1000분의 1초)동안 일시 정지

-thread import시 더 긴걸로

-delay import시 Long으로 

 

<4>@Volatile

-사용: 일반적인 경우 변수에 대한 쓰기작업은 CPU캐시에 저장 =>다른 쓰레드에서 변경된값 읽기 불가

=>Volatile사용시 변수값을 즉시 메모리에 반영하므로 다른 쓰레드에서도 읽기 가능

-예시

class SharedData {
    @Volatile
    var sharedVariable: Int = 0
}

=>sharedVariable이란 변수를 @Volatile이란 키워드를 붙여 선언: 값이 변경되면 즉시 메모리에 반영

 

3)코루틴

<1>의미: 비동기 프로그래밍을 위한 경량 쓰레드 =>코드 조작을 실행하는데 사용

-경량 쓰레드: 쓰레드 보다 가볍고 쓰레드 풀 없이도 수많은 동시성 작업 수행 가능하게 해줌.

-비동기적 실행

-일시 중단 및 재개: 코루딘은 중단 및 재개가 가능하여 비동기 작업을 동기적으로 보이게 하고 코드 가독성과 유지 보수성 향상

-구조적 동시성: 코드를 동시성 작업으로 분할 할 수 있도록 구조를 제공

 

@구조적 동시성: 동시성 작업을 관리하기 위한 패러다임

-Scope 관리: 동시성 작업이 특정 스코프 내에 존재 =>메모리 누수 방지, 작업 생명주기의 명확한 관리

-부모 - 자식 관계: 동시성 작업이 부모 - 자식 관계 형성 =>부모작업 완료시 자식 작업도 함꼐 중단 혹은 완료됨

-예외 처리: 부모작업에서 발생한 예외를 모든 자식 작업에게 전파=>예외 처리를 명확히 해 예외 처리가 안됐을때 어플리케이션이 전체 중단되는 것을 방지

 

@동시성 작업: 동시에 여러 작업이 실행되는 것

-병렬성: 동시에 여러 작업이 실행되는것

-비동기성: 여러작업이 동시에 실행되나 서로 독립적인것.

-이벤트 기반 작업: 이벤트 루프를 통해 비동기적으로 실행. 외부 이벤트나 사용자입력 같은 것을 처리하는 데 사용

 

@이벤트 루프: 비동기적 이벤트를 처리하고 응용 프로그램이 반응적 동작 수행을 도와주는 프로그래밍 패턴

=>수행작업들:

- 이벤트 대기: 여러 소스로부터 이벤트 대기. 예를 들어 시스템 호출, 네트워크 소켓, 사용자 입력등

- 이벤트 처리: 이벤트 발생시 처리. 새로운 작업을 시작하거나 이벤트에 따른 동작수행등을 의미

-비동기 작업 관리: 비동기 작업 완료를 기다리거나 작업완료시 이벤트 발생시킴

-이벤트 큐 처리: 이벤트 루프는 대기중 이벤트 처리를 위해 이벤트 큐 사용. 이벤트가 큐에 추가되고 처리시까지 이벤트 루프 대기

 

<2>코루틴 빌더

[1]lauch: 가장 간단한 코루틴 빌더

-코루틴 생성 후 결과로 Job 객체 반환

-반환된 Job객체로 코루틴 제어 가능

-launch로 생성된 코루틴은 결과를 반환하지 않음 =>작업 완료 여부나 결과를 기다릴 필요 없을 경우 사용

 

@Job객체 함수

-join(): 코루틴이 종료되기를 기다림

-cancle(): 코루틴을 즉시 종료

-isActive(): 코루틴이 실행중인지 여부 확인

-예시

import kotlinx.coroutines.*

fun main() {
    // Job 객체 생성
    val job = GlobalScope.launch {
        delay(1000L)
        println("World")
    }
    
    println("Hello")

    // 코루틴이 완료될 때까지 기다림
    runBlocking {
        job.join()
        println("Done")
    }
}

-원래라면 launch로 생성된 코루틴내 println이 실행되기전에 메인쓰레드가 종료되어 끝나지만 job.join()으로 job이 끝나기를 기다림. 

-runBlocking은 블록내의 코드가 순차 실행되므로 job에 해당하는 코루틴이 완료된다음  println이 실행된다

 

 

[2]async

-코루틴 생성후 결과로  Deferred객체 반환

-Deferred는 코루틴 실행 결과를 나타내며 await()함수로 해당 결과를 기다려 얻

-async로 생성된 코루틴은 결과를 반환함 => 다른 코루틴에서 작업 수행 후 해당 작업 결과를 기다려야 하는 경우 사용됨

 

[3]예시

fun main() {
    // launch를 사용하여 코루틴 생성
    val job = GlobalScope.launch {
        delay(1000L)
        println("World")
    }
    
    println("Hello")

    // async를 사용하여 코루틴 생성
    val deferred = GlobalScope.async {
        delay(1000L)
        "World"
    }

    println("Hello")

    // async의 결과를 기다림
    runBlocking {
        val result = deferred.await()
        println(result)
    }

    // launch의 작업을 취소
    job.cancel()
}

 

 

@runBlocking{}

-비동기적 실행 작업으로 코드내용이 실행 완료될때까지 코루틴이 완료되지 않게 막음

-주로 테스트에 사용

-블록 내의 코드는 순차적(동기적)으로 실행됨

-예시

fun main() {
    runBlocking {
        // 코루틴 내에서 비동기 작업 시작
        val job = launch {
            delay(1000) // 1초 동안 대기
            println("Coroutine completed")
        }
        
        println("Before coroutine")
        job.join() // 코루틴이 완료될 때까지 대기
        println("After coroutine")
    }
}
//before => coroutine => after순으로 출력

 

 

<3>코루틴 스코프

-코루틴 범위 지정

 

-종류

[1]GlobalScope: 앱이 실행된 이후 계속 수행

[2]CoroutineScope: 필요할때 생성하고 사용 후 정리 필요

=>cancle()을 이용해 정리함

 

-Dispatcher

[1]의미: 코루틴을 실행할 쓰레드를 지정

[2]종류

-Dispatchers.Main: UI와 상호작용하기 위한 메인쓰레드

-Dispatchers.IO: 네트워크나 디스크 I/O작업에 최적화되어있는 쓰레드

-Dispatchers.Default: 기본적으로 CPU최적화되어있는 쓰레드

 

-예시

	println("메인쓰레드 시작")
    var job = CoroutineScope(Dispatchers.Default).launch {
        var fileDownloadCoroutine = async(Dispatchers.IO) {
            delay(10000)
            "파일 다운로드 완료"
        }
        var databaseConnectCoroutine = async(Dispatchers.IO) {
            delay(5000)
            "데이터베이스 연결 완료"
        }
        println("${fileDownloadCoroutine.await()}")
        println("${databaseConnectCoroutine.await()}")
    }
    runBlocking {
        job.join()
    }
    println("메인쓰레드 종료")
    job.cancel()

-파일 다운과 데이터베이스 연결을 병렬처리하여 메인쓰레드가 블로킹 되지 않도록 함.

-await()로 작업이 완료되기를 기다리고 이후 결과 출력하게해 코루틴간 의존성 관리

 

4)쓰레드와 코루틴 차이

<1>쓰레드

-작업단위: Thread

=>각 쓰레드가 독립적인 stack메모리 영역을 가짐

-동시성 보장 수단: Context switching

[1]운영체제 커널에 의한 context switching으로 동시성 보장

[2]블로킹(blocking): 쓰레드 A가 쓰레드 B의 결과를 기다리고 있으면 쓰레드 A를 블로킹 상태라 한다

=>A는 B의 결과가 나올떄까지 해당 자원 사용 불가

 

<2>코루틴

-작업단위: Coroutine Object

=>여러 작업에 각각 Object할당

=>이 역시 객체이기에 JVM Heap에 적재

@Heap: 동적 메모리 저장소. 동적으로 변하는 데이터 구조 저장에 사용

 

-동시성 보장 수단: Programmer Switching

[1]소스 코드를 통해 Switching시점을 정함 =>OS는 관여X

[2]Suspend<유예>(Non-Blocking):

-Object1이 Object2의 결과를 기다릴때 1은 suspend로 바뀜

-1을 수행하던 쓰레드는 유효

-즉, 2도 1과 동일 쓰레드에서 진행

-여러 task를 하나의 쓰레드에서 수행가능