Generics : 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에서 타입을 일반화해서 컴파일 시의 타입 체크를 해주는 기능
class Box<T> {
private T item;
public void set(T item) { this.item = item; }
public T get() { return item; }
}
class FruitBox<T extends Fruit & Eatable> { // 타입을 Fruit의 자손과 Eatable의 구현체로 제한할 수도 있다
// 멤버
}
class Main {
public static void main(String[] args) {
Box<Apple> appleBox = new Box<Apple>();
Box<Grape> grapeBox = new Box<>(); // 변수 타입과 생성자 타입은 일치해야 하므로 생성자에서 생략할 수 있다
}
}
<T extends Class1 & Class2> 는 허용되지 않는다. <T extends Class & Interface1 & Interface2> 는 가능하다T[] arr = new T[100]; // Xnew는 컴파일 시점에 타입이 무엇인지 정확히 알아야 하는데 타입 변수 T는 런타임에 결정되므로 사용할 수 없다와일드 카드(Wild Card) : Generic 타입을 다룰 때 정확한 타입 지정 없이 사용하고 싶을 때 유연하게 타입을 받을 수 있게 하는 문법
변수나 반환값의 타입에도 사용할 수 있지만 추천되지 않고 주로 매개변수의 타입에 사용한다
왜?
1. 타입이 명확하지 않기 때문에 변수를 읽기만 가능하고 쓰기가 거의 불가능하다
2. 리턴 받은 쪽에서 타입을 모르기 때문에 바로 사용할 수가 없다 → 실용성과 유지보수성이 떨어진다
Generic 타입만 바꿔서 오버로딩하는 것은 불가능(컴파일 에러)하기 때문에 다음과 같이 사용한다
→ Generic 타입은 컴파일 후에는 없어지기 때문에 동일한 메서드가 2개 존재하게 된다
void printList(List<?> list) { // 요소가 어떤 타입이든 출력 가능한 메서드
for (Object obj : list) {
System.out.println(obj);
}
}
| 구문 | 의미 | 읽기 (get) | 쓰기 (add) | 사용 목적 | 예시 |
|---|---|---|---|---|---|
<? extends T> | T와 그 하위 타입만 허용 | ✅ 가능 | ❌ 불가능 | 데이터를 읽기만 할 때 | List<? extends Number> |
<? super T> | T와 그 상위 타입만 허용 | ⚠️ 제한적 | ✅ 가능 | 데이터를 쓰기만 할 때 | List<? super Integer> |
<?> | 모든 타입 허용 | ✅ 가능 | ❌ 불가능 | 타입 모를 때 읽기 전용용 | List<?> |
Fruit > Apple > GreenApple 일때 타입이 GreeApple 이면 Apple이나 Fruit 타입은 쓰지 못한다Integer 일 경우, Number 나 Object 타입은 할당하지 못할 수도 있다Generic Method : 선언부에 Generic 타입이 선언된 메서드
클래스 전체를 Generic으로 만들지 않고 특정 메서드만 유연하게 만들고 싶을 때 사용한다 (Generic 클래스에서도 사용할 수 있다)
public <T> void print(T item) {
System.out.println(item);
}
와일드 카드는 매개변수의 타입을 모르기 때문에 읽기나 쓰기가 제한될 수 있지만
Generic 메서드는 메서드 호출 시 타입을 대입해야 하기 때문에 타입에 맞게 읽기나 쓰기를 할 수 있다
Generic → Raw : 가능. 경고 발생
Raw → Generic : 가능. 경고 발생
Generic 자손 → Generic 조상 : 에러
Generic 조상 → Generic 자손 : 에러
Generic 자손 or 자기 자신 → 와일드 카드 : 가능
와일드 카드 → Generic 자손 or 자기 자신 : 가능. 경고 발생
와일드 카드 → 와일드 카드 : 가능. 경고 발생
경계(bound) 제거
class Box<T extends Fruit> {
void add(T t) {...}
}
// →
class Box {
void add(Fruit t) {...}
}
타입이 다르면 형변환 추가
List list; // List.get()은 Object 타입을 반환한다
T get(int i) {
return list.get(i);
}
// →
Fruit get(int i) {
return (Fruit)list.get(i);
}
열거형(enums) : 서로 관련된 상수들을 하나의 타입으로 묶어서 정의
따로 클래스를 생성해도 되고 클래스 내부에서 정의해도 된다
클래스 내부에서 정의할 경우 static인 내부 클래스이다
사용하는 상황
| 상황 | enum을 쓰는 게 좋을까? | 이유 |
|---|---|---|
| 상수들이 명확히 정해져 있고, 의미 있는 이름이 있으며 | ✅ Yes | 타입 안정성, 가독성, 유지보수 용이 |
| 해당 상수들이 특정 클래스에서만 쓰이는 단순한 경우 | ❌ Maybe Not | 구조가 단순해서 enum 없이도 충분 |
| 나중에 조건 분기나 데이터 추가 가능성이 있음 | ✅ Definitely | enum 구조가 더 유연하고 확장에 적합 |
== 비교 연산자는 사용 가능하지만,>, < 는 사용 할 수 없다 → compareTo() 로 비교할 수 있다Direction d = new Direction(1); // 불가
public enum Day { // 정의
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Day d = Day.MONDAY; // 사용
모든 열거형의 조상은 java.lang.Enum 이다
| 메서드 | 반환 타입 | 설명 |
|---|---|---|
name() | String | 열거 상수의 이름을 반환 (선언된 그대로) |
ordinal() | int | 열거 상수가 정의된 순서(0부터 시작)를 반환 |
compareTo(E o) | int | 열거 상수 간 비교 (ordinal 값으로 비교) |
equals(Object other) | boolean | 두 열거 상수의 동일성 비교 (==와 동일) |
hashCode() | int | 열거 상수의 해시 코드 반환 |
toString() | String | 보통 name()과 동일 (필요시 오버라이드 가능) |
getDeclaringClass() | Class<E> | 열거 상수가 정의된 enum 클래스 반환 |
valueOf(Class<T> enumType, String name) | T | 문자열 이름으로 열거 상수 반환 (정적 메서드) |
Enum 클래스에 정의된 ordinal() 메서드로 정의된 순서를 받아 사용할 수 있지만, 내부적인 용도로만 사용되기 위한 값이므로 다른 value가 필요하다면 멤버를 추가해서 사용하는 것이 좋다
public enum Fruit {
APPLE("red", 95),
BANANA("yellow", 105),
ORANGE("orange", 80);
private final String color; // final이 필수는 아니지만 상수를 저장하기 위한 타입이므로 final을 붙인다
private final int calories;
Fruit(String color, int calories) {
this.color = color;
this.calories = calories;
}
public String getColor() {
return color;
}
public int getCalories() {
return calories;
}
}
각 상수마다 다른 기능이 필요할 때 추상 메서드를 선언하고 구현할 수 있다
추상 메서드를 선언하고 상수 뒤 블럭에서 구현한다
내부에서 각각 enum 타입을 상속한 익명 내부 클래스 안에 구현부가 있다
public enum Operation {
PLUS {
@Override
public int apply(int x, int y) {
return x + y;
}
},
MINUS {
@Override
public int apply(int x, int y) {
return x - y;
}
},
MULTIPLY {
@Override
public int apply(int x, int y) {
return x * y;
}
},
DIVIDE {
@Override
public int apply(int x, int y) {
return y != 0 ? x / y : 0;
}
};
// 추상 메서드 선언
public abstract int apply(int x, int y);
}
package java.lang;
import java.io.Serializable;
import java.lang.Comparable;
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
private final String name; // 상수의 이름
private final int ordinal; // 상수가 선언된 순서 (0부터 시작)
/**
* enum의 생성자
* - enum 선언 내부에서만 호출 가능
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
// ...
}
public final class Fruit extends Enum<Fruit> {
public static final Fruit APPLE = new Fruit("APPLE", 0, "red", 95);
public static final Fruit BANANA = new Fruit("BANANA", 1, "yellow", 105);
private final String color;
private final int calories;
private Fruit(String name, int ordinal, String color, int calories) {
super(name, ordinal); // Enum 클래스가 제공
this.color = color;
this.calories = calories;
}
// ...
}
전통적인 싱글톤 패턴의 문제점
리플렉션을 이용해 private 생성자를 호출할 수 있다
* 리플렉션(Reflection) : 런타임에 클래스 정보에 접근하고 조작할 수 있게 해주는 도구
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
Constructor<MyClass> constructor = MyClass.class.getDeclaredConstructor();
constructor.setAccessible(true); // private 무시
MyClass obj = constructor.newInstance(); // ✅ 생성됨!
}
}
직렬화/역직렬화를 잘못 처리하면 새로운 인스턴스가 생길 수 있다
* 직렬화/역직렬화(Serialization/Deserialization) : 객체를 바이트(byte) 형태로 변환해서 파일, 네트워크 등으로 저장하거나 전송할 수 있도록 만드는 과정. 역직렬화는 반대의 과정
public class MySingleton {
private static final MySingleton instance = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return instance;
}
}
public enum MySingleton {
INSTANCE;
public void doSomething() {
System.out.println("싱글턴 동작 중!");
}
}
// main
MySingleton singleton = MySingleton.INSTANCE;
singleton.doSomething();
annotation : 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것 (메타 데이터를 추가하는 것)
컴파일러, 프레임워크, 런타임 환경 등이 이 정보를 보고 자동으로 어떤 동작을 수행한다
| 어노테이션 | 패키지 | 적용 대상 | 설명 |
|---|---|---|---|
| @Override | java.lang | 메서드 | 부모 클래스 메서드 재정의 시 사용 |
| @Deprecated | java.lang | 클래스, 메서드 등 | 더 이상 사용하지 말라는 경고 표시 |
| @SuppressWarnings | java.lang | 클래스, 메서드 등 | 특정 컴파일 경고 무시 |
| @SafeVarargs | java.lang | 메서드, 생성자 | 제네릭 가변인자 사용 시 안전함 명시 |
| @FunctionalInterface | java.lang | 인터페이스 | 함수형 인터페이스임을 명시 |
| @Retention | java.lang.annotation | 어노테이션 정의용 | 어노테이션 유지 시점 설정 (SOURCE, CLASS, RUNTIME) |
| @Target | java.lang.annotation | 어노테이션 정의용 | 어노테이션 적용 가능한 위치 지정 |
| @Documented | java.lang.annotation | 어노테이션 정의용 | Javadoc에 어노테이션 포함 여부 설정 |
| @Inherited | java.lang.annotation | 어노테이션 정의용 | 하위 클래스에 자동 상속 여부 설정 |
| @Repeatable | java.lang.annotation | 어노테이션 정의용 | 같은 어노테이션을 여러 번 적용 가능하게 함 |
@을 제외하면 인터페이스 정의와 동일하다
각 속성은 매개변수는 없고 반환값이 있는 추상 메서드의 형태(getter와 같은)를 가지며 상속을 통해 구현하지 않아도 된다
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
// 어노테이션 정의
@Retention(RetentionPolicy.RUNTIME) // 런타임에도 유지됨
@Target(ElementType.METHOD) // 메서드에만 적용 가능
public @interface MyAnnotation {
String value(); // 속성 (필수)
int count() default 1; // 속성 (기본값 있음)
}
// 어노테이션 사용
@MyAnnotation(value = "Hello")
public void sayHello() {
System.out.println("Hello method");
}
속성이 하나도 정의되지 않은 annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Test {}