목표

자바의 람다식에 대해 학습하세요.

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

람다식이란?

메소드를 하나의 간결한 식(expression)으로 표현한 것

메소드를 람다식으로 표현하면 메소드의 이름과 반환값이 없어지므로 람다식을 '익명 함수(anonymous function)'라고도 한다.

 

람다식은 FunctionalInterface의 조건을 충족 해야 사용이 가능

FunctionalInterface : 오직 하나의 메소드 선언을 갖는 인터페이스

 

※메소드와 함수

메소드와 함수는 같은 의미지만 용어를 분리해서 사용했다. Java의 메소드는 객체의 행위,동작을 의미하며 특정 클래스에 반드시 속해야 한다는 제약이 있다. 그러나 JDK 1.8이후 람다식의 도입으로 메소드가 하나의 독립적인 기능을 하기 때문에 익명 함수라는 용어를 사용한다.

 

람다식 사용법

 

메소드의 이름과 반환타입을 제거하고 매개변수 선언부와 body{  } 사이에 ->를 추가한다.

ReturnType methodName(Parameter p) { 
	// body
}

(Parameter p) -> {
	// body
}

두 값 중에 큰 값을 반환하는 메소드 max()를 람다식으로 변환하기

int max(int a, int b) {
	return a > b ? a : b;
}

(int a, int b) -> {
	return a > b ? a : b;
}

return문 대신 식(expression)으로 대신할 수 있다. 식의 연산 결과가 자동으로 반환값이 된다.

문장이 아닌 식으로 끝에 세미콜론( ; )을 붙이지 않는다.

(int a, int b) -> a > b ? a : b

매개변수의 타입은 추론이 가능한 경우(대부분의 경우) 생략 가능하다.

참고로 반환타입을 제거할 수 있는 이유도 항상 추론이 가능하기 때문이다.

(a, b) -> a > b ? a : b

 

 

람다식 작성 문법 정리

 

1. 기본적인 작성 규칙
- 이름과 반환타입은 작성하지 않는다. (anonymous function)

2. 매개변수
- 추론이 가능한 매개변수의 타입은 생략할 수 있다.
- 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않는다.
- 선언된 매개변수가 하나인 경우 괄호( )를 생략 할 수 있다.
- 단, 매개변수의 타입을 작성한 경우엔 매개변수가 하나라도 괄호( )를 생략할 수 없다.

3. body { }
- return문(return statement) 대신 식(expression)으로 대체할 수 있다.
- 식(expression)의 끝에 세미콜론(;)은 붙이지 않는다.
- 괄호{ } 안의 문장이 하나일 때는 괄호{ }를 생략할 수 있다.
- 이 때, 문장의 끝에 세미콜론(;)은 붙이지 않는다.
- 그러나 return문은 괄호를 생략할 수 없다.

 

매개변수 규칙
  • 추론이 가능한 매개변수의 타입은 생략할 수 있다.
  • 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않는다.
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b

 

  • 선언된 매개변수가 하나인 경우 괄호( )를 생략 할 수 있다.
  • 단, 매개변수의 타입이 있으면 괄호( )를 생략할 수 없다.
a -> a * a	// OK

int a -> a * a	// Error

body { } 규칙
  • return문 대신 식으로 대체 할 수 있다.
  • 단, 식의 끝에 세미콜론(;)은 붙이지 않는다.
(int a, int b) -> {
	return a > b ? a : b;
}
(int a, int b) -> a > b ? a : b

 

  • 괄호 { } 안의 문장이 하나일 때 괄호 { }를 생략 할 수 있다.
  • 단, 문장의 끝에 세미콜론(;)은 붙이지 않는다.
(String name, int i) -> { System.out.println(name + "=" + i); }
(String name, int i) -> System.out.println(name + "=" + i)

함수형 인터페이스 (Functional Interface)

 

함수형 인터페이스는 람다식을 다루는 인터페이스이다. @FunctionalInterface 어노테이션을 사용한다.

 

람다식은 실제로 메소드 그 자체가 아닌 익명 클래스의 객체와 동등하다.

 

익명 객체의 메소드와 람다식의 매개변수, 반환값이 일치하면 익명 객체를 람다식으로 대체 할 수 있다.

@FunctionalInterface
interface MyFunction {
	public abstract int max(int a, int b);
}

위 인터페이스를 구현한 익명 클래스의 객체는 아래와 같이 생성 할 수 있다. 

MyFunction f = new MyFunction() {	//MyFunction인터페이스를 구현한 익명 클래스의 객체 생성
	public int max(int a, int b) {
    return a > b ? a : b
    }
}
int big = f.max(5, 3);	// 익명 객체의 메소드 호출    

여기서 MyFunction 인터페이스에 정의된 메소드 max( )는 람다식 '(int a, int b) -> a > b ? a : b' 와 일치한다.람다식은 익명 객체와 동등하므로 아래와 같이 대체 할 수 있다.

MyFunction f = (a , b) -> a > b ? a : b;	// 익명 객체를 람다식으로 대체

int big = f.max(5 , 3);	// 익명 객체의 메소드 호출

이렇게 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체 할 수 있는 이유는

람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메소드 max( )와

람다식의 매개변수 타입과 개수 그리고 반환값이 일치하기 때문이다.

 

하나의 메소드가 선언된 인터페이스를 정의해서 

람다식을 이용하는 것은 기존의 자바의 규칙을 어기지 않으면서 자연스럽다.

그렇기 때문에 인터페이스를 통해 람다식을 다루기로 결정되었고,

람다식을 다루기 위한 인터페이스를 함수형 인터페이스(functional interface)라 부르기로 했다.

@FunctionalInterface
interface MyFunction {
	public abstract int max(int a, int b);
}

단, 함수형 인터페이스에서는 오직 하나의 추상 메소드만 정의되어 있어야 한다는 제약이 있다.

그래야 람다식과 인터페이스의 메소드가 1 : 1로 연결될 수 있기 때문이다.

다만, static 메소드와 default 메소드의 개수에는 제약이 없다.

함수형 인터페이스로 구현한 인터페이스라면 반드시 '@FunctionalInterface' 어노테이션을 정의하자.

왜냐하면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주기 때문이다.

 

기본 함수형 인터페이스

자바에서 기본적으로 제공하는 함수형 인터페이스는 다음과 같은 것들이 있다.

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate

이 외에도 다양한 것들이 있다.

 

Runnable

Runnable은 인자를 받지 않고 리턴값도 없는 인터페이스이다.

public interface Runnable {
	public abstract void run();
}    

아래 코드처럼 사용할 수 있다.

Runnable runnable = ( ) -> System.out.println("run anything!");
runnable.run();
// 결과
// run anything!

Runnable은 run( )을 호출해야 한다. 함수형 인터페이스마다 run( )과 같은 실행 메소드 이름이 다르다.인터페이스 종류마다 만들어진 목적이 다르고, 그 목적에 맞는 이름을 실행 메소드 이름으로 정하였기 때문이다.

 

Supplier

Supplier<T>는 인자를 받지 않고 T타입의 객체를 리턴한다.

public interface Supplier<T> {
	T get();
}    

아래 코드처럼 사용할 수 있다.

Supplier<String> getString = () -> "Good morning!";
String str = getString.get();
System.out.println(str);

// 결과
// Good morning!

 

Consumer

Consumer<T>는 T 타입의 객체를 인자로 받고 리턴 값은 없다.

public interface Consumer<T> {
	void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
    	Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

아래 코드처럼 사용할 수 있다.

Consumer<String> printString = text -> System.out.println("Help " + text + "!!");
printString.accept("me");
// 결과
// Help me!!

또한, andThen( )을 사용하면 두개 이상의 Consumer를 연속적으로 실행할 수 있다.

Consumer<String> printString = text -> System.out.println("Help " + text + "!!");
Consumer<String> printString2 = text -> System.out.println("--> Yes");
printString.andThen(printString2).accept("me");
// 결과
// Help me!!
// --> Yes

 

Function

Function<T, R>는 T타입의 인자를 받고, R타입의 객체를 리턴한다.

public interface Function<T, R> {
	R apply(T t);
    
    default <V> FUnction<V, R> compose(Function<? Super V, ? extends T> before) {
    	Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
        default <V> FUnction<T, V> compose(Function<? Super R, ? extends V> after) {
    	Objects.requireNonNull(after);
        return (T t) -> after.apply(after.apply(t));
    }
    
    static <T> Function<T, T> identity() {
    	return t -> t;
    }
}

다음과 같이 사용할 수 있다. apply() 메소드를 사용한다.

Function<Integer, Integer> multiply = (value) -> value *2;
Integer result = multiply.apply(3);
System.out.println(result);
// 결과
// 6

 

compose( ) 는 두개의 Function을 조합하여 새로운 Function 객체를 만들어주는 메소드이다.

주의할 점은 andThen() 과는 실행 순서가 반대이다.

compose( ) 인자로 전달되는 Function이 먼저 수행되고 그 이후에 호출하는 객체의 Function이 수행된다.

 

예를들어, 다음과 같이 compose를 사용하여 새로운 Function을 만들 수 있다. 

apply를 호출하면 add 먼저 수행되고 그 이후에 multiply가 수행된다.

Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add      = (value) -> value + 3;

Function<Integer, Integer> addThenMultiply = multiply.compose(add);

Integer result1 = addThenMultiply.apply(3);
System.out.println(result1);
// 결과
// 12

 

Predicate

Predicate<T> 는 T타입 인자를 받고 결과로 boolean을 리턴한다.

public interface Predicate<T> {
    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

다음과 같이 사용할 수 있다. test( ) 메소드를 사용한다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
System.out.println("10 is bigger than 5? -> " + isBiggerThanFive.test(10));
// 결과
// 10 is bigger than 5? -> true

 

and( )와 or( )는 다른 Predicate와 함께 사용된다.

and( )는 두 개의 Predicate가 true일 때 true를 리턴하며, or( )는 두 개중에 하나만 true면 true를 리턴한다.

Predicate<Integer> isBiggerThanFive = num -> num > 5;
Predicate<Integer> isLowerThanSix = num -> num < 6;
System.out.println(isBiggerThanFive.and(isLowerThanSix).test(10));
System.out.println(isBiggerThanFive.or(isLowerThanSix).test(10));
// 결과
// false
// true

 

isEqual( )은 static 메소드로, 인자로 전달되는 객체와 같은지 체크하는 Predicate 객체를 만들어 준다.

Predicate<String> isEquals = Predicate.isEqual("Google");
isEquals.test("Google");
// 결과
// true

Variable Capture

람다식에서 외부 지역변수를 참조하는 행위를  Lambda Capturing(람다 캡쳐링)  이라고 한다.

 

람다에서 접근가능한 변수는 아래와 같이 세가지 종류가 있다.

 

1. 지역 변수

2. static 변수

3. 인스턴스 변수

 

지역변수만 변경이 불가능하고 나머지 변수들은 읽기 및 쓰기가 가능하다.

람다는 지역 변수가 존재하는 스택에 직접 접근하지 않고, 지역 변수를 자신(람다가 동작하는 쓰레드)의 스택에 복사한다.

각각의 쓰레드마다 고유한 스택을 갖고 있기 때문에  지역 변수가 존재하는 쓰레드가 사라져도

람다는 복사된 값을 참조하면서 에러가 발생하지 않는다.

 

그런데 멀티 쓰레드 환경이라면, 여러 개의 쓰레드에서 람다식을 사용하면서 람다 캡쳐링이 계속 발생하는데

이 때 외부 변수 값의 불변성을 보장하지 못하면서 동기화 문제가 발생한다.

이러한 문제로 지역변수 final,  Effectively Final  제약조건을 갖게된다.

Effectively Final
람다식 내부에서 외부 지역변수를 참조하였을 때 지역 변수는 재할당을 하지 않아야 하는 것을 의미

 

인스턴스 변수나 static 변수는 스택 영역이 아닌 힙 영역에 위치하고,

힙 영역은 모든 쓰레드가 공유하고 있는 메모리 영역이기 때문에, 값의 쓰기가 발생하여도 별 문제가 없는 것이다.

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

class Outer {
    int val = 10;

    class Inner {
        int val = 20;

        void method(int i) {    // void method(final int i)
            int val = 30;   // final int val = 30;
//            i = 10;       // ERROR. 상수의 값은 변경할 수 없다.

            MyFunction f = () -> {
                System.out.println("             i : " + i);
                System.out.println("           val : " + val);
                System.out.println("      this.val : " + ++this.val);
                System.out.println("Outer.this.val : " + ++Outer.this.val);
            };

            f.myMethod();
        }
    } // End Of Inner
}   // End Of Outer

public class LambdaEx {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method(100);
    }
}
// 결과
             i : 100
           val : 30
      this.val : 21
Outer.this.val : 11

 

  • 람다식 내에서 참조하는 지역변수 final이 붙지 않아도 상수로 간주된다.
  • 람다식 내에서 지역변수 i와 val을 참조하고 있으므로 람다식 내에서나 다른 곳에서도 이 변수들의 값을 변경할 수 없다.
  • 반면, Inner 클래스와 Outer 클래스의 인스턴스 변수인 this.val과 Outer.this.val은 상수로 간주되지
  • 않아서 값을 변경해도 된다.
  • 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조다.
  • 따라서 위의 코드에서 this는 중첩 객체인 Inner이다.

 


메소드, 생성자 레퍼런스

 

메소드 참조

람다식이 하나의 메소드만 호출하는 경우에는 '메소드 참조(method reference)' 를 통해 람다식을 간략하게 작성할 수 있다.

 

메소드 참조를 작성하는 방법은 아래와 같다.

클래스이름::메소드이름
or
참조변수::메소드이름
Function<String, Integer> f = s -> Integer.parseInt(s);

보통 위처럼 람다식을 작성하는 데, 이 람다식을 메소드로 표현하면 아래와 같다.

// Test Method
Integer exam(String s) {
	return Integer.parseInt(s)
}

이 wrapper 메소드는 별로 하는 일이 없다. 값을 받아서 Integer.parseInt()에게 넘겨줄 뿐이다.

 

Function<String, Integer> f = s -> Integer.parseInt(s);

// 메소드 참조
Function<String, Integer> f = Integer::parseInt;

메소드 참조를 이용하면 더 간략하게 작성할 수 있다.

위 메소드 참조에서 람다식 일부가 생략되었지만, 컴파일러는 생략된 부분을 우변의 parseInt 메소드의 선언부로부터,

또는 좌변의 Function 인터페이스에 지정된 제네릭 타입으로부터 쉽게 알아낸다.

 

또 다른 예를 보자.

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equlas(s2);

위 람다식에서 어떤 부분을 변경할 수 있을까?

 

  • 참조변수 f의 타입으로 유추하면 람다식이 두 개의 String 타입의 매개변수를 받는다. 따라서, 매개변수 생략가능.
  • 매개변수를 생락하면, equals만 남는데, 두 개의 String을 받아서 Boolean을 반환하는 equals라는 메소드 이므로
  • String::equals로 변경한다.
BiFunction<String, String, Boolean> f = String::equals;

 

메소드 참조로 변경한 결과이다.

MyClass obj = new MyClass();
Function<String, Boolean> f = x -> obj.equals(x);   //  람다식
Function<String, Boolean> f2 = obj::equals;         //  메소드 참조

메소드 참조를 사용하는 경우가 한 가지 더 있는데, 이미 생성된 객체의 메소드를 람다식에서 사용한 경우에는

클래스 이름 대신 그 객체의 참조변수 를 적어야 한다.

종류 람다 메소드 참조
static 메소드 참조 x -> ClassName.method(x) ClassName::method
인스턴스 메소드 참조 (obj, x) -> obj.method(x) ClassName:method
특정 객체 인스턴스 메소드 참조 x -> obj.method(x) obj::method

생성자의 메소드 참조

생성자를 호출하는 람다식도 메소드 참조로 변환할 수 있다.

Supplier<MyClass> s = () -> new MyClass();      //  람다식
Supplier<MyClass> s = MyClass:new;              //  메소드 참조

매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.

Function<Integer, MyClass> f = i -> new MyClasS(i);     //  람다식
Function<Integer, MyClass> f = MyClass:new;             //  메소드 참조

BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);  //  람다식
BiFunction<Integer, String, MyClass> bf = MyClass::new;                 //  메소드 참조

만약 배열을 생성할 때는 다음과 같이 사용한다.

Function<Integer, int[]> f = x -> new int[x];   //  람다
Function<Integer, int[]> f2 = int[]::new;       //  메소드 참조

 

예제

 

public class MethodReferences {
    public static void main(String[] args) {
//        Function<String, Integer> f = s -> Integer.parseInt(s);
        Function<String, Integer> f = Integer::parseInt;

        System.out.println(f.apply("100") + 200);

        // Supplier 입력 X, 출력 O
//        Supplier<MyClass> s = () -> new MyClass();
        Supplier<MyClass> s = MyClass::new;
        System.out.println(s.get());

        Function<Integer, MyClass> f2 = MyClass::new;
        MyClass m = f2.apply(100);
        System.out.println(m.iv);
        System.out.println(f2.apply(200).iv);

        Function<Integer, int[]> f3 = int[]::new;
        System.out.println(f3.apply(10).length);

    }
}

class MyClass {
    int iv;

    MyClass () {}

    MyClass (int iv) {
        this.iv = iv;
    }
}

 

 

references : atoz-develop.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4%EC%8B%9DLambda-Expression www.notion.so/758e363f9fb04872a604999f8af6a1ae codechacha.com/ko/java8-functional-interface/ watrv41.gitbook.io/devbook/java/java-live-study/15_week

 
좋아요1
공유하기
글 요소



출처: https://dev-coco.tistory.com/29 [슬기로운 개발생활😃]

'Language > Java' 카테고리의 다른 글

[8주차] 인터페이스  (0) 2022.02.06
[6주차] 상속  (0) 2022.01.30
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10

목표

자바의 인터페이스에 대해 학습하세요.

학습할 것 (필수)

  • 인터페이스 정의하는 방법
  • 인터페이스 구현하는 방법
  • 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  • 인터페이스 상속
  • 인터페이스의 기본 메소드 (Default Method), 자바 8
  • 인터페이스의 static 메소드, 자바 8
  • 인터페이스의 private 메소드, 자바 9

 

1. 인터페이스 정의하는 방법

인터페이스란?

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메소드를 갖지만 

추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 일반 메소드 또는 멤버변수를 구성원으로 가질 수 없다.

오직 추상메소드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.

* 기존에는 인터페이스에 일반 메소드를 구현할 수 없었지만, 자바 8버전부터 default 예약어를 통해 일반 메소드구현이 가능하다.

 

인터페이스의 장점

1. 개발 시간 단축
인터페이스가 작성되면 이를 사용해서 프로그램을 작성하는 것이 가능하다.
메서드를 호출하는 쪽에서는 선언부만 알면 되기 때문이다.

2. 표준화 가능
프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 하면 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

3. 서로 관계없는 클래스들 간의 관계를 맺어준다
하나의 인터페이스를 공통적으로 구현하도록 하여 관계를 맺어 줄 수 있다.

4. 독립적인 프로그래밍이 가능
인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다.

 

인터페이스를 작성하는 방법

인터페이스를 작성하는 것은 클래스를 작성하는 것과 동일하다. 단지 키워드로 class 대신 interface를 사용한다. 그리고 interface에도 클래스와 같이 접근제어자를 사용할 수 있다.

interface 인터페이스이름 {
    public static final 타입 상수이름 = 값;
    public abstract 메서드이름(매개변수목록);
}

일반적인 클래스의 멤버들과는 달리 인터페이스의 멤버들은 제약사항이 있다.

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. 단, static 메서드와 default 메서드는 예외(JDK 1.8부터)

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

 

인터페이스 구현

인터페이스는 구현한다는 의미의 키워드 'implements'를 사용한다.

public interface Animal {
    void sound();
}
public class Dog implements Animal {

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
public class Lion implements Animal {

    @Override
    public void sound() {
        System.out.println("크아앙");
    }
}

여기서 중요한건 Animal인터페이스에 정의된 sound()메소드를

Dog 와 Lion 클래스에서 구현할 때 접근제어자를 public으로 했다는 것이다.

이렇게 오버라이딩 할 때는 부모의 메소드보다 넓은 범위의 접근 제어자를 지정해야한다.

 


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

다형성을 공부하면 자손클래스의 인스턴스를 부모타입의 참조변수로 참조하는 것이 가능하다는 것을 알 수 있다.

인터페이스도 이를 구현한 클래스의 부모라 할 수 있으므로 해당 인터페이스 타입의 참조변수로클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

public interface Animal {
    void sound();
}
ublic class Dog implements Animal {

    @Override
    public void sound() {
        System.out.println("멍멍");
    }

    public void sleep() {
        System.out.println("새근새근 잡니다.");
    }
}
public class Lion implements Animal {

    @Override
    public void sound() {
        System.out.println("크아앙");
    }

    public void hunting() {
        System.out.println("사냥을 합니다.");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal lion = new Lion();

        dog.sound();
        lion.sound();

      //  dog.sleep();     X 
      //  lion.hunting();  X
      
      ((Dog)dog).sleep(); 	  // O
      ((Lion)lion).hunting();     // O
    }
}

dog가 바라보고 있는 것은 Animal 인터페이스이기 때문에 dog.sleep()은 호출하지 못하므로

 ((Dog)dog).sleep() 으로 캐스팅하여 사용해야 한다.

반대로 Dog dog = new Animal(); 는 선언하지 못하고 컴파일 에러가 발생한다.


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

다형성을 공부하면 자손클래스의 인스턴스를 부모타입의 참조변수로 참조하는 것이 가능하다는 것을 알 수 있다.

인터페이스도 이를 구현한 클래스의 부모라 할 수 있으므로 해당 인터페이스 타입의 참조변수로클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

public interface Animal {
    void sound();
}
ublic class Dog implements Animal {

    @Override
    public void sound() {
        System.out.println("멍멍");
    }

    public void sleep() {
        System.out.println("새근새근 잡니다.");
    }
}
public class Lion implements Animal {

    @Override
    public void sound() {
        System.out.println("크아앙");
    }

    public void hunting() {
        System.out.println("사냥을 합니다.");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal lion = new Lion();

        dog.sound();
        lion.sound();

      //  dog.sleep();     X 
      //  lion.hunting();  X
      
      ((Dog)dog).sleep(); 	  // O
      ((Lion)lion).hunting();     // O
    }
}

dog가 바라보고 있는 것은 Animal 인터페이스이기 때문에 dog.sleep()은 호출하지 못하므로

 ((Dog)dog).sleep() 으로 캐스팅하여 사용해야 한다.

반대로 Dog dog = new Animal(); 는 선언하지 못하고 컴파일 에러가 발생한다.


인터페이스 상속

자바에서 다중상속은 불가능하지만, 인터페이스는 예외이다.

인터페이스는 다중상속, 즉 여러 개의 인터페이스로부터 상속 받는 것이 가능하다.

인터페이스는 인터페이스로부터만 상속받을 수 있다.

 

자동차의 다양한 주행모드의 SportsMode, EchoMode, NormalMode를 예로 들어보자.

public interface EchoMode {

    void EchoMode();
}

public interface NormalMode {

    void NormalMode();
}

public interface SportsMode {

    void SportsMode();
}

아래의 Car 클래스는  3개 (EchoMode, SportsMode, NormalMode)의 인터페이스를 상속 받는다.

public class Car implements EchoMode, SportsMode, NormalMode{
    
    @Override
    public void EchoMode() {
        System.out.println("에코 모드로 주행 합니다.");
    }

    @Override
    public void NormalMode() {
        System.out.println("기본 모드로 주행 합니다.");
    }

    @Override
    public void SportsMode() {
        System.out.println("스포츠 모드로 주행 합니다.");
    }
}

 


인터페이스 기본 메소드 (default method), 자바 8

인터페이스는 기능에 대한 선언만 가능하기 때문에, 실제 코드를 구현한 로직은 포함될 수 없다.

하지만 자바8에서 이러한 룰을 깨트리는 기능이 나오게 되었는데, 그것이 default method이다.

 

메소드 선언시에 default를 명시하게 되면 인터페이스 내부에서도 코드가 포함된 메소드를 선언할 수 있다.

 

접근제어자에서 사용하는 default와 같은 키워드이지만, 접근제어자는 아무것도 명시하지 않은 접근제어자를 default라 하며

인터페이스의 default method는 'default'라는 키워드를 명시해야 한다.

interface MyInterface {
   default void printHello() {
      System.out.println("Hello World!");
      }
}

default라는 키워드를 메소드에 명시하게 되면 인터페이스 내부라도 코드를 작성 할 수 있다.

 

 

왜 사용할까?

사실 인터페이스는 기능에 대한 구현보다는, 기능에 대한 '선언'에 초점을 맞추어서 사용 하는데, 디폴트 메소드는 왜 등장했을까?

...(중략)... 바로 "하위 호환성"때문이다. 예를 들어 설명하자면, 여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다. (자바의 신 2권)

기존에 존재하던 인터페이스를 이용하여서 구현된 클래스를 만들고 사용하고 있는데,

인터페이스를 보완하는 과정에서 추가적으로 구현해야 할, 혹은 필수적으로 존재해야 할 메소드가 있다면,

이미 이 인터페이스를 구현한 클래스와의 호환성이 떨어지게 된다. 이러한 경우 default 메소드를 추가하게 된다면

하위 호환성은 유지되고 인터페이스의 보완을 진행 할 수 있다.

interface MyInterface {
    default void printHello() {
        System.out.println("Hello World");
    }
}
//구현체 생성
class MyClass implements MyInterface {
}

public class DefaultMethod {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.printHello(); //실행결과 Hello World 출력
    }
}

[정리]

-interface에서도 메소드 구현이 가능하다.

-참조 변수로 함수를 호출할 수 있다.

-implements한 클래스에서 재정의가 가능하다.


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

인스턴스 생성과 상관없이 인터페이스 타입으로 호출하는 메소드이다.

static 예약어를 사용하고, 접근제어자는 항상 public이며 생략 할 수 있다.

static method는 일반적으로 우리가 정의하는 메소드와는 다르다.

1. body가 있어야 한다.

2. implements 한 곳에서 override가 불가능하다.

public interface ICalculator {
 
    int add(int x, int y);
    int sub(int x, int y);
 
    default int mul(int x, int y) {
 
        return x * y;
    }
 
    static void print(int value) {
 
        System.out.println(value);
    }
}
public class CalcTest {
 
    public static void main(String[] args) {
 
        ICalculator cal = new Calculator();
 
        // cal.print(100); error
        ICalculator.print(100); 
        // interface의 static 메소드는 반드시 interface명.메소드 형식으로 호출
    }
}

static 메소드를 사용하는 데 주의해야할 점은 기존 클래스의 static 메소드처럼 class이름.메소드로 호출하는게 아니라

interface이름.메소드로 호출해야 한다.

100
 
Process finished with exit code 0

 

 


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

java8 에서는 default method와 static method가 추가 되었고,

java9 에서는 private method와 private static method가 추가 되었다.

 

java8의 default method와 static method는 여전히 불편하게 만든다.

 

단지 특정 기능을 처리하는 내부 method일 뿐인데도, 외부에 공개되는public method로 만들어야하기 때문이다.

interface를 구현하는 다른 interface 혹은 class가 해당 method에 엑세스 하거나 상속할 수 있는 것을 원하지 않아도, 

그렇게 될 수 있는 것이다.

 

java9 에서는 위와 같은 사항으로 인해 private method와 private static method라는 새로운 기능을 제공해준다.

→코드의 중복을 피하고 interface에 대한 캡슐화를 유지 할 수 있게 되었다.

public interface Car {
    void carMethod();

    default void defaultCarMethod() {
        System.out.println("Default Car Method");

        privateCarMethod();
        privateStaticCarMethod();
    }

    private void privateCarMethod() {
        System.out.println("private car method");
    }

    private static void privateStaticCarMethod() {
        System.out.println("private static car method");
    }
}

DefaultCar.java 클래스 - Car 인터페이스 구현체

public class DefaultCar implements Car{

    @Override
    public void carMethod() {
        System.out.println("car method by DefaultCar");
    }
}
public class Main {
    public static void main(String[] args) {
        DefaultCar car = new DefaultCar();

        car.carMethod();
        car.defaultCarMethod();
    }
}
car method by DefaultCar
Default Car Method
private car method
private static car method

Process finished with exit code 0

 

'Language > Java' 카테고리의 다른 글

람다식  (0) 2022.03.20
[6주차] 상속  (0) 2022.01.30
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10

학습할 것 (필수)

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

1. 자바 상속의 특징

1-1. 상속이란?

상속이란 상위클래스에서 정의한 필드와 메서드를 하위클래스도 동일하게 사용할 수 있게 물려받는 것이다.

1-2. 상속을 사용하는 이유

코드를 재사용하기에 편하고 클래스 간 계층구조를 분류하고 관리하기 쉬워진다.

  • 영웅이 빌런들을 물리치며 세계 평화를 지키는 게임을 만든다고 가정하자.
  • 영웅은 hp, 공격력, 레벨을 가지며 상대방을 공격할 수 있고 레벨만큼 상대방의 공격력을 방어할 수 있다.
  • 빌런은 hp, 공격력 방어력을 가지며 상대방을 공격할 수 있고 단단한 방어력으로 상대방의 공격을 방어력만큼 막을 수 있다.

위 요구사항을 1차적으로 구현한 코드는 다음과 같다.

Unit.java

package study.moon.test;

public class Unit {

    int hp;

    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public void attack(Unit unit) {
        unit.attackedBy(this);
    }

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }

}

Hero.java

package study.moon.test;

public class Hero extends Unit{

    int level;

    public Hero(int hp, int attackPoint, int level) {
        super(hp, attackPoint);
        this.level = level;
    }

}

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

}

위 요구사항을 보면 히어로와 빌런 둘 모두가 공통적으로 포함하고 있는 부분이 있다.

  1. hp를 가지고 있다.
  2. 공격력을 가지고 있다.
  3. 공격을 할 수 있다.
  4. 공격을 당할 수 있다.

따라서 위의 공통적인 부분을 Unit 으로 묶고 히어로와 빌런은 각각 Unit을 상속하고, 나머지를 구현하는것이 코드를 재사용할 수 있는 방법이다.

1-3. 자바 상속의 특징

1-3-1. 다중상속 금지

자바는 다중 상속을 허용하지 않는다. 예를들어 히어로와 빌런간의 이종 교배를 통하여 힐런이라는 종이 탄생했다고 치자. 힐런은 히어로와 빌런의 능력을 모두 이어받아 레벨도 있고 강력한 방어력도 가질 수 있다. 하지만 자바에서는 이러한 다중 상속을 허용하지 않는다.

public class Hellain extends Villain, Hero {
    "컴파일 에러"
}

1-3-2. 최상위 클래스 Object

자바의 모든 클래스는 최상위 클래스 Object의 서브클래스이다.

public class Main {

    public static void main(String[] args) {
        Object hero = new Hero(100,10,3);// Hero 는 Object의 서브클래스
        Object villain = new Villain(150,5,5);//Villain도 Object의 서브클래스
    }

}

2. super 키워드

2-1. super

super키워드를 사용하면 서브클래스가 수퍼클래스에 접근이 가능하다. super는 수퍼클래스의 참조변수라고 볼 수 있다.

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    int hp; // 보통 이렇게 하지 않지만 super를 설명하기 위해 상위클래스에서 사용한 변수명을 다시 사용한다.

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
        this.hp = 1000; // 빌런만 가지고있고 유닛은 없는 hp를 1,000으로 설정한다.
        super.hp = 10000; //유닛이 공통으로 가지고있는 hp를 10,000으로 설정한다.
    }

}

2-2. super()

super()를 사용하면 수퍼클래스의 생성자를 호출할 수 있다.

다시 정상적인 빌런과 유닛의 코드를 보자.

Unit.java

package study.moon.test;

public class Unit {

    int hp;

    int attackPoint;

    public Unit(int hp, int attackPoint) { // 해당 생성자가 빌런에서 super(hp,attackPoint)로 호출된다.
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public void attack(Unit unit) {
        unit.attackedBy(this);
    }

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }

}

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint); //Unit의 생성자를 호출한다.
        this.defensePoint = defensePoint;
    }

}

빌런이 super(hp,attackPoint)를 호출하였다.
즉, 수퍼클래스의 Unit(hp, attackPoint)생성자를 호출하여 초기화했다는 뜻이다.
그러면 이제 빌런은 추가적인 방어력 부분만 의존성으로 받아와서 새롭게 셋팅하면 된다.

!!! 수퍼클래스의 생성자의 인자가 없다면 서브클래스에서 super()를 작성하지 않아도 자동으로 컴파일시에 추가된다.

3. 메소드 오버라이딩

수퍼클래스가 가지고있는 메서드를 서브클래스에서 새롭게 다른 로직으로 정의하고 싶을 때 사용하는 문법이다.
상속관계에 있는 클래스간에 같은 이름의 메서드를 정의하는 문법을 오버라이딩이라고 한다.
오버라이딩 어노테이션은 생략할 수도 있다.

빌런은 공격을 받을때 히어로와는 다르게 방어력의 수치만큼 공격피해가 덜 들어와야 한다.
따라서 유닛이 공통으로 가지고있는 공격받는 로직을 새롭게 작성해주어야 한다.

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

    @Override
    public void attackedBy(Unit unit) {// Unit에 정의되어있는 메서드를 재정의했다.
        this.hp -= (unit.attackPoint - this.defensePoint); //빌런은 일반적인 유닛과는 다르게 방어력만큼 공격이 덜 들어온다.
    }

    @Override
    public String toString() {
        return "Villain{" +
            "hp=" + hp +
            ", attackPoint=" + attackPoint +
            ", defensePoint=" + defensePoint +
            '}';
    }
}

4. 다이나믹 디스패치

컴파일타임에는 알 수 없는 메서드의 의존성을 런타임에 늦게 바인딩 하는것이다.

다음 코드를 보도록 하자.

Main.java

package study.moon.test;

public class Main {

    public static void main(String[] args) {
        Hero hero = new Hero(100,10,3);
        Villain villain = new Villain(150,5,5);
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}

다음 코드는 각각의 객체가 어떤 메서드를 호출할 지 정확하게 예측이 가능하다.
히어로는 Hero의 attack()을 호출 할 것이고
빌런은 Villain의 attack()을 호출 할 것이다.
이것은 모두 컴파일 타임 에 파악할 수 있다.
컴파일된 class파일을 통해 확인해보도록 하자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        10
       8: bipush        30
      10: invokespecial #9                  
      13: astore_1
      14: new           #12                 
      17: dup
      18: sipush        150
      21: iconst_5
      22: iconst_5
      23: invokespecial #14                 
      26: astore_2
      27: aload_1
      28: aload_2
      29: invokevirtual #15         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Unit;)V
      32: aload_2
      33: aload_1
      34: invokevirtual #19         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Unit;)V
      37: getstatic     #20                 
      40: aload_1
      41: invokevirtual #26                 
      44: getstatic     #20                 
      47: aload_2
      48: invokevirtual #26                 
      51: return

장황한 주석들을 삭제하고 두 메서드가 관련된 부분만 살펴보도록 하자.
29행과 34행을 살펴보면 해당 인스턴스는 정확하게 어떤 클래스의 메서드를 호출 할 것인지 명확하게 알 수 있다.
hero는 Hero클래스의 attack을 호출, villain은 Villain클래스의 attack을 호출한다.

하지만 나는 다형성을 적용하여 코드를 조금 더 유연하게 작성하고 싶다.
다음과 같이 코드를 변경해보자

Main.java

package study.moon.test;

public class Main {

    public static void main(String[] args) {
        Unit hero = new Hero(100,10,3);// 타입을 Unit으로 변경하였다.
        Unit villain = new Villain(150,5,5);// 타입을 Unit으로 변경하였다.
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}

이렇게 코드가 변경된다면 과연 어떻게될까?
컴파일된 class 파일을 살펴보자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        10
       8: bipush        30
      10: invokespecial #9                  
      13: astore_1
      14: new           #12                 
      17: dup
      18: sipush        150
      21: iconst_5
      22: iconst_5
      23: invokespecial #14                 
      26: astore_2
      27: aload_1
      28: aload_2
      29: invokevirtual #15         // Method study/moon/test/Unit.attack:(Lstudy/moon/test/Unit;)V
      32: aload_2
      33: aload_1
      34: invokevirtual #15         // Method study/moon/test/Unit.attack:(Lstudy/moon/test/Unit;)V
      37: getstatic     #21                 
      40: aload_1
      41: invokevirtual #27                 
      44: getstatic     #21                 
      47: aload_2
      48: invokevirtual #27                 
      51: return

이번에도 딱 두 부분의 메서드가 컴파일 타임에 어떻게 바인딩되어있는지 확인해보도록 하자
29행과 34행을 보면 해당 메서드는 Unit의 attack을 사용하기로 결정되어있다.
그렇다면 빌런과 히어로 모두 유닛의 attack을 사용하는것일까??
그렇지 않다. 다음 결과물을 보도록 하자.

Main.java

public class Main {

    public static void main(String[] args) {
        Unit hero = new Hero(100,10,3);// 타입을 Unit으로 변경하였다.
        Unit villain = new Villain(150,5,5);// 타입을 Unit으로 변경하였다.
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}
Hero{hp=98, attackPoint=10, level=3}
Villain{hp=145, attackPoint=5, defensePoint=5} // 유닛의 메서드가아닌 빌런의 메서드가 호출된 결과이다.

위의 main 함수의 결과물이다.

빌런이 일반 유닛이라면 방어력의 영향을 받지 않고 히어로의 공격 10을 온전히 받아냈어야했다.
그러나 오버라이딩 된 빌런의 계산을 따랐고 그 결과 히어로의 공격력 10에서 방어력 5를 뺀 데미지만 입게 되었다.
히어로도 레벨의 영향을 받아 레벨만큼 공격을 덜받았다.

이처럼 컴파일 타임에는 메서드의 클래스타입이 정해져있지 않지만 런타임에 정해져서 메서드를 호출하는 것을
동적 dispatch 라고 한다.

많은 블로그 글을 둘러보면서 확인한 결과, 대부분의 블로그의 글들이 구현체가 없는 인터페이스, 혹은 추상클래스의 메서드를 호출할 때 클래스 타입에 따라 동적으로 메서드가 결정되는것이라고 설명해놓았다. 그런데 해 본 결과 굳이 추상클래스나 인터페이스가 아니어도 동적 디스패치는 발생할 수 있다. 수퍼클래스의 메서드를 오버라이딩해도 동적 디스패치가 가능하다.

5. 더블 디스패치

  • 히어로는 레벨이 올라 수퍼 히어로,하이퍼 히어로로 업그레이드할 수 있게 하고싶다.
  • 빌런도 일정 시간이 지나면 수퍼빌런, 하이퍼 빌런으로 업그레이드할 수 있게 만들고 싶다.
  • 그에 맞춰서 각각 업그레이드 한 상태에서 상대방을 공격할 때의 로직을 if문을 사용하지 않고 유기적으로 작성해주고싶다.
  • 어떻게 작성해야 할까?

ex)
수퍼히어로 -> 빌런
하이퍼히어로 -> 빌런
수퍼빌런 -> 히어로
하이퍼빌런 -> 히어로
모두 다른 로직을 작성하고싶다!

코드를 다음과 같이 수정해보자. 그러면 if문은 사용하지 않고도 분기처리를 진행할 수 있다.

주석에 적힌 부분을 따라가면 2번 동적 디스패치가 발생했다는 사실을 알 수 있다.

Unit.java

public abstract class Unit {
    int hp;
    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

}

Hero.java

public abstract class Hero extends Unit {

    int level;

    public Hero(int hp, int attackPoint, int level) {
        super(hp, attackPoint);
        this.level = level;
    }

    //1. goto  hyperHero.attack()  or  superHero.attack()
    public abstract void attack(Villain villain);  

    public void isAttackedBy(SuperVillain superVillain) {
        hp -= (superVillain.attackPoint * 2 - level);
    }

    public void isAttackedBy(HyperVillain hyperVillain) {
        hp -= (hyperVillain.attackPoint);
    }
}

HyperHero.java

public class HyperHero extends Hero {

    public HyperHero(int hp, int attackPoint, int level) {
        super(hp, attackPoint, level);
    }

    //2. goto villain.isAttackedBy(superHero)  or  villain.isAttackedBy(hyperHero)
    @Override
    public void attack(Villain villain) {
        villain.isAttackedBy(this);
    }

}

SuperHero.java

public class SuperHero extends Hero{

    public SuperHero(int hp, int attackPoint, int level) {
        super(hp, attackPoint, level);
    }

    //2. goto villain.isAttackedBy(superHero)  or  villain.isAttackedBy(hyperHero)
    @Override
    public void attack(Villain villain) {
        villain.isAttackedBy(this);
    }
}

Villain.java

public abstract class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

    //1. goto  hyperVillain.attack()  or  superVillain.attack() 
    public abstract void attack(Hero hero);

    public void isAttackedBy(SuperHero superHero) {
        hp -= (superHero.attackPoint + superHero.level - defensePoint);
    }

    public void isAttackedBy(HyperHero hyperHero) {
        hp -= (hyperHero.level * 3);
    }
}

HyperVillain.java

public class HyperVillain extends Villain {

    public HyperVillain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint, defensePoint);
    }

    //2. goto hero.isAttackedBy(superVillain)  or  hero.isAttackedBy(hyperVillain)
    @Override
    public void attack(Hero hero) {
        hero.isAttackedBy(this);
    }
}

SuperVillain.java

public class SuperVillain extends Villain{

    public SuperVillain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint, defensePoint);
    }

    //2. goto hero.isAttackedBy(superVillain)  or  hero.isAttackedBy(hyperVillain)
    @Override
    public void attack(Hero hero) {
        hero.isAttackedBy(this);
    }

}

Main.java

public class Main {

    public static void main(String[] args) {
        Hero superHero = new SuperHero(100,20,5);
        Hero hyperHero = new HyperHero(100,5,10);
        Villain superVillain = new SuperVillain(200,10,5);
        Villain hyperVillain = new HyperVillain(150,15,10);
        superHero.attack(superVillain);
        superHero.attack(hyperVillain);
        hyperHero.attack(superVillain);
        hyperHero.attack(hyperVillain);
        superVillain.attack(superHero);
        superVillain.attack(hyperHero);
        hyperVillain.attack(superHero);
        hyperVillain.attack(hyperHero);

        System.out.println(superHero);
        System.out.println(hyperHero);
        System.out.println(superVillain);
        System.out.println(hyperVillain);
    }
}

컴파일 된 클래스 파일을 확인하면서 어떻게 동적 디스패치가 일어났는지 확인해보자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        20
       8: iconst_5
       9: invokespecial #9                  
      12: astore_1
      13: new           #12                 
      16: dup
      17: bipush        100
      19: iconst_5
      20: bipush        10
      22: invokespecial #14                 
      25: astore_2
      26: new           #15                 
      29: dup
      30: sipush        200
      33: bipush        10
      35: iconst_5
      36: invokespecial #17                 
      39: astore_3
      40: new           #18                 
      43: dup
      44: sipush        150
      47: bipush        15
      49: bipush        10
      51: invokespecial #20                 
      54: astore        4
      56: aload_1
      57: aload_3
      58: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      61: aload_1
      62: aload         4
      64: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      67: aload_2
      68: aload_3
      69: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      72: aload_2
      73: aload         4
      75: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      78: aload_3
      79: aload_1
      80: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      83: aload_3
      84: aload_2
      85: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      88: aload         4
      90: aload_1
      91: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      94: aload         4
      96: aload_2
      97: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
     100: getstatic     #32                 
     103: aload_1
     104: invokevirtual #38                 
     107: getstatic     #32                 
     110: aload_2
     111: invokevirtual #38                
     114: getstatic     #32                 
     117: aload_3
     118: invokevirtual #38                 
     121: getstatic     #32                 
     124: aload         4
     126: invokevirtual #38                 
     129: return
}

다음과 같은 방식으로 코드를 작성하면 조금 더 유연한 코드가 된다. 업그레이드 된 히어로 빌런이 추가된다면 해당 부분을 추가해서 넣어주기만 하면 된다. 디자인 패턴으로는 다음과 같은 패턴을 방문자 패턴이라고 부른다고 한다.

6. 추상클래스

구체적이지 않은 클래스를 말한다. 예를들어 구체적인 클래스가 히어로, 빌런이라면 추상적인 클래스는 유닛이 될 수 있다.
공통된 부분으로 묶기에는 적당하지만 구현을 하지는 않을 클래스를 만들 때 추상클래스를 이용한다.

public abstract class Unit {
    int hp;
    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public abstract void attack(Unit unit);

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }
}
  • 클래스 앞에 abstract 키워드를 이용하면 해당 클래스는 추상클래스가 된다.
  • 추상클래스는 추상메서드를 작성할 수 있다. (추상메서드란, 구현부가 없는 메서드이다.)
  • 추상메서드는 메서드의 반환형 앞에 absract를 붙이면 된다.
  • 추상클래스는 인스턴스를 생성할 수 없다.
  • 추상 클래스를 상속받은 클래스는 수퍼클래스가 가지고있는 추상메서드를구현하지 않으면 추상클래스가 된다.

7. final

final은 다시 무언가를 정의내리는것을 막는 키워드이다.

  • class
    • 클래스의 상속을 막는다.
  • variable
    • 변수의 재할당을 막는다.
  • method
    • 메서드의 오버라이딩을 막는다.

많은 사람들이 오해하고 있는 부분중에 하나가 바로 변수에 final을 사용하면 불변한다는 것이다.
그러나 대상은 사실 변할 수 있다.예제를 살펴보자.

import java.util.ArrayList;
import java.util.List;

class Main {

    public static void main(String[] args) {
        final List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        System.out.println(list); // [1, 2, 3, 4, 5]
    }
}

list 내부의 값이 0개에서 5개로 증가했다.
이처럼 final에서의 불변은 대상이 불변하는것이 아니라 새롭게 할당하는것을 막는다는것을 의미한다.

그렇다면 final 키워드는 왜 사용할까?

  • 우리의 기억력이 완벽하지 않고, 우리 코드의 의도가 분명하지 않기때문에 사용한다.
  • 우리는 항상 실수하기 때문에 final을 사용하여 미리 실수를 차단할 수 있는 방안은 모두 사용해야한다.

8. Object

  • 자바의 최상위 클래스이다.
  • 따로 어디서 상속받지 않더라도 Obejct는 모든 클래스의 최상위 클래스이기 때문에 내가 클래스를 생성하면 그 클래스에는 자동으로 object의 기본메서드가 포함되어있다.

Object 클래스의 메서드

http://www.tcpschool.com/java/java_api_object

'Language > Java' 카테고리의 다른 글

람다식  (0) 2022.03.20
[8주차] 인터페이스  (0) 2022.02.06
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10

클래스와 객체

클래스는 객체지향 프로그래밍(Object-oriented programming)에서 객체를 생성하기 위해 상태(state)와 행동(behavior)을 정의하는 일종의 설계도이다. 여기서 객체란 어플리케이션의 기능을 구현하기 위해 서로 협력하는 개별적인 실체로써 물리적일 수도 있고 개념적일 수도 있다. 앞으로 배울 객체지향의 4대 특성(추상화, 캡슐화, 상속, 다형성)을 통해 프로그램 개발 및 유지보수를 더욱 쉽고 빠르게 할 수 있을 것이다.

클래스 정의

객체의 상태와 행동이 정의된 하나의 클래스로 비슷한 구조를 갖되 상태는 서로 다른 여러 객체를 만들 수 있다. 그렇다면 어떻게 정의해야 할까? 먼저 클래스의 구조를 살펴보자.

  • 필드(field) - 필드는 해당 클래스 객체의 상태 속성을 나타내며, 멤버 변수라고도 불린다. 여기서 초기화하는 것을 필드 초기화 또는 명시적 초기화라고 한다.
    • 인스턴스 변수 - 이름에서 알 수 있듯이 인스턴스가 갖는 변수이다. 그렇기에 인스턴스를 생성할 때 만들어진다. 서로 독립적인 값을 갖으므로 heap 영역에 할당되고 gc에 의해 관리된다.
    • 클래스 변수 - 정적을 의미하는 static키워드가 인스턴스 변수 앞에 붙으면 클래스 변수이다. 해당 클래스에서 파생된 모든 인스턴스는 이 변수를 공유한다. 그렇기 때문에 heap 영역이 아닌 static 영역에 할당되고 gc의 관리를 받지 않는다. 또한 public 키워드까지 앞에 붙이면 전역 변수라 볼 수 있다.
  • 메서드(method) - 메서드는 해당 객체의 행동을 나타내며, 보통 필드의 값을 조정하는데 쓰인다.
    • 인스턴스 메서드 - 인스턴스 변수와 연관된 작업을 하는 메서드이다. 인스턴스를 통해 호출할 수 있으므로 반드시 먼저 인스턴스를 생성해야 한다.
    • 클래스 메서드 - 정적 메서드라고도 한다. 일반적으로 인스턴스와 관계없는 메서드를 클래스 메서드로 정의한다.
  • 생성자(constructor) - 생성자는 객체가 생성된 직후에 클래스의 객체를 초기화하는 데 사용되는 코드 블록이다. 메서드와 달리 리턴 타입이 없으며, 클래스엔 최소 한 개 이상의 생성자가 존재한다.
  • 초기화 블록(initializer) - 초기화 블록 내에서는 조건문, 반복문 등을 사용해 명시적 초기화에선 불가능한 초기화를 수행할 수 있다.
    • 클래스 초기화 블록 - 클래스 변수 초기화에 쓰인다.
    • 인스턴스 초기화 블록 - 인스턴스 변수 초기화에 쓰인다.
    • 클래스 변수 초기화: 기본값 → 명시적 초기화 → 클래스 초기화 블록
      인스턴스 변수 초기화: 기본값 → 명시적 초기화 → 인스턴스 초기화 블록 → 생성자

위의 구조를 토대로 클래스를 정의한다면 다음과 같이 코드를 작성할 수 있다.

class Class {               // 클래스
    String constructor;
    String instanceVar;     // 인스턴스 변수
    static String classVar; // 클래스 변수

    static {                // 클래스 초기화 블록
        classVar = "Class Variable";
    }

    {                       // 인스턴스 초기화 블록
        instanceVar = "Instance Variable";
    }

    Class() {                // 생성자
        constructor = "Constructor";
    }

    void instanceMethod() {       // 인스턴스 메서드
        System.out.println(instanceVar);
    }

    static void classMethod() {   // 클래스 메서드
        System.out.println(classVar);
    }
}

접근 제어자 - public, protected, default, private
그 외 - static, final, abstract, transient, synchronized, volatile etc.

static이나 public같은 키워드를 제어자(modifier)라고 하며, 클래스나 멤버 선언 시 부가적인 의미를 부여한다.

  • 접근 제어자 - 접근 제어자는 해당 클래스 또는 멤버를 정해진 범위에서만 접근할 수 있도록 통제하는 역할을 한다. 클래스는 public과 default밖에 쓸 수 없다. 범위는 다음과 같다. 참고로 default는 아무것도 덧붙이지 않았을 때를 의미한다.
  • static - 변수, 메서드는 객체가 아닌 클래스에 속한다.
  • final
    • 클래스 앞에 붙으면 해당 클래스는 상속될 수 없다.
    • 변수 또는 메서드 앞에 붙으면 수정되거나 오버라이딩 될 수 없다.
  • abstract
    • 클래스 앞에 붙으면 추상 클래스가 되어 객체 생성이 불가하고, 접근을 위해선 상속받아야 한다.
    • 변수 앞에 지정할 수 없다. 메서드 앞에 붙는 경우는 오직 추상 클래스 내에서의 메서드밖에 없으며 해당 메서드는 선언부만 존재하고 구현부는 상속한 클래스 내 메서드에 의해 구현되어야 한다. 상속과 관련된 내용은 6주차에 다룰 예정이다.
  • transient - 변수 또는 메서드가 포함된 객체를 직렬화할 때 해당 내용은 무시된다.
  • synchronized - 메서드는 한 번에 하나의 쓰레드에 의해서만 접근 가능하다.
  • volatile - 해당 변수의 조작에 CPU 캐시가 쓰이지 않고 항상 메인 메모리로부터 읽힌다.

객체 생성

클래스에서 객체를 생성하려면 아래와 같이 new키워드를 생성자 중 하나와 함께 사용하면 된다.

public class Point {
    private int x = 1;
    private int y = 2;

    Point() {}  // 기본 생성자

    Point(int x, int y) {
        setX(x);
        setY(y);
    }

    public int getX() {
        return x;
    }

    public int getY() {

        return y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }
}
public class PointMain {
    public static void main(String[] args) {
        Point p1 = new Point();
        Point p2 = new Point(3, 4);
        System.out.println("" + p1.getX() + ", " + p1.getY());  // 1, 2
        System.out.println("" + p2.getX() + ", " + p2.getY());  // 3, 4
        p1.setX(3);
        p1.setY(4);
        System.out.println("" + p1.getX() + ", " + p1.getY());  // 3, 4
    }
}

new키워드는 새 객체에 메모리를 할당하고 해당 메모리에 대한 참조값을 반환하여 클래스를 인스턴스화한다. 일반적으로 객체가 메모리에 할당되면 인스턴스라 부른다. 참고로 인스턴스 p1과 p2은 서로 다른 생성자에 의해 생성되었다.

메서드 정의

public int getX() {
    return x;
}

public int getY() {
    return y;
}

public void setX(int x) {
    this.x = x;
}

public void setY(int y) {
    this.y = y;
}

  • 접근 제어자 및 기타 제어자 - 해당 메서드에 접근할 수 있는 범위를 명시하거나 위에서 언급했듯이 부가적인 의미를 부여한다.
  • 반환 타입 - 메서드가 모든 작업을 수행한 뒤에 반환할 타입을 명시한다.
  • 메서드 이름 - 메서드명은 동사여야 하고 lowerCamelCase를 따르며 뜻이 명확해야 한다. 위의 메서드는 getter/setter 메서드이다.
  • 매개변수 리스트 - 메서드에서 사용할 매개변수들을 명시한다.
  • 메서드 시그니처 - 컴파일러는 메서드 시그니처를 보고 오버로딩(overloading)을 구별한다. 물론 리스트의 순서도 동일해야 한다.

생성자 정의

Point() {}  // 기본 생성자

Point(int x, int y) {
    setX(x);
    setY(y);
}

앞서 말했듯이 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 생성한다. 하지만 기본 생성자가 아닌 다른 형태의 생성자만 명시했다면 기본 생성자는 컴파일시에 생성되지 않는다.

this 키워드

public void setX(int x) {
    this.x = x;
}

public void setY(int y) {
    this.y = y;
}

this키워드는 인스턴스 자신을 가르킨다. 위 코드에서 this를 사용함으로써 지역변수 x, y와 구분할 수 있다. 당연한 말이지만 클래스 메서드에서는 this를 쓸 수 없다. 왜냐하면 인스턴스가 생성되지 않았을 수도 있기 때문이다.

this()는 해당 클래스 생성자를 호출할 수 있다. 그렇기 때문에 생성자를 재사용하는 데 쓰인다. (생성자 체이닝)

public class Point {
    int x;
    int y;
    int z;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    Point(int x, int y, int z) {
        this(x, y);
        this.z = z;
    }
}

과제

  • int 값을 가지고 있는 이진 트리를 나타내는 Node 라는 클래스를 정의하세요.
  • int value, Node left, right를 가지고 있어야 합니다.
public class Node {
    private int value;
    private Node left;
    private Node right;

    Node(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }

    public Node addLeftNode(int value) {
        Node node = new Node(value);
        setLeft(node);
        return node;
    }

    public Node addRightNode(int value) {
        Node node = new Node(value);
        setRight(node);
        return node;
    }

    public int getValue() {
        return value;
    }

    public Node getLeft() {
        return left;
    }

    public Node getRight() {
        return right;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public void setRight(Node right) {
        this.right = right;
    }
}
  • BinrayTree라는 클래스를 정의하고 주어진 노드를 기준으로 출력하는 bfs(Node node)와 dfs(Node node) 메소드를 구현하세요.
  • DFS는 왼쪽, 루트, 오른쪽 순으로 순회하세요.
import java.util.*;

public class BinaryTree {
    public List<Integer> bfsList = new ArrayList<>();
    public List<Integer> dfsList = new ArrayList<>();

    public void bfs(Node node) {
        Queue<Node> queue = new LinkedList<>();
        queue.offer(node);
        while (!queue.isEmpty()) {
            Node n = queue.poll();
            bfsList.add(n.getValue());
            if (n.getLeft() != null) {
                queue.offer(n.getLeft());
            }
            if (n.getRight() != null) {
                queue.offer(n.getRight());
            }
        }
    }

    public void dfs(Node node) {
        if (node == null) return;
        if (node.getLeft() != null) {
            dfs(node.getLeft());
        }
        dfsList.add(node.getValue());
        if (node.getRight() != null) {
            dfs(node.getRight());
        }
    }
}

package week5.binarytree;

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

class BinaryTreeTest {
    static BinaryTree tree;
    static Node root;

    @BeforeAll
    static void createBinaryTree() {
        tree = new BinaryTree();
        root = new Node(1);

        Node temp = root.addLeftNode(2);
        temp.addRightNode(6);
        temp = temp.addLeftNode(3);
        temp.addLeftNode(4);
        temp.addRightNode(5);

        temp = root.addRightNode(7);
        temp.addLeftNode(8);
        temp.addRightNode(9);
    }

    @Test
    void bfs() {
        tree.bfs(root);
        List<Integer> answer = Arrays.asList(1, 2, 7, 3, 6, 8, 9, 4, 5);
        assertArrayEquals(answer.toArray(), tree.bfsList.toArray());
    }

    @Test
    void dfs() {
        tree.dfs(root);
        List<Integer> answer = Arrays.asList(4, 3, 5, 2, 6, 1, 8, 7, 9);
        assertArrayEquals(answer.toArray(), tree.dfsList.toArray());
    }
}

package 키워드

패키지란

자바에서 Package는 클래스, 서브 패키지 그리고 인터페이스로 이루어진 그룹을 캡슐화하는 매커니즘을 말한다. 그리고 다음과 같은 경우를 위해 사용된다:

  1. 이름 충돌 방지(Preventing naming conflicts)
  2. classes, interfaces, enumerations and annotations를 더 쉽게 찾고, 사용하기 위해
  3. 접근 제어
  4. 데이터 캡슐화로 간주되기도 한다.

패키지가 동작하는 방식

패키지는 directory structure와 밀접한 관련이 있다. 예를 들어 college.staff.cse라는 패키지 명이 있다면, 3개의 디렉토리(college, staff, cse)가 존재하며 cse는 staff안에 staff는 college 디렉토리 안에 위치한다.

또한, college 디렉토리는 CLASSPATH 변수를 통해서 접근할 수 있다.

패키지 네이밍 관습

domain name의 역순으로 명명된다. (i.e, org.geeksforgeeks.practice, college.tech.cse, college.tech.ee)

패키지에 클래스 추가하기

  1. 패키지 안에 클래스를 생성한다.
  2. 클래스를 정의하는 프로그램 코드 맨 윗줄에 패키지 이름을 선언한다.
/* dog.java */
package Animal

public class Dog {
    ...
}

서브 패키지

패키지 안에서 사용되는 또 다른 패키지를 말하며 import 선언을 통해 사용할 수 있다. 사윙 패키지가 서브 패키지의 멤버에 접근할 수 있는 특별한 권한은 따로 존재하지 않는다.

import java.util.*;

패키지의 종류

  1. Built-in Package
  2. User-defined Package

Built-in Package

수 많은 패키지가 기본적으로 자바에서 제공되며 그중 자주 쓰이는 몇가지의 용도를 정리해볼 수 있다.

  1. java.lang: 언어의 기본적인 클래스를 제공하기 때문에 항상 자동으로 import되는 패키지이다. (e.g. 데이터 원시 타입, 수학 연산 등을 포함하는 클래스)
  2. java.io: Input/Output을 관리하는 클래스를 포함하는 패키지
  3. java.util: 자료구조나 시간과 같은 유틸리티 클래스를 포함하는 패키지

User-defined Package

말 그래도 유저에 의해 정의된 패키지를 말한다.

import 키워드

서브 클래스 내부로 접근하기

// java안에 있는 util 패키지 내에 모든 클래스에 접근하기
import java.util.*;

// java 안에 있는 util 패키지 중 Vector 클래스에 접근하기
import java.util.Vector;

패키지를 정의하고 사용하기

  1. 처음 만들어진, 최상위에 위치하는 디렉토리의 이름은 패키지 이름과 동일해야한다.
  2. 그 아래 생성된 디렉토리와 클래스에는 .으로 구분하며 접근할 수 있다.
 `- example
    |- MyClass.java
    `- PrintName.java
/* MyClass.java */
package example;

public class MyClass {
    public void getNames(String s) {
        System.out.println(s);
    }
}

/* PrintName.java */
package main.java.example;

import example.MyClass;

public class PrintName 
{
   public static void main(String args[]) 
   {       
      String name = "GeeksforGeeks";
      MyClass obj = new MyClass();
      
      obj.getNames(name);
   }
}

static import

자바 버전 5 이후부터 소개된 기능이다. 한 클래스에 정의된 멤버들(fields와 methods)를 클래스를 특정하지 않고 public static으로 자바 코드 안에서 사용될 수 있게 만들어주는 기능을 말한다.

import static java.lang.System.*;

class StaticImportDemo {
    public static void main(String args[]) {
        out.println("Hello world");
    }
}

이름 충돌 다루기

예를 들어 상황을 살펴보자. java.util과 java.sql 패키지는 둘다 Date라는 이름의 클래스를 포함한다. 따라서 다음과 같은 코드는 컴파일 에러를 발생시킨다.

import java.util.*;
import java.sql.*;

Date today; // java.util과 java.sql 패키지 모두 
            // Date라는 클래스를 포함하기에 
            // 컴파일러는 어느것을 지칭하는지를 구분할 수 없다.

한 쪽에서만 Date 클래스를 사용해야 한다면 다음과 같은 코딩이 솔루션이 될 수 있다.

import java.util.Date
import java.sql.*;

하지만 양 쪽에서 써야한다면 변수 선언 시에 풀 패키지 명으로 선언해줘야 한다.

java.util.Date deadLine = new java.util.Date();
java.sql.Date today = new java.sql.Date();

Packages in Java - GeeksforGeeks

클래스패스

그림 출처 - 알짜배기 프로그래머 블로그

위 그림에서 클래스 로더는 JVM에 바이트 코드(.class)를 불러오는 역할을 한다. 그렇다면 클래스 로더는 어떻게 바이트 코드의 위치를 알 수 있었을까?

JVM의 클래스 로더는 JVM이 시작될 때 CLASSPATH 환경 변수를 호출하여 CLASSPATH 환경 변수에 설정되어 있는 디렉토리를 파악할 수 있었다.

그렇다면 클래스패스는 무엇인가?

클래스패스는 클래스가 위치하고 있는 경로를 말한다. 더 명확하게 말해보자면 JVM이 프로그램을 실행할 때, 클래스 파일을 찾는데 기준이 되는 파일 경로를 말한다.

기본적으로는 java 명령어가 실행된 위치를 default class path로 갖는다. 하지만 CLASSPATH 환경 변수 설정이나 커맨드 라인에서 java 명령어에 -classpath나 -cp 명령을 이용해 설정할 수 있다.

위 그림에서 볼 수 있듯이

  1. Java Source code(.java)를 컴파일하면
  2. Byte Code(.class) 형태로 변환된다.
  3. 이 클래스 파일(Byte Code)에 포함된 명령을 사용하려면 해당 파일을 찾을 수 있어야 한다.
  4. 그 경로를 찾기 위해 CLASSPATH 환경 변수에 저장된 클래스패스를 사용한다.

자바 클래스패스(classpath)란? - 코딩하는 오징어 블로그

CLASSPATH 환경변수

CLASSPATH 환경 변수는 앞에서도 언급했듯이 .class 파일을 찾기 위한 경로가 저장되어 있는 환경 변수를 말한다. 더 정확히는 .class 파일이 포함된 디렉토리와 파일을 세미콜론(;)으로 구분한 목록이다.

  1. java runtime은 이 CLASSPATH 환경 변수에 저장된 경로를 모두 검색해서 특정 클래스에 대한 코드가 포함된 .class 파일을 찾는다.
  2. 찾으려는 클래스 코드가 포함된 .class 파일을 찾으면 해당 파일을 사용한다.

Windows에서 CLASSPATH 환경 변수 설정하기

-classpath 옵션

커맨드 라인에서 java 명령어를 실행할 때 -classpath나 -cp를 붙여 클래스패스를 특정할 수 있다. 지금부터는 그 방법과 커맨드 라인에서 CLASSPATH를 설정할 수 있는 또 하나의 방법을 소개하겠다.

Use -classpath or -cp

C:> sdkTool -classpath classpath1;classpath2...

Use set CLASSPATH

C:> set CLASSPATH=classpath1;classpath2...
  1. sdkTool
    • java, javac, javadoc과 같은 커맨드 라인 툴이 해당한다.
  2. classpath1;classpath2
    • .jar, .zip, .class 파일이 위치하는 클래스패스를 뜻한다.
    • 각 클래스패스는 파일 이름이나 디렉토리 이름으로 끝나야한다.
  3. 다양한 클래스패스를 갖는다면 세미콜론(;)으로 구분한다.
  4. default class path는 현재 디렉토리를 가리킨다.
    • 다만, 다른 클래스 패스를 추가하려고 -classpath 옵션이나 set을 사용한다면 .을 맨 앞에 붙여 현재 위치를 클래스패스 목록에 추가할 수 있다.
  5. 디렉토리, 아카이브(.zip or .jar files)나 *이 아닌 클래스패스는 무시된다.

Setting the class path - Oracle Java SE Document

접근지시자

자바에서는 클래스, 인터페이스나 멤버에 대한 접근을 제어할 수 있는 접근 지시자(Access Modifier)라는 것을 제공한다. Effective Java에서는 Access Control Mechanism으로 표현하고 있다.

선언 위치에 따라 위에서 언급한 항목들에 대해 접근성이 정해진다는 것이다.

top-level(non-nested) classes and interfaces, there are only two possible access levels:

  • package-private
  • public

For members (fields, methods, nested classes, and nested interfaces), there are four possible access levels, listed here in order of increasing accessibility:

  • private: 해당 멤버가 선언된 클래스(top-level class) 안에서만 접근할 수 있다.
  • package-private: 해당 멤버가 선언된 패키지 안에서만 접근할 수 있다.
  • protected:
    • 해당 멤버가 선언된 클래스의 자식 클래스에서 접근할 수 있다.
    • 해당 멤버가 선언된 패키지 내의 어느 클래스에서든 접근할 수 있다.
  • public: 어디서든 접근이 가능하다.

'Language > Java' 카테고리의 다른 글

[8주차] 인터페이스  (0) 2022.02.06
[6주차] 상속  (0) 2022.01.30
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10
[2주차] 자바 데이터 타입, 변수 그리고 배열  (0) 2022.01.02

과제 0. JUnit 5 학습하세요.

  • 인텔리J, 이클립스, VS Code에서 JUnit 5로 테스트 코드 작성하는 방법에 익숙해 질 것.
  • 이미 JUnit 알고 계신분들은 다른 것 아무거나!
  • 더 자바, 테스트 강의도 있으니 참고하세요~

JUnit5의 Life-Cycle

  • @BeforeAll : @Test 메소드들이 실행되기 전에 실행
  • @BeforeEach : 각각의 @Test 메소드가 실행되기 전에 실행
  • @AfterEach : 각각의 @Test 메소드가 실행된 후에 실행
  • @AfterAll : @Test 메소드들이 실행된 후에 실행
import org.junit.jupiter.api.*;

public class JUnit5Test {
    @BeforeAll
    static void beforeAll() {
        System.out.println("BeforeAll Test");
    }
    @BeforeEach
    static void beforeEach(){
        System.out.println("BeforeEach");
    }

    @Test
    @DisplayName("테스트 1☆")
    static void testing() {
        System.out.println("testing");
    }

    @AfterEach
    static void afterEach() {
        System.out.println("AfterEach");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("AfterAll");
    }

JUnit5 Feature

  • @DisplayName : 한글, 스페이스,이모지를 통해 테스트 이름의 표기가능
  • @Nested : 계층 구조 테스트가 가능하게 지원
  • @ParameterizedTest : 여러개의 테스트 데이터를 매개변수 형태로 간편하게 사용 가능, NullSource, ValueSource, EmptySource, CsvSource, EnumSource, MethodSource등 최소 하나의 소스 어노테이션이 필요
//DisplayName을 이용한 테스트 이름 작성
@Test
@DisplayName("테스트 1☆")
static void testing() {
	System.out.println("testing");
}

//Nested를 이용한 계층 구조 테스트
@Nested
@DisplayName("people")
class People {
	@Nested
  @DisplayName("man")
  class Man {
	  @Test
    static void manTest() {
	    System.out.println("man");
    }
  }
	@Nested
  @DisplayName("woman")
  class Woman {
	  @Test
    static void womanTest() {
	    System.out.println("woman");
    }
  }
}

//ParameterizedTest를 이용한 매개변수이용 
@ParameterizedTest
@ValueSource(ints = {1,2,3,4,5})
static void isOdd(int num){
	assertTrue(Numbers.isOdd(num));
}

JUnit Assert

기존 JUnit4는 assert 메소드가 하나라도 실패하면 다음 assert를 실행하지 않았다. 하지만 JUnit5는 assertAll이라는 메소드를 통해 여러개의 assert를 실행하게 하여 실패하더라도 모든 결과를 확인할 수 있게 지원하였다.

@Test
static assertAllTest() {
	int num = 10;
	assertAll("assertAll test",
				  () -> assertEquals(10,num),
          () -> assertEquals(13,num+5),
          () -> assertEquals(15,num+5)
	);
}

또한, JUnit4의 경우 라이브러리를 이용해 예외 검증이 가능했다면, JUnit5는 assertThrows를 이용해 예외 검증이 가능하게 되었다.

@Test
static void assertThrowsTest() {
	Exception exception = assertThrows(
				  IllegalArgumentException.class, () -> {
					    throw new IllegalArgumentException("a message");
					}
	);
  assertEquals("message",exception.getMessage());
}

그리고, 어노테이션을 이용해 테스트 실행시간을 확인한것에 반해 assertTimeout을 이용해 테스트 실행시간에 대한 검증이 가능하게 되었다.

@Test
static void assertTimeoutTest() {
	assertTimeout(ofSeconds(1), () -> {
	  // 1초 이내에 수행해야함
  });
}

과제 1. live-study 대시 보드를 만드는 코드를 작성하세요.

  • 깃헙 이슈 1번부터 18번까지 댓글을 순회하며 댓글을 남긴 사용자를 체크 할 것.
  • 참여율을 계산하세요. 총 18회에 중에 몇 %를 참여했는지 소숫점 두자리가지 보여줄 것.
  • Github 자바 라이브러리를 사용하면 편리합니다.
  • 깃헙 API를 익명으로 호출하는데 제한이 있기 때문에 본인의 깃헙 프로젝트에 이슈를 만들고 테스트를 하시면 더 자주 테스트할 수 있습니다.
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.*;

public class GitHubIssue {
    //personal token need to secret
    private static final String MY_PERSONAL_TOKEN = "MY_SECRET_TOKEN";

    public static void main(String[] args) throws IOException {
        GitHub github = new GitHubBuilder().withOAuthToken(MY_PERSONAL_TOKEN).build();

        //Repository 연결
        GHRepository repo = github.getRepository("whiteship/live-study");

        //IssueState ALL, OPEN, CLOSED
        List<GHIssue> issues = repo.getIssues(GHIssueState.ALL);
        Map<String, Integer> participant = new HashMap<>();

        //1-18개 이슈
        for (GHIssue issue : issues) {
            Set<String> onlyOneParticipant = new HashSet<>();

            //댓글 한개 이상 단 경우 유저이름 중복 제거
            for (GHIssueComment comment : issue.getComments()) {
                onlyOneParticipant.add(comment.getUser().getName());
            }

            //카운트 증가해주기
            for (String name : onlyOneParticipant) {
                if(participant.containsKey(name)){
                    participant.replace(name,participant.get(name)+1);
                    continue;
                }
                participant.put(name,1);
            }
        }
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        //참여율 출력
        for(String name : participant.keySet()){
            double rate = (double)(participant.get(name) * 100) / issues.size();
            bw.write("name : " + name);
            bw.write(", Participation Rate : "+String.format("%.2f",rate)+"%");
            bw.newLine();
        }
        bw.close();
    }

}

 

과제 2. LinkedList를 구현하세요.

  • LinkedList에 대해 공부하세요.
  • 정수를 저장하는 ListNode 클래스를 구현하세요.
  • ListNode add(ListNode head, ListNode nodeToAdd, int position)를 구현하세요.
  • ListNode remove(ListNode head, int positionToRemove)를 구현하세요.
  • boolean contains(ListNode head, ListNode nodeTocheck)를 구현하세요.

LinkedList Interface

public interface LinkedList {

    // add remove contains
    ListNode add(ListNode head, ListNode nodeToAdd, int position);
    ListNode remove(ListNode head, int positionToRemove);
    boolean contains(ListNode head, ListNode nodeToCheck);
}

ListNode.java

public class ListNode implements LinkedList {
    int data;
    ListNode next;

    public ListNode() {}
    public ListNode(int data) {
        this.data = data;
        this.next = null;
    }

    @Override
    public ListNode add(ListNode head, ListNode nodeToAdd, int position) {
        ListNode node = head;

        //position 이전까지 탐색
        for (int i = 0; i < position - 1; i++) {
            node = node.next;
        }

        //지정 위치에 노드 삽입
        nodeToAdd.next = node.next;
        node.next = nodeToAdd;
        return head;
    }

    @Override
    public ListNode remove(ListNode head, int positionToRemove) {
        ListNode node = head;
				
				//삭제 위치가 가장 앞인경우 
        if(positionToRemove == 0){
            ListNode deleteToNode = node;
            head = node.next;
            deleteToNode.next = null;
            return deleteToNode;
        }
        for (int i = 0; i < positionToRemove - 1; i++) {
            node = node.next;
        }
				
				//지정 위치 노드 삭제
        ListNode deleteToNode = node.next;
        node.next = deleteToNode.next;
        deleteToNode.next = null;
        return deleteToNode;
    }

    @Override
    public boolean contains(ListNode head, ListNode nodeToCheck) {
        while (head.next != null) {
            if (head.next == nodeToCheck)
                return true;
            head = head.next;
        }
        return false;
    }
}

ListNodeTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

class ListNodeTest {
    private ListNode listNode;
    private static final int[] ADD_DATA = {1,3,4,5,7,9};
    private static final boolean[] CONTAINS_DATA = {true, false};
    private static List<Integer> acc_data;

    @BeforeEach
    void setUp() {
        acc_data = new ArrayList<>();
        listNode = new ListNode();
        ListNode firstNode = new ListNode(1);
        ListNode secondNode = new ListNode(3);
        ListNode thirdNode = new ListNode(5);
        ListNode fourthNode = new ListNode(7);
        ListNode fifthNode = new ListNode(9);

        this.listNode = firstNode;
        firstNode.next = secondNode;
        secondNode.next = thirdNode;
        thirdNode.next = fourthNode;
        fourthNode.next = fifthNode;
    }
    @Test
    void add() {
        listNode = listNode.add(listNode,new ListNode(4),2);

        while(listNode != null){
            acc_data.add(listNode.data);
            listNode = listNode.next;
        }

        for(int i=0;i<acc_data.size();i++) {
            Assertions.assertEquals(ADD_DATA[i],acc_data.get(i));
        }

    }

    @Test
    void remove() {
        ListNode removed = listNode.remove(listNode,2);
        Assertions.assertEquals(5,removed.data);
    }

    @Test
    void contains() {
        boolean[] result = new boolean[2];
        result[0] = listNode.contains(listNode,new ListNode(9));
        result[1] = listNode.contains(listNode,new ListNode(10));

        for(int i=0;i<acc_data.size();i++) {
            Assertions.assertEquals(CONTAINS_DATA[i],result[i]);
        }
    }
}

과제 3. Stack을 구현하세요.

  • int 배열을 사용해서 정수를 저장하는 Stack을 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.

Stack Interface

public interface Stack {
    void push(int data);
    int pop();
}

ArrayStack.java

public class ArrayStack implements Stack {
    int[] stack;
    int top;

    public ArrayStack(int size) {
        stack = new int[size];
        top = -1;
    }

    @Override
    public void push(int data) {
        stack[++top] = data;
    }

    @Override
    public int pop() {
        if(top == -1){
            System.out.println("Empty");
            return top;
        }
        return stack[top--];
    }
}

ArrayStackTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ArrayStackTest {
    private ArrayStack arrayStack;
    private static final int[] PUSH_DATA = {1,3,5,7,9};

    @BeforeEach
    void setUp() {
        arrayStack = new ArrayStack(5);
    }

    @Test
    void push() {
        arrayStack.push(1);
        arrayStack.push(3);
        arrayStack.push(5);
        arrayStack.push(7);
        arrayStack.push(9);

        for(int i=0;i<arrayStack.stack.length;i++){
            Assertions.assertEquals(PUSH_DATA[i],arrayStack.stack[i]);
        }
    }

    @Test
    void pop() {
        arrayStack.push(1);
        arrayStack.push(3);
        arrayStack.push(5);
        arrayStack.push(7);
        arrayStack.push(9);

        Assertions.assertEquals(9,arrayStack.pop());

    }
}

과제 4. 앞서 만든 ListNode를 사용해서 Stack을 구현하세요.

  • ListNode head를 가지고 있는 ListNodeStack 클래스를 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.

ListNodeStack.java

public class ListNodeStack implements Stack {

    static int top;
    ListNode node;
    public ListNodeStack() {
        this.node = null;
        this.top = -1;
    }

    @Override
    public void push(int data) {
        ListNode pushNode = new ListNode(data);
        if(node == null){
            node = new ListNode(data);
            top++;
        }else {
            node = node.add(node, pushNode, ++top);
        }
    }

    @Override
    public int pop() {
        if(top == -1) {
            System.out.println("Empty");
            return top;
        }
        return node.remove(node,top--).data;
    }
}

ListNodeStackTest.java

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

class ListNodeStackTest {
    private ListNodeStack stack;
    private static final int[] PUSH_DATA = {1,3,5,7,9};
    @BeforeEach
    void setUp() {
        stack = new ListNodeStack();
    }

    @Test
    void push() {
        stack.push(1);
        stack.push(3);
        stack.push(5);
        stack.push(7);
        stack.push(9);

        ListNode node = stack.node;

        int i=0;
        while(node != null) {
            Assertions.assertEquals(PUSH_DATA[i++],node.data);
            node = node.next;
        }
    }

    @Test
    void pop() {
        stack.push(1);
        stack.push(3);
        stack.push(5);
        stack.push(7);
        stack.push(9);

        Assertions.assertEquals(9,stack.pop());
        Assertions.assertEquals(7,stack.pop());
        Assertions.assertEquals(5,stack.pop());
        Assertions.assertEquals(3,stack.pop());
        Assertions.assertEquals(1,stack.pop());

    }
}

 

과제 5. Queue를 구현하세요.

  • 배열을 사용해서 한번
  • ListNode를 사용해서 한번.
public class ArrayQueue implements Queueable {
  int[] queue;
  int head, tail;

  public ArrayQueue(int capacity) {
    this.queue = new int[capacity];
    this.head = -1;
    this.tail = 0;
  }

  @Override
  public void push(int data) {
    this.queue[++this.head] = data;
  }

  @Override
  public int pop() {
    if (this.tail > this.head) throw new IndexOutOfBoundsException();
    return this.queue[this.tail++];
  }
}
public class ListNodeQueue implements Queueable {
  ListNode head;

  public ListNodeQueue() {
    this.head = new ListNode();
  }

  @Override
  public void push(int data) {
    ListNode node = new ListNode(data);
    ListNode cur = this.head;
    while (cur.next != null) cur = cur.next;
    cur.next = node;
  }

  @Override
  public int pop() {
    int data = this.head.next.data;
    this.head = this.head.next;
    return data;
  }
}

제어문

Java에서 코드는 위에서 아래 순으로 읽고 실행된다. 모든 일을 순차적으로 실행할 수 있다면 아무런 상관이 없겠지만, 어떤 코드를 반복해야 될 수도 있고 어떤 코드는 건너뛰는 등의 순서를 변경해야 되는 일이 발생한다. 이 때, 제어문을 사용하여 코드 실행 흐름을 제어할 수 있다.이러한 제어문은 선택문(if-then, if-then-else, switch)과, 반복문(for, while, do-while), 분기문(break, continue, return)으로 나뉜다.


 

선택문(Decision-making Statement)

Java는 **if/else문(조건문)과 Switch/case문(선택문)**을 제공한다.

if-then

if-then 문은 가장 기본적인 제어문중 하나로 지정한 조건이 만족할 시에 지정한 블록({}) 안에 있는 코드가 실행된다.

if(조건식) { // 한 줄일 경우 {} 생략 가능 
	조건이 참일 경우 실행되는 코드; 
}

만약 if-then문 안에 코드가 한 줄이라면 {}은 생략이 가능하다.

if-then-else

기본적인 if-then 문에서는 참일 경우만 실행이 됬다면, if-then-else 문은 거짓일 때도 실행할 수 있다.
즉, 조건식이 참일 경우와 거짓일 경우의 실행되는 코드들을 나눌 수 있다는 것이다.

if(조건식) { 
	참일 경우; 
} else { 
	거짓일 경우; 
}

이렇게 조건이 하나만 존재 할 수 있지만, 여러가지의 조건을 사용해야 할 경우가 생길 수 있다.(예 - 학점)
이 때는 else if()를 사용하여 또 다른 조건식을 사용할 수 있다.

switch

switch 문은 if-then과 if-then-else 문과 다르게 변수에 대해 평가를 하고 이를 분기할 수 있다.
평가 당하는 변수는 원시형 타입(int, byte, char...)일 수 있고, Enum형 혹은 String, Wrapper(Integer, Byte, Character...) 클래스도 가능하다. 여러개의 if문은 코드의 가독성 및 여러 조건 탐색을 해야하므로 속도가 늦어진다는 단점이있다. switch문은 switch의 매개변수에 맞는 조건에 따라 case문을 실행하여 다중 if문의 단점을 개선한 선택문이다.

각각의 case문에 break 키워드를 사용하지 않으면 switch문을 탈출하지 않으므로 다음 case문도 실행하기 때문에 주의해야한다.

switch(변수) { 
	case 값 A: 
		변수가 값 A에 해당하는 경우; 
		break; 
	case 값 B; 변수가 값 B에 해당하는 경우; 
		break; 
	default: 어떠한 값에도 해당하지 않는 경우; 
		break; 
}

위 예시는 다음과 같이 if-then-else 문으로 변경도 가능하다.

if(변수 == 값 A) { 
	변수가 값 A에 해당하는 경우; 
}else if(변수 == 값 B) { 
	변수가 값 B에 해당하는 경우; 
}else { 
	어떠한 값에도 해당하지 않는 경우; 
}

 

반복문(Looping Statement)

코드를 조건에 맞게 반복해주는 구문을 말한다.

반복문에는 for문, while문, do-while문, for-each(향상된 for문),Iterator가 있다.


 

for

코드를 조건에 맞게 반복해주는 구문을 말한다.

반복문에는 for문, while문, do-while문, for-each(향상된 for문),Iterator가 있다.

for(초기식; 조건식; 증감식) { 
	반복 될 코드; 
}

JDK 5.0 이상부터 배열 혹은 컬렉션의 순회시에 다음과 같이 조금 더 향상된 for 문을 사용할 수 있다.

for(타입 변수명 : 배열/컬렉션) { 
	반복 될 코드; 
}

foreach 스타일 for문

: 어떤 컬렉션이든 순회할 수 있음

 

Effetive java - item 46 : for문보다는 for-each를 사용하라

for-each문은 반복자나 인덱스 변수를 제거해 오류 가능성을 줄인다

 int[] nums = {1,2,3,4,5};
 for (int num : nums) {
	 System.out.println(num);
 }

while

while문은 조건의 값이 참인 경우에는 계속 반복하는 구문이다. 따라서, 조건이 항상 참인 경우 무한루프가 발생하기 때문에 유한적인 조건을 주거나 while문 내부에 탈출 조건을 반드시 명시해주어야 한다.

while(조건식) { 
	조건식이 참일 경우 반복되는 코드; 
}

조건식이 항상 참일 경우에는 계속해서 해당 코드들이 실행되므로 다음 명령들을 수행할 수 없는 상태가 된다.
따라서 조건식을 잘 유의해서 사용해야 된다.

do-while

do-while문은 while문과 달리 조건문이 하단에 있는 구문이다.

while문은 처음에 조건을 확인하고 실행하는 반면, do-while문의 경우 먼저 구문을 실행한 후 마지막에 조건을 확인함으로써 반드시 한번은 실행한다는 차이점이 있다.

do-while문은 반드시 하단 조건을 명시한 후 **세미콜론(;)**을 써야한다.

do { 
	조건식이 참일 경우 반복되는 코드; 
}while(조건식);

 

분기문(Branching Statement)

조건문에서 프로그래머의 의도에 의해 중간 흐름을 바꿀 수 있는 구문


 

break

break 문은 두가지 케이스로 나뉘는데, 이는 라벨링이 된 것과 안된 것이다.

먼저 라벨링이 되지 않는 break 문은 switch 문, 반복문에서 사용될 수 있으며, 해당 구문을 만났을 때 가장 가까운 조건문을 종료한다.

for(int i = 0; i <= 10; i++) { 
	if(i == 5) break;
    System.out.println(i); 
} // 출력 // 1 // 2 // 3 // 4

다음과 같은 예시가 존재할 때, i가 5가 되면 반복문을 종료하고 다음 코드로 진행하게 된다.

라벨링이 된 break 문은 똑같이 break를 만나면 제어문이 종료되지만, 해당 라벨링이 된 제어문 자체가 종료된다.
즉, 가장 가까운 제어문뿐만 아니라 자신이 표시한 위치안의 제어문을 종료하는 것이다.

int findIt = 10; 
search: 
for(int i = 0; i < arr.length; i++) { 
  for(int j = 0; j< arr[i].length; j++) { 
    if(arr[i][j] == findIt) { 
    	break search;
  	}
  } 
}

위의 예시에서는 arr의 값이 10일 경우, search: 안에 있는 두개의 for 문 모두 종료된다.

continue

continue 문은 반복문 안에서 사용되며, 조건에 해당할 경우에만 반복문을 건너뛴다.
continue 문도 break 문과 똑같이 라벨링이 된 경우와 안된 경우가 존재하고 똑같은 메커니즘으로 동작한다.

for(int i = 0; i <= 10; i++) { 
	if(i == 5) continue; System.out.println(i); 
}
// 출력 // 1 // 2 // 3 // 4 // 6 // 7 // 8 // 9 // 10

위의 예제에서는 i가 5일 경우에만 가장 가까운 반복문의 끝으로 건너뛴다.

int findIt = 10; 
search: 
for(int i = 0; i < arr.length; i++) {
  for(int j = 0; j< arr[i].length; j++) { 
    if(arr[i][j] == findIt) { 
      continue search; 
    } 
  } 
}

위의 예제의 경우, arr의 값이 10일 경우, search: 의 가장 바깥 for 문의 끝으로 건너뛰게 된다.

return

return 문은 현재 메소드를 종료시키고 해당 메소드를 호출한 위치로 돌아간다.
이 또한 메소드의 타입에 의해 뒤에 값이 올 수 있고 안올 수 있다.

int getAge(String name) { 
	if(name == "jongnan") return 28; 
	System.out.println("존재하지 않는 사람!"); 
	return -1; 
}

위의 예제는 메소드의 타입이 int 이므로 반환 값을 지정해주어야 한다.

void pringAge(String name) { 
	if(name == "jongnan") { 
    	System.out.println("28"); 
    	return; 
	} 
	System.out.println("존재하지 않는 사람!"); 
	return; 
}

Enumeration 

  • 초기 Collection만 지원
  • Snap-shot : 원본과 달리 사본을 남겨처리. 다른 처리가 발생하면 오류 가능성 생김
  • hasMoreElements(), nextElement()
Vector<Integer> vector = new Vector<>(Arrays.asList(1,2,3));
Enumeration elements = vector.elements();
while (elements.hasMoreElements()) {
	int e = (int)elements.nextElement();
	System.out.println(e);
}

Iterator

  • 모든 Collection 지원
  • enumeration에 비해 빠르다.
  • hasNext(), next(), remove()
Iterator<Integer> iterator = Arrays.asList(1, 2, 3).iterator();
while (iterator.hasNext()) {
	Integer number = iterator.next();
}

 


과제 1. live-study 대시 보드를 만드는 코드를 작성하세요.

 

과제 2. LinkedList를 구현하세요.

  • LinkedList에 대해 공부하세요.
  • 정수를 저장하는 ListNode 클래스를 구현하세요.
  • ListNode add(ListNode head, ListNode nodeToAdd, int position)를 구현하세요.
  • ListNode remove(ListNode head, int positionToRemove)를 구현하세요.
  • boolean contains(ListNode head, ListNode nodeTocheck)를 구현하세요.
public class Main {
  interface LinkedList {
    ListNode add(ListNode head, ListNode nodeToAdd, int position);

    void printList(ListNode head);

    ListNode remove(ListNode head, int positionToRemove);

    boolean contains(ListNode head, ListNode nodeTocheck);
  }

  static class LinkedListImpl implements LinkedList {

    public void printList(ListNode head) {
      while (head != null) {
        System.out.println(head.data);
        head = head.next;
      }
    }

    public int size(ListNode head) {
      ListNode node = head;
      int size = 1;
      while (node.next != null) {
        node = node.next;
        size++;
      }
      return size;
    }

    public ListNode add(ListNode head, ListNode nodeToAdd, int position) {

      ListNode node = head;
      if (position == 0) {
        if (head == null) {
          return nodeToAdd;
        }
        // 노드를 생성한다.
        ListNode add = nodeToAdd;
        // 새로운 노드의 다음 노드로 헤드를 지정한다.
        add.next = head;
        // 헤드로 새로운 노드를 지정한다.
        head = add;
        return head;
      }

      for (int i = 0; i < position - 1; i++) {
        node = node.next;
      }
      nodeToAdd.next = node.next;
      node.next = nodeToAdd;
      return head;
    }

    public boolean contains(ListNode head, ListNode nodeTocheck) {
      while (head != null) {

        if (head.data == nodeTocheck.data) {
          return true;
        }
        head = head.next;
      }
      return false;
    }

    public ListNode remove(ListNode head, int positionToRemove) {
      ListNode node = head;
      if (positionToRemove == 0) {
        head = head.next;
      } else {
        for (int i = 0; i < positionToRemove - 1; i++) {
          node = node.next;
        }
        ListNode delNode = node.next;
        node.next = node.next.next;
      }
      return head;
    }
  }

  static class ListNode {
    private int data;
    private ListNode next;

    public ListNode(int input) {
      data = input;
      next = null;
    }

    @Override
    public String toString() {
      return String.valueOf(data);
    }
  }

  public static void main(String[] args) {
    LinkedList numbers = new LinkedListImpl();
    // 시작에 추가
    ListNode head = null;
    for (int i = 0; i < 10; i++) {
      head = numbers.add(head, new ListNode(i), i); // 특정 포지션 추가
    }
    head = numbers.remove(head, 0);
    head = numbers.remove(head, 1);
    head = numbers.remove(head, 2);
    System.out.println(numbers.contains(head, new ListNode(5)));
    System.out.println(numbers.contains(head, new ListNode(2)));
    numbers.printList(head);
  }
}


출처: https://juntcom.tistory.com/118 [쥬니의 개발블로그]

https://www.notion.so/Live-Study-4-ca77be1de7674a73b473bf92abc4226a

프리미티브 타입 종류와 값의 범위 그리고 기본 값

Primitive Type (기본형, 프리미티브 타입)

  • 총 8가지의 기본형 타입이 있음
  • 자바에서 기본 자료형은 사용전에 반드시 Declared 되어야함
  • C에서와 다르게 OS에 따라 자료형의 길이가 변하지 않음
  • 실제값이 저장되는 공간으로 Stack 메모리 공간에 저장됨
  • 객체가 타입이 아니고, 기본값이 있어서 Null이 존재하지 않음
  Type Default Value(기본값) Size (할당크기) Range of Values (표현 가능 범위)
Integer byte 0 1 byte  -128 ~ 127
short 0 2 byte -32,768 ~ 32,767
int (default) 0 4 byte  -2,147,483,648 ~ 2,147,483,647
long 0 8 byte -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
Floating-Point float 0.0 4 byte 소수점 7 자리까지
double (default) 0.0 8 byte 소수점 16 자리까지
Char char \u0000 2 byte (유니코드)  0 ~ 65,535
Bool boolean false 1 byte true, false

프리미티브 타입과 레퍼런스 타입

Reference Type (참조형 타입)

  • 기본형 타입을 제외한 타입들이 모두 참조형 타입이다.
  • 빈 객체를 의미하는 Null이 존재한다. 그리고 기본값이 Null이다.
  • 값이 저장되어 있는 곳의 주소값을 저장하는 공간으로 힙 (Heap) 메모리에 저장됩니다.
  • Primitive Type vs Reference Type: 
  • 메모리상에 할당되는 각각의 공간에 실제 값이 들어가면 Primitive 타입이고, 다른 것을 참조하기 위한 주소값이 들어가면 Reference Type 이다.
  • 클래스 타입, 인터페이스 타입, 배열 타입, 열거 타입이 있습니다.

리터럴

Literal (리터럴)

  • 데이터 그 자체를 의미한다. 변수에 넣는 변하지 않는 데이터를 의미한다.
  • 프로그램에서 직접 표현한 값이다.
  • 소스 코드의 고정된 값을 대표하는 용어이다.
  • 정수, 실수, 문자, 논리, 문자열 리터럴이 존재한다.
int trueNum = 1;
  • 여기서 trueNum은 변수고 1이 리터럴이다.

 

  • integer (정수) 리터럴: 모든 정수 값은 정수 리터럴이다. 
int dec = 15; // 10진수 리터럴 15
int bin = 0b1101; // 0b로 시작하니 2진수 리터럴
int oct = 013; // 0으로 시작하면 8진수 리터럴
int hex = 0x13; // 0x로 시작하면 16진수 리터럴
long long_dec = 15L; // long타입 리터럴은 l 또는 L을 붙여 표시한다.

 

  • float (실수) 리터럴: 소수점이나 지수형태로 표현한 값
double a = 0.1122;
double b = 1122E-5; // 1122 * 10 * (-5) = 0.01122와 동일하다

float c = 0.1122f;
double d = .1122D;

 

  • char (문자) 리터럴: 단일 인용부호 (' ')로 문자를 표현해야한다. (" ") 이걸로 하면 Compile Error
char a = 'D';
char b = "E"; // Compile Error

Compile Error Message: Main.java:15: error: incompatible types: String cannot be converted to char char b = "g"; ^ 1 error

 

 

  • boolean (논리) 리터럴
boolean a = true;
boolean b = 10 > 0; // b 는 true
boolean c = 0; // C언어에서와 다르게 boolean type으로 1,0을 참, 거짓으로 사용불가능하다.

 

  • string (문자열) 리터럴
    • null 리터럴은 레퍼런스에 대입해서 사용한다.
    • 기본타입에는 사용이 불가능하고 String 같은경우에는 Class여서 사용 가능하다.
int a = null; // compile error
String str = null;
str = "JAVA"

변수 선언 및 초기화하는 방법

변수 선언 및 초기화하는 방법

변수란? 값을 저장할 수 있는 메모리 공간에 붙여진 이름이다. 변수라는 것을 선언하면 메모리 공간이 할당되고 할당된 메모리 공간에 이름이 붙는다.

 

변수선언

int num; // 변수형식 변수이름
int num = 20; // 변수형식 변수이름 = 초기화값
int num1, num2, num3, num4; // 변수형식 변수이름1, 변수이름2, 변수이름3, 변수이름4
int num1 = 10, num2 = 20, num3 = 30, num4 = 40; // 변수형식 변수이름1 = 초기화값, 변수이름2 = 초기화값, 변수이름3 = 초기화값, 변수이름4 = 초기화값;

 

 

변수의 초기화

변수를 선얺하고 값을 저장하는 것을 변수 초기화라고 한다. 변수의 초기화의 경우에 따라 필수적일수도 선택적일수도있지만 가능하면 선언과 동시에 적절값으로 초기화하는게 좋다. 

멤버 변수는 초기화 하지 않아도 변수의 타입에 맞는 기본값으로 초기화가 이루어지지만, 지역변수는 사용하기 전에 반드시 초기화가 이루어져야한다.

 

 

변수의 스코프와 라이프타임

  • 변수 스코프: 변수를 사용할수있는 범위를 이야기한다. {중괄호} 안에서 변수를 선언했을 경우 영역이 끝나기 전까지는 어디서든 사용이 가능하다.  
  • 자바의 변수 (클래스, 인스턴스, 지역변수)
    • 변수의 선언 위치에따라서 종류가 달라진다.
public class test {

	int instVar; // 인스턴스 변수    
    static int classVar; // 클래스 변수
    
    void method() {
    	int localVar; // 지역 변수
    }
}
변수의 종류 선언위치 생성시기 (메모리 활당 시기)
클래스 변수 클래스 영역 클래스가 메모리에 올라갈때
인스턴스 변수 인스턴스가 생성될때
지역 변수 클래스 이외의 영역 (메서드, 생성자, 초기화 블럭) 변수 선언문이 수행 되었을때

 

  • 인스턴스 변수: 인스턴스가 생성될때 생성된다. 인스턴스 변수의 값을 읽거나 저장하려면 인스턴스를 먼저 생성해야하고 인스턴스별로 다른값을 가질수있다. 각각의 인스턴스마다 고유의 값을 가져가야할때는 인스턴스 변수로 선언을 한다.
  • 클래스 변수: 인스턴스 변수에 static만 붙여주면 된다. 인스턴스 변수는 각각 고유한 값을 가지지만 클래스 변수는 모든 인스턴스가 공통된 값을 공유하게 된다. 한 클래스의 모든 인스턴스들이 공통으로 가져야할 때 클래스 변수로 선언한다. 클래스가 로딩될때 생성 (메모리에 딱 한번만 올라간다.)되고 종료 될 때 까지 유지되는 클래스 변수는 public을 붙이면 같은 프로그램 내에서 어디서든 접근할 수 있는 전역 변수가 된다.  또한 인스턴스가 변수의 접근법과 다르게 인스턴스를 생성하지 않고 클래스이름.클래스변수명 을 통해서 접근가능하다.
  • 지역변수: 메서드 내에서 선언되고 메서드 내에서만 사용 가능하다. 메서드가 실행될때 메모리를 할당 받고 메서드가 끝나면 소멸되므로 사용이 불가능하다.

타입 변환, 캐스팅 그리고 타입 프로모션

타입변환(캐스팅)은 특정 자료형을 다른 자료형으로 변환하는 예를들면, int형과 float형은 자료형을 저장하는 방식이 다르다. 또, int와 long도 자료를 저장하는 방식이 다르다. 특정 자료형을 다른 자료형과 호환되게 변경하는 방법을 타입변환, 타입케스팅, 형변환이라고 한다.

 

Primitive 타입들은 타입캐스팅이 모두 가능하게 되어있다. 문제가 생길경우는 큰 데이터형을 작은 데이터형으로 변환하게되면 테이터 손실이 있을수있다. 하지만 캐스팅이 실패하지는 않는다.

 

타입 프로모션이란 크기가 더 작은 자료형을 더 큰 자료형에 대입할 때, 자동으로 작은 자료형이 큰 자료형으로 변환되는 현상이다. 문제없이 자동 변환된다. (e.g. int -> float) 크기가 더 큰 자료형에 더 작은 자료형을 자연스럽게 넣을 수 있는것이라고 생각하면 된다.

 

클래스에서도 타입 변환(캐스팅)이 존재한다. 

하지만, 기본적으로 클래스 타입들은 타입 캐스팅이 불가능하지만, 다형성이라는 성질을 통해서 두 클래스가 상속 관계일 경우에 타입 변환이 가능하다.

  • 부모클래스는 명시적인 타입캐스팅 없이 자식과 연결 할 수 있다. 이를 업캐스팅(Up-casting)이라 한다. 
  • 자식클래스는 명시적인 타입캐스팅이 있다면 부모와 연결 할 수 있다. 이를 다운캐스팅(Donw-Casting)이라 한다.
  • 상속관계가 아니면 타입캐스팅은 불가능하다. 

1차 및 2차 배열 선언하기

변수는 하나의 데이터를 저장하지만, 배열은 여러개의 데이터들을 인덱스구조에 저장을 한다. 배열의 길이는 한번 생성하면 줄이거나 늘릴수없고 같은 타입들만이 배열의 원소로 사용된다.

 

1차원 배열

// 1. 배열 선언, 생성, 할당을 동시에 (2가지 방법)
// <자료형>[] 배열이름 = {원소들} 
// <자료형> 배열이름[] = {원소들}

int[] score = {10, 11, 12}; // or
int score[] = {10, 11, 12};


// 2. 배열 선언후에 생성과 할당을 하는방법

int score[];
string name[]; // 배열 선언

score = new int[] {10, 50, 100};
name = new string[] {"white", "ship}; // 생성과 할당

// 3. 배열 선언 후 -> 생성 후 -> 할당
int score[]; // 선언
score = new int[3]; // 생성
score[0] = 10;
score[1] = 50;
score[2] = 100; // 할당

 

 

2차원 배열

배열의 배열이라고 생각하면 된다.

int[][] array1 = new int[2][3]; // 정수를 3개씩 저장할수있는 배열이 2개가 생성된다.
int[][] array2 = new int[4][]; // array2는 4개짜리 배열을 참조한다. 그리고 4개짜리의 배열 각각은 아직 참조하는 배열이 없다.

array2[0] = new int[1]; // 정수 하나를 저장할 수 있는 배열을 생성 array2의 0 번째 요소가된다.
array2[1] = new int[2]; // 정수 두개를 저장할 수 있는 배열을 생성 array2의 1 번째 요소가된다.
array2[2] = new int[3]; // 정수 세개를 저장할 수 있는 배열을 생성 array2의 2 번째 요소가된다.

int[][] array2 = {{0}, {1, 2}, {3, 4, 5}}; // 선언과 동시에 초기화

타입 추론, var

타입추론이란 타입이 안정해진 변수타입을 컴파일러가 유추하는 기능이다.

var 는 자바 10부터 타입추론을 가능할수있는 키워드로 추가되었다. 이 키워드는 지역변수이면서 선언과 동시에 초기화가 반드시 되어야한다. msg는 컴파일되는 과정에서 컴파일러가 String으로 추론이 가능하다.

var msg = "Hello, hope to complete STUDYHALLE! :)";

'Language > Java' 카테고리의 다른 글

[6주차] 상속  (0) 2022.01.30
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10
[1주차] JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가  (0) 2021.12.26

JVM이란 무엇인가

  • Java Virtual Machine의 약어
  • 자바가상머신이라고도 불린다.
  • 자바 프로그램을 컴파일 해서 나온 결과인 바이트코드를 실행시켜주는 가상 머신이다.
  • 각 운영체제별 JVM은 자바측에서 개발하여 배포하므로, 프로그래머는 운영체제에 관계없이 프로그램을 개발할 수 있어, 한번 컴파일 됐으면 운영체제에 따라 다시 컴파일할 필요가 없는 WORA(Write Once Run Anywhere)를 만족한다.

자바 소스파일(.java)을 JVM으로 실행 과정

1) 프로그램이 실행되면 JVM은 운영체제로 부터 이 프로그램이 필요로 하는 메모리를 할당 받음

 

JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리

 

2) 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여 자바 바이트코드(.class)로 변환 
.java -> .class

3) Class Loader를 통해 class파일을 JVM 메모리에 적재

4) JVM 메모리 영역에 적재된 class 파일을 Execution engine을 통해 해석

컴파일 하는 방법

컴파일이란 우리의 언어는 컴퓨터가 이해하지 못하므로 컴퓨터가 이해할 수 있도록 "통역"하는 작업을 말한다.

1. 자바 컴파일러는 자바 개발 키트(JDK)에 포함되어 있기 때문에 작성된 코드를 컴파일해 바이트코드를 생성하기 위해선 우선 JDK가 필요하다.
2. 자바 언어 사양(JLS)을 충족하는 자바 소스코드(*.java) 파일을 작성한다.
3. 자바 컴파일러(javac.exe)를 통해 자바 소스코드(.java)를 자바 가상 머신 사양(JVMS)을 충족하는 바이트코드(.class)로 컴파일 한다.
    - javac Test.java
4. *.class 파일이 생성된 것을 확인 할 수 있다.
    - Test.class

실행하는 방법

java.exe -> 자바 인터프리터로서 컴파일러로 생성된 바이트 코드를 해석하고 실행한다.
- java Test

바이트코드란 무엇인가

프로그램을 실행하는 것은 결국 컴퓨터이다. 다시 말해 프로그램은 컴퓨터가 이해할 수 있는 형태로 작성되어 있어야 한다.

1) 바이너리코드란?

C언어는 컴파일러에 의해 소스파일(.c)이 목적파일(.obj)로 변환될 때 0과 1로 이루어진 바이너리코드로 변환된다. 
즉, 컴파일 후에 이미 컴퓨터가 이해할 수 있는 이진코드로 변환되는 것이다.

 

목적파일은 기본적으로 컴퓨터가 이해할 수 있는 바이너리코드의 형태이지만 실행될 수는 없다. 그 이유는 완전한 기계어가 아니기 때문 (변화된 목적 파일은 링커에 의해 실행 가능한 실행파일(.ex)로 변환 될 때 100% 기계어가 될 수 있다.

 

2) 기계어란 ?

기계어는 컴퓨터가 이해할수 있는 0과 1로 이루어진 바이너리코드이다.

기계어가 바이너리코드로 이루어졌을 뿐이지 모든 이진코드가 기계어인 것은 아니다( 바이너리코드 != 기계어)

3) 바이트코드란 ?

c언어와 다르게 Java에서는 컴파일러(javac)에 의해 소스파일(.java)이 컴퓨터가 바로 인식할 수 없는 바이트코드(.class)로 변환된다.

컴퓨터가 이해할 수 있는 언어가 바이너리코드라면 바이트코드는 가상 머신이 이해할 수 있는 언어이다.

고급언어로 작성된 소스코드를 가상 머신이 이해할 수 있는 중간 코드로 컴파일한 것을 말한다.

이러한 과정을 거치는 이유는 어떠한 플랫폼에도 종속되지 않고 JVM에 의해 실행 될수 있도록 하기 위함이다.

 

여기서 플랫폼이란 개발환경 실행환경 등 어떠한 목적을 수행할 수 있는 환경을 말한다.

 

 

ex) 프로그램이 실행되는 환경인 운영체제의 종류(window, mac), 개발이 수행되는 환경의 종류(안드로이드, 비주얼 스튜디오)

 

JIT 컴파일러란 무엇이며 어떻게 동작하는지

JIT 컴파일러란 무엇이며 어떻게 동작하는가?JIT 컴파일러란? 기존 클래스파일(바이트코드)를 실행하는 방법은 Interpreter 방식이 기본이다. Interpreter 방식은 3-1에서 설명한바와 같이 명령어를 하나씩 해석해서 처리하는 개념이기 때문에 명령어 하나하나 실행하는 속도는 빠를지 모르나 전체 코드 관점에서 보면 실행 속도가 느린 단점이 있다. 해당 문제를 해결하기 위해서 나온 방법이 JIT 컴파일러이고 JIT 컴파일러는 런타임 시 클래스파일(바이트코드)를 네이티브 기계어로 한방에 컴파일 후 사용하는 개념으로 이해하는 것이 편하다.

좀 더 상세히 얘기하자면 전체 컴파일 후 캐싱 -> 이후 변경된 부분만 컴파일하고 나머지는 캐시에서 가져다가 바로 실행 이다. 바로 꺼내서 사용하고 변경 부분만 컴파일 하기 때문에 코드 수행속도가 Interpreter 방식에 비해서 빠르다!

JVM 구성 요소

출처: https://www.guru99.com/images/1/2.png

6-1. Class Loader: 클래스 로더는 클래스 파일을 로드하는 데 사용되는 하위 시스템이다.

6-2. Method Area: JVM Method Area는 메타데이터, 상수 런타임 풀, 메서드에 대한 코드와 같은 클래스 구조를 저장한다.                               공유자원(여기서 공유자원이라는 의미는 다른 스레드에서도 활용 가능한 자원을 말함)이다.

6-3. Heap: 모든 개체, 관련 인스턴스 변수 및 배열은 힙에 저장된다. 
                   이 메모리는 여러 스레드에 걸쳐 공유된다.


6-4. JVM Language Stacks: Java Language Stacks는 로컬 변수를 저장하고 부분적인 결과를 얻는다. 
각 스레드에는 자체 JVM 스택이 있으며, 스레드가 생성될 때 동시에 생성된다. 
메서드를 호출할 때마다 새 프레임이 생성되고, 메서드 호출 프로세스가 완료되면 삭제된다.
스택은 공유자원이 아니므로 스레드 세이프(여러 스레드에서 공용 자원을 접근할 때 생길 수 있는 문제) 하다.
내부에는 Local Variable Array, Operand Stack, Frame Data의 영역이 있다.


6-5. PC Register: PC 레지스터는 현재 실행 중인 Java 가상 시스템 명령의 주소를 저장한다. 
자바에서는 각 스레드에 별도의 PC 레지스터가 있다.

6-6. Native Method Stack: 네이티브 메서드 스택은 네이티브 라이브러리에 따라 네이티브 코드 명령을 보관한다. 
자바 대신 다른 언어로 쓰여 있다.


6-7. Execution Engine: 런타임 데이터 영역에 할당 된 바이트코드는 실행 엔진에 의해 실행된다. 
실행 엔진은 바이트코드를 읽고 조각 별로 실행한다.


6-8. Native Method Interface: Native Method Interface는 프로그래밍 프레임워크다. JVM에서 실행 중인 Java 코드가 라이브러리 및 네이티브 애플리케이션으로 호출할 수 있도록 한다.


6-9. Native Method Libraries: Native Libraries는 실행 엔진에 필요한 Native Libraries(C, C++)의 모음이다.

JDK와 JRE의 차이

JRE

  • Java Runtime Environment의 약자로, 자바 실행 환경을 의미한다.
  • JVM을 동작하는데에 필요한 각종 자바 라이브러리를 담고 있다.

JDK

  • Java Development Kit의 약자로, 자바 개발 키트를 의미한다.
  • JRE와 javac 등의 컴파일러, 디버거등을 포함하는 프로그램이다.
  • 오라클사에서 제공하는 오라클 JDK와 오픈소스로 개발된 OpenJDK가 있으나, 일반적으로 사용되는것은 오라클 JDK이다.
 
 

 

'Language > Java' 카테고리의 다른 글

[6주차] 상속  (0) 2022.01.30
[5주차] 클래스, [7주차] 패키지  (0) 2022.01.22
[4주차] 과제  (0) 2022.01.16
[4주차] 제어문  (0) 2022.01.10
[2주차] 자바 데이터 타입, 변수 그리고 배열  (0) 2022.01.02

+ Recent posts