개요
회사 업무로 진행하는 이펙티브 자바 세미나 자료 내용이다. 책과 블로그를 참고하였으며, 자세한 코드는 여기서 확인할 수 있다.
item34. int 상수 대신 열거 타입을 사용하라
같은 그룹으로 묶이는 int 타입 상수들은 나열하지 말고, enum 클래스를 활용하자. 열거 타입은 그 자체가 클래스이며, 상수들이 개별로 인스턴스를 만들어 public static final로 공개한다.
- 외부에서 접근 가능한 생성자를 제공하지 않으므로 직접 인스턴스 생성불가. 사실상 모든 필드는 final이다.
- 임의의 메소드나 필드를 추가할 수 있고 인터페이스를 구현하게 할 수도 있다.
- 정수 상수보다 가독성이 좋다.
- 컴파일 타입에 안정성을 제공한다.
public class item34 {
public enum Season {
// 각 상수는 자체적으로 생성자를 가질 수 있음
SPRING("Warm"),
SUMMER("Hot"),
AUTUMN("Cool"),
WINTER("Cold");
// 각 상수는 개별적으로 필드를 가질 수 있음
private final String description;
Season(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
public static void main(String[] args) {
Season currentSeason = Season.SUMMER;
System.out.println("Current season: " + currentSeason);
System.out.println("Description: " + currentSeason.getDescription());
// 외부에서 열거 타입의 인스턴스를 직접 생성할 수 없음
// Season invalidSeason = new Season("Invalid"); // 컴파일 에러
// 열거 타입은 클래스이므로 다양한 메소드나 필드를 추가할 수 있음
System.out.println("All seasons:");
for (Season season : Season.values()) {
System.out.println(season + ": " + season.getDescription());
}
}
}
*상수 값에 따라 switch문을 이용하여 분기할 수도 있다. 하지만 이 방법은 상수가 추가될 때 마다 case를 추가해주어야 하는 단점이 있다.
⇒ 중첩 열거 타입을 두어 생성자에서 선택하고, 중접 열거 타입에따라 분기하는 방식이 제일 좋다.
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURDAY(WEEKDAY),
FRIDAY(WEEKDAY), SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// 전략 열거 타입
// 평일과 주말에 대한 임금을 계산 코드
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
// 초과 근무 수당 계산
abstract int overtimePay(int mins, int payRate);
// 8시간을 분 단위로 환산한 것, 표준 근무 시간
private static final int MINS_PER_SHIFT = 8 * 60;
// 근무 시간과 시급에 따른 임금
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
열거 타입 사용 기준
- 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
- 열거 타입에 정의된 상수가 영원히 불변일 필요는 없다.
item35. ordinal 메서드 대신 인스턴스 필드를 사용하라.
대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응된다. 그리고 모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal이라는 메서드를 제공한다.
이러한 ordinal은
- 상수 선언 순서가 바뀌는 순간 오동작할 가능성이 있다.
- 값을 중간에 비워둘 수 없기 때문에 더미 상수를 추가해야 한다.
따라서, 열거 타입에 연결된 값은 ordinal 대신 인스턴스 필드에 저장하여 사용한다.
인스턴스 필드에 저장하여 사용한 예시
public enum DayEnum {
MONDAY(1),
TUESDAY(2),
WEDNESDAY(3);
private final int dayNumber;
DayEnum(int dayNumber) {
this.dayNumber = dayNumber;
}
public static void main(String[] args) {
DayEnum day = DayEnum.MONDAY;
System.out.println("day = " + day);
}
}
ordinal로 정의한 예시
public enum DayOrdinal {
MONDAY,
TUESDAY,
WEDNESDAY;
// ordinal 메서드를 사용하여 열거 상수 위치 반환
public int getDayOrdinal() {
return ordinal() + 1;
}
public static void main(String[] args) {
DayOrdinal dayOrdinal = DayOrdinal.WEDNESDAY;
System.out.println("dayOrdinal = " + dayOrdinal.getDayOrdinal());
}
}
item36. 비트 필드 대신 EnumSet을 사용하라
비트 필드란?
메모리를 절약하기 위해 사용한 도구로 참-거짓값을 결헙해서 하나의 바이트로 만들어, 메모리를 절약한다. 예를 들어, c나 c++같은 경우 정수형을 저장할 때 1~2비트만 필요하지만 메모리는 16비트를 저장해야하는 케이스가 있다. 이러한 낮은 값을 갖는 정수 변수가 많을 때 메모리 소비를 줄이기 위해 사용한다.
public class TextAttribute {
public static final int BOLD = 1 << 0; // 1
public static final int ITALIC = 1 << 1; // 2
// public static final int UNDERLINE = 1 << 2; // 4
// public static final int STRIKE = 1 << 3; // 8
private int style;
public TextAttribute() {
// 초기값이 0으로 모든 스타일을 비활성화 해둠
this.style = 0;
}
// 비트 연산을 통해 값 확인
public boolean isBold() {
return (style & BOLD) != 0;
}
public boolean isItalic() {
return (style & ITALIC) != 0;
}
public void setBold(boolean bold) {
style = bold ? style | BOLD : style & ~BOLD;
}
public void setItalic(boolean italic) {
style = italic ? style | ITALIC : style & ~ITALIC;
}
public static void main(String[] args) {
TextAttribute textAttribute = new TextAttribute();
textAttribute.setBold(true);
textAttribute.setItalic(false);
System.out.println("textAttribute is Bold = " + textAttribute.isBold());
System.out.println("textAttribute is Italic = " + textAttribute.isItalic());
}
}
- 비트 필드는 비트별 연산을 통해서 집합 연산을 효율적으로 수행하지만, item34의 정수 열거 상수의 단점을 그대로 갖는다.
- 비트 필드값이 그대로 노출되면 코드 해석이 어렵다.
- 비트 필드 하나에 녹아있는 모든 원소를 순회하기가 까다롭다.
- 최대 몇 비트가 필요한지를 미리 예측하여 적절한 타입(int or long)을 선택해야 한다.
⇒ 따라서 bit 필드 대신 java.util.EnumSet을 사용하자.
- Set 인퍼페이스를 구현하여 어떤 Set 구현체와도 사용이 가능하다.
- 내부가 비트 벡터로 구현되어있다. EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다.
- EnumSet은 불변 객체 못 만드는데, 만들고 싶으면 guava 사용하기Collections.unmodifiableSet() 으로 감싸서 사용하면 불변 가능
public class TextAttributeEnumSet {
public enum Style {
BOLD,
ITALIC,
UNDERLINE,
STRIKE;
}
private EnumSet<Style> styles;
public TextAttributeEnumSet() {
this.styles = EnumSet.noneOf(Style.class);
}
public void setStyle(Style style, boolean active) {
if (active) {
styles.add(style);
} else {
styles.remove(style);
}
}
public boolean isStyleActive(Style style) {
return styles.contains(style);
}
public static void main(String[] args) {
TextAttributeEnumSet textAttributeEnumSet = new TextAttributeEnumSet();
textAttributeEnumSet.setStyle(Style.BOLD, true);
textAttributeEnumSet.setStyle(Style.ITALIC, false);
System.out.println("textAttributeEnumSet.isStyleActive(Style.BOLD) = " + textAttributeEnumSet.isStyleActive(Style.BOLD));
System.out.println("textAttributeEnumSet.isStyleActive(Style.ITALIC) = " + textAttributeEnumSet.isStyleActive(Style.ITALIC));
}
}
item38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
item34에서 밝혔듯, 열거 타입은 인터페이스로 구현이 가능하다.
열거 타입은 웹만하면 확장을 지양해야 한다. 확장한 타입의 원소를 기반 타입의 원소로 취급한다면 그 반대도 성립해야 하는데, 열거 타입은 그렇지 않다. 따라서 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법도 마땅치 않다.
예외! 확장할 수 있는 열거 타입이 어울리는 쓰임 ⇒ 연산 코드
public interface Operation {
double apply(double x, double y);
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
}
- 아래와 같은 방법으로 enum을 인터페이스로 확장 가능
연산을 구현할 때 좋은 방식
public enum ExtendedOperation implements Operation {
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("%") {
public double apply(double x, double y) { return x / y;}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
}
item39. 명명 패턴보다 애너테이션을 사용하라
명명 패턴이란?
변수나 함수의 이름을 일관된 방식으로 작성하는 채턴
예를 들면 JUnit3의 테스트 메서드 이름은 항상 test로 시작한다.
단점
- test를 tset로 오타를 내게된다면, 테스트 메서드로 인식하지 못하고 테스트를 수행하지 않는다.
- 명명 패턴을 의도한 곳에서, 올바르게 사용된다는 보증이 없다.
- 예를 들어 클래스 이름을 TestSafety로 지어서 JUni에 줘도, 테스트는 수행되지 않으며 경고 메시지조차 없다.
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
⇒ 에너테이션이 이러한 명명 패턴의 문제들을 해결해준다. (애너테이션은 JUnit4에서 도입되었다.)
마커 애너테이션
아무 매개변수 없이 단순히 대상에 마킹하는 용도
/**
* 테스트 메서드임을 선언하는 JUnit애너테이션이다.
* 매개변수 없는 정적 메서드 전용
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}
⇒ 프로그래머가 Test 이름에 오타를 내거나 메서드 선언 외의 프로그램 요소에 달면 컴파일 오류를 내준다.
@Retention: Test가 런타임에도 유지되어야 한다는 표시
@Target : Test가 반드시 메서드에 선언되어야 한다는 표시
마커 애너테이션을 사용한 프로그램 예시
public class Sample {
@Test
public static void m1() { } // 성공해야 한다.
public static void m2() { }
@Test public static void m3() { // 실패해야 한다.
throw new RuntimeException("실패");
}
public static void m4() { } // 테스트가 아니다.
@Test public void m5() { } // 잘못 사용한 예: 정적 메서드가 아니다.
public static void m6() { }
@Test public static void m7() { // 실패해야 한다.
throw new RuntimeException("실패");
}
public static void m8() { }
}
매개변수를 받는 애너테이션 타입
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value(); // 매개변수
}
이 애너테이션의 매개변수 타입은 Class<? extends Throwable>이다. 이는 Throwable을 확장한 클래스의 Class 객체라는 뜻으로, 모든 예외와 오류 타입을 수용한다.
item40. @Override 애너테이션을 일관되게 사용하라.
- 상위 클래스의 메서드를 재정의 하려는 모든 메서드에 @Override 애너테이션을 달아야 한다. 재정의 의도를 명확하게 나타낼 수 있고, 컴파일러가 잘못된 부분을 명확히 알려준다.
구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 @Override를 달지 않아도 된다.구체 클래스인데 아직 구현하지 않은 추상 메서드가 있다면 컴파일러가 이를 알려주기 때문이다.- 하지만 @Override를 일괄적으로 다는 게 좋아 보인다면 @Override를
달아줘도 상관없다.그렇게 하는 것이 좋다!
- 하지만 @Override를 일괄적으로 다는 게 좋아 보인다면 @Override를
'언어 > JAVA' 카테고리의 다른 글
[JAVA] Effective JAVA. 9장 일반적인 프로그래밍 원칙 1편(item 57, 58, 59, 60) (0) | 2024.03.24 |
---|---|
[JAVA] 날짜/시간 클래스 알아보기(Date, LocalDateTime, ZoneDateTime) (1) | 2024.03.08 |
[JAVA] 익명 클래스(Anonymous Class), 람다식(Lambda) (1) | 2023.10.22 |
[JAVA] JAVA String | StringBuilder | 문자열 결합 연산 | 문자열 연산의 복잡도 (0) | 2023.05.19 |