접근 제어자
자바는 4가지 종류의 접근 제어자를 제공한다.
- private : 모든 외부 호출을 막는다.
- default (package-private): 같은 패키지안에서 호출은 허용한다.
- protected : 같은 패키지안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
- public : 모든 외부 호출을 허용한다.
접근 제어자 사용 - 클래스 레벨
- 클래스 레벨의 접근 제어자는 public , default 만 사용할 수 있다.
- private , protected 는 사용할 수 없다.
- public 클래스는 반드시 파일명과 이름이 같아야 한다.
- 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있다.
- 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다
캡슐화
캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것이다.
캡슐화를 통해 데이터의 직접적인 변경을 방지할 수 있다.
즉 캡술화는 속성과 기능을 하나로 묶고, 외부에 필요한 기능만을 노출하고 나머지는 모두 내부로 숨기는 것이다.
자바 메모리 구조와 static
자바 메모리 구조
메서드 영역(Method Area): 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 이 영역은
프로그램의 모든 영역에서 공유한다.
- 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재한다.
- static 영역: static 변수들을 보관한다.
- 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다.
스택 영역(Stack Area): 자바 실행 시, 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결
과, 메서드 호출 정보 등을 포함한다.
- 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스
택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역이다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.
정적 메서드 사용법
- static 메서드는 static 만 사용할 수 있다.
- 클래스 내부의 기능을 사용할 때, 정적 메서드는 static 이 붙은 정적 메서드나 정적 변수만 사용할 수 있
다. - 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다. (매개변수로 전달하지 않을 때를 제외하고 참조값이 없기 때문에)
- 클래스 내부의 기능을 사용할 때, 정적 메서드는 static 이 붙은 정적 메서드나 정적 변수만 사용할 수 있
- 반대로 모든 곳에서 static 을 호출할 수 있다.
- 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static 을 호출할 수 있다
final
static final
static 영역은 단 하나만 존재하는 영역이다. static final을 사용하면 중복과 메모리 비효율 문제를 모두 해결할 수 있다.
이런 이유로 필드에 final + 필드 초기화를 사용하는 경우 static 을 붙여서 사용하는 것이 효과적이다
상속
상속이라고 해서 단순하게 부모의 필드와 메서드만 물려 받는게 아니다. 상속 관계를 사용하면 부모 클래스도 함께 포함
해서 생성된다. 외부에서 볼때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분된다
상속과 메모리 구조
- 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
- 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
- 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.
super - 생성자
상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다.
Child 를 만들면 부모인 Parent 까지 함께 만들어지는 것이다. 따라서 각각의 생성자도 모두 호출되어야 한다.
상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.(자동 생략)
다형성
다형성은 객체가 여러 타입의 객체로 취급될 수 있는 기능을 의미한다.
즉 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 것이다.
다형성의 핵심 이론 다형적 참조와 메서드 오버라이딩이 있다. !
1. 다형적 참조 : 부모 타입의 변수가 자식 인스턴스를 참조하는 것
부모 타입은 자신을 물론, 모든 자식 타입을 참조할 수 있다.
Parent p = new Child();
다형적 참조의 한계
위 처럼 자식을 참조한 상황에서 Child에 있는 메서드를 호출할 수 없다. 부모 방향으로는 올라갈 수있지만 자식 방향으로는 내려갈 수 없다.!
다형성과 캐스팅
이처럼 자식을 참조한 상황에서 자식 타입의 메서드를 호출하려면 다운 캐스팅이 필요하다.
*다운 캐스팅의 위험 : 객체를 생성할 때 해당 타입의 상위 부모 타입은 모두 함께 생성된다(하위 타입은 X). 따라서 상위로 타입을 변경하는 업캐스팅은 인스턴스가 모두 존재하기 때문에 매우 안전하다. 반면 다운 캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다.
2. 메서드 오버라이딩
오버라이딩 된 메서드는 항상 우선권을 가진다. 변수는 오버라이딩에 우선권을 가지지 않는다. 가장 하위 자식의 오버라이딩 된 메서드가 우선권을 가지게 된다.
//자식 변수가 자식 인스턴스 참조
Child child = new Child();
child.method();// 결과 : Child.method
//부모 변수가 부모 인스턴스 참조
Parent parent = new Parent();
parent.method(); // 결과 : Parent.method
//부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
poly.method(); //메서드 오버라이딩, 결과 : Child.method
다형성의 사용 이유
만약에 Cat , Dog, Cow 가 있고 해당 클래스의 메서드들을 각각 호출하고 싶을 때 해당 타입이 서로 다르기 때문에 코드가 번거롭게 작성을 해야한다. 하지만 다형성을 사용하게 되면 코드를 매우 간단하게 줄일 수 있다.
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cow.sound();
System.out.println("동물 소리 테스트 종료");
Animal이라는 큰 개념을 두고 dog, cow, cat을 상속받아 사용하면 각 개념은 크게 Animal로 묶을 수 있다.
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(caw);
}
//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
여기서 다형적 참조(부모 타입 변수는 자식 타입의 인스턴스를 생성할 수 있다.) , 메서드 오버라이딩(오버라이딩된 메서드가 우선권을 갖는다.)의 개념을 통해 Animal로 묶어서 많은 코드들의 중복을 제거하였다.
추상 클래스
만약 위에서 sound() 라는 메서드를 호출했지만 자식 클래스에서 만약 sound()라는 메서드를 정의하지 않았다면 어떻게 될까? 그럼 Animal.sound()가 호출되고 원하는 결과가 나오지 않게된다. 이를 방지하지 위해 상속에 제약을 두는 클래스가 추상 클래스이다.
- 추상 메서드는 선언할 때 abstract 키워드를 붙여주면 된다.
- 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
- 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩해서 사용해야 한다.
인터페이스
순수 추상 클래스란 상속하는 모든 메서드가 추상 메서드인 클래스를 의미한다. 즉 상속하는 클래스는 모든 메서드를 구현해야 한다는 의미이다. 자바에서는 이런 순수 추상 클래스를 편리하게 사용할 수 있도록 인터페이스 기능을 제공한다.
- 인스턴스를 생성할 수 없다.
- 상속 시 모든 메서드를 오버라이딩 해야 한다.
- 메서드에 public abstract 을 생략할 수 있다.
- 인터페이스에서 멤버 변수는 public , static, final이 모두 포함되어 있는 것으로 간주된다.
- 인터페이스는 다중 구현(상속)을 지원한다.
* 인터페이스에서 다중 상속을 지원하는 이유 : 인터페이스는 모두 추상 메서드로 이루어져 있기 때문에 인터페이스 메서드가 중복되더라도 어차피 오버라이딩된 메서드가 호출되게 때문에 충돌이 일어나지 않는다.
객체 지향 프로그래밍
객체 지향 프로그래밍은 컴퓨터 프로그램을 객체들의 모임으로 파악하는 것이다. 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
역할과 구현의 분리
역할과 구현으로 프로그램을 구분하면 프로그램이 단순해지고 유연해지며 변경이 편리해진다.
- 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
- 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
- 클라이언트는 구현 대상의 내부 구조가 변경돼도 영향을 받지 않는다.
- 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
다형성 적용 전
Driver 클래스는 요구사항이 추가 될 때마다 코드를 추가하고 변경해야 한다.
Driver 클래스는 여러 개의 차 클래스에 의존한다.
public class K3Car {
// 메서드 정의 ..
}
public class Model3Car{
// 메서드 정의 ..
}
public class Driver {
private K3Car k3Car;
private Model3Car model3Car; //추가
public void setK3Car(K3Car k3Car) {
this.k3Car = k3Car;
}
//추가
public void setModel3Car(Model3Car model3Car) {
this.model3Car = model3Car;
}
//변경
public void drive() {
System.out.println("자동차를 운전합니다.");
if (k3Car != null) {
k3Car.startEngine();
k3Car.pressAccelerator();
k3Car.offEngine();
} else if (model3Car != null) {
model3Car.startEngine();
model3Car.pressAccelerator();
model3Car.offEngine();
}
}
}
public class CarMain {
public static void main(String[] args) {
Driver driver = new Driver();
K3Car k3Car = new K3Car();
driver.setK3Car(k3Car);
driver.drive();
//추가
Model3Car model3Car = new Model3Car();
driver.setK3Car(null);
driver.setModel3Car(model3Car);
driver.drive();
}
}
다형성 적용 후
Driver 클래스는 요구사항이 추가돼도 어떠한 영향을 받지 않는다.
Driver 클래스는 단 하나의 클래스(인터페이스)만 의존한다.
public interface Car {
void startEngine();
void offEngine();
void pressAccelerator();
}
public class K3Car implements Car {
// 오버라이딩 ...
}
public class Model3Car implements Car {
// 오버라이딩 ...
}
public class Driver {
private Car car;
public void setCar(Car car) {
System.out.println("자동차를 설정합니다: " + car);
this.car = car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
car.startEngine();
car.pressAccelerator();
car.offEngine();
}
}
OCP 원칙
Open - Closed Principle
좋은 객체 지향 설계 원칙 중 하나인 OCP는 "확장에는 열려있고 변경에는 닫혀있다 "라는 의미를 갖고있다.
쉽게 이야기해서 기존의 코드 수정없이 새로운 기능을 추가할 수 있다는 말이다.
위의 코드처럼
확장에는 열려 있다 : Car(인터페이스)를 통해 새로운 차량을 추가할 수 있다.
변경에는 닫혀있다 : Driver 클래스를 보면 새로운 차량을 추가해도 전혀 변경되지 않는다.
라고 해석하면 된다.
즉 다형성을 활용하면 역할과 구현을 잘 분리할 수 있으며, 새로운 클래스가 추가되더라도 기존 코드를 유지한 채 변경과 확장이 용이해진다.
* 전략 패턴 (strategy Pattern): 전략 패턴은 알고리즘을 클라이언트 코드의 변경없이 쉽게 교체할 수 있다라는 의미로 앞의 코드가 전략 패턴을 사용한 코드이다.
불변 객체
데이터 타입은 가장 크게 기본형과 참조형으로 나눌 수 있다.
기본형은 하나의 값을 여러 변수에서 공유하지 않지만 참조형은 주소값을 공유하기 때문에 여러 변수에서 공유할 수 있다.
기본형
int a= 10;
int b=a;
b=20;
// 결과 값 : a = 10 , b =20
참조형
Human h1 = new Human("유재석");
Human h2 = h1;
h2.setName("박명수");
// 결과 : h1.getName : 박명수, h2.getName : 박명수
참조형 변수는 같은 참조값을 통해 같은 인스턴스를 참조할 수 있다. 이런 참조형의 특성때문에 사이드 이펙트가 발생할 수 있다. 이를 방지하기 위해 처음부터 서로 다른 인스턴스를 참조할 수 도 있지만 객체의 공유를 원하는 시점에는 사이드 이펙트를 방지해야 한다.
*사이드 이펙트 : 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것
불변 객체 도입
문제의 직접적인 원인은 공유된 객체의 값을 변경하는 것이다. 즉 값이 변경되지 않게 막기만 하면 되는 것이다.
바로 필드에 final을 붙여 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 된다.
불변 객체 값 변경
불변 객체를 사용할 때 값을 변경해야 되는 상황에는 새로운 객체를 만들어서 반환하면 된다.!
'JAVA > 기본 문법 내용 정리' 카테고리의 다른 글
웹 개발자 면접 정리 - 실무 면접 (0) | 2024.06.13 |
---|---|
Exception 종류 (0) | 2024.05.30 |
다형성 (0) | 2022.12.29 |
정규표현식 (0) | 2021.11.18 |
GUI 이벤트 처리 (0) | 2021.11.05 |