스레드와 관련된 내용 Thread 와 관련된 내용은 이전에도 정리를 많이 했을 뿐더러, 오늘 수업에 키포인트들만 요약해서 적도록 하겠다.
notify() / wait() -> synchronized
프로그램이 종료될 때 ? -> 모든 일반 스레드가 종료될 때
primitive type 은 synchronized 를 하여도, race condition 이 발생한다.
데드락 발생 조건 3가지
오늘 잊고있던 스레드 개념들에 대해서 다시 상기하게 되었다.
원자적 수행
을 보장한다. ⇒ 이 부분은 내가 volatile 을 정리했을때 설명했던 부분이다.long
과 double
혹은 dynamic-computed constant
의 경우 constant pool 에서 해당 index 를 참조하여 가져오는 과정이 포함되어 있어서 해당 과정에서 다른 Thread 가 끼어들 수 있기에 원자적 수행을 보장하지 못한다고 생각하는게 좋지 않을까 싶다.ldc2_w
를 key 포인트로 잡고 검색하면 된다.class GenericExample<T> {
T data;
public GenericExample(T data) {
this.data = data;
}
public static void main(String[] args) {
GenericExample<String> ge = new GenericExample<>("Cool");
}
}
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #7 // Field data:Ljava/lang/Object;
9: return
생성자가 위와 같이 되어있는데 간단하게 보면 일단 data
를 reference 로 stack Index #1 에 저장하고, 5 번줄에서 aload_1
을 통해 stack 내의 lva 에서 1번을 꺼내오고, putfield
를 해주는데 이때 putField
에서 매개변수로 넘어온 data object 의 ref 를 대입해준다. (틀린 부분이 있을수도..?)
new Object()
가 주소값을 주는 것이 아닐까에 대해서 궁금증이 생겨서 한변 살펴보았다.class GenericExample<T extends java.lang.Object> extends java.lang.Object
이렇게 숨켜져 있는걸 처음 알았다!
개념은 간단하게 정리할 수 없어 아주 짧게 요약하자면 Generice 자체가 중요하게 여기는 컨셉은 Compile 시점에 ClassCastException 을 방지하자 가 아닐까 싶다.
뭐 저러한 개념이 주요한 Concept 이기에 와일드카드 이런 개념도 나오지 않았을까 싶다.
<? extends T>
와일드 카드의 상한 제한(upper bound) - T와 그 자손들을 구현한 객체들만 매개변수로 가능<? super T>
와일드 카드의 하한 제한(lower bound) -T와 그 조상들을 구현한 객체들만 매개변수로 가능<?>
제한 없음위의 사진의 코드가 컴파일 될것 이라고 생각하는가 ? 고민 해보고 아래 답을 봤으면 좋겠다!
컴파일 된다. 왜냐하면 배열은 부모 / 자식 관계가 있어서 (상위/하위가 더 옳은표현인가..?) 여튼 해당 부분이 컴파일 되고 값 또한 넣을 수 있으며 정상적으로 출력된다. 근데 이러한 Concept 이 과연 좋은 것인가? 에 대한것은 상당히 의문이다.
그럼 아래 코드는 컴파일이 될까?
1 public class ArrayRuntimeException {
2
3 public static void main(String[] args) {
4 Object[] array = new Long[2];
5 array[0] = 1L;
6 array[1] = "이건 될꺄요?";
7 System.out.println(array[0]);
8 }
9 }
불행히도 컴파일이 가능하다.. 정확히 아무 오류도 나지 않는다.
이걸 컴파일 하고 실행시키게 되면 아래의 ArrayStoreException
이 발생한다.
왜 컴파일 시점에 이러한 오류를 잡아주지 못하는 것일까? Effective Java 에서는 배열이 Runtime 에 실체화(relify) 된다고 하는데, 이게 말이 어려운것 같은데 내 방식으로 쉽게 말하면 런타임에 값이 들어올때 그때 Type 검증을 한다고 생각하면 된다. 그니까 음.. 이걸 쉽게 이해하려면 Generic 의 개념을 들고와야 한다. 위 실체화 개념이 이해 안가도 밑에 보다보면 이해가 갈것이다
1 import java.util.*;
2
3 public class ListRuntimeException {
4
5 public static void main(String[] args) {
6 List<Object> ol = new ArrayList<Long>();
7 }
8 }
이 파일을 컴파일 하게 되면 컴파일 시점에 오류가 난다. 왜? 이건 Generic 은 부모 / 자식 관계를 따지지 않는다. 빨리 얘기해서 제네릭은 컴파일 시에 소거(erasure) 된다. (소거 개념은 Type-Parameter 를 key 로 잡고 검색하시면 찾을 수 있을겁니다) 그니까 컴파일 시에 소거 되어서 해당 Type 검증을 컴파일 시에 모두 진행한다. 따라서 런타임시에는 타입 검증을 할 수 없다! 이게 실체화와의 차이이다. 실체화는 런타임시에 이런 타입 검증을 해서 우리가 언제 오류가 발생할지 알 수 없다. 하지만 제네릭은 우리가 코드를 저렇게 실수한다고 해도, 컴파일 시에 잡아줌으로써 좀 더 안전하다는 것이다.
그리고 예외는 항상 런타임시에 발생하는 것 보다는 컴파일시에 발생하는 것이 좋다
(백기선님 스터디 중 백기선님 말씀 발췌)
위의 실체화 개념을 응용 하자면 아래와 같은 코드가 과연 될까 ?
1 import java.util.*;
2
3 public class ListMakeToArray {
4
5 public static void main(String[] args) {
6 List<String>[] stringLists = new ArrayList<String>[1];
7 }
8 }
~
아래 코드를 컴파일 하면 error: generic array creation
가 난다.
이건 Effective Java 166 쪽에 자세히 나와 있어서 따로 기술하진 않겠다.. 너무 오래걸린다
근데 간략히 설명하면 ClassCastException 이 일어날 수 있기에 미리 만들지 못하도록 막아버리는 것이다. Generic 에 Concept 에 위반되는 것이기에.
이건 어디까지나 내 추측인데 배열이 이러한 relify 로직때문에 비용이 더 들지 않을까 ? 라는 생각이 든다. 그니까 런타임시에 계속해서 값이 들어올때 마다 검증 로직을 해줘야되지 않을까? 라는 생각이 있다.
다만 고정되고 실수가 나올수 없는 것들에 대해서는 배열을 쓰는 것이 좋다고 생각될 때도 있다.
너무 어렵게 가진것 같은데 여튼 Generic 은 컴파일시에 소거되므로 컴파일시에 검증이 이뤄지고, 배열은 Runtime 시에 검증이 이뤄진다는 것이 중요한 컨셉이지 않을까 싶다.
우리가 코드스쿼드 과정중 Chess 에서 Array 대신 List 를 써보란 이유들도 해당 이유가 포함되어 있지 않을까 싶다.. 근데 지금 Blank 도 있고 빈값없는 상태로 크기가 고정되어 있는데 1차원 배열을 써서 표현해도 괜찮지 않을까 싶다. 그런데 이렇게 할시에는 Board 에 해당 index 배치 등등의 작업들이 너무 집중되는 감이 없지 않아 있다. 나는 Rank 라는 1급 콜렉션으로 분리하여 해당 Rank 에 대한 로직들을 분산시켰는데, 일차원 배열을 쓰게 되면 분산되지 않고, Board 에서 유지관리가 일어난다는 단점이 있을 것 같다.