# 클래스

  • 멤버 변수, 필드는 모두 클래스에 소속된 변수를 나타내는 용어이다.
  • 클래스 타입 기반으로 생성된 객체 변수에는 객체의 참조값이 저장된다.
  • 클래스 타입 배열은 null값으로 초기화된다.
    • Student[] students = new Student[2];
  • 멤버 변수는 숫자의 경우 0, 불리언은 false, 참조형은 null을 디폴트값으로 초기화된다.
  • 참조 타입이 메서드 아규먼트로 사용되는 경우 참조가 복사되어 외부 스코프에 위치하고 있는 객체의 멤버에도 영향을 미친다.
  • 자바는 JVM이 GC(가비지 컬렉션) 기반으로 더 이상 참조하지 않는 객체를 메모리에서 제거한다.
void main() {
    Student me = new Student();
    me.name = "gyeongjun";
    me.age = 1;
    me.grade = 2;

    System.out.println(me.name + me.age + me.grade);
}

public class Student {
    String name;
    int age;
    int grade;
}

# 가비지 컬렉션

가비지 컬렉션은 힙 영역에 할당된 객체들을 모아 정리하는 프로세스이다. 가비지 컬렉션 수행 시 stop-the-world가 발생하는데, 이는 GC를 실행하는 스레드 외에 나머지 모든 스레드가 모든 작업을 멈추는 것을 의미한다.

어떠한 GC 알고리즘을 사용하더라도 stop-the-world는 발생하지만, GC튜닝을 통해 stop-the-world 시간을 줄일 수는 있다.

자바에서는 System.gc()를 통해 명시적으로 GC를 수행할 수는 있지만 성능에 매우 큰 오버헤드를 주므로 사용하지 않아야 한다.

GC는 Weak Generational Hypothesis 가설 위에서 만들어졌다.

  • 대부분의 객체는 금방 접근 불가능 unreachable 상태가 된다.
  • 오래된 객체에서 젊은 객체로의 참조가 있는 경우는 매우 드물다.

이러한 가설을 살리기 위해 VM에서는 Young Generation과 Old Generation 이라는 구조로 논리적 공간으로써 나누어 객체를 관리한다.

  • Young 영역: 새로 생성한 객체 대부분이 이곳에 위치한다. 대부분이 객체가 이 영역에 생성되었다가 사라지고, 이 영역에서 사라질때 Minor GC가 발생한다고 표현한다.
  • Old 영역: 접근 불가능 상태로 변경되지 않아 Young 영역에서 살아남은 객체가 Old 영역으로 복사된다. 이 영역에서 객체가 사라질때 Major GC(Full GC)가 발생한다고 표현한다.

특정 객체가 unreachable 상태가 되면 GC를 수행하기에 Eligible한 상태가 된다. 이러한 상태는 개발자가 코드 상으로 적절히 구현해주어야 한다.

  1. 참조 변수를 null처리
  2. 기존 참조하던 참조 변수에 새 객체의 참조를 할당
  3. 메서드 내에서 생성된 객체 (스택 해제 후 unreachable)
  4. Island of Isolation: 객체가 서로 참조를 하고 있지만, 이 객체들 모두 다른 reachable한 상태의 외부 객체로부터 참조되고 있지 않을때를 말함

# 생성자

캡슐화란?

속성과 기능을 하나로 묶어서 필요한 기능을 메서드를 통해 외부에 제공하는 것

  • 생성자 내에서 this 참조를 통해 객체 자기 자신의 멤버를 참조할 수 있다.
    • 생성자 파라미터명과 멤버 필드명이 다르면 this 생략이 가능하다
  • 생성자 함수는 클래스 이름과 동일하게 함수를 정의하면 된다.
public class Student {
    String name;
    int age;
    int grade;

    Student(String name, int age, int grade) {
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}
  • 생성자 사용시 필수값 입력을 컴파일 레벨에서 보장할 수 있다.
  • 매개변수가 없는 생성자를 기본 생성자라고 한다.
    • 클래스에 생성자가 하나도 없으면 자바 컴파일러가 매개변수가 없고 작동하는 코드가 없는 기본 생성자를 자동으로 만들어준다.
    • 생성자가 하나라도 있으면 자바는 기본 생성자를 만들지 않는다.
  • 자바는 생성자 오버로딩을 지원한다.
  • 자바는 생성자 내부에서 자신의 다른 생성자를 호출하는 것도 가능하다.
    • this()를 통해 가능하다.
public class Student {
    String name;
    int age;
    int grade;

    Student(int grade) {
        this("jun", 12, grade);
    }

    Student(String name, int age, int grade) {
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}

# 패키지

  • 패키지 import시 특정 클래스만 가져올 수도 있고, 모든 클래스를 가져올 수도 있다.
    • import packageName.ClassName, import packageName.*
  • 서로 다른 패키지에서 같은 이름의 클래스를 사용하는 경우 코드에서 패키지 경로를 직접 명시해주면 된다.
    • 경로를 직접 명시해주는 경우 import해줄 필요는 없다.
  • 패키지명 관례는 역도메인이다. (com.company.app)
package pack;
import pack.a.Student;

public class Main {
    public static void main(String[] args){
        Student student = new Student();
        pack.b.Student studentB = new pack.b.Student();

        Data data = new Data();
    }
}

# 접근 제어자

  • 필드 및 메서드 레벨의 접근 제어자는 아래와 같다.
    • private: 모든 외부 호출을 막는다.
    • default(package-private): 같은 패키지 안에서 호출은 허용한다.
    • protected: 같은 패키지 안에서 호출은 허용한다. 패키지가 달라도 상속관계의 호출은 허용한다.
    • public: 모든 외부 호출을 허용한다.
  • 클래스 레벨의 접근 제어자는 아래와 같다.
    • public, default만 사용 가능하다.

캡슐화 적용 원칙 정리

  1. 데이터를 숨겨라 (속성값)
  2. 꼭 필요한 기능만 노출시켜라 (메서드)

# 자바 메모리 구조와 static

자바의 메모리 구조는 3가지로 구분된다.

  1. 메서드 영역
    • 클래스 정보: 클래스 실행 코드(바이트 코드), 필드, 메서드, 생성자 코드 등 모든 실행 코드가 보관되는 영역
    • static 영역: static 변수들을 보관하는 영역
      • static 변수는 JVM에서 가장 긴 생명주기를 갖는다.
      • 정적 메서드는 인스턴스 메서드 호출이 불가능하고, 인스턴스 변수에 접근도 불가능하다.
      • 정적 메서드에 자주 접근하는 경우 클래스명을 반복적으로 입력하지 않기 위해 import static이 가능하다.
    • 런타임 상수 풀: 프로그램 실행 시 필요한 공통 리터럴 상수들을 보관. 문자열 풀은 Java7부터 힙 영역으로 이동하였음
    • 객체 생성 시 인스턴스 변수에는 메모리가 할당되지만 메서드에 대해서는 새로운 메모리 할당이 없다.
  2. 스택 영역 - 실제 프로그램이 실행되는 영역. 메서드 실행시 스택 프레임이 하나씩 쌓이고 호출 종료 시 스택 프레임이 제거됨
    • 스레드별로 스택 영역이 생성된다.
    • 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
  3. 힙 영역 - 객체가 생성되는 영역
    • 객체와 배열이 생성되는 영역
    • GC가 수행되는 주요 영역
// import static 사용방법
import static accesscontrol.a.AccessData.staticCall;

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

# final

  • final을 지역 변수에 설정할 경우 최초 한번만 할당 가능하며, 변수 값 변경이 불가능하다.
  • 파라미터에도 final 선언이 가능한데, 이 경우 메서드 내에서 매개변수 값 변경이 불가능하다.
  • 생성자를 통해 final을 초기화 하는 경우 인스턴스별로 final 필드를 다르게 초기화 할 수는 있다.
  • 상수는 static final을 사용하여 선언하면 된다.
  • 참조 타입에 대한 final은 참조 변경만 불가능하고, 구현에 따라 참조 내부의 속성은 변경 가능하다.
public class Main {
    final int a;
    // final int a = 1;과 같은 방식으로 초기화 할 바에는 static을 사용하자.

    Main() {
        a = 1; // 생성자 혹은 멤버 디폴트값 초기화시에만 최초 1회 초기화 가능하다.
    }

    public void finalTest(final int param) {
        a = 2; // ERROR!
        param = 2; // ERROR!
        System.out.println(param);
    }
}

# 상속

  • 상속 시 extends 키워드를 사용한다. 다중 상속은 불가능하다.

상속관계 메모리 구조

  • 상속관계에서 자식 클래스 객체 생성 시 부모와 자식 모두가 생성되고 메모리 공간이 분리된다.
  • 메서드 호출 시 호출하는 변수의 타입(클래스)를 기준으로 선택한다.
    • 아래 코드에서 ec.charge 메서드 호출 시 변수 타입이 ElectricCar이므로 해당 타입의 charge 메서드가 호출된다.
    • move함수를 호출하는 경우 변수 타입이 ElectricCar인데, 메서드가 해당 타입에 존재하지 않으므로 부모 클래스 내에 해당 메서드가 있는지 찾아본 뒤 호출한다.
    • 부모 클래스에도 메서드가 없으면 컴파일 에러가 발생한다.
public class Car {
    public static void main(String[] args) {
        ElectricCar ec = new ElectricCar();
        ec.move();
        ec.charge();
    }
    public void move() {
        System.out.println("차를 이동");
    }
}

class ElectricCar extends Car {
    public void charge() {
        System.out.println("충전");
    }
}
  • 메서드 재정의(오버라이딩)는 메서드에 @Override 어노테이션을 추가한 뒤 새로 코드를 작성해주면 된다.
  • 메서드 오버라이딩 조건은 아래와 같다.
    • 메서드 이름: 부모 클래스와 동일해야 한다.
    • 메서드 파라미터: 부모 클래스와 타입, 순서, 갯수가 같아야 한다.
    • 반환 타입: 같아야 한다.
    • 접근 제어자: 부모 클래스 메서드보다 더 제한적이면 안된다. 부모는 default인데 자식에서 private으로 변경하면 안된다.
    • 부모 클래스 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없지만, 더 적거나 같은 수의 예외 또는 하위 타입의 예외는 선언할 수 있다.
    • static, final, private키워드가 붙은 메서드는 오버라이딩 불가능하다.
    • 생성자 오버라이딩은 불가능하다.
class ElectricCar extends Car {
    public void charge() {
        System.out.println("충전");
    }

    @Override
    public void move() {
        System.out.println("전기차 이동");
    }
}
  • Car의 move 메서드가 protected 접근 제어자이며 GasCar, TrashCar 클래스가 같은 패키지이지만 Car와는 다른 패키지일때, Car와 상속관계에 있는 GasCar는 move메서드 호출이 가능하지만 TrashCar에서는 다른 패키지이기 때문에 메서드 호출이 불가능하다
public class GasCar extends Car {
    public static void main(String[] args) {
        GasCar gc = new GasCar();
        gc.move();
    }
}

class TrashCar {
    public static void main(String[] args) {
        GasCar gc = new GasCar();
        gc.move();
    }
}
  • 메서드 오버라이딩이 되어있거나 부모와 자식 필드명이 동일한 경우 자식에서 부모 필드 또는 메서드를 호출할 수 없지만 super 키워드를 사용하면 부모를 직접 참조할 수 있다.
    • 부모와 자식 클래스에 대한 객체가 각각 다 만들어지기 때문에 super만으로도 부모 객체 참조가 가능하다.
  • 상속관계 하에 자식 클래스 생성자에서는 부모 클래스의 생성자를 반드시 호출해야 한다.
    • super() 형태로 호출한다. 기본 생성자인 경우 생략 가능하다.
class ElectricCar extends Car {
    ElectricCar(int speed) {
        super(speed);
    }

    public void charge() {
        System.out.println("충전");
    }

    @Override
    public void move() {
        System.out.println("전기차 이동");
    }
}

# 다형성

다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 다형성 이해를 위해서는 다형적 참조, 메서드 오버라이딩에 대해 이해해야 한다.

  1. 다형적 참조: 부모 타입은 자식 타입을 담을 수 있다. 자식 타입은 부모 타입을 담을 수 없다. (메모리 구조 생각)
    • 부모 타입의 변수가 자식 인스턴스를 참조하는 구조이다.
    • 메모리 상에 부모와 자식 인스턴스는 모두 생성된다.
    • 자식 인스턴스의 모든 하위 타입도 참조 가능하다.
    • 메서드를 찾는 것은 상위 타입을 향한 방향성만 성립하기 때문에, Parent poly = new Child()를 했다고 하더라도 자식 타입의 메서드를 호출할 수는 없는 것이다.
    • 하지만 메모리 구조 상으로는 부모 자식 객체는 모두 생성된 상태이기 때문에, 형 변환을 하게 되면 하위 타입의 메서드도 참조가 가능하다.
public static void main(String[] args) {
    System.out.println("Parent -> Parent");

    Parent parent = new Parent();
    parent.parentMethod();

    Child child = new Child();
    child.parentMethod();
    child.childMethod();

    Parent poly = new Child();
    poly.parentMethod();
    // childMethod는 호출 불가능

    // 타입캐스팅을 통해 자식 메서드 호출 가능
    child = (Child) poly;
    child.childMethod();
    child.parentMethod();

    // 일시적 타입캐스팅 가능
    ((Child) poly).childMethod();

    // 타입 업캐스팅
    Parent parent2 = new Child();
    parent2.parentMethod();
}
  • 업캐스팅은 자주 사용되기 때문에 생략을 권장한다.
    • 다운캐스팅은 잘못 사용되는 경우 런타임 오류가 발생할 수 있다.
    • 아래와 같이 부모 타입으로 생성된 객체이기 때문에, 자식 객체에 대한 정보가 존재하지 않는 상태이다. 이때 자식 타입으로 타입캐스팅을 하는 경우 런타임 에러가 발생한다.
Parent parent3 = new Parent();
Child child3 = (Child) parent3;
child3.childMethod();
  • instanceof를 통해 어떤 인스턴스를 참조 중인지 알 수 있다.

    • parent instanceof Parent: true
    • parent instanceof Child: false
    • child instanceof Parent: true
    • child instanceof Child: true
  • Java 16부터는 Pattern Matching for instanceof를 지원한다.

    • instanceof를 사용하면서 동시에 변수 선언이 가능하다.
Parent parent = new Child();

if (parent instanceof Child child) {
    child.parentMethod();
}

# 다형성과 오버라이딩

  • 메서드 오버라이딩에서 기억할 점은 오버라이딩 된 메서드가 항상 우선권을 갖는다는 점이다.
    • 아래 코드에서 poly 변수는 Parent 타입인데, 객체 참조 시 Parent 객체의 hello 메서드를 호출하려고 할때 오버라이딩 된 메서드가 존재하는 경우 자식 타입의 오버라이딩 메서드를 한번 더 찾아들어가서 호출하게 된다.
public static void main(String[] args) {
    Child child = new Child();
    child.hello(); // child hello

    Parent parent = new Parent();
    parent.hello(); // parent hello

    Parent poly = new Child();
    poly.hello(); // child hello
}

# 다형성 - 활용, 추상 클래스, 인터페이스

public static void main(String[] args) {
    Dog dog = new Dog();
    Cat cat = new Cat();
    Cow cow = new Cow();

    soundAnimal(dog);
    soundAnimal(cat);
    soundAnimal(cow);
}

private static void soundAnimal(Animal animal) {
    animal.sound();
}
  • soundAnimal 파라미터를 부모 타입으로 지정한 뒤 메서드를 호출하면 각 자식 타입에서 오버라이딩한 메서드를 호출하게 된다.
    • 다형성 참조 덕분에 animal 변수가 자식 타입의 객체를 참조할 수 있다.
    • 메서드 오버라이딩 덕분에 각 자식 인스턴스 메서드들을 호출할 수 있다.
  • 동일한 원리로 for each문에도 적용 가능하다.
Animal[] animalArr = new Animal[]{ dog, cat, caw };
Animal[] animalArr = { dog, cat, caw };

for (Animal animal : animalArr) {
    animal.sound();
}

# 추상 클래스

  • 위 코드에서 Animal 타입에 대해 객체 생성이 가능하며, 자식 클래스에서 오버라이딩을 하지 않은 경우 의도대로 구현이 되지 않는다.
  • 추상 클래스는 부모 클래스로서 역할은 하지만, 실제로 생성은 되면 안되는 클래스이다. 이를 통해 위의 한계를 극복할 수 있다.
    • abstract 키워드를 붙여주면 된다.
    • 상속받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에서 추상 메서드를 통해 정의할 수 있다.
    • 추상 메서드는 실체가 없고 메서드 바디가 없다.
  • 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
  • 추상 클래스는 객체 생성이 불가능하다.
public abstract class Animal {
    public abstract void sound();
}
  • 순수 추상 클래스: 모든 메서드가 추상 메서드인 추상 클래스 (개념적)
  • 자바에서는 순수 추상 클래스를 편리하게 사용할 수 있도록 인터페이스를 제공한다.

# 인터페이스

  • 인터페이스 메서드는 모두 public, abstract이다.
  • 메서드에 public abstract를 생략할 수 있고, 생략하는 것을 권장한다.
  • 다중 구현을 지원한다. 여러 인터페이스를 동시에 구현할 수 있다.
    • 인터페이스 멤버 변수는 public static final 키워드가 모두 포함된 것으로 간주한다.
  • 구현 시 @Override 키워드를 추가한다.
public interface InterfaceAnimal {
    public static final double PI = 3.14;
}

// 같은 코드
public interface InterfaceAnimal {
    double PI = 3.14;
}
public class Cat implements Animal {
    @Override
    public void sound() {

    }
}
  • 메모리 구조는 기존 클래스 상속과 동일하다.
    • 인터페이스 공간과 구현한 하위 타입의 메모리 공간이 함께 존재하는 구조이다.
  • 인터페이스를 써야 할 이유는 누군가가 실행 가능한 메서드를 끼워넣을 수 있는 여지를 원천 제거하는 것에 있다. 다중 구현 또한 이유 중 하나이다.
  • 다중 구현이 허용되는 이유는 인터페이스는 메서드 바디가 없는 것을 보장하기 때문이다.
    • 다중 상속의 경우 클래스에서 실행 가능한 메서드를 여러 클래스들 중 어떤 클래스의 메서드를 구현할지 모르기 때문에 허용하지 않는다.

# OCP (Open-Closed-Principle) 원칙

  • Open for extension: 새로운 기능의 추가나 변경 사항이 생겼을때, 기존 코드는 확장할 수 있어야 한다.
  • Closed for modification: 기존 코드는 수정되지 않아야 한다.

# Reference

  1. Naver D2 - Garbage Collection (opens new window)