티스토리 뷰

자바

4-5장 클래스와 객체 , 상속

sorikiki 2021. 10. 7. 01:07

✅ 객체 지향 언어로서 자바 언어의 특징 세 가지

💛 객체와 캡슐화 💫

❓ 캡슐화란 무엇인가

캡슐화: 객체를 캡슐로 싸서 내부를 보호하고 볼 수 없게 하는 것

=> 객체 지향 언어인 자바에서 캡슐은 곧 '클래스'

=> 자바에서는 C++와 달리 어떤 변수나 함수도 클래스 바깥에 존재할 수 없으며 클래스의 멤버로 존재하여야 한다.

=> 객체는 캡슐화가 기본 원칙이만, 몇 부분만 공개 노출하는데 보통 멤버 변수인 필드를 보호하고 멤버 함수인 메소드를 노출한다.

 

Q. 멤버들을 어떻게 보호하고 공개하는가? by (멤버의) 접근 지정자 

공개 범위 좁은 순 : private -> default -> protected -> public

접근 가능 영역: 클래스 내 -> 동일 패키지 내 -> 동일 패키지와 자식 클래스 -> 모든 클래스

 

🤔 바로 클래스의 접근 지정자도 알아보자. (private x, protected x)

◽ public 클래스: 패키지에 상관없이 다른 어떤 클래스에게도 사용이 허용된다. 패키지 내부 한 파일에는 public class가 하나만 올 수 있다. 보통 public 클래스 안에 main 메소드가 하나 오고, public 클래스에 종속되는 다른 클래스에는 main 메소드는 오지 않는다.

❗ 한 클래스에 메인 메소드는 최대 1개이다. 

 

◽ 디폴트 클래스(접근 지정자 생략): 같은 패키지 내의 클래스들에게만 사용이 허용된다.

👉 다른 파일이라도 같은 패키지 안에 있다면 클래스명을 중복할 수 없다는 의미이다. 

👉 어떤 패키지의 파일에서 다른 패키지에 있는 public 클래스에 접근하려고 할 때, import 패키지명.클래스명; 을 상단에 입력해주어야 한다. 타 패키지에서 다른 패키지에 있는 default 클래스에는 import문을 쓰더라도 컴파일 오류, 즉 접근이 불가능하다. 

디폴트의 경우, 접근 지정자가 생략이 되어있는 만큼 문제를 풀 때 주의를 해야한다. 

 

Q. 객체는 어떻게 생성하는가

i) 레퍼런스 변수 선언 ex. Circle pizza;

ii) 특정 클래스에 대한 객체 생성 ex. pizza = new Circle(); // 바로 이때 Circle 타입 크기의 객체 메모리가 할당되어 객체가 생성된다고 말할 수 있는 것이고, 레퍼런스 변수가 메모리에 할당된 객체에 접근가능하게 된 것이다.

iii) 이후 객체의 멤버에 접근

👉 레퍼런스: 객체에 대한 주소. 레퍼런스 변수가 특정 객체를 가리키게 되고 그러면 메모리에 있는 그 객체의 주소를 담게 된다. 레퍼런스 변수만 일단 생성하면 null로 초기화된다.  vs C++: Circle pizza 만으로 객체가 생성됨

👉 객체 생성시, 내부적으로 발생하는 것들: Circle 타입 크기의 메모리 할당 -> Circle() 생성자 코드 실행(1번 호출)

👉 객체가 생성되는 위치는 객체의 타입인 클래스 내부의 메인 메소드가 될 수도 있고, 혹은 다른 클래스의 메인 메소드가 될 수도 있는데... 후자의 경우에는 클래스 접근 지정자와 생성자의 접근 지정자 등을 고려해야 한다.

 

💛 상속 💫

상속: 자식 클래스가 부모 클래스의 속성을 물려받고 기능을 추가하여 확장(extends)하는 개념

=> 자식 클래스는 서브 클래스, 부모 클래스는 슈퍼 클래스

=> 상속의 장점: 슈퍼클래스의 필드와 메소드를 물려받아 코드를 재사용함으로써, 코드 작성에 드는 시간과 비용을 줄인다.

=> 슈퍼 클래스의 멤버를 그대로 물려받고 싶지 않을 수도 있음. 여기서 다형성의 개념이 등장함 !

슈퍼클래스의 필드를 서브 클래스에서 오버라이딩(덮어쓰기)하여 초기화하는 경우에는 반드시 생성자 내부에서 초기화되어야 한다. 교재에서는 필드의 오버라이딩에서 잘 안다루기는 하는데...

 

⭐⭐⭐⭐⭐⭐ 오버라이딩과 오버로딩의 차이점

💛 다형성(Polymorphism) 💫

다형성: 같은 이름의 메소드가 클래스 혹은 객체에 따라 다르게 동작하도록 구현하는 것

(메소드) 오버라이딩: 슈퍼클래스에 구현된 메소드를 서브 클래스에서, 즉 상속관계에서 동일한 이름으로 자신의 특징에 맞게(동일한 원형을 지키되) 다시 구현하는 것

 

👉 가끔 서브 클래스에서 오버라이딩 메소드를 작성하려다가, 슈퍼 클래스의 메소드의 원형과 다르게 작성하는 상황이 생길 수 있다.(이름은 같은데 매개변수의 타입이나 개수를 다르게 한다던가, 이 경우에는 오버로딩으로 취급) 이렇게 되면 클래스 외부에서 호출할 때 동적바인딩이 일어나지 않을 수 있다. 오버라이딩은 런타임시 발생하는 게 원칙이지만, 이러한 실수를 방지하기 위해 컴파일 시에서 오류를 쉽게 발견할 수 있도록 오버라이딩할 메소드 위에 @Override라는 annotation을 앞에 붙이는 선택을 할 수도 있다.

 

(메소드) 오버로딩: 클래스 내에서 혹은 상속 관계에서, 이름이 같지만 / 서로 다르게 동작하는(이름 말고는 매개변수의 개수, 타입 중 하나라도 달라야 함) 메소드를 여러 개 만드는 것 

👉 메소드 오버로딩은 클래스 내에서 여러 개의 생성자를 구현하는 것도 역시 오버로딩이라고 할 수 있다.

 

Q. 이 외에도 오버라이딩과 오버로딩의 차이점?

◽ 구현 방법

오버라이딩: 메소드의 이름, 매개변수의 타입과 개수, 리턴 타입이 모두 동일하여야 성립

오버로딩: 메소드 이름은 반드시 동일하고, 매개변수의 타입이나 개수가 달라야 성립. 리턴 타입은 상관 없음.

◽ 바인딩

오버라이딩: 동적바인딩(실행 시간에 오버라이딩된 메소드 찾아 호출) + 업캐스팅된 레퍼런스 변수라할 지라도, 동적바인딩에 의해 슈퍼 클래스의 메소드가 호출이 된후 서브 클래스의 오버라이딩된 메소드가 최종적으로 실행됨.

상속관계에서 생성자의 호출 및 실행 순서와 상속 관계에서 업캐스팅된 레퍼런스 변수의 메소드의 호출 및 실행 순서를 구분할 것

👉 이 경우, 동일한 이름의 슈퍼 클래스의 메소드는 호출이 되어도, 실행은 되지 않는다. 슈퍼 클래스 메소드 호출 -> 서브 클래스 메소드 호출 -> 서브 클래스 메소드 실행

vs  서브 클래스 생성자 호출 => 슈퍼 클래스 생성자 호출 by super() => 슈퍼클래스 생성자 실행 => 서브 클래스 생성자 실행

👉 컴파일 시에는 레퍼런스가 가리키는 객체 안에서 레퍼런스 변수의 타입의 해당 메소드가 존재하는 지만 판단하고, 런타임에 동적 바인딩이 일어나는 것이다!

오버로딩: 정적 바인딩(호출될 메소드는 컴파일 시에 결정)

 

💙 아래 코드가 컴파일 에러인 이유

public void equals(Object o) {
		Person p = (Person)o;
		
		if(this.name == p.name && this.address == p.address) {
			System.out.println(this + "와" + p + "는 같습니다.");
		} else {
			System.out.println(this + "와 " + p + "는 같지 않습니다.");
		}
	}

👉 오버라이딩과 오버로딩은 모두 메소드의 이름이 동일하다는 공통점이 있다. 위에서 equals 메소드의 리턴타입이 boolean에서 void로 바뀜에 따라 에러가 발생하였는데, 오버로딩은 리턴타입이랑 관계없으므로 리턴타입만 바꿀 경우 컴파일러는 동일한 이름의 오버라이딩을 했다고 생각한다. 그런데 오버라이딩은 리턴타입을 바꾸면 컴파일 에러를 발생시킨다. 즉, 상속 관계에서 동일한 이름의 메소드와 매개변수의 타입과 개수까지 일치한다면 이는 @annotation없이도 오버라이딩으로 간주되는 것이다.

 

💙 20 족보: equals()메소드를 오버로딩한 사례 💫

❗ System.out.println()의 매개변수 자리에는 void 타입을 리턴하는 객체를 넣을 수 없다.

원하는 결과

public class bitShift {

	public static void main(String[] args) {
		Person p1 = new Person("김숙명", "서울시 용산구");
		Person p2 = new Person("정순헌", "부산시 해운대구");
		Person p3 = new Person("박명신", "인천시 연수구");
		Person p4 = new Person("김숙명", "제주도 서귀포시");
		
		System.out.println(p1.equals(p4));
		System.out.println(p2);
		System.out.println(p3);
	}
	
}

class Person {
	String name;
	String address;
	int number;
	static int count = 0;
	
	Person(String name, String address) {
		this.name = name;
		this.address = address;
		count ++;
		this.number = count;
	}
	

	// return 타입을 boolean이 아니라 String으로 해주기 위해
	// 슈퍼클래스의 equals() 메소드를 오버라이딩이 아니라 오버로딩한 것.
	// 오버로딩하려면 매개변수의 타입을 다르게 해야하니 Object o대신에 Person p로 바꿔주었다. 
	public String equals(Person p) {
		
		if(this.name == p.name && this.address == p.address) {
			return this + "와" + p + "는 같습니다.";
		} else {
			return this + "와 " + p + "는 같지 않습니다.";
		}
	}
	
	@Override
	public String toString() {
		return "[" + this.number + "] 이름: " + this.name + ", 주소: " + this.address; 
	}
}

👉 지금까지 객체 지향 언어로서 자바 언어의 세 가지 특성을 알아보았는데, 객체 지향 언어의 목적을 정리하면 이는 소프트웨어의 재사용성을 높이기 위함이라고 할 수 있다. (교재에서 설명하듯, 코드의 재사용과 가장 관계 깊은 객체 지향 특성은 셋 중 상속이 가장 두드러진다고 할 수 있다.)

✅ 생성자

💛 생성자와 특징

생성자: 객체가 생성될 때 객체의 초기화를 위해 자동으로 호출되는 메소드

특징

1) 생성자의 이름은 클래스 이름과 반드시 동일하다.

2) 생성자는 여러 개 작성할 수 있다 => 앞에서 배운 오버로딩

3) 생성자는 new를 통해 객체를 생성할 때 자동으로 한 번만 호출된다.

4) 생성자에 리턴 타입을 지정할 수 없다. 따라서 클래스명 앞에 리턴 타입이 오지 않고, 리턴 타입이 오지 않는다고 해서 void를 리턴 타입으로 지정할 수 없다.

5) 생성자의 목적은 객체가 생성될 때 필요한 초기 작업을 위함이다.

6) 생성자에도 접근지정자를 쓸 수 있다. java.lang.Math 클래스로 예를 들면, Math 클래스의 생성자인 Math()의 접근지정자는 private으로 선언되어 있어 객체를 생성할 수 없다.

 

👉 정리하면, 생성자의 형태는 접근 지정자 + 클래스명 + constructor() { } // static, final, abstract 등은 당연히 x

👉 about 3) 즉, 생성자는 호출하고 싶을 때 아무 때나 호출할 수 있는 메소드가 아니다. 단, 최초로 호출되는 생성자에서 해당 클래스 내의 다른 생성자를 호출할 수는 있다. this() 설명 부분을 참고할 것.

👉 about 4) 리턴 타입은 지정할 수 없으나, 생성자 body 내에서 언제든 실행을 중단하고 싶을 때, 즉 메소드의 실행을 끝내고 호출한 곳으로 돌아가는 명령으로 return 문은 사용할 수 있다.

 

💛 기본 생성자

기본 생성자 매개변수와 실행코드가 없어 아무 일도 하지 않고 단순 리턴하는 생성자. 디폴트 생성자라고도 부름.

클래스의 생성자가 하나라도 없을 경우 자동으로 삽입되는 생성자임.

class Circle {

   public Circle() { } // 기본 생성자

}

특징

- 생성자는 반드시 한번 호출되어야 하기에, 클래스 내부에 생성자가 단 한개도 없다면 컴파일 오류가 날 듯하지만, 그렇지 않다.

- 생성자가 하나도 없다고 했을 때, 만일 메인 메소드에서 매개변수 없이 new를 사용하여 객체를 사용할 경우, 컴파일러에 의해 클래스 내부에 기본 생성자가 생성되기에 컴파일 에러가 아니다. 단, 자동 삽입된 기본 생성자는 사용자의 눈에는 보이지 않는다. 단 매개변수가 있는 상태로 new를 사용하여 객체를 사용할 때에는 컴파일 에러이다. 

- 그러나, 생성자가 하나라도 존재하는 클래스에는 컴파일러가 기본 생성자를 자동으로 삽입해주지 않는다. 만일 클래스 내부에 매개 변수를 하나 받는 생성자 한 개만 존재한다면, 매개변수 없이 new를 사용하여 객체를 생성하면 컴파일 오류이다. 

 

정리하면, 자바에서 컴파일러는 클래스 내부 생성자가 단 한개도 없을 경우에 기본 생성자 1개를 자동 삽입해준다. (출제 가능성 있어보임)

 

💛 this vs this()

this: 객체 자신에 대한 레퍼런스. 보다 정확히 말하면 현재 실행되고 있는 메소드가 속한 객체에 대한 레퍼런스

this의 필요성

1. 생성자에서 매개변수의 이름을 멤버 변수와 같은 이름으로 붙이고자 할 때, 멤버 변수 앞에 this 키워드를 붙여준다. (붙여주지 않으면 멤버 변수에 접근하지 못한다.)

2. 메소드가 객체 자신의 레퍼런스를 리턴해야 하는 경우

 

this(): 클래스 내에서 생성자가 다른 생성자를 호출할 때 사용

this() 사용 시 주의점

1. this()는 반드시 생성자 코드 내에서만 호출할 수 있다. 

2. this()는 반드시 같은 클래스 내 다른 생성자를 호출할 때 사용가능하다. 따라서 매개변수의 타입과 매개변수가 일치하는 생성자가 반드시 존재해야한다.

3. this()는 반드시 생성자의 첫 번째 문장이 되어야 한다. 

👉 상속에서 나오는 super()의 경우도, 1번과 3번의 특징을 가지고 있다. 2번의 내용을 super()에 적용하면, super()는 자신의 슈퍼 클래스 내 생성자를 호출할 때 사용된다. 서브 클래스의 생성자는 슈퍼 클래스의 생성자 역시 호출시키는 원칙적인(?) 의무를 가지고 있는데, 만일 명시적으로 슈퍼 클래스의 생성자를 지정하지 않을 경우를 대비하여 컴파일러가 자동으로 슈퍼 클래스의 기본 생성자 super()을 호출하도록 컴파일한다는 특징이 있다. 

👉 기본 생성자를 컴파일러가 자동으로 만들어준 것과 마찬가지로, 개발자의 명시적 지시가 없으면(기본 생성자의 경우 조건은 '생성자가 한 개도 없으면'이었음) 서브 클래스의 생성자가 기본 생성자이든 매개변수를 가진 것이든, 기본 생성자 super() 하나가 자동으로 만들어진다. 이때 슈퍼클래스에 기본 생성자가 정의되어 있지 않는다면, 이는 컴파일 오류이다. 생각의 전환을 해서 개발자가 슈퍼 클래스에 대한 생성자를 명시적으로 지정했다면, 그에 대응하는 생성자가 슈퍼 클래스에 존재하지 않을 때 컴파일 오류를 발생시킬 것이다.

 

🤔 this()와 super() 모두 생성자 내에서 첫 줄에 와야하는데, 이 둘은 함께 올 수 없다. 둘 다 써야하는 상황에서 그러면 어떻게 해야할까? 

=> 서브 클래스의 한 생성자에서 this()와 super()은 각각 자기 클래스 내 또 다른 생성자 호출을 하고 슈퍼 클래스의 생성자를 호출하려고 하는 것이다. 이 둘은 하나의 서브 클래스의 생성자에서 양립불가능하므로, 자기 클래스 내 또 다른 생성자에서(즉 this()를 먼저 쓴 후) 그 생성자 첫 줄에 super()을 써서 슈퍼 클래스의 생성자를 호출해준다면 충돌을 피해줄 수 있을 것이다.

 

💙 this()와 super()의 혼용

class Parent {
	
	Parent() {
		System.out.println("Parent의 기본 생성자 실행");
	}
	
	Parent(String name) {
		System.out.println("Parent의 매개변수 1개 생성자 실행");
	}
	
	Parent(String name, int age) {
		System.out.println("Parent의 매개변수 2개 생성자 실행");
	}
	
}

class Child extends Parent {
	String name;
	
	Child() {
		super("dasol", 24);
		System.out.println("Child의 기본 생성자 실행");
	}
	
	Child(String name) {
		this();
		this.name = name;
		System.out.println("Child의 매개변수 생성자 실행");
	}
}

public class Constr {
	
	public static void main(String[] args) {
		Child c = new Child("dasol");

	}

}

👉 순서: Child 매개변수 생성자 호출 => Child 기본 생성자 호출 => Parent 매개변수 2개 생성자 호출 => Parent 매개변수 2개 생성자 실행 => Child 기본 생성자 실행 => Child 매개변수 생성자 실행

 

✅ 객체 배열

객체 배열: 객체에 대한 레퍼런스를 원소로 갖는 배열 

 

//  < Basic Flow >

Circle  [] c; // Circle 배열에 대한 레퍼런스 변수 c 선언. 배열에 대한 레퍼런스 변수 선언 시에는 ,배열의 원소 개수를 지정해서는 안된다.

c = new Circle[5]; // 레퍼런스 배열 생성. c는 배열의 주소를 갖게 됨. 



for (int i=0; i<c.length; i++) {

  c[i] = new Circle(i); // 배열의 각 원소 객체 생성. 각각의 원소가 객체에 대한 레퍼런스, 주소를 갖게 함. 즉 배열의 원소는 객체가 아니라 레퍼런스이다. 

}

for (int i=0; i<c.length; i++) {

  System.out.println(c[i].멤버);

}

 

✅ 메소드에서 인자의 전달

메인 메소드에서 함수의 매개변수 자리에 primitive 타입(소문자로 시작하는 타입)이 들어가냐 혹은 unprimitive 타입(대문자로 시작하는 타입)이 들어가냐에 따라 메소드에 전달되는 양상이 달라진다!

 

◽ primitive 타입: 호출자가 건네는 값이 매개변수에 복사되어 전달. 따라서 메소드 내에서 건네받은 매개변수의 값을 변경하더라도, 원래 호출한 자리에 있던 값이 변경되지 않는다.

 

◽ unprimitive: 객체나 배열이 전달되는 경우라고 할 수 있다. 이러한 경우에는 객체 또는 배열 자체가 아니라, 이것의 레퍼런스가 메소드에 전달된다. 따라서 메소드 내에서 건네받은 레퍼런스를 바탕으로 멤버에 접근할 경우 객체의 필드나 배열의 원소 값을 변경할 수 있다.

 

👉 이처럼, 아무리 큰 객체나 배열도 하나의 정수 크기인 레퍼런스만 전달되므로, 매개 변수 전달로 인한 시간이나 메모리의 오버헤드가 없는 장점이 있다. 하지만, 메소드에서 전달받은 객체의 필드나 배열의 원소 값이 변경될 수 있다는 점을 감안해야 한다

 

✅ static 멤버

static과 함께 지금까지 배운 modifier를 정리해보면 static / final / abstract / public, protected, default, private 이고 이 뒤에는 리턴타입과 함수명이 잇따라 온다.

 

💛 static 멤버의 특징

1) 멤버 변수나 멤버 메소드 앞에 'static' 키워드를 붙이면 클래스 멤버 혹은 클래스 메소드가 된다.

2) non-static 멤버가 new를 이용하여 객체 생성 후에 생성되는 것과 달리, static 멤버는 객체 생성 전에 생성되어 접근이 가능하다.

👉 static 멤버가 생성되는 시점은 JVM에 따라 다를 수 있지만 일반적으로 static 멤버가 포함된 클래스가 로딩될 때이다. 따라서 레퍼런스 변수 선언 전부터 다음과 같이 '클래스명.static멤버'로 바로 접근할 수 있다.(컴파일 에러 아님!!!)

ex.

StaticSample s;

s.m = 10;

3) 마찬가지로, 객체가 사라지면 non-static 멤버는 사라지지만 static 멤버는 사라지지 않는다.

4) non-static 멤버는 객체마다 별도로 존재하여 여러 개이지만, static 멤버는 클래스 당 하나여서 동일한 클래스의 모든 객체들이 공유한다. 

 

💛 static 멤버에 접근하는 방법

1) 객체.static멤버 (이전에 배웠던 내용 그대로 non-static 필드와 메소드에 접근하는 방법과 유사)

👉 아래 그림을 통해 객체를 통해 static 멤버에 접근함으로써 객체와 객체가 공유하는 클래스 메모리의 내용이 어떻게 변경되는지 확인해보자.

 

2) 클래스명.static멤버

당연한 얘기이겠지만, non-static 멤버는 클래스명으로 접근할 수 없음을 유념하자.

 

💛 static 멤버의 필요성

: 모든 클래스에서 접근 가능한 전역 변수와 호출할 수 있는 전역 함수의 필요성 -> static 멤버를 가진 클래스!

ex. java.lang.Math => (int)(Math.random() * 100 + 1) // 1부터 100까지 중 랜덤으로 정수 하나를 가져올 때

 

💛 static 메소드의 제약조건 💫

static 멤버가 객체 생성 전부터 존재한다는 사실을 감안하면...

1) static 메소드 안에서 non-static 멤버에 접근할 수 없다. (단, non-static 메소드 안에서는 static 멤버에 접근할 수 있다.)

❗❗❗❗❗ main 메소드는 static 메소드인 것에 주의하자.

public class ClassInter {
	
	public static void main(String args[]) {
		f1();
		f2(); // ❌, static 메소드 안에서 non-static 메소드 호출 불가
	}
	
	static void f1() {
		
	}
	
	void f2() {
		f1(); // ⭕, non-static 메소드 안에서 static 메소드 호출 가능 
		f3(); // ⭕, non-static 메소드 안에서 non-static 메소드 호출 가능 
	}
	
	void f3() {
		
	}
}

2) static 메소드 안에는 this를 사용할 수 없다. this는 메소드가 속한 객체에 대한 레퍼런스이기 때문이다. 

 

static 멤버는 자신의 위치, 클래스와 불가분의 관계라는 사실을 감안하면...

1) static 멤버는 abstract와 양립할 수 없다! abstract는 일부의 기능을 미완성시키고 서브 클래스로 하여금 그 구현을 마치는데 목적이 있기 때문이다. 

2) 마찬가지로, 상속 관계에서 static으로 선언된 메소드는 서브 클래스에서 오버라이딩 할 수 없다. 아래 서브 클래스에서 오버라이딩할 수 없는 case가 나옴. 

 

🤔 static 멤버 앞에 private 제어자를 써도 될까? Yes! static 멤버는 모든 객체가 공유하기만 하면 되기에 반드시 클래스 외부에 공개되지 않아도 된다. 

 

✅ final

final 키워드는 클래스 앞, 메소드 앞, 필드 앞에서 모두 사용된다.

 

◽ final 클래스

: final이 붙은 해당 클래스는 상속할 수 없음. 당연히 abstract와 양립불가능할 것이다.

 

◽ final 메소드

: 오버라이딩할 수 없는 메소드. 즉 부모-자식 클래스 간 다형성을 구현할 수 없다는 얘기이지, 자식 클래스 타입의 객체가 해당 메소드에 접근할 수 없다는 얘기가 아니다. 

remind - 오버라이딩은 클래스의 상속 관계에서 등장하는 개념!

 

◽ final 필드: final 필드를 선언하면 상수가 된다. public static final을 세트로 쓰면 프로그램 전체에서 사용할 수 있는 상수가 된다. public static final이 붙은 상수는 해당 클래스 내에서는 변수명을 그대로 쓰면 되지만, 같은 패키지의 다른 클래스에서는 '클래스명.변수명'으로 접근해야 한다. 또한 다른 패키지에서는 '패키지명.클래스명.변수명'으로 접근해야 한다. 당연히 다른 패키지에서 접근해야 하는 클래스라면 상수를 멤버로 가지고 있는 그 클래스는 public 클래스여야 한다.  cf. Kotlin에서 클래스 내부 companion object { const val 변수명 = ~ } 으로 대체해서 사용한다. 

 

 가비지

Q. 자바에는 객체를 생성하는 new 연산자는 있지만 객체를 소멸시키는 연산자는 없다. 객체의 할당된 메모리는 어떻게 반환될까?

 

💛 가비지

가비지: 객체 치환(변수가 레퍼런스하는 객체를 바꾸는 것, 즉 화살표를 바꾸는 것)을 하면 원래 변수가 가리키던 객체가 아무도 가리키지 않게 되어 프로그램에서 접근할 수 없는 상태가 된다. 이처럼, 자바 응용프로그램에서 더 이상 사용되지 않게 된 객체나 배열 메모리를 가비지라고 부른다.

 

💛 객체 소멸, 가비지 컬렉션

객체소멸: new에 의해 생성된 객체 공간을 JVM에게 돌려주어 가용 메모리에 포함시키는 것이다. 

=> JVM의 가비지 컬렉터가 적절한 시점에 자동으로 이러한 기능을 수행한다.

vs C++: delete 연산자를 실행시켜 객체를 바로 소멸시킬 수 있고, 객체가 소멸될 때 소멸자(destructor) 함수가 호출되어 객체가 사라질 때 필요한 마무리 작업을 수행시키도록 할 수 있다.

 

가비지 컬렉션: 가용 메모리가 일정 크기 이하로 줄어드면 자동으로 가비지를 회수하여 가용 메모리를 늘리는 것

=> 가끔 가용 메모리가 부족해지는 경우가 있는데, 이때 가비지 컬렉터가 실행되며 응용 프로그램은 실행을 멈추고 가비지 컬렉션이 끝나기를 기다리게 되어, 사용자의 눈에는 프로그램이 중단된 것처럼 보인다.

=> System.gc(); // 가비지 컬렉션 강제 요청(즉시 가비지 컬렉터가 작동하는 것은 아님. 가비지 컬렉션은 자바 플랫폼이 전적으로 판단하여 적절한 시점에 작동시킨다. )

 

✅ 상속

💛 상속의 특징

객체 지향 언어의 특성 중 하나인 상속을 위에서 언급하였다. 이 부분에서는 상속에 관한 세부사항을 더 보도록 한다.

- 서브 클래스의 인스턴스인 객체는 자식 클래스와 부모 클래스에 만들어진 모든 멤버를 가지고 생성된다.

- 자바에서는 다중 상속을 지원하지 않는다. 즉 extends 다음에는 하나의 클래스명만 올 수 있다. vs C++: 지원 o

- 상속의 횟수에는 제한이 없다.

- 모든 클래스의 조상은 java.lang.Object 이다(선언여부와 상관없이). 즉, Object 클래스만이 슈퍼클래스가 없다. 

 

💫 다중 상속

클래스끼리의 상속과 인터페이스끼리의 상속에서 extends 뒤에서 각각 여러 개의 클래스, 여러 개의 인터페이스가 ,로 구분되어 여러 개 오는 것을 말한다. 단, 자바에서는 클래스의  다중 상속을 지원하지 않지만 인터페이스의 다중 상속은 지원한다. 

 

💛 상속과 생성자

위에서 super()의 쓰임을 미리 보았다. super()은 서브 클래스의 생성자에서 슈퍼 클래스의 생성자를 호출하는 것이다. 

이를 통해 유추해볼 수 있는 것은 생성자는 서브->슈퍼 클래스 순으로 호출이 되지만, 슈퍼 -> 서브 순으로 실행이 된다는 것을 알 수 있다. 왜냐하면 슈퍼 클래스의 멤버가 초기화되어야지만 서브 클래스에서 슈퍼 클래스의 멤버에 접근이 가능하기 때문이다. 

+ 서브 클래스에서 슈퍼 클래스 생성자 중 어떤 것을 선택할 지는 위에서 미리 보았다. 개발자가 명시적으로 super() 키워드를 사용했는지 아닌지의 여부에 따라 호출되는 생성자가 달라지고 컴파일 오류가 일어나는 조건도 살펴보았다.

 

🤔 앞서 봤듯이, 생성자에도 접근 제어자가 붙을 수 있다. 만일 생성자에 private이 붙었다면, 이는 해당 매개변수의 타입, 개수를 이용해서는 객체를 생성할 수 없음을 의미하는 동시에 서브 클래스에서 super()을 이용해 해당 생성자를 호출할 수 없다는 의미이다. 무엇이 되었든 간에, private은 클래스 외부에서는 무조건 접근할 수 없다.

 

✅ 업캐스팅과 instanceof

💙 업캐스팅

업캐스팅을 이해하기 위해 다음 코드를 보자.

Person p = new Student("이재문");  // 업캐스팅

Student s = (Student)p; // 다운캐스팅

 

업캐스팅 부분만 다시 쓰면 아래처럼 쓸 수도 있음.

Person p;

Student s = new Student();

p = s;

 

업캐스팅: 슈퍼 클래스 타입의 레퍼런스 변수가 서브 클래스 타입의 객체를 가리키도록 치환되는 것

👉 결과: 업캐스팅된 레퍼런스(p)로는 객체 내에 모든 멤버에 접근할 수 없고, 슈퍼 클래스의 멤버만 접근할 수 있음.

👉 p=s 대신에 p=(Person)s라고 써도 되지만, 생략할 수 있음(자동 형변환)

 

다운캐스팅: 업캐스팅과 반대로 캐스팅해서 결과적으로 객체 내에 모든 멤버에 접근할 수 있게 하는 것

👉 결과: s는 모든 멤버에 접근할 수 있게 된다.

👉 Student s = (Student)p에서 (Student)를 절대 생략할 수 없음(명시적 형변환)

 

🤔 어떤 메소드에서 슈퍼클래스 Person 타입의 매개변수를 받는다고 가정하자. 이전에 객체를 매개변수로 받을 때에는 객체 자체가 아니라 레퍼런스를 받는다고 하였다. 그런데 메소드를 작성하는 개발자는 레퍼런스에 대응하는 객체가 무엇인 지 알 수 없다. => instanceof

 

class Person {
	private String name;
	private int age;
	
	Person (String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}
	
	public String toString() {
		return "Person " + this.name + " " + this.age;
	}
}

class Student extends Person {
	private int sid;
	
	Student (String name, int age, int sid) {
		super(name, age);
		this.sid = sid;
	}
	
	public String toString() {
		return "Student " + super.getName() + " " + super.getAge() + " " +  this.sid;
	}
}

public class PersonEx {
	
	private static void printPerson(Person person) {
		System.out.println(person);
		// 매개변수로 넘어온 것이 Person 타입이라면, toString() 메소드를 호출할 때 슈퍼 클래스인 Person의 메소드가 호출된다.
		// 그러나, 매개변수로 넘어온 것이 Student 타입이 업캐스팅되어 들어온 것이라면, 동적바인딩에 의해 서브 클래스인 Student의 메소드가 호출된다.
		// 이처럼, 매개변수로 들어온 레퍼런스가 가리키는 객체가 슈퍼 클래스의 멤버만 가지고 있을 지, 아니면 슈퍼 클래스와 서브 클래스의 멤버를 모두 가지고 있을 지 모르기 때문에, instanceof 연산자가 필요한 것이다.  
	}

	public static void main(String[] args) {
		Person p = new Person("조한빈", 24);
		Student s = new Student("김다솔", 25, 1915202);
		
		printPerson(p);
		printPerson(s);
	}

}

이러한 경우, 메소드 내부에서 매개변수로 넘어온 레퍼런스가 특정 타입의 객체인지 아닌지의 여부를 반환된 boolean 값으로 판단할 수 있다. 쓰는 패턴은 '레퍼런스 instanceof 클래스명' 이다. 

즉, instanceof 뒤에는 unprimitive인 클래스 타입만이 올 수 있고, primitive 타입이 오면 컴파일 오류이다. 

 

private static void printPerson(Person person) {
		System.out.println(person);
		
		if (person instanceof Person) {
			System.out.println("이 레퍼런스는 Person 멤버를 포함합니다.");
		} 
		if (person instanceof Student) {
			System.out.println("이 레퍼런스는 Student 멤버를 포함합니다.");
		}
	}

실제로 활용된 모습이다. 출력 결과는 아래와 같다. 

Person 조한빈 24
이 레퍼런스는 Person 멤버를 포함합니다.
===========
Student 김다솔 25 1915202
이 레퍼런스는 Person 멤버를 포함합니다.
이 레퍼런스는 Student 멤버를 포함합니다.

 

✅  OVERRIDING

💛 메소드 오버라이딩의 제약사항 💫

위에서 객체 지향 언어의 다형성을 위해 오버로딩과 함께 오버라이딩의 특징을 보았다. 여기서는 메소드 오버라이딩이의 제약 사항을 알아보자.

- 동일한 원형(메소드 이름, 매개변수 타입과 개수, 리턴 타입)

- 슈퍼 클래스 메소드의 접근 지정자보다 접근의 범위를 좁혀 오버라이딩 할 수 없다. (오버라이딩은 클래스 간 상속뿐만 아니라 인터페이스의 구현도 마찬가지이다. 그러므로 인터페이스를 구현하는 특정 클래스에서 메소드를 오버라이딩할 때에도 접근의 범위를 좁혀 오버라이딩할 수 없다.)

- static이나 private 또는 final로 선언된 메소드는 서브 클래스에서 오버라이딩할 수 없다. (trivial...)

+ 만일 슈퍼클래스와 서브클래스가 다른 패키지에 있다고 가정해보자. 이때 public인 슈퍼클래스의 메소드의 접근 지정자가 default이면 서브 클래스에서 오버라이딩할 수 없다. 그러나 접근지정자가 protected나 public이면 오버라이딩할 수 있다.

- 인터페이스의 특정 추상 메소드가 void f(); 라고 해보자. 이는 public 접근 지정자가 생략된 것이므로 클래스에서 이를 구현할 때의 접근 지정자는 반드시 public이여야 한다. 

 

💙 super 키워드

: 오버라이딩이 동적 바인딩인 것에 반해서, super 키워드를 이용하면 정적 바인딩을 통해 슈퍼 클래스의 멤버에 접근 가능하며, super는 자바 컴파일러에 의해 지원되는 슈퍼 클래스에 대한 레퍼런스이다. 슈퍼 클래스에 대한 레퍼런스라는 개념이 조금 생소할 수 있는데, 현재 객체에 대한 레퍼런스가 암묵적으로 슈퍼 클래스에 대한 레퍼런스의 개념도 포함하고 있다는 정도만 알고 있으면 될 것 같고, 클래스 내부에서 슈퍼 클래스 멤버에 접근하기 위해 super 키워드를 사용한다는 것만 염두하자. 

 

// super 키워드 연습

class SuperObject {
	
	public void paint() {
		draw();
	}
	
	public void draw() {
		draw();
		System.out.println("Super Object");
	}
}

class SubObject extends SuperObject {
	
	@Override
	public void paint() {
		super.draw();
	}
	
	@Override
	public void draw() {
		System.out.println("Sub Object");
	}
}

public class Sample {

	public static void main(String[] args) {
		SuperObject b = new SubObject();
		b.paint();
	}

}

위의 코드 실행 결과는

Sub Object

Super Object

main 메소드에서 레퍼런스 변수 b의 타입을 SubObject로 바꿔도 같은 결과가 나온다. 

 

✅ 추상클래스와 인터페이스

 

🤔 위에서 상속 관계에 있어서 메소드의 다형성을 위해 오버라이딩의 개념을 살펴보았다. 서브클래스에서 오버라이딩하면 될걸, 왜 추상메소드(abstract 키워드가 붙은, 함수의 body가 구현되어 있지 않은)를 만들어놓는 것일까?

=> 추상메소드 역시 오버라이딩과 마찬가지로 클래스의 다형성을 실현하기 위함이다. 단순 오버라이딩은 재구현하는 의무에서 자유롭지만, 추상메소드의 경우 모든 서브 클래스에서 반드시 재구현되어야 한다. 즉, 추상클래스를 이용하면 설계와 구현을 분리할 수 있다. (인터페이스와 유사) 또한, 추상 클래스는 계층적 상속 관계를 가진 클래스들의 구조를 만들 때 적합하다. 

❗ void f() {} // abstract method가 아니다.

 

💛 추상 클래스

추상 클래스: abstract 키워드가 붙은, 추상 메소드가 있을 수도 없을 수도. 추상 메소드가 있으면 반드시 추상 클래스로 선언되어야 한다. 

 

특징

- 객체를 생성할 수 없다. 추상 클래스의 목적은 객체 생성에 있는 것이 아니기 때문이다. 단, 추상 클래스의 레퍼런스 변수를 선언하는 것은 오류가 아니다. 

- 추상 클래스를 상속받는 서브클래스는 슈퍼클래스에서 미완성된 기능을 구현할 수도, 안할 수도 있다(이 경우에는 또 다시 abstract를 붙이고 자손이 구현해야 할 책임을 진다). => 계층적 상속 관계를 가진 클래스 구조 

 

💛 인터페이스

특징

< 먼저, 인터페이스 내부 규칙에 관하여 >

 

◽ 변수 필드(멤버 변수)는 포함하지 않는다❓ 그런데 인터페이스 내 필드는 public, static, final (+default)가 가능하다네....? 따라서 int b = 3; 이나 public int b = 3;이 컴파일 에러가 아니다... 단 int b; 와 같이 blank값으로 두는 게 불가능하다.

 가능한 메소드 구성에 대한 경우의 수를 알아보기 위해 작성한 아래의 코드를 참고할 것. 

// 인터페이스의 멤버에 붙을 수 있는 modifier를 정리하면 
// 1. abstract이 붙으면 추상메소드인 것은 당연할 테고
// 2. 그러나 abstract는 private이나 static이나 default과 결합하면 컴파일 에러
// 3. abstract가 없다는 전제 하에 private 또는 static 또는 default가 들어가면 추상메소드 아니고 
// 4. 앞에서 언급된 모든 modifier가 없으면 무조건 추상메소드이다. (접근 지정자가 생략되면 public이 생략되었다고 간주..)
// 5. protected modifier는 아예 사용할 수 없는 접근자이다. 

interface if1 {
	abstract public void f1(); // 추상메소드
	abstract void f2(); // 추상메소드
	abstract private void f3(); // 컴파일 에러, 결합 불가
	abstract static void f4(); // 컴파일 에러, 결합 불가
	private void f5(); // 컴파일 에러, body 구현 필요
	static void f6(); // 컴파일 에러, body 구현 필요
	private static f7(); // 컴파일 에러, body 구현 필요
	void f8() ; // 추상메소드
	public void f9(); // 추상 메소드
	public abstract void f10(); // 추상메소드
}

👉 추상 메소드가 아닌 메소드의 경우 {}가 존재해야 하고, 추상 메소드인 경우 {}를 구현하면 컴파일 에러이다. 

👉 접근 지정자가 아무것도 없으면 public이 생략된 걸로 간주하고, 인터페이스를 구현하는 클래스에서는 반드시 접근 지정자를 public으로 해야한다.

 

< 그 외 규칙에 관하여... >

◽ 객체를 생성할 수 없다. 단, 인터페이스 타입의 레퍼런스 변수는 선언 가능하다. (추상클래스와 공통점)

 

당연한 얘기이겠지만, 인터페이스는 다른 인터페이스를 상속할 순 있어도, 구현할 수 없다. 다른 인터페이스를 상속할 때에는 반드시 슈퍼 인터페이스(?)의 추상 메소드를 구현할 필요 없다. 

 

인터페이스를 구현하여(implements) 클래스를 작성하면, 그리고 그 클래스가 abstract 클래스가 아니라면, 인터페이스의 모든 추상 메소드를 구현하여야 한다. 아래 예시를 참고할 것

 

 자바는 인터페이스의 다중 상속을 허용한다. 여러 개의 인터페이스는 ,로 구분한다. (클래스는 다중 상속 허용 x, 단 여러 개의 인터페이스는 구현 가능)

ex. interface iA extends iB, iC {} // ⭕

ex. class cA extends cB, cC {} // ❌

ex. class cA implements iA, iB {} // ⭕, 단 얘는 다중상속의 개념이 아님

 

◽ 클래스는 다른 클래스를 상속받는 동시에 하나 이상의 인터페이스를 구현할 수도 있다.

ex. class SmartPhone extends PDA implements MobilePhoneInterface, MP3Interface { }

 

💙 클래스와 인터페이스의 복잡한 관계 코드

interface if1 {
	public void f1();
}

interface if2 extends if1 {
	// f1()의 body를 구현하지 않았다고 해서 컴파일 에러가 아님.
}

abstract class c1 {
	abstract public void f2();
}

abstract class c2 extends c1 {
	abstract public void f2();
}

abstract class c3 implements if1 {
	// abstract를 붙이면 if1 인터페이스를 구현하더라도 에러가 아니다. 
}

class c4 extends c3 {
	// c4는 c3를 상속하므로 f1 인터페이스를 구현하는 것과 마찬가지이므로 반드시 아래처럼 써줘야 한다.
	// 단 인터페이스에서 f1의 접근 지정자는 public이므로 여기서도 public으로 해줘야 한다. 
	public void f1() {}; 
}

 

정리 <추상클래스와 인터페이스의 공통점>

추상클래스 인터페이스
객체를 생성할 수 없고(레퍼런스 변수 선언은 가능), 클래스의 다형성을 실현하기 위함이다. 설계와 구현을 분리하고자 하는 목표는 비슷하다.

👉 둘의 차이를 명백하게 서술하는게 어렵네.

👉 어떤 클래스 내부에서 인터페이스의 메소드를 구현할 때나, 슈퍼 클래스의 메소드를 오버라이딩할 때에는 반드시 접근 지정자를 좁혀서는 안되는 것에 유의해야 한다.

👉 어떤 인터페이스가 슈퍼 인터페이스를 상속할 때 메소드를 오버라이딩할 때에는 좁혀도 상관이 없더라. 너무 지엽적이니까 신경쓰지는 말자.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함