본문 바로가기
언어/JAVA

[클래스]

by 코딩맛집 2023. 1. 26.

6.3 클래스 선언

클래스 이름을 정했다면 "클래스이름.java"로 소스 파일을 생성해야 한다. 소스 파일 이름도 대소문자를 구분하므로 반드시 클래스 이름과 대소문자가 같도록 해야 한다. 소스 파일을 생성했다면 소스 파일을 열고 다음과 같이 클래스를 선언해준다.

public class 클래스이름{
}

일반적으로 소스 파일당 하나의 클래스를 선언한다. 하지만, 두 개 이상의 클래스 선언도 가능한다.

public class Car{
}

class Tire{
}

두 개 이상의 클래스가 선언된 소스 파일을 컴파일하면 바이트 코드 파일은 클래스를 선언한 개수만큼 생긴다.

소스 파일은 클래스 선언을 담고 있는 저장 단위일 뿐, 클래스 자체가 아니다. 상기 코드를 컴파일하면 Car.class와 Tire.class가 각각 생성된다.

하나의 소스 파일에 복수 개의 클래스를 선언할 때 주의할 점은 소스 파일 이름과 동일한 클래스만 public 클래스로 선언할 수 있다. 그렇지 않으면 컴파일 에러가 발생한다. 가급적이면 소스 파일 하나당 동일한 이름의 클래스 하나를 선언하는 것이 좋다.

 

6.4 객체 생성과 클래스 변수

클래스를 선언한 다음, 컴파일을 했다면 객체를 생성할 설계도가 만들어진 셈이다. 클래스로부터 객체를 생성하는 방법은 다음과 같이 new 연산자를 사용하면 된다.

new 클래스();

 new 연산자 뒤에는 생성자가 온다. 생성자는 클래스() 형태를 가지고 있다. new 연산자로 생성된 객체는 메모리 힙(heap) 영역에 생성되어 객체의 주소를 리턴해준다. 위치를 모르면 객체를 사용할 수 없다. 이 주소를 참조 타입인 클래스 변수에 저장해 두면, 변수를 통해 객체를 사용할 수 있다. 

클래스 변수 = new 클래스();

클래스 선언부터 생성까지의 예제

# Student.java

public class Student{
}
#StudentExample.java

public class StudentExample{
	public static void main(String[] args){
    	Student s1 = new Student();
        System.out.println("s1 변수가 Student 객체를 참조합니다.");
        
        Student s2 = new Student();
        System.out.println("s2 변수가 또 다른 Student 객체를 참조합니다.");
    }
}

예제가 실행되면 메모리에 클래스 변수와 객체가 생성된다. Student 클래스는 하나지만 new 연산자를 사용한 만큼 객체가 메모리에 생성된다. 같은 클래스에서 생성되었지만 각각의 student 객체는 자신만의 고유 데이터를 가지면서 메모리에서 활동하게 된다. s1과 s2가 참조하는 Student 객체는 완전히 독립된 서로 다른 객체이다.

 

클래스의 두 가지 용도

  • 라이브러리 클래스 - 실행할 수 없으며 다른 클래스에서 이용하는 클래스
  • 실행 클래스 - main() 메소드를 가지고 있는 실행 가능한 클래스

일반적으로 자바 프로그램은 하나의 실행 클래스와 여러 개의 라이브러리 클래스들로 구성된다. 실행 클래스는 실행하면서 라이브러리 클래스를 내부에서 이용한다.

 

6.5 클래스의 구성 멤버

생성자: new 연산자로 객체를 생성할 때 객체의 초기화 역할을 담당한다. 선언 형태는 메소드와 비슷하지만, 리턴 타입이 없고 이름은 클래스 이름과 동일하다.

 

필드: 객체의 데이터를 저장하는 역할을 한다. 선언 형태는 변수 선언과 비슷하지만 쓰임새는 다르다. 

 

메소드: 객체가 수행할 동작이다. 다른 프로그램 언어에서는 함수라고 하기도 하는데, 객체 내부의 함수는 메소드라고 부른다. 메소드는 객체와 객체간의 상호 작용을 위해 호출된다.

 

다음은 각 클래스 구성 멤버의 선언 형태이다.

6.6 필드 선언과 사용

필드 객체의 데이터를 저장하는 역할을 한다. 객체의 데이터에는 고유 데이터, 현재 상태 데이터, 부품 데이터가 있다.

 

필드 선언

반드시 클래스 블록에서 선언되어야만 필드 선언이 된다. 필드명은 첫 문자를 소문자로 하되, 캐멀 스타일로 작성하는 것이 관례이다. 초기값을 제공하지 않을 경우 필드는 객체 생성 시 자동으로 기본값으로 초기화된다.

타입 필드명 { = 초기값} ;

 

필드와 (로컬)변수의 차이점

구분 필드   (로컬)변수  
선언 위치 클래스 선언 블록 생성자, 메소드 선언 블록
존재 위치 객체 내부에 존재 생성자, 메소드 호출 시에만 존재
사용 위치 객체 내/외부 어디든 사용 생성자, 메소드 블록 내부에서만 사용

 

필드 사용

필드를 사용한다는 것은 필드값을 읽고 변경하는 것을 말한다. 클래스로부터 객체가 생성된 후에 필드를 사용할 수 있다. 

객체 내부에서는 단순히 필드명으로 읽고 변경할 수 있지만 외부 객체에서는 참조 변수와 도트 연산자를 이용해서 필드를 읽고 변경해야 한다. 

 

6.7 생성자 선언과 호출

new 연산자는 객체를 생성한 후 연이어 생성자를 호출해서 객체를 초기화하는 역할을 한다. 객체 초기화란 필드 초기화를 하거나 메소드를 호출해서 객체를 사용할 준비를 하는 것을 말한다.

 

기본 생성자

모든 클래스는 생성자가 존재하며, 하나 이상을 가질 수 있다.클래스에 생성자 선언이 없으면 컴파일러는 다음과 같은 기본 생성자를 바이트코드 파일에 자동으로 추가시킨다.

[public] 클래스() { }

클래스가 public class로 선언되면 기본 생성자도 public이 붙지만, 클래스가 public 없이 class로만 선언되면 기본 생성자에도 public이 붙지 않는다.

 

생성자 선언

개발자가 생성자를 선언하는 이유는 객체를 다양하게 초기화하기 위해서이다. 생성자는 메소드와 비슷한 모양을 가지고 있으나, 리턴 타입이 없고 클래스 이름과 동일하다. 매개 변수는 new 연산자로 생성자를 호출할 때 매개값을 생성자 블록 내부로 전달하는 역할을 한다. 매개변수 순서대로 타입에 맞게 매개값을 대입해야 올바른 값으로 생성자가 선언된다.

 

필드 초기화

객체마다 동일한 값을 갖고 있다면 필드 선언 시 초기값을 대입하는 것이 좋고, 객체마다 다른 값을 가져야 한다면 생성자에서 필드를 초기화하는 것이 좋다. 매개변수의 이름이 너무 짧으면 코드 가독성이 좋지 않기 때문에 가능하면 초기화시킬 필드명과 동일한 이름을 사용하는 것이 좋다.

#생성자 선언

public Korean (String name, String ssn){
    this.name = name;
    this.ssn = ssn;
    }

위와 같은 경우에는 매개변수명이 필드명과 동일하기 때문에 필드임을 구분하기 위해 this 키워드를 필드명 앞에 붙여주었다. this는 현재 객체를 말하며, this.name은 현재 객체의 데이터로서의 name을 뜻한다.

 

생성자 오버로딩

매개값으로 객체의 필드를 다양하게 초기화하려면 생성자 오버로딩이 필요하다. 생성자 오버로딩이란 매개변수를 달리하는 생성자를 여러 개 선언하는 것을 말한다. 다음은 Car 클래스에서 생성자를 오버로딩한 예이다.

public class Car {
    Car() {}
    Car(String model) {}
    Car(String model, String color) {}
    Car(String model, String color, int maxSpeed) {}
}

 

다른 생성자 호출

생성자 오버로딩이 많아질 경우 생성자 간의 중복된 코드가 발생할 수 있다. 매개변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서 이러한 중복 코드를 많이 볼 수 있다.

이 경우에는 공통 코드를 한 생성자에만 집중적으로 작성하고, 나머지 생성자는 this()를 사용하여 공통 코드를 가지고 있는 생성자를 호출하는 방법으로 개선할 수 있다.

this()는 생성자의 첫 줄에 작성되며 다른 생성자를 호출하는 역할을 한다. 호출하고 싶은 생성자의 매개변수에 맞게 매개값을 제공하면 된다. this() 다음에는 추가적인 실행문을 작성할 수 있는데, 호출되는 생성자의 실행이 끝나면 원래 생성자로 돌아와서 다음 실행문을 실행한다.

 

6.8 메소드 선언과 호출

메소드 선언은 객체의 동작을 실행 블록으로 정의하는 것을 말하고, 메소드 호출은 실행 블록을 실제로 실행하는 것을 말한다. 

 

메소드 선언

리턴 타입은 메소드가 실행한 후 호출한 곳으로 전달하는 결과값의 타입을 말한다. 리턴값이 없는 메소드는 void로 작성해야 한다. 리턴 타입이 있는 메소드는 실행 블록 안에서 return 문으로 리턴값을 반드시 지정해야 한다.

vodi powerOn() {}
double divide(int x, int y) {}

메소드명은 첫 문자를 소문자로 시작하고, 캐멀 스타일로 작성한다. 전달할 매개값이 없다면 매개변수는 생략할 수 있다.

 

메소드 호출

메소드는 객체의 동작이므로 객체가 존재하지 않으면 메소드를 호출할 수 없다. 클래스로부터 객체가 생성된 후에 메소드는 생성자와 다른 메소드 내부에서 호출될 수 있고, 객체 외부에서도 호출될 수 있다. 

객체 내부에서는 단순히 메소드명으로 호출하면 되지만, 외부 객체에서는 참조 변수와 도트(.) 연산자를 이용해서 호출한다. 또한 메소드가 매개변수를 가지고 있을 때는 호출할 때 매개변수의 타입과 수에 맞게 매개값을 제공해야 한다.

메소드가 리턴값이 있을 경우에는 대입 연산자를 사용해서 리턴값을 변수에 저장할 수 있다. 이때 변수 타입은 메소드의 리턴 타입과 동일하거나 자동 타입 변환될 수 있어야 한다.

 

가변길이 매개변수

메소드가 가변길이 매개변수를 가지고 있다면 매개변수의 개수와 상관없이 매개값을 줄 수 있다. 매개값들은 자동으로 배열 항목으로 변환되어 매소드에서 사용된다. 그렇기 때문에 메소드 호출 시 직접 배열을 매개값으로 제공해도 된다.

int sum(int ... values){ }

int[] values = {1,2,3};
int result = sum(values);

또는

int result = sum(new int[] {1,2,3});

 

return 문

메소드의 실행을 강제 종료하고 호출한 곳으로 돌아간다라는 의미이다. 메소드 선언에 리턴 타입이 있을 경우에는 return  문 뒤에 리턴값을 추가로 지정해야 한다.

return문 이후에 실행문을 작성하면 'Unreachable code'라는 컴파일 에러가 발생한다. 왜냐하면 return 문 이후의 실행문을 결코 실행되지 않기 때문이다.

 

메소드 오버로딩

메소드 이름은 같되 매개변수의 타입, 개수, 순서가 다른 메소드를 여러개 선언하는 것을 말한다. 메소드 오버로딩의 목적은 다양한 매개값을 처리하기 위해서이다.

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

double plus(double x, double y) {
    double result = x + y;
    return result;
}

메소드 오버로딩의 대표적인 예는 콘솔에 출력하는 System.out.println() 메소드로, 호출할 때 주어진 매개값의 타입에 따라서 오버로딩된 println() 메소드 중 하나를 실행한다.