[Java] 클래스

JM·2022년 8월 1일
1

Java_Live_Study

목록 보기
5/15
post-thumbnail

학습할 것

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

클래스의 구성요소

클래스는 객체의 상태를 나타내는 필드(field)와 객체의 행동을 나타내는 메소드(method), 그리고 생성자(constructor)로 구성된다.

  • 필드 : 클래스 내부에 정의된 변수
  • 메소드 : 특정 작업을 수행하는 명령문의 집합
  • 생성자 : 객체를 초기화하기 위한 함수

접근 제어자(access modifier)

접근 제어자는 클래스, 필드, 메소드에 대한 접근 권한을 제한하기 위해 사용되는 문법이다.

  • public : 접근 제한이 없다.
  • protected : 동일한 패키지에 존재하거나, 상속받았을 경우 접근 가능하다.
  • default : 접근 지정자를 명시하지 않는 경우이며, 동일한 패키지에서만 접근 가능하다.
  • private : 자기 자신의 클래스에서만 접근 가능하다.
접근 제어자를 통해 코딩의 실수를 줄일 수 있다.

예를 들어 클래스 내부의 필드값을 오직 클래스의 메소드를 통해서만 수정가능해야 한다고 하자.

public class Car {
    public void raisePrice() {
        price++;
    }

    private int price = 0;
}
public class Seller {
    private Car car = new Car();

    private void raisePrice(int price){
        car.raisePrice(); // 1 : OK
        car.price = 1000 // 2 : Error
    }
}

위 경우 1은 메소드를 통해 필드값을 수정하지만, 2는 직접 필드에 접근하여 값을 수정하려고 한다.
private이기 때문에 클래스의 외부에서 필드값을 바꿀 수 없다. 이와같이 제한을 두어 실수를 방지한다.

클래스의 상속관계와 기본 메소드

자바의 모든 클래스는 class Class를 상속하고 있으며 class Classclass Object를 상속하고 있다.
임의의 클래스 > Class 클래스 > Object 클래스
이 떄문에 모든 클래스는 다음과 같은 메소드를 기본으로 가지고 있다.

  • boolean equals(Object obj) : 래퍼런스 변수가 동일한 객체를 가지고 있는 지 알려준다.
  • int hashCode() : 객체가 가지고 있는 해시코드를 리턴한다. 해시코드는 다른 객체들과 구분되는 고유정보이다.
  • String toString() : 클래스의 이름과 객체의 해시코드를 String으로 리턴한다.
  • void notify() : 해당 객체를 wait하고 있는 thread를 한 놈만 깨운다.
  • Class<?> getClass() : 런타임에 해당 래퍼런스 변수가 가리키는 객체가 누구의 인스턴스인지 리턴한다.
  • void notifyAll() : 해당 객체를 wait하고 있는 모든 thread를 깨운다.
  • void wait() : 다른 thread가 깨울 때까지 해당 thread는 wait 상태로 남는다.

클래스 정의하는 방법

<접근 제어자> class class_name{
    // 필드
    // 메소드
    // 생성자
}
  • 접근 제어자 : class를 정의할 때 오직 publicdefault access만 사용할 수 있다.
  • Class 이름 : 반드시 대문자로 시작해야 한다.
  • Superclass : 부모 클래스는 extends 키워드를 통해 표현한다. 클래스는 오직 하나의 부모 클래스만
    상속받을 수 있다. 즉, 다중 상속이 허용되지 않는다.
  • Interfaces : 인터페이스는 implements 키워드를 통해 나타내며, 여러 인터페이스를 구현할 수 있다.

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

TestClass testClass = new TestClass()
1. 선언 : TestClass testClass에서 객체를 담을 변수의 이름과 객체의 타입을 지정한다.
2. 인스턴스화 : new 키워드는 객체를 생성할 때 사용하는 연산자이다.
3. 초기화 : new 키워드 다음에 객체를 초기화하는 생성자가 호출된다.

선언하는 단계는 단순히 객체의 래퍼런스를 담을 변수를 지정하는 단계이다. 따라서 초기화하지 않으면,
해당 변수는 아무것도 가리키지 않는 상태이다.

new 연산자는 새로운 객체를 메모리에 할당함으로서 클래스를 인스턴스화하며, 해당 메모리를 가리키는
래퍼런스를 반환한다. 또한 new연산자는 객체의 생성자를 호출한다.
new 연산자를 통해 반환된 래퍼런스는 보통 래퍼런스 변수에 할당되지만, 변수에 할당하지 않고 객체를
바로 사용할 수도 있다.
ex) int height = new Rectangle().height;

객체 생성 바이트코드

TestClass.java

public class TestClass {
    private int a;
    private int b;
    public TestClass(int a, int b){
        this.a = a;
        this.b = b;
    }

    public TestClass(int a){
        this.a = a;
        this.b = a;
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        TestClass testClass1 = new TestClass(10,20);
        TestClass testClass2 = new TestClass(30);
    }
}

Main.class

'''
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 3 L0
    NEW TestClass
    DUP
    BIPUSH 10
    BIPUSH 20
    INVOKESPECIAL TestClass.<init> (II)V
    ASTORE 1
   L1
'''

위 코드는 new 연산자를 통해 객체를 생성하는 과정 중 핵심적인 부분만을 발췌한 것이다.
명령어를 기준으로 설명하면 다음과 같다.
1. NEW TestClass : constant pool에 있는 TestClass의 인스턴스를 생성하고 해당 객체의 주소를 stack에 push한다.
2. DUP : stack의 top에 있는 데이터를 복사하여 stack에 push한다. 이 경우 객체의 주소가 복사된다.
3. BIPUSH 10 : 10을 stack에 push한다.
4. BIPUSH 20 : 20을 stack에 push한다.
5. INVOIKESPECIAL TestClass.<init> (II)V : stack에서 2번 pop하여 생성자의 argument로 가져올 값들을 가져오고 1번 더 pop하여 생성자를 호출하기 위한 객체 래퍼런스를 가져온다. 그리고 생성자를 호출한다.
6. ASTORE 1 : stack에서 pop하여 Local variable array의 1번 자리에 저장한다. 이 경우 TestClass의 인스턴스의 래퍼런스가 저장된다.

다시 정리하자면 new 키워드는 메모리를 할당하여 인스턴스를 생성하고 해당 객체에 대한 초기화는 생성자 함수를 통해 이뤄진다.

메소드 정의하는 방법

public int calculateOperator(int a, int b){
    // do the calculation here
}

메소드 정의의 요소는 6가지이며 다음과 같다.
1. Modifiers : public,private,protected와 같은 접근 지정자가 메소드 정의 가장 앞 단에 있다.
2. The return type : 접근 지정자 다음에 반환 값의 type을 지정한다.
3. The method name : 메소드 이름은 반드시 동사로 시작해야 하며 camelCase를 따른다.
4. The parameter list in parenthesis : ,로 구분되는 파라미터 리스트
5. An exception list : throws 키워드를 사용하여 예외를 날릴 예외 리스트들
6. The method body : {}안에 감싸진 영역이며, 내부 로컬 변수가 정의될 수 있다.

메소드 오버로딩

public class OverloadingClass(){
    public void overloadFunc(int i){

    }

    public void overloadFunc(String s){

    }
}

메소드는 이름이 동일하더라도 파라미터 리스트가 다르다면 동일한 이름의 메소드를 정의할 수 있다.
주의할 점은, return type은 메소드 오버로딩에서 신경쓰는 부분이 아니라는 것이다.
즉, 파라미터 리스트가 동일하고 return type이 다른 경우 compilation error가 발생한다.

클래스의 메소드가 정의되면 해당 메소드는 JVM에서 어느 영역에 존재할까?

JVM의 method area에 저장된다. 정확히 서술하자면, method를 만났을 때 해당 메소드의 클래스가 method area
에 있으면 거기서 method를 가져오고, 그렇지 않다면 클래스 파일의 바이트 코드를 method area에 로드 한 뒤
메소드를 가져온다.
method area에는 클래스의 메타 데이터가 저장된다. 이때 method에 대한 정보가 함께 저장된다. 따라서 메소드
를 정의한 뒤 해당 메소드를 호출할 때 method area에서 메소드를 불러올 것이다.

생성자 정의하는 방법

java compiler는 기본적으로 class의 생성자가 없으면 no argument인 default 생성자를 만들어 준다. 만약,
개발자가 정의한 생성자가 있다면, default 생성자는 생성되지 않는다. 그런데 default 생성자를 사용할 때
주의할 점이 있다. default 생성자는 superclass의 no argument 생성자를 호출한다. 그런데 superclass가
no argument 생성자를 가지고 있지 않다면 문제가 발생할 것이다. 따라서 default 생성자 사용시 이를 신경
써야 한다.

생성자 예시

public Bicycle(int startCadence, int startSpeed, int startGear){
    gear = startGear;
    cadence = startCadence;
    speed = startSpeed;
}

생성자는 메소드와 유사해 보이지만, return type이 없고 생성자 이름이 클래스 이름과 동일하다는 점에서
메소드와는 다르다. 생성자는 함수 오버로딩과 유사하게 파라미터 리스트가 다른 여러 생성자를 정의할 수
있다. 여기서 파라미터 리스트는 파라미터의 개수와, type의 개수를 의미한다. 이 요소들이 일치하지 않으면
여러 생성자를 정의할 수 있다.

private 생성자 용도

private 생성자는 인스턴스 생성이 무의미하거나, 싱글톤 패턴을 사용할 때 사용한다.

  • 인스턴스 생성이 무의미한 경우
public class SubClass {
    private SubClass(){}

    public static void printA(){
        System.out.println("hello world");
    }
    public static void printB(){System.out.println("hello world");}
}

위 경우 메소드가 모두 static이기 때문에 클래스의 인스턴스를 생성하는 것이 무의미하다.

  • 싱글톤 패턴
public class SubClass {
    private static SubClass subClass = new SubClass();

    private SubClass(){}

    public static SubClass getInstance(){
        if(subClass == null){
            subClass = new SubClass();
            return subClass;
        }
        else{
            return subClass;
        }
    }

    public void printA(){
        System.out.println("hello world");
    }
}

생성자에 private으로 접근 지정자를 지정하여 외부에서 객체를 생성하지 못하도록 막는다.
그리고 오직 getInstance()메소드를 통해서만 객체를 불러올 수 있도록 한다.

this 키워드 이해하기

this는 현재 오브젝트를 가리키는 reference 변수이다.
this는 6개의 사용 경우가 있다.

1. 현재 클래스 인스턴스를 가리키기 위한 용도

argument로 받는 변수명이 동일한 경우, 인스턴스의 변수와 argument를 구분하기 위해 쓰인다.

public class SubClass {
    private int a;
    private int b;

    public SubClass(int a, int b){
        a = a;
        b = b;
    }

    public void print(){
        System.out.println("a : "+ a + " b : "+ b); // a : 0 b : 0
    }
}

위 경우 인스턴스의 변수인 a,b는 동일한 이름을 가지고 있는 argument에 의해 가려지게 된다.
따라서 this 키워드를 사용하여 구분해야 한다. 만약, argument의 이름을 다른 것으로 지정하면
this를 굳이 사용하지 않아도 된다. 그러나 this를 사용하여 argument와 인스턴스의 변수명을
일치시킴으로써 코드의 의미를 명확하게 나타낼 수 있다.

2. 현재 클래스의 메소드를 호출하기 위한 용도

현재 클래스 메소드 내부에서 현재 클래스의 메소드를 사용하면 compiler는 자동으로 해당 메소드의 앞에
this.을 붙인다.
컴파일 전

public class SubClass {
    void m(){}
    void n(){
        m();
    }
}

컴파일 후

public class SubClass {
    public SubClass() {
    }

    void m() {
    }

    void n() {
        this.m();
    }
}

3. 현재 클래스의 생성자를 호출하기 위한 용도

this()는 현재 클래스의 default 생성자를 호출한다. 이는 파라미터를 갖는 생성자에서
기본 생성자를 재사용할 때 사용된다. 반대로 기본 생성자에서 파라미터를 갖는 생성자를 호출할
수도 있다. this()는 반드시 생성자의 가장 첫번째 줄에서 호출되어야 한다.

public class SubClass {
    private int a;
    private int b;
    private int c;

    public SubClass(int a, int b, int c){
        this(a,b);
        this.c = c;
    }

    public SubClass(int a, int b){
        this.a = a;
        this.b = b;
    }
}

4. 메소드의 argument로 넘겨지기 위한 용도

public class SubClass {
   public void m(SubClass obj){
       System.out.println(obj);
   }

   public void n(){
       m(this);
   }
}

5. 생성자 호출에서 argument로 넘겨지기 위한 용도

여러 클래스에서 하나의 객체를 함께 사용할 때 유용하다. 아래 코드에서 A4 클래스는 생성자에서
B 클래스의 인스턴스를 생성하고 생성자로 자기 자신을 넘겨주고 있다.
이러한 방식으로 A4의 인스턴스를 다른 클래스 생성자의 argument로 넘겨줘, 여러 클래스에서
하나의 객체를 공유하도록 할 수 있다.

class B{  
  A4 obj;  
  B(A4 obj){  
    this.obj=obj;  
  }  
  void display(){  
    System.out.println(obj.data);//using data member of A4 class  
  }  
}  
  
class A4{  
  int data=10;  
  A4(){  
   B b=new B(this);  
   b.display();  
  }  
  public static void main(String args[]){  
   A4 a=new A4();  
  }  
} 

6. 현재 클래스 인스턴스를 반환하기 위한 용도

public class SubClass {
    SubClass getSubClass(){
        return this;
    }

    void print(){
        System.out.println("SubClass print() method");
    }
}

과제1

int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.

int value, Node left, right를 가지고 있어야 합니다.

Node.java

public class Node {
    private int value;
    private Node left;
    private Node right;
    Node(int value){
        this.value = value;
        left = null;
        right = null;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public Node getLeft() {
        return left;
    }

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

    public Node getRight() {
        return right;
    }

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

과제2

BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.

DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.

BinaryTree.java

import java.util.*;

public class BinaryTree {
    Node root;

    BinaryTree(Node root){
        this.root = root;
    }

    List<Integer> bfs(Node node){
        List<Integer> result = new ArrayList<Integer>();
        Queue<Node> queue = new LinkedList<>();
        queue.add(node);

        while(!queue.isEmpty()){
            Node currNode = queue.poll();
            result.add(currNode.getValue());
            if(currNode.getLeft() != null){
                queue.add(currNode.getLeft());
            }
            if(currNode.getRight() != null){
                queue.add(currNode.getRight());
            }
        }
        return result;
    }

    List<Integer> dfs(Node node){
        List<Integer> result = new ArrayList<Integer>();
        Stack<Node> stack = new Stack<>();
        stack.push(node);

        while(!stack.isEmpty()){
            Node currNode = stack.pop();
            result.add(currNode.getValue());
            if(currNode.getRight() != null){
                stack.push(currNode.getRight());
            }
            if(currNode.getLeft() != null){
                stack.push(currNode.getLeft());
            }
        }
        return result;
    }
}

BinaryTreeTest.java

import org.junit.jupiter.api.Assertions;
import java.util.List;

class BinaryTreeTest {

    @org.junit.jupiter.api.Test
    void bfs() {
        Node[] nodeList = new Node[10];
        int[] expectedList = new int[10];

        for(int i = 0; i < 10; i++){
            nodeList[i] = new Node(i);
            expectedList[i] = i;
        }
        nodeList[0].setLeft(nodeList[1]);
        nodeList[0].setRight(nodeList[2]);
        nodeList[1].setLeft(nodeList[3]);
        nodeList[1].setRight(nodeList[4]);
        nodeList[2].setLeft(nodeList[5]);
        nodeList[2].setRight(nodeList[6]);
        nodeList[3].setLeft(nodeList[7]);
        nodeList[3].setRight(nodeList[8]);
        nodeList[4].setLeft(nodeList[9]);

        BinaryTree bTree = new BinaryTree(nodeList[0]);
        List<Integer> result = bTree.bfs(nodeList[0]);

        for(int i = 0; i < 10; i++){
            Assertions.assertEquals(expectedList[i],result.get(i));
        }

    }

    @org.junit.jupiter.api.Test
    void dfs() {
        Node[] nodeList = new Node[10];
        int[] expectedList = new int[]{0,1,3,7,8,4,9,2,5,6};

        for(int i = 0; i < 10; i++){
            nodeList[i] = new Node(i);
        }
        nodeList[0].setLeft(nodeList[1]);
        nodeList[0].setRight(nodeList[2]);
        nodeList[1].setLeft(nodeList[3]);
        nodeList[1].setRight(nodeList[4]);
        nodeList[2].setLeft(nodeList[5]);
        nodeList[2].setRight(nodeList[6]);
        nodeList[3].setLeft(nodeList[7]);
        nodeList[3].setRight(nodeList[8]);
        nodeList[4].setLeft(nodeList[9]);

        BinaryTree bTree = new BinaryTree(nodeList[0]);
        List<Integer> result = bTree.dfs(nodeList[0]);
        for(int i = 0; i < 10; i++){
            Assertions.assertEquals(expectedList[i],result.get(i));
        }
    }
}

참고자료

접근 지정자 : https://mainia.tistory.com/5574
클래스 정의 : https://www.javatpoint.com/class-definition-in-java
오라클 자바 튜토리얼 : https://docs.oracle.com/javase/tutorial/java/javaOO/classdecl.html
invokespecial : https://www.jrebel.com/blog/using-objects-and-calling-methods-in-java-bytecode
클래스 메소드 로딩 : https://blog.wanzargen.me/16
this keyword : https://www.javatpoint.com/this-keyword

profile
블로그 이전 : https://blog.naver.com/tjsqls2067

0개의 댓글