Wrapper 객체

📌 Java에서 기본 데이터 타입(primitive type)을 객체로 다룰 수 있도록 해주는 클래스들을 말합니다. 기본 데이터 타입은 객체가 아니기 때문에 객체로 다루어야 하는 경우에 Wrapper 클래스를 사용합니다.

 

  • 박싱(기본 데이터를 Wrapper 클래스로 변경하는 행위)해서 객체화된 원시 값들은 이제 클래스처럼, 구현되어 있는 메소드들을 자유롭게 이용이 가능하고, 객체만 할 수 있는 것들을 할 수 있게 됩니다.
  • generic을 알기 위해서는 Wrapper 객체를 이해하는 것이 좋습니다.

좌 : 기본 데이터 타입 / 우 : 객체 데이터 타입

  • 좀 더 전문적인 설명으로, 기본 타입은 '값'의 의미로만 사용되지만 객체로서의 기능이 필요한 경우 추상화를 통해 객체의 속성을 부여한다. 즉, 기본 값을 객체화 해서 "감싼다"라는 의미로 이러한 객체를 Wrapper Class라고 한다.
  • 추가로 반대로 객체를 값으로 되돌릴 경우 언박싱 이라고 한다.

Integer num = new Integer(17);  // Boxing
int n = num.intValue(); // UnBoxing

Character ch = 'X'; // AutoBoxing
char c = ch; // AutoUnBoxing

 

 

 

Generic

📌 Java에서 타입을 매개변수[메서드나 함수에 전달되는 값 또는 정보 ]로 사용할 수 있게 해주는 기능입니다. 즉, 클래스를 만들 때, 어떤 타입을 사용할지 미리 정의하지 않고 나중에 사용할 때 구체적인 타입을 지정할 수 있게 하는 방법입니다.

= 타입을 먼저 정하지 않고 나중에 정할 수 있게 해준다. 코드 재사용성, 안정성 보장에 큰 역활을 한다.

 

public class Generic {
    public String plusReturnFunction(int a, int b) { ... }

}

//=


public class Generic {
    public Object plusReturnFunction(Object a,Object b) { ... }
}

 

  • 데이터를 객체로서 만들어 사용하는 것은 메소드 사용이나 다른 기능적인 면에서 유용하지만
  • 기본적으로 타입 안정성이 침해받는다(int는 정수형 이라는 규정을 깨고 애매한 형태의 타입을 사용하니..)
  • 추상화로 인해 타입의 명확성이 떨어진 만큼 연산자의 사용도 당연히 불가능해진다.
 

타입 변수

제네릭에서 사용되는 변수로, 타입 매개변수라고도 불립니다. 타입 변수는 클래스, 메서드, 또는 인터페이스에서 사용될 타입을 일반화하여 정의한 변수입니다. 즉, 구체적인 타입을 지정하지 않고, 유연하게 다양한 타입을 처리할 수 있도록 하는 역할을 합니다.

 

  • 아래의 T를 말합니다.
// 1.
public class Generic<T> {
		// 2.
    private T t;
    // 3.
    public T get() {
        return this.t;
    }

    public void set(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
				// 4.
        Generic<String> stringGeneric = new Generic<>();
				// 5.
        stringGeneric.set("Hello World");
				
        String tValueTurnOutWithString = stringGeneric.get();

        System.out.println(tValueTurnOutWithString);
    }
}
더보기
  1. 제네릭은 클래스 또는 메서드에 사용할 수 있습니다. 클래스 이름 뒤에 <> 문법 안에 들어가야 할 타입 변수를 지정합니다.
  2. 🤔 타입 변수의 이름을 T로 사용하는 이유는 일종의 컨벤션이기 때문입니다. 당연히 컨벤션이기 때문에, 여러분이 원하시는 어떠한 변수를 넣어도 문제가 없습니다. 다만 당연히 컨벤션이기 때문에, 굳이 다른 이유가 없다면 T를 사용하는 게 협업에 유리하겠죠? 이와 함께 자주 사용되는 변수명으로는 T,U,V, E 등이 있습니다.
  3. 선언 해둔 타입 변수는 해당 클래스 내에서 특정한 타입이 들어갈 자리에 대신 들어갈 수 있습니다. 2번에서는 private 프로퍼티인 t의 타입이 들어가야 할 자리에 들어갔네요
  4. 메서드의 리턴 타입에 들어가는 것 역시 마찬가지입니다.
  5. 여기부터는 제네릭을 통해 구현한 클래스를 사용하는 부분입니다, 클래스에 선언했기 때문에 인스턴스를 만들기 위해서 타입 변수에 들어갈 실제 변수의 값을 넣어줘야 합니다. 여기서는 String이네요
  6. 아까 타입 변수로 대체해뒀던 곳에 String이 들어가 있기 때문에, 이와 같이 사용할 수 있습니다.

 

 

 

Generic 용어

< DevAndy 님의 Live Study 14주차 발췌>

 

Box<T> 제네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
T 타입 변수 또는 타입 매개변수. (T는 타입 문자)
Box 원시 타입(raw type)

 

https://velog.io/@dev-mage/hello-java-world-generics-type-parameter

 

제네릭 타입 호출 타입 변수에 타입을 지정하는 것을 말한다.
매개변수화된 타입 지정된 타입(String 같은 것들)
  • 컴파일 전까지는 Menu<Coffee>는 여전히 제네릭 타입으로 기능하고, 존재하지만
  • 컴파일 이후 '원시 타입'인 Menu로 바뀐다. 
  • 즉, 제네릭 타입은 제거된다.

 

Generic 다형성

제네릭 클래스의 객체를 생성할 때 참조 변수에 지정한 제네릭 타입생성자에 지정한 제네릭 타입은 일치해야 한다는 점에서 제네릭 타입은 상속 관계를 고려하지 않습니다. 하지만 타입 매개변수를 사용하는 제네릭 클래스에서 상속 관계에 있는 자식 객체를 저장할 수 있는 방법도 있습니다. 다만, 저장할 때꺼낼 때 주의해야 합니다.

1. 제네릭 클래스의 객체 생성 시 제네릭 타입 일치

제네릭 클래스는 타입 안전성을 제공하기 위해 참조 변수생성자에서 지정한 타입이 일치해야 합니다. 예를 들어:

// 클래스 상속 구조
public class Menu { ... }
public class CoffeeMenu extends Menu { ... }

// 제네릭 객체 생성 시
ArrayList<Menu> menus1 = new ArrayList<Menu>();  // 올바름
ArrayList<Menu> menus2 = new ArrayList<CoffeeMenu>();  // 에러: 타입 불일치

여기서 ArrayList<Menu>와 ArrayList<CoffeeMenu>는 타입이 불일치합니다. **ArrayList<Menu>**는 Menu 타입의 객체만 저장할 수 있으며, **ArrayList<CoffeeMenu>**는 CoffeeMenu 객체만 저장할 수 있습니다. 다형성을 적용하려면 제네릭 타입이 일치해야 하므로, menus2는 ArrayList<Menu>로 선언해야 합니다.

2. 제네릭 클래스에서 다형성 사용

제네릭 클래스에서 타입 매개변수상속 관계를 반영하지 않지만, 타입이 일치하는 경우에는 다형성을 적용할 수 있습니다. List<Menu>로 선언하고, CoffeeMenu 객체를 추가할 수 있습니다. 이는 제네릭 타입이 아닌 클래스 타입 간 다형성을 적용한 예입니다.

List<Menu> menus = new ArrayList<Menu>();  // List<Menu>로 선언
menus.add(new CoffeeMenu());  // OK: CoffeeMenu는 Menu의 자식 클래스

3. 형변환 필요

하지만 제네릭 타입의 타입 안전성을 유지하기 위해, 저장된 객체를 꺼낼 때에는 형변환이 필요합니다. ArrayList<Menu>에 저장된 객체를 CoffeeMenu 타입으로 꺼낼 때는 형변환을 해줘야 합니다.

List<Menu> menus = new ArrayList<Menu>();
menus.add(new CoffeeMenu());  // OK: CoffeeMenu는 Menu의 자식 클래스

// 꺼낼 때 형변환이 필요
CoffeeMenu coffeeMenu = (CoffeeMenu) menus.get(0);  // 형변환 필요

형변환을 통해 Menu 객체를 CoffeeMenu 객체로 변환할 수 있지만, 런타임 오류가 발생할 수 있습니다. 만약 menus에 CoffeeMenu 객체가 아닌 다른 Menu 객체가 저장되어 있으면 ClassCastException이 발생합니다.

 

제네릭의 제한

1. 객체의 static 멤버에 사용할 수 없습니다.

static T get() { ... } // 에러

static void set(T t) { ... } // 에러
  • 타입 변수는 인스턴스 변수로 간주되고, 모든 객체에 동일하게 동작해야 하는 static 필드 특성상 사용할 수 없습니다

2. 제네릭 배열을 생성할 수 없습니다.

 

제네릭의 문법

 

1. 다수의 타입 변수를 사용할 수 있습니다.

public class Generic<T, U, E> {
    public E multiTypeMethod(T t, U u) { ... }
}


Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);

 

2. 다형성 즉 상속과 타입의 관계는 그대로 적용됩니다.

  • 대표적으로 부모 클래스로 제네릭 타입 변수를 지정하고, 그 안에 자식 클래스를 넘기는 것은 잘 동작합니다.

 

3. 와일드카드를 통해 제네릭의 제한을 구체적으로 정할 수 있습니다.

public class ParkingLot<T extends Car> { ... }

ParkingLot<BMW> bmwParkingLot = new ParkingLot();
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // error!
  1. <? extends T> : T와 그 자손들만 사용 가능
  2. <? super T> : T와 그 조상들만 가능
  3. <?> : 제한 없음
더보기

이렇게 제한을 하는 이유는 다형성 때문입니다. 위의 코드에서, T는 Car의 자손 클래스들이라고 정의했기 때문에, 해당 클래스 내부에서 최소 Car 객체에 멤버를 접근하는 코드를 적을 수 있습니다. 반대로 그러한 코드들이 있을 여지가 있기 때문에, Car 객체의 자손이 아닌 클래스는 제한하는 것이죠

 

**와일드카드(Wildcard)**는 제네릭에서 사용되는 특별한 기호(?)로, 타입을 불특정으로 지정할 때 사용됩니다. 와일드카드는 타입을 유연하게 다룰 수 있도록 도와주며, extends나 super와 함께 사용하여 타입을 제한할 수 있습니다.

 

 

4. 메서드를 스코프로 제네릭을 별도로 선언할 수 있습니다.

출처 : Head First Java

// 또는 ..
static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
  1. 이렇게 반환 타입(T) 앞에 <> 제네릭을 사용한 경우, 해당 메서드에만 적용되는 제네릭 타입 변수를 선언할 수 있습니다.
  2. 타입 변수를 클래스 내부의 인스턴스 변수 취급하기 때문에 제네릭 클래스의 타입 변수를 static 메서드에는 사용할 수 없었지만, 제네릭 메소드의 제네릭 타입 변수는 해당 메소드에만 적용되기 때문에 메소드 하나를 기준으로 선언하고 사용할 수 있습니다.
  3. 같은 이름의 변수를 사용했다고 해도 제네릭 메소드의 타입 변수는 제네릭 클래스의 타입 변수와 다릅니다.
public class Generic<T, U, E> {
		// Generic<T,U,E> 의 T와 아래의 T는 이름만 같을뿐 다른 변수
    static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
}

 

 

스코프

📌 프로그래밍에서 변수나 함수, 클래스 등의 유효 범위를 의미합니다. 즉, 코드 내에서 어떤 변수나 함수가 접근 가능한 영역을 말합니다.

스코프의 종류

  1. 전역 스코프(Global Scope): 프로그램 전체에서 접근할 수 있는 범위입니다. 전역 변수나 전역 함수가 여기에 해당합니다. 예를 들어, 클래스의 필드나 정적 변수들이 전역 스코프에 속합니다.
  2. 지역 스코프(Local Scope): 특정 함수나 메서드, 블록 내에서만 유효한 범위입니다. 함수 안에 선언된 변수나 매개변수는 그 함수 내부에서만 접근할 수 있습니다.
  3. 블록 스코프(Block Scope): 특정 코드 블록(if, for, while 등의 제어문) 내에서만 유효한 범위입니다. let이나 const로 선언된 변수는 이 블록 내에서만 접근 가능합니다.
  4. 클래스 스코프(Class Scope): 클래스 내에서 정의된 필드나 메서드는 그 클래스 내부에서만 접근할 수 있습니다.
public class ScopeExample {

    // 전역 변수
    static int globalVar = 10;

    public static void main(String[] args) {
        // main 메서드 내에서만 유효한 지역 변수
        int localVar = 5;

        // if문 안에서만 유효한 블록 변수
        if (true) {
            int blockVar = 20;
            System.out.println(blockVar); // 출력 가능
        }

        System.out.println(globalVar); // 출력 가능
        System.out.println(localVar);  // 출력 가능

        // System.out.println(blockVar); // 오류! blockVar는 if문 블록 밖에서는 접근 불가
    }
}
더보기

코드 설명

  1. 전역 변수(globalVar): globalVar는 클래스 어디서든지 접근할 수 있는 변수입니다. 여기서는 main 메서드 내에서 사용됩니다.
  2. 지역 변수(localVar): localVar는 main 메서드 내에서만 사용 가능하고, 다른 메서드나 클래스 바깥에서는 접근할 수 없습니다.
  3. 블록 변수(blockVar): blockVar는 if문 블록 내에서만 유효한 변수로, 블록 외부에서는 접근할 수 없습니다.

 

제네릭에서의 스코프

제네릭에서 스코프는 제네릭 타입 매개변수가 유효한 범위를 나타냅니다. 예를 들어, 클래스 레벨에서 제네릭을 선언하면, 그 클래스 내에서 제네릭 타입을 사용할 수 있고, 메서드 레벨에서 제네릭을 선언하면, 그 메서드 내에서만 제네릭 타입을 사용할 수 있습니다.

public class MyClass<T> {
    T obj; // 클래스 스코프

    public <E> void print(E element) {  // 메서드 스코프
        System.out.println(element);
    }
}

 

  • 클래스 스코프: 클래스 자체에 제네릭 타입 T를 선언하면, 클래스 내부의 모든 메서드와 필드에서 T를 사용할 수 있습니다.
  • 메서드 스코프: 메서드 내에서 제네릭 타입 E를 선언하면, 그 메서드 내에서만 E를 사용할 수 있습니다.

 

 

자료구조

 

1. 자료구조와 인터페이스

  • 자바의 **컬렉션(Collection)**은 컴퓨터공학의 자료구조 개념을 추상화하여 구현한 것입니다.
  • 자바의 인터페이스는 실제 구현이 없고, 추상적인 명세(규격)만을 정의합니다. 즉, 인터페이스는 "어떤 행동을 해야 한다"라는 규칙만 제시하고, 실제 동작은 이를 구현한 클래스에서 정의됩니다.
  • 예를 들어, List 인터페이스는 순서를 가지며 중복을 허용하는 자료구조의 규칙을 정의하고, 이를 구현하는 클래스(예: ArrayList, LinkedList)는 이 규칙을 따릅니다.

2. 배열 vs. 리스트

  • 배열은 메모리에서 연속적인 공간을 차지하고, 인덱스를 사용해 빠르게 접근할 수 있지만, 중간에 데이터를 추가하거나 삭제할 때 비용이 큽니다.
  • 리스트는 배열처럼 순서를 가지고 있지만, 배열과 달리 동적 크기를 가지며, 특정 연산에 따라 구현체가 달라질 수 있습니다.

3. 제네릭과 자료구조

  • 제네릭은 자료구조에 저장되는 데이터 타입을 유연하게 정의할 수 있도록 합니다. 예를 들어, List<Integer>는 Integer 타입만 저장할 수 있도록 제한하고, List<String>은 String 타입만 저장할 수 있습니다.
  • List<E>와 같은 제네릭 인터페이스는 저장되는 데이터 타입을 추상화하여, 다양한 타입에 대해 동일한 동작을 적용할 수 있도록 해줍니다.

4. List 인터페이스 예시

  • 자바에서 List<E> 인터페이스는 데이터를 저장하고 관리하는 다양한 메서드(add(), size(), isEmpty() 등)를 정의하고 있습니다.
  • addAll() 메서드와 같이 와일드카드(? extends E)를 사용하면 다른 타입의 컬렉션을 받아 처리할 수 있습니다.

5. Collection 구조

  • 자바의 컬렉션은 Iterable을 상속하고, Collection 인터페이스를 기반으로 하여 List, Queue, Set 등이 구현됩니다.
  • 자료구조를 사용할 때는 인터페이스에서 제공하는 기능을 확인하고, 실제 구현체에서 어떻게 동작하는지 살펴보는 것이 중요합니다.

 

자바 컬렉션

결론

  • 자바의 컬렉션은 자료구조의 개념을 추상화하고, 이를 인터페이스와 제네릭을 통해 구현한 것
  • 자료구조는 추상적인 타입으로 정의되고, 실제 동작은 구현체에서 정의됩니다. 제네릭을 사용하면 다양한 데이터 타입을 처리할 수 있는 유연성을 제공한다. 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] NULL  (0) 2024.11.13
[JAVA] 쓰레드 & 람다 함수 & 스트림  (3) 2024.11.13
[JAVA] 오류 및 예외에 대한 이해  (1) 2024.11.12
[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12

오류 및 예외에 대한 이해

 

오류(Error)🔥 예외(Exception)🚨
  • 오류(Error)는 일반적으로 회복이 불가능한 문제입니다.
    • 이는 시스템 레벨에서, 또는 주로 환경적인 이유로 발생합니다.
    • 코드의 문제로 발생하는 경우도 있지만, 일단 발생하는 경우 일반적으로 회복이 불가능합니다.
    • 에러가 발생한 경우 우리는 어떠한 에러로 프로그램이 종료되었는지를 확인하고 대응합니다.
  • 예외(Exception)는 일반적으로 회복이 가능한 문제입니다.
    • 회복이 가능하다는 전제는 우리가 “그 예외가 발생할 수 있다는 것을 인지하고, 대응했을 것입니다”.
    • 현실적으로 코드 레벨에서 할 수 있는 문제 상황에 대한 대응은 “예외 처리”에 속합니다.
  • 보통 만나게 되는 문제는 예외이고 이는 "예외처리"로 예방이 가능하다.
  • 보통 예외는 코드의 실행, 예외 처리 관점 2가지의 종류로 나뉘는데
코드 실행 관점 예외처리 관점
  • 컴파일 에러(예외) 📂
    • .java 파일을 .class 파일로 컴파일할 때 발생하는 에러
    • 대부분 여러분이 자바 프로그래밍 언어의 규칙을 지키지 않았기 때문에 발생합니다.
    • 예를 들어 있지 않은 클래스를 호출한다거나, 접근이 불가능한 프로퍼티나 메소드에 접근한다거나 하는 경우에 발생합니다.
    • 컴파일 에러가 발생하는 경우 해결 방법은 문법에 맞게 다시 작성하는 것입니다.
  • 런타임 에러(예외) ❤️‍🔥
    • 우리가 주로 다루게 될 에러(예외)입니다.
    • 문법적인 오류는 아니라서, 컴파일은 잘 되었지만 “프로그램”이 실행 도중 맞닥뜨리게 되는 예외입니다.
확인된 예외✅ (Checked Exception)
  • 컴파일 시점에 확인하는 예외입니다.
  • 반드시 예외 처리를 해줘야 하는 예외입니다.

    주로 외부 자원(파일, 네트워크, 데이터베이스 등)과의 작업에서 발생할 수 있는 예외입니다.



미확인된 예외🚫 (Unchecked Exception)
  • 런타임 시점에 확인되는 예외입니다.
  • 예외 처리가 반드시 필요하지 않은 예외입니다.

    프로그래머의 실수로 발생하는 예외(정수를 0으로 나눈다던가...)

 

+ 컴파일 에러는 예외 처리를 하지 않아서 발생하는 오류()를 말한다. 만약 예외 처리를 생각도 못했는데 컴파일 중 에러가 나왔다면 이건 그냥 에러다.

 


1. Checked Exception (체크드 예외)

컴파일 타임에 반드시 예외 처리를 요구하는 예외입니다.
컴파일러가 예외 처리(try-catch 또는 throws 선언)가 되어 있는지 확인하며, 처리하지 않으면 컴파일 에러가 발생합니다.

특징

  • 발생 가능성이 높은 예외로 간주되며, 반드시 처리해야 합니다.
  • 주로 외부 환경과 상호작용하는 코드에서 발생합니다.
    예: 파일 입출력, 네트워크 연결, 데이터베이스 접근 등.
  • Exception 클래스에서 파생되지만, RuntimeException을 제외한 모든 예외가 해당됩니다.

대표적인 Checked Exception

  • IOException (파일 입출력 오류)
  • SQLException (SQL 관련 오류)
  • ClassNotFoundException (클래스를 찾을 수 없음)
import java.io.*;

public class CheckedExample {
    public static void main(String[] args) {
        try {
            FileReader reader = new FileReader("nonexistent.txt");  // 파일 읽기 시도
        } catch (FileNotFoundException e) {
            System.out.println("파일을 찾을 수 없습니다: " + e.getMessage());
        }
    }
}

 

  • 여기서 FileReader는 파일이 존재하지 않을 경우 FileNotFoundException을 발생시킵니다.
  • 이를 try-catch 블록으로 처리하지 않으면 컴파일 에러가 발생합니다.

 

 

 

보통 대부분의 예외는 아래의 처리 방식을 기준으로 한다

1 예외를 인지, 정의 = 예외 클래스 생성
2 예외 발생시 알림 = throw( 예약어 )
3 사용자가 예외 핸들링(처리)

 

2. Unchecked Exception (언체크드 예외)

컴파일 타임에 예외 처리를 강제하지 않는 예외입니다.
예외 처리를 하지 않아도 프로그램은 정상적으로 컴파일됩니다. 다만, 런타임 시 발생하면 프로그램이 중단될 수 있습니다.

특징

  • 주로 프로그래머의 실수로 인해 발생합니다.
  • 개발자가 예외 처리를 강제받지 않아도 되지만, 적절히 처리해야 프로그램이 중단되지 않습니다.
  • RuntimeException 클래스와 그 하위 클래스들이 포함됩니다.

대표적인 Unchecked Exception

  • NullPointerException (널 참조 접근)
  • ArrayIndexOutOfBoundsException (배열 인덱스 초과)
  • ArithmeticException (0으로 나누기)
  • ClassCastException (잘못된 형변환)

예제

public class UncheckedExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        // 예외 처리 없이 실행
        System.out.println(numbers[5]);  // ArrayIndexOutOfBoundsException 발생
    }
}
  • 이 코드는 예외 처리를 하지 않아도 컴파일은 되지만, 실행 시 ArrayIndexOutOfBoundsException이 발생하며 프로그램이 중단됩니다.

 

3. Checked vs Unchecked 정리

구분 Checked Exception Unchecked Exception
검사 시점 컴파일 타임 런타임
예외 처리 강제 여부 예, 처리하지 않으면 컴파일 에러 발생 아니요, 처리하지 않아도 컴파일 가능
주요 원인 외부 환경(입출력, 네트워크 등) 프로그래머 실수 또는 논리 오류
상위 클래스 Exception (단, RuntimeException 제외) RuntimeException
대표 예 IOException, SQLException NullPointerException, ArithmeticException

 

4. 실전에서의 사용

  • Checked Exception: 외부 환경과 상호작용하는 코드에서 주로 발생하므로 적절히 예외 처리를 반드시 해야 합니다.
    • 예: 파일을 읽거나 쓰는 작업에서 파일이 없을 가능성을 항상 고려.
  • Unchecked Exception: 런타임 중 예상치 못한 오류를 처리하려면 예외 처리를 추가로 넣을 수 있지만, 기본적으로 개발자가 코드를 주의 깊게 작성하는 것이 중요합니다.
    • 예: 배열 인덱스를 안전하게 접근하도록 범위를 체크.

 

5. 코드 설계 관점

  • Checked Exception은 호출자에게 예외를 알리는 역할을 하므로 예측 가능한 문제를 나타낼 때 사용.
  • Unchecked Exception코드의 버그나 프로그래머 실수를 나타내며, 예외가 발생했을 때 빠르게 수정을 유도합니다.

 

예외 발생과 try-catch, finally 문

 

자바에서 예외는 크게 **Checked Exception**과 Unchecked Exception 두 가지로 구분됩니다. 두 예외의 차이는 컴파일러가 예외 처리 여부를 검사하는지 여부에 따라 나뉩니다.

throw

📌 프로그래머가 의도적으로 예외를 발생시킬 때 사용하는 키워드입니다. 주로 특정 조건에서 문제가 발생할 가능성이 있다고 판단될 때 직접 예외를 던져서(발생시켜서) 경고하는 용도로 사용됩니다.

public class Main {
    public static void main(String[] args) {
        try {
            checkEligibility(15);  // 18세 미만이므로 예외 발생
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public static void checkEligibility(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("나이는 18세 이상이어야 합니다.");  // 예외 발생
        }
        System.out.println("자격이 있습니다.");
    }
}

 

  • 실제로 예외가 발생한 후 실행되는 함수이기에, 이 코드가 실행되면 throw 문과 함께 메서드가 종료된다.
  • 이게 있는 이유는 아래 설명할 예외 던지기 때문에 필요하다.
  • 일반적인 상황에서 프로그래머가 수정을 위해서라면, 컴파일 에러면 충분하다.

예외 던지기

  • 메서드에서 발생한 예외를 처리한 후 다시 호출한 쪽으로 전달하거나, 처리하지 않고 바로 호출한 쪽으로 넘기는 경우에 사용됩니다.

 

1. 예외 되던지기를 사용하는 이유

예외를 처리하지 않고 상위 메서드로 넘기는 이유는:

  1. 책임 분리
    • 모든 메서드가 자신의 역할에만 집중해야 하기 때문입니다.
    • 예외를 완전히 처리하기 위한 **문맥(Context)**은 상위 메서드에서 더 많이 알고 있을 가능성이 큽니다.
    • 예를 들어, readFile 메서드가 파일을 읽는 동안 오류가 발생했을 때, 이 메서드가 문제를 해결할 수는 없고, 상위 메서드에서 다른 파일을 선택하거나 사용자에게 알리는 등 더 적절한 작업을 할 수 있습니다.
  2. 공통 처리 로직 적용
    • 상위 계층에서 예외를 한꺼번에 처리하면, 중복 코드가 줄어듭니다.
    • 예를 들어, 여러 메서드에서 파일을 다룬다면, 파일이 없을 경우 기본적으로 보여줄 에러 메시지는 상위 계층에서 일괄 처리할 수 있습니다.
  3. 다양한 예외의 관리
    • 하위 메서드에서 발생하는 여러 유형의 예외를 상위 계층에서 분류하고 관리할 수 있습니다.
    • 예: FileNotFoundException, SQLException 등 서로 다른 예외를 공통된 방식으로 처리하거나 변환.

 

2. 예제 코드: 두 번의 예외 처리

(1) 예외를 처리한 후 다시 던지는 경우

public class ExceptionRethrowingExample {
    public static void main(String[] args) {
        try {
            processFile();
        } catch (Exception e) {
            // 최상위 메서드(main)에서 최종적으로 예외 처리
            System.out.println("main에서 예외 처리: " + e.getMessage());
        }
    }

    static void processFile() throws Exception {
        try {
            readFile();
        } catch (Exception e) {
            // 하위 메서드에서 일부 로그를 남기고 다시 예외를 던짐
            System.out.println("processFile에서 예외 처리 중... 메시지: " + e.getMessage());
            throw e; // 예외를 상위 메서드로 넘김
        }
    }

    static void readFile() throws Exception {
        // 파일 읽기 중 예외 발생
        throw new Exception("파일 읽기 실패");
    }
}

processFile에서 예외 처리 중... 메시지: 파일 읽기 실패
main에서 예외 처리: 파일 읽기 실패
  • readFile()에서 예외 발생 → processFile()에서 로그 처리 후 예외 되던지기 → main()에서 최종 처리.
  • processFile()이 예외를 일부 처리하고 다시 던짐으로써 예외가 호출 계층을 따라 전달됩니다.

 

(2) 예외 정보를 추가하여 되던지기

public class ExceptionRethrowingWithInfo {
    public static void main(String[] args) {
        try {
            processFile();
        } catch (Exception e) {
            // 최종적으로 예외 처리
            e.printStackTrace();
        }
    }

    static void processFile() throws Exception {
        try {
            readFile();
        } catch (Exception e) {
            // 새로운 예외로 감싸서 추가 정보를 전달
            throw new Exception("processFile 중 문제 발생", e);
        }
    }

    static void readFile() throws Exception {
        // 원래 예외 발생
        throw new Exception("파일 읽기 실패");
    }
}


java.lang.Exception: processFile 중 문제 발생
	at ExceptionRethrowingWithInfo.processFile(ExceptionRethrowingWithInfo.java:10)
	at ExceptionRethrowingWithInfo.main(ExceptionRethrowingWithInfo.java:4)
Caused by: java.lang.Exception: 파일 읽기 실패
	at ExceptionRethrowingWithInfo.readFile(ExceptionRethrowingWithInfo.java:16)
	... 2 more
  • 예외를 감싸면서 추가 정보를 전달하기 때문에, 디버깅 시 문제의 흐름을 쉽게 파악할 수 있습니다.

 

3. 예외 되던지기의 사용 사례

대표적인 사용 상황

  1. 모듈 간 책임 분리
    • 예외 처리 책임을 호출한 메서드에 맡기고, 중간 메서드는 일부 로그나 정리 작업만 수행한 뒤 다시 던집니다.
  2. 일부 처리 후 상위 메서드로 전달
    • 예외가 발생했을 때, 중간 메서드에서 로그를 남기거나 상태를 정리한 후 상위 호출자에게 문제를 알립니다.
  3. 예외 타입 변환
    • 저수준 예외(IOException 등)를 고수준 예외(CustomException 등)로 변환해 호출 계층에 전달.

 

4. 정리

  • 예외 되던지기는 하위 메서드에서 처리하지 못하는 예외를 상위 메서드로 전달하는 중요한 기법입니다.
  • 필요한 경우:
    1. 하위 메서드에서 예외를 완전히 처리할 수 없을 때.
    2. 추가 정보를 포함하거나 새로운 예외로 변환할 때.
    3. 호출 계층에서 더 나은 처리를 기대할 때.

 

throws

📌 해당 메서드가 특정 예외를 던질 가능성이 있음을 알리는 역할을 합니다. 예외 처리 책임을 메서드 호출부로 넘길 때 주로 사용됩니다.

 

public class Main {
    public static void main(String[] args) {
        try {
            divide(10, 0);  // 0으로 나누기 시도, 예외 발생
        } catch (ArithmeticException e) {
            System.out.println("예외 발생: " + e.getMessage());
        }
    }

    // 예외가 발생할 가능성이 있으므로 throws로 명시
    public static int divide(int a, int b) throws ArithmeticException {
        if (b == 0) {
            throw new ArithmeticException("0으로 나눌 수 없습니다.");  // 예외 발생
        }
        return a / b;
    }
}

 

 

예외를 handling

📌 말 그대로 예외 처리를 해주면 된다.

1 위험 감지하기 ( throw )
2 위험 처리 ( try-catch(finally) )

 

  • 이건 팁인데, 타인의 코드를 사용할 경우 클래스, 메소드의 에러처리를 잘 해줘야한다. 추후 꼬이면 아주 고통스러운 시간을 보내게 된다...

 

public class StudyException {
    public static void main(String[] args) {
        OurClass ourClass = new OurClass();

        try {
            // 1. 위험한 메소드의 실행을 "시도" 해 봅니다.
            // "시도" 해보는 코드가 들어가는 블럭입니다.
            ourClass.thisMethodIsDangerous();
        } catch (OurBadException e) {
            // 2. 예외가 발생하면, "잡아서" handling 합니다.
            // 예외가 발생하는경우 "handling" 하는 코드가 들어가는 블럭입니다.
						// 즉 try 블럭 내의 구문을 실행하다가 예외가 발생하면
						// 예외가 발생한 줄에서 바로 코드 실행을 멈추고
						// 여기 있는 catch 블럭 내의 코드가 실행됩니다.
            System.out.println(e.getMessage());
        } finally {
            // 3. 예외의 발생 여부와 상관없이, 실행시켜야 하는 코드가 들어갑니다.
            // 무조건 실행되는 코드가 들어가는 블럭입니다.
            System.out.println("우리는 방금 예외를 handling 했습니다!");
        }

    }
}

 

  1. 위험을 감지했다면, try-catch(finally) 키워드 이용하기
    • **try** - **catch**는 각각 중괄호{}를 통해 실행할 코드들을 담습니다.
    • try 단어의 **“시도한다”**라는 뜻에 맞게 중괄호{} 안에는 예외가 발생할 수 있지만 실행을 시도할 코드를 담습니다.
    • catch 단어의 **“잡는다”**라는 의미에 맞게 중괄호{} 안에는 try 안에 있는 코드를 실행하다가 예외가 났을 때 실행할 코드를 담습니다.
      • catch 는 소괄호()를 통해 어떤 예외 클래스를 받아서 처리할지 정의해 주어야 합니다.
      • catch로 모든 예외를 다 받고 싶으면 Exception 을 넣어주면 됩니다.
      • catch로 일부 예외만 받아서 처리하고 싶으면 해당 예외 클래스명을 넣어주면 됩니다.
      • 1개의 try 문에 catch 문은 여러 개 사용할 수 있습니다. ex) 1try : 4catch
    • 기존 **try - catch**의 맨 마지막에 **finally**를 붙여서 마지막에 반드시 실행할 코드를 넣을 수 있습니다.

 

 

모든 클래스에 다 이렇게 넣는가?

  • 당연히 아니다... 핵심적인 로직이 있는 중요한 메서드에만 예외 처리를 추가하거나,
  • 여러 메서드에서 예외가 발생할 수 있는 경우, 상위 메서드(또는 main 메서드 등)에 try-catch 블록을 두어 한 번에 예외를 처리합니다.

 

 

예외 클래스 구조 이해하기

 

자바의 Throwable Class

    • 시작은 모든 객체의 원형인 Object 클래스에서 시작합니다.
    • 아까 정의한 “문제 상황”을 뜻하는 Throwable 클래스가 Object 클래스를 상속합니다.
    • Throwable 클래스의 자식으로 앞서배운 에러(Error)와 예외(Exception) 클래스가 있습니다.
    • 에러(Error) 클래스와 예외(Exception) 클래스는 각각 IOError 클래스, RuntimeException 클래스와 같이 구분하여 처리됩니다.

  • 참고로 그림의 RuntimeException을 상속한 예외들은 UncheckedException, 반대로 상속하지 않은 예외들은 CheckedException으로 구현되어 있습니다.
즉 NullPointException, ArrayIndexOutOfBoundsException 등의 예외 구현체들은 명시적인 에러 처리를 하지 않아도 컴파일 에러가 발생하지는 않겠죠? 또 Checked Exception에 속하는 에러 구현체들은 핸들링 하지 않으면 컴파일 에러가 발생하는 대신, 컴파일이 됐다면 100% 복구가 가능한 에러였다는 것 역시 알아두시면 좋을 것 같습니다.

 

  • 위의 설명대로 exception은 Object 클래스를 통해 많은 예외 처리를 상속 받은 상태이다. 하지만 결국 프로그램의 세계는 무궁무진 하기에 직접 에러를 정의 구현하는 경우도 있다.
class OurBadException extends Exception {
	public OurBadException() {
		super("위험한 행동을 하면 예외처리를 꼭 해야합니다!");
	}
}

 

 

Chained Exception

📌 원인 예외를 새로운 예외에 등록한 후 다시 새로운 예외를 발생시키는데, 이를 예외 연결이라고 합니다.

원인 예외 연결된 예외
b 라이브러리가 없음 함수가 실행 안됨
  • 이렇게 하는 이유는 추후 한번에 묶어서 다루기가 편하기 때문이다.

 

  • 원인 예외를 다루기 위한 메소드
    • initCause()
      • 지정한 예외를 원인 예외로 등록하는 메소드
    • getCause()
      • 원인 예외를 반환하는 메소드
// 연결된 예외 
public class main {

    public static void main(String[] args) {
        try {
            // 예외 생성
            NumberFormatException ex = new NumberFormatException("가짜 예외이유");

            // 원인 예외 설정(지정한 예외를 원인 예외로 등록)
            ex.initCause(new NullPointerException("진짜 예외이유"));

            // 예외를 직접 던집니다.
            throw ex;
        } catch (NumberFormatException ex) {
            // 예외 로그 출력
            ex.printStackTrace();
            // 예외 원인 조회 후 출력
            ex.getCause().printStackTrace();
        }

        // checked exception 을 감싸서 unchecked exception 안에 넣습니다.
        throw new RuntimeException(new Exception("이것이 진짜 예외 이유 입니다."));
    }
}

// 출력
Caused by: java.lang.NullPointerException: 진짜 예외이유

 

 

예외 처리

📌예외 처리 방식은 크게 3가지로 나뉜다

1 예외 복구하기
2 예외 처리 회피하기
3 예외 전환하기

 

예외 복구하기

public String getDataFromAnotherServer(String dataPath) {
		try {
				return anotherServerClient.getData(dataPath).toString();
		} catch (GetDataException e) {
				return defaultData;
		}
}
  • 실제로 try-catch로 예외를 처리하고 프로그램을 정상 상태로 복구하는 방법입니다.
  • 가장 기본적인 방식이지만, 현실적으로 복구가 가능한 상황이 아닌 경우가 많거나 최소한의 대응만 가능한 경우가 많기 때문에 자주 사용되지는 않습니다.

 

예외 처리 회피하기

public void someMethod() throws Exception { ... }

public void someIrresponsibleMethod() throws Exception {
		this.someMethod();
}
  • 이렇게 처리하면, someMethod()에서 발생한 에러가 someIrresponsibleMethod()의 throws를 통해서 그대로 다시 흘러나가게 되겠죠, 물론 같은 객체 내에서 이런 일은 하지는 않습니다, 예외 처리 회피를 보여드리기 위한 단순한 예시 코드입니다.
  • 관심사를 분리해서 한 레이어에서 처리하기 위해서 이렇게 에러를 회피해서 그대로 흘러 보내는 경우도 있습니다.

 

예외 전환하기

public void someMethod() throws IOException { ... }

public void someResponsibleMethod() throws MoreSpecificException {
		try {
			this.someMethod();
		} catch (IOException e) {
			throw new MoreSpecificException(e.getMessage());
		}
}
  • 예외 처리 회피하기의 방법과 비슷하지만, 조금 더 적절한 예외를 던져주는 경우입니다.
  • 보통은 예외 처리에 더 신경 쓰고 싶은 경우나, 오히려 RuntimeException처럼 일괄적으로 처리하기 편한 예외로 바꿔서 던지고 싶은 경우 사용합니다.

< 추가설명 >

 

  • 추상화된 예외로 변환: 구체적인 예외를 숨기고, 상위 계층에서는 필요하지 않은 세부 사항을 가리지 않도록 상위 계층에서 의미 있는 예외로 전환합니다. 예를 들어, 데이터베이스 접근 중 SQLException이 발생하면, 이 예외를 DataAccessException과 같은 의미 있는 커스텀 예외로 바꾸는 식입니다.
  • 런타임 예외로 일괄 처리: Checked Exception을 RuntimeException 계열로 변환하여 전체 코드에서 예외 처리를 일괄적으로 처리할 수 있게 합니다. 예외 처리를 모든 곳에서 강제하지 않아 코드가 더 간결해지죠.

 

 

< 런타임 예외 일괄 처리 부가 설명 >

1. Checked Exception의 특성

  • Checked Exception은 예외가 발생할 가능성이 있는 모든 곳에서 반드시 try-catch 구문으로 처리하거나 throws 키워드를 이용해 메서드 선언부에 던져야 합니다.
  • 이로 인해 예외가 발생할 가능성이 있는 메서드가 여러 군데 있다면, 그곳마다 예외 처리를 반복적으로 작성해야 해서 코드가 길어지고 복잡해질 수 있어요.

2. RuntimeException으로 전환 시 장점

  • RuntimeException 계열 예외는 일명 "unchecked" 예외라고도 불리며, 이 예외는 컴파일러가 예외 처리를 강제하지 않아요.
  • 그래서 try-catch로 처리하지 않아도 컴파일 오류가 발생하지 않습니다.
  • 즉, 반드시 처리하지 않아도 되는 예외로 바뀌므로 코드가 간결해집니다. 예외가 꼭 필요한 상위 레벨에서만 일괄 처리할 수 있어요.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 쓰레드 & 람다 함수 & 스트림  (3) 2024.11.13
[JAVA] Generic  (3) 2024.11.12
[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12

인터페이스

📌 Java에서 클래스가 구현해야 하는 메서드들의 목록을 정의하는 일종의 계약입니다.

 

인터페이스 선언

public interface 인터페이스명 { 

	public static final char A = 'A';
    static char B = 'B';
    final char C = 'C';
    char D = 'D';

    void turnOn(); // public abstract void turnOn();
}

 

  • 모든 멤버 변수는 public static final이어야 합니다.
    • 생략 가능합니다.
  • 모든 메서드는 public abstract이어야 합니다.
    • 생략 가능합니다. (static 메서드와 default 메서드 예외)
  • 생략되는 제어자는 컴파일러가 자동으로 추가해줍니다.

 

  • 단순하게 클래스는 설계도, 인터페이스는 장보기 리스트 라고 생각하면 편하다
  • 클래스는 내용과 기능에 대한 부분이 상세히 기록되지만, 인터페이스는 필요한 기능만 제시되어 있고 구현은 안되어 있다.

 

인터페이스 구현

📌 인터페이스는 추상 클래스와 마찬가지로 직접 인스턴스를 생성할 수 없기 때문에 클래스에 구현되어 생성됩니다.

  • implements 키워드를 사용하여 인터페이스를 구현할 수 있습니다. 
public class 클래스명 implements 인터페이스명 { 
			// 추상 메서드 오버라이딩
			@Override
	    public 리턴타입 메서드이름(매개변수, ...) {
			       // 실행문
	    }
}
  • 인터페이스의 추상 메서드는 구현될 때 반드시 오버라이딩 되어야 합니다.
  • 만약 인터페이스의 추상 메서드를 일부만 구현해야 한다면 해당 클래스를 추상 클래스로 변경해 주면 됩니다.
  • 인터페이스 선언 = 인터페이스(명세서) 제작, 인터페이스 구현 = 인터페이스를 구현한 클래스에서 기능 구현

 

인터페이스 상속

📌 인터페이스 간의 상속이 가능합니다.

  • 인터페이스 간의 상속은 implements 가 아니라 extends 키워드를 사용합니다.
  • 인터페이스는 클래스와는 다르게 다중 상속이 가능합니다.
public class Main extends D implements C {

    @Override
    public void a() {
        System.out.println("A");
    }

    @Override
    public void b() {
        System.out.println("B");
    }

    @Override
    void d() {
        super.d();
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.a();
        main.b();
        main.d();
    }
}

interface A {
    void a();
}

interface B {
    void b();
}

interface C extends A, B {
}

class D {
    void d() {
        System.out.println("D");
    }
}
  • 인터페이스 C는 아무것도 선언되어 있지 않지만 인터페이스 A, B를 다중 상속받았기 때문에 추상 메서드 a, b를 갖고 있는 상태입니다.
  • 따라서 Main 클래스에서 인터페이스 C가 구현될 때 a, b 추상 메서드가 오버라이딩됩니다.
  • 또한 인터페이스의 구현은 상속과 함께 사용될 수 있습니다.

 

  • 뭔가 이쯤 오면 추상 클래스가 생각날건데, 둘이 비슷한 건 맞으나, 사용 목적과 특징이 다르다.
특성 추상 클래스 (Abstract Class) 인터페이스 (Interface)
메서드 구현 메서드 구현이 가능 (일부는 구현되고, 일부는 추상 메서드로) 메서드는 구현 없이 선언만 가능 (Java 8 이후 default 메서드 예외)
상속 방식 단일 상속 (하나의 추상 클래스만 상속 가능) 다중 상속 (여러 개의 인터페이스를 구현 가능)
클래스는 내용이 구현되어 있고 변수나 메소드 명이 같으면
구분이 안되는 일이 있는데 인터페이스는 내용이 없어서
다중 상속이 가능하다.
필드 **상태(필드)**를 가질 수 있으며, 인스턴스 변수로 사용 가능 상수 가질 수 있으며, public static final이어야 함
사용 목적 공통된 기능의 구현과 일부 기능의 재사용을 위해 사용 여러 클래스가 공통된 기능을 갖게 하며, 다형성을 구현하기 위해 사용
인스턴스화 인스턴스화 불가 (추상 클래스 자체로 객체 생성 불가) 인스턴스화 불가 (인터페이스 자체로 객체 생성 불가)

 

 

 

디폴트 메서드와 static 메서드

📌 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드입니다.

  • 메서드 앞에 default 키워드를 붙이며 블럭{ }이 존재해야 합니다.
  • default 메서드 역시 접근 제어자가 public이며 생략이 가능합니다.
  • 추상 메서드가 아니기 때문에 인터페이스의 구현체들에서 필수로 재정의 할 필요는 없습니다.
public class Main implements A {

    @Override
    public void a() {
        System.out.println("A");
    }


    public static void main(String[] args) {
        Main main = new Main();
        main.a();

        // 디폴트 메서드 재정의 없이 바로 사용가능합니다.
        main.aa();
    }
}

interface A {
    void a();
    default void aa() {
        System.out.println("AA");
    }
}

 

static 메서드

📌 인터페이스에서 static 메서드 선언이 가능합니다.

  • static의 특성 그대로 인터페이스의 static 메서드 또한 객체 없이 호출이 가능합니다.
  • 선언하는 방법과 호출하는 방법은 클래스의 static 메서드와 동일합니다.
    • 접근 제어자를 생략하면 컴파일러가 public을 추가해 줍니다. 
public class Main implements A {

    @Override
    public void a() {
        System.out.println("A");
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.a();
        main.aa();
        System.out.println();

        // static 메서드 aaa() 호출
        A.aaa();
    }
}

interface A {
    void a();
    default void aa() {
        System.out.println("AA");
    }
    static void aaa() {
        System.out.println("static method");
    }
}

 

 

다형성

 

자동 타입 변환

public class Main {
    public static void main(String[] args) {
        
        // A 인터페이스에 구현체 B 대입
        A a1 = new B();
        
        // A 인터페이스에 구편체 B를 상속받은 C 대입
        A a2 = new C();
        
    }
}

interface A { }
class B implements A {}
class C extends B {}

 

강제 타입 변환

public class Main {
    public static void main(String[] args) {

        // A 인터페이스에 구현체 B 대입
        A a1 = new B();
        a1.a();
        // a1.b(); // 불가능

        System.out.println("\nB 강제 타입변환");
        B b = (B) a1;
        b.a();
        b.b(); // 강제 타입변환으로 사용 가능
        System.out.println();

        // A 인터페이스에 구편체 B를 상속받은 C 대입
        A a2 = new C();
        a2.a();
        //a2.b(); // 불가능
        //a2.c(); // 불가능

        System.out.println("\nC 강제 타입변환");
        C c = (C) a2;
        c.a();
        c.b(); // 강제 타입변환으로 사용 가능
        c.c(); // 강제 타입변환으로 사용 가능


    }
}

interface A {
    void a();
}
class B implements A {
    @Override
    public void a() {
        System.out.println("B.a()");
    }

    public void b() {
        System.out.println("B.b()");
    }
}
class C extends B {
    public void c() {
        System.out.println("C.c()");
    }
}

 

 

인터페이스의 다형성

// LG TV 구현체를 조작
MultiRemoteController mrc = new LgTv("LG");
mrc.turnOnOff();
mrc.volumeUp();

// 조작 대상을 Samsung TV로 교체
mrc = new SamsungTv("Samsung");
mrc.turnOnOff();
mrc.channelUp();
  • 멀티 리모컨 인터페이스 변수 = TV 구현 객체;를 선언하여 자동 타입 변환된 인터페이스 변수를 사용하여 TV 구현 객체의 기능을 조작할 수 있습니다.
  • TV 구현 객체를 교체해도 멀티 리모컨 인터페이스 변수는 전혀 수정 작업 없이 그대로 기능을 호출할 수 있습니다.
  • 다형성은 ‘여러 가지 형태를 가질 수 있는 능력’이라고 배웠습니다.
  • 사용 방법은 동일하지만 다양한 특징과 결과를 가질 수 있는 것이 바로 다형성입니다.
    • 즉, 멀티 리모컨으로 티비를 사용하는 방법은 동일하지만 어떤 TV 구현 객체가 대입되었느냐에 따라 실행 결과가 다르게 나옴을 통해 다형성이 적용되었음을 확인할 수 있었습니다.
// 매개변수와 반환타입 다형성 확인 메서드
default MultiRemoteController getTV(Tv tv) {
    if(tv instanceof SamsungTv) {
        return (SamsungTv) tv;
    } else if(tv instanceof LgTv){
        return (LgTv) tv;
    } else {
        throw new NullPointerException("일치하는 Tv 없음");
    }
}
  • 또한 인터페이스도 마찬가지로 매개변수와 반환 타입에서 다형성이 적용될 수 있습니다.
  • 위 예제는 반환 타입에는 인터페이스, 매개변수에는 추상 클래스로 다형성이 적용되어있습니다.
    • 인터페이스의 default 메서드입니다.
  • 전체 예제를 통해 더 자세하게 확인해 보겠습니다.
public abstract class Tv {

    private String company; // 티비 회사
    private int channel = 1; // 현재 채널 상태
    private int volume = 0;  // 현재 볼륨 상태
    private boolean power = false; // 현재 전원 상태

    public Tv(String company) {
        this.company = company;
    }

    public void displayPower(String company, boolean power) {
        if(power) {
            System.out.println(company + " Tv 전원이 켜졌습니다.");
        } else {
            System.out.println(company + " Tv 전원이 종료되었습니다.");
        }
    }

    public void displayChannel(int channel) {
        System.out.println("현재 채널은 " + channel);
    }

    public void displayVolume(int volume) {
        System.out.println("현재 볼륨은 " + volume);
    }

    public String getCompany() {
        return company;
    }

    public int getChannel() {
        return channel;
    }

    public int getVolume() {
        return volume;
    }

    public boolean isPower() {
        return power;
    }

    public void setChannel(int channel) {
        this.channel = Math.max(channel, 0);
    }

    public void setVolume(int volume) {
        this.volume = Math.max(volume, 0);
    }

    public void setPower(boolean power) {
        this.power = power;
    }
}

public class SamsungTv extends Tv implements MultiRemoteController{

    public SamsungTv(String company) {
        super(company);
    }

    @Override
    public void turnOnOff() {
        setPower(!isPower());
        displayPower(getCompany(), isPower());
    }

    @Override
    public void channelUp() {
        setChannel(getChannel() + 1);
        displayChannel(getChannel());
    }

    @Override
    public void channelDown() {
        setChannel(getChannel() - 1);
        displayChannel(getChannel());
    }

    @Override
    public void volumeUp() {
        setVolume(getVolume() + 1);
        displayVolume(getVolume());
    }

    @Override
    public void volumeDown() {
        setVolume(getVolume() - 1);
        displayVolume(getVolume());
    }
}

public class LgTv extends Tv implements MultiRemoteController {

    public LgTv(String company) {
        super(company);
    }

    @Override
    public void turnOnOff() {
        setPower(!isPower());
        displayPower(getCompany(), isPower());
    }

    @Override
    public void channelUp() {
        setChannel(getChannel() + 1);
        displayChannel(getChannel());
    }

    @Override
    public void channelDown() {
        setChannel(getChannel() - 1);
        displayChannel(getChannel());
    }

    @Override
    public void volumeUp() {
        setVolume(getVolume() + 1);
        displayVolume(getVolume());
    }

    @Override
    public void volumeDown() {
        setVolume(getVolume() - 1);
        displayVolume(getVolume());
    }
}

public interface MultiRemoteController {

    void turnOnOff();
    void channelUp();
    void channelDown();
    void volumeUp();
    void volumeDown();

    // 매개변수와 반환타입 다형성 확인 메서드
    default MultiRemoteController getTV(Tv tv) {
        if(tv instanceof SamsungTv) {
            return (SamsungTv) tv;
        } else if(tv instanceof LgTv){
            return (LgTv) tv;
        } else {
            throw new NullPointerException("일치하는 Tv 없음");
        }
    }

}

public class Main {
    public static void main(String[] args) {

        // LG TV 구현체를 조작
        MultiRemoteController mrc = new LgTv("LG");
        mrc.turnOnOff();
        mrc.volumeUp();
        mrc.channelDown();
        mrc.channelUp();
        mrc.turnOnOff();

        // 조작 대상을 Samsung TV로 교체
        System.out.println("\n<Samsung TV로 교체>");
        mrc = new SamsungTv("Samsung");
        mrc.turnOnOff();
        mrc.channelUp();
        mrc.volumeDown();
        mrc.volumeUp();
        mrc.turnOnOff();

        // 매개변수, 반환타입 다형성 체크
        System.out.println("\n<매개변수, 반환타입 다형성 체크>");

        MultiRemoteController samsung = mrc.getTV(new SamsungTv("Samsung"));
        samsung.turnOnOff();

        SamsungTv samsungTv = (SamsungTv) samsung;
        samsungTv.turnOnOff();


        System.out.println();
        MultiRemoteController lg = mrc.getTV(new LgTv("LG"));
        lg.turnOnOff();

        LgTv lgTv = (LgTv) lg;
        lgTv.turnOnOff();

    }
}

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] Generic  (3) 2024.11.12
[JAVA] 오류 및 예외에 대한 이해  (1) 2024.11.12
[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12
[JAVA] 접근 제어자  (1) 2024.11.12

상속

📌 상속의 사전적 정의는 부모가 자식에게 물려주는 행위를 말합니다.

  • 객체 지향 프로그램에서도 부모 클래스의 필드와 메서드를 자식 클래스에게 물려줄 수 있습니다.
  • 상속을 사용하면 적은 양의 코드로 새로운 클래스를 작성할 수도 있고 공통적인 코드를 관리하여 코드의 추가와 변경이 쉬워질 수도 있습니다.
  • 이러한 특성 때문에 상속을 사용하면 코드의 중복이 제거되고 재사용성이 크게 증가하여 생산성과 유지 보수성에 매우 유리해집니다. 

= 자바 객체 지향의 중요한 기능 중 하나로, 한번 변수나 메서드 선언하고 그걸 다른 이들이 빌려쓰는 형태로 만들어, 매번 변수, 메서드를 반복해서 선언하는 것을 방지한 방식이다.

 

https://wonkang.tistory.com/88

 

  • 위의 그림처럼 개발자, 댄서, 가수 모두 인간의 특징인 이름,나이,학습,일등의 특징을 가질 수 밖에 없다. 이 인간으로서의 특징을 개발자, 댄서, 가수에 각각 변수로 선언하면 같은 내용이 3배가 될 뿐이니, 상위 클래스(부모)를 생성하여 부모에서 인간의 특징을 정하고 다른 하위 클래스(자식)에게 정보를 공유하는게 효과적이다.

 

public class 자식클래스 extends 부모클래스 {

}

 

  • 상속은 위와 같이 선언하게 되는데, 잘 보면 extends라는 키워드가 존재한다.
  • 말 그대로 확장이란 의미인데, 상속이 이 확장의 개념과 동일하다

 

부모의 확장형 자식

 

  1. 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
  2. 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
  3. 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.

‘부모가 자식보다 큰 사람이니까 부모 클래스도 마찬가지로 자식 클래스 보다 큰 범위겠지?’ 라고 생각하면 망한다. 여기서는 '청출어람'이라는 느낌으로 자식이 부모보다 더 많은 데이터를 가지고 있다. 단 위의 설명처럼 자식은 부모에게 영향을 주지 못하지만 부모는 자식에게 영향을 줄 수 있다.

 

< 1980년대 일반적인 가정을 생각하면 편하다 >

  • 1. 자식이 인터넷 세상에 태어나면서 자연스럽게 부모보다 많은 데이터를 가지게 되었다.
  • 2. 보통 부모님이 자식에게 끼치는 영향이 크다.
  • 3. 부모의 수보다 자식 수가 보통 많다.

 

<예제>

public class Car {

    String company; // 자동차 회사
    private String model; // 자동차 모델
    private String color; // 자동차 색상
    private double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public char changeGear(char type) {
        gear = type;
        return gear;
    }

    public boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    public void horn() {
        System.out.println("빵빵");
    }

}

public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }
}


public class Main {
    public static void main(String[] args) {
        // 부모 클래스 객체에서 자식 클래스 멤버 사용
        Car car = new Car();
        // car.engine = "Orion"; // 오류
        // car.booster(); // 오류

        // 자식 클래스 객체 생성
        SportsCar sportsCar = new SportsCar();
        sportsCar.engine = "Orion";
        sportsCar.booster();

        // 자식 클래스 객체에서 부모 클래스 멤버 사용
        sportsCar.company = "GENESIS";
        sportsCar.setModel("GV80");
        System.out.println("sportsCar.company = " + sportsCar.company);
        System.out.println("sportsCar.getModel() = " + sportsCar.getModel());
        System.out.println();

        sportsCar.horn();
        System.out.println(sportsCar.changeGear('D'));
    }
}

 

 

클래스 간의 관계

📌 클래스 간의 관계를 분석하여 관계 설정을 해줄 수 있다.

  • 상속관계 : is - a (”~은 ~(이)다”)
  • 포함관계 : has - a (”~은 ~을(를) 가지고 있다”) 
상속관계  포함관계
  • 상속관계 : 고래는 포유류다 👍
  • 포함관계 : 고래는 포유류를 가지고 있다…? 🤔
  • 자동차는 타이어를 가지고 있다. 👍
  • 자동차는 차 문을 가지고 있다. 👍
  • 자동차는 핸들을 가지고 있다. 👍

1. 포함(Composition)

포함은 "has-a" 관계를 표현합니다.
한 클래스가 다른 클래스를 속성(멤버 변수)으로 가지는 형태입니다.

특징

  • 유연성: 포함된 객체는 독립적이며, 필요에 따라 다른 객체로 교체할 수 있습니다.
  • 코드 재사용: 여러 클래스에서 포함된 클래스를 재사용할 수 있습니다.
  • 의존성 낮음: 포함된 객체의 내부 구현은 외부 클래스에 영향을 주지 않습니다.
class Circle{
	int x;
    int y;
    int r;
}

----------------------------

class Circle{
	Poiont c = new Point();
    int r;
}

class Point {
	int x;
    int y;
}

2. 상속(Inheritance)

상속은 "is-a" 관계를 표현합니다.
하위 클래스(Subclass)가 상위 클래스(Superclass)의 속성과 메서드물려받는 구조입니다.

특징

  • 코드 재사용: 공통 기능을 상위 클래스에서 정의하고, 하위 클래스에서 상속받아 사용.
  • 유연성 낮음: 상속받은 클래스는 상위 클래스와 강한 결합을 가지며, 부모 클래스의 변경이 하위 클래스에 영향을 줌.
  • 다형성 지원: 부모 타입의 참조로 자식 객체를 사용할 수 있음.

 

3. 포함 vs 상속의 차이점

구분 포함(Composition) 상속(Inheritance)
관계 "has-a" 관계 "is-a" 관계
결합도 낮음 (유연성 높음) 높음 (부모 클래스에 강하게 의존)
코드 재사용성 객체를 포함하여 재사용 가능 공통된 동작을 부모 클래스에서 정의
확장성 포함된 객체를 교체하여 기능 확장 가능 부모 클래스 설계 변경 시 제한적
다형성 지원 다형성 제공하지 않음 다형성(Polymorphism) 지원
목적 객체의 조립, 독립적인 관계 유지 객체의 계층 구조 정의, 공통 기능 상속

 

4. 포함과 상속의 사용 시점

포함(Composition)이 적합한 경우:

  1. 두 클래스가 명확한 "has-a" 관계일 때.
  2. 한 클래스가 다른 클래스의 특정 기능만 필요할 때.
  3. 코드 재사용과 독립성을 유지하면서 기능 확장이 필요할 때.
  4. 설계 변경 가능성이 높아 유연성이 중요한 경우.

상속(Inheritance)이 적합한 경우:

  1. 두 클래스가 명확한 "is-a" 관계일 때.
  2. 하위 클래스가 상위 클래스의 대체 가능성을 가져야 할 때(다형성).
  3. 공통 기능을 상위 클래스에 정의하고, 하위 클래스에서 추가적으로 확장할 때.

5. 실무에서의 선호

  • 포함이 더 자주 사용됩니다.
    이유: 유연성, 낮은 결합도, 유지보수 용이성 때문.
  • 상속은 사용이 간단하지만, 잘못된 설계로 인해 강한 결합이나 불필요한 상속 계층 구조를 초래할 수 있습니다.
    따라서, 상속보다는 포함을 우선적으로 고려하고, 정말 필요한 경우에만 상속을 사용합니다.

 

단일 상속과 다중 상속

📌 Java는 클래스의 다중 상속을 허용하지 않습니다…

  • 다중 상속을 허용하면 복잡한 소프트웨어의 기능을 구현할 때 여러 개의 클래스를 상속받아 쉽게 구현할 수 있다는 장점이 있는데.. 왜? 허용하지 않을까요?
  • 왜냐하면 다중 상속을 허용하면 클래스 간의 관계가 복잡해지는 문제가 생기기 때문입니다.
  • 만약 자식 클래스에서 상속받는 서로 다른 부모 클래스들이 같은 이름의 멤버를 가지고 있다면?
  • 자식 클래스에서는 이 멤버를 구별할 수 있는 방법이 없다는 문제가 생깁니다. 

= 자식이 부모를 여럿 골라도 되는데(?), 부모 입장에서 자식 2명이 이름이 같으면 구분을 못한다.

 

 

final 클래스와 final 메서드

📌 클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 됩니다.

public class Car {
    public final void horn() {
        System.out.println("빵빵");
    }
}

...

public class SportsCar extends Car{
    public void horn() { // 오류가 발생합니다.
        super.horn();
    }
}

 

  • final은 마치 채권처럼 모든 것을 처음 상태로 동결한다. 채권이 구매 시의 세금,가격,이자로 모든 변수를 상수로 지정해버리는 것과 같다 = 절대로 무엇도 바꿀 수 없다. 그저 그 모양그대로 가져와서 써야한다

 

Object

📌 Object는 말 그대로 “객체”를 의미하는 단어이며 보통, Object 클래스를 의미합니다.

  • Object 클래스는 Java 내 모든 클래스들의 최상위 부모 클래스입니다.
  • 따라서, 모든 클래스는 Object의 메서드를 사용할 수 있습니다.
  • 또한 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 Object 클래스를 상속받게 됩니다.
  • Object 클래스는 프로그래머가 만드는 클래스가 아니라 자체적으로 제공되는 최상위 클래스이다.

object 클래스의 메서드

 

 


 

 

오버라이딩

 🐳 부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 오버라이딩이라고 합니다.

  • 부모 클래스의 메서드를 그대로 사용 가능하지만 자식 클래스의 상황에 맞게 변경을 해야 하는 경우 오버라이딩을 사용합니다.
  • 오버라이딩을 하기 위해서는 아래 조건들을 만족해야 합니다.
  1. 선언부가 부모 클래스의 메서드와 일치해야 합니다.
  2. 접근 제어자를 부모 클래스의 메서드 보다 좁은 범위로 변경할 수 없습니다.
  3. 예외는 부모 클래스의 메서드 보다 많이 선언할 수 없습니다
public class Car {

    String company; // 자동차 회사
    private String model; // 자동차 모델
    private String color; // 자동차 색상
    private double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public char changeGear(char type) {
        gear = type;
        return gear;
    }

    public boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    public void horn() {
        System.out.println("빵빵");
    }

}

public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }

    public SportsCar(String engine) {
        this.engine = engine;
    }

    @Override
    public double brakePedal() {
        speed = 100;
        System.out.println("스포츠카에 브레이크란 없다");
        return speed;
    }

    @Override
    public void horn() {
        booster();
    }
}

public class Main {
    public static void main(String[] args) {
        // 부모 클래스 자동차 객체 생성
        Car car = new Car();
        car.horn(); // 경적

        System.out.println();
        // 자식 클래스 스포츠카 객체 생성
        SportsCar sportsCar = new SportsCar("Orion");

        // 오버라이딩한 brakePedal(), horn() 메서드 호출
        sportsCar.brakePedal();
        sportsCar.horn();

    }
}
  • 오버라이딩에서 자식 클래스에서 부모 클래스의 메서드를 바꾼다고 해서 부모 클래스의 메서드가 바뀌는 것은 아닙니다.
  • 자식 클래스에서 그 메서드를 새로운 방식으로 재정의하는 것입니다.  =  "엄마"를 자식이 "착한 엄마"라고 자신이 원하는(필요한) 대로 재해석한다는 말. 부모는 "엄마"라는건 바뀌지 않지만 자식에 한해서는 "착한 엄마"가 됨
  • 그래서 아래와 같은 답이 나온다.
더보기

빵빵

 

스포츠카에 브레이크란 없다

엔진 Orion 부앙~

 

  • 이름 비슷하지만 엄연히 오버로드와 오버라이딩은 다른 개념이다.
오버로드 같은 메서드 이름을 다른 매개변수를 사용하여 여러 번 정의하는 것입니다. 반환 타입은 동일하거나 달라질 수 있습니다.
오버라이딩 상속 관계에서 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것입니다. 메서드 이름, 매개변수, 반환 타입이 부모 클래스와 정확히 일치해야 합니다.

 

 

 

super 와 super()

 🐳 super는 부모 클래스의 멤버를 참조할 수 있는 키워드입니다.

  • 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용될 수 있습니다.
  • 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같을 경우 이를 구분하기 위해 사용됩니다.
public void setCarInfo(String model, String color, double price) {
    super.model = model; // model은 부모 필드에 set
    super.color = color; // color는 부모 필드에 set
    this.price = price; // price는 자식 필드에 set
}
  • 말 그대로 자식 클래스에서 부모 클래스의 맴버에 접근하기 위해 필요한 기능
  • super = 부모 , this = 만들어질 객체
  • model, color는 부모 클래스의 매개변수가 바뀐다. price는 생성될 객체의 price값이 바뀐다.

 

🐳 super()는 부모 클래스의 생성자를 호출할 수 있는 키워드입니다.

  • 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스의 생성자를 호출하기 위해 사용될 수 있습니다.
  • 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성됩니다.
  • 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행이 되어야 합니다.
    • 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출됩니다.
    • 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 합니다.
// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
     // this.engine = engine; // 오류 발생
    super(model, color, price);
    this.engine = engine;
}
  • 자식 클래스 객체를 생성할 때 생성자 매개변수에 매개값을 받아와 super(…)를 사용해 부모 생성자의 매개변수에 매개값을 전달하여 호출하면서 부모 클래스의 멤버를 먼저 초기화합니다.
  • 오버로딩된 부모 클래스의 생성자가 없다고 하더라도 부모 클래스의 기본 생성자를 호출해야 합니다.
    • 따라서 눈에 보이지는 않지만 컴파일러가 super();를 자식 클래스 생성자 첫 줄에 자동으로 추가해 줍니다.
  • 간단히 말해, 자식 클래스에서 객체를 생성하면
  • 위 처럼 자식이 부모 클래스의 필드, 메서드를 포함함으로 자식의 객체에도 부모의 필드, 메서드가 포함되어야한다.
  • 당연히 객체가 새로 생성되는 만큼 부모 필드도 초기화를 해줘야하는데
  • 위의 코드에서 super(model, color, price);로 초기화를 따로 해줬지만, 안써도 자동으로 컴파일러가 초기화 해준다는 말
  • 쉽게 말하면, 자식 클래스라는 붕어빵 틀(자식 클래스) 안에 부모 클래스 호두과자 틀(부모 클래스)이 또 있다면 당연히 붕어빵을 만들기 전에 붕어빵 틀만이 아니라 호두과자 틀도 청소해야한다.(초기화)

 


 

 

참조 변수의 타입 변환

자동 타입 변환

🧩 부모 타입 변수 = 자식 타입 객체; 는 자동으로 부모 타입으로 변환이 일어납니다.

  • 자식 객체는 부모 객체의 멤버를 상속받기 때문에 부모와 동일하게 취급될 수 있습니다.
    • 예를 들어 포유류 클래스를 상속받은 고래 클래스가 있다면 포유류 고래 = 고래 객체; 가 성립될 수 있습니다.
    • 왜냐하면 고래 객체는 포유류의 특징인 모유 수유 행위를 가지고 있기 때문입니다.
    • 다만 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있습니다. 
class Mammal {
    // 포유류는 새끼를 낳고 모유수유를 한다.
    public void feeding() {
        System.out.println("모유수유를 합니다.");
    }
}

class Whale extends Mammal {
    // 고래는 포유류 이면서 바다에 살며 수영이 가능하다.
    public void swimming() {
        System.out.println("수영하다.");
    }

    @Override
    public void feeding() {
        System.out.println("고래는 모유수유를 합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 고래는 포유류이기 때문에 포유류 타입으로 변환될 수 있습니다.
        Mammal mammal = new Whale();

        // 하지만 포유류 전부가 바다에 살고 수영을 할 수 있는 것은 아니기 때문에
        // 수영 하다 메서드는 실행 불가
        // 즉, 부모 클래스에 swimming이 선언되어있지 않아서 사용 불가능합니다.
        // mammal.swimming(); // 오류 발생

        // 반대로 모든 포유류가 전부 고래 처럼 수영이 가능한 것이 아니기 때문에 타입변환이 불가능합니다.
        // 즉, 부모타입의 객체는 자식타입의 변수로 변환될 수 없습니다.
        // Whale whale = new Mammal(); // 오류 발생

        mammal.feeding();
    }
}

 

  • Mammal mammal = new Whale();은 자식 클래스인 Whale의 객체를 만들었지만, 이를 부모 클래스 타입 변수인 mammal에 할당했다. 
  • 원래 안될 것 같은 이 행위가 되는 이유는, 자식이 부모의 일부분의 데이터를 공유하기 때문이다.
  • 즉 위 처럼 부모 클래스 Mammal이 가지고 있는 메서드나 필드가 아닌 요청은 오류가 나고 나머지는 가능한것

 

강제 타입 변환

🧩 자식 타입 변수 = (자식 타입) 부모 타입 객체;

  • 부모 타입 객체는 자식 타입 변수로 자동으로 타입 변환되지 않습니다.
  • 이럴 때는 (자식 타입) 즉, 타입 변환 연산자를 사용하여 강제로 자식 타입으로 변환할 수 있습니다.
class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Woof Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();  // 부모 타입 변수에 자식 타입 객체 할당
        animal.makeSound();  // Animal 클래스의 메서드 호출

        // 강제 타입 변환
        Dog dog = (Dog) animal;  // Animal 타입을 Dog 타입으로 강제 변환
        dog.bark();  // Dog 클래스의 메서드 호출
    }
}
더보기

설명:

  • Animal animal = new Dog();에서 자식 객체인 Dog를 부모 타입인 Animal 변수에 할당합니다.
  • animal.makeSound();는 부모 클래스인 Animal의 메서드를 호출합니다.
  • 그 후, animal 변수는 Dog 객체를 참조하고 있기 때문에, 강제 타입 변환을 통해 Animal 타입 변수 animal을 Dog 타입 변수 dog로 변환합니다.
  • 강제 타입 변환 (Dog) animal을 통해 이제 dog 변수는 Dog 클래스에서 정의한 bark() 메서드를 호출할 수 있게 됩니다.
  • "Woof Woof!"가 결과로 나옵니다.
  • 다만 무조건 강제 타입 변환을 할 수 있는 것은 아닙니다.
    • 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능합니다.
    • 부모 타입 변수로는 자식 타입 객체의 고유한 멤버를 사용할 수 없기 때문에 사용이 필요한 경우가 생겼을 때 강제 타입 변환을 사용합니다.
Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생


public class Main {
    public static void main(String[] args) {
        // 자동 타입변환된 부모타입의 변수의 자식 객체
        Mammal mammal = new Whale();
        mammal.feeding();

        // 자식객체 고래의 수영 기능을 사용하고 싶다면
        // 다시 자식타입으로 강제 타입변환을 하면된다.
        Whale whale = (Whale) mammal;
        whale.swimming();

        Mammal newMammal = new Mammal();
        // Whale newWhale = (Whale) newMammal;
    }
}

 

  • Mammal mammal = new Whale();이걸로 자동 타입 변환 한 상태면, Whale whale = (Whale) mammal;로 다시 자식타입으로 강제로 되돌릴 수 있다.
  • 되돌리는게 된다는 것이기 때문에, 부모를 자식의 변수에 할당하는 것은 불가능하다.
  • 용암을 5칸(자식)짜리 틀을 들고왔는데, 3칸(부모)에 부워버릴 수 없다. 다 넘친ㄷ;;;
  • 그러니까 저 말은 3칸을 5칸으로 늘렸던 걸 다시 3칸으로 줄인다는 말이다.

 

다형성

🧩 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미합니다.

  • 예를 들어 자동차의 핸들을 교체하면 핸들링이 부드러워지고 바퀴를 교체하면 승차감이 좋아집니다.
  • 소프트웨어 또한 구성하고 있는 객체를 바꿨을 때 소프트웨어의 실행 성능 및 결과물이 다르게 나올 수 있습니다.
  • 일전에 배운 참조 변수 타입 변환을 활용해서 다형성을 구현할 수 있습니다. 
public Car(Tire tire) {
    this.tire = tire;
}

...

Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
  • 위에서 본것 처럼 객체도 된다. 지금 위 예시는 자식 객체를 선언하고 그 자식 객체를 부모 타입 객체로 다시 재 선언한 코드이다.

 

Tire getHankookTire() {
    return new HankookTire("HANKOOK");
}

Tire getKiaTire() {
    return new KiaTire("KIA");
}

...

Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
  • 심지어는 매개변수도 된다... 여기서도 마찬가지로 반환 타입이 부모 타이어이기 때문에 자식 타이어 객체들을 반환값으로 지정할 수 있다. 당연히 이 자동 타입 변환 후라면 다시 강제 타입 변환도 된다......

 

  • 아마 위의 설명만으로는 이걸 왜 하는지 이해가 안될거다. 간단히 설명하면
// Tire 클래스 (부모 클래스)
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// KiaTire와 HankookTire는 Tire를 상속받는 자식 클래스들
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }
}

class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }
}

// Car 클래스
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;  // Tire 타입 객체를 받는다
    }

    public void showTireInfo() {
        tire.getTireInfo();  // Tire 객체의 메서드 호출
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 Tire 타입을 받아 Car 객체 생성 가능
        Car car1 = new Car(new KiaTire("KIA"));
        Car car2 = new Car(new HankookTire("HANKOOK"));

        car1.showTireInfo();  // Tire Brand: KIA
        car2.showTireInfo();  // Tire Brand: HANKOOK
    }
}

왜 이렇게 해야 할까?

  • 자동차의 타이어는 여러 종류일 수 있습니다. KiaTire, HankookTire 등 다양한 브랜드의 타이어가 있을 수 있는데, Car 클래스는 이 타이어를 구체적인 종류와 관계없이 받아들일 수 있어야 합니다.
  • 타이어 브랜드가 늘어난다고 해서 Car 클래스를 매번 수정하는 것보다는, 부모 클래스인 Tire 타입으로 받아들여 자식 클래스에서 Tire 객체를 사용하게 하면, 확장성이 좋아지고 변경에 강한 코드가 됩니다.

  • 말이 어려운데, 자 여기서 Car를 만들고자 한다. 자동차는 타이어라는 자식 클래스를 포함한다.
  • 그런데 타이어에 A,B 종류가 있다면?
  • Car가 tire의 종류를 파악하려면 , car -> 요청 -> tire -> 요청 -> A의 과정을 거친다
  • 만약 tire가 A의 정보를 바로 접속해서 이미 들고 있다면? car -> 요청 -> tire(자식 클래스 A에서 정보 이미 받음)
  • 더 빠르다.
변형 X car -> 요청 -> tire -> 요청 -> A
변형 O  car -> 요청 -> tire(자식 클래스 A에서 정보 다이렉트로 받음)

 

(변형 없이 접근)

// 부모 클래스 : Tire
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// 자식 클래스 : KiaTire
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }

    public void getKiaTireInfo() {
        System.out.println("Kia Tire Information: " + brand);
    }
}

// 자식 클래스 : HankookTire
class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }

    public void getHankookTireInfo() {
        System.out.println("Hankook Tire Information: " + brand);
    }
}

// 자동차 클래스 : Car
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    // 변형 없이 타이어 종류에 맞는 메서드 직접 호출
    public void showTireInfo() {
        if (tire instanceof KiaTire) {
            KiaTire kiaTire = (KiaTire) tire;
            kiaTire.getKiaTireInfo();
        } else if (tire instanceof HankookTire) {
            HankookTire hankookTire = (HankookTire) tire;
            hankookTire.getHankookTireInfo();
        } else {
            tire.getTireInfo();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 타이어 종류를 Car에 할당
        Car car1 = new Car(new KiaTire("KIA"));
        car1.showTireInfo();  // Kia Tire Information: KIA

        Car car2 = new Car(new HankookTire("HANKOOK"));
        car2.showTireInfo();  // Hankook Tire Information: HANKOOK
    }
}

 

(다형성 접근)

// 부모 클래스 : Tire
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// 자식 클래스 : KiaTire
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }

    @Override
    public void getTireInfo() {
        System.out.println("Kia Tire Information: " + brand);
    }
}

// 자식 클래스 : HankookTire
class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }

    @Override
    public void getTireInfo() {
        System.out.println("Hankook Tire Information: " + brand);
    }
}

// 자동차 클래스 : Car
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    // 다형성 활용, 부모 타입으로 자식 클래스 메서드를 호출
    public void showTireInfo() {
        tire.getTireInfo();  // Tire 타입으로 메서드 호출
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 타이어 종류를 Car에 할당
        Car car1 = new Car(new KiaTire("KIA"));
        car1.showTireInfo();  // Kia Tire Information: KIA

        Car car2 = new Car(new HankookTire("HANKOOK"));
        car2.showTireInfo();  // Hankook Tire Information: HANKOOK
    }
}

 

  • 이게 더 효율적이다.... 정말 자바는 말 그대로 효율의 극한, 중복을 정말 정말 정말 극혐한다.....
  • 위 방식으로 굳이 새로운 정보가 필요할 때마다 클래스를 수정할 필요 없이 Main에서 다형성 코딩만 진행하면되고 관리 유지, 오류 방지에도 탁월하다.... (만든 사람이 진짜 변태인가 보다...)

 

instanceof

🧩 다형성 기능으로 인해 해당 클래스 객체의 원래 클래스명을 체크하는 것이 필요한데 이때 사용할 수 있는 명령어가 instance of입니다.

  • 이 명령어를 통해서 해당 객체가 내가 의도하는 클래스의 객체인지 확인할 수 있습니다.
  • {대상 객체} instance of {클래스 이름} 와 같은 형태로 사용하면 응답값은 boolean입니다. 
  • 다형성 설명 보면 알겠지만, 상당이 코드가 복잡해진다. 
  • 이로 인해 프로그래머가 코드를 짜면서도 이 객체(붕어빵)가 어디 클래스(설계도)를 바탕으로 만들어 진건지 헷갈리는 경우가 있는데
  • 이때 해당 객체가 어느 클래스의 객체인지 확인하기 위해 있는 기능이다.
// 다형성

class Parent { }
class Child extends Parent { }
class Brother extends Parent { }


public class Main {
    public static void main(String[] args) {

				Parent pc = new Child();  // 다형성 허용 (자식 -> 부모)

        Parent p = new Parent();

        System.out.println(p instanceof Object); // true 출력
        System.out.println(p instanceof Parent); // true 출력
        System.out.println(p instanceof Child);  // false 출력

        Parent c = new Child();

        System.out.println(c instanceof Object); // true 출력
        System.out.println(c instanceof Parent); // true 출력
        System.out.println(c instanceof Child);  // true 출력

    }
}

 

 

 


 

 

추상 클래스

🎵 클래스가 설계도라면 추상 클래스는 미완성된 설계도입니다.

  • abstract 키워드를 사용하여 추상 클래스를 선언할 수 있습니다.
public abstract class 추상클래스명 {
		abstract 리턴타입 메서드이름(매개변수, ...);
}
  • 추상 클래스는 추상 메서드를 포함할 수 있습니다.
    • 추상 메서드가 없어도 추상 클래스로 선언할 수 있습니다.
  • 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성될 수 있습니다.
  • 추상 클래스는 여러 개의 자식 클래스들에서 공통적인 필드나 메서드를 추출해서 만들 수 있습니다.
  • 추상 메서드는 일반적인 메서드와는 다르게 블록{ }이 없습니다. 즉, 정의만 할 뿐, 실행 내용은 가지고 있지 않습니다.

 

추상 클래스는 왜 쓰는가?

  1. 공통된 기능을 구현: 추상 클래스는 여러 자식 클래스가 공통적으로 가지는 기능을 제공하는데 유용합니다. 예를 들어, 여러 종류의 자동차가 있지만, 모든 자동차는 움직이는 기능이 필요하고, 이 기능을 추상 클래스에 구현할 수 있습니다.
  2. 구체적인 구현과 공통의 계약을 결합: 추상 클래스는 구체적인 메서드 구현을 제공하고, 일부는 추상 메서드로 남겨두어 자식 클래스가 구현하도록 할 수 있습니다. 이를 통해, 자식 클래스는 기본 구현을 상속받고, 필요한 부분만 구체화합니다.
  3. 상태(필드) 공유: 추상 클래스는 **상태(필드)**를 가질 수 있기 때문에, 자식 클래스에서 공통적으로 사용하는 값을 선언하고, 이를 자식 클래스에서 상속받아 사용할 수 있습니다.
  4. 일관성 유지: 추상 클래스를 사용함으로써, 모든 자식 클래스가 공통적으로 특정 메서드를 구현하도록 강제할 수 있습니다. 이를 통해 일관된 동작을 보장할 수 있습니다.

 

추상 클래스 상속

public class 클래스명 extends 추상클래스명 {
		@Override
    public 리턴타입 메서드이름(매개변수, ...) {
		       // 실행문
    }
}

 

  • 상속받은 클래스에서 추상 클래스의 추상 메서드는 반드시 오버라이딩 되어야 합니다.

< 예제 >

public abstract class Car {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public abstract void horn();
}

public class BenzCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Benz 빵빵");
    }

}

public class AudiCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Audi 빵빵");
    }

}

public class ZenesisCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h
    
    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Zenesis 빵빵");
    }

}

public class Main {
    public static void main(String[] args) {
        Car car1 = new BenzCar();
        car1.horn();
        System.out.println();

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println();

        Car car3 = new ZenesisCar();
        car3.horn();
    }
}
  • BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
  • 따라서 horn() 메서드를 추상 메서드로 선언하여 자식 클래스에서 재정의 될 수 있도록 합니다.
  • BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
  • 즉 위의 자식 클래스에서 horn()메서드만 내용이 제각각이라는거
  • 각자 메서드 선언을 하면 3번 해야하니
  • 추상 클래스를 선언해서 메서드 선언을 1번으로 줄이고
  • 자식들이 내용만 채우라는것....
  • 아래 참고...
public class BenzCar extends Car {

    @Override
    public void horn() {
        System.out.println("Benz 빵빵");
    }
}

public class AudiCar extends Car {

    @Override
    public void horn() {
        System.out.println("Audi 빵빵");
    }
}

public class AudiCar extends Car {

    @Override
    public void horn() {
        System.out.println("Audi 빵빵");
    }
}

public abstract class Car {
    String company; // 자동차 회사
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public abstract void horn(); // 추상클래스~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new BenzCar();
        car1.horn();
        System.out.println();

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println();

        Car car3 = new ZenesisCar();
        car3.horn();
    }
}

 

 


<  일단 정리 >

 

  • 다형성은 데이터 접근이 많을 때 그를 단순화 하기 위해 사용 ( 부모가 자식에게 손자의 정보를 달라고 할때)
  • 추상 클래스는 여러 자식들 중 메서드가 다 다른 애가 있으면 그걸 추상 클래스의 메서드로 만들어서 자식들이 내용만 채우게 (추상 : 그리기 / 자식들 : 원, 삼각형 동그라미 등)
  • 다형성은 다양한 데이터나 객체를 하나의 부모 타입으로 처리해야 할 때 유용하게 사용됩니다. 예를 들어, 여러 자식 클래스의 객체들을 부모 클래스 타입으로 다루면서, 각 자식 클래스의 고유한 기능을 호출하는 상황에서 사용됩니다.
  • 추상 클래스는 여러 자식 클래스가 공통된 기능을 가지고 있어야 할 때 유용합니다. 부모 클래스에서 추상 메서드를 정의해 두고, 각 자식 클래스가 그 메서드를 어떻게 구현할지 결정하게 만들 수 있습니다. 즉, 자식들이 부모 클래스의 특정 기능을 "구현"하게 강제하는 경우입니다.

< 요약 >

  • 추상 클래스는 공통적인 기능을 정의하지만, 세부 구현은 자식 클래스에서 하도록 강제합니다.
  • 다형성은 부모 타입으로 여러 자식 객체를 다룰 수 있게 해주며, 각 자식 클래스의 고유 메서드를 호출할 수 있도록 합니다.
  • 오버라이딩은 부모 클래스에서 정의한 메서드를 자식 클래스에서 구체적으로 구현하여 각 자식 클래스에 맞는 동작을 하도록 만듭니다.

 

 

 

오늘의 문제

다음은 자동차와 관련된 클래스 구조입니다. Car 클래스를 부모 클래스로 하고, 이를 상속받은 Sedan과 SUV 클래스를 만들어보세요. 각 차종은 drive라는 메서드를 오버라이딩하여 다르게 동작해야 합니다. 이때 Car는 추상 클래스이며, drive 메서드는 추상 메서드로 정의되어야 합니다. 또한, 다형성을 활용하여 Car 타입의 배열을 만들어 다양한 차들을 처리하는 방법을 구현하세요.

요구사항

  1. Car 클래스는 추상 클래스이며, drive()라는 추상 메서드를 가지고 있습니다.
  2. Sedan 클래스와 SUV 클래스는 Car 클래스를 상속받으며, 각자의 drive() 메서드를 구현해야 합니다.
  3. Main 클래스에서 Car 타입의 배열을 이용해 여러 종류의 자동차 객체를 생성하고, 다형성을 이용해 각 차종의 drive() 메서드를 호출하는 코드 작성.
// 1. 부모 클래스 Car
abstract class Car {
    String model;
    
    public Car(String model) {
        this.model = model;
    }
    
    // 2. 추상 메서드 drive()
    public abstract void drive();
}

// 3. 자식 클래스 Sedan
class Sedan extends Car {
    
    public Sedan(String model) {
        super(model);
    }
    
    // 4. 오버라이딩된 drive() 메서드
    @Override
    public void drive() {
        System.out.println(model + " 세단은 부드럽게 달립니다.");
    }
}

// 5. 자식 클래스 SUV
class SUV extends Car {
    
    public SUV(String model) {
        super(model);
    }
    
    // 6. 오버라이딩된 drive() 메서드
    @Override
    public void drive() {
        System.out.println(model + " SUV는 험한 길도 잘 달립니다.");
    }
}

// 7. Main 클래스
public class Main {
    public static void main(String[] args) {
        // 8. Car 타입 배열 선언
        Car[] cars = new Car[2];
        
        // 9. 다양한 차종 생성
        cars[0] = new Sedan("아반떼");
        cars[1] = new SUV("투싼");
        
        // 10. 다형성 활용하여 각 차의 drive 메서드 호출
        for (Car car : cars) {
            car.drive(); // 각 차종의 구체적인 drive 메서드가 호출됨
        }
    }
}

 

 

더보기

해설

  1. 추상 클래스 Car: Car 클래스는 abstract로 선언되어 있으며, drive()라는 추상 메서드를 정의하고 있습니다. 추상 메서드는 자식 클래스에서 반드시 구현해야 하므로, Car 클래스는 직접 drive()를 구현하지 않고, 자식 클래스들이 이 메서드를 구현하도록 강제합니다.
  2. 자식 클래스 Sedan과 SUV: Sedan과 SUV는 Car 클래스를 상속받고, 각자의 drive() 메서드를 구현합니다. 이때 drive() 메서드는 Car 클래스에서 정의된 추상 메서드를 오버라이딩하여 각자의 방식으로 자동차가 달리는 방식을 다르게 구현합니다.
  3. 다형성: Main 클래스에서 Car 타입의 배열을 만들고, 그 배열에 Sedan과 SUV 객체를 각각 저장합니다. 이후 Car 타입으로 선언된 변수 car를 통해 Sedan과 SUV 객체를 다룰 수 있습니다. 이때 각 객체는 다형성에 의해 자신의 drive() 메서드를 호출합니다. 즉, Sedan과 SUV는 부모 클래스 Car의 타입을 가지지만, 각자 다른 구현을 가진 drive() 메서드를 호출하게 됩니다.
  4. 오버라이딩: Sedan과 SUV 클래스에서 Car 클래스의 drive() 메서드를 오버라이딩하여, 각자 다른 방식으로 자동차의 운전 기능을 구현합니다. 이 메서드는 부모 클래스의 메서드를 자식 클래스에서 수정한 것으로, 해당 자식 객체에 맞는 동작을 하게 됩니다.

 

<결과>

아반떼 세단은 부드럽게 달립니다.
투싼 SUV는 험한 길도 잘 달립니다.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 오류 및 예외에 대한 이해  (1) 2024.11.12
[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12
[JAVA] 접근 제어자  (1) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12

package

📌 패키지(Package)는 Java에서 클래스를 그룹으로 묶어 관리하는 폴더 같은 개념이다. 관련된 클래스들을 함께 묶어서 하나의 패키지로 정의함으로써 코드의 구조를 더 체계적이고 조직적으로 유지할 수 있다. 

 

패키지의 장점

  1. 코드의 논리적 그룹화: 비슷한 기능이나 역할을 하는 클래스들을 하나의 패키지에 넣어 관리할 수 있습니다.
  2. 네임스페이스 제공: 서로 다른 패키지에 같은 이름의 클래스가 있어도 충돌이 발생하지 않습니다.
  3. 접근 제어 강화: 특정 클래스나 멤버가 패키지 내에서만 접근 가능하게 하여 클래스 간의 의존성을 조절할 수 있습니다.

패키지 선언

패키지는 package 키워드를 통해 클래스 파일의 최상단에 선언하며, 선언된 패키지 경로는 실제 파일 경로와 일치해야 합니다.

  • 패키지는 상위 패키지와 하위 패키지를 도트(.)로 구분합니다.
  • package 상위 패키지.하위 패키지; 이렇게 선언할 수 있습니다.
  • 예를 들어 oop.pk1이라는 패키지와 oop.pk2라는 패키지가 있다고 가정해 보겠습니다.
    • 두 패키지에 모두 Car 클래스가 존재한다면? 그리고 이를 사용하려고 한다면?
    • Java는 패키지의 경로를 통해 이를 구분합니다.
    • 아래 예제를 통해 확인해 보겠습니다. 

 

oop.pk1.Car 클래스

package oop.pk1;

public class Car {
    public void horn() {
        System.out.println("pk1 빵빵");
    }
}

 

oop.pk2.Car 클래스

package oop.pk2;

public class Car {
    public void horn() {
        System.out.println("pk2 빵빵");
    }
}

 

main 메서드

package oop.main;

public class Main {
    public static void main(String[] args) {
        oop.pk1.Car car = new oop.pk1.Car();
        car.horn(); // pk1 빵빵

        oop.pk2.Car car2 = new oop.pk2.Car();
        car2.horn(); // pk2 빵빵
    }
}

 

 

 

import

📌 Java에서 다른 패키지에 있는 클래스를 가져와서 사용할 수 있게 해주는 키워드입니다. 이를 통해 특정 패키지에 속한 클래스들을 코드에 포함하고, 해당 클래스의 메서드와 필드에 접근할 수 있습니다.

  • 위에서 살펴본 oop.pk1이라는 패키지와 oop.pk2라는 패키지를 import로 명시해 보겠습니다.
    • import oop.pk1.Car;, import oop.pk2.Car;
  • 클래스 이름을 생략하고 *를 사용하여 import oop.pk1.*; 이렇게 표현하면 oop.pk1 패키지 아래에 있는 모든 클래스를 사용할 수 있습니다.
  • 다만 서로 다른 패키지에 있는 같은 이름의 클래스를 동시에 사용하려면 해당 클래스에 패키지 명을 전부 명시해야 합니다.
  • 아래 예제를 통해 확인해 보겠습니다. 


oop.pk1.Car 클래스

package oop.pk1;

public class Car {
    public void horn() {
        System.out.println("pk1 빵빵");
    }
}

 

oop.pk2.Car 클래스

package oop.pk2;

public class Car {
    public void horn() {
        System.out.println("pk2 빵빵");
    }
}

 

main 메서드

package oop.main;

public class Main {
    public static void main(String[] args) {
        oop.pk1.Car car = new oop.pk1.Car();
        car.horn(); // pk1 빵빵

        oop.pk2.Car car2 = new oop.pk2.Car();
        car2.horn(); // pk2 빵빵
    }
}

 

 

위처럼

import 패키지명.클래스명;

이런 방식으로 하나씩 코드에 추가하는 방법도 있지만

import 패키지명.*;

이런 방식으로 패키지(폴더)내 모든 클래스를 가져올 수도 있다.

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] 접근 제어자  (1) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12

접근 제어자

  • 클래스, 메서드, 변수 등의 접근 범위를 제한하는 키워드로, 외부에서 사용할 수 있는지 여부를 결정한다
  • 클래스, 맴버 변수, 메서드 생성자에 사용되고, 지정되어 있지 않으면 default로 지정된다.

1. public

  • 범위: 모든 클래스에서 접근 가능.
  • 사용처: 클래스, 메서드, 필드.
  • 설명: public으로 선언된 요소는 모든 클래스에서 접근할 수 있습니다. 프로젝트 내 어디서든지 사용 가능한 공개 멤버로 설정됩니다.

2. protected

  • 범위: 같은 패키지 내의 클래스나 자식 클래스에서 접근 가능.
  • 사용처: 메서드, 필드 (클래스에는 사용할 수 없음).
  • 설명: protected는 동일 패키지 내의 모든 클래스와 다른 패키지의 자식 클래스에서 접근할 수 있도록 허용합니다.

3. default (아무 접근 제어자도 명시하지 않은 경우)

  • 범위: 같은 패키지 내에서만 접근 가능.
  • 사용처: 클래스, 메서드, 필드.
  • 설명: 접근 제어자를 명시하지 않으면 패키지 전용(default) 접근 수준이 됩니다. 같은 패키지 내에서만 접근할 수 있으며, 패키지를 벗어나면 접근할 수 없습니다.

4. private

  • 범위: 같은 클래스 내에서만 접근 가능.
  • 사용처: 메서드, 필드 (클래스에는 사용할 수 없음).
  • 설명: private로 선언된 멤버는 해당 클래스 내에서만 접근할 수 있습니다. 외부에서는 접근할 수 없으므로 정보 은닉에 유용합니다.

 

< 접근 제어자 정리표 >

접근 제어자 같은 클래스 같은 패키지 (클래스 폴더) 자식 클래스 외부 클래스
public O O O O
protected O O O X
default O O X X
private O X X X

 

< 사용 가능한 접근 제어자  >

클래스 public, default
메서드 & 멤버 변수 public, protected, default, private
지역변수 없음

 

접근 제어자를 이용한 캡슐화 (은닉성)

  • 접근제어자는 클래스 내부에 선언된 데이터를 보호하기 위해서 사용합니다.
  • 유효한 값을 유지하도록, 함부로 변경하지 못하도록 접근을 제한하는 것이 필요합니다.

생성자의 접근 제어자

  • 생성자에 접근 제어자를 사용함으로 인스턴스의 생성을 제한할 수 있습니다.
  • 일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치합니다.

 

 

 

+ 같은 패키지 내

src
├── packageA
│   └── Car.java
└── packageB
    └── Main.java

 

  • Car 클래스에 default 접근 제어자가 적용된 필드나 메서드는 packageA 내에서만 접근할 수 있습니다.
  • packageB에 있는 Main 클래스에서는 Car의 default 필드나 메서드에 접근할 수 없습니다.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12
[JAVA] 인스턴스 멤버와 클래스 멤버  (0) 2024.11.12

This

 

this는 객체 즉, 인스턴스 자신을 표현하는 키워드다.

 

이게 무슨 말인가 싶을건데, 아래의 예시를 보면 이해가 쉽다

매개변수 값이 변경된다

 

만약 이처럼 생성자를 선언하는데 매개변수명(생성자의 지역변수)객체의 필드명(클래스의 맴버변수)이 동일할 경우 오류가 발생하지는 않지만 생성자 블록 내부에서 해당 변수들은 객체의 필드가 아닌 가장 가까운 매개변수명을 가리키게 됨으로 자기 자신에게 값을 대입하는 상황이 되어 버린다.

this 키워드를 통해 변수명에 해당하는 객체의 필드에 접근하여 받아온 매개변수의 매개값을 객체의 필드에 대입하여 저장할 수 있다.

 

Car returnInstance() {
    return this;
}

또한 this는 인스턴스 자신을 뜻하기 때문에 객체의 메서드에서 리턴 타입이 인스턴스 자신의 클래스 타입이라면 this를 사용하여 인스턴스 자신의 주소를 반환할 수도 있습니다. (여기선 car1)

 

 

This()

this(…)는 객체 즉, 인스턴스 자신의 생성자를 호출하는 키워드다.

 

 

이건 또 뭔말인가 싶을건데, 아래의 설명을 확인해 보자

public Car(String model) {
    this.model = model;
    this.color = "Blue";
    this.price = 50000000;
}

public Car(String model, String color) {
    this.model = model;
    this.color = color;
    this.price = 50000000;
}

public Car(String model, String color, double price) {
    this.model = model;
    this.color = color;
    this.price = price;
}

//-----------------------------------------------------------------------

public Car(String model) {
    this(model, "Blue", 50000000);
}

public Car(String model, String color) {
    ★this★(model, color, 100000000);
}

public Car(String model, String color, double price) {
    this.model = model;
    this.color = color;
    this.price = price;
}

public class Main {
    public static void main(String[] args) {
        Car ★car1★ = new Car("Sedan", "Red");  // 객체 car1 생성
    }
}

 

2개는 같은 코드인데, ★대로 this는 car1(인스턴스)을 말한다.

 

< 장점 >

  • 객체 내부 생성자 및 메서드에서 해당 객체의 생성자를 호출하기 위해 사용될 수 있습니다.
  • 생성자를 통해 객체의 필드를 초기화할 때 중복되는 코드를 줄여줄 수 있습니다.

 

< ⚠️ 주의  >

  • this() 키워드를 사용해서 다른 생성자를 호출할 때는 반드시 해당 생성자의 첫 줄에 작성되어야 한다.
public Car(String model) {
    System.out.println("model = " + model);
    this(model, "Blue", 50000000);
}
  • 이처럼 this() 키워드로 다른 생성자 호출 이전에 코드가 존재하면 오류가 발생한다.

 

 

public class Car {

    static final String COMPANY = "GENESIS"; // 자동차 회사 : GENESIS
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태


    public Car(String model) {
        this(model, "Blue", 50000000);
    }

    public Car(String model, String color) {
        this(model, color, 100000000);
    }

    public Car(String model, String color, double price) {
        this.model = model;
        this.color = color;
        this.price = price;
    }

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    double brakePedal() {
        speed = 0;
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    void horn() {
        System.out.println("빵빵");
    }

    Car returnInstance() {
        return this;
    }
}






public class Main {
    public static void main(String[] args) {

        // 생성자 오버로딩을 통해 여러 상황에서 자동차 생산
        // 제네시스 자동차를 생산 : static final String COMPANY = "GENESIS"; 상수 고정
        // 모든 자동차는 생산시 기어의 최초 상태 'P' 로 고정 : char gear = 'P'; 직접 대입하여 초기화

        // 모델을 변경하면서 만들 수 있고 색상 : Blue, 가격 50000000 고정
        Car car1 = new Car("GV60");
        System.out.println("car1.model = " + car1.model);
        System.out.println("car1.color = " + car1.color);
        System.out.println("car1.price = " + car1.price);
        System.out.println("car1.gear = " + car1.gear + "\n");

        // 모델, 색상을 변경하면서 만들 수 있고 가격 100000000 고정
        Car car2 = new Car("GV70", "Red");
        System.out.println("car2.model = " + car2.model);
        System.out.println("car2.color = " + car2.color);
        System.out.println("car2.price = " + car2.price);
        System.out.println("car2.gear = " + car2.gear + "\n");

        // GV80 모델, 색상 Black, 가격 120000000 으로 완전하게 고정된 경우
        Car car3 = new Car("GV80", "Black", 120000000);
        System.out.println("car3.model = " + car3.model);
        System.out.println("car3.color = " + car3.color);
        System.out.println("car3.price = " + car3.price);
        System.out.println("car3.gear = " + car3.gear + "\n");

        // this 키워드를 통해 car3 인스턴스 자신을 반환 : car3.returnInstance() = 인스턴스의 주소
        System.out.println(car3.returnInstance().model); // car3의 model
        System.out.println(car3.returnInstance().color); // car3의 color
        System.out.println(car3.returnInstance().price); // car3의 price

    }
}

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] package와 import  (0) 2024.11.12
[JAVA] 접근 제어자  (1) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12
[JAVA] 인스턴스 멤버와 클래스 멤버  (0) 2024.11.12
[JAVA] 객체의 필드와 메서드  (0) 2024.11.12

생성자

public Car() {} // 선언

...

Car car = new Car(); // 호출
  • 생성자는 객체가 생성될 때 호출되며 객체를 초기화하는 역할을 수행합니다.
  • 생성자는 반환 타입이 없고 이름은 클래스의 이름과 동일합니다.
  • new 연산자에 의해 객체가 생성되면서 Car(); 즉, 생성자가 호출됩니다.
  • 위와 같이 ()만 존재하는 경우를 기본 생성자라한다. 
  • 만약 생성자를 하나도 생성하지 않으면 컴파일러가 자동으로 기본 생성자를 자동으로 추가한다.
public class Car {
		public Car(String model) {} // 생성자 선언
		// 생성자가 한개 이상 선언되었기 때문에 기본 생성자를 추가하지 않음.
}
  • 즉 이 경우는 컴파일러는 기본 생성자를 생성하지 않음 (public Car가 선언되어 있음으로)
public class Car {
		public Car() {} // 컴파일러가 추가시켜줌
}

class Car { // 클래스가 public임으로 여기도 접근제어자는 없지만 public으로 기본 배치된다.
		Car() {} // 컴파일러가 추가시켜줌
}

컴파일러에 의해 생성되는 기본 생성자는 해당 클래스의 접근 제어자(public, …)를 따른다.

 

 

필드 초기화

  • 위의 기본 생성자처럼 생성자를 자동으로 추가하는 것이 아닌 프로그래머가 직접 지정하는 방식
  • 인스턴스마다 동일한 데이터를 가지는 필드는 초기값을 대입하는 것이 좋다.
public Car(String modelName, String colorName, double priceValue) {
    model = modelName;
    color = colorName;
    price = priceValue;
}

...

Car car = new Car(); // 오류 발생
더보기

한 개 이상의 생성자가 존재하기 때문에 컴파일러가 자동으로 기본 생성자를 추가해 주지 않기 때문에 기본 생성자가 존재하지 않아 오류가 발생합니다.

기본 생성자 필드 초기화
인스턴스마다 다른 값을 가져야 한다면 생성자를 통해서 필드를 초기화할 수 있습니다.

예를 들어 만들어지는 자동차마다 모델, 색상, 가격이 다르다면 생성자를 사용하여 필드의 값을 초기화하는 것이 좋습니다.

= 계속 변경할거면 굳이 직접 정해줄 이유가 없으니
반대로 인스턴스마다 동일한 데이터를 가지는 필드는 초기값을 대입하는 것이 좋습니다.

예를 들어 자동차가 만들어질 때마다 기어의 상태를 ‘P’로 고정해야 한다면 초기값을 직접 대입하는 것이 좋습니다.

= 변동이 없다면 미리 한번만 지정하면 되니

 

 

생성자 오버로딩

 

생성자를 통해 필드를 초기화할 때 오버로딩을 적용할 수 있다.

  • 예를 들어 우리가 자동차를 생성할 때 모델, 색상, 가격이 다른 자동차를 여러 대 생성할 수도 있고 색상만 다른 자동차를 여러 대 생성할 수도 있기 때문에 오버로딩을 사용하면 이를 효율적으로 처리할 수 있다. 
public Car(String modelName, String colorName, double priceValue)
public Car(String colorName, String modelName, double priceValue)

이거 될까 안될까?

 

더보기
  • modelName과 colorName 매개변수의 위치가 다르기 때문에 가능할 것처럼 보이지만
  • String, String, double : 매개변수의 개수, 타입, 순서가 동일하기 때문에 중복이 불가능합니다.
public class Car {

    static final String COMPANY = "GENESIS"; // 자동차 회사 : GENESIS
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태


    public Car(String modelName) {
        model = modelName;
    }

    public Car(String modelName, String colorName) {
        model = modelName;
        color = colorName;
    }

    public Car(String modelName, String colorName, double priceValue) {
        model = modelName;
        color = colorName;
        price = priceValue;
    }

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    double brakePedal() {
        speed = 0;
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    void horn() {
        System.out.println("빵빵");
    }
}

public class Main {
    public static void main(String[] args) {

        // 기본 생성자 호출 오류 확인
        // Car car1 = new Car(); // 오류 발생

        // 생성자 오버로딩을 통해 여러 상황에서 자동차 생산
        // 제네시스 자동차를 생산 : static final String COMPANY = "GENESIS"; 상수 고정
        // 모든 자동차는 생산시 기어의 최초 상태 'P' 로 고정 : char gear = 'P'; 직접 대입하여 초기화

        // GV60 모델만 기본으로 선택
        Car car2 = new Car("GV60");
        System.out.println("car2.model = " + car2.model);
        System.out.println("car2.gear = " + car2.gear + "\n");

        // GV70 모델, 색상 Blue 만 기본으로 선택
        Car car3 = new Car("GV70", "Blue");
        System.out.println("car3.model = " + car3.model);
        System.out.println("car3.color = " + car3.color);
        System.out.println("car3.gear = " + car3.gear + "\n");

        // GV80 모델, 색상 Black, 가격 50000000 으로 완전하게 고정된 경우
        Car car4 = new Car("GV80", "Black", 50000000);
        System.out.println("car4.model = " + car4.model);
        System.out.println("car4.color = " + car4.color);
        System.out.println("car4.price = " + car4.price);
        System.out.println("car4.gear = " + car4.gear + "\n");

    }
}

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 접근 제어자  (1) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 인스턴스 멤버와 클래스 멤버  (0) 2024.11.12
[JAVA] 객체의 필드와 메서드  (0) 2024.11.12
[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
public class Car {
    // 인스턴스 멤버 (각 객체마다 고유)
    String company; // 자동차 회사
    String model;   // 자동차 모델
    String color;   // 자동차 색상
    double price;   // 자동차 가격
    double speed;   // 자동차 속도, km/h 단위
    char gear;      // 기어 상태, P (주차), R (후진), N (중립), D (주행)
    boolean lights; // 자동차 조명 상태, 켜짐(true) 또는 꺼짐(false)

    // ★클래스 멤버 (모든 객체가 공유하는 값)★
    static int carCount = 0; // 생성된 자동차 객체 수

    // 기본 생성자: 매개변수 없이 Car 객체를 생성
    public Car(String company, String model, String color, double price) {
        this.company = company;
        this.model = model;
        this.color = color;
        this.price = price;
        this.speed = 0;  // 기본 속도 0
        this.gear = 'P'; // 기본 기어 P (주차)
        this.lights = false; // 기본적으로 조명은 꺼짐
        carCount++;  // 객체 생성 시 carCount 증가
    }

    // gasPedal 메서드: 속도를 설정하는 메서드로 kmh 값을 speed 필드에 저장하고 반환
    public double gasPedal(double kmh) {
        speed = kmh; // 자동차의 현재 속도를 kmh 값으로 설정
        return speed; // 설정된 속도 반환
    }

    // brakePedal 메서드: 속도를 0으로 설정하는 메서드로, speed 필드를 0으로 설정하고 반환
    public double brakePedal() {
        speed = 0; // 속도를 0으로 설정하여 정지 상태로 만듦
        return speed; // 속도 0을 반환
    }

    // changeGear 메서드: 기어 상태를 변경하는 메서드로, type 값을 gear 필드에 저장하고 반환
    public char changeGear(char type) {
        gear = type; // 기어를 매개변수 type의 값으로 설정
        return gear; // 설정된 기어 상태 반환
    }

    // onOffLights 메서드: 조명 상태를 반전시키는 메서드로, 현재 lights 값을 반전시키고 반환
    public boolean onOffLights() {
        lights = !lights; // 현재 조명 상태를 반전(true -> false, false -> true)
        return lights; // 변경된 조명 상태 반환
    }

    // horn 메서드: 경적을 울리는 메서드로, 호출 시 "빵빵" 소리를 콘솔에 출력
    public void horn() {
        System.out.println("빵빵"); // 경적 소리 출력
    }

    // ★클래스 메서드 (static): carCount를 출력하는 메서드로, 모든 객체에서 공유★
    public static void showCarCount() {
        System.out.println("총 자동차 객체 수: " + carCount);
    }
}

public class Main {
    public static void main(String[] args) {
        // Car 객체 생성
        Car car1 = new Car("Hyundai", "Sonata", "Red", 25000);
        Car car2 = new Car("Tesla", "Model S", "Black", 80000);
        Car car3 = new Car("Toyota", "Corolla", "Blue", 22000);

        // 각 자동차 객체에서 메서드 호출
        System.out.println("Car1 Speed: " + car1.gasPedal(100));  // Car1의 속도 설정
        System.out.println("Car2 Speed after braking: " + car2.brakePedal()); // Car2의 속도 정지
        System.out.println("Car3 Gear: " + car3.changeGear('D'));  // Car3의 기어 변경

        // 클래스 메서드 호출
        Car.showCarCount();  // 총 자동차 객체 수 출력 (클래스 변수 carCount 사용)

        // 각 자동차의 경적 울리기
        car1.horn();  // "빵빵" 출력
        car2.horn();  // "빵빵" 출력
        car3.horn();  // "빵빵" 출력
    }
}

 

기본적으로 지금까지 사용한 필드와 메서드 전부 인스턴스 맴버였다. 보통 클래스 맴버는 static이라는 메서드를 포함하는데, 이는 특정 인스턴트가 아닌 클래스 자체에 고정적으로 위치시킨다는 의미이다.(위의 ★로 된 코드가 클래스 맴버이다.)

 

간단히 말하면, Static은 전역변수와 유사한 역활을 한다고 생각하면 편하다. 모든 클래스 내의 객체가 공통적으로 가져야하는 값들에 Static을 사용한다. 위의 예시 처럼 '생성된 자동차 수'는 클래스 내에서 한개만 존재하고 이를 모두가 공유하면 됨으로 클래스 맴버에 속한다.

 

 

인스턴스 맴버

  • 인스턴스 멤버는 객체를 생성해야 사용할 수 있다고 했습니다.
  • 또한 객체의 인스턴스 필드는 각각의 인스턴스마다 고유하게 값을 가질 수 있습니다.
  • 그렇다면 객체가 인스턴스화할 때마다 객체의 메서드들은 인스턴스에 포함되어 매번 생성이 될까요?
    • 그렇지 않습니다. 매번 저장한다면 중복 저장으로 인해 메모리 효율이 매우 떨어지기 때문에 메서드는 메서드 영역에 두고서 모든 인스턴스들이 공유해서 사용합니다.
    • 대신 무조건 객체를 생성 즉, 인스턴스를 통해서만 메서드가 사용될 수 있도록 제한을 걸어둔 것입니다.

 

클래스 멤버

클래스는 Java의 클래스 로더에 의해 메서드 영역에 저장되고 사용됩니다.

  • 이때 클래스 멤버란 메서드 영역의 클래스와 같은 위치에 고정적으로 위치하고 있는 멤버를 의미합니다.
  • 따라서 클래스 멤버는 객체의 생성 필요 없이 바로 사용이 가능합니다. 

여기서 중요한게

1. 인스턴스 맴버는 클래스 맴버를 사용 가능하다

2. 클래스 맴버는 인스턴스 맴버를 사용 불가능하다. 

 

이유는 단순히, 인스턴스 맴버는 각 개체마다 값이 달라질 수 있다보니, static으로 전 클래스에 고정되는 전역변수 형태의 클래스 맴버에는 사용 불가능, 간단히, 클래스 맴버는 어떠한 객체든 공통으로 값이 같아야하니 객체마다 값이 다를 수 있는 인스턴스 맴버는 사용이 불가능하다.


<예시>

static String company = "GENESIS"; // 자동차 회사 : GENESIS

String getCompany() {
    return "(주)" + company;
}

 

  • Car 클래스를 통해 제품을 만들 때 만들어지는 자동차들의 회사가 “GENESIS”로 고정되어 있다고 가정해 보겠습니다.
    • 그렇다면 모든 Car 클래스의 객체마다 company 인스턴스 필드를 가지고 있을 필요 없이 클래스 필드로 만들어 공유하게 만든다면 메모리를 효율적으로 사용할 수 있습니다.
    • 또한 인스턴스 메서드인 getCompany()는 클래스 필드인 company를 사용할 수 있습니다.
static String setCompany(String companyName) {
    // System.out.println("자동차 모델 확인: " + model); // 인스턴스 사용 불가
    company = companyName;
    return company;
}
  • 자동차 회사를 변경할 수 있는 setCompany(String companyName)를 클래스 메서드로 만들어서 사용할 수 있습니다.
    • 이때 인스턴스 필드인 model을 사용하려고 하면 오류가 발생합니다.

클래스 맴버 사용


 

Car car = new Car(); // 객체 생성

car.company = "Ferrari";
String companyName2 = car.setCompany("Lamborghini");
  • 참조형 변수를 사용하여 클래스 멤버에 접근은 가능하지만 추천하지 않습니다.
    • 클래스 이름으로 접근하는 것이 좋습니다.

+ car.company에서 .(닷)은 참조 연산자입니다. 이 연산자는 객체의 필드메서드에 접근할 때 사용됩니다. 다시 말해, .은 객체를 통해 해당 객체의 속성이나 메서드에 접근하는 역할을 합니다. = . 은 '참조할게요' 라는 의미( 그렇다고 해서 .이 붙으면 다 참조형 변수라는건 아니다..  )

 

public class Car {

    static String company = "GENESIS"; // 자동차 회사 : GENESIS
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태


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

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    double brakePedal() {
        speed = 0;
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    void horn() {
        System.out.println("빵빵");
    }

    String getCompany() {
        return "(주)" + company;
    }

    static String setCompany(String companyName) {
        // System.out.println("자동차 모델 확인: " + model); // 인스턴스 필드 사용 불가
        company = companyName;
        return company;
    }
}


public class Main {
    public static void main(String[] args) {
        // 클래스 필드 company 확인
        System.out.println(Car.company + "\n");
        // 클래스 필드 변경 및 확인
        Car.company = "Audi";
        System.out.println(Car.company + "\n");

        // 클래스 메서드 호출
        String companyName = Car.setCompany("Benz");
        System.out.println("companyName = " + companyName);

        System.out.println();
        // 참조형 변수 사용
        Car car = new Car(); // 객체 생성

        car.company = "Ferrari";
        System.out.println(car.company + "\n");

        String companyName2 = car.setCompany("Lamborghini");
        System.out.println("companyName2 = " + companyName2);
    }
}

 

 + 이거 헷갈릴 수 있는데, Car.company는 클래스 이름을 사용하여 클래스의 company 이름을 수정한거고, car.company가 Car 객체를 만들고 car가 참조형 변수로서 Car객체의 company 이름을 변경한거다.

 

+ 그러니까 클래스의 내용을 수정한건 Audi고 ferrari는 클래스라는 설계도를 기반으로한 객체 Car의 내용을 수정한거다


지역 변수


  • 메서드 내부에 선언한 변수를 의미합니다.
  • 메서드가 실행될 때마다 독립적인 값을 저장하고 관리하게 됩니다.
  • 지역 변수는 메서드 내부에서 정의될 때 생성되어 메서드가 종료될 때까지만 유지됩니다.
public class Main {
    public static void main(String[] args) {
        Main main = new Main();

        // 메서드 호출 : main.getClass()
        System.out.println("main.getClass() = " + main.getNumber());
        System.out.println("main.getClass() = " + main.getNumber());
        System.out.println("main.getClass() = " + main.getNumber());
    }

    public int getNumber() {
        int number = 1; // 지역 변수
        number += 1;
        return number; // 메서드 종료되면 지역변수 제거됨
    }
}

// 출력
//main.getNumber() = 2
//main.getNumber() = 2
//main.getNumber() = 2

final 필드와 상수


final은 ‘최종적’ 이라는 의미입니다.

  • final 필드는 초기값이 저장되면 해당값을 프로그램이 실행하는 도중에는 절대로 수정할 수 없습니다.
  • 또한 final 필드는 반드시 초기값을 지정해야 합니다. 
  • 말 그대로 변경 불가 필드
final String company = "GENESIS";

...

Car car = new Car();
System.out.println(car.company);
  • 필드 타입 앞에 final 키워드를 추가하여 final 필드를 선언할 수 있습니다.
  • 사용방법은 일반적인 인스턴스 필드와 동일합니다. 다만 수정이 불가능합니다.
    • car.company = "Benz"; 이렇게 수정하려 하면 오류가 발생합니다.

상수

  • 상수의 특징은 값이 반드시 한 개이며 불변의 값을 의미합니다.
  • 따라서 인스턴스마다 상수를 저장할 필요가 없습니다.
  • 그냥 final이 붙은 변수는 상수가 된다.
static final String COMPANY = "GENESIS";

...

System.out.println(Car.COMPANY);
  • final 앞에 static 키워드를 추가하여 모든 인스턴스가 공유할 수 있는 값이 한 개이며 불변인 상수를 선언할 수 있습니다.
  • 사용방법은 일반적인 클래스 필드와 동일합니다. 다만 수정이 불가능합니다.
    • Car.COMPANY = "Benz"; 이렇게 수정하려 하면 오류가 발생합니다.
  • 일반적으로 상수는 대문자로 작성하는 것이 관례입니다.

+ static도 변수로 만들면 바꿀 수 있고 상수로 만들면 못 변경하기도 한다.

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12
[JAVA] 객체의 필드와 메서드  (0) 2024.11.12
[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
[JAVA] 객체지향 프로그래밍 & 다형성  (1) 2024.11.12

필드 = 객체의 속성

필드는 객체의 데이터를 저장하는 역할을 한다.

  • 객체의 필드는 크게 고유한 데이터, 상태 데이터, 객체 데이터로 분류할 수 있다.
  • 이처럼 자동차 객체는 4개의 고유한 데이터와 3개의 상태 데이터 그리고 3개의 객체 데이터를 가질 수 있다.
    • 우리가 처음 소프트웨어의 부품을 객체라 표현한다.
    • 이 3개의 객체 데이터를 자동차를 만들기 위한 부품 데이터라고 이해해도 좋다.
public class Car {

    String company; // 자동차 회사
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    Tire tire;
    Door door;
    Handle handle;

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

    double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    double brakePedal() {
        speed = 0;
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}
}

 

우리가 정의하여 선언한 클래스의 필드들은 기본적으로 초기값을 제공하지 않을 경우 객체가 생성될 때 자동으로 기본값으로 초기화된다.

  • 초기값을 제공하는 방법은 ‘필드 타입 필드명 = 값;’ 이렇게 직접 초기화할 수 있다.
    • String model = "Gv80"; 

 

필드 사용방법

필드를 사용한다’라는 의미는 필드의 값을 변경하거나 읽는 것을 의미합니다.

  • 우리가 클래스에 필드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
  • 클래스는 설계도일 뿐 실제로 필드의 데이터를 가지고 있는 것은 객체입니다.
  • 따라서 객체를 생성한 후에 필드를 사용할 수 있습니다. 
  • 간단히, 클래스는 설계도이다. 붕어빵 틀 설계도에 속(데이터)를 넣을 수 없다. 붕어빵 틀(객체)에 속(데이터)를 넣어야한다.
  • 외부 접근
    • Car car = new Car();
      • 이렇게 객체를 생성했다면 우리는 참조 변수 car를 이용하여 외부에서 객체 내부의 필드에 접근하여 사용할 수 있습니다.
      • 이때 객체의 내부 필드에 접근하는 방법은 도트(.) 연산자를 사용하면 됩니다.
        • car.color = "blue";
  • 내부 접근
    • 도트 연산자를 사용하여 외부에서 객체 내부에 접근할 수 있을 뿐만 아니라 객체 내부 메서드에서도 내부 필드에 접근할 수 있습니다.
double brakePedal() {
    speed = 0;
    return speed;
}
  • 이처럼 brakePedal() 메서드 내부에서 객체의 필드 speed를 바로 호출해서 사용할 수 있습니다.

 

public class Car {

    String company; // 자동차 회사
    String model = "Gv80"; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights = true; // 자동차 조명의 상태

    Tire tire = new Tire();
    Door door;
    Handle handle;

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

    double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    double brakePedal() {
        speed = 0;
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}
}

//- model 필드 값에 “Gv80” 초기값을 주겠습니다.
//- lights 필드 값에 true 초기값을 주겠습니다.
//- tire 필드 값에 `new Tire()` 초기값을 주겠습니다.

 

main 메서드를 사용하여 테스트한 경우

public class Main {
    public static void main(String[] args) {
        Car car = new Car(); // 객체 생성

        // 초기값과 기본값 확인하기

        System.out.println("car.model = " + car.model); // 초기값 "Gv80"이 출력됩니다.
        System.out.println("car.color = " + car.color); // 기본값 null이 출력됩니다.
        System.out.println();

        System.out.println("car.speed = " + car.speed); // 기본값 0.0이 출력됩니다.
        System.out.println("car.gear = " + car.gear);  // 기본값 \u0000(공백)이 출력됩니다.
        System.out.println("car.lights = " + car.lights); // 초기값 true가 출력됩니다.
        System.out.println();

        System.out.println("car.tire = " + car.tire); // 초기값 인스턴스의 주소가 출력됩니다.
        System.out.println("car.door = " + car.door); // 기본값 null이 출력됩니다.
        System.out.println();

        // 필드 사용

        car.color = "blue"; // 필드 color에 "blue" 데이터를 저장합니다.
        car.speed = 100;    // 필드 speed에 100 데이터를 저장합니다.
        car.lights = false; // 필드 lights에 false 데이터를 저장합니다.

        System.out.println("car.color = " + car.color); // 저장된 "blue" 데이터가 출력됩니다.
        System.out.println("car.speed = " + car.speed); // 저장된 100.0 데이터가 출력됩니다.
        System.out.println("car.lights = " + car.lights); // 저장된 false 데이터가 출력됩니다.

    }
}

 

 

 

 

메서드 = 객체의 행위

  • 특정 작업을 수행하는 코드 블록, 객체 지향 프로그래밍에서 메서드는 주로 클래스 내부에 정의되며, 해당 클래스의 객체에서 호출하여 사용할 수 있다. 메서드는 작업을 캡슐화하여 코드의 재사용성을 높이고, 유지보수와 확장성을 용이하게 한다.
<접근 제어자> <반환 타입> <메서드 이름>(<매개변수>) {
    // 메서드 본문 (실제 작업을 수행)
    return <값>;
}

 

반환(리턴) 타입

double brakePedal() {...} // double 타입 반환
char changeGear(char type) {...} // char 타입 반환
boolean onOffLights() {...} // boolean 타입 반환
void horn() {...} // 반환할 값 없음
  • 리턴 타입이란 메서드가 실행된 후 호출을 한 곳으로 값을 반환할 때 해당 값의 타입을 의미한다.
    • return 리턴 타입의 반환값;
    • 주의할 점은 메서드에 리턴 타입을 선언하여 반환할 값이 있다면 반드시 return 문으로 해당하는 리턴 타입의 반환값을 지정해야 한다.
  • 반환할 값이 없을 때는 리턴 타입에 void를 작성해야 한다.
    • 반환값이 없음으로 return문을 반드시 지정할 필요는 없습다.
    • 메서드는 실행할 때 return문을 만나면 그대로 종료하게 되는데 void 타입일 때 return; 이렇게 return문을 사용하여 원하는 지점에서 메서드를 종료할 수도 있다.

매개변수

double gasPedal(double kmh, char type) {
    speed = kmh;
    return speed;
}
  • 매개변수는 메서드를 호출할 때 메서드로 전달하려는 값을 받기 위해 사용되는 변수이다.
  • 위 gasPedal(double kmh, char type) 메서드의 매개변수는 double 타입의 kmh, char 타입의 type이다.
    • 해당 매개변수에 값을 전달하기 위해서는 순서와 타입에 맞춰 값을 넣어주면 된다.
    • gasPedal(100, 'D');
  • 전달하려는 값이 없다면 생략 가능하다.

 

가변길이 매개변수 

void carSpeeds(double ... speeds) {
    for (double v : speeds) {
        System.out.println("v = " + v);
    }
}
  • double … speeds 이렇게 … 을 사용하면 아래처럼 매개값을 , 로 구분하여 개수 상관없이 전달 가능하다.
  • carSpeeds(100, 80);
  • carSpeeds(110, 120, 150);

 

 

메서드 호출 방법

‘메서드를 호출한다’라는 의미는 메서드의 블록 내부에 작성된 코드를 실행한다는 의미입니다.

  • 필드와 마찬가지로 클래스의 메서드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
  • 클래스는 설계도일 뿐 메서드는 객체의 행위를 정의한 것입니다.
  • 따라서 객체를 생성한 후에 메서드를 사용할 수 있습니다. 
  • 외부 접근
    • Car car = new Car();
      • 이렇게 객체를 생성했다면 우리는 참조 변수 car를 이용하여 외부에서 객체 내부의 메서드에 접근하여 호출할 수 있습니다.
      • 이때 객체의 내부 메서드에 접근하는 방법은 도트(.) 연산자를 사용하면 됩니다.
        • car.brakePedal();
      • 또한 메서드가 매개변수를 가지고 있다면 반드시 호출할 때 매개변수의 순서와 타입에 맞게 매개값을 넣어줘야 합니다.
        • car.gasPedal(100, 'D');
  • 내부 접근
    • 도트 연산자를 사용하여 외부에서 객체 내부에 접근할 수 있을 뿐만 아니라 객체 내부 메서드에서도 내부 메서드에 접근하여 호출할 수 있습니다.
      • 아래처럼 gasPedal(double kmh, char type) 메서드 내부에서 해당 객체의 changeGear(type); 메서드를 호출할 수 있습니다.
double gasPedal(double kmh, char type) {
    changeGear(type);
    speed = kmh;
    return speed;
}
  • 반환 값 저장
    • 메서드의 리턴 타입을 선언하여 반환할 값이 있다면 변수를 사용하여 받아줄 수 있습니다.
      • 반드시 리턴 타입과 변수의 타입이 동일하거나 자동 타입 변환될 수 있어야 합니다.
    double speed = car.gasPedal(100, 'D');
    
    • double 타입의 변수 speed를 사용하여 double gasPedal(double kmh, char type) 메서드의 double 타입의 반환값을 받아 저장할 수 있습니다.
public class Car {

    String company; // 자동차 회사
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

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

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    double brakePedal() {
        speed = 0;
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}

    void carSpeeds(double ... speeds) {
        for (double v : speeds) {
            System.out.println("v = " + v);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car(); // 객체 생성

        // 메서드 호출 및 반환값 저장
        double speed = car.gasPedal(100, 'D');
        System.out.println("speed = " + speed);

        boolean lights = car.onOffLights();
        System.out.println("lights = " + lights);

        System.out.println();
        // gasPedal 메서드 내부에 호출된 changeGear(type); 메서드의 결과 확인
        // gear의 초기값은 'P'
        System.out.println("car.gear = " + car.gear); // 'D' 출력

        System.out.println();
        // 가변길이 매개변수 확인
        car.carSpeeds(100, 80);
        System.out.println();
        car.carSpeeds(110, 120, 150);
    }
}

 

 

오버로딩

  • 같은 이름의 메서드나 생성자 매개변수의 개수나 타입이 달라지도록 여러 번 정의하는 기법입니다. 오버로딩을 사용하면 같은 이름으로 다양한 작업을 처리할 수 있습니다.

오버로딩의 특징

  • 같은 이름의 메서드나 생성자지만 매개변수의 개수나 타입이 달라야 합니다.
  • 반환 타입은 오버로딩을 구별하는 기준이 되지 않습니다.
  • 컴파일 시점에 메서드 호출을 구분하기 때문에, 실행 시간에 결정되는 것이 아니라 호출 시점에서 적절한 메서드가 선택됩니다.

오버로딩의 조건

  • 메서드의 이름이 같고, 매개변수의 개수, 타입, 순서가 달라야 합니다.
  • '응답 값만' 다른 것은 오버로딩을 할 수 없습니다.
  • 접근 제어자만 다른 것도 오버로딩을 할 수 없습니다.
  • 결론, 오버로딩은 매개변수의 차이로만 구현할 수 있습니다.

오버로딩의 장점

  • 코드의 가독성 향상: 비슷한 작업을 수행하는 메서드를 같은 이름으로 처리할 수 있어 코드가 깔끔하고 이해하기 쉬워집니다.
  • 유지보수 용이성: 같은 이름의 메서드를 사용하여 여러 매개변수에 대한 처리 로직을 관리할 수 있어 유지보수가 용이합니다.

오버로딩 규칙

  1. 매개변수의 타입이 달라야 오버로딩으로 인식됩니다.
  2. 매개변수의 개수가 달라야 오버로딩으로 인식됩니다.
  3. 매개변수의 순서가 달라야 오버로딩으로 인식됩니다.
public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable
{
			...
			
		public void println() {
        newLine();
    }

    public void println(boolean x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(char x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(int x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(long x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(float x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(double x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(char[] x) {
        if (getClass() == PrintStream.class) {
            writeln(x);
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(String x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(Object x) {
        String s = String.valueOf(x);
        if (getClass() == PrintStream.class) {
            // need to apply String.valueOf again since first invocation
            // might return null
            writeln(String.valueOf(s));
        } else {
            synchronized (this) {
                print(s);
                newLine();
            }
        }
    }


		  ...
}

 

 

기본형 & 참조형 매개변수

 

기본형 매개변수

  • 매개변수의 타입이 기본형일 때는 값 자체가 복사되어 넘어가기 때문에 매개값으로 지정된 변수의 원본 값이 변경되지 않습니다.
  • 메서드를 호출할 때 전달할 매개값으로 지정한 값을 메서드의 매개변수에 복사해서 전달합니다.

참조형 매개변수

  • 매개변수를 참조형으로 선언하면 값이 저장된 곳의 원본 주소를 알 수 있기 때문에 값을 읽어 오는 것은 물론 값을 변경하는 것도 가능합니다.
  • 메서드의 매개변수뿐만 아니라 반환 타입도 참조형이 될 수 있습니다.
    • 반환 타입이 참조형이라는 것은 반환하는 값의 타입이 “실제 값의 주소”라는 의미입니다.
public class Car {

    String company; // 자동차 회사
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    Tire tire;
    Door door = new Door();
    Handle handle = new Handle();

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

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    double brakePedal(char type) {
        speed = 0;
        type = 'P'; // 정지 후 매개변수 type을 어떤 타입으로 전달 받았는지 상관없이 'P'로 고정시키기
        changeGear(type);
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}

    Tire setTire(Tire tireCompany) {
        tireCompany.company = "KIA"; // 금호 타이어를 전달 받았지만 강제로 KIA 타이어로 교체
        tire = tireCompany;
        return tire;
    }
}
public class Tire {
    String company; // 타이어 회사
    public Tire() {}
}
public class Main {
    public static void main(String[] args) {
        Car car = new Car(); // 객체 생성

        // 기본형 매개변수
        char type = 'D';
        car.brakePedal(type);

        // 메서드 실행 완료 후 전달할 매개값으로 지정된 type 값 확인
        System.out.println("type = " + type); // 기존에 선언한 값 'D' 출력, 원본 값 변경되지 않음
        // 메서드 실행 완료 후 반환된 car 인스턴스의 gear 타입 확인
        System.out.println("gear = " + car.gear); // 객체 내부에서 type을 변경하여 수정했기 때문에 'P' 출력

        System.out.println();
        // 참조형 매개변수
        Tire tire = new Tire();
        tire.company = "금호"; // 금호 타이어 객체 생성

        // 차 객체의 타이어를 등록하는 메서드 호출한 후 반환값으로 차 객체의 타이어 객체 반환
        Tire carInstanceTire = car.setTire(tire);

        // 메서드 실행 완료 후 전달할 매개값으로 지정된 참조형 변수 tire의 company 값 확인
        System.out.println("tire.company = " + tire.company); // "KIA" 출력
        // 전달할 매개값으로 지정된 tire 인스턴스의 주소값이 전달되었기 때문에 호출된 메서드에 의해 값이 변경됨.

        // 메서드 실행 완료 후 반환된 car 인스턴스의 tire 객체 값이 반환되어 저장된 참조형 변수 carInstanceTire의 company 값 확인
        System.out.println("carInstanceTire.company = " + carInstanceTire.company); // "KIA" 출력
    }
}

 

+ Recent posts