제출하지 못했던 10주차까지 완료!

 

프로세스?

스레드 단위 작업을 지원하기 위한 자원의 할당 단위이며, 커널 위에서 현재 실행 중인 프로그램을 의미한다.

 

프로세스 안에는 하나 이상의 스레드가 존재한다.

 

 

왜 멀티 프로세스 방식을 사용하지 않는가?

프로세스를 이용하여서도 멀티 프로세싱이라고 하는 방법을 이용하여 하나의 작업을 병렬적으로 처리할 수 있다. 하지만 이 방식은 각각의 프로세스가 쉽게 공유되지 않는 자신만의 데이터 영역들을 가지고 있기 때문에 데이터 처리, 공유 방식과 메모리 공간 점유에 대한 문제가 발생한다.

  • 작업을 진행할 때마다 프로세스의 작업 내용이 cpu로 로딩이 된다. 이후 다른 작업을 진행하여야 할 때 기존에 사용하는 것을 내리고 다른 프로세스를 로딩하게 되는데 스레드 방식보다 들어가는 비용이 크다.
  • 여러 프로세스가 하나의 작업을 같이 하는 상황에서 각각의 프로세스들은 다른 프로세스가 가지는 정보를 사용하기 위해 별도의 ICP(Inter Process Communication)가 필요하다.

 

하나의 작업을 병렬로 처리하고 진행하는 작업을 변경하는 과정에서 멀티 쓰레드를 사용한 작업 방식이 더 좋은 효율을 보인다.

 

 

스레드

프로세스 안에서 실제로 작업을 처리하는 단위를 의미하며, 가지고 있는 자원을 이용하여 연산을 처리하는 주체이다.

 

 

멀티 스레드

하나의 프로세스 내에서 둘 이상의 스레드가 여러 작업을 동시에 수행하는 방식을 말한다.

 

CPU 코어는 한 번에 하나의 작업만 수행하므로, 코어의 개수와 동시에 처리되는 최대 작업 개수는 동일하다.

 

 

멀티스레드의 장점

  • 오랜 시간이 걸리는 작업 때문에 Blocking 되지 않고 별도의 작업을 처리할 수 있다.
  • 작업 간의 대기 시간을 효율적으로 활용할 수 있다. 결과가 필요한 작업 외에는 처리한다.
  • 같은 시간 내에 더 많은 작업을 처리할 수 있다. 한도가 정해진 상황
    • 각 스레드의 처리가 동시에 끝나도록 잘 분할해야 하며,
    • 각 쓰레드의 처리가 최대한 다른 스레드를 의존하지 않게끔 해야 한다.

멀티스레드의 단점

  • 멀티 프로세스보단 덜하지만 싱글 스레드보다 사용하는 자원(메모리) 량이 크다.
  • 커널 스레드의 경우 전환할 때 오버헤드(Context Switching)가 발생하기에 총 처리시간이 단일 작업 처리보다 상대적으로 길다. 멀티 프로세스 방식보단 덜하다.
  • 동시성 특유의 문제가 발생한다.
    • 잘못된 데이터의 덮어쓰기
    • 교착 상태의 발생 (각 스레드가 상대방의 리소스가 해제되는 것을 기다리는 상황)
    • 순서가 정해지지 않은 스레드들의 처리 때문에 예외가 발생할 수 있다.
      • 어떠한 자원을 자료구조에 넣어놨을 경우 필요한 스레드가 접근하기 이전에 다른 쓰레드가 가져가게 되고, 그에 따른 예외가 발생한다.
    • 무한 루프가 발생할 수 있다. (Hashmap의 put 연산 등)

그렇기에 구현 시 스레드의 안정성을 고려하여야 한다.

 

 

쓰레드의 안정성?

여러 스레드가 어떤 변수나 함수 또는 클래스 객체에 접근할 때 계속해서 개발자가 의도한 대로 정확하게 동작한다는 것으로 정의한다. 동기화 방식 이외에도 호출하는(사용하는) 쪽에서 동기화 코드 없이도 올바르게 동작할 수 있음을 의미한다.

 

멀티 스레드의 궁극적인 목표는 쓰레드 안정성을 지키면서 성능을 최대한 뽑아내는 것이다.

 

이를 지키기 위해 사용되는 것으로는 불변 객체 구현, Atomic API, 순수 함수 등이 있다.

 

 

스레드의 종류

커널 래벨 (네이티브) 스레드란?

커널을 통해 관리되는 커널 종속적인 스레드이다.

  • 애플리케이션 프로세스의 첫 번째 스레드는 커널 스레드이다.
  • 프로그래머의 요청에 따라 스레드를 생성하고 해당 스레드가 커널을 통해 스케줄링된다면, 커널 레벨 스레드라고 한다.

 

커널 래벨 (네이티브) 스레드의 장점

하나의 프로세서 안에 스레드들을 몇몇 프로세서에 한꺼번에 디스 패치하는 것이 가능함으로 멀티 프로세스 환경에서 매우 빠르게 동작한다.

  • 디스패치? : 준비 상태에 있던 스레드가 CPU에 할당받아 실행되는 것을 말한다.
  • 다른 스레드가 입출력 작업이 끝날 때까지 다른 쓰레드를 사용하여 작업을 진행할 수 있다.
  • 커널이 각 쓰레드를 개별적으로 관리할 수 있다.
  • 쓰레드 작업 시에 Context Switching 이 발생한다.

 

커널 래벨 (네이티브) 스레드의 단점

  • 쓰레드의 관리를 위하여 커널을 호출하는데 일정 시간이 소비된다.
  • 스레드 간의 동기화 문제가 발생할 수 있으며, 그에 따른 처리가 복잡하다.
  • 사용자 스레드에 비하여 자원을 더 많이 소비한다.

 

사용자 (그린) 스레드란?

사용자 영역에서 연산을 수행한다.

  • 프로세스 내부에 존재하는 Main 스레드를 통해 호출되며, Context Switching을 하지 않기에 커널 스레드보다 오버헤드가 적다.
    • Main Thread : Main Method를 실행하는 스레드, JVM이 Application 구동을 위해 사용.
  • 프로세스 내의 하나의 스레드가 커널로 진입하게 된다면, 모든 쓰레드가 Blocking 된다.
  • 사용자 스레드는 커널을 통해 스케줄링이 되지 않기에, 각 CPU에 효율적으로 스레드를 분배할 수 없다.

 

Thread behavior in the JVM

 

Thread behavior in the JVM

The JVM does what it wants to do, so how can you predict the order of thread execution?

www.infoworld.com

 

데몬 스레드란?

(프로세스의) 메인 스레드의 작업을 돕는 보조적인 역활을 하는 쓰레드를 말한다.

  • JVM의 Main 스레드의 작업이 종료된다면, 데몬 쓰레드는 동작을 중지한다.
  • 쓰레드 생성 시에 SetDaemon() 메서드를 이용하여 설정할 수 있다.

 

JVM에서의 데몬 스레드?

  • VM Background Thread : Compile, Optimization, GC를 수행하는 Background 데몬 스레드.
  • 기타 User Thread : thread.SetDaemon()

 

 

Concurrency, Parallelism, Parallel and Concurrency

 

Concurrency?

https://blog.kakaocdn.net/dn/XHuvv/btqUo8YDtkZ/EDBIYFN00kik3nGk1khCAk/img.png

하나의 코어가 여러 프로세스를 번갈아가며 실행하는 것을 의미한다. 이는 사용자에게 동시에 실행되는 것처럼 보이게 만드는 효과를 가지며, 단위 시간 내에 더 많은 일을 처리한다.

 

프로세스 간의 콘텍스트 스위칭이 발생한다.

 

Concurrency의 장단점

장점

  • CPU의 처리량이 증가한다.
  • 자원의 활용도가 증가한다.
  • 프로세스 간의 대기시간이 감소된다.

단점

  • Context Switching에 대한 Overhead가 발생한다.

 

Parallelism?

하나의 프로세스를 분할하여 처리

https://blog.kakaocdn.net/dn/lsJcS/btqUtXB0wCK/whsWh7APILE2QEkbHJpnP0/img.png

여러 개의 코어가 하나의 프로세스의 작업을 분할하여 처리하는 것을 의미할 수 있다. 이는 내부적으로 동작하는 스레드의 개수만큼 CPU에 할당할 수 있음을 의미한다.

 

화면을 랜더링 하는 스레드, 계산을 진행하는 스레드, 서버와 통신하는 스레드 등...

 

한 번에 여러 프로세스를 실행

https://blog.kakaocdn.net/dn/cYwXYo/btqUseRMDBc/AX17UVqB6Xrb59UMMCp1Rk/img.png

이와 같이 각 코어가 별개의 프로세스를 동작시킴으로써 단위 시간 내에 여러 프로세서를 동작시키는 것을 의미할 수도 있다.

 

Parallelism의 장단점

장점

  • 하나의 프로세스를 분할하여 여러 작업으로 처리할 수 있다.
  • 하나의 작업에 대해 가용 자원을 더 많이 할당할 수 있다.
  • 여러 프로세스에 대해서도 동시에 수행할 수 있다.

단점

  • 단일 코어 방식보다 어려운 작성 방식을 가진다.
  • 프로세스 분할 처리 시 발생하는 추가 비용이 더 크다. 데이터 전송, 동기화, 통신, 전환 등
  • 각각의 시스템 아키텍처에 맞게 알고리즘 로직의 조정이 필요하다.

 

Parallel and Concurrency

https://blog.kakaocdn.net/dn/bzCy2i/btqUvCxoJng/dkGcWQfs1JGR9bkIY6CbU1/img.png

여러 개의 코어에서 여러 프로세스들을 번갈아 실행하는 상황을 의미한다.

 

물리적인 개념(Parallel)과 논리적인 개념(Context Switching)이 연계된 것이다.

 

 

Thread 클래스와 Runnable 인터페이스

자바에서 멀티 스레드 방식을 지원하기 위해 존재하는 API이다.

 

 

Java에서 Thread를 생성하는 방법

  1. Runnable 인터페이스를 구현하는 것 필요한 기능만 포함하는 구현체가 필요한 경우
  2. Thread Class를 사용하는 것 일반적인 로직 처리 상에서 Async 한 메서드, Thread Pool 사용 시
  3. Thread Class를 상속받는 것 기존 스레드 기능을 확장해야 하는 경우

 

스레드의 상태

효율적인 스레드 관리를 위해서 알아야할 쓰레드 상태 값

 

NEW : 스레드가 생성되었지만, 아직 시작되지 않은 상태

 

RUNNABLE : 실행 중 또는 실행 가능한 상태

 

BLOCKED : 동기화 블록에 의해서 실행 중지된 상태 lock이 풀릴 때까지 대기

 

WAITING : 스레드의 작업이 대기하고 있는 상태

 

TIMED_WAITING : 특정 시간만큼 대기하도록 지정된 상태

 

TERMINATED : 스레드가 종료된 상태

 

 

스레드의 우선순위

자바 스레드에는 우선순위라는 상태 변수가 존재한다. 이 값에 따라 스레드가 접근 권한을 얻을 때까지의 대기시간이 달라질 수 있는데, 우선순위가 높은 쓰레드가 자주 동작하는 로직을 처리하는 경우 낮은 우선순위를 가지는 스레드는 처리되지 못하고 기아 상태에 빠질 수 있다.

private int priority;

// 우선 순위 상수 값

public static final int MAX_PRIORITY = 10 // 최대 우선 순위

public static final int MIN_PRIORITY = 1 // 최소 우선 순위

public static final int NORM_PRIORITY = 5 //보통 우선 순위

// 우선 순위 관련 메서드

public final int getPriority() {
    return priority;
}

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}

 

 

동기화?

여러 개의 스레드가 하나의 변수, 객체를 사용하고자 할 때, 선점한 스레드를 제외하고 나머지 스레드들은 접근할 수 없도록 대기하게 하는 것이다.

 

자바에서는 synchronized를 사용하여 해당 기능을 지원한다.

  • Java의 synchronized 범위는 Object 단위의 단일 개체 잠금이다.
    • 모든 Object는 하나의 모니터를 지니고 있다.
  • 하나의 스레드가 Monitor의 Lock을 획득하면 내부의 다른 임계 영역에도 접근할 수 있다.
  • 스레드가 객체 내부의 임계영역에 접근할 경우, Lock 을 통해서 접근하게 되며, 다른 쓰레드는 해당 쓰레드가 임계영역에서 나올 때까지 Wait Queue 에서 대기하여야 한다.

 

synchronized Method

  • 해당 Method를 사용하기 위해서는 우선적으로 Lock을 획득하여야 한다.

  • 해당 Method가 Instance Method인 경우 Method를 호출하는 객체에 대하여서 Lock을 획득한다.

    어떠한 스레드가 해당 객체의 method 단위의 임계 영역을 접근한다면, 이 객체의 다른 메서드를 사용하는 스레드들도 모두 Block 상태가 된다.

     

      class b {
    
      A a = new A();
          a.someMethod(); // instance a 에 대한 Lock을 획득하고 호출한다.
    
      }
    
      class A {
          public synchronized void someMethod() {
              // Do SomeThing
        }
    
      }
  • Class (static) Method인 경우 해당 method가 속해진 Class Instance에 대한 Lock을 획득한다.

    즉 해당 class insatance가 여러 개가 존재하더라도, 해당 메서드에 대해 동시 접근이 불가능하다.

    왜냐하면 Class (Static) Object는 Heap 영역에 하나만 존재하기 때문이다.

      class B {
          A.someMethodIsStatic(); // class A 에 대한 Lock을 획득하고 호출한다.
      }
    
      class A {
          public synchronized static void someMethodIsStatic() {
              // Do SomeThing
        }
    
          public synchronized void someMethod() {
              // Do SomeThing
        }
      }

 

 

synchronized Statement

해당 방식은 런타임 내의 분기를 이용하여 동기화가 필요한 작은 공간에서만 Lock을 획득하게끔 합니다.

if (조건식) {
        synchronized (object) {
                // do something
        }
} else {
        // do something
}

 

해당 조건이 맞는 경우에만 동기화를 진행하고 로직 실행

 

Object 내부의 동기화 관련 메서드

 

wait

해당 Object를 선점하고 있다가 접근 권한을 다른 스레드에게 넘기고 notify, notifyAll 이 호출될 때까지 해당 스레드를 대기하게끔 설정하는 메서드이다.

  • Wait Set에 들어가게 된다.
  • 추가 : Block 은 아직 접근조차 하지 못한 상태를 말하는 것이다.

 

notify

해당 Object의 선점을 위해 Wait Set에 대기 중인 하나의 스레드를 실행 대기 상태로 만든다.

  • Entry Set의 스레드들과 같이 경합에 참여한다.

 

notifyAll

해당 Obejct의 선점을 위해 Wait Set에 대기 중인 모든 스레드를 실행 대기 상태로 만든다.

  • notify와 동일한 경합 상태가 발생한다.

 

Object 선점 흐름

 

Object에 대한 Thread 초기 접근.

  • Thread → Entry set → Monitor Lock 획득 시도
  • 선점 Thread Wait() → notify, notifyAll → 대기 중인 Thread 중 Monitor Lock 획득

 

Object에 대한 Thread 접근 (Wait 이후)

  • Wait set → notify, notifyAll → 대기중인 Thread 중 Monitor Lock 획득

Wait Set의 Thread가 임계 영역을 벗어나기 위해서는 접근 권한을 선점하고 Lock을 놓는 방법뿐이다.

 

 

 

synchronized vs java lock?

1.5에 추가된 Concurrent 패키지의 Lock 은 기존의 synchronized 블록을 확장한 좀 더 유연하고 정교한 동기화 방식이다.

 

독점적인 락을 사용해야 할 때, 여러 범위에서 락을 확보하고 사용되는 경우, 세심한 락 조절을 통해 독점적인 락을 사용하는 부분과 시간을 최소화하고, 여러 유틸 성 기능을 제공할 수 있게끔 지원한다.

 

 

synchronized 와의 차이점

  • 별도의 synchronized Statement 등을 만들어 사용하던 방식을 사용하지 않고도 메서드를 통하여서 임계 영역을 지정, 제공할 수 있다. (lock(), unlock())

  • synchronized를 사용할 때 발생했던 문제인 기아상태에 대한 해결책을 지원한다. 공정성 제공 fairness Property

    • synchronized는 스레드 들의 진입 순서를 보장하지 않는다.

      Thread의 기아상태가 발생한다.

  • tryLock()을 통해 해당 임계 영역 접근 불가능할 때 스레드를 차단하지 않고 가능한 경우에만 잠금을 획득하게끔 지원하고, 스레드가 차단된 시간을 감소시킵니다.

  • 임계 영역에 대한 접근 권한을 얻기 위해 대기 중인 스레드는 인터럽트 할 수 없다.

synchronized는 스레드의 진입 순서를 보장하지 않는 반면에 Lock은 진입 순서를 보장한다.

 

 

 

Thread의 기아상태?

다수의 스레드가 동일한 임계영역에 계속해서 접근을 한다면, 하나 이상의 쓰레드가 접근 권한을 받지 못하는 경우가 발생할 수 있다.

 

기아 상태의 원인

  • 높은 우선순위에 따른 스레드들의 CPU 독점

    자바에선 쓰레드들 각각에 대한 우선순위를 설정할 수 있다. (1~10) 일반적으로는 우선순위를 지정하지 않고 사용하는 것이 좋다.

  • 임계 영역 동기화로 인하여 진입 대기 중 Block 상태 (기아 상태)

    스레드가 임계영역으로 진입하기 위해 계속 접근 시도를 하지만, 다른 쓰레드 들의 진입권 획득으로 인하여 영원히 Block된 상태로 남아있을 수 있다는 것이다.

  • wait 호출로 인한 객체에 대한 무한한 대기 상태

    둘 이상의 쓰레드가 동일한 객체의 wait 호출로 인하여 대기 상태에 놓였을 때. notify 메서드는 어떤 스레드를 깨울 것인지 알 수 없다. 깨어나지 못하고 계속 대기할 수 있다.

이 문제를 해결하기 위해선 공정성의 개념을 지원하여야 한다.

 

 

 

Volatile long vs Atomic long

Volatile

해당 변수를 메인 메모리에서만 접근 가능하게끔 하는 것을 말한다.

  • 읽기 연산, 쓰기 연산 등의 결과를 Cache 메모리가 아닌 메인 메모리에서 반영한다.
  • volatile 변수와 같이 사용되는 모든 변수들도 메인 메모리에서 함께 조작된다.
  • Cache 메모리에 변수의 값을 복사해가지 않는다.

스레드 간의 가시성을 해결하기 위한 것으로, 각 스레드는 다른 스레드에서 기록하지 않은 값을 확인할 수 없기에 발생하는 것이 가시성이며, 잘못된 결과로 나타날 수 있다.

 

volatile 만을 사용해볼 만한 상황.

하나의 스레드만이 해당 변수에 대한 연산을 진행하고, 다른 모든 스레드가 조회만 할 경우

  • 읽는 스레드에서 가장 최근에 쓰인 값을 보는 것을 보장한다.

 

volatile의 성능 이슈.

JVM은 프로그램 성능의 향상을 위해 내부 코드에 대하여서 Code reordering를 진행하게 되는데, 동기화되지 않는 코드나 Volatile에 대하여선 진행을 하지 않기에, 상대적으로 성능상의 문제점이 있을 수 있다.

 

사용하지 못하는 상황

여러 스레드가 읽기, 쓰기 연산을 모두 조작하는 경우에는 변경점이 많으므로 문제가 발생할 수 있다.

 

여러 쓰레드 캐싱 값을 쓰기에 메인 메모리에 존재하는 변수 값을 이상하게 덮어쓸 수 있다.

 

이 상황을 해결하기 위해 사용하는 것이 Atomic 패키지이며, Atomic 변수의 개념은 volatile 변수에 대하여서 원자성을 만족하는 연산을 제공하는 것이다. CAS 알고리즘

 

 

 

간단하게 CAS 짚고 넘어가기

CAS에 대한 세 가지 매개변수

  • 현재 값을 교체해야 하는 메모리 위치 V
  • 제일 최근에 스레드를 통해 읽은 값 A
  • V 위치에 덮어써야 되는 새로운 값 B

 

V는 A라는 값을 가지고 있어야 되며, 해당 경우 B를 덮어쓰면 된다. (V == A , V = B)

  • 같은 위치, 연산이라고 판단하게 된다.

그렇지 않은 경우에는 값을 반영하지 않고 재시도한다.

 

 

 

데드락

프로세스, 스레드가 작업에 필요한 모든 자원을 얻지 못하여 선점한 자원을 가지고 대기 중인 상태로 교착 상태라고도 한다.

 

멀티프로그래밍 환경에서 한정된 자원을 여러 곳에서 사용하려 할 때 발생할 수 있다.

 

데드락의 발생 조건

  • 상호 배제

    자원은 한 번에 한 프로세스만이 사용할 수 있어야 한다.

  • 점유 대기

    최소환 하나의 자원을 점유하고 있으면서, 다른 프로세스에 할당되어 사용되고 있는 자원을 사용하기 위해 대기하고 있어야 한다.

  • 비선점

    다른 프로세스, 스레드에 할당된 자원의 접근 권한은 그 작업이 끝나기 전까지 뺏어올 수 없다.

  • 순환 대기

    각각의 프로세스, 스레드가 상대의 자원을 점유하는 상태여야 한다.

'Live Study' 카테고리의 다른 글

Live Study_Week 15. Lambda Expression  (0) 2021.03.01
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

 

 

함수형 프로그래밍이란?

순수 함수들을 조합하여 사이드 이펙트(부작용)를 제거하고, 모듈화를 높여 유지보수와 생산성을 올리는데 초점을 둔 패러다임이다. Non Blocking과 Asynchronous , Parallel Programming을 구현, 지원하는데 적합하다고 한다.

 

함수형 프로그래밍의 사고방식은 문제 해결에 대해 선언적인 행위(함수)들을 조합(구성)하여 해결하는 것이다.

자바도 스칼라, 자바스크립트와 같은 함수형 패러다임 언어 혹은 지원하는 언어, 기술들의 대두로 인하여 JDK 8부터 해당 기능을 도입하게 되었다.

 

함수형 인터페이스, 람다, 메서드 레퍼런스, 디폴트 메서드, Future, Fork-Join, 리액티브 등 추가

 

 

1급 객체

함수형 프로그래밍의 중요한 조건 중 하나를 의미한다. 이는 변수나 데이터 구조 안에 넣을 수 있고, 인자로 전달 가능하고, 동적으로 속성 값을 할당 가능하며, 리턴 값으로도 사용될 수 있는 메서드를 말한다.

 

 

순수 함수

같은 입력에 대해서 항상 같은 출력을 반환하는 형태의 메서드를 의미한다. 이는 인자로 주어지는 것들만 사용하고, 상태를 유지하지 않으며, 함수 외부 변수를 사용하지 않는 형태이다. 멀티스레드 환경에서 안전한 메서드.

public int minus(int a, int b) {
    return a - b;
}

 

 

고차 함수

1급 객체의 서브셋으로 메서드의 인자로 전달할 수 있고, 리턴 값으로 사용할 수 있는 메서드를 의미한다.

 

자바에서의 고차 함수는 하나 이상의 인자로 Lambda 식을 가지고 있거나, Lambda 식을 반환하는 메서드를 말한다.

default Comparator<T> reversed() {
    return Collections.reverseOrder(this);
}

 

 

익명 함수

이름이 없는 함수를 의미하며, 이는 Lambda expression으로 구현되는 메서드를 의미한다.

 

 

합성 함수

원하는 값을 도출하기 위해, 둘 이상의 메서드를 조합하는 것을 말한다. Stream API 처럼 데이터가 흐르는 파이프라인을 구성하고, 필요한 메서드를 연속적으로 호출하여 구현한다.

 

 

 

람다식 사용법

Example

람다는 익명 클래스를 단순화하고 표현식을 메서드의 인수로 넘기거나, 객체를 생성하는 데 사용한다.

 

익명 객체 생성하기

Thread thread = new Thread(new Runnable() {

        @Override
        public void run() {
                System.out.println("Hello Lambda!");
        }

});

thread.start();

------------------------

// 익명 객체 내부에 한줄 코드만 존재하는 경우, { }와 return을 생략하고 작성할 수 있다.

Thread thread = new Thread( () -> System.out.println("Hello Lambda!") ); 

------------------------

// 2줄인 경우

Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello Lambda!");
            System.out.println("Line");
        }
});

------------------------

Thread thread = new Thread(() -> {
        System.out.println("Hello Lambda!");
        System.out.println("Line");
});

 

 

메서드의 인수로 넘기기

// JDK Dynamic Proxy code
TargetObject getRealObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            TargetObject targetObject = new TargetObjectImpl();

            System.out.println("Before");
            Object invoke = method.invoke(targetObject, args);
            System.out.println("After");

            return invoke;
        }});

------------------------

// InvocationHandler 인터페이스의 익명 객체를 Lambda Expression으로 대체하였다.

TargetObject realObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class},
            (proxy, method, args) -> {
                TargetObject targetObject = new TargetObjectImpl();

                System.out.println("Before");
                Object invoke = method.invoke(targetObject, args);
                System.out.println("After");

                return invoke;
            });

 

 

인자 값 사용(소비) 하기

인자로 전달된 값을 사용하여 데이터를 처리하고 로직을 완료한다. 리턴 타입은 void이다.

String value = "val";
someMethod(value, (value) -> System.out.println(value));

 

 

불 값 리턴하기

인자로 전달된 값을 기반으로 불 값을 리턴한다. 주로 값의 유효성 검증 및 비교 작업을 담당한다.

String value = "val";
someMethod(value, (value) -> "val".equals(value));

 

 

두 객체 비교하기

각각의 타입으로 전달된 두 객체를 비교하여 결과 값을 리턴한다.

String value1 = "val";
String value2 = "val";

Boolean result = someMethod(value1, value2, (value1, value2) -> value1.equals(value2));

 

 

객체 생성하기

인자로 전달되는 것 없이 객체를 생성한다. 리턴 타입은 void이다.

Lob lob = someMethod(() -> new Lob());

 

 

객체를 변경하기

인자로 전달된 값을 변경해서 다른 객체로 리턴한다.

String value = "hello";
String subString = someMethod(value, (value) -> value.substring(0, 3))

 

 

값을 조합, 병합하기

인자로 전달된 값을 조합해서 새로운 값을 리턴한다.

String prefix = "hello ";
String suffix = "world";
String fullText = someMethod(prefix, suffix, (prefix, suffix) -> prefix + suffix);

 

 

 

함수형 인터페이스 ( java.util.function )

하나의 추상 메서드를 가지고 있는 인터페이스나 @FunctionaInterface Annotation이 작성된 인터페이스를 말한다.

 

@FunctionaInterface는 함수형 인터페이스 형식을 제약 사항으로 지정한다.

 

Function <T, R>

T라는 타입의 한 인자를 받아서 R 타입으로 반환하는 함수형 인터페이스이다.

 

받은 인자를 다른 값으로 변환해서 리턴할 때, 값을 변경하거나 매핑할 때 사용한다.

 

R apply (T value)

compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

public static Long parseLong(String value, Function<String, Long> function) {
    return function.apply(value);
}

----------------------

public static void main(String[] args) {

        System.out.println(parseLong("100", Long::valueOf));
}

 

유사한 함수형 인터페이스

  • BiFunction <T, U, R>

    각각의 타입을 가지는 두 인자를 받아서 다른 타입으로 반환하는 함수형 인터페이스이다.

     

    R apply(T t, U u);

    compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

     

    String, Integer  -> Long

      public static Long parseLong(String value1, Integer value2 , BiFunction<String, Integer, Long> function) {
          return function.apply(value1, value2);
      }
    
      ----------------------
    
      public static void main(String[] args) {
    
              System.out.println(parseLong("100", 100, (value1, value2) -> Long.parseLong(value1)+value2));
      }
  • DoubleFunction

    입력되는 인자가 double인 함수형 인터페이스이다.

    R apply(double value);

     

  • DoubleToIntFunction

    입력되는 인자가 double, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(double value);

     

  • DoubleToLongFunction

    입력되는 인자가 double, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(double value);

     

  • IntFunction

    입력되는 인자가 int인 함수형 인터페이스이다.

    R apply(int value);

     

  • IntToDoubleFunction

    입력되는 인자가 int, 반환 타입은 int인 함수형 인터페이스이다.

    double applyAsDouble(int value);

     

  • IntToLongFunction

    입력되는 인자가 int, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(int value);

     

  • LongFunction

    입력되는 인자가 long인 함수형 인터페이스이다.

    R apply(long value);

     

  • LongToDoubleFunction

    입력되는 인자가 long, 반환 타입은 double인 함수형 인터페이스이다.

    double applyAsDouble(long value);

     

  • LongToIntFunction

    입력되는 인자가 long, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(long value);

     

  • ToDoubleBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 double를 반환하는 함수형 인터페이스이다.

    double applyAsDouble(T t, U u);

     

  • ToIntBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToIntFunction

    T라는 타입의 한 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T value);

     

  • ToLongBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 long을 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToLongFunction

    T라는 타입의 한 인자를 받아서 long를 반환하는 함수형 인터페이스이다.

    long applyAsLong(T value);

     

Consumer

T라는 타입의 한 인자를 받아서 소모하고 아무것도 반환하지 않는 함수형 인터페이스이다.

 

인자를 전달하여 처리한 뒤 결과를 리턴 받을 필요가 없을 때 사용한다.

 

void Aceept(T t)

forEach() 등의 메서드를 구현하는 데 사용된다.

public static void printList(List<String> list, Consumer<String> consumer) {
    for (String item : list) {
        consumer.accept(item);
        }
}

----------------------

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        printList(list, System.out::println);
}

 

유사한 함수형 인터페이스

  • BiConsumer <T, U>

    입력되는 인자가 2개인 함수형 인터페이스이다.

    void accept(T t, U u);

     

  • DoubleConsumer

    기본형 타입인 double의 인자를 사용하는 함수형 인터페이스이다.

    void accept(double value);

     

  • IntConsumer

    기본형 타입인 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(int value);

     

  • LongConsumer

    기본형 타입인 long 인자를 사용하는 함수형 인터페이스이다.

    void accept(long value);

     

  • ObjDoubleConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 double 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, double value);

     

  • ObjIntConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, int value);

     

  • ObjLongConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 long 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, long value);

     

Supplier

T라는 타입의 한 인자를 반환하는 함수형 인터페이스이다.

 

여기서 T는 받는 인자가 아닌 반환 타입을 지정한다.

 

T get();

orElseThrow(), orElseGet(), requireNonNull() 등의 메서드를 구현하는 데 사용된다.

public static String executeValue(Supplier<String> supplier) {
    return supplier.get();
}

----------------------

public static void main(String[] args) {

        String val = "value";
    System.out.println(executeValue(() -> val) );
}

 

유사한 함수형 인터페이스

  • BooleanSupplier

    boolean 값을 반환하는 함수형 인터페이스이다.

    boolean getAsBoolean();

     

  • DoubleSupplier

    double 값을 반환하는 함수형 인터페이스이다.

    double getAsDouble();

     

  • IntSupplier

    int 값을 반환하는 함수형 인터페이스이다.

    int getAsInt();

     

  • LongSupplier

    long 값을 반환하는 함수형 인터페이스이다.

    long getAsLong();

 

Predicate

T라는 타입의 한 인자를 받아서 그에 대한 Boolean 값을 제공하는 함수형 인터페이스이다.

 

주로 데이터를 필터링하거나, 조건에 맞는지 여부를 확인하는 데 사용한다.

 

boolean test(T t);

removeIf(), filter() 등의 메서드를 구현하는 데 사용된다.

public static boolean check(String value, Predicate<String> predicate) {
    return predicate.test(value);
}

----------------------

public static void main(String[] args) {

        System.out.println(check("hello", (value) -> value.length() > 4) );
}

 

유사한 함수형 인터페이스

  • BiPredicate <T, U>

    각각의 타입을 가지는 두 인자를 받고 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(T t, U u);

     

  • DoublePredicate

    double 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(double value);

     

  • IntPredicate

    int 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(int value);

     

  • LongPredicate

    long 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(long value);

     

Operator

특정한 정수, 실수형 데이터를 처리하는 데 사용되는 함수형 인터페이스이다.

 

Operator 인터페이스

  • UnaryOperator extends Function <T, T>

    T라는 타입의 한 인자를 받아서 해당 타입으로 반환하는 함수형 인터페이스이다.

     

    내부적으로 Function을 상속받았다.

      static <T> UnaryOperator<T> identity() {
          return t -> t;
      }

 

  • BinaryOperator extends BiFunction <T, T, T>

      public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
      }
    
      public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
      }

 

  • DoubleBinaryOperator

    double 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    double applyAsDouble(double left, double right);

     

  • DoubleUnaryOperator

    인자와 반환 타입이 double인 메서드를 제공하는 함수형 인터페이스이다.

    double applyAsDouble(double operand);

     

  • IntBinaryOperator

    int 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    int applyAsInt(int left, int right);

     

  • IntUnaryOperator

    인자와 반환 타입이 int인 메서드를 제공하는 함수형 인터페이스이다.

    int applyAsInt(int operand);

     

  • LongBinaryOperator

    long 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    long applyAsLong(long left, long right);

     

  • LongUnaryOperator

    인자와 반환 타입이 long인 메서드를 제공하는 함수형 인터페이스이다.

    long applyAsLong(long operand);

     

 

번외 : Runnable

해당 인터페이스는 JDK 1.0 부터 존재해왔던 오래된 인터페이스이지만, 자바의 함수형 인터페이스 제약을 지키고 있기에 추가하였다.

 

인자 타입과 반환 타입이 존재하지 않는 메서드를 제공하는 (논리적인) 함수형 인터페이스이다.

 

public abstract void run();

Runnable runnable = () -> System.out.println("class.run");
runnable.run();

 

 

번외 : Comparator<T>

해당 인터페이스는 JDK 1.2 부터 존재해왔던 인터페이스로 역시 자바의 함수형 인터페이스 제약을 지키고 있다. 

@FunctionalInterface도 타입 래벨에 정의되어 있음을 알 수 있다.

 

 

T라는 타입을 가지는 두 인자를 받아 int 값을 반환하는 메서드를 제공하는 함수형 인터페이스이다. 

객체, 값 객체 간의 우선 순위 즉 정렬을 위한 값을 얻어내는데 주로 사용된다.

 

int compare(T o1, T o2);

 Arrays.sort(split, (o1, o2) -> o2.compareTo(o1));

 

 

 

Variable Capture (Lambda Capturing)

Lambda의 body에서 인자로 넘어온 것 이외의 변수를 접근하는 것을 Variable Capture라고 한다.

 

 

Lambda는 인스턴스, 정적 변수final로 선언된 혹은 final처럼 사용되고 있는 지역 변수를 참조할 수 있다.

 

지역변수를 사용할 때에는 해당 변수에게 값의 재할당이 일어나서는 안된다.

// final int value = 100;
// 값을 통한 초기화 이후에 value의 값은 변경되어서는 안된다.
int value = 100;

// 순수 함수 형태가 아닌 사용 방식.
Lob lob = () -> new Lob(value);

--------------------
예제

@FunctionalInterface
interface Lob {
    public abstract void print();
}

//private final int a = 10; 도 동일하게 재정의가 가능하다.
private int a = 10;

public void hello() {

    final int b = 20;
    int c = 30;
    int d = 40;

    final Lob lobA = () -> System.out.println(a);
    lobA.print();

    // a 재정의
    a = 20;
    final Lob lobB = () -> System.out.println(a);
    lobB.print();

    final Lob lobC = () -> System.out.println(b);
    lobC.print();

    final Lob lobD = () -> System.out.println(c);
    lobD.print();

    final Lob lobE = () -> System.out.println(d);
    lobE.print();
}

public static void main(String[] args) {
        Lambda lambda = new Lambda();
        lambda.hello();
}

 

왜 지역 변수를 재정의해서 사용할 수 없는가?

이는 지역 변수가 스택 영역에 존재하기에 발생하는 문제점인데, 해당 변수를 초기화하는 스레드가 사라져 변수 할당이 해제된 경우에, Lambda를 실행하고 있는 별도의 스레드가 해당 변수 정보에 접근하려는 경우가 발생할 수 있다.

 

지역변수는 스레드 간에 공유가 되지 않는다.

 

자바에서는 람다를 실행하는 스레드의 스택에 지역 변수 할당 시 생성한 변수의 복사본을 저장하여 동작시키게 되는데, 이 값이 변경되었을 경우를 예측할 수 없기에 final 혹은 재할당 방지 제약조건을 걸어둔 것이다.

 

 

인스턴스 변수와 정적 변수는 왜 이런 제약조건을 걸지 않았는가?

이는 앞서 말했던 스레드 간의 가시성 문제의 연장선인데, 인스턴스 변수와 정적 변수는 모든 스레드에서 접근 가능한 값이기 때문에, 값의 변경이 이루어져도 직접적으로 접근할 수 있다.

 

 

 

메서드, 생성자 레퍼런스

메소드 참조?

JDK 8에 추가된 기능으로 함수를 메서드의 인자로 전달하는 것을 메서드 참조라고 한다.

 

해당 방식을 사용함으로써 해당 메서드 시그니처를 여러 곳에서 재사용할 수 있고, 기본적인 제공 메서드와 커스텀한 메서드 모두를 사용할 수 있다는 장점이 있다.

 

추가적으로 메서드 참조는 람다 표현식을 한번 더 축약적으로 표현할 수 있으며, 그를 통해 가독성을 향상할 수 있다.

 

람다 표현식을 대체하기보다는 상호 보완적인 관계를 형성한다.

// example

public static void printList(List<String> list, Consumer<String> consumer) {
        for (String item : list) {
            consumer.accept(item);
        }
}

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        // 람다 표현식
        printList(list, (item) -> System.out.println(item));

        // 메서드 참조
        // 한 단계 더 축약된 것을 알 수 있다.
        printList(list, System.out::println);
}

 

메서드 참조를 사용하는 방법?

  • 정적 메서드의 참조 : ( Class::staticMethod )

    static으로 정의한 메서드를 참조할 때 사용하는 방식이다.

     

    Long.parseLong(value); → Long::parseLong

      // 람다 표현식
      someMethod(value, (value) -> Long.parseLong(value));
    
      // 메서드 참조
      someMethod(value, Long::parseLong);

 

  • 비한정적 메서드의 참조 (인스턴스) : ( Class::instanceMethod )

    public 혹은 protected로 정의된 메서드를 참조할 때 사용되는 방식이다.

     

    비한정적이라는 표현은 구문 자체가 특정한 객체를 참조하기 위한 변수를 지정하지 않는다는 것을 의미한다.

     

    string.length() → String::length

      // 람다 표현식
      someMethod(value, (value) -> value.length());
    
      // 메서드 참조
      someMethod(value, String::length);

 

  • 한정적 메서드 참조 (외부 인스턴스 변수) : ( Instance::instanceMethod )

    Lambda body 외부에서 선언된 객체의 메서드를 호출하거나, 객체를 생성해서 메서드 참조할 때 사용되는 방식이다.

     

    한정적이라는 표현은 참조하는 메서드가 특정 객체의 변수로 제한되는 것을 의미한다.

     

    lob.isLob() → lob::isLob

      Lob lob = new Lob(true);
    
      // 람다 표현식
      someMethod(() -> lob.isLob());
    
      // 메서드 참조
      someMethod(lob::isLob);

 

 

생성자 참조?

자바 언어에서는 메서드와 생성자를 구분하고 있다.

 

문법적인 구조상의 차이로는 리턴 타입이 없다는 것이 있지만, 메서드는 접근 권한만 있다면 호출 가능하나, 생성자는 객체가 생성될 때에만 호출할 수 있다.

 

생성자 참조는 ClassName::new 형식으로 작성된다.

 

생성자 참조는 새로운 객체를 생성하고 리턴하는 경우 등에서 사용된다.

// 람다 표현식
someMethod(String name -> new Lob(name)).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자 참조
someMethod(Lob::new).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자, 메서드 참조
someMethod(Lob::new).forEach(System.out::println);

 

 

 

참고 자료

  • Practical 모던 자바

 

 

 

'Live Study' 카테고리의 다른 글

Live Study_Week 10. Multithreading programming  (3) 2021.03.02
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

 

 

스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O

 

I/O?

Input : 입력, Output : 출력

Application(JVM) 외부의 입력을 받고, 내부의 결과를 외부로 전달하는 모든 흐름을 의미한다.

  • Scanner, InputStream을 통한 키보드, CSV 등의 포맷 파일 입력 등
  • OutputStream을 통한 Console 출력, TCP/IP를 이용한 외부 Application(JVM)에 전달 등

 

 

Stream - Java I/O?

데이터를 전달하고, 입력받을 때 필요한 단방향 통로를 말한다. 기본적인 Java I/O API에서는 입력과 출력을 위한 각각의 Stream이 필요하다.

  • 기본적으로 Buffer를 사용하지 않는 방식이며, 데이터를 받은 즉시 처리한다.
  • 데이터를 순차적으로 처리한다. (FIFO : 들어간 순서대로 나온다.)
  • Blocking 방식으로 동작한다. 스트림 호출 시 동작 스레드는 대기 상태가 된다.
  • 시스템 콜과 CPU 자원 사용, 커널에서 Direct Buffer → 다시 JVM 내부 Buffer로 메모리를 복사하는 과정이 존재하는 등 큰 오버헤드가 존재한다.

 

 

Buffer?

일반적으로 데이터가 한 위치에서 다른 위치로 이동하는 동안 일시적으로 데이터를 보유하는 데이터 구조 또는 메모리 영역을 말한다.

 

각각의 데이터가 큰 경우에 해당 영역에 버퍼 크기만큼의 데이터를 저장하고 전송함으로써 디스크와의 상호 작용 (호출) 수를 줄일 수 있다. - I/O 비용을 최소화

  • 데이터를 받는 순서대로 쌓아 보관하다 일정량이 되면 한번에 전송한다.
  • 데이터를 실시간으로 처리할 필요가 없고, 지속적으로 데이터를 넘긴다면 사용하는 것이 좋다.
  • 서버가 요청을 처리하는 시간이 받는 요청을 따라가지 못한다면, 요청이 유실될 수 있다. → 이때 요청들을 담아둘 버퍼를 이용할 수 있다. server socket 구현체에는 buffer가 포함되어 있다.

 

 

Channel - Java N I/O?

데이터가 양방향으로 전송될 수 있는 통로를 말하며, Java 4, 7에 추가된 N I/O 1, 2 API는 기본적으로 Channel과 Buffer 그리고 Selector를 통하여 기능을 제공한다.

 

셀럭터에는 채널들을 등록하고, 사용 가능한 채널을 통해서 각각의 데이터를 전송하거나 받고 이를 버퍼에 보관한다. Application은 버퍼를 통해 데이터를 처리한다.

  • "Non-blocking 방식으로도" 동작 가능하다. files API의 기능들은 Blocking으로 동작한다.
  • Blocking 방식으로 동작하는 API에 대해서 인터럽트를 던짐으로써 대기 상태에서 빠져나올 수 있다.
  • 기존 I/O 방식에서 접근할 수 없었던 Direct Buffer를 직접 참조할 수 있다. → 버퍼 내용을 복사할 필요가 없다.

 

 

InputStream과 OutputStream

바이트 기반 입출력 스트림들의 최상위 추상 클래스들이 InputStream, OutputStream이다.

 

InputStream?

입력과 관련된 API와 Closeable 인터페이스가 구현되어 있다.

바이트 기반의 입력 스트림을 구현하는데 사용되는 클래스이다.

 

하위 구현체로는

  • FileInputStream : 파일을 읽는 데 사용하는 구현체 - 텍스트, 바이트 코드 등을 읽을 때 사용한다.
  • FilterInputStream : InputStream을 재정의한 구현체 - 다른 입력 스트림을 포괄한다.
  • ObjectInputStream : ObjectOutputStream으로 저장된 데이터를 읽는 데 사용한다. → 역직렬화

등이 있다.

 

 

OutPutStream?

출력과 관련된 API와 Closeable 인터페이스가 구현되어 있다.

바이트 기반의 출력 스트림을 구현하는데 사용되는 클래스이다.

하위 구현체로는

  • FileOutputStream : 파일을 출력하는 데 사용하는 구현체
  • FilterOutputStream : OutputStream을 재정의한 구현체
  • ObjectOutputStream : Object를 Byte 기반으로 직렬 화할 때 사용한다.

등이 있다.

 

 

 

Byte와 Character 스트림

 

Byte Stream

기본적으로 사용되는 InputStream, OutputStream과 같이 1 바이트 단위를 처리하는 Stream들을 의미한다. 외부 요청을 서버가 처리하는 경우에는 이러한 데이터를 InputStreamReader를 통해 char로 변환하여 처리하게 되는데, 이 경우 한글 등으로 작성된 Unicode 기반의 정보들이 깨지는 문제가 발생하게 된다.

이를 위해 Character Stream이나 BufferdReader라는 것들이 만들어지게 되었다.

 

 

Character Stream

2 바이트 단위의 데이터를 처리하는 Stream들을 말하며, 기본적으로 Unicode charset에 맞게 데이터를 처리한다. 즉 Text 기반의 데이터를 입출력하는데 사용되는 Stream이다. JSON, HTML, XML...

 

 

보조 스트림

입출력 대상이 되는 파일이나 Binary 데이터를 직접 읽거나 쓰는 기능이 존재하지 않지만, 부가적인 기능을 추가하는 데 사용되는 Stream들을 말한다. Wrapper Stream이라고 하기도 한다.

 

이러한 구조를 가지는 구현 방식을 Decorator Pattern이라고 한다. 보조 스트림 중의 최상위는 FilterInputStream & FilterOutputStream이다.

 

하위 구현체로는

  • BufferedInputStream : 입력받는 바이트 스트림에 버퍼 기능을 제공하는 보조 스트림
  • BufferedOutputStream : 출력하는 바이트 스트림에 버퍼 기능을 제공하는 보조 스트림
  • BufferedReader : 문자 단위로 입력받는 스트림에 버퍼 기능을 제공하는 보조 스트림
  • BufferedWriter : 문자 단위로 출력하는 스트림에 버퍼 기능을 제공하는 보조 스트림
  • InputStreamReader : 입력되는 바이트 정보를 문자로 변환해주는 보조 스트림
  • OutputStreamReader : 출력되는 바이트 정보를 문자로 변환해주는 보조 스트림
  • DataInputStream : 입력되는 바이트를 자료형에 맞게 제공해주는 보조 스트림
  • DataOutputStream : 입력되는 바이트를 자료형에 맞게 출력해주는 보조 스트림

등이 있다.

 

 

 

표준 스트림 (System.in, System.out, System.err)

 

System.in

System.in 은 Static 한 InputStream 타입의 변수이다.

JVM이 메모리로 올라오면서 사용될 하위 구현체를 인스턴스화 하고, 기본적으로 키보드의 입력을 받을 때 사용한다.

바이트 단위로 동작한다.

 

 

System.out

System.out 은 Static 한 PrintStream 타입의 변수이다.

자체적인 Buffer를 가지고 있으며, print 요청을 모아두었다가 적절한 출력 시점에 flush()를 호출한다.

 

콘솔에 특정 문자열을 출력하는 데에 주로 사용된다. 로직 Logging을 위해서 등.. 하지만

https://lob-dev.tistory.com/entry/Logging-slf4j-Logback-Framework

여기에 정리하였듯이 Application의 전체 성능에 큰 영향을 줄 수 있으므로 조심하여야 한다.

 

이는 print가 단순히 구문에 대해서만 최적화를 시도하는 반면, Logger들은 Application이 다른 요청을 처리하지 않을 때 동작하기 때문이다.

 

 

System.err

System.err 도 System.out과 마찬가지로 static 한 PrintStream 타입의 변수이다.

 

그렇기에 마찬가지로 자체적인 Buffer를 가지고 있지만, out과 다른 점은 출력 시점을 미루지 않고, 호출되는 순간마다 flush() 하기 때문에, out과 동시로 사용했을 경우 코드 흐름과 다른 결과를 낼 수 있다.

 

그 외에는 동일한 사용법을 제공한다.

 

 

 

파일 읽고 쓰기

 

텍스트 파일 읽기

 

Java 6

해당 버전에서 Text File을 쓸 때에는 FileInputStream과 InputStreamReader를 사용한다.

BufferdReader는 부가적으로 사용한다.

File file = new File("../test.txt")

BufferedReader br = null;
StringBulider sb = new StringBulider();

try {
        br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));

        for (String line; (line = br.readLine()) != null) {
                sb.append(line);
        }
} catch (UnsupportedEncodingException exception || FileNotFoundException exception ||
                 IOExcecption exception) {
        //
} finally {
        if (br != null) {
            try {
                    br.close();
            }    catch (IOException exception) {
                    //
            }
        }
}

 

 

Java 7

해당 버전부터는 Files API의 newBufferedReader 메서드를 이용하여 구현할 수 있다.

Path path = Paths.get("../test.txt");

StringBulider sb = new StringBulider();

try (BufferedWriter br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {

        for (String line; (line = br.readLine()) != null;) {
                sb.append(line);
        }

} catch (IOException exception) {
        //
}

 

 

텍스트 파일 쓰기

 

Java 6

해당 버전에서는 FileOutputStream과 OutputStreamWriter 클래스를 사용하여 파일을 기록할 수 있다.

File file = new File("../test.txt");

BufferedWriter bw = null;

try {
        bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))

        bw.append("1");
        bw.newLine();
        bw.append("2");

} catch (UnsupportedEncodingException exception || IOExcecption exception) {
        //
} finally {
        if (bw != null) {
            try {
                    bw.close();
            }    catch (IOException exception) {
                    //
            }
        }
}

 

 

Java 7

해당 버전에서는 Files API의 newBufferedWriter() 메서드를 이용하여 구현할 수 있다.

Path path = Paths.get("../test.txt");

try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {

        bw.append("1");
        bw.newLine();
        bw.append("2");

} catch (IOException exception) {
        //
}

 

 

Files API

파일과 관련된 다양한 Utils Method 들을 Static 하게 제공하는 API이다. Path 객체를 이용하여 **사용할 수 있다.

  • Files.createFile(Path...) : 파일을 만들 때 사용하는 Static Method.
  • Paths.get("test.csv").toAbsolutePath() : Path 객체를 사용하여 파일의 절대 경로를 획득.
  • Files.exists(Path...) : 파일 존재 여부를 확인하는 Static Method.
  • Files.createDirectory(Path...) : 디렉터리를 생성하는 Static Method.
  • Files.delete(Path...) : 디렉터리나 파일을 삭제하는 Static Method.
  • Files.write(Path, "text". getBytes(charset), StandardOpenOption) : 파일 쓰기 Static Method.
  • Files.copy(기존 Path, 복사할 Path, StandardCopyOption) : 파일 복사 Static Method.
  • Files.lines(Path). count() : 파일의 현재 줄 수를 가져오는 Static Method.

 

자료 출처

  • 자바 마스터북

'Live Study' 카테고리의 다른 글

Live Study_Week 10. Multithreading programming  (3) 2021.03.02
Live Study_Week 15. Lambda Expression  (0) 2021.03.01
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

 

 

Annotation을 정의하는 방법


 

Annotation이란?

JDK 1.5에 추가된 기능으로 실제 코드에 직접적인 영향을 주지 않고, 해당 코드를 처리하는 ****Compiler , Interpreter 혹은 Runtime에서 JVM에게 처리를 위한 정보를 제공하는 것이 주목적이다.

 

Single-Value Annotation

단일 변수만을 갖는 Annotation을 말하며, 값을 명시하여 데이터를 전달할 수 있다.

@SuppressWarnings, @Retention, @Target...

 

Full Annotation

두 개 이상의 변수를 갖는 어노테이션으로 여러 데이터를 ", "를 기준으로 Key : value 형식으로 전달하게 된다. @Generated...

 

Marker Annotation과 Interface

구현할 메서드, 상수 필드가 존재하지 않는 Interface나 필드 변수가 존재하지 않는 Annotation을 명칭 하며, Compiler, Interpreter, JVM에게 어떠한 정보를 제공하는 용도로만 사용된다.

Cloneable, Serializable, Remote Interface or @Overide, @Native, @Nullable, @Documented...

 

 

Annotation 종류

Compile Time에서 정보를 제공하는 Annotation (: Compiler )

  • @Override : 상위 클래스의 메서드를 재정의하였음을 나타내는 Annotation이다. 상위 클래스에 해당 메서드가 존재하지 않는 경우 Compiler가 경고하는 기능을 제공한다.
  • @SuppressWarnings : 컴파일러가 경고하는 내용을 Ignore 할 때 사용하는 Annotation이다.
  • @Native : Native Code에 의해 참조되는 상수 필드에 적용하는 Annotation으로 JNI를 통해 (C, C++ code) Native Method가 호출된다.
  • @Generated : 소스 코드를 생성하는 도구에 의해서 해당 코드가 자동적으로 생성되었음을 나타내는 Annotation이다.

 

Load-Build Time에서 정보를 제공하는 Annotation (: Compiler )

  • @Nullable : parameter에 대해 Null을 허용함을 나타내는 Annotation이다. ( JSR 305 )

 

Runtime Time에서 정보를 제공하는 Annotation (: Reflection API)

  • @FunctionalInterface : JDK 8부터 추가된 Annotation으로 해당 Interface가 하나의 메서드만을 제공하는 함수형 인터페이스임을 나타내는 Annotation이다.
  • @Deprecated : 더 이상 지원하지 않는 기능임을 알려주는 Annotation이다.
  • @SafeVarargs : Generic과 같은 가변 인자 Parameter를 사용할 때 발생하는 경고를 ignore 하는 Annotation이다.
  • @Repeatable : 해당 Annotation을 반복적으로 사용할 수 있게 지원하는 Annotation이다.
  • @Inherited : Meta Annotation 중 하나로 써 적용된 대상 Annotation을 하위 Class, Interface에게 상속시키는 Annotation이다.
  • @Retention
  • @Target
  • @Documented

 

Custom Annotation 정의

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface SampleAnnotation {

        String name() default "";

}

 

 

@Retention

 

Meta Annotation 중 하나로써 적용된 대상 Annotation이 어느 시점까지 정보를 유지할 것인지 정의하는 Annotation이다.

 

 

RetentionPolicy 필드 변수

  • SOURCE : Annotation 정보가 Compile Time에서 사라진다.
  • CLASS : Annotation 정보가 Compiler에 의해 참조되고 Runtime 시점에선 사라지게 된다.
  • RUNTIME : Annotation 정보가 Runtime 시점에 JVM이나 Reflection API에 의해 참조 가능하다.

 

 

@Target

Meta Annotation 중 하나로써 적용된 대상 Annotation이 어느 위치에 적용될 수 있는지를 정의하는 Annotation이다.

 

 

ElementType 필드 변수

  • TYPE : Class, Interface 등의 Level에 선언 가능하다.
  • METHOD : 메서드에 선언 가능하다.
  • FIELD : Enum, 상수, Field 변수에 대해 선언 가능하다.
  • PARAMETER : 매개변수에 선언 가능하다.
  • CONTRUCTOR : 생성자 Type에 선언 가능하다.
  • LOCAL_VARIABLE : 지역 변수에 대해 선언 가능하다.
  • ANNOTATION_TYPE : Annotation에 선언 가능하다.
  • PACKAGE : 패키지에 선언 가능하다.

 

 

@Documented

Meta Annotation 중 하나로써 적용된 대상 Annotation에 대한 정보를 Javadoc에 나타내는 Annotation이다.

 

 

Annotation Processor

Compile 시점에서 정의된 Annotation을 분석하고 처리하기 위해 사용되는 API이다.

 

소스 코드를 구문 분석하여 나타나는 결과인 Abstract Syntax Tree를 조작하여 코드를 추가하거나, 설정 정보를 변경하거나, 컴파일 에러, 경고 등을 생성하기도 한다.

 

Annotation Processing Flow

  1. 자바 컴파일러에 의해 빌드가 시작된다.

  2. Annotation Processor들이 각각의 처리를 시작한다.

  3. 주석이 달린 Class, Method, Field를 찾는다.

  4. 발견된 부분들의 정보를 이용하여 코드를 생성, 검증한다. (Lombok 등의 코드 생성 시점)

  5. 생성된 정보, 코드, 문자열 등을 class로 작성한다.

  6. 컴파일러가 모든 Processor가 실행되었는지 확인하고 다음 Round를 진행하거나 실행 종료한다.

    3~6을 반복하게 되며, 각 Round 마다 처리할 각각의 Annotation을 식별한다.

 

Annotation Processor 구현

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface SampleAnnotation {

        String name() default "";

}

//------

@SupportedAnnotationTypes("com.example.annotation.SampleAnnotation")
@AutoService(Processor.class)
public class SampleProcessor extends AbstractProcessor {

        @Override
        public SourceVersion getSupportedSourceVersion() {
                return SourceVersion.latestSupported();
        }

        @Override
    public boolean process(Set<? extends TypeElement> annotations, 
      RoundEnvironment roundEnv) {

                for (TypeElement annotation : anotations) {
                        for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                                processingEnv.getMessager().printMessage(Diagnostic.kind.NOTE, "found SampleAnnotation : " + element);
                        }
                }

                return true;
    }

}

 

 

 

참고 자료

'Live Study' 카테고리의 다른 글

Live Study_Week 15. Lambda Expression  (0) 2021.03.01
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11
Live Study_Week 08. 인터페이스  (0) 2021.01.05

 

 

enum 정의하는 방법

해당 타입은 JDK 5에 추가되었으며, 상수들을 정의, 열거하는 데 사용되는 키워드이다.

 

기존에 사용되던 final static keyword를 사용한 변수 선언 등을 대체하였다.

 

 

해당 키워드의 특징으로는

  1. 모든 Java의 enum들은 암시적으로 java.lang.Enum Class를 확장한다.
  2. 정의된 상수 하나당 하나의 인스턴스가 생성된다. public static final type instance
  3. 데이터를 비교할 때 실제 값과 함께 타입을 체크한다.
  4. 메서드와 필드를 추가하고 인터페이스를 구현할 수 있다.
  5. 다른 Primitive Type Variable 과 같이 Switch Statement에서 사용할 수 있다.
  6. 정의 시 Class Level에서 사용된다.

정도가 있다.

 

 

enum 선언, 할당 방식

 

JDK 5.0 이전의 방식

Class Role {
        public final static int ADMIN = 0;
        public final static int USER = 1;
        public final static int GUEST = 2;
}

 

일반적인 선언, 할당

// 선언
public enum Role {
        ADMIN, USER, GUEST 
}

// 할당
Role role;

role = Role.ADMIN;
role = Role.USER;
role = Role.GUEST;

 

enum Class 내부에 Method, Variable 선언

public enum Role {
        ADMIN, USER, GUEST;

        //내부 변수
        private String a;
        private String b;

    //생성자 
        Role(String a, String b) {
                this.a = a;
                this.b = b;
        }

        public String getA() {
                return a;
        }

        public String getB() {
                return b;
        }    
}

 

 

enum 사용 방식

 

조건문 : if

if (role == Role.USER) {
        System.out.println("USER");
}
if (role == Role.GUEST) {
        System.out.println("GUEST");
}
if (role == Role.ADMIN) {
        System.out.println("ADMIN");
}

 

조건문 : Switch

switch (role) {
        case USER : 
                System.out.println("USER"); 
                break;
        case GUEST : 
                System.out.println("GUEST"); 
                break;
        case ADMIN : 
                System.out.println("ADMIN"); 
                break;
}

 

반복문 : for-each

for (Role role : Role.values()) {
        System.out.println(role);
}

 

 

enum이 제공하는 메서드 (values()와 valueOf())

컴파일러가 자동으로 추가해주는 메서드들이라고 한다.

 

values()

정의된 모든 상수를 배열에 담아 반환한다.

Role[] role = Role.values();

for (Role r : role) {
        System.out.println(r);
}

for (Role role : Role.values()) {
        System.out.println(role);
}

 

valueOf()

정의된 상수와 변수로 넘긴 문자열을 비교한 뒤 그 결과를 반환한다.

 

전달된 문자열과 동일한 상수가 없을 경우 NPE가 발생한다.

role = Role.valueOf("USER");

// NPE
role = Role.valueOf("aaa");

 

기타 메서드

 

ordinal()

현재 상수 값이 선언된 enum Class 내에서 어느 위치(index)에 있는지 반환하는 메서드이다.

상수의 위치가 변경되거나 새로운 상수가 추가된다면 해당 메서드를 사용한 로직들은 깨지게 된다.

int idx = role.ordinal();

 

compareTo()

상수의 ordinal 값을 이용하여 두 개의 상수를 비교하는 메서드이다. 반환 값은 int이다.

// return = self.ordinal - role.ordinal;
Role.USER.compareTo(role);

 

name()

정의된 상수의 이름을 문자열 형식 값으로 반환한다.

 

해당 메서드는 final 메서드이기에 재정의가 불가능하다.

String str = Role.User.name();

 

toString()

기본 정의로는 상수의 문자열 형식 값을 반환한다.

 

해당 메서드는 재정의가 가능하다.

String str = Role.USER.toString();

 

 

java.lang.Enum

Java의 enum들이 암시적으로 확장하는 Super Class이다.

 

컴파일러가 호출하는 생성자와 위에서 작성된 메서드를 제외하고도 Object.Class의 메서드들과 getDeclaring()가 포함되어 있다.

 

 

EnumSet

Set 인터페이스와 enum을 함께 사용하기 위해 일부 구현한 추상 클래스이다.

하위 구현체로는 RegularEnumSet, JomboEnumSet이 있다.

 

RegularEnumSet

단일 long의 각각의 비트가 enum 값을 나타내는 Set이다. 최대 64개의 요소를 저장한다.

 

JomboEnumSet

long 배열을 이용하여 enum 값을 나타내는 Set이다. 위와 같이 하나의 long 요소는 64개를 저장한다.

 

 

해당 Set의 특징

  1. 동기화되지 않았다.
  2. Null을 허용하지 않는다.
  3. Fail-safe Iterator를 사용한다. 이는 복제본을 만들고, 그 요소를 순회하는 방식을 말한다.
  4. enum 유형만을 사용하는 것을 강제한다.
  5. 각 요소의 저장되는 순서는 상수의 정의된 순서를 따른다.
  6. 모든 메서드는 산술 비트 연산을 사용하여 구현된다. 매우 빠른 성능을 지닌다.

 

EnumSet의 몇 가지 메서드

  • allOf() : 넘겨받은 enum.class의 모든 요소를 포함한 Set에 반환한다.
  • noneOf() : 비어있는 enum.class Type의 Set을 반환한다.
  • range() : 정의된 상수의 index 값을 기준으로 요소를 저장한다.
  • complementOf() : 해당 Set을 만들 때 전달된 요소들을 제외할 수 있다.
  • add, contains, forEach, remove 등과 같이 기존 메서드는 동일하게 사용 가능하다.

'Live Study' 카테고리의 다른 글

Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 09. 예외 처리  (0) 2021.01.11
Live Study_Week 08. 인터페이스  (0) 2021.01.05
Live Study_Week 07. 패키지  (0) 2020.12.28

 

9주 차 시작!

 

자바에서 예외 처리 방법 (try, catch, throw, throws, finally)

try Statements

try {
        // Do SomeThing..
}

예외가 발생할 수 있는 로직을 실행할 때 예외가 발생하는 것을 잡아내기 위해 사용하는 Statements이다. 주로 JDBC Connection이나 File Reader, API에 대한 URL Connection 등을 처리하는 로직을 감싸는 데 사용한다.

 

Ex) JDBC

try {
        connection = dataSource.getConnection(); 

        //SQLException 이 발생 가능하다.
    ResultSet resultSet = connection.prepareStatement("SELECT * FROM USER")
                        .executeQuery();
} // catch Statements

 

catch Statements

try {
        // Do SomeThing
} catch (Exception exception) {
        // Stack Trace, Ignore 등
}

Try Statements의 내부에서 발생한 Exception을 잡아 사전 정의된 동작을 하는 Statements 이다.

 

Ex) JDBC

try {
        connection = dataSource.getConnection(); 

    ResultSet resultSet = connection.prepareStatement("SELECT * FROM USER")
                        .executeQuery();
} catch (SQLException exception){

        // 해당 예외가 발생한 시점까지의의 메서드 정보를 호출한다.
        exception.stackTrace();
}

 

Multiple catch

Try Statements에서 발생한 2개 이상의 Exception에 대한 Catch Statements를 처리하는 것을 말한다.

 

JDK 7 이전

try {
        // Do Something..
} catch (IOException exception) {
        exception.stackTrace();
} catch (FileNotFoundException exception) {
        exception.stackTrace();
}

발생하는 Exception들에 대해 동일하게 stackTrace()를 호출하는 것을 볼 수 있다. 모든 Exception에 대해 같은 처리를 한다고 생각하면 같은 로직이 중복되는 것을 알 수 있다.

 

JDK 7 이후 버전부터는 해당 부분에 대해 개선이 이루어짐으로써

try {
        // Do Something..
} catch (IOException exception | FileNotFoundException exception) {
        exception.stackTrace();
} 

이런 식으로 처리할 수 있다.

 

throw

메서드 내에서 사용 가능하며, 로직을 진행하면서 조건에 따라 명시적으로 예외를 던질 때 사용하는 키워드이다. Check Exception은 해당 키워드를 통해 전파시킬 수 없으며 인스턴스만 전달 가능하고 한 번에 여러 예외를 전달할 수 있게끔 작성할 수 없다.

 

Spring에서는 ExceptionHanler를 이용하여 원하는 처리를 진행할 수 있다.

public void someMethod() {
        // Do Something..
        if(a < 1) {
                throw new RuntimeException();
        }
        // Do Something
}

// 당연히 Try-catch 문도 이용 가능하다.
try {
            throw new RuntimeException("hello");
} catch (RuntimeException exception){
            exception.printStackTrace();
}

 

throws

메서드 시그니쳐에 사용 가능하며, 해당 메서드를 호출하는 클라이언트에게 예외를 던진다.

해당 키워드는 Check Exception도 전달할 수 있으며, 메서드 뒤에 예외 클래스 형태로 정의된다.

쉼표를 이용하여 여러 Exception을 던질 수 있음을 선언할 수 있다.

void userDaoTest_selectOnes() throws SQLException {
        ResultSet resultSet = connection.prepareStatement("SELECT * FROM USER WHERE ID = 3")
                        .executeQuery();
}

void userDaoTest_selectOnes() throws IOException, FileNotFoundException{
        // Do Someting..
}

 

finally

try문, try-catch 문의 과정, 결과와 상관없이 마지막에 꼭 실행하여야 하는 로직을 정의하는 문법, 주로 사용된 Resource에 대해 반납하는 코드를 작성한다.

} finally {            
        // 리소스 반납 코드
        try {
                if (ps != null) {
                        ps.close();
                }
                if (con != null) {
                        con.close();
                }
        } catch (SQLException exception) {
                exception.printStackTrace();
        }
}

 

try-catch-finally

위에서 언급했던 try, catch, finally 문법들을 같이 사용하는 것을 의미한다.

Connection con = null;
PreparedStatement ps = null;

try {
        con = dataSource.getConnection();
        ps = con.prepareStatement("SQL");
        ps.executeUpdate();

} catch (SQLException exception) {
        exception.printStackTrace();

} finally {            
        // 리소스 반납 코드
        try {
                if (ps != null) {
                        ps.close();
                }
                if (con != null) {
                    con.close();
                }
        } catch (SQLException exception) {
                exception.printStackTrace();
        }
}

해당 로직의 경우 하는 일이 많이 없기에 가독성의 대한 큰 문제를 느끼지 못하지만, 복잡한 흐름을 가지기에 로직이 복잡해질수록 가독성이 떨어지게 된다. 추가적으로 반환되어야 할 자원이 많을수록 개발자가 어떠한 자원에 대하여 정리하는 로직을 작성하지 못해 문제를 일으킬 수 있다.

 

이를 해결하기 위해 JDK 7부터는 AutoCloseable 인터페이스를 구현한 클래스들에 대해 자동적으로 자원을 회수할 수 있는 문법을 제공한다. 이를 try-with-resources 문법이라고 한다.

 

try-with-resources

해당 방식은 여러 로직을 세미콜론으로 구분하여 선언하게 된다. 해당 방식은 로직이 진행되고, 내부적으로 close() 호출하여 모든 리소스를 반납하게 된다.

try (Connection con = dataSource.getConnection();
         PreparedStatement ps = con.prepareStatement("SELECT * FROM USER")){

        ResultSet resultSet = ps1.executeQuery();

} catch (SQLException exception) {
        exception.printStackTrace();
}

 

자바가 제공하는 예외 계층 구조

 

출처 : https://coderanch.com/t/627585/certification/Error-unchecked-exception

 

Throwable

Java에 존재하는 모든 예외와 오류에 대한 상위 클래스이다. 해당 클래스를 상속 받음으로써 thorw 문이나 catch 문 등에 사용할 수 있는 파라미터가 될 수 있으며, 추가적으로 JVM에 의해 throw 될 수 있다.

 

주로 사용되는 getLocalizedMessage, printStackTrace, initCause, toString 등의 메서드들을 구현한다.

 

Exception

프로그램 실행 중 개발자가 구현한 로직에서 던져진 문제에 대한 정보를 담는 클래스를 말하며, 자바에서의 Exception은 컴파일 시점에서 컴파일러에게 발견되는 확인된 예외이다.

 

Exception와 해당 클래스를 상속받는 Sub Class들(Runtime Exception 제외)은 try, catch Statements로 감싸거나 throw를 작성하는 등 명시적인 예외 처리를 하여야 한다.

 

Error

프로그램 실행 중 시스템에서 발생한 문제에 대한 정보를 담는 클래스를 말하며, JVM에 의해 던져지는 것이다. Application 로직에서 잡을 수 없는 대처 불가능한 것이기에 프로그램 로직을 구현할 때 상정하지 않아도 된다.

(심각한 수준의 상황을 의미한다.)

 

Exception과 Error의 차이는 JVM, WAS와 같은 System Level의 문제와 Application Level의 문제의 차이이다.

 

RuntimeException

Exception을 상속받는 클래스이며, 컴파일 시점에서 식별되지 않는 (Unchecked) Exception들의 집합이다. 주로 개발자의 실수로 인해 발생하는 예외이다. (Array Index를 넘어간다던지, 0으로 나누는 등)

 

RuntimeException과 RE가 아닌 것의 차이는?

 

Exception과 (Runtime Exception을 제외한) Sub Class들 (Checked Exceptions)

  • 컴파일 시점에서 compiler에서 확인할 수 있는 예외이다.
  • 프로그램 실행, 구현의 흐름상 예외의 발생 가능성이 있다면 명시적인 예외처리를 작성하게 강제한다.
  • IOException, ServletException, SQLException 등이 있다.

 

RuntimeException과 Sub Class들 그리고 Error (UnChecked Exceptions)

  • 런타임 시점에서 발생하는 Exception, Error들을 말한다. Checked Exception보다 좀 더 구체적인 뜻을 가지고 있다.
  • 개발자 부주의로 인한 문제에서 발생하게끔 의도된 것들이거나(null 체크, 형 변환, 메서드 호출) 시스템에서 발생하는 것들을 말한다.

NullPointerException, IllegalArgumentException, SecurityException. IndexOutOfBoundsException

ClassCastException 등이 있다.

 

커스텀한 예외 만드는 방법

Custom Exception을 만들 때에는 해당 예외의 특징을 고려하여 extend를 통해 Exception이나 RuntimeException을 상속받아 작성한다.

 

개인적으로 해당 Exception에 대해 좀 더 명확한 정보 전달을 위해 유사한 표준 에러를 상속받아 좀 더 구체적인 Custom Exception 들을 작성하고 있다.

// Custom Runtime Exception
public class NoSuchPostException extends NoSuchElementException {

    // Do SomeThing..

    public NoSuchPostException(String errorMessage) {
        super(errorMessage);
    }

}

 

커스텀 예외를 사용한다는 것은 표준 예외에 비해 좀더 명확한 정보를 전달할 수 있음을 의미하지만, 오용하게 될 경우 지나치게 많은 클래스가 만들어짐으로써 메모리의 문제( Metaspace는 상관없으며, Perm gen을 사용하는 경우)와 클래스 로딩 문제가 발생할 수 있다.

 

그렇기에 그저 이름을 바꿔 구현하는 것보다는 범용 에러를 활용하면서 메시지를 잘 작성하고 꼭 필요한 시점에만 명확한 정보를 추가적으로 제공함으로써 디버그를 진행함에 있어서 도움이 되게끔 잘 활용하여야 한다.

'Live Study' 카테고리의 다른 글

Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 08. 인터페이스  (0) 2021.01.05
Live Study_Week 07. 패키지  (0) 2020.12.28
Live Study_Week 06. 상속  (0) 2020.12.21

8주 차 시작!

 

인터페이스 정의하는 방법

 

인터페이스란?

인터페이스는 (구현한) 하위 인스턴스를 참조할 수 있는 타입이며, 해당 인스턴스가 어떠한 행위를 할 수 있는지 클라이언트에게 알려주는 일종의 계약서 역할을 한다.

클라이언트는 해당 행위를 알려주고 구현부는 해당 타입의 인스턴스에게 맡김으로써 정보 은닉을 지킬 수 있다. 이는 반대로 생각한다면 클라이언트는 구현부의 변경에 따른 여파가 없음을 의미한다.

 

 

인터페이스를 정의하는 방법?

{Access-Level-Modifier} interface {Name} {

        // JDK 7까지는 기본적으로 Static Method를 제외하고 추상 타입의 public Method 만을 
    // 선언할 수 있다.

        // 이 Method는 해당 Interface를 구현하는 Class에서 무조건 Overiding하여야 한다. 
        public String xxxMethod(String str);

        // Access-Level-Modifier 를 작성하지 않고 정의할 수 있으며, 기본 값은 Public이다.
        String xxxMethod(String str);

        // 상수 정의 
        // 인터페이스의 용도를 반하는 대표적인 안티패턴이다. 
        // 상수 필드를 정의하는 것은 해당 타입의 하위 구현체의 구현부를 노출하는 행위이다.
        // 클라이언트에게는 필요없는 정보이기도 하다.
        static final String name = "Lob!";

}

 

인터페이스 구현하는 방법

 

Interface(만) 를 instantiation 할 수 있을까?

public interface SampleInterface {

    String xxxMethod(String str);

}

----------

// error: SampleInterface is abstract; cannot be instantiated
SampleInterface sampleInterface = new SampleInterface();

Interface 만으로는 instantiation시킬 수 없으며, 해당 Interface를 Implement 한 Sub Class(Instance)를 통해서만 구현할 수 있다.

 

 

구현 방법 1 : 익명 클래스 방식

해당 방식은 일회성으로 사용되고, 재사용할 필요가 없는 경우에 이용할 수 있다.

SampleInterface sampleInterface = new SampleInterface() {
            @Override
            public String xxxMethod(String str) {
                return "Hello "+str;
            }
        };

        // lambda 방식 (JDK 8+)
        SampleInterface sampleInterfaceOfLambda = str -> "Hello "+str;

        System.out.println(sampleInterface.xxxMethod("lob"));
        System.out.println(sampleInterfaceOfLambda.xxxMethod("lob"));

 

구현 방법 2 : 인터페이스를 구현하는 방식

Interface의 Abstract Method를 Override한 뒤 생성하는 방식이다.

public class SampleInterfaceImpl implements SampleInterface {
    @Override
    public String xxxMethod(String str) {
        return "Hello "+str;
    }
}

----------

SampleInterface sampleInterfaceOfSubClass = new SampleInterfaceImpl();

System.out.println(sampleInterfaceOfSubClass.xxxMethod("lob"));

 

인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

Interface도 Abstract Class나 Super Class같이 Sub Class를 참조하는 타입으로써 사용할 수 있다.

클래스 상속 관계같이 인터페이스 타입으로 Casting할 경우 인터페이스에 정의된 Method만을 참조하여 사용할 수 있다.

public class SampleInterfaceImpl2 implements SampleInterface {

    public String someMethod(String str) {
        return "Sub Class Hello "+str;
    }

    @Override
    public String xxxMethod(String str) {
        return "Hello "+str;
    }
}

SampleInterface sampleInterface1 = (SampleInterface) new SampleInterfaceImpl2();

sampleInterface1.xxxMethod("lob");

// error: cannot find symbol sampleInterface1.someMethod("lob");
sampleInterface1.someMethod("lob");

// 이렇게 Down Casting 하면 사용 가능하다.
((SampleInterfaceImpl2) sampleInterface1).someMethod("lob");

 

객체는 인터페이스를 사용해 참조하자. (JDK 5+)

이펙티브 자바에서는 적합한 Interface가 존재하는 경우 매개변수뿐만 아니라 반환 값, 변수, 필드 변수에 대한 모든 타입을 Interface로 선언하라고 한다. 이는 하위 구현체들이 모두 Interface 타입으로 참조가 가능하기에, 좀 더 유연한 코드를 작성할 수 있음을 의미한다.

// 좋은 예
// 해당 경우에는 컬랙션 변수가 Param이라고 가정하였을 때 ArrayList, LinkedList, Stack, 
// Vector 등이 전달되어도 문제가 발생하지 않는다.
List<String> list = new ArrayList<>();

// 나쁜 예
// 해당 경우에는 구체적인 클래스 타입을 적용하였기에 다른 List 구현체들이 오게될 경우
// 에러가 발생한다.
ArrayList<String> list = new ArrayList<>();

물론 Class가 특별한 기능을 제공하고, 해당 기능을 사용하여야 하는 경우에는 그러지 않아도 된다.

 

적합한 Interface가 존재하지 않는다면, Class의 계층 구조 중 필요한 기능을 만족하고 추상적인 클래스를 타입으로 사용하자.

 

인터페이스 상속

Interface는 Interface 끼리만 상속 관계를 연결할 수 있으며, Interface를 이용하여 다중 상속(구현)을 지원할 수 있다.

public interface SampleInterface {

    String xxxMethod(String str);

}

public interface SampleInterfaceForDefault {

    default String xxxxMethod(String str) {
        return "Hello "+str;
    }
}

public class SampleInterfaceImpl3 implements SampleInterface, SampleInterfaceForDefault {
    @Override
    public String xxxMethod(String str) {
        return "override Hello! "+str;
    }
}

----------

SampleInterfaceImpl3 sampleInterface2 = new SampleInterfaceImpl3();

sampleInterface2.xxxMethod("lob");
sampleInterface2.xxxxMethod("lob");

Interface들을 다중 구현하였을 경우 동일한 메서드 시그니쳐가 존재한다면, Override하여 사용하여야 한다. (그렇지 않다면 컴파일 에러가 발생한다.)

 

Class와 Interface를 같이 상속, 구현하였을 경우에는 Class에 존재하는 메서드가 우선권을 가진다.

 

인터페이스의 기본 메소드 (Default Method), 자바 8

JDK 8 이후부터는 Interface에 Static Method를 제공하는 것 말고도 Default Method라는 것이 생기게 되었다. 이는 기존에 사용되던 Interface의 문제점인 한번 배포된 Interface는 수정이 어렵다.라는 것을 해결하기 위하여 추가되었다고 생각한다.

 

Default Method가 도입된 JDK 8 이후에는 많은 Method들이 추가되었음을 알 수 있다.

 

이미 배포된 Interface 에 메서드를 추가하게 된다면, 기존에 해당 Interface를 사용하던 모든 프로젝트에서는 개발 중일 때에는 컴파일 에러, 실행 중 인 것들에 대해서는 NoSuchMethod Error가 발생하게 된다.

 

메서드를 추가한다면?

public interface SampleInterface {

    String xxxMethod(String str);

    // 해당 메서드가 추가되었다.
    void addMethod(String str);

}

----------

// error: <anonymous interfaceexample.InterfaceClient$1> is not abstract and does not override abstract method addMethod(String) in SampleInterface
SampleInterface sampleInterface = new SampleInterface() {
            @Override
            public String xxxMethod(String str) {
                return "Hello "+str;
            }
        };

// Multiple non-overriding abstract methods found in interface interfaceexample.SampleInterface
// 구현 방식 1 = lambda 방식 (JDK 8+)
SampleInterface sampleInterfaceOfLambda = str -> "Hello "+str;

----------

//Class 'SampleInterfaceImpl3' must either be declared abstract or implement abstract method 'addMethod(String)' in 'SampleInterface'
public class SampleInterfaceImpl3 implements SampleInterface, SampleInterfaceForDefault {
    @Override
    public String xxxMethod(String str) {
        return "override Hello! "+str;
    }
}

이렇게 해당 Interface를 구현하는 모든 클래스, 인터페이스에서 문제가 발생한다.

public interface SampleInterface {

    String xxxMethod(String str);

    default void addMethod(String str) {
        System.out.println("Hello "+str);
    }

}

default 키워드를 사용하여 Method를 추가한 뒤 구현하면 해당 문제가 발생하지 않는다.

 

추가적으로 default Method도 Public으로 인식되며, Static Method와 달리 재정의가 가능하다.

이는 default Method도 Instance와 같이 가시되며, 런타임 시점에서 Dispatch가 되기 때문이다.

 

인터페이스의 static 메서드, 자바 8

Interface의 Static Method도 JDK 8 이후에 제공되기 시작하였으며, default Method와 같이 구현부를 가지게 된다.

다른 점은 위에서 이야기하였던

  • Override가 불가능한 것

  • Instance가 아닌 Interface와 가시 되며. 컴파일 시점에서 Dispatch 된다는 것

  • 그리고 일반적인 Static Method와 달리 상속되지 않고 Interface Type을 직접 참조하여 호출해야 한다는 것이다.

    이는 Interface를 다중 상속을 하였을 경우 발생할 수 있는 문제를 방지하는 조치인 것 같다.

public interface SampleInterface {

    String xxxMethod(String str);

    default void addMethod(String str) {
        System.out.println("Hello "+str);
    }

    static void addStaticMethod(String str) {
        System.out.println("Static Hello "+str);
    }

}

----------

SampleInterface.addStaticMethod("lob");

SampleInterface sampleInterface3 = new SampleInterface() {
            @Override
            public String xxxMethod(String str) {
                return "null";
            }


            // Method does not override method from its superclass
            *@Override*
            // Inner classes cannot have static declarations
            static void addStaticMethod(String str) {
                System.out.println("Static Hello "+str);
            }
        };

해당 메서드도 인터페이스를 구현하지 않고도 Util Method를 이용하고 싶다면 구현하는 방식으로 사용하면 좋을 것 같다.

 

인터페이스의 private 메서드, 자바 9

private Method가 추가된 이유로는 인터페이스의 static, default Method의 로직을 공통화하고 재사용하기 위함이다. 이는 JDK 8에 발생했던 중복 코드 문제를 해결하게 되었다.

 

private Method도 Static, default Method 같이 구현부를 가져야한다는 동일한 제약을 가진다.

 

간단한 예시 코드

 

JDK 8

default void multiplyAfterAddingNumbers(long num1, long num2) {
        long val = num1 + num2;
        val = val * val;
        System.out.println("result = " + val);
}

default void multiplyAfterSubtractingNumbers(long num1, long num2) {
    long val = num1 - num2;
    val = val * val;
    System.out.println("result = " + val);
}

 

JDK 9

default void multiplyAfterAddingNumbers(long num1, long num2) {
        long val = multiplyNumbers(num1 + num2);
        System.out.println("result = " + val);
    }

    default void multiplyAfterSubtractingNumbers(long num1, long num2) {
        long val = multiplyNumbers(num1 - num2);
        System.out.println("result = " + val);
    }

    private long multiplyNumbers(long val) {
        return val * val;
    }

 

참고 자료

  • 이펙티브 자바

'Live Study' 카테고리의 다른 글

Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11
Live Study_Week 07. 패키지  (0) 2020.12.28
Live Study_Week 06. 상속  (0) 2020.12.21
Live Study_Week 05. 클래스  (0) 2020.12.15

7주 차 시작!

 

package 키워드

자바의 패키지는 비슷한 성격의 자바 클래스들을 모아 놓은 자바의 디렉터리를 말한다.

크게는 built-in package 와 custom package로 구분할 수 있다.

 

package 키워드는 모든 클래스 파일에 최상단에 존재하여야 하고, 하나의 소스만 존재해야 한다.

 

 

Built-in package? (내장 패키지)

java, lang, javax, net, io, util, math... 등 Java API에 포함된 모든 패키지들을 말한다.

 

Custom package? (사용자 정의 패키지)

사용자(개발자)가 정의한 패키지를 말한다.

 

Sub package? (하위 패키지)

어떤 package 내부에 존재하는 package를 Sub package라고 한다.

이는 하나의 package 에서도 객체의 행위를 기반으로 다시 분류하기 위함이다.

  • 입력, 출력 작업 → io package
  • 네트워크 작업 → net package...

 

package를 사용함으로써 얻을 수 있는 장점

  • 클래스와 인터페이스를 성향, 쓰임세에 맞게 분류하여 쉽게 유지 보수할 수 있다.

  • 접근 지정자를 통한 가시 범위(보호 범위)를 제공한다.

접근 지정자가 명시되지 않은 경우 package-private, 즉 패키지 내에서만 참조 가능하다.

  • 클래스, 인터페이스가 같은 이름을 가짐으로써 발생하는 충돌을 방지한다.

 

package 명명 규칙

  • 패키지 이름은 클래스, 인터페이스 이름과 충돌하지 않도록 소문자로 작성한다.

    → 대문자를 사용하지 않는 것이 좋다. (권고사항)

  • 최상위 도메인/나라 코드. 회사명/팀명/그룹명(회사 내규). 프로젝트명/프로그램 명으로 작성한다.

    → com.example.project (패키지 정의 표준 사항)

  • 자바 예약어를 사용해서는 안된다.

자바 패키지 내부 클래스 실행 방법

// Sample.class
java com.example.project.main.Sample

 

Package Class?

해당 package에 대한 Spec 및 Implement 정보를 얻을 수 있는 메서드를 제공하는 클래스이다.

ClassLoader와 BootLoader를 사용하여 해당 정보를 로드하고 접근한다.

@Test
    void PackageTest() {
        Package aPackage = Package.getPackage("java.lang");
        Package[] aPackages = Package.getPackages();
        System.out.println(Arrays.toString(aPackages));

        System.out.println(aPackages.length);

        // package name
        System.out.println(aPackage.getName());

        // package spec Title
        System.out.println(aPackage.getSpecificationVersion());
        // package spec Vendor
        System.out.println(aPackage.getSpecificationTitle());
        // package spec Version
        System.out.println(aPackage.getSpecificationVendor());

        // package impl Title
        System.out.println(aPackage.getImplementationVersion());
        // package impl Vendor
        System.out.println(aPackage.getImplementationTitle());
        // package impl Version
        System.out.println(aPackage.getImplementationVendor());

        // package annotations
        Annotation[] annotations = aPackage.getAnnotations();
        System.out.println(Arrays.toString(annotations));

        // package declaredAnnotations
        Annotation[] declaredAnnotations = aPackage.getDeclaredAnnotations();
        System.out.println(Arrays.toString(declaredAnnotations));
    }

 

import 키워드

import?

기본적으로 접근할 수 없는 다른 패키지에 접근하기 위해 사용되는 키워드이다.

  • java lang과 현재 패키지에 대해서는 생략할 수 있다.
// 형식
import domain.project(program).subpackage;
import domain.project.*;

// 예시
import org.junit.jupiter.api.Test;

// .* 를 사용함으로써 해당 패키지의 하위 패키지, 클래스를 모두 접근할 수 있다.
import org.junit.jupiter.api.*;

 

static import?

JDK 5에 추가된 기능으로 어떤 클래스의 정적 멤버에게 직접 액세스 할 수 있도록 지원하는 기능이다.

Class 명을 명시하지 않고 static import 설정을 함으로써 정적 멤버만을 명시하여 사용할 수 있다.

  • static variable, static method

장점

  • 자주 사용하는 정적 메서드(기능)를 한결 쉽게 사용할 수 있다.

      import org.junit.jupiter.api.Assertions;
      import org.junit.jupiter.api.Test;
    
      import static org.junit.jupiter.api.Assertions.assertEquals;
    
      @Test
      void staticImportTest() {
          // non-static-import
          Assertions.assertEquals("A", "A");
    
          // static-import
          assertEquals("A", "A");
      }

단점

  • 과도하게 사용하는 경우 (정적 메서드 이름 중복 등) 어떤 클래스의 정적 메서 드을 가져온 것인지 확인하여야 한다. (의도한 동작이 발생하지 않을 수 있고, 로직을 하나하나 찾아보아야 한다.)

 

클래스패스

JVM에서 사용하는 매개변수로 Java 프로그램을 컴파일 및 실행하는 데 사용되며, Built-in package와 Custom package에 경로를 지정하여 Application ClassLoader를 통해 참조, 접근할 수 있다.

 

CLASSPATH를 설정하는 경우

  • 현재 Directory나 Sub Directory에 없는 Class를 사용하여야 하는 경우
  • 지정한 위치에 존재하지 않는 Class를 불러와야 하는 경우

 

CLASSPATH 형식

CLASSPATH는. class 파일이 포함된 디렉터리와 파일을 콜론으로 구분한 목록이다.

  • CLASSPATH의 기본 값은 . 이며, 현재 검색된 Directory를 의미한다.

    → 이는 CLASSPATH 변수나 -classpath 명령에 의해 변경할 수 있다.

  • 여러 클래스 경로를 설정하려면 (새미 콜론) ; 를 사용하여 CLASSPATH를 구분하여야 한다.

 

CLASSPATH를 설정하는 항목에 따라 다른 형식을 가진다

// 지정할 파일이 JAR 또는 zip 인 경우
C:\Program Files\Java\jdk-1.8.0.252-2\jre\lib\resources.jar
C:\Program Files\Java\jre1.8\MySQL-Connector Java.jar
C:\Users\xxx\Downloads\example.zip

// 클래스 파일 이름이 지정되지 않은 패키지에 있는 경우
C:\Users\xxx\someDirectory

 

자바에서 사용되는 CLASSPATH Library는 GNU CLASSPATH라는 opensource이다.

 

GNU CLASSPATH Properties

  • gcj.dumpobject

    빈 문자열이 아닌 문자열을 설정한 경우, Library Debug build에 사용되는 Serialize Debuging을 활성화시킨다.

  • gnu.classpath.vm.shortname

    VM의 간결한 이름을 나타낸다.

  • gnu.classpath.home.url

    시스템 속성 파일을 찾는 데 사용되는 기본 URL Pattern 값을 지정한다.

    기본적으로 java.home의 lib Directory를 가리키는 값을 가진다.

추가! )

Spring Framework에서 사용되는 표준 Resolver인 PathMatchingResourcePatternResolver는 리소스에 접근할 때 CLASSPATH Pattern으로 매개변수를 받아 접근한다.

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
resolver.getResources("classpath:mybatis/mapper/*.xml")

 

CLASSPATH 환경변수

고급 시스템 설정 / CLASSPATH 변수 설정

Window의 경우 win + Pause key를 눌러 제어판 \ 모든 제어판 항목 \ 시스템으로 이동할 수 있다.

여기서 고급 시스템 설정으로 들어간다.

여기서 환경 변수를 클릭하면 사용자 변수와 시스템 변수를 수정, 추가할 수 있다.

CLASSPATH 속성이 있는 경우 편집 버튼을 클릭하고 ;을 입력한 뒤 파일 경로를 추가하면 된다.

없는 경우에는 새로 만들기를 클릭하고 변수의 이름을 CLASSPATH로 설정한 뒤 값을 지정한다.

 

명령 프롬프트 설정

명령 프롬프트를 실행시킨 뒤

// 클래스 패스 속성이 이미 설정된 경우
SET CLASSPATH=%CLASSPATH%;C:\Program Files\Java\jdk-1.8.0.252-2\jre\lib\resources.jar

// 없는 경우
SET CLASSPATH=C:\Program Files\Java\jdk-1.8.0.252-2\jre\lib\resources.jar

;을 우선 작성하고 추가할 파일의 경로를 작성한다.

→ %으로 묶여 있는 변수는 기존의 환경 변수를 뜻한다.

 

CLASSPATH 옵션

. java 파일을 컴파일하기 전에 해당 파일이 필요로 하는 클래스들을 찾기 위해 사용하는 옵션이다.

-classpath 뒤에 Directory Path를 작성하면 컴파일 시 해당 Directory를 탐색하게 된다.

// javac -classpath <find-Directory-path> <Executable-file-path>
javac -classpath C:\Java\example example.java

 

접근 지시자

public : 모든 클래스에서 해당 클래스를 참조 가능하다.

public class PublicExample {

    public static void main(String[] args) {
        PublicExample example = new PublicExample();
        example.run();
    }

    public void run() {
        PublicClass publicClass = new PublicClass();
        publicClass.getName();
    }
}

public class PrivateClass {
    private final String name = "Lob!";

    private void getName() {
        System.out.println("PrivateClass.getName = " + name);
    }
}
PublicClass.getName = Lob!

 

package-private : 해당 클래스가 포함된 패키지 내에서만 참조 가능하다. (지정이 없는 경우)

public class PackagePrivateExample {

    public static void main(String[] args) {
        PackagePrivateExample example = new PackagePrivateExample();
        example.run();
    }

    public void run() {
        PackagePrivateClass packagePrivateClass = new PackagePrivateClass();
        // error: getName() is not public in PackagePrivateClass; cannot be accessed 
        // from outside package packagePrivateClass.getName();
        packagePrivateClass.getName();
    }
}

public class PackagePrivateClass {
    final String name = "Lob!";

    void getName() {
        System.out.println("PackagePrivateClass.getName = " + name);
    }

}

 

protect : 서브, 하위 클래스와 동일 패키지 내부에 존재하는 클래스만 참조 가능하다.

  • 기본적인 Class 에는 적용할 수 없으며, 중첩, 내부 클래스에는 적용 가능하다.
public class ProtectExample extends ProtectClass{

    public static void main(String[] args) {
        ProtectExample example = new ProtectExample();
        example.run();
    }

    public void run() {
        ProtectClass protectClass = new ProtectClass();
        protectClass.getName();
    }
}

public class ProtectExample2 {

    public static void main(String[] args) {
        ProtectExample example = new ProtectExample();
        example.run();
    }

    public void run() {
        ProtectClass protectClass = new ProtectClass();
        protectClass.getName();
    }
}

public class ProtectClass {

    private final String name = "Lob!";

    protected void getName() {
        System.out.println("ProtectClass.getName = " + name);
    }
}

--------------------------
// (패키지 외부) = 상속시 사용 가능
public class ProtectExample3 {

    public static void main(String[] args) {
        ProtectExample example = new ProtectExample();
        example.run();
    }

    public void run() {
        ProtectClass protectClass = new ProtectClass();
        // error: getName() has protected access in ProtectClass protectClass.getName();
        protectClass.getName();
    }
}
// ProtectExample 
ProtectClass.getName = Lob!

// ProtectExample2 
ProtectClass.getName = Lob!

 

private : 자기 자신인 객체 내부에서만 참조 가능하다. (인터페이스도 JDK9부터 사용 가능)

public class PrivateExample {

    public static void main(String[] args) {
        PrivateExample example = new PrivateExample();
        example.run();
    }

    public void run() {
        PrivateClass privateClass = new PrivateClass();

                // 'getName()' has private access in 'example.accessmodifiers.PrivateClass'
        privateClass.getName();
    }
}

-----------------------------

public class PrivateClass {
    private final String name = "Lob!";

    private void getName() {
        System.out.println("PrivateClass.getName = " + name);
    }
}
error: getName() has private access in PrivateClass privateClass.getName();

 

참고 자료

'Live Study' 카테고리의 다른 글

Live Study_Week 09. 예외 처리  (0) 2021.01.11
Live Study_Week 08. 인터페이스  (0) 2021.01.05
Live Study_Week 06. 상속  (0) 2020.12.21
Live Study_Week 05. 클래스  (0) 2020.12.15
Live Study_Week 04. 제어문 + 과제  (0) 2020.12.01

자바 상속의 특징

 

OOP 이전의 방식

이전 패러다임에서 코드를 재사용하는 방법은 복사한 후 로직 인자에 맞게 수정하는 방식이었다.

이후 기존 로직을 변경해야 할 때, 두 개의 코드다 변경해야 되는 것도 문제고, 하나의 코드를 수정하였다고 하여서 다른 코드를 그렇게 수정하면 된다는 보장이 존재하지 않는다.

 

즉 유지보수에 취약한 코드가 작성, 양산되는 것이다.

 

새롭게 등장한 OOP 패러다임에서는 이러한 부분을 해결하기 위해 도입한 방식이 상속이다.

 

 

상속이란?

기반이 되는 상위 클래스의 특성을 하위 클래스에게 적용하고, 거기에 더해 필요한 특성을 추가, 확장하는 방식을 말한다. 상속의 목적은 기존 기능의 확장과 코드의 재사용이다.

 

 

상위 클래스?

어떠한 속성과 코드를 (하위 클래스들에게) 제공하는 클래스를 말한다.

 

 

하위 클래스?

상위 클래스를 상속받는 클래스를 말한다.

 

 

상속의 장점

하위 클래스에 상위 클래스를 extend만 하여도 코드가 자동적으로 재사용된다는 것이며, 확장성을 위해 변경하여야 할 부분의 로직만 overiding을 사용하여 재정의도 가능하다.

 

 

상속 시 고려해야 할 것, 상속의 단점

상속을 사용하여 코드를 재사용하는 경우에는 기존 상위 클래스가 어떻게 작성되었는지, 어떤 동작을 해야 되는 것인지를 정확히 고려하고 진행해야 한다.

 

그렇지 않고 상속을 과용하게 되면 위에서 고려하지 않은 다른 동작을 하위 클래스가 구현하게 되고, 상속의 특성상 상위 클래스와 하위 클래스가 강한 결합상태로 묶이기 때문에, 추후의 코드의 수정이 어려워짐으로써 상위 클래스의 변경 여파가 모든 하위 클래스에 전파될 수 있는 상황이 발생한다.

 

이를 취약한 기반 클래스 문제라고 한다.

 

 

상속을 사용하는 경우

  • 상위 클래스와 필요한 하위 클래스에 대하여서 잘 알고 있는 경우
  • 프로젝트 구조 자체가 상속을 위해 설계된 경우
  • is - a 관계가 명확한 경우 (동물 → 포유류 → 고래 등, 상위 분류와 하위분류가 명확한 경우)

 

자바의 상속 구조

 

단일 상속 구조

public class 상위 클래스 {

}

public class 하위 클래스 extends 상위 클래스 {

}

 

 

다중 레밸 상속 구조

public class 최상위 클래스 {

}

public class 상위 클래스 extends 최상위 클래스 {

}

public class 하위 클래스 extends 상위 클래스 {

}

 

 

계층적 상속 구조

public class 상위 클래스 {

}

public class 하위 클래스 A extends 상위 클래스 {

}

public class 하위 클래스 B extends 상위 클래스 {

}

 

자바는 다중 상속을 허용하지 않는다.

다중 상속을 허용하지 않는 이유는 하위 클래스의 코드를 복잡하게 만들어 프로그램을 취약하게 만들고, 버그를 유발할 수 있으며, 유지 보수하기 어렵게 만들기 때문이라고 한다.

 

해당 제약을 우회하는 방법으로는 인터페이스를 이용한 다중 상속이 있다.

public class 상위 클래스 {

}

public interface 인터페이스 {

}

public class 하위 클래스 extends 상위 클래스 implements 인터페이스 {

}

 

 

다중 상속 문제 : The Deadly Diamond of Death

이러한 구조를 가지고 있을 경우에 인터페이스 간의 동일한 이름의 메서드가 존재한다면, 어떤 메서드를 사용할지 모르게 되므로 컴파일 에러가 발생하게 된다.

 

이러한 에러를 해결하기 위하여선 해당 메서드를 하위 클래스에서 overriding 할 수밖에 없다.

 

 

살짝 다른 경우!

이러한 경우에는 클래스가 인터페이스에 비해서 우선순위를 가진다.

(동일한 메서드 시그니처가 존재하는 경우 클래스의 메서드를 먼저 호출한다. ) 

 

 

상속시 메모리 구조

 

이러한 코드를 작성하였다면?

public static void main(String[] args) {
        Dog dog = new Dog();
        dog.name = "푸들";
        dog.habit = "멍멍";
        dog.showHabit();
        dog.showName();


        Animal animal = new Dog();
        animal.name = "리트리버";
        animal.showName();
}

 

메모리 상으로 이러한 구조를 가지게 된다.

 

Super 키워드

상위 클래스로부터 상속받은 필드나 메서드를 하위 클래스에서 참조하는 데 사용하는 참조 변수이다.

public class A {
    protected String property = "properties";

    public void printProperties() {
        System.out.println("A.printProperties");
        System.out.println(property);
    }
}

public class B extends A {
    protected String property = "properties of Sub Class";

    @Override
    public void printProperties() {
        super.printProperties();
        System.out.println("B.printProperties");
        System.out.println(super.property);
        System.out.println(property);
    }
}

 

super() 메서드

부모 클래스의 생성자를 호출할 때 사용되는 메서드이다.

상속 구조 관계에서 하위 클래스를 생성한 경우 내부적으로 super를 호출하여 상위 클래스부터 생성한다.

public class A {
        public String name;
        public Long age;
        public A() {
            System.out.println("A.A constructor");
        }

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

        public A(String name, Long age) {
            this.name = name;
            this.age = age;
        }
    }

    public class B extends A {

        public B() {
            // super() 내부적으로 동작한다.
            System.out.println("B.B constructor");
        }

                public B(String name, Long age) {
            super(name, age);
            System.out.println("B.B args constructor");
        }
    }

        // 실행 함수
    public void run() {
        B b = new B();
                B b1 = new B("name", 20L);
    }

출력 결과

A.A constructor
B.B constructor
A.A args constructor
B.B args constructor

 

메서드 오버 라이딩

상속 구조 관계에서 상위 클래스의 메서드를 하위 클래스에서 다시 정의하는 것을 말한다.

상위 클래스에서 선언된 메서드 시그니처와 동일하게 작성하여야 한다. (메서드 명, 인자 + 반환 값)

→ 리턴 값을 다르게 설정할 경우 attempting to use incompatible return type 컴파일 에러가 발생

public class SuperClass {

    public SuperClass() {
        System.out.println("SuperClass constructor");
    }

    public void showPrint() {
        System.out.println("SuperClass.showPrint");
    }
}

public class SubClass extends SuperClass{

    public SubClass() {
        System.out.println("SubClass constructor");
    }

    @Override
    public void showPrint() {
        System.out.println("SubClass.showPrint");
    }
}

@Test
public void extendTest(){
    SuperClass superClass = new SubClass();
    superClass.showPrint(); // 하위 클래스의 메서드가 호출된다.
}

출력 결과

SuperClass constructor
SubClass constructor
SubClass.showPrint

 

다이내믹 메서드 디스패치

오버 라이딩된 메서드들이 있는 경우 어떤 것을 호출할지를 런타임에서 결정하는 것을 말한다.

해당 메소드 참조에 의해 호출될 때 객체 유형에 따라서 실행할 메서드를 결정하게 된다.

 

자바는 묵시적으로 메서드를 호출한 객체를 인자로 넘기기 때문에 메서드 내부에서 호출 객체를 참조할 수 있다.

-> 이는 런타임에서 호출하는 객체도 결정된 것을 말한다.

public class SuperClass {

    public SuperClass() {
        System.out.println("SuperClass constructor");
    }

    public void showPrint() {
        System.out.println("SuperClass.showPrint");
    }
}

public class SubClass extends SuperClass{

    public SubClass() {
        System.out.println("SubClass constructor");
    }

    @Override
    public void showPrint() {
        System.out.println("SubClass.showPrint");
    }
}

public class SubClazz extends SuperClass{

    public SubClazz() {
        System.out.println("SubClazz constructor");
    }

    @Override
    public void showPrint() {
        System.out.println("SubClazz.showPrint");
    }
}

@Test
public void extendTest(){
    SuperClass clazz = new SubClazz();
    clazz.showPrint();
}

출력 결과

SuperClass constructor
SubClazz constructor
notefive.oop.SubClazz@eafc191
SubClazz.showPrint

 

추상 클래스

하나의 추상 메서드를 지니는 클래스로 구현할 때에는 유사한 여러 클래스의 동일한 부분을 잘라내어 추상화하고, 공통화한 클래스를 말한다.

 

추상 클래스는 주로 클래스의 기반 뼈대나 공통 처리(재사용)에 사용한다.

 

추상 클래스의 장점

  • 유사한 동작을 가진 클래스들의 코드를 공통적으로 모아 추상화 하기에 하나의 추상 클래스에서 해당 코드를 관리할 수 있습니다.
  • 기능 확장 시 유사한 코드를 더 작성하지 않고 상위 클래스를 상속 받음으로써 간단하게 확장할 수 있습니다.

 

추상 클래스의 단점

  • 추상 클래스를 도입하고 상속을 진행하다 보면 전체 클래스의 수가 많아지게 된다.
  • 상속 계층이 깊어질수록 해당 부분을 개발하지 않고 하위 클래스를 사용하는 개발자는 로직의 이해를 위해 해당 계층의 상위부터 동작을 이해하여야 한다. (유지 보수가 어려워진다.)

 

Final 키워드

final : 해당 지정자가 작성된 것에 대해 상속이나 변경을 금지한다는 의미이다.

  • 객체 변수 : 그 필드의 값이 변경되는 것을 금지한다.

    → 값을 해당 필드에서 초기화하지 않는다면, 생성자를 통한 초기화가 강제된다.

      private final String name = "name";
    
      혹은
    
      private final String name;
    
      public Example(String name) {
              this.name = name;
      }
  • 객체 메서드 : 해당 클래스 타입을 상속받는 서브, 하위 클래스에서 오버라이드를 금지한다.

      public class A {
    
          public final void someMethod() {
              // Do something...
              System.out.println("super class!");
          }
      }
    
      public class B extends A {
    
                      // 컴파일 에러가 발생한다.
                      // cannot override someMethod() ... overridden method is final
                      @Override 
                      public final void someMethod() {
              }
      }
    
          public void run() {
              B b = new B();
              b.someMethod();
          }
  • 클래스 자신 : 해당 클래스의 상속을 금지한다. (String, Integer.... 등)

      public final class A {
    
          public final void someMethod() {
              // Do something...
              System.out.println("super class!");
          }
      }
    
      // 컴파일 에러가 발생한다.
      // cannot inherit from final
      public class B extends A { 
      }
    
          public void run() {
              B b = new B();
              b.someMethod();
          }
      }

 

Object 클래스

최상위 클래스이며, 모든 클래스는 묵시적으로 Object 클래스를 상속받게 된다.

  • equals(), toString(), clone() 등 유틸 메서드
  • wait(), notify(), notifyAll() 등 스레드 관련 메서드
  • finalize() : GC 전 리소스를 반환하기 위한 메서드
  • hashcode() : 해당 객체의 참조 값을 나타내는 메서드

들을 제공한다.

 

equals, toString, clone, getClass

public static void main(String[] args) throws CloneNotSupportedException {

    ObjectExample example = new ObjectExample();

    System.out.println(example.toString());

    System.out.println(example.equals(example));

    System.out.println(example.hashCode());

    System.out.println(example.getClass());

}

출력 결과

notefive.anonymous.ObjectExample@43a25848
true
1134712904
class notefive.anonymous.ObjectExample

 

스레드 관련 메서드

 

wait

  • 해당 Object를 선점하고 있다가 접근 권한을 다른 쓰레드에게 넘기고 notify, notifyAll 이 호출될 때까지 해당 쓰레드를 대기하게끔 설정하는 메서드이다.
    • 스레드는 Wait Set에 들어가게 된다.

notify

  • 해당 Object의 선점을 위해 Wait Set 에 대기중인 하나의 쓰레드를 실행 대기 상태로 만든다.
    • Entry Set의 쓰레드들과 같이 경합에 참여한다.

 

notifyAll

  • 해당 Obejct의 선점을 위해 Wait Set 에 대기중인 모든 쓰레드를 실행 대기 상태로 만든다.
    • notify와 동일한 경합상태가 발생한다.

 

참고자료

'Live Study' 카테고리의 다른 글

Live Study_Week 08. 인터페이스  (0) 2021.01.05
Live Study_Week 07. 패키지  (0) 2020.12.28
Live Study_Week 05. 클래스  (0) 2020.12.15
Live Study_Week 04. 제어문 + 과제  (0) 2020.12.01
Live Study_Week 03. 연산자  (0) 2020.11.24

 

클래스란 무엇일까?

클래스는 어떠한 객체들에 대한 분류이다.

  • 분류, 집합, 같은 속성과 기능을 가진 객체들을 총칭하는 개념이다.
  • 클래스는 해당 객체가 무엇인지 판단할 수 있는 일종의 식별 타입이 된다.

 

클래스의 구성요소

  • 변수 : 클래스의 Scope = { } 안에서 존재하는 변수를 말한다.

    • 객체 변수 : 접근 제어자와 변수 타입, 변수 명을 가지는 일반적인 상태 변수를 말한다.

      • 해당 변수는 클래스 정보에서 초기화되지 않고, 해당 타입을 가지는, 상속받은 객체가 생성되었을 때 Scope에 맞게 가시 된다. (공유된다)
    • 클래스 변수 : 위의 요소를 가지면서, static 이 작성된 상태 변수를 말한다.

      • 클래스가 로드되는 시점Class 정보가 저장되는 영역에서 해당 정보가 등록, 초기화 되기 때문에 이러한 명칭을 가진다. (Perm Gen, Meta space)
      • 모든 영역에서 가시되는 성질을 가진다.
  • 메서드 : 이것도 클래스의 Scope = { } 안에서 존재한다.

    • 객체 메서드 : 접근 제어자와 반환 타입, 메서드 명, 매개변수를 가지는 메서드를 말한다.

      • 인스턴스 변수와 마찬가지로 객체가 생성되었을 때 Scope에 맞게 가시 된다.
    • 클래스 메서드 : 위의 요소를 가지면서, static이 작성된 상태 변수를 말한다.

      • 클래스 변수와 마찬가지로 클래스 로드 시에 등록, 초기화된다.
      • 클래스를 생성하는 메서드인 팩토리 메서드를 정의할 때 static을 사용한다.
      • 어떠한 기능을 사용하기 위해 해당 타입의 객체를 생성하는 것이 무리가 있을 때, (유틸 성 기능의 필요함으로) static을 적용한다.
        • 예 : String.valueOf(), String.join(), String.format() 등
  • 생성자 : 당연히.. 클래스의 { } 안에 있다.

    • 객체를 생성할 때 사용되는 특별한 메서드를 말한다.
      • 별도의 생성자를 작성하지 않을 경우 인자가 없는 생성자가 자동으로 생성된다.
      • 생성 시점부터 필요한 상태 변수가 있을 경우 매개 변수를 가지는 생성자를 이용한다.

 

클래스의 생성 방법

Public final class FullName {

        // 어떠한 분류(클래스 타입)에 속하는 객체들이 가지는 상태 변수 : 객체의 상태 
        private final String firstName;
        private final String lastName;

        // 객체가 가지는 상태 값을 조작, 제공하는 메서드 : 객체의 행위이자 책임이다.

        // 메서드 선언부 public String getFirstName() : 객체가 수행해야할 책임
        // -> 인터페이스 구현, 추상 클래스의 상속을 통한 추상적인 책임은 무조건 구현하여햐 한다.

        // 메서드 구현부 { } : 객체가 해당 책임을 수행하는 방법. (객체의 자율적인 행위)
        public String getFirstName() {
                return this.firstName;
        }

        public String getLastName() {
                return this.lastName;
        }

        // 객체를 생성할 때 사용되는 생성자 : 인자 값을 통한 초기화를 진행하거나, 별도 로직을 수행한다.
        public FullName(String first, String last) {
                this.firstName = first;
                this.lastName = last;
        }
        
        public FullName() {
        	this("dev","lob");
        }
}

 

클래스에서 사용될 수 있는 수식자

  • 접근 제어자 : 적용된 키워드에 따라 해당 객체, 메서드가 가시 될 범위를 결정한다.

    • public : 모든 클래스에서 해당 클래스를 참조 가능하다.
    • package-private : 해당 클래스가 포함된 패키지 내에서만 참조 가능하다. (지정이 없는 경우)
    • protect : 서브, 하위 클래스와 동일 패키지 내부에 존재하는 클래스만 참조 가능하다.
    • private : 자기 자신인 객체 내부에서만 참조 가능하다. (인터페이스도 JDK9부터 사용 가능)
  • abstract : 해당 클래스, 메서드에서 구현부를 가지지 않고 상속 시 구현을 강제하는 지정 방식이다.

    • 추상 클래스, 인터페이스에서 사용된다.
  • static : 해당 클래스가 인스턴스화 되어있지 않아도 사용 가능하다는 지정 방식이다.

    • 위에서도 언급하였지만 생성 방식과 시점이 다르다.
  • final : 해당 지정자가 작성된 것에 대해 상속이나 변경을 금지한다는 의미이다.

    → 일종의 관례로써 전역적으로 사용되는 static 요소들은 final을 붙인다고 한다.

    → 하지만 private가 지정된 메서드나 final class에 대해선 명시하지 않아도 된다.

    • 객체 변수 : 그 필드의 값이 변경되는 것을 금지한다.

      → 값을 해당 필드에서 초기화하지 않는다면, 생성자를 통한 초기화가 강제된다.

    • 객체 메서드 : 해당 클래스 타입을 상속받는 서브, 하위 클래스에서 오버라이드를 금지한다.

    • 클래스 자신 : 해당 클래스의 상속을 금지한다. (String, Integer.... 등)

  • transient : 객체 직렬화 시에 직렬화 대상에서 제외한다. (이 경우 null로 처리된다.)

  • volatile : 해당 지정자가 붙은 변수에 대해서는 스레드가 값을 캐시하지 않고, 메인 메모리에서 처리한다.

    • 시간에 따라서 각 쓰레드가 참조하는 값이 달라지는 것을 방지한다.

    • 하나의 쓰레드가 값을 증가시키고, 여러 쓰레드가 값을 읽는 경우 항상 최신의 값을 제공하기 위해서는 해당 지정자를 사용하는 것이 좋다.

      → 여러 쓰레드가 값을 증가시키고, 읽는다면 Atomic Variable을 사용하자.

  • sysnchronized : 해당 지정자가 붙은 메서드와 스코프에 스레드 간 동기화를 진행한다는 의미이다.

    • (이후에 멀티 쓰레딩이라는 주제가 있으니 그때 알아보자.)
  • native : 해당 지정자가 붙은 메서드는 Java 가 아닌 네이티브 코드를 사용하는 것을 의미한다.

    • C/C++ 등으로 작성된 DDL이나 JNI에서 제공하는 코드를 말한다.
  • strictfp : 해당 지정자가 붙은 Double, Float에 대하여서 IEEE 754 규격을 적용한다는 의미이다.

    • 인터페이스나 메서드에 지정할 수 있다.

 

객체를 생성하는 방법?

객체를 생성하는 방법은 new 키워드를 사용하는 것이다.

// public static void main 함수나 그외 메서드 영역, 클래스에서 사용한다.

// 클래스 타입, 인스턴스(레퍼런스) 변수명 = new 키워드 생성자(인자...); 의 형태를 가진다.
// new 를 이용한 객체의 생성을 인스턴스화라고 한다.
FullName fullName = new FullName("babo", "Lob");

객체를 생성하면, 해당 객체는 바로 Heap 영역에서 생성된다. 이때 final을 제외한 모든 변수는 초기화되지 않은 상황이 되는데, 이 경우 초깃값을 가지게 된다.

  • char = "\u0000"
  • byte, short, int = 0
  • long = 0L
  • float = 0.0F
  • double = 0.0D
  • boolean = false
  • 배열, 레퍼런스 변수 = null

 

 

올바른 객체지향 시점 (내가 매번 복기하는 3가지 문장)

  • 클래스는 객체지향의 핵심이 아니다. 클래스는 객체를 구현하는 메커니즘이자 설계도일 뿐이다.
  • 클래스는 결국 비슷한 특징을 가지는 객체들의 분류를 의미한다.
  • 클래스는 어떠한 값을 담는 것이 중요한 게 아니다. 객체가 되었을 때, 제공할 행위가 중요하다.

 

메서드를 정의하는 방법?

 

메서드 시그니처?

메서드 정의 시에 메서드의 이름과 매개 변수들을 의미한다.

 

다음 주제에서 정리해야 될 것으로 예상되는 오버 로딩을 제공하기 위함이다.

// {public, private} {static, strictfp, sysnchronized} {Types, void} 메서드명 (인자들) {} 

public String getFirstName() {
        return this.firstName;
}

 

메서드의 기본적인 동작

반환될 값의 타입이 존재한다면 (void 가 아니라면) return을 통한 값 전달이 강제된다.

  • return을 통한 값 전달은 쉽게 생각하면, 메서드가 호출된 곳에 값을 주는 것이다.

 

생성자를 정의하는 방법?

생성자에 대해서도 메서드 시그니처가 적용된다. 이는 점층적 생성자 패턴을 통해 알 수 있다.

 

전달하는 인자에 의해서 호출되는 생성자가 달라진다.

  • 점층적으로 생성되는 생성자가 5개 이상으로 늘어난다면 코드가 복잡해진다.. 이때에는 빌더 패턴을 고려해보자. (이펙티브 자바)
public SomeClass() {
}

SomeClass some = new SomeClass();

public SomeClass(String a) {
        this.aa = a;
}

SomeClass some = new SomeClass(a, b);

public SomeClass(String a, String b, String c) {
        this.aa = a;
        this.bb = b;
        this.cc = c;
}

SomeClass some = new SomeClass(a, b, c);

...... 

 

this 키워드 이해하기

this 참조 변수는 객체가 자기 자신을 참조하는 데 사용하는 키워드이다.

즉 this는 해당 객체의 참조 값 (hashcode 값)을 가지고 있는 것이다.

@Test
public void helloThis(){
    Animal animal = new Animal();
    animal.showThis();
    System.out.println(animal);
}

public class Animal {

    public Animal() {
    }

    public void showThis() {
        System.out.println(this);
    }
}
notefive.oop.Animal@4eb7f003
notefive.oop.Animal@4eb7f003

 

결과를 보면 System.out.println(this), System.out.println(animal)가 동일한 결과를 출력함을 알 수 있다.

객체를 println으로 출력할 때는 인스턴스를 출력할 경우 toString()을 호출한다는 것은 알 것이라고 생각한다.

재정의가 되지 않았을 경우, Object의 toString()을 호출한다.

문자를 Bold 처리한 부분을 보면 출력되는 정보에 뒷부분에 hashCode()를 사용함을 볼 수 있다.

// Object의 toString
public String toString() {
    return getClass().getName() + "@" + **Integer.toHexString(hashCode());**
}

 

this는 언제 사용하는가?

  • 객체 변수와 같은 이름을 가진 다른 것이 존재할 때 객체 변수를 가리키고 있음을 명확히 한다.
String name;
public void someMethod(String name) {
    this.name = name;
}
  • 생성자에서 다른 클래스의 생성자를 호출하는 데 사용할 수 있다.
// 해당 코드는 밑의 생성자를 호출한다.
public SomeClass() {
        this("someString");
}

public SomeClass(String a) {
        this.aa = a;
}
  • 현재 Java 객체를 매개 변수로 전달하는 데 사용한다.
public void SomeClassA{
    public void run() {
        SomeClassB someClassB = new SomeClassB();
        someClassB.getClass(this);
    }
  }

public void SomeClassB{
public void getClass (SomeClassA some){
        // Do SomeThing
    }
}
  • 현재 객체를 반환하는 데 사용할 수 있다. 해당 코드는 빌더 패턴에서 볼 수 있다.
public Builder name(int val) {
    name = val;   // 사용되는 변수명이 다르므로 this를 생략 가능하다.
    return this;  // 객체 자기 자신을 반환함으로써 참조 연산자 . 을 이용한 체이닝이 가능하다.
}

 

5주 차 과제 BinaryTree 구현하기

트리 (Tree)

  • 나무의 형태를 뒤집은 것 같은 형태의 자료구조이며, 각 노드는 M개만큼의 자식 노드를 가지게 된다. 루트 노드부터 시작하여 각 노드는 단반향 간선을 통해서 연결되어 있으며, 하위 레밸에서도 트리가 반복되는 모습을 보이게 되어있다.
  • 트리의 길이는 출발한 노드부터 목적지 노드까지의 거쳐야 하는 가짓수를 의미한다.
  • 트리의 깊이는 루트 노드부터 특정 노드까지의 길이를 의미한다.

 

이진트리 (Binary Tree)

  • 모든 노드의 자식 노드가 최대 2개까지인 트리를 말한다.

 

Binary Tree의 종류

  • 이진 탐색 트리 (Binary Search Tree)

    • Root 노드의 값을 기준하여 좌, 우측에 값을 나누어 저장하는 반 정렬 상태의 이진트리이다.
    • 기본적으로 좌측의 서브 트리, 노드들은 Root 노드보다 작으며, 우측은 크다.
    • 중복된 값을 허용하지 않는다.
  • 정 이진트리 (Full Binary Tree)

    • 마지막 래벨의 노드(리프 노드)를 제외한 모든 노드가 두 자식을 가지고 있는 트리를 말한다.
  • 완전 이진트리

    • 마지막 레밸을 제외하고는 모든 레밸이 완전히 채워져 있는 상태의 트리를 말한다.
    • 마지막 레밸의 노드를 채울 때에는 가장 왼쪽부터 채우게 된다.

 

int 값을 가지고 있는 이진트리를 나타내는 Node라는 클래스를 정의하세요.

  • int value, Node left, right를 가지고 있어야 합니다.
public class Node {

    private int element;
    private Node left;
    private Node right;

    public Node(int element) {
        this.element = element;
        left = right = null;
    }

    public Node(int element, Node left, Node right) {
        this.element = element;
        this.left = left;
        this.right = right;
    }

    public int getElement() {
        return element;
    }

    public Node getLeft() {
        return left;
    }

    public Node getRight() {
        return right;
    }
}

Lob-dev/DashBoardDemo

 

 

BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메서드를 구현하세요.

  • DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
public class BinaryTree {

    public void dfs(Node node) {
        if (node == null) {
            return;
        }
        dfs(node.getLeft());
        System.out.print(node.getElement() + " ");
        dfs(node.getRight());
    }

    public void bfs(Node node) {
        if (node == null) {
            return;
        }
        Queue<Node> queue = new ArrayDeque<>();
        queue.offer(node);
        while (!queue.isEmpty()) {
            traverse(queue);
        }
    }

    private void traverse(Queue<Node> queue) {
        Node poll = queue.poll();
        if (poll != null){
            System.out.print(poll.getElement() + " ");
            getChild(queue, poll.getLeft());
            getChild(queue, poll.getRight());
        }
    }

    private void getChild(Queue<Node> queue, Node child) {
        if (child != null) {
            queue.add(child);
        }
    }
}

Lob-dev/DashBoardDemo

 

테스트 코드

Lob-dev/DashBoardDemo

 

참고자료

+ Recent posts