Back-End

[JAVA] 3중 상속과 인터페이스

Minch13r 2025. 1. 17. 17:48
package day014;

import java.util.Random;
import java.util.Scanner;

// 인터페이스를 쓰는 이유 : 지금은 강제해야 할 메서드 수가 적지만, 나중에 그 수가 많아지면 한 곳에서 관리하는게
// 유지보수에 용이해 따로 interface 파일을 사용했고 참조했습니다.

// 클래스 내부 순서 1. 멤버변수 혹은 클래스 변수 | 2. 생성자 | 3. 메서드 | 4. getter/setter | 5. toString

// 동물 클래스 선언, AnimalCryImpl 파일 참조
// abstract 클래스를 쓰는 이유는 객체 생성을 막기위해서 추상화 시켰습니다.
// 자녀 클래스(Camel, Dog, Turtle, Lizard)만 객체 생성이 가능해야 하기 때문입니다.
abstract class Animal implements AnimalCryImpl {
    // 접근제어자 멤버변수
    // private를 쓴 이유는 내부에서만 참조 가능하고 외부에서 참조가 불가능하게 하기 위해서 사용
    // 이 접근제어자를 사용했기 때문에 getter와 setter로 값을 재정의 및 가져오게끔 해봤습니다.
    private String name; // 멤버변수 이름
    private int age; // 멤버변수 나이
    private String category; // 멤버변수 카테고리
    static Random rand = new Random(); // 클래스변수 랜덤 객체 선언
    // random을 왜 클래스변수를 썼냐면, 랜덤객체를 계속 선언하면 memory leak가 발생하기 때문에
    // 이를 막기 위해 가장 상위 클래스에 클래스변수로 선언했습니다.

    // 생성자는 초기화 하는 역할
    Animal(String name, int age, String category) { // 생성자
        this.name = name;
        this.age = age;
        this.category = category;
    }

    @Override // AnimalCryImpl 오버라이딩
    // 인터페이스에서는 void cry라 선언했지만
    // 사실상 public abstract가 생략된 것이기에 사라진 것이 아니므로
    // 오버라이딩의 조건인 메서드 시그니처가 똑같아야 하기 때문에
    // public abstract를 붙이고 오버라이딩을 진행했습니다.
    public abstract void cry(); // 울음소리 메서드

    // 최대 랜덤 수 반환
    int returnRandomNum(int max) {
        // 인자를 정수형 max로 선언해서
        // 랜덤으로 자식 수 반환
        return rand.nextInt(max + 1);
    }

    //Getter, Setter
    public String getName() {
        // IDE에서는 자동으로 생성해주지 않지만
        // 저희는 공부하는 입장이고, 이게 어디를 참조하고 있는지
        // 명확하게 표현하기 위해서 getter에도 this를 붙여봤습니다.
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public String getCategory() {
        return this.category;
    }
    public void setCategory(String category) {
        this.category = category;
    }

    @Override // toString 오버라이딩
    // toString 오버라이딩을 진행한 이유는 실제로 쓰이지는 않지만
    // 만약에 값을 호출했을 때 hash 상태로 값이 나오기 때문에
    // 따로 오버라이딩을 진행해 표현해놨습니다.
    // 여기도 똑같이 this를 통해 자기자신을 가르키는 형식으로 진행시켜놨습니다.
    public String toString() {
        return "동물[" + "이름 = " + this.name + ", 나이 = " + this.age +
                ", 카테고리 = " + this.category + ']';
    }
}

// 새끼는 어미 포유류와 함께 같이 들어온다는 가정
// 포유류 클래스 선언, 동물 상속
abstract class Mammal extends Animal {
    private int cubNumber; // 새끼수
    private String fur; // 털 길이 정도

    // 포유류 생성자, 부모 상속, 카테고리 포유류로 설정
    Mammal(String name, int age, int max) {
        // 자식 생성자는 부모생성자를 가장 먼저 호출하기 때문에
        // 제일 위에 써준다.
        super(name, age, "포유류");
        // 생성자에서 새끼 수를 랜덤으로 설정 (예: 최대 5마리)
        // 함수에도 this가 생략되어 있어서 공부하는 입장이기에 this를 붙였습니다.
        this.cubNumber = this.returnRandomNum(max);  // 최대 2마리까지 새끼 설정
        this.fur = this.furLength(); // 생성 시 털길이도 랜덤으로 생성

    }

    // 총 마리수(어미 1마리 + 랜덤 생성 새끼 수)
    // 정수형을 반환해주기 때문에 int로 선언했습니다.
    int sumNum() {
        return 1 + this.cubNumber;  // 어미 1마리 + 새끼 수
    }

    // 털길이 랜덤생성
    // 문자열을 반환해주기 때문에 String 형식으로 선언
    String furLength() {
        // 털길이를 0~2 중 숫자를 랜덤으로 받아서
        // 털길이의 특징을 해당하는 숫자의 문자열을 반환해 나타냈습니다
        int furLength = this.returnRandomNum(2); // 0~2

        if (furLength == 0) {
            return "없음";
        } else if (furLength == 1) {
            return "짧음";
        } else {
            return "김";    // furLength가 2인 경우
        }
    }

    @Override
    public abstract void cry(); // 울음소리 메서드

    // Getter, Setter
    public String getFur() {
        return this.fur;
    }
    public void setFur(String fur) {
        this.fur = fur;
    }

    public int getCubNumber() {
        return this.cubNumber;
    }
    void setCubNumber(int cubNumber) {
        this.cubNumber = cubNumber;
    }

    // 동적바인딩
    // 부모에게 있는걸 자식이 끌어와서 부모걸 무시하고 자기 걸 쓰는 것
    @Override
    public String toString() {
        return this.getCategory() + "[새끼수 = " + this.cubNumber + ", 털 길이 =" + this.fur + ']';
    }
}

// 파충류 클래스, 동물 상속
abstract class Reptiles extends Animal {
    private String scalePattern; // 비늘무늬
    private int eggNumber; // 알 개수

    // 멤버변수에는 maxLay가 없지만 왜 썼냐면 super 를 통해서
    // 부모생성자를 호출해서 랜덤 관련 객체를 사용할 수 있게 했습니다.
    // 가장 상위 클래스에 선언을 해서 상속을 통해 재사용이 가능합니다.
    // 유지보수에 용이하게 코드를 짰습니다.
    Reptiles(String name, int age, int maxLay) {
        super(name, age, "파충류"); // 부모 생성자 호출
        // 비늘무늬 랜덤생성 메서드 호출
        this.scalePattern = this.returnRandomScalePatter();
        // 알 개수 랜덤생성 메서드 호출
        this.eggNumber = this.returnRandomNum(maxLay);
    }

    // 비늘무늬 랜덤생성 메서드
    public String returnRandomScalePatter() {
        // 0~2 사이의 숫자를 랜덤으로 출력해서
        // 숫자에 해당하는 문자열을 반환하도록 했습니다.
        int num = this.returnRandomNum(2);

        if (num == 0) {
            return "다이아몬드";
        } else if (num == 1) {
            return "빗살무늬";
        } else {
            return "점";
        }
    }

    // 울음소리 오버라이딩
    @Override
    public abstract void cry(); // 울음소리 메서드

    // Getter, Setter
    public String getScalePattern() {
        return this.scalePattern;
    }

    public void setScalePattern(String scalePattern) {
        this.scalePattern = scalePattern;
    }

    public int getEggNumber() {
        return this.eggNumber;
    }

    public void setEggNumber(int eggNumber) {
        this.eggNumber = eggNumber;
    }

    // 파충류 정보출력
    @Override
    public String toString() {
        return this.getCategory() + "[비늘무늬 = " + this.scalePattern + ", 알 개수 = " + this.eggNumber + ']';
    }
}

// 낙타 클래스 선언, 포유류 상속
class Camel extends Mammal {
    private int hump;  // 혹 수

    // 혹 수와 나이를 받는 생성자로 수정
    Camel(int hump, int age) {
        // 최대 새끼수는 2마리로 설정
        super("낙타", age, 2);  // 부모 생성자 호출
        this.hump = hump;
    }

    // 울음소리 오버라이딩
    @Override
    public void cry() { // 울음소리 메서드
        System.out.println("우어우어어ㅓㅓㅓㅓ어ㅓㅓ…어어ㅓ어…ㅓㅓㅓ어");
    }

    // Getter, Setter
    public int getHump() {
        return this.hump;
    }

    public void setHump(int hump) {
        this.hump = hump;
    }

    // 정보 출력
    @Override
    public String toString() {
        return "이름 : " + this.getName() + "\n" +
                "카테고리 : " + this.getCategory() + "\n" +
                "나이 : " + this.getAge() + "살\n" +
                "털 길이 : " + this.getFur() + "\n" +
                "혹 수 : " + this.hump + "\n" +
                "새끼 수 : " + this.getCubNumber() + "\n" +
                "전체 수 : " + this.sumNum();
    }
}

// 강아지 클래스 선언, 포유류 상속
class Dog extends Mammal {
    private String job; // 강아지 직업

    // 생성자
    Dog(String job, int age) {
        // 최대새끼수 5마리로 설정, 부모 생성자 호출
        super("강아지", age, 5);
        this.job = job;
    }

    // 울음소리 오버라이딩
    @Override
    public void cry() { // 울음소리 메서드
        System.out.println("멍멍!");
    }

    // Getter, Setter
    public String getJob() {
        return this.job;
    }
    public void setJob(String job) {
        this.job = job;
    }

    // 강아지 정보 출력
    @Override
    public String toString() {
        return "이름 : " + this.getName() + "\n" +
                "카테고리 : " + this.getCategory() + "\n" +
                "나이 : " + this.getAge() + "살\n" +
                "털 길이 : " + this.getFur() + "\n" +
                "직업 : " + this.job + "\n" +
                "새끼 수 : " + this.getCubNumber() + "\n" +
                "전체 수 : " + this.sumNum();
    }
}

// 거북이 클래스, 파충류 상속
class Turtle extends Reptiles {
    private int turtleSize; // 거북이 등껍질 크기

    // 거북이 생성자
    Turtle(int turtleSize, int age) {
        super("거북이", age, 10);
        this.turtleSize = turtleSize;
    }

    // 울음소리 오버라이딩
    @Override
    public void cry() { // 울음소리 메서드
        System.out.println("끡끡");
    }

    // Getter, Setter
    public int getTurtleSize() {
        return this.turtleSize;
    }
    public void setTurtleSize(int turtleSize) {
        this.turtleSize = turtleSize;
    }

    // 정보출력 오버라이딩
    @Override
    public String toString() {
        return "이름 : " + this.getName() + "\n" +
                "카테고리 : " + this.getCategory() + "\n" +
                "나이 : " + this.getAge() + "살\n" +
                "비늘무늬 : " + this.getScalePattern() + "\n" +
                "등껍질 크기 : " + this.getTurtleSize() + "cm\n" +
                "알 수 : " + this.getEggNumber() + "개";
    }
}

// 도마뱀 클래스, 파충류 생성자
class Lizard extends Reptiles {
    private String tailRegen; // 꼬리재생여부 가능,불가능

    // 도마뱀 생성자
    Lizard(String tailRegen, int age) {
        // 부모 클래스 호출
        super("도마뱀", age, 3);
        this.tailRegen = tailRegen;
    }

    // 울음소리 오버라이딩
    @Override
    public void cry() { // 울음소리 메서드
        System.out.println("엥옝옝옝옝옝옝예");
    }

    // Getter, Setter
    public String getTailRegen() {
        return this.tailRegen;
    }
    public void setTailRegen(String tailRegen) {
        this.tailRegen = tailRegen;
    }

    // 정보출력 오버라이딩
    @Override
    public String toString() {
        return "이름 : " + this.getName() + "\n" +
                "카테고리 : " + this.getCategory() + "\n" +
                "나이 : " + this.getAge() + "살\n" +
                "비늘무늬 : " + this.getScalePattern() + "\n" +
                "꼬리 재생여부 :" + this.getTailRegen() + "\n" +
                "알 개수 : " + this.getEggNumber() + "개" + "\n";
    }
}


public class Zoo {
    // 동물 전체 출력 함수
    // animals의 배열 길이만큼 반복하게 설정
    // i+1을 한 이유는 실제 인덱스는 0부터 시작하기에 우리가 원하는 숫자인 1부터 시작하기 위해서 적었습니다.
    public static void printAnimalInfo(Animal[] animals) {
        for (int i = 0; i < animals.length; i++) {
            // NPE 체크
            if (animals[i] != null) {  // null 체크 필요, 없으면 출력 안 해도 되니까
                System.out.println("\n=== " + (i + 1) + "번째 동물 정보 ===");
                System.out.println(animals[i].toString()); // 해당 인덱스의 동물 정보 출력
            }
        }
    }


    // main 함수
    public static void main(String[] args) {
        // 배열 선언 길이 4
        Animal[] animals = new Animal[4];

        // 샘플 데이터
        animals[0] = new Camel(2, 5); // 낙타,  혹 2개, 나이 5살
        animals[1] = new Dog("경찰견", 3); // 강아지, 직업 경찰견, 나이 3살
        animals[2] = new Lizard("가능", 3); // 도마뱀, 재생 가능, 나이 3살
        animals[3] = new Turtle(8, 6); // 거북이, 크기 8cm, 나이 6살

        // 스캐너와 랜덤 객체를 계속 new 해주면 memory leak가 발생하기 때문에
        // while문 바깥에다가 생성해줬습니다.

        // 스캐너 객체 선언
        Scanner sc = new Scanner(System.in);

        // 랜덤 객체 선언
        // 클래스에서 쓰는 Random 객체와는 다르게 main 함수에서 사용하기에 새로 선언
        Random rand = new Random();

        // 동물 유무 유효성 검사
        boolean hasAnimalFlag = false; // 동물이 없는 상태로 가정
        for (Animal animal : animals) {
            if (animal != null) { // 동물이 있으면
                hasAnimalFlag = true; // true로 값변환
                break; // for문 종료
            }
        }

        // true로 내려오면 false로 진행되기 때문에 if문이 실행이 안 됨
        if (!hasAnimalFlag) { // 동물이 없으면 false가 그대로 내려와 true진행
            System.out.println("동물이 하나도 없습니다."); // 에러안내
            return; // 종료
        }

        // 메뉴(기능) 관련 반복문, 무한루프
        while (true) {
            System.out.println("===== 메뉴판 =====");
            System.out.println("1. 동물 전체 출력");
            System.out.println("2. 가장 나이가 많은 동물 정보 출력");
            System.out.println("3. 이름 검색하여 정보 출력");
            System.out.println("4. ★★★ 오늘 하루 행운의 동물은? ★★★");
            System.out.println("0. 종료");
            System.out.print("숫자를 입력해주세요(0~4) \n>> ");
            int num = sc.nextInt(); // 스캐너로 숫자 입력

            if(num < 0 || 4 < num) { // 옳지 않은 메뉴번호 입력, 유효성 검사
                System.out.println("잘못된 입력입니다! 숫자를 다시 입력해주세요."); // 에러 안내
                continue; // 메뉴 처음으로, 가독성 향상
            }
            else if (num == 0) { // 0. 종료 기능
                System.out.println("종료됩니다."); // 종료안내 UI/UX
                break; // while 탈출
            }
            else if (num == 1) { // 1. 동물 전체 출력
                printAnimalInfo(animals); // 동물 전체 출력 함수 호출
                continue; // 메뉴 처음으로, 가독성 향상
            }
            else if (num == 2) { // 2. 가장 나이가 많은 동물 정보 출력
                Animal pivot = animals[0]; // 0번 인덱스가 피봇으로 기준
                for (int i = 1; i < animals.length; i++) {
                    if (animals[i].getAge() > pivot.getAge()) { // i번 인덱스가 pivot보다 크면?
                        pivot = animals[i]; // pivot은 i번 인덱스의 값으로 저장
                        System.out.println("가장 나이가 많은 동물 정보 출력!!"); // UI/UX
                        System.out.println(animals[i].toString()); // 해당 동물 정보 출력
                    }
                }
            }
            else if (num == 3) { // 3. 이름 검색하여 정보 출력
                while (true) { // 언제까지 입력할지 몰라서 무한루프
                    // 유일하게 계속 반복되기 때문에 종료 0번을 따로 둬서 사용자가 원할 때 탈출할 수 있게
                    // 편리성을 좀 더 높여봤습니다.
                    System.out.print("동물 이름을 입력해주세요 (종료는 0 입력) >> ");
                    String ans = sc.next(); // 동물 이름도 입력받고 숫자도 입력받아야 해서 String
                    // 0 입력시 검색 종료(뒤로 가게)
                    if (ans.equals("0")) { // "0"으로 넣어서 정수를 입력하는 것처럼 보이지만 실제로는 문자열임을 설정
                        // tip. 원시타입과 원시타입이 아닌 자료형은 ==으로 비교가 안돼서 .equals 사용
                        break;
                    }
                    boolean hasStringContainsFlag = false;  // 동물을 찾았는지 확인하는 플래그
                    // 못 찾았다고 가정
                    for (int i = 0; i < animals.length; i++) {
                        // ans.contains(animals[i].getName()) 일 경우, ans에서 저 배열에 포함된 값을 비교하게 되니까
                        // 비교하는 위치(앞, 뒤)가 중요하기 때문에 유의해야 한다.
                        if (animals[i].getName().contains(ans)) { // contain을 사용해 문자 하나만 해당해도 출력하게끔 진행
                            System.out.println("\n=== 검색된 동물 정보 ===");

                            System.out.println(animals[i].toString());  // 배열에 해당하는 정보 출력
                            hasStringContainsFlag = true;  // 동물을 찾았으므로 false로 변경
                        }
                    }

                    if (!hasStringContainsFlag) {  // 동물을 못 찾은 경우에만 메시지 출력
                        System.out.println("입력하신 이름의 동물이 없습니다.");
                    }
                }
            }
            // else로 진행해도 되는데 확장할 경우 가독성을 위해서 else if (num == 4)로 진행
            else if (num == 4) { // 4. 행운의 동물 랜덤으로 출력
                int randNum = rand.nextInt(4); // 0~3번 인덱스
                animals[randNum].cry(); // 동물 울음소리
                // 해당 번호 동물 이름 호출

                // 이름 불러오기, UI/UX
                System.out.println("오늘 하루 행운의 동물은 " + animals[randNum].getName() + "입니다.");

                // 대답 언제까지 제대로 입력할 줄 몰라서 while 무한루프
                while(true) {
                    System.out.print(animals[randNum].getName() + "의 정보를 보시겠습니까? (Y/N)\n>> ");
                    String ans = sc.next();
                    //유효성 검사, y랑 n 아니면 다시 입력하게 선언, y랑 n 둘 다 대/소문자 상관없이 확인
                    if(!ans.equalsIgnoreCase("y") && !ans.equalsIgnoreCase("n")){
                        System.out.println("잘못 입력하셨습니다 다시 입력해주세요"); // 에러
                        continue; // 가독성 향상
                    }
                    // y 대/소문자 상관없이 정보 확인해줌
                    else if(ans.equalsIgnoreCase("y")){
                        System.out.println("정보를 확인합니다\n"); // UI/UX
                        // 랜덤숫자에 해당하는 인덱스에 위치한 정보 출력
                        // .toString()을 사용 안 해도 자동호출 돼서 정상적으로 실행이 되지만
                        // 공부하는 입장이기 때문에 진행
                        System.out.println(animals[randNum].toString());
                        break; // 반복문 탈출
                    }
                    // n 대/소문자 상관없이 정보 확인 안 하고 반복문 탈출
                    else if (ans.equalsIgnoreCase("n")){
                        System.out.println("정보를 확인하지 않습니다.");
                        // 반복문 탈출
                        break;
                    }
                }
            }
        }
    }
}

package day014;

public interface AnimalCryImpl {
    void cry();
}

'Back-End' 카테고리의 다른 글

[JAVA] 기초 공부 12 File I/O  (2) 2025.01.21
[JAVA] 기초공부 11 제네릭과 컬렉션  (1) 2025.01.20
[JAVA] 기초 공부 10  (1) 2025.01.15
[JAVA] 기초 공부 9  (1) 2025.01.14
[JAVA] 기초 공부 8  (1) 2025.01.13