ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] static 알아보기
    프로그래밍/Java 2021. 10. 13. 00:01

    메서드와 필드에 붙는 static은 왜 붙는 것이고 무슨 역할을 할까? 예제를 만들어봤다.

    public class Marin {
    	public int range = 4;
    	public boolean isUpgrade;
    }
    
    class Academy {
    	void upgradeRange(Marin m) {
    		if (!m.isUpgrade) {
    			m.range = 5;
    			m.isUpgrade = true;
    		}
    	}
    }

    마린 유닛의 사정거리는 4이고, 아카데미에서 업그레이드를 하면 사정거리는 5가 된다. 업그레이드는 한 번밖에 못하니 isUpgrade 조건을 넣어주었고, 당연히 업그레이드는 지금 생성된 유닛은 물론이고 앞으로 생성될 유닛까지 모두 오른다. 그러면 한번 작동시켜보자.

    public class Test {
    	public static void main(String[] args) {
    		Academy arademy1 = new Academy();
    		Marin marin1 = new Marin();
    		Marin marin2 = new Marin();
    
    		arademy1.upgradeRange(marin1); // 업그레이드를 시켜주었다
    
    		Marin marin3 = new Marin(); // 업그레이드를 하고 마린이 나왔다
    
    		System.out.println("marin1 range : " + marin1.range);
    		System.out.println("marin2 range : " + marin2.range);
    		System.out.println("marin3 range : " + marin3.range);
    	}
    }
    marin1 range : 5
    marin2 range : 4
    marin3 range : 4

    생각했던 거랑은 다르다. 모든 유닛이 range의 값을 공유해야 한다. 모든 마린 객체들은 range와 isUpgrade 변수를 공유하여야 한다. 변수에 static 키워드를 넣어주면 변수를 각 인스턴스가 공유한다.

    public class Marin {
    	public static int range = 4;
    	public static boolean isUpgrade;
    }
    
    class Academy {
    	void upgradeRange() {
    		if (!Marin.isUpgrade) {
    			Marin.range = 5;
    			Marin.isUpgrade = true;
    		}
    	}
    }

    Marin 클래스의 range, isUpgrade 필드에 static 키워드를 넣어주었고, Academy 클래스의 upgradeRange의 구현부도 클래스명.메서드명() 형식으로 static 변수는 클래스에서 직접 접근하였다.

    public class Test {
    	public static void main(String[] args) {
    		Academy arademy1 = new Academy();
    		Marin marin1 = new Marin();
    		Marin marin2 = new Marin();
    		
    		arademy1.upgradeRange(); // 업그레이드를 시켜주었다
    		
    		Marin marin3 = new Marin(); // 업그레이드를 하고 마린이 나왔다
    		
    		System.out.println("marin1 range : " + marin1.range);
    		System.out.println("marin2 range : " + marin2.range);
    		System.out.println("marin3 range : " + marin3.range);
    	}
    }
    marin1 range : 5
    marin2 range : 5
    marin3 range : 5

    생각한 대로 모두 잘 업그레이드가 되었다. 그럼 static은 값을 공유할 때만 쓰이는 걸까? 필드에만 static이 쓰이는 걸까? Math 클래스를 가져와보았다.


    public final class Math {
        public static int max(int a, int b) {
            return (a >= b) ? a : b;
        }
    }

    Math클래스의 max메서드를 가져왔다. static 메서드이고 매개변수 a와 b 중 큰 수를 반환하는 간단한 메서드이다. 사실 static이 없어도 작동하는데 문제가 없다. 그러나 static을 붙이는 것과 안 붙이는 것에는 이 메서드를 사용할 때 차이가 있다.

    public class Test {
    	public int nonMax(int a, int b) { // 인스턴스 메서드
    		return (a >= b) ? a : b;
    	}
    
    	public static int max(int a, int b) { // static 메서드
    		return (a >= b) ? a : b;
    	}
    
    	public static void main(String[] args) {
    		Test test = new Test(); // 객체를 생성 한 다음
    		test.nonMax(1, 1); // 인스턴스 메서드를 사용할 수 있다.
    
    		Test.max(1, 2); // 클래스 이름으로 바로 static 메서드를 사용할 수 있다.
    	}
    }

    static이 없는 메서드는 인스턴스를 만들어줘야 한다. 그러나 static 메서드는 인스턴스를 만들어줄 필요 없이 클래스.메서드() 형식으로 바로 사용할 수 있다. 즉, 개발자가 인스턴스 없이도 메서드를 사용할 수 있다고 판단된다면 static 메서드를 쓰는 것이다.


    static 키워드가 붙은 변수, 메서드를

    • static 변수 / static 메서드
    • 정적 변수 / 정적 메서드
    • 클래스 변수 / 클래스 메서드

    라고 부른다. 다 같은 말이다. 이제 static의 특징을 알아보자.

    public class Test {
    
    	int iv;
    	static int cv;
    
    	static void classMethod1() {
    		cv = 1;
    //		iv = 1; // Cannot make a static reference to the non-static field iv
    	}
    
    	void method1() {
    		cv = 1;
    		iv = 1;
    	}
    
    	static void classMethod2() {
    		classMethod1();
    //		method1(); // Cannot make a static reference to the non-static method method1() from the type Test
    	}
    
    	void method2() {
    		classMethod1();
    		method1();
    	}
    
    	public Test() {
    		Test test = new Test();
    
    		test.cv = 1;
    		test.iv = 1;
    //		참조변수.변수명
    		test.classMethod1();
    		test.method1();
    //		참조변수.메서드명()
    
    		Test.cv = 1;
    //		클래스명.변수명
    		Test.classMethod1();
    //		클래스명.메서드명()
    	}
    }

    static 변수와 메서드도 참조 변수를 통해서 접근이 가능하지만 권장하지는 않으며, 보통 클래스명으로 접근한다.

    인스턴스 메서드는 static이 붙든 안 붙든 모두 사용할 수가 있다. 그러나 static 메서드는 인스턴스 메서드와 필드는 사용할 수 없다. 그 이유는 static은 프로그램이 시작될 때 메모리에 남아있지만, 인스턴스은 인스턴스가 생성되야지 메모리에 생기기 때문이다.

     

    여담으로 iv는 인스턴스 변수(instance variable), cv는 클래스 변수(class variable), lv는 지역 변수(local variable)의 약자이다.

    Java에서의 메모리 구조에 대해 자세한 것은 다음에 포스팅 하겠다.



    static 메서드는 오버 라이딩이 불가능하다. 그러나 상속static 변수라도 가능하다.

    public class SonClass extends Test {
    	@Override
    	void method1() {
    		super.iv = 1;
    		super.cv = 1;
    	}
    
    //	void classMethod1() {} // This instance method cannot override the static method
    }

    정적 내부 클래스는 static이 붙는다.

    public class OuterClass {
    
    	int iv = 1;
    	void method() {};
    
    	static int cv = 1;
    	static void classMethod() {};
    
    	class InnerClass {
    
    		void Innermethod() {
    			iv = 1;
    			cv = 1;
    
    			method();
    			classMethod();
    		}
    	}
    
    	static class StaticMemberClass {
    
    		void staticMethod() {
    //			iv = 1; // Cannot make a static reference to the non-static field iv
    			cv = 1;
    
    //			method(); // Cannot make a static reference to the non-static method method() from the type OuterClass
    			classMethod();
    		}
    	}
    
    	public static void main(String[] args) {
    		OuterClass test1 = new OuterClass(); // 외부 클래스의 객체를 만들고
    		OuterClass.InnerClass inner = test1.new InnerClass(); // 내부 클래스의 객체를 만들었다
    
    //		OuterClass.StaticMemberClass staticClass = test1.new StaticMemberClass(); // Illegal enclosing instance specification for type OuterClass.
    		OuterClass.StaticMemberClass staticClass = new OuterClass.StaticMemberClass(); // 정적 멤버 클래스는 바로 만들 수 있다
    	}
    }

    정적 멤버 클래스는 내부 클래스와 다르게 외부 클래스의 멤버에 접근할 때 static 멤버나 메서드만 접근 가능하며 정적 멤버 클래스의 객체 생성 만들 때 외부 클래스의 객체를 만들 필요가 없다.

     

    내부 클래스에 대해 자세한 것은 다음에 포스팅하겠다.



    초기화 블록에도 static이 붙는다. 초기화 블록은 생성자보다 먼저 실행된다. 초기화 블록을 쓰는 이유는 생성자의 중복된 코드를 초기화 블록에 넣어 코드를 줄일 수 있다. 물론 생성자 오버 로딩을 이용해서도 코드를 줄일 수 있다.

    public class Test {
    	static int cv;
    	int iv;
    
    	static {
    		cv = 1;
    //		iv = 1; // Cannot make a static reference to the non-static field iv
    		System.out.println("static 초기화 블럭 RUN");
    	}
    
    	{
    		cv = 1;
    		iv = 1;
    		System.out.println("인스턴스 초기화 블럭 RUN");
    	}
    
    	public Test() {
    		cv = 1;
    		iv = 1;
    		System.out.println("생성자 블럭 RUN");
    	}
    
    	public static void main(String[] args) {
    		System.out.println("main RUN");
    		new Test();
    	}
    }
    static 초기화 블럭 RUN
    main RUN
    인스턴스 초기화 블럭 RUN
    생성자 블럭 RUN

    static 초기화 블럭은 클래스 로딩 시에 딱 1번만 실행이 된다. 즉 JVM이 실행되자마자 실행이 된다. 그리고 static 초기화 블록에는 인스턴스 필드는 넣을 수 없다. 이유는 static 메서드에 인스턴스 변수를 넣지 못하는 것과 같다. static 초기화 블록을 쓰는 목적으론 static 변수의 복잡한 초기화 과정이 필요할 때 보통 쓰인다.


    그렇다면 웬만하면 static을 써야겠다는 생각이 드는데 static은 신중하게 써야한다.

    • static 프로그램 시작 시에 메모리에 생성되고 프로그램 종료 시에 사라진다.

    프로그램 돌아가는 내내 이를 사용하든 안 하든 메모리 안에 존재하며, 이는 메모리 관리에 좋지 않다. 또한 static은 메모리에서 제거되지 않는다. static을 메모리에 올리는 시점도 개발자가 컨트롤 하는 것이 아닌 JVM이 컨트롤한다.

    • 멀티 스레드에 대비하여야 한다.
    public class Test {
    	static int account = 500;
    
    	static void withdraw(int money) {
    		try {
    			if (account <= 0)
    				return;
    			Thread.sleep(500); // 성능상 약간의 딜레이 발생
    			account -= money;
    			System.out.println(Test.account);
    		} catch (InterruptedException e) {
    		}
    	}
    
    	public static void main(String[] args) {
    		new Thread(() -> {
    			Test.withdraw(500);
    		}).start();
    		Test.withdraw(500);
    	}
    }
    -500
    -500

    account의 값이 0 이하일 땐 return 하게 되어 있으나 딜레이로 인해 값의 업데이트가 늦어지면서 통장이 마이너스 통장이 되어버렸다. 이런 경우를 대비해서 Java에선 synchronized라는 키워드를 지원한다.

    public class Test {
    	static int account = 500;
    
    	static synchronized void withdraw(int money) { // 동기화된 메소드
    		try {
    			if (account <= 0)
    				return;
    			Thread.sleep(500); // 성능상 약간의 딜레이 발생
    			account -= money;
    			System.out.println(Test.account);
    		} catch (InterruptedException e) {
    		}
    	}
    
    	public static void main(String[] args) {
    		new Thread(() -> {
    			Test.withdraw(500);
    		}).start();
    		Test.withdraw(500);
    	}
    }
    0

    synchronized 키워드를 쓰면 동시에 여러 스레드가 접근을 하더라도 한 쓰레드가 모든 작업을 끝낼때까지 다른 쓰레드가 들어올 수 없게 만든다. 그러나 synchronized은 성능 저하를 일으킬 수 있기 때문에 신중히 필요할 때만 써야 한다.

    • 전역에서 접근이 가능하기 때문에 테스트하기 어렵다.

    static 필드는 프로그램 전역에서 사용할 수 있기 때문에 큰 프로그램을 테스트할 때 이 필드가 어디서 사용되는지, 어디서 값이 변경되었는지 추적하기 어렵다 

    • 직렬화가 불가능하다.

    직렬화는 객체나 데이터를 바이트 형태로 변환하는 것이다. 정적 필드는 클래스 자체 정보이기 때문에 직렬화에 포함되지 않는다.

     

    직렬화에 대한 자세한 것은 다음에 포스팅하겠다.



    https://it-mesung.tistory.com/86?category=830540 

     

    [Java 기초] static

    static static은 컴파일러에 의해 .java에서 .class 파일로 로드될 시 우선적으로 method 영역(static, class영역이라고도 부름) 메모리에 할당된다. 이런 이유로, 객체가 heap영역 메모리에 올라가기 전에 호

    it-mesung.tistory.com

    https://sjh836.tistory.com/145

     

    중첩클래스를 알아보자 (내부클래스, 정적 중첩클래스, 지역클래스, 익명클래스)

    일반클래스는 Package member class 라고들 하며, 아래 항목에서 제외했다. 구글링해보면 클래스간에 관계나 큰 틀에서의 구조를 안잡고 나열하는 식이 많은 것 같다ㅋㅋ 1. 중첩 클래스는 왜 쓰는가?

    sjh836.tistory.com

    https://dustink.tistory.com/m/74?category=880788 

     

    자바(Java) - 중첩 클래스의 접근 제한

    중첩 클래스의 접근 제한 바깥 필드와 메소드에서 사용 제한 A 가 있어야, B를 사용할 수 있다. C는 A가 없이도 사용 가능하다. 예시1) 바깥 필드와 메소드에서 사용 제한 public class A { //인스턴스

    dustink.tistory.com

    https://velog.io/@tomato2532/%EC%B4%88%EA%B8%B0%ED%99%94-%EB%B8%94%EB%9F%AD%EA%B3%BC-%EC%83%9D%EC%84%B1%EC%9E%90

     

    [JAVA] 초기화 블럭과 생성자

    멤버 초기화 초기화 스태틱 변수와 인스턴스 변수는 초기화하지 않아도 기본값이 존재 0 혹은 0에 준하는 값, 참조형이라면 null 지역변수는 초기화하지 않으면 사용할 수 없음 초기화 방법 : 명

    velog.io

    https://kellis.tistory.com/127

     

    Static 사용을 피해야 하는 이유

    Java에는 static이라는 키워드가 존재하며, 이는 static으로 지시된 특정한 멤버가 해당 클래스의 인스턴스가 아니라 클래스 자체에 속해 있음을 나타냅니다. 즉, 클래스의 모든 인스턴스에서 공유

    kellis.tistory.com

Designed by Tistory.