-
[ch7 자바 객체] 캡슐화(getter & setter)와 다형성(polymorphism)프로그래밍 언어/JAVA 2022. 6. 19. 16:49
캡슐화에 대해 알아보기 전 접근제어자를 알고 넘어가야한다. 위에 포스팅 아래 부분에서 참고!
캡슐화란?
클래스나 멤버, 주로 멤버에 접근 제어자를 사용하는 이유는 클래스 내부에 선언된 데이터를 보호하기 위해서이다. 데이터가 유효한 값을 유지하도록, 또는 비밀번호와 같은 데이터를 외부에서 함부로 변경하지 못하도록 하기 위해서는 외부로부터 접근을 제한하는 것이 필요하다. 이것을 데이터 감추기 (data hiding)라고 하며, 객체지향개념의 캡슐화(encapsulation)에 해당하는 내용이다.
또 다른 이유는 클래스 내에서만 사용되는, 내부 작업을 위해 임시로 사용되는 멤버변수나 부분작업을 처리하기 위한 메서드 등의 멤버들을 클래스 내부에 감추기 위해서이다. 외부에서 접근할 필요가 없는 멤버들을 private로 지정하여 외부에 노출시키지 않음으로써 복잡성을 줄일 수 있다.
예를 들어, 시간을 표시하기 위한 클래스 Time이 다음과 같이 정의되어 있을 때, 클래스의 인스턴스를 생성한 다음, 멤버변수에 직접 접근하여 값을 변경할 수 있을 것이다.
시간에서 멤버변수 hour은 0보다는 같거나 크고 24보다는 작은 범위의 값을 가져야 하지만 위의 코드에서 처럼 잘못된 값을 지정한다고 해도 이것을 막을 방법이 없다. 이런 경우 멤머변수를 private나 protected로 제한하고 멤버변수의 값을 읽고 변경할 수 있도록 public 메서드를 제공함으로써 간접적으로 멤버변수의 값을 다룰 수 있도록 하는 것이 바람직하다.
※ 예제 1번 ※
public class Ex7_6 { public static void main(String[] args) { Time t = new Time(); System.out.println(t.getHour()); //0 t.setHour(16); System.out.println(t.getHour()); //16 t.setHour(28); System.out.println(t.getHour()); //16 } } class time{ //접근 제어자를 private로 하여 외부에서 직접 접근하지 못하게 하였다. private int hour; private int minute; private int second; public int getHour() {return hour;} public void setHour(int hour) { if(hour <0 || hour >23)return; this.hour = hour; } public int getMinute() {return minute;} public void setMinute(int minute) { if(minute <0 || minute >59)return; this.minute = minute; } public int getSecond() {return second;} public void setSecond(int second) { if(second <0 || second >59)return; this.second = second; } }
get으로 시작하는 메서드는 단순히 멤버변수의 값을 반환하는 일을 하고, set으로 시작하는 메서드는 매개변수에 지정된 값을 검사하여 조건에 맞는 값일 때만 멤버변수의 값을 변경하도록 작성되어 있다. 만일 상속을 통해 확장될 것이 예상된는 클래스라면 멤버에 접근 제한을 주되 자손 클래스에서 접근하는 것이 가능하도록 하기 위해 private 대신 protected를 사용한다. private이 붙은 멤버는 자손 클래스에서도 접근이 불가능하기 때문이다.
보통 멤버변수의 값을 읽는 메서드의 이름을 'get멤버변수이름'으로 하고, 멤버변수의 값을 변경하는 메서드의 이름을 'set멤버변수이름'으로 한다.
위의 예제 1번을 살펴볼 때 getHour()은 현재 멤버변수인 hour에 저장되어 있는 값을 반환하고 setHour(int hour)은 매개변수로 받은 값을 검사하여 조건에 맞는 값이면 새로 값을 변경하고 조건에 맞지 않으면 원래 저장되어 있는 값을 반환하도록 되어있다.
다형성(polymorphism)
다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다. 좀 더 구체적으로 말하자면, 조상클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.
class Tv1{ boolean power; int channel; void power() {power =!power;} void channelUp() {++channel;} } class SmartTv1 extends Tv1{ String text; void caption() {} }
위와 같이 조상 클래스 Tv1과 자손클래스 smartTv1이 있다고 했을 때 지금까지는 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다.
TV t = new Tv();
SmartTv s = new SmartTv();위처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만 다형성을 구현함으로써 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능해졌다.
SmartTv s = new SmartTv(); // 참조변수와 인스턴스의 타입 일치
Tv t = new SmartTv(); //타입 불일치. 조상 타입의 참조변수로 자손 인스턴스 참조위의 코드에서 smartTv 인스턴스를 2개 생성하고, 참조변수 s와 t가 생성된 인스턴스를 하나씩 참조하도록 하였다. 그럼 둘이 같은 것일까? 아니다.! Tv타입의 참조변수로는 SmartTv 인스턴스 중에서 Tv 클래스의 멤버들(상속받은 멤버 포함)만 사용할 수 있다. 따라서, 생성된 smartTv 인스턴스의 멤버중에서 Tv 클래스에 정의되지 않응 멤버, text와 caption은 참조변수 t 로는 사용이 불가능하다. 즉) t.text 또는 t.caption()이 불가능하다는 것. 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
정리해보자면)
- 조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
- 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.그럼 왜 다형성을 사용하는걸까?
다형성의 장점
1. 유지보수가 쉽다.
: 개발자가 여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이하다.
2. 재사용성 증가
: 다형성을 활용하면 객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높아진다.
3. 느슨한 결합
: 다형성을 활용하면 클래스간 의존성이 줄어들며 확장성이 높고 결합도가 낮아져 안정성이 높아진다.
다형성의 필수 조건
1. 상속 관계
: 다형성을 활용하기 위해 필수로 조상-자손 간에 상속이 이루어져야 한다.
2. 오버라이딩 필수( 자식 클래스에서 메소드 재정의)
: 다형성이 보장되기 위해서 하위 클래스 메소드가 반드시 재정의 되어 있어야 한다.
3. 업캐스팅(자식 클래스의 객체가 부모 클래스 타입으로 형변환 되는 것)
: 부모 타입으로 자식클래스를 업캐스팅하여 객체를 생성해야 된다.
※ 예제 2번 ※
package ch7; public class Ex7_7 { public static void main(String[] args) { Book n = new Novel("시나공","길벗"); //타입 불일치, 조상 타입의 참조변수로 자손 인스턴스 참조 System.out.println(n.name+", "+n.publisher); n.print(); Book b = new Book("시나공1","길벗1"); //참조변수와 인스턴스의 타입 일치 System.out.println(b.name+", "+b.publisher); b.print(); } } class Book{ public String name; public String publisher; Book(){ this.name = ""; this.publisher = ""; } Book(String name, String publisher){ this.name = name; this.publisher = publisher; } void print(){ System.out.println("print : Book"); } } class Novel extends Book{ public String name; public String publisher; Novel(String name, String publisher){ super(name, publisher); } @Override void print(){ //오버라이딩 재정의 System.out.println("print : Novel"); } }
다형성을 사용하면 부모 클래스로 인스턴스를 선언했지만 동적 바인딩되어 자식클래스의 멤버함수가 호출되는 것을 볼 수 있다.
참조변수의 형변환
기본형 변수처럼 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자손 타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로의 형변환만 가능하다. 바로 윗 조상이나 자손이 아닌, 조상의 조상으로도 형변환이 가능하다. 따라서 모든 참조변수는 모든 클래스의 조상인 Object클래스 타입으로 형변환이 가능하다.
class Car { }
class FireEngine extends Car { }
class Ambulance extends Car { }이와 같이 Car클래스를 상속받고 있는 FireEngine , Ambulance 클래스가 있을 때, FireEngine타입의 참조변수 f는 조상타입인 Car로 형변환이 가능하다. 반대도 물론 Car차입의 참조변수를 자손타입의 FireEngine 으로 형변환하는 것도 가능하다. 그러나! FireEngine 과 Ambulance 는 상속관계가 아니므로 형변환이 불가능하다.
FireEngine f = new FireEngine();
Car c = (Car)f; // ok. 조상인 Car 타입으로 형변환(생략가능)
FireEngine f2 = (FireEngine) c; //ok. 자손인 FireEngine 타입으로 형변환 (생략불가능)
Ambulance a = (Ambulance)f ; //에러, 상속관계가 아닌 클래스 간의 형변환 불가기본형의 형변환과 달리 참조형의 형변환은 변수에 저장된 값(주소값)이 변환되는 것이 아니다. 참조변수의 형변환은 그저 리모콘(참조변수)을 다른 종류의 것으로 바꾸는 것 뿐이다. (예를들어 smartTv에서 Tv 리모컨으로 바꾸는 것처럼) 리모콘 즉) 참조변수를 바꾸는 이유는 사용할 수 있는 멤버 개수를 조절하기 위한 것이다.
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행할 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 먼저 확인하는게 중요하다.
※ 예제 3번 ※
package ch7; public class Ex7_7 { public static void main(String[] args) { Car car = null; FireEngine fe = new FireEngine(); FireEngine fe2 = null; fe.water(); car = fe; // car = (Car)fe;에서 형변환이 생략됨 car.drive();// car에 값을 대입하기 전에 실행시킴 에러! // car.water();//컴파일 에러! Car 타입의 참조변수로 water() 호출 불가 fe2 = (FireEngine)car; //자손타입 <- 조상타입. 형변환 생략 불가 fe2.water(); fe2.drive(); } } class Car{ String color; int door; void drive() { //운전하는 기능 System.out.println("drive, Brr~"); } void stop() { //멈추는 기능 System.out.println("stop!!"); } } class FireEngine extends Car{ void water() {//물 리는 기능 System.out.println("water!!"); } }
Car car = null;
FireEngine fire = new FireEngine();
car = (Car) fe;위 문장을 그림으로 그려보면 위와 같다 참조변수 fe값을 car에 저장해서 car로도 FireEngine 인스턴스를 다룰 수 있게 된 것. 다만 참조변수 fe와 달리 car로는 FireEngine 인스턴스의 멤버 중 4개만 사용 가능하다. why? car의 참조변수는 Car이기 때문에 !!
instanceof 연산자
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용한다. 주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 그리고 연산 결과로 boolean값인 true와 false 중의 하나를 반환한다. instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
void doWork(Car c) { if(c instanceof FireEngine) { FireEngine fe3 = (FireEngine)c; fe3.water(); } }
위으 코드는 Car 타입의 참조변수 c를 매개변수로 하는 메서드이다. 이 메서드가 호출될 때, 매개변수로 Car클래스 또는 자손 클래스으 인스턴스를 넘겨받겠지만 메서드 내에는 정확히 어떤 인스턴스인지 알 길이 없다. 그래서 instanceof 연산자로 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크하고, 적절히 형변환 한 다음에 작업을 해야한다. 조상타입의 참조변수로는 실제 인스턴스의 멤버들을 모두 사용할 수 없기 때문에, 실제 인스턴스와 같은 타입의 참조변수로 형변환을 해야만 인스턴스의 모든 멤버들을 사용할 수 있다.
매개변수의 다형성
참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다. 예를 들어 Product, Tv, Computer, Buyer 클래스가 정의되어 있다고 가정해봤을 때,
class Product{ int price; //제품 가격 int bonusPoint; //제품 구매 시 제공되는 보너스 점수 } class Tv extends Product{} class Computer extends Product{} class Buyer{ //고객, 물건을 사는 사람 int money = 1000; //소유 금액 int bonusPoint = 0; //보너스 점수 }
Product 클래스는 Tv와 Computer 클래스의 조상이며, Buyer클래스는 제품(Product)를 구입하는 사람을 클래스로 표현한 것이다. Buyer 클래스에 물건을 구입하는 기능의 메서드를 추가해보자. 구입할 대상이 필요하므로 매개변수로 구입할 제품을 넘겨 받아야 한다. Tv를 산다고 가정하고 매개변수를 Tv 타입으로 하였다.
void buy(Tv2 t) { //Buyer가 가진 돈(money)에서 제품 가격(t.price)만큼 뺐다. money = money - t.price; //Byert의 보너스점수에 제품 보너스점수를 더한다. bonusPoint = bonusPoint + t.bonusPoint; }
buy(Tv t)는 제품을 구입하면 제품을 구이반 사람이 가진 돈에서 제품의 가격을 빼고, 보너스 점수는 추가하는 작업을 하도록 작성되어 있다. 근데 이 buy메서드는 매개변수로 Tv를 받기 때문에 Tv밖에 살 수 없기 때문에 다른 제품을 구입할 수 있는 메서드가 추가로 필요하다. void buy(Computer c) ,,, 이런 식으로!! 그런데 이렇게 제품의 종류가 늘어날 때마다 새로운 buy 클래스를 추가해주면 너무 비효율적이다. 따라서 메서드의 매개변수에 다형성을 적용하면 간단하게 메서드 하나로 처리할 수 있다.
void buy(Product p) { //Buyer가 가진 돈(money)에서 제품 가격 만큼 뺐다. money -= p.price; //Byert의 보너스점수에 제품 보너스점수를 더한다. bonusPoint += p.bonusPoint; }
매개변수가 Product타입의 참조변수라는 것은, 매개변수로 Product클래스의 자손 타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다. 그리고 Procut 클래스에 price와 bonusPoint가 선언되어 있기 때문에 참조변수 p로 인스턴스의 price와 bonusPoint를 사용할 수 있다.
Buyer b = new Buyer();
Tv t = new Tv();
Computer c = new Computer();
b.buy(t);
b.buy(c);앞으로 다른 제품 클래스를 추가할 때 Product 클래스를 상속받기만 하면, buy(Product p)메서드의 매개변수로 받아들여질 수 있다.
※ 예제 4번 ※
package ch7; public class Ex7_8 { public static void main(String[] args) { Buyer b = new Buyer(); Tv2 t = new Tv2(); b.buy(t); System.out.println("현재 남은 금액은" + b.money); System.out.println("현재 보너스 점수는 " + b.bonusPoint); Computer c = new Computer(); b.buy(c); System.out.println("현재 남은 금액은" + b.money); System.out.println("현재 보너스 점수는 " + b.bonusPoint); } } class Product{ int price; //제품 가격 int bonusPoint; //제품 구매 시 제공되는 보너스 점수 Product(int price) { this.price = price; bonusPoint = (int)(price/10.0); } } class Tv2 extends Product{ Tv2(){ //조상클래스의 생성자 Product(int price)를 호출한다. super(100); } public String toString() { return "Tv"; } } class Computer extends Product{ Computer(){ super(200); } public String toString() { return "Computer"; } } class Buyer{ //고객, 물건을 사는 사람 int money = 1000; //소유 금액 int bonusPoint = 0; //보너스 점수 void buy(Product p) { if(money<p.price) { System.out.println("금액이 부족합니다."); }else { //Buyer가 가진 돈(money)에서 제품 가격 만큼 뺐다. money -= p.price; //Byert의 보너스점수에 제품 보너스점수를 더한다. bonusPoint += p.bonusPoint; System.out.println("구매하신 제품은 " + p +"입니다."); } } }
'참조변수 + 문자열'은 '참조변수.toString()'로 처리된다. 따라서 System.out.println("구매하신 제품은 " + p +"입니다."); 이렇게 입력했을 때 각 제품 메서드의 toString()에 정의 되어 있는 값이 반환 된 것!
여러 종류의 객체를 배열로 다루기
조상 타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로, Product클래스가 Tv, Computer, Audio 클래스의 조상일 때, 다음과 같이 할 수 있는 것을 알았다.
Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();위의 코드를 Product 타입의 참조변수 배열로 처리하면 다음과 같다.
Product p [ ] = new Produc [3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();이처럼 조상 타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다. 이러한 특징을 이용해서 위의 예제 4번에서 Buyer클래스에 구입한 제품을 저장하기 위한 Product 배열을 추가해보도록 한다.
※ 예제 5번 ※
package ch7; public class Ex7_9 { public static void main(String[] args) { Buyer2 b = new Buyer2(); b.buy(new Tv3()); // Tv3 t = new Tv3(); 이렇게 참조변수를 만들어서 인자롤 넣어도 된다. b.buy(new Computer2()); b.summary(); System.out.println("남은 돈" + b.money); System.out.println("적립된 포인트" + b.bonusPoint); } } class Product2{ int price; int bonusPoint; Product2(int price){ this.price = price; bonusPoint = (int)(price/10.0); } } class Tv3 extends Product2{ Tv3(){ super(100); } public String toString() { return "Tv"; } } class Computer2 extends Product2{ Computer2(){ super(200); } public String toString() { return "Computer2"; } } class Buyer2 { int money = 1000; int bonusPoint = 0; Product2 cart []= new Product2[10]; //구입한 제품을 저장하기 위한 배열 int i=0; //Product 배열에 사용될 카운터 void buy(Product2 p) { if(money<p.price) { System.out.println("금액이 부족합니다."); }else { money -= p.price; bonusPoint += p.bonusPoint; cart[i++] = p; // 구입한 제품을 Product2[ ] cart 배열에 저장한다. System.out.println("구입하신 물건은" + p +"입니다."); } } void summary(){ int sum =0; //구입한 물품의 가격 합계 String list=""; //구입한 물품 목록 for(int i =0; i<cart.length; i++) { if(cart[i]==null) break; list += cart[i] + ", "; sum += cart[i].price; } System.out.println("구입하신 물품의 총액은"+sum+"만원 입니다."); System.out.println("구입하신 물품의 목록은" + list+"입니다."); } }
위 예제에서 Product2 배열로 구입한 제품들을 저장할 수 있도록 했지만, 배열의 크기를 10으로 했기 때문에 11개 이상의 제품을 구입할 수 없다. 그렇다고해서 무조건 배열의 크기를 크게 설정할 수도 없는 일.
이런 경우 Vector 클래스를 사용하면 된다. Vector 클래스는 내부적으로 Object타입의 배열을 가지고 있어, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다. 그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경 쓰지 않아도 된다.
public class Vector extends AbstractList implements List, Cloneable, java.io.Serializable{
protected Object elementData[];
}반응형'프로그래밍 언어 > JAVA' 카테고리의 다른 글
[ch7 자바 객체] 내부 클래스(inner class)와 익명 클래스(anonymous class) (0) 2022.06.22 [ch7 자바 객체] 추상(abstract) 클래스와 인터페이스(interface) (0) 2022.06.22 [ch7 자바 객체] 패키지(package)와 import, 접근제어자(access modifier) (0) 2022.06.17 [자바 JAVA] 자동으로 import하는 꿀 방법 (0) 2022.06.17 [ch7 자바 객체] 상속과 오버라이딩(overriding), super 알아보기 (0) 2022.06.16