📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   제네릭을 사용하지 않는 경우_ 585p

 

• 객체를 저장하는 클래스

class Onion { }

class Goods1 {
    private Onion onion = new Onion();
    public Onion get() {
        return onionl
    }
    
    public void set(Onion onion) {
        this.onion = onion;
    }
}
Goods1 g1 = new Goods1();
g1.setOnion(new Onion());
Onion onion = (Onion)g1.get();
Onion 클래스와 Onion 클래스를 담을 수 있는 Goods1 클래스가 있다. Goods1은 Onion 클래스만 담을 수 있으니 Onion이 아닌 다른 것을 담고 싶어지면 새로운 클래스를 생성해야한다. 하지만 어떤 코드는 하나의 클래스에 다양한 클래스를 담고 싶을 수도 있다.

 

 

• Object로 정의한 클래스

class Onion {}
class Radish {}

class Goods {
    private Object object = new Object();
    public Object get() {
        return object;
    }
    public void set(Object object) {
        this.object = object;
    }
}
Goods g1 = new Goods();
g1.set(new Onion());
Onion onion = (Onion)g1.get();

Goods g2 = new Goods();
g2.set(new Radish());
Radish radish = (Radish)g2.get();
Onion과 Radish는 모두 Object 타입의 자식이다. 왜냐하면 자바에서 모든 클래스의 최상위 부모는 Object를 상속받기 때문이다. 예제 코드는 set( ) 메서드를 통해 객체를 저장하고, get( ) 메서드를 통해 저장된 값을 꺼낸다.

맨 처음 제너릭 없이 객체를 저장한 클래스의 예시를 살펴보자. 객체를 저장할 때 g1.setOnion이라고 적었다. 하지만 Object로 정의한 코드를 살펴보면 g1.set(new Onion());으로 Onion이 Object 타입으로 저장되어있다. 이 점에서 두 코드의 차이를 확인할 수 있다.

Object 타입으로 저장했으니 get에서도 Object 타입으로 꺼낼 것 같지만 사실 그렇지 않다. 꺼낼 때는 객체를 사용하기 위해 Onion과 Radish 객체로 다운 캐스팅해야 한다. 만약 잘못 캐스팅이 된 경우 ClassCastException 실행 예외가 발생하며 프로그램이 종료된다. 이를 약한 타입 체크라고 한다. 

 

 

 


 

 

 

✔️   제네릭 문법_ 591p

 

• 제네릭

: 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법

 

• 강한 타입 체크

: 잘못된 캐스팅을 할 때 문법 오류를 발생시켜 잘못된 캐스팅으로 발생할 수 있는 문제를 사전에 예방

 

• 제네릭 타입 변수의 관례적 표기 및 의미

T 타입(Type)
K 키(Key)
V 값(Value)
N 숫자(Number)
E 원소(Element)

 

class KeyValue<K, V> {
    private K key;
    private V value;
    public K getKey() {
        return key;
    }
    
    public void setKey(K key) {
        this.key = key;
    }
    
    public void setValue(V value) {
        this.value = value;
    }
}
KeyValue<String, Integer> kv1 = new KeyValue<>();
kv1.setKey("사과");
kv1.setValue(1000);
String key1 = kv1.getKey();
int value1 = kv1.getValue();
System.out.println("key: " + key1 + "value: " + value1);

KeyValue<String, Void> kv2 = new KeyValue<>();
kv2.setKey("키 값만 사용");
String key2 = kv2.getKey();
System.out.println("key: " + key2);
kv1과 kv2의 타입이 다르게 설정할 수 있는 이유는 제네릭 클래스는 클래스를 정의하는 시점에 타입을 지정하는 것이 아닌 객체를 생성하는 시점에 타입을 지정하기 때문이다. 그래서 하나의 제네릭 클래스로 다양한 타입의 값을 저장 및 관리할 수 있는 객체를 생성할 수 있는 것이다.

Object 타입의 필드와 제네릭의 가장 큰 차이점은 setter 메서드를 사용할 때 입력 타입을 정확히 확인할 수 있고, 다운캐스팅을 하지 않아도 해당 타입으로 알아서 리턴된다는 것이다. 만약 kv2의 setKey에 문자열이 아닌 숫자를 대입하거나 get할 때 String이 아닌 다른 타입으로 꺼내는 경우 강한 타입 체크로 인해 문법 오류 발생이 일어난다. 

코드를 살펴보자 KeyValue<String, Integer> kv1 = new KeyValue<>();에서  KeyValue<String, Integer> kv1 = new KeyValue<String, integer>();으로 작성해도 된다. 그러나 제네릭 타입은 항상 왼쪽 항과 동일하기 때문에 생략할 수 있다. 위의 코드에서는 제네릭 타입 변수를 2개 가진 경우로 작성되었는데, 만약 하나의 타입만 필요한 경우 Void를 적으면 된다. 

 

 

 


 

 

 

✔️   제네릭 메서드_ 597p

 

• 제네릭 메서드

: 일반 클래스 내부의 특정 메서드만 제네릭으로 선언한 경우

class GenericMethods{
    public <K,V> method1(K k, V v) {
        System.out.println(k + ":" + v);
    }
}
GenericMethods gm = new GenericMethods();

gm.<String, Integer>method1("국어", 80);
gm.method1("국어", 80);
일반 클래스 GenericMethods 내에 제네릭 메서드로 2개의 제네릭 타입 변수를 사용하고 있다. 참조객체 gm을 활용한 코드를 보면 실제 제네릭 타입이 어떤 타입이지 정의해둔 코드와 생략한 코드를 볼 수 있다. 입력매개변수를 통해 제네릭 타입이 어떤 타입인지 알 수 있기 생략해도 무관하다. 다만 입력매개변수 자체가 없거나 입력매개변수에서 제네릭 타입 변수를 사용하지 않는 public static <T> void doSomething() 이런 예시 코드의 경우 입력값으로 예측할 수 없기 때문에 생략이 불가하다.

 

 

• 제네릭 Object 타입

public <T> void method(T t) {
    System.out.println(t.equals("안녕"));
}
위의 코드는 타입에 제한이 없는 코드다. 이런 경우 컴파일 시 T를 Object로 간주하여 Object에 정의된 메서드 hashcode, eqauls, toString 등만 사용가능하다. 이렇게 되면 제네릭 메서드의 활용 범위가 매우 좁아지게된다.

아래에 정리되겠지만, 그래서 이런 경우 더 넓게 활용하기 위해 제네릭 타입의 범위를 제한한다. 현재 코드에서 t.equals가 아닌 t.length인 String 메서드를 사용하고 싶은 경우 <T>를 <T extends String>으로 변경하면 String 클래스에 있는 메서드도 사용가능해진다.

 

 

 


 

 

 

✔️   제네릭 타입 범위 제한_ 602p

 

• 제네릭 클래스 타입 범위 제한

class A {}
class B extends A {}
class C extends B {}

class D <T extends B> {
    private T t;
    public T get() {
        return t;
    }
    public void set(T t) {
        this.t = t;
    }
}
D<B> d2 = new D<>();
D<C> d3 = new D<>();
D d4 = new D();

d2.set(new B());
d2.set(new C());

d3.set(new C());

d4.set(new B());
d4.set(new C());
제네릭 클래스의 타입 제한하는 방법은 제네릭 타입으로 대입될 수 있는 최상위 클래스를 extends 키워드와 함께 정의하는 것이다. 그래서 <제네릭 타입 변수 extends 상위 클래스>의 형태로 사용하며 이때 extends는 상속하라는 의미가 아닌 최상위 클래스 혹은 인터페이스로 지정한다라는 의미로 가진다.  코드에서는 <T extends B> 로 작성했으며 A <- B <- C의 상속 구조를 갖고 있다. 

제네릭 클래스 D를 살펴보자. 클래스 D는 클래스 B 또는 클래스 B의 자식 클래스만 오도록 제한했다. 그렇다는 것은 A는 제한에 걸려 D<A> d1 = new D<>(); 객체 생성이 불가하다는 의미이다. d2는 제네릭 타입으로 B를 지정하여 B와 C 객체가 입력가능하다. 그런데 d3는 C 타입으로 지정했기 때문에 C의 상위클래스 B객체를 입력할 수 없다. d4는 타입을 지정하지 않았지만 모든 타입의 최상위 클래스가 <B> 타입이므로 제네릭 타입으로 대입되었다. 그래서 d4는 B와 C 객체가 입력가능하다. 

 

 

 

• 제네릭 메서드 타입 범위 제한

class A {}
class B extends A {}
class C extends B {}
class D extends C {}

class Goods <T> {
    private T t;
    public T get() {
        return t;
    }
    public void set(T t) {
        this.t = t;
    }
}
void methods(Goods<A> g) {}
void methods(Goods<?> g) {}
void methods(Goods<? extends B> g) {}
void methods(Goods<? super B> g) {}
extends는 아까와 같이 해당 클래스와 자식 클래스만 가능하다. 그럼 코드에서 B라고 적혀있으니 B 클래스와 B의 자식 클래스인 객체가 들어갈 것이다. super는 해당 클래스와 부모 클래스인 객체만 가능하므로  B 또는 B의 부모 클래스만 가능할 것이다.

상속구조를 살펴보면 A <- B <- C <- D가 된다. Goods<A>는 A인 객체만 가능하고 Goods<?> 모든 타입인 객체가 가능하기에 A, B, C, D 타입이 모두 ? 자리에 올 수 있다. Goods<? extends B>는 B 또는 B의 자식 클래스인 객체만 가능하기에 B, C, D 타입이 가능하며 Goods<? super B>는 B 또는 B의 부모 클래스인 객체만 가능해서 A, B가 제네릭 타입으로 지정될 수 있다. 

 

 

 


 

 

 

✔️   제네릭의 상속_ 612p

 

• 제네릭 클래스의 상속

class Parent {
    <T extends Number void print(T t) {
        System.out.println(t);
    }
}

class Child extends Parents {
}
Parent p = new Parent();
p.<Integer>print(10);
p.print(10);

Child c = new Child();
c.<Double>print(5.8);
c.print(5.8);
부모클래스가 제네릭 클래스인 경우 자식 클래스도 제네릭 클래스가 되어 제네릭 타입 변수를 자식 클래스가 그대로 물려받게 된다. 또한 자식 클래스에서 제네릭 타입 변수를 추가해 정의할 수도 있다. 그러므로 자식 클래스의 제네릭 타입 변수의 개수는 언제나 부모와 같거나 그 이상일 것이다.

코드를 보면 부모 클래스에 최상위 제네릭 타입이 Number로 제한되어 있다. Number 클래스는 boolean과 char을 제외한 Byte, Short, Integer, Long, Float, Double이 있다. 자식 클래스는 Number 클래스 내에 있는 6가지 자료형을 사용할 수 있으며 부모 클래스가 가지고 있는 print( )메서드도 이용할 수 있다.

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   동기화_ 539p

 

• 동기화

: 하나의 작업이 완료된 후 다른 작업을 수행하는 것

 

• 비동기화

: 하나의 작업 명령 이후 완료 여부와 상관없이 바로 다른 작업 명령을 수행하는 것

 

• 메서드 동기화

: 2개의 쓰레드가 동시에 메서드를 실행할 수 없다는 것

접근지정자 synchronized 리턴타입 메서드명(입력매개변수){ }
메서드를 동기화할 때는 동기화하고자 하는 메서드의 리턴 타입 앞에 synchronized 키워드만 넣으면 된다. 이렇게 되면 동시에 2개의 쓰레드에서 해당 메서드를 실행할 수 없게된다. 

 

 

 

• 블록 동기화

: 2개의 쓰레드가 동시에 해당 블록을 실행할 수 없다는 것

어떤 메서드 {
    synchronized (임의의 객체) { }
}
전체 메서드를 동기화하지 않고 원하는 부분만 동기화할 수 있게 한다. 필요한 코드만 동기화를 진행하기 때문에 메서드 동기화보다 더 정밀하게 코드를 설정할 수 있다. 여기서 임의의 객체란 내가 선택한 어떤 객체로 잠금을 설정하겠다는 의미로 생각하면 되고 주로 this를 사용해서 현재 인스턴스 객체로 잠금을 설정한다. 그러면 이 잠근 객체와 같은 객체를 가지는 블록들은 동시 사용이 불가하다.

 

 

 

• 메서드 동기화와 블록 동기화

class MyData {
    
    Object keyObject = new Object();
    synchronized void abc() {
    ...
    }
    
    synchronized void bcd() {
    ...
    }
    
    synchronized void cde() {
    ...
    }
    
    void def() {
        synchronized (keyObject) {
        ...
        }
    }
    
    void efg() {
        synchronized (keyObject) {
        ...
        }
    }
}
abc와 bcd는 동기화 메서드이다. cde와 def 그리고 efg는 동기화 블록을 가지고 있다. 여기서 주의 깊게 봐야할 점은 어떤 메서드가 동기화 메서드인지 동기화 블록인지가 아니다. 각 메서드와 블록이 어떤 키로 잠금이 되어있는지를 봐야한다. abc와 bcd 그리고 cde는 this 객체로 잠금이 되어있다. 그러면 abc,bcd,cde는 동시에 사용할 수 없다. def와 efg는 keyObject로 잠금이 되어있다. 그러면 def와 efg는 동시에 사용할 수 없다.

여기서 각 키는 this와 keyObject로 나뉜다. 키가 다르다면? 동시에 사용가능하다. abc는 cde와 동시에 사용 불가하지만 abc가 def와 동시 사용은 가능할 것이다. 동기화에서 중요한 것은 어떤 객체로 잠금을 설정했는지다.

아마 같은 키를 가지는 메서드를 동시에 사용 불가한 이유는 키가 하나이기 때문일 것이다. 처음에 abc가 키를 선점하고 해야할 활동을 마치면 abc는 키를 반납할 것이다. 이때 키는 다음 순서인 bcd로 넘어가지 않는다. bcd와 cde가 반납된 키를 가지기 위해 서로 경쟁할 것이고 키를 선점한 메서드가 먼저 활동하게 된다.

 

 

 

 


 

 

 

 

✔️   쓰레드 6가지 상태_ 552p

 

• NEW 상태

: Thread 객체를 new 키워드를 이용해 생성한 시점인 실행 이전 상태

Thread thread = new Thread() {
    @Override
    public void run() {
        for(long i = 0; i < 1000000000L; i++) {}
    }
}

 

 

• RUNNABLE 상태

: start( ) 메서드를 호출한 상태, 실행 상태로 언제든지 갈 수 있는 상태

thread.start();

 

 

• TERMINATED 상태

: run( ) 메서드가 완전히 종료된 실행을 마친 상태

try {
    myTread.join();
} catch (InterruptedException e) {}

 

 

• TIMED_WAITING 상태

: 정적 메서드인 Thread.sleep 또는 인스턴스 메서드인 join이 호출된 상태

 Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2000); // 2초 동안 기다림
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
Thread t2 = new Thread(() -> {
            try {
                t1.join(1000); // 최대 1초 동안 t1을 기다림
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
위의 코드는 Runnable 상태에서 일시정지 상태로 전환되는 TIMED_WAITING 상태이다. TIMED_WAITING 상태는 주로 sleep(long millis)를 호출하거나 join(long millis)가 호출됐을 때다. 

Thread.sleep( )은 이 메서드를 호출한 쓰레드를 일시 정지하라는 의미로 일시정지 시간 동안 CPU를 어떤 쓰레드가 사용하든 상관하지 않는다. 쓰레드 객체.join( ) 메서드는 특정 쓰레드 객체에게 일정 시간 동안 CPU를 할당하라는 의미다.

쓰레드 t1은 sleep 메서드로 일시정지가 되었고 2초의 시간동안 어떤 쓰레드가 CPU를 사용하든 신경쓰지 않을 것이다. 그러나 쓰레드 t2는 join 메서드를 사용했기에 t2가 잠시 멈춘 1초 동안 t1 쓰레드가 CPU를 사용하도록 했다.

 

 

 

• BLOCKED 상태

: 동기화 메서드 또는 동기화 블록을 실행하고자 할 때 이미 다른 쓰레드가 해당 영역을 실행하고 있는 경우 발생.

Thread t1 = new Thread("thread1") {
    public void run() {
        mc.syncMethod();
    };
};  

Thread t2 = new Thread("thread2") {
    public void run() {
        mc.syncMethod();
    };
}; 

Thread t3 = new Thread("thread3") {
    public void run() {
        mc.syncMethod();
    };
};
쓰레드 t1이 먼저 실행하고 있을 때, t1이 실행을 완료하고 해당 동기화 영역의 열쇠를 반납할 때까지 기다려야한다. 이때 이런 상황을 BLOCKED 상태라고 한다. 

t1이 반납을 하게 되면 그 다음 순서인 t2가 키를 가질 것 같지만 사실 그렇지 않다. 키가 반납된 순간부터 t2와 t3는 서로 키를 가지기 위해 경쟁할 것이다. 그렇게 먼저 키를 가지게 된 쓰레드가 t1 다음으로 실행하게 될 것이다.
void startAll() {
    t1.start();
    t2.start();
    t3.start();
}
startAll 메서드에서 t1, t2, t3 쓰레드가 동시에 실행하려고 한다. 그러면 당연하게 t1, t2, t3는 경쟁할 것이고 키를 차지한 쓰레드 먼저 실행되며 나머지 두 쓰레드는 BLOCKED 상태가 될 것이다.

 

 


• WAITING 상태

: 쓰레드가 무한정 기다리고 있는 상태

Thread t = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

 

시간을 지정하지 않은 join 메서드 또는 object.wait 메서드를 사용하게 되면 WAITING 상태가 된다. join은 다른 쓰레드가 끝날때까지 기다려야하고 wait는 notify( )로 깨울 수 있다. 

lock 객체로 잠금을 건 쓰레드 t는 lock.wait( )으로 lock을 잠깐 반납했다. 언제까지 반납할지 시간을 지정하지 않았기 때문에 notify( )로 호출하여 WAITING 상태에서 빠져나오도록 해야한다.
synchronized (lock) {
    lock.notify(); 
        }

 

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   프로그램,  프로세스, 쓰레드_ 511p

 

• CPU 

: 연산을 수행함으로써 실제 프로그램을 실행하는 장치로, 가장 빠른 속도로 동작.

 

 하드디스크

: 데이터의 저장 역할을 수행하며, 상대적으로 가장 낮은 속도로 동작.

 

 메모리

: 저장된 데이터를 CPU로 보내는 역할을 하며, CPU와 근접한 속도로 동작.

 

 프로그램

: 하드디스크에 저장된 파일들의 모임.

 

프로세스

: 메모리상에 로딩된 프로그램으로 메모리는 필요한 부분만을 동적으로 로딩.

 

 멀티프로세스

: 하나의 응용 프로그램에 대해 동시에 여러 개의 프로세스를 실행할 수 있게 하는 기술.

 

 쓰레드

: 여러 개의 작업이 동시에 수행되도록 하기 위해서 한정된 코어의 수를 갖는 CPU에 여러 개의 작업을 나눠 사용하며, CPU를 사용하는 최소 단위.

 

 단일 쓰레드

public class ThreadExample01 {
    public static void main(String[] args) {
    
        for (int i = 1; i <= 5; i++) {
            System.out.println("작업 1 - i: " + i);
            try {
                Thread.sleep(500); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        for (int j = 1; j <= 5; j++) {
            System.out.println("작업 2 - j: " + j);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
해당 코드는 단일 쓰레드를 나타낸 것이다. 첫번째 for문과 두번째 for문이 번갈아가며 나오길 원했지만 단일 쓰레드로 첫번째 for이 모두 수행된 다음 두번째 for문이 수행된다.

 

 

 

 멀티쓰레드

: 하나의 프로세스 안에서 여러 개의 쓰레드가 있는 것

 

 

extends Thread + 3개의 쓰레드

class Task1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("작업 1 - i: " + i);
            try {
                Thread.sleep(500); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Task2 extends Thread {
    @Override
    public void run() {
        for (int j = 1; j <= 5; j++) {
            System.out.println("작업 2 - j: " + j);
            try {
                Thread.sleep(500); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample02 {
    public static void main(String[] args) {
        Task1 t1 = new Task1(); 
        Task2 t2 = new Task2(); 

        t1.start();
        t2.start(); 
    }
}
쓰레드에는 동시성(concurrency)과 병렬성(parallelism)이 있다. 동시성은 처리할 작업의 수가 CPU의 코어 수보다 많을 때 각 작업의 쓰레드의 요청 작업을 번갈아가면서 실행한다. 매우 짧은 간격으로 교차 실행하기 때문에 사용자는 두 작업이 마치 동시에 실행되는 것처럼 보인다. 병렬성은 CPU의 코어 수가 작업 수보다 많을 때로 각각의 작업을 각각의 코어에 할당해 동시에 실행하도록 한다. 

여러 작업이 있고 여러 CPU가 있다고 가정하자. 그러면 개발자는 어느 작업을 멀티 쓰레드로 진행할건지 코드로 적어두면 된다. 그 다음 JVM이 운영체제로 해당 쓰레드를 넘기고 운영체제는 각 쓰레드를 어느 CPU에 배분할지 알아서 정한다. (보통 배분은 스케줄러가 한다)

위의 코드를 살펴보자. extends Thread로 Thread를 상속받았고 Thread 내부엔 run( )메서드가 있다. 그래서 Thread를 상속받은 클래스에 run( ) 메서드 안에 어떤 동작을 수행할지 재정의하면 오버라이딩되어 Thread에서 재정의한 동작을 수행하게 된다.

쓰레드에서 특이한 점이 있다. 메서드 run을 재정의했으니 run을 실행시키면 될텐데, run( )이 아닌 start( )가 적혀있다. 쓰레드가 실제 CPU와 이야기하기 위해서 자신만의 스택 메모리에 포함해 준비해야 할 것이 많다. 여기서 start 메서드가 준비단계 포함한 run 메서드 실행을 의미한다. start( )는 새로운 쓰레드 생성/추가하기 위한 모든 준비 + 새로운 쓰레드 위에 run( ) 실행이다.

 

 

 

 extends Thread + 2개의 쓰레드

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("작업 1 - i: " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample03 {
    public static void main(String[] args) {
        MyThread t = new MyThread(); 
        t.start();

        for (int j = 1; j <= 5; j++) {
            System.out.println("작업 2 - j: " + j);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
앞에 있었던 예제를 살펴보면 그저 for문만 있었는데 단일쓰레드, extends Thread가 2개였는데 총 3개의 쓰레드였고, 이번 코드는 extends Thread가 1개인데 총 2개의 쓰레드이다. 그 이유를 살펴보면 main에 있다. 개발자가 명시하지 않아도 자바 프로그램이 실행될 때 JVM은 자동으로 main 쓰레드를 생성한다. 그래서 해당 코드 main에 적어둔 for문이 멀티 쓰레드로 작동하게 된다.

 

 

 

 Runnable + 3개의 쓰레드

class Task1 implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("작업 1 - i: " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Task2 implements Runnable {
    @Override
    public void run() {
        for (int j = 1; j <= 5; j++) {
            System.out.println("작업 2 - j: " + j);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample04 {
    public static void main(String[] args) {
        Runnable task1 = new Task1();
        Thread t1 = new Thread(task1);
        t1.start();
        
        Runnable task2 = new Task2();
        Thread t2 = new Thread(task2);
        t2.start();
    }
}
Runnable 인터페이스를 구현한 클래스를 정의하고, run( ) 추상 메서드를 구현하면서 쓰레드의 작업 내용을 작성했다. 그리고 main에서 Runnable 객체를 생성했다.

extends Thread와 차이점이 있다. 바로 Runnable 객체의 내부에는 start( ) 메서드가 존재하지 않는다는 것이다.  그래서 main을 보면 start( )를 가지고 있는 Thread 객체를 생성해서 Thread 객체로 감싸 start( )를 호출했다. 

Runnable로 구현했을 때의 장점은 인터페이스기 때문에 다른 클래스를 상속할 수 있다는 것이다. 상속은 한 번만 되는데 쓰레드를 상속해버리면 다른 클래스를 상속할 수 없게된다. 

 

 

 

 


 

 

 

✔️   쓰레드의 속성_ 527p

 

• currentThread( )

: 연산을 현재 쓰레드 객체의 참좃값을 얻어올 수 있다.

new Thread(() -> {
    System.out.println("작업 시작");
}).start();
System.out.println("main 스레드 이름: " + Thread.currentThread().getName());
 
 or
 
Thread current = Thread.currentThread()
System.out.println("현재 스레드 이름: " + current.getName());
위의 코드는 참조 변수 없이 실행한 코드이다. 이렇게 참조 변수 없이 일회성으로 쓰레드를 실행할 경우 .getName( ) 같은 객체의 속성을 가져올 수 없다. 그래서 static Thread Thread.currentThread( )를 통해 현재 쓰레드 객체 참좃값을 얻어올 수 있다.

 

 

 

• activeCount( )

: 현재 실행 중인 쓰레드의 개수를 알 수 있다.

public class ThreadExample05 {
    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 1 실행 중");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1 종료");
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 2 실행 중");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2 종료");
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 3 실행 중");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 3 종료");
            }
        });

        t1.start();
        t2.start();
        t3.start();
    }
}
System.out.println("현재 활성 스레드 수: " + Thread.activeCount());
main 쓰레드 내에는 총 3개의 쓰레드가 동작하고 있다. 이렇게 같은 그룹 내에서 실행 중인 쓰레드의 개수를 알고싶을 때 activeCount를 사용한다.

 

 

 

• setName( )

: 쓰레드에 직접 이름을 부여한다.

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.setName("1번");
t2.setName("2번");

 

 

 

• getName( )

: 직접 지정했거나 자동으로 부여된 쓰레드의 이름을 가져온다.

for(int i=0; i<3; i++){
    Thread t1 = new Thread();
    t1.setName(i + "번째 쓰레드");
    System.out.println(t1.getName());
    t1.start();
}

 

 

 

• 우선순위

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
void setPriority(int priority)
int getPriority()
모든 쓰레드는 1~10 사이의 우선 순위를 가지고 있다. 1이 가장 낮은 순위이며 10이 가장 높은 순위다. 우선순위를 지정하지 않으면 기본값으로 5의 우선순위를 갖는다. 우선순위는 쓰레드의 동시성과 관계가 있다. 만일 2개의 작업이 일정 시간 간격으로 번갈아가면서 실행할 경우 우선순위가 더 높은 쪽이 상대적으로 많은 시간을 할당받게 된다.

setPriority( )는 우선순위를 정할 때 사용하는 메서드고, getPriority( )는 지정된 우선순위 값을 가져오는 메서드다.

 

 

 

• availableProcessors

현재 컴퓨터의 CPU 코어 수를 알고 싶을 때 사용한다.

public native int availableProcessors();

 

 

 

 


 

 

 

✔️  데몬 설정_ 533p

 

• 데몬 쓰레드

: 일반 쓰레드가 모두 종료되면 함께 종료되는 쓰레드

Thread t1 = new MyThread();
t1.setDeamon(false);
t1.setName("t1");
t1.start();

Thread t2 = new MyThread();
t2.setDeamon(true);
t2.setName("t2");
t2.start();

try{Thread.sleep(3500);} catch (InterruptedException e) {}
System.out.println("main Thread 종료");
쓰레드의 데몬 설정은 Thread 클래스의 인스턴스 메서드인 setDeamon( )메서드를 사용한다. 이때 기본 값은 false로 일반 쓰레드로 설정해둔다. 생성한 객체의 데몬 설정 여부는 isDeamon( )메서드를 사용해 언제든지 확인할 수 있다. 위의 코드에선 쓰레드 t1은 일반 쓰레드, t2는 데몬 쓰레드로 설정했다.

데몬 설정은 반드시 쓰레드를 실행하기 전인 start( ) 메서드 호출 전에 설정해야 한다. 쓰레드가 실행되고가면 데몬 설정은 바꿀 수 없게 된다. 

만약 main에 일반 쓰레드 t1과 데몬 쓰레드 t2가 있다고 가정해보자. 데몬 쓰레드는 자신을 호출한 main 쓰레드가 종료되면 함께 종료될거라 예상하겠지만 데몬쓰레드는 주 쓰레드의 종료가 아닌 프로세스 내의 모든 일반 쓰레드가 종료되어야 종료된다는 것을 알아야한다. main이 먼저 끝내도 t1 쓰레드가 종료되지 않은 상태라면 t2는 여전히 작동하고 있을 것이다.

 

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   예외처리_ 467p

 

try

: 예외가 발생할 수 있는 코드가 포함되어있다.

 

catch

: 예외가 발생했을 때 처리할 코드가 포함된다.

 

finally

: 예외가 발생하든, 발생하지 않든 항상 실행되는 블록으로, 일반적으로 리소스 해제나 try{ }, catch{ }의 공통 기능 코드가 포함되어있다.

 

try {
    int a = 10 / 0;
}

catch (Exception e) {
    System.out.println("예외 발생!");
}

catch (ArithmeticException e) {
    System.out.println("0으로 나눌 수 없습니다!");
}

finally {
    System.out.println("프로그램 종류");
}
try는 예외가 발생할 수 있는 코드를 담고 있다. 해당 코드에선 ArithmeticException이란 예외를 발생시킨다. catch는 예외를 처리하는 방식을 나타내는데 여러 개를 정의할 수 있다. try에서 예외가 발생하면 그에 맞는 처리방법을 찾기 위해 첫번째 catch부터 finally를 향해 내려간다.

그런데 위의 코드는 예외를 처리하는 과정에서 오류가 발생한다. 모든 예외는 Exception 클래스의 하위 클래스로 어떤 예외가 발생하든 첫번째 catch 블록만 실행되므로 두번째 catch 블록에 도달할 수 없는 코드가 된다. 그래서 unreachable code 오류가 발생한다.

 

 

모든 예외는 Exception의 하위클래스다.

 

 

try {
    int a = 10 / 0;
}

catch (ArithmeticException e) {
    System.out.println("0으로 나눌 수 없습니다!");
}

catch (Exception e) {
    System.out.println("예외 발생!");
}

finally {
    System.out.println("프로그램 종류");
}
순서를 바꿔주면 오류가 해결되어 예외 처리가 가능해진다.

 

 

 

다중 예외 처리

public class TryCatchPratice1 {
    public static void main(String[] args) {
    
        try {
            System.out.println(10 / 0); 
        } catch (ArithmeticException e) {
            System.out.println("0으로 나눌 수 없습니다.");
        } finally {
            System.out.println("프로그램을 종류합니다.");
        }    

        try {
            int[] arr = new int[3];
            System.out.println(arr[5]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("배열 인덱스 범위를 벗어났습니다.");
        } finally {
            System.out.println("프로그램을 종류합니다.");
        }    
    }
}
첫번째 try에서는 0으로 나누는 것이 불가능하여 ArithmeticException 예외 처리방법을 뒀다. 두번째 try에서는 배열의 크기가 3임에도 그 이상에 있는 배열을 출력시키며 발생한 인덱스 범위 오류 예외를 처리했다. finally의 경우 중복되어 코드를 작성했다.

 

public class TryCatchPratice02 {
    public static void main(String[] args) {
        try {
            int[] arr = new int[3];
            System.out.println(10 / 0);     
            System.out.println(arr[5]);      
        } catch (ArithmeticException e) {
            System.out.println("0으로 나눌 수 없습니다.");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("배열 인덱스 범위를 벗어났습니다.");
        } finally {
            System.out.println("프로그램을 종류합니다.");
        }    
    }
}
하나의 try 블록 안에 여러 예외를 넣고 catch로 예외처리를 한다. 이렇게 하면 훨씬 간결한 코드가 완성된다. try 블록에 있는 예외 코드는 catch 블록의 순서와 연관없이 알아서 작성해주면 된다. try와 catch에 중복된 코드가 있을 경우, finally에 적어 중복을 피할 수 있다.

 

 

 

 다중 예외를 한 블록에서 처리하는 방법

public class TryCatchPratice02 {
    public static void main(String[] args) {
        try {
            int[] arr = new int[3];
            System.out.println(10 / 0);     
            System.out.println(arr[5]);      
        } catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
            System.out.println("예외가 발생했습니다.");
        } finally {
            System.out.println("프로그램을 종류합니다.");
        }    
    }
}
하나의 catch 블록에서 두 개의 예외를 처리할 수 있다. 그럴 경우 |(OR)을 사용하여 나타내면 된다.

 

 

 

 


 

 

 

✔️   리소스 해제_ 477p

 

리소스 해제

: 더이상 사용하지 않는 자원을 반납하는 것을 의미한다.

 

InputStreamReader를 이용하여 리소스 자동 해제

try (InputStreamReader isr1 = new InputStreamReader (System.in);) {
    char input = (char) isr1.read();
    System.out.println("입력 글자 = " + input);
} catch (IOException e) {
    e.printStackTrace();
}

try (InputStreamReader isr2 = new InputStreamReader (System.in);) {
    char input = (char) isr2.read();
    System.out.println("입력 글자 = " + input);
} catch (IOException e) {
    e.printStackTrace();
}
InputStreamReader에는 AutoCloseable을 구현하고 있다. try의 괄호 안에 선언된 객체가 예외 처리 구문의 실행이 끝나면 자동으로 close( ) 호출해 리소스를 해제한다. 

그런데 위의 코드는 첫번째 try 블록에서 예외처리를 마치고 리소스를 해제했다. 그러면 그 다음 try 블록은 어떻게 진행되는걸까. System.in은 콘솔 입력을 처리하는 리소스로 자바 가상 머신이 단 하나의 객체를 생성해 제공하기 때문에 이를 반납하면 더이상 콘솔을 입력할 수 없게된다. 그러므로 첫번째 try 블록에서 리소스를 반납해서 다음 try 블록은 사용할 수 없게된다.

 

 

 

 직접 close를 설정하여 리소스 수동 해제

A a1 = null;

try {
    a1 = new A("특정 파일")
}

catch (Exception e) {
    System.out.println("예외 처리");
}

finally {
    if(a1.resource!=null) {
        try{
            a1.close();
        } catch (Exception e) {
        }
    }
}
finally에 직접 close( )를 정의한다. finally에 정의하지 않는다면, 클래스 A에 AutoCloseable 인터페이스를 구현한다. AutoCloseable 내부에 close( ) 메서드를 포함하고 있기에 finally에 직접 정의하지 않아도, 리소스를 알아서 해제할 것이다.

 

 

 

 


 

 

 

✔️   예외전가_ 485p

 

예외 전가

: 예외 처리의 의무를 호출한 메서드가 예외를 갖게 되며, 상위의 메서드도 자신을 호출한 지점으로 예외를 전가할 수 있다.

 

throws

: 예외 전가 문법이다.

    public static void readFile() throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader("example.txt"));
        String line = reader.readLine();
        System.out.println("읽은 내용: " + line);
        reader.close();
    }

    public static void main(String[] args) {
        try {
            readFile();
        } catch (IOException e) {
            System.out.println("파일을 읽는 중 오류 발생: " + e.getMessage());
        }
    }
readFile 메서드에서 발생하는 예외를 IOException으로 전가되었으며 메서드 내부에서 try-catch 구문으로 처리하지 않았다. 대신, main 메서드에서 readFile( )을 호출할 때  try-catch로 예외를 처리한다. 예외 전가는 위의 코드처럼 예외를 유연하게 분리 시켜 나중에 처리하도록 해준다.

예외를 처리하지 않고 계속 전가하게 되면 자바 가상 머신이 직접 예외를 처리하게 된다. 자바 가상 머신의 예외 처리 방법은 우리가 잘 아는 방법이다. 발생한 예외의 정보를 화면에 출력한 뒤 프로그램을 강제 종료하는 것이다.

예외 전가도 다중으로 처리 가능하다. throws 예외 클래스 A, 예외 클래스 B... 로 한 번에 전가하면 된다.

 

 

 

 


 

 

 

✔️   사용자 정의 예외 클래스_ 492p

 

사용자 정의 예외 클래스

: 자바에 존재 하지 않는 예외를 직접 작성한다.

 

 사용자 정의 예외 클래스 작성

public class MyException extends Exception {
    public MyException(String message) {
        super(message);
    }
}
생성자에서 message를 받아, 예외 메시지를 부모에게 전달한다.

 

 

사용자 정의 예외 객체 생성과 예외 객체 던지기

throw new MyException("나이가 너무 어려서 가입할 수 없습니다.");

 

 

 사용자 정의 예외 객체 생성과 예외 객체 던지기

public class Pratice01 {

    public static void checkAge(int age) throws MyException {
        if (age < 18) {
            throw new MyException("나이가 너무 어려서 가입할 수 없습니다.");
        } else {
            System.out.println("가입이 가능합니다.");
        }
    }

    public static void main(String[] args) {
        try {
            checkAge(15);
        } catch (MyException e) {
            System.out.println("예외 발생: " + e.getMessage());
        }
    }
}
18살 이하면 MyException 예외가 발생하도록 만들었다. try 블록에서 15를 인자로 보냈고 예외 MyException이 발생해 예외를 던졌다. catch 구문에서 받아 예외 발생 문구를 출력한다.

 

 

 

 


 

 

 

✔️   예외 클래스의 메서드_ 497p

 

getMassage( ) 메서드

: 예외가 발생했을 때 생성자로 넘긴 메시지를 문자열 형태로 리턴하는 메서드

 throw new MyException("나이가 너무 어려서 가입할 수 없습니다.");
System.out.println("예외 발생: " + e.getMessage());
나이가 너무 어려서 예외가 발생했다. 예외는 "나이가 너무 어려서 가입할 수 없습니다."라는 메시지를 포함하고 있다. 해당 메시지는 예외 객체를 생성할 때 입력매개변수로 전달된다. 이 문자열은 getMassage( ) 메서드를 통해 리턴한다.

 

 

 

printStackTrace( ) 메서드

예외 발생이 전달되는 경로로, 예외 발생 시 개발자가 디버깅할 수 있도록 예외의 원인을 자세히 출력한다.

try (InputStreamReader isr1 = new InputStreamReader(System.in)) {
    char input = (char) isr1.read();
    System.out.println("입력 글자 = " + input);
} catch (IOException e) {
    e.printStackTrace();
}
콘솔에 예외가 어디에서 전가되었는지 등 추적 정보를 출력한다.

 

 

#6027_ 10진 정수를 입력받아 16진수로 출력하기1

소문자 형태로 출력

입력: 255
출력: ff
a = int(input())
print('%x'%a)
a = input()
b = int(a)
print('%x'%b)

 

 

 

#6028_ 10진 정수를 입력받아 16진수로 출력하기2

대문자 형태로 출력

입력: 255
출력: FF
a = int(input())
print('%X'%a)
a = input()
b = int(a)
print('%X'%b)

 

 

 

#6029_ 16진 정수를 입력받아 8진수로 출력하기2

입력: f
출력: 17
a = input()
print(f'{int(a,16):o}')
a = input()
b = int(a, 16)
print('%o'%b)
파이썬에서 진법 간 변환을 할 때 중간에 10진수를 거쳐야한다.

 

 

 

#6030_ 영문자 1개를 입력받아 10진수로 변환하기

입력: A
출력: 65
a = ord(input())
print(a)
ord는 문자의 아스키 코드값 혹은 유니코드를 나타내는 숫자로 변환해주는 함수다.
그래서 변환되었기에 a만 출력해도 원하는 값이 나온다.

 

 

 

#6031_ 정수 입력받아 유니코드 문자로 변환하기

입력: 65
출력: A
a = int(input())
print(chr(a))

 

 

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   예외_ 457p

 

 예외

: 연산 오류, 숫자 포맷 오류 등과 같이 상황에 따라 개발자가 해결할 수 있는 오류를 의미한다.

 

 에러

: 자바 가상 머신 자체에서 발생하는 오류로 개발자 해결할 수 없는 오류를 의미한다.

 

 

예외는 개발자가 해결할 수 있는 오류를 말한다고 했는데, 이때 오류를 수정해서 해결하는 것이 아닌 오류가 발생했을 때 차선책을 선택하는 것을 말한다.에러는 아예 처리자체를 할 수 없는 오류인 블루스크린 같은 것을 말한다.

자바에서 예외의 최상위 클래스는 Exception 클래스고, 에러의 최상위 클래스는 Error 클래스다. 이 두 개의 클래스는 Throwable 클래스를 상속한다. 따라서 에러와 예외 모두 Throwable 클래스의 모든 기능을 포함한다. 

 

 

 

 


 

 

 

✔️   일반 예외_ 458p

 

 일반 예외

: 컴파일 전에 예외 발생 문법을 검사하며 예외 처리를 하지 않으면 문법 오류가 발생하여 컴파일이 불가하다.

 

• InterruptedException

: 일정 시간 동안 해당 쓰레드를 멈추게 만든다.

 Thread thread = new Thread(() -> {
            try {
                System.out.println("작업 시작: 5초 대기 중");
                Thread.sleep(5000);  // 5초 대기
                System.out.println("작업 완료");
            } catch (InterruptedException e) {
                System.out.println("스레드가 인터럽트되었습니다.");
            }
        });
쓰레드는 프로그램 실행 과정에서 CPU를 사용하는 최소 단위로, 프로세스 내에 존재한다. 즉, 일반적으로 자바 프로그램은 커다란 하나의 작업을 작은 작업들로 나누어 각각 쓰레드에 할당한 후 실행된다. 동시에 여러 쓰레드가 실행되니 전체 작업을 빨리 끝낼 수 있도록 해준다.

사용자가 전체 작업을 취소하여 종료해야할 경우, 쓰레드가 종료할 때까지 기다리는 것이 아닌 작업이 더이상 필요하지 않으니 실행을 그만두고 리소스들을 정리한 후 종료하도록 알린다. 이때 인터럽트가 사용된다.

이때 만약 쓰레드가 어딘간에 블로킹 되어있다면 인터럽트를 체크하고 종료 로직을 넣어놨어도 인터럽트 로직 실행은 영원히 실행되지 않을 수 있다. 그렇기에 sleep에서 벗어나 인터럽트 로직이 실행될 수 있도록 해야하는데 Interrupted Exception을 try-catch를 이용하여 처리하거나 메서드 시그니처에 명시해서 메서드 호출자에게 처리를 위임해야한다.

여기서 중요한 것은 Interrupted Exception은 반드시 적절하게 처리해야한다는 것이다. 만약 Interrupted Exception을 무시하는 코드가 여기저기 남발한다면 실행이 종료되지 않고 kill-9로 강제 종료해야한다.

 

 

[Java] InterruptedException이란?

자바로 코드를 작성할 때 가장 많이 고려되어야 하는 예외 중 하나가 InterruptedException이다. 스레드의 실행을 잠깐 동안 멈추기 위해 사용하는 sleep 코드를 살펴보자 try { Thread.sleep(1000); } catch (Inter

hbase.tistory.com

 

 


• ClassNotFoundException

: JVM이 클래스 이름을 문자열로 받아서 로딩하려고 할 때, 해당 클래스가 존재하지 않아 찾지 못하는 경우 발생한다.

public class ClassNotFoundExample {
    public static void main(String[] args) {
        try {
            Class.forName("com.example.NotExistClass");
        } catch (ClassNotFoundException e) {
            System.out.println("클래스를 찾을 수 없습니다: " + e.getMessage());
        }
    }
}
Class.forName("클래스명")을 사용할 때 주로 발생하는 예외다. ClassNotFoundException는 checked Exception이라 반드시 try-catch로 처리해야 한다.

 

 

 

• IOException

: 자바 입출력에서 주로 보이는 일반 예외로, 콘솔이나 파일에 데이터를 쓰거나 읽을 때 발생한다.

    try {
            BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
            String line = reader.readLine();
            System.out.println("파일 내용: " + line);
            reader.close();
        } catch (IOException e) {
            System.out.println("파일을 읽는 중 오류가 발생했습니다: " + e.getMessage());
        }
FileReader와 BufferedReader를 이용하여 test.txt 파일의 첫 줄을 읽는다. 파일이 존재하지 않거나 읽는 중 문제가 생기면
IOException이 발생한다. 제대로 실행을 하고자 원한다면 같은 폴더에 test.txt 파일이 제대로 존재하는지 확인을 해야한다. 

 

 

 

• FileNotFoundException

: IOException의 하위 클래스이며, 파일을 읽을 때 해당 경로에 파일이 없거나 쓰기 권한이 없는 파일에 접근하려고 할 때 발생한다.

  try {
            FileInputStream fis = new FileInputStream("test.txt");
        } catch (FileNotFoundException e) {
            System.out.println("파일을 찾을 수 없습니다: " + e.getMessage());
        }
test.txt 파일이 없는 경우 발생한다.

 

 

 

• CloneNotSupportedException

: Cloneable 인터페이스를 상속하지 않은 클래스의 객체를 복사하기 위해 clone( ) 메서드를 호출하면 발생한다.

class Person implements Cloneable {
    String name;

    Person(String name) {
        this.name = name;
    }

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

public class CloneExample {
    public static void main(String[] args) {
        Person person1 = new Person("Alice");

        try {
            Person person2 = (Person) person1.clone();
            System.out.println("복제 성공");
        } catch (CloneNotSupportedException e) {
            System.out.println("복제할 수 없습니다: " + e.getMessage());
        }
    }
}
Object 클래스의 메서드 중 clone( )은 자신의 객체를 복사한 클론 객체를 생성해 리턴하는 메서드로 반드시 Cloneable 인터페이스를 상속해야한다.

clone 메서드는 Java의 최상위 클래스인 java.lang.Object 클래스에서 정의된 메서드로 그 안에 protected로 정의되어 있다. 하위 클래스에서 super.clone( )을 호출해야 복제된다. 만약 clone 메서드를 외부에서 사용하려면 protected -> public으로 오버라이딩해야 한다.

 

 

 

 


 

 

 

✔️   실행 예외_ 463p

 실행 예외

: 컴파일 전이 아닌 실행할 때 발생하는 예외로, 예외 처리를 따로 하지 않더라도 문법 오류가 발생하지 않고 프로그램이 강제 종료된다.

 

ArithmeticException

: 연산 자체가 불가능할 때 발생한다.

        try {
            int a = 10;
            int b = 0;
            int result = a / b; 
            System.out.println("결과: " + result);
        } catch (ArithmeticException e) {
            System.out.println("산술 오류 발생: " + e.getMessage());
        }
위의 코드는 분모가 0인 경우로 연산이 불가하여 예외가 발생한 경우이다. 만약 이렇게 오류가 발생했을 경우를 대비해 result 값이 -1 혹은 1000 등의 값을 넣은 코드를 try-catch로 감싸 프로그램이 종료되지 않도록 막으면서 결과 값을 보고 연산 코드에서 오류가 발생했음을 발견한다.

 

 

 

ClassCastException

: 상속 관계에 있는 클래스 간의 다운 캐스팅이 불가능할 때,  다운캐스팅을 시도하면 발생하거나 잘못된 형변환을 하면 발생한다.

        Object obj = "나는 문자열입니다";  // String 객체

        try {
            Integer num = (Integer) obj;  // 잘못된 형변환
        } catch (ClassCastException e) {
            System.out.println("형변환 오류 발생: " + e.getMessage());
        }
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

public class CastExample {
    public static void main(String[] args) {
        Animal animal = new Cat();  // 실제 객체는 Cat

        try {
            Dog dog = (Dog) animal;  // 잘못된 다운캐스팅
        } catch (ClassCastException e) {
            System.out.println("형변환 오류: " + e.getMessage());
        }
    }
}

 

 

 

ArrayIndexOutOfBoundException

: 배열의 인덱스를 잘못 사용했을 때 발생한다.

        int[] numbers = {10, 20, 30};

        try {
            System.out.println(numbers[3]); // 인덱스 3은 존재하지 않음
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("배열 인덱스 오류 발생: " + e.getMessage());
        }

 

 

 

NumberFormatException

: 문자열을 숫자 또는 실수로 변환할 때 문자열이 변환하고자 하는 숫자 형식이 아니면 변환이 실패하는데 이때 발생한다.

        String input = "123ABC";  // 숫자가 아닌 문자가 포함된 문자열

        try {
            int number = Integer.parseInt(input);  // 예외 발생
            System.out.println("변환된 숫자: " + number);
        } catch (NumberFormatException e) {
            System.out.println("숫자 형식 오류: " + e.getMessage());
        }

 

 

 

NullPointerException

: 참조 변수가 실제 객체를 가리키고 있지 않은 상황에서 필드나 메서드를 호출할 때 발생한다.

        String text = null;  // 문자열 변수에 null 할당

        try {
            System.out.println("문자열 길이: " + text.length());  // 예외 발생
        } catch (NullPointerException e) {
            System.out.println("널 포인터 예외 발생: " + e.getMessage());
        }
위의 코드에서 text는 null 값이다. null은 위칫값을 저장하는 참조 변수의 초깃값으로만 사용할 수 있으며, 현재 가리키고 있는 객체가 없다는 것을 의미한다.

 

 

 

 

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   익명 이너 클래스_ 436p

 

 익명 이너 클래스

: 이름을 알 수 없는 이너 클래스를 의미하며, 인스턴스 익명 클래스와 지역 익명 이너 클래스가 있다.

 

 

이너클래스와 익명 이너 클래스 비교하기.

interface MyInterface {
    public abstract void myMethod1();
}

class OuterClass {
     MyInterface inner = new InnerClass();
     void myMethod2(){
         inner.myMethod1();
     }
     
     class InnerClass implements MyInterface {
         public void myMethod1() {
             System.out.println("인스턴스 이너 클래스!");
         }
     }
}

public class PracticeInnerClass {
     public static void main(String[] args) {
     
         OuterClass outer = new OuterClass();
         outer.myMethod2();
     }
}
위의 코드는 이너클래스에서 인터페이스를 상속 받아 인터페이스 myMethod1을 구현한 코드이다. 출력되는 코드는 'myMethod1!'이 출력될 것이다. 위의 코드가 익명 이너 클래스 코드와 어떻게 다른지 비교하면 더 좋을 것 같다.
interface MyInterface {
    public abstract void myMethod1();
}

class OuterClass {

	 MyInterface myinter = new MyInterface() {
         public void myMethod1() {
              System.out.println("익명 이너 클래스");
         }
     };
     
     void myMethod2() {
         myinter.myMethod1();
     }    
     
}

public class PracticeAnonymousClass {
     public static void main(String[] args) {
     
         OuterClass outer = new OuterClass();
         outer.myMethod2();
     }
}
익명 이너 클래스는 인터페이스인 MyInterface를 참조 변수 타입으로 받아 생성자를 호출했다. 그리고 그 생성자 중괄호 안에 인터페이스의 추상 메서드인 myMethod1를 구현했다. 그러면 컴파일러가 알아서 MyInterface를 상속받아 클래스를 내부적으로 생성한 후(익명 클래스) 해당 클래스로 객체를 생성해 참조 변수에 대입한다.

익명 이너 클래스를 사용하면서 주의해야할 것은 항상 부모 타입으로만 선언할 수 있으며, 부모타입으로 선언되었기에 부모에게 없는 메서드는 호출할 수 없다.

 

Q. 그런데 인터페이스는 객체를 생성할 수 없는데, 익명 이너 클래스는 어떻게 인터페이스 객체를 생성한걸까?

A. 위의 설명과 같다. 해당 코드가 오류 없이 실행되는 이유는, 인터페이스 자체를 인스턴스화한 것이 아니라, 컴파일러가 인터페이스를 구현한 익명 클래스를 자동으로 생성해서 그 인스턴스를 생성했기 때문이다. 쉽게 말하면, 우리가 코드에서 인터페이스를 직접 사용하는 것처럼 보이지만, 실제로는 인터페이스를 구현한 이름 없는 클래스가 내부에서 생성되어 그 객체가 인터페이스 타입 변수에 저장되었기 때문에 가능한 것이다.

 

Q. 책에 보면 '메서드를 호출을 할 수 없으므로 애초에 만들 필요가 없을까? 그렇지 않다. 오버라이딩 메서드 내부에서는 호출할 수 있으므로 작성해야할 내용이 많을 때 메서드를 분리해 작성하는 것이 효율적이며, 이때 익명 이너 클래스 정의식 내부에 추가 메서드를 정의해 사용하는 것이 편할 수 있다.'라고 되어있다. 이게 무슨 말일까?

A. 문장을 차근차근 살펴보겠다. 오버라이딩 메서드 내부란 위의 코드에서는 OuterClass에 있는 myMethod1을 의미한다. 해당 메서드는 인터페이스에 있는 메서드를 오버라이딩 한 것이다. 위에서 설명하길 익명 이너 클래스는 항상 부모 타입으로만 선언되기 때문에 부모에게 없는 메서드는 호출할 수 없다고 했지만 만약 어떤 기능을 myMethod1 안에서 실행되는 것으로 충분하다면 굳이 외부에 만들지 말고 myMethod1에 원하는 메서드를 구현하여, 원래 있던 메서드와 분리해 작성하면 된다고 하는 것이다. 쉽게 한 줄로 정리하자면 새로운 메서드를 만들고 싶을 경우 부모에게 없는 메서드여도, 오버라이딩 메서드 내부에 만들면 된다라고 하는 것이다.

 

 

 


 

 

 

✔️   익명 이너 클래스를 활용한 인터페이스 타입의 입력매개변수 전달_ 439p

 

인터페이스 타입의 입력매개변수 전달 방법_ 코드: 익명이너클래스 + 참조변수활용X

interface Messenger {
    void send(String message);
}

class MessageProcessor {
    void process(Messenger messenger) {
        messenger.send("익명 이너 클래스 메시지 전송!");
    }
}

public class Main {
    public static void main(String[] args) {
        MessageProcessor processor = new MessageProcessor();
        
        // 참조 변수 없이 익명 이너 클래스를 직접 인자로 전달
        processor.process(new Messenger() {
            public void send(String message) {
                System.out.println("보낸 메시지: " + message);
            }
        });
    }
}
인터페이스 타입의 입력 매개변수를 전달하는 방법은 총 4가지가 있다. 자식 클래스를 직접 생성하여 참조 변수명을 활용하는 방법과 참조 변수명 없이 활용하는 방법이 있다. 그리고 익명 이너 클래스를 활용하는데 이때 참조 변수명을 활용하는 방법과 참조 변수명을 활용하지 않는 방법이 있다. 

객체 참조 변수의 역할은 객체의 참좃값을 전달하기 위함이다. 단순히 객체의 참좃값만을 전달하고자 한다면 굳이 참조 변수를 사용하지 않고 메서드 입력매개변수 위치에서 바로 객체를 생성하면 생성된 객체의 참좃값이 메서드로 전달될 것이다.

 

 

 

 


 

 

 

 

✔️   이너 인터페이스_ 444p

 

 정적 이너 인터페이스

: 클래스 내부에 인터페이스를 정의하여 해당 클래스에 의존적인 기능을 수행한다.

// 구현 클래스

class Outer {
    interface MyInterface {
        void hello();
    }
}

public class Main {
    public static void main(String[] args) {

        Outer.MyInterface obj1 = new Outer.MyInterface() {
            public void hello() {
                System.out.println("익명 클래스!!");
            }
        };
        obj1.hello();

        class Impl implements Outer.MyInterface {
            public void hello() {
                System.out.println("구현 클래스!!");
            }
        }

        Outer.MyInterface obj2 = new Impl();
        obj2.hello();
    }
}

 

이너 인터페이스는 무조건 정적 이너 인터페이스만 존재할 수 있다. 만약 static 제어자를 생략하면, 컴파일러가 자동으로 추가해준다. 위의 코드에서 컴파일을 하게 되면 내부 인터페이스의 바이트 코드는 'Outer$MyInterface.class'로 형성된다. 아우터 클래스 내에 속해 있는 인터페이스도 당연하게 인터페이스므로 직접 객체 생성은 불가하다. 그래서 객체 생성을 하기 위해 다른 클래스로부터 상속을 받거나, 익명 이너 클래스를 활용해야한다.

위의 코드에선 두가지 모두를 반영한 코드로, obj1이라는 객체를 생성한 방법 그리고 Impl 클래스를 만들어 Outer.MyInterface를 상속한 방법을 보여주고 있다.

 

 

 

 이벤트 처리 방법

class Button {

    // 인터페이스 OnClickListener타입을 가지는 ocl 변수인 필드
    OnClickListner ocl;
    
    // OnClickListener타입의 ocl 변수를 인자로 받아
    // Button 클래스의 ocl 변수를 재정의하는 setOnClickListener 메서드
    void setOnClickListener(OnClickListener ocl) {
         this.ocl = ocl;
    }
    
    // 클래스 내부에 있는 인터페이스
    // 추상 메서드 onClick이 있음을 정의
    interface OnclickListener {
        public abstract void onClick();
    }
    
    void click() {
         ocl.onClick();
    }
}
public static void main(String[] ar) {
    // Button 클래스의 객체인 button1
    Button button1 = new Button();
    
    // Button 클래스의 내부 인터페이스를 익명 클래스로 구현
    button1.setOnClickListener(new Button.OnClicklistener() {
        @Override
        public void onClick() {
             System.out.println("개발자 1. 음악재생");
        }
    });
    button1.click();
}
main에서 Button 클래스의 객체인 button1이 만들어지고, 이 객체는 OnClickListener 타입의 ocl 필드를 가지고 있다. OnClickListener에서 onClick 메서드를 구현하고 있으며 setOnclickListener는 이 객체를 받아서 버튼 안에 있는 ocl 필드에 저장한다. this.ocl = ocl로 개발자가 전달한 동작을 버튼이 기억하게 된다.

button1.click();로 버튼이 클릭되면, onClick 메서드가 호출되어 "개발자 1. 음악재생"이 출력된다.

 

 

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   인스턴스 멤버 이너 클래스_ 426p

 

 인스턴스 멤버 이너 클래스

: 아우터 클래스 내에서 인스턴스인 객체 내부 멤버의 형태로 존재하는 클래스

 

class OuterClass {

    public int a = 1;
    protected int b = 2;
    private int c = 3;
    
    void outerMethod(){
        System.out.println("아우터클래스!");
    }    

    class InnerClass {
        
        void innerMethod() {
            System.out.println(a);
            System.out.println(b);
            System.out.println(c);
            
            outerMethod();
        }
    }    
     
}

public class PraticeInnerClass1 {
    public static void main(String[] args) {
    
        OuterClass outer = new OuterClass();
        
        OuterClass.InnerClass inner = outer.new InnerClass();
        
        inner.innerMethod();
        
    }
}
멤버 이너 클래스 중 하나인 인스턴스 멤버 클래스는 객체 내부의 멤버의 형태로 존재한다. 객체의 멤버란, 해당 클래스에 있는 필드나 메서드를 이야기 하는데 해당 클래스 내부에 클래스가 생긴다면 이 클래스도 멤버라는 것이다. 그렇기에 인스턴스 멤버 이너 클래스는 아우터 클래스의 모든 접근 지정자의 멤버에 접근할 수 있게 된다.

자바는 컴파일을 수행하면 각 클래스별로 바이트 코드인 .class 파일이 생성된다. 이너 클래스 마찬가지로 생성된다. 하지만 그 형태는 아우터 클래스와는 다를 수 있다. 아우터 클래스는 'OuterClass.class'라고 생성이 될 것이다. 그러면 아우터 클래스 안에 있는 인스턴스 멤버 클래스는 'OuterClass$InnerClass.class'로 파일이 생성된다. 그 이유는 당연하게도 아우터 클래스 내부에 있기 때문에 그것을 표시하고자 생성된다.

InnerClass 내에 innerMethod가 있다. innerMethod를 사용하고자 하여 인스턴스 멤버 클래스의 객체를 생성하려한다. 인스턴스 멤버 클래스의 객체를 생성하기 위해서는 먼저 아우터 클래스의 객체 생성이 필요하다. 그 후에 아우터 클래스 내에 있는 이너 클래스이니, 생성한 아우터 클래스 객체의 참조 변수를 이용해 객체 내부에 있는 이너 클래스의 생성자를 호출한다. 그러면 OuterClass.InnerClass inner = outer.new InnerClass( );가 된다.

위의 코드에서 innerMethod를 보면 아우터 클래스 필드에 접근하고 있다. 접근 가능한 이유는 InnerClass 내에 OuterClass의 멤버와 동일한 이름이 없기 때문인데, 만약 같은 이름이 InnerClass내에 존재한다면 섀도잉이 발생해 InnerClass 내부 필드의 값으로 변경되어 출력될 것이다. 그럼 동일한 이름을 가진 상태에서 아우터 클래스의 멤버에 접근하고자 한다면 어떻게 해야할까? 'OuterClass.this.a'를 붙이면 된다. OuterClass를 붙이지 않으면 InnerClass의 멤버에 접근하니 유의해야한다.

 

 

 

 


 

 

 

 

✔️   정적 멤버 이너 클래스_ 431p

 

 정적 멤버 이너 클래스

: 이너 클래스 앞에 static이 포함된 클래스다.

class OuterClass {
    int a = 1;
    static int b = 2;
    
    void outerMethod1() {
        System.out.println("인스턴스 메서드!");
    }
    
    static void outerMethod2() {
        System.out.println("정적 메서드!");
    }    
     
    
    static class InnerClass {
        void innerMethod() {
            System.out.println(b);
            outerMethod();
        }
    }
}

public class PracticeInnerClass2 {
    public static void main(String[] args) {
        OuterClass.InnerClass inner = new OuterClass.InnerClass();
        
        inner.innerMethod();
    }
}
static이 포함된 이너 클래스는 static 특성을 지니게 된다. 그 특징이 아우터 클래스 내에 static이 붙은 멤버만 정적 이너 클래스 내부에서 사용할 수 있게 한다. 또 다른 특성을 살펴보자. 위의 코드를 살펴보면 main에서 아우터 클래스의 객체를 만들지 않고 바로 정적 이너 클래스의 객체를 생성한 것을 볼 수 있다. 아우터 클래스 내에 있는 정적 이너 클래스도 정적 멤버이므로 클래스명으로 바로 접근 가능하다.

 

 

 

 


 

 

 

✔️   지역 이너 클래스_ 433p


 지역 이너 클래스

: 메서드 내에서 정의되는 클래스

class OuterClass {
    int a = 1;
    
    void outerMethod(){
        int b = 2;
        
        class InnerClass {
            void innerMethod() {
                System.out.println(a);
                System.out.println(b);
            }
        }
        
    InnerClass inner = new InnerClass();
    inner.innerMethod();
    }
}

public class PracticeInnerClass3 {
    public static void main(String[] args) {
        
        OuterClass outer = new OuterClass();
        outer.outerMethod();
        
    }
}
지역 이너 클래스는 정의된 메서드 내부에서만 사용 가능하며, 지역 이너 클래스를 선언 이후 바로 객체를 생성해서 사용한다는 특징이 있다. 메서드가 호출이 되면 그때만 메모리에 로딩되기 때문에 정적 영역과는 다른 특성을 가지고 있다. 정적 영역은 객체가 생성되기 전에 먼저 메모리에 로딩되어있다.

메서드 내부에 있는 변수는 지역 변수라고 한다. 지역 이너 클래스가 지역 변수를 사용하고자 하면 해당 지역 변수가 final로 선언되어있어야 한다. 만약 final을 선언하지 않아도 컴파일러가 알아서 해당 지역 변수에 추가해준다. 또 해당 지역 변수의 값이 변동된다면 컴파일러 에러가 발생한다.

 

 

Q. 지역 변수에만 final을 붙여야하는 이유가 뭘까?

A. 메서드가 끝이 나더라도 지역 이너 클래스의 객체는 메서드 밖에서 살아남을 수 있다. 그런데 메서드 내에 있는 변수인 지역 변수는 메서드가 끝나면 사라지게 된다. 지역 이너 클래스의 객체는 살아남아서 활동해야하는데, 변수가 끝나버리면 해당 클래스는 제대로 사용할 수 없게된다. 그렇기에 final을 붙여 해당 값이 변동되지 못하도록하고 값을 복사하여 메서드가 끝이나도 사용할 수 있게 해준다.

 

 

 

 

 

 

 

📚 참고한 책

 

: 출처 예스24 홈페이지

 

 

 

- 기본적인 개념을 정리하고 추가적으로 궁금한 것들을 정리했습니다 -

 

 

🐥 🐥 🐥

 


 

✔️   추상클래스_ 395p

 

 추상클래스

: 메서드 본채가 완성되지 않은 미완성 메서드로, 메서드의 기능을 정의하는 중괄호 자체가 없으며 명령어의 끝을 알리는 세미클론으로 끝난다.

abstract class A {
    abstract void abc();    
    void bcd() {
    }
}
추상 메서드를 1개 이상 포함하고 있는 클래스는 반드시 추상 클래스로 정의해야한다. 일반적으로 추상 클래스는 메서드의 기능이 정의되어 있지 않는 미완성 메서드인 중괄호가 없는 형태의 메서드가 1개 이상 존재한다는 의미이다. 추상 클래스의 형식은 class 키워드 앞에 abstract를 붙여 표현한다.

추상 클래스의 주의해야할 점은, 중괄호의 여부이다. 만일 중괄호가 있는 상태로 코드를 완성하면 그 메서드는 '아무런 기능을 하지 않는 완성된 메서드'라고 기능을 명확히 정의하게 된다.

추상 클래스는 내부의 미완성 메서드를 지니고 있기 때문에 객체를 직접 생성할 수 없다. 그 이유는 힙 메모리의 특성 때문이다. 힙 메모리는 값이 비어있는 필드를 저장할 수 없는 특성을 지녔는데, 이로 인해 미완성된 메서드를 가진 추상 클래스는 A a = new A( )와 같이 생성자 호출 자체를 할 수 없다.

 

 

 

추상 클래스 객체 생성하는 방법 : 1. 추상 클래스를 상속한 일반 클래스

abstract class A {
	abstract void abc();
}

class B extends A {
	void aaa() {
    	System.out.println("자식 클래스로 추상 클래스 이용하기");
    }
}

public class AbstractClass_1 {
	public static void main(String[] args) {
    	A b1 = new B();
        A b2 = new B();
        
        b1.aaa();
        b2.aaa();
    }
}
B는 추상 클래스를 상속했다. B가 일반 클래스로 역할을 하고 싶다면, 추상 클래스를 상속했기에 추상 메서드의 구체적으로 어떤 역할을 할 것인지 정의해야한다. 만약 객체를 여러개 만들어야하는 상황이라면 위의 코드와 같이 자식 클래스를 직접 정의하는 것이 더 적절한다.

 

 

 

 추상 클래스 객체 생성하는 방법 : 2. 이너클래스 

abstract class A {
    abstract void abc();
}

public class AbstractClass {
    public static main(String[] args) {
    
        A a1 = new A();
            void abc() {
                System.out.println("이너클래스로 이용하기");
            }
        };
                  
        a1.aaa();
}
이너클래스를 이용하는 것은, A( )는 컴파일러가 클래스 A를 상속받아 aaa( ) 메서드를 오버라이딩한 익명 클래스의 생성자를 호출하는 것이다. 이 경우 클래스 A의 생성자를 호출하는 것이 아니다. 익명 이너 클래스를 활용한 장점으로는 코드가 간결해 보인다는 점이 있지만, 상속한 경우와 달리 객체를 여러개 만들기엔 적합하지 않다. 

 

 

 

 


 

 

 

✔️   인터페이스_ 402p

 

 인터페이스

: 서로 다른 시스템이나 장치, 또는 사용자 간의 정보 교환을 위한 접점이나 경계면

interface abc {
    public static final int a = 1;
    public abstract void abc();
}
인터페이스 내부의 모든 필드는 public static final로 정의되어있고, static과 default 메서드 이외의 모든 메서드는 public abstract로 정의되어있다. 클래스는 class라고 이름 앞에 붙여주었다면, 인터페이스는 interface임을 이름 앞에 명시한다. 제어자를 명시적으로 적지 않았어도 public static final과 public abstract는 자동으로 추가된다.

 

Q. 인터페이스 내 필드에 static과 final을 붙이는 이유는?

A. 인터페이스는 기능의 약속을 정의하는 곳으로 "어떤 기능을 제공하겠다."라는 메서드 이름과 규약만 가지고 있다. 인터페이스는 행위에 대한 약속만 하지, 변수에 대해서는 기본적으로 정의하지 않기 때문에 인터페이스 내의 변수는 기본적으로 변하지 않는 상수여야만 한다. 여기서 static은 클래스(인터페이스) 자체에 붙어있는 것으로 객체 생성 없이 접근 가능하며, final은 한 번 정의한 값은 변경되지 않는 특성을 부여해준다.
인터페이스는 인스턴스를 만들 수 없기 때문에 변수에 static을 붙여 인터페이스 내에 저장되도록 하여 접근가능하게 만들고, 인터페이스는 메서드를 관리하는 곳이기 때문에 변경되는 변수에 의해 메서드의 변동이 생기는 것을 방지해야함으로 final을 붙인다.

 

 

Q. 인터페이스와 추상 클래스의 차이점은 무엇이며 어떤 상황에 맞춰 써야하는가?

A. 인터페이스는 메서드의 이름만 정의해두는 곳이다. 인터페이스를 상속받은 클래스에서 구체화가 이루어져 정확히 이 메서드가 어떤 기능을 가지는지 정의한다. 추상 클래스는 추상화가 들어있는 클래스라고 생각하면 된다. 일반 변수, 상수 모두 정의할 수 있으며 공통 기능과 약속된 기능도 함께 있다. 접근제어자도 자유롭게 지정가능하다.
인터페이스는 다양한 클래스들이 공통 없이 제각각 동작하는 경우 사용한다. 혹은 다중 구현이 필요하거나 특정 역할로 나눌 때 사용한다. 추상 클래스의 경우 기본 형태는 유지하면서 세부적인 동작을 다르게 하고 싶을 때 사용한다. 같은 타입끼리 묶고 공통 코드를 재사용한다고 생각하면 된다.

 

 

 

 인터페이스의 상속

class ABC extends myClass implements myInterface1, MyInterface2 {

}
인터페이스는 미완성 메서드를 완성하기 때문에 상속하는 의미인 extends가 아닌 구현하는 의미인 implements를 사용한다. 그래서 인터페이스에 정의된 메서드는 자식 클래스에서 모두 구현되어야 제대로 작동된다.

1. 한 클래스는 인터페이스를 다중 상속 가능하다.

클래스는 두 부모 클래스에 동일한 이름의 필드 혹은 메서드가 존재하면 어느 부모 클래스의 필드(메서드)를 가져올지 혼동하여 오류가 발생하게 되고 그것을 막고자 클래스는 다른 클래스를 다중 상속하는 것을 막고 있다. 하지만 인터페이스를 다중 상속하는 것은 가능하다. 왜냐하면 모든 필드가 public static final로 정의되어 각 인터페이스 내에 존재하기 때문에 저장 공간이 겹치지 않기 때문이다. 메서드의 경우 어차피 자식 클래스에서 완성되기 때문에 문제되는 부분이 없다.

2. 한 클래스는 하나의 클래스와 여러 인터페이스를 동시에 상속할 수 있다.
클래스만 다중 상속하지 않는다면 문제될 건 없다. 다만 상속할 경우 순서에 유의해야한다. 반드시 처음에 클래스 먼저 상속해주고 그 다음 인터페이스를 상속해야한다.

3. 인터페이스는 인터페이스를 상속할 수 있다.
인터페이스는 인터페이스를 상속할 수 있지만 이때 implements가 아닌 extends로 상속해야한다. 예시를 들어보자면 interface myInterface1 extends myInterface2의 형식이 될 것이다.

4. 인터페이스는 클래스를 상속하지 않는다.
인터페이스는 완성되지 않은 테두리만 있는 메서드를 가진다. 그런데 완성된 메서드를 지닌 클래스를 상속하게 된다면 인터페이스의 의미가 사라지게 되니 클래스는 인터페이스에 상속되지 못한다.

5. 모든 자식 클래스의 구현 메서드는 public만 가능하다.
하위 클래스에 오버라이딩이 수행될 때 접근 지정자는 부모 메서드의 접근 지정자와 접근 범위가 같거나 커야 한다. 인터페이스의 모든 필드와 메서드는 public으로 강제되어있어서 인터페이스를 상속받는 모든 자식 클래스는 public 접근 지정자만 가져야한다.

인터페이스의 객체 생성 방법은 추상 클래스와 동일하다.

 

 

 

인터페이스의 디폴트 메서드

interface myInterface {
    public default void myMethod {
    
    }
}
myInterface가 여러 클래스에 상속되어 사용되고 있다고 가정하자. 무수히 많은 클래스에 사용되고 있는 와중에 인터페이스 내에 새로운 메서드가 추가된다면? myInterface를 상속하고 있는 모든 클래스에 오류가 발생할 것이다. 그 이유는 인터페이스에 정의된 메서드는 상속한 클래스에서 모두 구현되어야하는데, 새로 추가된 메서드는 모든 클래스에 구현되지 않아서 오류가 발생했다. 이런 문제점을 해결하고자 추가된 것이 default 메서드이다. 디폴트 메서드는 인터페이스 내부에 완성된 메서드를 삽입하여 자식 클래스에서 이 메서드를 반드시 오버라이딩 하지 않아도 되는 것이다. 

 

 

 

 인터페이스의 super

interface myInterface {
    default void myMethod(){
        System.out.println("MyInterface의 메서드 출력");
     }
}

class myClass implements myInterface {
    @Override
    public void myMethod(){
        myInterface.super.myMethod();
        System.out.println("myClass의 메서드 출력");
    }
}
myClass.myMethod( );를 호출한다면, super로 인해 인터페이스의 메서드가 호출된 후 그 다음에 클래스의 메서드가 호출될 것이다. super를 클래스에 사용할 땐, 그냥 메서드명 앞에 super.을 붙이면 됐는데 인터페이스의 경우 인터페이스명.super.메서드명을 붙였다는 차이가 있다. 아마 그 이유는 인터페이스는 다중 상속이 가능하기 때문에, 어떤 인터페이스의 메서드를 방문할 것인지 정확하게 적어야하는 듯 하다.

 

 

 

 인터페이스의 static

interface MyInterface {
    static void greet() {
        System.out.println("Hello from Interface!");
    }
}

public class Main {
    public static void main(String[] args) {
        MyInterface.greet(); 
    }
}
인터페이스에 static 메서드를 사용할 수 있는데, 이런 경우 Main 클래스에 보이는 것과 같이 객체를 생성하지 않고 바로 인터페이스의 greet 메서드를 호출할 수 있게 됐다. 이렇게 되면 오버라이딩 신경쓸 것 없이 독립적으로 존재 가능하며, static 메서드를 외부로 빼지 않아도 되어 개발자 입장에선 좀 더 편리해진다.

 

 

 

🤸🏻 🤸🏻 🤸🏻

 

 

 

 

포인터(Pointer)

  • 포인터는 변수의 주솟값을 저장하는 공간이다.
  • int *b = &a
  • int *b는 b가 어떤 int형 주소를 담을 수 있다는 의미이다.
  • = &a는 b에 a의 값을 저장하는 의미이다.

 

1. 기본 개념 

int a = 10;
int *b = &a;
a의 값은 10, a의 주소는 0x7ffee3b8dace

b의 값은 a의 주소가 되고, *b의 값은 a의 값이 된다.
이때 *과 &는 서로 상쇄하는 효과를 지닌다. 즉, *과 &가 같이 만나면 사라져서 해당 변수만 남게 된다. &는 "집 주소 좀 알려줘!"라는 의미라면, *은 "그 안에 사는 사람(값)을 알려줘!"라고 생각하면 된다. (사실 의미 하나하나를 이해하기보다, 그냥 상쇄된다는 것만 알아두고 1차원 배열, 2차원 배열 등 어떻게 쓰이는지 외우는게 문제풀이에 더 효과적이다)

*&a가 있다면, [ a의 집주소를 알려줘 -> 그 집에 누가있어? -> 10 ]
&*b가 있다면, [ 주소 b안에 누가있어? -> 10 -> 10의 주소를 알려줘 ] 가 된다.

 

 

 

int a = 20;
int *b = &a;

printf("%d %d %d", a, *b, *(&a));
a의 값은 20이다.
*b 안에는 &a(a의 값)이 담겨 있으니 20이다.
*(&a)는 *와 &가 상쇄되어 a의 값만 남아 20이 된다.

 

 

 

 

2. 1차원 배열

int a[3] = {1, 2};
int *p = a;

print("%d %d %d\n", *a, *(a+1), *(a+2));
print("%d %d %d\n", *p, *(p+1), *(p+2));

 


a가 1차원 배열을 가지게 되면서 int *p = a;로 a가 상수를 가졌을 때와 달리 a 앞에 &가 사라졌다. 그 이유를 살펴보면, int *p = &a 식에서 &a는 배열 a의 전체 주소를 가리키기 때문에 "int 3개 짜리 배열을 가리키는 포인터"라는 의미를 가지게 된다. 컴파일러마다 다르겠지만, int *p는 "한 개의 int를 가리키는 포인터"기 때문에 서로 타입이 일치하지 않아 경고 혹은 오류가 발생할 수 있다.
그래서 만약 int *p = &a로 사용하고 싶다면, int (*p)[3] = &a라고 써줘야한다. 

[ 공식으로 기억하자! ]
"자료형 배열명[요소];"인 경우,
➀ 배열+i == &배열[i]
➁ *(배열+i) == 배열[i] 

int *p = a는 배열 전체를 의미하기는 하나, 컴파일러가 알아서 ➀  int *p = &a[0]으로 변환한다. 그리고 *a는 *(&a[0])이 되어 *과 &가 상쇄되고 a[0]만 남게 된다. 그리고 *(a+1)은 *(&a[0]+1)이 되는데, +1이 괄호 안에 있으니 a[0] 다음 배열 a[1]을 뜻하게 된다. 그래서 결과적으로 *(&a[1])이 되고 *과 &는 상쇄되어 a[1]만 남아 2가 된다. *(a+2)는 똑같은 과정을 거쳐서 a[2]만 되는데 지금 배열이 {1,2}로 되어있지만 크기는 3이기 때문에 실질적으로 {1,2,0}의 형태이다. 그래서 a[2]는 0이 된다.

아까 설명에서 int *p = a;라고 했지만, 컴파일러가 알아서 ➀  int *p = &a[0]으로 바꿔준다고 했다. 그래서 p는 a[0]의 주솟값(&)을 가지고 있는데, ➁ p 앞에 *가 붙으니 &와 *이 상쇄되어 a[0]의 값을 알려달라는 의미로 변환하게 된다. 그러면 *p는 a[0]인 값 1이 되고 *(p+1)은 *(&a[0]+1)이 되어서 a[1]인 2가 되고, *(p+2)는 *(&a[0]+2)로 a[2]가 되어 0의 값을 가진다.

 

 

 

 

3. 2차원 배열

  • 2차원 배열과 1차원 포인터
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
int *p = a[1];

printf("%d %d %d\n", *a[0], *a[1], *a[2]);
printf("%d %d %d\n", **a, **(a+1), **(a+2);
printf("%d %d\n", *p, *(p+1));
printf("%d %d\n", p[0], p[1]);
위에서 ➀  a = &a[0]이 된다고 했었다. 여기도 똑같다. *a[0]은 *(&a[0][0]) 붙고 *과 &은 상쇄되어 a[0][0]만 남아 1을 출력한다. *a[1]은 *(&a[1][0])이 되고 a[1][0]으로 3을 출력한다.

**a도 같다. **(&a[0])이 되고 *와 &가 상쇄되어 *(a[0])이 된 후 다시 *(&a[0][0])으로 a[0][0]만 남는다. 그로인해 값은 1이 된다. 그냥 쉽게 생각해서 *를 없애고 싶으면 &를 가져와야하는데 &는 [0]과 한 묶음이라 같이 가져온다고 외우면 좋을 것 같다. 다시 이어서 **(a+1)도 살펴보자 **(&a[1])이 되고 *(a[1])이 되었다. *(&a[1][0])이 되고 서로 상쇄되어 a[1][0]만 남아 3을 출력한다.


p는 a[1]의 주솟값인 &a[1][0] 가진다. *p는 *(&a[1][0])이 되어 a[1][0]이 된다. 그러면 값은 3이 되는 것이다. *(p+1)은 *(&a[1][0]+1)로 a[1][0]다음 값인 a[1][1]이 된다. 그래서 4를 출력한다.

p[0]는 *(p+0)과 같다. 이유는 포인터 연산자를 더 살펴봐야하는데 간단하게 설명하자면 c언어에서 배열과 인덱스는 포인터 연산자로 변환 가능하기 때문이다. 그래서 *(p+0)은 p가 a[1]에서 시작하니 *(&a[1][0])이 되어 a[1][0]의 값인 3을 가진다. p[1]도 마찬가지다. *(p+1)이 되고 *(&a[1][1])로 4가 출력된다.

 

 

 

  • 2차원 배열과 포인터 배열
int a[3][2] = {{1,2}, {3,4}, {5,6}};
int *p[3] = {a[2], a[0], a[1]};

printf("%d, %d\n", *a[0], *a[1]);
printf("%d, %d\n", p[1][0], p[2][0]);
printf("%d, %d\n", *p[1], *p[2]);
*a[0]은 *(&a[0[0])이 되어서 1이 출력된다. *a[1]은 *(&a[1][0])으로 3이 출력된다.

p는 int * 3개짜리 배열이다. p[1][0]을 살펴보자. p[1]은 a[0]을 저장하고 있다. 그래서 p[1][0]은 *(p[1]+0) -> *(a[0]+0) -> *(&a[0][0])이 되어 1이 출력된다. p[2][0]은 p[2]가 a[1]을 저장하고 있으니, *(&a[1][0])으로 3이 된다.

p에 대해서 더 쉽게 생각할 수 있다. p의 0번째 행에 a의 2번재 행이 들어갔다고 생각하면 된다.

p[1] a[0][0]
1
a[0][1]
2
p[2] a[1][0]
3
a[1][1]
4
p[0] a[2][0]
5
a[2][1]
6

그러면 *p[1]는 *(&p[1][0])이 되어 1이 출력되고, *p[2]는 *(&p[2][0])이 되어 3이 출력된다.

 

 

 

  • 2차원 배열과 2차원 포인터
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
int (*p)[2] = a;
int (*q)[2] = a+1;

printf("%d %d %d \n", p[0][0], p[0][1], p[1][0]);
printf("%d %d %d \n", q[0][0], q[0][1], q[1][0]);
변수 p는 &a[0]을 가리킨다. 그러면 p[0]은 *(p+0)이 되고 *(&a[0])이 되면서 a[0]이 된다. 그러면 p[0][0]은 *(*(p+0)+0))이 되고 *(*(&a[0])+0) -> *(a[0]+0) -> *(&a[0][0]) -> 1이 된다. p[0][1]은 *(*(p+0)+1) -> *(*(&a[0])+1) -> *(&(a[0][1])) -> 2가된다.

변수 q는 &a[1]을 가리킨다. 그러면 q[0][0] -> *(*(q+0)+0) ->  *(*(&a[1])+0) -> *(&a[1][1]) -> 3이 된다. q[0][1]은  *(*(q+0)+1) ->  *(*(&a[1])+1) -> *(&a[1][2]) -> 4가 된다.

 

 

 

 

4. 구조체와 포인터

  • 일반 구조체 변수로 접근할 때 .으로 접근한다.
  • 구조체 포인터로 접근할 때는 ->로 접근한다. 

 

struct Student {
  char gender;
  int age;
};

void main(){
  struct Student s[3] = { 'F', 21, 'M', 20, 'M', 24};
  struct Student *p = s;
  
  printf("%c %d\n", s[0].gender, s[0].age);
  printf("%c %d\n", (*s).gender, (*s).age);
  printf("%c %d\n", s->gender, s->age);
  printf("%c %d\n", (s+1)->gender, (s+1)->age);
  printf("%c %d\n", p[0].gender, p[0].age);
  printf("%c %d\n", (*p).gender, (*p).age);
  printf("%c %d\n", p->gender, p->age);
  printf("%c %d\n", (p+1)->gender, (p+1)->age);
}

 

구조체에서 .은 일반 구조체 변수에 접근할 때 사용한다고 했다. 쉽게 생각하면 주소가 아닌 값에 바로 접근하면 .을 사용한다고 보면 될 것 같다. 위에 코드를 확인해보니, .은 s[0], *s, p[0], *p와 같이 그 값을 가리킬 때 사용했다. ->는 s, (s+1), p, (p+1)과 같이 &가 붙는 곳에 사용했다. 아마 위의 내용을 충분히 이해했다면 구조체 포인터는 어렵지 않게 바로 이해할 수 있을 것이다.

 

 

 

 

5. 문제

 

  • 문자열 
char *p = "KOREA";

printf("%s\n", p);
printf("%s\n", p+3);
printf("%c\n", *p);
printf("%c\n", *(p+3));
printf("%c\n", *p+2);

 


 

"KOREA"는 문자 6개 짜리 상수 문자열이다. 실제 메모리에는 K O R E A null 이렇게 총 6바이트가 저장된다. 여기서 "KOREA"는 문자열 상수의 첫번째 문자로 K의 주소로 자동 변환되면서 "KOREA"가 &"KOREA"[0]가 되고 결국 &p[0]이 된다. 문제를 풀면서 팁이라면, %s를 썼는지, %c를 썼는지를 보고 문자열이 출력되는지 문자 하나가 출력되는지 알 수 있다. 왜냐하면 %s는 문자열 포맷으로 문자열을 출력되는 방식이 주소값을 하나 받아서 그 주소에 들어있는 것을 null이 나올 때 까지 차례대로 읽기 때문이다.

[ 해설 ]
1. KOREA
: p는 주소를 저장하고 있다. 그래서 %s가 p의 첫번째 문자 주소로가서 null 값이 나올 때까지 차례로 읽기 때문에 KOREA가 출력된다. , &p[0]

2. EA
: p+3은 p가 K의 주소를 가리키고 있었으니 거기서 3개를 건너가면 E가 나온다. 그래서 E의 주소로 가서 null 값이 나올 때까지 차례로  읽으니 EA가 된다. , &p[3]

3. K
: *p는 p의 값을 말하는 것으로, p[0]의 값을 의미하여 K가 된다. , *(&p[0]) = p[0]

4. E

: *(p+3)은 p의 3번째 주소의 값을 의미하여 E가 된다. , *(&p[0]+3) = *(&p[3]) = p[3]

5. M
: *p+2는 *이 괄호 없이 p에 붙어있다. 그렇다는 것은 3번처럼 *p가 p[0]을 의미하며, 그 값에서 2를 더한 값이 된다. p[0]은 K이고 K 다음은 L 그 다음은 M이니 2를 더한 값은 M이 된다. , *(&p[0])+2 = p[0]+2 = M


[ 답 ]
1. KOREA
2. EA
3. K
4. E
5. M

 

 

  • 구조체 포인터 문제
struct Pointer1 {
  char name[10];
  int age;
};

void main() {
  struct Pointer1 p[] = {"Lee", 10, "Park", 19, "Baek", 21, "Yoon", 13};
  struct Pointer1 *s;
  p = s;
  p++;
  printf("%s\n", s->name);
  printf("%d\n", s->age); 
}

[ 풀이 ]
p = &s[0]으로 p는 "Lee"를 가리키는 주솟값을 가지고 있을 것이다. 그런데 printf로 출력하기 전, p++를 통해 &s[0]이 증가하여 &s[1]이 되었다. 그러면 "Lee"를 가리키던 주솟값은 "Park"으로 변동되었을 것이다. 이 상태에서 p -> name과 p -> age를 출력한다면, 현재 p는 s[1]의 주솟값을 가리키니 ->를 사용하여 나타냈고 각 name과 age는 Park와 19가 될 것이다.

[ 답 ]
Park
19

+ Recent posts