자바의 정석 3판(7) 연습문제 : OOP 2편

NtoZ·2023년 3월 9일
0

Java

목록 보기
16/23
post-thumbnail

자바의 정석 3판 챕터 7 OOP part2


7-1. 조건에 부합하는 객체 배열 만들기

  • 문제 : [7-1] 섯다카드 20장을 포함하는 섯다카드 한 벌(SutdaDeck클래스)을 정의한 것이다. 섯다카드 20장을 담는 SutdaCard배열을 초기화하시오.
    단, 섯다카드는 1부터 10까지의 숫자 가 적힌 카드가 한 쌍씩 있고,
    숫자가 1, 3, 8인 경우에는 둘 중의 한 장은 광(Kwang)이 어야 한다.
    즉, SutdaCard의 인스턴스변수 isKwang의 값이 true이어야 한다.

  • 예제 :
class SutdaDeck {
	final int CARD_NUM = 20;
	SutdaCard[] cards = new SutdaCard[CARD_NUM];

	SutdaDeck() {
		/*
		 * (1) 배열 SutdaCard를 적절히 초기화 하시오.
		 */
		for(int i=0; i<cards.length; i++) {
			SutdaCard card = new SutdaCard(num);
		}
	}
}

class SutdaCard {
	int num;
	boolean isKwang;

	SutdaCard() {
		this(1, true);
	}

	SutdaCard(int num, boolean isKwang) 
		{ this.num = num;
		this.isKwang = isKwang;
	}

	// info()대신 Object클래스의 toString()을 오버라이딩했다. 
	public String toString() {
	return num+(isKwang?"K":"");
	}
}

class S_Exercise7_1 {
	public static void main(String args[]) {
		SutdaDeck deck = new SutdaDeck();

		for (int i = 0; i < deck.cards.length; i++)
			System.out.print(deck.cards[i] + ",");
	}
}
  • 풀이접근 : cards는 SutdaCard의 배열참조변수이다. SutdaCard는 기본생성자와 (num, isKwang)을 초기화하는 생성자가 있다. for문을 사용하여 주어진 조건에 따라 SutdaCard(num, isKwang) 생성자를 통해 객체를 생성함과 동시에 초기화하여 바로 cards[i]배열에 포함하는 것이 문제의 의도이다.

  • 풀이실수 :

SutdaDeck() {
		/*
		 * (1) 배열 SutdaCard를 적절히 초기화 하시오.
		 */
		for(int i=0; i<cards.length; i++) {
			int num = (i+1)%10; // num은 1~9까지 수
				if(num==0) num=10;	//num이 0일 경우 10으로 변환. num은 1~10까지의 수
			cards[i] = new SutdaCard(num, (num<10)&&(num==1)||(num==3)||(num==8)); //🔥🔥🔥 조건식 자체를 boolean으로 반환
		}
	}
    < 반환결과>
    1K,2,3K,4,5,6,7,8K,9,10,1K,2,3K,4,5,6,7,8K,9,10,
  • (num<10)&&(num==1)||(num==3)||(num==8) 의 원래 의도 :
    num이 10보다 작고 1, 3, 8 중 하나라면 true를 반환.
    카드가 한 쌍이므로, 앞쪽 1, 3, 8에만 isKwang을 붙이려는 의도였지만
    애초에 앞에서 내가 int num에 부여한 조건식 때문에 num은 무조건 10 미만이다. num은 카드의 숫자에 입력될 값을 새로 만들어 준 것이므로,
    num이 아니라 반복문의 증감식 변수인 i를 입력하는 것이 옳다.
    1️⃣(i<10)&&(num==1)||(num==3)||(num==8)로 바꿔본다.
    ➡️ 그럼에도 불구하고 오류 발생. 이유가 뭘까?
    생각해봤더니 &&다음에 괄호연산자로 || 시리즈를 묶어야 원하는 계산이 나온다. 괄호를 사용하지 않으면 앞의 값이 false가 나오더라도 num==3 또는 8이면 false||true가 되어버려서 true값을 반환한다.
    따라서 i가 10미만이고(&&)(num이 1 또는 3 또는 8)의 조건으로 묶어주는 것이 옳은 방법이다.
    2️⃣(i<10)&&((num==1)||(num==3)||(num==8))로 바꿔본다.

  • 내 풀이2:

class SutdaDeck {
	final int CARD_NUM = 20;
	SutdaCard[] cards = new SutdaCard[CARD_NUM];

	SutdaDeck() {
		/*
		 * (1) 배열 SutdaCard를 적절히 초기화 하시오.
		 */
		for(int i=0; i<cards.length; i++) {
			int num = (i+1)%10; // num은 1~9까지 수
				if(num==0) num=10;	//num이 0일 경우 10으로 변환. num은 1~10까지의 수
			cards[i] = new SutdaCard(num, (i<10)&&((num==1)||(num==3)||(num==8))); //🔥🔥🔥 조건식 자체를 boolean으로 반환
		}
	}
}

class SutdaCard {
	int num;
	boolean isKwang;

	SutdaCard() {
		this(1, true);
	}

	SutdaCard(int num, boolean isKwang) 
		{ this.num = num;
		this.isKwang = isKwang;
	}

	// info()대신 Object클래스의 toString()을 오버라이딩했다. 
	public String toString() {
	return num+(isKwang?"K":"");	//🔥isKwang이 true이면 num 옆에 K를 덧붙임.
	}
}

class S_Exercise7_1 {
	public static void main(String args[]) {
		SutdaDeck deck = new SutdaDeck();

		for (int i = 0; i < deck.cards.length; i++)
			System.out.print(deck.cards[i] + ",");
	}
}
<결과>
1K,2,3K,4,5,6,7,8K,9,10,1,2,3,4,5,6,7,8,9,10,
  • 모범답안 :
SutdaDeck() {
for(int i=0;i < cards.length;i++) { int num = i%10+1;
boolean isKwang = (i < 10)&&(num==1||num==3||num==8); //✔️

cards[i] = new SutdaCard(num,isKwang);
}
}
  • ✔️내 풀이와 다른 점: boolean isKwang 이라는 지역변수를 만들어 num, isKwang이 생성자로 초기화되도록 했다. 다른 사람들이 봤을 때 훨씬 직관적이라는 점에서 클린코드이다.
    또다른 점으로 (i < 10)&&(num==1||num==3||num==8)이 i가 10보다 작고 num이 1 or 3 or 8이라는 점에서 더 직관적이다.

  • 💡해당 문제 아이디어 :

  1. 생성자 안에서 활용할 지역변수의 명은 호출할 생성자의 지역변수 명과 동일하게 짓는 것이 훨씬 직관적이고 깔끔한 방법이다.
  2. 매개변수로 넘길 변수(num)과 for문 증감문 변수(i)가 구분되도록 코드를 작성해본 점이 좋았다.
  3. 객체의 배열을 만들 때 생성자를 활용하라.

💡7-2. [7-1]에 조건에 부합하는 메서드 추가하고 테스트하기.

  • 문제 :

    [7-2] 문제7-1의 SutdaDeck클래스에 다음에 정의된 새로운 메서드를 추가하고 테스트 하시오.
    [주의] Math.random()을 사용하는 경우 실행결과와 다를 수 있음.

    1. 메서드명 : shuffle
      기 능 : 배열 cards에 담긴 카드의 위치를 뒤섞는다.(Math.random()사용) 반환타입 : 없음
      매개변수 : 없음
    2. 메서드명 : pick
      기 능 : 배열 cards에서 지정된 위치의 SutdaCard를 반환한다.
      반환타입 : SutdaCard
      매개변수 : int index - 위치
    3. 메서드명 : pick
      기 능 : 배열 cards에서 임의의 위치의 SutdaCard를 반환한다.(Math.random()사용)
      반환타입 : SutdaCard 매개변수 : 없음
class SutdaDeck2 {
	final int CARD_NUM = 20;
	SutdaCard2[] cards = new SutdaCard2[CARD_NUM];

	SutdaDeck2() {
		for(int i=0;i < cards.length;i++) { int num = i%10+1;
		boolean isKwang = (i < 10)&&(num==1||num==3||num==8); //✔️

		cards[i] = new SutdaCard2(num,isKwang);
		}
	}

	/*🔥
	 * (1) 위에 정의된 세 개의 메서드를 작성하시오.
	 *🔥/
} // SutdaDeck

class SutdaCard2 {
	int num;
	boolean isKwang;

	SutdaCard2() {
		this(1, true);
	}

	SutdaCard2(int num, boolean isKwang) {
		this.num = num;
		this.isKwang = isKwang;
	}

	public String toString() {
		return num + (isKwang ? "K" : "");
	}
}

class Exercise7_2 {
	public static void main(String args[]) {
		SutdaDeck2 deck = new SutdaDeck2();

		System.out.println(deck.pick(0));
		System.out.println(deck.pick());
		deck.shuffle();

		for (int i = 0; i < deck.cards.length; i++)
			System.out.print(deck.cards[i] + ",");

		System.out.println();
		System.out.println(deck.pick(0));
	}
}
  • 문제 풀이 접근 : shuffle, pick에 대한 단순한 문제이다. 객체 배열에 관한 내용이며, 객체 배열의 인스턴스 타입이 무엇인지 살피고 조건에 따른 적절한 참조변수를 반환하는 것이 중요하다.

  • 내 풀이:

class SutdaDeck2 {
	final int CARD_NUM = 20;
	SutdaCard2[] cards = new SutdaCard2[CARD_NUM];

	SutdaDeck2() {
		/*
		 * (1) 배열 SutdaCard를 적절히 초기화 하시오.
		 */
		for(int i=0; i<cards.length; i++) {
			int num = (i+1)%10; // num은 1~9까지 수
				if(num==0) num=10;	//num이 0일 경우 10으로 변환. num은 1~10까지의 수
			cards[i] = new SutdaCard2(num, (i<10)&&((num==1)||(num==3)||(num==8))); // 조건식 자체를 boolean으로 반환
		}
	}

	/*
	 * (1) 위에 정의된 세 개의 메서드를 작성하시오.
	 */
	void shuffle() {	//🔥
		int randomIdx = (int)(Math.random()*cards.length); //random은 0부터 카드개수-1까지 임의 정수 반환
		for(int i=0; i<cards.length; i++) {
			SutdaCard2 tmp = cards[i];	//임시 참조변수 tmp에 cards 배열의 i번째 요소 담기
			cards[i] = cards[randomIdx]; //cards[i]의 공간에 cards[랜덤인덱스] 담기
			cards[randomIdx] = tmp;
		}
	}
	
	SutdaCard2 pick(int index) {	//🔥
		return cards[index];
	}
	
	SutdaCard2 pick() {		//🔥
		int randomIdx = (int)(Math.random()*cards.length); // 사실 랜덤인덱스 지역변수는 인스턴스 변수로 빼면 중복을 줄일 수 있다.
		return cards[randomIdx];
	}
	
} // SutdaDeck

class SutdaCard2 {
	int num;
	boolean isKwang;

	SutdaCard2() {
		this(1, true);
	}

	SutdaCard2(int num, boolean isKwang) {
		this.num = num;
		this.isKwang = isKwang;
	}

	public String toString() {
		return num + (isKwang ? "K" : "");
	}
}

class Sol_Exercise7_2 {
	public static void main(String args[]) {
		SutdaDeck2 deck = new SutdaDeck2();

		System.out.println(deck.pick(0));
		System.out.println(deck.pick());
		deck.shuffle();

		for (int i = 0; i < deck.cards.length; i++)
			System.out.print(deck.cards[i] + ",");

		System.out.println();
		System.out.println(deck.pick(0));
	}
}
  • 모범답안:
void shuffle() {
for(int i=0; i<cards.length;i++) {
int j = (int)(Math.random()*cards.length);

// cards[i]와 cards[j]의 값을 서로 바꾼다. 
SutdaCard tmp = cards[i];
cards[i] = cards[j]; 
cards[j] = tmp;
}
}

SutdaCard pick(int index) {
if(index < 0 || index >= CARD_NUM) // ✔️✔️index의 유효성을 검사한다. return null;
return cards[index];
}
SutdaCard pick() {
int index = (int)(Math.random()*cards.length); 
return pick(index); // ✔️✔️pick(int index)를 호출한다.
}
  • 💡✔️내 답안과 차이점 :
    매개변수가 있는 경우 호출한 메서드에서 유효하지 않은 값이 넘어오는것을 방지하기 위해 유효성 검사를 해주어야 한다.
    pick(int index) 메서드에서
    if(index < 0 || index >= CARD_NUM) // ✔️✔️index의 유효성을 검사한다. return null; 해당 부분에서 index가 0미만이거나 CARD_NUM을 초과하면 카드의 인스턴스가 아닌 null을 반환한다.
  • ✔️✔️더 생각해볼 점:
    객체 지향적 관점에서 pick() 메서드는 이미 기존에 존재하고 있는 pick(int index)를 사용하는 것이 좋다.

💡7-5. 다음 코드에서 에러가 발생하는 이유

  • 예제:
/* 다음의 코드는 컴파일하면 에러가 발생한다. 그 이유를 설명하고 에러를 수정하기 위해서는 코드를 어떻게 바꾸어야 하는가? */

//class Product {
//	int price;
//
//	int bonusPoint; // 제품구매 시 제공하는 보너스점수
//
//	Product(int price) {
//		this.price = price;
//		bonusPoint = (int) (price / 10.0);
//	}
//}
//
//class Tv extends Product {
//	Tv() {
//	}
//
//	public String toString() {
//		return "Tv";
//	}
//}
//
//class Exercise7_5 {
//	public static void main(String[] args) {
//		Tv t = new Tv();
//	}
//}
  • 내 답안:
    자손 클래스의 생성자는 첫 줄에 반드시 조상 클래스의 생성자를 호출한다.
    Tv() { }에서 컴파일러가 구현부 안에서 super()를 호출하였는데
    super()는 존재하지 않고 super(int price)만이 존재하기 때문에
    오류가 발생한다.

    따라서 Product 클래스에 Product()를 추가해주거나,
    Tv 클래스에서 기본생성자 Tv(){}를 Tv(int price){}로 바꿔주면 해결된다.

  • 💡 왜 자손 클래스의 생성자는 super()를 호출하는 걸까?:
    A: '생성자 체인' 때문에.

    클래스의 생성자는 객체를 생성할 때 호출되는 메서드입니다. 생성자는 객체를 초기화하고 클래스의 인스턴스 변수를 설정합니다. 이 때, 생성자가 호출되면 먼저 수행되어야 하는 작업이 있을 수 있습니다.

    따라서 생성자는 첫 줄에 다른 생성자를 호출함으로써 상위 클래스의 생성자나 동일 클래스의 다른 생성자를 먼저 호출하는 것이 일반적입니다. 이것을 "생성자 체인"이라고합니다. 이것은 상속과 관련이 있습니다. 상위 클래스의 생성자가 먼저 호출되어야 상위 클래스의 인스턴스 변수가 초기화되기 때문입니다. 그런 다음 하위 클래스의 생성자가 호출되고 하위 클래스의 인스턴스 변수가 초기화됩니다.

    그러나 생성자가 호출되는 순서는 생성자가 초기화할 인스턴스 변수에 따라 달라질 수 있습니다. 예를 들어, 하위 클래스에서 상위 클래스의 인스턴스 변수를 사용하지 않는 경우 상위 클래스의 생성자를 호출하지 않아도 됩니다.

    또한 생성자 체인을 사용하는 것이 항상 가능한 것은 아닙니다. 예를 들어, 생성자 매개변수를 사용하는 경우 생성자 체인을 사용할 수 없습니다. 이러한 경우 다른 초기화 방법을 사용해야 할 수 있습니다.

  • 교차검증 출처 : [Java]생성자 체인(Constructor Chaining)

  • 💡💡💡 개념 : 유효한 iv값 유지를 위해 생성자가 먼저 해야할 초기화 작업이 존재할 수 있다. 이런 '생성자 체인' 때문에 모든 생성자는 첫 줄에 다른 생성자를 호출한다. 상속 관계에서는 상위 생성자가 먼저 상위 클래스의 인스턴스 변수를 초기화한 다음에 자손 클래스의 인스턴스 변수를 초기화해야 하므로 super()를 자동호출하는 것이다.

💡조상에 정의된 인스턴스 변수들이 초기화되도록 하기 위해서.

자손클래스의 인스턴스를 생성하면 조상으로부터 상속받은 인스턴스변수들도 생성 되는데, 이 상속받은 인스턴스변수들 역시 적절히 초기되어야 한다.

상속받은 조상의 인스턴스변수들을 자손의 생성자에서 직접 초기화하기보다는 조상의 생성자를 호출함으로써 초기화되도록 하는 것이 바람직하다.

💡각 클래스의 생성자는 해당 클래스에 선언된 인스턴스변수의 초기화만을 담당하고, 조상 클래스로부터 상속받은 인스턴스변수의 초기화는 조상클래스의 생성자가 처리하도록 해야 하는 것이다.


💡✔️7-7. 생성자 호출의 순서와 실행결과 적기

  • 예제: 다음 코드를 실행했을 때 호출되는 생성자의 순서와 실행결과를 적으시오.
/* [7-7] 다음 코드의 실행했을 때 호출되는 생성자의 순서와 실행결과를 적으시오. */
class Parent {
	int x = 100;

	Parent() {
		this(200);
	}

	Parent(int x) {
		this.x = x;
	}

	int getX() {
		return x;
	}
}

class Child extends Parent {
	int x = 3000;

	Child() {
		this(1000);
	}

	Child(int x) {
		this.x = x;
	}
}

class Exercise7_7 {
	public static void main(String[] args) {
		Child c = new Child();

		System.out.println("x=" + c.getX());
	}
}
  • 내 생각 :
    메인 메서드에서 new Child();로 자손 클래스 객체를 생성할 때
    Child() 생성자 구현부 안에서 super();가 가장 먼저 호출된다.
    조상멤버의 유효성을 위해, 가장 먼저 상속받은 조상 클래스의 멤버부터 초기화해야하기 때문이다.
    super()가 발동하면 this(200)이 적용되어 super.x는 200이 된다.
    그 다음 자손 Child 클래스에서 this(1000)이 발동한다.
    이 때 this(1000)은 같은 자손클래스의 생성자이며 this.x = 1000;으로 만든다.
    this는 Child 클래스의 객체를 말하므로 x=1000;의 값이 출력된다.

    => 답이 틀렸다.?

  • 다시 풀이 :
    💡오개념 주의 : 가장 핵심적인 개념은
    생성자는 첫 줄에 다른 생성자를 호출해야한다는 것이다.
    무조건 조상 생성자를 호출하는 것이 아닌, this()를 호출해도 다른 생성자를 호출하는 원칙에 위배되지 않는다.
    만약 첫 줄에 아무런 생성자가 없을 경우에만 super()를 호출한다.
    그러나 결국에는 어떤 생성자를 호출하던지 super()를 호출하게 되어 있다. this(매개변수)를 타고 들어가다 보면 구현부 첫 줄에 생성자 호출이 되어 있지 않은 생성자를 마주하게 된다. 이 때 컴파일러가 자동으로 super()를 삽입하게 된다. 조상 클래스의 멤버를 초기화하는 것이 가장 안전한 방식이기 때문이다.
    결국 생성자는 인스턴스 변수를 초기화하는 조금 특별한 메서드일 뿐이다!

  1. Child()가 호출
  2. this(1000)이 호출 // 이 단계에서 this.x=1000
  3. super()가 호출
  4. super(200)이 호출 // 이 단계에서
  5. Object()를 호출 ✔️✔️✔️ // 하고 super(200)의 나머지 구현부에 따라 super.x=200이 된다.
    마지막에 c.getX()메서드는 Child 객체의 입장에서 super.x를 의미하므로 x=200이 출력된다.
  • 💡 결국 생성자를 타고 올라가다보면 반드시 Object()를 마주치게 된다.

  • 정답:
    Child() → Child(int x) → Parent() → Parent(int x) → Object()의 순서로 호출되니까, Child클래스의 인스턴스변수 x는 1000이 되고, Parent클래스의 인스턴스 변수 x는 200이 된다. getX()는 조상인
    Parent클래스에 정의된 것이라서, getX()에서 x는 Parent클래스의 인스턴스변수 x를 의미한다. 그래서 x=200이 출력된다.


💡7-12. 접근제어자 특징

  • 💡개념 : 지역변수에는 접근제어자 사용이 불가능하다. (O)
  • 내 생각 : 지역변수는 어차피 메소드, 생성자, 초기화 블럭 안에서 활용되며, 다른 메서드나 클래스에서 접근할 수 없으므로 접근제어자 사용이 의미 없는 것 같다.
  • 접근 제어자가 사용될 수 있는 곳 - 클래스, 멤버변수, 메서드, 생성자

    지역 변수(local variable)는 메서드 내에 선언되는 변수로서, 해당 메서드 내에서만 사용 가능합니다. 따라서 지역 변수에는 접근 제어자를 붙일 수 없습니다.

    접근 제어자는 클래스, 멤버 변수, 메서드와 같이 클래스의 구성 요소에 대해서만 적용됩니다. 지역 변수는 메서드 내에서만 사용되며, 해당 메서드가 종료되면 메모리에서 사라지기 때문에 다른 클래스나 메서드에서 접근할 수 없습니다. 따라서 지역 변수에는 접근 제어자를 붙일 수 없습니다.

💡[7-13] 추가개념 :

Math 클래스의 생성자는 private 접근 제어자를 사용한다.
Math 클래스는 몇 개의 상수와 static 멤버들로만 구성되어 있으므로 객체를 생성할 이유가 없다. 따라서 외부로부터의 불필요한 접근을 막기 위해 private 접근 제어자를 사용한다.


💡7-14. 캡슐화

  • 문제 : [7-14] 문제7-1에 나오는 섯다카드의 숫자와 종류(isKwang)는 사실 한번 값이 지정되면 변경되어서는 안 되는 값이다. 카드의 숫자가 한번 잘못 바뀌면 똑같은 카드가 두 장이 될 수 도 있기 때문이다. 이러한 문제점이 발생하지 않도록 아래의 SutdaCard를 수정하시오.
class SutdaCard3 {
	int num;
	boolean isKwang;

	SutdaCard3() {
		this(1, true);
	}

	SutdaCard3(int num, boolean isKwang) {
		this.num = num;
		this.isKwang = isKwang;
	}

	public String toString() {
		return num + (isKwang ? "K" : "");
	}
}

class Exercise7_14 {
	public static void main(String args[]) {
		SutdaCard3 card = new SutdaCard3(1, true);
	}
}
  • 내 풀이 :
lass SutdaCard3 {
	private final int num;	//🔥해당 객체값을 변경하지 못하도록 iv의 접근제어자를 private와 final로 선언
	private final boolean isKwang; //🔥 private로 선언하면 외부클래스에서는 접근이 불가능, final로 선언하면 메서드를 통해서도 값 변경 불가능. 

	SutdaCard3() {		//🔥생성자를 통해서는 기존 인스턴스 멤버에 접근 가능하다.
		this(1, true);
	}

	SutdaCard3(int num, boolean isKwang) {
		this.num = num;
		this.isKwang = isKwang;
	}

	public String toString() {
		return num + (isKwang ? "K" : "");
	}
}

class Sol_Exercise7_14 {
	public static void main(String args[]) {
		SutdaCard3 card = new SutdaCard3(3, true);
		System.out.println(card.toString());
		//card.num = 1; // 🔥private로 선언했으므로 애초에 접근 불가능. 접근한다고 하더라도 final이므로 수정 불가능.
	}
}
  • 풀이 하다가 생긴 궁금증과 해답 :
    SutdaCard3에서 final로 num와 isKwang을 선언했을 때 자동초기화로 0이나 false가 되지 않는가? 그 상태에서 생성자로 num, isKwang을 수정하는 원리가 어떤 것일까? 생각해보니, 인스턴스 변수가 생성되는 시점은 인스턴스가 생성된 그 시점이다. 따라서 두 변수는 final로 선언만 되었을 뿐 초기화된 시점이 아니기 때문에 두 변수가 메인단에서 이상 없이 final 상수로써 초기화 될 수 있는 것이다.

  • 💡 private와 final을 적절히 사용하자. 외부에서 접근하지 못하게 적절한 접근 제어자를 사용하고, final을 어떤 경우에 써야아하는지 판명하자.
    좋은 개발자가 되는 방법이다.


💡7-16. instanceof 용례 파악

  • 💡개념: instanceof 메서드를 사용했을 때 true를 반환한다는 것은 해당 타입으로 형변환이 가능하다는 것을 의미한다.
    참조변수가 형변환이 가능하다는 것은 해당 클래스 자신 또는 그 조상타입이라는 것이다.
    따라서 참조변수를 형변환하기 전에 instanceof가 true를 반환하는지 먼저 살펴보는 것이 좋다.

  • [7-18] 적용례 참고


💡💡7-19. 객체 배열의 메서드 구현

  • 문제 : [7-19] 다음은 물건을 구입하는 사람을 정의한 Buyer클래스이다. 이 클래스는 멤버변수로 돈(money)과 장바구니(cart)를 가지고 있다.
    제품을 구입하는 기능의 buy메서드와 장바구니에 구입한 물건을 추가하는 add메서드, 구입한 물건의 목록과 사용금액, 그리고 남은 금액을 출력하는 summary메서드를 완성하시오.
  1. 메서드명 : buy
    기 능 : 지정된 물건을 구입한다. 가진 돈(money)에서 물건의 가격을 빼고, 장바구니(cart)에 담는다.
    만일 가진 돈이 물건의 가격보다 적다면 바로 종료한다.
    반환타입 : 없음
    매개변수 : Product p - 구입할 물건

  2. 메서드명 : add
    기 능 : 지정된 물건을 장바구니에 담는다.
    만일 장바구니에 담을 공간이 없으면, 장바구니의 크기를 2배로 늘린 다음에 담는다.
    반환타입 : 없음
    매개변수 : Product p - 구입할 물건

  3. 메서드명 : summary
    기 능 : 구입한 물건의 목록과 사용금액, 남은 금액을 출력한다.
    반환타입 : 없음
    매개변수 : 없음

class Exercise7_19 {
	public static void main(String args[]) {
		Buyer b = new Buyer();
		b.buy(new Tv5());
		b.buy(new Computer());
		b.buy(new Tv5());
		b.buy(new Audio());
		b.buy(new Computer());
		b.buy(new Computer());
		b.buy(new Computer());

		b.summary();
	}
}

class Buyer {
	int money = 1000;
	Product5[] cart = new Product5[3]; // 구입한 제품을 저장하기 위한 배열
	int i = 0; // Product배열 cart에 사용될 index

	void buy(Product5 p) {
		/*
		 * (1) 아래의 로직에 맞게 코드를 작성하시오. 1.1 가진 돈과 물건의 가격을 비교해서 가진 돈이 적으면 메서드를 종료한다. 1.2 가진
		 * 돈이 충분하면, 제품의 가격을 가진 돈에서 빼고 1.3 장바구니에 구입한 물건을 담는다.(add메서드 호출)
		 */
	}

	void add(Product5 p) {
		/*
		 * (2) 아래의 로직에 맞게 코드를 작성하시오. 1.1 i의 값이 장바구니의 크기보다 같거나 크면 1.1.1 기존의 장바구니보다 2배 큰
		 * 새로운 배열을 생성한다. 1.1.2 기존의 장바구니의 내용을 새로운 배열에 복사한다. 1.1.3 새로운 장바구니와 기존의 장바구니를
		 * 바꾼다. 1.2 물건을 장바구니(cart)에 저장한다. 그리고 i의 값을 1 증가시킨다.
		 */
	} // add(Product p)

	void summary() {
		/*
		 * (3) 아래의 로직에 맞게 코드를 작성하시오. 1.1 장바구니에 담긴 물건들의 목록을 만들어 출력한다. 1.2 장바구니에 담긴 물건들의
		 * 가격을 모두 더해서 출력한다. 1.3 물건을 사고 남은 금액(money)를 출력한다.
		 */
	} // summary()
}

class Product5 {
	int price; // 제품의 가격

	Product5(int price) {
		this.price = price;
	}
}

class Tv5 extends Product5 {
	Tv5() {
		super(100);
	}

	public String toString() {
		return "Tv";
	}
}

class Computer extends Product5 {
	Computer() {
		super(200);
	}

	public String toString() {
		return "Computer";
	}
}

class Audio extends Product5 {
	Audio() {
		super(50);
	}

	public String toString() {
		return "Audio";
	}
}

/*
 * 실행결과
 * 잔액이 부족하여 Computer을/를 살수 없습니다.
 * 구입한 물건:Tv,Computer,Tv,Audio,Computer,Computer, 
 * 사용한 금액:850
 * 남은 금액:150
*/
  • 내 풀이
import java.util.Arrays;
class Sol_Exercise7_19 {
	public static void main(String args[]) {
		Buyer b = new Buyer();
		b.buy(new Tv5());
		b.buy(new Computer());
		b.buy(new Tv5());
		b.buy(new Audio());
		b.buy(new Computer());
		b.buy(new Computer());
		b.buy(new Computer());

		b.summary();
	}
}

class Buyer {
	int money = 1000;
	Product5[] cart = new Product5[3]; // 구입한 제품을 저장하기 위한 배열
	int i = 0; // Product배열 cart에 사용될 index

	void buy(Product5 p) {
		/*
		 * (1) 아래의 로직에 맞게 코드를 작성하시오. 1.1 가진 돈과 물건의 가격을 비교해서 가진 돈이 적으면 메서드를 종료한다. 1.2 가진
		 * 돈이 충분하면, 제품의 가격을 가진 돈에서 빼고 1.3 장바구니에 구입한 물건을 담는다.(add메서드 호출)
		 */
		if(p.price>this.money) return;
		else {
			money -= p.price;
			//3. 장바구니에 구입한 물건을 담는다.
		}
	}

	void add(Product5 p) {	//💡💡 배열길이 확장 메서드
		/*
		 * (2) 아래의 로직에 맞게 코드를 작성하시오. 1.1 i의 값이 장바구니의 크기보다 같거나 크면 1.1.1 기존의 장바구니보다 2배 큰
		 * 새로운 배열을 생성한다. 1.1.2 기존의 장바구니의 내용을 새로운 배열에 복사한다. 1.1.3 새로운 장바구니와 기존의 장바구니를
		 * 바꾼다. 1.2 물건을 장바구니(cart)에 저장한다. 그리고 i의 값을 1 증가시킨다.
		 */
		if(i>=cart.length) {
			Product5[] cart2 = new Product5[cart.length*2];	//Product 배열 cart의 길이보다 2배 길이인 배열 cart2 생성
			cart2 = Arrays.copyOf(cart, i);	// cart2에 cart의 인덱스 i까지의 요소 담기
			cart = cart2;	//❓❓이러면 cart가 바꿔지나? 애초에 cart 배열의 크기는 [3]이었는데 참조하는 객체배열의 주소가 바뀌는거니까 바꿔질것같다
			cart[i] = p;	// 물건을 장바구니(cart)에 저장한다.
			i++;	//i의 값을 1 증가시킨다.
		}
		
	} // add(Product p)

	void summary() {
		/*
		 * (3) 아래의 로직에 맞게 코드를 작성하시오. 1.1 장바구니에 담긴 물건들의 목록을 만들어 출력한다. 1.2 장바구니에 담긴 물건들의
		 * 가격을 모두 더해서 출력한다. 1.3 물건을 사고 남은 금액(money)를 출력한다.
		 */
		System.out.println(Arrays.toString(cart));
		int total = 0;
		for(int k=0; k<cart.length; k++) {
			total += cart[i].price;
		}
		System.out.println("물건 구입 총액 : " + total);
		System.out.println("잔돈 : " + this.money);
		
	} // summary()
}

class Product5 {
	int price; // 제품의 가격

	Product5(int price) {
		this.price = price;
	}
}

class Tv5 extends Product5 {
	Tv5() {
		super(100);
	}

	public String toString() {
		return "Tv";
	}
}

class Computer extends Product5 {
	Computer() {
		super(200);
	}

	public String toString() {
		return "Computer";
	}
}

class Audio extends Product5 {
	Audio() {
		super(50);
	}

	public String toString() {
		return "Audio";
	}
}
  • nullPointException 에러로 인한 다시 풀이
void buy(Product5 p) {
		/*
		 * (1) 아래의 로직에 맞게 코드를 작성하시오. 1.1 가진 돈과 물건의 가격을 비교해서 가진 돈이 적으면 메서드를 종료한다. 1.2 가진
		 * 돈이 충분하면, 제품의 가격을 가진 돈에서 빼고 1.3 장바구니에 구입한 물건을 담는다.(add메서드 호출)
		 */
		if(p.price>this.money) return;
		else {
			money -= p.price;
			//3. 장바구니에 구입한 물건을 담는다.
			add(p);	//🔥 이걸 추가 안했네
		}
	}

	void add(Product5 p) {	//💡💡 배열길이 확장 메서드
		/*
		 * (2) 아래의 로직에 맞게 코드를 작성하시오. 1.1 i의 값이 장바구니의 크기보다 같거나 크면 1.1.1 기존의 장바구니보다 2배 큰
		 * 새로운 배열을 생성한다. 1.1.2 기존의 장바구니의 내용을 새로운 배열에 복사한다. 1.1.3 새로운 장바구니와 기존의 장바구니를
		 * 바꾼다. 1.2 물건을 장바구니(cart)에 저장한다. 그리고 i의 값을 1 증가시킨다.
		 */
		if(i>=cart.length) {
			Product5[] cart2 = new Product5[cart.length*2];	//Product 배열 cart의 길이보다 2배 길이인 배열 cart2 생성
			cart2 = Arrays.copyOf(cart, i);	// cart2에 cart의 인덱스 i까지의 요소 담기
			cart = cart2;	//❓❓이러면 cart가 바꿔지나? 애초에 cart 배열의 크기는 [3]이었는데 참조하는 객체배열의 주소가 바뀌는거니까 바꿔질것같다
		}
		//🔥if문 안에 들어가면 안돼 현재 넣을 인덱스가 cart.length보다 클 때만 저장하면 nullPointerException이 발생하잖아.
		cart[i] = p;	// 물건을 장바구니(cart)에 저장한다.
		i++;	//i의 값을 1 증가시킨다.
		
	} // add(Product p)
  • 💡💡아이디어 : 배열의 길이를 무한히 확장할 때 사용할 수 있는 add()메서드를 잘 살펴보자. 처음 객체 배열의 크기를 가리키는 int 변수가 별개로 존재해야한다. 이 int 변수가 현재 배열의 크기보다 같거나 크다면 배열의 확장을 준비한다. 크기가 현재 배열의 크기의 두 배인 배열을 생성하고 참조변수에 저장한다. 해당 참조변수에는 null 값이 들어가 있는 상태이므로 현재 배열의 참조변수를 Arrays.copyof()하여 그 품목을 저장한다.
    품목을 저장한 상태에서 해당 객체 배열을 원래 참조변수가 가리키도록 갈아 끼워준다. 그리고 현재 인덱스에 넣으려는 물건을 마저 넣고 다음 위치(null 값)을 가리키도록 처음 객체 배열의 크기를 가리켰던 int 변수를 1 증가시킨다.
  • ❓❓풀이 할 때 헷갈렸던 부분 :
    void add(Product5 p) 메서드에서 1.1.3 새로운 장바구니와 기존의 장바구니를 바꾼다는 부분이 헷갈렸다. cart=cart2의 내용을 담았을 때, cart는 배열의 길이가 3이었는데 배열의 길이가 6인 cart2를 담을 수 있는가?에 대한 궁금증이 생겼다. 그러나 cart는 객체배열을 가리키는 참조변수 일 뿐이라는 사실을 기억하니 의문이 풀렸다. cart는 참조변수일 뿐이다. 배열의 길이가 3인 배열객체에서 배열의 길이가 6인 객체로 갈아 끼웠을 뿐이다.
  • <그런데> 이제는 NullPointException이 아니라 ArrayIndexOutOfBoundsException이 발생함. 왜 이럴까...
    add에서 toString으로 cart를 찍어보니 요소 3개 까지는 잘 저장되는 것을 확인할 수 있었다. 한 마디로 예상과 다르게 배열의 길이가 늘어난 채로 저장되지 않았다. 💡Arrays.copyOf(원본배열, 원본 배열에서 복사해올 길이)는 배열의 길이까지 복사한다. 따라서 cart.length를 사용하면 배열의 길이가 cart.length*2에서 cart.length가 되는 것이다.
    그렇기 때문에 cart.length*2를 인덱싱으로 유지해 주거나
    System.arraycopy(원본배열, 원본 배열의 복사 시작 지점, 복사할 배열, 복사할 배열의 복사 시작 지점, 복사할 요소의 개수)를 사용하는 것이 맞다.
    (이 경우 System.arraycopy(cart, 0, cart2, 0, cart.length);)

Arrays.copyOf() 메서드와 마찬가지로, System.arraycopy() 메서드도 배열의 요소를 복사하는 데 사용됩니다. 그러나 두 메서드 사이에는 몇 가지 중요한 차이점이 있습니다.

1.반환값의 차이
Arrays.copyOf() 메서드는 복사본 배열을 반환하지만, System.arraycopy() 메서드는 void를 반환합니다. 따라서 System.arraycopy() 메서드를 사용하면 복사본 배열을 생성하는 것이 아니라 원래 배열 내부에서 복사가 수행됩니다.

2.매개변수의 차이
Arrays.copyOf() 메서드는 원래 배열과 복사본 배열의 길이를 모두 매개변수로 받습니다. 하지만 System.arraycopy() 메서드는 원본 배열, 시작 인덱스, 대상 배열, 대상 배열에서 복사가 시작될 인덱스, 복사할 요소의 개수를 매개변수로 받습니다.

3.성능의 차이
System.arraycopy() 메서드는 복사할 요소의 개수만큼 반복문을 수행하므로, 대용량 배열의 경우 더 빠르게 실행됩니다. 반면 Arrays.copyOf() 메서드는 내부적으로 System.arraycopy()를 호출하므로, 배열의 크기가 작거나 복사할 요소의 개수가 적을 경우 더 빠르게 실행됩니다.
교차검증 : [Java] System.arraycopy() 와Arrays.copyOf()의 차이 (배열 복사)

따라서 두 메서드 중 어느 것을 사용할지는 상황에 따라 다르며, 대용량 배열의 경우 System.arraycopy() 메서드를 사용하는 것이 더 효율적입니다. 그러나 복사본 배열이 필요하거나 작은 크기의 배열의 경우 Arrays.copyOf() 메서드를 사용하는 것이 더 간단하고 가독성이 좋습니다.

  • ✔️✔️ 다른 문제점 :
void summary() {
		/*
		 * (3) 아래의 로직에 맞게 코드를 작성하시오. 1.1 장바구니에 담긴 물건들의 목록을 만들어 출력한다. 1.2 장바구니에 담긴 물건들의
		 * 가격을 모두 더해서 출력한다. 1.3 물건을 사고 남은 금액(money)를 출력한다.
		 */
		System.out.println(Arrays.toString(cart));
		int total = 0;
		for(int k=0; k<cart.length; k++) {
			if(cart[k]==null) break;	//✔️✔️ cart[k]==null이면 for문을 빠져나가야함.
			total += cart[i].price;	//✔️✔️ i가 아니라 k를 써야지... i는 null포함이야.
		}
		System.out.println("사용한 금액 : " + total);
		System.out.println("남은 금액 : " + this.money);
		
	} // summary()
  • 💡✔️ 체크해야할 부분 : for문 안에 if(cart[k]==null) break;를 추가해줘야 한다. void summary()에는 매개변수가 없기 때문에 유효성검사를 생각하지 않았지만 사실 배열참조변수cart에는 아직 구매하지 않은 배열의 빈 공간이 남아있을 수 있다. null값일 때 for문을 빠져나가지 않으면 total += cart[i].price;total += null.price가 되므로 nullPointerExeption 오류가 발생하게 된다.
  • 💡💡 아이디어 : 함수에 매개변수가 존재할 때 뿐만 아니라 다른 참조변수를 구현부에 가져다 쓸 때도 항상 유효성 검사에 대한 생각을 해야 한다.
  • 잘못한 부분2 : total += cart[i].price; 가 아니라 total += cart[k].price;이다. 내가 어떤 변수를 사용하고 있는지 잘 인식하자. k는 for문에 의해 cart.length의 인덱스 안에서 증가가 이루어지지만 i는 add()메서드에 의해 어느순간 cart.length가 가지고 있는 범위를 넘어서게 되므로 summary()메서드 내에서는 ArrayIndexOutOfBoundsException이 발생하게 되는 것이다.
class Buyer {
	int money = 1000;
	Product5[] cart = new Product5[3]; // 구입한 제품을 저장하기 위한 배열
	int i = 0; // Product배열 cart에 사용될 index (🔥i는 0부터 시작하여 물건을 구입할 때마다 1씩 증가)

	void buy(Product5 p) {
		/*
		 * (1) 아래의 로직에 맞게 코드를 작성하시오. 1.1 가진 돈과 물건의 가격을 비교해서 가진 돈이 적으면 메서드를 종료한다. 1.2 가진
		 * 돈이 충분하면, 제품의 가격을 가진 돈에서 빼고 1.3 장바구니에 구입한 물건을 담는다.(add메서드 호출)
		 */
		if(p.price>this.money) {
			System.out.println("잔액이 부족하여 " +p+" 을/를 살 수 없습니다.");
			return;
			}
		else {	//잔액이 충분하면
			money -= p.price;
			//3. 장바구니에 구입한 물건을 담는다.
			add(p);	//🔥 카트에 물건 p를 담는다.
		}
	}

	void add(Product5 p) {	//💡💡 배열길이 확장 메서드 (&물건을 객체 배열에담기)
		/*
		 * (2) 아래의 로직에 맞게 코드를 작성하시오. 1.1 i의 값이 장바구니의 크기보다 같거나 크면 1.1.1 기존의 장바구니보다 2배 큰
		 * 새로운 배열을 생성한다. 1.1.2 기존의 장바구니의 내용을 새로운 배열에 복사한다. 1.1.3 새로운 장바구니와 기존의 장바구니를
		 * 바꾼다. 1.2 물건을 장바구니(cart)에 저장한다. 그리고 i의 값을 1 증가시킨다.
		 */
		if(i>=cart.length) {
			Product5[] cart2 = new Product5[cart.length*2];	//Product 배열 cart의 길이보다 2배 길이인 배열 cart2 생성
			//cart2 = Arrays.copyOf(cart, cart.length);	// cart2에 cart의 인덱스 i까지의 요소 담기 //이러면 다시 배열의 길이가 cart.length가 됨.
			System.arraycopy(cart, 0, cart2, 0, cart.length);	//✔️✔️
			cart = cart2;	//❓❓이러면 cart가 바꿔지나? 애초에 cart 배열의 크기는 [3]이었는데 참조하는 객체배열의 주소가 바뀌는거니까 바꿔질것같다. O 바꿔진다.
			// System.out.println(Arrays.toString(cart)); //(디버깅용)
		}
		//🔥if문 안에 들어가면 안돼 현재 넣을 인덱스가 cart.length보다 클 때만 저장하면 nullPointerException이 발생하잖아.
		cart[i] = p;	// 물건을 장바구니(cart)에 저장한다.
		i++;	//i의 값을 1 증가시킨다.
		
	} // add(Product p)

	void summary() {
		/*
		 * (3) 아래의 로직에 맞게 코드를 작성하시오. 1.1 장바구니에 담긴 물건들의 목록을 만들어 출력한다. 1.2 장바구니에 담긴 물건들의
		 * 가격을 모두 더해서 출력한다. 1.3 물건을 사고 남은 금액(money)를 출력한다.
		 */
		System.out.println(Arrays.toString(cart));
		int total = 0;
		for(int k=0; k<cart.length; k++) {
			if(cart[k]==null) break;	//✔️✔️ cart[k]==null이면 for문을 빠져나가야함.
			total += cart[k].price;
		}
		System.out.println("사용한 금액 : " + total);
		System.out.println("남은 금액 : " + this.money);
		
	} // summary()
}
<실행결과>
잔액이 부족하여 Computer/를 살 수 없습니다.
[Tv, Computer, Tv, Audio, Computer, Computer]
사용한 금액 : 850
남은 금액 : 150
  • 모범 풀이:
class Exercise7_19 {
public static void main(String args[]) { Buyer b = new Buyer();
b.buy(new Tv()); b.buy(new Computer()); b.buy(new Tv());
b.buy(new Audio()); b.buy(new Computer()); b.buy(new Computer()); b.buy(new Computer());

b.summary();
}
}

class Buyer {
int money = 1000;
Product[] cart = new Product[3];	// 구입한 제품을 저장하기 위한 배열
int i = 0;	// Product배열 cart에 사용될 index

void buy(Product p) {
//	1.1 가진 돈과 물건의 가격을 비교해서 가진 돈이 적으면 메서드를 종료한다.
if(money < p.price) {
System.out.println("잔액이 부족하여 "+ p +"을/를 살수 없습니다."); return;
}
//	1.2 가진 돈이 충분하면, 제품의 가격을 가진 돈에서 빼고
money -= p.price;
//	1.3 장바구니에 구입한 물건을 담는다.(add메서드 호출)
add(p);
}

void add(Product p) {
//	1.1 i의 값이 장바구니의 크기보다 같거나 크면
if(i >= cart.length) {
//	1.1.1 기존의 장바구니보다 2배 큰 새로운 배열을 생성한다.
Product[] tmp = new Product[cart.length*2];
//	1.1.2 기존의 장바구니의 내용을 새로운 배열에 복사한다.
System.arraycopy(cart,0,tmp,0,cart.length);
//	1.1.3 새로운 장바구니와 기존의 장바구니를 바꾼다.
cart = tmp;
}
//	1.2 물건을 장바구니(cart)에 저장한다. 그리고 i의 값을 1 증가시킨다.
cart[i++]=p;
} // add(Product p)

void summary() {
String itemList = ""; int sum = 0;

for(int i=0; i < cart.length;i++) { if(cart[i]==null)
break;
//	1.1 장바구니에 담긴 물건들의 목록을 만들어 출력한다.
itemList += cart[i] + ",";
//	1.2 장바구니에 담긴 물건들의 가격을 모두 더해서 출력한다.
sum += cart[i].price;
}

//	1.3 물건을 사고 남은 금액(money)를 출력한다.
System.out.println("구입한 물건:"+itemList); System.out.println("사용한 금액:"+sum); System.out.println("남은 금액:"+money);
} // summary()
}

 
class Product { int price;
 

// 제품의 가격
 

Product(int price) { this.price = price;
}
}

class Tv extends Product { Tv() { super(100); }

public String toString() { return "Tv"; }
}

class Computer extends Product { Computer() { super(200); }
public String toString() { return "Computer";}
}

class Audio extends Product { Audio() { super(50); }

public String toString() { return "Audio"; }
}

💡✔️7-20. 코드의 실행 결과 예측하기

  • 문제 : [7-20] 다음의 코드를 실행한 결과를 적으시오.
class Sol_Exercise7_20 {
	public static void main(String[] args) {
		Parent2 p = new Child2();	//조상참조변수 p는 자손인스턴스 참조
		Child2 c = new Child2();	//자손참조변수 c는 동타입 인스턴스 참조

		System.out.println("p.x = " + p.x);	//200
		p.method();

		System.out.println("c.x = " + c.x);	//200
		c.method();
	}
}

class Parent2 {
	int x = 100;

	void method() {
		System.out.println("Parent Method");
	}
}

class Child2 extends Parent2 {
	int x = 200;

	void method() {
		System.out.println("Child Method");
	}
}
  • 내 풀이 :
    상속과 객체의 다형성을 이해하고 있는지에 관한 문제이다.
    자손 인스턴스 변수와 조상 인스턴스 변수의 이름이 겹친다면
    (예를 들어 그 변수가 x라고 가정하면)
    x= this.x이다. 조상 인스턴스 변수를 호출하려면 super.x를 호출해야한다.
    p.x와 c.x는 모두 동일한 자손 인스턴스를 가리키고 있으므로 해당 멤버를 호출하는 것은 자손 클래스의 x를 호출하는 것이다.
    따라서 결과는
    p.x = 200
    Child Method
    c.x = 200
    Child Method
    가 나올 것이다.

  • 오답 발생과 그 이유 :
    결과는
    p.x = 100 //✔️✔️
    Child Method
    c.x = 200
    Child Method
    이었다. 무슨 차이가 있는 것일까?
    어떤 참조변수에 자손 인스턴스가 내장되어 있을 때 조상 클래스와 동명의 메서드를 실행하면 자손 클래스에서 오버라이딩 된 것으로 동작한다.
    따라서 p.method()c.method 모두에서 Child Method가 동작하는 것은 당연하다.(내장 객체가 모두 Child 인스턴스 이므로)
    그러나 멤버변수의 경우에는 참조변수의 타입을 따라가는 것 같다. 객체 Child(new Child()) 안에는 int x가 2개 존재한다. Parent에서 상속받은x와 Child에서 생성한 x. 만약 x가 두 클래스 중 하나에만 존재했다면 당연히 문제될 것이 없었지만 상속관계에서 동명의 멤버변수가 겹치면 참조변수의 것을 따라간다. Parent p = new Child();에서 만약 자손의 x를 표현하고 싶다면 참조변수의 형변환이 필요하다.

  • 정답과 해설

    [해설] 조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.

    메서드의 경우 💡조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.

    타입은 다르지만, 참조변수 p, c모두 Child인스턴스를 참조하고 있다.
    그리고, Parent클래스와 Child클래스는 서로 같은 멤버들을 정의하고 있다.

    이 때 조상타입의 참조변수 p로 Child인스턴스의 멤버들을 사용하는 것과 자손타입의 참조변수 c로 Child인스턴스의 멤버들을 사용하는 것의 차이를 알 수 있다.
    메서드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인
    Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라서 달라진다.

  • 💡정리 : 조상 참조변수는 자손인스턴스를 참조할 때 조상의 멤버 + 오버라이딩 된 자손의 메서드 까지 사용할 수 있다.


✔️✔️7-22. 상속받는 클래스의 적절한 생성자 작성하기

  • 문제 :
    [7-22] 아래는 도형을 정의한 Shape클래스이다. 이 클래스를 조상으로 하는 Circle클래 스와 Rectangle클래스를 작성하시오. 이 때, 생성자도 각 클래스에 맞게 적절히 추가해야 한다.

  • 조건 :
    (1)
    클래스명 : Circle
    조상클래스 : Shape
    멤버변수 : double r - 반지름
    (2)
    클래스명 : Rectangle
    조상클래스 : Shape
    멤버변수: double width - 폭 double height - 높이
    메서드 :
    메서드명 : isSquare
    기 능 : 정사각형인지 아닌지를 알려준다.
    반환타입 : boolean
    매개변수 : 없음

abstract class Shape {
	Point p;

	Shape() {
		this(new Point(0, 0));
	}

	Shape(Point p) {
		this.p = p;
	}

	abstract double calcArea();

// 도형의 면적을 계산해서 반환하는 메서드
	Point getPosition() {
		return p;
	}

	void setPosition(Point p) {
		this.p = p;
	}
}

class Point {
	int x;
	int y;

	Point() {
		this(0, 0);
	}

	Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public String toString() {
		return "[" + x + "," + y + "]";
	}
}
  • 내 풀이:
class Rectangle extends Shape {

	double width, height; //폭, 높이
	
	Rectangle() {
		
	}
	
	Rectangle(int width, int height) {
		this.height = height;
		this.width = width;
	}
	
	@Override
	double calcArea() {
		double result = width*height;
		return result;
	}
	
	boolean isSquare() {	//🔥 무슨 메서드인지 모르겠다. 매개변수가 있어야 하는거 아닌가?
		return true;	
	}
}

class Circle extends Shape {

	double r;	// 반지름

	Circle() {
		//super();
	}
	
	Circle(double radius) {
		//super();
		this.r = radius;
	}
	
	@Override
	double calcArea() {
		
		double result = r*r*Math.PI;
		return result;
	}
	
}

abstract class Shape {
	Point p;		//🔥 Point를 has a 관계로 가지고 있음.

	Shape() {
		this(new Point(0, 0));	//🔥	 Shape 객체를 생성하면 Point의 객체가 (x=0, y=0)을 가지고 생성됨. (has a 포함관계)
	}

	Shape(Point p) {
		this.p = p;
	}

	abstract double calcArea(); // 도형의 면적을 계산해서 반환하는 메서드
	
	Point getPosition() {
		return p;
	}

	void setPosition(Point p) {
		this.p = p;
	}
}

class Point {
	int x;
	int y;

	Point() {
		this(0, 0);
	}

	Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public String toString() {
		return "[" + x + "," + y + "]";
	}
}
  • 모범 답안
class Rect extends Shape {
	double width;
	double height;

	Rect(double width, double height) {
		this(new Point(0, 0), width, height);	//✔️ 해당 클래스의 다른 생성자를 
        										//통해 조상클래스의 인스턴스 멤버 new Point(0, 0) 초기화
	}

	Rect(Point p, double width, double height) {
		super(p); //✔️✔️ 조상의 멤버는 조상의 생성자가 초기화하도록 한다. 
        this.width = width;
		this.height = height;
	}

	boolean isSquare() {
		// width나 height가 0이 아니고 width와 height가 같으면 true를 반환한다. 
		return width*height!=0 && width==height;
        //💡✔️ 정확히 해당 인스턴스인지는 instanceof로 확인할 수 없다.
        //💡해당 인스턴스가 가지고 있는 멤버변수의 값이 자동초기화된 값(0)이면 false, 
        //특정 값으로 초기화 되었으면 true를 반환하게 하면 된다!
	}

	double calcArea() {
		return width * height;
	}
}

class Circle extends Shape {
	double r; // 반지름

	Circle(double r) {
		this(new Point(0, 0), r); //✔️✔️ Circle(Point p, double r)를 호출
	}

	Circle(Point p, double r) {
		super(p); //✔️✔️ 조상의 멤버는 조상의 생성자가 초기화하도록 한다. 
        this.r = r;
	}

	double calcArea() {
		return Math.PI * r * r;
	}
}

abstract class Shape {
	Point p;

	Shape() {
		this(new Point(0, 0));
	}

	Shape(Point p) {
		this.p = p;
	}

	abstract double calcArea(); // 도형의 면적을 계산해서 반환하는 메서드

	Point getPosition() {
		return p;
	}

	void setPosition(Point p) {
		this.p = p;
	}
}

class Point {
	int x;
	int y;

	Point() {
		this(0, 0);
	}

	Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public String toString() {
		return "[" + x + "," + y + "]";
	}
}
  • 내 풀이 반성 :
    Shape 클래스는 Point 클래스를 has a 관계로 포함하고 있다.
    Point p;는 Shape 클래스의 인스턴스 변수이므로,
    Shape를 상속받는 Rect 클래스와 Circle 클래스에서도 이를 적절하게 초기화하는 것이 중요하다. (왜냐하면 Point는 이차평면 상의 도형의 좌표가 되는 기준점이기 때문. 기준점이 없으면 불가능.)
    그렇기 때문에 상속받은 클래스의 생성자에서는 Point를 new Point(0,0)을 사용하여 초기화해야 하는데, 💡이 때 this()를 사용하여 코드의 중복이 발생하지 않도록 설계하는 것이 중요하다.
    또한 💡super(p)를 사용하여 조상의 멤버는 조상이 초기화 하도록 하는 생성자 체인 설계가 중요하다.
    💡💡 객체 instanceof 타입은 객체가 해당 타입으로 형변환이 가능한지 여부를 true로 반환하는 것이라 객체의 조상 타입도 true로 반환한다. 따라서 객체가 그 클래스 자체 타입인지 확인하기 위해서는(boolean isSquare) instanceof 메서드를 사용할 것이 아니라 해당 클래스가 가지고 있는 유니크한 변수값이 초기화 되어 있는지 확인한다.(직사각형 클래스의 경우 인스턴스 변수 width와 height) 초기화 되어 있으면 해당 클래스라는 뜻이고 초기화가 되어있지 않으면 해당 클래스가 아니라는 뜻이다.
profile
9에서 0으로, 백엔드 개발블로그

0개의 댓글