목표
자바의 Class에 대해 학습하세요.
학습할 것
- 클래스 정의하는 방법
- 객체 만드는 방법 (new 키워드 이해하기)
- 메소드 정의하는 방법
- 생성자 정의하는 방법
- this 키워드 이해하기
과제
- int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
- int value, Node left, right를 가지고 있어야 합니다.
- BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
- DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
1. 클래스 정의하는 방법
객체지향언어 ( 객체지향프로그래밍 )
현재 세계의 사물을 객체로 보고, 객체의 속성과 기능을 기반으로 프로그래밍하는 기법
객체지향언어의 가장 큰 장점은 ‘코드의 재사용성이 높고 유지보수가 용이하다.'는 것이다.
클래스란?
객체를 정의해놓은 것으로 객체를 생성하는데 사용한다.
여기서 객체란 ‘실제로 존재하는 것'으로 사물과 같은 유형적인 것뿐만 아니라, 개념이나 논리와 같은 무형적인 것들도 객체로 간주한다.
클래스와 객체의 관계를 우리가 살고 있는 실생활에서 예를 들자면, 제품 설계도와 제품과의 관계라고 할 수 있다. TV설계도(클래스)는 TV라는 제품(객체)을 정의한 것이며, TV를 보기위해서는 설계도가 아니라 TV가 필요한 것 처럼 클래스도 객체를 만드는데만 사용되지 객체 그 자체는 아닌 것이다. 그리고 객체는 클래스를 통해 생성한 다음에야 사용 가능하다.
이처럼 클래스를 선언해두면 그 이후 필요할 때마다 해당 클래스를 통해 객체를 생성해 사용할 수 있다.
클래스는 객체를 정의하기 위한 속성(멤버 변수)와 기능(메소드)로 구성된다.
인스턴스
어떤 클래스로부터 만들어진 객체를 그 클레스의 인스턴스라고 한다. 즉, 인스턴스는 객체를 생성하여 메모리가 할당되어 있는 상태라고 할 수 있다.
따라서, 클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화라고 한다.
클래스 정의하는 방법
작성 규칙
- 하나 이상의 문자로 이뤄저야 한다.
- Car, SportsCar
- 첫 번째 글자는 숫자가 올 수 없다.
- 5
Car
- 5
**$
,-
외의 특수 문자는 사용할 수 없다.**@Car, !Car
- 자바 키워드는 사용할 수 없다.
int, for
클래스 정의
[접근 제어자] class [이름] {
// 필드
static String classVar; // 클래스 변수
String instanceVar; // 인스턴스 변수
// 초기화 블록
static { // 클래스 초기화 블록
classVar = "Class Variable";
}
{ // 인스턴스 초기화 블록
instanceVar = "Instance Variable";
}
// 생성자
public TestClass() { // 기본 생성자
}
public TestClass(String instanceVar) { // 매개변수가 있는 생성자
this.instanceVar = instanceVar;
}
// 메서드
public static void classMethod() {
System.out.println(classVar);
}
public void instanceMethod() {
System.out.println(instanceVar);
}
}
- 필드
- 객체의 속성을 나타내며, 멤버 변수라고도 불린다. 여기서 초기화하는 것을 명시적 초기화라고 한다.
- 인스턴스 변수 - 클래스 영역에서 선언되며, 클래스의 인스턴스를 생성할 때 만들어 진다. 서로 독립적인 값을 갖으므로
heap
영역에 할당되고 GC에 의해 관리된다. - 클래스 변수 -
static
키워드가 인스턴스 변수 앞에 붙을 경우 클래스 변수가 되며 한 클래스의 모든 인스턴스가 값을 공유한다. ( 공통된 저장 공간 ) 그렇기 때문에heap
영역이 아닌메서드
영역에 할당되고 GC의 관리를 받지 않는다.
- 인스턴스 변수 - 클래스 영역에서 선언되며, 클래스의 인스턴스를 생성할 때 만들어 진다. 서로 독립적인 값을 갖으므로
- 객체의 속성을 나타내며, 멤버 변수라고도 불린다. 여기서 초기화하는 것을 명시적 초기화라고 한다.
- 메서드
- 객체의 기능을 나타내며, 메소드내에 정의된 행위를 실행하는 역할을 한다.
- 인스턴스 메서드 - 인스턴스 변수와 관련된 작업을 하는 메서드이다. 인스턴스를 통해 호출할 수 있으므로 반드시 먼저 인스턴스를 생성해야 한다.
- 클래스 메서드 - 인스턴스와 관계 없는 메서드를 클래스 메서드(static 메서드)로 정의한다.
- 객체의 기능을 나타내며, 메소드내에 정의된 행위를 실행하는 역할을 한다.
- 생성자
- 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드이다. 메서드와 달리 리턴값이 없고 클래스엔 최소 한 개 이상의 생성자가 존재한다.
- 초기화 블록
- 초기화 블록 내에서는 조건문, 반복문 등을 사용해 명시적 초기화에선 불가능한 초기화를 수행할 수 있다.
- 클래스 초기화 블록 - 클래스 변수 초기화에 사용하며 클래스가 처음 메모리에 로딩될 때 한번만 수행된다.
- 클래스 변수의 초기화 순서 : 기본값 → 명시적 초기화 → 클래스 초기화 블럭
- 인스턴스 초기화 블록 - 인스턴스 변수 초기화에 사용하며 인스턴스 초기화 블록이 생성자보다 먼저 수행된다.
- 클래스 변수의 초기화 순서 : 기본값 → 명시적 초기화 → 클래스 초기화 블럭
- 클래스 초기화 블록 - 클래스 변수 초기화에 사용하며 클래스가 처음 메모리에 로딩될 때 한번만 수행된다.
- 초기화 블록 내에서는 조건문, 반복문 등을 사용해 명시적 초기화에선 불가능한 초기화를 수행할 수 있다.
static
이나 public
같은 키워드를 제어자라고 하며, 클래스나 멤버 선언 시 부가적인 의미를 부여한다.
접근 제어자
- 해당 클래스 또는 멤버를 정해진 범위에서만 접근할 수 있도록 통제하는 역할을 한다. 클래스는
public
과default
밖에 쓸 수 없다.
- 해당 클래스 또는 멤버를 정해진 범위에서만 접근할 수 있도록 통제하는 역할을 한다. 클래스는
static
- 변수, 메서드는 객체가 아닌 클래스에 속한다.
final
- 클래스 앞에 붙으면 해당 클래스는 상속될 수 없다.
- 변수 또는 메서드 앞에 붙으면 수정되거나 오버라이딩 될 수 없다.
abstract
- 클래스 앞에 붙으면 추상 클래스가 되어 객체 생성이 불가하고, 접근을 위해선 상속받아야 한다.
- 변수 앞에 지정할 수 없다. 메서드 앞에 붙는 경우는 오직 추상 클래스 내에서의 메서드밖에 없으며 해당 메서드는 선언부만 존재하고 구현부는 상속한 클래스 내 메서드에 의해 구현되어야 한다.
transient
- 변수 또는 메서드가 포함된 객체를 직렬화할 때 해당 내용은 무시된다.
synchonized
- 메서드는 한 번에 하나의 쓰레드에 의해서만 접근 가능하다.
volatile
- 해당 변수의 조작에 CPU캐시가 쓰이지 않고 항상 메인 메모리로부터 읽힌다.
2. 객체 만드는 방법 (new 키워드 이해하기)
클래스명 변수명 = new 클래스명();
클래스를 작성했다면, 이제 이 클래스를 통해 객체를 생성할 수 있다.
Java의 new 키워드는 클래스의 인스턴스를 만드는 데 사용된다. 즉, 새 객체에 대한 메모리를 할당하고 해당 메모리에 대한 주소를 반환하여 클래스를 인스턴스화 한다.
new keyword 특징
- 객체를 만드는 데 사용한다.
- 런타임에 메모리에 할당한다.
- 모든 객체는 힙 영역에서 메모리를 차지한다.
- 객체 생성자를 호출한다.
Car.java
public class Car {
private String name;
private int position;
public Car(String name, int position) {
this.name = name;
this.position = position;
}
public void go() {
System.out.println("앞으로 전진합니다.");
}
public void stop() {
System.out.println("멈춥니다.");
}
}
CarMain.java
public class CarMain {
public static void main(String[] args) {
Car carOne = new Car("pobi", 0);
Car carTwo = new Car("hwan", 1);
carOne.go();
carOne.stop();
}
}
====================================
앞으로 전진합니다.
멈춥니다.
메모리에서의 동작
3. 메소드 정의하는 방법
객체의 기능을 나타내며, 메소드내에 정의된 행위를 실행하는 역할을 한다.
[접근제어자] [반환 타입] 메서드 이름 (매개변수...) {
// 메소드 호출 시 수행될 코드
}
중복되는 코드를 하나의 객체의 메소드로 정의하며 가독성을 높이고 문제 발생시에 손쉽게 처리하기 위함으로 사용한다. 반환 타입을 지정할 수 있고, 매개변수도 받을 수 있으며 오버로딩 또한 가능하다.
메소드 호출
// 1. 기본적인 메소드 호출
String carName = getName();
// 2. 클래스 메소드 호출
String carName = Car.getName();
// 3. 인스턴스 메소드 호출
Car car = new Car("pobi", 0);
String carName = car.getName()
메모리에서의 동작
public class CallStackTest {
public static void main(String[] args) {
firstMethod();
}
static void firstMethod() {
secondMethod();
}
static void secondMethod() {
System.out.println("secondMethod()");
}
}
기본형 매개변수와 참조형 매개변수
자바에서는 메소드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사해서 넘겨준다. 매개변수의 타입이 기본형일 때는 기본형 값이 복사되지만, 참조형일 경우 인스턴스의 주소가 복사된다.
즉, 메소드의 매개변수를 기본형으로 선언하면 단순히 저장된 값만 얻지만, 참조형으로 선언할 경우 값이 저장된 곳의 주소를 알 수 있기 때문에 값을 읽어 오는 것은 물론 값을 변경하는 것도 가능하다.
기본형 매개변수 - 변수의 값을 일기만 할 수 있다. (read only)
참조형 매개변수 - 변수의 값을 읽고 변경할 수 있다. (read & write)
그렇다면 자바는 Call by reference가 있을까?
우리는 Java에서 객체를 전달받고, 그 객체를 수정하면 원본도 같이 수정되기 때문에 Call by reference라고 생각하기도 한다.
하지만 자바는 무조건적으로 Call by value이다.
예제와 함께 살펴보자!
public class Number {
int value;
public Number(int value) {
this.value = value;
}
}
public class NumberMain {
public static void main(String[] args) {
Number one = new Number(1);
Number two = new Number(2);
run(one, two);
System.out.println(one.value);
System.out.println(two.value);
}
public static void run(Number argOne, Number argTwo) {
argOne.value = 111;
argTwo = argOne;
}
}
================================
111
2
해당 예제에서 one의 value가 111로 변경이 되었다. 이것만 보고 one의 value를 변경하니 원본 one의 값도 변경되었으니 이것을 call by reference라고 헷갈리는 것이다. 하지만 one에서 argOne으로 매개변수를 넘기는 과정에서 직접적인 참조를 넘긴 것이 아닌, 주소 값을 복사해서 넘기기 때문에 이는 call by value가 된다. 복사된 주소 값으로 참조가 가능하니 주소 값이 가리키는 객체의 내용이 변경되는 것이다.
그렇기 때문에 run 메소드에서 argTwo에 argOne의 값을 저장한다고 해도 이는 run 메소드 내에 존재하는 argTwo가 argOne이 가진 주소값을 복사하여 저장하는 것일 뿐 원본 two와는 독립된 변수이기 때문에 two의 value는 변경되지 않는 것이다.
그를 메모리 영역에서 본다면 아래와 같다.
즉, Java는 기본적으로 모든 전달 방식이 Call by value이다.
클래스 메소드와 인스턴스 메소드
인스턴스 메서드
- 인스턴스 변수와 관련된 작업을 하는 메서드이다. 인스턴스를 통해 호출할 수 있으므로 반드시 먼저 인스턴스를 생성해야 한다.
클래스 메서드
- 인스턴스와 관계 없는 메서드를 클래스 메서드(static 메서드)로 정의한다.
클래스멤버를 사용시 주의해야 할 점이 있다.
인스턴스멤버가 클래스멤버를 참조 또는 호출하고자 하는 경우에는 문제가 없다.
하지만 클래스멤버가 인스턴스 멤버를 참조 또는 호출하고자 하는 경우에는 무조건 인스턴스를 생성해야 한다.
그 이유는 인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만, 클래스 멤버가 존재하는 시점에 인스턴스 멤버가 존재하지 않을 수도 있기 때문이다.
public class TestClass {
public static void classMethod() { // 클래스 메소드
}
public static void classMethod2() { // 클래스 메소드
instanceMethod(); // 에러!! 인스턴스 메소드 호출 불가
classMethod(); // class 메소드 호출
}
public void instanceMethod() { // 인스턴스 메소드
}
public void instanceMethod2() { // 인스턴스 메소드
instanceMethod(); // 인스턴스 메소드 호출
classMethod(); // class 메소드 호출
}
}
오버로딩 ( overloading )
한 클래스 내에 같은 이름의 메소드를 여러 개 정의하는 것
오버로딩이 성립하기 위해서는 다음과 같은 조건을 만족해야 한다.
- 메소드의 이름이 같아야 한다.
- 매개변수의 개수 또는 타입이 달라야 한다.
매개변수에 의해서만 구별될 수 있지 반환 타입은 오버로딩을 구현하는데 아무런 영향을 주지 못한다.
즉, 매개변수는 동일하고 리턴타입이 다른 경우에 오버로딩이 성립하지 않는다는 것이다.
대표적인 예로는 우리가 자주 사용하는 println 메소드이다.
//오버로딩
public String getName() { ... }
public String getName(int age) { ... }
//println 메소드
System.out.println("문자열");
System.out.println(1234);
오버로딩의 장점은 오버로딩을 통해 여러 메서드들이 하나의 이름으로 정의될 수 있다는 점이다.
또한, 메소드의 이름만을 봐서 동일한 기능을 할 수 있다고 예측이 가능하다.
오버라이딩(Overriding)
상위 클래스가 정의한 메소드를 하위 클래스가 가져와 변경하거나 확장하는 기법이다. 즉, 하위 클래스에서 메소드를 재정의하는 기법이다.
public class Person {
public void print() {
System.out.println("사람입니다.");
}
}
public class Adult extends Person {
@Override
public void print() {
System.out.println("어른입니다.");
}
}
public class Child extends Person {
@Override
public void print() {
System.out.println("어린이입니다.");
}
}
public class PersonMain {
public static void main(String[] args) {
Person person = new Person();
Adult adult = new Adult();
Child child = new Child();
person.print();
adult.print();
child.print();
}
}
======================
사람입니다.
어른입니다.
어린이입니다.
오버라이딩은 상위 클래스의 메소드를 하위 클래스에서 메소드를 재정의하기 때문에 확장과 변경에 용이하다는 장점이 있다.
가변인자
JDK1.5부터 메소드의 매개변수를 동적으로 지정해 줄 수 있게 되었으며, 이 기능을 가변인자라고 한다.
가변인자는 타입... 변수명
과 같은 형식으로 선언하며, PrintStream클래스의 printf()가 대표적인 예이다.
public PrintStream printf(String format, Object... args) {...}
가변인자외에도 매개변수가 더 있다면, 가변인자를 매개변수 중에서 제일 마지막에 선언해야한다.
4. 생성자 정의하는 방법
인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드이다. 메서드와 달리 리턴값이 없고 클래스엔 최소 한 개 이상의 생성자가 존재한다.
변수를 선언하고 초기화하는 것과 마찬가지로 클래스를 생성하고 객체를 호출할 때 객체를 초기화 하기 위해 사용된다.
생성자는 3가지 종류가 있다.
- 기본 생성자
- 클래스에 생성자가 하나도 정의되지 않는 경우 컴파일러는 자동적으로 기본 생성자를 추가하여 컴파일 한다.
- 묵시적 생성자 ( 매개변수가 없는 생성자 )
- 파라미터 값을 가지지 않는 생성자이다.
- 명시적 생성자 ( 매개변수가 있는 생성자 )
- 메서드처럼 매개변수를 선언하여 호출 시 값을 넘겨받아 인스턴스의 초기화 작업에 사용할 수 있다.
class Person {
Person() {} // 1. 기본 생성자 , 컴파일러가 자동적으로 추가해줌.
}
public class Person {
String name;
int age;
// 2. 묵시적 생성자
public Person() {
this.name = "홍길동";
this.age = 1;
}
// 3. 명시적 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
만약 명시적 생성자만 선언되있는 경우 파라미터가 없는 생성자를 사용하고 싶다면 묵시적 생성자를 선언해줘야 한다. 생성자가 클래스 내부에 이미 선언되어 있기 때문에 컴파일러가 기본 생성자를 생성하지 않기 때문이다.
5. this 키워드 이해하기
this
클래스가 인스턴스화 되었을 때 자기 자신의 메모리 주소를 가지고 있다. 즉, 인스턴스 자기 자신을 가르킨다.
클래스 내부의 필드 이름과 메소드를 통해 넘어온 파라미터의 변수명이 동일한 경우 this 키워드를 통해 클래스 내부의 필드이름과 파라미터를 구분하여 사용할 수 있다.
또한, 모든 인스턴스메소드에 지역변수로 숨겨진 채로 존재한다.
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
위에서 말했듯이 동일하게 클래스 메서드에서는 this
키워드를 사용할 수 없다.
this(), this(매개변수)
해당 클래스 생성자를 호출할 수 있다.
public class Person {
String name;
int age;
// 묵시적 생성자
public Person() {
this("홍길동", 1);
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
주로 생성자를 재사용하는 데 쓰인다. ( 생성자 체이닝 )
호출하는 곳의 첫 번째 문장에서 호출되어야 한다.
과제
Node.java
public class Node {
private final int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
public boolean hasLeft() {
return Objects.nonNull(left);
}
public boolean hasRight() {
return Objects.nonNull(right);
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public int getValue() {
return value;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
}
BinaryTree.java
public class BinaryTree {
public void bfs(Node node) {
if (node == null) {
throw new NoSuchElementException();
}
Queue<Node> queue = new LinkedList<>();
queue.add(node);
while (!queue.isEmpty()) {
Node curNode = queue.remove();
System.out.print(curNode.getValue() + " ");
if (curNode.hasLeft()) {
queue.add(curNode.getLeft());
}
if (curNode.hasRight()) {
queue.add(curNode.getRight());
}
}
System.out.println();
}
public void dfs(Node node) {
if (node == null) {
throw new NoSuchElementException();
}
if (node.hasLeft()) {
dfs(node.getLeft());
}
System.out.print(node.getValue() + " ");
if (node.hasRight()) {
dfs(node.getRight());
}
}
}
BinaryTreeTest.java
class BinaryTreeTest {
private static BinaryTree binaryTree;
private final ByteArrayOutputStream output = new ByteArrayOutputStream();
@BeforeAll
static void setup() {
binaryTree = new BinaryTree();
}
private List<Node> createNodes() {
List<Node> nodes = new ArrayList<>();
for (int i = 0; i <= 4; i++) {
nodes.add(new Node(i));
}
return nodes;
}
private Node setupOne() {
List<Node> nodes = createNodes();
nodes.get(0).setLeft(nodes.get(1));
nodes.get(0).setRight(nodes.get(2));
nodes.get(1).setLeft(nodes.get(3));
nodes.get(1).setRight(nodes.get(4));
return nodes.get(0);
}
private Node setupTwo() {
List<Node> nodes = createNodes();
nodes.get(3).setLeft(nodes.get(4));
nodes.get(3).setRight(nodes.get(2));
nodes.get(4).setLeft(nodes.get(1));
nodes.get(4).setRight(nodes.get(0));
return nodes.get(3);
}
@DisplayName("bfs 메서드는")
@Nested
class Describe_bfs {
@DisplayName("아무런 노드가 존재하지 않았을 때")
@Nested
class Context_with_not_exist_node {
@DisplayName("예외를 발생한다.")
@Test
void it_throws_exception() {
assertThrows(NoSuchElementException.class, () -> binaryTree.bfs(null));
}
}
@DisplayName("알맞은 이진 트리가 주어졌을 때")
@Nested
class Context_with_binary_tree {
final String CASE_1_OUTPUT_RESULT = "0 1 2 3 4 \n";
final String CASE_2_OUTPUT_RESULT = "3 4 2 1 0 \n";
@DisplayName("너비를 우선으로 탐색한다.")
@Test
void it_search_width_first() {
assertAll(
() -> {
System.setOut(new PrintStream(output));
binaryTree.bfs(setupOne());
assertEquals(CASE_1_OUTPUT_RESULT, output.toString());
output.reset();
},
() -> {
System.setOut(new PrintStream(output));
binaryTree.bfs(setupTwo());
assertEquals(CASE_2_OUTPUT_RESULT, output.toString());
output.reset();
}
);
}
}
}
@DisplayName("dfs 메서드는")
@Nested
class Describe_dfs {
@DisplayName("아무런 노드가 존재하지 않았을 때")
@Nested
class Context_with_not_exist_node {
@DisplayName("예외를 발생한다.")
@Test
void it_throws_exception() {
assertThrows(NoSuchElementException.class, () -> binaryTree.dfs(null));
}
}
@DisplayName("알맞은 이진 트리가 주어졌을 때")
@Nested
class Context_with_binary_tree {
final String CASE_1_OUTPUT_RESULT = "3 1 4 0 2 ";
final String CASE_2_OUTPUT_RESULT = "1 4 0 3 2 ";
@DisplayName("중위순회로 탐색한다.")
@Test
void it_search_width_first() {
assertAll(
() -> {
System.setOut(new PrintStream(output));
binaryTree.dfs(setupOne());
assertEquals(CASE_1_OUTPUT_RESULT, output.toString());
output.reset();
},
() -> {
System.setOut(new PrintStream(output));
binaryTree.dfs(setupTwo());
assertEquals(CASE_2_OUTPUT_RESULT, output.toString());
output.reset();
}
);
}
}
}
}
배운점
- System.out에 대한 테스트
- 콘솔창에 출력하는 메소드이기 때문에 출력에 대한 값을 테스트하는 방법을 배울 수 있었다.
- System.in과 System.out에 대한 테스트
- 위의 글처럼 BeforeEach와 AfterEach를 사용하여 테스트하고 싶었지만 assertAll을 사용해서 그대로 쓰면 적용이 되지 않았다.
- 이에
ParameterizedTest
의MethodSource
을 사용해서 해보려 했지만 적용이 되지 않았다. - 어떤 방법이 좋을 지 생각해보고 수정해봐야 겠다.
- 이에
Reference
- 남궁성. Java의 정석 3판. 도우출판, 2016.
- System.in과 System.out에 대한 테스트
'Back-End > 백기선님의 자바 스터디' 카테고리의 다른 글
7주차 - 패키지 (0) | 2022.04.05 |
---|---|
6주차 - 상속 (0) | 2022.03.31 |
4주차 - 제어문 (0) | 2022.03.16 |
3주차 - 연산자 (0) | 2022.03.10 |
2주차 - 자바 데이터 타입, 변수 그리고 배열 (0) | 2022.03.10 |
댓글