[Java] Java 기초 - Generics, Lambda

Hyunjun Kim·2025년 4월 11일
0

Data_Engineering

목록 보기
25/153

13. 제네릭스(Generics)

제네릭스는 타입시스템을 유연하게 사용할 수 있는 기능이다.

13.1 제네릭스(Generics)란

클래스나 메소드 레벨에서 명세(동작)는 같지만 사용되는 타입만 다른 경우, 객체를 생성할 때 타입을 지정할 수 있도록 하는 기능이다.

  • 객체는 런타임에 생성되지만,객체가 사용하는 타입은 선언적으로 정의함으로써 컴파일 타임의 타입 체크를 통해서 더 안전하고 유연한 코드를 작성할 수 있다.
  • 만약에 List가 Integer용, String용 모두 구현체가 따로 있고, instanceof와 캐스팅으로 타입체크를 하면서 써야한다면 매우 불편할 것이다.

13.2 제네릭스의 형식과 약어

13.2.1 제네릭스의 형식

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}
<T> 리턴타입 함수명(파라미터타입 a){...} // T를 리턴타입, 파라미터 타입, 함수 구현체 등에서 타입으로 쓸 수 있음.

13.2.2 자주 사용되는 타입인자 약어

  • <T> == Type
  • <E> == Element
  • <K> == Key
  • <V> == Value
  • <N> == Number
  • <R> == Result

13.3 제네릭스를 활용한 예제

Collection 클래스를 살펴보면 제네릭스를 어떻게 사용하는지 가장 잘 알 수 있다.

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList();
        Collection<String> collection = list;
    }
}

Collection.java 중 일부

public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    Iterator<E> iterator();
    boolean add(E e);
    <T> T[] toArray(T[] a);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
}

List.java 중 일부

public interface List<E> extends Collection<E> {
    // Collection 에 있는 메소드들 모두 포함
    // + List 에만 있는 메소드들
    boolean add(E e);
}

ArrayList.java 중 일부

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

    public boolean add(E e) {
        ensureCapacityInternal(size + 1); // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
}



14. 람다(Lambda)

람다는 클래스와 함수를 선언하지 않고, 함수형 표현을 할 수 있는 문법이다.

14.1 람다식(Lambda expression)이란

"식별자 없이 실행 가능한 함수" 로, 함수의 이름을 따로 정의하지 않아도 곧바로 함수처럼 사용할 수 있다. 문법이 간결하여 보다 편리한 방식이다 (익명 함수라고도 부른다.)

14.2 람다식의 형식

14.2.1 람다식 형식

'→'의 의미는 매개변수를 활용하여 {}안에 있는 코드를 실행한다.

[기존의 메소드 형식]

반환타입 메소드이름(매개변수 선언) {
	수행 코드 블록
}

[람다식의 형식]

반환타입 메소드이름(매개변수 선언) -> {
	수행 코드 블록
}

14.2.2 람다식 예제

public class Main {
    public static void main(String[] args) {
        ArrayList<String> strList =
                new ArrayList<>(Arrays.asList("korea", "japan", "china", "france", "england"));
        Stream<String> stream = strList.stream();
        stream.map(str -> str.toUpperCase()).forEach(System.out::println);
    }
}

14.2.3 이중 콜론 연산자

위의 예제에서 :: 가 사용됐다. ::(이중 콜론 연산자) 는 호출하고자 하는 함수의 파라미터의 갯수와 각각의 타입이 labmda식에 전달되는 인자와 일치하는 경우, object 변수에 대한 선언과 파라미터 값 입력을 생략하고 호출하고자 하는 클래스의 함수만을 입력해서 코드 양을 줄일 수 있도록 도와준다.

이중 콜론 연산자는 functional(Java 8의 Stream) programming에서 사용할 수 있는 연산자다.

예제 - 이중 콜론 연산자를 사용한 경우

public class Main {
    public static void main(String[] args) {
        List<String> cities = Arrays.asList("서울", "부산", "속초", "수원", "대구");
        cities.forEach(System.out::println);
    }
}

예제 - 이중 콜론 연산자를 사용하지 않은 경우

public class Main {
    public static void main(String[] args) {
        List<String> cities = Arrays.asList("서울", "부산", "속초", "수원", "대구");
        cities.forEach(x -> System.out.println(x));
    }
}

14.3 람다식의 단점

람다식이 코드를 보다 간결하게 만들어주는 역할을 하지만 그렇다고 무조건 좋다고만 이야기 할 수는 없다.

  1. 람다를 사용하여서 만든 익명 함수는 재사용이 불가능하다.
  2. 람다만을 사용할 경우 비슷한 메소드를 중복되게 생성할 가능성이 있어서 지저분해질 수 있다. (+ 변경에 취약해질 수 있음)
  3. 로그를 보거나 디버깅을 하기 어려울 수 있다. 함수의 정확한 이름과 위치가 나타나지 않을 수 있기 때문이다.

람다를 사용하면 로그를 보거나 디버깅을 할 때 함수의 이름이 없다.
우리가 함수를 선언한다면 클래스를 선언하고 클래스는 패키지 이름까지 해서 자기의 이름이 있고 함수가 있고 안에 라인이 있으니까 찾아가기 쉽다. 그런데 식제로 익명함수를 쓰면 anonymous라는 이름으로 라인만 뜨는데 그 라인이 또 우리가 보는 코드의 라인과 일치하는 않는 경우도 있다. 그래서 함수의 정확한 이름과 위치가 나타나지 않을 수 있기 때문에 디버깅 시 람다를 많이 쓰면 어렵다.

14.3.1. 예제 - 람다 사용ㅎ해 만든 익명 함수는 재사용이 불가능하다

StreamWithoutFunction.java

public class StreamWithoutFunction {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>(Arrays.
                asList("Korea","Japan","China","France","England"));
        Stream<String> stream = stringList.stream();
        stream.map(s -> {
            System.out.println(s);
            System.out.println("logic");
            return s.toUpperCase();
        }).forEach(System.out::println);

        Stream<String> stream2 = stringList.stream();
        stream2.map(s -> {
            System.out.println(s);
            System.out.println("logic");
            return s.toUpperCase();
        }).forEach(System.out::println);
    }
}

StreamWithFunction.java

// 람다를 사용하지 않고 함수 사용하면 더 간결하게 코드 작성할 수 있다.
public class StreamWithFunction {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>(Arrays.
                asList("Korea", "Japan", "China", "France", "England"));
        Stream<String> stream = stringList.stream();
        stream.map(StreamWithFunction::logic).forEach(System.out::println);

        Stream<String> stream2 = stringList.stream();
        stream2.map(StreamWithFunction::logic).forEach(System.out::println);
    }

    public static String logic(String param){
        System.out.println(param);
        System.out.println("logic");
        return param.toUpperCase();
    }
}

14.3.2. 예제 - 람다식에서 에러가 나는 경우

public class Main {
    public static void main(String[] args) {
        List<String> stringList =
                new ArrayList<>(Arrays.asList("Korea", "Japan", "China", "France", "England"));
        Stream<String> errorStream = stringList.stream();
        errorStream.map(Main::logic).map((str) -> new ArrayList<>(Arrays.asList(str)).stream()
                .map(String::toLowerCase).map((nextStr) -> {
                    System.out.println("inner lambda");
                    if ("korea".equals(nextStr)) {
                        throw new RuntimeException("error");
                    }
                    return nextStr;
                }).findFirst()).collect(Collectors.toList());
    }
}

재사용 불가한 익명 함수

.map((nextStr) -> {
    // ...
})
  • 예제에서처럼 nextStr -> { ... } 는 람다 내부에 직접 구현된 익명 함수다.
  • 이 코드는 재사용이 불가능하고, 다른 곳에서도 같은 로직이 필요하면 다시 람다를 복붙해야 함. (👉 중복 발생 + 유지보수 어려움)

디버깅 및 예외 추적 어려움

if ("korea".equals(nextStr)) {
    throw new RuntimeException("error");
}
  • 위 코드처럼 람다 내부에서 예외를 발생시키면, 예외 로그에 함수 이름이나 위치 정보가 제대로 나오지 않음.
  • RuntimeException이 발생한 위치가 익명 람다이기 때문에 스택 트레이스가 불친절해 디버깅이 어렵다.
profile
Data Analytics Engineer 가 되

0개의 댓글