자바를 제대로 사용할 수 있는 방법


자바의 역사와 JVM




JIT 컴파일러는 도대체 뭘까?

동적 변환(dynamic translation)이라고 보면 된다.
명칭은 컴파일러지만 실행시에 적용되는 기술이다.


JIT은 위 두 가지 방식을 혼합한 것이라고 보면 된다.
변환 작업은 인터프리터에 의해서 지속적으로 수행되지만, 필요한 코드의 정보는 캐시에 담아두었다가 재사용한다.

분명히 javac 명령어로 컴파일 했는데 그럼 정적 컴파일 방식 아닌가?
javac 명령어로 컴파일 하는 단계에서 만들어진 .class 파일은 바이트 코드일 뿐이다.
이렇게 한 번 컴파일한 코드는 어느 OS에서나 사용할 수 있다.
즉 class 파일은 컴퓨터가 알아들을 수 있도록 기계 코드로 다시 변환 작업이 필요하다는 것이다.
이 변환 작업을 JIT 컴파일러에서 한다고 보면 된다.




HotSpot은 뭘까?

Sun에서 개발한 JVM 이름이다.

HotSpot 종류




실수를 방지할 수 있도록 도와주는 제네릭

자바는 여러 타입이 존재하기 때문에 형변환을 하면서 많은 예외가 발생할 수 있다.

public class CastingDTO implements Serializable {
    private Object object;
    
    // Setter, Getter
}


CastingDTO dto1 = new CastingDTO();
dto1.setObject(new String());

CastingDTO dto2 = new CastingDTO();
dto2.setObject(new StringBuilder());

CastingDTO dto3 = new CastingDTO();
dto3.setObject(new StringBuffer());

위 코드는 컴파일과 문제 없이 실행된다.
그런데 저장되어 있는 값을 꺼낼 때 문제가 발생한다.
getObject() 메서드 리턴 타입이 Object이기 때문이다.

다음과 같은 형변환이 필요하다.

String temp = (String)dto1.getObject();


그런데 인스턴스 변수 타입이 StringBuilder인지 StringBuffer인지 혼동될 경우 어떻게 될까?
instanceof 예약어를 사용하여 타입을 점검하면 된다.

if(temp instanceof StringBuilder)

이러한 단점을 보완하기 위해서 Java 5 부터 새롭게 추가된 제네릭(Generic)이라는 것이 있다.




제네릭은 뭘까?

제네릭은 타입 형변환에서 발생할 수 있는 문제점을 사전에 없애기 위해서 만들어졌다.
컴파일할 때 점검한다는 의미다.

public class CastingGenericDTO<T> implements Serializable {
    private T object;
    
    // Setter, Getter
}


CastingGenericDTO<String> dto = new CastingGenericDTO<>();

객체를 선언할 때 꺽쇠 안에 각 타입을 명시해 줘서 귀찮을 것 같지만
getObject() 메서드를 사용하여 객체를 가져올 때는 간단해진다.

String temp = dto.getObject();

형변환할 필요가 없어졌다.
만약 잘못된 타입으로 치환하면 컴파일 자체가 안 된다.
따라서 실행 시에 형변환으로 인해 예외가 발생하는 일은 없다.




제네릭을 사용하는 이유

(1) 컴파일할 때 타입을 체크해서 에러를 사전에 잡을 수 있다.

(2) 컴파일러가 타입 캐스팅을 해주기 때문에 개발자가 편리하다.

(3) 타입만 다르고 코드의 내용이 대부분 일치할 때 코드의 재사용성이 좋아진다.




Java 8




Java8: Optional

optional이라는 단어는 “선택적인”이라는 의미다.
Optional은 Functional 언어인 Haskell과 Scala에서 제공하는 기능을 따 온 것이다.
객체를 편리하게 처리하기 위해서 만든 클래스라고 보면 된다.

Optional 클래스는 java.util 패키지에 속해 있다.

public final class Optional<T> extends Object

Optional 클래스는 깡통이라고 생각하면 된다.
이 깡통에 물건을 넣을 수도 있고, 아무 물건이 없을 수도 있다.
그래서 기본적인 깡통으로 만들기 위해서 Optional 클래스는 new Optional()과 같이 객체를 생성하지 않는다.




Optional 객체 생성

API 문서를 잘 살펴보면 Optional 클래스를 리턴하는
empty(), of(), ofNullable() 메서드들이 존재한다.

(1) Optional.empty()
null이 아닌 객체를 담고 있는 Optional 객체를 생성한다.
이 비어있는 객체는 Optional 내부적으로 미리 생성해놓은 싱글톤 인스턴스입니다.

(2) Optional.of(value)
null이 아닌 객체를 담고 있는 Optional 객체를 생성한다.
null이 넘어올 경우 NullPointerException을 던지기 때문에 주의해서 사용해야 한다.

(3) Optional.ofNullable(value)
null인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성한다.
null이 넘어올 경우 NPE를 던지지 않고 Optional.empty()와 동일하게
비어있는 Optional 객체를 얻어옵니다.




객체 꺼내기

Optional이 담고 있는 객체를 꺼내로기 위해서 다양한 인스턴스 메서드를 제공한다.

(1) get()
비어있는 Optional 객체에 대해서 NoSuchElementException을 던진다.

(2) orElse(T other)
비어있는 Optional 객체에 대해서 넘어온 인자를 반환한다.

(3) orElseGet(Supplier<? extends T> other)
비어있는 Optional 객체에 대해서 넘어온 함수형 인자를 통해 생성된 객체를 반환한다.
orElse(T other)의 게으른 버전이다.
비어있는 경우에만 함수가 호출되기 때문에
orElse(T other) 대비 성능상 이점을 기대할 수 있다.

(4) orElseThrow(Supplier<? extends T> exceptionSupplier)
비어있는 Optional 객체에 대해서 넘어온 함수형 인자를 통해 생성된 예외를 던집니다.




Java8: Default method

Java8부터는 default 메서드가 추가되었다.

public interface DefaultStaticInterface {
    default String getEmail() {
        return name + "@gmail.com";
    } 
}

위 코드는 Java8에서 컴파일이 잘 된다.
DefaultStaticInterface를 구현하고 getName() 메서드를 재정의하면?
이 경우도 괜찮다.

그렇다면 default 메서드를 왜 만들었을까?
“하위 호환성” 때문이다.

예를들어, 오픈소스를 만들었다고 가정하자.
많은 사람들이 사용하고 있는데 인터페이스에 새로운 메서드를 만들어야 하는 상황이 발생했다.
자칫 잘못하면 오류가 발생하고 수정해야 하는 일이 발생할 수도 있다.
이럴 때 사용하는 것이 default 메서드다.




Java8: 병렬 배열 정렬 (Parallel array sorting)

배열을 정렬하는 가장 간편한 방법은 java.util 패키지의 Arrays 클래스를 사용하는 것이다.
이 Arrays 클래스에는 다음과 같은 static 메서드들이 존재한다.


Java8 에서는 paralleleSort()라는 정렬 메섬드가 제공되며
Java7 에서 소개된 Fork-Join 프레임워크가 내부적으로 사용된다.

사용법은 다음과 같다.

int[] intValues = new int[10];
Arrays.parallelSort(intValues);

sort()의 경우 단일 스레드로 수행되며,
parallelSort()는 필요에 따라 여러개의 스레드로 나뉘어 작업이 수행된다.

parallelSort()가 CPU를 더 많이 사용하게 되겠지만 처리 속도는 더 빠르다.




Java8: StringJoiner

StringJoiner은 java.util에 포함되어 있으며
순차적으로 나열되는 문자열을 처리할 떄 사용한다.

String[] stringsArray = new String[]{"A", "B", "C"};

위 배열을 (A, B, C) 이렇게 변환하고 싶으면 어떻게 해야 할까?
콤마(,) 처리를 위해 if문을 넣거나 substring을 사용해야 하는데
이러한 단점을 보완하기 위해 StringJoiner가 만들어졌다.

StringJoiner joiner = new StringJoiner(",");

for(Stirng s : stringArray) {
    joiner.add(s);   
}

System.out.println(joiner);




Java8: Lambda 표현식

익명 클래스를 사용하면 가독성도 떨어지고 불편한데
이러한 단점을 보완하기 위해 람다 표현식이 만들어졌다.

대신 이 표현식은 인터페이스에 메서드가 하나인 것들만 적용 가능하다.
그래서 익명 클래스 <-> 람다 표현식 전환이 가능하다.




Java8: Stream

자바의 스트림은 “연속된 정보”를 처리하는 데 사용한다.
자바에 연속된 정보로 배열, 컬렉션이 있다.
배열에는 스트림을 사용할 수 없지만 List로 변환하는 방법은 다양하다.

Integer[] values = {1, 3, 5};

// (1)
List<Integer> list = new ArrayList<Integer>(Arrays.asList(values));

// (2)
List<Integer> list = Arrays.stream(values).collect(Collectors.toList());


스트림 구조

list.stram().filter(x -> x > 10).count()

메서드 참조

forEach()를 사용해 목록을 출력해보자.

forEach(System.out::println)

:: 이 더블 콜론은 정확하게 Method Reference 라고 부른다.
즉 메서드 참조를 의미한다.



stream map()

map()은 스트림 값을 변환한다.

list.stream.map(x -> x*3).forEach(System.out::println)

이렇게 map()을 사용하면 스트림에서 처리하는 값들을 중간에 변경할 수 있다.