Wrapper 객체
📌 Java에서 기본 데이터 타입(primitive type)을 객체로 다룰 수 있도록 해주는 클래스들을 말합니다. 기본 데이터 타입은 객체가 아니기 때문에 객체로 다루어야 하는 경우에 Wrapper 클래스를 사용합니다.
- 박싱(기본 데이터를 Wrapper 클래스로 변경하는 행위)해서 객체화된 원시 값들은 이제 클래스처럼, 구현되어 있는 메소드들을 자유롭게 이용이 가능하고, 객체만 할 수 있는 것들을 할 수 있게 됩니다.
- generic을 알기 위해서는 Wrapper 객체를 이해하는 것이 좋습니다.
- 좀 더 전문적인 설명으로, 기본 타입은 '값'의 의미로만 사용되지만 객체로서의 기능이 필요한 경우 추상화를 통해 객체의 속성을 부여한다. 즉, 기본 값을 객체화 해서 "감싼다"라는 의미로 이러한 객체를 Wrapper Class라고 한다.
- 추가로 반대로 객체를 값으로 되돌릴 경우 언박싱 이라고 한다.
Integer num = new Integer(17); // Boxing
int n = num.intValue(); // UnBoxing
Character ch = 'X'; // AutoBoxing
char c = ch; // AutoUnBoxing
Generic
📌 Java에서 타입을 매개변수[메서드나 함수에 전달되는 값 또는 정보 ]로 사용할 수 있게 해주는 기능입니다. 즉, 클래스를 만들 때, 어떤 타입을 사용할지 미리 정의하지 않고 나중에 사용할 때 구체적인 타입을 지정할 수 있게 하는 방법입니다.
= 타입을 먼저 정하지 않고 나중에 정할 수 있게 해준다. 코드 재사용성, 안정성 보장에 큰 역활을 한다.
public class Generic {
public String plusReturnFunction(int a, int b) { ... }
}
//=
public class Generic {
public Object plusReturnFunction(Object a,Object b) { ... }
}
- 데이터를 객체로서 만들어 사용하는 것은 메소드 사용이나 다른 기능적인 면에서 유용하지만
- 기본적으로 타입 안정성이 침해받는다(int는 정수형 이라는 규정을 깨고 애매한 형태의 타입을 사용하니..)
- 추상화로 인해 타입의 명확성이 떨어진 만큼 연산자의 사용도 당연히 불가능해진다.
타입 변수
제네릭에서 사용되는 변수로, 타입 매개변수라고도 불립니다. 타입 변수는 클래스, 메서드, 또는 인터페이스에서 사용될 타입을 일반화하여 정의한 변수입니다. 즉, 구체적인 타입을 지정하지 않고, 유연하게 다양한 타입을 처리할 수 있도록 하는 역할을 합니다.
- 아래의 T를 말합니다.
// 1.
public class Generic<T> {
// 2.
private T t;
// 3.
public T get() {
return this.t;
}
public void set(T t) {
this.t = t;
}
public static void main(String[] args) {
// 4.
Generic<String> stringGeneric = new Generic<>();
// 5.
stringGeneric.set("Hello World");
String tValueTurnOutWithString = stringGeneric.get();
System.out.println(tValueTurnOutWithString);
}
}
- 제네릭은 클래스 또는 메서드에 사용할 수 있습니다. 클래스 이름 뒤에 <> 문법 안에 들어가야 할 타입 변수를 지정합니다.
- 🤔 타입 변수의 이름을 T로 사용하는 이유는 일종의 컨벤션이기 때문입니다. 당연히 컨벤션이기 때문에, 여러분이 원하시는 어떠한 변수를 넣어도 문제가 없습니다. 다만 당연히 컨벤션이기 때문에, 굳이 다른 이유가 없다면 T를 사용하는 게 협업에 유리하겠죠? 이와 함께 자주 사용되는 변수명으로는 T,U,V, E 등이 있습니다.
- 선언 해둔 타입 변수는 해당 클래스 내에서 특정한 타입이 들어갈 자리에 대신 들어갈 수 있습니다. 2번에서는 private 프로퍼티인 t의 타입이 들어가야 할 자리에 들어갔네요
- 메서드의 리턴 타입에 들어가는 것 역시 마찬가지입니다.
- 여기부터는 제네릭을 통해 구현한 클래스를 사용하는 부분입니다, 클래스에 선언했기 때문에 인스턴스를 만들기 위해서 타입 변수에 들어갈 실제 변수의 값을 넣어줘야 합니다. 여기서는 String이네요
- 아까 타입 변수로 대체해뒀던 곳에 String이 들어가 있기 때문에, 이와 같이 사용할 수 있습니다.
Generic 용어
Box<T> | 제네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다. |
T | 타입 변수 또는 타입 매개변수. (T는 타입 문자) |
Box | 원시 타입(raw type) |
제네릭 타입 호출 | 타입 변수에 타입을 지정하는 것을 말한다. |
매개변수화된 타입 | 지정된 타입(String 같은 것들) |
- 컴파일 전까지는 Menu<Coffee>는 여전히 제네릭 타입으로 기능하고, 존재하지만
- 컴파일 이후 '원시 타입'인 Menu로 바뀐다.
- 즉, 제네릭 타입은 제거된다.
Generic 다형성
제네릭 클래스의 객체를 생성할 때 참조 변수에 지정한 제네릭 타입과 생성자에 지정한 제네릭 타입은 일치해야 한다는 점에서 제네릭 타입은 상속 관계를 고려하지 않습니다. 하지만 타입 매개변수를 사용하는 제네릭 클래스에서 상속 관계에 있는 자식 객체를 저장할 수 있는 방법도 있습니다. 다만, 저장할 때와 꺼낼 때 주의해야 합니다.
1. 제네릭 클래스의 객체 생성 시 제네릭 타입 일치
제네릭 클래스는 타입 안전성을 제공하기 위해 참조 변수와 생성자에서 지정한 타입이 일치해야 합니다. 예를 들어:
// 클래스 상속 구조
public class Menu { ... }
public class CoffeeMenu extends Menu { ... }
// 제네릭 객체 생성 시
ArrayList<Menu> menus1 = new ArrayList<Menu>(); // 올바름
ArrayList<Menu> menus2 = new ArrayList<CoffeeMenu>(); // 에러: 타입 불일치
여기서 ArrayList<Menu>와 ArrayList<CoffeeMenu>는 타입이 불일치합니다. **ArrayList<Menu>**는 Menu 타입의 객체만 저장할 수 있으며, **ArrayList<CoffeeMenu>**는 CoffeeMenu 객체만 저장할 수 있습니다. 다형성을 적용하려면 제네릭 타입이 일치해야 하므로, menus2는 ArrayList<Menu>로 선언해야 합니다.
2. 제네릭 클래스에서 다형성 사용
제네릭 클래스에서 타입 매개변수는 상속 관계를 반영하지 않지만, 타입이 일치하는 경우에는 다형성을 적용할 수 있습니다. List<Menu>로 선언하고, CoffeeMenu 객체를 추가할 수 있습니다. 이는 제네릭 타입이 아닌 클래스 타입 간 다형성을 적용한 예입니다.
List<Menu> menus = new ArrayList<Menu>(); // List<Menu>로 선언
menus.add(new CoffeeMenu()); // OK: CoffeeMenu는 Menu의 자식 클래스
3. 형변환 필요
하지만 제네릭 타입의 타입 안전성을 유지하기 위해, 저장된 객체를 꺼낼 때에는 형변환이 필요합니다. ArrayList<Menu>에 저장된 객체를 CoffeeMenu 타입으로 꺼낼 때는 형변환을 해줘야 합니다.
List<Menu> menus = new ArrayList<Menu>();
menus.add(new CoffeeMenu()); // OK: CoffeeMenu는 Menu의 자식 클래스
// 꺼낼 때 형변환이 필요
CoffeeMenu coffeeMenu = (CoffeeMenu) menus.get(0); // 형변환 필요
형변환을 통해 Menu 객체를 CoffeeMenu 객체로 변환할 수 있지만, 런타임 오류가 발생할 수 있습니다. 만약 menus에 CoffeeMenu 객체가 아닌 다른 Menu 객체가 저장되어 있으면 ClassCastException이 발생합니다.
제네릭의 제한
1. 객체의 static 멤버에 사용할 수 없습니다.
static T get() { ... } // 에러
static void set(T t) { ... } // 에러
- 타입 변수는 인스턴스 변수로 간주되고, 모든 객체에 동일하게 동작해야 하는 static 필드 특성상 사용할 수 없습니다
2. 제네릭 배열을 생성할 수 없습니다.
제네릭의 문법
1. 다수의 타입 변수를 사용할 수 있습니다.
public class Generic<T, U, E> {
public E multiTypeMethod(T t, U u) { ... }
}
Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);
2. 다형성 즉 상속과 타입의 관계는 그대로 적용됩니다.
- 대표적으로 부모 클래스로 제네릭 타입 변수를 지정하고, 그 안에 자식 클래스를 넘기는 것은 잘 동작합니다.
3. 와일드카드를 통해 제네릭의 제한을 구체적으로 정할 수 있습니다.
public class ParkingLot<T extends Car> { ... }
ParkingLot<BMW> bmwParkingLot = new ParkingLot();
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // error!
- <? extends T> : T와 그 자손들만 사용 가능
- <? super T> : T와 그 조상들만 가능
- <?> : 제한 없음
이렇게 제한을 하는 이유는 다형성 때문입니다. 위의 코드에서, T는 Car의 자손 클래스들이라고 정의했기 때문에, 해당 클래스 내부에서 최소 Car 객체에 멤버를 접근하는 코드를 적을 수 있습니다. 반대로 그러한 코드들이 있을 여지가 있기 때문에, Car 객체의 자손이 아닌 클래스는 제한하는 것이죠
**와일드카드(Wildcard)**는 제네릭에서 사용되는 특별한 기호(?)로, 타입을 불특정으로 지정할 때 사용됩니다. 와일드카드는 타입을 유연하게 다룰 수 있도록 도와주며, extends나 super와 함께 사용하여 타입을 제한할 수 있습니다.
4. 메서드를 스코프로 제네릭을 별도로 선언할 수 있습니다.
// 또는 ..
static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
- 이렇게 반환 타입(T) 앞에 <> 제네릭을 사용한 경우, 해당 메서드에만 적용되는 제네릭 타입 변수를 선언할 수 있습니다.
- 타입 변수를 클래스 내부의 인스턴스 변수 취급하기 때문에 제네릭 클래스의 타입 변수를 static 메서드에는 사용할 수 없었지만, 제네릭 메소드의 제네릭 타입 변수는 해당 메소드에만 적용되기 때문에 메소드 하나를 기준으로 선언하고 사용할 수 있습니다.
- 같은 이름의 변수를 사용했다고 해도 제네릭 메소드의 타입 변수는 제네릭 클래스의 타입 변수와 다릅니다.
public class Generic<T, U, E> {
// Generic<T,U,E> 의 T와 아래의 T는 이름만 같을뿐 다른 변수
static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
}
스코프
📌 프로그래밍에서 변수나 함수, 클래스 등의 유효 범위를 의미합니다. 즉, 코드 내에서 어떤 변수나 함수가 접근 가능한 영역을 말합니다.
스코프의 종류
- 전역 스코프(Global Scope): 프로그램 전체에서 접근할 수 있는 범위입니다. 전역 변수나 전역 함수가 여기에 해당합니다. 예를 들어, 클래스의 필드나 정적 변수들이 전역 스코프에 속합니다.
- 지역 스코프(Local Scope): 특정 함수나 메서드, 블록 내에서만 유효한 범위입니다. 함수 안에 선언된 변수나 매개변수는 그 함수 내부에서만 접근할 수 있습니다.
- 블록 스코프(Block Scope): 특정 코드 블록(if, for, while 등의 제어문) 내에서만 유효한 범위입니다. let이나 const로 선언된 변수는 이 블록 내에서만 접근 가능합니다.
- 클래스 스코프(Class Scope): 클래스 내에서 정의된 필드나 메서드는 그 클래스 내부에서만 접근할 수 있습니다.
public class ScopeExample {
// 전역 변수
static int globalVar = 10;
public static void main(String[] args) {
// main 메서드 내에서만 유효한 지역 변수
int localVar = 5;
// if문 안에서만 유효한 블록 변수
if (true) {
int blockVar = 20;
System.out.println(blockVar); // 출력 가능
}
System.out.println(globalVar); // 출력 가능
System.out.println(localVar); // 출력 가능
// System.out.println(blockVar); // 오류! blockVar는 if문 블록 밖에서는 접근 불가
}
}
코드 설명
- 전역 변수(globalVar): globalVar는 클래스 어디서든지 접근할 수 있는 변수입니다. 여기서는 main 메서드 내에서 사용됩니다.
- 지역 변수(localVar): localVar는 main 메서드 내에서만 사용 가능하고, 다른 메서드나 클래스 바깥에서는 접근할 수 없습니다.
- 블록 변수(blockVar): blockVar는 if문 블록 내에서만 유효한 변수로, 블록 외부에서는 접근할 수 없습니다.
제네릭에서의 스코프
제네릭에서 스코프는 제네릭 타입 매개변수가 유효한 범위를 나타냅니다. 예를 들어, 클래스 레벨에서 제네릭을 선언하면, 그 클래스 내에서 제네릭 타입을 사용할 수 있고, 메서드 레벨에서 제네릭을 선언하면, 그 메서드 내에서만 제네릭 타입을 사용할 수 있습니다.
public class MyClass<T> {
T obj; // 클래스 스코프
public <E> void print(E element) { // 메서드 스코프
System.out.println(element);
}
}
- 클래스 스코프: 클래스 자체에 제네릭 타입 T를 선언하면, 클래스 내부의 모든 메서드와 필드에서 T를 사용할 수 있습니다.
- 메서드 스코프: 메서드 내에서 제네릭 타입 E를 선언하면, 그 메서드 내에서만 E를 사용할 수 있습니다.
자료구조
1. 자료구조와 인터페이스
- 자바의 **컬렉션(Collection)**은 컴퓨터공학의 자료구조 개념을 추상화하여 구현한 것입니다.
- 자바의 인터페이스는 실제 구현이 없고, 추상적인 명세(규격)만을 정의합니다. 즉, 인터페이스는 "어떤 행동을 해야 한다"라는 규칙만 제시하고, 실제 동작은 이를 구현한 클래스에서 정의됩니다.
- 예를 들어, List 인터페이스는 순서를 가지며 중복을 허용하는 자료구조의 규칙을 정의하고, 이를 구현하는 클래스(예: ArrayList, LinkedList)는 이 규칙을 따릅니다.
2. 배열 vs. 리스트
- 배열은 메모리에서 연속적인 공간을 차지하고, 인덱스를 사용해 빠르게 접근할 수 있지만, 중간에 데이터를 추가하거나 삭제할 때 비용이 큽니다.
- 리스트는 배열처럼 순서를 가지고 있지만, 배열과 달리 동적 크기를 가지며, 특정 연산에 따라 구현체가 달라질 수 있습니다.
3. 제네릭과 자료구조
- 제네릭은 자료구조에 저장되는 데이터 타입을 유연하게 정의할 수 있도록 합니다. 예를 들어, List<Integer>는 Integer 타입만 저장할 수 있도록 제한하고, List<String>은 String 타입만 저장할 수 있습니다.
- List<E>와 같은 제네릭 인터페이스는 저장되는 데이터 타입을 추상화하여, 다양한 타입에 대해 동일한 동작을 적용할 수 있도록 해줍니다.
4. List 인터페이스 예시
- 자바에서 List<E> 인터페이스는 데이터를 저장하고 관리하는 다양한 메서드(add(), size(), isEmpty() 등)를 정의하고 있습니다.
- addAll() 메서드와 같이 와일드카드(? extends E)를 사용하면 다른 타입의 컬렉션을 받아 처리할 수 있습니다.
5. Collection 구조
- 자바의 컬렉션은 Iterable을 상속하고, Collection 인터페이스를 기반으로 하여 List, Queue, Set 등이 구현됩니다.
- 자료구조를 사용할 때는 인터페이스에서 제공하는 기능을 확인하고, 실제 구현체에서 어떻게 동작하는지 살펴보는 것이 중요합니다.
결론
- 자바의 컬렉션은 자료구조의 개념을 추상화하고, 이를 인터페이스와 제네릭을 통해 구현한 것
- 자료구조는 추상적인 타입으로 정의되고, 실제 동작은 구현체에서 정의됩니다. 제네릭을 사용하면 다양한 데이터 타입을 처리할 수 있는 유연성을 제공한다.
'Back-End (Web) > JAVA' 카테고리의 다른 글
[JAVA] NULL (0) | 2024.11.13 |
---|---|
[JAVA] 쓰레드 & 람다 함수 & 스트림 (3) | 2024.11.13 |
[JAVA] 오류 및 예외에 대한 이해 (1) | 2024.11.12 |
[JAVA] 인터페이스 (2) | 2024.11.12 |
[JAVA] 클래스 간의 관계와 상속 (0) | 2024.11.12 |