Collection 의 합을 구하는 방법

Collection 의 합을 구하는 방법은 reducesum 두 가지가 존재합니다.

단, Stream 에서 sum() 을 사용하려면 IntStream, LongStream, DoubleStream 와 같은 기본형 (Primitive Type) 특화 스트림을 사용해야 합니다.

그래서 보통 mapToInt, mapToLong, mapToDouble 같은 메소드로 스트림을 변환시키고 사용합니다.

 

reduce

reduce(초기값, 연산) 형식으로 사용합니다.

초기값부터 시작하여 각 원소를 차례대로 순회하며 연산을 수행합니다.

이전 연산의 결과를 다음 초기값으로 넘기면서 연산의 결과를 누적해서 총 결과값을 구하는 메서드입니다.

int sum = Stream.of(1, 2, 3).reduce(0, Integer::sum);

 

sum

Stream 의 총합을 구하는 메서드입니다.

기본형 특화 스트림에서만 사용 가능합니다.

int sum = Stream.of(1, 2, 3).mapToInt(e -> e).sum();

 

그렇다면 합을 구할 때 어떤걸 사용할까?

처음 생각 할 때는 reduce 로 한번에 하는게 Stream 처리가 적어서 더 빠를거라고 생각했습니다.

가독성도 크게 차이 안납니다.

그런데 실제로 테스트 해보니 결과는 달랐습니다.

 

Test Code

public static void main(String[] args) {
    Map<String, Integer> hashmap = new HashMap<>();

    for (int i = 0; i < 10000000; i++) {
        hashmap.put(i + "", i);
    }

    // for-loop 시간측정
    long start1 = System.currentTimeMillis();
    int sum1 = 0;
    for (String key : hashmap.keySet()) {
        sum1 += hashmap.get(key);
    }
    long end1 = System.currentTimeMillis();

    // stream mapToInt 시간측정
    long start2 = System.currentTimeMillis();
    int sum2 = hashmap.values().stream().reduce(0, Integer::sum);
    long end2 = System.currentTimeMillis();

    // stream reduce 시간측정
    long start3 = System.currentTimeMillis();
    int sum3 = hashmap.values().stream().mapToInt(i -> i).sum();
    long end3 = System.currentTimeMillis();

    System.out.println("for-loop: " + (end1 - start1));
    System.out.println("int reduce: " + (end2 - start2));
    System.out.println("mapToInt: " + (end3 - start3));
}

Result

for-loop: 303
reduce: 368
mapToInt: 310

 

원인 & 결론

reduce 에는 박싱, 언박싱 비용이 들어갑니다.

내부적으로 합계를 계산하기 위해 Integerint 형으로 언박싱 하고 다시 int -> Integer 로 박싱하는 과정이 숨겨져 있어서 시간이 더 오래 걸립니다.

그래서 reduce 의 성능이 더 느리니 IntStream 에서 제공하는 메서드가 있는 경우에는 해당 메서드를 사용하는 게 유용합니다.

 

Reference

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

Java Stream count() 의 비밀  (0) 2021.04.18
Java 8 함수형 인터페이스 (Functional Interface)  (1) 2021.04.10
Mac OS Java (OpenJDK) 설치 및 버전 변경  (2) 2021.03.08
[Java] Static  (0) 2020.09.19
[Java] Map, HashMap, LinkedHashMap  (0) 2020.04.25

1. Collection Sort

Kotlin 에서는 Collection 을 정렬하기 위한 여러가지 유틸리티들을 제공합니다.


1.1. Sort, Sorted

가장 쉬운 방법은 sort 메소드를 호출하는 겁니다.

기본적으로 오름차순으로 정렬합니다.

val list = mutableListOf(1, 2, 7, 6, 5, 6)
list.sort()
println(list)  // [1, 2, 5, 6, 6, 7]

sort 메소드는 해당 Collection 의 원소 위치가 변경됩니다.

기존 Collection 은 그대로 둔 채 새로운 Collection 으로 받길 원한다면 sorted 메소드를 사용해야 합니다.

sorted 메소드를 사용하면 기존 Collection 은 변하지 않습니다.

val list = mutableListOf(1, 2, 7, 6, 5, 6)
val sorted = list.sorted()
println(sorted)  // [1, 2, 5, 6, 6, 7]
println(list)    // [1, 2, 7, 6, 5, 6] (sorted 를 사용했기 때문에 변하지 않음)

내림차순으로 정렬하고 싶다면 sortByDescending 를 사용하거나 reverse 메소드를 사용하면 됩니다.

마찬가지로 sortedByDescending 를 사용하면 원래 Collection 의 변경 없이 내림차순으로 정렬된 값을 구할 수 있습니다.

// 1. sortByDescending 로 내림차순 정렬
list.sortByDescending { it }

val sorted = list.sortedByDescending { it }

// 2. reverse 사용해서 정렬 후 뒤집기
list.sort()
list.reverse()

val sorted = list.sorted().reversed()

1.2. SortBy

만약 Object 의 특정 Property 들을 기준으로 정렬하고 싶다면 sortBy 메소드를 사용하면 됩니다.

sortBy 메소드는 Object 를 받아서 Property 를 반환하는 Lamdba 식을 파라미터로 받습니다.

val list = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
list.sortBy { it.second }
println(list)  // [(1, a), (2, b), (7, c), (5, c), (6, d), (6, e)]

sort 와 마찬가지로 기존 Collection 의 변경 없이 정렬된 값을 받고 싶다면 sortedBy 를 사용하면 됩니다.

그리고 내림차순을 지원하는 sortByDescending 도 있습니다.


1.3. SortWith

sortWith 메소드를 사용하면 여러 가지 조건을 섞어서 정렬할 수 있습니다.

sortWith 메소드는 Comparator 를 파라미터로 받습니다.

(Kotlin 에서 Comparator 를 생성하는 여러가지 방법은 다음 챕터에서 다룹니다)

val list = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
list.sortWith(compareBy({it.second}, {it.first}))
println(list)  // [(1, a), (2, b), (5, c), (7, c), (6, d), (6, e)]

위 Collection 은 it.second(문자) 로 먼저 정렬된 후에 it.first(숫자) 로 정렬됩니다.

그리고 역시 sortedWith 메소드가 존재하며, 역순으로 정렬할때는 reverse 를 사용하거나 Comparator 를 반대로 수정하면 됩니다.


2. Comparison

Kotlin 은 Comparator 를 만들기 위해 kotlin.comparisons 라는 유용한 패키지를 제공합니다.

이 챕터에서는 아래 컨텐츠를 다룹니다.

  • Comparator creation
  • Handling of null values
  • Comparator rules extension

2.1. Comparator Creation

Kotlin 은 Comparator 를 생성하는 여러 팩토리 메서드를 제공합니다.


2.1.1. naturalOrder

가장 간단한 생성 메서드는 naturalOrder() 입니다.

아무런 파라미터를 필요로 하지 않으며 오름차순을 기본으로 합니다.

val ascComparator = naturalOrder<Long>()

2.1.2. compareBy

여러 개의 속성을 사용하고 싶다면 compareBy 메소드를 사용하면 됩니다.

파라미터로는 Comparable 를 리턴하는 정렬 규칙을 여러 개 사용할 수 있습니다.

그럼 넘겨진 규칙들은 순차적으로 호출 되며 원소들을 정렬합니다.

만약 먼저 나온 규칙에서 원소의 우열이 가려져 정렬 처리가 되었다면 뒤의 규칙들은 확인하지 않습니다.

val complexComparator = compareBy<Pair<Int, String?>>({it.first}, {it.second})

위 코드에서 it.first 값을 사용해 먼저 비교를 하고 값이 같은 경우에만 it.second 비교까지 이루어집니다.


2.1.3. Comparator

간단하게 new Comparator 를 선언해서 만들 수도 있습니다.

자바와 마찬가지로 두 원소에 대한 비교 조건을 넣어줘야 합니다.

val caomparator = Comparator<Int> { a, b -> a.compareTo(b) }

2.2. Handling of null Values

정렬하려는 Collection 이 null 값을 갖고 있을 수도 있습니다.

nullsFirst 또는 nullsLast 와 함께 Comparator 를 사용하면 null 값을 가장 처음 또는 가장 마지막에 위치하도록 설정할 수 있습니다.

val list = mutableListOf(4, null, 1, -2, 3)

list.sortWith(nullsFirst())  // [null, -2, 1, 3, 4]

list.sortWith(nullsLast())  // [-2, 1, 3, 4, null]

list.sortWith(nullsFirst(reverseOrder()))  // [null, 4, 3, 1, -2]

list.sortWith(nullsLast(compareBy { it }))  // [-2, 1, 3, 4, null]

2.3. Comparator Rules Extension

Comparator 오브젝트는 추가적인 정렬 규칙과 혼합되거나 확장할 수 있습니다.

kotlin.comparable 패키지에 있는 then 키워드를 활용하면 됩니다.

첫 번째 비교의 결과가 동일할 때만 두번째 비교가 이루어집니다.

val students = mutableListOf(21 to "Helen", 21 to "Tom", 20 to "Jim")

val ageComparator = compareBy<Pair<Int, String?>> {it.first}
val ageAndNameComparator = ageComparator.thenByDescending {it.second}

// [(20, Jim), (21, Tom), (21, Helen)]
println(students.sortedWith(ageAndNameComparator))

위 코드는 나이가 어린 순으로 먼저 정렬하고 나이가 같으면 이름을 알파벳 역순으로 정렬합니다.


Reference

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

Kotlin Collections 와 Sequences 의 차이점 (feat. Java Stream)  (1) 2022.01.27
Kotlin Enum  (0) 2021.10.06
[Kotlin] Swap  (0) 2020.11.11
[Kotlin] For 문  (0) 2020.11.11
[Kotlin] String - drop, dropLast, dropWhile, dropLastWhile  (0) 2020.09.25

Swap

Python 만큼은 아니지만 Kotlin 에서도 문법을 활용하여 값의 Swap 을 쉽게 짤 수 있다.

var a = 1
var b = 2

a = b.also { b = a }

println(a) // print 2
println(b) // print 1

For

Kotlin 에서의 for 문은 자바와 마찬가지로 iterator 를 사용합니다.

따라서 iterator(), next(), hasNext() 을 제공하는 모든 컬렉션에서 for 문을 사용할 수 있습니다.

기본 for 문은 아래와 같습니다.

for (item in collection) {
    print(item)
}

특정 범위 (range) 를 지정해서 사용할 수도 있습니다.

.. 을 사용하면 마지막 숫자까지 포함하until 을 사용하면 마지막 숫자는 포함하지 않습니다.

// 1, 2, 3
for (i in 1..3) {
    println(i)
}

// 1, 2
for (i in 1 until 3) {
    println(i)
}

downTo 를 이용하면 감소하는 for 문을 만들 수 있고 step 을 사용하면 증가되는 양을 수정할 수 있습니다.

// 6, 4, 2, 0
for (i in 6 downTo 0 step 2) {
    println(i)
}

배열을 순회할 때는 index 만 뽑아내거나 (index, value) 형태로 순회 가능합니다.

for (i in array.indices) {
    println(array[i])
}
for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

컬렉션 타입에서는 forEach, forEachIndexed 를 사용하여 람다식으로 표현할 수 있습니다.

// 1, 2, 3
listOf(1, 2, 3).forEach { println(it) }

/**
 * index: 0, value: 4
 * index: 1, value: 5
 * index: 2, value: 6
 */
listOf(4, 5, 6).forEachIndexed { index, value -> 
    println("index: $index, value: $value")
}

drop, dropLast, dropWhile, dropLastWhile

drop 은 문자열의 앞이나 뒷부분을 자를 때 사용된다.

내부적으로는 substring 으로 구현되어 있다.

  • drop : 앞에서부터 n 개의 문자를 제거한 String 을 반환
  • dropLast: 뒤에서부터 n 개의 문자를 제거
  • dropWhile, dropLastWhile: 조건을 만족하지 않는 문자가 나올때까지 제거


/* definition */
fun String.drop(n: Int): String

fun String.dropLast(n: Int): String

fun String.dropWhile(
  predicate: (Char) -> Boolean
): String

fun String.dropLastWhile(
  predicate: (Char) -> Boolean
): String


/* exmaple */
val string = "<<<First Grade>>>"

println(string.drop(6)) // st Grade>>>
println(string.dropLast(6)) // <<<First Gr
println(string.dropWhile { !it.isLetter() }) // First Grade>>>
println(string.dropLastWhile { !it.isLetter() }) // <<<First Grade


Reference

String

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

Kotlin Collections 와 Sequences 의 차이점 (feat. Java Stream)  (1) 2022.01.27
Kotlin Enum  (0) 2021.10.06
Kotlin Collection Sorting (정렬)  (0) 2021.02.19
[Kotlin] Swap  (0) 2020.11.11
[Kotlin] For 문  (0) 2020.11.11

Java Static 이란?

변수나 메소드 앞에 static 키워드를 붙여 사용한다.

static 은 같은 클래스에 있는 경우 같은 메모리 주소를 바라본다는 점을 이용해서 메모리 효율 최적화, 데이터 공유 등을 위해 사용한다.

한글로는 "정적인" 이라는 뜻인데 한번에 이해하기가 쉽지 않다.

동적인 다른 자원들과 달리 프로그램 상의 변하지 않는 자원이라고 생각하면 된다.


생성 시기와 소멸 시기

static 키워드를 사용하면 객체가 생성되는 시점이 아닌 Class Loader 가 클래스를 Load 할 때 Data 영역에 메모리가 할당되게 된다.

이 영역은 같은 유형의 클래스마다 공유되며 Process 가 종료되는 시점에서 해제되므로 static 키워드의 생명주기 역시 Class Load 시 생성되고 Process 종료 시 해제되게 된다.


static 변수

클래스 내에서 static 으로 선언된 변수는 모든 클래스 객체에서 같은 메모리 주소를 바라본다.

예를 들어 Student 라는 클래스가 있다. 이 클래스에는 학생들이 살고 있는 도시인 teacher 변수를 갖는다.

public class Student {
  String name;
  String teacher;
}

학생들은 모두 같은 반이라서 선생님이 같다고 가정하자.

만약 반의 선생님이 바뀌어도 모든 학생들이 동시에 바뀌어야 한다.

보통 한 반에 학생이 30 명이라고 가정하면, 선생님이 바뀌었을 때 30 명의 학생들을 전부 바꿔줘야 한다.

하지만 이럴 때 teacher 변수를 static 으로 선언한다면 Student 객체의 모든 teacher 가 같은 메모리를 공유해서 데이터를 바꾸면 동시에 바뀐다.


static 메소드

static 메소드는 객체 생성 없이 사용할 수 있으며 보통 유틸리티 클래스에서 주로 사용한다.

static 메소드 내에서는 static 변수만 사용 가능하고 this 호출이 불가능하다.

"이메일 형식 검사", "현재 시간 구하기" 등은 새로운 객체를 할당할 필요가 없기 때문에 static 으로 작성하는 게 좋다.


static 을 응용항 싱글톤 패턴 (Singleton pattern)

디자인 패턴 중 하나인 싱글톤 패턴은 static 의 개념을 이용한 패턴이다.

싱글톤은 단 하나의 객체만 생성하는 패턴이다.

싱글톤 클래스를 만드는 규칙은 다음과 같다.

  1. 싱글톤 클래스의 생성자를 private 로 하여 외부에서 생성하지 못하게 한다.
  2. 클래스 내부에 static 변수를 선언한다.
  3. getInstance() 메소드를 static 으로 만들고 내부에서 싱글턴 객체를 선언하여 리턴한다.
class Singleton {
  private static Singleton instance;

  // 생성자를 private 로 하여 외부에서 생성 불가능하게 만듬
  private Singleton() { }

  public static Singleton getInstacne() {
    if (instance == null) {
      instance = new Singleton();
    }

    return instance;
  }
}


템플릿 리터럴

템플릿 리터럴 (template literals)은 변수를 이용해서 동적으로 문자열을 생성할 수 있다.

기존에는 문자열에 변수를 추가하려면 더하기 기호를 반복해서 사용해야했다.

const year = 22;
const name = 'alice';
const text = 'Hi! My name is ' + name + ' and I am ' + year * 2 + ' years old';
console.log(text);      // Hi! My name is alice and I am 22 years old


이런식으로 코드를 작성할 경우 시간도 너무 오래걸리고 따옴표로 인한 가독성도 굉장히 떨어지게 된다.

ES6 에서는 템플릿 리터럴로 바꾸어 표현할 수 있다.

템플릿 리터럴은 백틱(` `) 을 사용하며 변수나 식은 ${variable} ${expression} 으로 입력한다.

const text = `Hi! My name is ${name} and I am ${year * 2} years old`


여러 줄을 입력할 때는 엔터를 입력하면 된다.

// 기존: \n 을 사용
const text = 'Hi! My name is ' + name + '\n and I am ' + year * 2 + ' years old';

// ES6: 엔터 추가
const text = `Hi! My name is ${name} 
and I am ${year * 2} years old`


async await

Promise 를 사용해도 어쩔 수 없는 한계가 존재한다.

콜백 함수에 비해서 조금 나아졌다고는 하나 중첩해서 쓰거나 return 으로 함수 호출하면서 로직이 복잡해지면 가독성이 떨어진다.

async await 을 사용하면 then 체이닝보다 가독성이 좋아진다.

하지만 async await 도 프로미스를 활용하는 개념이기 때문에 프로미스를 완전히 대체할 수는 없다.


1. 사용하기

1.1. async

async 키워드를 이용해서 정의된 함수는 항상 프로미스를 반환한다.

따라서 함수 호출 뒤에 then 으로 체이닝이 가능하다.

리턴값이 프로미스라면 그대로 반환된다.

async function getData1() {
  return 111;
}

const getData2 = async () => {
  throw new Error(222);
}

async function getData3() {
  return Promise.resolve(333);
}

getData1().then(data => console.log(data));     // 111
getData2().catch(err => console.log('getData2: ' + err));   // getData2: Error: 222
getData3().then(data => console.log(data));     // 333


1.2. await

await 키워드는 async 함수 내부에서만 사용된다.

await 키워드를 붙여서 프로미스를 사용하면 해당 프로미스가 처리됨 상태가 될 때까지 기다린다.

function printAfter(seconds) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('print ' + seconds);
      resolve(seconds);
    }, seconds * 1000)
  },);
}

async function log() {
  const data1 = await printAfter(3);    // 3 초 뒤 출력
  const data2 = await printAfter(5);    // 8 초 뒤 출력
  console.log(data1, data2);
}

log();


2. 활용하기

2.1. 비동기 함수 병렬 실행

await 키워드가 붙으면 해당 함수가 끝날때까지 기다리게 된다.

하지만 여러 개의 await 함수를 호출할 때 함수들 간의 의존성이 없다면 굳이 순차적으로 실행할 필요가 없다.

프로미스는 생성과 동시에 실행된다는 점을 활용하여 프로미스 생성을 먼저 하고 await 키워드를 나중에 호출하는 방법을 사용하면 된다.

async function log() {
  const p3 = printAfter(3);     // 3초 뒤 출력
  const p5 = printAfter(5);     // 5초 뒤 출력

  const data1 = await p3;
  const data2 = await p5;
  
  console.log(data1, data2);
}


아니면 Promise.all 을 사용한다면 더 간단하게 표현 할 수 있다.

async function log() {
  const [data1, data2] = await Promise.all([printAfter(3), printAfter(5)]);

  console.log(data1, data2);
}


2.2. 예외 처리

try catch 문으로 감싸면 async / sync 함수 구분하지 않고 발생하는 예외들을 모두 잡는다.

async function log() {
  try {
    const data = printAfter(3);
    console.log(data);
  } catch (error) {
    console.log(error);
  }
}


+ Recent posts