이 클래스는 다른 클래스의 기반이 된다. 수퍼클래스의 속성과 메소드는 서브클래스에 상속될 수 있다. 예를 들어, '동물' 클래스에는 여러 공통 특징이 정의되어 있을 수 있다.
수퍼클래스로부터 상속을 받는 클래스다. 서브클래스는 수퍼클래스의 속성과 메소드를 이어받으며, 추가적인 속성이나 메소드를 가질 수 있다. 예를 들어, '고양이' 클래스는 '동물' 클래스의 서브클래스가 될 수 있다.
클래스가 다른 클래스로부터 상속을 받을 때 사용하는 키워드다. 예를 들어 class 고양이 extends 동물이라고 선언하면, '고양이' 클래스는 '동물' 클래스의 모든 속성과 메소드를 상속받는다. 자바에서는 클래스 상속(extends)은 단일 상속만 허용한다.
예시코드
class 동물 {
void 먹다() {
System.out.println("먹는 중...");
}
}
class 고양이 extends 동물 {
void 울다() {
System.out.println("야옹");
}
}
public class Main {
public static void main(String[] args) {
고양이 내고양이 = new 고양이();
내고양이.먹다(); // '동물' 클래스에서 상속받은 메소드
내고양이.울다(); // '고양이' 클래스의 메소드
}
}
이 코드에서 고양이 클래스는 동물 클래스로부터 먹다() 메소드를 상속받는다. 추가로, 고양이 클래스는 자신만의 메소드인 울다()를 가진다. 이렇게 상속을 사용하면 코드의 재사용성을 높이고, 관리하기 쉬운 구조를 만들 수 있다.

이 키워드는 클래스가 하나 이상의 인터페이스를 구현할 때 사용된다. 인터페이스는 메소드의 시그니처만을 선언하고, 구현 클래스에서 이 메소드들을 실제로 구현해야 한다. 예를 들어, class 고양이 implements 포유류라고 선언하면, '고양이' 클래스는 '포유류' 인터페이스에 정의된 모든 메소드를 구현해야 한다.
인터페이스와 implements 키워드를 사용하는 예시는 다음과 같다.
먼저, 인터페이스를 정의해보자. 인터페이스는 특정 행위를 정의하지만, 이 행위의 구체적인 실행 방법은 제공하지 않는다. 예를 들어, 소리내기 인터페이스를 만들어보면 아래와 같다.
interface 소리내기 {
void 소리내다();
}
이제 이 인터페이스를 구현하는 클래스를 만들어보자. 예를 들어, 고양이 클래스와 개 클래스가 소리내기 인터페이스를 구현할 수 있다.
class 고양이 implements 소리내기 {
public void 소리내다() {
System.out.println("야옹");
}
}
class 개 implements 소리내기 {
public void 소리내다() {
System.out.println("멍멍");
}
}
여기서 고양이와 개 클래스는 소리내기 인터페이스의 소리내다() 메소드를 구현한다. 각 클래스는 이 메소드를 자신의 방식으로 구현하여, 각각 고양이와 개의 소리를 낸다.
이제 메인 클래스에서 이를 사용해보면 다음과 같다.
public class Main {
public static void main(String[] args) {
고양이 내고양이 = new 고양이();
개 내개 = new 개();
내고양이.소리내다(); // 출력: 야옹
내개.소리내다(); // 출력: 멍멍
}
}
이 예시에서 implements 키워드는 고양이와 개 클래스가 소리내기 인터페이스의 계약을 준수하고, 소리내다() 메소드를 구현하도록 보장한다. 이를 통해 다형성을 실현하고, 코드의 유연성과 확장성을 높일 수 있다.
인터페이스에서 public static final 변수(상수)를 사용하는 것은 자바에서 흔히 볼 수 있는 패턴이다. 인터페이스 내의 모든 변수는 기본적으로 public static final이며, 이는 다음과 같은 의미를 가진다.
public: 변수가 공개적으로 접근 가능하다는 것을 의미한다. 즉, 인터페이스를 구현하는 모든 클래스나 인터페이스 외부에서도 이 변수에 접근할 수 있다.static: 변수가 정적(static)이라는 것을 의미한다. 이는 해당 변수가 인터페이스의 인스턴스가 아닌, 인터페이스 자체에 속한다는 것을 뜻한다. 따라서 이 변수는 인터페이스를 구현하는 모든 객체에 의해 공유된다.final: 변수가 최종적이며 변경할 수 없다는 것을 의미한다. 한 번 초기화되면, 그 값은 프로그램 실행 동안 변경될 수 없다.이러한 변수는 보통 인터페이스에서 공유되어야 하는 불변의 값들을 정의할 때 사용된다. 예를 들어, 설정 값, 에러 코드, 또는 고정된 문자열과 같은 값들이 여기에 해당된다.
interface MyInterface {
public static final int MAX_SIZE = 10;
public static final String ERROR_MESSAGE = "에러 발생";
}
이 경우, MAX_SIZE와 ERROR_MESSAGE는 MyInterface.MAX_SIZE 및 MyInterface.ERROR_MESSAGE으로 접근할 수 있으며, 이들의 값은 변경할 수 없다.
이러한 상수들은 인터페이스의 정의와 그것을 구현하는 모든 클래스들에게 공통적인, 변경되지 않는 값을 제공하는 데 유용하다. 하지만, 인터페이스 내에서 너무 많은 상수를 정의하는 것은 인터페이스의 명확성을 흐릴 수 있으므로 주의해야 한다.
static 메소드를 사용하는 방식과 인터페이스를 사용하는 방식은 각기 다른 상황과 목적에 적합하다.
이 둘의 차이점은 다음과 같다.
static 메소드를 사용하는 것이 간단해 보일 수 있지만, 객체 지향 프로그래밍에서는 객체의 행동을 객체의 상태와 밀접하게 연결하는 것이 중요하다. 인터페이스를 사용하면 각 객체의 상태에 따라 다르게 행동할 수 있는 메소드를 정의할 수 있으며, 코드의 재사용성과 유지보수성을 높일 수 있다.
예를 들어, 소리내기 인터페이스를 사용하면 다양한 동물 클래스가 각자의 특성에 맞게 소리내다() 메소드를 구현할 수 있다. 이렇게 함으로써, 코드는 더 유연하고 확장 가능하며, 객체 지향적인 설계를 갖출 수 있다.
인터페이스가 클래스 안에 완전히 구현되지 않은 경우, 주로 두 가지 상황이 발생할 수 있다.
interface 소리내기 {
void 소리내다();
}
abstract class 동물 implements 소리내기 {
// '소리내다' 메소드는 구현되지 않았으므로, '동물' 클래스는 추상 클래스가 된다.
}
class 고양이 extends 동물 {
public void 소리내다() {
System.out.println("야옹");
}
// '고양이' 클래스에서 '소리내다' 메소드를 구현해야 한다.
}
동물 클래스는 추상 클래스로, 소리내다 메소드를 구현하지 않았다. 고양이 클래스는 이 추상 클래스를 상속받고, 미구현된 소리내다 메소드를 구현해야 한다.interface 소리내기 {
void 소리내다();
}
class 개 implements 소리내기 {
// '소리내다' 메소드가 구현되지 않았고, '개' 클래스도 추상 클래스로 선언되지 않았으므로, 컴파일 에러가 발생한다.
}
개 클래스는 인터페이스 소리내기의 소리내다 메소드를 구현하지 않았기 때문에, 컴파일 과정에서 에러가 발생한다.따라서 인터페이스를 사용할 때는 모든 메소드를 구현하거나, 미구현 메소드를 가진 클래스를 추상 클래스로 선언해야 한다.
인터페이스의 주요 의의는 클래스에 특정 메소드를 강제하는 것 뿐만 아니라, 여러 다른 중요한 목적을 가지고 있다.
이처럼 인터페이스는 단순히 메소드 구현을 강제하는 것을 넘어서, 객체 지향 프로그래밍에서 코드의 설계와 유지보수, 확장성, 유연성을 개선하는 데 매우 중요한 역할을 한다.
package com.mystudy.ex03_interface;
interface Call {
void call();
default void defaultCall() {
System.out.println(">> 전화걸기");
}
}
interface ReceiveCall {
void receiveCall();
default void defaultReceiveCall() {
System.out.println(">> 전화받기~~");
}
}
interface View {
void view(String type, String phoneNo);
default void defaultView(String type, String phoneNo) {
System.out.println("타입 : " + type + ", 전화번호 : " + phoneNo);
}
}
class Phone implements Call, ReceiveCall, View { // extends Object 생략 사
// 필드(속성)-------------
private String type; // 전화타입(형태)
private String phoneNo; // 전화번호
// 생성자 ----------------
public Phone(String phoneNo) {
type = "Phone 타입";
this.phoneNo = phoneNo;
}
public Phone(String type, String phoneNo) {
this.type = type;
this.phoneNo = phoneNo;
}
// 메소드
public String getType() {
return type;
}
public String getPhoneNo() {
return phoneNo;
}
@Override
public void view(String type, String phoneNo) {
// TODO Auto-generated method stub
defaultView(this.type, this.phoneNo);
}
@Override
public void receiveCall() {
// TODO Auto-generated method stub
defaultReceiveCall();
}
@Override
public void call() {
// TODO Auto-generated method stub
defaultCall();
}
}
package com.mystudy.ex03_interface;
// 인터페이스를 구현(implements)해서 클래스 만들기
class PhoneImpl implements I_Phone{
@Override //어노테이션(주석) : 오버라이딩해서 만든 메소드
public void view() {
// TODO Auto-generated method stub
}
@Override
public void call() {
// TODO Auto-generated method stub
}
@Override
public void receiveCall() {
// TODO Auto-generated method stub
}
@Override
public void sendMsh() {
// TODO Auto-generated method stub
}
@Override
public void receiveMsg() {
// TODO Auto-generated method stub
}
}
package com.mystudy.ex03_interface;
// 인터페이스 final 속성값, 추상메소드가 있다.
interface I_Phone {
// {} 없음 : 추상메소드(abstract method)
// 인터페이스에 정의된 추상메소드는 public abstract 메소드
public abstract void view();
public void call(); // abstract를 생략해도, 컴파일러가 알아서 넣어준다.
// void receiveCall() {}; // abstract인데 구현부인 {}가 정의되어 있으므로, 에러가 난다. 아래와 같이 바꿔주자.
void receiveCall(); //public, abstract 생
void sendMsh(); // 메세지 보내
void receiveMsg(); // 메세지 받기
}
// public class Phone extends Object {
class Phone { // extends Object 생략 사
// 필드(속성)-------------
private String type; // 전화타입(형태)
private String phoneNo; // 전화번호
// 생성자 ----------------
public Phone(String phoneNo) {
type = "Phone 타입";
this.phoneNo = phoneNo;
}
public Phone(String type, String phoneNo) {
this.type = type;
this.phoneNo = phoneNo;
}
// 메소드
public String getType() {
return type;
}
public String getPhoneNo() {
return phoneNo;
}
// 전화걸기 기능
public void call() {
System.out.println(">> 전화걸기");
}
// 전화받기 기능
public void receiveCall() {
System.out.println(">> 전화받기~~");
}
// 전화정보 보기
public void view() {
System.out.println("타입 : " + type + ", 전화번호 : " + phoneNo);
}
}
// Phone클래스를 상속받아(extends:확장해서) Mp3
// Phone 허용된 속성, 기능을 사용 + Mp3Phone 속성, 기능 사용
class Mp3Phone extends Phone {
// private String type;
// private String phoneNo;
public Mp3Phone(String phoneNo) {
super(phoneNo);
}
public Mp3Phone(String type, String phoneNo) {
super(type, phoneNo);
}
// 메소드(기능)-----------
public void playMusic() {
System.out.println(">> MpePhone - 음악플레이~~");
}
}
class WebPhone extends Phone { // ctrl + T : 상속관계도 보기
public WebPhone(String phoneNo) {
super(phoneNo);
}
public WebPhone(String type, String phoneNo) {
super(type, phoneNo);
}
public void searchWeb() {
System.out.println(">>WebPhone - 웹 검색");
}
}
public class Mp3WebPhone extends Mp3Phone {
public Mp3WebPhone(String phoneNo) {
super("Mp3WebPhone 타입", phoneNo);
}
public Mp3WebPhone(String type, String phoneNo) {
super(type, phoneNo);
}
public void searchWeb() {
System.out.println(">>Mp3WebPhone - 웹 검색");
}
}
public class PhoneTest {
public static void main(String[] args) {
// Phone 객체(인스턴스) 생성 후 기능 확인(사용)
System.out.println("=== Phone =========");
Phone ph1 = new Phone("010-1111-1111");
ph1.view();
ph1.call();
ph1.receiveCall();
System.out.println("-------------------------");
Phone ph2 = new Phone("심성갤럭시", "010-2222-2222");
ph2.view();
ph2.call();
ph2.receiveCall();
System.out.println("=== Mp3Phone ==========");
Mp3Phone mp3ph3 = new Mp3Phone("Mp3Phone 타입", "010-3333-3333");
mp3ph3.view(); // Phone 타입의 메소드
mp3ph3.call(); // Phone 타입의 메소드
mp3ph3.receiveCall(); // Phone 타입의 메소드
mp3ph3.playMusic(); // Mp3Phone 의 메소드
System.out.println("=== WebPhone ==========");
WebPhone wph = new WebPhone("WebPhone 타입", "010-3333-3333");
wph.view();
wph.call();
wph.receiveCall();
wph.searchWeb();
System.out.println("=== Mp3WebPhone =====");
Mp3WebPhone mp3WebPhone = new Mp3WebPhone("010-5555-5555");
mp3WebPhone.view();
mp3WebPhone.call();
mp3WebPhone.receiveCall();
mp3WebPhone.playMusic(); // Mp3Phone 상속
mp3WebPhone.searchWeb();
}
}
=== Phone =========
타입 : Phone 타입, 전화번호 : 010-1111-1111
>> 전화걸기
>> 전화받기~~
-------------------------
타입 : 심성갤럭시, 전화번호 : 010-2222-2222
>> 전화걸기
>> 전화받기~~
=== Mp3Phone ==========
타입 : Mp3Phone 타입, 전화번호 : 010-3333-3333
>> 전화걸기
>> 전화받기~~
>> MpePhone - 음악플레이~~
=== WebPhone ==========
타입 : WebPhone 타입, 전화번호 : 010-3333-3333
>> 전화걸기
>> 전화받기~~
>>WebPhone - 웹 검색
=== Mp3WebPhone =====
타입 : Mp3WebPhone 타입, 전화번호 : 010-5555-5555
>> 전화걸기
>> 전화받기~~
>> MpePhone - 음악플레이~~
>>Mp3WebPhone - 웹 검색
package com.mystudy.ex04_interface2;
// Mp3Phone 클래스를 상속확장(extends)해서 사용하고,
// I_WebPhone 인터페이스를 구현(implements)해서 만드시
interface Call {
void call();
default void defaultCall() {
System.out.println(">> 전화걸기");
}
}
interface ReceiveCall {
void receiveCall();
default void defaultReceiveCall() {
System.out.println(">> 전화받기~~");
}
}
interface View {
void view(String type, String phoneNo);
default void defaultView(String type, String phoneNo) {
System.out.println("타입 : " + type + ", 전화번호 : " + phoneNo);
}
}
interface WebSearch {
void webSearch();
default void defaultWebSearch() {
System.out.println(">>웹 검색중~~");
}
}
interface SmartPhone extends Call, ReceiveCall, View, WebSearch, I_Mp3Phone {
}
class WebPhone extends Mp3Phone implements SmartPhone {
public WebPhone(String phoneNo) {
super(phoneNo);
}
public WebPhone(String type, String phoneNo) {
super(type, phoneNo);
// TODO Auto-generated constructor stub
}
@Override
public void view(String type, String phoneNo) {
// TODO Auto-generated method stub
defaultView(type, phoneNo);
}
@Override
public void webSearch() {
// TODO Auto-generated method stub
defaultWebSearch();
}
}
public class Car {
String type;
public Car() {
}
public Car(String type) {
this.type = type;
}
void drive() {
System.out.println(">>이동");
}
void stop() {
System.out.println(">>정지");
}
}
public class Ambulance extends Car {
String type;
public Ambulance() {
}
public Ambulance(String type) {
super(type);
}
void siren() {
System.out.println(">>응급환자 이송 사이렌~~");
}
}
//소방차
public class FireEngine extends Car {
void siren() {
System.out.println(">> 불끄기 출동 사이렌~~~");
}
void water() {
System.out.println(">> 물뿌리기~~");
}
}
public class CarTest {
public static void main(String[] args) {
System.out.println("=== Car 클래스 ===");
Car car = new Car();
car.type = "자동차";
System.out.println("타입 : " + car.type);
car.drive();
car.stop();
System.out.println("=== Ambulance ===");
Ambulance am = new Ambulance();
am.type = "구급차";
System.out.println("타입 : " + am.type);
am.drive();
am.stop();
am.siren();
System.out.println("=== FireEngine ===");
FireEngine fe = new FireEngine();
fe.type = "소방차";
System.out.println("타입 : " + fe.type);
fe.drive();
fe.stop();
fe.siren();
fe.water();
System.out.println("======================");
// 형변환
Car carTemp = new Car(); // Car <-- Car
carTemp.type = "일반자동차";
carTemp.drive();
carTemp.stop();
// carTemp.siren(); // 작동을 안한다. (undefined) 왜? Car 타입에는 siren()이 없기 때문이다.
System.out.println("--- Car <-- Ambulance 저장 ---");
// Car car2 = new Ambulance();
carTemp = am; // Car <--- Ambulance : 자동 형변환, 왜? 부모 클래스이기 때문에! Car타입이 더 상위의 데이터 타입이라 보면 된다.
carTemp.drive();
carTemp.stop();
// carTemp.siren(); // 안된다. Car타입에는 siren이 없으므로!
System.out.println("--- Ambulace <--- Car <--- Ambulance --- : 다시 Ambulace에 넣기");
// Ambulance am2 = carTemp; // Type mismatch: cannot convert from Car to Ambulance
Ambulance am2 = (Ambulance)carTemp; // 이렇게 하면 된다.
am2.drive();
am2.stop();
am2.siren();
// 엠뷸런스 고유 기능 siren() 사용하려면 Ambulance 타입이 되어야 한다.
((Ambulance)carTemp).siren(); // 변환으로 사이렌기능 사용하기
System.out.println("--- Ambulance <- Car 사이렌 울리기 ---");
// Car 객체(인스턴스)를 Ambulance로 만들어서 사용 못함
// ((Ambulance) car).siren(); // 에러 발생 ClassCastException
}
// 인터페이스엔 백퍼센트 추상체, 클래스에는 백퍼센트 구현체를 사용한다. 단, 디폴트와 스태틱은 구현 메소드이다.
}
클래스 상속: Car 클래스는 Ambulance와 FireEngine 클래스의 부모 클래스로 기능한다. 이는 Ambulance와 FireEngine가 Car의 모든 속성과 메소드를 상속받는다는 것을 의미한다. 예를 들어, Car 클래스에 정의된 drive()와 stop() 메소드는 Ambulance와 FireEngine에서도 사용할 수 있다.
오버라이딩과 확장: Ambulance와 FireEngine 클래스는 Car 클래스의 기능을 확장하여, 각각 siren() 및 water() 메소드를 추가로 제공한다. 이러한 메소드는 부모 클래스에는 없는 자식 클래스만의 특별한 기능들이다.
다형성과 형변환: 자바에서는 부모 클래스 타입의 참조 변수가 자식 클래스의 객체를 참조할 수 있다. 이것을 "업캐스팅"이라고 하며, 자동으로 이루어진다. 예를 들어, Car carTemp = new Ambulance();는 가능하다. 하지만, 이 때 carTemp 변수를 통해 Ambulance 클래스의 siren() 메소드에 접근하려면 명시적으로 "다운캐스팅"을 해야 한다.
형변환 시 주의점: 형변환은 항상 신중하게 이루어져야 한다. 잘못된 형변환은 ClassCastException을 발생시킬 수 있다. 예를 들어, Ambulance am2 = (Ambulance)carTemp;는 carTemp가 Ambulance의 인스턴스일 때만 가능하다. Car 타입의 객체를 Ambulance로 변환하려고 하면 오류가 발생한다.
명시적 형변환과 런타임 에러: 코드의 마지막 부분에서, Car 객체를 Ambulance로 강제 형변환하려고 할 때 런타임 에러(ClassCastException)가 발생합니다. 이는 형변환을 시도하는 객체가 실제로 변환하려는 클래스의 인스턴스가 아니기 때문입니다.
접근 제어자와 캡슐화: Car, Ambulance, FireEngine 클래스의 type 필드가 public으로 선언되어 있다. 이것은 좋은 캡슐화 원칙에 어긋난다. 일반적으로, 클래스의 필드는 private이나 protected로 선언하고, 필요한 경우 게터(getter)와 세터(setter) 메소드를 통해 접근하는 것이 좋다.