Back-End (Web)/JAVA

[JAVA] 참조형 자료구조 정리(LIST / STACK / QUEUE / SET / MAP)

JABHACK 2024. 11. 11. 17:30

참조형 변수는 왜 쓸까?

변수 선언에서 보통 참조형 변수가 사람의 머리를 가장 잘 쪼개주는데, 그냥 상자에 물건을 담는게 아니라 물건을 HEAP이라는 상자에 담고 STACK에는 HEAP이라는 상자의 위치(주소)를 저장해 버린다..

 

참조형 변수(배열)

 

사실 생각해보면 이상하다. 그냥 값만 저장하면 1개만 저장하면 되는데, 뭐하러 주소와 값을 같이 저장해서 메모리 공간을 2배로 차지하는 것일까.. 하지만 여기에는 중요한 이유가 있다. 

 

왜 이런 일이 발생했는지 간단히 설명해 보자면 '객체 지향 프로그래밍과 효율적인 메모리 관리를 위해서' 이다. 예를 들어보자

 

1. 효율적인 메모리 관리를 위해

String str = "Hello";

 

여기서 str 변수는 "Hello"라는 값을 직접 저장하는 것이 아니라, "Hello"라는 문자열이 메모리 어딘가에 저장된 주소를 저장한다. 만약 "Hello"라는 문자열을 직접 값으로 저장하려면, "Hello"라는 데이터를 여러 번 복사해야 하는 번거로움이 발생할 수 있는 반면, 참조형 변수는 이 주소만 저장하면 되므로 메모리 절약이 가능하다.

 

= 재활용이 가능하다. hello라는 변수를 5번 선언해 메모리 5칸 먹을 바엔, 5개의 변수에 주소 1개, 값 1개만 할당하면 메모리 2칸만 사용한다는 의미이다.

 

2. 객체 지향 프로그래밍(OOP)의 핵심

이게 무슨 말인지를 알기 위해서는 Java의 핵심인 객체 지향이 뭔지 알아야한다.

 

객체 지향에서 객체속성(데이터)과 행동(기능)을 가지고 있는 구분 가능한 세트를 말한다.

객체 지향

 

예를 들어, 강아지라는 객체는 이름, 나이라는 속성을 가지고, 짖기, 산책하기와 같은 행동을 할 수 있다. 이러한 특징의 세트를 가진 존재를 객체라고 정의한다.

 

Person person1 = new Person("Alice", 30);
Person person2 = person1; // person2는 person1의 주소를 가리킴
person2.setName("Bob");  // person1과 person2 모두 "Bob"을 가리키게 됨

위 코드에서 사람이라는 객체는 (앨리스, 30)이라는 속성을 가지고 있다. 그리고 그 중 이름을 bob으로 수정하는 코드이다.

 

만약에 위와 같이 객체의 내용을 바꿔야할 때 마다. 객체를 복사해야한다면 어떨까?

1. 객체를 복사한다
2. 복사된 객체의 정보를 저장한다
3. 원본 객체를 지운다.

 

이 과정을 거친다. 이 과정은 메모리 사용량을 늘리고, 복잡한 코드를 작성하게 만든다. 차라

1. 객체 중 이름의 변수 내용을 수정한다
2. 수정한 내용을 저장한다.

 

이게 더 편하고 유용하다는 것.

 

 

정말 간단하게 이해하자면

상황 a 아파트에 배달을 해야하는데, 아파트의 이름이 b로 바뀌어야한다.
객체 지향 배달원에게 아파트이름이 a->b로 바뀌었다고 알려준다
절차적 프로그래밍 아파트 b를 새로 만들고 아파트a를 부순다.

 

아무리 그래도 아파트를 부수고 다시 건축하는 방식보단 주소를 변경해주는 것이 여러 의미로 이로울 것이다;;

 

결론적으로 위와 같은 객체 지향적 프로그래밍과 효율적인 메모리 관리를 위해 참조형 변수를 사용하게 되었다는 것

 

 

Java의 Collection

  • 데이터를 저장하고 다룰 수 있는 다양한 방법을 제공하는 인터페이스클래스들의 집합으로, 자바에서 Collection Framework는 데이터를 효율적으로 다룰 수 있도록 도와주는 핵심적인 역할을 하며, 여러 가지 자료구조와 알고리즘을 제공한다.
  • 대표적으로 List, Set, Queue, Map등 다양한 종류가 있다.
  • Collection은 기본형 변수가 아닌 참조형 변수를 저장한다.

Collection

 

List

  • 순서가 있는 데이터의 집합 (데이터 중복 허용) 으로 배열과 유사하다
    ArrayList

ArrayList

  • 배열은( Array ) 타입과 저장량을 지정하면 변경이 안되며 저장된 내용도 수정이 불가능하다(기본형 변수)
  • 리스트( ArrayList )의 경우 타입, 저장량, 수정 모두 관계없이 동적으로 늘어나고 자유롭게 수정이 가능하다.(참조형 변수) 다만,  배열(Array)처럼 일렬로 데이터를 저장하고 조회하여 순번 값(인덱스)로 값을 하나씩 조회할 수 있다.
// ArrayList 
// (사용하기 위해선 import java.util.ArrayList; 를 추가해야합니다.)
import java.util.ArrayList;

public class Main {

	public static void main(String[] args) {
		ArrayList<Integer> intList = new ArrayList<Integer>(); // 선언 및 생성
		
		intList.add(1);
		intList.add(2);
		intList.add(3);
		
		System.out.println(intList.get(0)); // 1 출력
		System.out.println(intList.get(1)); // 2 출력
		System.out.println(intList.get(2)); // 3 출력
		System.out.println(intList.toString()); // [1,2,3] 출력
		
		intList.set(1, 10); // 1번순번의 값을 10으로 수정합니다.
		System.out.println(intList.get(1)); // 10 출력
		
		
		intList.remove(1); // 1번순번의 값을 삭제합니다.
		System.out.println(intList.toString()); // [1,3] 출력
		
		intList.clear(); // 전체 값을 삭제합니다.
		System.out.println(intList.toString()); // [] 출력
	}
}
더보기

기능

  • 선언 : ArrayList<Integer> intList 형태로 선언합니다.
  • 생성 : new ArrayList<Integer>(); 형태로 생성합니다.
  • 초기화 : 사이즈를 지정하는 것이 없기 때문에 초기화가 필요 없습니다.
  • 값 추가 : intList.add({추가할 값}) 형태로 값을 추가합니다.
  • 값 수정 : intList.set({수정할 순번}, {수정할 값}) 형태로 값을 수정합니다.
  • 값 삭제 : intList.remove({삭제할 순번}) 형태로 값을 삭제합니다.
  • 전체 출력 : intList.toString() 형태로 전체 값을 대괄호[]로 묶어서 출력합니다.
  • 전체 제거 : intList.clear() 형태로 전체 값을 삭제합니다.

 

LinkedList

LinkedList

  • 메모리에 남는 공간을 요청해서 여기저기 나누어서 실제 값을 담아 놓고, 실제 값이 있는 주소값으로 목록을 구성하고 저장합니다. (남는 공간 아무대나 저장하고 데이터 위치를 주소로 찾는다)
  • 기본적인 기능은 ArrayList 와 동일하지만 LinkedList는 값을 나누어 담기 때문에 모든 값을 조회하는 속도가 느립니다. 대신에, 값을 중간에 추가하거나 삭제할 때는 속도가 빠릅니다.
  • 중간에 값을 추가하는 기능이 있습니다. (여기선 속도 빠르다. 순서 지정 없이 아무대나 막 저장해서)
// LinkedList 
// (사용하기 위해선 import java.util.LinkedList; 를 추가해야합니다.)
import java.util.LinkedList;

public class Main {

	public static void main(String[] args) {
		LinkedList<Integer> linkedList = new LinkedList<>(); // 선언 및 생성

		linkedList.add(1);
		linkedList.add(2);
		linkedList.add(3);

		System.out.println(linkedList.get(0)); // 1 출력
		System.out.println(linkedList.get(1)); // 2 출력
		System.out.println(linkedList.get(2)); // 3 출력
		System.out.println(linkedList.toString()); // [1,2,3] 출력 (속도 느림)

		linkedList.add(2, 4); // 2번 순번에 4 값을 추가합니다.
		System.out.println(linkedList); // [1,2,4,3] 출력

		linkedList.set(1, 10); // 1번순번의 값을 10으로 수정합니다.
		System.out.println(linkedList.get(1)); // 10 출력

		linkedList.remove(1); // 1번순번의 값을 삭제합니다.
		System.out.println(linkedList); // [1,4,3] 출력

		linkedList.clear(); // 전체 값을 삭제합니다.
		System.out.println(linkedList); // [] 출력
	}
}
더보기

기능

  • 선언 : LinkedList<Integer> linkedList 형태로 선언합니다.
  • 생성 : new LinkedList<Integer>(); 형태로 생성합니다.
  • 초기화 : 사이즈를 지정하는 것이 없기 때문에 초기화가 필요 없습니다.
  • 값 추가 : linkedList.add({추가할 값}) 형태로 값을 추가합니다.
  • 값 중간에 추가 : linkedList.add({추가할 순번}, {추가할 값}) 형태로 값을 중간에 추가합니다.
  • 값 수정 : linkedList.set({수정할 순번}, {수정할 값}) 형태로 값을 수정합니다.
  • 값 삭제 : linkedList.remove({삭제할 순번}) 형태로 값을 삭제합니다.
  • 전체 출력 : linkedList.toString() 형태로 전체 값을 대괄호[]로 묶어서 출력합니다.
  • 전체 제거 : linkedList.clear() 형태로 전체 값을 삭제합니다.

 

stack 

  • 수직으로 쌓아놓고 넣었다가 빼서 조회하는 형식으로 데이터를 관리하는 방식
  • 이걸 “나중에 들어간 것이 가장 먼저 나온다(Last-In-First-out)” 성질을 가졌다고 표현한다.
  • 넣는 기능(push()) 과 조회(peek()), 꺼내는(pop()) 기능만 존재한다.

Stack

 

스택을 사용하는 이유:
  1. 간단한 메모리 관리
    스택은 후입선출(LIFO) 구조로 동작하는데, 이는 함수 호출이나 작업을 관리하는 데 유용합니다. 프로그램에서 함수가 호출될 때마다 그 함수의 실행 정보를 스택에 저장하고, 함수가 끝나면 그 정보를 스택에서 꺼내는 방식으로 동작합니다. 이를 통해 함수의 호출 및 반환을 효율적으로 관리할 수 있습니다.
  2. 재귀 알고리즘 처리
    재귀 함수는 자기 자신을 호출하는 방식으로 동작합니다. 재귀 함수 호출 시, 각 호출의 상태를 스택에 저장하며, 함수 호출이 끝나면 스택에서 해당 상태를 꺼내어 돌아가게 됩니다. 스택은 재귀 함수의 동작을 간단하게 처리할 수 있게 도와줍니다.
  3. 작업의 순서 보장
    스택은 마지막에 추가된 데이터를 먼저 꺼내기 때문에, 작업의 순서를 보장하는 데 유용합니다. 예를 들어, 웹 브라우저의 뒤로 가기 버튼은 이전 페이지의 URL을 스택에 저장하고, 뒤로 가기 버튼을 클릭하면 스택에서 URL을 꺼내서 이전 페이지로 돌아갑니다.
  4. Undo/Redo 기능
    스택은 되돌리기(Undo)와 다시 하기(Redo) 기능을 구현하는 데 매우 유용합니다. 예를 들어, 문서 편집 프로그램에서 사용자가 어떤 작업을 실행할 때마다 그 작업을 스택에 저장하고, Undo 버튼을 클릭하면 마지막 작업을 꺼내서 이전 상태로 되돌릴 수 있습니다. 다시 하기 기능은 Undo 스택을 이용해 처리할 수 있습니다.
  5. 괄호 짝 맞추기
    컴파일러나 코드 분석기에서 스택을 사용하여 괄호의 짝을 맞추는 작업을 처리합니다. 괄호가 열릴 때마다 스택에 저장하고, 괄호가 닫힐 때마다 스택에서 꺼내는 방식으로 짝을 맞출 수 있습니다.

위의 기능을 많이 이야기 하지만, 가장 중요한 점은 최근 저장된 데이터를 나열하고 싶거나 데이터의 중복 처리를 막고 싶을 때 사용한다.

 

// Stack 
// (사용하기 위해선 import java.util.Stack; 를 추가해야합니다.)
import java.util.Stack;

public class Main {

	public static void main(String[] args) {
		Stack<Integer> intStack = new Stack<Integer>(); // 선언 및 생성
		
		intStack.push(1);
		intStack.push(2);
		intStack.push(3);

		while (!intStack.isEmpty()) { // 다 지워질때까지 출력
		    System.out.println(intStack.pop()); // 3,2,1 출력
		}

		// 다시 추가
		intStack.push(1);
		intStack.push(2);
		intStack.push(3);
		
		// peek()
		System.out.println(intStack.peek()); // 3 출력
		System.out.println(intStack.size()); // 3 출력 (peek() 할때 삭제 안됬음)
		
		// pop()
		System.out.println(intStack.pop()); // 3 출력
		System.out.println(intStack.size()); // 2 출력 (pop() 할때 삭제 됬음)		
		
		System.out.println(intStack.pop()); // 2 출력
		System.out.println(intStack.size()); // 1 출력 (pop() 할때 삭제 됬음)		

		while (!intStack.isEmpty()) { // 다 지워질때까지 출력
		    System.out.println(intStack.pop()); // 1 출력 (마지막 남은거 하나)
		}
	}
}
더보기

기능

  • 선언 : Stack<Integer> intStack 형태로 선언합니다.
  • 생성 : new Stack<Integer>(); 형태로 생성합니다.
  • 추가 : intStack.push({추가할 값}) 형태로 값을 추가합니다.
  • 조회 : intStack.peek() 형태로 맨 위값을 조회합니다.
  • 꺼내기 : intStack.pop() 형태로 맨 위값을 꺼냅니다. (꺼내고 나면 삭제됨)

Queue

  • 빨대처럼 한쪽에서 데이터를 넣고 반대쪽에서 데이터를 뺄 수 있는 집합이다.
  • 편의점 알바에서 많이 듣게 되는 선입선출(Fist In First Out)을 성질로 갖고 있는 Collection
  • 넣는 기능(add()) 과 조회(peek()), 꺼내는(poll()) 기능만 존재한다.

 

  • Queue는 생성자가 없는 껍데기라서 바로 생성할 수는 없습니다. (껍데기 = 인터페이스)
  • 생성자가 존재하는 클래스인 LinkedList를 사용하여 Queue를 생성해서 받을 수 있습니다.
// LinkedList 를 생성하면 Queue 기능을 할 수 있습니다. (Queue 가 부모/ LinkedList 가 자식이기 떄문)
Queue<Integer> intQueue = new LinkedList<Integer>();

 

이걸 이해하기 위해서는 인터페이스와 생성자, 오버로딩의 개념을 알 필요가 있다.

 

인터페이스 (Interface) = 설계도

  • 자바에서 클래스가 구현해야 하는 계약을 정의하는 구조로 쉽게 말해, 인터페이스는 "어떤 메서드들이 있어야 한다"고 약속만 정의하고, 그 구현 내용은 다른 클래스에서 작성합니다.
  • 말 그대로 클래스의 설계도로, 아래의 Animal 처럼 Animal 인터페이스에 포함될 클래스는 반드시 sound라는 메서드를 포함 시켜 주세요라는 설계도를 말한다.
// Animal 인터페이스
interface Animal {
    void sound();  // sound 메서드만 선언, 구현은 강제
}

// Dog 클래스는 Animal 인터페이스를 구현해야 하므로 sound 메서드를 반드시 구현해야 한다.
class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Bark");
    }
}

// Cat 클래스도 Animal 인터페이스를 구현해야 하므로 sound 메서드를 반드시 구현해야 한다.
class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        
        dog.sound();  // Bark
        cat.sound();  // Meow
    }
}

 

생성자 (Constructor)

  • 객체가 생성될 때 초기화 작업을 담당하는 특별한 메서드입니다. 자바에서 생성자는 클래스 이름과 동일하고, 반환 값이 없습니다(즉, void도 사용하지 않습니다). 생성자는 객체가 생성될 때 자동으로 호출되며, 클래스의 속성(멤버 변수)을 초기화하는 데 사용됩니다.
  • 말이 어려운데, 객체의 기본 초기값을 설정하여 객체를 추가로 생성해도 값이 미리 지정해 둔 값으로 초기화 될 수 있도록 한 메소드

생성자의 특징:

  1. 클래스 이름과 동일한 이름을 가집니다.
  2. 반환 타입이 없습니다 (반환 타입으로 void도 사용하지 않습니다 = 반환이 필요 없다).
  3. 자동 호출됩니다: 객체가 생성될 때마다 생성자가 자동으로 실행됩니다. (미리 지정해 두었으니..)
  4. 생성자는 오버로딩할 수 있습니다: 같은 클래스 내에서 여러 개의 생성자를 정의할 수 있습니다. 이때, 매개변수의 개수나 타입을 달리하여 구별합니다.

기본 생성자 (Default Constructor)

  • 생성자를 명시적으로 정의하지 않으면, 자바 컴파일러가 기본 생성자(default constructor)를 자동으로 제공합니다.
  • 기본 생성자는 매개변수가 없고, 객체를 생성할 때 기본적인 초기화 작업을 합니다.
class Person {
    String name;
    int age;

    // 기본 생성자
    public Person() {
        name = "Unknown";
        age = 0;
    }

    // 매개변수가 있는 생성자 (이름만 받는 생성자)
    public Person(String name) {
        this.name = name;
        this.age = 0;  // 나이는 기본값 0
    }

    // 매개변수가 있는 생성자 (이름과 나이 받는 생성자)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 메서드
    public void introduce() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

public class Main {
    public static void main(String[] args) {
        // 기본 생성자 사용
        Person person1 = new Person();
        person1.introduce();  // 이름: Unknown, 나이: 0

        // 이름만 받는 생성자 사용
        Person person2 = new Person("Alice");
        person2.introduce();  // 이름: Alice, 나이: 0

        // 이름과 나이를 받는 생성자 사용
        Person person3 = new Person("Bob", 25);
        person3.introduce();  // 이름: Bob, 나이: 25
    }
}

 

 

오버로딩

  • 같은 이름의 메서드나 생성자 매개변수의 개수나 타입이 달라지도록 여러 번 정의하는 기법입니다. 오버로딩을 사용하면 같은 이름으로 다양한 작업을 처리할 수 있습니다.

오버로딩의 특징

  1. 같은 이름의 메서드나 생성자지만 매개변수의 개수나 타입이 달라야 합니다.
  2. 반환 타입은 오버로딩을 구별하는 기준이 되지 않습니다.
  3. 컴파일 시점에 메서드 호출을 구분하기 때문에, 실행 시간에 결정되는 것이 아니라 호출 시점에서 적절한 메서드가 선택됩니다.

오버로딩의 장점

  • 코드의 가독성 향상: 비슷한 작업을 수행하는 메서드를 같은 이름으로 처리할 수 있어 코드가 깔끔하고 이해하기 쉬워집니다.
  • 유지보수 용이성: 같은 이름의 메서드를 사용하여 여러 매개변수에 대한 처리 로직을 관리할 수 있어 유지보수가 용이합니다.

오버로딩 규칙

  1. 매개변수의 타입이 달라야 오버로딩으로 인식됩니다.
  2. 매개변수의 개수가 달라야 오버로딩으로 인식됩니다.
  3. 매개변수의 순서가 달라야 오버로딩으로 인식됩니다.
class Person {
    String name;
    int age;

    // 기본 생성자
    public Person() {
        name = "Unknown";
        age = 0;
    }

    // 매개변수가 있는 생성자 (이름만 받는 생성자)
    public Person(String name) {
        this.name = name;
        this.age = 0;  // 나이는 기본값 0
    }

    // 매개변수가 있는 생성자 (이름과 나이 받는 생성자)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 메서드
    public void introduce() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

public class Main {
    public static void main(String[] args) {
        // 기본 생성자 사용
        Person person1 = new Person();
        person1.introduce();  // 이름: Unknown, 나이: 0

        // 이름만 받는 생성자 사용
        Person person2 = new Person("Alice");
        person2.introduce();  // 이름: Alice, 나이: 0

        // 이름과 나이를 받는 생성자 사용
        Person person3 = new Person("Bob", 25);
        person3.introduce();  // 이름: Bob, 나이: 25
    }
}

 

< 출력 결과 >

이름: Unknown, 나이: 0
이름: Alice, 나이: 0
이름: Bob, 나이: 25
더보기

이해를 위한 오버로딩의 어원
**오버로딩(Overloading)**이라는 용어는 **'과잉(Over)'**과 **'적재(Loading)'**의 결합으로, 프로그래밍에서 특정 작업이나 기능이 여러 방식으로 "과잉"으로 제공된다는 개념을 나타냅니다.

 

즉, **오버로딩(Overloading)**은 하나의 메소드나 생성자가 여러 형태로 존재한다는 개념입니다. 메소드 이름은 같지만, 매개변수의 수나 타입에 따라 서로 다른 구현이 가능하다는 뜻입니다. 이 개념은 하나의 이름으로 여러 기능을 제공하는 것으로, "과잉 적재"의 개념을 표현하고 있습니다.

 

< 위 예시의 해석입니다. >

오버로딩된 생성자들
1. **기본 생성자** (`Person()`):
   - 매개변수가 없는 생성자입니다.
   - `name`을 `"Unknown"`으로, `age`를 `0`으로 초기화합니다.

2. **이름만 받는 생성자** (`Person(String name)`):
   - 하나의 매개변수(`String name`)만 받는 생성자입니다.
   - `name`을 매개변수로 받아 초기화하고, `age`는 기본값인 `0`으로 초기화합니다.

3. **이름과 나이를 받는 생성자** (`Person(String name, int age)`):
   - 두 개의 매개변수(`String name`, `int age`)를 받는 생성자입니다.
   - `name`과 `age`를 매개변수로 받아 초기화합니다.

오버로딩 설명
**생성자 오버로딩**은 **같은 이름**의 생성자를 **매개변수의 개수나 타입이 다르게 정의**하여, 객체를 생성할 때 다양한 방법으로 초기화를 할 수 있게 해주는 기능입니다. 위 코드에서 `Person` 클래스의 생성자는 **세 가지 형태로 오버로딩**되어 있으며, 각기 다른 방식으로 객체를 초기화할 수 있습니다.

어떻게 오버로딩이 이루어졌는지

1. **기본 생성자** (`Person()`):
   - `Person person1 = new Person();`  
     이 코드에서는 매개변수가 없는 기본 생성자가 호출됩니다. 그래서 `name`은 `"Unknown"`, `age`는 `0`으로 초기화됩니다.

2. **이름만 받는 생성자** (`Person(String name)`):
   - `Person person2 = new Person("Alice");`  
     이 코드에서는 이름만 받는 생성자가 호출됩니다. `name`은 `"Alice"`, `age`는 기본값인 `0`으로 초기화됩니다.

3. **이름과 나이를 받는 생성자** (`Person(String name, int age)`):
   - `Person person3 = new Person("Bob", 25);`  
     이 코드에서는 이름과 나이를 받는 생성자가 호출됩니다. `name`은 `"Bob"`, `age`는 `25`로 초기화됩니다.

생성자 오버로딩의 장점:
- **유연성**: 같은 클래스의 객체를 다양한 방법으로 생성할 수 있습니다. 예를 들어, `Person` 클래스의 객체를 `name`만 받거나, `name`과 `age`를 함께 받거나, 기본값을 사용하여 객체를 생성할 수 있습니다.
- **코드의 가독성 향상**: 사용자가 원하는 방식으로 객체를 생성할 수 있기 때문에, 코드가 더 직관적이고 가독성이 좋습니다.

오버로딩의 규칙:
- **매개변수의 개수**가 다르면 오버로딩된 생성자로 인식됩니다.
- **매개변수의 타입**이 다르면 오버로딩된 생성자로 인식됩니다.
- **매개변수의 순서**가 다르면 오버로딩된 생성자로 인식됩니다.

 

 

자 이제 다시한번 Queue에 관한 설명을 읽어보자

  • Queue는 생성자가 없는 껍데기라서 바로 생성할 수는 없습니다. (껍데기 = 인터페이스)
  • 생성자가 존재하는 클래스인 LinkedList를 사용하여 Queue를 생성해서 받을 수 있습니다.

= 설계도를 생성하고 그 안에 값을 지정해 주는 생성자가 포함된 클래스를 통해 Queue를 생성

= Queue<String> queue = new LinkedList<>();

 

= Queue라는 설계도(인터페이스)를 바탕으로 LinkedList 메소드를 사용하여 큐를 초기화하여 String 타입을 처리하는 queue큐를 생성해라

 

= Queue라는 설계도(건설업장의 규칙 = 먼저 온 사람이 먼저 일한다 fist in first out )를 바탕으로 LinkedList 메소드(도구 = 곡괭이를 쓸지 드릴을 사용할지 [배열 기반의 Queue , 우선순위 Queue와 같은 방식들])를 사용하여 큐를 초기화하여 String 타입을 처리하는 queue큐를 생성해라

 

// Queue 
// (사용하기 위해선 java.util.LinkedList; 와 import java.util.Queue; 를 추가해야합니다.)
import java.util.LinkedList;
import java.util.Queue;

public class Main {

    public static void main(String[] args) {
        // Queue 인터페이스를 구현한 LinkedList 클래스를 사용
        Queue<String> queue = new LinkedList<>();

        // 큐에 데이터 삽입 (enqueuing)
        queue.offer("Apple");
        queue.offer("Banana");
        queue.offer("Cherry");

        // 큐에서 데이터 꺼내기 (dequeuing)
        System.out.println("First item: " + queue.poll()); // Apple
        System.out.println("Second item: " + queue.poll()); // Banana

        // 큐에 또 추가
        queue.offer("Grapes");

        // 큐에서 데이터 꺼내기
        System.out.println("Third item: " + queue.poll()); // Cherry
        System.out.println("Fourth item: " + queue.poll()); // Grapes

        // 큐가 비었는지 확인
        if (queue.isEmpty()) {
            System.out.println("큐가 비었습니다.");
	}
}
더보기

기능

  • 선언 : Queue<Integer> intQueue 형태로 선언합니다.
  • 생성 : new LinkedList<Integer>(); 형태로 생성합니다.
  • 추가 : intQueue.add({추가할 값}) 형태로 값을 맨 위에 추가합니다.
  • 조회 : intQueue.peek() 형태로 맨 아래 값을 조회합니다.
  • 꺼내기 : intQueue.poll() 형태로 맨 아래 값을 꺼냅니다. (꺼내고 나면 삭제됨)

 

SET

  • 순서가 없는 데이터의 집합 (데이터 중복 허용 안 함) - 순서 없고 중복 없는 배열
  • 여기도 Queue와 같이 인터페이스로 생성자가 존재하는 클래스인 HashSet, Set, TreeSet등을 생성해서 만들 수 있다.

  • HashSet : 가장 빠르며 순서를 전혀 예측할 수 없음
  • TreeSet : 정렬된 순서대로 보관하며 정렬 방법을 지정할 수 있음
  • LinkedHashSet : 추가된 순서, 또는 가장 최근에 접근한 순서대로 접근 가능
    즉, 보통 HashSet 을 쓰는데 순서 보장이 필요하면 LinkedHashSet 을 주로 사용한다.

// Set 
// (사용하기 위해선 import java.util.Set; 와 java.util.HashSet; 를 추가해야합니다.)
import java.util.HashSet;
import java.util.Set;

public class Main {

	public static void main(String[] args) {
		Set<Integer> intSet = new HashSet<Integer>(); // 선언 및 생성

		intSet.add(1);
		intSet.add(2);
		intSet.add(3);
		intSet.add(3); // 중복된 값은 덮어씁니다.
		intSet.add(3); // 중복된 값은 덮어씁니다.

		for (Integer value : intSet) {
			System.out.println(value); // 1,2,3 출력
		}

		// contains()
		System.out.println(intSet.contains(2)); // true 출력
		System.out.println(intSet.contains(4)); // false 출력

		// remove()
		intSet.remove(3); // 3 삭제

		for (Integer value : intSet) {
			System.out.println(value); // 1,2 출력
		}
	}
}
더보기

기능

  • 선언 : Set<Integer> intSet 형태로 선언합니다.
  • 생성 : new HashSet<Integer>(); 형태로 생성합니다.
  • 추가 : intSet.add({추가할 값}) 형태로 값을 맨 위에 추가합니다.
  • 삭제 : intSet.remove({삭제할 값}) 형태로 삭제할 값을 직접 지정합니다.
  • 포함 확인 : intSet.contains({포함 확인 할 값}) 형태로 해당 값이 포함되어있는지 boolean 값으로 응답받습니다.

 

Map

  • key-value 구조로 구성된 데이터를 저장한다.
  • 시간복잡도가 1 / 한번에 정보의 위치를 찾을 수 있는 자료구조형태
  • 꼭 주소가 포함되어야 한다는건 아니다 / 근데 보통 구현하면 주소로 구현한다.
  • key는 사실 인덱스와 비슷한 역활을 한다. 즉 절대로 중복되서는 안된다. (주소가 중복되면??)
  • 여기도 HashSet, TreeSet 등으로 응용하여 사용할 수 있다. 즉 여기도 인터페이스


  • HashMap : 중복을 허용하지 않고 순서를 보장하지 않음, 키와 값으로 null이 허용
  • TreeMap : key 값을 기준으로 정렬을 할 수 있습니다. 다만, 저장 시 정렬(오름차순)을 하기 때문에 저장시간이 다소 오래 걸림 
// Map 
// (사용하기 위해선 import java.util.Map; 를 추가해야합니다.)
import java.util.Map;

public class Main {

	public static void main(String[] args) {
		Map<String, Integer> intMap = new HashMap<>(); // 선언 및 생성

		//          키 , 값
		intMap.put("일", 11);
		intMap.put("이", 12);
		intMap.put("삼", 13);
		intMap.put("삼", 14); // 중복 Key값은 덮어씁니다.
		intMap.put("삼", 15); // 중복 Key값은 덮어씁니다.

		// key 값 전체 출력
		for (String key : intMap.keySet()) {
			System.out.println(key); // 일,이,삼 출력
		}

		// value 값 전체 출력
		for (Integer key : intMap.values()) {
			System.out.println(key); // 11,12,15 출력
		}

		// get()
		System.out.println(intMap.get("삼")); // 15 출력
	}
}
더보기

기능

  • 선언 : Map<String, Integer> intMap 형태로 Key타입과 Value타입을 지정해서 선언합니다.
  • 생성 : new HashMap<>(); 형태로 생성합니다.
  • 추가 : intMap.put({추가할 Key값},{추가할 Value값}) 형태로 Key에 Value값을 추가합니다.
  • 조회 : intMap.get({조회할 Key값}) 형태로 Key에 있는 Value값을 조회합니다.
  • 전체 key 조회 : intMap.keySet() 형태로 전체 key 값들을 조회합니다.
  • 전체 value 조회 : intMap.values() 형태로 전체 value 값들을 조회합니다.
  • 삭제 : intMap.remove({삭제할 Key값}) 형태로 Key에 있는 Value값을 삭제합니다.

 

배열의 길이 파악

1. length

  • arrays(int[], double[], String[])
  • length는 배열의 길이를 조회해 줍니다.
int[] numbers = {1, 2, 3, 4, 5};
System.out.println("배열의 길이: " + numbers.length);  // 출력: 배열의 길이: 5

2. length()

  • String related Object(String, StringBuilder etc)
  • length()는 문자열의 길이를 조회해 줍니다. (ex. “ABCD”.length() == 4)
String text = "Hello, World!";
System.out.println("문자열의 길이: " + text.length());  // 출력: 문자열의 길이: 13

3. size()

  • Collection Object(ArrayList, Set etc)
  • size()는 컬렉션 타입 목록의 길이를 조회해 줍니다.
ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
System.out.println("리스트의 크기: " + list.size());  // 출력: 리스트의 크기: 3

 

 

뭔가 많고 CS 지식을 추가로 확보해야하는 내용이었다. 추후 CS관련된 내용도 정리할 필요가 있어보인다....

 

오늘 요약...

import java.util.*;

public class DataStructureExample {
    public static void main(String[] args) {
        // 1. List 사용: 순서가 있는 데이터 저장, 중복 허용
        List<String> fruits = new ArrayList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");
        fruits.add("Apple");  // 중복 허용

        System.out.println("List (Fruits): " + fruits);

        // 2. Stack 사용: LIFO 구조로 데이터 처리
        Stack<String> bookStack = new Stack<>();
        bookStack.push("Book 1");
        bookStack.push("Book 2");
        bookStack.push("Book 3");

        System.out.println("Stack (Books - Last item): " + bookStack.peek());
        System.out.println("Stack (Pop Book): " + bookStack.pop());  // 마지막으로 들어온 데이터 제거
        System.out.println("Stack after pop: " + bookStack);

        // 3. Queue 사용: FIFO 구조로 데이터 처리
        Queue<String> lineQueue = new LinkedList<>();
        lineQueue.offer("Person 1");
        lineQueue.offer("Person 2");
        lineQueue.offer("Person 3");

        System.out.println("Queue (First in line): " + lineQueue.peek());
        System.out.println("Queue (Remove from line): " + lineQueue.poll());  // 처음으로 들어온 데이터 제거
        System.out.println("Queue after poll: " + lineQueue);

        // 4. Set 사용: 중복 없는 데이터 저장
        Set<String> uniqueNames = new HashSet<>();
        uniqueNames.add("Alice");
        uniqueNames.add("Bob");
        uniqueNames.add("Alice");  // 중복 추가 시도 (저장되지 않음)

        System.out.println("Set (Unique Names): " + uniqueNames);

        // 5. Map 사용: 키-값 쌍으로 데이터 저장
        Map<String, Integer> ageMap = new HashMap<>();
        ageMap.put("Alice", 30);
        ageMap.put("Bob", 25);
        ageMap.put("Charlie", 35);

        System.out.println("Map (Ages): " + ageMap);
        System.out.println("Age of Alice: " + ageMap.get("Alice"));  // 특정 키를 이용해 값 가져오기
    }
}

 

 

가비지 컬렉터

자바 프로그램에서 더 이상 사용되지 않는 객체들을 자동으로 메모리에서 해제하는 기능을 담당합니다. 즉, 자바에서는 메모리 관리가 자동으로 이루어지며, 개발자가 명시적으로 메모리를 해제할 필요가 없습니다. 이는 자바의 중요한 특징 중 하나로, 메모리 누수를 방지하고 효율적인 메모리 관리를 가능하게 합니다.

1. 가비지 컬렉터의 역할

가비지 컬렉터는 메모리에서 더 이상 필요하지 않은 객체를 찾아서 자동으로 삭제합니다. 메모리에서 객체를 삭제하는 과정을 **"가비지 수집"**이라고 부릅니다.

2. 가비지 컬렉터가 작동하는 방식

가비지 컬렉터는 객체가 더 이상 참조되지 않거나 접근할 수 없는 상태일 때 그 객체를 가비지로 간주하여 수집합니다. 즉, 객체가 "reachable" 하지 않게 되면 가비지 컬렉터가 이를 회수하고 메모리를 확보합니다.

3. 참조와 가비지 컬렉터

  • 참조(Reference): 객체를 가리키는 포인터나 주소입니다. 예를 들어, Object obj = new Object();에서 obj는 그 객체를 가리키는 참조입니다.
  • 참조 끊김: 객체가 더 이상 참조되지 않으면, 이 객체는 가비지 컬렉터에 의해 수거될 수 있는 상태가 됩니다.
public class GarbageCollectorExample {
    public static void main(String[] args) {
        Object obj1 = new Object(); // obj1이 새 객체를 참조
        Object obj2 = obj1;         // obj2도 동일 객체를 참조
        obj1 = null;                // obj1 참조를 null로 설정, obj2는 여전히 객체를 참조
        obj2 = null;                // obj2도 null로 설정, 이제 객체는 참조되지 않음
        // 이제 객체는 가비지 컬렉터가 회수할 수 있습니다.
    }
}

위 코드에서 obj1과 obj2가 모두 null로 설정되면, 그 객체는 더 이상 참조되지 않으므로 가비지 컬렉터가 이를 회수합니다.

 

4. 가비지 컬렉션의 과정

가비지 컬렉션은 다음과 같은 주요 단계를 거칩니다:

  1. 마킹(Marking):
    • 먼저, **활성 객체(참조되는 객체)**를 찾습니다. 이를 위해 **루트 객체(Root objects)**에서부터 시작해 모든 참조 가능한 객체를 추적합니다.
    • 루트 객체는 스택, 레지스터, static 변수 등에서 참조되는 객체입니다.
  2. 스위핑(Sweeping):
    • 마킹 과정에서 찾지 못한 객체들은 더 이상 사용되지 않으므로 삭제됩니다. 이 단계에서 가비지가 수집됩니다.
  3. 압축(Compaction) (일부 GC에서만 수행):
    • 수거된 가비지 객체들이 있을 경우, 남은 객체들을 메모리의 앞쪽에 압축하여 빈 공간을 없앱니다.
    • 이 과정은 메모리의 단편화(fragmentation) = 연속된 메모리 공간이 부족해져서 메모리를 효율적으로 활용할 수 없는 상태 문제를 해결하는 데 도움을 줍니다.

 

5. 자바에서의 가비지 컬렉션 종류

자바의 JVM(Java Virtual Machine)은 여러 가지 가비지 컬렉터를 지원합니다. 각 가비지 컬렉터는 서로 다른 방식으로 메모리를 관리하므로, 성능과 효율성에 차이가 있습니다.

대표적인 가비지 컬렉터 종류:

  1. Serial GC:
    • 단일 스레드로 작업을 처리하는 간단한 가비지 컬렉터입니다. 메모리 할당과 가비지 수집을 하나의 스레드에서 처리하므로, 멀티코어 환경에서는 효율성이 떨어질 수 있습니다.
    • JVM 옵션: -XX:+UseSerialGC
  2. Parallel GC (Throughput Collector):
    • 여러 스레드를 사용하여 가비지 수집을 병렬로 처리하는 방식입니다. 성능을 중시하며, 대용량 애플리케이션에서 유리합니다.
    • JVM 옵션: -XX:+UseParallelGC
  3. CMS (Concurrent Mark-Sweep) GC:
    • 병렬 및 동시 작업을 통해 짧은 가비지 수집 시간을 목표로 합니다. 특히, 애플리케이션의 일시적인 멈춤 시간을 최소화하려는 목적으로 사용됩니다.
    • JVM 옵션: -XX:+UseConcMarkSweepGC
  4. G1 GC (Garbage First GC):
    • 고급 가비지 컬렉터로, 애플리케이션의 예측 가능한 지연 시간을 보장하고자 설계되었습니다. 작은 단위로 메모리를 수집하고, 각 단위에 대해 우선순위를 두어 수집합니다.
    • JVM 옵션: -XX:+UseG1GC
  5. ZGC (Z Garbage Collector):
    • Low-latency를 제공하는 새로운 가비지 컬렉터로, 애플리케이션의 일시적인 멈춤 시간을 매우 짧게 유지합니다. 큰 힙(heap) 메모리에서 높은 효율성을 보입니다.
    • JVM 옵션: -XX:+UseZGC

6. 가비지 컬렉터와 성능

  • 가비지 컬렉션은 메모리 회수를 자동화하여 프로그램을 편리하게 만들지만, GC 작업실행 도중 애플리케이션을 멈추게 할 수 있기 때문에 성능에 영향을 미칠 수 있습니다. 이 현상을 Stop-the-world라고 합니다.
  • 가비지 컬렉션이 자주 발생하거나 긴 시간이 걸리면, 애플리케이션의 응답성이 저하될 수 있습니다.

7. GC를 강제로 호출

자바에서는 System.gc() 메서드를 사용하여 가비지 컬렉션을 강제로 요청할 수 있습니다. 그러나 이 방법은 권장되지 않으며, JVM이 가비지 컬렉션을 최적화하여 자동으로 처리하는 것이 훨씬 효율적입니다.

System.gc();  // 가비지 컬렉션 강제 호출

 

8. 정리

  • 가비지 컬렉터는 자바 프로그램의 메모리 관리를 자동으로 처리하여, 사용하지 않는 객체를 메모리에서 해제하는 역할을 합니다.
  • 가비지 컬렉션이 어떻게 작동하는지, 그 종류와 성능에 대한 이해는 애플리케이션 성능 최적화 및 안정성 유지에 매우 중요합니다.

 

 

 

누가 이딴거 만들었어....

 

 

 

오늘의 문제...

import java.util.LinkedList;
import java.util.Queue;

class Customer {
    public void purchaseTicket() {
        System.out.println("Customer purchases ticket");
    }
}

class RegularCustomer extends Customer {
    @Override
    public void purchaseTicket() {
        System.out.println("Regular customer purchases ticket");
    }
}

class VipCustomer extends Customer {
    @Override
    public void purchaseTicket() {
        System.out.println("VIP customer purchases ticket with priority");
    }
}

public class TicketQueue {
    public static void main(String[] args) {
        // Queue 생성
        Queue<Customer> ticketQueue = new LinkedList<>();

        // Queue에 고객 추가 (RegularCustomer와 VipCustomer)
        ticketQueue.add(new RegularCustomer());
        ticketQueue.add(new VipCustomer());
        ticketQueue.add(new RegularCustomer());
        ticketQueue.add(new VipCustomer());

        // 각 고객이 차례로 티켓을 구매함
        while (!ticketQueue.isEmpty()) {
            Customer customer = ticketQueue.poll(); // Queue에서 다음 고객을 가져옴
            customer.purchaseTicket(); // 오버라이딩된 메서드 호출
        }
    }
}
더보기

<해답>

Regular customer purchases ticket
VIP customer purchases ticket with priority
Regular customer purchases ticket
VIP customer purchases ticket with priority

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
[JAVA] 객체지향 프로그래밍  (1) 2024.11.12
[JAVA] 배열  (0) 2024.11.11
[JAVA] 반복문  (0) 2024.11.11
[JAVA] 조건문  (0) 2024.11.11