본문 바로가기

Java/기초

[Java] 객체지향 프로그래밍 2

1. JVM의 메모리 구조

응용프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

cv : 클래스변수, lc : 지역변수, iv : 인스턴스변수

1) 메서드영역(method area)

- 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 이 때, 그 클래스의 클래스변수(class variable)도 이 영역에 함께 생성된다.

2) 힙(heap)

- 인스턴스가 생성되는 공간, 프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 즉, 인스턴스변수(instance variable)들이 생성되는 공간이다.

3) 호출스택(call stack 또는 execution stack)

- 호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 그리고 메서드가 작업을 마치면 할당되었던 메모리공간은 반환되어 비워진다.

https://yaboong.github.io/java/2018/05/26/java-memory-management/

 

자바 메모리 관리 - 스택 & 힙

개요 Java 에서 메모리 관리는 어떻게 이루어지는지 알아보기 위함. Stack 과 Heap 영역 각 역할에 대해 알아본다. 간단한 코드예제와 함께 실제 코드에서 어떻게 Stack 과 Heap 영역이 사용되는지 살펴

yaboong.github.io

https://yaboong.github.io/java/2018/06/09/java-garbage-collection/

 

자바 메모리 관리 - 가비지 컬렉션

개요 Java 가비지 컬렉션에 대해서 공부한 내용을 정리해본다. Java 에서 메모리 관리는 어떻게 이루어지는지 이해하고 있으면 좋다. 자바 메모리 관리 - 스택 & 힙 를 먼저 읽는 것을 추천한다. 모�

yaboong.github.io

Heap 영역의 오브젝트 중 Stack 에서 도달 불가능한 (Unreachable) 오브젝트들은 가비지 컬렉션의 대상이 된다.

2. 기본형 매개변수와 참조형 매개변수

기본형 매개변수 - 변수의 값을 읽기만 할 수 있다.(read only)

참조형 매개변수 - 변수의 값을 읽고 변경할 수 있다.(read & write)

3. 재귀호출(revursive call)

왜? 굳이 반복문 대신 재귀호출을 사용할까?. 그 이유는 재귀호출이 주는 논리적 간결함 때문이다. 몇 겹의 반복문과 조건문으로 복잡하게 작성된 코드가 재귀호출로 작성하면 보다 단순한 구조로 바뀔 수도 있다. 아무리 효율적이라도 알아보기 힘들게 작성하는 것보다 다소 비효율적이라도 알아보기 쉽게 작성하는 것이 논리적 오류가 발생할 확률도 줄어들고 나중에 수정하기도 좋다.

4. 클래스 메서드(static메서드)와 인스턴스 메서드

메서드 앞에 static이 붙어 있으면 클래스메서드이고 붙어 있지 않으면 인스턴스 메서드이다. 클래스 메서드도 클래스변수처럼, 객체를 생성하지 않고도 '클래스이름.메서드이름(매개변수)'와 같은 식으로 호출이 가능하다. 반면에 인스턴스 메서드는 반드시 객체를 생성해야만 호출할 수 있다.

그렇다면 클래스를 정의할 때, 어느 경우에 static을 사용해서 클래스 메서드로 정의해야 하는 것일까?

클래스는 '데이터(변수)와 데이터에 관련된 메서드의 집합'이므로, 같은 클래스 내에 있는 메서드와 멤버변수는 아주 밀접한 관계가 있다.

인스턴스 메서드는 인스턴스 변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드이다. 

클래스(static) 메서드는 인스턴스와 관계없는(인스턴스 변수, 인스터스 메서드를 사용하지 않는) 메서드이다.

[참고] 클래스 영역에 선언된 변수를 멤버변수라 한다. 멤버변수 중에 static이 붙은 것은 클래스변수(static변수), static이 붙지 않은 것은 인스턴스변수라 한다. 멤버변수는 인스턴스변수와 static변수를 모두 통칭하는 말이다.

 

1) 클래스를 설계할 때, 멤버변수 중 모든 인스턴스에 공통적으로 사용해야하는 것에 static을 붙인다.

- 생성된 각 인스턴스는 서로 독립적이기 때문에 각 인스턴스의 변수(iv)는 서로 다른 값을 유지한다. 그러나 모든 인스턴스에서 같은 값이 유지되어야 하는 변수는 static을 붙여서 클래스변수로 정의해야 한다.

2) 클래스 변수(static변수)는 인스턴스를 생성하지 않아도 사용할 수 있다.

- static이 붙은 변수(클래스변수)는 클래스가 메모리에 올라갈 때 이미 자동적으로 생성되기 때문이다.

3) 클래스 메서드(static메서드)는 인스턴스 변수를 사용할 수 없다.

- 인스턴스 변수는 인스턴스가 반드시 존재해야만 사용할 수 있는데, 클래스 메서드(static이 붙은 메서드)는 인스턴스 생성없이 호출가능하므로 클래스 메서드가 호출되었을 때 인스턴스가 존재하지 않을 수 있다. 그래서 클래스 메서드에서 인스턴스번수의 사용을 금지한다.

- 반면에, 인스턴스 변수나 인스턴스 메서드에서는 static이 붙은 멤버들을 사용하는 것이 언제나 가능하다. 인스턴스변수가 존재한다는 것은 static변수가 이미 메모리에 존재한다는 것을 의미하기 때문이다.

4) 메서드 내에서 인스턴수를 사용하지 않는다면, static을 붙이는 것을 고려한다.

- 메서드의 작업내용 중에서 인스턴스변수를 필요로 한다면, static을 붙일 수 없다. 반대로 인스턴스변수를 필요로 하지 않는다면 static을 붙이자. 메서드 호출시간이 짧아지므로 성능이 향상된다. static을 안 붙인 메서드(인스턴스메서드)는 실행 시 호출되어야 할 메서드를 찾는 과정이 필요하기 때문에 시간이 더 걸린다.

 

클래스의 멤버변수 중 인스턴스에 공통된 값을 유지해야하는 것이 있는지 살펴보고 있으면, static을 붙여준다.

작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에 static을 붙일 것을 고려한다.

5. 오버로딩(overloading)

1) 오버로딩이란?

- 메서드도 변수와 마찬가지로 같은 클래스 내에서 서로 구별될 수 있어야 하기 때문에 각기 다른 아름을 가져야 한다. 그러나 자바에서는 한 크랠스 내에 이미 사용하려는 이름과 같은 이름을 가진 메서드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메서드를 정의 할 수 있다.

이처럼, 한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것을 '메서드 오버로딩(method overloading)'또는 간단히 '오버로딩(overloading)'이라 한다.

 

2) 오버로딩의 조건

- 메서드 이름이 같아야 한다.

- 매개변수의 개수 또는 타입이 달라야 한다.

* 반환타입은 오버로딩을 구현하는게 아무런 영향을 주지 못한다.

 

3) 오버로딩의 예

가장 대표적인 것은 println메서드 이다. println메서드의 괄호 안에 값만 지정해주면 화면에 출력하는데 아무런 어려움이 없었다. 하지만 실제로는 println메서드를 호출할 때 매개변수로 지정하는 값의 타입에 따라서 호출되는 println메서드가 달라진다.

PrintStream클래스에는 어떤 종류의 매개변수를 지정해도 출력할 수 있도록 아래와 같이 10개의 오버로딩된 println메서드를 정의해 놓고 있다.

https://www.javatpoint.com/PrintStream-class

 

Java PrintStream class - javatpoint

The PrintStream class provides methods to write data to another stream. The PrintStream class automatically flushes the data so there is no need to call flush() method. Moreover, its methods doesn't throw IOException.

www.javatpoint.com

4) 오버로딩의 장점

근본적을는 같은 기능을 하는 메서드들이지만, 서로 다른 이름을 가져야 하기 때문에 메서드를 작성하는 쪽에서는 이름을 짓기도 어렵고, 메서드를 사용하는 쪽에서는 이름을 일일이 구분해서 기억해야하기 때문에 서로 부담이 된다. 

하지만 오버로딩을 통해 여러 메서드들이 println이라는 하나의 이름을 정의될 수 있다면, println이라는 이름만 기억하면 되므로 기억하기도 쉽고 이름도 짧게 할 수 있어서 오류의 가능성을 많이 줄일 수 있다. 그리고 메서드의 이름만 보고도 ' 이 메서드을은 이름이 같으니, 같은 기능을 하겠구나.'라고 쉽게 예측할 수 있게 된다.

 

5) 가변인자(varargs)와 오버로딩

기존에는 메서드의 매개변수가 고정적이었으나 JDK1.5부터 동적으로 지정해 줄 수 있게 되었으며, 이 기능을 '가변인자(variable arguments)'라고 한다.

가변인자는 '타입...변수명'과 같은 형식으로 선언하며, PrintStream클래스의 printf()가 대표적인 예이다.

 

public PrintStream printf(String format, Object...args){ ... }

 

위와 같이 가변인자 외에도 매개변수가 더 있다면, 가변인자를 매개변수 중에서 제일 마지막에 선언해야 한다. 그렇지 않으면, 컴파일 에러가 발생한다. 가변인자인지 아닌지를 구별할 방법이 없기 때문에 허용하지 않는 것이다.

6. 생성자(Constructor)

1) 생성자란

- 생성자는 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메서드'이다. 따라서 인스턴스변수의 초기화 작업에 주로 사용되며, 인스턴스 생성 시에 실행되어야 할 작업을 위해서도 사용된다.

- 생성자의 조건

1.1) 생성자의 이름은 클래스의 이름과 같아야 한다.

1.2) 생성자는 리턴 값이 없다.

[참고] 인스턴스 초기화란, 인스턴스변수들을 초기화하는 것을 뜻한다.

연산자 new가 인스턴스를 생성하는 것이지 생성자가 인스턴스를 생성하는 것이 아니다.

 

Card c = new Card();

1.1) 연산자 new에 의해서 메모리(heap)에 Card클래스의 인스턴스가 생성된다.

1.2) 생성자 Card()가 호출되어 수행된다.

1.3) 연산자 new의 결과로, 생성된 Card인스턴스의 주소가 반환되어 참조변수 c에 저장된다.

 

2) 기본 생성자(default constructor)

모든 클래스에는 반드시 하나 이상의 생성자가 정의되 있어야 한다. 그러나 지금까지 클래스에 생성자를 정의하지 않고도 인스턴스를 생성할 수 있었던 이유는 컴파일러가 제공하는 '기본 생성자(default constructor)'덕분이었다.

컴파일 할 때, 소스파일(*.java)의 클래스에 생성자가 하나도 정의되지 않은 경우 컴파일러는 자동적으로 아래와 같은 내용의 기본 생성자를 추가하여 컴파일 한다.

  클래스 이름() {}  -> Card(){}

컴파일러가 자동적으로 추가해주는 기본 생성자는 이와 같이 매개변수도 없고 아무런 내용도 없는 아주 간단한 것이다. 특별히 인스턴스 초기화 작업이 요구되어지지 않는다면 생성자를 정의하지 않고 컴파일러가 제공하는 기본 생성자를 사용하는 것도 좋다.

[참고] 클래스의 '접근 제어자(Access Modifier)'가 public인 경우에는 기본생성자로 'public 클래스이름() {}'이 추가된다.

* 기본 생성자가 컴파일러에 의해서 추가되는 경우는 클래스에 정의된 생성자가 하나도 없을 때 뿐이다.

 

3) 매개변수가 있는 생성자

생성자도 메서드처럼 매개변수를 선언하여 호출 시 값을 넘겨받아서 인스턴스의 초기화 작업에 사용할 수 있다. 인스턴스마다 각기 다른 값으로 초기화되어야 하는 경우가 많기 때문에 매개변수를 사용한 초기화는 매우 유용한다.

 

4) 생성자에서 다른 생성자 호출하기 - this(), this

같은 클래스의 멤버들 간에 서로 호출할 수 있는 것처럼 생성자 간에도 서로 호출이 가능하다. 단, 다음의 두 조건을 만족시켜야 한다.

- 생성자의 이름으로 클래스이름 대신 this를 사용한다.

- 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.

생성자에서 다른 생성자를 첫 줄에서만 호출이 가능하도록 한 이유는 생성자 내에서 초기화 작업도중에 다른 생성자를 호출하게 되면, 호출된 다른 생성자 내에서도 멤버변수들의 값을 초기화 할 것이므로 다른 생성자를 호출하기 이전의 초기화 작업이 무의미해질 수 있기 때문이다.

- this : 인스턴스 자신을 가리키는 참조변수, 인스턴스의 주소가 저장되어 있다. 모든 인스턴스메서드에 지역변수로 숨겨진 채로 존재한다.

- this(), this(매개변수) : 생성자, 같은 클래스의 다른 생성자를 호출할 때 사용한다.

[참고] this와 this()는 비슷하게 생겼을 뿐 완전히 다른 것이다. this는 '참조 변수'이고, this()는 '생성자'이다.

 

5) 생성자를 이용한 인스턴스의 복사

현재 사용하고 있는 인스턴스와 같은 상태를 갖는 인스턴스를 하나 더 마들고자 할 때 생성자를 이용할 수 있다. 두 인스턴스가 같은 상태를 갖는다는 것은 두 인스턴스의 모든 인스턴스가 변수(상태)가 동일한 값을 갖고 있다는 것을 뜻한다.

하나의 클래스로부터 생성된 모든 인스턴스의 메서드와 클래스변수는 서로 동일하기 때문에 인스턴스간의 차이는, 인스턴스마다 각기 다른 값을 가질 수 있는 인스턴스변수 뿐이다.

  Car(Car c){

          color = c.color;

          gearType = c.gearType;

          door = c.door;

  }

위의 코드는 Car클래스의 참조변수를 매개변수로 선언한 생성자이다. 매개변수로 넘겨진 참조변수가 가리키는 Car인스턴스의 인스턴스변수인 color, gearTyp, door의 값을 인스턴스 자신으로 복사하는 것이다.

어떤 인스턴스의 상태를 전혀 알지 못해도 똑같은 상태의 인스턴스를 추가로 생성할 수 있다.

https://taetaetae.github.io/2018/08/21/how-to-use-cloneUtils/

 

자바 객체 복사하기 ( feat. how to use CloneUtils? )

자바(Java)로 개발을 하다보면 한번쯤 객체를 복사하는 로직을 작성할때가 있다. 그때마다 나오는 이야기인 Shalldow Copy 와 Deep Copy. 한국어로 표현하면 얕은 복사와 깊은 복사라고 이야기를 하는데

taetaetae.github.io

인스턴스를 생성할 떄는 다음의 2가지 사항을 결정해야한다.

- 클래스 : 어떤 클래스의 인스턴스를 생서할 것인가?

- 생성자 : 선택한 클래스의 어떤 생성자로 인스턴스를 생성할 것인가?

 

7. 변수의 초기화

변수를 선언하고 처음으로 값을 저장하는 것을 '변수의 초기화'라고 한다.  멤버변수는 초기화를 하지 않아도 자동적으로 변수의 자료형애 맞는 기본값으로 초기화가 이루어지므로 초기화하지 않고 사용해도 되지만, 지역변수는 사용하기 전에 반드시 초기화해야 한다.

  class InitTest {

         int x;          //인스턴스변수

         int y = x;    //인스턴스 변수

 

         void method1(){

                int i;      //지역변수

                int j = i; //에러. 지역변수를 초기화하지 않고 사용

         }

  }

멤버변수(클래스 ,인스턴스 변수)와 배열의 초기화를 선택적이지만, 지역변수의 초기화는 필수적이다.

멤버변수의 초기화 방법

- 명시적 초기화(explicit initialization)

- 생성자(constructor)

- 초기화 블럭(initialization block)

     ├ 인스턴스 초기화 블럭 : 인스턴스변수를 초기화 하는데 사용

     └ 클래스 초기화 블럭    : 클래스변수를 초기화 하는데 사용

 

1) 명시적 초기화(explicit initialization)

변수를 선언과 동시에 초기화하는 것을 명시적 초기화라고 한다. 가장 기본적이면서도 간단한 초기화 방법이므로 여러 초기화 방법중에서 가장 우선적으로 고려되어야 한다.

  class Car {

     int door = 4;                // 기본형(primitive type) 변수의 초기화

     Engint e = new Engine(); // 참조형(reference type) 변수의 초기화

     //...

  }

명시적 초기화가 간단하고 명료하긴 하지만, 보다 복잡한 초기화 작업이 필요할 때는 '초기화 블럭(initialization block)'또는 생성자를 사용해야 한다.

 

2) 초기화 블럭(initialization block)

초기화 블럭에는 '클래스 초기화 블럭'과 '인스턴스 초기화 블럭' 두 가지 종류가 있다. 

- 클래스 초기화 블럭    : 클래스변수의 복잡한 초기화에 사용된다.

- 인스턴스 초기화 블록 : 인스턴스변수의 복잡한 초기화에 사용된다.

초기화 블럭을 작성하려면, 인스턴스 초기화 블럭은 단순히 클래스 내에 블럭{}만들고 그 안에 코드를 작성하기만 하면 된다. 그리고 클래스 블럭은 인스턴스 초기화 블럭 앞에 단순히 static을 덧붙이기만 하면 된다.

초기화 블럭 내에는 메서드 내에서와 같이 조건문, 반복문, 예외처리구문 등을 자유롭게 사용할 수 있으므로, 초기화 작업이 복잡하여 명시적 초기화만으로는 부족한 경우 초기화 블럭을 사용한다.

  class InitBlock{

     static{ /* 클래스 초기화블럭 입니다. */ }

     { /* 인스턴스 초기화블럭 입니다. */ }

  }

클래스 초기화 블럭은 클래스가 메모리에 처음 로딩될 때 한번만 수행되며, 인스턴스 초기화 블럭은 생성자와 같이 인스턴스를 생성할 때 마다 수행된다. 그리고 생성자보다 인스턴스 초기화 블럭이 먼저 수행된다.

[참고] 클래스가 처음 로딩될 때 클래스변수들이 자동적으로 메모리에 만들어지고, 곧바로 클래스 초기화블럭이 클래스변수들을 초기화하게 되는 것이다.

 

3) 멤버변수의 초기화 시기와 순서

- 클래스변수의 초기화시점    : 클래스가 처음 로딩될 때 단 한번 초기화 된다.

- 인스턴스변수의 초기화시점 : 인스턴스가 생성될 때마다 각 인스턴스별로 초기화가 이루어진다.

 

- 클래스변수의 초기화순서    : 기본값 -> 명시적초기화 -> 클래스 초기화 블럭

- 인스턴스변수의 초기화순서 : 기본값 -> 명시적초기화 -> 인스턴스 초기화 블럭 -> 생성자

 

프로그램 실해도중 클래스에 대한 정보가 요구될 때, 클래스는 메모리에 로딩된다. 예를 들면, 클래스 멤버를 사용했을 때, 인스턴스를 생성할 때 등이 이에 해당된다.

하지만, 해당 클래스가 이미 메모리에 로딩되어 있다면, 또다시 로딩하지 않는다. 물론 초기화도 다시 수행하지 않는다.

[참고] 클래스의 로딩 시기는 JVM의 종류에 따라 좀 다를 수 있는데, 클래스가 필요할 때 바로 메모리에 로딩하도록 설계가 되어있는 것도 있고, 실행효율을 높이기 위해서 사용될 클래스들을 프로그램이 시작될 때 미리 로딩하도록 되어있는 것도 있다.

'Java > 기초' 카테고리의 다른 글

[Java] 객체지향 프로그래밍 4  (0) 2020.07.19
[Java] 객체지향 프로그래밍 3  (0) 2020.07.12
[Java] 객체지향 프로그래밍 1  (0) 2020.06.24
[Java] 배열(array)  (0) 2020.06.23
[Java] 형변환  (0) 2020.06.21