본문 바로가기

내배캠/TIL

Java 문법 종합반 4~5주

예외처리, 제네릭

오류(Error) vs 예외(Exception)

오류 (Error)

  • 회복 불가능
  • 시스템 레벨에서, 주로 환경적인 이유로 발생
  • 어떤 에러로 프로그램이 종료되었는지 확인하고 대응

예외(Exception)

  • 회복 가능
  • 그 예외가 발생할 수 있다는 것을 인지하고, 대응함
  • 코드 레벨에서 할 수 있는 문제 상황에 대한 대응은 여기에 속함

예외의 종류

컴파일 에러(예외)

  • .java 파일을 .class 파일로 컴파일할 때 발생하는 에러
  • 대부분 자바 프로그래밍 언어의 규칙을 지키지 않았기 때문에 발생

런타임 에러(예외)

  • 주로 다루게 될 에러(예외)
  • 문법적인 오류는 아니라 컴파일은 잘 되었지만 “프로그램”이 실행 도중 맞닥뜨리게 되는 예외

예외 발생과 try-catch, finally 문

예외 정의하기

class OurBadException extends Exception {
	public OurBadException() {
		super("위험한 행동을 하면 예외처리를 꼭 해야합니다!");
	}
}

class OurClass {
    private final Boolean just = true;
		
    public void thisMethodIsDangerous() throws OurBadException {
        if (just) {
            throw new OurBadException();
        }
    }
}
throws
throw
메서드 이름 뒤에 붙어 이 메서드가 어떠한 예외사항을 던질 수 있는지 알려주는 예약어
메서드 안에서, 실제로 예외 객체를 던질 때 사용하는 예약어
여러 종류의 예외사항을 적을 수 있다.
일반 메서드의 return 키워드처럼 throw 아래의 구문들은 실행되지 않고 종료된다.

예외 handling

public class StudyException {
    public static void main(String[] args) {
        OurClass ourClass = new OurClass();

        try {
            // 1. 위험한 메소드의 실행을 "시도"
            ourClass.thisMethodIsDangerous();
        } catch (OurBadException e) {
            // 2. 예외가 발생하면, "잡아서" handling
						// 즉 try 블럭 내의 구문을 실행하다가 예외가 발생하면
						// 예외가 발생한 줄에서 바로 코드 실행을 멈추고
						// 여기 있는 catch 블럭 내의 코드가 실행
            System.out.println(e.getMessage());
        } finally {
            // 3. 예외의 발생 여부와 상관없이 실행시켜야 하는 코드
            System.out.println("우리는 방금 예외를 handling 했습니다!");
        }
    }
}

Exception class 구조

연결된 예외 (Chained Exception)

  • 여러 가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해 사용
  • checked exception을 unchecked exception으로 포장(wrapping) 하는데 사용
  • initCause()
  • getCause()
// 연결된 예외 
public class main {

    public static void main(String[] args) {
        try {
            // 예외 생성
            NumberFormatException ex = new NumberFormatException("가짜 예외이유");

            // 원인 예외 설정(지정한 예외를 원인 예외로 등록)
            ex.initCause(new NullPointerException("진짜 예외이유"));

            // 예외 직접 던지기
            throw ex;
        } catch (NumberFormatException ex) {
            // 예외 로그 출력
            ex.printStackTrace();
            // 예외 원인 조회 후 출력
            ex.getCause().printStackTrace();
        }

        // checked exception 을 감싸서 unchecked exception 안에 넣기
        throw new RuntimeException(new Exception("이것이 진짜 예외 이유 입니다."));
    }
}

// 출력
Caused by: java.lang.NullPointerException: 진짜 예외이유

실제로 예외 처리하기

  1. 예외 복구하기
public String getDataFromAnotherServer(String dataPath) {
		try {
				return anotherServerClient.getData(dataPath).toString();
		} catch (GetDataException e) {
				return defaultData;
		}
}
  • try-catch로 예외를 처리하고 프로그램을 정상 상태로 복구하는 방법
  • 현실적으로 복구가 가능한 상황이 아닌 경우가 많거나 최소한의 대응만 가능한 경우가 많기 때문에 자주 사용되지는 않는다. 추천 X
  1. 예외 처리 회피하기
public void someMethod() throws Exception { ... }

public void someIrresponsibleMethod() throws Exception {
		this.someMethod();
}
  • someMethod()에서 발생한 에러가 someIrresponsibleMethod()의 throws를 통해서 그대로 다시 흘러나가게 된다. (예시 코드일 뿐 물론 같은 객체 내에서 이런 일은 안한다)
  • 관심사를 분리해서 한 레이어에서 처리하기 위해서 이렇게 에러를 회피해서 그대로 흘러 보내는 경우도 있다.
  1. 예외 전환하기
public void someMethod() throws IOException { ... }

public void someResponsibleMethod() throws MoreSpecificException {
		try {
			this.someMethod();
		} catch (IOException e) {
			throw new MoreSpecificException(e.getMessage());
		}
}
  • 예외 처리 회피하기의 방법과 비슷하지만, 조금 더 적절한 예외를 던져주는 경우
  • 보통은 예외 처리에 더 신경 쓰고 싶은 경우나, 오히려 RuntimeException처럼 일괄적으로 처리하기 편한 예외로 바꿔서 던지고 싶은 경우 사용

제네릭 (Generic)

  • 타입 언어에서 “중복되거나 필요 없는 코드”를 줄여준다. (타입이 다르므로 같은 로직 수행하는 메소드라도 오버로딩을 통해 여러 번 구현해줘야 함)
  • 타입 안정성을 해치지 않는다. (타입을 Object로 대신 작성해도 형변환, 연산자, 순서 등 내부에서 수 많은 부수적인 코드 구현이 필요해짐)
// 1. 제네릭은 클래스 또는 메서드에 사용 가능
// <> 안에 들어가야 할 타입 명시
public class Generic<T> {
		// 2. T 전부 String으로 바뀌어서 사용됨
    private T t;
    
    // 3. 리턴 타입 String
    public T get() {
        return this.t;
    }

    public void set(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
				// 4.
        Generic<String> stringGeneric = new Generic<>();
				// 5.
        stringGeneric.set("Hello World");
				
        String tValueTurnOutWithString = stringGeneric.get();

        System.out.println(tValueTurnOutWithString);
    }
}

문법

  • 제네릭 클래스 : 제네릭을 사용한 클래스
  • <> 사이에 들어가는 변수명 T : 타입 변수
  • Generic 클래스 : 원시 타입
  • 객체의 static 멤버에 사용할 수 없음.
static T get() { ... } // 에러

static void set(T t) { ... } // 에러

타입 변수는 인스턴스 변수로 간주됨.

  • 제네릭 배열을 생성할 수 없음.
  • 다수의 타입 변수 사용 가능
public class Generic<T, U, E> {
    public E multiTypeMethod(T t, U u) { ... }
}


Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);
  • 다형성 즉 상속과 타입의 관계는 그대로 적용
  • 와일드카드를 통해 제네릭의 제한을 구체적으로 정할 수 있음
public class ParkingLot<T extends Car> { ... }

ParkingLot<BMW> bmwParkingLot = new ParkingLot();
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // error!
  1. <? extends T> : T와 그 자손들만 사용 가능
  2. <? super T> : T와 그 조상들만 가능
  3. <?> : 제한 없음
  • 메서드를 스코프로 제네릭을 별도로 선언할 수 있음
public class Generic<T, U, E> {
		// Generic<T,U,E> 의 T와 아래의 T는 이름만 같을뿐 다른 변수
    static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
}
  1. 이렇게 반환 타입 앞에 <> 제네릭을 사용한 경우, 해당 메서드에만 적용되는 제네릭 타입 변수를 선언할 수 있다.
  2. 타입 변수를 클래스 내부의 인스턴스 변수로 취급하기 때문에 제네릭 클래스의 타입 변수를 static 메서드에는 사용할 수 없었지만, 제네릭 메소드의 제네릭 타입 변수는 해당 메소드에만 적용되기 때문에 메소드 하나를 기준으로 선언하고 사용할 수 있다.
  3. 같은 이름의 변수를 사용했다고 해도 제네릭 메소드의 타입 변수는 제네릭 클래스의 타입 변수와 다르다.

Collection

Collection을 선언할 때 사용한 <>도 제네릭임.

추상적인 타입인 인터페이스로 어떤 속성을 가져야 하는지 명세를 정리하고 이를 구현체들이 구현함.

  1. Collection(집합적 자료)라는 속성은 Iterable(순회 가능)이라는 속성을 상속받고 있음
  2. Collection의 하위 ‘속성’으로는 List, Queue, Set 등이 있음
  3. List의 실제 구현체들은 Arr, Linked, Vector, Stack 등등
  4. 상황에 맞는 적절한 자료구조를 택하는 방법

Wrapper 객체

  • 자바는 소수, 반올림, 정수와 같은 개념들을 추상화하여 객체화 해둠 (wrapper class)
  • 그러나 성능상의 이유로 “값”을 사용하는 이상의 의의를 가지지 않을 경우 그냥 원시 타입 사용
  • 객체로의 “기능”이 필요할 때 잠시 객체로 만들어 사용할 수도 있음
  • 기본값 → 객체화 : 박싱
  • 객체 → 기본값 : 언박싱
Integer num = new Integer(17);  // Boxing
int n = num.intValue(); // UnBoxing

Character ch = 'X'; // AutoBoxing
char c = ch; // AutoUnBoxing

 


 

쓰레드, 람다, 스트림, Optional

쓰레드

프로세스와 쓰레드

프로세스

  • 운영체제로부터 자원을 할당받는 작업의 단위
  • “실행 중인 프로그램”을 의미
  • 예를 들어 우리가 Java 프로그램을 실행시키면 이 프로그램은 프로세스라는 이름으로 운영체제 위에서 실행됨
  • 즉, OS 위에서 실행되는 모든 프로그램은 OS가 만들어준 프로세스에서 실행된다.
📌
OS가 프로그램 실행을 위한 프로세스를 할당해 줄 때 프로세스 안에 프로그램 Code와 Data 그리고 메모리 영역(Stack, Heap)을 함께 할당해 준다.
  1. Code는 Java main 메소드와 같은 코드를 말한다.
  2. Data는 프로그램이 실행 중 저장할 수 있는 저장 공간을 의미한다.
  3. Memory (메모리 영역)
📌
각 프로그램은 프로세스를 통해 Code, Data, Memory (Stack, Heap)를 OS로부터 할당받는다.

쓰레드

📌
쓰레드는 프로세스 내에서 일하는 일꾼(코드 실행의 흐름)
  • 프로세스가 작업 중인 프로그램에서 실행 요청이 들어오면 쓰레드(일꾼)을 만들어 명령을 처리하도록 한다.
  • 프로세스 안에는 여러 쓰레드들이 있고, 쓰레드들은 실행을 위한 프로세스 내 주소 공간이나 메모리 공간(Heap)을 공유 받는다.
  • 추가로, 쓰레드들은 각각 명령 처리를 위한 자신만의 메모리 공간(Stack)도 할당받는다.

자바 쓰레드

📌
일반 쓰레드와 동일하며 JVM 프로세스 안에서 실행되는 쓰레드를 말한다.
  • Java 프로그램을 실행하면 앞서 배운 JVM 프로세스 위에서 실행된다.
  • Java 프로그램 쓰레드는 Java Main 쓰레드부터 실행되며 JVM에 의해 실행된다.

멀티 쓰레드

  • Java는 메인 쓰레드가 main() 메서드를 실행시키면서 시작이 된다.
  • 메인 쓰레드는 필요에 따라서 작업 쓰레드들을 생성해서 병렬로 코드를 실행시킬 수 있다.
  • JVM의 메인 쓰레드가 종료되면, JVM도 같이 종료된다.
  • 하나의 프로세스는 여러 개의 실행 단위(쓰레드)를 가질 수 있으며 이 쓰레드들은 프로세스의 자원을 공유한다.
  • 멀티 쓰레드 장점
  • 멀티 쓰레드 단점

Thread와 Runnable

Thread

  • 자바에서 제공하는 Thread 클래스를 상속받아 쓰레드를 구현해준다.
public class Main {
    public static void main(String[] args) {
        TestThread thread = new TestThread();
        thread.start();
    }
}

class TestThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <100; i++) {
            System.out.print("*");
        }
    }
}

run() 메서드에 작성된 코드가 쓰레드가 수행할 작업이다.

Runnable

  • 자바에서 제공하는 Runnable 인터페이스를 사용하여 쓰레드를 구현해준다.
public class Main {
    public static void main(String[] args) {

        Runnable run = new TestRunnable();
        Thread thread = new Thread(run);

        thread.start();
    }
}

class TestRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <100; i++) {
            System.out.print("$");
        }
    }
}

마찬가지로 run() 메서드에 작성된 코드가 쓰레드가 수행할 작업이다.

Java는 다중 상속을 지원하지 않기 때문에 Thread를 상속받아 처리하는 방법은 확장성이 매우 떨어진다. 반대로 Runnable은 인터페이스이기 때문에 다른 필요한 클래스를 상속받을 수 있다. 따라서 확장성에 매우 유리하다.

대부분 람다식을 이용해 쓰레드를 구현한다.

Copy
public class Main {
    public static void main(String[] args) {
        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();
    }
}

데몬 쓰레드와 사용자 쓰레드

데몬 쓰레드

  • 보이지 않는 곳(background) 에서 실행되는 낮은 우선순위를 가진 쓰레드.
  • 보조적인 역할을 담당하며 대표적인 데몬 쓰레드로는 메모리 영역을 정리해 주는 가비지 컬렉터(GC)가 있다.
public class Main {
    public static void main(String[] args) {
        Runnable demon = () -> {
            for (int i = 0; i < 1000000; i++) {
                System.out.println("demon");
            }
        };

        Thread thread = new Thread(demon);
        thread.setDaemon(true); // true로 설정시 데몬스레드로 실행됨

        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("task");
        }
    }
}
  • demon 쓰레드는 우선순위가 낮고 다른 쓰레드가 모두 종료되면 강제 종료 당하기 때문에 main() 쓰레드의 task가 100번이 먼저 찍히면 종료되어 1000000번 수행이 되지 않고 종료됨.

사용자 쓰레드

  • 보이는 곳(foregorund)에서 실행되는 높은 우선순위를 가진 쓰레드
  • 프로그램 기능을 담당하며 대표적인 사용자 쓰레드로는 메인 쓰레드가 있다.
  • 디폴트 쓰레드.
⚠️
JVM 은 사용자 쓰레드의 작업이 끝나면 데몬 쓰레드도 자동으로 종료시켜 버린다.

쓰레드 우선순위와 쓰레드 그룹

쓰레드 우선순위

📌
쓰레드 작업의 중요도에 따라서 쓰레드의 우선순위를 부여할 수 있다.
  • 작업의 중요도가 높을 때 우선순위를 높게 지정하면 더 많은 작업시간을 부여받아 빠르게 처리될 수 있음.
  • 직접 정하기도, JVM에 의해 지정되기도 함.
  • JVM의 우선순위
public class Main {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.print("$");
            }
        };

        Runnable task2 = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.print("*");
            }
        };

        Thread thread1 = new Thread(task1);
        thread1.setPriority(8);   // 우선순위 설정 (8)
        int threadPriority = thread1.getPriority();   // 우선순위 반환
        System.out.println("threadPriority = " + threadPriority);

        Thread thread2 = new Thread(task2);
        thread2.setPriority(2);   // 우선순위 설정 (2)

        thread1.start();
        thread2.start();
        // 우선순위가 더 높은 thread1이 더 일찍 끝날 확률이 높음.
    }
}

쓰레드 그룹

📌
서로 관련이 있는 쓰레드들을 그룹으로 묶어서 다룰 수 있다.
  • 쓰레드들은 기본적으로 그룹에 포함되어 있다.
  • 메인 쓰레드는 system 그룹 하위에 있는 main 그룹에 포함.
  • 모든 쓰레드들은 반드시 하나의 그룹에 포함되어 있어야 한다.
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            while (!Thread.currentThread().isInterrupted()) {
                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();

    }
}

쓰레드 상태와 제어

쓰레드 상태

  • 쓰레드는 실행과 대기를 반복하며 run() 메서드를 수행
  • run() 메서드가 종료되면 실행이 멈추게 됨
  • 일시정지 상태에서는 쓰레드가 실행을 할 수 없는 상태가 됨
  • 쓰레드가 다시 실행 상태로 넘어가기 위해서는 우선 일시정지 상태에서 실행 대기 상태로 넘어가야 한다.
상태
Enum
설명
객체생성
NEW
쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태
실행대기
RUNNABLE
실행 상태로 언제든지 갈 수 있는 상태
일시정지
WAITING
다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태
일시정지
TIMED_WAITING
주어진 시간 동안 기다리는 상태
일시정지
BLOCKED
사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태
종료
TERMINATED
쓰레드의 작업이 종료된 상태

쓰레드 제어

  • sleep()
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
            // 특정 쓰레드 지목 불가
                Thread.sleep(2000);
                // TIMED_WAITING (주어진 시간동안만 기다리는 상태)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("task : " + Thread.currentThread().getName());
        };

        Thread thread = new Thread(task, "Thread");
        thread.start();   // NEW -> RUNNABLE

        try {
        // 1초가 지나면 RUNNABLE 상태로 변하여 다시 실행
        // 특정 쓰레드 지목해서 멈추게 하는 것은 불가능 (sleep은 static 메소드)
            thread.sleep(1000);
            System.out.println("sleep(1000) : " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 원래의 의도는 Thread라고 이름지어진 특정 쓰레드를 1초 잠재운 후 출력하려 했으나
        // main이 1초 잠들고 main 출력, 그리고 1초 후 (총 2초) Thread 출력됨.
    }
}
  • interrupt()
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println("task : " + Thread.currentThread().getName());
        };

        Thread thread = new Thread(task, "Thread");
        thread.start();

        thread.interrupt();

        System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
        
        // 바로 interrupt 되었기 때문에 1초 잠들지 않음
    }
}
  • join()
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                Thread.sleep(5000); // 5초
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(task, "thread");

        thread.start();

        long start = System.currentTimeMillis();

        try {
            thread.join();
            // join()하지 않으면 기다리지 않고 main 끝나면서 바로 끝나버림
            // join()해서 main이 기다려줌

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // thread의 소요시간인 5000ms 동안 main 쓰레드가 기다렸기 때문에 5000이상이 출력
        System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
    }
}
  • 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();
            }
        };

        Thread thread1 = new Thread(task, "thread1");
        Thread thread2 = new Thread(task, "thread2");

        thread1.start();
        thread2.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread1.interrupt();

    }
}

thread1과 thread2가 같이 1초에 한 번씩 출력되다가 5초 뒤에 thread1에서 InterruptedException이 발생하면서 Thread.yield(); 이 실행되어 thread1은 실행 대기 상태로 변경되면서 남은 시간은 thread2에게 리소스가 양보된다.

  • synchronized
📌
멀티 쓰레드의 경우 여러 쓰레드가 한 프로세스의 자원을 공유해서 작업하기 때문에 서로에게 영향을 줄 수 있다. 이로 인해서 장애나 버그가 발생할 수 있다.
  • 이러한 일을 방지하기 위해 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것을 '쓰레드 동기화(Synchronization)'라고 한다.
  • 동기화를 하려면 다른 쓰레드의 침범을 막아야 하는 코드들을 ‘임계 영역’으로 설정하면 된다.
  • 임계 영역에는 Lock을 가진 단 하나의 쓰레드만 출입이 가능하다.
  • 실행할 메서드 또는 실행할 코드 묶음 앞에 synchronized를 붙여서 임계 영역을 지정, 다른 쓰레드의 침범을 막을 수 있다. (침범을 막다 = Lock을 걸다)
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());
            }

        };

        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) {
        // lock걸고 싶은 코드 묶음 앞에 synchronized로 묶어줌.
        // 없다면 동시에 들어가 있지도 않은 사과 먹으면서 -1 -2 출력되는데
        // 동기화해준다면 한 스레드가 이 안에서 작업을 하고 있을 때 다른 스레드 접근 막음
        // 결국 사과 0개되면 if문 통과 못하면서 정상적으로 실행
            if(storedApple > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                storedApple -= 1;
            }
        }
    }
}
  • wait(), notify()
예제
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) {
                    int randomItem = (int) (Math.random() * MAX_ITEM);
                    appleStore.restock(itemList[randomItem]);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException ignored) {
                    }
                }
        };

        // 고객
        Runnable Customer = () -> {
                while (true) {
                    try {
                        Thread.sleep(77);
                    } catch (InterruptedException ignored) {
                    }

                    int randomItem = (int) (Math.random() * MAX_ITEM);
                    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(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
                    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!");
            try {
                wait(); // 재고가 없기 때문에 고객 대기중
                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!");
                wait();
                Thread.sleep(333);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
 
  • Lock, Condition (이해 x)

모던 자바 : 자바 8 변경점 알아보기

  1. 함수형 프로그래밍의 아이디어와 문법을 자바 8에서 지원한다.
  2. 함수형 프로그래밍의 아이디어인 (함수를 값으로 다루거나, 다른 함수에 넘길 수 있다)와 같은 일들이 가능하다.
  3. 함수형 프로그래밍의 문법인 익명 함수 문법을 지원한다.
  4. 스트림이라는 컬렉션의 흐름과 같은 것을 지원한다.
  5. 스트림 기능의 지원으로 우리는 더 간결하고, 유연하고, 성능 좋은 코드를 작성할 수 있다.

람다와 스트림

람다

예시 코드
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<>();

        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(parkCars(carsWantToPark, Car::hasTicket));
        parkingLot.addAll(parkCars(carsWantToPark, Car::noTicketButMoney));


        for (Car car : parkingLot) {
            System.out.println("Parked Car : " + car.getCompany() + "-" + car.getModel());
        }


    }

    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);
}
 
람다 익명 함수 예시
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<>();

        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(parkCars(carsWantToPark, Car::hasTicket));
        parkingLot.addAll(parkCars(carsWantToPark, Car::noTicketButMoney));

        weekendParkingLot.addAll(parkCars(carsWantToPark, (Car car) -> car.hasParkingTicket() && car.getParkingMoney() > 1000));


        for (Car car : parkingLot) {
            System.out.println("Parked Car : " + car.getCompany() + "-" + car.getModel());
        }


    }

    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);
}
 

Optional

  • Java8에서는 Optional<T> 클래스를 사용해 Null Pointer Exception을 방지할 수 있도록 도와줌.
  • Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스
Optional<String> carName = getCarNameFromDB();
// orElse()를 통해 값을 받아온다, 파라미터로는 null인 경우 반환할 값을 적는다.
String carName = getCarNameFromDB().orElse("NoCar");

// orElseGet()이라는 메서드를 사용해서 값을 받아올 수 있다.
// 파라미터로는 없는 경우 실행될 함수를 전달한다.
Car car = getCarNameFromDB().orElseGet(Car::new);

// 값이 없으면, 그 아래 로직을 수행하는데 큰 장애가 되는경우 에러를 발생시킬수도 있다.
Car car = getCarNameFromDB()
						.orElseThrow(() -> new CarNotFoundException("NO CAR!)

'내배캠 > TIL' 카테고리의 다른 글

24. 07. 26  (0) 2024.07.26
N의 약수 찾기 알고리즘  (0) 2024.07.25
Java 문법 종합반 1~3주  (6) 2024.07.23
SQL 정리 2  (7) 2024.07.22
SQL 정리 1  (1) 2024.07.22