TIL

TIL - 2024/06/07

기석김 2024. 6. 7. 15:08

제네릭(Generics)

제네릭은 코드의 재사용성 ↑타입안전성 ↑ 중요한 역할을 한다

 

타입 안전성이란 : 컴파일 시 타입 검사를 할 수 있어 런타임 에러를 줄일 수 있음

코드 재사용성은 다양한 타입을 처리할 수 있는 코드 작성 가능

명시적인 타입 정보를 제공하여 코드의 가독성

 

제네릭이란?

클래스나 메소드에서 사용할 데이터 타입을 외부에서 지정할 수 있게 하는 기법

 

제네릭을 사용한 리스트 선언 예제

ArrayList<String> list = new ArrayList<>();

위 코드에서 꺾쇠 괄호(<>) 안에 있는 String이 제네릭이다.

 

 

리스트에서 제네릭 사용 예제를 간단하게 보자

List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");

for (String str : stringList) {
    System.out.println(str);
}

리스트는 제네릭을 사용하여 다양한 타입의 데이터를 저장할 수 있다

 

맵에서 제네릭 사용 예제를 보자

Map<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);


for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

키와 값을 제네릭 타입으로 정의 가능하다

 

제네릭의 장점

1. 타입 안정성

제네릭을 사용하면 컴파일 시 타입을 검사하여 런타임 에러를 줄일 수 있다

List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(1); // 컴파일 에러

만약 1을 넣을경우 String이 아니라 int 여서 오류가 난다.

 

2.코드 재사용성

제네릭을 사요하면 다양한 타입을 처리할 수 있는 코드 작성 가능

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

 

3. 가독성

명시적인 타입 정보를 제공하여 코드의 가독성 향상 가능

Box<String> intBox = new Box<>();
intBox.setContent("");
System.out.println(intBox.getContent());

 

제네릭의 제한

1. 제한된 타입 파라미터

제네릭 타입 파라미터는 특정 타입을 상속받거나 구현하도록 제한할 수 있다

public class NumberBox<T extends Number> {
    private T number;

    public void setNumber(T number) {
        this.number = number;
    }

    public T getNumber() {
        return number;
    }
}

 

2. 와일드카드

와일드카드는 제네릭 타입을 보다 유연하게 사용할 수 있도록 한다

public void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

// Number와 그 하위 타입만을 받는다
public static void print(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num);
    }
}

 

와일드카드는 세가지 종류가 있다

1. <?>: 모든 타입 허용

2. <? extends 타입>: 타입과 그 하위 타입 허용

3. <? super 타입>: 타입과 그 상위 타입 허용

 

 

이 사진을 잘 이해해야 한다. 

 

모든 타입 허용의 예제다

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

public class WildcardExample {
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.println(elem);
        }
    }

    public static void main(String[] args) {
        List<Object> objList = new ArrayList<>();
        objList.add("String");
        objList.add(1);

        List<Number> numList = new ArrayList<>();
        numList.add(1);
        numList.add(1.5);

        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);

        printList(objList);  // OK
        printList(numList);  // OK
        printList(intList);  // OK
    }
}

 

<? extends number>의 예제

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

public class WildcardExample {
    public static void printNumberList(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        List<Object> objList = new ArrayList<>();
        objList.add("String");
        objList.add(1);

        List<Number> numList = new ArrayList<>();
        numList.add(1);
        numList.add(1.5);

        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);

        // printNumberList(objList);  // Error
        printNumberList(numList);  // OK
        printNumberList(intList);  // OK
    }
}

 

<? super Integer>의 예제

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

public class WildcardExample {
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);  // OK
        list.add(2);  // OK
        // list.add(1.5);  // Error
    }

    public static void main(String[] args) {
        List<Object> objList = new ArrayList<>();
        objList.add("String");
        objList.add(1);

        List<Number> numList = new ArrayList<>();
        numList.add(1);
        numList.add(1.5);

        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);

        addNumbers(objList);  // OK
        addNumbers(numList);  // OK
        // addNumbers(intList);  // Error
    }
}

 

정리 하자면

 

List<?>: 어떤 타입이든 허용된다

List<Object>, List<Number>, List<Integer> 모두 허용된다

 

List<? extends Number>: Number와 그 하위 타입만 허용된다

List<Number>, List<Integer>는 허용되지만 List<Object>는 허용되지 않는다

 

List<? super Integer>: Integer와 그 상위 타입만 허용된다

List<Object>, List<Number>는 허용되지만 List<Long>는 허용되지 않는다

 

Exception과 Stream

예외의 개념을 알아보자 , 예외란?

예외(Exception)는 프로그램 실행 중 발생할 수 있는 예기치 못한 상황을 의미한다

예외 처리는 프로그램의 안정성을 높이고 오류를 우아하게 처리하는 데 필수적이다

 

Java는 Primitive Type(원시타입)을 제외한 모든것이 Object(객체) 이다. → Exception도 객체

 

예외의 종류를 알아보자

1. Error: 개발자가 직접 처리할 수 없는 오류

ex) OutOfMemoryError, StackOverflowError, InternalError, UnknownError 등

 

OutofMemoryError: JVM이 힙 메모리를 모두 사용한 경우 발생

StackOverflowError: 스택 메모리가 모두 사용된 경우 발생

InternalError: JVM 내부에서 예상치 못한 문제가 발생했을때 발생

UnknownError: 예상치 못한 심각한 시스템 오류가 발생했을때 발생

 

2.Exception: 개발자가 직접 처리할 수 있는 오류

a. Checked Exception: 컴파일 시에 예외 처리를 강제하는 에외

ex) IOException, SQLException, ClassNotFoundException, InstantiationExcepiton 등

 

IOEXCEPTION: 입출력 작업 중 발생하는 예외

SQLException: db 액세스 오류가 발생할때 발생

ClassNotFoundException: 클래스 파일을 찾을 수 없을 때 발생

InstantiationExcepiton: 클래스의 인스턴스를 생성할 수 없을 때 발생

 

예외처리

1. try-catch 블록

try {
    // 예외 발생 가능성 있는 코드
} catch (ExceptionType e) {
    // 예외 처리 코드
}
public class FileNotFoundExceptionExample {
    public static void main(String[] args) {
        try {
            FileInputStream file = new FileInputStream("nonexistentfile.txt");
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        }
    }
}

 

// FileNotFoundException 처리

public class SQLExceptionExample {
    public static void main(String[] args) {
        try {
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "user", "password");
        } catch (SQLException e) {
            System.out.println("SQL error: " + e.getMessage());
        }
    }
}

// SQLException 처리

 

2 finally 블록

try {
    // 예외 발생 가능성 있는 코드
} catch (ExceptionType e) {
    // 예외 처리 코드
} finally {
    // 항상 실행되는 코드
}
public class FinallyBlockDBExample {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            // 데이터베이스 연결
            conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "user", "password");
            stmt = conn.createStatement();
            // 데이터베이스 쿼리 실행
            ResultSet rs = stmt.executeQuery("SELECT * FROM users");
            while (rs.next()) {
                System.out.println("사용자 ID: " + rs.getInt("id") + ", 이름: " + rs.getString("name"));
            }
        } catch (SQLException e) {
            System.out.println("SQL 오류: " + e.getMessage());
        } finally {
            // 리소스 정리
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    System.out.println("Statement 닫기 오류: " + e.getMessage());
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    System.out.println("Connection 닫기 오류: " + e.getMessage());
                }
            }
            System.out.println("데이터베이스 작업 완료.");
        }
    }
}

//DB연결에서 finally 블록사용한 예시

 

3 사용자 정의 예외

사용자 정의 예외는 Exception 클래스를 상속받아 생성할 수 있다

class MyCustomException extends Exception {
    public MyCustomException(String message) {
        super(message);
    }
}

Exception과 try-catch

Checked Exception이란?

Checked Exception은 컴파일 타임에 체크되는 예외다

예를 들어, 파일 입출력(IO), 네트워크 연결, 데이터베이스 접근 시 발생할 수 있는 예외 !

 

try-catch 블록을 무조건 사용

1 컴파일러 강제: Checked Exception은 컴파일러가 예외 처리를 강제, 예외를 처리하지 않으면 컴파일 오류가 발생

2 안정성 확보: Checked Exception을 처리함으로써 프로그램이 예상치 못한 상황에서 중단되지 않도록 한다.

예를 들어, 파일을 읽는 도중 발생할 수 있는 IOException을 처리하지 않으면 프로그램이 중단될 수 있다

// Checked Exception 예제
public void readFile() {
    try {
        FileReader reader = new FileReader("somefile.txt");
        reader.read();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

생략 방법

try-catch 블록 대신 메소드 시그니처에 throws 키워드를 사용해 호출자에게 예외 처리를 위임할 수 있다

public void readFile() throws IOException {
    FileReader reader = new FileReader("somefile.txt");
    reader.read();
}

렇게 하면 readFile 메소드를 호출하는 쪽에서 예외 처리를 강제하게 된다

 

Unchecked Exception이란?

Unchecked Exception은 런타임에 발생하는 예외로, 컴파일 타임에 체크되지 않는다.

예를 들어, NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException 등

 

try-catch 블록을 생략 가능하다

1 명시적 선언 불필요: Unchecked Exception은 메소드 시그니처에 명시적으로 선언하지 않아도 된다.

코드가 간결해지고, 예외 처리를 강제하지 않아도 된다

2 일반적인 오류: 대부분의 Unchecked Exception은 프로그래밍 오류로 인해 발생하며,

이런 오류는 코드를 수정하여 방지할 수 있기 때문에 try-catch 블록으로 처리하지 않는 경우가 많다.

// Unchecked Exception을 따로 처리하지 않는 예제
public void divide(int a, int b) {
    System.out.println(a / b);
}

 

그래도 try-catch로 잡으려면?

// Unchecked Exception 예제
public void divide(int a, int b) {
    try {
        System.out.println(a / b);
    } catch (ArithmeticException e) {
        e.printStackTrace();
    }
}

 

결론

Checked Exception은 반드시 try-catch 블록으로 처리하거나,

메소드 시그니처에 throws로 선언하여 호출자에게 예외 처리를 위임해야 한다.

예외가 발생할 가능성이 높은 상황에서 프로그램의 안정성을 확보하기 위함이다.

 

Unchecked Exception은 try-catch 블록으로 처리하지 않아도 된다. 주로 프로그래밍 오류로 인해 발생하며,

코드 수정으로 예방할 수 있기 때문이다 필요 시 전역 예외 처리기를 사용하여 관리할 수 있다.

스트림 (Stream)

자바 8에서 추가된 스트림은 람다를 활용할 수 있는 기술 중 하나!.

반복문으로 요소 하나씩 순회하면서 다루는 방식을 함수 여러개를 조합해서 결과를 필터링하고

가공하는 방식으로 변경한 것 입니다. 즉 배열과 컬렉션을 함수형으로 처리한것이 스트림 이다.

 

스트림이란?

스트림(Stream)은 데이터의 흐름을 추상화한 것으로, 연속된 데이터 처리 작업을 수행할 수 있게 한다.

스트림은 데이터를 변경하지 않고, 데이터를 필터링하고 변환하며 집계하는 데 사용된다

1. 생성하기 : 스트림 인스턴스 생성

2. 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations)

3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations)

 

스트림의 특징

지연 연산(Lazy Evaluation): 중간 연산은 결과를 바로 생성하지 않고, 최종 연산이 수행될 때까지 연산을 지연시킨다

병렬 처리(Parallel Processing): 병렬 스트림을 사용하여 데이터 처리 작업을 병렬로 수행할 수 있다

불변성(Immutability): 스트림의 원본 데이터는 변경되지 않으며, 모든 연산은 새로운 스트림을 반환한다

 

스트림의 생성

스트림은 여러 가지 방법으로 생성할 수 있다. 대표적인 방법은 컬렉션(Collection)에서 스트림을 생성하는 것

List<String> list = Arrays.asList("a", "b", "c", "d");
Stream<String> stream = list.stream();

 

중간 연산 (Intermediate Operations)

중간 연산은 스트림을 변환하거나 필터링하는 데 사용된다.

중간 연산은 또 다른 스트림을 반환하며, 연산은 지연되어 최종 연산이 호출될 때 수행된다.

 

1. 필터링 (Filtering)

필터(filter)는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다.

인자로 받는 Predicate 는 boolean 을 리턴하는 함수형 인터페이스로 평가식이 들어가게 된다.

Stream<T> filter(Predicate<? super T> predicate);
List<String> filteredList = list.stream()
                                .filter(s -> s.startsWith("a"))
                                .collect(Collectors.toList());

 

2. 맵핑 (Mapping)

맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해준다.이 때 값을 변환하기 위한 람다를 인자로 받습니다

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

스트림에 들어가 있는 값이 input 이 되어서 특정 로직을 거친 후

output 이 되어 (리턴되는) 새로운 스트림에 담기게 된다. 이러한 작업을 맵핑(mapping)이라고 한다.

List<String> mappedList = list.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());

 

최종 연산 (Terminal Operations)

최종 연산은 스트림을 소모하며, 최종 결과를 생성한다. 대표적인 최종 연산에는 forEach, collect, reduce 등이 있다

//forEach
list.stream()
    .forEach(System.out::println);
    
//collect
List<String> collectedList = list.stream()
                                 .collect(Collectors.toList());
                                 
//reduce
Optional<String> reduced = list.stream()
                               .reduce((s1, s2) -> s1 + s2);

 

스트림 활용 예시

public class StreamExample {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("apple", "banana", "orange", "grape", "melon");

        // 1. 필터링: "a"로 시작하는 단어만 선택
        List<String> filteredList = list.stream()
                                        .filter(s -> s.startsWith("a"))
                                        .collect(Collectors.toList());

        // 2. 맵핑: 모든 단어를 대문자로 변환
        List<String> mappedList = list.stream()
                                      .map(String::toUpperCase)
                                      .collect(Collectors.toList());

        // 3. 출력
        System.out.println("Filtered List: " + filteredList);
        System.out.println("Mapped List: " + mappedList);
    }
}