# 클래스
- 멤버 변수, 필드는 모두 클래스에 소속된 변수를 나타내는 용어이다.
- 클래스 타입 기반으로 생성된 객체 변수에는 객체의 참조값이 저장된다.
- 클래스 타입 배열은
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한 상태가 된다. 이러한 상태는 개발자가 코드 상으로 적절히 구현해주어야 한다.
- 참조 변수를 null처리
- 기존 참조하던 참조 변수에 새 객체의 참조를 할당
- 메서드 내에서 생성된 객체 (스택 해제 후 unreachable)
- 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만 사용 가능하다.
캡슐화 적용 원칙 정리
- 데이터를 숨겨라 (속성값)
- 꼭 필요한 기능만 노출시켜라 (메서드)
# 자바 메모리 구조와 static
자바의 메모리 구조는 3가지로 구분된다.
- 메서드 영역
- 클래스 정보: 클래스 실행 코드(바이트 코드), 필드, 메서드, 생성자 코드 등 모든 실행 코드가 보관되는 영역
- static 영역:
static변수들을 보관하는 영역- static 변수는 JVM에서 가장 긴 생명주기를 갖는다.
- 정적 메서드는 인스턴스 메서드 호출이 불가능하고, 인스턴스 변수에 접근도 불가능하다.
- 정적 메서드에 자주 접근하는 경우 클래스명을 반복적으로 입력하지 않기 위해
import static이 가능하다.
- 런타임 상수 풀: 프로그램 실행 시 필요한 공통 리터럴 상수들을 보관. 문자열 풀은 Java7부터 힙 영역으로 이동하였음
- 객체 생성 시 인스턴스 변수에는 메모리가 할당되지만 메서드에 대해서는 새로운 메모리 할당이 없다.
- 스택 영역 - 실제 프로그램이 실행되는 영역. 메서드 실행시 스택 프레임이 하나씩 쌓이고 호출 종료 시 스택 프레임이 제거됨
- 스레드별로 스택 영역이 생성된다.
- 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
- 힙 영역 - 객체가 생성되는 영역
- 객체와 배열이 생성되는 영역
- 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("전기차 이동");
}
}
# 다형성
다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 다형성 이해를 위해서는 다형적 참조, 메서드 오버라이딩에 대해 이해해야 한다.
- 다형적 참조: 부모 타입은 자식 타입을 담을 수 있다. 자식 타입은 부모 타입을 담을 수 없다. (메모리 구조 생각)
- 부모 타입의 변수가 자식 인스턴스를 참조하는 구조이다.
- 메모리 상에 부모와 자식 인스턴스는 모두 생성된다.
- 자식 인스턴스의 모든 하위 타입도 참조 가능하다.
- 메서드를 찾는 것은 상위 타입을 향한 방향성만 성립하기 때문에,
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: trueparent instanceof Child: falsechild instanceof Parent: truechild 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: 기존 코드는 수정되지 않아야 한다.