프로그래밍/JAVA

<22.12.24>JAVA 공부 일지 #03. 클래스에 대해 공부해보자

mayberry 2022. 12. 24. 17:43

JAVA 공부일지 #03

 

Java는 OOP를 위해 고안된 언어이기 때문에, 모든 것을 클래스로 관리한다. 애초에 프로그램 본문도  "public class 사용자클래스파일이름"으로 시작하니까. 그 외 사용자 정의 클래스를 작성할 수 있다. 

 

public class Person {
	// ... code of Person Class
}

 

기본적으로 위와 같이 사용할 수 있다. 클래스의 껍데기를 생성했다면, 클래스 내부에서 사용할 변수를 선언할 수도 있다. 인스턴스 혹은 클래스에서 사용되는 변수를 선언하는 곳을 클래스 필드(Field)라고 한다. 

 

public class Person {
	private String name;
	private String gender;
	private int age;
}

초기값을 전달 할 수도 있으나, 추후 생성자를 통해 자동으로 초기화를 시킬 수도 있으며, new라는 선언자를 활용하여 클래스를 생성할 때, 인자를 전달하여 초기화 할 수도 있다.

JAVA의 클래스는 Heap 영역에 생성된다. Stack 영역에는 주소값만 존재하기 때문에, 객체를 생성하하고 콘솔에 출력하여도 주소값만 나올뿐, 객체의 값이 그대로 나오지 않는다. 

 

 

1. 생성자(Constructor)


클래스를 통해 인스턴스를 생성하려면, new라는 키워드와 함께 생성자를 작성하여야 한다. 생성자의 형태는 아래와 같다.

 

public className(parameter) { Constructor Logic; return; }

 

className은 실제로 생성한 class의 이름과 반드시 동일해야한다. 또한 매개변수를 줄 수 있어 클래스 필드 혹은 멤버필드의 값을 할당할 수 있다.

생성자의 역할은 객체 생성시점에 특정 행위/로직을 수행하도록 한다. 또한 객체 생성 시 생성자는 단 한 번만 호출된다. 한 번 생성된 객체에 대해서는 생성자를 두 번 호출 할 수 없다는 의미다.

개발자가 생성자를 지정하지 않을 경우 컴파일 단계에서 자동으로 디폴트 생성자를 추가해준다. 만약 생성자를 작성할 경우, 디폴트 생성자는 추가되지 않는다.

위에서 작성한 Person 클래스의 인스턴스를 생성하는 코드를 보자.

 

public class Practice {
	public static void main(String[] args) {
		Person A = new Person();
	}
}

위와 같이 A라는 Person 객체를 생성할 수 있다. 만일, 클래스 생성 시 특정 인자를 인스턴스 변수에 지정하고 싶다면, 클래스 선언 시 아래와 같이 작성하여야한다. 만약 클래스 내부에서 필드에 접근하고 싶다면, `this`를 사용하여야한다.

 

public class Person {
	private String name;
	private String gender;
	private int age;

	public Person(String name, String gender, int age) {
		this.name = name;
		this.gender = gender;
		this.age = age;
	}
}

생성자의 기능 중 가장 많이 활용할 수 있는 기능은 필드의 초기화/할당이다. 생성자를 작성하기 위해서는 class의 이름과 동일하게 지정하여야하며, 이를 활용한 코드는 아래와 같다.

 

public class Practice {
	public static void main(String[] args) {
		Person A = new Person("Mayberry", "Women", 28);
	}
}

인스턴스 생성 시 곧바로 인자를 전달하여 인스턴스 변수/클래스 변수에 할당할 수 있다.

생성자에 대해서도 오버로드가 가능하다. 즉, 동일한 생성자를 두고도 메소드 시그니처, 파라미터의 타입에 따라 다양한 생성자를 생성할 수 있다.

 

class Lec03 {
	public Lec03() {
		System.out.println("객체 생성!");
	}

	public Lec03(int su) {
		System.out.println("객체 생성 "+su);
	}
}

생성자 작성 시 조심해야할 것은, 생성자 내에서 재귀적인 로직의 작성은 피해야한다. 무한루프에 빠질 위험이 있기 때문이다.

생성자 오버로드 시, 다른 생성자를 활용할 수 있다.

 

class Car {
	// field
	public Car() {
		this("부가티", 400);
	}

	public Car(String name, int speed) {
		// ...logic of Constructor
	}
}

 

 

2. 메소드


클래스를 선언하면 해당 객체의 동작을 선언해야하는데, 이를 메소드(Method)라고 한다. 메소드의 선언은 아래와 같이 진행할 수 있다.

 

public class Person {
	// Field
	private String name;
	private String gender;
	private int age;

	// Constructor
	public Person(String name, String gender, int age) {
		// ...logic of Constructor
	}

	// Method
	public String getName() {
		return this.name;
	}

	public void Aging() {
		this.age++;
	}

}

메소드는 함수 생성과 동일하다. 함수(혹은 메소드)가 반환하는 자료형을 선언하고, 그 내용을 선언한다. 다만 메소드는 그 앞에 접근제어자를 작성하여야한다.

 

 

2-1.  메소드 오버로딩/메소드 오버로드


메소드 오버로딩은 같은 이름의 메소드를 중복하여 정의하는 것을 의미한다. 이에 대해 소개하기 전에, 우선 메소드 시그니처(Method Signature)를 알아야한다. 메소드 시그니처는, 메소드의 선언부에 명시되는 매개변수의 리스트를 말한다.

만약 두 메소드가 매개변수의 개수와 타입, 그 순서까지 같다면, 해당 메소드들의 시그니처는 같다고 할 수 있다.

아래와 같은 코드는 자바에서 동작하지 않는다.

 

public class Person {
	// ...Field
	// ...Constructor
	public Person(String name, String gender, int age) {
		// ...logic of Constructor
	};
	// ...method
	public void Aging() {
		this.age++;
	}

	public void Aging() {
		this.age += 2;
	}
}

`Person` 객체에서는 `Aging()`이라는 메소드가 두 개나 선언되었다. 메소드 시그니처가 같기 때문에 자바 컴파일러는 Aging() 이라는 메소드를 호출하여도 어느 것을 실행해야 할 지 모를것이다. 하지만 아래와 같은 코드는 가능하다.

 

public class Person {
	// ...Field
	// ...Constructor
	public Person(String name, String gender, int age) {
		// ...logic of Constructor
	};
	// ...method
	public void Aging() {
		this.age++;
	}

	public void Aging(int num) {
		this.age += num;
	}
}

"Aging()"이라는 메소드는 두 개이지만, 메소드 시그니처가 다르기 때문에 자바 컴파일러에서는 서로 같은 Aging() 메소드라 하더라도 이를 구분할 수 있다.

메소드 오버로드가 불가능한 경우도 있다. 아래 코드를 보자.

 

class Ex01 {
	public static int func01() {
		// ...logic of func01
		return 10;
	}

	public static void func01() {
		// ...logic of func01
	}
	public static void main(String[] args) {
		int a = func01();
		func01();
	}
}

위 코드는 오류가 발생한다. 메소드 오버로드를 진행하면서 메소드의 구분 기준으로써 반환 타입은 존재하지 않는다. 그렇기에 위 코드는 '이미 선언된 메소드'라는 이름의 에러가 발생하게 된다.

메소드 오버로딩은 서로 다른 기능을 하는 메소드를 같은 이름으로 통일하여 개발자의 편의성을 추구하게 된다는 장점이 있다. 하지만, 서로 다른 기능을 수행하는 메소드를 과도하게 오버로딩으로 설계하게 되면, 개발자 입장에서는 해당 메소드가 어떤 기능을 수행하는 것인지 직관적으로 알 수 없게 되어버린다.

 

 

 3.  static 키워드


JAVA 코드를 처음 작성하면 static 키워드가 들어간다. 아래 코드를 보자.

class Ex04 {
	public static void main(String[] args) {
		func01();
		Ex04 me = new Ex04();
		me.func02();
	}

	public static void func01() {
		System.out.println("Ex04 Class - func01 run!");
	}

	public void func02() {
		System.out.println("non-static method func02");
	}
}

non-static 메소드를 사용하기 위해서는 인스턴스를 별도로 선언한 뒤 인스턴스에서 해당 메소드를 사용하여야한다. 아래 표는 static과 non-static 메소드에 따른 static, non-static 메소드 호출 방법을 정리한 표다.

 

호출 순서 메소드 호출 방법
static -> static [클래스명].메소드명();
static -> non-static 참조변수.메소드명();
non-static -> static [클래스명].메소드명();
non-static -> non-static  [참조변수].메소드명();

static 키워드의 사용에 따라 프로그램의 성능을 좌지우지된다. 소스 코드 내에 작성된 모든 static 영역(메소드, 클래스 포함)은 JVM 메모리 내에 위치하는 클래스 영역에 위치하게 된다.(좀 더 자세하게 말하면, 클래스 영역에 존재하는 static 영역에 별도로 저장된다.)

static 영역 또한 JVM 메모리 내의 stack 영역에 쌓이거나, 지워지거나 할 수 있다. static 영역은 기본적으로 프로그램 내에서 언제 등장하고 언제 지워질지 예측할 수 없다. 그렇기 때문에 static 키워드로 작성된 메소드, 클래스들은 항상 일정한 메모리 영역을 점유하게 된다. 이는 프로그램 종료 때까지는 일정한 영역의 메모리는 항상 점유하고 있다는 의미다.

반면, non-static은 프로그램 실행 때부터 존재할 필요는 없다. 그렇기 때문에 JVM 메모리 내의 클래스 영역에 할당되지 않는다. 따라서 non-static 클래스를 생성할 때는 JVM 메모리 내에 있는 Heap 영역에 객체가 생성된다. Heap 영역에 있는 객체를 호출하기 위해서는 Stack 영역에 저장된 주소값을 사용하여야한다. 

 


JAVA의 Garbage Collection

JAVA에서 메모리를 자동으로 관리하는 가비지 컬렉션은, Heap 영역에서만 동작한다. Heap 영역 내에는 실질적인 객체들이 존재하는데, 이 때 참조변수를 통해 쓰이지 않은 객체들을 지워버린다.


Ex04 me = new Ex04();
me = new Ex04();

 

초기에 Ex04라는 클래스를 생성하여 me에 할당되었다. 그리고 두 번째 줄에서 또 다른 객체를 생성하여 동일한 변수에 재할당하였다. 이 경우 첫 번째 라인에 생성했던 Ex04 객체는 더 이상 사용할 수 없게되니, 가비지 컬렉션에 의해 자동으로 메모리에서 해제된다.

 

 

 

4. 변수(Field)


클래스의 구성요소 세 가지 중 변수에 대한 설명을 다룬다. 변수는 메소드에서 그렇듯, static 변수와 non-static 변수로 구분 할 수 있다. 이 때, static 변수는 클래스 변수라고 하며, non-static 변수는 멤버필드라고도 불린다. 이들의 호출 방법은 메소드에서와 동일하다.

 

class Ex05 {
	public static int su1 = 1111;
	public int su2 = 2222;
	
	public static void main(String[] args){
		System.out.println(su1);
		Ex05 me = new Ex05();
		System.out.println(me.su2);
	}
}

static 변수는 모든 인스턴스에서 공유하는 일종의 전역변수인셈이다. 하지만 non-static 변수(멤버필드)는 인스턴스마다 보유하고 있는 독립된 메모리 영역에 할당된 서로 다른 지역변수다.

static 변수는 몇 가지 특징이 있다.

1. 전역변수의 특성을 가진다.
2. 초기화 없이 사용가능하다.
3. default 값이 존재한다.
4. default 값을 사용하든지, 그렇지 않을 경우 선언과 초기화가 동시에 이루어져야한다.

static 변수의 경우 default 값이 존재한다. 이는, 코드레벨에서는 초기화를 진행하지 않고 사용할 수 있다는 의미가 된다. static 변수는 컴파일 단계에서 자동으로 초기화를 진행하기 때문에 아래와 같은 코드가 가능하다.

 

class Ex05{
	public static int su1;

	public static void main(String[] args) {
		System.out.println(su1);
	}
}

JAVA의 원시 자료형에는 기본값이 존재한다. boolean의 경우 false, 정수형/실수형의 경우 0을 갖는다.  

4번 특징의 경우는 아래와 같은 경우다.

 

class Ex05 {
	public static int su1;
	su1 = 1234;

	// ...logic of Ex05
}

위와 같은 코드는 오류를 발생시킨다. 위 영역은 JVM 메모리 내에서 static 영역에서 이루어지기 때문에 위와 같은 코드는 동작하지 않는다. Stack 영역에서 일어나야하는 코드기 때문이다. static 변수로 default 값을 사용하기로 했다면,  main 메소드나 다른 메소드에서 값의 할당을 진행해야한다.

필드 변수를 사용할 때는 조심해야 할 점이 있다. 바로 `final` 키워드의 사용이다. 아래 코드를 보자.

class Ex04 {
	public final static int num = 300;
	public final int num2;

	public static void main(String[] args) {
		System.out.println(num2);
	}
}

위 코드는 컴파일 에러를 발생시킨다. 'public final int num2'의 경우 선언만 되었고 초기화는 진행하지 않았다. final 키워드로 작성된 변수는 선언,초기화 이후 값의 변경이 불가하다. 따라서 위 코드는 한 번 선언되고 다시는 쓰일 일이 없기 때문에 무의미하다. 따라서 'variable num2 not initialized in the default constructor' 라는 에러가 발생한다. 디폴트 생성자에서 초기화 시키지 않았다는 오류다. 하지만 아래와 같은 코드는 가능하다.

 

class Ex04 {
	public final static int num = 300;
	public final int num2;

	public Ex04(int num) {
		num2 = num;
	}
}

위 코드는 정상적으로 동작한다.  멤버 필드에서 final로 상수형 변수가 선언되었으나, 생성자에서 받은 파라미터를 통해 초기화하였기 때문에 에러가 발생하지 않는다. 다만 객체 생성 이후 num2 값을 변경 할 수는 없다.