자바의 클래스에 대해 정리합니다
학습할 내용은 다음과 같습니다.
- 클래스 정의하는 방법
- 객체 만드는 방법 (new 키워드 이해하기)
- 메소드 정의하는 방법
- 생성자 정의하는 방법
- this 키워드 이해하기
- 과제
- int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
- int value, Node left, right를 가지고 있어야 합니다.
- BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
- DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
모든 자바 프로그램은 오브젝트를 사용하고 이런 오브젝트의 타입은 interface나 class로 정의됩니다.
자바는 객체 지향 프로그래밍이며 객체 지향 프로그래밍에서는 모든 데이터를 객체(object)로 취급하며, 이러한 객체가 바로 프로그래밍의 중심이 됩니다.
객체(object)란 간단히 이야기하자면 실생활에서 우리가 인식할 수 있는 사물로 설명할 수 있습니다.
이러한 객체의 상태(state)와 행동(behavior)을 구체화하는 형태의 프로그래밍이 바로 객체 지향 프로그래밍입니다.
이때 객체를 만들어 내기 위한 설계도와 같은 개념을 클래스(class)라고 합니다.
자바에서 클래스(class)란 객체를 정의하는 틀 또는 설계도와 같은 의미로 사용됩니다.
자바에서는 이러한 설계도인 클래스를 가지고, 여러 객체를 생성하여 사용하게 됩니다.
클래스는 객체의 상태를 나타내는 필드(field)와 객체의 행동을 나타내는 메소드(method)로 구성됩니다.
즉, 필드(field)란 클래스에 포함된 변수(variable)를 의미합니다.
또한, 메소드(method)란 어떠한 특정 작업을 수행하기 위한 명령문의 집합이라 할 수 있습니다.
자바에서 클래스를 사용하기 위해서는 우선 해당 클래스 타입의 객체(object)를 선언해야 합니다.
이렇게 클래스로부터 객체를 선언하는 과정을 클래스의 인스턴스 화라고 합니다.
또한, 이렇게 선언된 해당 클래스 타입의 객체를 인스턴스(instance)라고 합니다.
즉, 인스턴스란 메모리에 할당된 객체를 의미합니다.
자바에서는 하나의 클래스로부터 여러 개의 인스턴스를 생성할 수 있습니다.
이렇게 생성된 인스턴스는 독립된 메모리 공간에 저장된 자신만의 필드를 가질 수 있습니다.
하지만 해당 클래스의 모든 메소드(method)는 해당 클래스에서 생성된 모든 인스턴스가 공유하게 됩니다.
클래스 정의는 다음과 같습니다.
// 클래스 정의는 class라는 키워드를 통해 사용됩니다.
// 클래스가 다른 클래스를 상속하고 있다면 extends라는 키워드를 통해 사용됩니다.
// 만약 클래스가 하나 이상의 인터페이스를 구현해야 한다면 implements라는 키워드를 사용해야 합니다.
// 클래스 access control modifier로 public이나, protected 같은 키워드를 사용할 수 있습니다.
// 또한 modifier로 abstract, final, strictfp라는 키워드를 사용할 수 있습니다.
public class Integer extends Number implements Serializable, Comparable {
// class members go here
}
abstract 키워드는 implementation이 불완전하므로 인스턴스화 할 수 없습니다.
implementation이 불완전하단 뜻은 하나 이상의 추상 메소드를 포함한다는 의미입니다. 그러므로 추상 클래스를 상속받으면 이 추상 메소드를 Overriding 해야합니다.
즉, 반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언해 놓으면, 이 클래스를 상속받는 모든 클래스에서는 이 추상 메소드를 반드시 재정의해야 합니다.
final이라는 키워드가 붙은 메소드나 클래스는 상속이 불가능합니다.
그러므로 abstract랑 같이 쓸 순 없겠죠?
자기가 정의한 클래스를 상속받지 못하도록 할 때 final 키워드를 사용합니다.
final 키워드를 언제 붙여서 사용하는지 계속 Reserach 중입니다.
Strictfp 키워드가 붙은 클래스는 모든 메소드에서 부동 소수점 계산에서 정확히 동일한 결과를 얻도록 보장합니다.
클래스가 선언되었다면 new 키워드를 사용하여 인스턴스를 생성하고 해당 인스턴스의 주소를 미리 선언한 참조 변수에 저장하여 사용할 수 있습니다.
new 키워드의 과정을 좀 더 디테일하게 살펴보면 메모리를 새로운 인스턴스에 할당하고 주어진 Argument에 따라 the constructor body가 호출됩니다.
그 후 초기화 되고나서 객체가 생성됩니다.
모든 자바 클래스는 하나 이상의 constructor를 가지고 초기화 과정이 있습니다.
만약 의도적으로 contructor를 명시하지 않았다면 Javac 컴파일러가 자동적으로 constructor를 추가해줍니다.
아래의 예제는 특별한 Arguments와 Initialization이 없습니다.
// 개념
객체참조변수이름 = new 클래스이름();
// 예제
myCar = new Car();
자바에서 클래스는 멤버(member)로 속성을 표현하는 필드(field)와 기능을 표현하는 메소드(method)를 가집니다.
그중에서 메소드(method)란 어떠한 특정 작업을 수행하기 위한 명령문의 집합이라 할 수 있습니다.
클래스에서 메소드를 작성하여 사용하는 이유는 중복되는 코드의 반복적인 프로그래밍을 피할 수 있기 때문입니다.
또한, 모듈화로 인해 코드의 가독성도 좋아집니다.
그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때도 손쉽게 유지보수를 할 수 있게 됩니다.
메소드를 작성할 때는 되도록 하나의 메소드가 하나의 기능만을 수행하도록 작성하는 것이 좋습니다.
클래스에서 메소드를 정의하는 방법은 일반 함수를 정의하는 방법과 크게 다르지 않습니다.
자바에서 메소드를 정의하는 방법은 다음과 같습니다.
// 문법
접근제어자 반환타입 메소드이름(매개변수목록) { // 선언부
// 구현부
}
// 예제
class Car {
private int currentSpeed;
private int accelerationTime;
...
public void accelerate(int speed, int second) { // 선언부
// 구현부
System.out.println(second + "초간 속도를 시속 " + speed + "(으)로 가속함!!");
}
...
}
접근 제어자 : 해당 메소드에 접근할 수 있는 범위를 명시합니다.
반환 타입(return type) : 메소드가 모든 작업을 마치고 반환하는 데이터의 타입을 명시합니다.
메소드 이름 : 메소드를 호출하기 위한 이름을 명시합니다.
매개변수 목록(parameters) : 메소드 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시합니다.
구현부 : 메소드의 고유 기능을 수행하는 명령문의 집합입니다.
클래스를 가지고 객체를 생성하면, 해당 객체는 메모리에 즉시 생성됩니다.
하지만 이렇게 생성된 객체는 모든 인스턴스 변수가 아직 초기화되지 않은 상태입니다.
자바에서 클래스 변수와 인스턴스 변수는 별도로 초기화하지 않으면, 다음 값으로 자동 초기화됩니다.
변수의 타입 | 초깃값 |
---|---|
char | '\u0000' |
byte, short, int | 0 |
long | 0L |
float | 0.0F |
double | 0.0 또는 0.0D |
boolean | false |
배열, 인스턴스 등 | null |
하지만 사용자가 원하는 값으로 인스턴스 변수를 초기화하려면, 일반적인 초기화 방식으로는 초기화할 수 없습니다.
인스턴스 변수 중에는 private 변수도 있으며, 이러한 private 변수에는 사용자나 프로그램이 직접 접근할 수 없기 때문입니다.
따라서 private 인스턴스 변수에도 접근할 수 있는 초기화만을 위한 public 메소드가 필요해집니다.
이러한 초기화만을 위한 메소드는 객체가 생성된 후부터 사용되기 전까지 반드시 인스턴스 변수의 초기화를 위해 호출되어야 합니다.
자바에서는 객체의 생성과 동시에 인스턴스 변수를 원하는 값으로 초기화할 수 있는 생성자(constructor)라는 메소드를 제공합니다.
자바에서 생성자(constructor)의 이름은 해당 클래스의 이름과 같아야 합니다.
생성자는 반환값이 없지만, 반환 타입을 void형으로 선언하지 않습니다.
생성자는 초기화를 위한 데이터를 인수로 전달받을 수 있습니다.
객체를 초기화하는 방법이 여러 개 존재할 경우에는 하나의 클래스가 여러 개의 생성자를 가질 수 있습니다.
즉, 생성자도 하나의 메소드이므로, 메소드 오버로딩이 가능하다는 의미입니다.
// 문법
1. 클래스이름() { ... } // 매개변수가 없는 생성자 선언
2. 클래스이름(인수1, 인수2, ...) { ... } // 매개변수가 있는 생성자 선언
// 에제
Car(String modelName) {}
Car(String modelName, int modelYear) {}
Car(String modelName, int modelYear, String color) {}
Car(String modelName, int modelYear, String color, int maxSpeeds) {}
Car(String modelName, int modelYear, String color, int maxSpeeds) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
this 참조 변수는 인스턴스가 바로 자기 자신을 참조하는 데 사용하는 변수입니다.
이러한 this 참조 변수는 해당 인스턴스의 주소를 가리키고 있습니다.
Car(String modelName, int modelYear, String color, int maxSpeeds) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
위의 예제처럼 생성자의 매개변수 이름과 인스턴스 변수의 이름이 같을 경우에는 인스턴스 변수 앞에 this 키워드를 붙여 구분해만 합니다.
이렇게 자바에서는 this 참조 변수를 사용하여 인스턴스 변수에 접근할 수 있습니다.
이러한 this 참조 변수를 사용할 수 있는 영역은 인스턴스 메소드뿐이며, 클래스 메소드에서는 사용할 수 없습니다.
모든 인스턴스 메소드에는 this 참조 변수가 숨겨진 지역 변수로 존재하고 있습니다.
this() 메소드는 생성자 내부에서만 사용할 수 있으며, 같은 클래스의 다른 생성자를 호출할 때 사용합니다.
this() 메소드에 인수를 전달하면, 생성자 중에서 메소드 시그니처가 일치하는 다른 생성자를 찾아 호출해 줍니다.
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
// this() 메소드 예제
Car() {
this("소나타", 2012, "검정색", 160); // 다른 생성자를 호출함.
}
package me.jeongmin.javatest;
public class Node {
private int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
this.left = null;
this.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;
}
}
package me.jeongmin.javatest;
import java.util.LinkedList;
import java.util.Optional;
import java.util.Queue;
public class BinarySearchTree {
private Node root;
public Node getRoot() {
return root;
}
public BinarySearchTree() {
root = null;
}
@Override
public String toString() {
return "BinarySearchTree{" +
"root=" + root +
'}';
}
public void insert(int value){
insertRec(this.root, value);
}
private void insertRec(Node present, int value){
if(present == null){
root = new Node(value);
return;
}
if(present.getValue() > value){
if(present.getLeft() != null){
insertRec(present.getLeft(), value);
}else{
Node leftNode = new Node(value);
present.setLeft(leftNode);
}
}else if(present.getValue() < value){
if(present.getRight() != null){
insertRec(present.getRight(), value);
}else{
Node rightNode = new Node(value);
present.setRight(rightNode);
}
}
}
public void DFSLeftFirst(){
DFSLeftFirstRec(root);
}
public void DFSRootFirst(){
DFSRootFirstRec(root);
}
public void DFSRightFirst(){
DFSRightFirstRec(root);
}
private void DFSRightFirstRec(Node present) {
if(present == null) return;
DFSRightFirstRec(present.getRight());
System.out.println(present.getValue());
DFSRightFirstRec(present.getLeft());
}
private void DFSRootFirstRec(Node present) {
if(present == null) return;
System.out.println(present.getValue());
DFSRootFirstRec(present.getLeft());
DFSRootFirstRec(present.getRight());
}
private void DFSLeftFirstRec(Node present){
if(present == null) return;
DFSLeftFirstRec(present.getLeft());
System.out.println(present.getValue());
DFSLeftFirstRec(present.getRight());
}
public void BFS(){
if(root != null){
System.out.println(root.getValue());
Queue<Node> queue = new LinkedList<>();
queue.add(root.getLeft());
queue.add(root.getRight());
while (!queue.isEmpty()){
Optional<Node> node = Optional.ofNullable(queue.poll());
if(node.isPresent()){
System.out.println(node.orElse(null).getValue());
queue.add(node.orElse(null).getLeft());
queue.add(node.orElse(null).getRight());
}
}
}
}
}
package me.jeongmin.javatest;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.LinkedList;
import java.util.Queue;
import static org.junit.jupiter.api.Assertions.*;
class BinarySearchTreeTest {
private static BinarySearchTree binarySearchTree = new BinarySearchTree();
@BeforeAll
static void setBinarySearchTreeTest(){
binarySearchTree.insert(50);
binarySearchTree.insert(40);
binarySearchTree.insert(30);
binarySearchTree.insert(20);
binarySearchTree.insert(10);
binarySearchTree.insert(60);
binarySearchTree.insert(70);
binarySearchTree.insert(80);
binarySearchTree.insert(90);
binarySearchTree.insert(100);
binarySearchTree.insert(15);
binarySearchTree.insert(25);
binarySearchTree.insert(35);
binarySearchTree.insert(45);
binarySearchTree.insert(55);
binarySearchTree.insert(65);
binarySearchTree.insert(75);
binarySearchTree.insert(85);
binarySearchTree.insert(95);
}
@Test
@DisplayName("BST Insert Test")
void insertTest(){
BinarySearchTree binarySearchTree = new BinarySearchTree();
binarySearchTree.insert(50);
binarySearchTree.insert(40);
binarySearchTree.insert(30);
binarySearchTree.insert(20);
binarySearchTree.insert(10);
binarySearchTree.insert(60);
binarySearchTree.insert(70);
binarySearchTree.insert(80);
binarySearchTree.insert(90);
binarySearchTree.insert(100);
assertNotNull(binarySearchTree.getRoot());
}
@Test
@DisplayName("BFS Test")
void BFSTest(){
binarySearchTree.BFS();
}
@Test
@DisplayName("DFS Left First Test")
void DFSLeftTest(){
binarySearchTree.DFSLeftFirst();
}
@Test
@DisplayName("DFS Root First Test")
void DFSRootTest(){
binarySearchTree.DFSRootFirst();
}
@Test
@DisplayName("DFS Right First Test")
void DFSRightFirstTest(){
binarySearchTree.DFSRightFirst();
}
}