클래스는 객체의 상태를 나타내는 필드(field)와 객체의 행동을 나타내는 메소드(method), 그리고 생성자(constructor)로 구성된다.
접근 제어자는 클래스, 필드, 메소드에 대한 접근 권한을 제한하기 위해 사용되는 문법이다.
예를 들어 클래스 내부의 필드값을 오직 클래스의 메소드를 통해서만 수정가능해야 한다고 하자.
public class Car {
public void raisePrice() {
price++;
}
private int price = 0;
}
public class Seller {
private Car car = new Car();
private void raisePrice(int price){
car.raisePrice(); // 1 : OK
car.price = 1000 // 2 : Error
}
}
위 경우 1은 메소드를 통해 필드값을 수정하지만, 2는 직접 필드에 접근하여 값을 수정하려고 한다.
private이기 때문에 클래스의 외부에서 필드값을 바꿀 수 없다. 이와같이 제한을 두어 실수를 방지한다.
자바의 모든 클래스는 class Class
를 상속하고 있으며 class Class
는 class Object
를 상속하고 있다.
임의의 클래스 > Class 클래스 > Object 클래스
이 떄문에 모든 클래스는 다음과 같은 메소드를 기본으로 가지고 있다.
<접근 제어자> class class_name{
// 필드
// 메소드
// 생성자
}
public
과 default access
만 사용할 수 있다.extends
키워드를 통해 표현한다. 클래스는 오직 하나의 부모 클래스만implements
키워드를 통해 나타내며, 여러 인터페이스를 구현할 수 있다.TestClass testClass = new TestClass()
1. 선언 : TestClass testClass
에서 객체를 담을 변수의 이름과 객체의 타입을 지정한다.
2. 인스턴스화 : new
키워드는 객체를 생성할 때 사용하는 연산자이다.
3. 초기화 : new
키워드 다음에 객체를 초기화하는 생성자가 호출된다.
선언하는 단계는 단순히 객체의 래퍼런스를 담을 변수를 지정하는 단계이다. 따라서 초기화하지 않으면,
해당 변수는 아무것도 가리키지 않는 상태이다.
new
연산자는 새로운 객체를 메모리에 할당함으로서 클래스를 인스턴스화하며, 해당 메모리를 가리키는
래퍼런스를 반환한다. 또한 new
연산자는 객체의 생성자를 호출한다.
new
연산자를 통해 반환된 래퍼런스는 보통 래퍼런스 변수에 할당되지만, 변수에 할당하지 않고 객체를
바로 사용할 수도 있다.
ex) int height = new Rectangle().height;
TestClass.java
public class TestClass {
private int a;
private int b;
public TestClass(int a, int b){
this.a = a;
this.b = b;
}
public TestClass(int a){
this.a = a;
this.b = a;
}
}
Main.java
public class Main {
public static void main(String[] args) {
TestClass testClass1 = new TestClass(10,20);
TestClass testClass2 = new TestClass(30);
}
}
Main.class
'''
public static main([Ljava/lang/String;)V
L0
LINENUMBER 3 L0
NEW TestClass
DUP
BIPUSH 10
BIPUSH 20
INVOKESPECIAL TestClass.<init> (II)V
ASTORE 1
L1
'''
위 코드는 new
연산자를 통해 객체를 생성하는 과정 중 핵심적인 부분만을 발췌한 것이다.
명령어를 기준으로 설명하면 다음과 같다.
1. NEW TestClass
: constant pool에 있는 TestClass의 인스턴스를 생성하고 해당 객체의 주소를 stack에 push한다.
2. DUP
: stack의 top에 있는 데이터를 복사하여 stack에 push한다. 이 경우 객체의 주소가 복사된다.
3. BIPUSH 10
: 10을 stack에 push한다.
4. BIPUSH 20
: 20을 stack에 push한다.
5. INVOIKESPECIAL TestClass.<init> (II)V
: stack에서 2번 pop하여 생성자의 argument로 가져올 값들을 가져오고 1번 더 pop하여 생성자를 호출하기 위한 객체 래퍼런스를 가져온다. 그리고 생성자를 호출한다.
6. ASTORE 1
: stack에서 pop하여 Local variable array의 1번 자리에 저장한다. 이 경우 TestClass의 인스턴스의 래퍼런스가 저장된다.
다시 정리하자면 new
키워드는 메모리를 할당하여 인스턴스를 생성하고 해당 객체에 대한 초기화는 생성자 함수를 통해 이뤄진다.
public int calculateOperator(int a, int b){
// do the calculation here
}
메소드 정의의 요소는 6가지이며 다음과 같다.
1. Modifiers : public,private,protected와 같은 접근 지정자가 메소드 정의 가장 앞 단에 있다.
2. The return type : 접근 지정자 다음에 반환 값의 type을 지정한다.
3. The method name : 메소드 이름은 반드시 동사로 시작해야 하며 camelCase를 따른다.
4. The parameter list in parenthesis : ,
로 구분되는 파라미터 리스트
5. An exception list : throws
키워드를 사용하여 예외를 날릴 예외 리스트들
6. The method body : {}
안에 감싸진 영역이며, 내부 로컬 변수가 정의될 수 있다.
public class OverloadingClass(){
public void overloadFunc(int i){
}
public void overloadFunc(String s){
}
}
메소드는 이름이 동일하더라도 파라미터 리스트가 다르다면 동일한 이름의 메소드를 정의할 수 있다.
주의할 점은, return type은 메소드 오버로딩에서 신경쓰는 부분이 아니라는 것이다.
즉, 파라미터 리스트가 동일하고 return type이 다른 경우 compilation error가 발생한다.
JVM의 method area에 저장된다. 정확히 서술하자면, method를 만났을 때 해당 메소드의 클래스가 method area
에 있으면 거기서 method를 가져오고, 그렇지 않다면 클래스 파일의 바이트 코드를 method area에 로드 한 뒤
메소드를 가져온다.
method area에는 클래스의 메타 데이터가 저장된다. 이때 method에 대한 정보가 함께 저장된다. 따라서 메소드
를 정의한 뒤 해당 메소드를 호출할 때 method area에서 메소드를 불러올 것이다.
java compiler는 기본적으로 class의 생성자가 없으면 no argument인 default 생성자를 만들어 준다. 만약,
개발자가 정의한 생성자가 있다면, default 생성자는 생성되지 않는다. 그런데 default 생성자를 사용할 때
주의할 점이 있다. default 생성자는 superclass의 no argument 생성자를 호출한다. 그런데 superclass가
no argument 생성자를 가지고 있지 않다면 문제가 발생할 것이다. 따라서 default 생성자 사용시 이를 신경
써야 한다.
생성자 예시
public Bicycle(int startCadence, int startSpeed, int startGear){
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
생성자는 메소드와 유사해 보이지만, return type이 없고 생성자 이름이 클래스 이름과 동일하다는 점에서
메소드와는 다르다. 생성자는 함수 오버로딩과 유사하게 파라미터 리스트가 다른 여러 생성자를 정의할 수
있다. 여기서 파라미터 리스트는 파라미터의 개수와, type의 개수를 의미한다. 이 요소들이 일치하지 않으면
여러 생성자를 정의할 수 있다.
private 생성자는 인스턴스 생성이 무의미하거나, 싱글톤 패턴을 사용할 때 사용한다.
public class SubClass {
private SubClass(){}
public static void printA(){
System.out.println("hello world");
}
public static void printB(){System.out.println("hello world");}
}
위 경우 메소드가 모두 static이기 때문에 클래스의 인스턴스를 생성하는 것이 무의미하다.
public class SubClass {
private static SubClass subClass = new SubClass();
private SubClass(){}
public static SubClass getInstance(){
if(subClass == null){
subClass = new SubClass();
return subClass;
}
else{
return subClass;
}
}
public void printA(){
System.out.println("hello world");
}
}
생성자에 private으로 접근 지정자를 지정하여 외부에서 객체를 생성하지 못하도록 막는다.
그리고 오직 getInstance()
메소드를 통해서만 객체를 불러올 수 있도록 한다.
this
는 현재 오브젝트를 가리키는 reference 변수이다.
this
는 6개의 사용 경우가 있다.
argument로 받는 변수명이 동일한 경우, 인스턴스의 변수와 argument를 구분하기 위해 쓰인다.
public class SubClass {
private int a;
private int b;
public SubClass(int a, int b){
a = a;
b = b;
}
public void print(){
System.out.println("a : "+ a + " b : "+ b); // a : 0 b : 0
}
}
위 경우 인스턴스의 변수인 a
,b
는 동일한 이름을 가지고 있는 argument에 의해 가려지게 된다.
따라서 this
키워드를 사용하여 구분해야 한다. 만약, argument의 이름을 다른 것으로 지정하면
this
를 굳이 사용하지 않아도 된다. 그러나 this
를 사용하여 argument와 인스턴스의 변수명을
일치시킴으로써 코드의 의미를 명확하게 나타낼 수 있다.
현재 클래스 메소드 내부에서 현재 클래스의 메소드를 사용하면 compiler는 자동으로 해당 메소드의 앞에
this.
을 붙인다.
컴파일 전
public class SubClass {
void m(){}
void n(){
m();
}
}
컴파일 후
public class SubClass {
public SubClass() {
}
void m() {
}
void n() {
this.m();
}
}
this()
는 현재 클래스의 default 생성자를 호출한다. 이는 파라미터를 갖는 생성자에서
기본 생성자를 재사용할 때 사용된다. 반대로 기본 생성자에서 파라미터를 갖는 생성자를 호출할
수도 있다. this()
는 반드시 생성자의 가장 첫번째 줄에서 호출되어야 한다.
public class SubClass {
private int a;
private int b;
private int c;
public SubClass(int a, int b, int c){
this(a,b);
this.c = c;
}
public SubClass(int a, int b){
this.a = a;
this.b = b;
}
}
public class SubClass {
public void m(SubClass obj){
System.out.println(obj);
}
public void n(){
m(this);
}
}
여러 클래스에서 하나의 객체를 함께 사용할 때 유용하다. 아래 코드에서 A4 클래스는 생성자에서
B 클래스의 인스턴스를 생성하고 생성자로 자기 자신을 넘겨주고 있다.
이러한 방식으로 A4의 인스턴스를 다른 클래스 생성자의 argument로 넘겨줘, 여러 클래스에서
하나의 객체를 공유하도록 할 수 있다.
class B{
A4 obj;
B(A4 obj){
this.obj=obj;
}
void display(){
System.out.println(obj.data);//using data member of A4 class
}
}
class A4{
int data=10;
A4(){
B b=new B(this);
b.display();
}
public static void main(String args[]){
A4 a=new A4();
}
}
public class SubClass {
SubClass getSubClass(){
return this;
}
void print(){
System.out.println("SubClass print() method");
}
}
int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
int value, Node left, right를 가지고 있어야 합니다.
Node.java
public class Node {
private int value;
private Node left;
private Node right;
Node(int value){
this.value = value;
left = null;
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;
}
}
BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
BinaryTree.java
import java.util.*;
public class BinaryTree {
Node root;
BinaryTree(Node root){
this.root = root;
}
List<Integer> bfs(Node node){
List<Integer> result = new ArrayList<Integer>();
Queue<Node> queue = new LinkedList<>();
queue.add(node);
while(!queue.isEmpty()){
Node currNode = queue.poll();
result.add(currNode.getValue());
if(currNode.getLeft() != null){
queue.add(currNode.getLeft());
}
if(currNode.getRight() != null){
queue.add(currNode.getRight());
}
}
return result;
}
List<Integer> dfs(Node node){
List<Integer> result = new ArrayList<Integer>();
Stack<Node> stack = new Stack<>();
stack.push(node);
while(!stack.isEmpty()){
Node currNode = stack.pop();
result.add(currNode.getValue());
if(currNode.getRight() != null){
stack.push(currNode.getRight());
}
if(currNode.getLeft() != null){
stack.push(currNode.getLeft());
}
}
return result;
}
}
BinaryTreeTest.java
import org.junit.jupiter.api.Assertions;
import java.util.List;
class BinaryTreeTest {
@org.junit.jupiter.api.Test
void bfs() {
Node[] nodeList = new Node[10];
int[] expectedList = new int[10];
for(int i = 0; i < 10; i++){
nodeList[i] = new Node(i);
expectedList[i] = i;
}
nodeList[0].setLeft(nodeList[1]);
nodeList[0].setRight(nodeList[2]);
nodeList[1].setLeft(nodeList[3]);
nodeList[1].setRight(nodeList[4]);
nodeList[2].setLeft(nodeList[5]);
nodeList[2].setRight(nodeList[6]);
nodeList[3].setLeft(nodeList[7]);
nodeList[3].setRight(nodeList[8]);
nodeList[4].setLeft(nodeList[9]);
BinaryTree bTree = new BinaryTree(nodeList[0]);
List<Integer> result = bTree.bfs(nodeList[0]);
for(int i = 0; i < 10; i++){
Assertions.assertEquals(expectedList[i],result.get(i));
}
}
@org.junit.jupiter.api.Test
void dfs() {
Node[] nodeList = new Node[10];
int[] expectedList = new int[]{0,1,3,7,8,4,9,2,5,6};
for(int i = 0; i < 10; i++){
nodeList[i] = new Node(i);
}
nodeList[0].setLeft(nodeList[1]);
nodeList[0].setRight(nodeList[2]);
nodeList[1].setLeft(nodeList[3]);
nodeList[1].setRight(nodeList[4]);
nodeList[2].setLeft(nodeList[5]);
nodeList[2].setRight(nodeList[6]);
nodeList[3].setLeft(nodeList[7]);
nodeList[3].setRight(nodeList[8]);
nodeList[4].setLeft(nodeList[9]);
BinaryTree bTree = new BinaryTree(nodeList[0]);
List<Integer> result = bTree.dfs(nodeList[0]);
for(int i = 0; i < 10; i++){
Assertions.assertEquals(expectedList[i],result.get(i));
}
}
}
접근 지정자 : https://mainia.tistory.com/5574
클래스 정의 : https://www.javatpoint.com/class-definition-in-java
오라클 자바 튜토리얼 : https://docs.oracle.com/javase/tutorial/java/javaOO/classdecl.html
invokespecial : https://www.jrebel.com/blog/using-objects-and-calling-methods-in-java-bytecode
클래스 메소드 로딩 : https://blog.wanzargen.me/16
this keyword : https://www.javatpoint.com/this-keyword