public static void main(String[] args){
final int a;
a = 10; // 가능
final int b = 10; // 가능
}
package final1;
//final 필드 - 생성자 초기화
public class ConstructInit {
final int value;
final int CONST_VALUE = 10;
public ConstructInit(int value) {
this.value = value;
// this.CONST_VALUE = value; // 컴파일 에러
}
}
💡 여기서 한번 생각해보자
- 초기화 되지 않은
final
변수의 경우, 매개변수에 들어오는 값에 따라 다른 값이 저장될 수 있다.
변수로서 아직은 가치가 있다는 것이다.- 하지만 위 코드의
CONST_VALUE
처럼 선언과 동시에 초기화했다면?
CONST_VALUE
가 있는 클래스로 객체를 만든다면 몇 개를 만들던CONST_VALUE
는 같은 값이 된다.- 즉, 객체를 만들때마다 항상 같은 값인 변수로 메모리가 할당되고 이것은 메모리 낭비로 이어진다.
- 이때, 우리는 이전에 배운
static
을 떠올릴 수 있다.
[모든 객체에서 같은 값으로써 사용되는 변수] == [모든 객체에서 “공유”하는 변수] 로 볼 수 있다.
단지final
로 인해 두번 다시 값이 변경되지 않는 다는 특성을 가진 공유변수인 셈이다.- 그래서
static
으로 선언한다면 메서드 영역의static
영역에서 딱 한개로 생성되어 메모리 낭비를 막으며 기존의 선언과 동시에 초기화한final
변수와 같은 역할을 할 수 있다.
Java에서 fianl인데 선언과 동시에 초기화를 해야 한다면 다음을 따른다.
1. static final로 사용
2. 변수명을 모두 대문자로 작성
static final
을 상수를 선언할 때 사용한다._
)를 사용한다.import static ~
을 사용할 수 도 있다.public
을 사용하며, 중앙에서 값을 관리할 수 있다.final
을 사용할 수 있다.final
이라면 변경할 수 없다)package final1;
public class Data {
public int value;
}
package final1;
public class FinalRefMain {
public static void main(String[] args) {
final Data data = new Data();
//data = new Data(); //참조값 변경 불가능
//참조 대상의 값은 변경 가능
data.value = 10;
System.out.println(data.value);
data.value = 20;
System.out.println(data.value);
}
}
package extends1.ex2;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
package extends1.ex2;
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.ex2;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.ex2;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
}
}
/* 실행 결과
차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.
*/
ElectricCar
와 GasCar
는 가지고 있지 않는 move()
를 사용할 수 있다.ElectricCar electricCar = new ElectricCar();
electricCar.move();
ElectricCar
클래스를 확인한다.❓ 이때, 메서드의 정보는 메서드 영역에 저장되어있을 텐데 힙 영역에서 어떻게 확인할 수 있을까? 궁금해서 찾아보았다!
1. 클래스 로딩
- Java 프로그램이 실행될 때, JVM은 필요한 클래스를 메모리에 로드합니다.
이 과정에서 클래스의 정보가 메서드 영역에 저장됩니다.
- 메서드 영역엔 클래스의 이름, 필드, 메서드, 상수 풀, 부모 클래스 정보 등이 포함됩니다.
2. 인스턴스 생성
- 각 객체 인스턴스는 자신의 클래스에 대한 정보를 참조하기 위해 내부적으로 클래스의 메타데이터에 대한 포인터를 가지고 있습니다.
- 이 포인터는 메서드 영역에 있는 클래스 정보를 가리킵니다.
3. 따라서 b단계를 정정하면,electricCar
인스턴스가 가지고 있는ElectricCar
클래스 정보에 대한 포인터를 통해 메서드 영역에 있는ElectricCar
클래스의 메서드를 확인한다.
4. 메서드 영역의ElectricCar
클래스 정보에는move()
메서드 정보를 확인하지 못했다.
5. “호출자”의 자료형 클래스에 해당 메서드가 없다면, 상속 관계인 부모 클래스의 메서드를 확인한다.
6. 이제 다시 힙 영역에서 x001을 참조하여 인스턴스 생성 시 저장되었던 부모 클래스의 정보에 대한 포인터를 통해 메서드 영역에 있는Car
클래스의 메서드를 확인한다.
7. 드디어move()
메서드를 발견했다.move()
를 실행한다.
8. 위 과정은 메서드가 아닌 변수여도 동일하다!
ElectricCar
클래스에도 move()
메서드가 생긴 것이다.ElectricCar
클래스에서 move()
메서드를 먼저 발견한다Car
클래스까지 찾으러 가지않고 발견한 메서드를 바로 실행한다.@Override
package extends1.overriding;
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
@Override
를 작성해준다.throws
로 선언할 수 없다.상속 관계에서 자식 클래스가 부모 클래스의 멤버 변수나 메서드를 사용할 때,
클래스의 패키지 위치에 따라 접근제어자 영향을 그대로 받는다
실제로 부모 클래스와 자식 클래스의 패키지를 다르게 하여 확인해보자!
예시 코드
Parent와 Child 클래스의 패키지 위치가 다름을 유의하자
package extends1.access.parent; // Child 클래스와 패키지가 다름을 유의하자
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() {
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectedValue);
System.out.println("defaultValue = " + defaultValue);
System.out.println("privateValue = " + privateValue);
//부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; //상속 관계 or 같은 패키지
//defaultValue = 1; //다른 패키지 접근 불가, 컴파일 오류
//privateValue = 1; //접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); //상속 관계 or 같은 패키지
//defaultMethod(); //다른 패키지 접근 불가, 컴파일 오류
//privateMethod(); //접근 불가, 컴파일 오류
printParent();
}
}
Parent
클래스와 Child
클래스를 다른 패키지에서 정의한다.Child
클래스는 Parent
클래스로부터 상속받는다.Parent
클래스의 멤버 변수/메서드는 접근제어자의 영향을 받는다.protected
접근제어자의 경우 특징이 있다.protected
는 패키지가 다르면 접근할 수 없게한다💡 상속관계에서 부모 클래스의 멤버 변수로 접근하는 경우에도 위의 메서드에 접근하는 과정과 같다!
“호출자”의 자료형 클래스에 찾는 변수가 없다면 다음으로 부모 클래스에서 찾는다.
super
를 통해서 접근하게되면, 바로 부모 클래스의 변수/메서드로 접근한다.package extends1.super1;
public class Parent {
public String value = "parent";
public void hello() {
System.out.println("Parent.hello");
}
}
package extends1.super1;
public class Child extends Parent {
public String value = "child";
@Override
public void hello() {
System.out.println("Child.hello");
}
public void call() {
System.out.println("this value = " + this.value); //this 생략 가능
System.out.println("super value = " + super.value);
this.hello(); //this 생략 가능
super.hello();
}
}
/* call() 메서드 실행 결과
this value = child
super value = parent
Child.hello
Parent.hello
*/
super.value
또는 super.hello()
처럼 super
키워드를 통해 변수/메서드를 접근하면Parent
클래스의 value
변수, hello()
메서드로 바로 접근한다.super()
로 사용한다.super()
를 생략할 수 있다.super(매개변수)
를 넣어주면 된다.💡 우리는 생성자 Section에서
this()
를 배웠다. 자신의 다른 생성자를 호출하는 메서드이다.
this()
메서드는 생성자의 첫번째 줄에만 위치할 수 있다. 그렇다면super()
와 겹치지 않겠는가?
아래 예시 코드로 알아보자package extends1.super2; public class ClassA { public ClassA() { //기본 생성자 System.out.println("ClassA 생성자"); } }
package extends1.super2; public class ClassB extends ClassA { public ClassB(int a) { this(a, 0); System.out.println("ClassB 생성자 a=" + a); } public ClassB(int a, int b) { super(); //ClassA의 생성자가 기본 생성자이기에 생략 가능 System.out.println("ClassB 생성자 a=" + a + " b=" + b); } }
package extends1.super2; public class ClassC extends ClassB { public ClassC() { super(10); System.out.println("ClassC 생성자"); } }
package extends1.super2; public class Super2Main { public static void main(String[] args) { ClassC classC = new ClassC(); } } /* 실행 결과 ClassA 생성자 ClassB 생성자 a=10 b=0 ClassB 생성자 a=10 ClassC 생성자 */
- 위 코드를 보면, 자식 클래스인 ClassB에서 매개변수가 하나인 생성자에서는 super()가 아닌 this()를 사용하고 있다. 그리고 this()를 통해 매개변수가 2개인 생성자를 호출하게되면 그 안에 super()가 있음을 알 수 있다.
- 정리하면, this()를 통해 다른 생성자를 호출하는것은 가능하다! 다만, 마지막에 호출되는 생성자에서는 맨 첫줄에 super()를 무조건 사용하여 부모 클래스의 생성자를 호출해야한다!
public final Class Parent{
...
}
// 위 클래스를 상속받을 수 없다! 컴파일 에러 발생
public Class Parent{
public final void print(){
System.out.println("I am Parent Class");
}
}
// 위의 print 메서드는 오버라이디될 수 없다! 컴파일 에러 발생