[클래스] Abstract Class / 추상 클래스

Java는 클래스를 중심으로 객체들을 설계하는 언어이다. 바꿔 말하면 문제를 해결하기 위해 클래스를 설계하고, 이를 인스턴스화 하여 사용한다. 또한 Java를 사용할 때 의 장점 중(객체지향 프로

dev-whoan.xyz

인터페이스를 한 단어로 표현하면 '규약'이다. 개발하면서 꼭 지켜야 할 '규약'같은 존재로, 바꿔말하면 개발할 때 반드시 포함해야하는 멤버를 모아놓은것이 인터페이스다.
이렇게 말하면, 추상 클래스와 다른점이 무엇인지 헷갈릴 수 있다.
추상 클래스는 쉽게 말해 개발이 들어간 시점에서 공통된 속성을 모아놓은 클래스이며, 해당 추상 클래스를 '상속'하는 클래스는 해당 추상 메소드들을 모두 구현하여야 한다.
인터페이스는 이와 다르게 개발이 들어가기 전에 만들어질 메소드들에 대해 '규칙'을 정해놓은 것이고, 이를 '구현'하는 클래스들은 해당 메소드들을 모두 내용을 담아 만들어야 한다.


규칙? 그게 뭔데?

아직까지 이해가 안되는가?
그렇다면 경우를 들어 설명해 보자면, A와 B가 함께 자동차 객체를 생성하는 클래스를 만든다고 하자. 이 때, A는 자동차의 구동을 담당하고, B는 자동차의 프레임을 담당한다고 하자.
결국 자동차는 프레임과 구동부가 결합되어야 하는데, 만약 결합부가 일치하지 않는다면 설계에 실패했다고 볼 수 있고, 즉 자동차 객체를 생성 못한다고 볼 수 있다.
이를 위해 결합부에 대해 어떻게 설계하겠다는 약속을 정해야 하는데, 이를 인터페이스를 이용해 약속하는 것이다.


인터페이스 생김새

인터페이스는 이렇게 생겼다.

interface CarInterface{
    public void move();
    public void setSpeed(int speed);
}

CarInterface는 move와 setSpeed에 대해 설계 방식을 정하고 있고, 해당 인터페이스를 구현하는 클래스들은 두 함수에 대해 실질적인 내용을 담아야한다.
즉, 인터페이스는 함수내부가 어떻게 생겼을 지 모르겠지만, return 타입, 받아들일 파라미터 등 함수가 어떻게 생겼는지를 정해놓은 것이다.
따라서 A와 B가 같은 자동차를 설계하기 전에 인터페이스를 만들어 놓고, 결합부에 대해 약속을 정한다면 결합부가 일치하지 않아 자동차를 만들 수 없는 일은 발생하지 않을것이다.


왜 하필 인터페이스? 추상 클래스로는 안돼?

인터페이스가 가지는 추상클래스와의 가장 큰 다른점은 재사용성이다. 클래스는 다중상속이 불가능하지만, 인터페이스는 다중상속이 가능하다.

interface Vehicle{
    public void move();
}

interface WheelObjects{
    public void setWheels();
    public void getWheels();
}

interface Car extends Vehicle, WheelObjects{
    public void setHorsePower(float horsePower);
}

interface Airplane extends Vehicle{
    public void setMaha(float Maha);
}

위와 같은 다중상속이 가능해짐으로써, Car라는 인터페이스를 만들기 위해 Vehicle과 WheelObjects의 인터페이스를 상속받아 각 인터페이스가 갖는 규칙을 모두 가질 수 있는 것이다.
뿐만 아니라, Vehicle 인터페이스를 Car에만 사용하는 것이 아니라 Airplane이라는 움직이는 방법이 전혀 다른 이동수단이 상속받을 수 있게 설계할 수 있다.
만약 추상클래스였다면, Car가 갖는 속성들을 Airplane은 가질수 없는게 당연한것과는 반대되는 성질이다.
Java 8이 넘어가면서 추상클래스와 인터페이스의 관계는 많이 모호해지긴 했지만, 인터페이스의 특징을 잘 살리면 인터페이스 나름의 이용성이 큰 녀석이기 때문에 잘 이해해두면 좋은 친구다.

[자료구조] Java ArrayList 구현

 

[자료구조] Java ArrayList 구현

ArrayList는 크기가 고정되지않은, 즉 동적 배열을 의미한다. 배열(Array)와의 차이점으로 배열은 초기화할 때 크기를 지정해야 하지만, ArrayList는 그러지 않아도 된다. 즉 배열은 고정값의 크기를 가

dev-whoan.xyz

모든 데이터 및 알고리즘을 List  구조만 이용해서 최적화된 프로그래밍을 할 수 있으면 얼마나좋을까?

안타깝게도 그렇지 않기 때문에 세상에는 List를 제외한 많은 자료구조가 존재한다.

이번에는 Stack과 관련된 구현을 할 예정이다

Stack은 Last In First Out 구조를 갖는다. 이 LIFO 구조를 갖는 가장 흔한 예를 들어보면 많이 원기둥으로 된 초콜릿 통을 얘기한다. 하지만 우리 프로그래밍 세계에는 초콜릿 통만 만들지 않으니, 다른 예를 알아볼 필요도 있다.

이런 예는 어떨까?

구조선에 총 500kg의 인원을 태울 수 있다. 구조를 기다리는 사람들은 차례로 줄 서서 탑승을 기다리고 있다. 구조선이 도착지에 도착하면, 탑승구에 있는 사람부터 내린다. 구조선은 총 탑승 중량이 500kg을 넘어가면, 출발한지 조금 지나 침몰한다. 탑승할 때, 중량을 나타내는 전자저울이 있다.

무인도 예

해당 문제는 도착지에 도착하건, 중량초과를 하건, 제일 마지막에 탑승한 사람부터 내려야 한다. 이러한 경우 Stack을 이용해 구현할 수 있겠다.


Stack은 Last In First Out

Stack에 대해 이미지를 연상해보면 다음과 같이 생각하면 편하다.

stack 이미지

즉, 제일 위에서(top) 데이터를 삽입하거나, 데이터를 반환한다.

stack은 리스트처럼 1. 크기가 고정(배열) 2. 크기가 동적(ArrayList)의 성격을 가질 수 있고, 구현 방법은 그 성격과 같다.

크기를 고정시키고 싶다면 배열로 구현하고, 크기를 동적으로 변경하고 싶다면 ArrayList로 구현하면 된다. 오늘 게시글에서는 크기가 고정된 Stack을 구현해 보겠다.


Stack에는 어떤게 필요할까?

Stack은 어떤 기능이 필요할까? 기본적인 목적은 제일 위에서 데이터를 삽입, 반환할 수 있어야 한다. 부가적으로, 현재 Stack의 사이즈와 크기, 초기화, 그리고 비어있거나 꽉찼는지를 구할 수 있으면 된다.

따라서 제일 위를 구하는 top, 크기를 담는 capacity를 가지면 된다.

Stack에서는 ArrayList와 달리 add, remove라는 함수 이름을 사용하지 않고 각각 push, pop이라는 이름의 함수를 사용한다.


Stack 구현

따라서 Stack의 기본은 다음과 같다.

public class Stack<T> {
	int top;
	int capacity = -1;
	T[] stack;
	Stack(int capacity){
		this.capacity = capacity;
		stack = (T[]) (new Object[capacity]);
		top = -1;
	}
}

top을 -1로 초기화 하는 이유는 개발의 간편함을 위함이다. 따라서 스택이 비어있을때 top은 -1이 된다.

고정된 크기를 갖는 Stack을 구현하기 생성자는 capacity값을 전달해야 한다.

데이터를 삽입할 때는 Stack이 가득찼는지 확인해야 한다. 그리고 공간이 남아있다면 데이터를 삽입하면 된다.

public class Stack<T> {
	...
	public void push(T element) {
		if(isFull()) {
			System.out.println("Stack이 가득 찼습니다.");
			return;
		}
		
		stack[++top] = element;
	}
	
	public boolean isFull() {	return (this.top == this.capacity-1);	}
   	...
}

isFull(): 스택이 가득찼는지 확인한다.

데이터를 반환할 때는 삽입할 때와 반대로 Stack이 비어있는지 확인해야 한다. 그리고 비어있지 않다면 데이터를 반환하면 된다.

public class Stack<T> {
	...
	public T pop() {
		if(isEmpty()) {
			System.out.println("Stack이 비어있습니다.");
			return null;
		}
        
		return stack[top--];
	}
	
	public boolean isEmpty() {	return (this.top == -1);	}
   	...
}

isEmpty(): 스택이 비어있는지 확인한다.

그런데 우리는 반환하지 않고 데이터를 확인만 하고 싶을수도 있다. 이럴 때를 위해 peek()라는 함수가 존재한다. 이는 stack에서 데이터를 삭제하지 않고, top에 어떤 data가 존재하는지 확인을 가능하게 한다.

public class Stack<T> {
	...
	public T peek() {
		if(isEmpty()) {
			System.out.println("Stack이 비어있습니다.");
			return null;
		}
		
		return stack[top];
	}
	
	public boolean isEmpty() {	return (this.top == -1);	}
   	...
}

pop과의 차이점은 return (T) stack[top--]; 과 return (T) stack[top]; 이다.

pop의 경우 top을 하나 감소시켜 다음 데이터가 삽입될때, 반환한 데이터 위치에 새로운 데이터가 덮어씌워지지만, peek의 경우 top의 데이터 조작을 일체 하지 않음으로써 다음 pop 혹은 push를 수행할 때 기존과 같은 역활을 하게 하는 것이다.

이제 나머지 부가기능인 stack의 크기와 초기화 하는 함수를 만들어주면 된다.

public class Stack<T> {
	...
    public void clear(){
        if(isEmpty()){
        	System.out.println("Stack은 이미 비어있습니다.");
            return;
        }
        top = -1;
        stack = (T[]) (new Object[capacity]);
        System.out.println("Stack 초기화 완료!");
    }
    
    public int size(){
    	return (top+1);
    }
}

 


전체코드와 실행 결과

public class mainStack {
	public static void main(String[] args) {
		System.out.println("=====짧은머리 개발자=====");
		Stack<Integer> stack = new Stack<>(5);
		for(int i = 0; i < 5; i++) {
			stack.push((i+1));
			System.out.println(i + " 번째 peek: " + stack.peek());
		}
		System.out.println("===Pop===");
		for(int i = stack.size(); i > 0; i--) {
			System.out.print(i + " 번째 : " + stack.pop() + " | " );
		}
	}
}

public class Stack<T> {
	int top;
	int capacity = -1;
	T[] stack;
	public Stack(int capacity){
		this.capacity = capacity;
		stack = (T[]) (new Object[capacity]);
		System.out.println("size : " + capacity);
		top = -1;
	}
	
	public void push(T element) {
		if(isFull()) {
			System.out.println("Stack이 가득 찼습니다.");
			return;
		}
		
		stack[++top] = element;
	}
	
	public T pop() {
		if(isEmpty()) {
			System.out.println("Stack이 비어있습니다.");
			return null;
		}
		return stack[top--];
	}
	
	public T peek() {
		if(isEmpty()) {
			System.out.println("Stack이 비어있습니다.");
			return null;
		}
		
		return stack[top];
	}
	
	public void clear(){
        if(isEmpty()){
        	System.out.println("Stack은 이미 비어있습니다.");
            return;
        }
        top = 0;
        stack = (T[]) (new Object[capacity]);
        System.out.println("Stack 초기화 완료!");
    }
	public int size(){
    	return (top+1);
    }
	
	public boolean isEmpty() {	return (this.top == -1);	}
	public boolean isFull() {	return (this.top == this.capacity-1);	}
}

+ Recent posts