열거형은 자바 5에서 추가된 자료형으로, 의미가 부여된 이름을 갖는 상수의 선언에 그 목적이 있습니다.
열거형 학습에 앞서, 자바 5 이전에는 의미가 부여된 이름을 갖는 상수를 어떻게 선언했는지 다음 코드로 확인해보겠습니다.
public interface Scale {
int DO = 0;
int RE = 1;
int MI = 2;
int FA = 3;
int SO = 4;
int RA = 5;
int TI = 6;
}
public class InterfaceBaseConst {
public static void main(String[] args) {
int sc = Scale.DO;
switch (sc) {
case Scale.DO:
System.out.println("음 이름 : 도");
break;
case Scale.RE:
System.out.println("음 이름 : 레");
break;
case Scale.MI:
System.out.println("음 이름 : 미");
break;
case Scale.FA:
System.out.println("음 이름 : 파");
break;
default:
System.out.println("음 이름 : 솔, 라, 시");
}
}
}
출력 결과값은 다음과 같습니다.
음 이름 : 도
인터페이스 내에 선언된 변수는 public, static, final 이 선언된 것으로 간주합니다.
따라서 인터페이스 Scale 을 통해 7개의 상수가 선언되었습니다.
인터페이스 Scale 은 음계를 표현한 상수들을 담고 있습니다.
이 경우 중요한 것은 상수의 값이 아니라 상수의 이름입니다.
즉 상수의 값이 바뀌어도 이름이 바뀌지 않는다면 코드에 아무런 영향을 주지 않습니다.
그리고 이렇게 연관된 상수들을 하나의 인터페이스로 묶어서 선언하는 것이 자바 5 이전에 사용하던 방법입니다.
그러나 이 방법에는 문제가 하나 있는데, 그 문제점을 다음 코드에서 확인하겠습니다.
public interface Animal {
int Dog = 1;
int CAT = 2;
}
public interface Person {
int MAN = 1;
int WOMAN = 2;
}
public class NonSafeConst {
public static void main(String[] args) {
who(Person.MAN);
who(Animal.Dog);
}
public static void who(int man) {
switch (man) {
case Person.MAN:
System.out.println("남자입니다.");
break;
case Person.WOMAN:
System.out.println("여자입니다.");
break;
}
}
}
출력 결과값은 다음과 같습니다.
남자입니다.
남자입니다.
Person.MAN 도 값이 1이고 Animal.DOG 도 값이 1이기 때문에,
위와 같은 실수를 범해도 컴파일 오류는 물론 실행 오류도 발생하지 않습니다.
그 문제점을 해결하기 위해 자바 5에서 열거형이 소개되었습니다.
열거형의 정의
열거형은 다음과 같이 정의합니다.
그 안에 위치한 이름들을 가리켜 '열거형 값' 이라고 합니다.
public enum Scale {
DO, RE, MI, FA, SOL, RA, THI
}
열거형은 클래스와 성격이 유사합니다.
따라서 다음과 같이 참조변수의 선언도 가능합니다.
단 이렇게 선언된 참조변수는 해당 열거형 내에 선언된 '열거형 값'만 대입이 가능합니다.
Scale scale = Scale.DO;
기본적으로 열거형 값은 Scale.DO 와 같이 표현하지만,
case 문에서는 표현의 간결함을 위해 Do 와 같이 열거형 값의 이름만 명시하기로 약속되어 있습니다.
다음은 열거형 이전의 코드를 열거형 이후의 코드로 수정한 코드입니다.
public enum Scale {
DO, RE, MI, FA, SOL, RA, THI
}
public class SimpleEnum {
public static void main(String[] args) {
Scale scale = Scale.DO;
System.out.println(scale);
switch (scale) {
case DO:
System.out.println("음 이름 : 도");
break;
case RE:
System.out.println("음 이름 : 레");
break;
case MI:
System.out.println("음 이름 : 미");
break;
case FA:
System.out.println("음 이름 : 파");
break;
default:
System.out.println("음 이름 : 솔, 라, 시");
}
}
}
출력 결과값은 다음과 같습니다.
DO
음 이름 : 도
그렇다면 열거형 이전의 문제점이 해결되었는지 아래의 코드로 확인해보겠습니다.
public enum Animal {
DOG, CAT
}
public enum Person {
MAN, WOMAN
}
public class SafeEnum {
public static void main(String[] args) {
who(Person.MAN);
who(Animal.DOG);
Animal animal = Person.MAN;
}
public static void who(Person who) {
switch (who) {
case MAN:
System.out.println("남자입니다.");
break;
case WOMAN:
System.out.println("여자입니다.");
break;
}
}
}
열거형 Animal 과 Person 이 정의되었습니다.
코드를 실행시켜보지만 아래의 코드에서 자료형 불일치로 인한 컴파일 오류가 발생합니다.
따라서 앞서 소개한 자바 5 이전의 문제점이 열거형을 사용할 경우 발생하지 않습니다.
who(Animal.DOG);
Animal animal = Person.MAN;
클래스 내에 열거형 정의
특정 클래스 내에서만 사용하고자 하는 열거형 값이 있다면, 아래의 코드와 같이 정의하면 됩니다.
public class Customer {
enum Gender {
MALE, FEMALE
}
private String name;
private Gender gender;
public Customer(String name, String gender) {
this.name = name;
if ("man".equals(gender)) {
this.gender = Gender.MALE;
} else {
this.gender = Gender.FEMALE;
}
}
@Override
public String toString() {
if (gender == Gender.MALE) {
return "Mr " + name;
} else {
return "Ms " + name;
}
}
}
public class InnerEnum {
public static void main(String[] args) {
Customer customer1 = new Customer("Park", "man");
Customer customer2 = new Customer("Han", "woman");
System.out.println(customer1);
System.out.println(customer2);
}
}
출력 결과값은 다음과 같습니다.
Mr Park
Ms Han
열거형 값의 정체
진행하기 전에 먼저 다음과 같이 클래스 정의가 가능함을 소개하고자 합니다.
public class Person {
public static final Person MAN = new Person();
public static final Person WOMAN = new Person();
@Override
public String toString() {
return "I am Person";
}
}
public class InClassInst {
public static void main(String[] args) {
System.out.println(Person.MAN);
System.out.println(Person.WOMAN);
}
}
출력 결과값은 다음과 같습니다.
I am Person
I am Person
위 코드에서 보이듯이 Person 클래스 내에서 Person 형 참조변수를 선언하는 것도, Perosn 인스턴스를 생성하는 것도 가능합니다.
그럼 다음 코드를 보겠습니다. 이 코드에서는 열거형 값이 해당 자료형의 인스턴스라는 사실을 알려줍니다.
public enum Person {
MAN, WOMAN;
@Override
public String toString() {
return "Enum Person Object";
}
}
public class EnumConst {
public static void main(String[] args) {
System.out.println(Person.MAN);
System.out.println(Person.WOMAN);
}
}
출력 결과값은 다음과 같습니다.
Enum Person Object
Enum Person Object
위 결과에서 확인할 수 있는 점은 다음과 같습니다.
열거형은 클래스입니다. 모든 열거형은 java.lang.Enum<E> 클래스를 상속합니다. 그리고 Enum<E> 는 Object 클래스를 상속합니다. 이러한 측면에서 볼 때 열거형은 클래스입니다.
열거형의 값은 참조변수 입니다. 이에 대한 증거로 출력 결과값에서 toString 메소드가 호출되었음을 보면 알 수 있습니다.
열거형의 정의에도 생성자가 따로 없다면 디폴트 생성자가 삽입됩니다.
다만 이 생성자는 private으로 선언되어 직접 인스턴스를 생성하는 것이 불가능할 뿐입니다.
이번에는 열거형 생성자를 정의해봄으로서, 열거형 값의 정체를 더 알아보겠습니다.
public enum Person {
MAN, WOMAN;
private Person() {
System.out.println("Person 생성자 호출");
}
@Override
public String toString() {
return "Enum Person Object";
}
}
public class EnumConstructor {
public static void main(String[] args) {
System.out.println(Person.MAN);
System.out.println(Person.WOMAN);
}
}
출력 결과값은 다음과 같습니다.
Person 생성자 호출
Person 생성자 호출
Enum Person Object
Enum Person Object
위 코드에서는 두 개의 열거형 값이 존재하기에 두 번의 생성자 호출이 이뤄졌습니다.
따라서 열거형 값의 정체는 다음과 같이 표현할 수 있습니다.
열거형 값은 생성자가 private이라 실제 컴파일은 되지 않습니다.
public static final Person MAN = new Person();
public static final Person WOMAN = new Person();
열거형의 생성자 정의 방법과 호출 방법
public enum Person {
MAN(29),
WOMAN(25);
int age;
private Person(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" + "age=" + age + '}';
}
}
public class EnumParamConstructor {
public static void main(String[] args) {
System.out.println(Person.MAN);
System.out.println(Person.WOMAN);
}
}
출력 결과값은 다음과 같습니다.
열거형의 생성자는 무조건 private 으로 선언해야 합니다.
그리고 열거형 값의 선언에서 아래의 코드와 같이 소괄호를 통해서 생성자에 인자를 전달할 수 있습니다.
Person{age=29}
Person{age=25}
결론은 간단합니다.
열거형도 Object 클래스를 상속하는 일종의 클래스입니다.
따라서 생성자는 물론, 인스턴스 변수와 메소드 둘 다 가질 수 있습니다.
다만 모든 생성자를 private으로 선언해야 하기 때문에 열거형 값이 유일한 인스턴스 생성 방법이라는 차이가 있을 뿐입니다.
구현 클래스에 따라 메소드의 동작 방식의 변경이 필요한 경우에는 공통의 메소드를 오버라이딩하여 사용함으로서 아래의 이점을 얻을 수 있습니다.
1. 생산성의 향상을 도모
2. 연관된 일련의 클래스들에 대해 공통적인 규약을 정의
아래의 코드는 그 목적에 맞게 추상 클래스를 이용한 코드입니다.
public abstract class Animal {
private final String name;
private final int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void information() {
System.out.println("이름은 " + name);
System.out.println("나이는 " + age);
}
abstract void cry();
}
public class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
@Override
void cry() {
System.out.println("멍멍");
}
}
public class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
}
@Override
void cry() {
System.out.println("야옹");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Park", 11);
Animal cat = new Cat("Han", 10);
dog.information();
cat.information();
dog.cry();
cat.cry();
}
}
결과값은 다음과 같습니다.
이름은 Park
나이는 11
이름은 Han
나이는 10
멍멍
야옹
추상 클래스는 말 그대로 인스턴스를 생성할 수 없습니다.
아래의 코드와 같이 익명 클래스를 구현해서 사용하는 방법 밖에는 없습니다.
Animal animal = new Animal() {
@Override
public void cry() {
System.out.println("익명 클래스");
}
};
단, 다음 인터페이스를 구현한 인스턴스를 구현한 인스턴스를 대상으로만 clone 메소드를 호출할 수 있습니다.
만약 Cloneable 인터페이스를 구현하지 않은 클래스의 인스턴스를 대상으로 clone 메소드를 호출하면,
CloneNotSupportedException 예외가 발생합니다.
Cloneable 인터페이스의 구현은 "이 클래스의 인스턴스는 복사해도 됩니다" 와 같은 마커 인터페이스 역할을 합니다.
즉, 정의해야 할 메소드가 존재하지 않는, 복사를 해도 된다는 표식의 인터페이스입니다.
인스턴스의 복사는 클래스에 따라 허용해서는 안되는 작업이 될 수 있습니다.
따라서 인스턴스 복사의 허용 여부를 클래스를 정의하는 과정에서 고민하고 결정한 후 Cloneable 인터페이스를 구현해서 clone 메소드의 호출이 가능하도록 하면 됩니다.
아래의 코드로 확인해보겠습니다.
public class Point implements Cloneable{
private int xPos;
private int yPos;
public Point(int xPos, int yPos) {
this.xPos = xPos;
this.yPos = yPos;
}
public void showPosition() {
System.out.printf("[%d, %d]", xPos, yPos);
System.out.println();
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class InstanceCloning {
public static void main(String[] args) throws CloneNotSupportedException {
Point original = new Point(3, 5);
Point copy;
copy = (Point) original.clone();
original.showPosition();
copy.showPosition();
}
}
결과 출력은 다음과 같습니다.
[3, 5]
[3, 5]
실행 결과는 인스턴스의 복사가 정상적으로 이뤄졌음을 보여줍니다.
이건 관련성이 없는 문제이지만, 위 코드에서는 clone 메소드를 다음과 같이 오버라이딩 했습니다.
잘보면 그저 상위 클래스의 clone 메소드를 호출한 것이 전부이기 때문에, 오버라이딩이 무의미해 보입니다.
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
그러나 중요한 차이가 있습니다.
앞서 Object 에 정의된 clone 메소드는 접근제어지시자가 protected 로 되어있었습니다.
즉 오버라이딩의 목적은 protected -> public 으로 접근 범위를 넓힌 것입니다.
하지만 반대의 경우인 public -> protected 로 접근 범위를 제한하는 형태의 오버라이딩은 불가능합니다.
마지막으로 얕은 복사와 깊은 복사라는 개념을 아래의 코드로 확인해보겠습니다.
public class Point implements Cloneable{
private int xPos;
private int yPos;
public Point(int xPos, int yPos) {
this.xPos = xPos;
this.yPos = yPos;
}
public void showPosition() {
System.out.printf("[%d, %d]", xPos, yPos);
System.out.println();
}
public void changePos(int x, int y) {
xPos = x;
yPos = y;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Rectangle implements Cloneable{
private Point upperLeft; //좌측 상단 좌표
private Point lowerRight; //우측 상단 좌표
public Rectangle(int x1, int y1, int x2, int y2) {
upperLeft = new Point(x1, y1);
lowerRight = new Point(x2, y2);
}
public void changePos(int x1, int y1, int x2, int y2) {
upperLeft.changePos(x1, y1);
lowerRight.changePos(x2, y2);
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void showPosition() {
System.out.print("좌측 상단 좌표 : ");
upperLeft.showPosition();
System.out.print("우측 하단 좌표 : ");
lowerRight.showPosition();
System.out.println();
}
}
public class ShallowCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Rectangle original = new Rectangle(1, 1, 9, 9);
Rectangle copy;
copy = (Rectangle) original.clone();
original.changePos(2, 2, 7, 7);
original.showPosition();
copy.showPosition();
System.out.println(original);
System.out.println(copy);
System.out.println(original.getUpperLeft());
System.out.println(original.getLowerRight());
System.out.println(copy.getUpperLeft());
System.out.println(copy.getLowerRight());
}
}
public class ParentAdder {
public int add(int a, int b) {
return a + b;
}
}
public class ChildAdder extends ParentAdder {
public double add(double a, double b) {
System.out.println("ChildAdder.add");
return a + b;
}
}
public class OverrideMistake {
public static void main(String[] args) {
ParentAdder parentAdder = new ChildAdder();
System.out.println(parentAdder.add(3, 4));
}
}
이 코드 작성자의 기대값은 이렇습니다.
ChildAdder.add
7
하지만 실제 결과값은 다음과 같습니다.
7
실수를 찾으셨나요?
개발자의 실수는 아래의 코드입니다.
public double add(double a, double b) {}
개발자의 의도는 상위 클래스의 add 메소드를 Override 하는 것이었습니다.
하지만 매개변수형과 반환형이 달랐기 때문에 Override 되지 않았고, 그 결과값이 달라지게 된 것입니다.
이런 유형의 실수는 매우 흔합니다.
그럼에도 불구하고 발견이 쉽지 않기 때문에 치명적인 실수가 될 수 있습니다.
개발자의 실수들은 컴파일 과정에서 확인되는 것이 가장 좋습니다.
그러나 위의 경우에는 정상적으로 컴파일도 되고 실행도 됩니다.
이러한 상황을 방지하기 위해서 @Override 어노테이션을 사용하는 것입니다.
쉽게 말해 자바 컴파일러에게 '나는 상위 클래스의 메소드를 오버라이딩(Overriding) 할거야!'
라는 메모를 남기는 것이라고 생각하면 됩니다.
이제 개발자의 실수가 들어간 코드에 @Override 어노테이션을 적용해보겠습니다.
public class ChildAdder extends ParentAdder {
@Override
public double add(double a, double b) {
System.out.println("ChildAdder.add");
return a + b;
}
}
결과는 아래와 같습니다.
컴파일 과정에서 오류가 발생하여 프로그램이 실행되지 않고, 컴파일러가 친절하게 오류 원인을 제공합니다.
컴파일러 덕분에 실수를 인지할 수 있었고, 코드를 다음과 같이 수정할 수 있었습니다.
public class ChildAdder extends ParentAdder {
public int add(int a, int b) {
System.out.println("ChildAdder.add");
return a + b;
}
}
코드의 결과값은 다음과 같습니다.
드디어 개발자가 원하는 결과를 제공할 수 있게 되었습니다.
ChildAdder.add
7
코드를 잘 보시면 @Override 어노테이션이 없다는 것을 알 수 있습니다.
맞습니다. 해당 어노테이션이 존재하지 않아도 정상적으로 Override 를 할 수 있습니다.
하지만, 우리는 조금 더 안정적인 코드 작성 습관을 들여야할 필요가 있습니다.
따라서 다음과 같이 메소드를 수정합니다.
package inheritance;
public class ChildAdder extends ParentAdder {
@Override
public int add(int a, int b) {
System.out.println("ChildAdder.add");
return a + b;
}
}
예외 클래스의 예와 그 발생 상황을 정리하면 다음과 같습니다.(객체명에 Error가 포함됩니다.)
VirtualMachineError -> 가상머신에 심각한 오류 발생
IOError -> 입출력 관련해서 코드 수준 복구가 불가능한 오류 발생
VirtualMachineError의 구체적인 예
VirtualMachineError 클래스를 상속하는 예외 클래스로 OutOfMemoryError가 있습니다.
이는 프로그램의 실행에 필요한 메모리 공간이 부족한 상황에서 발생하는 예외입니다.
따라서 이 예외가 발생하면 메모리를 비효율적으로 또는 부적절하게 사용하는 부분의 코드를 수정해야 합니다.
IOError의 구체적인 예
자바 프로그램이 임의의 파일에 저장된 데이터를 읽는 중에 갑자기 하드디스크에 물리적 오류가 발생하여 더 이상 파일에 저장된 데이터를 읽을 수 없는 상황이 생길 수 있습니다.
그리고 이런 상항에서 발생하는 것이 IOError Exception 입니다.
즉 Error 클래스를 상속하는 예외는 처리의 대상이 아닙니다.
바꿔 말하면 처리할 수 있는 예외가 아닙니다. 따라서 이런 유형의 예외가 발생하면 그냥 프로그램이 종료되도록 놔두고 이후에 원인을 파악하는 과정이 필요합니다.
2. RuntimeException 클래스를 상속하는 예외 클래스
앞서 보였던 예외 클래스가 이에 해당합니다.
ArithmeticException
ClassCastException
IndexOutOfBoundsException
NegativeArraySizeException -> 배열 생성시 길이를 음수로 지정하는 예외 발생
NullPointerException
ArrayStoreException -> 배열에 적절치 않는 인스턴스를 저장하는 예외 발생
3. Exception 클래스를 상속하는 예외 클래스
마지막으로 Exception 클래스를 상속하는 예외 클래스가 있습니다.
RuntimeException 클래스를 직접 혹은 간접적으로 상속하지 않고 Exception 클래스만을 상속하는 예외클래스가 해당됩니다.
그 수로만 따지면 나머지 둘에 비해 가장 많습니다.
그리고 이들은 특정 클래스 또는 메소드에 연관되어 있어서 나열하여 설명하는 것은 의미가 없습니다.
앞으로 공부하면서 이 부류에 속한 예외 클래스를 하나씩 접하게 됩니다.
Exception을 상속하는 예외 클래스 중에서 비교적 빨리 접하게 될 클래스는 다음과 같습니다.
java.io.IOException
프로그래머가 정의하는 예외
지금까지 소개한 예외 클래스는 모두 자바에서 정의한 클래스였습니다.
그러나 프로그래머가 직접 예외 클래스를 정의하고 이를 기반으로 특정 상황에서 예외가 발생하도록 할 수 있습니다.
프로그래머가 정의하는 예외 클래스의 예의 핵심은 Exception 을 상속하는데 있습니다.
Exception 클래스를 아래의 코드로 구현해보겠습니다.
package ExceptionHandling;
public class ReadAgeException extends Exception {
public ReadAgeException() {
super("유효하지 않은 나이가 입력되었습니다.");
}
}
위의 클래스는 Exception 을 상속하는 점을 제외하면 일반 클래스와 차이가 없습니다.
생성자에서는 상위 클래스의 생성자를 호출하면서 예외 상황에 대한 설명을 담고 있는 문자열을 전달하는데,
이 문자열은 앞서 보였던 Throwable 클래스에 정의된 public String getMessage() 메소드 호출 시 반환 됩니다.
그럼 우리가 정의한 ReadAgeException 예외 클래스를 대상으로 예외를 발생시키고 이를 처리해보겠습니다.
package ExceptionHandling;
import java.util.Scanner;
public class MyExceptionClass {
public static void main(String[] args) {
System.out.println("나이 입력 : ");
try {
int age = readAge();
System.out.printf("입력된 나이 : %d", age);
} catch (ReadAgeException e) {
System.out.println(e.getMessage());
}
}
public static int readAge() throws ReadAgeException {
Scanner scanner = new Scanner(System.in);
int age = scanner.nextInt();
if (age < 0) {
throw (new ReadAgeException());
}
return age;
}
}
정수를 입력받았는데 그 수가 음수인 것은 문법적으로는 오류가 아닙니다.
하지만 프로그램 특성상 사람의 나이는 음수가 될 수 없으므로 오류가 맞습니다.
이러한 상황을 예외로 처리하기 위해서 예외 클래스를 직접 정의하였습니다.
이렇듯 예외 클래스의 인스턴스를 생성하고, 이를 대상으로 throw 선언을 하면 예외가 발생합니다.
물론 이렇게 발생한 예외도 Exception 을 상속하는 예외이므로 try ~ catch 문으로 처리를 하거나 throws 선언을 통해 넘겨야 합니다.
잘못된 catch 구문의 구성
다음과 같이 세 개의 예외 클래스가 정의되었다고 가정합니다.
class FirstException extends Exception {
}
class SecondException extends FirstException {
}
class ThirdException extends SecondException {
}
그리고 위의 세 종류 예외가 모두 발생 가능한 영역에 다음과 같이 try ~ catch 문을 구성하였다고 가정합니다.