자바 스터디 3회차 준비

20 minute read

아래는 6월 6일 진행 할 스터디 준비 내용이다.

  • 3주차 목차
    • 객체지향 프로그래밍 2
      • 상속
      • 클래스 간의 관계 - 포함관계
      • 클래스 간의 관계 결정
      • 단일 상속
      • Object 클래스
      • Overriding
        • Overriding 조건
      • super
      • super()
      • 패키지
      • 클래스패스
      • import문
      • static import문
        • System.out.println()
      • 제어자 (modifier)
        • 접근 제어자 (access modifier)
        • 그 외 제어자
          • static
          • final
          • abstract
          • native
          • transient
          • synchronized
          • volatile
          • strictfp
      • 다형성
      • 추상화
      • 인터페이스
        • 인터페이스의 상속
        • 인터페이스의 구현
        • 인터페이스의 장점
        • extends, implements
        • default 메서드
        • static 메서드
        • private 메서드
      • 내부 클래스
      • 익명 클래스
    • 2주차 과제
      • Overloadding, Overriding 차이점
      • 추상클래스, 인터페이스 차이점

1. 객체지향 프로그래밍 2

1-1) 상속

상속이란 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.
이를 통해 코드의 재사용성을 높이고, 코드의 중복을 줄일 수 있어 생산성과 유지보수에 유리해진다.

상속을 구현하는 방법은 간단하다.

public class Person {}

public class Me extends Person {}

상속받을 클래스에 뒤에 extends (확장) 키워드를 붙이고, 상속할 클래스 이름을 쓴다.

error

상속관계는 보통 위와 같이 Me 클래스가 Person 클래스를 상속받았다는 것을 나타낸다.

public class Person {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
	
	
}

public class Me extends Person {
    void work() {
        System.out.println("출근준비 합시다...");
    }
}

public class Main {
    public static void main(String[] args) {
        Me me = new Me();
        me.setAge(10);
        me.work();

        Person person = new Person();
        person.setAge(100);
        person.work(); // 에러발생
    }
}

Me 클래스에선 Person 클래스의 멤버에 접근할 수 있다.
그러나 Person 클래스는 Me 클래스의 멤버에 접근하지 못한다.

위 코드를 그림으로 나타내면 이해가 조금 더 쉽게 될 것 이다.

error


1-2) 클래스 간의 관계 - 포함관계

클래스 간 관계는 상속 말고도 포함관계가 있다.
포함관계란 이런 것을 의미한다.

public class Users {

    private int age;
    private String name;
    private String streetAddress;
    private String city;
    private String zipCode;

}

위는 유저에 대한 정보를 담은 클래스이다.
자세히 보면 streetAddress, city, zipCode는 주소에 관련된 것을 알 수 있다.
이런 경우 클래스를 분리해서 작성할 수 있다.

public class Users {

    private int age;
    private String name;
    private Address address;

}

public class Address {

    private String streetAddress;
    private String city;
    private String zipCode;

}

위와같은 관계를 포함관계라 한다.


1-3) 클래스 간의 관계 결정

자 그럼 클래스 간의 관계로 상속과 포함을 알아봤다.
그럼 언제 어떻게 쓰는지 알아보도록 하자.

상속 - 자식클래스 is a 부모클래스
포함 - 자식클래스 has a 부모클래스

예를들어 유저는 주소를 가지고 있다.
그렇기 때문에 위 예제는 포함관계가 더 어울린다.

또한 자바는 프로그래밍 언어이다.
그렇기 때문에 상속 관계가 더 어울린다.


1-4) 단일 상속

C++에서는 부모 클래스를 여러개 가질 수 있다.
덕분에 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있다.
그러나 자바는 단 하나의 클래스만을 상속할 수 있다.

public class Java extends ProgrammingLanguage, OOP {} // 에러 발생

다중상속을 하지 못하도록 한 이유는 다음과 같다.

여러개의 클래스를 상속받았다.
그 클래스들은 동일한 이름의 메서드가 있다.
과연 자식클래스는 이러한 메서드를 사용할 때 어떤 클래스의 메서드를 사용할까?

이런 상황에서 메서드를 구분할 수 있는 방법은 쉽지 않을 것이다.

이러한 단점을 극복하기 위해 자바는 다중상속을 포기하고, 단일상속만을 허용한다.

대신 단일상속으로 인해 클래스 간의 관계가 보다 명확하고, 코드를 신뢰할 수 있게 만들어준다는 장점이 있다.


1-5) Object 클래스

Object 클래스는 최상위 부모클래스이다.
상속을 받도록 따로 작성하지 않은 클래스는 컴파일러에 의해 Object 클래스를 상속받는다.

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

    }
}

// 바이트 코드

public class Inheritance {
  public Inheritance();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: return
}

여기서 주의깊게 봐야하는 부분은 invokespecial이다.
invokespecial는 생성자, private 메서드, 부모 클래스의 메서드 호출을 담당하는데,
우측 주석을 보면 Object 클래스의 메서드 호출한다.
즉 Object 클래스가 부모 클래스라는 것이다.

조금 더 직관적으로 확인해보자.

public class Inheritance {
    public static void main(String[] args) {
        System.out.println(Inheritance.class.getSuperclass());
    }
}

// class java.lang.Object

위와 같은 기법을 reflection이라 한다.
객체를 통해 실행중인 클래스의 정보를 분석해 낼 수 있다.
super클래스는 Object라 나온다.

error

상속받은 클래스는 없지만 Object 클래스의 메서드들을 Override 할 수 있는 것이 보인다.

지금까지 자주 사용해왔던 equals(), toString()과 같은 메서드는 기본적으로 제공하는 메서드가 아닌
Object 클래스를 암묵적으로 상속 받고 있기 때문에 사용할 수 있는 메서드라는 것을 확인 할 수 있다.


1-6) Overriding

오버라이딩은 부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 말한다.

예를들어 곰탕을 파는 음식점이 있다.
사골 만둣국을 신메뉴로 출시하려한다.

만둣국 레시피를 새롭게 만드는 것보단 이미 사골에 대한 레시피를 가지고 있으니, 기존 레시피에서 조금 수정하고자 한다.

사골 클래스에서 recipe 메서드를 호출했을 때 레시피가 출력됬으니,
만둣국 클래스도 마찬가지일 것을 기대하기 때문이다.

이렇게 상속받은 클래스에서 자신의 클래스에 맞게 메서드를 재정의 하는 것을 오버라이딩이라고 한다.

public class Sagol {
	
    public String recipe() {
        return "사골을 끓인다.";
    }
	
}


public class SagolMandooGook extends Sagol {

    @Override
    public String recipe() {
        return "사골을 끓인다. 만두를 넣는다.";
    }

}


public class Main {
    Sagol sagol = new Sagol();
    SagolMandooGook sagolMandooGook = new SagolMandooGook();

    System.out.println(sagol.recipe());
    System.out.println(sagolMandooGook.recipe());
}

오버라이딩 한 메서드를 보면 @Override 라는 문장이 붙어있다.
Annotation 이라는 것인데, 주석이다.
이 주석은 기능이 있을 수도, 없을 수도 있는데,
@Override 는 실질적으로 기능은 가지고 있지 않으나,

컴파일러에게 이 메서드는 오버라이딩 되었다는 것을 알려주는 역할을 한다.

그렇기 때문에 오버라이딩 조건에 만족하지 않는 메서드에 @Override가 붙어있으면 컴파일 에러가 발생한다.

이 Annotation은 붙이던 말던 자유이긴 하나, 더 안전한 코딩을 할 수 있기에 붙이는 것을 더 권장한다.

public class SagolMandooGook extends Sagol {

    @Override
    public String mandoogookRecipe() {    // 컴파일 에러
        return "사골을 끓인다. 만두를 넣는다.";
    }

}

1-6-1) Overriding 조건

오버라이딩은 메서드의 내용을 재정의 하는 것이다.
메서드의 내용은 구현부에 작성한다고 했었다.
그렇기때문에 메서드의 선언부는 부모 클래스의 선언부와 완전히 동일해야한다.

다만 예외적인 상황이 있다.

  1. 접근제어자는 부모클래스의 메서드보다 넓은 범위로는 변경 가능하다.
  2. 부모 클래스의 메서드보다 많은 수의 예외는 선언할 수 없다.

1-7) super

멤버변수와 지역변수의 이름이 같을 때 this를 사용했었다.
상속받은 멤버와, 자신의 멤버의 이름이 같을 때 super를 통해 구분/사용할 수 있다.

먼저 멤버 변수를 구분하는 예제이다.

public class Parent {

    int x = 10;

}

public class Child extends Parent {

    int x = 100;

    public void print() {
        System.out.println("x = " + x);
        System.out.println("this.x = " + this.x);
        System.out.println("super.x = " + super.x);
    }

}

public class Main {
    
    public static void main(String[] args) {
        Child child = new Child();
        child.print();
    }   
	
}

// x = 100
// this.x = 100
// super.x = 10

x와 this.x 는 Child 클래스의 멤버변수 x를 출력했고, super.x 는 Child 클래스의 부모 클래스인 Parent 클래스의 x를 출력했다는 것을 알 수 있다.


다음은 사용이다.
사골 예시를 다시 확인해보자.
레시피 중 “사골을 끓인다.” 는 이미 부모 클래스에 정의 되어있다.
그대로 가져와 아래처럼 사용할 수 있다.

public class Sagol {
	
    public String recipe() {
        return "사골을 끓인다.";
    }
	
}


public class SagolMandooGook extends Sagol {

    @Override
    public String recipe() {
        return super.recipe() + " " + "만두를 넣는다.";
    }

}


public class Main {
    Sagol sagol = new Sagol();
    SagolMandooGook sagolMandooGook = new SagolMandooGook();

    System.out.println(sagol.recipe());
    System.out.println(sagolMandooGook.recipe());
}

1-8) super()

this()는 자신의 클래스의 생성자를 호출하는 키워드였다.
super()는 부모 클래스의 생성자를 호출하는 키워드이다.

다음과 같이 유저 클래스가 있다.
기간이 지나고 관리자 클래스가 추가되었다.
둘의 기능과 속성을 동일하다고 생각해보자.

이때 super() 를 사용하여 부모 클래스의 생성자인 Users를 호출해,
Admin 클래스에 별다른 내용 없이도,
Users 클래스와 동일한 생성자 효과를 낼 수 있다.

public class Users {

    private String id;
    private String password;

    public Users(String id, String password) {
        this.id = id;
        this.password = password;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User Login\n"
                + "id : " + id + "\n"
                + "pw : " + password;
    }

}


public class Admin extends Users {

    public Admin(String id, String password) {
        super(id, password);
    }

    @Override
    public String toString() {
        return "Admin Login\n"
                + "id : " + super.getId() + "\n"
                + "pw : " + super.getPassword();
    }

}


public class Main {

    public static void main(String[] args) {
        Admin admin = new Admin("LEE", "1234");
        System.out.println(admin.toString() + "\n");
        
        Users users = new Users("GICHEOL", "9876");
        System.out.println(users);
    }
	
}


1-9) 패키지

패키지는 클래스의 묶음이다.
서로 관련된 클래스끼리 그룹 단위로 묶는 작업을 통해 클래스를 더욱 효과적으로 관리할 수 있다.
패키지를 구분함으로써 다른 패키지에 있는 같은 이름의 클래스와 구분을 할 수 있다.

우리가 자주 사용하는 String 클래스도 java.lang 패키지에 속해있다.

우리는 지금까지 컴퓨터에 수많은 파일을 관리하기 위해 폴더를 생성했었다.
그 폴더는 대부분 관련있는 파일들끼리 묶어서 보관했을 것이다.

다른 폴더끼리는 같은 이름의 파일이 있어도 상관이 없으며, 원하는 파일을 찾기 쉽도록 일 것이다.

패키지는 폴더와 매우 흡사하다.


패키지 선언을 알아보자.

package 패키지명;

매우 간단하다.
패키지 선언의 규칙으로는 항상 소스파일의 첫번째 문장이어야하며,
소스파일에는 항상 하나의 패키지가 선언되어야 한다.
또한 소스파일 내 단 한번만 선언될 수 있다.

패키지명의 컨벤션은 보통 클래스명과 구분이 쉽도록하기 위해 소문자로 작성한다.

여기서 소스파일 내 항상 하나의 패키지가 선언되어야 한다고 했는데,
패키지 없이 src 밑에 바로 클래스 만들어 본 경험이 있을 것 이다.
이는 패키지없이 클래스를 생성하면 default package를 만들어 주기 때문이다.
가상의 폴더를 만들어 한 곳에서 관리한다고 생각하면 쉬울 것이다.


1-10) 클래스패스

클래스 패스 참고


1-11) import문

원칙적으로 코드를 작성할 때 Scanner, Arrays, List, Map 등 다른 패키지의 클래스를 사용하기 위해선
아래와 같이 패키지명을 포함한 클래스 이름을 작성해야한다.

public class Main {
    public static void main(String[] args) {
        int[] arr = { 5, 10, 15 };
        System.out.println(java.util.Arrays.toString(arr));

        java.util.Scanner scanner = new java.util.Scanner(System.in);
    }
}

너무나도 읽기 싫고 쓰기 싫게 생겼다..
이러한 불편함을 해결하기위해 import문이 존재한다.
클래스명 앞의 패키지명을 똑 잘라서 패키지가 선언된 코드 아래에 import문을 선언한다.

package something;

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int[] arr = { 5, 10, 15 };
        System.out.println(Arrays.toString(arr));

        Scanner scanner = new Scanner(System.in);
    }
}

이클립스의 경우 cmd + shift + o (윈도우의 경우 ctrl + shift + o) 단축키를 사용하면 자동으로 전체 import문을 추가/제거 해준다.

인텔리제이의 경우 한번에 import는 못하고, option + enter 를 통해 자동 import를 할 수 있다.

위와 같이 java.util과 같은 패키지는 상당히 자주 쓰이는 패키지라
import문이 소스코드의 많은 양을 차지할 수 있다.

이러한 경우 * 기호를 사용할 수 있다.

package something;

import java.util.*;

public class Main {
    public static void main(String[] args) {
        int[] arr = { 5, 10, 15 };
        System.out.println(Arrays.toString(arr));

        Scanner scanner = new Scanner(System.in);
    }
}

이전의 코드는 컴파일러가 java.util 안에 필요한 클래스를 직접 찾아야하는 수고가 생기지만 성능차이는 없다.
인텔리제이의 경우 기본 세팅으로 3개 이상 동일 패키지에서 import가 있으면 * 처리를 해준다.


1-12) static import문

static 메서드를 호출할때 클래스이름.메서드이름() 으로 호출했던것이 기억이 날 것이다.
static import문을 사용하면, 클래스이름을 뗄 수 있다.

사용법은 import문과 거의 동일하다.
그저 import 뒤에 static을 붙일 뿐이다.

java.lang 패키지의 대표적인 static 클래스는 Math 클래스이다.
수학과 관련된 많은 메서드가 담겨져 있다.

package something;

public class Main {

    public static void main(String[] args) {
        int x = 10;
        
        System.out.println(Math.pow(x, 2));
    }
	
}

기존의 static 메서드를 사용하는 방식이다.

static import문을 사용하면 아래와 같이 사용할 수 있다.

package something;

import static java.lang.Math.pow;
import static java.lang.System.out;

public class Main {

    public static void main(String[] args) {
        int x = 10;
        
        out.println(pow(x, 2));
    }
	
}

static import문을 통해 코드를 더욱 간략화 할 수 있다.

여기서 잠깐 System.out.println() 을 알아보자.


1-12-1) System.out.println()

출력 메서드는 지금까지 엄청나게 많이 사용했지만, 어떤 구조인지 잘 몰랐을 것이다.
심지어 구조가 조금 특이하다는 것을 알 수 있다.

위에서 본대로 out은 System 클래스의 static 메서드이다.
뒤에 괄호가 없으니 변수일텐데, 그 뒤로 또 . 이 붙고 메서드가 붙는다.

System 클래스와 out을 확인해보자.

public static final PrintStream out = null;

PrintStream 타입의 참조변수 out이라는 것을 알 수 있다.
PrintStream을 확인해보자.

public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable {
    
        ...
    
    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}

FilterOutputStream을 상속받고, Appendable, Closeable 인터페이스를 구현한 클래스이다.
System.out.println은 System 클래스에 선언된
out이라는 참조변수 즉 PrintStream의 println 메서드를 호출한 것이다.

자주 쓰이는 만큼 어떤 구조인지 잘 알고 있다면 좋을 것 같다.


1-13) 제어자 (modifier)

제어자는 클래스, 변수, 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여한다.
크게 접근제어자와 그 외의 제어자로 나뉘어진다.

종 류 키워드
접근 제어자 public, protected, default, private
그 외 static, final, abstract, native, transient, synchronized, volatile, strictfp

접근제어자는 하나에 대상에 대해 단 하나만 사용할 수 있지만,
그 외의 제어자는 다양하게 조합해서 사용할 수 있다.
또한 그 외의 제어자는 static, final, abstract, synchronized를 제외하고는 쉽게 보기 힘든 제어자이니 너무 부담 가지지 말도록 하자.

1-13-1) 접근 제어자 (access modifier)

접근 제어자는 멤버 또는 클래스에 사용된다.
해당 대상에 대해 외부에서 접근하지 못하도록 제한하는 역할을 한다.

종 류 특 징
public 접근 제한이 전혀 없다.
protected 같은 패키지 내에서, 그리고 다른 패키지의 자식 클래스에서 접근 가능하다.
default 같은 패키지 내에서만 접근 가능하다.
private 같은 클래스 내에서만 접근 가능하다.

위에서부터 아래로 접근 범위가 넓은쪽에서 좁은쪽이다.
default는 붙이지 않는다.
오히려 붙인다면 에러가 발생하니
접근 제어자가 아무것도 붙지 않은 클래스 혹은 메서드, 멤버변수, 생성자는
default가 붙었다고 생각하도록 한다.

package modifier;

public class Person {

	private int age;
	protected String name;
	
	
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public String getName() {
		return greeting(name);
	}
	public void setName(String name) {
		this.name = name;
	}
	
	
	private String greeting(String name) {
		return name + "님 안녕하세요!";
	}
}


package modifier;

public class Me extends Person {

    String hello() {
        return "좋은 아침입니다~";
    }
	
}


package modifier;

public class Main {

    public static void main(String[] args) {
    	Me me = new Me();
        me.age = 26;                        // 사용불가
    	me.name = "LEEGICHEOL";             // 사용가능
    	
    	System.out.println(me.getName());   // 사용가능
    	System.out.println(me.hello());     // 사용가능
    	System.out.println(me.greeting());  // 사용불가
    }
    
}


package something;

public class Something {
    public static void main(String[] args) {
        Me me = new Me();
        me.age = 26;                        // 사용불가
        me.name = "LEEGICHEOL";             // 사용불가

        System.out.println(me.getName());   // 사용가능
        System.out.println(me.hello());     // 사용불가
        System.out.println(me.greeting());  // 사용불가
    }
}


package something;

public class Anything extends Person {
    public static void main(String[] args) {
        Me me = new Me();
        me.age = 26;                        // 사용불가
        me.name = "LEEGICHEOL";             // 사용가능

        System.out.println(me.getName());   // 사용가능
        System.out.println(me.hello());     // 사용불가
        System.out.println(me.greeting());  // 사용불가

        Anything anything = new Anything();
        anything.name = "EKKKK1";           // 사용가능
    }
}

Person 클래스의 변수 age는 접근제어자가 private 이므로 같은 클래스가 아닌 이상 어디서도 접근할 수 없다.
greeting 메서드도 마찬가지이다.

반면 protected인 변수 name은 같은 패키지인 Main 클래스에서 사용할 수 있다.
Something 클래스는 다른 패키지기 때문에 name을 사용 못했으며,
Anything 클래스는 다른 패키지이지만 Person을 상속받았기 때문에 name을 사용할 수 있다.

Me 클래스의 hello()는 default이기 때문에 같은 패키지가 아니라면 사용하지 못한다.

마지막으로 Person의 getName()은 public이다.
어디서든 사용할 수 있다.


1-13-2) 그 외 제어자

static

static은 클래스의 또는 공통적인의 의미를 가진다.
우리가 지금까지 써왔던 static이다.

final

final은 마지막의 또는 변경될 수 없는의 의미를 가진다.

변수에 사용된다면 값을 변경하지 못하는 상수가 되고,
메서드에 사용한다면 오버라이딩을 할 수 없으며,
클래스에 사용되면 해당 클래스를 상속받지 못한다.

abstract

abstract는 미완성의 의미를 가진다.
추상 클래스, 추상 메서드를 선언하는데 사용된다.

native

자바 API를 보면 native라는 키워드를 볼 수 있다.
해당 키워드가 붙은 메서드는 자바에서 구현하기 까다로운 메서드를
C나 C++과 같은 다른 언어를 사용해 구현한 것이다.

protected native Object clone() throws CloneNotSupportedException;

clone은 배열을 다른 배열로 복사할때 사용하는 Object 클래스의 메서드이다.

코드의 구현부가 보이지 않기 때문에 추상 메서드로 오해하지 말도록 하자.

transient

transient는 변수앞에 사용되며 직렬화, 역직렬화를 제외하는 키워드이다.
참고사항으로 직렬화는 대략 이런 것 이다.
유저 정보를 DB가 아닌 파일화 시켜 저장한다.
그러기 위해서 객체를 byte 타입으로 변환시키는 작업이고,
역직렬화는 직력화된 파일을 다시 역으로 직렬화시켜 객체로 만드는 것이다.

synchronized

synchronized는 동기화 키워드이다.
asynchronized의 반대되는 개념이다.

자바에서 synchronized는 Multi-Thread에 인한 동시 접근을 막는다.
synchronized를 통해 Multi-Thread 환경에서 Thread-Safe하게 작업할 수 있다.

volatile

Multi-Thread 환경에서 변수의 작업은 성능상의 이유로 CPU Cache를 사용한다.
volatile 키워드가 붙은 변수는 CPU Cache가 아닌 Main Memory 에 저장되어 읽고 쓰여진다. 

strictfp

플랫폼 간 부동소수점 연산의 결과를 동일하게 해주는 접근제어자이다.

public strictfp class Main {
    public static void main(String[] args) {
        double x = 0.0;
    	
    	for (int i = 0; i < 10; i++) {
            x += 0.1;
        }
    	
    	System.out.println(x);
    }
}

0.1을 10번 더하면 1.0이 나와야 하지만
0.9999999999999999이 나온다.
컴퓨터는 실수또한 2진수로 표현하다보니,
어쩔수 없이 버리는 수가 있기 때문에 발생하는 일인데,
strictfp 키워드를 통해 부동소수점을 해결할 수 있다고 한다.

근데 해결은 안됬다. 방법을 모르겠다.


1-14) 다형성

다형성 참고


1-15) 추상화

추상화 참고


1-16) 인터페이스

인터페이스는 추상 클래스보다 더 추상화의 정도가 높은 일종의 추상 클래스이다.

추상 클래스의 경우 가질 수 있는 멤버로는,
추상 메서드 말고도, 변수와 상수, 생성자, 게다가 일반적인 메서드도 가질 수 있다.

그러나 인터페이스는 오로지 추상 메서드와 상수만을 가질 수 있다.

추상클래스가 미완성 설계도라면, 인터페이스는 아직 아무것도 그려지지 않은 기본 설계도로 볼 수 있다.

인터페이스를 선언하는 방법은 클래스를 선언하는 방법과 동일한데,
class 대신 interface 키워드를 사용한다.
헷갈릴 수 있는 건 추상 클래스는 class 앞에 abstract을 붙이는데 인터페이스는 class 대신 interface이다.

public interface Something {}

또한 인터페이스의 멤버들은 아래와 같은 제약사항이 있다.

  • 모든 멤버변수는 public static final 이어야 한다. (생략 가능)
  • 모든 메서드는 public abstract 이어야 한다. (생략 가능)
    • 단 JDK 1.8 부터 추가된 static 메서드와 default 메서드는 제외

이러한 제약사항은 인터페이스의 멤버라면 필수적으로 적용되는 사항이기 때문에,
생략 가능하며, 보통 생략해서 사용한다.

public interface Something {

    int number = 1;

    int sum(int num, int num2);
 
}

error

컴파일러에 의해 상수는 public static final, 메서드는 public abstract 키워드가 붙은걸 알 수 있다.

추상 클래스는 이와 같은 생략이 불가능하다는 점을 주의하자.

추상 클래스에서 생략이 불가능한 이유는
변수와 상수, 그리고 일반 메서드와 추상 메서드를 각각 구분하기 위해서이다.


1-16-1) 인터페이스의 상속

클래스의 상속과 인터페이스의 상속은 방법은 동일하나, 차이점이 몇 가지 있다.

  1. 인터페이스는 인터페이스로부터만 상속 받을 수 있다.
  2. 인터페이스는 다중상속이 가능하다.
  3. Object와 같은 최고 조상이 없다.
public interface A {}
public interface B {}

public interface Something extends A, B {}

1-16-2) 인터페이스의 구현

반대로 인터페이스와 추상클래스의 공통점은 자기 자신을 객체로 만들지 못한다는 점 이다.

public abstract class A {}
public interface B {}

public class Main {
    public static void main(String[] args) {
        A a = new A();  // 컴파일 에러
        B b = new B();  // 컴파일 에러
    }
}

추상클래스를 사용하기 위해 extends를 사용해서 상속받을 클래스를 정의하는 것과 비슷하게,
인터페이스는 implements 키워드를 사용한다.

public interface A {}

public class Main implements A {}

implements는 여러개의 인터페이스에 대해 사용할 수 있고, extends까지 같이 사용할 수 있다.

public abstract class Creature {

    abstract void breath();

}


public abstract class Mammal extends Creature {

    @Override
    void breath() {
        System.out.println("포유류는 폐를 통해 호흡한다.");
    }

    public abstract void baby();

}


public abstract class Fish extends Creature {

    @Override
    void breath() {
        System.out.println("물고기는 아가미로 호흡한다.");
    }

    public abstract void egg();

}


public interface Talkable {
    void talk();
}

public interface Swimming {
    void swim();
}


public class Human extends Mammal implements Talkable, Swimming {

    public Human(String name) {
        System.out.println(name);
    }

    @Override
    public void baby() {
        System.out.println("인간은 아이를 낳는다.");
    }

    @Override
    public void talk() {
        System.out.println("인간은 대화를 할 수 있다.");
    }

    @Override
    public void swim() {
        System.out.println("인간은 수영을 할 수 있다.");
    }

}


public class GoldFish extends Fish implements Swimming {

    public GoldFish(String name) {
        System.out.println(name);
    }

    @Override
    public void egg() {
        System.out.println("물고기는 대부분 알을 낳는다.");
    }

    @Override
    public void swim() {
        System.out.println("물고기는 수영을 한다.");
    }
}



public class Main {

    public static void main(String[] args) {
        Human human = new Human("인간");
        human.breath();
        human.baby();
        human.talk();
        human.swim();

        System.out.println("==========");

        GoldFish goldFish = new GoldFish("금붕어");
        goldFish.breath();
        goldFish.egg();
        goldFish.swim();

    }

}

위 코드를 그림으로 나타내면 아래와 같다.

error


1-16-3) 인터페이스의 장점

인터페이스를 사용함으로써 얻을 수 있는 장점은 아래와 같다.

  1. 표준화가 가능하다.
    • 먼저 기본틀을 만들어두고 개발을 하기 때문에 표준화를 시킬 수 있다.
  2. 개발시간을 단축시킬 수 있다.
    • 선언과 구현을 분리시켰기 때문에 업무를 분업화 하기 좋아짐으로, 개발 시간을 단축할 수 있다.
  3. 서로 관계없는 클래스들의 관계를 맺을 수 있다.
    • 서로 상속 관계가 아닌 클래스간의 관계를 맺을 수 있다.
  4. 독립적인 프로그래밍이 가능하다.
    • 선언과 구현을 분리시켰기 때문에 어떤 한 클래스의 변경사항이 다른 클래스에 영향을 미치지 않는다.

1-16-4) extends, implements

extends는 확장한다는 의미로, 상속을 받을 때 사용된다.
implements는 구현한다는 의미로, 인터페이스를 구현할 때 사용된다.
그렇기때문에 인터페이스 간의 상속에도 extends를 사용하는 것이다.


1-16-5) default 메서드

기존 인터페이스는 무조건 상수와 추상 메서드만을 사용할 수 있었다.
그러나 JDK 1.8 부터 추가된 default 메서드를 사용함으로써
일반 메서드를 사용할 수 있게 되었다.

이렇게 추가된 이유는 아래와 같다.
부모 클래스에 추상 메서드를 새로 추가한다는 것은 상당히 큰 일이다.
일반 메서드의 경우 그저 새로운 메서드가 추가되는 것 뿐이지만,
추상 메서드는 자식 클래스가 반드시 구현을 해줘야하기 때문이다.

인터페이스의 경우 메서드가 추가된다는 것은 항상 추상 메서드가 추가 되는 것이기 때문에,
해당 인터페이스를 구현한 모든 클래스에 새로운 메서드를 추가 구현해야한다.

인터페이스 설계를 아무리 잘해도 언젠가 추가/수정 될 가능성은 항상 있기 때문에,
default 메서드가 추가되었다.

default 메서드는 일반 메서드이며, 그렇기에 구현은 필수가 아니다.

public interface Something {

    void method();
    
    default void defaultMethod() {
        System.out.println("Default Method");
    }

}

public class Anything implements Something {

    public void method() {
        System.out.println("필수로 구현해야 한다.");
    }

}

public class Main {
    public static void main(String[] args) {
        Anything anything = new Anything();
        anything.method();
        anything.defaultMethod();
    }
}

여기서 주의할 점은 default 메서드는 접근제어자 default와 다르다.
default 메서드의 접근제어자는 기본적으로 public이며, 마찬가지로 생략가능하다.

// class version 55.0 (55)
// access flags 0x601
public abstract interface Something {

  // compiled from: Something.java

  // access flags 0x401
  public abstract method()V

  // access flags 0x1
  public default defaultMethod()V       // <- public이 접근제어자이다.
   L0
    LINENUMBER 6 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "Default Method"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 7 L1
    RETURN
   L2
    LOCALVARIABLE this LSomething; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

만약 메서드 이름이 중복되어 충돌되는 상황이 발생한다면
  1. 클래스에서 default 메서드를 재정의했다면 첫번째 우선 순위이다.
  2. 상속구조의 인터페이스에서 자식 인터페이스가 부모 인터페이스의 default 메서드를 재정의했다면 두번째 우선 순위이다.
  3. 우선 순위가 없는 경우라면 구현 클래스에서 오버라이딩을 해주면 된다.
    • 이 경우 선언할 때 명시적으로 인터페이스명.super.메서드명 으로 선언한다.

1-16-6) static 메서드

인터페이스는 단독적으로 객체를 만들 수 없다.
그러나 static 메서드는 객체를 만들지 않고 사용하기 때문에
인터페이스에서 사용하는 것이 이상할 이유가 없다.

그러나 자바 진영에선 인터페이스의 단순화를 위해 static 메서드를 사용하지 못하게 했었다.

JDK 1.8 부터 default 메서드와 같이 static 메서드를 사용할 수 있게 추가되었다.

error

error
구현할 수 있는 메서드는 두개 뿐이다.

error
Anything 객체를 통해 staticMethod()를 호출할 수 없다.
일반적으로 static 메서드 호출하듯이, “인터페이스명.메서드명()” 으로 호출한다.

static 메서드 또한 접근제어자는 public이며 생략 가능하다.


1-16-7) private 메서드

JDK 1.9 부터 인터페이스에서 private 메서드를 허용했다.

인터페이스 private 메서드 참고


1-17) 내부 클래스

내부 클래스는 클래스 안에 선언된 클래스이다.

public class OutterClass {
    class InnerClass {

    }
}

이러한 내부 클래스를 사용함으로써 두 클래스의 멤버들 간의 접근이 쉬워지며,
외부로 클래스를 숨길 수 있다.

변수가 static변수, 인스턴스변수, 지역변수로 나뉘는 것과 동일하게 클래스도 마찬가지로 선언 위치에 따라 유효범위와 접근성이 달라진다.

public class OutterClass {

    private int x = 10;
    private static int y = 100;


    class InstanceInnerClass {
        int i = x;
        int j = y;
    }

    static class StaticInnerClass {
        // int num = x; // 사용불가
        int num2 = y;
    }

    void outterClassMethod() {
        int localVariable = 1000;
        final int finalLocalVariable = 10000;
        
        class LocalInnerClass {
            int a = x;
            int b = y;
            int c = localVariable;
            int d = finalLocalVariable;

            void localInnerMethod() {
                System.out.println("outterClassMethod.LocalInnerClass.localInnerMethod.a = " + a);
                System.out.println("outterClassMethod.LocalInnerClass.localInnerMethod.b = " + b);
                System.out.println("outterClassMethod.LocalInnerClass.localInnerMethod.c = " + c);
                System.out.println("outterClassMethod.LocalInnerClass.localInnerMethod.d = " + d);
            }
        }

        LocalInnerClass localInnerClass = new LocalInnerClass();
        localInnerClass.localInnerMethod();
    }

}



public class Main {

    public static void main(String[] args) {
        OutterClass outterClass = new OutterClass();
        outterClass.outterClassMethod();

        OutterClass.InstanceInnerClass instanceInnerClass = outterClass.new InstanceInnerClass();
        System.out.println("instanceInnerClass.i = " + instanceInnerClass.i);
        System.out.println("instanceInnerClass.j = " + instanceInnerClass.j);

        OutterClass.StaticInnerClass staticInnerClass = new OutterClass.StaticInnerClass();
        System.out.println("staticInnerClass.num2 = " + staticInnerClass.num2);
    }

}

위의 클래스를 컴파일하면 아래와 같은 class 파일이 생성된다.

error


1-18) 익명 클래스

익명 클래스는 말 그대로 이름없는 클래스이다.
이름이 없고, 객체의 선언과 생성을 동시에 하기 때문에,
단 한번만 사용될 수 있는 일회용 클래스이다.

public class RunThread {

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


class ThreadClass implements Runnable {

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

}

Runnable 인터페이스를 구현하면 Thread를 만들 수 있다.
물론 ThreadClass의 크기가 크다면 이처럼 분리해서 사용하는 것이 나을 수 있겠지만,
아니라면 익명클래스로 만들어 더욱 간단하게 표현할 수 있다.

public class RunThread {
    
    public static void main(String[] args) {
         Runnable anonymousThread = new Runnable() {
    
            @Override
            public void run() {
                System.out.println("AnonymousThread Run!");
            }
    
        };

        anonymousThread.run();
    }

}

여기서 한번 더 나아가면 Lambda식을 만들 수 있다.

public class RunThread {

    public static void main(String[] args) {
        Runnable lambdaThread = () -> System.out.println("LambdaThread Run!");
        lambdaThread.run();
    }
}

1-19) 과제

1-19-1) Overloading, Overriding 차이점

오버로딩과 오버라이딩은 이름이 비슷해 서로 혼동하기 쉽다.
하지만 둘은 아예 다른 기능이기 때문에 주의해서 사용하도록 한다.

  • 오버로딩은 이름이 동일한 새로운 메서드를 정의하는 것.
  • 오버라이딩은 기존의 메서드의 내용을 재정의 하는 것.

다시말해 오버로딩은 새로운 메서드를 만드는 것이고, 오버라이딩은 기존의 메서드를 수정하는 것이다.

아래의 코드를 보고 오버로딩인지, 오버라이딩인지 구분해보도록 한다.

public class Parent {
    void parentMethod() {}
}

public class Child extends Parent {
    void parentMethod() {}
    void parentMethod(int i) {}

    void childMethod() {}
    void childMethod(int i) {}
    void childMethod() {}
}

1-19-2) 추상 클래스, 인터페이스 공통점과 차이점

추상 클래스와 인터페이스의 공통점은 아래와 같다.

  1. new 연산자를 통해 객체 생성을 하지 못하고,
    확장/구현한 클래스를 통해서만 사용할 수 있다.
  2. 추상 메서드는 확장/구현한 클래스 내에서 필수적으로 구현해야 한다.

아래는 차이점이다.

인터페이스 추상 클래스
다중상속 가능 다중상속 불가
상수와 추상 메서드만을 멤버로 가질 수 있다. 생성자, 변수, 상수, 일반 메서드, 추상 메서드를 멤버로 가질 수 있다.
implements를 사용해서 구현한다. extends를 사용해서 확장한다.
메서드의 선언만 가능하다. 메서드의 구현도 가능하다.


추상 클래스와 인터페이스가 헷갈리는 이유는 결과물이 거의 동일하기 때문이다.
하지만 사용하는 의도는 충분히 다르니 이해하도록 하자.

추상 클래스를 사용하는 의도

  • 상속관계에서 동일한 기능을 사용할 경우 (is a 관계)
  • public final static 변수, public abstract 메서드 이외의 다른 제어자를 사용할 경우

인터페이스를 사용하는 의도

  • 상속관계가 아닌데 동일한 기능을 사용할 경우 (has a 관계)
  • 다중상속이 필요할 경우

Leave a comment