반응형

목차

1. Java 프로젝트에 롬복 적용

2. Spring Boot 프로젝트에 롬복 적용

3. Stable 기능

3-1. val

3-2. var

4. Experimental 기능


1. Java 프로젝트에 롬복 적용

1) 롬복 라이브러리 파일 설치

프로젝트에서 롬복을 사용하려면 라이브러리 파일이 필요합니다.

https://projectlombok.org/download

 

Download

 

projectlombok.org

 

2) 프로젝트에 라이브러리 적용

File -> Project Structure(Ctrl + Alt + Shift + S)

Libraries 에 설치한 lombok.jar 지정

External Libraries 에 lombok 라이브러리 추가 확인

 

3) Lombok Plugins 설치

4) Enable annotation processing 체크


2. Spring Boot 프로젝트에 롬복 적용

1) Dependency 설정

https://mvnrepository.com/artifact/org.projectlombok/lombok

gradle dependency

// https://mvnrepository.com/artifact/org.projectlombok/lombok
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

 

2) Lombok Plugins 설치

3) Enable annotation processing 체크


3. Stable 기능

1) val

val 은 변수의 type 을 선언하는 것 대신 사용할 수 있습니다.

val 은 local 변수와 foreach 식에서 사용할 수 있습니다.

자동적으로 type 을 유추하여 final 로 선언합니다. 

val 을 사용하려면 lombok.val 패키지를 import 해야 합니다.

 

이 기능은 NetBeans 에서 동작하지 않습니다.

 

package lombokTest;

import lombok.val;

import java.util.ArrayList;
import java.util.HashMap;

public class ValExample {
    public static void main(String[] args) {
        System.out.println(exampleMethod());
        exampleMethodTwo();
    }

    public static String exampleMethod() {
        val valList = new ArrayList<String>();
        valList.add("HELLO WORLD");
        val valVariable = valList.get(0);
        return valVariable.toLowerCase();
    }

    public static void exampleMethodTwo() {
        val map = new HashMap<Integer, String>();
        map.put(0, "Zero");
        map.put(5, "Five");
        for (val entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

 

2) var

var 은 지역 변수에 final 키워드를 넣지 않는 것 빼고는 val 과 동일합니다.

package lombokTest;

import java.util.ArrayList;
import java.util.HashMap;

public class VarExample {
    public static void main(String[] args) {
        System.out.println(exampleMethod());
        exampleMethodTwo();
    }

    public static String exampleMethod() {
        var valList = new ArrayList<String>();
        valList.add("HELLO WORLD");
        var valVariable = valList.get(0);
        valVariable = "ABC";
        return valVariable.toLowerCase();
    }

    public static void exampleMethodTwo() {
        var map = new HashMap<Integer, String>();
        map.put(0, "Zero");
        map.put(5, "Five");
        map.put(5, "Seven");
        for (var entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

 

 


4. Experimental 기능

 

반응형
반응형

 

반응형

'JAVA > 기본 개념' 카테고리의 다른 글

#10 열거형(Enum)  (0) 2022.08.31
#5 추상 클래스 (Abstract Class)  (0) 2022.08.24
#8 Object 클래스  (0) 2022.08.23
#4 예외처리(Exception Handling)  (0) 2022.08.22
#3 인터페이스  (0) 2022.08.22
반응형

열거형의 목적

열거형은 자바 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

 

위 결과에서 확인할 수 있는 점은 다음과 같습니다.

  1. 열거형은 클래스입니다.
    모든 열거형은 java.lang.Enum<E> 클래스를 상속합니다.
    그리고 Enum<E> 는 Object 클래스를 상속합니다.
    이러한 측면에서 볼 때 열거형은 클래스입니다.
  2. 열거형의 값은 참조변수 입니다.
    이에 대한 증거로 출력 결과값에서 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으로 선언해야 하기 때문에 열거형 값이 유일한 인스턴스 생성 방법이라는 차이가 있을 뿐입니다.

 

 

오늘도 좋은 하루 보내세요.

감사합니다.

반응형

'JAVA > 기본 개념' 카테고리의 다른 글

#11 Optional  (0) 2022.09.05
#5 추상 클래스 (Abstract Class)  (0) 2022.08.24
#8 Object 클래스  (0) 2022.08.23
#4 예외처리(Exception Handling)  (0) 2022.08.22
#3 인터페이스  (0) 2022.08.22
반응형

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

  1. 추상 클래스는 접근제어자가 public, protected, private 인 메소드를 가질 수 있습니다.
    반면에 인터페이스는 public 메소드만 가질 수 있습니다.
  2. 추상 클래스에는 멤버변수 선언이 가능하지만 인터페이스는 public static 변수만 선언이 가능합니다.
  3. 추상클래스는 구현 클래스에서 extends 예약어를 사용합니다.
    인터페이스는 구현 클래스에서 implements 예약어를 사용합니다.

 

 

 

이상입니다.

오늘도 좋은 하루 보내세요.

감사합니다.

반응형

'JAVA > 호기심천국' 카테고리의 다른 글

#1 @Override 를 쓰는 이유  (0) 2022.08.23
반응형

추상 클래스의 특징

1. 추상 메소드를 하나라도 가진 클래스입니다.

여기서 추상 메소드란 메소드의 선언부만 존재하고, 구현부는 없는 메소드입니다.

2. 클래스이기 때문에 변수를 가질 수 있습니다.

3. 추상 클래스를 extends 한 클래스가 추상 메소드를 구현하도록 강제합니다.

4. 추상 메소드에 필수적으로 abstract 예약어를 작성해야 합니다.

자바에서 추상 메소드는 다음과 같이 작성합니다.

abstract 반환형 메소드명();

5. 추상 클래스는 인스턴스를 생성할 수 없습니다.

6. 구현 클래스에서는 extends 예약어를 사용합니다.


자바에서 추상 클래스를 사용하는 목적

중복되는 부분이나 공통적인 부분은 공통 메소드를 사용하고,

구현 클래스에 따라 메소드의 동작 방식의 변경이 필요한 경우에는 공통의 메소드를 오버라이딩하여 사용함으로서 아래의 이점을 얻을 수 있습니다.

 

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("익명 클래스");
            }
        };

 

이상입니다.

 

오늘도 좋은 하루 보내세요.

감사합니다.

반응형

'JAVA > 기본 개념' 카테고리의 다른 글

#11 Optional  (0) 2022.09.05
#10 열거형(Enum)  (0) 2022.08.31
#8 Object 클래스  (0) 2022.08.23
#4 예외처리(Exception Handling)  (0) 2022.08.22
#3 인터페이스  (0) 2022.08.22
반응형

Object 클래스는 모든 클래스가 직간접적으로 상속을 하는 클래스이므로 모든 클래스들의 조상이라고 할 수 있습니다.

Object 클래스 내부의 메소드 중에서도 아래의 메소드들을 순서대로 정리해보겠습니다.

1. finalize

2. equals

3. clone


1. finalize 메소드

Object 클래스에는 finalize 메소드가 다음과 같이 정의되어 있습니다.

이 메소드는 아무도 참조하지 않는 인스턴스가 가비지 컬렉션에 의해 소멸되기 전에 자동으로 호출되는 메소드입니다.

    protected void finalize() throws Throwable { }

 

따라서 인스턴스 소멸 시 반드시 실행해야 할 코드가 있다면 이 메소드의 오버라이딩을 고려해볼 수 있습니다.

아래는 간단하게 작성한 예제입니다.

public class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Instance Destroy : " + name);
    }
}

public class FinalizeMethod {
    public static void main(String[] args) {
        Person person1 = new Person("Park");
        Person person2 = new Person("Han");

        person1 = null;
        person2 = null;

        System.gc();
        System.runFinalization();
        
        System.out.println("Main Method End");
    }
}​

 

위 예제 코드에서는 인스턴스의 소멸을 강제하기 위해 GC를 강제로 진행했지만,

평소 JVM은 매우 합리적인 방법으로 GC를 수행하기 때문에 특별한 상횡이 아니라면 강제하지 않는 것이 좋습니다.

결과 출력값은 다음과 같습니다.

Instance Destroy : Park
Instance Destroy : Han
Main Method End

2. equals 메소드

equals 메소드는 간단하게  두 개의 값을 비교할 때 사용합니다.

따라서 비슷한 역할을 가진 == 연산자와 많은 비교를 하곤 합니다.

 

이 둘은 양 쪽의 값을 비교한 후 그 결과를 boolean type 으로 반환한다는 공통점을 가집니다.

하지만 가장 눈에 띄는 차이점으로는 equals() 는 메소드이고, ==은 연산자라는 점이 있습니다.

우선 자세한 내용은 아래의 코드들을 통해 알아보도록 합시다.

 

Object 클래스에는 equals 메소드가 다음과 같이 정의되어 있습니다.

    public boolean equals(Object obj) {
        return (this == obj);
    }

 

equals 메소드는 양 쪽의 내용을 비교해야하는 상황일 때 오버라이딩을 고려해볼 수 있습니다.

아래는 간단하게 작성한 예제입니다.

public class INum {
    private int num;

    public INum(int num) {
        this.num = num;
    }

    @Override
    public boolean equals(Object obj) {
        return this.num == ((INum) (obj)).num;
    }
}

public class ObjectEquality {
    public static void main(String[] args) {
        INum num1 = new INum(10);
        INum num2 = new INum(20);
        INum num3 = new INum(10);

        if (num1.equals(num2)) {
            System.out.println("num1 = num2");
        } else {
            System.out.println("num1 != num2");
        }

        if (num1.equals(num3)) {
            System.out.println("num1 = num3");
        } else {
            System.out.println("num1 != num3");
        }
    }
}

 

결과 출력값은 다음과 같습니다.

num1 != num2
num1 = num3

 

위 코드의 INum 클래스는 Object 클래스의 equals 메소드를 다음과 같이 클래스 내에 선언된 인스턴스 변수의 내용을 비교하여,

그 결과에 따라 true 또는 false의 boolean 값을 반환하도록 오버라이딩 했습니다.

@Override
    public boolean equals(Object obj) {
        return this.num == ((INum) (obj)).num;
    }

 

이렇듯 두 인스턴스의 비교 조건은 해당 클래스를 정의하는 개발자가 결정해야 합니다.

그리고 그 결정 사항을 equals 메소드를 equals 메소드의 오버라이딩을 통해 반영해야 합니다.

Object 메소드는 참조변수의 참조값을 비교하도록 정의되어 있습니다.

그런데 == 연산을 통해서도 참조값은 비교가 가능합니다.

따라서 equals 메소드의 호출을 통해 참조값을 비교할 필요는 없습니다.

즉 equals 메소드는 내용 비교가 이뤄지도록 오버라이딩 하라고 존재하는 메소드입니다.

자바에서 제공하는 표준 클래스의 경우 equals 메소드가 이미 내용 비교를 하도록 오버라이딩 된 경우가 많습니다.

그 대표적인 예가 String 클래스인데 이와 관련하여 다음 코드를 보겠습니다.

public class StringEquality {
    public static void main(String[] args) {
        String str1 = new String("Park");
        String str2 = new String("Park");

        if (str1 == str2) {
            System.out.println("str1 = str2");
        } else {
            System.out.println("str1 != str2");
        }

        if (str1.equals(str2)) {
            System.out.println("str1 = str2");
        } else {
            System.out.println("str1 != str2");
        }
    }
}

 

결과 출력은 다음과 같습니다.

str1 != str2
str1 = str2

 

분명 일반적인 equals 메소드일 경우는 str1 과 str2는 동일하지 않다고 출력할 것입니다.

하지만 결과는 그렇지 않습니다.

그 이유는 String 내부에 오버라이딩 된 equals 메소드는 내용을 비교하도록 정의되어있기 때문입니다.

 public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

 

결론입니다.

두 인스턴스의 내용 비교를 원한다면 Object 클래스의 equals 메소드를 오버라이딩하여 사용합니다.

자바 개발자들이 원한 설계가 바로 그것입니다.

만약 단순한 참조변수의 참조값을 비교하려면 == 연산을 사용합니다.


3. clone 메소드

Object 클래스에는 인스턴스의 복사를 위한 다음 메소드가 정의되어 있습니다.

protected native Object clone() throws CloneNotSupportedException;

 

이 메소드가 호출되면, 호출된 메소드가 속한 인스턴스의 복사본이 생성되고,

이렇게 만들어진 복사본의 참조값이 반환됩니다.

단, 다음 인터페이스를 구현한 인스턴스를 구현한 인스턴스를 대상으로만 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());
    }
}

 

결과 기대값은 아래와 같습니다.

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

좌측 상단 좌표 : [1, 1]
우측 하단 좌표 : [9, 9]

 

실제 결과 출력값은 아래와 같습니다.

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

 

예상했던 결과와 상이합니다.

인스턴스가 정상적으로 복사된 것은 맞지만,

Rectangle 클래스 내부의 Point 형 참조변수인 upperLeft 와 lowerRight 의 참조값이 그대로 새 인스턴스에 복사가 된 것입니다.

	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());

출력 결과값은 다음과 같습니다.

복사본의 내부 참조변수의 참조값이 원본의 참조변수의 참조값과 동일하단 것을 알 수 있습니다.

그리고 이런 현상을 '얕은 복사'라고 합니다.

또한 원래 기대하던 완전한 복사를 '깊은 복사'라고 생각하면 됩니다.

objectClass.Rectangle@60e53b93
objectClass.Rectangle@5e2de80c
objectClass.Point@1d44bcfa
objectClass.Point@266474c2
objectClass.Point@1d44bcfa
objectClass.Point@266474c2

 

깊은 복사를 하기 위해 Rectangle 에 오버라이딩 된 clone 메소드를 아래의 코드와 같이 수정합니다.

    @Override
    public Object clone() throws CloneNotSupportedException {
        Rectangle copy = (Rectangle) super.clone();

        copy.upperLeft = (Point) upperLeft.clone();
        copy.lowerRight = (Point) lowerRight.clone();

        return copy;
    }

 

출력 결과값입니다.

처음 기대한 값 그대로 결과값으로 확인할 수 있습니다.

그렇다면 String 도 참조형인데 방금과 같은 깊은 복사를 적용해야 하나? 라는 의문점이 생길 수도 있습니다.

다행히도 String 인스턴스의 내용을 이루는 문자열은 인스턴스 생성 시 결정이 되고,

변경이 불가능하기 때문에 얕은 복사를 해도 문제 없습니다.

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

좌측 상단 좌표 : [1, 1]
우측 하단 좌표 : [9, 9]

objectClass.Rectangle@60e53b93
objectClass.Rectangle@5e2de80c
objectClass.Point@1d44bcfa
objectClass.Point@266474c2
objectClass.Point@6f94fa3e
objectClass.Point@5e481248

 

이상입니다.

오늘도 좋은 하루 보내세요.

감사합니다.

반응형

'JAVA > 기본 개념' 카테고리의 다른 글

#10 열거형(Enum)  (0) 2022.08.31
#5 추상 클래스 (Abstract Class)  (0) 2022.08.24
#4 예외처리(Exception Handling)  (0) 2022.08.22
#3 인터페이스  (0) 2022.08.22
#1 자바 개발 핵심 원칙  (0) 2022.07.11
반응형

우선 결론부터 말씀 드리겠습니다.

@Override 어노테이션은 쓰지 않아도 프로그램 동작에는 문제가 없지만,

안정적인 프로그램을 구현하기 위해서는 작성하는 것이 정석입니다.

 


백문이 불여일견,

한 번 코드로 확인해보겠습니다.

이 코드는 정상적으로 컴파일도 되고 실행도 잘 됩니다.

아래의 코드들의 동작 결과를 예상해보시고, 개발자의 실수를 찾아보세요.

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;
    }
}

 

 

이상입니다.

오늘도 좋은 하루 보내세요.

감사합니다.

반응형

'JAVA > 호기심천국' 카테고리의 다른 글

#2 추상클래스 vs 인터페이스  (0) 2022.08.25
반응형

자바의 예외처리 관련 내용이 의외로 적지 않습니다.

따라서 예외처리를 하지 않는 상황에서는 불필요한 내용이라 생각하고 넘어가고 싶을 수도 있지만,

자바에서는 예외처리가 차지하는 부분이 크기 때문에 마지막까지 이해하고 넘어가야 합니다.

 

예외 상황을 알리기 위해 정의된 클래스의 종류

package ExceptionHandling;

public class ArrayIndexOutOfBounds {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        for (int i = 0; i < 4; i++) {
            System.out.println(arr[i]);
        }
    }
}

package ExceptionHandling;

class Board {
}

class PBoard extends Board {
}

public class ClassCast {
    public static void main(String[] args) {
        Board pboard1 = new PBoard();
        PBoard pBoard2 = (PBoard) pboard1;

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

        Board board1 = new Board();
        PBoard board2 = (PBoard) board1;
    }
}

package ExceptionHandling;

public class NullPointer {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str);
        int length = str.length();
    }
}


예외 클래스의 구분

예외 클래스의 최상위 클래스는 Throwable 입니다.

그런데 이를 상속하는 예외 클래스는 다음과 같이 세 부류로 나뉩니다.

  1. Error 클래스를 상속하는 예외 클래스
  2. RuntimeException 클래스를 상속하는 예외 클래스 -> RuntimeException 클래스는 Exception 클래스를 상속합니다.
  3. Exception 클래스를 상속하는 예외 클래스

1.  Error 클래스를 상속하는 예외 클래스

예외 클래스의 예와 그 발생 상황을 정리하면 다음과 같습니다.(객체명에 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 문을 구성하였다고 가정합니다.

 try {
            ...
        } catch (FirstException e) {

        } catch (SecondException e) {

        } catch (ThirdException e) {
            
        }

예외처리의 내용만 놓고 보면 문제가 없어보이지만 컴파일 오류가 발생합니다.

그리고 그 오류의 내용은 다음과 같습니다.

"두 번째, 세 번째 catch 구문은 실행될 일이 절대 없습니다."

그 이유는 예외 클래스 Second, Third 가 직접 혹은 간접적으로 FirstException 을 상속하고 있기 때문입니다.

그래서 catch (FirstException e) {} 구문에서 모든 예외 인스턴스를 처리할 수 있기 때문에 컴파일 오류가 발생합니다.

따라서 위와 같이 catch 문을 구성하고자 한다면 다음과 같이 순서를 변경해야 합니다.

try {
            ...
        } catch (ThirdException e) {

        } catch (SecondException e) {

        } catch (FirstException e) {
            
        }

finally 구문

try ~ catch 문은 하나의 문장이므로 try 구문 홀로 존재할 수 없습니다.

반드시 catch 구문이 하나 이상 등장해야 합니다.

그런데 try 에 이어서 다음과 같이 finally 구문을 둘 수도 있습니다.

반응형

'JAVA > 기본 개념' 카테고리의 다른 글

#5 추상 클래스 (Abstract Class)  (0) 2022.08.24
#8 Object 클래스  (0) 2022.08.23
#3 인터페이스  (0) 2022.08.22
#1 자바 개발 핵심 원칙  (0) 2022.07.11
[Java] 컴파일(Compile)의 이해  (0) 2020.11.04

+ Recent posts