https://github.com/whiteship/live-study
백기선님 자바 기초 스터디 5주차
목표
자바의 Class에 대해 학습하세요.
학습할 것
- 클래스 정의하는 방법
- 객체 만드는 방법 (new 키워드 이해하기)
- 메소드 정의하는 방법
- 생성자 정의하는 방법
- this 키워드 이해하기
과제
- int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
- int value, Node left, right를 가지고 있어야 합니다.
- BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
- DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
클래스 정의하는 방법
클래스
클래스란 '객체를 정의해놓은 것.' 또는 객체의 '설계도 또는 틀'이라고 정의할 수 있다.
예를 들면, 제품 설계도와 제품의 관계라고 할 수 있다. TV설계도(클래스)는 TV라는 제품(객체)을 정의한 것이며, TV(객체)를 만드는데 사용된다.
클래스 | 객체 |
TV 설계도 | TV |
즉, 클래스는 단지 객체를 생성하는데 사용될 뿐, 객체 그 자체는 아니다. 우리가 원하는 기능의 객체를 사용하기 위해서는
1. 먼저 클래스를 정의하고
2. 클래스로부터 객체를 생성하는 과정이 필요하다.
이는 설계도를 통해서 제품을 만드는 이유와 같다. 하나의 설계도만 잘 만들어 놓으면 제품을 만드는 일이 쉬워진다. 제품을 만들 때마다 매번 고민할 필요없이 설계도대로만 만들면 되기 때문이다.
JDK에서는 프로그래밍을 위해 수많은 유용한 클래스(Java API)를 기본적으로 제공하고 있으며, 우리는 이 클래스들을 이용해서 원하는 기능의 프로그램을 보다 쉽게 작성할 수 있다.
클래스 정의
객체는 속성과 기능, 두 종류의 구성요소로 이루어져 있으며, 클래스란 객체를 정의한 것이므로 클래스에는 객체의 모든 속성과 기능이 정의되어 있다.
TV를 예로 들어보자.
TV의 속성으로는 전원상태, 크기, 길이 같은 것들이 있으며, 기능으로는 켜기, 끄기 등이 있다.
속성 | 기능 |
크기, 길이, 높이, 색상, 볼륨, 채널 등 | 켜기, 끄기, 볼륨 높이기, 볼륨 낮추기, 채널변경하기 등 |
객체 지향 프로그래밍에서는 속성과 기능을 각각 멤버변수와 메서드로 표현한다.
- 속성(property) -> 멤버변수(variable)
- 기능(function) -> 메서드(method)
위에서 분석한 내용을 토대로 Tv 클래스를 만들어 보면 다음과 같다.
class Tv {
//Tv의 속성(멤버변수)
String color; //색상
boolean power; //전원상태(on/off)
int channel; //채널
//Tv의 기능(메서드)
void power() { power = !power;} //Tv를 켜거나 끄는 기능을 하는 메서드
void channelUp() { channel++; } //Tv의 채널을 높이는 기능을 하는 메서드
void channelDown() { channel--; } //Tv의 채널을 낮추는 기능을 하는 메서드
}
이처럼 Tv라는 객체에서 프로그래밍에 필요한 속성과 기능만을 선택하여 클래스를 작성하면 된다.
각 멤버변수의 자료형은 속성의 값에 알맞은 것을 선택해야 한다.
예를 들어 power(전원상태)의 경우 on과 off 두 가지 값을 가질 수 있으므로 boolean형으로 선언했다.
메서드의 경우 해당 기능에 대한 내용을 { } 안에 작성하면 된다. (메서드에 대해서는 뒤에서 자세하게 다루겠다)
객체 만드는 방법 (new키워드 이해하기)
클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화(instantiate)라고 하며,
어떤 클래스로부터 만들어진 객체를 그 클래스의 인스턴스(instance)라고 한다.
클래스 -----(인스턴스화)-----> 인스턴스(객체)
결국 인스턴스는 객체와 같은 의미이지만, 객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖고 있으며, 인스턴스는 어떤 클래스로부터 만들어진 것인지를 강조하는 보다 구체적인 의미를 갖고 있다.
Tv클래스를 선언한 것은 Tv설계도를 작성한 것에 불과하므로, Tv인스턴스를 생성해야 제품(Tv)를 사용할 수 있다.
클래스로부터 인스턴스를 생성하는 방법은 일반적으로 다음과 같다.
클래스명 변수명;
변수명 = new 클래스명();
Tv t;
t= new Tv();
다음 예제를 보자.
class Tv {
//Tv의 속성(멤버변수)
String color; //색상
boolean power; //전원상태(on/off)
int channel; //채널
//Tv의 기능(메서드)
void power() { power = !power;} //Tv를 켜거나 끄는 기능을 하는 메서드
void channelUp() { channel++; } //Tv의 채널을 높이는 기능을 하는 메서드
void channelDown() { channel--; } //Tv의 채널을 낮추는 기능을 하는 메서드
}
class TvTest {
public static void main(String[] args) {
Tv t; // Tv인스턴스를 참조하기 위한 변수 t를 선언
t = new Tv(); //Tv인스턴스를 생성한다.
t.channel = 7; //Tv인스턴스의 멤버변수 channel의 값을 7로 한다.
t.channelDown(); //Tv인스턴스의 메서드 channelDown()을 호출한다.
}
}
1. Tv t;
Tv클래스 타입의 참조변수 t를 선언한다. 메모리에 참조변수 메모리에 참조변수 t를 위한 공간(스택 영역)이 마련된다. 아직 인스턴스가 생성되지 않았으므로 참조변수로 아무것도 할 수 없다.
2. t = new Tv();
연산자 new에 의해 Tv클래스의 인스턴스가 메모리의 빈 공간(힙 영역)에 생성된다. 즉, 인스턴스는 new 키워드를 통해서 생성할 수 있다.
이 때, 멤버 변수는 각 자료형에 해당하는 기본값으로 초기화 된다.
그 다음 대입연산자(=)에 의해서 생성된 인스턴스의 주소값이 참조변수 t에 저장된다. 이제 참조변수 t를 통해 Tv인스턴스에 접근할 수 있다.
3. t.channel = 7;
참조변수 t에 저장된 주소에 있는 인스턴스의 멤버변수 channel에 7을 저장한다.
즉, 인스턴스의 멤버변수를 사용하려면 '참조변수.멤버변수'와 같이 하면 된다.
4. t.channelDown();
참조변수 t가 참조하고 있는 Tv인스턴스의 channelDown() 메서드를 호출한다.
channelDown()메서드는 멤버변수 channel에 저장되어 있는 값을 1 감소시킨다. (7 -> 6)
void channelDown() { channel--; }
정리하자면, 인스턴스는 new 키워드를 통해 생성할 수 있고, 참조변수를 통해서만 다룰 수 있다.
인스턴스와 참조변수의 관계는 TV와 TV 리모콘의 관계라고 생각할 수 있다.
클래스 | 인스턴스 | 참조변수 |
TV 설계도 | TV | TV 리모콘 |
메소드 정의하는 법
메소드
메소드(method)는 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다.
수학의 함수와 유사하며, 어떤 값을 입력하면 작업을 수행해서 결과를 반환한다.
예를 들어 제곱근을 국하는 메소드 Math.sqrt()는 4.0을 입력하면, 2.0을 결과로 반환한다.
그저 메소드가 작업을 수행하는데 필요한 값만 넣고 원하는 결과를 얻으면 될 뿐, 이 메소드가 내부적으로 어떤 과정을 거쳐 결과를 만들어내는지 전혀 몰라도 된다.
즉, 메소드의 입력값과 출력값만 알면 되는 것이다. 그래서 메소드를 내부가 보이지 않는 '블랙박스(black box)'라고도 한다.
메소드 정의
메소드는 크게 '선언부'와 '구현부'로 이루어져 있다.
반환타입 메소드이름 (타입 변수명, 타입 변수명, ...) //선언부
{
// 메소드 호출시 수행될 코드 //구현부
}
int add(int a, int b) //선언부
{
return a + b; //구현부
}
1. 메소드 선언부
메소드 선언부는 매개변수, 메소드 이름, 반환 타입으로 구성되어 있다.
메소드가 작업을 수행하기 위해 어떤 값(매개변수)들을 필요로 하고 작업의 결과로 어떤 타입의 값(반환 타입)을 반환하는지에 대한 정보를 제공한다.
2. 메소드 구현부
메소드의 선언부 다음에 오는 괄호 { }를 구현부라고 하는데, 여기에 메소드를 호출했을 때 수행될 문장들을 넣는다.
특히 return문은 메소드 선언부의 반환 타입과 일치하거나 적어도 자동 형변환이 가능한 것이어야 한다.
<메소드의 장점>
1. 높은 재사용성 - 한 번 만들어 놓으면 몇 번이고 호출할 수 있다.
2. 중복된 코드의 제거 - 코드의 양이 줄어들어 오류가 발생할 가능성도 함께 줄어든다.
3. 프로그램의 구조화 - 큰 규모의 프로그램은 메소드 단위로 나누어 단순화 시키는 것이 필수다.
오버로딩
메소드도 변수와 마찬가지로 같은 클래스 내에서 서로 구별될 수 있어야 하기 때문에 각기 다른 이름을 가져야 한다.
그러나 자바에서는 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메소드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메소드를 재정의할 수 있다.
이처럼, 한 클래스 내에 같은 이름의 메소드를 여러 개 정의하는 것을 '오버로딩(Overloading)'이라고 한다.
오버로딩의 예로 가장 대표적인 것은 println메소드이다. 지금까지 println메소드 괄호 안에 값만 지정해주면 화면에 출력하는데 아무런 어려움이 없었다.
이는, 실제로 println메소드가 다음과 같이 10개의 오버로딩된 메소드를 정의해놓았기 때문이다.
void println();
void println(boolean x);
void println(char x);
void println(char[] x);
void println(double x);
void println(float x);
void println(int x);
void println(long x);
void println(Object x);
void println(String x);
<오버로딩의 조건>
1. 메소드 이름이 같아야 한다.
2. 매개변수의 개수 또는 타입이 달라야 한다.
+) 반환 타입은 오버로딩을 구현하는데 아무런 영향을 주지 못한다.
다음 예를 통해 알아보자.
int add(int a, int b) { return a + b };
// 오버로딩x, 매개변수의 개수 또는 타입이 달라야 한다.
int add(int x, int y) { return x + y };
//오버로딩x, 반환타입만 다르면 오버로딩으로 간주되지 않는다.
long add(int a, int b) { return (long)(a + b) };
//오버로딩o, 매개변수의 개수가 다르다.
int add(int a, int b, int c) { return a + b + c };
//오버로딩o, 매개변수의 타입이 다르다.
double add(double a, double b) { return a + b };
<오버로딩의 장점>
만일 메소드가 변수처럼 단지 이름만으로 구별된다면, 한 클래스 내의 모든 메소드들은 이름이 달라져야 한다.
즉, println같은 경우는 매개변수 타입에 따라 10개의 메소드의 이름이 모두 달라야 한다. 근본적으로 같은 기능을 하는 메소드들이지만, 서로 다른 이름을 가져야 하기 때문에 이름을 짓기 어렵고, 사용하는 쪽에서도 일일이 기억해야 하기 때문에 부담이 된다.
하지만 오버로딩을 통해 여러 메소드들이 println이라는 하나의 이름으로 정의될 수 있기 때문에, 이름을 짓기도 쉽고, 기억하기도 쉽다.
생성자 정의하는 법
생성자 정의
생성자는 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메소드'이다. 따라서 인스턴스 변수의 초기화 작업에 주로 사용되며, 인스턴스 생성 시에 실행되어야 할 작업을 위해서도 사용된다.
생성자를 정의할 때의 조건은 다음과 같다.
1. 생성자의 이름은 클래스의 이름과 같아야 한다.
2. 생성자는 리턴 값이 없다.
클래스이름 (타입 변수명, 타입 변수명, ...) {
//인스턴스 생성 시 수행될 코드,
//주로 인스턴스 변수의 초기화 코드를 적는다.
}
class Card {
Card() { //매개변수가 없는 생성자.
...
}
Card(String k, int num) { //매개변수가 있는 생성자
...
}
...
}
생성자도 오버로딩이 가능하므로 위와 같이 하나의 클래스에 여러 개의 생성자가 존재할 수 있다.
오해하기 쉬운 것이, 연산자 new가 인스턴스를 생성하는 것이지 생성자가 인스턴스를 생성하는 것이 아니다.
생성자는 단순히 인스턴스 변수들의 초기화에 사용되는 조금 특별한 메소드일 뿐이다.
Card 클래스를 예로 들어보자.
Card c = new Card();
1. 연산자 new에 의해서 메모리(heap)에 Card클래스의 인스턴스가 생성된다.
2. 생성사 Card()가 호출되어 수행된다.
3. 연산자 new의 결과로, 생성된 Card인스턴스의 주소가 반환되어 참조변수 c에 저장된다.
이처럼 생성자는 인스턴수변수가 생성될 때, 자동으로 호출되는 메서드이다.
지금까지 인스턴스를 생성하기 위해 사용해왔던 '클래스이름()'이 바로 생성자였던 것이다.
기본 생성자
모든 클래스에는 반드시 하나 이상의 생성자가 정의되어 있어야 한다. 그러나 지금까지 클래스에 생성자를 정의하지 않고도 인스턴스를 생성할 수 있었던 이유는 컴파일러가 제공하는 '기본 생성자(default constructor)'덕분이었다.
컴파일 시, 클래스에 생성자가 하나도 정의되지 않은 경우 컴파일러는 자동적으로 아래와 같은 내용의 기본 생성자를 추가하여 컴파일 한다.
클래스이름() { }
Card() { }
여기서 주의할 점은 기본 생성자가 컴파일러에 의해서 추가되는 경우는 클래스에 정의된 생성자가 하나도 없을 때 뿐이라는 것이다.
Card(int x) { ... }와 같은 생성자가 정의되어 있으면 기본 생성자는 추가되지 않는다.
매개변수가 있는 생성자
보통 멤버변수들을 초기화할 때, 매개변수가 있는 생성자를 사용한다.
다음 예시를 보자.
class Tv {
//Tv의 속성(멤버변수)
String color; //색상
boolean power; //전원상태(on/off)
int channel; //채널
Tv() {} //기본 생성자
Tv(String c, boolean p, int ch) { //매개변수가 있는 생성자
color = c;
power = p;
channel = ch;
}
...
}
Tv인스턴스를 생성할 때, 기본 생성자를 사용한다면, 인스턴스를 생성한 다음에 인스턴스변수들을 따로 초기화해주어야 하지만,
매개변수가 있는 생성자를 사용한다면 인스턴스를 생성하는 동시에 원하는 값으로 초기화를 할 수 있게 된다.
<기본 생성자>
Tv t = new Tv();
t.color = "white";
t.power = true;
t.channel = 7;
<매개변수가 있는 생성자>
Tv t = new Tv("white", true, 7);
같은 내용의 코드이지만, 매개변수가 있는 생성자 코드가 더 간결하고 직관적이다.
this 키워드 이해하기
참조 변수 this
Tv(String c, boolean p, int ch) {
color = c;
power = p;
channel = ch;
}
위 생성자는 매개변수로 선언된 지역변수 c의 값을 인스턴스 변수 color에 저장한다.
이 때, color와 c는 이름만으로 구별되므로 아무런 문제가 없다.
이런 경우는 어떨까?
Tv(String color, boolean power, int channel) {
color = color;
power = power;
channel = channel;
}
이 경우 이름만으로 두 변수가 서로 구별이 가지 않는다. 그래서 'color = color' 둘다 지역변수로 간주된다.
이처럼 매개변수와 인스턴스변수의 이름이 같을 경우에는 인스턴스 변수 앞에 'this'를 사용하면 된다.
Tv(String color, boolean power, int channel) {
this.color = color;
this.power = power;
this.channel = channel;
}
this 키워드를 사용하면 위와 같이 인스턴스변수와 같은 이름의 매개변수와 구분할 수 있다.
여기서 this는 참조변수로 인스턴스 자신을 가리킨다.
생성자는 매개변수로 인스턴스변수의 초기값을 제공받는 경우가 많기 때문에 매개변수와 인스턴스변수의 이름이 일치하는 경우가 자주 있다. 이 때는 매개변수 이름을 다르게 하는 것보다는 'this'를 사용해서 구별되도록 하는 것이 의미가 더 명확하고 이해하기 쉽다.
생성자 this()
this 키워드는 생성자에도 적용할 수 있다.
한 클래스의 여러 생성자 간에도 서로 호출이 가능한데, 이 때 두 조건을 만족시켜야 한다.
1. 생성자의 이름으로 클래스이름 대신 this를 사용한다.
2. 한 생성자에서 다른 생성자를 호출할 때는 첫 줄에서만 호출이 가능하다.
다음 예시를 보자.
Tv(Sting color) {
channel = 5;
Tv(color, true, 7); //에러1. 생성자의 두 번째 줄에서 다른 생성자 호출
} //에러2. this(color, true, 7);로 해야함
에러 1 -> 한 생성자에서 다른 생성자 호출은 반드시 첫 번째 줄에서 해야 함.
에러 2 -> 한 생성자에서 다른 생서자를 호출할 때는 클래스 이름인 'Tv' 대신 'this'를 사용해야 한다.
이제 생성자 호출 조건을 만족하는 예시를 보자.
Tv(String color) {
this(color, true, 0);
}
Tv(String color, boolean power, int channel) {
this.color = color;
this.power = power;
this.channel = channel;
}
같은 클래스 내의 생성자들은 일반적으로 서로 관계가 깊은 경우가 많아서 이처럼 서로 호출하도록 하여 유기적으로 연결해주면 유지보수가 쉬워지는 등 더 좋은 코드를 얻을 수 있다.
+) 참고
위 코드에서 this.color의 'this'와, this(color, ture, 0)의 'this()'는 완전히 다른 것이다.
'this'는 '참조 변수'이고, 'this()'는 '생성자'이다.
과제
- int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
- int value, Node left, right를 가지고 있어야 합니다.
- BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
- DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
<Node>
public class Node {
private int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
}
public Node(int value, Node left, Node right) {
this.value = value;
this.left = left;
this.right = right;
}
public int getValue() {
return value;
}
public Node getLeft() {
return left;
}
public Node getRight() {
return right;
}
}
<BinaryTree>
public class BinaryTree {
public static List<Integer> bfs(Node node) {
if (node == null) {
return null;
}
List<Integer> result = new ArrayList<>();
Queue<Node> queue = new LinkedList<>();
queue.add(node);
while (!queue.isEmpty()) {
Node popNode = queue.remove();
result.add(popNode.getValue());
if (popNode.getLeft() != null) {
queue.add(popNode.getLeft());
}
if (popNode.getRight() != null) {
queue.add(popNode.getRight());
}
}
return result;
}
public static List<Integer> dfs(Node node) {
if (node == null) {
return null;
}
List<Integer> result = new ArrayList<>();
Node cur = node;
Stack<Node> stack = new Stack<>();
stack.push(node);
while (!stack.isEmpty()) {
while (cur.getLeft() != null) {
cur = cur.getLeft();
stack.push(cur);
}
cur = stack.pop();
result.add(cur.getValue());
if (cur.getRight() != null) {
cur = cur.getRight();
stack.push(cur);
}
}
return result;
}
}
<테스트>
class BinaryTreeTest {
Node node;
@BeforeEach
public void createBinaryTree() {
Node leftNode = new Node(2, new Node(4), new Node(5));
Node rightNode = new Node(3, new Node(6), new Node(7));
node = new Node(1, leftNode, rightNode);
/*
1
2 3
4 5 6 7
*/
}
@Test
void bfs() {
List<Integer> result = BinaryTree.bfs(node);
assertThat(result).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
}
@Test
void dfs() {
List<Integer> result = BinaryTree.dfs(node);
assertThat(result).isEqualTo(Arrays.asList(4, 2, 5, 1, 6, 3, 7));
}
}
'java > java' 카테고리의 다른 글
[Java] 패키지 (2) | 2021.07.21 |
---|---|
[Java] 상속 (3) | 2021.07.12 |
[Java] 제어문 (3) | 2021.06.29 |
[Java] Java 환경 변수 설정 - MacOS (0) | 2021.06.29 |
[Java] 연산자 (2) | 2021.06.23 |