4-객체지향
클래스
클래스=class=설계도
//클래스 = 여러가지 상태(특징)을 가지고 있다.
public class Dog {
int age = 20;
String name = "a";
final String color = "b"; //변경 불가능한 상수
}// = 상태 = 필드 = 전역변수
public class DogApp {
public static void main(String[] args) {
Dog d = new Dog();
System.out.println(d.age);
System.out.println(d.name);
System.out.println(d.color);
d.age+=1;
d.name="aa";
//d.color"bb"; 불가
}
}
생성자
//값을 초기화 하지 않는 이유는 new 할때마다 새로운 값을 주기 위해서다.
//초기화 안하면 null
public class Cat {
//여기서 초기화 하지말고, 생성자를 통해서 초기화 하자
String name;
String color;
public Cat() {
//디폴트 생성자, 원래는 개발자가 생략해도 된다
//만약 생성자를 임의로 추가하면 디폴트 생성자는 필수다.!!
System.out.println("디폴트 생성자");
}
public Cat(String name,String color) {
//매개변수는 stack에 존재
//개발자가 생성자 구현함 = 디폴트 생성자 추가해줘야 한다.
System.out.println(name+color);
//stack에 있는 값을 heap으로 옮겨 오랫동안 보관
this.name = name;
this.color=color;
}
}
public class CatApp {
public static void main(String[] args) {
Cat c1 = new Cat("n1","n2");
System.out.println(c1.name);
System.out.println(c1.color);
Cat c2= new Cat();
}
}
this
public class People {
String name;
int age;
public People(String name, int age){
System.out.println("People 메서드 스택 name :"+name);
System.out.println("People 메서드 스택 age :"+age);
this.name=name;
this.age=age;
};
}
public class PeopleApp {
public static void main(String[] args) {
People p1 = new People("a",1);
}
}
클래스,오브젝트,인스턴스
클래스 : 신이 가지고 있는 설계도 = .java 파일
오브젝트 : new가 가능한것
클래스 -new-> heap에 존재 = 인스턴스
Person a = new Person(); -> 객체 생성 동시에 인스턴스화
Person a; -> 객체 생성
a = new Person(); ->인스턴스
클래스의 상태와 행위
oop의 원칙 : 클래스의 필드는 메서드에 의해서 변한다
실수 방지를 위해서 클래스 필드에는 private를 꼭 붙여주자
class Player {
String name;
private int thirsty;
public Player(String name, int thirsty) {
this.name = name;
this.thirsty = thirsty;
}
void 물마시기(){ //행위 = 메서드 = 값을 변경
this.thirsty-=50;
}
int 목마름상태확인(){
return this.thirsty;
}
}
public class OOPEx01 {
public static void main(String[] args) {
Player p1 = new Player("a", 100);
System.out.println(p1.name);
// 마법 : 원인없이 변화 발생
// p1.name="b";
// 실수 가능한 코드
// p1.물마시기();
// p1.thirsty=50
p1.물마시기();
System.out.println(p1.목마름상태확인());
}
}
상속
1. 추상화
2. 상태, 행위를 물려 받을 수 있다. <=> import : 상태,행위 가져와서 쓴다.
자동차는 엔진을 상속 할 수 없다 = 타입 일치 불가
치즈햄버거는 햄버거(추상화된 존재)를 상속 받는다 = 타입 일치 가능
치킨햄버거는 햄버거(추상화된 존재)를 상속 받는다 = 타입 일치 가능
=>치즈,치킨 햄버거는 햄버거냐? ok
=>자동차는 엔진이냐? x
class Engine{
int power =2000;
}
class Car{ // 자동차는 엔진이 아니니까 상속 안된다.
//컴포지션
Engine e;
public Car(Engine e) {
this.e = e;
}
}
class Hamburger{
String name ="기본 햄버거";
String 재료1 ="양상추";
}
// 상속은 상태와 행위를 물려 받을 수 있지만 꼭 타입이 일치 되어야 한다.!!
class CheeseHamburger extends Hamburger{ //치즈 햄버거는 햄버거이다.!!
//겹치지 않은 상태(필드)만 물려 받는다.
String name ="치즈 햄버거";
}
class ChickenHamburger{
String name = "치킨햄버거";
//이건 컴포지션
Hamburger h;
public ChickenHamburger(Hamburger h) {
this.h = h;
}
}
public class OOPEx02 {
public static void main(String[] args) {
Engine e = new Engine();
Car c1 = new Car(e);
System.out.println(c1.e.power);
Hamburger h1 = new CheeseHamburger();
System.out.println(h1.name);
Hamburger h2 = new Hamburger();
ChickenHamburger ck1 = new ChickenHamburger(h2);
System.out.println(ck1.name);
}
}
다형성
class 요리사{
String name ="요리사";
}
class 홍길동 extends 요리사{
String name="홍길동";
}
public class OOPEx03 {
public static void main(String[] args) {
홍길동 h1 = new 홍길동(); //홍길동, 요리사 동시에 heap에 존재, 바라보는건 홍길동
System.out.println(h1.name);
요리사 y1 = new 홍길동(); //홍길동, 요리사 동시에 heap에 존재, 바라보는건 요리사
System.out.println(y1.name);
//홍길동 h2 = new 요리사(); -> 이건 안된다, 요리사를 heap에 띄우면 요리사만 존재하기 때문에
}
}
오버로딩, 오버로딩의 한계
Overloading = 과적재, 함수이름이 같아도 다른 함수로 인식한다, 매개변수가 다르니까
class 임꺽정{
void 달리기(){
System.out.println("달리기1");
}
//과적재=overloading 사용
void 달리기(int speed){
System.out.println("달리기2");
}
}
public class OOPEx04 {
public static void main(String[] args) {
임꺽정 e = new 임꺽정();
e.달리기();
e.달리기(1);
}
}
오버로딩 안하면 함수 이름, 매개변수 계속 기억해야한다.
오버로딩 하면 그냥 동일한 함수 사용하고 매개변수만 변경하면 된다.
그러나 오버로딩은 갯수가 적을때는 되지만 많은 수 앞에서는 한계가 명확하다 - 매번 매개변수 변경하며 함수 생성해야한다
package ch05;
class 전사{//검
String name ="전사";
void 기본공격(궁수 e1){
System.out.println(e1.name+"검으로 공격");
}
void 기본공격2(광전사 e1){
System.out.println(e1.name+"검으로 공격");
}
}
class 궁수{//활
String name ="궁수";
void 기본공격(광전사 e1){
System.out.println(e1.name+"활로 공격");
}
void 기본공격(전사 e1){
System.out.println(e1.name+"활로 공격");
}
}
class 광전사{//도끼
String name ="광전사";
}
public class OOPEx05 {
public static void main(String[] args) {
전사 u1 = new 전사();
궁수 u2 = new 궁수();
광전사 u3 = new 광전사();
//오버로딩 안하면 할때마다 함수 이름 기억해야한다
u1.기본공격(u2);
u1.기본공격2(u3);
//오버로딩 하면 기억해야할게 매개변수만 기억하면 된다.
u2.기본공격(u3);
}
}
오버라이딩
동적바인딩을 이용해서 내가 원하는 메소드(자식 메소드)를 실행하는 전략!!
class 프로{
String name ="프로";
void 공격(프로 e){
System.out.println("프로 메서드");
}
String 이름확인(){
return "프로";
}
}
class 질럿 extends 프로{
String name ="질럿";
//오버 라이드 = 무효화, 부모의 메서드를 무효화 한다
void 공격(프로 e){
System.out.println("타깃 :"+e.이름확인()+"실행 : "+this.name);
}
//오버 라이드 = 무효화, 부모의 메서드를 무효화 한다
String 이름확인(){
return name;
}
}
class 드라 extends 프로{
String name ="드라";
void 공격(프로 e){
System.out.println("타깃 :"+e.이름확인()+"실행 : "+this.name);
}
String 이름확인(){
return name;
}
}
class 다크 extends 프로{
String name ="다크";
void 공격(프로 e){
System.out.println("타깃 :"+e.이름확인()+"실행 : "+this.name);
}
//🔥함수명 다르게하면 제대로 오버라이딩 안된다🔥
String 이름체크(){
return name;
}
}
public class OOPEx06 {
public static void main(String[] args) {
프로 u1 = new 질럿();
프로 u2 = new 드라();
프로 u3 = new 다크();
u1.공격(u2);
u2.공격(u3);
u3.공격(u1);
}
}
동적 바인딩
부모 name = new 자식(); -> 자식, 부모 둘다 heap에 생성, 가리키는건 부모
name.method -> 부모에 있는 메서드 실행할려다가, 자식에 동일한 메서드 있으면 자식 메서드 실행
===>>>그게 바로 동적 바인
추상 클래스
추상적이다 = new 할 수 없다 = 메모리에 띄울 수 없다 = 미완성 설계
추상 클래스 = 추상 메서드, 일반 메서드 둘다 가능
추상 메서드 = 자식은 반드시 구현해야한다, 실수를 줄일 수 있다
abstract class Animal{
//추상 메서드는 중괄호 필요없다. 어차피 안쓰이기도 하니까
abstract void speak();
//추상클래스 안에 추상 메서드만 있어야 하는건 아니다
void hello(){
System.out.println("hi");
}
}
class Dog extends Animal{
void speak(){
System.out.println("멍");
}
}
class Cat extends Animal{
//부모가 추상메서드를 가지고 있으면 자식은 추상메서드를 반드시 상속해야한다
//강제성이 있기 때문에 실수 할 수가 없다!!!!!!
@Override
void speak() {
System.out.println("옹");
}
}
public class OOPEx07 {
public static void main(String[] args) {
Animal a1 = new Dog();
a1.hello();
//동적 바인딩!!
a1.speak();
//Animal a3 = new Animal(); -> 불가, 추상클래스는 new 할 수 없다.
}
}
인터페이스
인터페이스 : 일방적인 약속(갑과 을이 존재하는 약속)
vs
프로토콜 : 상호 협의 약속(동등한 관계)
자바의 인터페이스 : 행위에 대한 제약을 준다
기본틀!!
인터페이스 : 다중 구현 가능! 기능 명세 제공! -> 이런 기능은 필수야!! = 설계 계약(Contract)
추상클래스 : 단일 상속 가능! 공통 기능,일부 구현! -> 공통은 내가주고 나머진 너가 채워!! = 공통 기능의 재사용
심화!!!! 인터페이스,추상메서드 : 같은거 아니냐??
- 눈, 코, 날개는 기능(능력) → 인터페이스로 표현하면 딱 좋음
- 동물은 **공통 속성(예: 이름, 종족)**과 공통 동작(예: 먹다) → 추상 클래스로 표현
- 강아지는 날개 없음 → 날다 기능을 구현하면 안 됨
- 그래서 인터페이스를 써야만 강아지에게 '없는 기능'을 강요하지 않게 됨
"모든 동물이 날 수는 없지만, 날 수 있는 동물은 '날 수 있다'는 능력이 있어야 하니까 인터페이스로 분리해야 한다!"
❌ 잘못된 구조 (비추천: 추상 클래스만 쓸 경우)
abstract class Animal {
abstract void 뜨다(); // 눈 관련
abstract void 숨쉬다(); // 코 관련
abstract void 앞으로날다(); // 날개 관련
}
class Dog extends Animal {
void 뜨다() { ... }
void 숨쉬다() { ... }
void 앞으로날다() { ... } // ❌ 날개 없어도 구현해야 함
}
✅ 올바른 구조 (인터페이스 활용)
interface Eye {
void 뜨다();
void 감다();
void 깜빡이다();
}
interface Nose {
void 숨쉬다();
void 숨참다();
void 냄새맡다();
}
interface Wing {
void 앞으로날다();
void 뒤로날다();
}
abstract class Animal {
String name;
abstract void 먹다();
}
🐶 강아지 클래스
class Dog extends Animal implements Eye, Nose {
@Override
public void 뜨다() { System.out.println("강아지가 눈을 떴다"); }
@Override
public void 감다() { System.out.println("강아지가 눈을 감았다"); }
@Override
public void 깜빡이다() { System.out.println("강아지가 눈을 깜빡인다"); }
@Override
public void 숨쉬다() { System.out.println("강아지가 숨쉰다"); }
@Override
public void 숨참다() { System.out.println("강아지가 숨참는다"); }
@Override
public void 냄새맡다() { System.out.println("강아지가 냄새를 맡는다"); }
@Override
void 먹다() { System.out.println("강아지가 먹는다"); }
}
🦅 독수리 클래스
class Eagle extends Animal implements Eye, Nose, Wing {
@Override
public void 뜨다() { System.out.println("독수리가 눈을 떴다"); }
@Override
public void 감다() { System.out.println("독수리가 눈을 감았다"); }
@Override
public void 깜빡이다() { System.out.println("독수리가 눈을 깜빡인다"); }
@Override
public void 숨쉬다() { System.out.println("독수리가 숨쉰다"); }
@Override
public void 숨참다() { System.out.println("독수리가 숨참는다"); }
@Override
public void 냄새맡다() { System.out.println("독수리가 냄새를 맡는다"); }
@Override
public void 앞으로날다() { System.out.println("독수리가 앞으로 난다"); }
@Override
public void 뒤로날다() { System.out.println("독수리가 뒤로 난다"); }
@Override
void 먹다() { System.out.println("독수리가 먹는다"); }
}

interface MoveAble {
//제어자 안적어주면 public, abstract 2개가 생략되어 있다.
void 위();
}
interface MoveAble2 {
//제어자 안적어주면 public, abstract 2개가 생략되어 있다.
void 위();
void 땅();
}
/*
만약 추상 클래스가 아닌 일반 클래스가 implement 하면 오류발생한다
interface의 메서드는 반드시 구현해줘야 하는데
추상 클래스와 달리 일반 클래스 이미 완성되어 있기 때문에 implement 할 수 없다.
따라서 추상클래스가 interface를 impelments하면
자식 클래스가 interface의 메서드를 구현해야 하는데,
추상클래스가 대신해서 구현하면 상관없다.
*/
//추상 클래스가 자식 클래스 대신 interface 메서드를 구현한 예시
abstract class 사나운 implements MoveAble {
abstract void 공격();
@Override
public void 위() {
System.out.println("사나운 위로");
}
}
class 사자 extends 사나운 {
@Override//어노테이션 : JVM이 실행시에 분석해서 확인 = JVM의 힌트
void 공격() {
System.out.println("사자 공격");
}
public void 위(){
System.out.println("사자 위로");
}
}
abstract class 온순한 implements MoveAble2 {
abstract void 채집();
}
//자식 클래스가 interface의 메서드를 구현한 예시
class 소 extends 온순한 {
@Override
void 채집() {}
@Override
public void 위() {}
@Override
public void 땅() {}
}
public class OOPEx09 {
/*
이 메서드의 매개변수 타입은 사나운입니다.
이 함수는 사나운 타입이거나 그 하위 클래스 객체를 받을 수 있습니다.
✅ 즉, 컴파일러는 매개변수를 받을 때 "사자냐?"가 아니라 "사나운이냐, 혹은 그 자식이냐?"를 기준으로 봅니다.
*/
static void 조이스틱(사나운 u) {}
public static void main(String[] args) {
/*
👉 기능(행위)에만 관심이 있다면, 상위 타입(추상 클래스나 인터페이스)으로 선언하세요.
👉 사자의 고유한 기능(오직 사자만의 메서드 등)에 접근해야 한다면, 사자 타입으로 선언하세요.
*/
사자 uu = new 사자();
uu.공격();
}
}
SRP,DIP
A가 B에 의존한다 = B에 변경사항이 생기면 A가 변경되야만 한것
SRP = Single Response Principle (단일 책임 원칙)
책임 = 행위 = 메서드 -> 하나의 행위에 대한 책임은 한명이 갖는다
DIP = Dependency Inversion Principle가 필요한 이유
처음부터 코드를 완벽하게 만들 수 없다 -> Continuous Integration는 필수!!
즉, “상위 모듈도, 하위 모듈도 모두 구체적인 것이 아닌 추상(인터페이스, 추상 클래스)에 의존해야 한다.”
package ch05;
// ✅ [인터페이스] CanAble: "대화할 수 있는 능력"만을 명시 → SRP에 적합
interface CanAble {
void talk();
}
// ✅ [추상 클래스] 홀직원: 홀에서 일하는 직원의 공통 기능 정의
// - 공통 기능: talk() 구현 (손님과 대화)
// - SRP에 따라 "홀직원 역할"에만 집중
abstract class 홀직원 implements CanAble {
abstract void 청소();
@Override
public void talk() {
System.out.println("손님과 대화");
}
}
// ✅ 종업원: 홀직원의 하위 역할 (서빙과 주문)
// - 역할 구분 명확 → SRP에 부합
abstract class 종업원 extends 홀직원 {
void 서빙() {
System.out.println("서빙");
}
void 주문() {
System.out.println("주문받기");
}
}
// ✅ 캐셔: 홀직원의 다른 하위 역할 (정산과 계산)
abstract class 캐셔 extends 홀직원 {
void 정산하기() {
System.out.println("정산하기");
}
void 계산하기() {
System.out.println("계산하기");
}
}
// ✅ 요리장인: 홀과 무관한 역할 (talk 기능 없음)
// - talk을 구현하지 않음 → CanAble과 분리 → SRP 명확
abstract class 요리장인 {
abstract void 요리();
}
// ✅ 기: 종업원 역할 수행
class 기 extends 종업원 {
@Override
void 청소() {
System.out.println("기 청소");
}
}
class 니 extends 종업원 {
@Override
void 청소() {
System.out.println("니 청소");
}
}
class 디 extends 캐셔 {
@Override
void 청소() {
System.out.println("디 계산");
}
}
class 리 extends 캐셔 {
@Override
void 청소() {
System.out.println("리 계산");
}
}
class 미 extends 요리장인 {
@Override
void 요리() {
System.out.println("미 요리");
}
}
class 비 extends 요리장인 {
@Override
void 요리() {
System.out.println("비 요리");
}
}
// ✅ DIP 적용: 상위 모듈이 인터페이스(CanAble)에 의존하고 구현체는 몰라도 됨
class 응대매니저 {
// 상위 모듈은 "대화할 수 있는 능력(CanAble)"에만 의존
// 기, 디 등의 구체 클래스에 의존하지 않음
public void 응대하기(CanAble staff) {
staff.talk(); // 이 staff가 누구든 talk()만 있으면 됨
}
}
public class OOPEx10 {
public static void main(String[] args) {
// 구체적인 구현체를 인터페이스 타입으로 다루기
// => 구현이 바뀌더라도 응대매니저 코드는 변경될 필요가 없음
CanAble staff1 = new 기();
CanAble staff2 = new 디();
응대매니저 manager = new 응대매니저();
manager.응대하기(staff1);
manager.응대하기(staff2);
}
}
/*
✅ SRP (Single Responsibility Principle, 단일 책임 원칙)
- 각 클래스가 "하나의 역할"만을 책임지도록 설계됨
- 종업원: 서빙 & 주문
- 캐셔: 정산 & 계산
- 요리장인: 요리만
- 인터페이스 CanAble은 "talk"이라는 기능만을 명세함
✅ DIP (Dependency Inversion Principle, 의존 역전 원칙)
- 기존: 상위 모듈이 하위 구현 클래스(기, 디 등)에 직접 의존하면 유연성이 떨어짐
- 개선: 상위 모듈(응대매니저)은 인터페이스(CanAble)에만 의존
- 구체 클래스가 바뀌어도 응대매니저는 전혀 영향을 받지 않음
- ex) 새로운 대화 가능한 클래스가 추가되면 그대로 확장 가능
- 이는 코드의 확장성, 유지보수성, 테스트 용이성을 향상시킴
*/