제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입을 체크해주는 역할을 한다.
Java에서 다양한 데이터 타입을 사용하는 클래스나 메서드를 사용할 때, 코드의 재사용성과 안정성을 향성시키기 위해 도입되었다.
선언 방식은 다음과 같다.
public class ClassName <T> {...}
public class ClassName <T, V> {...} // map
package oop2.jenerics;
import java.util.ArrayList;
public class MyCustomList<T> {
ArrayList<T> List = new ArrayList<>();
public void addElement(T element) {
list.add(element);
}
public void removeElement(T element) {
list.remove(element);
}
}
package oop2.jenerics;
public class GenericsRunner {
public static void main(Stirng[] args) {
MyCustomList<String> list = new MyCustomList();
list.addElement("Element 1");
lst.addElement("Element 2");
MyCustomList<Integer> list2 = new MyCustomList();
list2.addElement(Integer.valueOf(5));
list2.addElement(Integer.valueOf(7));
}
}
👉 MyCustomList 클래스의 타입을 별도로 지정하지 않았으므로, String 타입과 Integer 타입 모두 사용해 리스트를 생성할 수 있다.
제네릭 타입 | 설명 |
---|---|
T | Type |
E | Element |
K | Key |
V | Value |
N | Name |
제네릭 타입을 좀 더 유연하게 다룰 수 있는 도구로, 주로
?
를 통해 사용된다.
List<?> list = new ArrayList<String>();
// 하위타입만 지정
List<? extends Number> numbers = new ArrayList<Integer>();
// 상위타입만 지정
List<? super Integer> integerss = new ArrayList<Number>();
⛔️ caution!
와일드카드는 유연성을 제공하지만 그만큼 코드를 읽고 유지보수하는데 어려움을 초래할 수 있다. 따라서 사용 시 주의하여 필요한 상황에서만 사용하는 것이 좋다.
Java에서 스트림(Stream)이란
데이터의 흐름
을 의미하는데, 스트림을 이용해 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다.
스트림(Stream)은 자바8에서 추가된 기술로, 람다를 활용할 수 있다.
자바8 이전에는 배열 또는 컬렉션 인스턴스를 다루기 위해 for
이나 forEach
문을 사용해 요소 하나 하나를 꺼내어 다루어야 했다. 간단한 경우라면 상관 없지만 로직이 복잡할수록 코드의 양이 많아지고, 메소드를 나눌 경우 루프를 여러 번 돌며 반복되는 작업을 해야하는 등 문제가 많았다.
❗️ 이를 해결하기 위해 등장한 것이 바로 스트림(Stream)
이다.
스트림의 가장 큰 장점은 람다를 활용할 수 있다는 점이다. 람다를 통해 코드의 양을 줄이고 간결하게 표현할 수 있다. 즉 배열과 컬렉션을 함수형으로 처리할 수 있는 것이다.
스트림의 두 번째 장점은 간단하게 병렬처리(multi-threading) 를 할 수 있다는 점이다. 멀티 스레드 작업을 통해 많은 요소들을 빠르게 처리할 수 있다.
💡병렬 처리(parallel processing) : 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 말한다.
스트림의 중간 연산 의 횟수에는 제한이 없다.
sorted
(정렬) : 인자들을 오름차순/내림차순으로 정렬numbers.stream().sorted().forEach(e -> System.out.println(e));
distinct
(중복 제거) : 중복된 요소를 제외하고 입력된 순서대로 출력 numbers.stream().distinct().forEach(e -> System.out.println(e));
map
: 스트리의 각 요소를 콜백함수에 매핑하여 새로운 배열 또는 컬렉션 인스턴스로 반환numbers.stream().distinct().sorted().map(e -> e * e).forEach(e -> System.out.println(e));
// 1~10까지의 숫자를 제곱해서 출력
IntStream.range(1,11).map(e -> e * e).forEach(p -> System.out.println(p));
// 1, 4, 9, 16, 25, 36, 49, 64, 81, 100
// 리스트를 모두 소문자로 바꿔서 출력
List.of("Apple", "Ant", "Bat").stream().map(s -> s.toLowerCase()).forEach(p -> System.out.println(p));
// apple, ant, bat
// 리스트를 글자 수 반환
List.of("Apple", "Ant", "Bat").stream().map(s -> s.length()).forEach(p -> System.out.println(p));
// 5, 3, 3
reduce()
: 하나의 결과값을 반환하는데 사용되며, 주로 총합이나 최대, 최소값 계산 시 사용된다. // reduce 문법
T reduce(T identity, BinaryOperator<T> accumulator)
// 1부터 10까지의 정수의 총합을 계산
int sum = IntStream.range(1, 11).reduce(0, (n1, n2) -> n1 + n2);
max()
: 최대값을 반환하는 메서드로, 요소들이 Comparable 인터페이스를 구현하고 있어야 한다. // 정수 리스트에서 최대값을 구하는 종단 연산
List<Integer> numbers = List.of(23, 12, 34, 53);
numbers.stream().max((n1,n2) -> Integer.compare(n1,n2)).get();
min()
: 최소값을 구하는 메소드// 최솟값 찾기
List<Integer> numbers = List.of(23, 12, 34, 53);
numbers.stream().min((n1,n2) -> Integer.compare(n1,n2)).get();
collect()
: 스트림의 요소를 모아 새로운 컬렉션을 생성하거나 요약할 때 사용한다.// 홀수만 찾아 리스트화 하기
numbers.stream().filter(e -> e % 2 == 1).collect(Collectors.toList());
numbers.stream().filter(e -> e % 2 == 0).collect(Collectors.toList()); // 짝수
boxed()
: 기본자료형 스트림을 해당 기본 자료형의 래퍼 클래스 스트림으로 변환한다. 💡 기본 자료형 스트림을 래퍼 클래스의 스트림으로 변환하는 이유
- 기본 자료형 스트림 : 기본 자료형에 특화된 연산만 사용 가능
- 래퍼 클래스 스트림 : 일반적인 스트림 연산도 모두 사용 가능
// 10개의 정수를 제곱한 값으로 리스트 생성
IntStream.range(1, 11).map(e -> e * e).boxed().collect(Collectors.toList());
자바 8에서 도입된 클래스로, 값이 존재하지 않을 수 있는 상황에서 기본값을 명시적으로 다루기 위해 사용한다.
empty
상태를 나타낸다.null
의 사용을 대체하고, 코드에서 명시적으로 값의 존재 여부를 다룰 수 있도록 도와준다.Java의 함수형 프로그래밍 개념 중 하나로 메소드를 가리키는 방식을 의미하며,
::
이라는 연산자를 사용해 표현한다.
메서드 참조는 람다 표현식을 간결하게 표현하고 코드의 가독성을 향상시킬 수 있다는 장점이 있다.
클래스이름::정적 메서드
형태로 표현//람다 표현식
(a, b) -> MyClass.staticMethod(a, b)
// 메소드 참조
MyClass::staticMethod
객체참조::인스턴스 메서드
형태로 표현한다.// 람다 표현식
(a, b) -> myObject.instanceMethod(a, b)
// 메소드 참조
myObject::instanceMethod
클래스이름:인스턴스 메서드
형태로 표현한다.// 람다 표현식
() -> "example".length()
// 메소드 참조
String::length
클래스이름:new
형태로 표현한다.// 람다 표현식
() -> new MyClass()
// 메소드 참조
MyClass::new
// 1.Thread클래스 확장
class MyThread extends Thread {
public void run() {
// 스레드가 실행할 코드 작성
}
}
// 1-1. 스레드 생성 및 시작
MyThread myThread = new MyThread();
myThread.start();
// 2.Runnable 인터페이스 구현
class MyRunnable implements Runnable {
public void run() {
// 스레드가 실행할 코드 작성
}
}
// 2-1. 스레드 생성 및 시작
MyRunnable myRunnable = new MyRunnable();
Thread myThread = new Thread(myRunnable);
myThread.start();
NEW(새로 생성됨) : 스레드 객체가 생성되었지만, 아직 start() 메소드
가 호출되지 않은 상태로, 즉 스레드가 아직 실행되기 전의 초기 상태이다.
RUNNABLE(실행 가능) : start() 메소드
가 호출되고, 스레드가 실행 대기 중인 상태이다.
RUNNING(실행중) : 스레드가 실제로 실행 중인 상태
BLOCKED(차단됨) : 스레드가 특정 자원을 얻지 못하고, 대기 상태에 있는 경우이다.
WAITING, TIMED_WAITING(대기 중, 제한된 기간 동안 대기 중) : - 스레드가 다른 스레드에 의해 깨워질 때까지 대기하고 있는 상태이다.
wait() 메소드
나 sleep() 메소드
호출 시 대기 중 상태가 된다.TIMED_WAITING
은 제한된 시간 동안 대기하고 있는 상태로, sleep() 메소드
에 인자를 전달하여 설정한다.TERMINATED / DEAD(종료됨) : 스레드의 run() 메소드
가 종료되었거나, stop() 메소드
에 의해 종료된 경우이다.
💡 Thread run과 start의 차이
start()
메서드 : 새로운 스레드를 생성해run()
메서드를 별도의 실행 경로에서 실행시킨다.run()
메서드 : 새로운 스레드의 생성 없이,run()
메서드는 현재 스레드에서run()
메서드로 호출한 코드가 실행된다.👉 따라서, 병렬 실행을 원한다면
start()
메서드를 사용하면 된다.
스레드 스케줄러에 의해 결정되는 값으로, 스래드의 실행 우선 순위를 나타낸다.
새로운 스레드가 생성되면 기본적으로 기본적으로 Thread.NORM_PRIORITY 값(5) 이 설정되며, 1~10까지의 정수 범위 내에서 우선순위를 표현할 수 있다.
z-index
처럼 값이 높을수록 높은 우선순의를 의미한다.
setPriority(int newPriority)
: Tread 클래스에서 제공하는 메서드로, 스레드의 우선순위를 결정한다.(1~10)⛔️Caution!
우선순위는 단순히 추천사항으로 이해해야 한다. Why? JAVA는 우선순위를 고려하긴 하지만, 반드시 지켜지지는 않기 때문이다.
👉 즉, 높은 우선순위를 가진 스레드일수록 더 많은 CPU 자원을 얻을 가능성이 있지만, 이 사실은 반드지 보장되는 것은 아니다.
특정 스레드가 끝날 때까지 대기하도록 하는 역할로, 주로 스레드의 실행 순서를 조절하고 스레드 간 동기화 작업을 할 때 활용된다.
ex)task1.join()
은 Task1 스레드 실행이 종료될 때까지 대기 후 다음 줄의 코드를 실행하게 된다.
join()
의 예외 처리는 throws InterruptedException
을 추가하거나, try-catch
블록으로 처리한다.
Tread.sleep(ms)
은 현재 실행 중인 스레드를 일정 시간동안 정지시킨다.Tread.sleep(3000);
// 해당 스레드가 3초동안 대기 상태에 들어가게 된다.
Thread.yield()
는 현재 실행 중인 스레드가 다른 스레드에게 CPU를 양보(yield)하도록 요청한다.Synchronized
로 묶인 메소드나 블록 내에서는 해당 코드를 실행하는 동안엔 다른 스레드가 접근하지 못하게 막는다.
- Overhead : 동기화된 코드는 하나의 스레드만이 실행하도록 보장하기 때문에 다른 스레드들이 해당 코드가 종료되길 기다려야 한다. 이로 인해 성능에 영향을 미칠 수 있다.
- 동기화 대안 : Java의 최근 버전에서
Synchronized
의 대안책으로concurrent collection
등 다양한 기능이 도입되었다.
- 이를 통해 스레드 간의 안전한 데이터 공유가 보다 효율적으로 이루어질 수 있다.
스레드는 현재 몇 개의 스레드가 실행 중인지 추적하기 어렵고, 특정 Task의 실행이 완료된 후에 다음 작업을 수행하는 것이 쉽지 않다는 한계가 존재한다.
이런 문제를 해결하기 위해 Executor Service
가 도입되었는데, 기존의 Tread 문제를 극복하고 고수준의 스레드 관리 기능을 제공한다.
최고👍