본문 바로가기

Study/개발일지

[백엔드온라인TIL] java 학습 2일차

요리 레시피 메모장 만들기

  • 입력값
    • 내가 좋아하는 요리 제목을 먼저 입력합니다.
    • 요리 별점을 1~5 사이의 소수점이 있는 실수로 입력해주세요. (ex. 3.5)
    • 이어서 내가 좋아하는 요리 레시피를 한 문장씩 10문장을 입력합니다.
  • 출력값
    • 입력이 종료되면 요리 제목을 괄호로 감싸서 먼저 출력 해줍니다.
    • 이어서, 요리 별점을 소수점을 제외한 정수로만 출력해줍니다. (ex. 3)
    • 바로 뒤에 정수별점을 5점만점 퍼센트로 표현했을 때 값을 실수로 출력해줍니다. (ex. 60.0%)
    • 이어서, 입력한 모든 문장 앞에 번호를 붙여서 모두 출력 해줍니다.

ex) 입력 예시

백종원 돼지고기 김치찌개 만들기
4.5
돼지고기는 핏물을 빼주세요.
잘익은 김치 한포기를 꺼내서 잘라주세요.
냄비에 들기름 적당히 두르고 김치를 넣고 볶아주세요.
다진마늘 한스푼, 설탕 한스푼 넣어주세요.
종이컵으로 물 8컵 부어서 센불에 끓여주세요.
핏물 뺀 돼지고기를 넣어주세요.
된장 반스푼, 양파 반개, 청양고추 한개를 썰어서 넣어주세요.
간장 두스푼반, 새우젓 두스푼, 고춧가루 두스푼반 넣어주세요.
중불로 줄여서 오래 끓여주세요~!!	
마지막에 파 쏭쏭 썰어서 마무리하면 돼요^^

 

 

 

 

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String title = sc.nextLine();
        float rate = sc.nextFloat();
        sc.nextLine();
        String input1 = sc.nextLine();
        String input2 = sc.nextLine();
        String input3 = sc.nextLine();
        String input4 = sc.nextLine();
        String input5 = sc.nextLine();
        String input6 = sc.nextLine();
        String input7 = sc.nextLine();
        String input8 = sc.nextLine();
        String input9 = sc.nextLine();
        String input10 = sc.nextLine();

        System.out.println("["+ title + "]");
        int intRate = (int)rate; // 강제 형변환
        double percentageRate = intRate * 100 / 5.0;
        System.out.println("별점 : " + (int) rate + " (" +percentageRate + ")");
        System.out.println("1. " + input1);
        System.out.println("2. " + input2);
        System.out.println("3. " + input3);
        System.out.println("4. " + input4);
        System.out.println("5. " + input5);
        System.out.println("6. " + input6);
        System.out.println("7. " + input7);
        System.out.println("8. " + input8);
        System.out.println("9. " + input9);
        System.out.println("10. " + input10);

    }
}

rate 변수 입력 후 출력시 개행문자가 input1변수에 들어가는 현상 해결 방법
nextFloat 소스코드 뒤에 sc.nextLine() 메소드를 한번더 사용한다 .

 

오류 원인

Scanner.nextInt() 메소드가 사용자가 입력한 enter(개행문자) 를 제거하지 않기 때문이다.

사용자는 콘솔창에 숫자입력 후 enter를 누르는데,

이때 nextInt() 가 숫자는 입력받고, 개행문자는 제거하지않은 채 그대로 남겨둔다.

남겨진 개행문자가 다음 scan.nextLine() 의 입력으로 처리되어

곧바로 개행되고, 위와같이 출력된다.

 

깊은복사 vs 얕은 복사 

 

깊은 복사와 얕은 복사라는 개념은 평소에 접한적이 꽤 있었습니다.

하지만 오늘 알고리즘 문제를 풀면서 아무런 의심없이(?) 다음과 같이 컬렉션 List를 얕은 복사하는 코드를 작성했었고, 이에 따라 참조하고 있는 두 리스트가 모두 값이 변경되어 생각했던 아웃풋과 다르게 나와서 약간 어리둥절한 상태였습니다. 🤔

List<String> list = new ArrayList<>();

...

List<String> temp = list; // shallow copy

 

해당 문제점은 디버깅을 통해 파악할 수 있었는데요, 기본적인 내용이지만 확실하게 정리하고 넘어가도록 하겠습니다 😃

 

 

깊은 복사(Deep Copy)는 '실제 값'을 새로운 메모리 공간에 복사하는 것을 의미하며,

얕은 복사(Shallow Copy)는 '주소 값'을 복사한다는 의미입니다.

 

얕은 복사의 경우 주소 값을 복사하기 때문에, 참조하고 있는 실제값은 같습니다.

예제와 설명을 통해 확인해보겠습니다.

 

 

 

🎯  얕은 복사(Shallow Copy)

public class CopyObject {

    private String name;
    private int age;
    
    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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



import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CopyObjectTest {

    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original; // 얕은 복사

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }
}

위 코드에서는 copy 객체에 set메소드를 통해 이름을 변경했는데,

실제 결과는 original 객체와 copy 객체 모두 값이 변경이 되었습니다.

 

CopyObject copy = original 의 코드에서 객체의 얕은 복사를 통해 '주소 값'을 변경했기 때문에

참조하고 있는 실제 값은 동일하고, 복사한 객체가 변경된다면 기존의 객체도 변경이 되는 것입니다.

 

 

위 상태에 대한 메모리 구조를 나타내면 다음과 같이 됩니다.

CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

스택이 스팸으로 보이는건 배가 고파서일까요..

original 인스턴스를 생성하면 Stack 영역에 참조값이, Heap 영역에 실제값이 저장이 됩니다.

그리고 얕은 복사를 통해 객체를 복사했기 때문에 copy 인스턴스는 original 인스턴스가 참조하고 있는

Heap 영역의 참조값을 동일하게 바라보고 있는 상태가 됩니다.

 

그 후 set 메소드를 통해 값을 변경하면 동일한 주소를 참조하고 있기 때문에 아래와 같이 됩니다.

따라서 코드로는 copy 객체의 name만 변경했지만,

동일한 주소를 참조하고 있기 때문에 original의 객체에도 영향을 끼치게 됩니다.

 

그래서 객체를 출력해보면 아래와 같이 동일한 주소가 출력이 됩니다.

CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

System.out.println(original);
System.out.println(copy);

 

 

 

 

🎯  깊은 복사(Deep Copy)

깊은 복사를 구현하는 방법은 여러가지가 있습니다.

  • Cloneable 인터페이스 구현
  • 복사 생성자
  • 복사 팩터리 등등....

 

 

 Cloneable 인터페이스 구현

Cloneable 인터페이스는 위와 같이 빈 껍데기의 인터페이스지만

주석을 살펴보면 Object 클래스의 clone() 메소드를 반드시 구현하라고 설명이 되어있습니다.

 

 

public class CopyObject implements Cloneable {

    private String name;
    private int age;

    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected CopyObject clone() throws CloneNotSupportedException {
        return (CopyObject) super.clone();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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


    @Test
    void shallowCopy() throws CloneNotSupportedException {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original.clone();

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }

깊은 복사를 통해 테스트를 진행해보면 얕은 복사와는 달리 original 인스턴스의 값은 변경이 되지 않습니다.

 

 

 Effective Java 13장에서는 clone 재정의는 주의해서 진행하라 라는 아이템이 있습니다.

해당 책의 내용을 대략적으로 살펴보면 다음과 같습니다.

Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못했다. 여기서 큰 문제점은 clone 메서드가 선언된 곳이 Cloneable이 아닌 OBject이고, 그 마저도 protected이다. 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메소드를 호출할 수 없다. 리플렉션을 사용하면 가능하지만, 100% 성공하는 것도 아니다. 

이러한 여러 문제점을 가진 인터페이스이지만, Cloneable 방식은 널리 쓰이고 있어서 잘 알아두는 것이 좋다.

 

Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. final 클래스라면 Cloneable을 구현해도 위험이 크지는 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.

 

기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 최고' 라는 것이다.

단, 배열만은 clone 메소드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.

 

http://www.yes24.com/Product/Goods/65551284

 

 

 

◎ 복사 생성자, 복사 팩터리

public class CopyObject {

    private String name;
    private int age;

    public CopyObject() {
    }

    /* 복사 생성자 */
    public CopyObject(CopyObject original) {
        this.name = original.name;
        this.age = original.age;
    }

    /* 복사 팩터리 */
    public static CopyObject copy(CopyObject original) {
        CopyObject copy = new CopyObject();
        copy.name = original.name;
        copy.age = original.age;
        return copy;
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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


    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copyConstructor = new CopyObject(original);
        CopyObject copyFactory = CopyObject.copy(original);

        copyConstructor.setName("JuBal");
        copyFactory.setName("BalJu");

        System.out.println(original.getName());
        System.out.println(copyConstructor.getName());
        System.out.println(copyFactory.getName());
    }

복사 생성자와 복사 팩터리를 통해 객체를 복사하는 과정도 깊은 복사임을 알 수 있습니다.

 

 

깊은 복사를 그림으로 나타내면 다음과 같습니다.

얕은 복사와는 다르게 Heap 영역에 새로운 메모리 공간을 생성하여 실제 값을 복사합니다.

 

Collections나 Map의 경우 이미 복사 팩터리인 copy() 메소드를 구현하고 있습니다.

    /**
     * Copies all of the elements from one list into another.  After the
     * operation, the index of each copied element in the destination list
     * will be identical to its index in the source list.  The destination
     * list's size must be greater than or equal to the source list's size.
     * If it is greater, the remaining elements in the destination list are
     * unaffected. <p>
     *
     * This method runs in linear time.
     *
     * @param  <T> the class of the objects in the lists
     * @param  dest The destination list.
     * @param  src The source list.
     * @throws IndexOutOfBoundsException if the destination list is too small
     *         to contain the entire source List.
     * @throws UnsupportedOperationException if the destination list's
     *         list-iterator does not support the {@code set} operation.
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

 

1. 제어문(control statement)

- 프로그램의 흐름(flow)을 바꾸는 역할을 하는 문장

- 조건문 : 조건에 따라 다른 문장이 수행되도록 함

- 반복문 : 특정 문장들을 반복해서 수행

 

2. 조건문 - if, switch

1) if문

 

if (조건식) {
       // 조건식이 참(true)일 때 수행될 문장
}

 

- 만일(if) 조건식이 참(true)이면 괄호 { } 안의 문장들을 수행하라

- 조건식은 일반적으로 비교연산자와 논리연산자로 구성 (반드시 true 혹은 false의 값을 가져야 함)

- 블럭(block) : 괄호 '{ }'를 이용해서 여러 문장을 하나의 단위로 묶음

 

[잘못된 예시]
if(score > 60) 
   System.out.println("합격");   // if문에 속한 문장
   System.out.println("축하");   // if문에 속하지 않은 문장

[올바른 예시]
if(score > 60) {
   System.out.println("합격");   // if문에 속한 문장
   System.out.println("축하");   // if문에 속한 문장
}

-> 블럭 내의 문장이 하나라면 괄호를 생략할 수 있지만, 여러 문장일 경우 꼭 괄호로 묶어줘야 함

 

2) if-else문

 

if (조건식) {
     // 조건식이 참(true)일 때 수행될 문장
} else {
     // 조건식이 거짓(false)일 때 수행될 문장
}

 

3) if-else if문

 

if (조건식1) {
     // 조건식1이 참(true)일 때 수행될 문장
} else if (조건식2) {
    // 조건식2이 참(true)일 때 수행될 문장
} else if (조건식3) {
    // 조건식3이 참(true)일 때 수행될 문장
} else {

     // 조건식1,2,3이 모두 거짓(false)일 때 수행될 문장
}

 

4) 중첩 if문

 

if (조건식1) {   
     // 조건식1이 참(true)일 때 수행될 문장
     if (조건식2) {
             // 조건식1이 true면서 조건식2가 true일 때 수행될 문장
     } else {
             // 조건식1이 true면서 조건식2가 false일 때 수행될 문장
     }
} else {
    // 조건식1이 거짓(false)일 때 수행될 문장
}

 

5) swtich문

 

switch (조건식) {
         case 값1 :
                     // 조건식의 결과가 값1과 같을 경우 수행될 문장
                     break;
         case 값2 :
                     // 조건식의 결과가 값2와 같을 경우 수행될 문장
                     break;
         default :
                     // 조건식의 결과와 일치하는 case문이 없을 때 수행될 문장들
}

 

- 조건식의 결과는 정수, 문자열만 가능

- case문의 값은 정수, 상수만 가능하며 중복 불가능 (변수, 실수 불가능!)

- 조건식의 결과와 일치하는 case문이 하나도 없는 경우 default문으로 이동

- break문을 사용하지 않으면 다른 break문을 만나거나 switch문 블럭의 끝을 만날 때까지 나오는 모든 문장들을 수행하므로 의도한게 아니라면 case문 끝에 break를 써주는 것을 잊지 말아야 함 (맨 마지막은 생략 가능)

- switch문도 if문처럼 중첩이 가능함

 

3. 반복문 - for, while, do-while

1) for문

- 주로 반복횟수를 알고 있을 때 사용

 

for(int i=1; i<=5; i++) {
         // 반복 수행할 문장
}
1) 초기값 : 반복문에 사용될 변수를 초기화하며 처음 한번만 수행, 둘 이상의 변수 선언 가능 (타입이 같아야 함)
ex) for(int i=1, j=0; i<=5; i++) { ... }
2) 조건식 : 조건식의 값이 참(true)이면 반복을 계속하고, 거짓(false)이면 반복을 중단하고 for문을 벗어남
3) 증감식 : 반복문을 제어하는 변수이 값을 증가 또는 감소시킴

-> 세 요소는 필요하지 않으면 생략할 수 있으며, 모두 생략도 가능함
ex) for(;;) { ... }

 

2) 중첩 for문

- for문안에 for문을 포함시킬 수 있음

 

for(int i=1; i<=5; i++) {
   for(int j=1; j<=10; j++) {
          // 반복 수행할 문장
   }
   // 반복 수행할 문장
}

 

3) 향상된 for문(enhanced for statement)

- JDK1.5부터 for문의 새로운 문법이 추가되었음

- 타입은 배열 또는 컬렉션의 요소의 타입이어야 함

- 배열 또는 컬렉션에 저장된 값이 매 반복마다 하나씩 순서대로 읽혀서 변수에 저장됨

for( 타입 변수명 : 배열 또는 컬렉션) {
      // 반복 수행할 문장
}

 

4) while문

- 먼저 조건식을 평가해서 조건식이 거짓이면 문장 전체를 벗어나고, 참이면 블럭 내의 문장을 수행하고 다시 조건식으로 돌아감

- 조건식이 거짓이 될 때까지 이 과정이 계속 반복됨

- 조건식은 생략 불가능

- 조건식에 true를 넣으면 무한루프에 빠질 수 있음에 주의

 

while (조건식) {
       // 조건식의 연산결과가 참(true)일 동안, 반복 수행할 문장
}

 

5) do-while문

- 최소한 한번은 수행될 것을 보장하는 while문

- while문과 반대로 블록 내의 문장을 먼저 수행한 후 조건식을 평가함

- while(조건식) 뒤에 세미콜론(;)을 붙여야 함에 유의

 

do {
        // 조건식의 연산결과가 참(true)일 동안, 반복 수행할 문장
} while (조건식);

 

6) break문

- 자신이 포함된 가장 가까운 반복문을 벗어남

- 무한 반복문에 조건문과 break문을 같이 사용할 수 있음

 

7) continue문

- 반복이 진행되는 도중에 continue문을 만나면 반복문의 끝으로 이동하여 다음 반복으로 넘어감

- 전체 반복 중에 특정조건을 만족하는 경우를 제외하고자 할 때 사용

 

8) 이름 붙은 반복문

- 여러 개의 반복문이 중첩된 경우 break문으로 중첩 반복문을 완전히 벗어날 수 없음

- 중첩 반복문 앞에 이름을 붙이고 break문과 continue문에 이름을 지정해 줌으로써 하나 이상의 반복문을 벗어나거나 반복을 건너뛸 수 있음

 

Loop1 : for(int i=2; i<=9; i++) {
           for(int j=1; j<=9; j++) {
                if(j==5)
                    break Loop1;     // j가 5가 되면 반복문(Loop1)을 완전히 벗어남
                System.out.println(i + "*" + j + "=" + i*j);
            }
            System.out.println();
      }

 

 

-- 문제 

 

자료구조 요리 레시피 메모장 만들기

  • 입력값
    • 저장할 자료구조명을 입력합니다. (List / Set / Map)
    • 내가 좋아하는 요리 제목을 먼저 입력합니다.
    • 이어서 내가 좋아하는 요리 레시피를 한문장씩 입력합니다.
    • 입력을 마쳤으면 마지막에 “끝” 문자를 입력합니다.
  • 출력값
    • 입력이 종료되면 저장한 자료구조 이름과 요리 제목을 괄호로 감싸서 먼저 출력 해줍니다.
    • 이어서, 입력한 모든 문장앞에 번호를 붙여서 입력 순서에 맞게 모두 출력 해줍니다.

 

 

import java.util.*;

public class Main02 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String dataType =  sc.nextLine();
        String title = sc.nextLine();

        ArrayList<String> recipe = new ArrayList<String>();
            switch (dataType) {
                case "List" :
                    ArrayList<String> strList = new ArrayList<>();
                    while(true) {
                        //
                        String text = sc.nextLine();
                        if(Objects.equals(text,"끝")) {
                            break;
                        }
                        strList.add(text);
                    }
                    title = "[ List로 저장된 " + title + " ]";
                    System.out.println(title);

                    //strList 한줄씩 출력
                    for (int i =0; i< strList.size(); i++) {
                        int number = i+1;
                        System.out.println(number + ". " + strList.get(i));

                    }
                    break;
                case "Set" :
                    LinkedHashSet<String> strSet = new LinkedHashSet<>();
                    while(true) {
                        //
                        String text = sc.nextLine();
                        if (Objects.equals(text, "끝")) {
                            break;
                        }
                        strSet.add(text);
                    }
                    title = "[ Set으로 저장된 " + title + " ]";
                    System.out.println(title);
                    Iterator iterator = strSet.iterator();
                    //strSet 한줄씩 출력
                    for (int i =0; i< strSet.size(); i++) {
                        int number = i+1;
                        System.out.println(number + ". " + iterator.next());
                    }
                    break;
                case "Map":
                    Map<Integer, String> strMap = new HashMap<>();
                    int lineNumber = 1;
                    while(true) {
                        String text = sc.nextLine();
                        if(Objects.equals(text, "끝")) {
                            break;
                        }
                        strMap.put(lineNumber++,text);
                    }
                    title = "[ Map으로 저장된 " + title + " ]";
                    System.out.println(title);

                    // strList 한줄씩 출력
                    for (int i = 0; i < strMap.size(); i++) {
                        int number = i + 1;
                        System.out.println(number + ". " + strMap.get(i + 1));
                    }
                    break;
                default:
                    System.out.println("저장할 수 없는 자료구조입니다.");

            }


    }
}
728x90