Posts 학원 #33일차: 인터페이스, 추상 클래스, 싱글톤
Post
Cancel

학원 #33일차: 인터페이스, 추상 클래스, 싱글톤

인터페이스 사용법

1
2
3
4
5
public interface A { void m1();}
public interface B extends A { void m2(); }
public interface C { void m3(); }
public interface D { void m2(); void m4; }
public interface D2 { int m3(); }

위와 같이 인터페이스가 정의되어 있다고 하자. 어떻게 사용할 수 있을까?

인터페이스 상속

  • 서브 인터페이스는 수퍼 인터페이스의 메서드까지 모두 구현해야 한다.
  • 인터페이스 레퍼런스는 인터페이스를 구현한 클래스의 인스턴스를 가리킬 수 있고, 해당 인터페이스에 선언된 메서드(수퍼 인스턴스 메서드 포함)만 호출할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Exam implements B {
    @Override
    public void m2() {}
    @Override
    public void m1() {}
    public void x1() {}
    public void x2() {}
    
    public static void main(String[] args) {
        // 클래스 타입 레퍼런스는 해당 클래스에 정의된 메서드를 호출할 수 있다.
        Exam01 obj = new Exam01();
        obj.m1();
        obj.m2();
        obj.x1();
        obj.x2();
        
        // 인터페이스 레퍼런스는 해당 인터페이스에 선언된 메서드만 호출할 수 있다.
        B obj2 = obj;
    }
}
  • 클래스 타입 레퍼런스는 해당 클래스에 정의된 메서드를 호출할 수 있다.
  • 인터페이스 레퍼런스는 해당 인터페이스에 선언된 메서드만 호출할 수 있다.

다중 인터페이스

  • 클래스는 여러 개의 규칙을 이행할 수 있다.

  • 한 클래스가 여러 개의 인터페이스(사용 규칙)을 구현했다면,

    각 인터페이스로 구분해서 그 객체를 사용할 수 있다.

    즉, 구현체를 어떤 인터페이스 레퍼런스에 담느냐에 따라 사용할 때 따르는 규칙이 달라진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Exam02 implements B, C {
    @Override
    public void m2() {} // B의 인터페이스 구현
    @Override
    public void m1() {} // B의 수퍼 인터페이스 구현
    @Override
    public void x3() {} // C의 인터페이스 구현
    
    public static void main(String[] args) {
        // 클래스 타입 레퍼런스는 해당 클래스에 정의된 메서드를 호출할 수 있다.
        Exam02 obj = new Exam02();
        
        // B 인터페이스 레퍼런스에 담는다. 
        // 즉, Exam02 객체를 B 규칙에 따라 사용한다.
        B obj2 = obj;
        obj2.m1();
        obj2.m2();
		//obj2.m3(); => 컴파일 오류
        
        // C 인터페이스 레퍼런스에 담는다.
        // 즉, Exam02 객체를 C 규칙에 따라 사용한다.
        C obj3 = obj;
        //obj3.m1(); => 컴파일 오류
        //obj3.m2(); => 컴파일 오류
        obj3.m3();
    }
}
  • 규칙 중에서 메서드가 겹칠 수 있다. 같은 메서드를 여러 인터페이스에서 공유할 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class Exam03 implements B,C,D {
      public void m1(){} // B의 수퍼인터페이스인 A인터페이스 구현
      public void m2(){} // B의 D인터페이스 구현
      public void m3(){} // C의 인터페이스 구현
      public void m4(){} // D의 인터페이스 구현
        
      public static void main(String[] args) {
          Exam03 obj = new Exam03();
            
          B obj2 = obj;
          obj2.m2(); // 여기서는 B.m2()
          obj2.m1();
            
          C obj3 = obj;
          obj3.m3();
            
          D obj4 = obj;
          obj4.m2(); // 여기서는 D.m2()
          obj4.m4();
      }
    }
    

그러나 여러 개의 인터페이스를 구현하지 못하는 경우가 있다. 메서드 명과 파라미터는 같지만, 리턴 타입이 다른 경우이다. 클래스는 이름이 같고 파라미터 형식이 다른 메서드를 여러 개 정의할 수 있지만(오버로딩), 이름이 같고 파라미터 형식도 같고 다만 리턴 타입이 다른 메서드를 여러개 정의할 수 없다.

1
2
3
4
public class Exam04 implements C, D2 {
    public void m3(){} // C의 인터페이스 구현
    public int m3(){} // D2의 인터페이스 구현
}

위의 코드는 컴파일 오류이다.

=> Multiple markers at this line. The return type is incompatible with D2.m3()

인터페이스 다중 상속

인터페이스는 클래스와 달리 다중 상속이 가능하다. 이는 인터페이스의 메서드는 추상 메서드로 구현하기 전의 메서드이기 때문에 어떤 인터페이스의 메서드를 상속받아도 같기 때문이다.

한편 상위 인터페이스에 있는 메서드 중에서 메서드 명과 파라미터 형식은 같지만 리턴 타입이 다른 메서드가 있다면 둘 중 어떤 것을 상속받느냐에 따라 규칙이 달라지기 때문에 다중 상속이 불가능하다.

1
2
3
4
5
public interface A { void m1(); }
public interface B { void m1(); void m2(); }
public interface B2 { int m1(); void m2(); }
public interface C extends A, B { void m3(); }
public interface C2 extends A, B2 { void m3(); }
1
2
3
4
5
public class Exam01 implements C {
    public void m1(){} // A와 B의 규칙
    public void m2(){} // B의 규칙
    public void m3(){} // C의 규칙
}

인터페이스와 default 메서드

규칙을 변경하면, 그 규칙에 따라 만든 모든 클래스를 변경해야 한다. 내가 만든 규칙에 따라 내가 클래스를 만들었다면 변경하면 되지만, 내가 만든 규칙을 다른 많은 개발자들이 가져가서 클래스를 만들어 사용하기 때문에 쉽게 규칙을 변경할 수 없다. 규칙 즉 인터페이스의 메서드를 변경하거나 제거, 추가하는 순간 이 인터페이스를 구현한 모든 클래스들에서 컴파일 오류가 발생하게 된다. 규칙이 달라졌기 때문에 클래스 구현 오류가 발생하는 것이다. 클래스는 항상 인터페이스의 모든 메서드를 구현해야 한다.

그러나 그렇다고 계속 옛날 규칙을 가져갈 수 없고, 새 프로젝트에는 변경된 규칙으로 클래스를 만들고 싶다면 새 규칙을 새 인터페이스로 정의할 수 있지만, 그렇게 하면 기존에 진행한 모든 프로젝트들과 호환되지 않는 문제가 발생한다.

1
2
3
4
// 기존 인터페이스
public interface Computer {
    void compute();
}
1
2
3
4
5
6
// 새 규칙 정의
// 기존 규칙을 가져오기 위해 상속받는다.
public interface Computer2 extends Computer {
    // 추가하고 싶은 규칙
    void touch();
}

이에 Java 8 부터는 기존 규칙을 변경하되, 기존 구현체(기존 규칙에 따라 작성한 클래스)에는 default 메서드 문법을 사용할 수 있도록 하였다. 따라서 default 문법을 이용하면 기존 클래스 입장에서는 구현된 메서드를 상속받기 때문에 오류가 발생하지 않는다.

1
2
3
4
5
6
public interface Computer {
    void compute();
    default void touch(){
        // 구현할 코드가 있으면 작성하고, 없으면 빈 채로 놔둔다.
    }
}

위와 같이 touch()라는 새 규칙을 추가하더라도, 이 메서드는 default 메서드로 인터페이스에서 구현되어 있기 때문에 기존에 Computer 인터페이스를 구현한 클래스에서 컴파일 오류가 발생하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FirstComputer implements Computer {
    @Override
    public void compute() {
        System.out.println("단순히 계산을 수행한다.");
    }
}

public class FirstComputer implements Computer {
    @Override
    public void compute() {
        System.out.println("단순히 계산을 수행한다.");
    }
}

public class NewComputer implements Computer {
    // 기존 규칙 구현
    @Override
    public void compute() {
        System.out.println("새 컴퓨터")
    }
    // 새로 추가한 규칙도 구현
    @Override
    public void touch() {
        Sysptem.out.println("터치가 된다.")
    }
}

그러나 default 메서드 문법은 메서드 구현을 강제할 수 없다는 단점이 있다. 이는 default메서드가 추상메서드가 아니기 때문이다. 추상 메서드인 경우 클래스가 반드시 해당 메서드를 구현해야 하는데 default 메서드는 이미 구현하였기 때문에 클래스가 다시 구현하지 않아도 된다. 따라서 개발자가 새 기능을 깜빡할 수 있다.

1
2
3
4
5
6
public class NewComputer2 implements Computer {
    @Override
    public void compute() {
        System.out.println("새 컴퓨터..");
    }
}

따라서 default문법은 가능한 새 인터페이스를 만들 때가 아니라 기존 인터페이스에 새 기능을 추가할 때만 사용하는 것이 좋다.

추상 클래스/인터페이스의 차이점

  • 추상클래스와 인터페이스 모두 추상 메서드를 갖기 때문에 헷갈릴 수 있으나 둘의 사용 목적은 엄연히 다르다.
  • 추상 클래스의 목적은 필드와 메서드를 물려주는 것이다. 이때 겸사겸사 추상 메서드도 선언한다

  • 만약 추상 클래스에 추상 메서드만 있다면, 상속해줄 필드와 메서드가 없기 때문에 추상클래스로 만들 이유가 없다. 이 경우 인터페이스로 만들어야 옳다.
  • 인터페이스의 목적은 이 메서드를 구현해야 한다는 제약점을 만드는 것, 규칙을 정의하는 문법이다.

인터페이스 간접 구현

  • 상속 받는 순간 부모 클래스가 따르는 규칙(인터페이스)를 자식 클래스도 따르게 된다.

  • 부모 클래스가 인터페이스를 구현하면 자식 클래스도 그 인터페이스도 구현한 것이 된다.
  • 따라서 자식 객체를 해당 인터페이스 레퍼런스가 가리킬 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 인터페이스
public interface A {
    void m1();
}

// 인터페이스를 직접 구현한 클래스
public class Exam01 implements A {
    public void m1() {}
}

// 인터페이스를 간접 구현한 클래스
public class Exam02 extends Exam01 {
    public static void main(String[] args) {
        Exam02 obj = new Exam02();
        
        // Exam02는 Exam01이 구현한 A의 모든 메서드를 상속받는다.
        // 따라서 Exam02는 A 인터페이스를 구현한 것이 된다.
        A obj2 = obj; // ok
        obj2.m1();
    }
}

// 간접 구현 확인
public class Exam03 {
    public static void main(String) {
        A r1 = new Exam01(); // ok
        A r2 = new Exam02(); // ok
    }
}

인터페이스와 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
// 인터페이스가 가질 수 있는 메서드
public interface A {
    // 1. static 메서드
    static String m1() {
        return "안녕하세요!";
    }
    
    // 2. default 메서드
    default void m2(){}
    
    // 3. abstract 메서드
    void m3();
}

인터페이스는 abstract, default, static 메서드를 가질 수 있다.

  • static 메서드
    • 특정 인스턴스에 종속되지 않고 작업하는 경우 static 메서드로 정의한다.
    • 인스턴스 없이 수행하는 작업을 작성한다.
  • default 메서드
    • 기존 프로젝트에 영향을 미치지 않으면서 기존 규칙에 새 메서드를 추가할 때 유용하다.
    • 새 메서드는 새 프로젝트의 구현체가 오버라이딩할 테니 자세하기 정의하지 않아도 된다.
  • abstract 메서드
    • static 메서드나 default 메서드는 이미 구현된 메서드이기 때문에 abstract메서드가 아니다.

위의 인터페이스는 추상 메서드가 하나인데, 따라서 람다(lambda)로 구현할 수 있다.

람다란? 스트림과 함께 함수형 프로그래밍을 받아들이기 위해 Java8부터 추가된 문법이다. 람다의 핵심은 지울 수 있는 것은 모두 지우자는 것이다. 모든 걸 컴파일러의 추론에 의지하고 코드로 표현하는 건 다 없애버려 코드를 간결하게 만드는 것이다. 람다 표현식으로 구현이 가능한 인터페이스는 오직 추상 메서드가 1개뿐인 인터페이스만 가능하며 그렇기 때문에 추상 메서드가 1개인 인터페이스를 “함수형 인터페이스”라고 부르게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13

public class Exam0110 extends B implements A {
    // 추상 메서드는 반드시 구현해야 한다.
    @Override
    public void m3(){}
    
    // default 메서드는 구현해도 되고 안 해도 된다.
    @Override
    public void m2(){}
    
    // static 메서드는 구현할 수 없다.
}

인터페이스와 메서드 호출

수퍼 클래스와의 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface A {
    static String m1() {
        return "안녕하세요!";
    }
    default void m2(){}
  
    void m3();
}

public class B {
    static void x1() {
        System.out.println("B.x1() : 스태틱 메서드");
    }
    void x2() {
        System.out.println("B.x2() : 인스턴스 메서드");
    }
}

public class Exam0110 extends B implements A {
    @Override
    public void m3(){}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Exam0140 {
    public static void main(String[] args) {
        Exam0110 obj = new Exam0110();
        
        // 수퍼 클래스의 메서드 호출
        Exam0110.x1(); // 서브 클래스를 통해 수퍼 클래스의 스태틱 메서드를 호출할 수 있다. 
        B.x1(); //스태틱 메서드를 정의한 클래스를 통해 호출할 수 있다. 
        obj.x1(); // 서브 클래스의 레퍼런스를 통해 수퍼 클래스의 스태틱 메서드를 호출할 수 있다.
        obj.x2(); //수퍼 클래스의 인스턴스 메서드를 호출할 수 있다.
        
        // 인터페이스의 메서드 호출
        //Exam0110.m1(); //인터페이스를 구현한 클래스를 통해서는 인터페이스의 스태틱 메서드를 호출할 수 없다.
        //obj.m1(); //인터페이스의 스태틱 메서드를 호출할 수 없다.
        obj.m2();
        obj.m3();
    }
}
  • 수퍼 클래스의 메서드 호출
    • 서브 클래스를 통해 수퍼 클래스의 스태틱 메서드를 호출할 수 잇따.
    • 스태틱 메서드를 정의한 클래스를 통해 호출할 수 있다.
    • 서브 클래스의 레퍼런스를 통해 수퍼 클래스의 스태틱 메서드를 호출할 수 있다.
  • 인터페이스의 메서드 호출
    • 인터페이스를 구현한 클래스를 통해서는 인터페이스의 스태틱 메서드를 호출할 수 없다.
    • 인터페이스를 구현한 클래스의 레퍼런스를 통해 인터페이스의 스태틱 메서드를 호출할 수 없다.

인터페이스의 static 메서드

  • 인터페이스에서 스태틱 메서드는 보통 그 인스턴스를 구현한 객체를 다루는 일을 한다.

인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface CarCheckInfo {
    int getGas():
    int getBrakeOil();
    int getEngineOil();
    
    static boolean validate(CarCheckInfo carInfo) {
        if (carInfo.getBrakeOil() == 0 ||
            carInfo.getEngineOil() == 0 ||
            carInfo.getGas() == 0) {
            return false;
        }
        return true;
    }
}

인터페이스를 구현한 추상 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public abstract class Car implements CarCheckInfo {
    int gas;
    int brakeOil;
    int engineOil;
    
    String maker;
    String model;
    int cc;
    
    public String getMaker(){ return maker; }
    public void setMaker(){ this.maker = maker; }
    public String getModel(){ return model; }
    public void setModel(){ this.model = model; }
    public int getCc() { return cc; }
    public void setCc(int cc) { this.cc = cc; }
    
    @Override
    public int getGas() { return gas; }
    @Override
    public int brakeOil() { return brakeOil; }
    @Override
    public int getEngine() { return engineOil; }
    
    public void start() {
        System.out.println("시동 건다!");
    }
    public void shutDown() {
        System.out.println("시동 끈다!");
    }
}

추상 클래스를 상속받은 일반 클래스 (인터페이스를 간접 구현한다.)

1
2
3
4
5
6
public class Tico extends Car {
    @Override
    public void run() {
        System.out.println("붕붕~ 잘 달린다!");
    }
}

실행 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exam0110 {
    public static void main(String[] args) {
        Car c1 = new Tico();
        
        if (CarCheckInfo.validate(c1)) {
            c1.start();
            c1.run();
            c1.shutdown();
        } else {
            System.out.println("자동차를 점검하시기 바랍니다. 문제 발생!");
        }
    }
}

인터페이스 성격 자체가 규칙이니까 굳이 클래스의 인스턴스 변수를 통해서 인터페이스의 스테틱 메서드를 호출하는 것을 허용할 필요는 없다고 여겼을 것이다. 헷갈리지 않기 위해서 차단한 거라고 생각하자..

v24. 인터페이스를 활용한 객체 사용 규칙 정의

인터페이스란?

  • 객체 간의 사용 규칙을 정의할 때 사용하는 문법이다.
  • 즉 호출할 때 어떤 이름의 메서드를 어떤 파라미터로 호출해야 하는 지 정의한 것이다.
  • 구체적인 동작은 클래스가 구현한다.
  • 호출자와 피호출자 간의 직접적인 종속성을 제거할 수 있어 유지보수에 좋다.

인터페이스에 대한 추상클래스의 활용

  • 인터페이스에 정의된 규칙이 많을 경우, 그 많은 메서드를 직접 구현하는 일은 번거롭다.
  • 그래서 추상 클래스 를 통해 일부 메서드를 구현하고, 나머지는 서브 클래스에게 맡기는 프로그래밍 기법을 많이 사용한다.

실습 내용

  • 데이터 목록을 다루는 객체의 사용 규칙을 List 인터페이스로 정의한다.
  • AbstractList 추상 클래스가 List 인터페이스를 구현한다.
  • XxxHandler에서 데이터 목록을 가리키는 의존 객체를 지정할 때 클래스 대신 인터페이스를 사용한다.

1단계: 데이터 목록을 다루는 객체의 사용 규칙을 인터페이스로 정의한다.

데이터 목록을 다루는 객체의 사용 규칙을 따로 정의한다. 여기서 사용 규칙이란 호출할 메서드의 시그니처를 말한다. 사용 규칙이기 때문에 모든 메서드는 추상 메서드이고, 규칙은 공개되어야 하기 때문에 모든 메서드는 public이다.

1
2
3
4
5
6
7
8
9
// 데이터의 목록을 다루는 객체의 사용 규칙을 따로 정의한다.
public interface List<E> {
    public /*abstract*/ boolean add(E e);
    /*public*/ abstract E get(int index);
    E set(int index, E element);
    Object[] toArray();
    E[] toArray(E[] arr);
    int size();
}

추상 클래스

추상 메서드는 서브 클래스에 구현을 강제하는 문법이다. 메서드 구현을 강제한다는 것은 해당 객체가 반드시 갖춰야 할 기능을 선언하는 것, 즉 객체의 사용법(사용 규칙)을 명시하는 효과를 얻는다. 객체의 구현 방법에 영향을 받지 않는 메서드를 따로 분리하여 별도의 사용 규칙으로 정의하게 되면 좀 더 유연하고 교체하기 쉬운 구조로 코드를 관리할 수 있다.

사용 방법

  • 추상 클래스를 정의한 다음 추상 메서드를 설정한다.
  • 추상 메서드는 인터페이스 문법을 사용하여 별도로 분리한다.
  • 클래스에서 사용 규칙(메서드 호출 규칙)을 따로 분리해서 정의해두면 좀 더 다양한 하위 클래스를 정의하고 다룰 수 있다.
  • 추상 메서드가 대부분인 경우 인터페이스로 정의하는 것이 낫다.
    • 추상 클래스의 목적이 하위 클래스에게 공통 필드나 메서드를 상속해주는 것인데, 대부분의 메서드가 추상 메서드라서 서브 클래스 입장에서 딱히 도움이 안 되는 상황일 때 그냥 메서드 사용 규칙만 정의하는 것이 코드 관리에 낫기 때문이다.

구현(implement)

  • 구현이란 메서드의 몸체(body)를 정의하는 것, 즉 메서드에 코드를 정의하는 것을 말한다.
  • 다시 말해, 메서드가 해야 할 일이 무엇인지 코드를 정의하는 것이다.
    • 추상 메서드: abstract void m1();
    • 메서드 구현: void m1();

2단계: List인터페이스를 구현하도록 AbstractList를 변경한다.

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class AbstractList<E> implements List<E> {
    // 서브 클래스가 직접 인스턴스 필드를 사용할 수 있게 한다.
    protected int size;
    
    @Override
    public int size() {
        return size;
    }
    
    // 나머지 메서드는 서브 클래스에서 구체적으로 구현해야 하기 때문에
    // 추상 메서드인 채로 남긴다.
}

3단계: xxxHandler에서 의존하는 데이터 객체 목록의 타입을 인터페이스로 변경한다.

목록을 다루는 객체를 저장할 때 특정 클래스(AbstractList, LinkedList, ArrayList)를 지정하는 대신 사용 규칙(List)를 지정함으로써 더 다양한 타입의 객체로 교체할 수 있게 된다. 즉 List 규칙을 따르는 객체라면 어떤 클래스의 객체든지 사용할 수 있다. 따라서 유지보수를 유연하게 한다.

1
2
3
4
5
6
7
public class BoardHandler {
    List<Board> boardList;
    
    public BoardHandler(List<Board> list) {
        this.boardList = list;
    }
}

왜 인터페이스를 파라미터로 많이 쓸까? 인터페이스 레퍼런스를 파라미터로 사용할 경우 인터페이스의 구현체라면 뭐든지 아규먼트로 넘겨줄 수 있기 때문이다. 따라서 실무에서는 확장성과 유연성을 위해서 인터페이스를 파라미터로 많이 쓴다. (유에스비라면 실제 제품이 중국산이든지 한국산이든자 꽂기만 하면 연결되는 것과 같음. 교체 용이)

싱글톤 패턴

  • 인스턴스를 한 개만 생성하여 공유하고 싶다면, Singleton 설계 방식으로 클래스를 정의한다.
  • 만약 API를 사용할 때 생성자가 private으로 막혀 있다면 클래스의 static 메서드인 getInstance 를 찾아보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Car {
    String model;
    int cc;
    // 인스턴스 주소를 받을 클래스 필드
    private static Car instance;
    
    // 1. 생성자를 외부에서 호출할 수 없도록 private으로 정의
    //    => 내부에서만 사용 가능 
    private Car(){}
    
    // 2. 인스턴스를 생성해주는 메서드 정의
    public static void getInstance(){
    	if (instance == null) {
            // 인스턴스 생성한 적이 없다면 즉시 인스턴스 생성
            instance = new Car();
        }
        return instance;
    }
}

public class Test01 {
    public static void main(String[] args) {
        //Car c1 = new Car(); => 컴파일 오류!
        
        // 인스턴스 생성해주는 메서드를 통해 인스턴스 얻는다.
        Car c1 = Car.getInstance();
        Car c2 = Car.getInstance();
        System.out.println(c1 == c2); //true
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Kimbap {
    String bap;
    String kim;
    private static Kimbap instance;
    
    private Kimbap(){}
    
    public static Kimbap getInstance() {
        if (instance == null)
            instance = new Kimbap();
        return instance;
    }
}
This post is licensed under CC BY 4.0 by the author.

:book: 자바의 정석 #7.4: 제어자

:book: 자바의 정석 #7.5: 다형성

Loading comments from Disqus ...