자바와 객체 지향
객체 지향은 인간 지향이다.
절차적/구조적 프로그래밍까지의 과정은 기계 종속적인 개발이었다.
구조적 프로그래밍에서 제일 중요한 것은 함수이다. 복잡한 문제를 작은 문제로 분할하여 정복하는 방법이다.
그러나 몇 천, 몇 만 라인의 명령어를 논리적인 단위로 나누어 블록화하여 작성해야한다.
그 후 객체 지향 프로그래밍이 등장했다.
객체 지향의 출발은 "우리가 주변에서 사물을 인식하는 방식대로 프로그래밍할 수 있지 않겠는가?"라는 물음에서 시작되었다.
0,1로 대변하는 기계(컴퓨터)에 맞춰 사고하던 방식을 버리고 현실세계를 인지하는 방식으로 프로그램을 만들자는 것이다.
그래서 객체 지향은 직관적이다.
객체 지향 이해하기
- 세상에 존재하는 모든 것은 사물, 즉 객체(Object)이다.
- 각각의 사물은 고유하다.
- 사물은 속성(property)을 갖는다.
- 사물은 행위(method)를 한다.
객체 지향은 인간의 사고 방식을 그대로 프로그래밍에 접목하여 직관적이고 쉽다.
객체 지향은 가장 인간을 지향하는 프로그래밍이다.
객체 지향의 4대 특성
- 캡슐화(Encapsulation) : 정보 은닉
- 상속 (Inheritance) : 재사용
- 추상화(Abstraction) : 모델링
- 다형성(Polymorphism) : 사용 편의
클래스와 객체
클래스(Class) : 객체들의 분류, 집합, 설계도(blueprint)
객체(Object) : 세상에 존재하는 유일무이한 사물, 실체
세상에 존재하는 유일무이한 객체를 특성(속성+기능)에 따라 분류해 보니 객체를 통칭할 수 있는 집합적 개념, 즉 클래스(분류)가 나온다.
클래스 : 객체 = 사람 : 김연아 = 판다 : 푸바오
클래스를 이용해 객체를 만들었다는 것을 강조할 때는 object 표현보다는 클래스의 instance라는 표현을 쓴다.
객체(object) = 클래스의 인스턴스
추상화 : 모델링
추상(abstract) : 여러 가지 사물이나 개념에서 공통되는 특성이나 속성 따위를 추출하여 파악하려는 작용
추상화 : 구체적인 것을 분해해서 관찰자가 관심 있는 특성만 가지고 재조합하는 것
객체지향의 추상화는 곧 모델링이다.
개발자는 애플리케이션의 창조자가 된다.
우리는 객체 지향 프로그래밍을 할 때 클래스를 먼저 설계하게 된다.
클래스 생성시 객체들이 가진 공통된 특성을 찾게 된다.
- 명사로 표현되는 공통 특성 : 속성
- ex) 시력, 몸무게, 혈액형, 키, 나이, 직업, 취미
- 동사로 표현되는 공통 특성 : 메서드(기능/행위)
- ex) 먹다, 자다, 일하다
클래스 설계에서 추상화가 사용된다.
애플리케이션 경계(Context)
내가 만들고자 하는 애플리케이션은 어디에서 사용될 것인가?
- 병원 애플리케이션 : 사람 => 환자
- 은행 애플리케이션 : 사람 => 고객
애플리케이션 경계에 따라 클래스 설계가 달라질 수 있다.
사람이라는 큰 클래스(분류)에 대해 애플리케이션 경계에 맞게 더 구체적으로 클래스를 정의할 수 있고, 필요하지 않는 속성을 배제할 수 있다.
추상화
추상화란 구체적인 것을 분해해서 관심 영역(애플리케이션 경계)에 있는 특성만 가지고 재조합하는 것 = 모델링
지구본은 지구를 모델링한다. 지구의 굴곡이나 기후 등 모든 부분을 표현하지 않고 표현할 수도 없지만 지구의 주요 특성들을 표현한다.
태양계 모델은 태양계를 모델링한다. 태양계를 정확히 묘사하진 않지만, 태양을 축구공 크기로 표현할 때 수성은 좁쌀 모양의 크기로 표현한다. 즉, 상대적인 크기에 대한 특성을 표현한다.
태양계에서 보여주고 싶은 주요 특성을 뽑아 태양계 모델을 만든다.
모델 : 실제 사물을 정확히 복제하는 게 아니라, 목적에 맞게 관심 있는 특성만을 추출해서 표현하는 것이다.
-> 추상화를 통해 실제 사물을 단순하게 묘사
실제 사물은 애플리케이션 경계에 맞춰 관심있는 특성만 표현되어 단순하게 묘사된다.
객체 지향에서 추상화의 결과는 클래스이다.
- 논리적 설계 : 개발 환경(언어 등)에 영향을 받지 않는 설계
- 물리적 설계 : 개발 환경에 맞춰진 설계
코드로 표현하기 위해 논리적 설계를 물리적 설계로 바꿔야 한다.
java에서 추상화 지원
자바는 class로 객체 지향의 추상화를 지원한다.
추상화 = 모델링 = 자바의 class 키워드
클래스 이름은 집합, 분류를 더 잘 나타내도록 짓자.
객체는 유일무이한 사물처럼 짓자.
클래스 객체_참조_변수 = new 클래스();
1. 객체 참조 변수를 선언한다.
2. 새로운 객체를 하나 생성한다.
3. 객체의 주소값을 객체 참조 변수에 할당한다.
추상화에서 기억할 부분
- OOP의 추상화는 모델링이다.
- 클래스 : 객체 = 사람 : 김연아
- 클래스 설계에서 추상화가 사용된다.
- 클래스 설계를 위해서는 애플리케이션 경계부터 정해야한다.
- 객체 지향에서 추상화의 결과는 클래스다.
추상화와 T 메모리
Mouse 클래스
package abstraction01;
public class Mouse {
public String name;
public int age;
public int countOfTail;
public void sing(){
System.out.println(name + "찍찍!!");
}
}
name, age, countOfTail 은 static 키워드가 붙지 않았으므로 객체 참조 변수이다.
객체 참조 변수들은 new 키워드로 객체가 생길 때 함께 생기며(힙에 존재) 객체에 속하는 변수이다.
테스트용 클래스
package abstraction01;
public class MouseDriver{
public static void main(String[] args){
Mouse mickey = new Mouse();
mickey.name = "미키";
mickey.age = 85;
mickey.countOfTail = 1; // 9번째 줄
mickey.sing(); // 11번째 줄
mickey = null;
}
}
9번째 줄 메모리 상태
new 키워드로 클래스의 인스턴스가 생기면 heap에 할당된다.
- static : "고정된"이라는 뜻, static 영역에 올라간 정보는 main 메서드가 시작되기 전에 올라가서 main 메서드가 종료된 후 내려올 정도로 단단히 스태틱 영역에 고정되어있다.
- 스택 : LIFO(선입후출구조), 사이즈가 고정(프로그램 시작시 결정), 작은 데이터 빠르게 처리 가능, 함수 호출 관리에 적합
- 힙 : 대용량 자료를 저장할 수 있다 - 크기 유연하게 할당 가능, 개발자가 직접 할당하고 해제하거나 garbage collector가 수거
11번째 줄 메모리 상태
mickey = null;
객체 참조 변수 mickey가 더이상 힙 영역에 존재하는 Mouse 객체를 참조하지 않는다.
- 가비지 컬렉터*는 아무도 참조하지 않는 Mouse 객체를 쓰레기로 인지하여 수거한다.
Mouse mickey = new Mouse();
- Mouse mickey; // Mouse 객체에 대한 참조 변수 mickey를 만든다.
- mickey = new Mouse(); // Mouse 클래스의 인스턴스를 만들어 힙에 배치한다.
- 객체 참조 변수에 생성한 객체에 대한 주소(포인터)를 할당한다.
static 멤버 vs 인스턴스 멤버
모든 쥐의 꼬리는 1개이다.
객체 수만큼 저장하는 것보다 그 값을 쥐 클래스에 저장하자. => static 사용
static
해당 클래스의 모든 객체들이 같은 값을 가질 때 사용
package abstraction01;
public class Mouse {
public String name;
public int age;
public static int countOfTail;
public void sing(){
System.out.println(name + "찍찍!!");
}
}
- 객체_참조_변수.속성명 또는 클래스명.속성명으로 접근할 수 있다. 정적 멤버는 클래스명.속성명으로 접근하는 것을 권장한다.
package abstraction01;
public class MouseDriver {
public static void main(String[] args){
// 클래스명.countOtTail
Mouse.countOfTail = 1;
Mouse mickey = new Mouse();
Mouse jerry = new Mouse();
// 객체명.countOfTail
System.out.println(mickey.countOfTail); // 1
System.out.println(jerry.countOfTail); // 1
// 클래스명.countOfTail
System.out.println(Mouse.countOfTail); // 1
}
}
- 클래스 멤버 = static 멤버 = 정적 멤버
- 객체 멤버 = 인스턴스 멤버
정적 메서드는 객체들의 존재 여부에 관계없이 쓸 수 있는 메서드이다.
정적 메서드는 객체가 아닌 클래스에 속해 있다.
=> 클래스는 JVM 구동시 메모리의 스태틱 영역에 바로 배치되기 때문에 객체의 존재 여부에 관계 없이 쓸 수 있다.
어떤 메서드가 정적 메서드이어야 할까?
프로그램 실행시 어떠한 객체도 존재하지 않기 때문에, 프로그램을 실행하는 main 메서드는 정적 메서드여야한다.
- main() 메서드
- main() 메서드의 논리를 함수로 분할해서 사용하는 경우
- 정적 변수에 대한 getter(), setter() => 정적 변수를 조회하거나 설정
- 클래스의 인스턴스를 만들지 않고 사용하는 유틸리티성 메서드 ex) Math 클래스의 정적 메서드
static 변수 vs 인스턴스 변수 vs local 변수
static 변수와 인스턴스 변수는 기본값으로 초기화된다.
정수형 : 0, 부동소수점형 : 0.0, 논리형 : false, 객체 : null
지역변수는 개발자가 별도로 초기화하지 않으면 쓰레기값을 갖게 된다.
상속 : 재사용 + 확장
- 객체 지향의 상속은 상위-하위 클래스 또는 슈퍼-서브 클래스이다.
- 객체 지향의 상속은 부모-자식 클래스가 아니다.
- 객체 지향의 상속은 분류도이다.
- 계층도(할아버지-아버지) 또는 조직도가 아니다.
- 객체 지향의 상속은 재사용과 확장이다.
- 상위 클래스로 갈수록 추상화, 일반화되었다고 말하며, 하위 클래스 쪽으로 갈수록 구체화, 특수화된다.
하위 클래스는 상위 클래스다.
- 뽀로로는 펭귄이다
펭귄은 뽀로로다(X) : 뽀로로가 펭귄을 대표할 수 없기 때문이다.
뽀로로는 펭귄이다 : 펭귄이 뽀로로와 그 외 수많은 펭귄 1,2,3..을 대표할 수 있다.
- 펭귄은 동물이다.
동물 뽀로로를 선언하고, 펭귄을 생성한 후 펭귄을 뽀로로에 할당한다.
펭귄은 동물이고 동물은 펭귄을 대표할 수 있다.
동물 뽀로로 = new 펭귄();
- 포유류는 동물이다.
- 고래는 포유류다.
하위 클래스를 상위 클래스에 할당하는 것이 자연스럽다.
상위 클래스는 하위 클래스를 포함한다.
- java에서 상속을 표현할 때 inheritance라는 키워드가 아닌 extends(확장) 키워드로 표현한다.
자식 클래스가 부모 클래스를 확장하기 때문이다.
하위 클래스는 상위 클래스이다.(상위 클래스로 표현할 수 있다, 상위 클래스를 포함한다, 확장한다.)
상위 클래스에 하위 클래스를 할당할 수 있다. => 하지만 그때부터 상위 클래스의 필드와 메서드만 접근할 수 있다. 하위 클래스에서 확장한 프로퍼티와 메서드는 사용할 수 없다.
예제 코드
package inheritance01;
public class 동물 {
String myClass; // default 접근제어자(같은 패키지에서 접근 가능)
동물(){
myClass = "동물";
}
void showMe(){
System.out.println(myClass);
}
}
package interitance01;
public class 포유류 extends 동물 {
포유류(){
myClass = "포유류";
}
}
- 자식 클래스는 부모 클래스의 필드와 메서드를 상속받는다.
package interitance01;
public class 고래 extends 포유류 {
포유류(){
myClass = "고래";
}
}
package inheritance01;
public class Driver01 {
public static void main(String[] args){
동물 animal = new 동물();
포유류 mammalia = new 포유류();
고래 whale = new 고래();
animal.showMe();
mammalia.showMe();
whale.showMe();
}
}
- 상위 클래스에서 구현한 메서드를 하위 클래스에서 사용 가능하다.
package inheritance01;
public class Driver01 {
public static void main(String[] args){
동물 animal = new 동물();
동물 mammalia = new 포유류();
동물 whale = new 고래();
animal.showMe();
mammalia.showMe();
whale.showMe();
}
}
- 하위 클래스는 상위 클래스를 할당할 수 있다.
package inheritance01;
public class Driver01 {
public static void main(String[] args){
동물[] animals = new 동물[7];
동물[0] = new 동물();
동물[1] = new 포유류();
동물[2] = new 고래();
for (int index=0;index<animals.length();i++){
animals[i].showMe();
}
}
}
- 또한 모든 클래스는 결국 Object를 물려받는다. 그래서 어떤 클래스의 인스턴스이든 상관없이 개발자는 toString() 메서드를 사용할 수 있다.
package Practice;
public class prac15 {
public static class Animal {
public void eat(){
System.out.println("Animal is eating food");
}
}
public static void main(String[] args){
Animal animal = new Animal();
animal.eat();
System.out.println(animal.toString()); // Practice.prac15$Animal@2d209079
}
}
상속은 is kind of 관계이다.
- 하위 클래스 is kind of 상위 클래스
- 하위 클래스는 상위 클래스의 한 분류이다.
- 펭귄 is kind of 조류
- 뽀로로 is kind of 펭귄
상속에서 기억할 부분
- 객체 지향의 상속은 상위 클래스의 특성을 재사용하는 것이다.
- 객체 지향의 상속은 상위 클래스의 특성을 확장하는 것이다.
- 객체 지햐의 상속은 is kind of 관계를 만족해야 한다.
다중 상속
c++과 달리 java와 c#은 다이아몬드 문제를 발생시키는 다중 상속을 포기했다.
하지만 java는 c++에는 없는 인터페이스를 도입해 다중 상속의 득만 취하고 실은 버렸다.
인터페이스는 is able to 관계이다.
- 구현 클래스 is able to 인터페이스
- 구현 클래스는 인터페이스할 수 있다.
- 고래 is able to 헤엄
- 고래는 헤엄 인터페이스를 구현하므로 헤엄칠 수 있다.
상위 클래스는 하위 클래스에게 특성(속성과 메서드)를 상속해주고, 인터페이스는 클래스가 무엇을 할 수 있다라고 하는 기능을 구현하도록 강제하게 된다.
java API에서 인터페이스
- Serializable 인터페이스 : 직렬화할 수 있는
- Cloneable 인터페이스 : 복제할 수 있는
- Comparable 인터페이스 : 비교할 수 있는
- Runnable 인터페이스 : 실행할 수 있는
분류도에 적용
예제 코드
package inheritance01;
public interface 헤엄칠수있는 {
void swim();
}
package inheritance01;
public class 고래 extends 포유류 implements 헤엄칠수있는{
고래(){
myClass = "고래";
}
public void swim(){ // 구현 강제
System.out.println(myClass + " 수영 중");
}
}
package inheritance01;
public class Driver{
public static void main(String[] args){
}
}
상속과 T 메모리
package inheritance02;
public class Animal{
public String name;
public void showName(){
System.out.printf("안녕 나는 %s야", name);
}
}
package inheritance02;
public class Penguin extends Animal{
public String habitat;
public void showHabitat(){
System.out.printf("%s는 %s에 살아", name, habitat);
}
}
package inheritance02;
public class Driver {
public static void main(String[] args){
Penguin pororo = new Penguin();
pororo.name = "뽀로로";
pororo.habitat = "남극";
pororo.showName();
pororo.showHabitat();
Animal pengsu = new Penguin(); // 13번째 줄
pengsu.name = "펭수";
// pengsu.habitat = "남극";
pengsu.showName();
// pengsu.showHabitat();
}
}
- pororo는 Penguin 클래스의 인스턴스이고, pengsu는 Animal 클래스의 인스턴스이다.
- pengsu는 Animal 클래스의 인스턴스이다.
하위 클래스인 Penguin을 상위 클래스 Animal에 할당했다.
하위 클래스는 상위 클래스에서 확장된 것이므로 대입은 가능하지만, Penguin에서 확장한 프로퍼티와 메서드는 사용할 수 없다. 즉, 하위 클래스인 Penguin 클래스의 프로퍼티와 메서드에 접근할 수 없다.
코드 읽는 연습
코드는 인간의 언어로 번역하며 읽어야한다.
그렇게 읽을 때 논리적으로 이해하기 쉬운 코드가 되어야한다.
객체 지향은 인간 지향이기 때문이다.
코드를 보면서 번역해서 읽기 힘들다면 객체 지향 언어의 아름다움을 충분히 활용하지 못하고 있는 것이다.
13번째 줄 실행 후 T 메모리 구조
- Penguin 클래스의 인스턴스 뿐만 아니라 상위 클래스인 Animal 클래스의 인스턴스도 함께 Heap 영역에 생겼다.
- 하위 클래스의 인스턴스가 생성될 때 상위 클래스의 인스턴스도 함께 생성된다.
- 모든 클래스의 최상위 클래스인 Object 클래스의 인스턴스도 함께 생성된다.
인터페이스를 통해 다형성 제공하기
인터페이스 자체는 객체를 생성할 수 없지만, 인터페이스를 구현한 클래스의 객체는 생성할 수 있다.
interface Flyable {
void fly();
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("새가 날아갑니다.");
}
}
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("비행기가 날아갑니다.");
}
}
public class Main {
public static void main(String[] args) {
// 인터페이스 객체 생성 불가능
// Flyable f = new Flyable(); // 오류 발생
Flyable f1 = new Bird();
f1.fly(); // "새가 날아갑니다." 출력
Flyable f2 = new Airplane();
f2.fly(); // "비행기가 날아갑니다." 출력
}
}
다형성 : 사용편의성
오버라이딩과 오버로딩
- 오버라이딩(overriding): 같은 메서드 이름, 같은 인자 목록으로 상위 클래스의 메서드를 재정의
- 오버로딩(overloading) : 같은 메서드 이름, 다른 인자 목록으로 다수의 메서드를 중복 정의
인공위성에서 볼 때 오토바이를 올라탄(riding) 사람은 오토바이가 가려져 사람만 보인다.
짐이 적재(loading)될 때 옆으로 적재된 모든 적재물이 보인다.
즉, 오버라이딩은 하위 클래스로 인해 상위 클래스의 메서드 정의를 덮어씌우고, 오버로딩은 메서드 여러개를 추가적으로 중복 정의한다.
예제 코드
package polymorphism01;
public class Animal {
public String name;
public void showName(){
System.out.printf("안녕 %s야 반가워\n", name);
}
}
package polymorphism01;
public class Penguin extends Animal {
public String habitat;
public void showHabitat(){
System.out.printf("%s는 %s에 살아\n", name, habitat);
}
// 오버라이딩 - 재정의
public void showName(){
System.out.printf("펭귄의 이름은 비밀이야\n");
}
// 오버로딩
public void showName(String yourName){
System.out.printf("%s야 안녕, 나는 %s야\n", yourName, name);
}
}
package polymorphism01;
public class Driver{
public static void main(String[] args){
Penguin pororo = new Penguin();
pororo.name = "뽀로로";
pororo.habitat = "남극";
pororo.showName();
pororo.showName("초보람보");
pororo.showHabitat();
Animal pengsu = new Penguin(); // 14번째 줄
pengsu.name = "펭수";
pengsu.showName();
}
}
출력
펭귄의 이름은 비밀이야
초코람보야 안녕, 나는 뽀로로야
뽀로로는 남극에 살아
펭귄의 이름은 비밀이야
pengsu.showName();
- Animal 객체에 있는 showName()메서드는 Penguin 객체에 있는 showName() 메서드에 의해 재정의, 즉 가려졌기에 Penguin 객체에서 재정의한 showName() 메서드가 호출된다.
=> 상위 클래스 타입의 객체 참조 변수를 사용하더라도 하위 클래스에서 오버라이딩(재정의)한 메서드가 호출된다.
14번째 줄 실행 후 T 메모리
- 객체 참조 변수 : pororo는 Penguin을, pingu(코드에서 pengsu)는 Animal을 가리킨다.
- pengsu는 Penguin 클래스의 인스턴스이지만 Animal(상위) 클래스 타입의 객체 참조 변수를 가진다. showName()은 Penguin 클래스에서 오버라이딩(덮어쓰기)되었으므로, showName()을 호출하면 덮어쓰기한 penguin의 showName()이 호출된다.
- pengsu는 Animal 클래스 타입의 객체 참조 변수이므로 오버라이딩된 메서드 이외에 Penguin 클래스의 프로퍼티와 메서드에 여전히 접근할 수 없다.
다형성이 사용편의성인 이유
- 오버로딩 : 함수명 하나를 가지고 인자 목록만 다르게 하면 된다.
- ex) 다형성이 지원되지 않는다면 addIntInt, addIntDouble, addDoubleInt 메서드명 다 다르게 만들어야한다.
- 제네릭을 사용하면 함수명 하나를 가지고 다수의 함수를 구현한 효과를 낼 수 있다.
- 오버라이딩 : 하위 클래스가 재정의한 메서드를 알아서 호출해준다.
- 상위 클래스 타입의 객체 참조 변수여도 하위 클래스가 오버라이딩한 메서드를 자동으로 호출해준다.
상위 클래스로 큰 분류를 묶어 각각의 하위 클래스 객체를 생성할 수 있다.
상위 클래스 타입의 객체 참조 변수여도 각 하위 클래스 타입의 메서드를 호출해줌으로써 각 하위 클래스 인스턴스들의 특성을 오버라이딩된 하나의 함수로 드러낼 수 있다.
예제 코드
class Driver{
public static void main(String[] args){
동물[] 동물들 = new 동물[3];
동물[0] = new 쥐();
동물[1] = new 고양이();
동물[2] = new 강아지();
for (int i=0;i<동물들.length();i++){
동물[i].cry(); // 다형성
}
}
}
찍찍
야옹
멍멍
캡슐화 : 정보 은닉
객체 멤버의 접근제어자
- 정의만 외우면 안된다. 생각보다 단순하지 않다.
접근 범위 맞추기
구조도
각 클래스의 runSomeThing() 메서드와 runStaticThing() 메서드(static)에서 접근할 수 있는 classA의 속성은 무엇일까?
classA
public class ClassA {
private int pri;
int def;
protected int pro;
public int pub;
public void runSomething() {
}
public static void runStaticThing() {
}
}
답!
ClassA(같은 클래스)에서 멤버 접근하기
public class ClassA {
private int pri;
int def;
protected int pro;
public int pub;
public void runSomething() {
// non-static 메서드에서는 모든 멤버 접근 가능
System.out.println("runSomething()");
System.out.println(pri);
System.out.println(def);
System.out.println(pro);
System.out.println(pub);
}
public static void runStaticThing() {
System.out.println("runStaticThing()");
// static 메서드에서는 인스턴스 멤버 접근 불가
// System.out.println(pri); // 컴파일 에러
// 대신 클래스 멤버에는 접근 가능
System.out.println(new ClassA().def);
System.out.println(new ClassA().pro);
System.out.println(new ClassA().pub);
}
public static void main(String[] args) {
new ClassA().runSomething();
ClassA.runStaticThing();
}
}
- runSomething() => pri, def, pro, pub : 같은 클래스이므로 모든 속성 접근 가능
- runStaticThing()=> def, pro, pub : static 메서드이므로 static이 아닌 인스턴스 멤버에 바로 접근할 수 없다. 객체를 생성하여 객체 참조 변수로 접근해야하는데, private은 외부에서 접근이 불가능하다.
ClassB (다른 클래스, 같은 패키지)
public class ClassB {
void runSomething() {
ClassA a = new ClassA();
// System.out.println(a.pri); // private 접근 불가
System.out.println(a.def);
System.out.println(a.pro);
System.out.println(a.pub);
}
static void runStaticThing() {
ClassA a = new ClassA();
System.out.println(a.def);
System.out.println(a.pro);
System.out.println(a.pub);
}
}
- runSomething() => def, pro, pub
- runStaticThing()=> def, pro, pub
ClassAA (같은 패키지, 상속)
public class ClassAA {
void runSomething() {
ClassA a = new ClassA();
// System.out.println(a.pri); // private 접근 불가
System.out.println(a.def);
System.out.println(a.pro);
System.out.println(a.pub);
}
static void runStaticThing() {
ClassA a = new ClassA();
System.out.println(a.def);
System.out.println(a.pro);
System.out.println(a.pub);
}
}
- runSomething() => def, pro, pub
- runStaticThing()=> def, pro, pub
ClassAB (다른 패키지, 상속)
package Practice.Practice2;
import Practice.Practice1.ClassA;
public class ClassAB extends ClassA {
public void runSomething() {
System.out.println("runSomething()");
System.out.println(pro); // ClassA 로부터 상속받았으므로 접근 가능
System.out.println(pub);
// 여기 ClassA는 단지 다른 패키지의 클래스를 호출한 것, 상속 관계와 상관없음
ClassA a = new ClassA();
// System.out.println(a.pro); // 다른 패키지이고 상속과 상관없으므로 접근 불가
System.out.println(a.pub);
ClassAB ab = new ClassAB();
System.out.println(ab.pro); // ClassA로부터 상속받았으므로 접근 가능
System.out.println(ab.pub);
}
public static void runStaticThing() {
System.out.println("runStaticThing()");
// 인스턴스 생성해서만 접근 가능
ClassA a = new ClassA();
// System.out.println(a.pro); // 다른 패키지이고 상속과 상관없으므로 접근 불가
System.out.println(a.pub);
// ClassAB에 대한 멤버 참조이므로 정답과는 관련 없음
ClassAB ab = new ClassAB();
System.out.println(ab.pro);
System.out.println(ab.pub);
}
public static void main(String[] args) {
new ClassAB().runSomething();
ClassAB.runStaticThing();
}
}
- runSomething() => pro, pub
- runStaticThing()=> pub
여기서 "pro"는 this.pro (ClassAB의 pro)를 뜻한다.
static 메서드에서 protected 멤버에 직접 접근할 수 없는 이유는 static 메서드가 클래스 레벨에서 작동하고, 인스턴스(객체) 레벨의 속성이나 메서드에 대한 직접적인 참조를 가질 수 없기 때문이다.
static은 인스턴스 없이 호출되므로(클래스에 속해있음) static 메서드 내부에서는 this를 참조할 수 없다. 즉, 상속받은 멤버에 접근할 수 없다.
참고 코드 - protected 멤버에 접근
public class Parent {
protected int protectedVar = 10;
}
public class Child extends Parent {
public void instanceMethod() {
System.out.println(this.protectedVar); // 'this'를 사용하여 접근
System.out.println(protectedVar); // 직접 접근도 가능
}
}
ClassC (다른 패키지)
public class ClassC {
void runSomething() {
ClassA a = new ClassA();
System.out.println(a.pub);
}
static void runStaticThing() {
ClassA a = new ClassA();
System.out.println(a.pub);
}
}
- runSomething() => pub
- runStaticThing()=> pub
classA의 모든 프로퍼티가 static으로 바뀐 경우
class A
public class ClassA {
private static int pri;
static int def;
protected static int pro;
public static int pub;
public void runSomething() {
}
public static void runStaticThing() {
}
}
classA에서 접근 (같은 클래스)
public class ClassA {
private static int pri;
static int def;
protected static int pro;
public static int pub;
public void runSomething() {
// 모든 멤버 접근 가능
System.out.println("runSomething()");
System.out.println(pri);
System.out.println(def);
System.out.println(pro);
System.out.println(pub);
}
public static void runStaticThing() {
System.out.println("runStaticThing()");
// static 멤버 바로 접근 가능 (인스턴스 생성할 필요 X)
System.out.println(pri);
System.out.println(def);
System.out.println(pro);
System.out.println(pub);
// 클래스로 바로 접근 가능
System.out.println(ClassA.pri); // 같은 클래스이므로
System.out.println(ClassA.def);
System.out.println(ClassA.pro);
System.out.println(ClassA.pub);
}
public static void main(String[] args) {
new ClassA().runSomething();
ClassA.runStaticThing();
}
}
- static 멤버 바로 접근 가능. 클래스명으로 접근 가능 => static 멤버는 클래스 레벨에 존재
- runSomeThing() => pri, def, pro, pub
- runStaticThing()=> def, pro, pub
ClassB (같은 패키지)
public class ClassB {
void runSomething() {
// System.out.println(ClassA.pri); // private 접근 불가
System.out.println(ClassA.def);
System.out.println(ClassA.pro);
System.out.println(ClassA.pub);
}
static void runStaticThing() {
System.out.println(ClassA.def);
System.out.println(ClassA.pro);
System.out.println(ClassA.pub);
}
public static void main(String[] args) {
new ClassA().runSomething();
ClassA.runStaticThing();
}
}
- runSomeThing() => def, pro, pub
- runStaticThing()=> def, pro, pub
ClassAA (같은 패키지, 상속)
public class ClassAA extends ClassA {
public void runSomething() {
// System.out.println(pri); // private 접근 불가
System.out.println(def);
System.out.println(pro);
System.out.println(pub);
}
public static void runStaticThing() {
System.out.println(def);
System.out.println(pro);
System.out.println(pub);
}
public static void main(String[] args) {
new ClassA().runSomething();
ClassA.runStaticThing();
}
}
- runSomeThing() => def, pro, pub
- runStaticThing()=> def, pro, pub
ClassAB (다른 패키지, 상속)
import Practice.Practice1.ClassA;
public class ClassAB extends ClassA {
public void runSomething() {
System.out.println("runSomething()");
System.out.println(pro); // ClassA 로부터 상속받았으므로 접근 가능
System.out.println(pub);
System.out.println(ClassA.pro);
System.out.println(ClassA.pub);
ClassAB ab = new ClassAB();
System.out.println(ClassAB.pro); // ClassA로부터 상속받았으므로 접근 가능
System.out.println(ClassAB.pub);
}
public static void runStaticThing() {
System.out.println("runStaticThing()");
// 클래스 레벨에서 접근 가능
System.out.println(ClassA.pro); //상속받은 클래스에서도 클래스 이름으로 접근 가능
System.out.println(ClassA.pub);
}
public static void main(String[] args) {
new ClassAB().runSomething();
ClassAB.runStaticThing();
}
}
- runSomeThing() => pro, pub
- runStaticThing()=> pro, pub
static 메서드에서도 Protected 속성에 접근한다는 점이 이전 non-static 속성과 다르다.
static 멤버는 클래스에서 관리하므로 static 메서드가 클래스에 속하여 인스턴스와 관계가 없더라도 상속받았음을 알고 접근할 수 있다.
ClassC (다른 패키지)
package Practice.Practice2;
import Practice.Practice1.ClassA;
public class ClassC {
void runSomething() {
System.out.println(ClassA.pub);
}
static void runStaticThing() {
System.out.println(ClassA.pub);
}
public static void main(String[] args) {
new ClassAB().runSomething();
ClassAB.runStaticThing();
}
}
- runSomething() => pub
- runStaticThing()=> pub
기억할 부분
- 상속을 받지 않았다면 객체 멤버는 객체를 생성한 후 객체 참조 변수를 이용해 접근해야한다.
- 정적 멤버는 클래스명.정적멤버 형식으로 접근하는 것을 권장한다.
- 클래스 인스턴스들의 공통된 특성이므로 클래스가 관리한다. 클래스로 접근하자.
- 메모리의 물리적 접근 측면에서 바로 클래스에서 접근하는 것이 스택과 힙을 거쳐 클래스로 값을 접근하는 것보다 효율적이다.
참조 변수의 복사
- Call By Value(값에 의한 호출)
- Call By Reference(참조에 의한 호출) = Call By Address(주소에 의한 호출)
Call By Value
public class CallByValue {
public static void main(String[] args){
int a = 10;
int b = a; // Call By Value
a = 20
System.out.println(a); // 10
System.out.println(b); // 20
}
}
- a가 가진 값이 b에 복사된다.
Call By Reference
public class CallByReference{
public static void main(String[] args){
Animal ref_a = new Animal();
Animal ref_b = ref_a; // Call By Reference
ref_a.age = 10;
ref_b.age = 20;
System.out.println(ref_a.age); // 20
System.out.println(ref_b.age); // 20
}
}
class Animal {
public int age;
}
- ref_a가 가진 주소가 ref_b에 복사된다.
- 두 변수가 같은 객체를 참조한다.
즉, 기본 자료형 변수는 저장하고 있는 값을 값 그 자체로 판단하고, 참조 변수는 저장하고 있는 값을 주소로 판단한다.
기억할 부분
- 기본 자료형 변수는 값을 값 자체로 판단한다.
- 참조 자료형 변수는 값을 주소, 즉 포인터로 판단한다.
- 기본 자료형 변수를 복사할 때, 참조 자료형 변수를 복사할 때 일어나는 일은 같다.
즉, 가지고 있는 값을 그대로 복사해서 넘겨 준다.
결론
캡 : 캡슐화 (정보 은닉) - 접근 제어자 public, protected, default, private
상 : 상속(재사용과 확장) - extends, implements
- 상속 : 하위 클래스 is kind of 상위 클래스, 분류도
- 인터페이스 : 클래스 is able to 인터페이스
추 : 추상화(모델링) - class, objects
- 추상화란 구체적인 것을 분해해서 관심 영역(애플리케이션 경계)에 있는 특성만 가지고 재조합하는 것 = 모델링
다 : 다형성(사용편의성) - 오버라이딩, 오버로딩
- 오버로딩 : 함수명 하나를 가지고 인자 목록만 다르게 하면 된다.
- 오버라이딩 : 하위 클래스가 재정의한 메서드를 알아서 호출해준다.
Reference
https://m.yes24.com/Goods/Detail/17350624
+ 책 내용 기반으로 추가적으로 내용을 작성했는데 오개념이 있다면 지적 부탁드립니다~!!
'Spring > 객체지향' 카테고리의 다른 글
코드에 SRP 원칙 적용 후 Mock 테스트 작성하기 (0) | 2024.04.06 |
---|---|
객체 지향과 디자인 패턴 (0) | 2024.04.02 |
객체 지향 설계 5원칙: SOLID (0) | 2024.04.01 |
Java가 확장한 객체 지향 (abstract, 생성자, static, final, this, super) (0) | 2024.03.26 |
프로그래밍의 발전 및 Java와 절차적/구조적 프로그래밍 (2) | 2024.03.23 |