제어자(Modifier)는 클래스, 변수, 메소드의 선언부에 함께 사용되며, 부가적인 의미를 부여한다.
제어자는 접근제어자와 그 기타 제어자로 나뉜다.
static은 "공통적인", "클래스의"라는 의미를 갖는다.
이전에 배웠던 static
에 대해 다시 상기시켜보자.
변수 앞에 static
으로 선언하면 jvm의 stack area
에 저장이 되면서 인스턴스가 생성이 되도 늘 같은 값을 갖게 된다. 또한, 클래스가 로드되는 시기에 변수가 생성이 되기 때문에 인스턴스 생성 없이 클래스.변수
형태로도 접근이 가능했다. 또한, stack area
에 저장이 되기 때문에 모든 인스턴스에서 변수의 접근이 가능했다.
물론, 이는 변수 뿐 아니라 메소드 역시 가능하다. 즉, 메소드에 static
키워드를 붙이면 static
을 붙인 변수와 동일하게 인스턴스를 생성하지 않아도 사용할 수 있다.
우리가 콘솔입력을 받기 위해 Scanner
클래스의 인스턴스를 생성해서 콘솔 입력을 받는것에 반해 제곱근을 구하기 위해서는 Math
클래스를 생성하지 않고도 Math.sqrt()
메소드를 호출할 수 있는 이유는 Math
클래스의 sqrt()
메소드가 static
으로 선언되 있기 때문이다.
final은 "마지막의", "변경할 수 없는" 이라는 의미를 갖는다.
final
키워드를 사용하면 변수의 경우 값을 변경할 수 없는 상수 형태의 변수가 된다.
final int CNT=10;
CNT=20;// 컴파일 오류!
또한 메소드 앞에 final
키워드를 붙이면 오버라이딩을 할 수 없다.
final int function(){
System.out.println("hello.");
}
@Override
final int function(){// 컴파일 오류! 오버라이딩 불가!
System.out.println("Hello World.");
}
만약 클래스 앞에 final
키워드를 붙이면 상속을 할 수 없다.
final class Car{
}
class SuperCar extended Car{// 컴파일 오류! 상속 불가!
}
abstract는 "추상의", "미완성의"라는 의미를 갖는다.
abstract
는 "추상의", "미완성의"라는 의미를 갖는 제어자로, 메소드나 클래스 앞에 붙여 실제 내용은 구현하지 않는 추상메소드를 선언하는데 주로 사용한다.
또한 클래스 앞에 붙여 추상클래스를 만드는데 사용한다.
추상클래스와 추상메소드는 추후에 자세히 다루도록 하겠다.
접근제어자란 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다.
우리가 클래스를 설계하면서 외부에서 임의로 내부의 변수를 수정하지 못하도록 하고 싶은 경우가 있다. 예를들어보자.
아래와 같이 자동차를 제어하는 클래스가 있다 가정하자.
public class Car {
boolean startupState;
float speed;
{// 초기화 블럭
this.startupState=false;
this.speed=0.0f;
}
void startUp() {// 시동걸기
this.startupState=true;
System.out.println("시동걸림.");
}
void drive(float speed) {// 이동
this.speed=speed;
System.out.println("속도: "+this.speed+"km");
}
void stop() {// 브레이크
this.speed=0.0f;
this.startupState=false;
System.out.println("시동끔.");
}
void horn() {// 경적
System.out.println("빵빵!! 피자빵!! 초코빵!! 크림빵!!");
}
}
이 자동차를 제어하는 클래스는 stop()
메소드를 사용해 시동을 끌 수 있도록 설계가 됬다. 그러나, 실제로는 다음과 같은 방법으로도 시동을 끌 수 있다.
Car car1 = new Car();
car1.startupState=false;// 임의로 시동 끔
이와같이 변수에 직접 접근해서 시동을 끄는것 역시 가능하다. 그러나, 우리가 클래스를 설계한 의도는 stop()
메소드를 사용해서 시동을 끄는 것이지, 사용자가 변수에 직접 접근해서 시동을 끄는 기능을 의도하지는 않았다. 이러한 의도하지 않은 동작을 막기 위해서는 인스턴스 내에 있는 시동을 제어하는 변수의 외부 접근을 차단할 필요가 있다. 이를 캡슐화(encapsulation)
라고 한다.
즉, 캡슐화
는 필요한 부분만 노출하고, 필요 없는 부분은 외부 접근을 차단해 클래스가 설계된 대로 동작하도록 하는것이다.
이러한 캡슐화는 접근제어자를 통해 가능하다.
접근제어자는 접근 허용 범위에 따라
public
,protected
,default
,private
로 구분된다.
public
은 접근제한이 없는 것으로, 외부로부터의 모든 접근을 허용한다. 즉, 같은 클래스의 멤버는 물론이고, 다른 패키지에서도 모두 접근이 가능하다.
public class AccessModifier{
public int num;// 어디서든 접근 가능
public void printTest(){// 어디서든 접근/사용 가능
System.out.println("hello world");
}
}
참고로 대부분의 클래스는 public
을 붙여 외부에서 클래스를 사용할 수 있도록 하고있다. 즉, 대부분의 클래스는 다음과 같이 작성하는것이 일반적이다.
public class AccessModifier{
..// 클래스 내용
}
protected
는 같은 패키지 내에서는 전부 접근이 가능하나, 다른 패키지에서 import
를 이용해 클래스를 불러오면 해당 클래스를 상속했을 경우에만 접근이 가능하다.
public class AccessModifier{
protected int num;// 같은 패키지, 다른 패키지는 상속만 접근가능
protected void printTest(){// 같은 패키지, 다른 패키지는 상속만 접근/사용 가능
System.out.println("hello world");
}
}
예를들어 다음과 같은 경우는 사용이 불가능하다.
public class AccessModifier{
protected int num;// 같은 패키지, 다른 패키지는 상속만 접근가능
protected void printTest(){// 같은 패키지, 다른 패키지는 상속만 접근/사용 가능
System.out.println("hello world");
}
}
외부 패키지
import accessModifier;
public class Main{
public static void main(String[] args){
AccessModifier accessTest = new AccessModifier();
accessTest.printTest();// 사용불가!!
}
}
default
는 같은 패키지에서만 접근이 가능한 것으로, 앞에 접근제어자를 붙이지 않으면 default
가 된다.
public class AccessModifier{
int num;// 같은 패키지에서 접근 가능
void printTest(){// 같은 패키지에서 접근 가능
System.out.println("hello world");
}
}
즉, 같은 패키지 내에서는 인스턴스를 생성하고 접근이 가능하며, 같은 패키지에서 상속을 받아도 super
키워드로 접근이 가능하나, 다른 패키지에서는 접근이 불가능하다.
private
의 경우 해당 클래스 내에서만 접근이 가능하며, 같은 패키지라도 인스턴스를 생성하거나 상속받아도 자식클래스에서 부모클래스의 멤버에 접근이 불가능하다.
즉, private
의 경우 클래스 내에서만 접근이 가능하다.
public class AccessModifier{
private int num;// 클래스 내에서만 접근이 가능
private void printTest(){// 클래스 내에서만 접근이 가능
System.out.println("hello world");
}
}
따라서 다음과 같은 경우 접근이 불가능하다.
public class Main{
public static void main(String[] args){
AccessModifier accessTest = new AccessModifier();
accessTest.printTest();// 사용불가!
}
}
이제 학습한 접근제어자를 이용해 캡슐화를 해보자. 코드는 이전에 작성했던 자동차 제어 클래스를 사용하도록 하겠다.
public class Car {
boolean startupState;
float speed;
{// 초기화 블럭
this.startupState=false;
this.speed=0.0f;
}
Car(){}
Car(float speed){
this.speed=speed;
}
void startUp() {// 시동걸기
this.startupState=true;
System.out.println("시동걸림.");
}
void drive(float speed) {// 이동
this.speed=speed;
System.out.println("속도: "+this.speed+"km");
}
void stop() {// 브레이크
this.speed=0.0f;
this.startupState=false;
System.out.println("시동끔.");
}
void horn() {// 경적
System.out.println("빵빵!! 피자빵!! 초코빵!! 크림빵!!");
}
}
위 코드에서 가장 먼저 수정해야할 부분은 바로 멤버 변수이다. 앞에서 언급한 바와 같이 현재 코드는 인스턴스를 생성하면 외부에서 멤버 변수에 대해 임의 적으로 접근이 가능한 상태이다.
이를 막기 위해 멤버변수를 private
로 접근을 제한하면 다음과 같다.
public class Car {
private boolean startupState;// 접근제한
private float speed;// 접근제한
{// 초기화 블럭
this.startupState=false;
this.speed=0.0f;
}
Car(){}
Car(float speed){
this.speed=speed;
}
void startUp() {// 시동걸기
this.startupState=true;
System.out.println("시동걸림.");
}
void drive(float speed) {// 이동
this.speed=speed;
System.out.println("속도: "+this.speed+"km");
}
void stop() {// 브레이크
this.speed=0.0f;
this.startupState=false;
System.out.println("시동끔.");
}
void horn() {// 경적
System.out.println("빵빵!! 피자빵!! 초코빵!! 크림빵!!");
}
}
다음으로, 외부 메소드는 늘 접근이 가능해야 하므로 public
으로 설정하면 다음과 같다.
public class Car {
private boolean startupState;
private float speed;
{// 초기화 블럭
this.startupState=false;
this.speed=0.0f;
}
Car(){}
Car(float speed){
this.speed=speed;
}
public void startUp() {// 시동걸기
this.startupState=true;
System.out.println("시동걸림.");
}
public void drive(float speed) {// 이동
this.speed=speed;
System.out.println("속도: "+this.speed+"km");
}
public void stop() {// 브레이크
this.speed=0.0f;
this.startupState=false;
System.out.println("시동끔.");
}
public void horn() {// 경적
System.out.println("빵빵!! 피자빵!! 초코빵!! 크림빵!!");
}
}
이렇게 하면 외부에서 인스턴스를 생성하면 메소드에만 접근할 수 있기 때문에 캡슐화가 되었다.