본문 바로가기
자바/스터디

[자바 스터디] 5주차 과제 - 클래스

by jeonghaemin 2020. 12. 17.
728x90

본 게시글은 백기선 님의 live-study 과제를 수행하면서 작성한 글입니다.

목표

자바의 Class에 대해 학습하세요

학습할 것(필수)

  • 클래스 정의하는 방법
  • 객체 만드는 방법 (new 키워드 이해하기)
  • 메소드 정의하는 방법
  • 생성자 정의하는 방법
  • this 키워드 이해하기

클래스

클래스란 '객체 생성을 위한 설계도'라고 할 수 있다.

예를 들어 TV가 있다면 실제 TV 제품은 객체이고 이 TV의 설계도를 클래스라고 할 수 있다.

클래스 구성요소

클래스는 멤버 변수와 메서드로 구성되어 있다.

다시 한번 TV로 예를 들어보면 TV의 크기, 길이, 색상, 볼륨, 채널 등과 같은 상태는 멤버 변수라고 할 수 있고, 티비를 켜거나 끄고, 볼륨을 조절하고, 채널을 변경하는 행동은 메서드라고 할 수 있다.

클래스 정의

public class TV {

    // --------- 멤버 변수 ---------
    private String color;
    private int volume;
    private int channel;

      // --------- 메서드 ---------

      // 채널 올리기
      public void channelUp() { 
        this.channel++;
    }

    // 채널 내리기
    public void channelDown() {
        this.channel--;
    }

      // 볼륨 올리기
    public void volumeUp() {
        this.volume++;
    }

      // 볼륨 내리기
    public void volumeDown() {
        this.volume--;
    }
}

접근 제어자

위 코드를 보면 메서드와 멤버 변수 앞에 public, private 키워드가 붙어 있는 것을 볼 수 있으며 어떤 것을 사용하느냐에 따라 해당 메서드나 변수에 접근할 수 있는 권한을 제한할 수 있다.

  • public : 접근 제한이 없다.
  • default(아무 것도 붙이지 않음) : 같은 패키지 내에서만 접근 가능하다.
  • protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근 가능하다.
  • private : 같은 클래스 내에서만 접근 가능하다.

변수

변수 종류

멤버 변수의 경우 선언 위치와 static 키워드 유무에 따라 구분할 수 있다.

public class Sample {
  int iv; // 인스턴스 변수
  static int cv = 0; // 클래스 변수

  void method() {
    int lv = 0; // 지역 변수
  }
}
  • 멤버 변수
    • 인스턴스 변수 : 객체를 생성할 때 만들어지기 때문에 객체를 만들어야 사용할 수 있고, 객체는 서로 독립적인 메모리 공간을 가지므로 객체마다 값이 다를 수 있다.
    • 클래스 변수
      • static 키워드를 붙혀서 사용할 수 있으며, 인스턴스 변수와 다르게 클래스 변수는 모든 객체와 변수를 공유하여 사용하기 때문에 모든 객체에서 같은 값을 가지고, 객체를 생성하지 않아도 사용할 수 있다.
      • '객체명.변수명'으로 접근하는 인스턴스 변수와 달리 '클래스명.변수명'으로 값에 접근할 수 있다.
      • 클래스 메모리에 로딩될 때 생성되어 프로그램이 종료될 때까지 유지된다.
      • 클래스는 참조 변수의 선언이나 객체의 생성과 같이 클래스의 정보가 필요할 때 메모리에 로딩된다.
  • 지역 변수 : 메서드 내에 선언되어, 메서드 안에서만 사용이 가능하며, 메서드가 종료되면 소멸되어 사용할 수 없다.

변수 초기화

멤버 변수(클래스 변수, 인스턴스 변수)는 초기화하지 않으면 변수의 자료형에 맞는 기본값으로 초기화된다.

자료형 기본값
boolean false
char '\u0000'
byte, short, int 0
long 0L
float 0.0f
double 0.0d 또는 0.0
참조형 null

지역 변수는 반드시 사용 전에 초기화해야 한다.

변수 초기화 시점
  • 클래스 변수 : 클래스가 처음 로딩될 때 최초 한번 초기화된다.
  • 인스턴스 변수 : 인스턴스가 생성될 때마다 각 인스턴스 별로 초기화가 된다.
변수 초기화 순서
  • 클래스 변수 : 기본값 -> 명시적 초기화 -> 클래스 초기화 블럭
  • 인스턴스 변수 : 기본값 -> 명시적 초기화 -> 인스턴스 초기화 블럭 -> 생성자

초기화 블럭

생성자 외에 초기화 블럭을 이용해서도 인스턴스를 초기화할 수 있다.

{
    // 인스턴스 초기화 블럭
}

static {
    // 클래스 초기화 블럭
}

초기화 블럭은 위 코드와 같이 {}안에 코드를 작성해 주기만하면되며 클래스 초기화 블럭은 앞에 static을 붙혀 주기만 하면 된다.

클래스 초기화 블럭은 클래스가 메모리에 처음 로딩될 때 한번만 실행되고, 인스턴스 초기화 블럭은 인스턴스를 만들때마다 생성자가 실행되기 전에 실행된다.

public class Sample {

    public static void main(String[] args) {
        Sample s1 = new Sample();
        Sample s2 = new Sample();
    }

    {
        System.out.println("인스턴스 초기화 블럭");
    }

    static {
        System.out.println("클래스 초기화 블럭");
    }

    public Sample() {
        System.out.println("생성자");
    }
}

실행 결과 클래스 초기화 블럭은 최초 1회만 실행, 인스턴스 초기화 블럭은 인스턴스 생성 시마다 생성자가 실행되기 전에 실행되는 것을 볼 수 있다.

객체 만드는 방법 (new 키워드 이해하기)

클래스를 선언하는 것은 설계도를 만드는 것이기 때문에 실제 제품인 객체를 만들어야 한다. 클래스로부터 객체를 만드는 방법은 일반적으로 new 키워드를 사용한다.

class TV {
  //.. tv class code
}

class TvTest {
    public static void main(String[] args) {
          TV tv1 = new TV();  
      tv.volumeDown();
      tv.channelUp();

      Tv tv2 = new TV();

      }
}

new 연산자에 의해 객체가 메모리 공간에 할당되고 대입 연산자에 의해 객체의 주소 값이 참조 변수 tv에 저장되며, 이 참조 변수를 이용하여 객체에 접근할 수 있다.

또한 그림에서 볼 수 있듯이 객체마다 독립적인 메모리 공간을 가지고 있다.

객체를 인스턴스라고 부르기도 하는데 크게 보면 객체와 인스턴스는 같은 말이라고 봐도 무방하다. 하지만 객체는 모든 인스턴스를 포함하는 포괄적인 의미를 갖고 있으며, 인스턴스는 어떤 특정 클래스로부터 만들어진 것인지를 강조하는 보다 구체적인 의미를 가지고 있다.

메서드 정의하는 방법

메서드는 특정 작업을 수행하는 일련의 코드들을 하나로 묶은 것이라고 할 수 있으며, 수학에서의 함수와 유사하게 입력 값과 반환 값을 가진다. 클래스 내에 선언할 수 있다.

메서드의 형태는 다음과 같으며 매개변수라고 불리는 입력 값은 원하는 만큼 여러 개를 받아올 수 있다.

여러 개의 변수를 선언할 수 있는 매개변수와 달리 반환 값은 최대 1개 만을 허용한다.

반환 값과 반환 타입은 같아야 하며 반환 값이 없을 경우는 반환 타입을 void로 적어주고 return문은 생략할 수 있으며 생략해도 컴파일러가 자동으로 추가해준다.

public class Calculator {

  int plus(int x, int y) {
      return x+y;
    }

}

간단한 예제로 두 개의 정수를 받아서 두 수의 합을 반환하는 메소드를 작성하면 위와 같이 작성할 수 있다.

static 메서드

변수에서와 마찬가지로 메서드에도 static 키워드를 붙일 수 있다. static 메서드도 static 변수처럼 객체를 생성하지 않고 '클래스이름.메서드이름(매개변수)'와 같은 형식으로 호출하여 사용할 수 있다. 객체를 생성하지 않아도 사용할 수 있기 때문에 메서드 내에서 인스턴스 변수 또는 메서드를 사용할 수 없고, static 변수, 메서드는 사용할 수 있다.

public calss Sample {
  int iv;
  static int cv = 0;

  void method1() {
    // code
  }

  static void method2() {
    iv++; // 인스턴스 변수 사용불가 X
       method(); // 인스턴스 메서드 사용불가 X
    cv++ // static 변수 사용가능 O
  }
}

메서드 오버로딩

보통 메서드의 이름은 같은 클래스 내에서 중복되면 안 되지만, 자바에서는 한 클래스 내에 메서드의 이름이 중복되더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메소드를 정의할 수 있도록 하였다. 주의할 점은 반환 타입은 메서드 오버로딩에 있어서 아무 영향을 주지 않는다.

우리가 println()이라는 메서드에 정수, 실수, 문자열 등 어떤 타입을 넣든 같은 이름의 메서드로 출력을 해주었던 것이 바로 메서드 오버로딩 떄문이다. 자바 공식 문서를 보면 println() 메서드가 여러 타입으로 오버로딩 되어있는 것을 볼 수 있다.

(출처 : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/PrintStream.html)

가변인자(varargs)

보통 메서드는 매개변수의 개수는 메서드 선언 시에 적어준 개수만큼만 사용할 수 있어 개수가 고정적이었다. 그러나 JDK 1.5부터 매개변수의 개수를 동적으로 지정할 수 있게 되었는데 이를 '가변인자'라고 한다.

매개변수 부분에 '타입 ... 변수명' 의 형식으로 사용할 수 있다.

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

JDK1.5에서 추가된 printf() 메서드를 사용할 때 서식 지정자만큼 매개변수를 전달할 수 있었던 것이 바로 가변인자 때문이다.

가변인자를 사용할 때 몇가지 주의할 점이 있다.

  1. 가변인자 매개변수는 매개변수 중 제일 마지막에 선언해야 된다. 그렇지 않으면 컴파일 에러가 발생하고 그 이유는 가변인자인지 아닌지를 구별하기 힘들기 때문이다.
void method(int ... args, int n);

다음과 같이 가변인자를 마지막 매개변수로 선언하지 않으면 다음과 같이 메서드를 사용할 때 어디까지가 가변인자 값인지 구별할 수 없게 된다.

//어디까지가 매개변수...?
method(1, 2, 3, 4);
  1. 가변인자는 내부적으로 배열을 사용하기 때문에 가변인자가 선언된 메서드를 호출할 때마다 배열이 하나씩 생성된다.
public class Sample {
    static void method(int ... args) {
        System.out.println(args.getClass().getSimpleName());
    }

    public static void main(String[] args) {
        method(1,2,3);
    }
}

다음과 같이 getSimpleName() 메서드를 이용하여 int형 가변인자 클래스 이름을 출력해보면 int형 배열이 출력된다.

생성자 정의하는 방법

생성자는 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메서드'이다. 인스턴스 변수의 초기화나 인스턴스 생성 시에 수행되어야 하는 작업을 위해서 사용된다.

생성자는 클래스와 이름이 같아야 하며 리턴 값이 없는 메서드 형태이다. 보통 메서드는 리턴 값이 없으면 void 키워드를 붙여주지만 생성자는 아무것도 붙이지 않는다.

메서드, 멤버 변수와 마찬가지로 접근 제어자를 사용할 수 있다.

class Car{
  String color;
  int door;

  // 매개변수 없는 생성자
    Car() {
    // code
  } 

  // 매개변수 있는 생성자
  public Car(String c, int d) {
    // code
  }

  private Car(String c) {
    // code
  }
} 

class Main {
  public static void main(String[] args) {
    // 생성자를 이용하지 않는 인스턴스 변수 초기화
    Car car1 = new Car();
    car1.color = "red";
    car1.door = 4;

    // 생성자를 이용한 인스턴스 변수 초기화
    Car car2 = new Car("red", 4);
  }
}

생성자는 위 코드와 같은 형태이며 메서드와 같이 매개변수를 사용할 수 있으며 오버로딩이 가능하다.

메인 함수를 보면 생성자를 이용하여 인스턴스 변수를 초기화하는 것이 코드가 더 깔끔해 보이는 것을 알 수 있다.

인스턴스 생성 과정

Car c = new Car();
  1. new 연산자에 의해 메모리에 크래스 인스턴스가 생성
  2. 생성자가 호출되어 수행
  3. new 연산자의 결과로 생성된 Car 인스턴스의 주소가 반환되어 변수 s에 저장

기본 생성자

클래스에는 반드시 하나 이상의 생성자가 있어야 한다. 하지만 클래스에 생성자가 없어도 정상적으로 컴파일이 된다. 그 이유가 기본 생성자 때문이다. 컴파일러는 클래스에 생성자가 하나도 선언되어 있지 않을 때 다음과 같이 아무 매개변수, 내용도 없는 생성자를 자동으로 추가해준다.

class Car {
    Car() {}
}

하지만 클래스에 생성자가 하나라도 있다면 기본 생성자는 추가되지 않는다.

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

this

this(), this(매개변수) 를 이용하면 생성자 내에서 해당 클래스의 다른 생성자를 호출할 수 있다.

생성자에서 다른 생성자를 호출 시 무조건 첫째 줄에서 호출해야 한다.

생성자 내의 인스턴스 변수에도 this가 사용된 것을 볼 수 있다. 여기서 this는 인스턴스 자신을 가리키는 참조변수로, 인스턴스의 주소가 저장되어 있다. 그렇기 때문에 인스턴스 변수와 생성자 매개변수의 이름이 같아도 this.color, this.door는 인스턴스변수, color와 door는 생성자 매개변수로 구분할 수 있다.

public class Sample {

    public static void main(String[] args) {
        Sample s = new Sample();
        System.out.println("s의 주소 : " + s);
    }

    public Sample() {
        System.out.println("this의 주소 : " + this);
    }
}

인스턴스를 주소와 해당 인스턴스의 this가 가리키는 주소를 출력했을 때 같은 값이 나오는 것을 볼 수 있다.

앞서 말했듯이 this는 인스턴스 자신을 가리키는 것이기 때문에 인스턴스를 생성하지 않고도 사용할 수 있는 static 메서드에서는 this를 사용할 수 없다.

과제 (Optional)

  • int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
  • int value, Node left, right를 가지고 있어야 합니다.
  • BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
  • DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.

소스 코드

Node

public class Node {

    private int data;
    private Node left;
    private Node right;

    public Node(int data) {
        this.data = data;
        this.left = null;
        this.right = null;
    }

    public int getData() { return data; }

    public Node getLeft() { return left; }

    public Node getRight() { return right; }

    public void setLeft(Node left) { this.left = left; }

    public void setRight(Node right) { this.right = right; }

}

BinaryTree

public class BinaryTree {

    public static void bfs(Node node) {
        if (node == null)
            return;

        System.out.print(node.getData() + " ");
        bfs(node.getLeft());
        bfs(node.getRight());
    }

    public static void dfs(Node node) {
        Queue<Node> queue = new LinkedList<>();
        queue.offer(node);

        while(!queue.isEmpty()) {
           Node parent =  queue.poll();

            System.out.print(parent.getData() + " ");

            if(parent.getLeft() != null) {
                queue.offer(parent.getLeft());
            }

            if (parent.getRight() != null) {
                queue.offer(parent.getRight());
            }
        }
    }
}

테스트 코드

class BinaryTreeTest {

    @Test
    @DisplayName("BFS 테스트(중위 순회)")
    void bfsTest() {
        System.out.print("BFS Test : ");
        BinaryTree.bfs(makeBinaryTree());
        System.out.println();
    }

    @Test
    @DisplayName("DFS 테스트")
    void dfsTest() {
        System.out.print("DFS Test : ");
        BinaryTree.dfs(makeBinaryTree());
        System.out.println();
    }

    Node makeBinaryTree() {
        Node parentNode = new Node(1);
        parentNode.setLeft(new Node(2));
        parentNode.setRight(new Node(3));

        parentNode.getLeft().setLeft(new Node(4));
        parentNode.getLeft().setRight(new Node(5));

        parentNode.getRight().setLeft(new Node(6));
        parentNode.getRight().setRight(new Node(7));

        return parentNode;
    }
}

참고

  • 자바의 정석 3판(남궁성 저)

댓글