본문 바로가기
Course/JSCODE - 자바

3주차 스터디 노트

by Dev Lighthouse 2024. 8. 29.
320x100
320x100

컬렉션

스레드


 

답변

컬렉션

  • JCF란 무엇인가요?

- JCF: Java Collections Framework

재사용 가능한 컬렉션 데이터 구조를 구현하는 클래스 및 인터페이스 집합

프레임워크라고 하지만, 라이브러리 방식으로 작동

컬렉션 프레임워크는 다양한 컬렉션을 정의하는 인터페이스와 컬렉션을 구현하는 클래스를 모두 제공

 

위키백과 정의

 

- 출처

https://en.wikipedia.org/wiki/Java_collections_framework

 

  • JCF의 계층 구조를 설명해 주세요.

JCF는 크게 Collection Map 인터페이스로 나눌 수 있다

오라클 자바 공식문서
위키백과 - Collection
위키백과 - Map

실제로 인텔리제이를 켜서 안으로 들어가서 Collection을 다이어그램으로 확인해봤다

복잡시럽군

다..다이어그램 말고 인터페이스를 볼 수 있는 자바 파일로 들어가봤다

Collection

Iterable을 상속받고 제네릭 인터페이스로 되어있는 Collection를 볼 수 있다

Map

아무것도 상속받지 않고, 제네릭 두개를 갖는 Key, Value 형태의 Map을 볼 수 있다

 

- 출처

https://docs.oracle.com/javase/tutorial/collections/index.html

https://docs.oracle.com/javase/tutorial/collections/interfaces/index.html

https://en.wikipedia.org/wiki/Java_collections_framework

 

 

  • List 인터페이스는 무엇이고, 구현체의 종류는 무엇이 있나요?

순서가 있는 요소들의 컬렉션으로, 중복 요소를 허용

또한 인덱스를 통해 요소에 접근할 수 있으며, ArrayList, LinkedList, Vector 등이 주요 구현체

List

 

 

  • ArrayList에 대해 설명해 주세요.

내부적으로 배열을 사용하여 요소를 저장하는 List 인터페이스의 구현체

인덱스를 통해 빠르게 요소에 접근할 수 있지만, 배열의 크기를 늘리거나 줄이는 작업은 비용이 많이 필요

 

 

  • ArrayList는 어떻게 동적으로 사이즈가 늘어나나요?

기본 크기

기본적으로 크기가 고정된 배열을 사용(Size: 10)

생성자

생성자로 크기가 주어진 경우

add()

배열에 요소를 추가할때(add) 배열의 크기를 확인하고, 크기가 가득 차면 grow()라는 함수를 통해 배열 크기가 확장된다

배열이 다 차면 더 큰 사이즈로 확장하여 데이터를 복사한다(1.5배)

grow()

 

 

  • LinkedList에 대해 설명해 주세요.

이중 연결 리스트를 사용하여 구현된 List 인터페이스의 구현체

요소의 삽입과 삭제가 빈번하게 발생할 때 유리하며, 각 요소는 노드로 구성되어 있고, 노드는 이전과 다음 노드를 참조함

first와 last를 갖고 있는 LinkedList

 

사용 가능한 메서드

이중 연결 리스트를 사용하기 때문에 Last, First쪽으로 데이터를 넣을 수 있다

 

 

  • 언제 ArrayList를 사용하고, 언제 LinkedList를 사용할까요?

ArrayList: 요소의 접근이 빈번하며, 삽입과 삭제가 비교적 적은 경우 사용. 랜덤 액세스가 빠름

LinkedList: 요소의 삽입과 삭제가 빈번한 경우 사용. 삽입과 삭제가 빠르지만, 인덱스를 통한 접근 속도는 느림

 

시간복잡도 차이

- Access(조회/접근)

1. ArrayList: 내부적으로 배열을 사용하기때문에 배열 인덱스를 접근하는 것과 같음 O(1)

2. LinkedList: 노드들이 서로 연결되어 있기 때문에 특정 인덱스로 접근하기 위해서는 처음부터 해당 인덱스까지 순차탐색 필요 O(N)

 

- Insert/Delete(삽입/삭제)

1. ArrayList: 중간에 요소를 삽입하는 경우 평균적으로 O(N). 맨 끝에서 데이터를 넣거나 삭제 시에는 O(1). 

2. LinkedList: 맨 앞이나 맨 뒤에서 데이터를 넣거나 삭제 시에는 O(1). 대신 특정 인데스가 주어진 곳에 처리할 때는 O(N)

 

 

  • ArrayList와 Vector는 어떠한 차이가 있나요?

Vector: 동기화(synchronized)되어 있어 멀티스레드 환경에서 안전

ArrayList: 동기화되지 않아 단일 스레드 환경에서 더 빠름

Vector 설명

위에 살펴보면 스레드-safe한 상태가 필요치 않다면 ArrayList를 사용하는 걸 추천한다고 써있다

스레드 세이프하다는걸 보면 synchronized 키워드가 있을 것으로 예상되는데... 두 컬렉션의 add 메서드를 추가로 살펴봤다

Vector의 add()
ArrayList의 add()

예상적중~!!

 

 

  • Stack과 Queue가 무엇인가요?

 

- Stack

LIFO(Last-In-First-Out) 구조를 따르는 컬렉션

push, pop, peek 등의 오퍼레이터 사용

 

- Queue

FIFO(First-In-First-Out) 구조를 따르는 컬렉션

offer, poll, peek 등의 오퍼레이터 사용

 

 

  • Set이 무엇이고, 구현 클래스가 무엇이 있는지 설명해 주세요.

Set은 중복을 허용하지 않는 요소들의 컬렉션

구현 클래스: HashSet, LinkedHashSet, TreeSet 등

- 구현 클래스별 차이

HashSet: 중복 제거, 빠른 탐색

LinkedHashSet: 중복 제거, 순서 유지

TreeSet: 중복 제거, 정렬 상태

 

 

  • Set에서 중복 요소를 어떻게 걸러내는지 설명해 주세요.

eqauls()와 hashCode() 메서드를 활용하여 중복 요소 제거

 

- 중복 요소 확인 과정

  1. 해시 코드 계산: hash(key)를 통해 주어진 요소의 해시 코드를 계산합니다.
  2. 버킷 탐색: 계산된 해시 코드를 기반으로 해당 요소가 저장될 버킷을 찾습니다. 이 버킷의 인덱스는 (n - 1) & hash로 결정됩니다.
  3. 버킷 내에서 탐색:
    • 만약 해당 버킷이 비어있으면, 해당 위치에 새로운 노드를 추가합니다.
    • 만약 해당 버킷에 이미 노드가 존재하면, 해당 노드의 해시 코드와 주어진 요소의 해시 코드가 동일한지 확인합니다.
    • 해시 코드가 동일하다면, equals() 메서드를 호출하여 주어진 요소와 이미 존재하는 요소가 동일한지 확인합니다.
  4. 중복 확인:
    • 만약 hashCode()가 동일하고, equals() 메서드가 true를 반환하면, 중복된 요소로 판단하여 새로운 노드를 추가하지 않고 기존 노드를 반환합니다.
    • 그렇지 않으면, 새로운 노드를 버킷에 연결 리스트의 형태로 추가합니다.

IntelliJ로 break point를 걸어서 확인해 봤다

HashSet add()

map에 put을 한다

HashSet을 인스턴스화한 결과는 HashMap

참고로 hashSet은 내부적으로 HashMap을 사용한다

HashMap의 put을 하면 putVal 메서드를 호출한다

HashSet에서 HashMap의 메서드를 사용하는 것을 볼 수 있다

putVal
버킷이 비어있는 경우

if 총 2개 구문이다!!

if, else-if 구문이 아니기때문에 2번 연속 이 condition 브랜치를 탈 가능성이 있다(1번째, 2번째 if 둘다 걸리는 경우)

 

- 해시 테이블이 초기화되지 않은 경우(첫번째 if): table이 null이거나 크기가 0이면 resize()를 호출해서 테이블을 초기화하거나 크기를 조절한다

- 버킷이 비어있는 경우(두번째 if): 계산된 해시 코드(hash)에 해당하는 인덱스 i를 찾고, 해당 버킷이 비어있으면(p == null), 그 위치에 새로운 노드를 추가한다

 

Node tab, Node p

p가 노드라는 것을 기억하자 -> 연결 리스트

이젠 else구문을 살펴보자

버킷이 비어있지 않은 경우

이 블록에서는 버킷이 비어있지 않으면서 해시 충돌이 발생한 경우(동일한 해시 코드의 다른 요소가 이미 존재하는 경우)의 처리 방식을 정의한다

 

- 첫번째 if

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

노드의 해시값과 키가 새로 삽입하려는 키와 같은지 확인

해시값이 같고, 키가 동일하거나 키가 이미 존재하고, equals() 메서드로 비교했을 때 같다면, 이 노드를 업데이트한다

 

- else if

else if (p instanceof TreeNode)

 

만약 p가 트리 구조로 관리되는 노드(TreeNode)라면, 트리 형태로 처리하는 메서드 putTreeVal()을 호출한다

 

- else

else {
    for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
            p.next = newNode(hash, key, value, null);
            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                treeifyBin(tab, hash);
            break;
        }
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            break;
        p = e;
    }
}

버킷이 비어있지 않을 때 수행되는 블록

 

 

- 첫번째 if: 연결 리스트의 끝에 도달했을 때

p.next가 null이라는 것은 현재 요소 p가 연결 리스트의 마지막 요소라는 의미

이 경우, 새로운 요소를 연결 리스트의 끝에 추가한다

newNode(hash, key, value, null)를 호출하여 새로운 노드를 생성하고, 이를 p.next에 추가한다

 

연결 리스트의 길이(binCount)가 TREEIFY_THRESHOLD - 1을 초과한 경우 리스트를 트리 구조로 변환한다

이를 위해 treeifyBin(tab, hash) 메서드가 호출됨

 

- 두번째 if: 만약 p.next가 null이 아니라면, 현재 노드 e의 해시코드와 새로 추가하려는 요소의 해시코드를 비교

해시코드가 같고, equals() 메서드가 true를 반환하면, 이는 동일한 요소라는 뜻

이 경우, 새로운 요소를 추가하지 않고 break로 루프를 종료한다!!(중복 요소를 방지하는 핵심 로직)

 

- p = e: 다음 노드로 이동

위의 두 조건들이 모두 충족되지 않는다면, 현재 노드 p를 다음 노드(e)로 이동시킴

 

 

 

  • Map이 무엇이고, 구현 클래스가 무엇이 있는지 설명해 주세요.

키와 값의 쌍으로 이루어진 데이터를 관리하는 컬렉션

구현클래스로는 HashMap, LinkedHashMap, TreeMap, HashTable 등이 있음

 

 

 

  • HashMap은 어떻게 동작하나요?

Node로 이루어진 내부 필드 table

기타 블로그나 ChatGPT를 사용하면 HashMap은 내부적으로 HashTable을 사용한다고 나와있다

그건 어느정도는 틀린 이야기다

key가 해싱되어서 들어가면서 table이랑 내부변수를 사용하지만, HashTable이라는 클래스하고는 다른 개념인 것이다

어쨌든 이 Node는 버킷이라고 불린다. 그리고 배열 형태로 갖고 있다

내부 동작 원리부터 알아보자

 

사실 위에 Set에서 중복 요소를 건너내는 부분에서 거의 설명하긴 했다

1. 해시맵에 요소를 넣음: put(K, V)

put

2. 키 해싱: hash(key)

키를 해싱하는 메서드

3. 버킷 인덱스 결정

비트 연산을 통해 모듈러 연산을 최적화시켜 인덱스의 노드를 구함

tab[i = (n - 1) & hash]

최적화된 모듈러 연산

4. 해시충돌이 일어나면 separate chaining을 통해 해결

기본적으로 연결리스트로 해결하다가 연결리스트가 TREEIFY 임계값(8) - 1보다 길어지면 Tree형태로 변환(Red-Black Tree)

리스트 -> 트리

해시충돌이 일어날때 해결하는 방법 2가지가 있는데 1. Open Addressing 2. Seperate Chaining

Java에서는 separate chaining(분리 연결법)으로 해결하고 있다

Node와 다음 Node를 탐색하는 것만 봐도 알 수 있다

 

- 출처

https://github.com/wjdrbs96/Today-I-Learn/blob/master/Java/Collection/Map/Java%20HashMap%20%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC.md

 

 

 

  • HashMap의 최악의 시간 복잡도를 설명해 주세요.

딱 말하면 Java8 미만버전에서는 O(n)이고 자바8이후부터는 O(logN)

이유는 자바8부터 연결리스트에서 연결사이즈가 일정크기 이상일때는 Tree로 변환되기 때문에 Tree의 높이만큼 검색하기 때문이다

아래는 내가 직접 자바7을 오라클에서 다운받아서 프로젝트가 추가하고 살펴본 자바7의 HashMap의 put()메서드이다

자바 7(1.7)
자바7의 put 메서드
자바8

자바8에서는 put메서드에서 putVal메서드를 내부적으로 한 번 더 호출하고, 여기에서 연결리스트->트리로 만들고 있는 것을 볼 수 있었다

Red-Black Tree

또한 이 트리는 레드블랙 트리의 특성을 갖도록 구현되는데

레드 블랙 트리란 이진 탐색 트리의 일종으로, 모든 삽입, 삭제, 탐색 연산의 시간 복잡도가 O(log n)으로 유지하는 트리이다

 

 

 

  • Map 인터페이스는 왜 Collection 인터페이스에 상속을 받지 않았나요?

- Collection: 단일 요소의 집합<T>을 관리하는 인터페이스

- Map: <K, V> 형태의 자료구조를 저장하기 위한 인터페이스

즉 두 인터페이스가 관리하는 자료구조가 다르기때문

관심사가 다르기 때문에 억지로 메서드를 맞추면 어색해지기때문에 JCF 하위의 두 인터페이스로 두지 않았나 싶다

 

아래에 인텔리제이의 다이어그램을 사용해서 두 인터페이스를 비교해봤다

Map, Collection Diagram

지원하는 메서드도 다르고, Map은 Entry<K,V>로 순회가 가능하고 Collection은 Iterable<T>로 순회가 가능하다는 것을 알 수 있다

 

 

 

  • iterable과 iterator의 차이는 무엇인가요?

바로 위에 제공한 스크린샷에서 보면 Collection과 Iterable의 상관관계를 알 수 있다

Collection Interface
Iterable Diagram

사실 Collection은 이미 Iterable<T> 인터페이스를 상속받는 인터페이스이다

 

- iterable

컬렉션이 반복될 수 있음을 나타내는 인터페이스

또한 이 인터페이스를 구현한 클래스는 iterator를 구현해서 제공해야만 한다

자바8 공식문서

공식문서에 보면 Iterable을 구현한 Object는 for-each loop 구문의 사용을 허락한다고 나와있다

 

- iterator

컬렉션의 요소들을 순차적으로 접근(traverse)하기 위한 인터페이스

Iterator는 컬렉션 내의 각 요소를 반복적으로 탐색하면서, 다음 요소로 이동하거나 현재 요소에 대한 조작을 할 수 있다

Iterator Interface
IIterator Interface Method

예제코드를 작성해봤다

Collection

Collection에 속하는 ArrayList, HashSet, Stack, ArrayDeque가 Iterable 인터페이스를 구현했기 때문에

for-each문으로 접근이 가능할 것이라 예상했다

실행결과

Iterator를 테스트해봤다

Iterator

기본적으로 iterator객체를 가져와서

while문으로 .hasNext()가 있을때까지.. 반복을 돌린다

다만 삭제할 때 주의할 점이 있다

remove()를 호출할 때는 먼저 next를 호출해야만 한다!!

remove
예외
메서드 설명

next()를 한 값을 쓰지 않는다고 해도, 한번 호출한 이후에 remove()를 해준다면.. 예외는 발생하지 않게된다

올바른 사용

 

그럼 공식문서에서 나와있는 for-each loop문을 사용할 수 있게 만드는 현업에서 사용할만한 코드는 어떤게 있을까?싶어서 클래스를 간단하게 만들어봤다

Person 클래스

main 메서드

children iterator

그래서 이게뭔데?? 하겠지만..

맨 아래에 사용된

for (Person child : parent) {
    System.out.println(child.getName() + " is " + child.getAge() + " years old.");
}

이 코드는 네이밍도 그렇고 부모가 자식을 가진 것 같이 가독성있고 간단하게 표현할 수 있다

 

그런데 만약에 Person 클래스가 내부적으로 List<Person> children을 갖는데 Iterable<Person>을 구현하지 않은 상태라면?

class Person implements Iterable<Person> // 기존
class Person // 이 경우

작동하지 않는 기존 코드
에러메시지

Iterable 인터페이스를 받아서 Iterator를 구현하며 children List의 iterator()를 반환하는 식으로 컴파일에러를 잡을 수 있었다

 

- 출처

https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html

 

 

 

  • Collection과 Collections의 차이는 무엇인가요?

- Collection: 자바의 최상위 컬렉션 인터페이스

- Collections: 자바 컬렉션과 관련된 유틸리티 메서드들을 제공하는 유틸리티 클래스. 대부분 정적(static) 메서드로 구성

Collection과 Collections

같은점이라면 둘 다 java.util패키지에 속해있다

하지만 Collection은 인터페이스, Collections은 클래스 모양인 것을 확인할 수 있다

스레드

  • Java에서 스레드를 만드는 방법을 설명해 주세요.

1. Thread 클래스를 상속받는 방법

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

상속, 구현
스레드 생성 & 실행
실행

 

  • runnable과 callable의 차이는 무엇인가요?

코딩공부는 단순 암기가 아니다. 공식문서나 jdk 라이브러리 내의 코드 설명을 보면된다. 약간의 영어해석 실력과 함께

Split View

이제 비교해보자

 

- Runnable

Runnable

1. java.lang 패키지에 있다

2. javadoc 해석 - 결과값으로 아무것도 리턴하지 않는 하나의 오퍼레이션을 나타낸단다. 이것은 1개의 run()이라는 추상메서드를 갖는 함수형 인터페이스라고 한다. 이것과 같이 볼만한 내용으로 java.util패키지에 있는 Callable을 살펴보라고 한다

 

이제 함수의 반환형과 시그니쳐를 살펴보자

반환형은 void이고 함수 인자로 아무것도 들어가지 않는다

 

- Callable

Callable

1. java.util.concurrent 패키지에 있다.

궁금해서 패키지를 찾아가봤다. concurrent 패키지 하위로 atomic, locks 패키지가 있는 것을 볼 수 있다

java.util.concurrent

2. javadoc 해석 - 하나의 결과값을 반환할 수 있고, 예외도 던질 수 있는 하나의 태스크(작업)이라고 한다.

구현체는 아규먼트(인자)가 없는 1개의 단일 call() 메서드를 정의한다

Callable 인터페이스는 Runnable 인터페이스와 비슷하다, 두 인터페이스 모두 다른 스레드에 의해 실행될 수 있는 클래스에 사용되기 위해 디자인(고안)되었다고 한다

하지만 Runnable은 반환값도 없고, checked 예외를 throw 할 수 없다

또한 Executors 클래스들은 다른 일반적인 형태를 Callable 클래스로 변환할 수 있는 유틸리티 메서드를 포함하고 있다

1.5 버전에서 만들어졌다

이것과 함께 볼만한 내용으로 Executor가 있다고 한다

 

이제 함수의 반환형과 시그니처를 살펴보자

반환형은 V(제네릭)이고, 인자로는 아무것도 받지 않으며 Exception을 던질 수 있다고 명시하고 있다

 

위 두 내용을 번역기나 AI 사용 없이, 내가 직접 해석한 것이기때문에 조금 어색할 수 있는 점 양해바란다

그래도 뭔가를 공부할때는 이렇게 하는게 맞다고 생각한다. 어디 블로그 가서 똑같이 보고 따라적지말길..

 

  • sleep()과 wait()의 차이는 무엇인가요?

- sleep()

1. 현재 실행 중인 쓰레드를 지정된 시간 동안 멈추게 합니다. 지정된 시간이 지나면 자동으로 쓰레드는 다시 실행됨

2. 정적 메서드(static method)로서, 특정 객체에 종속되지 않음

3. sleep() 메서드를 호출하면 쓰레드는 지정된 시간 동안 일시 정지 상태로 전환. 이 동안 다른 쓰레드가 실행될 수 있다

4. InterruptedException 예외를 핸들링해야 한다(try-catch 혹은 메서드 레벨에서 throws)

5. ex)

Thread.sleeep(1000L);

 

 

- wait()

1. wait()은 객체의 인스턴스 메서드이며, 반드시 synchronized 블록 안에서 호출되어야 함(스레드간의 동기화를 위해 사용됨)

2. wait()은 쓰레드를 멈추게 하지만, 특정 조건을 만족할때까지 or 다른 스레드가 notify()나 notifyAll()을 호출할때까지 해당 스레드는 계속 대기함

3. synchronized 블록 내에서 사용되지 않으면 IllegalMonitorStateException 예외가 발생함

4. ex)

 public synchronized void waitForSignal() {
        try {
            System.out.println("Waiting...");
            wait();
            System.out.println("Notified and resumed");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void sendSignal() {
        System.out.println("Sending signal...");
        notify()
    }

 

 

  • 스레드 풀이란 무엇이고, 왜 사용할까요?

스레드 풀 정의

1. 미리 생성된 스레드들의 집합으로, 필요할 때 스레드를 재사용 할 수 있게 함

2. 스레드 작업이 들어오면 새로운 스레드를 생성하는 대신, 풀에 있는 기존 스레드를 재사용

3. java.util.concurrent.Executors 클래스에서 다양한 스레드 풀을 제공하는 메서드를 사용해 생성 가능

 

사용하는 목적

1. 성능 향상: 스레드를 반복적으로 생성하고 소멸시키는 오버헤드 감소

2. 자원 관리: 동시에 실행되는 스레드 수를 제한하여 시스템 자원의 고갈을 방지

3. 작업 관리: 큐에 작업을 제출하고, 스레드 풀이 작업을 수행하도록 하여 작업 스케줄링과 관리가 용이해짐

 

320x100

'Course > JSCODE - 자바' 카테고리의 다른 글

4주차 스터디 노트  (1) 2024.09.05
2주차 스터디 노트  (0) 2024.08.22
1주차 스터디 노트  (0) 2024.08.16

댓글