Java 클래스에서 of(), from(), getInstance()와 같은 정적 팩토리 메소드를 많이 사용하는 것을 보고, 정적 팩토리 메소드를 사용하는 이유에 대해서 궁금하여 글을 정리하게 되었습니다.

 

 

1. 정적 팩토리 메소드(static factory method)는 무엇인가?

  • 정적 팩토리 메소드는 객체 생성의 역할을 하는 클래스 메소드이다.
  • 자바 코드를 짜봤다면 정적 팩토리 메소드를 써본 적이 있을 것입니다.
public class Example {
    public static void main(String[] args) {
        // 직접 생성자를 통해서 객체를 생성
        List<String> list1 = new ArrayList<>();
        list1.add("김씨");
        list1.add("박씨");
        list1.add("정씨");
        list1.add("이씨");
        list1.add("윤씨");
        
        // 정적 팩토리 메소드를 통해서 객체를 생성
        List<String> list2 = List.of("김씨", "박씨", "정씨", "이씨", "윤씨");
    }
}

위 코드는 List.of()를 사용하여 직접 생성자를 사용하지 않고, 리스트를 만드는 방법에 대한 코드입니다.

 

 

 

2. 정적 팩토리 메소드는 왜 사용하는가?

  • 사실 저도 코드를 짜면서 이 부분이 가장 의문이었습니다. 객체 생성은 생성자라는 것을 통해서 할 수 있는데, 굳이 왜 정적 팩토리 메소드를 사용해서 객체를 생성하는 것일까에 대한 의문입니다.
  • 정적 팩토리 메소드를 사용하는 이유를 알아보기 위해서는 생성자와 어떤 점이 다르고, 어떤 점에서 더 좋은 점이 있는지 먼저 알아야 합니다.

 

 

 

3. 정적 팩토리 메소드와 생성자의 차이

(1) 이름을 가질 수 있습니다.

  • 객체의 생성은 목적에 따라서 생성자를 구별해서 사용해야 합니다. 생성자는 이름을 가지고 있지 않기 때문에 목적에 맞게 객체를 생성하려면 해당 클래스의 내부 구조를 잘 알고 있어야 합니다
  • 정적 팩토리 메소드를 사용하면 객체 생성 목적에 맞게 이름을 지정할 수 있습니다. 이를 통해 얻을 수 있는 효과는 코드의 가독성을 더욱 높일 수 있습니다.

 

 

(2) 호출할 때마다 새로운 객체를 생성할 필요가 없습니다.

  • 생성자를 호출하여 객체를 가져올 때는 새로운 객체를 생성해야 합니다.
  • 하지만 만약 싱글톤처럼 미리 만들어져 있는 객체를 가져와서 사용해도 되는 상황이라면 굳이 생성자를 직접 호출해서 사용할 필요가 없습니다.
  • 이때 정적 팩토리 메소드를 이용하면 호출할 때마다 새로운 객체를 생성하지 않아도 됩니다. 정적 팩토리 메소드를 사용해서 객체의 생성을 제한하고 싶다면 객체의 생성자를 private로 설정하는 것이 좋습니다.

 

 

(3) 하위 자료형 객체를 반환할 수 있습니다.

  • 생성자 역할을 하는 정적 팩토리 메소드는 반환값을 가지고 있기 때문에 하위 타입의 객체를 반환할 수 있습니다.
  • 아래는 점수만 입력하면 해당 점수에 맞는 티어의 타입을 반환해 주는 분기분을 팩토리 메소드에서 구현할 수 있습니다.
public class Level {
    public static Level of(int score) {
        if (score < 1000) {
            return new Bronze();
        } else if (score < 10000) {
            return new Silver();
        } else if (score < 100000) {
            return new Gold();
        } else {
            return new Platinum();
        }
    }
}

class Bronze extends Level {
    public Bronze() {
        System.out.println("브론즈 티어");
    }
}

class Silver extends Level {
    public Silver() {
        System.out.println("실버 티어");
    }
}

class Gold extends Level {
    public Gold() {
        System.out.println("골드 티어");
    }
}

class Platinum extends Level {
    public Platinum() {
        System.out.println("플래티넘 티어");
    }
}

 

 

(4) 객체 생성을 캡슐화할 수 있습니다.

  • 생성자에 private를 붙이고, 정적 팩토리 메소드 안에서 생성자를 호출함으로써 내부 상태를 외부에 드러낼 필요 없이 객체 생성 인터페이스를 단순화할 수 있습니다.
  • DTO에서 사용하는 from()을 한 가지 예시로 볼 수 있습니다.
  • StudentDto의 내부 구현을 모르고 있더라도 Student 객체를 정적 팩토리 메소드 from()의 파라미터로 넣어주면 해당 Student에 대한 Dto 객체를 얻을 수 있습니다.
public class StudentDto {
    private String name;
    private Integer age;
    
    private StudentDto(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    
    public static StudentDto from(Student student) {
        return new StudentDto(student.getName(), student.getAge());
    }
    
    // Getter, Setter 생략
}

 

 

 

객체 사이의 형변환이 필요하거나, 객체 생성이 반복되는 경우에는 생성자보다는 정적 팩토리 메소드를 활용해 보는 것이 좋을 것으로 보입니다. 

 

 

🔗 참고자료

정적 팩토리 메서드(Static Factory Method)는 왜 사용할까?

평소에 늘 JDK11 버전을 사용해서 모르고 있었는데, 최근 JDK16으로 버전을 바꿔보면서 새롭게 사용해 본 기능이 있어서 기록해 볼까 합니다. 추가로 평소에 궁금했었던 불변 객체에 대해서도 조금 찾아서 기록해보려고 합니다.

 

 

📖 불변 객체(Immutable Object)란?

  • 생성 후 그 상태를 바꿀 수 없는 객체를 의미합니다.
  • 불변객체는 읽을 수만 있는 Read-Only 메서드만을 제공합니다.
  • Java의 대표적인 불변객체로는 String이 있습니다.

 

# String Class Description

 

그럼 String은 생성된 후 값을 변경할 수 없다는건가? 그럼 아래 코드는 무엇인가? 값을 변경하는 것이 아닌가?

public class Main {
    public static void main(String[] args) {
        String stringEx = "Hello";
        System.out.println(stringEx); // Hello
        
        stringEx = "World";
        System.out.println(stringEx); // World
    }
}
  • stringEx라는 String 객체의 값을 Hello에서 World로 바꾼 것이 아닌가?라는 생각이 들 수 있습니다.
  • 이는 값을 변경한 것이 아니고 기존에는 String Constant Pool에 있는 "Hello"를 참조하고 있다가 "World"로 참조 방향이 변경된 것 입니다. 즉, stringEx의 값이 변경된 것이 아닙니다.
  • String 포스팅이 아니기 때문에 해당 링크를 참고해주시면 이해가 쉬울 거 같습니다.

 

 

 

📖 불변 객체를 만드는 방법

1. final 키워드를 이용하자

  • Java에서는 불변성을 확보하기 위해서 final이라는 키워드를 제공합니다.
  • Java의 기본 필드는 가변적이지만 final 키워드를 앞에 붙여주면 객체 생성시 값 초기화는 가능하지만, 생성된 이후에는 변경이 불가능합니다.

 

# final 키워드를 이용한 필드 불변성 확보 시 문제점

  • 하지만 final 키워드를 사용하더라도 해당 객체 내부의 상태를 변경시키지 못하는 것은 아닙니다.
  • 아래의 예시는 List에 final 키워드를 붙여서 불변성을 확보했습니다. 그리고 List에 값을 추가해 보는 예시입니다.
public class Ex {
    public static void main(String[] args) {
        WorkSpace workSpace = new WorkSpace("Test WorkSpace");
        workSpace.getWorkSpace().add(new Worker("윤씨", 27)); // 불변 필드의 내부 상태를 변경
        workSpace.getWorkSpace().add(new Worker("정씨", 29)); // 불변 필드의 내부 상태를 변경
        
        workSpace.getWorkers().forEach(worker -> {
            System.out.println(worker);
        });
        // [출력]
        // Worker[김씨, 26]
        // Worker[박씨, 30]
        // Worker[윤씨, 27]
        // Worker[정씨, 29]
    }
}

class WorkSpace {
    // 불변성을 확보하기 위해서 필드를 final로 선언
    private final List<Worker> workers;
    private final String workSpaceName;

    public WorkSpace(String workSpaceName) {
        this.workers = new ArrayList<>(List.of(new Worker("김씨", 26), new Worker("박씨", 30)));
        this.workSpaceName = workSpaceName;
    }

    // final 필드를 읽기만 하는 Read-Only 메서드 선언
    public List<Worker> getWorkers() {
        return this.workers;
    }
    
    public String getWorkSpaceName() {
        return this.workSpaceName;
    }
}

class Worker {
    private String name;
    private Integer age;

    public Worker(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Worker[" + name + ", " + age + "]";
    }
}
  • 위 코드를 보면 알 수 있듯이 WorkSpace Class 내부의 workSpace 필드를 불변성 확보를 위해서 final 키워드로 선언했습니다.
  • 그리고 해당 필드를 읽을 수 있는 Getter 메서드를 선언해줬습니다.
  • 하지만 외부(Ex Class)에서 final로 불변성을 확보한 필드 내부에 새로운 객체가 계속 추가돼도 문제가 없는 상태가 되었습니다.
  • Java에서는 위와 같이 참조에 의해서 값이 변경될 수 있는 점을 주의해야 합니다. 그래서 완벽한 불변 객체를 만들기 위해서는 "불변 클래스"로 만들어야 합니다.

 

 

2. 불변 클래스로 만들자

  • 불변 클래스가 되기 위해서는 Class를 final 키워드를 사용해서 선언합니다.
  • Class의 모든 필드를 private final로 선언합니다.
  • 객체를 생성할 수 있는 생성자 또는 정적 팩토리 메서드(of)를 선언합니다.
  • 위에서 봤던 List와 같이 참조에 의해서 변경될 가능성이 있는 필드는 방어적 복사를 이용하여 제공합니다.

 

# WorkSpace를 완벽한 불변 클래스로 만드는 리펙토링

// final 키워드로 class를 선언
final class WorkSpace {
    // 불변성을 확보하기 위해서 필드를 private final로 선언
    private final List<Worker> workers;
    private final String workSpaceName;

    // 생성자를 내부적으로 호출할 수 있도록 private로 선언
    private WorkSpace(String workSpaceName) {
        this.workers = new ArrayList<>(List.of(new Worker("김씨", 26), new Worker("박씨", 30)));
        this.workSpaceName = workSpaceName;
    }

    // 정적 팩토리 메서드를 선언
    public static WorkSpace of(String workSpaceName) {
        return new WorkSpace(workSpaceName);
    }

    // 방어적 복사를 이용하여 workers를 제공
    public List<Worker> getWorkers() {
        return Collections.unmodifiableList(this.workers);
    }

    public String getWorkSpaceName() {
        return this.workSpaceName;
    }
}
  • 첫 번째로 final 키워드로 class를 선언하여 불변 클래스로 만들었습니다.
  • 두 번째로 모든 필드는 private final로 선언해 줬습니다.
  • 세 번째로 내부 생성자를 private로 선언하여 정적 팩토리 메서드를 통해서만 객체를 생성할 수 있도록 했습니다.

    • 이렇게 설정한 이유는 아무런 생성자 없이 정적 팩토리 메서드만 생성한다면 기본적으로 비어있는 내부 생성자를 컴파일러에서 자동으로 만들어줍니다. 그렇기 때문에 클래스 외부에서 어디서든 객체를 만들고 접근할 수 있기 때문입니다.
  • 마지막으로 workers List가 필요한 경우가 있을 수 있기 때문에 불변성을 확보하기 위해서 방어적 복사를 사용하여 List를 제공했습니다.

 

 

 

📖 Java의 Record

  • JDK14에서 preview로 소개하고, JDK16 버전부터 정식으로 사용할 수 있게 되었습니다.
  • 불변 데이터 객체를 쉽게 생성할 수 있도록 도와줍니다.

출처: https://www.baeldung.com/java-15-new

 

1. Record의 특징

  • Record는 따로 적어주지 않아도 암묵적으로 final 클래스이기 때문에 상속이 불가능합니다.
  • 다른 클래스를 상속(extends) 받을 수는 없지만, 인터페이스를 구현(implements)은 할 수 있습니다.
  • 필드에 따로 private final 키워드를 붙여주지 않아도 됩니다.
  • 모든 필드를 파라미터로 갖는 생성자를 기본적으로 제공합니다.
  • 객체 생성 시 재정의 해줘야 하는 toString(), hashCode(), equals()를 기본적으로 제공합니다.

 

 

2. Class  vs Record (비교)

비교를 위해서 먼저 Record를 사용하지 않고, 불변 객체를 만드는 코드를 작성해 봅니다.

 

# 기존의 Class를 이용하여 불변 객체 Student를 생성

public final class Student {
    private final String studentId;
    private final String name;
    private final Integer age;
    
    public Student(String studentId, String name, Integer age) {
        this.studentId = studentId;
        this.name = name;
        this.age = age;
    }

    public String getStudentId() {
        return this.studentId;
    }

    public String getName() {
        return this.name;
    }

    public Integer getAge() {
        return this.age;
    }

    @Override
    public String toString() {
        return String.format("Student[studentId=%s, name=%s, age=%d]", studentId, name, age);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (!Objects.equals(studentId, student.studentId)) return false;
        if (!Objects.equals(name, student.name)) return false;
        return Objects.equals(age, student.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(studentId, name, age);
    }
}

 

# Record를 이용하여 불변객체 Student 생성

public record Student(String studentId, String name, Integer age) {
   // 끝
}
  • 이렇게만 봐도 엄청난 기능을 가지고 있다고 볼 수 있습니다. 값을 가져올 수 있는 getter()도 모두 기본적으로 제공됩니다. 메서드의 이름은 만약 studentId를 가져오고 싶다면 그냥 studentId()라는 메서드로 사용하면 getter와 동일한 기능을 합니다.
  • @Override 해서 재정의해줬던 toString(), hashCode(), equals()도 모두 기본적으로 제공하기 때문에 따로 정의해주지 않아도 됩니다.
  • 모든 필드를 파라미터로 갖는 생성자도 기본적으로 제공해 주기 때문에 따로 정의해주지 않아도 됩니다.

 

 

3. Record의 추가적인 기능

추가적인 기능을 기존의 class와 비교하면서 설명드리겠습니다.

 

 

(1) 생성자로 들어오는 파라미터 유효성 검사하기

20살이 넘는 age 값이 들어왔을 때는 객체가 생성되지 않고, 예외를 던지도록 코드를 설정해 봅니다.

// Class 사용

public final class Student {
    private final String studentId;
    private final String name;
    private final Integer age;

    public Student(String studentId, String name, Integer age) {
        if (age >= 20) {
            throw new IllegalArgumentException("20살 이상은 학생이 아닙니다!");
        }
        this.studentId = studentId;
        this.name = name;
        this.age = age;
    }
}

 

// Record 사용

public record Student(String studentId, String name, Integer age) {

    public Student {
        if (age >= 20) {
            throw new IllegalArgumentException("20살 이상은 학생이 아닙니다!");
        }
    }
}

위 방법을 이용하면 Null에 대한 처리도 충분히 할 수 있을 거 같습니다.

 

 

(2) Person interface 구현하기

// Person Interface

public interface Person {
    void walk();
    void eat();
}
// Class 사용

public final class Student implements Person {
    /* 코드 생략 */
    
    @Override
    public void walk() {
        System.out.println(this.name + "가 걷습니다.");
    }

    @Override
    public void eat() {
        System.out.println(this.name + "가 밥을 먹습니다.");
    }
}
// Record 사용

public record Student(String studentId, String name, Integer age) implements Person {
    /* 코드 생략 */
    
    @Override
    public void walk() {
        System.out.println(this.name + "가 걷습니다.");
    }

    @Override
    public void eat() {
        System.out.println(this.name + "가 밥을 먹습니다.");
    }

}

Record는 Interface는 기존의 Class처럼 구현할 수 있습니다. 하지만 Class를 상속(extends) 받는 것은 불가능합니다.

 

Record에서 extends를 하려고 할 때

 

 

 

 

 

사실 Lombok 라이브러리나 IDE에서 toString(), hashCode(), equals()를 자동으로 Generate해주는 기능도 보면서 정말 편하게 사용하기 위해서 여러 가지 기능을 만들고 있구나라는 생각을 했는데, Record를 보고 나서 발전은 정말 끝이 없다는 것을 느꼈습니다. 나중에는 어떤 기능들이 점점 더 추가될지 기대되면서도 무섭다는 생각을 했습니다.

 

궁금증이 들어서 코드를 쳐보다가 알게 된 점이 있어서 TreeSet에 대해서 기록해두고자 합니다.

 

기본적으로 Set은 중복을 허용하지 않는 자료구조입니다. 여기에 Tree가 붙으면서 이진 탐색 트리(binary search tree)의 형태로 Set이 구현된 자료구조가 됩니다. 즉, 데이터들이 정렬이 되어 저장된다는 것입니다.

 

정렬이 된다는 부분에서 Integer 타입이나 String 타입이 들어갔을 때는 아 정렬이 쉽게 되겠구나라고 생각을 했습니다. 근데 만약 Student 같은 커스텀 객체가 들어간다면 어떻게 될까라는 궁금점이 생겨서 코드를 작성해 봤습니다.

 

# Student 객체 생성

Student 객체는 학번(studentId)과 이름(name)만을 필드로 갖는 객체입니다.

public class Student {
    private String name;
    private int studentId;


   public Student(int studentId, String name) {
       this.studentId = studentId;
       this.name = name;
   }

    public int getStudentId() {
        return studentId;
    }

    public String getName() {
        return name;
    }
}

 

 

# TreeSet에 Student 객체 넣기

TreeSet을 만들어서 이 안에 Student 객체를 넣어볼 것입니다.

public class RunStudent {
    public static void main(String[] args) {
        TreeSet<Student> school = new TreeSet<>();
        school.add(new Student(1, "김씨"));
    }
}

1이라는 학번과 "김씨"라는 이름을 갖는 학생을 하나 넣어줬습니다. 컴파일 오류도 안 나고 한번 실행시켜 봅니다.

Exception in thread "main" java.lang.ClassCastException: class comparator_comparable.Student cannot be cast to class java.lang.Comparable (comparator_comparable.Student is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap') at java.base/java.util.TreeMap.compare(TreeMap.java:1291) at java.base/java.util.TreeMap.put(TreeMap.java:536) at java.base/java.util.TreeSet.add(TreeSet.java:255) at comparator_comparable.RunStudent.main(RunStudent.java:11)

ClassCastException이 발생합니다. 읽어보면 Student는 Comparable를 cast 하지 않았다는 어쩌고 쓰여있습니다. 이는 TreeSet은 데이터를 넣었을 때, 정렬을 해주는 자료구조이기 때문에 해당 객체의 정렬 기준이 필요합니다.

 

하지만 Student 클래스에는 학번과 이름이라는 2개의 필드가 존재하는데 Java에서 난 뭘로 정렬을 해야 할지 모르겠다는 예외를 발생시킨 겁니다. 그럼 Comparable 인터페이스를 사용해서 Student의 정렬 기준을 설정해 보겠습니다.

 

 

# Student implements Comparable

public class Student implements Comparable<Student> {
    private String name;
    private int studentId;

    ... 생략 ...
    
    @Override
    public int compareTo(Student std) {
       return this.studentId - std.studentId;
    }
}

Comparable 인터페이스를 implements 받으면 반드시 compareTo() 메서드를 오버라이드해서 재정의해줘야 합니다. 여기서 Student 클래스의 정렬 기준을 정의할 수 있습니다. 

 

compareTo() 메서드는 양수(1), 음수(-1), 0을 리턴값으로 사용할 수 있습니다. 위에서 작성한 compareTo()는 파라미터로 들어온 다른 Student 객체와 나 자신(this)을 비교합니다. 즉, 정렬의 기준이 studentId라는 것을 의미합니다.

 

나의 학번(this.studentId)과 다른 사람의 학번(std.studentId)를 뺀 값을 리턴합니다. 이 결괏값은 같으면 0, 내가 크면 양수, 쟤가 크면 음수의 리턴 값을 가집니다. 내가(this) 비교의 앞에 있기 때문에 위 코드는 학번을 기준으로 오름차순 정렬이 진행됩니다. 

 

그럼 내림차순 정렬을 하고 싶을 때는 어떻게 해야 할까?

@Override
public int compareTo(Student std) {
   return std.studentId - this.studentId;
}

이렇게 두 개를 바꿔서 리턴을 해주면 됩니다. Comparable의 compareTo()를 잘 이용해 주면 정렬 조건이 2개여도 쉽게 정렬을 할 수 있게 됩니다.

 

 

# TreeSet 다시 실행해 보기

이제 오류 없이 잘 돌아가는 것을 확인할 수 있습니다. 학생을 4명 정도 넣어서 정렬이 잘 되어서 나오는지 코드로 확인해 보겠습니다.

public class RunStudent {
    public static void main(String[] args) {
        TreeSet<Student> school = new TreeSet<>();
        school.add(new Student(1, "김씨"));
        school.add(new Student(2, "박씨"));
        school.add(new Student(3, "정씨"));
        school.add(new Student(4, "오씨"));

        for (Student student : school) {
            System.out.println("학번: " + student.getStudentId() + " 이름: " + student.getName());
        }
    }
}
<출력 결과>
학번: 1 이름: 김씨
학번: 2 이름: 박씨
학번: 3 이름: 정씨
학번: 4 이름: 오씨

의도한 대로 오름차순으로 정렬돼서 결과가 출력된 것을 확인할 수 있었습니다. 근데 한 가지 의문점은 여기서부터 시작됩니다. 만약 학번이 1인 학생이 한 명 더 추가될 때는 어떻게 될까? (같은 학번의 학생)

 

 

# 같은 학번을 가진 학생을 추가해 보기

public class RunStudent {
    public static void main(String[] args) {
        TreeSet<Student> school = new TreeSet<>();
        school.add(new Student(1, "김씨"));
        school.add(new Student(2, "박씨"));
        school.add(new Student(3, "정씨"));
        school.add(new Student(4, "오씨"));

        school.add(new Student(1, "이씨"));

        for (Student student : school) {
            System.out.println("학번: " + student.getStudentId() + " 이름: " + student.getName());
        }
    }
}

출력 결과를 보면 "이씨"는 출력되지 않았습니다. 즉 추가가 되지 않았다는 것을 볼 수 있습니다. 궁금해서 TreeSet의 contains() 메서드를 사용해서 어떤 결과가 나오는지 확인해 봤습니다.

school.add(new Student(1, "김씨"));
school.add(new Student(2, "박씨"));
school.add(new Student(3, "정씨"));
school.add(new Student(4, "오씨"));

Student lee = new Student(1, "이씨");

if (school.contains(lee)) {
    System.out.println(lee.getName() + "은 이미 학교에 존재합니다.");
} else {
    System.out.println(lee.getName() + "은 학교에 존재하지 않습니다.");
}

출력 결과는 "이씨은 이미 학교에 존재합니다."가 나왔습니다. school.contains()에 Integer나 String이 아닌 Student 객체를 넣어준 것인데, 존재 여부를 어떻게 판단할 수 있는 걸까 라는 의문이 들었습니다.

 

그래서 school에 존재하는 모든 학생 객체를 순회하면서 equals()를 사용하여 lee와 같은 객체가 있는지 확인해 봤습니다.

TreeSet<Student> school = new TreeSet<>();
school.add(new Student(1, "김씨"));
school.add(new Student(2, "박씨"));
school.add(new Student(3, "정씨"));
school.add(new Student(4, "오씨"));

Student lee = new Student(1, "이씨");

for (Student student : school) {
    if (student.equals(lee)) {
        System.out.println("lee와 같은 객체입니다.");
    }
}

결과는 아무것도 출력되지 않았습니다. 즉, "김씨", "박씨", "정씨", "오씨" 모두 "이씨"와는 다른 객체라는 것입니다. 근데 contains()에서 포함이 되어있다고 한다...?

 

궁금해서 Student에 다른 필드 값인 학년(grade)을 넣어서 확인해 봤습니다.

 

 

# Student 클래스에 grade 필드값 추가

public class Student implements Comparable<Student> {
    private String name;
    private int studentId;
    private int grade;


   public Student(int studentId, String name, int grade) {
       this.studentId = studentId;
       this.name = name;
       this.grade = grade;
   }

    ... 생략 ...

    public int getGrade() {
       return grade;
    }

    @Override
    public int compareTo(Student std) {
       return this.studentId - std.studentId;
    }
}

새롭게 추가해 주고, 다시 contains()를 사용해 봤습니다.

TreeSet<Student> school = new TreeSet<>();
school.add(new Student(1, "김씨", 1));
school.add(new Student(2, "박씨", 2));
school.add(new Student(3, "정씨", 3));
school.add(new Student(4, "오씨", 4));

Student lee = new Student(1, "이씨", 1);

System.out.println(school.contains(lee));

여전히 결괏값은 true가 나옵니다. lee가 school에 포함되어 있다는... school을 순회하면 모두 출력했을 때는 "이씨"가 존재하지 않습니다. 여기서 이제 "이씨"의 학번을 겹치지 않게 5로 바꾸고, 학년은 "김씨"와 겹치는 1로 유지한채 school에 넣어봤습니다.

TreeSet<Student> school = new TreeSet<>();
school.add(new Student(1, "김씨", 1));
school.add(new Student(2, "박씨", 2));
school.add(new Student(3, "정씨", 3));
school.add(new Student(4, "오씨", 4));

Student lee = new Student(5, "이씨", 1);
school.add(lee);

for (Student student : school) {
    System.out.println("학번: " + student.getStudentId() + " 이름: " + student.getName());
}
학번: 1 이름: 김씨
학번: 2 이름: 박씨
학번: 3 이름: 정씨
학번: 4 이름: 오씨
학번: 5 이름: 이씨

어..? grade는 겹치게 넣었는데, 이번에는 들어가네라는 의문에서 거의 확신이 섰습니다. 이진트리는 중복되는 숫자가 존재하지 않습니다. 즉, 우리가 Student를 만들 때 정렬 조건을 studentId로 지정했고 이것이 Student의 key 값이 되어 적용되고 있었습니다. 그래서 학번이 같으면 TreeSet에 넣어지지 않고, 학년이 같았을 때는 넣어졌던 것입니다.

 

이것에 확신을 얻기 위해서 Student의 정렬 조건을 학번(studentId)에서 학년(grade)으로 바꿔서 진행해 봤습니다.

@Override
public int compareTo(Student std) {
   return this.getGrade() - std.getGrade();
}
학번: 1 이름: 김씨
학번: 2 이름: 박씨
학번: 3 이름: 정씨
학번: 4 이름: 오씨

예상했던 대로 "김씨"와 학년이 같았던 "이씨"는 추가되지 않았습니다. 이렇게 TreeSet에 대해서... 좀 더 확실히 알 수 있는 계기가 되었던 거 같습니다.

 

자주 사용했던 자료구조는 아니었는데, 궁금해서 TreeSet을 사용한 출석부를 만들어봤습니다. 근데 그 과정에서 위와 같은 의문점이 들었고 TreeSet에 대해서 확실히 알게 되었던 과정이었습니다.. 👀

최근 프로그래머스 문제를 풀면서 문자열을 가공해서 사용해야하는 문제가 많이 있다고 생각했고, String과 한번 친해져보자는 생각으로 글을 작성하게 되었다. 적어도 이정도는 까먹지 말자라는 느낌으로... 시작해보자

 

예시로 들 문제는 아래와 같다.

https://school.programmers.co.kr/learn/courses/30/lessons/72410

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

프로그래머스에 2021 KAKAO BLIND RECRUITMENT의 레벨 1문제이다. String과 친해지기 정말 좋은 문제라고 생각된다.

 

문제를 짧게 설명해 보면 다음과 같다.

1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.

2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.

3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.

4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.

5단계 new_id가 빈 문자열이라면, new_id에 "a"를 대입합니다.

6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다. 만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.

7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.

문자열 new_id가 입력되는데, 해당 문자열을 위와 같이 7단계를 거쳐가며 가공한다. 최종 결과를 반환하면 되는 문제이다. 문제를 해결하면서 정규표현식으로도 풀 수 있고, 직접 문자열을 하나하나 확인하며 해결할 수도 있다. 여기서는 정규표현식을 주로 사용하며 문제를 해결해보자.

 

 

# 1단계 : 모든 대문자를 소문자로 변경

new_id = new_id.toLowerCase();

아주 간단한 String의 메소드인 toLowerCase()를 사용해주면 모든 대문자가 소문자로 변경된다. 그럼 반대로 소문자를 대문자로 바꾸기 위해서는 toUpperCase()를 사용해주면 된다. 

 

 

 

# 2단계 : 영문 소문자, 숫자, 마침표(.), 빼기(-), 밑줄(_)을 제외한 모든 특수문자를 제거

여기서부터 정규표현식(regex)이 사용된다. 그리고 우리는 String의 replaceAll() 메소드를 사용할 것이다. replace()도 있지 않냐라고 물어본다면, 두 메소드 다 원하는 문자열을 찾아서 어떤 문자열로 교체해주는 역할을 한다. 하지만 replaceAll()의 경우 target으로 정규표현식을 사용할 수 있고, replace는 사용하지 못한다는 특징이 존재한다. 이제 사용해보자.

 

영문 소문자, 숫자, 마침표(.), 빼기(-), 밑줄(_)은 다 어떻게 표현할 수 있을까?

영어 소문자 => [a-z]
숫자 => [0-9]
마침표(.) => [.]
밑줄(_) => [_]
빼기(-) => [-]

위 처럼 []을 이용해서 특수문자 또는 문자, 숫자의 범위로 표현할 수 있다. 이 부분을 replaceAll()의 첫 번째 인자로 넣어주면 해당 정규표현식에 포함되는 문자를 원하는 문자로 변경할 수 있다.

 

하지만 우리가 하고 싶은 것은 영어 소문자, 숫자, 마침표, 밑줄, 빼기가 아닌 다른 문자들을 변경시키고 싶은 것이다. 그렇기 때문에 정규표현식에서 부정(반대)의 표시는 앞에 ^을 붙이는 것이다. 예를 하나 들어보자.

영어 소문자가 아닌 것들 => [^a-z]
숫자가 아닌 것들 => [^0-9]
마침표(.)가 아닌 것들 => [^.]
밑줄(_)이 아닌 것들 => [^_]
빼기(-)가 아닌 것들 => [^-]

이제 거의 완성이 되어간다. 우리는 위에 있는 모든 조건을 포함할 수 있는 정규표현식을 만들어야 한다. 그럼 이제 2단계를 완료해보자.

new_id = new_id.replaceAll("[^-_.a-z0-9]", "");

이렇게 작성해주면 빼기(-), 밑줄(_), 마침표(.), 영어 소문자, 숫자를 제외한 다른 문자는 모두 제외된다. 하지만 여기서 주의해야할 점이 있다. 빼기(-)는 a-z처럼 문자의 범위에 사용되기 때문에 아래와 같이 작성하면 빼기(-)는 제거되지 않을 거다.

new_id = new_id.replaceAll("[^.-_a-z0-9]", "");

뭐가 다른건지 찾아보면 [-_.]대신 [.-_]이 들어갔다. 이게 뭔 차이냐라고 물어보신다면 빼기(-)가 가운데 들어가는 순간 문자의 범위를 나타내게 된다. 즉, [.-_]의 의미는 문자(.)부터 문자(_)까지 범위에 있는 모든 문자를 표현하는 정규표현식이 되는 것이다.

 

아스키 코드표를 보면 마침표(.)는 46번이고, 밑줄(_)은 95번이다. 그리고 빼기(-)는 45번이기 때문에 [.-_]에는 빼기(-)가 포함되지 않는다. 그래서 아래 코드와 같이 작성하면 테스트에 통과하지 못할 것이다. 빼기(-) 기호를 정규표현식에서 사용할 때는 신경써서 사용해야 한다.

 

 

 

# 3단계 : 마침표(.)가 2개이상 반복되는 부분은 모두 마침표(.) 하나로 변경

여기서도 정규 표현식이 사용된다. 마침표가 여러번 반복되는지는 어떻게 알 수 있을까? 이때 사용하는 것이 {}이다. 중괄호는 정규표현식에서 해당 문자의 빈도수 또는 범위를 나타낼 수 있다. 이렇게 말로만 하면 이해하기 어려우니 코드로 살펴보자.

.이 2번 이상 반복되는 것을 찾고 싶다 => [.]{2,}
.이 3번 이상 반복되는 것을 찾고 싶다 => [.]{3,}
.이 2번만 반복되는 것을 찾고 싶다 => [.]{2}
.이 2번 이상 6번 이하로 반복되는 것을 찾고 싶다 => [.]{2,6}

이렇게 범위를 지정해서 찾을 수 있다. 여기서 주의해야할 점은 {}안에 범위 지정할 때, {2, 6} 이런식으로 공백이 있으면 PatternSyntaxException 예외가 발생하기 때문에 주의하길 바란다.

 

그럼 이제 3단계도 마무리를 해보자.

new_id = new_id.replaceAll("[.]{2,}", ".");

이제 마침표(.)가 2개 이상 반복되는 모든 부분을 찾아서 마침표(.) 하나로 변경해줄 것이다.

 

 

 

# 4단계 : 마침표(.)가 처음이나 끝에 있으면 제거한다.

여기서는 여러가지 방법을 생각할 수도 있다. 예를 들어 String의 startsWith()나 endsWith()를 사용하는 방법이다. 

if (new_id.startsWith(".")) {
	// .으로 시작하면 첫 번째 .을 제거하는 로직 실행
}

if (new_id.endsWith(".")) {
    // .으로 끝나면 마지막 .을 제거하는 로직 실행
}

하지만 이왕하는거 정규 표현식으로 해결을 해보자. 처음 시작 문자와 마지막 끝 문자를 확인하는 방법이 있을까? 물론 정규 표현식에 존재한다. 코드로 한번 살펴보자.

문자열의 첫 번째 문자 => ^.
문자열의 마지막 문자 => .$

이렇게 보면 조금 어려울 수 있기 때문에 간단한 예시를 하나 들어보자.

String str = "아녕하세용";

// 문자열의 첫 번째 문자를 "안"로 변경하고 싶다.
str = str.replaceAll("^.", "안"); // 안녕하세용

// 문자열의 마지막 문자를 "요"로 변경하고 싶다.
str = str.replaceAll(".$", "요"); // 안녕하세요

이제 이해가 됐을 거라고 생각한다. 하지만 우리가 해야할 것은 첫 번째, 마지막 문자를 무조건 바꾸는게 아니고 어떠한 조건(. 인가)에 대해서 참이면 바꿔 줘야 한다. 이때 사용할 수 있는 정규 표현식도 존재한다. 

// 문자열의 첫 문자가 "아"면 "안"으로 바꿔줘
str = str.replaceAll("^[아]", "안");

// 문자열의 첫 문자가 "."이라면 ""으로 바꿔줘
str = str.replaceAll("^[.]", "");

// 문자열의 마지막 문자가 "용"이면 "요"로 바꿔줘
str = str.replaceAll("[용]$", "요");

// 문자열의 마지막 문자가 "."이면 ""로 바꿔줘
str = str.replaceAll("[.]$", "");

그럼 이제 우리는 4단계도 마무리할 수 있다.

new_id = new_id.replaceAll("^[.]", ""); // 첫 문자 . 제거
new_id = new_id.replaceAll("[.]$", ""); // 마지막 문자 . 제거

근데 위 코드를 한 줄로 표현할 수는 없을까? 이때 정규표현식에서는 or 표현 (|)으로 여러 조건을 한 번에 해결할 수 있다. 한 줄로 표현해보자.

new_id = new_id.replaceAll("^[.]|[.]$", "");

이제 앞 뒤에 마침표(.)가 있으면 모두 제거될 것이다.

 

 

 

# 5단계 : 비어있는 문자열("")이라면 "a"를 추가

이 부분은 매우 싶고, 2가지 방법이 있습니다.

// 첫 번째 방법
if (new_id.length() == 0) {
    new_id = "a";
}

// 두 번째 방법
if (new_id.isEmpty()) {
    new_id = "a";
}

저는 가독성도 좋고, 이왕이면 String의 메소드를 활용하기 위해서 isEmpty() 메소드를 사용했습니다. isEmpty()는 비어있는 문자열이면 true를 반환하고, 아니면 false를 반환합니다.

 

 

 

# 6단계 (1) : 문자열의 길이가 16자 이상이 넘어가면 앞에부터 15번째 문자열만 남기고 뒤쪽은 모두 제거합니다.

문자열의 길이가 16자 이상인지 확인하고, 만약 넘어간다면 String의 index 0부터 14까지만 살려되면 되는 작업입니다. 이때는 String을 원하는 범위만큼 자를 수 있는 substring()이라는 메소드를 사용해서 해결했습니다.

if (new_id.length() >= 16) {
    new_id = new_id.substring(0, 15); // index가 0이상 15미만 (0 ~ 14) 만큼 잘라냄
}

6단계의 다른 조건으로 이제 넘어가 봅니다.

 

 

 

# 6단계 (2) : 6 -1 단계에서 제거한 문자열의 마지막이 마침표(.)라면 그 마침표는 제거합니다.

이 부분은 이미 해본 것이기 때문에 넘어가겠습니다.

 

 

 

# 7단계 : 문자열의 길이가 2 이하라면 문자열의 마지막 문자를 뒤에 이어붙입니다. 이때 길이가 3이되면 종료

이어 붙일 때는 StringBuilder의 append()를 사용해도 되고, 그냥 String에 더해줘도 됩니다. 반복횟수는 많아도 3번이기 때문에 그냥 String에 더하는 방법을 사용하겠습니다.

while (new_id.length() <= 2) {
    new_id += new_id.charAt(new_id.length() - 1);
}

문자열의 길이가 2이하면 new_id에 마지막 문자를 더해주는 로직을 작성하고, 모든 단계가 마무리 되었습니다.

간단한 문법의 활용이긴 하지만, 사용할 때마다 헷갈려서 기록해두려고 한다. 

 

1. Integer[] 배열을 List<Integer>로 변환

Integer[] 배열을 만들어서 List<Integer>로 만드는 방법은 매우 간단하다.

// 배열
Integer[] array1 = new Integer[]{1, 2, 3, 4, 5, 6, 7};

// 변환한 리스트
List<Integer> list1 = Arrays.asList(array1);

이는 Integer는 오브젝트이기 때문에 별다른 중간 작업이 필요없이 Arrays.asList()로만으로 변환이 가능하다.

 

 

2. int[] 배열을 List<Integer>로 변환

위에 Integer와는 다르게 int는 오브젝트가 아닌 기본형 타입이기 때문에 위와 같이 코드를 작성하면 어떻게 될까?

java: incompatible types: inference variable T has incompatible bounds
    equality constraints: java.lang.Integer
    lower bounds: int[]

위와 같은 컴파일 에러가 발생한다. List는 제너릭을 사용해서 Integer라는 Wrapper 객체를 받아야 하는데, int[]는 기본형 타입이기 때문이다. 그럼 어떻게 해줘야할까? 자바의 Stream을 사용하면 int[] 배열의 모든 인자를 Wrapper 클래스로 바꿔준 후 List로 변환할 수가 있다. 한번 코드로 작성해보자.

// 배열
int[] array1 = new int[]{1, 2, 3, 4, 5, 6, 7};

// 변환한 리스트
List<Integer> list1 = Arrays.stream(array1).boxed().collect(Collectors.toList());

위 코드를 해석해보면 먼저, array1 배열을 stream으로 만든 후에 boxed() 메소드를 적용하면 int 기본형 타입을 모두 Wrapper 클래스로 변환시켜 줍니다. 그 후 collect()를 사용해서 List로 변환시켜 줍니다. 

 

 

3. List<Integer> 리스트를 int[] 배열로 변환

위에서 기본형 타입인 int 배열을 Stream을 사용해서 List<Integer>로 변환해봤다. 이제 반대로는 어떻게 변환할 수 있을까? 반대로 변환할 때도 Stream을 써볼 것이다.

// 리스트
List<Integer> list1 = List.of(1, 2, 3, 4, 5, 6, 7);

// 변환한 배열
int[] array1 = exList.stream().mapToInt(Integer::intValue).toArray();

새로운 메소드가 나왔다. mapToInt() 이다. 해당 메소드를 위와 같이 mapToInte(Integer::intValue)로 사용하면 리스트에 있는 Wrapper 타입의 인자들을 하나씩 돌면서 기본형 타입인 int로 변환해줍니다. 그리고 마무리로 toArray() 메소드를 통해서 배열로 변환할 수 있습니다.

 

생각보다 int[]를 List<Integer>로 변환하는 경우가 좀 있었던 거 같아서 이렇게 기록해두려고 했다. 이제는 안 까먹어야지. 😄

📘 열거형 (enums)

enum은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용합니다. 즉 서로 관련이 있는 상수들을 모아 상수들을 대표할 수 있는 이름으로 타입을 정하는 것을 말합니다. 자바의 열거형은 열거형이 갖는 값뿐만 아니라 타입도 관리하기 때문에 논리적인 오류를 줄일 수 있습니다. 예를 들어 열거형을 모를 때 사용했었던 아래와 같은 int enum pattern이 있다고 생각해봅시다.

public class EnumEx {
    static final int APPLE = 0;
    static final int BANANA = 1;
    static final int ORANGE = 2;

    static final int MONKEY = 0;
    static final int BIRD = 1;
    static final int ELEPHANT = 2;

    public static void main(String[] args) {
        if (EnumEx.APPLE == EnumEx.MONKEY) {
            System.out.println("APPLE과 MONKEY는 같습니다.");
        }
    }
}

위에서 APPLE이라는 상수의 값도 0이고, MONKEY라는 상수의 값도 0입니다. 하지만 이는 값은 같지만 분명 다른 타입을 가지고 있는 상수입니다. APPLE은 과일이라는 타입의 상수이고, MONKEY의 경우에는 동물이라는 타입의 상수입니다. 위 코드의 출력 결과는 다음과 같습니다.

APPLE과 MONKEY는 같습니다.

이렇게 나오는 이유는 둘이 가지고 있는 값이 동일하기 때문에 조건문에서 참의 값을 갖습니다. 하지만 논리적으로는 “APPLE은 MONKEY이다 (사과는 원숭이다)”라는 말은 false가 나와야 맞습니다. 이것을 열거형을 사용해서 표현하면 논리적인 오류를 해결할 수 있습니다.

public class EnumEx {
    public enum Fruit {
        APPLE, BANANA, ORANGE
    }
    public enum Animal {
        MONKEY, BIRD, ELEPHANT
    }

    public static void main(String[] args) {
        if (EnumEx.Fruit.APPLE == EnumEx.Animal.MONKEY) {
            System.out.println("같습니다.");
        }
    }
}

위 처럼 enum을 이용해서 Fruit 타입과 Animal 타입을 나누어 주고, 실행을 해보면 다음과 같은 결과가 나옵니다.

java: incomparable types: EnumEx.Fruit and EnumEx.Animal

Fruit과 Animal은 비교할 수 없는 타입이라는 컴파일 에러가 발생합니다. 이렇게 enum을 사용하면 논리적인 오류를 막을 수 있습니다. enum의 사용 방법에 대해서 알아봅시다.

 

 

 

📖 enum 선언하기

1. 별도의 파일로 선언

// Job.java
public enum Job {
    STUDENT, TEACHER, DEVELOPER
}
// Person.java
public class Person {
    String name;
    int age;
    Job job;

    public static void main(String[] args) {
        Person person = new Person();
        person.name = "홍길동";
        person.age = 25;
        person.job = Job.STUDENT;

        System.out.println("이름은: " + person.name);
        System.out.println("나이는: " + person.age);
        System.out.println("직업은: " + person.job);
    }
}
이름은 홍길동
나이는 25
직업은 STUDENT

열거형을 별도의 파일에서 클래스 처럼 선언할 수 있다. 사용하는 것도 보면 Job이라는 열거형 타입을 선언하고, 값을 넣어서 사용합니다.

 

 

2. 클래스 내부에서 선언

public class Person {
    String name;
    int age;
    Job job;

    public enum Job {
        STUDENT, TEACHER, DEVELOPER
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.name = "홍길동";
        person.age = 25;
        person.job = Job.STUDENT;

        System.out.println("이름은: " + person.name);
        System.out.println("나이는: " + person.age);
        System.out.println("직업은: " + person.job);
    }
}

 

 

3. 클래스 외부에서 선언

public class Person {
    String name;
    int age;
    Job job;

    public static void main(String[] args) {
        Person person = new Person();
        person.name = "홍길동";
        person.age = 25;
        person.job = Job.STUDENT;

        System.out.println("이름은: " + person.name);
        System.out.println("나이는: " + person.age);
        System.out.println("직업은: " + person.job);
    }
}

enum Job {
    STUDENT, TEACHER, DEVELOPER
}

위에 3가지 선언 방법이 있고, 출력되는 결과는 모두 동일한 것을 확인할 수 있습니다.

열거형의 특징은 다음과 같습니다.

  • 열거형으로 선언된 순서대로 0부터 인덱스 값을 가지게됩니다.
    • STUDENT는 0, TEACHER는 1, DEVELOPER는 2의 인덱스를 가지게 됩니다.


  • enum에 선언된 상수들은 모두 대문자로 작성해야 합니다.

  • 마지막 열거형 변수를 선언하고 세미콜론(;)을 찍지 않습니다.
    • 하지만 상수와 연관된 문자를 지정하는 경우에는 세미콜론(;)을 찍습니다.

 

 

 

📖 enum 메소드

대표적으로 사용되는 values(), ordinal(), valueOf()에 대해서 알아보겠습니다.

 

1. values()

열거된 모든 상수를 배열에 담아서 순서대로 리턴합니다.

public class Main {
    public static void main(String[] args) {
        for (Fruit fruit : Fruit.values()) {
            System.out.print(fruit + " ");
        }
    }
}

enum Fruit {
    PINEAPPLE, APPLE, WATERMELON, ORANGE
}
출력
PINEAPPLE APPLE WATERMELON ORANGE

 

 

2.  ordinal()

enum에 열겨된 순서를 정수형으로 반환

public class Main {
    public static void main(String[] args) {
        for (Fruit fruit : Fruit.values()) {
            System.out.println(fruit + "은 " + fruit.ordinal());
        }
    }
}

enum Fruit {
    PINEAPPLE, APPLE, WATERMELON, ORANGE
}

 

출력
PINEAPPLE은 0
APPLE은 1
WATERMELON은 2
ORANGE은 3

 

 

3. valueOf()

매개변수로 입력되는 문자열과 동일한 이름을 갖는 원소를 반환

public class Main {
    public static void main(String[] args) {
        Fruit apple = Fruit.valueOf("APPLE");
        System.out.println(apple);
    }
}

enum Fruit {
    PINEAPPLE, APPLE, WATERMELON, ORANGE
}

 

출력
APPLE

일치하지 않는 경우에는 IllegalArgumentException을 발생

public class Main {
    public static void main(String[] args) {
        Fruit apple = Fruit.valueOf("BANANA");
        System.out.println(apple);
    }
}

enum Fruit {
    PINEAPPLE, APPLE, WATERMELON, ORANGE
}
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Fruit.BANANA
  at java.base/java.lang.Enum.valueOf(Enum.java:240)
  at Fruit.valueOf(Main.java:10)
  at Main.main(Main.java:5)

 

 

 

📖 열거형 상수를 다른 값과 연결하기

위에서 열거형을 선언했을 때는 세미콜론(;)을 붙이지 않았습니다. 하지만 열거형 상수를 다른 값과 연결하는 경우에는 마지막 열거형 원소 뒤에 세미콜론(;)을 붙여야 한다.

public class Main {
    public static void main(String[] args) {
        for (Fruit fruit : Fruit.values()) {
            System.out.println(fruit + "의 한국어는 " + fruit.getName());
        }
    }
}

enum Fruit {
    PINEAPPLE("파인애플"),
    APPLE("사과"),
    WATERMELON("수박"),
    ORANGE("오렌지");

    private final String name;
    
    Fruit(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
출력
PINEAPPLE의 한국어는 파인애플
APPLE의 한국어는 사과
WATERMELON의 한국어는 수박
ORANGE의 한국어는 오렌지

 

위에서 열거형 원소 옆에 ()를 사용해서 연결되는 값을 입력할 수 있습니다. 이때 생성자를 사용해서 연결된 값을 넣어줄 수 있습니다.

여기서 알아둬야 하는 것은 enum 열거형 생성자의 제어자가 private으로 기본 설정되어 있다는 것입니다. 그렇기 때문에 enum 밖에서 생성자는 사용할 수 없습니다.

enum Fruit {
    PINEAPPLE("파인애플"),
    APPLE("사과"),
    WATERMELON("수박"),
    ORANGE("오렌지");

    private final String name;
    
    Fruit(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Fruit fruit = new Fruit("수박");  // 컴파일 에러

그리고 enum의 원소는 여러 개의 값과 연결될 수도 있습니다.

과일의 한국어 이름과 색깔을 연결된 값으로 가지고 있는 Fruit 열거형에 대해서 예를 들어보겠습니다.

public class Main {
    public static void main(String[] args) {
        for (Fruit fruit : Fruit.values()) {
            System.out.println(fruit + "의 한국어는 " + fruit.getName() + "이고, 색깔은 " + fruit.getColor() + "이다.");
        }
    }
}

enum Fruit {
    PINEAPPLE("파인애플", "노란색"),
    APPLE("사과", "빨간색"),
    WATERMELON("수박", "초록색"),
    ORANGE("오렌지", "주황색");

    private final String name;
    private final String color;
    Fruit(String name, String color) {
        this.name = name;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public String getColor() {
        return color;
    }
}
PINEAPPLE의 한국어는 파인애플이고, 색깔은 노란색이다.
APPLE의 한국어는 사과이고, 색깔은 빨간색이다.
WATERMELON의 한국어는 수박이고, 색깔은 초록색이다.
ORANGE의 한국어는 오렌지이고, 색깔은 주황색이다.

위 처럼 생성자에 하나의 매개변수가 더 들어가고, color 값을 얻어올 수 있는 getColor()라는 새로운 메소드를 추가해줬습니다. 이렇게 enum의 원소는 여러개의 값과 연결되어 사용될 수도 있습니다.

+ Recent posts