다형성 간단히 설명
- 다형성 : 다양한 객체를 통해 다양한 실행결과를 출력시킴
- 다형성의 예 : 모든 타이어는 달리게 해주는 기능이 들어있지만 어떤 타이어를 장착하느냐에 따라 주행 성능이 달라짐
- 예를 들어 자동차는 한국타이어와 금호타이어 중에서 타이어 타입을 선택하고 각 타이어 마다 성능은 다르게 나온다.
- 다형성을 구현하려면 메소드 재정의 + 타입변환이 필요하다.
자동 타입변환
- 클래스에서의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생하며 자식이 부모 타입으로 자동 변환이 가능하다.
- 자동 타입 변환 (=promotion)은 프로그램 실행 도중에 자동적으로 일어나는 타입 변환을 말함
- 자동 타입 변환 형태 : 부모타입 변수 = 자식타입;
- 자동 타입 변환 개념 : 자식이 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급된다는 의미를 가짐
- 예시
class Animal {} // 부모 클래스
class Cat extends Animal {} // 자식 클래스
Cat cat = new Cat();
Animal animal = cat; // 자동 타입 변환
Animal animal = new Cat();
- 메모리 상태 : cat 변수와 animal 변수는 각각 타입은 다르지만 둘다 Cat 객체를 참조한다.
- 실제로 == 연산시 true 가 나오며 이는 두 변수가 동일한 객체를 참조함을 의미한다.
자동 타입변환 예제
class A {} // 조상
class B extends A {} // B = A의 자식
class C extends A {} // C = A의 자식
class D extends B {} // D = B의 자식, A의 손자
class E extends C {} // E = C의 자식, A의 손자
public class PromotionExample {
public static void main(String[] args) {
B b = new B();
C c = new C();
D d = new D();
E e = new E();
A a1 = b; // 부모 타입으로 자동 타입 변환
A a2 = c; // 부모 타입으로 자동 타입 변환
A a3 = d; // 조부모 타입으로 자동 타입 변환
A a4 = e; // 조부모 타입으로 자동 타입 변환
B b1 = d; // 부모 타입으로 자동 타입 변환
C c1 = e; // 부모 타입으로 자동 타입 변환
// B b2 = e; // B와 E는 상속 관계에 있지 않으므로 에러 발생
// C c2 = d; // C와 D는 상속 관계에 있지 않으므로 에러 발생
}
}
재정의된 메소드로의 접근
- 부모 타입으로 자동 타입 변환이 되면 부모 클래스의 필드와 메소드만 접근이 가능함
- 비록 변수가 자식 객체를 참조하지만 접근 가능한 멤버는 부모 클래스 멤버만 가능
- 그러나 부모 클래스의 메소드가 자식 클래스에서 재정의되었다면 메소드 호출시 자식 클래스의 메소드가 대신 호출됨
(이것이 바로 다형성의 개념)
재정의된 메소드로의 접근 예제
public class Parent {
//메소드
public void method1() {
System.out.println("Parent-method1()");
}
public void method2() {
System.out.println("Parent-method2()");
}
}
public class Child extends Parent {
@Override
public void method2() {
System.out.println("Child-method2()"); // method2는 재정의된 메소드임
}
public void method3() {
System.out.println("Child-method3()");
}
}
public class ChildExample {
public static void main(String[] args) {
Child child = new Child();
Parent parent = child; // 자동 타입 변환
parent.method1(); // Parent-method1() 출력
parent.method2(); // 재정의된 메소드가 호출됨 -> Child-method2() 출력
// parent.method3(); // 호출 불가능
}
}
필드의 다형성
1. Tire 클래스
public class Tire {
// 필드
public int maxRotation; // 최대 회전수(타이어 수명)
public int accumulatedRotation; // 누적 회전수
public String location; // 타이어의 위치
// 생성자
public Tire(String location, int maxRotation) {
this.location = location; // 초기화
this.maxRotation = maxRotation; // 초기화
}
//메소드
public boolean roll() {
++accumulatedRotation; // 누적 회전수 1 증가
if(accumulatedRotation < maxRotation) {
System.out.println(location + " Tire 수명: " + (maxRotation-accumulatedRotation) + "회");
return true; // 정상회전(누적<최대)일 경우 실행
} else {
System.out.println("*** " + location + " Tire 펑크 ***");
return false; // 펑크(누적=최대)일 경우 실행
}
}
}
2. Car 클래스
public class Car {
//필드
Tire frontLeft = new Tire("앞 왼쪽", 6); // 자동차에 4개의 타이어 객체 생성
Tire frontRight = new Tire("앞 오른쪽", 2);
Tire backLeft = new Tire("뒤 왼쪽", 3);
Tire backRight = new Tire("뒤 오른쪽", 4);
//생성자
//메소드
int run() {
System.out.println("[자동차가 달립니다.]");
// 모든 타이어가 1회씩 실행되어야 하므로 각 Tire 객체의 roll() 메소드를 호출
// false(펑크)를 리턴하는게 나오면 stop() 메소드 호출 후 해당 타이어 번호를 리턴
if(frontLeft.roll() == false) {stop(); return 1;}
if(frontRight.roll() == false) {stop(); return 2;}
if(backLeft.roll() == false) {stop(); return 3;}
if(backRight.roll() == false) {stop(); return 4;}
return 0;
}
void stop() {
System.out.println("[자동차가 멈춥니다.]"); // 펑크났을때 실행
}
}
3. Tire의 자식 클래스
public class HankookTire extends Tire {
//생성자
public HankookTire(String location, int maxRotation) {
super(location, maxRotation);
}
//메소드
@Override // 조금 다르게 내용을 출력하려고 roll() 메소드를 재정의함
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation < maxRotation) {
System.out.println(location + " HankookTire 수명: " + (maxRotation - accumulatedRotation) + "회");
return true;
} else {
System.out.println("*** " + location + " HankookTire 펑크 ***");
return false;
}
}
}
public class KumhoTire extends Tire {
//생성자
public KumhoTire(String location, int maxRotation) {
super(location, maxRotation);
}
//메소드
@Override // 조금 다르게 내용을 출력하려고 roll() 메소드를 재정의함
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation < maxRotation) {
System.out.println(location + " kumhoTire 수명: " + (maxRotation - accumulatedRotation) + "회");
return true;
} else {
System.out.println("*** " + location + " KumhoTire 펑크 ***");
return false;
}
}
}
4. CarExample 클래스(실행 클래스)
public class CarExample {
public static void main(String[] args) {
Car car = new Car(); // Car 객체 생성
for(int i=1; i<=5; i++) { // Car 객체의 run() 메소드 5번 반복 실행
int problemLocation = car.run();
switch(problemLocation) {
case 1: // 앞 왼쪽 타이어가 펑크났을때 HankookTire로 교체
System.out.println("앞 왼쪽 타이어를 HankokTire로 교체");
car.frontLeft = new HankookTire("앞 왼쪽", 15);
break;
case 2: // 앞 오른쪽 타이어가 펑크났을때 KumhoTire로 교체
System.out.println("앞 오른쪽 타이어를 KumhoTire로 교체");
car.frontRight = new KumhoTire("앞 오른쪽", 13);
break;
case 3: // 뒤 왼쪽 타이어가 펑크났을때 HankookTire로 교체
System.out.println("뒤 왼쪽 타이어를 HankokTire로 교체");
car.backLeft = new HankookTire("뒤 왼쪽", 14);
break;
case 4: // 뒤 오른쪽 타이어가 펑크났을때 KumhoTire로 교체
System.out.println("뒤 오른쪽 타이어를 KumhoTire로 교체");
car.backRight = new KumhoTire("뒤 오른쪽", 17);
break;
}
System.out.println("------------------------------------");
}
}
}
5. 출력 결과
[자동차가 달립니다.]
앞 왼쪽 Tire 수명: 5회
앞 오른쪽 Tire 수명: 1회
뒤 왼쪽 Tire 수명: 2회
뒤 오른쪽 Tire 수명: 3회
------------------------------------
[자동차가 달립니다.]
앞 왼쪽 Tire 수명: 4회
*** 앞 오른쪽 Tire 펑크 ***
[자동차가 멈춥니다.]
앞 오른쪽 타이어를 KumhoTire로 교체
------------------------------------
[자동차가 달립니다.]
앞 왼쪽 Tire 수명: 3회
앞 오른쪽 kumhoTire 수명: 12회
뒤 왼쪽 Tire 수명: 1회
뒤 오른쪽 Tire 수명: 2회
------------------------------------
[자동차가 달립니다.]
앞 왼쪽 Tire 수명: 2회
앞 오른쪽 kumhoTire 수명: 11회
*** 뒤 왼쪽 Tire 펑크 ***
[자동차가 멈춥니다.]
뒤 왼쪽 타이어를 HankokTire로 교체
------------------------------------
[자동차가 달립니다.]
앞 왼쪽 Tire 수명: 1회
앞 오른쪽 kumhoTire 수명: 10회
뒤 왼쪽 HankookTire 수명: 13회
뒤 오른쪽 Tire 수명: 1회
------------------------------------
매개 변수의 다형성
1. 부모 클래스 Vehicle
public class Vehicle { // 매개변수의 타입을 만듬
public void run() {
System.out.println("차량이 달립니다.");
}
}
2. Vehicle을 이용하는 또다른 클래스
public class Driver {
//Vehicle 타입의 매개값을 받는 drive() 메소드 선언
public void drive(Vehicle vehicle) {
vehicle.run();
}
}
3. 자식 클래스 Bus와 Taxi
public class Bus extends Vehicle { // Vehicle 클래스로부터 상속받음
@Override
public void run() { // 원래 Vehicle 클래스에 있던 run() 메소드를 오버라이딩함
System.out.println("버스가 달립니다.");
}
}
public class Taxi extends Vehicle { // Vehicle 클래스로부터 상속받음
@Override
public void run() { // 원래 Vehicle 클래스에 있던 run() 메소드를 오버라이딩함
System.out.println("택시가 달립니다.");
}
}
4. 실행 클래스
public class DriverExample {
public static void main(String[] args) {
Driver driver = new Driver();
Bus bus = new Bus();
Taxi taxi = new Taxi();
// 매개변수로 vehicle을 받아야하는데 Vehicle의 자식 객체인 bus와 taxi를 받음
// 자동 타입변환된 것임
driver.drive(bus); // Vehicle vehicle = bus;
driver.drive(taxi); // Vehicle vehicle = taxi;
}
}
5. 출력 결과
버스가 달립니다.
택시가 달립니다.
- drive() 메소드를 실행하면 Vehicle의 run()이 출력되도록 코드를 짰는데 실제로 drive() 메소드를 호출하면 Bus와 Taxi에서 오버라이딩된 run() 메소드가 출력이 된다.
- 이 말은 매개값이 자동 타입 변환이 되었음을 의미한다.
- 그렇다면 우리는 메소드 재정의를 이용해서 매개 변수의 다형성을 구현해본 것이다.
강제 타입 변환
- 부모 타입을 자식 타입으로 변환하는 것을 말함
- 모든 부모 타입을 자식 타입으로 강제 변환할 수 있는 건 아님
- 자식 타입이 부모 타입으로 자동 타입 변환한 후 다시 자식 타입으로 변환할 때만 가능하다.
- 형태
Parent parent = new Child(); // 자동 타입 변환
Child child = (Child) parent; // 강제 타입 변환
- 강제 타입 변환이 필요한 이유 : 자식 타입이 부모으로 자동 타입 변환되면 부모에 선언된 필드와 메소드만 사용 가능하다.
- 만약 다시 자식의 필드와 메소드를 사용하고 싶다면 강제 타입 변환을 해주면 사용이 가능하다.
강제 타입 변환 예제
public class Parent {
//필드
public String field1;
//메소드
public void method1() {
System.out.println("Parent-method1()");
}
public void method2() {
System.out.println("Parent-method2()");
}
}
public class Child extends Parent {
public String field2;
public void method3() {
System.out.println("child-method3()");
}
}
public class ChildExample {
public static void main(String[] args) {
Parent parent = new Child(); // 자동 타입 변환
parent.field1 = "data1";
parent.method1();
parent.method2();
// parent.field2 = "data2"; // 불가능
// parent.method3(); // 불가능
Child child = (Child) parent;
child.field2 = "data2"; // 가능
child.method3(); // 가능
}
}
객체 타입 확인 (instanceof 연산자)
- 강제 타입 변환은 자식 타입이 부모 타입으로 자동 타입 변환이 된 경우만 사용이 가능하므로
- 아래와 같이 처음부터 부모 타입으로 생성된 객체는 강제 타입 변환이 불가능하다.
Parent parent = new Parent(); // 애초에 부모 타입으로 생성된 객체이므로
Child child = (child) parent; // 강제타입변환이 불가능하다
- instanceof 연산자 : 강제 타입 변환을 할 수 있는지 없는지 확인하기 위해 사용한다.
- 형태
boolean result = 객체 instanceof 타입
- 객체 타입 확인 (instanceof 연산자) 예제
public class Parent {}
public class Child extends Parent{}
public class InstanceofExample {
//타입변환이 가능한지 확인하는 메소드
public static void method1(Parent parent) {
if(parent instanceof Child) {
Child child = (Child) parent;
System.out.println("method1 - Child로 변환 성공");
} else {
System.out.println("method1 - Child로 변환되지 않음");
}
}
//타입변환이 가능한지 확인하지 않고 그냥 바로 변환하는 메소드
public static void method2(Parent parent) {
Child child = (Child) parent; // classCastException 에러가 발생할 수도 있음
System.out.println("method1 - Child로 변환 성공");
}
public static void main(String[] args) {
Parent parentA = new Child(); // 먼저 자동형변환 해주기
method1(parentA); // Child 객체를 매개값으로 전달
method2(parentA); // 강제형변환이 가능한 경우이므로 method1,2 둘다 에러X
Parent parentB = new Parent(); // 그냥 Parent 타입으로 받음
method1(parentB); // Parent 객체를 매개값으로 전달하며 강제형변환 불가하다고 뜸
method2(parentB); // 예외 발생 -> 강제형변환이 안되는 경우이며 그마저도 타입변환이 가능한지 확인해보지도 않아서 예외가 발생
}
}
method1 - Child로 변환 성공
method1 - Child로 변환 성공
method1 - Child로 변환되지 않음
Exception in thread "main" java.lang.ClassCastException
- Child 객체를 매개값으로 받으면 두 메소드 모두 예외가 발생하지않음
- 하지만 Parent 객체를 매개값으로 전달하면 method2()에서는 ClassCastException이 발생한다.
- 무조건 변환하려고 하면 안되고 method1() 처럼 강제 타입 변환이 가능한지 instanceof 연산자로 확인을 해보는 작업이 필요하다.
- ClassCastException 예외가 발생하면 프로그램이 즉시 종료되어버린다.
다형성 추가 예제 - Friend 배열 만들기 (상속 클래스 사용 안한 버전)
// 대학 친구
class UnivFriend1 {
// 필드
private String name;
private String major;
private String phone;
// 생성자
public UnivFriend1(String na, String ma, String ph) {
name = na;
major = ma;
phone = ph;
}
// 메소드
public void showInfo() {
System.out.println("이름 : " + name);
System.out.println("전공 : " + major);
System.out.println("전화번호 : " + phone);
}
}
// 직장 동료
class CompFriend1 {
// 필드
private String name;
private String department;
private String phone;
// 생성자
public CompFriend1 (String na, String de, String ph) {
name = na;
department = de;
phone = ph;
}
// 메소드
public void showInfo() {
System.out.println("이름 : " + name);
System.out.println("부서 : " + department);
System.out.println("전화번호 : " + phone);
}
}
<실행 클래스>
public class MyFriends_not_Inherited {
public static void main(String[] args) {
// 대학 친구의 관리를 위한 배열과 변수
UnivFriend[] uf = new UnivFriend[3];
int uc = 0;
// 직장 동료의 관리를 위한 배열과 변수
CompFriend[] cf = new CompFriend[3];
int cc = 0;
// 대학친구 추가
uf[uc++] = new UnivFriend("kim", "computer", "010-1");
uf[uc++] = new UnivFriend("lee", "electronics", "010-2");
uf[uc++] = new UnivFriend("seo", "Math", "010-3");
// 직장동료 추가
cf[cc++] = new CompFriend("kim", "R&D", "010-1");
cf[cc++] = new CompFriend("lee", "R&D", "010-2");
cf[cc++] = new CompFriend("seo", "R&D", "010-3");
for(int i=0; i<uf.length; i++) {
uf[i].showInfo();
System.out.println();
}
for(int i=0; i<cf.length; i++) {
cf[i].showInfo();
System.out.println();
}
}
}
<출력 결과>
이름 : kim
전화번호 : 010-1
전공 : computer
이름 : lee
전화번호 : 010-2
전공 : electronics
이름 : seo
전화번호 : 010-3
전공 : Math
이름 : kim
전화번호 : 010-1
부서 : R&D
이름 : lee
전화번호 : 010-2
부서 : R&D
이름 : seo
전화번호 : 010-3
부서 : R&D
다형성 추가 예제 - Friend 배열 만들기 (상속 클래스 사용 버전)
<부모 클래스>
class Friend {
// 필드
public String name;
public String phone;
// 생성자
public Friend(String na, String ph) {
name = na;
phone =ph;
}
// 메소드
public void showInfo() {
System.out.println("이름 : " + name);
System.out.println("전화번호 : " + phone);
}
}
<자식 클래스 1>
class UnivFriend extends Friend {
// 필드
public String major;
// 생성자
public UnivFriend(String na, String ma, String ph) {
super(na, ph);
major =ma;
}
// 메소드
@Override
public void showInfo() {
super.showInfo();
System.out.println("전공 : " + major);
}
}
<자식 클래스 2>
class CompFriend extends Friend{
// 필드
public String department;
// 생성자
public CompFriend(String na, String de, String ph) {
super(na, ph);
department = de;
}
// 메소드
@Override
public void showInfo() {
super.showInfo();
System.out.println("부서 : " + department);
}
}
<실행 클래스>
public class MyFriends {
public static void main(String[] args) {
Friend[] friends = new Friend[6];
int count=0;
// 대학친구 추가
friends[count++] = new UnivFriend("kim", "computer", "010-1");
friends[count++] = new UnivFriend("lee", "electronics", "010-2");
friends[count++] = new UnivFriend("seo", "Math", "010-3");
// 직장동료 추가
friends[count++] = new CompFriend("kim", "R&D", "010-1");
friends[count++] = new CompFriend("lee", "R&D", "010-2");
friends[count++] = new CompFriend("seo", "R&D", "010-3");
for(int i=0; i<friends.length; i++) {
friends[i].showInfo();
if(friends[i].name == "kim") {
System.out.println("kim 을" + i+1 + "번째에서 찾았습니다.");
} System.out.println();
}
}
}
<출력 결과>
이름 : kim
전화번호 : 010-1
전공 : computer
kim 을01번째에서 찾았습니다.
이름 : lee
전화번호 : 010-2
전공 : electronics
이름 : seo
전화번호 : 010-3
전공 : Math
이름 : kim
전화번호 : 010-1
부서 : R&D
kim 을31번째에서 찾았습니다.
이름 : lee
전화번호 : 010-2
부서 : R&D
이름 : seo
전화번호 : 010-3
부서 : R&D
다형성 추가 예제 - phone 클래스
<부모 클래스>
class MobilePhone {
// 필드
protected String number;
// 생성자
public MobilePhone(String num) {
this.number = num;
}
// 메소드
public void receive() {
System.out.println("Hi~from " + this.number);
}
}
<자식 클래스>
class SmartPhone extends MobilePhone {
// 필드
private String androidVer;
// 생성자
public SmartPhone(String num, String ver) {
super(num);
androidVer = ver;
}
// 메소드
public void send(String number) {
System.out.println("Hello~to " + number);
}
public void playApp() {
System.out.println("App is runnung in " + androidVer);
}
@Override
public void receive() {
super.receive();
System.out.println("I am smart");
}
}
<실행 클래스>
public class MobileSmartPhone {
public static void main(String[] args) {
// 스마트폰 객체 생성
SmartPhone phone1 = new SmartPhone("010-2","Ios 7.0");
MobilePhone phone2 = new SmartPhone("010-3","Ios 14.7.1");
//전화걸기
phone1.send("010-1"); // Hello~to 010-1 -> 자식 클래스의 메소드 호출
// phone2.send("010-1"); // 컴파일 에러
// 부모 타입으로 자동 타입 변환된 참조 변수인 phone2로 자식 클래스 소속 메소드(send())를 호출할 수 없다.
//전화받기
phone1.receive(); // Hi~from 010-2 I am smart -> 자식 클래스 메소드가 호출됨
phone2.receive(); // Hi~from 010-3 I am smart
// -> 부모 타입으로 자동 타입 변환된거지만 receive() 메소드는 애초에 부모 클래스에도 있어서 호출이 되는 것임
// 그런데 자식 클래스에서 메소드 재정의를 했기 때문에 재정의된 메소드가 호출되는 것이다.
//앱을 선택하여 실행하기
phone1.playApp(); // App is runnung in Ios 7.0
// phone2.playApp(); // 컴파일 에러
// 부모 타입으로 자동 타입 변환된 참조 변수인 phone2로 자식 클래스 소속 메소드(playApp())를 호출할 수 없다.
}
}
<출력 결과>
Hello~to 010-1
Hi~from 010-2
I am smart
Hi~from 010-3
I am smart
App is runnung in Ios 7.0
다형성 추가 예제 - Wrapping 클래스
public class Wrapping {
public static void main(String[] args) {
// 객체 생성
Box box1 = new Box();
PaperBox box2 = new PaperBox();
GoldPaperBox box3 = new GoldPaperBox();
// 메소드 호출
wrapBox(box1); //true false false
wrapBox(box2); // true true false
wrapBox(box3); // true true true
}
// 메소드 선언
private static void wrapBox(Box box) {
System.out.println(box instanceof Box);
System.out.println(box instanceof PaperBox);
System.out.println(box instanceof GoldPaperBox);
System.out.println();
}
}
class Box {}
class PaperBox extends Box{}
class GoldPaperBox extends PaperBox{}
다형성 추가 예제 - CakeControlCode 클래스
class Cake {
// 메소드
public void sweet() {System.out.println("Cake.sweet()");}
public void send() {System.out.println("Cake sending....");}
}
class CheeseCake extends Cake {
// 메소드
public void milky() {System.out.println("CheeseCake.milky()");}
@Override // 메소드 오버라이딩
public void send() {System.out.println("CheeseCake Sending....");}
}
class StrawberryCheeseCake extends CheeseCake {
// 메소드
public void sour() {System.out.println("StrawberryCheeseCake.sour()");}
@Override // 메소드 오버라이딩
public void send() {System.out.println("StrawberryCheeseCake sending....");}
}
<실행 클래스>
public class CakeControlCode {
public static void main(String[] args) {
// StrawberryCheeseCake 인스턴스는 Cake과 CheeseCake의 인스턴스다.
Cake cake1 = new StrawberryCheeseCake(); // 딸기치즈케익을 케익 타입으로 자동 타입 변환
CheeseCake cake2 = new StrawberryCheeseCake(); // 딸기치즈케익을 치즈케익 타입으로 자동 타입 변환
cake1.sweet(); // Cake.sweet()
cake2.milky(); // CheeseCake.milky()
cake2.sweet(); // Cake.sweet() -> 더 상위 클래스의 메소드는 호츌이 된다.
// 참조변수 간 대입과 형변환
CheeseCake cake3 = new CheeseCake();
Cake cake4 = cake3; // 자동 형변환
Cake cake5 = new CheeseCake();
// CheeseCake cake6 = cake5; // 불가능함. 이미 Cake 타입으로 형변환된 상태이므로.
CheeseCake cake6 = (CheeseCake)cake5;
// 일괄처리 (배열 관점)
Cake[] cakes = new Cake[3]; // 배열 안에 참조를 넣음
cakes[0] = new Cake();
cakes[1] = new CheeseCake();
cakes[2] = new StrawberryCheeseCake();
for(int i=0; i<cakes.length; i++) {
cakes[i] = new CheeseCake(); // 이 과정을 꼭 거쳐야함. cakes의 각 방에 CheeseCake 객체를 넣어준다.
cakes[i].send();
}
}
}
'👨🏫Study > JAVA' 카테고리의 다른 글
[JAVA] 08 - 1 인터페이스 (0) | 2022.03.21 |
---|---|
[JAVA] 07 - 3 추상 클래스 (0) | 2022.03.21 |
[JAVA] 07 - 1 상속 (0) | 2021.08.11 |
[JAVA] 06 - 6 패키지와 접근 제한자 (0) | 2021.08.06 |
[JAVA] 06 - 5 인스턴스 멤버와 정적 멤버 (0) | 2021.08.06 |
댓글