static 메서드와 정적 import
static 메서드가 인스턴스 상태에 직접 접근할 수 없는 이유를 확인하고 무상태 회원 저장소 유틸리티와 정적 import의 사용 범위를 판단합니다.
static 메서드는 특정 객체 참조 없이 클래스 이름으로 호출됩니다.
따라서 어느 인스턴스의 필드를 사용할지 정할 수 없습니다.
입력만으로 결과를 만드는 무상태 계산이나 클래스 전체 상태를 다루는 행동에 적합하며, 객체별 상태를 바꾸는 행동은 인스턴스 메서드로 둡니다.
static 문맥의 접근 제한
public final class StaticInstanceAccess {
private int size;
public static void main(String[] args) {
System.out.println(size);
}
}error: non-static variable size cannot be referenced from a static contextmain은 객체 생성 없이 JVM이 클래스에서 바로 호출하는 static 메서드입니다.
size는 StaticInstanceAccess 객체마다 다른 인스턴스 필드인데, 현재 코드에는 어떤 객체를 뜻하는 this도 없습니다.
참조를 명시하면 접근할 수 있습니다.
StaticInstanceAccess instance = new StaticInstanceAccess();
System.out.println(instance.size);static이 인스턴스 기능을 절대 사용할 수 없다는 뜻이 아닙니다.
특정 객체 참조를 매개변수나 지역 변수로 확보한 뒤 그 객체의 공개 메서드를 호출할 수 있습니다.
직접적인 암묵적 수신자 this가 없다는 뜻입니다.
정적·인스턴스 호출 방향
public final class StaticCallRules {
private static int globalCount;
private int localCount;
public static void main(String[] args) {
StaticCallRules first = new StaticCallRules();
StaticCallRules second = new StaticCallRules();
first.register();
second.register();
second.register();
System.out.println(first.localSummary());
System.out.println(second.localSummary());
System.out.println(globalSummary());
}
private void register() {
localCount++;
globalCount++;
}
private String localSummary() {
return "local=" + localCount + ", global=" + globalCount;
}
private static String globalSummary() {
return "all=" + globalCount;
}
}local=1, global=3
local=2, global=3
all=3인스턴스 메서드 register와 localSummary에는 this가 있어 인스턴스 필드와 static 필드 모두 사용할 수 있습니다.
static globalSummary는 globalCount만 직접 읽습니다.
인스턴스 메서드는 클래스 공유 상태에 접근할 수 있지만, 결합이 필요한지 판단해야 합니다.
무상태 유틸리티 클래스
public final class MemberStatisticsUtility {
public static void main(String[] args) {
int[] age = {40, 50, 30};
System.out.println("sum=" + MemberStatistics.sum(age));
System.out.println("average=" + MemberStatistics.average(age));
System.out.println("max=" + MemberStatistics.max(age));
}
private static final class MemberStatistics {
private MemberStatistics() {
throw new AssertionError("no instances");
}
static int sum(int[] values) {
int total = 0;
for (int value : values) {
total += value;
}
return total;
}
static double average(int[] values) {
if (values.length == 0) return 0.0;
return (double) sum(values) / values.length;
}
static int max(int[] values) {
if (values.length == 0) throw new IllegalArgumentException("empty");
int result = values[0];
for (int index = 1; index < values.length; index++) {
if (values[index] > result) result = values[index];
}
return result;
}
}
}sum=120
average=40.0
max=50MemberStatistics 메서드는 인수만으로 결과를 만들고 객체별 필드가 없습니다.
인스턴스를 생성할 이유가 없으므로 생성자를 private으로 막습니다.
이 패턴을 모든 행동에 적용하지 않습니다.
members와 size를 가진 MemberRegistry의 add를 static으로 바꾸면 상태를 매번 인수로 전달해 객체 책임을 잃습니다.
유틸리티 메서드도 입력 배열을 바꾸는지 여부를 계약에 명시해야 합니다.
위 계산들은 읽기만 하므로 호출자의 배열 상태를 바꾸지 않습니다.
정적 import
package members.app;
import static java.lang.Math.max;
import static java.lang.Math.min;
public final class StaticImportDemo {
public static void main(String[] args) {
int age = 130;
int normalized = max(14, min(age, 120));
System.out.println("normalized=" + normalized);
}
}normalized=120일반 호출은 Math.min(age, 120)처럼 클래스 소유자를 적습니다.
정적 import를 사용하면 min만 적어도 컴파일러가 java.lang.Math.min을 찾습니다.
수식에서 같은 클래스의 정적 함수를 반복해 소유자가 이미 명확할 때 읽기 쉬울 수 있습니다.
무분별한 정적 import는 메서드가 어느 클래스의 규칙인지 숨기고 이름 충돌을 만듭니다.
특히 프로젝트 도메인 메서드는 SignupPolicy.normalizeAge처럼 소유자를 남기는 편이 맥락을 줍니다.
테스트의 assertion이나 수학 함수처럼 관례가 강한 경우에 제한적으로 사용합니다.
별표 정적 import도 가능하지만 공개 정적 멤버가 추가될 때 이름 충돌 가능성이 커집니다.
import static java.lang.Math.*;필요한 멤버를 개별 import하면 파일 의존성이 명시적입니다.
조립점으로서의 main
main이 static인 이유는 프로그램 시작 전에 특정 애플리케이션 객체가 존재하지 않기 때문입니다.
main은 필요한 객체를 만들고 협력을 연결한 뒤 인스턴스 메서드에 실행을 넘길 수 있습니다.
public final class StaticMainComposition {
public static void main(String[] args) {
SignupApplication application = new SignupApplication(new MemberRegistry(2));
application.run();
}
private static final class SignupApplication {
private final MemberRegistry registry;
SignupApplication(MemberRegistry registry) {
this.registry = registry;
}
void run() {
registry.add(40);
registry.add(50);
System.out.println("total=" + registry.total());
}
}
private static final class MemberRegistry {
private final int[] age;
private int size;
MemberRegistry(int capacity) {
age = new int[capacity];
}
void add(int value) {
age[size++] = value;
}
int total() {
return MemberCalculations.sum(age, size);
}
}
private static final class MemberCalculations {
static int sum(int[] values, int size) {
int total = 0;
for (int index = 0; index < size; index++) {
total += values[index];
}
return total;
}
}
}total=90main은 조립만 하고 애플리케이션 상태는 인스턴스가 소유합니다.
무상태 합계 계산만 static 유틸리티로 분리했습니다.
연습 문제
인스턴스를 만들 수 없는 MemberAgeRules 유틸리티를 만들고 isAdult(int age)와 group(int age)를 static으로 제공하세요.
14세 미만과 120세 초과는 거절하고, 18세 이상은 성인으로 판정하며 65세 이상은 senior로 분류합니다.
해설 보기
public final class MemberAgeRulesUtility {
public static void main(String[] args) {
System.out.println(MemberAgeRules.isAdult(19));
System.out.println(MemberAgeRules.group(65));
}
private static final class MemberAgeRules {
private MemberAgeRules() {
throw new AssertionError("no instances");
}
static boolean isAdult(int age) {
validate(age);
return age >= 18;
}
static String group(int age) {
validate(age);
if (age >= 65) return "senior";
return age >= 18 ? "adult" : "teen";
}
private static void validate(int age) {
if (age < 14 || age > 120) throw new IllegalArgumentException("age");
}
}
}true
senior두 결과는 인수에만 의존하고 저장할 인스턴스 상태가 없습니다.
long 곱셈으로 중간 오버플로 가능성도 줄였습니다.