다형성은 코드의 유연성을 높이고, 확장성 및 유지보수성을 향상시키는 데 도움을 준다.
다형성은 객체 지향 프로그래밍에서 중요한 개념 중 하나이다. 간단히 말해서, 다형성은 동일한 인터페이스나 부모 클래스를 가진 서로 다른 객체들이 해당 인터페이스나 부모 클래스의 참조를 통해 접근될 때 각 객체의 고유한 방식으로 메소드를 실행할 수 있게 해준다.
자바에서 다형성을 구현하는 방법은 크게 두 가지가 있다: 오버라이딩(Overriding)과 오버로딩(Overloading).

같은 이름의 메소드를 매개변수의 타입이나 개수를 다르게 하여 여러 개 정의하는 것이다. 오버로딩은 다형성과는 조금 다른 개념이지만, 유사한 형태로 다양한 입력에 대응할 수 있게 해준다.
예를 들어, draw() 메소드가 Shape 클래스에 정의되어 있고, Circle, Square, Triangle 등이 Shape을 상속받는다면, 각 하위 클래스에서 draw() 메소드를 오버라이딩하여 특정 모양을 그리는 코드를 구현할 수 있다. 이렇게 하면 Shape 타입의 변수를 사용해도, 실제 참조하는 객체의 타입에 따라 다른 draw() 메소드가 호출된다.
상속받은 자식 클래스에서 부모 클래스의 메소드를 재정의하는 것이다. 이를 통해 같은 이름의 메소드라도 다른 행동을 할 수 있게 된다. 예를 들어, Animal 클래스의 speak() 메소드를 Dog 클래스에서 "멍멍"이라고 출력하도록 재정의할 수 있다.
package com.mystudy.poly1_instanceof;
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 = car; // Car <---- Car
System.out.println("자동차 타입 : " + carTemp.type);
carTemp = am; // Car <----- Ambulance
carTemp = fe; // Car <------ FireEngine
System.out.println("자동차 타입 : " + carTemp.type);
System.out.println(" --- 타입확인(instanceof) ---");
if (carTemp instanceof Object) {
System.out.println(">>Object타입이다." + (carTemp instanceof Object));
}
if (carTemp instanceof Car) {
System.out.println(">>Car타입이다." + (carTemp instanceof Car));
}
if (carTemp instanceof Ambulance) {
System.out.println(">>Ambulance타입이다." + (carTemp instanceof Ambulance));
}
if (carTemp instanceof FireEngine) {
System.out.println(">>FireEngine타입이다." + (carTemp instanceof FireEngine));
}
System.out.println("========================");
}
}
여기서 중요한 부분은 Car 클래스의 변수 carTemp가 다른 객체를 참조하는 방식이다.
첫 번째 할당 (Car carTemp = car;): carTemp는 Car 객체 car를 참조한다. 따라서 carTemp.type은 car 객체의 type 필드 값을 출력한다.
두 번째 할당 (carTemp = am;): carTemp는 이제 Ambulance 객체 am을 참조한다.
세 번째 할당 (carTemp = fe;): 마지막으로 carTemp는 FireEngine 객체 fe를 참조한다. 이 경우, carTemp.type은 fe 객체의 type 필드 값인 "소방차"를 출력한다.
이 예제에서 주목해야 할 점은, Car 클래스의 참조 변수 carTemp가 Car의 서브클래스 인스턴스를 참조할 때, 해당 인스턴스의 실제 타입에 속한 필드 값을 사용한다는 것이다. 이는 자바의 다형성과 필드에 대한 동적 바인딩이 적용되지 않는 정적 바인딩(static binding)의 특징 때문이다. 다형성은 주로 메서드에 적용되며, 필드의 경우 참조 변수의 타입에 따라 결정된다. 그러나 여기서는 carTemp가 참조하는 객체의 실제 타입의 필드에 접근하므로, carTemp.type은 마지막으로 할당된 fe의 type 값을 출력하게 된다.
class Animal {
String name; // 이름, 명칭
int legCnt; // 다리 갯수
void eat() {
System.out.println(">>먹는다.");
}
void sleep() {
System.out.println(">>잠을 잔다");
}
// 자녀클래스에 공통으로 있는 메소드를 정의
// 자녀클래스에서 필요에 따라 재정의(override)해서 사용하도록 함
void sound() {
System.out.println(">>울음 없음.");
}
}
class Cat extends Animal {
// 부모클래스(Animal)의 sound()메소드와 동일하게 선언하고
// 기능은 다르게 구현(메소드 재정의 - method overriding)
// 메소드 재정의시 기본적으로 메소드 선언부를 동일하게 사용
// 단, 메소드 접근제한자의 범위를 부모와 동일하거나 넓게 정의할 수 있다.
// (접근제한을 좁게 설정할 수 없다.)
@Override // 메소드 오버라이딩해서 사용하는 메소드임을 알리는 어노테이션(주석)
void sound() {
System.out.println("야옹야옹");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println(">> 멍멍멍");
}
}
public class TypeTest {
public static void main(String[] args) {
Animal animal = new Animal();
Cat cat = new Cat();
Dog dog = new Dog();
cat.sound();
dog.sound();
System.out.println("--- 메소드 오버로딩(overloading) 방식 ---");
sound_overloading(cat);
sound_overloading(dog);
System.out.println("--- instanceof 사용 패턴 ---");
sound_instanceof(animal);
sound_instanceof(cat);
sound_instanceof(dog);
System.out.println("--- 메소드 오버라이딩(overriding) 사용 ----");
sound(animal);
sound(cat);
sound(dog);
}
private static void sound(Animal animal) {
animal.sound();
}
//instanceof 사용 패턴 ----------------------------
// 잘못되었다. else if 가 작동하지 않는다. 자세한 것은 밑에 참조.
static void sound_instanceof(Animal animal) {
if (animal instanceof Animal) {
animal.sound();
} else if (animal instanceof Cat) {
((Cat) animal).sound();
} else if (animal instanceof Dog) {
((Dog) animal).sound();
}
}
//메소드 오버로딩(overloading) 방식 -------------------
static void sound_overloading(Cat cat) {
cat.sound();
}
static void sound_overloading(Dog dog) {
dog.sound();
}
}
Animal 클래스에 sound() 메소드가 정의되어 있다. Cat과 Dog 클래스는 Animal 클래스를 상속받고, sound() 메소드를 각각 "야옹야옹", "멍멍멍"으로 출력하도록 재정의한다. 이는 각 클래스의 특성에 맞게 부모 클래스의 메소드를 구체화하는 과정이다.sound_overloading 메소드는 오버로딩의 예이다. sound_overloading은 두 개의 버전으로 정의되어 있으며, 하나는 Cat 객체를 매개변수로 받고, 다른 하나는 Dog 객체를 매개변수로 받는다. 매개변수의 타입이 다르기 때문에 이는 유효한 오버로딩이다.System.out.println(cat instanceof Animal); // true
System.out.println(dog instanceof Animal); // true
instanceof 연산자는 자바에서 한 객체가 특정 클래스의 인스턴스인지, 혹은 그 클래스를 상속받은 자식 클래스의 인스턴스인지 확인할 때 사용된다.
이 연산자는 주어진 객체가 지정된 클래스 타입이거나, 해당 클래스 타입을 상속받은 경우에 true를 반환하며, 그렇지 않으면 false를 반환한다.
위 코드에서 cat instanceof Animal과 dog instanceof Animal 구문을 살펴보면:
cat instanceof Animal: 이 구문은 cat 객체가 Animal 클래스의 인스턴스인지 확인한다. 여기서 cat은 Cat 클래스의 인스턴스이며, Cat 클래스는 Animal 클래스를 상속받는다. 따라서, cat instanceof Animal은 true를 반환한다.
dog instanceof Animal: 이 구문은 dog 객체가 Animal 클래스의 인스턴스인지 확인한다. dog는 Dog 클래스의 인스턴스이고, Dog 클래스도 Animal 클래스를 상속받는다. 그러므로, dog instanceof Animal 역시 true를 반환한다.
결론적으로, 이 코드는 Cat과 Dog 클래스가 Animal 클래스를 상속받았기 때문에, cat instanceof Animal과 dog instanceof Animal 모두 true를 반환하게 된다. 이는 객체 지향 프로그래밍에서 상속의 개념을 활용한 결과이다.
추상 클래스(abstract class)는 객체 지향 프로그래밍에서 중요한 개념이다. 자바에서 추상 클래스는 'abstract' 키워드를 사용해 정의한다. 추상 클래스의 주요 특징은 다음과 같다.
public abstract void draw(); 같은 식으로 선언할 수 있다.new 키워드를 사용해 추상 클래스의 객체를 만들 수 없다. 추상 클래스는 상속을 통해 자식 클래스에서 구현되어야 한다.abstract class Shape {
int x, y;
// 추상 메소드
public abstract void draw();
// 일반 메소드
public void move(int x, int y) {
this.x = x;
this.y = y;
}
}
class Circle extends Shape {
private int radius;
// 추상 메소드 구현
public void draw() {
System.out.println("원을 그립니다.");
}
}
이 예제에서 Shape는 추상 클래스이며, draw()는 추상 메소드이다. Circle 클래스는 Shape를 상속받아 draw() 메소드를 구현한다. 이렇게 하면 다형성을 이용하여 여러 형태의 Shape 객체를 유연하게 다룰 수 있다.
// 추상클래스 : =
// 클래스에 추상메소드가 1개라도 있으면 추상클래스로 선언해야 한다.
// 추상클래스는 객체(인스턴스)를 생성할 수 없다.
// 부모타입으로 자식에게 상속은 가능하다.
abstract class AbstractAnimal {
String name; //이름, 명칭
int legCnt; //다리갯수
void eat() {
System.out.println(">> 먹는다");
}
void sleep() {
System.out.println(">> 잠을 잔다");
}
abstract void sound(); // 추상메소드(abstact method)
}
class Cat extends AbstractAnimal {
// 부모클래스(Animal)의 sound()메소드와 동일하게 선언하고
// 기능은 다르게 구현(메소드 재정의 - method overriding)
// 메소드 재정의시 기본적으로 메소드 선언부를 동일하게 사용
// 단, 메소드 접근제한자의 범위를 부모와 동일하거나 넓게 정의할 수 있다.
// (접근제한을 좁게 설정할 수 없다.)
@Override // 메소드 오버라이딩해서 사용하는 메소드임을 알리는 어노테이션(주석)
void sound() {
System.out.println("야옹야옹");
}
}
class Dog extends AbstractAnimal {
@Override
void sound() {
System.out.println(">> 멍멍멍");
}
}
class Pig extends AbstractAnimal{
void sound() {
System.out.println(">> 꿀 꿀~~");
}
}
public class TypeTestt {
public static void main(String[] args) {
// TODO Auto-generated method stub
Cat cat = new Cat();
Dog dog = new Dog();
Pig pig = new Pig();
cat.sound();
dog.sound();
pig.sound();
System.out.println("------------------");
sound(cat);
sound(dog);
}
private static void sound(AbstractAnimal animal) {
animal.sound();
}
}
public interface Animal {
void eat(); // 추상메소드(public abstract생략됨)
void sleep();
void sound();
}
abstract class AbstractAnimal implements Animal {
String name = "동물";
int legCnt;
public AbstractAnimal() {}
public AbstractAnimal(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void eat() {
System.out.println(">> 먹는다.");
}
@Override
public void sleep() {
System.out.println(">> 잠잔다.");
}
@Override
public abstract void sound();
}
class Cat extends AbstractAnimal {
public Cat() {
super("고양이");
}
public Cat(String name) {
super(name);
}
@Override
public void sound() {
System.out.println("야옹야옹");
}
}
public class TypeTest {
public static void main(String[] args) {
Cat cat = new Cat();
cat.sound(); // 야옹야옹
System.out.println("===== Animal 인터페이스 사용 =====");
soundPoly(cat); // 야옹야옹
}
// 인터페이스 Animal 타입으로 데이터를 받아서
// 인터페이스 타입에 저장된 데이터에 있는 sound() 메소드 호출
// (메소드 오버라이딩에 의해 저장된 객체의 메소드가 호출)
static void soundPoly(AbstractAnimal animal) { // Animal animal도 된다.
animal.sound();
}
}
eat(), sleep(), sound()를 정의한다. 인터페이스에 선언된 메소드들은 기본적으로 public과 abstract이다. 즉, 이 메소드들은 구현이 없으며, Animal 인터페이스를 구현하는 클래스에서 이 메소드들을 구현해야 한다.AbstractAnimal 클래스는 Animal 인터페이스를 구현한다. 이 클래스는 추상 클래스로 선언되어 있으며, Animal 인터페이스의 eat()와 sleep() 메소드를 구현합니다. 하지만 sound() 메소드는 추상 메소드로 남겨두어, 이 클래스를 상속받는 하위 클래스가 sound()를 구현하도록 한다.AbstractAnimal에는 기본 생성자와 이름을 매개변수로 받는 생성자가 있으며, name과 legCnt라는 멤버 변수가 있다.Cat 클래스는 AbstractAnimal 클래스를 상속받는다. Cat 클래스는 AbstractAnimal의 추상 메소드인 sound()를 구현하여 "야옹야옹"을 출력한다.Cat 클래스에는 두 개의 생성자가 정의되어 있으며, 하나는 기본 이름 "고양이"를 사용하고, 다른 하나는 사용자가 지정한 이름을 사용한다.Cat 객체를 생성하고, sound() 메소드를 호출한다. 이는 Cat 클래스에 오버라이딩된 sound() 메소드가 호출됨을 의미합니다.soundPoly 메소드는 AbstractAnimal 타입의 매개변수를 받아 sound() 메소드를 호출한다. 이는 다형성을 보여주는 좋은 예이다. soundPoly 메소드에 Cat 객체를 전달하면, Cat 클래스에서 오버라이딩된 sound() 메소드가 실행된다.예외는 프로그램 실행 중에 발생하는 오류나 비정상적인 상황을 말한다. 예외 처리는 이런 상황들을 안전하게 다루고 프로그램이 갑자기 중단되는 것을 방지하기 위해 사용된다.
예외처리엔 다음과 같은 두 가지 유형이 있다.
try {
// 예외가 발생할 수 있는 코드
} catch (ExceptionType e) {
// 예외 처리 코드
}
try 블록 안에는 예외가 발생할 수 있는 코드를 넣는다. 예외가 발생하면, try 블록의 나머지 부분은 실행되지 않고 바로 catch 블록으로 이동한다.catch 블록에서는 해당 예외를 처리하는 코드를 넣는다. ExceptionType은 처리하고자 하는 예외의 타입이다.Java에서 예외 처리를 위한 완전한 구조를 제공한다. 이 구문은 프로그램에서 예외를 처리하고, 예외 발생 여부와 상관없이 특정 코드를 실행할 수 있도록 해준다.
try {
// 예외가 발생할 수 있는 코드
} catch (ExceptionType1 e) {
// ExceptionType1을 처리하는 코드
} catch (ExceptionType2 e) {
// ExceptionType2를 처리하는 코드
} finally {
// 예외 발생 여부와 상관없이 항상 실행되는 코드
}
try 블록: 이 블록 안의 코드는 예외가 발생할 수 있는 코드를 포함해. 여기서 예외가 발생하면, 나머지 try 블록의 코드는 실행되지 않고, 적절한 catch 블록으로 이동한다.
catch 블록: 이 블록은 특정 유형의 예외를 처리하는 코드를 포함한다. 여러 개의 catch 블록을 사용해서 다양한 유형의 예외를 각각 처리할 수 있다. catch 블록은 발생한 예외 유형과 일치하는 첫 번째 블록에서 처리되며, 나머지는 무시된다.
finally 블록: 이 블록은 예외 발생 여부와 상관없이 항상 실행되는 코드를 포함한다. 주로 리소스 해제, 파일 닫기, 네트워크 연결 끊기 등의 정리 작업에 사용된다. finally 블록은 try 블록 내에서 return 문이 실행되어도 호출된다.
finally 블록의 중요한 점은, 프로그램의 흐름이 try 또는 catch 블록 내의 코드에 의해 변경되더라도, 예외가 있든 없든 항상 실행된다는 것이다. 이것은 리소스 누수를 방지하고 프로그램의 안정성을 높이는 데 중요하다.
Java에서 예외는 크게 두 가지 주요 범주로 나뉜다. Exception과 RuntimeException. 그리고 ArithmeticException은 RuntimeException의 하위 클래스 중 하나이다.
Exception: 이것은 체크 예외(Checked Exception)라고 불리며, 컴파일 시점에 확인될 수 있는 예외들이다. 즉, 이러한 예외들은 반드시 try-catch 블록을 사용하거나, throws 키워드를 통해 처리해야 한다. 예를 들어, IOException이나 SQLException 같은 예외들이 여기에 속한다.
RuntimeException: 이것은 언체크 예외(Unchecked Exception)라고 불리며, 주로 프로그래밍 실수로 인해 발생하는 예외들이다. 이들은 컴파일러에 의해 강제되지 않아서, 명시적으로 처리하지 않아도 된다. NullPointerException, IndexOutOfBoundsException 그리고 여기서 언급한 ArithmeticException 등이 여기에 속한다.
package com.mystudy.ex01_exception;
public class Exception_Exam {
public static void main(String[] args) {
System.out.println("--- main() 시작 -----------");
int num1 = 100;
int num2 = 20;
int result = 0;
System.out.println(">> 연산 시작");
num2 = 0;
// result = num1 / num2; // Exception in thread "main" java.lang.ArithmeticException: / by zero
// 예외처리 ---
if (num2 == 0) {
System.out.println("[예외발생] 나누는 값이 0이다.");
} else {
result = num1 / num2;
System.out.println("result : " + result);
}
System.out.println("--- main() 끝 -----------");
}
}
--- main() 시작 -----------
>> 연산 시작
[예외발생] 나누는 값이 0이다.
--- main() 끝 -----------
package com.mystudy.ex01_exception;
public class Exception_Exam {
public static void main(String[] args) {
System.out.println("--- main() 시작 -----------");
int num1 = 100;
int num2 = 20;
int result = 0;
System.out.println(">> 연산 시작");
num2 = 0;
// result = num1 / num2; // Exception in thread "main" java.lang.ArithmeticException: / by zero
System.out.println("=== try catch 예외 처리 ====");
try {
result = num1 / num2;
System.out.println("result : " + result);
} catch (ArithmeticException e) {
System.out.println("[예외발생] 나누는 값이 0이다.");
}
System.out.println("--- main() 끝 -----------");
}
}
--- main() 시작 -----------
=== try catch 예외 처리 ====
[예외발생] 나누는 값이 0이다.
--- main() 끝 -----------
package com.mystudy.ex01_exception;
public class Exception_Exam {
public static void main(String[] args) {
System.out.println("--- main() 시작 -----------");
int num1 = 100;
int num2 = 20;
int result = 0;
System.out.println(">> 연산 시작");
num2 = 0;
// result = num1 / num2; // Exception in thread "main" java.lang.ArithmeticException: / by zero
System.out.println("=== try catch 예외 처리 ====");
try {
result = num1 / num2;
System.out.println("result : " + result);
} catch (ArithmeticException e) {
System.out.println("[예외발생-ArithmeticException]" + e);
} catch (RuntimeException e) {
System.out.println("[예외발생-RuntimeException]" + e);
} catch (Exception e) {
System.out.println("[예외발생-Exception]" + e);
}
System.out.println("--- main() 끝 -----------");
}
}
--- main() 시작 -----------
=== try catch 예외 처리 ====
[예외발생-ArithmeticException]java.lang.ArithmeticException: / by zero
--- main() 끝 -----------
package com.mystudy.ex01_exception;
public class Exception_finally {
public static void main(String[] args) {
// try ~ catch ~ finally
System.out.println("--- main() 시작 -----------");
int num = 5;
int result = 0;
try {
System.out.println("--- try문 시작");
num = 0;
result = 100 / num;
System.out.println("연산결과 : " + result);
} catch (ArithmeticException e) {
System.out.println(">>> catch문 실행");
return ;
} finally {
System.out.println("****>> finally : 항상 실행(무조건)");
}
System.out.println("--- main() 끝 -----------");
}
}
--- main() 시작 -----------
--- try문 시작
>>> catch문 실행
****>> finally : 항상 실행(무조건)
package com.mystudy.ex01_exception;
public class Exception_throw_throws {
public static void main(String[] args) {
// throws : 예외를 던질 때(위임, 전가) 사용
System.out.println("---- main() 시작 ---");
int num1 = 100;
int num2 = 50;
int result = 0;
System.out.println(">> 연산처리 시작");
num2 = 0;
try {
result = div(num1, num2);
} catch (Exception e) {
e.printStackTrace();
System.out.println("[예외발생] main() div Throws : " + e.getMessage());
}
System.out.println("계산결과 : " + result);
System.out.println("---- main() 끝 ---");
}
// throws : 메소드 선언부에 작성해서 호출한 곳으로 예외를 던짐
static int div(int num1, int num2) throws Exception {
System.out.println("----->> div() 시작");
int result = -1;
try {
result = num1 / num2;
} catch (ArithmeticException e) {
System.out.println("[예외발생] " + e.getMessage());
throw new Exception("::: 내가발생시킨 예외 - 0으로 나눔");
}
System.out.println("----->> div() 끝");
return result;
}
}
---- main() 시작 ---
>> 연산처리 시작
----->> div() 시작
[예외발생] / by zero
java.lang.Exception: ::: 내가발생시킨 예외 - 0으로 나눔
at com.mystudy.ex01_exception.Exception_throw_throws.div(Exception_throw_throws.java:34)
at com.mystudy.ex01_exception.Exception_throw_throws.main(Exception_throw_throws.java:15)
[예외발생] main() div Throws : ::: 내가발생시킨 예외 - 0으로 나눔
계산결과 : 0
---- main() 끝 ---
main 메소드에서 div 함수를 호출하고 있다. 이때 num1은 100, num2는 0으로 설정된다.div 함수는 throws Exception을 사용해 예외를 선언하고 있어서, main 메소드는 try-catch 블록으로 div 함수를 감싸서 호출해야 한다. 이것은 div 함수에서 발생할 수 있는 예외를 main 메소드에서 처리하겠다는 의미다.</span<stack trace를 출력하고, 사용자 정의 메시지를 출력한다.div 메소드는 두 개의 정수를 받아 나눗셈을 수행하고, 결과를 반환한다.div 메소드 내부에서도 try-catch 블록이 사용된다. 이 블록은 ArithmeticException (즉, 수학적 오류, 여기서는 0으로 나누는 오류)을 잡아내기 위한 것이다.ArithmeticException이 발생하고 catch 블록이 실행된다. 이 블록 내에서는 사용자에게 예외에 대한 정보를 출력하고, throw new Exception(...)을 통해 새로운 예외를 생성하고 던진다. 이 새로운 예외는 div 메소드를 호출한 main 메소드로 전달된다.