정적 바인딩

  • 컴파일 시점에 호출할 메서드가 결정
  • private, static, final 메서드는 정적 바인딩 적용
  • 오버라이딩 불가능 -> 숨김 발생
class Animal {
    static void staticMethod() {
        System.out.println("Animal의 staticMethod");
    }
}

class Dog extends Animal {
    static void staticMethod() { // 오버라이딩(X) → 숨김(Hiding)
        System.out.println("Dog의 staticMethod");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 업캐스팅
        animal.staticMethod(); // "Animal의 staticMethod"
    }
}

 

숨김(Hiding)의 동작 원리

  • static 메서드는 객체가 아니라 "변수 타입"에 따라 실행할 메서드 결정
  • 자식 클래스에서 동일한 이름의 static/private 메서드를 선언하면 부모 클래스의 메서드를 오버라이딩 하지 않고, 새로운 static 메서드 정의

동적 바인딩 

  • 런타임(실행) 시점에 호출할 메서드가 결정
  • 인스턴스 메서드는 동적 바인딩 적용
  • 오버라이딩 발생
  • 슈퍼 클래스 메소드의 접근 지정자보다 접근의 범위를 좁혀서 오버라이딩할 수 없음 (public > protected > default > private)
class Animal {
    void makeSound() { System.out.println("동물이 소리를 냅니다."); }
}

class Dog extends Animal {
    @Override
    void makeSound() { System.out.println("멍멍!"); }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 업캐스팅
        animal.makeSound(); // "멍멍!" (동적 바인딩)
    }
}

 

동적 바인딩의 동작 원리

내부 구조

  • 가상 메서드 테이블(Virtual Method Table, V-Table) 구조를 이용해 동적 바인딩 구현
  • V-Table에는 해당 클래스에서 오버라이딩된 메서드의 주소가 저장됨
  • 객체가 생성되면, JVM은 V-Table을 참조해서 오버라이딩된 메서드를 실행

 

 


정적 바인딩 vs 동적 바인딩 정리

☝️ private 변수 자체는 정적 바인딩이지만, getter, setter를 통해 접근하기 때문에 결과적으로는 동적 바인딩이 이루어짐

 

Arrays.toString()

  • 배열을 문자열로 변환하여 요소를 한 줄로 출력
  • Object 클래스의 toString() 메서드를 오버라이딩한 것이 아니라, Arrays 클래스에 정의된 특별한 메서드
    • 배열은 객체이기 때문에, 만약 toString()을 직접 호출하면 배열 객체의 주소값이 출력
    • Arrays.toString()은 내부적으로 배열의 요소를 하나씩 순회하면서 각 값을 문자열로 변환하고, 이를 [와 ]로 감싸서 출력하는 방식으로 구현
int[] intArray = {1, 2, 3, 4, 5};
System.out.println(Arrays.toString(intArray));  // [1, 2, 3, 4, 5]

Arrays.sort()

  • 배열을 오름차순으로 정렬
  • 내림차순 정렬하려면 Comparator 사용
int[] intArray = {5, 2, 8, 1, 3};
Arrays.sort(intArray);
System.out.println(Arrays.toString(intArray));  // [1, 2, 3, 5, 8]

Arrays.binarySearch()

  • 이진 탐색을 이용하여 배열에서 특정 값을 탐색
  • 이 메서드는 정렬된 배열에서만 사용
  • Arrays.binarySearch()는 값이 배열에 없으면 음수를 반환
int[] intArray = {1, 2, 3, 4, 5};
int index = Arrays.binarySearch(intArray, 3);
System.out.println(index);  // 2 (3이 배열의 인덱스 2에 있음)

Arrays.equals()

  • 두 배열이 같은지 비교하는 메서드
  • 배열의 각 요소가 동일한지 비교하며, 배열 크기도 동일해야 true를 반환
int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};
System.out.println(Arrays.equals(array1, array2));  // true

Arrays.fill()

  • 배열의 모든 요소를 특정 값으로 채우는 메서드
int[] intArray = new int[5];
Arrays.fill(intArray, 10);  // 배열의 모든 요소를 10으로 채운다.
System.out.println(Arrays.toString(intArray));  // [10, 10, 10, 10, 10]

Arrays.copyOf()

  • 배열을 복사하고 크기를 변경할 수 있는 메서드
int[] originalArray = {1, 2, 3, 4};
int[] copiedArray = Arrays.copyOf(originalArray, 6);  // 배열 크기를 6으로 확장
System.out.println(Arrays.toString(copiedArray));  // [1, 2, 3, 4, 0, 0]

 

 


 

Arrays.asList()와 배열

공통점 : 크기 변경 불가

  • Arrays.asList()로 만든 리스트는 배열 기반의 리스트로 크기가 고정되어 있다.
  • add()나 remove()같은 메서드로 요소를 추가하거나 제거할 수 없다.

차이점 : 기능

  • Arrays.asList()로 만든 리스트는 List 인터페이스를 구현하기 때문에 get(), set(), indexOf(), contains() 등의 메서드를 사용할 수 있다.
  • 배열은 위의 메서드를 사용할 수 없다.

불변 객체의 장점

1. 스레드 안전성

  • 불변 객체는 공유해도 안전하기 때문에 동기화(synchronization) 없이 사용할 수 있다.
  • 가변 객체(Mutable Object)는 여러 스레드에서 동시에 수정하면서 데이터 불일치 문제가 생길 수 있다.
Integer x = 100;
Integer y = x; // x와 y가 같은 객체를 참조

x = x + 1; // 새로운 Integer 객체가 생성됨 (기존 객체 변경 없음)

System.out.println(y); // 여전히 100 (안전!)
System.out.println(x); // 101 (새 객체 생성)

 

 

2. 해싱 최적화

 

  • 해시 기반 자료구조(HashMap, HashSet 등)에서 키로 사용할 때 안정적이다.
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        Integer key = 100;  // Integer는 불변 객체!

        map.put(key, "Hello");
        System.out.println(map.get(key)); // Hello

        // key의 값은 변하지 않으므로, 해시 값도 그대로 유지됨
        System.out.println(key.hashCode()); // 항상 100 출력
    }
}

불변 객체의 단점

값이 변경될 때 새로운 객체를 계속 생성해야 하기 때문에, 메모리를 더 많이 사용한다.

하지만, 자바는 캐싱을 통해 불필요한 객체 생성을 줄이고 있다.


캐싱을 통한 불필요한 객체 생성 방지

Integer a = 127;
Integer b = 127;

System.out.println(a == b); // true (같은 객체를 재사용)

Integer c = 128;
Integer d = 128;

System.out.println(c == d); // false (새 객체 생성)

 

 

자바에서는 Integer 객체를 매번 새로 생성하면 메모리를 많이 낭비할 수 있어서, 자주 사용되는 숫자 범위(-128~127)는 미리 캐싱해서 재사용하도록 설계되어 있다.

자바 8부터는 JVM 옵션(-XX:AutoBoxCacheMax=<값>)을 통해 캐싱 범위를 변경할 수 있다.


 

그렇다면 내부적으로 어떻게 같은 값이라는 걸 알고 기존 객체를 재활용하는 걸까?

캐싱 동작 탐구

Step 1

`Integer.valueOf(int)`로 객체화된 값을 저장

Integer a = 127; // 내부적으로 Integer.valueOf(127) 호출
Integer b = 127; // 동일한 Integer 객체를 재사용
System.out.println(a == b); // true (같은 객체)

 

 

Step 2

`Integer.valueOf(int)` 내부 코드를 보면, 값이 -128~127인 경우는 IntegerCache 클래스를 호출하고, 그 외는 새로운 객체 생성

public static Integer valueOf(int i) {
    if (i >= -128 && i <= 127) { // 캐싱 범위 확인
        return IntegerCache.cache[i + 128]; // 기존 객체 반환
    }
    return new Integer(i); // 범위 밖이면 새로운 객체 생성
}

 

 

Step 3

Integer 클래스의 내부 클래스인 IntegerCache 클래스를 보면, -128~127의 객체를 모두 생성해둔다.

private static class IntegerCache {
    static final Integer cache[];

    static {
        cache = new Integer[256]; // -128 ~ 127 저장
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new Integer(i - 128); // 미리 Integer 객체 생성
        }
    }
}

 

결론적으로, 이미 Integer 클래스가 로드될 때 static 블록으로 인해 -128~127의 객체를 바로 생성해두고, 필요 시 해당 객체를 반환하는 형식으로 캐싱이 이루어진다.


 

그렇다면 -128~127의 수를 모두 객체화 해두는 건 비효율적이지 않을까?

 

  1. -128 ~ 127 범위는 자주 사용되는 숫자들이라 캐싱해두면 오히려 성능에 좋음
    • 루프 변수 (for (int i = 0; i < 100; i++))
    • 배열 인덱스, 개수(size, length)
    • 흔한 숫자(0, 1, -1, 100 등)
  2. 캐싱된 객체가 차지하는 메모리가 크지 않음
    • IntegerCache는 256개의 Integer 객체(-128 ~ 127)를 미리 생성하는데,
    • Integer 객체 1개 ≈ 16바이트 (객체 헤더 + 필드)
    • 256개 × 16바이트 ≈ 4KB 정도의 메모리 사용

✅ 1️⃣ 캐싱이 적용되는 클래스

  • Integer, Short, Byte, Long, Character → 작은 범위의 정수를 캐싱
  • Boolean → true, false 두 개만 존재하므로 항상 같은 객체 사용
  • String → String Pool을 사용하여 리터럴 문자열 재사용
    • new String("hello")로 생성하면 새로운 객체 참조
    • .intern()을 호출하면 스트링 풀에 있는 "hello"와 동일한 객체를 참조
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello"); 
String s4 = new String("hello"); 
System.out.println(s1 == s2); // true (같은 객체)
System.out.println(s3 == s4); // false (다른 객체)
System.out.println(s1 == s3); // false (다른 객체)

String str1 = "hello";
String str2 = new String("hello").intern(); // intern() 메서드로 동일한 객체 탐색 후 참조
String str3 = str1.intern(); // 이렇게도 사용 가능
System.out.println(str1 == str2);  // true

 

 

❌ 2️⃣ 캐싱이 없는 클래스

  • Double, Float → 실수값이 무한대에 가깝고, 자주 같은 값을 쓸 일이 적음
  • BigInteger, BigDecimal → 숫자의 범위가 너무 커서 캐싱하면 메모리 낭비가 심함

공통 메서드

Wrapper 클래스는 실재하지 않고, Integer, Double, Character 클래스 등을 묶어 Wrapper 클래스라고 통칭한다.

이 클래스들이 공통적으로 사용할 수 있는 메서드는 아래와 같다.

 

(1) `     `.valueOf()

  • valueOf(기본형 값) : 기본형 값을 해당 Wrapper 객체로 변환 (=> 객체화)
  • valueOf(String s) : 문자열을 해당 Wrapper 객체로 변환 (=> 파싱 목적)
// String 클래스
String.valueOf(123);    // "123"

// Integer 클래스
Integer.valueOf("123"); // 123

// Double 클래스
Double.valueOf("45.67"); // 45.67

// Boolean 클래스
Boolean.valueOf("true"); // true

// 기본형 -> 객체 (박싱)
Integer.valueOf(123);   // 123

 

 

(2) `     `Value()

  • Wrapper 객체를 기본형 값으로 변환
Integer intObj = Integer.valueOf(50); // valueOf로 객체화
int num = intObj.intValue();  // intValue로 다시 기본형 int로 변환

 

 

(3) parse`     `(String s)

  • 문자열을 기본형 값으로 변환
  • parseInt, parseDouble, parseBoolean 등 다양한 변형 존재
int num = Integer.parseInt("123");
double d = Double.parseDouble("45.67");
문자열을 기본형으로 변환시키는 메서드가 왜 굳이 객체화 시키는 Wrapper 클래스에 있을까?
결론적으로는 큰 의미는 없다.
Wrapper 클래스는 객체화를 위해 기본형을 다루기 때문에, 기본형으로 변환시키는 메서드도 편리를 위해 같이 존재하는 것 뿐이다.

 

파싱?
파싱과 형변환의 차이점

 


결론❗️
Wrapper 객체 -> 기본형은 `     `Value()
문자열 -> 기본형은 parse`     `()
기본형의 객체화는 `     `.valueOf()       (+Wrapper <-> 문자열 간의 파싱 목적도 존재)

 


각 Wrapper 클래스별 주요 메서드

(1) Integer 클래스

  • static String toBinaryString(int i): 2진수 문자열 변환
  • static String toHexString(int i): 16진수 문자열 변환
  • static int compare(int x, int y): 두 숫자 비교 (compareTo와 유사)
  • static int sum(int a, int b): 두 숫자 합 반환

(2) Double 클래스

 
  • static boolean isNaN(double v): NaN 값인지 확인
  • static boolean isInfinite(double v): 무한대인지 확인
  • static double sum(double a, double b): 두 숫자 합 반환

(3) Character 클래스

  • static boolean isDigit(char ch): 숫자인지 확인
  • static boolean isLetter(char ch): 문자인지 확인
  • static boolean isUpperCase(char ch): 대문자인지 확인
  • static boolean isLowerCase(char ch): 소문자인지 확인
  • static char toUpperCase(char ch): 대문자로 변환
  • static char toLowerCase(char ch): 소문자로 변환

(4) Boolean 클래스

 
  • static boolean logicalAnd(boolean a, boolean b): 논리 AND 연산
  • static boolean logicalOr(boolean a, boolean b): 논리 OR 연산
  • static boolean logicalXor(boolean a, boolean b): 논리 XOR 연산

equals()

Object 클래스의 메서드 중 하나다.

Object 클래스는 계층 구조 상 최상위 클래스로, 이 클래스의 메서드는 모든 객체가 상속받아 사용할 수 있다.

`equals()`는 현재 객체와 매개변수로 받아온 객체가 같은 내용이면 true를 반환하고, 그렇지 않으면 false를 반환하는 메서드다.


equals()는 동일한 타입의 객체끼리 비교해야 한다

equals() 메서드는 두 객체가 "같은 클래스"의 인스턴스인지 확인한 후, 값이 같은지 비교한다.

따라서 Integer(숫자)와 String(문자열)을 비교하면, 클래스가 다르기 때문에 false를 반환한다.

public class EqualsExample {
    public static void main(String[] args) {
        Integer num = 100;     // Integer 객체
        String str = "100";    // String 객체

        System.out.println(num.equals(str)); // false
    }
}

 

👉  같은 클래스로 파싱 후 비교

public class EqualsExample {
    public static void main(String[] args) {
        Integer num = 100;
        String str = "100";

        System.out.println(num.toString().equals(str)); // true
    }
}
public class EqualsExample {
    public static void main(String[] args) {
        Integer num = 100;
        String str = "100";

        System.out.println(num.equals(Integer.parseInt(str))); // true
    }
}

 


equals()는 같은 자료형이라면, 객체와 기본형의 비교도 가능하다

parseInt는 기본형 int로 값을 반환하는 함수이다.

그런데 위의 예시 코드를 보면 parseInt로 반환된 기본형 int와 객체 Integer가 equals()를 사용한다.

바로 Wrapper 클래스인 Integer가 내부적으로 객체 생성자를 사용해서 오토박싱했기 때문에, 결과적으로는 객체 두 개를 비교하는 코드가 된 것이다.


 

결론❗️

- 객체는 equals()를 사용해서 내용 비교 가능
- 객체와 기본형은 equals() 사용 가능
- 서로 다른 클래스(자료형)의 두 객체는 파싱 이후에 equals() 사용 가능

static 변수

  • 클래스의 인스턴스를 생성하지 않아도 접근 가능
  • 모든 객체가 static 변수를 공유

static 메서드

  • 클래스의 인스턴스를 생성하지 않아도 호출 가능

static 블록

  • 클래스가 메모리에 로드될 때 단 한 번만 실행
class MyClass {
    static {
        System.out.println("클래스가 로드될 때 한 번만 실행됩니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        new MyClass();  // MyClass가 메모리에 로드될 때 static 블록 실행
        new MyClass();	// static 블록 실행 안 함
        new MyClass();	// static 블록 실행 안 함
    }
}
☝️ 위 코드에서 MyClass 객체를 여러 번 생성해도, 문장은 처음 객체를 생성했을 때만 출력된다.
프로그램을 종료하게 되면 JVM이 메모리를 비우고, 재실행될 때 JVM이 다시 메모리에 클래스를 로드하기 때문에 static 블록은 다시 실행하게 된다.

static 클래스

  • 클래스 안에 선언된 클래스(=내부 클래스)만 static 클래스로 지정이 가능하다.
  • 외부 클래스의 인스턴스를 생성하지 않아도 바로 내부 클래스를 사용할 수 있다.
class Outer {
    static class Inner {
        void display() {
            System.out.println("Static 내부 클래스입니다!");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer.Inner obj = new Outer.Inner();  // 바깥 클래스 없이 바로 사용 가능!
        obj.display();
    }
}
☝️ 내부 클래스가 static 클래스가 아니었다면, 내부 클래스를 사용할 때 `new Outer().new Inner();` 와 같은 코드를 작성해야 한다.

 

// Map.Entry가 대표 예시인데 이해 안 됨! -> Map 공부하고 다시 보기

 

 

일반 내부 클래스 vs 정적 내부 클래스

내부 클래스 유형 바깥 클래스 속성 접근 가능 여부
일반 내부 클래스 (static 없음) ✅ 바깥 클래스의 모든 멤버(private 포함)에 접근 가능
정적 내부 클래스 (static 있음) ❌ 바깥 클래스의 인스턴스 변수에는 직접 접근 불가 (객체를 전달해야 함)

 

일반 내부 클래스는 바깥 클래스의 일부분으로 포함되어 있는 클래스이기 때문에, 바깥 클래스의 private 멤버에도 접근이 가능하다.

정적 내부 클래스는 static이 아닌 동적인 멤버에는 직접적으로 접근할 수 없다.

따라서 접근하기 위해서는 개별적인 클래스처럼 바깥 클래스의 객체를 생성하고 객체를 정적 내부 클래스에 전달해주어야 한다.


 

클래스의 로딩 시점

  • 클래스의 객체 생성 시
  • 정적 메서드, 정적 변수에 처음 접근할 때
class Counter {
    static int count = 0;  // 클래스 변수
    
    static {
        System.out.println("Counter 클래스가 로딩되었습니다.");
    }

    void increment() {
        count++;
    }
}

public class Main {
    public static void main(String[] args) {
        // 클래스가 처음 사용될 때 로딩됨
        System.out.println(Counter.count);  // 클래스 변수에 접근
        
        Counter c1 = new Counter();  // 클래스의 인스턴스를 생성
        c1.increment();
        System.out.println(Counter.count);  // 클래스 변수에 접근
    }
}
Counter 클래스가 로딩되었습니다. //static 블록으로 인해 처음 로드될 때 한 번 실행됨
0
1

클래스 로딩 방식

자바는 클래스를 지연 로딩(Lazy Loading)방식으로 로딩한다.

즉, 모든 클래스를 프로그램을 실행하자마자 로딩하는 방식이 아니라, 해당 클래스가 참조되는 순간 메모리에 로드되는 것이다.

그 이후는 해당 클래스가 여러 번 참조되더라도 프로그램을 재실행하지 않는 이상 다시 로딩하지 않는다.

한 번 로드된 클래스는 메모리 캐시에 유지되어 프로그램이 종료되기 전까지 꺼내어 사용할 수 있기 때문이다.

 

정적 메서드, 정적 변수는 로드될 때 method area에 저장되기 때문에 모든 객체가 데이터를 공유하며 상시 접근이 가능하다.

그러나 method area에 동적 데이터에 대한 정보는 선언된 틀만 존재하고, 데이터를 할당할 공간 자체가 없다.

힙에 각 객체의 동적 데이터를 저장함으로써 모든 객체는 동일한 틀을 가지면서도 각기 다른 값을 저장할 수 있는 것이다.

이러한 이유로 static이 아닌, 동적 데이터는 객체가 생성되어 힙에 저장된 순간부터 접근할 수 있다.

문제 상황

package com.example.calculator;
import java.util.Scanner;

public class Calculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int num1 = scanner.nextInt();
        int num2 = scanner.nextInt();
        char calc = scanner.next().charAt(0);
        int result = 0;

        switch(calc){
            case '+':
                result = num1 + num2;
                break;
            case '-':
                result = num1 - num2;
                break;
            case '*':
                result = num1 * num2;
                break;
            case '/':
                result = num1 / num2;
                break;
            case '%':
                result = num1 % num2;
                break;
            default:
                System.out.println("잘못된 연산 기호입니다.");
                break;
        }

        System.out.println(num1 + calc + num2 + "=" + result);
    }
}

 

 

 

 

 

 

 

출력값이 4+7=11로 출력이 돼야 하는데, 54=11로 출력되는 문제 발생


해결 과정

문제 발생 원인으로 추측되는 부분인 값을 입력받는 부분과, 값을 출력하는 코드에 breakpoint를 걸어주고 디버깅을 해봤다.

 

calc 변수에 '+'만 입력되지 않고 숫자 43이 함께 있어서, 아스키코드를 살펴봤을 때 43이 '+'를 의미하고 있었다.

43은 '+'의 아스키 코드값을 표기해주느라 같이 보여주는 것 같다.

4+7+43을 연산해서 54가 출력되었고 =11은 제대로 값이 출력되어 결과물이 54=11이 된 것이다.

 

그렇다면 왜 num1, calc, num2를 다 더하는 연산을 한 것일지 궁금해서, 가장 뒤에 calc를 또 넣어보았다.

System.out.println( num1 + calc + num2 + "=" + result + calc);

 

앞의 num1, calc, num2은 더하기 연산이 된 상태로 출력되었고, 가장 뒤에 있는 calc는 원래 의도대로 '+' 기호로 출력되는 것을 볼 수 있다.

이 결과를 보고 자바는 앞에 문자열 없이 정수형 변수만 출력하는 경우는 각 변수를 이어주는 '+'를 연산 의도로 보고 다 더해서 출력하고,

앞에 문자열이 포함되어 있으면 '+'를 각 변수를 이어주는 역할로 판단해서 연산 기호로 출력해준다는 것을 알 수 있었다.

 


결과

System.out.println("" + num1 + calc + num2 + "=" + result);

 

앞에 의미없는 문자열을 넣어서 함께 출력하니까 제대로 된 결과가 나오는 것을 볼 수 있다!

문제

  1. 컴퓨터가 랜덤으로 영어단어를 선택합니다.
    1. 영어단어의 자리수를 알려줍니다.
    2. ex ) PICTURE = 7자리 ⇒ _ _ _ _ _ _ _
  2. 사용자는 A 부터 Z 까지의 알파벳 중에서 하나를 입력합니다.
    1. 입력값이 A-Z 사이의 알파벳이 아니라면 다시 입력을 받습니다
      • Java 의 Charactor.isLetter() 을 활용해보세요
    2. 입력값이 한 글자가 아니라면 다시 입력을 받습니다
    3. 이미 입력했던 알파벳이라면 다시 입력을 받습니다.
    4. 입력값이 정답에 포함된 알파벳일 경우 해당 알파벳이 들어간 자리를 전부 보여주고, 다시 입력을 받습니다.
      1. ex ) 정답이 eyes 인 경우에 E 를 입력했을 때
        1. _ _ _ _ → E _ E _
    5. 입력값이 정답에 포함되지 않은 알파벳일 경우 기회가 하나 차감되고, 다시 입력을 받습니다.
  3. 사용자가 9번 틀리면 게임오버됩니다.
  4. 게임오버 되기 전에 영어단어의 모든 자리를 알아내면 플레이어의 승리입니다.

알고리즘 설계

  1. 영단어 목록을 String 타입 배열 `words`에 저장
  2. Random()을 사용해서 임의의 영단어 하나를 String 타입 변수 `correctWord`에 저장
  3. length()로 문자열의 길이를 구해서 길이만큼 언더바 출력
  4. 반복문
    • `inputLetter`가 2글자 이상 입력인 경우 try-catch문으로 예외 처리
      • try -> Scanner로 char 타입 문자 입력받아서 char 타입 변수 `inputLetter`에 저장
      • catch -> 잘못된 입력 안내 후 continue
    • 조건문
      • Charactor.isLetter()로 알파벳 입력인지 확인 후 알파벳이 아니면 continue
      • `inputLetterLog`에 이미 존재하는 문자인지 반복하면서 확인 후, 이미 존재하면 continue
      • else -> char 타입 배열 `inputLetterLog`에 저장 
    • `correctWord`와 `inputLetter`을 charAt으로 반복하면서 확인
      • 동일한 문자가 있는 위치는 해당 문자 출력 / 그 외의 문자는 언더바 출력
      • 동일한 문자가 없으면 count++
    • count가 9인 경우 Game Over 출력 후 break

변경 사항

1. Scanner 객체는 char 타입으로 입력 ❌

`scanner.next().charAt(0)`으로 코드를 작성하면 문자열로 입력받아도 결과적으로 한 글자만 입력받은 것처럼 사용할 수 있다.

하지만 문제 자체가 한 글자가 아닌 경우 다시 입력받도록 하는 시스템이기 때문에, 문자열로 입력받고 조건문을 추가해야겠다.

 

2. inputLetterLog에 이미 존재하는 문자인지 확인 후 continue 대신 flag 사용

문자를 맞히기 위한 반복문 안에서 inputLetter와 inputLetterLog를 순서대로 비교하는 반복문을 중첩으로 사용했다.

중첩된 반복문 안에서 continue를 사용하면 inputLetterLog의 다음 인덱스로 넘어가는 로직이 되기 때문에, 내 의도대로 다음 입력을 받기 위한 continue가 될 수 없다. 

따라서 inputLetterLog와 inputLetter의 데이터 중복 여부를 알 수 있는 flag를 만들고, 이 flag값을 조건으로 줘서 입력값을 inputLetterLog에 저장하도록 수정하자.

 

3. 시도 횟수를 나타내는 count 변수를 반복을 진행하면서 증가시키는 대신 감소시키기

처음 초기화할 때 가능한 시도 횟수를 지정해주는 게, 추후 시도 횟수를 변경하는 상황에서 수정이 편리할 것 같다.

 

4. underBarCnt , letterCnt 변수 추가

`correctWord`의 어떤 위치에도 `inputLetter`값이 없는 상황을 판단하기 위해

`correctWord`와 `inputLetter`을 charAt으로 반복하는 과정에서, 언더바를 출력할 때 변수 `underBarCnt`를 증가시킨다.

그리고 `correctWord`의 문자열 길이와 `underBarCnt`가 같으면 시도 횟수를 증가시킨다.

 

정답을 맞힌 경우 시스템을 종료하기 위해

`correctWord`와 `inputLetter`을 charAt으로 반복하는 과정에서, 문자를 출력할 때 변수 `letterCnt`를 증가시킨다.

 

5. count 변수명을 tryCount로 변경

underBarCnt와의 혼동 방지

 

6. 정답을 맞힌 경우 시스템을 종료하는 코드 추가

모든 인덱스의 문자를 맞혀도 계속 진행되는 논리적 오류 발생

추가한 `letterCnt` 변수의 값이 정답 문자열의 길이와 같으면 종료하는 코드를 추가하자

 


최종 실행 과정

1. 영단어 목록을 String 타입 배열 `words`에 저장

2. Random()을 사용해서 임의의 영단어 하나를 String 타입 변수 `correctWord`에 저장

Random random = new Random();
String correctWord = words[random.nextInt(15)];

 

 

3. length()로 문자열의 길이를 구해서 길이만큼 언더바 출력

for (int i = 0; i < correctWord.length(); i++) {
        System.out.print("_");
}

 

4. 반복문 

  •  조건문
    • `inputLetter`가 2글자 이상 입력인 경우 continue
    • Charactor.isLetter()로 알파벳 입력인지 확인 후 알파벳이 아니면 continue
    • `inputLetterLog`에 이미 존재하는 문자인지 반복하면서 확인 후, 이미 존재하면 디폴트가 false였던 `isDuplicate` 변수를 true로 변경
    • `isDuplicate`가 false인 상황에서만 `inputLetterLog`에 입력값 저장 
String inputLetter = scanner.next();

// 입력값이 한 글자가 아니면 다시 입력
if (inputLetter.length() != 1) {
	continue;
}

// 입력값이 알파벳이 아니면 다시 입력
if (!Character.isLetter(inputLetter.charAt(0))) {
	continue;
}

boolean isDuplicate = false;

// 입력값이 inputLetterLog에 있는지 확인
for (String string : inputLetterLog) {
	if (string.equals(inputLetter)) {
		isDuplicate = true;
	}
}

// 이전에 입력하지 않은 값만 저장
if(!isDuplicate){
	inputLetterLog.add(inputLetter);
}
  • `correctWord`와 `inputLetter`을 charAt으로 반복하면서 확인
    • 동일한 문자가 있는 위치는 해당 문자 출력
    • 그 외의 문자는 언더바 출력 후 underBarCnt++
    • `correctWord`의 길이와 `underBarCnt`가 같으면(어떤 위치에도 입력한 문자가 없으면) tryCount--
for(int i=0; i<correctWord.length();i++){
	if(correctWord.charAt(i)==inputLetter.charAt(0)){
		System.out.print(inputLetter);
		letterCnt++;
	}
	else{
		System.out.print("_");
		underBarCnt++;
		if(correctWord.length()==underBarCnt){
			tryCount--;
		}
	}
}
  • `tryCount`가 0인 경우 "Game Over" 출력 후 break
// tryCount가 0인 경우 Game Over
if(tryCount==0){
	System.out.println("\nGame over!");
	break;
}
  • `letterCnt`가 `correctWord`의 길이와 값이 같은 경우 "You are win"과 `correctWord` 출력 후 break
// correctWord의 모든 인덱스를 맞힌 경우 시스템 종료
if(correctWord.length()==letterCnt){
	System.out.println("\n" + correctWord);
    System.out.println("You are win!");
    break;
}

결과

 

 

☝️ if문 조건식의 반환값이 boolean인 경우
값이 true면 `!true`가 돼서 실행 X 
값이 false면 `!false`, 즉 `true`가 돼서 실행 O

☝️ Character.isLetter(char)
값이 문자면 true
문자가 아니면 false 반환

 

스트림

  • 데이터를 효율적으로 처리할 수 있는 흐름
  • 선언형 스타일로, 가독성 뛰어남
  • 데이터 준비 -> 중간 연산 -> 최종 연산 순으로 처리
  • 컬렉션 (List, Set 등)과 함께 자주 활용

for문 vs 스트림

`arrayList`에 저장한 리스트 데이터를 곱셈 연산한 결과값을 저장하는 과정

// for문
public class Main {
    public static void main(String[] args) {
    
        // ArrayList 선언
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ for 명령형 스타일: 각 요소 * 10 처리
        List<Integer> ret1 = new ArrayList<>();
        for (Integer num : arrayList) {
            int multipliedNum = num * 10; // 각 요소 * 10
            ret1.add(multipliedNum);
        }
        System.out.println("ret1 = " + ret1); 
    }
}
// 스트림
public class Main {
    public static void main(String[] args) {

        // ArrayList 선언
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ 스트림 선언적 스타일: 각 요소 * 10 처리
        List<Integer> ret2 = arrayList.stream()
					.map(num -> num * 10)
					.collect(Collectors.toList());
        System.out.println("ret2 = " + ret2);
    }
}

 

☝️ ArrayList를 List 인터페이스로 받는 이유

List 인터페이스로 구현체(ArrayList)를 받으면 나중에 다른 구현체(LinkedList, Vector) 로 변경할 때 코드 수정을 최소화

 


스트림 처리 단계

단계 설명 주요 API
1. 데이터 준비 컬렉션을 스트림으로 변환 `stream()`, `parallelStream()`
2. 중간 연산 등록 (즉시 실행 X) 데이터 변환 및 필터링 `map()`, `filter()`, `sorted()`, `distinct()`
3. 최종 연산 최종 처리 및 데이터 변환 `collect()`, `forEach()`, `count()`, `reduce()`

 


 

스트림 생성 방법

1. 스트림 생성 

// 컬렉션 기반 스트림
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> stream = list.stream();

// 배열 -> 스트림
String[] arr = {"apple", "banana", "cherry"};
Stream<String> stream = Arrays.stream(arr);

// 여러 값 -> 스트림
Stream<String> stream = Stream.of("apple", "banana", "cherry");
  • stream() : 스트림 생성
  • Stream.of() : 여러 값으로 스트림 직접 생성

 

스트림 중간 연산

  • 스트림 변환 작업
  • 실행 전까지 수행되지 않음

filter() - 조건을 만족하는 요소만 필터링

List<String> list = Arrays.asList("apple", "banana", "cherry");
list.stream()
    .filter(s -> s.startsWith("b"))
    .forEach(System.out::println);  // 출력: banana

 

map() - 각 요소를 변환

List<String> list = Arrays.asList("apple", "banana", "cherry");
list.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);
// 출력: APPLE, BANANA, CHERRY

 

sorted() - 정렬

List<String> list = Arrays.asList("cherry", "banana", "apple");
list.stream()
    .sorted() 			// sorted(Comparator.reverseOrder()) 내림차순 정렬
    .forEach(System.out::println);
// 출력: apple, banana, cherry (오름차순 정렬)

 

distinct() - 중복 제거

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 4);
numbers.stream()
    .distinct()
    .forEach(System.out::println);
// 출력: 1, 2, 3, 4

 

limit() - 특정 개수만 출력

Stream.of(1, 2, 3, 4, 5)
    .limit(3)
    .forEach(System.out::println);
// 출력: 1, 2, 3

 

skip() - 앞에서 n개 요소 건너뛰기

Stream.of(1, 2, 3, 4, 5)
    .skip(2)
    .forEach(System.out::println);
// 출력: 3, 4, 5

 


 

스트림 최종 연산

  • 결과 반환하는 연산

forEach() - 모든 요소 처리

Stream.of("apple", "banana", "cherry")
    .forEach(System.out::println);

 

collect() - 결과를 컬렉션으로 변환

List<String> list = Stream.of("apple", "banana", "cherry")
    .collect(Collectors.toList());

2025.03.07 - [언어/Java] - Collectors 클래스

 

count() - 요소 개수 반환

long count = Stream.of(1, 2, 3, 4, 5)
    .count();
System.out.println(count);  // 출력: 5

 

reduce() - 요소를 누적하여 결과 반환

int sum = Stream.of(1, 2, 3, 4, 5)
    .reduce(0, Integer::sum);      // 초기값 0
System.out.println(sum);  // 출력: 15

 

 


병렬 스트림 (Parallel Stream)

  • 스트림은 기본적으로 순차적으로 처리
  • 병렬 스트림을 사용하면 멀티코어 CPU를 활용한 병렬 처리 가능
  • 병렬로 실행되므로 출력 순서 일정하지 않을 수 있음
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.parallelStream()
    .forEach(System.out::println);

익명 클래스

  • 이름이 없는 클래스
  • 별도의 클래스 파일을 만들지 않고, 코드 내에서 일회성으로 정의해서 사용
  • 인터페이스 / 일반 클래스 / 추상 클래스의 구현과 상속을 위해 익명 클래스 활용 가능
    • 람다는 인터페이스 구현을 위해 익명 클래스 활용
    • 아래 코드는 인터페이스 객체를 생성하는 것이 아니라, 인터페이스를 구현한 익명 클래스의 객체를 생성
public interface Calculator {

    int sum(int a, int b);
}
public class Main {

    public static void main(String[] args) {
		    // ✅ 익명 클래스 활용
        Calculator calculator1 = new Calculator() {
            @Override
            public int sum(int a, int b) {
                return a + b;
            }
        };       
        int ret1 = calculator1.sum(1, 1);
        System.out.println("ret1 = " + ret1);
    }
}

 


 

람다 (Lambda)

  • 자바 8에서 도입
  • 익명 클래스를 더 간결하게 표현하는 문법
  • 함수형 인터페이스(=하나의 추상 메서드만 가지는 인터페이스)로 구현
  • 컴파일러가 내부적으로 람다식을 익명 클래스로 변환

기본 구조

(매개변수들) -> {실행할 코드}

 

 

 

  • 매개변수가 하나인 경우는 괄호 생략 가능
  • 단일 표현식이라면 중괄호 생략 가능

예시

(int a, int b) -> { a + b; }

int a -> a + 1;

 

 

메서드 참조 형식으로 람다 표현식 변환

`ClassName::methodName`

// 람다 표현식
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(s -> System.out.println(s));

// 메소드 참조
list.forEach(System.out::println);

 

 

주의 사항

1) 람다식 내의 this는 람다를 정의한 객체가 아니라, 람다의 외부 클래스의 인스턴스

public class LambdaTest {
    public void test() {
        Runnable r = () -> System.out.println(this); //this는 LamdaTest의 인스턴스
        r.run();
    }
}

 

2) 값의 변경이 없는 외부 변수만 람다식에서 사용 가능 (외부 변수 캡처)

int a = 10;
Runnable r = () -> System.out.println(a);  // a는 final이어야만 사용 가능
r.run();

 


 

함수형 인터페이스

  • annotation `@FunctionalInterface` 사용해서 함수형 인터페이스로 구현하는 것 권장
  • annotation으로 구현하면, 컴파일러가 하나의 추상 메서드만 가지도록 강제함
  • 람다 표현식을 쓸 때, 추상 메서드가 2개 이상이면 람다가 어떤 메서드를 구현하는지 모호해지기 때문
  • 디폴트 메서드, static 메서드는 여러 개 들어가도 상관 없음
@FunctionalInterface // ✅ 함수형 인터페이스 선언
public interface Calculator {

    int sum(int a, int b); // ✅ 오직 하나의 추상 메서드만 선언해야합니다.
}
public class Main {

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

        // ✅ 람다식 활용
        Calculator calculator2 = (a, b) -> a + b;
        int ret2 = calculator2.sum(2, 2);
        System.out.println("ret2 = " + ret2);
    }
}

 

 

함수형 인터페이스별 람다식 활용 예시

Predicate 인터페이스

  • 입력값을 받아서 boolean값을 반환하는 함수형 인터페이스
  • 필터링 작업에서 유용
List<String> list = Arrays.asList("apple", "banana", "cherry");
Predicate<String> startsWithB = s -> s.startsWith("b"); // startsWith는 String의 메서드

list.stream()
    .filter(startsWithB)
    .forEach(System.out::println);  // 출력: banana

 

Consumer 인터페이스

  • 입력값을 받아서 처리 후 결과는 반환하지 않는 함수형 인터페이스
List<String> list = Arrays.asList("apple", "banana", "cherry");
Consumer<String> print = s -> System.out.println(s);

list.forEach(print);  // 출력: apple, banana, cherry

 

Function 인터페이스

  • 입력값을 받아서 변환된 값을 반환하는 함수형 인터페이스
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5));  // 출력: 25

 

Supplier 인터페이스

  • 입력값이 없고, 결과값만 제공 (랜덤값 생성, 객체 생성)
// 값 반환
Supplier<String> supplier = () -> "Hello, Supplier!";
System.out.println(supplier.get());  // 출력: Hello, Supplier!

// 객체 생성
Supplier<Integer> randomNumberSupplier = () -> (int) (Math.random() * 100); // 0~99 사이의 랜덤 숫자 생성
System.out.println(randomNumberSupplier.get());  // 랜덤 값 출력

'언어, 프레임워크 > Java' 카테고리의 다른 글

자바 클래스 로딩과 static  (1) 2025.03.01
스트림 (Stream) 알아보기  (0) 2025.02.27
[Java] 기본형과 래퍼형 성능 비교  (1) 2025.02.25
Wrapper 클래스  (0) 2025.02.25
Optional (null 처리)  (0) 2025.02.07