프로세스 / 쓰레드
Code는 Java main 메소드와 같은 코드Data는 프로그램이 실행 중 저장 할 수 있는 저장공간 (전역변수, 정적변수(static), 배열 등 초기화된 데이터를 저장하는 공간)Memory(메모리 영역)new())주소공간이나 메모리공간(Heap)을 공유받는다.메모리공간(Stack)도 할당받는다.
Main 쓰레드부터 실행되며 JVM에 의해 실행된다.📌 Java는 메인 쓰레드가 main() 메서드를 실행시키면서 시작된다.
프로세스 안에서 하나의 쓰레드만 실행되는 것을 말한다.
main() 메서드(=메인 쓰레드)만 실행시켰을 때 이것을 싱글 쓰레드라고 한다.프로세스 안에서 여러 개의 쓰레드가 실행되는 것을 말한다.
▶︎ 멀티 쓰레드의 장점
▶︎ 멀티 쓰레드의 단점
교착 상태(데드락, Dead-Lock)이 발생할 수 있다.Java에서 제공하는 Thread 클래스를 상속받아 쓰레드를 구현한다.
▶︎ TestThread.java
package week05.thread;
/**
* 1. Thread Class를 이용하는 것(상속)
*/
public class TestThread extends Thread {
@Override
public void run() {
// 실제 우리가 쓰레드에서 수행할 작업
System.out.println("테스트입니다!!");
for (int i = 0; i < 100; i++) {
System.out.print("*");
}
}
}
Java에서 제공하는 Runnable 인터페이스를 사용하여 쓰레드를 구현한다.
▶︎ TestRunnable.java
package week05.thread;
/**
* 2. Runnable interface를 구현
*/
public class TestRunnable implements Runnable {
@Override
public void run() {
// 쓰레드에서 수행할 작업 정의!
for (int i = 0; i < 100; i++) {
System.out.print("$");
}
}
}
🤔 Thread 보다 Runnable 인터페이스를 이용하여 쓰레드를 구현하는 이유?
클래스와 인터페이스의 차이 (다중상속을 지원하지 않음)
Thread를 상속받아 처리하는 방법은 확장성이 매우 떨어진다. 반대로 Runnable은 인터페이스이기 때문에 다른 필요한 클래스를 상속받을 수 있다.
→ Runnable이 Thread보다 확장성에 유리하다.
▶︎ Main.java
package week05.thread;
public class Main {
public static void main(String[] args) {
// 1. Thread Class 를 상속 -> run() 오버라이딩
TestThread threadExtends = new TestThread();
threadExtends.start(); // run()에 작성한 작업을 start()로 실행시킬 수 있다.
// 2. Runnable 인터페이스 implements -> run() 구현
Runnable run = new TestRunnable();
Thread threadRunnable = new Thread(run);
threadRunnable.start();
// 3. task 익명함수 객체 생성 (lambda)
Runnable task = () -> {
int sum = 0;
for (int i=0; i<50; i++) {
sum += i;
System.out.println(sum);
}
System.out.println(Thread.currentThread().getName() + " 최종 합 : " + sum);
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task);
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
package week05.thread.single;
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("2번 => " + Thread.currentThread().getName());
for (int i = 0; i < 100; i++) {
System.out.print("$");
}
};
System.out.println("1번 => " + Thread.currentThread().getName());
Thread thread1 = new Thread(task);
thread1.setName("thread1");
thread1.start();
}
}

package week05.thread.multi;
public class Main {
public static void main(String[] args) {
// 걸리는 시간이나, 동작을 예측할 수 없다.
// 1st
Runnable task = () -> {
System.out.println("실행: " + Thread.currentThread().getName());
for (int i=0; i<100; i++) {
System.out.print("$");
}
};
// 2nd
Runnable task2 = () -> {
System.out.println("실행: " + Thread.currentThread().getName());
for (int i=0; i<100; i++) {
System.out.print("*");
}
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task2);
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}

보이지 않는 곳(background)에서 실행되는 낮은 우선순위를 가진 쓰레드를 말한다.
▶︎ 데몬 쓰레드 설정 방법
package week05.thread.daemon;
public class Main {
public static void main(String[] args) {
Runnable daemon = () -> {
for (int i = 0; i < 1000000; i++) {
System.out.println(i+"번째 daemon");
}
};
// 우선순위가 낮다 => 상대적으로 다른 쓰레드에 비해 리소스를 적게 할당받는다.
Thread thread = new Thread(daemon);
thread.setDaemon(true); // 쓰레드를 데몬 쓰레드로 설정함
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println(i + "번째 task");
}
}
}

보이는 곳(foreground)에서 실행되는 높은 우선순위를 가진 쓰레드
⚠️ JVM은 사용자 쓰레드의 작업이 끝나면 데몬 쓰레드도 자동으로 종료시켜 버린다.
📌 쓰레드 작업의 중요도에 따라서 쓰레드의 우선순위를 부여할 수 있다.
최대 우선순위(MAX_PRIORITY) = 10최소 우선순위(MIN_PRIORITY) = 1보통 우선순위(NROM_PRIORITY) = 5setPriority() 메서드로 설정할 수 있다.▶︎ 쓰레드의 우선순위 실습
package week05.thread.priority;
public class Main {
public static void main(String[] args) {
Runnable task1 = () -> {
for (int i=0; i<100; i++) {
System.out.print("$");
}
System.out.println();
System.out.println("task1 끝!");
};
Runnable task2 = () -> {
for (int i=0; i<100; i++) {
System.out.print("*");
}
System.out.println();
System.out.println("task2 끝!");
};
Thread thread1 = new Thread(task1);
thread1.setPriority(8);
int thread1Priority = thread1.getPriority();
System.out.println("thread1 priority: " + thread1Priority);
Thread thread2 = new Thread(task2);
thread2.setPriority(2);
int thread2Priority = thread2.getPriority();
System.out.println("thread2 priority: " + thread2Priority);
thread1.start(); // 작업시간을 더 많이 할당받아 더 빨리 끝날 가능성이 높다.
thread2.start();
}
}

→ 실제로는 OS의 스케줄러에 의한 변수가 많기 때문에 무조건 우선순위가 높은 작업이 먼저 끝난다고 보장할 수 없다.
서로 관련이 있는 쓰레드들을 그룹으로 묶어서 다룰 수 있다.

package week05.thread.group;
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
// main의 interrupt()가 호출되기 전까지 계속 수행
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
break;
}
}
System.out.println(Thread.currentThread().getName() + " Interrupted");
};
// ThreadGroup 클래스로 객체를 만듦
ThreadGroup group1 = new ThreadGroup("Group1");
// Thread 객체 생성 시 첫번째 매개변수로 넣어준다.
// Thread(ThreadGroup group, Runnable target, String name);
Thread thread1 = new Thread(group1, task, "Thread 1");
Thread thread2 = new Thread(group1, task, "Thread 2");
// Thread가 ThreadGroup 에 할당된 것을 확인할 수 있다.
System.out.println("Group of Thread1 : " + thread1.getThreadGroup().getName());
System.out.println("Group of Thread2 : " + thread2.getThreadGroup().getName());
thread1.start();
thread2.start();
try {
// 현재 쓰레드를 지정된 시간동안 멈추게 합니다.
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만듭니다.
group1.interrupt();
}
}

쓰레드는 상태가 존재하고 이를 제어할 수 있다.


▶︎ 쓰레드의 상태
| 상태 | Enum | 설명 |
|---|---|---|
| 객체생성 | NEW | 쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태 |
| 실행대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
| 일시정지 | WAITING | 다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태 |
| 일시정지 | TIMED_WAITING | 주어진 시간 동안 기다리는 상태 |
| 일시정지 | BLOCKED | 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태 |
| 종료 | TERMINATED | 쓰레드의 작업이 종료된 상태 |
▶︎ 쓰레드 제어

▶︎ sleep
sleep(): 현재 쓰레드를 지정된 시간동안 멈추게 한다.
package week05.thread.stat.sleep;
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
// (1) 예외처리 필수
// - interrupt()를 만나면 다시 실행되기 때문에
// - InterruptException이 발생할 수 있다. -> try-catch로 예외처리 반드시 필요
// (2) 특정 쓰레드 지목 불가 : sleep() -> static method
Thread.sleep(2000); // TIMED_WATING(주어진 시간동안만 기다리는 상태)
// 객체.메서드();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task : " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "Thread"); // NEW
thread.start(); // NEW -> RUNNABLE
try {
// 1초가 지나고 나면 runnable 상태로 변하여 다시 실행된다.
// 특정 스레드를 지정해서 멈추게 하는 것은 불가능하다.
// Static member 'java.lang.Thread.sleep(long)' accessed via instance reference
thread.sleep(1000); // -> 인스턴스에서 static 메서드를 호출하고 있음 : 의미없다.
// Thread.sleep(1000); // sleep을 통해서 메서드를 정지시킬 땐 특정 쓰레드를 지정할 수 없다.
System.out.println("sleep(1000) : " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
▶︎ interrupt
interrupt(): 일시정시 상태인 쓰레드를 실행대기 상태로 만든다.
package week05.thread.stat.interrupt;
// - 쓰레드가 'start()' 된 후 동작하다 'interrupt()'를 만나 실행하면 interrupted 상태가 true가 된다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
// sleep 도중 interrupt 발생 시, catch!
Thread.sleep(1000);
System.out.println("task - try 안 : " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task - try 밖 : " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "Thread"); // New
thread.start(); // Runnable
thread.interrupt(); // sleep -> interrupt
System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
}
}

thread.isInterrupted() : 현재 쓰레드가 interrupted 상태인지 여부 (true: interrupt 상태)public static void main(String[] args) {
Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// sleep 도중 interrupt 발생 시, catch!
Thread.sleep(1000);
System.out.println("task - try 안 : " + Thread.currentThread().getName());
} catch (InterruptedException e) {
break;
}
}
System.out.println("task - try 밖 : " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "Thread"); // New
thread.start(); // Runnable
thread.interrupt(); // sleep -> interrupt
System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
}
▶︎ join
join(): 정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다린다.
package week05.thread.stat.join;
// 정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다립니다.
// - 시간을 지정하지 않았을 때는 지정한 쓰레드가 작업이 끝날 때 까지 기다립니다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task, "thread"); // NEW
thread.start(); // NEW -> RUNNABLE
long start = System.currentTimeMillis();
try {
// 시간을 지정하지 않았기 때문에 thread가 작업을 끝낼 때까지 main 쓰레드는 기다리게 됩니다.
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// thread의 소요시간이 5000ms 동안 main 쓰레드가 기다리기 때문에 5000 이상이 출력된다.
System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
}
}

▶︎ yield
yield(): 남은 시간을 다음 쓰레드에게 양보하고 쓰레드 자신은 실행 대기 상태가 된다.
✏️ yield: "넘겨주다", "양보하다"라는 뜻을 가지고 있다.
package week05.thread.stat.yield;
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Thread.yield();
// e.printStackTrace();
}
};
Thread thread1 = new Thread(task, "thread1"); // NEW
Thread thread2 = new Thread(task, "thread2"); // NEW
thread1.start(); // NEW -> RUNNABLE
thread2.start(); // NEW -> RUNNABLE
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
}
interrupt()를 발생시키기 전(5초)까지 thread1, thread2에서 1초마다 Thread.currentThread().getName() 현재 쓰레드의 이름을 출력, interrupt 발생 후 thread1은 InterruptException 예외로 catch되어 yield()를 통해 가지고 있던 리소스(공유하는 프로세스 자원)을 모두 thread2에 양보한다.▶︎ synchronized
멀티 쓰레드의 경우 여러 쓰레드가 한 프로세스의 자원을 공유해서 작업하기 때문에 서로에게 영향을 줄 수 있다. → 이로 인한 장애나 버그가 발생할 수 있다. (교착상태)
쓰레드 동기화(Synchronized)라고 한다.synchronized를 사용한 동기화package week05.thread.stat.sync;
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
// 3개의 thread를 한꺼번에 만들어서 start()를 해버림
// 생성(NEW)과 동시에 start(NEW -> RUNNABLE)
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
synchronized (this) {
if(storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
}

침범을 막은 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait()을 호출하여 쓰레드가 Lock을 반납하고 기다리게 할 수 있다.
notify()를 호출해서,▶︎ wait()
실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다린다.
▶︎ notify()
해당 객체의 대기실(waiting pool)에 있는 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받는다.
→ 특정 쓰레드를 집어서 통지할 수 없다.
package week05.thread.stat.waitnotify;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Main {
public static String[] itemList = {
"MacBook", "IPhone", "AirPods", "iMac", "Mac mini"
};
public static AppleStore appleStore = new AppleStore();
public static final int MAX_ITEM = 5;
public static void main(String[] args) {
// 가게 점원
Runnable StoreClerk = () -> {
while (true) {
// 0부터 4사이의 정수 중, Random한 값 뽑아내기 위함
int randomItem = (int) (Math.random() * MAX_ITEM);
// restock : 재고를 넣는 메서드
appleStore.restock(itemList[randomItem]);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
};
// 고객
Runnable Customer = () -> {
while (true) {
try {
Thread.sleep(77);
} catch (InterruptedException e) {
}
int randomItem = (int) (Math.random() * MAX_ITEM);
// sale : 판매하는 메서드
appleStore.sale(itemList[randomItem]);
System.out.println(Thread.currentThread().getName() + " Purchase Item " + itemList[randomItem]);
}
};
new Thread(StoreClerk, "StoreClerk").start();
new Thread(Customer, "Customer1").start();
new Thread(Customer, "Customer2").start();
}
}
class AppleStore {
private List<String> inventory = new ArrayList<>();
// 재입고
public void restock(String item) {
synchronized (this) {
while (inventory.size() >= Main.MAX_ITEM) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
// restock()이 wating pool로 들어감.. 물건이 판매되는 순간 sale() -> notify();
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 재입고
inventory.add(item);
notify(); // 재입고 되었음을 고객에게 알려주기
System.out.println("Inventory 현황: " + inventory.toString());
}
}
public synchronized void sale(String itemName) {
while (inventory.size() == 0) {
System.out.println(Thread.currentThread().getName() + " Waiting 1! itemName: " + itemName);
try {
wait(); // 재고가 없기 때문에 고객 대기중
// sale() 이 wating pool로 들어감.. 재입고 되는 순간 restock() -> notify();
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while(true) {
// 고객이 주문한 제품이 있는지 확인
for(int i = 0; i < inventory.size(); i++) {
if (itemName.equals(inventory.get(i))) {
inventory.remove(itemName);
notify(); // 제품 하나 팔렸으니 재입고 하라고 알려주기
return; // 메서드 종료
}
}
// 고객이 찾는 제품이 없을 경우
try {
System.out.println(Thread.currentThread().getName() + " Waiting 2! itemName: " + itemName);
wait();
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


▶︎ Lock
synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만, 같은 메서드 내에서만 Lock을 걸 수 있다는 제약이 있다. 이런 제약을 해결하기 위해 Lock 클래스를 사용한다.
ReenterantLock public class MyClass {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
methodB();
}
}
public void methodB() {
synchronized (lock2) {
// do something
methodA();
}
}
}
methodA에서 이미 lock1을 가지고 있으므로 lock2를 기다리는 상태가 되어 데드락이 발생할 가능성이 있다.
ReentrantLock을 사용하면 코드의 유연성을 높일 수 있다.ReenterantReadWriteLock
StampedLock
ReenterantReadWriteLock에 낙관적인 Lock의 기능을 추가▶︎ Condition
wait() & notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못한다는 것을 해결한 것이 Condition이다.
📌 wait()과 notify()는 객체에 대한 모니터링 락(lock)을 이용하여 스레드를 대기시키고 깨웁니다. 그러나 wait()과 notify()는 waiting pool 내에 대기 중인 스레드를 구분하지 못하므로, 특정 조건을 만족하는 스레드만 깨우기가 어렵습니다.
이러한 문제를 해결하기 위해 JDK 5에서는 java.util.concurrent.locks 패키지에서 Condition 인터페이스를 제공합니다. Condition은 waiting pool 내의 스레드를 분리하여 특정 조건이 만족될 때만 깨우도록 할 수 있으며, ReentrantLock 클래스와 함께 사용됩니다. 따라서 Condition을 사용하면 wait()과 notify()의 문제점을 보완할 수 있습니다.
wait() & notify() 대신 Condition의 await() & signal() 을 사용합니다.
package week05.thread.stat.condition;
import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static final int MAX_TASK = 5;
private ReentrantLock lock = new ReentrantLock();
// lock으로 condition 생성
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private ArrayList<String> tasks = new ArrayList<>();
public void addMethod(String task) {
lock.lock(); // 임계영역 시작
try {
while(tasks.size() >= MAX_TASK) {
String name = Thread.currentThread().getName();
System.out.println(name + " is waiting.");
try {
condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
Thread.sleep(500);
} catch (InterruptedException e) {}
}
tasks.add(task);
condition2.signal(); // notify(); 기다리고 있는 condition2를 깨워준다.
System.out.println("Tasks: " + tasks.toString());
} finally {
lock.unlock(); // 임계영역 끝
}
}
}
👉 정리하면, wait() 작업에 이름을 부여하여(=condition.await()) 특정 조건이 충족되면 직접 해당 작업을 지정하여 깨울 수 있다 notify() (=condition.signal())
📌 자바는 진화하는 언어이며, 가장 큰 진화는 Java 8 에서 이루어졌다.
▶︎ 함수형 프로그래밍
함수형 프로그래밍은 순수한 함수의 모음으로 바라보고 구현한다.
▶︎ Java 8에서 추가된 개념들
함수=객체(변수))람다(lambda): 익명 함수package week05.stream;
import java.util.ArrayList;
import java.util.List;
// 주차장 예제
// 티켓, 파킹머니가 있는 차량 -> 주차 가능
public class LambdaAndStream {
public static void main(String[] args) {
// 주차대상 차량
ArrayList<Car> carsWantToPark = new ArrayList<>();
// 주차장
ArrayList<Car> parkingLot = new ArrayList<>();
// 주말 주차장
ArrayList<Car> weekendParkingLot = new ArrayList<>();
// 5개의 car 인스턴스 생성
Car car1 = new Car("Benz", "Class E", true, 0);
Car car2 = new Car("BMW", "Series 7", false, 100);
Car car3 = new Car("BMW", "X9", false, 0);
Car car4 = new Car("Audi", "A7", true, 0);
Car car5 = new Car("Hyundai", "Ionic 6", false, 10000);
carsWantToPark.add(car1);
carsWantToPark.add(car2);
carsWantToPark.add(car3);
carsWantToPark.add(car4);
carsWantToPark.add(car5);
// parkingLot.addAll(parkingCarWithTicket(carsWantToPark));
parkingLot.addAll(parkCars(carsWantToPark, Car::hasTicket));
// parkingLot.addAll(parkingCarWithMoney(carsWantToPark));
parkingLot.addAll(parkCars(carsWantToPark, Car::noTicketButMoney));
// 익명함수 적용
parkingLot.addAll(parkCars(carsWantToPark, (Car car) -> car.hasParkingTicket() && car.getParkingMoney() > 1000));
for (Car car : parkingLot) {
System.out.println("Parked Car : " + car.getCompany() + "-" + car.getModel());
}
}
// 타입 -> (함수형) 인터페이스
// 인터페이스는 타입 역할을 할 수 있기 때문
// 함수형 인터페이스: 추상 메서드를 딱 하나만 가지고 있음
// public exampleMethod(int parameter1, ? parameterFunction) {
// parameterFunction~~;
// }
/*
private static List<Car> parkingCarWithTicket(List<Car> carsWantToPark) {
ArrayList<Car> cars = new ArrayList<>();
for (Car car : carsWantToPark) {
if (car.hasParkingTicket()) {
cars.add(car);
}
}
return cars;
}
private static List<Car> parkingCarWithMoney(List<Car> carsWantToPark) {
ArrayList<Car> cars = new ArrayList<>();
for (Car car : carsWantToPark) {
if (!car.hasParkingTicket() && car.getParkingMoney() > 1000) {
cars.add(car);
}
}
return cars;
}
*/
// 위의 두 메소드를 하나로 : 내부 주요 로직을 함수로 전달받자
public static List<Car> parkCars(List<Car> carsWantToPark, Predicate<Car> function) {
List<Car> cars = new ArrayList<>();
for (Car car : carsWantToPark) {
// 전달될 함수를 사용하여 구현
if (function.test(car)) {
cars.add(car);
}
}
return cars;
}
}
class Car {
private final String company; // 자동차 회사
private final String model; // 자동차 모델
private final boolean hasParkingTicket;
private final int parkingMoney;
public Car(String company, String model, boolean hasParkingTicket, int parkingMoney) {
this.company = company;
this.model = model;
this.hasParkingTicket = hasParkingTicket;
this.parkingMoney = parkingMoney;
}
public String getCompany() {
return company;
}
public String getModel() {
return model;
}
public boolean hasParkingTicket() {
return hasParkingTicket;
}
public int getParkingMoney() {
return parkingMoney;
}
public static boolean hasTicket(Car car) {
return car.hasParkingTicket;
}
public static boolean noTicketButMoney(Car car) {
return !car.hasParkingTicket && car.getParkingMoney() > 1000;
}
}
interface Predicate<T> {
boolean test(T t);
}
▶︎ 스트림 → map, filter
스트림은 데이터 처리연산을 지원하도록 소스에서 추출된 연속된 요소📌 자료구조(리스트, 맵, 셋 등)의 흐름을 객체로 제공해주고, 그 흐름동안 사용할 수 있는 메서드들을 api로 제공해주는 것
▶︎ 스트림의 특징

Collection 클래스 내부 stream(): 모든 컬렉션을 상속하는 구현체들은 스트림을 반환할 수 있다.

스트림 받아오기 → 스트림 가공하기 → 스트림 결과 만들기
▶︎ 스트림 API
map(), forEach(), filter()
런타임 중 NullPointerException 발생
public class NullIsDanger {
public static void main(String[] args) {
SomeDBClient myDB = new SomeDBClient();
String userId = myDB.findUserIdByUsername("HelloWorldMan");
System.out.println("HelloWorldMan's user Id is : " + userId);
}
}
class SomeDBClient {
public String findUserIdByUsername(String username) {
// ... db에서 찾아오는 로직
String data = "DB Connection Result";
if (data != null) {
return data;
} else {
return null;
}
}
}
nullPointerException이 발생하게 된다.리턴값을 객체로 감싸서 NullPointerException을 방지하자 → 이것을 발전시켜 자바에서 제공하는 것이 java.util.Optional 객체이다.
▶︎ Optional 사용법
Optional<Car> emptyOptional = Optional.empty();
Optional<Car> hasDataOptional = Optional.of(new Car());
Optional<Car> hasDataOptional = Optional.ofNullable(getCarFromDB());
Optional<String> carName = getCarNameFromDB();
// orElse()를 통해 값을 받아온다, 파라미터로는 null인 경우 반환할 값을 적는다.
String realCarName = carName.orElse("NoCar");
// 위는 예시, 실제로 사용하는 방법
String carName = getCarNameFromDB().orElse("NoCar");
// orElseGet()이라는 메서드를 사용해서 값을 받아올 수 있습니다.
// 파라미터로는 없는 경우 실행될 함수를 전달합니다.
Car car = getCarNameFromDB().orElseGet(Car::new);
// 값이 없으면, 그 아래 로직을 수행하는데 큰 장애가 되는경우 예외를 발생시킬수도 있습니다.
Car car = getCarNameFromDB()
.orElseThrow(() -> new CarNotFoundException("NO CAR!")