더 자바, Java 8 - 002

12 분 소요

https://www.inflearn.com/course/the-java-java8/dashboard

Optional

비어있을 수도 있고 값이 있을 수도 있는 상황

java 에서 종종 만나는 NullPointerException

  • null 체크를 빼먹지 않고 개발하는 것은 힘들다.
  • 또한 null 을 리턴하는 것 자체가 문제일 수도 있다.

아래는 개발중 값을 제대로 리턴할 수 없으면 선택하는 방법이다.

  • 예외를 쓰는 것
    • 비용이 비싸다.
  • null 을 리턴하는 것
    • 비용은 문제없다.
    • 클라이언트 코드에서 알아서 사용해야 한다.
  • Optional 을 리턴하는 것
    • 명시적으로 빈 값이 올 수도 있음을 표현한다.
    • 클라이언트에서 빈 값인 경우에 대한 처리를 수행하도록 강제한다.

주의사항

  • 리턴값으로만 쓰기를 권장함
    • 파라미터로 Optional 을 쓰면?
      • 있으면 메소드를 수행하고, 없으면 수행안하고 해야하나?
    • 맵의 키 타입
      • key 는 비어있을 수 없다.
    • 인스턴스 필드의 타입
      • 상태자체가 있을 수도 있고 없을 수도 있다?
  • Optional 을 리턴하는 메소드에서 null 을 리턴하지 말 것
    • Optional.empty(); 로 리턴
  • primitive 타입용 optional 이 따로 있다.
    • OptionalInt, OptionalLong
  • Collection, Map, Stream Array, Optional 은 Optional 로 감싸지 말 것
    • 자체에 비어있음을 판단할 수 있는 컨테이너 성격의 타입들

참고

Optional API

isPresent()

Optional<OnlineClass> spring = springClasses.stream()
        .filter(oc -> oc.getTitle().startsWith("spring"))
        .findFirst();
        
boolean present = spring.isPresent(); // true

isEmpty(Java 11 부터)

get()

optional 에서 값을 꺼내기

값이 없는데 get 을 하면 NoSuchElementException

그럴 때는 ifPresent 로 진행

Optional<OnlineClass> spring = springClasses.stream()
        .filter(oc -> oc.getTitle().startsWith("AAA"))
        .findFirst();
        
OnlineClass optional = spring.get(); // 없으면 NoSuchElementException

// 권장되는 방법
optional.ifPresent(oc -> {
	System.out.println(oc.getTitle());
});

orElse()

optional 에 값이 없을 때 새로 만들기

// 없을 때 새로 만들기
OnlineClass onlineClass = optional.orElse(createNewOnlineClass());

인거 같았는데, 값이 있어도 createNewOnlineClass() 로 생성

orElseGet()

optional 에 값이 null 일 때 새로 만들기

// null 일 때 새로 만들기
OnlineClass onlineClass = 
    optional.orElseGet(App::createNewOnlineClass());

참고

orElseThrow()

optional 에 값이 없을 때 throw

// 없을 때 예외
OnlineClass onelineClass = 
    optional.orElseThrow(IllegalStateException::new);

filter()

Optional 에 조건을 넣어서 조건이 맞으면 Optional 반환

// 필터링
Optional<OnlineClass> onlineClass = 
    optional.filter(oc -> !oc.isClosed());

map()

optional 을 변환

Optional<Integer> integer = optional.map(OnlineClass::getId);
System.out.println(integer.isPresent());

flatMap()

optional 안의 인스턴스가 optional 인 경우에 사용

참고

Data 와 Time API

지금까지 사용된 java.util.Date 클래스는 mutable 해서 thread safe 하지가 않았다.

그래서 Java 8 부터는 새로운 Date-Time API 를 생성함

디자인 철학

  • Clear
  • Fluent
  • Immutable
  • Extensible

Date/Time API

기계용 시간 (Epoch Time)

Instant now = Instant.now();
System.out.println(now); // 현재시간의 UTC 를 리턴
System.out.println(now.atZone(ZoneId.of("UTC")));

ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());
System.out.println(zonedDateTime); // 시스템의 타임존 시간을 리턴

ZonedDateTime zoneddateTimeOfUTC = now.atZone(ZoneId.of("UTC"));
System.out.println(zoneddateTimeOfUTC); // UTC 타임존 시간을 리턴

인간용 시간

  • LocalDateTime.now(): 현재 시스템 Zone에 해당하는(로컬) 일시를 리턴한다.
  • LocalDateTime.of(int, Month, int, int, int, int): 로컬의 특정 일시를 리턴한다.
  • ZonedDateTime.of(int, Month, int, int, int, int, ZoneId): 특정 Zone의 특정 일시를 리턴한다.

기간 표현하기

Period

LocalDate today = LocalDate.now();
LocalDate dDay = LocalDate.of(2023, Month.JULY, 1);
Period between = Period.between(today, dDay);
System.out.println(between.getDays()); // 일수

Period until = today.until(dDay); // 일수
System.out.println(between.get(ChronoUnit.DAYS)); // 일수

Duration 은 기계용 시간 비교 시 사용

포매팅

LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/d/yyyy");

System.out.println(today.format(formatter));

LocalDate date = LocalDate.parse("07/15/1982", formatter);
System.out.println(date);

레거시에서 변환

GregorianCalendar와 Date 타입의 인스턴스를 Instant나 ZonedDateTime으로 변환 가능

Date date = new Date();
Instant instant = date.toInstant();
Date newDate = Date.from(instant);

GregorianCalendar gregorianCalendar = new GregorianCalendar();
ZonedDateTime dateTime = gregorianCalendar.toInstant().atZone(ZoneId.systemDefault());
GregorianCalendar from = GregorianCalendar.from(dateTime);

ZoneId zoneId = TimeZone.getTimeZone("PST").toZoneId();
TimeZone timeZone = TimeZone.getTimeZone(zoneld);

참고

CompletableFuture

Concurrent 소프트웨어란 동시에 여러 작업을 할 수 있는 소프트웨어를 말한다.

자바에서 지원하는 Concurrent 프로그래밍은 멀티 프로세싱과 멀티 쓰레드

자바 멀티쓰레드 프로그래밍

Thread 클래스를 상속받아 만드는 방법

public static void main(String[] args) {
    HelloThread helloThread = new HelloThread();
    helloThread.start();
    System.out.println("hello : " + Thread.currentThread().getName());
}

static class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("world : " + Thread.currentThread().getName());
    }
}

Runnable 인터페이스를 구현하는 방법

// Runnable 인터페이스의 람다식
Thread thread = new Thread(() -> {
    System.out.println("world : " + Thread.currentThread().getName());
});
thread.start();
System.out.println("hello : " + Thread.currentThread().getName());

쓰레드의 주요 기능

  • sleep():
    • 현재 쓰레드 멈춰두기
    • 다른 쓰레드가 처리할 수 있도록 기회를 주지만 그렇다고 락을 놔주진 않는다. (잘못하면 데드락 걸릴 수 있겠죠.)
  • interupt():
    • 다른 쓰레드 깨우기
    • 다른 쓰레드를 깨워서 interruptedExeption을 발생
    • 그 에러가 발생했을 때 할 일은 코딩하기 나름. 종료 시킬 수도 있고 계속 하던 일 할 수도 있고.
  • join():
    • 다른 쓰레드 기다리기
    • 다른 쓰레드가 끝날 때까지 기다린다.

참고

이렇게 쓰레드이 기능이 너무 어렵기 때문에 Executors 가 나오게 되었음.

Executors

High-Level Concurrency 프로그래밍을 위해 사용하는 인터페이스

쓰레드를 만들고 관리하는 작업을 애플리케이션에서 분리해서 위임

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
    System.out.println("Hello :" + Thread.currentThread().getName());
});



// 처리중인 작업 기다렸다가 종료
executorService.shutdown(); 

// 당장 종료
executorService.shutdownNow(); 

ScheduledExecutorService 도 있음

고정된 쓰레드풀에서 여러개의 쓰레드 만들려면 newFixedThreadPool

ExecutorService executorService = Executors.newFixedThreadPool(2);

ExecutorService 내부에 Blocking Queue 가 있다.

Blocking Queue 에 작업을 쌓아두고 Thread 에 작업을진행함

Runnable 은 리턴값이 없어서 작업의 결과를 받아올 수 없다.

그래서 리턴값이 필요할 때는 Callable 을 사용한다.

참고

Callable 과 Future

ExecutorService 를 이용해서 Callable 을 수행할 수 있다.

ExecutorService executorService = Executors.newSingleThreadExecutor();

Callable<String> hello = new Callable<String>() {
	@Override
    public String call() throws Execption {
    	return "Hello";
    }
};

// Funcional Interface
// Callable<String> hello = () -> "Hello";

Future<String> helloFuture = executorService.submit(hello);

helloFuture.isDone();

helloFuture.get();

isDone() 메소드로 동작 상태를 알 수 있다. true / false

cancel() 메소드로 작업을 취소할 수 있다.

get() 메소드로 결과를 받아올 수 있다. get() 메소드는 blocking call 이므로 대기시간이 발생한다.

invokeAll() 메소드는 모든 쓰레드의 작업이 끝난 후 이후 작업을 수행한다.

ExecutorService executorService = Executors.newFixedThreadPool(4);

List<Future<String>> futures = executorService.invokeAll(Arrays.asList(/* 여러 쓰레드 */));

for (Future<String> f : futures) {
	System.out.println(f.get());
}

invokeAny() 메소드는 쓰레드 중 작업이 빠르게 끝난 Future 가 있으면 먼저 값을 반환하고, 작업을 수행한다.

참고

CompletableFuture

Future 는 Future 의 get() 메소드를 호출하기 전에는 다른 작업을 수행할 수 없는 문제가 있다.

Future<String> future = executorServicei.submit(() -> "hello");

future.get(); // future.get() 을 호출하기 전에는 다른 작업을 수행할 수 없다.

이 외의 다른 문제들

Future로는 하기 어렵던 작업들

  • Future를 외부에서 완료 시킬 수 없다. 취소하거나, get()에 타임아웃을 설정할 수는 있다.
  • 블로킹 코드(get())를 사용하지 않고서는 작업이 끝났을 때 콜백을 실행할 수 없다.
  • 여러 Future를 조합할 수 없다, 예) Event 정보 가져온 다음 Event에 참석하는 회원 목록 가져오기
  • 예외 처리용 API를 제공하지 않는다.

그래서 CompletableFuture 가 나왔다.

ComapletableFuture 는 자바에서 비동기(Asynchronous) 프로그래밍을 가능케하는 인터페이스이다.

CompletableFuture<String> future = new CompletableFuture<>();
future.complete("keesun");

future.get();

비동기로 작업 실행하기

  • 리턴값이 없는 경우: runAsync()
  • 리턴값이 있는 경우: supplyAsync()
  • 원하는 Executor(쓰레드풀)를 사용해서 실행할 수도 있다. (기본은 ForkJoinPool.commonPool())
- // 리턴값이 없는 경우
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("hello");
});
future.get();

// 리턴값이 있는 경우
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "hello";
});
futurue.get();

콜백을 이용하여 비동기적인 수행을 할 수 있다.

콜백 제공하기

  • thenApply(Function): 리턴값을 받아서 다른 값으로 바꾸는 콜백
  • thenAccept(Consumer): 리턴값을 또 다른 작업을 처리하는 콜백 (리턴없이)
  • thenRun(Runnable): 리턴값 받지않고 다른 작업을 처리하는 콜백
  • 콜백 자체를 또 다른 쓰레드에서 실행할 수 있다.
// 리턴값이 없는 경우
CompletableFuture<String> future = CompletableFuture.runAsync(() -> {
    return "hello";
}).thenApply((s) -> {
    return s.toUpperCase();
})
future.get();

// 리턴값이 있는 경우
CompletableFuture<String> future = CompletableFuture.runAsync(() -> {
    return "hello";
}).thenAccept((s) -> {
    s.toUpperCase();
})
future.get();

// 리턴값을 받지 않는 경우
CompletableFuture<String> future = CompletableFuture.runAsync(() -> {
    return "hello";
}).thenRun(() -> {
    System.out.println("hello");
})
future.get();

참고

조합하기

  • thenCompose(): 두 작업이 서로 이어서 실행하도록 조합
  • thenCombine(): 두 작업을 독립적으로 실행하고 둘 다 종료 했을 때 콜백 실행
  • allOf(): 여러 작업을 모두 실행하고 모든 작업 결과에 콜백 실행
  • anyOf(): 여러 작업 중에 가장 빨리 끝난 하나의 결과에 콜백 실행

thenCompose() : 두 작업을 서로 이어서 실행하도록 조합하기

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> {
        System.out.println("Hello" + Thread.currentThread().getName ());
        return "Hello";
    });
    
    CompletableFuture<String> future = hello.thenCompose(App::getWorld);
    
    System.out.printin(future.get());
}
    
private static CompletableFuture<String> getWorld(String message) {
    return CompletableFuture.supplyAsync(() - {
        System.out.printin("World" + Thread.currentThread().getName ());
        return message + " World";
    });
}

thenCombine() : 서로 관계가 없는 작업을 조합하기

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> {
        System.out.println("Hello" + Thread.currentThread().getName ());
        return "Hello";
    });
    
    CompletableFuture<String> world = CompletableFuture.supplyAsync(() -> {
        System.out.println("World" + Thread.currentThread().getName ());
        return "World";
    });
    
    CompletableFuture<String> future = hello.thenCombine(world, (h, w) -> {
        return h + " " + w;
    })
    future.get();
}

allOf() : 여러 작업을 모두 실행하고 모든 작업 결과에 콜백 실행

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> {
        System.out.println("Hello" + Thread.currentThread().getName ());
        return "Hello";
    });
    
    CompletableFuture<String> world = CompletableFuture.supplyAsync(() -> {
        System.out.println("World" + Thread.currentThread().getName ());
        return "World";
    });
    
    List<CompetableFuture> futures = Arrays.asList(hello, world);
    CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[futures.size()]);
    
    CompletableFuture<List<Object>> futureResults = CompletableFuture.allOf(futuresArray).thenArray(v -> {
        return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList()
    });
    
    futureResults.get();
}

anyOf() : 여러 작업 중에 가장 빨리 끝난 하나의 결과에 콜백 실행

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> {
        System.out.println("Hello" + Thread.currentThread().getName ());
        return "Hello";
    });
    
    CompletableFuture<String> world = CompletableFuture.supplyAsync(() -> {
        System.out.println("World" + Thread.currentThread().getName ());
        return "World";
    });
    
    CompletableFuture<Void> future = CompletableFuture.anyOf(hello, world).thenAccept(System.out::println);
    future.get();
}

예외처리하기

  • exeptionally(Function)
  • handle(BiFunction)

exceptionally

public static void main(String[] args) throws ExecutionException, InterruptedException {
    boolean throwError = true; // 강제로 에러 발생
    
    CompletableFutre<String> hello = CompletableFuture.supplyAsync(() -> {
        if (throwError) {
            throw new IllegalArgumentException();
        }
    
        System.out.println("Hello " + Thread.currentThread().getName());
        return "Hello";
    }).exceptionally(ex -> {
        return "Error!";
    });
    
    System.out.println(hello.get());
}

handle(정상적인 결과값, 에러)

public static void main(String[] args) throws ExecutionException, InterruptedException {
    boolean throwError = true; // 강제로 에러 발생
    
    CompletableFutre<String> hello = CompletableFuture.supplyAsync(() -> {
        if (throwError) {
            throw new IllegalArgumentException();
        }
    
        System.out.println("Hello " + Thread.currentThread().getName());
        return "Hello";
    }).handle((result, ex) -> {
        if (ex != null) {
            System.out.println(ex);
            return "Error!";
        }
        return result;
    });
    
    System.out.println(hello.get());
}

그밖에

애노테이션의 변화

  • 자바 8부터 애노테이션을 타입 선언부에도 사용할 수 있게 됨.
  • 자바 8부터 애노테이션을 중복해서 사용할 수 있게 됨.

TYPE_PARAMETER : 애노테이션을 타입에 붙일 수 있게 되었다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_PARAMETER)
public @interface Chicken {

}

// ...

static class FeelsLikeChicken<@Chicken T> {

    public static <@Chicken C> void print(C c) {
        
    }
}

TYPE_USE : 모든 타입에 붙일 수 있다.

public static void main(@Chicken String[] args) throws @Chicken RuntimeException {
    List<@Chicken String> names = Arrays.asList("keesun");
}

중복 사용할 수 있는 애노테이션

  • 중복 사용할 애노테이션 만들기
  • 중복 애노테이션 컨테이너 만들기
  • 컨테이너 애노테이션은 중복 애노테이션과 @Retention 및 @Target이 같거나 더 넓어야 한다.

애노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
@Repeatable(ChickenContainer.class)
public @interface Chicken {
    String value();
}

애노테이션 컨테이너 애노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface ChickenContainer {
    Chicken[] value();
}

사용법

@Chicken("양념")
@Chicken("간장")
public static void main(String[] args) {
    // 애노테이션
    Chicken[] chickens = App.class.getAnnotationsByType(Chicken.class);
    Arrays.stream(chicken).forEach(c -> {
        System.out.println(c.value());
    });
    
    // 애노테이션 컨테이너 애노테이션
    ChickenContainer chickenContainer = App.class.getAnnotation(ChickenContainer.class);
    Arrays.stream(chickenContainer.value()).forEach(c -> {
        System.out.println(c.value());
    });
}

배열 정렬

Arrays.parallelSort() : Fork/Join 프레임워크를 이용한 배열 병렬 정렬 기능

병렬 정렬 알고리즘

  • 배열을 적당한 사이즈까지 계속 양분한다.
  • 합치면서 정렬한다.
int size = 1500;
int[] numbers = new int[size];
Random random = new Random();
IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());

long start = System.nanoTime();
Arrays.sort(numbers);
System.out.println("serial sorting took " + (System.nanoTime() - start));

IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
start = System.nanoTime();
Arrays.parallelSort(numbers);
System.out.println("parallel sorting took " + (System.nanoTime() - start));

알고리즘 효율은 같다.

시간: O(n logN), 공간: O(n)

Metaspace

Java 8 부터 바뀐거

JVM의 여러 메모리 영역 중에 PermGen 메모리 영역이 없어지고 Metaspace 영역이 생김

PermGen(Permanent Generation)

  • 클래스 메타데이터를 담는 곳
  • Heap 영역에 속함
  • 기본값으로 제한된 크기를 가지고 있음 -> 동적으로 많은 클래스를 만들면 에러 발생
  • -XX:PermSize=N, PermGen 초기 사이즈 설정
  • -XX:MaxPermSize=N, PermGen 최대 사이즈 설정

Metaspace

  • 클래스 메타데이터를 담는 곳.
  • Heap 영역이 아니라, Native 메모리 영역
  • 기본값으로 제한된 크기를 가지고 있지 않음 -> 필요한 만큼 계속 늘어남
  • 자바 8부터는 PermGen 관련 java 옵션은 무시
  • -XX:MetaspaceSize=N, Metaspace 초기 사이즈 설정
  • -XX:MaxMetaspaceSize=N, Metaspace 최대 사이즈 설정

참고