4. 제어자(modifier)
4.1 제어자란?
제어자(modifier)는 클래스, 변수 또는 메서드의 선언부와 함께 사용되어 부가적인 의미를 부여한다.
접근제어자 public, protected, default, private <- 4개중에 1개만 가능
그 외 static, final, abstract
제어자는 클래스나 멤버변수와 메서드에 주로 사용되며, 하나의 대상에 대해서 여러 제어자를 조합하여 사용하는 것이 가능하다. 단, 접근제어자는 한 번에 네 가지 중 하나만 선택해서 사용할 수 있다.
❗제어자들 간에 순서는 관계없지만 주로 접근 제어자를 제일 왼쪽에 놓는 경향이 있다.
4.2 static - 클래스의, 공통적인
static은 ‘클래스의’ 또는 ‘공통적인’의 의미를 가지고 있다.
인스턴스메서드와 static메서드의 근본적인 차이는 메서드 내에서 인스턴스 멤버를 사용하는가의 여부에 있다.
static이 사용될 수 있는 곳 - 멤버변수, 메서드, 초기화블럭
제어자 | 대상 | 의미 |
static | 멤버변수 | -모든 인스턴스에 공통적으로 사용되는 클래스변수가 된다. -클래스변수는 인스턴스를 생성하지 않고도 사용 가능하다. -클래스가 메모리에 로드될 때 생성된다 |
메서드 | -인스턴스를 생성하지 않고도 호출이 가능한 static메서드가 된다. -static메서드 내에서는 인스턴스멤버들을 직접 사용할 수 없다. |
4.3 final - 마지막의, 변경될 수 없는
거의 모든 대상에 사용될 수 있다.
변수에 사용하면 값을 변경할 수 없는 상수가 되며, 메서드에 사용되면 오버라이딩을 할수 없게 되고 클래스에 사용되면 자신을 확장하는 자손클래스를 정의하지 못하게 된다.
final이 사용될 수 있는 곳 - 클래스, 메서드, 멤버변수, 지역변수
제어자 | 대상 | 의미 |
final | 클래스 | 변경될 수 없는 클래스, 확장될 수 없는 클래스가 된다. 그래서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다. |
메서드 | 변경될 수 없는 메서드, final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다. |
|
멤버변수 | 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다. | |
지역변수 |
❗대표적인 final클래스 - String, Math
생성자를 이용한 final멤버 변수의 초기화
이 기능을 활용하면 각 인스턴스마다 final이 붙은 멤버변수가 다른 값을 갖도록 하는 것이 가능하다.
4.4 abstract - 추상의, 미완성의
abstract는 ‘미완성’의 의미를 가지고 있다. 메서드의 선언부만 작성하고 실제 수행내용은 구현하지 않은 추상 메서드를 선언하는데 사용된다.
abstract가 사용될 수 있는 곳 - 클래스, 메서드
제어자 | 대상 | 의미 |
absctract | 클래스 | 클래스 내에 추상 메서드가 선언되어 있음을 의미한다. |
메서드 | 선언부만 작성하고 구현부는 작성하지 않은 추상 메서드임을 알린다. |
인스턴스를 생성하지 못하게 클래스 앞에 제어자 'abstract'를 붙여 놓은 것이다.
✏️추상클래스를 상속받아서 완전한 클래스를 만든후에 객체생성가능
4.5 접근 제어자(access modifier)
접근 제어자는 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스 외부에서 접근하지 못하도록 제한하는 역할을 한다.
접근 제어자가 사용될 수 있는 곳 - 클래스, 멤버변수, 메서드, 생성자
- private 같은 클래스 내에서만 접근이 가능하다.
- default 같은 패키지 내에서만 접근이 가능하다.
- protected 같은 패키지 + 다른 패키지의 자손 클래스에서 접근이 가능하다.
- public 접근 제한이 전혀 없다.
제어자 | 같은클래스 | 같은패키지 | 자손클래스 | 전체 |
public | ————— | ————— | ————— | ————— |
protected | ————— | ————— | ————— | |
(defaulte) | ————— | ————— | ||
private | ————— |
public > protected > (defaulte) > private
[대상에 따라 사용할 수 있는 접근 제어자]
대상 | 사용 가능한 접근자 |
클래스 | public, (default) |
메서드 | public, protected, (default), private |
멤버변수 | public, protected, (default), private |
지역변수 | 없음 |
✏️public 은 파일명과 일치하는 클래스만 사용 가능함
접근 제어자를 이용한 캡슐화
클래스나 멤버, 주로 멤버에 접근 제어자를 사용하는 이유는 클래스 내부에 선언된 (외부로부터)데이터를 보호하기 위해서이다.
데이터가 유효한 값을 유지하도록, 또는 비밀번호와 같은 데이터를 외부에서 함부로 변경하지 못하도록 하기 위해서는 외부로부터의 접근을 제한하는 것이 필요하다.
이것을 데이터 감추기(data hiding)라고 하며, 객체지향개념의 캡슐화(encapsulation)에 해당하는 내용이다.
외부에서 접근할 필요가 없는 멤버들을 private으로 지정하여 외부에 노출시키지 않음으로써 복잡성을 줄일 수 있다. 이것 역시 캡슐화에 해당한다.
접근제어자를 사용하는 이유
- 외부로부터 데이터를 보호하기 위해서
- 외부에는 불필요한, 내부적으로만 사용되는, 부분을 감추기 위해서
만일 메서드 하나를 변경해야 한다고 가정했을 때, 오류가 없는지 테스트해야 한다.
- public = 테스트 범위가 넓다
- default = 패키지 내부만 확인하면 된다
- private = 클래스 하나만 살펴보면 된다.
이처럼 접근 제어자 하나가 때로는 상당한 차이를 만들어낼 수 있다.
잘못된 값을 지정한다고 해도 이것을 막을 방법은 없다. 멤버변수를 private이나 protected로 제한하고 멤버변수의 값을 읽고 변경할 수 있는 public메서드를 제공함으로써 간접적으로 멤버변수의 값을 다룰 수 있도록 하는 것이 바람직하다.
보통 멤버변수의 값을 읽는 메서드의 이름을 'get멤버변수이름'으로 하고, 멤버변수의 값을 변경하는 메서드의 이름을 'set멤버변수이름'으로 한다.
*암묵적인 규칙 get = getter게터 set = setter세터
생성자의 접근 제어자
생성자에 접근 제어자를 사용함으로써 인스턴스의 생성을 제한할 수 있다. 보통 생성정자의 접근 제어자는 클래스의 접근 제어자와 같지만, 다르게 지정할 수도 있다.
생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 그래서 클래스 앞에 final을 더 추가하여 상속할 수 없는 클래스라는 것을 알리는 것이 좋다.
4.6 제어자(modifier)의 조합
대상 | 사용 가능한 제어자 |
클래스 | public, (default), final, abstrack |
메서드 | 모든 접근 제어자, final, abstrack, static |
멤버변수 | 모든 접근 제어자, final, static |
지역변수 | final |
제어자를 조합해서 사용할 때 주의사항
- 메서드에 static과 abstrack를 함께 사용할 수 없다.
- 클래스에 abstrack와 final을 동시에 사용할 수 없다.
- abstrack메서드의 접근 제어자가 private일 수 없다.
- 메서드에 private과 final을 같이 사용할 필요는 없다.
5. 다형성(polymorphism)
5.1 다형성이란?
객체지향개념에서 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것
class Tv {
boolean power;
int channel;
void power() {}
void channelUp() {}
void channelDown() {}
}
class CaptionTv extends Tv {
String text;
void caption() {}
}
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv(); //조상타입의 참조변수로 자손 인스턴스를 참조
Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다.
둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 불가능하다. 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다.
참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.
❗클래스는 상속을 통해서 확장될 수는 있어도 축소될 수는 없어서, 조상 인스턴스의 멤버 개수는 자손 인스턴스의 멤버 개수보다 항상 적거나 많다.
❗모든 참조변수는 null 또는 4 byte의 주소값이 저장되며, 참조변수 타입은 참조할 수 있는 객체의 종류와 사용할 수 있는 멤버의 수를 결정한다.
조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다. 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.
5.2 참조변수의 형변환
결론 - 사용할 수 있는 멤버의 개수를 조절하는 것(리모콘 변경)
기본형 변수와 같이 참조변수도 형변환이 가능하다. 단,서로 상속관계에 있는 클래스사이에서만 가능
자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로의 형변환만 가능하다.
❗바로 윗 조상이나 자손이 아닌, 조상의 조상으로도 형변환이 가능하다.(모든 참조변수는 Object클래스 타입으로 형변환 가능)
자손타입->조상타입(Up-casting) : 형변환 생략가능
조상타입->자손타입(Down-casting) : 형변환 생략불가
다운캐스팅 : 조상타입의 참조변수를 자손타입의 참조변수로 변환하는 것
업캐스팅 : 자손타입의 참조변수를 조상타입의 참조변수로 변환하는 것
참조변수간의 형변환 역시 캐스트연산자를 사용, 괄호()안에 변환하고자 하는 타입의 이름(클래스명)을 적어주면 된다.
❗상속관계도를 그려보면 형변환가능여부쉽게 확인 가능
형변환을 수행하기 전에 instanceof연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스 타입을 확인하는 것이 안전하다.
형변환은 참조변수의 타입을 변환하려는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv(); //조상타입의 참조변수로 자손 인스턴스를 참조
//Tv t = (Tv)new CaptionTv(); 의 생략된 형태
//두줄로 나누면 이해하기 쉽다.
CaptionTv c = new CaptionTv();
Tv t = (Tv)c;
class CastingTest1 {
public static void main(String[] args) {
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
fe.water();
car = fe; //car = (Car)fe;에서 형변환이 생략된 형태다
car.water();
fe2 = (FireEngine)car; //자손타입<-조상타입
fe.water();
}
}
class Car {
String color;
int door;
void drive() {/*작업내용*/}
void stop() {/*작업내용*/}
}
class FireEngine extends Car {
void water() {/*작업내용*/}
}
- Car car = null;
Car타입의 참조변수 car를 선언하고 null로 초기화한다. - 2. FireEngine fe = new FireEngine();
FireEngine인스턴스를 생성하고 FireEngine타입의 참조변수가 참조하도록 한다. - car = fe; //조상타입 ← 자손타입
참조변수 car를 통해서도 FireEngine인스턴스를 사용할 수 있지만, fe와는 달리 car는 Car타입이므로 Car클래스의 멤버가 아닌 water()는 사용할 수 없다. - fe2 = (FireEngine)car; //자손타입 ← 조상타입
✏️리모콘을 변경함으로써 사용가능 멤버를 늘렸다 줄였다 할 수 있다.
fe = 5개, car = 4개, fe2 = 5개
Car car = new Car();
FireEngine fe = null;
fe = (FireEngine)car; //실행시 에러 발생
car가 참조하고 있는 인스턴스가 Car타입의 인스턴스이다.
조상타입의 인스턴스를 자손타입의 참조변수로 참조하는것은 혀용되지 않는다.ㅏ
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.
5.3 instanceof 연산자
①확인. 형변환 해도 되는지(instanceof로)
②형변환
- 참조변수의 형변환 가능여부 확인에 사용. 가능하면 true 반환
- 형변환 전에 반드시 instanceof로 확인해야 함
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.
주로 조건문에 사용됨
instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.
그리고 연산의 결과로 boolean값인 true와 false 중의 하나를 반환한다.
❗값이 null인 참조변수에 instanceof연산을 수행하면 false결과
//Car타입의 참조변수 c를 매개변수로 하는 메서드
//이 메서드가 호출될 때 매개변수로 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨받는다.
//메서드 내에서는 정확히 어떤 인스턴스인지 알 길이 없 다.
//instanceof연산자를 이용해서 참조변수 c가 가리기코 있는 인스턴스의 타입을 체크
void doWork(Car c) { //Car또는 Car의 모든 자손이 여기에 들어올 수 있음.
//new Car(); new FireEngine(); new Ambulance();
if (c instanceof FireEngine) { //형변환이 가능한지 확인 //ture
//c가 가리키는 객체가 FireEngine 타입이냐?(또는 자손이냐?)물어보는 것
FireEngine fe = (FireEngine)c;
fe.water();
}else if (c instanceof Ambulance) {
Ambulance a = (Ambulance)c;
a.siren();
}
}
어떤 타입에 대한 instanceof연산의 결과가 true라는 것은 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
5.4 참조변수와 인스턴스의 연결
조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입의 참조변수로 자손 타입의 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
❗static메서드는 static변수처럼 참조변수의 타입에 영향을 받는다. 참조변수의 타입에 영향을 받지 않는 것은 인스턴스메서드 뿐이다. 그래서 static메서드는 반드시 참조변수가 아닌 ‘클래스이름.메서드()’로 호출해야 한다.
멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 대는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언도니 멤버변수가 사용된다.
메서드인 method()는 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라서 달라진다.
class Test {
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();
System.out.println(p.x);
p.method();
System.out.println(c.x);
c.method();
}
}
class Parent {
int x = 100;
void method() {
System.out.println("Parent Method");
}
}
class Child extends Parent {
int x = 200;
void method() {
System.out.println("Child Method");
}
}
▼실행결과
100
Child Method
200
Child Method
자손 클래스에서 조상 클래스의 멤버를 중복으로 정의하지 않았을 때는 참조변수의 타입에 따른 변화는 없다.
멤버변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록 하지, 이번 예제에서처럼 다른 외부 클래스에서 참조변수를 통해 직접적으로 인스턴스변수에 접근할 수 있게 하지 않는다.
5.5 매개변수의 다형성
장점
- 다형적 매개변수
- 하나의 배열(보통 같은 타입 저장)로 여러 종류 객체 다루기 -> 다형성 이용하면 가능
-참조형 매개변수는 메서드 호출시, 자신과 같은 타입 또는 자손타입의 인스턴스를 넘겨줄 수 있다.
class Product {
int price;
int bonusPoint;
}
class Tv extends Product {}
class Computer extends Product{}
class Audio extends Product{}
class Buyer {
int money = 1000;
int bonusPoint = 0;
void buy(Tv t) {//Tv로 하면 모든제품을 다 만들어줘야함
//Buyer가 가진 돈(money)에서 제폼의 가격(t.price)만큼 뺀다.
money = money-t.price;
//Buyer의 보너스점수(bonusPoint)에 제품의 보너스점수(t.bonusPoint)를 더한다.
bonusPoint = bonusPoint + t.bonusPoint;
}
void buy(Product p) { //이 메서드 하나로 여러 물건들을 살 수 있음
money = money-p.price; //money -= p.price;
bonusPoint = bonusPoint + p.bonusPoint; //bonusPoint += p.bonusPoint;
//매개변수가 Product타입의 참조변수라는 것은,
//메서드의 매개변수로 Product클래스의 자손타입의 참조변수면
//어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.
}
}
class PolyargumentTest {
public static void main(String[] args) {
Buyer b = new Buyer();
b.buy(new Tv()); //buy(Product p) 호출
}
}
앞으로 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product p)메서드의 매개변수로 받아들여질 수 있다.
Buyer b = new Buyer();
Tv t = new Tv();
Conputer c = new Computer();
b.buy(t);
b.buy(c);
❗Tv t = new Tv(); b.buy(t); 를 한 문장으로 줄이면 b.buy(new Tv()); 가 된다. →참조변수가 없어서 메인메서드 안에서 Tv사용불가(리모컨x)buy메서드 안에선 가능
5.6 여러 종류의 객체를 배열로 다루기
Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();
//위 코드를 Product타입의 참조변수 배열로 처리하면 아래와 같다.
Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2[ = new Audio();
조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다. 또는 묶어서 다루고싶은 객체들의 상속관계를 따져서 가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성해서 객체들을 저장하면 된다.
//Product배열 추가 //많이 씀 기본적인거
class Buyer { //Buyer클래스에 구입한 제품을 저장하기 위한 Product배열 추가
int money = 1000;
int bonusPoint = 0;
Product[] item = new Product[10]; //구입한 제품을 저장하기 위한 배열
int i = 0; //Product배열 item에 사용될 index
void buy(Product p) {
if(money < p.price) {
System.out.println("잔액이 부족하여 물건을 살수 없습니다.");
return;
}
money -= p.price; //가진 돈에서 제품가격을 뺀다
bonusPoint += p.bonusPoint; //제품의 보너스포인트를 더한다.
item[i++] = p; //제품을 Product[] item에 저장한다.
System.out.println(p + "을/를 구입하셨습니다.");
}
}
출처 : 남궁성. 「자바의 정석」. 도우출판. 2016
'프로그래밍언어 > Java' 카테고리의 다른 글
[자바의 정석] 09. java.lang패키지와 유용한 클래스 (1) - java.lang패키지 (0) | 2023.08.05 |
---|---|
[자바의 정석] 07. 객체지향 프로그래밍Ⅱ(3) (0) | 2023.08.03 |
[자바의 정석] 07. 객체지향 프로그래밍Ⅱ(1) (0) | 2023.08.01 |
[자바의 정석] 06. 객체지향 프로그래밍Ⅰ(2) (0) | 2023.07.31 |
[자바의 정석] 06. 객체지향 프로그래밍Ⅰ(1) (0) | 2023.07.29 |