스무번 째 수업

정혅·2024년 3월 9일

더 조은 아카데미

목록 보기
25/76

오전 시험

1. 어제 했던 고교정보, 대학정보 친구들 번호 저장하는 프로그램 만들기
package com.test.memo;

import java.util.Scanner;

class Friend {
    private String name, num, add;

    Friend(String name, String num, String add) {
        this.name = name;
        this.num = num;
        this.add = add;
    }

    public void showData() {
        System.out.print("이름: " + name + " ");
        System.out.print("전화번호: " + num + " ");
        System.out.print("주소: " + add + " ");
    }

    public void showBasicInfo() {
        System.out.print("이름: " + name + " ");
        System.out.print("전화번호: " + num + " ");
    }

    public void showOneInfo() {
        System.out.print("이름: " + name + " ");
    }
}

class HighFriend extends Friend {
    private String work;

    HighFriend(String name, String num, String add, String work) {
        super(name, num, add);
        this.work = work;
    }

    public void showData() {
        super.showData();
        System.out.print("직장: " + work + " ");
    }

    public void BasicInfo() {
        super.showBasicInfo();
        System.out.print("직장: " + work + " ");
    }
}

class UnivFriend extends Friend {
    private String major;

    UnivFriend(String name, String num, String add, String major) {
        super(name, num, add);
        this.major = major;
    }

    public void showData() {
        super.showData();
        System.out.print("전공: " + major);
    }

    public void BasicData() {
        super.showOneInfo();
        System.out.print("전공: " + major);
    }
}

class FriendInfoHandler {
    private Friend[] friends;
    private int friendCnt;

    public FriendInfoHandler(int num) {
        friends = new Friend[num];
        friendCnt = 0;
    }

    private void addFriendInfo(Friend fr) {// 객체 생성하면 배열에 넣는 메소드
        friends[friendCnt++] = fr;
    }

    public void addFriend(int user) {
        String name, num, address, work, major;

        Scanner scan = new Scanner(System.in);

        System.out.print("이름: ");
        name = scan.nextLine();
        System.out.print("전화 : ");
        num = scan.nextLine();
        System.out.print("주소 : ");
        address = scan.nextLine();

        if (user == 1) {
            System.out.print("직장: ");
            work = scan.nextLine();
            addFriendInfo(new HighFriend(name, num, address, work));// 객체를 배열에 넣어야하니까
        } else {
            System.out.print("전공: ");
            major = scan.nextLine();
            addFriendInfo(new UnivFriend(name, num, address, major));
        }
        System.out.println("입력완료! \n");
    }

    public void showAllData() {
        System.out.println("전체 정보를 출력합니다.");
        for (int i = 0; i < friendCnt; i++) {
            friends[i].showData();
            System.out.println();
        }
    }

    public void showBasicData() {
        System.out.println("전체 기본 정보를 출력합니다.");
        for (int i = 0; i < friendCnt; i++) {
            friends[i].showBasicInfo();
            System.out.println();
        }
    }
}

class Practice1 {
    private static void printUI() {
        System.out.println("*** 메뉴 선택 ***");
        System.out.println("1. 고교 정보 저장");
        System.out.println("2. 대학 친구 저장");
        System.out.println("3. 전체 정보 출력");
        System.out.println("4. 전체 기본 정보 출력");
        System.out.println("5. 프로그램 종료");
        System.out.print("선택>> ");
    }

    public static void main(String[] args) {
        FriendInfoHandler handler = new FriendInfoHandler(30);
        boolean flag = true;
        int user = 0;

        Scanner sc = new Scanner(System.in);

        while (flag) {
            printUI();
            user = sc.nextInt();
            sc.nextLine();
            switch (user) {
            case 1:
            case 2:
                handler.addFriend(user);
                break;
            case 3:
                handler.showAllData();
                break;
            case 4:
                handler.showBasicData();
                break;
            case 5:
                System.out.println("프로그램을 종료합니다.");
                flag = false;
                break;
            default:
                System.out.println("잘못된 입력입니다. 다시 입력하세요.");
            }
        }
    }
}
  1. case문 만들 때 숫자랑 case를 붙여놔서 안됐고

  2. 사용자에게 입력을 받았는데 다시 printUI()메소드가 실행되는것이다. 알고보니 nextInt() 때문에 버퍼에 남아있는 엔터값 때문이였다. 또 오타 때문에 시간이 많이 허비됐다....


2. 이진 검색을 이용해서 배열의 인덱스를 출력하자
package com.test.memo;

import java.util.Arrays;
import java.util.Scanner;

class Practice1 {

    public static void main(String[] args) {
        int[] num = new int[10];

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.print("검색하고자 하는 숫자를 입력하세요(종료하려면 00): ");
            int user = sc.nextInt();

            if (user == 00) {
                System.out.println("프로그램을 종료합니다.");
                break;//이 부분이 break가 안되
            }

            int result = binarySearch(num, user);

            if (result != -1) {
                System.out.printf("정수 %d는 인덱스 %d에 위치해 있습니다.", user, result);
            } else {
                System.out.println("사용자가 찾는 수가 없습니다.");
            }
        }
    }

    private static int binarySearch(int[] num, int user) {
        int start = 0, end = num.length - 1, cnt = 0;

        for (int i = 0; i < num.length; i++) {
            num[i] = (int) (Math.random() * 101);// 100까지 난수를 배열에 넣기
        }
        Arrays.sort(num);// 오름차순 정렬

        while (start <= end) {
            cnt++;
            int mid = (start + end) / 2; // 값 비교에 기준점

            if (user == num[mid]) {
                System.out.printf("%d번만에 찾았습니다\n", cnt);
                return mid;
            } else if (user > num[mid]) {
                start = mid + 1;
            } else {
                end = mid - 1;
            }
        }
        return -1;
    }
}
  1. 배열을 난수로 만들어서 Arrays를 이용해 정렬시켜줬다.

  2. 메인 메소드에서 사용자에게 입력을 받아서 binarySearch 메소드에서 배열 비교를 해줬다.

  3. 종료시킬 수 있는 숫자를 배열비교 메소드 호출하기 전에 위치시켜 사용자가 00을 입력하면 바로 종료될 수 있게끔 프로그래밍 했다.


5단계 전화번호부 프로그램

내가 짠 코드
package com.test.memo;

import java.util.Scanner;

class PhoneInfo {
    private String name, phoneNumber, birth;

    PhoneInfo(String name, String phoneNumber, String birth) {
        this.name = name;
        this.phoneNumber = phoneNumber;
        this.birth = birth;
    }

    PhoneInfo(String name, String phoneNumber) {// birth 없이 출력 가능하게
        this.name = name;
        this.phoneNumber = phoneNumber;
    }

    public String getName() {
        return name;
    }

    public void showData() {// 전체 정보
        System.out.printf("이름: %s\n", name);
        System.out.printf("전화번호: %s\n", phoneNumber);
        System.out.printf("생년월일: %s\n", birth);
    }

    public void basicData() {// 기본정보
        System.out.printf("이름: %s\n", name);
        System.out.printf("전화번호: %s\n", phoneNumber);
    }
}

class PhoneUnivInfo extends PhoneInfo {// 대학 동기들의 전화번호 저장
    private String major; // 전공
    private int year; // 학년

    PhoneUnivInfo(String name, String phoneNumber, String major, int year) {
        super(name, phoneNumber);
        this.major = major;
        this.year = year;
    }

    public void showData() {
        super.showData();
        System.out.printf("전공: %s\n", major);
        System.out.printf("학년: %d\n", year);
    }

    public void basicData() {
        super.basicData();
        System.out.printf("전공: %s\n", major);
        System.out.printf("학년: %d\n", year);
    }
}

class PhoneCompanyInfo extends PhoneInfo {
    private String company;

    public PhoneCompanyInfo(String name, String phoneNumber, String company) {
        super(name, phoneNumber);
        this.company = company;
    }

    public void showData() {
        super.showData();
        System.out.printf("회사: %s\n", company);
    }

    public void basicData() {
        super.basicData();
        System.out.printf("회사: %s\n", company);
    }
}

class PhoneInfoHandler {
    private PhoneInfo phone[];
    private int phoneCnt;

    public PhoneInfoHandler(int num) {
        phone = new PhoneInfo[num];
        phoneCnt = 0;
    }

    public int getPhoneCnt() {
        return phoneCnt;
    }

    private void addPhoneInfo(PhoneInfo pp) {
        phone[phoneCnt++] = pp;
    }

    public void addPhone() {
        String name, phoneNum, major, company;
        int year;
        Scanner sc = new Scanner(System.in);

        System.out.println("데이터 입력을 시작합니다.");
        System.out.println("1.일반 2.대학 3.회사");
        System.out.print("선택: ");
        int user = sc.nextInt();
        sc.nextLine();// 버퍼에 엔터 빼주기

        System.out.print("이름: ");// 공통된 입력 질문들
        name = sc.nextLine();
        System.out.print("전화번호: ");
        phoneNum = sc.nextLine();

        if (user == 1) {
            addPhoneInfo(new PhoneInfo(name, phoneNum));
        } else if (user == 2) {
            System.out.print("전공: ");
            major = sc.nextLine();
            System.out.print("학년(숫자만): ");
            year = sc.nextInt();
            sc.nextLine();

            addPhoneInfo(new PhoneUnivInfo(name, phoneNum, major, year));
        } else {
            System.out.print("회사: ");
            company = sc.nextLine();
            addPhoneInfo(new PhoneCompanyInfo(name, phoneNum, company));
        }
        System.out.println("데이터 입력이 완료되었습니다.");
    }

    public void searchDataByName(String searchName) {
        boolean found = false;
        for (int i = 0; i < phoneCnt; i++) {
            if (phone[i].getName().equals(searchName)) {
                phone[i].basicData();
                found = true;
            }
        }
        if (!found) {
            System.out.println("해당 이름의 데이터를 찾을 수 없습니다.");
        }
    }

    public void deleteDataByName(String deleteName) {
        boolean found = false;

        for (int i = 0; i < phoneCnt; i++) {
            if (phone[i].getName().equals(deleteName)) {
                found = true;
                System.out.println(deleteName + "의 데이터를 삭제합니다.");

                for (int j = i; j < phoneCnt; j++) {// 삭제할 인덱스부터 존재하는 인덱스까지 한칸 씩 땡기기위해서
                    phone[j] = phone[j + 1];
                }
                phoneCnt--;
                break;
            }
        }
        if (!found) {
            System.out.println("해당 이름의 데이터를 찾을 수 없습니다.");
        }
    }

    public void showAllData() {
        for (int i = 0; i < phoneCnt; i++) {
            phone[i].basicData();
        }
    }
}

class Practice {
    private static void printUI() {
        System.out.println("선택하세요...");
        System.out.println("1. 데이터 입력");
        System.out.println("2. 데이터 검색");
        System.out.println("3. 데이터 삭제");
        System.out.println("4. 모든 데이터 보기");
        System.out.println("5. 프로그램 종료");
        System.out.print("선택: ");
    }

    public static void main(String[] args) {
        PhoneInfoHandler handler = new PhoneInfoHandler(10);
        int user = 0;

        Scanner sc = new Scanner(System.in);

        while (user != 5) {
            printUI();
            user = sc.nextInt();
            sc.nextLine();

            switch (user) {
            case 1:
                handler.addPhone();
                break;
            case 2:
                System.out.println("데이터 검색을 시작합니다.");
                System.out.print("이름: ");
                String searchName = sc.nextLine();
                handler.searchDataByName(searchName);
                break;
            case 3:
                System.out.println("데이터 삭제를 시작합니다.");
                System.out.print("이름: ");
                String deleteName = sc.nextLine();
                handler.deleteDataByName(deleteName);
                break;
            case 4:
                System.out.println("모든 데이터를 출력합니다.");
                handler.showAllData();
                break;
            default:
                System.out.println("잘못 선택하셨습니다.");
            }
        }
    }
}//객체를 하나로 두고 그 안에 배열을 생성해야하는데(싱글톤) 값을 입력할 때마다 배열을 생성하고 있고, 만약 배열이 다 찼을때 메모리가 찼다는 오류메시지를 보내고있지 않고있
선생님 코드
import java.util.Scanner;

class PhoneInfo
{
    private String name;
    private String phone;

    PhoneInfo(String name, String phone)
    {
        this.name = name;
        this.phone = phone;
    }
    public String getName()
    {
        return name;
    }
    public void showPhoneInfo()
    {
        System.out.println("이름 : " + name);
        System.out.println("전화번호 : " + phone);
    }
}

class PhoneUnivInfo extends PhoneInfo
{
    private String major;
    private int year;

    PhoneUnivInfo(String name ,String phone, String major, int year)
    {
        super(name, phone);
        this.major = major;
        this.year = year; 
    }
    public void showPhoneInfo()
    {
        super.showPhoneInfo();
        System.out.println("전공 : " + major);
        System.out.println("학년 : " + year);
    }
}

class PhoneCompanyInfo extends PhoneInfo
{
    private String company;

    PhoneCompanyInfo(String name ,String phone, String company)
    {
        super(name, phone);
        this.company = company;
    }
    public void showPhoneInfo()
    {
        super.showPhoneInfo();
        System.out.println("회사 : " + company);
    }
}

class PhoneBook
{
    private static PhoneBook pb;
    private PhoneInfo[] pInfoArr;
    private int cntOfPhone;
    private PhoneBook(int sizePhoneInfo)
    {
        pInfoArr = new PhoneInfo[sizePhoneInfo];
        cntOfPhone = 0;
    }
    public static PhoneBook getPhoneBookInst(int sizePhoneInfo)//객체를 하나로 제한하는 > 싱글
    {
        if(pb == null)
            pb = new PhoneBook(sizePhoneInfo);
        return pb;
    }
    public void inputPhoneInfo(PhoneInfo pInfo)//삽입 정렬 메소드
    {
        int i = 0, j=0;
        if(cntOfPhone >= pInfoArr.length)// 용량이 안되면 저장 x 문장 출력
        {
            System.out.println("더 이상 저장할 수 없습니다.");
            return;
        }
        for(i=0;i<cntOfPhone;i++)
        {
            if(pInfoArr[i].getName().compareTo(pInfo.getName()) > 0)//알파벳 순서대로 배열에 들어가는.. compareTo를 String과 쓰면 문자열을 사전적으로 비교한다.
            {
                for(j=cntOfPhone-1;j>=i;j--)
                //변경될 배열을 중심으로 오른쪽으로 한칸씩 이동하고,
                {
                    pInfoArr[j+1] = pInfoArr[j];//변경될 배열에 새로운 배열 삽입
                }
                break;
            }
        }
        pInfoArr[i] = pInfo;//사전적으로 비교했는데, 새로 저장되는 값이 기존에 있던 값보다 사전적으로 앞에 위치한다면 그냥 저장된다. 
        cntOfPhone++;
    }
    public void searchPhoneInfo(String name)
    {
        int result = search(name);
        if(result != -1)
            pInfoArr[result].showPhoneInfo();
        else
            System.out.println("찾으시는 데이터가 없습니다.");
    }
    public void deletePhoneInfo(int idx)
    {
        int i=0;
        for(i=idx;i<cntOfPhone-1;i++)
            pInfoArr[i] = pInfoArr[i+1];
        pInfoArr[i] = null;
        cntOfPhone--;
        System.out.println("삭제가 완료되었습니다.");
    }


    public int search(String name)//검색하는 이름의 인덱스를 찾아주는 메소
    {
        for(int i=0;i<cntOfPhone;i++)
        {
            if(pInfoArr[i].getName().compareTo(name) == 0)
                return i;
        }
        return -1;        
    }

    public void showAllPhoneInfo()
    {
        for(int i=0;i<cntOfPhone;i++)
            pInfoArr[i].showPhoneInfo();
    }
}

class PhoneUI
{
    private static final int MAX_CNT=100;
    public static Scanner sc = new Scanner(System.in);
    private static PhoneBook pb = PhoneBook.getPhoneBookInst(MAX_CNT);

    public static final int INPUT_PHONEINFO = 1;
    public static final int SEARCH_PHONEINFO = 2;
    public static final int DELETE_PHONEINFO = 3;
    public static final int SHOW_ALL_PHONEINFO = 4;
    public static final int PROGRAM_QUIT = 5;

    public static final int GENERAL     = 1;
    public static final int UNIVERSITY     = 2;
    public static final int COMPANY     = 3;

    public static final int YES = 1;
    public static final int NO = 2;

    public static void mainMenu()
    {
        System.out.println("선택하세요...");
        System.out.println("1. 데이터 입력");
        System.out.println("2. 데이터 검색");
        System.out.println("3. 데이터 삭제");
        System.out.println("4. 모든 데이터 보기");
        System.out.println("5. 프로그램 종료");
        System.out.print("선택 : ");
    }

    public static void inputMenu()
    {
        System.out.println("1. 일반, 2. 대학, 3. 회사");
    }
    public static void inputMenuChoice()
    {
        int choice=0;
        choice = sc.nextInt();
        sc.nextLine();//버퍼에 엔터 제
        switch(choice)
        {
        case GENERAL:
            inputGeneralPhoneInfo();
            break;
        case UNIVERSITY:
            inputUniversityPhoneInfo();
            break;
        case COMPANY:
            inputCompanyPhoneInfo();
            break;
        default:
            System.out.println("잘못 입력 하셨습니다.");
        }
    }

    public static void inputGeneralPhoneInfo()
    {
        String name, phone;

        System.out.println("데이터 입력을 시작합니다.");
        System.out.print("이름 : ");
        name = sc.nextLine();
        System.out.print("전화번호 : ");
        phone = sc.nextLine();
        System.out.println("데이터 입력이 완료되었습니다.");
        pb.inputPhoneInfo( new PhoneInfo(name, phone) );//새로운 객체에 값을 저장해 호출
    }

    public static void inputUniversityPhoneInfo()
    {
        String name, phone, major;
        int year;

        System.out.println("데이터 입력을 시작합니다.");
        System.out.print("이름 : ");
        name = sc.nextLine();
        System.out.print("전화번호 : ");
        phone = sc.nextLine();
        System.out.print("전공 : ");
        major = sc.nextLine();
        System.out.print("학년 : ");
        year = sc.nextInt();
        sc.nextLine();
        System.out.println("데이터 입력이 완료되었습니다.");
        pb.inputPhoneInfo( new PhoneUnivInfo(name, phone, major, year) );
    }

    public static void inputCompanyPhoneInfo()
    {
        String name, phone, company;

        System.out.println("데이터 입력을 시작합니다.");
        System.out.print("이름 : ");
        name = sc.nextLine();
        System.out.print("전화번호 : ");
        phone = sc.nextLine();
        System.out.print("회사 : ");
        company = sc.nextLine();
        System.out.println("데이터 입력이 완료되었습니다.");
        pb.inputPhoneInfo( new PhoneCompanyInfo(name, phone, company) );
    }

    public static void searchPhoneInfo()
    {
        String name;
        System.out.println("데이터 검색을 시작합니다.");
        System.out.println("검색하시고자 하는 이름을 입력하세요.");
        name = sc.nextLine();
        pb.searchPhoneInfo(name);            
    }
    public static void deletePhoneInfo()
    {
        String name;
        int result=0, answer=0;
        System.out.println("검색하시고자 하는 이름을 입력하세요.");        
        name = sc.nextLine();        
        result = pb.search(name);
        if(result != -1)
        {
            System.out.println("정말 삭제하시겠습니까? 1. Yes 2. No");
            answer = sc.nextInt();
            sc.nextLine();
            switch(answer)
            {
            case YES:
                pb.deletePhoneInfo(result);
                break;
            case NO:
                break;
            default:
                System.out.println("잘못 누르셨습니다.");
            }
        }
        else
            System.out.println("삭제하시려는 데이터가 없습니다.");
    }
    public static void showAllPhoneInfo()
    {
        pb.showAllPhoneInfo();
    }
}

class PhoneMain
{        
    public static void main(String[] args)
    {
        int choice=0;

        while(true)
        {
            PhoneUI.mainMenu();
            choice = PhoneUI.sc.nextInt();
            PhoneUI.sc.nextLine();
            switch(choice)
            {
                case PhoneUI.INPUT_PHONEINFO:
                    PhoneUI.inputMenu();
                    PhoneUI.inputMenuChoice();
                    break;
                case PhoneUI.SEARCH_PHONEINFO:
                    PhoneUI.searchPhoneInfo();
                    break;
                case PhoneUI.DELETE_PHONEINFO:
                    PhoneUI.deletePhoneInfo();
                    break;
                case PhoneUI.SHOW_ALL_PHONEINFO:
                    PhoneUI.showAllPhoneInfo();
                    break;
                case PhoneUI.PROGRAM_QUIT:
                    return;
                default:
                    System.out.println("잘못 선택하셨습니다.");
            }        
        }
    }
}

.compareTo(); int

두 객체의 크기를 비교한다. > 정렬 / 순서 비교에 사용

비교 대상 객체가 null이라면 NullPointerException발생

  • 숫자형 데이터(Integer, Double등) : 일반적으로는 숫자의 크기를 비교한다.

    • 숫자 값이 크면 양수, 작으면 음수
  • 문자열(String) : 사전순으로 비교한다.

    • 알파벳 순서에 따라 크기를 결정 > 대소문자 구분

      • 양수 : 호출한 문자열이 인자로 전달된 문자열보다 사전적으로 뒤에 위치함

      • 음수 : 호출한 문자열이 인자로 전달된 문자열보다 사전적으로 앞에 위치함

      • 0 : 문자열이 사전적으로 동일

  • 사용자 정의 클래스 : 해당 클래스에서 compareTo 메소드를 오버라이딩하여 비교 로직 정의 가능

.equals(); boolean

두 객체가 동들한지의 여부를 판단한다.(내용이 같은지)

  • 객체간의 내용이 동일한지 비교할 때 사용된다.

검색하는 방법을 이진검색으로 (기존에 있는건 순차검색)

  • search메소드를 수정

    내가 푼 코드
    public int search(String name) {
          int start = 0, end = cntOfPhone - 1;//인덱스는 0부터 시작하니까 
          Arrays.sort(pInfoArr);// 배열 정렬
          int mid = (start + end) / 2;
    
          while (start <= end) {
              String arr = pInfoArr[mid].getName();
    
              if (name.equals(arr)) {
                  return mid;
              }
              if (name.compareTo(arr) > 0) {//name이 arr보다 사전적으로 뒤에 위치한다. > 그래서 start를 mid+1로 
                  start = mid + 1;
              } else if (name.compareTo(arr) < 0) {//name이 arr 보다 사전적으로 앞에 위치한다 >그래서 start를 mid-1 
                  end = mid - 1;
              } else
                  return mid;
    
              mid = (start + end) / 2;
          }
          return -1;
      }
    • 선생님과는 지금 비교하는 위치가 다르기 떄문에 음수 양수가 반대인것
선생님 코드
int first = 0;
      int last = cntOfPhone-1;
      int mid = (first+last)/2;
      while(first<=last)
      {
          if(pInfoArr[mid].getName().compareTo(name) < 0) first = mid + 1;
          else if(pInfoArr[mid].getName().compareTo(name) > 0) last = mid - 1;
          else return mid;    
          mid = (first+last)/2;
      }
      return -1;                
  }
compareTo()메소드
String str1 = "apple";
String str2 = "banana";
  int result = str1.compareTo(str2);
  if (result < 0) {
  System.out.println("str1이 str2보다 작다.");
  } else if (result == 0) {
  System.out.println("str1과 str2는 같다.");
  } else {
  System.out.println("str1이 str2보다 크다.");
  }
  • compareTo를 이용해 배열에 스펠링 순서대로 들어가는것

    • str1이 "apple"이고 str2가 "banana"이기 때문에, result 값은 양수가 될 것이며

    • "str1이 str2보다 크다."가 출력되어야한다..

    • compareTo 메소드는 호출하는 객체가 매개변수로 전달된 객체보다 크면 양수를 반환한다.


배열 관련 함수

import static java.lang.Math.*;
import java.util.Arrays;

class Test
{
    public static void main(String[] args)
    {
        System.out.println(PI);
        int[] numArr1 = new int[10];
        int[] numArr2 = new int[10];
        Arrays.fill(numArr1, 5);//배열 전체에 넣을 값 
        System.out.println(Arrays.toString(numArr1));//배열을 String형태로 출력 
        System.arraycopy(numArr1,0, numArr2,0, 5);//배열에서 배열로 복사 
        System.out.println(Arrays.toString(numArr2));        
    }
}

3.141592653589793
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5][5, 5, 5, 5, 5, 0, 0, 0, 0, 0] < 출력 결과


가변인자(Varargs)

public static int example(Object ... values)

위와 같이 ... 이 삽입된 메소드의 매개변수 선언을 '가변 인자 선언'이라고 한다.

매개변수의 가변 인자 선언과 호출

가변 인자 선언을 하면 전달되는 인자의 수에 제한을 두지 않고 동적으로 인자를 전달받을 수 있다. > 오버로딩과 유사

  • 가변인자를 사용하면 메소드를 호출할 때 필요한 만큼의 인자를 전달해 코드를 간결하게 유지할 수 있다.

    • 오버로딩 처럼 고정된 수의 매개변수를 사용하지 않고, 가변적으로 인자를 받을 수 있다.
public class test {

    public static void showAll(String ... vargs) {
        System.out.println("길이: " + vargs.length);
//위 코드를 통해 알 수 있는 사실 >** vargs가 배열을 참조** > 따라서 length를 통해 길이를 확인할 수 o
        for(String str : vargs) {
            System.out.print(str + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {

        showAll("A");
        showAll("A", "B");
        showAll("A", "B", "C");
        showAll("A", "B", "C", "D");    
    }//위와 같이 메서드 호출이 이루어지면, 배열을 생성하여 전달되는 인자들("A", "B", "C")를 모두 담고, 그 배열이 매개변수 vargs에 전달
}

  • 따라서 메서드 내에서는 매개변수 vargs를 배열로 이해하고 코드를 작성할 수 있다.

    • 가변인자는 배열로 처리되므로 for each문을 이용해 출력 가능하다.
가변인자 사용법과 주의사항
  1. 사용법

    • 가변인자 선언: 자료형...변수명

    • 메서드 선언시 가변인자는 매개변수 중에서 제일 마지막에 선언해야 한다.

      • != 컴파일 에러 발생 : 컴파일러가 가변인자를 구분할 수 없다.

        • public void printf(Object ... args, String name){} > 오
    • 각 메서드는 하나의 가변인자만 가질 수 있다.(여러 개 선언 x)

  • 만일 여러 문자열을 하나로 결합하여 반환하는 concatenate메서드를 작성한다면, 오버로딩처럼 매개변수의 개수를 다르게 해 여러개의 메서드를 작성해야한다. > 이럴 때 가변인자를 사용하면 간단히 대체가능하다.

    • String concatenate(String...str){} > 이 메서드를 호출할 때는 인자의 개수를 가변적으로 할 수 있다. > 인자가 없어도되고, 배열도 인자가 될 수 있다.
  1. 주의사항

    • 가변인자가 선언된 메서드를 호출할 때마다배열이 새로 생성된다. > 비효율적이므로 필요한 경우에만 가변인자를 사용

    • 매개변수의 타입을 배열로 하면, 반드시 인자를 지정해 줘야한다. 가변인자는 인자를 생략할 수 있기때문에 배열과의 차이점이 발생한다.

가변인자 와 오버로딩 예시
  1. 오버로딩
class Organize {// 오버로딩 사용
    public static int add(int num1) {
        return num1;
    }

    public static int add(int num1, int num2) {
        return num1 + num2;
    }

    public static int add(int num1, int num2, int num3) {
        return num1 + num2 + num3;
    }

    public static void main(String[] arg) {
        System.out.println(add(5)); // 5
        System.out.println(add(5, 10)); // 15
        System.out.println(add(5, 10, 15)); // 30
    }
}
  1. 가변인자
class Organize {
    public static int add(int... num) {// 가변인자 사용하는 방식
        int sum = 0;
        for (int a : num) {
            sum += a;
        }
        return sum;
    }

    public static void main(String[] args) {

        System.out.println(add(5)); // 5
        System.out.println(add(5, 10)); // 15
        System.out.println(add(5, 10, 15)); // 30
    }
}
  • 가변인자는 내부적으로 배열로 저장되는것이기 때문에 for each문을 이용해 빼내는 것이다.
  • 가능하면 가변인자를 사용한 메서드는 오버로딩하지 않는것이 좋다.

인터페이스 개념 다시 정리

클래스들이 필수로 구현해야 하는 추상 자료형 > 객체의 사용방법을 가이드라인 하는것

다른 클래스를 작성할 때 기본이 되는 틀을 제공 > 다른 클래스 사이의 중간 매개 역할까지 담당하는 일종의 추상 클래스(생성자, 필드, 일반 메소드 포함)

  • 인터페이스 : 추상 메서드와 상수로만 구성> 구현된 코드 x 이므로 인터페이스로 인스턴스도 사용 x

인터페이스 정의

  • 인터페이스를 작성하는 것은 추상 클래스를 작성하는 것과 같다고 보면 된다. (추상 메서드 집합)

  • 인터페이스도 필드를 선언할 수 있지만 변수가 아닌 상수(final)로서만 정의할 수 있다. 

  • public static final 과 public abstract 제어자는 생략이 가능하다.

    • 기본적으로 가지는 키워드라 굳이 선언할 필요 x

    • 생략된 제어자는 컴파일 시에 컴파일러가 자동으로 추가해 준다.

TIP

인터페이스도 따지고 보면 상속이지만 extends 키워드 대신 implements 라는 '구현' 이라는 키워드를 사용하는 이유는, 상속은 클래스간의 부모 - 자식 관계를 연관 시키는데 의미가 중점 된다면, 구현은 클래스를 확장 시켜 다양히 이용하는데 중점이 되기 때문이다

TIP

인터페이스를 구현받고 추상 메서드를 구체적으로 구현할때 접근제어자 설정에 주의해야 한다.
기본적으로 메서드를 오버라이딩(overriding) 할때는 부모의 메서드 보다 넓은 범위의 접근제어자를 지정해야 한다는 규칙이 존재한다. 따라서 인터페이스의 추상 메소드는 기본적으로 pubic abstract 가 생략된 상태이기 때문에 반드시 자식클래스의 메서드 구현부에서는 제어자를 public으로 설정해 주어야 한다.

인터페이스 특징

1. 다중 상속 가능
  • 인터페이스는 껍데기만 존재해 클래스 상속시 발생했던 모호함 x > 다중 상속 가능
2. 추상 메서드와 상수만 사용 가능
  • 인터페이스에는 구현 소스를 생성할 수 없다 > 상수와 추상 메서드만 가질 수 있다.
3. 생성자 사용 불가
  • 인터페이스 객체가 아니므로 생성자를 사용할 수 x
4. 메서드 오버라이딩 필수
  • 자식 클래스는 부모 인터페이스의 추상 메서드를 모두 오버라이딩 해야한다.

인터페이스의 선언

  • 접근제어자와 함께 interface 키워드를 사용한다.

  • 자바에서 인터페이스는 다음과 같이 선언한다.

    접근제어자 interface 인터페이스이름{
      public static final 타입 상수이름 =;
    ...
      public absract 메소드이름(매개변수 목록);
    ...
  • 단 클래스와는 달리 인터페이스의 모든 필드는 public static final 이어야 하며, 모든 메소드는 public abstract 여야한다. > 생략된 제어자는 컴파일 시 자바 컴파일러가 자동으로 추가

인터페이스의 구현

  • 인터페이스는 추상 클래스와 마찬가지로 자신이 직접 인스턴스를 생성할 수 x

    • 인터페이스가 포함하고 있는 추상 메소드를 구현해 줄 클래스를 작성해야함

    • 인터페이스를 구현하는 클래스에서 구현하는 메소드에 publie을 명시해야한다.

      • 인터페이스의 메소드는 기본적으로 public이기 때문이다. > 인터페이스는 외부에서 해당 메소드를 호출 할 수 있어야하므로, 메소드는 public이여야한다.

      • 구현하는 클래스에서도 해당 메소드를 오버라이딩 할때도 public 을 명시해야한다.

        • 인터페이스 클래스에서 메소드에 public 을 명시하지않으면 컴파일러가 자동으로 public 으로 간주한다.
  • 인터페이스는 아래와 같은 문법으로 구현

  • class 클래스이름 implements 인터페이스이름 
  • 아래와 같이 상속과 구현을 동시에 할 수 있다.

    • class 클래스이름 extend 상위클래스이름 implements 인터페이스이름{...}

      인터페이스는 인터페이스로만 상속을 받을 수 있으며, 여러 인터페이스를 상속받을 수 있다.

package com.test.memo;

interface Animal {
    public abstract void cry();
}

interface Pet {
    public abstract void play();
}

class Cat implements Animal, Pet {
    public void cry() {
        System.out.println("냐오옹");
    }

    public void play() {
        System.out.println("쥐 잡기 놀이하자옹~");
    }
}

class Dog implements Animal, Pet {
    public void cry() {
        System.out.println("멍멍!");
    }

    public void play() {
        System.out.println("산책가자~!");
    }
}

class Practice3 {
    public static void main(String[] args) {
        Cat c = new Cat();
        Dog d = new Dog();

        c.cry();//냐오옹 출력 
        c.play();//쥐 잡기~ 출력 
        d.cry();//멍멍 출력 
        d.play();
    }
}

결국 인터페이스는 일종의 추상 클래스로, 메소드의 선언만을 가지고 있고, 메소드 정의는 없다.

변수를 가질 수 있지만, 해당 변수는 상수로 취급되며, 인터페이스를 구현하는 클래스에서는 이 변수를 변경할 수 없다.

  • 상수인 변수도 아래와 같이 클래스이름으로 바로 호출 가능
interface MyInterface {
    int MY_CONSTANT = 42; // 상수

    void myMethod();
}

public class Main {
    public static void main(String[] args) {
        System.out.println(MyInterface.MY_CONSTANT); // 인터페이스를 통해 상수 호출
    }
}

인터페이스 구현 메소드

본래 인터페이스의 메서드는 몸통(구현체)을 가질 수 없지만 현재 java8 이후에는 default메서드와, static메서드를 통해 추상 클래스처럼 구현 메소드를 정의할 수 있게됐다.

  1. 상수(Constants): public static final이 기본적으로 적용된다.

    • 인터페이스 이름을 통해 바로 호출 가능
  2. 추상 메소드(Abstract Mehod): 메소드의 시그니처만 선언되고 본문이 없는 메소드이다.public abstract가 기본적으로 적용된다.

    • 인터페이스를 구현하는 하위클래스에서 반드시 몸통을 만들어줘야한다.
  3. 디폴트 메소드(Default Method): 인터페이스에 기본 구현이 포함된 메소드이다. default 키워드가 사용된다.접근제어자가 public 이며 생략 가능하다.

    • 인터페이스를 구현하는 클래스에서 따로 default메소드를 구현하지 않아도 되고, 기본구현이 자동으로 제공된다.
  4. 정적 메소드(Static Method): 인터페이스의 클래스들이 해당 메소드를 공유할 수 있다. static키워드가 사용된다.

    • 클래스가 인터페이스의 정적 메소드를 호출할 때 인터페이스 이름을 통해 호출 가능하다.
Default Method
  • 자식 클래스에서 default 메소드를 오버라이딩 하여 재정의가 가능하다
  • 주의해야 할 점은 인터페이스는 Object클래스를 상속받지 않기 때문에, Object 클래스가 제공하는 기능(equals)등은 기본 메소드로 제공되지 않는다.
  • 인터페이스의 default 메소드를 호출하기 위해선 객체의 타입이 반드시 인터페이스 타입으로 업캐스팅 해주어야한다.
  1. 다중 인터페이스들 간의 디폴트 메서드 충돌

    • 애초에 똑같은 디폴트 메서드를 가진 두 인터페이스를 하나의 클래스에 구현하고 아무런 조치를 취하지 않으면 컴파일 자체가 되지 않는다.

    • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 하여 하나로 통합한다.

interface A1 {
    public void styleA();

    // 메소드 시그니처가 같은 디폴트 메서드
    default public void styleSame() {
        System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
    }
}

interface B1 {
    public void styleB();

    // 메소드 시그니처가 같은 디폴트 메서드
    default public void styleSame() {
        System.out.println("B1 인터페이스의 디폴트 메서드 입니다.");
    }
}

class MultiInterface implements A1, B1 {
    @Override
    public void styleA() {
    }

    @Override
    public void styleB() {
    }

    // 두 인터페이스 디폴트 메서드중 A1 인터페이스의 디폴트 메서드를 오버라이딩 하여 구현
    default public void styleSame() {
        System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        MultiInterface m1 = new MultiInterface();
        m1.styleSame(); // "A1 인터페이스의 디폴트 메서드 입니다."
    }
}
  1. 인터페이스의 디폴트 메서드와 부모 클래스 메서드 간의 충돌

    • 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다. - 첫 번째 예시

    • 만일 인터페이스 쪽의 디폴트 메서드를 사용할 필요가 있다면, 필요한 쪽의 메서드와 같은 내용으로 그냥 오버라이딩 해버리면 된다. - 두 번째 예시

interface A1 {
    public void styleA();

    // C1 클래스와 메소드 시그니처가 같은 디폴트 메서드
    default public void styleSame() {
        System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
    }
}

abstract class C1 {
    // A1 인터페이스와 메소드 시그니처가 같은 인스턴스 메서드
    public void styleSame() {
        System.out.println("C1 클래스의 인스턴스 메서드 입니다.");
    }
}

// 메서드 시그니처가 같은 두 추상화들을 동시에 상속
class MultiClassInterface extends C1 implements A1 {
    @Override
    public void styleA() {
    }
}

public class Main {
    public static void main(String[] args) {
        MultiClassInterface m1 = new MultiClassInterface();
        m1.styleSame(); // "C1 클래스의 인스턴스 메서드 입니다." - 클래스의 메서드 시그니처가 우선되어 적용됨

        // 마찬가지로 인터페이스 타입으로 다운캐스팅 해도 클래스 인스턴스 메서드로 호출 됨
        ((A1) m1).styleSame(); // "C1 클래스의 인스턴스 메서드 입니다."
    }
}
  • 클래스의 메소드가 인터페이스의 디폴트 메소드보다 우선 순위를 가지게 된다. 클래스의 메소드와 동일한 시그니처를 가진 인터페이스의 디폴트 메소드는 무시된다.
interface A1 {
    public void styleA();

    // C1 클래스와 메소드 시그니처가 같은 디폴트 메서드
    default public void styleSame() {
        System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
    }
}

abstract class C1 {
    // A1 인터페이스와 메소드 시그니처가 같은 인스턴스 메서드
    public void styleSame() {
        System.out.println("C1 클래스의 인스턴스 메서드 메서드입니다."); // 메서드 시그니처가 같은 두 추상화들을 동시에 상속
    }
} 
class MultiClassInterface extends C1 implements A1 {
    @Override
    public void styleA() {
    }

    // 클래스의 인스턴스 메서드를 무시하고 인터페이스의 디폴트 메서드를 사용하기 위해 그대로 오버라이딩
    public void styleSame() {
        System.out.println("A1 인터페이스의 디폴트 메서드 입니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        MultiClassInterface m1 = new MultiClassInterface();
        m1.styleSame(); // "A1 인터페이스의 디폴트 메서드 입니다."
    }
}

class DefaultMethodTest {
    public static void main(String[] args) {
        Child c = new Child();
        c.method1();
        c.method2();
        MyInterface.staticMethod(); 
        MyInterface2.staticMethod();
    }
}

class Child extends Parent implements MyInterface, MyInterface2 {
    public void method1() {    
        System.out.println("method1() in Child"); // 오버라이딩
    }            
}

class Parent {
    public void method2() {    
        System.out.println("method2() in Parent");
    }
}

interface MyInterface {
    default void method1() { 
        System.out.println("method1() in MyInterface");
    }

    default void method2() { 
        System.out.println("method2() in MyInterface");
    }

    static  void staticMethod() { 
        System.out.println("staticMethod() in MyInterface");
    }
}

interface MyInterface2 {
    default void method1() { 
        System.out.println("method1() in MyInterface2");
    }

    static  void staticMethod() { 
        System.out.println("staticMethod() in MyInterface2");
    }

출력:
method1() in Child

method2() in Parent

staticMethod() in MyInterface

staticMethod() in MyInterface2

  • defalut 메소드가 이름이 같으므로 반드시 오버라이딩시킨다.
  • 클래스 메소드가 인터페이스 메소드보다 우선시된다.
Default 메소드의 super
  • 상위 클래스를 상속하고, 상위의 메소드를 오버라이딩하여 재정의하였을때, 만일 부모 메서드를 호출할 일이 생긴다면 super키워드를 통해 부모 메서드를 호출할 수 있다.

    • 이처럼 인터페이스도 디폴트 메서드를 구현한 클래스에서 오버라이딩 하였을때, super키워드를 통해 인터페이스의 원래의 디폴트 메서드를 호출할 수 있다.
interface IPrint{
    default void print(){
        System.out.println("인터페이스의 디폴트 메서드 입니다.");
    }
}

class MyClass implements IPrint {
    @Override
    public void print() {
        IPrint.super.print(); // 인터페이스의 super 메서드를 호출
        System.out.println("인터페이스의 디폴트 메서드를 오버라이딩한 메서드 입니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass cls = new MyClass();
        cls.print();
    }
}
  • 인터페이스의 super을 이용해 print메서드를 호출할 수 있다.

    • 출력은 인터페이스 print()메서드가 출력되고, 그 밑에 MyClass의 print()메서드가 실행된다.
static 메소드
  • 인스턴스 생성과 상관없이 인터페이스 타입으로 접근해 사용할 수 있는 메서드
  • 인터페이스 전용 static 메소드라 해서 특별한 것은 없다.
    • 일반 클래스의 static 메소드와 다를 바 없다. (똑같이 취급 하면 된다)
  • 인터페이스 이름을 통해 호출할 수 있다. Calculator.explain();
private 메소드

자바9 버전에 추가된 메서드

  • 인터페이스에 default, static 메소드가 생긴 이후, 이러한 메소드들의로직을 공통화하고 재사용하기 위해 생긴 메소드
  • private 메소드도 구현부를 가져야한다.
  • 단, private 메소드는인터페이스 내부에서만 돌아가는 코드이다. (인터페이스를 구현한 클래스에서 사용하거나 재정의 할 수 없음)
  • 따라서 인터페이스 내부에서 private 메소드를 호출할때,default 메소드 내부에서 호출해야 하며,
    만일 private static 키워드를 붙인 메소드는 static 메소드에서만 호출이 가능
    하다.

tip

어렵게 생각할 필요없이, 클래스에서도 private 접근제어자를 가진 메서드를 정의하였을때, 호출 메서드에서 private 내부 메서드를 호출하여 사용하는 식 이었던 것 처럼, 인터페이스도 어렵지 않게 똑같이 생각하면 된다.
단, 인터페이스는 클래스가 아니기 때문에 this 키워드를 사용할순 없다.

interface Calculator {
    public int plus(int i, int j);
    public int multiple(int i, int j);

    // private 메서드
    private void printf() {
        System.out.println("private 메서드는 default 내부에서만 호출이 가능합니다.");
    }

    // private 스태틱 메서드
    private static void printfStatic() {
        System.out.println("private static 메서드는 static 메서드 내부에서만 호출이 가능합니다.");
    }

    // 디폴트 메서드
    default void callPrivate() {
        printf(); // private 메서드 호출
    }

    // 스태틱 메서드
    static void callPrivateStatic() {
        printfStatic(); // private 스태틱 메서드 호출
    }
}

class MyCalculator implements Calculator {
    @Override
    public int plus(int i, int j) {  return i + j; }
    @Override
    public int multiple(int i, int j) { return i * j; }
}

public class Main {
    public static void main(String[] args){
        // 인터페이스 디폴트 메서드를 통한 private 메서드 호출
        Calculator c = new MyCalculator(); // 인터페이스 타입으로 업캐스팅
        c.callPrivate(); // "private 메서드는 default 내부에서만 호출이 가능합니다."

        // 인터페이스 스태틱 메서드를 통한 private static 메서드 호출
        Calculator.callPrivateStatic(); // "private static 메서드는 static 메서드 내부에서만 호출이 가능합니다."
    }
}
  • 인터페이스의 상수는 private으로 만들 수 없다. > 서로간의 약속

    • 인터페이스에 선언하는 필드들은 자동으로 public static final이 된다.

인터페이스 사용 이유

프레임워크 사용을 위해서 > 스프링 > 인터페이스 자주 사용

  • 추상 클래스를 통해 객체들 간의 네이밍을 통일할 수 있고 이로 인해 소스의 가독성과 유지보수가 향상된다.
  • 확장에는 열려있고 변경에는 닫혀있는 객체 간 결합도(코드 종속성)를 낮춘 유연한 방식의 개발이 가능하다.

인터페이스의 다형성

  • 부모 클래스 타입으로 자식 클래스 타입을 포함시킬 수 있다는 다형성의 법칙도 인터페이스에 고대로 적용이 가능하다.

    • 클래스가 여러 개의 인터페이스를 구현하게 되면, 결과적으로 변수의 타입으로도 다양하게 쓰일 수 있다는것을 의미하게 된다.

      • 인터페이스 타입으로 변수를 선언하게 되면 사용하는 입장에서는 뒤에 오는 모든 객체는 간단히 인터페이스만 구현한 객체이면 되개 때문에 시스템이 더 유연해진다.
interface Keyboard { }

class Logitec_Keyboard implements Keyboard { }

class Samsung_Keyboard implements Keyboard { }

class Apple_Keyboard implements Keyboard { }

public class Main {
    public static void main(String[] args) {
        // 인터페이스 타입 배열로 여러가지 클래스들을 한번에 타입 묶음을 할 수 있다.
        Keyboard[] k = {
                new Logitec_Keyboard(),
                new Samsung_Keyboard(),
                new Apple_Keyboard(),
        };
    }
}
  • 객체는 클래스가 아닌 인터페이스로 참조하라 라는 의미로 확장할 수 있다. 적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하면 좋다.

    1. 객체는 인터페이스를 사용해 참조하라

    2. 적당한 인터페이스가 있다면 매개변수 뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라

    3. 객체의 실제 클래스를 사용할 상황은 '오직' 생성자로 생성할 때 뿐이다.

    4. 매개변수 타입으로는 클래스보다는 인터페이스를 활용하라

마커 인터페이스

일반적인 인터페이스와 동일하지만 사실상 아무 메소드도 선언하지 않은 빈 껍데기 인터페이스를 의미

  • 인터페이스를 자유롭게 다중 상속이 가능하다는 점 > 객체의 타입과 관련된 정보만을 제공

    • 컴파일러와 JVM은 이 마커 인터페이스를 통해 객체에 대한 추가적인 정보를 얻을 수 있다.
class Animal {//새끼를 낳는 동물인지 알을 낳는 동물인지 구분하기 위해 일이 instanceof 연산자 사
    public static void born(Animal a) {
        if(a instanceof Lion) {
            System.out.println("새끼를 낳았습니다.");
        } else if(a instanceof Chicken) {
            System.out.println("알을 낳았습니다.");
        } else if(a instanceof Snake) {
            System.out.println("알을 낳았습니다.");
        }
        // ...
    }
}

class Lion extends Animal { }
class Chicken extends Animal { }
class Snake extends Animal { }Copy
  • 이러한 방식은 자식 클래스 갯수가 많으면 많을수록 코드가 난잡해지고 길어진다는 단점이 있다.

    • 따라서 아무런 내용이 없는 빈 껍데기 인터페이스를 선언하고 적절한 클래스에 implements 시킴으로써 단순한 타입 체크용으로 사용가능하다. (아래와같이)
// 새끼를 낮을 수 있다는 표식 역할을 해주는 마커 인터페이스
interface Breedable {}

class Animal {
    public static void born(Animal a) {
        if(a instanceof Breedable) {//비교해서 Breedable을 구현했는지 확인해 그에따라 출력이 나뉜
            System.out.println("새끼를 낳았습니다.");
        } else {
            System.out.println("알을 낳았습니다.");
        }
    }
}

class Lion extends Animal implements Breedable { }
class Chicken extends Animal { }
class Snake extends Animal { }
  • 마커 인터페이스를 사용하여 특정 기능이나 특징을 가진 클래스를 식별할 수 있다.

    • 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스 라고한다.

인터페이스 문제

abstract class PersonalNumberStorage
{
    public abstract void addPersonalInfo(String perNum, String name);
    public abstract String searchName(String perNum);
}

class PersonalNumInfo
{
    String name;
    String number;

    PersonalNumInfo(String name, String number)
    {
        this.name=name;
        this.number=number;
    }

    String getName(){return name;}
    String getNumber(){return number;}
}

class PersonalNumberStorageImpl extends PersonalNumberStorage
  • 위를 인터페이스로 변환시키시오
interface  PersonalNumberStorage {

     void addPersonalInfo(String perNum, String name);
    public String searchName(String perNum);
    //위 두개 다 public abstract void ~로 선언되는것
}

class PersonalNumInfo {
    String name;
    String number;

    PersonalNumInfo(String name, String number) {
        this.name = name;
        this.number = number;
    }

    String getName() {
        return name;
    }

    String getNumber() {
        return number;
    }
}

class PersonalNumberStorageImpl implements PersonalNumberStorage {
    PersonalNumInfo[] perArr;
    int numOfPerInfo;
  • abstract를 interface로 변환시키고 상속받는 클래스를 implements로 변환시키면 끝

  • 뭐 오버라이딩 시켜야된다고 했는데 몰랑..


println구현 문제

System.out.println 오버로딩 된 메소드 중에 public void println(Object x)가 없다고 가정하고 폴더에 있는 ClassPrinter에 있는 class에 print 메소드를 만드시오.
[실행결과][x:1, y:2]
[x:5, y:9]

class ClassPrinter 
{
}

class Point
{
    private int xPos, yPos;

    Point(int x, int y)
    {
        xPos=x;
        yPos=y;
    }

    public String toString()
    {
        String posInfo="[x:"+xPos + ", y:"+yPos+"]";
        return posInfo;
    }
}

class ImplObjectPrintln
{
    public static void main(String[] args)
    {
        Point pos1=new Point(1, 2);
        Point pos2=new Point(5, 9);

        ClassPrinter.print(pos1);
        ClassPrinter.print(pos2);
    }
}
내가 푼 코드
class ClassPrinter {

    public static void print(Point num) {
        System.out.println(num);//생각해보니까 저게 어떻게 toString메서드 양식에 맞게 출력된거지..
    }//자동으로 toString이 호출됐
}
  • 나는 그냥 받아오는 자료형이 Point여서 Point로 줬는데..이게 됐네..?
  • 근데 문제에서 public void println(Object x)라고 했으니까 내가 잘못한건가 흠
선생님 코드
class ClassPrinter 
{
    public static void print(Object obj)
    {
        System.out.println(obj.toString());
    }
}
  • 제일 최상위 클래스인 Object를 이용해서 값을 받아 출력한다.

println구현 응용 문제

9번 문제에서 정의하고 있는 print 메소드에 다음의 기능을 추가하시오.
"인터페이스 UpperCasePrintable을 구현하는 클래스의 인스턴스가 print 메소드의 인자로 전달되면 문자열을 전부 대문자로 출력한다."
[실행결과][X POS:1, Y POS:1]
[x pos:2, y pos:2][X POS:3, Y POS:3]
[x pos:4, y pos:4]

선생님 코드
package com.test.memo;

interface UpperCasePrintable {

}

class ClassPrinter {
    public static void print(Object obj) {
        String org = obj.toString();
        if (obj instanceof UpperCasePrintable) {
            org = org.toUpperCase();
        }
        System.out.println(org);
    }
}

class PointOne implements UpperCasePrintable {
    private int xPos, yPos;

    PointOne(int x, int y) {
        xPos = x;
        yPos = y;
    }

    public String toString() {
        String posInfo = "[x pos:" + xPos + ", yPos:" + yPos + "]";
        return posInfo;
    }
}

class PointTwo {
    private int xPos, yPos;

    public PointTwo(int x, int y) {
        xPos = x;
        yPos = y;
    }

    public String toString() {
        String posInfo = "[x pos:" + xPos + ", yPos:" + yPos + "]";
        return posInfo;
    }
}

class Organize {
    public static void main(String[] args) {
        PointOne pos1 = new PointOne(1, 1);
        PointTwo pos2 = new PointTwo(2, 2);
        PointOne pos3 = new PointOne(3, 3);
        PointTwo pos4 = new PointTwo(4, 4);

        ClassPrinter.print(pos1);
        ClassPrinter.print(pos2);
        ClassPrinter.print(pos3);
        ClassPrinter.print(pos4);
    }
}
  • PointOne 과 PointTwo 의 클래스를 각각 만들어준다(기능은 같다.)

  • PointOne은 implements 로 interface를구현하고있기 때문에 메인 클래스에서 객체를 각각 만든후 호출하면

    • ClassPrinter 클래스를 호출해 toString()을 호출하고 해당 리턴값이 instaceof와 비교한다.

      • 참이면 toUpperCase()로 대문자 변경해주는 것이고

      • 거짓이면 소문자 그대로 출력한다.

    • 그러니까 Upper인터페이스를 구현받는 클래스와 그렇지 않은 클래스를 구별해서(instaceof) 출력하는것이다.

  • UpperCasePrintable 인터페이스가 갖는 의미는 무엇인가?

    "다른 클래스와의 구별을 위한 특별한 표시의 목적으로 사용되었다."

  • 이렇듯 인터페이스는 클래스의 정의에, 약속된 형태의 특별한 표시를 위한 용도로도 사용이 된다. 그리고
    이러한 경우에는 위 예제에서 보이듯이 아무것도 채워지지 않은 형태로 인터페이스가 정의되기도 한다.

    • 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스(marker interface)라 한다.

상속과 구현을 동시에 받을때는 class IPTC extends TV implements Computer 형태이다

상속이 먼저온다.

구현을 통해 상속과 같은 효과
package com.test.memo;

interface TV {
    public void onTV();//public abstract void onTV 인것

}

class TVImpl {
    public void onTV() {//implements 키워드가 없지만, onTV메소드의 시그니처가 인터페이스의 메소드와 동일하므로 자동으로 해당 인터페이스를 구현한것으로 간주한다. 
        System.out.println("영상 출력 중");
    }
}

interface Computer {
    public void dataReceive();
}

class ComputerImpl {
    public void dataReceive() {
        System.out.println("영상 데이터 수신 중");
    }
}

class IPTV implements TV, Computer {
    ComputerImpl comp = new ComputerImpl();
    TVImpl tv = new TVImpl();

    public void dataReceive() {
        comp.dataReceive();
    }

    public void onTV() {
        tv.onTV();
    }

    public void powerOn() {
        dataReceive();
        onTV();
    }
}

class Organize {
    public static void main(String[] args) {
        IPTV iptv = new IPTV();
        iptv.powerOn();

        TV tv = iptv;
        Computer comp = iptv;
    }
}//영상데이터 수신 중 
//영상 출력 중
//출력

Outer Class(외부 클래스)

  • 가장 일반적인 클래스로, 다른 클래스 안에 정의되지 않은 독립적인 클래스를 의미한다. > 모든 객체의 기본이 되는 클래

  • 외부 클래스는 다른 클래스의 멤버 변수 및 메소드로 사용될 수 있다.

  • 내부 클래스를 포함한 클래스다.


Inner Class(내부 클래스)

Outer 클래스 내부에 선언되어 있는 클래스로, Outer클래스의 멤버 변수와 메서드에 접근할 수 있다.

주로 Outer클래스의 기능을 확장하거나 캡슐화할 때 사용된다.

  • 하나의 클래스 내부 안에 정의된 또 다 클래스로, 외부 클래스의 멤버로 사용된다.

  • 내부 클래스는 외부 클래스의 멤버 변수 및 메소드에 접근할 수 있다.

public class OuterClass {
    class InnerClass {
        // 내부 클래스의 멤버들 > 인스턴스 , 스태틱, 지
    }
}
class Outer{
    class InstanceInner{..}//인스턴스 클래스
    static class StaticInner {..}//스태틱 클래스

    void method1(){
        class LocalInner{..}//지역 클래스 
    }
}//클래스 멤버 접근 제어자 특성과 비슷 
Inner 클래스의 알고리즘
@Test
void innerClass() {
    OuterClass outerObject = new OuterClass("MangKyu");//외부 클래스 객체 생성 
    OuterClass.InnerClass innerObject = outerObject.new InnerClass();//위에서 생성한 객체로 내부 클래스 인스턴스 생
    innerObject.print();
}

class OuterClass {

    public OuterClass(String name) {
        this.name = name;
    }

    private String name;

    // inner class 라고 부름
    class InnerClass {

        public void print() {
            System.out.println(name);
        }

    }
}

Inner 클래스의 인스턴스는 외부 클래스의 인스턴스 내에 존재하기 때문에 외부 클래스의 인스턴스와 연관이 있다.

  • 위와 같이 외부 클래스 객체로부터 생성하고 사용할 수 있으며, 외부 클래스 객체의 메서드 및 필드에 직접 접근할 수 있다.

Inner Class의 장점

  1. 내부 클래스에서 외부 클래스의 멤버에 손쉽게 접근할 수 있다.

  2. 서로 관련있는 클래스를 논리적으로 묶어서 표현함으로써, 코드의 캡슐화를 증가시킨다.

  3. 외부에서는 내브 클래스에 접근할 수 없으므로, 코드의 복잡성은 줄일 수 있다.

    • 하나의 클래스에서 어떤 공통적인 작업을 수행하는 클래스가 필요한데 다른 클래스에서는 그 클래스가 전혀 필요 없을 때 사용

Inner Class의 종류

멤버 변수도 선언되는 위치나 접근 제어자에 따라 역할과 이름이 달라지듯, 내부 클래스도 선언된 위치, static 키워드의 유무등에 따라 구분된다.

1. instance class

로컬클래스와 인스턴스 클래스는 사용되는 위치와 생명 주기에 차이가 있다.

인스턴스 클래스는 외부 클래스의 인스턴스가 생성된 후에 해단 인스턴스의 메소드에서 사용할 수 있다.

  • 외부 클래스의 멤버변수 위치에 선언

  • 외부 클래스의 인스턴스 멤버와 같이 취급 <> 로컬 클래스와 차이점(=로컬클래스는 외부 클래스 메소드내에서만 사용 가능)

  • 인스턴스 클래스 내부에는 instance 멤버만 선언할 수 있다. (static멤버는 선언 불가)

  • 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업을 위해 선언 > 외부 클래스의 인스턴스가 존재하는 동안 계속해서 사용 가

class PocketBall {
    // 인스턴스 변수
    int size = 100;
    int price = 5000;

    // 인스턴스 내부 클래스
    class PocketMonster {
        String name = "이상해씨";
        int level = 10;

        // static int cost = 100; - 에러! 인스턴스 내부 클래스에는 static 변수를 선언할 수 없다.
        static final int cost = 100; // final static은 상수이므로 허용

        public void getPoketMember() {
            // 별다른 조치없이 외부 클래스 맴버 접근 가능
            System.out.println(size);
            System.out.println(price);

            // 내부 클래스 멤버
            System.out.println(name);
            System.out.println(level);
            System.out.println(cost);
        }
    }
}
public class Main {
    public static void main(String[] args) {

        PocketBall ball = new PocketBall(); // 먼저 외부 클래스를 인스턴스화 해주고
        PocketBall.PocketMonster poketmon = ball.new PocketMonster(); // 외부클래스.내부클래스 형식으로 내부클래스를 초기화 하여 사용할 수도 있다
        poketmon.getPoketMember();

        // 위의 단계를 한줄로 표현
        PocketBall.PocketMonster poketmon2 = new PocketBall().new PocketMonster();
    }
}
  • 외부 클래스를 인스턴스화하면

    • 외부 클래스의 코드가 메모리에 올라오게 되고, 이때 내부 클래스의 코드도 메모리에 올라오게 된다. > 이렇게 메모리에 코드를 올린 이후에야 내부 클래스의 인스턴스를 생성할 수 있다.

      • 내부 클래스는 다른 클래스에서 직접 사용하는 것보다 해당(외부)클래스에서만 사용하는 것이 일반적이므로 저렇게 인스턴스를 만드는경우는 드물다.
이름이 같은 외부 클래스 메서드 호출
// 외부 클래스
public class Main {

    public print(String txt) {
        System.out.println(txt);
    }

    // 내부 클래스
    class Sub {
        public print() {         
        }
    }
}
  • 외부-내부 클래스 관계는 상속관계가 아니기 때문에 다른 방법으로 외부 클래스의 멤버를 호출해야한다.

  • 이때 정규화된 this를 사용하면 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

    • 정규화된 this란 클래스명.this형태로 바깥 클래스의 이름을 명시하는 용법이다.
public class Main {

    public void print(String txt) {
        System.out.println(txt);
    }

    class Sub {
        public void print() {
            Main.this.print("외부 클래스 메소드 호출");
            System.out.println("내부 클래스 메소드 호출");

             }
           } 
}
public static void main(String[] args) {
    Main.Sub s = new Main().new Sub();
    s.print();
    /*      
    외부 클래스 메소드 호출
    내부 클래스 메소드 호출
    */
}

static내부 클래스에서는 정규화된 this문법을 사용할 수 없다.

인스턴스 클래스 컴파일
  • 내부 클래스는 클래스 안의 클래스 형태이기 때문에 컴파일 형태가 남다르다.

  • 컴파일 시 생성되는 클래스 파일명은 "외부클래스$내부클래스.class"형태이다.


2. static class

static 키워드가 붙은 내부 클래스

일반적인 static 필드 변수나 static 메서드와 똑같이 생각하면 x

  • static 클래스 내부에는 instance 멤버와 static 멤버 모두 선언 할 수 있다.

    • 외부 클래스의 정적 멤버에 접근 가능하다.

    • 따라서, 외부 클래스의 정적(static) 멤버에 접근하거나, 정적 메서드 내에서만 사용될 데이터를 처리할 때 선언하는게 일반적이다.

  • 일반적인 static 메서드와 동일하게 외부 클래스의 인스턴스 멤버에는 접근이 불가능하고, 정적(static)멤버(변수)에만 접근할 수 있다.

    • static 클래스 내부에서 인스턴스 변수를 선언하면 static클래스에 선언된 인스턴스멤버, 정적 멤버 둘다 접근 가능하나, 외부 클래스에 있는 인스턴스는 접근 불가능하다.

    • 내부 클래스 중 유일하게 static멤버를 가질 수 있다. (static 상수는 모든 내부 클래스에서 정의 가능)

    class PocketBall {
        int size = 100;
        static int price = 5000;
    
        // static 내부 클래스
        static class PocketMonster {
            static String name = "이상해씨";
            int level = 10;
    
            public static void getPoketMember() {
                // 외부 클래스 인스턴스 맴버 접근 불가능
                // System.out.println(size);
    
                // 외부 클래스 스태틱 멤버 접근 가능
                System.out.println(price);
    
                // 내부 클래스 멤버 스태틱 맴버만 접근 가능
                System.out.println(name);
                //System.out.println(level);
            }
    
                public void PoketMem() {
                    //System.out.println(size); //외부클래스에 있는 인스턴스 멤버 접근 x
    
                    System.out.println(price);//외부 클래스 스태틱 멤버 접근 가능
    
                    System.out.println(level);//내부 클래스 인스턴스 멤버 접근 가
                }
        }
    }
    public class Main {
        public static void main(String[] args) {
            // 스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다.
            PocketBall.PocketMonster poketmon = new PocketBall.PocketMonster();
            System.out.println(poketmon.level);
            System.out.println(PocketBall.PocketMonster.name);
    
            // 클래스.정적내부클래스.정적메소드()
            PocketBall.PocketMonster.getPoketMember();
        }
    }
  1. static 클래스 안에 static 메서드에서는 외부 클래스의 멤버변수와 내부 클래스의 멤버변수에 접근가능하다.
  2. static 클래스 안에 instance 메서드에서는 외부클래스의 멤버변수와 내부 클래스의 인스턴스 변수, 멤버변수 둘다 접근 가능하다.
static 클래스에 대한 오해

static이니까 메모리에 하나만 올라가는 인스턴스 라고 착각

  • static 클래스는, 키워드가 static이 들어갔을 뿐이지, 우리가 알던 static 멤버와는 전혀 다른 녀석
    • static 변수는 우리가 알던대로 메모리에서 한번만 생성되니까 반환되는 데이터는 같다.
  • 하지만 내부 static크래스는 일반 클래스처럼 초기화를 할때마다 다른 객체가 만들어짐을 볼 수 있다.
public class Main {
    // 스태틱 필드 변수
    static Integer num = new Integer(0);

    // 내부 인스턴스 클래스
    class InnerClass{
    }

    // 내부 스태틱 클래스
    static class InnerStaticClass{
    }

    public static void main(String[] args) {

        // 스태틱 필드 변수는 유일해서 서로 같다
        Integer num1 = Main.num;
        Integer num2 = Main.num;
        System.out.println(num1 == num2); // true


        // 생성된 내부 클래스 인스턴스는 서로 다르다
        Main.InnerClass inner1 = new Main().new InnerClass();
        Main.InnerClass inner2 = new Main().new InnerClass();
        System.out.println(inner1 == inner2); // false


        // 생성된 내부 스태틱 클래스 인스턴스는 서로 다르다
        Main.InnerStaticClass static1 = new InnerStaticClass();
        Main.InnerStaticClass static2 = new InnerStaticClass();
        System.out.println(static1 == static2); // false
    }
}
  • 복잡하게 생각할 필요없이 static 클래스는 static 이라고 해서 메모리에 한번만 로드되는 객체 개념이 아닌 것

    • 즉,내부 인스턴스 클래스처럼 외부 인스턴스를 먼저 선언하고 초기화해야하는 작업은 필요없다. > 내부 클래스의 인스턴스를 바로 생성할 수 있다는 차이점 존재
내부 클래스는 static클래스로 선언하자

만약 내부클래스에서 바깥 외부의 인스턴스를 사용할 일이 없다면 static 클래스로 선언해야한다. > 인텔리제이 같은 IDE에서 경

  • 왜냐면 static이 아닌 내부 인스턴스 클래스는 외부와 연결이 되어있어 외부 참조를 갖게되어 메모리를 먹고 여러 문제점을 일으킨다.
static클래스 컴파일
  • 위의 인스턴스 내부 클래스와 같다.

3. Local class

메소드 내부에 위치하는 클래스(지역 변수와 같은 성질을 지님)

  • 지역 변수처럼 해당 메서드 내부에서만 한정적으로 사용된다. (해당 메소드 실행 외에는 클래스 접근 및 사용 불가)

  • 접근제한자와 static을 붙일 수 없다.

    • 메소드 내부에서만 사용되므로 접근을 제한할 필요가 없고, 원래 메소드 내에는 static 멤버를 선언할 수 없기 때문이다. > 지역 클래스 내 final / effectively final인 변수만 선언 가능하다.
  • 메소드가 종료되면 로컬 클래스의 인스턴스도 소멸한다.

class Practice3 {
    public void outerMethod() {
        // final 또는 effectively final 변수
        final int localVar = 42;

        // 지역 클래스 정의
        class LocalClass {
            final int num1 = 0; //final 변수 
            int num2 = 0;//effectively final 변수  

            public void printLocalVar() {
                System.out.println("Local Variable: " + localVar);
            }
        }

        // 지역 클래스의 인스턴스 생성 및 메서드 호출
        LocalClass localInstance = new LocalClass();
        localInstance.printLocalVar();
    }

    public static void main(String[] args) {
        Practice3 example = new Practice3();
        example.outerMethod();
    }
}
  • effectively final 변수는 명시적으로 final 키워드를 사용하지 않았지만, 초기화 이후에 값이 변경되지 않은 변수를 의미한다.

    Local class 개념 간단하게 예제 들어서

public class LocalClassTest{
  int g = 10; //일반 지역변수

  //멤버 메소드
  void aaa(){
      int a = 50; //지역변수 - 다른 지역에서는 인식 불가

      //메소드 영역 안에 클래스 설계
      //Local class [지역 클래스, 내장 클래스, 내부 클래스]
      //설계된 지역 안에서만 인식 가능한 설계도(클래스)
      class Good{
          int n = 10;
          void show(){
              System.out.pritnln("Good..show);
          }
       }
       Good good = new Good();//객체 생성
       good.show();
    }
    void bbb(){
    g = 100;
    //a = 200; //error - 메소드 안에서 선언되는 변수는 상수 (값이 변경될 수 x)
    //Good good = new Good(); //error 인식 불가
  }
}
  • Local class 사용 이유

    1. 그 메소드가 실행중일때만 잠시 1회용처럼 사용하는 객체를 만들고 싶을 때

    2. 나중에 배울 익명 클래스를 사용할 때

    예제.

public class LocalClassExample {

    // 외부 클래스의 멤버 변수
    private int outerVariable = 10;

    public void outerMethod() {
        final int localVar = 20; // 메소드 내의 지역 변수 (final로 선언되어야 함)
                                //선언하지 않고, 밑에서 초기화 이후 값이 바뀌지 않으면 상수로 인식 
        // 지역 클래스 정의
        class LocalClass {
            // 지역 클래스에서 외부 클래스의 멤버 변수 및 메소드에 접근 가능
            void display() {
                System.out.println("Outer Variable: " + outerVariable);
                System.out.println("Local Variable: " + localVar);
            }
        }

        // 지역 클래스의 인스턴스 생성 및 메소드 호출 /  지역 클래스는 메인메서드에서 객체 생성 x
        LocalClass localObj = new LocalClass();
        localObj.display();
    }

    public static void main(String[] args) {
        LocalClassExample example = new LocalClassExample();//외부 클래스의 인스턴스 생성 
        example.outerMethod();//인스턴스 메서드 호출(인스턴스를 통해 호)
    }
}//Outer Variable:10
//Local Variable:20
class Practice3 {
    int size = 100;
    int price = 5000;

    public void pocketMethod() {
        int exp = 5000;// 메서드 내에 선언되었고 초기화 이후 변경 x > java는 해당 변수를 상수로 간주

        // 메소드 내에서 클래스를 선언
        class PocketMonster {
            String name = "이상해씨";
            int level = 10;// 메서드 불록 외부에서 클래스 멤버로 선언 > 선언된 지역 변수 중 초기화 이후 변경 x > 상수로 간주

            public void getPoketLevel() {
                System.out.println(level); // 인스턴스 변수 출력
                System.out.println(exp); // 메소드 지역 상수 출력
                System.out.println(name);
            }
        }

        // 메소드 내에서 클래스를 선언
        class PocketMonster2 {
            String name = "리자몽";
            int level = 50;

            public void getPoket2() {
                System.out.println(name);
                System.out.println(level);
                System.out.println(exp);
            }
        }

        PocketMonster pm = new PocketMonster();
        pm.getPoketLevel();
        System.out.println();
        PocketMonster2 p2 = new PocketMonster2();
        p2.getPoket2();
        System.out.println("메소드 실행 완료");
    }

    public static void main(String[] args) {
        Practice3 pc = new Practice3();//Practice3의 인스턴스 생성 
        pc.pocketMethod();
    }
}

만약 메서드가 static이라면 위처럼 인스턴스를 생성해서 인스턴스를 이용해 호출하지 않아도 됌(내가 알고있던 개념 그대로 괜히 헷갈리지말기)

class Practice3 {
    int size = 100;
    int price = 5000;

    public static void pocketMethod() {
        int exp = 5000;// 메서드 내에 선언되었고 초기화 이후 변경 x > java는 해당 변수를 상수로 간주

        // 메소드 내에서 클래스를 선언
        class PocketMonster {
            String name = "이상해씨";
            int level = 10;// 메서드 불록 외부에서 클래스 멤버로 선언 > 선언된 지역 변수 중 초기화 이후 변경 x > 상수로 간주

            public void getPoketLevel() {
                System.out.println(level); // 인스턴스 변수 출력
                System.out.println(exp); // 메소드 지역 상수 출력
                System.out.println(name);
            }
        }

        // 메소드 내에서 클래스를 선언
        class PocketMonster2 {
            String name = "리자몽";
            int level = 50;

            public void getPoket2() {
                System.out.println(name);
                System.out.println(level);
                System.out.println(exp);
            }
        }

        PocketMonster pm = new PocketMonster();
        pm.getPoketLevel();
        System.out.println();
        PocketMonster2 p2 = new PocketMonster2();
        p2.getPoket2();
        System.out.println("메소드 실행 완료");
    }

    public static void main(String[] args) {
        Practice3.pocketMethod();

//        Practice3 pc = new Practice3(); static메서드면 알고있던 방식으로 호출 
//        pc.pocketMethod();
    }
}
////10
//5000
//이상해씨
//
//리자몽
//50
//5000
//메소드 실행 완료

메소드 내에서 잠시 사용하는 지역 변수 개념과 같이 지역 클래스 자료형을 사용하고 버리는 것으로 이해하면 된다.

  • 메소드 내의 모든 내용은 스택 프레임 안에 생성됐다가 메소드 종료 시 사라지게 되므로, 다른 곳에서는 접근 및 사용자체가 불가능

  • 메소드 내에서 인스턴스를 생성한 후 사용하고 메소드 종료와 함께 레퍼런스가 사라지면서 힙 메모리 영역의 실제 데이터도 나중에 가비지 컬렉터에 의해 지워지게 된다.

로컬 클래스 지역 상수 접근
  • 메서드내의 로컬 클래스에서 지역 변수에 접근해서 값을 사용하려고 할때 반드시final 상수화 된 지역 변수만 사용이 가능하다.
    • 변수에 final을 안붙여도 실행이 되는 이유는, JDK1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final 을 생략할 수 있기 때문이다. (컴파일러가 자동으로 붙여줌)
  • 그러나 지역 변수(상수)의 값이 바뀌는 문장이 있으면 이는 상수가 아닌 변수이니, 컴파일 에러가 발생한다.

로컬 클래스 컴파일
  • 컴파일 시 생성되는 클래스 파일명: "외부클래스 $1 내부 클래스.class"

  • 지역 클래스는 여러 메소드 내에서 생성이 가능하니 같은 클래스명이 중복될수 있어 앞에 숫자로 구분한다.

4. Anonymous class 익명 클래스

  • 클래스 이름이 존재하지 않는 이너 클래스다.

    • 이름이 없기 때문에 생성자를 가질 수 없다.(생성자 존재 x)
  • 클래스의 선언과 동시에 객체를 생성하고 단 하나의 객체만을 생성한다.(일회용) > 마칠 때 중괄호 뒤에 세미콜론!!!

    • 만일 어느 메소드에서 부모 클래스의 자원을 상속받아 재정의하여 사용할 자식 클래스가 한번만 사용되고 버려질 자료형이면, 굳이 상단에 클래스를 정의하기보다는, 지역 변수처럼 익명 클래스로 정의하고 스택이 끝나면 삭제되도록 하는 것이 유지보수면에서나 프로그램 메모리면에서나 이점을 얻을 수 있다.
  • 익명 클래스는 기존에 존재하는 클래스를 메서드 내에서 일회용으로 클래스 내부 구성을 선언하여 필요한 메서드를 재정의 하여 사용하는 기법이다.

  • 조상 클래스(또는 구현하고자하는 인터페이스)의 이름을 사용하여 정의하기 때문에 하나의 클래스(인터페이스)만 상속(구현)받을 수 있다.

익명 클래스 컴파일
  • 컴파일시 생성되는 클래스 파일명 : "외부클래스$1.class"

  • 익명 클래스이기 때문에 클래스명은 없고 오로지 숫자로 구분한다.

// 부모 클래스
class Animal {
    public String bark() {
        return "동물이 웁니다";
    }
}

public class Main {
    public static void main(String[] args) {
        // 익명 클래스 : 클래스 정의와 객체화를 동시에. 일회성으로 사용
        Animal dog = new Animal() {
            @Override
            public String bark() {
                return "개가 짖습니다";
            }
        }; // 단 익명 클래스는 끝에 세미콜론을 반드시 붙여 주어야 한다.

        // 익명 클래스 객체 사용
        dog.bark();
    }
}
  • 익명 클래스는 재사용할 필요가 없는 일회성 클래스를 굳이 클래스를 정의하고 생성하는 것이 비효율적이기 때문에, 익명 클래스를 통해 코드를 줄이는 일종의 기법이라고 말 할 수 있다

Tip

익명 클래스는 전혀 새로운 클래스를 익명으로 사용하는 것이 아니라, 이미 정의되어 있는 클래스의 멤버들을 재정의 하여 사용할 필요가 있을때 그리고 그것이 일회성으로 이용될때 사용하는 기법이다.
즉, 익명 클래스는 부모 클래스의 자원을 일회성으로 재정의하여 사용하기 위한 용도 인 것이다.

익명 클래스는 이름이 없기 때문에 생성자를 가질 수 없으며, 가질 필요도 없다.

익명 클래스 유의점

익명 클래스 방식으로 선언한다면 오버라이딩 한 메소드 사용만 가능하고, 새로 정의한 메소드는 외부에서 사용이 불가능 하다.

// 부모 클래스
class Animal {
    public String bark() {
        return "동물이 웁니다";
    }
}

public class Main {
    public static void main(String[] args) {
            Animal dog = new Animal() {//익명 클래스: 클래스 정의와 객체화를 동시에 (일회성 사
            // @Override 메소드
            public String bark() {
                return "개가 짖습니다";
            }

            // 새로 정의한 메소드
            public String run() {
                return "달리기 ㄱㄱ싱";
            }
        };//익명 클래스는 끝에 세미콜론을 반드시 붙여 줘야한다.

        dog.bark();//익명 클래스 객체 사
        dog.run(); // ! Error - 외부에서 호출 불가능
    }
}
  • new Anima() {} 를 통해서 생성하는 인스턴스는 별도의 클래스가 아닌 Animal 클래스를 상속받는 익명 클래스이기 때문에, 부모인 Animal 클래스 자체에는 run() 메서드가 선언되어 있지 않기 때문에 사용하지 못하는 것. (다형성의 법칙을 따른다)

    • 새로 정의한 메소드는 외부에서 호출할 수 없고 익명 클래스 내에서만 호출이 가능하다.
  • 익명 클래스 끝에는 반드시 세미콜론을 붙여야한다.

익명 클래스도 내부 클래스의 일종이기 때문에, 외부의 지역 변수를 이용하려고 할때 똑같이 내부 클래스의 제약을 받게 된다. 따라서 내부 클래스에서 가져올 수 있는 외부 변수는 final 상수인 것만 가져와 사용할 수 있다

익명 클래스 선언 위치
  1. 클래스 필드로 이용 > 특정 클래스 내부에서 여러 메소드에서 이용될 때

  2. 지역 변수로서 이용 > 메소드에서 일회용으로 사용하고 버려질 클래스라면

  3. 메소드 아규먼트로 이용 > 아규먼트로 익명 객체 넘겨주면 된다.

익명 클래스 컴파일

내부 클래스를 컴파일 하면 $ 기호가 들어간 클래스명 class 파일을 얻게 된다.

익명 클래스도 내부 클래스의 일종이니 마찬가지이다. 

  • Main.java 파일에서 Animal 클래스의 익명 객체를 정의했다면, 컴파일 되면 Main.class, Animal.class, Animal$1.class 이렇게 3개 클래스 파일이 생기게 된다.

    • 익명 클래스 파일 부분이 Animal.class 인데, Animal 클래스를 이용해 만든 익명 클래스를 이름이 없는 자식 클래스니까 $1 으로 표현한 것이다.
  • 만약 익명 객체를 2개 정의했다면, Animal1.class, Animal$2.class 식으로, 자바파일명{익명객체정의된순번}.class 규칙순으로 클래스 파일이 생기게 된다.

    • 익명 객체 끼리는 아무리 내용이 똑같다고 하더라도 전혀 서로 다른 객체이기 때문에 별개로 취급되기 때문이다.
// 부모 클래스
class Animal {
}

// Main.java 파일
public class Main {
    public static void main(String[] args) {

        // 익명 객체 $1
        Animal dog = new Animal() {
        };

        // 익명 객체 $2
        Animal cat = new Animal() {
        };

        System.out.println(dog.getClass().getName()); // Main$1
        System.out.println(cat.getClass().getName()); // Main$2
    }
}

인터페이스 익명 구현 객체

인터페이스를 익명 객체로 선언 > 이게 많이 씀

  • 익명 클래스는 일회성 오버라이딩 용이라고 학습했다. > 추상화 구조인 인터페이스를 일회용으로 구현해, 익명 구현 객체로 선언해서 사용하면 시너지가 잘 맞는다.
// 인터페이스
interface IAnimal {
    public String bark(); // 추상 메소드
    public String run();
}

public class Main {
    public static void main(String[] args) {
        // 인터페이스 익명 구현 객체 생성
        IAnimal dog = new IAnimal() {
            @Override
            public String bark() {
                return "개가 짖습니다";
            }

            @Override
            public String run() {
                return "개가 달립니다";
            }
        };

        // 인터페이스 구현 객체 사용
        System.out.println(dog.bark()); // "개가 짖습니다" 출력
        System.out.println(dog.run());  // "개가 달립니다" 출력
    }
}
  • 인터페이스를 클래스 생성자 처럼 초기화하여 인스턴스화 한것같아보이지만, 인터페이스 자체로는 객체를 만들 수 없다.

    • 위의 코드에서 new 인터페이스명() 은 그렇게 보일 뿐이지, 사실 자식 클래스를 생성해서 implements 하고 클래스 초기화 한 것과 다름이 없다.
      그냥 익명클래스를 작성함과 동시에 객체를 생성하도록하는 Java의 문법으로 보면 된다.

추상클래스도 이런식의 익명 구현 객체 생성이 가능하다.

  • 클래스가 인터페이스를 구현한 후 인터페이스를 구현한 클래스로 객체를 만들어야하는데, 위의 코드는 인터페이스를 바로구현해서 구현한 클래스명이 없이 객체를 만들기 때문에 이를 익명 구현 객체라고 부른다.

    • 일반 상속 익명 객체와 다른 점은 상속과 다르게 인터페이스는 강제적으로 메소드 정의를 통해 사용해야하는 규약이 있기 때문에 규격화에 도움이 된다.

Nested Class(중첩 클래스)

Inner 클래스 전체를 가리킬 때 사용된다.

Outer클래스의 멤버로 취급되기 때문에 모든 접근제어지시자를 선언할 수 있다.

  • 정적(static) 또는 비정적(non-static) 클래스로 나눌 수 있다.

    • static :static nested class

    • non-static :inner class

    • class OuterClass
      
      // inner class 라고 부름
          class InnerClass {
              ...
          }
      
          // static nested class 라고 부름
          static class StaticNestedClass {
              ...
          }
      }
  • Inner class는 위 내용들과 같다. 외부 클래스의 인스턴스에 종속적이며, 외부 클래스의 인스턴스를 생성한 후에야만 Inner클래스의 인스턴스를 생성할 수 있다.

    • 외부 클래스의 멤버에 자유롭게 접근할 수 있다.
  • static nested class 와 static class는 같다고 생각하면 된다.

    • 외부 클래스의 인스턴스와 무관하게 동작할 수 있고, 정적으로 선언되어 있어 외부 클래스의 인스턴스에 종속되지 않는다.

      • 그래서 외부 클래스의 인스턴스를 생성하지 않고도 static 클래스를 직접 생성하고 사용할 수 있다.
class OuterClass {

    private String name;

    public OuterClass(String name) {
        this.name = name;
    }

    // inner class 라고 부름
    class InnerClass {

        public void print() {
            System.out.println(name);
        }
    }

    // static nested class 라고 부름
    static class StaticNestedClass {
        public void print() {
            System.out.println(name); // 컴파일 에러 발생
        }
    }
}
  • Inner 클래스(non-static class)는 해당 클래스를 감싸는 클래스의 멤버들로 접근할 수 있다. 해당 멤버가 private으로 선언되어 있어도 가능하다.

  • 반면에 static nested class는 감싸는 클래스의 다른 멤버로 접근할 수 없다.

Nested Class의 필요성

  • 한 곳에서만 사용되는 클래스를 논리적으로 그룹화 할 수 있다.

    • 어떤 클래스가 다른 클래스 하나에만 유용하다면 해당 클래스에 포함시켜 두 클래스를 함께 유지하는 것이 논리적이다. > 패키지가 더욱 간소화
  • 캡슐화가 증가한다. 두 개의 최상위 클래스인 A와 B가 있을 때, B가 A의 private 멤버에 접근해야 한다면 B클래스를 A내부에 둠으로써 A의 private 멤버에 접근할 수 있을 뿐 아니라 B자체를 외부에서 숨길 수도 있따.

수업 중 예제

OuterClass, NestedClass

class OuterClassOne
{
    OuterClassOne()
    {
        NestedClass nst=new NestedClass();
        nst.simpleMethod();
    }

    static class NestedClass
    {
        public void simpleMethod()
        {
            System.out.println("Nested Instance Method One");
        }
    }
}

class OuterClassTwo
{
    OuterClassTwo()
    {
        NestedClass nst=new NestedClass();
        nst.simpleMethod();        
    }

    private static class NestedClass
    {
        public void simpleMethod()
        {
            System.out.println("Nested Instance Method Two");
        }
    }
}

class NestedClassTest//static InnerClass = NestedClass
{    
    public static void main(String[] args){
    OuterClassOne one=new OuterClassOne();
        OuterClassTwo two=new OuterClassTwo();

        OuterClassOne.NestedClass nst1=new OuterClassOne.NestedClass();//독립적으로 객체 생성가능 
        nst1.simpleMethod();
        // OuterClassTwo.Nested nst2=new OuterClassTwo.Nested();
        // nst2.simpleMethod();
  • 주석처리는 private으로 선언되어 있기 때문에 객체 생성 불가

  • OuterClassOne.NestedClass nst1=new OuterClassOne.NestedClass(); > 얘 하나만으로도 독립적으로 객체 생성 가능

    OuterClass, InnerClass

class OuterClass
{
    private String myName;
    private int num;

    OuterClass(String name)
    {
        myName=name;
        num=0;
    }

    public void whoAreYou()
    {
        num++;
        System.out.println(myName+ " OuterClass "+num);
    }

    class InnerClass
    {
        InnerClass()
        {
            whoAreYou();
        }
    }
}class InnerClassTest
{    
    public static void main(String[] args)
    {
        OuterClass out1=new OuterClass("First");
        OuterClass out2=new OuterClass("Second");//얘네를 만들어야지만
        out1.whoAreYou();
        out2.whoAreYou();

        OuterClass.InnerClass inn1=out1.new InnerClass();//InnerClass 생성 가능
        OuterClass.InnerClass inn2=out2.new InnerClass();    
        OuterClass.InnerClass inn3=out1.new InnerClass();
        OuterClass.InnerClass inn4=out1.new InnerClass();
        OuterClass.InnerClass inn5=out2.new InnerClass();
    }
}
  • 위와는 다르게 OuterClass가 먼저 객체로 생성되어 있어야지만 InnerClass의 객체를 생성할 수 있다.(InnerClass를 외부에서 객체로 생성할 일은 별로 x)

LocalClass

interface Readable 
{
    public void read();
}

class OuterClass
{
    private String myName;

    OuterClass(String name)
    {
        myName=name;
    }

    public Readable createLocalClassInst()//자료형이 LocalClass일 수 없다 > 메소드 안에서 선언하기 때문에 인식 x
    {                                   //만약 이 안에 LocalClass를 준다면 final로 줘야한다..
        class LocalClass implements Readable//LocalClass가 Readable을 구현하게끔 함
        {
            public void read()
            {
                System.out.println("Outer inst name: "+myName);
            }
        }

        return new LocalClass();
    }
}

class LocalClassTest
{    
    public static void main(String[] args)
    {
        OuterClass out1=new OuterClass("First");
        Readable localInst1=out1.createLocalClassInst();//인터페이스에 대한 자료형으로 선언 > 외부에서 인식해야하기 때문에
        localInst1.read();

        OuterClass out2=new OuterClass("Second");    
        Readable localInst2=out2.createLocalClassInst();
        localInst2.read();
    }
}
  • 메인 메소드 안에서 자료형으로 사용하지 못하고, 클래스의 자료형에도 LocalClass로 사용할 수 없다. > 그래서 인터페이스를 사용

  • 외부에서 LocalClass를 인식하지 못하기 때문에 코드를 짤때는 애너니머스 클래스로 클래스 이름을 주지 않는다. 밑에처럼 사용

    public Readable createLocalClassInst(){
        return new Readable(){
            public void read(){
                syso("Outer~~);
            }
        }
    }

static import

0개의 댓글