Java - 객체, 상수, 캡슐화, 상속, 생성자

김지원·2022년 7월 28일
0

JAVA 총 정리

목록 보기
4/11

객체에 대해 공부해보자.
-> Kichen클래스 (메인) / Pasta 클래스 생성

  • 여기서 왜 noddle 호출이 안되는 걸까?
    지금 public String noddle; 비정적인 멤버 변수이다.
    그렇기 때문에 Stack에 올라가 있지 않다. (즉 어디에서도 존재하지 않음.)
    "스파게티"라는 값을 Heap 넣을 만한 연결된 Stack이 없기 때문에 오류가 뜨는 것이다.
  • static으로 해주면 다들 행복해 한다.
  • 이 두개의 noddle은 동일한 변수이기 때문에 하나의 stack을 가르키게 된다.
    이런 방법으로는 다른 값을 가지는 두개를 만들 수 없다.

❗️그렇기 때문에 각자의 값을 각자가 가질 수 있는 객체를 생성해서 접근을 해야한다.

Pasta 클래스에서 static을 다시 빼고 객체화를 해보자.
비정적인 멤버에 접근하기 위해서는 해당 클래스를 객체화 해야한다.

  • 이제 각 변수는 같은 타입 (Pasta) 이지만 다른 변수임으로 각각의 값이 잘 들어가게 된다.

System.out.println(capellini == spaghetti);
-> Stack에 있는 값 그 자체를 비교하기 때문에 당연히 false가 뜬다.


Memory 구조

Pasta capellini = new Pasta();
capellini.noodle = "카펠리니";

  • new Pasta(); : {Pasta} 엔터런스가 생성이 된다.

pasta는 noddle을 가지고 있어서 noddle이라는 레퍼런스 타입에 대한 Stack에 대한 내용이 또 생기게 된다.
누구의 noddle인지 어떻게 아냐면 0x10이라는 애가 1x01~1x04이라는 값을 가지게 된다.

풀이를 하면 방금 객체화한 파스타 타입인 capellini이 가지고 있는 noddle이라는 String은 참조타입이기때문에 그 값을 Stack에서 가지지 않는다.
1x01~1x04이라는 Heap의 영역을 받아서 "카,펠,리,니"라는 값이 들어가게 된다.

따라서 capellini.noddle로 접근을 하면
0x00 → 1x00 으로 접근을 하고 0x10 → 1x01~1x04로 가게 되는 것이다.

Pasta spaghetti = new Pasta();
spaghetti.noodle = "스파게티";

스파게티를 또 생성하게 되면 스파게티에 대한 주소(0x01)가 생성이 되고 비어있는 Stack에 들어가게 된다. 그리고 비어있는 Heap의 1x05에 만들어지고 noddle의 0x11이라는 값을 받게 된다.

→ 이유 : pasta가 가지고 있는 멤버이기 때문이다.

지역변수(메서드안 변수)는 값을 대입해놓지 않으면 값 대입을 제외한 그 어떠한 것도 할 수가 없다.
그와 달리 클래스가 직접 가진 멤버변수들은 값 지정을 하지 않으면 기본값으로 설정이 된다.

기본 값 : int = 0 / boolean = false / 기타 나머지 레퍼런스 타입 : null

즉, 스파게티에 대한 누들(값)을 지정해주지 않으면? 1x05의 주소값을 찾으려니 null이 되는 형태가 되버리는 것이다.
그렇게 되면 NullPoingException이 뜬다.

Sout(spaghetti.noddle.length());
→ NullpointExeption 발생

그래서 noddle에 대한 Stack영역에 대한 공간을 지정해주고 "스,파,게,티"라고 넣어준다.


  • noodle에 static이 붙어도 객체를 통해서 정적인 객체에 접근이 가능하기 때문에 사용은 가능하다.

그런데 capellini로 noodle에 접근을 했는데 스파게티가 출력이 되었다.
Main이 돌아가기전에 정적인것들 먼저 호출되는데 Pasta에 아무것도 적지않았으니 stack에 null이 들어간다. 왜냐?

Pasta capellini = new Pasta(); 만들어 진다고 Pasta 클래스의 객체화가 일어나는 것보다 접근할 수 있게 만들어 준 것으로 이해하면 된다.
Stack에 #Pasta.noodle 이 생기고 0x00이라는 주소값이 주어지고 Stack의 0x00자리에 Null이 들어간다. 그리고 main메서드가 실행된다.

그 다음으로 capellini가 생기고 0x01에 들어간다.

noodle은 Pasta가 가진 멤버변수이지만 정적인 멤버변수이다.
즉, 객체화를 할 필요가 없다는 의미이다. (정적인 멤버변수는 클래스가 객체화 될 때 객체화가 되지 않는다. 원래는 접근조차 되지 않는다.)

  • capellini.noodle = "카펠리니"; noodle은 정적이라서 접근을 할 수 없다. 경고를 날려주고 있다.
    Static member 'dev.jwkim.kitchen.Pasta.noodle' accessed via instance reference
    : noodle은 정적인데 객체(capellini)를 통해서 접근하고 있다 라는 경고를 해준다. 정적인 것에 접근할 때는 클래스를 통해서 접근한다. (Integer.parseInt() 처럼)

그래서 발생하는 일은 내부적으로 객체에 접근하는게 아니라 capellini.noodle = "카펠리니"; 하면 알아서 capellini의 타입인 Pasta가 가진 noodle이라고 인식한다.
따라서 둘 다 Pasta.noodle(); 으로 바뀌게 되고 가르키는 대상이 같아진다.

스파게티라는것도 생성이 되는데 @1x01~@1x04를 가르키고 있던 애가 @1x06~@1x09으로 바뀐다. (남아있는 1x01 ~ 1x04는 시간이 흐른 후 GC가 처리해준다.)

결국 프로그램내에 정적인것은 하나만 존재해야하고 클래스의 멤버 변수들은 비정적인 멤버 변수이여야 한다.


상수

  • final Pasta capellini = new Pasta(); : 상수
    capellini.noodle = "카펠리니"; capellini는 상수인데 이게 왜 될까?
    기초타입이 아니라 참조타입이기 때문이다.

final로 지정한 변수들은 Stack이 가진 값들이 변하면 안된다.

int num = 5;
num = 6;    // 얘네는 기초타입이기 때문에 stack의 값이 바뀌는 것임으로 안된다.

final Pasta capellini = new Pasta();이라면
capellini = null; 만 안하면 된다.
즉, Stack에 있는 1x00이 null 로 변하지 않으면 되는 것이다.

  • noodle을 변하지 못하게 하려면 Pasta클래스의 noodle을 상수로 지정해주면 된다. (final을 붙여주면 선언을 못하게 된다.)

멤버 변수 캡슐화(Encapsulation)

  • 정적이지 않고 상수가 아닌 멤버 변수의 접근 제한자(Access Modifier)를 private으로 지정함.
  • 해당 멤버 변수의 값을 가져오기 위한 메서드인 Getter 메서드와 필요에 따라 값을 지정하기 위한 메서드인 Setter 메서드를 생성.
    : 이 두 조건을 만족시킨다면 캡슐화를 한 것이다.

  • 정적이지 않고 상수가 아닌 멤버 변수 : noodle이 대상이 된다.

Getter, Setter

  • private으로 접근제한자를 수정하니 private은 클래스 내부에서만 접근이 가능하기 때문에 main(kitchen) 에서 오류가 터진다. (static 붙어있는 건 무시..)
    => getter, setter을 생성한다. (getter, setter의 접근 제한자는 public임으로 다른 클래스에서 사용이 가능하다.)
  • noodle 멤버 변수에 대한 Getter 메서드
  • noodle 멤버 변수에 대한 Setter 메서드

오류가 터진 main을 수정해보자.

  • 값을 지정해주고 싶다면 set을 사용한다.
  • 값을 불러올 때는 get을 사용한다.

리팩토링, Refactoring

: 어떠한 멤버나 클래스의 이름을 변경할 때 참조하고 잇는 부분의 코드도 함께 바꾸어주는 것. 반드시 써야함.

단축키 : shift + f6

this : 객체로의 자기 자신

  • 객체화된 Pasta 타입의 객체인데 자기 자신을 의미한다.
    capellini.setNoodle 했을 때 set이 호출이 되는데 여기서 this는 main의 capellini를 의미한다.
  • 객체화된 자기자신을 의미한다.

  • printSame이라는 메서드를 Pasta클래스에 생성하였다.
  • main에서 호출하면 false / true 가 뜨게 된다.
  • spaghetti 객체를 통한 printSame 메서드를 호출하면 spaghetti을 전달해준다. spaghetti가 들어오게 되고 this가 위의 spaghetti와 동일하는가에 대해서 물어보게 된다. this는 객체화된 자기자신을 의미한다. 즉 spaghetti를 의미하게 됨으로 이러한 결과가 나오게 된다.

Setter 메서드의 당위성.

  • String noodle 자리에 라면같은 다른 애들이 못오도록 하기 위해서 setter을 사용한다.

  • 밑에 this.noodle = noodle 은 스파게티이거나 카펠리니 일때만 작동을 한다.

멤버변수를 private으로 막아야하기 때문에 setter을 생성했고 그에 따라 필연적으로 getter도 생성을 하는 것이다. getter을 생성하지 않으면 값을 불러올 방법이 없기 때문이다.

(private를 public으로 바꾸면 setter메서드가 필요없어진다.)

참고) 반환타입이 없는 (void인) 메서드에서 그냥 return; 을 적으면 
메서드의 실행을 중단한다. 당연히 이때에는 뒤에 반환값을 적으면 안됨.

파스타 면에 소스를 부어보자.

  • Pasta 클래스에서 private으로 멤버변수를 하나 생성하고 getter, setter도 생성해준다.
  • main에서 각 면에 소스를 지정해준다.
  • getName이라는 메서드를 Pasta클래스에 생성해서 소스를 부어 완성된 파스타를 출력해보자.
  • main에서 출력해보면 합쳐서 잘 나온다.

❗️ 그런데 만일 소스의 칼로리를 가지는 정수형 멤버변수를 Pasta 클래스에 만들었다고 가정해보자.
Pasta랑 noodle / sauce 관계에는 끼어들게 없다.
그런데 칼로리랑은 직접 맞닿아있으면 클래스 구조를 잘 짰다고 말하기 어렵다.
왜냐하면 pasta가 sauce를 가지고 있고 sauce가 칼로리를 가지고 있는 것이기 때문이다.
따라서 sauce라는 타입을 따로 만들어야한다.

  • 소스 이름에 대한 멤버변수 / 칼로리에 대한 멤버변수와 getter setter을 만들어준다.

  • Pasta클래스에 있던 sauce의 타입을 String에서 Sauce로 바꾸어준다. 물론 게터세터도 수정해야한다.
  • 수정을 해도 오류가 남아있다. main으로 넘어가자.

  • Sauce 타입의 소스를 줘야하는데 String의 소스를 줘서 main에서 오류가 터져있다.
  • 토마토 소스 / 트러플 크림 소스 두가지 객체를 만들어주고 이름과 칼로리를 지정해준다.

  • 그리고 출력을 해보았더니 응? 주소값이 찍히게 된다.

  • this.sauce 가 아닌 this.sauce.getName()을 호출해야한다.

Sauce tomatoSauce = new Sauce();0x00
tomatoSauce.setName(“토마토”);
tomatoSauce.setKcal(300);

Pasta capellini = new Pasta();
capellini.setNoodle(“카펠리니”);
capellini.setSauce(tomatoSauce);

sout(capellini.getName());
// 토마토 카펠리니


  • 트러플 소스는 포함시키지 않았다.
  • 둘 다 도마도 소스가 뜨게 된다.

capellini와 spaghetti는 다른 객체지만 소스는 같은 객체이다.

capellini 토마토 소스를 주고나서 setName하고 스파게티에게 주었다.
스파게티는 도마도가 뜨는 것이 이해가 되는데 capellini는 왜 도마도가 뜨는 걸까?

먼저 tomatoSauce에 setName 해준것을 “도마도”로 바꾸어서 @1x01~@1x03 은 도마도가 되었다. 그 다음 capellini와 spaghetti 객체를 만들고 각 파스타의 소스를 tomatoSauce를 지정했더니 둘 다 같은 @1x00으로 가게 되며 1x00에는 Sauce의 Name의 주소가 0x10으로 되어있고 결과적으로 @1x01~@1x03 = “도마도”로 이동이된다.
그렇기 떄문에 둘다 도마도 카펠리니 / 도마도 스파게티 로 출력이 되는 것이다.


캡슐화를 위해 Noodle도 클래스를 생성해주자.

  • Sauce와 같이 타입 변경을 싹 해준다.

  • Noodle, Sauce, pasta 모두 Object라는 클래슬 상속 받고 있는 상황이기 때문에 우리가 만들어준 적 없는 toString이라는 메서드를 가지고 있다.

이 세개중에서 누가 누구를 소속한다라는 것을 정의할 수 있을까?

면은 무엇이고 소스가 무엇인지에 대해서 속성으로 가지고 있는 것이지 면이 가진특성을 파스타가 받아 속성이 확장되었다고 할 수 없다.

=> 포함관계라는 것은 음식이라는 것에 Pasta가 있는 이러한 것들이다.

이해를 위해 Food클래스 생성해보자.

  • eat 기능 구현도 해준다.

  • 카펠리니 객체는 Pasta 타입인데 Pasta에는 eat이란게 없어서 카펠리니 객체에 eat을 호출할 수 없다.

파스타로 하여금 푸드를 상속받게 하자.

  • Pasta 타입이 Food클래스를 상속 받고 있기 때문에 카펠리니 객체에 eat을 호출할 수 있게 되었다.
  • 여기서 eat을 ctrl 클릭을 해보니 Food 클래스로 가게 된다.

상속 (Inheritance)

  • 어떠한 클래로 하여금 상속 받을 부모 클래스를 명시하지 않으면 이는 반드시 Object 클래스를 상속 받는다.
  • Class 'Food' explicitly extends'java.lang.Object'
    : extends Object 쓸때 없는 표현을 지우라는 경고가 뜬다.
  • 그렇기 때문에 항상 1개(= Object)의 클래스를 상속받고 있다.
  • 어떠한 클래스는 반드시 하나의 클래스만 상속 받아야한다.
  • Class cannot extend multiple classes

extends

{클래스} extends {부모 클래스}

상속을 사용하는 이유

부모 클래스가 가지고 있는 특성이나 메서드를 그대로 가지면서 추가 기능을 만들어 주기 위해 사용한다.
부모클래스가 가지지 않은 새로운 기능을 만드는 것인데 대신 부모가 가진 모든 기능을 구현을 해야한다.

class Pasta extends Food {...}

------------------------------------
         자식클래스 {Pasta}
     getNoodle(), setNoodle()
     ------------------------
         부모 클래스 {Food}
              eat()
     ------------------------
      	 자식클래스 {Pasta}
------------------------------------
  • 자식클래스 안에 부모클래스가 존재한다.

❗️Food클래스의 존재 이유는 모든 음식이 가진 반복적인 부분을 반복적으로 처리하지 않게 하기 위하여 만들어진 클래스이다.

=> 그래서 사용하는것 생성자이다.


생성자 (Constructor)

  • 생성자는 그 클래스가 객체화되기 위해 반드시 실행되어야 하는 일종의 메서드이다.
  • 생성자를 별도로 명시하지 않을 경우 항상 기본 생성자(Default Constructor)가 생략된 채로 존재한다.
  • 생성자 구현부의 첫 구분은 반드시 super(...)이거나 this(...)이어야한다.
  • 생성자 호출이 완전히 끝나야 객체화가 완료된 것을 본다.
  • 생성자의 형태
(접근 제한자) (클래스 이름) { ... }
  • 기본 생성자의 형태
public Food() {
	super();
}

  • 앞의 Pasta는 변수 타입인 Pasta이고 뒤의 Pasta는 Pasta클래스의 기본생성자의 Pasta이다.
  • 뒤 Pasta를 ctrl 클릭해서 들어가보면 생성자로 가는것도 확인이 된다.

  • 파스타 클래스에 기본생성자 사용하여서 부모 클래스 생성자 호출 super() 을 하였다. (여기서 super은 Food)

  • main에서 출력을 해보니
  • 직접 적은적이 없지만 생성자에 적어놓은 내용이 출력이 되었다.

super

: 부모 클래스

생성자 구현부의 첫 구분은 반드시 super(...)이거나 this(...)이어야한다.

  • super 즉 부모 클래스 호출이 먼저 되어야한다.

생성자 호출이 완전히 끝나야 객체화가 완료된 것을 본다.

  • Food클래스에도 빈 생성자를 생성해주었다.
  • main에서 출력해보니 Pasta보다 Food가 먼저 객체화된걸 확인할 수 있다. 즉, Food 객체화가 끝나야 Pasta를 객체화 하는 것이다.

부모를 객체화하기전에는 아무것도 하지 못한다!

Pasta() {
	super();
    ~~
}
Food() {
	super();
    ~~
}
object() {}
  • super가 끝나기 전에는 밑(: ~~)으로 내려가지 않는다.
  • Pasta 생성자의 super 은 Food다. Food의 super은 Object이다.
    Object로 가니 아무것도 없으니 호출이 끝났다. 이렇게 되면 Object가 객체화 된 것이다.
    그리고 Food의 super 밑에 있는 ~~ 을 실행하고 Food는 객체화 된다.
    마지막으로 Food 구현부도 끝났으니 Pasta 생성자로가서 super 밑에 있는 ~~ 이 실행되고 Pasta도 객체화가 된다.
  • 그래서 Pasta에서 eat, equlas, toString을 사용할 수 있는 것이다.
    상속관계를 만들며 개발을 하는 것이 개발을 잘하는 것이다.

  • kitchen과 Food 클래스의 패키지는 다르다.
  • Food 생성자의 접근제한자를 protected로 두었다.
  • 그렇기 때문에 protected인 것에 대해서 접근을 할 수 없다.

  • 그런데 Food를 상속받는 Pasta는 왜 접근이 가능한 것 일까?
    Food랑 Pasta는 상속관계임으로 접근이 가능한 것이다.
    다른 패키지에 위치한다고 해도 접근이 가능하다.

protected는 default(같은 패키지만 접근 가능)이면서도 상속관계에 있는 것은 허용한다. 그러한 목적으로 protected를 많이 사용한다.

본 클래스가 객체화가 되지 않아야 하며 Food클래스는 직접 객체화를 못해야 하고 그것을 상속하는 것들에 대해서 객체화가 되게끔 할 때는 생성자의 접근제한자를 protected로 한다.


  • String을 객체화하는데 받는 타입을 Object로 했다면 기능의 축소이다.

  • str이 String 객체는 맞지만 객체를 통해서 접근할 수 있는 범위를 Object로 제한하는 것이다. 따라서 String 메서드인 length가 호출이 되지 않는 것이다.

  • Food food = {Pasta};
    : Food타입의 food에 Pasta타입인 카펠리니라고 했다.

  • Food타입의 food에는 메서드eat가 존재하는 것이 맞다.

  • Obejct타입의 ob가 Pasta타입인 카펠리니라고 했을 떄는 eat메서드가 존재하지 않는다.
    이러한 것이 기능의 축소이다.

부모 타입은 자식 객체를 받을 수 없다.

=> 강제 형변환이 되어야한다.

강제형변환 (Explicit Conversion, Denituibm Downcasting)

: 부모 객체가 부모 객체를 받을 때

자동 형변환(Implicit Conversion, Promotion, Upcasting) 이 발생한다.

: 자식 객체가 부모 객체를 받을 때


  • Food타입의 food가 Pasta 타입의 cp를 받게 되면 오류가 발생한다.
    Food가 Pasta의 부모이기 때문에 자동 형변환이 되지 않기 때문이다.
    확대 될 것에 대한 기능을 가지고 있지 않으면 무엇을 해야할지 모르기 때문에 오류가 발생한다.
    그래서 부모가 자식 객체를 받을 때는 형변환이 필요하다.

다른 예시로 강제형변환을 해보자.

  • "kkk"를 String타입의 string이라고 했고 그 string을 오브젝트 타입의 objectString이라고 하니 내부적으로는 자동형변환이 일어났지만 아무런 일이 발생하지 않는다.
    그런데 objectString을 String타입의 stringObject라고 하니 안된다고 햊준다.
    부모 객체가 자식 객체를 받으려고 했기 때문이다. 그럴 때 사용하는 것이 강제형변환이다.

강제형변환은 상당히 주의를 기울여야한다.

위의 예시는 문제가 없다. 왜냐하면 강제형변환이 되는 원래의 objectString은 원래 String이였기 때문에 문제가 없다.
아래의 예시를 보자.

  • 실행을 해보면 예외가 발생한다.
    ClassCastException : 형변환을 하다가 예외가 발생했다.
    objectFood은 표면상 Object인데 파고보니 Pasta 타입이다. 그래서 String으로 형변환 할 수 없음.

개발코드를 잘 짰다면 강제형변환 할 일은 많지 않다. 반면에 자동형변환은 엄청나게 많이 한다.


다형성

: 어떠한 객체가 다른 여러타입으로 변할 수 있음을 나타내고 그것을 통해 로직을 구현하는 그 자체.

0개의 댓글