클래스

​ 클래스는 객체 지향 언어의 ‘객체’를 만드는 수단아며, 어떤 대상을 추상화하는 의미 단위이기도 합니다. 클래스의 구조는 필드, 생성자, 메서드로 이루어집니다. 필드나 메서드를 선언할 때는 public, priavte 같은 access modifier 로 공개 범위를 설정해 주어야 합니다.

​ 다음과 같이 키워드 new 를 사용하면 클래스의 객체가 생성되고 q1, q2 는 각각 생성된 객체를 reference 하게 됩니다.

Sample q1 = new Sample();
Sample q2 = new Sample();

자바 기초 문법

변수 타입, 상수

​ primitive type 에는 byte, short, int, long, float, double, char, boolean 이 있습니다. 그리고 primitive 를 제외한 모든 타입은 클래스입니다. 배열도 하나의 클래스입니다. 하지만 배열의 인스턴스를 어떻게 쓰는지는 알겠는데, 구현 클래스는 뭘까요? 자바에서 배열 클래스는 runtime 에 타입에 기초해 JVM 에 의해 동적으로 만들어지기 때문에 java library 에 없습니다. 클래스 이름은 다음과 같이 만들어집니다.

  • int[]: [I

    • int[] arr = new int[0];
      System.out.println(arr.getClass().getName()); // Output: [I
      
  • byte[]: [B

  • short[]: [S

  • long[]: [J

  • float[]: [F

  • double[]: [D

  • char[]: [C

  • boolean[]: [Z

  • Object[]: [L<fully_qualified_class_name>;

    • String[] strArr = new String[0];
      System.out.println(strArr.getClass().getName()); // Output: [Ljava.lang.String;
      

복사호출, 참조호출

​ 함수를 호출하는 방식에는 복사호출(Call-by-Value) 와 참조호출(Call-by-Reference) 가 있습니다. sample(x) 라는 함수가 있을 때 복사호출은 인자값으로 변수 x 의 값을 넘기고, 참조호출은 주소값을 넘깁니다. 복사호출에서는 값을 복사해서 사용하므로 원래 x 는 변하지 않습니다. 다음과 같습니다.

void twofold(int a){
	a = a * 2
}

int x = 5;
twofold(x);
System.out.println(x); //5

int x 를 복사호출로 2 를 곱해도 원래 있는 x 는 변하지 않습니다.

​ 반면 참조호출의 경우에는 함수가 수행될 때 해당값의 주소가 복사되어 넘겨집니다. 주소값은 실제 힙 영역에 접근하게 합니다. 그렇게 되면 함수 내에서 해당 값을 수정하면 원래 값에도 그대로 반영이 됩니다.

void deleteLast(int[] a){
	a[a.length - 1] = 0;
}

int[] y = {1, 2, 3};
deleteLast(y);
System.out.println(Arrays.toString(y)); //[1, 2, 0]

상속 기능

​ 상속은 sub class 가 super class 로부터 특성을 받는 것입니다. 다음과 같이 사용할 수 있습니다,

public class InheritedStack extends LinkedList {
	public InheritedStack(){
		super();
	}
	...
	public E pop(){
		E x = get(0);
		remove(0);
		return x;
	}
	
	public E top(){
		return get(0);
	}
	
	public void popAll(){
		clear();
	}
}

top() 메서드는 상위 클래스의 get() 메서드를 사용하고 있습니다. 이 때 super 를 사용해 다음과 같이 상위 클래스에 있음을 명시적으로 지정해도 됩니다.

public E top(){
		return super.get(0);
	}

​ 자바에서 상속은 상위 클래스를 하나만 명시할 수 있습니다. 하지만 상위 클래스가 다른 어떤 클래스를 상속받았으면 상속의 계보가 이어집니다. 즉, 상속의 조상이 두 갈래로 갈라질 수는 없지만 한 갈래로는 제한없이 상속받을 수 있습니다.

​ 클래스 Object 는 모든 클래스의 상위 클래스입니다. 예시에서 LinkedList 의 상위 클래스가 없다고 가정하면, 실제 상속 계보는 Object -> LinkedList -> InheritedStack 으로 이어집니다.

인터페이스

​ 자바에서 클래스를 추상적으로 묘사하는 방법은 interface 와 abstract 가 있습니다. 이중 interface 는 전체를 추상적으로 묘사하는 방식으로 ADT(추상 데이터 타입) 과도 잘 어울립니다. 예를 들어 아래와 같이 만들 수 있습니다.

public interface A {
	void insert(String x);
	Object search(String x);
	void remove(String x);
}

​ 인터페이스는 상속과 달리 둘 이상의 인터페이스를 구현할 수 있습니다. 둘 이상의 인터페이스를 구현할 때 다음과 같이 나열하면 됩니다.

public class BST implements A, B {
	...
}

Comparable

​ Comparable 은 객체 간의 비교를 위한 인터페이스입니다. 다음과 같이 선언되어있습니다.

public interface Comparable<T> {
    
    public int compareTo(T o);
}

각 클래스는 Comprable 의 compareTo 를 구현해서 사용합니다. Comparable 은 java.lang 에 있는 인터페이스로, 대소 비교가 가능한 클래스면 사용할 수 있습니다. 자바에는 150여 개의 비교가능한 기본 클래스가 있습니다. 예를 들어 Wrapper Class 인 Float 를 보겠습니다.

public final class Float extends Number implements Comparable<Float> {
	...
	public int compareTo(Float anotherFloat) {
        return Float.compare(value, anotherFloat.value);
    }
    
    public static int compare(float f1, float f2) {
        if (f1 < f2)
            return -1;           // Neither val is NaN, thisVal is smaller
        if (f1 > f2)
            return 1;            // Neither val is NaN, thisVal is larger

        // Cannot use floatToRawIntBits because of possibility of NaNs.
        int thisBits    = Float.floatToIntBits(f1);
        int anotherBits = Float.floatToIntBits(f2);

        return (thisBits == anotherBits ?  0 : // Values are equal
                (thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
                 1));                          // (0.0, -0.0) or (NaN, !NaN)
    }
    ...
}

​ Float 클래스는 compareTo 를 통해서 overloading 된 다른 compareTo 를 호출하고, 비교합니다.

​ 논리적으로 Comparable 의 Generic 에 다른 타입을 선언하여 다른 타입의 객체와 비교도 가능하지만, 일반적으로 같은 타입끼리 비교합니다. 또한 Comparable 을 사용하지 않고 다음과 같이 비교할 수도 있습니다.

a.value < b.value

이는 클래스 안의 비교할 기초타입을 public 으로 열어놓은 다음 비교하는 방법입니다. 하지만 이런 방식은 캡슐화의 원칙과 어울리지 않습니다.

제네릭

​ 제네릭은 필드, 메서드의 타입을 객체가 선언될 때 지정하는 것으로, 범용성을 제공하면서 타입도 강력하게 관리하기 위해 설계됩니다. 다음과 같이 사용합니다.

public interface InterfaceA<E, T>{
	void insert(E x);
	public T search(E x);
	public void remove(E x);
}

public class SamlepleClass implements InterfaceA<Float, TreeNode>{
	void insert(Float x);
	public TreeNode search(Float x);
	public void remove(Float x);
}

프로그램 수행

main() : 수행 시작점

​ 프로그램 수행은 메서드 main() 에서 시작됩니다. 아래와 같이 접근 제어자와 키워드, 반환 형식, 메서드 이름, 인자가 모두 맞아야 합니다.

public static void main(String[] args) {
	...
}

에러 처리 구조

​ Exception 은 throws 로 밖으로 던지거나 try~catch 로 처리할 수 있습니다.

public class Heap {
	public Heap(int n){
		item = new int[n];
		size = 0;
		maxHeap = n;
	}
	
	class HeapException extends Exception {
		public HeapException (String msg){
			super(msg);
		}
	}
	
	public void insert(int newItem) throws HeapException {
		if(size == maxHeap){
			throw new HeapException ("Overflow in insert()");
		}else{
			/* 정상수행 */
		}
	}
	
	public static void main(String[] args){
		Heap h = new Heap(3);
		try{
			h.insert(1);
			h.insert(10);
			h.insert(20);
			h.insert(30); //예외 발생
		}catch(HeapException ex){
			System.out.println("HeapException: " + ex.getMessage());
		}
	}
}

​ size 가 maxHeap 이랑 같아지면 Custom Exception 인 HeapException 을 던집니다. main 메서드에서는 catch 로 받아서 sout 을 합니다.

​ 자바의 모든 Exception 은 Exception 클래스의 하위 클래스입니다. 즉, Exception 을 상속합니다. 자바에서는 NullPointerException, IndexOutOfBoundsException 등 미리 만들어놓은 Exception 클래스가 있습니다.

​ 위 예시에서 굳이 catch 로 하지 않아도 배열의 범위를 넘어서는 접근이므로 IndexOutOfBoundsException 이 발생됩니다. 따라서 IndexOutOfBoundsException 을 catch 문에서 사용해도 해당 에러를 잡을 수는 있습니다. 하지만 이렇게 미리 준비해놓은 예외를 이용하는 방식은 피하는 것이 좋습니다. 왜냐하면 다른 곳에서 다른 원인으로 IndexOutOfBoundsException 이 발생할 수 있는데, catch 문에 걸려서 처리되어버리기 때문입니다.

메모리 사용 관행 : 정적/동적 영역, GC

​ JVM 은 할당받은 메모리를 정적 영역과 동적 영역으로 나누어 관리를 합니다. 큰 틀에서 아래 그림과 같습니다. image-20230514002713517

​ 정적 영역은 프로그램 코드와 정적 변수(Static Variable), 전역 변수 등을 포함합니다. 프로그램이 끝날 때까지 없어지지 않습니다. Method Memory 라고도 합니다.

​ 반면 메서드는 한시적으로 호출되므로 스택에 공간을 할당받은 다음 적재됩니다. 스택 영역은 이름처럼 FILO 구조입니다. 임의의 메서드가 호출되어 수행을 시작하면 스택 영역에 해당 메서드가 호출되면서 넘겨받은 파라미터, 지역 변수 등을 위한 공간이 할당됩니다. 또한 모든 메서드는 스택 공간에 쌓이지만 main() 메서드만 별도로 정적 영역에 적재됩니다.

​ 힙 영역에는 수행 중에 생성된 객체가 저장되며 GC 의 청소 대상이 되는 공간입니다. 예를 들어 다음과 같은 메서드를 호출한다고 가정하겠습니다.

public class Main {

    int size;

    public Main(int size){
        this.size = size;
    }

    public int add(int n) {
        return n + size;
    }

    public static void main(String[] args) {

        Main main = new Main(3);
        main.add(5);

    }
}

객체의 reference 인 main 은 스택 영역에 저장되고, 객체는 힙 메모리에 저장됩니다. add 메서드가 호출되면 스택 영역에 add 실행을 위한 스택 영역이 할당되고 파라미터와 리턴값 저장합니다. 메서드가 종료되고 이 객체를 가리키고 있는 reference 가 없어집니다. 따라서 힙 영역에 있는 이 객체의 메모리는 GC 의 대상이 됩니다.

​ 자바는 병렬 처리를 위한 스레드를 제공하는데, 이들은 독립적으로 수행되기 때문에 각각의 스택 영역을 할당받습니다. 즉, 스레드는 독립적인 스택 공간이 1개씩 존재합니다.

댓글남기기