제네릭(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)는 프로그램 실행 중 발생할 수 있는 예기치 못한 상황을 의미한다
예외 처리는 프로그램의 안정성을 높이고 오류를 우아하게 처리하는 데 필수적이다
예외의 종류를 알아보자
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);
}
}
'TIL' 카테고리의 다른 글
TIL - 2024/06/11 (0) | 2024.06.11 |
---|---|
TIL - 2024/06/10 (0) | 2024.06.10 |
TIL - 2024/06/05 (0) | 2024.06.05 |
TIL - 2024/06/04 (0) | 2024.06.04 |
TIL - 2024/06/03 (0) | 2024.06.03 |