1. Overview

Java 의 람다 표현식은 익명 함수처럼 자유 변수 (Free Variable) 를 활용할 수 있습니다.

여기서 자유변수란 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수 를 의미합니다.


2. 지역 변수 제약

하지만 모든 자유 변수를 사용할 수 있는건 아닙니다.

변수의 종류에는 클래스 변수, 인스턴스 변수, 지역 변수가 있는데 이 중에서 지역 변수는 final 이거나 final 처럼 사용 되어야 합니다.

여기서 final 처럼 사용되어야 한다는건 effectively final 라고 표현하는데 final 선언이 되어 있지 않지만 변수의 재할당이 발생하지 않는 변수 를 의미합니다.


3. Effectively final

람다 표현식에서 사용 불가능한 지역 변수는 인텔리제이에서 다음과 같이 경고해줍니다.

Variable used in lambda expression should be final or effectively final

간단한 예를 통해 effectively final 가 뭔지 쉽게 이해해 봅니다.


3.1. 가능

int number = 1357;
Runnable r = () -> System.out.println(number);

위 코드에서 number 변수는 final 선언이 되어 있지 않지만 에러가 발생하지 않습니다.

변수의 재할당이 이루어지지 않았기 때문입니다.


3.2. 불가능

int number = 1357;
number = 2;
Runnable r = () -> System.out.println(number);

위 코드는 number 가 한번 선언된 후 2 로 재할당 되었기 때문에 람다 표현식 내부에서 사용 불가능합니다.


int number = 1357;
Runnable r = () -> System.out.println(number);
number = 3;

마찬가지로 사용한 이후에 변경되어도 사용 불가능합니다.


4. 이유가 뭘까?

우선 인스턴스 변수와 지역 변수의 차이점부터 알아야 합니다.

인스턴스 변수는 힙에 저장되고 지역 변수는 스택에 저장됩니다.

스택 영역은 힙과 달리 각 쓰레드끼리 공유되지 않는 영역 입니다.

참고로 클래스 변수는 쓰레드끼리 공유 되는 메소드 영역에 저장됩니다.


4.1. Lambda Capturing

람다 표현식은 다른 쓰레드에서도 실행 가능합니다.

만약 A 쓰레드에서 생성한 람다 표현식을 B 쓰레드에서 사용한다고 생각해봅니다.

인스턴스 변수는 쓰레드끼리 공유 가능한 힙 영역에 저장되기 때문에 공유 가능하지만 스택 영역에 있는 지역 변수는 외부 쓰레드에서 접근 불가능합니다.

따라서 자바에서는 스택 영역에 있는 변수를 사용할 수 있도록 지역 변수의 복사본을 제공해서 람다 표현식에서 접근 가능하도록 합니다.

이를 람다 캡쳐링이라고 합니다.

그런데 원본 값이 바뀌면 복사본에 있는 값과의 동기화가 보장되지 않기 때문에 동시성 이슈가 발생합니다.

그래서 지역 변수는 값이 변하지 않는다는 final 임이 보장되어야 합니다.


5. Conclusion

Inner 클래스, 익명 (Anonymous) 클래스, 람다 표현식 등에서 사용되는 외부 지역 변수가 final 또는 effectively final 상태여야 하는 이유를 알아봤습니다.

요약하면 아래와 같습니다.

  1. 람다 표현식은 여러 쓰레드에서 사용할 수 있다.
  2. 힙 영역에 저장되는 인스턴스 변수와 달리 스택 영역에 저장되는 지역 변수는 외부 쓰레드에서 접근 불가능하다.
  3. 외부 쓰레드에서도 지역 변수 값을 사용할 수 있도록 복사본 기능을 제공하는데 이를 람다 캡쳐링이라고 한다.
  4. 복사본은 원본의 값이 바뀌어도 알 수 없기 때문에 쓰레드 동기화를 위해 지역 변수는 final 또는 effectively final 상태여야 한다.

Reference

1. Overview

Java 에서 테스트 코드를 짤 때 특정 자료구조의 원소 값을 확인해야 하는 테스트가 있습니다.

반복문을 돌면서 일일히 확인해야 하거나 그냥 코드 한줄 한줄 입력하는 방법도 있지만 org.assertj.core.api.Assertions 에서 제공하는 assertThat().contains() 를 사용하면 좀 더 깔끔하게 확인할 수 있습니다.


2. contains

Assertions.assertThat 이후에 사용하는 contains 메소드는 단순합니다.

중복여부, 순서에 관계 없이 값만 일치하면 테스트가 성공합니다.


3. 사용법

void containsTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    // Success: 모든 원소를 입력하지 않아도 성공
    assertThat(list).contains(1, 2);

    // Success: 중복된 값이 있어도 포함만 되어 있으면 성공
    assertThat(list).contains(1, 2, 2);

    // Success: 순서가 바뀌어도 값만 맞으면 성공
    assertThat(list).contains(3, 2);

    // Fail: List 에 없는 값을 입력하면 실패
    assertThat(list).contains(1, 2, 3, 4);
}

assertThat(비교대상 자료구조).contains(원소1, 원소2, 원소3, ..) 형식으로 사용합니다.

위 예시만 보면 사용법을 한눈에 알 수 있습니다.


4. String, Array, Set, List 모두 사용 가능

@Test
void stringContainsTest() {
    String str = "abc";
    assertThat(str).contains("a", "b", "c");
}

@Test
void arrayContainsTest() {
    int[] arr = {1, 2, 3, 4};
    assertThat(arr).contains(1, 2, 3, 4);
}

@Test
void setContainsTest() {
    Set<Integer> set = Set.of(1, 2, 3);
    assertThat(set).contains(1, 2, 3);
}

List 는 위에서 테스트 했었고, 다른 자료구조도 가능합니다.


5. containsOnly, containsExactly

추가적으로 좀 더 구체적인 테스트를 위한 여러 가지 메소드가 제공됩니다.

그 중에서 두 가지만 추가로 알아봅니다.


5.1. containsOnly: 순서, 중복을 무시하는 대신 원소값과 갯수가 정확히 일치

/*
 * containsOnly 실패 케이스
 *
 * assertThat(list).containsOnly(1, 2);       -> 원소 3 이 일치하지 않아서 실패
 * assertThat(list).containsOnly(1, 2, 3, 4); -> 원소 4 가 일치하지 않아서 실패
 */
@Test
void containsOnlyTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    assertThat(list).containsOnly(1, 2, 3);
    assertThat(list).containsOnly(3, 2, 1);
    assertThat(list).containsOnly(1, 2, 3, 3);
}

containsOnly 는 원소의 순서, 중복 여부 관계 없이 값만 일치하면 됩니다.

contains 와 다른 점은 원소의 갯수까지 정확히 일치해야 한다는 점입니다.

예를 들어 위 list 에서 contains(1, 2) 는 성공하지만 containsOnly(1, 2) 는 실패합니다.


5.2. containsExactly: 순서를 포함해서 정확히 일치

/*
 * containsExactly 실패 케이스
 *
 * assertThat(list).containsExactly(1, 2);       -> 원소 3 이 일치하지 않아서
 * assertThat(list).containsExactly(3, 2, 1);    -> list 의 순서가 달라서 실패
 * assertThat(list).containsExactly(1, 2, 3, 3); -> list 에 중복된 원소가 있어서 실패
 */
@Test
void containsExactlyTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    assertThat(list).containsExactly(1, 2, 3);
}

containsExactly 는 원소가 정확히 일치해야 합니다.

중복된 값이 있어도 안되고 순서가 달라져도 안됩니다.

특정 자료구조의 정확한 값을 테스트 하고 싶은 경우에는 이 메소드를 사용할 수 있습니다.


Referecne

1. H2 Database 홈페이지에서 다운로드

다운로드 링크 : https://www.h2database.com/html/main.html


최신 버전보다는 안정화된 버전이 괜찮습니다.



2. 압축 풀고 실행

다운 받은 파일의 압축을 풀면 다음과 같은 구성으로 되어 있습니다.

여기서 ./bin/h2.sh 를 입력하면 h2 console 을 실행합니다.

만약 권한이 없으면 chmod 755 ./bin/h2.sh 로 권한을 부여합니다.



3. 한번 연결해서 ~.mv.db 파일 생성 후 실행

처음 진입하면 아래와 같은 화면이 나옵니다.

다른 칸은 전부 그대로 두고 JDBC URL 부분만 표시한 것처럼 바꿔줍니다.

그리고 연결 버튼을 눌러서 진입합니다.


그럼 다음과 같이 내 Root 폴더에 my-db-test.mv.db 파일이 생깁니다.

이후에 다시 h2 console 에서 연결 끊기 후 jdbc:h2:tcp://localhost/~/my-db-test 로 접속해서 사용하면 됩니다.



4. Spring Boot 에 연결

application.yml 에서 DB 설정할 때 spring.datasource.url 에 JDBC URL 과 동일하게 세팅합니다.

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/my-db-test
    username: sa
    password:
    driver-class-name: org.h2.Driver

Reference

Overview

Java 8 Stream 에는 count() 라는 종결 함수가 있습니다.

현재 Stream 의 원소 갯수를 카운트 해서 long 타입으로 리턴합니다.


count 의 중간 연산으로 peek 사용

public class NotePad {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
              .peek(System.out::println)
              .count();
    }
}

위 코드는 어떻게 동작할까요?

아무것도 나오지 않습니다.

원인을 몰라서 구글링 해봤더니 Java 9 Stream API Reference 에 다음과 같은 글이 있었습니다.


The number of elements covered by the stream source, a List, is known and the intermediate operation, peek, does not inject into or remove elements from the stream (as may be the case for flatMap or filter operations). Thus the count is the size of the List and there is no need to execute the pipeline and, as a side-effect, print out the list elements.


요약하자면 count() 라는 종결함수는 Stream 의 갯수를 세기 때문에 효율을 위해 중간 연산을 생략하기도 한다는 뜻입니다.

그래서 중간 연산을 강제로 실행시키고 싶다면 filterflatMap 과 같이 Stream 요소의 갯수를 변화시킬 수 있는 중간 연산을 추가하면 됩니다.


filter 로 강제 출력

public class NotePad {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
              .filter(e -> e > 0)
              .peek(System.out::println)
              .count();
    }
}

filter 를 추가하면 peek 도 정상적으로 동작합니다.


1
2
3
4
5

Reference

Overview

함수형 인터페이스란 1 개의 추상 메소드를 갖는 인터페이스를 말합니다.

Java8 부터 인터페이스는 기본 구현체를 포함한 디폴트 메서드 (default method) 를 포함할 수 있습니다.

여러 개의 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스입니다.

자바의 람다 표현식은 함수형 인터페이스로만 사용 가능합니다.


1. Functional Interface

함수형 인터페이스는 위에서도 설명했듯이 추상 메서드가 오직 하나인 인터페이스를 의미합니다.

추상 메서드가 하나라는 뜻은 default method 또는 static method 는 여러 개 존재해도 상관 없다는 뜻입니다.

그리고 @FunctionalInterface 어노테이션을 사용하는데, 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해줍니다.

@FunctionalInterface 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는 게 좋습니다.


1.1. Functional Interface 만들기

@FunctionalInterface
interface CustomInterface<T> {
    // abstract method 오직 하나
    T myCall();

    // default method 는 존재해도 상관없음
    default void printDefault() {
        System.out.println("Hello Default");
    }

    // static method 는 존재해도 상관없음
    static void printStatic() {
        System.out.println("Hello Static");
    }
}

위 인터페이스는 함수형 인터페이스입니다.

default method, static method 를 넣어도 문제 없습니다.

어차피 함수형 인터페이스 형식에 맞지 않는다면 @FunctionalInterface 이 다음 에러를 띄워줍니다.

Multiple non-overriding abstract methods found in interface com.practice.notepad.CustomFunctionalInterface


1.2. 실제 사용

CustomInterface<String> customInterface = () -> "Hello Custom";

// abstract method
String s = customInterface.myCall();
System.out.println(s);

// default method
customInterface.printDefault();

// static method
CustomFunctionalInterface.printStatic();

함수형 인터페이스라서 람다식으로 표현할 수 있습니다.

String 타입을 래핑했기 때문에 myCall()String 타입을 리턴합니다.

마찬가지로 default method, static method 도 그대로 사용할 수 있습니다.

위 코드를 실행한 결과값은 다음과 같습니다.

Hello Custom
Hello Default
Hello Static

2. Java 에서 기본적으로 제공하는 Functional Interfaces

매번 함수형 인터페이스를 직접 만들어서 사용하는 건 번거로운 일입니다.

그래서 Java 에서는 기본적으로 많이 사용되는 함수형 인터페이스를 제공합니다.

기본적으로 제공되는 것만 사용해도 웬만한 람다식은 다 만들 수 있기 때문에 개발자가 직접 함수형 인터페이스를 만드는 경우는 거의 없습니다.


함수형 인터페이스 Descripter Method
Predicate T -> boolean boolean test(T t)
Consumer T -> void void accept(T t)
Supplier () -> T T get()
Function<T, R> T -> R R apply(T t)
Comparator (T, T) -> int int compare(T o1, T o2)
Runnable () -> void void run()
Callable () -> T V call()

2.1. Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Predicate 는 인자 하나를 받아서 boolean 타입을 리턴합니다.

람다식으로는 T -> boolean 로 표현합니다.


2.2. Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Consumer 는 인자 하나를 받고 아무것도 리턴하지 않습니다.

람다식으로는 T -> void 로 표현합니다.

소비자라는 이름에 걸맞게 무언가 (인자) 를 받아서 소비만 하고 끝낸다고 생각하면 됩니다.


2.3. Supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier 는 아무런 인자를 받지 않고 T 타입의 객체를 리턴합니다.

람다식으로는 () -> T 로 표현합니다.

공급자라는 이름처럼 아무것도 받지 않고 특정 객체를 리턴합니다.


2.4. Function

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Function 은 T 타입 인자를 받아서 R 타입을 리턴합니다.

람다식으로는 T -> R 로 표현합니다.

수학식에서의 함수처럼 특정 값을 받아서 다른 값으로 반환해줍니다.

T 와 R 은 같은 타입을 사용할 수도 있습니다.


2.5. Comparator

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

Comparator 은 T 타입 인자 두개를 받아서 int 타입을 리턴합니다.

람다식으로는 (T, T) -> int 로 표현합니다.


2.6. Runnable

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 은 아무런 객체를 받지 않고 리턴도 하지 않습니다.

람다식으로는 () -> void 로 표현합니다.

Runnable 이라는 이름에 맞게 "실행 가능한" 이라는 뜻을 나타내며 이름 그대로 실행만 할 수 있다고 생각하면 됩니다.


2.7. Callable

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable 은 아무런 인자를 받지 않고 T 타입 객체를 리턴합니다.

람다식으로는 () -> T 로 표현합니다.

Runnable 과 비슷하게 Callable 은 "호출 가능한" 이라고 생각하면 좀 더 와닿습니다.


Supplier vs Callable

SupplierCallable 은 완전히 동일합니다.

아무런 인자도 받지 않고 특정 타입을 리턴해줍니다.

둘이 무슨 차이가 있을까..?

사실 그냥 차이가 없다고 생각하시면 됩니다.

단지 CallableRunnable 과 함께 병렬 처리를 위해 등장했던 개념으로서 ExecutorService.submit 같은 함수는 인자로 Callable 을 받습니다.


3. 두 개의 인자를 받는 Bi 인터페이스

특정 인자를 받는 Predicate, Consumer, Function 등은 두 개 이상의 타입을 받을 수 있는 인터페이스가 존재합니다.


함수형 인터페이스 Descripter Method
BiPredicate (T, U) -> boolean boolean test(T t, U u)
BiConsumer (T, U) -> void void accept(T t, U u)
BiFunction (T, U) -> R R apply(T t, U u)

4. 기본형 특화 인터페이스

지금까지 확인한 함수형 인터페이스를 제네릭 함수형 인터페이스라고 합니다.

자바의 모든 형식은 참조형 또는 기본형입니다.

  • 참조형 (Reference Type) : Byte, Integer, Object, List
  • 기본형 (Primitive Type) : int, double, byte, char

Consumer<T> 에서 T 는 참조형만 사용 가능합니다.

Java 에서는 기본형과 참조형을 서로 변환해주는 박싱, 언박싱 기능을 제공합니다.

  • 박싱 (Boxing) : 기본형 -> 참조형 (int -> Integer)
  • 언박싱 (Unboxing) : 참조형 -> 기본형 (Integer -> int)

게다가 개발자가 박싱, 언박싱을 신경쓰지 않고 개발할 수 있게 자동으로 변환해주는 오토박싱 (Autoboxing) 이라는 기능도 제공합니다.

예를 들어 List<Integer> list 에서 list.add(3) 처럼 기본형을 바로 넣어도 사용 가능한 것도 오토박싱 덕분입니다.

하지만 이런 변환 과정은 비용이 소모되기 때문에, 함수형 인터페이스에서는 이런 오토박싱 동작을 피할 수 있도록 기본형 특화 함수형 인터페이스 를 제공합니다.

IntPredicate, LongPredicate 등등 특정 타입만 받는 것이 확실하다면 기본형 특화 인터페이스를 사용하는 것이 더 좋습니다.

아래에서 소개하는 인터페이스 외에 UnaryOperatorBi 인터페이스에도 기본형 특화를 제공합니다.


4.1 Predicate (T -> boolean)

기본형을 받은 후 boolean 리턴

  • IntPredicate
  • LongPredicate
  • DoublePredicate

4.2. Consumer (T -> void)

기본형을 받은 후 소비

  • IntConsumer
  • LongConsumer
  • DoubleConsumer

4.3. Function (T -> R)

기본형을 받아서 기본형 리턴

  • IntToDoubleFunction
  • IntToLongFunction
  • LongToDoubleFunction
  • LongToIntFunction
  • DoubleToIntFunction
  • DoubleToLongFunction

기본형을 받아서 R 타입 리턴

  • IntFunction<R>
  • LongFunction<R>
  • DoubleFunction<R>

T 타입 받아서 기본형 리턴

  • ToIntFunction<T>
  • ToDoubleFunction<T>
  • ToLongFunction<T>

4.4. Supplier (() -> T)

아무것도 받지 않고 기본형 리턴

  • BooleanSupplier
  • IntSupplier
  • LongSupplier
  • DoubleSupplier

Conclusion

Java 8 에서 람다에 활용 가능한 함수형 인터페이스를 제공하고 있습니다.

직접 만들어서 쓸 수도 있지만 이미 제공하는 인터페이스로도 대부분 처리 가능하므로 어떤 게 있는지 잘 파악해서 활용해야 합니다.


Reference

Problem


로봇이 존재합니다.

로봇은 3 개의 명령어만 듣습니다.

  • G: 앞으로 한칸 이동
  • L: 왼쪽으로 90 도 회전
  • R: 오른쪽으로 90 도 회전

로봇이 수행해야하는 일련의 명령어 리스트인 instructions 이 주어졌을 때, 이 로봇이 영원히 같은 위치를 순회하는 지 판단하는 문제입니다.



Solution

로봇을 움직이는건 굉장히 단순합니다.

(0, 0) 좌표에서 시작하여 처음 방향은 0 으로 설정합니다.

모든 행동을 완료한 후에 로봇의 상태만 확인하면 됩니다.

  1. 행동을 완료했을 때 (0, 0) 위치에 있다면 몇번을 해도 같은 자리를 반복한다.
  2. 행동을 완료했을 때 (0, 0) 위치는 아니지만 다른 방향을 보고 있다.

1 번은 너무 당연한 겁니다.

2 번은 example 3 에도 나와 있습니다.

다른 위치에 도달했을 때, 다른 방향을 보고 있다면 두번째 instructions 은 그 위치와 방향에서 새로 시작합니다.

그렇기 때문에 최대 4 번을 반복하면 원래 위치로 돌아옵니다.

반복문을 4 번 돌리면서 (0, 0) 위치에 돌아왔는지 확인해도 되지만, 위와 같은 방식으로 쉽게 확인할 수 있습니다.



Java Code

class Solution {
    public boolean isRobotBounded(String instructions) {
        int[] dx = {-1, 0, 1, 0};
        int[] dy = {0, 1, 0, -1};
        int x = 0, y = 0, dir = 0;

        for (char instruction : instructions.toCharArray()) {
            if (instruction == 'G') {
                x += dx[dir];
                y += dy[dir];
            } else if (instruction == 'L') {
                dir = (dir + 1) % 4;
            } else {
                dir = (dir + 3) % 4;
            }
        }

        // 1. 싸이클 완료후에 (0, 0) 에 있는지
        // 2. 또는 위치가 바꼈더라도 초기 방향과 다른 방향을 보고 있다면 
        //    4번 순회했을 시 원래 자리로 돌아옴
        return x == 0 && y == 0 || dir != 0;
    }
}

Problem


주어진 배열을 3 등분 합니다.

나뉘어진 각 부분배열들의 모든 합이 똑같게 되도록 3등분 할 수 있는지 묻는 문제입니다.



Solution

아이디어를 알면 쉽게 풀 수 있는데 모르면 굉장히 복잡해집니다.

가장 중요한 포인트는 다음 세개입니다.

  1. non-empty 배열. 즉, 부분 배열에는 최소 하나의 원소가 존재
  2. n 등분이 아니라 3 등분으로 숫자가 고정되어 있음
  3. 모든 부분 배열의 합은 같음

위 조건들로 우리는 몇 가지 사실을 알 수 있습니다.

  1. 주어진 배열의 합이 정확히 3 으로 나누어 떨어져야 함
  2. 부분 배열의 합은 sum / 3 으로 고정되어 있음

부분 배열의 합을 미리 알고 있기 때문에 좀더 수월하게 답을 구할 수 있습니다.

각 부분 배열의 누적합 partition 이 목표값인 goal 에 도달하면 갯수를 하나씩 증가시킵니다.

count 가 3 이 되는 순간 조건이 성립합니다.

혹시 남은 원소가 있더라도 부분 배열의 합이 무조건 goal 이기 때문에 남은 원소의 합은 0 이 될 겁니다.



Java Code

class Solution {
    public boolean canThreePartsEqualSum(int[] arr) {
        int sum = 0;
        for (int a : arr) { sum += a; }

        // 3등분 했을 때 모두 같아야 하기 때문에 한 파트의 합은 무조건 sum/3 이다.
        int goal = sum / 3; 

        if (sum % 3 != 0) return false;

        int partition = 0, count = 0;
        for (int a : arr) {
            partition += a;

            if (partition == goal) {
                partition = 0;
                count++;
            }

            if (count == 3) {
                return true;
            }
        }

        return false;
    }
}

Problem


2진수 두개를 받아서 합한 결과를 리턴하는 문제입니다.



Solution

처음에는 2진수 -> 10진수 변환 후 더하려고 했는데 2진수 길이가 너무 길어서 그런지 NumberFormatException 이 발생했습니다.

그래서 그냥 1 의 자리부터 더한 다음에 뒤집는 방식으로 구현했습니다.

Stack 자료구조를 써도 됩니다.



Java Code

class Solution {
    public String addBinary(String a, String b) {
        int aIdx = a.length() - 1;
        int bIdx = b.length() - 1;

        int sum = 0;
        StringBuilder sb = new StringBuilder();

        while (aIdx >= 0 || bIdx >= 0) {
            if (aIdx >= 0) sum += a.charAt(aIdx--) - '0';
            if (bIdx >= 0) sum += b.charAt(bIdx--) - '0';

            sb.append(sum % 2);
            sum /= 2;
        }

        if (sum == 1) sb.append(sum);
        return sb.reverse().toString();
    }
}

+ Recent posts